diff --git a/src/cancellationToken/cancellationToken.ts b/src/cancellationToken/cancellationToken.ts index aaa19750b8710..429ea193d2922 100644 --- a/src/cancellationToken/cancellationToken.ts +++ b/src/cancellationToken/cancellationToken.ts @@ -1,13 +1,10 @@ /// - import fs = require("fs"); - interface ServerCancellationToken { isCancellationRequested(): boolean; setRequest(requestId: number): void; resetRequest(requestId: number): void; } - function pipeExists(name: string): boolean { // Unlike statSync, existsSync doesn't throw an exception if the target doesn't exist. // A comment in the node code suggests they're stuck with that decision for back compat @@ -18,7 +15,6 @@ function pipeExists(name: string): boolean { // implementation returned false from its catch block. return fs.existsSync(name); } - function createCancellationToken(args: string[]): ServerCancellationToken { let cancellationPipeName: string | undefined; for (let i = 0; i < args.length - 1; i++) { @@ -62,7 +58,7 @@ function createCancellationToken(args: string[]): ServerCancellationToken { } else { return { - isCancellationRequested: () => pipeExists(cancellationPipeName!), // TODO: GH#18217 + isCancellationRequested: () => pipeExists(cancellationPipeName!), setRequest: (_requestId: number): void => void 0, resetRequest: (_requestId: number): void => void 0 }; diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 54e7c3cdbd439..c7b15a9163728 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -1,4420 +1,4006 @@ - +import { __String, FlowLabel, ModuleDeclaration, Node, createMap, getNodeId, SyntaxKind, isEnumConst, EnumDeclaration, hasModifier, ModifierFlags, ExportDeclaration, forEachChild, Debug, Identifier, ExportSpecifier, isBlock, isModuleBlock, isSourceFile, nodeHasName, FlowNode, SourceFile, CompilerOptions, perfLogger, ScriptTarget, JSDocTypedefTag, JSDocCallbackTag, JSDocEnumTag, NodeFlags, SymbolFlags, UnderscoreEscapedMap, FlowFlags, TransformFlags, DiagnosticMessage, DiagnosticWithLocation, createDiagnosticForNodeInSourceFile, getSourceFileOfNode, getEmitScriptTarget, createUnderscoreEscapedMap, objectAllocator, getStrictOptionValue, Declaration, appendIfUnique, createSymbolTable, isAssignmentDeclaration, isEffectiveModuleDeclaration, ExportAssignment, InternalSymbolName, getNameOfDeclaration, isAmbientModule, getTextOfIdentifierOrLiteral, StringLiteral, isGlobalScopeAugmentation, isStringOrNumericLiteralLike, escapeLeadingUnderscores, isSignedNumericLiteral, tokenToString, isWellKnownSymbolSyntactically, getPropertyNameForKnownSymbolName, idText, PropertyAccessExpression, isPrivateIdentifier, getContainingClass, getSymbolNameForPrivateIdentifier, isPropertyNameLiteral, getEscapedTextOfIdentifierOrLiteral, getAssignmentDeclarationKind, BinaryExpression, AssignmentDeclarationKind, isJSDocConstructSignature, JSDocFunctionType, ParameterDeclaration, isNamedDeclaration, declarationNameToString, unescapeLeadingUnderscores, SymbolTable, hasDynamicName, Diagnostics, length, DiagnosticRelatedInformation, isTypeAliasDeclaration, nodeIsMissing, forEach, addRelatedInfo, getCombinedModifierFlags, isJSDocTypeAlias, isInJSFile, FunctionLikeDeclaration, getImmediatelyInvokedFunctionExpression, FunctionExpression, ArrowFunction, MethodDeclaration, nodeIsPresent, ConstructorDeclaration, NodeArray, WhileStatement, DoStatement, ForStatement, ForInOrOfStatement, IfStatement, ReturnStatement, ThrowStatement, BreakOrContinueStatement, TryStatement, SwitchStatement, CaseBlock, CaseClause, ExpressionStatement, LabeledStatement, PrefixUnaryExpression, PostfixUnaryExpression, DeleteExpression, ConditionalExpression, VariableDeclaration, AccessExpression, CallExpression, Block, Expression, ParenthesizedExpression, TypeOfExpression, isPropertyAccessExpression, isNonNullExpression, isParenthesizedExpression, isElementAccessExpression, isOptionalChain, isTypeOfExpression, isStringLiteralLike, FlowReduceLabel, contains, isExpressionOfOptionalChainRoot, isNullishCoalesce, isPrefixUnaryExpression, isOutermostOptionalChain, Statement, concatenate, isDottedName, unusedLabelIsError, ArrayLiteralExpression, SpreadElement, ObjectLiteralExpression, isAssignmentOperator, isAssignmentTarget, ElementAccessExpression, isBinaryExpression, ArrayBindingElement, isOmittedExpression, isBindingPattern, isForInOrOfStatement, JSDocClassTag, getHostSignatureFromJSDoc, OptionalChain, skipParentheses, isIdentifier, isPushOrUnshiftIdentifier, isObjectLiteralOrClassExpressionMethod, PropertyDeclaration, isFunctionLike, isExternalModule, tryCast, isExportDeclaration, isExportAssignment, isModuleAugmentationExternal, Pattern, hasZeroOrOneAsteriskCharacter, tryParsePattern, append, PatternAmbientModule, SignatureDeclaration, JSDocSignature, getErrorSpanForNode, createFileDiagnostic, JsxAttributes, JsxAttribute, isExternalOrCommonJsModule, getJSDocHost, findAncestor, getEnclosingBlockScopeContainer, isJSDocEnumTag, isPropertyAccessEntityNameExpression, getAssignmentDeclarationPropertyAccessKind, isIdentifierName, PrivateIdentifier, isLeftHandSideExpression, CatchClause, FunctionDeclaration, NumericLiteral, TokenFlags, WithStatement, isDeclarationStatement, isVariableStatement, getSpanOfTokenAtPosition, getTokenPosOfNode, TextRange, DiagnosticCategory, hasJSDocNodes, isPrologueDirective, getSourceTextOfNodeFromSourceFile, isExpression, isSpecialPropertyDeclaration, isModuleExportsAccessExpression, BindableStaticPropertyAssignmentExpression, BindablePropertyAssignmentExpression, TypeParameterDeclaration, BindingElement, PropertySignature, isObjectLiteralMethod, TypeLiteralNode, MappedTypeNode, JSDocTypeLiteral, BindableObjectDefinePropertyCall, ClassLikeDeclaration, NamespaceExportDeclaration, ImportClause, ModuleBlock, JSDocParameterTag, JSDocPropertyLikeTag, isJsonSourceFile, removeFileExtension, exportAssignmentIsAlias, isNamespaceExport, isClassExpression, getRightMostAssignedExpression, isEmptyObjectLiteral, LiteralLikeElementAccessExpression, getThisContainer, isBindableStaticAccessExpression, isPrototypeAccess, DynamicNamedDeclaration, EntityNameExpression, BindableStaticAccessExpression, isFunctionSymbol, cast, BindableStaticNameExpression, isFunctionLikeDeclaration, getAssignedExpandoInitializer, isCallExpression, isBindableObjectDefinePropertyCall, some, BindableAccessExpression, isVariableDeclaration, getExpandoInitializer, getElementOrPropertyAccessName, getNameOrArgument, isRequireCall, symbolName, isBlockOrCatchScoped, isParameterDeclaration, isParameterPropertyDeclaration, isAsyncFunction, ConditionalTypeNode, isConditionalTypeNode, isJSDocTemplateTag, find, JSDoc, isStatementButNotDeclaration, unreachableCodeIsError, getCombinedNodeFlags, isStatement, sliceAfter, getRangesWhere, isFunctionDeclaration, isEnumDeclaration, isExportsIdentifier, isAssignmentExpression, Symbol, NewExpression, VariableDeclarationList, VariableStatement, ClassDeclaration, ClassExpression, HeritageClause, ExpressionWithTypeArguments, AccessorDeclaration, ImportEqualsDeclaration, skipOuterExpressions, isSuperOrSuperProperty, isSuperProperty, isThisIdentifier, isComputedPropertyName, hasStaticModifier, getModifierFlags, isIterationStatement, isExternalModuleImportEqualsDeclaration, NoSubstitutionTemplateLiteral, TemplateHead, TemplateMiddle, TemplateTail, hasInvalidEscape, TaggedTemplateExpression, ForOfStatement, PropertyName } from "./ts"; +import { mark, measure } from "./ts.performance"; +import * as ts from "./ts"; /* @internal */ -namespace ts { - export const enum ModuleInstanceState { - NonInstantiated = 0, - Instantiated = 1, - ConstEnumOnly = 2 - } - - interface ActiveLabel { - next: ActiveLabel | undefined; - name: __String; - breakTarget: FlowLabel; - continueTarget: FlowLabel | undefined; - referenced: boolean; - } - - export function getModuleInstanceState(node: ModuleDeclaration, visited?: Map): ModuleInstanceState { - if (node.body && !node.body.parent) { - // getModuleInstanceStateForAliasTarget needs to walk up the parent chain, so parent pointers must be set on this tree already - setParentPointers(node, node.body); - } - return node.body ? getModuleInstanceStateCached(node.body, visited) : ModuleInstanceState.Instantiated; - } - - function getModuleInstanceStateCached(node: Node, visited = createMap()) { - const nodeId = "" + getNodeId(node); - if (visited.has(nodeId)) { - return visited.get(nodeId) || ModuleInstanceState.NonInstantiated; - } - visited.set(nodeId, undefined); - const result = getModuleInstanceStateWorker(node, visited); - visited.set(nodeId, result); - return result; +export const enum ModuleInstanceState { + NonInstantiated = 0, + Instantiated = 1, + ConstEnumOnly = 2 +} +/* @internal */ +interface ActiveLabel { + next: ActiveLabel | undefined; + name: __String; + breakTarget: FlowLabel; + continueTarget: FlowLabel | undefined; + referenced: boolean; +} +/* @internal */ +export function getModuleInstanceState(node: ModuleDeclaration, visited?: ts.Map): ModuleInstanceState { + if (node.body && !node.body.parent) { + // getModuleInstanceStateForAliasTarget needs to walk up the parent chain, so parent pointers must be set on this tree already + setParentPointers(node, node.body); } - - function getModuleInstanceStateWorker(node: Node, visited: Map): ModuleInstanceState { - // A module is uninstantiated if it contains only - switch (node.kind) { - // 1. interface declarations, type alias declarations - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.TypeAliasDeclaration: + return node.body ? getModuleInstanceStateCached(node.body, visited) : ModuleInstanceState.Instantiated; +} +/* @internal */ +function getModuleInstanceStateCached(node: Node, visited = createMap()) { + const nodeId = "" + getNodeId(node); + if (visited.has(nodeId)) { + return visited.get(nodeId) || ModuleInstanceState.NonInstantiated; + } + visited.set(nodeId, undefined); + const result = getModuleInstanceStateWorker(node, visited); + visited.set(nodeId, result); + return result; +} +/* @internal */ +function getModuleInstanceStateWorker(node: Node, visited: ts.Map): ModuleInstanceState { + // A module is uninstantiated if it contains only + switch (node.kind) { + // 1. interface declarations, type alias declarations + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.TypeAliasDeclaration: + return ModuleInstanceState.NonInstantiated; + // 2. const enum declarations + case SyntaxKind.EnumDeclaration: + if (isEnumConst((node as EnumDeclaration))) { + return ModuleInstanceState.ConstEnumOnly; + } + break; + // 3. non-exported import declarations + case SyntaxKind.ImportDeclaration: + case SyntaxKind.ImportEqualsDeclaration: + if (!(hasModifier(node, ModifierFlags.Export))) { return ModuleInstanceState.NonInstantiated; - // 2. const enum declarations - case SyntaxKind.EnumDeclaration: - if (isEnumConst(node as EnumDeclaration)) { - return ModuleInstanceState.ConstEnumOnly; - } - break; - // 3. non-exported import declarations - case SyntaxKind.ImportDeclaration: - case SyntaxKind.ImportEqualsDeclaration: - if (!(hasModifier(node, ModifierFlags.Export))) { - return ModuleInstanceState.NonInstantiated; - } - break; - // 4. Export alias declarations pointing at only uninstantiated modules or things uninstantiated modules contain - case SyntaxKind.ExportDeclaration: - const exportDeclaration = node as ExportDeclaration; - if (!exportDeclaration.moduleSpecifier && exportDeclaration.exportClause && exportDeclaration.exportClause.kind === SyntaxKind.NamedExports) { - let state = ModuleInstanceState.NonInstantiated; - for (const specifier of exportDeclaration.exportClause.elements) { - const specifierState = getModuleInstanceStateForAliasTarget(specifier, visited); - if (specifierState > state) { - state = specifierState; - } - if (state === ModuleInstanceState.Instantiated) { - return state; - } - } - return state; - } - break; - // 5. other uninstantiated module declarations. - case SyntaxKind.ModuleBlock: { + } + break; + // 4. Export alias declarations pointing at only uninstantiated modules or things uninstantiated modules contain + case SyntaxKind.ExportDeclaration: + const exportDeclaration = (node as ExportDeclaration); + if (!exportDeclaration.moduleSpecifier && exportDeclaration.exportClause && exportDeclaration.exportClause.kind === SyntaxKind.NamedExports) { let state = ModuleInstanceState.NonInstantiated; - forEachChild(node, n => { - const childState = getModuleInstanceStateCached(n, visited); - switch (childState) { - case ModuleInstanceState.NonInstantiated: - // child is non-instantiated - continue searching - return; - case ModuleInstanceState.ConstEnumOnly: - // child is const enum only - record state and continue searching - state = ModuleInstanceState.ConstEnumOnly; - return; - case ModuleInstanceState.Instantiated: - // child is instantiated - record state and stop - state = ModuleInstanceState.Instantiated; - return true; - default: - Debug.assertNever(childState); + for (const specifier of exportDeclaration.exportClause.elements) { + const specifierState = getModuleInstanceStateForAliasTarget(specifier, visited); + if (specifierState > state) { + state = specifierState; } - }); + if (state === ModuleInstanceState.Instantiated) { + return state; + } + } return state; } - case SyntaxKind.ModuleDeclaration: - return getModuleInstanceState(node as ModuleDeclaration, visited); - case SyntaxKind.Identifier: - // Only jsdoc typedef definition can exist in jsdoc namespace, and it should - // be considered the same as type alias - if ((node).isInJSDocNamespace) { - return ModuleInstanceState.NonInstantiated; + break; + // 5. other uninstantiated module declarations. + case SyntaxKind.ModuleBlock: { + let state = ModuleInstanceState.NonInstantiated; + forEachChild(node, n => { + const childState = getModuleInstanceStateCached(n, visited); + switch (childState) { + case ModuleInstanceState.NonInstantiated: + // child is non-instantiated - continue searching + return; + case ModuleInstanceState.ConstEnumOnly: + // child is const enum only - record state and continue searching + state = ModuleInstanceState.ConstEnumOnly; + return; + case ModuleInstanceState.Instantiated: + // child is instantiated - record state and stop + state = ModuleInstanceState.Instantiated; + return true; + default: + Debug.assertNever(childState); } + }); + return state; } - return ModuleInstanceState.Instantiated; - } - - function getModuleInstanceStateForAliasTarget(specifier: ExportSpecifier, visited: Map) { - const name = specifier.propertyName || specifier.name; - let p: Node | undefined = specifier.parent; - while (p) { - if (isBlock(p) || isModuleBlock(p) || isSourceFile(p)) { - const statements = p.statements; - let found: ModuleInstanceState | undefined; - for (const statement of statements) { - if (nodeHasName(statement, name)) { - if (!statement.parent) { - setParentPointers(p, statement); - } - const state = getModuleInstanceStateCached(statement, visited); - if (found === undefined || state > found) { - found = state; - } - if (found === ModuleInstanceState.Instantiated) { - return found; - } + case SyntaxKind.ModuleDeclaration: + return getModuleInstanceState((node as ModuleDeclaration), visited); + case SyntaxKind.Identifier: + // Only jsdoc typedef definition can exist in jsdoc namespace, and it should + // be considered the same as type alias + if ((node).isInJSDocNamespace) { + return ModuleInstanceState.NonInstantiated; + } + } + return ModuleInstanceState.Instantiated; +} +/* @internal */ +function getModuleInstanceStateForAliasTarget(specifier: ExportSpecifier, visited: ts.Map) { + const name = specifier.propertyName || specifier.name; + let p: Node | undefined = specifier.parent; + while (p) { + if (isBlock(p) || isModuleBlock(p) || isSourceFile(p)) { + const statements = p.statements; + let found: ModuleInstanceState | undefined; + for (const statement of statements) { + if (nodeHasName(statement, name)) { + if (!statement.parent) { + setParentPointers(p, statement); + } + const state = getModuleInstanceStateCached(statement, visited); + if (found === undefined || state > found) { + found = state; + } + if (found === ModuleInstanceState.Instantiated) { + return found; } - } - if (found !== undefined) { - return found; } } - p = p.parent; - } - return ModuleInstanceState.Instantiated; // Couldn't locate, assume could refer to a value - } - - const enum ContainerFlags { - // The current node is not a container, and no container manipulation should happen before - // recursing into it. - None = 0, - - // The current node is a container. It should be set as the current container (and block- - // container) before recursing into it. The current node does not have locals. Examples: - // - // Classes, ObjectLiterals, TypeLiterals, Interfaces... - IsContainer = 1 << 0, - - // The current node is a block-scoped-container. It should be set as the current block- - // container before recursing into it. Examples: - // - // Blocks (when not parented by functions), Catch clauses, For/For-in/For-of statements... - IsBlockScopedContainer = 1 << 1, - - // The current node is the container of a control flow path. The current control flow should - // be saved and restored, and a new control flow initialized within the container. - IsControlFlowContainer = 1 << 2, - - IsFunctionLike = 1 << 3, - IsFunctionExpression = 1 << 4, - HasLocals = 1 << 5, - IsInterface = 1 << 6, - IsObjectLiteralOrClassExpressionMethod = 1 << 7, - } - - function initFlowNode(node: T) { - Debug.attachFlowNodeDebugInfo(node); - return node; - } - - const binder = createBinder(); - - export function bindSourceFile(file: SourceFile, options: CompilerOptions) { - performance.mark("beforeBind"); - perfLogger.logStartBindFile("" + file.fileName); - binder(file, options); - perfLogger.logStopBindFile(); - performance.mark("afterBind"); - performance.measure("Bind", "beforeBind", "afterBind"); - } - - function createBinder(): (file: SourceFile, options: CompilerOptions) => void { - let file: SourceFile; - let options: CompilerOptions; - let languageVersion: ScriptTarget; - let parent: Node; - let container: Node; - let thisParentContainer: Node; // Container one level up - let blockScopeContainer: Node; - let lastContainer: Node; - let delayedTypeAliases: (JSDocTypedefTag | JSDocCallbackTag | JSDocEnumTag)[]; - let seenThisKeyword: boolean; - - // state used by control flow analysis - let currentFlow: FlowNode; - let currentBreakTarget: FlowLabel | undefined; - let currentContinueTarget: FlowLabel | undefined; - let currentReturnTarget: FlowLabel | undefined; - let currentTrueTarget: FlowLabel | undefined; - let currentFalseTarget: FlowLabel | undefined; - let currentExceptionTarget: FlowLabel | undefined; - let preSwitchCaseFlow: FlowNode | undefined; - let activeLabelList: ActiveLabel | undefined; - let hasExplicitReturn: boolean; - - // state used for emit helpers - let emitFlags: NodeFlags; - - // If this file is an external module, then it is automatically in strict-mode according to - // ES6. If it is not an external module, then we'll determine if it is in strict mode or - // not depending on if we see "use strict" in certain places or if we hit a class/namespace - // or if compiler options contain alwaysStrict. - let inStrictMode: boolean; - - let symbolCount = 0; - - let Symbol: new (flags: SymbolFlags, name: __String) => Symbol; - let classifiableNames: UnderscoreEscapedMap; - - const unreachableFlow: FlowNode = { flags: FlowFlags.Unreachable }; - const reportedUnreachableFlow: FlowNode = { flags: FlowFlags.Unreachable }; - - // state used to aggregate transform flags during bind. - let subtreeTransformFlags: TransformFlags = TransformFlags.None; - let skipTransformFlagAggregation: boolean; - - /** - * Inside the binder, we may create a diagnostic for an as-yet unbound node (with potentially no parent pointers, implying no accessible source file) - * If so, the node _must_ be in the current file (as that's the only way anything could have traversed to it to yield it as the error node) - * This version of `createDiagnosticForNode` uses the binder's context to account for this, and always yields correct diagnostics even in these situations. - */ - function createDiagnosticForNode(node: Node, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number): DiagnosticWithLocation { - return createDiagnosticForNodeInSourceFile(getSourceFileOfNode(node) || file, node, message, arg0, arg1, arg2); - } - - function bindSourceFile(f: SourceFile, opts: CompilerOptions) { - file = f; - options = opts; - languageVersion = getEmitScriptTarget(options); - inStrictMode = bindInStrictMode(file, opts); - classifiableNames = createUnderscoreEscapedMap(); - symbolCount = 0; - skipTransformFlagAggregation = file.isDeclarationFile; - - Symbol = objectAllocator.getSymbolConstructor(); - - // Attach debugging information if necessary - Debug.attachFlowNodeDebugInfo(unreachableFlow); - Debug.attachFlowNodeDebugInfo(reportedUnreachableFlow); - - if (!file.locals) { - bind(file); - file.symbolCount = symbolCount; - file.classifiableNames = classifiableNames; - delayedBindJSDocTypedefTag(); + if (found !== undefined) { + return found; } - - file = undefined!; - options = undefined!; - languageVersion = undefined!; - parent = undefined!; - container = undefined!; - thisParentContainer = undefined!; - blockScopeContainer = undefined!; - lastContainer = undefined!; - delayedTypeAliases = undefined!; - seenThisKeyword = false; - currentFlow = undefined!; - currentBreakTarget = undefined; - currentContinueTarget = undefined; - currentReturnTarget = undefined; - currentTrueTarget = undefined; - currentFalseTarget = undefined; - currentExceptionTarget = undefined; - activeLabelList = undefined; - hasExplicitReturn = false; - emitFlags = NodeFlags.None; - subtreeTransformFlags = TransformFlags.None; } - - return bindSourceFile; - - function bindInStrictMode(file: SourceFile, opts: CompilerOptions): boolean { - if (getStrictOptionValue(opts, "alwaysStrict") && !file.isDeclarationFile) { - // bind in strict mode source files with alwaysStrict option - return true; - } - else { - return !!file.externalModuleIndicator; - } + p = p.parent; + } + return ModuleInstanceState.Instantiated; // Couldn't locate, assume could refer to a value +} +/* @internal */ +const enum ContainerFlags { + // The current node is not a container, and no container manipulation should happen before + // recursing into it. + None = 0, + // The current node is a container. It should be set as the current container (and block- + // container) before recursing into it. The current node does not have locals. Examples: + // + // Classes, ObjectLiterals, TypeLiterals, Interfaces... + IsContainer = 1 << 0, + // The current node is a block-scoped-container. It should be set as the current block- + // container before recursing into it. Examples: + // + // Blocks (when not parented by functions), Catch clauses, For/For-in/For-of statements... + IsBlockScopedContainer = 1 << 1, + // The current node is the container of a control flow path. The current control flow should + // be saved and restored, and a new control flow initialized within the container. + IsControlFlowContainer = 1 << 2, + IsFunctionLike = 1 << 3, + IsFunctionExpression = 1 << 4, + HasLocals = 1 << 5, + IsInterface = 1 << 6, + IsObjectLiteralOrClassExpressionMethod = 1 << 7 +} +/* @internal */ +function initFlowNode(node: T) { + Debug.attachFlowNodeDebugInfo(node); + return node; +} +/* @internal */ +const binder = createBinder(); +/* @internal */ +export function bindSourceFile(file: SourceFile, options: CompilerOptions) { + mark("beforeBind"); + perfLogger.logStartBindFile("" + file.fileName); + binder(file, options); + perfLogger.logStopBindFile(); + mark("afterBind"); + measure("Bind", "beforeBind", "afterBind"); +} +/* @internal */ +function createBinder(): (file: SourceFile, options: CompilerOptions) => void { + let file: SourceFile; + let options: CompilerOptions; + let languageVersion: ScriptTarget; + let parent: Node; + let container: Node; + let thisParentContainer: Node; // Container one level up + let blockScopeContainer: Node; + let lastContainer: Node; + let delayedTypeAliases: (JSDocTypedefTag | JSDocCallbackTag | JSDocEnumTag)[]; + let seenThisKeyword: boolean; + // state used by control flow analysis + let currentFlow: FlowNode; + let currentBreakTarget: FlowLabel | undefined; + let currentContinueTarget: FlowLabel | undefined; + let currentReturnTarget: FlowLabel | undefined; + let currentTrueTarget: FlowLabel | undefined; + let currentFalseTarget: FlowLabel | undefined; + let currentExceptionTarget: FlowLabel | undefined; + let preSwitchCaseFlow: FlowNode | undefined; + let activeLabelList: ActiveLabel | undefined; + let hasExplicitReturn: boolean; + // state used for emit helpers + let emitFlags: NodeFlags; + // If this file is an external module, then it is automatically in strict-mode according to + // ES6. If it is not an external module, then we'll determine if it is in strict mode or + // not depending on if we see "use strict" in certain places or if we hit a class/namespace + // or if compiler options contain alwaysStrict. + let inStrictMode: boolean; + let symbolCount = 0; + let Symbol: new (flags: SymbolFlags, name: __String) => ts.Symbol; + let classifiableNames: UnderscoreEscapedMap; + const unreachableFlow: FlowNode = { flags: FlowFlags.Unreachable }; + const reportedUnreachableFlow: FlowNode = { flags: FlowFlags.Unreachable }; + // state used to aggregate transform flags during bind. + let subtreeTransformFlags: TransformFlags = TransformFlags.None; + let skipTransformFlagAggregation: boolean; + /** + * Inside the binder, we may create a diagnostic for an as-yet unbound node (with potentially no parent pointers, implying no accessible source file) + * If so, the node _must_ be in the current file (as that's the only way anything could have traversed to it to yield it as the error node) + * This version of `createDiagnosticForNode` uses the binder's context to account for this, and always yields correct diagnostics even in these situations. + */ + function createDiagnosticForNode(node: Node, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number): DiagnosticWithLocation { + return createDiagnosticForNodeInSourceFile(getSourceFileOfNode(node) || file, node, message, arg0, arg1, arg2); + } + function bindSourceFile(f: SourceFile, opts: CompilerOptions) { + file = f; + options = opts; + languageVersion = getEmitScriptTarget(options); + inStrictMode = bindInStrictMode(file, opts); + classifiableNames = createUnderscoreEscapedMap(); + symbolCount = 0; + skipTransformFlagAggregation = file.isDeclarationFile; + Symbol = objectAllocator.getSymbolConstructor(); + // Attach debugging information if necessary + Debug.attachFlowNodeDebugInfo(unreachableFlow); + Debug.attachFlowNodeDebugInfo(reportedUnreachableFlow); + if (!file.locals) { + bind(file); + file.symbolCount = symbolCount; + file.classifiableNames = classifiableNames; + delayedBindJSDocTypedefTag(); + } + file = undefined!; + options = undefined!; + languageVersion = undefined!; + parent = undefined!; + container = undefined!; + thisParentContainer = undefined!; + blockScopeContainer = undefined!; + lastContainer = undefined!; + delayedTypeAliases = undefined!; + seenThisKeyword = false; + currentFlow = undefined!; + currentBreakTarget = undefined; + currentContinueTarget = undefined; + currentReturnTarget = undefined; + currentTrueTarget = undefined; + currentFalseTarget = undefined; + currentExceptionTarget = undefined; + activeLabelList = undefined; + hasExplicitReturn = false; + emitFlags = NodeFlags.None; + subtreeTransformFlags = TransformFlags.None; + } + return bindSourceFile; + function bindInStrictMode(file: SourceFile, opts: CompilerOptions): boolean { + if (getStrictOptionValue(opts, "alwaysStrict") && !file.isDeclarationFile) { + // bind in strict mode source files with alwaysStrict option + return true; } - - function createSymbol(flags: SymbolFlags, name: __String): Symbol { - symbolCount++; - return new Symbol(flags, name); - } - - function addDeclarationToSymbol(symbol: Symbol, node: Declaration, symbolFlags: SymbolFlags) { - symbol.flags |= symbolFlags; - - node.symbol = symbol; - symbol.declarations = appendIfUnique(symbol.declarations, node); - - if (symbolFlags & (SymbolFlags.Class | SymbolFlags.Enum | SymbolFlags.Module | SymbolFlags.Variable) && !symbol.exports) { - symbol.exports = createSymbolTable(); - } - - if (symbolFlags & (SymbolFlags.Class | SymbolFlags.Interface | SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral) && !symbol.members) { - symbol.members = createSymbolTable(); - } - - // On merge of const enum module with class or function, reset const enum only flag (namespaces will already recalculate) - if (symbol.constEnumOnlyModule && (symbol.flags & (SymbolFlags.Function | SymbolFlags.Class | SymbolFlags.RegularEnum))) { - symbol.constEnumOnlyModule = false; - } - - if (symbolFlags & SymbolFlags.Value) { - setValueDeclaration(symbol, node); - } + else { + return !!file.externalModuleIndicator; } - - function setValueDeclaration(symbol: Symbol, node: Declaration): void { - const { valueDeclaration } = symbol; - if (!valueDeclaration || - (isAssignmentDeclaration(valueDeclaration) && !isAssignmentDeclaration(node)) || - (valueDeclaration.kind !== node.kind && isEffectiveModuleDeclaration(valueDeclaration))) { - // other kinds of value declarations take precedence over modules and assignment declarations - symbol.valueDeclaration = node; - } + } + function createSymbol(flags: SymbolFlags, name: __String): ts.Symbol { + symbolCount++; + return new Symbol(flags, name); + } + function addDeclarationToSymbol(symbol: ts.Symbol, node: Declaration, symbolFlags: SymbolFlags) { + symbol.flags |= symbolFlags; + node.symbol = symbol; + symbol.declarations = appendIfUnique(symbol.declarations, node); + if (symbolFlags & (SymbolFlags.Class | SymbolFlags.Enum | SymbolFlags.Module | SymbolFlags.Variable) && !symbol.exports) { + symbol.exports = createSymbolTable(); + } + if (symbolFlags & (SymbolFlags.Class | SymbolFlags.Interface | SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral) && !symbol.members) { + symbol.members = createSymbolTable(); + } + // On merge of const enum module with class or function, reset const enum only flag (namespaces will already recalculate) + if (symbol.constEnumOnlyModule && (symbol.flags & (SymbolFlags.Function | SymbolFlags.Class | SymbolFlags.RegularEnum))) { + symbol.constEnumOnlyModule = false; + } + if (symbolFlags & SymbolFlags.Value) { + setValueDeclaration(symbol, node); + } + } + function setValueDeclaration(symbol: ts.Symbol, node: Declaration): void { + const { valueDeclaration } = symbol; + if (!valueDeclaration || + (isAssignmentDeclaration(valueDeclaration) && !isAssignmentDeclaration(node)) || + (valueDeclaration.kind !== node.kind && isEffectiveModuleDeclaration(valueDeclaration))) { + // other kinds of value declarations take precedence over modules and assignment declarations + symbol.valueDeclaration = node; } - - // Should not be called on a declaration with a computed property name, - // unless it is a well known Symbol. - function getDeclarationName(node: Declaration): __String | undefined { - if (node.kind === SyntaxKind.ExportAssignment) { - return (node).isExportEquals ? InternalSymbolName.ExportEquals : InternalSymbolName.Default; + } + // Should not be called on a declaration with a computed property name, + // unless it is a well known Symbol. + function getDeclarationName(node: Declaration): __String | undefined { + if (node.kind === SyntaxKind.ExportAssignment) { + return (node).isExportEquals ? InternalSymbolName.ExportEquals : InternalSymbolName.Default; + } + const name = getNameOfDeclaration(node); + if (name) { + if (isAmbientModule(node)) { + const moduleName = getTextOfIdentifierOrLiteral((name as Identifier | StringLiteral)); + return (isGlobalScopeAugmentation((node)) ? "__global" : `"${moduleName}"`) as __String; } - - const name = getNameOfDeclaration(node); - if (name) { - if (isAmbientModule(node)) { - const moduleName = getTextOfIdentifierOrLiteral(name as Identifier | StringLiteral); - return (isGlobalScopeAugmentation(node) ? "__global" : `"${moduleName}"`) as __String; - } - if (name.kind === SyntaxKind.ComputedPropertyName) { - const nameExpression = name.expression; - // treat computed property names where expression is string/numeric literal as just string/numeric literal - if (isStringOrNumericLiteralLike(nameExpression)) { - return escapeLeadingUnderscores(nameExpression.text); - } - if (isSignedNumericLiteral(nameExpression)) { - return tokenToString(nameExpression.operator) + nameExpression.operand.text as __String; - } - - Debug.assert(isWellKnownSymbolSyntactically(nameExpression)); - return getPropertyNameForKnownSymbolName(idText((nameExpression).name)); + if (name.kind === SyntaxKind.ComputedPropertyName) { + const nameExpression = name.expression; + // treat computed property names where expression is string/numeric literal as just string/numeric literal + if (isStringOrNumericLiteralLike(nameExpression)) { + return escapeLeadingUnderscores(nameExpression.text); } - if (isWellKnownSymbolSyntactically(name)) { - return getPropertyNameForKnownSymbolName(idText(name.name)); + if (isSignedNumericLiteral(nameExpression)) { + return tokenToString(nameExpression.operator) + nameExpression.operand.text as __String; } - if (isPrivateIdentifier(name)) { - // containingClass exists because private names only allowed inside classes - const containingClass = getContainingClass(node); - if (!containingClass) { - // we can get here in cases where there is already a parse error. - return undefined; - } - const containingClassSymbol = containingClass.symbol; - return getSymbolNameForPrivateIdentifier(containingClassSymbol, name.escapedText); + Debug.assert(isWellKnownSymbolSyntactically(nameExpression)); + return getPropertyNameForKnownSymbolName(idText((nameExpression).name)); + } + if (isWellKnownSymbolSyntactically(name)) { + return getPropertyNameForKnownSymbolName(idText(name.name)); + } + if (isPrivateIdentifier(name)) { + // containingClass exists because private names only allowed inside classes + const containingClass = getContainingClass(node); + if (!containingClass) { + // we can get here in cases where there is already a parse error. + return undefined; } - return isPropertyNameLiteral(name) ? getEscapedTextOfIdentifierOrLiteral(name) : undefined; + const containingClassSymbol = containingClass.symbol; + return getSymbolNameForPrivateIdentifier(containingClassSymbol, name.escapedText); } - switch (node.kind) { - case SyntaxKind.Constructor: - return InternalSymbolName.Constructor; - case SyntaxKind.FunctionType: - case SyntaxKind.CallSignature: - case SyntaxKind.JSDocSignature: - return InternalSymbolName.Call; - case SyntaxKind.ConstructorType: - case SyntaxKind.ConstructSignature: - return InternalSymbolName.New; - case SyntaxKind.IndexSignature: - return InternalSymbolName.Index; - case SyntaxKind.ExportDeclaration: - return InternalSymbolName.ExportStar; - case SyntaxKind.SourceFile: - // json file should behave as + return isPropertyNameLiteral(name) ? getEscapedTextOfIdentifierOrLiteral(name) : undefined; + } + switch (node.kind) { + case SyntaxKind.Constructor: + return InternalSymbolName.Constructor; + case SyntaxKind.FunctionType: + case SyntaxKind.CallSignature: + case SyntaxKind.JSDocSignature: + return InternalSymbolName.Call; + case SyntaxKind.ConstructorType: + case SyntaxKind.ConstructSignature: + return InternalSymbolName.New; + case SyntaxKind.IndexSignature: + return InternalSymbolName.Index; + case SyntaxKind.ExportDeclaration: + return InternalSymbolName.ExportStar; + case SyntaxKind.SourceFile: + // json file should behave as + // module.exports = ... + return InternalSymbolName.ExportEquals; + case SyntaxKind.BinaryExpression: + if (getAssignmentDeclarationKind((node as BinaryExpression)) === AssignmentDeclarationKind.ModuleExports) { // module.exports = ... return InternalSymbolName.ExportEquals; - case SyntaxKind.BinaryExpression: - if (getAssignmentDeclarationKind(node as BinaryExpression) === AssignmentDeclarationKind.ModuleExports) { - // module.exports = ... - return InternalSymbolName.ExportEquals; - } - Debug.fail("Unknown binary declaration kind"); - break; - case SyntaxKind.JSDocFunctionType: - return (isJSDocConstructSignature(node) ? InternalSymbolName.New : InternalSymbolName.Call); - case SyntaxKind.Parameter: - // Parameters with names are handled at the top of this function. Parameters - // without names can only come from JSDocFunctionTypes. - Debug.assert(node.parent.kind === SyntaxKind.JSDocFunctionType, "Impossible parameter parent kind", () => `parent is: ${(ts as any).SyntaxKind ? (ts as any).SyntaxKind[node.parent.kind] : node.parent.kind}, expected JSDocFunctionType`); - const functionType = node.parent; - const index = functionType.parameters.indexOf(node as ParameterDeclaration); - return "arg" + index as __String; - } + } + Debug.fail("Unknown binary declaration kind"); + break; + case SyntaxKind.JSDocFunctionType: + return (isJSDocConstructSignature(node) ? InternalSymbolName.New : InternalSymbolName.Call); + case SyntaxKind.Parameter: + // Parameters with names are handled at the top of this function. Parameters + // without names can only come from JSDocFunctionTypes. + Debug.assert(node.parent.kind === SyntaxKind.JSDocFunctionType, "Impossible parameter parent kind", () => `parent is: ${(ts as any).SyntaxKind ? (ts as any).SyntaxKind[node.parent.kind] : node.parent.kind}, expected JSDocFunctionType`); + const functionType = (node.parent); + const index = functionType.parameters.indexOf((node as ParameterDeclaration)); + return "arg" + index as __String; } - - function getDisplayName(node: Declaration): string { - return isNamedDeclaration(node) ? declarationNameToString(node.name) : unescapeLeadingUnderscores(Debug.checkDefined(getDeclarationName(node))); + } + function getDisplayName(node: Declaration): string { + return isNamedDeclaration(node) ? declarationNameToString(node.name) : unescapeLeadingUnderscores(Debug.checkDefined(getDeclarationName(node))); + } + /** + * Declares a Symbol for the node and adds it to symbols. Reports errors for conflicting identifier names. + * @param symbolTable - The symbol table which node will be added to. + * @param parent - node's parent declaration. + * @param node - The declaration to be added to the symbol table + * @param includes - The SymbolFlags that node has in addition to its declaration type (eg: export, ambient, etc.) + * @param excludes - The flags which node cannot be declared alongside in a symbol table. Used to report forbidden declarations. + */ + function declareSymbol(symbolTable: SymbolTable, parent: ts.Symbol | undefined, node: Declaration, includes: SymbolFlags, excludes: SymbolFlags, isReplaceableByMethod?: boolean): ts.Symbol { + Debug.assert(!hasDynamicName(node)); + const isDefaultExport = hasModifier(node, ModifierFlags.Default); + // The exported symbol for an export default function/class node is always named "default" + const name = isDefaultExport && parent ? InternalSymbolName.Default : getDeclarationName(node); + let symbol: ts.Symbol | undefined; + if (name === undefined) { + symbol = createSymbol(SymbolFlags.None, InternalSymbolName.Missing); } - - /** - * Declares a Symbol for the node and adds it to symbols. Reports errors for conflicting identifier names. - * @param symbolTable - The symbol table which node will be added to. - * @param parent - node's parent declaration. - * @param node - The declaration to be added to the symbol table - * @param includes - The SymbolFlags that node has in addition to its declaration type (eg: export, ambient, etc.) - * @param excludes - The flags which node cannot be declared alongside in a symbol table. Used to report forbidden declarations. - */ - function declareSymbol(symbolTable: SymbolTable, parent: Symbol | undefined, node: Declaration, includes: SymbolFlags, excludes: SymbolFlags, isReplaceableByMethod?: boolean): Symbol { - Debug.assert(!hasDynamicName(node)); - - const isDefaultExport = hasModifier(node, ModifierFlags.Default); - - // The exported symbol for an export default function/class node is always named "default" - const name = isDefaultExport && parent ? InternalSymbolName.Default : getDeclarationName(node); - - let symbol: Symbol | undefined; - if (name === undefined) { - symbol = createSymbol(SymbolFlags.None, InternalSymbolName.Missing); + else { + // Check and see if the symbol table already has a symbol with this name. If not, + // create a new symbol with this name and add it to the table. Note that we don't + // give the new symbol any flags *yet*. This ensures that it will not conflict + // with the 'excludes' flags we pass in. + // + // If we do get an existing symbol, see if it conflicts with the new symbol we're + // creating. For example, a 'var' symbol and a 'class' symbol will conflict within + // the same symbol table. If we have a conflict, report the issue on each + // declaration we have for this symbol, and then create a new symbol for this + // declaration. + // + // Note that when properties declared in Javascript constructors + // (marked by isReplaceableByMethod) conflict with another symbol, the property loses. + // Always. This allows the common Javascript pattern of overwriting a prototype method + // with an bound instance method of the same type: `this.method = this.method.bind(this)` + // + // If we created a new symbol, either because we didn't have a symbol with this name + // in the symbol table, or we conflicted with an existing symbol, then just add this + // node as the sole declaration of the new symbol. + // + // Otherwise, we'll be merging into a compatible existing symbol (for example when + // you have multiple 'vars' with the same name in the same container). In this case + // just add this node into the declarations list of the symbol. + symbol = symbolTable.get(name); + if (includes & SymbolFlags.Classifiable) { + classifiableNames.set(name, true); + } + if (!symbol) { + symbolTable.set(name, symbol = createSymbol(SymbolFlags.None, name)); + if (isReplaceableByMethod) + symbol.isReplaceableByMethod = true; + } + else if (isReplaceableByMethod && !symbol.isReplaceableByMethod) { + // A symbol already exists, so don't add this as a declaration. + return symbol; } - else { - // Check and see if the symbol table already has a symbol with this name. If not, - // create a new symbol with this name and add it to the table. Note that we don't - // give the new symbol any flags *yet*. This ensures that it will not conflict - // with the 'excludes' flags we pass in. - // - // If we do get an existing symbol, see if it conflicts with the new symbol we're - // creating. For example, a 'var' symbol and a 'class' symbol will conflict within - // the same symbol table. If we have a conflict, report the issue on each - // declaration we have for this symbol, and then create a new symbol for this - // declaration. - // - // Note that when properties declared in Javascript constructors - // (marked by isReplaceableByMethod) conflict with another symbol, the property loses. - // Always. This allows the common Javascript pattern of overwriting a prototype method - // with an bound instance method of the same type: `this.method = this.method.bind(this)` - // - // If we created a new symbol, either because we didn't have a symbol with this name - // in the symbol table, or we conflicted with an existing symbol, then just add this - // node as the sole declaration of the new symbol. - // - // Otherwise, we'll be merging into a compatible existing symbol (for example when - // you have multiple 'vars' with the same name in the same container). In this case - // just add this node into the declarations list of the symbol. - symbol = symbolTable.get(name); - - if (includes & SymbolFlags.Classifiable) { - classifiableNames.set(name, true); - } - - if (!symbol) { + else if (symbol.flags & excludes) { + if (symbol.isReplaceableByMethod) { + // Javascript constructor-declared symbols can be discarded in favor of + // prototype symbols like methods. symbolTable.set(name, symbol = createSymbol(SymbolFlags.None, name)); - if (isReplaceableByMethod) symbol.isReplaceableByMethod = true; - } - else if (isReplaceableByMethod && !symbol.isReplaceableByMethod) { - // A symbol already exists, so don't add this as a declaration. - return symbol; } - else if (symbol.flags & excludes) { - if (symbol.isReplaceableByMethod) { - // Javascript constructor-declared symbols can be discarded in favor of - // prototype symbols like methods. - symbolTable.set(name, symbol = createSymbol(SymbolFlags.None, name)); + else if (!(includes & SymbolFlags.Variable && symbol.flags & SymbolFlags.Assignment)) { + // Assignment declarations are allowed to merge with variables, no matter what other flags they have. + if (isNamedDeclaration(node)) { + node.name.parent = node; } - else if (!(includes & SymbolFlags.Variable && symbol.flags & SymbolFlags.Assignment)) { - // Assignment declarations are allowed to merge with variables, no matter what other flags they have. - if (isNamedDeclaration(node)) { - node.name.parent = node; - } - // Report errors every position with duplicate declaration - // Report errors on previous encountered declarations - let message = symbol.flags & SymbolFlags.BlockScopedVariable - ? Diagnostics.Cannot_redeclare_block_scoped_variable_0 - : Diagnostics.Duplicate_identifier_0; - let messageNeedsName = true; - - if (symbol.flags & SymbolFlags.Enum || includes & SymbolFlags.Enum) { - message = Diagnostics.Enum_declarations_can_only_merge_with_namespace_or_other_enum_declarations; + // Report errors every position with duplicate declaration + // Report errors on previous encountered declarations + let message = symbol.flags & SymbolFlags.BlockScopedVariable + ? Diagnostics.Cannot_redeclare_block_scoped_variable_0 + : Diagnostics.Duplicate_identifier_0; + let messageNeedsName = true; + if (symbol.flags & SymbolFlags.Enum || includes & SymbolFlags.Enum) { + message = Diagnostics.Enum_declarations_can_only_merge_with_namespace_or_other_enum_declarations; + messageNeedsName = false; + } + let multipleDefaultExports = false; + if (length(symbol.declarations)) { + // If the current node is a default export of some sort, then check if + // there are any other default exports that we need to error on. + // We'll know whether we have other default exports depending on if `symbol` already has a declaration list set. + if (isDefaultExport) { + message = Diagnostics.A_module_cannot_have_multiple_default_exports; messageNeedsName = false; + multipleDefaultExports = true; } - - let multipleDefaultExports = false; - if (length(symbol.declarations)) { - // If the current node is a default export of some sort, then check if - // there are any other default exports that we need to error on. - // We'll know whether we have other default exports depending on if `symbol` already has a declaration list set. - if (isDefaultExport) { + else { + // This is to properly report an error in the case "export default { }" is after export default of class declaration or function declaration. + // Error on multiple export default in the following case: + // 1. multiple export default of class declaration or function declaration by checking NodeFlags.Default + // 2. multiple export default of export assignment. This one doesn't have NodeFlags.Default on (as export default doesn't considered as modifiers) + if (symbol.declarations && symbol.declarations.length && + (node.kind === SyntaxKind.ExportAssignment && !(node).isExportEquals)) { message = Diagnostics.A_module_cannot_have_multiple_default_exports; messageNeedsName = false; multipleDefaultExports = true; } - else { - // This is to properly report an error in the case "export default { }" is after export default of class declaration or function declaration. - // Error on multiple export default in the following case: - // 1. multiple export default of class declaration or function declaration by checking NodeFlags.Default - // 2. multiple export default of export assignment. This one doesn't have NodeFlags.Default on (as export default doesn't considered as modifiers) - if (symbol.declarations && symbol.declarations.length && - (node.kind === SyntaxKind.ExportAssignment && !(node).isExportEquals)) { - message = Diagnostics.A_module_cannot_have_multiple_default_exports; - messageNeedsName = false; - multipleDefaultExports = true; - } - } - } - - const relatedInformation: DiagnosticRelatedInformation[] = []; - if (isTypeAliasDeclaration(node) && nodeIsMissing(node.type) && hasModifier(node, ModifierFlags.Export) && symbol.flags & (SymbolFlags.Alias | SymbolFlags.Type | SymbolFlags.Namespace)) { - // export type T; - may have meant export type { T }? - relatedInformation.push(createDiagnosticForNode(node, Diagnostics.Did_you_mean_0, `export type { ${unescapeLeadingUnderscores(node.name.escapedText)} }`)); } - - const declarationName = getNameOfDeclaration(node) || node; - forEach(symbol.declarations, (declaration, index) => { - const decl = getNameOfDeclaration(declaration) || declaration; - const diag = createDiagnosticForNode(decl, message, messageNeedsName ? getDisplayName(declaration) : undefined); - file.bindDiagnostics.push( - multipleDefaultExports ? addRelatedInfo(diag, createDiagnosticForNode(declarationName, index === 0 ? Diagnostics.Another_export_default_is_here : Diagnostics.and_here)) : diag - ); - if (multipleDefaultExports) { - relatedInformation.push(createDiagnosticForNode(decl, Diagnostics.The_first_export_default_is_here)); - } - }); - - const diag = createDiagnosticForNode(declarationName, message, messageNeedsName ? getDisplayName(node) : undefined); - file.bindDiagnostics.push(addRelatedInfo(diag, ...relatedInformation)); - - symbol = createSymbol(SymbolFlags.None, name); } + const relatedInformation: DiagnosticRelatedInformation[] = []; + if (isTypeAliasDeclaration(node) && nodeIsMissing(node.type) && hasModifier(node, ModifierFlags.Export) && symbol.flags & (SymbolFlags.Alias | SymbolFlags.Type | SymbolFlags.Namespace)) { + // export type T; - may have meant export type { T }? + relatedInformation.push(createDiagnosticForNode(node, Diagnostics.Did_you_mean_0, `export type { ${unescapeLeadingUnderscores(node.name.escapedText)} }`)); + } + const declarationName = getNameOfDeclaration(node) || node; + forEach(symbol.declarations, (declaration, index) => { + const decl = getNameOfDeclaration(declaration) || declaration; + const diag = createDiagnosticForNode(decl, message, messageNeedsName ? getDisplayName(declaration) : undefined); + file.bindDiagnostics.push(multipleDefaultExports ? addRelatedInfo(diag, createDiagnosticForNode(declarationName, index === 0 ? Diagnostics.Another_export_default_is_here : Diagnostics.and_here)) : diag); + if (multipleDefaultExports) { + relatedInformation.push(createDiagnosticForNode(decl, Diagnostics.The_first_export_default_is_here)); + } + }); + const diag = createDiagnosticForNode(declarationName, message, messageNeedsName ? getDisplayName(node) : undefined); + file.bindDiagnostics.push(addRelatedInfo(diag, ...relatedInformation)); + symbol = createSymbol(SymbolFlags.None, name); } } - - addDeclarationToSymbol(symbol, node, includes); - if (symbol.parent) { - Debug.assert(symbol.parent === parent, "Existing symbol parent should match new one"); - } - else { - symbol.parent = parent; - } - - return symbol; } - - function declareModuleMember(node: Declaration, symbolFlags: SymbolFlags, symbolExcludes: SymbolFlags): Symbol { - const hasExportModifier = getCombinedModifierFlags(node) & ModifierFlags.Export; - if (symbolFlags & SymbolFlags.Alias) { - if (node.kind === SyntaxKind.ExportSpecifier || (node.kind === SyntaxKind.ImportEqualsDeclaration && hasExportModifier)) { - return declareSymbol(container.symbol.exports!, container.symbol, node, symbolFlags, symbolExcludes); - } - else { - return declareSymbol(container.locals!, /*parent*/ undefined, node, symbolFlags, symbolExcludes); - } + addDeclarationToSymbol(symbol, node, includes); + if (symbol.parent) { + Debug.assert(symbol.parent === parent, "Existing symbol parent should match new one"); + } + else { + symbol.parent = parent; + } + return symbol; + } + function declareModuleMember(node: Declaration, symbolFlags: SymbolFlags, symbolExcludes: SymbolFlags): ts.Symbol { + const hasExportModifier = getCombinedModifierFlags(node) & ModifierFlags.Export; + if (symbolFlags & SymbolFlags.Alias) { + if (node.kind === SyntaxKind.ExportSpecifier || (node.kind === SyntaxKind.ImportEqualsDeclaration && hasExportModifier)) { + return declareSymbol(container.symbol.exports!, container.symbol, node, symbolFlags, symbolExcludes); } else { - // Exported module members are given 2 symbols: A local symbol that is classified with an ExportValue flag, - // and an associated export symbol with all the correct flags set on it. There are 2 main reasons: - // - // 1. We treat locals and exports of the same name as mutually exclusive within a container. - // That means the binder will issue a Duplicate Identifier error if you mix locals and exports - // with the same name in the same container. - // TODO: Make this a more specific error and decouple it from the exclusion logic. - // 2. When we checkIdentifier in the checker, we set its resolved symbol to the local symbol, - // but return the export symbol (by calling getExportSymbolOfValueSymbolIfExported). That way - // when the emitter comes back to it, it knows not to qualify the name if it was found in a containing scope. - - // NOTE: Nested ambient modules always should go to to 'locals' table to prevent their automatic merge - // during global merging in the checker. Why? The only case when ambient module is permitted inside another module is module augmentation - // and this case is specially handled. Module augmentations should only be merged with original module definition - // and should never be merged directly with other augmentation, and the latter case would be possible if automatic merge is allowed. - if (isJSDocTypeAlias(node)) Debug.assert(isInJSFile(node)); // We shouldn't add symbols for JSDoc nodes if not in a JS file. - if ((!isAmbientModule(node) && (hasExportModifier || container.flags & NodeFlags.ExportContext)) || isJSDocTypeAlias(node)) { - if (!container.locals || (hasModifier(node, ModifierFlags.Default) && !getDeclarationName(node))) { - return declareSymbol(container.symbol.exports!, container.symbol, node, symbolFlags, symbolExcludes); // No local symbol for an unnamed default! - } - const exportKind = symbolFlags & SymbolFlags.Value ? SymbolFlags.ExportValue : 0; - const local = declareSymbol(container.locals, /*parent*/ undefined, node, exportKind, symbolExcludes); - local.exportSymbol = declareSymbol(container.symbol.exports!, container.symbol, node, symbolFlags, symbolExcludes); - node.localSymbol = local; - return local; - } - else { - return declareSymbol(container.locals!, /*parent*/ undefined, node, symbolFlags, symbolExcludes); - } + return declareSymbol(container.locals!, /*parent*/ undefined, node, symbolFlags, symbolExcludes); } } - - // All container nodes are kept on a linked list in declaration order. This list is used by - // the getLocalNameOfContainer function in the type checker to validate that the local name - // used for a container is unique. - function bindContainer(node: Node, containerFlags: ContainerFlags) { - // Before we recurse into a node's children, we first save the existing parent, container - // and block-container. Then after we pop out of processing the children, we restore - // these saved values. - const saveContainer = container; - const saveThisParentContainer = thisParentContainer; - const savedBlockScopeContainer = blockScopeContainer; - - // Depending on what kind of node this is, we may have to adjust the current container - // and block-container. If the current node is a container, then it is automatically - // considered the current block-container as well. Also, for containers that we know - // may contain locals, we eagerly initialize the .locals field. We do this because - // it's highly likely that the .locals will be needed to place some child in (for example, - // a parameter, or variable declaration). - // - // However, we do not proactively create the .locals for block-containers because it's - // totally normal and common for block-containers to never actually have a block-scoped - // variable in them. We don't want to end up allocating an object for every 'block' we - // run into when most of them won't be necessary. + else { + // Exported module members are given 2 symbols: A local symbol that is classified with an ExportValue flag, + // and an associated export symbol with all the correct flags set on it. There are 2 main reasons: // - // Finally, if this is a block-container, then we clear out any existing .locals object - // it may contain within it. This happens in incremental scenarios. Because we can be - // reusing a node from a previous compilation, that node may have had 'locals' created - // for it. We must clear this so we don't accidentally move any stale data forward from - // a previous compilation. - if (containerFlags & ContainerFlags.IsContainer) { - if (node.kind !== SyntaxKind.ArrowFunction) { - thisParentContainer = container; - } - container = blockScopeContainer = node; - if (containerFlags & ContainerFlags.HasLocals) { - container.locals = createSymbolTable(); - } - addToContainerChain(container); + // 1. We treat locals and exports of the same name as mutually exclusive within a container. + // That means the binder will issue a Duplicate Identifier error if you mix locals and exports + // with the same name in the same container. + // TODO: Make this a more specific error and decouple it from the exclusion logic. + // 2. When we checkIdentifier in the checker, we set its resolved symbol to the local symbol, + // but return the export symbol (by calling getExportSymbolOfValueSymbolIfExported). That way + // when the emitter comes back to it, it knows not to qualify the name if it was found in a containing scope. + // NOTE: Nested ambient modules always should go to to 'locals' table to prevent their automatic merge + // during global merging in the checker. Why? The only case when ambient module is permitted inside another module is module augmentation + // and this case is specially handled. Module augmentations should only be merged with original module definition + // and should never be merged directly with other augmentation, and the latter case would be possible if automatic merge is allowed. + if (isJSDocTypeAlias(node)) + Debug.assert(isInJSFile(node)); // We shouldn't add symbols for JSDoc nodes if not in a JS file. + if ((!isAmbientModule(node) && (hasExportModifier || container.flags & NodeFlags.ExportContext)) || isJSDocTypeAlias(node)) { + if (!container.locals || (hasModifier(node, ModifierFlags.Default) && !getDeclarationName(node))) { + return declareSymbol(container.symbol.exports!, container.symbol, node, symbolFlags, symbolExcludes); // No local symbol for an unnamed default! + } + const exportKind = symbolFlags & SymbolFlags.Value ? SymbolFlags.ExportValue : 0; + const local = declareSymbol(container.locals, /*parent*/ undefined, node, exportKind, symbolExcludes); + local.exportSymbol = declareSymbol(container.symbol.exports!, container.symbol, node, symbolFlags, symbolExcludes); + node.localSymbol = local; + return local; } - else if (containerFlags & ContainerFlags.IsBlockScopedContainer) { - blockScopeContainer = node; - blockScopeContainer.locals = undefined; + else { + return declareSymbol(container.locals!, /*parent*/ undefined, node, symbolFlags, symbolExcludes); } - if (containerFlags & ContainerFlags.IsControlFlowContainer) { - const saveCurrentFlow = currentFlow; - const saveBreakTarget = currentBreakTarget; - const saveContinueTarget = currentContinueTarget; - const saveReturnTarget = currentReturnTarget; - const saveExceptionTarget = currentExceptionTarget; - const saveActiveLabelList = activeLabelList; - const saveHasExplicitReturn = hasExplicitReturn; - const isIIFE = containerFlags & ContainerFlags.IsFunctionExpression && !hasModifier(node, ModifierFlags.Async) && - !(node).asteriskToken && !!getImmediatelyInvokedFunctionExpression(node); - // A non-async, non-generator IIFE is considered part of the containing control flow. Return statements behave - // similarly to break statements that exit to a label just past the statement body. - if (!isIIFE) { - currentFlow = initFlowNode({ flags: FlowFlags.Start }); - if (containerFlags & (ContainerFlags.IsFunctionExpression | ContainerFlags.IsObjectLiteralOrClassExpressionMethod)) { - currentFlow.node = node; - } - } - // We create a return control flow graph for IIFEs and constructors. For constructors - // we use the return control flow graph in strict property initialization checks. - currentReturnTarget = isIIFE || node.kind === SyntaxKind.Constructor ? createBranchLabel() : undefined; - currentExceptionTarget = undefined; - currentBreakTarget = undefined; - currentContinueTarget = undefined; - activeLabelList = undefined; - hasExplicitReturn = false; - bindChildren(node); - // Reset all reachability check related flags on node (for incremental scenarios) - node.flags &= ~NodeFlags.ReachabilityAndEmitFlags; - if (!(currentFlow.flags & FlowFlags.Unreachable) && containerFlags & ContainerFlags.IsFunctionLike && nodeIsPresent((node).body)) { - node.flags |= NodeFlags.HasImplicitReturn; - if (hasExplicitReturn) node.flags |= NodeFlags.HasExplicitReturn; - (node).endFlowNode = currentFlow; - } - if (node.kind === SyntaxKind.SourceFile) { - node.flags |= emitFlags; - } - - if (currentReturnTarget) { - addAntecedent(currentReturnTarget, currentFlow); - currentFlow = finishFlowLabel(currentReturnTarget); - if (node.kind === SyntaxKind.Constructor) { - (node).returnFlowNode = currentFlow; - } - } - if (!isIIFE) { - currentFlow = saveCurrentFlow; + } + } + // All container nodes are kept on a linked list in declaration order. This list is used by + // the getLocalNameOfContainer function in the type checker to validate that the local name + // used for a container is unique. + function bindContainer(node: Node, containerFlags: ContainerFlags) { + // Before we recurse into a node's children, we first save the existing parent, container + // and block-container. Then after we pop out of processing the children, we restore + // these saved values. + const saveContainer = container; + const saveThisParentContainer = thisParentContainer; + const savedBlockScopeContainer = blockScopeContainer; + // Depending on what kind of node this is, we may have to adjust the current container + // and block-container. If the current node is a container, then it is automatically + // considered the current block-container as well. Also, for containers that we know + // may contain locals, we eagerly initialize the .locals field. We do this because + // it's highly likely that the .locals will be needed to place some child in (for example, + // a parameter, or variable declaration). + // + // However, we do not proactively create the .locals for block-containers because it's + // totally normal and common for block-containers to never actually have a block-scoped + // variable in them. We don't want to end up allocating an object for every 'block' we + // run into when most of them won't be necessary. + // + // Finally, if this is a block-container, then we clear out any existing .locals object + // it may contain within it. This happens in incremental scenarios. Because we can be + // reusing a node from a previous compilation, that node may have had 'locals' created + // for it. We must clear this so we don't accidentally move any stale data forward from + // a previous compilation. + if (containerFlags & ContainerFlags.IsContainer) { + if (node.kind !== SyntaxKind.ArrowFunction) { + thisParentContainer = container; + } + container = blockScopeContainer = node; + if (containerFlags & ContainerFlags.HasLocals) { + container.locals = createSymbolTable(); + } + addToContainerChain(container); + } + else if (containerFlags & ContainerFlags.IsBlockScopedContainer) { + blockScopeContainer = node; + blockScopeContainer.locals = undefined; + } + if (containerFlags & ContainerFlags.IsControlFlowContainer) { + const saveCurrentFlow = currentFlow; + const saveBreakTarget = currentBreakTarget; + const saveContinueTarget = currentContinueTarget; + const saveReturnTarget = currentReturnTarget; + const saveExceptionTarget = currentExceptionTarget; + const saveActiveLabelList = activeLabelList; + const saveHasExplicitReturn = hasExplicitReturn; + const isIIFE = containerFlags & ContainerFlags.IsFunctionExpression && !hasModifier(node, ModifierFlags.Async) && + !(node).asteriskToken && !!getImmediatelyInvokedFunctionExpression(node); + // A non-async, non-generator IIFE is considered part of the containing control flow. Return statements behave + // similarly to break statements that exit to a label just past the statement body. + if (!isIIFE) { + currentFlow = initFlowNode({ flags: FlowFlags.Start }); + if (containerFlags & (ContainerFlags.IsFunctionExpression | ContainerFlags.IsObjectLiteralOrClassExpressionMethod)) { + currentFlow.node = (node); } - currentBreakTarget = saveBreakTarget; - currentContinueTarget = saveContinueTarget; - currentReturnTarget = saveReturnTarget; - currentExceptionTarget = saveExceptionTarget; - activeLabelList = saveActiveLabelList; - hasExplicitReturn = saveHasExplicitReturn; } - else if (containerFlags & ContainerFlags.IsInterface) { - seenThisKeyword = false; - bindChildren(node); - node.flags = seenThisKeyword ? node.flags | NodeFlags.ContainsThis : node.flags & ~NodeFlags.ContainsThis; - } - else { - bindChildren(node); + // We create a return control flow graph for IIFEs and constructors. For constructors + // we use the return control flow graph in strict property initialization checks. + currentReturnTarget = isIIFE || node.kind === SyntaxKind.Constructor ? createBranchLabel() : undefined; + currentExceptionTarget = undefined; + currentBreakTarget = undefined; + currentContinueTarget = undefined; + activeLabelList = undefined; + hasExplicitReturn = false; + bindChildren(node); + // Reset all reachability check related flags on node (for incremental scenarios) + node.flags &= ~NodeFlags.ReachabilityAndEmitFlags; + if (!(currentFlow.flags & FlowFlags.Unreachable) && containerFlags & ContainerFlags.IsFunctionLike && nodeIsPresent((node).body)) { + node.flags |= NodeFlags.HasImplicitReturn; + if (hasExplicitReturn) + node.flags |= NodeFlags.HasExplicitReturn; + (node).endFlowNode = currentFlow; } - - container = saveContainer; - thisParentContainer = saveThisParentContainer; - blockScopeContainer = savedBlockScopeContainer; - } - - function bindChildren(node: Node): void { - if (skipTransformFlagAggregation) { - bindChildrenWorker(node); + if (node.kind === SyntaxKind.SourceFile) { + node.flags |= emitFlags; } - else if (node.transformFlags & TransformFlags.HasComputedFlags) { - skipTransformFlagAggregation = true; - bindChildrenWorker(node); - skipTransformFlagAggregation = false; - subtreeTransformFlags |= node.transformFlags & ~getTransformFlagsSubtreeExclusions(node.kind); + if (currentReturnTarget) { + addAntecedent(currentReturnTarget, currentFlow); + currentFlow = finishFlowLabel(currentReturnTarget); + if (node.kind === SyntaxKind.Constructor) { + (node).returnFlowNode = currentFlow; + } } - else { - const savedSubtreeTransformFlags = subtreeTransformFlags; - subtreeTransformFlags = 0; - bindChildrenWorker(node); - subtreeTransformFlags = savedSubtreeTransformFlags | computeTransformFlagsForNode(node, subtreeTransformFlags); + if (!isIIFE) { + currentFlow = saveCurrentFlow; } + currentBreakTarget = saveBreakTarget; + currentContinueTarget = saveContinueTarget; + currentReturnTarget = saveReturnTarget; + currentExceptionTarget = saveExceptionTarget; + activeLabelList = saveActiveLabelList; + hasExplicitReturn = saveHasExplicitReturn; } - - function bindEachFunctionsFirst(nodes: NodeArray | undefined): void { - bindEach(nodes, n => n.kind === SyntaxKind.FunctionDeclaration ? bind(n) : undefined); - bindEach(nodes, n => n.kind !== SyntaxKind.FunctionDeclaration ? bind(n) : undefined); + else if (containerFlags & ContainerFlags.IsInterface) { + seenThisKeyword = false; + bindChildren(node); + node.flags = seenThisKeyword ? node.flags | NodeFlags.ContainsThis : node.flags & ~NodeFlags.ContainsThis; } - - function bindEach(nodes: NodeArray | undefined, bindFunction: (node: Node) => void = bind): void { - if (nodes === undefined) { - return; - } - - if (skipTransformFlagAggregation) { - forEach(nodes, bindFunction); - } - else { - const savedSubtreeTransformFlags = subtreeTransformFlags; - subtreeTransformFlags = TransformFlags.None; - let nodeArrayFlags = TransformFlags.None; - for (const node of nodes) { - bindFunction(node); - nodeArrayFlags |= node.transformFlags & ~TransformFlags.HasComputedFlags; - } - nodes.transformFlags = nodeArrayFlags | TransformFlags.HasComputedFlags; - subtreeTransformFlags |= savedSubtreeTransformFlags; - } + else { + bindChildren(node); } - - function bindEachChild(node: Node) { - forEachChild(node, bind, bindEach); + container = saveContainer; + thisParentContainer = saveThisParentContainer; + blockScopeContainer = savedBlockScopeContainer; + } + function bindChildren(node: Node): void { + if (skipTransformFlagAggregation) { + bindChildrenWorker(node); } - - function bindChildrenWorker(node: Node): void { - if (checkUnreachable(node)) { - bindEachChild(node); - bindJSDoc(node); - return; - } - if (node.kind >= SyntaxKind.FirstStatement && node.kind <= SyntaxKind.LastStatement && !options.allowUnreachableCode) { - node.flowNode = currentFlow; - } - switch (node.kind) { - case SyntaxKind.WhileStatement: - bindWhileStatement(node); - break; - case SyntaxKind.DoStatement: - bindDoStatement(node); - break; - case SyntaxKind.ForStatement: - bindForStatement(node); - break; - case SyntaxKind.ForInStatement: - case SyntaxKind.ForOfStatement: - bindForInOrForOfStatement(node); - break; - case SyntaxKind.IfStatement: - bindIfStatement(node); - break; - case SyntaxKind.ReturnStatement: - case SyntaxKind.ThrowStatement: - bindReturnOrThrow(node); - break; - case SyntaxKind.BreakStatement: - case SyntaxKind.ContinueStatement: - bindBreakOrContinueStatement(node); - break; - case SyntaxKind.TryStatement: - bindTryStatement(node); - break; - case SyntaxKind.SwitchStatement: - bindSwitchStatement(node); - break; - case SyntaxKind.CaseBlock: - bindCaseBlock(node); - break; - case SyntaxKind.CaseClause: - bindCaseClause(node); - break; - case SyntaxKind.ExpressionStatement: - bindExpressionStatement(node); - break; - case SyntaxKind.LabeledStatement: - bindLabeledStatement(node); - break; - case SyntaxKind.PrefixUnaryExpression: - bindPrefixUnaryExpressionFlow(node); - break; - case SyntaxKind.PostfixUnaryExpression: - bindPostfixUnaryExpressionFlow(node); - break; - case SyntaxKind.BinaryExpression: - bindBinaryExpressionFlow(node); - break; - case SyntaxKind.DeleteExpression: - bindDeleteExpressionFlow(node); - break; - case SyntaxKind.ConditionalExpression: - bindConditionalExpressionFlow(node); - break; - case SyntaxKind.VariableDeclaration: - bindVariableDeclarationFlow(node); - break; - case SyntaxKind.PropertyAccessExpression: - case SyntaxKind.ElementAccessExpression: - bindAccessExpressionFlow(node); - break; - case SyntaxKind.CallExpression: - bindCallExpressionFlow(node); - break; - case SyntaxKind.JSDocTypedefTag: - case SyntaxKind.JSDocCallbackTag: - case SyntaxKind.JSDocEnumTag: - bindJSDocTypeAlias(node as JSDocTypedefTag | JSDocCallbackTag | JSDocEnumTag); - break; - // In source files and blocks, bind functions first to match hoisting that occurs at runtime - case SyntaxKind.SourceFile: { - bindEachFunctionsFirst((node as SourceFile).statements); - bind((node as SourceFile).endOfFileToken); - break; - } - case SyntaxKind.Block: - case SyntaxKind.ModuleBlock: - bindEachFunctionsFirst((node as Block).statements); - break; - default: - bindEachChild(node); - break; - } - bindJSDoc(node); + else if (node.transformFlags & TransformFlags.HasComputedFlags) { + skipTransformFlagAggregation = true; + bindChildrenWorker(node); + skipTransformFlagAggregation = false; + subtreeTransformFlags |= node.transformFlags & ~getTransformFlagsSubtreeExclusions(node.kind); } - - function isNarrowingExpression(expr: Expression): boolean { - switch (expr.kind) { - case SyntaxKind.Identifier: - case SyntaxKind.ThisKeyword: - case SyntaxKind.PropertyAccessExpression: - case SyntaxKind.ElementAccessExpression: - return containsNarrowableReference(expr); - case SyntaxKind.CallExpression: - return hasNarrowableArgument(expr); - case SyntaxKind.ParenthesizedExpression: - return isNarrowingExpression((expr).expression); - case SyntaxKind.BinaryExpression: - return isNarrowingBinaryExpression(expr); - case SyntaxKind.PrefixUnaryExpression: - return (expr).operator === SyntaxKind.ExclamationToken && isNarrowingExpression((expr).operand); - case SyntaxKind.TypeOfExpression: - return isNarrowingExpression((expr).expression); - } - return false; + else { + const savedSubtreeTransformFlags = subtreeTransformFlags; + subtreeTransformFlags = 0; + bindChildrenWorker(node); + subtreeTransformFlags = savedSubtreeTransformFlags | computeTransformFlagsForNode(node, subtreeTransformFlags); } - - function isNarrowableReference(expr: Expression): boolean { - return expr.kind === SyntaxKind.Identifier || expr.kind === SyntaxKind.ThisKeyword || expr.kind === SyntaxKind.SuperKeyword || - (isPropertyAccessExpression(expr) || isNonNullExpression(expr) || isParenthesizedExpression(expr)) && isNarrowableReference(expr.expression) || - isElementAccessExpression(expr) && isStringOrNumericLiteralLike(expr.argumentExpression) && isNarrowableReference(expr.expression); - } - - function containsNarrowableReference(expr: Expression): boolean { - return isNarrowableReference(expr) || isOptionalChain(expr) && containsNarrowableReference(expr.expression); - } - - function hasNarrowableArgument(expr: CallExpression) { - if (expr.arguments) { - for (const argument of expr.arguments) { - if (containsNarrowableReference(argument)) { - return true; - } - } - } - if (expr.expression.kind === SyntaxKind.PropertyAccessExpression && - containsNarrowableReference((expr.expression).expression)) { - return true; - } - return false; + } + function bindEachFunctionsFirst(nodes: NodeArray | undefined): void { + bindEach(nodes, n => n.kind === SyntaxKind.FunctionDeclaration ? bind(n) : undefined); + bindEach(nodes, n => n.kind !== SyntaxKind.FunctionDeclaration ? bind(n) : undefined); + } + function bindEach(nodes: NodeArray | undefined, bindFunction: (node: Node) => void = bind): void { + if (nodes === undefined) { + return; } - - function isNarrowingTypeofOperands(expr1: Expression, expr2: Expression) { - return isTypeOfExpression(expr1) && isNarrowableOperand(expr1.expression) && isStringLiteralLike(expr2); - } - - function isNarrowableInOperands(left: Expression, right: Expression) { - return isStringLiteralLike(left) && isNarrowingExpression(right); - } - - function isNarrowingBinaryExpression(expr: BinaryExpression) { - switch (expr.operatorToken.kind) { - case SyntaxKind.EqualsToken: - return containsNarrowableReference(expr.left); - case SyntaxKind.EqualsEqualsToken: - case SyntaxKind.ExclamationEqualsToken: - case SyntaxKind.EqualsEqualsEqualsToken: - case SyntaxKind.ExclamationEqualsEqualsToken: - return isNarrowableOperand(expr.left) || isNarrowableOperand(expr.right) || - isNarrowingTypeofOperands(expr.right, expr.left) || isNarrowingTypeofOperands(expr.left, expr.right); - case SyntaxKind.InstanceOfKeyword: - return isNarrowableOperand(expr.left); - case SyntaxKind.InKeyword: - return isNarrowableInOperands(expr.left, expr.right); - case SyntaxKind.CommaToken: - return isNarrowingExpression(expr.right); - } - return false; + if (skipTransformFlagAggregation) { + forEach(nodes, bindFunction); } - - function isNarrowableOperand(expr: Expression): boolean { - switch (expr.kind) { - case SyntaxKind.ParenthesizedExpression: - return isNarrowableOperand((expr).expression); - case SyntaxKind.BinaryExpression: - switch ((expr).operatorToken.kind) { - case SyntaxKind.EqualsToken: - return isNarrowableOperand((expr).left); - case SyntaxKind.CommaToken: - return isNarrowableOperand((expr).right); - } - } - return containsNarrowableReference(expr); - } - - function createBranchLabel(): FlowLabel { - return initFlowNode({ flags: FlowFlags.BranchLabel, antecedents: undefined }); - } - - function createLoopLabel(): FlowLabel { - return initFlowNode({ flags: FlowFlags.LoopLabel, antecedents: undefined }); - } - - function createReduceLabel(target: FlowLabel, antecedents: FlowNode[], antecedent: FlowNode): FlowReduceLabel { - return initFlowNode({ flags: FlowFlags.ReduceLabel, target, antecedents, antecedent }); - } - - function setFlowNodeReferenced(flow: FlowNode) { - // On first reference we set the Referenced flag, thereafter we set the Shared flag - flow.flags |= flow.flags & FlowFlags.Referenced ? FlowFlags.Shared : FlowFlags.Referenced; - } - - function addAntecedent(label: FlowLabel, antecedent: FlowNode): void { - if (!(antecedent.flags & FlowFlags.Unreachable) && !contains(label.antecedents, antecedent)) { - (label.antecedents || (label.antecedents = [])).push(antecedent); - setFlowNodeReferenced(antecedent); + else { + const savedSubtreeTransformFlags = subtreeTransformFlags; + subtreeTransformFlags = TransformFlags.None; + let nodeArrayFlags = TransformFlags.None; + for (const node of nodes) { + bindFunction(node); + nodeArrayFlags |= node.transformFlags & ~TransformFlags.HasComputedFlags; } + nodes.transformFlags = nodeArrayFlags | TransformFlags.HasComputedFlags; + subtreeTransformFlags |= savedSubtreeTransformFlags; } - - function createFlowCondition(flags: FlowFlags, antecedent: FlowNode, expression: Expression | undefined): FlowNode { - if (antecedent.flags & FlowFlags.Unreachable) { - return antecedent; - } - if (!expression) { - return flags & FlowFlags.TrueCondition ? antecedent : unreachableFlow; - } - if ((expression.kind === SyntaxKind.TrueKeyword && flags & FlowFlags.FalseCondition || - expression.kind === SyntaxKind.FalseKeyword && flags & FlowFlags.TrueCondition) && - !isExpressionOfOptionalChainRoot(expression) && !isNullishCoalesce(expression.parent)) { - return unreachableFlow; - } - if (!isNarrowingExpression(expression)) { - return antecedent; - } - setFlowNodeReferenced(antecedent); - return initFlowNode({ flags, antecedent, node: expression }); + } + function bindEachChild(node: Node) { + forEachChild(node, bind, bindEach); + } + function bindChildrenWorker(node: Node): void { + if (checkUnreachable(node)) { + bindEachChild(node); + bindJSDoc(node); + return; } - - function createFlowSwitchClause(antecedent: FlowNode, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number): FlowNode { - setFlowNodeReferenced(antecedent); - return initFlowNode({ flags: FlowFlags.SwitchClause, antecedent, switchStatement, clauseStart, clauseEnd }); + if (node.kind >= SyntaxKind.FirstStatement && node.kind <= SyntaxKind.LastStatement && !options.allowUnreachableCode) { + node.flowNode = currentFlow; } - - function createFlowMutation(flags: FlowFlags, antecedent: FlowNode, node: Node): FlowNode { - setFlowNodeReferenced(antecedent); - const result = initFlowNode({ flags, antecedent, node }); - if (currentExceptionTarget) { - addAntecedent(currentExceptionTarget, result); + switch (node.kind) { + case SyntaxKind.WhileStatement: + bindWhileStatement((node)); + break; + case SyntaxKind.DoStatement: + bindDoStatement((node)); + break; + case SyntaxKind.ForStatement: + bindForStatement((node)); + break; + case SyntaxKind.ForInStatement: + case SyntaxKind.ForOfStatement: + bindForInOrForOfStatement((node)); + break; + case SyntaxKind.IfStatement: + bindIfStatement((node)); + break; + case SyntaxKind.ReturnStatement: + case SyntaxKind.ThrowStatement: + bindReturnOrThrow((node)); + break; + case SyntaxKind.BreakStatement: + case SyntaxKind.ContinueStatement: + bindBreakOrContinueStatement((node)); + break; + case SyntaxKind.TryStatement: + bindTryStatement((node)); + break; + case SyntaxKind.SwitchStatement: + bindSwitchStatement((node)); + break; + case SyntaxKind.CaseBlock: + bindCaseBlock((node)); + break; + case SyntaxKind.CaseClause: + bindCaseClause((node)); + break; + case SyntaxKind.ExpressionStatement: + bindExpressionStatement((node)); + break; + case SyntaxKind.LabeledStatement: + bindLabeledStatement((node)); + break; + case SyntaxKind.PrefixUnaryExpression: + bindPrefixUnaryExpressionFlow((node)); + break; + case SyntaxKind.PostfixUnaryExpression: + bindPostfixUnaryExpressionFlow((node)); + break; + case SyntaxKind.BinaryExpression: + bindBinaryExpressionFlow((node)); + break; + case SyntaxKind.DeleteExpression: + bindDeleteExpressionFlow((node)); + break; + case SyntaxKind.ConditionalExpression: + bindConditionalExpressionFlow((node)); + break; + case SyntaxKind.VariableDeclaration: + bindVariableDeclarationFlow((node)); + break; + case SyntaxKind.PropertyAccessExpression: + case SyntaxKind.ElementAccessExpression: + bindAccessExpressionFlow((node)); + break; + case SyntaxKind.CallExpression: + bindCallExpressionFlow((node)); + break; + case SyntaxKind.JSDocTypedefTag: + case SyntaxKind.JSDocCallbackTag: + case SyntaxKind.JSDocEnumTag: + bindJSDocTypeAlias((node as JSDocTypedefTag | JSDocCallbackTag | JSDocEnumTag)); + break; + // In source files and blocks, bind functions first to match hoisting that occurs at runtime + case SyntaxKind.SourceFile: { + bindEachFunctionsFirst((node as SourceFile).statements); + bind((node as SourceFile).endOfFileToken); + break; } - return result; + case SyntaxKind.Block: + case SyntaxKind.ModuleBlock: + bindEachFunctionsFirst((node as Block).statements); + break; + default: + bindEachChild(node); + break; } - - function createFlowCall(antecedent: FlowNode, node: CallExpression): FlowNode { - setFlowNodeReferenced(antecedent); - return initFlowNode({ flags: FlowFlags.Call, antecedent, node }); + bindJSDoc(node); + } + function isNarrowingExpression(expr: Expression): boolean { + switch (expr.kind) { + case SyntaxKind.Identifier: + case SyntaxKind.ThisKeyword: + case SyntaxKind.PropertyAccessExpression: + case SyntaxKind.ElementAccessExpression: + return containsNarrowableReference(expr); + case SyntaxKind.CallExpression: + return hasNarrowableArgument((expr)); + case SyntaxKind.ParenthesizedExpression: + return isNarrowingExpression((expr).expression); + case SyntaxKind.BinaryExpression: + return isNarrowingBinaryExpression((expr)); + case SyntaxKind.PrefixUnaryExpression: + return (expr).operator === SyntaxKind.ExclamationToken && isNarrowingExpression((expr).operand); + case SyntaxKind.TypeOfExpression: + return isNarrowingExpression((expr).expression); } - - function finishFlowLabel(flow: FlowLabel): FlowNode { - const antecedents = flow.antecedents; - if (!antecedents) { - return unreachableFlow; - } - if (antecedents.length === 1) { - return antecedents[0]; - } - return flow; - } - - function isStatementCondition(node: Node) { - const parent = node.parent; - switch (parent.kind) { - case SyntaxKind.IfStatement: - case SyntaxKind.WhileStatement: - case SyntaxKind.DoStatement: - return (parent).expression === node; - case SyntaxKind.ForStatement: - case SyntaxKind.ConditionalExpression: - return (parent).condition === node; + return false; + } + function isNarrowableReference(expr: Expression): boolean { + return expr.kind === SyntaxKind.Identifier || expr.kind === SyntaxKind.ThisKeyword || expr.kind === SyntaxKind.SuperKeyword || + (isPropertyAccessExpression(expr) || isNonNullExpression(expr) || isParenthesizedExpression(expr)) && isNarrowableReference(expr.expression) || + isElementAccessExpression(expr) && isStringOrNumericLiteralLike(expr.argumentExpression) && isNarrowableReference(expr.expression); + } + function containsNarrowableReference(expr: Expression): boolean { + return isNarrowableReference(expr) || isOptionalChain(expr) && containsNarrowableReference(expr.expression); + } + function hasNarrowableArgument(expr: CallExpression) { + if (expr.arguments) { + for (const argument of expr.arguments) { + if (containsNarrowableReference(argument)) { + return true; + } } - return false; } - - function isLogicalExpression(node: Node) { - while (true) { - if (node.kind === SyntaxKind.ParenthesizedExpression) { - node = (node).expression; - } - else if (node.kind === SyntaxKind.PrefixUnaryExpression && (node).operator === SyntaxKind.ExclamationToken) { - node = (node).operand; - } - else { - return node.kind === SyntaxKind.BinaryExpression && ( - (node).operatorToken.kind === SyntaxKind.AmpersandAmpersandToken || - (node).operatorToken.kind === SyntaxKind.BarBarToken || - (node).operatorToken.kind === SyntaxKind.QuestionQuestionToken); + if (expr.expression.kind === SyntaxKind.PropertyAccessExpression && + containsNarrowableReference((expr.expression).expression)) { + return true; + } + return false; + } + function isNarrowingTypeofOperands(expr1: Expression, expr2: Expression) { + return isTypeOfExpression(expr1) && isNarrowableOperand(expr1.expression) && isStringLiteralLike(expr2); + } + function isNarrowableInOperands(left: Expression, right: Expression) { + return isStringLiteralLike(left) && isNarrowingExpression(right); + } + function isNarrowingBinaryExpression(expr: BinaryExpression) { + switch (expr.operatorToken.kind) { + case SyntaxKind.EqualsToken: + return containsNarrowableReference(expr.left); + case SyntaxKind.EqualsEqualsToken: + case SyntaxKind.ExclamationEqualsToken: + case SyntaxKind.EqualsEqualsEqualsToken: + case SyntaxKind.ExclamationEqualsEqualsToken: + return isNarrowableOperand(expr.left) || isNarrowableOperand(expr.right) || + isNarrowingTypeofOperands(expr.right, expr.left) || isNarrowingTypeofOperands(expr.left, expr.right); + case SyntaxKind.InstanceOfKeyword: + return isNarrowableOperand(expr.left); + case SyntaxKind.InKeyword: + return isNarrowableInOperands(expr.left, expr.right); + case SyntaxKind.CommaToken: + return isNarrowingExpression(expr.right); + } + return false; + } + function isNarrowableOperand(expr: Expression): boolean { + switch (expr.kind) { + case SyntaxKind.ParenthesizedExpression: + return isNarrowableOperand((expr).expression); + case SyntaxKind.BinaryExpression: + switch ((expr).operatorToken.kind) { + case SyntaxKind.EqualsToken: + return isNarrowableOperand((expr).left); + case SyntaxKind.CommaToken: + return isNarrowableOperand((expr).right); } - } } - - function isTopLevelLogicalExpression(node: Node): boolean { - while (isParenthesizedExpression(node.parent) || - isPrefixUnaryExpression(node.parent) && node.parent.operator === SyntaxKind.ExclamationToken) { - node = node.parent; - } - return !isStatementCondition(node) && - !isLogicalExpression(node.parent) && - !(isOptionalChain(node.parent) && node.parent.expression === node); - } - - function doWithConditionalBranches(action: (value: T) => void, value: T, trueTarget: FlowLabel, falseTarget: FlowLabel) { - const savedTrueTarget = currentTrueTarget; - const savedFalseTarget = currentFalseTarget; - currentTrueTarget = trueTarget; - currentFalseTarget = falseTarget; - action(value); - currentTrueTarget = savedTrueTarget; - currentFalseTarget = savedFalseTarget; - } - - function bindCondition(node: Expression | undefined, trueTarget: FlowLabel, falseTarget: FlowLabel) { - doWithConditionalBranches(bind, node, trueTarget, falseTarget); - if (!node || !isLogicalExpression(node) && !(isOptionalChain(node) && isOutermostOptionalChain(node))) { - addAntecedent(trueTarget, createFlowCondition(FlowFlags.TrueCondition, currentFlow, node)); - addAntecedent(falseTarget, createFlowCondition(FlowFlags.FalseCondition, currentFlow, node)); - } + return containsNarrowableReference(expr); + } + function createBranchLabel(): FlowLabel { + return initFlowNode({ flags: FlowFlags.BranchLabel, antecedents: undefined }); + } + function createLoopLabel(): FlowLabel { + return initFlowNode({ flags: FlowFlags.LoopLabel, antecedents: undefined }); + } + function createReduceLabel(target: FlowLabel, antecedents: FlowNode[], antecedent: FlowNode): FlowReduceLabel { + return initFlowNode({ flags: FlowFlags.ReduceLabel, target, antecedents, antecedent }); + } + function setFlowNodeReferenced(flow: FlowNode) { + // On first reference we set the Referenced flag, thereafter we set the Shared flag + flow.flags |= flow.flags & FlowFlags.Referenced ? FlowFlags.Shared : FlowFlags.Referenced; + } + function addAntecedent(label: FlowLabel, antecedent: FlowNode): void { + if (!(antecedent.flags & FlowFlags.Unreachable) && !contains(label.antecedents, antecedent)) { + (label.antecedents || (label.antecedents = [])).push(antecedent); + setFlowNodeReferenced(antecedent); } - - function bindIterativeStatement(node: Statement, breakTarget: FlowLabel, continueTarget: FlowLabel): void { - const saveBreakTarget = currentBreakTarget; - const saveContinueTarget = currentContinueTarget; - currentBreakTarget = breakTarget; - currentContinueTarget = continueTarget; - bind(node); - currentBreakTarget = saveBreakTarget; - currentContinueTarget = saveContinueTarget; + } + function createFlowCondition(flags: FlowFlags, antecedent: FlowNode, expression: Expression | undefined): FlowNode { + if (antecedent.flags & FlowFlags.Unreachable) { + return antecedent; } - - function setContinueTarget(node: Node, target: FlowLabel) { - let label = activeLabelList; - while (label && node.parent.kind === SyntaxKind.LabeledStatement) { - label.continueTarget = target; - label = label.next; - node = node.parent; - } - return target; - } - - function bindWhileStatement(node: WhileStatement): void { - const preWhileLabel = setContinueTarget(node, createLoopLabel()); - const preBodyLabel = createBranchLabel(); - const postWhileLabel = createBranchLabel(); - addAntecedent(preWhileLabel, currentFlow); - currentFlow = preWhileLabel; - bindCondition(node.expression, preBodyLabel, postWhileLabel); - currentFlow = finishFlowLabel(preBodyLabel); - bindIterativeStatement(node.statement, postWhileLabel, preWhileLabel); - addAntecedent(preWhileLabel, currentFlow); - currentFlow = finishFlowLabel(postWhileLabel); - } - - function bindDoStatement(node: DoStatement): void { - const preDoLabel = createLoopLabel(); - const preConditionLabel = setContinueTarget(node, createBranchLabel()); - const postDoLabel = createBranchLabel(); - addAntecedent(preDoLabel, currentFlow); - currentFlow = preDoLabel; - bindIterativeStatement(node.statement, postDoLabel, preConditionLabel); - addAntecedent(preConditionLabel, currentFlow); - currentFlow = finishFlowLabel(preConditionLabel); - bindCondition(node.expression, preDoLabel, postDoLabel); - currentFlow = finishFlowLabel(postDoLabel); - } - - function bindForStatement(node: ForStatement): void { - const preLoopLabel = setContinueTarget(node, createLoopLabel()); - const preBodyLabel = createBranchLabel(); - const postLoopLabel = createBranchLabel(); - bind(node.initializer); - addAntecedent(preLoopLabel, currentFlow); - currentFlow = preLoopLabel; - bindCondition(node.condition, preBodyLabel, postLoopLabel); - currentFlow = finishFlowLabel(preBodyLabel); - bindIterativeStatement(node.statement, postLoopLabel, preLoopLabel); - bind(node.incrementor); - addAntecedent(preLoopLabel, currentFlow); - currentFlow = finishFlowLabel(postLoopLabel); - } - - function bindForInOrForOfStatement(node: ForInOrOfStatement): void { - const preLoopLabel = setContinueTarget(node, createLoopLabel()); - const postLoopLabel = createBranchLabel(); - bind(node.expression); - addAntecedent(preLoopLabel, currentFlow); - currentFlow = preLoopLabel; - if (node.kind === SyntaxKind.ForOfStatement) { - bind(node.awaitModifier); + if (!expression) { + return flags & FlowFlags.TrueCondition ? antecedent : unreachableFlow; + } + if ((expression.kind === SyntaxKind.TrueKeyword && flags & FlowFlags.FalseCondition || + expression.kind === SyntaxKind.FalseKeyword && flags & FlowFlags.TrueCondition) && + !isExpressionOfOptionalChainRoot(expression) && !isNullishCoalesce(expression.parent)) { + return unreachableFlow; + } + if (!isNarrowingExpression(expression)) { + return antecedent; + } + setFlowNodeReferenced(antecedent); + return initFlowNode({ flags, antecedent, node: expression }); + } + function createFlowSwitchClause(antecedent: FlowNode, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number): FlowNode { + setFlowNodeReferenced(antecedent); + return initFlowNode({ flags: FlowFlags.SwitchClause, antecedent, switchStatement, clauseStart, clauseEnd }); + } + function createFlowMutation(flags: FlowFlags, antecedent: FlowNode, node: Node): FlowNode { + setFlowNodeReferenced(antecedent); + const result = initFlowNode({ flags, antecedent, node }); + if (currentExceptionTarget) { + addAntecedent(currentExceptionTarget, result); + } + return result; + } + function createFlowCall(antecedent: FlowNode, node: CallExpression): FlowNode { + setFlowNodeReferenced(antecedent); + return initFlowNode({ flags: FlowFlags.Call, antecedent, node }); + } + function finishFlowLabel(flow: FlowLabel): FlowNode { + const antecedents = flow.antecedents; + if (!antecedents) { + return unreachableFlow; + } + if (antecedents.length === 1) { + return antecedents[0]; + } + return flow; + } + function isStatementCondition(node: Node) { + const parent = node.parent; + switch (parent.kind) { + case SyntaxKind.IfStatement: + case SyntaxKind.WhileStatement: + case SyntaxKind.DoStatement: + return (parent).expression === node; + case SyntaxKind.ForStatement: + case SyntaxKind.ConditionalExpression: + return (parent).condition === node; + } + return false; + } + function isLogicalExpression(node: Node) { + while (true) { + if (node.kind === SyntaxKind.ParenthesizedExpression) { + node = (node).expression; } - addAntecedent(postLoopLabel, currentFlow); - bind(node.initializer); - if (node.initializer.kind !== SyntaxKind.VariableDeclarationList) { - bindAssignmentTargetFlow(node.initializer); + else if (node.kind === SyntaxKind.PrefixUnaryExpression && (node).operator === SyntaxKind.ExclamationToken) { + node = (node).operand; } - bindIterativeStatement(node.statement, postLoopLabel, preLoopLabel); - addAntecedent(preLoopLabel, currentFlow); - currentFlow = finishFlowLabel(postLoopLabel); - } - - function bindIfStatement(node: IfStatement): void { - const thenLabel = createBranchLabel(); - const elseLabel = createBranchLabel(); - const postIfLabel = createBranchLabel(); - bindCondition(node.expression, thenLabel, elseLabel); - currentFlow = finishFlowLabel(thenLabel); - bind(node.thenStatement); - addAntecedent(postIfLabel, currentFlow); - currentFlow = finishFlowLabel(elseLabel); - bind(node.elseStatement); - addAntecedent(postIfLabel, currentFlow); - currentFlow = finishFlowLabel(postIfLabel); - } - - function bindReturnOrThrow(node: ReturnStatement | ThrowStatement): void { - bind(node.expression); - if (node.kind === SyntaxKind.ReturnStatement) { - hasExplicitReturn = true; - if (currentReturnTarget) { - addAntecedent(currentReturnTarget, currentFlow); - } + else { + return node.kind === SyntaxKind.BinaryExpression && ((node).operatorToken.kind === SyntaxKind.AmpersandAmpersandToken || + (node).operatorToken.kind === SyntaxKind.BarBarToken || + (node).operatorToken.kind === SyntaxKind.QuestionQuestionToken); } - currentFlow = unreachableFlow; } - - function findActiveLabel(name: __String) { - for (let label = activeLabelList; label; label = label.next) { - if (label.name === name) { - return label; - } - } - return undefined; + } + function isTopLevelLogicalExpression(node: Node): boolean { + while (isParenthesizedExpression(node.parent) || + isPrefixUnaryExpression(node.parent) && node.parent.operator === SyntaxKind.ExclamationToken) { + node = node.parent; + } + return !isStatementCondition(node) && + !isLogicalExpression(node.parent) && + !(isOptionalChain(node.parent) && node.parent.expression === node); + } + function doWithConditionalBranches(action: (value: T) => void, value: T, trueTarget: FlowLabel, falseTarget: FlowLabel) { + const savedTrueTarget = currentTrueTarget; + const savedFalseTarget = currentFalseTarget; + currentTrueTarget = trueTarget; + currentFalseTarget = falseTarget; + action(value); + currentTrueTarget = savedTrueTarget; + currentFalseTarget = savedFalseTarget; + } + function bindCondition(node: Expression | undefined, trueTarget: FlowLabel, falseTarget: FlowLabel) { + doWithConditionalBranches(bind, node, trueTarget, falseTarget); + if (!node || !isLogicalExpression(node) && !(isOptionalChain(node) && isOutermostOptionalChain(node))) { + addAntecedent(trueTarget, createFlowCondition(FlowFlags.TrueCondition, currentFlow, node)); + addAntecedent(falseTarget, createFlowCondition(FlowFlags.FalseCondition, currentFlow, node)); } - - function bindBreakOrContinueFlow(node: BreakOrContinueStatement, breakTarget: FlowLabel | undefined, continueTarget: FlowLabel | undefined) { - const flowLabel = node.kind === SyntaxKind.BreakStatement ? breakTarget : continueTarget; - if (flowLabel) { - addAntecedent(flowLabel, currentFlow); - currentFlow = unreachableFlow; + } + function bindIterativeStatement(node: Statement, breakTarget: FlowLabel, continueTarget: FlowLabel): void { + const saveBreakTarget = currentBreakTarget; + const saveContinueTarget = currentContinueTarget; + currentBreakTarget = breakTarget; + currentContinueTarget = continueTarget; + bind(node); + currentBreakTarget = saveBreakTarget; + currentContinueTarget = saveContinueTarget; + } + function setContinueTarget(node: Node, target: FlowLabel) { + let label = activeLabelList; + while (label && node.parent.kind === SyntaxKind.LabeledStatement) { + label.continueTarget = target; + label = label.next; + node = node.parent; + } + return target; + } + function bindWhileStatement(node: WhileStatement): void { + const preWhileLabel = setContinueTarget(node, createLoopLabel()); + const preBodyLabel = createBranchLabel(); + const postWhileLabel = createBranchLabel(); + addAntecedent(preWhileLabel, currentFlow); + currentFlow = preWhileLabel; + bindCondition(node.expression, preBodyLabel, postWhileLabel); + currentFlow = finishFlowLabel(preBodyLabel); + bindIterativeStatement(node.statement, postWhileLabel, preWhileLabel); + addAntecedent(preWhileLabel, currentFlow); + currentFlow = finishFlowLabel(postWhileLabel); + } + function bindDoStatement(node: DoStatement): void { + const preDoLabel = createLoopLabel(); + const preConditionLabel = setContinueTarget(node, createBranchLabel()); + const postDoLabel = createBranchLabel(); + addAntecedent(preDoLabel, currentFlow); + currentFlow = preDoLabel; + bindIterativeStatement(node.statement, postDoLabel, preConditionLabel); + addAntecedent(preConditionLabel, currentFlow); + currentFlow = finishFlowLabel(preConditionLabel); + bindCondition(node.expression, preDoLabel, postDoLabel); + currentFlow = finishFlowLabel(postDoLabel); + } + function bindForStatement(node: ForStatement): void { + const preLoopLabel = setContinueTarget(node, createLoopLabel()); + const preBodyLabel = createBranchLabel(); + const postLoopLabel = createBranchLabel(); + bind(node.initializer); + addAntecedent(preLoopLabel, currentFlow); + currentFlow = preLoopLabel; + bindCondition(node.condition, preBodyLabel, postLoopLabel); + currentFlow = finishFlowLabel(preBodyLabel); + bindIterativeStatement(node.statement, postLoopLabel, preLoopLabel); + bind(node.incrementor); + addAntecedent(preLoopLabel, currentFlow); + currentFlow = finishFlowLabel(postLoopLabel); + } + function bindForInOrForOfStatement(node: ForInOrOfStatement): void { + const preLoopLabel = setContinueTarget(node, createLoopLabel()); + const postLoopLabel = createBranchLabel(); + bind(node.expression); + addAntecedent(preLoopLabel, currentFlow); + currentFlow = preLoopLabel; + if (node.kind === SyntaxKind.ForOfStatement) { + bind(node.awaitModifier); + } + addAntecedent(postLoopLabel, currentFlow); + bind(node.initializer); + if (node.initializer.kind !== SyntaxKind.VariableDeclarationList) { + bindAssignmentTargetFlow(node.initializer); + } + bindIterativeStatement(node.statement, postLoopLabel, preLoopLabel); + addAntecedent(preLoopLabel, currentFlow); + currentFlow = finishFlowLabel(postLoopLabel); + } + function bindIfStatement(node: IfStatement): void { + const thenLabel = createBranchLabel(); + const elseLabel = createBranchLabel(); + const postIfLabel = createBranchLabel(); + bindCondition(node.expression, thenLabel, elseLabel); + currentFlow = finishFlowLabel(thenLabel); + bind(node.thenStatement); + addAntecedent(postIfLabel, currentFlow); + currentFlow = finishFlowLabel(elseLabel); + bind(node.elseStatement); + addAntecedent(postIfLabel, currentFlow); + currentFlow = finishFlowLabel(postIfLabel); + } + function bindReturnOrThrow(node: ReturnStatement | ThrowStatement): void { + bind(node.expression); + if (node.kind === SyntaxKind.ReturnStatement) { + hasExplicitReturn = true; + if (currentReturnTarget) { + addAntecedent(currentReturnTarget, currentFlow); } } - - function bindBreakOrContinueStatement(node: BreakOrContinueStatement): void { - bind(node.label); - if (node.label) { - const activeLabel = findActiveLabel(node.label.escapedText); - if (activeLabel) { - activeLabel.referenced = true; - bindBreakOrContinueFlow(node, activeLabel.breakTarget, activeLabel.continueTarget); - } - } - else { - bindBreakOrContinueFlow(node, currentBreakTarget, currentContinueTarget); + currentFlow = unreachableFlow; + } + function findActiveLabel(name: __String) { + for (let label = activeLabelList; label; label = label.next) { + if (label.name === name) { + return label; } } - - function bindTryStatement(node: TryStatement): void { - // We conservatively assume that *any* code in the try block can cause an exception, but we only need - // to track code that causes mutations (because only mutations widen the possible control flow type of - // a variable). The exceptionLabel is the target label for control flows that result from exceptions. - // We add all mutation flow nodes as antecedents of this label such that we can analyze them as possible - // antecedents of the start of catch or finally blocks. Furthermore, we add the current control flow to - // represent exceptions that occur before any mutations. - const saveReturnTarget = currentReturnTarget; - const saveExceptionTarget = currentExceptionTarget; - const normalExitLabel = createBranchLabel(); - const returnLabel = createBranchLabel(); - let exceptionLabel = createBranchLabel(); - if (node.finallyBlock) { - currentReturnTarget = returnLabel; + return undefined; + } + function bindBreakOrContinueFlow(node: BreakOrContinueStatement, breakTarget: FlowLabel | undefined, continueTarget: FlowLabel | undefined) { + const flowLabel = node.kind === SyntaxKind.BreakStatement ? breakTarget : continueTarget; + if (flowLabel) { + addAntecedent(flowLabel, currentFlow); + currentFlow = unreachableFlow; + } + } + function bindBreakOrContinueStatement(node: BreakOrContinueStatement): void { + bind(node.label); + if (node.label) { + const activeLabel = findActiveLabel(node.label.escapedText); + if (activeLabel) { + activeLabel.referenced = true; + bindBreakOrContinueFlow(node, activeLabel.breakTarget, activeLabel.continueTarget); } + } + else { + bindBreakOrContinueFlow(node, currentBreakTarget, currentContinueTarget); + } + } + function bindTryStatement(node: TryStatement): void { + // We conservatively assume that *any* code in the try block can cause an exception, but we only need + // to track code that causes mutations (because only mutations widen the possible control flow type of + // a variable). The exceptionLabel is the target label for control flows that result from exceptions. + // We add all mutation flow nodes as antecedents of this label such that we can analyze them as possible + // antecedents of the start of catch or finally blocks. Furthermore, we add the current control flow to + // represent exceptions that occur before any mutations. + const saveReturnTarget = currentReturnTarget; + const saveExceptionTarget = currentExceptionTarget; + const normalExitLabel = createBranchLabel(); + const returnLabel = createBranchLabel(); + let exceptionLabel = createBranchLabel(); + if (node.finallyBlock) { + currentReturnTarget = returnLabel; + } + addAntecedent(exceptionLabel, currentFlow); + currentExceptionTarget = exceptionLabel; + bind(node.tryBlock); + addAntecedent(normalExitLabel, currentFlow); + if (node.catchClause) { + // Start of catch clause is the target of exceptions from try block. + currentFlow = finishFlowLabel(exceptionLabel); + // The currentExceptionTarget now represents control flows from exceptions in the catch clause. + // Effectively, in a try-catch-finally, if an exception occurs in the try block, the catch block + // acts like a second try block. + exceptionLabel = createBranchLabel(); addAntecedent(exceptionLabel, currentFlow); currentExceptionTarget = exceptionLabel; - bind(node.tryBlock); + bind(node.catchClause); addAntecedent(normalExitLabel, currentFlow); - if (node.catchClause) { - // Start of catch clause is the target of exceptions from try block. - currentFlow = finishFlowLabel(exceptionLabel); - // The currentExceptionTarget now represents control flows from exceptions in the catch clause. - // Effectively, in a try-catch-finally, if an exception occurs in the try block, the catch block - // acts like a second try block. - exceptionLabel = createBranchLabel(); - addAntecedent(exceptionLabel, currentFlow); - currentExceptionTarget = exceptionLabel; - bind(node.catchClause); - addAntecedent(normalExitLabel, currentFlow); - } - currentReturnTarget = saveReturnTarget; - currentExceptionTarget = saveExceptionTarget; - if (node.finallyBlock) { - // Possible ways control can reach the finally block: - // 1) Normal completion of try block of a try-finally or try-catch-finally - // 2) Normal completion of catch block (following exception in try block) of a try-catch-finally - // 3) Return in try or catch block of a try-finally or try-catch-finally - // 4) Exception in try block of a try-finally - // 5) Exception in catch block of a try-catch-finally - // When analyzing a control flow graph that starts inside a finally block we want to consider all - // five possibilities above. However, when analyzing a control flow graph that starts outside (past) - // the finally block, we only want to consider the first two (if we're past a finally block then it - // must have completed normally). Likewise, when analyzing a control flow graph from return statements - // in try or catch blocks in an IIFE, we only want to consider the third. To make this possible, we - // inject a ReduceLabel node into the control flow graph. This node contains an alternate reduced - // set of antecedents for the pre-finally label. As control flow analysis passes by a ReduceLabel - // node, the pre-finally label is temporarily switched to the reduced antecedent set. - const finallyLabel = createBranchLabel(); - finallyLabel.antecedents = concatenate(concatenate(normalExitLabel.antecedents, exceptionLabel.antecedents), returnLabel.antecedents); - currentFlow = finallyLabel; - bind(node.finallyBlock); - if (currentFlow.flags & FlowFlags.Unreachable) { - // If the end of the finally block is unreachable, the end of the entire try statement is unreachable. - currentFlow = unreachableFlow; - } - else { - // If we have an IIFE return target and return statements in the try or catch blocks, add a control - // flow that goes back through the finally block and back through only the return statements. - if (currentReturnTarget && returnLabel.antecedents) { - addAntecedent(currentReturnTarget, createReduceLabel(finallyLabel, returnLabel.antecedents, currentFlow)); - } - // If the end of the finally block is reachable, but the end of the try and catch blocks are not, - // convert the current flow to unreachable. For example, 'try { return 1; } finally { ... }' should - // result in an unreachable current control flow. - currentFlow = normalExitLabel.antecedents ? createReduceLabel(finallyLabel, normalExitLabel.antecedents, currentFlow) : unreachableFlow; - } + } + currentReturnTarget = saveReturnTarget; + currentExceptionTarget = saveExceptionTarget; + if (node.finallyBlock) { + // Possible ways control can reach the finally block: + // 1) Normal completion of try block of a try-finally or try-catch-finally + // 2) Normal completion of catch block (following exception in try block) of a try-catch-finally + // 3) Return in try or catch block of a try-finally or try-catch-finally + // 4) Exception in try block of a try-finally + // 5) Exception in catch block of a try-catch-finally + // When analyzing a control flow graph that starts inside a finally block we want to consider all + // five possibilities above. However, when analyzing a control flow graph that starts outside (past) + // the finally block, we only want to consider the first two (if we're past a finally block then it + // must have completed normally). Likewise, when analyzing a control flow graph from return statements + // in try or catch blocks in an IIFE, we only want to consider the third. To make this possible, we + // inject a ReduceLabel node into the control flow graph. This node contains an alternate reduced + // set of antecedents for the pre-finally label. As control flow analysis passes by a ReduceLabel + // node, the pre-finally label is temporarily switched to the reduced antecedent set. + const finallyLabel = createBranchLabel(); + finallyLabel.antecedents = concatenate(concatenate(normalExitLabel.antecedents, exceptionLabel.antecedents), returnLabel.antecedents); + currentFlow = finallyLabel; + bind(node.finallyBlock); + if (currentFlow.flags & FlowFlags.Unreachable) { + // If the end of the finally block is unreachable, the end of the entire try statement is unreachable. + currentFlow = unreachableFlow; } else { - currentFlow = finishFlowLabel(normalExitLabel); + // If we have an IIFE return target and return statements in the try or catch blocks, add a control + // flow that goes back through the finally block and back through only the return statements. + if (currentReturnTarget && returnLabel.antecedents) { + addAntecedent(currentReturnTarget, createReduceLabel(finallyLabel, returnLabel.antecedents, currentFlow)); + } + // If the end of the finally block is reachable, but the end of the try and catch blocks are not, + // convert the current flow to unreachable. For example, 'try { return 1; } finally { ... }' should + // result in an unreachable current control flow. + currentFlow = normalExitLabel.antecedents ? createReduceLabel(finallyLabel, normalExitLabel.antecedents, currentFlow) : unreachableFlow; } } - - function bindSwitchStatement(node: SwitchStatement): void { - const postSwitchLabel = createBranchLabel(); - bind(node.expression); - const saveBreakTarget = currentBreakTarget; - const savePreSwitchCaseFlow = preSwitchCaseFlow; - currentBreakTarget = postSwitchLabel; - preSwitchCaseFlow = currentFlow; - bind(node.caseBlock); - addAntecedent(postSwitchLabel, currentFlow); - const hasDefault = forEach(node.caseBlock.clauses, c => c.kind === SyntaxKind.DefaultClause); - // We mark a switch statement as possibly exhaustive if it has no default clause and if all - // case clauses have unreachable end points (e.g. they all return). Note, we no longer need - // this property in control flow analysis, it's there only for backwards compatibility. - node.possiblyExhaustive = !hasDefault && !postSwitchLabel.antecedents; - if (!hasDefault) { - addAntecedent(postSwitchLabel, createFlowSwitchClause(preSwitchCaseFlow, node, 0, 0)); - } - currentBreakTarget = saveBreakTarget; - preSwitchCaseFlow = savePreSwitchCaseFlow; - currentFlow = finishFlowLabel(postSwitchLabel); + else { + currentFlow = finishFlowLabel(normalExitLabel); } - - function bindCaseBlock(node: CaseBlock): void { - const savedSubtreeTransformFlags = subtreeTransformFlags; - subtreeTransformFlags = 0; - const clauses = node.clauses; - const isNarrowingSwitch = isNarrowingExpression(node.parent.expression); - let fallthroughFlow = unreachableFlow; - for (let i = 0; i < clauses.length; i++) { - const clauseStart = i; - while (!clauses[i].statements.length && i + 1 < clauses.length) { - bind(clauses[i]); - i++; - } - const preCaseLabel = createBranchLabel(); - addAntecedent(preCaseLabel, isNarrowingSwitch ? createFlowSwitchClause(preSwitchCaseFlow!, node.parent, clauseStart, i + 1) : preSwitchCaseFlow!); - addAntecedent(preCaseLabel, fallthroughFlow); - currentFlow = finishFlowLabel(preCaseLabel); - const clause = clauses[i]; - bind(clause); - fallthroughFlow = currentFlow; - if (!(currentFlow.flags & FlowFlags.Unreachable) && i !== clauses.length - 1 && options.noFallthroughCasesInSwitch) { - clause.fallthroughFlowNode = currentFlow; - } + } + function bindSwitchStatement(node: SwitchStatement): void { + const postSwitchLabel = createBranchLabel(); + bind(node.expression); + const saveBreakTarget = currentBreakTarget; + const savePreSwitchCaseFlow = preSwitchCaseFlow; + currentBreakTarget = postSwitchLabel; + preSwitchCaseFlow = currentFlow; + bind(node.caseBlock); + addAntecedent(postSwitchLabel, currentFlow); + const hasDefault = forEach(node.caseBlock.clauses, c => c.kind === SyntaxKind.DefaultClause); + // We mark a switch statement as possibly exhaustive if it has no default clause and if all + // case clauses have unreachable end points (e.g. they all return). Note, we no longer need + // this property in control flow analysis, it's there only for backwards compatibility. + node.possiblyExhaustive = !hasDefault && !postSwitchLabel.antecedents; + if (!hasDefault) { + addAntecedent(postSwitchLabel, createFlowSwitchClause(preSwitchCaseFlow, node, 0, 0)); + } + currentBreakTarget = saveBreakTarget; + preSwitchCaseFlow = savePreSwitchCaseFlow; + currentFlow = finishFlowLabel(postSwitchLabel); + } + function bindCaseBlock(node: CaseBlock): void { + const savedSubtreeTransformFlags = subtreeTransformFlags; + subtreeTransformFlags = 0; + const clauses = node.clauses; + const isNarrowingSwitch = isNarrowingExpression(node.parent.expression); + let fallthroughFlow = unreachableFlow; + for (let i = 0; i < clauses.length; i++) { + const clauseStart = i; + while (!clauses[i].statements.length && i + 1 < clauses.length) { + bind(clauses[i]); + i++; + } + const preCaseLabel = createBranchLabel(); + addAntecedent(preCaseLabel, isNarrowingSwitch ? createFlowSwitchClause(preSwitchCaseFlow!, node.parent, clauseStart, i + 1) : preSwitchCaseFlow!); + addAntecedent(preCaseLabel, fallthroughFlow); + currentFlow = finishFlowLabel(preCaseLabel); + const clause = clauses[i]; + bind(clause); + fallthroughFlow = currentFlow; + if (!(currentFlow.flags & FlowFlags.Unreachable) && i !== clauses.length - 1 && options.noFallthroughCasesInSwitch) { + clause.fallthroughFlowNode = currentFlow; + } + } + clauses.transformFlags = subtreeTransformFlags | TransformFlags.HasComputedFlags; + subtreeTransformFlags |= savedSubtreeTransformFlags; + } + function bindCaseClause(node: CaseClause): void { + const saveCurrentFlow = currentFlow; + currentFlow = preSwitchCaseFlow!; + bind(node.expression); + currentFlow = saveCurrentFlow; + bindEach(node.statements); + } + function bindExpressionStatement(node: ExpressionStatement): void { + bind(node.expression); + // A top level call expression with a dotted function name and at least one argument + // is potentially an assertion and is therefore included in the control flow. + if (node.expression.kind === SyntaxKind.CallExpression) { + const call = (node.expression); + if (isDottedName(call.expression)) { + currentFlow = createFlowCall(currentFlow, call); } - clauses.transformFlags = subtreeTransformFlags | TransformFlags.HasComputedFlags; - subtreeTransformFlags |= savedSubtreeTransformFlags; } - - function bindCaseClause(node: CaseClause): void { - const saveCurrentFlow = currentFlow; - currentFlow = preSwitchCaseFlow!; - bind(node.expression); - currentFlow = saveCurrentFlow; - bindEach(node.statements); - } - - function bindExpressionStatement(node: ExpressionStatement): void { - bind(node.expression); - // A top level call expression with a dotted function name and at least one argument - // is potentially an assertion and is therefore included in the control flow. - if (node.expression.kind === SyntaxKind.CallExpression) { - const call = node.expression; - if (isDottedName(call.expression)) { - currentFlow = createFlowCall(currentFlow, call); - } - } + } + function bindLabeledStatement(node: LabeledStatement): void { + const postStatementLabel = createBranchLabel(); + activeLabelList = { + next: activeLabelList, + name: node.label.escapedText, + breakTarget: postStatementLabel, + continueTarget: undefined, + referenced: false + }; + bind(node.label); + bind(node.statement); + if (!activeLabelList.referenced && !options.allowUnusedLabels) { + errorOrSuggestionOnNode(unusedLabelIsError(options), node.label, Diagnostics.Unused_label); + } + activeLabelList = activeLabelList.next; + addAntecedent(postStatementLabel, currentFlow); + currentFlow = finishFlowLabel(postStatementLabel); + } + function bindDestructuringTargetFlow(node: Expression) { + if (node.kind === SyntaxKind.BinaryExpression && (node).operatorToken.kind === SyntaxKind.EqualsToken) { + bindAssignmentTargetFlow((node).left); } - - function bindLabeledStatement(node: LabeledStatement): void { - const postStatementLabel = createBranchLabel(); - activeLabelList = { - next: activeLabelList, - name: node.label.escapedText, - breakTarget: postStatementLabel, - continueTarget: undefined, - referenced: false - }; - bind(node.label); - bind(node.statement); - if (!activeLabelList.referenced && !options.allowUnusedLabels) { - errorOrSuggestionOnNode(unusedLabelIsError(options), node.label, Diagnostics.Unused_label); - } - activeLabelList = activeLabelList.next; - addAntecedent(postStatementLabel, currentFlow); - currentFlow = finishFlowLabel(postStatementLabel); - } - - function bindDestructuringTargetFlow(node: Expression) { - if (node.kind === SyntaxKind.BinaryExpression && (node).operatorToken.kind === SyntaxKind.EqualsToken) { - bindAssignmentTargetFlow((node).left); - } - else { - bindAssignmentTargetFlow(node); - } + else { + bindAssignmentTargetFlow(node); } - - function bindAssignmentTargetFlow(node: Expression) { - if (isNarrowableReference(node)) { - currentFlow = createFlowMutation(FlowFlags.Assignment, currentFlow, node); - } - else if (node.kind === SyntaxKind.ArrayLiteralExpression) { - for (const e of (node).elements) { - if (e.kind === SyntaxKind.SpreadElement) { - bindAssignmentTargetFlow((e).expression); - } - else { - bindDestructuringTargetFlow(e); - } + } + function bindAssignmentTargetFlow(node: Expression) { + if (isNarrowableReference(node)) { + currentFlow = createFlowMutation(FlowFlags.Assignment, currentFlow, node); + } + else if (node.kind === SyntaxKind.ArrayLiteralExpression) { + for (const e of (node).elements) { + if (e.kind === SyntaxKind.SpreadElement) { + bindAssignmentTargetFlow((e).expression); } - } - else if (node.kind === SyntaxKind.ObjectLiteralExpression) { - for (const p of (node).properties) { - if (p.kind === SyntaxKind.PropertyAssignment) { - bindDestructuringTargetFlow(p.initializer); - } - else if (p.kind === SyntaxKind.ShorthandPropertyAssignment) { - bindAssignmentTargetFlow(p.name); - } - else if (p.kind === SyntaxKind.SpreadAssignment) { - bindAssignmentTargetFlow(p.expression); - } + else { + bindDestructuringTargetFlow(e); } } } - - function bindLogicalExpression(node: BinaryExpression, trueTarget: FlowLabel, falseTarget: FlowLabel) { - const preRightLabel = createBranchLabel(); - if (node.operatorToken.kind === SyntaxKind.AmpersandAmpersandToken) { - bindCondition(node.left, preRightLabel, falseTarget); - } - else { - bindCondition(node.left, trueTarget, preRightLabel); - } - currentFlow = finishFlowLabel(preRightLabel); - bind(node.operatorToken); - bindCondition(node.right, trueTarget, falseTarget); - } - - function bindPrefixUnaryExpressionFlow(node: PrefixUnaryExpression) { - if (node.operator === SyntaxKind.ExclamationToken) { - const saveTrueTarget = currentTrueTarget; - currentTrueTarget = currentFalseTarget; - currentFalseTarget = saveTrueTarget; - bindEachChild(node); - currentFalseTarget = currentTrueTarget; - currentTrueTarget = saveTrueTarget; - } - else { - bindEachChild(node); - if (node.operator === SyntaxKind.PlusPlusToken || node.operator === SyntaxKind.MinusMinusToken) { - bindAssignmentTargetFlow(node.operand); + else if (node.kind === SyntaxKind.ObjectLiteralExpression) { + for (const p of (node).properties) { + if (p.kind === SyntaxKind.PropertyAssignment) { + bindDestructuringTargetFlow(p.initializer); + } + else if (p.kind === SyntaxKind.ShorthandPropertyAssignment) { + bindAssignmentTargetFlow(p.name); + } + else if (p.kind === SyntaxKind.SpreadAssignment) { + bindAssignmentTargetFlow(p.expression); } } } - - function bindPostfixUnaryExpressionFlow(node: PostfixUnaryExpression) { + } + function bindLogicalExpression(node: BinaryExpression, trueTarget: FlowLabel, falseTarget: FlowLabel) { + const preRightLabel = createBranchLabel(); + if (node.operatorToken.kind === SyntaxKind.AmpersandAmpersandToken) { + bindCondition(node.left, preRightLabel, falseTarget); + } + else { + bindCondition(node.left, trueTarget, preRightLabel); + } + currentFlow = finishFlowLabel(preRightLabel); + bind(node.operatorToken); + bindCondition(node.right, trueTarget, falseTarget); + } + function bindPrefixUnaryExpressionFlow(node: PrefixUnaryExpression) { + if (node.operator === SyntaxKind.ExclamationToken) { + const saveTrueTarget = currentTrueTarget; + currentTrueTarget = currentFalseTarget; + currentFalseTarget = saveTrueTarget; + bindEachChild(node); + currentFalseTarget = currentTrueTarget; + currentTrueTarget = saveTrueTarget; + } + else { bindEachChild(node); if (node.operator === SyntaxKind.PlusPlusToken || node.operator === SyntaxKind.MinusMinusToken) { bindAssignmentTargetFlow(node.operand); } } - - const enum BindBinaryExpressionFlowState { - BindThenBindChildren, - MaybeBindLeft, - BindToken, - BindRight, - FinishBind - } - - function bindBinaryExpressionFlow(node: BinaryExpression) { - const workStacks: { - expr: BinaryExpression[], - state: BindBinaryExpressionFlowState[], - inStrictMode: (boolean | undefined)[], - parent: (Node | undefined)[], - subtreeFlags: (number | undefined)[] - } = { - expr: [node], - state: [BindBinaryExpressionFlowState.MaybeBindLeft], - inStrictMode: [undefined], - parent: [undefined], - subtreeFlags: [undefined] - }; - let stackIndex = 0; - while (stackIndex >= 0) { - node = workStacks.expr[stackIndex]; - switch (workStacks.state[stackIndex]) { - case BindBinaryExpressionFlowState.BindThenBindChildren: { - // This state is used only when recuring, to emulate the work that `bind` does before - // reaching `bindChildren`. A normal call to `bindBinaryExpressionFlow` will already have done this work. - node.parent = parent; - const saveInStrictMode = inStrictMode; - bindWorker(node); - const saveParent = parent; - parent = node; - - let subtreeFlagsState: number | undefined; - // While this next part does the work of `bindChildren` before it descends into `bindChildrenWorker` - // and uses `subtreeFlagsState` to queue up the work that needs to be done once the node is bound. - if (skipTransformFlagAggregation) { - // do nothing extra - } - else if (node.transformFlags & TransformFlags.HasComputedFlags) { - skipTransformFlagAggregation = true; - subtreeFlagsState = -1; - } - else { - const savedSubtreeTransformFlags = subtreeTransformFlags; - subtreeTransformFlags = 0; - subtreeFlagsState = savedSubtreeTransformFlags; - } - - advanceState(BindBinaryExpressionFlowState.MaybeBindLeft, saveInStrictMode, saveParent, subtreeFlagsState); - break; + } + function bindPostfixUnaryExpressionFlow(node: PostfixUnaryExpression) { + bindEachChild(node); + if (node.operator === SyntaxKind.PlusPlusToken || node.operator === SyntaxKind.MinusMinusToken) { + bindAssignmentTargetFlow(node.operand); + } + } + const enum BindBinaryExpressionFlowState { + BindThenBindChildren, + MaybeBindLeft, + BindToken, + BindRight, + FinishBind + } + function bindBinaryExpressionFlow(node: BinaryExpression) { + const workStacks: { + expr: BinaryExpression[]; + state: BindBinaryExpressionFlowState[]; + inStrictMode: (boolean | undefined)[]; + parent: (Node | undefined)[]; + subtreeFlags: (number | undefined)[]; + } = { + expr: [node], + state: [BindBinaryExpressionFlowState.MaybeBindLeft], + inStrictMode: [undefined], + parent: [undefined], + subtreeFlags: [undefined] + }; + let stackIndex = 0; + while (stackIndex >= 0) { + node = workStacks.expr[stackIndex]; + switch (workStacks.state[stackIndex]) { + case BindBinaryExpressionFlowState.BindThenBindChildren: { + // This state is used only when recuring, to emulate the work that `bind` does before + // reaching `bindChildren`. A normal call to `bindBinaryExpressionFlow` will already have done this work. + node.parent = parent; + const saveInStrictMode = inStrictMode; + bindWorker(node); + const saveParent = parent; + parent = node; + let subtreeFlagsState: number | undefined; + // While this next part does the work of `bindChildren` before it descends into `bindChildrenWorker` + // and uses `subtreeFlagsState` to queue up the work that needs to be done once the node is bound. + if (skipTransformFlagAggregation) { + // do nothing extra } - case BindBinaryExpressionFlowState.MaybeBindLeft: { - const operator = node.operatorToken.kind; - // TODO: bindLogicalExpression is recursive - if we want to handle deeply nested `&&` expressions - // we'll need to handle the `bindLogicalExpression` scenarios in this state machine, too - // For now, though, since the common cases are chained `+`, leaving it recursive is fine - if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.BarBarToken || operator === SyntaxKind.QuestionQuestionToken) { - if (isTopLevelLogicalExpression(node)) { - const postExpressionLabel = createBranchLabel(); - bindLogicalExpression(node, postExpressionLabel, postExpressionLabel); - currentFlow = finishFlowLabel(postExpressionLabel); - } - else { - bindLogicalExpression(node, currentTrueTarget!, currentFalseTarget!); - } - completeNode(); + else if (node.transformFlags & TransformFlags.HasComputedFlags) { + skipTransformFlagAggregation = true; + subtreeFlagsState = -1; + } + else { + const savedSubtreeTransformFlags = subtreeTransformFlags; + subtreeTransformFlags = 0; + subtreeFlagsState = savedSubtreeTransformFlags; + } + advanceState(BindBinaryExpressionFlowState.MaybeBindLeft, saveInStrictMode, saveParent, subtreeFlagsState); + break; + } + case BindBinaryExpressionFlowState.MaybeBindLeft: { + const operator = node.operatorToken.kind; + // TODO: bindLogicalExpression is recursive - if we want to handle deeply nested `&&` expressions + // we'll need to handle the `bindLogicalExpression` scenarios in this state machine, too + // For now, though, since the common cases are chained `+`, leaving it recursive is fine + if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.BarBarToken || operator === SyntaxKind.QuestionQuestionToken) { + if (isTopLevelLogicalExpression(node)) { + const postExpressionLabel = createBranchLabel(); + bindLogicalExpression(node, postExpressionLabel, postExpressionLabel); + currentFlow = finishFlowLabel(postExpressionLabel); } else { - advanceState(BindBinaryExpressionFlowState.BindToken); - maybeBind(node.left); + bindLogicalExpression(node, currentTrueTarget!, currentFalseTarget!); } - break; - } - case BindBinaryExpressionFlowState.BindToken: { - advanceState(BindBinaryExpressionFlowState.BindRight); - maybeBind(node.operatorToken); - break; + completeNode(); } - case BindBinaryExpressionFlowState.BindRight: { - advanceState(BindBinaryExpressionFlowState.FinishBind); - maybeBind(node.right); - break; + else { + advanceState(BindBinaryExpressionFlowState.BindToken); + maybeBind(node.left); } - case BindBinaryExpressionFlowState.FinishBind: { - const operator = node.operatorToken.kind; - if (isAssignmentOperator(operator) && !isAssignmentTarget(node)) { - bindAssignmentTargetFlow(node.left); - if (operator === SyntaxKind.EqualsToken && node.left.kind === SyntaxKind.ElementAccessExpression) { - const elementAccess = node.left; - if (isNarrowableOperand(elementAccess.expression)) { - currentFlow = createFlowMutation(FlowFlags.ArrayMutation, currentFlow, node); - } + break; + } + case BindBinaryExpressionFlowState.BindToken: { + advanceState(BindBinaryExpressionFlowState.BindRight); + maybeBind(node.operatorToken); + break; + } + case BindBinaryExpressionFlowState.BindRight: { + advanceState(BindBinaryExpressionFlowState.FinishBind); + maybeBind(node.right); + break; + } + case BindBinaryExpressionFlowState.FinishBind: { + const operator = node.operatorToken.kind; + if (isAssignmentOperator(operator) && !isAssignmentTarget(node)) { + bindAssignmentTargetFlow(node.left); + if (operator === SyntaxKind.EqualsToken && node.left.kind === SyntaxKind.ElementAccessExpression) { + const elementAccess = (node.left); + if (isNarrowableOperand(elementAccess.expression)) { + currentFlow = createFlowMutation(FlowFlags.ArrayMutation, currentFlow, node); } } - completeNode(); - break; } - default: return Debug.fail(`Invalid state ${workStacks.state[stackIndex]} for bindBinaryExpressionFlow`); + completeNode(); + break; } + default: return Debug.fail(`Invalid state ${workStacks.state[stackIndex]} for bindBinaryExpressionFlow`); } - - /** - * Note that `advanceState` sets the _current_ head state, and that `maybeBind` potentially pushes on a new - * head state; so `advanceState` must be called before any `maybeBind` during a state's execution. - */ - function advanceState(state: BindBinaryExpressionFlowState, isInStrictMode?: boolean, parent?: Node, subtreeFlags?: number) { - workStacks.state[stackIndex] = state; - if (isInStrictMode !== undefined) { - workStacks.inStrictMode[stackIndex] = isInStrictMode; - } - if (parent !== undefined) { - workStacks.parent[stackIndex] = parent; + } + /** + * Note that `advanceState` sets the _current_ head state, and that `maybeBind` potentially pushes on a new + * head state; so `advanceState` must be called before any `maybeBind` during a state's execution. + */ + function advanceState(state: BindBinaryExpressionFlowState, isInStrictMode?: boolean, parent?: Node, subtreeFlags?: number) { + workStacks.state[stackIndex] = state; + if (isInStrictMode !== undefined) { + workStacks.inStrictMode[stackIndex] = isInStrictMode; + } + if (parent !== undefined) { + workStacks.parent[stackIndex] = parent; + } + if (subtreeFlags !== undefined) { + workStacks.subtreeFlags[stackIndex] = subtreeFlags; + } + } + function completeNode() { + if (workStacks.inStrictMode[stackIndex] !== undefined) { + if (workStacks.subtreeFlags[stackIndex] === -1) { + skipTransformFlagAggregation = false; + subtreeTransformFlags |= node.transformFlags & ~getTransformFlagsSubtreeExclusions(node.kind); } - if (subtreeFlags !== undefined) { - workStacks.subtreeFlags[stackIndex] = subtreeFlags; + else if (workStacks.subtreeFlags[stackIndex] !== undefined) { + subtreeTransformFlags = workStacks.subtreeFlags[stackIndex]! | computeTransformFlagsForNode(node, subtreeTransformFlags); } + inStrictMode = workStacks.inStrictMode[stackIndex]!; + parent = workStacks.parent[stackIndex]!; } - - function completeNode() { - if (workStacks.inStrictMode[stackIndex] !== undefined) { - if (workStacks.subtreeFlags[stackIndex] === -1) { - skipTransformFlagAggregation = false; - subtreeTransformFlags |= node.transformFlags & ~getTransformFlagsSubtreeExclusions(node.kind); - } - else if (workStacks.subtreeFlags[stackIndex] !== undefined) { - subtreeTransformFlags = workStacks.subtreeFlags[stackIndex]! | computeTransformFlagsForNode(node, subtreeTransformFlags); - } - inStrictMode = workStacks.inStrictMode[stackIndex]!; - parent = workStacks.parent[stackIndex]!; - } - stackIndex--; + stackIndex--; + } + /** + * If `node` is a BinaryExpression, adds it to the local work stack, otherwise recursively binds it + */ + function maybeBind(node: Node) { + if (node && isBinaryExpression(node)) { + stackIndex++; + workStacks.expr[stackIndex] = node; + workStacks.state[stackIndex] = BindBinaryExpressionFlowState.BindThenBindChildren; + workStacks.inStrictMode[stackIndex] = undefined; + workStacks.parent[stackIndex] = undefined; + workStacks.subtreeFlags[stackIndex] = undefined; } - - /** - * If `node` is a BinaryExpression, adds it to the local work stack, otherwise recursively binds it - */ - function maybeBind(node: Node) { - if (node && isBinaryExpression(node)) { - stackIndex++; - workStacks.expr[stackIndex] = node; - workStacks.state[stackIndex] = BindBinaryExpressionFlowState.BindThenBindChildren; - workStacks.inStrictMode[stackIndex] = undefined; - workStacks.parent[stackIndex] = undefined; - workStacks.subtreeFlags[stackIndex] = undefined; - } - else { - bind(node); - } + else { + bind(node); } } - - function bindDeleteExpressionFlow(node: DeleteExpression) { - bindEachChild(node); - if (node.expression.kind === SyntaxKind.PropertyAccessExpression) { - bindAssignmentTargetFlow(node.expression); + } + function bindDeleteExpressionFlow(node: DeleteExpression) { + bindEachChild(node); + if (node.expression.kind === SyntaxKind.PropertyAccessExpression) { + bindAssignmentTargetFlow(node.expression); + } + } + function bindConditionalExpressionFlow(node: ConditionalExpression) { + const trueLabel = createBranchLabel(); + const falseLabel = createBranchLabel(); + const postExpressionLabel = createBranchLabel(); + bindCondition(node.condition, trueLabel, falseLabel); + currentFlow = finishFlowLabel(trueLabel); + bind(node.questionToken); + bind(node.whenTrue); + addAntecedent(postExpressionLabel, currentFlow); + currentFlow = finishFlowLabel(falseLabel); + bind(node.colonToken); + bind(node.whenFalse); + addAntecedent(postExpressionLabel, currentFlow); + currentFlow = finishFlowLabel(postExpressionLabel); + } + function bindInitializedVariableFlow(node: VariableDeclaration | ArrayBindingElement) { + const name = !isOmittedExpression(node) ? node.name : undefined; + if (isBindingPattern(name)) { + for (const child of name.elements) { + bindInitializedVariableFlow(child); } } - - function bindConditionalExpressionFlow(node: ConditionalExpression) { - const trueLabel = createBranchLabel(); - const falseLabel = createBranchLabel(); - const postExpressionLabel = createBranchLabel(); - bindCondition(node.condition, trueLabel, falseLabel); - currentFlow = finishFlowLabel(trueLabel); - bind(node.questionToken); - bind(node.whenTrue); - addAntecedent(postExpressionLabel, currentFlow); - currentFlow = finishFlowLabel(falseLabel); - bind(node.colonToken); - bind(node.whenFalse); - addAntecedent(postExpressionLabel, currentFlow); - currentFlow = finishFlowLabel(postExpressionLabel); + else { + currentFlow = createFlowMutation(FlowFlags.Assignment, currentFlow, node); } - - function bindInitializedVariableFlow(node: VariableDeclaration | ArrayBindingElement) { - const name = !isOmittedExpression(node) ? node.name : undefined; - if (isBindingPattern(name)) { - for (const child of name.elements) { - bindInitializedVariableFlow(child); - } - } - else { - currentFlow = createFlowMutation(FlowFlags.Assignment, currentFlow, node); - } + } + function bindVariableDeclarationFlow(node: VariableDeclaration) { + bindEachChild(node); + if (node.initializer || isForInOrOfStatement(node.parent.parent)) { + bindInitializedVariableFlow(node); } - - function bindVariableDeclarationFlow(node: VariableDeclaration) { - bindEachChild(node); - if (node.initializer || isForInOrOfStatement(node.parent.parent)) { - bindInitializedVariableFlow(node); - } + } + function bindJSDocTypeAlias(node: JSDocTypedefTag | JSDocCallbackTag | JSDocEnumTag) { + node.tagName.parent = node; + if (node.kind !== SyntaxKind.JSDocEnumTag && node.fullName) { + setParentPointers(node, node.fullName); } - - function bindJSDocTypeAlias(node: JSDocTypedefTag | JSDocCallbackTag | JSDocEnumTag) { - node.tagName.parent = node; - if (node.kind !== SyntaxKind.JSDocEnumTag && node.fullName) { - setParentPointers(node, node.fullName); - } + } + function bindJSDocClassTag(node: JSDocClassTag) { + bindEachChild(node); + const host = getHostSignatureFromJSDoc(node); + if (host && host.kind !== SyntaxKind.MethodDeclaration) { + addDeclarationToSymbol(host.symbol, host, SymbolFlags.Class); } - - function bindJSDocClassTag(node: JSDocClassTag) { - bindEachChild(node); - const host = getHostSignatureFromJSDoc(node); - if (host && host.kind !== SyntaxKind.MethodDeclaration) { - addDeclarationToSymbol(host.symbol, host, SymbolFlags.Class); - } + } + function bindOptionalExpression(node: Expression, trueTarget: FlowLabel, falseTarget: FlowLabel) { + doWithConditionalBranches(bind, node, trueTarget, falseTarget); + if (!isOptionalChain(node) || isOutermostOptionalChain(node)) { + addAntecedent(trueTarget, createFlowCondition(FlowFlags.TrueCondition, currentFlow, node)); + addAntecedent(falseTarget, createFlowCondition(FlowFlags.FalseCondition, currentFlow, node)); } - - function bindOptionalExpression(node: Expression, trueTarget: FlowLabel, falseTarget: FlowLabel) { - doWithConditionalBranches(bind, node, trueTarget, falseTarget); - if (!isOptionalChain(node) || isOutermostOptionalChain(node)) { - addAntecedent(trueTarget, createFlowCondition(FlowFlags.TrueCondition, currentFlow, node)); - addAntecedent(falseTarget, createFlowCondition(FlowFlags.FalseCondition, currentFlow, node)); - } + } + function bindOptionalChainRest(node: OptionalChain) { + bind(node.questionDotToken); + switch (node.kind) { + case SyntaxKind.PropertyAccessExpression: + bind(node.name); + break; + case SyntaxKind.ElementAccessExpression: + bind(node.argumentExpression); + break; + case SyntaxKind.CallExpression: + bindEach(node.typeArguments); + bindEach(node.arguments); + break; } - - function bindOptionalChainRest(node: OptionalChain) { - bind(node.questionDotToken); - switch (node.kind) { - case SyntaxKind.PropertyAccessExpression: - bind(node.name); - break; - case SyntaxKind.ElementAccessExpression: - bind(node.argumentExpression); - break; - case SyntaxKind.CallExpression: - bindEach(node.typeArguments); - bindEach(node.arguments); - break; - } + } + function bindOptionalChain(node: OptionalChain, trueTarget: FlowLabel, falseTarget: FlowLabel) { + // For an optional chain, we emulate the behavior of a logical expression: + // + // a?.b -> a && a.b + // a?.b.c -> a && a.b.c + // a?.b?.c -> a && a.b && a.b.c + // a?.[x = 1] -> a && a[x = 1] + // + // To do this we descend through the chain until we reach the root of a chain (the expression with a `?.`) + // and build it's CFA graph as if it were the first condition (`a && ...`). Then we bind the rest + // of the node as part of the "true" branch, and continue to do so as we ascend back up to the outermost + // chain node. We then treat the entire node as the right side of the expression. + const preChainLabel = node.questionDotToken ? createBranchLabel() : undefined; + bindOptionalExpression(node.expression, preChainLabel || trueTarget, falseTarget); + if (preChainLabel) { + currentFlow = finishFlowLabel(preChainLabel); + } + doWithConditionalBranches(bindOptionalChainRest, node, trueTarget, falseTarget); + if (isOutermostOptionalChain(node)) { + addAntecedent(trueTarget, createFlowCondition(FlowFlags.TrueCondition, currentFlow, node)); + addAntecedent(falseTarget, createFlowCondition(FlowFlags.FalseCondition, currentFlow, node)); } - - function bindOptionalChain(node: OptionalChain, trueTarget: FlowLabel, falseTarget: FlowLabel) { - // For an optional chain, we emulate the behavior of a logical expression: - // - // a?.b -> a && a.b - // a?.b.c -> a && a.b.c - // a?.b?.c -> a && a.b && a.b.c - // a?.[x = 1] -> a && a[x = 1] - // - // To do this we descend through the chain until we reach the root of a chain (the expression with a `?.`) - // and build it's CFA graph as if it were the first condition (`a && ...`). Then we bind the rest - // of the node as part of the "true" branch, and continue to do so as we ascend back up to the outermost - // chain node. We then treat the entire node as the right side of the expression. - const preChainLabel = node.questionDotToken ? createBranchLabel() : undefined; - bindOptionalExpression(node.expression, preChainLabel || trueTarget, falseTarget); - if (preChainLabel) { - currentFlow = finishFlowLabel(preChainLabel); - } - doWithConditionalBranches(bindOptionalChainRest, node, trueTarget, falseTarget); - if (isOutermostOptionalChain(node)) { - addAntecedent(trueTarget, createFlowCondition(FlowFlags.TrueCondition, currentFlow, node)); - addAntecedent(falseTarget, createFlowCondition(FlowFlags.FalseCondition, currentFlow, node)); - } + } + function bindOptionalChainFlow(node: OptionalChain) { + if (isTopLevelLogicalExpression(node)) { + const postExpressionLabel = createBranchLabel(); + bindOptionalChain(node, postExpressionLabel, postExpressionLabel); + currentFlow = finishFlowLabel(postExpressionLabel); } - - function bindOptionalChainFlow(node: OptionalChain) { - if (isTopLevelLogicalExpression(node)) { - const postExpressionLabel = createBranchLabel(); - bindOptionalChain(node, postExpressionLabel, postExpressionLabel); - currentFlow = finishFlowLabel(postExpressionLabel); - } - else { - bindOptionalChain(node, currentTrueTarget!, currentFalseTarget!); - } + else { + bindOptionalChain(node, currentTrueTarget!, currentFalseTarget!); + } + } + function bindAccessExpressionFlow(node: AccessExpression) { + if (isOptionalChain(node)) { + bindOptionalChainFlow(node); + } + else { + bindEachChild(node); + } + } + function bindCallExpressionFlow(node: CallExpression) { + if (isOptionalChain(node)) { + bindOptionalChainFlow(node); } - - function bindAccessExpressionFlow(node: AccessExpression) { - if (isOptionalChain(node)) { - bindOptionalChainFlow(node); + else { + // If the target of the call expression is a function expression or arrow function we have + // an immediately invoked function expression (IIFE). Initialize the flowNode property to + // the current control flow (which includes evaluation of the IIFE arguments). + const expr = skipParentheses(node.expression); + if (expr.kind === SyntaxKind.FunctionExpression || expr.kind === SyntaxKind.ArrowFunction) { + bindEach(node.typeArguments); + bindEach(node.arguments); + bind(node.expression); } else { bindEachChild(node); } } - - function bindCallExpressionFlow(node: CallExpression) { - if (isOptionalChain(node)) { - bindOptionalChainFlow(node); - } - else { - // If the target of the call expression is a function expression or arrow function we have - // an immediately invoked function expression (IIFE). Initialize the flowNode property to - // the current control flow (which includes evaluation of the IIFE arguments). - const expr = skipParentheses(node.expression); - if (expr.kind === SyntaxKind.FunctionExpression || expr.kind === SyntaxKind.ArrowFunction) { - bindEach(node.typeArguments); - bindEach(node.arguments); - bind(node.expression); - } - else { - bindEachChild(node); - } - } - if (node.expression.kind === SyntaxKind.PropertyAccessExpression) { - const propertyAccess = node.expression; - if (isIdentifier(propertyAccess.name) && isNarrowableOperand(propertyAccess.expression) && isPushOrUnshiftIdentifier(propertyAccess.name)) { - currentFlow = createFlowMutation(FlowFlags.ArrayMutation, currentFlow, node); - } + if (node.expression.kind === SyntaxKind.PropertyAccessExpression) { + const propertyAccess = (node.expression); + if (isIdentifier(propertyAccess.name) && isNarrowableOperand(propertyAccess.expression) && isPushOrUnshiftIdentifier(propertyAccess.name)) { + currentFlow = createFlowMutation(FlowFlags.ArrayMutation, currentFlow, node); } } - - function getContainerFlags(node: Node): ContainerFlags { - switch (node.kind) { - case SyntaxKind.ClassExpression: - case SyntaxKind.ClassDeclaration: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.ObjectLiteralExpression: - case SyntaxKind.TypeLiteral: - case SyntaxKind.JSDocTypeLiteral: - case SyntaxKind.JsxAttributes: - return ContainerFlags.IsContainer; - - case SyntaxKind.InterfaceDeclaration: - return ContainerFlags.IsContainer | ContainerFlags.IsInterface; - - case SyntaxKind.ModuleDeclaration: - case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.MappedType: - return ContainerFlags.IsContainer | ContainerFlags.HasLocals; - - case SyntaxKind.SourceFile: - return ContainerFlags.IsContainer | ContainerFlags.IsControlFlowContainer | ContainerFlags.HasLocals; - - case SyntaxKind.MethodDeclaration: - if (isObjectLiteralOrClassExpressionMethod(node)) { - return ContainerFlags.IsContainer | ContainerFlags.IsControlFlowContainer | ContainerFlags.HasLocals | ContainerFlags.IsFunctionLike | ContainerFlags.IsObjectLiteralOrClassExpressionMethod; - } - // falls through - case SyntaxKind.Constructor: - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.MethodSignature: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - case SyntaxKind.CallSignature: - case SyntaxKind.JSDocSignature: - case SyntaxKind.JSDocFunctionType: - case SyntaxKind.FunctionType: - case SyntaxKind.ConstructSignature: - case SyntaxKind.IndexSignature: - case SyntaxKind.ConstructorType: - return ContainerFlags.IsContainer | ContainerFlags.IsControlFlowContainer | ContainerFlags.HasLocals | ContainerFlags.IsFunctionLike; - - case SyntaxKind.FunctionExpression: - case SyntaxKind.ArrowFunction: - return ContainerFlags.IsContainer | ContainerFlags.IsControlFlowContainer | ContainerFlags.HasLocals | ContainerFlags.IsFunctionLike | ContainerFlags.IsFunctionExpression; - - case SyntaxKind.ModuleBlock: - return ContainerFlags.IsControlFlowContainer; - case SyntaxKind.PropertyDeclaration: - return (node).initializer ? ContainerFlags.IsControlFlowContainer : 0; - - case SyntaxKind.CatchClause: - case SyntaxKind.ForStatement: - case SyntaxKind.ForInStatement: - case SyntaxKind.ForOfStatement: - case SyntaxKind.CaseBlock: - return ContainerFlags.IsBlockScopedContainer; - - case SyntaxKind.Block: - // do not treat blocks directly inside a function as a block-scoped-container. - // Locals that reside in this block should go to the function locals. Otherwise 'x' - // would not appear to be a redeclaration of a block scoped local in the following - // example: - // - // function foo() { - // var x; - // let x; - // } - // - // If we placed 'var x' into the function locals and 'let x' into the locals of - // the block, then there would be no collision. - // - // By not creating a new block-scoped-container here, we ensure that both 'var x' - // and 'let x' go into the Function-container's locals, and we do get a collision - // conflict. - return isFunctionLike(node.parent) ? ContainerFlags.None : ContainerFlags.IsBlockScopedContainer; - } - - return ContainerFlags.None; + } + function getContainerFlags(node: Node): ContainerFlags { + switch (node.kind) { + case SyntaxKind.ClassExpression: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.ObjectLiteralExpression: + case SyntaxKind.TypeLiteral: + case SyntaxKind.JSDocTypeLiteral: + case SyntaxKind.JsxAttributes: + return ContainerFlags.IsContainer; + case SyntaxKind.InterfaceDeclaration: + return ContainerFlags.IsContainer | ContainerFlags.IsInterface; + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.MappedType: + return ContainerFlags.IsContainer | ContainerFlags.HasLocals; + case SyntaxKind.SourceFile: + return ContainerFlags.IsContainer | ContainerFlags.IsControlFlowContainer | ContainerFlags.HasLocals; + case SyntaxKind.MethodDeclaration: + if (isObjectLiteralOrClassExpressionMethod(node)) { + return ContainerFlags.IsContainer | ContainerFlags.IsControlFlowContainer | ContainerFlags.HasLocals | ContainerFlags.IsFunctionLike | ContainerFlags.IsObjectLiteralOrClassExpressionMethod; + } + // falls through + case SyntaxKind.Constructor: + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.MethodSignature: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + case SyntaxKind.CallSignature: + case SyntaxKind.JSDocSignature: + case SyntaxKind.JSDocFunctionType: + case SyntaxKind.FunctionType: + case SyntaxKind.ConstructSignature: + case SyntaxKind.IndexSignature: + case SyntaxKind.ConstructorType: + return ContainerFlags.IsContainer | ContainerFlags.IsControlFlowContainer | ContainerFlags.HasLocals | ContainerFlags.IsFunctionLike; + case SyntaxKind.FunctionExpression: + case SyntaxKind.ArrowFunction: + return ContainerFlags.IsContainer | ContainerFlags.IsControlFlowContainer | ContainerFlags.HasLocals | ContainerFlags.IsFunctionLike | ContainerFlags.IsFunctionExpression; + case SyntaxKind.ModuleBlock: + return ContainerFlags.IsControlFlowContainer; + case SyntaxKind.PropertyDeclaration: + return (node).initializer ? ContainerFlags.IsControlFlowContainer : 0; + case SyntaxKind.CatchClause: + case SyntaxKind.ForStatement: + case SyntaxKind.ForInStatement: + case SyntaxKind.ForOfStatement: + case SyntaxKind.CaseBlock: + return ContainerFlags.IsBlockScopedContainer; + case SyntaxKind.Block: + // do not treat blocks directly inside a function as a block-scoped-container. + // Locals that reside in this block should go to the function locals. Otherwise 'x' + // would not appear to be a redeclaration of a block scoped local in the following + // example: + // + // function foo() { + // var x; + // let x; + // } + // + // If we placed 'var x' into the function locals and 'let x' into the locals of + // the block, then there would be no collision. + // + // By not creating a new block-scoped-container here, we ensure that both 'var x' + // and 'let x' go into the Function-container's locals, and we do get a collision + // conflict. + return isFunctionLike(node.parent) ? ContainerFlags.None : ContainerFlags.IsBlockScopedContainer; } - - function addToContainerChain(next: Node) { - if (lastContainer) { - lastContainer.nextContainer = next; - } - - lastContainer = next; - } - - function declareSymbolAndAddToSymbolTable(node: Declaration, symbolFlags: SymbolFlags, symbolExcludes: SymbolFlags): Symbol | undefined { - switch (container.kind) { - // Modules, source files, and classes need specialized handling for how their - // members are declared (for example, a member of a class will go into a specific - // symbol table depending on if it is static or not). We defer to specialized - // handlers to take care of declaring these child members. - case SyntaxKind.ModuleDeclaration: - return declareModuleMember(node, symbolFlags, symbolExcludes); - - case SyntaxKind.SourceFile: - return declareSourceFileMember(node, symbolFlags, symbolExcludes); - - case SyntaxKind.ClassExpression: - case SyntaxKind.ClassDeclaration: - return declareClassMember(node, symbolFlags, symbolExcludes); - - case SyntaxKind.EnumDeclaration: - return declareSymbol(container.symbol.exports!, container.symbol, node, symbolFlags, symbolExcludes); - - case SyntaxKind.TypeLiteral: - case SyntaxKind.JSDocTypeLiteral: - case SyntaxKind.ObjectLiteralExpression: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.JsxAttributes: - // Interface/Object-types always have their children added to the 'members' of - // their container. They are only accessible through an instance of their - // container, and are never in scope otherwise (even inside the body of the - // object / type / interface declaring them). An exception is type parameters, - // which are in scope without qualification (similar to 'locals'). - return declareSymbol(container.symbol.members!, container.symbol, node, symbolFlags, symbolExcludes); - - case SyntaxKind.FunctionType: - case SyntaxKind.ConstructorType: - case SyntaxKind.CallSignature: - case SyntaxKind.ConstructSignature: - case SyntaxKind.JSDocSignature: - case SyntaxKind.IndexSignature: - 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.JSDocFunctionType: - case SyntaxKind.JSDocTypedefTag: - case SyntaxKind.JSDocCallbackTag: - case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.MappedType: - // All the children of these container types are never visible through another - // symbol (i.e. through another symbol's 'exports' or 'members'). Instead, - // they're only accessed 'lexically' (i.e. from code that exists underneath - // their container in the tree). To accomplish this, we simply add their declared - // symbol to the 'locals' of the container. These symbols can then be found as - // the type checker walks up the containers, checking them for matching names. - return declareSymbol(container.locals!, /*parent*/ undefined, node, symbolFlags, symbolExcludes); - } + return ContainerFlags.None; + } + function addToContainerChain(next: Node) { + if (lastContainer) { + lastContainer.nextContainer = next; } - - function declareClassMember(node: Declaration, symbolFlags: SymbolFlags, symbolExcludes: SymbolFlags) { - return hasModifier(node, ModifierFlags.Static) - ? declareSymbol(container.symbol.exports!, container.symbol, node, symbolFlags, symbolExcludes) - : declareSymbol(container.symbol.members!, container.symbol, node, symbolFlags, symbolExcludes); - } - - function declareSourceFileMember(node: Declaration, symbolFlags: SymbolFlags, symbolExcludes: SymbolFlags) { - return isExternalModule(file) - ? declareModuleMember(node, symbolFlags, symbolExcludes) - : declareSymbol(file.locals!, /*parent*/ undefined, node, symbolFlags, symbolExcludes); - } - - function hasExportDeclarations(node: ModuleDeclaration | SourceFile): boolean { - const body = isSourceFile(node) ? node : tryCast(node.body, isModuleBlock); - return !!body && body.statements.some(s => isExportDeclaration(s) || isExportAssignment(s)); - } - - function setExportContextFlag(node: ModuleDeclaration | SourceFile) { - // A declaration source file or ambient module declaration that contains no export declarations (but possibly regular - // declarations with export modifiers) is an export context in which declarations are implicitly exported. - if (node.flags & NodeFlags.Ambient && !hasExportDeclarations(node)) { - node.flags |= NodeFlags.ExportContext; - } - else { - node.flags &= ~NodeFlags.ExportContext; - } + lastContainer = next; + } + function declareSymbolAndAddToSymbolTable(node: Declaration, symbolFlags: SymbolFlags, symbolExcludes: SymbolFlags): ts.Symbol | undefined { + switch (container.kind) { + // Modules, source files, and classes need specialized handling for how their + // members are declared (for example, a member of a class will go into a specific + // symbol table depending on if it is static or not). We defer to specialized + // handlers to take care of declaring these child members. + case SyntaxKind.ModuleDeclaration: + return declareModuleMember(node, symbolFlags, symbolExcludes); + case SyntaxKind.SourceFile: + return declareSourceFileMember(node, symbolFlags, symbolExcludes); + case SyntaxKind.ClassExpression: + case SyntaxKind.ClassDeclaration: + return declareClassMember(node, symbolFlags, symbolExcludes); + case SyntaxKind.EnumDeclaration: + return declareSymbol(container.symbol.exports!, container.symbol, node, symbolFlags, symbolExcludes); + case SyntaxKind.TypeLiteral: + case SyntaxKind.JSDocTypeLiteral: + case SyntaxKind.ObjectLiteralExpression: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.JsxAttributes: + // Interface/Object-types always have their children added to the 'members' of + // their container. They are only accessible through an instance of their + // container, and are never in scope otherwise (even inside the body of the + // object / type / interface declaring them). An exception is type parameters, + // which are in scope without qualification (similar to 'locals'). + return declareSymbol(container.symbol.members!, container.symbol, node, symbolFlags, symbolExcludes); + case SyntaxKind.FunctionType: + case SyntaxKind.ConstructorType: + case SyntaxKind.CallSignature: + case SyntaxKind.ConstructSignature: + case SyntaxKind.JSDocSignature: + case SyntaxKind.IndexSignature: + 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.JSDocFunctionType: + case SyntaxKind.JSDocTypedefTag: + case SyntaxKind.JSDocCallbackTag: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.MappedType: + // All the children of these container types are never visible through another + // symbol (i.e. through another symbol's 'exports' or 'members'). Instead, + // they're only accessed 'lexically' (i.e. from code that exists underneath + // their container in the tree). To accomplish this, we simply add their declared + // symbol to the 'locals' of the container. These symbols can then be found as + // the type checker walks up the containers, checking them for matching names. + return declareSymbol(container.locals!, /*parent*/ undefined, node, symbolFlags, symbolExcludes); } - - function bindModuleDeclaration(node: ModuleDeclaration) { - setExportContextFlag(node); - if (isAmbientModule(node)) { - if (hasModifier(node, ModifierFlags.Export)) { - errorOnFirstToken(node, Diagnostics.export_modifier_cannot_be_applied_to_ambient_modules_and_module_augmentations_since_they_are_always_visible); - } - if (isModuleAugmentationExternal(node)) { - declareModuleSymbol(node); - } - else { - let pattern: Pattern | undefined; - if (node.name.kind === SyntaxKind.StringLiteral) { - const { text } = node.name; - if (hasZeroOrOneAsteriskCharacter(text)) { - pattern = tryParsePattern(text); - } - else { - errorOnFirstToken(node.name, Diagnostics.Pattern_0_can_have_at_most_one_Asterisk_character, text); - } - } - - const symbol = declareSymbolAndAddToSymbolTable(node, SymbolFlags.ValueModule, SymbolFlags.ValueModuleExcludes)!; - file.patternAmbientModules = append(file.patternAmbientModules, pattern && { pattern, symbol }); - } - } - else { - const state = declareModuleSymbol(node); - if (state !== ModuleInstanceState.NonInstantiated) { - const { symbol } = node; - // if module was already merged with some function, class or non-const enum, treat it as non-const-enum-only - symbol.constEnumOnlyModule = (!(symbol.flags & (SymbolFlags.Function | SymbolFlags.Class | SymbolFlags.RegularEnum))) - // Current must be `const enum` only - && state === ModuleInstanceState.ConstEnumOnly - // Can't have been set to 'false' in a previous merged symbol. ('undefined' OK) - && symbol.constEnumOnlyModule !== false; - } - } + } + function declareClassMember(node: Declaration, symbolFlags: SymbolFlags, symbolExcludes: SymbolFlags) { + return hasModifier(node, ModifierFlags.Static) + ? declareSymbol(container.symbol.exports!, container.symbol, node, symbolFlags, symbolExcludes) + : declareSymbol(container.symbol.members!, container.symbol, node, symbolFlags, symbolExcludes); + } + function declareSourceFileMember(node: Declaration, symbolFlags: SymbolFlags, symbolExcludes: SymbolFlags) { + return isExternalModule(file) + ? declareModuleMember(node, symbolFlags, symbolExcludes) + : declareSymbol(file.locals!, /*parent*/ undefined, node, symbolFlags, symbolExcludes); + } + function hasExportDeclarations(node: ModuleDeclaration | SourceFile): boolean { + const body = isSourceFile(node) ? node : tryCast(node.body, isModuleBlock); + return !!body && body.statements.some(s => isExportDeclaration(s) || isExportAssignment(s)); + } + function setExportContextFlag(node: ModuleDeclaration | SourceFile) { + // A declaration source file or ambient module declaration that contains no export declarations (but possibly regular + // declarations with export modifiers) is an export context in which declarations are implicitly exported. + if (node.flags & NodeFlags.Ambient && !hasExportDeclarations(node)) { + node.flags |= NodeFlags.ExportContext; } - - function declareModuleSymbol(node: ModuleDeclaration): ModuleInstanceState { - const state = getModuleInstanceState(node); - const instantiated = state !== ModuleInstanceState.NonInstantiated; - declareSymbolAndAddToSymbolTable(node, - instantiated ? SymbolFlags.ValueModule : SymbolFlags.NamespaceModule, - instantiated ? SymbolFlags.ValueModuleExcludes : SymbolFlags.NamespaceModuleExcludes); - return state; + else { + node.flags &= ~NodeFlags.ExportContext; } - - function bindFunctionOrConstructorType(node: SignatureDeclaration | JSDocSignature): void { - // For a given function symbol "<...>(...) => T" we want to generate a symbol identical - // to the one we would get for: { <...>(...): T } - // - // We do that by making an anonymous type literal symbol, and then setting the function - // symbol as its sole member. To the rest of the system, this symbol will be indistinguishable - // from an actual type literal symbol you would have gotten had you used the long form. - const symbol = createSymbol(SymbolFlags.Signature, getDeclarationName(node)!); // TODO: GH#18217 - addDeclarationToSymbol(symbol, node, SymbolFlags.Signature); - - const typeLiteralSymbol = createSymbol(SymbolFlags.TypeLiteral, InternalSymbolName.Type); - addDeclarationToSymbol(typeLiteralSymbol, node, SymbolFlags.TypeLiteral); - typeLiteralSymbol.members = createSymbolTable(); - typeLiteralSymbol.members.set(symbol.escapedName, symbol); - } - - function bindObjectLiteralExpression(node: ObjectLiteralExpression) { - const enum ElementKind { - Property = 1, - Accessor = 2 + } + function bindModuleDeclaration(node: ModuleDeclaration) { + setExportContextFlag(node); + if (isAmbientModule(node)) { + if (hasModifier(node, ModifierFlags.Export)) { + errorOnFirstToken(node, Diagnostics.export_modifier_cannot_be_applied_to_ambient_modules_and_module_augmentations_since_they_are_always_visible); } - - if (inStrictMode && !isAssignmentTarget(node)) { - const seen = createUnderscoreEscapedMap(); - - for (const prop of node.properties) { - if (prop.kind === SyntaxKind.SpreadAssignment || prop.name.kind !== SyntaxKind.Identifier) { - continue; - } - - const identifier = prop.name; - - // ECMA-262 11.1.5 Object Initializer - // If previous is not undefined then throw a SyntaxError exception if any of the following conditions are true - // a.This production is contained in strict code and IsDataDescriptor(previous) is true and - // IsDataDescriptor(propId.descriptor) is true. - // b.IsDataDescriptor(previous) is true and IsAccessorDescriptor(propId.descriptor) is true. - // c.IsAccessorDescriptor(previous) is true and IsDataDescriptor(propId.descriptor) is true. - // d.IsAccessorDescriptor(previous) is true and IsAccessorDescriptor(propId.descriptor) is true - // and either both previous and propId.descriptor have[[Get]] fields or both previous and propId.descriptor have[[Set]] fields - const currentKind = prop.kind === SyntaxKind.PropertyAssignment || prop.kind === SyntaxKind.ShorthandPropertyAssignment || prop.kind === SyntaxKind.MethodDeclaration - ? ElementKind.Property - : ElementKind.Accessor; - - const existingKind = seen.get(identifier.escapedText); - if (!existingKind) { - seen.set(identifier.escapedText, currentKind); - continue; + if (isModuleAugmentationExternal(node)) { + declareModuleSymbol(node); + } + else { + let pattern: Pattern | undefined; + if (node.name.kind === SyntaxKind.StringLiteral) { + const { text } = node.name; + if (hasZeroOrOneAsteriskCharacter(text)) { + pattern = tryParsePattern(text); } - - if (currentKind === ElementKind.Property && existingKind === ElementKind.Property) { - const span = getErrorSpanForNode(file, identifier); - file.bindDiagnostics.push(createFileDiagnostic(file, span.start, span.length, - Diagnostics.An_object_literal_cannot_have_multiple_properties_with_the_same_name_in_strict_mode)); + else { + errorOnFirstToken(node.name, Diagnostics.Pattern_0_can_have_at_most_one_Asterisk_character, text); } } + const symbol = (declareSymbolAndAddToSymbolTable(node, SymbolFlags.ValueModule, SymbolFlags.ValueModuleExcludes)!); + file.patternAmbientModules = append(file.patternAmbientModules, pattern && { pattern, symbol }); } - - return bindAnonymousDeclaration(node, SymbolFlags.ObjectLiteral, InternalSymbolName.Object); - } - - function bindJsxAttributes(node: JsxAttributes) { - return bindAnonymousDeclaration(node, SymbolFlags.ObjectLiteral, InternalSymbolName.JSXAttributes); - } - - function bindJsxAttribute(node: JsxAttribute, symbolFlags: SymbolFlags, symbolExcludes: SymbolFlags) { - return declareSymbolAndAddToSymbolTable(node, symbolFlags, symbolExcludes); - } - - function bindAnonymousDeclaration(node: Declaration, symbolFlags: SymbolFlags, name: __String) { - const symbol = createSymbol(symbolFlags, name); - if (symbolFlags & (SymbolFlags.EnumMember | SymbolFlags.ClassMember)) { - symbol.parent = container.symbol; + } + else { + const state = declareModuleSymbol(node); + if (state !== ModuleInstanceState.NonInstantiated) { + const { symbol } = node; + // if module was already merged with some function, class or non-const enum, treat it as non-const-enum-only + symbol.constEnumOnlyModule = (!(symbol.flags & (SymbolFlags.Function | SymbolFlags.Class | SymbolFlags.RegularEnum))) + // Current must be `const enum` only + && state === ModuleInstanceState.ConstEnumOnly + // Can't have been set to 'false' in a previous merged symbol. ('undefined' OK) + && symbol.constEnumOnlyModule !== false; } - addDeclarationToSymbol(symbol, node, symbolFlags); - return symbol; } - - function bindBlockScopedDeclaration(node: Declaration, symbolFlags: SymbolFlags, symbolExcludes: SymbolFlags) { - switch (blockScopeContainer.kind) { - case SyntaxKind.ModuleDeclaration: + } + function declareModuleSymbol(node: ModuleDeclaration): ModuleInstanceState { + const state = getModuleInstanceState(node); + const instantiated = state !== ModuleInstanceState.NonInstantiated; + declareSymbolAndAddToSymbolTable(node, instantiated ? SymbolFlags.ValueModule : SymbolFlags.NamespaceModule, instantiated ? SymbolFlags.ValueModuleExcludes : SymbolFlags.NamespaceModuleExcludes); + return state; + } + function bindFunctionOrConstructorType(node: SignatureDeclaration | JSDocSignature): void { + // For a given function symbol "<...>(...) => T" we want to generate a symbol identical + // to the one we would get for: { <...>(...): T } + // + // We do that by making an anonymous type literal symbol, and then setting the function + // symbol as its sole member. To the rest of the system, this symbol will be indistinguishable + // from an actual type literal symbol you would have gotten had you used the long form. + const symbol = createSymbol(SymbolFlags.Signature, (getDeclarationName(node)!)); // TODO: GH#18217 + addDeclarationToSymbol(symbol, node, SymbolFlags.Signature); + const typeLiteralSymbol = createSymbol(SymbolFlags.TypeLiteral, InternalSymbolName.Type); + addDeclarationToSymbol(typeLiteralSymbol, node, SymbolFlags.TypeLiteral); + typeLiteralSymbol.members = createSymbolTable(); + typeLiteralSymbol.members.set(symbol.escapedName, symbol); + } + function bindObjectLiteralExpression(node: ObjectLiteralExpression) { + const enum ElementKind { + Property = 1, + Accessor = 2 + } + if (inStrictMode && !isAssignmentTarget(node)) { + const seen = createUnderscoreEscapedMap(); + for (const prop of node.properties) { + if (prop.kind === SyntaxKind.SpreadAssignment || prop.name.kind !== SyntaxKind.Identifier) { + continue; + } + const identifier = prop.name; + // ECMA-262 11.1.5 Object Initializer + // If previous is not undefined then throw a SyntaxError exception if any of the following conditions are true + // a.This production is contained in strict code and IsDataDescriptor(previous) is true and + // IsDataDescriptor(propId.descriptor) is true. + // b.IsDataDescriptor(previous) is true and IsAccessorDescriptor(propId.descriptor) is true. + // c.IsAccessorDescriptor(previous) is true and IsDataDescriptor(propId.descriptor) is true. + // d.IsAccessorDescriptor(previous) is true and IsAccessorDescriptor(propId.descriptor) is true + // and either both previous and propId.descriptor have[[Get]] fields or both previous and propId.descriptor have[[Set]] fields + const currentKind = prop.kind === SyntaxKind.PropertyAssignment || prop.kind === SyntaxKind.ShorthandPropertyAssignment || prop.kind === SyntaxKind.MethodDeclaration + ? ElementKind.Property + : ElementKind.Accessor; + const existingKind = seen.get(identifier.escapedText); + if (!existingKind) { + seen.set(identifier.escapedText, currentKind); + continue; + } + if (currentKind === ElementKind.Property && existingKind === ElementKind.Property) { + const span = getErrorSpanForNode(file, identifier); + file.bindDiagnostics.push(createFileDiagnostic(file, span.start, span.length, Diagnostics.An_object_literal_cannot_have_multiple_properties_with_the_same_name_in_strict_mode)); + } + } + } + return bindAnonymousDeclaration(node, SymbolFlags.ObjectLiteral, InternalSymbolName.Object); + } + function bindJsxAttributes(node: JsxAttributes) { + return bindAnonymousDeclaration(node, SymbolFlags.ObjectLiteral, InternalSymbolName.JSXAttributes); + } + function bindJsxAttribute(node: JsxAttribute, symbolFlags: SymbolFlags, symbolExcludes: SymbolFlags) { + return declareSymbolAndAddToSymbolTable(node, symbolFlags, symbolExcludes); + } + function bindAnonymousDeclaration(node: Declaration, symbolFlags: SymbolFlags, name: __String) { + const symbol = createSymbol(symbolFlags, name); + if (symbolFlags & (SymbolFlags.EnumMember | SymbolFlags.ClassMember)) { + symbol.parent = container.symbol; + } + addDeclarationToSymbol(symbol, node, symbolFlags); + return symbol; + } + function bindBlockScopedDeclaration(node: Declaration, symbolFlags: SymbolFlags, symbolExcludes: SymbolFlags) { + switch (blockScopeContainer.kind) { + case SyntaxKind.ModuleDeclaration: + declareModuleMember(node, symbolFlags, symbolExcludes); + break; + case SyntaxKind.SourceFile: + if (isExternalOrCommonJsModule((container))) { declareModuleMember(node, symbolFlags, symbolExcludes); break; - case SyntaxKind.SourceFile: - if (isExternalOrCommonJsModule(container)) { - declareModuleMember(node, symbolFlags, symbolExcludes); - break; - } - // falls through - default: - if (!blockScopeContainer.locals) { - blockScopeContainer.locals = createSymbolTable(); - addToContainerChain(blockScopeContainer); - } - declareSymbol(blockScopeContainer.locals, /*parent*/ undefined, node, symbolFlags, symbolExcludes); - } + } + // falls through + default: + if (!blockScopeContainer.locals) { + blockScopeContainer.locals = createSymbolTable(); + addToContainerChain(blockScopeContainer); + } + declareSymbol(blockScopeContainer.locals, /*parent*/ undefined, node, symbolFlags, symbolExcludes); } - - function delayedBindJSDocTypedefTag() { - if (!delayedTypeAliases) { - return; - } - const saveContainer = container; - const saveLastContainer = lastContainer; - const saveBlockScopeContainer = blockScopeContainer; - const saveParent = parent; - const saveCurrentFlow = currentFlow; - for (const typeAlias of delayedTypeAliases) { - const host = getJSDocHost(typeAlias); - container = findAncestor(host.parent, n => !!(getContainerFlags(n) & ContainerFlags.IsContainer)) || file; - blockScopeContainer = getEnclosingBlockScopeContainer(host) || file; - currentFlow = initFlowNode({ flags: FlowFlags.Start }); - parent = typeAlias; - bind(typeAlias.typeExpression); - const declName = getNameOfDeclaration(typeAlias); - if ((isJSDocEnumTag(typeAlias) || !typeAlias.fullName) && declName && isPropertyAccessEntityNameExpression(declName.parent)) { - // typedef anchored to an A.B.C assignment - we need to bind into B's namespace under name C - const isTopLevel = isTopLevelNamespaceAssignment(declName.parent); - if (isTopLevel) { - bindPotentiallyMissingNamespaces(file.symbol, declName.parent, isTopLevel, - !!findAncestor(declName, d => isPropertyAccessExpression(d) && d.name.escapedText === "prototype"), /*containerIsClass*/ false); - const oldContainer = container; - switch (getAssignmentDeclarationPropertyAccessKind(declName.parent)) { - case AssignmentDeclarationKind.ExportsProperty: - case AssignmentDeclarationKind.ModuleExports: - if (!isExternalOrCommonJsModule(file)) { - container = undefined!; - } - else { - container = file; - } - break; - case AssignmentDeclarationKind.ThisProperty: - container = declName.parent.expression; - break; - case AssignmentDeclarationKind.PrototypeProperty: - container = (declName.parent.expression as PropertyAccessExpression).name; - break; - case AssignmentDeclarationKind.Property: - container = isExportsOrModuleExportsOrAlias(file, declName.parent.expression) ? file - : isPropertyAccessExpression(declName.parent.expression) ? declName.parent.expression.name + } + function delayedBindJSDocTypedefTag() { + if (!delayedTypeAliases) { + return; + } + const saveContainer = container; + const saveLastContainer = lastContainer; + const saveBlockScopeContainer = blockScopeContainer; + const saveParent = parent; + const saveCurrentFlow = currentFlow; + for (const typeAlias of delayedTypeAliases) { + const host = getJSDocHost(typeAlias); + container = findAncestor(host.parent, n => !!(getContainerFlags(n) & ContainerFlags.IsContainer)) || file; + blockScopeContainer = getEnclosingBlockScopeContainer(host) || file; + currentFlow = initFlowNode({ flags: FlowFlags.Start }); + parent = typeAlias; + bind(typeAlias.typeExpression); + const declName = getNameOfDeclaration(typeAlias); + if ((isJSDocEnumTag(typeAlias) || !typeAlias.fullName) && declName && isPropertyAccessEntityNameExpression(declName.parent)) { + // typedef anchored to an A.B.C assignment - we need to bind into B's namespace under name C + const isTopLevel = isTopLevelNamespaceAssignment(declName.parent); + if (isTopLevel) { + bindPotentiallyMissingNamespaces(file.symbol, declName.parent, isTopLevel, !!findAncestor(declName, d => isPropertyAccessExpression(d) && d.name.escapedText === "prototype"), /*containerIsClass*/ false); + const oldContainer = container; + switch (getAssignmentDeclarationPropertyAccessKind(declName.parent)) { + case AssignmentDeclarationKind.ExportsProperty: + case AssignmentDeclarationKind.ModuleExports: + if (!isExternalOrCommonJsModule(file)) { + container = undefined!; + } + else { + container = file; + } + break; + case AssignmentDeclarationKind.ThisProperty: + container = declName.parent.expression; + break; + case AssignmentDeclarationKind.PrototypeProperty: + container = (declName.parent.expression as PropertyAccessExpression).name; + break; + case AssignmentDeclarationKind.Property: + container = isExportsOrModuleExportsOrAlias(file, declName.parent.expression) ? file + : isPropertyAccessExpression(declName.parent.expression) ? declName.parent.expression.name : declName.parent.expression; - break; - case AssignmentDeclarationKind.None: - return Debug.fail("Shouldn't have detected typedef or enum on non-assignment declaration"); - } - if (container) { - declareModuleMember(typeAlias, SymbolFlags.TypeAlias, SymbolFlags.TypeAliasExcludes); - } - container = oldContainer; + break; + case AssignmentDeclarationKind.None: + return Debug.fail("Shouldn't have detected typedef or enum on non-assignment declaration"); } - } - else if (isJSDocEnumTag(typeAlias) || !typeAlias.fullName || typeAlias.fullName.kind === SyntaxKind.Identifier) { - parent = typeAlias.parent; - bindBlockScopedDeclaration(typeAlias, SymbolFlags.TypeAlias, SymbolFlags.TypeAliasExcludes); - } - else { - bind(typeAlias.fullName); - } - } - container = saveContainer; - lastContainer = saveLastContainer; - blockScopeContainer = saveBlockScopeContainer; - parent = saveParent; - currentFlow = saveCurrentFlow; - } - - // The binder visits every node in the syntax tree so it is a convenient place to perform a single localized - // check for reserved words used as identifiers in strict mode code. - function checkStrictModeIdentifier(node: Identifier) { - if (inStrictMode && - node.originalKeywordKind! >= SyntaxKind.FirstFutureReservedWord && - node.originalKeywordKind! <= SyntaxKind.LastFutureReservedWord && - !isIdentifierName(node) && - !(node.flags & NodeFlags.Ambient) && - !(node.flags & NodeFlags.JSDoc)) { - - // Report error only if there are no parse errors in file - if (!file.parseDiagnostics.length) { - file.bindDiagnostics.push(createDiagnosticForNode(node, - getStrictModeIdentifierMessage(node), declarationNameToString(node))); + if (container) { + declareModuleMember(typeAlias, SymbolFlags.TypeAlias, SymbolFlags.TypeAliasExcludes); + } + container = oldContainer; } } - } - - function getStrictModeIdentifierMessage(node: Node) { - // Provide specialized messages to help the user understand why we think they're in - // strict mode. - if (getContainingClass(node)) { - return Diagnostics.Identifier_expected_0_is_a_reserved_word_in_strict_mode_Class_definitions_are_automatically_in_strict_mode; - } - - if (file.externalModuleIndicator) { - return Diagnostics.Identifier_expected_0_is_a_reserved_word_in_strict_mode_Modules_are_automatically_in_strict_mode; - } - - return Diagnostics.Identifier_expected_0_is_a_reserved_word_in_strict_mode; - } - - // The binder visits every node, so this is a good place to check for - // the reserved private name (there is only one) - function checkPrivateIdentifier(node: PrivateIdentifier) { - if (node.escapedText === "#constructor") { - // Report error only if there are no parse errors in file - if (!file.parseDiagnostics.length) { - file.bindDiagnostics.push(createDiagnosticForNode(node, - Diagnostics.constructor_is_a_reserved_word, declarationNameToString(node))); - } + else if (isJSDocEnumTag(typeAlias) || !typeAlias.fullName || typeAlias.fullName.kind === SyntaxKind.Identifier) { + parent = typeAlias.parent; + bindBlockScopedDeclaration(typeAlias, SymbolFlags.TypeAlias, SymbolFlags.TypeAliasExcludes); } - } - - function checkStrictModeBinaryExpression(node: BinaryExpression) { - if (inStrictMode && isLeftHandSideExpression(node.left) && isAssignmentOperator(node.operatorToken.kind)) { - // ECMA 262 (Annex C) The identifier eval or arguments may not appear as the LeftHandSideExpression of an - // Assignment operator(11.13) or of a PostfixExpression(11.3) - checkStrictModeEvalOrArguments(node, node.left); + else { + bind(typeAlias.fullName); } } - - function checkStrictModeCatchClause(node: CatchClause) { - // It is a SyntaxError if a TryStatement with a Catch occurs within strict code and the Identifier of the - // Catch production is eval or arguments - if (inStrictMode && node.variableDeclaration) { - checkStrictModeEvalOrArguments(node, node.variableDeclaration.name); + container = saveContainer; + lastContainer = saveLastContainer; + blockScopeContainer = saveBlockScopeContainer; + parent = saveParent; + currentFlow = saveCurrentFlow; + } + // The binder visits every node in the syntax tree so it is a convenient place to perform a single localized + // check for reserved words used as identifiers in strict mode code. + function checkStrictModeIdentifier(node: Identifier) { + if (inStrictMode && + (node.originalKeywordKind!) >= SyntaxKind.FirstFutureReservedWord && + (node.originalKeywordKind!) <= SyntaxKind.LastFutureReservedWord && + !isIdentifierName(node) && + !(node.flags & NodeFlags.Ambient) && + !(node.flags & NodeFlags.JSDoc)) { + // Report error only if there are no parse errors in file + if (!file.parseDiagnostics.length) { + file.bindDiagnostics.push(createDiagnosticForNode(node, getStrictModeIdentifierMessage(node), declarationNameToString(node))); } } - - function checkStrictModeDeleteExpression(node: DeleteExpression) { - // Grammar checking - if (inStrictMode && node.expression.kind === SyntaxKind.Identifier) { - // When a delete operator occurs within strict mode code, a SyntaxError is thrown if its - // UnaryExpression is a direct reference to a variable, function argument, or function name - const span = getErrorSpanForNode(file, node.expression); - file.bindDiagnostics.push(createFileDiagnostic(file, span.start, span.length, Diagnostics.delete_cannot_be_called_on_an_identifier_in_strict_mode)); - } + } + function getStrictModeIdentifierMessage(node: Node) { + // Provide specialized messages to help the user understand why we think they're in + // strict mode. + if (getContainingClass(node)) { + return Diagnostics.Identifier_expected_0_is_a_reserved_word_in_strict_mode_Class_definitions_are_automatically_in_strict_mode; } - - function isEvalOrArgumentsIdentifier(node: Node): boolean { - return isIdentifier(node) && (node.escapedText === "eval" || node.escapedText === "arguments"); - } - - function checkStrictModeEvalOrArguments(contextNode: Node, name: Node | undefined) { - if (name && name.kind === SyntaxKind.Identifier) { - const identifier = name; - if (isEvalOrArgumentsIdentifier(identifier)) { - // We check first if the name is inside class declaration or class expression; if so give explicit message - // otherwise report generic error message. - const span = getErrorSpanForNode(file, name); - file.bindDiagnostics.push(createFileDiagnostic(file, span.start, span.length, - getStrictModeEvalOrArgumentsMessage(contextNode), idText(identifier))); - } - } + if (file.externalModuleIndicator) { + return Diagnostics.Identifier_expected_0_is_a_reserved_word_in_strict_mode_Modules_are_automatically_in_strict_mode; } - - function getStrictModeEvalOrArgumentsMessage(node: Node) { - // Provide specialized messages to help the user understand why we think they're in - // strict mode. - if (getContainingClass(node)) { - return Diagnostics.Invalid_use_of_0_Class_definitions_are_automatically_in_strict_mode; - } - - if (file.externalModuleIndicator) { - return Diagnostics.Invalid_use_of_0_Modules_are_automatically_in_strict_mode; - } - - return Diagnostics.Invalid_use_of_0_in_strict_mode; - } - - function checkStrictModeFunctionName(node: FunctionLikeDeclaration) { - if (inStrictMode) { - // It is a SyntaxError if the identifier eval or arguments appears within a FormalParameterList of a strict mode FunctionDeclaration or FunctionExpression (13.1)) - checkStrictModeEvalOrArguments(node, node.name); + return Diagnostics.Identifier_expected_0_is_a_reserved_word_in_strict_mode; + } + // The binder visits every node, so this is a good place to check for + // the reserved private name (there is only one) + function checkPrivateIdentifier(node: PrivateIdentifier) { + if (node.escapedText === "#constructor") { + // Report error only if there are no parse errors in file + if (!file.parseDiagnostics.length) { + file.bindDiagnostics.push(createDiagnosticForNode(node, Diagnostics.constructor_is_a_reserved_word, declarationNameToString(node))); } } - - function getStrictModeBlockScopeFunctionDeclarationMessage(node: Node) { - // Provide specialized messages to help the user understand why we think they're in - // strict mode. - if (getContainingClass(node)) { - return Diagnostics.Function_declarations_are_not_allowed_inside_blocks_in_strict_mode_when_targeting_ES3_or_ES5_Class_definitions_are_automatically_in_strict_mode; - } - - if (file.externalModuleIndicator) { - return Diagnostics.Function_declarations_are_not_allowed_inside_blocks_in_strict_mode_when_targeting_ES3_or_ES5_Modules_are_automatically_in_strict_mode; - } - - return Diagnostics.Function_declarations_are_not_allowed_inside_blocks_in_strict_mode_when_targeting_ES3_or_ES5; - } - - function checkStrictModeFunctionDeclaration(node: FunctionDeclaration) { - if (languageVersion < ScriptTarget.ES2015) { - // Report error if function is not top level function declaration - if (blockScopeContainer.kind !== SyntaxKind.SourceFile && - blockScopeContainer.kind !== SyntaxKind.ModuleDeclaration && - !isFunctionLike(blockScopeContainer)) { - // We check first if the name is inside class declaration or class expression; if so give explicit message - // otherwise report generic error message. - const errorSpan = getErrorSpanForNode(file, node); - file.bindDiagnostics.push(createFileDiagnostic(file, errorSpan.start, errorSpan.length, - getStrictModeBlockScopeFunctionDeclarationMessage(node))); - } - } + } + function checkStrictModeBinaryExpression(node: BinaryExpression) { + if (inStrictMode && isLeftHandSideExpression(node.left) && isAssignmentOperator(node.operatorToken.kind)) { + // ECMA 262 (Annex C) The identifier eval or arguments may not appear as the LeftHandSideExpression of an + // Assignment operator(11.13) or of a PostfixExpression(11.3) + checkStrictModeEvalOrArguments(node, (node.left)); } - - function checkStrictModeNumericLiteral(node: NumericLiteral) { - if (inStrictMode && node.numericLiteralFlags & TokenFlags.Octal) { - file.bindDiagnostics.push(createDiagnosticForNode(node, Diagnostics.Octal_literals_are_not_allowed_in_strict_mode)); - } + } + function checkStrictModeCatchClause(node: CatchClause) { + // It is a SyntaxError if a TryStatement with a Catch occurs within strict code and the Identifier of the + // Catch production is eval or arguments + if (inStrictMode && node.variableDeclaration) { + checkStrictModeEvalOrArguments(node, node.variableDeclaration.name); } - - function checkStrictModePostfixUnaryExpression(node: PostfixUnaryExpression) { - // Grammar checking - // The identifier eval or arguments may not appear as the LeftHandSideExpression of an - // Assignment operator(11.13) or of a PostfixExpression(11.3) or as the UnaryExpression - // operated upon by a Prefix Increment(11.4.4) or a Prefix Decrement(11.4.5) operator. - if (inStrictMode) { - checkStrictModeEvalOrArguments(node, node.operand); - } + } + function checkStrictModeDeleteExpression(node: DeleteExpression) { + // Grammar checking + if (inStrictMode && node.expression.kind === SyntaxKind.Identifier) { + // When a delete operator occurs within strict mode code, a SyntaxError is thrown if its + // UnaryExpression is a direct reference to a variable, function argument, or function name + const span = getErrorSpanForNode(file, node.expression); + file.bindDiagnostics.push(createFileDiagnostic(file, span.start, span.length, Diagnostics.delete_cannot_be_called_on_an_identifier_in_strict_mode)); } - - function checkStrictModePrefixUnaryExpression(node: PrefixUnaryExpression) { - // Grammar checking - if (inStrictMode) { - if (node.operator === SyntaxKind.PlusPlusToken || node.operator === SyntaxKind.MinusMinusToken) { - checkStrictModeEvalOrArguments(node, node.operand); - } + } + function isEvalOrArgumentsIdentifier(node: Node): boolean { + return isIdentifier(node) && (node.escapedText === "eval" || node.escapedText === "arguments"); + } + function checkStrictModeEvalOrArguments(contextNode: Node, name: Node | undefined) { + if (name && name.kind === SyntaxKind.Identifier) { + const identifier = (name); + if (isEvalOrArgumentsIdentifier(identifier)) { + // We check first if the name is inside class declaration or class expression; if so give explicit message + // otherwise report generic error message. + const span = getErrorSpanForNode(file, name); + file.bindDiagnostics.push(createFileDiagnostic(file, span.start, span.length, getStrictModeEvalOrArgumentsMessage(contextNode), idText(identifier))); } } - - function checkStrictModeWithStatement(node: WithStatement) { - // Grammar checking for withStatement - if (inStrictMode) { - errorOnFirstToken(node, Diagnostics.with_statements_are_not_allowed_in_strict_mode); - } + } + function getStrictModeEvalOrArgumentsMessage(node: Node) { + // Provide specialized messages to help the user understand why we think they're in + // strict mode. + if (getContainingClass(node)) { + return Diagnostics.Invalid_use_of_0_Class_definitions_are_automatically_in_strict_mode; } - - function checkStrictModeLabeledStatement(node: LabeledStatement) { - // Grammar checking for labeledStatement - if (inStrictMode && options.target! >= ScriptTarget.ES2015) { - if (isDeclarationStatement(node.statement) || isVariableStatement(node.statement)) { - errorOnFirstToken(node.label, Diagnostics.A_label_is_not_allowed_here); - } - } + if (file.externalModuleIndicator) { + return Diagnostics.Invalid_use_of_0_Modules_are_automatically_in_strict_mode; } - - function errorOnFirstToken(node: Node, message: DiagnosticMessage, arg0?: any, arg1?: any, arg2?: any) { - const span = getSpanOfTokenAtPosition(file, node.pos); - file.bindDiagnostics.push(createFileDiagnostic(file, span.start, span.length, message, arg0, arg1, arg2)); + return Diagnostics.Invalid_use_of_0_in_strict_mode; + } + function checkStrictModeFunctionName(node: FunctionLikeDeclaration) { + if (inStrictMode) { + // It is a SyntaxError if the identifier eval or arguments appears within a FormalParameterList of a strict mode FunctionDeclaration or FunctionExpression (13.1)) + checkStrictModeEvalOrArguments(node, node.name); } - - function errorOrSuggestionOnNode(isError: boolean, node: Node, message: DiagnosticMessage): void { - errorOrSuggestionOnRange(isError, node, node, message); + } + function getStrictModeBlockScopeFunctionDeclarationMessage(node: Node) { + // Provide specialized messages to help the user understand why we think they're in + // strict mode. + if (getContainingClass(node)) { + return Diagnostics.Function_declarations_are_not_allowed_inside_blocks_in_strict_mode_when_targeting_ES3_or_ES5_Class_definitions_are_automatically_in_strict_mode; } - - function errorOrSuggestionOnRange(isError: boolean, startNode: Node, endNode: Node, message: DiagnosticMessage): void { - addErrorOrSuggestionDiagnostic(isError, { pos: getTokenPosOfNode(startNode, file), end: endNode.end }, message); + if (file.externalModuleIndicator) { + return Diagnostics.Function_declarations_are_not_allowed_inside_blocks_in_strict_mode_when_targeting_ES3_or_ES5_Modules_are_automatically_in_strict_mode; } - - function addErrorOrSuggestionDiagnostic(isError: boolean, range: TextRange, message: DiagnosticMessage): void { - const diag = createFileDiagnostic(file, range.pos, range.end - range.pos, message); - if (isError) { - file.bindDiagnostics.push(diag); - } - else { - file.bindSuggestionDiagnostics = append(file.bindSuggestionDiagnostics, { ...diag, category: DiagnosticCategory.Suggestion }); + return Diagnostics.Function_declarations_are_not_allowed_inside_blocks_in_strict_mode_when_targeting_ES3_or_ES5; + } + function checkStrictModeFunctionDeclaration(node: FunctionDeclaration) { + if (languageVersion < ScriptTarget.ES2015) { + // Report error if function is not top level function declaration + if (blockScopeContainer.kind !== SyntaxKind.SourceFile && + blockScopeContainer.kind !== SyntaxKind.ModuleDeclaration && + !isFunctionLike(blockScopeContainer)) { + // We check first if the name is inside class declaration or class expression; if so give explicit message + // otherwise report generic error message. + const errorSpan = getErrorSpanForNode(file, node); + file.bindDiagnostics.push(createFileDiagnostic(file, errorSpan.start, errorSpan.length, getStrictModeBlockScopeFunctionDeclarationMessage(node))); } } - - function bind(node: Node | undefined): void { - if (!node) { - return; - } - node.parent = parent; - const saveInStrictMode = inStrictMode; - - // Even though in the AST the jsdoc @typedef node belongs to the current node, - // its symbol might be in the same scope with the current node's symbol. Consider: - // - // /** @typedef {string | number} MyType */ - // function foo(); - // - // Here the current node is "foo", which is a container, but the scope of "MyType" should - // not be inside "foo". Therefore we always bind @typedef before bind the parent node, - // and skip binding this tag later when binding all the other jsdoc tags. - - // First we bind declaration nodes to a symbol if possible. We'll both create a symbol - // and then potentially add the symbol to an appropriate symbol table. Possible - // destination symbol tables are: - // - // 1) The 'exports' table of the current container's symbol. - // 2) The 'members' table of the current container's symbol. - // 3) The 'locals' table of the current container. - // - // However, not all symbols will end up in any of these tables. 'Anonymous' symbols - // (like TypeLiterals for example) will not be put in any table. - bindWorker(node); - // Then we recurse into the children of the node to bind them as well. For certain - // symbols we do specialized work when we recurse. For example, we'll keep track of - // the current 'container' node when it changes. This helps us know which symbol table - // a local should go into for example. Since terminal nodes are known not to have - // children, as an optimization we don't process those. - if (node.kind > SyntaxKind.LastToken) { - const saveParent = parent; - parent = node; - const containerFlags = getContainerFlags(node); - if (containerFlags === ContainerFlags.None) { - bindChildren(node); - } - else { - bindContainer(node, containerFlags); - } - parent = saveParent; - } - else if (!skipTransformFlagAggregation && (node.transformFlags & TransformFlags.HasComputedFlags) === 0) { - subtreeTransformFlags |= computeTransformFlagsForNode(node, 0); - const saveParent = parent; - if (node.kind === SyntaxKind.EndOfFileToken) parent = node; - bindJSDoc(node); - parent = saveParent; - } - inStrictMode = saveInStrictMode; - } - - function bindJSDoc(node: Node) { - if (hasJSDocNodes(node)) { - if (isInJSFile(node)) { - for (const j of node.jsDoc!) { - bind(j); - } - } - else { - for (const j of node.jsDoc!) { - setParentPointers(node, j); - } - } - } + } + function checkStrictModeNumericLiteral(node: NumericLiteral) { + if (inStrictMode && node.numericLiteralFlags & TokenFlags.Octal) { + file.bindDiagnostics.push(createDiagnosticForNode(node, Diagnostics.Octal_literals_are_not_allowed_in_strict_mode)); } - - function updateStrictModeStatementList(statements: NodeArray) { - if (!inStrictMode) { - for (const statement of statements) { - if (!isPrologueDirective(statement)) { - return; - } - - if (isUseStrictPrologueDirective(statement)) { - inStrictMode = true; - return; - } - } - } + } + function checkStrictModePostfixUnaryExpression(node: PostfixUnaryExpression) { + // Grammar checking + // The identifier eval or arguments may not appear as the LeftHandSideExpression of an + // Assignment operator(11.13) or of a PostfixExpression(11.3) or as the UnaryExpression + // operated upon by a Prefix Increment(11.4.4) or a Prefix Decrement(11.4.5) operator. + if (inStrictMode) { + checkStrictModeEvalOrArguments(node, (node.operand)); } - - /// Should be called only on prologue directives (isPrologueDirective(node) should be true) - function isUseStrictPrologueDirective(node: ExpressionStatement): boolean { - const nodeText = getSourceTextOfNodeFromSourceFile(file, node.expression); - - // Note: the node text must be exactly "use strict" or 'use strict'. It is not ok for the - // string to contain unicode escapes (as per ES5). - return nodeText === '"use strict"' || nodeText === "'use strict'"; - } - - function bindWorker(node: Node) { - switch (node.kind) { - /* Strict mode checks */ - case SyntaxKind.Identifier: - // for typedef type names with namespaces, bind the new jsdoc type symbol here - // because it requires all containing namespaces to be in effect, namely the - // current "blockScopeContainer" needs to be set to its immediate namespace parent. - if ((node).isInJSDocNamespace) { - let parentNode = node.parent; - while (parentNode && !isJSDocTypeAlias(parentNode)) { - parentNode = parentNode.parent; - } - bindBlockScopedDeclaration(parentNode as Declaration, SymbolFlags.TypeAlias, SymbolFlags.TypeAliasExcludes); - break; - } - // falls through - case SyntaxKind.ThisKeyword: - if (currentFlow && (isExpression(node) || parent.kind === SyntaxKind.ShorthandPropertyAssignment)) { - node.flowNode = currentFlow; - } - return checkStrictModeIdentifier(node); - case SyntaxKind.PrivateIdentifier: - return checkPrivateIdentifier(node as PrivateIdentifier); - case SyntaxKind.PropertyAccessExpression: - case SyntaxKind.ElementAccessExpression: - const expr = node as PropertyAccessExpression | ElementAccessExpression; - if (currentFlow && isNarrowableReference(expr)) { - expr.flowNode = currentFlow; - } - if (isSpecialPropertyDeclaration(expr)) { - bindSpecialPropertyDeclaration(expr); - } - if (isInJSFile(expr) && - file.commonJsModuleIndicator && - isModuleExportsAccessExpression(expr) && - !lookupSymbolForNameWorker(blockScopeContainer, "module" as __String)) { - declareSymbol(file.locals!, /*parent*/ undefined, expr.expression, - SymbolFlags.FunctionScopedVariable | SymbolFlags.ModuleExports, SymbolFlags.FunctionScopedVariableExcludes); - } - break; - case SyntaxKind.BinaryExpression: - const specialKind = getAssignmentDeclarationKind(node as BinaryExpression); - switch (specialKind) { - case AssignmentDeclarationKind.ExportsProperty: - bindExportsPropertyAssignment(node as BindableStaticPropertyAssignmentExpression); - break; - case AssignmentDeclarationKind.ModuleExports: - bindModuleExportsAssignment(node as BindablePropertyAssignmentExpression); - break; - case AssignmentDeclarationKind.PrototypeProperty: - bindPrototypePropertyAssignment((node as BindableStaticPropertyAssignmentExpression).left, node); - break; - case AssignmentDeclarationKind.Prototype: - bindPrototypeAssignment(node as BindableStaticPropertyAssignmentExpression); - break; - case AssignmentDeclarationKind.ThisProperty: - bindThisPropertyAssignment(node as BindablePropertyAssignmentExpression); - break; - case AssignmentDeclarationKind.Property: - bindSpecialPropertyAssignment(node as BindablePropertyAssignmentExpression); - break; - case AssignmentDeclarationKind.None: - // Nothing to do - break; - default: - Debug.fail("Unknown binary expression special property assignment kind"); - } - return checkStrictModeBinaryExpression(node); - case SyntaxKind.CatchClause: - return checkStrictModeCatchClause(node); - case SyntaxKind.DeleteExpression: - return checkStrictModeDeleteExpression(node); - case SyntaxKind.NumericLiteral: - return checkStrictModeNumericLiteral(node); - case SyntaxKind.PostfixUnaryExpression: - return checkStrictModePostfixUnaryExpression(node); - case SyntaxKind.PrefixUnaryExpression: - return checkStrictModePrefixUnaryExpression(node); - case SyntaxKind.WithStatement: - return checkStrictModeWithStatement(node); - case SyntaxKind.LabeledStatement: - return checkStrictModeLabeledStatement(node); - case SyntaxKind.ThisType: - seenThisKeyword = true; - return; - case SyntaxKind.TypePredicate: - break; // Binding the children will handle everything - case SyntaxKind.TypeParameter: - return bindTypeParameter(node as TypeParameterDeclaration); - case SyntaxKind.Parameter: - return bindParameter(node); - case SyntaxKind.VariableDeclaration: - return bindVariableDeclarationOrBindingElement(node); - case SyntaxKind.BindingElement: - node.flowNode = currentFlow; - return bindVariableDeclarationOrBindingElement(node); - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.PropertySignature: - return bindPropertyWorker(node as PropertyDeclaration | PropertySignature); - case SyntaxKind.PropertyAssignment: - case SyntaxKind.ShorthandPropertyAssignment: - return bindPropertyOrMethodOrAccessor(node, SymbolFlags.Property, SymbolFlags.PropertyExcludes); - case SyntaxKind.EnumMember: - return bindPropertyOrMethodOrAccessor(node, SymbolFlags.EnumMember, SymbolFlags.EnumMemberExcludes); - - case SyntaxKind.CallSignature: - case SyntaxKind.ConstructSignature: - case SyntaxKind.IndexSignature: - return declareSymbolAndAddToSymbolTable(node, SymbolFlags.Signature, SymbolFlags.None); - case SyntaxKind.MethodDeclaration: - case SyntaxKind.MethodSignature: - // If this is an ObjectLiteralExpression method, then it sits in the same space - // as other properties in the object literal. So we use SymbolFlags.PropertyExcludes - // so that it will conflict with any other object literal members with the same - // name. - return bindPropertyOrMethodOrAccessor(node, SymbolFlags.Method | ((node).questionToken ? SymbolFlags.Optional : SymbolFlags.None), - isObjectLiteralMethod(node) ? SymbolFlags.PropertyExcludes : SymbolFlags.MethodExcludes); - case SyntaxKind.FunctionDeclaration: - return bindFunctionDeclaration(node); - case SyntaxKind.Constructor: - return declareSymbolAndAddToSymbolTable(node, SymbolFlags.Constructor, /*symbolExcludes:*/ SymbolFlags.None); - case SyntaxKind.GetAccessor: - return bindPropertyOrMethodOrAccessor(node, SymbolFlags.GetAccessor, SymbolFlags.GetAccessorExcludes); - case SyntaxKind.SetAccessor: - return bindPropertyOrMethodOrAccessor(node, SymbolFlags.SetAccessor, SymbolFlags.SetAccessorExcludes); - case SyntaxKind.FunctionType: - case SyntaxKind.JSDocFunctionType: - case SyntaxKind.JSDocSignature: - case SyntaxKind.ConstructorType: - return bindFunctionOrConstructorType(node); - case SyntaxKind.TypeLiteral: - case SyntaxKind.JSDocTypeLiteral: - case SyntaxKind.MappedType: - return bindAnonymousTypeWorker(node as TypeLiteralNode | MappedTypeNode | JSDocTypeLiteral); - case SyntaxKind.JSDocClassTag: - return bindJSDocClassTag(node as JSDocClassTag); - case SyntaxKind.ObjectLiteralExpression: - return bindObjectLiteralExpression(node); - case SyntaxKind.FunctionExpression: - case SyntaxKind.ArrowFunction: - return bindFunctionExpression(node); - - case SyntaxKind.CallExpression: - const assignmentKind = getAssignmentDeclarationKind(node as CallExpression); - switch (assignmentKind) { - case AssignmentDeclarationKind.ObjectDefinePropertyValue: - return bindObjectDefinePropertyAssignment(node as BindableObjectDefinePropertyCall); - case AssignmentDeclarationKind.ObjectDefinePropertyExports: - return bindObjectDefinePropertyExport(node as BindableObjectDefinePropertyCall); - case AssignmentDeclarationKind.ObjectDefinePrototypeProperty: - return bindObjectDefinePrototypeProperty(node as BindableObjectDefinePropertyCall); - case AssignmentDeclarationKind.None: - break; // Nothing to do - default: - return Debug.fail("Unknown call expression assignment declaration kind"); - } - if (isInJSFile(node)) { - bindCallExpression(node); - } - break; - - // Members of classes, interfaces, and modules - case SyntaxKind.ClassExpression: - case SyntaxKind.ClassDeclaration: - // All classes are automatically in strict mode in ES6. - inStrictMode = true; - return bindClassLikeDeclaration(node); - case SyntaxKind.InterfaceDeclaration: - return bindBlockScopedDeclaration(node, SymbolFlags.Interface, SymbolFlags.InterfaceExcludes); - case SyntaxKind.TypeAliasDeclaration: - return bindBlockScopedDeclaration(node, SymbolFlags.TypeAlias, SymbolFlags.TypeAliasExcludes); - case SyntaxKind.EnumDeclaration: - return bindEnumDeclaration(node); - case SyntaxKind.ModuleDeclaration: - return bindModuleDeclaration(node); - // Jsx-attributes - case SyntaxKind.JsxAttributes: - return bindJsxAttributes(node); - case SyntaxKind.JsxAttribute: - return bindJsxAttribute(node, SymbolFlags.Property, SymbolFlags.PropertyExcludes); - - // Imports and exports - case SyntaxKind.ImportEqualsDeclaration: - case SyntaxKind.NamespaceImport: - case SyntaxKind.ImportSpecifier: - case SyntaxKind.ExportSpecifier: - return declareSymbolAndAddToSymbolTable(node, SymbolFlags.Alias, SymbolFlags.AliasExcludes); - case SyntaxKind.NamespaceExportDeclaration: - return bindNamespaceExportDeclaration(node); - case SyntaxKind.ImportClause: - return bindImportClause(node); - case SyntaxKind.ExportDeclaration: - return bindExportDeclaration(node); - case SyntaxKind.ExportAssignment: - return bindExportAssignment(node); - case SyntaxKind.SourceFile: - updateStrictModeStatementList((node).statements); - return bindSourceFileIfExternalModule(); - case SyntaxKind.Block: - if (!isFunctionLike(node.parent)) { - return; - } - // falls through - case SyntaxKind.ModuleBlock: - return updateStrictModeStatementList((node).statements); - - case SyntaxKind.JSDocParameterTag: - if (node.parent.kind === SyntaxKind.JSDocSignature) { - return bindParameter(node as JSDocParameterTag); - } - if (node.parent.kind !== SyntaxKind.JSDocTypeLiteral) { - break; - } - // falls through - case SyntaxKind.JSDocPropertyTag: - const propTag = node as JSDocPropertyLikeTag; - const flags = propTag.isBracketed || propTag.typeExpression && propTag.typeExpression.type.kind === SyntaxKind.JSDocOptionalType ? - SymbolFlags.Property | SymbolFlags.Optional : - SymbolFlags.Property; - return declareSymbolAndAddToSymbolTable(propTag, flags, SymbolFlags.PropertyExcludes); - case SyntaxKind.JSDocTypedefTag: - case SyntaxKind.JSDocCallbackTag: - case SyntaxKind.JSDocEnumTag: - return (delayedTypeAliases || (delayedTypeAliases = [])).push(node as JSDocTypedefTag | JSDocCallbackTag | JSDocEnumTag); + } + function checkStrictModePrefixUnaryExpression(node: PrefixUnaryExpression) { + // Grammar checking + if (inStrictMode) { + if (node.operator === SyntaxKind.PlusPlusToken || node.operator === SyntaxKind.MinusMinusToken) { + checkStrictModeEvalOrArguments(node, (node.operand)); } } - - function bindPropertyWorker(node: PropertyDeclaration | PropertySignature) { - return bindPropertyOrMethodOrAccessor(node, SymbolFlags.Property | (node.questionToken ? SymbolFlags.Optional : SymbolFlags.None), SymbolFlags.PropertyExcludes); - } - - function bindAnonymousTypeWorker(node: TypeLiteralNode | MappedTypeNode | JSDocTypeLiteral) { - return bindAnonymousDeclaration(node, SymbolFlags.TypeLiteral, InternalSymbolName.Type); + } + function checkStrictModeWithStatement(node: WithStatement) { + // Grammar checking for withStatement + if (inStrictMode) { + errorOnFirstToken(node, Diagnostics.with_statements_are_not_allowed_in_strict_mode); } - - function bindSourceFileIfExternalModule() { - setExportContextFlag(file); - if (isExternalModule(file)) { - bindSourceFileAsExternalModule(); - } - else if (isJsonSourceFile(file)) { - bindSourceFileAsExternalModule(); - // Create symbol equivalent for the module.exports = {} - const originalSymbol = file.symbol; - declareSymbol(file.symbol.exports!, file.symbol, file, SymbolFlags.Property, SymbolFlags.All); - file.symbol = originalSymbol; + } + function checkStrictModeLabeledStatement(node: LabeledStatement) { + // Grammar checking for labeledStatement + if (inStrictMode && (options.target!) >= ScriptTarget.ES2015) { + if (isDeclarationStatement(node.statement) || isVariableStatement(node.statement)) { + errorOnFirstToken(node.label, Diagnostics.A_label_is_not_allowed_here); } } - - function bindSourceFileAsExternalModule() { - bindAnonymousDeclaration(file, SymbolFlags.ValueModule, `"${removeFileExtension(file.fileName)}"` as __String); + } + function errorOnFirstToken(node: Node, message: DiagnosticMessage, arg0?: any, arg1?: any, arg2?: any) { + const span = getSpanOfTokenAtPosition(file, node.pos); + file.bindDiagnostics.push(createFileDiagnostic(file, span.start, span.length, message, arg0, arg1, arg2)); + } + function errorOrSuggestionOnNode(isError: boolean, node: Node, message: DiagnosticMessage): void { + errorOrSuggestionOnRange(isError, node, node, message); + } + function errorOrSuggestionOnRange(isError: boolean, startNode: Node, endNode: Node, message: DiagnosticMessage): void { + addErrorOrSuggestionDiagnostic(isError, { pos: getTokenPosOfNode(startNode, file), end: endNode.end }, message); + } + function addErrorOrSuggestionDiagnostic(isError: boolean, range: TextRange, message: DiagnosticMessage): void { + const diag = createFileDiagnostic(file, range.pos, range.end - range.pos, message); + if (isError) { + file.bindDiagnostics.push(diag); } - - function bindExportAssignment(node: ExportAssignment) { - if (!container.symbol || !container.symbol.exports) { - // Export assignment in some sort of block construct - bindAnonymousDeclaration(node, SymbolFlags.Alias, getDeclarationName(node)!); - } - else { - const flags = exportAssignmentIsAlias(node) - // An export default clause with an EntityNameExpression or a class expression exports all meanings of that identifier or expression; - ? SymbolFlags.Alias - // An export default clause with any other expression exports a value - : SymbolFlags.Property; - // If there is an `export default x;` alias declaration, can't `export default` anything else. - // (In contrast, you can still have `export default function f() {}` and `export default interface I {}`.) - const symbol = declareSymbol(container.symbol.exports, container.symbol, node, flags, SymbolFlags.All); - - if (node.isExportEquals) { - // Will be an error later, since the module already has other exports. Just make sure this has a valueDeclaration set. - setValueDeclaration(symbol, node); - } - } + else { + file.bindSuggestionDiagnostics = append(file.bindSuggestionDiagnostics, { ...diag, category: DiagnosticCategory.Suggestion }); } - - function bindNamespaceExportDeclaration(node: NamespaceExportDeclaration) { - if (node.modifiers && node.modifiers.length) { - file.bindDiagnostics.push(createDiagnosticForNode(node, Diagnostics.Modifiers_cannot_appear_here)); - } - const diag = !isSourceFile(node.parent) ? Diagnostics.Global_module_exports_may_only_appear_at_top_level - : !isExternalModule(node.parent) ? Diagnostics.Global_module_exports_may_only_appear_in_module_files - : !node.parent.isDeclarationFile ? Diagnostics.Global_module_exports_may_only_appear_in_declaration_files - : undefined; - if (diag) { - file.bindDiagnostics.push(createDiagnosticForNode(node, diag)); + } + function bind(node: Node | undefined): void { + if (!node) { + return; + } + node.parent = parent; + const saveInStrictMode = inStrictMode; + // Even though in the AST the jsdoc @typedef node belongs to the current node, + // its symbol might be in the same scope with the current node's symbol. Consider: + // + // /** @typedef {string | number} MyType */ + // function foo(); + // + // Here the current node is "foo", which is a container, but the scope of "MyType" should + // not be inside "foo". Therefore we always bind @typedef before bind the parent node, + // and skip binding this tag later when binding all the other jsdoc tags. + // First we bind declaration nodes to a symbol if possible. We'll both create a symbol + // and then potentially add the symbol to an appropriate symbol table. Possible + // destination symbol tables are: + // + // 1) The 'exports' table of the current container's symbol. + // 2) The 'members' table of the current container's symbol. + // 3) The 'locals' table of the current container. + // + // However, not all symbols will end up in any of these tables. 'Anonymous' symbols + // (like TypeLiterals for example) will not be put in any table. + bindWorker(node); + // Then we recurse into the children of the node to bind them as well. For certain + // symbols we do specialized work when we recurse. For example, we'll keep track of + // the current 'container' node when it changes. This helps us know which symbol table + // a local should go into for example. Since terminal nodes are known not to have + // children, as an optimization we don't process those. + if (node.kind > SyntaxKind.LastToken) { + const saveParent = parent; + parent = node; + const containerFlags = getContainerFlags(node); + if (containerFlags === ContainerFlags.None) { + bindChildren(node); } else { - file.symbol.globalExports = file.symbol.globalExports || createSymbolTable(); - declareSymbol(file.symbol.globalExports, file.symbol, node, SymbolFlags.Alias, SymbolFlags.AliasExcludes); - } - } - - function bindExportDeclaration(node: ExportDeclaration) { - if (!container.symbol || !container.symbol.exports) { - // Export * in some sort of block construct - bindAnonymousDeclaration(node, SymbolFlags.ExportStar, getDeclarationName(node)!); - } - else if (!node.exportClause) { - // All export * declarations are collected in an __export symbol - declareSymbol(container.symbol.exports, container.symbol, node, SymbolFlags.ExportStar, SymbolFlags.None); - } - else if (isNamespaceExport(node.exportClause)) { - // declareSymbol walks up parents to find name text, parent _must_ be set - // but won't be set by the normal binder walk until `bindChildren` later on. - node.exportClause.parent = node; - declareSymbol(container.symbol.exports, container.symbol, node.exportClause, SymbolFlags.Alias, SymbolFlags.AliasExcludes); - } - } - - function bindImportClause(node: ImportClause) { - if (node.name) { - declareSymbolAndAddToSymbolTable(node, SymbolFlags.Alias, SymbolFlags.AliasExcludes); + bindContainer(node, containerFlags); } + parent = saveParent; } - - function setCommonJsModuleIndicator(node: Node) { - if (file.externalModuleIndicator) { - return false; - } - if (!file.commonJsModuleIndicator) { - file.commonJsModuleIndicator = node; - bindSourceFileAsExternalModule(); - } - return true; + else if (!skipTransformFlagAggregation && (node.transformFlags & TransformFlags.HasComputedFlags) === 0) { + subtreeTransformFlags |= computeTransformFlagsForNode(node, 0); + const saveParent = parent; + if (node.kind === SyntaxKind.EndOfFileToken) + parent = node; + bindJSDoc(node); + parent = saveParent; } - - function bindObjectDefinePropertyExport(node: BindableObjectDefinePropertyCall) { - if (!setCommonJsModuleIndicator(node)) { - return; - } - const symbol = forEachIdentifierInEntityName(node.arguments[0], /*parent*/ undefined, (id, symbol) => { - if (symbol) { - addDeclarationToSymbol(symbol, id, SymbolFlags.Module | SymbolFlags.Assignment); + inStrictMode = saveInStrictMode; + } + function bindJSDoc(node: Node) { + if (hasJSDocNodes(node)) { + if (isInJSFile(node)) { + for (const j of node.jsDoc!) { + bind(j); } - return symbol; - }); - if (symbol) { - const flags = SymbolFlags.Property | SymbolFlags.ExportValue; - declareSymbol(symbol.exports!, symbol, node, flags, SymbolFlags.None); } - } - - function bindExportsPropertyAssignment(node: BindableStaticPropertyAssignmentExpression) { - // When we create a property via 'exports.foo = bar', the 'exports.foo' property access - // expression is the declaration - if (!setCommonJsModuleIndicator(node)) { - return; - } - const symbol = forEachIdentifierInEntityName(node.left.expression, /*parent*/ undefined, (id, symbol) => { - if (symbol) { - addDeclarationToSymbol(symbol, id, SymbolFlags.Module | SymbolFlags.Assignment); + else { + for (const j of node.jsDoc!) { + setParentPointers(node, j); } - return symbol; - }); - if (symbol) { - const flags = isClassExpression(node.right) ? - SymbolFlags.Property | SymbolFlags.ExportValue | SymbolFlags.Class : - SymbolFlags.Property | SymbolFlags.ExportValue; - declareSymbol(symbol.exports!, symbol, node.left, flags, SymbolFlags.None); } } - - function bindModuleExportsAssignment(node: BindablePropertyAssignmentExpression) { - // A common practice in node modules is to set 'export = module.exports = {}', this ensures that 'exports' - // is still pointing to 'module.exports'. - // We do not want to consider this as 'export=' since a module can have only one of these. - // Similarly we do not want to treat 'module.exports = exports' as an 'export='. - if (!setCommonJsModuleIndicator(node)) { - return; - } - const assignedExpression = getRightMostAssignedExpression(node.right); - if (isEmptyObjectLiteral(assignedExpression) || container === file && isExportsOrModuleExportsOrAlias(file, assignedExpression)) { - return; + } + function updateStrictModeStatementList(statements: NodeArray) { + if (!inStrictMode) { + for (const statement of statements) { + if (!isPrologueDirective(statement)) { + return; + } + if (isUseStrictPrologueDirective((statement))) { + inStrictMode = true; + return; + } } - - // 'module.exports = expr' assignment - const flags = exportAssignmentIsAlias(node) - ? SymbolFlags.Alias // An export= with an EntityNameExpression or a ClassExpression exports all meanings of that identifier or class - : SymbolFlags.Property | SymbolFlags.ExportValue | SymbolFlags.ValueModule; - const symbol = declareSymbol(file.symbol.exports!, file.symbol, node, flags | SymbolFlags.Assignment, SymbolFlags.None); - setValueDeclaration(symbol, node); } - - function bindThisPropertyAssignment(node: BindablePropertyAssignmentExpression | PropertyAccessExpression | LiteralLikeElementAccessExpression) { - Debug.assert(isInJSFile(node)); - // private identifiers *must* be declared (even in JS files) - const hasPrivateIdentifier = (isBinaryExpression(node) && isPropertyAccessExpression(node.left) && isPrivateIdentifier(node.left.name)) - || (isPropertyAccessExpression(node) && isPrivateIdentifier(node.name)); - if (hasPrivateIdentifier) { - return; - } - const thisContainer = getThisContainer(node, /*includeArrowFunctions*/ false); - switch (thisContainer.kind) { - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.FunctionExpression: - let constructorSymbol: Symbol | undefined = thisContainer.symbol; - // For `f.prototype.m = function() { this.x = 0; }`, `this.x = 0` should modify `f`'s members, not the function expression. - if (isBinaryExpression(thisContainer.parent) && thisContainer.parent.operatorToken.kind === SyntaxKind.EqualsToken) { - const l = thisContainer.parent.left; - if (isBindableStaticAccessExpression(l) && isPrototypeAccess(l.expression)) { - constructorSymbol = lookupSymbolForPropertyAccess(l.expression.expression, thisParentContainer); - } - } - - if (constructorSymbol && constructorSymbol.valueDeclaration) { - // Declare a 'member' if the container is an ES5 class or ES6 constructor - constructorSymbol.members = constructorSymbol.members || createSymbolTable(); - // It's acceptable for multiple 'this' assignments of the same identifier to occur - if (hasDynamicName(node)) { - bindDynamicallyNamedThisPropertyAssignment(node, constructorSymbol); - } - else { - declareSymbol(constructorSymbol.members, constructorSymbol, node, SymbolFlags.Property | SymbolFlags.Assignment, SymbolFlags.PropertyExcludes & ~SymbolFlags.Property); - } - addDeclarationToSymbol(constructorSymbol, constructorSymbol.valueDeclaration, SymbolFlags.Class); - } - break; - - case SyntaxKind.Constructor: - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - // this.foo assignment in a JavaScript class - // Bind this property to the containing class - const containingClass = thisContainer.parent; - const symbolTable = hasModifier(thisContainer, ModifierFlags.Static) ? containingClass.symbol.exports! : containingClass.symbol.members!; - if (hasDynamicName(node)) { - bindDynamicallyNamedThisPropertyAssignment(node, containingClass.symbol); - } - else { - declareSymbol(symbolTable, containingClass.symbol, node, SymbolFlags.Property | SymbolFlags.Assignment, SymbolFlags.None, /*isReplaceableByMethod*/ true); + } + /// Should be called only on prologue directives (isPrologueDirective(node) should be true) + function isUseStrictPrologueDirective(node: ExpressionStatement): boolean { + const nodeText = getSourceTextOfNodeFromSourceFile(file, node.expression); + // Note: the node text must be exactly "use strict" or 'use strict'. It is not ok for the + // string to contain unicode escapes (as per ES5). + return nodeText === '"use strict"' || nodeText === "'use strict'"; + } + function bindWorker(node: Node) { + switch (node.kind) { + /* Strict mode checks */ + case SyntaxKind.Identifier: + // for typedef type names with namespaces, bind the new jsdoc type symbol here + // because it requires all containing namespaces to be in effect, namely the + // current "blockScopeContainer" needs to be set to its immediate namespace parent. + if ((node).isInJSDocNamespace) { + let parentNode = node.parent; + while (parentNode && !isJSDocTypeAlias(parentNode)) { + parentNode = parentNode.parent; } + bindBlockScopedDeclaration((parentNode as Declaration), SymbolFlags.TypeAlias, SymbolFlags.TypeAliasExcludes); break; - case SyntaxKind.SourceFile: - // this.property = assignment in a source file -- declare symbol in exports for a module, in locals for a script - if (hasDynamicName(node)) { + } + // falls through + case SyntaxKind.ThisKeyword: + if (currentFlow && (isExpression(node) || parent.kind === SyntaxKind.ShorthandPropertyAssignment)) { + node.flowNode = currentFlow; + } + return checkStrictModeIdentifier((node)); + case SyntaxKind.PrivateIdentifier: + return checkPrivateIdentifier((node as PrivateIdentifier)); + case SyntaxKind.PropertyAccessExpression: + case SyntaxKind.ElementAccessExpression: + const expr = (node as PropertyAccessExpression | ElementAccessExpression); + if (currentFlow && isNarrowableReference(expr)) { + expr.flowNode = currentFlow; + } + if (isSpecialPropertyDeclaration(expr)) { + bindSpecialPropertyDeclaration(expr); + } + if (isInJSFile(expr) && + file.commonJsModuleIndicator && + isModuleExportsAccessExpression(expr) && + !lookupSymbolForNameWorker(blockScopeContainer, ("module" as __String))) { + declareSymbol((file.locals!), /*parent*/ undefined, expr.expression, SymbolFlags.FunctionScopedVariable | SymbolFlags.ModuleExports, SymbolFlags.FunctionScopedVariableExcludes); + } + break; + case SyntaxKind.BinaryExpression: + const specialKind = getAssignmentDeclarationKind((node as BinaryExpression)); + switch (specialKind) { + case AssignmentDeclarationKind.ExportsProperty: + bindExportsPropertyAssignment((node as BindableStaticPropertyAssignmentExpression)); break; - } - else if ((thisContainer as SourceFile).commonJsModuleIndicator) { - declareSymbol(thisContainer.symbol.exports!, thisContainer.symbol, node, SymbolFlags.Property | SymbolFlags.ExportValue, SymbolFlags.None); - } - else { - declareSymbolAndAddToSymbolTable(node, SymbolFlags.FunctionScopedVariable, SymbolFlags.FunctionScopedVariableExcludes); - } + case AssignmentDeclarationKind.ModuleExports: + bindModuleExportsAssignment((node as BindablePropertyAssignmentExpression)); + break; + case AssignmentDeclarationKind.PrototypeProperty: + bindPrototypePropertyAssignment((node as BindableStaticPropertyAssignmentExpression).left, node); + break; + case AssignmentDeclarationKind.Prototype: + bindPrototypeAssignment((node as BindableStaticPropertyAssignmentExpression)); + break; + case AssignmentDeclarationKind.ThisProperty: + bindThisPropertyAssignment((node as BindablePropertyAssignmentExpression)); + break; + case AssignmentDeclarationKind.Property: + bindSpecialPropertyAssignment((node as BindablePropertyAssignmentExpression)); + break; + case AssignmentDeclarationKind.None: + // Nothing to do + break; + default: + Debug.fail("Unknown binary expression special property assignment kind"); + } + return checkStrictModeBinaryExpression((node)); + case SyntaxKind.CatchClause: + return checkStrictModeCatchClause((node)); + case SyntaxKind.DeleteExpression: + return checkStrictModeDeleteExpression((node)); + case SyntaxKind.NumericLiteral: + return checkStrictModeNumericLiteral((node)); + case SyntaxKind.PostfixUnaryExpression: + return checkStrictModePostfixUnaryExpression((node)); + case SyntaxKind.PrefixUnaryExpression: + return checkStrictModePrefixUnaryExpression((node)); + case SyntaxKind.WithStatement: + return checkStrictModeWithStatement((node)); + case SyntaxKind.LabeledStatement: + return checkStrictModeLabeledStatement((node)); + case SyntaxKind.ThisType: + seenThisKeyword = true; + return; + case SyntaxKind.TypePredicate: + break; // Binding the children will handle everything + case SyntaxKind.TypeParameter: + return bindTypeParameter((node as TypeParameterDeclaration)); + case SyntaxKind.Parameter: + return bindParameter((node)); + case SyntaxKind.VariableDeclaration: + return bindVariableDeclarationOrBindingElement((node)); + case SyntaxKind.BindingElement: + node.flowNode = currentFlow; + return bindVariableDeclarationOrBindingElement((node)); + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.PropertySignature: + return bindPropertyWorker((node as PropertyDeclaration | PropertySignature)); + case SyntaxKind.PropertyAssignment: + case SyntaxKind.ShorthandPropertyAssignment: + return bindPropertyOrMethodOrAccessor((node), SymbolFlags.Property, SymbolFlags.PropertyExcludes); + case SyntaxKind.EnumMember: + return bindPropertyOrMethodOrAccessor((node), SymbolFlags.EnumMember, SymbolFlags.EnumMemberExcludes); + case SyntaxKind.CallSignature: + case SyntaxKind.ConstructSignature: + case SyntaxKind.IndexSignature: + return declareSymbolAndAddToSymbolTable((node), SymbolFlags.Signature, SymbolFlags.None); + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: + // If this is an ObjectLiteralExpression method, then it sits in the same space + // as other properties in the object literal. So we use SymbolFlags.PropertyExcludes + // so that it will conflict with any other object literal members with the same + // name. + return bindPropertyOrMethodOrAccessor((node), SymbolFlags.Method | ((node).questionToken ? SymbolFlags.Optional : SymbolFlags.None), isObjectLiteralMethod(node) ? SymbolFlags.PropertyExcludes : SymbolFlags.MethodExcludes); + case SyntaxKind.FunctionDeclaration: + return bindFunctionDeclaration((node)); + case SyntaxKind.Constructor: + return declareSymbolAndAddToSymbolTable((node), SymbolFlags.Constructor, /*symbolExcludes:*/ SymbolFlags.None); + case SyntaxKind.GetAccessor: + return bindPropertyOrMethodOrAccessor((node), SymbolFlags.GetAccessor, SymbolFlags.GetAccessorExcludes); + case SyntaxKind.SetAccessor: + return bindPropertyOrMethodOrAccessor((node), SymbolFlags.SetAccessor, SymbolFlags.SetAccessorExcludes); + case SyntaxKind.FunctionType: + case SyntaxKind.JSDocFunctionType: + case SyntaxKind.JSDocSignature: + case SyntaxKind.ConstructorType: + return bindFunctionOrConstructorType((node)); + case SyntaxKind.TypeLiteral: + case SyntaxKind.JSDocTypeLiteral: + case SyntaxKind.MappedType: + return bindAnonymousTypeWorker((node as TypeLiteralNode | MappedTypeNode | JSDocTypeLiteral)); + case SyntaxKind.JSDocClassTag: + return bindJSDocClassTag((node as JSDocClassTag)); + case SyntaxKind.ObjectLiteralExpression: + return bindObjectLiteralExpression((node)); + case SyntaxKind.FunctionExpression: + case SyntaxKind.ArrowFunction: + return bindFunctionExpression((node)); + case SyntaxKind.CallExpression: + const assignmentKind = getAssignmentDeclarationKind((node as CallExpression)); + switch (assignmentKind) { + case AssignmentDeclarationKind.ObjectDefinePropertyValue: + return bindObjectDefinePropertyAssignment((node as BindableObjectDefinePropertyCall)); + case AssignmentDeclarationKind.ObjectDefinePropertyExports: + return bindObjectDefinePropertyExport((node as BindableObjectDefinePropertyCall)); + case AssignmentDeclarationKind.ObjectDefinePrototypeProperty: + return bindObjectDefinePrototypeProperty((node as BindableObjectDefinePropertyCall)); + case AssignmentDeclarationKind.None: + break; // Nothing to do + default: + return Debug.fail("Unknown call expression assignment declaration kind"); + } + if (isInJSFile(node)) { + bindCallExpression((node)); + } + break; + // Members of classes, interfaces, and modules + case SyntaxKind.ClassExpression: + case SyntaxKind.ClassDeclaration: + // All classes are automatically in strict mode in ES6. + inStrictMode = true; + return bindClassLikeDeclaration((node)); + case SyntaxKind.InterfaceDeclaration: + return bindBlockScopedDeclaration((node), SymbolFlags.Interface, SymbolFlags.InterfaceExcludes); + case SyntaxKind.TypeAliasDeclaration: + return bindBlockScopedDeclaration((node), SymbolFlags.TypeAlias, SymbolFlags.TypeAliasExcludes); + case SyntaxKind.EnumDeclaration: + return bindEnumDeclaration((node)); + case SyntaxKind.ModuleDeclaration: + return bindModuleDeclaration((node)); + // Jsx-attributes + case SyntaxKind.JsxAttributes: + return bindJsxAttributes((node)); + case SyntaxKind.JsxAttribute: + return bindJsxAttribute((node), SymbolFlags.Property, SymbolFlags.PropertyExcludes); + // Imports and exports + case SyntaxKind.ImportEqualsDeclaration: + case SyntaxKind.NamespaceImport: + case SyntaxKind.ImportSpecifier: + case SyntaxKind.ExportSpecifier: + return declareSymbolAndAddToSymbolTable((node), SymbolFlags.Alias, SymbolFlags.AliasExcludes); + case SyntaxKind.NamespaceExportDeclaration: + return bindNamespaceExportDeclaration((node)); + case SyntaxKind.ImportClause: + return bindImportClause((node)); + case SyntaxKind.ExportDeclaration: + return bindExportDeclaration((node)); + case SyntaxKind.ExportAssignment: + return bindExportAssignment((node)); + case SyntaxKind.SourceFile: + updateStrictModeStatementList((node).statements); + return bindSourceFileIfExternalModule(); + case SyntaxKind.Block: + if (!isFunctionLike(node.parent)) { + return; + } + // falls through + case SyntaxKind.ModuleBlock: + return updateStrictModeStatementList((node).statements); + case SyntaxKind.JSDocParameterTag: + if (node.parent.kind === SyntaxKind.JSDocSignature) { + return bindParameter((node as JSDocParameterTag)); + } + if (node.parent.kind !== SyntaxKind.JSDocTypeLiteral) { break; - - default: - Debug.failBadSyntaxKind(thisContainer); - } + } + // falls through + case SyntaxKind.JSDocPropertyTag: + const propTag = (node as JSDocPropertyLikeTag); + const flags = propTag.isBracketed || propTag.typeExpression && propTag.typeExpression.type.kind === SyntaxKind.JSDocOptionalType ? + SymbolFlags.Property | SymbolFlags.Optional : + SymbolFlags.Property; + return declareSymbolAndAddToSymbolTable(propTag, flags, SymbolFlags.PropertyExcludes); + case SyntaxKind.JSDocTypedefTag: + case SyntaxKind.JSDocCallbackTag: + case SyntaxKind.JSDocEnumTag: + return (delayedTypeAliases || (delayedTypeAliases = [])).push((node as JSDocTypedefTag | JSDocCallbackTag | JSDocEnumTag)); } - - function bindDynamicallyNamedThisPropertyAssignment(node: BinaryExpression | DynamicNamedDeclaration, symbol: Symbol) { - bindAnonymousDeclaration(node, SymbolFlags.Property, InternalSymbolName.Computed); - addLateBoundAssignmentDeclarationToSymbol(node, symbol); + } + function bindPropertyWorker(node: PropertyDeclaration | PropertySignature) { + return bindPropertyOrMethodOrAccessor(node, SymbolFlags.Property | (node.questionToken ? SymbolFlags.Optional : SymbolFlags.None), SymbolFlags.PropertyExcludes); + } + function bindAnonymousTypeWorker(node: TypeLiteralNode | MappedTypeNode | JSDocTypeLiteral) { + return bindAnonymousDeclaration((node), SymbolFlags.TypeLiteral, InternalSymbolName.Type); + } + function bindSourceFileIfExternalModule() { + setExportContextFlag(file); + if (isExternalModule(file)) { + bindSourceFileAsExternalModule(); + } + else if (isJsonSourceFile(file)) { + bindSourceFileAsExternalModule(); + // Create symbol equivalent for the module.exports = {} + const originalSymbol = file.symbol; + declareSymbol((file.symbol.exports!), file.symbol, file, SymbolFlags.Property, SymbolFlags.All); + file.symbol = originalSymbol; } - - function addLateBoundAssignmentDeclarationToSymbol(node: BinaryExpression | DynamicNamedDeclaration, symbol: Symbol | undefined) { - if (symbol) { - const members = symbol.assignmentDeclarationMembers || (symbol.assignmentDeclarationMembers = createMap()); - members.set("" + getNodeId(node), node); - } + } + function bindSourceFileAsExternalModule() { + bindAnonymousDeclaration(file, SymbolFlags.ValueModule, (`"${removeFileExtension(file.fileName)}"` as __String)); + } + function bindExportAssignment(node: ExportAssignment) { + if (!container.symbol || !container.symbol.exports) { + // Export assignment in some sort of block construct + bindAnonymousDeclaration(node, SymbolFlags.Alias, (getDeclarationName(node)!)); } - - function bindSpecialPropertyDeclaration(node: PropertyAccessExpression | LiteralLikeElementAccessExpression) { - if (node.expression.kind === SyntaxKind.ThisKeyword) { - bindThisPropertyAssignment(node); - } - else if (isBindableStaticAccessExpression(node) && node.parent.parent.kind === SyntaxKind.SourceFile) { - if (isPrototypeAccess(node.expression)) { - bindPrototypePropertyAssignment(node, node.parent); - } - else { - bindStaticPropertyAssignment(node); - } + else { + const flags = exportAssignmentIsAlias(node) + // An export default clause with an EntityNameExpression or a class expression exports all meanings of that identifier or expression; + ? SymbolFlags.Alias + // An export default clause with any other expression exports a value + : SymbolFlags.Property; + // If there is an `export default x;` alias declaration, can't `export default` anything else. + // (In contrast, you can still have `export default function f() {}` and `export default interface I {}`.) + const symbol = declareSymbol(container.symbol.exports, container.symbol, node, flags, SymbolFlags.All); + if (node.isExportEquals) { + // Will be an error later, since the module already has other exports. Just make sure this has a valueDeclaration set. + setValueDeclaration(symbol, node); } } - - /** For `x.prototype = { p, ... }`, declare members p,... if `x` is function/class/{}, or not declared. */ - function bindPrototypeAssignment(node: BindableStaticPropertyAssignmentExpression) { - node.left.parent = node; - node.right.parent = node; - bindPropertyAssignment(node.left.expression, node.left, /*isPrototypeProperty*/ false, /*containerIsClass*/ true); - } - - function bindObjectDefinePrototypeProperty(node: BindableObjectDefinePropertyCall) { - const namespaceSymbol = lookupSymbolForPropertyAccess((node.arguments[0] as PropertyAccessExpression).expression as EntityNameExpression); - if (namespaceSymbol && namespaceSymbol.valueDeclaration) { - // Ensure the namespace symbol becomes class-like - addDeclarationToSymbol(namespaceSymbol, namespaceSymbol.valueDeclaration, SymbolFlags.Class); - } - bindPotentiallyNewExpandoMemberToNamespace(node, namespaceSymbol, /*isPrototypeProperty*/ true); + } + function bindNamespaceExportDeclaration(node: NamespaceExportDeclaration) { + if (node.modifiers && node.modifiers.length) { + file.bindDiagnostics.push(createDiagnosticForNode(node, Diagnostics.Modifiers_cannot_appear_here)); } - - /** - * For `x.prototype.y = z`, declare a member `y` on `x` if `x` is a function or class, or not declared. - * Note that jsdoc preceding an ExpressionStatement like `x.prototype.y;` is also treated as a declaration. - */ - function bindPrototypePropertyAssignment(lhs: BindableStaticAccessExpression, parent: Node) { - // Look up the function in the local scope, since prototype assignments should - // follow the function declaration - const classPrototype = lhs.expression as BindableStaticAccessExpression; - const constructorFunction = classPrototype.expression; - - // Fix up parent pointers since we're going to use these nodes before we bind into them - lhs.parent = parent; - constructorFunction.parent = classPrototype; - classPrototype.parent = lhs; - - bindPropertyAssignment(constructorFunction, lhs, /*isPrototypeProperty*/ true, /*containerIsClass*/ true); - } - - function bindObjectDefinePropertyAssignment(node: BindableObjectDefinePropertyCall) { - let namespaceSymbol = lookupSymbolForPropertyAccess(node.arguments[0]); - const isToplevel = node.parent.parent.kind === SyntaxKind.SourceFile; - namespaceSymbol = bindPotentiallyMissingNamespaces(namespaceSymbol, node.arguments[0], isToplevel, /*isPrototypeProperty*/ false, /*containerIsClass*/ false); - bindPotentiallyNewExpandoMemberToNamespace(node, namespaceSymbol, /*isPrototypeProperty*/ false); - } - - function bindSpecialPropertyAssignment(node: BindablePropertyAssignmentExpression) { - // Class declarations in Typescript do not allow property declarations - const parentSymbol = lookupSymbolForPropertyAccess(node.left.expression); - if (!isInJSFile(node) && !isFunctionSymbol(parentSymbol)) { - return; - } - // Fix up parent pointers since we're going to use these nodes before we bind into them - node.left.parent = node; - node.right.parent = node; - if (isIdentifier(node.left.expression) && container === file && isExportsOrModuleExportsOrAlias(file, node.left.expression)) { - // This can be an alias for the 'exports' or 'module.exports' names, e.g. - // var util = module.exports; - // util.property = function ... - bindExportsPropertyAssignment(node as BindableStaticPropertyAssignmentExpression); - } - else { - if (hasDynamicName(node)) { - bindAnonymousDeclaration(node, SymbolFlags.Property | SymbolFlags.Assignment, InternalSymbolName.Computed); - const sym = bindPotentiallyMissingNamespaces(parentSymbol, node.left.expression, isTopLevelNamespaceAssignment(node.left), /*isPrototype*/ false, /*containerIsClass*/ false); - addLateBoundAssignmentDeclarationToSymbol(node, sym); - } - else { - bindStaticPropertyAssignment(cast(node.left, isBindableStaticAccessExpression)); - } - } + const diag = !isSourceFile(node.parent) ? Diagnostics.Global_module_exports_may_only_appear_at_top_level + : !isExternalModule(node.parent) ? Diagnostics.Global_module_exports_may_only_appear_in_module_files + : !node.parent.isDeclarationFile ? Diagnostics.Global_module_exports_may_only_appear_in_declaration_files + : undefined; + if (diag) { + file.bindDiagnostics.push(createDiagnosticForNode(node, diag)); } - - /** - * For nodes like `x.y = z`, declare a member 'y' on 'x' if x is a function (or IIFE) or class or {}, or not declared. - * Also works for expression statements preceded by JSDoc, like / ** @type number * / x.y; - */ - function bindStaticPropertyAssignment(node: BindableStaticAccessExpression) { - node.expression.parent = node; - bindPropertyAssignment(node.expression, node, /*isPrototypeProperty*/ false, /*containerIsClass*/ false); - } - - function bindPotentiallyMissingNamespaces(namespaceSymbol: Symbol | undefined, entityName: BindableStaticNameExpression, isToplevel: boolean, isPrototypeProperty: boolean, containerIsClass: boolean) { - if (isToplevel && !isPrototypeProperty) { - // make symbols or add declarations for intermediate containers - const flags = SymbolFlags.Module | SymbolFlags.Assignment; - const excludeFlags = SymbolFlags.ValueModuleExcludes & ~SymbolFlags.Assignment; - namespaceSymbol = forEachIdentifierInEntityName(entityName, namespaceSymbol, (id, symbol, parent) => { - if (symbol) { - addDeclarationToSymbol(symbol, id, flags); - return symbol; - } - else { - const table = parent ? parent.exports! : - file.jsGlobalAugmentations || (file.jsGlobalAugmentations = createSymbolTable()); - return declareSymbol(table, parent, id, flags, excludeFlags); - } - }); - } - if (containerIsClass && namespaceSymbol && namespaceSymbol.valueDeclaration) { - addDeclarationToSymbol(namespaceSymbol, namespaceSymbol.valueDeclaration, SymbolFlags.Class); - } - return namespaceSymbol; + else { + file.symbol.globalExports = file.symbol.globalExports || createSymbolTable(); + declareSymbol(file.symbol.globalExports, file.symbol, node, SymbolFlags.Alias, SymbolFlags.AliasExcludes); } - - function bindPotentiallyNewExpandoMemberToNamespace(declaration: BindableStaticAccessExpression | CallExpression, namespaceSymbol: Symbol | undefined, isPrototypeProperty: boolean) { - if (!namespaceSymbol || !isExpandoSymbol(namespaceSymbol)) { - return; - } - - // Set up the members collection if it doesn't exist already - const symbolTable = isPrototypeProperty ? - (namespaceSymbol.members || (namespaceSymbol.members = createSymbolTable())) : - (namespaceSymbol.exports || (namespaceSymbol.exports = createSymbolTable())); - - let includes = SymbolFlags.None; - let excludes = SymbolFlags.None; - // Method-like - if (isFunctionLikeDeclaration(getAssignedExpandoInitializer(declaration)!)) { - includes = SymbolFlags.Method; - excludes = SymbolFlags.MethodExcludes; - } - // Maybe accessor-like - else if (isCallExpression(declaration) && isBindableObjectDefinePropertyCall(declaration)) { - if (some(declaration.arguments[2].properties, p => { - const id = getNameOfDeclaration(p); - return !!id && isIdentifier(id) && idText(id) === "set"; - })) { - // We mix in `SymbolFLags.Property` so in the checker `getTypeOfVariableParameterOrProperty` is used for this - // symbol, instead of `getTypeOfAccessor` (which will assert as there is no real accessor declaration) - includes |= SymbolFlags.SetAccessor | SymbolFlags.Property; - excludes |= SymbolFlags.SetAccessorExcludes; - } - if (some(declaration.arguments[2].properties, p => { - const id = getNameOfDeclaration(p); - return !!id && isIdentifier(id) && idText(id) === "get"; - })) { - includes |= SymbolFlags.GetAccessor | SymbolFlags.Property; - excludes |= SymbolFlags.GetAccessorExcludes; - } - } - - if (includes === SymbolFlags.None) { - includes = SymbolFlags.Property; - excludes = SymbolFlags.PropertyExcludes; - } - - declareSymbol(symbolTable, namespaceSymbol, declaration, includes | SymbolFlags.Assignment, excludes & ~SymbolFlags.Assignment); - } - - function isTopLevelNamespaceAssignment(propertyAccess: BindableAccessExpression) { - return isBinaryExpression(propertyAccess.parent) - ? getParentOfBinaryExpression(propertyAccess.parent).parent.kind === SyntaxKind.SourceFile - : propertyAccess.parent.parent.kind === SyntaxKind.SourceFile; - } - - function bindPropertyAssignment(name: BindableStaticNameExpression, propertyAccess: BindableStaticAccessExpression, isPrototypeProperty: boolean, containerIsClass: boolean) { - let namespaceSymbol = lookupSymbolForPropertyAccess(name); - const isToplevel = isTopLevelNamespaceAssignment(propertyAccess); - namespaceSymbol = bindPotentiallyMissingNamespaces(namespaceSymbol, propertyAccess.expression, isToplevel, isPrototypeProperty, containerIsClass); - bindPotentiallyNewExpandoMemberToNamespace(propertyAccess, namespaceSymbol, isPrototypeProperty); - } - - /** - * Javascript expando values are: - * - Functions - * - classes - * - namespaces - * - variables initialized with function expressions - * - with class expressions - * - with empty object literals - * - with non-empty object literals if assigned to the prototype property - */ - function isExpandoSymbol(symbol: Symbol): boolean { - if (symbol.flags & (SymbolFlags.Function | SymbolFlags.Class | SymbolFlags.NamespaceModule)) { - return true; - } - const node = symbol.valueDeclaration; - if (node && isCallExpression(node)) { - return !!getAssignedExpandoInitializer(node); - } - let init = !node ? undefined : - isVariableDeclaration(node) ? node.initializer : - isBinaryExpression(node) ? node.right : - isPropertyAccessExpression(node) && isBinaryExpression(node.parent) ? node.parent.right : - undefined; - init = init && getRightMostAssignedExpression(init); - if (init) { - const isPrototypeAssignment = isPrototypeAccess(isVariableDeclaration(node) ? node.name : isBinaryExpression(node) ? node.left : node); - return !!getExpandoInitializer(isBinaryExpression(init) && (init.operatorToken.kind === SyntaxKind.BarBarToken || init.operatorToken.kind === SyntaxKind.QuestionQuestionToken) ? init.right : init, isPrototypeAssignment); - } - return false; + } + function bindExportDeclaration(node: ExportDeclaration) { + if (!container.symbol || !container.symbol.exports) { + // Export * in some sort of block construct + bindAnonymousDeclaration(node, SymbolFlags.ExportStar, (getDeclarationName(node)!)); } - - function getParentOfBinaryExpression(expr: Node) { - while (isBinaryExpression(expr.parent)) { - expr = expr.parent; - } - return expr.parent; + else if (!node.exportClause) { + // All export * declarations are collected in an __export symbol + declareSymbol(container.symbol.exports, container.symbol, node, SymbolFlags.ExportStar, SymbolFlags.None); } - - function lookupSymbolForPropertyAccess(node: BindableStaticNameExpression, lookupContainer: Node = container): Symbol | undefined { - if (isIdentifier(node)) { - return lookupSymbolForNameWorker(lookupContainer, node.escapedText); - } - else { - const symbol = lookupSymbolForPropertyAccess(node.expression); - return symbol && symbol.exports && symbol.exports.get(getElementOrPropertyAccessName(node)); - } + else if (isNamespaceExport(node.exportClause)) { + // declareSymbol walks up parents to find name text, parent _must_ be set + // but won't be set by the normal binder walk until `bindChildren` later on. + node.exportClause.parent = node; + declareSymbol(container.symbol.exports, container.symbol, node.exportClause, SymbolFlags.Alias, SymbolFlags.AliasExcludes); } - - function forEachIdentifierInEntityName(e: BindableStaticNameExpression, parent: Symbol | undefined, action: (e: Declaration, symbol: Symbol | undefined, parent: Symbol | undefined) => Symbol | undefined): Symbol | undefined { - if (isExportsOrModuleExportsOrAlias(file, e)) { - return file.symbol; - } - else if (isIdentifier(e)) { - return action(e, lookupSymbolForPropertyAccess(e), parent); - } - else { - const s = forEachIdentifierInEntityName(e.expression, parent, action); - const name = getNameOrArgument(e); - // unreachable - if (isPrivateIdentifier(name)) { - Debug.fail("unexpected PrivateIdentifier"); - } - return action(name, s && s.exports && s.exports.get(getElementOrPropertyAccessName(e)), s); - } + } + function bindImportClause(node: ImportClause) { + if (node.name) { + declareSymbolAndAddToSymbolTable(node, SymbolFlags.Alias, SymbolFlags.AliasExcludes); } - - function bindCallExpression(node: CallExpression) { - // We're only inspecting call expressions to detect CommonJS modules, so we can skip - // this check if we've already seen the module indicator - if (!file.commonJsModuleIndicator && isRequireCall(node, /*checkArgumentIsStringLiteralLike*/ false)) { - setCommonJsModuleIndicator(node); - } + } + function setCommonJsModuleIndicator(node: Node) { + if (file.externalModuleIndicator) { + return false; } - - function bindClassLikeDeclaration(node: ClassLikeDeclaration) { - if (node.kind === SyntaxKind.ClassDeclaration) { - bindBlockScopedDeclaration(node, SymbolFlags.Class, SymbolFlags.ClassExcludes); - } - else { - const bindingName = node.name ? node.name.escapedText : InternalSymbolName.Class; - bindAnonymousDeclaration(node, SymbolFlags.Class, bindingName); - // Add name of class expression into the map for semantic classifier - if (node.name) { - classifiableNames.set(node.name.escapedText, true); - } - } - - const { symbol } = node; - - // TypeScript 1.0 spec (April 2014): 8.4 - // Every class automatically contains a static property member named 'prototype', the - // type of which is an instantiation of the class type with type Any supplied as a type - // argument for each type parameter. It is an error to explicitly declare a static - // property member with the name 'prototype'. - // - // Note: we check for this here because this class may be merging into a module. The - // module might have an exported variable called 'prototype'. We can't allow that as - // that would clash with the built-in 'prototype' for the class. - const prototypeSymbol = createSymbol(SymbolFlags.Property | SymbolFlags.Prototype, "prototype" as __String); - const symbolExport = symbol.exports!.get(prototypeSymbol.escapedName); - if (symbolExport) { - if (node.name) { - node.name.parent = node; - } - file.bindDiagnostics.push(createDiagnosticForNode(symbolExport.declarations[0], Diagnostics.Duplicate_identifier_0, symbolName(prototypeSymbol))); - } - symbol.exports!.set(prototypeSymbol.escapedName, prototypeSymbol); - prototypeSymbol.parent = symbol; - } - - function bindEnumDeclaration(node: EnumDeclaration) { - return isEnumConst(node) - ? bindBlockScopedDeclaration(node, SymbolFlags.ConstEnum, SymbolFlags.ConstEnumExcludes) - : bindBlockScopedDeclaration(node, SymbolFlags.RegularEnum, SymbolFlags.RegularEnumExcludes); - } - - function bindVariableDeclarationOrBindingElement(node: VariableDeclaration | BindingElement) { - if (inStrictMode) { - checkStrictModeEvalOrArguments(node, node.name); - } - - if (!isBindingPattern(node.name)) { - if (isBlockOrCatchScoped(node)) { - bindBlockScopedDeclaration(node, SymbolFlags.BlockScopedVariable, SymbolFlags.BlockScopedVariableExcludes); - } - else if (isParameterDeclaration(node)) { - // It is safe to walk up parent chain to find whether the node is a destructuring parameter declaration - // because its parent chain has already been set up, since parents are set before descending into children. - // - // If node is a binding element in parameter declaration, we need to use ParameterExcludes. - // Using ParameterExcludes flag allows the compiler to report an error on duplicate identifiers in Parameter Declaration - // For example: - // function foo([a,a]) {} // Duplicate Identifier error - // function bar(a,a) {} // Duplicate Identifier error, parameter declaration in this case is handled in bindParameter - // // which correctly set excluded symbols - declareSymbolAndAddToSymbolTable(node, SymbolFlags.FunctionScopedVariable, SymbolFlags.ParameterExcludes); - } - else { - declareSymbolAndAddToSymbolTable(node, SymbolFlags.FunctionScopedVariable, SymbolFlags.FunctionScopedVariableExcludes); - } - } + if (!file.commonJsModuleIndicator) { + file.commonJsModuleIndicator = node; + bindSourceFileAsExternalModule(); + } + return true; + } + function bindObjectDefinePropertyExport(node: BindableObjectDefinePropertyCall) { + if (!setCommonJsModuleIndicator(node)) { + return; } - - function bindParameter(node: ParameterDeclaration | JSDocParameterTag) { - if (node.kind === SyntaxKind.JSDocParameterTag && container.kind !== SyntaxKind.JSDocSignature) { - return; - } - if (inStrictMode && !(node.flags & NodeFlags.Ambient)) { - // It is a SyntaxError if the identifier eval or arguments appears within a FormalParameterList of a - // strict mode FunctionLikeDeclaration or FunctionExpression(13.1) - checkStrictModeEvalOrArguments(node, node.name); - } - - if (isBindingPattern(node.name)) { - bindAnonymousDeclaration(node, SymbolFlags.FunctionScopedVariable, "__" + (node as ParameterDeclaration).parent.parameters.indexOf(node as ParameterDeclaration) as __String); - } - else { - declareSymbolAndAddToSymbolTable(node, SymbolFlags.FunctionScopedVariable, SymbolFlags.ParameterExcludes); - } - - // If this is a property-parameter, then also declare the property symbol into the - // containing class. - if (isParameterPropertyDeclaration(node, node.parent)) { - const classDeclaration = node.parent.parent; - declareSymbol(classDeclaration.symbol.members!, classDeclaration.symbol, node, SymbolFlags.Property | (node.questionToken ? SymbolFlags.Optional : SymbolFlags.None), SymbolFlags.PropertyExcludes); + const symbol = forEachIdentifierInEntityName(node.arguments[0], /*parent*/ undefined, (id, symbol) => { + if (symbol) { + addDeclarationToSymbol(symbol, id, SymbolFlags.Module | SymbolFlags.Assignment); } + return symbol; + }); + if (symbol) { + const flags = SymbolFlags.Property | SymbolFlags.ExportValue; + declareSymbol((symbol.exports!), symbol, node, flags, SymbolFlags.None); } - - function bindFunctionDeclaration(node: FunctionDeclaration) { - if (!file.isDeclarationFile && !(node.flags & NodeFlags.Ambient)) { - if (isAsyncFunction(node)) { - emitFlags |= NodeFlags.HasAsyncFunctions; - } - } - - checkStrictModeFunctionName(node); - if (inStrictMode) { - checkStrictModeFunctionDeclaration(node); - bindBlockScopedDeclaration(node, SymbolFlags.Function, SymbolFlags.FunctionExcludes); - } - else { - declareSymbolAndAddToSymbolTable(node, SymbolFlags.Function, SymbolFlags.FunctionExcludes); - } + } + function bindExportsPropertyAssignment(node: BindableStaticPropertyAssignmentExpression) { + // When we create a property via 'exports.foo = bar', the 'exports.foo' property access + // expression is the declaration + if (!setCommonJsModuleIndicator(node)) { + return; } - - function bindFunctionExpression(node: FunctionExpression) { - if (!file.isDeclarationFile && !(node.flags & NodeFlags.Ambient)) { - if (isAsyncFunction(node)) { - emitFlags |= NodeFlags.HasAsyncFunctions; - } - } - if (currentFlow) { - node.flowNode = currentFlow; + const symbol = forEachIdentifierInEntityName(node.left.expression, /*parent*/ undefined, (id, symbol) => { + if (symbol) { + addDeclarationToSymbol(symbol, id, SymbolFlags.Module | SymbolFlags.Assignment); } - checkStrictModeFunctionName(node); - const bindingName = node.name ? node.name.escapedText : InternalSymbolName.Function; - return bindAnonymousDeclaration(node, SymbolFlags.Function, bindingName); + return symbol; + }); + if (symbol) { + const flags = isClassExpression(node.right) ? + SymbolFlags.Property | SymbolFlags.ExportValue | SymbolFlags.Class : + SymbolFlags.Property | SymbolFlags.ExportValue; + declareSymbol((symbol.exports!), symbol, node.left, flags, SymbolFlags.None); } - - function bindPropertyOrMethodOrAccessor(node: Declaration, symbolFlags: SymbolFlags, symbolExcludes: SymbolFlags) { - if (!file.isDeclarationFile && !(node.flags & NodeFlags.Ambient) && isAsyncFunction(node)) { - emitFlags |= NodeFlags.HasAsyncFunctions; - } - - if (currentFlow && isObjectLiteralOrClassExpressionMethod(node)) { - node.flowNode = currentFlow; - } - - return hasDynamicName(node) - ? bindAnonymousDeclaration(node, symbolFlags, InternalSymbolName.Computed) - : declareSymbolAndAddToSymbolTable(node, symbolFlags, symbolExcludes); - } - - function getInferTypeContainer(node: Node): ConditionalTypeNode | undefined { - const extendsType = findAncestor(node, n => n.parent && isConditionalTypeNode(n.parent) && n.parent.extendsType === n); - return extendsType && extendsType.parent as ConditionalTypeNode; - } - - function bindTypeParameter(node: TypeParameterDeclaration) { - if (isJSDocTemplateTag(node.parent)) { - const container = find((node.parent.parent as JSDoc).tags!, isJSDocTypeAlias) || getHostSignatureFromJSDoc(node.parent); // TODO: GH#18217 - if (container) { - if (!container.locals) { - container.locals = createSymbolTable(); + } + function bindModuleExportsAssignment(node: BindablePropertyAssignmentExpression) { + // A common practice in node modules is to set 'export = module.exports = {}', this ensures that 'exports' + // is still pointing to 'module.exports'. + // We do not want to consider this as 'export=' since a module can have only one of these. + // Similarly we do not want to treat 'module.exports = exports' as an 'export='. + if (!setCommonJsModuleIndicator(node)) { + return; + } + const assignedExpression = getRightMostAssignedExpression(node.right); + if (isEmptyObjectLiteral(assignedExpression) || container === file && isExportsOrModuleExportsOrAlias(file, assignedExpression)) { + return; + } + // 'module.exports = expr' assignment + const flags = exportAssignmentIsAlias(node) + ? SymbolFlags.Alias // An export= with an EntityNameExpression or a ClassExpression exports all meanings of that identifier or class + : SymbolFlags.Property | SymbolFlags.ExportValue | SymbolFlags.ValueModule; + const symbol = declareSymbol((file.symbol.exports!), file.symbol, node, flags | SymbolFlags.Assignment, SymbolFlags.None); + setValueDeclaration(symbol, node); + } + function bindThisPropertyAssignment(node: BindablePropertyAssignmentExpression | PropertyAccessExpression | LiteralLikeElementAccessExpression) { + Debug.assert(isInJSFile(node)); + // private identifiers *must* be declared (even in JS files) + const hasPrivateIdentifier = (isBinaryExpression(node) && isPropertyAccessExpression(node.left) && isPrivateIdentifier(node.left.name)) + || (isPropertyAccessExpression(node) && isPrivateIdentifier(node.name)); + if (hasPrivateIdentifier) { + return; + } + const thisContainer = getThisContainer(node, /*includeArrowFunctions*/ false); + switch (thisContainer.kind) { + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.FunctionExpression: + let constructorSymbol: ts.Symbol | undefined = thisContainer.symbol; + // For `f.prototype.m = function() { this.x = 0; }`, `this.x = 0` should modify `f`'s members, not the function expression. + if (isBinaryExpression(thisContainer.parent) && thisContainer.parent.operatorToken.kind === SyntaxKind.EqualsToken) { + const l = thisContainer.parent.left; + if (isBindableStaticAccessExpression(l) && isPrototypeAccess(l.expression)) { + constructorSymbol = lookupSymbolForPropertyAccess(l.expression.expression, thisParentContainer); + } + } + if (constructorSymbol && constructorSymbol.valueDeclaration) { + // Declare a 'member' if the container is an ES5 class or ES6 constructor + constructorSymbol.members = constructorSymbol.members || createSymbolTable(); + // It's acceptable for multiple 'this' assignments of the same identifier to occur + if (hasDynamicName(node)) { + bindDynamicallyNamedThisPropertyAssignment(node, constructorSymbol); + } + else { + declareSymbol(constructorSymbol.members, constructorSymbol, node, SymbolFlags.Property | SymbolFlags.Assignment, SymbolFlags.PropertyExcludes & ~SymbolFlags.Property); } - declareSymbol(container.locals, /*parent*/ undefined, node, SymbolFlags.TypeParameter, SymbolFlags.TypeParameterExcludes); + addDeclarationToSymbol(constructorSymbol, constructorSymbol.valueDeclaration, SymbolFlags.Class); + } + break; + case SyntaxKind.Constructor: + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + // this.foo assignment in a JavaScript class + // Bind this property to the containing class + const containingClass = thisContainer.parent; + const symbolTable = hasModifier(thisContainer, ModifierFlags.Static) ? containingClass.symbol.exports! : containingClass.symbol.members!; + if (hasDynamicName(node)) { + bindDynamicallyNamedThisPropertyAssignment(node, containingClass.symbol); } else { - declareSymbolAndAddToSymbolTable(node, SymbolFlags.TypeParameter, SymbolFlags.TypeParameterExcludes); + declareSymbol(symbolTable, containingClass.symbol, node, SymbolFlags.Property | SymbolFlags.Assignment, SymbolFlags.None, /*isReplaceableByMethod*/ true); } - } - else if (node.parent.kind === SyntaxKind.InferType) { - const container = getInferTypeContainer(node.parent); - if (container) { - if (!container.locals) { - container.locals = createSymbolTable(); - } - declareSymbol(container.locals, /*parent*/ undefined, node, SymbolFlags.TypeParameter, SymbolFlags.TypeParameterExcludes); + break; + case SyntaxKind.SourceFile: + // this.property = assignment in a source file -- declare symbol in exports for a module, in locals for a script + if (hasDynamicName(node)) { + break; + } + else if ((thisContainer as SourceFile).commonJsModuleIndicator) { + declareSymbol((thisContainer.symbol.exports!), thisContainer.symbol, node, SymbolFlags.Property | SymbolFlags.ExportValue, SymbolFlags.None); } else { - bindAnonymousDeclaration(node, SymbolFlags.TypeParameter, getDeclarationName(node)!); // TODO: GH#18217 + declareSymbolAndAddToSymbolTable(node, SymbolFlags.FunctionScopedVariable, SymbolFlags.FunctionScopedVariableExcludes); } + break; + default: + Debug.failBadSyntaxKind(thisContainer); + } + } + function bindDynamicallyNamedThisPropertyAssignment(node: BinaryExpression | DynamicNamedDeclaration, symbol: ts.Symbol) { + bindAnonymousDeclaration(node, SymbolFlags.Property, InternalSymbolName.Computed); + addLateBoundAssignmentDeclarationToSymbol(node, symbol); + } + function addLateBoundAssignmentDeclarationToSymbol(node: BinaryExpression | DynamicNamedDeclaration, symbol: ts.Symbol | undefined) { + if (symbol) { + const members = symbol.assignmentDeclarationMembers || (symbol.assignmentDeclarationMembers = createMap()); + members.set("" + getNodeId(node), node); + } + } + function bindSpecialPropertyDeclaration(node: PropertyAccessExpression | LiteralLikeElementAccessExpression) { + if (node.expression.kind === SyntaxKind.ThisKeyword) { + bindThisPropertyAssignment(node); + } + else if (isBindableStaticAccessExpression(node) && node.parent.parent.kind === SyntaxKind.SourceFile) { + if (isPrototypeAccess(node.expression)) { + bindPrototypePropertyAssignment(node, node.parent); } else { - declareSymbolAndAddToSymbolTable(node, SymbolFlags.TypeParameter, SymbolFlags.TypeParameterExcludes); + bindStaticPropertyAssignment(node); } } - - // reachability checks - - function shouldReportErrorOnModuleDeclaration(node: ModuleDeclaration): boolean { - const instanceState = getModuleInstanceState(node); - return instanceState === ModuleInstanceState.Instantiated || (instanceState === ModuleInstanceState.ConstEnumOnly && !!options.preserveConstEnums); - } - - function checkUnreachable(node: Node): boolean { - if (!(currentFlow.flags & FlowFlags.Unreachable)) { - return false; - } - if (currentFlow === unreachableFlow) { - const reportError = - // report error on all statements except empty ones - (isStatementButNotDeclaration(node) && node.kind !== SyntaxKind.EmptyStatement) || - // report error on class declarations - node.kind === SyntaxKind.ClassDeclaration || - // report error on instantiated modules or const-enums only modules if preserveConstEnums is set - (node.kind === SyntaxKind.ModuleDeclaration && shouldReportErrorOnModuleDeclaration(node)); - - if (reportError) { - currentFlow = reportedUnreachableFlow; - - if (!options.allowUnreachableCode) { - // unreachable code is reported if - // - user has explicitly asked about it AND - // - statement is in not ambient context (statements in ambient context is already an error - // so we should not report extras) AND - // - node is not variable statement OR - // - node is block scoped variable statement OR - // - node is not block scoped variable statement and at least one variable declaration has initializer - // Rationale: we don't want to report errors on non-initialized var's since they are hoisted - // On the other side we do want to report errors on non-initialized 'lets' because of TDZ - const isError = - unreachableCodeIsError(options) && - !(node.flags & NodeFlags.Ambient) && - ( - !isVariableStatement(node) || - !!(getCombinedNodeFlags(node.declarationList) & NodeFlags.BlockScoped) || - node.declarationList.declarations.some(d => !!d.initializer) - ); - - eachUnreachableRange(node, (start, end) => errorOrSuggestionOnRange(isError, start, end, Diagnostics.Unreachable_code_detected)); - } - } - } - return true; + } + /** For `x.prototype = { p, ... }`, declare members p,... if `x` is function/class/{}, or not declared. */ + function bindPrototypeAssignment(node: BindableStaticPropertyAssignmentExpression) { + node.left.parent = node; + node.right.parent = node; + bindPropertyAssignment(node.left.expression, node.left, /*isPrototypeProperty*/ false, /*containerIsClass*/ true); + } + function bindObjectDefinePrototypeProperty(node: BindableObjectDefinePropertyCall) { + const namespaceSymbol = lookupSymbolForPropertyAccess(((node.arguments[0] as PropertyAccessExpression).expression as EntityNameExpression)); + if (namespaceSymbol && namespaceSymbol.valueDeclaration) { + // Ensure the namespace symbol becomes class-like + addDeclarationToSymbol(namespaceSymbol, namespaceSymbol.valueDeclaration, SymbolFlags.Class); } + bindPotentiallyNewExpandoMemberToNamespace(node, namespaceSymbol, /*isPrototypeProperty*/ true); + } + /** + * For `x.prototype.y = z`, declare a member `y` on `x` if `x` is a function or class, or not declared. + * Note that jsdoc preceding an ExpressionStatement like `x.prototype.y;` is also treated as a declaration. + */ + function bindPrototypePropertyAssignment(lhs: BindableStaticAccessExpression, parent: Node) { + // Look up the function in the local scope, since prototype assignments should + // follow the function declaration + const classPrototype = (lhs.expression as BindableStaticAccessExpression); + const constructorFunction = classPrototype.expression; + // Fix up parent pointers since we're going to use these nodes before we bind into them + lhs.parent = parent; + constructorFunction.parent = classPrototype; + classPrototype.parent = lhs; + bindPropertyAssignment(constructorFunction, lhs, /*isPrototypeProperty*/ true, /*containerIsClass*/ true); } - - function eachUnreachableRange(node: Node, cb: (start: Node, last: Node) => void): void { - if (isStatement(node) && isExecutableStatement(node) && isBlock(node.parent)) { - const { statements } = node.parent; - const slice = sliceAfter(statements, node); - getRangesWhere(slice, isExecutableStatement, (start, afterEnd) => cb(slice[start], slice[afterEnd - 1])); + function bindObjectDefinePropertyAssignment(node: BindableObjectDefinePropertyCall) { + let namespaceSymbol = lookupSymbolForPropertyAccess(node.arguments[0]); + const isToplevel = node.parent.parent.kind === SyntaxKind.SourceFile; + namespaceSymbol = bindPotentiallyMissingNamespaces(namespaceSymbol, node.arguments[0], isToplevel, /*isPrototypeProperty*/ false, /*containerIsClass*/ false); + bindPotentiallyNewExpandoMemberToNamespace(node, namespaceSymbol, /*isPrototypeProperty*/ false); + } + function bindSpecialPropertyAssignment(node: BindablePropertyAssignmentExpression) { + // Class declarations in Typescript do not allow property declarations + const parentSymbol = lookupSymbolForPropertyAccess(node.left.expression); + if (!isInJSFile(node) && !isFunctionSymbol(parentSymbol)) { + return; + } + // Fix up parent pointers since we're going to use these nodes before we bind into them + node.left.parent = node; + node.right.parent = node; + if (isIdentifier(node.left.expression) && container === file && isExportsOrModuleExportsOrAlias(file, node.left.expression)) { + // This can be an alias for the 'exports' or 'module.exports' names, e.g. + // var util = module.exports; + // util.property = function ... + bindExportsPropertyAssignment((node as BindableStaticPropertyAssignmentExpression)); } else { - cb(node, node); + if (hasDynamicName(node)) { + bindAnonymousDeclaration(node, SymbolFlags.Property | SymbolFlags.Assignment, InternalSymbolName.Computed); + const sym = bindPotentiallyMissingNamespaces(parentSymbol, node.left.expression, isTopLevelNamespaceAssignment(node.left), /*isPrototype*/ false, /*containerIsClass*/ false); + addLateBoundAssignmentDeclarationToSymbol(node, sym); + } + else { + bindStaticPropertyAssignment(cast(node.left, isBindableStaticAccessExpression)); + } } } - // As opposed to a pure declaration like an `interface` - function isExecutableStatement(s: Statement): boolean { - // Don't remove statements that can validly be used before they appear. - return !isFunctionDeclaration(s) && !isPurelyTypeDeclaration(s) && !isEnumDeclaration(s) && - // `var x;` may declare a variable used above - !(isVariableStatement(s) && !(getCombinedNodeFlags(s) & (NodeFlags.Let | NodeFlags.Const)) && s.declarationList.declarations.some(d => !d.initializer)); + /** + * For nodes like `x.y = z`, declare a member 'y' on 'x' if x is a function (or IIFE) or class or {}, or not declared. + * Also works for expression statements preceded by JSDoc, like / ** @type number * / x.y; + */ + function bindStaticPropertyAssignment(node: BindableStaticAccessExpression) { + node.expression.parent = node; + bindPropertyAssignment(node.expression, node, /*isPrototypeProperty*/ false, /*containerIsClass*/ false); } - - function isPurelyTypeDeclaration(s: Statement): boolean { - switch (s.kind) { - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.TypeAliasDeclaration: - return true; - case SyntaxKind.ModuleDeclaration: - return getModuleInstanceState(s as ModuleDeclaration) !== ModuleInstanceState.Instantiated; - case SyntaxKind.EnumDeclaration: - return hasModifier(s, ModifierFlags.Const); - default: - return false; - } - } - - export function isExportsOrModuleExportsOrAlias(sourceFile: SourceFile, node: Expression): boolean { - let i = 0; - const q = [node]; - while (q.length && i < 100) { - i++; - node = q.shift()!; - if (isExportsIdentifier(node) || isModuleExportsAccessExpression(node)) { - return true; - } - else if (isIdentifier(node)) { - const symbol = lookupSymbolForNameWorker(sourceFile, node.escapedText); - if (!!symbol && !!symbol.valueDeclaration && isVariableDeclaration(symbol.valueDeclaration) && !!symbol.valueDeclaration.initializer) { - const init = symbol.valueDeclaration.initializer; - q.push(init); - if (isAssignmentExpression(init, /*excludeCompoundAssignment*/ true)) { - q.push(init.left); - q.push(init.right); - } + function bindPotentiallyMissingNamespaces(namespaceSymbol: ts.Symbol | undefined, entityName: BindableStaticNameExpression, isToplevel: boolean, isPrototypeProperty: boolean, containerIsClass: boolean) { + if (isToplevel && !isPrototypeProperty) { + // make symbols or add declarations for intermediate containers + const flags = SymbolFlags.Module | SymbolFlags.Assignment; + const excludeFlags = SymbolFlags.ValueModuleExcludes & ~SymbolFlags.Assignment; + namespaceSymbol = forEachIdentifierInEntityName(entityName, namespaceSymbol, (id, symbol, parent) => { + if (symbol) { + addDeclarationToSymbol(symbol, id, flags); + return symbol; } - } + else { + const table = parent ? parent.exports! : + file.jsGlobalAugmentations || (file.jsGlobalAugmentations = createSymbolTable()); + return declareSymbol(table, parent, id, flags, excludeFlags); + } + }); + } + if (containerIsClass && namespaceSymbol && namespaceSymbol.valueDeclaration) { + addDeclarationToSymbol(namespaceSymbol, namespaceSymbol.valueDeclaration, SymbolFlags.Class); + } + return namespaceSymbol; + } + function bindPotentiallyNewExpandoMemberToNamespace(declaration: BindableStaticAccessExpression | CallExpression, namespaceSymbol: ts.Symbol | undefined, isPrototypeProperty: boolean) { + if (!namespaceSymbol || !isExpandoSymbol(namespaceSymbol)) { + return; + } + // Set up the members collection if it doesn't exist already + const symbolTable = isPrototypeProperty ? + (namespaceSymbol.members || (namespaceSymbol.members = createSymbolTable())) : + (namespaceSymbol.exports || (namespaceSymbol.exports = createSymbolTable())); + let includes = SymbolFlags.None; + let excludes = SymbolFlags.None; + // Method-like + if (isFunctionLikeDeclaration((getAssignedExpandoInitializer(declaration)!))) { + includes = SymbolFlags.Method; + excludes = SymbolFlags.MethodExcludes; + } + // Maybe accessor-like + else if (isCallExpression(declaration) && isBindableObjectDefinePropertyCall(declaration)) { + if (some(declaration.arguments[2].properties, p => { + const id = getNameOfDeclaration(p); + return !!id && isIdentifier(id) && idText(id) === "set"; + })) { + // We mix in `SymbolFLags.Property` so in the checker `getTypeOfVariableParameterOrProperty` is used for this + // symbol, instead of `getTypeOfAccessor` (which will assert as there is no real accessor declaration) + includes |= SymbolFlags.SetAccessor | SymbolFlags.Property; + excludes |= SymbolFlags.SetAccessorExcludes; + } + if (some(declaration.arguments[2].properties, p => { + const id = getNameOfDeclaration(p); + return !!id && isIdentifier(id) && idText(id) === "get"; + })) { + includes |= SymbolFlags.GetAccessor | SymbolFlags.Property; + excludes |= SymbolFlags.GetAccessorExcludes; + } + } + if (includes === SymbolFlags.None) { + includes = SymbolFlags.Property; + excludes = SymbolFlags.PropertyExcludes; + } + declareSymbol(symbolTable, namespaceSymbol, declaration, includes | SymbolFlags.Assignment, excludes & ~SymbolFlags.Assignment); + } + function isTopLevelNamespaceAssignment(propertyAccess: BindableAccessExpression) { + return isBinaryExpression(propertyAccess.parent) + ? getParentOfBinaryExpression(propertyAccess.parent).parent.kind === SyntaxKind.SourceFile + : propertyAccess.parent.parent.kind === SyntaxKind.SourceFile; + } + function bindPropertyAssignment(name: BindableStaticNameExpression, propertyAccess: BindableStaticAccessExpression, isPrototypeProperty: boolean, containerIsClass: boolean) { + let namespaceSymbol = lookupSymbolForPropertyAccess(name); + const isToplevel = isTopLevelNamespaceAssignment(propertyAccess); + namespaceSymbol = bindPotentiallyMissingNamespaces(namespaceSymbol, propertyAccess.expression, isToplevel, isPrototypeProperty, containerIsClass); + bindPotentiallyNewExpandoMemberToNamespace(propertyAccess, namespaceSymbol, isPrototypeProperty); + } + /** + * Javascript expando values are: + * - Functions + * - classes + * - namespaces + * - variables initialized with function expressions + * - with class expressions + * - with empty object literals + * - with non-empty object literals if assigned to the prototype property + */ + function isExpandoSymbol(symbol: ts.Symbol): boolean { + if (symbol.flags & (SymbolFlags.Function | SymbolFlags.Class | SymbolFlags.NamespaceModule)) { + return true; + } + const node = symbol.valueDeclaration; + if (node && isCallExpression(node)) { + return !!getAssignedExpandoInitializer(node); + } + let init = !node ? undefined : + isVariableDeclaration(node) ? node.initializer : + isBinaryExpression(node) ? node.right : + isPropertyAccessExpression(node) && isBinaryExpression(node.parent) ? node.parent.right : + undefined; + init = init && getRightMostAssignedExpression(init); + if (init) { + const isPrototypeAssignment = isPrototypeAccess(isVariableDeclaration(node) ? node.name : isBinaryExpression(node) ? node.left : node); + return !!getExpandoInitializer(isBinaryExpression(init) && (init.operatorToken.kind === SyntaxKind.BarBarToken || init.operatorToken.kind === SyntaxKind.QuestionQuestionToken) ? init.right : init, isPrototypeAssignment); } return false; } - - function lookupSymbolForNameWorker(container: Node, name: __String): Symbol | undefined { - const local = container.locals && container.locals.get(name); - if (local) { - return local.exportSymbol || local; + function getParentOfBinaryExpression(expr: Node) { + while (isBinaryExpression(expr.parent)) { + expr = expr.parent; + } + return expr.parent; + } + function lookupSymbolForPropertyAccess(node: BindableStaticNameExpression, lookupContainer: Node = container): ts.Symbol | undefined { + if (isIdentifier(node)) { + return lookupSymbolForNameWorker(lookupContainer, node.escapedText); } - if (isSourceFile(container) && container.jsGlobalAugmentations && container.jsGlobalAugmentations.has(name)) { - return container.jsGlobalAugmentations.get(name); + else { + const symbol = lookupSymbolForPropertyAccess(node.expression); + return symbol && symbol.exports && symbol.exports.get(getElementOrPropertyAccessName(node)); } - return container.symbol && container.symbol.exports && container.symbol.exports.get(name); } - - /** - * Computes the transform flags for a node, given the transform flags of its subtree - * - * @param node The node to analyze - * @param subtreeFlags Transform flags computed for this node's subtree - */ - export function computeTransformFlagsForNode(node: Node, subtreeFlags: TransformFlags): TransformFlags { - const kind = node.kind; - switch (kind) { - case SyntaxKind.CallExpression: - return computeCallExpression(node, subtreeFlags); - - case SyntaxKind.NewExpression: - return computeNewExpression(node, subtreeFlags); - - case SyntaxKind.ModuleDeclaration: - return computeModuleDeclaration(node, subtreeFlags); - - case SyntaxKind.ParenthesizedExpression: - return computeParenthesizedExpression(node, subtreeFlags); - - case SyntaxKind.BinaryExpression: - return computeBinaryExpression(node, subtreeFlags); - - case SyntaxKind.ExpressionStatement: - return computeExpressionStatement(node, subtreeFlags); - - case SyntaxKind.Parameter: - return computeParameter(node, subtreeFlags); - - case SyntaxKind.ArrowFunction: - return computeArrowFunction(node, subtreeFlags); - - case SyntaxKind.FunctionExpression: - return computeFunctionExpression(node, subtreeFlags); - - case SyntaxKind.FunctionDeclaration: - return computeFunctionDeclaration(node, subtreeFlags); - - case SyntaxKind.VariableDeclaration: - return computeVariableDeclaration(node, subtreeFlags); - - case SyntaxKind.VariableDeclarationList: - return computeVariableDeclarationList(node, subtreeFlags); - - case SyntaxKind.VariableStatement: - return computeVariableStatement(node, subtreeFlags); - - case SyntaxKind.LabeledStatement: - return computeLabeledStatement(node, subtreeFlags); - - case SyntaxKind.ClassDeclaration: - return computeClassDeclaration(node, subtreeFlags); - - case SyntaxKind.ClassExpression: - return computeClassExpression(node, subtreeFlags); - - case SyntaxKind.HeritageClause: - return computeHeritageClause(node, subtreeFlags); - - case SyntaxKind.CatchClause: - return computeCatchClause(node, subtreeFlags); - - case SyntaxKind.ExpressionWithTypeArguments: - return computeExpressionWithTypeArguments(node, subtreeFlags); - - case SyntaxKind.Constructor: - return computeConstructor(node, subtreeFlags); - - case SyntaxKind.PropertyDeclaration: - return computePropertyDeclaration(node, subtreeFlags); - - case SyntaxKind.MethodDeclaration: - return computeMethod(node, subtreeFlags); - - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - return computeAccessor(node, subtreeFlags); - - case SyntaxKind.ImportEqualsDeclaration: - return computeImportEquals(node, subtreeFlags); - - case SyntaxKind.PropertyAccessExpression: - return computePropertyAccess(node, subtreeFlags); - - case SyntaxKind.ElementAccessExpression: - return computeElementAccess(node, subtreeFlags); - - default: - return computeOther(node, kind, subtreeFlags); + function forEachIdentifierInEntityName(e: BindableStaticNameExpression, parent: ts.Symbol | undefined, action: (e: Declaration, symbol: ts.Symbol | undefined, parent: ts.Symbol | undefined) => ts.Symbol | undefined): ts.Symbol | undefined { + if (isExportsOrModuleExportsOrAlias(file, e)) { + return file.symbol; + } + else if (isIdentifier(e)) { + return action(e, lookupSymbolForPropertyAccess(e), parent); + } + else { + const s = forEachIdentifierInEntityName(e.expression, parent, action); + const name = getNameOrArgument(e); + // unreachable + if (isPrivateIdentifier(name)) { + Debug.fail("unexpected PrivateIdentifier"); + } + return action(name, s && s.exports && s.exports.get(getElementOrPropertyAccessName(e)), s); } } - - function computeCallExpression(node: CallExpression, subtreeFlags: TransformFlags) { - let transformFlags = subtreeFlags; - const callee = skipOuterExpressions(node.expression); - const expression = node.expression; - - if (node.flags & NodeFlags.OptionalChain) { - transformFlags |= TransformFlags.ContainsES2020; + function bindCallExpression(node: CallExpression) { + // We're only inspecting call expressions to detect CommonJS modules, so we can skip + // this check if we've already seen the module indicator + if (!file.commonJsModuleIndicator && isRequireCall(node, /*checkArgumentIsStringLiteralLike*/ false)) { + setCommonJsModuleIndicator(node); } - - if (node.typeArguments) { - transformFlags |= TransformFlags.AssertTypeScript; + } + function bindClassLikeDeclaration(node: ClassLikeDeclaration) { + if (node.kind === SyntaxKind.ClassDeclaration) { + bindBlockScopedDeclaration(node, SymbolFlags.Class, SymbolFlags.ClassExcludes); } - - if (subtreeFlags & TransformFlags.ContainsRestOrSpread || isSuperOrSuperProperty(callee)) { - // If the this node contains a SpreadExpression, or is a super call, then it is an ES6 - // node. - transformFlags |= TransformFlags.AssertES2015; - if (isSuperProperty(callee)) { - transformFlags |= TransformFlags.ContainsLexicalThis; + else { + const bindingName = node.name ? node.name.escapedText : InternalSymbolName.Class; + bindAnonymousDeclaration(node, SymbolFlags.Class, bindingName); + // Add name of class expression into the map for semantic classifier + if (node.name) { + classifiableNames.set(node.name.escapedText, true); } } - - if (expression.kind === SyntaxKind.ImportKeyword) { - transformFlags |= TransformFlags.ContainsDynamicImport; + const { symbol } = node; + // TypeScript 1.0 spec (April 2014): 8.4 + // Every class automatically contains a static property member named 'prototype', the + // type of which is an instantiation of the class type with type Any supplied as a type + // argument for each type parameter. It is an error to explicitly declare a static + // property member with the name 'prototype'. + // + // Note: we check for this here because this class may be merging into a module. The + // module might have an exported variable called 'prototype'. We can't allow that as + // that would clash with the built-in 'prototype' for the class. + const prototypeSymbol = createSymbol(SymbolFlags.Property | SymbolFlags.Prototype, ("prototype" as __String)); + const symbolExport = symbol.exports!.get(prototypeSymbol.escapedName); + if (symbolExport) { + if (node.name) { + node.name.parent = node; + } + file.bindDiagnostics.push(createDiagnosticForNode(symbolExport.declarations[0], Diagnostics.Duplicate_identifier_0, symbolName(prototypeSymbol))); } - - node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; - return transformFlags & ~TransformFlags.ArrayLiteralOrCallOrNewExcludes; + symbol.exports!.set(prototypeSymbol.escapedName, prototypeSymbol); + prototypeSymbol.parent = symbol; } - - function computeNewExpression(node: NewExpression, subtreeFlags: TransformFlags) { - let transformFlags = subtreeFlags; - if (node.typeArguments) { - transformFlags |= TransformFlags.AssertTypeScript; + function bindEnumDeclaration(node: EnumDeclaration) { + return isEnumConst(node) + ? bindBlockScopedDeclaration(node, SymbolFlags.ConstEnum, SymbolFlags.ConstEnumExcludes) + : bindBlockScopedDeclaration(node, SymbolFlags.RegularEnum, SymbolFlags.RegularEnumExcludes); + } + function bindVariableDeclarationOrBindingElement(node: VariableDeclaration | BindingElement) { + if (inStrictMode) { + checkStrictModeEvalOrArguments(node, node.name); } - if (subtreeFlags & TransformFlags.ContainsRestOrSpread) { - // If the this node contains a SpreadElementExpression then it is an ES6 - // node. - transformFlags |= TransformFlags.AssertES2015; + if (!isBindingPattern(node.name)) { + if (isBlockOrCatchScoped(node)) { + bindBlockScopedDeclaration(node, SymbolFlags.BlockScopedVariable, SymbolFlags.BlockScopedVariableExcludes); + } + else if (isParameterDeclaration(node)) { + // It is safe to walk up parent chain to find whether the node is a destructuring parameter declaration + // because its parent chain has already been set up, since parents are set before descending into children. + // + // If node is a binding element in parameter declaration, we need to use ParameterExcludes. + // Using ParameterExcludes flag allows the compiler to report an error on duplicate identifiers in Parameter Declaration + // For example: + // function foo([a,a]) {} // Duplicate Identifier error + // function bar(a,a) {} // Duplicate Identifier error, parameter declaration in this case is handled in bindParameter + // // which correctly set excluded symbols + declareSymbolAndAddToSymbolTable(node, SymbolFlags.FunctionScopedVariable, SymbolFlags.ParameterExcludes); + } + else { + declareSymbolAndAddToSymbolTable(node, SymbolFlags.FunctionScopedVariable, SymbolFlags.FunctionScopedVariableExcludes); + } } - node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; - return transformFlags & ~TransformFlags.ArrayLiteralOrCallOrNewExcludes; - } - - function computeBinaryExpression(node: BinaryExpression, subtreeFlags: TransformFlags) { - let transformFlags = subtreeFlags; - const operatorTokenKind = node.operatorToken.kind; - const leftKind = node.left.kind; - - if (operatorTokenKind === SyntaxKind.QuestionQuestionToken) { - transformFlags |= TransformFlags.AssertES2020; - } - else if (operatorTokenKind === SyntaxKind.EqualsToken && leftKind === SyntaxKind.ObjectLiteralExpression) { - // Destructuring object assignments with are ES2015 syntax - // and possibly ES2018 if they contain rest - transformFlags |= TransformFlags.AssertES2018 | TransformFlags.AssertES2015 | TransformFlags.AssertDestructuringAssignment; - } - else if (operatorTokenKind === SyntaxKind.EqualsToken && leftKind === SyntaxKind.ArrayLiteralExpression) { - // Destructuring assignments are ES2015 syntax. - transformFlags |= TransformFlags.AssertES2015 | TransformFlags.AssertDestructuringAssignment; - } - else if (operatorTokenKind === SyntaxKind.AsteriskAsteriskToken - || operatorTokenKind === SyntaxKind.AsteriskAsteriskEqualsToken) { - // Exponentiation is ES2016 syntax. - transformFlags |= TransformFlags.AssertES2016; - } - - node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; - return transformFlags & ~TransformFlags.NodeExcludes; - } - - function computeParameter(node: ParameterDeclaration, subtreeFlags: TransformFlags) { - let transformFlags = subtreeFlags; - const name = node.name; - const initializer = node.initializer; - const dotDotDotToken = node.dotDotDotToken; - - // The '?' token, type annotations, decorators, and 'this' parameters are TypeSCript - // syntax. - if (node.questionToken - || node.type - || (subtreeFlags & TransformFlags.ContainsTypeScriptClassSyntax && some(node.decorators)) - || isThisIdentifier(name)) { - transformFlags |= TransformFlags.AssertTypeScript; + } + function bindParameter(node: ParameterDeclaration | JSDocParameterTag) { + if (node.kind === SyntaxKind.JSDocParameterTag && container.kind !== SyntaxKind.JSDocSignature) { + return; } - - // If a parameter has an accessibility modifier, then it is TypeScript syntax. - if (hasModifier(node, ModifierFlags.ParameterPropertyModifier)) { - transformFlags |= TransformFlags.AssertTypeScript | TransformFlags.ContainsTypeScriptClassSyntax; + if (inStrictMode && !(node.flags & NodeFlags.Ambient)) { + // It is a SyntaxError if the identifier eval or arguments appears within a FormalParameterList of a + // strict mode FunctionLikeDeclaration or FunctionExpression(13.1) + checkStrictModeEvalOrArguments(node, node.name); } - - // parameters with object rest destructuring are ES2018 syntax - if (subtreeFlags & TransformFlags.ContainsObjectRestOrSpread) { - transformFlags |= TransformFlags.AssertES2018; + if (isBindingPattern(node.name)) { + bindAnonymousDeclaration(node, SymbolFlags.FunctionScopedVariable, ("__" + (node as ParameterDeclaration).parent.parameters.indexOf((node as ParameterDeclaration)) as __String)); } - - // If a parameter has an initializer, a binding pattern or a dotDotDot token, then - // it is ES6 syntax and its container must emit default value assignments or parameter destructuring downlevel. - if (subtreeFlags & TransformFlags.ContainsBindingPattern || initializer || dotDotDotToken) { - transformFlags |= TransformFlags.AssertES2015; + else { + declareSymbolAndAddToSymbolTable(node, SymbolFlags.FunctionScopedVariable, SymbolFlags.ParameterExcludes); } - - node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; - return transformFlags & ~TransformFlags.ParameterExcludes; - } - - function computeParenthesizedExpression(node: ParenthesizedExpression, subtreeFlags: TransformFlags) { - let transformFlags = subtreeFlags; - const expression = node.expression; - const expressionKind = expression.kind; - - // If the node is synthesized, it means the emitter put the parentheses there, - // not the user. If we didn't want them, the emitter would not have put them - // there. - if (expressionKind === SyntaxKind.AsExpression - || expressionKind === SyntaxKind.TypeAssertionExpression) { - transformFlags |= TransformFlags.AssertTypeScript; + // If this is a property-parameter, then also declare the property symbol into the + // containing class. + if (isParameterPropertyDeclaration(node, node.parent)) { + const classDeclaration = (node.parent.parent); + declareSymbol((classDeclaration.symbol.members!), classDeclaration.symbol, node, SymbolFlags.Property | (node.questionToken ? SymbolFlags.Optional : SymbolFlags.None), SymbolFlags.PropertyExcludes); } - - node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; - return transformFlags & ~TransformFlags.OuterExpressionExcludes; } - - function computeClassDeclaration(node: ClassDeclaration, subtreeFlags: TransformFlags) { - let transformFlags: TransformFlags; - - if (hasModifier(node, ModifierFlags.Ambient)) { - // An ambient declaration is TypeScript syntax. - transformFlags = TransformFlags.AssertTypeScript; + function bindFunctionDeclaration(node: FunctionDeclaration) { + if (!file.isDeclarationFile && !(node.flags & NodeFlags.Ambient)) { + if (isAsyncFunction(node)) { + emitFlags |= NodeFlags.HasAsyncFunctions; + } + } + checkStrictModeFunctionName(node); + if (inStrictMode) { + checkStrictModeFunctionDeclaration(node); + bindBlockScopedDeclaration(node, SymbolFlags.Function, SymbolFlags.FunctionExcludes); } else { - // A ClassDeclaration is ES6 syntax. - transformFlags = subtreeFlags | TransformFlags.AssertES2015; - - // A class with a parameter property assignment or decorator is TypeScript syntax. - // An exported declaration may be TypeScript syntax, but is handled by the visitor - // for a namespace declaration. - if ((subtreeFlags & TransformFlags.ContainsTypeScriptClassSyntax) - || node.typeParameters) { - transformFlags |= TransformFlags.AssertTypeScript; - } + declareSymbolAndAddToSymbolTable(node, SymbolFlags.Function, SymbolFlags.FunctionExcludes); } - - node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; - return transformFlags & ~TransformFlags.ClassExcludes; } - - function computeClassExpression(node: ClassExpression, subtreeFlags: TransformFlags) { - // A ClassExpression is ES6 syntax. - let transformFlags = subtreeFlags | TransformFlags.AssertES2015; - - // A class with a parameter property assignment or decorator is TypeScript syntax. - if (subtreeFlags & TransformFlags.ContainsTypeScriptClassSyntax - || node.typeParameters) { - transformFlags |= TransformFlags.AssertTypeScript; + function bindFunctionExpression(node: FunctionExpression) { + if (!file.isDeclarationFile && !(node.flags & NodeFlags.Ambient)) { + if (isAsyncFunction(node)) { + emitFlags |= NodeFlags.HasAsyncFunctions; + } } - - node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; - return transformFlags & ~TransformFlags.ClassExcludes; - } - - function computeHeritageClause(node: HeritageClause, subtreeFlags: TransformFlags) { - let transformFlags = subtreeFlags; - - switch (node.token) { - case SyntaxKind.ExtendsKeyword: - // An `extends` HeritageClause is ES6 syntax. - transformFlags |= TransformFlags.AssertES2015; - break; - - case SyntaxKind.ImplementsKeyword: - // An `implements` HeritageClause is TypeScript syntax. - transformFlags |= TransformFlags.AssertTypeScript; - break; - - default: - Debug.fail("Unexpected token for heritage clause"); - break; + if (currentFlow) { + node.flowNode = currentFlow; } - - node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; - return transformFlags & ~TransformFlags.NodeExcludes; + checkStrictModeFunctionName(node); + const bindingName = node.name ? node.name.escapedText : InternalSymbolName.Function; + return bindAnonymousDeclaration(node, SymbolFlags.Function, bindingName); } - - function computeCatchClause(node: CatchClause, subtreeFlags: TransformFlags) { - let transformFlags = subtreeFlags; - - if (!node.variableDeclaration) { - transformFlags |= TransformFlags.AssertES2019; + function bindPropertyOrMethodOrAccessor(node: Declaration, symbolFlags: SymbolFlags, symbolExcludes: SymbolFlags) { + if (!file.isDeclarationFile && !(node.flags & NodeFlags.Ambient) && isAsyncFunction(node)) { + emitFlags |= NodeFlags.HasAsyncFunctions; } - else if (isBindingPattern(node.variableDeclaration.name)) { - transformFlags |= TransformFlags.AssertES2015; + if (currentFlow && isObjectLiteralOrClassExpressionMethod(node)) { + node.flowNode = currentFlow; } - - node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; - return transformFlags & ~TransformFlags.CatchClauseExcludes; - } - - function computeExpressionWithTypeArguments(node: ExpressionWithTypeArguments, subtreeFlags: TransformFlags) { - // An ExpressionWithTypeArguments is ES6 syntax, as it is used in the - // extends clause of a class. - let transformFlags = subtreeFlags | TransformFlags.AssertES2015; - - // If an ExpressionWithTypeArguments contains type arguments, then it - // is TypeScript syntax. - if (node.typeArguments) { - transformFlags |= TransformFlags.AssertTypeScript; + return hasDynamicName(node) + ? bindAnonymousDeclaration(node, symbolFlags, InternalSymbolName.Computed) + : declareSymbolAndAddToSymbolTable(node, symbolFlags, symbolExcludes); + } + function getInferTypeContainer(node: Node): ConditionalTypeNode | undefined { + const extendsType = findAncestor(node, n => n.parent && isConditionalTypeNode(n.parent) && n.parent.extendsType === n); + return extendsType && (extendsType.parent as ConditionalTypeNode); + } + function bindTypeParameter(node: TypeParameterDeclaration) { + if (isJSDocTemplateTag(node.parent)) { + const container = find(((node.parent.parent as JSDoc).tags!), isJSDocTypeAlias) || getHostSignatureFromJSDoc(node.parent); // TODO: GH#18217 + if (container) { + if (!container.locals) { + container.locals = createSymbolTable(); + } + declareSymbol(container.locals, /*parent*/ undefined, node, SymbolFlags.TypeParameter, SymbolFlags.TypeParameterExcludes); + } + else { + declareSymbolAndAddToSymbolTable(node, SymbolFlags.TypeParameter, SymbolFlags.TypeParameterExcludes); + } } - - node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; - return transformFlags & ~TransformFlags.NodeExcludes; - } - - function computeConstructor(node: ConstructorDeclaration, subtreeFlags: TransformFlags) { - let transformFlags = subtreeFlags; - - // TypeScript-specific modifiers and overloads are TypeScript syntax - if (hasModifier(node, ModifierFlags.TypeScriptModifier) - || !node.body) { - transformFlags |= TransformFlags.AssertTypeScript; + else if (node.parent.kind === SyntaxKind.InferType) { + const container = getInferTypeContainer(node.parent); + if (container) { + if (!container.locals) { + container.locals = createSymbolTable(); + } + declareSymbol(container.locals, /*parent*/ undefined, node, SymbolFlags.TypeParameter, SymbolFlags.TypeParameterExcludes); + } + else { + bindAnonymousDeclaration(node, SymbolFlags.TypeParameter, (getDeclarationName(node)!)); // TODO: GH#18217 + } } - - // function declarations with object rest destructuring are ES2018 syntax - if (subtreeFlags & TransformFlags.ContainsObjectRestOrSpread) { - transformFlags |= TransformFlags.AssertES2018; + else { + declareSymbolAndAddToSymbolTable(node, SymbolFlags.TypeParameter, SymbolFlags.TypeParameterExcludes); } - - node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; - return transformFlags & ~TransformFlags.ConstructorExcludes; - } - - function computeMethod(node: MethodDeclaration, subtreeFlags: TransformFlags) { - // A MethodDeclaration is ES6 syntax. - let transformFlags = subtreeFlags | TransformFlags.AssertES2015; - - // Decorators, TypeScript-specific modifiers, type parameters, type annotations, and - // overloads are TypeScript syntax. - if (node.decorators - || hasModifier(node, ModifierFlags.TypeScriptModifier) - || node.typeParameters - || node.type - || !node.body - || node.questionToken) { - transformFlags |= TransformFlags.AssertTypeScript; + } + // reachability checks + function shouldReportErrorOnModuleDeclaration(node: ModuleDeclaration): boolean { + const instanceState = getModuleInstanceState(node); + return instanceState === ModuleInstanceState.Instantiated || (instanceState === ModuleInstanceState.ConstEnumOnly && !!options.preserveConstEnums); + } + function checkUnreachable(node: Node): boolean { + if (!(currentFlow.flags & FlowFlags.Unreachable)) { + return false; } - - // function declarations with object rest destructuring are ES2018 syntax - if (subtreeFlags & TransformFlags.ContainsObjectRestOrSpread) { - transformFlags |= TransformFlags.AssertES2018; + if (currentFlow === unreachableFlow) { + const reportError = + // report error on all statements except empty ones + (isStatementButNotDeclaration(node) && node.kind !== SyntaxKind.EmptyStatement) || + // report error on class declarations + node.kind === SyntaxKind.ClassDeclaration || + // report error on instantiated modules or const-enums only modules if preserveConstEnums is set + (node.kind === SyntaxKind.ModuleDeclaration && shouldReportErrorOnModuleDeclaration((node))); + if (reportError) { + currentFlow = reportedUnreachableFlow; + if (!options.allowUnreachableCode) { + // unreachable code is reported if + // - user has explicitly asked about it AND + // - statement is in not ambient context (statements in ambient context is already an error + // so we should not report extras) AND + // - node is not variable statement OR + // - node is block scoped variable statement OR + // - node is not block scoped variable statement and at least one variable declaration has initializer + // Rationale: we don't want to report errors on non-initialized var's since they are hoisted + // On the other side we do want to report errors on non-initialized 'lets' because of TDZ + const isError = unreachableCodeIsError(options) && + !(node.flags & NodeFlags.Ambient) && + (!isVariableStatement(node) || + !!(getCombinedNodeFlags(node.declarationList) & NodeFlags.BlockScoped) || + node.declarationList.declarations.some(d => !!d.initializer)); + eachUnreachableRange(node, (start, end) => errorOrSuggestionOnRange(isError, start, end, Diagnostics.Unreachable_code_detected)); + } + } + } + return true; + } +} +/* @internal */ +function eachUnreachableRange(node: Node, cb: (start: Node, last: Node) => void): void { + if (isStatement(node) && isExecutableStatement(node) && isBlock(node.parent)) { + const { statements } = node.parent; + const slice = sliceAfter(statements, node); + getRangesWhere(slice, isExecutableStatement, (start, afterEnd) => cb(slice[start], slice[afterEnd - 1])); + } + else { + cb(node, node); + } +} +// As opposed to a pure declaration like an `interface` +/* @internal */ +function isExecutableStatement(s: Statement): boolean { + // Don't remove statements that can validly be used before they appear. + return !isFunctionDeclaration(s) && !isPurelyTypeDeclaration(s) && !isEnumDeclaration(s) && + // `var x;` may declare a variable used above + !(isVariableStatement(s) && !(getCombinedNodeFlags(s) & (NodeFlags.Let | NodeFlags.Const)) && s.declarationList.declarations.some(d => !d.initializer)); +} +/* @internal */ +function isPurelyTypeDeclaration(s: Statement): boolean { + switch (s.kind) { + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.TypeAliasDeclaration: + return true; + case SyntaxKind.ModuleDeclaration: + return getModuleInstanceState((s as ModuleDeclaration)) !== ModuleInstanceState.Instantiated; + case SyntaxKind.EnumDeclaration: + return hasModifier(s, ModifierFlags.Const); + default: + return false; + } +} +/* @internal */ +export function isExportsOrModuleExportsOrAlias(sourceFile: SourceFile, node: Expression): boolean { + let i = 0; + const q = [node]; + while (q.length && i < 100) { + i++; + node = q.shift()!; + if (isExportsIdentifier(node) || isModuleExportsAccessExpression(node)) { + return true; } - - // An async method declaration is ES2017 syntax. - if (hasModifier(node, ModifierFlags.Async)) { - transformFlags |= node.asteriskToken ? TransformFlags.AssertES2018 : TransformFlags.AssertES2017; + else if (isIdentifier(node)) { + const symbol = lookupSymbolForNameWorker(sourceFile, node.escapedText); + if (!!symbol && !!symbol.valueDeclaration && isVariableDeclaration(symbol.valueDeclaration) && !!symbol.valueDeclaration.initializer) { + const init = symbol.valueDeclaration.initializer; + q.push(init); + if (isAssignmentExpression(init, /*excludeCompoundAssignment*/ true)) { + q.push(init.left); + q.push(init.right); + } + } } - - if (node.asteriskToken) { - transformFlags |= TransformFlags.AssertGenerator; + } + return false; +} +/* @internal */ +function lookupSymbolForNameWorker(container: Node, name: __String): Symbol | undefined { + const local = container.locals && container.locals.get(name); + if (local) { + return local.exportSymbol || local; + } + if (isSourceFile(container) && container.jsGlobalAugmentations && container.jsGlobalAugmentations.has(name)) { + return container.jsGlobalAugmentations.get(name); + } + return container.symbol && container.symbol.exports && container.symbol.exports.get(name); +} +/** + * Computes the transform flags for a node, given the transform flags of its subtree + * + * @param node The node to analyze + * @param subtreeFlags Transform flags computed for this node's subtree + */ +/* @internal */ +export function computeTransformFlagsForNode(node: Node, subtreeFlags: TransformFlags): TransformFlags { + const kind = node.kind; + switch (kind) { + case SyntaxKind.CallExpression: + return computeCallExpression((node), subtreeFlags); + case SyntaxKind.NewExpression: + return computeNewExpression((node), subtreeFlags); + case SyntaxKind.ModuleDeclaration: + return computeModuleDeclaration((node), subtreeFlags); + case SyntaxKind.ParenthesizedExpression: + return computeParenthesizedExpression((node), subtreeFlags); + case SyntaxKind.BinaryExpression: + return computeBinaryExpression((node), subtreeFlags); + case SyntaxKind.ExpressionStatement: + return computeExpressionStatement((node), subtreeFlags); + case SyntaxKind.Parameter: + return computeParameter((node), subtreeFlags); + case SyntaxKind.ArrowFunction: + return computeArrowFunction((node), subtreeFlags); + case SyntaxKind.FunctionExpression: + return computeFunctionExpression((node), subtreeFlags); + case SyntaxKind.FunctionDeclaration: + return computeFunctionDeclaration((node), subtreeFlags); + case SyntaxKind.VariableDeclaration: + return computeVariableDeclaration((node), subtreeFlags); + case SyntaxKind.VariableDeclarationList: + return computeVariableDeclarationList((node), subtreeFlags); + case SyntaxKind.VariableStatement: + return computeVariableStatement((node), subtreeFlags); + case SyntaxKind.LabeledStatement: + return computeLabeledStatement((node), subtreeFlags); + case SyntaxKind.ClassDeclaration: + return computeClassDeclaration((node), subtreeFlags); + case SyntaxKind.ClassExpression: + return computeClassExpression((node), subtreeFlags); + case SyntaxKind.HeritageClause: + return computeHeritageClause((node), subtreeFlags); + case SyntaxKind.CatchClause: + return computeCatchClause((node), subtreeFlags); + case SyntaxKind.ExpressionWithTypeArguments: + return computeExpressionWithTypeArguments((node), subtreeFlags); + case SyntaxKind.Constructor: + return computeConstructor((node), subtreeFlags); + case SyntaxKind.PropertyDeclaration: + return computePropertyDeclaration((node), subtreeFlags); + case SyntaxKind.MethodDeclaration: + return computeMethod((node), subtreeFlags); + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + return computeAccessor((node), subtreeFlags); + case SyntaxKind.ImportEqualsDeclaration: + return computeImportEquals((node), subtreeFlags); + case SyntaxKind.PropertyAccessExpression: + return computePropertyAccess((node), subtreeFlags); + case SyntaxKind.ElementAccessExpression: + return computeElementAccess((node), subtreeFlags); + default: + return computeOther(node, kind, subtreeFlags); + } +} +/* @internal */ +function computeCallExpression(node: CallExpression, subtreeFlags: TransformFlags) { + let transformFlags = subtreeFlags; + const callee = skipOuterExpressions(node.expression); + const expression = node.expression; + if (node.flags & NodeFlags.OptionalChain) { + transformFlags |= TransformFlags.ContainsES2020; + } + if (node.typeArguments) { + transformFlags |= TransformFlags.AssertTypeScript; + } + if (subtreeFlags & TransformFlags.ContainsRestOrSpread || isSuperOrSuperProperty(callee)) { + // If the this node contains a SpreadExpression, or is a super call, then it is an ES6 + // node. + transformFlags |= TransformFlags.AssertES2015; + if (isSuperProperty(callee)) { + transformFlags |= TransformFlags.ContainsLexicalThis; } - - node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; - return propagatePropertyNameFlags(node.name, transformFlags & ~TransformFlags.MethodOrAccessorExcludes); - } - - function computeAccessor(node: AccessorDeclaration, subtreeFlags: TransformFlags) { - let transformFlags = subtreeFlags; - - // Decorators, TypeScript-specific modifiers, type annotations, and overloads are - // TypeScript syntax. - if (node.decorators - || hasModifier(node, ModifierFlags.TypeScriptModifier) - || node.type - || !node.body) { + } + if (expression.kind === SyntaxKind.ImportKeyword) { + transformFlags |= TransformFlags.ContainsDynamicImport; + } + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; + return transformFlags & ~TransformFlags.ArrayLiteralOrCallOrNewExcludes; +} +/* @internal */ +function computeNewExpression(node: NewExpression, subtreeFlags: TransformFlags) { + let transformFlags = subtreeFlags; + if (node.typeArguments) { + transformFlags |= TransformFlags.AssertTypeScript; + } + if (subtreeFlags & TransformFlags.ContainsRestOrSpread) { + // If the this node contains a SpreadElementExpression then it is an ES6 + // node. + transformFlags |= TransformFlags.AssertES2015; + } + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; + return transformFlags & ~TransformFlags.ArrayLiteralOrCallOrNewExcludes; +} +/* @internal */ +function computeBinaryExpression(node: BinaryExpression, subtreeFlags: TransformFlags) { + let transformFlags = subtreeFlags; + const operatorTokenKind = node.operatorToken.kind; + const leftKind = node.left.kind; + if (operatorTokenKind === SyntaxKind.QuestionQuestionToken) { + transformFlags |= TransformFlags.AssertES2020; + } + else if (operatorTokenKind === SyntaxKind.EqualsToken && leftKind === SyntaxKind.ObjectLiteralExpression) { + // Destructuring object assignments with are ES2015 syntax + // and possibly ES2018 if they contain rest + transformFlags |= TransformFlags.AssertES2018 | TransformFlags.AssertES2015 | TransformFlags.AssertDestructuringAssignment; + } + else if (operatorTokenKind === SyntaxKind.EqualsToken && leftKind === SyntaxKind.ArrayLiteralExpression) { + // Destructuring assignments are ES2015 syntax. + transformFlags |= TransformFlags.AssertES2015 | TransformFlags.AssertDestructuringAssignment; + } + else if (operatorTokenKind === SyntaxKind.AsteriskAsteriskToken + || operatorTokenKind === SyntaxKind.AsteriskAsteriskEqualsToken) { + // Exponentiation is ES2016 syntax. + transformFlags |= TransformFlags.AssertES2016; + } + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; + return transformFlags & ~TransformFlags.NodeExcludes; +} +/* @internal */ +function computeParameter(node: ParameterDeclaration, subtreeFlags: TransformFlags) { + let transformFlags = subtreeFlags; + const name = node.name; + const initializer = node.initializer; + const dotDotDotToken = node.dotDotDotToken; + // The '?' token, type annotations, decorators, and 'this' parameters are TypeSCript + // syntax. + if (node.questionToken + || node.type + || (subtreeFlags & TransformFlags.ContainsTypeScriptClassSyntax && some(node.decorators)) + || isThisIdentifier(name)) { + transformFlags |= TransformFlags.AssertTypeScript; + } + // If a parameter has an accessibility modifier, then it is TypeScript syntax. + if (hasModifier(node, ModifierFlags.ParameterPropertyModifier)) { + transformFlags |= TransformFlags.AssertTypeScript | TransformFlags.ContainsTypeScriptClassSyntax; + } + // parameters with object rest destructuring are ES2018 syntax + if (subtreeFlags & TransformFlags.ContainsObjectRestOrSpread) { + transformFlags |= TransformFlags.AssertES2018; + } + // If a parameter has an initializer, a binding pattern or a dotDotDot token, then + // it is ES6 syntax and its container must emit default value assignments or parameter destructuring downlevel. + if (subtreeFlags & TransformFlags.ContainsBindingPattern || initializer || dotDotDotToken) { + transformFlags |= TransformFlags.AssertES2015; + } + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; + return transformFlags & ~TransformFlags.ParameterExcludes; +} +/* @internal */ +function computeParenthesizedExpression(node: ParenthesizedExpression, subtreeFlags: TransformFlags) { + let transformFlags = subtreeFlags; + const expression = node.expression; + const expressionKind = expression.kind; + // If the node is synthesized, it means the emitter put the parentheses there, + // not the user. If we didn't want them, the emitter would not have put them + // there. + if (expressionKind === SyntaxKind.AsExpression + || expressionKind === SyntaxKind.TypeAssertionExpression) { + transformFlags |= TransformFlags.AssertTypeScript; + } + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; + return transformFlags & ~TransformFlags.OuterExpressionExcludes; +} +/* @internal */ +function computeClassDeclaration(node: ClassDeclaration, subtreeFlags: TransformFlags) { + let transformFlags: TransformFlags; + if (hasModifier(node, ModifierFlags.Ambient)) { + // An ambient declaration is TypeScript syntax. + transformFlags = TransformFlags.AssertTypeScript; + } + else { + // A ClassDeclaration is ES6 syntax. + transformFlags = subtreeFlags | TransformFlags.AssertES2015; + // A class with a parameter property assignment or decorator is TypeScript syntax. + // An exported declaration may be TypeScript syntax, but is handled by the visitor + // for a namespace declaration. + if ((subtreeFlags & TransformFlags.ContainsTypeScriptClassSyntax) + || node.typeParameters) { transformFlags |= TransformFlags.AssertTypeScript; } - - // function declarations with object rest destructuring are ES2018 syntax - if (subtreeFlags & TransformFlags.ContainsObjectRestOrSpread) { - transformFlags |= TransformFlags.AssertES2018; - } - - node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; - return propagatePropertyNameFlags(node.name, transformFlags & ~TransformFlags.MethodOrAccessorExcludes); } - - function computePropertyDeclaration(node: PropertyDeclaration, subtreeFlags: TransformFlags) { - let transformFlags = subtreeFlags | TransformFlags.ContainsClassFields; - - // Decorators, TypeScript-specific modifiers, and type annotations are TypeScript syntax. - if (some(node.decorators) || hasModifier(node, ModifierFlags.TypeScriptModifier) || node.type || node.questionToken || node.exclamationToken) { + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; + return transformFlags & ~TransformFlags.ClassExcludes; +} +/* @internal */ +function computeClassExpression(node: ClassExpression, subtreeFlags: TransformFlags) { + // A ClassExpression is ES6 syntax. + let transformFlags = subtreeFlags | TransformFlags.AssertES2015; + // A class with a parameter property assignment or decorator is TypeScript syntax. + if (subtreeFlags & TransformFlags.ContainsTypeScriptClassSyntax + || node.typeParameters) { + transformFlags |= TransformFlags.AssertTypeScript; + } + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; + return transformFlags & ~TransformFlags.ClassExcludes; +} +/* @internal */ +function computeHeritageClause(node: HeritageClause, subtreeFlags: TransformFlags) { + let transformFlags = subtreeFlags; + switch (node.token) { + case SyntaxKind.ExtendsKeyword: + // An `extends` HeritageClause is ES6 syntax. + transformFlags |= TransformFlags.AssertES2015; + break; + case SyntaxKind.ImplementsKeyword: + // An `implements` HeritageClause is TypeScript syntax. transformFlags |= TransformFlags.AssertTypeScript; - } - - // Hoisted variables related to class properties should live within the TypeScript class wrapper. - if (isComputedPropertyName(node.name) || (hasStaticModifier(node) && node.initializer)) { - transformFlags |= TransformFlags.ContainsTypeScriptClassSyntax; - } - - node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; - return propagatePropertyNameFlags(node.name, transformFlags & ~TransformFlags.PropertyExcludes); - } - - function computeFunctionDeclaration(node: FunctionDeclaration, subtreeFlags: TransformFlags) { - let transformFlags: TransformFlags; - const modifierFlags = getModifierFlags(node); - const body = node.body; - - if (!body || (modifierFlags & ModifierFlags.Ambient)) { - // An ambient declaration is TypeScript syntax. - // A FunctionDeclaration without a body is an overload and is TypeScript syntax. - transformFlags = TransformFlags.AssertTypeScript; - } - else { - transformFlags = subtreeFlags | TransformFlags.ContainsHoistedDeclarationOrCompletion; - - // TypeScript-specific modifiers, type parameters, and type annotations are TypeScript - // syntax. - if (modifierFlags & ModifierFlags.TypeScriptModifier - || node.typeParameters - || node.type) { - transformFlags |= TransformFlags.AssertTypeScript; - } - - // An async function declaration is ES2017 syntax. - if (modifierFlags & ModifierFlags.Async) { - transformFlags |= node.asteriskToken ? TransformFlags.AssertES2018 : TransformFlags.AssertES2017; - } - - // function declarations with object rest destructuring are ES2018 syntax - if (subtreeFlags & TransformFlags.ContainsObjectRestOrSpread) { - transformFlags |= TransformFlags.AssertES2018; - } - - // If a FunctionDeclaration is generator function and is the body of a - // transformed async function, then this node can be transformed to a - // down-level generator. - // Currently we do not support transforming any other generator functions - // down level. - if (node.asteriskToken) { - transformFlags |= TransformFlags.AssertGenerator; - } - } - - node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; - return transformFlags & ~TransformFlags.FunctionExcludes; + break; + default: + Debug.fail("Unexpected token for heritage clause"); + break; + } + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; + return transformFlags & ~TransformFlags.NodeExcludes; +} +/* @internal */ +function computeCatchClause(node: CatchClause, subtreeFlags: TransformFlags) { + let transformFlags = subtreeFlags; + if (!node.variableDeclaration) { + transformFlags |= TransformFlags.AssertES2019; + } + else if (isBindingPattern(node.variableDeclaration.name)) { + transformFlags |= TransformFlags.AssertES2015; + } + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; + return transformFlags & ~TransformFlags.CatchClauseExcludes; +} +/* @internal */ +function computeExpressionWithTypeArguments(node: ExpressionWithTypeArguments, subtreeFlags: TransformFlags) { + // An ExpressionWithTypeArguments is ES6 syntax, as it is used in the + // extends clause of a class. + let transformFlags = subtreeFlags | TransformFlags.AssertES2015; + // If an ExpressionWithTypeArguments contains type arguments, then it + // is TypeScript syntax. + if (node.typeArguments) { + transformFlags |= TransformFlags.AssertTypeScript; + } + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; + return transformFlags & ~TransformFlags.NodeExcludes; +} +/* @internal */ +function computeConstructor(node: ConstructorDeclaration, subtreeFlags: TransformFlags) { + let transformFlags = subtreeFlags; + // TypeScript-specific modifiers and overloads are TypeScript syntax + if (hasModifier(node, ModifierFlags.TypeScriptModifier) + || !node.body) { + transformFlags |= TransformFlags.AssertTypeScript; + } + // function declarations with object rest destructuring are ES2018 syntax + if (subtreeFlags & TransformFlags.ContainsObjectRestOrSpread) { + transformFlags |= TransformFlags.AssertES2018; + } + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; + return transformFlags & ~TransformFlags.ConstructorExcludes; +} +/* @internal */ +function computeMethod(node: MethodDeclaration, subtreeFlags: TransformFlags) { + // A MethodDeclaration is ES6 syntax. + let transformFlags = subtreeFlags | TransformFlags.AssertES2015; + // Decorators, TypeScript-specific modifiers, type parameters, type annotations, and + // overloads are TypeScript syntax. + if (node.decorators + || hasModifier(node, ModifierFlags.TypeScriptModifier) + || node.typeParameters + || node.type + || !node.body + || node.questionToken) { + transformFlags |= TransformFlags.AssertTypeScript; + } + // function declarations with object rest destructuring are ES2018 syntax + if (subtreeFlags & TransformFlags.ContainsObjectRestOrSpread) { + transformFlags |= TransformFlags.AssertES2018; + } + // An async method declaration is ES2017 syntax. + if (hasModifier(node, ModifierFlags.Async)) { + transformFlags |= node.asteriskToken ? TransformFlags.AssertES2018 : TransformFlags.AssertES2017; + } + if (node.asteriskToken) { + transformFlags |= TransformFlags.AssertGenerator; + } + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; + return propagatePropertyNameFlags(node.name, transformFlags & ~TransformFlags.MethodOrAccessorExcludes); +} +/* @internal */ +function computeAccessor(node: AccessorDeclaration, subtreeFlags: TransformFlags) { + let transformFlags = subtreeFlags; + // Decorators, TypeScript-specific modifiers, type annotations, and overloads are + // TypeScript syntax. + if (node.decorators + || hasModifier(node, ModifierFlags.TypeScriptModifier) + || node.type + || !node.body) { + transformFlags |= TransformFlags.AssertTypeScript; + } + // function declarations with object rest destructuring are ES2018 syntax + if (subtreeFlags & TransformFlags.ContainsObjectRestOrSpread) { + transformFlags |= TransformFlags.AssertES2018; + } + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; + return propagatePropertyNameFlags(node.name, transformFlags & ~TransformFlags.MethodOrAccessorExcludes); +} +/* @internal */ +function computePropertyDeclaration(node: PropertyDeclaration, subtreeFlags: TransformFlags) { + let transformFlags = subtreeFlags | TransformFlags.ContainsClassFields; + // Decorators, TypeScript-specific modifiers, and type annotations are TypeScript syntax. + if (some(node.decorators) || hasModifier(node, ModifierFlags.TypeScriptModifier) || node.type || node.questionToken || node.exclamationToken) { + transformFlags |= TransformFlags.AssertTypeScript; } - - function computeFunctionExpression(node: FunctionExpression, subtreeFlags: TransformFlags) { - let transformFlags = subtreeFlags; - + // Hoisted variables related to class properties should live within the TypeScript class wrapper. + if (isComputedPropertyName(node.name) || (hasStaticModifier(node) && node.initializer)) { + transformFlags |= TransformFlags.ContainsTypeScriptClassSyntax; + } + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; + return propagatePropertyNameFlags(node.name, transformFlags & ~TransformFlags.PropertyExcludes); +} +/* @internal */ +function computeFunctionDeclaration(node: FunctionDeclaration, subtreeFlags: TransformFlags) { + let transformFlags: TransformFlags; + const modifierFlags = getModifierFlags(node); + const body = node.body; + if (!body || (modifierFlags & ModifierFlags.Ambient)) { + // An ambient declaration is TypeScript syntax. + // A FunctionDeclaration without a body is an overload and is TypeScript syntax. + transformFlags = TransformFlags.AssertTypeScript; + } + else { + transformFlags = subtreeFlags | TransformFlags.ContainsHoistedDeclarationOrCompletion; // TypeScript-specific modifiers, type parameters, and type annotations are TypeScript // syntax. - if (hasModifier(node, ModifierFlags.TypeScriptModifier) + if (modifierFlags & ModifierFlags.TypeScriptModifier || node.typeParameters || node.type) { transformFlags |= TransformFlags.AssertTypeScript; } - - // An async function expression is ES2017 syntax. - if (hasModifier(node, ModifierFlags.Async)) { + // An async function declaration is ES2017 syntax. + if (modifierFlags & ModifierFlags.Async) { transformFlags |= node.asteriskToken ? TransformFlags.AssertES2018 : TransformFlags.AssertES2017; } - - // function expressions with object rest destructuring are ES2018 syntax + // function declarations with object rest destructuring are ES2018 syntax if (subtreeFlags & TransformFlags.ContainsObjectRestOrSpread) { transformFlags |= TransformFlags.AssertES2018; } - - // If a FunctionExpression is generator function and is the body of a + // If a FunctionDeclaration is generator function and is the body of a // transformed async function, then this node can be transformed to a // down-level generator. + // Currently we do not support transforming any other generator functions + // down level. if (node.asteriskToken) { transformFlags |= TransformFlags.AssertGenerator; } - - node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; - return transformFlags & ~TransformFlags.FunctionExcludes; } - - function computeArrowFunction(node: ArrowFunction, subtreeFlags: TransformFlags) { - // An ArrowFunction is ES6 syntax, and excludes markers that should not escape the scope of an ArrowFunction. - let transformFlags = subtreeFlags | TransformFlags.AssertES2015; - - // TypeScript-specific modifiers, type parameters, and type annotations are TypeScript - // syntax. - if (hasModifier(node, ModifierFlags.TypeScriptModifier) - || node.typeParameters - || node.type) { - transformFlags |= TransformFlags.AssertTypeScript; - } - - // An async arrow function is ES2017 syntax. - if (hasModifier(node, ModifierFlags.Async)) { - transformFlags |= TransformFlags.AssertES2017; - } - - // arrow functions with object rest destructuring are ES2018 syntax - if (subtreeFlags & TransformFlags.ContainsObjectRestOrSpread) { - transformFlags |= TransformFlags.AssertES2018; - } - - node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; - return transformFlags & ~TransformFlags.ArrowFunctionExcludes; - } - - function computePropertyAccess(node: PropertyAccessExpression, subtreeFlags: TransformFlags) { - let transformFlags = subtreeFlags; - - if (node.flags & NodeFlags.OptionalChain) { - transformFlags |= TransformFlags.ContainsES2020; - } - - // If a PropertyAccessExpression starts with a super keyword, then it is - // ES6 syntax, and requires a lexical `this` binding. - if (node.expression.kind === SyntaxKind.SuperKeyword) { - // super inside of an async function requires hoisting the super access (ES2017). - // same for super inside of an async generator, which is ES2018. - transformFlags |= TransformFlags.ContainsES2017 | TransformFlags.ContainsES2018; - } - - node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; - return transformFlags & ~TransformFlags.PropertyAccessExcludes; - } - - function computeElementAccess(node: ElementAccessExpression, subtreeFlags: TransformFlags) { - let transformFlags = subtreeFlags; - - if (node.flags & NodeFlags.OptionalChain) { - transformFlags |= TransformFlags.ContainsES2020; - } - - // If an ElementAccessExpression starts with a super keyword, then it is - // ES6 syntax, and requires a lexical `this` binding. - if (node.expression.kind === SyntaxKind.SuperKeyword) { - // super inside of an async function requires hoisting the super access (ES2017). - // same for super inside of an async generator, which is ES2018. - transformFlags |= TransformFlags.ContainsES2017 | TransformFlags.ContainsES2018; - } - - node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; - return transformFlags & ~TransformFlags.PropertyAccessExcludes; - } - - function computeVariableDeclaration(node: VariableDeclaration, subtreeFlags: TransformFlags) { - let transformFlags = subtreeFlags; - transformFlags |= TransformFlags.AssertES2015 | TransformFlags.ContainsBindingPattern; // TODO(rbuckton): Why are these set unconditionally? - - // A VariableDeclaration containing ObjectRest is ES2018 syntax - if (subtreeFlags & TransformFlags.ContainsObjectRestOrSpread) { - transformFlags |= TransformFlags.AssertES2018; - } - - // Type annotations are TypeScript syntax. - if (node.type || node.exclamationToken) { - transformFlags |= TransformFlags.AssertTypeScript; - } - - node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; - return transformFlags & ~TransformFlags.NodeExcludes; + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; + return transformFlags & ~TransformFlags.FunctionExcludes; +} +/* @internal */ +function computeFunctionExpression(node: FunctionExpression, subtreeFlags: TransformFlags) { + let transformFlags = subtreeFlags; + // TypeScript-specific modifiers, type parameters, and type annotations are TypeScript + // syntax. + if (hasModifier(node, ModifierFlags.TypeScriptModifier) + || node.typeParameters + || node.type) { + transformFlags |= TransformFlags.AssertTypeScript; } - - function computeVariableStatement(node: VariableStatement, subtreeFlags: TransformFlags) { - let transformFlags: TransformFlags; - const declarationListTransformFlags = node.declarationList.transformFlags; - - // An ambient declaration is TypeScript syntax. - if (hasModifier(node, ModifierFlags.Ambient)) { - transformFlags = TransformFlags.AssertTypeScript; - } - else { - transformFlags = subtreeFlags; - - if (declarationListTransformFlags & TransformFlags.ContainsBindingPattern) { - transformFlags |= TransformFlags.AssertES2015; - } - } - - node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; - return transformFlags & ~TransformFlags.NodeExcludes; - } - - function computeLabeledStatement(node: LabeledStatement, subtreeFlags: TransformFlags) { - let transformFlags = subtreeFlags; - - // A labeled statement containing a block scoped binding *may* need to be transformed from ES6. - if (subtreeFlags & TransformFlags.ContainsBlockScopedBinding - && isIterationStatement(node, /*lookInLabeledStatements*/ true)) { + // An async function expression is ES2017 syntax. + if (hasModifier(node, ModifierFlags.Async)) { + transformFlags |= node.asteriskToken ? TransformFlags.AssertES2018 : TransformFlags.AssertES2017; + } + // function expressions with object rest destructuring are ES2018 syntax + if (subtreeFlags & TransformFlags.ContainsObjectRestOrSpread) { + transformFlags |= TransformFlags.AssertES2018; + } + // If a FunctionExpression is generator function and is the body of a + // transformed async function, then this node can be transformed to a + // down-level generator. + if (node.asteriskToken) { + transformFlags |= TransformFlags.AssertGenerator; + } + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; + return transformFlags & ~TransformFlags.FunctionExcludes; +} +/* @internal */ +function computeArrowFunction(node: ArrowFunction, subtreeFlags: TransformFlags) { + // An ArrowFunction is ES6 syntax, and excludes markers that should not escape the scope of an ArrowFunction. + let transformFlags = subtreeFlags | TransformFlags.AssertES2015; + // TypeScript-specific modifiers, type parameters, and type annotations are TypeScript + // syntax. + if (hasModifier(node, ModifierFlags.TypeScriptModifier) + || node.typeParameters + || node.type) { + transformFlags |= TransformFlags.AssertTypeScript; + } + // An async arrow function is ES2017 syntax. + if (hasModifier(node, ModifierFlags.Async)) { + transformFlags |= TransformFlags.AssertES2017; + } + // arrow functions with object rest destructuring are ES2018 syntax + if (subtreeFlags & TransformFlags.ContainsObjectRestOrSpread) { + transformFlags |= TransformFlags.AssertES2018; + } + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; + return transformFlags & ~TransformFlags.ArrowFunctionExcludes; +} +/* @internal */ +function computePropertyAccess(node: PropertyAccessExpression, subtreeFlags: TransformFlags) { + let transformFlags = subtreeFlags; + if (node.flags & NodeFlags.OptionalChain) { + transformFlags |= TransformFlags.ContainsES2020; + } + // If a PropertyAccessExpression starts with a super keyword, then it is + // ES6 syntax, and requires a lexical `this` binding. + if (node.expression.kind === SyntaxKind.SuperKeyword) { + // super inside of an async function requires hoisting the super access (ES2017). + // same for super inside of an async generator, which is ES2018. + transformFlags |= TransformFlags.ContainsES2017 | TransformFlags.ContainsES2018; + } + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; + return transformFlags & ~TransformFlags.PropertyAccessExcludes; +} +/* @internal */ +function computeElementAccess(node: ElementAccessExpression, subtreeFlags: TransformFlags) { + let transformFlags = subtreeFlags; + if (node.flags & NodeFlags.OptionalChain) { + transformFlags |= TransformFlags.ContainsES2020; + } + // If an ElementAccessExpression starts with a super keyword, then it is + // ES6 syntax, and requires a lexical `this` binding. + if (node.expression.kind === SyntaxKind.SuperKeyword) { + // super inside of an async function requires hoisting the super access (ES2017). + // same for super inside of an async generator, which is ES2018. + transformFlags |= TransformFlags.ContainsES2017 | TransformFlags.ContainsES2018; + } + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; + return transformFlags & ~TransformFlags.PropertyAccessExcludes; +} +/* @internal */ +function computeVariableDeclaration(node: VariableDeclaration, subtreeFlags: TransformFlags) { + let transformFlags = subtreeFlags; + transformFlags |= TransformFlags.AssertES2015 | TransformFlags.ContainsBindingPattern; // TODO(rbuckton): Why are these set unconditionally? + // A VariableDeclaration containing ObjectRest is ES2018 syntax + if (subtreeFlags & TransformFlags.ContainsObjectRestOrSpread) { + transformFlags |= TransformFlags.AssertES2018; + } + // Type annotations are TypeScript syntax. + if (node.type || node.exclamationToken) { + transformFlags |= TransformFlags.AssertTypeScript; + } + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; + return transformFlags & ~TransformFlags.NodeExcludes; +} +/* @internal */ +function computeVariableStatement(node: VariableStatement, subtreeFlags: TransformFlags) { + let transformFlags: TransformFlags; + const declarationListTransformFlags = node.declarationList.transformFlags; + // An ambient declaration is TypeScript syntax. + if (hasModifier(node, ModifierFlags.Ambient)) { + transformFlags = TransformFlags.AssertTypeScript; + } + else { + transformFlags = subtreeFlags; + if (declarationListTransformFlags & TransformFlags.ContainsBindingPattern) { transformFlags |= TransformFlags.AssertES2015; } - - node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; - return transformFlags & ~TransformFlags.NodeExcludes; } - - function computeImportEquals(node: ImportEqualsDeclaration, subtreeFlags: TransformFlags) { - let transformFlags = subtreeFlags; - - // An ImportEqualsDeclaration with a namespace reference is TypeScript. - if (!isExternalModuleImportEqualsDeclaration(node)) { + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; + return transformFlags & ~TransformFlags.NodeExcludes; +} +/* @internal */ +function computeLabeledStatement(node: LabeledStatement, subtreeFlags: TransformFlags) { + let transformFlags = subtreeFlags; + // A labeled statement containing a block scoped binding *may* need to be transformed from ES6. + if (subtreeFlags & TransformFlags.ContainsBlockScopedBinding + && isIterationStatement(node, /*lookInLabeledStatements*/ true)) { + transformFlags |= TransformFlags.AssertES2015; + } + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; + return transformFlags & ~TransformFlags.NodeExcludes; +} +/* @internal */ +function computeImportEquals(node: ImportEqualsDeclaration, subtreeFlags: TransformFlags) { + let transformFlags = subtreeFlags; + // An ImportEqualsDeclaration with a namespace reference is TypeScript. + if (!isExternalModuleImportEqualsDeclaration(node)) { + transformFlags |= TransformFlags.AssertTypeScript; + } + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; + return transformFlags & ~TransformFlags.NodeExcludes; +} +/* @internal */ +function computeExpressionStatement(node: ExpressionStatement, subtreeFlags: TransformFlags) { + const transformFlags = subtreeFlags; + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; + return transformFlags & ~TransformFlags.NodeExcludes; +} +/* @internal */ +function computeModuleDeclaration(node: ModuleDeclaration, subtreeFlags: TransformFlags) { + let transformFlags = TransformFlags.AssertTypeScript; + const modifierFlags = getModifierFlags(node); + if ((modifierFlags & ModifierFlags.Ambient) === 0) { + transformFlags |= subtreeFlags; + } + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; + return transformFlags & ~TransformFlags.ModuleExcludes; +} +/* @internal */ +function computeVariableDeclarationList(node: VariableDeclarationList, subtreeFlags: TransformFlags) { + let transformFlags = subtreeFlags | TransformFlags.ContainsHoistedDeclarationOrCompletion; + if (subtreeFlags & TransformFlags.ContainsBindingPattern) { + transformFlags |= TransformFlags.AssertES2015; + } + // If a VariableDeclarationList is `let` or `const`, then it is ES6 syntax. + if (node.flags & NodeFlags.BlockScoped) { + transformFlags |= TransformFlags.AssertES2015 | TransformFlags.ContainsBlockScopedBinding; + } + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; + return transformFlags & ~TransformFlags.VariableDeclarationListExcludes; +} +/* @internal */ +function computeOther(node: Node, kind: SyntaxKind, subtreeFlags: TransformFlags) { + // Mark transformations needed for each node + let transformFlags = subtreeFlags; + let excludeFlags = TransformFlags.NodeExcludes; + switch (kind) { + case SyntaxKind.AsyncKeyword: + // async is ES2017 syntax, but may be ES2018 syntax (for async generators) + transformFlags |= TransformFlags.AssertES2018 | TransformFlags.AssertES2017; + break; + case SyntaxKind.AwaitExpression: + // await is ES2017 syntax, but may be ES2018 syntax (for async generators) + transformFlags |= TransformFlags.AssertES2018 | TransformFlags.AssertES2017 | TransformFlags.ContainsAwait; + break; + case SyntaxKind.TypeAssertionExpression: + case SyntaxKind.AsExpression: + case SyntaxKind.PartiallyEmittedExpression: + // These nodes are TypeScript syntax. transformFlags |= TransformFlags.AssertTypeScript; - } - - node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; - return transformFlags & ~TransformFlags.NodeExcludes; - } - - function computeExpressionStatement(node: ExpressionStatement, subtreeFlags: TransformFlags) { - const transformFlags = subtreeFlags; - node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; - return transformFlags & ~TransformFlags.NodeExcludes; - } - - function computeModuleDeclaration(node: ModuleDeclaration, subtreeFlags: TransformFlags) { - let transformFlags = TransformFlags.AssertTypeScript; - const modifierFlags = getModifierFlags(node); - - if ((modifierFlags & ModifierFlags.Ambient) === 0) { - transformFlags |= subtreeFlags; - } - - node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; - return transformFlags & ~TransformFlags.ModuleExcludes; - } - - function computeVariableDeclarationList(node: VariableDeclarationList, subtreeFlags: TransformFlags) { - let transformFlags = subtreeFlags | TransformFlags.ContainsHoistedDeclarationOrCompletion; - - if (subtreeFlags & TransformFlags.ContainsBindingPattern) { - transformFlags |= TransformFlags.AssertES2015; - } - - // If a VariableDeclarationList is `let` or `const`, then it is ES6 syntax. - if (node.flags & NodeFlags.BlockScoped) { - transformFlags |= TransformFlags.AssertES2015 | TransformFlags.ContainsBlockScopedBinding; - } - - node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; - return transformFlags & ~TransformFlags.VariableDeclarationListExcludes; - } - - function computeOther(node: Node, kind: SyntaxKind, subtreeFlags: TransformFlags) { - // Mark transformations needed for each node - let transformFlags = subtreeFlags; - let excludeFlags = TransformFlags.NodeExcludes; - - switch (kind) { - case SyntaxKind.AsyncKeyword: - // async is ES2017 syntax, but may be ES2018 syntax (for async generators) - transformFlags |= TransformFlags.AssertES2018 | TransformFlags.AssertES2017; - break; - case SyntaxKind.AwaitExpression: - // await is ES2017 syntax, but may be ES2018 syntax (for async generators) - transformFlags |= TransformFlags.AssertES2018 | TransformFlags.AssertES2017 | TransformFlags.ContainsAwait; - break; - - case SyntaxKind.TypeAssertionExpression: - case SyntaxKind.AsExpression: - case SyntaxKind.PartiallyEmittedExpression: - // These nodes are TypeScript syntax. - transformFlags |= TransformFlags.AssertTypeScript; - excludeFlags = TransformFlags.OuterExpressionExcludes; - break; - case SyntaxKind.PublicKeyword: - case SyntaxKind.PrivateKeyword: - case SyntaxKind.ProtectedKeyword: - case SyntaxKind.AbstractKeyword: - case SyntaxKind.DeclareKeyword: - case SyntaxKind.ConstKeyword: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.EnumMember: - case SyntaxKind.NonNullExpression: - case SyntaxKind.ReadonlyKeyword: - // These nodes are TypeScript syntax. - transformFlags |= TransformFlags.AssertTypeScript; + excludeFlags = TransformFlags.OuterExpressionExcludes; + break; + case SyntaxKind.PublicKeyword: + case SyntaxKind.PrivateKeyword: + case SyntaxKind.ProtectedKeyword: + case SyntaxKind.AbstractKeyword: + case SyntaxKind.DeclareKeyword: + case SyntaxKind.ConstKeyword: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.EnumMember: + case SyntaxKind.NonNullExpression: + case SyntaxKind.ReadonlyKeyword: + // These nodes are TypeScript syntax. + transformFlags |= TransformFlags.AssertTypeScript; + break; + case SyntaxKind.JsxElement: + case SyntaxKind.JsxSelfClosingElement: + case SyntaxKind.JsxOpeningElement: + case SyntaxKind.JsxText: + case SyntaxKind.JsxClosingElement: + case SyntaxKind.JsxFragment: + case SyntaxKind.JsxOpeningFragment: + case SyntaxKind.JsxClosingFragment: + case SyntaxKind.JsxAttribute: + case SyntaxKind.JsxAttributes: + case SyntaxKind.JsxSpreadAttribute: + case SyntaxKind.JsxExpression: + // These nodes are Jsx syntax. + transformFlags |= TransformFlags.AssertJsx; + break; + case SyntaxKind.NoSubstitutionTemplateLiteral: + case SyntaxKind.TemplateHead: + case SyntaxKind.TemplateMiddle: + case SyntaxKind.TemplateTail: + if ((node).templateFlags) { + transformFlags |= TransformFlags.AssertES2018; break; - - case SyntaxKind.JsxElement: - case SyntaxKind.JsxSelfClosingElement: - case SyntaxKind.JsxOpeningElement: - case SyntaxKind.JsxText: - case SyntaxKind.JsxClosingElement: - case SyntaxKind.JsxFragment: - case SyntaxKind.JsxOpeningFragment: - case SyntaxKind.JsxClosingFragment: - case SyntaxKind.JsxAttribute: - case SyntaxKind.JsxAttributes: - case SyntaxKind.JsxSpreadAttribute: - case SyntaxKind.JsxExpression: - // These nodes are Jsx syntax. - transformFlags |= TransformFlags.AssertJsx; + } + // falls through + case SyntaxKind.TaggedTemplateExpression: + if (hasInvalidEscape((node).template)) { + transformFlags |= TransformFlags.AssertES2018; break; - - case SyntaxKind.NoSubstitutionTemplateLiteral: - case SyntaxKind.TemplateHead: - case SyntaxKind.TemplateMiddle: - case SyntaxKind.TemplateTail: - if ((node).templateFlags) { - transformFlags |= TransformFlags.AssertES2018; - break; - } - // falls through - case SyntaxKind.TaggedTemplateExpression: - if (hasInvalidEscape((node).template)) { - transformFlags |= TransformFlags.AssertES2018; - break; - } - // falls through - case SyntaxKind.TemplateExpression: - case SyntaxKind.ShorthandPropertyAssignment: - case SyntaxKind.StaticKeyword: - case SyntaxKind.MetaProperty: - // These nodes are ES6 syntax. + } + // falls through + case SyntaxKind.TemplateExpression: + case SyntaxKind.ShorthandPropertyAssignment: + case SyntaxKind.StaticKeyword: + case SyntaxKind.MetaProperty: + // These nodes are ES6 syntax. + transformFlags |= TransformFlags.AssertES2015; + break; + case SyntaxKind.StringLiteral: + if ((node).hasExtendedUnicodeEscape) { transformFlags |= TransformFlags.AssertES2015; - break; - - case SyntaxKind.StringLiteral: - if ((node).hasExtendedUnicodeEscape) { - transformFlags |= TransformFlags.AssertES2015; - } - break; - - case SyntaxKind.NumericLiteral: - if ((node).numericLiteralFlags & TokenFlags.BinaryOrOctalSpecifier) { - transformFlags |= TransformFlags.AssertES2015; - } - break; - - case SyntaxKind.BigIntLiteral: - transformFlags |= TransformFlags.AssertESNext; - break; - - case SyntaxKind.ForOfStatement: - // This node is either ES2015 syntax or ES2017 syntax (if it is a for-await-of). - if ((node).awaitModifier) { - transformFlags |= TransformFlags.AssertES2018; - } + } + break; + case SyntaxKind.NumericLiteral: + if ((node).numericLiteralFlags & TokenFlags.BinaryOrOctalSpecifier) { transformFlags |= TransformFlags.AssertES2015; - break; - - case SyntaxKind.YieldExpression: - // This node is either ES2015 syntax (in a generator) or ES2017 syntax (in an async - // generator). - transformFlags |= TransformFlags.AssertES2018 | TransformFlags.AssertES2015 | TransformFlags.ContainsYield; - break; - - case SyntaxKind.AnyKeyword: - case SyntaxKind.NumberKeyword: - case SyntaxKind.BigIntKeyword: - case SyntaxKind.NeverKeyword: - case SyntaxKind.ObjectKeyword: - case SyntaxKind.StringKeyword: - case SyntaxKind.BooleanKeyword: - case SyntaxKind.SymbolKeyword: - case SyntaxKind.VoidKeyword: - case SyntaxKind.TypeParameter: - case SyntaxKind.PropertySignature: - case SyntaxKind.MethodSignature: - case SyntaxKind.CallSignature: - case SyntaxKind.ConstructSignature: - case SyntaxKind.IndexSignature: - case SyntaxKind.TypePredicate: - case SyntaxKind.TypeReference: - case SyntaxKind.FunctionType: - case SyntaxKind.ConstructorType: - case SyntaxKind.TypeQuery: - case SyntaxKind.TypeLiteral: - case SyntaxKind.ArrayType: - case SyntaxKind.TupleType: - case SyntaxKind.OptionalType: - case SyntaxKind.RestType: - case SyntaxKind.UnionType: - case SyntaxKind.IntersectionType: - case SyntaxKind.ConditionalType: - case SyntaxKind.InferType: - case SyntaxKind.ParenthesizedType: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.ThisType: - case SyntaxKind.TypeOperator: - case SyntaxKind.IndexedAccessType: - case SyntaxKind.MappedType: - case SyntaxKind.LiteralType: - case SyntaxKind.NamespaceExportDeclaration: - // Types and signatures are TypeScript syntax, and exclude all other facts. - transformFlags = TransformFlags.AssertTypeScript; - excludeFlags = TransformFlags.TypeExcludes; - break; - - case SyntaxKind.ComputedPropertyName: - // Even though computed property names are ES6, we don't treat them as such. - // This is so that they can flow through PropertyName transforms unaffected. - // Instead, we mark the container as ES6, so that it can properly handle the transform. - transformFlags |= TransformFlags.ContainsComputedPropertyName; - break; - - case SyntaxKind.SpreadElement: - transformFlags |= TransformFlags.AssertES2015 | TransformFlags.ContainsRestOrSpread; - break; - - case SyntaxKind.SpreadAssignment: + } + break; + case SyntaxKind.BigIntLiteral: + transformFlags |= TransformFlags.AssertESNext; + break; + case SyntaxKind.ForOfStatement: + // This node is either ES2015 syntax or ES2017 syntax (if it is a for-await-of). + if ((node).awaitModifier) { + transformFlags |= TransformFlags.AssertES2018; + } + transformFlags |= TransformFlags.AssertES2015; + break; + case SyntaxKind.YieldExpression: + // This node is either ES2015 syntax (in a generator) or ES2017 syntax (in an async + // generator). + transformFlags |= TransformFlags.AssertES2018 | TransformFlags.AssertES2015 | TransformFlags.ContainsYield; + break; + case SyntaxKind.AnyKeyword: + case SyntaxKind.NumberKeyword: + case SyntaxKind.BigIntKeyword: + case SyntaxKind.NeverKeyword: + case SyntaxKind.ObjectKeyword: + case SyntaxKind.StringKeyword: + case SyntaxKind.BooleanKeyword: + case SyntaxKind.SymbolKeyword: + case SyntaxKind.VoidKeyword: + case SyntaxKind.TypeParameter: + case SyntaxKind.PropertySignature: + case SyntaxKind.MethodSignature: + case SyntaxKind.CallSignature: + case SyntaxKind.ConstructSignature: + case SyntaxKind.IndexSignature: + case SyntaxKind.TypePredicate: + case SyntaxKind.TypeReference: + case SyntaxKind.FunctionType: + case SyntaxKind.ConstructorType: + case SyntaxKind.TypeQuery: + case SyntaxKind.TypeLiteral: + case SyntaxKind.ArrayType: + case SyntaxKind.TupleType: + case SyntaxKind.OptionalType: + case SyntaxKind.RestType: + case SyntaxKind.UnionType: + case SyntaxKind.IntersectionType: + case SyntaxKind.ConditionalType: + case SyntaxKind.InferType: + case SyntaxKind.ParenthesizedType: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.ThisType: + case SyntaxKind.TypeOperator: + case SyntaxKind.IndexedAccessType: + case SyntaxKind.MappedType: + case SyntaxKind.LiteralType: + case SyntaxKind.NamespaceExportDeclaration: + // Types and signatures are TypeScript syntax, and exclude all other facts. + transformFlags = TransformFlags.AssertTypeScript; + excludeFlags = TransformFlags.TypeExcludes; + break; + case SyntaxKind.ComputedPropertyName: + // Even though computed property names are ES6, we don't treat them as such. + // This is so that they can flow through PropertyName transforms unaffected. + // Instead, we mark the container as ES6, so that it can properly handle the transform. + transformFlags |= TransformFlags.ContainsComputedPropertyName; + break; + case SyntaxKind.SpreadElement: + transformFlags |= TransformFlags.AssertES2015 | TransformFlags.ContainsRestOrSpread; + break; + case SyntaxKind.SpreadAssignment: + transformFlags |= TransformFlags.AssertES2018 | TransformFlags.ContainsObjectRestOrSpread; + break; + case SyntaxKind.SuperKeyword: + // This node is ES6 syntax. + transformFlags |= TransformFlags.AssertES2015; + excludeFlags = TransformFlags.OuterExpressionExcludes; // must be set to persist `Super` + break; + case SyntaxKind.ThisKeyword: + // Mark this node and its ancestors as containing a lexical `this` keyword. + transformFlags |= TransformFlags.ContainsLexicalThis; + break; + case SyntaxKind.ObjectBindingPattern: + transformFlags |= TransformFlags.AssertES2015 | TransformFlags.ContainsBindingPattern; + if (subtreeFlags & TransformFlags.ContainsRestOrSpread) { transformFlags |= TransformFlags.AssertES2018 | TransformFlags.ContainsObjectRestOrSpread; - break; - - case SyntaxKind.SuperKeyword: - // This node is ES6 syntax. + } + excludeFlags = TransformFlags.BindingPatternExcludes; + break; + case SyntaxKind.ArrayBindingPattern: + transformFlags |= TransformFlags.AssertES2015 | TransformFlags.ContainsBindingPattern; + excludeFlags = TransformFlags.BindingPatternExcludes; + break; + case SyntaxKind.BindingElement: + transformFlags |= TransformFlags.AssertES2015; + if ((node).dotDotDotToken) { + transformFlags |= TransformFlags.ContainsRestOrSpread; + } + break; + case SyntaxKind.Decorator: + // This node is TypeScript syntax, and marks its container as also being TypeScript syntax. + transformFlags |= TransformFlags.AssertTypeScript | TransformFlags.ContainsTypeScriptClassSyntax; + break; + case SyntaxKind.ObjectLiteralExpression: + excludeFlags = TransformFlags.ObjectLiteralExcludes; + if (subtreeFlags & TransformFlags.ContainsComputedPropertyName) { + // If an ObjectLiteralExpression contains a ComputedPropertyName, then it + // is an ES6 node. transformFlags |= TransformFlags.AssertES2015; - excludeFlags = TransformFlags.OuterExpressionExcludes; // must be set to persist `Super` - break; - - case SyntaxKind.ThisKeyword: - // Mark this node and its ancestors as containing a lexical `this` keyword. - transformFlags |= TransformFlags.ContainsLexicalThis; - break; - - case SyntaxKind.ObjectBindingPattern: - transformFlags |= TransformFlags.AssertES2015 | TransformFlags.ContainsBindingPattern; - if (subtreeFlags & TransformFlags.ContainsRestOrSpread) { - transformFlags |= TransformFlags.AssertES2018 | TransformFlags.ContainsObjectRestOrSpread; - } - excludeFlags = TransformFlags.BindingPatternExcludes; - break; - - case SyntaxKind.ArrayBindingPattern: - transformFlags |= TransformFlags.AssertES2015 | TransformFlags.ContainsBindingPattern; - excludeFlags = TransformFlags.BindingPatternExcludes; - break; - - case SyntaxKind.BindingElement: + } + if (subtreeFlags & TransformFlags.ContainsObjectRestOrSpread) { + // If an ObjectLiteralExpression contains a spread element, then it + // is an ES2018 node. + transformFlags |= TransformFlags.AssertES2018; + } + break; + case SyntaxKind.ArrayLiteralExpression: + excludeFlags = TransformFlags.ArrayLiteralOrCallOrNewExcludes; + break; + case SyntaxKind.DoStatement: + case SyntaxKind.WhileStatement: + case SyntaxKind.ForStatement: + case SyntaxKind.ForInStatement: + // A loop containing a block scoped binding *may* need to be transformed from ES6. + if (subtreeFlags & TransformFlags.ContainsBlockScopedBinding) { transformFlags |= TransformFlags.AssertES2015; - if ((node).dotDotDotToken) { - transformFlags |= TransformFlags.ContainsRestOrSpread; - } - break; - - case SyntaxKind.Decorator: - // This node is TypeScript syntax, and marks its container as also being TypeScript syntax. - transformFlags |= TransformFlags.AssertTypeScript | TransformFlags.ContainsTypeScriptClassSyntax; - break; - - case SyntaxKind.ObjectLiteralExpression: - excludeFlags = TransformFlags.ObjectLiteralExcludes; - if (subtreeFlags & TransformFlags.ContainsComputedPropertyName) { - // If an ObjectLiteralExpression contains a ComputedPropertyName, then it - // is an ES6 node. - transformFlags |= TransformFlags.AssertES2015; - } - - if (subtreeFlags & TransformFlags.ContainsObjectRestOrSpread) { - // If an ObjectLiteralExpression contains a spread element, then it - // is an ES2018 node. - transformFlags |= TransformFlags.AssertES2018; - } - - break; - - case SyntaxKind.ArrayLiteralExpression: - excludeFlags = TransformFlags.ArrayLiteralOrCallOrNewExcludes; - break; - - case SyntaxKind.DoStatement: - case SyntaxKind.WhileStatement: - case SyntaxKind.ForStatement: - case SyntaxKind.ForInStatement: - // A loop containing a block scoped binding *may* need to be transformed from ES6. - if (subtreeFlags & TransformFlags.ContainsBlockScopedBinding) { - transformFlags |= TransformFlags.AssertES2015; - } - - break; - - case SyntaxKind.SourceFile: - break; - - case SyntaxKind.NamespaceExport: - transformFlags |= TransformFlags.AssertESNext; - break; - - case SyntaxKind.ReturnStatement: - // Return statements may require an `await` in ES2018. - transformFlags |= TransformFlags.ContainsHoistedDeclarationOrCompletion | TransformFlags.AssertES2018; - break; - - case SyntaxKind.ContinueStatement: - case SyntaxKind.BreakStatement: - transformFlags |= TransformFlags.ContainsHoistedDeclarationOrCompletion; - break; - - case SyntaxKind.PrivateIdentifier: - transformFlags |= TransformFlags.ContainsClassFields; - break; - } - - node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; - return transformFlags & ~excludeFlags; + } + break; + case SyntaxKind.SourceFile: + break; + case SyntaxKind.NamespaceExport: + transformFlags |= TransformFlags.AssertESNext; + break; + case SyntaxKind.ReturnStatement: + // Return statements may require an `await` in ES2018. + transformFlags |= TransformFlags.ContainsHoistedDeclarationOrCompletion | TransformFlags.AssertES2018; + break; + case SyntaxKind.ContinueStatement: + case SyntaxKind.BreakStatement: + transformFlags |= TransformFlags.ContainsHoistedDeclarationOrCompletion; + break; + case SyntaxKind.PrivateIdentifier: + transformFlags |= TransformFlags.ContainsClassFields; + break; } - - function propagatePropertyNameFlags(node: PropertyName, transformFlags: TransformFlags) { - return transformFlags | (node.transformFlags & TransformFlags.PropertyNamePropagatingFlags); + node.transformFlags = transformFlags | TransformFlags.HasComputedFlags; + return transformFlags & ~excludeFlags; +} +/* @internal */ +function propagatePropertyNameFlags(node: PropertyName, transformFlags: TransformFlags) { + return transformFlags | (node.transformFlags & TransformFlags.PropertyNamePropagatingFlags); +} +/** + * Gets the transform flags to exclude when unioning the transform flags of a subtree. + * + * NOTE: This needs to be kept up-to-date with the exclusions used in `computeTransformFlagsForNode`. + * For performance reasons, `computeTransformFlagsForNode` uses local constant values rather + * than calling this function. + */ +/* @internal */ +export function getTransformFlagsSubtreeExclusions(kind: SyntaxKind) { + if (kind >= SyntaxKind.FirstTypeNode && kind <= SyntaxKind.LastTypeNode) { + return TransformFlags.TypeExcludes; } - - /** - * Gets the transform flags to exclude when unioning the transform flags of a subtree. - * - * NOTE: This needs to be kept up-to-date with the exclusions used in `computeTransformFlagsForNode`. - * For performance reasons, `computeTransformFlagsForNode` uses local constant values rather - * than calling this function. - */ - export function getTransformFlagsSubtreeExclusions(kind: SyntaxKind) { - if (kind >= SyntaxKind.FirstTypeNode && kind <= SyntaxKind.LastTypeNode) { + switch (kind) { + case SyntaxKind.CallExpression: + case SyntaxKind.NewExpression: + case SyntaxKind.ArrayLiteralExpression: + return TransformFlags.ArrayLiteralOrCallOrNewExcludes; + case SyntaxKind.ModuleDeclaration: + return TransformFlags.ModuleExcludes; + case SyntaxKind.Parameter: + return TransformFlags.ParameterExcludes; + case SyntaxKind.ArrowFunction: + return TransformFlags.ArrowFunctionExcludes; + case SyntaxKind.FunctionExpression: + case SyntaxKind.FunctionDeclaration: + return TransformFlags.FunctionExcludes; + case SyntaxKind.VariableDeclarationList: + return TransformFlags.VariableDeclarationListExcludes; + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ClassExpression: + return TransformFlags.ClassExcludes; + case SyntaxKind.Constructor: + return TransformFlags.ConstructorExcludes; + case SyntaxKind.MethodDeclaration: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + return TransformFlags.MethodOrAccessorExcludes; + case SyntaxKind.AnyKeyword: + case SyntaxKind.NumberKeyword: + case SyntaxKind.BigIntKeyword: + case SyntaxKind.NeverKeyword: + case SyntaxKind.StringKeyword: + case SyntaxKind.ObjectKeyword: + case SyntaxKind.BooleanKeyword: + case SyntaxKind.SymbolKeyword: + case SyntaxKind.VoidKeyword: + case SyntaxKind.TypeParameter: + case SyntaxKind.PropertySignature: + case SyntaxKind.MethodSignature: + case SyntaxKind.CallSignature: + case SyntaxKind.ConstructSignature: + case SyntaxKind.IndexSignature: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.TypeAliasDeclaration: return TransformFlags.TypeExcludes; - } - - switch (kind) { - case SyntaxKind.CallExpression: - case SyntaxKind.NewExpression: - case SyntaxKind.ArrayLiteralExpression: - return TransformFlags.ArrayLiteralOrCallOrNewExcludes; - case SyntaxKind.ModuleDeclaration: - return TransformFlags.ModuleExcludes; - case SyntaxKind.Parameter: - return TransformFlags.ParameterExcludes; - case SyntaxKind.ArrowFunction: - return TransformFlags.ArrowFunctionExcludes; - case SyntaxKind.FunctionExpression: - case SyntaxKind.FunctionDeclaration: - return TransformFlags.FunctionExcludes; - case SyntaxKind.VariableDeclarationList: - return TransformFlags.VariableDeclarationListExcludes; - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ClassExpression: - return TransformFlags.ClassExcludes; - case SyntaxKind.Constructor: - return TransformFlags.ConstructorExcludes; - case SyntaxKind.MethodDeclaration: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - return TransformFlags.MethodOrAccessorExcludes; - case SyntaxKind.AnyKeyword: - case SyntaxKind.NumberKeyword: - case SyntaxKind.BigIntKeyword: - case SyntaxKind.NeverKeyword: - case SyntaxKind.StringKeyword: - case SyntaxKind.ObjectKeyword: - case SyntaxKind.BooleanKeyword: - case SyntaxKind.SymbolKeyword: - case SyntaxKind.VoidKeyword: - case SyntaxKind.TypeParameter: - case SyntaxKind.PropertySignature: - case SyntaxKind.MethodSignature: - case SyntaxKind.CallSignature: - case SyntaxKind.ConstructSignature: - case SyntaxKind.IndexSignature: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.TypeAliasDeclaration: - return TransformFlags.TypeExcludes; - case SyntaxKind.ObjectLiteralExpression: - return TransformFlags.ObjectLiteralExcludes; - case SyntaxKind.CatchClause: - return TransformFlags.CatchClauseExcludes; - case SyntaxKind.ObjectBindingPattern: - case SyntaxKind.ArrayBindingPattern: - return TransformFlags.BindingPatternExcludes; - case SyntaxKind.TypeAssertionExpression: - case SyntaxKind.AsExpression: - case SyntaxKind.PartiallyEmittedExpression: - case SyntaxKind.ParenthesizedExpression: - case SyntaxKind.SuperKeyword: - return TransformFlags.OuterExpressionExcludes; - case SyntaxKind.PropertyAccessExpression: - case SyntaxKind.ElementAccessExpression: - return TransformFlags.PropertyAccessExcludes; - default: - return TransformFlags.NodeExcludes; - } - } - - /** - * "Binds" JSDoc nodes in TypeScript code. - * Since we will never create symbols for JSDoc, we just set parent pointers instead. - */ - function setParentPointers(parent: Node, child: Node): void { - child.parent = parent; - forEachChild(child, grandchild => setParentPointers(child, grandchild)); + case SyntaxKind.ObjectLiteralExpression: + return TransformFlags.ObjectLiteralExcludes; + case SyntaxKind.CatchClause: + return TransformFlags.CatchClauseExcludes; + case SyntaxKind.ObjectBindingPattern: + case SyntaxKind.ArrayBindingPattern: + return TransformFlags.BindingPatternExcludes; + case SyntaxKind.TypeAssertionExpression: + case SyntaxKind.AsExpression: + case SyntaxKind.PartiallyEmittedExpression: + case SyntaxKind.ParenthesizedExpression: + case SyntaxKind.SuperKeyword: + return TransformFlags.OuterExpressionExcludes; + case SyntaxKind.PropertyAccessExpression: + case SyntaxKind.ElementAccessExpression: + return TransformFlags.PropertyAccessExcludes; + default: + return TransformFlags.NodeExcludes; } } +/** + * "Binds" JSDoc nodes in TypeScript code. + * Since we will never create symbols for JSDoc, we just set parent pointers instead. + */ +/* @internal */ +function setParentPointers(parent: Node, child: Node): void { + child.parent = parent; + forEachChild(child, grandchild => setParentPointers(child, grandchild)); +} diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index d79ca0447a8f4..9b6786a6b67f4 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -1,1199 +1,1093 @@ +import { DiagnosticCategory, DiagnosticMessageChain, ReusableBuilderState, Diagnostic, SourceFile, Path, BuilderState, Program, CompilerOptions, forEachKey, GetCanonicalFileName, createMap, compilerOptionsAffectSemanticDiagnostics, Debug, copyEntries, cloneMapOrUndefined, compilerOptionsAffectEmit, emptyArray, getDirectoryPath, getNormalizedAbsolutePath, getTsBuildInfoEmitOutputFilePath, DiagnosticRelatedInformation, cloneMap, CancellationToken, forEach, skipTypeChecking, getEmitDeclarations, forEachEntry, addToSeen, AffectedFileResult, EmitResult, concatenate, MapLike, arrayFrom, ensurePathIsNonModuleName, getRelativePathFromDirectory, getOptionsNameMap, hasProperty, CompilerOptionsValue, CommandLineOption, BuilderProgramHost, BuilderProgram, CompilerHost, ProjectReference, isArray, createProgram, SemanticDiagnosticsBuilderProgram, EmitAndSemanticDiagnosticsBuilderProgram, createGetCanonicalFileName, generateDjb2Hash, notImplemented, WriteFileCallback, CustomTransformers, maybeBind, handleNoEmitOptions, SourceMapEmitResult, addRange, arrayToSet, ReadBuildProgramHost, convertToOptionsWithAbsolutePaths, arrayToMap, isString, noop, returnUndefined } from "./ts"; +import * as ts from "./ts"; /*@internal*/ -namespace ts { - export interface ReusableDiagnostic extends ReusableDiagnosticRelatedInformation { - /** May store more in future. For now, this will simply be `true` to indicate when a diagnostic is an unused-identifier diagnostic. */ - reportsUnnecessary?: {}; - source?: string; - relatedInformation?: ReusableDiagnosticRelatedInformation[]; - } - - export interface ReusableDiagnosticRelatedInformation { - category: DiagnosticCategory; - code: number; - file: string | undefined; - start: number | undefined; - length: number | undefined; - messageText: string | ReusableDiagnosticMessageChain; - } - - export type ReusableDiagnosticMessageChain = DiagnosticMessageChain; - - export interface ReusableBuilderProgramState extends ReusableBuilderState { - /** - * Cache of bind and check diagnostics for files with their Path being the key - */ - semanticDiagnosticsPerFile?: ReadonlyMap | undefined; - /** - * The map has key by source file's path that has been changed - */ - changedFilesSet?: ReadonlyMap; - /** - * Set of affected files being iterated - */ - affectedFiles?: readonly SourceFile[] | undefined; - /** - * Current changed file for iterating over affected files - */ - currentChangedFilePath?: Path | undefined; - /** - * Map of file signatures, with key being file path, calculated while getting current changed file's affected files - * These will be committed whenever the iteration through affected files of current changed file is complete - */ - currentAffectedFilesSignatures?: ReadonlyMap | undefined; - /** - * Newly computed visible to outside referencedSet - */ - currentAffectedFilesExportedModulesMap?: Readonly | undefined; - /** - * True if the semantic diagnostics were copied from the old state - */ - semanticDiagnosticsFromOldState?: Map; - /** - * program corresponding to this state - */ - program?: Program | undefined; - /** - * compilerOptions for the program - */ - compilerOptions: CompilerOptions; - /** - * Files pending to be emitted - */ - affectedFilesPendingEmit?: readonly Path[] | undefined; - /** - * Files pending to be emitted kind. - */ - affectedFilesPendingEmitKind?: ReadonlyMap | undefined; - /** - * Current index to retrieve pending affected file - */ - affectedFilesPendingEmitIndex?: number | undefined; - /* - * true if semantic diagnostics are ReusableDiagnostic instead of Diagnostic - */ - hasReusableDiagnostic?: true; - } - - export const enum BuilderFileEmit { - DtsOnly, - Full - } - +export interface ReusableDiagnostic extends ReusableDiagnosticRelatedInformation { + /** May store more in future. For now, this will simply be `true` to indicate when a diagnostic is an unused-identifier diagnostic. */ + reportsUnnecessary?: {}; + source?: string; + relatedInformation?: ReusableDiagnosticRelatedInformation[]; +} +/* @internal */ +export interface ReusableDiagnosticRelatedInformation { + category: DiagnosticCategory; + code: number; + file: string | undefined; + start: number | undefined; + length: number | undefined; + messageText: string | ReusableDiagnosticMessageChain; +} +/* @internal */ +export type ReusableDiagnosticMessageChain = DiagnosticMessageChain; +/* @internal */ +export interface ReusableBuilderProgramState extends ReusableBuilderState { /** - * State to store the changed files, affected files and cache semantic diagnostics + * Cache of bind and check diagnostics for files with their Path being the key */ - // TODO: GH#18217 Properties of this interface are frequently asserted to be defined. - export interface BuilderProgramState extends BuilderState { - /** - * Cache of bind and check diagnostics for files with their Path being the key - */ - semanticDiagnosticsPerFile: Map | undefined; - /** - * The map has key by source file's path that has been changed - */ - changedFilesSet: Map; - /** - * Set of affected files being iterated - */ - affectedFiles: readonly SourceFile[] | undefined; - /** - * Current index to retrieve affected file from - */ - affectedFilesIndex: number | undefined; - /** - * Current changed file for iterating over affected files - */ - currentChangedFilePath: Path | undefined; - /** - * Map of file signatures, with key being file path, calculated while getting current changed file's affected files - * These will be committed whenever the iteration through affected files of current changed file is complete - */ - currentAffectedFilesSignatures: Map | undefined; - /** - * Newly computed visible to outside referencedSet - */ - currentAffectedFilesExportedModulesMap: BuilderState.ComputingExportedModulesMap | undefined; - /** - * Already seen affected files - */ - seenAffectedFiles: Map | undefined; - /** - * whether this program has cleaned semantic diagnostics cache for lib files - */ - cleanedDiagnosticsOfLibFiles?: boolean; - /** - * True if the semantic diagnostics were copied from the old state - */ - semanticDiagnosticsFromOldState?: Map; - /** - * program corresponding to this state - */ - program: Program | undefined; - /** - * compilerOptions for the program - */ - compilerOptions: CompilerOptions; - /** - * Files pending to be emitted - */ - affectedFilesPendingEmit: Path[] | undefined; - /** - * Files pending to be emitted kind. - */ - affectedFilesPendingEmitKind: Map | undefined; - /** - * Current index to retrieve pending affected file - */ - affectedFilesPendingEmitIndex: number | undefined; - /** - * true if build info is emitted - */ - emittedBuildInfo?: boolean; - /** - * Already seen emitted files - */ - seenEmittedFiles: Map | undefined; - /** - * true if program has been emitted - */ - programEmitComplete?: true; - } - - function hasSameKeys(map1: ReadonlyMap | undefined, map2: ReadonlyMap | undefined): boolean { - // Has same size and every key is present in both maps - return map1 as ReadonlyMap === map2 || map1 !== undefined && map2 !== undefined && map1.size === map2.size && !forEachKey(map1, key => !map2.has(key)); - } - + semanticDiagnosticsPerFile?: ts.ReadonlyMap | undefined; /** - * Create the state so that we can iterate on changedFiles/affected files + * The map has key by source file's path that has been changed */ - function createBuilderProgramState(newProgram: Program, getCanonicalFileName: GetCanonicalFileName, oldState?: Readonly): BuilderProgramState { - const state = BuilderState.create(newProgram, getCanonicalFileName, oldState) as BuilderProgramState; - state.program = newProgram; - const compilerOptions = newProgram.getCompilerOptions(); - state.compilerOptions = compilerOptions; - // With --out or --outFile, any change affects all semantic diagnostics so no need to cache them - if (!compilerOptions.outFile && !compilerOptions.out) { - state.semanticDiagnosticsPerFile = createMap(); - } - state.changedFilesSet = createMap(); - - const useOldState = BuilderState.canReuseOldState(state.referencedMap, oldState); - const oldCompilerOptions = useOldState ? oldState!.compilerOptions : undefined; - const canCopySemanticDiagnostics = useOldState && oldState!.semanticDiagnosticsPerFile && !!state.semanticDiagnosticsPerFile && - !compilerOptionsAffectSemanticDiagnostics(compilerOptions, oldCompilerOptions!); - if (useOldState) { - // Verify the sanity of old state - if (!oldState!.currentChangedFilePath) { - const affectedSignatures = oldState!.currentAffectedFilesSignatures; - Debug.assert(!oldState!.affectedFiles && (!affectedSignatures || !affectedSignatures.size), "Cannot reuse if only few affected files of currentChangedFile were iterated"); - } - const changedFilesSet = oldState!.changedFilesSet; - if (canCopySemanticDiagnostics) { - Debug.assert(!changedFilesSet || !forEachKey(changedFilesSet, path => oldState!.semanticDiagnosticsPerFile!.has(path)), "Semantic diagnostics shouldnt be available for changed files"); - } - - // Copy old state's changed files set - if (changedFilesSet) { - copyEntries(changedFilesSet, state.changedFilesSet); - } - if (!compilerOptions.outFile && !compilerOptions.out && oldState!.affectedFilesPendingEmit) { - state.affectedFilesPendingEmit = oldState!.affectedFilesPendingEmit.slice(); - state.affectedFilesPendingEmitKind = cloneMapOrUndefined(oldState!.affectedFilesPendingEmitKind); - state.affectedFilesPendingEmitIndex = oldState!.affectedFilesPendingEmitIndex; - } - } - - // Update changed files and copy semantic diagnostics if we can - const referencedMap = state.referencedMap; - const oldReferencedMap = useOldState ? oldState!.referencedMap : undefined; - const copyDeclarationFileDiagnostics = canCopySemanticDiagnostics && !compilerOptions.skipLibCheck === !oldCompilerOptions!.skipLibCheck; - const copyLibFileDiagnostics = copyDeclarationFileDiagnostics && !compilerOptions.skipDefaultLibCheck === !oldCompilerOptions!.skipDefaultLibCheck; - state.fileInfos.forEach((info, sourceFilePath) => { - let oldInfo: Readonly | undefined; - let newReferences: BuilderState.ReferencedSet | undefined; - - // if not using old state, every file is changed - if (!useOldState || - // File wasnt present in old state - !(oldInfo = oldState!.fileInfos.get(sourceFilePath)) || - // versions dont match - oldInfo.version !== info.version || - // Referenced files changed - !hasSameKeys(newReferences = referencedMap && referencedMap.get(sourceFilePath), oldReferencedMap && oldReferencedMap.get(sourceFilePath)) || - // Referenced file was deleted in the new program - newReferences && forEachKey(newReferences, path => !state.fileInfos.has(path) && oldState!.fileInfos.has(path))) { - // Register file as changed file and do not copy semantic diagnostics, since all changed files need to be re-evaluated - state.changedFilesSet.set(sourceFilePath, true); - } - else if (canCopySemanticDiagnostics) { - const sourceFile = newProgram.getSourceFileByPath(sourceFilePath as Path)!; - - if (sourceFile.isDeclarationFile && !copyDeclarationFileDiagnostics) { return; } - if (sourceFile.hasNoDefaultLib && !copyLibFileDiagnostics) { return; } - - // Unchanged file copy diagnostics - const diagnostics = oldState!.semanticDiagnosticsPerFile!.get(sourceFilePath); - if (diagnostics) { - state.semanticDiagnosticsPerFile!.set(sourceFilePath, oldState!.hasReusableDiagnostic ? convertToDiagnostics(diagnostics as readonly ReusableDiagnostic[], newProgram, getCanonicalFileName) : diagnostics as readonly Diagnostic[]); - if (!state.semanticDiagnosticsFromOldState) { - state.semanticDiagnosticsFromOldState = createMap(); - } - state.semanticDiagnosticsFromOldState.set(sourceFilePath, true); - } - } - }); - - if (oldCompilerOptions && compilerOptionsAffectEmit(compilerOptions, oldCompilerOptions)) { - // Add all files to affectedFilesPendingEmit since emit changed - newProgram.getSourceFiles().forEach(f => addToAffectedFilesPendingEmit(state, f.resolvedPath, BuilderFileEmit.Full)); - Debug.assert(state.seenAffectedFiles === undefined); - state.seenAffectedFiles = createMap(); - } - - state.emittedBuildInfo = !state.changedFilesSet.size && !state.affectedFilesPendingEmit; - return state; - } - - function convertToDiagnostics(diagnostics: readonly ReusableDiagnostic[], newProgram: Program, getCanonicalFileName: GetCanonicalFileName): readonly Diagnostic[] { - if (!diagnostics.length) return emptyArray; - const buildInfoDirectory = getDirectoryPath(getNormalizedAbsolutePath(getTsBuildInfoEmitOutputFilePath(newProgram.getCompilerOptions())!, newProgram.getCurrentDirectory())); - return diagnostics.map(diagnostic => { - const result: Diagnostic = convertToDiagnosticRelatedInformation(diagnostic, newProgram, toPath); - result.reportsUnnecessary = diagnostic.reportsUnnecessary; - result.source = diagnostic.source; - const { relatedInformation } = diagnostic; - result.relatedInformation = relatedInformation ? - relatedInformation.length ? - relatedInformation.map(r => convertToDiagnosticRelatedInformation(r, newProgram, toPath)) : - emptyArray : - undefined; - return result; - }); - - function toPath(path: string) { - return ts.toPath(path, buildInfoDirectory, getCanonicalFileName); - } - } - - function convertToDiagnosticRelatedInformation(diagnostic: ReusableDiagnosticRelatedInformation, newProgram: Program, toPath: (path: string) => Path): DiagnosticRelatedInformation { - const { file } = diagnostic; - return { - ...diagnostic, - file: file ? newProgram.getSourceFileByPath(toPath(file)) : undefined - }; - } - + changedFilesSet?: ts.ReadonlyMap; /** - * Releases program and other related not needed properties + * Set of affected files being iterated */ - function releaseCache(state: BuilderProgramState) { - BuilderState.releaseCache(state); - state.program = undefined; - } - + affectedFiles?: readonly SourceFile[] | undefined; /** - * Creates a clone of the state + * Current changed file for iterating over affected files */ - function cloneBuilderProgramState(state: Readonly): BuilderProgramState { - const newState = BuilderState.clone(state) as BuilderProgramState; - newState.semanticDiagnosticsPerFile = cloneMapOrUndefined(state.semanticDiagnosticsPerFile); - newState.changedFilesSet = cloneMap(state.changedFilesSet); - newState.affectedFiles = state.affectedFiles; - newState.affectedFilesIndex = state.affectedFilesIndex; - newState.currentChangedFilePath = state.currentChangedFilePath; - newState.currentAffectedFilesSignatures = cloneMapOrUndefined(state.currentAffectedFilesSignatures); - newState.currentAffectedFilesExportedModulesMap = cloneMapOrUndefined(state.currentAffectedFilesExportedModulesMap); - newState.seenAffectedFiles = cloneMapOrUndefined(state.seenAffectedFiles); - newState.cleanedDiagnosticsOfLibFiles = state.cleanedDiagnosticsOfLibFiles; - newState.semanticDiagnosticsFromOldState = cloneMapOrUndefined(state.semanticDiagnosticsFromOldState); - newState.program = state.program; - newState.compilerOptions = state.compilerOptions; - newState.affectedFilesPendingEmit = state.affectedFilesPendingEmit && state.affectedFilesPendingEmit.slice(); - newState.affectedFilesPendingEmitKind = cloneMapOrUndefined(state.affectedFilesPendingEmitKind); - newState.affectedFilesPendingEmitIndex = state.affectedFilesPendingEmitIndex; - newState.seenEmittedFiles = cloneMapOrUndefined(state.seenEmittedFiles); - newState.programEmitComplete = state.programEmitComplete; - return newState; - } - + currentChangedFilePath?: Path | undefined; /** - * Verifies that source file is ok to be used in calls that arent handled by next + * Map of file signatures, with key being file path, calculated while getting current changed file's affected files + * These will be committed whenever the iteration through affected files of current changed file is complete */ - function assertSourceFileOkWithoutNextAffectedCall(state: BuilderProgramState, sourceFile: SourceFile | undefined) { - Debug.assert(!sourceFile || !state.affectedFiles || state.affectedFiles[state.affectedFilesIndex! - 1] !== sourceFile || !state.semanticDiagnosticsPerFile!.has(sourceFile.resolvedPath)); - } - + currentAffectedFilesSignatures?: ts.ReadonlyMap | undefined; /** - * This function returns the next affected file to be processed. - * Note that until doneAffected is called it would keep reporting same result - * This is to allow the callers to be able to actually remove affected file only when the operation is complete - * eg. if during diagnostics check cancellation token ends up cancelling the request, the affected file should be retained + * Newly computed visible to outside referencedSet */ - function getNextAffectedFile(state: BuilderProgramState, cancellationToken: CancellationToken | undefined, computeHash: BuilderState.ComputeHash): SourceFile | Program | undefined { - while (true) { - const { affectedFiles } = state; - if (affectedFiles) { - const seenAffectedFiles = state.seenAffectedFiles!; - let affectedFilesIndex = state.affectedFilesIndex!; // TODO: GH#18217 - while (affectedFilesIndex < affectedFiles.length) { - const affectedFile = affectedFiles[affectedFilesIndex]; - if (!seenAffectedFiles.has(affectedFile.resolvedPath)) { - // Set the next affected file as seen and remove the cached semantic diagnostics - state.affectedFilesIndex = affectedFilesIndex; - handleDtsMayChangeOfAffectedFile(state, affectedFile, cancellationToken, computeHash); - return affectedFile; - } - affectedFilesIndex++; - } - - // Remove the changed file from the change set - state.changedFilesSet.delete(state.currentChangedFilePath!); - state.currentChangedFilePath = undefined; - // Commit the changes in file signature - BuilderState.updateSignaturesFromCache(state, state.currentAffectedFilesSignatures!); - state.currentAffectedFilesSignatures!.clear(); - BuilderState.updateExportedFilesMapFromCache(state, state.currentAffectedFilesExportedModulesMap); - state.affectedFiles = undefined; - } - - // Get next changed file - const nextKey = state.changedFilesSet.keys().next(); - if (nextKey.done) { - // Done - return undefined; - } - - // With --out or --outFile all outputs go into single file - // so operations are performed directly on program, return program - const program = Debug.checkDefined(state.program); - const compilerOptions = program.getCompilerOptions(); - if (compilerOptions.outFile || compilerOptions.out) { - Debug.assert(!state.semanticDiagnosticsPerFile); - return program; - } - - // Get next batch of affected files - state.currentAffectedFilesSignatures = state.currentAffectedFilesSignatures || createMap(); - if (state.exportedModulesMap) { - state.currentAffectedFilesExportedModulesMap = state.currentAffectedFilesExportedModulesMap || createMap(); - } - state.affectedFiles = BuilderState.getFilesAffectedBy(state, program, nextKey.value as Path, cancellationToken, computeHash, state.currentAffectedFilesSignatures, state.currentAffectedFilesExportedModulesMap); - state.currentChangedFilePath = nextKey.value as Path; - state.affectedFilesIndex = 0; - state.seenAffectedFiles = state.seenAffectedFiles || createMap(); - } - } - + currentAffectedFilesExportedModulesMap?: Readonly | undefined; /** - * Returns next file to be emitted from files that retrieved semantic diagnostics but did not emit yet + * True if the semantic diagnostics were copied from the old state */ - function getNextAffectedFilePendingEmit(state: BuilderProgramState) { - const { affectedFilesPendingEmit } = state; - if (affectedFilesPendingEmit) { - const seenEmittedFiles = state.seenEmittedFiles || (state.seenEmittedFiles = createMap()); - for (let i = state.affectedFilesPendingEmitIndex!; i < affectedFilesPendingEmit.length; i++) { - const affectedFile = Debug.checkDefined(state.program).getSourceFileByPath(affectedFilesPendingEmit[i]); - if (affectedFile) { - const seenKind = seenEmittedFiles.get(affectedFile.resolvedPath); - const emitKind = Debug.checkDefined(Debug.checkDefined(state.affectedFilesPendingEmitKind).get(affectedFile.resolvedPath)); - if (seenKind === undefined || seenKind < emitKind) { - // emit this file - state.affectedFilesPendingEmitIndex = i; - return { affectedFile, emitKind }; - } - } - } - state.affectedFilesPendingEmit = undefined; - state.affectedFilesPendingEmitKind = undefined; - state.affectedFilesPendingEmitIndex = undefined; - } - return undefined; - } - + semanticDiagnosticsFromOldState?: ts.Map; /** - * Handles semantic diagnostics and dts emit for affectedFile and files, that are referencing modules that export entities from affected file - * This is because even though js emit doesnt change, dts emit / type used can change resulting in need for dts emit and js change + * program corresponding to this state */ - function handleDtsMayChangeOfAffectedFile(state: BuilderProgramState, affectedFile: SourceFile, cancellationToken: CancellationToken | undefined, computeHash: BuilderState.ComputeHash) { - removeSemanticDiagnosticsOf(state, affectedFile.resolvedPath); - - // If affected files is everything except default library, then nothing more to do - if (state.allFilesExcludingDefaultLibraryFile === state.affectedFiles) { - if (!state.cleanedDiagnosticsOfLibFiles) { - state.cleanedDiagnosticsOfLibFiles = true; - const program = Debug.checkDefined(state.program); - const options = program.getCompilerOptions(); - forEach(program.getSourceFiles(), f => - program.isSourceFileDefaultLibrary(f) && - !skipTypeChecking(f, options, program) && - removeSemanticDiagnosticsOf(state, f.resolvedPath) - ); - } - return; - } - - if (!state.compilerOptions.assumeChangesOnlyAffectDirectDependencies) { - forEachReferencingModulesOfExportOfAffectedFile(state, affectedFile, (state, path) => handleDtsMayChangeOf(state, path, cancellationToken, computeHash)); - } - } - + program?: Program | undefined; /** - * Handle the dts may change, so they need to be added to pending emit if dts emit is enabled, - * Also we need to make sure signature is updated for these files + * compilerOptions for the program */ - function handleDtsMayChangeOf(state: BuilderProgramState, path: Path, cancellationToken: CancellationToken | undefined, computeHash: BuilderState.ComputeHash) { - removeSemanticDiagnosticsOf(state, path); - - if (!state.changedFilesSet.has(path)) { - const program = Debug.checkDefined(state.program); - const sourceFile = program.getSourceFileByPath(path); - if (sourceFile) { - // Even though the js emit doesnt change and we are already handling dts emit and semantic diagnostics - // we need to update the signature to reflect correctness of the signature(which is output d.ts emit) of this file - // This ensures that we dont later during incremental builds considering wrong signature. - // Eg where this also is needed to ensure that .tsbuildinfo generated by incremental build should be same as if it was first fresh build - BuilderState.updateShapeSignature( - state, - program, - sourceFile, - Debug.checkDefined(state.currentAffectedFilesSignatures), - cancellationToken, - computeHash, - state.currentAffectedFilesExportedModulesMap - ); - // If not dts emit, nothing more to do - if (getEmitDeclarations(state.compilerOptions)) { - addToAffectedFilesPendingEmit(state, path, BuilderFileEmit.DtsOnly); - } - } - } - - return false; - } - + compilerOptions: CompilerOptions; /** - * Removes semantic diagnostics for path and - * returns true if there are no more semantic diagnostics from the old state + * Files pending to be emitted */ - function removeSemanticDiagnosticsOf(state: BuilderProgramState, path: Path) { - if (!state.semanticDiagnosticsFromOldState) { - return true; - } - state.semanticDiagnosticsFromOldState.delete(path); - state.semanticDiagnosticsPerFile!.delete(path); - return !state.semanticDiagnosticsFromOldState.size; - } - - function isChangedSignagure(state: BuilderProgramState, path: Path) { - const newSignature = Debug.checkDefined(state.currentAffectedFilesSignatures).get(path); - const oldSignagure = Debug.checkDefined(state.fileInfos.get(path)).signature; - return newSignature !== oldSignagure; - } - + affectedFilesPendingEmit?: readonly Path[] | undefined; /** - * Iterate on referencing modules that export entities from affected file + * Files pending to be emitted kind. */ - function forEachReferencingModulesOfExportOfAffectedFile(state: BuilderProgramState, affectedFile: SourceFile, fn: (state: BuilderProgramState, filePath: Path) => boolean) { - // If there was change in signature (dts output) for the changed file, - // then only we need to handle pending file emit - if (!state.exportedModulesMap || !state.changedFilesSet.has(affectedFile.resolvedPath)) { - return; - } - - if (!isChangedSignagure(state, affectedFile.resolvedPath)) return; - - // Since isolated modules dont change js files, files affected by change in signature is itself - // But we need to cleanup semantic diagnostics and queue dts emit for affected files - if (state.compilerOptions.isolatedModules) { - const seenFileNamesMap = createMap(); - seenFileNamesMap.set(affectedFile.resolvedPath, true); - const queue = BuilderState.getReferencedByPaths(state, affectedFile.resolvedPath); - while (queue.length > 0) { - const currentPath = queue.pop()!; - if (!seenFileNamesMap.has(currentPath)) { - seenFileNamesMap.set(currentPath, true); - const result = fn(state, currentPath); - if (result && isChangedSignagure(state, currentPath)) { - const currentSourceFile = Debug.checkDefined(state.program).getSourceFileByPath(currentPath)!; - queue.push(...BuilderState.getReferencedByPaths(state, currentSourceFile.resolvedPath)); - } - } - } - } - - Debug.assert(!!state.currentAffectedFilesExportedModulesMap); - const seenFileAndExportsOfFile = createMap(); - // Go through exported modules from cache first - // If exported modules has path, all files referencing file exported from are affected - if (forEachEntry(state.currentAffectedFilesExportedModulesMap, (exportedModules, exportedFromPath) => - exportedModules && - exportedModules.has(affectedFile.resolvedPath) && - forEachFilesReferencingPath(state, exportedFromPath as Path, seenFileAndExportsOfFile, fn) - )) { - return; - } - - // If exported from path is not from cache and exported modules has path, all files referencing file exported from are affected - forEachEntry(state.exportedModulesMap, (exportedModules, exportedFromPath) => - !state.currentAffectedFilesExportedModulesMap!.has(exportedFromPath) && // If we already iterated this through cache, ignore it - exportedModules.has(affectedFile.resolvedPath) && - forEachFilesReferencingPath(state, exportedFromPath as Path, seenFileAndExportsOfFile, fn) - ); - } - + affectedFilesPendingEmitKind?: ts.ReadonlyMap | undefined; /** - * Iterate on files referencing referencedPath + * Current index to retrieve pending affected file */ - function forEachFilesReferencingPath(state: BuilderProgramState, referencedPath: Path, seenFileAndExportsOfFile: Map, fn: (state: BuilderProgramState, filePath: Path) => boolean) { - return forEachEntry(state.referencedMap!, (referencesInFile, filePath) => - referencesInFile.has(referencedPath) && forEachFileAndExportsOfFile(state, filePath as Path, seenFileAndExportsOfFile, fn) - ); - } - + affectedFilesPendingEmitIndex?: number | undefined; + /* + * true if semantic diagnostics are ReusableDiagnostic instead of Diagnostic + */ + hasReusableDiagnostic?: true; +} +/* @internal */ +export const enum BuilderFileEmit { + DtsOnly, + Full +} +/** + * State to store the changed files, affected files and cache semantic diagnostics + */ +// TODO: GH#18217 Properties of this interface are frequently asserted to be defined. +/* @internal */ +export interface BuilderProgramState extends BuilderState { + /** + * Cache of bind and check diagnostics for files with their Path being the key + */ + semanticDiagnosticsPerFile: ts.Map | undefined; + /** + * The map has key by source file's path that has been changed + */ + changedFilesSet: ts.Map; + /** + * Set of affected files being iterated + */ + affectedFiles: readonly SourceFile[] | undefined; + /** + * Current index to retrieve affected file from + */ + affectedFilesIndex: number | undefined; + /** + * Current changed file for iterating over affected files + */ + currentChangedFilePath: Path | undefined; + /** + * Map of file signatures, with key being file path, calculated while getting current changed file's affected files + * These will be committed whenever the iteration through affected files of current changed file is complete + */ + currentAffectedFilesSignatures: ts.Map | undefined; + /** + * Newly computed visible to outside referencedSet + */ + currentAffectedFilesExportedModulesMap: BuilderState.ComputingExportedModulesMap | undefined; + /** + * Already seen affected files + */ + seenAffectedFiles: ts.Map | undefined; + /** + * whether this program has cleaned semantic diagnostics cache for lib files + */ + cleanedDiagnosticsOfLibFiles?: boolean; + /** + * True if the semantic diagnostics were copied from the old state + */ + semanticDiagnosticsFromOldState?: ts.Map; + /** + * program corresponding to this state + */ + program: Program | undefined; + /** + * compilerOptions for the program + */ + compilerOptions: CompilerOptions; /** - * fn on file and iterate on anything that exports this file + * Files pending to be emitted */ - function forEachFileAndExportsOfFile(state: BuilderProgramState, filePath: Path, seenFileAndExportsOfFile: Map, fn: (state: BuilderProgramState, filePath: Path) => boolean): boolean { - if (!addToSeen(seenFileAndExportsOfFile, filePath)) { - return false; + affectedFilesPendingEmit: Path[] | undefined; + /** + * Files pending to be emitted kind. + */ + affectedFilesPendingEmitKind: ts.Map | undefined; + /** + * Current index to retrieve pending affected file + */ + affectedFilesPendingEmitIndex: number | undefined; + /** + * true if build info is emitted + */ + emittedBuildInfo?: boolean; + /** + * Already seen emitted files + */ + seenEmittedFiles: ts.Map | undefined; + /** + * true if program has been emitted + */ + programEmitComplete?: true; +} +/* @internal */ +function hasSameKeys(map1: ts.ReadonlyMap | undefined, map2: ts.ReadonlyMap | undefined): boolean { + // Has same size and every key is present in both maps + return (map1 as ts.ReadonlyMap) === map2 || map1 !== undefined && map2 !== undefined && map1.size === map2.size && !forEachKey(map1, key => !map2.has(key)); +} +/** + * Create the state so that we can iterate on changedFiles/affected files + */ +/* @internal */ +function createBuilderProgramState(newProgram: Program, getCanonicalFileName: GetCanonicalFileName, oldState?: Readonly): BuilderProgramState { + const state = (BuilderState.create(newProgram, getCanonicalFileName, oldState) as BuilderProgramState); + state.program = newProgram; + const compilerOptions = newProgram.getCompilerOptions(); + state.compilerOptions = compilerOptions; + // With --out or --outFile, any change affects all semantic diagnostics so no need to cache them + if (!compilerOptions.outFile && !compilerOptions.out) { + state.semanticDiagnosticsPerFile = createMap(); + } + state.changedFilesSet = createMap(); + const useOldState = BuilderState.canReuseOldState(state.referencedMap, oldState); + const oldCompilerOptions = useOldState ? oldState!.compilerOptions : undefined; + const canCopySemanticDiagnostics = useOldState && oldState!.semanticDiagnosticsPerFile && !!state.semanticDiagnosticsPerFile && + !compilerOptionsAffectSemanticDiagnostics(compilerOptions, (oldCompilerOptions!)); + if (useOldState) { + // Verify the sanity of old state + if (!oldState!.currentChangedFilePath) { + const affectedSignatures = oldState!.currentAffectedFilesSignatures; + Debug.assert(!oldState!.affectedFiles && (!affectedSignatures || !affectedSignatures.size), "Cannot reuse if only few affected files of currentChangedFile were iterated"); } - - if (fn(state, filePath)) { - // If there are no more diagnostics from old cache, done - return true; + const changedFilesSet = oldState!.changedFilesSet; + if (canCopySemanticDiagnostics) { + Debug.assert(!changedFilesSet || !forEachKey(changedFilesSet, path => oldState!.semanticDiagnosticsPerFile!.has(path)), "Semantic diagnostics shouldnt be available for changed files"); } - - Debug.assert(!!state.currentAffectedFilesExportedModulesMap); - // Go through exported modules from cache first - // If exported modules has path, all files referencing file exported from are affected - if (forEachEntry(state.currentAffectedFilesExportedModulesMap, (exportedModules, exportedFromPath) => - exportedModules && - exportedModules.has(filePath) && - forEachFileAndExportsOfFile(state, exportedFromPath as Path, seenFileAndExportsOfFile, fn) - )) { - return true; + // Copy old state's changed files set + if (changedFilesSet) { + copyEntries(changedFilesSet, state.changedFilesSet); } - - // If exported from path is not from cache and exported modules has path, all files referencing file exported from are affected - if (forEachEntry(state.exportedModulesMap!, (exportedModules, exportedFromPath) => - !state.currentAffectedFilesExportedModulesMap!.has(exportedFromPath) && // If we already iterated this through cache, ignore it - exportedModules.has(filePath) && - forEachFileAndExportsOfFile(state, exportedFromPath as Path, seenFileAndExportsOfFile, fn) - )) { - return true; + if (!compilerOptions.outFile && !compilerOptions.out && oldState!.affectedFilesPendingEmit) { + state.affectedFilesPendingEmit = oldState!.affectedFilesPendingEmit.slice(); + state.affectedFilesPendingEmitKind = cloneMapOrUndefined(oldState!.affectedFilesPendingEmitKind); + state.affectedFilesPendingEmitIndex = oldState!.affectedFilesPendingEmitIndex; } - - // Remove diagnostics of files that import this file (without going to exports of referencing files) - return !!forEachEntry(state.referencedMap!, (referencesInFile, referencingFilePath) => - referencesInFile.has(filePath) && - !seenFileAndExportsOfFile.has(referencingFilePath) && // Not already removed diagnostic file - fn(state, referencingFilePath as Path) // Dont add to seen since this is not yet done with the export removal - ); } - - - /** - * This is called after completing operation on the next affected file. - * The operations here are postponed to ensure that cancellation during the iteration is handled correctly - */ - function doneWithAffectedFile( - state: BuilderProgramState, - affected: SourceFile | Program, - emitKind?: BuilderFileEmit, - isPendingEmit?: boolean, - isBuildInfoEmit?: boolean - ) { - if (isBuildInfoEmit) { - state.emittedBuildInfo = true; - } - else if (affected === state.program) { - state.changedFilesSet.clear(); - state.programEmitComplete = true; + // Update changed files and copy semantic diagnostics if we can + const referencedMap = state.referencedMap; + const oldReferencedMap = useOldState ? oldState!.referencedMap : undefined; + const copyDeclarationFileDiagnostics = canCopySemanticDiagnostics && !compilerOptions.skipLibCheck === !oldCompilerOptions!.skipLibCheck; + const copyLibFileDiagnostics = copyDeclarationFileDiagnostics && !compilerOptions.skipDefaultLibCheck === !oldCompilerOptions!.skipDefaultLibCheck; + state.fileInfos.forEach((info, sourceFilePath) => { + let oldInfo: Readonly | undefined; + let newReferences: BuilderState.ReferencedSet | undefined; + // if not using old state, every file is changed + if (!useOldState || + // File wasnt present in old state + !(oldInfo = oldState!.fileInfos.get(sourceFilePath)) || + // versions dont match + oldInfo.version !== info.version || + // Referenced files changed + !hasSameKeys(newReferences = referencedMap && referencedMap.get(sourceFilePath), oldReferencedMap && oldReferencedMap.get(sourceFilePath)) || + // Referenced file was deleted in the new program + newReferences && forEachKey(newReferences, path => !state.fileInfos.has(path) && oldState!.fileInfos.has(path))) { + // Register file as changed file and do not copy semantic diagnostics, since all changed files need to be re-evaluated + state.changedFilesSet.set(sourceFilePath, true); } - else { - state.seenAffectedFiles!.set((affected as SourceFile).resolvedPath, true); - if (emitKind !== undefined) { - (state.seenEmittedFiles || (state.seenEmittedFiles = createMap())).set((affected as SourceFile).resolvedPath, emitKind); + else if (canCopySemanticDiagnostics) { + const sourceFile = (newProgram.getSourceFileByPath((sourceFilePath as Path))!); + if (sourceFile.isDeclarationFile && !copyDeclarationFileDiagnostics) { + return; } - if (isPendingEmit) { - state.affectedFilesPendingEmitIndex!++; + if (sourceFile.hasNoDefaultLib && !copyLibFileDiagnostics) { + return; } - else { - state.affectedFilesIndex!++; + // Unchanged file copy diagnostics + const diagnostics = oldState!.semanticDiagnosticsPerFile!.get(sourceFilePath); + if (diagnostics) { + state.semanticDiagnosticsPerFile!.set(sourceFilePath, oldState!.hasReusableDiagnostic ? convertToDiagnostics(diagnostics as readonly ReusableDiagnostic[], newProgram, getCanonicalFileName) : diagnostics as readonly Diagnostic[]); + if (!state.semanticDiagnosticsFromOldState) { + state.semanticDiagnosticsFromOldState = createMap(); + } + state.semanticDiagnosticsFromOldState.set(sourceFilePath, true); } } + }); + if (oldCompilerOptions && compilerOptionsAffectEmit(compilerOptions, oldCompilerOptions)) { + // Add all files to affectedFilesPendingEmit since emit changed + newProgram.getSourceFiles().forEach(f => addToAffectedFilesPendingEmit(state, f.resolvedPath, BuilderFileEmit.Full)); + Debug.assert(state.seenAffectedFiles === undefined); + state.seenAffectedFiles = createMap(); } - - /** - * Returns the result with affected file - */ - function toAffectedFileResult(state: BuilderProgramState, result: T, affected: SourceFile | Program): AffectedFileResult { - doneWithAffectedFile(state, affected); - return { result, affected }; - } - - /** - * Returns the result with affected file - */ - function toAffectedFileEmitResult( - state: BuilderProgramState, - result: EmitResult, - affected: SourceFile | Program, - emitKind: BuilderFileEmit, - isPendingEmit?: boolean, - isBuildInfoEmit?: boolean - ): AffectedFileResult { - doneWithAffectedFile(state, affected, emitKind, isPendingEmit, isBuildInfoEmit); - return { result, affected }; - } - - /** - * Gets semantic diagnostics for the file which are - * bindAndCheckDiagnostics (from cache) and program diagnostics - */ - function getSemanticDiagnosticsOfFile(state: BuilderProgramState, sourceFile: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[] { - return concatenate( - getBinderAndCheckerDiagnosticsOfFile(state, sourceFile, cancellationToken), - Debug.checkDefined(state.program).getProgramDiagnostics(sourceFile) - ); + state.emittedBuildInfo = !state.changedFilesSet.size && !state.affectedFilesPendingEmit; + return state; +} +/* @internal */ +function convertToDiagnostics(diagnostics: readonly ReusableDiagnostic[], newProgram: Program, getCanonicalFileName: GetCanonicalFileName): readonly Diagnostic[] { + if (!diagnostics.length) + return emptyArray; + const buildInfoDirectory = getDirectoryPath(getNormalizedAbsolutePath((getTsBuildInfoEmitOutputFilePath(newProgram.getCompilerOptions())!), newProgram.getCurrentDirectory())); + return diagnostics.map(diagnostic => { + const result: Diagnostic = convertToDiagnosticRelatedInformation(diagnostic, newProgram, toPath); + result.reportsUnnecessary = diagnostic.reportsUnnecessary; + result.source = diagnostic.source; + const { relatedInformation } = diagnostic; + result.relatedInformation = relatedInformation ? + relatedInformation.length ? + relatedInformation.map(r => convertToDiagnosticRelatedInformation(r, newProgram, toPath)) : emptyArray : + undefined; + return result; + }); + function toPath(path: string) { + return ts.toPath(path, buildInfoDirectory, getCanonicalFileName); } - - /** - * Gets the binder and checker diagnostics either from cache if present, or otherwise from program and caches it - * Note that it is assumed that when asked about binder and checker diagnostics, the file has been taken out of affected files/changed file set - */ - function getBinderAndCheckerDiagnosticsOfFile(state: BuilderProgramState, sourceFile: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[] { - const path = sourceFile.resolvedPath; - if (state.semanticDiagnosticsPerFile) { - const cachedDiagnostics = state.semanticDiagnosticsPerFile.get(path); - // Report the bind and check diagnostics from the cache if we already have those diagnostics present - if (cachedDiagnostics) { - return cachedDiagnostics; +} +/* @internal */ +function convertToDiagnosticRelatedInformation(diagnostic: ReusableDiagnosticRelatedInformation, newProgram: Program, toPath: (path: string) => Path): DiagnosticRelatedInformation { + const { file } = diagnostic; + return { + ...diagnostic, + file: file ? newProgram.getSourceFileByPath(toPath(file)) : undefined + }; +} +/** + * Releases program and other related not needed properties + */ +/* @internal */ +function releaseCache(state: BuilderProgramState) { + BuilderState.releaseCache(state); + state.program = undefined; +} +/** + * Creates a clone of the state + */ +/* @internal */ +function cloneBuilderProgramState(state: Readonly): BuilderProgramState { + const newState = (BuilderState.clone(state) as BuilderProgramState); + newState.semanticDiagnosticsPerFile = cloneMapOrUndefined(state.semanticDiagnosticsPerFile); + newState.changedFilesSet = cloneMap(state.changedFilesSet); + newState.affectedFiles = state.affectedFiles; + newState.affectedFilesIndex = state.affectedFilesIndex; + newState.currentChangedFilePath = state.currentChangedFilePath; + newState.currentAffectedFilesSignatures = cloneMapOrUndefined(state.currentAffectedFilesSignatures); + newState.currentAffectedFilesExportedModulesMap = cloneMapOrUndefined(state.currentAffectedFilesExportedModulesMap); + newState.seenAffectedFiles = cloneMapOrUndefined(state.seenAffectedFiles); + newState.cleanedDiagnosticsOfLibFiles = state.cleanedDiagnosticsOfLibFiles; + newState.semanticDiagnosticsFromOldState = cloneMapOrUndefined(state.semanticDiagnosticsFromOldState); + newState.program = state.program; + newState.compilerOptions = state.compilerOptions; + newState.affectedFilesPendingEmit = state.affectedFilesPendingEmit && state.affectedFilesPendingEmit.slice(); + newState.affectedFilesPendingEmitKind = cloneMapOrUndefined(state.affectedFilesPendingEmitKind); + newState.affectedFilesPendingEmitIndex = state.affectedFilesPendingEmitIndex; + newState.seenEmittedFiles = cloneMapOrUndefined(state.seenEmittedFiles); + newState.programEmitComplete = state.programEmitComplete; + return newState; +} +/** + * Verifies that source file is ok to be used in calls that arent handled by next + */ +/* @internal */ +function assertSourceFileOkWithoutNextAffectedCall(state: BuilderProgramState, sourceFile: SourceFile | undefined) { + Debug.assert(!sourceFile || !state.affectedFiles || state.affectedFiles[state.affectedFilesIndex! - 1] !== sourceFile || !state.semanticDiagnosticsPerFile!.has(sourceFile.resolvedPath)); +} +/** + * This function returns the next affected file to be processed. + * Note that until doneAffected is called it would keep reporting same result + * This is to allow the callers to be able to actually remove affected file only when the operation is complete + * eg. if during diagnostics check cancellation token ends up cancelling the request, the affected file should be retained + */ +/* @internal */ +function getNextAffectedFile(state: BuilderProgramState, cancellationToken: CancellationToken | undefined, computeHash: BuilderState.ComputeHash): SourceFile | Program | undefined { + while (true) { + const { affectedFiles } = state; + if (affectedFiles) { + const seenAffectedFiles = state.seenAffectedFiles!; + let affectedFilesIndex = state.affectedFilesIndex!; // TODO: GH#18217 + while (affectedFilesIndex < affectedFiles.length) { + const affectedFile = affectedFiles[affectedFilesIndex]; + if (!seenAffectedFiles.has(affectedFile.resolvedPath)) { + // Set the next affected file as seen and remove the cached semantic diagnostics + state.affectedFilesIndex = affectedFilesIndex; + handleDtsMayChangeOfAffectedFile(state, affectedFile, cancellationToken, computeHash); + return affectedFile; + } + affectedFilesIndex++; } + // Remove the changed file from the change set + state.changedFilesSet.delete(state.currentChangedFilePath!); + state.currentChangedFilePath = undefined; + // Commit the changes in file signature + BuilderState.updateSignaturesFromCache(state, (state.currentAffectedFilesSignatures!)); + state.currentAffectedFilesSignatures!.clear(); + BuilderState.updateExportedFilesMapFromCache(state, state.currentAffectedFilesExportedModulesMap); + state.affectedFiles = undefined; } - - // Diagnostics werent cached, get them from program, and cache the result - const diagnostics = Debug.checkDefined(state.program).getBindAndCheckDiagnostics(sourceFile, cancellationToken); - if (state.semanticDiagnosticsPerFile) { - state.semanticDiagnosticsPerFile.set(path, diagnostics); + // Get next changed file + const nextKey = state.changedFilesSet.keys().next(); + if (nextKey.done) { + // Done + return undefined; } - return diagnostics; - } - - export type ProgramBuildInfoDiagnostic = string | [string, readonly ReusableDiagnostic[]]; - export interface ProgramBuildInfo { - fileInfos: MapLike; - options: CompilerOptions; - referencedMap?: MapLike; - exportedModulesMap?: MapLike; - semanticDiagnosticsPerFile?: ProgramBuildInfoDiagnostic[]; - } - - /** - * Gets the program information to be emitted in buildInfo so that we can use it to create new program - */ - function getProgramBuildInfo(state: Readonly, getCanonicalFileName: GetCanonicalFileName): ProgramBuildInfo | undefined { - if (state.compilerOptions.outFile || state.compilerOptions.out) return undefined; - const currentDirectory = Debug.checkDefined(state.program).getCurrentDirectory(); - const buildInfoDirectory = getDirectoryPath(getNormalizedAbsolutePath(getTsBuildInfoEmitOutputFilePath(state.compilerOptions)!, currentDirectory)); - const fileInfos: MapLike = {}; - state.fileInfos.forEach((value, key) => { - const signature = state.currentAffectedFilesSignatures && state.currentAffectedFilesSignatures.get(key); - fileInfos[relativeToBuildInfo(key)] = signature === undefined ? value : { version: value.version, signature }; - }); - - const result: ProgramBuildInfo = { - fileInfos, - options: convertToReusableCompilerOptions(state.compilerOptions, relativeToBuildInfoEnsuringAbsolutePath) - }; - if (state.referencedMap) { - const referencedMap: MapLike = {}; - state.referencedMap.forEach((value, key) => { - referencedMap[relativeToBuildInfo(key)] = arrayFrom(value.keys(), relativeToBuildInfo); - }); - result.referencedMap = referencedMap; + // With --out or --outFile all outputs go into single file + // so operations are performed directly on program, return program + const program = Debug.checkDefined(state.program); + const compilerOptions = program.getCompilerOptions(); + if (compilerOptions.outFile || compilerOptions.out) { + Debug.assert(!state.semanticDiagnosticsPerFile); + return program; } - + // Get next batch of affected files + state.currentAffectedFilesSignatures = state.currentAffectedFilesSignatures || createMap(); if (state.exportedModulesMap) { - const exportedModulesMap: MapLike = {}; - state.exportedModulesMap.forEach((value, key) => { - const newValue = state.currentAffectedFilesExportedModulesMap && state.currentAffectedFilesExportedModulesMap.get(key); - // Not in temporary cache, use existing value - if (newValue === undefined) exportedModulesMap[relativeToBuildInfo(key)] = arrayFrom(value.keys(), relativeToBuildInfo); - // Value in cache and has updated value map, use that - else if (newValue) exportedModulesMap[relativeToBuildInfo(key)] = arrayFrom(newValue.keys(), relativeToBuildInfo); - }); - result.exportedModulesMap = exportedModulesMap; - } - - if (state.semanticDiagnosticsPerFile) { - const semanticDiagnosticsPerFile: ProgramBuildInfoDiagnostic[] = []; - // Currently not recording actual errors since those mean no emit for tsc --build - state.semanticDiagnosticsPerFile.forEach((value, key) => semanticDiagnosticsPerFile.push( - value.length ? - [ - relativeToBuildInfo(key), - state.hasReusableDiagnostic ? - value as readonly ReusableDiagnostic[] : - convertToReusableDiagnostics(value as readonly Diagnostic[], relativeToBuildInfo) - ] : - relativeToBuildInfo(key) - )); - result.semanticDiagnosticsPerFile = semanticDiagnosticsPerFile; + state.currentAffectedFilesExportedModulesMap = state.currentAffectedFilesExportedModulesMap || createMap(); } - - return result; - - function relativeToBuildInfoEnsuringAbsolutePath(path: string) { - return relativeToBuildInfo(getNormalizedAbsolutePath(path, currentDirectory)); + state.affectedFiles = BuilderState.getFilesAffectedBy(state, program, (nextKey.value as Path), cancellationToken, computeHash, state.currentAffectedFilesSignatures, state.currentAffectedFilesExportedModulesMap); + state.currentChangedFilePath = (nextKey.value as Path); + state.affectedFilesIndex = 0; + state.seenAffectedFiles = state.seenAffectedFiles || createMap(); + } +} +/** + * Returns next file to be emitted from files that retrieved semantic diagnostics but did not emit yet + */ +/* @internal */ +function getNextAffectedFilePendingEmit(state: BuilderProgramState) { + const { affectedFilesPendingEmit } = state; + if (affectedFilesPendingEmit) { + const seenEmittedFiles = state.seenEmittedFiles || (state.seenEmittedFiles = createMap()); + for (let i = state.affectedFilesPendingEmitIndex!; i < affectedFilesPendingEmit.length; i++) { + const affectedFile = Debug.checkDefined(state.program).getSourceFileByPath(affectedFilesPendingEmit[i]); + if (affectedFile) { + const seenKind = seenEmittedFiles.get(affectedFile.resolvedPath); + const emitKind = Debug.checkDefined(Debug.checkDefined(state.affectedFilesPendingEmitKind).get(affectedFile.resolvedPath)); + if (seenKind === undefined || seenKind < emitKind) { + // emit this file + state.affectedFilesPendingEmitIndex = i; + return { affectedFile, emitKind }; + } + } } - - function relativeToBuildInfo(path: string) { - return ensurePathIsNonModuleName(getRelativePathFromDirectory(buildInfoDirectory, path, getCanonicalFileName)); + state.affectedFilesPendingEmit = undefined; + state.affectedFilesPendingEmitKind = undefined; + state.affectedFilesPendingEmitIndex = undefined; + } + return undefined; +} +/** + * Handles semantic diagnostics and dts emit for affectedFile and files, that are referencing modules that export entities from affected file + * This is because even though js emit doesnt change, dts emit / type used can change resulting in need for dts emit and js change + */ +/* @internal */ +function handleDtsMayChangeOfAffectedFile(state: BuilderProgramState, affectedFile: SourceFile, cancellationToken: CancellationToken | undefined, computeHash: BuilderState.ComputeHash) { + removeSemanticDiagnosticsOf(state, affectedFile.resolvedPath); + // If affected files is everything except default library, then nothing more to do + if (state.allFilesExcludingDefaultLibraryFile === state.affectedFiles) { + if (!state.cleanedDiagnosticsOfLibFiles) { + state.cleanedDiagnosticsOfLibFiles = true; + const program = Debug.checkDefined(state.program); + const options = program.getCompilerOptions(); + forEach(program.getSourceFiles(), f => program.isSourceFileDefaultLibrary(f) && + !skipTypeChecking(f, options, program) && + removeSemanticDiagnosticsOf(state, f.resolvedPath)); } + return; } - - function convertToReusableCompilerOptions(options: CompilerOptions, relativeToBuildInfo: (path: string) => string) { - const result: CompilerOptions = {}; - const { optionsNameMap } = getOptionsNameMap(); - - for (const name in options) { - if (hasProperty(options, name)) { - result[name] = convertToReusableCompilerOptionValue( - optionsNameMap.get(name.toLowerCase()), - options[name] as CompilerOptionsValue, - relativeToBuildInfo - ); + if (!state.compilerOptions.assumeChangesOnlyAffectDirectDependencies) { + forEachReferencingModulesOfExportOfAffectedFile(state, affectedFile, (state, path) => handleDtsMayChangeOf(state, path, cancellationToken, computeHash)); + } +} +/** + * Handle the dts may change, so they need to be added to pending emit if dts emit is enabled, + * Also we need to make sure signature is updated for these files + */ +/* @internal */ +function handleDtsMayChangeOf(state: BuilderProgramState, path: Path, cancellationToken: CancellationToken | undefined, computeHash: BuilderState.ComputeHash) { + removeSemanticDiagnosticsOf(state, path); + if (!state.changedFilesSet.has(path)) { + const program = Debug.checkDefined(state.program); + const sourceFile = program.getSourceFileByPath(path); + if (sourceFile) { + // Even though the js emit doesnt change and we are already handling dts emit and semantic diagnostics + // we need to update the signature to reflect correctness of the signature(which is output d.ts emit) of this file + // This ensures that we dont later during incremental builds considering wrong signature. + // Eg where this also is needed to ensure that .tsbuildinfo generated by incremental build should be same as if it was first fresh build + BuilderState.updateShapeSignature(state, program, sourceFile, Debug.checkDefined(state.currentAffectedFilesSignatures), cancellationToken, computeHash, state.currentAffectedFilesExportedModulesMap); + // If not dts emit, nothing more to do + if (getEmitDeclarations(state.compilerOptions)) { + addToAffectedFilesPendingEmit(state, path, BuilderFileEmit.DtsOnly); } } - if (result.configFilePath) { - result.configFilePath = relativeToBuildInfo(result.configFilePath); - } - return result; } - - function convertToReusableCompilerOptionValue(option: CommandLineOption | undefined, value: CompilerOptionsValue, relativeToBuildInfo: (path: string) => string) { - if (option) { - if (option.type === "list") { - const values = value as readonly (string | number)[]; - if (option.element.isFilePath && values.length) { - return values.map(relativeToBuildInfo); + return false; +} +/** + * Removes semantic diagnostics for path and + * returns true if there are no more semantic diagnostics from the old state + */ +/* @internal */ +function removeSemanticDiagnosticsOf(state: BuilderProgramState, path: Path) { + if (!state.semanticDiagnosticsFromOldState) { + return true; + } + state.semanticDiagnosticsFromOldState.delete(path); + state.semanticDiagnosticsPerFile!.delete(path); + return !state.semanticDiagnosticsFromOldState.size; +} +/* @internal */ +function isChangedSignagure(state: BuilderProgramState, path: Path) { + const newSignature = Debug.checkDefined(state.currentAffectedFilesSignatures).get(path); + const oldSignagure = Debug.checkDefined(state.fileInfos.get(path)).signature; + return newSignature !== oldSignagure; +} +/** + * Iterate on referencing modules that export entities from affected file + */ +/* @internal */ +function forEachReferencingModulesOfExportOfAffectedFile(state: BuilderProgramState, affectedFile: SourceFile, fn: (state: BuilderProgramState, filePath: Path) => boolean) { + // If there was change in signature (dts output) for the changed file, + // then only we need to handle pending file emit + if (!state.exportedModulesMap || !state.changedFilesSet.has(affectedFile.resolvedPath)) { + return; + } + if (!isChangedSignagure(state, affectedFile.resolvedPath)) + return; + // Since isolated modules dont change js files, files affected by change in signature is itself + // But we need to cleanup semantic diagnostics and queue dts emit for affected files + if (state.compilerOptions.isolatedModules) { + const seenFileNamesMap = createMap(); + seenFileNamesMap.set(affectedFile.resolvedPath, true); + const queue = BuilderState.getReferencedByPaths(state, affectedFile.resolvedPath); + while (queue.length > 0) { + const currentPath = queue.pop()!; + if (!seenFileNamesMap.has(currentPath)) { + seenFileNamesMap.set(currentPath, true); + const result = fn(state, currentPath); + if (result && isChangedSignagure(state, currentPath)) { + const currentSourceFile = (Debug.checkDefined(state.program).getSourceFileByPath(currentPath)!); + queue.push(...BuilderState.getReferencedByPaths(state, currentSourceFile.resolvedPath)); } } - else if (option.isFilePath) { - return relativeToBuildInfo(value as string); - } } - return value; } - - function convertToReusableDiagnostics(diagnostics: readonly Diagnostic[], relativeToBuildInfo: (path: string) => string): readonly ReusableDiagnostic[] { - Debug.assert(!!diagnostics.length); - return diagnostics.map(diagnostic => { - const result: ReusableDiagnostic = convertToReusableDiagnosticRelatedInformation(diagnostic, relativeToBuildInfo); - result.reportsUnnecessary = diagnostic.reportsUnnecessary; - result.source = diagnostic.source; - const { relatedInformation } = diagnostic; - result.relatedInformation = relatedInformation ? - relatedInformation.length ? - relatedInformation.map(r => convertToReusableDiagnosticRelatedInformation(r, relativeToBuildInfo)) : - emptyArray : - undefined; - return result; - }); + Debug.assert(!!state.currentAffectedFilesExportedModulesMap); + const seenFileAndExportsOfFile = createMap(); + // Go through exported modules from cache first + // If exported modules has path, all files referencing file exported from are affected + if (forEachEntry(state.currentAffectedFilesExportedModulesMap, (exportedModules, exportedFromPath) => exportedModules && + exportedModules.has(affectedFile.resolvedPath) && + forEachFilesReferencingPath(state, (exportedFromPath as Path), seenFileAndExportsOfFile, fn))) { + return; } - - function convertToReusableDiagnosticRelatedInformation(diagnostic: DiagnosticRelatedInformation, relativeToBuildInfo: (path: string) => string): ReusableDiagnosticRelatedInformation { - const { file } = diagnostic; - return { - ...diagnostic, - file: file ? relativeToBuildInfo(file.resolvedPath) : undefined - }; + // If exported from path is not from cache and exported modules has path, all files referencing file exported from are affected + forEachEntry(state.exportedModulesMap, (exportedModules, exportedFromPath) => !state.currentAffectedFilesExportedModulesMap!.has(exportedFromPath) && // If we already iterated this through cache, ignore it + exportedModules.has(affectedFile.resolvedPath) && + forEachFilesReferencingPath(state, (exportedFromPath as Path), seenFileAndExportsOfFile, fn)); +} +/** + * Iterate on files referencing referencedPath + */ +/* @internal */ +function forEachFilesReferencingPath(state: BuilderProgramState, referencedPath: Path, seenFileAndExportsOfFile: ts.Map, fn: (state: BuilderProgramState, filePath: Path) => boolean) { + return forEachEntry((state.referencedMap!), (referencesInFile, filePath) => referencesInFile.has(referencedPath) && forEachFileAndExportsOfFile(state, (filePath as Path), seenFileAndExportsOfFile, fn)); +} +/** + * fn on file and iterate on anything that exports this file + */ +/* @internal */ +function forEachFileAndExportsOfFile(state: BuilderProgramState, filePath: Path, seenFileAndExportsOfFile: ts.Map, fn: (state: BuilderProgramState, filePath: Path) => boolean): boolean { + if (!addToSeen(seenFileAndExportsOfFile, filePath)) { + return false; + } + if (fn(state, filePath)) { + // If there are no more diagnostics from old cache, done + return true; + } + Debug.assert(!!state.currentAffectedFilesExportedModulesMap); + // Go through exported modules from cache first + // If exported modules has path, all files referencing file exported from are affected + if (forEachEntry(state.currentAffectedFilesExportedModulesMap, (exportedModules, exportedFromPath) => exportedModules && + exportedModules.has(filePath) && + forEachFileAndExportsOfFile(state, (exportedFromPath as Path), seenFileAndExportsOfFile, fn))) { + return true; } - - export enum BuilderProgramKind { - SemanticDiagnosticsBuilderProgram, - EmitAndSemanticDiagnosticsBuilderProgram + // If exported from path is not from cache and exported modules has path, all files referencing file exported from are affected + if (forEachEntry((state.exportedModulesMap!), (exportedModules, exportedFromPath) => !state.currentAffectedFilesExportedModulesMap!.has(exportedFromPath) && // If we already iterated this through cache, ignore it + exportedModules.has(filePath) && + forEachFileAndExportsOfFile(state, (exportedFromPath as Path), seenFileAndExportsOfFile, fn))) { + return true; } - - export interface BuilderCreationParameters { - newProgram: Program; - host: BuilderProgramHost; - oldProgram: BuilderProgram | undefined; - configFileParsingDiagnostics: readonly Diagnostic[]; + // Remove diagnostics of files that import this file (without going to exports of referencing files) + return !!forEachEntry((state.referencedMap!), (referencesInFile, referencingFilePath) => referencesInFile.has(filePath) && + !seenFileAndExportsOfFile.has(referencingFilePath) && // Not already removed diagnostic file + fn(state, (referencingFilePath as Path)) // Dont add to seen since this is not yet done with the export removal + ); +} +/** + * This is called after completing operation on the next affected file. + * The operations here are postponed to ensure that cancellation during the iteration is handled correctly + */ +/* @internal */ +function doneWithAffectedFile(state: BuilderProgramState, affected: SourceFile | Program, emitKind?: BuilderFileEmit, isPendingEmit?: boolean, isBuildInfoEmit?: boolean) { + if (isBuildInfoEmit) { + state.emittedBuildInfo = true; + } + else if (affected === state.program) { + state.changedFilesSet.clear(); + state.programEmitComplete = true; } - - export function getBuilderCreationParameters(newProgramOrRootNames: Program | readonly string[] | undefined, hostOrOptions: BuilderProgramHost | CompilerOptions | undefined, oldProgramOrHost?: BuilderProgram | CompilerHost, configFileParsingDiagnosticsOrOldProgram?: readonly Diagnostic[] | BuilderProgram, configFileParsingDiagnostics?: readonly Diagnostic[], projectReferences?: readonly ProjectReference[]): BuilderCreationParameters { - let host: BuilderProgramHost; - let newProgram: Program; - let oldProgram: BuilderProgram; - if (newProgramOrRootNames === undefined) { - Debug.assert(hostOrOptions === undefined); - host = oldProgramOrHost as CompilerHost; - oldProgram = configFileParsingDiagnosticsOrOldProgram as BuilderProgram; - Debug.assert(!!oldProgram); - newProgram = oldProgram.getProgram(); + else { + state.seenAffectedFiles!.set((affected as SourceFile).resolvedPath, true); + if (emitKind !== undefined) { + (state.seenEmittedFiles || (state.seenEmittedFiles = createMap())).set((affected as SourceFile).resolvedPath, emitKind); } - else if (isArray(newProgramOrRootNames)) { - oldProgram = configFileParsingDiagnosticsOrOldProgram as BuilderProgram; - newProgram = createProgram({ - rootNames: newProgramOrRootNames, - options: hostOrOptions as CompilerOptions, - host: oldProgramOrHost as CompilerHost, - oldProgram: oldProgram && oldProgram.getProgramOrUndefined(), - configFileParsingDiagnostics, - projectReferences - }); - host = oldProgramOrHost as CompilerHost; + if (isPendingEmit) { + state.affectedFilesPendingEmitIndex!++; } else { - newProgram = newProgramOrRootNames; - host = hostOrOptions as BuilderProgramHost; - oldProgram = oldProgramOrHost as BuilderProgram; - configFileParsingDiagnostics = configFileParsingDiagnosticsOrOldProgram as readonly Diagnostic[]; + state.affectedFilesIndex!++; } - return { host, newProgram, oldProgram, configFileParsingDiagnostics: configFileParsingDiagnostics || emptyArray }; } - - export function createBuilderProgram(kind: BuilderProgramKind.SemanticDiagnosticsBuilderProgram, builderCreationParameters: BuilderCreationParameters): SemanticDiagnosticsBuilderProgram; - export function createBuilderProgram(kind: BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram, builderCreationParameters: BuilderCreationParameters): EmitAndSemanticDiagnosticsBuilderProgram; - export function createBuilderProgram(kind: BuilderProgramKind, { newProgram, host, oldProgram, configFileParsingDiagnostics }: BuilderCreationParameters) { - // Return same program if underlying program doesnt change - let oldState = oldProgram && oldProgram.getState(); - if (oldState && newProgram === oldState.program && configFileParsingDiagnostics === newProgram.getConfigFileParsingDiagnostics()) { - newProgram = undefined!; // TODO: GH#18217 - oldState = undefined; - return oldProgram; +} +/** + * Returns the result with affected file + */ +/* @internal */ +function toAffectedFileResult(state: BuilderProgramState, result: T, affected: SourceFile | Program): AffectedFileResult { + doneWithAffectedFile(state, affected); + return { result, affected }; +} +/** + * Returns the result with affected file + */ +/* @internal */ +function toAffectedFileEmitResult(state: BuilderProgramState, result: EmitResult, affected: SourceFile | Program, emitKind: BuilderFileEmit, isPendingEmit?: boolean, isBuildInfoEmit?: boolean): AffectedFileResult { + doneWithAffectedFile(state, affected, emitKind, isPendingEmit, isBuildInfoEmit); + return { result, affected }; +} +/** + * Gets semantic diagnostics for the file which are + * bindAndCheckDiagnostics (from cache) and program diagnostics + */ +/* @internal */ +function getSemanticDiagnosticsOfFile(state: BuilderProgramState, sourceFile: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[] { + return concatenate(getBinderAndCheckerDiagnosticsOfFile(state, sourceFile, cancellationToken), Debug.checkDefined(state.program).getProgramDiagnostics(sourceFile)); +} +/** + * Gets the binder and checker diagnostics either from cache if present, or otherwise from program and caches it + * Note that it is assumed that when asked about binder and checker diagnostics, the file has been taken out of affected files/changed file set + */ +/* @internal */ +function getBinderAndCheckerDiagnosticsOfFile(state: BuilderProgramState, sourceFile: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[] { + const path = sourceFile.resolvedPath; + if (state.semanticDiagnosticsPerFile) { + const cachedDiagnostics = state.semanticDiagnosticsPerFile.get(path); + // Report the bind and check diagnostics from the cache if we already have those diagnostics present + if (cachedDiagnostics) { + return cachedDiagnostics; } - - /** - * Create the canonical file name for identity - */ - const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames()); - /** - * Computing hash to for signature verification - */ - const computeHash = host.createHash || generateDjb2Hash; - let state = createBuilderProgramState(newProgram, getCanonicalFileName, oldState); - let backupState: BuilderProgramState | undefined; - newProgram.getProgramBuildInfo = () => getProgramBuildInfo(state, getCanonicalFileName); - - // To ensure that we arent storing any references to old program or new program without state - newProgram = undefined!; // TODO: GH#18217 - oldProgram = undefined; - oldState = undefined; - - const builderProgram = createRedirectedBuilderProgram(state, configFileParsingDiagnostics); - builderProgram.getState = () => state; - builderProgram.backupState = () => { - Debug.assert(backupState === undefined); - backupState = cloneBuilderProgramState(state); - }; - builderProgram.restoreState = () => { - state = Debug.checkDefined(backupState); - backupState = undefined; - }; - builderProgram.getAllDependencies = sourceFile => BuilderState.getAllDependencies(state, Debug.checkDefined(state.program), sourceFile); - builderProgram.getSemanticDiagnostics = getSemanticDiagnostics; - builderProgram.emit = emit; - builderProgram.releaseProgram = () => { - releaseCache(state); - backupState = undefined; - }; - - if (kind === BuilderProgramKind.SemanticDiagnosticsBuilderProgram) { - (builderProgram as SemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile = getSemanticDiagnosticsOfNextAffectedFile; + } + // Diagnostics werent cached, get them from program, and cache the result + const diagnostics = Debug.checkDefined(state.program).getBindAndCheckDiagnostics(sourceFile, cancellationToken); + if (state.semanticDiagnosticsPerFile) { + state.semanticDiagnosticsPerFile.set(path, diagnostics); + } + return diagnostics; +} +/* @internal */ +export type ProgramBuildInfoDiagnostic = string | [string, readonly ReusableDiagnostic[]]; +/* @internal */ +export interface ProgramBuildInfo { + fileInfos: MapLike; + options: CompilerOptions; + referencedMap?: MapLike; + exportedModulesMap?: MapLike; + semanticDiagnosticsPerFile?: ProgramBuildInfoDiagnostic[]; +} +/** + * Gets the program information to be emitted in buildInfo so that we can use it to create new program + */ +/* @internal */ +function getProgramBuildInfo(state: Readonly, getCanonicalFileName: GetCanonicalFileName): ProgramBuildInfo | undefined { + if (state.compilerOptions.outFile || state.compilerOptions.out) + return undefined; + const currentDirectory = Debug.checkDefined(state.program).getCurrentDirectory(); + const buildInfoDirectory = getDirectoryPath(getNormalizedAbsolutePath((getTsBuildInfoEmitOutputFilePath(state.compilerOptions)!), currentDirectory)); + const fileInfos: MapLike = {}; + state.fileInfos.forEach((value, key) => { + const signature = state.currentAffectedFilesSignatures && state.currentAffectedFilesSignatures.get(key); + fileInfos[relativeToBuildInfo(key)] = signature === undefined ? value : { version: value.version, signature }; + }); + const result: ProgramBuildInfo = { + fileInfos, + options: convertToReusableCompilerOptions(state.compilerOptions, relativeToBuildInfoEnsuringAbsolutePath) + }; + if (state.referencedMap) { + const referencedMap: MapLike = {}; + state.referencedMap.forEach((value, key) => { + referencedMap[relativeToBuildInfo(key)] = arrayFrom(value.keys(), relativeToBuildInfo); + }); + result.referencedMap = referencedMap; + } + if (state.exportedModulesMap) { + const exportedModulesMap: MapLike = {}; + state.exportedModulesMap.forEach((value, key) => { + const newValue = state.currentAffectedFilesExportedModulesMap && state.currentAffectedFilesExportedModulesMap.get(key); + // Not in temporary cache, use existing value + if (newValue === undefined) + exportedModulesMap[relativeToBuildInfo(key)] = arrayFrom(value.keys(), relativeToBuildInfo); + // Value in cache and has updated value map, use that + else if (newValue) + exportedModulesMap[relativeToBuildInfo(key)] = arrayFrom(newValue.keys(), relativeToBuildInfo); + }); + result.exportedModulesMap = exportedModulesMap; + } + if (state.semanticDiagnosticsPerFile) { + const semanticDiagnosticsPerFile: ProgramBuildInfoDiagnostic[] = []; + // Currently not recording actual errors since those mean no emit for tsc --build + state.semanticDiagnosticsPerFile.forEach((value, key) => semanticDiagnosticsPerFile.push(value.length ? + [ + relativeToBuildInfo(key), + state.hasReusableDiagnostic ? + value as readonly ReusableDiagnostic[] : + convertToReusableDiagnostics((value as readonly Diagnostic[]), relativeToBuildInfo) + ] : + relativeToBuildInfo(key))); + result.semanticDiagnosticsPerFile = semanticDiagnosticsPerFile; + } + return result; + function relativeToBuildInfoEnsuringAbsolutePath(path: string) { + return relativeToBuildInfo(getNormalizedAbsolutePath(path, currentDirectory)); + } + function relativeToBuildInfo(path: string) { + return ensurePathIsNonModuleName(getRelativePathFromDirectory(buildInfoDirectory, path, getCanonicalFileName)); + } +} +/* @internal */ +function convertToReusableCompilerOptions(options: CompilerOptions, relativeToBuildInfo: (path: string) => string) { + const result: CompilerOptions = {}; + const { optionsNameMap } = getOptionsNameMap(); + for (const name in options) { + if (hasProperty(options, name)) { + result[name] = convertToReusableCompilerOptionValue(optionsNameMap.get(name.toLowerCase()), (options[name] as CompilerOptionsValue), relativeToBuildInfo); } - else if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram) { - (builderProgram as EmitAndSemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile = getSemanticDiagnosticsOfNextAffectedFile; - (builderProgram as EmitAndSemanticDiagnosticsBuilderProgram).emitNextAffectedFile = emitNextAffectedFile; + } + if (result.configFilePath) { + result.configFilePath = relativeToBuildInfo(result.configFilePath); + } + return result; +} +/* @internal */ +function convertToReusableCompilerOptionValue(option: CommandLineOption | undefined, value: CompilerOptionsValue, relativeToBuildInfo: (path: string) => string) { + if (option) { + if (option.type === "list") { + const values = value as readonly (string | number)[]; + if (option.element.isFilePath && values.length) { + return values.map(relativeToBuildInfo); + } } - else { - notImplemented(); + else if (option.isFilePath) { + return relativeToBuildInfo(value as string); } - - return builderProgram; - - /** - * Emits the next affected file's emit result (EmitResult and sourceFiles emitted) or returns undefined if iteration is complete - * The first of writeFile if provided, writeFile of BuilderProgramHost if provided, writeFile of compiler host - * in that order would be used to write the files - */ - function emitNextAffectedFile(writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): AffectedFileResult { - let affected = getNextAffectedFile(state, cancellationToken, computeHash); - let emitKind = BuilderFileEmit.Full; - let isPendingEmitFile = false; - if (!affected) { - if (!state.compilerOptions.out && !state.compilerOptions.outFile) { - const pendingAffectedFile = getNextAffectedFilePendingEmit(state); - if (!pendingAffectedFile) { - if (state.emittedBuildInfo) { - return undefined; - } - - const affected = Debug.checkDefined(state.program); - return toAffectedFileEmitResult( - state, - // When whole program is affected, do emit only once (eg when --out or --outFile is specified) - // Otherwise just affected file - affected.emitBuildInfo(writeFile || maybeBind(host, host.writeFile), cancellationToken), - affected, - /*emitKind*/ BuilderFileEmit.Full, - /*isPendingEmitFile*/ false, - /*isBuildInfoEmit*/ true - ); + } + return value; +} +/* @internal */ +function convertToReusableDiagnostics(diagnostics: readonly Diagnostic[], relativeToBuildInfo: (path: string) => string): readonly ReusableDiagnostic[] { + Debug.assert(!!diagnostics.length); + return diagnostics.map(diagnostic => { + const result: ReusableDiagnostic = convertToReusableDiagnosticRelatedInformation(diagnostic, relativeToBuildInfo); + result.reportsUnnecessary = diagnostic.reportsUnnecessary; + result.source = diagnostic.source; + const { relatedInformation } = diagnostic; + result.relatedInformation = relatedInformation ? + relatedInformation.length ? + relatedInformation.map(r => convertToReusableDiagnosticRelatedInformation(r, relativeToBuildInfo)) : emptyArray : + undefined; + return result; + }); +} +/* @internal */ +function convertToReusableDiagnosticRelatedInformation(diagnostic: DiagnosticRelatedInformation, relativeToBuildInfo: (path: string) => string): ReusableDiagnosticRelatedInformation { + const { file } = diagnostic; + return { + ...diagnostic, + file: file ? relativeToBuildInfo(file.resolvedPath) : undefined + }; +} +/* @internal */ +export enum BuilderProgramKind { + SemanticDiagnosticsBuilderProgram, + EmitAndSemanticDiagnosticsBuilderProgram +} +/* @internal */ +export interface BuilderCreationParameters { + newProgram: Program; + host: BuilderProgramHost; + oldProgram: BuilderProgram | undefined; + configFileParsingDiagnostics: readonly Diagnostic[]; +} +/* @internal */ +export function getBuilderCreationParameters(newProgramOrRootNames: Program | readonly string[] | undefined, hostOrOptions: BuilderProgramHost | CompilerOptions | undefined, oldProgramOrHost?: BuilderProgram | CompilerHost, configFileParsingDiagnosticsOrOldProgram?: readonly Diagnostic[] | BuilderProgram, configFileParsingDiagnostics?: readonly Diagnostic[], projectReferences?: readonly ProjectReference[]): BuilderCreationParameters { + let host: BuilderProgramHost; + let newProgram: Program; + let oldProgram: BuilderProgram; + if (newProgramOrRootNames === undefined) { + Debug.assert(hostOrOptions === undefined); + host = (oldProgramOrHost as CompilerHost); + oldProgram = (configFileParsingDiagnosticsOrOldProgram as BuilderProgram); + Debug.assert(!!oldProgram); + newProgram = oldProgram.getProgram(); + } + else if (isArray(newProgramOrRootNames)) { + oldProgram = (configFileParsingDiagnosticsOrOldProgram as BuilderProgram); + newProgram = createProgram({ + rootNames: newProgramOrRootNames, + options: (hostOrOptions as CompilerOptions), + host: (oldProgramOrHost as CompilerHost), + oldProgram: oldProgram && oldProgram.getProgramOrUndefined(), + configFileParsingDiagnostics, + projectReferences + }); + host = (oldProgramOrHost as CompilerHost); + } + else { + newProgram = newProgramOrRootNames; + host = (hostOrOptions as BuilderProgramHost); + oldProgram = (oldProgramOrHost as BuilderProgram); + configFileParsingDiagnostics = (configFileParsingDiagnosticsOrOldProgram as readonly Diagnostic[]); + } + return { host, newProgram, oldProgram, configFileParsingDiagnostics: configFileParsingDiagnostics || emptyArray }; +} +/* @internal */ +export function createBuilderProgram(kind: BuilderProgramKind.SemanticDiagnosticsBuilderProgram, builderCreationParameters: BuilderCreationParameters): SemanticDiagnosticsBuilderProgram; +/* @internal */ +export function createBuilderProgram(kind: BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram, builderCreationParameters: BuilderCreationParameters): EmitAndSemanticDiagnosticsBuilderProgram; +/* @internal */ +export function createBuilderProgram(kind: BuilderProgramKind, { newProgram, host, oldProgram, configFileParsingDiagnostics }: BuilderCreationParameters) { + // Return same program if underlying program doesnt change + let oldState = oldProgram && oldProgram.getState(); + if (oldState && newProgram === oldState.program && configFileParsingDiagnostics === newProgram.getConfigFileParsingDiagnostics()) { + newProgram = undefined!; // TODO: GH#18217 + oldState = undefined; + return oldProgram; + } + /** + * Create the canonical file name for identity + */ + const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames()); + /** + * Computing hash to for signature verification + */ + const computeHash = host.createHash || generateDjb2Hash; + let state = createBuilderProgramState(newProgram, getCanonicalFileName, oldState); + let backupState: BuilderProgramState | undefined; + newProgram.getProgramBuildInfo = () => getProgramBuildInfo(state, getCanonicalFileName); + // To ensure that we arent storing any references to old program or new program without state + newProgram = undefined!; // TODO: GH#18217 + oldProgram = undefined; + oldState = undefined; + const builderProgram = createRedirectedBuilderProgram(state, configFileParsingDiagnostics); + builderProgram.getState = () => state; + builderProgram.backupState = () => { + Debug.assert(backupState === undefined); + backupState = cloneBuilderProgramState(state); + }; + builderProgram.restoreState = () => { + state = Debug.checkDefined(backupState); + backupState = undefined; + }; + builderProgram.getAllDependencies = sourceFile => BuilderState.getAllDependencies(state, Debug.checkDefined(state.program), sourceFile); + builderProgram.getSemanticDiagnostics = getSemanticDiagnostics; + builderProgram.emit = emit; + builderProgram.releaseProgram = () => { + releaseCache(state); + backupState = undefined; + }; + if (kind === BuilderProgramKind.SemanticDiagnosticsBuilderProgram) { + (builderProgram as SemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile = getSemanticDiagnosticsOfNextAffectedFile; + } + else if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram) { + (builderProgram as EmitAndSemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile = getSemanticDiagnosticsOfNextAffectedFile; + (builderProgram as EmitAndSemanticDiagnosticsBuilderProgram).emitNextAffectedFile = emitNextAffectedFile; + } + else { + notImplemented(); + } + return builderProgram; + /** + * Emits the next affected file's emit result (EmitResult and sourceFiles emitted) or returns undefined if iteration is complete + * The first of writeFile if provided, writeFile of BuilderProgramHost if provided, writeFile of compiler host + * in that order would be used to write the files + */ + function emitNextAffectedFile(writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): AffectedFileResult { + let affected = getNextAffectedFile(state, cancellationToken, computeHash); + let emitKind = BuilderFileEmit.Full; + let isPendingEmitFile = false; + if (!affected) { + if (!state.compilerOptions.out && !state.compilerOptions.outFile) { + const pendingAffectedFile = getNextAffectedFilePendingEmit(state); + if (!pendingAffectedFile) { + if (state.emittedBuildInfo) { + return undefined; } - ({ affectedFile: affected, emitKind } = pendingAffectedFile); - isPendingEmitFile = true; - } - else { - const program = Debug.checkDefined(state.program); - if (state.programEmitComplete) return undefined; - affected = program; + const affected = Debug.checkDefined(state.program); + return toAffectedFileEmitResult(state, + // When whole program is affected, do emit only once (eg when --out or --outFile is specified) + // Otherwise just affected file + affected.emitBuildInfo(writeFile || maybeBind(host, host.writeFile), cancellationToken), affected, + /*emitKind*/ BuilderFileEmit.Full, + /*isPendingEmitFile*/ false, + /*isBuildInfoEmit*/ true); } + ({ affectedFile: affected, emitKind } = pendingAffectedFile); + isPendingEmitFile = true; } - - return toAffectedFileEmitResult( - state, - // When whole program is affected, do emit only once (eg when --out or --outFile is specified) - // Otherwise just affected file - Debug.checkDefined(state.program).emit( - affected === state.program ? undefined : affected as SourceFile, - writeFile || maybeBind(host, host.writeFile), - cancellationToken, - emitOnlyDtsFiles || emitKind === BuilderFileEmit.DtsOnly, - customTransformers - ), - affected, - emitKind, - isPendingEmitFile, - ); - } - - /** - * Emits the JavaScript and declaration files. - * When targetSource file is specified, emits the files corresponding to that source file, - * otherwise for the whole program. - * In case of EmitAndSemanticDiagnosticsBuilderProgram, when targetSourceFile is specified, - * it is assumed that that file is handled from affected file list. If targetSourceFile is not specified, - * it will only emit all the affected files instead of whole program - * - * The first of writeFile if provided, writeFile of BuilderProgramHost if provided, writeFile of compiler host - * in that order would be used to write the files - */ - function emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult { - if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram) { - assertSourceFileOkWithoutNextAffectedCall(state, targetSourceFile); - const result = handleNoEmitOptions(builderProgram, targetSourceFile, cancellationToken); - if (result) return result; - if (!targetSourceFile) { - // Emit and report any errors we ran into. - let sourceMaps: SourceMapEmitResult[] = []; - let emitSkipped = false; - let diagnostics: Diagnostic[] | undefined; - let emittedFiles: string[] = []; - - let affectedEmitResult: AffectedFileResult; - while (affectedEmitResult = emitNextAffectedFile(writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers)) { - emitSkipped = emitSkipped || affectedEmitResult.result.emitSkipped; - diagnostics = addRange(diagnostics, affectedEmitResult.result.diagnostics); - emittedFiles = addRange(emittedFiles, affectedEmitResult.result.emittedFiles); - sourceMaps = addRange(sourceMaps, affectedEmitResult.result.sourceMaps); - } - return { - emitSkipped, - diagnostics: diagnostics || emptyArray, - emittedFiles, - sourceMaps - }; - } + else { + const program = Debug.checkDefined(state.program); + if (state.programEmitComplete) + return undefined; + affected = program; } - return Debug.checkDefined(state.program).emit(targetSourceFile, writeFile || maybeBind(host, host.writeFile), cancellationToken, emitOnlyDtsFiles, customTransformers); } - - /** - * Return the semantic diagnostics for the next affected file or undefined if iteration is complete - * If provided ignoreSourceFile would be called before getting the diagnostics and would ignore the sourceFile if the returned value was true - */ - function getSemanticDiagnosticsOfNextAffectedFile(cancellationToken?: CancellationToken, ignoreSourceFile?: (sourceFile: SourceFile) => boolean): AffectedFileResult { - while (true) { - const affected = getNextAffectedFile(state, cancellationToken, computeHash); - if (!affected) { - // Done - return undefined; - } - else if (affected === state.program) { - // When whole program is affected, get all semantic diagnostics (eg when --out or --outFile is specified) - return toAffectedFileResult( - state, - state.program.getSemanticDiagnostics(/*targetSourceFile*/ undefined, cancellationToken), - affected - ); - } - - // Add file to affected file pending emit to handle for later emit time - if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram) { - addToAffectedFilesPendingEmit(state, (affected as SourceFile).resolvedPath, BuilderFileEmit.Full); - } - - // Get diagnostics for the affected file if its not ignored - if (ignoreSourceFile && ignoreSourceFile(affected as SourceFile)) { - // Get next affected file - doneWithAffectedFile(state, affected); - continue; + return toAffectedFileEmitResult(state, + // When whole program is affected, do emit only once (eg when --out or --outFile is specified) + // Otherwise just affected file + Debug.checkDefined(state.program).emit(affected === state.program ? undefined : affected as SourceFile, writeFile || maybeBind(host, host.writeFile), cancellationToken, emitOnlyDtsFiles || emitKind === BuilderFileEmit.DtsOnly, customTransformers), affected, emitKind, isPendingEmitFile); + } + /** + * Emits the JavaScript and declaration files. + * When targetSource file is specified, emits the files corresponding to that source file, + * otherwise for the whole program. + * In case of EmitAndSemanticDiagnosticsBuilderProgram, when targetSourceFile is specified, + * it is assumed that that file is handled from affected file list. If targetSourceFile is not specified, + * it will only emit all the affected files instead of whole program + * + * The first of writeFile if provided, writeFile of BuilderProgramHost if provided, writeFile of compiler host + * in that order would be used to write the files + */ + function emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult { + if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram) { + assertSourceFileOkWithoutNextAffectedCall(state, targetSourceFile); + const result = handleNoEmitOptions(builderProgram, targetSourceFile, cancellationToken); + if (result) + return result; + if (!targetSourceFile) { + // Emit and report any errors we ran into. + let sourceMaps: SourceMapEmitResult[] = []; + let emitSkipped = false; + let diagnostics: Diagnostic[] | undefined; + let emittedFiles: string[] = []; + let affectedEmitResult: AffectedFileResult; + while (affectedEmitResult = emitNextAffectedFile(writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers)) { + emitSkipped = emitSkipped || affectedEmitResult.result.emitSkipped; + diagnostics = addRange(diagnostics, affectedEmitResult.result.diagnostics); + emittedFiles = addRange(emittedFiles, affectedEmitResult.result.emittedFiles); + sourceMaps = addRange(sourceMaps, affectedEmitResult.result.sourceMaps); } - - return toAffectedFileResult( - state, - getSemanticDiagnosticsOfFile(state, affected as SourceFile, cancellationToken), - affected - ); + return { + emitSkipped, + diagnostics: diagnostics || emptyArray, + emittedFiles, + sourceMaps + }; } } - - /** - * Gets the semantic diagnostics from the program corresponding to this state of file (if provided) or whole program - * The semantic diagnostics are cached and managed here - * Note that it is assumed that when asked about semantic diagnostics through this API, - * the file has been taken out of affected files so it is safe to use cache or get from program and cache the diagnostics - * In case of SemanticDiagnosticsBuilderProgram if the source file is not provided, - * it will iterate through all the affected files, to ensure that cache stays valid and yet provide a way to get all semantic diagnostics - */ - function getSemanticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[] { - assertSourceFileOkWithoutNextAffectedCall(state, sourceFile); - const compilerOptions = Debug.checkDefined(state.program).getCompilerOptions(); - if (compilerOptions.outFile || compilerOptions.out) { - Debug.assert(!state.semanticDiagnosticsPerFile); - // We dont need to cache the diagnostics just return them from program - return Debug.checkDefined(state.program).getSemanticDiagnostics(sourceFile, cancellationToken); + return Debug.checkDefined(state.program).emit(targetSourceFile, writeFile || maybeBind(host, host.writeFile), cancellationToken, emitOnlyDtsFiles, customTransformers); + } + /** + * Return the semantic diagnostics for the next affected file or undefined if iteration is complete + * If provided ignoreSourceFile would be called before getting the diagnostics and would ignore the sourceFile if the returned value was true + */ + function getSemanticDiagnosticsOfNextAffectedFile(cancellationToken?: CancellationToken, ignoreSourceFile?: (sourceFile: SourceFile) => boolean): AffectedFileResult { + while (true) { + const affected = getNextAffectedFile(state, cancellationToken, computeHash); + if (!affected) { + // Done + return undefined; } - - if (sourceFile) { - return getSemanticDiagnosticsOfFile(state, sourceFile, cancellationToken); + else if (affected === state.program) { + // When whole program is affected, get all semantic diagnostics (eg when --out or --outFile is specified) + return toAffectedFileResult(state, state.program.getSemanticDiagnostics(/*targetSourceFile*/ undefined, cancellationToken), affected); } - - // When semantic builder asks for diagnostics of the whole program, - // ensure that all the affected files are handled - // eslint-disable-next-line no-empty - while (getSemanticDiagnosticsOfNextAffectedFile(cancellationToken)) { + // Add file to affected file pending emit to handle for later emit time + if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram) { + addToAffectedFilesPendingEmit(state, (affected as SourceFile).resolvedPath, BuilderFileEmit.Full); } - - let diagnostics: Diagnostic[] | undefined; - for (const sourceFile of Debug.checkDefined(state.program).getSourceFiles()) { - diagnostics = addRange(diagnostics, getSemanticDiagnosticsOfFile(state, sourceFile, cancellationToken)); + // Get diagnostics for the affected file if its not ignored + if (ignoreSourceFile && ignoreSourceFile((affected as SourceFile))) { + // Get next affected file + doneWithAffectedFile(state, affected); + continue; } - return diagnostics || emptyArray; + return toAffectedFileResult(state, getSemanticDiagnosticsOfFile(state, (affected as SourceFile), cancellationToken), affected); } } - - function addToAffectedFilesPendingEmit(state: BuilderProgramState, affectedFilePendingEmit: Path, kind: BuilderFileEmit) { - if (!state.affectedFilesPendingEmit) state.affectedFilesPendingEmit = []; - if (!state.affectedFilesPendingEmitKind) state.affectedFilesPendingEmitKind = createMap(); - - const existingKind = state.affectedFilesPendingEmitKind.get(affectedFilePendingEmit); - state.affectedFilesPendingEmit.push(affectedFilePendingEmit); - state.affectedFilesPendingEmitKind.set(affectedFilePendingEmit, existingKind || kind); - - // affectedFilesPendingEmitIndex === undefined - // - means the emit state.affectedFilesPendingEmit was undefined before adding current affected files - // so start from 0 as array would be affectedFilesPendingEmit - // else, continue to iterate from existing index, the current set is appended to existing files - if (state.affectedFilesPendingEmitIndex === undefined) { - state.affectedFilesPendingEmitIndex = 0; + /** + * Gets the semantic diagnostics from the program corresponding to this state of file (if provided) or whole program + * The semantic diagnostics are cached and managed here + * Note that it is assumed that when asked about semantic diagnostics through this API, + * the file has been taken out of affected files so it is safe to use cache or get from program and cache the diagnostics + * In case of SemanticDiagnosticsBuilderProgram if the source file is not provided, + * it will iterate through all the affected files, to ensure that cache stays valid and yet provide a way to get all semantic diagnostics + */ + function getSemanticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[] { + assertSourceFileOkWithoutNextAffectedCall(state, sourceFile); + const compilerOptions = Debug.checkDefined(state.program).getCompilerOptions(); + if (compilerOptions.outFile || compilerOptions.out) { + Debug.assert(!state.semanticDiagnosticsPerFile); + // We dont need to cache the diagnostics just return them from program + return Debug.checkDefined(state.program).getSemanticDiagnostics(sourceFile, cancellationToken); } - } - - function getMapOfReferencedSet(mapLike: MapLike | undefined, toPath: (path: string) => Path): ReadonlyMap | undefined { - if (!mapLike) return undefined; - const map = createMap(); - // Copies keys/values from template. Note that for..in will not throw if - // template is undefined, and instead will just exit the loop. - for (const key in mapLike) { - if (hasProperty(mapLike, key)) { - map.set(toPath(key), arrayToSet(mapLike[key], toPath)); - } + if (sourceFile) { + return getSemanticDiagnosticsOfFile(state, sourceFile, cancellationToken); } - return map; - } - - export function createBuildProgramUsingProgramBuildInfo(program: ProgramBuildInfo, buildInfoPath: string, host: ReadBuildProgramHost): EmitAndSemanticDiagnosticsBuilderProgram { - const buildInfoDirectory = getDirectoryPath(getNormalizedAbsolutePath(buildInfoPath, host.getCurrentDirectory())); - const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames()); - - const fileInfos = createMap(); - for (const key in program.fileInfos) { - if (hasProperty(program.fileInfos, key)) { - fileInfos.set(toPath(key), program.fileInfos[key]); - } + // When semantic builder asks for diagnostics of the whole program, + // ensure that all the affected files are handled + // eslint-disable-next-line no-empty + while (getSemanticDiagnosticsOfNextAffectedFile(cancellationToken)) { } - - const state: ReusableBuilderProgramState = { - fileInfos, - compilerOptions: convertToOptionsWithAbsolutePaths(program.options, toAbsolutePath), - referencedMap: getMapOfReferencedSet(program.referencedMap, toPath), - exportedModulesMap: getMapOfReferencedSet(program.exportedModulesMap, toPath), - semanticDiagnosticsPerFile: program.semanticDiagnosticsPerFile && arrayToMap(program.semanticDiagnosticsPerFile, value => toPath(isString(value) ? value : value[0]), value => isString(value) ? emptyArray : value[1]), - hasReusableDiagnostic: true - }; - return { - getState: () => state, - backupState: noop, - restoreState: noop, - getProgram: notImplemented, - getProgramOrUndefined: returnUndefined, - releaseProgram: noop, - getCompilerOptions: () => state.compilerOptions, - getSourceFile: notImplemented, - getSourceFiles: notImplemented, - getOptionsDiagnostics: notImplemented, - getGlobalDiagnostics: notImplemented, - getConfigFileParsingDiagnostics: notImplemented, - getSyntacticDiagnostics: notImplemented, - getDeclarationDiagnostics: notImplemented, - getSemanticDiagnostics: notImplemented, - emit: notImplemented, - getAllDependencies: notImplemented, - getCurrentDirectory: notImplemented, - emitNextAffectedFile: notImplemented, - getSemanticDiagnosticsOfNextAffectedFile: notImplemented, - close: noop, - }; - - function toPath(path: string) { - return ts.toPath(path, buildInfoDirectory, getCanonicalFileName); + let diagnostics: Diagnostic[] | undefined; + for (const sourceFile of Debug.checkDefined(state.program).getSourceFiles()) { + diagnostics = addRange(diagnostics, getSemanticDiagnosticsOfFile(state, sourceFile, cancellationToken)); } - - function toAbsolutePath(path: string) { - return getNormalizedAbsolutePath(path, buildInfoDirectory); + return diagnostics || emptyArray; + } +} +/* @internal */ +function addToAffectedFilesPendingEmit(state: BuilderProgramState, affectedFilePendingEmit: Path, kind: BuilderFileEmit) { + if (!state.affectedFilesPendingEmit) + state.affectedFilesPendingEmit = []; + if (!state.affectedFilesPendingEmitKind) + state.affectedFilesPendingEmitKind = createMap(); + const existingKind = state.affectedFilesPendingEmitKind.get(affectedFilePendingEmit); + state.affectedFilesPendingEmit.push(affectedFilePendingEmit); + state.affectedFilesPendingEmitKind.set(affectedFilePendingEmit, existingKind || kind); + // affectedFilesPendingEmitIndex === undefined + // - means the emit state.affectedFilesPendingEmit was undefined before adding current affected files + // so start from 0 as array would be affectedFilesPendingEmit + // else, continue to iterate from existing index, the current set is appended to existing files + if (state.affectedFilesPendingEmitIndex === undefined) { + state.affectedFilesPendingEmitIndex = 0; + } +} +/* @internal */ +function getMapOfReferencedSet(mapLike: MapLike | undefined, toPath: (path: string) => Path): ts.ReadonlyMap | undefined { + if (!mapLike) + return undefined; + const map = createMap(); + // Copies keys/values from template. Note that for..in will not throw if + // template is undefined, and instead will just exit the loop. + for (const key in mapLike) { + if (hasProperty(mapLike, key)) { + map.set(toPath(key), arrayToSet(mapLike[key], toPath)); } } - - export function createRedirectedBuilderProgram(state: { program: Program | undefined; compilerOptions: CompilerOptions; }, configFileParsingDiagnostics: readonly Diagnostic[]): BuilderProgram { - return { - getState: notImplemented, - backupState: noop, - restoreState: noop, - getProgram, - getProgramOrUndefined: () => state.program, - releaseProgram: () => state.program = undefined, - getCompilerOptions: () => state.compilerOptions, - getSourceFile: fileName => getProgram().getSourceFile(fileName), - getSourceFiles: () => getProgram().getSourceFiles(), - getOptionsDiagnostics: cancellationToken => getProgram().getOptionsDiagnostics(cancellationToken), - getGlobalDiagnostics: cancellationToken => getProgram().getGlobalDiagnostics(cancellationToken), - getConfigFileParsingDiagnostics: () => configFileParsingDiagnostics, - getSyntacticDiagnostics: (sourceFile, cancellationToken) => getProgram().getSyntacticDiagnostics(sourceFile, cancellationToken), - getDeclarationDiagnostics: (sourceFile, cancellationToken) => getProgram().getDeclarationDiagnostics(sourceFile, cancellationToken), - getSemanticDiagnostics: (sourceFile, cancellationToken) => getProgram().getSemanticDiagnostics(sourceFile, cancellationToken), - emit: (sourceFile, writeFile, cancellationToken, emitOnlyDts, customTransformers) => getProgram().emit(sourceFile, writeFile, cancellationToken, emitOnlyDts, customTransformers), - getAllDependencies: notImplemented, - getCurrentDirectory: () => getProgram().getCurrentDirectory(), - close: noop, - }; - - function getProgram() { - return Debug.checkDefined(state.program); + return map; +} +/* @internal */ +export function createBuildProgramUsingProgramBuildInfo(program: ProgramBuildInfo, buildInfoPath: string, host: ReadBuildProgramHost): EmitAndSemanticDiagnosticsBuilderProgram { + const buildInfoDirectory = getDirectoryPath(getNormalizedAbsolutePath(buildInfoPath, host.getCurrentDirectory())); + const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames()); + const fileInfos = createMap(); + for (const key in program.fileInfos) { + if (hasProperty(program.fileInfos, key)) { + fileInfos.set(toPath(key), program.fileInfos[key]); } } + const state: ReusableBuilderProgramState = { + fileInfos, + compilerOptions: convertToOptionsWithAbsolutePaths(program.options, toAbsolutePath), + referencedMap: getMapOfReferencedSet(program.referencedMap, toPath), + exportedModulesMap: getMapOfReferencedSet(program.exportedModulesMap, toPath), + semanticDiagnosticsPerFile: program.semanticDiagnosticsPerFile && arrayToMap(program.semanticDiagnosticsPerFile, value => toPath(isString(value) ? value : value[0]), value => isString(value) ? emptyArray : value[1]), + hasReusableDiagnostic: true + }; + return { + getState: () => state, + backupState: noop, + restoreState: noop, + getProgram: notImplemented, + getProgramOrUndefined: returnUndefined, + releaseProgram: noop, + getCompilerOptions: () => state.compilerOptions, + getSourceFile: notImplemented, + getSourceFiles: notImplemented, + getOptionsDiagnostics: notImplemented, + getGlobalDiagnostics: notImplemented, + getConfigFileParsingDiagnostics: notImplemented, + getSyntacticDiagnostics: notImplemented, + getDeclarationDiagnostics: notImplemented, + getSemanticDiagnostics: notImplemented, + emit: notImplemented, + getAllDependencies: notImplemented, + getCurrentDirectory: notImplemented, + emitNextAffectedFile: notImplemented, + getSemanticDiagnosticsOfNextAffectedFile: notImplemented, + close: noop, + }; + function toPath(path: string) { + return ts.toPath(path, buildInfoDirectory, getCanonicalFileName); + } + function toAbsolutePath(path: string) { + return getNormalizedAbsolutePath(path, buildInfoDirectory); + } +} +/* @internal */ +export function createRedirectedBuilderProgram(state: { + program: Program | undefined; + compilerOptions: CompilerOptions; +}, configFileParsingDiagnostics: readonly Diagnostic[]): BuilderProgram { + return { + getState: notImplemented, + backupState: noop, + restoreState: noop, + getProgram, + getProgramOrUndefined: () => state.program, + releaseProgram: () => state.program = undefined, + getCompilerOptions: () => state.compilerOptions, + getSourceFile: fileName => getProgram().getSourceFile(fileName), + getSourceFiles: () => getProgram().getSourceFiles(), + getOptionsDiagnostics: cancellationToken => getProgram().getOptionsDiagnostics(cancellationToken), + getGlobalDiagnostics: cancellationToken => getProgram().getGlobalDiagnostics(cancellationToken), + getConfigFileParsingDiagnostics: () => configFileParsingDiagnostics, + getSyntacticDiagnostics: (sourceFile, cancellationToken) => getProgram().getSyntacticDiagnostics(sourceFile, cancellationToken), + getDeclarationDiagnostics: (sourceFile, cancellationToken) => getProgram().getDeclarationDiagnostics(sourceFile, cancellationToken), + getSemanticDiagnostics: (sourceFile, cancellationToken) => getProgram().getSemanticDiagnostics(sourceFile, cancellationToken), + emit: (sourceFile, writeFile, cancellationToken, emitOnlyDts, customTransformers) => getProgram().emit(sourceFile, writeFile, cancellationToken, emitOnlyDts, customTransformers), + getAllDependencies: notImplemented, + getCurrentDirectory: () => getProgram().getCurrentDirectory(), + close: noop, + }; + function getProgram() { + return Debug.checkDefined(state.program); + } } diff --git a/src/compiler/builderPublic.ts b/src/compiler/builderPublic.ts index bff123b915cb5..ae5fbd50448f2 100644 --- a/src/compiler/builderPublic.ts +++ b/src/compiler/builderPublic.ts @@ -1,162 +1,156 @@ -namespace ts { - export type AffectedFileResult = { result: T; affected: SourceFile | Program; } | undefined; - - export interface BuilderProgramHost { - /** - * return true if file names are treated with case sensitivity - */ - useCaseSensitiveFileNames(): boolean; - /** - * If provided this would be used this hash instead of actual file shape text for detecting changes - */ - createHash?: (data: string) => string; - /** - * When emit or emitNextAffectedFile are called without writeFile, - * this callback if present would be used to write files - */ - writeFile?: WriteFileCallback; - } - - /** - * Builder to manage the program state changes - */ - export interface BuilderProgram { - /*@internal*/ - getState(): ReusableBuilderProgramState; - /*@internal*/ - backupState(): void; - /*@internal*/ - restoreState(): void; - /** - * Returns current program - */ - getProgram(): Program; - /** - * Returns current program that could be undefined if the program was released - */ - /*@internal*/ - getProgramOrUndefined(): Program | undefined; - /** - * Releases reference to the program, making all the other operations that need program to fail. - */ - /*@internal*/ - releaseProgram(): void; - /** - * Get compiler options of the program - */ - getCompilerOptions(): CompilerOptions; - /** - * Get the source file in the program with file name - */ - getSourceFile(fileName: string): SourceFile | undefined; - /** - * Get a list of files in the program - */ - getSourceFiles(): readonly SourceFile[]; - /** - * Get the diagnostics for compiler options - */ - getOptionsDiagnostics(cancellationToken?: CancellationToken): readonly Diagnostic[]; - /** - * Get the diagnostics that dont belong to any file - */ - getGlobalDiagnostics(cancellationToken?: CancellationToken): readonly Diagnostic[]; - /** - * Get the diagnostics from config file parsing - */ - getConfigFileParsingDiagnostics(): readonly Diagnostic[]; - /** - * Get the syntax diagnostics, for all source files if source file is not supplied - */ - getSyntacticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[]; - /** - * Get the declaration diagnostics, for all source files if source file is not supplied - */ - getDeclarationDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly DiagnosticWithLocation[]; - /** - * Get all the dependencies of the file - */ - getAllDependencies(sourceFile: SourceFile): readonly string[]; - - /** - * Gets the semantic diagnostics from the program corresponding to this state of file (if provided) or whole program - * The semantic diagnostics are cached and managed here - * Note that it is assumed that when asked about semantic diagnostics through this API, - * the file has been taken out of affected files so it is safe to use cache or get from program and cache the diagnostics - * In case of SemanticDiagnosticsBuilderProgram if the source file is not provided, - * it will iterate through all the affected files, to ensure that cache stays valid and yet provide a way to get all semantic diagnostics - */ - getSemanticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[]; - /** - * Emits the JavaScript and declaration files. - * When targetSource file is specified, emits the files corresponding to that source file, - * otherwise for the whole program. - * In case of EmitAndSemanticDiagnosticsBuilderProgram, when targetSourceFile is specified, - * it is assumed that that file is handled from affected file list. If targetSourceFile is not specified, - * it will only emit all the affected files instead of whole program - * - * The first of writeFile if provided, writeFile of BuilderProgramHost if provided, writeFile of compiler host - * in that order would be used to write the files - */ - emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult; - /** - * Get the current directory of the program - */ - getCurrentDirectory(): string; - /*@internal*/ - close(): void; - } - - /** - * The builder that caches the semantic diagnostics for the program and handles the changed files and affected files - */ - export interface SemanticDiagnosticsBuilderProgram extends BuilderProgram { - /** - * Gets the semantic diagnostics from the program for the next affected file and caches it - * Returns undefined if the iteration is complete - */ - getSemanticDiagnosticsOfNextAffectedFile(cancellationToken?: CancellationToken, ignoreSourceFile?: (sourceFile: SourceFile) => boolean): AffectedFileResult; - } - - /** - * The builder that can handle the changes in program and iterate through changed file to emit the files - * The semantic diagnostics are cached per file and managed by clearing for the changed/affected files - */ - export interface EmitAndSemanticDiagnosticsBuilderProgram extends SemanticDiagnosticsBuilderProgram { - /** - * Emits the next affected file's emit result (EmitResult and sourceFiles emitted) or returns undefined if iteration is complete - * The first of writeFile if provided, writeFile of BuilderProgramHost if provided, writeFile of compiler host - * in that order would be used to write the files - */ - emitNextAffectedFile(writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): AffectedFileResult; - } - - /** - * Create the builder to manage semantic diagnostics and cache them - */ - export function createSemanticDiagnosticsBuilderProgram(newProgram: Program, host: BuilderProgramHost, oldProgram?: SemanticDiagnosticsBuilderProgram, configFileParsingDiagnostics?: readonly Diagnostic[]): SemanticDiagnosticsBuilderProgram; - export function createSemanticDiagnosticsBuilderProgram(rootNames: readonly string[] | undefined, options: CompilerOptions | undefined, host?: CompilerHost, oldProgram?: SemanticDiagnosticsBuilderProgram, configFileParsingDiagnostics?: readonly Diagnostic[], projectReferences?: readonly ProjectReference[]): SemanticDiagnosticsBuilderProgram; - export function createSemanticDiagnosticsBuilderProgram(newProgramOrRootNames: Program | readonly string[] | undefined, hostOrOptions: BuilderProgramHost | CompilerOptions | undefined, oldProgramOrHost?: CompilerHost | SemanticDiagnosticsBuilderProgram, configFileParsingDiagnosticsOrOldProgram?: readonly Diagnostic[] | SemanticDiagnosticsBuilderProgram, configFileParsingDiagnostics?: readonly Diagnostic[], projectReferences?: readonly ProjectReference[]) { - return createBuilderProgram(BuilderProgramKind.SemanticDiagnosticsBuilderProgram, getBuilderCreationParameters(newProgramOrRootNames, hostOrOptions, oldProgramOrHost, configFileParsingDiagnosticsOrOldProgram, configFileParsingDiagnostics, projectReferences)); - } - - /** - * Create the builder that can handle the changes in program and iterate through changed files - * to emit the those files and manage semantic diagnostics cache as well - */ - export function createEmitAndSemanticDiagnosticsBuilderProgram(newProgram: Program, host: BuilderProgramHost, oldProgram?: EmitAndSemanticDiagnosticsBuilderProgram, configFileParsingDiagnostics?: readonly Diagnostic[]): EmitAndSemanticDiagnosticsBuilderProgram; - export function createEmitAndSemanticDiagnosticsBuilderProgram(rootNames: readonly string[] | undefined, options: CompilerOptions | undefined, host?: CompilerHost, oldProgram?: EmitAndSemanticDiagnosticsBuilderProgram, configFileParsingDiagnostics?: readonly Diagnostic[], projectReferences?: readonly ProjectReference[]): EmitAndSemanticDiagnosticsBuilderProgram; - export function createEmitAndSemanticDiagnosticsBuilderProgram(newProgramOrRootNames: Program | readonly string[] | undefined, hostOrOptions: BuilderProgramHost | CompilerOptions | undefined, oldProgramOrHost?: CompilerHost | EmitAndSemanticDiagnosticsBuilderProgram, configFileParsingDiagnosticsOrOldProgram?: readonly Diagnostic[] | EmitAndSemanticDiagnosticsBuilderProgram, configFileParsingDiagnostics?: readonly Diagnostic[], projectReferences?: readonly ProjectReference[]) { - return createBuilderProgram(BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram, getBuilderCreationParameters(newProgramOrRootNames, hostOrOptions, oldProgramOrHost, configFileParsingDiagnosticsOrOldProgram, configFileParsingDiagnostics, projectReferences)); - } - - /** - * Creates a builder thats just abstraction over program and can be used with watch - */ - export function createAbstractBuilder(newProgram: Program, host: BuilderProgramHost, oldProgram?: BuilderProgram, configFileParsingDiagnostics?: readonly Diagnostic[]): BuilderProgram; - export function createAbstractBuilder(rootNames: readonly string[] | undefined, options: CompilerOptions | undefined, host?: CompilerHost, oldProgram?: BuilderProgram, configFileParsingDiagnostics?: readonly Diagnostic[], projectReferences?: readonly ProjectReference[]): BuilderProgram; - export function createAbstractBuilder(newProgramOrRootNames: Program | readonly string[] | undefined, hostOrOptions: BuilderProgramHost | CompilerOptions | undefined, oldProgramOrHost?: CompilerHost | BuilderProgram, configFileParsingDiagnosticsOrOldProgram?: readonly Diagnostic[] | BuilderProgram, configFileParsingDiagnostics?: readonly Diagnostic[], projectReferences?: readonly ProjectReference[]): BuilderProgram { - const { newProgram, configFileParsingDiagnostics: newConfigFileParsingDiagnostics } = getBuilderCreationParameters(newProgramOrRootNames, hostOrOptions, oldProgramOrHost, configFileParsingDiagnosticsOrOldProgram, configFileParsingDiagnostics, projectReferences); - return createRedirectedBuilderProgram({ program: newProgram, compilerOptions: newProgram.getCompilerOptions() }, newConfigFileParsingDiagnostics); - } +import { SourceFile, Program, WriteFileCallback, ReusableBuilderProgramState, CompilerOptions, CancellationToken, Diagnostic, DiagnosticWithLocation, CustomTransformers, EmitResult, CompilerHost, ProjectReference, createBuilderProgram, BuilderProgramKind, getBuilderCreationParameters, createRedirectedBuilderProgram } from "./ts"; +export type AffectedFileResult = { + result: T; + affected: SourceFile | Program; +} | undefined; +export interface BuilderProgramHost { + /** + * return true if file names are treated with case sensitivity + */ + useCaseSensitiveFileNames(): boolean; + /** + * If provided this would be used this hash instead of actual file shape text for detecting changes + */ + createHash?: (data: string) => string; + /** + * When emit or emitNextAffectedFile are called without writeFile, + * this callback if present would be used to write files + */ + writeFile?: WriteFileCallback; +} +/** + * Builder to manage the program state changes + */ +export interface BuilderProgram { + /*@internal*/ + getState(): ReusableBuilderProgramState; + /*@internal*/ + backupState(): void; + /*@internal*/ + restoreState(): void; + /** + * Returns current program + */ + getProgram(): Program; + /** + * Returns current program that could be undefined if the program was released + */ + /*@internal*/ + getProgramOrUndefined(): Program | undefined; + /** + * Releases reference to the program, making all the other operations that need program to fail. + */ + /*@internal*/ + releaseProgram(): void; + /** + * Get compiler options of the program + */ + getCompilerOptions(): CompilerOptions; + /** + * Get the source file in the program with file name + */ + getSourceFile(fileName: string): SourceFile | undefined; + /** + * Get a list of files in the program + */ + getSourceFiles(): readonly SourceFile[]; + /** + * Get the diagnostics for compiler options + */ + getOptionsDiagnostics(cancellationToken?: CancellationToken): readonly Diagnostic[]; + /** + * Get the diagnostics that dont belong to any file + */ + getGlobalDiagnostics(cancellationToken?: CancellationToken): readonly Diagnostic[]; + /** + * Get the diagnostics from config file parsing + */ + getConfigFileParsingDiagnostics(): readonly Diagnostic[]; + /** + * Get the syntax diagnostics, for all source files if source file is not supplied + */ + getSyntacticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[]; + /** + * Get the declaration diagnostics, for all source files if source file is not supplied + */ + getDeclarationDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly DiagnosticWithLocation[]; + /** + * Get all the dependencies of the file + */ + getAllDependencies(sourceFile: SourceFile): readonly string[]; + /** + * Gets the semantic diagnostics from the program corresponding to this state of file (if provided) or whole program + * The semantic diagnostics are cached and managed here + * Note that it is assumed that when asked about semantic diagnostics through this API, + * the file has been taken out of affected files so it is safe to use cache or get from program and cache the diagnostics + * In case of SemanticDiagnosticsBuilderProgram if the source file is not provided, + * it will iterate through all the affected files, to ensure that cache stays valid and yet provide a way to get all semantic diagnostics + */ + getSemanticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[]; + /** + * Emits the JavaScript and declaration files. + * When targetSource file is specified, emits the files corresponding to that source file, + * otherwise for the whole program. + * In case of EmitAndSemanticDiagnosticsBuilderProgram, when targetSourceFile is specified, + * it is assumed that that file is handled from affected file list. If targetSourceFile is not specified, + * it will only emit all the affected files instead of whole program + * + * The first of writeFile if provided, writeFile of BuilderProgramHost if provided, writeFile of compiler host + * in that order would be used to write the files + */ + emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult; + /** + * Get the current directory of the program + */ + getCurrentDirectory(): string; + /*@internal*/ + close(): void; +} +/** + * The builder that caches the semantic diagnostics for the program and handles the changed files and affected files + */ +export interface SemanticDiagnosticsBuilderProgram extends BuilderProgram { + /** + * Gets the semantic diagnostics from the program for the next affected file and caches it + * Returns undefined if the iteration is complete + */ + getSemanticDiagnosticsOfNextAffectedFile(cancellationToken?: CancellationToken, ignoreSourceFile?: (sourceFile: SourceFile) => boolean): AffectedFileResult; +} +/** + * The builder that can handle the changes in program and iterate through changed file to emit the files + * The semantic diagnostics are cached per file and managed by clearing for the changed/affected files + */ +export interface EmitAndSemanticDiagnosticsBuilderProgram extends SemanticDiagnosticsBuilderProgram { + /** + * Emits the next affected file's emit result (EmitResult and sourceFiles emitted) or returns undefined if iteration is complete + * The first of writeFile if provided, writeFile of BuilderProgramHost if provided, writeFile of compiler host + * in that order would be used to write the files + */ + emitNextAffectedFile(writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): AffectedFileResult; +} +/** + * Create the builder to manage semantic diagnostics and cache them + */ +export function createSemanticDiagnosticsBuilderProgram(newProgram: Program, host: BuilderProgramHost, oldProgram?: SemanticDiagnosticsBuilderProgram, configFileParsingDiagnostics?: readonly Diagnostic[]): SemanticDiagnosticsBuilderProgram; +export function createSemanticDiagnosticsBuilderProgram(rootNames: readonly string[] | undefined, options: CompilerOptions | undefined, host?: CompilerHost, oldProgram?: SemanticDiagnosticsBuilderProgram, configFileParsingDiagnostics?: readonly Diagnostic[], projectReferences?: readonly ProjectReference[]): SemanticDiagnosticsBuilderProgram; +export function createSemanticDiagnosticsBuilderProgram(newProgramOrRootNames: Program | readonly string[] | undefined, hostOrOptions: BuilderProgramHost | CompilerOptions | undefined, oldProgramOrHost?: CompilerHost | SemanticDiagnosticsBuilderProgram, configFileParsingDiagnosticsOrOldProgram?: readonly Diagnostic[] | SemanticDiagnosticsBuilderProgram, configFileParsingDiagnostics?: readonly Diagnostic[], projectReferences?: readonly ProjectReference[]) { + return createBuilderProgram(BuilderProgramKind.SemanticDiagnosticsBuilderProgram, getBuilderCreationParameters(newProgramOrRootNames, hostOrOptions, oldProgramOrHost, configFileParsingDiagnosticsOrOldProgram, configFileParsingDiagnostics, projectReferences)); +} +/** + * Create the builder that can handle the changes in program and iterate through changed files + * to emit the those files and manage semantic diagnostics cache as well + */ +export function createEmitAndSemanticDiagnosticsBuilderProgram(newProgram: Program, host: BuilderProgramHost, oldProgram?: EmitAndSemanticDiagnosticsBuilderProgram, configFileParsingDiagnostics?: readonly Diagnostic[]): EmitAndSemanticDiagnosticsBuilderProgram; +export function createEmitAndSemanticDiagnosticsBuilderProgram(rootNames: readonly string[] | undefined, options: CompilerOptions | undefined, host?: CompilerHost, oldProgram?: EmitAndSemanticDiagnosticsBuilderProgram, configFileParsingDiagnostics?: readonly Diagnostic[], projectReferences?: readonly ProjectReference[]): EmitAndSemanticDiagnosticsBuilderProgram; +export function createEmitAndSemanticDiagnosticsBuilderProgram(newProgramOrRootNames: Program | readonly string[] | undefined, hostOrOptions: BuilderProgramHost | CompilerOptions | undefined, oldProgramOrHost?: CompilerHost | EmitAndSemanticDiagnosticsBuilderProgram, configFileParsingDiagnosticsOrOldProgram?: readonly Diagnostic[] | EmitAndSemanticDiagnosticsBuilderProgram, configFileParsingDiagnostics?: readonly Diagnostic[], projectReferences?: readonly ProjectReference[]) { + return createBuilderProgram(BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram, getBuilderCreationParameters(newProgramOrRootNames, hostOrOptions, oldProgramOrHost, configFileParsingDiagnosticsOrOldProgram, configFileParsingDiagnostics, projectReferences)); +} +/** + * Creates a builder thats just abstraction over program and can be used with watch + */ +export function createAbstractBuilder(newProgram: Program, host: BuilderProgramHost, oldProgram?: BuilderProgram, configFileParsingDiagnostics?: readonly Diagnostic[]): BuilderProgram; +export function createAbstractBuilder(rootNames: readonly string[] | undefined, options: CompilerOptions | undefined, host?: CompilerHost, oldProgram?: BuilderProgram, configFileParsingDiagnostics?: readonly Diagnostic[], projectReferences?: readonly ProjectReference[]): BuilderProgram; +export function createAbstractBuilder(newProgramOrRootNames: Program | readonly string[] | undefined, hostOrOptions: BuilderProgramHost | CompilerOptions | undefined, oldProgramOrHost?: CompilerHost | BuilderProgram, configFileParsingDiagnosticsOrOldProgram?: readonly Diagnostic[] | BuilderProgram, configFileParsingDiagnostics?: readonly Diagnostic[], projectReferences?: readonly ProjectReference[]): BuilderProgram { + const { newProgram, configFileParsingDiagnostics: newConfigFileParsingDiagnostics } = getBuilderCreationParameters(newProgramOrRootNames, hostOrOptions, oldProgramOrHost, configFileParsingDiagnosticsOrOldProgram, configFileParsingDiagnostics, projectReferences); + return createRedirectedBuilderProgram({ program: newProgram, compilerOptions: newProgram.getCompilerOptions() }, newConfigFileParsingDiagnostics); } diff --git a/src/compiler/builderState.ts b/src/compiler/builderState.ts index 00f51cbedb6c8..979a1250600da 100644 --- a/src/compiler/builderState.ts +++ b/src/compiler/builderState.ts @@ -1,566 +1,510 @@ +import { Program, SourceFile, CancellationToken, CustomTransformers, EmitOutput, OutputFile, Symbol, getSourceFileOfNode, TypeChecker, StringLiteralLike, Path, GetCanonicalFileName, toPath, getDirectoryPath, isStringLiteral, createMap, ModuleKind, Debug, cloneMap, emptyArray, fileExtensionIs, Extension, getAnyExtensionFromPath, ExportedModulesFromDeclarationEmit, arrayFrom, mapDefinedIterator, isModuleWithStringLiteralName, some, isGlobalScopeAugmentation, ModuleDeclaration, isExternalModule } from "./ts"; +import * as ts from "./ts"; /*@internal*/ -namespace ts { - export function getFileEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, - cancellationToken?: CancellationToken, customTransformers?: CustomTransformers, forceDtsEmit?: boolean): EmitOutput { - const outputFiles: OutputFile[] = []; - const emitResult = program.emit(sourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers, forceDtsEmit); - return { outputFiles, emitSkipped: emitResult.emitSkipped, exportedModulesFromDeclarationEmit: emitResult.exportedModulesFromDeclarationEmit }; - - function writeFile(fileName: string, text: string, writeByteOrderMark: boolean) { - outputFiles.push({ name: fileName, writeByteOrderMark, text }); +export function getFileEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, cancellationToken?: CancellationToken, customTransformers?: CustomTransformers, forceDtsEmit?: boolean): EmitOutput { + const outputFiles: OutputFile[] = []; + const emitResult = program.emit(sourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers, forceDtsEmit); + return { outputFiles, emitSkipped: emitResult.emitSkipped, exportedModulesFromDeclarationEmit: emitResult.exportedModulesFromDeclarationEmit }; + function writeFile(fileName: string, text: string, writeByteOrderMark: boolean) { + outputFiles.push({ name: fileName, writeByteOrderMark, text }); + } +} +/* @internal */ +export interface ReusableBuilderState { + /** + * Information of the file eg. its version, signature etc + */ + fileInfos: ts.ReadonlyMap; + /** + * Contains the map of ReferencedSet=Referenced files of the file if module emit is enabled + * Otherwise undefined + * Thus non undefined value indicates, module emit + */ + readonly referencedMap?: ts.ReadonlyMap | undefined; + /** + * Contains the map of exported modules ReferencedSet=exported module files from the file if module emit is enabled + * Otherwise undefined + */ + readonly exportedModulesMap?: ts.ReadonlyMap | undefined; +} +/* @internal */ +export interface BuilderState { + /** + * Information of the file eg. its version, signature etc + */ + fileInfos: ts.Map; + /** + * Contains the map of ReferencedSet=Referenced files of the file if module emit is enabled + * Otherwise undefined + * Thus non undefined value indicates, module emit + */ + readonly referencedMap: ts.ReadonlyMap | undefined; + /** + * Contains the map of exported modules ReferencedSet=exported module files from the file if module emit is enabled + * Otherwise undefined + */ + readonly exportedModulesMap: ts.Map | undefined; + /** + * Map of files that have already called update signature. + * That means hence forth these files are assumed to have + * no change in their signature for this version of the program + */ + hasCalledUpdateShapeSignature: ts.Map; + /** + * Cache of all files excluding default library file for the current program + */ + allFilesExcludingDefaultLibraryFile?: readonly SourceFile[]; + /** + * Cache of all the file names + */ + allFileNames?: readonly string[]; +} +/* @internal */ +export namespace BuilderState { + /** + * Information about the source file: Its version and optional signature from last emit + */ + export interface FileInfo { + readonly version: string; + signature: string | undefined; + } + /** + * Referenced files with values for the keys as referenced file's path to be true + */ + export type ReferencedSet = ts.ReadonlyMap; + /** + * Compute the hash to store the shape of the file + */ + export type ComputeHash = (data: string) => string; + /** + * Exported modules to from declaration emit being computed. + * This can contain false in the affected file path to specify that there are no exported module(types from other modules) for this file + */ + export type ComputingExportedModulesMap = ts.Map; + /** + * Get the referencedFile from the imported module symbol + */ + function getReferencedFileFromImportedModuleSymbol(symbol: Symbol) { + if (symbol.declarations && symbol.declarations[0]) { + const declarationSourceFile = getSourceFileOfNode(symbol.declarations[0]); + return declarationSourceFile && declarationSourceFile.resolvedPath; } } - - export interface ReusableBuilderState { - /** - * Information of the file eg. its version, signature etc - */ - fileInfos: ReadonlyMap; - /** - * Contains the map of ReferencedSet=Referenced files of the file if module emit is enabled - * Otherwise undefined - * Thus non undefined value indicates, module emit - */ - readonly referencedMap?: ReadonlyMap | undefined; - /** - * Contains the map of exported modules ReferencedSet=exported module files from the file if module emit is enabled - * Otherwise undefined - */ - readonly exportedModulesMap?: ReadonlyMap | undefined; + /** + * Get the referencedFile from the import name node from file + */ + function getReferencedFileFromImportLiteral(checker: TypeChecker, importName: StringLiteralLike) { + const symbol = checker.getSymbolAtLocation(importName); + return symbol && getReferencedFileFromImportedModuleSymbol(symbol); } - - export interface BuilderState { - /** - * Information of the file eg. its version, signature etc - */ - fileInfos: Map; - /** - * Contains the map of ReferencedSet=Referenced files of the file if module emit is enabled - * Otherwise undefined - * Thus non undefined value indicates, module emit - */ - readonly referencedMap: ReadonlyMap | undefined; - /** - * Contains the map of exported modules ReferencedSet=exported module files from the file if module emit is enabled - * Otherwise undefined - */ - readonly exportedModulesMap: Map | undefined; - /** - * Map of files that have already called update signature. - * That means hence forth these files are assumed to have - * no change in their signature for this version of the program - */ - hasCalledUpdateShapeSignature: Map; - /** - * Cache of all files excluding default library file for the current program - */ - allFilesExcludingDefaultLibraryFile?: readonly SourceFile[]; - /** - * Cache of all the file names - */ - allFileNames?: readonly string[]; + /** + * Gets the path to reference file from file name, it could be resolvedPath if present otherwise path + */ + function getReferencedFileFromFileName(program: Program, fileName: string, sourceFileDirectory: Path, getCanonicalFileName: GetCanonicalFileName): Path { + return toPath(program.getProjectReferenceRedirect(fileName) || fileName, sourceFileDirectory, getCanonicalFileName); } - - export namespace BuilderState { - /** - * Information about the source file: Its version and optional signature from last emit - */ - export interface FileInfo { - readonly version: string; - signature: string | undefined; - } - /** - * Referenced files with values for the keys as referenced file's path to be true - */ - export type ReferencedSet = ReadonlyMap; - /** - * Compute the hash to store the shape of the file - */ - export type ComputeHash = (data: string) => string; - - /** - * Exported modules to from declaration emit being computed. - * This can contain false in the affected file path to specify that there are no exported module(types from other modules) for this file - */ - export type ComputingExportedModulesMap = Map; - - /** - * Get the referencedFile from the imported module symbol - */ - function getReferencedFileFromImportedModuleSymbol(symbol: Symbol) { - if (symbol.declarations && symbol.declarations[0]) { - const declarationSourceFile = getSourceFileOfNode(symbol.declarations[0]); - return declarationSourceFile && declarationSourceFile.resolvedPath; + /** + * Gets the referenced files for a file from the program with values for the keys as referenced file's path to be true + */ + function getReferencedFiles(program: Program, sourceFile: SourceFile, getCanonicalFileName: GetCanonicalFileName): ts.Map | undefined { + let referencedFiles: ts.Map | undefined; + // We need to use a set here since the code can contain the same import twice, + // but that will only be one dependency. + // To avoid invernal conversion, the key of the referencedFiles map must be of type Path + if (sourceFile.imports && sourceFile.imports.length > 0) { + const checker: TypeChecker = program.getTypeChecker(); + for (const importName of sourceFile.imports) { + const declarationSourceFilePath = getReferencedFileFromImportLiteral(checker, importName); + if (declarationSourceFilePath) { + addReferencedFile(declarationSourceFilePath); + } } } - - /** - * Get the referencedFile from the import name node from file - */ - function getReferencedFileFromImportLiteral(checker: TypeChecker, importName: StringLiteralLike) { - const symbol = checker.getSymbolAtLocation(importName); - return symbol && getReferencedFileFromImportedModuleSymbol(symbol); - } - - /** - * Gets the path to reference file from file name, it could be resolvedPath if present otherwise path - */ - function getReferencedFileFromFileName(program: Program, fileName: string, sourceFileDirectory: Path, getCanonicalFileName: GetCanonicalFileName): Path { - return toPath(program.getProjectReferenceRedirect(fileName) || fileName, sourceFileDirectory, getCanonicalFileName); - } - - /** - * Gets the referenced files for a file from the program with values for the keys as referenced file's path to be true - */ - function getReferencedFiles(program: Program, sourceFile: SourceFile, getCanonicalFileName: GetCanonicalFileName): Map | undefined { - let referencedFiles: Map | undefined; - - // We need to use a set here since the code can contain the same import twice, - // but that will only be one dependency. - // To avoid invernal conversion, the key of the referencedFiles map must be of type Path - if (sourceFile.imports && sourceFile.imports.length > 0) { - const checker: TypeChecker = program.getTypeChecker(); - for (const importName of sourceFile.imports) { - const declarationSourceFilePath = getReferencedFileFromImportLiteral(checker, importName); - if (declarationSourceFilePath) { - addReferencedFile(declarationSourceFilePath); - } - } + const sourceFileDirectory = getDirectoryPath(sourceFile.resolvedPath); + // Handle triple slash references + if (sourceFile.referencedFiles && sourceFile.referencedFiles.length > 0) { + for (const referencedFile of sourceFile.referencedFiles) { + const referencedPath = getReferencedFileFromFileName(program, referencedFile.fileName, sourceFileDirectory, getCanonicalFileName); + addReferencedFile(referencedPath); } - - const sourceFileDirectory = getDirectoryPath(sourceFile.resolvedPath); - // Handle triple slash references - if (sourceFile.referencedFiles && sourceFile.referencedFiles.length > 0) { - for (const referencedFile of sourceFile.referencedFiles) { - const referencedPath = getReferencedFileFromFileName(program, referencedFile.fileName, sourceFileDirectory, getCanonicalFileName); - addReferencedFile(referencedPath); + } + // Handle type reference directives + if (sourceFile.resolvedTypeReferenceDirectiveNames) { + sourceFile.resolvedTypeReferenceDirectiveNames.forEach((resolvedTypeReferenceDirective) => { + if (!resolvedTypeReferenceDirective) { + return; } - } - - // Handle type reference directives - if (sourceFile.resolvedTypeReferenceDirectiveNames) { - sourceFile.resolvedTypeReferenceDirectiveNames.forEach((resolvedTypeReferenceDirective) => { - if (!resolvedTypeReferenceDirective) { - return; - } - - const fileName = resolvedTypeReferenceDirective.resolvedFileName!; // TODO: GH#18217 - const typeFilePath = getReferencedFileFromFileName(program, fileName, sourceFileDirectory, getCanonicalFileName); - addReferencedFile(typeFilePath); - }); - } - - // Add module augmentation as references - if (sourceFile.moduleAugmentations.length) { - const checker = program.getTypeChecker(); - for (const moduleName of sourceFile.moduleAugmentations) { - if (!isStringLiteral(moduleName)) { continue; } - const symbol = checker.getSymbolAtLocation(moduleName); - if (!symbol) { continue; } - - // Add any file other than our own as reference - addReferenceFromAmbientModule(symbol); + const fileName = resolvedTypeReferenceDirective.resolvedFileName!; // TODO: GH#18217 + const typeFilePath = getReferencedFileFromFileName(program, fileName, sourceFileDirectory, getCanonicalFileName); + addReferencedFile(typeFilePath); + }); + } + // Add module augmentation as references + if (sourceFile.moduleAugmentations.length) { + const checker = program.getTypeChecker(); + for (const moduleName of sourceFile.moduleAugmentations) { + if (!isStringLiteral(moduleName)) { + continue; } - } - - // From ambient modules - for (const ambientModule of program.getTypeChecker().getAmbientModules()) { - if (ambientModule.declarations.length > 1) { - addReferenceFromAmbientModule(ambientModule); + const symbol = checker.getSymbolAtLocation(moduleName); + if (!symbol) { + continue; } - } - - return referencedFiles; - - function addReferenceFromAmbientModule(symbol: Symbol) { // Add any file other than our own as reference - for (const declaration of symbol.declarations) { - const declarationSourceFile = getSourceFileOfNode(declaration); - if (declarationSourceFile && - declarationSourceFile !== sourceFile) { - addReferencedFile(declarationSourceFile.resolvedPath); - } - } + addReferenceFromAmbientModule(symbol); + } + } + // From ambient modules + for (const ambientModule of program.getTypeChecker().getAmbientModules()) { + if (ambientModule.declarations.length > 1) { + addReferenceFromAmbientModule(ambientModule); } - - function addReferencedFile(referencedPath: Path) { - if (!referencedFiles) { - referencedFiles = createMap(); + } + return referencedFiles; + function addReferenceFromAmbientModule(symbol: Symbol) { + // Add any file other than our own as reference + for (const declaration of symbol.declarations) { + const declarationSourceFile = getSourceFileOfNode(declaration); + if (declarationSourceFile && + declarationSourceFile !== sourceFile) { + addReferencedFile(declarationSourceFile.resolvedPath); } - referencedFiles.set(referencedPath, true); } } - - /** - * Returns true if oldState is reusable, that is the emitKind = module/non module has not changed - */ - export function canReuseOldState(newReferencedMap: ReadonlyMap | undefined, oldState: Readonly | undefined) { - return oldState && !oldState.referencedMap === !newReferencedMap; + function addReferencedFile(referencedPath: Path) { + if (!referencedFiles) { + referencedFiles = createMap(); + } + referencedFiles.set(referencedPath, true); } - - /** - * Creates the state of file references and signature for the new program from oldState if it is safe - */ - export function create(newProgram: Program, getCanonicalFileName: GetCanonicalFileName, oldState?: Readonly): BuilderState { - const fileInfos = createMap(); - const referencedMap = newProgram.getCompilerOptions().module !== ModuleKind.None ? createMap() : undefined; - const exportedModulesMap = referencedMap ? createMap() : undefined; - const hasCalledUpdateShapeSignature = createMap(); - const useOldState = canReuseOldState(referencedMap, oldState); - - // Create the reference map, and set the file infos - for (const sourceFile of newProgram.getSourceFiles()) { - const version = Debug.checkDefined(sourceFile.version, "Program intended to be used with Builder should have source files with versions set"); - const oldInfo = useOldState ? oldState!.fileInfos.get(sourceFile.resolvedPath) : undefined; - if (referencedMap) { - const newReferences = getReferencedFiles(newProgram, sourceFile, getCanonicalFileName); - if (newReferences) { - referencedMap.set(sourceFile.resolvedPath, newReferences); - } - // Copy old visible to outside files map - if (useOldState) { - const exportedModules = oldState!.exportedModulesMap!.get(sourceFile.resolvedPath); - if (exportedModules) { - exportedModulesMap!.set(sourceFile.resolvedPath, exportedModules); - } + } + /** + * Returns true if oldState is reusable, that is the emitKind = module/non module has not changed + */ + export function canReuseOldState(newReferencedMap: ts.ReadonlyMap | undefined, oldState: Readonly | undefined) { + return oldState && !oldState.referencedMap === !newReferencedMap; + } + /** + * Creates the state of file references and signature for the new program from oldState if it is safe + */ + export function create(newProgram: Program, getCanonicalFileName: GetCanonicalFileName, oldState?: Readonly): BuilderState { + const fileInfos = createMap(); + const referencedMap = newProgram.getCompilerOptions().module !== ModuleKind.None ? createMap() : undefined; + const exportedModulesMap = referencedMap ? createMap() : undefined; + const hasCalledUpdateShapeSignature = createMap(); + const useOldState = canReuseOldState(referencedMap, oldState); + // Create the reference map, and set the file infos + for (const sourceFile of newProgram.getSourceFiles()) { + const version = Debug.checkDefined(sourceFile.version, "Program intended to be used with Builder should have source files with versions set"); + const oldInfo = useOldState ? oldState!.fileInfos.get(sourceFile.resolvedPath) : undefined; + if (referencedMap) { + const newReferences = getReferencedFiles(newProgram, sourceFile, getCanonicalFileName); + if (newReferences) { + referencedMap.set(sourceFile.resolvedPath, newReferences); + } + // Copy old visible to outside files map + if (useOldState) { + const exportedModules = oldState!.exportedModulesMap!.get(sourceFile.resolvedPath); + if (exportedModules) { + exportedModulesMap!.set(sourceFile.resolvedPath, exportedModules); } } - fileInfos.set(sourceFile.resolvedPath, { version, signature: oldInfo && oldInfo.signature }); } - - return { - fileInfos, - referencedMap, - exportedModulesMap, - hasCalledUpdateShapeSignature - }; + fileInfos.set(sourceFile.resolvedPath, { version, signature: oldInfo && oldInfo.signature }); } - - /** - * Releases needed properties - */ - export function releaseCache(state: BuilderState) { - state.allFilesExcludingDefaultLibraryFile = undefined; - state.allFileNames = undefined; + return { + fileInfos, + referencedMap, + exportedModulesMap, + hasCalledUpdateShapeSignature + }; + } + /** + * Releases needed properties + */ + export function releaseCache(state: BuilderState) { + state.allFilesExcludingDefaultLibraryFile = undefined; + state.allFileNames = undefined; + } + /** + * Creates a clone of the state + */ + export function clone(state: Readonly): BuilderState { + const fileInfos = createMap(); + state.fileInfos.forEach((value, key) => { + fileInfos.set(key, { ...value }); + }); + // Dont need to backup allFiles info since its cache anyway + return { + fileInfos, + referencedMap: cloneMapOrUndefined(state.referencedMap), + exportedModulesMap: cloneMapOrUndefined(state.exportedModulesMap), + hasCalledUpdateShapeSignature: cloneMap(state.hasCalledUpdateShapeSignature), + }; + } + /** + * Gets the files affected by the path from the program + */ + export function getFilesAffectedBy(state: BuilderState, programOfThisState: Program, path: Path, cancellationToken: CancellationToken | undefined, computeHash: ComputeHash, cacheToUpdateSignature?: ts.Map, exportedModulesMapCache?: ComputingExportedModulesMap): readonly SourceFile[] { + // Since the operation could be cancelled, the signatures are always stored in the cache + // They will be committed once it is safe to use them + // eg when calling this api from tsserver, if there is no cancellation of the operation + // In the other cases the affected files signatures are committed only after the iteration through the result is complete + const signatureCache = cacheToUpdateSignature || createMap(); + const sourceFile = programOfThisState.getSourceFileByPath(path); + if (!sourceFile) { + return emptyArray; } - - /** - * Creates a clone of the state - */ - export function clone(state: Readonly): BuilderState { - const fileInfos = createMap(); - state.fileInfos.forEach((value, key) => { - fileInfos.set(key, { ...value }); - }); - // Dont need to backup allFiles info since its cache anyway - return { - fileInfos, - referencedMap: cloneMapOrUndefined(state.referencedMap), - exportedModulesMap: cloneMapOrUndefined(state.exportedModulesMap), - hasCalledUpdateShapeSignature: cloneMap(state.hasCalledUpdateShapeSignature), - }; + if (!updateShapeSignature(state, programOfThisState, sourceFile, signatureCache, cancellationToken, computeHash, exportedModulesMapCache)) { + return [sourceFile]; } - - /** - * Gets the files affected by the path from the program - */ - export function getFilesAffectedBy(state: BuilderState, programOfThisState: Program, path: Path, cancellationToken: CancellationToken | undefined, computeHash: ComputeHash, cacheToUpdateSignature?: Map, exportedModulesMapCache?: ComputingExportedModulesMap): readonly SourceFile[] { - // Since the operation could be cancelled, the signatures are always stored in the cache - // They will be committed once it is safe to use them - // eg when calling this api from tsserver, if there is no cancellation of the operation - // In the other cases the affected files signatures are committed only after the iteration through the result is complete - const signatureCache = cacheToUpdateSignature || createMap(); - const sourceFile = programOfThisState.getSourceFileByPath(path); - if (!sourceFile) { - return emptyArray; - } - - if (!updateShapeSignature(state, programOfThisState, sourceFile, signatureCache, cancellationToken, computeHash, exportedModulesMapCache)) { - return [sourceFile]; - } - - const result = (state.referencedMap ? getFilesAffectedByUpdatedShapeWhenModuleEmit : getFilesAffectedByUpdatedShapeWhenNonModuleEmit)(state, programOfThisState, sourceFile, signatureCache, cancellationToken, computeHash, exportedModulesMapCache); - if (!cacheToUpdateSignature) { - // Commit all the signatures in the signature cache - updateSignaturesFromCache(state, signatureCache); - } - return result; + const result = (state.referencedMap ? getFilesAffectedByUpdatedShapeWhenModuleEmit : getFilesAffectedByUpdatedShapeWhenNonModuleEmit)(state, programOfThisState, sourceFile, signatureCache, cancellationToken, computeHash, exportedModulesMapCache); + if (!cacheToUpdateSignature) { + // Commit all the signatures in the signature cache + updateSignaturesFromCache(state, signatureCache); } - - /** - * Updates the signatures from the cache into state's fileinfo signatures - * This should be called whenever it is safe to commit the state of the builder - */ - export function updateSignaturesFromCache(state: BuilderState, signatureCache: Map) { - signatureCache.forEach((signature, path) => { - state.fileInfos.get(path)!.signature = signature; - state.hasCalledUpdateShapeSignature.set(path, true); - }); + return result; + } + /** + * Updates the signatures from the cache into state's fileinfo signatures + * This should be called whenever it is safe to commit the state of the builder + */ + export function updateSignaturesFromCache(state: BuilderState, signatureCache: ts.Map) { + signatureCache.forEach((signature, path) => { + state.fileInfos.get(path)!.signature = signature; + state.hasCalledUpdateShapeSignature.set(path, true); + }); + } + /** + * Returns if the shape of the signature has changed since last emit + */ + export function updateShapeSignature(state: Readonly, programOfThisState: Program, sourceFile: SourceFile, cacheToUpdateSignature: ts.Map, cancellationToken: CancellationToken | undefined, computeHash: ComputeHash, exportedModulesMapCache?: ComputingExportedModulesMap) { + Debug.assert(!!sourceFile); + Debug.assert(!exportedModulesMapCache || !!state.exportedModulesMap, "Compute visible to outside map only if visibleToOutsideReferencedMap present in the state"); + // If we have cached the result for this file, that means hence forth we should assume file shape is uptodate + if (state.hasCalledUpdateShapeSignature.has(sourceFile.resolvedPath) || cacheToUpdateSignature.has(sourceFile.resolvedPath)) { + return false; } - - /** - * Returns if the shape of the signature has changed since last emit - */ - export function updateShapeSignature(state: Readonly, programOfThisState: Program, sourceFile: SourceFile, cacheToUpdateSignature: Map, cancellationToken: CancellationToken | undefined, computeHash: ComputeHash, exportedModulesMapCache?: ComputingExportedModulesMap) { - Debug.assert(!!sourceFile); - Debug.assert(!exportedModulesMapCache || !!state.exportedModulesMap, "Compute visible to outside map only if visibleToOutsideReferencedMap present in the state"); - - // If we have cached the result for this file, that means hence forth we should assume file shape is uptodate - if (state.hasCalledUpdateShapeSignature.has(sourceFile.resolvedPath) || cacheToUpdateSignature.has(sourceFile.resolvedPath)) { - return false; + const info = state.fileInfos.get(sourceFile.resolvedPath); + if (!info) + return Debug.fail(); + const prevSignature = info.signature; + let latestSignature: string; + if (sourceFile.isDeclarationFile) { + latestSignature = sourceFile.version; + if (exportedModulesMapCache && latestSignature !== prevSignature) { + // All the references in this file are exported + const references = state.referencedMap ? state.referencedMap.get(sourceFile.resolvedPath) : undefined; + exportedModulesMapCache.set(sourceFile.resolvedPath, references || false); } - - const info = state.fileInfos.get(sourceFile.resolvedPath); - if (!info) return Debug.fail(); - - const prevSignature = info.signature; - let latestSignature: string; - if (sourceFile.isDeclarationFile) { - latestSignature = sourceFile.version; + } + else { + const emitOutput = getFileEmitOutput(programOfThisState, sourceFile, + /*emitOnlyDtsFiles*/ true, cancellationToken, + /*customTransformers*/ undefined, + /*forceDtsEmit*/ true); + const firstDts = emitOutput.outputFiles && + programOfThisState.getCompilerOptions().declarationMap ? + emitOutput.outputFiles.length > 1 ? emitOutput.outputFiles[1] : undefined : + emitOutput.outputFiles.length > 0 ? emitOutput.outputFiles[0] : undefined; + if (firstDts) { + Debug.assert(fileExtensionIs(firstDts.name, Extension.Dts), "File extension for signature expected to be dts", () => `Found: ${getAnyExtensionFromPath(firstDts.name)} for ${firstDts.name}:: All output files: ${JSON.stringify(emitOutput.outputFiles.map(f => f.name))}`); + latestSignature = computeHash(firstDts.text); if (exportedModulesMapCache && latestSignature !== prevSignature) { - // All the references in this file are exported - const references = state.referencedMap ? state.referencedMap.get(sourceFile.resolvedPath) : undefined; - exportedModulesMapCache.set(sourceFile.resolvedPath, references || false); + updateExportedModules(sourceFile, emitOutput.exportedModulesFromDeclarationEmit, exportedModulesMapCache); } } else { - const emitOutput = getFileEmitOutput( - programOfThisState, - sourceFile, - /*emitOnlyDtsFiles*/ true, - cancellationToken, - /*customTransformers*/ undefined, - /*forceDtsEmit*/ true - ); - const firstDts = emitOutput.outputFiles && - programOfThisState.getCompilerOptions().declarationMap ? - emitOutput.outputFiles.length > 1 ? emitOutput.outputFiles[1] : undefined : - emitOutput.outputFiles.length > 0 ? emitOutput.outputFiles[0] : undefined; - if (firstDts) { - Debug.assert(fileExtensionIs(firstDts.name, Extension.Dts), "File extension for signature expected to be dts", () => `Found: ${getAnyExtensionFromPath(firstDts.name)} for ${firstDts.name}:: All output files: ${JSON.stringify(emitOutput.outputFiles.map(f => f.name))}`); - latestSignature = computeHash(firstDts.text); - if (exportedModulesMapCache && latestSignature !== prevSignature) { - updateExportedModules(sourceFile, emitOutput.exportedModulesFromDeclarationEmit, exportedModulesMapCache); - } - } - else { - latestSignature = prevSignature!; // TODO: GH#18217 - } - + latestSignature = prevSignature!; // TODO: GH#18217 } - cacheToUpdateSignature.set(sourceFile.resolvedPath, latestSignature); - - return !prevSignature || latestSignature !== prevSignature; } - - /** - * Coverts the declaration emit result into exported modules map - */ - function updateExportedModules(sourceFile: SourceFile, exportedModulesFromDeclarationEmit: ExportedModulesFromDeclarationEmit | undefined, exportedModulesMapCache: ComputingExportedModulesMap) { - if (!exportedModulesFromDeclarationEmit) { - exportedModulesMapCache.set(sourceFile.resolvedPath, false); - return; - } - - let exportedModules: Map | undefined; - exportedModulesFromDeclarationEmit.forEach(symbol => addExportedModule(getReferencedFileFromImportedModuleSymbol(symbol))); - exportedModulesMapCache.set(sourceFile.resolvedPath, exportedModules || false); - - function addExportedModule(exportedModulePath: Path | undefined) { - if (exportedModulePath) { - if (!exportedModules) { - exportedModules = createMap(); - } - exportedModules.set(exportedModulePath, true); - } - } + cacheToUpdateSignature.set(sourceFile.resolvedPath, latestSignature); + return !prevSignature || latestSignature !== prevSignature; + } + /** + * Coverts the declaration emit result into exported modules map + */ + function updateExportedModules(sourceFile: SourceFile, exportedModulesFromDeclarationEmit: ExportedModulesFromDeclarationEmit | undefined, exportedModulesMapCache: ComputingExportedModulesMap) { + if (!exportedModulesFromDeclarationEmit) { + exportedModulesMapCache.set(sourceFile.resolvedPath, false); + return; } - - /** - * Updates the exported modules from cache into state's exported modules map - * This should be called whenever it is safe to commit the state of the builder - */ - export function updateExportedFilesMapFromCache(state: BuilderState, exportedModulesMapCache: ComputingExportedModulesMap | undefined) { - if (exportedModulesMapCache) { - Debug.assert(!!state.exportedModulesMap); - exportedModulesMapCache.forEach((exportedModules, path) => { - if (exportedModules) { - state.exportedModulesMap!.set(path, exportedModules); - } - else { - state.exportedModulesMap!.delete(path); - } - }); + let exportedModules: ts.Map | undefined; + exportedModulesFromDeclarationEmit.forEach(symbol => addExportedModule(getReferencedFileFromImportedModuleSymbol(symbol))); + exportedModulesMapCache.set(sourceFile.resolvedPath, exportedModules || false); + function addExportedModule(exportedModulePath: Path | undefined) { + if (exportedModulePath) { + if (!exportedModules) { + exportedModules = createMap(); + } + exportedModules.set(exportedModulePath, true); } } - - /** - * Get all the dependencies of the sourceFile - */ - export function getAllDependencies(state: BuilderState, programOfThisState: Program, sourceFile: SourceFile): readonly string[] { - const compilerOptions = programOfThisState.getCompilerOptions(); - // With --out or --outFile all outputs go into single file, all files depend on each other - if (compilerOptions.outFile || compilerOptions.out) { - return getAllFileNames(state, programOfThisState); - } - - // If this is non module emit, or its a global file, it depends on all the source files - if (!state.referencedMap || isFileAffectingGlobalScope(sourceFile)) { - return getAllFileNames(state, programOfThisState); - } - - // Get the references, traversing deep from the referenceMap - const seenMap = createMap(); - const queue = [sourceFile.resolvedPath]; - while (queue.length) { - const path = queue.pop()!; - if (!seenMap.has(path)) { - seenMap.set(path, true); - const references = state.referencedMap.get(path); - if (references) { - const iterator = references.keys(); - for (let iterResult = iterator.next(); !iterResult.done; iterResult = iterator.next()) { - queue.push(iterResult.value as Path); - } - } + } + /** + * Updates the exported modules from cache into state's exported modules map + * This should be called whenever it is safe to commit the state of the builder + */ + export function updateExportedFilesMapFromCache(state: BuilderState, exportedModulesMapCache: ComputingExportedModulesMap | undefined) { + if (exportedModulesMapCache) { + Debug.assert(!!state.exportedModulesMap); + exportedModulesMapCache.forEach((exportedModules, path) => { + if (exportedModules) { + state.exportedModulesMap!.set(path, exportedModules); } - } - - return arrayFrom(mapDefinedIterator(seenMap.keys(), path => { - const file = programOfThisState.getSourceFileByPath(path as Path); - return file ? file.fileName : path; - })); + else { + state.exportedModulesMap!.delete(path); + } + }); } - - /** - * Gets the names of all files from the program - */ - function getAllFileNames(state: BuilderState, programOfThisState: Program): readonly string[] { - if (!state.allFileNames) { - const sourceFiles = programOfThisState.getSourceFiles(); - state.allFileNames = sourceFiles === emptyArray ? emptyArray : sourceFiles.map(file => file.fileName); - } - return state.allFileNames; + } + /** + * Get all the dependencies of the sourceFile + */ + export function getAllDependencies(state: BuilderState, programOfThisState: Program, sourceFile: SourceFile): readonly string[] { + const compilerOptions = programOfThisState.getCompilerOptions(); + // With --out or --outFile all outputs go into single file, all files depend on each other + if (compilerOptions.outFile || compilerOptions.out) { + return getAllFileNames(state, programOfThisState); } - - /** - * Gets the files referenced by the the file path - */ - export function getReferencedByPaths(state: Readonly, referencedFilePath: Path) { - return arrayFrom(mapDefinedIterator(state.referencedMap!.entries(), ([filePath, referencesInFile]) => - referencesInFile.has(referencedFilePath) ? filePath as Path : undefined - )); + // If this is non module emit, or its a global file, it depends on all the source files + if (!state.referencedMap || isFileAffectingGlobalScope(sourceFile)) { + return getAllFileNames(state, programOfThisState); } - - /** - * For script files that contains only ambient external modules, although they are not actually external module files, - * they can only be consumed via importing elements from them. Regular script files cannot consume them. Therefore, - * there are no point to rebuild all script files if these special files have changed. However, if any statement - * in the file is not ambient external module, we treat it as a regular script file. - */ - function containsOnlyAmbientModules(sourceFile: SourceFile) { - for (const statement of sourceFile.statements) { - if (!isModuleWithStringLiteralName(statement)) { - return false; + // Get the references, traversing deep from the referenceMap + const seenMap = createMap(); + const queue = [sourceFile.resolvedPath]; + while (queue.length) { + const path = queue.pop()!; + if (!seenMap.has(path)) { + seenMap.set(path, true); + const references = state.referencedMap.get(path); + if (references) { + const iterator = references.keys(); + for (let iterResult = iterator.next(); !iterResult.done; iterResult = iterator.next()) { + queue.push((iterResult.value as Path)); + } } } - return true; - } - - /** - * Return true if file contains anything that augments to global scope we need to build them as if - * they are global files as well as module - */ - function containsGlobalScopeAugmentation(sourceFile: SourceFile) { - return some(sourceFile.moduleAugmentations, augmentation => isGlobalScopeAugmentation(augmentation.parent as ModuleDeclaration)); } - - /** - * Return true if the file will invalidate all files because it affectes global scope - */ - function isFileAffectingGlobalScope(sourceFile: SourceFile) { - return containsGlobalScopeAugmentation(sourceFile) || - !isExternalModule(sourceFile) && !containsOnlyAmbientModules(sourceFile); + return arrayFrom(mapDefinedIterator(seenMap.keys(), path => { + const file = programOfThisState.getSourceFileByPath((path as Path)); + return file ? file.fileName : path; + })); + } + /** + * Gets the names of all files from the program + */ + function getAllFileNames(state: BuilderState, programOfThisState: Program): readonly string[] { + if (!state.allFileNames) { + const sourceFiles = programOfThisState.getSourceFiles(); + state.allFileNames = sourceFiles === emptyArray ? emptyArray : sourceFiles.map(file => file.fileName); } - - /** - * Gets all files of the program excluding the default library file - */ - function getAllFilesExcludingDefaultLibraryFile(state: BuilderState, programOfThisState: Program, firstSourceFile: SourceFile): readonly SourceFile[] { - // Use cached result - if (state.allFilesExcludingDefaultLibraryFile) { - return state.allFilesExcludingDefaultLibraryFile; - } - - let result: SourceFile[] | undefined; - addSourceFile(firstSourceFile); - for (const sourceFile of programOfThisState.getSourceFiles()) { - if (sourceFile !== firstSourceFile) { - addSourceFile(sourceFile); - } + return state.allFileNames; + } + /** + * Gets the files referenced by the the file path + */ + export function getReferencedByPaths(state: Readonly, referencedFilePath: Path) { + return arrayFrom(mapDefinedIterator(state.referencedMap!.entries(), ([filePath, referencesInFile]) => referencesInFile.has(referencedFilePath) ? filePath as Path : undefined)); + } + /** + * For script files that contains only ambient external modules, although they are not actually external module files, + * they can only be consumed via importing elements from them. Regular script files cannot consume them. Therefore, + * there are no point to rebuild all script files if these special files have changed. However, if any statement + * in the file is not ambient external module, we treat it as a regular script file. + */ + function containsOnlyAmbientModules(sourceFile: SourceFile) { + for (const statement of sourceFile.statements) { + if (!isModuleWithStringLiteralName(statement)) { + return false; } - state.allFilesExcludingDefaultLibraryFile = result || emptyArray; + } + return true; + } + /** + * Return true if file contains anything that augments to global scope we need to build them as if + * they are global files as well as module + */ + function containsGlobalScopeAugmentation(sourceFile: SourceFile) { + return some(sourceFile.moduleAugmentations, augmentation => isGlobalScopeAugmentation((augmentation.parent as ModuleDeclaration))); + } + /** + * Return true if the file will invalidate all files because it affectes global scope + */ + function isFileAffectingGlobalScope(sourceFile: SourceFile) { + return containsGlobalScopeAugmentation(sourceFile) || + !isExternalModule(sourceFile) && !containsOnlyAmbientModules(sourceFile); + } + /** + * Gets all files of the program excluding the default library file + */ + function getAllFilesExcludingDefaultLibraryFile(state: BuilderState, programOfThisState: Program, firstSourceFile: SourceFile): readonly SourceFile[] { + // Use cached result + if (state.allFilesExcludingDefaultLibraryFile) { return state.allFilesExcludingDefaultLibraryFile; - - function addSourceFile(sourceFile: SourceFile) { - if (!programOfThisState.isSourceFileDefaultLibrary(sourceFile)) { - (result || (result = [])).push(sourceFile); - } + } + let result: SourceFile[] | undefined; + addSourceFile(firstSourceFile); + for (const sourceFile of programOfThisState.getSourceFiles()) { + if (sourceFile !== firstSourceFile) { + addSourceFile(sourceFile); } } - - /** - * When program emits non modular code, gets the files affected by the sourceFile whose shape has changed - */ - function getFilesAffectedByUpdatedShapeWhenNonModuleEmit(state: BuilderState, programOfThisState: Program, sourceFileWithUpdatedShape: SourceFile) { - const compilerOptions = programOfThisState.getCompilerOptions(); - // If `--out` or `--outFile` is specified, any new emit will result in re-emitting the entire project, - // so returning the file itself is good enough. - if (compilerOptions && (compilerOptions.out || compilerOptions.outFile)) { - return [sourceFileWithUpdatedShape]; + state.allFilesExcludingDefaultLibraryFile = result || emptyArray; + return state.allFilesExcludingDefaultLibraryFile; + function addSourceFile(sourceFile: SourceFile) { + if (!programOfThisState.isSourceFileDefaultLibrary(sourceFile)) { + (result || (result = [])).push(sourceFile); } + } + } + /** + * When program emits non modular code, gets the files affected by the sourceFile whose shape has changed + */ + function getFilesAffectedByUpdatedShapeWhenNonModuleEmit(state: BuilderState, programOfThisState: Program, sourceFileWithUpdatedShape: SourceFile) { + const compilerOptions = programOfThisState.getCompilerOptions(); + // If `--out` or `--outFile` is specified, any new emit will result in re-emitting the entire project, + // so returning the file itself is good enough. + if (compilerOptions && (compilerOptions.out || compilerOptions.outFile)) { + return [sourceFileWithUpdatedShape]; + } + return getAllFilesExcludingDefaultLibraryFile(state, programOfThisState, sourceFileWithUpdatedShape); + } + /** + * When program emits modular code, gets the files affected by the sourceFile whose shape has changed + */ + function getFilesAffectedByUpdatedShapeWhenModuleEmit(state: BuilderState, programOfThisState: Program, sourceFileWithUpdatedShape: SourceFile, cacheToUpdateSignature: ts.Map, cancellationToken: CancellationToken | undefined, computeHash: ComputeHash | undefined, exportedModulesMapCache: ComputingExportedModulesMap | undefined) { + if (isFileAffectingGlobalScope(sourceFileWithUpdatedShape)) { return getAllFilesExcludingDefaultLibraryFile(state, programOfThisState, sourceFileWithUpdatedShape); } - - /** - * When program emits modular code, gets the files affected by the sourceFile whose shape has changed - */ - function getFilesAffectedByUpdatedShapeWhenModuleEmit(state: BuilderState, programOfThisState: Program, sourceFileWithUpdatedShape: SourceFile, cacheToUpdateSignature: Map, cancellationToken: CancellationToken | undefined, computeHash: ComputeHash | undefined, exportedModulesMapCache: ComputingExportedModulesMap | undefined) { - if (isFileAffectingGlobalScope(sourceFileWithUpdatedShape)) { - return getAllFilesExcludingDefaultLibraryFile(state, programOfThisState, sourceFileWithUpdatedShape); - } - - const compilerOptions = programOfThisState.getCompilerOptions(); - if (compilerOptions && (compilerOptions.isolatedModules || compilerOptions.out || compilerOptions.outFile)) { - return [sourceFileWithUpdatedShape]; - } - - // Now we need to if each file in the referencedBy list has a shape change as well. - // Because if so, its own referencedBy files need to be saved as well to make the - // emitting result consistent with files on disk. - const seenFileNamesMap = createMap(); - - // Start with the paths this file was referenced by - seenFileNamesMap.set(sourceFileWithUpdatedShape.resolvedPath, sourceFileWithUpdatedShape); - const queue = getReferencedByPaths(state, sourceFileWithUpdatedShape.resolvedPath); - while (queue.length > 0) { - const currentPath = queue.pop()!; - if (!seenFileNamesMap.has(currentPath)) { - const currentSourceFile = programOfThisState.getSourceFileByPath(currentPath)!; - seenFileNamesMap.set(currentPath, currentSourceFile); - if (currentSourceFile && updateShapeSignature(state, programOfThisState, currentSourceFile, cacheToUpdateSignature, cancellationToken, computeHash!, exportedModulesMapCache)) { // TODO: GH#18217 - queue.push(...getReferencedByPaths(state, currentSourceFile.resolvedPath)); - } + const compilerOptions = programOfThisState.getCompilerOptions(); + if (compilerOptions && (compilerOptions.isolatedModules || compilerOptions.out || compilerOptions.outFile)) { + return [sourceFileWithUpdatedShape]; + } + // Now we need to if each file in the referencedBy list has a shape change as well. + // Because if so, its own referencedBy files need to be saved as well to make the + // emitting result consistent with files on disk. + const seenFileNamesMap = createMap(); + // Start with the paths this file was referenced by + seenFileNamesMap.set(sourceFileWithUpdatedShape.resolvedPath, sourceFileWithUpdatedShape); + const queue = getReferencedByPaths(state, sourceFileWithUpdatedShape.resolvedPath); + while (queue.length > 0) { + const currentPath = queue.pop()!; + if (!seenFileNamesMap.has(currentPath)) { + const currentSourceFile = programOfThisState.getSourceFileByPath(currentPath)!; + seenFileNamesMap.set(currentPath, currentSourceFile); + if (currentSourceFile && updateShapeSignature(state, programOfThisState, currentSourceFile, cacheToUpdateSignature, cancellationToken, computeHash!, exportedModulesMapCache)) { // TODO: GH#18217 + queue.push(...getReferencedByPaths(state, currentSourceFile.resolvedPath)); } } - - // Return array of values that needs emit - // Return array of values that needs emit - return arrayFrom(mapDefinedIterator(seenFileNamesMap.values(), value => value)); } + // Return array of values that needs emit + // Return array of values that needs emit + return arrayFrom(mapDefinedIterator(seenFileNamesMap.values(), value => value)); } - - export function cloneMapOrUndefined(map: ReadonlyMap | undefined) { - return map ? cloneMap(map) : undefined; - } +} +/* @internal */ +export function cloneMapOrUndefined(map: ts.ReadonlyMap | undefined) { + return map ? cloneMap(map) : undefined; } diff --git a/src/compiler/builderStatePublic.ts b/src/compiler/builderStatePublic.ts index 2e70063f90a7f..357ac6952590d 100644 --- a/src/compiler/builderStatePublic.ts +++ b/src/compiler/builderStatePublic.ts @@ -1,13 +1,11 @@ -namespace ts { - export interface EmitOutput { - outputFiles: OutputFile[]; - emitSkipped: boolean; - /* @internal */ exportedModulesFromDeclarationEmit?: ExportedModulesFromDeclarationEmit; - } - - export interface OutputFile { - name: string; - writeByteOrderMark: boolean; - text: string; - } -} \ No newline at end of file +import { ExportedModulesFromDeclarationEmit } from "./ts"; +export interface EmitOutput { + outputFiles: OutputFile[]; + emitSkipped: boolean; + /* @internal */ exportedModulesFromDeclarationEmit?: ExportedModulesFromDeclarationEmit; +} +export interface OutputFile { + name: string; + writeByteOrderMark: boolean; + text: string; +} diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 1b2e6931e02c4..66c570c69a890 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -1,37475 +1,33929 @@ +import { __String, GenericType, Type, Node, DiagnosticMessage, createMapFromTemplate, Symbol, Signature, DiagnosticWithLocation, and, ModuleDeclaration, getModuleInstanceState, ModuleInstanceState, TypeCheckerHost, TypeChecker, memoize, createMap, forEachEntry, CancellationToken, ExternalEmitHelpers, objectAllocator, createSymbolTable, identity, VarianceFlags, getEmitScriptTarget, getEmitModuleKind, getAllowSyntheticDefaultImports, getStrictOptionValue, ObjectFlags, SymbolFlags, CheckFlags, sum, getParseTreeNode, isParameter, Debug, escapeLeadingUnderscores, isTypeNode, isExportSpecifier, isAssignmentPattern, isIdentifier, Expression, ContextFlags, isExpression, findAncestor, isCallLikeExpression, isObjectLiteralElementLike, isJsxAttributeLike, isPropertyAccessOrQualifiedNameOrImportTypeNode, isPropertyAccessExpression, isFunctionLike, createGetSymbolWalker, getFirstIdentifier, TypeFlags, TypeParameter, unescapeLeadingUnderscores, skipTypeChecking, emptyArray, NodeCheckFlags, addRange, containsParseError, NodeFlags, DiagnosticCategory, CallLikeExpression, UnionType, LiteralType, IndexedAccessType, SubstitutionType, EvolvingArrayType, UnderscoreEscapedMap, InternalSymbolName, FreshableIntrinsicType, ObjectType, TypeReference, TypePredicateKind, SignatureFlags, IterationTypes, Diagnostics, SourceFile, PatternAmbientModule, FlowNode, SymbolLinks, NodeLinks, FlowType, createDiagnosticCollection, EntityName, RelationComparisonResult, getSourceFileOfNode, isArray, parseIsolatedEntityName, visitNode, createQualifiedName, createIdentifier, VisitResult, visitEachChild, nullTransformationContext, Diagnostic, createDiagnosticForNode, createCompilerDiagnostic, DiagnosticMessageChain, createDiagnosticForNodeFromMessageChain, addRelatedInfo, TransientSymbol, cloneMap, isAssignmentDeclaration, isEffectiveModuleDeclaration, getNameOfDeclaration, comparePaths, Comparison, getOrUpdate, pushIfUnique, getExpandoInitializer, getNameOfExpando, forEach, length, SymbolTable, hasEntries, StringLiteral, Identifier, isGlobalScopeAugmentation, some, Declaration, SyntaxKind, isExternalOrCommonJsModule, getCheckFlags, ParameterDeclaration, getAncestor, BindingElement, isBindingElement, VariableDeclaration, isClassDeclaration, isComputedPropertyName, isPropertyDeclaration, isParameterPropertyDeclaration, getEnclosingBlockScopeContainer, ScriptTarget, getContainingClass, ExportAssignment, isForInOrOfStatement, PropertyDeclaration, hasModifier, ModifierFlags, ParameterPropertyDeclaration, FunctionLikeDeclaration, ConditionalTypeNode, isModuleDeclaration, getLocalSymbolForExportDefault, getDeclarationOfKind, isSourceFile, isJSDocTypeAlias, ClassLikeDeclaration, InterfaceDeclaration, ClassExpression, ExpressionWithTypeArguments, HeritageClause, isClassLike, FunctionExpression, isClassElement, getJSDocHost, getRootDeclaration, isInJSFile, isRequireCall, declarationNameToString, every, isNamespaceExportDeclaration, isValidTypeOnlyAliasUseSite, typeOnlyDeclarationIsExport, isTypeQueryNode, isFunctionLikeDeclaration, FunctionLike, ArrowFunction, getImmediatelyInvokedFunctionExpression, PrivateIdentifier, isString, isJSDocTemplateTag, find, JSDoc, getThisContainer, InterfaceType, getTextOfNode, EntityNameExpression, isEntityNameExpression, isQualifiedName, isBlockOrCatchScoped, AnyImportSyntax, ImportEqualsDeclaration, ImportClause, NamespaceImport, ImportSpecifier, exportAssignmentIsAlias, isBinaryExpression, getAssignmentDeclarationKind, AssignmentDeclarationKind, PropertyAssignment, isAliasableExpression, isFunctionExpression, getExternalModuleImportEqualsDeclarationExpression, TypeOnlyCompatibleAliasDeclaration, isExportAssignment, isSourceFileJS, isShorthandAmbientModuleSymbol, ModuleKind, NamespaceExport, deduplicate, concatenate, equateValues, ImportOrExportSpecifier, ImportDeclaration, ExportDeclaration, map, NamespaceExportDeclaration, ExportSpecifier, BinaryExpression, isClassExpression, isEntityName, PropertyAccessExpression, ShorthandPropertyAssignment, isTypeOnlyImportOrExportDeclaration, isInternalModuleImportEqualsDeclaration, isRightSideOfQualifiedNameOrPropertyAccess, QualifiedName, SymbolFormatFlags, EntityNameOrEntityNameExpression, nodeIsMissing, nodeIsSynthesized, isVariableDeclaration, CallExpression, getAliasDeclarationFromName, TypeReferenceNode, isJSDocNode, isExpressionStatement, isObjectLiteralMethod, isPropertyAssignment, getHostSignatureFromJSDocHost, getAssignedExpandoInitializer, hasOnlyExpressionInitializer, getDeclaredExpandoInitializer, isStringLiteralLike, startsWith, removePrefix, getResolvedModule, getResolutionDiagnostic, resolutionExtensionIsTSOrJson, findBestPatternMatch, tryExtractTSExtension, removeExtension, fileExtensionIs, Extension, getEmitModuleResolutionKind, ModuleResolutionKind, hasJsonModuleEmitEnabled, ResolvedModuleFull, isExternalModuleNameRelative, chainDiagnosticMessages, mangleScopedPackageName, getTypesPackageName, isImportDeclaration, getNamespaceDeclarationNode, isImportCall, SignatureKind, StructuredType, getObjectFlags, append, isExternalModule, mapDefined, isAmbientModule, isAccessExpression, isModuleExportsAccessExpression, isExportsIdentifier, ConstructorDeclaration, nodeIsPresent, IntrinsicType, arrayFrom, CharacterCodes, IndexInfo, ResolvedType, isUMDExportSymbol, isExternalModuleImportEqualsDeclaration, SymbolAccessibility, SymbolAccessibilityResult, first, isObjectLiteralExpression, isModuleWithStringLiteralName, SymbolVisibilityResult, LateVisibilityPaintedStatement, filter, isVariableStatement, isLateVisibilityPaintedStatement, appendIfUnique, isExpressionWithTypeArgumentsInClassExtendsClause, EmitTextWriter, NodeBuilderFlags, usingSingleLineStringWriter, createPrinter, EmitHint, TypeFormatFlags, getTrailingSemicolonDeferringWriter, createTextWriter, defaultMaximumTruncationLength, SymbolTracker, IndexKind, noop, Program, maybeBind, TypeNode, createKeywordTypeNode, ImportTypeNode, createTypeReferenceNode, symbolName, StringLiteralType, createLiteralTypeNode, setEmitFlags, createLiteral, EmitFlags, NumberLiteralType, createPrefix, pseudoBigIntToString, BigIntLiteralType, createTrue, createFalse, createTypeOperatorNode, createThis, contains, createInferTypeNode, idText, IntersectionType, createUnionOrIntersectionTypeNode, IndexType, createIndexedAccessTypeNode, ConditionalType, createConditionalTypeNode, MappedType, ReadonlyToken, PlusToken, MinusToken, createToken, QuestionToken, createMappedTypeNode, createTypeLiteralNode, FunctionTypeNode, ConstructorTypeNode, createArrayTypeNode, TupleType, createRestTypeNode, createOptionalTypeNode, createTupleTypeNode, rangeEquals, isImportTypeNode, TypeElement, createPropertySignature, CallSignatureDeclaration, ConstructSignatureDeclaration, IndexSignatureDeclaration, getDeclarationModifierFlagsFromSymbol, isElementAccessExpression, isPropertyAccessEntityNameExpression, MethodSignature, JSDocPropertyTag, setSyntheticLeadingComments, setCommentRange, getNameFromIndexInfo, createParameter, createIndexSignature, SignatureDeclaration, TypeParameterDeclaration, createThisTypeNode, createTypePredicateNodeWithModifier, createSignatureDeclaration, createTypeParameterDeclaration, JSDocParameterTag, isTransientSymbol, getSynthesizedClone, isRestParameter, BindingName, pathIsRelative, NodeArray, createNodeArray, IndexedAccessTypeNode, isIndexedAccessTypeNode, getNonAugmentationDeclaration, getOriginalNode, createImportTypeNode, createTypeQueryNode, isSingleOrDoubleQuote, isIdentifierStart, createPropertyAccess, createElementAccess, isStringLiteral, isKnownSymbol, createComputedPropertyName, isIdentifierText, UniqueESSymbolType, Statement, ClassElement, createProperty, DeclarationStatement, isModuleBlock, getModifierFlags, createExportDeclaration, createNamedExports, flatMap, createExportSpecifier, nodeHasName, isExportDeclaration, isNamedExports, cast, group, isExternalModuleIndicator, hasScopeMarker, needsScopeMarker, createEmptyExports, isEnumDeclaration, isFunctionDeclaration, isExternalModuleAugmentation, isInterfaceDeclaration, createModifiersFromModifierFlags, isStringANonContextualKeyword, createExportAssignment, isParameterDeclaration, isJsonSourceFile, createTypeAliasDeclaration, createHeritageClause, createInterfaceDeclaration, arrayToMultiMap, createModuleBlock, createModuleDeclaration, createEnumDeclaration, isEnumMember, EnumMember, createEnumMember, isVariableDeclarationList, setTextRange, createVariableStatement, createVariableDeclarationList, createVariableDeclaration, FunctionDeclaration, NamespaceDeclaration, isNamedDeclaration, isPrivateIdentifier, createPrivateIdentifier, createClassDeclaration, createImportEqualsDeclaration, createExternalModuleReference, createNamespaceExportDeclaration, createImportDeclaration, createImportClause, createNamespaceImport, createNamespaceExport, createNamedImports, createImportSpecifier, getExportAssignmentExpression, getPropertyAssignmentAliasLikeExpression, isStringAKeyword, Decorator, Modifier, PropertyName, AccessorDeclaration, or, isAccessor, isPropertySignature, createSetAccessor, isSetAccessor, createGetAccessor, isGetAccessor, MethodDeclaration, getEffectiveTypeAnnotationNode, getMutableClone, isJSDocAllType, isJSDocUnknownType, isJSDocNullableType, createUnionTypeNode, isJSDocOptionalType, isJSDocNonNullableType, isExpressionWithTypeArguments, isTypeReferenceNode, isJSDocIndexSignature, isJSDocFunctionType, isJSDocConstructSignature, createConstructorTypeNode, visitNodes, createFunctionTypeNode, isLiteralImportTypeNode, updateImportTypeNode, updateLiteralTypeNode, createGetCanonicalFileName, getResolvedExternalModuleName, getSelectedModifierFlags, createConstructor, createExpressionWithTypeArguments, stripQuotes, TypePredicate, escapeString, firstDefined, isCallExpression, isBindableObjectDefinePropertyCall, isBindingPattern, BindingPattern, getCombinedModifierFlags, JSDocEnumTag, BindingElementGrandparent, isStringOrNumericLiteralLike, ElementAccessExpression, createNode, LeftHandSideExpression, ArrayLiteralExpression, TupleTypeReference, walkUpBindingElementsAndPatterns, UnionReduction, getJSDocType, skipParentheses, PropertySignature, getCombinedNodeFlags, isFunctionTypeNode, isJsxAttribute, getJSDocTypeTag, getAssignmentDeclarationPropertyAccessKind, BindableObjectDefinePropertyCall, copyEntries, isPrototypePropertyAssignment, ObjectBindingPattern, lastOrUndefined, isOmittedExpression, findLastIndex, VariableLikeDeclaration, isCatchClauseVariableDeclarationOrBindingElement, isBindableStaticElementAccessExpression, isJSDocPropertyLikeTag, isNumericLiteral, isMethodDeclaration, isMethodSignature, isShorthandPropertyAssignment, getEffectiveReturnTypeNode, getEffectiveSetAccessorTypeAnnotationNode, getDeclarationOfExpando, HasInitializer, ReverseMappedSymbol, MappedTypeNode, getEffectiveTypeParameterDeclarations, DeclarationWithTypeParameters, isTypeAlias, TypeAliasDeclaration, JSDocTypedefTag, JSDocCallbackTag, getEffectiveBaseTypeNode, sameMap, BaseType, getEffectiveImplementsTypeNodes, resolvingEmptyArray, getInterfaceBaseTypeNodes, PrefixUnaryExpression, EnumKind, EnumDeclaration, ArrayTypeNode, getEffectiveConstraintOfTypeParameter, hasInitializer, TypeMapper, isPrivateIdentifierPropertyDeclaration, InterfaceTypeWithDeclaredMembers, DeclarationName, LateBoundName, LateBoundDeclaration, LateBoundBinaryExpressionDeclaration, hasDynamicName, isDynamicName, getMembersOfDeclaration, hasStaticModifier, JSDocSignature, countWhere, AnonymousType, ReverseMappedType, TypeOperatorNode, UnionOrIntersectionType, ObjectLiteralExpression, JsxAttributes, ObjectLiteralElementLike, JsxAttributeLike, InstantiableType, isNodeDescendantOf, isTypeParameterDeclaration, getJSDocParameterTags, hasQuestionToken, isJSDocParameterTag, isValueSignatureDeclaration, hasJSDocParameterTags, ClassDeclaration, hasRestParameter, isJSDocSignature, getJSDocTags, isJSDocVariadicType, isExpressionNode, NamedDeclaration, nodeStartsNewLexicalEnvironment, isPartOfTypeNode, forEachChild, isTypePredicateNode, TypePredicateNode, findIndex, isIndexSignatureDeclaration, getHostSignatureFromJSDoc, TupleTypeNode, DeferredTypeReference, NodeWithTypeArguments, isJSDocAugmentsTag, TypeReferenceType, TypeVariable, isStatement, JSDocNullableType, isConstTypeReference, isAssertionExpression, TypeQueryNode, ParenthesizedTypeNode, OptionalTypeNode, JSDocTypeReferencingNode, RestTypeNode, UnionOrIntersectionTypeNode, isTypeOperatorNode, binarySearch, compareValues, orderedRemoveItemAt, UnionTypeNode, reduceLeft, replaceElement, IntersectionTypeNode, BigIntLiteral, parsePseudoBigInt, walkUpParenthesizedTypes, ArrayBindingPattern, ComputedPropertyName, NumericLiteral, SyntheticExpression, getPropertyNameForKnownSymbolName, isPropertyName, getPropertyNameForPropertyNameNode, getAssignmentTargetKind, AssignmentKind, isAssignmentTarget, isDeleteTarget, ConditionalRoot, InferenceFlags, InferencePriority, InferTypeNode, isJSDocTypeLiteral, isParenthesizedTypeNode, createUnderscoreEscapedMap, PseudoBigInt, LiteralTypeNode, isValidESSymbolDeclaration, isConstructorDeclaration, ThisExpression, ThisTypeNode, JSDocOptionalType, JSDocTypeExpression, JSDocVariadicType, InferenceContext, getParameterSymbolFromJSDoc, JsxChild, ConditionalExpression, ParenthesizedExpression, isJsxOpeningElement, JsxAttribute, JsxExpression, firstOrUndefined, parameterIsThisKeyword, Ternary, isBlock, getFunctionFlags, FunctionFlags, isJsxSpreadAttribute, JsxElement, isJsxText, isJsxElement, formatMessage, isSpreadAssignment, TypeComparer, isIdentifierTypePredicate, IdentifierTypePredicate, FreshableType, DiagnosticRelatedInformation, concatenateDiagnosticMessageChains, FreshObjectLiteralType, isJsxAttributes, isJsxOpeningLikeElement, cartesianProduct, getSymbolNameForPrivateIdentifier, getDeclarationName, isAbstractConstructorType, OptionalChain, isOutermostOptionalChain, isExpressionOfOptionalChainRoot, isOptionalChain, WideningContext, isCheckJsEnabledForFile, isCallSignatureDeclaration, isTypeNodeKind, InferenceInfo, ObjectFlagsType, isWriteOnlyAccess, NonNullExpression, AccessExpression, ForOfStatement, SpreadElement, CaseClause, DefaultClause, SwitchStatement, IncompleteType, isPushOrUnshiftIdentifier, Block, ModuleBlock, isFunctionOrModuleBlock, getSpanOfTokenAtPosition, createFileDiagnostic, FlowFlags, FlowAssignment, FlowCondition, FlowArrayMutation, PreFinallyFlow, FlowCall, FlowLabel, FlowSwitchClause, FlowReduceLabel, FlowStart, isVarConst, TypeOfExpression, LiteralExpression, isCallChain, getContainingFunction, nodeIsDecorated, isObjectLiteralOrClassExpressionMethod, ForStatement, isIterationStatement, isForStatement, PostfixUnaryExpression, SuperCall, isSuperCall, getClassExtendsHeritageElement, getThisParameter, getJSDocClassTag, JSDocFunctionType, getJSDocThisTag, getSuperContainer, isSuperProperty, walkUpParenthesizedExpressions, AwaitExpression, YieldExpression, TemplateExpression, TaggedTemplateExpression, isDefaultedExpandoInitializer, getElementOrPropertyAccessName, JsxSpreadAttribute, NewExpression, AssertionExpression, indexOfNode, JsxOpeningLikeElement, JsxReferenceKind, isInJsonFile, getJSDocEnumTag, isWellKnownSymbolSyntactically, JsxSelfClosingElement, JsxFragment, JsxEmit, stringContains, JsxTagNameExpression, isIntrinsicJsxName, JsxClosingElement, JsxFlags, JsxOpeningFragment, isThisProperty, getClassLikeDeclarationOfSymbol, getTextOfIdentifierOrLiteral, PropertyAccessChain, isCallOrNewExpression, tryGetPropertyAccessOrIdentifierToString, getSpellingSuggestion, ForInStatement, VariableDeclarationList, ElementAccessChain, JsxOpeningElement, isTaggedTemplateExpression, last, isJsxSelfClosingElement, entityNameToString, isOptionalChainRoot, skipOuterExpressions, getErrorSpanForNode, elementAt, createDiagnosticForNodeArray, flatten, minAndMax, isLineBreak, skipTrivia, isPrototypeAccess, getInitializerOfBinaryExpression, isDottedName, ImportCall, SyntheticDefaultModuleType, UnaryExpression, MetaProperty, getNewTargetContainer, forEachYieldExpression, forEachReturnStatement, OuterExpressionKinds, DeleteExpression, VoidExpression, TextSpan, isEffectiveExternalModule, tokenToString, isPrivateIdentifierPropertyAccessExpression, isAssignmentOperator, isJSDocTypedefTag, HasExpressionInitializer, getEffectiveInitializer, isDeclarationReadonly, isParenthesizedExpression, isArrayLiteralExpression, isSpreadElement, CallChain, TypeAssertion, StringLiteralLike, TypeLiteralNode, ExpressionStatement, isPrologueDirective, isInJSDoc, tryCast, isTypeReferenceType, isPropertyNameLiteral, getEscapedTextOfIdentifierOrLiteral, PromiseOrAwaitableType, getEntityNameFromTypeNode, getRestParameterElementType, nodeCanBeDecorated, getFirstConstructorWithBody, JSDocTemplateTag, JSDocTypeTag, findLast, JSDocImplementsTag, JSDocAugmentsTag, CaseBlock, NodeSet, DeclarationWithTypeParameterChildren, rangeOfNode, rangeOfTypeParameters, isObjectBindingPattern, isArrayBindingPattern, isVariableLike, VariableStatement, IfStatement, DoStatement, WhileStatement, ForInOrOfStatement, AwaitKeywordToken, MatchingKeys, IterableOrIteratorType, BreakOrContinueStatement, ReturnStatement, WithStatement, CaseOrDefaultClause, LabeledStatement, ThrowStatement, TryStatement, forEachKey, getTextOfPropertyName, isEnumConst, isLiteralExpression, getExternalModuleName, hasModifiers, isNamespaceExport, forEachImportClauseDeclaration, getEmitDeclarations, JSDocContainer, isJSDocTypeExpression, clear, ImportsNotUsedAsValues, relativeComplement, compareDiagnostics, introducesArgumentsExoticObject, getCombinedLocalAndExportSymbolFlags, isDeclarationName, PropertyAccessEntityNameExpression, getTypeParameterFromJsDoc, isJSXTagName, isImportOrExportSpecifier, isLiteralComputedPropertyDeclarationName, isInExpressionContext, isLiteralTypeNode, tryGetClassImplementingOrExtendingExpressionWithTypeArguments, isDeclaration, AssignmentPattern, singleElementArray, isGeneratedIdentifier, isModuleOrEnumDeclaration, isStatementWithLocals, isBlockScopedContainerTopLevel, isImportEqualsDeclaration, TypeReferenceSerializationKind, isVariableLikeOrAccessor, KeywordTypeNode, EmitResolver, AllAccessorDeclarations, isGetOrSetAccessorDeclaration, SetAccessorDeclaration, GetAccessorDeclaration, resolveTripleslashReference, AnyImportOrReExport, bindSourceFile, externalHelpersModuleNameText, getAllAccessorDeclarations, modifierToFlag, findUseStrictPrologue, isArrowFunction, getLineAndCharacterOfPosition, ExclamationToken, isCommaSequence, getSetAccessorValueParameter, isVariableDeclarationInVariableStatement, isLet, getJSDocTypeParameterDeclarations, TokenFlags, isChildOfNodeWithKind, isPrefixUnaryExpression, textSpanEnd } from "./ts"; +import { countPathComponents, getModuleSpecifiers } from "./ts.moduleSpecifiers"; +import { mark, measure } from "./ts.performance"; +import * as ts from "./ts"; /* @internal */ -namespace ts { - const ambientModuleSymbolRegex = /^".+"$/; - const anon = "(anonymous)" as __String & string; - - let nextSymbolId = 1; - let nextNodeId = 1; - let nextMergeId = 1; - let nextFlowId = 1; - - const enum IterationUse { - AllowsSyncIterablesFlag = 1 << 0, - AllowsAsyncIterablesFlag = 1 << 1, - AllowsStringInputFlag = 1 << 2, - ForOfFlag = 1 << 3, - YieldStarFlag = 1 << 4, - SpreadFlag = 1 << 5, - DestructuringFlag = 1 << 6, - - // Spread, Destructuring, Array element assignment - Element = AllowsSyncIterablesFlag, - Spread = AllowsSyncIterablesFlag | SpreadFlag, - Destructuring = AllowsSyncIterablesFlag | DestructuringFlag, - - ForOf = AllowsSyncIterablesFlag | AllowsStringInputFlag | ForOfFlag, - ForAwaitOf = AllowsSyncIterablesFlag | AllowsAsyncIterablesFlag | AllowsStringInputFlag | ForOfFlag, - - YieldStar = AllowsSyncIterablesFlag | YieldStarFlag, - AsyncYieldStar = AllowsSyncIterablesFlag | AllowsAsyncIterablesFlag | YieldStarFlag, - - GeneratorReturnType = AllowsSyncIterablesFlag, - AsyncGeneratorReturnType = AllowsAsyncIterablesFlag, - +const ambientModuleSymbolRegex = /^".+"$/; +/* @internal */ +const anon = ("(anonymous)" as __String & string); +/* @internal */ +let nextSymbolId = 1; +/* @internal */ +let nextNodeId = 1; +/* @internal */ +let nextMergeId = 1; +/* @internal */ +let nextFlowId = 1; +/* @internal */ +const enum IterationUse { + AllowsSyncIterablesFlag = 1 << 0, + AllowsAsyncIterablesFlag = 1 << 1, + AllowsStringInputFlag = 1 << 2, + ForOfFlag = 1 << 3, + YieldStarFlag = 1 << 4, + SpreadFlag = 1 << 5, + DestructuringFlag = 1 << 6, + // Spread, Destructuring, Array element assignment + Element = AllowsSyncIterablesFlag, + Spread = AllowsSyncIterablesFlag | SpreadFlag, + Destructuring = AllowsSyncIterablesFlag | DestructuringFlag, + ForOf = AllowsSyncIterablesFlag | AllowsStringInputFlag | ForOfFlag, + ForAwaitOf = AllowsSyncIterablesFlag | AllowsAsyncIterablesFlag | AllowsStringInputFlag | ForOfFlag, + YieldStar = AllowsSyncIterablesFlag | YieldStarFlag, + AsyncYieldStar = AllowsSyncIterablesFlag | AllowsAsyncIterablesFlag | YieldStarFlag, + GeneratorReturnType = AllowsSyncIterablesFlag, + AsyncGeneratorReturnType = AllowsAsyncIterablesFlag +} +/* @internal */ +const enum IterationTypeKind { + Yield, + Return, + Next +} +/* @internal */ +interface IterationTypesResolver { + iterableCacheKey: "iterationTypesOfAsyncIterable" | "iterationTypesOfIterable"; + iteratorCacheKey: "iterationTypesOfAsyncIterator" | "iterationTypesOfIterator"; + iteratorSymbolName: "asyncIterator" | "iterator"; + getGlobalIteratorType: (reportErrors: boolean) => GenericType; + getGlobalIterableType: (reportErrors: boolean) => GenericType; + getGlobalIterableIteratorType: (reportErrors: boolean) => GenericType; + getGlobalGeneratorType: (reportErrors: boolean) => GenericType; + resolveIterationType: (type: Type, errorNode: Node | undefined) => Type | undefined; + mustHaveANextMethodDiagnostic: DiagnosticMessage; + mustBeAMethodDiagnostic: DiagnosticMessage; + mustHaveAValueDiagnostic: DiagnosticMessage; +} +/* @internal */ +const enum WideningKind { + Normal, + GeneratorYield +} +/* @internal */ +const enum TypeFacts { + None = 0, + TypeofEQString = 1 << 0, + TypeofEQNumber = 1 << 1, + TypeofEQBigInt = 1 << 2, + TypeofEQBoolean = 1 << 3, + TypeofEQSymbol = 1 << 4, + TypeofEQObject = 1 << 5, + TypeofEQFunction = 1 << 6, + TypeofEQHostObject = 1 << 7, + TypeofNEString = 1 << 8, + TypeofNENumber = 1 << 9, + TypeofNEBigInt = 1 << 10, + TypeofNEBoolean = 1 << 11, + TypeofNESymbol = 1 << 12, + TypeofNEObject = 1 << 13, + TypeofNEFunction = 1 << 14, + TypeofNEHostObject = 1 << 15, + EQUndefined = 1 << 16, + EQNull = 1 << 17, + EQUndefinedOrNull = 1 << 18, + NEUndefined = 1 << 19, + NENull = 1 << 20, + NEUndefinedOrNull = 1 << 21, + Truthy = 1 << 22, + Falsy = 1 << 23, + All = (1 << 24) - 1, + // The following members encode facts about particular kinds of types for use in the getTypeFacts function. + // The presence of a particular fact means that the given test is true for some (and possibly all) values + // of that kind of type. + BaseStringStrictFacts = TypeofEQString | TypeofNENumber | TypeofNEBigInt | TypeofNEBoolean | TypeofNESymbol | TypeofNEObject | TypeofNEFunction | TypeofNEHostObject | NEUndefined | NENull | NEUndefinedOrNull, + BaseStringFacts = BaseStringStrictFacts | EQUndefined | EQNull | EQUndefinedOrNull | Falsy, + StringStrictFacts = BaseStringStrictFacts | Truthy | Falsy, + StringFacts = BaseStringFacts | Truthy, + EmptyStringStrictFacts = BaseStringStrictFacts | Falsy, + EmptyStringFacts = BaseStringFacts, + NonEmptyStringStrictFacts = BaseStringStrictFacts | Truthy, + NonEmptyStringFacts = BaseStringFacts | Truthy, + BaseNumberStrictFacts = TypeofEQNumber | TypeofNEString | TypeofNEBigInt | TypeofNEBoolean | TypeofNESymbol | TypeofNEObject | TypeofNEFunction | TypeofNEHostObject | NEUndefined | NENull | NEUndefinedOrNull, + BaseNumberFacts = BaseNumberStrictFacts | EQUndefined | EQNull | EQUndefinedOrNull | Falsy, + NumberStrictFacts = BaseNumberStrictFacts | Truthy | Falsy, + NumberFacts = BaseNumberFacts | Truthy, + ZeroNumberStrictFacts = BaseNumberStrictFacts | Falsy, + ZeroNumberFacts = BaseNumberFacts, + NonZeroNumberStrictFacts = BaseNumberStrictFacts | Truthy, + NonZeroNumberFacts = BaseNumberFacts | Truthy, + BaseBigIntStrictFacts = TypeofEQBigInt | TypeofNEString | TypeofNENumber | TypeofNEBoolean | TypeofNESymbol | TypeofNEObject | TypeofNEFunction | TypeofNEHostObject | NEUndefined | NENull | NEUndefinedOrNull, + BaseBigIntFacts = BaseBigIntStrictFacts | EQUndefined | EQNull | EQUndefinedOrNull | Falsy, + BigIntStrictFacts = BaseBigIntStrictFacts | Truthy | Falsy, + BigIntFacts = BaseBigIntFacts | Truthy, + ZeroBigIntStrictFacts = BaseBigIntStrictFacts | Falsy, + ZeroBigIntFacts = BaseBigIntFacts, + NonZeroBigIntStrictFacts = BaseBigIntStrictFacts | Truthy, + NonZeroBigIntFacts = BaseBigIntFacts | Truthy, + BaseBooleanStrictFacts = TypeofEQBoolean | TypeofNEString | TypeofNENumber | TypeofNEBigInt | TypeofNESymbol | TypeofNEObject | TypeofNEFunction | TypeofNEHostObject | NEUndefined | NENull | NEUndefinedOrNull, + BaseBooleanFacts = BaseBooleanStrictFacts | EQUndefined | EQNull | EQUndefinedOrNull | Falsy, + BooleanStrictFacts = BaseBooleanStrictFacts | Truthy | Falsy, + BooleanFacts = BaseBooleanFacts | Truthy, + FalseStrictFacts = BaseBooleanStrictFacts | Falsy, + FalseFacts = BaseBooleanFacts, + TrueStrictFacts = BaseBooleanStrictFacts | Truthy, + TrueFacts = BaseBooleanFacts | Truthy, + SymbolStrictFacts = TypeofEQSymbol | TypeofNEString | TypeofNENumber | TypeofNEBigInt | TypeofNEBoolean | TypeofNEObject | TypeofNEFunction | TypeofNEHostObject | NEUndefined | NENull | NEUndefinedOrNull | Truthy, + SymbolFacts = SymbolStrictFacts | EQUndefined | EQNull | EQUndefinedOrNull | Falsy, + ObjectStrictFacts = TypeofEQObject | TypeofEQHostObject | TypeofNEString | TypeofNENumber | TypeofNEBigInt | TypeofNEBoolean | TypeofNESymbol | TypeofNEFunction | NEUndefined | NENull | NEUndefinedOrNull | Truthy, + ObjectFacts = ObjectStrictFacts | EQUndefined | EQNull | EQUndefinedOrNull | Falsy, + FunctionStrictFacts = TypeofEQFunction | TypeofEQHostObject | TypeofNEString | TypeofNENumber | TypeofNEBigInt | TypeofNEBoolean | TypeofNESymbol | TypeofNEObject | NEUndefined | NENull | NEUndefinedOrNull | Truthy, + FunctionFacts = FunctionStrictFacts | EQUndefined | EQNull | EQUndefinedOrNull | Falsy, + UndefinedFacts = TypeofNEString | TypeofNENumber | TypeofNEBigInt | TypeofNEBoolean | TypeofNESymbol | TypeofNEObject | TypeofNEFunction | TypeofNEHostObject | EQUndefined | EQUndefinedOrNull | NENull | Falsy, + NullFacts = TypeofEQObject | TypeofNEString | TypeofNENumber | TypeofNEBigInt | TypeofNEBoolean | TypeofNESymbol | TypeofNEFunction | TypeofNEHostObject | EQNull | EQUndefinedOrNull | NEUndefined | Falsy, + EmptyObjectStrictFacts = All & ~(EQUndefined | EQNull | EQUndefinedOrNull), + EmptyObjectFacts = All +} +/* @internal */ +const typeofEQFacts: ts.ReadonlyMap = createMapFromTemplate({ + string: TypeFacts.TypeofEQString, + number: TypeFacts.TypeofEQNumber, + bigint: TypeFacts.TypeofEQBigInt, + boolean: TypeFacts.TypeofEQBoolean, + symbol: TypeFacts.TypeofEQSymbol, + undefined: TypeFacts.EQUndefined, + object: TypeFacts.TypeofEQObject, + function: TypeFacts.TypeofEQFunction +}); +/* @internal */ +const typeofNEFacts: ts.ReadonlyMap = createMapFromTemplate({ + string: TypeFacts.TypeofNEString, + number: TypeFacts.TypeofNENumber, + bigint: TypeFacts.TypeofNEBigInt, + boolean: TypeFacts.TypeofNEBoolean, + symbol: TypeFacts.TypeofNESymbol, + undefined: TypeFacts.NEUndefined, + object: TypeFacts.TypeofNEObject, + function: TypeFacts.TypeofNEFunction +}); +/* @internal */ +type TypeSystemEntity = Node | Symbol | Type | Signature; +/* @internal */ +const enum TypeSystemPropertyName { + Type, + ResolvedBaseConstructorType, + DeclaredType, + ResolvedReturnType, + ImmediateBaseConstraint, + EnumTagType, + ResolvedTypeArguments +} +/* @internal */ +const enum CheckMode { + Normal = 0, + Contextual = 1 << 0, + Inferential = 1 << 1, + SkipContextSensitive = 1 << 2, + SkipGenericFunctions = 1 << 3, + IsForSignatureHelp = 1 << 4 +} +/* @internal */ +const enum AccessFlags { + None = 0, + NoIndexSignatures = 1 << 0, + Writing = 1 << 1, + CacheSymbol = 1 << 2, + NoTupleBoundsCheck = 1 << 3 +} +/* @internal */ +const enum SignatureCheckMode { + BivariantCallback = 1 << 0, + StrictCallback = 1 << 1, + IgnoreReturnTypes = 1 << 2, + StrictArity = 1 << 3, + Callback = BivariantCallback | StrictCallback +} +/* @internal */ +const enum IntersectionState { + None = 0, + Source = 1 << 0, + Target = 1 << 1 +} +/* @internal */ +const enum MappedTypeModifiers { + IncludeReadonly = 1 << 0, + ExcludeReadonly = 1 << 1, + IncludeOptional = 1 << 2, + ExcludeOptional = 1 << 3 +} +/* @internal */ +const enum ExpandingFlags { + None = 0, + Source = 1, + Target = 1 << 1, + Both = Source | Target +} +/* @internal */ +const enum MembersOrExportsResolutionKind { + resolvedExports = "resolvedExports", + resolvedMembers = "resolvedMembers" +} +/* @internal */ +const enum UnusedKind { + Local, + Parameter +} +/** @param containingNode Node to check for parse error */ +/* @internal */ +type AddUnusedDiagnostic = (containingNode: Node, type: UnusedKind, diagnostic: DiagnosticWithLocation) => void; +/* @internal */ +const isNotOverloadAndNotAccessor = and(isNotOverload, isNotAccessor); +/* @internal */ +const enum DeclarationMeaning { + GetAccessor = 1, + SetAccessor = 2, + PropertyAssignment = 4, + Method = 8, + GetOrSetAccessor = GetAccessor | SetAccessor, + PropertyAssignmentOrMethod = PropertyAssignment | Method +} +/* @internal */ +const enum DeclarationSpaces { + None = 0, + ExportValue = 1 << 0, + ExportType = 1 << 1, + ExportNamespace = 1 << 2 +} +/* @internal */ +export function getNodeId(node: Node): number { + if (!node.id) { + node.id = nextNodeId; + nextNodeId++; } - - const enum IterationTypeKind { - Yield, - Return, - Next, + return node.id; +} +/* @internal */ +export function getSymbolId(symbol: Symbol): number { + if (!symbol.id) { + symbol.id = nextSymbolId; + nextSymbolId++; } - - interface IterationTypesResolver { - iterableCacheKey: "iterationTypesOfAsyncIterable" | "iterationTypesOfIterable"; - iteratorCacheKey: "iterationTypesOfAsyncIterator" | "iterationTypesOfIterator"; - iteratorSymbolName: "asyncIterator" | "iterator"; - getGlobalIteratorType: (reportErrors: boolean) => GenericType; - getGlobalIterableType: (reportErrors: boolean) => GenericType; - getGlobalIterableIteratorType: (reportErrors: boolean) => GenericType; - getGlobalGeneratorType: (reportErrors: boolean) => GenericType; - resolveIterationType: (type: Type, errorNode: Node | undefined) => Type | undefined; - mustHaveANextMethodDiagnostic: DiagnosticMessage; - mustBeAMethodDiagnostic: DiagnosticMessage; - mustHaveAValueDiagnostic: DiagnosticMessage; + return symbol.id; +} +/* @internal */ +export function isInstantiatedModule(node: ModuleDeclaration, preserveConstEnums: boolean) { + const moduleState = getModuleInstanceState(node); + return moduleState === ModuleInstanceState.Instantiated || + (preserveConstEnums && moduleState === ModuleInstanceState.ConstEnumOnly); +} +/* @internal */ +export function createTypeChecker(host: TypeCheckerHost, produceDiagnostics: boolean): TypeChecker { + const getPackagesSet: () => ts.Map = memoize(() => { + const set = createMap(); + host.getSourceFiles().forEach(sf => { + if (!sf.resolvedModules) + return; + forEachEntry(sf.resolvedModules, r => { + if (r && r.packageId) + set.set(r.packageId.name, true); + }); + }); + return set; + }); + // Cancellation that controls whether or not we can cancel in the middle of type checking. + // In general cancelling is *not* safe for the type checker. We might be in the middle of + // computing something, and we will leave our internals in an inconsistent state. Callers + // who set the cancellation token should catch if a cancellation exception occurs, and + // should throw away and create a new TypeChecker. + // + // Currently we only support setting the cancellation token when getting diagnostics. This + // is because diagnostics can be quite expensive, and we want to allow hosts to bail out if + // they no longer need the information (for example, if the user started editing again). + let cancellationToken: CancellationToken | undefined; + let requestedExternalEmitHelpers: ExternalEmitHelpers; + let externalHelpersModule: ts.Symbol; + const Symbol = objectAllocator.getSymbolConstructor(); + const Type = objectAllocator.getTypeConstructor(); + const Signature = objectAllocator.getSignatureConstructor(); + let typeCount = 0; + let symbolCount = 0; + let enumCount = 0; + let totalInstantiationCount = 0; + let instantiationCount = 0; + let instantiationDepth = 0; + let constraintDepth = 0; + let currentNode: Node | undefined; + const emptySymbols = createSymbolTable(); + const identityMapper: (type: ts.Type) => ts.Type = identity; + const arrayVariances = [VarianceFlags.Covariant]; + const compilerOptions = host.getCompilerOptions(); + const languageVersion = getEmitScriptTarget(compilerOptions); + const moduleKind = getEmitModuleKind(compilerOptions); + const allowSyntheticDefaultImports = getAllowSyntheticDefaultImports(compilerOptions); + const strictNullChecks = getStrictOptionValue(compilerOptions, "strictNullChecks"); + const strictFunctionTypes = getStrictOptionValue(compilerOptions, "strictFunctionTypes"); + const strictBindCallApply = getStrictOptionValue(compilerOptions, "strictBindCallApply"); + const strictPropertyInitialization = getStrictOptionValue(compilerOptions, "strictPropertyInitialization"); + const noImplicitAny = getStrictOptionValue(compilerOptions, "noImplicitAny"); + const noImplicitThis = getStrictOptionValue(compilerOptions, "noImplicitThis"); + const keyofStringsOnly = !!compilerOptions.keyofStringsOnly; + const freshObjectLiteralFlag = compilerOptions.suppressExcessPropertyErrors ? 0 : ObjectFlags.FreshLiteral; + const emitResolver = createResolver(); + const nodeBuilder = createNodeBuilder(); + const globals = createSymbolTable(); + const undefinedSymbol = createSymbol(SymbolFlags.Property, ("undefined" as __String)); + undefinedSymbol.declarations = []; + const globalThisSymbol = createSymbol(SymbolFlags.Module, ("globalThis" as __String), CheckFlags.Readonly); + globalThisSymbol.exports = globals; + globalThisSymbol.declarations = []; + globals.set(globalThisSymbol.escapedName, globalThisSymbol); + const argumentsSymbol = createSymbol(SymbolFlags.Property, ("arguments" as __String)); + const requireSymbol = createSymbol(SymbolFlags.Property, ("require" as __String)); + /** This will be set during calls to `getResolvedSignature` where services determines an apparent number of arguments greater than what is actually provided. */ + let apparentArgumentCount: number | undefined; + // for public members that accept a Node or one of its subtypes, we must guard against + // synthetic nodes created during transformations by calling `getParseTreeNode`. + // for most of these, we perform the guard only on `checker` to avoid any possible + // extra cost of calling `getParseTreeNode` when calling these functions from inside the + // checker. + const checker: TypeChecker = { + getNodeCount: () => sum(host.getSourceFiles(), "nodeCount"), + getIdentifierCount: () => sum(host.getSourceFiles(), "identifierCount"), + getSymbolCount: () => sum(host.getSourceFiles(), "symbolCount") + symbolCount, + getTypeCount: () => typeCount, + getInstantiationCount: () => totalInstantiationCount, + getRelationCacheSizes: () => ({ + assignable: assignableRelation.size, + identity: identityRelation.size, + subtype: subtypeRelation.size, + strictSubtype: strictSubtypeRelation.size, + }), + isUndefinedSymbol: symbol => symbol === undefinedSymbol, + isArgumentsSymbol: symbol => symbol === argumentsSymbol, + isUnknownSymbol: symbol => symbol === unknownSymbol, + getMergedSymbol, + getDiagnostics, + getGlobalDiagnostics, + getTypeOfSymbolAtLocation: (symbol, location) => { + location = getParseTreeNode(location); + return location ? getTypeOfSymbolAtLocation(symbol, location) : errorType; + }, + getSymbolsOfParameterPropertyDeclaration: (parameterIn, parameterName) => { + const parameter = getParseTreeNode(parameterIn, isParameter); + if (parameter === undefined) + return Debug.fail("Cannot get symbols of a synthetic parameter that cannot be resolved to a parse-tree node."); + return getSymbolsOfParameterPropertyDeclaration(parameter, escapeLeadingUnderscores(parameterName)); + }, + getDeclaredTypeOfSymbol, + getPropertiesOfType, + getPropertyOfType: (type, name) => getPropertyOfType(type, escapeLeadingUnderscores(name)), + getPrivateIdentifierPropertyOfType: (leftType: ts.Type, name: string, location: Node) => { + const node = getParseTreeNode(location); + if (!node) { + return undefined; + } + const propName = escapeLeadingUnderscores(name); + const lexicallyScopedIdentifier = lookupSymbolForPrivateIdentifierDeclaration(propName, node); + return lexicallyScopedIdentifier ? getPrivateIdentifierPropertyOfType(leftType, lexicallyScopedIdentifier) : undefined; + }, + getTypeOfPropertyOfType: (type, name) => getTypeOfPropertyOfType(type, escapeLeadingUnderscores(name)), + getIndexInfoOfType, + getSignaturesOfType, + getIndexTypeOfType, + getBaseTypes, + getBaseTypeOfLiteralType, + getWidenedType, + getTypeFromTypeNode: nodeIn => { + const node = getParseTreeNode(nodeIn, isTypeNode); + return node ? getTypeFromTypeNode(node) : errorType; + }, + getParameterType: getTypeAtPosition, + getPromisedTypeOfPromise, + getReturnTypeOfSignature, + isNullableType, + getNullableType, + getNonNullableType, + getNonOptionalType: removeOptionalTypeMarker, + getTypeArguments, + typeToTypeNode: nodeBuilder.typeToTypeNode, + indexInfoToIndexSignatureDeclaration: nodeBuilder.indexInfoToIndexSignatureDeclaration, + signatureToSignatureDeclaration: nodeBuilder.signatureToSignatureDeclaration, + symbolToEntityName: nodeBuilder.symbolToEntityName, + symbolToExpression: nodeBuilder.symbolToExpression, + symbolToTypeParameterDeclarations: nodeBuilder.symbolToTypeParameterDeclarations, + symbolToParameterDeclaration: nodeBuilder.symbolToParameterDeclaration, + typeParameterToDeclaration: nodeBuilder.typeParameterToDeclaration, + getSymbolsInScope: (location, meaning) => { + location = getParseTreeNode(location); + return location ? getSymbolsInScope(location, meaning) : []; + }, + getSymbolAtLocation: node => { + node = getParseTreeNode(node); + // set ignoreErrors: true because any lookups invoked by the API shouldn't cause any new errors + return node ? getSymbolAtLocation(node, /*ignoreErrors*/ true) : undefined; + }, + getShorthandAssignmentValueSymbol: node => { + node = getParseTreeNode(node); + return node ? getShorthandAssignmentValueSymbol(node) : undefined; + }, + getExportSpecifierLocalTargetSymbol: nodeIn => { + const node = getParseTreeNode(nodeIn, isExportSpecifier); + return node ? getExportSpecifierLocalTargetSymbol(node) : undefined; + }, + getExportSymbolOfSymbol(symbol) { + return getMergedSymbol(symbol.exportSymbol || symbol); + }, + getTypeAtLocation: node => { + node = getParseTreeNode(node); + return node ? getTypeOfNode(node) : errorType; + }, + getTypeOfAssignmentPattern: nodeIn => { + const node = getParseTreeNode(nodeIn, isAssignmentPattern); + return node && getTypeOfAssignmentPattern(node) || errorType; + }, + getPropertySymbolOfDestructuringAssignment: locationIn => { + const location = getParseTreeNode(locationIn, isIdentifier); + return location ? getPropertySymbolOfDestructuringAssignment(location) : undefined; + }, + signatureToString: (signature, enclosingDeclaration, flags, kind) => { + return signatureToString(signature, getParseTreeNode(enclosingDeclaration), flags, kind); + }, + typeToString: (type, enclosingDeclaration, flags) => { + return typeToString(type, getParseTreeNode(enclosingDeclaration), flags); + }, + symbolToString: (symbol, enclosingDeclaration, meaning, flags) => { + return symbolToString(symbol, getParseTreeNode(enclosingDeclaration), meaning, flags); + }, + typePredicateToString: (predicate, enclosingDeclaration, flags) => { + return typePredicateToString(predicate, getParseTreeNode(enclosingDeclaration), flags); + }, + writeSignature: (signature, enclosingDeclaration, flags, kind, writer) => { + return signatureToString(signature, getParseTreeNode(enclosingDeclaration), flags, kind, writer); + }, + writeType: (type, enclosingDeclaration, flags, writer) => { + return typeToString(type, getParseTreeNode(enclosingDeclaration), flags, writer); + }, + writeSymbol: (symbol, enclosingDeclaration, meaning, flags, writer) => { + return symbolToString(symbol, getParseTreeNode(enclosingDeclaration), meaning, flags, writer); + }, + writeTypePredicate: (predicate, enclosingDeclaration, flags, writer) => { + return typePredicateToString(predicate, getParseTreeNode(enclosingDeclaration), flags, writer); + }, + getAugmentedPropertiesOfType, + getRootSymbols, + getContextualType: (nodeIn: Expression, contextFlags?: ContextFlags) => { + const node = getParseTreeNode(nodeIn, isExpression); + if (!node) { + return undefined; + } + const containingCall = findAncestor(node, isCallLikeExpression); + const containingCallResolvedSignature = containingCall && getNodeLinks(containingCall).resolvedSignature; + if ((contextFlags!) & ContextFlags.Completions && containingCall) { + let toMarkSkip = (node as Node); + do { + getNodeLinks(toMarkSkip).skipDirectInference = true; + toMarkSkip = toMarkSkip.parent; + } while (toMarkSkip && toMarkSkip !== containingCall); + getNodeLinks(containingCall).resolvedSignature = undefined; + } + const result = getContextualType(node, contextFlags); + if ((contextFlags!) & ContextFlags.Completions && containingCall) { + let toMarkSkip = (node as Node); + do { + getNodeLinks(toMarkSkip).skipDirectInference = undefined; + toMarkSkip = toMarkSkip.parent; + } while (toMarkSkip && toMarkSkip !== containingCall); + getNodeLinks(containingCall).resolvedSignature = containingCallResolvedSignature; + } + return result; + }, + getContextualTypeForObjectLiteralElement: nodeIn => { + const node = getParseTreeNode(nodeIn, isObjectLiteralElementLike); + return node ? getContextualTypeForObjectLiteralElement(node) : undefined; + }, + getContextualTypeForArgumentAtIndex: (nodeIn, argIndex) => { + const node = getParseTreeNode(nodeIn, isCallLikeExpression); + return node && getContextualTypeForArgumentAtIndex(node, argIndex); + }, + getContextualTypeForJsxAttribute: (nodeIn) => { + const node = getParseTreeNode(nodeIn, isJsxAttributeLike); + return node && getContextualTypeForJsxAttribute(node); + }, + isContextSensitive, + getFullyQualifiedName, + getResolvedSignature: (node, candidatesOutArray, argumentCount) => getResolvedSignatureWorker(node, candidatesOutArray, argumentCount, CheckMode.Normal), + getResolvedSignatureForSignatureHelp: (node, candidatesOutArray, argumentCount) => getResolvedSignatureWorker(node, candidatesOutArray, argumentCount, CheckMode.IsForSignatureHelp), + getExpandedParameters, + hasEffectiveRestParameter, + getConstantValue: nodeIn => { + const node = getParseTreeNode(nodeIn, canHaveConstantValue); + return node ? getConstantValue(node) : undefined; + }, + isValidPropertyAccess: (nodeIn, propertyName) => { + const node = getParseTreeNode(nodeIn, isPropertyAccessOrQualifiedNameOrImportTypeNode); + return !!node && isValidPropertyAccess(node, escapeLeadingUnderscores(propertyName)); + }, + isValidPropertyAccessForCompletions: (nodeIn, type, property) => { + const node = getParseTreeNode(nodeIn, isPropertyAccessExpression); + return !!node && isValidPropertyAccessForCompletions(node, type, property); + }, + getSignatureFromDeclaration: declarationIn => { + const declaration = getParseTreeNode(declarationIn, isFunctionLike); + return declaration ? getSignatureFromDeclaration(declaration) : undefined; + }, + isImplementationOfOverload: node => { + const parsed = getParseTreeNode(node, isFunctionLike); + return parsed ? isImplementationOfOverload(parsed) : undefined; + }, + getImmediateAliasedSymbol, + getAliasedSymbol: resolveAlias, + getEmitResolver, + getExportsOfModule: getExportsOfModuleAsArray, + getExportsAndPropertiesOfModule, + getSymbolWalker: createGetSymbolWalker(getRestTypeOfSignature, getTypePredicateOfSignature, getReturnTypeOfSignature, getBaseTypes, resolveStructuredTypeMembers, getTypeOfSymbol, getResolvedSymbol, getIndexTypeOfStructuredType, getConstraintOfTypeParameter, getFirstIdentifier, getTypeArguments), + getAmbientModules, + getJsxIntrinsicTagNamesAt, + isOptionalParameter: nodeIn => { + const node = getParseTreeNode(nodeIn, isParameter); + return node ? isOptionalParameter(node) : false; + }, + tryGetMemberInModuleExports: (name, symbol) => tryGetMemberInModuleExports(escapeLeadingUnderscores(name), symbol), + tryGetMemberInModuleExportsAndProperties: (name, symbol) => tryGetMemberInModuleExportsAndProperties(escapeLeadingUnderscores(name), symbol), + tryFindAmbientModuleWithoutAugmentations: moduleName => { + // we deliberately exclude augmentations + // since we are only interested in declarations of the module itself + return tryFindAmbientModule(moduleName, /*withAugmentations*/ false); + }, + getApparentType, + getUnionType, + isTypeAssignableTo: (source, target) => { + return isTypeAssignableTo(source, target); + }, + createAnonymousType, + createSignature, + createSymbol, + createIndexInfo, + getAnyType: () => anyType, + getStringType: () => stringType, + getNumberType: () => numberType, + createPromiseType, + createArrayType, + getElementTypeOfArrayType, + getBooleanType: () => booleanType, + getFalseType: (fresh?) => fresh ? falseType : regularFalseType, + getTrueType: (fresh?) => fresh ? trueType : regularTrueType, + getVoidType: () => voidType, + getUndefinedType: () => undefinedType, + getNullType: () => nullType, + getESSymbolType: () => esSymbolType, + getNeverType: () => neverType, + getOptionalType: () => optionalType, + isSymbolAccessible, + isArrayType, + isTupleType, + isArrayLikeType, + isTypeInvalidDueToUnionDiscriminant, + getAllPossiblePropertiesOfTypes, + getSuggestedSymbolForNonexistentProperty, + getSuggestionForNonexistentProperty, + getSuggestedSymbolForNonexistentSymbol: (location, name, meaning) => getSuggestedSymbolForNonexistentSymbol(location, escapeLeadingUnderscores(name), meaning), + getSuggestionForNonexistentSymbol: (location, name, meaning) => getSuggestionForNonexistentSymbol(location, escapeLeadingUnderscores(name), meaning), + getSuggestedSymbolForNonexistentModule, + getSuggestionForNonexistentExport, + getBaseConstraintOfType, + getDefaultFromTypeParameter: type => type && type.flags & TypeFlags.TypeParameter ? getDefaultFromTypeParameter((type as TypeParameter)) : undefined, + resolveName(name, location, meaning, excludeGlobals) { + return resolveName(location, escapeLeadingUnderscores(name), meaning, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false, excludeGlobals); + }, + getJsxNamespace: n => unescapeLeadingUnderscores(getJsxNamespace(n)), + getAccessibleSymbolChain, + getTypePredicateOfSignature, + resolveExternalModuleSymbol, + tryGetThisTypeAt: (node, includeGlobalThis) => { + node = getParseTreeNode(node); + return node && tryGetThisTypeAt(node, includeGlobalThis); + }, + getTypeArgumentConstraint: nodeIn => { + const node = getParseTreeNode(nodeIn, isTypeNode); + return node && getTypeArgumentConstraint(node); + }, + getSuggestionDiagnostics: (file, ct) => { + if (skipTypeChecking(file, compilerOptions, host)) { + return emptyArray; + } + let diagnostics: DiagnosticWithLocation[] | undefined; + try { + // Record the cancellation token so it can be checked later on during checkSourceElement. + // Do this in a finally block so we can ensure that it gets reset back to nothing after + // this call is done. + cancellationToken = ct; + // Ensure file is type checked + checkSourceFile(file); + Debug.assert(!!(getNodeLinks(file).flags & NodeCheckFlags.TypeChecked)); + diagnostics = addRange(diagnostics, suggestionDiagnostics.getDiagnostics(file.fileName)); + checkUnusedIdentifiers(getPotentiallyUnusedIdentifiers(file), (containingNode, kind, diag) => { + if (!containsParseError(containingNode) && !unusedIsError(kind, !!(containingNode.flags & NodeFlags.Ambient))) { + (diagnostics || (diagnostics = [])).push({ ...diag, category: DiagnosticCategory.Suggestion }); + } + }); + return diagnostics || emptyArray; + } + finally { + cancellationToken = undefined; + } + }, + runWithCancellationToken: (token, callback) => { + try { + cancellationToken = token; + return callback(checker); + } + finally { + cancellationToken = undefined; + } + }, + getLocalTypeParametersOfClassOrInterfaceOrTypeAlias, + isDeclarationVisible, + }; + function getResolvedSignatureWorker(nodeIn: CallLikeExpression, candidatesOutArray: ts.Signature[] | undefined, argumentCount: number | undefined, checkMode: CheckMode): ts.Signature | undefined { + const node = getParseTreeNode(nodeIn, isCallLikeExpression); + apparentArgumentCount = argumentCount; + const res = node ? getResolvedSignature(node, candidatesOutArray, checkMode) : undefined; + apparentArgumentCount = undefined; + return res; } - - const enum WideningKind { - Normal, - GeneratorYield + const tupleTypes = createMap(); + const unionTypes = createMap(); + const intersectionTypes = createMap(); + const literalTypes = createMap(); + const indexedAccessTypes = createMap(); + const substitutionTypes = createMap(); + const evolvingArrayTypes: EvolvingArrayType[] = []; + const undefinedProperties = (createMap() as UnderscoreEscapedMap); + const unknownSymbol = createSymbol(SymbolFlags.Property, ("unknown" as __String)); + const resolvingSymbol = createSymbol(0, InternalSymbolName.Resolving); + const anyType = createIntrinsicType(TypeFlags.Any, "any"); + const autoType = createIntrinsicType(TypeFlags.Any, "any"); + const wildcardType = createIntrinsicType(TypeFlags.Any, "any"); + const errorType = createIntrinsicType(TypeFlags.Any, "error"); + const nonInferrableAnyType = createIntrinsicType(TypeFlags.Any, "any", ObjectFlags.ContainsWideningType); + const unknownType = createIntrinsicType(TypeFlags.Unknown, "unknown"); + const undefinedType = createIntrinsicType(TypeFlags.Undefined, "undefined"); + const undefinedWideningType = strictNullChecks ? undefinedType : createIntrinsicType(TypeFlags.Undefined, "undefined", ObjectFlags.ContainsWideningType); + const optionalType = createIntrinsicType(TypeFlags.Undefined, "undefined"); + const nullType = createIntrinsicType(TypeFlags.Null, "null"); + const nullWideningType = strictNullChecks ? nullType : createIntrinsicType(TypeFlags.Null, "null", ObjectFlags.ContainsWideningType); + const stringType = createIntrinsicType(TypeFlags.String, "string"); + const numberType = createIntrinsicType(TypeFlags.Number, "number"); + const bigintType = createIntrinsicType(TypeFlags.BigInt, "bigint"); + const falseType = (createIntrinsicType(TypeFlags.BooleanLiteral, "false") as FreshableIntrinsicType); + const regularFalseType = (createIntrinsicType(TypeFlags.BooleanLiteral, "false") as FreshableIntrinsicType); + const trueType = (createIntrinsicType(TypeFlags.BooleanLiteral, "true") as FreshableIntrinsicType); + const regularTrueType = (createIntrinsicType(TypeFlags.BooleanLiteral, "true") as FreshableIntrinsicType); + trueType.regularType = regularTrueType; + trueType.freshType = trueType; + regularTrueType.regularType = regularTrueType; + regularTrueType.freshType = trueType; + falseType.regularType = regularFalseType; + falseType.freshType = falseType; + regularFalseType.regularType = regularFalseType; + regularFalseType.freshType = falseType; + const booleanType = createBooleanType([regularFalseType, regularTrueType]); + // Also mark all combinations of fresh/regular booleans as "Boolean" so they print as `boolean` instead of `true | false` + // (The union is cached, so simply doing the marking here is sufficient) + createBooleanType([regularFalseType, trueType]); + createBooleanType([falseType, regularTrueType]); + createBooleanType([falseType, trueType]); + const esSymbolType = createIntrinsicType(TypeFlags.ESSymbol, "symbol"); + const voidType = createIntrinsicType(TypeFlags.Void, "void"); + const neverType = createIntrinsicType(TypeFlags.Never, "never"); + const silentNeverType = createIntrinsicType(TypeFlags.Never, "never"); + const nonInferrableType = createIntrinsicType(TypeFlags.Never, "never", ObjectFlags.NonInferrableType); + const implicitNeverType = createIntrinsicType(TypeFlags.Never, "never"); + const unreachableNeverType = createIntrinsicType(TypeFlags.Never, "never"); + const nonPrimitiveType = createIntrinsicType(TypeFlags.NonPrimitive, "object"); + const stringNumberSymbolType = getUnionType([stringType, numberType, esSymbolType]); + const keyofConstraintType = keyofStringsOnly ? stringType : stringNumberSymbolType; + const numberOrBigIntType = getUnionType([numberType, bigintType]); + const emptyObjectType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined); + const emptyJsxObjectType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined); + emptyJsxObjectType.objectFlags |= ObjectFlags.JsxAttributes; + const emptyTypeLiteralSymbol = createSymbol(SymbolFlags.TypeLiteral, InternalSymbolName.Type); + emptyTypeLiteralSymbol.members = createSymbolTable(); + const emptyTypeLiteralType = createAnonymousType(emptyTypeLiteralSymbol, emptySymbols, emptyArray, emptyArray, undefined, undefined); + const emptyGenericType = (createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined)); + emptyGenericType.instantiations = createMap(); + const anyFunctionType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined); + // The anyFunctionType contains the anyFunctionType by definition. The flag is further propagated + // in getPropagatingFlagsOfTypes, and it is checked in inferFromTypes. + anyFunctionType.objectFlags |= ObjectFlags.NonInferrableType; + const noConstraintType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined); + const circularConstraintType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined); + const resolvingDefaultType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined); + const markerSuperType = createTypeParameter(); + const markerSubType = createTypeParameter(); + markerSubType.constraint = markerSuperType; + const markerOtherType = createTypeParameter(); + const noTypePredicate = createTypePredicate(TypePredicateKind.Identifier, "<>", 0, anyType); + const anySignature = createSignature(undefined, undefined, undefined, emptyArray, anyType, /*resolvedTypePredicate*/ undefined, 0, SignatureFlags.None); + const unknownSignature = createSignature(undefined, undefined, undefined, emptyArray, errorType, /*resolvedTypePredicate*/ undefined, 0, SignatureFlags.None); + const resolvingSignature = createSignature(undefined, undefined, undefined, emptyArray, anyType, /*resolvedTypePredicate*/ undefined, 0, SignatureFlags.None); + const silentNeverSignature = createSignature(undefined, undefined, undefined, emptyArray, silentNeverType, /*resolvedTypePredicate*/ undefined, 0, SignatureFlags.None); + const enumNumberIndexInfo = createIndexInfo(stringType, /*isReadonly*/ true); + const iterationTypesCache = createMap(); // cache for common IterationTypes instances + const noIterationTypes: IterationTypes = { + get yieldType(): ts.Type { return Debug.fail("Not supported"); }, + get returnType(): ts.Type { return Debug.fail("Not supported"); }, + get nextType(): ts.Type { return Debug.fail("Not supported"); }, + }; + const anyIterationTypes = createIterationTypes(anyType, anyType, anyType); + const anyIterationTypesExceptNext = createIterationTypes(anyType, anyType, unknownType); + const defaultIterationTypes = createIterationTypes(neverType, anyType, undefinedType); // default iteration types for `Iterator`. + const asyncIterationTypesResolver: IterationTypesResolver = { + iterableCacheKey: "iterationTypesOfAsyncIterable", + iteratorCacheKey: "iterationTypesOfAsyncIterator", + iteratorSymbolName: "asyncIterator", + getGlobalIteratorType: getGlobalAsyncIteratorType, + getGlobalIterableType: getGlobalAsyncIterableType, + getGlobalIterableIteratorType: getGlobalAsyncIterableIteratorType, + getGlobalGeneratorType: getGlobalAsyncGeneratorType, + resolveIterationType: getAwaitedType, + mustHaveANextMethodDiagnostic: Diagnostics.An_async_iterator_must_have_a_next_method, + mustBeAMethodDiagnostic: Diagnostics.The_0_property_of_an_async_iterator_must_be_a_method, + mustHaveAValueDiagnostic: Diagnostics.The_type_returned_by_the_0_method_of_an_async_iterator_must_be_a_promise_for_a_type_with_a_value_property, + }; + const syncIterationTypesResolver: IterationTypesResolver = { + iterableCacheKey: "iterationTypesOfIterable", + iteratorCacheKey: "iterationTypesOfIterator", + iteratorSymbolName: "iterator", + getGlobalIteratorType, + getGlobalIterableType, + getGlobalIterableIteratorType, + getGlobalGeneratorType, + resolveIterationType: (type, _errorNode) => type, + mustHaveANextMethodDiagnostic: Diagnostics.An_iterator_must_have_a_next_method, + mustBeAMethodDiagnostic: Diagnostics.The_0_property_of_an_iterator_must_be_a_method, + mustHaveAValueDiagnostic: Diagnostics.The_type_returned_by_the_0_method_of_an_iterator_must_have_a_value_property, + }; + interface DuplicateInfoForSymbol { + readonly firstFileLocations: Node[]; + readonly secondFileLocations: Node[]; + readonly isBlockScoped: boolean; } - - const enum TypeFacts { - None = 0, - TypeofEQString = 1 << 0, // typeof x === "string" - TypeofEQNumber = 1 << 1, // typeof x === "number" - TypeofEQBigInt = 1 << 2, // typeof x === "bigint" - TypeofEQBoolean = 1 << 3, // typeof x === "boolean" - TypeofEQSymbol = 1 << 4, // typeof x === "symbol" - TypeofEQObject = 1 << 5, // typeof x === "object" - TypeofEQFunction = 1 << 6, // typeof x === "function" - TypeofEQHostObject = 1 << 7, // typeof x === "xxx" - TypeofNEString = 1 << 8, // typeof x !== "string" - TypeofNENumber = 1 << 9, // typeof x !== "number" - TypeofNEBigInt = 1 << 10, // typeof x !== "bigint" - TypeofNEBoolean = 1 << 11, // typeof x !== "boolean" - TypeofNESymbol = 1 << 12, // typeof x !== "symbol" - TypeofNEObject = 1 << 13, // typeof x !== "object" - TypeofNEFunction = 1 << 14, // typeof x !== "function" - TypeofNEHostObject = 1 << 15, // typeof x !== "xxx" - EQUndefined = 1 << 16, // x === undefined - EQNull = 1 << 17, // x === null - EQUndefinedOrNull = 1 << 18, // x === undefined / x === null - NEUndefined = 1 << 19, // x !== undefined - NENull = 1 << 20, // x !== null - NEUndefinedOrNull = 1 << 21, // x != undefined / x != null - Truthy = 1 << 22, // x - Falsy = 1 << 23, // !x - All = (1 << 24) - 1, - // The following members encode facts about particular kinds of types for use in the getTypeFacts function. - // The presence of a particular fact means that the given test is true for some (and possibly all) values - // of that kind of type. - BaseStringStrictFacts = TypeofEQString | TypeofNENumber | TypeofNEBigInt | TypeofNEBoolean | TypeofNESymbol | TypeofNEObject | TypeofNEFunction | TypeofNEHostObject | NEUndefined | NENull | NEUndefinedOrNull, - BaseStringFacts = BaseStringStrictFacts | EQUndefined | EQNull | EQUndefinedOrNull | Falsy, - StringStrictFacts = BaseStringStrictFacts | Truthy | Falsy, - StringFacts = BaseStringFacts | Truthy, - EmptyStringStrictFacts = BaseStringStrictFacts | Falsy, - EmptyStringFacts = BaseStringFacts, - NonEmptyStringStrictFacts = BaseStringStrictFacts | Truthy, - NonEmptyStringFacts = BaseStringFacts | Truthy, - BaseNumberStrictFacts = TypeofEQNumber | TypeofNEString | TypeofNEBigInt | TypeofNEBoolean | TypeofNESymbol | TypeofNEObject | TypeofNEFunction | TypeofNEHostObject | NEUndefined | NENull | NEUndefinedOrNull, - BaseNumberFacts = BaseNumberStrictFacts | EQUndefined | EQNull | EQUndefinedOrNull | Falsy, - NumberStrictFacts = BaseNumberStrictFacts | Truthy | Falsy, - NumberFacts = BaseNumberFacts | Truthy, - ZeroNumberStrictFacts = BaseNumberStrictFacts | Falsy, - ZeroNumberFacts = BaseNumberFacts, - NonZeroNumberStrictFacts = BaseNumberStrictFacts | Truthy, - NonZeroNumberFacts = BaseNumberFacts | Truthy, - BaseBigIntStrictFacts = TypeofEQBigInt | TypeofNEString | TypeofNENumber | TypeofNEBoolean | TypeofNESymbol | TypeofNEObject | TypeofNEFunction | TypeofNEHostObject | NEUndefined | NENull | NEUndefinedOrNull, - BaseBigIntFacts = BaseBigIntStrictFacts | EQUndefined | EQNull | EQUndefinedOrNull | Falsy, - BigIntStrictFacts = BaseBigIntStrictFacts | Truthy | Falsy, - BigIntFacts = BaseBigIntFacts | Truthy, - ZeroBigIntStrictFacts = BaseBigIntStrictFacts | Falsy, - ZeroBigIntFacts = BaseBigIntFacts, - NonZeroBigIntStrictFacts = BaseBigIntStrictFacts | Truthy, - NonZeroBigIntFacts = BaseBigIntFacts | Truthy, - BaseBooleanStrictFacts = TypeofEQBoolean | TypeofNEString | TypeofNENumber | TypeofNEBigInt | TypeofNESymbol | TypeofNEObject | TypeofNEFunction | TypeofNEHostObject | NEUndefined | NENull | NEUndefinedOrNull, - BaseBooleanFacts = BaseBooleanStrictFacts | EQUndefined | EQNull | EQUndefinedOrNull | Falsy, - BooleanStrictFacts = BaseBooleanStrictFacts | Truthy | Falsy, - BooleanFacts = BaseBooleanFacts | Truthy, - FalseStrictFacts = BaseBooleanStrictFacts | Falsy, - FalseFacts = BaseBooleanFacts, - TrueStrictFacts = BaseBooleanStrictFacts | Truthy, - TrueFacts = BaseBooleanFacts | Truthy, - SymbolStrictFacts = TypeofEQSymbol | TypeofNEString | TypeofNENumber | TypeofNEBigInt | TypeofNEBoolean | TypeofNEObject | TypeofNEFunction | TypeofNEHostObject | NEUndefined | NENull | NEUndefinedOrNull | Truthy, - SymbolFacts = SymbolStrictFacts | EQUndefined | EQNull | EQUndefinedOrNull | Falsy, - ObjectStrictFacts = TypeofEQObject | TypeofEQHostObject | TypeofNEString | TypeofNENumber | TypeofNEBigInt | TypeofNEBoolean | TypeofNESymbol | TypeofNEFunction | NEUndefined | NENull | NEUndefinedOrNull | Truthy, - ObjectFacts = ObjectStrictFacts | EQUndefined | EQNull | EQUndefinedOrNull | Falsy, - FunctionStrictFacts = TypeofEQFunction | TypeofEQHostObject | TypeofNEString | TypeofNENumber | TypeofNEBigInt | TypeofNEBoolean | TypeofNESymbol | TypeofNEObject | NEUndefined | NENull | NEUndefinedOrNull | Truthy, - FunctionFacts = FunctionStrictFacts | EQUndefined | EQNull | EQUndefinedOrNull | Falsy, - UndefinedFacts = TypeofNEString | TypeofNENumber | TypeofNEBigInt | TypeofNEBoolean | TypeofNESymbol | TypeofNEObject | TypeofNEFunction | TypeofNEHostObject | EQUndefined | EQUndefinedOrNull | NENull | Falsy, - NullFacts = TypeofEQObject | TypeofNEString | TypeofNENumber | TypeofNEBigInt | TypeofNEBoolean | TypeofNESymbol | TypeofNEFunction | TypeofNEHostObject | EQNull | EQUndefinedOrNull | NEUndefined | Falsy, - EmptyObjectStrictFacts = All & ~(EQUndefined | EQNull | EQUndefinedOrNull), - EmptyObjectFacts = All, + interface DuplicateInfoForFiles { + readonly firstFile: SourceFile; + readonly secondFile: SourceFile; + /** Key is symbol name. */ + readonly conflictingSymbols: ts.Map; } - - const typeofEQFacts: ReadonlyMap = createMapFromTemplate({ - string: TypeFacts.TypeofEQString, - number: TypeFacts.TypeofEQNumber, - bigint: TypeFacts.TypeofEQBigInt, - boolean: TypeFacts.TypeofEQBoolean, - symbol: TypeFacts.TypeofEQSymbol, - undefined: TypeFacts.EQUndefined, - object: TypeFacts.TypeofEQObject, - function: TypeFacts.TypeofEQFunction - }); - - const typeofNEFacts: ReadonlyMap = createMapFromTemplate({ - string: TypeFacts.TypeofNEString, - number: TypeFacts.TypeofNENumber, - bigint: TypeFacts.TypeofNEBigInt, - boolean: TypeFacts.TypeofNEBoolean, - symbol: TypeFacts.TypeofNESymbol, - undefined: TypeFacts.NEUndefined, - object: TypeFacts.TypeofNEObject, - function: TypeFacts.TypeofNEFunction + /** Key is "/path/to/a.ts|/path/to/b.ts". */ + let amalgamatedDuplicates: ts.Map | undefined; + const reverseMappedCache = createMap(); + let ambientModulesCache: ts.Symbol[] | undefined; + /** + * List of every ambient module with a "*" wildcard. + * Unlike other ambient modules, these can't be stored in `globals` because symbol tables only deal with exact matches. + * This is only used if there is no exact match. + */ + let patternAmbientModules: PatternAmbientModule[]; + let patternAmbientModuleAugmentations: ts.Map | undefined; + let globalObjectType: ObjectType; + let globalFunctionType: ObjectType; + let globalCallableFunctionType: ObjectType; + let globalNewableFunctionType: ObjectType; + let globalArrayType: GenericType; + let globalReadonlyArrayType: GenericType; + let globalStringType: ObjectType; + let globalNumberType: ObjectType; + let globalBooleanType: ObjectType; + let globalRegExpType: ObjectType; + let globalThisType: GenericType; + let anyArrayType: ts.Type; + let autoArrayType: ts.Type; + let anyReadonlyArrayType: ts.Type; + let deferredGlobalNonNullableTypeAlias: ts.Symbol; + // The library files are only loaded when the feature is used. + // This allows users to just specify library files they want to used through --lib + // and they will not get an error from not having unrelated library files + let deferredGlobalESSymbolConstructorSymbol: ts.Symbol | undefined; + let deferredGlobalESSymbolType: ObjectType; + let deferredGlobalTypedPropertyDescriptorType: GenericType; + let deferredGlobalPromiseType: GenericType; + let deferredGlobalPromiseLikeType: GenericType; + let deferredGlobalPromiseConstructorSymbol: ts.Symbol | undefined; + let deferredGlobalPromiseConstructorLikeType: ObjectType; + let deferredGlobalIterableType: GenericType; + let deferredGlobalIteratorType: GenericType; + let deferredGlobalIterableIteratorType: GenericType; + let deferredGlobalGeneratorType: GenericType; + let deferredGlobalIteratorYieldResultType: GenericType; + let deferredGlobalIteratorReturnResultType: GenericType; + let deferredGlobalAsyncIterableType: GenericType; + let deferredGlobalAsyncIteratorType: GenericType; + let deferredGlobalAsyncIterableIteratorType: GenericType; + let deferredGlobalAsyncGeneratorType: GenericType; + let deferredGlobalTemplateStringsArrayType: ObjectType; + let deferredGlobalImportMetaType: ObjectType; + let deferredGlobalExtractSymbol: ts.Symbol; + let deferredGlobalOmitSymbol: ts.Symbol; + let deferredGlobalBigIntType: ObjectType; + const allPotentiallyUnusedIdentifiers = createMap(); // key is file name + let flowLoopStart = 0; + let flowLoopCount = 0; + let sharedFlowCount = 0; + let flowAnalysisDisabled = false; + let flowInvocationCount = 0; + let lastFlowNode: FlowNode | undefined; + let lastFlowNodeReachable: boolean; + let flowTypeCache: ts.Type[] | undefined; + const emptyStringType = getLiteralType(""); + const zeroType = getLiteralType(0); + const zeroBigIntType = getLiteralType({ negative: false, base10Value: "0" }); + const resolutionTargets: TypeSystemEntity[] = []; + const resolutionResults: boolean[] = []; + const resolutionPropertyNames: TypeSystemPropertyName[] = []; + let suggestionCount = 0; + const maximumSuggestionCount = 10; + const mergedSymbols: ts.Symbol[] = []; + const symbolLinks: SymbolLinks[] = []; + const nodeLinks: NodeLinks[] = []; + const flowLoopCaches: ts.Map[] = []; + const flowLoopNodes: FlowNode[] = []; + const flowLoopKeys: string[] = []; + const flowLoopTypes: ts.Type[][] = []; + const sharedFlowNodes: FlowNode[] = []; + const sharedFlowTypes: FlowType[] = []; + const flowNodeReachable: (boolean | undefined)[] = []; + const potentialThisCollisions: Node[] = []; + const potentialNewTargetCollisions: Node[] = []; + const potentialWeakMapCollisions: Node[] = []; + const awaitedTypeStack: number[] = []; + const diagnostics = createDiagnosticCollection(); + const suggestionDiagnostics = createDiagnosticCollection(); + const typeofTypesByName: ts.ReadonlyMap = createMapFromTemplate({ + string: stringType, + number: numberType, + bigint: bigintType, + boolean: booleanType, + symbol: esSymbolType, + undefined: undefinedType }); - - type TypeSystemEntity = Node | Symbol | Type | Signature; - - const enum TypeSystemPropertyName { - Type, - ResolvedBaseConstructorType, - DeclaredType, - ResolvedReturnType, - ImmediateBaseConstraint, - EnumTagType, - ResolvedTypeArguments, + const typeofType = createTypeofType(); + let _jsxNamespace: __String; + let _jsxFactoryEntity: EntityName | undefined; + let outofbandVarianceMarkerHandler: ((onlyUnreliable: boolean) => void) | undefined; + const subtypeRelation = createMap(); + const strictSubtypeRelation = createMap(); + const assignableRelation = createMap(); + const comparableRelation = createMap(); + const identityRelation = createMap(); + const enumRelation = createMap(); + const builtinGlobals = createSymbolTable(); + builtinGlobals.set(undefinedSymbol.escapedName, undefinedSymbol); + initializeTypeChecker(); + return checker; + function getJsxNamespace(location: Node | undefined): __String { + if (location) { + const file = getSourceFileOfNode(location); + if (file) { + if (file.localJsxNamespace) { + return file.localJsxNamespace; + } + const jsxPragma = file.pragmas.get("jsx"); + if (jsxPragma) { + const chosenpragma = isArray(jsxPragma) ? jsxPragma[0] : jsxPragma; + file.localJsxFactory = parseIsolatedEntityName(chosenpragma.arguments.factory, languageVersion); + visitNode(file.localJsxFactory, markAsSynthetic); + if (file.localJsxFactory) { + return file.localJsxNamespace = getFirstIdentifier(file.localJsxFactory).escapedText; + } + } + } + } + if (!_jsxNamespace) { + _jsxNamespace = ("React" as __String); + if (compilerOptions.jsxFactory) { + _jsxFactoryEntity = parseIsolatedEntityName(compilerOptions.jsxFactory, languageVersion); + visitNode(_jsxFactoryEntity, markAsSynthetic); + if (_jsxFactoryEntity) { + _jsxNamespace = getFirstIdentifier(_jsxFactoryEntity).escapedText; + } + } + else if (compilerOptions.reactNamespace) { + _jsxNamespace = escapeLeadingUnderscores(compilerOptions.reactNamespace); + } + } + if (!_jsxFactoryEntity) { + _jsxFactoryEntity = createQualifiedName(createIdentifier(unescapeLeadingUnderscores(_jsxNamespace)), "createElement"); + } + return _jsxNamespace; + function markAsSynthetic(node: Node): VisitResult { + node.pos = -1; + node.end = -1; + return visitEachChild(node, markAsSynthetic, nullTransformationContext); + } } - - const enum CheckMode { - Normal = 0, // Normal type checking - Contextual = 1 << 0, // Explicitly assigned contextual type, therefore not cacheable - Inferential = 1 << 1, // Inferential typing - SkipContextSensitive = 1 << 2, // Skip context sensitive function expressions - SkipGenericFunctions = 1 << 3, // Skip single signature generic functions - IsForSignatureHelp = 1 << 4, // Call resolution for purposes of signature help + function getEmitResolver(sourceFile: SourceFile, cancellationToken: CancellationToken) { + // Ensure we have all the type information in place for this file so that all the + // emitter questions of this resolver will return the right information. + getDiagnostics(sourceFile, cancellationToken); + return emitResolver; } - - const enum AccessFlags { - None = 0, - NoIndexSignatures = 1 << 0, - Writing = 1 << 1, - CacheSymbol = 1 << 2, - NoTupleBoundsCheck = 1 << 3, + function lookupOrIssueError(location: Node | undefined, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): Diagnostic { + const diagnostic = location + ? createDiagnosticForNode(location, message, arg0, arg1, arg2, arg3) + : createCompilerDiagnostic(message, arg0, arg1, arg2, arg3); + const existing = diagnostics.lookup(diagnostic); + if (existing) { + return existing; + } + else { + diagnostics.add(diagnostic); + return diagnostic; + } } - - const enum SignatureCheckMode { - BivariantCallback = 1 << 0, - StrictCallback = 1 << 1, - IgnoreReturnTypes = 1 << 2, - StrictArity = 1 << 3, - Callback = BivariantCallback | StrictCallback, + function error(location: Node | undefined, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): Diagnostic { + const diagnostic = location + ? createDiagnosticForNode(location, message, arg0, arg1, arg2, arg3) + : createCompilerDiagnostic(message, arg0, arg1, arg2, arg3); + diagnostics.add(diagnostic); + return diagnostic; } - - const enum IntersectionState { - None = 0, - Source = 1 << 0, - Target = 1 << 1, + function addErrorOrSuggestion(isError: boolean, diagnostic: DiagnosticWithLocation) { + if (isError) { + diagnostics.add(diagnostic); + } + else { + suggestionDiagnostics.add({ ...diagnostic, category: DiagnosticCategory.Suggestion }); + } } - - const enum MappedTypeModifiers { - IncludeReadonly = 1 << 0, - ExcludeReadonly = 1 << 1, - IncludeOptional = 1 << 2, - ExcludeOptional = 1 << 3, + function errorOrSuggestion(isError: boolean, location: Node, message: DiagnosticMessage | DiagnosticMessageChain, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): void { + addErrorOrSuggestion(isError, "message" in message ? createDiagnosticForNode(location, message, arg0, arg1, arg2, arg3) : createDiagnosticForNodeFromMessageChain(location, message)); // eslint-disable-line no-in-operator } - - const enum ExpandingFlags { - None = 0, - Source = 1, - Target = 1 << 1, - Both = Source | Target, + function errorAndMaybeSuggestAwait(location: Node, maybeMissingAwait: boolean, message: DiagnosticMessage, arg0?: string | number | undefined, arg1?: string | number | undefined, arg2?: string | number | undefined, arg3?: string | number | undefined): Diagnostic { + const diagnostic = error(location, message, arg0, arg1, arg2, arg3); + if (maybeMissingAwait) { + const related = createDiagnosticForNode(location, Diagnostics.Did_you_forget_to_use_await); + addRelatedInfo(diagnostic, related); + } + return diagnostic; } - - const enum MembersOrExportsResolutionKind { - resolvedExports = "resolvedExports", - resolvedMembers = "resolvedMembers" + function createSymbol(flags: SymbolFlags, name: __String, checkFlags?: CheckFlags) { + symbolCount++; + const symbol = ((new Symbol(flags | SymbolFlags.Transient, name))); + symbol.checkFlags = checkFlags || 0; + return symbol; } - - const enum UnusedKind { - Local, - Parameter, + function getExcludedSymbolFlags(flags: SymbolFlags): SymbolFlags { + let result: SymbolFlags = 0; + if (flags & SymbolFlags.BlockScopedVariable) + result |= SymbolFlags.BlockScopedVariableExcludes; + if (flags & SymbolFlags.FunctionScopedVariable) + result |= SymbolFlags.FunctionScopedVariableExcludes; + if (flags & SymbolFlags.Property) + result |= SymbolFlags.PropertyExcludes; + if (flags & SymbolFlags.EnumMember) + result |= SymbolFlags.EnumMemberExcludes; + if (flags & SymbolFlags.Function) + result |= SymbolFlags.FunctionExcludes; + if (flags & SymbolFlags.Class) + result |= SymbolFlags.ClassExcludes; + if (flags & SymbolFlags.Interface) + result |= SymbolFlags.InterfaceExcludes; + if (flags & SymbolFlags.RegularEnum) + result |= SymbolFlags.RegularEnumExcludes; + if (flags & SymbolFlags.ConstEnum) + result |= SymbolFlags.ConstEnumExcludes; + if (flags & SymbolFlags.ValueModule) + result |= SymbolFlags.ValueModuleExcludes; + if (flags & SymbolFlags.Method) + result |= SymbolFlags.MethodExcludes; + if (flags & SymbolFlags.GetAccessor) + result |= SymbolFlags.GetAccessorExcludes; + if (flags & SymbolFlags.SetAccessor) + result |= SymbolFlags.SetAccessorExcludes; + if (flags & SymbolFlags.TypeParameter) + result |= SymbolFlags.TypeParameterExcludes; + if (flags & SymbolFlags.TypeAlias) + result |= SymbolFlags.TypeAliasExcludes; + if (flags & SymbolFlags.Alias) + result |= SymbolFlags.AliasExcludes; + return result; } - - /** @param containingNode Node to check for parse error */ - type AddUnusedDiagnostic = (containingNode: Node, type: UnusedKind, diagnostic: DiagnosticWithLocation) => void; - - const isNotOverloadAndNotAccessor = and(isNotOverload, isNotAccessor); - - const enum DeclarationMeaning { - GetAccessor = 1, - SetAccessor = 2, - PropertyAssignment = 4, - Method = 8, - GetOrSetAccessor = GetAccessor | SetAccessor, - PropertyAssignmentOrMethod = PropertyAssignment | Method, + function recordMergedSymbol(target: ts.Symbol, source: ts.Symbol) { + if (!source.mergeId) { + source.mergeId = nextMergeId; + nextMergeId++; + } + mergedSymbols[source.mergeId] = target; } - - const enum DeclarationSpaces { - None = 0, - ExportValue = 1 << 0, - ExportType = 1 << 1, - ExportNamespace = 1 << 2, + function cloneSymbol(symbol: ts.Symbol): ts.Symbol { + const result = createSymbol(symbol.flags, symbol.escapedName); + result.declarations = symbol.declarations ? symbol.declarations.slice() : []; + result.parent = symbol.parent; + if (symbol.valueDeclaration) + result.valueDeclaration = symbol.valueDeclaration; + if (symbol.constEnumOnlyModule) + result.constEnumOnlyModule = true; + if (symbol.members) + result.members = cloneMap(symbol.members); + if (symbol.exports) + result.exports = cloneMap(symbol.exports); + recordMergedSymbol(result, symbol); + return result; } - - export function getNodeId(node: Node): number { - if (!node.id) { - node.id = nextNodeId; - nextNodeId++; + /** + * Note: if target is transient, then it is mutable, and mergeSymbol with both mutate and return it. + * If target is not transient, mergeSymbol will produce a transient clone, mutate that and return it. + */ + function mergeSymbol(target: ts.Symbol, source: ts.Symbol, unidirectional = false): ts.Symbol { + if (!(target.flags & getExcludedSymbolFlags(source.flags)) || + (source.flags | target.flags) & SymbolFlags.Assignment) { + if (source === target) { + // This can happen when an export assigned namespace exports something also erroneously exported at the top level + // See `declarationFileNoCrashOnExtraExportModifier` for an example + return target; + } + if (!(target.flags & SymbolFlags.Transient)) { + const resolvedTarget = resolveSymbol(target); + if (resolvedTarget === unknownSymbol) { + return source; + } + target = cloneSymbol(resolvedTarget); + } + // Javascript static-property-assignment declarations always merge, even though they are also values + if (source.flags & SymbolFlags.ValueModule && target.flags & SymbolFlags.ValueModule && target.constEnumOnlyModule && !source.constEnumOnlyModule) { + // reset flag when merging instantiated module into value module that has only const enums + target.constEnumOnlyModule = false; + } + target.flags |= source.flags; + if (source.valueDeclaration && + (!target.valueDeclaration || + isAssignmentDeclaration(target.valueDeclaration) && !isAssignmentDeclaration(source.valueDeclaration) || + isEffectiveModuleDeclaration(target.valueDeclaration) && !isEffectiveModuleDeclaration(source.valueDeclaration))) { + // other kinds of value declarations take precedence over modules and assignment declarations + target.valueDeclaration = source.valueDeclaration; + } + addRange(target.declarations, source.declarations); + if (source.members) { + if (!target.members) + target.members = createSymbolTable(); + mergeSymbolTable(target.members, source.members, unidirectional); + } + if (source.exports) { + if (!target.exports) + target.exports = createSymbolTable(); + mergeSymbolTable(target.exports, source.exports, unidirectional); + } + if (!unidirectional) { + recordMergedSymbol(target, source); + } + } + else if (target.flags & SymbolFlags.NamespaceModule) { + // Do not report an error when merging `var globalThis` with the built-in `globalThis`, + // as we will already report a "Declaration name conflicts..." error, and this error + // won't make much sense. + if (target !== globalThisSymbol) { + error(getNameOfDeclaration(source.declarations[0]), Diagnostics.Cannot_augment_module_0_with_value_exports_because_it_resolves_to_a_non_module_entity, symbolToString(target)); + } + } + else { // error + const isEitherEnum = !!(target.flags & SymbolFlags.Enum || source.flags & SymbolFlags.Enum); + const isEitherBlockScoped = !!(target.flags & SymbolFlags.BlockScopedVariable || source.flags & SymbolFlags.BlockScopedVariable); + const message = isEitherEnum + ? Diagnostics.Enum_declarations_can_only_merge_with_namespace_or_other_enum_declarations + : isEitherBlockScoped + ? Diagnostics.Cannot_redeclare_block_scoped_variable_0 + : Diagnostics.Duplicate_identifier_0; + const sourceSymbolFile = source.declarations && getSourceFileOfNode(source.declarations[0]); + const targetSymbolFile = target.declarations && getSourceFileOfNode(target.declarations[0]); + const symbolName = symbolToString(source); + // Collect top-level duplicate identifier errors into one mapping, so we can then merge their diagnostics if there are a bunch + if (sourceSymbolFile && targetSymbolFile && amalgamatedDuplicates && !isEitherEnum && sourceSymbolFile !== targetSymbolFile) { + const firstFile = comparePaths(sourceSymbolFile.path, targetSymbolFile.path) === Comparison.LessThan ? sourceSymbolFile : targetSymbolFile; + const secondFile = firstFile === sourceSymbolFile ? targetSymbolFile : sourceSymbolFile; + const filesDuplicates = getOrUpdate(amalgamatedDuplicates, `${firstFile.path}|${secondFile.path}`, () => ({ firstFile, secondFile, conflictingSymbols: createMap() })); + const conflictingSymbolInfo = getOrUpdate(filesDuplicates.conflictingSymbols, symbolName, () => ({ isBlockScoped: isEitherBlockScoped, firstFileLocations: [], secondFileLocations: [] })); + addDuplicateLocations(conflictingSymbolInfo.firstFileLocations, source); + addDuplicateLocations(conflictingSymbolInfo.secondFileLocations, target); + } + else { + addDuplicateDeclarationErrorsForSymbols(source, message, symbolName, target); + addDuplicateDeclarationErrorsForSymbols(target, message, symbolName, source); + } + } + return target; + function addDuplicateLocations(locs: Node[], symbol: ts.Symbol): void { + for (const decl of symbol.declarations) { + pushIfUnique(locs, (getExpandoInitializer(decl, /*isPrototypeAssignment*/ false) ? getNameOfExpando(decl) : getNameOfDeclaration(decl)) || decl); + } } - return node.id; } - - export function getSymbolId(symbol: Symbol): number { - if (!symbol.id) { - symbol.id = nextSymbolId; - nextSymbolId++; + function addDuplicateDeclarationErrorsForSymbols(target: ts.Symbol, message: DiagnosticMessage, symbolName: string, source: ts.Symbol) { + forEach(target.declarations, node => { + const errorNode = (getExpandoInitializer(node, /*isPrototypeAssignment*/ false) ? getNameOfExpando(node) : getNameOfDeclaration(node)) || node; + addDuplicateDeclarationError(errorNode, message, symbolName, source.declarations); + }); + } + function addDuplicateDeclarationError(errorNode: Node, message: DiagnosticMessage, symbolName: string, relatedNodes: readonly Node[] | undefined) { + const err = lookupOrIssueError(errorNode, message, symbolName); + for (const relatedNode of relatedNodes || emptyArray) { + err.relatedInformation = err.relatedInformation || []; + if (length(err.relatedInformation) >= 5) + continue; + addRelatedInfo(err, !length(err.relatedInformation) ? createDiagnosticForNode(relatedNode, Diagnostics._0_was_also_declared_here, symbolName) : createDiagnosticForNode(relatedNode, Diagnostics.and_here)); } - - return symbol.id; } - - export function isInstantiatedModule(node: ModuleDeclaration, preserveConstEnums: boolean) { - const moduleState = getModuleInstanceState(node); - return moduleState === ModuleInstanceState.Instantiated || - (preserveConstEnums && moduleState === ModuleInstanceState.ConstEnumOnly); + function combineSymbolTables(first: SymbolTable | undefined, second: SymbolTable | undefined): SymbolTable | undefined { + if (!hasEntries(first)) + return second; + if (!hasEntries(second)) + return first; + const combined = createSymbolTable(); + mergeSymbolTable(combined, first); + mergeSymbolTable(combined, second); + return combined; } - - export function createTypeChecker(host: TypeCheckerHost, produceDiagnostics: boolean): TypeChecker { - const getPackagesSet: () => Map = memoize(() => { - const set = createMap(); - host.getSourceFiles().forEach(sf => { - if (!sf.resolvedModules) return; - - forEachEntry(sf.resolvedModules, r => { - if (r && r.packageId) set.set(r.packageId.name, true); - }); - }); - return set; + function mergeSymbolTable(target: SymbolTable, source: SymbolTable, unidirectional = false) { + source.forEach((sourceSymbol, id) => { + const targetSymbol = target.get(id); + target.set(id, targetSymbol ? mergeSymbol(targetSymbol, sourceSymbol, unidirectional) : sourceSymbol); }); - - // Cancellation that controls whether or not we can cancel in the middle of type checking. - // In general cancelling is *not* safe for the type checker. We might be in the middle of - // computing something, and we will leave our internals in an inconsistent state. Callers - // who set the cancellation token should catch if a cancellation exception occurs, and - // should throw away and create a new TypeChecker. - // - // Currently we only support setting the cancellation token when getting diagnostics. This - // is because diagnostics can be quite expensive, and we want to allow hosts to bail out if - // they no longer need the information (for example, if the user started editing again). - let cancellationToken: CancellationToken | undefined; - let requestedExternalEmitHelpers: ExternalEmitHelpers; - let externalHelpersModule: Symbol; - - const Symbol = objectAllocator.getSymbolConstructor(); - const Type = objectAllocator.getTypeConstructor(); - const Signature = objectAllocator.getSignatureConstructor(); - - let typeCount = 0; - let symbolCount = 0; - let enumCount = 0; - let totalInstantiationCount = 0; - let instantiationCount = 0; - let instantiationDepth = 0; - let constraintDepth = 0; - let currentNode: Node | undefined; - - const emptySymbols = createSymbolTable(); - const identityMapper: (type: Type) => Type = identity; - const arrayVariances = [VarianceFlags.Covariant]; - - const compilerOptions = host.getCompilerOptions(); - const languageVersion = getEmitScriptTarget(compilerOptions); - const moduleKind = getEmitModuleKind(compilerOptions); - const allowSyntheticDefaultImports = getAllowSyntheticDefaultImports(compilerOptions); - const strictNullChecks = getStrictOptionValue(compilerOptions, "strictNullChecks"); - const strictFunctionTypes = getStrictOptionValue(compilerOptions, "strictFunctionTypes"); - const strictBindCallApply = getStrictOptionValue(compilerOptions, "strictBindCallApply"); - const strictPropertyInitialization = getStrictOptionValue(compilerOptions, "strictPropertyInitialization"); - const noImplicitAny = getStrictOptionValue(compilerOptions, "noImplicitAny"); - const noImplicitThis = getStrictOptionValue(compilerOptions, "noImplicitThis"); - const keyofStringsOnly = !!compilerOptions.keyofStringsOnly; - const freshObjectLiteralFlag = compilerOptions.suppressExcessPropertyErrors ? 0 : ObjectFlags.FreshLiteral; - - const emitResolver = createResolver(); - const nodeBuilder = createNodeBuilder(); - - const globals = createSymbolTable(); - const undefinedSymbol = createSymbol(SymbolFlags.Property, "undefined" as __String); - undefinedSymbol.declarations = []; - - const globalThisSymbol = createSymbol(SymbolFlags.Module, "globalThis" as __String, CheckFlags.Readonly); - globalThisSymbol.exports = globals; - globalThisSymbol.declarations = []; - globals.set(globalThisSymbol.escapedName, globalThisSymbol); - - const argumentsSymbol = createSymbol(SymbolFlags.Property, "arguments" as __String); - const requireSymbol = createSymbol(SymbolFlags.Property, "require" as __String); - - /** This will be set during calls to `getResolvedSignature` where services determines an apparent number of arguments greater than what is actually provided. */ - let apparentArgumentCount: number | undefined; - - // for public members that accept a Node or one of its subtypes, we must guard against - // synthetic nodes created during transformations by calling `getParseTreeNode`. - // for most of these, we perform the guard only on `checker` to avoid any possible - // extra cost of calling `getParseTreeNode` when calling these functions from inside the - // checker. - const checker: TypeChecker = { - getNodeCount: () => sum(host.getSourceFiles(), "nodeCount"), - getIdentifierCount: () => sum(host.getSourceFiles(), "identifierCount"), - getSymbolCount: () => sum(host.getSourceFiles(), "symbolCount") + symbolCount, - getTypeCount: () => typeCount, - getInstantiationCount: () => totalInstantiationCount, - getRelationCacheSizes: () => ({ - assignable: assignableRelation.size, - identity: identityRelation.size, - subtype: subtypeRelation.size, - strictSubtype: strictSubtypeRelation.size, - }), - isUndefinedSymbol: symbol => symbol === undefinedSymbol, - isArgumentsSymbol: symbol => symbol === argumentsSymbol, - isUnknownSymbol: symbol => symbol === unknownSymbol, - getMergedSymbol, - getDiagnostics, - getGlobalDiagnostics, - getTypeOfSymbolAtLocation: (symbol, location) => { - location = getParseTreeNode(location); - return location ? getTypeOfSymbolAtLocation(symbol, location) : errorType; - }, - getSymbolsOfParameterPropertyDeclaration: (parameterIn, parameterName) => { - const parameter = getParseTreeNode(parameterIn, isParameter); - if (parameter === undefined) return Debug.fail("Cannot get symbols of a synthetic parameter that cannot be resolved to a parse-tree node."); - return getSymbolsOfParameterPropertyDeclaration(parameter, escapeLeadingUnderscores(parameterName)); - }, - getDeclaredTypeOfSymbol, - getPropertiesOfType, - getPropertyOfType: (type, name) => getPropertyOfType(type, escapeLeadingUnderscores(name)), - getPrivateIdentifierPropertyOfType: (leftType: Type, name: string, location: Node) => { - const node = getParseTreeNode(location); - if (!node) { - return undefined; - } - const propName = escapeLeadingUnderscores(name); - const lexicallyScopedIdentifier = lookupSymbolForPrivateIdentifierDeclaration(propName, node); - return lexicallyScopedIdentifier ? getPrivateIdentifierPropertyOfType(leftType, lexicallyScopedIdentifier) : undefined; - }, - getTypeOfPropertyOfType: (type, name) => getTypeOfPropertyOfType(type, escapeLeadingUnderscores(name)), - getIndexInfoOfType, - getSignaturesOfType, - getIndexTypeOfType, - getBaseTypes, - getBaseTypeOfLiteralType, - getWidenedType, - getTypeFromTypeNode: nodeIn => { - const node = getParseTreeNode(nodeIn, isTypeNode); - return node ? getTypeFromTypeNode(node) : errorType; - }, - getParameterType: getTypeAtPosition, - getPromisedTypeOfPromise, - getReturnTypeOfSignature, - isNullableType, - getNullableType, - getNonNullableType, - getNonOptionalType: removeOptionalTypeMarker, - getTypeArguments, - typeToTypeNode: nodeBuilder.typeToTypeNode, - indexInfoToIndexSignatureDeclaration: nodeBuilder.indexInfoToIndexSignatureDeclaration, - signatureToSignatureDeclaration: nodeBuilder.signatureToSignatureDeclaration, - symbolToEntityName: nodeBuilder.symbolToEntityName, - symbolToExpression: nodeBuilder.symbolToExpression, - symbolToTypeParameterDeclarations: nodeBuilder.symbolToTypeParameterDeclarations, - symbolToParameterDeclaration: nodeBuilder.symbolToParameterDeclaration, - typeParameterToDeclaration: nodeBuilder.typeParameterToDeclaration, - getSymbolsInScope: (location, meaning) => { - location = getParseTreeNode(location); - return location ? getSymbolsInScope(location, meaning) : []; - }, - getSymbolAtLocation: node => { - node = getParseTreeNode(node); - // set ignoreErrors: true because any lookups invoked by the API shouldn't cause any new errors - return node ? getSymbolAtLocation(node, /*ignoreErrors*/ true) : undefined; - }, - getShorthandAssignmentValueSymbol: node => { - node = getParseTreeNode(node); - return node ? getShorthandAssignmentValueSymbol(node) : undefined; - }, - getExportSpecifierLocalTargetSymbol: nodeIn => { - const node = getParseTreeNode(nodeIn, isExportSpecifier); - return node ? getExportSpecifierLocalTargetSymbol(node) : undefined; - }, - getExportSymbolOfSymbol(symbol) { - return getMergedSymbol(symbol.exportSymbol || symbol); - }, - getTypeAtLocation: node => { - node = getParseTreeNode(node); - return node ? getTypeOfNode(node) : errorType; - }, - getTypeOfAssignmentPattern: nodeIn => { - const node = getParseTreeNode(nodeIn, isAssignmentPattern); - return node && getTypeOfAssignmentPattern(node) || errorType; - }, - getPropertySymbolOfDestructuringAssignment: locationIn => { - const location = getParseTreeNode(locationIn, isIdentifier); - return location ? getPropertySymbolOfDestructuringAssignment(location) : undefined; - }, - signatureToString: (signature, enclosingDeclaration, flags, kind) => { - return signatureToString(signature, getParseTreeNode(enclosingDeclaration), flags, kind); - }, - typeToString: (type, enclosingDeclaration, flags) => { - return typeToString(type, getParseTreeNode(enclosingDeclaration), flags); - }, - symbolToString: (symbol, enclosingDeclaration, meaning, flags) => { - return symbolToString(symbol, getParseTreeNode(enclosingDeclaration), meaning, flags); - }, - typePredicateToString: (predicate, enclosingDeclaration, flags) => { - return typePredicateToString(predicate, getParseTreeNode(enclosingDeclaration), flags); - }, - writeSignature: (signature, enclosingDeclaration, flags, kind, writer) => { - return signatureToString(signature, getParseTreeNode(enclosingDeclaration), flags, kind, writer); - }, - writeType: (type, enclosingDeclaration, flags, writer) => { - return typeToString(type, getParseTreeNode(enclosingDeclaration), flags, writer); - }, - writeSymbol: (symbol, enclosingDeclaration, meaning, flags, writer) => { - return symbolToString(symbol, getParseTreeNode(enclosingDeclaration), meaning, flags, writer); - }, - writeTypePredicate: (predicate, enclosingDeclaration, flags, writer) => { - return typePredicateToString(predicate, getParseTreeNode(enclosingDeclaration), flags, writer); - }, - getAugmentedPropertiesOfType, - getRootSymbols, - getContextualType: (nodeIn: Expression, contextFlags?: ContextFlags) => { - const node = getParseTreeNode(nodeIn, isExpression); - if (!node) { - return undefined; - } - const containingCall = findAncestor(node, isCallLikeExpression); - const containingCallResolvedSignature = containingCall && getNodeLinks(containingCall).resolvedSignature; - if (contextFlags! & ContextFlags.Completions && containingCall) { - let toMarkSkip = node as Node; - do { - getNodeLinks(toMarkSkip).skipDirectInference = true; - toMarkSkip = toMarkSkip.parent; - } while (toMarkSkip && toMarkSkip !== containingCall); - getNodeLinks(containingCall).resolvedSignature = undefined; - } - const result = getContextualType(node, contextFlags); - if (contextFlags! & ContextFlags.Completions && containingCall) { - let toMarkSkip = node as Node; - do { - getNodeLinks(toMarkSkip).skipDirectInference = undefined; - toMarkSkip = toMarkSkip.parent; - } while (toMarkSkip && toMarkSkip !== containingCall); - getNodeLinks(containingCall).resolvedSignature = containingCallResolvedSignature; - } - return result; - }, - getContextualTypeForObjectLiteralElement: nodeIn => { - const node = getParseTreeNode(nodeIn, isObjectLiteralElementLike); - return node ? getContextualTypeForObjectLiteralElement(node) : undefined; - }, - getContextualTypeForArgumentAtIndex: (nodeIn, argIndex) => { - const node = getParseTreeNode(nodeIn, isCallLikeExpression); - return node && getContextualTypeForArgumentAtIndex(node, argIndex); - }, - getContextualTypeForJsxAttribute: (nodeIn) => { - const node = getParseTreeNode(nodeIn, isJsxAttributeLike); - return node && getContextualTypeForJsxAttribute(node); - }, - isContextSensitive, - getFullyQualifiedName, - getResolvedSignature: (node, candidatesOutArray, argumentCount) => - getResolvedSignatureWorker(node, candidatesOutArray, argumentCount, CheckMode.Normal), - getResolvedSignatureForSignatureHelp: (node, candidatesOutArray, argumentCount) => - getResolvedSignatureWorker(node, candidatesOutArray, argumentCount, CheckMode.IsForSignatureHelp), - getExpandedParameters, - hasEffectiveRestParameter, - getConstantValue: nodeIn => { - const node = getParseTreeNode(nodeIn, canHaveConstantValue); - return node ? getConstantValue(node) : undefined; - }, - isValidPropertyAccess: (nodeIn, propertyName) => { - const node = getParseTreeNode(nodeIn, isPropertyAccessOrQualifiedNameOrImportTypeNode); - return !!node && isValidPropertyAccess(node, escapeLeadingUnderscores(propertyName)); - }, - isValidPropertyAccessForCompletions: (nodeIn, type, property) => { - const node = getParseTreeNode(nodeIn, isPropertyAccessExpression); - return !!node && isValidPropertyAccessForCompletions(node, type, property); - }, - getSignatureFromDeclaration: declarationIn => { - const declaration = getParseTreeNode(declarationIn, isFunctionLike); - return declaration ? getSignatureFromDeclaration(declaration) : undefined; - }, - isImplementationOfOverload: node => { - const parsed = getParseTreeNode(node, isFunctionLike); - return parsed ? isImplementationOfOverload(parsed) : undefined; - }, - getImmediateAliasedSymbol, - getAliasedSymbol: resolveAlias, - getEmitResolver, - getExportsOfModule: getExportsOfModuleAsArray, - getExportsAndPropertiesOfModule, - getSymbolWalker: createGetSymbolWalker( - getRestTypeOfSignature, - getTypePredicateOfSignature, - getReturnTypeOfSignature, - getBaseTypes, - resolveStructuredTypeMembers, - getTypeOfSymbol, - getResolvedSymbol, - getIndexTypeOfStructuredType, - getConstraintOfTypeParameter, - getFirstIdentifier, - getTypeArguments, - ), - getAmbientModules, - getJsxIntrinsicTagNamesAt, - isOptionalParameter: nodeIn => { - const node = getParseTreeNode(nodeIn, isParameter); - return node ? isOptionalParameter(node) : false; - }, - tryGetMemberInModuleExports: (name, symbol) => tryGetMemberInModuleExports(escapeLeadingUnderscores(name), symbol), - tryGetMemberInModuleExportsAndProperties: (name, symbol) => tryGetMemberInModuleExportsAndProperties(escapeLeadingUnderscores(name), symbol), - tryFindAmbientModuleWithoutAugmentations: moduleName => { - // we deliberately exclude augmentations - // since we are only interested in declarations of the module itself - return tryFindAmbientModule(moduleName, /*withAugmentations*/ false); - }, - getApparentType, - getUnionType, - isTypeAssignableTo: (source, target) => { - return isTypeAssignableTo(source, target); - }, - createAnonymousType, - createSignature, - createSymbol, - createIndexInfo, - getAnyType: () => anyType, - getStringType: () => stringType, - getNumberType: () => numberType, - createPromiseType, - createArrayType, - getElementTypeOfArrayType, - getBooleanType: () => booleanType, - getFalseType: (fresh?) => fresh ? falseType : regularFalseType, - getTrueType: (fresh?) => fresh ? trueType : regularTrueType, - getVoidType: () => voidType, - getUndefinedType: () => undefinedType, - getNullType: () => nullType, - getESSymbolType: () => esSymbolType, - getNeverType: () => neverType, - getOptionalType: () => optionalType, - isSymbolAccessible, - isArrayType, - isTupleType, - isArrayLikeType, - isTypeInvalidDueToUnionDiscriminant, - getAllPossiblePropertiesOfTypes, - getSuggestedSymbolForNonexistentProperty, - getSuggestionForNonexistentProperty, - getSuggestedSymbolForNonexistentSymbol: (location, name, meaning) => getSuggestedSymbolForNonexistentSymbol(location, escapeLeadingUnderscores(name), meaning), - getSuggestionForNonexistentSymbol: (location, name, meaning) => getSuggestionForNonexistentSymbol(location, escapeLeadingUnderscores(name), meaning), - getSuggestedSymbolForNonexistentModule, - getSuggestionForNonexistentExport, - getBaseConstraintOfType, - getDefaultFromTypeParameter: type => type && type.flags & TypeFlags.TypeParameter ? getDefaultFromTypeParameter(type as TypeParameter) : undefined, - resolveName(name, location, meaning, excludeGlobals) { - return resolveName(location, escapeLeadingUnderscores(name), meaning, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false, excludeGlobals); - }, - getJsxNamespace: n => unescapeLeadingUnderscores(getJsxNamespace(n)), - getAccessibleSymbolChain, - getTypePredicateOfSignature, - resolveExternalModuleSymbol, - tryGetThisTypeAt: (node, includeGlobalThis) => { - node = getParseTreeNode(node); - return node && tryGetThisTypeAt(node, includeGlobalThis); - }, - getTypeArgumentConstraint: nodeIn => { - const node = getParseTreeNode(nodeIn, isTypeNode); - return node && getTypeArgumentConstraint(node); - }, - getSuggestionDiagnostics: (file, ct) => { - if (skipTypeChecking(file, compilerOptions, host)) { - return emptyArray; + } + function mergeModuleAugmentation(moduleName: StringLiteral | Identifier): void { + const moduleAugmentation = (moduleName.parent); + if (moduleAugmentation.symbol.declarations[0] !== moduleAugmentation) { + // this is a combined symbol for multiple augmentations within the same file. + // its symbol already has accumulated information for all declarations + // so we need to add it just once - do the work only for first declaration + Debug.assert(moduleAugmentation.symbol.declarations.length > 1); + return; + } + if (isGlobalScopeAugmentation(moduleAugmentation)) { + mergeSymbolTable(globals, moduleAugmentation.symbol.exports!); + } + else { + // find a module that about to be augmented + // do not validate names of augmentations that are defined in ambient context + const moduleNotFoundError = !(moduleName.parent.parent.flags & NodeFlags.Ambient) + ? Diagnostics.Invalid_module_name_in_augmentation_module_0_cannot_be_found + : undefined; + let mainModule = resolveExternalModuleNameWorker(moduleName, moduleName, moduleNotFoundError, /*isForAugmentation*/ true); + if (!mainModule) { + return; + } + // obtain item referenced by 'export=' + mainModule = resolveExternalModuleSymbol(mainModule); + if (mainModule.flags & SymbolFlags.Namespace) { + // If we're merging an augmentation to a pattern ambient module, we want to + // perform the merge unidirectionally from the augmentation ('a.foo') to + // the pattern ('*.foo'), so that 'getMergedSymbol()' on a.foo gives you + // all the exports both from the pattern and from the augmentation, but + // 'getMergedSymbol()' on *.foo only gives you exports from *.foo. + if (some(patternAmbientModules, module => mainModule === module.symbol)) { + const merged = mergeSymbol(moduleAugmentation.symbol, mainModule, /*unidirectional*/ true); + if (!patternAmbientModuleAugmentations) { + patternAmbientModuleAugmentations = createMap(); + } + // moduleName will be a StringLiteral since this is not `declare global`. + patternAmbientModuleAugmentations.set((moduleName as StringLiteral).text, merged); } - - let diagnostics: DiagnosticWithLocation[] | undefined; - try { - // Record the cancellation token so it can be checked later on during checkSourceElement. - // Do this in a finally block so we can ensure that it gets reset back to nothing after - // this call is done. - cancellationToken = ct; - - // Ensure file is type checked - checkSourceFile(file); - Debug.assert(!!(getNodeLinks(file).flags & NodeCheckFlags.TypeChecked)); - - diagnostics = addRange(diagnostics, suggestionDiagnostics.getDiagnostics(file.fileName)); - checkUnusedIdentifiers(getPotentiallyUnusedIdentifiers(file), (containingNode, kind, diag) => { - if (!containsParseError(containingNode) && !unusedIsError(kind, !!(containingNode.flags & NodeFlags.Ambient))) { - (diagnostics || (diagnostics = [])).push({ ...diag, category: DiagnosticCategory.Suggestion }); - } - }); - - return diagnostics || emptyArray; + else { + mergeSymbol(mainModule, moduleAugmentation.symbol); } - finally { - cancellationToken = undefined; + } + else { + // moduleName will be a StringLiteral since this is not `declare global`. + error(moduleName, Diagnostics.Cannot_augment_module_0_because_it_resolves_to_a_non_module_entity, (moduleName as StringLiteral).text); + } + } + } + function addToSymbolTable(target: SymbolTable, source: SymbolTable, message: DiagnosticMessage) { + source.forEach((sourceSymbol, id) => { + const targetSymbol = target.get(id); + if (targetSymbol) { + // Error on redeclarations + forEach(targetSymbol.declarations, addDeclarationDiagnostic(unescapeLeadingUnderscores(id), message)); + } + else { + target.set(id, sourceSymbol); + } + }); + function addDeclarationDiagnostic(id: string, message: DiagnosticMessage) { + return (declaration: Declaration) => diagnostics.add(createDiagnosticForNode(declaration, message, id)); + } + } + function getSymbolLinks(symbol: ts.Symbol): SymbolLinks { + if (symbol.flags & SymbolFlags.Transient) + return symbol; + const id = getSymbolId(symbol); + return symbolLinks[id] || (symbolLinks[id] = {}); + } + function getNodeLinks(node: Node): NodeLinks { + const nodeId = getNodeId(node); + return nodeLinks[nodeId] || (nodeLinks[nodeId] = ({ flags: 0 } as NodeLinks)); + } + function isGlobalSourceFile(node: Node) { + return node.kind === SyntaxKind.SourceFile && !isExternalOrCommonJsModule((node)); + } + function getSymbol(symbols: SymbolTable, name: __String, meaning: SymbolFlags): ts.Symbol | undefined { + if (meaning) { + const symbol = symbols.get(name); + if (symbol) { + Debug.assert((getCheckFlags(symbol) & CheckFlags.Instantiated) === 0, "Should never get an instantiated symbol here."); + if (symbol.flags & meaning) { + return symbol; } - }, - - runWithCancellationToken: (token, callback) => { - try { - cancellationToken = token; - return callback(checker); + if (symbol.flags & SymbolFlags.Alias) { + const target = resolveAlias(symbol); + // Unknown symbol means an error occurred in alias resolution, treat it as positive answer to avoid cascading errors + if (target === unknownSymbol || target.flags & meaning) { + return symbol; + } } - finally { - cancellationToken = undefined; + } + } + // return undefined if we can't find a symbol. + } + /** + * Get symbols that represent parameter-property-declaration as parameter and as property declaration + * @param parameter a parameterDeclaration node + * @param parameterName a name of the parameter to get the symbols for. + * @return a tuple of two symbols + */ + function getSymbolsOfParameterPropertyDeclaration(parameter: ParameterDeclaration, parameterName: __String): [ts.Symbol, ts.Symbol] { + const constructorDeclaration = parameter.parent; + const classDeclaration = parameter.parent.parent; + const parameterSymbol = getSymbol((constructorDeclaration.locals!), parameterName, SymbolFlags.Value); + const propertySymbol = getSymbol(getMembersOfSymbol(classDeclaration.symbol), parameterName, SymbolFlags.Value); + if (parameterSymbol && propertySymbol) { + return [parameterSymbol, propertySymbol]; + } + return Debug.fail("There should exist two symbols, one as property declaration and one as parameter declaration"); + } + function isBlockScopedNameDeclaredBeforeUse(declaration: Declaration, usage: Node): boolean { + const declarationFile = getSourceFileOfNode(declaration); + const useFile = getSourceFileOfNode(usage); + if (declarationFile !== useFile) { + if ((moduleKind && (declarationFile.externalModuleIndicator || useFile.externalModuleIndicator)) || + (!compilerOptions.outFile && !compilerOptions.out) || + isInTypeQuery(usage) || + declaration.flags & NodeFlags.Ambient) { + // nodes are in different files and order cannot be determined + return true; + } + // declaration is after usage + // can be legal if usage is deferred (i.e. inside function or in initializer of instance property) + if (isUsedInFunctionOrInstanceProperty(usage, declaration)) { + return true; + } + const sourceFiles = host.getSourceFiles(); + return sourceFiles.indexOf(declarationFile) <= sourceFiles.indexOf(useFile); + } + if (declaration.pos <= usage.pos) { + // declaration is before usage + if (declaration.kind === SyntaxKind.BindingElement) { + // still might be illegal if declaration and usage are both binding elements (eg var [a = b, b = b] = [1, 2]) + const errorBindingElement = (getAncestor(usage, SyntaxKind.BindingElement) as BindingElement); + if (errorBindingElement) { + return findAncestor(errorBindingElement, isBindingElement) !== findAncestor(declaration, isBindingElement) || + declaration.pos < errorBindingElement.pos; } - }, - - getLocalTypeParametersOfClassOrInterfaceOrTypeAlias, - isDeclarationVisible, - }; - - function getResolvedSignatureWorker(nodeIn: CallLikeExpression, candidatesOutArray: Signature[] | undefined, argumentCount: number | undefined, checkMode: CheckMode): Signature | undefined { - const node = getParseTreeNode(nodeIn, isCallLikeExpression); - apparentArgumentCount = argumentCount; - const res = node ? getResolvedSignature(node, candidatesOutArray, checkMode) : undefined; - apparentArgumentCount = undefined; - return res; + // or it might be illegal if usage happens before parent variable is declared (eg var [a] = a) + return isBlockScopedNameDeclaredBeforeUse((getAncestor(declaration, SyntaxKind.VariableDeclaration) as Declaration), usage); + } + else if (declaration.kind === SyntaxKind.VariableDeclaration) { + // still might be illegal if usage is in the initializer of the variable declaration (eg var a = a) + return !isImmediatelyUsedInInitializerOfBlockScopedVariable((declaration as VariableDeclaration), usage); + } + else if (isClassDeclaration(declaration)) { + // still might be illegal if the usage is within a computed property name in the class (eg class A { static p = "a"; [A.p]() {} }) + return !findAncestor(usage, n => isComputedPropertyName(n) && n.parent.parent === declaration); + } + else if (isPropertyDeclaration(declaration)) { + // still might be illegal if a self-referencing property initializer (eg private x = this.x) + return !isPropertyImmediatelyReferencedWithinDeclaration(declaration, usage, /*stopAtAnyPropertyDeclaration*/ false); + } + else if (isParameterPropertyDeclaration(declaration, declaration.parent)) { + const container = getEnclosingBlockScopeContainer(declaration.parent); + // foo = this.bar is illegal in esnext+useDefineForClassFields when bar is a parameter property + return !(compilerOptions.target === ScriptTarget.ESNext && !!compilerOptions.useDefineForClassFields + && getContainingClass(declaration) === getContainingClass(usage) + && isUsedInFunctionOrInstanceProperty(usage, declaration, container)); + } + return true; } - - const tupleTypes = createMap(); - const unionTypes = createMap(); - const intersectionTypes = createMap(); - const literalTypes = createMap(); - const indexedAccessTypes = createMap(); - const substitutionTypes = createMap(); - const evolvingArrayTypes: EvolvingArrayType[] = []; - const undefinedProperties = createMap() as UnderscoreEscapedMap; - - const unknownSymbol = createSymbol(SymbolFlags.Property, "unknown" as __String); - const resolvingSymbol = createSymbol(0, InternalSymbolName.Resolving); - - const anyType = createIntrinsicType(TypeFlags.Any, "any"); - const autoType = createIntrinsicType(TypeFlags.Any, "any"); - const wildcardType = createIntrinsicType(TypeFlags.Any, "any"); - const errorType = createIntrinsicType(TypeFlags.Any, "error"); - const nonInferrableAnyType = createIntrinsicType(TypeFlags.Any, "any", ObjectFlags.ContainsWideningType); - const unknownType = createIntrinsicType(TypeFlags.Unknown, "unknown"); - const undefinedType = createIntrinsicType(TypeFlags.Undefined, "undefined"); - const undefinedWideningType = strictNullChecks ? undefinedType : createIntrinsicType(TypeFlags.Undefined, "undefined", ObjectFlags.ContainsWideningType); - const optionalType = createIntrinsicType(TypeFlags.Undefined, "undefined"); - const nullType = createIntrinsicType(TypeFlags.Null, "null"); - const nullWideningType = strictNullChecks ? nullType : createIntrinsicType(TypeFlags.Null, "null", ObjectFlags.ContainsWideningType); - const stringType = createIntrinsicType(TypeFlags.String, "string"); - const numberType = createIntrinsicType(TypeFlags.Number, "number"); - const bigintType = createIntrinsicType(TypeFlags.BigInt, "bigint"); - const falseType = createIntrinsicType(TypeFlags.BooleanLiteral, "false") as FreshableIntrinsicType; - const regularFalseType = createIntrinsicType(TypeFlags.BooleanLiteral, "false") as FreshableIntrinsicType; - const trueType = createIntrinsicType(TypeFlags.BooleanLiteral, "true") as FreshableIntrinsicType; - const regularTrueType = createIntrinsicType(TypeFlags.BooleanLiteral, "true") as FreshableIntrinsicType; - trueType.regularType = regularTrueType; - trueType.freshType = trueType; - regularTrueType.regularType = regularTrueType; - regularTrueType.freshType = trueType; - falseType.regularType = regularFalseType; - falseType.freshType = falseType; - regularFalseType.regularType = regularFalseType; - regularFalseType.freshType = falseType; - const booleanType = createBooleanType([regularFalseType, regularTrueType]); - // Also mark all combinations of fresh/regular booleans as "Boolean" so they print as `boolean` instead of `true | false` - // (The union is cached, so simply doing the marking here is sufficient) - createBooleanType([regularFalseType, trueType]); - createBooleanType([falseType, regularTrueType]); - createBooleanType([falseType, trueType]); - const esSymbolType = createIntrinsicType(TypeFlags.ESSymbol, "symbol"); - const voidType = createIntrinsicType(TypeFlags.Void, "void"); - const neverType = createIntrinsicType(TypeFlags.Never, "never"); - const silentNeverType = createIntrinsicType(TypeFlags.Never, "never"); - const nonInferrableType = createIntrinsicType(TypeFlags.Never, "never", ObjectFlags.NonInferrableType); - const implicitNeverType = createIntrinsicType(TypeFlags.Never, "never"); - const unreachableNeverType = createIntrinsicType(TypeFlags.Never, "never"); - const nonPrimitiveType = createIntrinsicType(TypeFlags.NonPrimitive, "object"); - const stringNumberSymbolType = getUnionType([stringType, numberType, esSymbolType]); - const keyofConstraintType = keyofStringsOnly ? stringType : stringNumberSymbolType; - const numberOrBigIntType = getUnionType([numberType, bigintType]); - - const emptyObjectType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined); - const emptyJsxObjectType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined); - emptyJsxObjectType.objectFlags |= ObjectFlags.JsxAttributes; - - const emptyTypeLiteralSymbol = createSymbol(SymbolFlags.TypeLiteral, InternalSymbolName.Type); - emptyTypeLiteralSymbol.members = createSymbolTable(); - const emptyTypeLiteralType = createAnonymousType(emptyTypeLiteralSymbol, emptySymbols, emptyArray, emptyArray, undefined, undefined); - - const emptyGenericType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined); - emptyGenericType.instantiations = createMap(); - - const anyFunctionType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined); - // The anyFunctionType contains the anyFunctionType by definition. The flag is further propagated - // in getPropagatingFlagsOfTypes, and it is checked in inferFromTypes. - anyFunctionType.objectFlags |= ObjectFlags.NonInferrableType; - - const noConstraintType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined); - const circularConstraintType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined); - const resolvingDefaultType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined); - - const markerSuperType = createTypeParameter(); - const markerSubType = createTypeParameter(); - markerSubType.constraint = markerSuperType; - const markerOtherType = createTypeParameter(); - - const noTypePredicate = createTypePredicate(TypePredicateKind.Identifier, "<>", 0, anyType); - - const anySignature = createSignature(undefined, undefined, undefined, emptyArray, anyType, /*resolvedTypePredicate*/ undefined, 0, SignatureFlags.None); - const unknownSignature = createSignature(undefined, undefined, undefined, emptyArray, errorType, /*resolvedTypePredicate*/ undefined, 0, SignatureFlags.None); - const resolvingSignature = createSignature(undefined, undefined, undefined, emptyArray, anyType, /*resolvedTypePredicate*/ undefined, 0, SignatureFlags.None); - const silentNeverSignature = createSignature(undefined, undefined, undefined, emptyArray, silentNeverType, /*resolvedTypePredicate*/ undefined, 0, SignatureFlags.None); - - const enumNumberIndexInfo = createIndexInfo(stringType, /*isReadonly*/ true); - - const iterationTypesCache = createMap(); // cache for common IterationTypes instances - const noIterationTypes: IterationTypes = { - get yieldType(): Type { return Debug.fail("Not supported"); }, - get returnType(): Type { return Debug.fail("Not supported"); }, - get nextType(): Type { return Debug.fail("Not supported"); }, - }; - - const anyIterationTypes = createIterationTypes(anyType, anyType, anyType); - const anyIterationTypesExceptNext = createIterationTypes(anyType, anyType, unknownType); - const defaultIterationTypes = createIterationTypes(neverType, anyType, undefinedType); // default iteration types for `Iterator`. - - const asyncIterationTypesResolver: IterationTypesResolver = { - iterableCacheKey: "iterationTypesOfAsyncIterable", - iteratorCacheKey: "iterationTypesOfAsyncIterator", - iteratorSymbolName: "asyncIterator", - getGlobalIteratorType: getGlobalAsyncIteratorType, - getGlobalIterableType: getGlobalAsyncIterableType, - getGlobalIterableIteratorType: getGlobalAsyncIterableIteratorType, - getGlobalGeneratorType: getGlobalAsyncGeneratorType, - resolveIterationType: getAwaitedType, - mustHaveANextMethodDiagnostic: Diagnostics.An_async_iterator_must_have_a_next_method, - mustBeAMethodDiagnostic: Diagnostics.The_0_property_of_an_async_iterator_must_be_a_method, - mustHaveAValueDiagnostic: Diagnostics.The_type_returned_by_the_0_method_of_an_async_iterator_must_be_a_promise_for_a_type_with_a_value_property, - }; - - const syncIterationTypesResolver: IterationTypesResolver = { - iterableCacheKey: "iterationTypesOfIterable", - iteratorCacheKey: "iterationTypesOfIterator", - iteratorSymbolName: "iterator", - getGlobalIteratorType, - getGlobalIterableType, - getGlobalIterableIteratorType, - getGlobalGeneratorType, - resolveIterationType: (type, _errorNode) => type, - mustHaveANextMethodDiagnostic: Diagnostics.An_iterator_must_have_a_next_method, - mustBeAMethodDiagnostic: Diagnostics.The_0_property_of_an_iterator_must_be_a_method, - mustHaveAValueDiagnostic: Diagnostics.The_type_returned_by_the_0_method_of_an_iterator_must_have_a_value_property, - }; - - interface DuplicateInfoForSymbol { - readonly firstFileLocations: Node[]; - readonly secondFileLocations: Node[]; - readonly isBlockScoped: boolean; - } - interface DuplicateInfoForFiles { - readonly firstFile: SourceFile; - readonly secondFile: SourceFile; - /** Key is symbol name. */ - readonly conflictingSymbols: Map; - } - /** Key is "/path/to/a.ts|/path/to/b.ts". */ - let amalgamatedDuplicates: Map | undefined; - const reverseMappedCache = createMap(); - let ambientModulesCache: Symbol[] | undefined; - /** - * List of every ambient module with a "*" wildcard. - * Unlike other ambient modules, these can't be stored in `globals` because symbol tables only deal with exact matches. - * This is only used if there is no exact match. - */ - let patternAmbientModules: PatternAmbientModule[]; - let patternAmbientModuleAugmentations: Map | undefined; - - let globalObjectType: ObjectType; - let globalFunctionType: ObjectType; - let globalCallableFunctionType: ObjectType; - let globalNewableFunctionType: ObjectType; - let globalArrayType: GenericType; - let globalReadonlyArrayType: GenericType; - let globalStringType: ObjectType; - let globalNumberType: ObjectType; - let globalBooleanType: ObjectType; - let globalRegExpType: ObjectType; - let globalThisType: GenericType; - let anyArrayType: Type; - let autoArrayType: Type; - let anyReadonlyArrayType: Type; - let deferredGlobalNonNullableTypeAlias: Symbol; - - // The library files are only loaded when the feature is used. - // This allows users to just specify library files they want to used through --lib - // and they will not get an error from not having unrelated library files - let deferredGlobalESSymbolConstructorSymbol: Symbol | undefined; - let deferredGlobalESSymbolType: ObjectType; - let deferredGlobalTypedPropertyDescriptorType: GenericType; - let deferredGlobalPromiseType: GenericType; - let deferredGlobalPromiseLikeType: GenericType; - let deferredGlobalPromiseConstructorSymbol: Symbol | undefined; - let deferredGlobalPromiseConstructorLikeType: ObjectType; - let deferredGlobalIterableType: GenericType; - let deferredGlobalIteratorType: GenericType; - let deferredGlobalIterableIteratorType: GenericType; - let deferredGlobalGeneratorType: GenericType; - let deferredGlobalIteratorYieldResultType: GenericType; - let deferredGlobalIteratorReturnResultType: GenericType; - let deferredGlobalAsyncIterableType: GenericType; - let deferredGlobalAsyncIteratorType: GenericType; - let deferredGlobalAsyncIterableIteratorType: GenericType; - let deferredGlobalAsyncGeneratorType: GenericType; - let deferredGlobalTemplateStringsArrayType: ObjectType; - let deferredGlobalImportMetaType: ObjectType; - let deferredGlobalExtractSymbol: Symbol; - let deferredGlobalOmitSymbol: Symbol; - let deferredGlobalBigIntType: ObjectType; - - const allPotentiallyUnusedIdentifiers = createMap(); // key is file name - - let flowLoopStart = 0; - let flowLoopCount = 0; - let sharedFlowCount = 0; - let flowAnalysisDisabled = false; - let flowInvocationCount = 0; - let lastFlowNode: FlowNode | undefined; - let lastFlowNodeReachable: boolean; - let flowTypeCache: Type[] | undefined; - - const emptyStringType = getLiteralType(""); - const zeroType = getLiteralType(0); - const zeroBigIntType = getLiteralType({ negative: false, base10Value: "0" }); - - const resolutionTargets: TypeSystemEntity[] = []; - const resolutionResults: boolean[] = []; - const resolutionPropertyNames: TypeSystemPropertyName[] = []; - - let suggestionCount = 0; - const maximumSuggestionCount = 10; - const mergedSymbols: Symbol[] = []; - const symbolLinks: SymbolLinks[] = []; - const nodeLinks: NodeLinks[] = []; - const flowLoopCaches: Map[] = []; - const flowLoopNodes: FlowNode[] = []; - const flowLoopKeys: string[] = []; - const flowLoopTypes: Type[][] = []; - const sharedFlowNodes: FlowNode[] = []; - const sharedFlowTypes: FlowType[] = []; - const flowNodeReachable: (boolean | undefined)[] = []; - const potentialThisCollisions: Node[] = []; - const potentialNewTargetCollisions: Node[] = []; - const potentialWeakMapCollisions: Node[] = []; - const awaitedTypeStack: number[] = []; - - const diagnostics = createDiagnosticCollection(); - const suggestionDiagnostics = createDiagnosticCollection(); - - const typeofTypesByName: ReadonlyMap = createMapFromTemplate({ - string: stringType, - number: numberType, - bigint: bigintType, - boolean: booleanType, - symbol: esSymbolType, - undefined: undefinedType - }); - const typeofType = createTypeofType(); - - let _jsxNamespace: __String; - let _jsxFactoryEntity: EntityName | undefined; - let outofbandVarianceMarkerHandler: ((onlyUnreliable: boolean) => void) | undefined; - - const subtypeRelation = createMap(); - const strictSubtypeRelation = createMap(); - const assignableRelation = createMap(); - const comparableRelation = createMap(); - const identityRelation = createMap(); - const enumRelation = createMap(); - - const builtinGlobals = createSymbolTable(); - builtinGlobals.set(undefinedSymbol.escapedName, undefinedSymbol); - - initializeTypeChecker(); - - return checker; - - function getJsxNamespace(location: Node | undefined): __String { - if (location) { - const file = getSourceFileOfNode(location); - if (file) { - if (file.localJsxNamespace) { - return file.localJsxNamespace; - } - const jsxPragma = file.pragmas.get("jsx"); - if (jsxPragma) { - const chosenpragma = isArray(jsxPragma) ? jsxPragma[0] : jsxPragma; - file.localJsxFactory = parseIsolatedEntityName(chosenpragma.arguments.factory, languageVersion); - visitNode(file.localJsxFactory, markAsSynthetic); - if (file.localJsxFactory) { - return file.localJsxNamespace = getFirstIdentifier(file.localJsxFactory).escapedText; - } - } - } - } - if (!_jsxNamespace) { - _jsxNamespace = "React" as __String; - if (compilerOptions.jsxFactory) { - _jsxFactoryEntity = parseIsolatedEntityName(compilerOptions.jsxFactory, languageVersion); - visitNode(_jsxFactoryEntity, markAsSynthetic); - if (_jsxFactoryEntity) { - _jsxNamespace = getFirstIdentifier(_jsxFactoryEntity).escapedText; - } - } - else if (compilerOptions.reactNamespace) { - _jsxNamespace = escapeLeadingUnderscores(compilerOptions.reactNamespace); - } - } - if (!_jsxFactoryEntity) { - _jsxFactoryEntity = createQualifiedName(createIdentifier(unescapeLeadingUnderscores(_jsxNamespace)), "createElement"); - } - return _jsxNamespace; - - function markAsSynthetic(node: Node): VisitResult { - node.pos = -1; - node.end = -1; - return visitEachChild(node, markAsSynthetic, nullTransformationContext); - } - } - - function getEmitResolver(sourceFile: SourceFile, cancellationToken: CancellationToken) { - // Ensure we have all the type information in place for this file so that all the - // emitter questions of this resolver will return the right information. - getDiagnostics(sourceFile, cancellationToken); - return emitResolver; + // declaration is after usage, but it can still be legal if usage is deferred: + // 1. inside an export specifier + // 2. inside a function + // 3. inside an instance property initializer, a reference to a non-instance property + // (except when target: "esnext" and useDefineForClassFields: true and the reference is to a parameter property) + // 4. inside a static property initializer, a reference to a static method in the same class + // 5. inside a TS export= declaration (since we will move the export statement during emit to avoid TDZ) + // or if usage is in a type context: + // 1. inside a type query (typeof in type position) + // 2. inside a jsdoc comment + if (usage.parent.kind === SyntaxKind.ExportSpecifier || (usage.parent.kind === SyntaxKind.ExportAssignment && (usage.parent as ExportAssignment).isExportEquals)) { + // export specifiers do not use the variable, they only make it available for use + return true; } - - function lookupOrIssueError(location: Node | undefined, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): Diagnostic { - const diagnostic = location - ? createDiagnosticForNode(location, message, arg0, arg1, arg2, arg3) - : createCompilerDiagnostic(message, arg0, arg1, arg2, arg3); - const existing = diagnostics.lookup(diagnostic); - if (existing) { - return existing; - } - else { - diagnostics.add(diagnostic); - return diagnostic; - } + // When resolving symbols for exports, the `usage` location passed in can be the export site directly + if (usage.kind === SyntaxKind.ExportAssignment && (usage as ExportAssignment).isExportEquals) { + return true; } - - function error(location: Node | undefined, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): Diagnostic { - const diagnostic = location - ? createDiagnosticForNode(location, message, arg0, arg1, arg2, arg3) - : createCompilerDiagnostic(message, arg0, arg1, arg2, arg3); - diagnostics.add(diagnostic); - return diagnostic; + const container = getEnclosingBlockScopeContainer(declaration); + if (!!(usage.flags & NodeFlags.JSDoc) || isInTypeQuery(usage)) { + return true; } - - function addErrorOrSuggestion(isError: boolean, diagnostic: DiagnosticWithLocation) { - if (isError) { - diagnostics.add(diagnostic); + if (isUsedInFunctionOrInstanceProperty(usage, declaration, container)) { + if (compilerOptions.target === ScriptTarget.ESNext && !!compilerOptions.useDefineForClassFields) { + return (isPropertyDeclaration(declaration) || isParameterPropertyDeclaration(declaration, declaration.parent)) && + !isPropertyImmediatelyReferencedWithinDeclaration(declaration, usage, /*stopAtAnyPropertyDeclaration*/ true); } else { - suggestionDiagnostics.add({ ...diagnostic, category: DiagnosticCategory.Suggestion }); - } - } - function errorOrSuggestion(isError: boolean, location: Node, message: DiagnosticMessage | DiagnosticMessageChain, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): void { - addErrorOrSuggestion(isError, "message" in message ? createDiagnosticForNode(location, message, arg0, arg1, arg2, arg3) : createDiagnosticForNodeFromMessageChain(location, message)); // eslint-disable-line no-in-operator - } - - function errorAndMaybeSuggestAwait( - location: Node, - maybeMissingAwait: boolean, - message: DiagnosticMessage, - arg0?: string | number | undefined, arg1?: string | number | undefined, arg2?: string | number | undefined, arg3?: string | number | undefined): Diagnostic { - const diagnostic = error(location, message, arg0, arg1, arg2, arg3); - if (maybeMissingAwait) { - const related = createDiagnosticForNode(location, Diagnostics.Did_you_forget_to_use_await); - addRelatedInfo(diagnostic, related); + return true; } - return diagnostic; } - - function createSymbol(flags: SymbolFlags, name: __String, checkFlags?: CheckFlags) { - symbolCount++; - const symbol = (new Symbol(flags | SymbolFlags.Transient, name)); - symbol.checkFlags = checkFlags || 0; - return symbol; - } - - function getExcludedSymbolFlags(flags: SymbolFlags): SymbolFlags { - let result: SymbolFlags = 0; - if (flags & SymbolFlags.BlockScopedVariable) result |= SymbolFlags.BlockScopedVariableExcludes; - if (flags & SymbolFlags.FunctionScopedVariable) result |= SymbolFlags.FunctionScopedVariableExcludes; - if (flags & SymbolFlags.Property) result |= SymbolFlags.PropertyExcludes; - if (flags & SymbolFlags.EnumMember) result |= SymbolFlags.EnumMemberExcludes; - if (flags & SymbolFlags.Function) result |= SymbolFlags.FunctionExcludes; - if (flags & SymbolFlags.Class) result |= SymbolFlags.ClassExcludes; - if (flags & SymbolFlags.Interface) result |= SymbolFlags.InterfaceExcludes; - if (flags & SymbolFlags.RegularEnum) result |= SymbolFlags.RegularEnumExcludes; - if (flags & SymbolFlags.ConstEnum) result |= SymbolFlags.ConstEnumExcludes; - if (flags & SymbolFlags.ValueModule) result |= SymbolFlags.ValueModuleExcludes; - if (flags & SymbolFlags.Method) result |= SymbolFlags.MethodExcludes; - if (flags & SymbolFlags.GetAccessor) result |= SymbolFlags.GetAccessorExcludes; - if (flags & SymbolFlags.SetAccessor) result |= SymbolFlags.SetAccessorExcludes; - if (flags & SymbolFlags.TypeParameter) result |= SymbolFlags.TypeParameterExcludes; - if (flags & SymbolFlags.TypeAlias) result |= SymbolFlags.TypeAliasExcludes; - if (flags & SymbolFlags.Alias) result |= SymbolFlags.AliasExcludes; - return result; - } - - function recordMergedSymbol(target: Symbol, source: Symbol) { - if (!source.mergeId) { - source.mergeId = nextMergeId; - nextMergeId++; + return false; + function isImmediatelyUsedInInitializerOfBlockScopedVariable(declaration: VariableDeclaration, usage: Node): boolean { + const container = getEnclosingBlockScopeContainer(declaration); + switch (declaration.parent.parent.kind) { + case SyntaxKind.VariableStatement: + case SyntaxKind.ForStatement: + case SyntaxKind.ForOfStatement: + // variable statement/for/for-of statement case, + // use site should not be inside variable declaration (initializer of declaration or binding element) + if (isSameScopeDescendentOf(usage, declaration, container)) { + return true; + } + break; } - mergedSymbols[source.mergeId] = target; - } - - function cloneSymbol(symbol: Symbol): Symbol { - const result = createSymbol(symbol.flags, symbol.escapedName); - result.declarations = symbol.declarations ? symbol.declarations.slice() : []; - result.parent = symbol.parent; - if (symbol.valueDeclaration) result.valueDeclaration = symbol.valueDeclaration; - if (symbol.constEnumOnlyModule) result.constEnumOnlyModule = true; - if (symbol.members) result.members = cloneMap(symbol.members); - if (symbol.exports) result.exports = cloneMap(symbol.exports); - recordMergedSymbol(result, symbol); - return result; + // ForIn/ForOf case - use site should not be used in expression part + const grandparent = declaration.parent.parent; + return isForInOrOfStatement(grandparent) && isSameScopeDescendentOf(usage, grandparent.expression, container); } - - /** - * Note: if target is transient, then it is mutable, and mergeSymbol with both mutate and return it. - * If target is not transient, mergeSymbol will produce a transient clone, mutate that and return it. - */ - function mergeSymbol(target: Symbol, source: Symbol, unidirectional = false): Symbol { - if (!(target.flags & getExcludedSymbolFlags(source.flags)) || - (source.flags | target.flags) & SymbolFlags.Assignment) { - if (source === target) { - // This can happen when an export assigned namespace exports something also erroneously exported at the top level - // See `declarationFileNoCrashOnExtraExportModifier` for an example - return target; - } - if (!(target.flags & SymbolFlags.Transient)) { - const resolvedTarget = resolveSymbol(target); - if (resolvedTarget === unknownSymbol) { - return source; - } - target = cloneSymbol(resolvedTarget); - } - // Javascript static-property-assignment declarations always merge, even though they are also values - if (source.flags & SymbolFlags.ValueModule && target.flags & SymbolFlags.ValueModule && target.constEnumOnlyModule && !source.constEnumOnlyModule) { - // reset flag when merging instantiated module into value module that has only const enums - target.constEnumOnlyModule = false; - } - target.flags |= source.flags; - if (source.valueDeclaration && - (!target.valueDeclaration || - isAssignmentDeclaration(target.valueDeclaration) && !isAssignmentDeclaration(source.valueDeclaration) || - isEffectiveModuleDeclaration(target.valueDeclaration) && !isEffectiveModuleDeclaration(source.valueDeclaration))) { - // other kinds of value declarations take precedence over modules and assignment declarations - target.valueDeclaration = source.valueDeclaration; - } - addRange(target.declarations, source.declarations); - if (source.members) { - if (!target.members) target.members = createSymbolTable(); - mergeSymbolTable(target.members, source.members, unidirectional); - } - if (source.exports) { - if (!target.exports) target.exports = createSymbolTable(); - mergeSymbolTable(target.exports, source.exports, unidirectional); - } - if (!unidirectional) { - recordMergedSymbol(target, source); - } - } - else if (target.flags & SymbolFlags.NamespaceModule) { - // Do not report an error when merging `var globalThis` with the built-in `globalThis`, - // as we will already report a "Declaration name conflicts..." error, and this error - // won't make much sense. - if (target !== globalThisSymbol) { - error(getNameOfDeclaration(source.declarations[0]), Diagnostics.Cannot_augment_module_0_with_value_exports_because_it_resolves_to_a_non_module_entity, symbolToString(target)); - } - } - else { // error - const isEitherEnum = !!(target.flags & SymbolFlags.Enum || source.flags & SymbolFlags.Enum); - const isEitherBlockScoped = !!(target.flags & SymbolFlags.BlockScopedVariable || source.flags & SymbolFlags.BlockScopedVariable); - const message = isEitherEnum - ? Diagnostics.Enum_declarations_can_only_merge_with_namespace_or_other_enum_declarations - : isEitherBlockScoped - ? Diagnostics.Cannot_redeclare_block_scoped_variable_0 - : Diagnostics.Duplicate_identifier_0; - const sourceSymbolFile = source.declarations && getSourceFileOfNode(source.declarations[0]); - const targetSymbolFile = target.declarations && getSourceFileOfNode(target.declarations[0]); - const symbolName = symbolToString(source); - - // Collect top-level duplicate identifier errors into one mapping, so we can then merge their diagnostics if there are a bunch - if (sourceSymbolFile && targetSymbolFile && amalgamatedDuplicates && !isEitherEnum && sourceSymbolFile !== targetSymbolFile) { - const firstFile = comparePaths(sourceSymbolFile.path, targetSymbolFile.path) === Comparison.LessThan ? sourceSymbolFile : targetSymbolFile; - const secondFile = firstFile === sourceSymbolFile ? targetSymbolFile : sourceSymbolFile; - const filesDuplicates = getOrUpdate(amalgamatedDuplicates, `${firstFile.path}|${secondFile.path}`, () => - ({ firstFile, secondFile, conflictingSymbols: createMap() })); - const conflictingSymbolInfo = getOrUpdate(filesDuplicates.conflictingSymbols, symbolName, () => - ({ isBlockScoped: isEitherBlockScoped, firstFileLocations: [], secondFileLocations: [] })); - addDuplicateLocations(conflictingSymbolInfo.firstFileLocations, source); - addDuplicateLocations(conflictingSymbolInfo.secondFileLocations, target); - } - else { - addDuplicateDeclarationErrorsForSymbols(source, message, symbolName, target); - addDuplicateDeclarationErrorsForSymbols(target, message, symbolName, source); - } - } - return target; - - function addDuplicateLocations(locs: Node[], symbol: Symbol): void { - for (const decl of symbol.declarations) { - pushIfUnique(locs, (getExpandoInitializer(decl, /*isPrototypeAssignment*/ false) ? getNameOfExpando(decl) : getNameOfDeclaration(decl)) || decl); + function isUsedInFunctionOrInstanceProperty(usage: Node, declaration: Node, container?: Node): boolean { + return !!findAncestor(usage, current => { + if (current === container) { + return "quit"; } - } - } - - function addDuplicateDeclarationErrorsForSymbols(target: Symbol, message: DiagnosticMessage, symbolName: string, source: Symbol) { - forEach(target.declarations, node => { - const errorNode = (getExpandoInitializer(node, /*isPrototypeAssignment*/ false) ? getNameOfExpando(node) : getNameOfDeclaration(node)) || node; - addDuplicateDeclarationError(errorNode, message, symbolName, source.declarations); - }); - } - - function addDuplicateDeclarationError(errorNode: Node, message: DiagnosticMessage, symbolName: string, relatedNodes: readonly Node[] | undefined) { - const err = lookupOrIssueError(errorNode, message, symbolName); - for (const relatedNode of relatedNodes || emptyArray) { - err.relatedInformation = err.relatedInformation || []; - if (length(err.relatedInformation) >= 5) continue; - addRelatedInfo(err, !length(err.relatedInformation) ? createDiagnosticForNode(relatedNode, Diagnostics._0_was_also_declared_here, symbolName) : createDiagnosticForNode(relatedNode, Diagnostics.and_here)); - } - } - - function combineSymbolTables(first: SymbolTable | undefined, second: SymbolTable | undefined): SymbolTable | undefined { - if (!hasEntries(first)) return second; - if (!hasEntries(second)) return first; - const combined = createSymbolTable(); - mergeSymbolTable(combined, first); - mergeSymbolTable(combined, second); - return combined; - } - - function mergeSymbolTable(target: SymbolTable, source: SymbolTable, unidirectional = false) { - source.forEach((sourceSymbol, id) => { - const targetSymbol = target.get(id); - target.set(id, targetSymbol ? mergeSymbol(targetSymbol, sourceSymbol, unidirectional) : sourceSymbol); - }); - } - - function mergeModuleAugmentation(moduleName: StringLiteral | Identifier): void { - const moduleAugmentation = moduleName.parent; - if (moduleAugmentation.symbol.declarations[0] !== moduleAugmentation) { - // this is a combined symbol for multiple augmentations within the same file. - // its symbol already has accumulated information for all declarations - // so we need to add it just once - do the work only for first declaration - Debug.assert(moduleAugmentation.symbol.declarations.length > 1); - return; - } - - if (isGlobalScopeAugmentation(moduleAugmentation)) { - mergeSymbolTable(globals, moduleAugmentation.symbol.exports!); - } - else { - // find a module that about to be augmented - // do not validate names of augmentations that are defined in ambient context - const moduleNotFoundError = !(moduleName.parent.parent.flags & NodeFlags.Ambient) - ? Diagnostics.Invalid_module_name_in_augmentation_module_0_cannot_be_found - : undefined; - let mainModule = resolveExternalModuleNameWorker(moduleName, moduleName, moduleNotFoundError, /*isForAugmentation*/ true); - if (!mainModule) { - return; + if (isFunctionLike(current)) { + return true; } - // obtain item referenced by 'export=' - mainModule = resolveExternalModuleSymbol(mainModule); - if (mainModule.flags & SymbolFlags.Namespace) { - // If we're merging an augmentation to a pattern ambient module, we want to - // perform the merge unidirectionally from the augmentation ('a.foo') to - // the pattern ('*.foo'), so that 'getMergedSymbol()' on a.foo gives you - // all the exports both from the pattern and from the augmentation, but - // 'getMergedSymbol()' on *.foo only gives you exports from *.foo. - if (some(patternAmbientModules, module => mainModule === module.symbol)) { - const merged = mergeSymbol(moduleAugmentation.symbol, mainModule, /*unidirectional*/ true); - if (!patternAmbientModuleAugmentations) { - patternAmbientModuleAugmentations = createMap(); + const initializerOfProperty = current.parent && + current.parent.kind === SyntaxKind.PropertyDeclaration && + (current.parent).initializer === current; + if (initializerOfProperty) { + if (hasModifier(current.parent, ModifierFlags.Static)) { + if (declaration.kind === SyntaxKind.MethodDeclaration) { + return true; } - // moduleName will be a StringLiteral since this is not `declare global`. - patternAmbientModuleAugmentations.set((moduleName as StringLiteral).text, merged); } else { - mergeSymbol(mainModule, moduleAugmentation.symbol); - } - } - else { - // moduleName will be a StringLiteral since this is not `declare global`. - error(moduleName, Diagnostics.Cannot_augment_module_0_because_it_resolves_to_a_non_module_entity, (moduleName as StringLiteral).text); - } - } - } - - function addToSymbolTable(target: SymbolTable, source: SymbolTable, message: DiagnosticMessage) { - source.forEach((sourceSymbol, id) => { - const targetSymbol = target.get(id); - if (targetSymbol) { - // Error on redeclarations - forEach(targetSymbol.declarations, addDeclarationDiagnostic(unescapeLeadingUnderscores(id), message)); - } - else { - target.set(id, sourceSymbol); - } - }); - - function addDeclarationDiagnostic(id: string, message: DiagnosticMessage) { - return (declaration: Declaration) => diagnostics.add(createDiagnosticForNode(declaration, message, id)); - } - } - - function getSymbolLinks(symbol: Symbol): SymbolLinks { - if (symbol.flags & SymbolFlags.Transient) return symbol; - const id = getSymbolId(symbol); - return symbolLinks[id] || (symbolLinks[id] = {}); - } - - function getNodeLinks(node: Node): NodeLinks { - const nodeId = getNodeId(node); - return nodeLinks[nodeId] || (nodeLinks[nodeId] = { flags: 0 } as NodeLinks); - } - - function isGlobalSourceFile(node: Node) { - return node.kind === SyntaxKind.SourceFile && !isExternalOrCommonJsModule(node); - } - - function getSymbol(symbols: SymbolTable, name: __String, meaning: SymbolFlags): Symbol | undefined { - if (meaning) { - const symbol = symbols.get(name); - if (symbol) { - Debug.assert((getCheckFlags(symbol) & CheckFlags.Instantiated) === 0, "Should never get an instantiated symbol here."); - if (symbol.flags & meaning) { - return symbol; - } - if (symbol.flags & SymbolFlags.Alias) { - const target = resolveAlias(symbol); - // Unknown symbol means an error occurred in alias resolution, treat it as positive answer to avoid cascading errors - if (target === unknownSymbol || target.flags & meaning) { - return symbol; + const isDeclarationInstanceProperty = declaration.kind === SyntaxKind.PropertyDeclaration && !hasModifier(declaration, ModifierFlags.Static); + if (!isDeclarationInstanceProperty || getContainingClass(usage) !== getContainingClass(declaration)) { + return true; } } } - } - // return undefined if we can't find a symbol. - } - - /** - * Get symbols that represent parameter-property-declaration as parameter and as property declaration - * @param parameter a parameterDeclaration node - * @param parameterName a name of the parameter to get the symbols for. - * @return a tuple of two symbols - */ - function getSymbolsOfParameterPropertyDeclaration(parameter: ParameterDeclaration, parameterName: __String): [Symbol, Symbol] { - const constructorDeclaration = parameter.parent; - const classDeclaration = parameter.parent.parent; - - const parameterSymbol = getSymbol(constructorDeclaration.locals!, parameterName, SymbolFlags.Value); - const propertySymbol = getSymbol(getMembersOfSymbol(classDeclaration.symbol), parameterName, SymbolFlags.Value); - - if (parameterSymbol && propertySymbol) { - return [parameterSymbol, propertySymbol]; - } - - return Debug.fail("There should exist two symbols, one as property declaration and one as parameter declaration"); + return false; + }); } - - function isBlockScopedNameDeclaredBeforeUse(declaration: Declaration, usage: Node): boolean { - const declarationFile = getSourceFileOfNode(declaration); - const useFile = getSourceFileOfNode(usage); - if (declarationFile !== useFile) { - if ((moduleKind && (declarationFile.externalModuleIndicator || useFile.externalModuleIndicator)) || - (!compilerOptions.outFile && !compilerOptions.out) || - isInTypeQuery(usage) || - declaration.flags & NodeFlags.Ambient) { - // nodes are in different files and order cannot be determined - return true; - } - // declaration is after usage - // can be legal if usage is deferred (i.e. inside function or in initializer of instance property) - if (isUsedInFunctionOrInstanceProperty(usage, declaration)) { - return true; - } - const sourceFiles = host.getSourceFiles(); - return sourceFiles.indexOf(declarationFile) <= sourceFiles.indexOf(useFile); - } - - if (declaration.pos <= usage.pos) { - // declaration is before usage - if (declaration.kind === SyntaxKind.BindingElement) { - // still might be illegal if declaration and usage are both binding elements (eg var [a = b, b = b] = [1, 2]) - const errorBindingElement = getAncestor(usage, SyntaxKind.BindingElement) as BindingElement; - if (errorBindingElement) { - return findAncestor(errorBindingElement, isBindingElement) !== findAncestor(declaration, isBindingElement) || - declaration.pos < errorBindingElement.pos; - } - // or it might be illegal if usage happens before parent variable is declared (eg var [a] = a) - return isBlockScopedNameDeclaredBeforeUse(getAncestor(declaration, SyntaxKind.VariableDeclaration) as Declaration, usage); - } - else if (declaration.kind === SyntaxKind.VariableDeclaration) { - // still might be illegal if usage is in the initializer of the variable declaration (eg var a = a) - return !isImmediatelyUsedInInitializerOfBlockScopedVariable(declaration as VariableDeclaration, usage); - } - else if (isClassDeclaration(declaration)) { - // still might be illegal if the usage is within a computed property name in the class (eg class A { static p = "a"; [A.p]() {} }) - return !findAncestor(usage, n => isComputedPropertyName(n) && n.parent.parent === declaration); - } - else if (isPropertyDeclaration(declaration)) { - // still might be illegal if a self-referencing property initializer (eg private x = this.x) - return !isPropertyImmediatelyReferencedWithinDeclaration(declaration, usage, /*stopAtAnyPropertyDeclaration*/ false); - } - else if (isParameterPropertyDeclaration(declaration, declaration.parent)) { - const container = getEnclosingBlockScopeContainer(declaration.parent); - // foo = this.bar is illegal in esnext+useDefineForClassFields when bar is a parameter property - return !(compilerOptions.target === ScriptTarget.ESNext && !!compilerOptions.useDefineForClassFields - && getContainingClass(declaration) === getContainingClass(usage) - && isUsedInFunctionOrInstanceProperty(usage, declaration, container)); - } - return true; - } - - - // declaration is after usage, but it can still be legal if usage is deferred: - // 1. inside an export specifier - // 2. inside a function - // 3. inside an instance property initializer, a reference to a non-instance property - // (except when target: "esnext" and useDefineForClassFields: true and the reference is to a parameter property) - // 4. inside a static property initializer, a reference to a static method in the same class - // 5. inside a TS export= declaration (since we will move the export statement during emit to avoid TDZ) - // or if usage is in a type context: - // 1. inside a type query (typeof in type position) - // 2. inside a jsdoc comment - if (usage.parent.kind === SyntaxKind.ExportSpecifier || (usage.parent.kind === SyntaxKind.ExportAssignment && (usage.parent as ExportAssignment).isExportEquals)) { - // export specifiers do not use the variable, they only make it available for use - return true; - } - // When resolving symbols for exports, the `usage` location passed in can be the export site directly - if (usage.kind === SyntaxKind.ExportAssignment && (usage as ExportAssignment).isExportEquals) { - return true; - } - - const container = getEnclosingBlockScopeContainer(declaration); - if (!!(usage.flags & NodeFlags.JSDoc) || isInTypeQuery(usage)) { - return true; - } - if (isUsedInFunctionOrInstanceProperty(usage, declaration, container)) { - if (compilerOptions.target === ScriptTarget.ESNext && !!compilerOptions.useDefineForClassFields) { - return (isPropertyDeclaration(declaration) || isParameterPropertyDeclaration(declaration, declaration.parent)) && - !isPropertyImmediatelyReferencedWithinDeclaration(declaration, usage, /*stopAtAnyPropertyDeclaration*/ true); - } - else { - return true; - } + /** stopAtAnyPropertyDeclaration is used for detecting ES-standard class field use-before-def errors */ + function isPropertyImmediatelyReferencedWithinDeclaration(declaration: PropertyDeclaration | ParameterPropertyDeclaration, usage: Node, stopAtAnyPropertyDeclaration: boolean) { + // always legal if usage is after declaration + if (usage.end > declaration.end) { + return false; } - return false; - - function isImmediatelyUsedInInitializerOfBlockScopedVariable(declaration: VariableDeclaration, usage: Node): boolean { - const container = getEnclosingBlockScopeContainer(declaration); - - switch (declaration.parent.parent.kind) { - case SyntaxKind.VariableStatement: - case SyntaxKind.ForStatement: - case SyntaxKind.ForOfStatement: - // variable statement/for/for-of statement case, - // use site should not be inside variable declaration (initializer of declaration or binding element) - if (isSameScopeDescendentOf(usage, declaration, container)) { - return true; - } - break; + // still might be legal if usage is deferred (e.g. x: any = () => this.x) + // otherwise illegal if immediately referenced within the declaration (e.g. x: any = this.x) + const ancestorChangingReferenceScope = findAncestor(usage, (node: Node) => { + if (node === declaration) { + return "quit"; } - - // ForIn/ForOf case - use site should not be used in expression part - const grandparent = declaration.parent.parent; - return isForInOrOfStatement(grandparent) && isSameScopeDescendentOf(usage, grandparent.expression, container); - } - - function isUsedInFunctionOrInstanceProperty(usage: Node, declaration: Node, container?: Node): boolean { - return !!findAncestor(usage, current => { - if (current === container) { - return "quit"; - } - if (isFunctionLike(current)) { + switch (node.kind) { + case SyntaxKind.ArrowFunction: return true; - } - - const initializerOfProperty = current.parent && - current.parent.kind === SyntaxKind.PropertyDeclaration && - (current.parent).initializer === current; - - if (initializerOfProperty) { - if (hasModifier(current.parent, ModifierFlags.Static)) { - if (declaration.kind === SyntaxKind.MethodDeclaration) { + case SyntaxKind.PropertyDeclaration: + // even when stopping at any property declaration, they need to come from the same class + return stopAtAnyPropertyDeclaration && + (isPropertyDeclaration(declaration) && node.parent === declaration.parent + || isParameterPropertyDeclaration(declaration, declaration.parent) && node.parent === declaration.parent.parent) + ? "quit" : true; + case SyntaxKind.Block: + switch (node.parent.kind) { + case SyntaxKind.GetAccessor: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.SetAccessor: return true; - } + default: + return false; } - else { - const isDeclarationInstanceProperty = declaration.kind === SyntaxKind.PropertyDeclaration && !hasModifier(declaration, ModifierFlags.Static); - if (!isDeclarationInstanceProperty || getContainingClass(usage) !== getContainingClass(declaration)) { - return true; + default: + return false; + } + }); + return ancestorChangingReferenceScope === undefined; + } + } + /** + * Resolve a given name for a given meaning at a given location. An error is reported if the name was not found and + * the nameNotFoundMessage argument is not undefined. Returns the resolved symbol, or undefined if no symbol with + * the given name can be found. + * + * @param isUse If true, this will count towards --noUnusedLocals / --noUnusedParameters. + */ + function resolveName(location: Node | undefined, name: __String, meaning: SymbolFlags, nameNotFoundMessage: DiagnosticMessage | undefined, nameArg: __String | Identifier | undefined, isUse: boolean, excludeGlobals = false, suggestedNameNotFoundMessage?: DiagnosticMessage): ts.Symbol | undefined { + return resolveNameHelper(location, name, meaning, nameNotFoundMessage, nameArg, isUse, excludeGlobals, getSymbol, suggestedNameNotFoundMessage); + } + function resolveNameHelper(location: Node | undefined, name: __String, meaning: SymbolFlags, nameNotFoundMessage: DiagnosticMessage | undefined, nameArg: __String | Identifier | undefined, isUse: boolean, excludeGlobals: boolean, lookup: typeof getSymbol, suggestedNameNotFoundMessage?: DiagnosticMessage): ts.Symbol | undefined { + const originalLocation = location; // needed for did-you-mean error reporting, which gathers candidates starting from the original location + let result: ts.Symbol | undefined; + let lastLocation: Node | undefined; + let lastSelfReferenceLocation: Node | undefined; + let propertyWithInvalidInitializer: Node | undefined; + let associatedDeclarationForContainingInitializer: ParameterDeclaration | BindingElement | undefined; + let withinDeferredContext = false; + const errorLocation = location; + let grandparent: Node; + let isInExternalModule = false; + loop: while (location) { + // Locals of a source file are not in scope (because they get merged into the global symbol table) + if (location.locals && !isGlobalSourceFile(location)) { + if (result = lookup(location.locals, name, meaning)) { + let useResult = true; + if (isFunctionLike(location) && lastLocation && lastLocation !== (location).body) { + // symbol lookup restrictions for function-like declarations + // - Type parameters of a function are in scope in the entire function declaration, including the parameter + // list and return type. However, local types are only in scope in the function body. + // - parameters are only in the scope of function body + // This restriction does not apply to JSDoc comment types because they are parented + // at a higher level than type parameters would normally be + if (meaning & result.flags & SymbolFlags.Type && lastLocation.kind !== SyntaxKind.JSDocComment) { + useResult = result.flags & SymbolFlags.TypeParameter + // type parameters are visible in parameter list, return type and type parameter list + ? lastLocation === (location).type || + lastLocation.kind === SyntaxKind.Parameter || + lastLocation.kind === SyntaxKind.TypeParameter + // local types not visible outside the function body + : false; + } + if (meaning & result.flags & SymbolFlags.Variable) { + // expression inside parameter will lookup as normal variable scope when targeting es2015+ + const functionLocation = (location); + if (compilerOptions.target && compilerOptions.target >= ScriptTarget.ES2015 && isParameter(lastLocation) && + functionLocation.body && result.valueDeclaration.pos >= functionLocation.body.pos && result.valueDeclaration.end <= functionLocation.body.end) { + useResult = false; + } + else if (result.flags & SymbolFlags.FunctionScopedVariable) { + // parameters are visible only inside function body, parameter list and return type + // technically for parameter list case here we might mix parameters and variables declared in function, + // however it is detected separately when checking initializers of parameters + // to make sure that they reference no variables declared after them. + useResult = + lastLocation.kind === SyntaxKind.Parameter || + (lastLocation === (location).type && + !!findAncestor(result.valueDeclaration, isParameter)); } } } - return false; - }); - } - - /** stopAtAnyPropertyDeclaration is used for detecting ES-standard class field use-before-def errors */ - function isPropertyImmediatelyReferencedWithinDeclaration(declaration: PropertyDeclaration | ParameterPropertyDeclaration, usage: Node, stopAtAnyPropertyDeclaration: boolean) { - // always legal if usage is after declaration - if (usage.end > declaration.end) { - return false; - } - - // still might be legal if usage is deferred (e.g. x: any = () => this.x) - // otherwise illegal if immediately referenced within the declaration (e.g. x: any = this.x) - const ancestorChangingReferenceScope = findAncestor(usage, (node: Node) => { - if (node === declaration) { - return "quit"; + else if (location.kind === SyntaxKind.ConditionalType) { + // A type parameter declared using 'infer T' in a conditional type is visible only in + // the true branch of the conditional type. + useResult = lastLocation === (location).trueType; } - - switch (node.kind) { - case SyntaxKind.ArrowFunction: - return true; - case SyntaxKind.PropertyDeclaration: - // even when stopping at any property declaration, they need to come from the same class - return stopAtAnyPropertyDeclaration && - (isPropertyDeclaration(declaration) && node.parent === declaration.parent - || isParameterPropertyDeclaration(declaration, declaration.parent) && node.parent === declaration.parent.parent) - ? "quit": true; - case SyntaxKind.Block: - switch (node.parent.kind) { - case SyntaxKind.GetAccessor: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.SetAccessor: - return true; - default: - return false; - } - default: - return false; + if (useResult) { + break loop; } - }); - - return ancestorChangingReferenceScope === undefined; + else { + result = undefined; + } + } } - } - - /** - * Resolve a given name for a given meaning at a given location. An error is reported if the name was not found and - * the nameNotFoundMessage argument is not undefined. Returns the resolved symbol, or undefined if no symbol with - * the given name can be found. - * - * @param isUse If true, this will count towards --noUnusedLocals / --noUnusedParameters. - */ - function resolveName( - location: Node | undefined, - name: __String, - meaning: SymbolFlags, - nameNotFoundMessage: DiagnosticMessage | undefined, - nameArg: __String | Identifier | undefined, - isUse: boolean, - excludeGlobals = false, - suggestedNameNotFoundMessage?: DiagnosticMessage): Symbol | undefined { - return resolveNameHelper(location, name, meaning, nameNotFoundMessage, nameArg, isUse, excludeGlobals, getSymbol, suggestedNameNotFoundMessage); - } - - function resolveNameHelper( - location: Node | undefined, - name: __String, - meaning: SymbolFlags, - nameNotFoundMessage: DiagnosticMessage | undefined, - nameArg: __String | Identifier | undefined, - isUse: boolean, - excludeGlobals: boolean, - lookup: typeof getSymbol, - suggestedNameNotFoundMessage?: DiagnosticMessage): Symbol | undefined { - const originalLocation = location; // needed for did-you-mean error reporting, which gathers candidates starting from the original location - let result: Symbol | undefined; - let lastLocation: Node | undefined; - let lastSelfReferenceLocation: Node | undefined; - let propertyWithInvalidInitializer: Node | undefined; - let associatedDeclarationForContainingInitializer: ParameterDeclaration | BindingElement | undefined; - let withinDeferredContext = false; - const errorLocation = location; - let grandparent: Node; - let isInExternalModule = false; - - loop: while (location) { - // Locals of a source file are not in scope (because they get merged into the global symbol table) - if (location.locals && !isGlobalSourceFile(location)) { - if (result = lookup(location.locals, name, meaning)) { - let useResult = true; - if (isFunctionLike(location) && lastLocation && lastLocation !== (location).body) { - // symbol lookup restrictions for function-like declarations - // - Type parameters of a function are in scope in the entire function declaration, including the parameter - // list and return type. However, local types are only in scope in the function body. - // - parameters are only in the scope of function body - // This restriction does not apply to JSDoc comment types because they are parented - // at a higher level than type parameters would normally be - if (meaning & result.flags & SymbolFlags.Type && lastLocation.kind !== SyntaxKind.JSDocComment) { - useResult = result.flags & SymbolFlags.TypeParameter - // type parameters are visible in parameter list, return type and type parameter list - ? lastLocation === (location).type || - lastLocation.kind === SyntaxKind.Parameter || - lastLocation.kind === SyntaxKind.TypeParameter - // local types not visible outside the function body - : false; - } - if (meaning & result.flags & SymbolFlags.Variable) { - // expression inside parameter will lookup as normal variable scope when targeting es2015+ - const functionLocation = location; - if (compilerOptions.target && compilerOptions.target >= ScriptTarget.ES2015 && isParameter(lastLocation) && - functionLocation.body && result.valueDeclaration.pos >= functionLocation.body.pos && result.valueDeclaration.end <= functionLocation.body.end) { - useResult = false; - } - else if (result.flags & SymbolFlags.FunctionScopedVariable) { - // parameters are visible only inside function body, parameter list and return type - // technically for parameter list case here we might mix parameters and variables declared in function, - // however it is detected separately when checking initializers of parameters - // to make sure that they reference no variables declared after them. - useResult = - lastLocation.kind === SyntaxKind.Parameter || - ( - lastLocation === (location).type && - !!findAncestor(result.valueDeclaration, isParameter) - ); - } + withinDeferredContext = withinDeferredContext || getIsDeferredContext(location, lastLocation); + switch (location.kind) { + case SyntaxKind.SourceFile: + if (!isExternalOrCommonJsModule((location))) + break; + isInExternalModule = true; + // falls through + case SyntaxKind.ModuleDeclaration: + const moduleExports = getSymbolOfNode((location as SourceFile | ModuleDeclaration)).exports || emptySymbols; + if (location.kind === SyntaxKind.SourceFile || (isModuleDeclaration(location) && location.flags & NodeFlags.Ambient && !isGlobalScopeAugmentation(location))) { + // It's an external module. First see if the module has an export default and if the local + // name of that export default matches. + if (result = moduleExports.get(InternalSymbolName.Default)) { + const localSymbol = getLocalSymbolForExportDefault(result); + if (localSymbol && (result.flags & meaning) && localSymbol.escapedName === name) { + break loop; } + result = undefined; } - else if (location.kind === SyntaxKind.ConditionalType) { - // A type parameter declared using 'infer T' in a conditional type is visible only in - // the true branch of the conditional type. - useResult = lastLocation === (location).trueType; + // Because of module/namespace merging, a module's exports are in scope, + // yet we never want to treat an export specifier as putting a member in scope. + // Therefore, if the name we find is purely an export specifier, it is not actually considered in scope. + // Two things to note about this: + // 1. We have to check this without calling getSymbol. The problem with calling getSymbol + // on an export specifier is that it might find the export specifier itself, and try to + // resolve it as an alias. This will cause the checker to consider the export specifier + // a circular alias reference when it might not be. + // 2. We check === SymbolFlags.Alias in order to check that the symbol is *purely* + // an alias. If we used &, we'd be throwing out symbols that have non alias aspects, + // which is not the desired behavior. + const moduleExport = moduleExports.get(name); + if (moduleExport && + moduleExport.flags === SymbolFlags.Alias && + (getDeclarationOfKind(moduleExport, SyntaxKind.ExportSpecifier) || getDeclarationOfKind(moduleExport, SyntaxKind.NamespaceExport))) { + break; } - - if (useResult) { - break loop; + } + // ES6 exports are also visible locally (except for 'default'), but commonjs exports are not (except typedefs) + if (name !== InternalSymbolName.Default && (result = lookup(moduleExports, name, meaning & SymbolFlags.ModuleMember))) { + if (isSourceFile(location) && location.commonJsModuleIndicator && !result.declarations.some(isJSDocTypeAlias)) { + result = undefined; } else { - result = undefined; + break loop; } } - } - withinDeferredContext = withinDeferredContext || getIsDeferredContext(location, lastLocation); - switch (location.kind) { - case SyntaxKind.SourceFile: - if (!isExternalOrCommonJsModule(location)) break; - isInExternalModule = true; - // falls through - case SyntaxKind.ModuleDeclaration: - const moduleExports = getSymbolOfNode(location as SourceFile | ModuleDeclaration).exports || emptySymbols; - if (location.kind === SyntaxKind.SourceFile || (isModuleDeclaration(location) && location.flags & NodeFlags.Ambient && !isGlobalScopeAugmentation(location))) { - - // It's an external module. First see if the module has an export default and if the local - // name of that export default matches. - if (result = moduleExports.get(InternalSymbolName.Default)) { - const localSymbol = getLocalSymbolForExportDefault(result); - if (localSymbol && (result.flags & meaning) && localSymbol.escapedName === name) { - break loop; - } - result = undefined; - } - - // Because of module/namespace merging, a module's exports are in scope, - // yet we never want to treat an export specifier as putting a member in scope. - // Therefore, if the name we find is purely an export specifier, it is not actually considered in scope. - // Two things to note about this: - // 1. We have to check this without calling getSymbol. The problem with calling getSymbol - // on an export specifier is that it might find the export specifier itself, and try to - // resolve it as an alias. This will cause the checker to consider the export specifier - // a circular alias reference when it might not be. - // 2. We check === SymbolFlags.Alias in order to check that the symbol is *purely* - // an alias. If we used &, we'd be throwing out symbols that have non alias aspects, - // which is not the desired behavior. - const moduleExport = moduleExports.get(name); - if (moduleExport && - moduleExport.flags === SymbolFlags.Alias && - (getDeclarationOfKind(moduleExport, SyntaxKind.ExportSpecifier) || getDeclarationOfKind(moduleExport, SyntaxKind.NamespaceExport))) { - break; - } - } - - // ES6 exports are also visible locally (except for 'default'), but commonjs exports are not (except typedefs) - if (name !== InternalSymbolName.Default && (result = lookup(moduleExports, name, meaning & SymbolFlags.ModuleMember))) { - if (isSourceFile(location) && location.commonJsModuleIndicator && !result.declarations.some(isJSDocTypeAlias)) { - result = undefined; - } - else { - break loop; + break; + case SyntaxKind.EnumDeclaration: + if (result = lookup((getSymbolOfNode(location)!.exports!), name, meaning & SymbolFlags.EnumMember)) { + break loop; + } + break; + case SyntaxKind.PropertyDeclaration: + // TypeScript 1.0 spec (April 2014): 8.4.1 + // Initializer expressions for instance member variables are evaluated in the scope + // of the class constructor body but are not permitted to reference parameters or + // local variables of the constructor. This effectively means that entities from outer scopes + // by the same name as a constructor parameter or local variable are inaccessible + // in initializer expressions for instance member variables. + if (!hasModifier(location, ModifierFlags.Static)) { + const ctor = findConstructorDeclaration((location.parent as ClassLikeDeclaration)); + if (ctor && ctor.locals) { + if (lookup(ctor.locals, name, meaning & SymbolFlags.Value)) { + // Remember the property node, it will be used later to report appropriate error + propertyWithInvalidInitializer = location; } } - break; - case SyntaxKind.EnumDeclaration: - if (result = lookup(getSymbolOfNode(location)!.exports!, name, meaning & SymbolFlags.EnumMember)) { - break loop; + } + break; + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ClassExpression: + case SyntaxKind.InterfaceDeclaration: + // The below is used to lookup type parameters within a class or interface, as they are added to the class/interface locals + // These can never be latebound, so the symbol's raw members are sufficient. `getMembersOfNode` cannot be used, as it would + // trigger resolving late-bound names, which we may already be in the process of doing while we're here! + if (result = lookup(getSymbolOfNode((location as ClassLikeDeclaration | InterfaceDeclaration)).members || emptySymbols, name, meaning & SymbolFlags.Type)) { + if (!isTypeParameterSymbolDeclaredInContainer(result, location)) { + // ignore type parameters not declared in this container + result = undefined; + break; } - break; - case SyntaxKind.PropertyDeclaration: - // TypeScript 1.0 spec (April 2014): 8.4.1 - // Initializer expressions for instance member variables are evaluated in the scope - // of the class constructor body but are not permitted to reference parameters or - // local variables of the constructor. This effectively means that entities from outer scopes - // by the same name as a constructor parameter or local variable are inaccessible - // in initializer expressions for instance member variables. - if (!hasModifier(location, ModifierFlags.Static)) { - const ctor = findConstructorDeclaration(location.parent as ClassLikeDeclaration); - if (ctor && ctor.locals) { - if (lookup(ctor.locals, name, meaning & SymbolFlags.Value)) { - // Remember the property node, it will be used later to report appropriate error - propertyWithInvalidInitializer = location; - } - } + if (lastLocation && hasModifier(lastLocation, ModifierFlags.Static)) { + // TypeScript 1.0 spec (April 2014): 3.4.1 + // The scope of a type parameter extends over the entire declaration with which the type + // parameter list is associated, with the exception of static member declarations in classes. + error(errorLocation, Diagnostics.Static_members_cannot_reference_class_type_parameters); + return undefined; } - break; - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ClassExpression: - case SyntaxKind.InterfaceDeclaration: - // The below is used to lookup type parameters within a class or interface, as they are added to the class/interface locals - // These can never be latebound, so the symbol's raw members are sufficient. `getMembersOfNode` cannot be used, as it would - // trigger resolving late-bound names, which we may already be in the process of doing while we're here! - if (result = lookup(getSymbolOfNode(location as ClassLikeDeclaration | InterfaceDeclaration).members || emptySymbols, name, meaning & SymbolFlags.Type)) { - if (!isTypeParameterSymbolDeclaredInContainer(result, location)) { - // ignore type parameters not declared in this container - result = undefined; - break; - } - if (lastLocation && hasModifier(lastLocation, ModifierFlags.Static)) { - // TypeScript 1.0 spec (April 2014): 3.4.1 - // The scope of a type parameter extends over the entire declaration with which the type - // parameter list is associated, with the exception of static member declarations in classes. - error(errorLocation, Diagnostics.Static_members_cannot_reference_class_type_parameters); - return undefined; - } + break loop; + } + if (location.kind === SyntaxKind.ClassExpression && meaning & SymbolFlags.Class) { + const className = (location).name; + if (className && name === className.escapedText) { + result = location.symbol; break loop; } - if (location.kind === SyntaxKind.ClassExpression && meaning & SymbolFlags.Class) { - const className = (location).name; - if (className && name === className.escapedText) { - result = location.symbol; - break loop; + } + break; + case SyntaxKind.ExpressionWithTypeArguments: + // The type parameters of a class are not in scope in the base class expression. + if (lastLocation === (location).expression && (location.parent).token === SyntaxKind.ExtendsKeyword) { + const container = location.parent.parent; + if (isClassLike(container) && (result = lookup((getSymbolOfNode(container).members!), name, meaning & SymbolFlags.Type))) { + if (nameNotFoundMessage) { + error(errorLocation, Diagnostics.Base_class_expressions_cannot_reference_class_type_parameters); } + return undefined; } - break; - case SyntaxKind.ExpressionWithTypeArguments: - // The type parameters of a class are not in scope in the base class expression. - if (lastLocation === (location).expression && (location.parent).token === SyntaxKind.ExtendsKeyword) { - const container = location.parent.parent; - if (isClassLike(container) && (result = lookup(getSymbolOfNode(container).members!, name, meaning & SymbolFlags.Type))) { - if (nameNotFoundMessage) { - error(errorLocation, Diagnostics.Base_class_expressions_cannot_reference_class_type_parameters); - } - return undefined; - } + } + break; + // It is not legal to reference a class's own type parameters from a computed property name that + // belongs to the class. For example: + // + // function foo() { return '' } + // class C { // <-- Class's own type parameter T + // [foo()]() { } // <-- Reference to T from class's own computed property + // } + // + case SyntaxKind.ComputedPropertyName: + grandparent = location.parent.parent; + if (isClassLike(grandparent) || grandparent.kind === SyntaxKind.InterfaceDeclaration) { + // A reference to this grandparent's type parameters would be an error + if (result = lookup((getSymbolOfNode((grandparent as ClassLikeDeclaration | InterfaceDeclaration)).members!), name, meaning & SymbolFlags.Type)) { + error(errorLocation, Diagnostics.A_computed_property_name_cannot_reference_a_type_parameter_from_its_containing_type); + return undefined; } + } + break; + case SyntaxKind.ArrowFunction: + // when targeting ES6 or higher there is no 'arguments' in an arrow function + // for lower compile targets the resolved symbol is used to emit an error + if ((compilerOptions.target!) >= ScriptTarget.ES2015) { break; - // It is not legal to reference a class's own type parameters from a computed property name that - // belongs to the class. For example: + } + // falls through + case SyntaxKind.MethodDeclaration: + case SyntaxKind.Constructor: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + case SyntaxKind.FunctionDeclaration: + if (meaning & SymbolFlags.Variable && name === "arguments") { + result = argumentsSymbol; + break loop; + } + break; + case SyntaxKind.FunctionExpression: + if (meaning & SymbolFlags.Variable && name === "arguments") { + result = argumentsSymbol; + break loop; + } + if (meaning & SymbolFlags.Function) { + const functionName = (location).name; + if (functionName && name === functionName.escapedText) { + result = location.symbol; + break loop; + } + } + break; + case SyntaxKind.Decorator: + // Decorators are resolved at the class declaration. Resolving at the parameter + // or member would result in looking up locals in the method. // - // function foo() { return '' } - // class C { // <-- Class's own type parameter T - // [foo()]() { } // <-- Reference to T from class's own computed property + // function y() {} + // class C { + // method(@y x, y) {} // <-- decorator y should be resolved at the class declaration, not the parameter. // } // - case SyntaxKind.ComputedPropertyName: - grandparent = location.parent.parent; - if (isClassLike(grandparent) || grandparent.kind === SyntaxKind.InterfaceDeclaration) { - // A reference to this grandparent's type parameters would be an error - if (result = lookup(getSymbolOfNode(grandparent as ClassLikeDeclaration | InterfaceDeclaration).members!, name, meaning & SymbolFlags.Type)) { - error(errorLocation, Diagnostics.A_computed_property_name_cannot_reference_a_type_parameter_from_its_containing_type); - return undefined; - } - } - break; - case SyntaxKind.ArrowFunction: - // when targeting ES6 or higher there is no 'arguments' in an arrow function - // for lower compile targets the resolved symbol is used to emit an error - if (compilerOptions.target! >= ScriptTarget.ES2015) { - break; - } - // falls through - case SyntaxKind.MethodDeclaration: - case SyntaxKind.Constructor: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - case SyntaxKind.FunctionDeclaration: - if (meaning & SymbolFlags.Variable && name === "arguments") { - result = argumentsSymbol; - break loop; - } - break; - case SyntaxKind.FunctionExpression: - if (meaning & SymbolFlags.Variable && name === "arguments") { - result = argumentsSymbol; - break loop; - } - - if (meaning & SymbolFlags.Function) { - const functionName = (location).name; - if (functionName && name === functionName.escapedText) { - result = location.symbol; - break loop; - } - } - break; - case SyntaxKind.Decorator: - // Decorators are resolved at the class declaration. Resolving at the parameter - // or member would result in looking up locals in the method. - // - // function y() {} - // class C { - // method(@y x, y) {} // <-- decorator y should be resolved at the class declaration, not the parameter. - // } - // - if (location.parent && location.parent.kind === SyntaxKind.Parameter) { - location = location.parent; - } - // - // function y() {} - // class C { - // @y method(x, y) {} // <-- decorator y should be resolved at the class declaration, not the method. - // } - // - - // class Decorators are resolved outside of the class to avoid referencing type parameters of that class. - // - // type T = number; - // declare function y(x: T): any; - // @param(1 as T) // <-- T should resolve to the type alias outside of class C - // class C {} - if (location.parent && (isClassElement(location.parent) || location.parent.kind === SyntaxKind.ClassDeclaration)) { - location = location.parent; - } - break; - case SyntaxKind.JSDocTypedefTag: - case SyntaxKind.JSDocCallbackTag: - case SyntaxKind.JSDocEnumTag: - // js type aliases do not resolve names from their host, so skip past it - location = getJSDocHost(location); - break; - case SyntaxKind.Parameter: - if (lastLocation && lastLocation === (location as ParameterDeclaration).initializer) { - associatedDeclarationForContainingInitializer = location as ParameterDeclaration; - } - break; - case SyntaxKind.BindingElement: - if (lastLocation && lastLocation === (location as BindingElement).initializer) { - const root = getRootDeclaration(location); - if (root.kind === SyntaxKind.Parameter) { - associatedDeclarationForContainingInitializer = location as BindingElement; - } + if (location.parent && location.parent.kind === SyntaxKind.Parameter) { + location = location.parent; + } + // + // function y() {} + // class C { + // @y method(x, y) {} // <-- decorator y should be resolved at the class declaration, not the method. + // } + // + // class Decorators are resolved outside of the class to avoid referencing type parameters of that class. + // + // type T = number; + // declare function y(x: T): any; + // @param(1 as T) // <-- T should resolve to the type alias outside of class C + // class C {} + if (location.parent && (isClassElement(location.parent) || location.parent.kind === SyntaxKind.ClassDeclaration)) { + location = location.parent; + } + break; + case SyntaxKind.JSDocTypedefTag: + case SyntaxKind.JSDocCallbackTag: + case SyntaxKind.JSDocEnumTag: + // js type aliases do not resolve names from their host, so skip past it + location = getJSDocHost(location); + break; + case SyntaxKind.Parameter: + if (lastLocation && lastLocation === (location as ParameterDeclaration).initializer) { + associatedDeclarationForContainingInitializer = (location as ParameterDeclaration); + } + break; + case SyntaxKind.BindingElement: + if (lastLocation && lastLocation === (location as BindingElement).initializer) { + const root = getRootDeclaration(location); + if (root.kind === SyntaxKind.Parameter) { + associatedDeclarationForContainingInitializer = (location as BindingElement); } - break; - } - if (isSelfReferenceLocation(location)) { - lastSelfReferenceLocation = location; - } - lastLocation = location; - location = location.parent; + } + break; } - - // We just climbed up parents looking for the name, meaning that we started in a descendant node of `lastLocation`. - // If `result === lastSelfReferenceLocation.symbol`, that means that we are somewhere inside `lastSelfReferenceLocation` looking up a name, and resolving to `lastLocation` itself. - // That means that this is a self-reference of `lastLocation`, and shouldn't count this when considering whether `lastLocation` is used. - if (isUse && result && (!lastSelfReferenceLocation || result !== lastSelfReferenceLocation.symbol)) { - result.isReferenced! |= meaning; + if (isSelfReferenceLocation(location)) { + lastSelfReferenceLocation = location; } - - if (!result) { - if (lastLocation) { - Debug.assert(lastLocation.kind === SyntaxKind.SourceFile); - if ((lastLocation as SourceFile).commonJsModuleIndicator && name === "exports" && meaning & lastLocation.symbol.flags) { - return lastLocation.symbol; - } - } - - if (!excludeGlobals) { - result = lookup(globals, name, meaning); + lastLocation = location; + location = location.parent; + } + // We just climbed up parents looking for the name, meaning that we started in a descendant node of `lastLocation`. + // If `result === lastSelfReferenceLocation.symbol`, that means that we are somewhere inside `lastSelfReferenceLocation` looking up a name, and resolving to `lastLocation` itself. + // That means that this is a self-reference of `lastLocation`, and shouldn't count this when considering whether `lastLocation` is used. + if (isUse && result && (!lastSelfReferenceLocation || result !== lastSelfReferenceLocation.symbol)) { + result.isReferenced! |= meaning; + } + if (!result) { + if (lastLocation) { + Debug.assert(lastLocation.kind === SyntaxKind.SourceFile); + if ((lastLocation as SourceFile).commonJsModuleIndicator && name === "exports" && meaning & lastLocation.symbol.flags) { + return lastLocation.symbol; } } - if (!result) { - if (originalLocation && isInJSFile(originalLocation) && originalLocation.parent) { - if (isRequireCall(originalLocation.parent, /*checkArgumentIsStringLiteralLike*/ false)) { - return requireSymbol; - } + if (!excludeGlobals) { + result = lookup(globals, name, meaning); + } + } + if (!result) { + if (originalLocation && isInJSFile(originalLocation) && originalLocation.parent) { + if (isRequireCall(originalLocation.parent, /*checkArgumentIsStringLiteralLike*/ false)) { + return requireSymbol; } } - if (!result) { - if (nameNotFoundMessage) { - if (!errorLocation || - !checkAndReportErrorForMissingPrefix(errorLocation, name, nameArg!) && // TODO: GH#18217 + } + if (!result) { + if (nameNotFoundMessage) { + if (!errorLocation || + !checkAndReportErrorForMissingPrefix(errorLocation, name, nameArg!) && // TODO: GH#18217 !checkAndReportErrorForExtendingInterface(errorLocation) && !checkAndReportErrorForUsingTypeAsNamespace(errorLocation, name, meaning) && !checkAndReportErrorForExportingPrimitiveType(errorLocation, name) && !checkAndReportErrorForUsingTypeAsValue(errorLocation, name, meaning) && !checkAndReportErrorForUsingNamespaceModuleAsValue(errorLocation, name, meaning) && !checkAndReportErrorForUsingValueAsType(errorLocation, name, meaning)) { - let suggestion: Symbol | undefined; - if (suggestedNameNotFoundMessage && suggestionCount < maximumSuggestionCount) { - suggestion = getSuggestedSymbolForNonexistentSymbol(originalLocation, name, meaning); - if (suggestion) { - const suggestionName = symbolToString(suggestion); - const diagnostic = error(errorLocation, suggestedNameNotFoundMessage, diagnosticName(nameArg!), suggestionName); - if (suggestion.valueDeclaration) { - addRelatedInfo( - diagnostic, - createDiagnosticForNode(suggestion.valueDeclaration, Diagnostics._0_is_declared_here, suggestionName) - ); - } + let suggestion: ts.Symbol | undefined; + if (suggestedNameNotFoundMessage && suggestionCount < maximumSuggestionCount) { + suggestion = getSuggestedSymbolForNonexistentSymbol(originalLocation, name, meaning); + if (suggestion) { + const suggestionName = symbolToString(suggestion); + const diagnostic = error(errorLocation, suggestedNameNotFoundMessage, diagnosticName(nameArg!), suggestionName); + if (suggestion.valueDeclaration) { + addRelatedInfo(diagnostic, createDiagnosticForNode(suggestion.valueDeclaration, Diagnostics._0_is_declared_here, suggestionName)); } } - if (!suggestion) { - error(errorLocation, nameNotFoundMessage, diagnosticName(nameArg!)); - } - suggestionCount++; - } - } - return undefined; - } - - // Perform extra checks only if error reporting was requested - if (nameNotFoundMessage) { - if (propertyWithInvalidInitializer && !(compilerOptions.target === ScriptTarget.ESNext && compilerOptions.useDefineForClassFields)) { - // We have a match, but the reference occurred within a property initializer and the identifier also binds - // to a local variable in the constructor where the code will be emitted. Note that this is actually allowed - // with ESNext+useDefineForClassFields because the scope semantics are different. - const propertyName = (propertyWithInvalidInitializer).name; - error(errorLocation, Diagnostics.Initializer_of_instance_member_variable_0_cannot_reference_identifier_1_declared_in_the_constructor, - declarationNameToString(propertyName), diagnosticName(nameArg!)); - return undefined; - } - - // Only check for block-scoped variable if we have an error location and are looking for the - // name with variable meaning - // For example, - // declare module foo { - // interface bar {} - // } - // const foo/*1*/: foo/*2*/.bar; - // The foo at /*1*/ and /*2*/ will share same symbol with two meanings: - // block-scoped variable and namespace module. However, only when we - // try to resolve name in /*1*/ which is used in variable position, - // we want to check for block-scoped - if (errorLocation && - (meaning & SymbolFlags.BlockScopedVariable || - ((meaning & SymbolFlags.Class || meaning & SymbolFlags.Enum) && (meaning & SymbolFlags.Value) === SymbolFlags.Value))) { - const exportOrLocalSymbol = getExportSymbolOfValueSymbolIfExported(result); - if (exportOrLocalSymbol.flags & SymbolFlags.BlockScopedVariable || exportOrLocalSymbol.flags & SymbolFlags.Class || exportOrLocalSymbol.flags & SymbolFlags.Enum) { - checkResolvedBlockScopedVariable(exportOrLocalSymbol, errorLocation); } - } - - // If we're in an external module, we can't reference value symbols created from UMD export declarations - if (result && isInExternalModule && (meaning & SymbolFlags.Value) === SymbolFlags.Value && !(originalLocation!.flags & NodeFlags.JSDoc)) { - const merged = getMergedSymbol(result); - if (length(merged.declarations) && every(merged.declarations, d => isNamespaceExportDeclaration(d) || isSourceFile(d) && !!d.symbol.globalExports)) { - errorOrSuggestion(!compilerOptions.allowUmdGlobalAccess, errorLocation!, Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead, unescapeLeadingUnderscores(name)); - } - } - - // If we're in a parameter initializer, we can't reference the values of the parameter whose initializer we're within or parameters to the right - if (result && associatedDeclarationForContainingInitializer && !withinDeferredContext && (meaning & SymbolFlags.Value) === SymbolFlags.Value) { - const candidate = getMergedSymbol(getLateBoundSymbol(result)); - const root = (getRootDeclaration(associatedDeclarationForContainingInitializer) as ParameterDeclaration); - // A parameter initializer or binding pattern initializer within a parameter cannot refer to itself - if (candidate === getSymbolOfNode(associatedDeclarationForContainingInitializer)) { - error(errorLocation, Diagnostics.Parameter_0_cannot_be_referenced_in_its_initializer, declarationNameToString(associatedDeclarationForContainingInitializer.name)); - } - // And it cannot refer to any declarations which come after it - else if (candidate.valueDeclaration && candidate.valueDeclaration.pos > associatedDeclarationForContainingInitializer.pos && root.parent.locals && lookup(root.parent.locals, candidate.escapedName, meaning) === candidate) { - error(errorLocation, Diagnostics.Initializer_of_parameter_0_cannot_reference_identifier_1_declared_after_it, declarationNameToString(associatedDeclarationForContainingInitializer.name), declarationNameToString(errorLocation)); + if (!suggestion) { + error(errorLocation, nameNotFoundMessage, diagnosticName(nameArg!)); } - } - if (result && errorLocation && meaning & SymbolFlags.Value && result.flags & SymbolFlags.Alias) { - checkSymbolUsageInExpressionContext(result, name, errorLocation); + suggestionCount++; } } - return result; + return undefined; } - - function checkSymbolUsageInExpressionContext(symbol: Symbol, name: __String, useSite: Node) { - if (!isValidTypeOnlyAliasUseSite(useSite)) { - const typeOnlyDeclaration = getTypeOnlyAliasDeclaration(symbol); - if (typeOnlyDeclaration) { - const isExport = typeOnlyDeclarationIsExport(typeOnlyDeclaration); - const message = isExport - ? Diagnostics._0_cannot_be_used_as_a_value_because_it_was_exported_using_export_type - : Diagnostics._0_cannot_be_used_as_a_value_because_it_was_imported_using_import_type; - const relatedMessage = isExport - ? Diagnostics._0_was_exported_here - : Diagnostics._0_was_imported_here; - const unescapedName = unescapeLeadingUnderscores(name); - addRelatedInfo( - error(useSite, message, unescapedName), - createDiagnosticForNode(typeOnlyDeclaration, relatedMessage, unescapedName)); - } + // Perform extra checks only if error reporting was requested + if (nameNotFoundMessage) { + if (propertyWithInvalidInitializer && !(compilerOptions.target === ScriptTarget.ESNext && compilerOptions.useDefineForClassFields)) { + // We have a match, but the reference occurred within a property initializer and the identifier also binds + // to a local variable in the constructor where the code will be emitted. Note that this is actually allowed + // with ESNext+useDefineForClassFields because the scope semantics are different. + const propertyName = (propertyWithInvalidInitializer).name; + error(errorLocation, Diagnostics.Initializer_of_instance_member_variable_0_cannot_reference_identifier_1_declared_in_the_constructor, declarationNameToString(propertyName), diagnosticName(nameArg!)); + return undefined; } - } - - function getIsDeferredContext(location: Node, lastLocation: Node | undefined): boolean { - if (location.kind !== SyntaxKind.ArrowFunction && location.kind !== SyntaxKind.FunctionExpression) { - // initializers in instance property declaration of class like entities are executed in constructor and thus deferred - return isTypeQueryNode(location) || (( - isFunctionLikeDeclaration(location) || - (location.kind === SyntaxKind.PropertyDeclaration && !hasModifier(location, ModifierFlags.Static)) - ) && (!lastLocation || lastLocation !== (location as FunctionLike | PropertyDeclaration).name)); // A name is evaluated within the enclosing scope - so it shouldn't count as deferred - } - if (lastLocation && lastLocation === (location as FunctionExpression | ArrowFunction).name) { - return false; + // Only check for block-scoped variable if we have an error location and are looking for the + // name with variable meaning + // For example, + // declare module foo { + // interface bar {} + // } + // const foo/*1*/: foo/*2*/.bar; + // The foo at /*1*/ and /*2*/ will share same symbol with two meanings: + // block-scoped variable and namespace module. However, only when we + // try to resolve name in /*1*/ which is used in variable position, + // we want to check for block-scoped + if (errorLocation && + (meaning & SymbolFlags.BlockScopedVariable || + ((meaning & SymbolFlags.Class || meaning & SymbolFlags.Enum) && (meaning & SymbolFlags.Value) === SymbolFlags.Value))) { + const exportOrLocalSymbol = getExportSymbolOfValueSymbolIfExported(result); + if (exportOrLocalSymbol.flags & SymbolFlags.BlockScopedVariable || exportOrLocalSymbol.flags & SymbolFlags.Class || exportOrLocalSymbol.flags & SymbolFlags.Enum) { + checkResolvedBlockScopedVariable(exportOrLocalSymbol, errorLocation); + } + } + // If we're in an external module, we can't reference value symbols created from UMD export declarations + if (result && isInExternalModule && (meaning & SymbolFlags.Value) === SymbolFlags.Value && !(originalLocation!.flags & NodeFlags.JSDoc)) { + const merged = getMergedSymbol(result); + if (length(merged.declarations) && every(merged.declarations, d => isNamespaceExportDeclaration(d) || isSourceFile(d) && !!d.symbol.globalExports)) { + errorOrSuggestion(!compilerOptions.allowUmdGlobalAccess, (errorLocation!), Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead, unescapeLeadingUnderscores(name)); + } + } + // If we're in a parameter initializer, we can't reference the values of the parameter whose initializer we're within or parameters to the right + if (result && associatedDeclarationForContainingInitializer && !withinDeferredContext && (meaning & SymbolFlags.Value) === SymbolFlags.Value) { + const candidate = getMergedSymbol(getLateBoundSymbol(result)); + const root = (getRootDeclaration(associatedDeclarationForContainingInitializer) as ParameterDeclaration); + // A parameter initializer or binding pattern initializer within a parameter cannot refer to itself + if (candidate === getSymbolOfNode(associatedDeclarationForContainingInitializer)) { + error(errorLocation, Diagnostics.Parameter_0_cannot_be_referenced_in_its_initializer, declarationNameToString(associatedDeclarationForContainingInitializer.name)); + } + // And it cannot refer to any declarations which come after it + else if (candidate.valueDeclaration && candidate.valueDeclaration.pos > associatedDeclarationForContainingInitializer.pos && root.parent.locals && lookup(root.parent.locals, candidate.escapedName, meaning) === candidate) { + error(errorLocation, Diagnostics.Initializer_of_parameter_0_cannot_reference_identifier_1_declared_after_it, declarationNameToString(associatedDeclarationForContainingInitializer.name), declarationNameToString((errorLocation))); + } + } + if (result && errorLocation && meaning & SymbolFlags.Value && result.flags & SymbolFlags.Alias) { + checkSymbolUsageInExpressionContext(result, name, errorLocation); + } + } + return result; + } + function checkSymbolUsageInExpressionContext(symbol: ts.Symbol, name: __String, useSite: Node) { + if (!isValidTypeOnlyAliasUseSite(useSite)) { + const typeOnlyDeclaration = getTypeOnlyAliasDeclaration(symbol); + if (typeOnlyDeclaration) { + const isExport = typeOnlyDeclarationIsExport(typeOnlyDeclaration); + const message = isExport + ? Diagnostics._0_cannot_be_used_as_a_value_because_it_was_exported_using_export_type + : Diagnostics._0_cannot_be_used_as_a_value_because_it_was_imported_using_import_type; + const relatedMessage = isExport + ? Diagnostics._0_was_exported_here + : Diagnostics._0_was_imported_here; + const unescapedName = unescapeLeadingUnderscores(name); + addRelatedInfo(error(useSite, message, unescapedName), createDiagnosticForNode(typeOnlyDeclaration, relatedMessage, unescapedName)); } - // generator functions and async functions are not inlined in control flow when immediately invoked - if ((location as FunctionExpression | ArrowFunction).asteriskToken || hasModifier(location, ModifierFlags.Async)) { + } + } + function getIsDeferredContext(location: Node, lastLocation: Node | undefined): boolean { + if (location.kind !== SyntaxKind.ArrowFunction && location.kind !== SyntaxKind.FunctionExpression) { + // initializers in instance property declaration of class like entities are executed in constructor and thus deferred + return isTypeQueryNode(location) || ((isFunctionLikeDeclaration(location) || + (location.kind === SyntaxKind.PropertyDeclaration && !hasModifier(location, ModifierFlags.Static))) && (!lastLocation || lastLocation !== (location as FunctionLike | PropertyDeclaration).name)); // A name is evaluated within the enclosing scope - so it shouldn't count as deferred + } + if (lastLocation && lastLocation === (location as FunctionExpression | ArrowFunction).name) { + return false; + } + // generator functions and async functions are not inlined in control flow when immediately invoked + if ((location as FunctionExpression | ArrowFunction).asteriskToken || hasModifier(location, ModifierFlags.Async)) { + return true; + } + return !getImmediatelyInvokedFunctionExpression(location); + } + function isSelfReferenceLocation(node: Node): boolean { + switch (node.kind) { + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.ModuleDeclaration: // For `namespace N { N; }` return true; - } - return !getImmediatelyInvokedFunctionExpression(location); + default: + return false; } - - function isSelfReferenceLocation(node: Node): boolean { - switch (node.kind) { - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.ClassDeclaration: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.ModuleDeclaration: // For `namespace N { N; }` - return true; - default: - return false; + } + function diagnosticName(nameArg: __String | Identifier | PrivateIdentifier) { + return isString(nameArg) ? unescapeLeadingUnderscores((nameArg as __String)) : declarationNameToString((nameArg as Identifier)); + } + function isTypeParameterSymbolDeclaredInContainer(symbol: ts.Symbol, container: Node) { + for (const decl of symbol.declarations) { + if (decl.kind === SyntaxKind.TypeParameter) { + const parent = isJSDocTemplateTag(decl.parent) ? getJSDocHost(decl.parent) : decl.parent; + if (parent === container) { + return !(isJSDocTemplateTag(decl.parent) && find(((decl.parent.parent as JSDoc).tags!), isJSDocTypeAlias)); // TODO: GH#18217 + } } } - - function diagnosticName(nameArg: __String | Identifier | PrivateIdentifier) { - return isString(nameArg) ? unescapeLeadingUnderscores(nameArg as __String) : declarationNameToString(nameArg as Identifier); + return false; + } + function checkAndReportErrorForMissingPrefix(errorLocation: Node, name: __String, nameArg: __String | Identifier): boolean { + if (!isIdentifier(errorLocation) || errorLocation.escapedText !== name || isTypeReferenceIdentifier(errorLocation) || isInTypeQuery(errorLocation)) { + return false; } - - function isTypeParameterSymbolDeclaredInContainer(symbol: Symbol, container: Node) { - for (const decl of symbol.declarations) { - if (decl.kind === SyntaxKind.TypeParameter) { - const parent = isJSDocTemplateTag(decl.parent) ? getJSDocHost(decl.parent) : decl.parent; - if (parent === container) { - return !(isJSDocTemplateTag(decl.parent) && find((decl.parent.parent as JSDoc).tags!, isJSDocTypeAlias)); // TODO: GH#18217 + const container = getThisContainer(errorLocation, /*includeArrowFunctions*/ false); + let location = container; + while (location) { + if (isClassLike(location.parent)) { + const classSymbol = getSymbolOfNode(location.parent); + if (!classSymbol) { + break; + } + // Check to see if a static member exists. + const constructorType = getTypeOfSymbol(classSymbol); + if (getPropertyOfType(constructorType, name)) { + error(errorLocation, Diagnostics.Cannot_find_name_0_Did_you_mean_the_static_member_1_0, diagnosticName(nameArg), symbolToString(classSymbol)); + return true; + } + // No static member is present. + // Check if we're in an instance method and look for a relevant instance member. + if (location === container && !hasModifier(location, ModifierFlags.Static)) { + const instanceType = ((getDeclaredTypeOfSymbol(classSymbol)).thisType!); // TODO: GH#18217 + if (getPropertyOfType(instanceType, name)) { + error(errorLocation, Diagnostics.Cannot_find_name_0_Did_you_mean_the_instance_member_this_0, diagnosticName(nameArg)); + return true; } } } - - return false; + location = location.parent; } - - function checkAndReportErrorForMissingPrefix(errorLocation: Node, name: __String, nameArg: __String | Identifier): boolean { - if (!isIdentifier(errorLocation) || errorLocation.escapedText !== name || isTypeReferenceIdentifier(errorLocation) || isInTypeQuery(errorLocation)) { - return false; - } - - const container = getThisContainer(errorLocation, /*includeArrowFunctions*/ false); - let location = container; - while (location) { - if (isClassLike(location.parent)) { - const classSymbol = getSymbolOfNode(location.parent); - if (!classSymbol) { - break; - } - - // Check to see if a static member exists. - const constructorType = getTypeOfSymbol(classSymbol); - if (getPropertyOfType(constructorType, name)) { - error(errorLocation, Diagnostics.Cannot_find_name_0_Did_you_mean_the_static_member_1_0, diagnosticName(nameArg), symbolToString(classSymbol)); + return false; + } + function checkAndReportErrorForExtendingInterface(errorLocation: Node): boolean { + const expression = getEntityNameForExtendingInterface(errorLocation); + if (expression && resolveEntityName(expression, SymbolFlags.Interface, /*ignoreErrors*/ true)) { + error(errorLocation, Diagnostics.Cannot_extend_an_interface_0_Did_you_mean_implements, getTextOfNode(expression)); + return true; + } + return false; + } + /** + * Climbs up parents to an ExpressionWithTypeArguments, and returns its expression, + * but returns undefined if that expression is not an EntityNameExpression. + */ + function getEntityNameForExtendingInterface(node: Node): EntityNameExpression | undefined { + switch (node.kind) { + case SyntaxKind.Identifier: + case SyntaxKind.PropertyAccessExpression: + return node.parent ? getEntityNameForExtendingInterface(node.parent) : undefined; + case SyntaxKind.ExpressionWithTypeArguments: + if (isEntityNameExpression((node).expression)) { + return (node).expression; + } + // falls through + default: + return undefined; + } + } + function checkAndReportErrorForUsingTypeAsNamespace(errorLocation: Node, name: __String, meaning: SymbolFlags): boolean { + const namespaceMeaning = SymbolFlags.Namespace | (isInJSFile(errorLocation) ? SymbolFlags.Value : 0); + if (meaning === namespaceMeaning) { + const symbol = resolveSymbol(resolveName(errorLocation, name, SymbolFlags.Type & ~namespaceMeaning, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false)); + const parent = errorLocation.parent; + if (symbol) { + if (isQualifiedName(parent)) { + Debug.assert(parent.left === errorLocation, "Should only be resolving left side of qualified name as a namespace"); + const propName = parent.right.escapedText; + const propType = getPropertyOfType(getDeclaredTypeOfSymbol(symbol), propName); + if (propType) { + error(parent, Diagnostics.Cannot_access_0_1_because_0_is_a_type_but_not_a_namespace_Did_you_mean_to_retrieve_the_type_of_the_property_1_in_0_with_0_1, unescapeLeadingUnderscores(name), unescapeLeadingUnderscores(propName)); return true; } - - // No static member is present. - // Check if we're in an instance method and look for a relevant instance member. - if (location === container && !hasModifier(location, ModifierFlags.Static)) { - const instanceType = (getDeclaredTypeOfSymbol(classSymbol)).thisType!; // TODO: GH#18217 - if (getPropertyOfType(instanceType, name)) { - error(errorLocation, Diagnostics.Cannot_find_name_0_Did_you_mean_the_instance_member_this_0, diagnosticName(nameArg)); - return true; - } - } } - - location = location.parent; + error(errorLocation, Diagnostics._0_only_refers_to_a_type_but_is_being_used_as_a_namespace_here, unescapeLeadingUnderscores(name)); + return true; } - return false; } - - - function checkAndReportErrorForExtendingInterface(errorLocation: Node): boolean { - const expression = getEntityNameForExtendingInterface(errorLocation); - if (expression && resolveEntityName(expression, SymbolFlags.Interface, /*ignoreErrors*/ true)) { - error(errorLocation, Diagnostics.Cannot_extend_an_interface_0_Did_you_mean_implements, getTextOfNode(expression)); + return false; + } + function checkAndReportErrorForUsingValueAsType(errorLocation: Node, name: __String, meaning: SymbolFlags): boolean { + if (meaning & (SymbolFlags.Type & ~SymbolFlags.Namespace)) { + const symbol = resolveSymbol(resolveName(errorLocation, name, ~SymbolFlags.Type & SymbolFlags.Value, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false)); + if (symbol && !(symbol.flags & SymbolFlags.Namespace)) { + error(errorLocation, Diagnostics._0_refers_to_a_value_but_is_being_used_as_a_type_here, unescapeLeadingUnderscores(name)); return true; } - return false; } - /** - * Climbs up parents to an ExpressionWithTypeArguments, and returns its expression, - * but returns undefined if that expression is not an EntityNameExpression. - */ - function getEntityNameForExtendingInterface(node: Node): EntityNameExpression | undefined { - switch (node.kind) { - case SyntaxKind.Identifier: - case SyntaxKind.PropertyAccessExpression: - return node.parent ? getEntityNameForExtendingInterface(node.parent) : undefined; - case SyntaxKind.ExpressionWithTypeArguments: - if (isEntityNameExpression((node).expression)) { - return (node).expression; - } - // falls through - default: - return undefined; - } + return false; + } + function isPrimitiveTypeName(name: __String) { + return name === "any" || name === "string" || name === "number" || name === "boolean" || name === "never" || name === "unknown"; + } + function checkAndReportErrorForExportingPrimitiveType(errorLocation: Node, name: __String): boolean { + if (isPrimitiveTypeName(name) && errorLocation.parent.kind === SyntaxKind.ExportSpecifier) { + error(errorLocation, Diagnostics.Cannot_export_0_Only_local_declarations_can_be_exported_from_a_module, (name as string)); + return true; } - - function checkAndReportErrorForUsingTypeAsNamespace(errorLocation: Node, name: __String, meaning: SymbolFlags): boolean { - const namespaceMeaning = SymbolFlags.Namespace | (isInJSFile(errorLocation) ? SymbolFlags.Value : 0); - if (meaning === namespaceMeaning) { - const symbol = resolveSymbol(resolveName(errorLocation, name, SymbolFlags.Type & ~namespaceMeaning, /*nameNotFoundMessage*/undefined, /*nameArg*/ undefined, /*isUse*/ false)); - const parent = errorLocation.parent; - if (symbol) { - if (isQualifiedName(parent)) { - Debug.assert(parent.left === errorLocation, "Should only be resolving left side of qualified name as a namespace"); - const propName = parent.right.escapedText; - const propType = getPropertyOfType(getDeclaredTypeOfSymbol(symbol), propName); - if (propType) { - error( - parent, - Diagnostics.Cannot_access_0_1_because_0_is_a_type_but_not_a_namespace_Did_you_mean_to_retrieve_the_type_of_the_property_1_in_0_with_0_1, - unescapeLeadingUnderscores(name), - unescapeLeadingUnderscores(propName), - ); - return true; - } - } - error(errorLocation, Diagnostics._0_only_refers_to_a_type_but_is_being_used_as_a_namespace_here, unescapeLeadingUnderscores(name)); - return true; - } + return false; + } + function checkAndReportErrorForUsingTypeAsValue(errorLocation: Node, name: __String, meaning: SymbolFlags): boolean { + if (meaning & (SymbolFlags.Value & ~SymbolFlags.NamespaceModule)) { + if (isPrimitiveTypeName(name)) { + error(errorLocation, Diagnostics._0_only_refers_to_a_type_but_is_being_used_as_a_value_here, unescapeLeadingUnderscores(name)); + return true; } - - return false; - } - - function checkAndReportErrorForUsingValueAsType(errorLocation: Node, name: __String, meaning: SymbolFlags): boolean { - if (meaning & (SymbolFlags.Type & ~SymbolFlags.Namespace)) { - const symbol = resolveSymbol(resolveName(errorLocation, name, ~SymbolFlags.Type & SymbolFlags.Value, /*nameNotFoundMessage*/undefined, /*nameArg*/ undefined, /*isUse*/ false)); - if (symbol && !(symbol.flags & SymbolFlags.Namespace)) { - error(errorLocation, Diagnostics._0_refers_to_a_value_but_is_being_used_as_a_type_here, unescapeLeadingUnderscores(name)); - return true; - } + const symbol = resolveSymbol(resolveName(errorLocation, name, SymbolFlags.Type & ~SymbolFlags.Value, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false)); + if (symbol && !(symbol.flags & SymbolFlags.NamespaceModule)) { + const message = isES2015OrLaterConstructorName(name) + ? Diagnostics._0_only_refers_to_a_type_but_is_being_used_as_a_value_here_Do_you_need_to_change_your_target_library_Try_changing_the_lib_compiler_option_to_es2015_or_later + : Diagnostics._0_only_refers_to_a_type_but_is_being_used_as_a_value_here; + error(errorLocation, message, unescapeLeadingUnderscores(name)); + return true; } - return false; } - - function isPrimitiveTypeName(name: __String) { - return name === "any" || name === "string" || name === "number" || name === "boolean" || name === "never" || name === "unknown"; - } - - function checkAndReportErrorForExportingPrimitiveType(errorLocation: Node, name: __String): boolean { - if (isPrimitiveTypeName(name) && errorLocation.parent.kind === SyntaxKind.ExportSpecifier) { - error(errorLocation, Diagnostics.Cannot_export_0_Only_local_declarations_can_be_exported_from_a_module, name as string); + return false; + } + function isES2015OrLaterConstructorName(n: __String) { + switch (n) { + case "Promise": + case "Symbol": + case "Map": + case "WeakMap": + case "Set": + case "WeakSet": return true; - } - return false; } - - function checkAndReportErrorForUsingTypeAsValue(errorLocation: Node, name: __String, meaning: SymbolFlags): boolean { - if (meaning & (SymbolFlags.Value & ~SymbolFlags.NamespaceModule)) { - if (isPrimitiveTypeName(name)) { - error(errorLocation, Diagnostics._0_only_refers_to_a_type_but_is_being_used_as_a_value_here, unescapeLeadingUnderscores(name)); - return true; - } - const symbol = resolveSymbol(resolveName(errorLocation, name, SymbolFlags.Type & ~SymbolFlags.Value, /*nameNotFoundMessage*/undefined, /*nameArg*/ undefined, /*isUse*/ false)); - if (symbol && !(symbol.flags & SymbolFlags.NamespaceModule)) { - const message = isES2015OrLaterConstructorName(name) - ? Diagnostics._0_only_refers_to_a_type_but_is_being_used_as_a_value_here_Do_you_need_to_change_your_target_library_Try_changing_the_lib_compiler_option_to_es2015_or_later - : Diagnostics._0_only_refers_to_a_type_but_is_being_used_as_a_value_here; - error(errorLocation, message, unescapeLeadingUnderscores(name)); - return true; - } + return false; + } + function checkAndReportErrorForUsingNamespaceModuleAsValue(errorLocation: Node, name: __String, meaning: SymbolFlags): boolean { + if (meaning & (SymbolFlags.Value & ~SymbolFlags.NamespaceModule & ~SymbolFlags.Type)) { + const symbol = resolveSymbol(resolveName(errorLocation, name, SymbolFlags.NamespaceModule & ~SymbolFlags.Value, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false)); + if (symbol) { + error(errorLocation, Diagnostics.Cannot_use_namespace_0_as_a_value, unescapeLeadingUnderscores(name)); + return true; } - return false; } - - function isES2015OrLaterConstructorName(n: __String) { - switch (n) { - case "Promise": - case "Symbol": - case "Map": - case "WeakMap": - case "Set": - case "WeakSet": - return true; + else if (meaning & (SymbolFlags.Type & ~SymbolFlags.NamespaceModule & ~SymbolFlags.Value)) { + const symbol = resolveSymbol(resolveName(errorLocation, name, (SymbolFlags.ValueModule | SymbolFlags.NamespaceModule) & ~SymbolFlags.Type, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false)); + if (symbol) { + error(errorLocation, Diagnostics.Cannot_use_namespace_0_as_a_type, unescapeLeadingUnderscores(name)); + return true; } - return false; } - - function checkAndReportErrorForUsingNamespaceModuleAsValue(errorLocation: Node, name: __String, meaning: SymbolFlags): boolean { - if (meaning & (SymbolFlags.Value & ~SymbolFlags.NamespaceModule & ~SymbolFlags.Type)) { - const symbol = resolveSymbol(resolveName(errorLocation, name, SymbolFlags.NamespaceModule & ~SymbolFlags.Value, /*nameNotFoundMessage*/undefined, /*nameArg*/ undefined, /*isUse*/ false)); - if (symbol) { - error( - errorLocation, - Diagnostics.Cannot_use_namespace_0_as_a_value, - unescapeLeadingUnderscores(name)); - return true; - } + return false; + } + function checkResolvedBlockScopedVariable(result: ts.Symbol, errorLocation: Node): void { + Debug.assert(!!(result.flags & SymbolFlags.BlockScopedVariable || result.flags & SymbolFlags.Class || result.flags & SymbolFlags.Enum)); + if (result.flags & (SymbolFlags.Function | SymbolFlags.FunctionScopedVariable | SymbolFlags.Assignment) && result.flags & SymbolFlags.Class) { + // constructor functions aren't block scoped + return; + } + // Block-scoped variables cannot be used before their definition + const declaration = find(result.declarations, d => isBlockOrCatchScoped(d) || isClassLike(d) || (d.kind === SyntaxKind.EnumDeclaration)); + if (declaration === undefined) + return Debug.fail("checkResolvedBlockScopedVariable could not find block-scoped declaration"); + if (!(declaration.flags & NodeFlags.Ambient) && !isBlockScopedNameDeclaredBeforeUse(declaration, errorLocation)) { + let diagnosticMessage; + const declarationName = declarationNameToString(getNameOfDeclaration(declaration)); + if (result.flags & SymbolFlags.BlockScopedVariable) { + diagnosticMessage = error(errorLocation, Diagnostics.Block_scoped_variable_0_used_before_its_declaration, declarationName); } - else if (meaning & (SymbolFlags.Type & ~SymbolFlags.NamespaceModule & ~SymbolFlags.Value)) { - const symbol = resolveSymbol(resolveName(errorLocation, name, (SymbolFlags.ValueModule | SymbolFlags.NamespaceModule) & ~SymbolFlags.Type, /*nameNotFoundMessage*/undefined, /*nameArg*/ undefined, /*isUse*/ false)); - if (symbol) { - error(errorLocation, Diagnostics.Cannot_use_namespace_0_as_a_type, unescapeLeadingUnderscores(name)); - return true; - } + else if (result.flags & SymbolFlags.Class) { + diagnosticMessage = error(errorLocation, Diagnostics.Class_0_used_before_its_declaration, declarationName); } - return false; - } - - function checkResolvedBlockScopedVariable(result: Symbol, errorLocation: Node): void { - Debug.assert(!!(result.flags & SymbolFlags.BlockScopedVariable || result.flags & SymbolFlags.Class || result.flags & SymbolFlags.Enum)); - if (result.flags & (SymbolFlags.Function | SymbolFlags.FunctionScopedVariable | SymbolFlags.Assignment) && result.flags & SymbolFlags.Class) { - // constructor functions aren't block scoped - return; + else if (result.flags & SymbolFlags.RegularEnum) { + diagnosticMessage = error(errorLocation, Diagnostics.Enum_0_used_before_its_declaration, declarationName); } - // Block-scoped variables cannot be used before their definition - const declaration = find( - result.declarations, - d => isBlockOrCatchScoped(d) || isClassLike(d) || (d.kind === SyntaxKind.EnumDeclaration)); - - if (declaration === undefined) return Debug.fail("checkResolvedBlockScopedVariable could not find block-scoped declaration"); - - if (!(declaration.flags & NodeFlags.Ambient) && !isBlockScopedNameDeclaredBeforeUse(declaration, errorLocation)) { - let diagnosticMessage; - const declarationName = declarationNameToString(getNameOfDeclaration(declaration)); - if (result.flags & SymbolFlags.BlockScopedVariable) { - diagnosticMessage = error(errorLocation, Diagnostics.Block_scoped_variable_0_used_before_its_declaration, declarationName); - } - else if (result.flags & SymbolFlags.Class) { + else { + Debug.assert(!!(result.flags & SymbolFlags.ConstEnum)); + if (compilerOptions.preserveConstEnums) { diagnosticMessage = error(errorLocation, Diagnostics.Class_0_used_before_its_declaration, declarationName); } - else if (result.flags & SymbolFlags.RegularEnum) { - diagnosticMessage = error(errorLocation, Diagnostics.Enum_0_used_before_its_declaration, declarationName); - } - else { - Debug.assert(!!(result.flags & SymbolFlags.ConstEnum)); - if (compilerOptions.preserveConstEnums) { - diagnosticMessage = error(errorLocation, Diagnostics.Class_0_used_before_its_declaration, declarationName); - } - } - - if (diagnosticMessage) { - addRelatedInfo(diagnosticMessage, - createDiagnosticForNode(declaration, Diagnostics._0_is_declared_here, declarationName) - ); - } } - } - - /* Starting from 'initial' node walk up the parent chain until 'stopAt' node is reached. - * If at any point current node is equal to 'parent' node - return true. - * Return false if 'stopAt' node is reached or isFunctionLike(current) === true. - */ - function isSameScopeDescendentOf(initial: Node, parent: Node | undefined, stopAt: Node): boolean { - return !!parent && !!findAncestor(initial, n => n === stopAt || isFunctionLike(n) ? "quit" : n === parent); - } - - function getAnyImportSyntax(node: Node): AnyImportSyntax | undefined { - switch (node.kind) { - case SyntaxKind.ImportEqualsDeclaration: - return node as ImportEqualsDeclaration; - case SyntaxKind.ImportClause: - return (node as ImportClause).parent; - case SyntaxKind.NamespaceImport: - return (node as NamespaceImport).parent.parent; - case SyntaxKind.ImportSpecifier: - return (node as ImportSpecifier).parent.parent.parent; - default: - return undefined; + if (diagnosticMessage) { + addRelatedInfo(diagnosticMessage, createDiagnosticForNode(declaration, Diagnostics._0_is_declared_here, declarationName)); } } - - function getDeclarationOfAliasSymbol(symbol: Symbol): Declaration | undefined { - return find(symbol.declarations, isAliasSymbolDeclaration); + } + /* Starting from 'initial' node walk up the parent chain until 'stopAt' node is reached. + * If at any point current node is equal to 'parent' node - return true. + * Return false if 'stopAt' node is reached or isFunctionLike(current) === true. + */ + function isSameScopeDescendentOf(initial: Node, parent: Node | undefined, stopAt: Node): boolean { + return !!parent && !!findAncestor(initial, n => n === stopAt || isFunctionLike(n) ? "quit" : n === parent); + } + function getAnyImportSyntax(node: Node): AnyImportSyntax | undefined { + switch (node.kind) { + case SyntaxKind.ImportEqualsDeclaration: + return node as ImportEqualsDeclaration; + case SyntaxKind.ImportClause: + return (node as ImportClause).parent; + case SyntaxKind.NamespaceImport: + return (node as NamespaceImport).parent.parent; + case SyntaxKind.ImportSpecifier: + return (node as ImportSpecifier).parent.parent.parent; + default: + return undefined; } - - /** - * An alias symbol is created by one of the following declarations: - * import = ... - * import from ... - * import * as from ... - * import { x as } from ... - * export { x as } from ... - * export * as ns from ... - * export = - * export default - * module.exports = - * {} - * {name: } - */ - function isAliasSymbolDeclaration(node: Node): boolean { - return node.kind === SyntaxKind.ImportEqualsDeclaration || - node.kind === SyntaxKind.NamespaceExportDeclaration || - node.kind === SyntaxKind.ImportClause && !!(node).name || - node.kind === SyntaxKind.NamespaceImport || - node.kind === SyntaxKind.NamespaceExport || - node.kind === SyntaxKind.ImportSpecifier || - node.kind === SyntaxKind.ExportSpecifier || - node.kind === SyntaxKind.ExportAssignment && exportAssignmentIsAlias(node) || - isBinaryExpression(node) && getAssignmentDeclarationKind(node) === AssignmentDeclarationKind.ModuleExports && exportAssignmentIsAlias(node) || - isPropertyAccessExpression(node) - && isBinaryExpression(node.parent) - && node.parent.left === node - && node.parent.operatorToken.kind === SyntaxKind.EqualsToken - && isAliasableOrJsExpression(node.parent.right) || - node.kind === SyntaxKind.ShorthandPropertyAssignment || - node.kind === SyntaxKind.PropertyAssignment && isAliasableOrJsExpression((node as PropertyAssignment).initializer); + } + function getDeclarationOfAliasSymbol(symbol: ts.Symbol): Declaration | undefined { + return find(symbol.declarations, isAliasSymbolDeclaration); + } + /** + * An alias symbol is created by one of the following declarations: + * import = ... + * import from ... + * import * as from ... + * import { x as } from ... + * export { x as } from ... + * export * as ns from ... + * export = + * export default + * module.exports = + * {} + * {name: } + */ + function isAliasSymbolDeclaration(node: Node): boolean { + return node.kind === SyntaxKind.ImportEqualsDeclaration || + node.kind === SyntaxKind.NamespaceExportDeclaration || + node.kind === SyntaxKind.ImportClause && !!(node).name || + node.kind === SyntaxKind.NamespaceImport || + node.kind === SyntaxKind.NamespaceExport || + node.kind === SyntaxKind.ImportSpecifier || + node.kind === SyntaxKind.ExportSpecifier || + node.kind === SyntaxKind.ExportAssignment && exportAssignmentIsAlias((node)) || + isBinaryExpression(node) && getAssignmentDeclarationKind(node) === AssignmentDeclarationKind.ModuleExports && exportAssignmentIsAlias(node) || + isPropertyAccessExpression(node) + && isBinaryExpression(node.parent) + && node.parent.left === node + && node.parent.operatorToken.kind === SyntaxKind.EqualsToken + && isAliasableOrJsExpression(node.parent.right) || + node.kind === SyntaxKind.ShorthandPropertyAssignment || + node.kind === SyntaxKind.PropertyAssignment && isAliasableOrJsExpression((node as PropertyAssignment).initializer); + } + function isAliasableOrJsExpression(e: Expression) { + return isAliasableExpression(e) || isFunctionExpression(e) && isJSConstructor(e); + } + function getTargetOfImportEqualsDeclaration(node: ImportEqualsDeclaration, dontResolveAlias: boolean): ts.Symbol | undefined { + if (node.moduleReference.kind === SyntaxKind.ExternalModuleReference) { + const immediate = resolveExternalModuleName(node, getExternalModuleImportEqualsDeclarationExpression(node)); + const resolved = resolveExternalModuleSymbol(immediate); + markSymbolOfAliasDeclarationIfTypeOnly(node, immediate, resolved, /*overwriteEmpty*/ false); + return resolved; } - - function isAliasableOrJsExpression(e: Expression) { - return isAliasableExpression(e) || isFunctionExpression(e) && isJSConstructor(e); + const resolved = getSymbolOfPartOfRightHandSideOfImportEquals(node.moduleReference, dontResolveAlias); + checkAndReportErrorForResolvingImportAliasToTypeOnlySymbol(node, resolved); + return resolved; + } + function checkAndReportErrorForResolvingImportAliasToTypeOnlySymbol(node: ImportEqualsDeclaration, resolved: ts.Symbol | undefined) { + if (markSymbolOfAliasDeclarationIfTypeOnly(node, /*immediateTarget*/ undefined, resolved, /*overwriteEmpty*/ false)) { + const typeOnlyDeclaration = getTypeOnlyAliasDeclaration(getSymbolOfNode(node))!; + const isExport = typeOnlyDeclarationIsExport(typeOnlyDeclaration); + const message = isExport + ? Diagnostics.An_import_alias_cannot_reference_a_declaration_that_was_exported_using_export_type + : Diagnostics.An_import_alias_cannot_reference_a_declaration_that_was_imported_using_import_type; + const relatedMessage = isExport + ? Diagnostics._0_was_exported_here + : Diagnostics._0_was_imported_here; + // Non-null assertion is safe because the optionality comes from ImportClause, + // but if an ImportClause was the typeOnlyDeclaration, it had to have a `name`. + const name = unescapeLeadingUnderscores(typeOnlyDeclaration.name!.escapedText); + addRelatedInfo(error(node.moduleReference, message), createDiagnosticForNode(typeOnlyDeclaration, relatedMessage, name)); } - - function getTargetOfImportEqualsDeclaration(node: ImportEqualsDeclaration, dontResolveAlias: boolean): Symbol | undefined { - if (node.moduleReference.kind === SyntaxKind.ExternalModuleReference) { - const immediate = resolveExternalModuleName(node, getExternalModuleImportEqualsDeclarationExpression(node)); - const resolved = resolveExternalModuleSymbol(immediate); - markSymbolOfAliasDeclarationIfTypeOnly(node, immediate, resolved, /*overwriteEmpty*/ false); - return resolved; - } - const resolved = getSymbolOfPartOfRightHandSideOfImportEquals(node.moduleReference, dontResolveAlias); - checkAndReportErrorForResolvingImportAliasToTypeOnlySymbol(node, resolved); - return resolved; + } + function resolveExportByName(moduleSymbol: ts.Symbol, name: __String, sourceNode: TypeOnlyCompatibleAliasDeclaration | undefined, dontResolveAlias: boolean) { + const exportValue = moduleSymbol.exports!.get(InternalSymbolName.ExportEquals); + if (exportValue) { + return getPropertyOfType(getTypeOfSymbol(exportValue), name); + } + const exportSymbol = moduleSymbol.exports!.get(name); + const resolved = resolveSymbol(exportSymbol, dontResolveAlias); + markSymbolOfAliasDeclarationIfTypeOnly(sourceNode, exportSymbol, resolved, /*overwriteEmpty*/ false); + return resolved; + } + function isSyntacticDefault(node: Node) { + return ((isExportAssignment(node) && !node.isExportEquals) || hasModifier(node, ModifierFlags.Default) || isExportSpecifier(node)); + } + function canHaveSyntheticDefault(file: SourceFile | undefined, moduleSymbol: ts.Symbol, dontResolveAlias: boolean) { + if (!allowSyntheticDefaultImports) { + return false; } - - function checkAndReportErrorForResolvingImportAliasToTypeOnlySymbol(node: ImportEqualsDeclaration, resolved: Symbol | undefined) { - if (markSymbolOfAliasDeclarationIfTypeOnly(node, /*immediateTarget*/ undefined, resolved, /*overwriteEmpty*/ false)) { - const typeOnlyDeclaration = getTypeOnlyAliasDeclaration(getSymbolOfNode(node))!; - const isExport = typeOnlyDeclarationIsExport(typeOnlyDeclaration); - const message = isExport - ? Diagnostics.An_import_alias_cannot_reference_a_declaration_that_was_exported_using_export_type - : Diagnostics.An_import_alias_cannot_reference_a_declaration_that_was_imported_using_import_type; - const relatedMessage = isExport - ? Diagnostics._0_was_exported_here - : Diagnostics._0_was_imported_here; - - // Non-null assertion is safe because the optionality comes from ImportClause, - // but if an ImportClause was the typeOnlyDeclaration, it had to have a `name`. - const name = unescapeLeadingUnderscores(typeOnlyDeclaration.name!.escapedText); - addRelatedInfo(error(node.moduleReference, message), createDiagnosticForNode(typeOnlyDeclaration, relatedMessage, name)); + // Declaration files (and ambient modules) + if (!file || file.isDeclarationFile) { + // Definitely cannot have a synthetic default if they have a syntactic default member specified + const defaultExportSymbol = resolveExportByName(moduleSymbol, InternalSymbolName.Default, /*sourceNode*/ undefined, /*dontResolveAlias*/ true); // Dont resolve alias because we want the immediately exported symbol's declaration + if (defaultExportSymbol && some(defaultExportSymbol.declarations, isSyntacticDefault)) { + return false; } - } - - function resolveExportByName(moduleSymbol: Symbol, name: __String, sourceNode: TypeOnlyCompatibleAliasDeclaration | undefined, dontResolveAlias: boolean) { - const exportValue = moduleSymbol.exports!.get(InternalSymbolName.ExportEquals); - if (exportValue) { - return getPropertyOfType(getTypeOfSymbol(exportValue), name); + // It _might_ still be incorrect to assume there is no __esModule marker on the import at runtime, even if there is no `default` member + // So we check a bit more, + if (resolveExportByName(moduleSymbol, escapeLeadingUnderscores("__esModule"), /*sourceNode*/ undefined, dontResolveAlias)) { + // If there is an `__esModule` specified in the declaration (meaning someone explicitly added it or wrote it in their code), + // it definitely is a module and does not have a synthetic default + return false; } - const exportSymbol = moduleSymbol.exports!.get(name); - const resolved = resolveSymbol(exportSymbol, dontResolveAlias); - markSymbolOfAliasDeclarationIfTypeOnly(sourceNode, exportSymbol, resolved, /*overwriteEmpty*/ false); - return resolved; + // There are _many_ declaration files not written with esmodules in mind that still get compiled into a format with __esModule set + // Meaning there may be no default at runtime - however to be on the permissive side, we allow access to a synthetic default member + // as there is no marker to indicate if the accompanying JS has `__esModule` or not, or is even native esm + return true; } - - function isSyntacticDefault(node: Node) { - return ((isExportAssignment(node) && !node.isExportEquals) || hasModifier(node, ModifierFlags.Default) || isExportSpecifier(node)); + // TypeScript files never have a synthetic default (as they are always emitted with an __esModule marker) _unless_ they contain an export= statement + if (!isSourceFileJS(file)) { + return hasExportAssignmentSymbol(moduleSymbol); } - - function canHaveSyntheticDefault(file: SourceFile | undefined, moduleSymbol: Symbol, dontResolveAlias: boolean) { - if (!allowSyntheticDefaultImports) { - return false; + // JS files have a synthetic default if they do not contain ES2015+ module syntax (export = is not valid in js) _and_ do not have an __esModule marker + return !file.externalModuleIndicator && !resolveExportByName(moduleSymbol, escapeLeadingUnderscores("__esModule"), /*sourceNode*/ undefined, dontResolveAlias); + } + function getTargetOfImportClause(node: ImportClause, dontResolveAlias: boolean): ts.Symbol | undefined { + const moduleSymbol = resolveExternalModuleName(node, node.parent.moduleSpecifier); + if (moduleSymbol) { + let exportDefaultSymbol: ts.Symbol | undefined; + if (isShorthandAmbientModuleSymbol(moduleSymbol)) { + exportDefaultSymbol = moduleSymbol; } - // Declaration files (and ambient modules) - if (!file || file.isDeclarationFile) { - // Definitely cannot have a synthetic default if they have a syntactic default member specified - const defaultExportSymbol = resolveExportByName(moduleSymbol, InternalSymbolName.Default, /*sourceNode*/ undefined, /*dontResolveAlias*/ true); // Dont resolve alias because we want the immediately exported symbol's declaration - if (defaultExportSymbol && some(defaultExportSymbol.declarations, isSyntacticDefault)) { - return false; + else { + exportDefaultSymbol = resolveExportByName(moduleSymbol, InternalSymbolName.Default, node, dontResolveAlias); + } + const file = find(moduleSymbol.declarations, isSourceFile); + const hasSyntheticDefault = canHaveSyntheticDefault(file, moduleSymbol, dontResolveAlias); + if (!exportDefaultSymbol && !hasSyntheticDefault) { + if (hasExportAssignmentSymbol(moduleSymbol)) { + const compilerOptionName = moduleKind >= ModuleKind.ES2015 ? "allowSyntheticDefaultImports" : "esModuleInterop"; + const exportEqualsSymbol = moduleSymbol.exports!.get(InternalSymbolName.ExportEquals); + const exportAssignment = exportEqualsSymbol!.valueDeclaration; + const err = error(node.name, Diagnostics.Module_0_can_only_be_default_imported_using_the_1_flag, symbolToString(moduleSymbol), compilerOptionName); + addRelatedInfo(err, createDiagnosticForNode(exportAssignment, Diagnostics.This_module_is_declared_with_using_export_and_can_only_be_used_with_a_default_import_when_using_the_0_flag, compilerOptionName)); } - // It _might_ still be incorrect to assume there is no __esModule marker on the import at runtime, even if there is no `default` member - // So we check a bit more, - if (resolveExportByName(moduleSymbol, escapeLeadingUnderscores("__esModule"), /*sourceNode*/ undefined, dontResolveAlias)) { - // If there is an `__esModule` specified in the declaration (meaning someone explicitly added it or wrote it in their code), - // it definitely is a module and does not have a synthetic default - return false; + else { + if (moduleSymbol.exports && moduleSymbol.exports.has(node.symbol.escapedName)) { + error(node.name, Diagnostics.Module_0_has_no_default_export_Did_you_mean_to_use_import_1_from_0_instead, symbolToString(moduleSymbol), symbolToString(node.symbol)); + } + else { + error(node.name, Diagnostics.Module_0_has_no_default_export, symbolToString(moduleSymbol)); + } } - // There are _many_ declaration files not written with esmodules in mind that still get compiled into a format with __esModule set - // Meaning there may be no default at runtime - however to be on the permissive side, we allow access to a synthetic default member - // as there is no marker to indicate if the accompanying JS has `__esModule` or not, or is even native esm - return true; } - // TypeScript files never have a synthetic default (as they are always emitted with an __esModule marker) _unless_ they contain an export= statement - if (!isSourceFileJS(file)) { - return hasExportAssignmentSymbol(moduleSymbol); + else if (hasSyntheticDefault) { + // per emit behavior, a synthetic default overrides a "real" .default member if `__esModule` is not present + const resolved = resolveExternalModuleSymbol(moduleSymbol, dontResolveAlias) || resolveSymbol(moduleSymbol, dontResolveAlias); + markSymbolOfAliasDeclarationIfTypeOnly(node, moduleSymbol, resolved, /*overwriteTypeOnly*/ false); + return resolved; } - // JS files have a synthetic default if they do not contain ES2015+ module syntax (export = is not valid in js) _and_ do not have an __esModule marker - return !file.externalModuleIndicator && !resolveExportByName(moduleSymbol, escapeLeadingUnderscores("__esModule"), /*sourceNode*/ undefined, dontResolveAlias); + markSymbolOfAliasDeclarationIfTypeOnly(node, exportDefaultSymbol, /*finalTarget*/ undefined, /*overwriteTypeOnly*/ false); + return exportDefaultSymbol; } - - function getTargetOfImportClause(node: ImportClause, dontResolveAlias: boolean): Symbol | undefined { - const moduleSymbol = resolveExternalModuleName(node, node.parent.moduleSpecifier); - if (moduleSymbol) { - let exportDefaultSymbol: Symbol | undefined; + } + function getTargetOfNamespaceImport(node: NamespaceImport, dontResolveAlias: boolean): ts.Symbol | undefined { + const moduleSpecifier = node.parent.parent.moduleSpecifier; + const immediate = resolveExternalModuleName(node, moduleSpecifier); + const resolved = resolveESModuleSymbol(immediate, moduleSpecifier, dontResolveAlias, /*suppressUsageError*/ false); + markSymbolOfAliasDeclarationIfTypeOnly(node, immediate, resolved, /*overwriteEmpty*/ false); + return resolved; + } + function getTargetOfNamespaceExport(node: NamespaceExport, dontResolveAlias: boolean): ts.Symbol | undefined { + const moduleSpecifier = node.parent.moduleSpecifier; + const immediate = moduleSpecifier && resolveExternalModuleName(node, moduleSpecifier); + const resolved = moduleSpecifier && resolveESModuleSymbol(immediate, moduleSpecifier, dontResolveAlias, /*suppressUsageError*/ false); + markSymbolOfAliasDeclarationIfTypeOnly(node, immediate, resolved, /*overwriteEmpty*/ false); + return resolved; + } + // This function creates a synthetic symbol that combines the value side of one symbol with the + // type/namespace side of another symbol. Consider this example: + // + // declare module graphics { + // interface Point { + // x: number; + // y: number; + // } + // } + // declare var graphics: { + // Point: new (x: number, y: number) => graphics.Point; + // } + // declare module "graphics" { + // export = graphics; + // } + // + // An 'import { Point } from "graphics"' needs to create a symbol that combines the value side 'Point' + // property with the type/namespace side interface 'Point'. + function combineValueAndTypeSymbols(valueSymbol: ts.Symbol, typeSymbol: ts.Symbol): ts.Symbol { + if (valueSymbol === unknownSymbol && typeSymbol === unknownSymbol) { + return unknownSymbol; + } + if (valueSymbol.flags & (SymbolFlags.Type | SymbolFlags.Namespace)) { + return valueSymbol; + } + const result = createSymbol(valueSymbol.flags | typeSymbol.flags, valueSymbol.escapedName); + result.declarations = deduplicate(concatenate(valueSymbol.declarations, typeSymbol.declarations), equateValues); + result.parent = valueSymbol.parent || typeSymbol.parent; + if (valueSymbol.valueDeclaration) + result.valueDeclaration = valueSymbol.valueDeclaration; + if (typeSymbol.members) + result.members = cloneMap(typeSymbol.members); + if (valueSymbol.exports) + result.exports = cloneMap(valueSymbol.exports); + return result; + } + function getExportOfModule(symbol: ts.Symbol, specifier: ImportOrExportSpecifier, dontResolveAlias: boolean): ts.Symbol | undefined { + if (symbol.flags & SymbolFlags.Module) { + const name = (specifier.propertyName ?? specifier.name).escapedText; + const exportSymbol = getExportsOfSymbol(symbol).get(name); + const resolved = resolveSymbol(exportSymbol, dontResolveAlias); + markSymbolOfAliasDeclarationIfTypeOnly(specifier, exportSymbol, resolved, /*overwriteEmpty*/ false); + return resolved; + } + } + function getPropertyOfVariable(symbol: ts.Symbol, name: __String): ts.Symbol | undefined { + if (symbol.flags & SymbolFlags.Variable) { + const typeAnnotation = (symbol.valueDeclaration).type; + if (typeAnnotation) { + return resolveSymbol(getPropertyOfType(getTypeFromTypeNode(typeAnnotation), name)); + } + } + } + function getExternalModuleMember(node: ImportDeclaration | ExportDeclaration, specifier: ImportOrExportSpecifier, dontResolveAlias = false): ts.Symbol | undefined { + const moduleSymbol = resolveExternalModuleName(node, node.moduleSpecifier!)!; // TODO: GH#18217 + const name = specifier.propertyName || specifier.name; + const suppressInteropError = name.escapedText === InternalSymbolName.Default && !!(compilerOptions.allowSyntheticDefaultImports || compilerOptions.esModuleInterop); + const targetSymbol = resolveESModuleSymbol(moduleSymbol, node.moduleSpecifier!, dontResolveAlias, suppressInteropError); + if (targetSymbol) { + if (name.escapedText) { if (isShorthandAmbientModuleSymbol(moduleSymbol)) { - exportDefaultSymbol = moduleSymbol; + return moduleSymbol; + } + let symbolFromVariable: ts.Symbol | undefined; + // First check if module was specified with "export=". If so, get the member from the resolved type + if (moduleSymbol && moduleSymbol.exports && moduleSymbol.exports.get(InternalSymbolName.ExportEquals)) { + symbolFromVariable = getPropertyOfType(getTypeOfSymbol(targetSymbol), name.escapedText); } else { - exportDefaultSymbol = resolveExportByName(moduleSymbol, InternalSymbolName.Default, node, dontResolveAlias); + symbolFromVariable = getPropertyOfVariable(targetSymbol, name.escapedText); } - - const file = find(moduleSymbol.declarations, isSourceFile); - const hasSyntheticDefault = canHaveSyntheticDefault(file, moduleSymbol, dontResolveAlias); - if (!exportDefaultSymbol && !hasSyntheticDefault) { - if (hasExportAssignmentSymbol(moduleSymbol)) { - const compilerOptionName = moduleKind >= ModuleKind.ES2015 ? "allowSyntheticDefaultImports" : "esModuleInterop"; - const exportEqualsSymbol = moduleSymbol.exports!.get(InternalSymbolName.ExportEquals); - const exportAssignment = exportEqualsSymbol!.valueDeclaration; - const err = error(node.name, Diagnostics.Module_0_can_only_be_default_imported_using_the_1_flag, symbolToString(moduleSymbol), compilerOptionName); - - addRelatedInfo(err, createDiagnosticForNode( - exportAssignment, - Diagnostics.This_module_is_declared_with_using_export_and_can_only_be_used_with_a_default_import_when_using_the_0_flag, - compilerOptionName - )); + // if symbolFromVariable is export - get its final target + symbolFromVariable = resolveSymbol(symbolFromVariable, dontResolveAlias); + let symbolFromModule = getExportOfModule(targetSymbol, specifier, dontResolveAlias); + if (symbolFromModule === undefined && name.escapedText === InternalSymbolName.Default) { + const file = find(moduleSymbol.declarations, isSourceFile); + if (canHaveSyntheticDefault(file, moduleSymbol, dontResolveAlias)) { + symbolFromModule = resolveExternalModuleSymbol(moduleSymbol, dontResolveAlias) || resolveSymbol(moduleSymbol, dontResolveAlias); + } + } + const symbol = symbolFromModule && symbolFromVariable && symbolFromModule !== symbolFromVariable ? + combineValueAndTypeSymbols(symbolFromVariable, symbolFromModule) : + symbolFromModule || symbolFromVariable; + if (!symbol) { + const moduleName = getFullyQualifiedName(moduleSymbol, node); + const declarationName = declarationNameToString(name); + const suggestion = getSuggestedSymbolForNonexistentModule(name, targetSymbol); + if (suggestion !== undefined) { + const suggestionName = symbolToString(suggestion); + const diagnostic = error(name, Diagnostics.Module_0_has_no_exported_member_1_Did_you_mean_2, moduleName, declarationName, suggestionName); + if (suggestion.valueDeclaration) { + addRelatedInfo(diagnostic, createDiagnosticForNode(suggestion.valueDeclaration, Diagnostics._0_is_declared_here, suggestionName)); + } } else { - if (moduleSymbol.exports && moduleSymbol.exports.has(node.symbol.escapedName)) { - error( - node.name, - Diagnostics.Module_0_has_no_default_export_Did_you_mean_to_use_import_1_from_0_instead, - symbolToString(moduleSymbol), - symbolToString(node.symbol), - ); + if (moduleSymbol.exports?.has(InternalSymbolName.Default)) { + error(name, Diagnostics.Module_0_has_no_exported_member_1_Did_you_mean_to_use_import_1_from_0_instead, moduleName, declarationName); } else { - error(node.name, Diagnostics.Module_0_has_no_default_export, symbolToString(moduleSymbol)); + reportNonExportedMember(name, declarationName, moduleSymbol, moduleName); } - } } - else if (hasSyntheticDefault) { - // per emit behavior, a synthetic default overrides a "real" .default member if `__esModule` is not present - const resolved = resolveExternalModuleSymbol(moduleSymbol, dontResolveAlias) || resolveSymbol(moduleSymbol, dontResolveAlias); - markSymbolOfAliasDeclarationIfTypeOnly(node, moduleSymbol, resolved, /*overwriteTypeOnly*/ false); - return resolved; - } - markSymbolOfAliasDeclarationIfTypeOnly(node, exportDefaultSymbol, /*finalTarget*/ undefined, /*overwriteTypeOnly*/ false); - return exportDefaultSymbol; + return symbol; } } - - function getTargetOfNamespaceImport(node: NamespaceImport, dontResolveAlias: boolean): Symbol | undefined { - const moduleSpecifier = node.parent.parent.moduleSpecifier; - const immediate = resolveExternalModuleName(node, moduleSpecifier); - const resolved = resolveESModuleSymbol(immediate, moduleSpecifier, dontResolveAlias, /*suppressUsageError*/ false); - markSymbolOfAliasDeclarationIfTypeOnly(node, immediate, resolved, /*overwriteEmpty*/ false); - return resolved; + } + function reportNonExportedMember(name: Identifier, declarationName: string, moduleSymbol: ts.Symbol, moduleName: string): void { + const localSymbol = moduleSymbol.valueDeclaration.locals?.get(name.escapedText); + const exports = moduleSymbol.exports; + if (localSymbol) { + const exportedSymbol = exports && !exports.has(InternalSymbolName.ExportEquals) + ? find(symbolsToArray(exports), symbol => !!getSymbolIfSameReference(symbol, localSymbol)) + : undefined; + const diagnostic = exportedSymbol + ? error(name, Diagnostics.Module_0_declares_1_locally_but_it_is_exported_as_2, moduleName, declarationName, symbolToString(exportedSymbol)) + : error(name, Diagnostics.Module_0_declares_1_locally_but_it_is_not_exported, moduleName, declarationName); + addRelatedInfo(diagnostic, ...map(localSymbol.declarations, (decl, index) => createDiagnosticForNode(decl, index === 0 ? Diagnostics._0_is_declared_here : Diagnostics.and_here, declarationName))); } - - function getTargetOfNamespaceExport(node: NamespaceExport, dontResolveAlias: boolean): Symbol | undefined { - const moduleSpecifier = node.parent.moduleSpecifier; - const immediate = moduleSpecifier && resolveExternalModuleName(node, moduleSpecifier); - const resolved = moduleSpecifier && resolveESModuleSymbol(immediate, moduleSpecifier, dontResolveAlias, /*suppressUsageError*/ false); - markSymbolOfAliasDeclarationIfTypeOnly(node, immediate, resolved, /*overwriteEmpty*/ false); - return resolved; + else { + error(name, Diagnostics.Module_0_has_no_exported_member_1, moduleName, declarationName); } - - // This function creates a synthetic symbol that combines the value side of one symbol with the - // type/namespace side of another symbol. Consider this example: - // - // declare module graphics { - // interface Point { - // x: number; - // y: number; - // } - // } - // declare var graphics: { - // Point: new (x: number, y: number) => graphics.Point; - // } - // declare module "graphics" { - // export = graphics; - // } - // - // An 'import { Point } from "graphics"' needs to create a symbol that combines the value side 'Point' - // property with the type/namespace side interface 'Point'. - function combineValueAndTypeSymbols(valueSymbol: Symbol, typeSymbol: Symbol): Symbol { - if (valueSymbol === unknownSymbol && typeSymbol === unknownSymbol) { - return unknownSymbol; - } - if (valueSymbol.flags & (SymbolFlags.Type | SymbolFlags.Namespace)) { - return valueSymbol; - } - const result = createSymbol(valueSymbol.flags | typeSymbol.flags, valueSymbol.escapedName); - result.declarations = deduplicate(concatenate(valueSymbol.declarations, typeSymbol.declarations), equateValues); - result.parent = valueSymbol.parent || typeSymbol.parent; - if (valueSymbol.valueDeclaration) result.valueDeclaration = valueSymbol.valueDeclaration; - if (typeSymbol.members) result.members = cloneMap(typeSymbol.members); - if (valueSymbol.exports) result.exports = cloneMap(valueSymbol.exports); - return result; + } + function getTargetOfImportSpecifier(node: ImportSpecifier, dontResolveAlias: boolean): ts.Symbol | undefined { + const resolved = getExternalModuleMember(node.parent.parent.parent, node, dontResolveAlias); + markSymbolOfAliasDeclarationIfTypeOnly(node, /*immediateTarget*/ undefined, resolved, /*overwriteEmpty*/ false); + return resolved; + } + function getTargetOfNamespaceExportDeclaration(node: NamespaceExportDeclaration, dontResolveAlias: boolean): ts.Symbol { + const resolved = resolveExternalModuleSymbol(node.parent.symbol, dontResolveAlias); + markSymbolOfAliasDeclarationIfTypeOnly(node, /*immediateTarget*/ undefined, resolved, /*overwriteEmpty*/ false); + return resolved; + } + function getTargetOfExportSpecifier(node: ExportSpecifier, meaning: SymbolFlags, dontResolveAlias?: boolean) { + const resolved = node.parent.parent.moduleSpecifier ? + getExternalModuleMember(node.parent.parent, node, dontResolveAlias) : + resolveEntityName(node.propertyName || node.name, meaning, /*ignoreErrors*/ false, dontResolveAlias); + markSymbolOfAliasDeclarationIfTypeOnly(node, /*immediateTarget*/ undefined, resolved, /*overwriteEmpty*/ false); + return resolved; + } + function getTargetOfExportAssignment(node: ExportAssignment | BinaryExpression, dontResolveAlias: boolean): ts.Symbol | undefined { + const expression = ((isExportAssignment(node) ? node.expression : node.right) as EntityNameExpression | ClassExpression); + const resolved = getTargetOfAliasLikeExpression(expression, dontResolveAlias); + markSymbolOfAliasDeclarationIfTypeOnly(node, /*immediateTarget*/ undefined, resolved, /*overwriteEmpty*/ false); + return resolved; + } + function getTargetOfAliasLikeExpression(expression: Expression, dontResolveAlias: boolean) { + if (isClassExpression(expression)) { + return checkExpressionCached(expression).symbol; } - - function getExportOfModule(symbol: Symbol, specifier: ImportOrExportSpecifier, dontResolveAlias: boolean): Symbol | undefined { - if (symbol.flags & SymbolFlags.Module) { - const name = (specifier.propertyName ?? specifier.name).escapedText; - const exportSymbol = getExportsOfSymbol(symbol).get(name); - const resolved = resolveSymbol(exportSymbol, dontResolveAlias); - markSymbolOfAliasDeclarationIfTypeOnly(specifier, exportSymbol, resolved, /*overwriteEmpty*/ false); - return resolved; - } + if (!isEntityName(expression) && !isEntityNameExpression(expression)) { + return undefined; } - - function getPropertyOfVariable(symbol: Symbol, name: __String): Symbol | undefined { - if (symbol.flags & SymbolFlags.Variable) { - const typeAnnotation = (symbol.valueDeclaration).type; - if (typeAnnotation) { - return resolveSymbol(getPropertyOfType(getTypeFromTypeNode(typeAnnotation), name)); - } - } + const aliasLike = resolveEntityName(expression, SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace, /*ignoreErrors*/ true, dontResolveAlias); + if (aliasLike) { + return aliasLike; } - - function getExternalModuleMember(node: ImportDeclaration | ExportDeclaration, specifier: ImportOrExportSpecifier, dontResolveAlias = false): Symbol | undefined { - const moduleSymbol = resolveExternalModuleName(node, node.moduleSpecifier!)!; // TODO: GH#18217 - const name = specifier.propertyName || specifier.name; - const suppressInteropError = name.escapedText === InternalSymbolName.Default && !!(compilerOptions.allowSyntheticDefaultImports || compilerOptions.esModuleInterop); - const targetSymbol = resolveESModuleSymbol(moduleSymbol, node.moduleSpecifier!, dontResolveAlias, suppressInteropError); - if (targetSymbol) { - if (name.escapedText) { - if (isShorthandAmbientModuleSymbol(moduleSymbol)) { - return moduleSymbol; - } - - let symbolFromVariable: Symbol | undefined; - // First check if module was specified with "export=". If so, get the member from the resolved type - if (moduleSymbol && moduleSymbol.exports && moduleSymbol.exports.get(InternalSymbolName.ExportEquals)) { - symbolFromVariable = getPropertyOfType(getTypeOfSymbol(targetSymbol), name.escapedText); - } - else { - symbolFromVariable = getPropertyOfVariable(targetSymbol, name.escapedText); - } - - // if symbolFromVariable is export - get its final target - symbolFromVariable = resolveSymbol(symbolFromVariable, dontResolveAlias); - let symbolFromModule = getExportOfModule(targetSymbol, specifier, dontResolveAlias); - if (symbolFromModule === undefined && name.escapedText === InternalSymbolName.Default) { - const file = find(moduleSymbol.declarations, isSourceFile); - if (canHaveSyntheticDefault(file, moduleSymbol, dontResolveAlias)) { - symbolFromModule = resolveExternalModuleSymbol(moduleSymbol, dontResolveAlias) || resolveSymbol(moduleSymbol, dontResolveAlias); - } - } - - const symbol = symbolFromModule && symbolFromVariable && symbolFromModule !== symbolFromVariable ? - combineValueAndTypeSymbols(symbolFromVariable, symbolFromModule) : - symbolFromModule || symbolFromVariable; - if (!symbol) { - const moduleName = getFullyQualifiedName(moduleSymbol, node); - const declarationName = declarationNameToString(name); - const suggestion = getSuggestedSymbolForNonexistentModule(name, targetSymbol); - if (suggestion !== undefined) { - const suggestionName = symbolToString(suggestion); - const diagnostic = error(name, Diagnostics.Module_0_has_no_exported_member_1_Did_you_mean_2, moduleName, declarationName, suggestionName); - if (suggestion.valueDeclaration) { - addRelatedInfo(diagnostic, - createDiagnosticForNode(suggestion.valueDeclaration, Diagnostics._0_is_declared_here, suggestionName) - ); - } - } - else { - if (moduleSymbol.exports?.has(InternalSymbolName.Default)) { - error( - name, - Diagnostics.Module_0_has_no_exported_member_1_Did_you_mean_to_use_import_1_from_0_instead, - moduleName, - declarationName - ); - } - else { - reportNonExportedMember(name, declarationName, moduleSymbol, moduleName); - } - } - } - return symbol; - } - } + checkExpressionCached(expression); + return getNodeLinks(expression).resolvedSymbol; + } + function getTargetOfPropertyAssignment(node: PropertyAssignment, dontRecursivelyResolve: boolean): ts.Symbol | undefined { + const expression = node.initializer; + return getTargetOfAliasLikeExpression(expression, dontRecursivelyResolve); + } + function getTargetOfPropertyAccessExpression(node: PropertyAccessExpression, dontRecursivelyResolve: boolean): ts.Symbol | undefined { + if (!(isBinaryExpression(node.parent) && node.parent.left === node && node.parent.operatorToken.kind === SyntaxKind.EqualsToken)) { + return undefined; } - - function reportNonExportedMember(name: Identifier, declarationName: string, moduleSymbol: Symbol, moduleName: string): void { - const localSymbol = moduleSymbol.valueDeclaration.locals?.get(name.escapedText); - const exports = moduleSymbol.exports; - - if (localSymbol) { - const exportedSymbol = exports && !exports.has(InternalSymbolName.ExportEquals) - ? find(symbolsToArray(exports), symbol => !!getSymbolIfSameReference(symbol, localSymbol)) - : undefined; - const diagnostic = exportedSymbol - ? error(name, Diagnostics.Module_0_declares_1_locally_but_it_is_exported_as_2, moduleName, declarationName, symbolToString(exportedSymbol)) - : error(name, Diagnostics.Module_0_declares_1_locally_but_it_is_not_exported, moduleName, declarationName); - - addRelatedInfo(diagnostic, - ...map(localSymbol.declarations, (decl, index) => - createDiagnosticForNode(decl, index === 0 ? Diagnostics._0_is_declared_here : Diagnostics.and_here, declarationName))); + return getTargetOfAliasLikeExpression(node.parent.right, dontRecursivelyResolve); + } + function getTargetOfAliasDeclaration(node: Declaration, dontRecursivelyResolve = false): ts.Symbol | undefined { + switch (node.kind) { + case SyntaxKind.ImportEqualsDeclaration: + return getTargetOfImportEqualsDeclaration((node), dontRecursivelyResolve); + case SyntaxKind.ImportClause: + return getTargetOfImportClause((node), dontRecursivelyResolve); + case SyntaxKind.NamespaceImport: + return getTargetOfNamespaceImport((node), dontRecursivelyResolve); + case SyntaxKind.NamespaceExport: + return getTargetOfNamespaceExport((node), dontRecursivelyResolve); + case SyntaxKind.ImportSpecifier: + return getTargetOfImportSpecifier((node), dontRecursivelyResolve); + case SyntaxKind.ExportSpecifier: + return getTargetOfExportSpecifier((node), SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace, dontRecursivelyResolve); + case SyntaxKind.ExportAssignment: + case SyntaxKind.BinaryExpression: + return getTargetOfExportAssignment((node), dontRecursivelyResolve); + case SyntaxKind.NamespaceExportDeclaration: + return getTargetOfNamespaceExportDeclaration((node), dontRecursivelyResolve); + case SyntaxKind.ShorthandPropertyAssignment: + return resolveEntityName((node as ShorthandPropertyAssignment).name, SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace, /*ignoreErrors*/ true, dontRecursivelyResolve); + case SyntaxKind.PropertyAssignment: + return getTargetOfPropertyAssignment((node as PropertyAssignment), dontRecursivelyResolve); + case SyntaxKind.PropertyAccessExpression: + return getTargetOfPropertyAccessExpression((node as PropertyAccessExpression), dontRecursivelyResolve); + default: + return Debug.fail(); + } + } + /** + * Indicates that a symbol is an alias that does not merge with a local declaration. + * OR Is a JSContainer which may merge an alias with a local declaration + */ + function isNonLocalAlias(symbol: ts.Symbol | undefined, excludes = SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace): symbol is ts.Symbol { + if (!symbol) + return false; + return (symbol.flags & (SymbolFlags.Alias | excludes)) === SymbolFlags.Alias || !!(symbol.flags & SymbolFlags.Alias && symbol.flags & SymbolFlags.Assignment); + } + function resolveSymbol(symbol: ts.Symbol, dontResolveAlias?: boolean): ts.Symbol; + function resolveSymbol(symbol: ts.Symbol | undefined, dontResolveAlias?: boolean): ts.Symbol | undefined; + function resolveSymbol(symbol: ts.Symbol | undefined, dontResolveAlias?: boolean): ts.Symbol | undefined { + return !dontResolveAlias && isNonLocalAlias(symbol) ? resolveAlias(symbol) : symbol; + } + function resolveAlias(symbol: ts.Symbol): ts.Symbol { + Debug.assert((symbol.flags & SymbolFlags.Alias) !== 0, "Should only get Alias here."); + const links = getSymbolLinks(symbol); + if (!links.target) { + links.target = resolvingSymbol; + const node = getDeclarationOfAliasSymbol(symbol); + if (!node) + return Debug.fail(); + const target = getTargetOfAliasDeclaration(node); + if (links.target === resolvingSymbol) { + links.target = target || unknownSymbol; } else { - error(name, Diagnostics.Module_0_has_no_exported_member_1, moduleName, declarationName); + error(node, Diagnostics.Circular_definition_of_import_alias_0, symbolToString(symbol)); } } - - function getTargetOfImportSpecifier(node: ImportSpecifier, dontResolveAlias: boolean): Symbol | undefined { - const resolved = getExternalModuleMember(node.parent.parent.parent, node, dontResolveAlias); - markSymbolOfAliasDeclarationIfTypeOnly(node, /*immediateTarget*/ undefined, resolved, /*overwriteEmpty*/ false); - return resolved; + else if (links.target === resolvingSymbol) { + links.target = unknownSymbol; } - - function getTargetOfNamespaceExportDeclaration(node: NamespaceExportDeclaration, dontResolveAlias: boolean): Symbol { - const resolved = resolveExternalModuleSymbol(node.parent.symbol, dontResolveAlias); - markSymbolOfAliasDeclarationIfTypeOnly(node, /*immediateTarget*/ undefined, resolved, /*overwriteEmpty*/ false); - return resolved; + return links.target; + } + /** + * Marks a symbol as type-only if its declaration is syntactically type-only. + * If it is not itself marked type-only, but resolves to a type-only alias + * somewhere in its resolution chain, save a reference to the type-only alias declaration + * so the alias _not_ marked type-only can be identified as _transitively_ type-only. + * + * This function is called on each alias declaration that could be type-only or resolve to + * another type-only alias during `resolveAlias`, so that later, when an alias is used in a + * JS-emitting expression, we can quickly determine if that symbol is effectively type-only + * and issue an error if so. + * + * @param aliasDeclaration The alias declaration not marked as type-only + * has already been marked as not resolving to a type-only alias. Used when recursively resolving qualified + * names of import aliases, e.g. `import C = a.b.C`. If namespace `a` is not found to be type-only, the + * import declaration will initially be marked as not resolving to a type-only symbol. But, namespace `b` + * must still be checked for a type-only marker, overwriting the previous negative result if found. + * @param immediateTarget The symbol to which the alias declaration immediately resolves + * @param finalTarget The symbol to which the alias declaration ultimately resolves + * @param overwriteEmpty Checks `resolvesToSymbol` for type-only declarations even if `aliasDeclaration` + */ + function markSymbolOfAliasDeclarationIfTypeOnly(aliasDeclaration: Declaration | undefined, immediateTarget: ts.Symbol | undefined, finalTarget: ts.Symbol | undefined, overwriteEmpty: boolean): boolean { + if (!aliasDeclaration) + return false; + // If the declaration itself is type-only, mark it and return. + // No need to check what it resolves to. + const sourceSymbol = getSymbolOfNode(aliasDeclaration); + if (isTypeOnlyImportOrExportDeclaration(aliasDeclaration)) { + const links = getSymbolLinks(sourceSymbol); + links.typeOnlyDeclaration = aliasDeclaration; + return true; } - - function getTargetOfExportSpecifier(node: ExportSpecifier, meaning: SymbolFlags, dontResolveAlias?: boolean) { - const resolved = node.parent.parent.moduleSpecifier ? - getExternalModuleMember(node.parent.parent, node, dontResolveAlias) : - resolveEntityName(node.propertyName || node.name, meaning, /*ignoreErrors*/ false, dontResolveAlias); - markSymbolOfAliasDeclarationIfTypeOnly(node, /*immediateTarget*/ undefined, resolved, /*overwriteEmpty*/ false); - return resolved; + const links = getSymbolLinks(sourceSymbol); + return markSymbolOfAliasDeclarationIfTypeOnlyWorker(links, immediateTarget, overwriteEmpty) + || markSymbolOfAliasDeclarationIfTypeOnlyWorker(links, finalTarget, overwriteEmpty); + } + function markSymbolOfAliasDeclarationIfTypeOnlyWorker(aliasDeclarationLinks: SymbolLinks, target: ts.Symbol | undefined, overwriteEmpty: boolean): boolean { + if (target && (aliasDeclarationLinks.typeOnlyDeclaration === undefined || overwriteEmpty && aliasDeclarationLinks.typeOnlyDeclaration === false)) { + const exportSymbol = target.exports?.get(InternalSymbolName.ExportEquals) ?? target; + const typeOnly = exportSymbol.declarations && find(exportSymbol.declarations, isTypeOnlyImportOrExportDeclaration); + aliasDeclarationLinks.typeOnlyDeclaration = typeOnly ?? getSymbolLinks(exportSymbol).typeOnlyDeclaration ?? false; } - - function getTargetOfExportAssignment(node: ExportAssignment | BinaryExpression, dontResolveAlias: boolean): Symbol | undefined { - const expression = (isExportAssignment(node) ? node.expression : node.right) as EntityNameExpression | ClassExpression; - const resolved = getTargetOfAliasLikeExpression(expression, dontResolveAlias); - markSymbolOfAliasDeclarationIfTypeOnly(node, /*immediateTarget*/ undefined, resolved, /*overwriteEmpty*/ false); - return resolved; + return !!aliasDeclarationLinks.typeOnlyDeclaration; + } + /** Indicates that a symbol directly or indirectly resolves to a type-only import or export. */ + function getTypeOnlyAliasDeclaration(symbol: ts.Symbol): TypeOnlyCompatibleAliasDeclaration | undefined { + if (!(symbol.flags & SymbolFlags.Alias)) { + return undefined; } - - function getTargetOfAliasLikeExpression(expression: Expression, dontResolveAlias: boolean) { - if (isClassExpression(expression)) { - return checkExpressionCached(expression).symbol; - } - if (!isEntityName(expression) && !isEntityNameExpression(expression)) { - return undefined; + const links = getSymbolLinks(symbol); + return links.typeOnlyDeclaration || undefined; + } + function markExportAsReferenced(node: ImportEqualsDeclaration | ExportSpecifier) { + const symbol = getSymbolOfNode(node); + const target = resolveAlias(symbol); + if (target) { + const markAlias = target === unknownSymbol || + ((target.flags & SymbolFlags.Value) && !isConstEnumOrConstEnumOnlyModule(target) && !getTypeOnlyAliasDeclaration(symbol)); + if (markAlias) { + markAliasSymbolAsReferenced(symbol); } - const aliasLike = resolveEntityName(expression, SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace, /*ignoreErrors*/ true, dontResolveAlias); - if (aliasLike) { - return aliasLike; + } + } + // When an alias symbol is referenced, we need to mark the entity it references as referenced and in turn repeat that until + // we reach a non-alias or an exported entity (which is always considered referenced). We do this by checking the target of + // the alias as an expression (which recursively takes us back here if the target references another alias). + function markAliasSymbolAsReferenced(symbol: ts.Symbol) { + const links = getSymbolLinks(symbol); + if (!links.referenced) { + links.referenced = true; + const node = getDeclarationOfAliasSymbol(symbol); + if (!node) + return Debug.fail(); + // We defer checking of the reference of an `import =` until the import itself is referenced, + // This way a chain of imports can be elided if ultimately the final input is only used in a type + // position. + if (isInternalModuleImportEqualsDeclaration(node)) { + const target = resolveSymbol(symbol); + if (target === unknownSymbol || target.flags & SymbolFlags.Value) { + // import foo = + checkExpressionCached((node.moduleReference)); + } } - checkExpressionCached(expression); - return getNodeLinks(expression).resolvedSymbol; } - - function getTargetOfPropertyAssignment(node: PropertyAssignment, dontRecursivelyResolve: boolean): Symbol | undefined { - const expression = node.initializer; - return getTargetOfAliasLikeExpression(expression, dontRecursivelyResolve); + } + // Aliases that resolve to const enums are not marked as referenced because they are not emitted, + // but their usage in value positions must be tracked to determine if the import can be type-only. + function markConstEnumAliasAsReferenced(symbol: ts.Symbol) { + const links = getSymbolLinks(symbol); + if (!links.constEnumReferenced) { + links.constEnumReferenced = true; } - - function getTargetOfPropertyAccessExpression(node: PropertyAccessExpression, dontRecursivelyResolve: boolean): Symbol | undefined { - if (!(isBinaryExpression(node.parent) && node.parent.left === node && node.parent.operatorToken.kind === SyntaxKind.EqualsToken)) { - return undefined; - } - - return getTargetOfAliasLikeExpression(node.parent.right, dontRecursivelyResolve); + } + // This function is only for imports with entity names + function getSymbolOfPartOfRightHandSideOfImportEquals(entityName: EntityName, dontResolveAlias?: boolean): ts.Symbol | undefined { + // There are three things we might try to look for. In the following examples, + // the search term is enclosed in |...|: + // + // import a = |b|; // Namespace + // import a = |b.c|; // Value, type, namespace + // import a = |b.c|.d; // Namespace + if (entityName.kind === SyntaxKind.Identifier && isRightSideOfQualifiedNameOrPropertyAccess(entityName)) { + entityName = (entityName.parent); + } + // Check for case 1 and 3 in the above example + if (entityName.kind === SyntaxKind.Identifier || entityName.parent.kind === SyntaxKind.QualifiedName) { + return resolveEntityName(entityName, SymbolFlags.Namespace, /*ignoreErrors*/ false, dontResolveAlias); + } + else { + // Case 2 in above example + // entityName.kind could be a QualifiedName or a Missing identifier + Debug.assert(entityName.parent.kind === SyntaxKind.ImportEqualsDeclaration); + return resolveEntityName(entityName, SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace, /*ignoreErrors*/ false, dontResolveAlias); } - - function getTargetOfAliasDeclaration(node: Declaration, dontRecursivelyResolve = false): Symbol | undefined { - switch (node.kind) { - case SyntaxKind.ImportEqualsDeclaration: - return getTargetOfImportEqualsDeclaration(node, dontRecursivelyResolve); - case SyntaxKind.ImportClause: - return getTargetOfImportClause(node, dontRecursivelyResolve); - case SyntaxKind.NamespaceImport: - return getTargetOfNamespaceImport(node, dontRecursivelyResolve); - case SyntaxKind.NamespaceExport: - return getTargetOfNamespaceExport(node, dontRecursivelyResolve); - case SyntaxKind.ImportSpecifier: - return getTargetOfImportSpecifier(node, dontRecursivelyResolve); - case SyntaxKind.ExportSpecifier: - return getTargetOfExportSpecifier(node, SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace, dontRecursivelyResolve); - case SyntaxKind.ExportAssignment: - case SyntaxKind.BinaryExpression: - return getTargetOfExportAssignment((node), dontRecursivelyResolve); - case SyntaxKind.NamespaceExportDeclaration: - return getTargetOfNamespaceExportDeclaration(node, dontRecursivelyResolve); - case SyntaxKind.ShorthandPropertyAssignment: - return resolveEntityName((node as ShorthandPropertyAssignment).name, SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace, /*ignoreErrors*/ true, dontRecursivelyResolve); - case SyntaxKind.PropertyAssignment: - return getTargetOfPropertyAssignment(node as PropertyAssignment, dontRecursivelyResolve); - case SyntaxKind.PropertyAccessExpression: - return getTargetOfPropertyAccessExpression(node as PropertyAccessExpression, dontRecursivelyResolve); - default: - return Debug.fail(); - } - } - - /** - * Indicates that a symbol is an alias that does not merge with a local declaration. - * OR Is a JSContainer which may merge an alias with a local declaration - */ - function isNonLocalAlias(symbol: Symbol | undefined, excludes = SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace): symbol is Symbol { - if (!symbol) return false; - return (symbol.flags & (SymbolFlags.Alias | excludes)) === SymbolFlags.Alias || !!(symbol.flags & SymbolFlags.Alias && symbol.flags & SymbolFlags.Assignment); + } + function getFullyQualifiedName(symbol: ts.Symbol, containingLocation?: Node): string { + return symbol.parent ? getFullyQualifiedName(symbol.parent, containingLocation) + "." + symbolToString(symbol) : symbolToString(symbol, containingLocation, /*meaning*/ undefined, SymbolFormatFlags.DoNotIncludeSymbolChain | SymbolFormatFlags.AllowAnyNodeKind); + } + /** + * Resolves a qualified name and any involved aliases. + */ + function resolveEntityName(name: EntityNameOrEntityNameExpression, meaning: SymbolFlags, ignoreErrors?: boolean, dontResolveAlias?: boolean, location?: Node): ts.Symbol | undefined { + if (nodeIsMissing(name)) { + return undefined; } - - function resolveSymbol(symbol: Symbol, dontResolveAlias?: boolean): Symbol; - function resolveSymbol(symbol: Symbol | undefined, dontResolveAlias?: boolean): Symbol | undefined; - function resolveSymbol(symbol: Symbol | undefined, dontResolveAlias?: boolean): Symbol | undefined { - return !dontResolveAlias && isNonLocalAlias(symbol) ? resolveAlias(symbol) : symbol; + const namespaceMeaning = SymbolFlags.Namespace | (isInJSFile(name) ? meaning & SymbolFlags.Value : 0); + let symbol: ts.Symbol | undefined; + if (name.kind === SyntaxKind.Identifier) { + const message = meaning === namespaceMeaning || nodeIsSynthesized(name) ? Diagnostics.Cannot_find_namespace_0 : getCannotFindNameDiagnosticForName(getFirstIdentifier(name)); + const symbolFromJSPrototype = isInJSFile(name) && !nodeIsSynthesized(name) ? resolveEntityNameFromAssignmentDeclaration(name, meaning) : undefined; + symbol = resolveName(location || name, name.escapedText, meaning, ignoreErrors || symbolFromJSPrototype ? undefined : message, name, /*isUse*/ true); + if (!symbol) { + return symbolFromJSPrototype; + } } - - function resolveAlias(symbol: Symbol): Symbol { - Debug.assert((symbol.flags & SymbolFlags.Alias) !== 0, "Should only get Alias here."); - const links = getSymbolLinks(symbol); - if (!links.target) { - links.target = resolvingSymbol; - const node = getDeclarationOfAliasSymbol(symbol); - if (!node) return Debug.fail(); - const target = getTargetOfAliasDeclaration(node); - if (links.target === resolvingSymbol) { - links.target = target || unknownSymbol; - } - else { - error(node, Diagnostics.Circular_definition_of_import_alias_0, symbolToString(symbol)); + else if (name.kind === SyntaxKind.QualifiedName || name.kind === SyntaxKind.PropertyAccessExpression) { + const left = name.kind === SyntaxKind.QualifiedName ? name.left : name.expression; + const right = name.kind === SyntaxKind.QualifiedName ? name.right : name.name; + let namespace = resolveEntityName(left, namespaceMeaning, ignoreErrors, /*dontResolveAlias*/ false, location); + if (!namespace || nodeIsMissing(right)) { + return undefined; + } + else if (namespace === unknownSymbol) { + return namespace; + } + if (isInJSFile(name)) { + if (namespace.valueDeclaration && + isVariableDeclaration(namespace.valueDeclaration) && + namespace.valueDeclaration.initializer && + isCommonJsRequire(namespace.valueDeclaration.initializer)) { + const moduleName = ((namespace.valueDeclaration.initializer as CallExpression).arguments[0] as StringLiteral); + const moduleSym = resolveExternalModuleName(moduleName, moduleName); + if (moduleSym) { + const resolvedModuleSymbol = resolveExternalModuleSymbol(moduleSym); + if (resolvedModuleSymbol) { + namespace = resolvedModuleSymbol; + } + } } } - else if (links.target === resolvingSymbol) { - links.target = unknownSymbol; + symbol = getSymbol(getExportsOfSymbol(namespace), right.escapedText, meaning); + if (!symbol) { + if (!ignoreErrors) { + error(right, Diagnostics.Namespace_0_has_no_exported_member_1, getFullyQualifiedName(namespace), declarationNameToString(right)); + } + return undefined; } - return links.target; } - - /** - * Marks a symbol as type-only if its declaration is syntactically type-only. - * If it is not itself marked type-only, but resolves to a type-only alias - * somewhere in its resolution chain, save a reference to the type-only alias declaration - * so the alias _not_ marked type-only can be identified as _transitively_ type-only. - * - * This function is called on each alias declaration that could be type-only or resolve to - * another type-only alias during `resolveAlias`, so that later, when an alias is used in a - * JS-emitting expression, we can quickly determine if that symbol is effectively type-only - * and issue an error if so. - * - * @param aliasDeclaration The alias declaration not marked as type-only - * has already been marked as not resolving to a type-only alias. Used when recursively resolving qualified - * names of import aliases, e.g. `import C = a.b.C`. If namespace `a` is not found to be type-only, the - * import declaration will initially be marked as not resolving to a type-only symbol. But, namespace `b` - * must still be checked for a type-only marker, overwriting the previous negative result if found. - * @param immediateTarget The symbol to which the alias declaration immediately resolves - * @param finalTarget The symbol to which the alias declaration ultimately resolves - * @param overwriteEmpty Checks `resolvesToSymbol` for type-only declarations even if `aliasDeclaration` - */ - function markSymbolOfAliasDeclarationIfTypeOnly( - aliasDeclaration: Declaration | undefined, - immediateTarget: Symbol | undefined, - finalTarget: Symbol | undefined, - overwriteEmpty: boolean, - ): boolean { - if (!aliasDeclaration) return false; - - // If the declaration itself is type-only, mark it and return. - // No need to check what it resolves to. - const sourceSymbol = getSymbolOfNode(aliasDeclaration); - if (isTypeOnlyImportOrExportDeclaration(aliasDeclaration)) { - const links = getSymbolLinks(sourceSymbol); - links.typeOnlyDeclaration = aliasDeclaration; - return true; - } - - const links = getSymbolLinks(sourceSymbol); - return markSymbolOfAliasDeclarationIfTypeOnlyWorker(links, immediateTarget, overwriteEmpty) - || markSymbolOfAliasDeclarationIfTypeOnlyWorker(links, finalTarget, overwriteEmpty); + else { + throw Debug.assertNever(name, "Unknown entity name kind."); } - - function markSymbolOfAliasDeclarationIfTypeOnlyWorker(aliasDeclarationLinks: SymbolLinks, target: Symbol | undefined, overwriteEmpty: boolean): boolean { - if (target && (aliasDeclarationLinks.typeOnlyDeclaration === undefined || overwriteEmpty && aliasDeclarationLinks.typeOnlyDeclaration === false)) { - const exportSymbol = target.exports?.get(InternalSymbolName.ExportEquals) ?? target; - const typeOnly = exportSymbol.declarations && find(exportSymbol.declarations, isTypeOnlyImportOrExportDeclaration); - aliasDeclarationLinks.typeOnlyDeclaration = typeOnly ?? getSymbolLinks(exportSymbol).typeOnlyDeclaration ?? false; - } - return !!aliasDeclarationLinks.typeOnlyDeclaration; + Debug.assert((getCheckFlags(symbol) & CheckFlags.Instantiated) === 0, "Should never get an instantiated symbol here."); + if (!nodeIsSynthesized(name) && isEntityName(name) && (symbol.flags & SymbolFlags.Alias || name.parent.kind === SyntaxKind.ExportAssignment)) { + markSymbolOfAliasDeclarationIfTypeOnly(getAliasDeclarationFromName(name), symbol, /*finalTarget*/ undefined, /*overwriteEmpty*/ true); } - - /** Indicates that a symbol directly or indirectly resolves to a type-only import or export. */ - function getTypeOnlyAliasDeclaration(symbol: Symbol): TypeOnlyCompatibleAliasDeclaration | undefined { - if (!(symbol.flags & SymbolFlags.Alias)) { - return undefined; + return (symbol.flags & meaning) || dontResolveAlias ? symbol : resolveAlias(symbol); + } + /** + * 1. For prototype-property methods like `A.prototype.m = function () ...`, try to resolve names in the scope of `A` too. + * Note that prototype-property assignment to locations outside the current file (eg globals) doesn't work, so + * name resolution won't work either. + * 2. For property assignments like `{ x: function f () { } }`, try to resolve names in the scope of `f` too. + */ + function resolveEntityNameFromAssignmentDeclaration(name: Identifier, meaning: SymbolFlags) { + if (isJSDocTypeReference(name.parent)) { + const secondaryLocation = getAssignmentDeclarationLocation(name.parent); + if (secondaryLocation) { + return resolveName(secondaryLocation, name.escapedText, meaning, /*nameNotFoundMessage*/ undefined, name, /*isUse*/ true); } - const links = getSymbolLinks(symbol); - return links.typeOnlyDeclaration || undefined; } - - function markExportAsReferenced(node: ImportEqualsDeclaration | ExportSpecifier) { - const symbol = getSymbolOfNode(node); - const target = resolveAlias(symbol); - if (target) { - const markAlias = target === unknownSymbol || - ((target.flags & SymbolFlags.Value) && !isConstEnumOrConstEnumOnlyModule(target) && !getTypeOnlyAliasDeclaration(symbol)); - - if (markAlias) { - markAliasSymbolAsReferenced(symbol); - } + } + function getAssignmentDeclarationLocation(node: TypeReferenceNode): Node | undefined { + const typeAlias = findAncestor(node, node => !(isJSDocNode(node) || node.flags & NodeFlags.JSDoc) ? "quit" : isJSDocTypeAlias(node)); + if (typeAlias) { + return; + } + const host = getJSDocHost(node); + if (isExpressionStatement(host) && + isBinaryExpression(host.expression) && + getAssignmentDeclarationKind(host.expression) === AssignmentDeclarationKind.PrototypeProperty) { + // X.prototype.m = /** @param {K} p */ function () { } <-- look for K on X's declaration + const symbol = getSymbolOfNode(host.expression.left); + if (symbol) { + return getDeclarationOfJSPrototypeContainer(symbol); } } - - // When an alias symbol is referenced, we need to mark the entity it references as referenced and in turn repeat that until - // we reach a non-alias or an exported entity (which is always considered referenced). We do this by checking the target of - // the alias as an expression (which recursively takes us back here if the target references another alias). - function markAliasSymbolAsReferenced(symbol: Symbol) { - const links = getSymbolLinks(symbol); - if (!links.referenced) { - links.referenced = true; - const node = getDeclarationOfAliasSymbol(symbol); - if (!node) return Debug.fail(); - // We defer checking of the reference of an `import =` until the import itself is referenced, - // This way a chain of imports can be elided if ultimately the final input is only used in a type - // position. - if (isInternalModuleImportEqualsDeclaration(node)) { - const target = resolveSymbol(symbol); - if (target === unknownSymbol || target.flags & SymbolFlags.Value) { - // import foo = - checkExpressionCached(node.moduleReference); - } - } + if ((isObjectLiteralMethod(host) || isPropertyAssignment(host)) && + isBinaryExpression(host.parent.parent) && + getAssignmentDeclarationKind(host.parent.parent) === AssignmentDeclarationKind.Prototype) { + // X.prototype = { /** @param {K} p */m() { } } <-- look for K on X's declaration + const symbol = getSymbolOfNode(host.parent.parent.left); + if (symbol) { + return getDeclarationOfJSPrototypeContainer(symbol); } } - - // Aliases that resolve to const enums are not marked as referenced because they are not emitted, - // but their usage in value positions must be tracked to determine if the import can be type-only. - function markConstEnumAliasAsReferenced(symbol: Symbol) { - const links = getSymbolLinks(symbol); - if (!links.constEnumReferenced) { - links.constEnumReferenced = true; - } + const sig = getHostSignatureFromJSDocHost(host); + if (sig) { + const symbol = getSymbolOfNode(sig); + return symbol && symbol.valueDeclaration; } - - // This function is only for imports with entity names - function getSymbolOfPartOfRightHandSideOfImportEquals(entityName: EntityName, dontResolveAlias?: boolean): Symbol | undefined { - // There are three things we might try to look for. In the following examples, - // the search term is enclosed in |...|: - // - // import a = |b|; // Namespace - // import a = |b.c|; // Value, type, namespace - // import a = |b.c|.d; // Namespace - if (entityName.kind === SyntaxKind.Identifier && isRightSideOfQualifiedNameOrPropertyAccess(entityName)) { - entityName = entityName.parent; + } + function getDeclarationOfJSPrototypeContainer(symbol: ts.Symbol) { + const decl = symbol.parent!.valueDeclaration; + if (!decl) { + return undefined; + } + const initializer = isAssignmentDeclaration(decl) ? getAssignedExpandoInitializer(decl) : + hasOnlyExpressionInitializer(decl) ? getDeclaredExpandoInitializer(decl) : + undefined; + return initializer || decl; + } + /** + * Get the real symbol of a declaration with an expando initializer. + * + * Normally, declarations have an associated symbol, but when a declaration has an expando + * initializer, the expando's symbol is the one that has all the members merged into it. + */ + function getExpandoSymbol(symbol: ts.Symbol): ts.Symbol | undefined { + const decl = symbol.valueDeclaration; + if (!decl || !isInJSFile(decl) || symbol.flags & SymbolFlags.TypeAlias || getExpandoInitializer(decl, /*isPrototypeAssignment*/ false)) { + return undefined; + } + const init = isVariableDeclaration(decl) ? getDeclaredExpandoInitializer(decl) : getAssignedExpandoInitializer(decl); + if (init) { + const initSymbol = getSymbolOfNode(init); + if (initSymbol) { + return mergeJSSymbols(initSymbol, symbol); } - // Check for case 1 and 3 in the above example - if (entityName.kind === SyntaxKind.Identifier || entityName.parent.kind === SyntaxKind.QualifiedName) { - return resolveEntityName(entityName, SymbolFlags.Namespace, /*ignoreErrors*/ false, dontResolveAlias); + } + } + function resolveExternalModuleName(location: Node, moduleReferenceExpression: Expression, ignoreErrors?: boolean): ts.Symbol | undefined { + return resolveExternalModuleNameWorker(location, moduleReferenceExpression, ignoreErrors ? undefined : Diagnostics.Cannot_find_module_0); + } + function resolveExternalModuleNameWorker(location: Node, moduleReferenceExpression: Expression, moduleNotFoundError: DiagnosticMessage | undefined, isForAugmentation = false): ts.Symbol | undefined { + return isStringLiteralLike(moduleReferenceExpression) + ? resolveExternalModule(location, moduleReferenceExpression.text, moduleNotFoundError, moduleReferenceExpression, isForAugmentation) + : undefined; + } + function resolveExternalModule(location: Node, moduleReference: string, moduleNotFoundError: DiagnosticMessage | undefined, errorNode: Node, isForAugmentation = false): ts.Symbol | undefined { + if (startsWith(moduleReference, "@types/")) { + const diag = Diagnostics.Cannot_import_type_declaration_files_Consider_importing_0_instead_of_1; + const withoutAtTypePrefix = removePrefix(moduleReference, "@types/"); + error(errorNode, diag, withoutAtTypePrefix, moduleReference); + } + const ambientModule = tryFindAmbientModule(moduleReference, /*withAugmentations*/ true); + if (ambientModule) { + return ambientModule; + } + const currentSourceFile = getSourceFileOfNode(location); + const resolvedModule = (getResolvedModule(currentSourceFile, moduleReference)!); // TODO: GH#18217 + const resolutionDiagnostic = resolvedModule && getResolutionDiagnostic(compilerOptions, resolvedModule); + const sourceFile = resolvedModule && !resolutionDiagnostic && host.getSourceFile(resolvedModule.resolvedFileName); + if (sourceFile) { + if (sourceFile.symbol) { + if (resolvedModule.isExternalLibraryImport && !resolutionExtensionIsTSOrJson(resolvedModule.extension)) { + errorOnImplicitAnyModule(/*isError*/ false, errorNode, resolvedModule, moduleReference); + } + // merged symbol is module declaration symbol combined with all augmentations + return getMergedSymbol(sourceFile.symbol); } - else { - // Case 2 in above example - // entityName.kind could be a QualifiedName or a Missing identifier - Debug.assert(entityName.parent.kind === SyntaxKind.ImportEqualsDeclaration); - return resolveEntityName(entityName, SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace, /*ignoreErrors*/ false, dontResolveAlias); + if (moduleNotFoundError) { + // report errors only if it was requested + error(errorNode, Diagnostics.File_0_is_not_a_module, sourceFile.fileName); } + return undefined; } - - function getFullyQualifiedName(symbol: Symbol, containingLocation?: Node): string { - return symbol.parent ? getFullyQualifiedName(symbol.parent, containingLocation) + "." + symbolToString(symbol) : symbolToString(symbol, containingLocation, /*meaning*/ undefined, SymbolFormatFlags.DoNotIncludeSymbolChain | SymbolFormatFlags.AllowAnyNodeKind); - } - - /** - * Resolves a qualified name and any involved aliases. - */ - function resolveEntityName(name: EntityNameOrEntityNameExpression, meaning: SymbolFlags, ignoreErrors?: boolean, dontResolveAlias?: boolean, location?: Node): Symbol | undefined { - if (nodeIsMissing(name)) { - return undefined; - } - - const namespaceMeaning = SymbolFlags.Namespace | (isInJSFile(name) ? meaning & SymbolFlags.Value : 0); - let symbol: Symbol | undefined; - if (name.kind === SyntaxKind.Identifier) { - const message = meaning === namespaceMeaning || nodeIsSynthesized(name) ? Diagnostics.Cannot_find_namespace_0 : getCannotFindNameDiagnosticForName(getFirstIdentifier(name)); - const symbolFromJSPrototype = isInJSFile(name) && !nodeIsSynthesized(name) ? resolveEntityNameFromAssignmentDeclaration(name, meaning) : undefined; - symbol = resolveName(location || name, name.escapedText, meaning, ignoreErrors || symbolFromJSPrototype ? undefined : message, name, /*isUse*/ true); - if (!symbol) { - return symbolFromJSPrototype; + if (patternAmbientModules) { + const pattern = findBestPatternMatch(patternAmbientModules, _ => _.pattern, moduleReference); + if (pattern) { + // If the module reference matched a pattern ambient module ('*.foo') but there's also a + // module augmentation by the specific name requested ('a.foo'), we store the merged symbol + // by the augmentation name ('a.foo'), because asking for *.foo should not give you exports + // from a.foo. + const augmentation = patternAmbientModuleAugmentations && patternAmbientModuleAugmentations.get(moduleReference); + if (augmentation) { + return getMergedSymbol(augmentation); } + return getMergedSymbol(pattern.symbol); } - else if (name.kind === SyntaxKind.QualifiedName || name.kind === SyntaxKind.PropertyAccessExpression) { - const left = name.kind === SyntaxKind.QualifiedName ? name.left : name.expression; - const right = name.kind === SyntaxKind.QualifiedName ? name.right : name.name; - let namespace = resolveEntityName(left, namespaceMeaning, ignoreErrors, /*dontResolveAlias*/ false, location); - if (!namespace || nodeIsMissing(right)) { - return undefined; - } - else if (namespace === unknownSymbol) { - return namespace; - } - if (isInJSFile(name)) { - if (namespace.valueDeclaration && - isVariableDeclaration(namespace.valueDeclaration) && - namespace.valueDeclaration.initializer && - isCommonJsRequire(namespace.valueDeclaration.initializer)) { - const moduleName = (namespace.valueDeclaration.initializer as CallExpression).arguments[0] as StringLiteral; - const moduleSym = resolveExternalModuleName(moduleName, moduleName); - if (moduleSym) { - const resolvedModuleSymbol = resolveExternalModuleSymbol(moduleSym); - if (resolvedModuleSymbol) { - namespace = resolvedModuleSymbol; - } - } - } - } - symbol = getSymbol(getExportsOfSymbol(namespace), right.escapedText, meaning); - if (!symbol) { - if (!ignoreErrors) { - error(right, Diagnostics.Namespace_0_has_no_exported_member_1, getFullyQualifiedName(namespace), declarationNameToString(right)); - } - return undefined; - } + } + // May be an untyped module. If so, ignore resolutionDiagnostic. + if (resolvedModule && !resolutionExtensionIsTSOrJson(resolvedModule.extension) && resolutionDiagnostic === undefined || resolutionDiagnostic === Diagnostics.Could_not_find_a_declaration_file_for_module_0_1_implicitly_has_an_any_type) { + if (isForAugmentation) { + const diag = Diagnostics.Invalid_module_name_in_augmentation_Module_0_resolves_to_an_untyped_module_at_1_which_cannot_be_augmented; + error(errorNode, diag, moduleReference, resolvedModule.resolvedFileName); } else { - throw Debug.assertNever(name, "Unknown entity name kind."); + errorOnImplicitAnyModule(/*isError*/ noImplicitAny && !!moduleNotFoundError, errorNode, resolvedModule, moduleReference); } - Debug.assert((getCheckFlags(symbol) & CheckFlags.Instantiated) === 0, "Should never get an instantiated symbol here."); - if (!nodeIsSynthesized(name) && isEntityName(name) && (symbol.flags & SymbolFlags.Alias || name.parent.kind === SyntaxKind.ExportAssignment)) { - markSymbolOfAliasDeclarationIfTypeOnly(getAliasDeclarationFromName(name), symbol, /*finalTarget*/ undefined, /*overwriteEmpty*/ true); - } - return (symbol.flags & meaning) || dontResolveAlias ? symbol : resolveAlias(symbol); + // Failed imports and untyped modules are both treated in an untyped manner; only difference is whether we give a diagnostic first. + return undefined; } - - /** - * 1. For prototype-property methods like `A.prototype.m = function () ...`, try to resolve names in the scope of `A` too. - * Note that prototype-property assignment to locations outside the current file (eg globals) doesn't work, so - * name resolution won't work either. - * 2. For property assignments like `{ x: function f () { } }`, try to resolve names in the scope of `f` too. - */ - function resolveEntityNameFromAssignmentDeclaration(name: Identifier, meaning: SymbolFlags) { - if (isJSDocTypeReference(name.parent)) { - const secondaryLocation = getAssignmentDeclarationLocation(name.parent); - if (secondaryLocation) { - return resolveName(secondaryLocation, name.escapedText, meaning, /*nameNotFoundMessage*/ undefined, name, /*isUse*/ true); + if (moduleNotFoundError) { + // See if this was possibly a projectReference redirect + if (resolvedModule) { + const redirect = host.getProjectReferenceRedirect(resolvedModule.resolvedFileName); + if (redirect) { + error(errorNode, Diagnostics.Output_file_0_has_not_been_built_from_source_file_1, redirect, resolvedModule.resolvedFileName); + return undefined; } } - } - - function getAssignmentDeclarationLocation(node: TypeReferenceNode): Node | undefined { - const typeAlias = findAncestor(node, node => !(isJSDocNode(node) || node.flags & NodeFlags.JSDoc) ? "quit" : isJSDocTypeAlias(node)); - if (typeAlias) { - return; + if (resolutionDiagnostic) { + error(errorNode, resolutionDiagnostic, moduleReference, resolvedModule.resolvedFileName); } - const host = getJSDocHost(node); - if (isExpressionStatement(host) && - isBinaryExpression(host.expression) && - getAssignmentDeclarationKind(host.expression) === AssignmentDeclarationKind.PrototypeProperty) { - // X.prototype.m = /** @param {K} p */ function () { } <-- look for K on X's declaration - const symbol = getSymbolOfNode(host.expression.left); - if (symbol) { - return getDeclarationOfJSPrototypeContainer(symbol); + else { + const tsExtension = tryExtractTSExtension(moduleReference); + if (tsExtension) { + const diag = Diagnostics.An_import_path_cannot_end_with_a_0_extension_Consider_importing_1_instead; + error(errorNode, diag, tsExtension, removeExtension(moduleReference, tsExtension)); } - } - if ((isObjectLiteralMethod(host) || isPropertyAssignment(host)) && - isBinaryExpression(host.parent.parent) && - getAssignmentDeclarationKind(host.parent.parent) === AssignmentDeclarationKind.Prototype) { - // X.prototype = { /** @param {K} p */m() { } } <-- look for K on X's declaration - const symbol = getSymbolOfNode(host.parent.parent.left); - if (symbol) { - return getDeclarationOfJSPrototypeContainer(symbol); + else if (!compilerOptions.resolveJsonModule && + fileExtensionIs(moduleReference, Extension.Json) && + getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs && + hasJsonModuleEmitEnabled(compilerOptions)) { + error(errorNode, Diagnostics.Cannot_find_module_0_Consider_using_resolveJsonModule_to_import_module_with_json_extension, moduleReference); + } + else { + error(errorNode, moduleNotFoundError, moduleReference); } - } - const sig = getHostSignatureFromJSDocHost(host); - if (sig) { - const symbol = getSymbolOfNode(sig); - return symbol && symbol.valueDeclaration; } } - - function getDeclarationOfJSPrototypeContainer(symbol: Symbol) { - const decl = symbol.parent!.valueDeclaration; - if (!decl) { - return undefined; - } - const initializer = isAssignmentDeclaration(decl) ? getAssignedExpandoInitializer(decl) : - hasOnlyExpressionInitializer(decl) ? getDeclaredExpandoInitializer(decl) : - undefined; - return initializer || decl; + return undefined; + } + function errorOnImplicitAnyModule(isError: boolean, errorNode: Node, { packageId, resolvedFileName }: ResolvedModuleFull, moduleReference: string): void { + const errorInfo = !isExternalModuleNameRelative(moduleReference) && packageId + ? typesPackageExists(packageId.name) + ? chainDiagnosticMessages( + /*details*/ undefined, Diagnostics.If_the_0_package_actually_exposes_this_module_consider_sending_a_pull_request_to_amend_https_Colon_Slash_Slashgithub_com_SlashDefinitelyTyped_SlashDefinitelyTyped_Slashtree_Slashmaster_Slashtypes_Slash_1, packageId.name, mangleScopedPackageName(packageId.name)) + : chainDiagnosticMessages( + /*details*/ undefined, Diagnostics.Try_npm_install_types_Slash_1_if_it_exists_or_add_a_new_declaration_d_ts_file_containing_declare_module_0, moduleReference, mangleScopedPackageName(packageId.name)) + : undefined; + errorOrSuggestion(isError, errorNode, chainDiagnosticMessages(errorInfo, Diagnostics.Could_not_find_a_declaration_file_for_module_0_1_implicitly_has_an_any_type, moduleReference, resolvedFileName)); + } + function typesPackageExists(packageName: string): boolean { + return getPackagesSet().has(getTypesPackageName(packageName)); + } + function resolveExternalModuleSymbol(moduleSymbol: ts.Symbol, dontResolveAlias?: boolean): ts.Symbol; + function resolveExternalModuleSymbol(moduleSymbol: ts.Symbol | undefined, dontResolveAlias?: boolean): ts.Symbol | undefined; + function resolveExternalModuleSymbol(moduleSymbol: ts.Symbol, dontResolveAlias?: boolean): ts.Symbol { + if (moduleSymbol) { + const exportEquals = resolveSymbol(moduleSymbol.exports!.get(InternalSymbolName.ExportEquals), dontResolveAlias); + const exported = getCommonJsExportEquals(getMergedSymbol(exportEquals), getMergedSymbol(moduleSymbol)); + return getMergedSymbol(exported) || moduleSymbol; + } + return undefined!; + } + function getCommonJsExportEquals(exported: ts.Symbol | undefined, moduleSymbol: ts.Symbol): ts.Symbol | undefined { + if (!exported || exported === unknownSymbol || exported === moduleSymbol || moduleSymbol.exports!.size === 1 || exported.flags & SymbolFlags.Alias) { + return exported; } - - /** - * Get the real symbol of a declaration with an expando initializer. - * - * Normally, declarations have an associated symbol, but when a declaration has an expando - * initializer, the expando's symbol is the one that has all the members merged into it. - */ - function getExpandoSymbol(symbol: Symbol): Symbol | undefined { - const decl = symbol.valueDeclaration; - if (!decl || !isInJSFile(decl) || symbol.flags & SymbolFlags.TypeAlias || getExpandoInitializer(decl, /*isPrototypeAssignment*/ false)) { - return undefined; + const links = getSymbolLinks(exported); + if (links.cjsExportMerged) { + return links.cjsExportMerged; + } + const merged = exported.flags & SymbolFlags.Transient ? exported : cloneSymbol(exported); + merged.flags = merged.flags | SymbolFlags.ValueModule; + if (merged.exports === undefined) { + merged.exports = createSymbolTable(); + } + moduleSymbol.exports!.forEach((s, name) => { + if (name === InternalSymbolName.ExportEquals) + return; + merged.exports!.set(name, merged.exports!.has(name) ? mergeSymbol(merged.exports!.get(name)!, s) : s); + }); + getSymbolLinks(merged).cjsExportMerged = merged; + return links.cjsExportMerged = merged; + } + // An external module with an 'export =' declaration may be referenced as an ES6 module provided the 'export =' + // references a symbol that is at least declared as a module or a variable. The target of the 'export =' may + // combine other declarations with the module or variable (e.g. a class/module, function/module, interface/variable). + function resolveESModuleSymbol(moduleSymbol: ts.Symbol | undefined, referencingLocation: Node, dontResolveAlias: boolean, suppressInteropError: boolean): ts.Symbol | undefined { + const symbol = resolveExternalModuleSymbol(moduleSymbol, dontResolveAlias); + if (!dontResolveAlias && symbol) { + if (!suppressInteropError && !(symbol.flags & (SymbolFlags.Module | SymbolFlags.Variable)) && !getDeclarationOfKind(symbol, SyntaxKind.SourceFile)) { + const compilerOptionName = moduleKind >= ModuleKind.ES2015 + ? "allowSyntheticDefaultImports" + : "esModuleInterop"; + error(referencingLocation, Diagnostics.This_module_can_only_be_referenced_with_ECMAScript_imports_Slashexports_by_turning_on_the_0_flag_and_referencing_its_default_export, compilerOptionName); + return symbol; } - const init = isVariableDeclaration(decl) ? getDeclaredExpandoInitializer(decl) : getAssignedExpandoInitializer(decl); - if (init) { - const initSymbol = getSymbolOfNode(init); - if (initSymbol) { - return mergeJSSymbols(initSymbol, symbol); + if (compilerOptions.esModuleInterop) { + const referenceParent = referencingLocation.parent; + if ((isImportDeclaration(referenceParent) && getNamespaceDeclarationNode(referenceParent)) || + isImportCall(referenceParent)) { + const type = getTypeOfSymbol(symbol); + let sigs = getSignaturesOfStructuredType(type, SignatureKind.Call); + if (!sigs || !sigs.length) { + sigs = getSignaturesOfStructuredType(type, SignatureKind.Construct); + } + if (sigs && sigs.length) { + const moduleType = getTypeWithSyntheticDefaultImportType(type, symbol, moduleSymbol!); + // Create a new symbol which has the module's type less the call and construct signatures + const result = createSymbol(symbol.flags, symbol.escapedName); + result.declarations = symbol.declarations ? symbol.declarations.slice() : []; + result.parent = symbol.parent; + result.target = symbol; + result.originatingImport = referenceParent; + if (symbol.valueDeclaration) + result.valueDeclaration = symbol.valueDeclaration; + if (symbol.constEnumOnlyModule) + result.constEnumOnlyModule = true; + if (symbol.members) + result.members = cloneMap(symbol.members); + if (symbol.exports) + result.exports = cloneMap(symbol.exports); + const resolvedModuleType = resolveStructuredTypeMembers((moduleType as StructuredType)); // Should already be resolved from the signature checks above + result.type = createAnonymousType(result, resolvedModuleType.members, emptyArray, emptyArray, resolvedModuleType.stringIndexInfo, resolvedModuleType.numberIndexInfo); + return result; + } } } } - - - function resolveExternalModuleName(location: Node, moduleReferenceExpression: Expression, ignoreErrors?: boolean): Symbol | undefined { - return resolveExternalModuleNameWorker(location, moduleReferenceExpression, ignoreErrors ? undefined : Diagnostics.Cannot_find_module_0); + return symbol; + } + function hasExportAssignmentSymbol(moduleSymbol: ts.Symbol): boolean { + return moduleSymbol.exports!.get(InternalSymbolName.ExportEquals) !== undefined; + } + function getExportsOfModuleAsArray(moduleSymbol: ts.Symbol): ts.Symbol[] { + return symbolsToArray(getExportsOfModule(moduleSymbol)); + } + function getExportsAndPropertiesOfModule(moduleSymbol: ts.Symbol): ts.Symbol[] { + const exports = getExportsOfModuleAsArray(moduleSymbol); + const exportEquals = resolveExternalModuleSymbol(moduleSymbol); + if (exportEquals !== moduleSymbol) { + addRange(exports, getPropertiesOfType(getTypeOfSymbol(exportEquals))); } - - function resolveExternalModuleNameWorker(location: Node, moduleReferenceExpression: Expression, moduleNotFoundError: DiagnosticMessage | undefined, isForAugmentation = false): Symbol | undefined { - return isStringLiteralLike(moduleReferenceExpression) - ? resolveExternalModule(location, moduleReferenceExpression.text, moduleNotFoundError, moduleReferenceExpression, isForAugmentation) - : undefined; + return exports; + } + function tryGetMemberInModuleExports(memberName: __String, moduleSymbol: ts.Symbol): ts.Symbol | undefined { + const symbolTable = getExportsOfModule(moduleSymbol); + if (symbolTable) { + return symbolTable.get(memberName); } - - function resolveExternalModule(location: Node, moduleReference: string, moduleNotFoundError: DiagnosticMessage | undefined, errorNode: Node, isForAugmentation = false): Symbol | undefined { - if (startsWith(moduleReference, "@types/")) { - const diag = Diagnostics.Cannot_import_type_declaration_files_Consider_importing_0_instead_of_1; - const withoutAtTypePrefix = removePrefix(moduleReference, "@types/"); - error(errorNode, diag, withoutAtTypePrefix, moduleReference); - } - - const ambientModule = tryFindAmbientModule(moduleReference, /*withAugmentations*/ true); - if (ambientModule) { - return ambientModule; - } - const currentSourceFile = getSourceFileOfNode(location); - const resolvedModule = getResolvedModule(currentSourceFile, moduleReference)!; // TODO: GH#18217 - const resolutionDiagnostic = resolvedModule && getResolutionDiagnostic(compilerOptions, resolvedModule); - const sourceFile = resolvedModule && !resolutionDiagnostic && host.getSourceFile(resolvedModule.resolvedFileName); - if (sourceFile) { - if (sourceFile.symbol) { - if (resolvedModule.isExternalLibraryImport && !resolutionExtensionIsTSOrJson(resolvedModule.extension)) { - errorOnImplicitAnyModule(/*isError*/ false, errorNode, resolvedModule, moduleReference); - } - // merged symbol is module declaration symbol combined with all augmentations - return getMergedSymbol(sourceFile.symbol); - } - if (moduleNotFoundError) { - // report errors only if it was requested - error(errorNode, Diagnostics.File_0_is_not_a_module, sourceFile.fileName); - } - return undefined; - } - - if (patternAmbientModules) { - const pattern = findBestPatternMatch(patternAmbientModules, _ => _.pattern, moduleReference); - if (pattern) { - // If the module reference matched a pattern ambient module ('*.foo') but there's also a - // module augmentation by the specific name requested ('a.foo'), we store the merged symbol - // by the augmentation name ('a.foo'), because asking for *.foo should not give you exports - // from a.foo. - const augmentation = patternAmbientModuleAugmentations && patternAmbientModuleAugmentations.get(moduleReference); - if (augmentation) { - return getMergedSymbol(augmentation); - } - return getMergedSymbol(pattern.symbol); + } + function tryGetMemberInModuleExportsAndProperties(memberName: __String, moduleSymbol: ts.Symbol): ts.Symbol | undefined { + const symbol = tryGetMemberInModuleExports(memberName, moduleSymbol); + if (symbol) { + return symbol; + } + const exportEquals = resolveExternalModuleSymbol(moduleSymbol); + if (exportEquals === moduleSymbol) { + return undefined; + } + const type = getTypeOfSymbol(exportEquals); + return type.flags & TypeFlags.Primitive || + getObjectFlags(type) & ObjectFlags.Class || + isArrayOrTupleLikeType(type) + ? undefined + : getPropertyOfType(type, memberName); + } + function getExportsOfSymbol(symbol: ts.Symbol): SymbolTable { + return symbol.flags & SymbolFlags.LateBindingContainer ? getResolvedMembersOrExportsOfSymbol(symbol, MembersOrExportsResolutionKind.resolvedExports) : + symbol.flags & SymbolFlags.Module ? getExportsOfModule(symbol) : + symbol.exports || emptySymbols; + } + function getExportsOfModule(moduleSymbol: ts.Symbol): SymbolTable { + const links = getSymbolLinks(moduleSymbol); + return links.resolvedExports || (links.resolvedExports = getExportsOfModuleWorker(moduleSymbol)); + } + interface ExportCollisionTracker { + specifierText: string; + exportsWithDuplicate: ExportDeclaration[]; + } + type ExportCollisionTrackerTable = UnderscoreEscapedMap; + /** + * Extends one symbol table with another while collecting information on name collisions for error message generation into the `lookupTable` argument + * Not passing `lookupTable` and `exportNode` disables this collection, and just extends the tables + */ + function extendExportSymbols(target: SymbolTable, source: SymbolTable | undefined, lookupTable?: ExportCollisionTrackerTable, exportNode?: ExportDeclaration) { + if (!source) + return; + source.forEach((sourceSymbol, id) => { + if (id === InternalSymbolName.Default) + return; + const targetSymbol = target.get(id); + if (!targetSymbol) { + target.set(id, sourceSymbol); + if (lookupTable && exportNode) { + lookupTable.set(id, ({ + specifierText: getTextOfNode((exportNode.moduleSpecifier!)) + } as ExportCollisionTracker)); } } - - // May be an untyped module. If so, ignore resolutionDiagnostic. - if (resolvedModule && !resolutionExtensionIsTSOrJson(resolvedModule.extension) && resolutionDiagnostic === undefined || resolutionDiagnostic === Diagnostics.Could_not_find_a_declaration_file_for_module_0_1_implicitly_has_an_any_type) { - if (isForAugmentation) { - const diag = Diagnostics.Invalid_module_name_in_augmentation_Module_0_resolves_to_an_untyped_module_at_1_which_cannot_be_augmented; - error(errorNode, diag, moduleReference, resolvedModule.resolvedFileName); + else if (lookupTable && exportNode && targetSymbol && resolveSymbol(targetSymbol) !== resolveSymbol(sourceSymbol)) { + const collisionTracker = lookupTable.get(id)!; + if (!collisionTracker.exportsWithDuplicate) { + collisionTracker.exportsWithDuplicate = [exportNode]; } else { - errorOnImplicitAnyModule(/*isError*/ noImplicitAny && !!moduleNotFoundError, errorNode, resolvedModule, moduleReference); + collisionTracker.exportsWithDuplicate.push(exportNode); } - // Failed imports and untyped modules are both treated in an untyped manner; only difference is whether we give a diagnostic first. - return undefined; } - - if (moduleNotFoundError) { - // See if this was possibly a projectReference redirect - if (resolvedModule) { - const redirect = host.getProjectReferenceRedirect(resolvedModule.resolvedFileName); - if (redirect) { - error(errorNode, Diagnostics.Output_file_0_has_not_been_built_from_source_file_1, redirect, resolvedModule.resolvedFileName); - return undefined; - } - } - - if (resolutionDiagnostic) { - error(errorNode, resolutionDiagnostic, moduleReference, resolvedModule.resolvedFileName); - } - else { - const tsExtension = tryExtractTSExtension(moduleReference); - if (tsExtension) { - const diag = Diagnostics.An_import_path_cannot_end_with_a_0_extension_Consider_importing_1_instead; - error(errorNode, diag, tsExtension, removeExtension(moduleReference, tsExtension)); - } - else if (!compilerOptions.resolveJsonModule && - fileExtensionIs(moduleReference, Extension.Json) && - getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs && - hasJsonModuleEmitEnabled(compilerOptions)) { - error(errorNode, Diagnostics.Cannot_find_module_0_Consider_using_resolveJsonModule_to_import_module_with_json_extension, moduleReference); + }); + } + function getExportsOfModuleWorker(moduleSymbol: ts.Symbol): SymbolTable { + const visitedSymbols: ts.Symbol[] = []; + // A module defined by an 'export=' consists of one export that needs to be resolved + moduleSymbol = resolveExternalModuleSymbol(moduleSymbol); + return visit(moduleSymbol) || emptySymbols; + // The ES6 spec permits export * declarations in a module to circularly reference the module itself. For example, + // module 'a' can 'export * from "b"' and 'b' can 'export * from "a"' without error. + function visit(symbol: ts.Symbol | undefined): SymbolTable | undefined { + if (!(symbol && symbol.exports && pushIfUnique(visitedSymbols, symbol))) { + return; + } + const symbols = cloneMap(symbol.exports); + // All export * declarations are collected in an __export symbol by the binder + const exportStars = symbol.exports.get(InternalSymbolName.ExportStar); + if (exportStars) { + const nestedSymbols = createSymbolTable(); + const lookupTable = (createMap() as ExportCollisionTrackerTable); + for (const node of exportStars.declarations) { + const resolvedModule = resolveExternalModuleName(node, ((node as ExportDeclaration).moduleSpecifier!)); + const exportedSymbols = visit(resolvedModule); + extendExportSymbols(nestedSymbols, exportedSymbols, lookupTable, (node as ExportDeclaration)); + } + lookupTable.forEach(({ exportsWithDuplicate }, id) => { + // It's not an error if the file with multiple `export *`s with duplicate names exports a member with that name itself + if (id === "export=" || !(exportsWithDuplicate && exportsWithDuplicate.length) || symbols.has(id)) { + return; } - else { - error(errorNode, moduleNotFoundError, moduleReference); + for (const node of exportsWithDuplicate) { + diagnostics.add(createDiagnosticForNode(node, Diagnostics.Module_0_has_already_exported_a_member_named_1_Consider_explicitly_re_exporting_to_resolve_the_ambiguity, lookupTable.get(id)!.specifierText, unescapeLeadingUnderscores(id))); } - } + }); + extendExportSymbols(symbols, nestedSymbols); } - return undefined; + return symbols; } - - function errorOnImplicitAnyModule(isError: boolean, errorNode: Node, { packageId, resolvedFileName }: ResolvedModuleFull, moduleReference: string): void { - const errorInfo = !isExternalModuleNameRelative(moduleReference) && packageId - ? typesPackageExists(packageId.name) - ? chainDiagnosticMessages( - /*details*/ undefined, - Diagnostics.If_the_0_package_actually_exposes_this_module_consider_sending_a_pull_request_to_amend_https_Colon_Slash_Slashgithub_com_SlashDefinitelyTyped_SlashDefinitelyTyped_Slashtree_Slashmaster_Slashtypes_Slash_1, - packageId.name, mangleScopedPackageName(packageId.name)) - : chainDiagnosticMessages( - /*details*/ undefined, - Diagnostics.Try_npm_install_types_Slash_1_if_it_exists_or_add_a_new_declaration_d_ts_file_containing_declare_module_0, - moduleReference, - mangleScopedPackageName(packageId.name)) - : undefined; - errorOrSuggestion(isError, errorNode, chainDiagnosticMessages( - errorInfo, - Diagnostics.Could_not_find_a_declaration_file_for_module_0_1_implicitly_has_an_any_type, - moduleReference, - resolvedFileName)); + } + function getMergedSymbol(symbol: ts.Symbol): ts.Symbol; + function getMergedSymbol(symbol: ts.Symbol | undefined): ts.Symbol | undefined; + function getMergedSymbol(symbol: ts.Symbol | undefined): ts.Symbol | undefined { + let merged: ts.Symbol; + return symbol && symbol.mergeId && (merged = mergedSymbols[symbol.mergeId]) ? merged : symbol; + } + function getSymbolOfNode(node: Declaration): ts.Symbol; + function getSymbolOfNode(node: Node): ts.Symbol | undefined; + function getSymbolOfNode(node: Node): ts.Symbol | undefined { + return getMergedSymbol(node.symbol && getLateBoundSymbol(node.symbol)); + } + function getParentOfSymbol(symbol: ts.Symbol): ts.Symbol | undefined { + return getMergedSymbol(symbol.parent && getLateBoundSymbol(symbol.parent)); + } + function getAlternativeContainingModules(symbol: ts.Symbol, enclosingDeclaration: Node): ts.Symbol[] { + const containingFile = getSourceFileOfNode(enclosingDeclaration); + const id = "" + getNodeId(containingFile); + const links = getSymbolLinks(symbol); + let results: ts.Symbol[] | undefined; + if (links.extendedContainersByFile && (results = links.extendedContainersByFile.get(id))) { + return results; + } + if (containingFile && containingFile.imports) { + // Try to make an import using an import already in the enclosing file, if possible + for (const importRef of containingFile.imports) { + if (nodeIsSynthesized(importRef)) + continue; // Synthetic names can't be resolved by `resolveExternalModuleName` - they'll cause a debug assert if they error + const resolvedModule = resolveExternalModuleName(enclosingDeclaration, importRef, /*ignoreErrors*/ true); + if (!resolvedModule) + continue; + const ref = getAliasForSymbolInContainer(resolvedModule, symbol); + if (!ref) + continue; + results = append(results, resolvedModule); + } + if (length(results)) { + (links.extendedContainersByFile || (links.extendedContainersByFile = createMap())).set(id, (results!)); + return results!; + } } - function typesPackageExists(packageName: string): boolean { - return getPackagesSet().has(getTypesPackageName(packageName)); + if (links.extendedContainers) { + return links.extendedContainers; } - - function resolveExternalModuleSymbol(moduleSymbol: Symbol, dontResolveAlias?: boolean): Symbol; - function resolveExternalModuleSymbol(moduleSymbol: Symbol | undefined, dontResolveAlias?: boolean): Symbol | undefined; - function resolveExternalModuleSymbol(moduleSymbol: Symbol, dontResolveAlias?: boolean): Symbol { - if (moduleSymbol) { - const exportEquals = resolveSymbol(moduleSymbol.exports!.get(InternalSymbolName.ExportEquals), dontResolveAlias); - const exported = getCommonJsExportEquals(getMergedSymbol(exportEquals), getMergedSymbol(moduleSymbol)); - return getMergedSymbol(exported) || moduleSymbol; - } - return undefined!; + // No results from files already being imported by this file - expand search (expensive, but not location-specific, so cached) + const otherFiles = host.getSourceFiles(); + for (const file of otherFiles) { + if (!isExternalModule(file)) + continue; + const sym = getSymbolOfNode(file); + const ref = getAliasForSymbolInContainer(sym, symbol); + if (!ref) + continue; + results = append(results, sym); } - - function getCommonJsExportEquals(exported: Symbol | undefined, moduleSymbol: Symbol): Symbol | undefined { - if (!exported || exported === unknownSymbol || exported === moduleSymbol || moduleSymbol.exports!.size === 1 || exported.flags & SymbolFlags.Alias) { - return exported; - } - const links = getSymbolLinks(exported); - if (links.cjsExportMerged) { - return links.cjsExportMerged; - } - const merged = exported.flags & SymbolFlags.Transient ? exported : cloneSymbol(exported); - merged.flags = merged.flags | SymbolFlags.ValueModule; - if (merged.exports === undefined) { - merged.exports = createSymbolTable(); + return links.extendedContainers = results || emptyArray; + } + /** + * Attempts to find the symbol corresponding to the container a symbol is in - usually this + * is just its' `.parent`, but for locals, this value is `undefined` + */ + function getContainersOfSymbol(symbol: ts.Symbol, enclosingDeclaration: Node | undefined): ts.Symbol[] | undefined { + const container = getParentOfSymbol(symbol); + // Type parameters end up in the `members` lists but are not externally visible + if (container && !(symbol.flags & SymbolFlags.TypeParameter)) { + const additionalContainers = mapDefined(container.declarations, fileSymbolIfFileSymbolExportEqualsContainer); + const reexportContainers = enclosingDeclaration && getAlternativeContainingModules(symbol, enclosingDeclaration); + if (enclosingDeclaration && getAccessibleSymbolChain(container, enclosingDeclaration, SymbolFlags.Namespace, /*externalOnly*/ false)) { + return concatenate(concatenate([container], additionalContainers), reexportContainers); // This order expresses a preference for the real container if it is in scope + } + const res = append(additionalContainers, container); + return concatenate(res, reexportContainers); + } + const candidates = mapDefined(symbol.declarations, d => { + if (!isAmbientModule(d) && d.parent && hasNonGlobalAugmentationExternalModuleSymbol(d.parent)) { + return getSymbolOfNode(d.parent); + } + if (isClassExpression(d) && isBinaryExpression(d.parent) && d.parent.operatorToken.kind === SyntaxKind.EqualsToken && isAccessExpression(d.parent.left) && isEntityNameExpression(d.parent.left.expression)) { + if (isModuleExportsAccessExpression(d.parent.left) || isExportsIdentifier(d.parent.left.expression)) { + return getSymbolOfNode(getSourceFileOfNode(d)); + } + checkExpressionCached(d.parent.left.expression); + return getNodeLinks(d.parent.left.expression).resolvedSymbol; } - moduleSymbol.exports!.forEach((s, name) => { - if (name === InternalSymbolName.ExportEquals) return; - merged.exports!.set(name, merged.exports!.has(name) ? mergeSymbol(merged.exports!.get(name)!, s) : s); - }); - getSymbolLinks(merged).cjsExportMerged = merged; - return links.cjsExportMerged = merged; + }); + if (!length(candidates)) { + return undefined; } - - // An external module with an 'export =' declaration may be referenced as an ES6 module provided the 'export =' - // references a symbol that is at least declared as a module or a variable. The target of the 'export =' may - // combine other declarations with the module or variable (e.g. a class/module, function/module, interface/variable). - function resolveESModuleSymbol(moduleSymbol: Symbol | undefined, referencingLocation: Node, dontResolveAlias: boolean, suppressInteropError: boolean): Symbol | undefined { - const symbol = resolveExternalModuleSymbol(moduleSymbol, dontResolveAlias); - - if (!dontResolveAlias && symbol) { - if (!suppressInteropError && !(symbol.flags & (SymbolFlags.Module | SymbolFlags.Variable)) && !getDeclarationOfKind(symbol, SyntaxKind.SourceFile)) { - const compilerOptionName = moduleKind >= ModuleKind.ES2015 - ? "allowSyntheticDefaultImports" - : "esModuleInterop"; - - error(referencingLocation, Diagnostics.This_module_can_only_be_referenced_with_ECMAScript_imports_Slashexports_by_turning_on_the_0_flag_and_referencing_its_default_export, compilerOptionName); - - return symbol; - } - - if (compilerOptions.esModuleInterop) { - const referenceParent = referencingLocation.parent; - if ( - (isImportDeclaration(referenceParent) && getNamespaceDeclarationNode(referenceParent)) || - isImportCall(referenceParent) - ) { - const type = getTypeOfSymbol(symbol); - let sigs = getSignaturesOfStructuredType(type, SignatureKind.Call); - if (!sigs || !sigs.length) { - sigs = getSignaturesOfStructuredType(type, SignatureKind.Construct); - } - if (sigs && sigs.length) { - const moduleType = getTypeWithSyntheticDefaultImportType(type, symbol, moduleSymbol!); - // Create a new symbol which has the module's type less the call and construct signatures - const result = createSymbol(symbol.flags, symbol.escapedName); - result.declarations = symbol.declarations ? symbol.declarations.slice() : []; - result.parent = symbol.parent; - result.target = symbol; - result.originatingImport = referenceParent; - if (symbol.valueDeclaration) result.valueDeclaration = symbol.valueDeclaration; - if (symbol.constEnumOnlyModule) result.constEnumOnlyModule = true; - if (symbol.members) result.members = cloneMap(symbol.members); - if (symbol.exports) result.exports = cloneMap(symbol.exports); - const resolvedModuleType = resolveStructuredTypeMembers(moduleType as StructuredType); // Should already be resolved from the signature checks above - result.type = createAnonymousType(result, resolvedModuleType.members, emptyArray, emptyArray, resolvedModuleType.stringIndexInfo, resolvedModuleType.numberIndexInfo); - return result; - } - } - } - } + return mapDefined(candidates, candidate => getAliasForSymbolInContainer(candidate, symbol) ? candidate : undefined); + function fileSymbolIfFileSymbolExportEqualsContainer(d: Declaration) { + const fileSymbol = getExternalModuleContainer(d); + const exported = fileSymbol && fileSymbol.exports && fileSymbol.exports.get(InternalSymbolName.ExportEquals); + return exported && container && getSymbolIfSameReference(exported, container) ? fileSymbol : undefined; + } + } + function getAliasForSymbolInContainer(container: ts.Symbol, symbol: ts.Symbol) { + if (container === getParentOfSymbol(symbol)) { + // fast path, `symbol` is either already the alias or isn't aliased return symbol; } - - function hasExportAssignmentSymbol(moduleSymbol: Symbol): boolean { - return moduleSymbol.exports!.get(InternalSymbolName.ExportEquals) !== undefined; + // Check if container is a thing with an `export=` which points directly at `symbol`, and if so, return + // the container itself as the alias for the symbol + const exportEquals = container.exports && container.exports.get(InternalSymbolName.ExportEquals); + if (exportEquals && getSymbolIfSameReference(exportEquals, symbol)) { + return container; } - - function getExportsOfModuleAsArray(moduleSymbol: Symbol): Symbol[] { - return symbolsToArray(getExportsOfModule(moduleSymbol)); + const exports = getExportsOfSymbol(container); + const quick = exports.get(symbol.escapedName); + if (quick && getSymbolIfSameReference(quick, symbol)) { + return quick; } - - function getExportsAndPropertiesOfModule(moduleSymbol: Symbol): Symbol[] { - const exports = getExportsOfModuleAsArray(moduleSymbol); - const exportEquals = resolveExternalModuleSymbol(moduleSymbol); - if (exportEquals !== moduleSymbol) { - addRange(exports, getPropertiesOfType(getTypeOfSymbol(exportEquals))); + return forEachEntry(exports, exported => { + if (getSymbolIfSameReference(exported, symbol)) { + return exported; } - return exports; + }); + } + /** + * Checks if two symbols, through aliasing and/or merging, refer to the same thing + */ + function getSymbolIfSameReference(s1: ts.Symbol, s2: ts.Symbol) { + if (getMergedSymbol(resolveSymbol(getMergedSymbol(s1))) === getMergedSymbol(resolveSymbol(getMergedSymbol(s2)))) { + return s1; } - - function tryGetMemberInModuleExports(memberName: __String, moduleSymbol: Symbol): Symbol | undefined { - const symbolTable = getExportsOfModule(moduleSymbol); - if (symbolTable) { - return symbolTable.get(memberName); + } + function getExportSymbolOfValueSymbolIfExported(symbol: ts.Symbol): ts.Symbol; + function getExportSymbolOfValueSymbolIfExported(symbol: ts.Symbol | undefined): ts.Symbol | undefined; + function getExportSymbolOfValueSymbolIfExported(symbol: ts.Symbol | undefined): ts.Symbol | undefined { + return getMergedSymbol(symbol && (symbol.flags & SymbolFlags.ExportValue) !== 0 ? symbol.exportSymbol : symbol); + } + function symbolIsValue(symbol: ts.Symbol): boolean { + return !!(symbol.flags & SymbolFlags.Value || symbol.flags & SymbolFlags.Alias && resolveAlias(symbol).flags & SymbolFlags.Value && !getTypeOnlyAliasDeclaration(symbol)); + } + function findConstructorDeclaration(node: ClassLikeDeclaration): ConstructorDeclaration | undefined { + const members = node.members; + for (const member of members) { + if (member.kind === SyntaxKind.Constructor && nodeIsPresent((member).body)) { + return member; } } - - function tryGetMemberInModuleExportsAndProperties(memberName: __String, moduleSymbol: Symbol): Symbol | undefined { - const symbol = tryGetMemberInModuleExports(memberName, moduleSymbol); - if (symbol) { - return symbol; + } + function createType(flags: TypeFlags): ts.Type { + const result = new Type(checker, flags); + typeCount++; + result.id = typeCount; + return result; + } + function createIntrinsicType(kind: TypeFlags, intrinsicName: string, objectFlags: ObjectFlags = 0): IntrinsicType { + const type = (createType(kind)); + type.intrinsicName = intrinsicName; + type.objectFlags = objectFlags; + return type; + } + function createBooleanType(trueFalseTypes: readonly ts.Type[]): IntrinsicType & UnionType { + const type = (getUnionType(trueFalseTypes)); + type.flags |= TypeFlags.Boolean; + type.intrinsicName = "boolean"; + return type; + } + function createObjectType(objectFlags: ObjectFlags, symbol?: ts.Symbol): ObjectType { + const type = (createType(TypeFlags.Object)); + type.objectFlags = objectFlags; + type.symbol = symbol!; + type.members = undefined; + type.properties = undefined; + type.callSignatures = undefined; + type.constructSignatures = undefined; + type.stringIndexInfo = undefined; + type.numberIndexInfo = undefined; + return type; + } + function createTypeofType() { + return getUnionType(arrayFrom(typeofEQFacts.keys(), getLiteralType)); + } + function createTypeParameter(symbol?: ts.Symbol) { + const type = (createType(TypeFlags.TypeParameter)); + if (symbol) + type.symbol = symbol; + return type; + } + // A reserved member name starts with two underscores, but the third character cannot be an underscore, + // @, or #. A third underscore indicates an escaped form of an identifier that started + // with at least two underscores. The @ character indicates that the name is denoted by a well known ES + // Symbol instance and the # character indicates that the name is a PrivateIdentifier. + function isReservedMemberName(name: __String) { + return (name as string).charCodeAt(0) === CharacterCodes._ && + (name as string).charCodeAt(1) === CharacterCodes._ && + (name as string).charCodeAt(2) !== CharacterCodes._ && + (name as string).charCodeAt(2) !== CharacterCodes.at && + (name as string).charCodeAt(2) !== CharacterCodes.hash; + } + function getNamedMembers(members: SymbolTable): ts.Symbol[] { + let result: ts.Symbol[] | undefined; + members.forEach((symbol, id) => { + if (!isReservedMemberName(id) && symbolIsValue(symbol)) { + (result || (result = [])).push(symbol); } - - const exportEquals = resolveExternalModuleSymbol(moduleSymbol); - if (exportEquals === moduleSymbol) { - return undefined; + }); + return result || emptyArray; + } + function setStructuredTypeMembers(type: StructuredType, members: SymbolTable, callSignatures: readonly ts.Signature[], constructSignatures: readonly ts.Signature[], stringIndexInfo: IndexInfo | undefined, numberIndexInfo: IndexInfo | undefined): ResolvedType { + (type).members = members; + (type).properties = members === emptySymbols ? emptyArray : getNamedMembers(members); + (type).callSignatures = callSignatures; + (type).constructSignatures = constructSignatures; + (type).stringIndexInfo = stringIndexInfo; + (type).numberIndexInfo = numberIndexInfo; + return type; + } + function createAnonymousType(symbol: ts.Symbol | undefined, members: SymbolTable, callSignatures: readonly ts.Signature[], constructSignatures: readonly ts.Signature[], stringIndexInfo: IndexInfo | undefined, numberIndexInfo: IndexInfo | undefined): ResolvedType { + return setStructuredTypeMembers(createObjectType(ObjectFlags.Anonymous, symbol), members, callSignatures, constructSignatures, stringIndexInfo, numberIndexInfo); + } + function forEachSymbolTableInScope(enclosingDeclaration: Node | undefined, callback: (symbolTable: SymbolTable) => T): T { + let result: T; + for (let location = enclosingDeclaration; location; location = location.parent) { + // Locals of a source file are not in scope (because they get merged into the global symbol table) + if (location.locals && !isGlobalSourceFile(location)) { + if (result = callback(location.locals)) { + return result; + } } - - const type = getTypeOfSymbol(exportEquals); - return type.flags & TypeFlags.Primitive || - getObjectFlags(type) & ObjectFlags.Class || - isArrayOrTupleLikeType(type) - ? undefined - : getPropertyOfType(type, memberName); - } - - function getExportsOfSymbol(symbol: Symbol): SymbolTable { - return symbol.flags & SymbolFlags.LateBindingContainer ? getResolvedMembersOrExportsOfSymbol(symbol, MembersOrExportsResolutionKind.resolvedExports) : - symbol.flags & SymbolFlags.Module ? getExportsOfModule(symbol) : - symbol.exports || emptySymbols; - } - - function getExportsOfModule(moduleSymbol: Symbol): SymbolTable { - const links = getSymbolLinks(moduleSymbol); - return links.resolvedExports || (links.resolvedExports = getExportsOfModuleWorker(moduleSymbol)); - } - - interface ExportCollisionTracker { - specifierText: string; - exportsWithDuplicate: ExportDeclaration[]; - } - - type ExportCollisionTrackerTable = UnderscoreEscapedMap; - - /** - * Extends one symbol table with another while collecting information on name collisions for error message generation into the `lookupTable` argument - * Not passing `lookupTable` and `exportNode` disables this collection, and just extends the tables - */ - function extendExportSymbols(target: SymbolTable, source: SymbolTable | undefined, lookupTable?: ExportCollisionTrackerTable, exportNode?: ExportDeclaration) { - if (!source) return; - source.forEach((sourceSymbol, id) => { - if (id === InternalSymbolName.Default) return; - - const targetSymbol = target.get(id); - if (!targetSymbol) { - target.set(id, sourceSymbol); - if (lookupTable && exportNode) { - lookupTable.set(id, { - specifierText: getTextOfNode(exportNode.moduleSpecifier!) - } as ExportCollisionTracker); - } - } - else if (lookupTable && exportNode && targetSymbol && resolveSymbol(targetSymbol) !== resolveSymbol(sourceSymbol)) { - const collisionTracker = lookupTable.get(id)!; - if (!collisionTracker.exportsWithDuplicate) { - collisionTracker.exportsWithDuplicate = [exportNode]; + switch (location.kind) { + case SyntaxKind.SourceFile: + if (!isExternalOrCommonJsModule((location))) { + break; } - else { - collisionTracker.exportsWithDuplicate.push(exportNode); + // falls through + case SyntaxKind.ModuleDeclaration: + const sym = getSymbolOfNode((location as ModuleDeclaration)); + // `sym` may not have exports if this module declaration is backed by the symbol for a `const` that's being rewritten + // into a namespace - in such cases, it's best to just let the namespace appear empty (the const members couldn't have referred + // to one another anyway) + if (result = callback(sym?.exports || emptySymbols)) { + return result; } - } - }); - } - - function getExportsOfModuleWorker(moduleSymbol: Symbol): SymbolTable { - const visitedSymbols: Symbol[] = []; - - // A module defined by an 'export=' consists of one export that needs to be resolved - moduleSymbol = resolveExternalModuleSymbol(moduleSymbol); - - return visit(moduleSymbol) || emptySymbols; - - // The ES6 spec permits export * declarations in a module to circularly reference the module itself. For example, - // module 'a' can 'export * from "b"' and 'b' can 'export * from "a"' without error. - function visit(symbol: Symbol | undefined): SymbolTable | undefined { - if (!(symbol && symbol.exports && pushIfUnique(visitedSymbols, symbol))) { - return; - } - const symbols = cloneMap(symbol.exports); - // All export * declarations are collected in an __export symbol by the binder - const exportStars = symbol.exports.get(InternalSymbolName.ExportStar); - if (exportStars) { - const nestedSymbols = createSymbolTable(); - const lookupTable = createMap() as ExportCollisionTrackerTable; - for (const node of exportStars.declarations) { - const resolvedModule = resolveExternalModuleName(node, (node as ExportDeclaration).moduleSpecifier!); - const exportedSymbols = visit(resolvedModule); - extendExportSymbols( - nestedSymbols, - exportedSymbols, - lookupTable, - node as ExportDeclaration - ); - } - lookupTable.forEach(({ exportsWithDuplicate }, id) => { - // It's not an error if the file with multiple `export *`s with duplicate names exports a member with that name itself - if (id === "export=" || !(exportsWithDuplicate && exportsWithDuplicate.length) || symbols.has(id)) { - return; - } - for (const node of exportsWithDuplicate) { - diagnostics.add(createDiagnosticForNode( - node, - Diagnostics.Module_0_has_already_exported_a_member_named_1_Consider_explicitly_re_exporting_to_resolve_the_ambiguity, - lookupTable.get(id)!.specifierText, - unescapeLeadingUnderscores(id) - )); + break; + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ClassExpression: + case SyntaxKind.InterfaceDeclaration: + // Type parameters are bound into `members` lists so they can merge across declarations + // This is troublesome, since in all other respects, they behave like locals :cries: + // TODO: the below is shared with similar code in `resolveName` - in fact, rephrasing all this symbol + // lookup logic in terms of `resolveName` would be nice + // The below is used to lookup type parameters within a class or interface, as they are added to the class/interface locals + // These can never be latebound, so the symbol's raw members are sufficient. `getMembersOfNode` cannot be used, as it would + // trigger resolving late-bound names, which we may already be in the process of doing while we're here! + let table: UnderscoreEscapedMap | undefined; + // TODO: Should this filtered table be cached in some way? + (getSymbolOfNode((location as ClassLikeDeclaration | InterfaceDeclaration)).members || emptySymbols).forEach((memberSymbol, key) => { + if (memberSymbol.flags & (SymbolFlags.Type & ~SymbolFlags.Assignment)) { + (table || (table = createSymbolTable())).set(key, memberSymbol); } }); - extendExportSymbols(symbols, nestedSymbols); - } - return symbols; + if (table && (result = callback(table))) { + return result; + } + break; } } - - function getMergedSymbol(symbol: Symbol): Symbol; - function getMergedSymbol(symbol: Symbol | undefined): Symbol | undefined; - function getMergedSymbol(symbol: Symbol | undefined): Symbol | undefined { - let merged: Symbol; - return symbol && symbol.mergeId && (merged = mergedSymbols[symbol.mergeId]) ? merged : symbol; - } - - function getSymbolOfNode(node: Declaration): Symbol; - function getSymbolOfNode(node: Node): Symbol | undefined; - function getSymbolOfNode(node: Node): Symbol | undefined { - return getMergedSymbol(node.symbol && getLateBoundSymbol(node.symbol)); - } - - function getParentOfSymbol(symbol: Symbol): Symbol | undefined { - return getMergedSymbol(symbol.parent && getLateBoundSymbol(symbol.parent)); + return callback(globals); + } + function getQualifiedLeftMeaning(rightMeaning: SymbolFlags) { + // If we are looking in value space, the parent meaning is value, other wise it is namespace + return rightMeaning === SymbolFlags.Value ? SymbolFlags.Value : SymbolFlags.Namespace; + } + function getAccessibleSymbolChain(symbol: ts.Symbol | undefined, enclosingDeclaration: Node | undefined, meaning: SymbolFlags, useOnlyExternalAliasing: boolean, visitedSymbolTablesMap: ts.Map = createMap()): ts.Symbol[] | undefined { + if (!(symbol && !isPropertyOrMethodDeclarationSymbol(symbol))) { + return undefined; } - - function getAlternativeContainingModules(symbol: Symbol, enclosingDeclaration: Node): Symbol[] { - const containingFile = getSourceFileOfNode(enclosingDeclaration); - const id = "" + getNodeId(containingFile); - const links = getSymbolLinks(symbol); - let results: Symbol[] | undefined; - if (links.extendedContainersByFile && (results = links.extendedContainersByFile.get(id))) { - return results; - } - if (containingFile && containingFile.imports) { - // Try to make an import using an import already in the enclosing file, if possible - for (const importRef of containingFile.imports) { - if (nodeIsSynthesized(importRef)) continue; // Synthetic names can't be resolved by `resolveExternalModuleName` - they'll cause a debug assert if they error - const resolvedModule = resolveExternalModuleName(enclosingDeclaration, importRef, /*ignoreErrors*/ true); - if (!resolvedModule) continue; - const ref = getAliasForSymbolInContainer(resolvedModule, symbol); - if (!ref) continue; - results = append(results, resolvedModule); - } - if (length(results)) { - (links.extendedContainersByFile || (links.extendedContainersByFile = createMap())).set(id, results!); - return results!; - } - } - if (links.extendedContainers) { - return links.extendedContainers; - } - // No results from files already being imported by this file - expand search (expensive, but not location-specific, so cached) - const otherFiles = host.getSourceFiles(); - for (const file of otherFiles) { - if (!isExternalModule(file)) continue; - const sym = getSymbolOfNode(file); - const ref = getAliasForSymbolInContainer(sym, symbol); - if (!ref) continue; - results = append(results, sym); - } - return links.extendedContainers = results || emptyArray; + const id = "" + getSymbolId(symbol); + let visitedSymbolTables = visitedSymbolTablesMap.get(id); + if (!visitedSymbolTables) { + visitedSymbolTablesMap.set(id, visitedSymbolTables = []); } - + return forEachSymbolTableInScope(enclosingDeclaration, getAccessibleSymbolChainFromSymbolTable); /** - * Attempts to find the symbol corresponding to the container a symbol is in - usually this - * is just its' `.parent`, but for locals, this value is `undefined` + * @param {ignoreQualification} boolean Set when a symbol is being looked for through the exports of another symbol (meaning we have a route to qualify it already) */ - function getContainersOfSymbol(symbol: Symbol, enclosingDeclaration: Node | undefined): Symbol[] | undefined { - const container = getParentOfSymbol(symbol); - // Type parameters end up in the `members` lists but are not externally visible - if (container && !(symbol.flags & SymbolFlags.TypeParameter)) { - const additionalContainers = mapDefined(container.declarations, fileSymbolIfFileSymbolExportEqualsContainer); - const reexportContainers = enclosingDeclaration && getAlternativeContainingModules(symbol, enclosingDeclaration); - if (enclosingDeclaration && getAccessibleSymbolChain(container, enclosingDeclaration, SymbolFlags.Namespace, /*externalOnly*/ false)) { - return concatenate(concatenate([container], additionalContainers), reexportContainers); // This order expresses a preference for the real container if it is in scope - } - const res = append(additionalContainers, container); - return concatenate(res, reexportContainers); + function getAccessibleSymbolChainFromSymbolTable(symbols: SymbolTable, ignoreQualification?: boolean): ts.Symbol[] | undefined { + if (!pushIfUnique((visitedSymbolTables!), symbols)) { + return undefined; } - const candidates = mapDefined(symbol.declarations, d => { - if (!isAmbientModule(d) && d.parent && hasNonGlobalAugmentationExternalModuleSymbol(d.parent)) { - return getSymbolOfNode(d.parent); + const result = trySymbolTable(symbols, ignoreQualification); + visitedSymbolTables!.pop(); + return result; + } + function canQualifySymbol(symbolFromSymbolTable: ts.Symbol, meaning: SymbolFlags) { + // If the symbol is equivalent and doesn't need further qualification, this symbol is accessible + return !needsQualification(symbolFromSymbolTable, enclosingDeclaration, meaning) || + // If symbol needs qualification, make sure that parent is accessible, if it is then this symbol is accessible too + !!getAccessibleSymbolChain(symbolFromSymbolTable.parent, enclosingDeclaration, getQualifiedLeftMeaning(meaning), useOnlyExternalAliasing, visitedSymbolTablesMap); + } + function isAccessible(symbolFromSymbolTable: ts.Symbol, resolvedAliasSymbol?: ts.Symbol, ignoreQualification?: boolean) { + return (symbol === (resolvedAliasSymbol || symbolFromSymbolTable) || getMergedSymbol(symbol) === getMergedSymbol(resolvedAliasSymbol || symbolFromSymbolTable)) && + // if the symbolFromSymbolTable is not external module (it could be if it was determined as ambient external module and would be in globals table) + // and if symbolFromSymbolTable or alias resolution matches the symbol, + // check the symbol can be qualified, it is only then this symbol is accessible + !some(symbolFromSymbolTable.declarations, hasNonGlobalAugmentationExternalModuleSymbol) && + (ignoreQualification || canQualifySymbol(getMergedSymbol(symbolFromSymbolTable), meaning)); + } + function trySymbolTable(symbols: SymbolTable, ignoreQualification: boolean | undefined): ts.Symbol[] | undefined { + // If symbol is directly available by its name in the symbol table + if (isAccessible(symbols.get(symbol!.escapedName)!, /*resolvedAliasSymbol*/ undefined, ignoreQualification)) { + return [symbol!]; + } + // Check if symbol is any of the aliases in scope + const result = forEachEntry(symbols, symbolFromSymbolTable => { + if (symbolFromSymbolTable.flags & SymbolFlags.Alias + && symbolFromSymbolTable.escapedName !== InternalSymbolName.ExportEquals + && symbolFromSymbolTable.escapedName !== InternalSymbolName.Default + && !(isUMDExportSymbol(symbolFromSymbolTable) && enclosingDeclaration && isExternalModule(getSourceFileOfNode(enclosingDeclaration))) + // If `!useOnlyExternalAliasing`, we can use any type of alias to get the name + && (!useOnlyExternalAliasing || some(symbolFromSymbolTable.declarations, isExternalModuleImportEqualsDeclaration)) + // While exports are generally considered to be in scope, export-specifier declared symbols are _not_ + // See similar comment in `resolveName` for details + && (ignoreQualification || !getDeclarationOfKind(symbolFromSymbolTable, SyntaxKind.ExportSpecifier))) { + const resolvedImportedSymbol = resolveAlias(symbolFromSymbolTable); + const candidate = getCandidateListForSymbol(symbolFromSymbolTable, resolvedImportedSymbol, ignoreQualification); + if (candidate) { + return candidate; + } } - if (isClassExpression(d) && isBinaryExpression(d.parent) && d.parent.operatorToken.kind === SyntaxKind.EqualsToken && isAccessExpression(d.parent.left) && isEntityNameExpression(d.parent.left.expression)) { - if (isModuleExportsAccessExpression(d.parent.left) || isExportsIdentifier(d.parent.left.expression)) { - return getSymbolOfNode(getSourceFileOfNode(d)); + if (symbolFromSymbolTable.escapedName === symbol!.escapedName && symbolFromSymbolTable.exportSymbol) { + if (isAccessible(getMergedSymbol(symbolFromSymbolTable.exportSymbol), /*aliasSymbol*/ undefined, ignoreQualification)) { + return [symbol!]; } - checkExpressionCached(d.parent.left.expression); - return getNodeLinks(d.parent.left.expression).resolvedSymbol; } }); - if (!length(candidates)) { - return undefined; + // If there's no result and we're looking at the global symbol table, treat `globalThis` like an alias and try to lookup thru that + return result || (symbols === globals ? getCandidateListForSymbol(globalThisSymbol, globalThisSymbol, ignoreQualification) : undefined); + } + function getCandidateListForSymbol(symbolFromSymbolTable: ts.Symbol, resolvedImportedSymbol: ts.Symbol, ignoreQualification: boolean | undefined) { + if (isAccessible(symbolFromSymbolTable, resolvedImportedSymbol, ignoreQualification)) { + return [symbolFromSymbolTable]; } - return mapDefined(candidates, candidate => getAliasForSymbolInContainer(candidate, symbol) ? candidate : undefined); - - function fileSymbolIfFileSymbolExportEqualsContainer(d: Declaration) { - const fileSymbol = getExternalModuleContainer(d); - const exported = fileSymbol && fileSymbol.exports && fileSymbol.exports.get(InternalSymbolName.ExportEquals); - return exported && container && getSymbolIfSameReference(exported, container) ? fileSymbol : undefined; + // Look in the exported members, if we can find accessibleSymbolChain, symbol is accessible using this chain + // but only if the symbolFromSymbolTable can be qualified + const candidateTable = getExportsOfSymbol(resolvedImportedSymbol); + const accessibleSymbolsFromExports = candidateTable && getAccessibleSymbolChainFromSymbolTable(candidateTable, /*ignoreQualification*/ true); + if (accessibleSymbolsFromExports && canQualifySymbol(symbolFromSymbolTable, getQualifiedLeftMeaning(meaning))) { + return [symbolFromSymbolTable].concat(accessibleSymbolsFromExports); } } - - function getAliasForSymbolInContainer(container: Symbol, symbol: Symbol) { - if (container === getParentOfSymbol(symbol)) { - // fast path, `symbol` is either already the alias or isn't aliased - return symbol; + } + function needsQualification(symbol: ts.Symbol, enclosingDeclaration: Node | undefined, meaning: SymbolFlags) { + let qualify = false; + forEachSymbolTableInScope(enclosingDeclaration, symbolTable => { + // If symbol of this name is not available in the symbol table we are ok + let symbolFromSymbolTable = getMergedSymbol(symbolTable.get(symbol.escapedName)); + if (!symbolFromSymbolTable) { + // Continue to the next symbol table + return false; } - // Check if container is a thing with an `export=` which points directly at `symbol`, and if so, return - // the container itself as the alias for the symbol - const exportEquals = container.exports && container.exports.get(InternalSymbolName.ExportEquals); - if (exportEquals && getSymbolIfSameReference(exportEquals, symbol)) { - return container; + // If the symbol with this name is present it should refer to the symbol + if (symbolFromSymbolTable === symbol) { + // No need to qualify + return true; } - const exports = getExportsOfSymbol(container); - const quick = exports.get(symbol.escapedName); - if (quick && getSymbolIfSameReference(quick, symbol)) { - return quick; + // Qualify if the symbol from symbol table has same meaning as expected + symbolFromSymbolTable = (symbolFromSymbolTable.flags & SymbolFlags.Alias && !getDeclarationOfKind(symbolFromSymbolTable, SyntaxKind.ExportSpecifier)) ? resolveAlias(symbolFromSymbolTable) : symbolFromSymbolTable; + if (symbolFromSymbolTable.flags & meaning) { + qualify = true; + return true; } - return forEachEntry(exports, exported => { - if (getSymbolIfSameReference(exported, symbol)) { - return exported; + // Continue to the next symbol table + return false; + }); + return qualify; + } + function isPropertyOrMethodDeclarationSymbol(symbol: ts.Symbol) { + if (symbol.declarations && symbol.declarations.length) { + for (const declaration of symbol.declarations) { + switch (declaration.kind) { + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + continue; + default: + return false; } - }); - } - - /** - * Checks if two symbols, through aliasing and/or merging, refer to the same thing - */ - function getSymbolIfSameReference(s1: Symbol, s2: Symbol) { - if (getMergedSymbol(resolveSymbol(getMergedSymbol(s1))) === getMergedSymbol(resolveSymbol(getMergedSymbol(s2)))) { - return s1; } + return true; } - - function getExportSymbolOfValueSymbolIfExported(symbol: Symbol): Symbol; - function getExportSymbolOfValueSymbolIfExported(symbol: Symbol | undefined): Symbol | undefined; - function getExportSymbolOfValueSymbolIfExported(symbol: Symbol | undefined): Symbol | undefined { - return getMergedSymbol(symbol && (symbol.flags & SymbolFlags.ExportValue) !== 0 ? symbol.exportSymbol : symbol); - } - - function symbolIsValue(symbol: Symbol): boolean { - return !!(symbol.flags & SymbolFlags.Value || symbol.flags & SymbolFlags.Alias && resolveAlias(symbol).flags & SymbolFlags.Value && !getTypeOnlyAliasDeclaration(symbol)); - } - - function findConstructorDeclaration(node: ClassLikeDeclaration): ConstructorDeclaration | undefined { - const members = node.members; - for (const member of members) { - if (member.kind === SyntaxKind.Constructor && nodeIsPresent((member).body)) { - return member; + return false; + } + function isTypeSymbolAccessible(typeSymbol: ts.Symbol, enclosingDeclaration: Node | undefined): boolean { + const access = isSymbolAccessible(typeSymbol, enclosingDeclaration, SymbolFlags.Type, /*shouldComputeAliasesToMakeVisible*/ false); + return access.accessibility === SymbolAccessibility.Accessible; + } + function isValueSymbolAccessible(typeSymbol: ts.Symbol, enclosingDeclaration: Node | undefined): boolean { + const access = isSymbolAccessible(typeSymbol, enclosingDeclaration, SymbolFlags.Value, /*shouldComputeAliasesToMakeVisible*/ false); + return access.accessibility === SymbolAccessibility.Accessible; + } + function isAnySymbolAccessible(symbols: ts.Symbol[] | undefined, enclosingDeclaration: Node | undefined, initialSymbol: ts.Symbol, meaning: SymbolFlags, shouldComputeAliasesToMakeVisible: boolean): SymbolAccessibilityResult | undefined { + if (!length(symbols)) + return; + let hadAccessibleChain: ts.Symbol | undefined; + let earlyModuleBail = false; + for (const symbol of symbols!) { + // Symbol is accessible if it by itself is accessible + const accessibleSymbolChain = getAccessibleSymbolChain(symbol, enclosingDeclaration, meaning, /*useOnlyExternalAliasing*/ false); + if (accessibleSymbolChain) { + hadAccessibleChain = symbol; + const hasAccessibleDeclarations = hasVisibleDeclarations(accessibleSymbolChain[0], shouldComputeAliasesToMakeVisible); + if (hasAccessibleDeclarations) { + return hasAccessibleDeclarations; + } + } + else { + if (some(symbol.declarations, hasNonGlobalAugmentationExternalModuleSymbol)) { + if (shouldComputeAliasesToMakeVisible) { + earlyModuleBail = true; + // Generally speaking, we want to use the aliases that already exist to refer to a module, if present + // In order to do so, we need to find those aliases in order to retain them in declaration emit; so + // if we are in declaration emit, we cannot use the fast path for module visibility until we've exhausted + // all other visibility options (in order to capture the possible aliases used to reference the module) + continue; + } + // Any meaning of a module symbol is always accessible via an `import` type + return { + accessibility: SymbolAccessibility.Accessible + }; } } + // If we haven't got the accessible symbol, it doesn't mean the symbol is actually inaccessible. + // It could be a qualified symbol and hence verify the path + // e.g.: + // module m { + // export class c { + // } + // } + // const x: typeof m.c + // In the above example when we start with checking if typeof m.c symbol is accessible, + // we are going to see if c can be accessed in scope directly. + // But it can't, hence the accessible is going to be undefined, but that doesn't mean m.c is inaccessible + // It is accessible if the parent m is accessible because then m.c can be accessed through qualification + let containers = getContainersOfSymbol(symbol, enclosingDeclaration); + // If we're trying to reference some object literal in, eg `var a = { x: 1 }`, the symbol for the literal, `__object`, is distinct + // from the symbol of the declaration it is being assigned to. Since we can use the declaration to refer to the literal, however, + // we'd like to make that connection here - potentially causing us to paint the declaration's visibility, and therefore the literal. + const firstDecl: Node = first(symbol.declarations); + if (!length(containers) && meaning & SymbolFlags.Value && firstDecl && isObjectLiteralExpression(firstDecl)) { + if (firstDecl.parent && isVariableDeclaration(firstDecl.parent) && firstDecl === firstDecl.parent.initializer) { + containers = [getSymbolOfNode(firstDecl.parent)]; + } + } + const parentResult = isAnySymbolAccessible(containers, enclosingDeclaration, initialSymbol, initialSymbol === symbol ? getQualifiedLeftMeaning(meaning) : meaning, shouldComputeAliasesToMakeVisible); + if (parentResult) { + return parentResult; + } + } + if (earlyModuleBail) { + return { + accessibility: SymbolAccessibility.Accessible + }; } - - function createType(flags: TypeFlags): Type { - const result = new Type(checker, flags); - typeCount++; - result.id = typeCount; - return result; + if (hadAccessibleChain) { + return { + accessibility: SymbolAccessibility.NotAccessible, + errorSymbolName: symbolToString(initialSymbol, enclosingDeclaration, meaning), + errorModuleName: hadAccessibleChain !== initialSymbol ? symbolToString(hadAccessibleChain, enclosingDeclaration, SymbolFlags.Namespace) : undefined, + }; } - - function createIntrinsicType(kind: TypeFlags, intrinsicName: string, objectFlags: ObjectFlags = 0): IntrinsicType { - const type = createType(kind); - type.intrinsicName = intrinsicName; - type.objectFlags = objectFlags; - return type; + } + /** + * Check if the given symbol in given enclosing declaration is accessible and mark all associated alias to be visible if requested + * + * @param symbol a Symbol to check if accessible + * @param enclosingDeclaration a Node containing reference to the symbol + * @param meaning a SymbolFlags to check if such meaning of the symbol is accessible + * @param shouldComputeAliasToMakeVisible a boolean value to indicate whether to return aliases to be mark visible in case the symbol is accessible + */ + function isSymbolAccessible(symbol: ts.Symbol | undefined, enclosingDeclaration: Node | undefined, meaning: SymbolFlags, shouldComputeAliasesToMakeVisible: boolean): SymbolAccessibilityResult { + if (symbol && enclosingDeclaration) { + const result = isAnySymbolAccessible([symbol], enclosingDeclaration, symbol, meaning, shouldComputeAliasesToMakeVisible); + if (result) { + return result; + } + // This could be a symbol that is not exported in the external module + // or it could be a symbol from different external module that is not aliased and hence cannot be named + const symbolExternalModule = forEach(symbol.declarations, getExternalModuleContainer); + if (symbolExternalModule) { + const enclosingExternalModule = getExternalModuleContainer(enclosingDeclaration); + if (symbolExternalModule !== enclosingExternalModule) { + // name from different external module that is not visible + return { + accessibility: SymbolAccessibility.CannotBeNamed, + errorSymbolName: symbolToString(symbol, enclosingDeclaration, meaning), + errorModuleName: symbolToString(symbolExternalModule) + }; + } + } + // Just a local name that is not accessible + return { + accessibility: SymbolAccessibility.NotAccessible, + errorSymbolName: symbolToString(symbol, enclosingDeclaration, meaning), + }; } - - function createBooleanType(trueFalseTypes: readonly Type[]): IntrinsicType & UnionType { - const type = getUnionType(trueFalseTypes); - type.flags |= TypeFlags.Boolean; - type.intrinsicName = "boolean"; - return type; + return { accessibility: SymbolAccessibility.Accessible }; + } + function getExternalModuleContainer(declaration: Node) { + const node = findAncestor(declaration, hasExternalModuleSymbol); + return node && getSymbolOfNode(node); + } + function hasExternalModuleSymbol(declaration: Node) { + return isAmbientModule(declaration) || (declaration.kind === SyntaxKind.SourceFile && isExternalOrCommonJsModule((declaration))); + } + function hasNonGlobalAugmentationExternalModuleSymbol(declaration: Node) { + return isModuleWithStringLiteralName(declaration) || (declaration.kind === SyntaxKind.SourceFile && isExternalOrCommonJsModule((declaration))); + } + function hasVisibleDeclarations(symbol: ts.Symbol, shouldComputeAliasToMakeVisible: boolean): SymbolVisibilityResult | undefined { + let aliasesToMakeVisible: LateVisibilityPaintedStatement[] | undefined; + if (!every(filter(symbol.declarations, d => d.kind !== SyntaxKind.Identifier), getIsDeclarationVisible)) { + return undefined; } - - function createObjectType(objectFlags: ObjectFlags, symbol?: Symbol): ObjectType { - const type = createType(TypeFlags.Object); - type.objectFlags = objectFlags; - type.symbol = symbol!; - type.members = undefined; - type.properties = undefined; - type.callSignatures = undefined; - type.constructSignatures = undefined; - type.stringIndexInfo = undefined; - type.numberIndexInfo = undefined; - return type; + return { accessibility: SymbolAccessibility.Accessible, aliasesToMakeVisible }; + function getIsDeclarationVisible(declaration: Declaration) { + if (!isDeclarationVisible(declaration)) { + // Mark the unexported alias as visible if its parent is visible + // because these kind of aliases can be used to name types in declaration file + const anyImportSyntax = getAnyImportSyntax(declaration); + if (anyImportSyntax && + !hasModifier(anyImportSyntax, ModifierFlags.Export) && // import clause without export + isDeclarationVisible(anyImportSyntax.parent)) { + return addVisibleAlias(declaration, anyImportSyntax); + } + else if (isVariableDeclaration(declaration) && isVariableStatement(declaration.parent.parent) && + !hasModifier(declaration.parent.parent, ModifierFlags.Export) && // unexported variable statement + isDeclarationVisible(declaration.parent.parent.parent)) { + return addVisibleAlias(declaration, declaration.parent.parent); + } + else if (isLateVisibilityPaintedStatement(declaration) // unexported top-level statement + && !hasModifier(declaration, ModifierFlags.Export) + && isDeclarationVisible(declaration.parent)) { + return addVisibleAlias(declaration, declaration); + } + // Declaration is not visible + return false; + } + return true; } - - function createTypeofType() { - return getUnionType(arrayFrom(typeofEQFacts.keys(), getLiteralType)); + function addVisibleAlias(declaration: Declaration, aliasingStatement: LateVisibilityPaintedStatement) { + // In function "buildTypeDisplay" where we decide whether to write type-alias or serialize types, + // we want to just check if type- alias is accessible or not but we don't care about emitting those alias at that time + // since we will do the emitting later in trackSymbol. + if (shouldComputeAliasToMakeVisible) { + getNodeLinks(declaration).isVisible = true; + aliasesToMakeVisible = appendIfUnique(aliasesToMakeVisible, aliasingStatement); + } + return true; } - - function createTypeParameter(symbol?: Symbol) { - const type = createType(TypeFlags.TypeParameter); - if (symbol) type.symbol = symbol; - return type; + } + function isEntityNameVisible(entityName: EntityNameOrEntityNameExpression, enclosingDeclaration: Node): SymbolVisibilityResult { + // get symbol of the first identifier of the entityName + let meaning: SymbolFlags; + if (entityName.parent.kind === SyntaxKind.TypeQuery || + isExpressionWithTypeArgumentsInClassExtendsClause(entityName.parent) || + entityName.parent.kind === SyntaxKind.ComputedPropertyName) { + // Typeof value + meaning = SymbolFlags.Value | SymbolFlags.ExportValue; + } + else if (entityName.kind === SyntaxKind.QualifiedName || entityName.kind === SyntaxKind.PropertyAccessExpression || + entityName.parent.kind === SyntaxKind.ImportEqualsDeclaration) { + // Left identifier from type reference or TypeAlias + // Entity name of the import declaration + meaning = SymbolFlags.Namespace; + } + else { + // Type Reference or TypeAlias entity = Identifier + meaning = SymbolFlags.Type; + } + const firstIdentifier = getFirstIdentifier(entityName); + const symbol = resolveName(enclosingDeclaration, firstIdentifier.escapedText, meaning, /*nodeNotFoundErrorMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false); + // Verify if the symbol is accessible + return (symbol && hasVisibleDeclarations(symbol, /*shouldComputeAliasToMakeVisible*/ true)) || { + accessibility: SymbolAccessibility.NotAccessible, + errorSymbolName: getTextOfNode(firstIdentifier), + errorNode: firstIdentifier + }; + } + function symbolToString(symbol: ts.Symbol, enclosingDeclaration?: Node, meaning?: SymbolFlags, flags: SymbolFormatFlags = SymbolFormatFlags.AllowAnyNodeKind, writer?: EmitTextWriter): string { + let nodeFlags = NodeBuilderFlags.IgnoreErrors; + if (flags & SymbolFormatFlags.UseOnlyExternalAliasing) { + nodeFlags |= NodeBuilderFlags.UseOnlyExternalAliasing; } - - // A reserved member name starts with two underscores, but the third character cannot be an underscore, - // @, or #. A third underscore indicates an escaped form of an identifier that started - // with at least two underscores. The @ character indicates that the name is denoted by a well known ES - // Symbol instance and the # character indicates that the name is a PrivateIdentifier. - function isReservedMemberName(name: __String) { - return (name as string).charCodeAt(0) === CharacterCodes._ && - (name as string).charCodeAt(1) === CharacterCodes._ && - (name as string).charCodeAt(2) !== CharacterCodes._ && - (name as string).charCodeAt(2) !== CharacterCodes.at && - (name as string).charCodeAt(2) !== CharacterCodes.hash; + if (flags & SymbolFormatFlags.WriteTypeParametersOrArguments) { + nodeFlags |= NodeBuilderFlags.WriteTypeParametersInQualifiedName; } - - function getNamedMembers(members: SymbolTable): Symbol[] { - let result: Symbol[] | undefined; - members.forEach((symbol, id) => { - if (!isReservedMemberName(id) && symbolIsValue(symbol)) { - (result || (result = [])).push(symbol); - } - }); - return result || emptyArray; + if (flags & SymbolFormatFlags.UseAliasDefinedOutsideCurrentScope) { + nodeFlags |= NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope; } - - function setStructuredTypeMembers(type: StructuredType, members: SymbolTable, callSignatures: readonly Signature[], constructSignatures: readonly Signature[], stringIndexInfo: IndexInfo | undefined, numberIndexInfo: IndexInfo | undefined): ResolvedType { - (type).members = members; - (type).properties = members === emptySymbols ? emptyArray : getNamedMembers(members); - (type).callSignatures = callSignatures; - (type).constructSignatures = constructSignatures; - (type).stringIndexInfo = stringIndexInfo; - (type).numberIndexInfo = numberIndexInfo; - return type; + if (flags & SymbolFormatFlags.DoNotIncludeSymbolChain) { + nodeFlags |= NodeBuilderFlags.DoNotIncludeSymbolChain; } - - function createAnonymousType(symbol: Symbol | undefined, members: SymbolTable, callSignatures: readonly Signature[], constructSignatures: readonly Signature[], stringIndexInfo: IndexInfo | undefined, numberIndexInfo: IndexInfo | undefined): ResolvedType { - return setStructuredTypeMembers(createObjectType(ObjectFlags.Anonymous, symbol), - members, callSignatures, constructSignatures, stringIndexInfo, numberIndexInfo); + const builder = flags & SymbolFormatFlags.AllowAnyNodeKind ? nodeBuilder.symbolToExpression : nodeBuilder.symbolToEntityName; + return writer ? symbolToStringWorker(writer).getText() : usingSingleLineStringWriter(symbolToStringWorker); + function symbolToStringWorker(writer: EmitTextWriter) { + const entity = builder(symbol, meaning!, enclosingDeclaration, nodeFlags)!; // TODO: GH#18217 + const printer = createPrinter({ removeComments: true }); + const sourceFile = enclosingDeclaration && getSourceFileOfNode(enclosingDeclaration); + printer.writeNode(EmitHint.Unspecified, entity, /*sourceFile*/ sourceFile, writer); + return writer; } - - function forEachSymbolTableInScope(enclosingDeclaration: Node | undefined, callback: (symbolTable: SymbolTable) => T): T { - let result: T; - for (let location = enclosingDeclaration; location; location = location.parent) { - // Locals of a source file are not in scope (because they get merged into the global symbol table) - if (location.locals && !isGlobalSourceFile(location)) { - if (result = callback(location.locals)) { - return result; - } - } - switch (location.kind) { - case SyntaxKind.SourceFile: - if (!isExternalOrCommonJsModule(location)) { - break; - } - // falls through - case SyntaxKind.ModuleDeclaration: - const sym = getSymbolOfNode(location as ModuleDeclaration); - // `sym` may not have exports if this module declaration is backed by the symbol for a `const` that's being rewritten - // into a namespace - in such cases, it's best to just let the namespace appear empty (the const members couldn't have referred - // to one another anyway) - if (result = callback(sym?.exports || emptySymbols)) { - return result; - } - break; - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ClassExpression: - case SyntaxKind.InterfaceDeclaration: - // Type parameters are bound into `members` lists so they can merge across declarations - // This is troublesome, since in all other respects, they behave like locals :cries: - // TODO: the below is shared with similar code in `resolveName` - in fact, rephrasing all this symbol - // lookup logic in terms of `resolveName` would be nice - // The below is used to lookup type parameters within a class or interface, as they are added to the class/interface locals - // These can never be latebound, so the symbol's raw members are sufficient. `getMembersOfNode` cannot be used, as it would - // trigger resolving late-bound names, which we may already be in the process of doing while we're here! - let table: UnderscoreEscapedMap | undefined; - // TODO: Should this filtered table be cached in some way? - (getSymbolOfNode(location as ClassLikeDeclaration | InterfaceDeclaration).members || emptySymbols).forEach((memberSymbol, key) => { - if (memberSymbol.flags & (SymbolFlags.Type & ~SymbolFlags.Assignment)) { - (table || (table = createSymbolTable())).set(key, memberSymbol); - } - }); - if (table && (result = callback(table))) { - return result; - } - break; - } + } + function signatureToString(signature: ts.Signature, enclosingDeclaration?: Node, flags = TypeFormatFlags.None, kind?: SignatureKind, writer?: EmitTextWriter): string { + return writer ? signatureToStringWorker(writer).getText() : usingSingleLineStringWriter(signatureToStringWorker); + function signatureToStringWorker(writer: EmitTextWriter) { + let sigOutput: SyntaxKind; + if (flags & TypeFormatFlags.WriteArrowStyleSignature) { + sigOutput = kind === SignatureKind.Construct ? SyntaxKind.ConstructorType : SyntaxKind.FunctionType; } - - return callback(globals); + else { + sigOutput = kind === SignatureKind.Construct ? SyntaxKind.ConstructSignature : SyntaxKind.CallSignature; + } + const sig = nodeBuilder.signatureToSignatureDeclaration(signature, sigOutput, enclosingDeclaration, toNodeBuilderFlags(flags) | NodeBuilderFlags.IgnoreErrors | NodeBuilderFlags.WriteTypeParametersInQualifiedName); + const printer = createPrinter({ removeComments: true, omitTrailingSemicolon: true }); + const sourceFile = enclosingDeclaration && getSourceFileOfNode(enclosingDeclaration); + printer.writeNode(EmitHint.Unspecified, (sig!), /*sourceFile*/ sourceFile, getTrailingSemicolonDeferringWriter(writer)); // TODO: GH#18217 + return writer; } - - function getQualifiedLeftMeaning(rightMeaning: SymbolFlags) { - // If we are looking in value space, the parent meaning is value, other wise it is namespace - return rightMeaning === SymbolFlags.Value ? SymbolFlags.Value : SymbolFlags.Namespace; + } + function typeToString(type: ts.Type, enclosingDeclaration?: Node, flags: TypeFormatFlags = TypeFormatFlags.AllowUniqueESSymbolType | TypeFormatFlags.UseAliasDefinedOutsideCurrentScope, writer: EmitTextWriter = createTextWriter("")): string { + const noTruncation = compilerOptions.noErrorTruncation || flags & TypeFormatFlags.NoTruncation; + const typeNode = nodeBuilder.typeToTypeNode(type, enclosingDeclaration, toNodeBuilderFlags(flags) | NodeBuilderFlags.IgnoreErrors | (noTruncation ? NodeBuilderFlags.NoTruncation : 0), writer); + if (typeNode === undefined) + return Debug.fail("should always get typenode"); + const options = { removeComments: true }; + const printer = createPrinter(options); + const sourceFile = enclosingDeclaration && getSourceFileOfNode(enclosingDeclaration); + printer.writeNode(EmitHint.Unspecified, typeNode, /*sourceFile*/ sourceFile, writer); + const result = writer.getText(); + const maxLength = noTruncation ? undefined : defaultMaximumTruncationLength * 2; + if (maxLength && result && result.length >= maxLength) { + return result.substr(0, maxLength - "...".length) + "..."; + } + return result; + } + function getTypeNamesForErrorDisplay(left: ts.Type, right: ts.Type): [string, string] { + let leftStr = symbolValueDeclarationIsContextSensitive(left.symbol) ? typeToString(left, left.symbol.valueDeclaration) : typeToString(left); + let rightStr = symbolValueDeclarationIsContextSensitive(right.symbol) ? typeToString(right, right.symbol.valueDeclaration) : typeToString(right); + if (leftStr === rightStr) { + leftStr = typeToString(left, /*enclosingDeclaration*/ undefined, TypeFormatFlags.UseFullyQualifiedType); + rightStr = typeToString(right, /*enclosingDeclaration*/ undefined, TypeFormatFlags.UseFullyQualifiedType); + } + return [leftStr, rightStr]; + } + function symbolValueDeclarationIsContextSensitive(symbol: ts.Symbol): boolean { + return symbol && symbol.valueDeclaration && isExpression(symbol.valueDeclaration) && !isContextSensitive(symbol.valueDeclaration); + } + function toNodeBuilderFlags(flags = TypeFormatFlags.None): NodeBuilderFlags { + return flags & TypeFormatFlags.NodeBuilderFlagsMask; + } + function createNodeBuilder() { + return { + typeToTypeNode: (type: ts.Type, enclosingDeclaration?: Node, flags?: NodeBuilderFlags, tracker?: SymbolTracker) => withContext(enclosingDeclaration, flags, tracker, context => typeToTypeNodeHelper(type, context)), + indexInfoToIndexSignatureDeclaration: (indexInfo: IndexInfo, kind: IndexKind, enclosingDeclaration?: Node, flags?: NodeBuilderFlags, tracker?: SymbolTracker) => withContext(enclosingDeclaration, flags, tracker, context => indexInfoToIndexSignatureDeclarationHelper(indexInfo, kind, context)), + signatureToSignatureDeclaration: (signature: ts.Signature, kind: SyntaxKind, enclosingDeclaration?: Node, flags?: NodeBuilderFlags, tracker?: SymbolTracker) => withContext(enclosingDeclaration, flags, tracker, context => signatureToSignatureDeclarationHelper(signature, kind, context)), + symbolToEntityName: (symbol: ts.Symbol, meaning: SymbolFlags, enclosingDeclaration?: Node, flags?: NodeBuilderFlags, tracker?: SymbolTracker) => withContext(enclosingDeclaration, flags, tracker, context => symbolToName(symbol, context, meaning, /*expectsIdentifier*/ false)), + symbolToExpression: (symbol: ts.Symbol, meaning: SymbolFlags, enclosingDeclaration?: Node, flags?: NodeBuilderFlags, tracker?: SymbolTracker) => withContext(enclosingDeclaration, flags, tracker, context => symbolToExpression(symbol, context, meaning)), + symbolToTypeParameterDeclarations: (symbol: ts.Symbol, enclosingDeclaration?: Node, flags?: NodeBuilderFlags, tracker?: SymbolTracker) => withContext(enclosingDeclaration, flags, tracker, context => typeParametersToTypeParameterDeclarations(symbol, context)), + symbolToParameterDeclaration: (symbol: ts.Symbol, enclosingDeclaration?: Node, flags?: NodeBuilderFlags, tracker?: SymbolTracker) => withContext(enclosingDeclaration, flags, tracker, context => symbolToParameterDeclaration(symbol, context)), + typeParameterToDeclaration: (parameter: TypeParameter, enclosingDeclaration?: Node, flags?: NodeBuilderFlags, tracker?: SymbolTracker) => withContext(enclosingDeclaration, flags, tracker, context => typeParameterToDeclaration(parameter, context)), + symbolTableToDeclarationStatements: (symbolTable: SymbolTable, enclosingDeclaration?: Node, flags?: NodeBuilderFlags, tracker?: SymbolTracker, bundled?: boolean) => withContext(enclosingDeclaration, flags, tracker, context => symbolTableToDeclarationStatements(symbolTable, context, bundled)), + }; + function withContext(enclosingDeclaration: Node | undefined, flags: NodeBuilderFlags | undefined, tracker: SymbolTracker | undefined, cb: (context: NodeBuilderContext) => T): T | undefined { + Debug.assert(enclosingDeclaration === undefined || (enclosingDeclaration.flags & NodeFlags.Synthesized) === 0); + const context: NodeBuilderContext = { + enclosingDeclaration, + flags: flags || NodeBuilderFlags.None, + // If no full tracker is provided, fake up a dummy one with a basic limited-functionality moduleResolverHost + tracker: tracker && tracker.trackSymbol ? tracker : { trackSymbol: noop, moduleResolverHost: (flags!) & NodeBuilderFlags.DoNotIncludeSymbolChain ? { + getCommonSourceDirectory: (host as Program).getCommonSourceDirectory ? () => (host as Program).getCommonSourceDirectory() : () => "", + getSourceFiles: () => host.getSourceFiles(), + getCurrentDirectory: maybeBind(host, host.getCurrentDirectory), + getProbableSymlinks: maybeBind(host, host.getProbableSymlinks), + } : undefined }, + encounteredError: false, + visitedTypes: undefined, + symbolDepth: undefined, + inferTypeParameters: undefined, + approximateLength: 0 + }; + const resultingNode = cb(context); + return context.encounteredError ? undefined : resultingNode; } - - function getAccessibleSymbolChain(symbol: Symbol | undefined, enclosingDeclaration: Node | undefined, meaning: SymbolFlags, useOnlyExternalAliasing: boolean, visitedSymbolTablesMap: Map = createMap()): Symbol[] | undefined { - if (!(symbol && !isPropertyOrMethodDeclarationSymbol(symbol))) { - return undefined; + function checkTruncationLength(context: NodeBuilderContext): boolean { + if (context.truncating) + return context.truncating; + return context.truncating = !(context.flags & NodeBuilderFlags.NoTruncation) && context.approximateLength > defaultMaximumTruncationLength; + } + function typeToTypeNodeHelper(type: ts.Type, context: NodeBuilderContext): TypeNode { + if (cancellationToken && cancellationToken.throwIfCancellationRequested) { + cancellationToken.throwIfCancellationRequested(); } - - const id = "" + getSymbolId(symbol); - let visitedSymbolTables = visitedSymbolTablesMap.get(id); - if (!visitedSymbolTables) { - visitedSymbolTablesMap.set(id, visitedSymbolTables = []); + const inTypeAlias = context.flags & NodeBuilderFlags.InTypeAlias; + context.flags &= ~NodeBuilderFlags.InTypeAlias; + if (!type) { + context.encounteredError = true; + return undefined!; // TODO: GH#18217 } - return forEachSymbolTableInScope(enclosingDeclaration, getAccessibleSymbolChainFromSymbolTable); - - /** - * @param {ignoreQualification} boolean Set when a symbol is being looked for through the exports of another symbol (meaning we have a route to qualify it already) - */ - function getAccessibleSymbolChainFromSymbolTable(symbols: SymbolTable, ignoreQualification?: boolean): Symbol[] | undefined { - if (!pushIfUnique(visitedSymbolTables!, symbols)) { - return undefined; - } - - const result = trySymbolTable(symbols, ignoreQualification); - visitedSymbolTables!.pop(); - return result; + if (type.flags & TypeFlags.Any) { + context.approximateLength += 3; + return createKeywordTypeNode(SyntaxKind.AnyKeyword); } - - function canQualifySymbol(symbolFromSymbolTable: Symbol, meaning: SymbolFlags) { - // If the symbol is equivalent and doesn't need further qualification, this symbol is accessible - return !needsQualification(symbolFromSymbolTable, enclosingDeclaration, meaning) || - // If symbol needs qualification, make sure that parent is accessible, if it is then this symbol is accessible too - !!getAccessibleSymbolChain(symbolFromSymbolTable.parent, enclosingDeclaration, getQualifiedLeftMeaning(meaning), useOnlyExternalAliasing, visitedSymbolTablesMap); + if (type.flags & TypeFlags.Unknown) { + return createKeywordTypeNode(SyntaxKind.UnknownKeyword); } - - function isAccessible(symbolFromSymbolTable: Symbol, resolvedAliasSymbol?: Symbol, ignoreQualification?: boolean) { - return (symbol === (resolvedAliasSymbol || symbolFromSymbolTable) || getMergedSymbol(symbol) === getMergedSymbol(resolvedAliasSymbol || symbolFromSymbolTable)) && - // if the symbolFromSymbolTable is not external module (it could be if it was determined as ambient external module and would be in globals table) - // and if symbolFromSymbolTable or alias resolution matches the symbol, - // check the symbol can be qualified, it is only then this symbol is accessible - !some(symbolFromSymbolTable.declarations, hasNonGlobalAugmentationExternalModuleSymbol) && - (ignoreQualification || canQualifySymbol(getMergedSymbol(symbolFromSymbolTable), meaning)); + if (type.flags & TypeFlags.String) { + context.approximateLength += 6; + return createKeywordTypeNode(SyntaxKind.StringKeyword); } - - function trySymbolTable(symbols: SymbolTable, ignoreQualification: boolean | undefined): Symbol[] | undefined { - // If symbol is directly available by its name in the symbol table - if (isAccessible(symbols.get(symbol!.escapedName)!, /*resolvedAliasSymbol*/ undefined, ignoreQualification)) { - return [symbol!]; - } - - // Check if symbol is any of the aliases in scope - const result = forEachEntry(symbols, symbolFromSymbolTable => { - if (symbolFromSymbolTable.flags & SymbolFlags.Alias - && symbolFromSymbolTable.escapedName !== InternalSymbolName.ExportEquals - && symbolFromSymbolTable.escapedName !== InternalSymbolName.Default - && !(isUMDExportSymbol(symbolFromSymbolTable) && enclosingDeclaration && isExternalModule(getSourceFileOfNode(enclosingDeclaration))) - // If `!useOnlyExternalAliasing`, we can use any type of alias to get the name - && (!useOnlyExternalAliasing || some(symbolFromSymbolTable.declarations, isExternalModuleImportEqualsDeclaration)) - // While exports are generally considered to be in scope, export-specifier declared symbols are _not_ - // See similar comment in `resolveName` for details - && (ignoreQualification || !getDeclarationOfKind(symbolFromSymbolTable, SyntaxKind.ExportSpecifier)) - ) { - - const resolvedImportedSymbol = resolveAlias(symbolFromSymbolTable); - const candidate = getCandidateListForSymbol(symbolFromSymbolTable, resolvedImportedSymbol, ignoreQualification); - if (candidate) { - return candidate; - } - } - if (symbolFromSymbolTable.escapedName === symbol!.escapedName && symbolFromSymbolTable.exportSymbol) { - if (isAccessible(getMergedSymbol(symbolFromSymbolTable.exportSymbol), /*aliasSymbol*/ undefined, ignoreQualification)) { - return [symbol!]; - } - } - }); - - // If there's no result and we're looking at the global symbol table, treat `globalThis` like an alias and try to lookup thru that - return result || (symbols === globals ? getCandidateListForSymbol(globalThisSymbol, globalThisSymbol, ignoreQualification) : undefined); + if (type.flags & TypeFlags.Number) { + context.approximateLength += 6; + return createKeywordTypeNode(SyntaxKind.NumberKeyword); } - - function getCandidateListForSymbol(symbolFromSymbolTable: Symbol, resolvedImportedSymbol: Symbol, ignoreQualification: boolean | undefined) { - if (isAccessible(symbolFromSymbolTable, resolvedImportedSymbol, ignoreQualification)) { - return [symbolFromSymbolTable]; - } - - // Look in the exported members, if we can find accessibleSymbolChain, symbol is accessible using this chain - // but only if the symbolFromSymbolTable can be qualified - const candidateTable = getExportsOfSymbol(resolvedImportedSymbol); - const accessibleSymbolsFromExports = candidateTable && getAccessibleSymbolChainFromSymbolTable(candidateTable, /*ignoreQualification*/ true); - if (accessibleSymbolsFromExports && canQualifySymbol(symbolFromSymbolTable, getQualifiedLeftMeaning(meaning))) { - return [symbolFromSymbolTable].concat(accessibleSymbolsFromExports); - } + if (type.flags & TypeFlags.BigInt) { + context.approximateLength += 6; + return createKeywordTypeNode(SyntaxKind.BigIntKeyword); } - } - - function needsQualification(symbol: Symbol, enclosingDeclaration: Node | undefined, meaning: SymbolFlags) { - let qualify = false; - forEachSymbolTableInScope(enclosingDeclaration, symbolTable => { - // If symbol of this name is not available in the symbol table we are ok - let symbolFromSymbolTable = getMergedSymbol(symbolTable.get(symbol.escapedName)); - if (!symbolFromSymbolTable) { - // Continue to the next symbol table - return false; - } - // If the symbol with this name is present it should refer to the symbol - if (symbolFromSymbolTable === symbol) { - // No need to qualify - return true; - } - - // Qualify if the symbol from symbol table has same meaning as expected - symbolFromSymbolTable = (symbolFromSymbolTable.flags & SymbolFlags.Alias && !getDeclarationOfKind(symbolFromSymbolTable, SyntaxKind.ExportSpecifier)) ? resolveAlias(symbolFromSymbolTable) : symbolFromSymbolTable; - if (symbolFromSymbolTable.flags & meaning) { - qualify = true; - return true; - } - - // Continue to the next symbol table - return false; - }); - - return qualify; - } - - function isPropertyOrMethodDeclarationSymbol(symbol: Symbol) { - if (symbol.declarations && symbol.declarations.length) { - for (const declaration of symbol.declarations) { - switch (declaration.kind) { - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - continue; - default: - return false; + if (type.flags & TypeFlags.Boolean) { + context.approximateLength += 7; + return createKeywordTypeNode(SyntaxKind.BooleanKeyword); + } + if (type.flags & TypeFlags.EnumLiteral && !(type.flags & TypeFlags.Union)) { + const parentSymbol = getParentOfSymbol(type.symbol)!; + const parentName = symbolToTypeNode(parentSymbol, context, SymbolFlags.Type); + const enumLiteralName = getDeclaredTypeOfSymbol(parentSymbol) === type + ? parentName + : appendReferenceToType((parentName as TypeReferenceNode | ImportTypeNode), createTypeReferenceNode(symbolName(type.symbol), /*typeArguments*/ undefined)); + return enumLiteralName; + } + if (type.flags & TypeFlags.EnumLike) { + return symbolToTypeNode(type.symbol, context, SymbolFlags.Type); + } + if (type.flags & TypeFlags.StringLiteral) { + context.approximateLength += ((type).value.length + 2); + return createLiteralTypeNode(setEmitFlags(createLiteral((type).value, !!(context.flags & NodeBuilderFlags.UseSingleQuotesForStringLiteralType)), EmitFlags.NoAsciiEscaping)); + } + if (type.flags & TypeFlags.NumberLiteral) { + const value = (type).value; + context.approximateLength += ("" + value).length; + return createLiteralTypeNode(value < 0 ? createPrefix(SyntaxKind.MinusToken, createLiteral(-value)) : createLiteral(value)); + } + if (type.flags & TypeFlags.BigIntLiteral) { + context.approximateLength += (pseudoBigIntToString((type).value).length) + 1; + return createLiteralTypeNode((createLiteral((type).value))); + } + if (type.flags & TypeFlags.BooleanLiteral) { + context.approximateLength += (type).intrinsicName.length; + return (type).intrinsicName === "true" ? createTrue() : createFalse(); + } + if (type.flags & TypeFlags.UniqueESSymbol) { + if (!(context.flags & NodeBuilderFlags.AllowUniqueESSymbolType)) { + if (isValueSymbolAccessible(type.symbol, context.enclosingDeclaration)) { + context.approximateLength += 6; + return symbolToTypeNode(type.symbol, context, SymbolFlags.Value); + } + if (context.tracker.reportInaccessibleUniqueSymbolError) { + context.tracker.reportInaccessibleUniqueSymbolError(); } } - return true; + context.approximateLength += 13; + return createTypeOperatorNode(SyntaxKind.UniqueKeyword, createKeywordTypeNode(SyntaxKind.SymbolKeyword)); } - return false; - } - - function isTypeSymbolAccessible(typeSymbol: Symbol, enclosingDeclaration: Node | undefined): boolean { - const access = isSymbolAccessible(typeSymbol, enclosingDeclaration, SymbolFlags.Type, /*shouldComputeAliasesToMakeVisible*/ false); - return access.accessibility === SymbolAccessibility.Accessible; - } - - function isValueSymbolAccessible(typeSymbol: Symbol, enclosingDeclaration: Node | undefined): boolean { - const access = isSymbolAccessible(typeSymbol, enclosingDeclaration, SymbolFlags.Value, /*shouldComputeAliasesToMakeVisible*/ false); - return access.accessibility === SymbolAccessibility.Accessible; - } - - function isAnySymbolAccessible(symbols: Symbol[] | undefined, enclosingDeclaration: Node | undefined, initialSymbol: Symbol, meaning: SymbolFlags, shouldComputeAliasesToMakeVisible: boolean): SymbolAccessibilityResult | undefined { - if (!length(symbols)) return; - - let hadAccessibleChain: Symbol | undefined; - let earlyModuleBail = false; - for (const symbol of symbols!) { - // Symbol is accessible if it by itself is accessible - const accessibleSymbolChain = getAccessibleSymbolChain(symbol, enclosingDeclaration, meaning, /*useOnlyExternalAliasing*/ false); - if (accessibleSymbolChain) { - hadAccessibleChain = symbol; - const hasAccessibleDeclarations = hasVisibleDeclarations(accessibleSymbolChain[0], shouldComputeAliasesToMakeVisible); - if (hasAccessibleDeclarations) { - return hasAccessibleDeclarations; - } - } - else { - if (some(symbol.declarations, hasNonGlobalAugmentationExternalModuleSymbol)) { - if (shouldComputeAliasesToMakeVisible) { - earlyModuleBail = true; - // Generally speaking, we want to use the aliases that already exist to refer to a module, if present - // In order to do so, we need to find those aliases in order to retain them in declaration emit; so - // if we are in declaration emit, we cannot use the fast path for module visibility until we've exhausted - // all other visibility options (in order to capture the possible aliases used to reference the module) - continue; - } - // Any meaning of a module symbol is always accessible via an `import` type - return { - accessibility: SymbolAccessibility.Accessible - }; - } - } - - // If we haven't got the accessible symbol, it doesn't mean the symbol is actually inaccessible. - // It could be a qualified symbol and hence verify the path - // e.g.: - // module m { - // export class c { - // } - // } - // const x: typeof m.c - // In the above example when we start with checking if typeof m.c symbol is accessible, - // we are going to see if c can be accessed in scope directly. - // But it can't, hence the accessible is going to be undefined, but that doesn't mean m.c is inaccessible - // It is accessible if the parent m is accessible because then m.c can be accessed through qualification - - let containers = getContainersOfSymbol(symbol, enclosingDeclaration); - // If we're trying to reference some object literal in, eg `var a = { x: 1 }`, the symbol for the literal, `__object`, is distinct - // from the symbol of the declaration it is being assigned to. Since we can use the declaration to refer to the literal, however, - // we'd like to make that connection here - potentially causing us to paint the declaration's visibility, and therefore the literal. - const firstDecl: Node = first(symbol.declarations); - if (!length(containers) && meaning & SymbolFlags.Value && firstDecl && isObjectLiteralExpression(firstDecl)) { - if (firstDecl.parent && isVariableDeclaration(firstDecl.parent) && firstDecl === firstDecl.parent.initializer) { - containers = [getSymbolOfNode(firstDecl.parent)]; - } - } - const parentResult = isAnySymbolAccessible(containers, enclosingDeclaration, initialSymbol, initialSymbol === symbol ? getQualifiedLeftMeaning(meaning) : meaning, shouldComputeAliasesToMakeVisible); - if (parentResult) { - return parentResult; - } - } - - if (earlyModuleBail) { - return { - accessibility: SymbolAccessibility.Accessible - }; - } - - if (hadAccessibleChain) { - return { - accessibility: SymbolAccessibility.NotAccessible, - errorSymbolName: symbolToString(initialSymbol, enclosingDeclaration, meaning), - errorModuleName: hadAccessibleChain !== initialSymbol ? symbolToString(hadAccessibleChain, enclosingDeclaration, SymbolFlags.Namespace) : undefined, - }; - } - } - - /** - * Check if the given symbol in given enclosing declaration is accessible and mark all associated alias to be visible if requested - * - * @param symbol a Symbol to check if accessible - * @param enclosingDeclaration a Node containing reference to the symbol - * @param meaning a SymbolFlags to check if such meaning of the symbol is accessible - * @param shouldComputeAliasToMakeVisible a boolean value to indicate whether to return aliases to be mark visible in case the symbol is accessible - */ - function isSymbolAccessible(symbol: Symbol | undefined, enclosingDeclaration: Node | undefined, meaning: SymbolFlags, shouldComputeAliasesToMakeVisible: boolean): SymbolAccessibilityResult { - if (symbol && enclosingDeclaration) { - const result = isAnySymbolAccessible([symbol], enclosingDeclaration, symbol, meaning, shouldComputeAliasesToMakeVisible); - if (result) { - return result; - } - - // This could be a symbol that is not exported in the external module - // or it could be a symbol from different external module that is not aliased and hence cannot be named - const symbolExternalModule = forEach(symbol.declarations, getExternalModuleContainer); - if (symbolExternalModule) { - const enclosingExternalModule = getExternalModuleContainer(enclosingDeclaration); - if (symbolExternalModule !== enclosingExternalModule) { - // name from different external module that is not visible - return { - accessibility: SymbolAccessibility.CannotBeNamed, - errorSymbolName: symbolToString(symbol, enclosingDeclaration, meaning), - errorModuleName: symbolToString(symbolExternalModule) - }; - } - } - - // Just a local name that is not accessible - return { - accessibility: SymbolAccessibility.NotAccessible, - errorSymbolName: symbolToString(symbol, enclosingDeclaration, meaning), - }; - } - - return { accessibility: SymbolAccessibility.Accessible }; - } - - function getExternalModuleContainer(declaration: Node) { - const node = findAncestor(declaration, hasExternalModuleSymbol); - return node && getSymbolOfNode(node); - } - - function hasExternalModuleSymbol(declaration: Node) { - return isAmbientModule(declaration) || (declaration.kind === SyntaxKind.SourceFile && isExternalOrCommonJsModule(declaration)); - } - - function hasNonGlobalAugmentationExternalModuleSymbol(declaration: Node) { - return isModuleWithStringLiteralName(declaration) || (declaration.kind === SyntaxKind.SourceFile && isExternalOrCommonJsModule(declaration)); - } - - function hasVisibleDeclarations(symbol: Symbol, shouldComputeAliasToMakeVisible: boolean): SymbolVisibilityResult | undefined { - let aliasesToMakeVisible: LateVisibilityPaintedStatement[] | undefined; - if (!every(filter(symbol.declarations, d => d.kind !== SyntaxKind.Identifier), getIsDeclarationVisible)) { - return undefined; - } - return { accessibility: SymbolAccessibility.Accessible, aliasesToMakeVisible }; - - function getIsDeclarationVisible(declaration: Declaration) { - if (!isDeclarationVisible(declaration)) { - // Mark the unexported alias as visible if its parent is visible - // because these kind of aliases can be used to name types in declaration file - - const anyImportSyntax = getAnyImportSyntax(declaration); - if (anyImportSyntax && - !hasModifier(anyImportSyntax, ModifierFlags.Export) && // import clause without export - isDeclarationVisible(anyImportSyntax.parent)) { - return addVisibleAlias(declaration, anyImportSyntax); - } - else if (isVariableDeclaration(declaration) && isVariableStatement(declaration.parent.parent) && - !hasModifier(declaration.parent.parent, ModifierFlags.Export) && // unexported variable statement - isDeclarationVisible(declaration.parent.parent.parent)) { - return addVisibleAlias(declaration, declaration.parent.parent); - } - else if (isLateVisibilityPaintedStatement(declaration) // unexported top-level statement - && !hasModifier(declaration, ModifierFlags.Export) - && isDeclarationVisible(declaration.parent)) { - return addVisibleAlias(declaration, declaration); - } - - // Declaration is not visible - return false; - } - - return true; - } - - function addVisibleAlias(declaration: Declaration, aliasingStatement: LateVisibilityPaintedStatement) { - // In function "buildTypeDisplay" where we decide whether to write type-alias or serialize types, - // we want to just check if type- alias is accessible or not but we don't care about emitting those alias at that time - // since we will do the emitting later in trackSymbol. - if (shouldComputeAliasToMakeVisible) { - getNodeLinks(declaration).isVisible = true; - aliasesToMakeVisible = appendIfUnique(aliasesToMakeVisible, aliasingStatement); - } - return true; - } - } - - function isEntityNameVisible(entityName: EntityNameOrEntityNameExpression, enclosingDeclaration: Node): SymbolVisibilityResult { - // get symbol of the first identifier of the entityName - let meaning: SymbolFlags; - if (entityName.parent.kind === SyntaxKind.TypeQuery || - isExpressionWithTypeArgumentsInClassExtendsClause(entityName.parent) || - entityName.parent.kind === SyntaxKind.ComputedPropertyName) { - // Typeof value - meaning = SymbolFlags.Value | SymbolFlags.ExportValue; - } - else if (entityName.kind === SyntaxKind.QualifiedName || entityName.kind === SyntaxKind.PropertyAccessExpression || - entityName.parent.kind === SyntaxKind.ImportEqualsDeclaration) { - // Left identifier from type reference or TypeAlias - // Entity name of the import declaration - meaning = SymbolFlags.Namespace; - } - else { - // Type Reference or TypeAlias entity = Identifier - meaning = SymbolFlags.Type; - } - - const firstIdentifier = getFirstIdentifier(entityName); - const symbol = resolveName(enclosingDeclaration, firstIdentifier.escapedText, meaning, /*nodeNotFoundErrorMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false); - - // Verify if the symbol is accessible - return (symbol && hasVisibleDeclarations(symbol, /*shouldComputeAliasToMakeVisible*/ true)) || { - accessibility: SymbolAccessibility.NotAccessible, - errorSymbolName: getTextOfNode(firstIdentifier), - errorNode: firstIdentifier - }; - } - - function symbolToString(symbol: Symbol, enclosingDeclaration?: Node, meaning?: SymbolFlags, flags: SymbolFormatFlags = SymbolFormatFlags.AllowAnyNodeKind, writer?: EmitTextWriter): string { - let nodeFlags = NodeBuilderFlags.IgnoreErrors; - if (flags & SymbolFormatFlags.UseOnlyExternalAliasing) { - nodeFlags |= NodeBuilderFlags.UseOnlyExternalAliasing; - } - if (flags & SymbolFormatFlags.WriteTypeParametersOrArguments) { - nodeFlags |= NodeBuilderFlags.WriteTypeParametersInQualifiedName; - } - if (flags & SymbolFormatFlags.UseAliasDefinedOutsideCurrentScope) { - nodeFlags |= NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope; - } - if (flags & SymbolFormatFlags.DoNotIncludeSymbolChain) { - nodeFlags |= NodeBuilderFlags.DoNotIncludeSymbolChain; - } - const builder = flags & SymbolFormatFlags.AllowAnyNodeKind ? nodeBuilder.symbolToExpression : nodeBuilder.symbolToEntityName; - return writer ? symbolToStringWorker(writer).getText() : usingSingleLineStringWriter(symbolToStringWorker); - - function symbolToStringWorker(writer: EmitTextWriter) { - const entity = builder(symbol, meaning!, enclosingDeclaration, nodeFlags)!; // TODO: GH#18217 - const printer = createPrinter({ removeComments: true }); - const sourceFile = enclosingDeclaration && getSourceFileOfNode(enclosingDeclaration); - printer.writeNode(EmitHint.Unspecified, entity, /*sourceFile*/ sourceFile, writer); - return writer; - } - } - - function signatureToString(signature: Signature, enclosingDeclaration?: Node, flags = TypeFormatFlags.None, kind?: SignatureKind, writer?: EmitTextWriter): string { - return writer ? signatureToStringWorker(writer).getText() : usingSingleLineStringWriter(signatureToStringWorker); - - function signatureToStringWorker(writer: EmitTextWriter) { - let sigOutput: SyntaxKind; - if (flags & TypeFormatFlags.WriteArrowStyleSignature) { - sigOutput = kind === SignatureKind.Construct ? SyntaxKind.ConstructorType : SyntaxKind.FunctionType; - } - else { - sigOutput = kind === SignatureKind.Construct ? SyntaxKind.ConstructSignature : SyntaxKind.CallSignature; - } - const sig = nodeBuilder.signatureToSignatureDeclaration(signature, sigOutput, enclosingDeclaration, toNodeBuilderFlags(flags) | NodeBuilderFlags.IgnoreErrors | NodeBuilderFlags.WriteTypeParametersInQualifiedName); - const printer = createPrinter({ removeComments: true, omitTrailingSemicolon: true }); - const sourceFile = enclosingDeclaration && getSourceFileOfNode(enclosingDeclaration); - printer.writeNode(EmitHint.Unspecified, sig!, /*sourceFile*/ sourceFile, getTrailingSemicolonDeferringWriter(writer)); // TODO: GH#18217 - return writer; - } - } - - function typeToString(type: Type, enclosingDeclaration?: Node, flags: TypeFormatFlags = TypeFormatFlags.AllowUniqueESSymbolType | TypeFormatFlags.UseAliasDefinedOutsideCurrentScope, writer: EmitTextWriter = createTextWriter("")): string { - const noTruncation = compilerOptions.noErrorTruncation || flags & TypeFormatFlags.NoTruncation; - const typeNode = nodeBuilder.typeToTypeNode(type, enclosingDeclaration, toNodeBuilderFlags(flags) | NodeBuilderFlags.IgnoreErrors | (noTruncation ? NodeBuilderFlags.NoTruncation : 0), writer); - if (typeNode === undefined) return Debug.fail("should always get typenode"); - const options = { removeComments: true }; - const printer = createPrinter(options); - const sourceFile = enclosingDeclaration && getSourceFileOfNode(enclosingDeclaration); - printer.writeNode(EmitHint.Unspecified, typeNode, /*sourceFile*/ sourceFile, writer); - const result = writer.getText(); - - const maxLength = noTruncation ? undefined : defaultMaximumTruncationLength * 2; - if (maxLength && result && result.length >= maxLength) { - return result.substr(0, maxLength - "...".length) + "..."; - } - return result; - } - - function getTypeNamesForErrorDisplay(left: Type, right: Type): [string, string] { - let leftStr = symbolValueDeclarationIsContextSensitive(left.symbol) ? typeToString(left, left.symbol.valueDeclaration) : typeToString(left); - let rightStr = symbolValueDeclarationIsContextSensitive(right.symbol) ? typeToString(right, right.symbol.valueDeclaration) : typeToString(right); - if (leftStr === rightStr) { - leftStr = typeToString(left, /*enclosingDeclaration*/ undefined, TypeFormatFlags.UseFullyQualifiedType); - rightStr = typeToString(right, /*enclosingDeclaration*/ undefined, TypeFormatFlags.UseFullyQualifiedType); - } - return [leftStr, rightStr]; - } - - function symbolValueDeclarationIsContextSensitive(symbol: Symbol): boolean { - return symbol && symbol.valueDeclaration && isExpression(symbol.valueDeclaration) && !isContextSensitive(symbol.valueDeclaration); - } - - function toNodeBuilderFlags(flags = TypeFormatFlags.None): NodeBuilderFlags { - return flags & TypeFormatFlags.NodeBuilderFlagsMask; - } - - function createNodeBuilder() { - return { - typeToTypeNode: (type: Type, enclosingDeclaration?: Node, flags?: NodeBuilderFlags, tracker?: SymbolTracker) => - withContext(enclosingDeclaration, flags, tracker, context => typeToTypeNodeHelper(type, context)), - indexInfoToIndexSignatureDeclaration: (indexInfo: IndexInfo, kind: IndexKind, enclosingDeclaration?: Node, flags?: NodeBuilderFlags, tracker?: SymbolTracker) => - withContext(enclosingDeclaration, flags, tracker, context => indexInfoToIndexSignatureDeclarationHelper(indexInfo, kind, context)), - signatureToSignatureDeclaration: (signature: Signature, kind: SyntaxKind, enclosingDeclaration?: Node, flags?: NodeBuilderFlags, tracker?: SymbolTracker) => - withContext(enclosingDeclaration, flags, tracker, context => signatureToSignatureDeclarationHelper(signature, kind, context)), - symbolToEntityName: (symbol: Symbol, meaning: SymbolFlags, enclosingDeclaration?: Node, flags?: NodeBuilderFlags, tracker?: SymbolTracker) => - withContext(enclosingDeclaration, flags, tracker, context => symbolToName(symbol, context, meaning, /*expectsIdentifier*/ false)), - symbolToExpression: (symbol: Symbol, meaning: SymbolFlags, enclosingDeclaration?: Node, flags?: NodeBuilderFlags, tracker?: SymbolTracker) => - withContext(enclosingDeclaration, flags, tracker, context => symbolToExpression(symbol, context, meaning)), - symbolToTypeParameterDeclarations: (symbol: Symbol, enclosingDeclaration?: Node, flags?: NodeBuilderFlags, tracker?: SymbolTracker) => - withContext(enclosingDeclaration, flags, tracker, context => typeParametersToTypeParameterDeclarations(symbol, context)), - symbolToParameterDeclaration: (symbol: Symbol, enclosingDeclaration?: Node, flags?: NodeBuilderFlags, tracker?: SymbolTracker) => - withContext(enclosingDeclaration, flags, tracker, context => symbolToParameterDeclaration(symbol, context)), - typeParameterToDeclaration: (parameter: TypeParameter, enclosingDeclaration?: Node, flags?: NodeBuilderFlags, tracker?: SymbolTracker) => - withContext(enclosingDeclaration, flags, tracker, context => typeParameterToDeclaration(parameter, context)), - symbolTableToDeclarationStatements: (symbolTable: SymbolTable, enclosingDeclaration?: Node, flags?: NodeBuilderFlags, tracker?: SymbolTracker, bundled?: boolean) => - withContext(enclosingDeclaration, flags, tracker, context => symbolTableToDeclarationStatements(symbolTable, context, bundled)), - }; - - function withContext(enclosingDeclaration: Node | undefined, flags: NodeBuilderFlags | undefined, tracker: SymbolTracker | undefined, cb: (context: NodeBuilderContext) => T): T | undefined { - Debug.assert(enclosingDeclaration === undefined || (enclosingDeclaration.flags & NodeFlags.Synthesized) === 0); - const context: NodeBuilderContext = { - enclosingDeclaration, - flags: flags || NodeBuilderFlags.None, - // If no full tracker is provided, fake up a dummy one with a basic limited-functionality moduleResolverHost - tracker: tracker && tracker.trackSymbol ? tracker : { trackSymbol: noop, moduleResolverHost: flags! & NodeBuilderFlags.DoNotIncludeSymbolChain ? { - getCommonSourceDirectory: (host as Program).getCommonSourceDirectory ? () => (host as Program).getCommonSourceDirectory() : () => "", - getSourceFiles: () => host.getSourceFiles(), - getCurrentDirectory: maybeBind(host, host.getCurrentDirectory), - getProbableSymlinks: maybeBind(host, host.getProbableSymlinks), - } : undefined }, - encounteredError: false, - visitedTypes: undefined, - symbolDepth: undefined, - inferTypeParameters: undefined, - approximateLength: 0 - }; - const resultingNode = cb(context); - return context.encounteredError ? undefined : resultingNode; - } - - function checkTruncationLength(context: NodeBuilderContext): boolean { - if (context.truncating) return context.truncating; - return context.truncating = !(context.flags & NodeBuilderFlags.NoTruncation) && context.approximateLength > defaultMaximumTruncationLength; - } - - function typeToTypeNodeHelper(type: Type, context: NodeBuilderContext): TypeNode { - if (cancellationToken && cancellationToken.throwIfCancellationRequested) { - cancellationToken.throwIfCancellationRequested(); - } - const inTypeAlias = context.flags & NodeBuilderFlags.InTypeAlias; - context.flags &= ~NodeBuilderFlags.InTypeAlias; - - if (!type) { - context.encounteredError = true; - return undefined!; // TODO: GH#18217 - } - - if (type.flags & TypeFlags.Any) { - context.approximateLength += 3; - return createKeywordTypeNode(SyntaxKind.AnyKeyword); - } - if (type.flags & TypeFlags.Unknown) { - return createKeywordTypeNode(SyntaxKind.UnknownKeyword); - } - if (type.flags & TypeFlags.String) { - context.approximateLength += 6; - return createKeywordTypeNode(SyntaxKind.StringKeyword); - } - if (type.flags & TypeFlags.Number) { - context.approximateLength += 6; - return createKeywordTypeNode(SyntaxKind.NumberKeyword); - } - if (type.flags & TypeFlags.BigInt) { - context.approximateLength += 6; - return createKeywordTypeNode(SyntaxKind.BigIntKeyword); - } - if (type.flags & TypeFlags.Boolean) { - context.approximateLength += 7; - return createKeywordTypeNode(SyntaxKind.BooleanKeyword); - } - if (type.flags & TypeFlags.EnumLiteral && !(type.flags & TypeFlags.Union)) { - const parentSymbol = getParentOfSymbol(type.symbol)!; - const parentName = symbolToTypeNode(parentSymbol, context, SymbolFlags.Type); - const enumLiteralName = getDeclaredTypeOfSymbol(parentSymbol) === type - ? parentName - : appendReferenceToType( - parentName as TypeReferenceNode | ImportTypeNode, - createTypeReferenceNode(symbolName(type.symbol), /*typeArguments*/ undefined) - ); - return enumLiteralName; - } - if (type.flags & TypeFlags.EnumLike) { - return symbolToTypeNode(type.symbol, context, SymbolFlags.Type); - } - if (type.flags & TypeFlags.StringLiteral) { - context.approximateLength += ((type).value.length + 2); - return createLiteralTypeNode(setEmitFlags(createLiteral((type).value, !!(context.flags & NodeBuilderFlags.UseSingleQuotesForStringLiteralType)), EmitFlags.NoAsciiEscaping)); - } - if (type.flags & TypeFlags.NumberLiteral) { - const value = (type).value; - context.approximateLength += ("" + value).length; - return createLiteralTypeNode(value < 0 ? createPrefix(SyntaxKind.MinusToken, createLiteral(-value)) : createLiteral(value)); - } - if (type.flags & TypeFlags.BigIntLiteral) { - context.approximateLength += (pseudoBigIntToString((type).value).length) + 1; - return createLiteralTypeNode((createLiteral((type).value))); - } - if (type.flags & TypeFlags.BooleanLiteral) { - context.approximateLength += (type).intrinsicName.length; - return (type).intrinsicName === "true" ? createTrue() : createFalse(); - } - if (type.flags & TypeFlags.UniqueESSymbol) { - if (!(context.flags & NodeBuilderFlags.AllowUniqueESSymbolType)) { - if (isValueSymbolAccessible(type.symbol, context.enclosingDeclaration)) { - context.approximateLength += 6; - return symbolToTypeNode(type.symbol, context, SymbolFlags.Value); - } - if (context.tracker.reportInaccessibleUniqueSymbolError) { - context.tracker.reportInaccessibleUniqueSymbolError(); - } - } - context.approximateLength += 13; - return createTypeOperatorNode(SyntaxKind.UniqueKeyword, createKeywordTypeNode(SyntaxKind.SymbolKeyword)); - } - if (type.flags & TypeFlags.Void) { - context.approximateLength += 4; - return createKeywordTypeNode(SyntaxKind.VoidKeyword); - } - if (type.flags & TypeFlags.Undefined) { - context.approximateLength += 9; - return createKeywordTypeNode(SyntaxKind.UndefinedKeyword); - } - if (type.flags & TypeFlags.Null) { - context.approximateLength += 4; - return createKeywordTypeNode(SyntaxKind.NullKeyword); - } - if (type.flags & TypeFlags.Never) { - context.approximateLength += 5; - return createKeywordTypeNode(SyntaxKind.NeverKeyword); - } - if (type.flags & TypeFlags.ESSymbol) { - context.approximateLength += 6; - return createKeywordTypeNode(SyntaxKind.SymbolKeyword); - } - if (type.flags & TypeFlags.NonPrimitive) { - context.approximateLength += 6; - return createKeywordTypeNode(SyntaxKind.ObjectKeyword); - } - if (isThisTypeParameter(type)) { - if (context.flags & NodeBuilderFlags.InObjectTypeLiteral) { - if (!context.encounteredError && !(context.flags & NodeBuilderFlags.AllowThisInObjectLiteral)) { - context.encounteredError = true; - } - if (context.tracker.reportInaccessibleThisError) { - context.tracker.reportInaccessibleThisError(); - } - } - context.approximateLength += 4; - return createThis(); - } - - if (!inTypeAlias && type.aliasSymbol && (context.flags & NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope || isTypeSymbolAccessible(type.aliasSymbol, context.enclosingDeclaration))) { - const typeArgumentNodes = mapToTypeNodes(type.aliasTypeArguments, context); - if (isReservedMemberName(type.aliasSymbol.escapedName) && !(type.aliasSymbol.flags & SymbolFlags.Class)) return createTypeReferenceNode(createIdentifier(""), typeArgumentNodes); - return symbolToTypeNode(type.aliasSymbol, context, SymbolFlags.Type, typeArgumentNodes); - } - - const objectFlags = getObjectFlags(type); - - if (objectFlags & ObjectFlags.Reference) { - Debug.assert(!!(type.flags & TypeFlags.Object)); - return (type).node ? visitAndTransformType(type, typeReferenceToTypeNode) : typeReferenceToTypeNode(type); - } - if (type.flags & TypeFlags.TypeParameter || objectFlags & ObjectFlags.ClassOrInterface) { - if (type.flags & TypeFlags.TypeParameter && contains(context.inferTypeParameters, type)) { - context.approximateLength += (symbolName(type.symbol).length + 6); - return createInferTypeNode(typeParameterToDeclarationWithConstraint(type as TypeParameter, context, /*constraintNode*/ undefined)); - } - if (context.flags & NodeBuilderFlags.GenerateNamesForShadowedTypeParams && - type.flags & TypeFlags.TypeParameter && - !isTypeSymbolAccessible(type.symbol, context.enclosingDeclaration)) { - const name = typeParameterToName(type, context); - context.approximateLength += idText(name).length; - return createTypeReferenceNode(createIdentifier(idText(name)), /*typeArguments*/ undefined); - } - // Ignore constraint/default when creating a usage (as opposed to declaration) of a type parameter. - return type.symbol - ? symbolToTypeNode(type.symbol, context, SymbolFlags.Type) - : createTypeReferenceNode(createIdentifier("?"), /*typeArguments*/ undefined); - } - if (type.flags & (TypeFlags.Union | TypeFlags.Intersection)) { - const types = type.flags & TypeFlags.Union ? formatUnionTypes((type).types) : (type).types; - if (length(types) === 1) { - return typeToTypeNodeHelper(types[0], context); - } - const typeNodes = mapToTypeNodes(types, context, /*isBareList*/ true); - if (typeNodes && typeNodes.length > 0) { - const unionOrIntersectionTypeNode = createUnionOrIntersectionTypeNode(type.flags & TypeFlags.Union ? SyntaxKind.UnionType : SyntaxKind.IntersectionType, typeNodes); - return unionOrIntersectionTypeNode; - } - else { - if (!context.encounteredError && !(context.flags & NodeBuilderFlags.AllowEmptyUnionOrIntersection)) { - context.encounteredError = true; - } - return undefined!; // TODO: GH#18217 - } - } - if (objectFlags & (ObjectFlags.Anonymous | ObjectFlags.Mapped)) { - Debug.assert(!!(type.flags & TypeFlags.Object)); - // The type is an object literal type. - return createAnonymousTypeNode(type); - } - if (type.flags & TypeFlags.Index) { - const indexedType = (type).type; - context.approximateLength += 6; - const indexTypeNode = typeToTypeNodeHelper(indexedType, context); - return createTypeOperatorNode(indexTypeNode); - } - if (type.flags & TypeFlags.IndexedAccess) { - const objectTypeNode = typeToTypeNodeHelper((type).objectType, context); - const indexTypeNode = typeToTypeNodeHelper((type).indexType, context); - context.approximateLength += 2; - return createIndexedAccessTypeNode(objectTypeNode, indexTypeNode); - } - if (type.flags & TypeFlags.Conditional) { - const checkTypeNode = typeToTypeNodeHelper((type).checkType, context); - const saveInferTypeParameters = context.inferTypeParameters; - context.inferTypeParameters = (type).root.inferTypeParameters; - const extendsTypeNode = typeToTypeNodeHelper((type).extendsType, context); - context.inferTypeParameters = saveInferTypeParameters; - const trueTypeNode = typeToTypeNodeHelper(getTrueTypeFromConditionalType(type), context); - const falseTypeNode = typeToTypeNodeHelper(getFalseTypeFromConditionalType(type), context); - context.approximateLength += 15; - return createConditionalTypeNode(checkTypeNode, extendsTypeNode, trueTypeNode, falseTypeNode); - } - if (type.flags & TypeFlags.Substitution) { - return typeToTypeNodeHelper((type).typeVariable, context); - } - - return Debug.fail("Should be unreachable."); - - function createMappedTypeNodeFromType(type: MappedType) { - Debug.assert(!!(type.flags & TypeFlags.Object)); - const readonlyToken = type.declaration.readonlyToken ? createToken(type.declaration.readonlyToken.kind) : undefined; - const questionToken = type.declaration.questionToken ? createToken(type.declaration.questionToken.kind) : undefined; - let appropriateConstraintTypeNode: TypeNode; - if (isMappedTypeWithKeyofConstraintDeclaration(type)) { - // We have a { [P in keyof T]: X } - // We do this to ensure we retain the toplevel keyof-ness of the type which may be lost due to keyof distribution during `getConstraintTypeFromMappedType` - appropriateConstraintTypeNode = createTypeOperatorNode(typeToTypeNodeHelper(getModifiersTypeFromMappedType(type), context)); - } - else { - appropriateConstraintTypeNode = typeToTypeNodeHelper(getConstraintTypeFromMappedType(type), context); - } - const typeParameterNode = typeParameterToDeclarationWithConstraint(getTypeParameterFromMappedType(type), context, appropriateConstraintTypeNode); - const templateTypeNode = typeToTypeNodeHelper(getTemplateTypeFromMappedType(type), context); - const mappedTypeNode = createMappedTypeNode(readonlyToken, typeParameterNode, questionToken, templateTypeNode); - context.approximateLength += 10; - return setEmitFlags(mappedTypeNode, EmitFlags.SingleLine); - } - - function createAnonymousTypeNode(type: ObjectType): TypeNode { - const typeId = "" + type.id; - const symbol = type.symbol; - if (symbol) { - if (isJSConstructor(symbol.valueDeclaration)) { - // Instance and static types share the same symbol; only add 'typeof' for the static side. - const isInstanceType = type === getDeclaredTypeOfClassOrInterface(symbol) ? SymbolFlags.Type : SymbolFlags.Value; - return symbolToTypeNode(symbol, context, isInstanceType); - } - // Always use 'typeof T' for type of class, enum, and module objects - else if (symbol.flags & SymbolFlags.Class && !getBaseTypeVariableOfClass(symbol) && !(symbol.valueDeclaration.kind === SyntaxKind.ClassExpression && context.flags & NodeBuilderFlags.WriteClassExpressionAsTypeLiteral) || - symbol.flags & (SymbolFlags.Enum | SymbolFlags.ValueModule) || - shouldWriteTypeOfFunctionSymbol()) { - return symbolToTypeNode(symbol, context, SymbolFlags.Value); - } - else if (context.visitedTypes && context.visitedTypes.has(typeId)) { - // If type is an anonymous type literal in a type alias declaration, use type alias name - const typeAlias = getTypeAliasForTypeLiteral(type); - if (typeAlias) { - // The specified symbol flags need to be reinterpreted as type flags - return symbolToTypeNode(typeAlias, context, SymbolFlags.Type); - } - else { - return createElidedInformationPlaceholder(context); - } - } - else { - return visitAndTransformType(type, createTypeNodeFromObjectType); - } - } - else { - // Anonymous types without a symbol are never circular. - return createTypeNodeFromObjectType(type); - } - function shouldWriteTypeOfFunctionSymbol() { - const isStaticMethodSymbol = !!(symbol.flags & SymbolFlags.Method) && // typeof static method - some(symbol.declarations, declaration => hasModifier(declaration, ModifierFlags.Static)); - const isNonLocalFunctionSymbol = !!(symbol.flags & SymbolFlags.Function) && - (symbol.parent || // is exported function symbol - forEach(symbol.declarations, declaration => - declaration.parent.kind === SyntaxKind.SourceFile || declaration.parent.kind === SyntaxKind.ModuleBlock)); - if (isStaticMethodSymbol || isNonLocalFunctionSymbol) { - // typeof is allowed only for static/non local functions - return (!!(context.flags & NodeBuilderFlags.UseTypeOfFunction) || (context.visitedTypes && context.visitedTypes.has(typeId))) && // it is type of the symbol uses itself recursively - (!(context.flags & NodeBuilderFlags.UseStructuralFallback) || isValueSymbolAccessible(symbol, context.enclosingDeclaration)); // And the build is going to succeed without visibility error or there is no structural fallback allowed - } - } - } - - function visitAndTransformType(type: Type, transform: (type: Type) => T) { - const typeId = "" + type.id; - const isConstructorObject = getObjectFlags(type) & ObjectFlags.Anonymous && type.symbol && type.symbol.flags & SymbolFlags.Class; - const id = getObjectFlags(type) & ObjectFlags.Reference && (type).node ? "N" + getNodeId((type).node!) : - type.symbol ? (isConstructorObject ? "+" : "") + getSymbolId(type.symbol) : - undefined; - // Since instantiations of the same anonymous type have the same symbol, tracking symbols instead - // of types allows us to catch circular references to instantiations of the same anonymous type - if (!context.visitedTypes) { - context.visitedTypes = createMap(); - } - if (id && !context.symbolDepth) { - context.symbolDepth = createMap(); - } - - let depth: number | undefined; - if (id) { - depth = context.symbolDepth!.get(id) || 0; - if (depth > 10) { - return createElidedInformationPlaceholder(context); - } - context.symbolDepth!.set(id, depth + 1); - } - context.visitedTypes.set(typeId, true); - const result = transform(type); - context.visitedTypes.delete(typeId); - if (id) { - context.symbolDepth!.set(id, depth!); - } - return result; - } - - function createTypeNodeFromObjectType(type: ObjectType): TypeNode { - if (isGenericMappedType(type)) { - return createMappedTypeNodeFromType(type); - } - - const resolved = resolveStructuredTypeMembers(type); - if (!resolved.properties.length && !resolved.stringIndexInfo && !resolved.numberIndexInfo) { - if (!resolved.callSignatures.length && !resolved.constructSignatures.length) { - context.approximateLength += 2; - return setEmitFlags(createTypeLiteralNode(/*members*/ undefined), EmitFlags.SingleLine); - } - - if (resolved.callSignatures.length === 1 && !resolved.constructSignatures.length) { - const signature = resolved.callSignatures[0]; - const signatureNode = signatureToSignatureDeclarationHelper(signature, SyntaxKind.FunctionType, context); - return signatureNode; - - } - - if (resolved.constructSignatures.length === 1 && !resolved.callSignatures.length) { - const signature = resolved.constructSignatures[0]; - const signatureNode = signatureToSignatureDeclarationHelper(signature, SyntaxKind.ConstructorType, context); - return signatureNode; - } - } - - const savedFlags = context.flags; - context.flags |= NodeBuilderFlags.InObjectTypeLiteral; - const members = createTypeNodesFromResolvedType(resolved); - context.flags = savedFlags; - const typeLiteralNode = createTypeLiteralNode(members); - context.approximateLength += 2; - return setEmitFlags(typeLiteralNode, (context.flags & NodeBuilderFlags.MultilineObjectLiterals) ? 0 : EmitFlags.SingleLine); - } - - function typeReferenceToTypeNode(type: TypeReference) { - const typeArguments: readonly Type[] = getTypeArguments(type); - if (type.target === globalArrayType || type.target === globalReadonlyArrayType) { - if (context.flags & NodeBuilderFlags.WriteArrayAsGenericType) { - const typeArgumentNode = typeToTypeNodeHelper(typeArguments[0], context); - return createTypeReferenceNode(type.target === globalArrayType ? "Array" : "ReadonlyArray", [typeArgumentNode]); - } - const elementType = typeToTypeNodeHelper(typeArguments[0], context); - const arrayType = createArrayTypeNode(elementType); - return type.target === globalArrayType ? arrayType : createTypeOperatorNode(SyntaxKind.ReadonlyKeyword, arrayType); - } - else if (type.target.objectFlags & ObjectFlags.Tuple) { - if (typeArguments.length > 0) { - const arity = getTypeReferenceArity(type); - const tupleConstituentNodes = mapToTypeNodes(typeArguments.slice(0, arity), context); - const hasRestElement = (type.target).hasRestElement; - if (tupleConstituentNodes) { - for (let i = (type.target).minLength; i < Math.min(arity, tupleConstituentNodes.length); i++) { - tupleConstituentNodes[i] = hasRestElement && i === arity - 1 ? - createRestTypeNode(createArrayTypeNode(tupleConstituentNodes[i])) : - createOptionalTypeNode(tupleConstituentNodes[i]); - } - const tupleTypeNode = createTupleTypeNode(tupleConstituentNodes); - return (type.target).readonly ? createTypeOperatorNode(SyntaxKind.ReadonlyKeyword, tupleTypeNode) : tupleTypeNode; - } - } - if (context.encounteredError || (context.flags & NodeBuilderFlags.AllowEmptyTuple)) { - const tupleTypeNode = createTupleTypeNode([]); - return (type.target).readonly ? createTypeOperatorNode(SyntaxKind.ReadonlyKeyword, tupleTypeNode) : tupleTypeNode; - } - context.encounteredError = true; - return undefined!; // TODO: GH#18217 - } - else if (context.flags & NodeBuilderFlags.WriteClassExpressionAsTypeLiteral && - type.symbol.valueDeclaration && - isClassLike(type.symbol.valueDeclaration) && - !isValueSymbolAccessible(type.symbol, context.enclosingDeclaration) - ) { - return createAnonymousTypeNode(type); - } - else { - const outerTypeParameters = type.target.outerTypeParameters; - let i = 0; - let resultType: TypeReferenceNode | ImportTypeNode | undefined; - if (outerTypeParameters) { - const length = outerTypeParameters.length; - while (i < length) { - // Find group of type arguments for type parameters with the same declaring container. - const start = i; - const parent = getParentSymbolOfTypeParameter(outerTypeParameters[i])!; - do { - i++; - } while (i < length && getParentSymbolOfTypeParameter(outerTypeParameters[i]) === parent); - // When type parameters are their own type arguments for the whole group (i.e. we have - // the default outer type arguments), we don't show the group. - if (!rangeEquals(outerTypeParameters, typeArguments, start, i)) { - const typeArgumentSlice = mapToTypeNodes(typeArguments.slice(start, i), context); - const flags = context.flags; - context.flags |= NodeBuilderFlags.ForbidIndexedAccessSymbolReferences; - const ref = symbolToTypeNode(parent, context, SymbolFlags.Type, typeArgumentSlice) as TypeReferenceNode | ImportTypeNode; - context.flags = flags; - resultType = !resultType ? ref : appendReferenceToType(resultType, ref as TypeReferenceNode); - } - } - } - let typeArgumentNodes: readonly TypeNode[] | undefined; - if (typeArguments.length > 0) { - const typeParameterCount = (type.target.typeParameters || emptyArray).length; - typeArgumentNodes = mapToTypeNodes(typeArguments.slice(i, typeParameterCount), context); - } - const flags = context.flags; - context.flags |= NodeBuilderFlags.ForbidIndexedAccessSymbolReferences; - const finalRef = symbolToTypeNode(type.symbol, context, SymbolFlags.Type, typeArgumentNodes); - context.flags = flags; - return !resultType ? finalRef : appendReferenceToType(resultType, finalRef as TypeReferenceNode); - } - } - - - function appendReferenceToType(root: TypeReferenceNode | ImportTypeNode, ref: TypeReferenceNode): TypeReferenceNode | ImportTypeNode { - if (isImportTypeNode(root)) { - // first shift type arguments - const innerParams = root.typeArguments; - if (root.qualifier) { - (isIdentifier(root.qualifier) ? root.qualifier : root.qualifier.right).typeArguments = innerParams; - } - root.typeArguments = ref.typeArguments; - // then move qualifiers - const ids = getAccessStack(ref); - for (const id of ids) { - root.qualifier = root.qualifier ? createQualifiedName(root.qualifier, id) : id; - } - return root; - } - else { - // first shift type arguments - const innerParams = root.typeArguments; - (isIdentifier(root.typeName) ? root.typeName : root.typeName.right).typeArguments = innerParams; - root.typeArguments = ref.typeArguments; - // then move qualifiers - const ids = getAccessStack(ref); - for (const id of ids) { - root.typeName = createQualifiedName(root.typeName, id); - } - return root; - } - } - - function getAccessStack(ref: TypeReferenceNode): Identifier[] { - let state = ref.typeName; - const ids = []; - while (!isIdentifier(state)) { - ids.unshift(state.right); - state = state.left; - } - ids.unshift(state); - return ids; - } - - function createTypeNodesFromResolvedType(resolvedType: ResolvedType): TypeElement[] | undefined { - if (checkTruncationLength(context)) { - return [createPropertySignature(/*modifiers*/ undefined, "...", /*questionToken*/ undefined, /*type*/ undefined, /*initializer*/ undefined)]; - } - const typeElements: TypeElement[] = []; - for (const signature of resolvedType.callSignatures) { - typeElements.push(signatureToSignatureDeclarationHelper(signature, SyntaxKind.CallSignature, context)); - } - for (const signature of resolvedType.constructSignatures) { - typeElements.push(signatureToSignatureDeclarationHelper(signature, SyntaxKind.ConstructSignature, context)); - } - if (resolvedType.stringIndexInfo) { - let indexSignature: IndexSignatureDeclaration; - if (resolvedType.objectFlags & ObjectFlags.ReverseMapped) { - indexSignature = indexInfoToIndexSignatureDeclarationHelper(createIndexInfo(anyType, resolvedType.stringIndexInfo.isReadonly, resolvedType.stringIndexInfo.declaration), IndexKind.String, context); - indexSignature.type = createElidedInformationPlaceholder(context); - } - else { - indexSignature = indexInfoToIndexSignatureDeclarationHelper(resolvedType.stringIndexInfo, IndexKind.String, context); - } - typeElements.push(indexSignature); - } - if (resolvedType.numberIndexInfo) { - typeElements.push(indexInfoToIndexSignatureDeclarationHelper(resolvedType.numberIndexInfo, IndexKind.Number, context)); - } - - const properties = resolvedType.properties; - if (!properties) { - return typeElements; - } - - let i = 0; - for (const propertySymbol of properties) { - i++; - if (context.flags & NodeBuilderFlags.WriteClassExpressionAsTypeLiteral) { - if (propertySymbol.flags & SymbolFlags.Prototype) { - continue; - } - if (getDeclarationModifierFlagsFromSymbol(propertySymbol) & (ModifierFlags.Private | ModifierFlags.Protected) && context.tracker.reportPrivateInBaseOfClassExpression) { - context.tracker.reportPrivateInBaseOfClassExpression(unescapeLeadingUnderscores(propertySymbol.escapedName)); - } - } - if (checkTruncationLength(context) && (i + 2 < properties.length - 1)) { - typeElements.push(createPropertySignature(/*modifiers*/ undefined, `... ${properties.length - i} more ...`, /*questionToken*/ undefined, /*type*/ undefined, /*initializer*/ undefined)); - addPropertyToElementList(properties[properties.length - 1], context, typeElements); - break; - } - addPropertyToElementList(propertySymbol, context, typeElements); - - } - return typeElements.length ? typeElements : undefined; - } - } - - function createElidedInformationPlaceholder(context: NodeBuilderContext) { - context.approximateLength += 3; - if (!(context.flags & NodeBuilderFlags.NoTruncation)) { - return createTypeReferenceNode(createIdentifier("..."), /*typeArguments*/ undefined); - } - return createKeywordTypeNode(SyntaxKind.AnyKeyword); - } - - function addPropertyToElementList(propertySymbol: Symbol, context: NodeBuilderContext, typeElements: TypeElement[]) { - const propertyIsReverseMapped = !!(getCheckFlags(propertySymbol) & CheckFlags.ReverseMapped); - const propertyType = propertyIsReverseMapped && context.flags & NodeBuilderFlags.InReverseMappedType ? - anyType : getTypeOfSymbol(propertySymbol); - const saveEnclosingDeclaration = context.enclosingDeclaration; - context.enclosingDeclaration = undefined; - if (context.tracker.trackSymbol && getCheckFlags(propertySymbol) & CheckFlags.Late) { - const decl = first(propertySymbol.declarations); - if (hasLateBindableName(decl)) { - if (isBinaryExpression(decl)) { - const name = getNameOfDeclaration(decl); - if (name && isElementAccessExpression(name) && isPropertyAccessEntityNameExpression(name.argumentExpression)) { - trackComputedName(name.argumentExpression, saveEnclosingDeclaration, context); - } - } - else { - trackComputedName(decl.name.expression, saveEnclosingDeclaration, context); - } - } - } - context.enclosingDeclaration = saveEnclosingDeclaration; - const propertyName = getPropertyNameNodeForSymbol(propertySymbol, context); - context.approximateLength += (symbolName(propertySymbol).length + 1); - const optionalToken = propertySymbol.flags & SymbolFlags.Optional ? createToken(SyntaxKind.QuestionToken) : undefined; - if (propertySymbol.flags & (SymbolFlags.Function | SymbolFlags.Method) && !getPropertiesOfObjectType(propertyType).length && !isReadonlySymbol(propertySymbol)) { - const signatures = getSignaturesOfType(filterType(propertyType, t => !(t.flags & TypeFlags.Undefined)), SignatureKind.Call); - for (const signature of signatures) { - const methodDeclaration = signatureToSignatureDeclarationHelper(signature, SyntaxKind.MethodSignature, context); - methodDeclaration.name = propertyName; - methodDeclaration.questionToken = optionalToken; - typeElements.push(preserveCommentsOn(methodDeclaration)); - } - } - else { - const savedFlags = context.flags; - context.flags |= propertyIsReverseMapped ? NodeBuilderFlags.InReverseMappedType : 0; - let propertyTypeNode: TypeNode; - if (propertyIsReverseMapped && !!(savedFlags & NodeBuilderFlags.InReverseMappedType)) { - propertyTypeNode = createElidedInformationPlaceholder(context); - } - else { - propertyTypeNode = propertyType ? typeToTypeNodeHelper(propertyType, context) : createKeywordTypeNode(SyntaxKind.AnyKeyword); - } - context.flags = savedFlags; - - const modifiers = isReadonlySymbol(propertySymbol) ? [createToken(SyntaxKind.ReadonlyKeyword)] : undefined; - if (modifiers) { - context.approximateLength += 9; - } - const propertySignature = createPropertySignature( - modifiers, - propertyName, - optionalToken, - propertyTypeNode, - /*initializer*/ undefined); - - typeElements.push(preserveCommentsOn(propertySignature)); - } - - function preserveCommentsOn(node: T) { - if (some(propertySymbol.declarations, d => d.kind === SyntaxKind.JSDocPropertyTag)) { - const d = find(propertySymbol.declarations, d => d.kind === SyntaxKind.JSDocPropertyTag)! as JSDocPropertyTag; - const commentText = d.comment; - if (commentText) { - setSyntheticLeadingComments(node, [{ kind: SyntaxKind.MultiLineCommentTrivia, text: "*\n * " + commentText.replace(/\n/g, "\n * ") + "\n ", pos: -1, end: -1, hasTrailingNewLine: true }]); - } - } - else if (propertySymbol.valueDeclaration) { - // Copy comments to node for declaration emit - setCommentRange(node, propertySymbol.valueDeclaration); - } - return node; - } - } - - function mapToTypeNodes(types: readonly Type[] | undefined, context: NodeBuilderContext, isBareList?: boolean): TypeNode[] | undefined { - if (some(types)) { - if (checkTruncationLength(context)) { - if (!isBareList) { - return [createTypeReferenceNode("...", /*typeArguments*/ undefined)]; - } - else if (types.length > 2) { - return [ - typeToTypeNodeHelper(types[0], context), - createTypeReferenceNode(`... ${types.length - 2} more ...`, /*typeArguments*/ undefined), - typeToTypeNodeHelper(types[types.length - 1], context) - ]; - } - } - const result = []; - let i = 0; - for (const type of types) { - i++; - if (checkTruncationLength(context) && (i + 2 < types.length - 1)) { - result.push(createTypeReferenceNode(`... ${types.length - i} more ...`, /*typeArguments*/ undefined)); - const typeNode = typeToTypeNodeHelper(types[types.length - 1], context); - if (typeNode) { - result.push(typeNode); - } - break; - } - context.approximateLength += 2; // Account for whitespace + separator - const typeNode = typeToTypeNodeHelper(type, context); - if (typeNode) { - result.push(typeNode); - } - } - - return result; - } - } - - function indexInfoToIndexSignatureDeclarationHelper(indexInfo: IndexInfo, kind: IndexKind, context: NodeBuilderContext): IndexSignatureDeclaration { - const name = getNameFromIndexInfo(indexInfo) || "x"; - const indexerTypeNode = createKeywordTypeNode(kind === IndexKind.String ? SyntaxKind.StringKeyword : SyntaxKind.NumberKeyword); - - const indexingParameter = createParameter( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, - name, - /*questionToken*/ undefined, - indexerTypeNode, - /*initializer*/ undefined); - const typeNode = typeToTypeNodeHelper(indexInfo.type || anyType, context); - if (!indexInfo.type && !(context.flags & NodeBuilderFlags.AllowEmptyIndexInfoType)) { - context.encounteredError = true; - } - context.approximateLength += (name.length + 4); - return createIndexSignature( - /*decorators*/ undefined, - indexInfo.isReadonly ? [createToken(SyntaxKind.ReadonlyKeyword)] : undefined, - [indexingParameter], - typeNode); - } - - function signatureToSignatureDeclarationHelper(signature: Signature, kind: SyntaxKind, context: NodeBuilderContext): SignatureDeclaration { - const suppressAny = context.flags & NodeBuilderFlags.SuppressAnyReturnType; - if (suppressAny) context.flags &= ~NodeBuilderFlags.SuppressAnyReturnType; // suppress only toplevel `any`s - let typeParameters: TypeParameterDeclaration[] | undefined; - let typeArguments: TypeNode[] | undefined; - if (context.flags & NodeBuilderFlags.WriteTypeArgumentsOfSignature && signature.target && signature.mapper && signature.target.typeParameters) { - typeArguments = signature.target.typeParameters.map(parameter => typeToTypeNodeHelper(instantiateType(parameter, signature.mapper), context)); - } - else { - typeParameters = signature.typeParameters && signature.typeParameters.map(parameter => typeParameterToDeclaration(parameter, context)); - } - - const parameters = getExpandedParameters(signature).map(parameter => symbolToParameterDeclaration(parameter, context, kind === SyntaxKind.Constructor)); - if (signature.thisParameter) { - const thisParameter = symbolToParameterDeclaration(signature.thisParameter, context); - parameters.unshift(thisParameter); - } - - let returnTypeNode: TypeNode | undefined; - const typePredicate = getTypePredicateOfSignature(signature); - if (typePredicate) { - const assertsModifier = typePredicate.kind === TypePredicateKind.AssertsThis || typePredicate.kind === TypePredicateKind.AssertsIdentifier ? - createToken(SyntaxKind.AssertsKeyword) : - undefined; - const parameterName = typePredicate.kind === TypePredicateKind.Identifier || typePredicate.kind === TypePredicateKind.AssertsIdentifier ? - setEmitFlags(createIdentifier(typePredicate.parameterName), EmitFlags.NoAsciiEscaping) : - createThisTypeNode(); - const typeNode = typePredicate.type && typeToTypeNodeHelper(typePredicate.type, context); - returnTypeNode = createTypePredicateNodeWithModifier(assertsModifier, parameterName, typeNode); - } - else { - const returnType = getReturnTypeOfSignature(signature); - if (returnType && !(suppressAny && isTypeAny(returnType))) { - returnTypeNode = typeToTypeNodeHelper(returnType, context); - } - else if (!suppressAny) { - returnTypeNode = createKeywordTypeNode(SyntaxKind.AnyKeyword); - } - } - context.approximateLength += 3; // Usually a signature contributes a few more characters than this, but 3 is the minimum - return createSignatureDeclaration(kind, typeParameters, parameters, returnTypeNode, typeArguments); - } - - function typeParameterToDeclarationWithConstraint(type: TypeParameter, context: NodeBuilderContext, constraintNode: TypeNode | undefined): TypeParameterDeclaration { - const savedContextFlags = context.flags; - context.flags &= ~NodeBuilderFlags.WriteTypeParametersInQualifiedName; // Avoids potential infinite loop when building for a claimspace with a generic - const name = typeParameterToName(type, context); - const defaultParameter = getDefaultFromTypeParameter(type); - const defaultParameterNode = defaultParameter && typeToTypeNodeHelper(defaultParameter, context); - context.flags = savedContextFlags; - return createTypeParameterDeclaration(name, constraintNode, defaultParameterNode); - } - - function typeParameterToDeclaration(type: TypeParameter, context: NodeBuilderContext, constraint = getConstraintOfTypeParameter(type)): TypeParameterDeclaration { - const constraintNode = constraint && typeToTypeNodeHelper(constraint, context); - return typeParameterToDeclarationWithConstraint(type, context, constraintNode); - } - - function symbolToParameterDeclaration(parameterSymbol: Symbol, context: NodeBuilderContext, preserveModifierFlags?: boolean): ParameterDeclaration { - let parameterDeclaration: ParameterDeclaration | JSDocParameterTag | undefined = getDeclarationOfKind(parameterSymbol, SyntaxKind.Parameter); - if (!parameterDeclaration && !isTransientSymbol(parameterSymbol)) { - parameterDeclaration = getDeclarationOfKind(parameterSymbol, SyntaxKind.JSDocParameterTag); - } - - let parameterType = getTypeOfSymbol(parameterSymbol); - if (parameterDeclaration && isRequiredInitializedParameter(parameterDeclaration)) { - parameterType = getOptionalType(parameterType); - } - const parameterTypeNode = typeToTypeNodeHelper(parameterType, context); - - const modifiers = !(context.flags & NodeBuilderFlags.OmitParameterModifiers) && preserveModifierFlags && parameterDeclaration && parameterDeclaration.modifiers ? parameterDeclaration.modifiers.map(getSynthesizedClone) : undefined; - const isRest = parameterDeclaration && isRestParameter(parameterDeclaration) || getCheckFlags(parameterSymbol) & CheckFlags.RestParameter; - const dotDotDotToken = isRest ? createToken(SyntaxKind.DotDotDotToken) : undefined; - const name = parameterDeclaration ? parameterDeclaration.name ? - parameterDeclaration.name.kind === SyntaxKind.Identifier ? setEmitFlags(getSynthesizedClone(parameterDeclaration.name), EmitFlags.NoAsciiEscaping) : - parameterDeclaration.name.kind === SyntaxKind.QualifiedName ? setEmitFlags(getSynthesizedClone(parameterDeclaration.name.right), EmitFlags.NoAsciiEscaping) : - cloneBindingName(parameterDeclaration.name) : - symbolName(parameterSymbol) : - symbolName(parameterSymbol); - const isOptional = parameterDeclaration && isOptionalParameter(parameterDeclaration) || getCheckFlags(parameterSymbol) & CheckFlags.OptionalParameter; - const questionToken = isOptional ? createToken(SyntaxKind.QuestionToken) : undefined; - const parameterNode = createParameter( - /*decorators*/ undefined, - modifiers, - dotDotDotToken, - name, - questionToken, - parameterTypeNode, - /*initializer*/ undefined); - context.approximateLength += symbolName(parameterSymbol).length + 3; - return parameterNode; - - function cloneBindingName(node: BindingName): BindingName { - return elideInitializerAndSetEmitFlags(node); - function elideInitializerAndSetEmitFlags(node: Node): Node { - if (context.tracker.trackSymbol && isComputedPropertyName(node) && isLateBindableName(node)) { - trackComputedName(node.expression, context.enclosingDeclaration, context); - } - const visited = visitEachChild(node, elideInitializerAndSetEmitFlags, nullTransformationContext, /*nodesVisitor*/ undefined, elideInitializerAndSetEmitFlags)!; - const clone = nodeIsSynthesized(visited) ? visited : getSynthesizedClone(visited); - if (clone.kind === SyntaxKind.BindingElement) { - (clone).initializer = undefined; - } - return setEmitFlags(clone, EmitFlags.SingleLine | EmitFlags.NoAsciiEscaping); - } - } - } - - function trackComputedName(accessExpression: EntityNameOrEntityNameExpression, enclosingDeclaration: Node | undefined, context: NodeBuilderContext) { - if (!context.tracker.trackSymbol) return; - // get symbol of the first identifier of the entityName - const firstIdentifier = getFirstIdentifier(accessExpression); - const name = resolveName(firstIdentifier, firstIdentifier.escapedText, SymbolFlags.Value | SymbolFlags.ExportValue, /*nodeNotFoundErrorMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ true); - if (name) { - context.tracker.trackSymbol(name, enclosingDeclaration, SymbolFlags.Value); - } - } - - function lookupSymbolChain(symbol: Symbol, context: NodeBuilderContext, meaning: SymbolFlags, yieldModuleSymbol?: boolean) { - context.tracker.trackSymbol!(symbol, context.enclosingDeclaration, meaning); // TODO: GH#18217 - return lookupSymbolChainWorker(symbol, context, meaning, yieldModuleSymbol); - } - - function lookupSymbolChainWorker(symbol: Symbol, context: NodeBuilderContext, meaning: SymbolFlags, yieldModuleSymbol?: boolean) { - // Try to get qualified name if the symbol is not a type parameter and there is an enclosing declaration. - let chain: Symbol[]; - const isTypeParameter = symbol.flags & SymbolFlags.TypeParameter; - if (!isTypeParameter && (context.enclosingDeclaration || context.flags & NodeBuilderFlags.UseFullyQualifiedType) && !(context.flags & NodeBuilderFlags.DoNotIncludeSymbolChain)) { - chain = Debug.checkDefined(getSymbolChain(symbol, meaning, /*endOfChain*/ true)); - Debug.assert(chain && chain.length > 0); - } - else { - chain = [symbol]; - } - return chain; - - /** @param endOfChain Set to false for recursive calls; non-recursive calls should always output something. */ - function getSymbolChain(symbol: Symbol, meaning: SymbolFlags, endOfChain: boolean): Symbol[] | undefined { - let accessibleSymbolChain = getAccessibleSymbolChain(symbol, context.enclosingDeclaration, meaning, !!(context.flags & NodeBuilderFlags.UseOnlyExternalAliasing)); - let parentSpecifiers: (string | undefined)[]; - if (!accessibleSymbolChain || - needsQualification(accessibleSymbolChain[0], context.enclosingDeclaration, accessibleSymbolChain.length === 1 ? meaning : getQualifiedLeftMeaning(meaning))) { - - // Go up and add our parent. - const parents = getContainersOfSymbol(accessibleSymbolChain ? accessibleSymbolChain[0] : symbol, context.enclosingDeclaration); - if (length(parents)) { - parentSpecifiers = parents!.map(symbol => - some(symbol.declarations, hasNonGlobalAugmentationExternalModuleSymbol) - ? getSpecifierForModuleSymbol(symbol, context) - : undefined); - const indices = parents!.map((_, i) => i); - indices.sort(sortByBestName); - const sortedParents = indices.map(i => parents![i]); - for (const parent of sortedParents) { - const parentChain = getSymbolChain(parent, getQualifiedLeftMeaning(meaning), /*endOfChain*/ false); - if (parentChain) { - if (parent.exports && parent.exports.get(InternalSymbolName.ExportEquals) && - getSymbolIfSameReference(parent.exports.get(InternalSymbolName.ExportEquals)!, symbol)) { - // parentChain root _is_ symbol - symbol is a module export=, so it kinda looks like it's own parent - // No need to lookup an alias for the symbol in itself - accessibleSymbolChain = parentChain; - break; - } - accessibleSymbolChain = parentChain.concat(accessibleSymbolChain || [getAliasForSymbolInContainer(parent, symbol) || symbol]); - break; - } - } - } - } - - if (accessibleSymbolChain) { - return accessibleSymbolChain; - } - if ( - // If this is the last part of outputting the symbol, always output. The cases apply only to parent symbols. - endOfChain || - // If a parent symbol is an anonymous type, don't write it. - !(symbol.flags & (SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral))) { - // If a parent symbol is an external module, don't write it. (We prefer just `x` vs `"foo/bar".x`.) - if (!endOfChain && !yieldModuleSymbol && !!forEach(symbol.declarations, hasNonGlobalAugmentationExternalModuleSymbol)) { - return; - } - return [symbol]; - } - - function sortByBestName(a: number, b: number) { - const specifierA = parentSpecifiers[a]; - const specifierB = parentSpecifiers[b]; - if (specifierA && specifierB) { - const isBRelative = pathIsRelative(specifierB); - if (pathIsRelative(specifierA) === isBRelative) { - // Both relative or both non-relative, sort by number of parts - return moduleSpecifiers.countPathComponents(specifierA) - moduleSpecifiers.countPathComponents(specifierB); - } - if (isBRelative) { - // A is non-relative, B is relative: prefer A - return -1; - } - // A is relative, B is non-relative: prefer B - return 1; - } - return 0; - } - } - } - - function typeParametersToTypeParameterDeclarations(symbol: Symbol, context: NodeBuilderContext) { - let typeParameterNodes: NodeArray | undefined; - const targetSymbol = getTargetSymbol(symbol); - if (targetSymbol.flags & (SymbolFlags.Class | SymbolFlags.Interface | SymbolFlags.TypeAlias)) { - typeParameterNodes = createNodeArray(map(getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol), tp => typeParameterToDeclaration(tp, context))); - } - return typeParameterNodes; - } - - function lookupTypeParameterNodes(chain: Symbol[], index: number, context: NodeBuilderContext) { - Debug.assert(chain && 0 <= index && index < chain.length); - const symbol = chain[index]; - const symbolId = "" + getSymbolId(symbol); - if (context.typeParameterSymbolList && context.typeParameterSymbolList.get(symbolId)) { - return undefined; - } - (context.typeParameterSymbolList || (context.typeParameterSymbolList = createMap())).set(symbolId, true); - let typeParameterNodes: readonly TypeNode[] | readonly TypeParameterDeclaration[] | undefined; - if (context.flags & NodeBuilderFlags.WriteTypeParametersInQualifiedName && index < (chain.length - 1)) { - const parentSymbol = symbol; - const nextSymbol = chain[index + 1]; - if (getCheckFlags(nextSymbol) & CheckFlags.Instantiated) { - const params = getTypeParametersOfClassOrInterface( - parentSymbol.flags & SymbolFlags.Alias ? resolveAlias(parentSymbol) : parentSymbol - ); - typeParameterNodes = mapToTypeNodes(map(params, (nextSymbol as TransientSymbol).mapper!), context); - } - else { - typeParameterNodes = typeParametersToTypeParameterDeclarations(symbol, context); - } - } - return typeParameterNodes; - } - - /** - * Given A[B][C][D], finds A[B] - */ - function getTopmostIndexedAccessType(top: IndexedAccessTypeNode): IndexedAccessTypeNode { - if (isIndexedAccessTypeNode(top.objectType)) { - return getTopmostIndexedAccessType(top.objectType); - } - return top; - } - - function getSpecifierForModuleSymbol(symbol: Symbol, context: NodeBuilderContext) { - const file = getDeclarationOfKind(symbol, SyntaxKind.SourceFile); - if (file && file.moduleName !== undefined) { - // Use the amd name if it is available - return file.moduleName; - } - if (!file) { - if (context.tracker.trackReferencedAmbientModule) { - const ambientDecls = filter(symbol.declarations, isAmbientModule); - if (length(ambientDecls)) { - for (const decl of ambientDecls) { - context.tracker.trackReferencedAmbientModule(decl, symbol); - } - } - } - if (ambientModuleSymbolRegex.test(symbol.escapedName as string)) { - return (symbol.escapedName as string).substring(1, (symbol.escapedName as string).length - 1); - } - } - if (!context.enclosingDeclaration || !context.tracker.moduleResolverHost) { - // If there's no context declaration, we can't lookup a non-ambient specifier, so we just use the symbol name - if (ambientModuleSymbolRegex.test(symbol.escapedName as string)) { - return (symbol.escapedName as string).substring(1, (symbol.escapedName as string).length - 1); - } - return getSourceFileOfNode(getNonAugmentationDeclaration(symbol)!).fileName; // A resolver may not be provided for baselines and errors - in those cases we use the fileName in full - } - const contextFile = getSourceFileOfNode(getOriginalNode(context.enclosingDeclaration)); - const links = getSymbolLinks(symbol); - let specifier = links.specifierCache && links.specifierCache.get(contextFile.path); - if (!specifier) { - const isBundle = (compilerOptions.out || compilerOptions.outFile); - // For declaration bundles, we need to generate absolute paths relative to the common source dir for imports, - // just like how the declaration emitter does for the ambient module declarations - we can easily accomplish this - // using the `baseUrl` compiler option (which we would otherwise never use in declaration emit) and a non-relative - // specifier preference - const { moduleResolverHost } = context.tracker; - const specifierCompilerOptions = isBundle ? { ...compilerOptions, baseUrl: moduleResolverHost.getCommonSourceDirectory() } : compilerOptions; - specifier = first(moduleSpecifiers.getModuleSpecifiers( - symbol, - specifierCompilerOptions, - contextFile, - moduleResolverHost, - host.getSourceFiles(), - { importModuleSpecifierPreference: isBundle ? "non-relative" : "relative" }, - host.redirectTargetsMap, - )); - links.specifierCache = links.specifierCache || createMap(); - links.specifierCache.set(contextFile.path, specifier); - } - return specifier; - } - - function symbolToTypeNode(symbol: Symbol, context: NodeBuilderContext, meaning: SymbolFlags, overrideTypeArguments?: readonly TypeNode[]): TypeNode { - const chain = lookupSymbolChain(symbol, context, meaning, !(context.flags & NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope)); // If we're using aliases outside the current scope, dont bother with the module - - const isTypeOf = meaning === SymbolFlags.Value; - if (some(chain[0].declarations, hasNonGlobalAugmentationExternalModuleSymbol)) { - // module is root, must use `ImportTypeNode` - const nonRootParts = chain.length > 1 ? createAccessFromSymbolChain(chain, chain.length - 1, 1) : undefined; - const typeParameterNodes = overrideTypeArguments || lookupTypeParameterNodes(chain, 0, context); - const specifier = getSpecifierForModuleSymbol(chain[0], context); - if (!(context.flags & NodeBuilderFlags.AllowNodeModulesRelativePaths) && getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs && specifier.indexOf("/node_modules/") >= 0) { - // If ultimately we can only name the symbol with a reference that dives into a `node_modules` folder, we should error - // since declaration files with these kinds of references are liable to fail when published :( - context.encounteredError = true; - if (context.tracker.reportLikelyUnsafeImportRequiredError) { - context.tracker.reportLikelyUnsafeImportRequiredError(specifier); - } - } - const lit = createLiteralTypeNode(createLiteral(specifier)); - if (context.tracker.trackExternalModuleSymbolOfImportTypeNode) context.tracker.trackExternalModuleSymbolOfImportTypeNode(chain[0]); - context.approximateLength += specifier.length + 10; // specifier + import("") - if (!nonRootParts || isEntityName(nonRootParts)) { - if (nonRootParts) { - const lastId = isIdentifier(nonRootParts) ? nonRootParts : nonRootParts.right; - lastId.typeArguments = undefined; - } - return createImportTypeNode(lit, nonRootParts as EntityName, typeParameterNodes as readonly TypeNode[], isTypeOf); - } - else { - const splitNode = getTopmostIndexedAccessType(nonRootParts); - const qualifier = (splitNode.objectType as TypeReferenceNode).typeName; - return createIndexedAccessTypeNode(createImportTypeNode(lit, qualifier, typeParameterNodes as readonly TypeNode[], isTypeOf), splitNode.indexType); - } - } - - const entityName = createAccessFromSymbolChain(chain, chain.length - 1, 0); - if (isIndexedAccessTypeNode(entityName)) { - return entityName; // Indexed accesses can never be `typeof` - } - if (isTypeOf) { - return createTypeQueryNode(entityName); - } - else { - const lastId = isIdentifier(entityName) ? entityName : entityName.right; - const lastTypeArgs = lastId.typeArguments; - lastId.typeArguments = undefined; - return createTypeReferenceNode(entityName, lastTypeArgs as NodeArray); - } - - function createAccessFromSymbolChain(chain: Symbol[], index: number, stopper: number): EntityName | IndexedAccessTypeNode { - const typeParameterNodes = index === (chain.length - 1) ? overrideTypeArguments : lookupTypeParameterNodes(chain, index, context); - const symbol = chain[index]; - - const parent = chain[index - 1]; - let symbolName: string | undefined; - if (index === 0) { - context.flags |= NodeBuilderFlags.InInitialEntityName; - symbolName = getNameOfSymbolAsWritten(symbol, context); - context.approximateLength += (symbolName ? symbolName.length : 0) + 1; - context.flags ^= NodeBuilderFlags.InInitialEntityName; - } - else { - if (parent && getExportsOfSymbol(parent)) { - const exports = getExportsOfSymbol(parent); - forEachEntry(exports, (ex, name) => { - if (getSymbolIfSameReference(ex, symbol) && !isLateBoundName(name) && name !== InternalSymbolName.ExportEquals) { - symbolName = unescapeLeadingUnderscores(name); - return true; - } - }); - } - } - if (!symbolName) { - symbolName = getNameOfSymbolAsWritten(symbol, context); - } - context.approximateLength += symbolName.length + 1; - - if (!(context.flags & NodeBuilderFlags.ForbidIndexedAccessSymbolReferences) && parent && - getMembersOfSymbol(parent) && getMembersOfSymbol(parent).get(symbol.escapedName) && - getSymbolIfSameReference(getMembersOfSymbol(parent).get(symbol.escapedName)!, symbol)) { - // Should use an indexed access - const LHS = createAccessFromSymbolChain(chain, index - 1, stopper); - if (isIndexedAccessTypeNode(LHS)) { - return createIndexedAccessTypeNode(LHS, createLiteralTypeNode(createLiteral(symbolName))); - } - else { - return createIndexedAccessTypeNode(createTypeReferenceNode(LHS, typeParameterNodes as readonly TypeNode[]), createLiteralTypeNode(createLiteral(symbolName))); - } - } - - const identifier = setEmitFlags(createIdentifier(symbolName, typeParameterNodes), EmitFlags.NoAsciiEscaping); - identifier.symbol = symbol; - - if (index > stopper) { - const LHS = createAccessFromSymbolChain(chain, index - 1, stopper); - if (!isEntityName(LHS)) { - return Debug.fail("Impossible construct - an export of an indexed access cannot be reachable"); - } - return createQualifiedName(LHS, identifier); - } - return identifier; - } - } - - function typeParameterShadowsNameInScope(escapedName: __String, context: NodeBuilderContext) { - return !!resolveName(context.enclosingDeclaration, escapedName, SymbolFlags.Type, /*nameNotFoundArg*/ undefined, escapedName, /*isUse*/ false); - } - - function typeParameterToName(type: TypeParameter, context: NodeBuilderContext) { - if (context.flags & NodeBuilderFlags.GenerateNamesForShadowedTypeParams && context.typeParameterNames) { - const cached = context.typeParameterNames.get("" + getTypeId(type)); - if (cached) { - return cached; - } - } - let result = symbolToName(type.symbol, context, SymbolFlags.Type, /*expectsIdentifier*/ true); - if (!(result.kind & SyntaxKind.Identifier)) { - return createIdentifier("(Missing type parameter)"); - } - if (context.flags & NodeBuilderFlags.GenerateNamesForShadowedTypeParams) { - const rawtext = result.escapedText as string; - let i = 0; - let text = rawtext; - while ((context.typeParameterNamesByText && context.typeParameterNamesByText.get(text)) || typeParameterShadowsNameInScope(text as __String, context)) { - i++; - text = `${rawtext}_${i}`; - } - if (text !== rawtext) { - result = createIdentifier(text, result.typeArguments); - } - (context.typeParameterNames || (context.typeParameterNames = createMap())).set("" + getTypeId(type), result); - (context.typeParameterNamesByText || (context.typeParameterNamesByText = createMap())).set(result.escapedText as string, true); - } - return result; - } - - function symbolToName(symbol: Symbol, context: NodeBuilderContext, meaning: SymbolFlags, expectsIdentifier: true): Identifier; - function symbolToName(symbol: Symbol, context: NodeBuilderContext, meaning: SymbolFlags, expectsIdentifier: false): EntityName; - function symbolToName(symbol: Symbol, context: NodeBuilderContext, meaning: SymbolFlags, expectsIdentifier: boolean): EntityName { - const chain = lookupSymbolChain(symbol, context, meaning); - - if (expectsIdentifier && chain.length !== 1 - && !context.encounteredError - && !(context.flags & NodeBuilderFlags.AllowQualifedNameInPlaceOfIdentifier)) { - context.encounteredError = true; - } - return createEntityNameFromSymbolChain(chain, chain.length - 1); - - function createEntityNameFromSymbolChain(chain: Symbol[], index: number): EntityName { - const typeParameterNodes = lookupTypeParameterNodes(chain, index, context); - const symbol = chain[index]; - - if (index === 0) { - context.flags |= NodeBuilderFlags.InInitialEntityName; - } - const symbolName = getNameOfSymbolAsWritten(symbol, context); - if (index === 0) { - context.flags ^= NodeBuilderFlags.InInitialEntityName; - } - - const identifier = setEmitFlags(createIdentifier(symbolName, typeParameterNodes), EmitFlags.NoAsciiEscaping); - identifier.symbol = symbol; - - return index > 0 ? createQualifiedName(createEntityNameFromSymbolChain(chain, index - 1), identifier) : identifier; - } - } - - function symbolToExpression(symbol: Symbol, context: NodeBuilderContext, meaning: SymbolFlags) { - const chain = lookupSymbolChain(symbol, context, meaning); - - return createExpressionFromSymbolChain(chain, chain.length - 1); - - function createExpressionFromSymbolChain(chain: Symbol[], index: number): Expression { - const typeParameterNodes = lookupTypeParameterNodes(chain, index, context); - const symbol = chain[index]; - - if (index === 0) { - context.flags |= NodeBuilderFlags.InInitialEntityName; - } - let symbolName = getNameOfSymbolAsWritten(symbol, context); - if (index === 0) { - context.flags ^= NodeBuilderFlags.InInitialEntityName; - } - let firstChar = symbolName.charCodeAt(0); - - if (isSingleOrDoubleQuote(firstChar) && some(symbol.declarations, hasNonGlobalAugmentationExternalModuleSymbol)) { - return createLiteral(getSpecifierForModuleSymbol(symbol, context)); - } - const canUsePropertyAccess = firstChar === CharacterCodes.hash ? - symbolName.length > 1 && isIdentifierStart(symbolName.charCodeAt(1), languageVersion) : - isIdentifierStart(firstChar, languageVersion); - if (index === 0 || canUsePropertyAccess) { - const identifier = setEmitFlags(createIdentifier(symbolName, typeParameterNodes), EmitFlags.NoAsciiEscaping); - identifier.symbol = symbol; - - return index > 0 ? createPropertyAccess(createExpressionFromSymbolChain(chain, index - 1), identifier) : identifier; - } - else { - if (firstChar === CharacterCodes.openBracket) { - symbolName = symbolName.substring(1, symbolName.length - 1); - firstChar = symbolName.charCodeAt(0); - } - let expression: Expression | undefined; - if (isSingleOrDoubleQuote(firstChar)) { - expression = createLiteral(symbolName.substring(1, symbolName.length - 1).replace(/\\./g, s => s.substring(1))); - (expression as StringLiteral).singleQuote = firstChar === CharacterCodes.singleQuote; - } - else if (("" + +symbolName) === symbolName) { - expression = createLiteral(+symbolName); - } - if (!expression) { - expression = setEmitFlags(createIdentifier(symbolName, typeParameterNodes), EmitFlags.NoAsciiEscaping); - expression.symbol = symbol; - } - return createElementAccess(createExpressionFromSymbolChain(chain, index - 1), expression); - } - } - } - - function isSingleQuotedStringNamed(d: Declaration) { - const name = getNameOfDeclaration(d); - if (name && isStringLiteral(name) && ( - name.singleQuote || - (!nodeIsSynthesized(name) && startsWith(getTextOfNode(name, /*includeTrivia*/ false), "'")) - )) { - return true; - } - return false; - } - - function getPropertyNameNodeForSymbol(symbol: Symbol, context: NodeBuilderContext) { - const singleQuote = !!length(symbol.declarations) && every(symbol.declarations, isSingleQuotedStringNamed); - const fromNameType = getPropertyNameNodeForSymbolFromNameType(symbol, context, singleQuote); - if (fromNameType) { - return fromNameType; - } - if (isKnownSymbol(symbol)) { - return createComputedPropertyName(createPropertyAccess(createIdentifier("Symbol"), (symbol.escapedName as string).substr(3))); - } - const rawName = unescapeLeadingUnderscores(symbol.escapedName); - return createPropertyNameNodeForIdentifierOrLiteral(rawName, singleQuote); - } - - // See getNameForSymbolFromNameType for a stringy equivalent - function getPropertyNameNodeForSymbolFromNameType(symbol: Symbol, context: NodeBuilderContext, singleQuote?: boolean) { - const nameType = getSymbolLinks(symbol).nameType; - if (nameType) { - if (nameType.flags & TypeFlags.StringOrNumberLiteral) { - const name = "" + (nameType).value; - if (!isIdentifierText(name, compilerOptions.target) && !isNumericLiteralName(name)) { - return createLiteral(name, !!singleQuote); - } - if (isNumericLiteralName(name) && startsWith(name, "-")) { - return createComputedPropertyName(createLiteral(+name)); - } - return createPropertyNameNodeForIdentifierOrLiteral(name); - } - if (nameType.flags & TypeFlags.UniqueESSymbol) { - return createComputedPropertyName(symbolToExpression((nameType).symbol, context, SymbolFlags.Value)); - } - } - } - - function createPropertyNameNodeForIdentifierOrLiteral(name: string, singleQuote?: boolean) { - return isIdentifierText(name, compilerOptions.target) ? createIdentifier(name) : createLiteral(isNumericLiteralName(name) ? +name : name, !!singleQuote); - } - - function cloneNodeBuilderContext(context: NodeBuilderContext): NodeBuilderContext { - const initial: NodeBuilderContext = { ...context }; - // Make type parameters created within this context not consume the name outside this context - // The symbol serializer ends up creating many sibling scopes that all need "separate" contexts when - // it comes to naming things - within a normal `typeToTypeNode` call, the node builder only ever descends - // through the type tree, so the only cases where we could have used distinct sibling scopes was when there - // were multiple generic overloads with similar generated type parameter names - // The effect: - // When we write out - // export const x: (x: T) => T - // export const y: (x: T) => T - // we write it out like that, rather than as - // export const x: (x: T) => T - // export const y: (x: T_1) => T_1 - if (initial.typeParameterNames) { - initial.typeParameterNames = cloneMap(initial.typeParameterNames); - } - if (initial.typeParameterNamesByText) { - initial.typeParameterNamesByText = cloneMap(initial.typeParameterNamesByText); - } - if (initial.typeParameterSymbolList) { - initial.typeParameterSymbolList = cloneMap(initial.typeParameterSymbolList); - } - return initial; - } - - function symbolTableToDeclarationStatements(symbolTable: SymbolTable, context: NodeBuilderContext, bundled?: boolean): Statement[] { - const serializePropertySymbolForClass = makeSerializePropertySymbol(createProperty, SyntaxKind.MethodDeclaration, /*useAcessors*/ true); - const serializePropertySymbolForInterfaceWorker = makeSerializePropertySymbol((_decorators, mods, name, question, type, initializer) => createPropertySignature(mods, name, question, type, initializer), SyntaxKind.MethodSignature, /*useAcessors*/ false); - - // TODO: Use `setOriginalNode` on original declaration names where possible so these declarations see some kind of - // declaration mapping - - // We save the enclosing declaration off here so it's not adjusted by well-meaning declaration - // emit codepaths which want to apply more specific contexts (so we can still refer to the root real declaration - // we're trying to emit from later on) - const enclosingDeclaration = context.enclosingDeclaration!; - let results: Statement[] = []; - const visitedSymbols: Map = createMap(); - let deferredPrivates: Map | undefined; - const oldcontext = context; - context = { - ...oldcontext, - usedSymbolNames: createMap(), - remappedSymbolNames: createMap(), - tracker: { - ...oldcontext.tracker, - trackSymbol: (sym, decl, meaning) => { - const accessibleResult = isSymbolAccessible(sym, decl, meaning, /*computeALiases*/ false); - if (accessibleResult.accessibility === SymbolAccessibility.Accessible) { - // Lookup the root symbol of the chain of refs we'll use to access it and serialize it - const chain = lookupSymbolChainWorker(sym, context, meaning); - if (!(sym.flags & SymbolFlags.Property)) { - includePrivateSymbol(chain[0]); - } - } - else if (oldcontext.tracker && oldcontext.tracker.trackSymbol) { - oldcontext.tracker.trackSymbol(sym, decl, meaning); - } - } - } - }; - if (oldcontext.usedSymbolNames) { - oldcontext.usedSymbolNames.forEach((_, name) => { - context.usedSymbolNames!.set(name, true); - }); - } - forEachEntry(symbolTable, (symbol, name) => { - const baseName = unescapeLeadingUnderscores(name); - void getInternalSymbolName(symbol, baseName); // Called to cache values into `usedSymbolNames` and `remappedSymbolNames` - }); - let addingDeclare = !bundled; - const exportEquals = symbolTable.get(InternalSymbolName.ExportEquals); - if (exportEquals && symbolTable.size > 1 && exportEquals.flags & SymbolFlags.Alias) { - symbolTable = createSymbolTable(); - // Remove extraneous elements from root symbol table (they'll be mixed back in when the target of the `export=` is looked up) - symbolTable.set(InternalSymbolName.ExportEquals, exportEquals); - } - - visitSymbolTable(symbolTable); - return mergeRedundantStatements(results); - - function isIdentifierAndNotUndefined(node: Node | undefined): node is Identifier { - return !!node && node.kind === SyntaxKind.Identifier; - } - - function getNamesOfDeclaration(statement: Statement): Identifier[] { - if (isVariableStatement(statement)) { - return filter(map(statement.declarationList.declarations, getNameOfDeclaration), isIdentifierAndNotUndefined); - } - return filter([getNameOfDeclaration(statement as DeclarationStatement)], isIdentifierAndNotUndefined); - } - - function flattenExportAssignedNamespace(statements: Statement[]) { - const exportAssignment = find(statements, isExportAssignment); - const ns = find(statements, isModuleDeclaration); - if (ns && exportAssignment && exportAssignment.isExportEquals && - isIdentifier(exportAssignment.expression) && isIdentifier(ns.name) && idText(ns.name) === idText(exportAssignment.expression) && - ns.body && isModuleBlock(ns.body)) { - // Pass 0: Correct situations where a module has both an `export = ns` and multiple top-level exports by stripping the export modifiers from - // the top-level exports and exporting them in the targeted ns, as can occur when a js file has both typedefs and `module.export` assignments - const excessExports = filter(statements, s => !!(getModifierFlags(s) & ModifierFlags.Export)); - if (length(excessExports)) { - ns.body.statements = createNodeArray([...ns.body.statements, createExportDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - createNamedExports(map(flatMap(excessExports, e => getNamesOfDeclaration(e)), id => createExportSpecifier(/*alias*/ undefined, id))), - /*moduleSpecifier*/ undefined - )]); - } - - - // Pass 1: Flatten `export namespace _exports {} export = _exports;` so long as the `export=` only points at a single namespace declaration - if (!find(statements, s => s !== ns && nodeHasName(s, ns.name as Identifier))) { - results = []; - forEach(ns.body.statements, s => { - addResult(s, ModifierFlags.None); // Recalculates the ambient (and export, if applicable from above) flag - }); - statements = [...filter(statements, s => s !== ns && s !== exportAssignment), ...results]; - } - } - return statements; - } - - function mergeExportDeclarations(statements: Statement[]) { - // Pass 2: Combine all `export {}` declarations - const exports = filter(statements, d => isExportDeclaration(d) && !d.moduleSpecifier && !!d.exportClause && isNamedExports(d.exportClause)) as ExportDeclaration[]; - if (length(exports) > 1) { - const nonExports = filter(statements, d => !isExportDeclaration(d) || !!d.moduleSpecifier || !d.exportClause); - statements = [...nonExports, createExportDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - createNamedExports(flatMap(exports, e => cast(e.exportClause, isNamedExports).elements)), - /*moduleSpecifier*/ undefined - )]; - } - // Pass 2b: Also combine all `export {} from "..."` declarations as needed - const reexports = filter(statements, d => isExportDeclaration(d) && !!d.moduleSpecifier && !!d.exportClause && isNamedExports(d.exportClause)) as ExportDeclaration[]; - if (length(reexports) > 1) { - const groups = group(reexports, decl => isStringLiteral(decl.moduleSpecifier!) ? ">" + decl.moduleSpecifier.text : ">"); - if (groups.length !== reexports.length) { - for (const group of groups) { - if (group.length > 1) { - // remove group members from statements and then merge group members and add back to statements - statements = [ - ...filter(statements, s => group.indexOf(s as ExportDeclaration) === -1), - createExportDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - createNamedExports(flatMap(group, e => cast(e.exportClause, isNamedExports).elements)), - group[0].moduleSpecifier - ) - ]; - } - } - } - } - return statements; - } - - function inlineExportModifiers(statements: Statement[]) { - // Pass 3: Move all `export {}`'s to `export` modifiers where possible - const exportDecl = find(statements, d => isExportDeclaration(d) && !d.moduleSpecifier && !!d.exportClause) as ExportDeclaration | undefined; - if (exportDecl && exportDecl.exportClause && isNamedExports(exportDecl.exportClause)) { - const replacements = mapDefined(exportDecl.exportClause.elements, e => { - if (!e.propertyName) { - // export {name} - look thru `statements` for `name`, and if all results can take an `export` modifier, do so and filter it - const associated = filter(statements, s => nodeHasName(s, e.name)); - if (length(associated) && every(associated, canHaveExportModifier)) { - forEach(associated, addExportModifier); - return undefined; - } - } - return e; - }); - if (!length(replacements)) { - // all clauses removed, filter the export declaration - statements = filter(statements, s => s !== exportDecl); - } - else { - // some items filtered, others not - update the export declaration - // (mutating because why not, we're building a whole new tree here anyway) - exportDecl.exportClause.elements = createNodeArray(replacements); - } - } - return statements; - } - - function mergeRedundantStatements(statements: Statement[]) { - statements = flattenExportAssignedNamespace(statements); - statements = mergeExportDeclarations(statements); - statements = inlineExportModifiers(statements); - - // Not a cleanup, but as a final step: If there is a mix of `export` and non-`export` declarations, but no `export =` or `export {}` add a `export {};` so - // declaration privacy is respected. - if (enclosingDeclaration && - ((isSourceFile(enclosingDeclaration) && isExternalOrCommonJsModule(enclosingDeclaration)) || isModuleDeclaration(enclosingDeclaration)) && - (!some(statements, isExternalModuleIndicator) || (!hasScopeMarker(statements) && some(statements, needsScopeMarker)))) { - statements.push(createEmptyExports()); - } - return statements; - } - - function canHaveExportModifier(node: Statement) { - return isEnumDeclaration(node) || - isVariableStatement(node) || - isFunctionDeclaration(node) || - isClassDeclaration(node) || - (isModuleDeclaration(node) && !isExternalModuleAugmentation(node) && !isGlobalScopeAugmentation(node)) || - isInterfaceDeclaration(node) || - isTypeDeclaration(node); - } - - function addExportModifier(statement: Statement) { - const flags = (getModifierFlags(statement) | ModifierFlags.Export) & ~ModifierFlags.Ambient; - statement.modifiers = createNodeArray(createModifiersFromModifierFlags(flags)); - statement.modifierFlagsCache = 0; - } - - function visitSymbolTable(symbolTable: SymbolTable, suppressNewPrivateContext?: boolean, propertyAsAlias?: boolean) { - const oldDeferredPrivates = deferredPrivates; - if (!suppressNewPrivateContext) { - deferredPrivates = createMap(); - } - symbolTable.forEach((symbol: Symbol) => { - serializeSymbol(symbol, /*isPrivate*/ false, !!propertyAsAlias); - }); - if (!suppressNewPrivateContext) { - // deferredPrivates will be filled up by visiting the symbol table - // And will continue to iterate as elements are added while visited `deferredPrivates` - // (As that's how a map iterator is defined to work) - deferredPrivates!.forEach((symbol: Symbol) => { - serializeSymbol(symbol, /*isPrivate*/ true, !!propertyAsAlias); - }); - } - deferredPrivates = oldDeferredPrivates; - } - - function serializeSymbol(symbol: Symbol, isPrivate: boolean, propertyAsAlias: boolean) { - // cache visited list based on merged symbol, since we want to use the unmerged top-level symbol, but - // still skip reserializing it if we encounter the merged product later on - const visitedSym = getMergedSymbol(symbol); - if (visitedSymbols.has("" + getSymbolId(visitedSym))) { - return; // Already printed - } - visitedSymbols.set("" + getSymbolId(visitedSym), true); - // Only actually serialize symbols within the correct enclosing declaration, otherwise do nothing with the out-of-context symbol - const skipMembershipCheck = !isPrivate; // We only call this on exported symbols when we know they're in the correct scope - if (skipMembershipCheck || (!!length(symbol.declarations) && some(symbol.declarations, d => !!findAncestor(d, n => n === enclosingDeclaration)))) { - const oldContext = context; - context = cloneNodeBuilderContext(context); - const result = serializeSymbolWorker(symbol, isPrivate, propertyAsAlias); - context = oldContext; - return result; - } - } - - // Synthesize declarations for a symbol - might be an Interface, a Class, a Namespace, a Type, a Variable (const, let, or var), an Alias - // or a merge of some number of those. - // An interesting challenge is ensuring that when classes merge with namespaces and interfaces, is keeping - // each symbol in only one of the representations - // Also, synthesizing a default export of some kind - // If it's an alias: emit `export default ref` - // If it's a property: emit `export default _default` with a `_default` prop - // If it's a class/interface/function: emit a class/interface/function with a `default` modifier - // These forms can merge, eg (`export default 12; export default interface A {}`) - function serializeSymbolWorker(symbol: Symbol, isPrivate: boolean, propertyAsAlias: boolean) { - const symbolName = unescapeLeadingUnderscores(symbol.escapedName); - const isDefault = symbol.escapedName === InternalSymbolName.Default; - if (isStringANonContextualKeyword(symbolName) && !isDefault) { - // Oh no. We cannot use this symbol's name as it's name... It's likely some jsdoc had an invalid name like `export` or `default` :( - context.encounteredError = true; - // TODO: Issue error via symbol tracker? - return; // If we need to emit a private with a keyword name, we're done for, since something else will try to refer to it by that name - } - const needsPostExportDefault = isDefault && !!( - symbol.flags & SymbolFlags.ExportDoesNotSupportDefaultModifier - || (symbol.flags & SymbolFlags.Function && length(getPropertiesOfType(getTypeOfSymbol(symbol)))) - ) && !(symbol.flags & SymbolFlags.Alias); // An alias symbol should preclude needing to make an alias ourselves - if (needsPostExportDefault) { - isPrivate = true; - } - const modifierFlags = (!isPrivate ? ModifierFlags.Export : 0) | (isDefault && !needsPostExportDefault ? ModifierFlags.Default : 0); - const isConstMergedWithNS = symbol.flags & SymbolFlags.Module && - symbol.flags & (SymbolFlags.BlockScopedVariable | SymbolFlags.FunctionScopedVariable | SymbolFlags.Property) && - symbol.escapedName !== InternalSymbolName.ExportEquals; - const isConstMergedWithNSPrintableAsSignatureMerge = isConstMergedWithNS && isTypeRepresentableAsFunctionNamespaceMerge(getTypeOfSymbol(symbol), symbol); - if (symbol.flags & (SymbolFlags.Function | SymbolFlags.Method) || isConstMergedWithNSPrintableAsSignatureMerge) { - serializeAsFunctionNamespaceMerge(getTypeOfSymbol(symbol), symbol, getInternalSymbolName(symbol, symbolName), modifierFlags); - } - if (symbol.flags & SymbolFlags.TypeAlias) { - serializeTypeAlias(symbol, symbolName, modifierFlags); - } - // Need to skip over export= symbols below - json source files get a single `Property` flagged - // symbol of name `export=` which needs to be handled like an alias. It's not great, but it is what it is. - if (symbol.flags & (SymbolFlags.BlockScopedVariable | SymbolFlags.FunctionScopedVariable | SymbolFlags.Property) - && symbol.escapedName !== InternalSymbolName.ExportEquals - && !(symbol.flags & SymbolFlags.Prototype) - && !(symbol.flags & SymbolFlags.Class) - && !isConstMergedWithNSPrintableAsSignatureMerge) { - serializeVariableOrProperty(symbol, symbolName, isPrivate, needsPostExportDefault, propertyAsAlias, modifierFlags); - } - if (symbol.flags & SymbolFlags.Enum) { - serializeEnum(symbol, symbolName, modifierFlags); - } - if (symbol.flags & SymbolFlags.Class) { - if (symbol.flags & SymbolFlags.Property) { - // Looks like a `module.exports.Sub = class {}` - if we serialize `symbol` as a class, the result will have no members, - // since the classiness is actually from the target of the effective alias the symbol is. yes. A BlockScopedVariable|Class|Property - // _really_ acts like an Alias, and none of a BlockScopedVariable, Class, or Property. This is the travesty of JS binding today. - serializeAsAlias(symbol, getInternalSymbolName(symbol, symbolName), modifierFlags); - } - else { - serializeAsClass(symbol, getInternalSymbolName(symbol, symbolName), modifierFlags); - } - } - if ((symbol.flags & (SymbolFlags.ValueModule | SymbolFlags.NamespaceModule) && (!isConstMergedWithNS || isTypeOnlyNamespace(symbol))) || isConstMergedWithNSPrintableAsSignatureMerge) { - serializeModule(symbol, symbolName, modifierFlags); - } - if (symbol.flags & SymbolFlags.Interface) { - serializeInterface(symbol, symbolName, modifierFlags); - } - if (symbol.flags & SymbolFlags.Alias) { - serializeAsAlias(symbol, getInternalSymbolName(symbol, symbolName), modifierFlags); - } - if (symbol.flags & SymbolFlags.Property && symbol.escapedName === InternalSymbolName.ExportEquals) { - serializeMaybeAliasAssignment(symbol); - } - if (symbol.flags & SymbolFlags.ExportStar) { - // synthesize export * from "moduleReference" - // Straightforward - only one thing to do - make an export declaration - for (const node of symbol.declarations) { - const resolvedModule = resolveExternalModuleName(node, (node as ExportDeclaration).moduleSpecifier!); - if (!resolvedModule) continue; - addResult(createExportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, /*exportClause*/ undefined, createLiteral(getSpecifierForModuleSymbol(resolvedModule, context))), ModifierFlags.None); - } - } - if (needsPostExportDefault) { - addResult(createExportAssignment(/*decorators*/ undefined, /*modifiers*/ undefined, /*isExportAssignment*/ false, createIdentifier(getInternalSymbolName(symbol, symbolName))), ModifierFlags.None); - } - } - - function includePrivateSymbol(symbol: Symbol) { - if (some(symbol.declarations, isParameterDeclaration)) return; - Debug.assertIsDefined(deferredPrivates); - getUnusedName(unescapeLeadingUnderscores(symbol.escapedName), symbol); // Call to cache unique name for symbol - deferredPrivates.set("" + getSymbolId(symbol), symbol); - } - - function isExportingScope(enclosingDeclaration: Node) { - return ((isSourceFile(enclosingDeclaration) && (isExternalOrCommonJsModule(enclosingDeclaration) || isJsonSourceFile(enclosingDeclaration))) || - (isAmbientModule(enclosingDeclaration) && !isGlobalScopeAugmentation(enclosingDeclaration))); - } - - // Prepends a `declare` and/or `export` modifier if the context requires it, and then adds `node` to `result` and returns `node` - // Note: This _mutates_ `node` without using `updateNode` - the assumption being that all nodes should be manufactured fresh by the node builder - function addResult(node: Statement, additionalModifierFlags: ModifierFlags) { - let newModifierFlags: ModifierFlags = ModifierFlags.None; - if (additionalModifierFlags & ModifierFlags.Export && - enclosingDeclaration && - isExportingScope(enclosingDeclaration) && - canHaveExportModifier(node) - ) { - // Classes, namespaces, variables, functions, interfaces, and types should all be `export`ed in a module context if not private - newModifierFlags |= ModifierFlags.Export; - } - if (addingDeclare && !(newModifierFlags & ModifierFlags.Export) && - (!enclosingDeclaration || !(enclosingDeclaration.flags & NodeFlags.Ambient)) && - (isEnumDeclaration(node) || isVariableStatement(node) || isFunctionDeclaration(node) || isClassDeclaration(node) || isModuleDeclaration(node))) { - // Classes, namespaces, variables, enums, and functions all need `declare` modifiers to be valid in a declaration file top-level scope - newModifierFlags |= ModifierFlags.Ambient; - } - if ((additionalModifierFlags & ModifierFlags.Default) && (isClassDeclaration(node) || isInterfaceDeclaration(node) || isFunctionDeclaration(node))) { - newModifierFlags |= ModifierFlags.Default; - } - if (newModifierFlags) { - node.modifiers = createNodeArray(createModifiersFromModifierFlags(newModifierFlags | getModifierFlags(node))); - node.modifierFlagsCache = 0; // Reset computed flags cache - } - results.push(node); - } - - function serializeTypeAlias(symbol: Symbol, symbolName: string, modifierFlags: ModifierFlags) { - const aliasType = getDeclaredTypeOfTypeAlias(symbol); - const typeParams = getSymbolLinks(symbol).typeParameters; - const typeParamDecls = map(typeParams, p => typeParameterToDeclaration(p, context)); - const jsdocAliasDecl = find(symbol.declarations, isJSDocTypeAlias); - const commentText = jsdocAliasDecl ? jsdocAliasDecl.comment || jsdocAliasDecl.parent.comment : undefined; - const oldFlags = context.flags; - context.flags |= NodeBuilderFlags.InTypeAlias; - addResult(setSyntheticLeadingComments( - createTypeAliasDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, getInternalSymbolName(symbol, symbolName), typeParamDecls, typeToTypeNodeHelper(aliasType, context)), - !commentText ? [] : [{ kind: SyntaxKind.MultiLineCommentTrivia, text: "*\n * " + commentText.replace(/\n/g, "\n * ") + "\n ", pos: -1, end: -1, hasTrailingNewLine: true }] - ), modifierFlags); - context.flags = oldFlags; - } - - function serializeInterface(symbol: Symbol, symbolName: string, modifierFlags: ModifierFlags) { - const interfaceType = getDeclaredTypeOfClassOrInterface(symbol); - const localParams = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol); - const typeParamDecls = map(localParams, p => typeParameterToDeclaration(p, context)); - const baseTypes = getBaseTypes(interfaceType); - const baseType = length(baseTypes) ? getIntersectionType(baseTypes) : undefined; - const members = flatMap(getPropertiesOfType(interfaceType), p => serializePropertySymbolForInterface(p, baseType)); - const callSignatures = serializeSignatures(SignatureKind.Call, interfaceType, baseType, SyntaxKind.CallSignature) as CallSignatureDeclaration[]; - const constructSignatures = serializeSignatures(SignatureKind.Construct, interfaceType, baseType, SyntaxKind.ConstructSignature) as ConstructSignatureDeclaration[]; - const indexSignatures = serializeIndexSignatures(interfaceType, baseType); - - const heritageClauses = !length(baseTypes) ? undefined : [createHeritageClause(SyntaxKind.ExtendsKeyword, mapDefined(baseTypes, b => trySerializeAsTypeReference(b)))]; - addResult(createInterfaceDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - getInternalSymbolName(symbol, symbolName), - typeParamDecls, - heritageClauses, - [...indexSignatures, ...constructSignatures, ...callSignatures, ...members] - ), modifierFlags); - } - - function getNamespaceMembersForSerialization(symbol: Symbol) { - return !symbol.exports ? [] : filter(arrayFrom((symbol.exports).values()), p => !((p.flags & SymbolFlags.Prototype) || (p.escapedName === "prototype"))); - } - - function isTypeOnlyNamespace(symbol: Symbol) { - return every(getNamespaceMembersForSerialization(symbol), m => !(resolveSymbol(m).flags & SymbolFlags.Value)); - } - - function serializeModule(symbol: Symbol, symbolName: string, modifierFlags: ModifierFlags) { - const members = getNamespaceMembersForSerialization(symbol); - // Split NS members up by declaration - members whose parent symbol is the ns symbol vs those whose is not (but were added in later via merging) - const locationMap = arrayToMultiMap(members, m => m.parent && m.parent === symbol ? "real" : "merged"); - const realMembers = locationMap.get("real") || emptyArray; - const mergedMembers = locationMap.get("merged") || emptyArray; - // TODO: `suppressNewPrivateContext` is questionable -we need to simply be emitting privates in whatever scope they were declared in, rather - // than whatever scope we traverse to them in. That's a bit of a complex rewrite, since we're not _actually_ tracking privates at all in advance, - // so we don't even have placeholders to fill in. - if (length(realMembers)) { - const localName = getInternalSymbolName(symbol, symbolName); - serializeAsNamespaceDeclaration(realMembers, localName, modifierFlags, !!(symbol.flags & (SymbolFlags.Function | SymbolFlags.Assignment))); - } - if (length(mergedMembers)) { - const localName = getInternalSymbolName(symbol, symbolName); - const nsBody = createModuleBlock([createExportDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - createNamedExports(map(filter(mergedMembers, n => n.escapedName !== InternalSymbolName.ExportEquals), s => { - const name = unescapeLeadingUnderscores(s.escapedName); - const localName = getInternalSymbolName(s, name); - const aliasDecl = s.declarations && getDeclarationOfAliasSymbol(s); - const target = aliasDecl && getTargetOfAliasDeclaration(aliasDecl, /*dontRecursivelyResolve*/ true); - includePrivateSymbol(target || s); - const targetName = target ? getInternalSymbolName(target, unescapeLeadingUnderscores(target.escapedName)) : localName; - return createExportSpecifier(name === targetName ? undefined : targetName, name); - })) - )]); - addResult(createModuleDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - createIdentifier(localName), - nsBody, - NodeFlags.Namespace - ), ModifierFlags.None); - } - } - - function serializeEnum(symbol: Symbol, symbolName: string, modifierFlags: ModifierFlags) { - addResult(createEnumDeclaration( - /*decorators*/ undefined, - createModifiersFromModifierFlags(isConstEnumSymbol(symbol) ? ModifierFlags.Const : 0), - getInternalSymbolName(symbol, symbolName), - map(filter(getPropertiesOfType(getTypeOfSymbol(symbol)), p => !!(p.flags & SymbolFlags.EnumMember)), p => { - // TODO: Handle computed names - // I hate that to get the initialized value we need to walk back to the declarations here; but there's no - // other way to get the possible const value of an enum member that I'm aware of, as the value is cached - // _on the declaration_, not on the declaration's symbol... - const initializedValue = p.declarations && p.declarations[0] && isEnumMember(p.declarations[0]) && getConstantValue(p.declarations[0] as EnumMember); - return createEnumMember(unescapeLeadingUnderscores(p.escapedName), initializedValue === undefined ? undefined : createLiteral(initializedValue)); - }) - ), modifierFlags); - } - - function serializeVariableOrProperty(symbol: Symbol, symbolName: string, isPrivate: boolean, needsPostExportDefault: boolean, propertyAsAlias: boolean | undefined, modifierFlags: ModifierFlags) { - if (propertyAsAlias) { - serializeMaybeAliasAssignment(symbol); - } - else { - const type = getTypeOfSymbol(symbol); - const localName = getInternalSymbolName(symbol, symbolName); - if (!(symbol.flags & SymbolFlags.Function) && isTypeRepresentableAsFunctionNamespaceMerge(type, symbol)) { - // If the type looks like a function declaration + ns could represent it, and it's type is sourced locally, rewrite it into a function declaration + ns - serializeAsFunctionNamespaceMerge(type, symbol, localName, modifierFlags); - } - else { - // A Class + Property merge is made for a `module.exports.Member = class {}`, and it doesn't serialize well as either a class _or_ a property symbol - in fact, _it behaves like an alias!_ - // `var` is `FunctionScopedVariable`, `const` and `let` are `BlockScopedVariable`, and `module.exports.thing =` is `Property` - const flags = !(symbol.flags & SymbolFlags.BlockScopedVariable) ? undefined - : isConstVariable(symbol) ? NodeFlags.Const - : NodeFlags.Let; - const name = (needsPostExportDefault || !(symbol.flags & SymbolFlags.Property)) ? localName : getUnusedName(localName, symbol); - let textRange: Node | undefined = symbol.declarations && find(symbol.declarations, d => isVariableDeclaration(d)); - if (textRange && isVariableDeclarationList(textRange.parent) && textRange.parent.declarations.length === 1) { - textRange = textRange.parent.parent; - } - const statement = setTextRange(createVariableStatement(/*modifiers*/ undefined, createVariableDeclarationList([ - createVariableDeclaration(name, serializeTypeForDeclaration(type, symbol)) - ], flags)), textRange); - addResult(statement, name !== localName ? modifierFlags & ~ModifierFlags.Export : modifierFlags); - if (name !== localName && !isPrivate) { - // We rename the variable declaration we generate for Property symbols since they may have a name which - // conflicts with a local declaration. For example, given input: - // ``` - // function g() {} - // module.exports.g = g - // ``` - // In such a situation, we have a local variable named `g`, and a separate exported variable named `g`. - // Naively, we would emit - // ``` - // function g() {} - // export const g: typeof g; - // ``` - // That's obviously incorrect - the `g` in the type annotation needs to refer to the local `g`, but - // the export declaration shadows it. - // To work around that, we instead write - // ``` - // function g() {} - // const g_1: typeof g; - // export { g_1 as g }; - // ``` - // To create an export named `g` that does _not_ shadow the local `g` - addResult( - createExportDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - createNamedExports([createExportSpecifier(name, localName)]) - ), - ModifierFlags.None - ); - } - } - } - } - - function serializeAsFunctionNamespaceMerge(type: Type, symbol: Symbol, localName: string, modifierFlags: ModifierFlags) { - const signatures = getSignaturesOfType(type, SignatureKind.Call); - for (const sig of signatures) { - // Each overload becomes a separate function declaration, in order - const decl = signatureToSignatureDeclarationHelper(sig, SyntaxKind.FunctionDeclaration, context) as FunctionDeclaration; - decl.name = createIdentifier(localName); - addResult(setTextRange(decl, sig.declaration), modifierFlags); - } - // Module symbol emit will take care of module-y members, provided it has exports - if (!(symbol.flags & (SymbolFlags.ValueModule | SymbolFlags.NamespaceModule) && !!symbol.exports && !!symbol.exports.size)) { - const props = filter(getPropertiesOfType(type), p => !((p.flags & SymbolFlags.Prototype) || (p.escapedName === "prototype"))); - serializeAsNamespaceDeclaration(props, localName, modifierFlags, /*suppressNewPrivateContext*/ true); - } - } - - function serializeAsNamespaceDeclaration(props: readonly Symbol[], localName: string, modifierFlags: ModifierFlags, suppressNewPrivateContext: boolean) { - if (length(props)) { - const localVsRemoteMap = arrayToMultiMap(props, p => - !length(p.declarations) || some(p.declarations, d => - getSourceFileOfNode(d) === getSourceFileOfNode(context.enclosingDeclaration!) - ) ? "local" : "remote" - ); - const localProps = localVsRemoteMap.get("local") || emptyArray; - // handle remote props first - we need to make an `import` declaration that points at the module containing each remote - // prop in the outermost scope (TODO: a namespace within a namespace would need to be appropriately handled by this) - // Example: - // import Foo_1 = require("./exporter"); - // export namespace ns { - // import Foo = Foo_1.Foo; - // export { Foo }; - // export const c: number; - // } - // This is needed because in JS, statements like `const x = require("./f")` support both type and value lookup, even if they're - // normally just value lookup (so it functions kinda like an alias even when it's not an alias) - // _Usually_, we'll simply print the top-level as an alias instead of a `var` in such situations, however is is theoretically - // possible to encounter a situation where a type has members from both the current file and other files - in those situations, - // emit akin to the above would be needed. - - // Add a namespace - const fakespace = createModuleDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, createIdentifier(localName), createModuleBlock([]), NodeFlags.Namespace); - fakespace.flags ^= NodeFlags.Synthesized; // unset synthesized so it is usable as an enclosing declaration - fakespace.parent = enclosingDeclaration as SourceFile | NamespaceDeclaration; - fakespace.locals = createSymbolTable(props); - fakespace.symbol = props[0].parent!; - const oldResults = results; - results = []; - const oldAddingDeclare = addingDeclare; - addingDeclare = false; - const subcontext = { ...context, enclosingDeclaration: fakespace }; - const oldContext = context; - context = subcontext; - // TODO: implement handling for the localVsRemoteMap.get("remote") - should be difficult to trigger (see comment above), as only interesting cross-file js merges should make this possible - visitSymbolTable(createSymbolTable(localProps), suppressNewPrivateContext, /*propertyAsAlias*/ true); - context = oldContext; - addingDeclare = oldAddingDeclare; - const declarations = results; - results = oldResults; - fakespace.flags ^= NodeFlags.Synthesized; // reset synthesized - fakespace.parent = undefined!; - fakespace.locals = undefined!; - fakespace.symbol = undefined!; - fakespace.body = createModuleBlock(declarations); - addResult(fakespace, modifierFlags); // namespaces can never be default exported - } - } - - function serializeAsClass(symbol: Symbol, localName: string, modifierFlags: ModifierFlags) { - const localParams = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol); - const typeParamDecls = map(localParams, p => typeParameterToDeclaration(p, context)); - const classType = getDeclaredTypeOfClassOrInterface(symbol); - const baseTypes = getBaseTypes(classType); - const implementsTypes = getImplementsTypes(classType); - const staticType = getTypeOfSymbol(symbol); - const staticBaseType = getBaseConstructorTypeOfClass(staticType as InterfaceType); - const heritageClauses = [ - ...!length(baseTypes) ? [] : [createHeritageClause(SyntaxKind.ExtendsKeyword, map(baseTypes, b => serializeBaseType(b, staticBaseType, localName)))], - ...!length(implementsTypes) ? [] : [createHeritageClause(SyntaxKind.ImplementsKeyword, map(implementsTypes, b => serializeBaseType(b, staticBaseType, localName)))] - ]; - const symbolProps = getPropertiesOfType(classType); - const publicSymbolProps = filter(symbolProps, s => { - const valueDecl = s.valueDeclaration; - Debug.assertIsDefined(valueDecl); - return !(isNamedDeclaration(valueDecl) && isPrivateIdentifier(valueDecl.name)); - }); - const hasPrivateIdentifier = some(symbolProps, s => { - const valueDecl = s.valueDeclaration; - Debug.assertIsDefined(valueDecl); - return isNamedDeclaration(valueDecl) && isPrivateIdentifier(valueDecl.name); - }); - // Boil down all private properties into a single one. - const privateProperties = hasPrivateIdentifier ? - [createProperty( - /*decorators*/ undefined, - /*modifiers*/ undefined, - createPrivateIdentifier("#private"), - /*questionOrExclamationToken*/ undefined, - /*type*/ undefined, - /*initializer*/ undefined, - )] : - emptyArray; - const publicProperties = flatMap(publicSymbolProps, p => serializePropertySymbolForClass(p, /*isStatic*/ false, baseTypes[0])); - // Consider static members empty if symbol also has function or module meaning - function namespacey emit will handle statics - const staticMembers = symbol.flags & (SymbolFlags.Function | SymbolFlags.ValueModule) - ? [] - : flatMap(filter( - getPropertiesOfType(staticType), - p => !(p.flags & SymbolFlags.Prototype) && p.escapedName !== "prototype" - ), p => serializePropertySymbolForClass(p, /*isStatic*/ true, staticBaseType)); - const constructors = serializeSignatures(SignatureKind.Construct, staticType, baseTypes[0], SyntaxKind.Constructor) as ConstructorDeclaration[]; - for (const c of constructors) { - // A constructor's return type and type parameters are supposed to be controlled by the enclosing class declaration - // `signatureToSignatureDeclarationHelper` appends them regardless, so for now we delete them here - c.type = undefined; - c.typeParameters = undefined; - } - const indexSignatures = serializeIndexSignatures(classType, baseTypes[0]); - addResult(setTextRange(createClassDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - localName, - typeParamDecls, - heritageClauses, - [...indexSignatures, ...staticMembers, ...constructors, ...publicProperties, ...privateProperties] - ), symbol.declarations && filter(symbol.declarations, d => isClassDeclaration(d) || isClassExpression(d))[0]), modifierFlags); - } - - function serializeAsAlias(symbol: Symbol, localName: string, modifierFlags: ModifierFlags) { - // synthesize an alias, eg `export { symbolName as Name }` - // need to mark the alias `symbol` points at - // as something we need to serialize as a private declaration as well - const node = getDeclarationOfAliasSymbol(symbol); - if (!node) return Debug.fail(); - const target = getMergedSymbol(getTargetOfAliasDeclaration(node, /*dontRecursivelyResolve*/ true)); - if (!target) { - return; - } - let verbatimTargetName = unescapeLeadingUnderscores(target.escapedName); - if (verbatimTargetName === InternalSymbolName.ExportEquals && (compilerOptions.esModuleInterop || compilerOptions.allowSyntheticDefaultImports)) { - // target refers to an `export=` symbol that was hoisted into a synthetic default - rename here to match - verbatimTargetName = InternalSymbolName.Default; - } - const targetName = getInternalSymbolName(target, verbatimTargetName); - includePrivateSymbol(target); // the target may be within the same scope - attempt to serialize it first - switch (node.kind) { - case SyntaxKind.ImportEqualsDeclaration: - // Could be a local `import localName = ns.member` or - // an external `import localName = require("whatever")` - const isLocalImport = !(target.flags & SymbolFlags.ValueModule); - addResult(createImportEqualsDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - createIdentifier(localName), - isLocalImport - ? symbolToName(target, context, SymbolFlags.All, /*expectsIdentifier*/ false) - : createExternalModuleReference(createLiteral(getSpecifierForModuleSymbol(symbol, context))) - ), isLocalImport ? modifierFlags : ModifierFlags.None); - break; - case SyntaxKind.NamespaceExportDeclaration: - // export as namespace foo - // TODO: Not part of a file's local or export symbol tables - // Is bound into file.symbol.globalExports instead, which we don't currently traverse - addResult(createNamespaceExportDeclaration(idText((node as NamespaceExportDeclaration).name)), ModifierFlags.None); - break; - case SyntaxKind.ImportClause: - addResult(createImportDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - createImportClause(createIdentifier(localName), /*namedBindings*/ undefined), - // We use `target.parent || target` below as `target.parent` is unset when the target is a module which has been export assigned - // And then made into a default by the `esModuleInterop` or `allowSyntheticDefaultImports` flag - // In such cases, the `target` refers to the module itself already - createLiteral(getSpecifierForModuleSymbol(target.parent || target, context)) - ), ModifierFlags.None); - break; - case SyntaxKind.NamespaceImport: - addResult(createImportDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - createImportClause(/*importClause*/ undefined, createNamespaceImport(createIdentifier(localName))), - createLiteral(getSpecifierForModuleSymbol(target, context)) - ), ModifierFlags.None); - break; - case SyntaxKind.NamespaceExport: - addResult(createExportDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - createNamespaceExport(createIdentifier(localName)), - createLiteral(getSpecifierForModuleSymbol(target, context)) - ), ModifierFlags.None); - break; - case SyntaxKind.ImportSpecifier: - addResult(createImportDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - createImportClause(/*importClause*/ undefined, createNamedImports([ - createImportSpecifier( - localName !== verbatimTargetName ? createIdentifier(verbatimTargetName) : undefined, - createIdentifier(localName) - ) - ])), - createLiteral(getSpecifierForModuleSymbol(target.parent || target, context)) - ), ModifierFlags.None); - break; - case SyntaxKind.ExportSpecifier: - // does not use localName because the symbol name in this case refers to the name in the exports table, - // which we must exactly preserve - const specifier = (node.parent.parent as ExportDeclaration).moduleSpecifier; - // targetName is only used when the target is local, as otherwise the target is an alias that points at - // another file - serializeExportSpecifier( - unescapeLeadingUnderscores(symbol.escapedName), - specifier ? verbatimTargetName : targetName, - specifier && isStringLiteralLike(specifier) ? createLiteral(specifier.text) : undefined - ); - break; - case SyntaxKind.ExportAssignment: - serializeMaybeAliasAssignment(symbol); - break; - case SyntaxKind.BinaryExpression: - case SyntaxKind.PropertyAccessExpression: - // Could be best encoded as though an export specifier or as though an export assignment - // If name is default or export=, do an export assignment - // Otherwise do an export specifier - if (symbol.escapedName === InternalSymbolName.Default || symbol.escapedName === InternalSymbolName.ExportEquals) { - serializeMaybeAliasAssignment(symbol); - } - else { - serializeExportSpecifier(localName, targetName); - } - break; - default: - return Debug.failBadSyntaxKind(node, "Unhandled alias declaration kind in symbol serializer!"); - } - } - - function serializeExportSpecifier(localName: string, targetName: string, specifier?: Expression) { - addResult(createExportDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - createNamedExports([createExportSpecifier(localName !== targetName ? targetName : undefined, localName)]), - specifier - ), ModifierFlags.None); - } - - function serializeMaybeAliasAssignment(symbol: Symbol) { - if (symbol.flags & SymbolFlags.Prototype) { - return; - } - const name = unescapeLeadingUnderscores(symbol.escapedName); - const isExportEquals = name === InternalSymbolName.ExportEquals; - const isDefault = name === InternalSymbolName.Default; - const isExportAssignment = isExportEquals || isDefault; - // synthesize export = ref - // ref should refer to either be a locally scoped symbol which we need to emit, or - // a reference to another namespace/module which we may need to emit an `import` statement for - const aliasDecl = symbol.declarations && getDeclarationOfAliasSymbol(symbol); - // serialize what the alias points to, preserve the declaration's initializer - const target = aliasDecl && getTargetOfAliasDeclaration(aliasDecl, /*dontRecursivelyResolve*/ true); - // If the target resolves and resolves to a thing defined in this file, emit as an alias, otherwise emit as a const - if (target && length(target.declarations) && some(target.declarations, d => getSourceFileOfNode(d) === getSourceFileOfNode(enclosingDeclaration))) { - // In case `target` refers to a namespace member, look at the declaration and serialize the leftmost symbol in it - // eg, `namespace A { export class B {} }; exports = A.B;` - // Technically, this is all that's required in the case where the assignment is an entity name expression - const expr = isExportAssignment ? getExportAssignmentExpression(aliasDecl as ExportAssignment | BinaryExpression) : getPropertyAssignmentAliasLikeExpression(aliasDecl as ShorthandPropertyAssignment | PropertyAssignment | PropertyAccessExpression); - const first = isEntityNameExpression(expr) ? getFirstNonModuleExportsIdentifier(expr) : undefined; - const referenced = first && resolveEntityName(first, SymbolFlags.All, /*ignoreErrors*/ true, /*dontResolveAlias*/ true, enclosingDeclaration); - if (referenced || target) { - includePrivateSymbol(referenced || target); - } - - // We disable the context's symbol traker for the duration of this name serialization - // as, by virtue of being here, the name is required to print something, and we don't want to - // issue a visibility error on it. Only anonymous classes that an alias points at _would_ issue - // a visibility error here (as they're not visible within any scope), but we want to hoist them - // into the containing scope anyway, so we want to skip the visibility checks. - const oldTrack = context.tracker.trackSymbol; - context.tracker.trackSymbol = noop; - if (isExportAssignment) { - results.push(createExportAssignment( - /*decorators*/ undefined, - /*modifiers*/ undefined, - isExportEquals, - symbolToExpression(target, context, SymbolFlags.All) - )); - } - else { - if (first === expr) { - // serialize as `export {target as name}` - serializeExportSpecifier(name, idText(first)); - } - else if (isClassExpression(expr)) { - serializeExportSpecifier(name, getInternalSymbolName(target, symbolName(target))); - } - else { - // serialize as `import _Ref = t.arg.et; export { _Ref as name }` - const varName = getUnusedName(name, symbol); - addResult(createImportEqualsDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - createIdentifier(varName), - symbolToName(target, context, SymbolFlags.All, /*expectsIdentifier*/ false) - ), ModifierFlags.None); - serializeExportSpecifier(name, varName); - } - } - context.tracker.trackSymbol = oldTrack; - } - else { - // serialize as an anonymous property declaration - const varName = getUnusedName(name, symbol); - // We have to use `getWidenedType` here since the object within a json file is unwidened within the file - // (Unwidened types can only exist in expression contexts and should never be serialized) - const typeToSerialize = getWidenedType(getTypeOfSymbol(symbol)); - if (isTypeRepresentableAsFunctionNamespaceMerge(typeToSerialize, symbol)) { - // If there are no index signatures and `typeToSerialize` is an object type, emit as a namespace instead of a const - serializeAsFunctionNamespaceMerge(typeToSerialize, symbol, varName, isExportAssignment ? ModifierFlags.None : ModifierFlags.Export); - } - else { - const statement = createVariableStatement(/*modifiers*/ undefined, createVariableDeclarationList([ - createVariableDeclaration(varName, serializeTypeForDeclaration(typeToSerialize, symbol)) - ], NodeFlags.Const)); - addResult(statement, name === varName ? ModifierFlags.Export : ModifierFlags.None); - } - if (isExportAssignment) { - results.push(createExportAssignment( - /*decorators*/ undefined, - /*modifiers*/ undefined, - isExportEquals, - createIdentifier(varName) - )); - } - else if (name !== varName) { - serializeExportSpecifier(name, varName); - } - } - } - - function isTypeRepresentableAsFunctionNamespaceMerge(typeToSerialize: Type, hostSymbol: Symbol) { - // Only object types which are not constructable, or indexable, whose members all come from the - // context source file, and whose property names are all valid identifiers and not late-bound, _and_ - // whose input is not type annotated (if the input symbol has an annotation we can reuse, we should prefer it) - const ctxSrc = getSourceFileOfNode(context.enclosingDeclaration); - return getObjectFlags(typeToSerialize) & (ObjectFlags.Anonymous | ObjectFlags.Mapped) && - !getIndexInfoOfType(typeToSerialize, IndexKind.String) && - !getIndexInfoOfType(typeToSerialize, IndexKind.Number) && - !!(length(getPropertiesOfType(typeToSerialize)) || length(getSignaturesOfType(typeToSerialize, SignatureKind.Call))) && - !length(getSignaturesOfType(typeToSerialize, SignatureKind.Construct)) && // TODO: could probably serialize as function + ns + class, now that that's OK - !getDeclarationWithTypeAnnotation(hostSymbol) && - !(typeToSerialize.symbol && some(typeToSerialize.symbol.declarations, d => getSourceFileOfNode(d) !== ctxSrc)) && - !some(getPropertiesOfType(typeToSerialize), p => isLateBoundName(p.escapedName)) && - !some(getPropertiesOfType(typeToSerialize), p => some(p.declarations, d => getSourceFileOfNode(d) !== ctxSrc)) && - every(getPropertiesOfType(typeToSerialize), p => isIdentifierText(symbolName(p), languageVersion) && !isStringAKeyword(symbolName(p))); - } - - function makeSerializePropertySymbol(createProperty: ( - decorators: readonly Decorator[] | undefined, - modifiers: readonly Modifier[] | undefined, - name: string | PropertyName, - questionOrExclamationToken: QuestionToken | undefined, - type: TypeNode | undefined, - initializer: Expression | undefined - ) => T, methodKind: SyntaxKind, useAccessors: true): (p: Symbol, isStatic: boolean, baseType: Type | undefined) => (T | AccessorDeclaration | (T | AccessorDeclaration)[]); - function makeSerializePropertySymbol(createProperty: ( - decorators: readonly Decorator[] | undefined, - modifiers: readonly Modifier[] | undefined, - name: string | PropertyName, - questionOrExclamationToken: QuestionToken | undefined, - type: TypeNode | undefined, - initializer: Expression | undefined - ) => T, methodKind: SyntaxKind, useAccessors: false): (p: Symbol, isStatic: boolean, baseType: Type | undefined) => (T | T[]); - function makeSerializePropertySymbol(createProperty: ( - decorators: readonly Decorator[] | undefined, - modifiers: readonly Modifier[] | undefined, - name: string | PropertyName, - questionOrExclamationToken: QuestionToken | undefined, - type: TypeNode | undefined, - initializer: Expression | undefined - ) => T, methodKind: SyntaxKind, useAccessors: boolean): (p: Symbol, isStatic: boolean, baseType: Type | undefined) => (T | AccessorDeclaration | (T | AccessorDeclaration)[]) { - return function serializePropertySymbol(p: Symbol, isStatic: boolean, baseType: Type | undefined) { - const modifierFlags = getDeclarationModifierFlagsFromSymbol(p); - const isPrivate = !!(modifierFlags & ModifierFlags.Private); - if (isStatic && (p.flags & (SymbolFlags.Type | SymbolFlags.Namespace | SymbolFlags.Alias))) { - // Only value-only-meaning symbols can be correctly encoded as class statics, type/namespace/alias meaning symbols - // need to be merged namespace members - return []; - } - if (p.flags & SymbolFlags.Prototype || (baseType && getPropertyOfType(baseType, p.escapedName) - && isReadonlySymbol(getPropertyOfType(baseType, p.escapedName)!) === isReadonlySymbol(p) - && (p.flags & SymbolFlags.Optional) === (getPropertyOfType(baseType, p.escapedName)!.flags & SymbolFlags.Optional) - && isTypeIdenticalTo(getTypeOfSymbol(p), getTypeOfPropertyOfType(baseType, p.escapedName)!))) { - return []; - } - const flag = (modifierFlags & ~ModifierFlags.Async) | (isStatic ? ModifierFlags.Static : 0); - const name = getPropertyNameNodeForSymbol(p, context); - const firstPropertyLikeDecl = find(p.declarations, or(isPropertyDeclaration, isAccessor, isVariableDeclaration, isPropertySignature, isBinaryExpression, isPropertyAccessExpression)); - if (p.flags & SymbolFlags.Accessor && useAccessors) { - const result: AccessorDeclaration[] = []; - if (p.flags & SymbolFlags.SetAccessor) { - result.push(setTextRange(createSetAccessor( - /*decorators*/ undefined, - createModifiersFromModifierFlags(flag), - name, - [createParameter( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, - "arg", - /*questionToken*/ undefined, - isPrivate ? undefined : serializeTypeForDeclaration(getTypeOfSymbol(p), p) - )], - /*body*/ undefined - ), find(p.declarations, isSetAccessor) || firstPropertyLikeDecl)); - } - if (p.flags & SymbolFlags.GetAccessor) { - const isPrivate = modifierFlags & ModifierFlags.Private; - result.push(setTextRange(createGetAccessor( - /*decorators*/ undefined, - createModifiersFromModifierFlags(flag), - name, - [], - isPrivate ? undefined : serializeTypeForDeclaration(getTypeOfSymbol(p), p), - /*body*/ undefined - ), find(p.declarations, isGetAccessor) || firstPropertyLikeDecl)); - } - return result; - } - // This is an else/if as accessors and properties can't merge in TS, but might in JS - // If this happens, we assume the accessor takes priority, as it imposes more constraints - else if (p.flags & (SymbolFlags.Property | SymbolFlags.Variable)) { - return setTextRange(createProperty( - /*decorators*/ undefined, - createModifiersFromModifierFlags((isReadonlySymbol(p) ? ModifierFlags.Readonly : 0) | flag), - name, - p.flags & SymbolFlags.Optional ? createToken(SyntaxKind.QuestionToken) : undefined, - isPrivate ? undefined : serializeTypeForDeclaration(getTypeOfSymbol(p), p), - // TODO: https://github.com/microsoft/TypeScript/pull/32372#discussion_r328386357 - // interface members can't have initializers, however class members _can_ - /*initializer*/ undefined - ), find(p.declarations, or(isPropertyDeclaration, isVariableDeclaration)) || firstPropertyLikeDecl); - } - if (p.flags & (SymbolFlags.Method | SymbolFlags.Function)) { - const type = getTypeOfSymbol(p); - const signatures = getSignaturesOfType(type, SignatureKind.Call); - if (flag & ModifierFlags.Private) { - return setTextRange(createProperty( - /*decorators*/ undefined, - createModifiersFromModifierFlags((isReadonlySymbol(p) ? ModifierFlags.Readonly : 0) | flag), - name, - p.flags & SymbolFlags.Optional ? createToken(SyntaxKind.QuestionToken) : undefined, - /*type*/ undefined, - /*initializer*/ undefined - ), find(p.declarations, isFunctionLikeDeclaration) || signatures[0] && signatures[0].declaration || p.declarations[0]); - } - - const results = []; - for (const sig of signatures) { - // Each overload becomes a separate method declaration, in order - const decl = signatureToSignatureDeclarationHelper(sig, methodKind, context) as MethodDeclaration; - decl.name = name; // TODO: Clone - if (flag) { - decl.modifiers = createNodeArray(createModifiersFromModifierFlags(flag)); - } - if (p.flags & SymbolFlags.Optional) { - decl.questionToken = createToken(SyntaxKind.QuestionToken); - } - results.push(setTextRange(decl, sig.declaration)); - } - return results as unknown as T[]; - } - // The `Constructor`'s symbol isn't in the class's properties lists, obviously, since it's a signature on the static - return Debug.fail(`Unhandled class member kind! ${(p as any).__debugFlags || p.flags}`); - }; - } - - function serializePropertySymbolForInterface(p: Symbol, baseType: Type | undefined) { - return serializePropertySymbolForInterfaceWorker(p, /*isStatic*/ false, baseType); - } - - function getDeclarationWithTypeAnnotation(symbol: Symbol) { - return symbol.declarations && find(symbol.declarations, s => !!getEffectiveTypeAnnotationNode(s) && !!findAncestor(s, n => n === enclosingDeclaration)); - } - - /** - * Unlike `typeToTypeNodeHelper`, this handles setting up the `AllowUniqueESSymbolType` flag - * so a `unique symbol` is returned when appropriate for the input symbol, rather than `typeof sym` - */ - function serializeTypeForDeclaration(type: Type, symbol: Symbol) { - const declWithExistingAnnotation = getDeclarationWithTypeAnnotation(symbol); - if (declWithExistingAnnotation && !isFunctionLikeDeclaration(declWithExistingAnnotation)) { - // try to reuse the existing annotation - const existing = getEffectiveTypeAnnotationNode(declWithExistingAnnotation)!; - const transformed = visitNode(existing, visitExistingNodeTreeSymbols); - return transformed === existing ? getMutableClone(existing) : transformed; - } - const oldFlags = context.flags; - if (type.flags & TypeFlags.UniqueESSymbol && - type.symbol === symbol) { - context.flags |= NodeBuilderFlags.AllowUniqueESSymbolType; - } - const result = typeToTypeNodeHelper(type, context); - context.flags = oldFlags; - return result; - - function visitExistingNodeTreeSymbols(node: T): Node { - if (isJSDocAllType(node)) { - return createKeywordTypeNode(SyntaxKind.AnyKeyword); - } - if (isJSDocUnknownType(node)) { - return createKeywordTypeNode(SyntaxKind.UnknownKeyword); - } - if (isJSDocNullableType(node)) { - return createUnionTypeNode([visitNode(node.type, visitExistingNodeTreeSymbols), createKeywordTypeNode(SyntaxKind.NullKeyword)]); - } - if (isJSDocOptionalType(node)) { - return createUnionTypeNode([visitNode(node.type, visitExistingNodeTreeSymbols), createKeywordTypeNode(SyntaxKind.UndefinedKeyword)]); - } - if (isJSDocNonNullableType(node)) { - return visitNode(node.type, visitExistingNodeTreeSymbols); - } - if ((isExpressionWithTypeArguments(node) || isTypeReferenceNode(node)) && isJSDocIndexSignature(node)) { - return createTypeLiteralNode([createIndexSignature( - /*decorators*/ undefined, - /*modifiers*/ undefined, - [createParameter( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotdotdotToken*/ undefined, - "x", - /*questionToken*/ undefined, - visitNode(node.typeArguments![0], visitExistingNodeTreeSymbols) - )], - visitNode(node.typeArguments![1], visitExistingNodeTreeSymbols) - )]); - } - if (isJSDocFunctionType(node)) { - if (isJSDocConstructSignature(node)) { - let newTypeNode: TypeNode | undefined; - return createConstructorTypeNode( - visitNodes(node.typeParameters, visitExistingNodeTreeSymbols), - mapDefined(node.parameters, (p, i) => p.name && isIdentifier(p.name) && p.name.escapedText === "new" ? (newTypeNode = p.type, undefined) : createParameter( - /*decorators*/ undefined, - /*modifiers*/ undefined, - p.dotDotDotToken, - p.name || p.dotDotDotToken ? `args` : `arg${i}`, - p.questionToken, - visitNode(p.type, visitExistingNodeTreeSymbols), - /*initializer*/ undefined - )), - visitNode(newTypeNode || node.type, visitExistingNodeTreeSymbols) - ); - } - else { - return createFunctionTypeNode( - visitNodes(node.typeParameters, visitExistingNodeTreeSymbols), - map(node.parameters, (p, i) => createParameter( - /*decorators*/ undefined, - /*modifiers*/ undefined, - p.dotDotDotToken, - p.name || p.dotDotDotToken ? `args` : `arg${i}`, - p.questionToken, - visitNode(p.type, visitExistingNodeTreeSymbols), - /*initializer*/ undefined - )), - visitNode(node.type, visitExistingNodeTreeSymbols) - ); - } - } - if (isLiteralImportTypeNode(node)) { - return updateImportTypeNode( - node, - updateLiteralTypeNode(node.argument, rewriteModuleSpecifier(node, node.argument.literal)), - node.qualifier, - visitNodes(node.typeArguments, visitExistingNodeTreeSymbols, isTypeNode), - node.isTypeOf - ); - } - - if (isEntityName(node) || isEntityNameExpression(node)) { - const leftmost = getFirstIdentifier(node); - const sym = resolveEntityName(leftmost, SymbolFlags.All, /*ignoreErrors*/ true, /*dontResolveALias*/ true); - if (sym) { - includePrivateSymbol(sym); - if (isIdentifier(node) && sym.flags & SymbolFlags.TypeParameter) { - const name = typeParameterToName(getDeclaredTypeOfSymbol(sym), context); - if (idText(name) !== idText(node)) { - return name; - } - return node; - } - } - } - - return visitEachChild(node, visitExistingNodeTreeSymbols, nullTransformationContext); - } - - function rewriteModuleSpecifier(parent: ImportTypeNode, lit: StringLiteral) { - if (bundled) { - if (context.tracker && context.tracker.moduleResolverHost) { - const targetFile = getExternalModuleFileFromDeclaration(parent); - if (targetFile) { - const getCanonicalFileName = createGetCanonicalFileName(!!host.useCaseSensitiveFileNames); - const resolverHost = { - getCanonicalFileName, - getCurrentDirectory: context.tracker.moduleResolverHost.getCurrentDirectory ? () => context.tracker.moduleResolverHost!.getCurrentDirectory!() : () => "", - getCommonSourceDirectory: () => context.tracker.moduleResolverHost!.getCommonSourceDirectory() - }; - const newName = getResolvedExternalModuleName(resolverHost, targetFile); - return createLiteral(newName); - } - } - } - else { - if (context.tracker && context.tracker.trackExternalModuleSymbolOfImportTypeNode) { - const moduleSym = resolveExternalModuleNameWorker(lit, lit, /*moduleNotFoundError*/ undefined); - if (moduleSym) { - context.tracker.trackExternalModuleSymbolOfImportTypeNode(moduleSym); - } - } - } - return lit; - } - } - - function serializeSignatures(kind: SignatureKind, input: Type, baseType: Type | undefined, outputKind: SyntaxKind) { - const signatures = getSignaturesOfType(input, kind); - if (kind === SignatureKind.Construct) { - if (!baseType && every(signatures, s => length(s.parameters) === 0)) { - return []; // No base type, every constructor is empty - elide the extraneous `constructor()` - } - if (baseType) { - // If there is a base type, if every signature in the class is identical to a signature in the baseType, elide all the declarations - const baseSigs = getSignaturesOfType(baseType, SignatureKind.Construct); - if (!length(baseSigs) && every(signatures, s => length(s.parameters) === 0)) { - return []; // Base had no explicit signatures, if all our signatures are also implicit, return an empty list - } - if (baseSigs.length === signatures.length) { - let failed = false; - for (let i = 0; i < baseSigs.length; i++) { - if (!compareSignaturesIdentical(signatures[i], baseSigs[i], /*partialMatch*/ false, /*ignoreThisTypes*/ false, /*ignoreReturnTypes*/ true, compareTypesIdentical)) { - failed = true; - break; - } - } - if (!failed) { - return []; // Every signature was identical - elide constructor list as it is inherited - } - } - } - let privateProtected: ModifierFlags = 0; - for (const s of signatures) { - if (s.declaration) { - privateProtected |= getSelectedModifierFlags(s.declaration, ModifierFlags.Private | ModifierFlags.Protected); - } - } - if (privateProtected) { - return [setTextRange(createConstructor( - /*decorators*/ undefined, - createModifiersFromModifierFlags(privateProtected), - /*parameters*/ [], - /*body*/ undefined, - ), signatures[0].declaration)]; - } - } - - const results = []; - for (const sig of signatures) { - // Each overload becomes a separate constructor declaration, in order - const decl = signatureToSignatureDeclarationHelper(sig, outputKind, context); - results.push(setTextRange(decl, sig.declaration)); - } - return results; - } - - function serializeIndexSignatures(input: Type, baseType: Type | undefined) { - const results: IndexSignatureDeclaration[] = []; - for (const type of [IndexKind.String, IndexKind.Number]) { - const info = getIndexInfoOfType(input, type); - if (info) { - if (baseType) { - const baseInfo = getIndexInfoOfType(baseType, type); - if (baseInfo) { - if (isTypeIdenticalTo(info.type, baseInfo.type)) { - continue; // elide identical index signatures - } - } - } - results.push(indexInfoToIndexSignatureDeclarationHelper(info, type, context)); - } - } - return results; - } - - function serializeBaseType(t: Type, staticType: Type, rootName: string) { - const ref = trySerializeAsTypeReference(t); - if (ref) { - return ref; - } - const tempName = getUnusedName(`${rootName}_base`); - const statement = createVariableStatement(/*modifiers*/ undefined, createVariableDeclarationList([ - createVariableDeclaration(tempName, typeToTypeNodeHelper(staticType, context)) - ], NodeFlags.Const)); - addResult(statement, ModifierFlags.None); - return createExpressionWithTypeArguments(/*typeArgs*/ undefined, createIdentifier(tempName)); - } - - function trySerializeAsTypeReference(t: Type) { - let typeArgs: TypeNode[] | undefined; - let reference: Expression | undefined; - // We don't use `isValueSymbolAccessible` below. since that considers alternative containers (like modules) - // which we can't write out in a syntactically valid way as an expression - if ((t as TypeReference).target && getAccessibleSymbolChain((t as TypeReference).target.symbol, enclosingDeclaration, SymbolFlags.Value, /*useOnlyExternalAliasing*/ false)) { - typeArgs = map(getTypeArguments(t as TypeReference), t => typeToTypeNodeHelper(t, context)); - reference = symbolToExpression((t as TypeReference).target.symbol, context, SymbolFlags.Type); - } - else if (t.symbol && getAccessibleSymbolChain(t.symbol, enclosingDeclaration, SymbolFlags.Value, /*useOnlyExternalAliasing*/ false)) { - reference = symbolToExpression(t.symbol, context, SymbolFlags.Type); - } - if (reference) { - return createExpressionWithTypeArguments(typeArgs, reference); - } - } - - function getUnusedName(input: string, symbol?: Symbol): string { - if (symbol) { - if (context.remappedSymbolNames!.has("" + getSymbolId(symbol))) { - return context.remappedSymbolNames!.get("" + getSymbolId(symbol))!; - } - } - if (symbol) { - input = getNameCandidateWorker(symbol, input); - } - let i = 0; - const original = input; - while (context.usedSymbolNames!.has(input)) { - i++; - input = `${original}_${i}`; - } - context.usedSymbolNames!.set(input, true); - if (symbol) { - context.remappedSymbolNames!.set("" + getSymbolId(symbol), input); - } - return input; - } - - function getNameCandidateWorker(symbol: Symbol, localName: string) { - if (localName === InternalSymbolName.Default || localName === InternalSymbolName.Class || localName === InternalSymbolName.Function) { - const flags = context.flags; - context.flags |= NodeBuilderFlags.InInitialEntityName; - const nameCandidate = getNameOfSymbolAsWritten(symbol, context); - context.flags = flags; - localName = nameCandidate.length > 0 && isSingleOrDoubleQuote(nameCandidate.charCodeAt(0)) ? stripQuotes(nameCandidate) : nameCandidate; - } - if (localName === InternalSymbolName.Default) { - localName = "_default"; - } - else if (localName === InternalSymbolName.ExportEquals) { - localName = "_exports"; - } - localName = isIdentifierText(localName, languageVersion) && !isStringANonContextualKeyword(localName) ? localName : "_" + localName.replace(/[^a-zA-Z0-9]/g, "_"); - return localName; - } - - function getInternalSymbolName(symbol: Symbol, localName: string) { - if (context.remappedSymbolNames!.has("" + getSymbolId(symbol))) { - return context.remappedSymbolNames!.get("" + getSymbolId(symbol))!; - } - localName = getNameCandidateWorker(symbol, localName); - // The result of this is going to be used as the symbol's name - lock it in, so `getUnusedName` will also pick it up - context.remappedSymbolNames!.set("" + getSymbolId(symbol), localName); - return localName; - } - } - } - - function typePredicateToString(typePredicate: TypePredicate, enclosingDeclaration?: Node, flags: TypeFormatFlags = TypeFormatFlags.UseAliasDefinedOutsideCurrentScope, writer?: EmitTextWriter): string { - return writer ? typePredicateToStringWorker(writer).getText() : usingSingleLineStringWriter(typePredicateToStringWorker); - - function typePredicateToStringWorker(writer: EmitTextWriter) { - const predicate = createTypePredicateNodeWithModifier( - typePredicate.kind === TypePredicateKind.AssertsThis || typePredicate.kind === TypePredicateKind.AssertsIdentifier ? createToken(SyntaxKind.AssertsKeyword) : undefined, - typePredicate.kind === TypePredicateKind.Identifier || typePredicate.kind === TypePredicateKind.AssertsIdentifier ? createIdentifier(typePredicate.parameterName) : createThisTypeNode(), - typePredicate.type && nodeBuilder.typeToTypeNode(typePredicate.type, enclosingDeclaration, toNodeBuilderFlags(flags) | NodeBuilderFlags.IgnoreErrors | NodeBuilderFlags.WriteTypeParametersInQualifiedName)! // TODO: GH#18217 - ); - const printer = createPrinter({ removeComments: true }); - const sourceFile = enclosingDeclaration && getSourceFileOfNode(enclosingDeclaration); - printer.writeNode(EmitHint.Unspecified, predicate, /*sourceFile*/ sourceFile, writer); - return writer; - } - } - - function formatUnionTypes(types: readonly Type[]): Type[] { - const result: Type[] = []; - let flags: TypeFlags = 0; - for (let i = 0; i < types.length; i++) { - const t = types[i]; - flags |= t.flags; - if (!(t.flags & TypeFlags.Nullable)) { - if (t.flags & (TypeFlags.BooleanLiteral | TypeFlags.EnumLiteral)) { - const baseType = t.flags & TypeFlags.BooleanLiteral ? booleanType : getBaseTypeOfEnumLiteralType(t); - if (baseType.flags & TypeFlags.Union) { - const count = (baseType).types.length; - if (i + count <= types.length && getRegularTypeOfLiteralType(types[i + count - 1]) === getRegularTypeOfLiteralType((baseType).types[count - 1])) { - result.push(baseType); - i += count - 1; - continue; - } - } - } - result.push(t); - } - } - if (flags & TypeFlags.Null) result.push(nullType); - if (flags & TypeFlags.Undefined) result.push(undefinedType); - return result || types; - } - - function visibilityToString(flags: ModifierFlags): string | undefined { - if (flags === ModifierFlags.Private) { - return "private"; - } - if (flags === ModifierFlags.Protected) { - return "protected"; - } - return "public"; - } - - function getTypeAliasForTypeLiteral(type: Type): Symbol | undefined { - if (type.symbol && type.symbol.flags & SymbolFlags.TypeLiteral) { - const node = findAncestor(type.symbol.declarations[0].parent, n => n.kind !== SyntaxKind.ParenthesizedType)!; - if (node.kind === SyntaxKind.TypeAliasDeclaration) { - return getSymbolOfNode(node); - } - } - return undefined; - } - - function isTopLevelInExternalModuleAugmentation(node: Node): boolean { - return node && node.parent && - node.parent.kind === SyntaxKind.ModuleBlock && - isExternalModuleAugmentation(node.parent.parent); - } - - interface NodeBuilderContext { - enclosingDeclaration: Node | undefined; - flags: NodeBuilderFlags; - tracker: SymbolTracker; - - // State - encounteredError: boolean; - visitedTypes: Map | undefined; - symbolDepth: Map | undefined; - inferTypeParameters: TypeParameter[] | undefined; - approximateLength: number; - truncating?: boolean; - typeParameterSymbolList?: Map; - typeParameterNames?: Map; - typeParameterNamesByText?: Map; - usedSymbolNames?: Map; - remappedSymbolNames?: Map; - } - - function isDefaultBindingContext(location: Node) { - return location.kind === SyntaxKind.SourceFile || isAmbientModule(location); - } - - function getNameOfSymbolFromNameType(symbol: Symbol, context?: NodeBuilderContext) { - const nameType = getSymbolLinks(symbol).nameType; - if (nameType) { - if (nameType.flags & TypeFlags.StringOrNumberLiteral) { - const name = "" + (nameType).value; - if (!isIdentifierText(name, compilerOptions.target) && !isNumericLiteralName(name)) { - return `"${escapeString(name, CharacterCodes.doubleQuote)}"`; - } - if (isNumericLiteralName(name) && startsWith(name, "-")) { - return `[${name}]`; - } - return name; - } - if (nameType.flags & TypeFlags.UniqueESSymbol) { - return `[${getNameOfSymbolAsWritten((nameType).symbol, context)}]`; - } - } - } - - /** - * Gets a human-readable name for a symbol. - * Should *not* be used for the right-hand side of a `.` -- use `symbolName(symbol)` for that instead. - * - * Unlike `symbolName(symbol)`, this will include quotes if the name is from a string literal. - * It will also use a representation of a number as written instead of a decimal form, e.g. `0o11` instead of `9`. - */ - function getNameOfSymbolAsWritten(symbol: Symbol, context?: NodeBuilderContext): string { - if (context && symbol.escapedName === InternalSymbolName.Default && !(context.flags & NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope) && - // If it's not the first part of an entity name, it must print as `default` - (!(context.flags & NodeBuilderFlags.InInitialEntityName) || - // if the symbol is synthesized, it will only be referenced externally it must print as `default` - !symbol.declarations || - // if not in the same binding context (source file, module declaration), it must print as `default` - (context.enclosingDeclaration && findAncestor(symbol.declarations[0], isDefaultBindingContext) !== findAncestor(context.enclosingDeclaration, isDefaultBindingContext)))) { - return "default"; - } - if (symbol.declarations && symbol.declarations.length) { - let declaration = firstDefined(symbol.declarations, d => getNameOfDeclaration(d) ? d : undefined); // Try using a declaration with a name, first - const name = declaration && getNameOfDeclaration(declaration); - if (declaration && name) { - if (isCallExpression(declaration) && isBindableObjectDefinePropertyCall(declaration)) { - return symbolName(symbol); - } - if (isComputedPropertyName(name) && !(getCheckFlags(symbol) & CheckFlags.Late)) { - const nameType = getSymbolLinks(symbol).nameType; - if (nameType && nameType.flags & TypeFlags.StringOrNumberLiteral) { - // Computed property name isn't late bound, but has a well-known name type - use name type to generate a symbol name - const result = getNameOfSymbolFromNameType(symbol, context); - if (result !== undefined) { - return result; - } - } - } - return declarationNameToString(name); - } - if (!declaration) { - declaration = symbol.declarations[0]; // Declaration may be nameless, but we'll try anyway - } - if (declaration.parent && declaration.parent.kind === SyntaxKind.VariableDeclaration) { - return declarationNameToString((declaration.parent).name); - } - switch (declaration.kind) { - case SyntaxKind.ClassExpression: - case SyntaxKind.FunctionExpression: - case SyntaxKind.ArrowFunction: - if (context && !context.encounteredError && !(context.flags & NodeBuilderFlags.AllowAnonymousIdentifier)) { - context.encounteredError = true; - } - return declaration.kind === SyntaxKind.ClassExpression ? "(Anonymous class)" : "(Anonymous function)"; - } - } - const name = getNameOfSymbolFromNameType(symbol, context); - return name !== undefined ? name : symbolName(symbol); - } - - function isDeclarationVisible(node: Node): boolean { - if (node) { - const links = getNodeLinks(node); - if (links.isVisible === undefined) { - links.isVisible = !!determineIfDeclarationIsVisible(); - } - return links.isVisible; - } - - return false; - - function determineIfDeclarationIsVisible() { - switch (node.kind) { - case SyntaxKind.JSDocCallbackTag: - case SyntaxKind.JSDocTypedefTag: - case SyntaxKind.JSDocEnumTag: - // Top-level jsdoc type aliases are considered exported - // First parent is comment node, second is hosting declaration or token; we only care about those tokens or declarations whose parent is a source file - return !!(node.parent && node.parent.parent && node.parent.parent.parent && isSourceFile(node.parent.parent.parent)); - case SyntaxKind.BindingElement: - return isDeclarationVisible(node.parent.parent); - case SyntaxKind.VariableDeclaration: - if (isBindingPattern((node as VariableDeclaration).name) && - !((node as VariableDeclaration).name as BindingPattern).elements.length) { - // If the binding pattern is empty, this variable declaration is not visible - return false; - } - // falls through - case SyntaxKind.ModuleDeclaration: - case SyntaxKind.ClassDeclaration: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.ImportEqualsDeclaration: - // external module augmentation is always visible - if (isExternalModuleAugmentation(node)) { - return true; - } - const parent = getDeclarationContainer(node); - // If the node is not exported or it is not ambient module element (except import declaration) - if (!(getCombinedModifierFlags(node as Declaration) & ModifierFlags.Export) && - !(node.kind !== SyntaxKind.ImportEqualsDeclaration && parent.kind !== SyntaxKind.SourceFile && parent.flags & NodeFlags.Ambient)) { - return isGlobalSourceFile(parent); - } - // Exported members/ambient module elements (exception import declaration) are visible if parent is visible - return isDeclarationVisible(parent); - - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.PropertySignature: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.MethodSignature: - if (hasModifier(node, ModifierFlags.Private | ModifierFlags.Protected)) { - // Private/protected properties/methods are not visible - return false; - } - // Public properties/methods are visible if its parents are visible, so: - // falls through - - case SyntaxKind.Constructor: - case SyntaxKind.ConstructSignature: - case SyntaxKind.CallSignature: - case SyntaxKind.IndexSignature: - case SyntaxKind.Parameter: - case SyntaxKind.ModuleBlock: - case SyntaxKind.FunctionType: - case SyntaxKind.ConstructorType: - case SyntaxKind.TypeLiteral: - case SyntaxKind.TypeReference: - case SyntaxKind.ArrayType: - case SyntaxKind.TupleType: - case SyntaxKind.UnionType: - case SyntaxKind.IntersectionType: - case SyntaxKind.ParenthesizedType: - return isDeclarationVisible(node.parent); - - // Default binding, import specifier and namespace import is visible - // only on demand so by default it is not visible - case SyntaxKind.ImportClause: - case SyntaxKind.NamespaceImport: - case SyntaxKind.ImportSpecifier: - return false; - - // Type parameters are always visible - case SyntaxKind.TypeParameter: - - // Source file and namespace export are always visible - // falls through - case SyntaxKind.SourceFile: - case SyntaxKind.NamespaceExportDeclaration: - return true; - - // Export assignments do not create name bindings outside the module - case SyntaxKind.ExportAssignment: - return false; - - default: - return false; - } - } - } - - function collectLinkedAliases(node: Identifier, setVisibility?: boolean): Node[] | undefined { - let exportSymbol: Symbol | undefined; - if (node.parent && node.parent.kind === SyntaxKind.ExportAssignment) { - exportSymbol = resolveName(node, node.escapedText, SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace | SymbolFlags.Alias, /*nameNotFoundMessage*/ undefined, node, /*isUse*/ false); - } - else if (node.parent.kind === SyntaxKind.ExportSpecifier) { - exportSymbol = getTargetOfExportSpecifier(node.parent, SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace | SymbolFlags.Alias); - } - let result: Node[] | undefined; - let visited: Map | undefined; - if (exportSymbol) { - visited = createMap(); - visited.set("" + getSymbolId(exportSymbol), true); - buildVisibleNodeList(exportSymbol.declarations); - } - return result; - - function buildVisibleNodeList(declarations: Declaration[]) { - forEach(declarations, declaration => { - const resultNode = getAnyImportSyntax(declaration) || declaration; - if (setVisibility) { - getNodeLinks(declaration).isVisible = true; - } - else { - result = result || []; - pushIfUnique(result, resultNode); - } - - if (isInternalModuleImportEqualsDeclaration(declaration)) { - // Add the referenced top container visible - const internalModuleReference = declaration.moduleReference; - const firstIdentifier = getFirstIdentifier(internalModuleReference); - const importSymbol = resolveName(declaration, firstIdentifier.escapedText, SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace, - undefined, undefined, /*isUse*/ false); - const id = importSymbol && "" + getSymbolId(importSymbol); - if (importSymbol && !visited!.has(id!)) { - visited!.set(id!, true); - buildVisibleNodeList(importSymbol.declarations); - } - } - }); - } - } - - /** - * Push an entry on the type resolution stack. If an entry with the given target and the given property name - * is already on the stack, and no entries in between already have a type, then a circularity has occurred. - * In this case, the result values of the existing entry and all entries pushed after it are changed to false, - * and the value false is returned. Otherwise, the new entry is just pushed onto the stack, and true is returned. - * In order to see if the same query has already been done before, the target object and the propertyName both - * must match the one passed in. - * - * @param target The symbol, type, or signature whose type is being queried - * @param propertyName The property name that should be used to query the target for its type - */ - function pushTypeResolution(target: TypeSystemEntity, propertyName: TypeSystemPropertyName): boolean { - const resolutionCycleStartIndex = findResolutionCycleStartIndex(target, propertyName); - if (resolutionCycleStartIndex >= 0) { - // A cycle was found - const { length } = resolutionTargets; - for (let i = resolutionCycleStartIndex; i < length; i++) { - resolutionResults[i] = false; - } - return false; - } - resolutionTargets.push(target); - resolutionResults.push(/*items*/ true); - resolutionPropertyNames.push(propertyName); - return true; - } - - function findResolutionCycleStartIndex(target: TypeSystemEntity, propertyName: TypeSystemPropertyName): number { - for (let i = resolutionTargets.length - 1; i >= 0; i--) { - if (hasType(resolutionTargets[i], resolutionPropertyNames[i])) { - return -1; - } - if (resolutionTargets[i] === target && resolutionPropertyNames[i] === propertyName) { - return i; - } - } - return -1; - } - - function hasType(target: TypeSystemEntity, propertyName: TypeSystemPropertyName): boolean { - switch (propertyName) { - case TypeSystemPropertyName.Type: - return !!getSymbolLinks(target).type; - case TypeSystemPropertyName.EnumTagType: - return !!(getNodeLinks(target as JSDocEnumTag).resolvedEnumType); - case TypeSystemPropertyName.DeclaredType: - return !!getSymbolLinks(target).declaredType; - case TypeSystemPropertyName.ResolvedBaseConstructorType: - return !!(target).resolvedBaseConstructorType; - case TypeSystemPropertyName.ResolvedReturnType: - return !!(target).resolvedReturnType; - case TypeSystemPropertyName.ImmediateBaseConstraint: - return !!(target).immediateBaseConstraint; - case TypeSystemPropertyName.ResolvedTypeArguments: - return !!(target as TypeReference).resolvedTypeArguments; - } - return Debug.assertNever(propertyName); - } - - /** - * Pop an entry from the type resolution stack and return its associated result value. The result value will - * be true if no circularities were detected, or false if a circularity was found. - */ - function popTypeResolution(): boolean { - resolutionTargets.pop(); - resolutionPropertyNames.pop(); - return resolutionResults.pop()!; - } - - function getDeclarationContainer(node: Node): Node { - return findAncestor(getRootDeclaration(node), node => { - switch (node.kind) { - case SyntaxKind.VariableDeclaration: - case SyntaxKind.VariableDeclarationList: - case SyntaxKind.ImportSpecifier: - case SyntaxKind.NamedImports: - case SyntaxKind.NamespaceImport: - case SyntaxKind.ImportClause: - return false; - default: - return true; - } - })!.parent; - } - - function getTypeOfPrototypeProperty(prototype: Symbol): Type { - // TypeScript 1.0 spec (April 2014): 8.4 - // Every class automatically contains a static property member named 'prototype', - // the type of which is an instantiation of the class type with type Any supplied as a type argument for each type parameter. - // It is an error to explicitly declare a static property member with the name 'prototype'. - const classType = getDeclaredTypeOfSymbol(getParentOfSymbol(prototype)!); - return classType.typeParameters ? createTypeReference(classType, map(classType.typeParameters, _ => anyType)) : classType; - } - - // Return the type of the given property in the given type, or undefined if no such property exists - function getTypeOfPropertyOfType(type: Type, name: __String): Type | undefined { - const prop = getPropertyOfType(type, name); - return prop ? getTypeOfSymbol(prop) : undefined; - } - - function getTypeOfPropertyOrIndexSignature(type: Type, name: __String): Type { - return getTypeOfPropertyOfType(type, name) || isNumericLiteralName(name) && getIndexTypeOfType(type, IndexKind.Number) || getIndexTypeOfType(type, IndexKind.String) || unknownType; - } - - function isTypeAny(type: Type | undefined) { - return type && (type.flags & TypeFlags.Any) !== 0; - } - - // Return the type of a binding element parent. We check SymbolLinks first to see if a type has been - // assigned by contextual typing. - function getTypeForBindingElementParent(node: BindingElementGrandparent) { - const symbol = getSymbolOfNode(node); - return symbol && getSymbolLinks(symbol).type || getTypeForVariableLikeDeclaration(node, /*includeOptionality*/ false); - } - - function isComputedNonLiteralName(name: PropertyName): boolean { - return name.kind === SyntaxKind.ComputedPropertyName && !isStringOrNumericLiteralLike(name.expression); - } - - function getRestType(source: Type, properties: PropertyName[], symbol: Symbol | undefined): Type { - source = filterType(source, t => !(t.flags & TypeFlags.Nullable)); - if (source.flags & TypeFlags.Never) { - return emptyObjectType; - } - if (source.flags & TypeFlags.Union) { - return mapType(source, t => getRestType(t, properties, symbol)); - } - const omitKeyType = getUnionType(map(properties, getLiteralTypeFromPropertyName)); - if (isGenericObjectType(source) || isGenericIndexType(omitKeyType)) { - if (omitKeyType.flags & TypeFlags.Never) { - return source; - } - - const omitTypeAlias = getGlobalOmitSymbol(); - if (!omitTypeAlias) { - return errorType; - } - return getTypeAliasInstantiation(omitTypeAlias, [source, omitKeyType]); - } - const members = createSymbolTable(); - for (const prop of getPropertiesOfType(source)) { - if (!isTypeAssignableTo(getLiteralTypeFromProperty(prop, TypeFlags.StringOrNumberLiteralOrUnique), omitKeyType) - && !(getDeclarationModifierFlagsFromSymbol(prop) & (ModifierFlags.Private | ModifierFlags.Protected)) - && isSpreadableProperty(prop)) { - members.set(prop.escapedName, getSpreadSymbol(prop, /*readonly*/ false)); - } - } - const stringIndexInfo = getIndexInfoOfType(source, IndexKind.String); - const numberIndexInfo = getIndexInfoOfType(source, IndexKind.Number); - const result = createAnonymousType(symbol, members, emptyArray, emptyArray, stringIndexInfo, numberIndexInfo); - result.objectFlags |= ObjectFlags.ObjectRestType; - return result; - } - - // Determine the control flow type associated with a destructuring declaration or assignment. The following - // forms of destructuring are possible: - // let { x } = obj; // BindingElement - // let [ x ] = obj; // BindingElement - // { x } = obj; // ShorthandPropertyAssignment - // { x: v } = obj; // PropertyAssignment - // [ x ] = obj; // Expression - // We construct a synthetic element access expression corresponding to 'obj.x' such that the control - // flow analyzer doesn't have to handle all the different syntactic forms. - function getFlowTypeOfDestructuring(node: BindingElement | PropertyAssignment | ShorthandPropertyAssignment | Expression, declaredType: Type) { - const reference = getSyntheticElementAccess(node); - return reference ? getFlowTypeOfReference(reference, declaredType) : declaredType; - } - - function getSyntheticElementAccess(node: BindingElement | PropertyAssignment | ShorthandPropertyAssignment | Expression): ElementAccessExpression | undefined { - const parentAccess = getParentElementAccess(node); - if (parentAccess && parentAccess.flowNode) { - const propName = getDestructuringPropertyName(node); - if (propName) { - const result = createNode(SyntaxKind.ElementAccessExpression, node.pos, node.end); - result.parent = node; - result.expression = parentAccess; - const literal = createNode(SyntaxKind.StringLiteral, node.pos, node.end); - literal.parent = result; - literal.text = propName; - result.argumentExpression = literal; - result.flowNode = parentAccess.flowNode; - return result; - } - } - } - - function getParentElementAccess(node: BindingElement | PropertyAssignment | ShorthandPropertyAssignment | Expression) { - const ancestor = node.parent.parent; - switch (ancestor.kind) { - case SyntaxKind.BindingElement: - case SyntaxKind.PropertyAssignment: - return getSyntheticElementAccess(ancestor); - case SyntaxKind.ArrayLiteralExpression: - return getSyntheticElementAccess(node.parent); - case SyntaxKind.VariableDeclaration: - return (ancestor).initializer; - case SyntaxKind.BinaryExpression: - return (ancestor).right; - } - } - - function getDestructuringPropertyName(node: BindingElement | PropertyAssignment | ShorthandPropertyAssignment | Expression) { - const parent = node.parent; - if (node.kind === SyntaxKind.BindingElement && parent.kind === SyntaxKind.ObjectBindingPattern) { - return getLiteralPropertyNameText((node).propertyName || (node).name); - } - if (node.kind === SyntaxKind.PropertyAssignment || node.kind === SyntaxKind.ShorthandPropertyAssignment) { - return getLiteralPropertyNameText((node).name); - } - return "" + (>(parent).elements).indexOf(node); - } - - function getLiteralPropertyNameText(name: PropertyName) { - const type = getLiteralTypeFromPropertyName(name); - return type.flags & (TypeFlags.StringLiteral | TypeFlags.NumberLiteral) ? "" + (type).value : undefined; - } - - /** Return the inferred type for a binding element */ - function getTypeForBindingElement(declaration: BindingElement): Type | undefined { - const pattern = declaration.parent; - let parentType = getTypeForBindingElementParent(pattern.parent); - // If no type or an any type was inferred for parent, infer that for the binding element - if (!parentType || isTypeAny(parentType)) { - return parentType; - } - // Relax null check on ambient destructuring parameters, since the parameters have no implementation and are just documentation - if (strictNullChecks && declaration.flags & NodeFlags.Ambient && isParameterDeclaration(declaration)) { - parentType = getNonNullableType(parentType); - } - - let type: Type | undefined; - if (pattern.kind === SyntaxKind.ObjectBindingPattern) { - if (declaration.dotDotDotToken) { - if (parentType.flags & TypeFlags.Unknown || !isValidSpreadType(parentType)) { - error(declaration, Diagnostics.Rest_types_may_only_be_created_from_object_types); - return errorType; - } - const literalMembers: PropertyName[] = []; - for (const element of pattern.elements) { - if (!element.dotDotDotToken) { - literalMembers.push(element.propertyName || element.name as Identifier); - } - } - type = getRestType(parentType, literalMembers, declaration.symbol); - } - else { - // Use explicitly specified property name ({ p: xxx } form), or otherwise the implied name ({ p } form) - const name = declaration.propertyName || declaration.name; - const indexType = getLiteralTypeFromPropertyName(name); - const declaredType = getConstraintForLocation(getIndexedAccessType(parentType, indexType, name), declaration.name); - type = getFlowTypeOfDestructuring(declaration, declaredType); - } - } - else { - // This elementType will be used if the specific property corresponding to this index is not - // present (aka the tuple element property). This call also checks that the parentType is in - // fact an iterable or array (depending on target language). - const elementType = checkIteratedTypeOrElementType(IterationUse.Destructuring, parentType, undefinedType, pattern); - const index = pattern.elements.indexOf(declaration); - if (declaration.dotDotDotToken) { - // If the parent is a tuple type, the rest element has a tuple type of the - // remaining tuple element types. Otherwise, the rest element has an array type with same - // element type as the parent type. - type = everyType(parentType, isTupleType) ? - mapType(parentType, t => sliceTupleType(t, index)) : - createArrayType(elementType); - } - else if (isArrayLikeType(parentType)) { - const indexType = getLiteralType(index); - const accessFlags = hasDefaultValue(declaration) ? AccessFlags.NoTupleBoundsCheck : 0; - const declaredType = getConstraintForLocation(getIndexedAccessTypeOrUndefined(parentType, indexType, declaration.name, accessFlags) || errorType, declaration.name); - type = getFlowTypeOfDestructuring(declaration, declaredType); - } - else { - type = elementType; - } - } - if (!declaration.initializer) { - return type; - } - if (getEffectiveTypeAnnotationNode(walkUpBindingElementsAndPatterns(declaration))) { - // In strict null checking mode, if a default value of a non-undefined type is specified, remove - // undefined from the final type. - return strictNullChecks && !(getFalsyFlags(checkDeclarationInitializer(declaration)) & TypeFlags.Undefined) ? - getTypeWithFacts(type, TypeFacts.NEUndefined) : - type; - } - return widenTypeInferredFromInitializer(declaration, getUnionType([getTypeWithFacts(type, TypeFacts.NEUndefined), checkDeclarationInitializer(declaration)], UnionReduction.Subtype)); - } - - function getTypeForDeclarationFromJSDocComment(declaration: Node) { - const jsdocType = getJSDocType(declaration); - if (jsdocType) { - return getTypeFromTypeNode(jsdocType); - } - return undefined; - } - - function isNullOrUndefined(node: Expression) { - const expr = skipParentheses(node); - return expr.kind === SyntaxKind.NullKeyword || expr.kind === SyntaxKind.Identifier && getResolvedSymbol(expr) === undefinedSymbol; - } - - function isEmptyArrayLiteral(node: Expression) { - const expr = skipParentheses(node); - return expr.kind === SyntaxKind.ArrayLiteralExpression && (expr).elements.length === 0; - } - - function addOptionality(type: Type, optional = true): Type { - return strictNullChecks && optional ? getOptionalType(type) : type; - } - - // Return the inferred type for a variable, parameter, or property declaration - function getTypeForVariableLikeDeclaration(declaration: ParameterDeclaration | PropertyDeclaration | PropertySignature | VariableDeclaration | BindingElement, includeOptionality: boolean): Type | undefined { - // A variable declared in a for..in statement is of type string, or of type keyof T when the - // right hand expression is of a type parameter type. - if (isVariableDeclaration(declaration) && declaration.parent.parent.kind === SyntaxKind.ForInStatement) { - const indexType = getIndexType(getNonNullableTypeIfNeeded(checkExpression(declaration.parent.parent.expression))); - return indexType.flags & (TypeFlags.TypeParameter | TypeFlags.Index) ? getExtractStringType(indexType) : stringType; - } - - if (isVariableDeclaration(declaration) && declaration.parent.parent.kind === SyntaxKind.ForOfStatement) { - // checkRightHandSideOfForOf will return undefined if the for-of expression type was - // missing properties/signatures required to get its iteratedType (like - // [Symbol.iterator] or next). This may be because we accessed properties from anyType, - // or it may have led to an error inside getElementTypeOfIterable. - const forOfStatement = declaration.parent.parent; - return checkRightHandSideOfForOf(forOfStatement.expression, forOfStatement.awaitModifier) || anyType; - } - - if (isBindingPattern(declaration.parent)) { - return getTypeForBindingElement(declaration); - } - - const isOptional = includeOptionality && ( - isParameter(declaration) && isJSDocOptionalParameter(declaration) - || !isBindingElement(declaration) && !isVariableDeclaration(declaration) && !!declaration.questionToken); - - // Use type from type annotation if one is present - const declaredType = tryGetTypeFromEffectiveTypeNode(declaration); - if (declaredType) { - return addOptionality(declaredType, isOptional); - } - - if ((noImplicitAny || isInJSFile(declaration)) && - declaration.kind === SyntaxKind.VariableDeclaration && !isBindingPattern(declaration.name) && - !(getCombinedModifierFlags(declaration) & ModifierFlags.Export) && !(declaration.flags & NodeFlags.Ambient)) { - // If --noImplicitAny is on or the declaration is in a Javascript file, - // use control flow tracked 'any' type for non-ambient, non-exported var or let variables with no - // initializer or a 'null' or 'undefined' initializer. - if (!(getCombinedNodeFlags(declaration) & NodeFlags.Const) && (!declaration.initializer || isNullOrUndefined(declaration.initializer))) { - return autoType; - } - // Use control flow tracked 'any[]' type for non-ambient, non-exported variables with an empty array - // literal initializer. - if (declaration.initializer && isEmptyArrayLiteral(declaration.initializer)) { - return autoArrayType; - } - } - - if (declaration.kind === SyntaxKind.Parameter) { - const func = declaration.parent; - // For a parameter of a set accessor, use the type of the get accessor if one is present - if (func.kind === SyntaxKind.SetAccessor && !hasNonBindableDynamicName(func)) { - const getter = getDeclarationOfKind(getSymbolOfNode(declaration.parent), SyntaxKind.GetAccessor); - if (getter) { - const getterSignature = getSignatureFromDeclaration(getter); - const thisParameter = getAccessorThisParameter(func as AccessorDeclaration); - if (thisParameter && declaration === thisParameter) { - // Use the type from the *getter* - Debug.assert(!thisParameter.type); - return getTypeOfSymbol(getterSignature.thisParameter!); - } - return getReturnTypeOfSignature(getterSignature); - } - } - if (isInJSFile(declaration)) { - const typeTag = getJSDocType(func); - if (typeTag && isFunctionTypeNode(typeTag)) { - return getTypeAtPosition(getSignatureFromDeclaration(typeTag), func.parameters.indexOf(declaration)); - } - } - // Use contextual parameter type if one is available - const type = declaration.symbol.escapedName === InternalSymbolName.This ? getContextualThisParameterType(func) : getContextuallyTypedParameterType(declaration); - if (type) { - return addOptionality(type, isOptional); - } - } - else if (isInJSFile(declaration)) { - const containerObjectType = getJSContainerObjectType(declaration, getSymbolOfNode(declaration), getDeclaredExpandoInitializer(declaration)); - if (containerObjectType) { - return containerObjectType; - } - } - - // Use the type of the initializer expression if one is present and the declaration is - // not a parameter of a contextually typed function - if (declaration.initializer) { - const type = widenTypeInferredFromInitializer(declaration, checkDeclarationInitializer(declaration)); - return addOptionality(type, isOptional); - } - - if (isJsxAttribute(declaration)) { - // if JSX attribute doesn't have initializer, by default the attribute will have boolean value of true. - // I.e is sugar for - return trueType; - } - - // If the declaration specifies a binding pattern and is not a parameter of a contextually - // typed function, use the type implied by the binding pattern - if (isBindingPattern(declaration.name)) { - return getTypeFromBindingPattern(declaration.name, /*includePatternInType*/ false, /*reportErrors*/ true); - } - - // No type specified and nothing can be inferred - return undefined; - } - - function getWidenedTypeForAssignmentDeclaration(symbol: Symbol, resolvedSymbol?: Symbol) { - // function/class/{} initializers are themselves containers, so they won't merge in the same way as other initializers - const container = getAssignedExpandoInitializer(symbol.valueDeclaration); - if (container) { - const tag = getJSDocTypeTag(container); - if (tag && tag.typeExpression) { - return getTypeFromTypeNode(tag.typeExpression); - } - const containerObjectType = getJSContainerObjectType(symbol.valueDeclaration, symbol, container); - return containerObjectType || getWidenedLiteralType(checkExpressionCached(container)); - } - let definedInConstructor = false; - let definedInMethod = false; - let jsdocType: Type | undefined; - let types: Type[] | undefined; - for (const declaration of symbol.declarations) { - const expression = (isBinaryExpression(declaration) || isCallExpression(declaration)) ? declaration : - isAccessExpression(declaration) ? isBinaryExpression(declaration.parent) ? declaration.parent : declaration : - undefined; - if (!expression) { - continue; // Non-assignment declaration merged in (eg, an Identifier to mark the thing as a namespace) - skip over it and pull type info from elsewhere - } - - const kind = isAccessExpression(expression) - ? getAssignmentDeclarationPropertyAccessKind(expression) - : getAssignmentDeclarationKind(expression); - if (kind === AssignmentDeclarationKind.ThisProperty) { - if (isDeclarationInConstructor(expression)) { - definedInConstructor = true; - } - else { - definedInMethod = true; - } - } - if (!isCallExpression(expression)) { - jsdocType = getAnnotatedTypeForAssignmentDeclaration(jsdocType, expression, symbol, declaration); - } - if (!jsdocType) { - (types || (types = [])).push((isBinaryExpression(expression) || isCallExpression(expression)) ? getInitializerTypeFromAssignmentDeclaration(symbol, resolvedSymbol, expression, kind) : neverType); - } - } - let type = jsdocType; - if (!type) { - if (!length(types)) { - return errorType; // No types from any declarations :( - } - let constructorTypes = definedInConstructor ? getConstructorDefinedThisAssignmentTypes(types!, symbol.declarations) : undefined; - // use only the constructor types unless they were only assigned null | undefined (including widening variants) - if (definedInMethod) { - const propType = getTypeOfAssignmentDeclarationPropertyOfBaseType(symbol); - if (propType) { - (constructorTypes || (constructorTypes = [])).push(propType); - definedInConstructor = true; - } - } - const sourceTypes = some(constructorTypes, t => !!(t.flags & ~TypeFlags.Nullable)) ? constructorTypes : types; // TODO: GH#18217 - type = getUnionType(sourceTypes!, UnionReduction.Subtype); - } - const widened = getWidenedType(addOptionality(type, definedInMethod && !definedInConstructor)); - if (filterType(widened, t => !!(t.flags & ~TypeFlags.Nullable)) === neverType) { - reportImplicitAny(symbol.valueDeclaration, anyType); - return anyType; - } - return widened; - } - - function getJSContainerObjectType(decl: Node, symbol: Symbol, init: Expression | undefined): Type | undefined { - if (!isInJSFile(decl) || !init || !isObjectLiteralExpression(init) || init.properties.length) { - return undefined; - } - const exports = createSymbolTable(); - while (isBinaryExpression(decl) || isPropertyAccessExpression(decl)) { - const s = getSymbolOfNode(decl); - if (s && hasEntries(s.exports)) { - mergeSymbolTable(exports, s.exports); - } - decl = isBinaryExpression(decl) ? decl.parent : decl.parent.parent; - } - const s = getSymbolOfNode(decl); - if (s && hasEntries(s.exports)) { - mergeSymbolTable(exports, s.exports); - } - const type = createAnonymousType(symbol, exports, emptyArray, emptyArray, undefined, undefined); - type.objectFlags |= ObjectFlags.JSLiteral; - return type; - } - - function getAnnotatedTypeForAssignmentDeclaration(declaredType: Type | undefined, expression: Expression, symbol: Symbol, declaration: Declaration) { - const typeNode = getEffectiveTypeAnnotationNode(expression.parent); - if (typeNode) { - const type = getWidenedType(getTypeFromTypeNode(typeNode)); - if (!declaredType) { - return type; - } - else if (declaredType !== errorType && type !== errorType && !isTypeIdenticalTo(declaredType, type)) { - errorNextVariableOrPropertyDeclarationMustHaveSameType(/*firstDeclaration*/ undefined, declaredType, declaration, type); - } - } - if (symbol.parent) { - const typeNode = getEffectiveTypeAnnotationNode(symbol.parent.valueDeclaration); - if (typeNode) { - return getTypeOfPropertyOfType(getTypeFromTypeNode(typeNode), symbol.escapedName); - } - } - - return declaredType; - } - - /** If we don't have an explicit JSDoc type, get the type from the initializer. */ - function getInitializerTypeFromAssignmentDeclaration(symbol: Symbol, resolvedSymbol: Symbol | undefined, expression: BinaryExpression | CallExpression, kind: AssignmentDeclarationKind) { - if (isCallExpression(expression)) { - if (resolvedSymbol) { - return getTypeOfSymbol(resolvedSymbol); // This shouldn't happen except under some hopefully forbidden merges of export assignments and object define assignments - } - const objectLitType = checkExpressionCached((expression as BindableObjectDefinePropertyCall).arguments[2]); - const valueType = getTypeOfPropertyOfType(objectLitType, "value" as __String); - if (valueType) { - return valueType; - } - const getFunc = getTypeOfPropertyOfType(objectLitType, "get" as __String); - if (getFunc) { - const getSig = getSingleCallSignature(getFunc); - if (getSig) { - return getReturnTypeOfSignature(getSig); - } - } - const setFunc = getTypeOfPropertyOfType(objectLitType, "set" as __String); - if (setFunc) { - const setSig = getSingleCallSignature(setFunc); - if (setSig) { - return getTypeOfFirstParameterOfSignature(setSig); - } - } - return anyType; - } - const type = resolvedSymbol ? getTypeOfSymbol(resolvedSymbol) : getWidenedLiteralType(checkExpressionCached(expression.right)); - if (type.flags & TypeFlags.Object && - kind === AssignmentDeclarationKind.ModuleExports && - symbol.escapedName === InternalSymbolName.ExportEquals) { - const exportedType = resolveStructuredTypeMembers(type as ObjectType); - const members = createSymbolTable(); - copyEntries(exportedType.members, members); - if (resolvedSymbol && !resolvedSymbol.exports) { - resolvedSymbol.exports = createSymbolTable(); - } - (resolvedSymbol || symbol).exports!.forEach((s, name) => { - if (members.has(name)) { - const exportedMember = exportedType.members.get(name)!; - const union = createSymbol(s.flags | exportedMember.flags, name); - union.type = getUnionType([getTypeOfSymbol(s), getTypeOfSymbol(exportedMember)]); - members.set(name, union); - } - else { - members.set(name, s); - } - }); - const result = createAnonymousType( - exportedType.symbol, - members, - exportedType.callSignatures, - exportedType.constructSignatures, - exportedType.stringIndexInfo, - exportedType.numberIndexInfo); - result.objectFlags |= (getObjectFlags(type) & ObjectFlags.JSLiteral); // Propagate JSLiteral flag - return result; - } - if (isEmptyArrayLiteralType(type)) { - reportImplicitAny(expression, anyArrayType); - return anyArrayType; - } - return type; - } - - function isDeclarationInConstructor(expression: Expression) { - const thisContainer = getThisContainer(expression, /*includeArrowFunctions*/ false); - // Properties defined in a constructor (or base constructor, or javascript constructor function) don't get undefined added. - // Function expressions that are assigned to the prototype count as methods. - return thisContainer.kind === SyntaxKind.Constructor || - thisContainer.kind === SyntaxKind.FunctionDeclaration || - (thisContainer.kind === SyntaxKind.FunctionExpression && !isPrototypePropertyAssignment(thisContainer.parent)); - } - - function getConstructorDefinedThisAssignmentTypes(types: Type[], declarations: Declaration[]): Type[] | undefined { - Debug.assert(types.length === declarations.length); - return types.filter((_, i) => { - const declaration = declarations[i]; - const expression = isBinaryExpression(declaration) ? declaration : - isBinaryExpression(declaration.parent) ? declaration.parent : undefined; - return expression && isDeclarationInConstructor(expression); - }); - } - - /** check for definition in base class if any declaration is in a class */ - function getTypeOfAssignmentDeclarationPropertyOfBaseType(property: Symbol) { - const parentDeclaration = forEach(property.declarations, d => { - const parent = getThisContainer(d, /*includeArrowFunctions*/ false).parent; - return isClassLike(parent) && parent; - }); - if (parentDeclaration) { - const classType = getDeclaredTypeOfSymbol(getSymbolOfNode(parentDeclaration)) as InterfaceType; - const baseClassType = classType && getBaseTypes(classType)[0]; - if (baseClassType) { - return getTypeOfPropertyOfType(baseClassType, property.escapedName); - } - } - } - - // Return the type implied by a binding pattern element. This is the type of the initializer of the element if - // one is present. Otherwise, if the element is itself a binding pattern, it is the type implied by the binding - // pattern. Otherwise, it is the type any. - function getTypeFromBindingElement(element: BindingElement, includePatternInType?: boolean, reportErrors?: boolean): Type { - if (element.initializer) { - // The type implied by a binding pattern is independent of context, so we check the initializer with no - // contextual type or, if the element itself is a binding pattern, with the type implied by that binding - // pattern. - const contextualType = isBindingPattern(element.name) ? getTypeFromBindingPattern(element.name, /*includePatternInType*/ true, /*reportErrors*/ false) : unknownType; - return addOptionality(widenTypeInferredFromInitializer(element, checkDeclarationInitializer(element, contextualType))); - } - if (isBindingPattern(element.name)) { - return getTypeFromBindingPattern(element.name, includePatternInType, reportErrors); - } - if (reportErrors && !declarationBelongsToPrivateAmbientMember(element)) { - reportImplicitAny(element, anyType); - } - // When we're including the pattern in the type (an indication we're obtaining a contextual type), we - // use the non-inferrable any type. Inference will never directly infer this type, but it is possible - // to infer a type that contains it, e.g. for a binding pattern like [foo] or { foo }. In such cases, - // widening of the binding pattern type substitutes a regular any for the non-inferrable any. - return includePatternInType ? nonInferrableAnyType : anyType; - } - - // Return the type implied by an object binding pattern - function getTypeFromObjectBindingPattern(pattern: ObjectBindingPattern, includePatternInType: boolean, reportErrors: boolean): Type { - const members = createSymbolTable(); - let stringIndexInfo: IndexInfo | undefined; - let objectFlags = ObjectFlags.ObjectLiteral | ObjectFlags.ContainsObjectOrArrayLiteral; - forEach(pattern.elements, e => { - const name = e.propertyName || e.name; - if (e.dotDotDotToken) { - stringIndexInfo = createIndexInfo(anyType, /*isReadonly*/ false); - return; - } - - const exprType = getLiteralTypeFromPropertyName(name); - if (!isTypeUsableAsPropertyName(exprType)) { - // do not include computed properties in the implied type - objectFlags |= ObjectFlags.ObjectLiteralPatternWithComputedProperties; - return; - } - const text = getPropertyNameFromType(exprType); - const flags = SymbolFlags.Property | (e.initializer ? SymbolFlags.Optional : 0); - const symbol = createSymbol(flags, text); - symbol.type = getTypeFromBindingElement(e, includePatternInType, reportErrors); - symbol.bindingElement = e; - members.set(symbol.escapedName, symbol); - }); - const result = createAnonymousType(undefined, members, emptyArray, emptyArray, stringIndexInfo, undefined); - result.objectFlags |= objectFlags; - if (includePatternInType) { - result.pattern = pattern; - result.objectFlags |= ObjectFlags.ContainsObjectOrArrayLiteral; - } - return result; - } - - // Return the type implied by an array binding pattern - function getTypeFromArrayBindingPattern(pattern: BindingPattern, includePatternInType: boolean, reportErrors: boolean): Type { - const elements = pattern.elements; - const lastElement = lastOrUndefined(elements); - const hasRestElement = !!(lastElement && lastElement.kind === SyntaxKind.BindingElement && lastElement.dotDotDotToken); - if (elements.length === 0 || elements.length === 1 && hasRestElement) { - return languageVersion >= ScriptTarget.ES2015 ? createIterableType(anyType) : anyArrayType; - } - const elementTypes = map(elements, e => isOmittedExpression(e) ? anyType : getTypeFromBindingElement(e, includePatternInType, reportErrors)); - const minLength = findLastIndex(elements, e => !isOmittedExpression(e) && !hasDefaultValue(e), elements.length - (hasRestElement ? 2 : 1)) + 1; - let result = createTupleType(elementTypes, minLength, hasRestElement); - if (includePatternInType) { - result = cloneTypeReference(result); - result.pattern = pattern; - result.objectFlags |= ObjectFlags.ContainsObjectOrArrayLiteral; - } - return result; - } - - // Return the type implied by a binding pattern. This is the type implied purely by the binding pattern itself - // and without regard to its context (i.e. without regard any type annotation or initializer associated with the - // declaration in which the binding pattern is contained). For example, the implied type of [x, y] is [any, any] - // and the implied type of { x, y: z = 1 } is { x: any; y: number; }. The type implied by a binding pattern is - // used as the contextual type of an initializer associated with the binding pattern. Also, for a destructuring - // parameter with no type annotation or initializer, the type implied by the binding pattern becomes the type of - // the parameter. - function getTypeFromBindingPattern(pattern: BindingPattern, includePatternInType = false, reportErrors = false): Type { - return pattern.kind === SyntaxKind.ObjectBindingPattern - ? getTypeFromObjectBindingPattern(pattern, includePatternInType, reportErrors) - : getTypeFromArrayBindingPattern(pattern, includePatternInType, reportErrors); - } - - // Return the type associated with a variable, parameter, or property declaration. In the simple case this is the type - // specified in a type annotation or inferred from an initializer. However, in the case of a destructuring declaration it - // is a bit more involved. For example: - // - // var [x, s = ""] = [1, "one"]; - // - // Here, the array literal [1, "one"] is contextually typed by the type [any, string], which is the implied type of the - // binding pattern [x, s = ""]. Because the contextual type is a tuple type, the resulting type of [1, "one"] is the - // tuple type [number, string]. Thus, the type inferred for 'x' is number and the type inferred for 's' is string. - function getWidenedTypeForVariableLikeDeclaration(declaration: ParameterDeclaration | PropertyDeclaration | PropertySignature | VariableDeclaration | BindingElement, reportErrors?: boolean): Type { - return widenTypeForVariableLikeDeclaration(getTypeForVariableLikeDeclaration(declaration, /*includeOptionality*/ true), declaration, reportErrors); - } - - function widenTypeForVariableLikeDeclaration(type: Type | undefined, declaration: any, reportErrors?: boolean) { - if (type) { - if (reportErrors) { - reportErrorsFromWidening(declaration, type); - } - - // always widen a 'unique symbol' type if the type was created for a different declaration. - if (type.flags & TypeFlags.UniqueESSymbol && (isBindingElement(declaration) || !declaration.type) && type.symbol !== getSymbolOfNode(declaration)) { - type = esSymbolType; - } - - return getWidenedType(type); - } - - // Rest parameters default to type any[], other parameters default to type any - type = isParameter(declaration) && declaration.dotDotDotToken ? anyArrayType : anyType; - - // Report implicit any errors unless this is a private property within an ambient declaration - if (reportErrors) { - if (!declarationBelongsToPrivateAmbientMember(declaration)) { - reportImplicitAny(declaration, type); - } - } - return type; - } - - function declarationBelongsToPrivateAmbientMember(declaration: VariableLikeDeclaration) { - const root = getRootDeclaration(declaration); - const memberDeclaration = root.kind === SyntaxKind.Parameter ? root.parent : root; - return isPrivateWithinAmbient(memberDeclaration); - } - - function tryGetTypeFromEffectiveTypeNode(declaration: Declaration) { - const typeNode = getEffectiveTypeAnnotationNode(declaration); - if (typeNode) { - return getTypeFromTypeNode(typeNode); - } - } - - function getTypeOfVariableOrParameterOrProperty(symbol: Symbol): Type { - const links = getSymbolLinks(symbol); - if (!links.type) { - const type = getTypeOfVariableOrParameterOrPropertyWorker(symbol); - // For a contextually typed parameter it is possible that a type has already - // been assigned (in assignTypeToParameterAndFixTypeParameters), and we want - // to preserve this type. - if (!links.type) { - links.type = type; - } - } - return links.type; - } - - function getTypeOfVariableOrParameterOrPropertyWorker(symbol: Symbol) { - // Handle prototype property - if (symbol.flags & SymbolFlags.Prototype) { - return getTypeOfPrototypeProperty(symbol); - } - // CommonsJS require and module both have type any. - if (symbol === requireSymbol) { - return anyType; - } - if (symbol.flags & SymbolFlags.ModuleExports) { - const fileSymbol = getSymbolOfNode(getSourceFileOfNode(symbol.valueDeclaration)); - const members = createSymbolTable(); - members.set("exports" as __String, fileSymbol); - return createAnonymousType(symbol, members, emptyArray, emptyArray, undefined, undefined); - } - // Handle catch clause variables - const declaration = symbol.valueDeclaration; - if (isCatchClauseVariableDeclarationOrBindingElement(declaration)) { - return anyType; - } - // Handle export default expressions - if (isSourceFile(declaration) && isJsonSourceFile(declaration)) { - if (!declaration.statements.length) { - return emptyObjectType; - } - return getWidenedType(getWidenedLiteralType(checkExpression(declaration.statements[0].expression))); - } - - // Handle variable, parameter or property - if (!pushTypeResolution(symbol, TypeSystemPropertyName.Type)) { - // Symbol is property of some kind that is merged with something - should use `getTypeOfFuncClassEnumModule` and not `getTypeOfVariableOrParameterOrProperty` - if (symbol.flags & SymbolFlags.ValueModule && !(symbol.flags & SymbolFlags.Assignment)) { - return getTypeOfFuncClassEnumModule(symbol); - } - return reportCircularityError(symbol); - } - let type: Type | undefined; - if (declaration.kind === SyntaxKind.ExportAssignment) { - type = widenTypeForVariableLikeDeclaration(checkExpressionCached((declaration).expression), declaration); - } - else if ( - isBinaryExpression(declaration) || - (isInJSFile(declaration) && - (isCallExpression(declaration) || (isPropertyAccessExpression(declaration) || isBindableStaticElementAccessExpression(declaration)) && isBinaryExpression(declaration.parent)))) { - type = getWidenedTypeForAssignmentDeclaration(symbol); - } - else if (isJSDocPropertyLikeTag(declaration) - || isPropertyAccessExpression(declaration) - || isElementAccessExpression(declaration) - || isIdentifier(declaration) - || isStringLiteralLike(declaration) - || isNumericLiteral(declaration) - || isClassDeclaration(declaration) - || isFunctionDeclaration(declaration) - || (isMethodDeclaration(declaration) && !isObjectLiteralMethod(declaration)) - || isMethodSignature(declaration) - || isSourceFile(declaration)) { - // Symbol is property of some kind that is merged with something - should use `getTypeOfFuncClassEnumModule` and not `getTypeOfVariableOrParameterOrProperty` - if (symbol.flags & (SymbolFlags.Function | SymbolFlags.Method | SymbolFlags.Class | SymbolFlags.Enum | SymbolFlags.ValueModule)) { - return getTypeOfFuncClassEnumModule(symbol); - } - type = isBinaryExpression(declaration.parent) ? - getWidenedTypeForAssignmentDeclaration(symbol) : - tryGetTypeFromEffectiveTypeNode(declaration) || anyType; - } - else if (isPropertyAssignment(declaration)) { - type = tryGetTypeFromEffectiveTypeNode(declaration) || checkPropertyAssignment(declaration); - } - else if (isJsxAttribute(declaration)) { - type = tryGetTypeFromEffectiveTypeNode(declaration) || checkJsxAttribute(declaration); - } - else if (isShorthandPropertyAssignment(declaration)) { - type = tryGetTypeFromEffectiveTypeNode(declaration) || checkExpressionForMutableLocation(declaration.name, CheckMode.Normal); - } - else if (isObjectLiteralMethod(declaration)) { - type = tryGetTypeFromEffectiveTypeNode(declaration) || checkObjectLiteralMethod(declaration, CheckMode.Normal); - } - else if (isParameter(declaration) - || isPropertyDeclaration(declaration) - || isPropertySignature(declaration) - || isVariableDeclaration(declaration) - || isBindingElement(declaration)) { - type = getWidenedTypeForVariableLikeDeclaration(declaration, /*includeOptionality*/ true); - } - // getTypeOfSymbol dispatches some JS merges incorrectly because their symbol flags are not mutually exclusive. - // Re-dispatch based on valueDeclaration.kind instead. - else if (isEnumDeclaration(declaration)) { - type = getTypeOfFuncClassEnumModule(symbol); - } - else if (isEnumMember(declaration)) { - type = getTypeOfEnumMember(symbol); - } - else if (isAccessor(declaration)) { - type = resolveTypeOfAccessors(symbol); - } - else { - return Debug.fail("Unhandled declaration kind! " + Debug.formatSyntaxKind(declaration.kind) + " for " + Debug.formatSymbol(symbol)); - } - - if (!popTypeResolution()) { - // Symbol is property of some kind that is merged with something - should use `getTypeOfFuncClassEnumModule` and not `getTypeOfVariableOrParameterOrProperty` - if (symbol.flags & SymbolFlags.ValueModule && !(symbol.flags & SymbolFlags.Assignment)) { - return getTypeOfFuncClassEnumModule(symbol); - } - return reportCircularityError(symbol); - } - return type; - } - - function getAnnotatedAccessorTypeNode(accessor: AccessorDeclaration | undefined): TypeNode | undefined { - if (accessor) { - if (accessor.kind === SyntaxKind.GetAccessor) { - const getterTypeAnnotation = getEffectiveReturnTypeNode(accessor); - return getterTypeAnnotation; - } - else { - const setterTypeAnnotation = getEffectiveSetAccessorTypeAnnotationNode(accessor); - return setterTypeAnnotation; - } - } - return undefined; - } - - function getAnnotatedAccessorType(accessor: AccessorDeclaration | undefined): Type | undefined { - const node = getAnnotatedAccessorTypeNode(accessor); - return node && getTypeFromTypeNode(node); - } - - function getAnnotatedAccessorThisParameter(accessor: AccessorDeclaration): Symbol | undefined { - const parameter = getAccessorThisParameter(accessor); - return parameter && parameter.symbol; - } - - function getThisTypeOfDeclaration(declaration: SignatureDeclaration): Type | undefined { - return getThisTypeOfSignature(getSignatureFromDeclaration(declaration)); - } - - function getTypeOfAccessors(symbol: Symbol): Type { - const links = getSymbolLinks(symbol); - return links.type || (links.type = getTypeOfAccessorsWorker(symbol)); - } - - function getTypeOfAccessorsWorker(symbol: Symbol): Type { - if (!pushTypeResolution(symbol, TypeSystemPropertyName.Type)) { - return errorType; - } - - let type = resolveTypeOfAccessors(symbol); - - if (!popTypeResolution()) { - type = anyType; - if (noImplicitAny) { - const getter = getDeclarationOfKind(symbol, SyntaxKind.GetAccessor); - error(getter, Diagnostics._0_implicitly_has_return_type_any_because_it_does_not_have_a_return_type_annotation_and_is_referenced_directly_or_indirectly_in_one_of_its_return_expressions, symbolToString(symbol)); - } - } - return type; - } - - function resolveTypeOfAccessors(symbol: Symbol) { - const getter = getDeclarationOfKind(symbol, SyntaxKind.GetAccessor); - const setter = getDeclarationOfKind(symbol, SyntaxKind.SetAccessor); - - if (getter && isInJSFile(getter)) { - const jsDocType = getTypeForDeclarationFromJSDocComment(getter); - if (jsDocType) { - return jsDocType; - } - } - // First try to see if the user specified a return type on the get-accessor. - const getterReturnType = getAnnotatedAccessorType(getter); - if (getterReturnType) { - return getterReturnType; - } - else { - // If the user didn't specify a return type, try to use the set-accessor's parameter type. - const setterParameterType = getAnnotatedAccessorType(setter); - if (setterParameterType) { - return setterParameterType; - } - else { - // If there are no specified types, try to infer it from the body of the get accessor if it exists. - if (getter && getter.body) { - return getReturnTypeFromBody(getter); - } - // Otherwise, fall back to 'any'. - else { - if (setter) { - if (!isPrivateWithinAmbient(setter)) { - errorOrSuggestion(noImplicitAny, setter, Diagnostics.Property_0_implicitly_has_type_any_because_its_set_accessor_lacks_a_parameter_type_annotation, symbolToString(symbol)); - } - } - else { - Debug.assert(!!getter, "there must exist a getter as we are current checking either setter or getter in this function"); - if (!isPrivateWithinAmbient(getter)) { - errorOrSuggestion(noImplicitAny, getter, Diagnostics.Property_0_implicitly_has_type_any_because_its_get_accessor_lacks_a_return_type_annotation, symbolToString(symbol)); - } - } - return anyType; - } - } - } - } - - function getBaseTypeVariableOfClass(symbol: Symbol) { - const baseConstructorType = getBaseConstructorTypeOfClass(getDeclaredTypeOfClassOrInterface(symbol)); - return baseConstructorType.flags & TypeFlags.TypeVariable ? baseConstructorType : - baseConstructorType.flags & TypeFlags.Intersection ? find((baseConstructorType as IntersectionType).types, t => !!(t.flags & TypeFlags.TypeVariable)) : - undefined; - } - - function getTypeOfFuncClassEnumModule(symbol: Symbol): Type { - let links = getSymbolLinks(symbol); - const originalLinks = links; - if (!links.type) { - const jsDeclaration = getDeclarationOfExpando(symbol.valueDeclaration); - if (jsDeclaration) { - const merged = mergeJSSymbols(symbol, getSymbolOfNode(jsDeclaration)); - if (merged) { - // note:we overwrite links because we just cloned the symbol - symbol = links = merged; - } - } - originalLinks.type = links.type = getTypeOfFuncClassEnumModuleWorker(symbol); - } - return links.type; - } - - function getTypeOfFuncClassEnumModuleWorker(symbol: Symbol): Type { - const declaration = symbol.valueDeclaration; - if (symbol.flags & SymbolFlags.Module && isShorthandAmbientModuleSymbol(symbol)) { - return anyType; - } - else if (declaration.kind === SyntaxKind.BinaryExpression || - isAccessExpression(declaration) && - declaration.parent.kind === SyntaxKind.BinaryExpression) { - return getWidenedTypeForAssignmentDeclaration(symbol); - } - else if (symbol.flags & SymbolFlags.ValueModule && declaration && isSourceFile(declaration) && declaration.commonJsModuleIndicator) { - const resolvedModule = resolveExternalModuleSymbol(symbol); - if (resolvedModule !== symbol) { - if (!pushTypeResolution(symbol, TypeSystemPropertyName.Type)) { - return errorType; - } - const exportEquals = getMergedSymbol(symbol.exports!.get(InternalSymbolName.ExportEquals)!); - const type = getWidenedTypeForAssignmentDeclaration(exportEquals, exportEquals === resolvedModule ? undefined : resolvedModule); - if (!popTypeResolution()) { - return reportCircularityError(symbol); - } - return type; - } - } - const type = createObjectType(ObjectFlags.Anonymous, symbol); - if (symbol.flags & SymbolFlags.Class) { - const baseTypeVariable = getBaseTypeVariableOfClass(symbol); - return baseTypeVariable ? getIntersectionType([type, baseTypeVariable]) : type; - } - else { - return strictNullChecks && symbol.flags & SymbolFlags.Optional ? getOptionalType(type) : type; - } - } - - function getTypeOfEnumMember(symbol: Symbol): Type { - const links = getSymbolLinks(symbol); - return links.type || (links.type = getDeclaredTypeOfEnumMember(symbol)); - } - - function getTypeOfAlias(symbol: Symbol): Type { - const links = getSymbolLinks(symbol); - if (!links.type) { - const targetSymbol = resolveAlias(symbol); - - // It only makes sense to get the type of a value symbol. If the result of resolving - // the alias is not a value, then it has no type. To get the type associated with a - // type symbol, call getDeclaredTypeOfSymbol. - // This check is important because without it, a call to getTypeOfSymbol could end - // up recursively calling getTypeOfAlias, causing a stack overflow. - links.type = targetSymbol.flags & SymbolFlags.Value - ? getTypeOfSymbol(targetSymbol) - : errorType; - } - return links.type; - } - - function getTypeOfInstantiatedSymbol(symbol: Symbol): Type { - const links = getSymbolLinks(symbol); - if (!links.type) { - if (!pushTypeResolution(symbol, TypeSystemPropertyName.Type)) { - return links.type = errorType; - } - let type = instantiateType(getTypeOfSymbol(links.target!), links.mapper); - if (!popTypeResolution()) { - type = reportCircularityError(symbol); - } - links.type = type; - } - return links.type; - } - - function reportCircularityError(symbol: Symbol) { - const declaration = symbol.valueDeclaration; - // Check if variable has type annotation that circularly references the variable itself - if (getEffectiveTypeAnnotationNode(declaration)) { - error(symbol.valueDeclaration, Diagnostics._0_is_referenced_directly_or_indirectly_in_its_own_type_annotation, - symbolToString(symbol)); - return errorType; - } - // Check if variable has initializer that circularly references the variable itself - if (noImplicitAny && (declaration.kind !== SyntaxKind.Parameter || (declaration).initializer)) { - error(symbol.valueDeclaration, Diagnostics._0_implicitly_has_type_any_because_it_does_not_have_a_type_annotation_and_is_referenced_directly_or_indirectly_in_its_own_initializer, - symbolToString(symbol)); - } - // Circularities could also result from parameters in function expressions that end up - // having themselves as contextual types following type argument inference. In those cases - // we have already reported an implicit any error so we don't report anything here. - return anyType; - } - - function getTypeOfSymbolWithDeferredType(symbol: Symbol) { - const links = getSymbolLinks(symbol); - if (!links.type) { - Debug.assertIsDefined(links.deferralParent); - Debug.assertIsDefined(links.deferralConstituents); - links.type = links.deferralParent.flags & TypeFlags.Union ? getUnionType(links.deferralConstituents) : getIntersectionType(links.deferralConstituents); - } - return links.type; - } - - function getTypeOfSymbol(symbol: Symbol): Type { - if (getCheckFlags(symbol) & CheckFlags.DeferredType) { - return getTypeOfSymbolWithDeferredType(symbol); - } - if (getCheckFlags(symbol) & CheckFlags.Instantiated) { - return getTypeOfInstantiatedSymbol(symbol); - } - if (getCheckFlags(symbol) & CheckFlags.ReverseMapped) { - return getTypeOfReverseMappedSymbol(symbol as ReverseMappedSymbol); - } - if (symbol.flags & (SymbolFlags.Variable | SymbolFlags.Property)) { - return getTypeOfVariableOrParameterOrProperty(symbol); - } - if (symbol.flags & (SymbolFlags.Function | SymbolFlags.Method | SymbolFlags.Class | SymbolFlags.Enum | SymbolFlags.ValueModule)) { - return getTypeOfFuncClassEnumModule(symbol); - } - if (symbol.flags & SymbolFlags.EnumMember) { - return getTypeOfEnumMember(symbol); - } - if (symbol.flags & SymbolFlags.Accessor) { - return getTypeOfAccessors(symbol); - } - if (symbol.flags & SymbolFlags.Alias) { - return getTypeOfAlias(symbol); - } - return errorType; - } - - function isReferenceToType(type: Type, target: Type) { - return type !== undefined - && target !== undefined - && (getObjectFlags(type) & ObjectFlags.Reference) !== 0 - && (type).target === target; - } - - function getTargetType(type: Type): Type { - return getObjectFlags(type) & ObjectFlags.Reference ? (type).target : type; - } - - // TODO: GH#18217 If `checkBase` is undefined, we should not call this because this will always return false. - function hasBaseType(type: Type, checkBase: Type | undefined) { - return check(type); - function check(type: Type): boolean { - if (getObjectFlags(type) & (ObjectFlags.ClassOrInterface | ObjectFlags.Reference)) { - const target = getTargetType(type); - return target === checkBase || some(getBaseTypes(target), check); - } - else if (type.flags & TypeFlags.Intersection) { - return some((type).types, check); - } - return false; - } - } - - // Appends the type parameters given by a list of declarations to a set of type parameters and returns the resulting set. - // The function allocates a new array if the input type parameter set is undefined, but otherwise it modifies the set - // in-place and returns the same array. - function appendTypeParameters(typeParameters: TypeParameter[] | undefined, declarations: readonly TypeParameterDeclaration[]): TypeParameter[] | undefined { - for (const declaration of declarations) { - typeParameters = appendIfUnique(typeParameters, getDeclaredTypeOfTypeParameter(getSymbolOfNode(declaration))); - } - return typeParameters; - } - - // Return the outer type parameters of a node or undefined if the node has no outer type parameters. - function getOuterTypeParameters(node: Node, includeThisTypes?: boolean): TypeParameter[] | undefined { - while (true) { - node = node.parent; // TODO: GH#18217 Use SourceFile kind check instead - if (node && isBinaryExpression(node)) { - // prototype assignments get the outer type parameters of their constructor function - const assignmentKind = getAssignmentDeclarationKind(node); - if (assignmentKind === AssignmentDeclarationKind.Prototype || assignmentKind === AssignmentDeclarationKind.PrototypeProperty) { - const symbol = getSymbolOfNode(node.left); - if (symbol && symbol.parent && !findAncestor(symbol.parent.valueDeclaration, d => node === d)) { - node = symbol.parent.valueDeclaration; - } - } - } - if (!node) { - return undefined; - } - switch (node.kind) { - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ClassExpression: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.CallSignature: - case SyntaxKind.ConstructSignature: - case SyntaxKind.MethodSignature: - case SyntaxKind.FunctionType: - case SyntaxKind.ConstructorType: - case SyntaxKind.JSDocFunctionType: - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.FunctionExpression: - case SyntaxKind.ArrowFunction: - case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.JSDocTemplateTag: - case SyntaxKind.JSDocTypedefTag: - case SyntaxKind.JSDocEnumTag: - case SyntaxKind.JSDocCallbackTag: - case SyntaxKind.MappedType: - case SyntaxKind.ConditionalType: - const outerTypeParameters = getOuterTypeParameters(node, includeThisTypes); - if (node.kind === SyntaxKind.MappedType) { - return append(outerTypeParameters, getDeclaredTypeOfTypeParameter(getSymbolOfNode((node).typeParameter))); - } - else if (node.kind === SyntaxKind.ConditionalType) { - return concatenate(outerTypeParameters, getInferTypeParameters(node)); - } - const outerAndOwnTypeParameters = appendTypeParameters(outerTypeParameters, getEffectiveTypeParameterDeclarations(node)); - const thisType = includeThisTypes && - (node.kind === SyntaxKind.ClassDeclaration || node.kind === SyntaxKind.ClassExpression || node.kind === SyntaxKind.InterfaceDeclaration || isJSConstructor(node)) && - getDeclaredTypeOfClassOrInterface(getSymbolOfNode(node as ClassLikeDeclaration | InterfaceDeclaration)).thisType; - return thisType ? append(outerAndOwnTypeParameters, thisType) : outerAndOwnTypeParameters; - } - } - } - - // The outer type parameters are those defined by enclosing generic classes, methods, or functions. - function getOuterTypeParametersOfClassOrInterface(symbol: Symbol): TypeParameter[] | undefined { - const declaration = symbol.flags & SymbolFlags.Class ? symbol.valueDeclaration : getDeclarationOfKind(symbol, SyntaxKind.InterfaceDeclaration)!; - Debug.assert(!!declaration, "Class was missing valueDeclaration -OR- non-class had no interface declarations"); - return getOuterTypeParameters(declaration); - } - - // The local type parameters are the combined set of type parameters from all declarations of the class, - // interface, or type alias. - function getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol: Symbol): TypeParameter[] | undefined { - let result: TypeParameter[] | undefined; - for (const node of symbol.declarations) { - if (node.kind === SyntaxKind.InterfaceDeclaration || - node.kind === SyntaxKind.ClassDeclaration || - node.kind === SyntaxKind.ClassExpression || - isJSConstructor(node) || - isTypeAlias(node)) { - const declaration = node; - result = appendTypeParameters(result, getEffectiveTypeParameterDeclarations(declaration)); - } - } - return result; - } - - // The full set of type parameters for a generic class or interface type consists of its outer type parameters plus - // its locally declared type parameters. - function getTypeParametersOfClassOrInterface(symbol: Symbol): TypeParameter[] | undefined { - return concatenate(getOuterTypeParametersOfClassOrInterface(symbol), getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol)); - } - - // A type is a mixin constructor if it has a single construct signature taking no type parameters and a single - // rest parameter of type any[]. - function isMixinConstructorType(type: Type) { - const signatures = getSignaturesOfType(type, SignatureKind.Construct); - if (signatures.length === 1) { - const s = signatures[0]; - return !s.typeParameters && s.parameters.length === 1 && signatureHasRestParameter(s) && getElementTypeOfArrayType(getTypeOfParameter(s.parameters[0])) === anyType; - } - return false; - } - - function isConstructorType(type: Type): boolean { - if (getSignaturesOfType(type, SignatureKind.Construct).length > 0) { - return true; - } - if (type.flags & TypeFlags.TypeVariable) { - const constraint = getBaseConstraintOfType(type); - return !!constraint && isMixinConstructorType(constraint); - } - return false; - } - - function getBaseTypeNodeOfClass(type: InterfaceType): ExpressionWithTypeArguments | undefined { - return getEffectiveBaseTypeNode(type.symbol.valueDeclaration as ClassLikeDeclaration); - } - - function getConstructorsForTypeArguments(type: Type, typeArgumentNodes: readonly TypeNode[] | undefined, location: Node): readonly Signature[] { - const typeArgCount = length(typeArgumentNodes); - const isJavascript = isInJSFile(location); - return filter(getSignaturesOfType(type, SignatureKind.Construct), - sig => (isJavascript || typeArgCount >= getMinTypeArgumentCount(sig.typeParameters)) && typeArgCount <= length(sig.typeParameters)); - } - - function getInstantiatedConstructorsForTypeArguments(type: Type, typeArgumentNodes: readonly TypeNode[] | undefined, location: Node): readonly Signature[] { - const signatures = getConstructorsForTypeArguments(type, typeArgumentNodes, location); - const typeArguments = map(typeArgumentNodes, getTypeFromTypeNode); - return sameMap(signatures, sig => some(sig.typeParameters) ? getSignatureInstantiation(sig, typeArguments, isInJSFile(location)) : sig); - } - - /** - * The base constructor of a class can resolve to - * * undefinedType if the class has no extends clause, - * * unknownType if an error occurred during resolution of the extends expression, - * * nullType if the extends expression is the null value, - * * anyType if the extends expression has type any, or - * * an object type with at least one construct signature. - */ - function getBaseConstructorTypeOfClass(type: InterfaceType): Type { - if (!type.resolvedBaseConstructorType) { - const decl = type.symbol.valueDeclaration; - const extended = getEffectiveBaseTypeNode(decl); - const baseTypeNode = getBaseTypeNodeOfClass(type); - if (!baseTypeNode) { - return type.resolvedBaseConstructorType = undefinedType; - } - if (!pushTypeResolution(type, TypeSystemPropertyName.ResolvedBaseConstructorType)) { - return errorType; - } - const baseConstructorType = checkExpression(baseTypeNode.expression); - if (extended && baseTypeNode !== extended) { - Debug.assert(!extended.typeArguments); // Because this is in a JS file, and baseTypeNode is in an @extends tag - checkExpression(extended.expression); - } - if (baseConstructorType.flags & (TypeFlags.Object | TypeFlags.Intersection)) { - // Resolving the members of a class requires us to resolve the base class of that class. - // We force resolution here such that we catch circularities now. - resolveStructuredTypeMembers(baseConstructorType); - } - if (!popTypeResolution()) { - error(type.symbol.valueDeclaration, Diagnostics._0_is_referenced_directly_or_indirectly_in_its_own_base_expression, symbolToString(type.symbol)); - return type.resolvedBaseConstructorType = errorType; - } - if (!(baseConstructorType.flags & TypeFlags.Any) && baseConstructorType !== nullWideningType && !isConstructorType(baseConstructorType)) { - const err = error(baseTypeNode.expression, Diagnostics.Type_0_is_not_a_constructor_function_type, typeToString(baseConstructorType)); - if (baseConstructorType.flags & TypeFlags.TypeParameter) { - const constraint = getConstraintFromTypeParameter(baseConstructorType); - let ctorReturn: Type = unknownType; - if (constraint) { - const ctorSig = getSignaturesOfType(constraint, SignatureKind.Construct); - if (ctorSig[0]) { - ctorReturn = getReturnTypeOfSignature(ctorSig[0]); - } - } - addRelatedInfo(err, createDiagnosticForNode(baseConstructorType.symbol.declarations[0], Diagnostics.Did_you_mean_for_0_to_be_constrained_to_type_new_args_Colon_any_1, symbolToString(baseConstructorType.symbol), typeToString(ctorReturn))); - } - return type.resolvedBaseConstructorType = errorType; - } - type.resolvedBaseConstructorType = baseConstructorType; - } - return type.resolvedBaseConstructorType; - } - - function getImplementsTypes(type: InterfaceType): BaseType[] { - let resolvedImplementsTypes: BaseType[] = emptyArray; - for (const declaration of type.symbol.declarations) { - const implementsTypeNodes = getEffectiveImplementsTypeNodes(declaration as ClassLikeDeclaration); - if (!implementsTypeNodes) continue; - for (const node of implementsTypeNodes) { - const implementsType = getTypeFromTypeNode(node); - if (implementsType !== errorType) { - if (resolvedImplementsTypes === emptyArray) { - resolvedImplementsTypes = [implementsType]; - } - else { - resolvedImplementsTypes.push(implementsType); - } - } - } - } - return resolvedImplementsTypes; - } - - function getBaseTypes(type: InterfaceType): BaseType[] { - if (!type.resolvedBaseTypes) { - if (type.objectFlags & ObjectFlags.Tuple) { - type.resolvedBaseTypes = [createArrayType(getUnionType(type.typeParameters || emptyArray), (type).readonly)]; - } - else if (type.symbol.flags & (SymbolFlags.Class | SymbolFlags.Interface)) { - if (type.symbol.flags & SymbolFlags.Class) { - resolveBaseTypesOfClass(type); - } - if (type.symbol.flags & SymbolFlags.Interface) { - resolveBaseTypesOfInterface(type); - } - } - else { - Debug.fail("type must be class or interface"); - } - } - return type.resolvedBaseTypes; - } - - function resolveBaseTypesOfClass(type: InterfaceType) { - type.resolvedBaseTypes = resolvingEmptyArray; - const baseConstructorType = getApparentType(getBaseConstructorTypeOfClass(type)); - if (!(baseConstructorType.flags & (TypeFlags.Object | TypeFlags.Intersection | TypeFlags.Any))) { - return type.resolvedBaseTypes = emptyArray; - } - const baseTypeNode = getBaseTypeNodeOfClass(type)!; - let baseType: Type; - const originalBaseType = baseConstructorType.symbol ? getDeclaredTypeOfSymbol(baseConstructorType.symbol) : undefined; - if (baseConstructorType.symbol && baseConstructorType.symbol.flags & SymbolFlags.Class && - areAllOuterTypeParametersApplied(originalBaseType!)) { - // When base constructor type is a class with no captured type arguments we know that the constructors all have the same type parameters as the - // class and all return the instance type of the class. There is no need for further checks and we can apply the - // type arguments in the same manner as a type reference to get the same error reporting experience. - baseType = getTypeFromClassOrInterfaceReference(baseTypeNode, baseConstructorType.symbol); - } - else if (baseConstructorType.flags & TypeFlags.Any) { - baseType = baseConstructorType; - } - else { - // The class derives from a "class-like" constructor function, check that we have at least one construct signature - // with a matching number of type parameters and use the return type of the first instantiated signature. Elsewhere - // we check that all instantiated signatures return the same type. - const constructors = getInstantiatedConstructorsForTypeArguments(baseConstructorType, baseTypeNode.typeArguments, baseTypeNode); - if (!constructors.length) { - error(baseTypeNode.expression, Diagnostics.No_base_constructor_has_the_specified_number_of_type_arguments); - return type.resolvedBaseTypes = emptyArray; - } - baseType = getReturnTypeOfSignature(constructors[0]); - } - - if (baseType === errorType) { - return type.resolvedBaseTypes = emptyArray; - } - if (!isValidBaseType(baseType)) { - error(baseTypeNode.expression, Diagnostics.Base_constructor_return_type_0_is_not_an_object_type_or_intersection_of_object_types_with_statically_known_members, typeToString(baseType)); - return type.resolvedBaseTypes = emptyArray; - } - if (type === baseType || hasBaseType(baseType, type)) { - error(type.symbol.valueDeclaration, Diagnostics.Type_0_recursively_references_itself_as_a_base_type, - typeToString(type, /*enclosingDeclaration*/ undefined, TypeFormatFlags.WriteArrayAsGenericType)); - return type.resolvedBaseTypes = emptyArray; - } - if (type.resolvedBaseTypes === resolvingEmptyArray) { - // Circular reference, likely through instantiation of default parameters - // (otherwise there'd be an error from hasBaseType) - this is fine, but `.members` should be reset - // as `getIndexedAccessType` via `instantiateType` via `getTypeFromClassOrInterfaceReference` forces a - // partial instantiation of the members without the base types fully resolved - type.members = undefined; - } - return type.resolvedBaseTypes = [baseType]; - } - - function areAllOuterTypeParametersApplied(type: Type): boolean { // TODO: GH#18217 Shouldn't this take an InterfaceType? - // An unapplied type parameter has its symbol still the same as the matching argument symbol. - // Since parameters are applied outer-to-inner, only the last outer parameter needs to be checked. - const outerTypeParameters = (type).outerTypeParameters; - if (outerTypeParameters) { - const last = outerTypeParameters.length - 1; - const typeArguments = getTypeArguments(type); - return outerTypeParameters[last].symbol !== typeArguments[last].symbol; - } - return true; - } - - // A valid base type is `any`, an object type or intersection of object types. - function isValidBaseType(type: Type): type is BaseType { - if (type.flags & TypeFlags.TypeParameter) { - const constraint = getBaseConstraintOfType(type); - if (constraint) { - return isValidBaseType(constraint); - } - } - // TODO: Given that we allow type parmeters here now, is this `!isGenericMappedType(type)` check really needed? - // There's no reason a `T` should be allowed while a `Readonly` should not. - return !!(type.flags & (TypeFlags.Object | TypeFlags.NonPrimitive | TypeFlags.Any)) && !isGenericMappedType(type) || - !!(type.flags & TypeFlags.Intersection) && every((type).types, isValidBaseType); - } - - function resolveBaseTypesOfInterface(type: InterfaceType): void { - type.resolvedBaseTypes = type.resolvedBaseTypes || emptyArray; - for (const declaration of type.symbol.declarations) { - if (declaration.kind === SyntaxKind.InterfaceDeclaration && getInterfaceBaseTypeNodes(declaration)) { - for (const node of getInterfaceBaseTypeNodes(declaration)!) { - const baseType = getTypeFromTypeNode(node); - if (baseType !== errorType) { - if (isValidBaseType(baseType)) { - if (type !== baseType && !hasBaseType(baseType, type)) { - if (type.resolvedBaseTypes === emptyArray) { - type.resolvedBaseTypes = [baseType]; - } - else { - type.resolvedBaseTypes.push(baseType); - } - } - else { - error(declaration, Diagnostics.Type_0_recursively_references_itself_as_a_base_type, typeToString(type, /*enclosingDeclaration*/ undefined, TypeFormatFlags.WriteArrayAsGenericType)); - } - } - else { - error(node, Diagnostics.An_interface_can_only_extend_an_object_type_or_intersection_of_object_types_with_statically_known_members); - } - } - } - } - } - } - - /** - * Returns true if the interface given by the symbol is free of "this" references. - * - * Specifically, the result is true if the interface itself contains no references - * to "this" in its body, if all base types are interfaces, - * and if none of the base interfaces have a "this" type. - */ - function isThislessInterface(symbol: Symbol): boolean { - for (const declaration of symbol.declarations) { - if (declaration.kind === SyntaxKind.InterfaceDeclaration) { - if (declaration.flags & NodeFlags.ContainsThis) { - return false; - } - const baseTypeNodes = getInterfaceBaseTypeNodes(declaration); - if (baseTypeNodes) { - for (const node of baseTypeNodes) { - if (isEntityNameExpression(node.expression)) { - const baseSymbol = resolveEntityName(node.expression, SymbolFlags.Type, /*ignoreErrors*/ true); - if (!baseSymbol || !(baseSymbol.flags & SymbolFlags.Interface) || getDeclaredTypeOfClassOrInterface(baseSymbol).thisType) { - return false; - } - } - } - } - } - } - return true; - } - - function getDeclaredTypeOfClassOrInterface(symbol: Symbol): InterfaceType { - let links = getSymbolLinks(symbol); - const originalLinks = links; - if (!links.declaredType) { - const kind = symbol.flags & SymbolFlags.Class ? ObjectFlags.Class : ObjectFlags.Interface; - const merged = mergeJSSymbols(symbol, getAssignedClassSymbol(symbol.valueDeclaration)); - if (merged) { - // note:we overwrite links because we just cloned the symbol - symbol = links = merged; - } - - const type = originalLinks.declaredType = links.declaredType = createObjectType(kind, symbol); - const outerTypeParameters = getOuterTypeParametersOfClassOrInterface(symbol); - const localTypeParameters = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol); - // A class or interface is generic if it has type parameters or a "this" type. We always give classes a "this" type - // because it is not feasible to analyze all members to determine if the "this" type escapes the class (in particular, - // property types inferred from initializers and method return types inferred from return statements are very hard - // to exhaustively analyze). We give interfaces a "this" type if we can't definitely determine that they are free of - // "this" references. - if (outerTypeParameters || localTypeParameters || kind === ObjectFlags.Class || !isThislessInterface(symbol)) { - type.objectFlags |= ObjectFlags.Reference; - type.typeParameters = concatenate(outerTypeParameters, localTypeParameters); - type.outerTypeParameters = outerTypeParameters; - type.localTypeParameters = localTypeParameters; - (type).instantiations = createMap(); - (type).instantiations.set(getTypeListId(type.typeParameters), type); - (type).target = type; - (type).resolvedTypeArguments = type.typeParameters; - type.thisType = createTypeParameter(symbol); - type.thisType.isThisType = true; - type.thisType.constraint = type; - } - } - return links.declaredType; - } - - function getDeclaredTypeOfTypeAlias(symbol: Symbol): Type { - const links = getSymbolLinks(symbol); - if (!links.declaredType) { - // Note that we use the links object as the target here because the symbol object is used as the unique - // identity for resolution of the 'type' property in SymbolLinks. - if (!pushTypeResolution(symbol, TypeSystemPropertyName.DeclaredType)) { - return errorType; - } - - const declaration = Debug.checkDefined(find(symbol.declarations, isTypeAlias), "Type alias symbol with no valid declaration found"); - const typeNode = isJSDocTypeAlias(declaration) ? declaration.typeExpression : declaration.type; - // If typeNode is missing, we will error in checkJSDocTypedefTag. - let type = typeNode ? getTypeFromTypeNode(typeNode) : errorType; - - if (popTypeResolution()) { - const typeParameters = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol); - if (typeParameters) { - // Initialize the instantiation cache for generic type aliases. The declared type corresponds to - // an instantiation of the type alias with the type parameters supplied as type arguments. - links.typeParameters = typeParameters; - links.instantiations = createMap(); - links.instantiations.set(getTypeListId(typeParameters), type); - } - } - else { - type = errorType; - error(isNamedDeclaration(declaration) ? declaration.name : declaration || declaration, Diagnostics.Type_alias_0_circularly_references_itself, symbolToString(symbol)); - } - links.declaredType = type; - } - return links.declaredType; - } - - function isStringConcatExpression(expr: Node): boolean { - if (isStringLiteralLike(expr)) { - return true; - } - else if (expr.kind === SyntaxKind.BinaryExpression) { - return isStringConcatExpression((expr).left) && isStringConcatExpression((expr).right); - } - return false; - } - - function isLiteralEnumMember(member: EnumMember) { - const expr = member.initializer; - if (!expr) { - return !(member.flags & NodeFlags.Ambient); - } - switch (expr.kind) { - case SyntaxKind.StringLiteral: - case SyntaxKind.NumericLiteral: - case SyntaxKind.NoSubstitutionTemplateLiteral: - return true; - case SyntaxKind.PrefixUnaryExpression: - return (expr).operator === SyntaxKind.MinusToken && - (expr).operand.kind === SyntaxKind.NumericLiteral; - case SyntaxKind.Identifier: - return nodeIsMissing(expr) || !!getSymbolOfNode(member.parent).exports!.get((expr).escapedText); - case SyntaxKind.BinaryExpression: - return isStringConcatExpression(expr); - default: - return false; - } - } - - function getEnumKind(symbol: Symbol): EnumKind { - const links = getSymbolLinks(symbol); - if (links.enumKind !== undefined) { - return links.enumKind; - } - let hasNonLiteralMember = false; - for (const declaration of symbol.declarations) { - if (declaration.kind === SyntaxKind.EnumDeclaration) { - for (const member of (declaration).members) { - if (member.initializer && isStringLiteralLike(member.initializer)) { - return links.enumKind = EnumKind.Literal; - } - if (!isLiteralEnumMember(member)) { - hasNonLiteralMember = true; - } - } - } - } - return links.enumKind = hasNonLiteralMember ? EnumKind.Numeric : EnumKind.Literal; - } - - function getBaseTypeOfEnumLiteralType(type: Type) { - return type.flags & TypeFlags.EnumLiteral && !(type.flags & TypeFlags.Union) ? getDeclaredTypeOfSymbol(getParentOfSymbol(type.symbol)!) : type; - } - - function getDeclaredTypeOfEnum(symbol: Symbol): Type { - const links = getSymbolLinks(symbol); - if (links.declaredType) { - return links.declaredType; - } - if (getEnumKind(symbol) === EnumKind.Literal) { - enumCount++; - const memberTypeList: Type[] = []; - for (const declaration of symbol.declarations) { - if (declaration.kind === SyntaxKind.EnumDeclaration) { - for (const member of (declaration).members) { - const value = getEnumMemberValue(member); - const memberType = getFreshTypeOfLiteralType(getLiteralType(value !== undefined ? value : 0, enumCount, getSymbolOfNode(member))); - getSymbolLinks(getSymbolOfNode(member)).declaredType = memberType; - memberTypeList.push(getRegularTypeOfLiteralType(memberType)); - } - } - } - if (memberTypeList.length) { - const enumType = getUnionType(memberTypeList, UnionReduction.Literal, symbol, /*aliasTypeArguments*/ undefined); - if (enumType.flags & TypeFlags.Union) { - enumType.flags |= TypeFlags.EnumLiteral; - enumType.symbol = symbol; - } - return links.declaredType = enumType; - } - } - const enumType = createType(TypeFlags.Enum); - enumType.symbol = symbol; - return links.declaredType = enumType; - } - - function getDeclaredTypeOfEnumMember(symbol: Symbol): Type { - const links = getSymbolLinks(symbol); - if (!links.declaredType) { - const enumType = getDeclaredTypeOfEnum(getParentOfSymbol(symbol)!); - if (!links.declaredType) { - links.declaredType = enumType; - } - } - return links.declaredType; - } - - function getDeclaredTypeOfTypeParameter(symbol: Symbol): TypeParameter { - const links = getSymbolLinks(symbol); - return links.declaredType || (links.declaredType = createTypeParameter(symbol)); - } - - function getDeclaredTypeOfAlias(symbol: Symbol): Type { - const links = getSymbolLinks(symbol); - return links.declaredType || (links.declaredType = getDeclaredTypeOfSymbol(resolveAlias(symbol))); - } - - function getDeclaredTypeOfSymbol(symbol: Symbol): Type { - return tryGetDeclaredTypeOfSymbol(symbol) || errorType; - } - - function tryGetDeclaredTypeOfSymbol(symbol: Symbol): Type | undefined { - if (symbol.flags & (SymbolFlags.Class | SymbolFlags.Interface)) { - return getDeclaredTypeOfClassOrInterface(symbol); - } - if (symbol.flags & SymbolFlags.TypeAlias) { - return getDeclaredTypeOfTypeAlias(symbol); - } - if (symbol.flags & SymbolFlags.TypeParameter) { - return getDeclaredTypeOfTypeParameter(symbol); - } - if (symbol.flags & SymbolFlags.Enum) { - return getDeclaredTypeOfEnum(symbol); - } - if (symbol.flags & SymbolFlags.EnumMember) { - return getDeclaredTypeOfEnumMember(symbol); - } - if (symbol.flags & SymbolFlags.Alias) { - return getDeclaredTypeOfAlias(symbol); - } - return undefined; - } - - /** - * A type is free of this references if it's the any, string, number, boolean, symbol, or void keyword, a string - * literal type, an array with an element type that is free of this references, or a type reference that is - * free of this references. - */ - function isThislessType(node: TypeNode): boolean { - switch (node.kind) { - case SyntaxKind.AnyKeyword: - case SyntaxKind.UnknownKeyword: - case SyntaxKind.StringKeyword: - case SyntaxKind.NumberKeyword: - case SyntaxKind.BigIntKeyword: - case SyntaxKind.BooleanKeyword: - case SyntaxKind.SymbolKeyword: - case SyntaxKind.ObjectKeyword: - case SyntaxKind.VoidKeyword: - case SyntaxKind.UndefinedKeyword: - case SyntaxKind.NullKeyword: - case SyntaxKind.NeverKeyword: - case SyntaxKind.LiteralType: - return true; - case SyntaxKind.ArrayType: - return isThislessType((node).elementType); - case SyntaxKind.TypeReference: - return !(node as TypeReferenceNode).typeArguments || (node as TypeReferenceNode).typeArguments!.every(isThislessType); - } - return false; - } - - /** A type parameter is thisless if its constraint is thisless, or if it has no constraint. */ - function isThislessTypeParameter(node: TypeParameterDeclaration) { - const constraint = getEffectiveConstraintOfTypeParameter(node); - return !constraint || isThislessType(constraint); - } - - /** - * A variable-like declaration is free of this references if it has a type annotation - * that is thisless, or if it has no type annotation and no initializer (and is thus of type any). - */ - function isThislessVariableLikeDeclaration(node: VariableLikeDeclaration): boolean { - const typeNode = getEffectiveTypeAnnotationNode(node); - return typeNode ? isThislessType(typeNode) : !hasInitializer(node); - } - - /** - * A function-like declaration is considered free of `this` references if it has a return type - * annotation that is free of this references and if each parameter is thisless and if - * each type parameter (if present) is thisless. - */ - function isThislessFunctionLikeDeclaration(node: FunctionLikeDeclaration): boolean { - const returnType = getEffectiveReturnTypeNode(node); - const typeParameters = getEffectiveTypeParameterDeclarations(node); - return (node.kind === SyntaxKind.Constructor || (!!returnType && isThislessType(returnType))) && - node.parameters.every(isThislessVariableLikeDeclaration) && - typeParameters.every(isThislessTypeParameter); - } - - /** - * Returns true if the class or interface member given by the symbol is free of "this" references. The - * function may return false for symbols that are actually free of "this" references because it is not - * feasible to perform a complete analysis in all cases. In particular, property members with types - * inferred from their initializers and function members with inferred return types are conservatively - * assumed not to be free of "this" references. - */ - function isThisless(symbol: Symbol): boolean { - if (symbol.declarations && symbol.declarations.length === 1) { - const declaration = symbol.declarations[0]; - if (declaration) { - switch (declaration.kind) { - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.PropertySignature: - return isThislessVariableLikeDeclaration(declaration); - case SyntaxKind.MethodDeclaration: - case SyntaxKind.MethodSignature: - case SyntaxKind.Constructor: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - return isThislessFunctionLikeDeclaration(declaration); - } - } - } - return false; - } - - // The mappingThisOnly flag indicates that the only type parameter being mapped is "this". When the flag is true, - // we check symbols to see if we can quickly conclude they are free of "this" references, thus needing no instantiation. - function createInstantiatedSymbolTable(symbols: Symbol[], mapper: TypeMapper, mappingThisOnly: boolean): SymbolTable { - const result = createSymbolTable(); - for (const symbol of symbols) { - result.set(symbol.escapedName, mappingThisOnly && isThisless(symbol) ? symbol : instantiateSymbol(symbol, mapper)); - } - return result; - } - - function addInheritedMembers(symbols: SymbolTable, baseSymbols: Symbol[]) { - for (const s of baseSymbols) { - if (!symbols.has(s.escapedName) && !isStaticPrivateIdentifierProperty(s)) { - symbols.set(s.escapedName, s); - } - } - } - - function isStaticPrivateIdentifierProperty(s: Symbol): boolean { - return !!s.valueDeclaration && isPrivateIdentifierPropertyDeclaration(s.valueDeclaration) && hasModifier(s.valueDeclaration, ModifierFlags.Static); - } - - function resolveDeclaredMembers(type: InterfaceType): InterfaceTypeWithDeclaredMembers { - if (!(type).declaredProperties) { - const symbol = type.symbol; - const members = getMembersOfSymbol(symbol); - (type).declaredProperties = getNamedMembers(members); - // Start with signatures at empty array in case of recursive types - (type).declaredCallSignatures = emptyArray; - (type).declaredConstructSignatures = emptyArray; - - (type).declaredCallSignatures = getSignaturesOfSymbol(members.get(InternalSymbolName.Call)); - (type).declaredConstructSignatures = getSignaturesOfSymbol(members.get(InternalSymbolName.New)); - (type).declaredStringIndexInfo = getIndexInfoOfSymbol(symbol, IndexKind.String); - (type).declaredNumberIndexInfo = getIndexInfoOfSymbol(symbol, IndexKind.Number); - } - return type; - } - - /** - * Indicates whether a type can be used as a property name. - */ - function isTypeUsableAsPropertyName(type: Type): type is StringLiteralType | NumberLiteralType | UniqueESSymbolType { - return !!(type.flags & TypeFlags.StringOrNumberLiteralOrUnique); - } - - /** - * Indicates whether a declaration name is definitely late-bindable. - * A declaration name is only late-bindable if: - * - It is a `ComputedPropertyName`. - * - Its expression is an `Identifier` or either a `PropertyAccessExpression` an - * `ElementAccessExpression` consisting only of these same three types of nodes. - * - The type of its expression is a string or numeric literal type, or is a `unique symbol` type. - */ - function isLateBindableName(node: DeclarationName): node is LateBoundName { - if (!isComputedPropertyName(node) && !isElementAccessExpression(node)) { - return false; - } - const expr = isComputedPropertyName(node) ? node.expression : node.argumentExpression; - return isEntityNameExpression(expr) - && isTypeUsableAsPropertyName(isComputedPropertyName(node) ? checkComputedPropertyName(node) : checkExpressionCached(expr)); - } - - function isLateBoundName(name: __String): boolean { - return (name as string).charCodeAt(0) === CharacterCodes._ && - (name as string).charCodeAt(1) === CharacterCodes._ && - (name as string).charCodeAt(2) === CharacterCodes.at; - } - - /** - * Indicates whether a declaration has a late-bindable dynamic name. - */ - function hasLateBindableName(node: Declaration): node is LateBoundDeclaration | LateBoundBinaryExpressionDeclaration { - const name = getNameOfDeclaration(node); - return !!name && isLateBindableName(name); - } - - /** - * Indicates whether a declaration has a dynamic name that cannot be late-bound. - */ - function hasNonBindableDynamicName(node: Declaration) { - return hasDynamicName(node) && !hasLateBindableName(node); - } - - /** - * Indicates whether a declaration name is a dynamic name that cannot be late-bound. - */ - function isNonBindableDynamicName(node: DeclarationName) { - return isDynamicName(node) && !isLateBindableName(node); - } - - /** - * Gets the symbolic name for a member from its type. - */ - function getPropertyNameFromType(type: StringLiteralType | NumberLiteralType | UniqueESSymbolType): __String { - if (type.flags & TypeFlags.UniqueESSymbol) { - return (type).escapedName; - } - if (type.flags & (TypeFlags.StringLiteral | TypeFlags.NumberLiteral)) { - return escapeLeadingUnderscores("" + (type).value); - } - return Debug.fail(); - } - - /** - * Adds a declaration to a late-bound dynamic member. This performs the same function for - * late-bound members that `addDeclarationToSymbol` in binder.ts performs for early-bound - * members. - */ - function addDeclarationToLateBoundSymbol(symbol: Symbol, member: LateBoundDeclaration | BinaryExpression, symbolFlags: SymbolFlags) { - Debug.assert(!!(getCheckFlags(symbol) & CheckFlags.Late), "Expected a late-bound symbol."); - symbol.flags |= symbolFlags; - getSymbolLinks(member.symbol).lateSymbol = symbol; - if (!symbol.declarations) { - symbol.declarations = [member]; - } - else { - symbol.declarations.push(member); - } - if (symbolFlags & SymbolFlags.Value) { - if (!symbol.valueDeclaration || symbol.valueDeclaration.kind !== member.kind) { - symbol.valueDeclaration = member; - } - } - } - - /** - * Performs late-binding of a dynamic member. This performs the same function for - * late-bound members that `declareSymbol` in binder.ts performs for early-bound - * members. - * - * If a symbol is a dynamic name from a computed property, we perform an additional "late" - * binding phase to attempt to resolve the name for the symbol from the type of the computed - * property's expression. If the type of the expression is a string-literal, numeric-literal, - * or unique symbol type, we can use that type as the name of the symbol. - * - * For example, given: - * - * const x = Symbol(); - * - * interface I { - * [x]: number; - * } - * - * The binder gives the property `[x]: number` a special symbol with the name "__computed". - * In the late-binding phase we can type-check the expression `x` and see that it has a - * unique symbol type which we can then use as the name of the member. This allows users - * to define custom symbols that can be used in the members of an object type. - * - * @param parent The containing symbol for the member. - * @param earlySymbols The early-bound symbols of the parent. - * @param lateSymbols The late-bound symbols of the parent. - * @param decl The member to bind. - */ - function lateBindMember(parent: Symbol, earlySymbols: SymbolTable | undefined, lateSymbols: UnderscoreEscapedMap, decl: LateBoundDeclaration | LateBoundBinaryExpressionDeclaration) { - Debug.assert(!!decl.symbol, "The member is expected to have a symbol."); - const links = getNodeLinks(decl); - if (!links.resolvedSymbol) { - // In the event we attempt to resolve the late-bound name of this member recursively, - // fall back to the early-bound name of this member. - links.resolvedSymbol = decl.symbol; - const declName = isBinaryExpression(decl) ? decl.left : decl.name; - const type = isElementAccessExpression(declName) ? checkExpressionCached(declName.argumentExpression) : checkComputedPropertyName(declName); - if (isTypeUsableAsPropertyName(type)) { - const memberName = getPropertyNameFromType(type); - const symbolFlags = decl.symbol.flags; - - // Get or add a late-bound symbol for the member. This allows us to merge late-bound accessor declarations. - let lateSymbol = lateSymbols.get(memberName); - if (!lateSymbol) lateSymbols.set(memberName, lateSymbol = createSymbol(SymbolFlags.None, memberName, CheckFlags.Late)); - - // Report an error if a late-bound member has the same name as an early-bound member, - // or if we have another early-bound symbol declaration with the same name and - // conflicting flags. - const earlySymbol = earlySymbols && earlySymbols.get(memberName); - if (lateSymbol.flags & getExcludedSymbolFlags(symbolFlags) || earlySymbol) { - // If we have an existing early-bound member, combine its declarations so that we can - // report an error at each declaration. - const declarations = earlySymbol ? concatenate(earlySymbol.declarations, lateSymbol.declarations) : lateSymbol.declarations; - const name = !(type.flags & TypeFlags.UniqueESSymbol) && unescapeLeadingUnderscores(memberName) || declarationNameToString(declName); - forEach(declarations, declaration => error(getNameOfDeclaration(declaration) || declaration, Diagnostics.Property_0_was_also_declared_here, name)); - error(declName || decl, Diagnostics.Duplicate_property_0, name); - lateSymbol = createSymbol(SymbolFlags.None, memberName, CheckFlags.Late); - } - lateSymbol.nameType = type; - addDeclarationToLateBoundSymbol(lateSymbol, decl, symbolFlags); - if (lateSymbol.parent) { - Debug.assert(lateSymbol.parent === parent, "Existing symbol parent should match new one"); - } - else { - lateSymbol.parent = parent; - } - return links.resolvedSymbol = lateSymbol; - } - } - return links.resolvedSymbol; - } - - function getResolvedMembersOrExportsOfSymbol(symbol: Symbol, resolutionKind: MembersOrExportsResolutionKind): UnderscoreEscapedMap { - const links = getSymbolLinks(symbol); - if (!links[resolutionKind]) { - const isStatic = resolutionKind === MembersOrExportsResolutionKind.resolvedExports; - const earlySymbols = !isStatic ? symbol.members : - symbol.flags & SymbolFlags.Module ? getExportsOfModuleWorker(symbol) : - symbol.exports; - - // In the event we recursively resolve the members/exports of the symbol, we - // set the initial value of resolvedMembers/resolvedExports to the early-bound - // members/exports of the symbol. - links[resolutionKind] = earlySymbols || emptySymbols; - - // fill in any as-yet-unresolved late-bound members. - const lateSymbols = createSymbolTable() as UnderscoreEscapedMap; - for (const decl of symbol.declarations) { - const members = getMembersOfDeclaration(decl); - if (members) { - for (const member of members) { - if (isStatic === hasStaticModifier(member) && hasLateBindableName(member)) { - lateBindMember(symbol, earlySymbols, lateSymbols, member); - } - } - } - } - const assignments = symbol.assignmentDeclarationMembers; - if (assignments) { - const decls = arrayFrom(assignments.values()); - for (const member of decls) { - const assignmentKind = getAssignmentDeclarationKind(member as BinaryExpression | CallExpression); - const isInstanceMember = assignmentKind === AssignmentDeclarationKind.PrototypeProperty - || assignmentKind === AssignmentDeclarationKind.ThisProperty - || assignmentKind === AssignmentDeclarationKind.ObjectDefinePrototypeProperty - || assignmentKind === AssignmentDeclarationKind.Prototype; // A straight `Prototype` assignment probably can never have a computed name - if (isStatic === !isInstanceMember && hasLateBindableName(member)) { - lateBindMember(symbol, earlySymbols, lateSymbols, member); - } - } - } - - links[resolutionKind] = combineSymbolTables(earlySymbols, lateSymbols) || emptySymbols; - } - - return links[resolutionKind]!; - } - - /** - * Gets a SymbolTable containing both the early- and late-bound members of a symbol. - * - * For a description of late-binding, see `lateBindMember`. - */ - function getMembersOfSymbol(symbol: Symbol) { - return symbol.flags & SymbolFlags.LateBindingContainer - ? getResolvedMembersOrExportsOfSymbol(symbol, MembersOrExportsResolutionKind.resolvedMembers) - : symbol.members || emptySymbols; - } - - /** - * If a symbol is the dynamic name of the member of an object type, get the late-bound - * symbol of the member. - * - * For a description of late-binding, see `lateBindMember`. - */ - function getLateBoundSymbol(symbol: Symbol): Symbol { - if (symbol.flags & SymbolFlags.ClassMember && symbol.escapedName === InternalSymbolName.Computed) { - const links = getSymbolLinks(symbol); - if (!links.lateSymbol && some(symbol.declarations, hasLateBindableName)) { - // force late binding of members/exports. This will set the late-bound symbol - const parent = getMergedSymbol(symbol.parent)!; - if (some(symbol.declarations, hasStaticModifier)) { - getExportsOfSymbol(parent); - } - else { - getMembersOfSymbol(parent); - } - } - return links.lateSymbol || (links.lateSymbol = symbol); - } - return symbol; - } - - function getTypeWithThisArgument(type: Type, thisArgument?: Type, needApparentType?: boolean): Type { - if (getObjectFlags(type) & ObjectFlags.Reference) { - const target = (type).target; - const typeArguments = getTypeArguments(type); - if (length(target.typeParameters) === length(typeArguments)) { - const ref = createTypeReference(target, concatenate(typeArguments, [thisArgument || target.thisType!])); - return needApparentType ? getApparentType(ref) : ref; - } - } - else if (type.flags & TypeFlags.Intersection) { - return getIntersectionType(map((type).types, t => getTypeWithThisArgument(t, thisArgument, needApparentType))); - } - return needApparentType ? getApparentType(type) : type; - } - - function resolveObjectTypeMembers(type: ObjectType, source: InterfaceTypeWithDeclaredMembers, typeParameters: readonly TypeParameter[], typeArguments: readonly Type[]) { - let mapper: TypeMapper; - let members: SymbolTable; - let callSignatures: readonly Signature[]; - let constructSignatures: readonly Signature[] | undefined; - let stringIndexInfo: IndexInfo | undefined; - let numberIndexInfo: IndexInfo | undefined; - if (rangeEquals(typeParameters, typeArguments, 0, typeParameters.length)) { - mapper = identityMapper; - members = source.symbol ? getMembersOfSymbol(source.symbol) : createSymbolTable(source.declaredProperties); - callSignatures = source.declaredCallSignatures; - constructSignatures = source.declaredConstructSignatures; - stringIndexInfo = source.declaredStringIndexInfo; - numberIndexInfo = source.declaredNumberIndexInfo; - } - else { - mapper = createTypeMapper(typeParameters, typeArguments); - members = createInstantiatedSymbolTable(source.declaredProperties, mapper, /*mappingThisOnly*/ typeParameters.length === 1); - callSignatures = instantiateSignatures(source.declaredCallSignatures, mapper); - constructSignatures = instantiateSignatures(source.declaredConstructSignatures, mapper); - stringIndexInfo = instantiateIndexInfo(source.declaredStringIndexInfo, mapper); - numberIndexInfo = instantiateIndexInfo(source.declaredNumberIndexInfo, mapper); - } - const baseTypes = getBaseTypes(source); - if (baseTypes.length) { - if (source.symbol && members === getMembersOfSymbol(source.symbol)) { - members = createSymbolTable(source.declaredProperties); - } - setStructuredTypeMembers(type, members, callSignatures, constructSignatures, stringIndexInfo, numberIndexInfo); - const thisArgument = lastOrUndefined(typeArguments); - for (const baseType of baseTypes) { - const instantiatedBaseType = thisArgument ? getTypeWithThisArgument(instantiateType(baseType, mapper), thisArgument) : baseType; - addInheritedMembers(members, getPropertiesOfType(instantiatedBaseType)); - callSignatures = concatenate(callSignatures, getSignaturesOfType(instantiatedBaseType, SignatureKind.Call)); - constructSignatures = concatenate(constructSignatures, getSignaturesOfType(instantiatedBaseType, SignatureKind.Construct)); - if (!stringIndexInfo) { - stringIndexInfo = instantiatedBaseType === anyType ? - createIndexInfo(anyType, /*isReadonly*/ false) : - getIndexInfoOfType(instantiatedBaseType, IndexKind.String); - } - numberIndexInfo = numberIndexInfo || getIndexInfoOfType(instantiatedBaseType, IndexKind.Number); - } - } - setStructuredTypeMembers(type, members, callSignatures, constructSignatures, stringIndexInfo, numberIndexInfo); - } - - function resolveClassOrInterfaceMembers(type: InterfaceType): void { - resolveObjectTypeMembers(type, resolveDeclaredMembers(type), emptyArray, emptyArray); - } - - function resolveTypeReferenceMembers(type: TypeReference): void { - const source = resolveDeclaredMembers(type.target); - const typeParameters = concatenate(source.typeParameters!, [source.thisType!]); - const typeArguments = getTypeArguments(type); - const paddedTypeArguments = typeArguments.length === typeParameters.length ? typeArguments : concatenate(typeArguments, [type]); - resolveObjectTypeMembers(type, source, typeParameters, paddedTypeArguments); - } - - function createSignature( - declaration: SignatureDeclaration | JSDocSignature | undefined, - typeParameters: readonly TypeParameter[] | undefined, - thisParameter: Symbol | undefined, - parameters: readonly Symbol[], - resolvedReturnType: Type | undefined, - resolvedTypePredicate: TypePredicate | undefined, - minArgumentCount: number, - flags: SignatureFlags - ): Signature { - const sig = new Signature(checker, flags); - sig.declaration = declaration; - sig.typeParameters = typeParameters; - sig.parameters = parameters; - sig.thisParameter = thisParameter; - sig.resolvedReturnType = resolvedReturnType; - sig.resolvedTypePredicate = resolvedTypePredicate; - sig.minArgumentCount = minArgumentCount; - sig.target = undefined; - sig.mapper = undefined; - sig.unionSignatures = undefined; - return sig; - } - - function cloneSignature(sig: Signature): Signature { - const result = createSignature(sig.declaration, sig.typeParameters, sig.thisParameter, sig.parameters, /*resolvedReturnType*/ undefined, - /*resolvedTypePredicate*/ undefined, sig.minArgumentCount, sig.flags & SignatureFlags.PropagatingFlags); - result.target = sig.target; - result.mapper = sig.mapper; - result.unionSignatures = sig.unionSignatures; - return result; - } - - function createUnionSignature(signature: Signature, unionSignatures: Signature[]) { - const result = cloneSignature(signature); - result.unionSignatures = unionSignatures; - result.target = undefined; - result.mapper = undefined; - return result; - } - - function getOptionalCallSignature(signature: Signature, callChainFlags: SignatureFlags): Signature { - if ((signature.flags & SignatureFlags.CallChainFlags) === callChainFlags) { - return signature; - } - if (!signature.optionalCallSignatureCache) { - signature.optionalCallSignatureCache = {}; - } - const key = callChainFlags === SignatureFlags.IsInnerCallChain ? "inner" : "outer"; - return signature.optionalCallSignatureCache[key] - || (signature.optionalCallSignatureCache[key] = createOptionalCallSignature(signature, callChainFlags)); - } - - function createOptionalCallSignature(signature: Signature, callChainFlags: SignatureFlags) { - Debug.assert(callChainFlags === SignatureFlags.IsInnerCallChain || callChainFlags === SignatureFlags.IsOuterCallChain, - "An optional call signature can either be for an inner call chain or an outer call chain, but not both."); - const result = cloneSignature(signature); - result.flags |= callChainFlags; - return result; - } - - function getExpandedParameters(sig: Signature): readonly Symbol[] { - if (signatureHasRestParameter(sig)) { - const restIndex = sig.parameters.length - 1; - const restParameter = sig.parameters[restIndex]; - const restType = getTypeOfSymbol(restParameter); - if (isTupleType(restType)) { - const elementTypes = getTypeArguments(restType); - const minLength = restType.target.minLength; - const tupleRestIndex = restType.target.hasRestElement ? elementTypes.length - 1 : -1; - const restParams = map(elementTypes, (t, i) => { - const name = getParameterNameAtPosition(sig, restIndex + i); - const checkFlags = i === tupleRestIndex ? CheckFlags.RestParameter : - i >= minLength ? CheckFlags.OptionalParameter : 0; - const symbol = createSymbol(SymbolFlags.FunctionScopedVariable, name, checkFlags); - symbol.type = i === tupleRestIndex ? createArrayType(t) : t; - return symbol; - }); - return concatenate(sig.parameters.slice(0, restIndex), restParams); - } - } - return sig.parameters; - } - - function getDefaultConstructSignatures(classType: InterfaceType): Signature[] { - const baseConstructorType = getBaseConstructorTypeOfClass(classType); - const baseSignatures = getSignaturesOfType(baseConstructorType, SignatureKind.Construct); - if (baseSignatures.length === 0) { - return [createSignature(undefined, classType.localTypeParameters, undefined, emptyArray, classType, /*resolvedTypePredicate*/ undefined, 0, SignatureFlags.None)]; - } - const baseTypeNode = getBaseTypeNodeOfClass(classType)!; - const isJavaScript = isInJSFile(baseTypeNode); - const typeArguments = typeArgumentsFromTypeReferenceNode(baseTypeNode); - const typeArgCount = length(typeArguments); - const result: Signature[] = []; - for (const baseSig of baseSignatures) { - const minTypeArgumentCount = getMinTypeArgumentCount(baseSig.typeParameters); - const typeParamCount = length(baseSig.typeParameters); - if (isJavaScript || typeArgCount >= minTypeArgumentCount && typeArgCount <= typeParamCount) { - const sig = typeParamCount ? createSignatureInstantiation(baseSig, fillMissingTypeArguments(typeArguments, baseSig.typeParameters, minTypeArgumentCount, isJavaScript)) : cloneSignature(baseSig); - sig.typeParameters = classType.localTypeParameters; - sig.resolvedReturnType = classType; - result.push(sig); - } - } - return result; - } - - function findMatchingSignature(signatureList: readonly Signature[], signature: Signature, partialMatch: boolean, ignoreThisTypes: boolean, ignoreReturnTypes: boolean): Signature | undefined { - for (const s of signatureList) { - if (compareSignaturesIdentical(s, signature, partialMatch, ignoreThisTypes, ignoreReturnTypes, partialMatch ? compareTypesSubtypeOf : compareTypesIdentical)) { - return s; - } - } - } - - function findMatchingSignatures(signatureLists: readonly (readonly Signature[])[], signature: Signature, listIndex: number): Signature[] | undefined { - if (signature.typeParameters) { - // We require an exact match for generic signatures, so we only return signatures from the first - // signature list and only if they have exact matches in the other signature lists. - if (listIndex > 0) { - return undefined; - } - for (let i = 1; i < signatureLists.length; i++) { - if (!findMatchingSignature(signatureLists[i], signature, /*partialMatch*/ false, /*ignoreThisTypes*/ false, /*ignoreReturnTypes*/ false)) { - return undefined; - } - } - return [signature]; - } - let result: Signature[] | undefined; - for (let i = 0; i < signatureLists.length; i++) { - // Allow matching non-generic signatures to have excess parameters and different return types. - // Prefer matching this types if possible. - const match = i === listIndex ? signature : findMatchingSignature(signatureLists[i], signature, /*partialMatch*/ true, /*ignoreThisTypes*/ false, /*ignoreReturnTypes*/ true); - if (!match) { - return undefined; - } - result = appendIfUnique(result, match); - } - return result; - } - - // The signatures of a union type are those signatures that are present in each of the constituent types. - // Generic signatures must match exactly, but non-generic signatures are allowed to have extra optional - // parameters and may differ in return types. When signatures differ in return types, the resulting return - // type is the union of the constituent return types. - function getUnionSignatures(signatureLists: readonly (readonly Signature[])[]): Signature[] { - let result: Signature[] | undefined; - let indexWithLengthOverOne: number | undefined; - for (let i = 0; i < signatureLists.length; i++) { - if (signatureLists[i].length === 0) return emptyArray; - if (signatureLists[i].length > 1) { - indexWithLengthOverOne = indexWithLengthOverOne === undefined ? i : -1; // -1 is a signal there are multiple overload sets - } - for (const signature of signatureLists[i]) { - // Only process signatures with parameter lists that aren't already in the result list - if (!result || !findMatchingSignature(result, signature, /*partialMatch*/ false, /*ignoreThisTypes*/ false, /*ignoreReturnTypes*/ true)) { - const unionSignatures = findMatchingSignatures(signatureLists, signature, i); - if (unionSignatures) { - let s = signature; - // Union the result types when more than one signature matches - if (unionSignatures.length > 1) { - let thisParameter = signature.thisParameter; - const firstThisParameterOfUnionSignatures = forEach(unionSignatures, sig => sig.thisParameter); - if (firstThisParameterOfUnionSignatures) { - const thisType = getIntersectionType(mapDefined(unionSignatures, sig => sig.thisParameter && getTypeOfSymbol(sig.thisParameter))); - thisParameter = createSymbolWithType(firstThisParameterOfUnionSignatures, thisType); - } - s = createUnionSignature(signature, unionSignatures); - s.thisParameter = thisParameter; - } - (result || (result = [])).push(s); - } - } - } - } - if (!length(result) && indexWithLengthOverOne !== -1) { - // No sufficiently similar signature existed to subsume all the other signatures in the union - time to see if we can make a single - // signature that handles all over them. We only do this when there are overloads in only one constituent. - // (Overloads are conditional in nature and having overloads in multiple constituents would necessitate making a power set of - // signatures from the type, whose ordering would be non-obvious) - const masterList = signatureLists[indexWithLengthOverOne !== undefined ? indexWithLengthOverOne : 0]; - let results: Signature[] | undefined = masterList.slice(); - for (const signatures of signatureLists) { - if (signatures !== masterList) { - const signature = signatures[0]; - Debug.assert(!!signature, "getUnionSignatures bails early on empty signature lists and should not have empty lists on second pass"); - results = signature.typeParameters && some(results, s => !!s.typeParameters) ? undefined : map(results, sig => combineSignaturesOfUnionMembers(sig, signature)); - if (!results) { - break; - } - } - } - result = results; - } - return result || emptyArray; - } - - function combineUnionThisParam(left: Symbol | undefined, right: Symbol | undefined): Symbol | undefined { - if (!left || !right) { - return left || right; - } - // A signature `this` type might be a read or a write position... It's very possible that it should be invariant - // and we should refuse to merge signatures if there are `this` types and they do not match. However, so as to be - // permissive when calling, for now, we'll intersect the `this` types just like we do for param types in union signatures. - const thisType = getIntersectionType([getTypeOfSymbol(left), getTypeOfSymbol(right)]); - return createSymbolWithType(left, thisType); - } - - function combineUnionParameters(left: Signature, right: Signature) { - const leftCount = getParameterCount(left); - const rightCount = getParameterCount(right); - const longest = leftCount >= rightCount ? left : right; - const shorter = longest === left ? right : left; - const longestCount = longest === left ? leftCount : rightCount; - const eitherHasEffectiveRest = (hasEffectiveRestParameter(left) || hasEffectiveRestParameter(right)); - const needsExtraRestElement = eitherHasEffectiveRest && !hasEffectiveRestParameter(longest); - const params = new Array(longestCount + (needsExtraRestElement ? 1 : 0)); - for (let i = 0; i < longestCount; i++) { - const longestParamType = tryGetTypeAtPosition(longest, i)!; - const shorterParamType = tryGetTypeAtPosition(shorter, i) || unknownType; - const unionParamType = getIntersectionType([longestParamType, shorterParamType]); - const isRestParam = eitherHasEffectiveRest && !needsExtraRestElement && i === (longestCount - 1); - const isOptional = i >= getMinArgumentCount(longest) && i >= getMinArgumentCount(shorter); - const leftName = i >= leftCount ? undefined : getParameterNameAtPosition(left, i); - const rightName = i >= rightCount ? undefined : getParameterNameAtPosition(right, i); - - const paramName = leftName === rightName ? leftName : - !leftName ? rightName : - !rightName ? leftName : - undefined; - const paramSymbol = createSymbol( - SymbolFlags.FunctionScopedVariable | (isOptional && !isRestParam ? SymbolFlags.Optional : 0), - paramName || `arg${i}` as __String - ); - paramSymbol.type = isRestParam ? createArrayType(unionParamType) : unionParamType; - params[i] = paramSymbol; - } - if (needsExtraRestElement) { - const restParamSymbol = createSymbol(SymbolFlags.FunctionScopedVariable, "args" as __String); - restParamSymbol.type = createArrayType(getTypeAtPosition(shorter, longestCount)); - params[longestCount] = restParamSymbol; - } - return params; - } - - function combineSignaturesOfUnionMembers(left: Signature, right: Signature): Signature { - const declaration = left.declaration; - const params = combineUnionParameters(left, right); - const thisParam = combineUnionThisParam(left.thisParameter, right.thisParameter); - const minArgCount = Math.max(left.minArgumentCount, right.minArgumentCount); - const result = createSignature( - declaration, - left.typeParameters || right.typeParameters, - thisParam, - params, - /*resolvedReturnType*/ undefined, - /*resolvedTypePredicate*/ undefined, - minArgCount, - (left.flags | right.flags) & SignatureFlags.PropagatingFlags - ); - result.unionSignatures = concatenate(left.unionSignatures || [left], [right]); - return result; - } - - function getUnionIndexInfo(types: readonly Type[], kind: IndexKind): IndexInfo | undefined { - const indexTypes: Type[] = []; - let isAnyReadonly = false; - for (const type of types) { - const indexInfo = getIndexInfoOfType(type, kind); - if (!indexInfo) { - return undefined; - } - indexTypes.push(indexInfo.type); - isAnyReadonly = isAnyReadonly || indexInfo.isReadonly; - } - return createIndexInfo(getUnionType(indexTypes, UnionReduction.Subtype), isAnyReadonly); - } - - function resolveUnionTypeMembers(type: UnionType) { - // The members and properties collections are empty for union types. To get all properties of a union - // type use getPropertiesOfType (only the language service uses this). - const callSignatures = getUnionSignatures(map(type.types, t => t === globalFunctionType ? [unknownSignature] : getSignaturesOfType(t, SignatureKind.Call))); - const constructSignatures = getUnionSignatures(map(type.types, t => getSignaturesOfType(t, SignatureKind.Construct))); - const stringIndexInfo = getUnionIndexInfo(type.types, IndexKind.String); - const numberIndexInfo = getUnionIndexInfo(type.types, IndexKind.Number); - setStructuredTypeMembers(type, emptySymbols, callSignatures, constructSignatures, stringIndexInfo, numberIndexInfo); - } - - function intersectTypes(type1: Type, type2: Type): Type; - function intersectTypes(type1: Type | undefined, type2: Type | undefined): Type | undefined; - function intersectTypes(type1: Type | undefined, type2: Type | undefined): Type | undefined { - return !type1 ? type2 : !type2 ? type1 : getIntersectionType([type1, type2]); - } - - function intersectIndexInfos(info1: IndexInfo | undefined, info2: IndexInfo | undefined): IndexInfo | undefined { - return !info1 ? info2 : !info2 ? info1 : createIndexInfo( - getIntersectionType([info1.type, info2.type]), info1.isReadonly && info2.isReadonly); - } - - function unionSpreadIndexInfos(info1: IndexInfo | undefined, info2: IndexInfo | undefined): IndexInfo | undefined { - return info1 && info2 && createIndexInfo( - getUnionType([info1.type, info2.type]), info1.isReadonly || info2.isReadonly); - } - - function findMixins(types: readonly Type[]): readonly boolean[] { - const constructorTypeCount = countWhere(types, (t) => getSignaturesOfType(t, SignatureKind.Construct).length > 0); - const mixinFlags = map(types, isMixinConstructorType); - if (constructorTypeCount > 0 && constructorTypeCount === countWhere(mixinFlags, (b) => b)) { - const firstMixinIndex = mixinFlags.indexOf(/*searchElement*/ true); - mixinFlags[firstMixinIndex] = false; - } - return mixinFlags; - } - - function includeMixinType(type: Type, types: readonly Type[], mixinFlags: readonly boolean[], index: number): Type { - const mixedTypes: Type[] = []; - for (let i = 0; i < types.length; i++) { - if (i === index) { - mixedTypes.push(type); - } - else if (mixinFlags[i]) { - mixedTypes.push(getReturnTypeOfSignature(getSignaturesOfType(types[i], SignatureKind.Construct)[0])); - } - } - return getIntersectionType(mixedTypes); - } - - function resolveIntersectionTypeMembers(type: IntersectionType) { - // The members and properties collections are empty for intersection types. To get all properties of an - // intersection type use getPropertiesOfType (only the language service uses this). - let callSignatures: Signature[] | undefined; - let constructSignatures: Signature[] | undefined; - let stringIndexInfo: IndexInfo | undefined; - let numberIndexInfo: IndexInfo | undefined; - const types = type.types; - const mixinFlags = findMixins(types); - const mixinCount = countWhere(mixinFlags, (b) => b); - for (let i = 0; i < types.length; i++) { - const t = type.types[i]; - // When an intersection type contains mixin constructor types, the construct signatures from - // those types are discarded and their return types are mixed into the return types of all - // other construct signatures in the intersection type. For example, the intersection type - // '{ new(...args: any[]) => A } & { new(s: string) => B }' has a single construct signature - // 'new(s: string) => A & B'. - if (!mixinFlags[i]) { - let signatures = getSignaturesOfType(t, SignatureKind.Construct); - if (signatures.length && mixinCount > 0) { - signatures = map(signatures, s => { - const clone = cloneSignature(s); - clone.resolvedReturnType = includeMixinType(getReturnTypeOfSignature(s), types, mixinFlags, i); - return clone; - }); - } - constructSignatures = appendSignatures(constructSignatures, signatures); - } - callSignatures = appendSignatures(callSignatures, getSignaturesOfType(t, SignatureKind.Call)); - stringIndexInfo = intersectIndexInfos(stringIndexInfo, getIndexInfoOfType(t, IndexKind.String)); - numberIndexInfo = intersectIndexInfos(numberIndexInfo, getIndexInfoOfType(t, IndexKind.Number)); - } - setStructuredTypeMembers(type, emptySymbols, callSignatures || emptyArray, constructSignatures || emptyArray, stringIndexInfo, numberIndexInfo); - } - - function appendSignatures(signatures: Signature[] | undefined, newSignatures: readonly Signature[]) { - for (const sig of newSignatures) { - if (!signatures || every(signatures, s => !compareSignaturesIdentical(s, sig, /*partialMatch*/ false, /*ignoreThisTypes*/ false, /*ignoreReturnTypes*/ false, compareTypesIdentical))) { - signatures = append(signatures, sig); - } - } - return signatures; - } - - /** - * Converts an AnonymousType to a ResolvedType. - */ - function resolveAnonymousTypeMembers(type: AnonymousType) { - const symbol = getMergedSymbol(type.symbol); - if (type.target) { - setStructuredTypeMembers(type, emptySymbols, emptyArray, emptyArray, undefined, undefined); - const members = createInstantiatedSymbolTable(getPropertiesOfObjectType(type.target), type.mapper!, /*mappingThisOnly*/ false); - const callSignatures = instantiateSignatures(getSignaturesOfType(type.target, SignatureKind.Call), type.mapper!); - const constructSignatures = instantiateSignatures(getSignaturesOfType(type.target, SignatureKind.Construct), type.mapper!); - const stringIndexInfo = instantiateIndexInfo(getIndexInfoOfType(type.target, IndexKind.String), type.mapper!); - const numberIndexInfo = instantiateIndexInfo(getIndexInfoOfType(type.target, IndexKind.Number), type.mapper!); - setStructuredTypeMembers(type, members, callSignatures, constructSignatures, stringIndexInfo, numberIndexInfo); - } - else if (symbol.flags & SymbolFlags.TypeLiteral) { - setStructuredTypeMembers(type, emptySymbols, emptyArray, emptyArray, undefined, undefined); - const members = getMembersOfSymbol(symbol); - const callSignatures = getSignaturesOfSymbol(members.get(InternalSymbolName.Call)); - const constructSignatures = getSignaturesOfSymbol(members.get(InternalSymbolName.New)); - const stringIndexInfo = getIndexInfoOfSymbol(symbol, IndexKind.String); - const numberIndexInfo = getIndexInfoOfSymbol(symbol, IndexKind.Number); - setStructuredTypeMembers(type, members, callSignatures, constructSignatures, stringIndexInfo, numberIndexInfo); - } - else { - // Combinations of function, class, enum and module - let members = emptySymbols; - let stringIndexInfo: IndexInfo | undefined; - if (symbol.exports) { - members = getExportsOfSymbol(symbol); - if (symbol === globalThisSymbol) { - const varsOnly = createMap() as SymbolTable; - members.forEach(p => { - if (!(p.flags & SymbolFlags.BlockScoped)) { - varsOnly.set(p.escapedName, p); - } - }); - members = varsOnly; - } - } - setStructuredTypeMembers(type, members, emptyArray, emptyArray, undefined, undefined); - if (symbol.flags & SymbolFlags.Class) { - const classType = getDeclaredTypeOfClassOrInterface(symbol); - const baseConstructorType = getBaseConstructorTypeOfClass(classType); - if (baseConstructorType.flags & (TypeFlags.Object | TypeFlags.Intersection | TypeFlags.TypeVariable)) { - members = createSymbolTable(getNamedMembers(members)); - addInheritedMembers(members, getPropertiesOfType(baseConstructorType)); - } - else if (baseConstructorType === anyType) { - stringIndexInfo = createIndexInfo(anyType, /*isReadonly*/ false); - } - } - const numberIndexInfo = symbol.flags & SymbolFlags.Enum && (getDeclaredTypeOfSymbol(symbol).flags & TypeFlags.Enum || - some(type.properties, prop => !!(getTypeOfSymbol(prop).flags & TypeFlags.NumberLike))) ? enumNumberIndexInfo : undefined; - setStructuredTypeMembers(type, members, emptyArray, emptyArray, stringIndexInfo, numberIndexInfo); - // We resolve the members before computing the signatures because a signature may use - // typeof with a qualified name expression that circularly references the type we are - // in the process of resolving (see issue #6072). The temporarily empty signature list - // will never be observed because a qualified name can't reference signatures. - if (symbol.flags & (SymbolFlags.Function | SymbolFlags.Method)) { - type.callSignatures = getSignaturesOfSymbol(symbol); - } - // And likewise for construct signatures for classes - if (symbol.flags & SymbolFlags.Class) { - const classType = getDeclaredTypeOfClassOrInterface(symbol); - let constructSignatures = symbol.members ? getSignaturesOfSymbol(symbol.members.get(InternalSymbolName.Constructor)) : emptyArray; - if (symbol.flags & SymbolFlags.Function) { - constructSignatures = addRange(constructSignatures.slice(), mapDefined( - type.callSignatures, - sig => isJSConstructor(sig.declaration) ? - createSignature(sig.declaration, sig.typeParameters, sig.thisParameter, sig.parameters, classType, /*resolvedTypePredicate*/ undefined, sig.minArgumentCount, sig.flags & SignatureFlags.PropagatingFlags) : - undefined)); - } - if (!constructSignatures.length) { - constructSignatures = getDefaultConstructSignatures(classType); - } - type.constructSignatures = constructSignatures; - } - } - } - - function resolveReverseMappedTypeMembers(type: ReverseMappedType) { - const indexInfo = getIndexInfoOfType(type.source, IndexKind.String); - const modifiers = getMappedTypeModifiers(type.mappedType); - const readonlyMask = modifiers & MappedTypeModifiers.IncludeReadonly ? false : true; - const optionalMask = modifiers & MappedTypeModifiers.IncludeOptional ? 0 : SymbolFlags.Optional; - const stringIndexInfo = indexInfo && createIndexInfo(inferReverseMappedType(indexInfo.type, type.mappedType, type.constraintType), readonlyMask && indexInfo.isReadonly); - const members = createSymbolTable(); - for (const prop of getPropertiesOfType(type.source)) { - const checkFlags = CheckFlags.ReverseMapped | (readonlyMask && isReadonlySymbol(prop) ? CheckFlags.Readonly : 0); - const inferredProp = createSymbol(SymbolFlags.Property | prop.flags & optionalMask, prop.escapedName, checkFlags) as ReverseMappedSymbol; - inferredProp.declarations = prop.declarations; - inferredProp.nameType = getSymbolLinks(prop).nameType; - inferredProp.propertyType = getTypeOfSymbol(prop); - inferredProp.mappedType = type.mappedType; - inferredProp.constraintType = type.constraintType; - members.set(prop.escapedName, inferredProp); - } - setStructuredTypeMembers(type, members, emptyArray, emptyArray, stringIndexInfo, undefined); - } - - // Return the lower bound of the key type in a mapped type. Intuitively, the lower - // bound includes those keys that are known to always be present, for example because - // because of constraints on type parameters (e.g. 'keyof T' for a constrained T). - function getLowerBoundOfKeyType(type: Type): Type { - if (type.flags & (TypeFlags.Any | TypeFlags.Primitive)) { - return type; - } - if (type.flags & TypeFlags.Index) { - return getIndexType(getApparentType((type).type)); - } - if (type.flags & TypeFlags.Conditional) { - if ((type).root.isDistributive) { - const checkType = (type).checkType; - const constraint = getLowerBoundOfKeyType(checkType); - if (constraint !== checkType) { - const mapper = makeUnaryTypeMapper((type).root.checkType, constraint); - return getConditionalTypeInstantiation(type, combineTypeMappers(mapper, (type).mapper)); - } - } - return type; - } - if (type.flags & TypeFlags.Union) { - return getUnionType(sameMap((type).types, getLowerBoundOfKeyType)); - } - if (type.flags & TypeFlags.Intersection) { - return getIntersectionType(sameMap((type).types, getLowerBoundOfKeyType)); - } - return neverType; - } - - /** Resolve the members of a mapped type { [P in K]: T } */ - function resolveMappedTypeMembers(type: MappedType) { - const members: SymbolTable = createSymbolTable(); - let stringIndexInfo: IndexInfo | undefined; - let numberIndexInfo: IndexInfo | undefined; - // Resolve upfront such that recursive references see an empty object type. - setStructuredTypeMembers(type, emptySymbols, emptyArray, emptyArray, undefined, undefined); - // In { [P in K]: T }, we refer to P as the type parameter type, K as the constraint type, - // and T as the template type. - const typeParameter = getTypeParameterFromMappedType(type); - const constraintType = getConstraintTypeFromMappedType(type); - const templateType = getTemplateTypeFromMappedType(type.target || type); - const modifiersType = getApparentType(getModifiersTypeFromMappedType(type)); // The 'T' in 'keyof T' - const templateModifiers = getMappedTypeModifiers(type); - const include = keyofStringsOnly ? TypeFlags.StringLiteral : TypeFlags.StringOrNumberLiteralOrUnique; - if (isMappedTypeWithKeyofConstraintDeclaration(type)) { - // We have a { [P in keyof T]: X } - for (const prop of getPropertiesOfType(modifiersType)) { - addMemberForKeyType(getLiteralTypeFromProperty(prop, include)); - } - if (modifiersType.flags & TypeFlags.Any || getIndexInfoOfType(modifiersType, IndexKind.String)) { - addMemberForKeyType(stringType); - } - if (!keyofStringsOnly && getIndexInfoOfType(modifiersType, IndexKind.Number)) { - addMemberForKeyType(numberType); - } - } - else { - forEachType(getLowerBoundOfKeyType(constraintType), addMemberForKeyType); - } - setStructuredTypeMembers(type, members, emptyArray, emptyArray, stringIndexInfo, numberIndexInfo); - - function addMemberForKeyType(t: Type) { - // Create a mapper from T to the current iteration type constituent. Then, if the - // mapped type is itself an instantiated type, combine the iteration mapper with the - // instantiation mapper. - const templateMapper = combineTypeMappers(type.mapper, createTypeMapper([typeParameter], [t])); - const propType = instantiateType(templateType, templateMapper); - // If the current iteration type constituent is a string literal type, create a property. - // Otherwise, for type string create a string index signature. - if (isTypeUsableAsPropertyName(t)) { - const propName = getPropertyNameFromType(t); - const modifiersProp = getPropertyOfType(modifiersType, propName); - const isOptional = !!(templateModifiers & MappedTypeModifiers.IncludeOptional || - !(templateModifiers & MappedTypeModifiers.ExcludeOptional) && modifiersProp && modifiersProp.flags & SymbolFlags.Optional); - const isReadonly = !!(templateModifiers & MappedTypeModifiers.IncludeReadonly || - !(templateModifiers & MappedTypeModifiers.ExcludeReadonly) && modifiersProp && isReadonlySymbol(modifiersProp)); - const prop = createSymbol(SymbolFlags.Property | (isOptional ? SymbolFlags.Optional : 0), propName, isReadonly ? CheckFlags.Readonly : 0); - // When creating an optional property in strictNullChecks mode, if 'undefined' isn't assignable to the - // type, we include 'undefined' in the type. Similarly, when creating a non-optional property in strictNullChecks - // mode, if the underlying property is optional we remove 'undefined' from the type. - prop.type = strictNullChecks && isOptional && !maybeTypeOfKind(propType, TypeFlags.Undefined | TypeFlags.Void) ? getOptionalType(propType) : - strictNullChecks && !isOptional && modifiersProp && modifiersProp.flags & SymbolFlags.Optional ? getTypeWithFacts(propType, TypeFacts.NEUndefined) : - propType; - if (modifiersProp) { - prop.syntheticOrigin = modifiersProp; - prop.declarations = modifiersProp.declarations; - } - prop.nameType = t; - members.set(propName, prop); - } - else if (t.flags & (TypeFlags.Any | TypeFlags.String)) { - stringIndexInfo = createIndexInfo(propType, !!(templateModifiers & MappedTypeModifiers.IncludeReadonly)); - } - else if (t.flags & (TypeFlags.Number | TypeFlags.Enum)) { - numberIndexInfo = createIndexInfo(numberIndexInfo ? getUnionType([numberIndexInfo.type, propType]) : propType, - !!(templateModifiers & MappedTypeModifiers.IncludeReadonly)); - } - } - } - - function getTypeParameterFromMappedType(type: MappedType) { - return type.typeParameter || - (type.typeParameter = getDeclaredTypeOfTypeParameter(getSymbolOfNode(type.declaration.typeParameter))); - } - - function getConstraintTypeFromMappedType(type: MappedType) { - return type.constraintType || - (type.constraintType = getConstraintOfTypeParameter(getTypeParameterFromMappedType(type)) || errorType); - } - - function getTemplateTypeFromMappedType(type: MappedType) { - return type.templateType || - (type.templateType = type.declaration.type ? - instantiateType(addOptionality(getTypeFromTypeNode(type.declaration.type), !!(getMappedTypeModifiers(type) & MappedTypeModifiers.IncludeOptional)), type.mapper || identityMapper) : - errorType); - } - - function getConstraintDeclarationForMappedType(type: MappedType) { - return getEffectiveConstraintOfTypeParameter(type.declaration.typeParameter); - } - - function isMappedTypeWithKeyofConstraintDeclaration(type: MappedType) { - const constraintDeclaration = getConstraintDeclarationForMappedType(type)!; // TODO: GH#18217 - return constraintDeclaration.kind === SyntaxKind.TypeOperator && - (constraintDeclaration).operator === SyntaxKind.KeyOfKeyword; - } - - function getModifiersTypeFromMappedType(type: MappedType) { - if (!type.modifiersType) { - if (isMappedTypeWithKeyofConstraintDeclaration(type)) { - // If the constraint declaration is a 'keyof T' node, the modifiers type is T. We check - // AST nodes here because, when T is a non-generic type, the logic below eagerly resolves - // 'keyof T' to a literal union type and we can't recover T from that type. - type.modifiersType = instantiateType(getTypeFromTypeNode((getConstraintDeclarationForMappedType(type)).type), type.mapper || identityMapper); - } - else { - // Otherwise, get the declared constraint type, and if the constraint type is a type parameter, - // get the constraint of that type parameter. If the resulting type is an indexed type 'keyof T', - // the modifiers type is T. Otherwise, the modifiers type is unknown. - const declaredType = getTypeFromMappedTypeNode(type.declaration); - const constraint = getConstraintTypeFromMappedType(declaredType); - const extendedConstraint = constraint && constraint.flags & TypeFlags.TypeParameter ? getConstraintOfTypeParameter(constraint) : constraint; - type.modifiersType = extendedConstraint && extendedConstraint.flags & TypeFlags.Index ? instantiateType((extendedConstraint).type, type.mapper || identityMapper) : unknownType; - } - } - return type.modifiersType; - } - - function getMappedTypeModifiers(type: MappedType): MappedTypeModifiers { - const declaration = type.declaration; - return (declaration.readonlyToken ? declaration.readonlyToken.kind === SyntaxKind.MinusToken ? MappedTypeModifiers.ExcludeReadonly : MappedTypeModifiers.IncludeReadonly : 0) | - (declaration.questionToken ? declaration.questionToken.kind === SyntaxKind.MinusToken ? MappedTypeModifiers.ExcludeOptional : MappedTypeModifiers.IncludeOptional : 0); - } - - function getMappedTypeOptionality(type: MappedType): number { - const modifiers = getMappedTypeModifiers(type); - return modifiers & MappedTypeModifiers.ExcludeOptional ? -1 : modifiers & MappedTypeModifiers.IncludeOptional ? 1 : 0; - } - - function getCombinedMappedTypeOptionality(type: MappedType): number { - const optionality = getMappedTypeOptionality(type); - const modifiersType = getModifiersTypeFromMappedType(type); - return optionality || (isGenericMappedType(modifiersType) ? getMappedTypeOptionality(modifiersType) : 0); - } - - function isPartialMappedType(type: Type) { - return !!(getObjectFlags(type) & ObjectFlags.Mapped && getMappedTypeModifiers(type) & MappedTypeModifiers.IncludeOptional); - } - - function isGenericMappedType(type: Type): type is MappedType { - return !!(getObjectFlags(type) & ObjectFlags.Mapped) && isGenericIndexType(getConstraintTypeFromMappedType(type)); - } - - function resolveStructuredTypeMembers(type: StructuredType): ResolvedType { - if (!(type).members) { - if (type.flags & TypeFlags.Object) { - if ((type).objectFlags & ObjectFlags.Reference) { - resolveTypeReferenceMembers(type); - } - else if ((type).objectFlags & ObjectFlags.ClassOrInterface) { - resolveClassOrInterfaceMembers(type); - } - else if ((type).objectFlags & ObjectFlags.ReverseMapped) { - resolveReverseMappedTypeMembers(type as ReverseMappedType); - } - else if ((type).objectFlags & ObjectFlags.Anonymous) { - resolveAnonymousTypeMembers(type); - } - else if ((type).objectFlags & ObjectFlags.Mapped) { - resolveMappedTypeMembers(type); - } - } - else if (type.flags & TypeFlags.Union) { - resolveUnionTypeMembers(type); - } - else if (type.flags & TypeFlags.Intersection) { - resolveIntersectionTypeMembers(type); - } - } - return type; - } - - /** Return properties of an object type or an empty array for other types */ - function getPropertiesOfObjectType(type: Type): Symbol[] { - if (type.flags & TypeFlags.Object) { - return resolveStructuredTypeMembers(type).properties; - } - return emptyArray; - } - - /** If the given type is an object type and that type has a property by the given name, - * return the symbol for that property. Otherwise return undefined. - */ - function getPropertyOfObjectType(type: Type, name: __String): Symbol | undefined { - if (type.flags & TypeFlags.Object) { - const resolved = resolveStructuredTypeMembers(type); - const symbol = resolved.members.get(name); - if (symbol && symbolIsValue(symbol)) { - return symbol; - } - } - } - - function getPropertiesOfUnionOrIntersectionType(type: UnionOrIntersectionType): Symbol[] { - if (!type.resolvedProperties) { - const members = createSymbolTable(); - for (const current of type.types) { - for (const prop of getPropertiesOfType(current)) { - if (!members.has(prop.escapedName)) { - const combinedProp = getPropertyOfUnionOrIntersectionType(type, prop.escapedName); - if (combinedProp) { - members.set(prop.escapedName, combinedProp); - } - } - } - // The properties of a union type are those that are present in all constituent types, so - // we only need to check the properties of the first type - if (type.flags & TypeFlags.Union) { - break; - } - } - type.resolvedProperties = getNamedMembers(members); - } - return type.resolvedProperties; - } - - function getPropertiesOfType(type: Type): Symbol[] { - type = getApparentType(type); - return type.flags & TypeFlags.UnionOrIntersection ? - getPropertiesOfUnionOrIntersectionType(type) : - getPropertiesOfObjectType(type); - } - - function isTypeInvalidDueToUnionDiscriminant(contextualType: Type, obj: ObjectLiteralExpression | JsxAttributes): boolean { - const list = obj.properties as NodeArray; - return list.some(property => { - const nameType = property.name && getLiteralTypeFromPropertyName(property.name); - const name = nameType && isTypeUsableAsPropertyName(nameType) ? getPropertyNameFromType(nameType) : undefined; - const expected = name === undefined ? undefined : getTypeOfPropertyOfType(contextualType, name); - return !!expected && isLiteralType(expected) && !isTypeAssignableTo(getTypeOfNode(property), expected); - }); - } - - function getAllPossiblePropertiesOfTypes(types: readonly Type[]): Symbol[] { - const unionType = getUnionType(types); - if (!(unionType.flags & TypeFlags.Union)) { - return getAugmentedPropertiesOfType(unionType); - } - - const props = createSymbolTable(); - for (const memberType of types) { - for (const { escapedName } of getAugmentedPropertiesOfType(memberType)) { - if (!props.has(escapedName)) { - const prop = createUnionOrIntersectionProperty(unionType as UnionType, escapedName); - // May be undefined if the property is private - if (prop) props.set(escapedName, prop); - } - } - } - return arrayFrom(props.values()); - } - - function getConstraintOfType(type: InstantiableType | UnionOrIntersectionType): Type | undefined { - return type.flags & TypeFlags.TypeParameter ? getConstraintOfTypeParameter(type) : - type.flags & TypeFlags.IndexedAccess ? getConstraintOfIndexedAccess(type) : - type.flags & TypeFlags.Conditional ? getConstraintOfConditionalType(type) : - getBaseConstraintOfType(type); - } - - function getConstraintOfTypeParameter(typeParameter: TypeParameter): Type | undefined { - return hasNonCircularBaseConstraint(typeParameter) ? getConstraintFromTypeParameter(typeParameter) : undefined; - } - - function getConstraintOfIndexedAccess(type: IndexedAccessType) { - return hasNonCircularBaseConstraint(type) ? getConstraintFromIndexedAccess(type) : undefined; - } - - function getSimplifiedTypeOrConstraint(type: Type) { - const simplified = getSimplifiedType(type, /*writing*/ false); - return simplified !== type ? simplified : getConstraintOfType(type); - } - - function getConstraintFromIndexedAccess(type: IndexedAccessType) { - const indexConstraint = getSimplifiedTypeOrConstraint(type.indexType); - if (indexConstraint && indexConstraint !== type.indexType) { - const indexedAccess = getIndexedAccessTypeOrUndefined(type.objectType, indexConstraint); - if (indexedAccess) { - return indexedAccess; - } - } - const objectConstraint = getSimplifiedTypeOrConstraint(type.objectType); - if (objectConstraint && objectConstraint !== type.objectType) { - return getIndexedAccessTypeOrUndefined(objectConstraint, type.indexType); - } - return undefined; - } - - function getDefaultConstraintOfConditionalType(type: ConditionalType) { - if (!type.resolvedDefaultConstraint) { - // An `any` branch of a conditional type would normally be viral - specifically, without special handling here, - // a conditional type with a single branch of type `any` would be assignable to anything, since it's constraint would simplify to - // just `any`. This result is _usually_ unwanted - so instead here we elide an `any` branch from the constraint type, - // in effect treating `any` like `never` rather than `unknown` in this location. - const trueConstraint = getInferredTrueTypeFromConditionalType(type); - const falseConstraint = getFalseTypeFromConditionalType(type); - type.resolvedDefaultConstraint = isTypeAny(trueConstraint) ? falseConstraint : isTypeAny(falseConstraint) ? trueConstraint : getUnionType([trueConstraint, falseConstraint]); - } - return type.resolvedDefaultConstraint; - } - - function getConstraintOfDistributiveConditionalType(type: ConditionalType): Type | undefined { - // Check if we have a conditional type of the form 'T extends U ? X : Y', where T is a constrained - // type parameter. If so, create an instantiation of the conditional type where T is replaced - // with its constraint. We do this because if the constraint is a union type it will be distributed - // over the conditional type and possibly reduced. For example, 'T extends undefined ? never : T' - // removes 'undefined' from T. - // We skip returning a distributive constraint for a restrictive instantiation of a conditional type - // as the constraint for all type params (check type included) have been replace with `unknown`, which - // is going to produce even more false positive/negative results than the distribute constraint already does. - // Please note: the distributive constraint is a kludge for emulating what a negated type could to do filter - // a union - once negated types exist and are applied to the conditional false branch, this "constraint" - // likely doesn't need to exist. - if (type.root.isDistributive && type.restrictiveInstantiation !== type) { - const simplified = getSimplifiedType(type.checkType, /*writing*/ false); - const constraint = simplified === type.checkType ? getConstraintOfType(simplified) : simplified; - if (constraint && constraint !== type.checkType) { - const mapper = makeUnaryTypeMapper(type.root.checkType, constraint); - const instantiated = getConditionalTypeInstantiation(type, combineTypeMappers(mapper, type.mapper)); - if (!(instantiated.flags & TypeFlags.Never)) { - return instantiated; - } - } - } - return undefined; - } - - function getConstraintFromConditionalType(type: ConditionalType) { - return getConstraintOfDistributiveConditionalType(type) || getDefaultConstraintOfConditionalType(type); - } - - function getConstraintOfConditionalType(type: ConditionalType) { - return hasNonCircularBaseConstraint(type) ? getConstraintFromConditionalType(type) : undefined; - } - - function getEffectiveConstraintOfIntersection(types: readonly Type[], targetIsUnion: boolean) { - let constraints: Type[] | undefined; - let hasDisjointDomainType = false; - for (const t of types) { - if (t.flags & TypeFlags.Instantiable) { - // We keep following constraints as long as we have an instantiable type that is known - // not to be circular or infinite (hence we stop on index access types). - let constraint = getConstraintOfType(t); - while (constraint && constraint.flags & (TypeFlags.TypeParameter | TypeFlags.Index | TypeFlags.Conditional)) { - constraint = getConstraintOfType(constraint); - } - if (constraint) { - constraints = append(constraints, constraint); - if (targetIsUnion) { - constraints = append(constraints, t); - } - } - } - else if (t.flags & TypeFlags.DisjointDomains) { - hasDisjointDomainType = true; - } - } - // If the target is a union type or if we are intersecting with types belonging to one of the - // disjoint domains, we may end up producing a constraint that hasn't been examined before. - if (constraints && (targetIsUnion || hasDisjointDomainType)) { - if (hasDisjointDomainType) { - // We add any types belong to one of the disjoint domains because they might cause the final - // intersection operation to reduce the union constraints. - for (const t of types) { - if (t.flags & TypeFlags.DisjointDomains) { - constraints = append(constraints, t); - } - } - } - return getIntersectionType(constraints); - } - return undefined; - } - - function getBaseConstraintOfType(type: Type): Type | undefined { - if (type.flags & (TypeFlags.InstantiableNonPrimitive | TypeFlags.UnionOrIntersection)) { - const constraint = getResolvedBaseConstraint(type); - return constraint !== noConstraintType && constraint !== circularConstraintType ? constraint : undefined; - } - return type.flags & TypeFlags.Index ? keyofConstraintType : undefined; - } - - /** - * This is similar to `getBaseConstraintOfType` except it returns the input type if there's no base constraint, instead of `undefined` - * It also doesn't map indexes to `string`, as where this is used this would be unneeded (and likely undesirable) - */ - function getBaseConstraintOrType(type: Type) { - return getBaseConstraintOfType(type) || type; - } - - function hasNonCircularBaseConstraint(type: InstantiableType): boolean { - return getResolvedBaseConstraint(type) !== circularConstraintType; - } - - /** - * Return the resolved base constraint of a type variable. The noConstraintType singleton is returned if the - * type variable has no constraint, and the circularConstraintType singleton is returned if the constraint - * circularly references the type variable. - */ - function getResolvedBaseConstraint(type: InstantiableType | UnionOrIntersectionType): Type { - let nonTerminating = false; - return type.resolvedBaseConstraint || - (type.resolvedBaseConstraint = getTypeWithThisArgument(getImmediateBaseConstraint(type), type)); - - function getImmediateBaseConstraint(t: Type): Type { - if (!t.immediateBaseConstraint) { - if (!pushTypeResolution(t, TypeSystemPropertyName.ImmediateBaseConstraint)) { - return circularConstraintType; - } - if (constraintDepth >= 50) { - // We have reached 50 recursive invocations of getImmediateBaseConstraint and there is a - // very high likelihood we're dealing with an infinite generic type that perpetually generates - // new type identities as we descend into it. We stop the recursion here and mark this type - // and the outer types as having circular constraints. - error(currentNode, Diagnostics.Type_instantiation_is_excessively_deep_and_possibly_infinite); - nonTerminating = true; - return t.immediateBaseConstraint = noConstraintType; - } - constraintDepth++; - let result = computeBaseConstraint(getSimplifiedType(t, /*writing*/ false)); - constraintDepth--; - if (!popTypeResolution()) { - if (t.flags & TypeFlags.TypeParameter) { - const errorNode = getConstraintDeclaration(t); - if (errorNode) { - const diagnostic = error(errorNode, Diagnostics.Type_parameter_0_has_a_circular_constraint, typeToString(t)); - if (currentNode && !isNodeDescendantOf(errorNode, currentNode) && !isNodeDescendantOf(currentNode, errorNode)) { - addRelatedInfo(diagnostic, createDiagnosticForNode(currentNode, Diagnostics.Circularity_originates_in_type_at_this_location)); - } - } - } - result = circularConstraintType; - } - if (nonTerminating) { - result = circularConstraintType; - } - t.immediateBaseConstraint = result || noConstraintType; - } - return t.immediateBaseConstraint; - } - - function getBaseConstraint(t: Type): Type | undefined { - const c = getImmediateBaseConstraint(t); - return c !== noConstraintType && c !== circularConstraintType ? c : undefined; - } - - function computeBaseConstraint(t: Type): Type | undefined { - if (t.flags & TypeFlags.TypeParameter) { - const constraint = getConstraintFromTypeParameter(t); - return (t as TypeParameter).isThisType || !constraint ? - constraint : - getBaseConstraint(constraint); - } - if (t.flags & TypeFlags.UnionOrIntersection) { - const types = (t).types; - const baseTypes: Type[] = []; - for (const type of types) { - const baseType = getBaseConstraint(type); - if (baseType) { - baseTypes.push(baseType); - } - } - return t.flags & TypeFlags.Union && baseTypes.length === types.length ? getUnionType(baseTypes) : - t.flags & TypeFlags.Intersection && baseTypes.length ? getIntersectionType(baseTypes) : - undefined; - } - if (t.flags & TypeFlags.Index) { - return keyofConstraintType; - } - if (t.flags & TypeFlags.IndexedAccess) { - const baseObjectType = getBaseConstraint((t).objectType); - const baseIndexType = getBaseConstraint((t).indexType); - const baseIndexedAccess = baseObjectType && baseIndexType && getIndexedAccessTypeOrUndefined(baseObjectType, baseIndexType); - return baseIndexedAccess && getBaseConstraint(baseIndexedAccess); - } - if (t.flags & TypeFlags.Conditional) { - const constraint = getConstraintFromConditionalType(t); - constraintDepth++; // Penalize repeating conditional types (this captures the recursion within getConstraintFromConditionalType and carries it forward) - const result = constraint && getBaseConstraint(constraint); - constraintDepth--; - return result; - } - if (t.flags & TypeFlags.Substitution) { - return getBaseConstraint((t).substitute); - } - return t; - } - } - - function getApparentTypeOfIntersectionType(type: IntersectionType) { - return type.resolvedApparentType || (type.resolvedApparentType = getTypeWithThisArgument(type, type, /*apparentType*/ true)); - } - - function getResolvedTypeParameterDefault(typeParameter: TypeParameter): Type | undefined { - if (!typeParameter.default) { - if (typeParameter.target) { - const targetDefault = getResolvedTypeParameterDefault(typeParameter.target); - typeParameter.default = targetDefault ? instantiateType(targetDefault, typeParameter.mapper) : noConstraintType; - } - else { - // To block recursion, set the initial value to the resolvingDefaultType. - typeParameter.default = resolvingDefaultType; - const defaultDeclaration = typeParameter.symbol && forEach(typeParameter.symbol.declarations, decl => isTypeParameterDeclaration(decl) && decl.default); - const defaultType = defaultDeclaration ? getTypeFromTypeNode(defaultDeclaration) : noConstraintType; - if (typeParameter.default === resolvingDefaultType) { - // If we have not been called recursively, set the correct default type. - typeParameter.default = defaultType; - } - } - } - else if (typeParameter.default === resolvingDefaultType) { - // If we are called recursively for this type parameter, mark the default as circular. - typeParameter.default = circularConstraintType; - } - return typeParameter.default; - } - - /** - * Gets the default type for a type parameter. - * - * If the type parameter is the result of an instantiation, this gets the instantiated - * default type of its target. If the type parameter has no default type or the default is - * circular, `undefined` is returned. - */ - function getDefaultFromTypeParameter(typeParameter: TypeParameter): Type | undefined { - const defaultType = getResolvedTypeParameterDefault(typeParameter); - return defaultType !== noConstraintType && defaultType !== circularConstraintType ? defaultType : undefined; - } - - function hasNonCircularTypeParameterDefault(typeParameter: TypeParameter) { - return getResolvedTypeParameterDefault(typeParameter) !== circularConstraintType; - } - - /** - * Indicates whether the declaration of a typeParameter has a default type. - */ - function hasTypeParameterDefault(typeParameter: TypeParameter): boolean { - return !!(typeParameter.symbol && forEach(typeParameter.symbol.declarations, decl => isTypeParameterDeclaration(decl) && decl.default)); - } - - function getApparentTypeOfMappedType(type: MappedType) { - return type.resolvedApparentType || (type.resolvedApparentType = getResolvedApparentTypeOfMappedType(type)); - } - - function getResolvedApparentTypeOfMappedType(type: MappedType) { - const typeVariable = getHomomorphicTypeVariable(type); - if (typeVariable) { - const constraint = getConstraintOfTypeParameter(typeVariable); - if (constraint && (isArrayType(constraint) || isTupleType(constraint))) { - const mapper = makeUnaryTypeMapper(typeVariable, constraint); - return instantiateType(type, combineTypeMappers(mapper, type.mapper)); - } - } - return type; - } - - /** - * For a type parameter, return the base constraint of the type parameter. For the string, number, - * boolean, and symbol primitive types, return the corresponding object types. Otherwise return the - * type itself. Note that the apparent type of a union type is the union type itself. - */ - function getApparentType(type: Type): Type { - const t = type.flags & TypeFlags.Instantiable ? getBaseConstraintOfType(type) || unknownType : type; - return getObjectFlags(t) & ObjectFlags.Mapped ? getApparentTypeOfMappedType(t) : - t.flags & TypeFlags.Intersection ? getApparentTypeOfIntersectionType(t) : - t.flags & TypeFlags.StringLike ? globalStringType : - t.flags & TypeFlags.NumberLike ? globalNumberType : - t.flags & TypeFlags.BigIntLike ? getGlobalBigIntType(/*reportErrors*/ languageVersion >= ScriptTarget.ESNext) : - t.flags & TypeFlags.BooleanLike ? globalBooleanType : - t.flags & TypeFlags.ESSymbolLike ? getGlobalESSymbolType(/*reportErrors*/ languageVersion >= ScriptTarget.ES2015) : - t.flags & TypeFlags.NonPrimitive ? emptyObjectType : - t.flags & TypeFlags.Index ? keyofConstraintType : - t.flags & TypeFlags.Unknown && !strictNullChecks ? emptyObjectType : - t; - } - - function createUnionOrIntersectionProperty(containingType: UnionOrIntersectionType, name: __String): Symbol | undefined { - const propSet = createMap(); - let indexTypes: Type[] | undefined; - const isUnion = containingType.flags & TypeFlags.Union; - const excludeModifiers = isUnion ? ModifierFlags.NonPublicAccessibilityModifier : 0; - // Flags we want to propagate to the result if they exist in all source symbols - let optionalFlag = isUnion ? SymbolFlags.None : SymbolFlags.Optional; - let syntheticFlag = CheckFlags.SyntheticMethod; - let checkFlags = 0; - for (const current of containingType.types) { - const type = getApparentType(current); - if (type !== errorType) { - const prop = getPropertyOfType(type, name); - const modifiers = prop ? getDeclarationModifierFlagsFromSymbol(prop) : 0; - if (prop && !(modifiers & excludeModifiers)) { - if (isUnion) { - optionalFlag |= (prop.flags & SymbolFlags.Optional); - } - else { - optionalFlag &= prop.flags; - } - const id = "" + getSymbolId(prop); - if (!propSet.has(id)) { - propSet.set(id, prop); - } - checkFlags |= (isReadonlySymbol(prop) ? CheckFlags.Readonly : 0) | - (!(modifiers & ModifierFlags.NonPublicAccessibilityModifier) ? CheckFlags.ContainsPublic : 0) | - (modifiers & ModifierFlags.Protected ? CheckFlags.ContainsProtected : 0) | - (modifiers & ModifierFlags.Private ? CheckFlags.ContainsPrivate : 0) | - (modifiers & ModifierFlags.Static ? CheckFlags.ContainsStatic : 0); - if (!isPrototypeProperty(prop)) { - syntheticFlag = CheckFlags.SyntheticProperty; - } - } - else if (isUnion) { - const indexInfo = !isLateBoundName(name) && (isNumericLiteralName(name) && getIndexInfoOfType(type, IndexKind.Number) || getIndexInfoOfType(type, IndexKind.String)); - if (indexInfo) { - checkFlags |= CheckFlags.WritePartial | (indexInfo.isReadonly ? CheckFlags.Readonly : 0); - indexTypes = append(indexTypes, isTupleType(type) ? getRestTypeOfTupleType(type) || undefinedType : indexInfo.type); - } - else if (isObjectLiteralType(type)) { - checkFlags |= CheckFlags.WritePartial; - indexTypes = append(indexTypes, undefinedType); - } - else { - checkFlags |= CheckFlags.ReadPartial; - } - } - } - } - if (!propSet.size) { - return undefined; - } - const props = arrayFrom(propSet.values()); - if (props.length === 1 && !(checkFlags & CheckFlags.ReadPartial) && !indexTypes) { - return props[0]; - } - let declarations: Declaration[] | undefined; - let firstType: Type | undefined; - let nameType: Type | undefined; - const propTypes: Type[] = []; - let firstValueDeclaration: Declaration | undefined; - let hasNonUniformValueDeclaration = false; - for (const prop of props) { - if (!firstValueDeclaration) { - firstValueDeclaration = prop.valueDeclaration; - } - else if (prop.valueDeclaration !== firstValueDeclaration) { - hasNonUniformValueDeclaration = true; - } - declarations = addRange(declarations, prop.declarations); - const type = getTypeOfSymbol(prop); - if (!firstType) { - firstType = type; - nameType = getSymbolLinks(prop).nameType; - } - else if (type !== firstType) { - checkFlags |= CheckFlags.HasNonUniformType; - } - if (isLiteralType(type)) { - checkFlags |= CheckFlags.HasLiteralType; - } - propTypes.push(type); - } - addRange(propTypes, indexTypes); - const result = createSymbol(SymbolFlags.Property | optionalFlag, name, syntheticFlag | checkFlags); - result.containingType = containingType; - if (!hasNonUniformValueDeclaration && firstValueDeclaration) { - result.valueDeclaration = firstValueDeclaration; - - // Inherit information about parent type. - if (firstValueDeclaration.symbol.parent) { - result.parent = firstValueDeclaration.symbol.parent; - } - } - - result.declarations = declarations!; - result.nameType = nameType; - if (propTypes.length > 2) { - // When `propTypes` has the potential to explode in size when normalized, defer normalization until absolutely needed - result.checkFlags |= CheckFlags.DeferredType; - result.deferralParent = containingType; - result.deferralConstituents = propTypes; - } - else { - result.type = isUnion ? getUnionType(propTypes) : getIntersectionType(propTypes); - } - return result; - } - - // Return the symbol for a given property in a union or intersection type, or undefined if the property - // does not exist in any constituent type. Note that the returned property may only be present in some - // constituents, in which case the isPartial flag is set when the containing type is union type. We need - // these partial properties when identifying discriminant properties, but otherwise they are filtered out - // and do not appear to be present in the union type. - function getUnionOrIntersectionProperty(type: UnionOrIntersectionType, name: __String): Symbol | undefined { - const properties = type.propertyCache || (type.propertyCache = createSymbolTable()); - let property = properties.get(name); - if (!property) { - property = createUnionOrIntersectionProperty(type, name); - if (property) { - properties.set(name, property); - } - } - return property; - } - - function getPropertyOfUnionOrIntersectionType(type: UnionOrIntersectionType, name: __String): Symbol | undefined { - const property = getUnionOrIntersectionProperty(type, name); - // We need to filter out partial properties in union types - return property && !(getCheckFlags(property) & CheckFlags.ReadPartial) ? property : undefined; - } - - /** - * Return the symbol for the property with the given name in the given type. Creates synthetic union properties when - * necessary, maps primitive types and type parameters are to their apparent types, and augments with properties from - * Object and Function as appropriate. - * - * @param type a type to look up property from - * @param name a name of property to look up in a given type - */ - function getPropertyOfType(type: Type, name: __String): Symbol | undefined { - type = getApparentType(type); - if (type.flags & TypeFlags.Object) { - const resolved = resolveStructuredTypeMembers(type); - const symbol = resolved.members.get(name); - if (symbol && symbolIsValue(symbol)) { - return symbol; - } - const functionType = resolved === anyFunctionType ? globalFunctionType : - resolved.callSignatures.length ? globalCallableFunctionType : - resolved.constructSignatures.length ? globalNewableFunctionType : - undefined; - if (functionType) { - const symbol = getPropertyOfObjectType(functionType, name); - if (symbol) { - return symbol; - } - } - return getPropertyOfObjectType(globalObjectType, name); - } - if (type.flags & TypeFlags.UnionOrIntersection) { - return getPropertyOfUnionOrIntersectionType(type, name); - } - return undefined; - } - - function getSignaturesOfStructuredType(type: Type, kind: SignatureKind): readonly Signature[] { - if (type.flags & TypeFlags.StructuredType) { - const resolved = resolveStructuredTypeMembers(type); - return kind === SignatureKind.Call ? resolved.callSignatures : resolved.constructSignatures; - } - return emptyArray; - } - - /** - * Return the signatures of the given kind in the given type. Creates synthetic union signatures when necessary and - * maps primitive types and type parameters are to their apparent types. - */ - function getSignaturesOfType(type: Type, kind: SignatureKind): readonly Signature[] { - return getSignaturesOfStructuredType(getApparentType(type), kind); - } - - function getIndexInfoOfStructuredType(type: Type, kind: IndexKind): IndexInfo | undefined { - if (type.flags & TypeFlags.StructuredType) { - const resolved = resolveStructuredTypeMembers(type); - return kind === IndexKind.String ? resolved.stringIndexInfo : resolved.numberIndexInfo; - } - } - - function getIndexTypeOfStructuredType(type: Type, kind: IndexKind): Type | undefined { - const info = getIndexInfoOfStructuredType(type, kind); - return info && info.type; - } - - // Return the indexing info of the given kind in the given type. Creates synthetic union index types when necessary and - // maps primitive types and type parameters are to their apparent types. - function getIndexInfoOfType(type: Type, kind: IndexKind): IndexInfo | undefined { - return getIndexInfoOfStructuredType(getApparentType(type), kind); - } - - // Return the index type of the given kind in the given type. Creates synthetic union index types when necessary and - // maps primitive types and type parameters are to their apparent types. - function getIndexTypeOfType(type: Type, kind: IndexKind): Type | undefined { - return getIndexTypeOfStructuredType(getApparentType(type), kind); - } - - function getImplicitIndexTypeOfType(type: Type, kind: IndexKind): Type | undefined { - if (isObjectTypeWithInferableIndex(type)) { - const propTypes: Type[] = []; - for (const prop of getPropertiesOfType(type)) { - if (kind === IndexKind.String || isNumericLiteralName(prop.escapedName)) { - propTypes.push(getTypeOfSymbol(prop)); - } - } - if (kind === IndexKind.String) { - append(propTypes, getIndexTypeOfType(type, IndexKind.Number)); - } - if (propTypes.length) { - return getUnionType(propTypes, UnionReduction.Subtype); - } - } - return undefined; - } - - // Return list of type parameters with duplicates removed (duplicate identifier errors are generated in the actual - // type checking functions). - function getTypeParametersFromDeclaration(declaration: DeclarationWithTypeParameters): TypeParameter[] | undefined { - let result: TypeParameter[] | undefined; - for (const node of getEffectiveTypeParameterDeclarations(declaration)) { - result = appendIfUnique(result, getDeclaredTypeOfTypeParameter(node.symbol)); - } - return result; - } - - function symbolsToArray(symbols: SymbolTable): Symbol[] { - const result: Symbol[] = []; - symbols.forEach((symbol, id) => { - if (!isReservedMemberName(id)) { - result.push(symbol); - } - }); - return result; - } - - function isJSDocOptionalParameter(node: ParameterDeclaration) { - return isInJSFile(node) && ( - // node.type should only be a JSDocOptionalType when node is a parameter of a JSDocFunctionType - node.type && node.type.kind === SyntaxKind.JSDocOptionalType - || getJSDocParameterTags(node).some(({ isBracketed, typeExpression }) => - isBracketed || !!typeExpression && typeExpression.type.kind === SyntaxKind.JSDocOptionalType)); - } - - function tryFindAmbientModule(moduleName: string, withAugmentations: boolean) { - if (isExternalModuleNameRelative(moduleName)) { - return undefined; - } - const symbol = getSymbol(globals, '"' + moduleName + '"' as __String, SymbolFlags.ValueModule); - // merged symbol is module declaration symbol combined with all augmentations - return symbol && withAugmentations ? getMergedSymbol(symbol) : symbol; - } - - function isOptionalParameter(node: ParameterDeclaration | JSDocParameterTag) { - if (hasQuestionToken(node) || isOptionalJSDocParameterTag(node) || isJSDocOptionalParameter(node)) { - return true; - } - - if (node.initializer) { - const signature = getSignatureFromDeclaration(node.parent); - const parameterIndex = node.parent.parameters.indexOf(node); - Debug.assert(parameterIndex >= 0); - return parameterIndex >= getMinArgumentCount(signature); - } - const iife = getImmediatelyInvokedFunctionExpression(node.parent); - if (iife) { - return !node.type && - !node.dotDotDotToken && - node.parent.parameters.indexOf(node) >= iife.arguments.length; - } - - return false; - } - - function isOptionalJSDocParameterTag(node: Node): node is JSDocParameterTag { - if (!isJSDocParameterTag(node)) { - return false; - } - const { isBracketed, typeExpression } = node; - return isBracketed || !!typeExpression && typeExpression.type.kind === SyntaxKind.JSDocOptionalType; - } - - function createTypePredicate(kind: TypePredicateKind, parameterName: string | undefined, parameterIndex: number | undefined, type: Type | undefined): TypePredicate { - return { kind, parameterName, parameterIndex, type } as TypePredicate; - } - - /** - * Gets the minimum number of type arguments needed to satisfy all non-optional type - * parameters. - */ - function getMinTypeArgumentCount(typeParameters: readonly TypeParameter[] | undefined): number { - let minTypeArgumentCount = 0; - if (typeParameters) { - for (let i = 0; i < typeParameters.length; i++) { - if (!hasTypeParameterDefault(typeParameters[i])) { - minTypeArgumentCount = i + 1; - } - } - } - return minTypeArgumentCount; - } - - /** - * Fill in default types for unsupplied type arguments. If `typeArguments` is undefined - * when a default type is supplied, a new array will be created and returned. - * - * @param typeArguments The supplied type arguments. - * @param typeParameters The requested type parameters. - * @param minTypeArgumentCount The minimum number of required type arguments. - */ - function fillMissingTypeArguments(typeArguments: readonly Type[], typeParameters: readonly TypeParameter[] | undefined, minTypeArgumentCount: number, isJavaScriptImplicitAny: boolean): Type[]; - function fillMissingTypeArguments(typeArguments: readonly Type[] | undefined, typeParameters: readonly TypeParameter[] | undefined, minTypeArgumentCount: number, isJavaScriptImplicitAny: boolean): Type[] | undefined; - function fillMissingTypeArguments(typeArguments: readonly Type[] | undefined, typeParameters: readonly TypeParameter[] | undefined, minTypeArgumentCount: number, isJavaScriptImplicitAny: boolean) { - const numTypeParameters = length(typeParameters); - if (!numTypeParameters) { - return []; - } - const numTypeArguments = length(typeArguments); - if (isJavaScriptImplicitAny || (numTypeArguments >= minTypeArgumentCount && numTypeArguments <= numTypeParameters)) { - const result = typeArguments ? typeArguments.slice() : []; - // Map invalid forward references in default types to the error type - for (let i = numTypeArguments; i < numTypeParameters; i++) { - result[i] = errorType; - } - const baseDefaultType = getDefaultTypeArgumentType(isJavaScriptImplicitAny); - for (let i = numTypeArguments; i < numTypeParameters; i++) { - let defaultType = getDefaultFromTypeParameter(typeParameters![i]); - if (isJavaScriptImplicitAny && defaultType && (isTypeIdenticalTo(defaultType, unknownType) || isTypeIdenticalTo(defaultType, emptyObjectType))) { - defaultType = anyType; - } - result[i] = defaultType ? instantiateType(defaultType, createTypeMapper(typeParameters!, result)) : baseDefaultType; - } - result.length = typeParameters!.length; - return result; - } - return typeArguments && typeArguments.slice(); - } - - function getSignatureFromDeclaration(declaration: SignatureDeclaration | JSDocSignature): Signature { - const links = getNodeLinks(declaration); - if (!links.resolvedSignature) { - const parameters: Symbol[] = []; - let flags = SignatureFlags.None; - let minArgumentCount = 0; - let thisParameter: Symbol | undefined; - let hasThisParameter = false; - const iife = getImmediatelyInvokedFunctionExpression(declaration); - const isJSConstructSignature = isJSDocConstructSignature(declaration); - const isUntypedSignatureInJSFile = !iife && - isInJSFile(declaration) && - isValueSignatureDeclaration(declaration) && - !hasJSDocParameterTags(declaration) && - !getJSDocType(declaration); - - // If this is a JSDoc construct signature, then skip the first parameter in the - // parameter list. The first parameter represents the return type of the construct - // signature. - for (let i = isJSConstructSignature ? 1 : 0; i < declaration.parameters.length; i++) { - const param = declaration.parameters[i]; - - let paramSymbol = param.symbol; - const type = isJSDocParameterTag(param) ? (param.typeExpression && param.typeExpression.type) : param.type; - // Include parameter symbol instead of property symbol in the signature - if (paramSymbol && !!(paramSymbol.flags & SymbolFlags.Property) && !isBindingPattern(param.name)) { - const resolvedSymbol = resolveName(param, paramSymbol.escapedName, SymbolFlags.Value, undefined, undefined, /*isUse*/ false); - paramSymbol = resolvedSymbol!; - } - if (i === 0 && paramSymbol.escapedName === InternalSymbolName.This) { - hasThisParameter = true; - thisParameter = param.symbol; - } - else { - parameters.push(paramSymbol); - } - - if (type && type.kind === SyntaxKind.LiteralType) { - flags |= SignatureFlags.HasLiteralTypes; - } - - // Record a new minimum argument count if this is not an optional parameter - const isOptionalParameter = isOptionalJSDocParameterTag(param) || - param.initializer || param.questionToken || param.dotDotDotToken || - iife && parameters.length > iife.arguments.length && !type || - isUntypedSignatureInJSFile || - isJSDocOptionalParameter(param); - if (!isOptionalParameter) { - minArgumentCount = parameters.length; - } - } - - // If only one accessor includes a this-type annotation, the other behaves as if it had the same type annotation - if ((declaration.kind === SyntaxKind.GetAccessor || declaration.kind === SyntaxKind.SetAccessor) && - !hasNonBindableDynamicName(declaration) && - (!hasThisParameter || !thisParameter)) { - const otherKind = declaration.kind === SyntaxKind.GetAccessor ? SyntaxKind.SetAccessor : SyntaxKind.GetAccessor; - const other = getDeclarationOfKind(getSymbolOfNode(declaration), otherKind); - if (other) { - thisParameter = getAnnotatedAccessorThisParameter(other); - } - } - - const classType = declaration.kind === SyntaxKind.Constructor ? - getDeclaredTypeOfClassOrInterface(getMergedSymbol((declaration.parent).symbol)) - : undefined; - const typeParameters = classType ? classType.localTypeParameters : getTypeParametersFromDeclaration(declaration); - if (hasRestParameter(declaration) || isInJSFile(declaration) && maybeAddJsSyntheticRestParameter(declaration, parameters)) { - flags |= SignatureFlags.HasRestParameter; - } - links.resolvedSignature = createSignature(declaration, typeParameters, thisParameter, parameters, - /*resolvedReturnType*/ undefined, /*resolvedTypePredicate*/ undefined, - minArgumentCount, flags); - } - return links.resolvedSignature; - } - - /** - * A JS function gets a synthetic rest parameter if it references `arguments` AND: - * 1. It has no parameters but at least one `@param` with a type that starts with `...` - * OR - * 2. It has at least one parameter, and the last parameter has a matching `@param` with a type that starts with `...` - */ - function maybeAddJsSyntheticRestParameter(declaration: SignatureDeclaration | JSDocSignature, parameters: Symbol[]): boolean { - if (isJSDocSignature(declaration) || !containsArgumentsReference(declaration)) { - return false; - } - const lastParam = lastOrUndefined(declaration.parameters); - const lastParamTags = lastParam ? getJSDocParameterTags(lastParam) : getJSDocTags(declaration).filter(isJSDocParameterTag); - const lastParamVariadicType = firstDefined(lastParamTags, p => - p.typeExpression && isJSDocVariadicType(p.typeExpression.type) ? p.typeExpression.type : undefined); - - const syntheticArgsSymbol = createSymbol(SymbolFlags.Variable, "args" as __String, CheckFlags.RestParameter); - syntheticArgsSymbol.type = lastParamVariadicType ? createArrayType(getTypeFromTypeNode(lastParamVariadicType.type)) : anyArrayType; - if (lastParamVariadicType) { - // Replace the last parameter with a rest parameter. - parameters.pop(); - } - parameters.push(syntheticArgsSymbol); - return true; - } - - function getSignatureOfTypeTag(node: SignatureDeclaration | JSDocSignature) { - const typeTag = isInJSFile(node) ? getJSDocTypeTag(node) : undefined; - const signature = typeTag && typeTag.typeExpression && getSingleCallSignature(getTypeFromTypeNode(typeTag.typeExpression)); - return signature && getErasedSignature(signature); - } - - function getReturnTypeOfTypeTag(node: SignatureDeclaration | JSDocSignature) { - const signature = getSignatureOfTypeTag(node); - return signature && getReturnTypeOfSignature(signature); - } - - function containsArgumentsReference(declaration: SignatureDeclaration): boolean { - const links = getNodeLinks(declaration); - if (links.containsArgumentsReference === undefined) { - if (links.flags & NodeCheckFlags.CaptureArguments) { - links.containsArgumentsReference = true; - } - else { - links.containsArgumentsReference = traverse((declaration as FunctionLikeDeclaration).body!); - } - } - return links.containsArgumentsReference; - - function traverse(node: Node): boolean { - if (!node) return false; - switch (node.kind) { - case SyntaxKind.Identifier: - return (node).escapedText === "arguments" && isExpressionNode(node); - - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - return (node).name!.kind === SyntaxKind.ComputedPropertyName - && traverse((node).name!); - - default: - return !nodeStartsNewLexicalEnvironment(node) && !isPartOfTypeNode(node) && !!forEachChild(node, traverse); - } - } - } - - function getSignaturesOfSymbol(symbol: Symbol | undefined): Signature[] { - if (!symbol) return emptyArray; - const result: Signature[] = []; - for (let i = 0; i < symbol.declarations.length; i++) { - const decl = symbol.declarations[i]; - if (!isFunctionLike(decl)) continue; - // Don't include signature if node is the implementation of an overloaded function. A node is considered - // an implementation node if it has a body and the previous node is of the same kind and immediately - // precedes the implementation node (i.e. has the same parent and ends where the implementation starts). - if (i > 0 && (decl as FunctionLikeDeclaration).body) { - const previous = symbol.declarations[i - 1]; - if (decl.parent === previous.parent && decl.kind === previous.kind && decl.pos === previous.end) { - continue; - } - } - result.push(getSignatureFromDeclaration(decl)); - } - return result; - } - - function resolveExternalModuleTypeByLiteral(name: StringLiteral) { - const moduleSym = resolveExternalModuleName(name, name); - if (moduleSym) { - const resolvedModuleSymbol = resolveExternalModuleSymbol(moduleSym); - if (resolvedModuleSymbol) { - return getTypeOfSymbol(resolvedModuleSymbol); - } - } - - return anyType; - } - - function getThisTypeOfSignature(signature: Signature): Type | undefined { - if (signature.thisParameter) { - return getTypeOfSymbol(signature.thisParameter); - } - } - - function getTypePredicateOfSignature(signature: Signature): TypePredicate | undefined { - if (!signature.resolvedTypePredicate) { - if (signature.target) { - const targetTypePredicate = getTypePredicateOfSignature(signature.target); - signature.resolvedTypePredicate = targetTypePredicate ? instantiateTypePredicate(targetTypePredicate, signature.mapper!) : noTypePredicate; - } - else if (signature.unionSignatures) { - signature.resolvedTypePredicate = getUnionTypePredicate(signature.unionSignatures) || noTypePredicate; - } - else { - const type = signature.declaration && getEffectiveReturnTypeNode(signature.declaration); - let jsdocPredicate: TypePredicate | undefined; - if (!type && isInJSFile(signature.declaration)) { - const jsdocSignature = getSignatureOfTypeTag(signature.declaration!); - if (jsdocSignature && signature !== jsdocSignature) { - jsdocPredicate = getTypePredicateOfSignature(jsdocSignature); - } - } - signature.resolvedTypePredicate = type && isTypePredicateNode(type) ? - createTypePredicateFromTypePredicateNode(type, signature) : - jsdocPredicate || noTypePredicate; - } - Debug.assert(!!signature.resolvedTypePredicate); - } - return signature.resolvedTypePredicate === noTypePredicate ? undefined : signature.resolvedTypePredicate; - } - - function createTypePredicateFromTypePredicateNode(node: TypePredicateNode, signature: Signature): TypePredicate { - const parameterName = node.parameterName; - const type = node.type && getTypeFromTypeNode(node.type); - return parameterName.kind === SyntaxKind.ThisType ? - createTypePredicate(node.assertsModifier ? TypePredicateKind.AssertsThis : TypePredicateKind.This, /*parameterName*/ undefined, /*parameterIndex*/ undefined, type) : - createTypePredicate(node.assertsModifier ? TypePredicateKind.AssertsIdentifier : TypePredicateKind.Identifier, parameterName.escapedText as string, - findIndex(signature.parameters, p => p.escapedName === parameterName.escapedText), type); - } - - function getReturnTypeOfSignature(signature: Signature): Type { - if (!signature.resolvedReturnType) { - if (!pushTypeResolution(signature, TypeSystemPropertyName.ResolvedReturnType)) { - return errorType; - } - let type = signature.target ? instantiateType(getReturnTypeOfSignature(signature.target), signature.mapper) : - signature.unionSignatures ? getUnionType(map(signature.unionSignatures, getReturnTypeOfSignature), UnionReduction.Subtype) : - getReturnTypeFromAnnotation(signature.declaration!) || - (nodeIsMissing((signature.declaration).body) ? anyType : getReturnTypeFromBody(signature.declaration)); - if (signature.flags & SignatureFlags.IsInnerCallChain) { - type = addOptionalTypeMarker(type); - } - else if (signature.flags & SignatureFlags.IsOuterCallChain) { - type = getOptionalType(type); - } - if (!popTypeResolution()) { - if (signature.declaration) { - const typeNode = getEffectiveReturnTypeNode(signature.declaration); - if (typeNode) { - error(typeNode, Diagnostics.Return_type_annotation_circularly_references_itself); - } - else if (noImplicitAny) { - const declaration = signature.declaration; - const name = getNameOfDeclaration(declaration); - if (name) { - error(name, Diagnostics._0_implicitly_has_return_type_any_because_it_does_not_have_a_return_type_annotation_and_is_referenced_directly_or_indirectly_in_one_of_its_return_expressions, declarationNameToString(name)); - } - else { - error(declaration, Diagnostics.Function_implicitly_has_return_type_any_because_it_does_not_have_a_return_type_annotation_and_is_referenced_directly_or_indirectly_in_one_of_its_return_expressions); - } - } - } - type = anyType; - } - signature.resolvedReturnType = type; - } - return signature.resolvedReturnType; - } - - function getReturnTypeFromAnnotation(declaration: SignatureDeclaration | JSDocSignature) { - if (declaration.kind === SyntaxKind.Constructor) { - return getDeclaredTypeOfClassOrInterface(getMergedSymbol((declaration.parent).symbol)); - } - if (isJSDocConstructSignature(declaration)) { - return getTypeFromTypeNode((declaration.parameters[0] as ParameterDeclaration).type!); // TODO: GH#18217 - } - const typeNode = getEffectiveReturnTypeNode(declaration); - if (typeNode) { - return getTypeFromTypeNode(typeNode); - } - if (declaration.kind === SyntaxKind.GetAccessor && !hasNonBindableDynamicName(declaration)) { - const jsDocType = isInJSFile(declaration) && getTypeForDeclarationFromJSDocComment(declaration); - if (jsDocType) { - return jsDocType; - } - const setter = getDeclarationOfKind(getSymbolOfNode(declaration), SyntaxKind.SetAccessor); - const setterType = getAnnotatedAccessorType(setter); - if (setterType) { - return setterType; - } - } - return getReturnTypeOfTypeTag(declaration); - } - - function isResolvingReturnTypeOfSignature(signature: Signature) { - return !signature.resolvedReturnType && findResolutionCycleStartIndex(signature, TypeSystemPropertyName.ResolvedReturnType) >= 0; - } - - function getRestTypeOfSignature(signature: Signature): Type { - return tryGetRestTypeOfSignature(signature) || anyType; - } - - function tryGetRestTypeOfSignature(signature: Signature): Type | undefined { - if (signatureHasRestParameter(signature)) { - const sigRestType = getTypeOfSymbol(signature.parameters[signature.parameters.length - 1]); - const restType = isTupleType(sigRestType) ? getRestTypeOfTupleType(sigRestType) : sigRestType; - return restType && getIndexTypeOfType(restType, IndexKind.Number); - } - return undefined; - } - - function getSignatureInstantiation(signature: Signature, typeArguments: Type[] | undefined, isJavascript: boolean, inferredTypeParameters?: readonly TypeParameter[]): Signature { - const instantiatedSignature = getSignatureInstantiationWithoutFillingInTypeArguments(signature, fillMissingTypeArguments(typeArguments, signature.typeParameters, getMinTypeArgumentCount(signature.typeParameters), isJavascript)); - if (inferredTypeParameters) { - const returnSignature = getSingleCallOrConstructSignature(getReturnTypeOfSignature(instantiatedSignature)); - if (returnSignature) { - const newReturnSignature = cloneSignature(returnSignature); - newReturnSignature.typeParameters = inferredTypeParameters; - const newInstantiatedSignature = cloneSignature(instantiatedSignature); - newInstantiatedSignature.resolvedReturnType = getOrCreateTypeFromSignature(newReturnSignature); - return newInstantiatedSignature; - } - } - return instantiatedSignature; - } - - function getSignatureInstantiationWithoutFillingInTypeArguments(signature: Signature, typeArguments: readonly Type[] | undefined): Signature { - const instantiations = signature.instantiations || (signature.instantiations = createMap()); - const id = getTypeListId(typeArguments); - let instantiation = instantiations.get(id); - if (!instantiation) { - instantiations.set(id, instantiation = createSignatureInstantiation(signature, typeArguments)); - } - return instantiation; - } - - function createSignatureInstantiation(signature: Signature, typeArguments: readonly Type[] | undefined): Signature { - return instantiateSignature(signature, createSignatureTypeMapper(signature, typeArguments), /*eraseTypeParameters*/ true); - } - - function createSignatureTypeMapper(signature: Signature, typeArguments: readonly Type[] | undefined): TypeMapper { - return createTypeMapper(signature.typeParameters!, typeArguments); - } - - function getErasedSignature(signature: Signature): Signature { - return signature.typeParameters ? - signature.erasedSignatureCache || (signature.erasedSignatureCache = createErasedSignature(signature)) : - signature; - } - - function createErasedSignature(signature: Signature) { - // Create an instantiation of the signature where all type arguments are the any type. - return instantiateSignature(signature, createTypeEraser(signature.typeParameters!), /*eraseTypeParameters*/ true); - } - - function getCanonicalSignature(signature: Signature): Signature { - return signature.typeParameters ? - signature.canonicalSignatureCache || (signature.canonicalSignatureCache = createCanonicalSignature(signature)) : - signature; - } - - function createCanonicalSignature(signature: Signature) { - // Create an instantiation of the signature where each unconstrained type parameter is replaced with - // its original. When a generic class or interface is instantiated, each generic method in the class or - // interface is instantiated with a fresh set of cloned type parameters (which we need to handle scenarios - // where different generations of the same type parameter are in scope). This leads to a lot of new type - // identities, and potentially a lot of work comparing those identities, so here we create an instantiation - // that uses the original type identities for all unconstrained type parameters. - return getSignatureInstantiation( - signature, - map(signature.typeParameters, tp => tp.target && !getConstraintOfTypeParameter(tp.target) ? tp.target : tp), - isInJSFile(signature.declaration)); - } - - function getBaseSignature(signature: Signature) { - const typeParameters = signature.typeParameters; - if (typeParameters) { - const typeEraser = createTypeEraser(typeParameters); - const baseConstraints = map(typeParameters, tp => instantiateType(getBaseConstraintOfType(tp), typeEraser) || unknownType); - return instantiateSignature(signature, createTypeMapper(typeParameters, baseConstraints), /*eraseTypeParameters*/ true); - } - return signature; - } - - function getOrCreateTypeFromSignature(signature: Signature): ObjectType { - // There are two ways to declare a construct signature, one is by declaring a class constructor - // using the constructor keyword, and the other is declaring a bare construct signature in an - // object type literal or interface (using the new keyword). Each way of declaring a constructor - // will result in a different declaration kind. - if (!signature.isolatedSignatureType) { - const kind = signature.declaration ? signature.declaration.kind : SyntaxKind.Unknown; - const isConstructor = kind === SyntaxKind.Constructor || kind === SyntaxKind.ConstructSignature || kind === SyntaxKind.ConstructorType; - const type = createObjectType(ObjectFlags.Anonymous); - type.members = emptySymbols; - type.properties = emptyArray; - type.callSignatures = !isConstructor ? [signature] : emptyArray; - type.constructSignatures = isConstructor ? [signature] : emptyArray; - signature.isolatedSignatureType = type; - } - - return signature.isolatedSignatureType; - } - - function getIndexSymbol(symbol: Symbol): Symbol | undefined { - return symbol.members!.get(InternalSymbolName.Index); - } - - function getIndexDeclarationOfSymbol(symbol: Symbol, kind: IndexKind): IndexSignatureDeclaration | undefined { - const syntaxKind = kind === IndexKind.Number ? SyntaxKind.NumberKeyword : SyntaxKind.StringKeyword; - const indexSymbol = getIndexSymbol(symbol); - if (indexSymbol) { - for (const decl of indexSymbol.declarations) { - const node = cast(decl, isIndexSignatureDeclaration); - if (node.parameters.length === 1) { - const parameter = node.parameters[0]; - if (parameter.type && parameter.type.kind === syntaxKind) { - return node; - } - } - } - } - - return undefined; - } - - function createIndexInfo(type: Type, isReadonly: boolean, declaration?: IndexSignatureDeclaration): IndexInfo { - return { type, isReadonly, declaration }; - } - - function getIndexInfoOfSymbol(symbol: Symbol, kind: IndexKind): IndexInfo | undefined { - const declaration = getIndexDeclarationOfSymbol(symbol, kind); - if (declaration) { - return createIndexInfo(declaration.type ? getTypeFromTypeNode(declaration.type) : anyType, - hasModifier(declaration, ModifierFlags.Readonly), declaration); - } - return undefined; - } - - function getConstraintDeclaration(type: TypeParameter): TypeNode | undefined { - return mapDefined(filter(type.symbol && type.symbol.declarations, isTypeParameterDeclaration), getEffectiveConstraintOfTypeParameter)[0]; - } - - function getInferredTypeParameterConstraint(typeParameter: TypeParameter) { - let inferences: Type[] | undefined; - if (typeParameter.symbol) { - for (const declaration of typeParameter.symbol.declarations) { - if (declaration.parent.kind === SyntaxKind.InferType) { - // When an 'infer T' declaration is immediately contained in a type reference node - // (such as 'Foo'), T's constraint is inferred from the constraint of the - // corresponding type parameter in 'Foo'. When multiple 'infer T' declarations are - // present, we form an intersection of the inferred constraint types. - const grandParent = declaration.parent.parent; - if (grandParent.kind === SyntaxKind.TypeReference) { - const typeReference = grandParent; - const typeParameters = getTypeParametersForTypeReference(typeReference); - if (typeParameters) { - const index = typeReference.typeArguments!.indexOf(declaration.parent); - if (index < typeParameters.length) { - const declaredConstraint = getConstraintOfTypeParameter(typeParameters[index]); - if (declaredConstraint) { - // Type parameter constraints can reference other type parameters so - // constraints need to be instantiated. If instantiation produces the - // type parameter itself, we discard that inference. For example, in - // type Foo = [T, U]; - // type Bar = T extends Foo ? Foo : T; - // the instantiated constraint for U is X, so we discard that inference. - const mapper = createTypeMapper(typeParameters, getEffectiveTypeArguments(typeReference, typeParameters)); - const constraint = instantiateType(declaredConstraint, mapper); - if (constraint !== typeParameter) { - inferences = append(inferences, constraint); - } - } - } - } - } - // When an 'infer T' declaration is immediately contained in a rest parameter - // declaration, we infer an 'unknown[]' constraint. - else if (grandParent.kind === SyntaxKind.Parameter && (grandParent).dotDotDotToken) { - inferences = append(inferences, createArrayType(unknownType)); - } - } - } - } - return inferences && getIntersectionType(inferences); - } - - /** This is a worker function. Use getConstraintOfTypeParameter which guards against circular constraints. */ - function getConstraintFromTypeParameter(typeParameter: TypeParameter): Type | undefined { - if (!typeParameter.constraint) { - if (typeParameter.target) { - const targetConstraint = getConstraintOfTypeParameter(typeParameter.target); - typeParameter.constraint = targetConstraint ? instantiateType(targetConstraint, typeParameter.mapper) : noConstraintType; - } - else { - const constraintDeclaration = getConstraintDeclaration(typeParameter); - typeParameter.constraint = constraintDeclaration ? getTypeFromTypeNode(constraintDeclaration) : - getInferredTypeParameterConstraint(typeParameter) || noConstraintType; - } - } - return typeParameter.constraint === noConstraintType ? undefined : typeParameter.constraint; - } - - function getParentSymbolOfTypeParameter(typeParameter: TypeParameter): Symbol | undefined { - const tp = getDeclarationOfKind(typeParameter.symbol, SyntaxKind.TypeParameter)!; - const host = isJSDocTemplateTag(tp.parent) ? getHostSignatureFromJSDoc(tp.parent) : tp.parent; - return host && getSymbolOfNode(host); - } - - function getTypeListId(types: readonly Type[] | undefined) { - let result = ""; - if (types) { - const length = types.length; - let i = 0; - while (i < length) { - const startId = types[i].id; - let count = 1; - while (i + count < length && types[i + count].id === startId + count) { - count++; - } - if (result.length) { - result += ","; - } - result += startId; - if (count > 1) { - result += ":" + count; - } - i += count; - } - } - return result; - } - - // This function is used to propagate certain flags when creating new object type references and union types. - // It is only necessary to do so if a constituent type might be the undefined type, the null type, the type - // of an object literal or the anyFunctionType. This is because there are operations in the type checker - // that care about the presence of such types at arbitrary depth in a containing type. - function getPropagatingFlagsOfTypes(types: readonly Type[], excludeKinds: TypeFlags): ObjectFlags { - let result: ObjectFlags = 0; - for (const type of types) { - if (!(type.flags & excludeKinds)) { - result |= getObjectFlags(type); - } - } - return result & ObjectFlags.PropagatingFlags; - } - - function createTypeReference(target: GenericType, typeArguments: readonly Type[] | undefined): TypeReference { - const id = getTypeListId(typeArguments); - let type = target.instantiations.get(id); - if (!type) { - type = createObjectType(ObjectFlags.Reference, target.symbol); - target.instantiations.set(id, type); - type.objectFlags |= typeArguments ? getPropagatingFlagsOfTypes(typeArguments, /*excludeKinds*/ 0) : 0; - type.target = target; - type.resolvedTypeArguments = typeArguments; - } - return type; - } - - function cloneTypeReference(source: TypeReference): TypeReference { - const type = createType(source.flags); - type.symbol = source.symbol; - type.objectFlags = source.objectFlags; - type.target = source.target; - type.resolvedTypeArguments = source.resolvedTypeArguments; - return type; - } - - function createDeferredTypeReference(target: GenericType, node: TypeReferenceNode | ArrayTypeNode | TupleTypeNode, mapper?: TypeMapper): DeferredTypeReference { - const aliasSymbol = getAliasSymbolForTypeNode(node); - const aliasTypeArguments = getTypeArgumentsForAliasSymbol(aliasSymbol); - const type = createObjectType(ObjectFlags.Reference, target.symbol); - type.target = target; - type.node = node; - type.mapper = mapper; - type.aliasSymbol = aliasSymbol; - type.aliasTypeArguments = mapper ? instantiateTypes(aliasTypeArguments, mapper) : aliasTypeArguments; - return type; - } - - function getTypeArguments(type: TypeReference): readonly Type[] { - if (!type.resolvedTypeArguments) { - if (!pushTypeResolution(type, TypeSystemPropertyName.ResolvedTypeArguments)) { - return type.target.localTypeParameters?.map(() => errorType) || emptyArray; - } - const node = type.node; - const typeArguments = !node ? emptyArray : - node.kind === SyntaxKind.TypeReference ? concatenate(type.target.outerTypeParameters, getEffectiveTypeArguments(node, type.target.localTypeParameters!)) : - node.kind === SyntaxKind.ArrayType ? [getTypeFromTypeNode(node.elementType)] : - map(node.elementTypes, getTypeFromTypeNode); - if (popTypeResolution()) { - type.resolvedTypeArguments = type.mapper ? instantiateTypes(typeArguments, type.mapper) : typeArguments; - } - else { - type.resolvedTypeArguments = type.target.localTypeParameters?.map(() => errorType) || emptyArray; - error( - type.node || currentNode, - type.target.symbol - ? Diagnostics.Type_arguments_for_0_circularly_reference_themselves - : Diagnostics.Tuple_type_arguments_circularly_reference_themselves - , - type.target.symbol && symbolToString(type.target.symbol) - ); - } - } - return type.resolvedTypeArguments; - } - - function getTypeReferenceArity(type: TypeReference): number { - return length(type.target.typeParameters); - } - - - /** - * Get type from type-reference that reference to class or interface - */ - function getTypeFromClassOrInterfaceReference(node: NodeWithTypeArguments, symbol: Symbol): Type { - const type = getDeclaredTypeOfSymbol(getMergedSymbol(symbol)); - const typeParameters = type.localTypeParameters; - if (typeParameters) { - const numTypeArguments = length(node.typeArguments); - const minTypeArgumentCount = getMinTypeArgumentCount(typeParameters); - const isJs = isInJSFile(node); - const isJsImplicitAny = !noImplicitAny && isJs; - if (!isJsImplicitAny && (numTypeArguments < minTypeArgumentCount || numTypeArguments > typeParameters.length)) { - const missingAugmentsTag = isJs && isExpressionWithTypeArguments(node) && !isJSDocAugmentsTag(node.parent); - const diag = minTypeArgumentCount === typeParameters.length ? - missingAugmentsTag ? - Diagnostics.Expected_0_type_arguments_provide_these_with_an_extends_tag : - Diagnostics.Generic_type_0_requires_1_type_argument_s : - missingAugmentsTag ? - Diagnostics.Expected_0_1_type_arguments_provide_these_with_an_extends_tag : - Diagnostics.Generic_type_0_requires_between_1_and_2_type_arguments; - - const typeStr = typeToString(type, /*enclosingDeclaration*/ undefined, TypeFormatFlags.WriteArrayAsGenericType); - error(node, diag, typeStr, minTypeArgumentCount, typeParameters.length); - if (!isJs) { - // TODO: Adopt same permissive behavior in TS as in JS to reduce follow-on editing experience failures (requires editing fillMissingTypeArguments) - return errorType; - } - } - if (node.kind === SyntaxKind.TypeReference && isDeferredTypeReferenceNode(node, length(node.typeArguments) !== typeParameters.length)) { - return createDeferredTypeReference(type, node, /*mapper*/ undefined); - } - // In a type reference, the outer type parameters of the referenced class or interface are automatically - // supplied as type arguments and the type reference only specifies arguments for the local type parameters - // of the class or interface. - const typeArguments = concatenate(type.outerTypeParameters, fillMissingTypeArguments(typeArgumentsFromTypeReferenceNode(node), typeParameters, minTypeArgumentCount, isJs)); - return createTypeReference(type, typeArguments); - } - return checkNoTypeArguments(node, symbol) ? type : errorType; - } - - function getTypeAliasInstantiation(symbol: Symbol, typeArguments: readonly Type[] | undefined): Type { - const type = getDeclaredTypeOfSymbol(symbol); - const links = getSymbolLinks(symbol); - const typeParameters = links.typeParameters!; - const id = getTypeListId(typeArguments); - let instantiation = links.instantiations!.get(id); - if (!instantiation) { - links.instantiations!.set(id, instantiation = instantiateType(type, createTypeMapper(typeParameters, fillMissingTypeArguments(typeArguments, typeParameters, getMinTypeArgumentCount(typeParameters), isInJSFile(symbol.valueDeclaration))))); - } - return instantiation; - } - - /** - * Get type from reference to type alias. When a type alias is generic, the declared type of the type alias may include - * references to the type parameters of the alias. We replace those with the actual type arguments by instantiating the - * declared type. Instantiations are cached using the type identities of the type arguments as the key. - */ - function getTypeFromTypeAliasReference(node: NodeWithTypeArguments, symbol: Symbol): Type { - const type = getDeclaredTypeOfSymbol(symbol); - const typeParameters = getSymbolLinks(symbol).typeParameters; - if (typeParameters) { - const numTypeArguments = length(node.typeArguments); - const minTypeArgumentCount = getMinTypeArgumentCount(typeParameters); - if (numTypeArguments < minTypeArgumentCount || numTypeArguments > typeParameters.length) { - error(node, - minTypeArgumentCount === typeParameters.length ? - Diagnostics.Generic_type_0_requires_1_type_argument_s : - Diagnostics.Generic_type_0_requires_between_1_and_2_type_arguments, - symbolToString(symbol), - minTypeArgumentCount, - typeParameters.length); - return errorType; - } - return getTypeAliasInstantiation(symbol, typeArgumentsFromTypeReferenceNode(node)); - } - return checkNoTypeArguments(node, symbol) ? type : errorType; - } - - function getTypeReferenceName(node: TypeReferenceType): EntityNameOrEntityNameExpression | undefined { - switch (node.kind) { - case SyntaxKind.TypeReference: - return node.typeName; - case SyntaxKind.ExpressionWithTypeArguments: - // We only support expressions that are simple qualified names. For other - // expressions this produces undefined. - const expr = node.expression; - if (isEntityNameExpression(expr)) { - return expr; - } - // fall through; - } - - return undefined; - } - - function resolveTypeReferenceName(typeReferenceName: EntityNameExpression | EntityName | undefined, meaning: SymbolFlags, ignoreErrors?: boolean) { - if (!typeReferenceName) { - return unknownSymbol; - } - - return resolveEntityName(typeReferenceName, meaning, ignoreErrors) || unknownSymbol; - } - - function getTypeReferenceType(node: NodeWithTypeArguments, symbol: Symbol): Type { - if (symbol === unknownSymbol) { - return errorType; - } - symbol = getExpandoSymbol(symbol) || symbol; - if (symbol.flags & (SymbolFlags.Class | SymbolFlags.Interface)) { - return getTypeFromClassOrInterfaceReference(node, symbol); - } - if (symbol.flags & SymbolFlags.TypeAlias) { - return getTypeFromTypeAliasReference(node, symbol); - } - // Get type from reference to named type that cannot be generic (enum or type parameter) - const res = tryGetDeclaredTypeOfSymbol(symbol); - if (res) { - return checkNoTypeArguments(node, symbol) ? - res.flags & TypeFlags.TypeParameter ? getConstrainedTypeVariable(res, node) : getRegularTypeOfLiteralType(res) : - errorType; - } - if (symbol.flags & SymbolFlags.Value && isJSDocTypeReference(node)) { - const jsdocType = getTypeFromJSDocValueReference(node, symbol); - if (jsdocType) { - return jsdocType; - } - else { - // Resolve the type reference as a Type for the purpose of reporting errors. - resolveTypeReferenceName(getTypeReferenceName(node), SymbolFlags.Type); - return getTypeOfSymbol(symbol); - } - } - return errorType; - } - - /** - * A JSdoc TypeReference may be to a value, but resolve it as a type anyway. - * Note: If the value is imported from commonjs, it should really be an alias, - * but this function's special-case code fakes alias resolution as well. - */ - function getTypeFromJSDocValueReference(node: NodeWithTypeArguments, symbol: Symbol): Type | undefined { - const links = getNodeLinks(node); - if (!links.resolvedJSDocType) { - const valueType = getTypeOfSymbol(symbol); - let typeType = valueType; - if (symbol.valueDeclaration) { - const decl = getRootDeclaration(symbol.valueDeclaration); - let isRequireAlias = false; - if (isVariableDeclaration(decl) && decl.initializer) { - let expr = decl.initializer; - // skip past entity names, eg `require("x").a.b.c` - while (isPropertyAccessExpression(expr)) { - expr = expr.expression; - } - isRequireAlias = isCallExpression(expr) && isRequireCall(expr, /*requireStringLiteralLikeArgument*/ true) && !!valueType.symbol; - } - const isImportTypeWithQualifier = node.kind === SyntaxKind.ImportType && (node as ImportTypeNode).qualifier; - // valueType might not have a symbol, eg, {import('./b').STRING_LITERAL} - if (valueType.symbol && (isRequireAlias || isImportTypeWithQualifier)) { - typeType = getTypeReferenceType(node, valueType.symbol); - } - } - links.resolvedJSDocType = typeType; - } - return links.resolvedJSDocType; - } - - function getSubstitutionType(typeVariable: TypeVariable, substitute: Type) { - if (substitute.flags & TypeFlags.AnyOrUnknown || substitute === typeVariable) { - return typeVariable; - } - const id = `${getTypeId(typeVariable)}>${getTypeId(substitute)}`; - const cached = substitutionTypes.get(id); - if (cached) { - return cached; - } - const result = createType(TypeFlags.Substitution); - result.typeVariable = typeVariable; - result.substitute = substitute; - substitutionTypes.set(id, result); - return result; - } - - function isUnaryTupleTypeNode(node: TypeNode) { - return node.kind === SyntaxKind.TupleType && (node).elementTypes.length === 1; - } - - function getImpliedConstraint(typeVariable: TypeVariable, checkNode: TypeNode, extendsNode: TypeNode): Type | undefined { - return isUnaryTupleTypeNode(checkNode) && isUnaryTupleTypeNode(extendsNode) ? getImpliedConstraint(typeVariable, (checkNode).elementTypes[0], (extendsNode).elementTypes[0]) : - getActualTypeVariable(getTypeFromTypeNode(checkNode)) === typeVariable ? getTypeFromTypeNode(extendsNode) : - undefined; - } - - function getConstrainedTypeVariable(typeVariable: TypeVariable, node: Node) { - let constraints: Type[] | undefined; - while (node && !isStatement(node) && node.kind !== SyntaxKind.JSDocComment) { - const parent = node.parent; - if (parent.kind === SyntaxKind.ConditionalType && node === (parent).trueType) { - const constraint = getImpliedConstraint(typeVariable, (parent).checkType, (parent).extendsType); - if (constraint) { - constraints = append(constraints, constraint); - } - } - node = parent; - } - return constraints ? getSubstitutionType(typeVariable, getIntersectionType(append(constraints, typeVariable))) : typeVariable; - } - - function isJSDocTypeReference(node: Node): node is TypeReferenceNode { - return !!(node.flags & NodeFlags.JSDoc) && (node.kind === SyntaxKind.TypeReference || node.kind === SyntaxKind.ImportType); - } - - function checkNoTypeArguments(node: NodeWithTypeArguments, symbol?: Symbol) { - if (node.typeArguments) { - error(node, Diagnostics.Type_0_is_not_generic, symbol ? symbolToString(symbol) : (node).typeName ? declarationNameToString((node).typeName) : anon); - return false; - } - return true; - } - - function getIntendedTypeFromJSDocTypeReference(node: TypeReferenceNode): Type | undefined { - if (isIdentifier(node.typeName)) { - const typeArgs = node.typeArguments; - switch (node.typeName.escapedText) { - case "String": - checkNoTypeArguments(node); - return stringType; - case "Number": - checkNoTypeArguments(node); - return numberType; - case "Boolean": - checkNoTypeArguments(node); - return booleanType; - case "Void": - checkNoTypeArguments(node); - return voidType; - case "Undefined": - checkNoTypeArguments(node); - return undefinedType; - case "Null": - checkNoTypeArguments(node); - return nullType; - case "Function": - case "function": - checkNoTypeArguments(node); - return globalFunctionType; - case "array": - return (!typeArgs || !typeArgs.length) && !noImplicitAny ? anyArrayType : undefined; - case "promise": - return (!typeArgs || !typeArgs.length) && !noImplicitAny ? createPromiseType(anyType) : undefined; - case "Object": - if (typeArgs && typeArgs.length === 2) { - if (isJSDocIndexSignature(node)) { - const indexed = getTypeFromTypeNode(typeArgs[0]); - const target = getTypeFromTypeNode(typeArgs[1]); - const index = createIndexInfo(target, /*isReadonly*/ false); - return createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, indexed === stringType ? index : undefined, indexed === numberType ? index : undefined); - } - return anyType; - } - checkNoTypeArguments(node); - return !noImplicitAny ? anyType : undefined; - } - } - } - - function getTypeFromJSDocNullableTypeNode(node: JSDocNullableType) { - const type = getTypeFromTypeNode(node.type); - return strictNullChecks ? getNullableType(type, TypeFlags.Null) : type; - } - - function getTypeFromTypeReference(node: TypeReferenceType): Type { - const links = getNodeLinks(node); - if (!links.resolvedType) { - // handle LS queries on the `const` in `x as const` by resolving to the type of `x` - if (isConstTypeReference(node) && isAssertionExpression(node.parent)) { - links.resolvedSymbol = unknownSymbol; - return links.resolvedType = checkExpressionCached(node.parent.expression); - } - let symbol: Symbol | undefined; - let type: Type | undefined; - const meaning = SymbolFlags.Type; - if (isJSDocTypeReference(node)) { - type = getIntendedTypeFromJSDocTypeReference(node); - if (!type) { - symbol = resolveTypeReferenceName(getTypeReferenceName(node), meaning, /*ignoreErrors*/ true); - if (symbol === unknownSymbol) { - symbol = resolveTypeReferenceName(getTypeReferenceName(node), meaning | SymbolFlags.Value); - } - else { - resolveTypeReferenceName(getTypeReferenceName(node), meaning); // Resolve again to mark errors, if any - } - type = getTypeReferenceType(node, symbol); - } - } - if (!type) { - symbol = resolveTypeReferenceName(getTypeReferenceName(node), meaning); - type = getTypeReferenceType(node, symbol); - } - // Cache both the resolved symbol and the resolved type. The resolved symbol is needed when we check the - // type reference in checkTypeReferenceNode. - links.resolvedSymbol = symbol; - links.resolvedType = type; - } - return links.resolvedType; - } - - function typeArgumentsFromTypeReferenceNode(node: NodeWithTypeArguments): Type[] | undefined { - return map(node.typeArguments, getTypeFromTypeNode); - } - - function getTypeFromTypeQueryNode(node: TypeQueryNode): Type { - const links = getNodeLinks(node); - if (!links.resolvedType) { - // TypeScript 1.0 spec (April 2014): 3.6.3 - // The expression is processed as an identifier expression (section 4.3) - // or property access expression(section 4.10), - // the widened type(section 3.9) of which becomes the result. - links.resolvedType = getRegularTypeOfLiteralType(getWidenedType(checkExpression(node.exprName))); - } - return links.resolvedType; - } - - function getTypeOfGlobalSymbol(symbol: Symbol | undefined, arity: number): ObjectType { - - function getTypeDeclaration(symbol: Symbol): Declaration | undefined { - const declarations = symbol.declarations; - for (const declaration of declarations) { - switch (declaration.kind) { - case SyntaxKind.ClassDeclaration: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.EnumDeclaration: - return declaration; - } - } - } - - if (!symbol) { - return arity ? emptyGenericType : emptyObjectType; - } - const type = getDeclaredTypeOfSymbol(symbol); - if (!(type.flags & TypeFlags.Object)) { - error(getTypeDeclaration(symbol), Diagnostics.Global_type_0_must_be_a_class_or_interface_type, symbolName(symbol)); - return arity ? emptyGenericType : emptyObjectType; - } - if (length((type).typeParameters) !== arity) { - error(getTypeDeclaration(symbol), Diagnostics.Global_type_0_must_have_1_type_parameter_s, symbolName(symbol), arity); - return arity ? emptyGenericType : emptyObjectType; - } - return type; - } - - function getGlobalValueSymbol(name: __String, reportErrors: boolean): Symbol | undefined { - return getGlobalSymbol(name, SymbolFlags.Value, reportErrors ? Diagnostics.Cannot_find_global_value_0 : undefined); - } - - function getGlobalTypeSymbol(name: __String, reportErrors: boolean): Symbol | undefined { - return getGlobalSymbol(name, SymbolFlags.Type, reportErrors ? Diagnostics.Cannot_find_global_type_0 : undefined); - } - - function getGlobalSymbol(name: __String, meaning: SymbolFlags, diagnostic: DiagnosticMessage | undefined): Symbol | undefined { - // Don't track references for global symbols anyway, so value if `isReference` is arbitrary - return resolveName(undefined, name, meaning, diagnostic, name, /*isUse*/ false); - } - - function getGlobalType(name: __String, arity: 0, reportErrors: boolean): ObjectType; - function getGlobalType(name: __String, arity: number, reportErrors: boolean): GenericType; - function getGlobalType(name: __String, arity: number, reportErrors: boolean): ObjectType | undefined { - const symbol = getGlobalTypeSymbol(name, reportErrors); - return symbol || reportErrors ? getTypeOfGlobalSymbol(symbol, arity) : undefined; - } - - function getGlobalTypedPropertyDescriptorType() { - return deferredGlobalTypedPropertyDescriptorType || (deferredGlobalTypedPropertyDescriptorType = getGlobalType("TypedPropertyDescriptor" as __String, /*arity*/ 1, /*reportErrors*/ true)) || emptyGenericType; - } - - function getGlobalTemplateStringsArrayType() { - return deferredGlobalTemplateStringsArrayType || (deferredGlobalTemplateStringsArrayType = getGlobalType("TemplateStringsArray" as __String, /*arity*/ 0, /*reportErrors*/ true)) || emptyObjectType; - } - - function getGlobalImportMetaType() { - return deferredGlobalImportMetaType || (deferredGlobalImportMetaType = getGlobalType("ImportMeta" as __String, /*arity*/ 0, /*reportErrors*/ true)) || emptyObjectType; - } - - function getGlobalESSymbolConstructorSymbol(reportErrors: boolean) { - return deferredGlobalESSymbolConstructorSymbol || (deferredGlobalESSymbolConstructorSymbol = getGlobalValueSymbol("Symbol" as __String, reportErrors)); - } - - function getGlobalESSymbolType(reportErrors: boolean) { - return deferredGlobalESSymbolType || (deferredGlobalESSymbolType = getGlobalType("Symbol" as __String, /*arity*/ 0, reportErrors)) || emptyObjectType; - } - - function getGlobalPromiseType(reportErrors: boolean) { - return deferredGlobalPromiseType || (deferredGlobalPromiseType = getGlobalType("Promise" as __String, /*arity*/ 1, reportErrors)) || emptyGenericType; - } - - function getGlobalPromiseLikeType(reportErrors: boolean) { - return deferredGlobalPromiseLikeType || (deferredGlobalPromiseLikeType = getGlobalType("PromiseLike" as __String, /*arity*/ 1, reportErrors)) || emptyGenericType; - } - - function getGlobalPromiseConstructorSymbol(reportErrors: boolean): Symbol | undefined { - return deferredGlobalPromiseConstructorSymbol || (deferredGlobalPromiseConstructorSymbol = getGlobalValueSymbol("Promise" as __String, reportErrors)); - } - - function getGlobalPromiseConstructorLikeType(reportErrors: boolean) { - return deferredGlobalPromiseConstructorLikeType || (deferredGlobalPromiseConstructorLikeType = getGlobalType("PromiseConstructorLike" as __String, /*arity*/ 0, reportErrors)) || emptyObjectType; - } - - function getGlobalAsyncIterableType(reportErrors: boolean) { - return deferredGlobalAsyncIterableType || (deferredGlobalAsyncIterableType = getGlobalType("AsyncIterable" as __String, /*arity*/ 1, reportErrors)) || emptyGenericType; - } - - function getGlobalAsyncIteratorType(reportErrors: boolean) { - return deferredGlobalAsyncIteratorType || (deferredGlobalAsyncIteratorType = getGlobalType("AsyncIterator" as __String, /*arity*/ 3, reportErrors)) || emptyGenericType; - } - - function getGlobalAsyncIterableIteratorType(reportErrors: boolean) { - return deferredGlobalAsyncIterableIteratorType || (deferredGlobalAsyncIterableIteratorType = getGlobalType("AsyncIterableIterator" as __String, /*arity*/ 1, reportErrors)) || emptyGenericType; - } - - function getGlobalAsyncGeneratorType(reportErrors: boolean) { - return deferredGlobalAsyncGeneratorType || (deferredGlobalAsyncGeneratorType = getGlobalType("AsyncGenerator" as __String, /*arity*/ 3, reportErrors)) || emptyGenericType; - } - - function getGlobalIterableType(reportErrors: boolean) { - return deferredGlobalIterableType || (deferredGlobalIterableType = getGlobalType("Iterable" as __String, /*arity*/ 1, reportErrors)) || emptyGenericType; - } - - function getGlobalIteratorType(reportErrors: boolean) { - return deferredGlobalIteratorType || (deferredGlobalIteratorType = getGlobalType("Iterator" as __String, /*arity*/ 3, reportErrors)) || emptyGenericType; - } - - function getGlobalIterableIteratorType(reportErrors: boolean) { - return deferredGlobalIterableIteratorType || (deferredGlobalIterableIteratorType = getGlobalType("IterableIterator" as __String, /*arity*/ 1, reportErrors)) || emptyGenericType; - } - - function getGlobalGeneratorType(reportErrors: boolean) { - return deferredGlobalGeneratorType || (deferredGlobalGeneratorType = getGlobalType("Generator" as __String, /*arity*/ 3, reportErrors)) || emptyGenericType; - } - - function getGlobalIteratorYieldResultType(reportErrors: boolean) { - return deferredGlobalIteratorYieldResultType || (deferredGlobalIteratorYieldResultType = getGlobalType("IteratorYieldResult" as __String, /*arity*/ 1, reportErrors)) || emptyGenericType; - } - - function getGlobalIteratorReturnResultType(reportErrors: boolean) { - return deferredGlobalIteratorReturnResultType || (deferredGlobalIteratorReturnResultType = getGlobalType("IteratorReturnResult" as __String, /*arity*/ 1, reportErrors)) || emptyGenericType; - } - - function getGlobalTypeOrUndefined(name: __String, arity = 0): ObjectType | undefined { - const symbol = getGlobalSymbol(name, SymbolFlags.Type, /*diagnostic*/ undefined); - return symbol && getTypeOfGlobalSymbol(symbol, arity); - } - - function getGlobalExtractSymbol(): Symbol { - return deferredGlobalExtractSymbol || (deferredGlobalExtractSymbol = getGlobalSymbol("Extract" as __String, SymbolFlags.TypeAlias, Diagnostics.Cannot_find_global_type_0)!); // TODO: GH#18217 - } - - function getGlobalOmitSymbol(): Symbol { - return deferredGlobalOmitSymbol || (deferredGlobalOmitSymbol = getGlobalSymbol("Omit" as __String, SymbolFlags.TypeAlias, Diagnostics.Cannot_find_global_type_0)!); // TODO: GH#18217 - } - - function getGlobalBigIntType(reportErrors: boolean) { - return deferredGlobalBigIntType || (deferredGlobalBigIntType = getGlobalType("BigInt" as __String, /*arity*/ 0, reportErrors)) || emptyObjectType; - } - - /** - * Instantiates a global type that is generic with some element type, and returns that instantiation. - */ - function createTypeFromGenericGlobalType(genericGlobalType: GenericType, typeArguments: readonly Type[]): ObjectType { - return genericGlobalType !== emptyGenericType ? createTypeReference(genericGlobalType, typeArguments) : emptyObjectType; - } - - function createTypedPropertyDescriptorType(propertyType: Type): Type { - return createTypeFromGenericGlobalType(getGlobalTypedPropertyDescriptorType(), [propertyType]); - } - - function createIterableType(iteratedType: Type): Type { - return createTypeFromGenericGlobalType(getGlobalIterableType(/*reportErrors*/ true), [iteratedType]); - } - - function createArrayType(elementType: Type, readonly?: boolean): ObjectType { - return createTypeFromGenericGlobalType(readonly ? globalReadonlyArrayType : globalArrayType, [elementType]); - } - - function getArrayOrTupleTargetType(node: ArrayTypeNode | TupleTypeNode): GenericType { - const readonly = isReadonlyTypeOperator(node.parent); - if (node.kind === SyntaxKind.ArrayType || node.elementTypes.length === 1 && node.elementTypes[0].kind === SyntaxKind.RestType) { - return readonly ? globalReadonlyArrayType : globalArrayType; - } - const lastElement = lastOrUndefined(node.elementTypes); - const restElement = lastElement && lastElement.kind === SyntaxKind.RestType ? lastElement : undefined; - const minLength = findLastIndex(node.elementTypes, n => n.kind !== SyntaxKind.OptionalType && n !== restElement) + 1; - return getTupleTypeOfArity(node.elementTypes.length, minLength, !!restElement, readonly, /*associatedNames*/ undefined); - } - - // Return true if the given type reference node is directly aliased or if it needs to be deferred - // because it is possibly contained in a circular chain of eagerly resolved types. - function isDeferredTypeReferenceNode(node: TypeReferenceNode | ArrayTypeNode | TupleTypeNode, hasDefaultTypeArguments?: boolean) { - return !!getAliasSymbolForTypeNode(node) || isResolvedByTypeAlias(node) && ( - node.kind === SyntaxKind.ArrayType ? mayResolveTypeAlias(node.elementType) : - node.kind === SyntaxKind.TupleType ? some(node.elementTypes, mayResolveTypeAlias) : - hasDefaultTypeArguments || some(node.typeArguments, mayResolveTypeAlias)); - } - - // Return true when the given node is transitively contained in type constructs that eagerly - // resolve their constituent types. We include SyntaxKind.TypeReference because type arguments - // of type aliases are eagerly resolved. - function isResolvedByTypeAlias(node: Node): boolean { - const parent = node.parent; - switch (parent.kind) { - case SyntaxKind.ParenthesizedType: - case SyntaxKind.TypeReference: - case SyntaxKind.UnionType: - case SyntaxKind.IntersectionType: - case SyntaxKind.IndexedAccessType: - case SyntaxKind.ConditionalType: - case SyntaxKind.TypeOperator: - return isResolvedByTypeAlias(parent); - case SyntaxKind.TypeAliasDeclaration: - return true; - } - return false; - } - - // Return true if resolving the given node (i.e. getTypeFromTypeNode) possibly causes resolution - // of a type alias. - function mayResolveTypeAlias(node: Node): boolean { - switch (node.kind) { - case SyntaxKind.TypeReference: - return isJSDocTypeReference(node) || !!(resolveTypeReferenceName((node).typeName, SymbolFlags.Type).flags & SymbolFlags.TypeAlias); - case SyntaxKind.TypeQuery: - return true; - case SyntaxKind.TypeOperator: - return (node).operator !== SyntaxKind.UniqueKeyword && mayResolveTypeAlias((node).type); - case SyntaxKind.ParenthesizedType: - case SyntaxKind.OptionalType: - case SyntaxKind.JSDocOptionalType: - case SyntaxKind.JSDocNullableType: - case SyntaxKind.JSDocNonNullableType: - case SyntaxKind.JSDocTypeExpression: - return mayResolveTypeAlias((node).type); - case SyntaxKind.RestType: - return (node).type.kind !== SyntaxKind.ArrayType || mayResolveTypeAlias(((node).type).elementType); - case SyntaxKind.UnionType: - case SyntaxKind.IntersectionType: - return some((node).types, mayResolveTypeAlias); - case SyntaxKind.IndexedAccessType: - return mayResolveTypeAlias((node).objectType) || mayResolveTypeAlias((node).indexType); - case SyntaxKind.ConditionalType: - return mayResolveTypeAlias((node).checkType) || mayResolveTypeAlias((node).extendsType) || - mayResolveTypeAlias((node).trueType) || mayResolveTypeAlias((node).falseType); - } - return false; - } - - function getTypeFromArrayOrTupleTypeNode(node: ArrayTypeNode | TupleTypeNode): Type { - const links = getNodeLinks(node); - if (!links.resolvedType) { - const target = getArrayOrTupleTargetType(node); - if (target === emptyGenericType) { - links.resolvedType = emptyObjectType; - } - else if (isDeferredTypeReferenceNode(node)) { - links.resolvedType = node.kind === SyntaxKind.TupleType && node.elementTypes.length === 0 ? target : - createDeferredTypeReference(target, node, /*mapper*/ undefined); - } - else { - const elementTypes = node.kind === SyntaxKind.ArrayType ? [getTypeFromTypeNode(node.elementType)] : map(node.elementTypes, getTypeFromTypeNode); - links.resolvedType = createTypeReference(target, elementTypes); - } - } - return links.resolvedType; - } - - function isReadonlyTypeOperator(node: Node) { - return isTypeOperatorNode(node) && node.operator === SyntaxKind.ReadonlyKeyword; - } - - // We represent tuple types as type references to synthesized generic interface types created by - // this function. The types are of the form: - // - // interface Tuple extends Array { 0: T0, 1: T1, 2: T2, ... } - // - // Note that the generic type created by this function has no symbol associated with it. The same - // is true for each of the synthesized type parameters. - function createTupleTypeOfArity(arity: number, minLength: number, hasRestElement: boolean, readonly: boolean, associatedNames: __String[] | undefined): TupleType { - let typeParameters: TypeParameter[] | undefined; - const properties: Symbol[] = []; - const maxLength = hasRestElement ? arity - 1 : arity; - if (arity) { - typeParameters = new Array(arity); - for (let i = 0; i < arity; i++) { - const typeParameter = typeParameters[i] = createTypeParameter(); - if (i < maxLength) { - const property = createSymbol(SymbolFlags.Property | (i >= minLength ? SymbolFlags.Optional : 0), - "" + i as __String, readonly ? CheckFlags.Readonly : 0); - property.type = typeParameter; - properties.push(property); - } - } - } - const literalTypes = []; - for (let i = minLength; i <= maxLength; i++) literalTypes.push(getLiteralType(i)); - const lengthSymbol = createSymbol(SymbolFlags.Property, "length" as __String); - lengthSymbol.type = hasRestElement ? numberType : getUnionType(literalTypes); - properties.push(lengthSymbol); - const type = createObjectType(ObjectFlags.Tuple | ObjectFlags.Reference); - type.typeParameters = typeParameters; - type.outerTypeParameters = undefined; - type.localTypeParameters = typeParameters; - type.instantiations = createMap(); - type.instantiations.set(getTypeListId(type.typeParameters), type); - type.target = type; - type.resolvedTypeArguments = type.typeParameters; - type.thisType = createTypeParameter(); - type.thisType.isThisType = true; - type.thisType.constraint = type; - type.declaredProperties = properties; - type.declaredCallSignatures = emptyArray; - type.declaredConstructSignatures = emptyArray; - type.declaredStringIndexInfo = undefined; - type.declaredNumberIndexInfo = undefined; - type.minLength = minLength; - type.hasRestElement = hasRestElement; - type.readonly = readonly; - type.associatedNames = associatedNames; - return type; - } - - function getTupleTypeOfArity(arity: number, minLength: number, hasRestElement: boolean, readonly: boolean, associatedNames?: __String[]): GenericType { - const key = arity + (hasRestElement ? "+" : ",") + minLength + (readonly ? "R" : "") + (associatedNames && associatedNames.length ? "," + associatedNames.join(",") : ""); - let type = tupleTypes.get(key); - if (!type) { - tupleTypes.set(key, type = createTupleTypeOfArity(arity, minLength, hasRestElement, readonly, associatedNames)); - } - return type; - } - - function createTupleType(elementTypes: readonly Type[], minLength = elementTypes.length, hasRestElement = false, readonly = false, associatedNames?: __String[]) { - const arity = elementTypes.length; - if (arity === 1 && hasRestElement) { - return createArrayType(elementTypes[0], readonly); - } - const tupleType = getTupleTypeOfArity(arity, minLength, arity > 0 && hasRestElement, readonly, associatedNames); - return elementTypes.length ? createTypeReference(tupleType, elementTypes) : tupleType; - } - - function sliceTupleType(type: TupleTypeReference, index: number) { - const tuple = type.target; - if (tuple.hasRestElement) { - // don't slice off rest element - index = Math.min(index, getTypeReferenceArity(type) - 1); - } - return createTupleType( - getTypeArguments(type).slice(index), - Math.max(0, tuple.minLength - index), - tuple.hasRestElement, - tuple.readonly, - tuple.associatedNames && tuple.associatedNames.slice(index), - ); - } - - function getTypeFromOptionalTypeNode(node: OptionalTypeNode): Type { - const type = getTypeFromTypeNode(node.type); - return strictNullChecks ? getOptionalType(type) : type; - } - - function getTypeId(type: Type) { - return type.id; - } - - function containsType(types: readonly Type[], type: Type): boolean { - return binarySearch(types, type, getTypeId, compareValues) >= 0; - } - - function insertType(types: Type[], type: Type): boolean { - const index = binarySearch(types, type, getTypeId, compareValues); - if (index < 0) { - types.splice(~index, 0, type); - return true; - } - return false; - } - - function addTypeToUnion(typeSet: Type[], includes: TypeFlags, type: Type) { - const flags = type.flags; - if (flags & TypeFlags.Union) { - return addTypesToUnion(typeSet, includes, (type).types); - } - // We ignore 'never' types in unions - if (!(flags & TypeFlags.Never)) { - includes |= flags & TypeFlags.IncludesMask; - if (flags & TypeFlags.StructuredOrInstantiable) includes |= TypeFlags.IncludesStructuredOrInstantiable; - if (type === wildcardType) includes |= TypeFlags.IncludesWildcard; - if (!strictNullChecks && flags & TypeFlags.Nullable) { - if (!(getObjectFlags(type) & ObjectFlags.ContainsWideningType)) includes |= TypeFlags.IncludesNonWideningType; - } - else { - const len = typeSet.length; - const index = len && type.id > typeSet[len - 1].id ? ~len : binarySearch(typeSet, type, getTypeId, compareValues); - if (index < 0) { - typeSet.splice(~index, 0, type); - } - } - } - return includes; - } - - // Add the given types to the given type set. Order is preserved, duplicates are removed, - // and nested types of the given kind are flattened into the set. - function addTypesToUnion(typeSet: Type[], includes: TypeFlags, types: readonly Type[]): TypeFlags { - for (const type of types) { - includes = addTypeToUnion(typeSet, includes, type); - } - return includes; - } - - function isSetOfLiteralsFromSameEnum(types: readonly Type[]): boolean { - const first = types[0]; - if (first.flags & TypeFlags.EnumLiteral) { - const firstEnum = getParentOfSymbol(first.symbol); - for (let i = 1; i < types.length; i++) { - const other = types[i]; - if (!(other.flags & TypeFlags.EnumLiteral) || (firstEnum !== getParentOfSymbol(other.symbol))) { - return false; - } - } - return true; - } - - return false; - } - - function removeSubtypes(types: Type[], primitivesOnly: boolean): boolean { - const len = types.length; - if (len === 0 || isSetOfLiteralsFromSameEnum(types)) { - return true; - } - let i = len; - let count = 0; - while (i > 0) { - i--; - const source = types[i]; - for (const target of types) { - if (source !== target) { - if (count === 100000) { - // After 100000 subtype checks we estimate the remaining amount of work by assuming the - // same ratio of checks per element. If the estimated number of remaining type checks is - // greater than an upper limit we deem the union type too complex to represent. The - // upper limit is 25M for unions of primitives only, and 1M otherwise. This for example - // caps union types at 5000 unique literal types and 1000 unique object types. - const estimatedCount = (count / (len - i)) * len; - if (estimatedCount > (primitivesOnly ? 25000000 : 1000000)) { - error(currentNode, Diagnostics.Expression_produces_a_union_type_that_is_too_complex_to_represent); - return false; - } - } - count++; - if (isTypeRelatedTo(source, target, strictSubtypeRelation) && ( - !(getObjectFlags(getTargetType(source)) & ObjectFlags.Class) || - !(getObjectFlags(getTargetType(target)) & ObjectFlags.Class) || - isTypeDerivedFrom(source, target))) { - orderedRemoveItemAt(types, i); - break; - } - } - } - } - return true; - } - - function removeRedundantLiteralTypes(types: Type[], includes: TypeFlags) { - let i = types.length; - while (i > 0) { - i--; - const t = types[i]; - const remove = - t.flags & TypeFlags.StringLiteral && includes & TypeFlags.String || - t.flags & TypeFlags.NumberLiteral && includes & TypeFlags.Number || - t.flags & TypeFlags.BigIntLiteral && includes & TypeFlags.BigInt || - t.flags & TypeFlags.UniqueESSymbol && includes & TypeFlags.ESSymbol || - isFreshLiteralType(t) && containsType(types, (t).regularType); - if (remove) { - orderedRemoveItemAt(types, i); - } - } - } - - // We sort and deduplicate the constituent types based on object identity. If the subtypeReduction - // flag is specified we also reduce the constituent type set to only include types that aren't subtypes - // of other types. Subtype reduction is expensive for large union types and is possible only when union - // types are known not to circularly reference themselves (as is the case with union types created by - // expression constructs such as array literals and the || and ?: operators). Named types can - // circularly reference themselves and therefore cannot be subtype reduced during their declaration. - // For example, "type Item = string | (() => Item" is a named type that circularly references itself. - function getUnionType(types: readonly Type[], unionReduction: UnionReduction = UnionReduction.Literal, aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]): Type { - if (types.length === 0) { - return neverType; - } - if (types.length === 1) { - return types[0]; - } - const typeSet: Type[] = []; - const includes = addTypesToUnion(typeSet, 0, types); - if (unionReduction !== UnionReduction.None) { - if (includes & TypeFlags.AnyOrUnknown) { - return includes & TypeFlags.Any ? includes & TypeFlags.IncludesWildcard ? wildcardType : anyType : unknownType; - } - switch (unionReduction) { - case UnionReduction.Literal: - if (includes & (TypeFlags.Literal | TypeFlags.UniqueESSymbol)) { - removeRedundantLiteralTypes(typeSet, includes); - } - break; - case UnionReduction.Subtype: - if (!removeSubtypes(typeSet, !(includes & TypeFlags.IncludesStructuredOrInstantiable))) { - return errorType; - } - break; - } - if (typeSet.length === 0) { - return includes & TypeFlags.Null ? includes & TypeFlags.IncludesNonWideningType ? nullType : nullWideningType : - includes & TypeFlags.Undefined ? includes & TypeFlags.IncludesNonWideningType ? undefinedType : undefinedWideningType : - neverType; - } - } - return getUnionTypeFromSortedList(typeSet, includes & TypeFlags.NotPrimitiveUnion ? 0 : ObjectFlags.PrimitiveUnion, aliasSymbol, aliasTypeArguments); - } - - function getUnionTypePredicate(signatures: readonly Signature[]): TypePredicate | undefined { - let first: TypePredicate | undefined; - const types: Type[] = []; - for (const sig of signatures) { - const pred = getTypePredicateOfSignature(sig); - if (!pred || pred.kind === TypePredicateKind.AssertsThis || pred.kind === TypePredicateKind.AssertsIdentifier) { - continue; - } - - if (first) { - if (!typePredicateKindsMatch(first, pred)) { - // No common type predicate. - return undefined; - } - } - else { - first = pred; - } - types.push(pred.type); - } - if (!first) { - // No union signatures had a type predicate. - return undefined; - } - const unionType = getUnionType(types); - return createTypePredicate(first.kind, first.parameterName, first.parameterIndex, unionType); - } - - function typePredicateKindsMatch(a: TypePredicate, b: TypePredicate): boolean { - return a.kind === b.kind && a.parameterIndex === b.parameterIndex; - } - - // This function assumes the constituent type list is sorted and deduplicated. - function getUnionTypeFromSortedList(types: Type[], objectFlags: ObjectFlags, aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]): Type { - if (types.length === 0) { - return neverType; - } - if (types.length === 1) { - return types[0]; - } - const id = getTypeListId(types); - let type = unionTypes.get(id); - if (!type) { - type = createType(TypeFlags.Union); - unionTypes.set(id, type); - type.objectFlags = objectFlags | getPropagatingFlagsOfTypes(types, /*excludeKinds*/ TypeFlags.Nullable); - type.types = types; - /* - Note: This is the alias symbol (or lack thereof) that we see when we first encounter this union type. - For aliases of identical unions, eg `type T = A | B; type U = A | B`, the symbol of the first alias encountered is the aliasSymbol. - (In the language service, the order may depend on the order in which a user takes actions, such as hovering over symbols.) - It's important that we create equivalent union types only once, so that's an unfortunate side effect. - */ - type.aliasSymbol = aliasSymbol; - type.aliasTypeArguments = aliasTypeArguments; - } - return type; - } - - function getTypeFromUnionTypeNode(node: UnionTypeNode): Type { - const links = getNodeLinks(node); - if (!links.resolvedType) { - const aliasSymbol = getAliasSymbolForTypeNode(node); - links.resolvedType = getUnionType(map(node.types, getTypeFromTypeNode), UnionReduction.Literal, - aliasSymbol, getTypeArgumentsForAliasSymbol(aliasSymbol)); - } - return links.resolvedType; - } - - function addTypeToIntersection(typeSet: Map, includes: TypeFlags, type: Type) { - const flags = type.flags; - if (flags & TypeFlags.Intersection) { - return addTypesToIntersection(typeSet, includes, (type).types); - } - if (isEmptyAnonymousObjectType(type)) { - if (!(includes & TypeFlags.IncludesEmptyObject)) { - includes |= TypeFlags.IncludesEmptyObject; - typeSet.set(type.id.toString(), type); - } - } - else { - if (flags & TypeFlags.AnyOrUnknown) { - if (type === wildcardType) includes |= TypeFlags.IncludesWildcard; - } - else if ((strictNullChecks || !(flags & TypeFlags.Nullable)) && !typeSet.has(type.id.toString())) { - if (type.flags & TypeFlags.Unit && includes & TypeFlags.Unit) { - // We have seen two distinct unit types which means we should reduce to an - // empty intersection. Adding TypeFlags.NonPrimitive causes that to happen. - includes |= TypeFlags.NonPrimitive; - } - typeSet.set(type.id.toString(), type); - } - includes |= flags & TypeFlags.IncludesMask; - } - return includes; - } - - // Add the given types to the given type set. Order is preserved, freshness is removed from literal - // types, duplicates are removed, and nested types of the given kind are flattened into the set. - function addTypesToIntersection(typeSet: Map, includes: TypeFlags, types: readonly Type[]) { - for (const type of types) { - includes = addTypeToIntersection(typeSet, includes, getRegularTypeOfLiteralType(type)); - } - return includes; - } - - function removeRedundantPrimitiveTypes(types: Type[], includes: TypeFlags) { - let i = types.length; - while (i > 0) { - i--; - const t = types[i]; - const remove = - t.flags & TypeFlags.String && includes & TypeFlags.StringLiteral || - t.flags & TypeFlags.Number && includes & TypeFlags.NumberLiteral || - t.flags & TypeFlags.BigInt && includes & TypeFlags.BigIntLiteral || - t.flags & TypeFlags.ESSymbol && includes & TypeFlags.UniqueESSymbol; - if (remove) { - orderedRemoveItemAt(types, i); - } - } - } - - // Check that the given type has a match in every union. A given type is matched by - // an identical type, and a literal type is additionally matched by its corresponding - // primitive type. - function eachUnionContains(unionTypes: UnionType[], type: Type) { - for (const u of unionTypes) { - if (!containsType(u.types, type)) { - const primitive = type.flags & TypeFlags.StringLiteral ? stringType : - type.flags & TypeFlags.NumberLiteral ? numberType : - type.flags & TypeFlags.BigIntLiteral ? bigintType : - type.flags & TypeFlags.UniqueESSymbol ? esSymbolType : - undefined; - if (!primitive || !containsType(u.types, primitive)) { - return false; - } - } - } - return true; - } - - function extractIrreducible(types: Type[], flag: TypeFlags) { - if (every(types, t => !!(t.flags & TypeFlags.Union) && some((t as UnionType).types, tt => !!(tt.flags & flag)))) { - for (let i = 0; i < types.length; i++) { - types[i] = filterType(types[i], t => !(t.flags & flag)); - } - return true; - } - return false; - } - - // If the given list of types contains more than one union of primitive types, replace the - // first with a union containing an intersection of those primitive types, then remove the - // other unions and return true. Otherwise, do nothing and return false. - function intersectUnionsOfPrimitiveTypes(types: Type[]) { - let unionTypes: UnionType[] | undefined; - const index = findIndex(types, t => !!(getObjectFlags(t) & ObjectFlags.PrimitiveUnion)); - if (index < 0) { - return false; - } - let i = index + 1; - // Remove all but the first union of primitive types and collect them in - // the unionTypes array. - while (i < types.length) { - const t = types[i]; - if (getObjectFlags(t) & ObjectFlags.PrimitiveUnion) { - (unionTypes || (unionTypes = [types[index]])).push(t); - orderedRemoveItemAt(types, i); - } - else { - i++; - } - } - // Return false if there was only one union of primitive types - if (!unionTypes) { - return false; - } - // We have more than one union of primitive types, now intersect them. For each - // type in each union we check if the type is matched in every union and if so - // we include it in the result. - const checked: Type[] = []; - const result: Type[] = []; - for (const u of unionTypes) { - for (const t of u.types) { - if (insertType(checked, t)) { - if (eachUnionContains(unionTypes, t)) { - insertType(result, t); - } - } - } - } - // Finally replace the first union with the result - types[index] = getUnionTypeFromSortedList(result, ObjectFlags.PrimitiveUnion); - return true; - } - - function createIntersectionType(types: Type[], aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]) { - const result = createType(TypeFlags.Intersection); - result.objectFlags = getPropagatingFlagsOfTypes(types, /*excludeKinds*/ TypeFlags.Nullable); - result.types = types; - result.aliasSymbol = aliasSymbol; // See comment in `getUnionTypeFromSortedList`. - result.aliasTypeArguments = aliasTypeArguments; - return result; - } - - // We normalize combinations of intersection and union types based on the distributive property of the '&' - // operator. Specifically, because X & (A | B) is equivalent to X & A | X & B, we can transform intersection - // types with union type constituents into equivalent union types with intersection type constituents and - // effectively ensure that union types are always at the top level in type representations. - // - // We do not perform structural deduplication on intersection types. Intersection types are created only by the & - // type operator and we can't reduce those because we want to support recursive intersection types. For example, - // a type alias of the form "type List = T & { next: List }" cannot be reduced during its declaration. - // Also, unlike union types, the order of the constituent types is preserved in order that overload resolution - // for intersections of types with signatures can be deterministic. - function getIntersectionType(types: readonly Type[], aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]): Type { - const typeMembershipMap: Map = createMap(); - const includes = addTypesToIntersection(typeMembershipMap, 0, types); - const typeSet: Type[] = arrayFrom(typeMembershipMap.values()); - // An intersection type is considered empty if it contains - // the type never, or - // more than one unit type or, - // an object type and a nullable type (null or undefined), or - // a string-like type and a type known to be non-string-like, or - // a number-like type and a type known to be non-number-like, or - // a symbol-like type and a type known to be non-symbol-like, or - // a void-like type and a type known to be non-void-like, or - // a non-primitive type and a type known to be primitive. - if (includes & TypeFlags.Never || - strictNullChecks && includes & TypeFlags.Nullable && includes & (TypeFlags.Object | TypeFlags.NonPrimitive | TypeFlags.IncludesEmptyObject) || - includes & TypeFlags.NonPrimitive && includes & (TypeFlags.DisjointDomains & ~TypeFlags.NonPrimitive) || - includes & TypeFlags.StringLike && includes & (TypeFlags.DisjointDomains & ~TypeFlags.StringLike) || - includes & TypeFlags.NumberLike && includes & (TypeFlags.DisjointDomains & ~TypeFlags.NumberLike) || - includes & TypeFlags.BigIntLike && includes & (TypeFlags.DisjointDomains & ~TypeFlags.BigIntLike) || - includes & TypeFlags.ESSymbolLike && includes & (TypeFlags.DisjointDomains & ~TypeFlags.ESSymbolLike) || - includes & TypeFlags.VoidLike && includes & (TypeFlags.DisjointDomains & ~TypeFlags.VoidLike)) { - return neverType; - } - if (includes & TypeFlags.Any) { - return includes & TypeFlags.IncludesWildcard ? wildcardType : anyType; - } - if (!strictNullChecks && includes & TypeFlags.Nullable) { - return includes & TypeFlags.Undefined ? undefinedType : nullType; - } - if (includes & TypeFlags.String && includes & TypeFlags.StringLiteral || - includes & TypeFlags.Number && includes & TypeFlags.NumberLiteral || - includes & TypeFlags.BigInt && includes & TypeFlags.BigIntLiteral || - includes & TypeFlags.ESSymbol && includes & TypeFlags.UniqueESSymbol) { - removeRedundantPrimitiveTypes(typeSet, includes); - } - if (includes & TypeFlags.IncludesEmptyObject && includes & TypeFlags.Object) { - orderedRemoveItemAt(typeSet, findIndex(typeSet, isEmptyAnonymousObjectType)); - } - if (typeSet.length === 0) { - return unknownType; - } - if (typeSet.length === 1) { - return typeSet[0]; - } - const id = getTypeListId(typeSet); - let result = intersectionTypes.get(id); - if (!result) { - if (includes & TypeFlags.Union) { - if (intersectUnionsOfPrimitiveTypes(typeSet)) { - // When the intersection creates a reduced set (which might mean that *all* union types have - // disappeared), we restart the operation to get a new set of combined flags. Once we have - // reduced we'll never reduce again, so this occurs at most once. - result = getIntersectionType(typeSet, aliasSymbol, aliasTypeArguments); - } - else if (extractIrreducible(typeSet, TypeFlags.Undefined)) { - result = getUnionType([getIntersectionType(typeSet), undefinedType], UnionReduction.Literal, aliasSymbol, aliasTypeArguments); - } - else if (extractIrreducible(typeSet, TypeFlags.Null)) { - result = getUnionType([getIntersectionType(typeSet), nullType], UnionReduction.Literal, aliasSymbol, aliasTypeArguments); - } - else { - // We are attempting to construct a type of the form X & (A | B) & Y. Transform this into a type of - // the form X & A & Y | X & B & Y and recursively reduce until no union type constituents remain. - // If the estimated size of the resulting union type exceeds 100000 constituents, report an error. - const size = reduceLeft(typeSet, (n, t) => n * (t.flags & TypeFlags.Union ? (t).types.length : 1), 1); - if (size >= 100000) { - error(currentNode, Diagnostics.Expression_produces_a_union_type_that_is_too_complex_to_represent); - return errorType; - } - const unionIndex = findIndex(typeSet, t => (t.flags & TypeFlags.Union) !== 0); - const unionType = typeSet[unionIndex]; - result = getUnionType(map(unionType.types, t => getIntersectionType(replaceElement(typeSet, unionIndex, t))), - UnionReduction.Literal, aliasSymbol, aliasTypeArguments); - } - } - else { - result = createIntersectionType(typeSet, aliasSymbol, aliasTypeArguments); - } - intersectionTypes.set(id, result); - } - return result; - } - - function getTypeFromIntersectionTypeNode(node: IntersectionTypeNode): Type { - const links = getNodeLinks(node); - if (!links.resolvedType) { - const aliasSymbol = getAliasSymbolForTypeNode(node); - links.resolvedType = getIntersectionType(map(node.types, getTypeFromTypeNode), - aliasSymbol, getTypeArgumentsForAliasSymbol(aliasSymbol)); - } - return links.resolvedType; - } - - function createIndexType(type: InstantiableType | UnionOrIntersectionType, stringsOnly: boolean) { - const result = createType(TypeFlags.Index); - result.type = type; - result.stringsOnly = stringsOnly; - return result; - } - - function getIndexTypeForGenericType(type: InstantiableType | UnionOrIntersectionType, stringsOnly: boolean) { - return stringsOnly ? - type.resolvedStringIndexType || (type.resolvedStringIndexType = createIndexType(type, /*stringsOnly*/ true)) : - type.resolvedIndexType || (type.resolvedIndexType = createIndexType(type, /*stringsOnly*/ false)); - } - - function getLiteralTypeFromPropertyName(name: PropertyName) { - if (isPrivateIdentifier(name)) { - return neverType; - } - return isIdentifier(name) ? getLiteralType(unescapeLeadingUnderscores(name.escapedText)) : - getRegularTypeOfLiteralType(isComputedPropertyName(name) ? checkComputedPropertyName(name) : checkExpression(name)); - } - - function getBigIntLiteralType(node: BigIntLiteral): LiteralType { - return getLiteralType({ - negative: false, - base10Value: parsePseudoBigInt(node.text) - }); - } - - function getLiteralTypeFromProperty(prop: Symbol, include: TypeFlags) { - if (!(getDeclarationModifierFlagsFromSymbol(prop) & ModifierFlags.NonPublicAccessibilityModifier)) { - let type = getSymbolLinks(getLateBoundSymbol(prop)).nameType; - if (!type && !isKnownSymbol(prop)) { - if (prop.escapedName === InternalSymbolName.Default) { - type = getLiteralType("default"); - } - else { - const name = prop.valueDeclaration && getNameOfDeclaration(prop.valueDeclaration) as PropertyName; - type = name && getLiteralTypeFromPropertyName(name) || getLiteralType(symbolName(prop)); - } - } - if (type && type.flags & include) { - return type; - } - } - return neverType; - } - - function getLiteralTypeFromProperties(type: Type, include: TypeFlags) { - return getUnionType(map(getPropertiesOfType(type), p => getLiteralTypeFromProperty(p, include))); - } - - function getNonEnumNumberIndexInfo(type: Type) { - const numberIndexInfo = getIndexInfoOfType(type, IndexKind.Number); - return numberIndexInfo !== enumNumberIndexInfo ? numberIndexInfo : undefined; - } - - function getIndexType(type: Type, stringsOnly = keyofStringsOnly, noIndexSignatures?: boolean): Type { - return type.flags & TypeFlags.Union ? getIntersectionType(map((type).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) : - type.flags & TypeFlags.Intersection ? getUnionType(map((type).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) : - maybeTypeOfKind(type, TypeFlags.InstantiableNonPrimitive) ? getIndexTypeForGenericType(type, stringsOnly) : - getObjectFlags(type) & ObjectFlags.Mapped ? filterType(getConstraintTypeFromMappedType(type), t => !(noIndexSignatures && t.flags & (TypeFlags.Any | TypeFlags.String))) : - type === wildcardType ? wildcardType : - type.flags & TypeFlags.Unknown ? neverType : - type.flags & (TypeFlags.Any | TypeFlags.Never) ? keyofConstraintType : - stringsOnly ? !noIndexSignatures && getIndexInfoOfType(type, IndexKind.String) ? stringType : getLiteralTypeFromProperties(type, TypeFlags.StringLiteral) : - !noIndexSignatures && getIndexInfoOfType(type, IndexKind.String) ? getUnionType([stringType, numberType, getLiteralTypeFromProperties(type, TypeFlags.UniqueESSymbol)]) : - getNonEnumNumberIndexInfo(type) ? getUnionType([numberType, getLiteralTypeFromProperties(type, TypeFlags.StringLiteral | TypeFlags.UniqueESSymbol)]) : - getLiteralTypeFromProperties(type, TypeFlags.StringOrNumberLiteralOrUnique); - } - - function getExtractStringType(type: Type) { - if (keyofStringsOnly) { - return type; - } - const extractTypeAlias = getGlobalExtractSymbol(); - return extractTypeAlias ? getTypeAliasInstantiation(extractTypeAlias, [type, stringType]) : stringType; - } - - function getIndexTypeOrString(type: Type): Type { - const indexType = getExtractStringType(getIndexType(type)); - return indexType.flags & TypeFlags.Never ? stringType : indexType; - } - - function getTypeFromTypeOperatorNode(node: TypeOperatorNode): Type { - const links = getNodeLinks(node); - if (!links.resolvedType) { - switch (node.operator) { - case SyntaxKind.KeyOfKeyword: - links.resolvedType = getIndexType(getTypeFromTypeNode(node.type)); - break; - case SyntaxKind.UniqueKeyword: - links.resolvedType = node.type.kind === SyntaxKind.SymbolKeyword - ? getESSymbolLikeTypeForNode(walkUpParenthesizedTypes(node.parent)) - : errorType; - break; - case SyntaxKind.ReadonlyKeyword: - links.resolvedType = getTypeFromTypeNode(node.type); - break; - default: - throw Debug.assertNever(node.operator); - } - } - return links.resolvedType; - } - - function createIndexedAccessType(objectType: Type, indexType: Type) { - const type = createType(TypeFlags.IndexedAccess); - type.objectType = objectType; - type.indexType = indexType; - return type; - } - - /** - * Returns if a type is or consists of a JSLiteral object type - * In addition to objects which are directly literals, - * * unions where every element is a jsliteral - * * intersections where at least one element is a jsliteral - * * and instantiable types constrained to a jsliteral - * Should all count as literals and not print errors on access or assignment of possibly existing properties. - * This mirrors the behavior of the index signature propagation, to which this behaves similarly (but doesn't affect assignability or inference). - */ - function isJSLiteralType(type: Type): boolean { - if (noImplicitAny) { - return false; // Flag is meaningless under `noImplicitAny` mode - } - if (getObjectFlags(type) & ObjectFlags.JSLiteral) { - return true; - } - if (type.flags & TypeFlags.Union) { - return every((type as UnionType).types, isJSLiteralType); - } - if (type.flags & TypeFlags.Intersection) { - return some((type as IntersectionType).types, isJSLiteralType); - } - if (type.flags & TypeFlags.Instantiable) { - return isJSLiteralType(getResolvedBaseConstraint(type)); - } - return false; - } - - function getPropertyNameFromIndex(indexType: Type, accessNode: StringLiteral | Identifier | PrivateIdentifier | ObjectBindingPattern | ArrayBindingPattern | ComputedPropertyName | NumericLiteral | IndexedAccessTypeNode | ElementAccessExpression | SyntheticExpression | undefined) { - const accessExpression = accessNode && accessNode.kind === SyntaxKind.ElementAccessExpression ? accessNode : undefined; - return isTypeUsableAsPropertyName(indexType) ? - getPropertyNameFromType(indexType) : - accessExpression && checkThatExpressionIsProperSymbolReference(accessExpression.argumentExpression, indexType, /*reportError*/ false) ? - getPropertyNameForKnownSymbolName(idText((accessExpression.argumentExpression).name)) : - accessNode && isPropertyName(accessNode) ? - // late bound names are handled in the first branch, so here we only need to handle normal names - getPropertyNameForPropertyNameNode(accessNode) : - undefined; - } - - function getPropertyTypeForIndexType(originalObjectType: Type, objectType: Type, indexType: Type, fullIndexType: Type, suppressNoImplicitAnyError: boolean, accessNode: ElementAccessExpression | IndexedAccessTypeNode | PropertyName | BindingName | SyntheticExpression | undefined, accessFlags: AccessFlags) { - const accessExpression = accessNode && accessNode.kind === SyntaxKind.ElementAccessExpression ? accessNode : undefined; - const propName = accessNode && isPrivateIdentifier(accessNode) ? undefined : getPropertyNameFromIndex(indexType, accessNode); - if (propName !== undefined) { - const prop = getPropertyOfType(objectType, propName); - if (prop) { - if (accessExpression) { - markPropertyAsReferenced(prop, accessExpression, /*isThisAccess*/ accessExpression.expression.kind === SyntaxKind.ThisKeyword); - if (isAssignmentToReadonlyEntity(accessExpression, prop, getAssignmentTargetKind(accessExpression))) { - error(accessExpression.argumentExpression, Diagnostics.Cannot_assign_to_0_because_it_is_a_read_only_property, symbolToString(prop)); - return undefined; - } - if (accessFlags & AccessFlags.CacheSymbol) { - getNodeLinks(accessNode!).resolvedSymbol = prop; - } - } - const propType = getTypeOfSymbol(prop); - return accessExpression && getAssignmentTargetKind(accessExpression) !== AssignmentKind.Definite ? - getFlowTypeOfReference(accessExpression, propType) : - propType; - } - if (everyType(objectType, isTupleType) && isNumericLiteralName(propName) && +propName >= 0) { - if (accessNode && everyType(objectType, t => !(t).target.hasRestElement) && !(accessFlags & AccessFlags.NoTupleBoundsCheck)) { - const indexNode = getIndexNodeForAccessExpression(accessNode); - if (isTupleType(objectType)) { - error(indexNode, Diagnostics.Tuple_type_0_of_length_1_has_no_element_at_index_2, - typeToString(objectType), getTypeReferenceArity(objectType), unescapeLeadingUnderscores(propName)); - } - else { - error(indexNode, Diagnostics.Property_0_does_not_exist_on_type_1, unescapeLeadingUnderscores(propName), typeToString(objectType)); - } - } - errorIfWritingToReadonlyIndex(getIndexInfoOfType(objectType, IndexKind.Number)); - return mapType(objectType, t => getRestTypeOfTupleType(t) || undefinedType); - } - } - if (!(indexType.flags & TypeFlags.Nullable) && isTypeAssignableToKind(indexType, TypeFlags.StringLike | TypeFlags.NumberLike | TypeFlags.ESSymbolLike)) { - if (objectType.flags & (TypeFlags.Any | TypeFlags.Never)) { - return objectType; - } - const stringIndexInfo = getIndexInfoOfType(objectType, IndexKind.String); - const indexInfo = isTypeAssignableToKind(indexType, TypeFlags.NumberLike) && getIndexInfoOfType(objectType, IndexKind.Number) || stringIndexInfo; - if (indexInfo) { - if (accessFlags & AccessFlags.NoIndexSignatures && indexInfo === stringIndexInfo) { - if (accessExpression) { - error(accessExpression, Diagnostics.Type_0_cannot_be_used_to_index_type_1, typeToString(indexType), typeToString(originalObjectType)); - } - return undefined; - } - if (accessNode && !isTypeAssignableToKind(indexType, TypeFlags.String | TypeFlags.Number)) { - const indexNode = getIndexNodeForAccessExpression(accessNode); - error(indexNode, Diagnostics.Type_0_cannot_be_used_as_an_index_type, typeToString(indexType)); - return indexInfo.type; - } - errorIfWritingToReadonlyIndex(indexInfo); - return indexInfo.type; - } - if (indexType.flags & TypeFlags.Never) { - return neverType; - } - if (isJSLiteralType(objectType)) { - return anyType; - } - if (accessExpression && !isConstEnumObjectType(objectType)) { - if (objectType.symbol === globalThisSymbol && propName !== undefined && globalThisSymbol.exports!.has(propName) && (globalThisSymbol.exports!.get(propName)!.flags & SymbolFlags.BlockScoped)) { - error(accessExpression, Diagnostics.Property_0_does_not_exist_on_type_1, unescapeLeadingUnderscores(propName), typeToString(objectType)); - } - else if (noImplicitAny && !compilerOptions.suppressImplicitAnyIndexErrors && !suppressNoImplicitAnyError) { - if (propName !== undefined && typeHasStaticProperty(propName, objectType)) { - error(accessExpression, Diagnostics.Property_0_is_a_static_member_of_type_1, propName as string, typeToString(objectType)); - } - else if (getIndexTypeOfType(objectType, IndexKind.Number)) { - error(accessExpression.argumentExpression, Diagnostics.Element_implicitly_has_an_any_type_because_index_expression_is_not_of_type_number); - } - else { - let suggestion: string | undefined; - if (propName !== undefined && (suggestion = getSuggestionForNonexistentProperty(propName as string, objectType))) { - if (suggestion !== undefined) { - error(accessExpression.argumentExpression, Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_2, propName as string, typeToString(objectType), suggestion); - } - } - else { - const suggestion = getSuggestionForNonexistentIndexSignature(objectType, accessExpression, indexType); - if (suggestion !== undefined) { - error(accessExpression, Diagnostics.Element_implicitly_has_an_any_type_because_type_0_has_no_index_signature_Did_you_mean_to_call_1, typeToString(objectType), suggestion); - } - else { - let errorInfo: DiagnosticMessageChain | undefined; - if (indexType.flags & TypeFlags.EnumLiteral) { - errorInfo = chainDiagnosticMessages(/* details */ undefined, Diagnostics.Property_0_does_not_exist_on_type_1, "[" + typeToString(indexType) + "]", typeToString(objectType)); - } - else if (indexType.flags & TypeFlags.UniqueESSymbol) { - const symbolName = getFullyQualifiedName((indexType as UniqueESSymbolType).symbol, accessExpression); - errorInfo = chainDiagnosticMessages(/* details */ undefined, Diagnostics.Property_0_does_not_exist_on_type_1, "[" + symbolName + "]", typeToString(objectType)); - } - else if (indexType.flags & TypeFlags.StringLiteral) { - errorInfo = chainDiagnosticMessages(/* details */ undefined, Diagnostics.Property_0_does_not_exist_on_type_1, (indexType as StringLiteralType).value, typeToString(objectType)); - } - else if (indexType.flags & TypeFlags.NumberLiteral) { - errorInfo = chainDiagnosticMessages(/* details */ undefined, Diagnostics.Property_0_does_not_exist_on_type_1, (indexType as NumberLiteralType).value, typeToString(objectType)); - } - else if (indexType.flags & (TypeFlags.Number | TypeFlags.String)) { - errorInfo = chainDiagnosticMessages(/* details */ undefined, Diagnostics.No_index_signature_with_a_parameter_of_type_0_was_found_on_type_1, typeToString(indexType), typeToString(objectType)); - } - - errorInfo = chainDiagnosticMessages( - errorInfo, - Diagnostics.Element_implicitly_has_an_any_type_because_expression_of_type_0_can_t_be_used_to_index_type_1, typeToString(fullIndexType), typeToString(objectType) - ); - diagnostics.add(createDiagnosticForNodeFromMessageChain(accessExpression, errorInfo)); - } - } - } - } - return undefined; - } - } - if (isJSLiteralType(objectType)) { - return anyType; - } - if (accessNode) { - const indexNode = getIndexNodeForAccessExpression(accessNode); - if (indexType.flags & (TypeFlags.StringLiteral | TypeFlags.NumberLiteral)) { - error(indexNode, Diagnostics.Property_0_does_not_exist_on_type_1, "" + (indexType).value, typeToString(objectType)); - } - else if (indexType.flags & (TypeFlags.String | TypeFlags.Number)) { - error(indexNode, Diagnostics.Type_0_has_no_matching_index_signature_for_type_1, typeToString(objectType), typeToString(indexType)); - } - else { - error(indexNode, Diagnostics.Type_0_cannot_be_used_as_an_index_type, typeToString(indexType)); - } - } - if (isTypeAny(indexType)) { - return indexType; - } - return undefined; - - function errorIfWritingToReadonlyIndex(indexInfo: IndexInfo | undefined): void { - if (indexInfo && indexInfo.isReadonly && accessExpression && (isAssignmentTarget(accessExpression) || isDeleteTarget(accessExpression))) { - error(accessExpression, Diagnostics.Index_signature_in_type_0_only_permits_reading, typeToString(objectType)); - } - } - } - - function getIndexNodeForAccessExpression(accessNode: ElementAccessExpression | IndexedAccessTypeNode | PropertyName | BindingName | SyntheticExpression) { - return accessNode.kind === SyntaxKind.ElementAccessExpression ? accessNode.argumentExpression : - accessNode.kind === SyntaxKind.IndexedAccessType ? accessNode.indexType : - accessNode.kind === SyntaxKind.ComputedPropertyName ? accessNode.expression : - accessNode; - } - - function isGenericObjectType(type: Type): boolean { - if (type.flags & TypeFlags.UnionOrIntersection) { - if (!((type).objectFlags & ObjectFlags.IsGenericObjectTypeComputed)) { - (type).objectFlags |= ObjectFlags.IsGenericObjectTypeComputed | - (some((type).types, isGenericObjectType) ? ObjectFlags.IsGenericObjectType : 0); - } - return !!((type).objectFlags & ObjectFlags.IsGenericObjectType); - } - return !!(type.flags & TypeFlags.InstantiableNonPrimitive) || isGenericMappedType(type); - } - - function isGenericIndexType(type: Type): boolean { - if (type.flags & TypeFlags.UnionOrIntersection) { - if (!((type).objectFlags & ObjectFlags.IsGenericIndexTypeComputed)) { - (type).objectFlags |= ObjectFlags.IsGenericIndexTypeComputed | - (some((type).types, isGenericIndexType) ? ObjectFlags.IsGenericIndexType : 0); - } - return !!((type).objectFlags & ObjectFlags.IsGenericIndexType); - } - return !!(type.flags & (TypeFlags.InstantiableNonPrimitive | TypeFlags.Index)); - } - - function isThisTypeParameter(type: Type): boolean { - return !!(type.flags & TypeFlags.TypeParameter && (type).isThisType); - } - - function getSimplifiedType(type: Type, writing: boolean): Type { - return type.flags & TypeFlags.IndexedAccess ? getSimplifiedIndexedAccessType(type, writing) : - type.flags & TypeFlags.Conditional ? getSimplifiedConditionalType(type, writing) : - type; - } - - function distributeIndexOverObjectType(objectType: Type, indexType: Type, writing: boolean) { - // (T | U)[K] -> T[K] | U[K] (reading) - // (T | U)[K] -> T[K] & U[K] (writing) - // (T & U)[K] -> T[K] & U[K] - if (objectType.flags & TypeFlags.UnionOrIntersection) { - const types = map((objectType as UnionOrIntersectionType).types, t => getSimplifiedType(getIndexedAccessType(t, indexType), writing)); - return objectType.flags & TypeFlags.Intersection || writing ? getIntersectionType(types) : getUnionType(types); - } - } - - function distributeObjectOverIndexType(objectType: Type, indexType: Type, writing: boolean) { - // T[A | B] -> T[A] | T[B] (reading) - // T[A | B] -> T[A] & T[B] (writing) - if (indexType.flags & TypeFlags.Union) { - const types = map((indexType as UnionType).types, t => getSimplifiedType(getIndexedAccessType(objectType, t), writing)); - return writing ? getIntersectionType(types) : getUnionType(types); - } - } - - function unwrapSubstitution(type: Type): Type { - if (type.flags & TypeFlags.Substitution) { - return (type as SubstitutionType).substitute; - } - return type; - } - - // Transform an indexed access to a simpler form, if possible. Return the simpler form, or return - // the type itself if no transformation is possible. The writing flag indicates that the type is - // the target of an assignment. - function getSimplifiedIndexedAccessType(type: IndexedAccessType, writing: boolean): Type { - const cache = writing ? "simplifiedForWriting" : "simplifiedForReading"; - if (type[cache]) { - return type[cache] === circularConstraintType ? type : type[cache]!; - } - type[cache] = circularConstraintType; - // We recursively simplify the object type as it may in turn be an indexed access type. For example, with - // '{ [P in T]: { [Q in U]: number } }[T][U]' we want to first simplify the inner indexed access type. - const objectType = unwrapSubstitution(getSimplifiedType(type.objectType, writing)); - const indexType = getSimplifiedType(type.indexType, writing); - // T[A | B] -> T[A] | T[B] (reading) - // T[A | B] -> T[A] & T[B] (writing) - const distributedOverIndex = distributeObjectOverIndexType(objectType, indexType, writing); - if (distributedOverIndex) { - return type[cache] = distributedOverIndex; - } - // Only do the inner distributions if the index can no longer be instantiated to cause index distribution again - if (!(indexType.flags & TypeFlags.Instantiable)) { - // (T | U)[K] -> T[K] | U[K] (reading) - // (T | U)[K] -> T[K] & U[K] (writing) - // (T & U)[K] -> T[K] & U[K] - const distributedOverObject = distributeIndexOverObjectType(objectType, indexType, writing); - if (distributedOverObject) { - return type[cache] = distributedOverObject; - } - } - // So ultimately (reading): - // ((A & B) | C)[K1 | K2] -> ((A & B) | C)[K1] | ((A & B) | C)[K2] -> (A & B)[K1] | C[K1] | (A & B)[K2] | C[K2] -> (A[K1] & B[K1]) | C[K1] | (A[K2] & B[K2]) | C[K2] - - // If the object type is a mapped type { [P in K]: E }, where K is generic, instantiate E using a mapper - // that substitutes the index type for P. For example, for an index access { [P in K]: Box }[X], we - // construct the type Box. - if (isGenericMappedType(objectType)) { - return type[cache] = mapType(substituteIndexedMappedType(objectType, type.indexType), t => getSimplifiedType(t, writing)); - } - return type[cache] = type; - } - - function getSimplifiedConditionalType(type: ConditionalType, writing: boolean) { - const checkType = type.checkType; - const extendsType = type.extendsType; - const trueType = getTrueTypeFromConditionalType(type); - const falseType = getFalseTypeFromConditionalType(type); - // Simplifications for types of the form `T extends U ? T : never` and `T extends U ? never : T`. - if (falseType.flags & TypeFlags.Never && getActualTypeVariable(trueType) === getActualTypeVariable(checkType)) { - if (checkType.flags & TypeFlags.Any || isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(extendsType))) { // Always true - return getSimplifiedType(trueType, writing); - } - else if (isIntersectionEmpty(checkType, extendsType)) { // Always false - return neverType; - } - } - else if (trueType.flags & TypeFlags.Never && getActualTypeVariable(falseType) === getActualTypeVariable(checkType)) { - if (!(checkType.flags & TypeFlags.Any) && isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(extendsType))) { // Always true - return neverType; - } - else if (checkType.flags & TypeFlags.Any || isIntersectionEmpty(checkType, extendsType)) { // Always false - return getSimplifiedType(falseType, writing); - } - } - return type; - } - - /** - * Invokes union simplification logic to determine if an intersection is considered empty as a union constituent - */ - function isIntersectionEmpty(type1: Type, type2: Type) { - return !!(getUnionType([intersectTypes(type1, type2), neverType]).flags & TypeFlags.Never); - } - - function substituteIndexedMappedType(objectType: MappedType, index: Type) { - const mapper = createTypeMapper([getTypeParameterFromMappedType(objectType)], [index]); - const templateMapper = combineTypeMappers(objectType.mapper, mapper); - return instantiateType(getTemplateTypeFromMappedType(objectType), templateMapper); - } - - function getIndexedAccessType(objectType: Type, indexType: Type, accessNode?: ElementAccessExpression | IndexedAccessTypeNode | PropertyName | BindingName | SyntheticExpression): Type { - return getIndexedAccessTypeOrUndefined(objectType, indexType, accessNode, AccessFlags.None) || (accessNode ? errorType : unknownType); - } - - function getIndexedAccessTypeOrUndefined(objectType: Type, indexType: Type, accessNode?: ElementAccessExpression | IndexedAccessTypeNode | PropertyName | BindingName | SyntheticExpression, accessFlags = AccessFlags.None): Type | undefined { - if (objectType === wildcardType || indexType === wildcardType) { - return wildcardType; - } - // If the object type has a string index signature and no other members we know that the result will - // always be the type of that index signature and we can simplify accordingly. - if (isStringIndexSignatureOnlyType(objectType) && !(indexType.flags & TypeFlags.Nullable) && isTypeAssignableToKind(indexType, TypeFlags.String | TypeFlags.Number)) { - indexType = stringType; - } - // If the index type is generic, or if the object type is generic and doesn't originate in an expression, - // we are performing a higher-order index access where we cannot meaningfully access the properties of the - // object type. Note that for a generic T and a non-generic K, we eagerly resolve T[K] if it originates in - // an expression. This is to preserve backwards compatibility. For example, an element access 'this["foo"]' - // has always been resolved eagerly using the constraint type of 'this' at the given location. - if (isGenericIndexType(indexType) || !(accessNode && accessNode.kind !== SyntaxKind.IndexedAccessType) && isGenericObjectType(objectType)) { - if (objectType.flags & TypeFlags.AnyOrUnknown) { - return objectType; - } - // Defer the operation by creating an indexed access type. - const id = objectType.id + "," + indexType.id; - let type = indexedAccessTypes.get(id); - if (!type) { - indexedAccessTypes.set(id, type = createIndexedAccessType(objectType, indexType)); - } - return type; - } - // In the following we resolve T[K] to the type of the property in T selected by K. - // We treat boolean as different from other unions to improve errors; - // skipping straight to getPropertyTypeForIndexType gives errors with 'boolean' instead of 'true'. - const apparentObjectType = getApparentType(objectType); - if (indexType.flags & TypeFlags.Union && !(indexType.flags & TypeFlags.Boolean)) { - const propTypes: Type[] = []; - let wasMissingProp = false; - for (const t of (indexType).types) { - const propType = getPropertyTypeForIndexType(objectType, apparentObjectType, t, indexType, wasMissingProp, accessNode, accessFlags); - if (propType) { - propTypes.push(propType); - } - else if (!accessNode) { - // If there's no error node, we can immeditely stop, since error reporting is off - return undefined; - } - else { - // Otherwise we set a flag and return at the end of the loop so we still mark all errors - wasMissingProp = true; - } - } - if (wasMissingProp) { - return undefined; - } - return accessFlags & AccessFlags.Writing ? getIntersectionType(propTypes) : getUnionType(propTypes); - } - return getPropertyTypeForIndexType(objectType, apparentObjectType, indexType, indexType, /* supressNoImplicitAnyError */ false, accessNode, accessFlags | AccessFlags.CacheSymbol); - } - - function getTypeFromIndexedAccessTypeNode(node: IndexedAccessTypeNode) { - const links = getNodeLinks(node); - if (!links.resolvedType) { - const objectType = getTypeFromTypeNode(node.objectType); - const indexType = getTypeFromTypeNode(node.indexType); - const resolved = getIndexedAccessType(objectType, indexType, node); - links.resolvedType = resolved.flags & TypeFlags.IndexedAccess && - (resolved).objectType === objectType && - (resolved).indexType === indexType ? - getConstrainedTypeVariable(resolved, node) : resolved; - } - return links.resolvedType; - } - - function getTypeFromMappedTypeNode(node: MappedTypeNode): Type { - const links = getNodeLinks(node); - if (!links.resolvedType) { - const type = createObjectType(ObjectFlags.Mapped, node.symbol); - type.declaration = node; - type.aliasSymbol = getAliasSymbolForTypeNode(node); - type.aliasTypeArguments = getTypeArgumentsForAliasSymbol(type.aliasSymbol); - links.resolvedType = type; - // Eagerly resolve the constraint type which forces an error if the constraint type circularly - // references itself through one or more type aliases. - getConstraintTypeFromMappedType(type); - } - return links.resolvedType; - } - - function getActualTypeVariable(type: Type): Type { - if (type.flags & TypeFlags.Substitution) { - return (type).typeVariable; - } - if (type.flags & TypeFlags.IndexedAccess && ( - (type).objectType.flags & TypeFlags.Substitution || - (type).indexType.flags & TypeFlags.Substitution)) { - return getIndexedAccessType(getActualTypeVariable((type).objectType), getActualTypeVariable((type).indexType)); - } - return type; - } - - function getConditionalType(root: ConditionalRoot, mapper: TypeMapper | undefined): Type { - const checkType = instantiateType(root.checkType, mapper); - const extendsType = instantiateType(root.extendsType, mapper); - if (checkType === wildcardType || extendsType === wildcardType) { - return wildcardType; - } - const checkTypeInstantiable = isGenericObjectType(checkType) || isGenericIndexType(checkType); - let combinedMapper: TypeMapper | undefined; - if (root.inferTypeParameters) { - const context = createInferenceContext(root.inferTypeParameters, /*signature*/ undefined, InferenceFlags.None); - // We skip inference of the possible `infer` types unles the `extendsType` _is_ an infer type - // if it was, it's trivial to say that extendsType = checkType, however such a pattern is used to - // "reset" the type being build up during constraint calculation and avoid making an apparently "infinite" constraint - // so in those cases we refain from performing inference and retain the uninfered type parameter - if (!checkTypeInstantiable || !some(root.inferTypeParameters, t => t === extendsType)) { - // We don't want inferences from constraints as they may cause us to eagerly resolve the - // conditional type instead of deferring resolution. Also, we always want strict function - // types rules (i.e. proper contravariance) for inferences. - inferTypes(context.inferences, checkType, extendsType, InferencePriority.NoConstraints | InferencePriority.AlwaysStrict); - } - combinedMapper = combineTypeMappers(mapper, context.mapper); - } - // Instantiate the extends type including inferences for 'infer T' type parameters - const inferredExtendsType = combinedMapper ? instantiateType(root.extendsType, combinedMapper) : extendsType; - // We attempt to resolve the conditional type only when the check and extends types are non-generic - if (!checkTypeInstantiable && !isGenericObjectType(inferredExtendsType) && !isGenericIndexType(inferredExtendsType)) { - if (inferredExtendsType.flags & TypeFlags.AnyOrUnknown) { - return instantiateType(root.trueType, combinedMapper || mapper); - } - // Return union of trueType and falseType for 'any' since it matches anything - if (checkType.flags & TypeFlags.Any) { - return getUnionType([instantiateType(root.trueType, combinedMapper || mapper), instantiateType(root.falseType, mapper)]); - } - // Return falseType for a definitely false extends check. We check an instantiations of the two - // types with type parameters mapped to the wildcard type, the most permissive instantiations - // possible (the wildcard type is assignable to and from all types). If those are not related, - // then no instantiations will be and we can just return the false branch type. - if (!isTypeAssignableTo(getPermissiveInstantiation(checkType), getPermissiveInstantiation(inferredExtendsType))) { - return instantiateType(root.falseType, mapper); - } - // Return trueType for a definitely true extends check. We check instantiations of the two - // types with type parameters mapped to their restrictive form, i.e. a form of the type parameter - // that has no constraint. This ensures that, for example, the type - // type Foo = T extends { x: string } ? string : number - // doesn't immediately resolve to 'string' instead of being deferred. - if (isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(inferredExtendsType))) { - return instantiateType(root.trueType, combinedMapper || mapper); - } - } - // Return a deferred type for a check that is neither definitely true nor definitely false - const erasedCheckType = getActualTypeVariable(checkType); - const result = createType(TypeFlags.Conditional); - result.root = root; - result.checkType = erasedCheckType; - result.extendsType = extendsType; - result.mapper = mapper; - result.combinedMapper = combinedMapper; - result.aliasSymbol = root.aliasSymbol; - result.aliasTypeArguments = instantiateTypes(root.aliasTypeArguments, mapper!); // TODO: GH#18217 - return result; - } - - function getTrueTypeFromConditionalType(type: ConditionalType) { - return type.resolvedTrueType || (type.resolvedTrueType = instantiateType(type.root.trueType, type.mapper)); - } - - function getFalseTypeFromConditionalType(type: ConditionalType) { - return type.resolvedFalseType || (type.resolvedFalseType = instantiateType(type.root.falseType, type.mapper)); - } - - function getInferredTrueTypeFromConditionalType(type: ConditionalType) { - return type.resolvedInferredTrueType || (type.resolvedInferredTrueType = type.combinedMapper ? instantiateType(type.root.trueType, type.combinedMapper) : getTrueTypeFromConditionalType(type)); - } - - function getInferTypeParameters(node: ConditionalTypeNode): TypeParameter[] | undefined { - let result: TypeParameter[] | undefined; - if (node.locals) { - node.locals.forEach(symbol => { - if (symbol.flags & SymbolFlags.TypeParameter) { - result = append(result, getDeclaredTypeOfSymbol(symbol)); - } - }); - } - return result; - } - - function getTypeFromConditionalTypeNode(node: ConditionalTypeNode): Type { - const links = getNodeLinks(node); - if (!links.resolvedType) { - const checkType = getTypeFromTypeNode(node.checkType); - const aliasSymbol = getAliasSymbolForTypeNode(node); - const aliasTypeArguments = getTypeArgumentsForAliasSymbol(aliasSymbol); - const allOuterTypeParameters = getOuterTypeParameters(node, /*includeThisTypes*/ true); - const outerTypeParameters = aliasTypeArguments ? allOuterTypeParameters : filter(allOuterTypeParameters, tp => isTypeParameterPossiblyReferenced(tp, node)); - const root: ConditionalRoot = { - node, - checkType, - extendsType: getTypeFromTypeNode(node.extendsType), - trueType: getTypeFromTypeNode(node.trueType), - falseType: getTypeFromTypeNode(node.falseType), - isDistributive: !!(checkType.flags & TypeFlags.TypeParameter), - inferTypeParameters: getInferTypeParameters(node), - outerTypeParameters, - instantiations: undefined, - aliasSymbol, - aliasTypeArguments - }; - links.resolvedType = getConditionalType(root, /*mapper*/ undefined); - if (outerTypeParameters) { - root.instantiations = createMap(); - root.instantiations.set(getTypeListId(outerTypeParameters), links.resolvedType); - } - } - return links.resolvedType; - } - - function getTypeFromInferTypeNode(node: InferTypeNode): Type { - const links = getNodeLinks(node); - if (!links.resolvedType) { - links.resolvedType = getDeclaredTypeOfTypeParameter(getSymbolOfNode(node.typeParameter)); - } - return links.resolvedType; - } - - function getIdentifierChain(node: EntityName): Identifier[] { - if (isIdentifier(node)) { - return [node]; - } - else { - return append(getIdentifierChain(node.left), node.right); - } - } - - function getTypeFromImportTypeNode(node: ImportTypeNode): Type { - const links = getNodeLinks(node); - if (!links.resolvedType) { - if (node.isTypeOf && node.typeArguments) { // Only the non-typeof form can make use of type arguments - error(node, Diagnostics.Type_arguments_cannot_be_used_here); - links.resolvedSymbol = unknownSymbol; - return links.resolvedType = errorType; - } - if (!isLiteralImportTypeNode(node)) { - error(node.argument, Diagnostics.String_literal_expected); - links.resolvedSymbol = unknownSymbol; - return links.resolvedType = errorType; - } - const targetMeaning = node.isTypeOf ? SymbolFlags.Value : node.flags & NodeFlags.JSDoc ? SymbolFlags.Value | SymbolFlags.Type : SymbolFlags.Type; - // TODO: Future work: support unions/generics/whatever via a deferred import-type - const innerModuleSymbol = resolveExternalModuleName(node, node.argument.literal); - if (!innerModuleSymbol) { - links.resolvedSymbol = unknownSymbol; - return links.resolvedType = errorType; - } - const moduleSymbol = resolveExternalModuleSymbol(innerModuleSymbol, /*dontResolveAlias*/ false); - if (!nodeIsMissing(node.qualifier)) { - const nameStack: Identifier[] = getIdentifierChain(node.qualifier!); - let currentNamespace = moduleSymbol; - let current: Identifier | undefined; - while (current = nameStack.shift()) { - const meaning = nameStack.length ? SymbolFlags.Namespace : targetMeaning; - const next = getSymbol(getExportsOfSymbol(getMergedSymbol(resolveSymbol(currentNamespace))), current.escapedText, meaning); - if (!next) { - error(current, Diagnostics.Namespace_0_has_no_exported_member_1, getFullyQualifiedName(currentNamespace), declarationNameToString(current)); - return links.resolvedType = errorType; - } - getNodeLinks(current).resolvedSymbol = next; - getNodeLinks(current.parent).resolvedSymbol = next; - currentNamespace = next; - } - links.resolvedType = resolveImportSymbolType(node, links, currentNamespace, targetMeaning); - } - else { - if (moduleSymbol.flags & targetMeaning) { - links.resolvedType = resolveImportSymbolType(node, links, moduleSymbol, targetMeaning); - } - else { - const errorMessage = targetMeaning === SymbolFlags.Value - ? Diagnostics.Module_0_does_not_refer_to_a_value_but_is_used_as_a_value_here - : Diagnostics.Module_0_does_not_refer_to_a_type_but_is_used_as_a_type_here_Did_you_mean_typeof_import_0; - - error(node, errorMessage, node.argument.literal.text); - - links.resolvedSymbol = unknownSymbol; - links.resolvedType = errorType; - } - } - } - return links.resolvedType; - } - - function resolveImportSymbolType(node: ImportTypeNode, links: NodeLinks, symbol: Symbol, meaning: SymbolFlags) { - const resolvedSymbol = resolveSymbol(symbol); - links.resolvedSymbol = resolvedSymbol; - if (meaning === SymbolFlags.Value) { - return getTypeOfSymbol(symbol); // intentionally doesn't use resolved symbol so type is cached as expected on the alias - } - else { - return getTypeReferenceType(node, resolvedSymbol); // getTypeReferenceType doesn't handle aliases - it must get the resolved symbol - } - } - - function getTypeFromTypeLiteralOrFunctionOrConstructorTypeNode(node: TypeNode): Type { - const links = getNodeLinks(node); - if (!links.resolvedType) { - // Deferred resolution of members is handled by resolveObjectTypeMembers - const aliasSymbol = getAliasSymbolForTypeNode(node); - if (getMembersOfSymbol(node.symbol).size === 0 && !aliasSymbol) { - links.resolvedType = emptyTypeLiteralType; - } - else { - let type = createObjectType(ObjectFlags.Anonymous, node.symbol); - type.aliasSymbol = aliasSymbol; - type.aliasTypeArguments = getTypeArgumentsForAliasSymbol(aliasSymbol); - if (isJSDocTypeLiteral(node) && node.isArrayType) { - type = createArrayType(type); - } - links.resolvedType = type; - } - } - return links.resolvedType; - } - - function getAliasSymbolForTypeNode(node: Node) { - let host = node.parent; - while (isParenthesizedTypeNode(host) || isTypeOperatorNode(host) && host.operator === SyntaxKind.ReadonlyKeyword) { - host = host.parent; - } - return isTypeAlias(host) ? getSymbolOfNode(host) : undefined; - } - - function getTypeArgumentsForAliasSymbol(symbol: Symbol | undefined) { - return symbol ? getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol) : undefined; - } - - function isNonGenericObjectType(type: Type) { - return !!(type.flags & TypeFlags.Object) && !isGenericMappedType(type); - } - - function isEmptyObjectTypeOrSpreadsIntoEmptyObject(type: Type) { - return isEmptyObjectType(type) || !!(type.flags & (TypeFlags.Null | TypeFlags.Undefined | TypeFlags.BooleanLike | TypeFlags.NumberLike | TypeFlags.BigIntLike | TypeFlags.StringLike | TypeFlags.EnumLike | TypeFlags.NonPrimitive | TypeFlags.Index)); - } - - function isSinglePropertyAnonymousObjectType(type: Type) { - return !!(type.flags & TypeFlags.Object) && - !!(getObjectFlags(type) & ObjectFlags.Anonymous) && - (length(getPropertiesOfType(type)) === 1 || every(getPropertiesOfType(type), p => !!(p.flags & SymbolFlags.Optional))); - } - - function tryMergeUnionOfObjectTypeAndEmptyObject(type: UnionType, readonly: boolean): Type | undefined { - if (type.types.length === 2) { - const firstType = type.types[0]; - const secondType = type.types[1]; - if (every(type.types, isEmptyObjectTypeOrSpreadsIntoEmptyObject)) { - return isEmptyObjectType(firstType) ? firstType : isEmptyObjectType(secondType) ? secondType : emptyObjectType; - } - if (isEmptyObjectTypeOrSpreadsIntoEmptyObject(firstType) && isSinglePropertyAnonymousObjectType(secondType)) { - return getAnonymousPartialType(secondType); - } - if (isEmptyObjectTypeOrSpreadsIntoEmptyObject(secondType) && isSinglePropertyAnonymousObjectType(firstType)) { - return getAnonymousPartialType(firstType); - } - } - - function getAnonymousPartialType(type: Type) { - // gets the type as if it had been spread, but where everything in the spread is made optional - const members = createSymbolTable(); - for (const prop of getPropertiesOfType(type)) { - if (getDeclarationModifierFlagsFromSymbol(prop) & (ModifierFlags.Private | ModifierFlags.Protected)) { - // do nothing, skip privates - } - else if (isSpreadableProperty(prop)) { - const isSetonlyAccessor = prop.flags & SymbolFlags.SetAccessor && !(prop.flags & SymbolFlags.GetAccessor); - const flags = SymbolFlags.Property | SymbolFlags.Optional; - const result = createSymbol(flags, prop.escapedName, readonly ? CheckFlags.Readonly : 0); - result.type = isSetonlyAccessor ? undefinedType : getTypeOfSymbol(prop); - result.declarations = prop.declarations; - result.nameType = getSymbolLinks(prop).nameType; - result.syntheticOrigin = prop; - members.set(prop.escapedName, result); - } - } - const spread = createAnonymousType( - type.symbol, - members, - emptyArray, - emptyArray, - getIndexInfoOfType(type, IndexKind.String), - getIndexInfoOfType(type, IndexKind.Number)); - spread.objectFlags |= ObjectFlags.ObjectLiteral | ObjectFlags.ContainsObjectOrArrayLiteral; - return spread; - } - } - - /** - * Since the source of spread types are object literals, which are not binary, - * this function should be called in a left folding style, with left = previous result of getSpreadType - * and right = the new element to be spread. - */ - function getSpreadType(left: Type, right: Type, symbol: Symbol | undefined, objectFlags: ObjectFlags, readonly: boolean, isParentTypeNullable?: boolean): Type { - if (left.flags & TypeFlags.Any || right.flags & TypeFlags.Any) { - return anyType; - } - if (left.flags & TypeFlags.Unknown || right.flags & TypeFlags.Unknown) { - return unknownType; - } - if (left.flags & TypeFlags.Never) { - return right; - } - if (right.flags & TypeFlags.Never) { - return left; - } - if (left.flags & TypeFlags.Union) { - const merged = tryMergeUnionOfObjectTypeAndEmptyObject(left as UnionType, readonly); - if (merged) { - return getSpreadType(merged, right, symbol, objectFlags, readonly, isParentTypeNullable); - } - return mapType(left, t => getSpreadType(t, right, symbol, objectFlags, readonly, isParentTypeNullable)); - } - if (right.flags & TypeFlags.Union) { - const merged = tryMergeUnionOfObjectTypeAndEmptyObject(right as UnionType, readonly); - if (merged) { - return getSpreadType(left, merged, symbol, objectFlags, readonly, maybeTypeOfKind(right, TypeFlags.Nullable)); - } - return mapType(right, t => getSpreadType(left, t, symbol, objectFlags, readonly, maybeTypeOfKind(right, TypeFlags.Nullable))); - } - if (right.flags & (TypeFlags.BooleanLike | TypeFlags.NumberLike | TypeFlags.BigIntLike | TypeFlags.StringLike | TypeFlags.EnumLike | TypeFlags.NonPrimitive | TypeFlags.Index)) { - return left; - } - - if (isGenericObjectType(left) || isGenericObjectType(right)) { - if (isEmptyObjectType(left)) { - return right; - } - // When the left type is an intersection, we may need to merge the last constituent of the - // intersection with the right type. For example when the left type is 'T & { a: string }' - // and the right type is '{ b: string }' we produce 'T & { a: string, b: string }'. - if (left.flags & TypeFlags.Intersection) { - const types = (left).types; - const lastLeft = types[types.length - 1]; - if (isNonGenericObjectType(lastLeft) && isNonGenericObjectType(right)) { - return getIntersectionType(concatenate(types.slice(0, types.length - 1), [getSpreadType(lastLeft, right, symbol, objectFlags, readonly)])); - } - } - return getIntersectionType([left, right]); - } - - const members = createSymbolTable(); - const skippedPrivateMembers = createUnderscoreEscapedMap(); - let stringIndexInfo: IndexInfo | undefined; - let numberIndexInfo: IndexInfo | undefined; - if (left === emptyObjectType) { - // for the first spread element, left === emptyObjectType, so take the right's string indexer - stringIndexInfo = getIndexInfoOfType(right, IndexKind.String); - numberIndexInfo = getIndexInfoOfType(right, IndexKind.Number); - } - else { - stringIndexInfo = unionSpreadIndexInfos(getIndexInfoOfType(left, IndexKind.String), getIndexInfoOfType(right, IndexKind.String)); - numberIndexInfo = unionSpreadIndexInfos(getIndexInfoOfType(left, IndexKind.Number), getIndexInfoOfType(right, IndexKind.Number)); - } - - for (const rightProp of getPropertiesOfType(right)) { - if (getDeclarationModifierFlagsFromSymbol(rightProp) & (ModifierFlags.Private | ModifierFlags.Protected)) { - skippedPrivateMembers.set(rightProp.escapedName, true); - } - else if (isSpreadableProperty(rightProp)) { - members.set(rightProp.escapedName, getSpreadSymbol(rightProp, readonly)); - } - } - - for (const leftProp of getPropertiesOfType(left)) { - if (skippedPrivateMembers.has(leftProp.escapedName) || !isSpreadableProperty(leftProp)) { - continue; - } - if (members.has(leftProp.escapedName)) { - const rightProp = members.get(leftProp.escapedName)!; - const rightType = getTypeOfSymbol(rightProp); - if (rightProp.flags & SymbolFlags.Optional) { - const declarations = concatenate(leftProp.declarations, rightProp.declarations); - const flags = SymbolFlags.Property | (leftProp.flags & SymbolFlags.Optional); - const result = createSymbol(flags, leftProp.escapedName); - result.type = getUnionType([getTypeOfSymbol(leftProp), getTypeWithFacts(rightType, TypeFacts.NEUndefined)]); - result.leftSpread = leftProp; - result.rightSpread = rightProp; - result.declarations = declarations; - result.nameType = getSymbolLinks(leftProp).nameType; - members.set(leftProp.escapedName, result); - } - else if (strictNullChecks && - !isParentTypeNullable && - symbol && - !isFromSpreadAssignment(leftProp, symbol) && - isFromSpreadAssignment(rightProp, symbol) && - !maybeTypeOfKind(rightType, TypeFlags.Nullable)) { - error(leftProp.valueDeclaration, Diagnostics._0_is_specified_more_than_once_so_this_usage_will_be_overwritten, unescapeLeadingUnderscores(leftProp.escapedName)); - } - } - else { - members.set(leftProp.escapedName, getSpreadSymbol(leftProp, readonly)); - } - } - - const spread = createAnonymousType( - symbol, - members, - emptyArray, - emptyArray, - getIndexInfoWithReadonly(stringIndexInfo, readonly), - getIndexInfoWithReadonly(numberIndexInfo, readonly)); - spread.objectFlags |= ObjectFlags.ObjectLiteral | ObjectFlags.ContainsObjectOrArrayLiteral | ObjectFlags.ContainsSpread | objectFlags; - return spread; - } - - /** We approximate own properties as non-methods plus methods that are inside the object literal */ - function isSpreadableProperty(prop: Symbol): boolean { - return !some(prop.declarations, isPrivateIdentifierPropertyDeclaration) && - (!(prop.flags & (SymbolFlags.Method | SymbolFlags.GetAccessor | SymbolFlags.SetAccessor)) || - !prop.declarations.some(decl => isClassLike(decl.parent))); - } - - function getSpreadSymbol(prop: Symbol, readonly: boolean) { - const isSetonlyAccessor = prop.flags & SymbolFlags.SetAccessor && !(prop.flags & SymbolFlags.GetAccessor); - if (!isSetonlyAccessor && readonly === isReadonlySymbol(prop)) { - return prop; - } - const flags = SymbolFlags.Property | (prop.flags & SymbolFlags.Optional); - const result = createSymbol(flags, prop.escapedName, readonly ? CheckFlags.Readonly : 0); - result.type = isSetonlyAccessor ? undefinedType : getTypeOfSymbol(prop); - result.declarations = prop.declarations; - result.nameType = getSymbolLinks(prop).nameType; - result.syntheticOrigin = prop; - return result; - } - - function getIndexInfoWithReadonly(info: IndexInfo | undefined, readonly: boolean) { - return info && info.isReadonly !== readonly ? createIndexInfo(info.type, readonly, info.declaration) : info; - } - - function createLiteralType(flags: TypeFlags, value: string | number | PseudoBigInt, symbol: Symbol | undefined) { - const type = createType(flags); - type.symbol = symbol!; - type.value = value; - return type; - } - - function getFreshTypeOfLiteralType(type: Type): Type { - if (type.flags & TypeFlags.Literal) { - if (!(type).freshType) { - const freshType = createLiteralType(type.flags, (type).value, (type).symbol); - freshType.regularType = type; - freshType.freshType = freshType; - (type).freshType = freshType; - } - return (type).freshType; - } - return type; - } - - function getRegularTypeOfLiteralType(type: Type): Type { - return type.flags & TypeFlags.Literal ? (type).regularType : - type.flags & TypeFlags.Union ? getUnionType(sameMap((type).types, getRegularTypeOfLiteralType)) : - type; - } - - function isFreshLiteralType(type: Type) { - return !!(type.flags & TypeFlags.Literal) && (type).freshType === type; - } - - function getLiteralType(value: string | number | PseudoBigInt, enumId?: number, symbol?: Symbol) { - // We store all literal types in a single map with keys of the form '#NNN' and '@SSS', - // where NNN is the text representation of a numeric literal and SSS are the characters - // of a string literal. For literal enum members we use 'EEE#NNN' and 'EEE@SSS', where - // EEE is a unique id for the containing enum type. - const qualifier = typeof value === "number" ? "#" : typeof value === "string" ? "@" : "n"; - const key = (enumId ? enumId : "") + qualifier + (typeof value === "object" ? pseudoBigIntToString(value) : value); - let type = literalTypes.get(key); - if (!type) { - const flags = (typeof value === "number" ? TypeFlags.NumberLiteral : - typeof value === "string" ? TypeFlags.StringLiteral : TypeFlags.BigIntLiteral) | - (enumId ? TypeFlags.EnumLiteral : 0); - literalTypes.set(key, type = createLiteralType(flags, value, symbol)); - type.regularType = type; - } - return type; - } - - function getTypeFromLiteralTypeNode(node: LiteralTypeNode): Type { - const links = getNodeLinks(node); - if (!links.resolvedType) { - links.resolvedType = getRegularTypeOfLiteralType(checkExpression(node.literal)); - } - return links.resolvedType; - } - - function createUniqueESSymbolType(symbol: Symbol) { - const type = createType(TypeFlags.UniqueESSymbol); - type.symbol = symbol; - type.escapedName = `__@${type.symbol.escapedName}@${getSymbolId(type.symbol)}` as __String; - return type; - } - - function getESSymbolLikeTypeForNode(node: Node) { - if (isValidESSymbolDeclaration(node)) { - const symbol = getSymbolOfNode(node); - const links = getSymbolLinks(symbol); - return links.uniqueESSymbolType || (links.uniqueESSymbolType = createUniqueESSymbolType(symbol)); - } - return esSymbolType; - } - - function getThisType(node: Node): Type { - const container = getThisContainer(node, /*includeArrowFunctions*/ false); - const parent = container && container.parent; - if (parent && (isClassLike(parent) || parent.kind === SyntaxKind.InterfaceDeclaration)) { - if (!hasModifier(container, ModifierFlags.Static) && - (!isConstructorDeclaration(container) || isNodeDescendantOf(node, container.body))) { - return getDeclaredTypeOfClassOrInterface(getSymbolOfNode(parent as ClassLikeDeclaration | InterfaceDeclaration)).thisType!; - } - } - - // inside x.prototype = { ... } - if (parent && isObjectLiteralExpression(parent) && isBinaryExpression(parent.parent) && getAssignmentDeclarationKind(parent.parent) === AssignmentDeclarationKind.Prototype) { - return getDeclaredTypeOfClassOrInterface(getSymbolOfNode(parent.parent.left)!.parent!).thisType!; - } - // /** @return {this} */ - // x.prototype.m = function() { ... } - const host = node.flags & NodeFlags.JSDoc ? getHostSignatureFromJSDoc(node) : undefined; - if (host && isFunctionExpression(host) && isBinaryExpression(host.parent) && getAssignmentDeclarationKind(host.parent) === AssignmentDeclarationKind.PrototypeProperty) { - return getDeclaredTypeOfClassOrInterface(getSymbolOfNode(host.parent.left)!.parent!).thisType!; - } - // inside constructor function C() { ... } - if (isJSConstructor(container) && isNodeDescendantOf(node, container.body)) { - return getDeclaredTypeOfClassOrInterface(getSymbolOfNode(container)).thisType!; - } - error(node, Diagnostics.A_this_type_is_available_only_in_a_non_static_member_of_a_class_or_interface); - return errorType; - } - - function getTypeFromThisTypeNode(node: ThisExpression | ThisTypeNode): Type { - const links = getNodeLinks(node); - if (!links.resolvedType) { - links.resolvedType = getThisType(node); - } - return links.resolvedType; - } - - function getTypeFromTypeNode(node: TypeNode): Type { - switch (node.kind) { - case SyntaxKind.AnyKeyword: - case SyntaxKind.JSDocAllType: - case SyntaxKind.JSDocUnknownType: - return anyType; - case SyntaxKind.UnknownKeyword: - return unknownType; - case SyntaxKind.StringKeyword: - return stringType; - case SyntaxKind.NumberKeyword: - return numberType; - case SyntaxKind.BigIntKeyword: - return bigintType; - case SyntaxKind.BooleanKeyword: - return booleanType; - case SyntaxKind.SymbolKeyword: - return esSymbolType; - case SyntaxKind.VoidKeyword: - return voidType; - case SyntaxKind.UndefinedKeyword: - return undefinedType; - case SyntaxKind.NullKeyword: - return nullType; - case SyntaxKind.NeverKeyword: - return neverType; - case SyntaxKind.ObjectKeyword: - return node.flags & NodeFlags.JavaScriptFile && !noImplicitAny ? anyType : nonPrimitiveType; - case SyntaxKind.ThisType: - case SyntaxKind.ThisKeyword: - return getTypeFromThisTypeNode(node as ThisExpression | ThisTypeNode); - case SyntaxKind.LiteralType: - return getTypeFromLiteralTypeNode(node); - case SyntaxKind.TypeReference: - return getTypeFromTypeReference(node); - case SyntaxKind.TypePredicate: - return (node).assertsModifier ? voidType : booleanType; - case SyntaxKind.ExpressionWithTypeArguments: - return getTypeFromTypeReference(node); - case SyntaxKind.TypeQuery: - return getTypeFromTypeQueryNode(node); - case SyntaxKind.ArrayType: - case SyntaxKind.TupleType: - return getTypeFromArrayOrTupleTypeNode(node); - case SyntaxKind.OptionalType: - return getTypeFromOptionalTypeNode(node); - case SyntaxKind.UnionType: - return getTypeFromUnionTypeNode(node); - case SyntaxKind.IntersectionType: - return getTypeFromIntersectionTypeNode(node); - case SyntaxKind.JSDocNullableType: - return getTypeFromJSDocNullableTypeNode(node); - case SyntaxKind.JSDocOptionalType: - return addOptionality(getTypeFromTypeNode((node as JSDocOptionalType).type)); - case SyntaxKind.ParenthesizedType: - case SyntaxKind.JSDocNonNullableType: - case SyntaxKind.JSDocTypeExpression: - return getTypeFromTypeNode((node).type); - case SyntaxKind.RestType: - return getElementTypeOfArrayType(getTypeFromTypeNode((node).type)) || errorType; - case SyntaxKind.JSDocVariadicType: - return getTypeFromJSDocVariadicType(node as JSDocVariadicType); - case SyntaxKind.FunctionType: - case SyntaxKind.ConstructorType: - case SyntaxKind.TypeLiteral: - case SyntaxKind.JSDocTypeLiteral: - case SyntaxKind.JSDocFunctionType: - case SyntaxKind.JSDocSignature: - return getTypeFromTypeLiteralOrFunctionOrConstructorTypeNode(node); - case SyntaxKind.TypeOperator: - return getTypeFromTypeOperatorNode(node); - case SyntaxKind.IndexedAccessType: - return getTypeFromIndexedAccessTypeNode(node); - case SyntaxKind.MappedType: - return getTypeFromMappedTypeNode(node); - case SyntaxKind.ConditionalType: - return getTypeFromConditionalTypeNode(node); - case SyntaxKind.InferType: - return getTypeFromInferTypeNode(node); - case SyntaxKind.ImportType: - return getTypeFromImportTypeNode(node); - // This function assumes that an identifier or qualified name is a type expression - // Callers should first ensure this by calling isTypeNode - case SyntaxKind.Identifier: - case SyntaxKind.QualifiedName: - const symbol = getSymbolAtLocation(node); - return symbol ? getDeclaredTypeOfSymbol(symbol) : errorType; - default: - return errorType; - } - } - - function instantiateList(items: readonly T[], mapper: TypeMapper, instantiator: (item: T, mapper: TypeMapper) => T): readonly T[]; - function instantiateList(items: readonly T[] | undefined, mapper: TypeMapper, instantiator: (item: T, mapper: TypeMapper) => T): readonly T[] | undefined; - function instantiateList(items: readonly T[] | undefined, mapper: TypeMapper, instantiator: (item: T, mapper: TypeMapper) => T): readonly T[] | undefined { - if (items && items.length) { - for (let i = 0; i < items.length; i++) { - const item = items[i]; - const mapped = instantiator(item, mapper); - if (item !== mapped) { - const result = i === 0 ? [] : items.slice(0, i); - result.push(mapped); - for (i++; i < items.length; i++) { - result.push(instantiator(items[i], mapper)); - } - return result; - } - } - } - return items; - } - - function instantiateTypes(types: readonly Type[], mapper: TypeMapper): readonly Type[]; - function instantiateTypes(types: readonly Type[] | undefined, mapper: TypeMapper): readonly Type[] | undefined; - function instantiateTypes(types: readonly Type[] | undefined, mapper: TypeMapper): readonly Type[] | undefined { - return instantiateList(types, mapper, instantiateType); - } - - function instantiateSignatures(signatures: readonly Signature[], mapper: TypeMapper): readonly Signature[] { - return instantiateList(signatures, mapper, instantiateSignature); - } - - function makeUnaryTypeMapper(source: Type, target: Type) { - return (t: Type) => t === source ? target : t; - } - - function makeBinaryTypeMapper(source1: Type, target1: Type, source2: Type, target2: Type) { - return (t: Type) => t === source1 ? target1 : t === source2 ? target2 : t; - } - - function makeArrayTypeMapper(sources: readonly Type[], targets: readonly Type[] | undefined) { - return (t: Type) => { - for (let i = 0; i < sources.length; i++) { - if (t === sources[i]) { - return targets ? targets[i] : anyType; - } - } - return t; - }; - } - - function createTypeMapper(sources: readonly TypeParameter[], targets: readonly Type[] | undefined): TypeMapper { - Debug.assert(targets === undefined || sources.length === targets.length); - return sources.length === 1 ? makeUnaryTypeMapper(sources[0], targets ? targets[0] : anyType) : - sources.length === 2 ? makeBinaryTypeMapper(sources[0], targets ? targets[0] : anyType, sources[1], targets ? targets[1] : anyType) : - makeArrayTypeMapper(sources, targets); - } - - function createTypeEraser(sources: readonly TypeParameter[]): TypeMapper { - return createTypeMapper(sources, /*targets*/ undefined); - } - - /** - * Maps forward-references to later types parameters to the empty object type. - * This is used during inference when instantiating type parameter defaults. - */ - function createBackreferenceMapper(context: InferenceContext, index: number): TypeMapper { - return t => findIndex(context.inferences, info => info.typeParameter === t) >= index ? unknownType : t; - } - - function combineTypeMappers(mapper1: TypeMapper | undefined, mapper2: TypeMapper): TypeMapper; - function combineTypeMappers(mapper1: TypeMapper, mapper2: TypeMapper | undefined): TypeMapper; - function combineTypeMappers(mapper1: TypeMapper, mapper2: TypeMapper): TypeMapper { - if (!mapper1) return mapper2; - if (!mapper2) return mapper1; - return t => instantiateType(mapper1(t), mapper2); - } - - function createReplacementMapper(source: Type, target: Type, baseMapper: TypeMapper): TypeMapper { - return t => t === source ? target : baseMapper(t); - } - - function permissiveMapper(type: Type) { - return type.flags & TypeFlags.TypeParameter ? wildcardType : type; - } - - function getRestrictiveTypeParameter(tp: TypeParameter) { - return tp.constraint === unknownType ? tp : tp.restrictiveInstantiation || ( - tp.restrictiveInstantiation = createTypeParameter(tp.symbol), - (tp.restrictiveInstantiation as TypeParameter).constraint = unknownType, - tp.restrictiveInstantiation - ); - } - - function restrictiveMapper(type: Type) { - return type.flags & TypeFlags.TypeParameter ? getRestrictiveTypeParameter(type) : type; - } - - function cloneTypeParameter(typeParameter: TypeParameter): TypeParameter { - const result = createTypeParameter(typeParameter.symbol); - result.target = typeParameter; - return result; - } - - function instantiateTypePredicate(predicate: TypePredicate, mapper: TypeMapper): TypePredicate { - return createTypePredicate(predicate.kind, predicate.parameterName, predicate.parameterIndex, instantiateType(predicate.type, mapper)); - } - - function instantiateSignature(signature: Signature, mapper: TypeMapper, eraseTypeParameters?: boolean): Signature { - let freshTypeParameters: TypeParameter[] | undefined; - if (signature.typeParameters && !eraseTypeParameters) { - // First create a fresh set of type parameters, then include a mapping from the old to the - // new type parameters in the mapper function. Finally store this mapper in the new type - // parameters such that we can use it when instantiating constraints. - freshTypeParameters = map(signature.typeParameters, cloneTypeParameter); - mapper = combineTypeMappers(createTypeMapper(signature.typeParameters, freshTypeParameters), mapper); - for (const tp of freshTypeParameters) { - tp.mapper = mapper; - } - } - // Don't compute resolvedReturnType and resolvedTypePredicate now, - // because using `mapper` now could trigger inferences to become fixed. (See `createInferenceContext`.) - // See GH#17600. - const result = createSignature(signature.declaration, freshTypeParameters, - signature.thisParameter && instantiateSymbol(signature.thisParameter, mapper), - instantiateList(signature.parameters, mapper, instantiateSymbol), - /*resolvedReturnType*/ undefined, - /*resolvedTypePredicate*/ undefined, - signature.minArgumentCount, - signature.flags & SignatureFlags.PropagatingFlags); - result.target = signature; - result.mapper = mapper; - return result; - } - - function instantiateSymbol(symbol: Symbol, mapper: TypeMapper): Symbol { - const links = getSymbolLinks(symbol); - if (links.type && !maybeTypeOfKind(links.type, TypeFlags.Object | TypeFlags.Instantiable)) { - // If the type of the symbol is already resolved, and if that type could not possibly - // be affected by instantiation, simply return the symbol itself. - return symbol; - } - if (getCheckFlags(symbol) & CheckFlags.Instantiated) { - // If symbol being instantiated is itself a instantiation, fetch the original target and combine the - // type mappers. This ensures that original type identities are properly preserved and that aliases - // always reference a non-aliases. - symbol = links.target!; - mapper = combineTypeMappers(links.mapper, mapper); - } - // Keep the flags from the symbol we're instantiating. Mark that is instantiated, and - // also transient so that we can just store data on it directly. - const result = createSymbol(symbol.flags, symbol.escapedName, CheckFlags.Instantiated | getCheckFlags(symbol) & (CheckFlags.Readonly | CheckFlags.Late | CheckFlags.OptionalParameter | CheckFlags.RestParameter)); - result.declarations = symbol.declarations; - result.parent = symbol.parent; - result.target = symbol; - result.mapper = mapper; - if (symbol.valueDeclaration) { - result.valueDeclaration = symbol.valueDeclaration; - } - if (links.nameType) { - result.nameType = links.nameType; - } - return result; - } - - function getObjectTypeInstantiation(type: AnonymousType | DeferredTypeReference, mapper: TypeMapper) { - const target = type.objectFlags & ObjectFlags.Instantiated ? type.target! : type; - const node = type.objectFlags & ObjectFlags.Reference ? (type).node! : type.symbol.declarations[0]; - const links = getNodeLinks(node); - let typeParameters = links.outerTypeParameters; - if (!typeParameters) { - // The first time an anonymous type is instantiated we compute and store a list of the type - // parameters that are in scope (and therefore potentially referenced). For type literals that - // aren't the right hand side of a generic type alias declaration we optimize by reducing the - // set of type parameters to those that are possibly referenced in the literal. - let declaration = node; - if (isInJSFile(declaration)) { - const paramTag = findAncestor(declaration, isJSDocParameterTag); - if (paramTag) { - const paramSymbol = getParameterSymbolFromJSDoc(paramTag); - if (paramSymbol) { - declaration = paramSymbol.valueDeclaration; - } - } - } - let outerTypeParameters = getOuterTypeParameters(declaration, /*includeThisTypes*/ true); - if (isJSConstructor(declaration)) { - const templateTagParameters = getTypeParametersFromDeclaration(declaration as DeclarationWithTypeParameters); - outerTypeParameters = addRange(outerTypeParameters, templateTagParameters); - } - typeParameters = outerTypeParameters || emptyArray; - typeParameters = (target.objectFlags & ObjectFlags.Reference || target.symbol.flags & SymbolFlags.TypeLiteral) && !target.aliasTypeArguments ? - filter(typeParameters, tp => isTypeParameterPossiblyReferenced(tp, declaration)) : - typeParameters; - links.outerTypeParameters = typeParameters; - if (typeParameters.length) { - links.instantiations = createMap(); - links.instantiations.set(getTypeListId(typeParameters), target); - } - } - if (typeParameters.length) { - // We are instantiating an anonymous type that has one or more type parameters in scope. Apply the - // mapper to the type parameters to produce the effective list of type arguments, and compute the - // instantiation cache key from the type IDs of the type arguments. - const typeArguments = map(typeParameters, combineTypeMappers(type.mapper, mapper)); - const id = getTypeListId(typeArguments); - let result = links.instantiations!.get(id); - if (!result) { - const newMapper = createTypeMapper(typeParameters, typeArguments); - result = target.objectFlags & ObjectFlags.Reference ? createDeferredTypeReference((type).target, (type).node, newMapper) : - target.objectFlags & ObjectFlags.Mapped ? instantiateMappedType(target, newMapper) : - instantiateAnonymousType(target, newMapper); - links.instantiations!.set(id, result); - } - return result; - } - return type; - } - - function maybeTypeParameterReference(node: Node) { - return !(node.kind === SyntaxKind.QualifiedName || - node.parent.kind === SyntaxKind.TypeReference && (node.parent).typeArguments && node === (node.parent).typeName || - node.parent.kind === SyntaxKind.ImportType && (node.parent as ImportTypeNode).typeArguments && node === (node.parent as ImportTypeNode).qualifier); - } - - function isTypeParameterPossiblyReferenced(tp: TypeParameter, node: Node) { - // If the type parameter doesn't have exactly one declaration, if there are invening statement blocks - // between the node and the type parameter declaration, if the node contains actual references to the - // type parameter, or if the node contains type queries, we consider the type parameter possibly referenced. - if (tp.symbol && tp.symbol.declarations && tp.symbol.declarations.length === 1) { - const container = tp.symbol.declarations[0].parent; - for (let n = node; n !== container; n = n.parent) { - if (!n || n.kind === SyntaxKind.Block || n.kind === SyntaxKind.ConditionalType && forEachChild((n).extendsType, containsReference)) { - return true; - } - } - return !!forEachChild(node, containsReference); - } - return true; - function containsReference(node: Node): boolean { - switch (node.kind) { - case SyntaxKind.ThisType: - return !!tp.isThisType; - case SyntaxKind.Identifier: - return !tp.isThisType && isPartOfTypeNode(node) && maybeTypeParameterReference(node) && - getTypeFromTypeNode(node) === tp; - case SyntaxKind.TypeQuery: - return true; - } - return !!forEachChild(node, containsReference); - } - } - - function getHomomorphicTypeVariable(type: MappedType) { - const constraintType = getConstraintTypeFromMappedType(type); - if (constraintType.flags & TypeFlags.Index) { - const typeVariable = getActualTypeVariable((constraintType).type); - if (typeVariable.flags & TypeFlags.TypeParameter) { - return typeVariable; - } - } - return undefined; - } - - function instantiateMappedType(type: MappedType, mapper: TypeMapper): Type { - // For a homomorphic mapped type { [P in keyof T]: X }, where T is some type variable, the mapping - // operation depends on T as follows: - // * If T is a primitive type no mapping is performed and the result is simply T. - // * If T is a union type we distribute the mapped type over the union. - // * If T is an array we map to an array where the element type has been transformed. - // * If T is a tuple we map to a tuple where the element types have been transformed. - // * Otherwise we map to an object type where the type of each property has been transformed. - // For example, when T is instantiated to a union type A | B, we produce { [P in keyof A]: X } | - // { [P in keyof B]: X }, and when when T is instantiated to a union type A | undefined, we produce - // { [P in keyof A]: X } | undefined. - const typeVariable = getHomomorphicTypeVariable(type); - if (typeVariable) { - const mappedTypeVariable = instantiateType(typeVariable, mapper); - if (typeVariable !== mappedTypeVariable) { - return mapType(mappedTypeVariable, t => { - if (t.flags & (TypeFlags.AnyOrUnknown | TypeFlags.InstantiableNonPrimitive | TypeFlags.Object | TypeFlags.Intersection) && t !== wildcardType && t !== errorType) { - const replacementMapper = createReplacementMapper(typeVariable, t, mapper); - return isArrayType(t) ? instantiateMappedArrayType(t, type, replacementMapper) : - isTupleType(t) ? instantiateMappedTupleType(t, type, replacementMapper) : - instantiateAnonymousType(type, replacementMapper); - } - return t; - }); - } - } - return instantiateAnonymousType(type, mapper); - } - - function getModifiedReadonlyState(state: boolean, modifiers: MappedTypeModifiers) { - return modifiers & MappedTypeModifiers.IncludeReadonly ? true : modifiers & MappedTypeModifiers.ExcludeReadonly ? false : state; - } - - function instantiateMappedArrayType(arrayType: Type, mappedType: MappedType, mapper: TypeMapper) { - const elementType = instantiateMappedTypeTemplate(mappedType, numberType, /*isOptional*/ true, mapper); - return elementType === errorType ? errorType : - createArrayType(elementType, getModifiedReadonlyState(isReadonlyArrayType(arrayType), getMappedTypeModifiers(mappedType))); - } - - function instantiateMappedTupleType(tupleType: TupleTypeReference, mappedType: MappedType, mapper: TypeMapper) { - const minLength = tupleType.target.minLength; - const elementTypes = map(getTypeArguments(tupleType), (_, i) => - instantiateMappedTypeTemplate(mappedType, getLiteralType("" + i), i >= minLength, mapper)); - const modifiers = getMappedTypeModifiers(mappedType); - const newMinLength = modifiers & MappedTypeModifiers.IncludeOptional ? 0 : - modifiers & MappedTypeModifiers.ExcludeOptional ? getTypeReferenceArity(tupleType) - (tupleType.target.hasRestElement ? 1 : 0) : - minLength; - const newReadonly = getModifiedReadonlyState(tupleType.target.readonly, modifiers); - return contains(elementTypes, errorType) ? errorType : - createTupleType(elementTypes, newMinLength, tupleType.target.hasRestElement, newReadonly, tupleType.target.associatedNames); - } - - function instantiateMappedTypeTemplate(type: MappedType, key: Type, isOptional: boolean, mapper: TypeMapper) { - const templateMapper = combineTypeMappers(mapper, createTypeMapper([getTypeParameterFromMappedType(type)], [key])); - const propType = instantiateType(getTemplateTypeFromMappedType(type.target || type), templateMapper); - const modifiers = getMappedTypeModifiers(type); - return strictNullChecks && modifiers & MappedTypeModifiers.IncludeOptional && !maybeTypeOfKind(propType, TypeFlags.Undefined | TypeFlags.Void) ? getOptionalType(propType) : - strictNullChecks && modifiers & MappedTypeModifiers.ExcludeOptional && isOptional ? getTypeWithFacts(propType, TypeFacts.NEUndefined) : - propType; - } - - function instantiateAnonymousType(type: AnonymousType, mapper: TypeMapper): AnonymousType { - const result = createObjectType(type.objectFlags | ObjectFlags.Instantiated, type.symbol); - if (type.objectFlags & ObjectFlags.Mapped) { - (result).declaration = (type).declaration; - // C.f. instantiateSignature - const origTypeParameter = getTypeParameterFromMappedType(type); - const freshTypeParameter = cloneTypeParameter(origTypeParameter); - (result).typeParameter = freshTypeParameter; - mapper = combineTypeMappers(makeUnaryTypeMapper(origTypeParameter, freshTypeParameter), mapper); - freshTypeParameter.mapper = mapper; - } - result.target = type; - result.mapper = mapper; - result.aliasSymbol = type.aliasSymbol; - result.aliasTypeArguments = instantiateTypes(type.aliasTypeArguments, mapper); - return result; - } - - function getConditionalTypeInstantiation(type: ConditionalType, mapper: TypeMapper): Type { - const root = type.root; - if (root.outerTypeParameters) { - // We are instantiating a conditional type that has one or more type parameters in scope. Apply the - // mapper to the type parameters to produce the effective list of type arguments, and compute the - // instantiation cache key from the type IDs of the type arguments. - const typeArguments = map(root.outerTypeParameters, mapper); - const id = getTypeListId(typeArguments); - let result = root.instantiations!.get(id); - if (!result) { - const newMapper = createTypeMapper(root.outerTypeParameters, typeArguments); - result = instantiateConditionalType(root, newMapper); - root.instantiations!.set(id, result); - } - return result; - } - return type; - } - - function instantiateConditionalType(root: ConditionalRoot, mapper: TypeMapper): Type { - // Check if we have a conditional type where the check type is a naked type parameter. If so, - // the conditional type is distributive over union types and when T is instantiated to a union - // type A | B, we produce (A extends U ? X : Y) | (B extends U ? X : Y). - if (root.isDistributive) { - const checkType = root.checkType; - const instantiatedType = mapper(checkType); - if (checkType !== instantiatedType && instantiatedType.flags & (TypeFlags.Union | TypeFlags.Never)) { - return mapType(instantiatedType, t => getConditionalType(root, createReplacementMapper(checkType, t, mapper))); - } - } - return getConditionalType(root, mapper); - } - - function instantiateType(type: Type, mapper: TypeMapper | undefined): Type; - function instantiateType(type: Type | undefined, mapper: TypeMapper | undefined): Type | undefined; - function instantiateType(type: Type | undefined, mapper: TypeMapper | undefined): Type | undefined { - if (!type || !mapper || mapper === identityMapper) { - return type; - } - if (instantiationDepth === 50 || instantiationCount >= 5000000) { - // We have reached 50 recursive type instantiations and there is a very high likelyhood we're dealing - // with a combination of infinite generic types that perpetually generate new type identities. We stop - // the recursion here by yielding the error type. - error(currentNode, Diagnostics.Type_instantiation_is_excessively_deep_and_possibly_infinite); - return errorType; - } - totalInstantiationCount++; - instantiationCount++; - instantiationDepth++; - const result = instantiateTypeWorker(type, mapper); - instantiationDepth--; - return result; - } - - function instantiateTypeWorker(type: Type, mapper: TypeMapper): Type { - const flags = type.flags; - if (flags & TypeFlags.TypeParameter) { - return mapper(type); - } - if (flags & TypeFlags.Object) { - const objectFlags = (type).objectFlags; - if (objectFlags & ObjectFlags.Anonymous) { - // If the anonymous type originates in a declaration of a function, method, class, or - // interface, in an object type literal, or in an object literal expression, we may need - // to instantiate the type because it might reference a type parameter. - return couldContainTypeVariables(type) ? - getObjectTypeInstantiation(type, mapper) : type; - } - if (objectFlags & ObjectFlags.Mapped) { - return getObjectTypeInstantiation(type, mapper); - } - if (objectFlags & ObjectFlags.Reference) { - if ((type).node) { - return getObjectTypeInstantiation(type, mapper); - } - const resolvedTypeArguments = (type).resolvedTypeArguments; - const newTypeArguments = instantiateTypes(resolvedTypeArguments, mapper); - return newTypeArguments !== resolvedTypeArguments ? createTypeReference((type).target, newTypeArguments) : type; - } - return type; - } - if ((flags & TypeFlags.Intersection) || (flags & TypeFlags.Union && !(flags & TypeFlags.Primitive))) { - if (!couldContainTypeVariables(type)) { - return type; - } - const types = (type).types; - const newTypes = instantiateTypes(types, mapper); - return newTypes === types - ? type - : (flags & TypeFlags.Intersection) - ? getIntersectionType(newTypes, type.aliasSymbol, instantiateTypes(type.aliasTypeArguments, mapper)) - : getUnionType(newTypes, UnionReduction.Literal, type.aliasSymbol, instantiateTypes(type.aliasTypeArguments, mapper)); - } - if (flags & TypeFlags.Index) { - return getIndexType(instantiateType((type).type, mapper)); - } - if (flags & TypeFlags.IndexedAccess) { - return getIndexedAccessType(instantiateType((type).objectType, mapper), instantiateType((type).indexType, mapper)); - } - if (flags & TypeFlags.Conditional) { - return getConditionalTypeInstantiation(type, combineTypeMappers((type).mapper, mapper)); - } - if (flags & TypeFlags.Substitution) { - const maybeVariable = instantiateType((type).typeVariable, mapper); - if (maybeVariable.flags & TypeFlags.TypeVariable) { - return getSubstitutionType(maybeVariable as TypeVariable, instantiateType((type).substitute, mapper)); - } - else { - const sub = instantiateType((type).substitute, mapper); - if (sub.flags & TypeFlags.AnyOrUnknown || isTypeAssignableTo(getRestrictiveInstantiation(maybeVariable), getRestrictiveInstantiation(sub))) { - return maybeVariable; - } - return sub; - } - } - return type; - } - - function getPermissiveInstantiation(type: Type) { - return type.flags & (TypeFlags.Primitive | TypeFlags.AnyOrUnknown | TypeFlags.Never) ? type : - type.permissiveInstantiation || (type.permissiveInstantiation = instantiateType(type, permissiveMapper)); - } - - function getRestrictiveInstantiation(type: Type) { - if (type.flags & (TypeFlags.Primitive | TypeFlags.AnyOrUnknown | TypeFlags.Never)) { - return type; - } - if (type.restrictiveInstantiation) { - return type.restrictiveInstantiation; - } - type.restrictiveInstantiation = instantiateType(type, restrictiveMapper); - // We set the following so we don't attempt to set the restrictive instance of a restrictive instance - // which is redundant - we'll produce new type identities, but all type params have already been mapped. - // This also gives us a way to detect restrictive instances upon comparisons and _disable_ the "distributeive constraint" - // assignability check for them, which is distinctly unsafe, as once you have a restrctive instance, all the type parameters - // are constrained to `unknown` and produce tons of false positives/negatives! - type.restrictiveInstantiation.restrictiveInstantiation = type.restrictiveInstantiation; - return type.restrictiveInstantiation; - } - - function instantiateIndexInfo(info: IndexInfo | undefined, mapper: TypeMapper): IndexInfo | undefined { - return info && createIndexInfo(instantiateType(info.type, mapper), info.isReadonly, info.declaration); - } - - // Returns true if the given expression contains (at any level of nesting) a function or arrow expression - // that is subject to contextual typing. - function isContextSensitive(node: Expression | MethodDeclaration | ObjectLiteralElementLike | JsxAttributeLike | JsxChild): boolean { - Debug.assert(node.kind !== SyntaxKind.MethodDeclaration || isObjectLiteralMethod(node)); - switch (node.kind) { - case SyntaxKind.FunctionExpression: - case SyntaxKind.ArrowFunction: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.FunctionDeclaration: // Function declarations can have context when annotated with a jsdoc @type - return isContextSensitiveFunctionLikeDeclaration(node); - case SyntaxKind.ObjectLiteralExpression: - return some((node).properties, isContextSensitive); - case SyntaxKind.ArrayLiteralExpression: - return some((node).elements, isContextSensitive); - case SyntaxKind.ConditionalExpression: - return isContextSensitive((node).whenTrue) || - isContextSensitive((node).whenFalse); - case SyntaxKind.BinaryExpression: - return ((node).operatorToken.kind === SyntaxKind.BarBarToken || (node).operatorToken.kind === SyntaxKind.QuestionQuestionToken) && - (isContextSensitive((node).left) || isContextSensitive((node).right)); - case SyntaxKind.PropertyAssignment: - return isContextSensitive((node).initializer); - case SyntaxKind.ParenthesizedExpression: - return isContextSensitive((node).expression); - case SyntaxKind.JsxAttributes: - return some((node).properties, isContextSensitive) || isJsxOpeningElement(node.parent) && some(node.parent.parent.children, isContextSensitive); - case SyntaxKind.JsxAttribute: { - // If there is no initializer, JSX attribute has a boolean value of true which is not context sensitive. - const { initializer } = node as JsxAttribute; - return !!initializer && isContextSensitive(initializer); - } - case SyntaxKind.JsxExpression: { - // It is possible to that node.expression is undefined (e.g
) - const { expression } = node as JsxExpression; - return !!expression && isContextSensitive(expression); - } - } - - return false; - } - - function isContextSensitiveFunctionLikeDeclaration(node: FunctionLikeDeclaration): boolean { - return (!isFunctionDeclaration(node) || isInJSFile(node) && !!getTypeForDeclarationFromJSDocComment(node)) && - (hasContextSensitiveParameters(node) || hasContextSensitiveReturnExpression(node)); - } - - function hasContextSensitiveParameters(node: FunctionLikeDeclaration) { - // Functions with type parameters are not context sensitive. - if (!node.typeParameters) { - // Functions with any parameters that lack type annotations are context sensitive. - if (some(node.parameters, p => !getEffectiveTypeAnnotationNode(p))) { - return true; - } - if (node.kind !== SyntaxKind.ArrowFunction) { - // If the first parameter is not an explicit 'this' parameter, then the function has - // an implicit 'this' parameter which is subject to contextual typing. - const parameter = firstOrUndefined(node.parameters); - if (!(parameter && parameterIsThisKeyword(parameter))) { - return true; - } - } - } - return false; - } - - function hasContextSensitiveReturnExpression(node: FunctionLikeDeclaration) { - // TODO(anhans): A block should be context-sensitive if it has a context-sensitive return value. - return !getEffectiveReturnTypeNode(node) && !!node.body && node.body.kind !== SyntaxKind.Block && isContextSensitive(node.body); - } - - function isContextSensitiveFunctionOrObjectLiteralMethod(func: Node): func is FunctionExpression | ArrowFunction | MethodDeclaration { - return (isInJSFile(func) && isFunctionDeclaration(func) || isFunctionExpressionOrArrowFunction(func) || isObjectLiteralMethod(func)) && - isContextSensitiveFunctionLikeDeclaration(func); - } - - function getTypeWithoutSignatures(type: Type): Type { - if (type.flags & TypeFlags.Object) { - const resolved = resolveStructuredTypeMembers(type); - if (resolved.constructSignatures.length || resolved.callSignatures.length) { - const result = createObjectType(ObjectFlags.Anonymous, type.symbol); - result.members = resolved.members; - result.properties = resolved.properties; - result.callSignatures = emptyArray; - result.constructSignatures = emptyArray; - return result; - } - } - else if (type.flags & TypeFlags.Intersection) { - return getIntersectionType(map((type).types, getTypeWithoutSignatures)); - } - return type; - } - - // TYPE CHECKING - - function isTypeIdenticalTo(source: Type, target: Type): boolean { - return isTypeRelatedTo(source, target, identityRelation); - } - - function compareTypesIdentical(source: Type, target: Type): Ternary { - return isTypeRelatedTo(source, target, identityRelation) ? Ternary.True : Ternary.False; - } - - function compareTypesAssignable(source: Type, target: Type): Ternary { - return isTypeRelatedTo(source, target, assignableRelation) ? Ternary.True : Ternary.False; - } - - function compareTypesSubtypeOf(source: Type, target: Type): Ternary { - return isTypeRelatedTo(source, target, subtypeRelation) ? Ternary.True : Ternary.False; - } - - function isTypeSubtypeOf(source: Type, target: Type): boolean { - return isTypeRelatedTo(source, target, subtypeRelation); - } - - function isTypeAssignableTo(source: Type, target: Type): boolean { - return isTypeRelatedTo(source, target, assignableRelation); - } - - // An object type S is considered to be derived from an object type T if - // S is a union type and every constituent of S is derived from T, - // T is a union type and S is derived from at least one constituent of T, or - // S is a type variable with a base constraint that is derived from T, - // T is one of the global types Object and Function and S is a subtype of T, or - // T occurs directly or indirectly in an 'extends' clause of S. - // Note that this check ignores type parameters and only considers the - // inheritance hierarchy. - function isTypeDerivedFrom(source: Type, target: Type): boolean { - return source.flags & TypeFlags.Union ? every((source).types, t => isTypeDerivedFrom(t, target)) : - target.flags & TypeFlags.Union ? some((target).types, t => isTypeDerivedFrom(source, t)) : - source.flags & TypeFlags.InstantiableNonPrimitive ? isTypeDerivedFrom(getBaseConstraintOfType(source) || unknownType, target) : - target === globalObjectType ? !!(source.flags & (TypeFlags.Object | TypeFlags.NonPrimitive)) : - target === globalFunctionType ? !!(source.flags & TypeFlags.Object) && isFunctionObjectType(source as ObjectType) : - hasBaseType(source, getTargetType(target)); - } - - /** - * This is *not* a bi-directional relationship. - * If one needs to check both directions for comparability, use a second call to this function or 'checkTypeComparableTo'. - * - * A type S is comparable to a type T if some (but not necessarily all) of the possible values of S are also possible values of T. - * It is used to check following cases: - * - the types of the left and right sides of equality/inequality operators (`===`, `!==`, `==`, `!=`). - * - the types of `case` clause expressions and their respective `switch` expressions. - * - the type of an expression in a type assertion with the type being asserted. - */ - function isTypeComparableTo(source: Type, target: Type): boolean { - return isTypeRelatedTo(source, target, comparableRelation); - } - - function areTypesComparable(type1: Type, type2: Type): boolean { - return isTypeComparableTo(type1, type2) || isTypeComparableTo(type2, type1); - } - - function checkTypeAssignableTo(source: Type, target: Type, errorNode: Node | undefined, headMessage?: DiagnosticMessage, containingMessageChain?: () => DiagnosticMessageChain | undefined, errorOutputObject?: { errors?: Diagnostic[] }): boolean { - return checkTypeRelatedTo(source, target, assignableRelation, errorNode, headMessage, containingMessageChain, errorOutputObject); - } - - /** - * Like `checkTypeAssignableTo`, but if it would issue an error, instead performs structural comparisons of the types using the given expression node to - * attempt to issue more specific errors on, for example, specific object literal properties or tuple members. - */ - function checkTypeAssignableToAndOptionallyElaborate(source: Type, target: Type, errorNode: Node | undefined, expr: Expression | undefined, headMessage?: DiagnosticMessage, containingMessageChain?: () => DiagnosticMessageChain | undefined): boolean { - return checkTypeRelatedToAndOptionallyElaborate(source, target, assignableRelation, errorNode, expr, headMessage, containingMessageChain, /*errorOutputContainer*/ undefined); - } - - function checkTypeRelatedToAndOptionallyElaborate( - source: Type, - target: Type, - relation: Map, - errorNode: Node | undefined, - expr: Expression | undefined, - headMessage: DiagnosticMessage | undefined, - containingMessageChain: (() => DiagnosticMessageChain | undefined) | undefined, - errorOutputContainer: { errors?: Diagnostic[], skipLogging?: boolean } | undefined - ): boolean { - if (isTypeRelatedTo(source, target, relation)) return true; - if (!errorNode || !elaborateError(expr, source, target, relation, headMessage, containingMessageChain, errorOutputContainer)) { - return checkTypeRelatedTo(source, target, relation, errorNode, headMessage, containingMessageChain, errorOutputContainer); - } - return false; - } - - function isOrHasGenericConditional(type: Type): boolean { - return !!(type.flags & TypeFlags.Conditional || (type.flags & TypeFlags.Intersection && some((type as IntersectionType).types, isOrHasGenericConditional))); - } - - function elaborateError( - node: Expression | undefined, - source: Type, - target: Type, - relation: Map, - headMessage: DiagnosticMessage | undefined, - containingMessageChain: (() => DiagnosticMessageChain | undefined) | undefined, - errorOutputContainer: { errors?: Diagnostic[], skipLogging?: boolean } | undefined - ): boolean { - if (!node || isOrHasGenericConditional(target)) return false; - if (!checkTypeRelatedTo(source, target, relation, /*errorNode*/ undefined) - && elaborateDidYouMeanToCallOrConstruct(node, source, target, relation, headMessage, containingMessageChain, errorOutputContainer)) { - return true; - } - switch (node.kind) { - case SyntaxKind.JsxExpression: - case SyntaxKind.ParenthesizedExpression: - return elaborateError((node as ParenthesizedExpression | JsxExpression).expression, source, target, relation, headMessage, containingMessageChain, errorOutputContainer); - case SyntaxKind.BinaryExpression: - switch ((node as BinaryExpression).operatorToken.kind) { - case SyntaxKind.EqualsToken: - case SyntaxKind.CommaToken: - return elaborateError((node as BinaryExpression).right, source, target, relation, headMessage, containingMessageChain, errorOutputContainer); - } - break; - case SyntaxKind.ObjectLiteralExpression: - return elaborateObjectLiteral(node as ObjectLiteralExpression, source, target, relation, containingMessageChain, errorOutputContainer); - case SyntaxKind.ArrayLiteralExpression: - return elaborateArrayLiteral(node as ArrayLiteralExpression, source, target, relation, containingMessageChain, errorOutputContainer); - case SyntaxKind.JsxAttributes: - return elaborateJsxComponents(node as JsxAttributes, source, target, relation, containingMessageChain, errorOutputContainer); - case SyntaxKind.ArrowFunction: - return elaborateArrowFunction(node as ArrowFunction, source, target, relation, containingMessageChain, errorOutputContainer); - } - return false; - } - - function elaborateDidYouMeanToCallOrConstruct( - node: Expression, - source: Type, - target: Type, - relation: Map, - headMessage: DiagnosticMessage | undefined, - containingMessageChain: (() => DiagnosticMessageChain | undefined) | undefined, - errorOutputContainer: { errors?: Diagnostic[], skipLogging?: boolean } | undefined - ): boolean { - const callSignatures = getSignaturesOfType(source, SignatureKind.Call); - const constructSignatures = getSignaturesOfType(source, SignatureKind.Construct); - for (const signatures of [constructSignatures, callSignatures]) { - if (some(signatures, s => { - const returnType = getReturnTypeOfSignature(s); - return !(returnType.flags & (TypeFlags.Any | TypeFlags.Never)) && checkTypeRelatedTo(returnType, target, relation, /*errorNode*/ undefined); - })) { - const resultObj: { errors?: Diagnostic[] } = errorOutputContainer || {}; - checkTypeAssignableTo(source, target, node, headMessage, containingMessageChain, resultObj); - const diagnostic = resultObj.errors![resultObj.errors!.length - 1]; - addRelatedInfo(diagnostic, createDiagnosticForNode( - node, - signatures === constructSignatures ? Diagnostics.Did_you_mean_to_use_new_with_this_expression : Diagnostics.Did_you_mean_to_call_this_expression - )); - return true; - } - } - return false; - } - - function elaborateArrowFunction( - node: ArrowFunction, - source: Type, - target: Type, - relation: Map, - containingMessageChain: (() => DiagnosticMessageChain | undefined) | undefined, - errorOutputContainer: { errors?: Diagnostic[], skipLogging?: boolean } | undefined - ): boolean { - // Don't elaborate blocks - if (isBlock(node.body)) { - return false; - } - // Or functions with annotated parameter types - if (some(node.parameters, ts.hasType)) { - return false; - } - const sourceSig = getSingleCallSignature(source); - if (!sourceSig) { - return false; - } - const targetSignatures = getSignaturesOfType(target, SignatureKind.Call); - if (!length(targetSignatures)) { - return false; - } - const returnExpression = node.body; - const sourceReturn = getReturnTypeOfSignature(sourceSig); - const targetReturn = getUnionType(map(targetSignatures, getReturnTypeOfSignature)); - if (!checkTypeRelatedTo(sourceReturn, targetReturn, relation, /*errorNode*/ undefined)) { - const elaborated = returnExpression && elaborateError(returnExpression, sourceReturn, targetReturn, relation, /*headMessage*/ undefined, containingMessageChain, errorOutputContainer); - if (elaborated) { - return elaborated; - } - const resultObj: { errors?: Diagnostic[] } = errorOutputContainer || {}; - checkTypeRelatedTo(sourceReturn, targetReturn, relation, returnExpression, /*message*/ undefined, containingMessageChain, resultObj); - if (resultObj.errors) { - if (target.symbol && length(target.symbol.declarations)) { - addRelatedInfo(resultObj.errors[resultObj.errors.length - 1], createDiagnosticForNode( - target.symbol.declarations[0], - Diagnostics.The_expected_type_comes_from_the_return_type_of_this_signature, - )); - } - if ((getFunctionFlags(node) & FunctionFlags.Async) === 0 - // exclude cases where source itself is promisy - this way we don't make a suggestion when relating - // an IPromise and a Promise that are slightly different - && !getTypeOfPropertyOfType(sourceReturn, "then" as __String) - && checkTypeRelatedTo(createPromiseType(sourceReturn), targetReturn, relation, /*errorNode*/ undefined) - ) { - addRelatedInfo(resultObj.errors[resultObj.errors.length - 1], createDiagnosticForNode( - node, - Diagnostics.Did_you_mean_to_mark_this_function_as_async - )); - } - return true; - } - } - return false; - } - - function getBestMatchIndexedAccessTypeOrUndefined(source: Type, target: Type, nameType: Type) { - const idx = getIndexedAccessTypeOrUndefined(target, nameType); - if (idx) { - return idx; - } - if (target.flags & TypeFlags.Union) { - const best = getBestMatchingType(source, target as UnionType); - if (best) { - return getIndexedAccessTypeOrUndefined(best, nameType); - } - } - } - - type ElaborationIterator = IterableIterator<{ errorNode: Node, innerExpression: Expression | undefined, nameType: Type, errorMessage?: DiagnosticMessage | undefined }>; - /** - * For every element returned from the iterator, checks that element to issue an error on a property of that element's type - * If that element would issue an error, we first attempt to dive into that element's inner expression and issue a more specific error by recuring into `elaborateError` - * Otherwise, we issue an error on _every_ element which fail the assignability check - */ - function elaborateElementwise( - iterator: ElaborationIterator, - source: Type, - target: Type, - relation: Map, - containingMessageChain: (() => DiagnosticMessageChain | undefined) | undefined, - errorOutputContainer: { errors?: Diagnostic[], skipLogging?: boolean } | undefined - ) { - // Assignability failure - check each prop individually, and if that fails, fall back on the bad error span - let reportedError = false; - for (let status = iterator.next(); !status.done; status = iterator.next()) { - const { errorNode: prop, innerExpression: next, nameType, errorMessage } = status.value; - const targetPropType = getBestMatchIndexedAccessTypeOrUndefined(source, target, nameType); - if (!targetPropType || targetPropType.flags & TypeFlags.IndexedAccess) continue; // Don't elaborate on indexes on generic variables - const sourcePropType = getIndexedAccessTypeOrUndefined(source, nameType); - if (sourcePropType && !checkTypeRelatedTo(sourcePropType, targetPropType, relation, /*errorNode*/ undefined)) { - const elaborated = next && elaborateError(next, sourcePropType, targetPropType, relation, /*headMessage*/ undefined, containingMessageChain, errorOutputContainer); - if (elaborated) { - reportedError = true; - } - else { - // Issue error on the prop itself, since the prop couldn't elaborate the error - const resultObj: { errors?: Diagnostic[] } = errorOutputContainer || {}; - // Use the expression type, if available - const specificSource = next ? checkExpressionForMutableLocation(next, CheckMode.Normal, sourcePropType) : sourcePropType; - const result = checkTypeRelatedTo(specificSource, targetPropType, relation, prop, errorMessage, containingMessageChain, resultObj); - if (result && specificSource !== sourcePropType) { - // If for whatever reason the expression type doesn't yield an error, make sure we still issue an error on the sourcePropType - checkTypeRelatedTo(sourcePropType, targetPropType, relation, prop, errorMessage, containingMessageChain, resultObj); - } - if (resultObj.errors) { - const reportedDiag = resultObj.errors[resultObj.errors.length - 1]; - const propertyName = isTypeUsableAsPropertyName(nameType) ? getPropertyNameFromType(nameType) : undefined; - const targetProp = propertyName !== undefined ? getPropertyOfType(target, propertyName) : undefined; - - let issuedElaboration = false; - if (!targetProp) { - const indexInfo = isTypeAssignableToKind(nameType, TypeFlags.NumberLike) && getIndexInfoOfType(target, IndexKind.Number) || - getIndexInfoOfType(target, IndexKind.String) || - undefined; - if (indexInfo && indexInfo.declaration && !getSourceFileOfNode(indexInfo.declaration).hasNoDefaultLib) { - issuedElaboration = true; - addRelatedInfo(reportedDiag, createDiagnosticForNode(indexInfo.declaration, Diagnostics.The_expected_type_comes_from_this_index_signature)); - } - } - - if (!issuedElaboration && (targetProp && length(targetProp.declarations) || target.symbol && length(target.symbol.declarations))) { - const targetNode = targetProp && length(targetProp.declarations) ? targetProp.declarations[0] : target.symbol.declarations[0]; - if (!getSourceFileOfNode(targetNode).hasNoDefaultLib) { - addRelatedInfo(reportedDiag, createDiagnosticForNode( - targetNode, - Diagnostics.The_expected_type_comes_from_property_0_which_is_declared_here_on_type_1, - propertyName && !(nameType.flags & TypeFlags.UniqueESSymbol) ? unescapeLeadingUnderscores(propertyName) : typeToString(nameType), - typeToString(target) - )); - } - } - } - reportedError = true; - } - } - } - return reportedError; - } - - function *generateJsxAttributes(node: JsxAttributes): ElaborationIterator { - if (!length(node.properties)) return; - for (const prop of node.properties) { - if (isJsxSpreadAttribute(prop)) continue; - yield { errorNode: prop.name, innerExpression: prop.initializer, nameType: getLiteralType(idText(prop.name)) }; - } - } - - function *generateJsxChildren(node: JsxElement, getInvalidTextDiagnostic: () => DiagnosticMessage): ElaborationIterator { - if (!length(node.children)) return; - let memberOffset = 0; - for (let i = 0; i < node.children.length; i++) { - const child = node.children[i]; - const nameType = getLiteralType(i - memberOffset); - const elem = getElaborationElementForJsxChild(child, nameType, getInvalidTextDiagnostic); - if (elem) { - yield elem; - } - else { - memberOffset++; - } - } - } - - function getElaborationElementForJsxChild(child: JsxChild, nameType: LiteralType, getInvalidTextDiagnostic: () => DiagnosticMessage) { - switch (child.kind) { - case SyntaxKind.JsxExpression: - // child is of the type of the expression - return { errorNode: child, innerExpression: child.expression, nameType }; - case SyntaxKind.JsxText: - if (child.containsOnlyTriviaWhiteSpaces) { - break; // Whitespace only jsx text isn't real jsx text - } - // child is a string - return { errorNode: child, innerExpression: undefined, nameType, errorMessage: getInvalidTextDiagnostic() }; - case SyntaxKind.JsxElement: - case SyntaxKind.JsxSelfClosingElement: - case SyntaxKind.JsxFragment: - // child is of type JSX.Element - return { errorNode: child, innerExpression: child, nameType }; - default: - return Debug.assertNever(child, "Found invalid jsx child"); - } - } - - function getSemanticJsxChildren(children: NodeArray) { - return filter(children, i => !isJsxText(i) || !i.containsOnlyTriviaWhiteSpaces); - } - - function elaborateJsxComponents( - node: JsxAttributes, - source: Type, - target: Type, - relation: Map, - containingMessageChain: (() => DiagnosticMessageChain | undefined) | undefined, - errorOutputContainer: { errors?: Diagnostic[], skipLogging?: boolean } | undefined - ) { - let result = elaborateElementwise(generateJsxAttributes(node), source, target, relation, containingMessageChain, errorOutputContainer); - let invalidTextDiagnostic: DiagnosticMessage | undefined; - if (isJsxOpeningElement(node.parent) && isJsxElement(node.parent.parent)) { - const containingElement = node.parent.parent; - const childPropName = getJsxElementChildrenPropertyName(getJsxNamespaceAt(node)); - const childrenPropName = childPropName === undefined ? "children" : unescapeLeadingUnderscores(childPropName); - const childrenNameType = getLiteralType(childrenPropName); - const childrenTargetType = getIndexedAccessType(target, childrenNameType); - const validChildren = getSemanticJsxChildren(containingElement.children); - if (!length(validChildren)) { - return result; - } - const moreThanOneRealChildren = length(validChildren) > 1; - const arrayLikeTargetParts = filterType(childrenTargetType, isArrayOrTupleLikeType); - const nonArrayLikeTargetParts = filterType(childrenTargetType, t => !isArrayOrTupleLikeType(t)); - if (moreThanOneRealChildren) { - if (arrayLikeTargetParts !== neverType) { - const realSource = createTupleType(checkJsxChildren(containingElement, CheckMode.Normal)); - const children = generateJsxChildren(containingElement, getInvalidTextualChildDiagnostic); - result = elaborateElementwise(children, realSource, arrayLikeTargetParts, relation, containingMessageChain, errorOutputContainer) || result; - } - else if (!isTypeRelatedTo(getIndexedAccessType(source, childrenNameType), childrenTargetType, relation)) { - // arity mismatch - result = true; - const diag = error( - containingElement.openingElement.tagName, - Diagnostics.This_JSX_tag_s_0_prop_expects_a_single_child_of_type_1_but_multiple_children_were_provided, - childrenPropName, - typeToString(childrenTargetType) - ); - if (errorOutputContainer && errorOutputContainer.skipLogging) { - (errorOutputContainer.errors || (errorOutputContainer.errors = [])).push(diag); - } - } - } - else { - if (nonArrayLikeTargetParts !== neverType) { - const child = validChildren[0]; - const elem = getElaborationElementForJsxChild(child, childrenNameType, getInvalidTextualChildDiagnostic); - if (elem) { - result = elaborateElementwise( - (function*() { yield elem; })(), - source, - target, - relation, - containingMessageChain, - errorOutputContainer - ) || result; - } - } - else if (!isTypeRelatedTo(getIndexedAccessType(source, childrenNameType), childrenTargetType, relation)) { - // arity mismatch - result = true; - const diag = error( - containingElement.openingElement.tagName, - Diagnostics.This_JSX_tag_s_0_prop_expects_type_1_which_requires_multiple_children_but_only_a_single_child_was_provided, - childrenPropName, - typeToString(childrenTargetType) - ); - if (errorOutputContainer && errorOutputContainer.skipLogging) { - (errorOutputContainer.errors || (errorOutputContainer.errors = [])).push(diag); - } - } - } - } - return result; - - function getInvalidTextualChildDiagnostic() { - if (!invalidTextDiagnostic) { - const tagNameText = getTextOfNode(node.parent.tagName); - const childPropName = getJsxElementChildrenPropertyName(getJsxNamespaceAt(node)); - const childrenPropName = childPropName === undefined ? "children" : unescapeLeadingUnderscores(childPropName); - const childrenTargetType = getIndexedAccessType(target, getLiteralType(childrenPropName)); - const diagnostic = Diagnostics._0_components_don_t_accept_text_as_child_elements_Text_in_JSX_has_the_type_string_but_the_expected_type_of_1_is_2; - invalidTextDiagnostic = { ...diagnostic, key: "!!ALREADY FORMATTED!!", message: formatMessage(/*_dummy*/ undefined, diagnostic, tagNameText, childrenPropName, typeToString(childrenTargetType)) }; - } - return invalidTextDiagnostic; - } - } - - function *generateLimitedTupleElements(node: ArrayLiteralExpression, target: Type): ElaborationIterator { - const len = length(node.elements); - if (!len) return; - for (let i = 0; i < len; i++) { - // Skip elements which do not exist in the target - a length error on the tuple overall is likely better than an error on a mismatched index signature - if (isTupleLikeType(target) && !getPropertyOfType(target, ("" + i) as __String)) continue; - const elem = node.elements[i]; - if (isOmittedExpression(elem)) continue; - const nameType = getLiteralType(i); - yield { errorNode: elem, innerExpression: elem, nameType }; - } - } - - function elaborateArrayLiteral( - node: ArrayLiteralExpression, - source: Type, - target: Type, - relation: Map, - containingMessageChain: (() => DiagnosticMessageChain | undefined) | undefined, - errorOutputContainer: { errors?: Diagnostic[], skipLogging?: boolean } | undefined - ) { - if (target.flags & TypeFlags.Primitive) return false; - if (isTupleLikeType(source)) { - return elaborateElementwise(generateLimitedTupleElements(node, target), source, target, relation, containingMessageChain, errorOutputContainer); - } - // recreate a tuple from the elements, if possible - // Since we're re-doing the expression type, we need to reapply the contextual type - const oldContext = node.contextualType; - node.contextualType = target; - try { - const tupleizedType = checkArrayLiteral(node, CheckMode.Contextual, /*forceTuple*/ true); - node.contextualType = oldContext; - if (isTupleLikeType(tupleizedType)) { - return elaborateElementwise(generateLimitedTupleElements(node, target), tupleizedType, target, relation, containingMessageChain, errorOutputContainer); - } - return false; - } - finally { - node.contextualType = oldContext; - } - } - - function *generateObjectLiteralElements(node: ObjectLiteralExpression): ElaborationIterator { - if (!length(node.properties)) return; - for (const prop of node.properties) { - if (isSpreadAssignment(prop)) continue; - const type = getLiteralTypeFromProperty(getSymbolOfNode(prop), TypeFlags.StringOrNumberLiteralOrUnique); - if (!type || (type.flags & TypeFlags.Never)) { - continue; - } - switch (prop.kind) { - case SyntaxKind.SetAccessor: - case SyntaxKind.GetAccessor: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.ShorthandPropertyAssignment: - yield { errorNode: prop.name, innerExpression: undefined, nameType: type }; - break; - case SyntaxKind.PropertyAssignment: - yield { errorNode: prop.name, innerExpression: prop.initializer, nameType: type, errorMessage: isComputedNonLiteralName(prop.name) ? Diagnostics.Type_of_computed_property_s_value_is_0_which_is_not_assignable_to_type_1 : undefined }; - break; - default: - Debug.assertNever(prop); - } - } - } - - function elaborateObjectLiteral( - node: ObjectLiteralExpression, - source: Type, - target: Type, - relation: Map, - containingMessageChain: (() => DiagnosticMessageChain | undefined) | undefined, - errorOutputContainer: { errors?: Diagnostic[], skipLogging?: boolean } | undefined - ) { - if (target.flags & TypeFlags.Primitive) return false; - return elaborateElementwise(generateObjectLiteralElements(node), source, target, relation, containingMessageChain, errorOutputContainer); - } - - /** - * This is *not* a bi-directional relationship. - * If one needs to check both directions for comparability, use a second call to this function or 'isTypeComparableTo'. - */ - function checkTypeComparableTo(source: Type, target: Type, errorNode: Node, headMessage?: DiagnosticMessage, containingMessageChain?: () => DiagnosticMessageChain | undefined): boolean { - return checkTypeRelatedTo(source, target, comparableRelation, errorNode, headMessage, containingMessageChain); - } - - function isSignatureAssignableTo(source: Signature, - target: Signature, - ignoreReturnTypes: boolean): boolean { - return compareSignaturesRelated(source, target, ignoreReturnTypes ? SignatureCheckMode.IgnoreReturnTypes : 0, /*reportErrors*/ false, - /*errorReporter*/ undefined, /*errorReporter*/ undefined, compareTypesAssignable, /*reportUnreliableMarkers*/ undefined) !== Ternary.False; - } - - type ErrorReporter = (message: DiagnosticMessage, arg0?: string, arg1?: string) => void; - - /** - * Returns true if `s` is `(...args: any[]) => any` or `(this: any, ...args: any[]) => any` - */ - function isAnySignature(s: Signature) { - return !s.typeParameters && (!s.thisParameter || isTypeAny(getTypeOfParameter(s.thisParameter))) && s.parameters.length === 1 && - signatureHasRestParameter(s) && (getTypeOfParameter(s.parameters[0]) === anyArrayType || isTypeAny(getTypeOfParameter(s.parameters[0]))) && - isTypeAny(getReturnTypeOfSignature(s)); - } - - /** - * See signatureRelatedTo, compareSignaturesIdentical - */ - function compareSignaturesRelated(source: Signature, - target: Signature, - checkMode: SignatureCheckMode, - reportErrors: boolean, - errorReporter: ErrorReporter | undefined, - incompatibleErrorReporter: ((source: Type, target: Type) => void) | undefined, - compareTypes: TypeComparer, - reportUnreliableMarkers: TypeMapper | undefined): Ternary { - // TODO (drosen): De-duplicate code between related functions. - if (source === target) { - return Ternary.True; - } - - if (isAnySignature(target)) { - return Ternary.True; - } - - const targetCount = getParameterCount(target); - const sourceHasMoreParameters = !hasEffectiveRestParameter(target) && - (checkMode & SignatureCheckMode.StrictArity ? hasEffectiveRestParameter(source) || getParameterCount(source) > targetCount : getMinArgumentCount(source) > targetCount); - if (sourceHasMoreParameters) { - return Ternary.False; - } - - if (source.typeParameters && source.typeParameters !== target.typeParameters) { - target = getCanonicalSignature(target); - source = instantiateSignatureInContextOf(source, target, /*inferenceContext*/ undefined, compareTypes); - } - - const sourceCount = getParameterCount(source); - const sourceRestType = getNonArrayRestType(source); - const targetRestType = getNonArrayRestType(target); - if (sourceRestType || targetRestType) { - void instantiateType(sourceRestType || targetRestType, reportUnreliableMarkers); - } - if (sourceRestType && targetRestType && sourceCount !== targetCount) { - // We're not able to relate misaligned complex rest parameters - return Ternary.False; - } - - const kind = target.declaration ? target.declaration.kind : SyntaxKind.Unknown; - const strictVariance = !(checkMode & SignatureCheckMode.Callback) && strictFunctionTypes && kind !== SyntaxKind.MethodDeclaration && - kind !== SyntaxKind.MethodSignature && kind !== SyntaxKind.Constructor; - let result = Ternary.True; - - const sourceThisType = getThisTypeOfSignature(source); - if (sourceThisType && sourceThisType !== voidType) { - const targetThisType = getThisTypeOfSignature(target); - if (targetThisType) { - // void sources are assignable to anything. - const related = !strictVariance && compareTypes(sourceThisType, targetThisType, /*reportErrors*/ false) - || compareTypes(targetThisType, sourceThisType, reportErrors); - if (!related) { - if (reportErrors) { - errorReporter!(Diagnostics.The_this_types_of_each_signature_are_incompatible); - } - return Ternary.False; - } - result &= related; - } - } - - const paramCount = sourceRestType || targetRestType ? Math.min(sourceCount, targetCount) : Math.max(sourceCount, targetCount); - const restIndex = sourceRestType || targetRestType ? paramCount - 1 : -1; - - for (let i = 0; i < paramCount; i++) { - const sourceType = i === restIndex ? getRestTypeAtPosition(source, i) : getTypeAtPosition(source, i); - const targetType = i === restIndex ? getRestTypeAtPosition(target, i) : getTypeAtPosition(target, i); - // In order to ensure that any generic type Foo is at least co-variant with respect to T no matter - // how Foo uses T, we need to relate parameters bi-variantly (given that parameters are input positions, - // they naturally relate only contra-variantly). However, if the source and target parameters both have - // function types with a single call signature, we know we are relating two callback parameters. In - // that case it is sufficient to only relate the parameters of the signatures co-variantly because, - // similar to return values, callback parameters are output positions. This means that a Promise, - // where T is used only in callback parameter positions, will be co-variant (as opposed to bi-variant) - // with respect to T. - const sourceSig = checkMode & SignatureCheckMode.Callback ? undefined : getSingleCallSignature(getNonNullableType(sourceType)); - const targetSig = checkMode & SignatureCheckMode.Callback ? undefined : getSingleCallSignature(getNonNullableType(targetType)); - const callbacks = sourceSig && targetSig && !getTypePredicateOfSignature(sourceSig) && !getTypePredicateOfSignature(targetSig) && - (getFalsyFlags(sourceType) & TypeFlags.Nullable) === (getFalsyFlags(targetType) & TypeFlags.Nullable); - let related = callbacks ? - compareSignaturesRelated(targetSig!, sourceSig!, (checkMode & SignatureCheckMode.StrictArity) | (strictVariance ? SignatureCheckMode.StrictCallback : SignatureCheckMode.BivariantCallback), reportErrors, errorReporter, incompatibleErrorReporter, compareTypes, reportUnreliableMarkers) : - !(checkMode & SignatureCheckMode.Callback) && !strictVariance && compareTypes(sourceType, targetType, /*reportErrors*/ false) || compareTypes(targetType, sourceType, reportErrors); - // With strict arity, (x: number | undefined) => void is a subtype of (x?: number | undefined) => void - if (related && checkMode & SignatureCheckMode.StrictArity && i >= getMinArgumentCount(source) && i < getMinArgumentCount(target) && compareTypes(sourceType, targetType, /*reportErrors*/ false)) { - related = Ternary.False; - } - if (!related) { - if (reportErrors) { - errorReporter!(Diagnostics.Types_of_parameters_0_and_1_are_incompatible, - unescapeLeadingUnderscores(getParameterNameAtPosition(source, i)), - unescapeLeadingUnderscores(getParameterNameAtPosition(target, i))); - } - return Ternary.False; - } - result &= related; - } - - if (!(checkMode & SignatureCheckMode.IgnoreReturnTypes)) { - // If a signature resolution is already in-flight, skip issuing a circularity error - // here and just use the `any` type directly - const targetReturnType = isResolvingReturnTypeOfSignature(target) ? anyType - : target.declaration && isJSConstructor(target.declaration) ? getDeclaredTypeOfClassOrInterface(getMergedSymbol(target.declaration.symbol)) - : getReturnTypeOfSignature(target); - if (targetReturnType === voidType) { - return result; - } - const sourceReturnType = isResolvingReturnTypeOfSignature(source) ? anyType - : source.declaration && isJSConstructor(source.declaration) ? getDeclaredTypeOfClassOrInterface(getMergedSymbol(source.declaration.symbol)) - : getReturnTypeOfSignature(source); - - // The following block preserves behavior forbidding boolean returning functions from being assignable to type guard returning functions - const targetTypePredicate = getTypePredicateOfSignature(target); - if (targetTypePredicate) { - const sourceTypePredicate = getTypePredicateOfSignature(source); - if (sourceTypePredicate) { - result &= compareTypePredicateRelatedTo(sourceTypePredicate, targetTypePredicate, reportErrors, errorReporter, compareTypes); - } - else if (isIdentifierTypePredicate(targetTypePredicate)) { - if (reportErrors) { - errorReporter!(Diagnostics.Signature_0_must_be_a_type_predicate, signatureToString(source)); - } - return Ternary.False; - } - } - else { - // When relating callback signatures, we still need to relate return types bi-variantly as otherwise - // the containing type wouldn't be co-variant. For example, interface Foo { add(cb: () => T): void } - // wouldn't be co-variant for T without this rule. - result &= checkMode & SignatureCheckMode.BivariantCallback && compareTypes(targetReturnType, sourceReturnType, /*reportErrors*/ false) || - compareTypes(sourceReturnType, targetReturnType, reportErrors); - if (!result && reportErrors && incompatibleErrorReporter) { - incompatibleErrorReporter(sourceReturnType, targetReturnType); - } - } - - } - - return result; - } - - function compareTypePredicateRelatedTo( - source: TypePredicate, - target: TypePredicate, - reportErrors: boolean, - errorReporter: ErrorReporter | undefined, - compareTypes: (s: Type, t: Type, reportErrors?: boolean) => Ternary): Ternary { - if (source.kind !== target.kind) { - if (reportErrors) { - errorReporter!(Diagnostics.A_this_based_type_guard_is_not_compatible_with_a_parameter_based_type_guard); - errorReporter!(Diagnostics.Type_predicate_0_is_not_assignable_to_1, typePredicateToString(source), typePredicateToString(target)); - } - return Ternary.False; - } - - if (source.kind === TypePredicateKind.Identifier || source.kind === TypePredicateKind.AssertsIdentifier) { - if (source.parameterIndex !== (target as IdentifierTypePredicate).parameterIndex) { - if (reportErrors) { - errorReporter!(Diagnostics.Parameter_0_is_not_in_the_same_position_as_parameter_1, source.parameterName, (target as IdentifierTypePredicate).parameterName); - errorReporter!(Diagnostics.Type_predicate_0_is_not_assignable_to_1, typePredicateToString(source), typePredicateToString(target)); - } - return Ternary.False; - } - } - - const related = source.type === target.type ? Ternary.True : - source.type && target.type ? compareTypes(source.type, target.type, reportErrors) : - Ternary.False; - if (related === Ternary.False && reportErrors) { - errorReporter!(Diagnostics.Type_predicate_0_is_not_assignable_to_1, typePredicateToString(source), typePredicateToString(target)); - } - return related; - } - - function isImplementationCompatibleWithOverload(implementation: Signature, overload: Signature): boolean { - const erasedSource = getErasedSignature(implementation); - const erasedTarget = getErasedSignature(overload); - - // First see if the return types are compatible in either direction. - const sourceReturnType = getReturnTypeOfSignature(erasedSource); - const targetReturnType = getReturnTypeOfSignature(erasedTarget); - if (targetReturnType === voidType - || isTypeRelatedTo(targetReturnType, sourceReturnType, assignableRelation) - || isTypeRelatedTo(sourceReturnType, targetReturnType, assignableRelation)) { - - return isSignatureAssignableTo(erasedSource, erasedTarget, /*ignoreReturnTypes*/ true); - } - - return false; - } - - function isEmptyResolvedType(t: ResolvedType) { - return t !== anyFunctionType && - t.properties.length === 0 && - t.callSignatures.length === 0 && - t.constructSignatures.length === 0 && - !t.stringIndexInfo && - !t.numberIndexInfo; - } - - function isEmptyObjectType(type: Type): boolean { - return type.flags & TypeFlags.Object ? !isGenericMappedType(type) && isEmptyResolvedType(resolveStructuredTypeMembers(type)) : - type.flags & TypeFlags.NonPrimitive ? true : - type.flags & TypeFlags.Union ? some((type).types, isEmptyObjectType) : - type.flags & TypeFlags.Intersection ? every((type).types, isEmptyObjectType) : - false; - } - - function isEmptyAnonymousObjectType(type: Type) { - return !!(getObjectFlags(type) & ObjectFlags.Anonymous) && isEmptyObjectType(type); - } - - function isStringIndexSignatureOnlyType(type: Type): boolean { - return type.flags & TypeFlags.Object && !isGenericMappedType(type) && getPropertiesOfType(type).length === 0 && getIndexInfoOfType(type, IndexKind.String) && !getIndexInfoOfType(type, IndexKind.Number) || - type.flags & TypeFlags.UnionOrIntersection && every((type).types, isStringIndexSignatureOnlyType) || - false; - } - - function isEnumTypeRelatedTo(sourceSymbol: Symbol, targetSymbol: Symbol, errorReporter?: ErrorReporter) { - if (sourceSymbol === targetSymbol) { - return true; - } - const id = getSymbolId(sourceSymbol) + "," + getSymbolId(targetSymbol); - const entry = enumRelation.get(id); - if (entry !== undefined && !(!(entry & RelationComparisonResult.Reported) && entry & RelationComparisonResult.Failed && errorReporter)) { - return !!(entry & RelationComparisonResult.Succeeded); - } - if (sourceSymbol.escapedName !== targetSymbol.escapedName || !(sourceSymbol.flags & SymbolFlags.RegularEnum) || !(targetSymbol.flags & SymbolFlags.RegularEnum)) { - enumRelation.set(id, RelationComparisonResult.Failed | RelationComparisonResult.Reported); - return false; - } - const targetEnumType = getTypeOfSymbol(targetSymbol); - for (const property of getPropertiesOfType(getTypeOfSymbol(sourceSymbol))) { - if (property.flags & SymbolFlags.EnumMember) { - const targetProperty = getPropertyOfType(targetEnumType, property.escapedName); - if (!targetProperty || !(targetProperty.flags & SymbolFlags.EnumMember)) { - if (errorReporter) { - errorReporter(Diagnostics.Property_0_is_missing_in_type_1, symbolName(property), - typeToString(getDeclaredTypeOfSymbol(targetSymbol), /*enclosingDeclaration*/ undefined, TypeFormatFlags.UseFullyQualifiedType)); - enumRelation.set(id, RelationComparisonResult.Failed | RelationComparisonResult.Reported); - } - else { - enumRelation.set(id, RelationComparisonResult.Failed); - } - return false; - } - } - } - enumRelation.set(id, RelationComparisonResult.Succeeded); - return true; - } - - function isSimpleTypeRelatedTo(source: Type, target: Type, relation: Map, errorReporter?: ErrorReporter) { - const s = source.flags; - const t = target.flags; - if (t & TypeFlags.AnyOrUnknown || s & TypeFlags.Never || source === wildcardType) return true; - if (t & TypeFlags.Never) return false; - if (s & TypeFlags.StringLike && t & TypeFlags.String) return true; - if (s & TypeFlags.StringLiteral && s & TypeFlags.EnumLiteral && - t & TypeFlags.StringLiteral && !(t & TypeFlags.EnumLiteral) && - (source).value === (target).value) return true; - if (s & TypeFlags.NumberLike && t & TypeFlags.Number) return true; - if (s & TypeFlags.NumberLiteral && s & TypeFlags.EnumLiteral && - t & TypeFlags.NumberLiteral && !(t & TypeFlags.EnumLiteral) && - (source).value === (target).value) return true; - if (s & TypeFlags.BigIntLike && t & TypeFlags.BigInt) return true; - if (s & TypeFlags.BooleanLike && t & TypeFlags.Boolean) return true; - if (s & TypeFlags.ESSymbolLike && t & TypeFlags.ESSymbol) return true; - if (s & TypeFlags.Enum && t & TypeFlags.Enum && isEnumTypeRelatedTo(source.symbol, target.symbol, errorReporter)) return true; - if (s & TypeFlags.EnumLiteral && t & TypeFlags.EnumLiteral) { - if (s & TypeFlags.Union && t & TypeFlags.Union && isEnumTypeRelatedTo(source.symbol, target.symbol, errorReporter)) return true; - if (s & TypeFlags.Literal && t & TypeFlags.Literal && - (source).value === (target).value && - isEnumTypeRelatedTo(getParentOfSymbol(source.symbol)!, getParentOfSymbol(target.symbol)!, errorReporter)) return true; - } - if (s & TypeFlags.Undefined && (!strictNullChecks || t & (TypeFlags.Undefined | TypeFlags.Void))) return true; - if (s & TypeFlags.Null && (!strictNullChecks || t & TypeFlags.Null)) return true; - if (s & TypeFlags.Object && t & TypeFlags.NonPrimitive) return true; - if (relation === assignableRelation || relation === comparableRelation) { - if (s & TypeFlags.Any) return true; - // Type number or any numeric literal type is assignable to any numeric enum type or any - // numeric enum literal type. This rule exists for backwards compatibility reasons because - // bit-flag enum types sometimes look like literal enum types with numeric literal values. - if (s & (TypeFlags.Number | TypeFlags.NumberLiteral) && !(s & TypeFlags.EnumLiteral) && ( - t & TypeFlags.Enum || t & TypeFlags.NumberLiteral && t & TypeFlags.EnumLiteral)) return true; - } - return false; - } - - function isTypeRelatedTo(source: Type, target: Type, relation: Map) { - if (isFreshLiteralType(source)) { - source = (source).regularType; - } - if (isFreshLiteralType(target)) { - target = (target).regularType; - } - if (source === target) { - return true; - } - if (relation !== identityRelation) { - if (relation === comparableRelation && !(target.flags & TypeFlags.Never) && isSimpleTypeRelatedTo(target, source, relation) || isSimpleTypeRelatedTo(source, target, relation)) { - return true; - } - } - else { - if (!(source.flags === target.flags && source.flags & TypeFlags.Substructure)) return false; - } - if (source.flags & TypeFlags.Object && target.flags & TypeFlags.Object) { - const related = relation.get(getRelationKey(source, target, IntersectionState.None, relation)); - if (related !== undefined) { - return !!(related & RelationComparisonResult.Succeeded); - } - } - if (source.flags & TypeFlags.StructuredOrInstantiable || target.flags & TypeFlags.StructuredOrInstantiable) { - return checkTypeRelatedTo(source, target, relation, /*errorNode*/ undefined); - } - return false; - } - - function isIgnoredJsxProperty(source: Type, sourceProp: Symbol) { - return getObjectFlags(source) & ObjectFlags.JsxAttributes && !isUnhyphenatedJsxName(sourceProp.escapedName); - } - - function getNormalizedType(type: Type, writing: boolean): Type { - do { - const t = isFreshLiteralType(type) ? (type).regularType : - getObjectFlags(type) & ObjectFlags.Reference && (type).node ? createTypeReference((type).target, getTypeArguments(type)) : - type.flags & TypeFlags.Substitution ? writing ? (type).typeVariable : (type).substitute : - type.flags & TypeFlags.Simplifiable ? getSimplifiedType(type, writing) : - type; - if (t === type) break; - type = t; - } while (true); - return type; - } - - /** - * Checks if 'source' is related to 'target' (e.g.: is a assignable to). - * @param source The left-hand-side of the relation. - * @param target The right-hand-side of the relation. - * @param relation The relation considered. One of 'identityRelation', 'subtypeRelation', 'assignableRelation', or 'comparableRelation'. - * Used as both to determine which checks are performed and as a cache of previously computed results. - * @param errorNode The suggested node upon which all errors will be reported, if defined. This may or may not be the actual node used. - * @param headMessage If the error chain should be prepended by a head message, then headMessage will be used. - * @param containingMessageChain A chain of errors to prepend any new errors found. - * @param errorOutputContainer Return the diagnostic. Do not log if 'skipLogging' is truthy. - */ - function checkTypeRelatedTo( - source: Type, - target: Type, - relation: Map, - errorNode: Node | undefined, - headMessage?: DiagnosticMessage, - containingMessageChain?: () => DiagnosticMessageChain | undefined, - errorOutputContainer?: { errors?: Diagnostic[], skipLogging?: boolean }, - ): boolean { - let errorInfo: DiagnosticMessageChain | undefined; - let relatedInfo: [DiagnosticRelatedInformation, ...DiagnosticRelatedInformation[]] | undefined; - let maybeKeys: string[]; - let sourceStack: Type[]; - let targetStack: Type[]; - let maybeCount = 0; - let depth = 0; - let expandingFlags = ExpandingFlags.None; - let overflow = false; - let overrideNextErrorInfo = 0; // How many `reportRelationError` calls should be skipped in the elaboration pyramid - let lastSkippedInfo: [Type, Type] | undefined; - let incompatibleStack: [DiagnosticMessage, (string | number)?, (string | number)?, (string | number)?, (string | number)?][] = []; - - Debug.assert(relation !== identityRelation || !errorNode, "no error reporting in identity checking"); - - const result = isRelatedTo(source, target, /*reportErrors*/ !!errorNode, headMessage); - if (incompatibleStack.length) { - reportIncompatibleStack(); - } - if (overflow) { - const diag = error(errorNode, Diagnostics.Excessive_stack_depth_comparing_types_0_and_1, typeToString(source), typeToString(target)); - if (errorOutputContainer) { - (errorOutputContainer.errors || (errorOutputContainer.errors = [])).push(diag); - } - } - else if (errorInfo) { - if (containingMessageChain) { - const chain = containingMessageChain(); - if (chain) { - concatenateDiagnosticMessageChains(chain, errorInfo); - errorInfo = chain; - } - } - - let relatedInformation: DiagnosticRelatedInformation[] | undefined; - // Check if we should issue an extra diagnostic to produce a quickfix for a slightly incorrect import statement - if (headMessage && errorNode && !result && source.symbol) { - const links = getSymbolLinks(source.symbol); - if (links.originatingImport && !isImportCall(links.originatingImport)) { - const helpfulRetry = checkTypeRelatedTo(getTypeOfSymbol(links.target!), target, relation, /*errorNode*/ undefined); - if (helpfulRetry) { - // Likely an incorrect import. Issue a helpful diagnostic to produce a quickfix to change the import - const diag = createDiagnosticForNode(links.originatingImport, Diagnostics.Type_originates_at_this_import_A_namespace_style_import_cannot_be_called_or_constructed_and_will_cause_a_failure_at_runtime_Consider_using_a_default_import_or_import_require_here_instead); - relatedInformation = append(relatedInformation, diag); // Cause the error to appear with the error that triggered it - } - } - } - const diag = createDiagnosticForNodeFromMessageChain(errorNode!, errorInfo, relatedInformation); - if (relatedInfo) { - addRelatedInfo(diag, ...relatedInfo); - } - if (errorOutputContainer) { - (errorOutputContainer.errors || (errorOutputContainer.errors = [])).push(diag); - } - if (!errorOutputContainer || !errorOutputContainer.skipLogging) { - diagnostics.add(diag); - } - } - if (errorNode && errorOutputContainer && errorOutputContainer.skipLogging && result === Ternary.False) { - Debug.assert(!!errorOutputContainer.errors, "missed opportunity to interact with error."); - } - return result !== Ternary.False; - - function resetErrorInfo(saved: ReturnType) { - errorInfo = saved.errorInfo; - lastSkippedInfo = saved.lastSkippedInfo; - incompatibleStack = saved.incompatibleStack; - overrideNextErrorInfo = saved.overrideNextErrorInfo; - relatedInfo = saved.relatedInfo; - } - - function captureErrorCalculationState() { - return { - errorInfo, - lastSkippedInfo, - incompatibleStack: incompatibleStack.slice(), - overrideNextErrorInfo, - relatedInfo: !relatedInfo ? undefined : relatedInfo.slice() as ([DiagnosticRelatedInformation, ...DiagnosticRelatedInformation[]] | undefined) - }; - } - - function reportIncompatibleError(message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number) { - overrideNextErrorInfo++; // Suppress the next relation error - lastSkippedInfo = undefined; // Reset skipped info cache - incompatibleStack.push([message, arg0, arg1, arg2, arg3]); - } - - function reportIncompatibleStack() { - const stack = incompatibleStack; - incompatibleStack = []; - const info = lastSkippedInfo; - lastSkippedInfo = undefined; - if (stack.length === 1) { - reportError(...stack[0]); - if (info) { - // Actually do the last relation error - reportRelationError(/*headMessage*/ undefined, ...info); - } - return; - } - // The first error will be the innermost, while the last will be the outermost - so by popping off the end, - // we can build from left to right - let path = ""; - const secondaryRootErrors: typeof incompatibleStack = []; - while (stack.length) { - const [msg, ...args] = stack.pop()!; - switch (msg.code) { - case Diagnostics.Types_of_property_0_are_incompatible.code: { - // Parenthesize a `new` if there is one - if (path.indexOf("new ") === 0) { - path = `(${path})`; - } - const str = "" + args[0]; - // If leading, just print back the arg (irrespective of if it's a valid identifier) - if (path.length === 0) { - path = `${str}`; - } - // Otherwise write a dotted name if possible - else if (isIdentifierText(str, compilerOptions.target)) { - path = `${path}.${str}`; - } - // Failing that, check if the name is already a computed name - else if (str[0] === "[" && str[str.length - 1] === "]") { - path = `${path}${str}`; - } - // And finally write out a computed name as a last resort - else { - path = `${path}[${str}]`; - } - break; - } - case Diagnostics.Call_signature_return_types_0_and_1_are_incompatible.code: - case Diagnostics.Construct_signature_return_types_0_and_1_are_incompatible.code: - case Diagnostics.Call_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code: - case Diagnostics.Construct_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code: { - if (path.length === 0) { - // Don't flatten signature compatability errors at the start of a chain - instead prefer - // to unify (the with no arguments bit is excessive for printback) and print them back - let mappedMsg = msg; - if (msg.code === Diagnostics.Call_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code) { - mappedMsg = Diagnostics.Call_signature_return_types_0_and_1_are_incompatible; - } - else if (msg.code === Diagnostics.Construct_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code) { - mappedMsg = Diagnostics.Construct_signature_return_types_0_and_1_are_incompatible; - } - secondaryRootErrors.unshift([mappedMsg, args[0], args[1]]); - } - else { - const prefix = (msg.code === Diagnostics.Construct_signature_return_types_0_and_1_are_incompatible.code || - msg.code === Diagnostics.Construct_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code) - ? "new " - : ""; - const params = (msg.code === Diagnostics.Call_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code || - msg.code === Diagnostics.Construct_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code) - ? "" - : "..."; - path = `${prefix}${path}(${params})`; - } - break; - } - default: - return Debug.fail(`Unhandled Diagnostic: ${msg.code}`); - } - } - if (path) { - reportError(path[path.length - 1] === ")" - ? Diagnostics.The_types_returned_by_0_are_incompatible_between_these_types - : Diagnostics.The_types_of_0_are_incompatible_between_these_types, - path - ); - } - else { - // Remove the innermost secondary error as it will duplicate the error already reported by `reportRelationError` on entry - secondaryRootErrors.shift(); - } - for (const [msg, ...args] of secondaryRootErrors) { - const originalValue = msg.elidedInCompatabilityPyramid; - msg.elidedInCompatabilityPyramid = false; // Teporarily override elision to ensure error is reported - reportError(msg, ...args); - msg.elidedInCompatabilityPyramid = originalValue; - } - if (info) { - // Actually do the last relation error - reportRelationError(/*headMessage*/ undefined, ...info); - } - } - - function reportError(message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): void { - Debug.assert(!!errorNode); - if (incompatibleStack.length) reportIncompatibleStack(); - if (message.elidedInCompatabilityPyramid) return; - errorInfo = chainDiagnosticMessages(errorInfo, message, arg0, arg1, arg2, arg3); - } - - function associateRelatedInfo(info: DiagnosticRelatedInformation) { - Debug.assert(!!errorInfo); - if (!relatedInfo) { - relatedInfo = [info]; - } - else { - relatedInfo.push(info); - } - } - - function reportRelationError(message: DiagnosticMessage | undefined, source: Type, target: Type) { - if (incompatibleStack.length) reportIncompatibleStack(); - const [sourceType, targetType] = getTypeNamesForErrorDisplay(source, target); - - if (target.flags & TypeFlags.TypeParameter && target.immediateBaseConstraint !== undefined && isTypeAssignableTo(source, target.immediateBaseConstraint)) { - reportError( - Diagnostics._0_is_assignable_to_the_constraint_of_type_1_but_1_could_be_instantiated_with_a_different_subtype_of_constraint_2, - sourceType, - targetType, - typeToString(target.immediateBaseConstraint), - ); - } - - if (!message) { - if (relation === comparableRelation) { - message = Diagnostics.Type_0_is_not_comparable_to_type_1; - } - else if (sourceType === targetType) { - message = Diagnostics.Type_0_is_not_assignable_to_type_1_Two_different_types_with_this_name_exist_but_they_are_unrelated; - } - else { - message = Diagnostics.Type_0_is_not_assignable_to_type_1; - } - } - - reportError(message, sourceType, targetType); - } - - function tryElaborateErrorsForPrimitivesAndObjects(source: Type, target: Type) { - const sourceType = symbolValueDeclarationIsContextSensitive(source.symbol) ? typeToString(source, source.symbol.valueDeclaration) : typeToString(source); - const targetType = symbolValueDeclarationIsContextSensitive(target.symbol) ? typeToString(target, target.symbol.valueDeclaration) : typeToString(target); - - if ((globalStringType === source && stringType === target) || - (globalNumberType === source && numberType === target) || - (globalBooleanType === source && booleanType === target) || - (getGlobalESSymbolType(/*reportErrors*/ false) === source && esSymbolType === target)) { - reportError(Diagnostics._0_is_a_primitive_but_1_is_a_wrapper_object_Prefer_using_0_when_possible, targetType, sourceType); - } - } - - /** - * Try and elaborate array and tuple errors. Returns false - * if we have found an elaboration, or we should ignore - * any other elaborations when relating the `source` and - * `target` types. - */ - function tryElaborateArrayLikeErrors(source: Type, target: Type, reportErrors: boolean): boolean { - /** - * The spec for elaboration is: - * - If the source is a readonly tuple and the target is a mutable array or tuple, elaborate on mutability and skip property elaborations. - * - If the source is a tuple then skip property elaborations if the target is an array or tuple. - * - If the source is a readonly array and the target is a mutable array or tuple, elaborate on mutability and skip property elaborations. - * - If the source an array then skip property elaborations if the target is a tuple. - */ - if (isTupleType(source)) { - if (source.target.readonly && isMutableArrayOrTuple(target)) { - if (reportErrors) { - reportError(Diagnostics.The_type_0_is_readonly_and_cannot_be_assigned_to_the_mutable_type_1, typeToString(source), typeToString(target)); - } - return false; - } - return isTupleType(target) || isArrayType(target); - } - if (isReadonlyArrayType(source) && isMutableArrayOrTuple(target)) { - if (reportErrors) { - reportError(Diagnostics.The_type_0_is_readonly_and_cannot_be_assigned_to_the_mutable_type_1, typeToString(source), typeToString(target)); - } - return false; - } - if (isTupleType(target)) { - return isArrayType(source); - } - return true; - } - - /** - * Compare two types and return - * * Ternary.True if they are related with no assumptions, - * * Ternary.Maybe if they are related with assumptions of other relationships, or - * * Ternary.False if they are not related. - */ - function isRelatedTo(originalSource: Type, originalTarget: Type, reportErrors = false, headMessage?: DiagnosticMessage, intersectionState = IntersectionState.None): Ternary { - // Normalize the source and target types: Turn fresh literal types into regular literal types, - // turn deferred type references into regular type references, simplify indexed access and - // conditional types, and resolve substitution types to either the substitution (on the source - // side) or the type variable (on the target side). - let source = getNormalizedType(originalSource, /*writing*/ false); - let target = getNormalizedType(originalTarget, /*writing*/ true); - - if (source === target) return Ternary.True; - - if (relation === identityRelation) { - return isIdenticalTo(source, target); - } - - // Try to see if we're relating something like `Foo` -> `Bar | null | undefined`. - // If so, reporting the `null` and `undefined` in the type is hardly useful. - // First, see if we're even relating an object type to a union. - // Then see if the target is stripped down to a single non-union type. - // Note - // * We actually want to remove null and undefined naively here (rather than using getNonNullableType), - // since we don't want to end up with a worse error like "`Foo` is not assignable to `NonNullable`" - // when dealing with generics. - // * We also don't deal with primitive source types, since we already halt elaboration below. - if (target.flags & TypeFlags.Union && source.flags & TypeFlags.Object && - (target as UnionType).types.length <= 3 && maybeTypeOfKind(target, TypeFlags.Nullable)) { - const nullStrippedTarget = extractTypesOfKind(target, ~TypeFlags.Nullable); - if (!(nullStrippedTarget.flags & (TypeFlags.Union | TypeFlags.Never))) { - if (source === nullStrippedTarget) return Ternary.True; - target = nullStrippedTarget; - } - } - - if (relation === comparableRelation && !(target.flags & TypeFlags.Never) && isSimpleTypeRelatedTo(target, source, relation) || - isSimpleTypeRelatedTo(source, target, relation, reportErrors ? reportError : undefined)) return Ternary.True; - - const isComparingJsxAttributes = !!(getObjectFlags(source) & ObjectFlags.JsxAttributes); - const isPerformingExcessPropertyChecks = !(intersectionState & IntersectionState.Target) && (isObjectLiteralType(source) && getObjectFlags(source) & ObjectFlags.FreshLiteral); - if (isPerformingExcessPropertyChecks) { - if (hasExcessProperties(source, target, reportErrors)) { - if (reportErrors) { - reportRelationError(headMessage, source, target); - } - return Ternary.False; - } - } - - const isPerformingCommonPropertyChecks = relation !== comparableRelation && !(intersectionState & IntersectionState.Target) && - source.flags & (TypeFlags.Primitive | TypeFlags.Object | TypeFlags.Intersection) && source !== globalObjectType && - target.flags & (TypeFlags.Object | TypeFlags.Intersection) && isWeakType(target) && - (getPropertiesOfType(source).length > 0 || typeHasCallOrConstructSignatures(source)); - if (isPerformingCommonPropertyChecks && !hasCommonProperties(source, target, isComparingJsxAttributes)) { - if (reportErrors) { - const calls = getSignaturesOfType(source, SignatureKind.Call); - const constructs = getSignaturesOfType(source, SignatureKind.Construct); - if (calls.length > 0 && isRelatedTo(getReturnTypeOfSignature(calls[0]), target, /*reportErrors*/ false) || - constructs.length > 0 && isRelatedTo(getReturnTypeOfSignature(constructs[0]), target, /*reportErrors*/ false)) { - reportError(Diagnostics.Value_of_type_0_has_no_properties_in_common_with_type_1_Did_you_mean_to_call_it, typeToString(source), typeToString(target)); - } - else { - reportError(Diagnostics.Type_0_has_no_properties_in_common_with_type_1, typeToString(source), typeToString(target)); - } - } - return Ternary.False; - } - - let result = Ternary.False; - const saveErrorInfo = captureErrorCalculationState(); - - // Note that these checks are specifically ordered to produce correct results. In particular, - // we need to deconstruct unions before intersections (because unions are always at the top), - // and we need to handle "each" relations before "some" relations for the same kind of type. - if (source.flags & TypeFlags.Union) { - result = relation === comparableRelation ? - someTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive), intersectionState) : - eachTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive)); - } - else { - if (target.flags & TypeFlags.Union) { - result = typeRelatedToSomeType(getRegularTypeOfObjectLiteral(source), target, reportErrors && !(source.flags & TypeFlags.Primitive) && !(target.flags & TypeFlags.Primitive)); - } - else if (target.flags & TypeFlags.Intersection) { - result = typeRelatedToEachType(getRegularTypeOfObjectLiteral(source), target as IntersectionType, reportErrors, IntersectionState.Target); - if (result && (isPerformingExcessPropertyChecks || isPerformingCommonPropertyChecks)) { - // Validate against excess props using the original `source` - if (!propertiesRelatedTo(source, target, reportErrors, /*excludedProperties*/ undefined, IntersectionState.None)) { - return Ternary.False; - } - } - } - else if (source.flags & TypeFlags.Intersection) { - // Check to see if any constituents of the intersection are immediately related to the target. - // - // Don't report errors though. Checking whether a constituent is related to the source is not actually - // useful and leads to some confusing error messages. Instead it is better to let the below checks - // take care of this, or to not elaborate at all. For instance, - // - // - For an object type (such as 'C = A & B'), users are usually more interested in structural errors. - // - // - For a union type (such as '(A | B) = (C & D)'), it's better to hold onto the whole intersection - // than to report that 'D' is not assignable to 'A' or 'B'. - // - // - For a primitive type or type parameter (such as 'number = A & B') there is no point in - // breaking the intersection apart. - result = someTypeRelatedToType(source, target, /*reportErrors*/ false, IntersectionState.Source); - } - if (!result && (source.flags & TypeFlags.StructuredOrInstantiable || target.flags & TypeFlags.StructuredOrInstantiable)) { - if (result = recursiveTypeRelatedTo(source, target, reportErrors, intersectionState)) { - resetErrorInfo(saveErrorInfo); - } - } - } - if (!result && source.flags & (TypeFlags.Intersection | TypeFlags.TypeParameter)) { - // The combined constraint of an intersection type is the intersection of the constraints of - // the constituents. When an intersection type contains instantiable types with union type - // constraints, there are situations where we need to examine the combined constraint. One is - // when the target is a union type. Another is when the intersection contains types belonging - // to one of the disjoint domains. For example, given type variables T and U, each with the - // constraint 'string | number', the combined constraint of 'T & U' is 'string | number' and - // we need to check this constraint against a union on the target side. Also, given a type - // variable V constrained to 'string | number', 'V & number' has a combined constraint of - // 'string & number | number & number' which reduces to just 'number'. - // This also handles type parameters, as a type parameter with a union constraint compared against a union - // needs to have its constraint hoisted into an intersection with said type parameter, this way - // the type param can be compared with itself in the target (with the influence of its constraint to match other parts) - // For example, if `T extends 1 | 2` and `U extends 2 | 3` and we compare `T & U` to `T & U & (1 | 2 | 3)` - const constraint = getEffectiveConstraintOfIntersection(source.flags & TypeFlags.Intersection ? (source).types: [source], !!(target.flags & TypeFlags.Union)); - if (constraint && (source.flags & TypeFlags.Intersection || target.flags & TypeFlags.Union)) { - if (everyType(constraint, c => c !== source)) { // Skip comparison if expansion contains the source itself - // TODO: Stack errors so we get a pyramid for the "normal" comparison above, _and_ a second for this - if (result = isRelatedTo(constraint, target, /*reportErrors*/ false, /*headMessage*/ undefined, intersectionState)) { - resetErrorInfo(saveErrorInfo); - } - } - } - } - - if (!result && reportErrors) { - source = originalSource.aliasSymbol ? originalSource : source; - target = originalTarget.aliasSymbol ? originalTarget : target; - let maybeSuppress = overrideNextErrorInfo > 0; - if (maybeSuppress) { - overrideNextErrorInfo--; - } - if (source.flags & TypeFlags.Object && target.flags & TypeFlags.Object) { - const currentError = errorInfo; - tryElaborateArrayLikeErrors(source, target, reportErrors); - if (errorInfo !== currentError) { - maybeSuppress = !!errorInfo; - } - } - if (source.flags & TypeFlags.Object && target.flags & TypeFlags.Primitive) { - tryElaborateErrorsForPrimitivesAndObjects(source, target); - } - else if (source.symbol && source.flags & TypeFlags.Object && globalObjectType === source) { - reportError(Diagnostics.The_Object_type_is_assignable_to_very_few_other_types_Did_you_mean_to_use_the_any_type_instead); - } - else if (isComparingJsxAttributes && target.flags & TypeFlags.Intersection) { - const targetTypes = (target as IntersectionType).types; - const intrinsicAttributes = getJsxType(JsxNames.IntrinsicAttributes, errorNode); - const intrinsicClassAttributes = getJsxType(JsxNames.IntrinsicClassAttributes, errorNode); - if (intrinsicAttributes !== errorType && intrinsicClassAttributes !== errorType && - (contains(targetTypes, intrinsicAttributes) || contains(targetTypes, intrinsicClassAttributes))) { - // do not report top error - return result; - } - } - if (!headMessage && maybeSuppress) { - lastSkippedInfo = [source, target]; - // Used by, eg, missing property checking to replace the top-level message with a more informative one - return result; - } - reportRelationError(headMessage, source, target); - } - return result; - } - - function isIdenticalTo(source: Type, target: Type): Ternary { - const flags = source.flags & target.flags; - if (!(flags & TypeFlags.Substructure)) { - return Ternary.False; - } - if (flags & TypeFlags.UnionOrIntersection) { - let result = eachTypeRelatedToSomeType(source, target); - if (result) { - result &= eachTypeRelatedToSomeType(target, source); - } - return result; - } - return recursiveTypeRelatedTo(source, target, /*reportErrors*/ false, IntersectionState.None); - } - - function getTypeOfPropertyInTypes(types: Type[], name: __String) { - const appendPropType = (propTypes: Type[] | undefined, type: Type) => { - type = getApparentType(type); - const prop = type.flags & TypeFlags.UnionOrIntersection ? getPropertyOfUnionOrIntersectionType(type, name) : getPropertyOfObjectType(type, name); - const propType = prop && getTypeOfSymbol(prop) || isNumericLiteralName(name) && getIndexTypeOfType(type, IndexKind.Number) || getIndexTypeOfType(type, IndexKind.String) || undefinedType; - return append(propTypes, propType); - }; - return getUnionType(reduceLeft(types, appendPropType, /*initial*/ undefined) || emptyArray); - } - - function hasExcessProperties(source: FreshObjectLiteralType, target: Type, reportErrors: boolean): boolean { - if (!isExcessPropertyCheckTarget(target) || !noImplicitAny && getObjectFlags(target) & ObjectFlags.JSLiteral) { - return false; // Disable excess property checks on JS literals to simulate having an implicit "index signature" - but only outside of noImplicitAny - } - const isComparingJsxAttributes = !!(getObjectFlags(source) & ObjectFlags.JsxAttributes); - if ((relation === assignableRelation || relation === comparableRelation) && - (isTypeSubsetOf(globalObjectType, target) || (!isComparingJsxAttributes && isEmptyObjectType(target)))) { - return false; - } - let reducedTarget = target; - let checkTypes: Type[] | undefined; - if (target.flags & TypeFlags.Union) { - reducedTarget = findMatchingDiscriminantType(source, target, isRelatedTo) || filterPrimitivesIfContainsNonPrimitive(target); - checkTypes = reducedTarget.flags & TypeFlags.Union ? (reducedTarget).types : [reducedTarget]; - } - for (const prop of getPropertiesOfType(source)) { - if (shouldCheckAsExcessProperty(prop, source.symbol) && !isIgnoredJsxProperty(source, prop)) { - if (!isKnownProperty(reducedTarget, prop.escapedName, isComparingJsxAttributes)) { - if (reportErrors) { - // Report error in terms of object types in the target as those are the only ones - // we check in isKnownProperty. - const errorTarget = filterType(reducedTarget, isExcessPropertyCheckTarget); - // We know *exactly* where things went wrong when comparing the types. - // Use this property as the error node as this will be more helpful in - // reasoning about what went wrong. - if (!errorNode) return Debug.fail(); - if (isJsxAttributes(errorNode) || isJsxOpeningLikeElement(errorNode) || isJsxOpeningLikeElement(errorNode.parent)) { - // JsxAttributes has an object-literal flag and undergo same type-assignablity check as normal object-literal. - // However, using an object-literal error message will be very confusing to the users so we give different a message. - // TODO: Spelling suggestions for excess jsx attributes (needs new diagnostic messages) - if (prop.valueDeclaration && isJsxAttribute(prop.valueDeclaration) && getSourceFileOfNode(errorNode) === getSourceFileOfNode(prop.valueDeclaration.name)) { - // Note that extraneous children (as in `extra`) don't pass this check, - // since `children` is a SyntaxKind.PropertySignature instead of a SyntaxKind.JsxAttribute. - errorNode = prop.valueDeclaration.name; - } - reportError(Diagnostics.Property_0_does_not_exist_on_type_1, symbolToString(prop), typeToString(errorTarget)); - } - else { - // use the property's value declaration if the property is assigned inside the literal itself - const objectLiteralDeclaration = source.symbol && firstOrUndefined(source.symbol.declarations); - let suggestion; - if (prop.valueDeclaration && findAncestor(prop.valueDeclaration, d => d === objectLiteralDeclaration) && getSourceFileOfNode(objectLiteralDeclaration) === getSourceFileOfNode(errorNode)) { - const propDeclaration = prop.valueDeclaration as ObjectLiteralElementLike; - Debug.assertNode(propDeclaration, isObjectLiteralElementLike); - - errorNode = propDeclaration; - - const name = propDeclaration.name!; - if (isIdentifier(name)) { - suggestion = getSuggestionForNonexistentProperty(name, errorTarget); - } - } - if (suggestion !== undefined) { - reportError(Diagnostics.Object_literal_may_only_specify_known_properties_but_0_does_not_exist_in_type_1_Did_you_mean_to_write_2, - symbolToString(prop), typeToString(errorTarget), suggestion); - } - else { - reportError(Diagnostics.Object_literal_may_only_specify_known_properties_and_0_does_not_exist_in_type_1, - symbolToString(prop), typeToString(errorTarget)); - } - } - } - return true; - } - if (checkTypes && !isRelatedTo(getTypeOfSymbol(prop), getTypeOfPropertyInTypes(checkTypes, prop.escapedName), reportErrors)) { - if (reportErrors) { - reportIncompatibleError(Diagnostics.Types_of_property_0_are_incompatible, symbolToString(prop)); - } - return true; - } - } - } - return false; - } - - function shouldCheckAsExcessProperty(prop: Symbol, container: Symbol) { - return prop.valueDeclaration && container.valueDeclaration && prop.valueDeclaration.parent === container.valueDeclaration; - } - - function eachTypeRelatedToSomeType(source: UnionOrIntersectionType, target: UnionOrIntersectionType): Ternary { - let result = Ternary.True; - const sourceTypes = source.types; - for (const sourceType of sourceTypes) { - const related = typeRelatedToSomeType(sourceType, target, /*reportErrors*/ false); - if (!related) { - return Ternary.False; - } - result &= related; - } - return result; - } - - function typeRelatedToSomeType(source: Type, target: UnionOrIntersectionType, reportErrors: boolean): Ternary { - const targetTypes = target.types; - if (target.flags & TypeFlags.Union && containsType(targetTypes, source)) { - return Ternary.True; - } - for (const type of targetTypes) { - const related = isRelatedTo(source, type, /*reportErrors*/ false); - if (related) { - return related; - } - } - if (reportErrors) { - const bestMatchingType = getBestMatchingType(source, target, isRelatedTo); - isRelatedTo(source, bestMatchingType || targetTypes[targetTypes.length - 1], /*reportErrors*/ true); - } - return Ternary.False; - } - - function typeRelatedToEachType(source: Type, target: IntersectionType, reportErrors: boolean, intersectionState: IntersectionState): Ternary { - let result = Ternary.True; - const targetTypes = target.types; - for (const targetType of targetTypes) { - const related = isRelatedTo(source, targetType, reportErrors, /*headMessage*/ undefined, intersectionState); - if (!related) { - return Ternary.False; - } - result &= related; - } - return result; - } - - function someTypeRelatedToType(source: UnionOrIntersectionType, target: Type, reportErrors: boolean, intersectionState: IntersectionState): Ternary { - const sourceTypes = source.types; - if (source.flags & TypeFlags.Union && containsType(sourceTypes, target)) { - return Ternary.True; - } - const len = sourceTypes.length; - for (let i = 0; i < len; i++) { - const related = isRelatedTo(sourceTypes[i], target, reportErrors && i === len - 1, /*headMessage*/ undefined, intersectionState); - if (related) { - return related; - } - } - return Ternary.False; - } - - function eachTypeRelatedToType(source: UnionOrIntersectionType, target: Type, reportErrors: boolean): Ternary { - let result = Ternary.True; - const sourceTypes = source.types; - for (const sourceType of sourceTypes) { - const related = isRelatedTo(sourceType, target, reportErrors); - if (!related) { - return Ternary.False; - } - result &= related; - } - return result; - } - - function typeArgumentsRelatedTo(sources: readonly Type[] = emptyArray, targets: readonly Type[] = emptyArray, variances: readonly VarianceFlags[] = emptyArray, reportErrors: boolean, intersectionState: IntersectionState): Ternary { - if (sources.length !== targets.length && relation === identityRelation) { - return Ternary.False; - } - const length = sources.length <= targets.length ? sources.length : targets.length; - let result = Ternary.True; - for (let i = 0; i < length; i++) { - // When variance information isn't available we default to covariance. This happens - // in the process of computing variance information for recursive types and when - // comparing 'this' type arguments. - const varianceFlags = i < variances.length ? variances[i] : VarianceFlags.Covariant; - const variance = varianceFlags & VarianceFlags.VarianceMask; - // We ignore arguments for independent type parameters (because they're never witnessed). - if (variance !== VarianceFlags.Independent) { - const s = sources[i]; - const t = targets[i]; - let related = Ternary.True; - if (varianceFlags & VarianceFlags.Unmeasurable) { - // Even an `Unmeasurable` variance works out without a structural check if the source and target are _identical_. - // We can't simply assume invariance, because `Unmeasurable` marks nonlinear relations, for example, a relation tained by - // the `-?` modifier in a mapped type (where, no matter how the inputs are related, the outputs still might not be) - related = relation === identityRelation ? isRelatedTo(s, t, /*reportErrors*/ false) : compareTypesIdentical(s, t); - } - else if (variance === VarianceFlags.Covariant) { - related = isRelatedTo(s, t, reportErrors, /*headMessage*/ undefined, intersectionState); - } - else if (variance === VarianceFlags.Contravariant) { - related = isRelatedTo(t, s, reportErrors, /*headMessage*/ undefined, intersectionState); - } - else if (variance === VarianceFlags.Bivariant) { - // In the bivariant case we first compare contravariantly without reporting - // errors. Then, if that doesn't succeed, we compare covariantly with error - // reporting. Thus, error elaboration will be based on the the covariant check, - // which is generally easier to reason about. - related = isRelatedTo(t, s, /*reportErrors*/ false); - if (!related) { - related = isRelatedTo(s, t, reportErrors, /*headMessage*/ undefined, intersectionState); - } - } - else { - // In the invariant case we first compare covariantly, and only when that - // succeeds do we proceed to compare contravariantly. Thus, error elaboration - // will typically be based on the covariant check. - related = isRelatedTo(s, t, reportErrors, /*headMessage*/ undefined, intersectionState); - if (related) { - related &= isRelatedTo(t, s, reportErrors, /*headMessage*/ undefined, intersectionState); - } - } - if (!related) { - return Ternary.False; - } - result &= related; - } - } - return result; - } - - // Determine if possibly recursive types are related. First, check if the result is already available in the global cache. - // Second, check if we have already started a comparison of the given two types in which case we assume the result to be true. - // Third, check if both types are part of deeply nested chains of generic type instantiations and if so assume the types are - // equal and infinitely expanding. Fourth, if we have reached a depth of 100 nested comparisons, assume we have runaway recursion - // and issue an error. Otherwise, actually compare the structure of the two types. - function recursiveTypeRelatedTo(source: Type, target: Type, reportErrors: boolean, intersectionState: IntersectionState): Ternary { - if (overflow) { - return Ternary.False; - } - const id = getRelationKey(source, target, intersectionState, relation); - const entry = relation.get(id); - if (entry !== undefined) { - if (reportErrors && entry & RelationComparisonResult.Failed && !(entry & RelationComparisonResult.Reported)) { - // We are elaborating errors and the cached result is an unreported failure. The result will be reported - // as a failure, and should be updated as a reported failure by the bottom of this function. - } - else { - if (outofbandVarianceMarkerHandler) { - // We're in the middle of variance checking - integrate any unmeasurable/unreliable flags from this cached component - const saved = entry & RelationComparisonResult.ReportsMask; - if (saved & RelationComparisonResult.ReportsUnmeasurable) { - instantiateType(source, reportUnmeasurableMarkers); - } - if (saved & RelationComparisonResult.ReportsUnreliable) { - instantiateType(source, reportUnreliableMarkers); - } - } - return entry & RelationComparisonResult.Succeeded ? Ternary.True : Ternary.False; - } - } - if (!maybeKeys) { - maybeKeys = []; - sourceStack = []; - targetStack = []; - } - else { - for (let i = 0; i < maybeCount; i++) { - // If source and target are already being compared, consider them related with assumptions - if (id === maybeKeys[i]) { - return Ternary.Maybe; - } - } - if (depth === 100) { - overflow = true; - return Ternary.False; - } - } - const maybeStart = maybeCount; - maybeKeys[maybeCount] = id; - maybeCount++; - sourceStack[depth] = source; - targetStack[depth] = target; - depth++; - const saveExpandingFlags = expandingFlags; - if (!(expandingFlags & ExpandingFlags.Source) && isDeeplyNestedType(source, sourceStack, depth)) expandingFlags |= ExpandingFlags.Source; - if (!(expandingFlags & ExpandingFlags.Target) && isDeeplyNestedType(target, targetStack, depth)) expandingFlags |= ExpandingFlags.Target; - let originalHandler: typeof outofbandVarianceMarkerHandler; - let propagatingVarianceFlags: RelationComparisonResult = 0; - if (outofbandVarianceMarkerHandler) { - originalHandler = outofbandVarianceMarkerHandler; - outofbandVarianceMarkerHandler = onlyUnreliable => { - propagatingVarianceFlags |= onlyUnreliable ? RelationComparisonResult.ReportsUnreliable : RelationComparisonResult.ReportsUnmeasurable; - return originalHandler!(onlyUnreliable); - }; - } - const result = expandingFlags !== ExpandingFlags.Both ? structuredTypeRelatedTo(source, target, reportErrors, intersectionState) : Ternary.Maybe; - if (outofbandVarianceMarkerHandler) { - outofbandVarianceMarkerHandler = originalHandler; - } - expandingFlags = saveExpandingFlags; - depth--; - if (result) { - if (result === Ternary.True || depth === 0) { - // If result is definitely true, record all maybe keys as having succeeded - for (let i = maybeStart; i < maybeCount; i++) { - relation.set(maybeKeys[i], RelationComparisonResult.Succeeded | propagatingVarianceFlags); - } - maybeCount = maybeStart; - } - } - else { - // A false result goes straight into global cache (when something is false under - // assumptions it will also be false without assumptions) - relation.set(id, (reportErrors ? RelationComparisonResult.Reported : 0) | RelationComparisonResult.Failed | propagatingVarianceFlags); - maybeCount = maybeStart; - } - return result; - } - - function structuredTypeRelatedTo(source: Type, target: Type, reportErrors: boolean, intersectionState: IntersectionState): Ternary { - const flags = source.flags & target.flags; - if (relation === identityRelation && !(flags & TypeFlags.Object)) { - if (flags & TypeFlags.Index) { - return isRelatedTo((source).type, (target).type, /*reportErrors*/ false); - } - let result = Ternary.False; - if (flags & TypeFlags.IndexedAccess) { - if (result = isRelatedTo((source).objectType, (target).objectType, /*reportErrors*/ false)) { - if (result &= isRelatedTo((source).indexType, (target).indexType, /*reportErrors*/ false)) { - return result; - } - } - } - if (flags & TypeFlags.Conditional) { - if ((source).root.isDistributive === (target).root.isDistributive) { - if (result = isRelatedTo((source).checkType, (target).checkType, /*reportErrors*/ false)) { - if (result &= isRelatedTo((source).extendsType, (target).extendsType, /*reportErrors*/ false)) { - if (result &= isRelatedTo(getTrueTypeFromConditionalType(source), getTrueTypeFromConditionalType(target), /*reportErrors*/ false)) { - if (result &= isRelatedTo(getFalseTypeFromConditionalType(source), getFalseTypeFromConditionalType(target), /*reportErrors*/ false)) { - return result; - } - } - } - } - } - } - if (flags & TypeFlags.Substitution) { - return isRelatedTo((source).substitute, (target).substitute, /*reportErrors*/ false); - } - return Ternary.False; - } - - let result: Ternary; - let originalErrorInfo: DiagnosticMessageChain | undefined; - let varianceCheckFailed = false; - const saveErrorInfo = captureErrorCalculationState(); - - // We limit alias variance probing to only object and conditional types since their alias behavior - // is more predictable than other, interned types, which may or may not have an alias depending on - // the order in which things were checked. - if (source.flags & (TypeFlags.Object | TypeFlags.Conditional) && source.aliasSymbol && - source.aliasTypeArguments && source.aliasSymbol === target.aliasSymbol && - !(source.aliasTypeArgumentsContainsMarker || target.aliasTypeArgumentsContainsMarker)) { - const variances = getAliasVariances(source.aliasSymbol); - if (variances === emptyArray) { - return Ternary.Maybe; - } - const varianceResult = relateVariances(source.aliasTypeArguments, target.aliasTypeArguments, variances, intersectionState); - if (varianceResult !== undefined) { - return varianceResult; - } - } - - if (target.flags & TypeFlags.TypeParameter) { - // A source type { [P in Q]: X } is related to a target type T if keyof T is related to Q and X is related to T[Q]. - if (getObjectFlags(source) & ObjectFlags.Mapped && isRelatedTo(getIndexType(target), getConstraintTypeFromMappedType(source))) { - if (!(getMappedTypeModifiers(source) & MappedTypeModifiers.IncludeOptional)) { - const templateType = getTemplateTypeFromMappedType(source); - const indexedAccessType = getIndexedAccessType(target, getTypeParameterFromMappedType(source)); - if (result = isRelatedTo(templateType, indexedAccessType, reportErrors)) { - return result; - } - } - } - } - else if (target.flags & TypeFlags.Index) { - // A keyof S is related to a keyof T if T is related to S. - if (source.flags & TypeFlags.Index) { - if (result = isRelatedTo((target).type, (source).type, /*reportErrors*/ false)) { - return result; - } - } - // A type S is assignable to keyof T if S is assignable to keyof C, where C is the - // simplified form of T or, if T doesn't simplify, the constraint of T. - const constraint = getSimplifiedTypeOrConstraint((target).type); - if (constraint) { - // We require Ternary.True here such that circular constraints don't cause - // false positives. For example, given 'T extends { [K in keyof T]: string }', - // 'keyof T' has itself as its constraint and produces a Ternary.Maybe when - // related to other types. - if (isRelatedTo(source, getIndexType(constraint, (target as IndexType).stringsOnly), reportErrors) === Ternary.True) { - return Ternary.True; - } - } - } - else if (target.flags & TypeFlags.IndexedAccess) { - // A type S is related to a type T[K] if S is related to C, where C is the base - // constraint of T[K] for writing. - if (relation !== identityRelation) { - const objectType = (target).objectType; - const indexType = (target).indexType; - const baseObjectType = getBaseConstraintOfType(objectType) || objectType; - const baseIndexType = getBaseConstraintOfType(indexType) || indexType; - if (!isGenericObjectType(baseObjectType) && !isGenericIndexType(baseIndexType)) { - const accessFlags = AccessFlags.Writing | (baseObjectType !== objectType ? AccessFlags.NoIndexSignatures : 0); - const constraint = getIndexedAccessTypeOrUndefined(baseObjectType, baseIndexType, /*accessNode*/ undefined, accessFlags); - if (constraint && (result = isRelatedTo(source, constraint, reportErrors))) { - return result; - } - } - } - } - else if (isGenericMappedType(target)) { - // A source type T is related to a target type { [P in X]: T[P] } - const template = getTemplateTypeFromMappedType(target); - const modifiers = getMappedTypeModifiers(target); - if (!(modifiers & MappedTypeModifiers.ExcludeOptional)) { - if (template.flags & TypeFlags.IndexedAccess && (template).objectType === source && - (template).indexType === getTypeParameterFromMappedType(target)) { - return Ternary.True; - } - if (!isGenericMappedType(source)) { - const targetConstraint = getConstraintTypeFromMappedType(target); - const sourceKeys = getIndexType(source, /*stringsOnly*/ undefined, /*noIndexSignatures*/ true); - const includeOptional = modifiers & MappedTypeModifiers.IncludeOptional; - const filteredByApplicability = includeOptional ? intersectTypes(targetConstraint, sourceKeys) : undefined; - // A source type T is related to a target type { [P in Q]: X } if Q is related to keyof T and T[Q] is related to X. - // A source type T is related to a target type { [P in Q]?: X } if some constituent Q' of Q is related to keyof T and T[Q'] is related to X. - if (includeOptional - ? !(filteredByApplicability!.flags & TypeFlags.Never) - : isRelatedTo(targetConstraint, sourceKeys)) { - const typeParameter = getTypeParameterFromMappedType(target); - const indexingType = filteredByApplicability ? getIntersectionType([filteredByApplicability, typeParameter]) : typeParameter; - const indexedAccessType = getIndexedAccessType(source, indexingType); - const templateType = getTemplateTypeFromMappedType(target); - if (result = isRelatedTo(indexedAccessType, templateType, reportErrors)) { - return result; - } - } - originalErrorInfo = errorInfo; - resetErrorInfo(saveErrorInfo); - } - } - } - - if (source.flags & TypeFlags.TypeVariable) { - if (source.flags & TypeFlags.IndexedAccess && target.flags & TypeFlags.IndexedAccess) { - // A type S[K] is related to a type T[J] if S is related to T and K is related to J. - if (result = isRelatedTo((source).objectType, (target).objectType, reportErrors)) { - result &= isRelatedTo((source).indexType, (target).indexType, reportErrors); - } - if (result) { - resetErrorInfo(saveErrorInfo); - return result; - } - } - else { - const constraint = getConstraintOfType(source); - if (!constraint || (source.flags & TypeFlags.TypeParameter && constraint.flags & TypeFlags.Any)) { - // A type variable with no constraint is not related to the non-primitive object type. - if (result = isRelatedTo(emptyObjectType, extractTypesOfKind(target, ~TypeFlags.NonPrimitive))) { - resetErrorInfo(saveErrorInfo); - return result; - } - } - // hi-speed no-this-instantiation check (less accurate, but avoids costly `this`-instantiation when the constraint will suffice), see #28231 for report on why this is needed - else if (result = isRelatedTo(constraint, target, /*reportErrors*/ false, /*headMessage*/ undefined, intersectionState)) { - resetErrorInfo(saveErrorInfo); - return result; - } - // slower, fuller, this-instantiated check (necessary when comparing raw `this` types from base classes), see `subclassWithPolymorphicThisIsAssignable.ts` test for example - else if (result = isRelatedTo(getTypeWithThisArgument(constraint, source), target, reportErrors, /*headMessage*/ undefined, intersectionState)) { - resetErrorInfo(saveErrorInfo); - return result; - } - } - } - else if (source.flags & TypeFlags.Index) { - if (result = isRelatedTo(keyofConstraintType, target, reportErrors)) { - resetErrorInfo(saveErrorInfo); - return result; - } - } - else if (source.flags & TypeFlags.Conditional) { - if (target.flags & TypeFlags.Conditional) { - // Two conditional types 'T1 extends U1 ? X1 : Y1' and 'T2 extends U2 ? X2 : Y2' are related if - // one of T1 and T2 is related to the other, U1 and U2 are identical types, X1 is related to X2, - // and Y1 is related to Y2. - const sourceParams = (source as ConditionalType).root.inferTypeParameters; - let sourceExtends = (source).extendsType; - let mapper: TypeMapper | undefined; - if (sourceParams) { - // If the source has infer type parameters, we instantiate them in the context of the target - const ctx = createInferenceContext(sourceParams, /*signature*/ undefined, InferenceFlags.None, isRelatedTo); - inferTypes(ctx.inferences, (target).extendsType, sourceExtends, InferencePriority.NoConstraints | InferencePriority.AlwaysStrict); - sourceExtends = instantiateType(sourceExtends, ctx.mapper); - mapper = ctx.mapper; - } - if (isTypeIdenticalTo(sourceExtends, (target).extendsType) && - (isRelatedTo((source).checkType, (target).checkType) || isRelatedTo((target).checkType, (source).checkType))) { - if (result = isRelatedTo(instantiateType(getTrueTypeFromConditionalType(source), mapper), getTrueTypeFromConditionalType(target), reportErrors)) { - result &= isRelatedTo(getFalseTypeFromConditionalType(source), getFalseTypeFromConditionalType(target), reportErrors); - } - if (result) { - resetErrorInfo(saveErrorInfo); - return result; - } - } - } - else { - const distributiveConstraint = getConstraintOfDistributiveConditionalType(source); - if (distributiveConstraint) { - if (result = isRelatedTo(distributiveConstraint, target, reportErrors)) { - resetErrorInfo(saveErrorInfo); - return result; - } - } - const defaultConstraint = getDefaultConstraintOfConditionalType(source); - if (defaultConstraint) { - if (result = isRelatedTo(defaultConstraint, target, reportErrors)) { - resetErrorInfo(saveErrorInfo); - return result; - } - } - } - } - else { - // An empty object type is related to any mapped type that includes a '?' modifier. - if (relation !== subtypeRelation && relation !== strictSubtypeRelation && isPartialMappedType(target) && isEmptyObjectType(source)) { - return Ternary.True; - } - if (isGenericMappedType(target)) { - if (isGenericMappedType(source)) { - if (result = mappedTypeRelatedTo(source, target, reportErrors)) { - resetErrorInfo(saveErrorInfo); - return result; - } - } - return Ternary.False; - } - const sourceIsPrimitive = !!(source.flags & TypeFlags.Primitive); - if (relation !== identityRelation) { - source = getApparentType(source); - } - else if (isGenericMappedType(source)) { - return Ternary.False; - } - if (getObjectFlags(source) & ObjectFlags.Reference && getObjectFlags(target) & ObjectFlags.Reference && (source).target === (target).target && - !(getObjectFlags(source) & ObjectFlags.MarkerType || getObjectFlags(target) & ObjectFlags.MarkerType)) { - // We have type references to the same generic type, and the type references are not marker - // type references (which are intended by be compared structurally). Obtain the variance - // information for the type parameters and relate the type arguments accordingly. - const variances = getVariances((source).target); - // We return Ternary.Maybe for a recursive invocation of getVariances (signalled by emptyArray). This - // effectively means we measure variance only from type parameter occurrences that aren't nested in - // recursive instantiations of the generic type. - if (variances === emptyArray) { - return Ternary.Maybe; - } - const varianceResult = relateVariances(getTypeArguments(source), getTypeArguments(target), variances, intersectionState); - if (varianceResult !== undefined) { - return varianceResult; - } - } - else if (isReadonlyArrayType(target) ? isArrayType(source) || isTupleType(source) : isArrayType(target) && isTupleType(source) && !source.target.readonly) { - if (relation !== identityRelation) { - return isRelatedTo(getIndexTypeOfType(source, IndexKind.Number) || anyType, getIndexTypeOfType(target, IndexKind.Number) || anyType, reportErrors); - } - else { - // By flags alone, we know that the `target` is a readonly array while the source is a normal array or tuple - // or `target` is an array and source is a tuple - in both cases the types cannot be identical, by construction - return Ternary.False; - } - } - // Consider a fresh empty object literal type "closed" under the subtype relationship - this way `{} <- {[idx: string]: any} <- fresh({})` - // and not `{} <- fresh({}) <- {[idx: string]: any}` - else if ((relation === subtypeRelation || relation === strictSubtypeRelation) && isEmptyObjectType(target) && getObjectFlags(target) & ObjectFlags.FreshLiteral && !isEmptyObjectType(source)) { - return Ternary.False; - } - // Even if relationship doesn't hold for unions, intersections, or generic type references, - // it may hold in a structural comparison. - // In a check of the form X = A & B, we will have previously checked if A relates to X or B relates - // to X. Failing both of those we want to check if the aggregation of A and B's members structurally - // relates to X. Thus, we include intersection types on the source side here. - if (source.flags & (TypeFlags.Object | TypeFlags.Intersection) && target.flags & TypeFlags.Object) { - // Report structural errors only if we haven't reported any errors yet - const reportStructuralErrors = reportErrors && errorInfo === saveErrorInfo.errorInfo && !sourceIsPrimitive; - result = propertiesRelatedTo(source, target, reportStructuralErrors, /*excludedProperties*/ undefined, intersectionState); - if (result) { - result &= signaturesRelatedTo(source, target, SignatureKind.Call, reportStructuralErrors); - if (result) { - result &= signaturesRelatedTo(source, target, SignatureKind.Construct, reportStructuralErrors); - if (result) { - result &= indexTypesRelatedTo(source, target, IndexKind.String, sourceIsPrimitive, reportStructuralErrors, intersectionState); - if (result) { - result &= indexTypesRelatedTo(source, target, IndexKind.Number, sourceIsPrimitive, reportStructuralErrors, intersectionState); - } - } - } - } - if (varianceCheckFailed && result) { - errorInfo = originalErrorInfo || errorInfo || saveErrorInfo.errorInfo; // Use variance error (there is no structural one) and return false - } - else if (result) { - return result; - } - } - // If S is an object type and T is a discriminated union, S may be related to T if - // there exists a constituent of T for every combination of the discriminants of S - // with respect to T. We do not report errors here, as we will use the existing - // error result from checking each constituent of the union. - if (source.flags & (TypeFlags.Object | TypeFlags.Intersection) && target.flags & TypeFlags.Union) { - const objectOnlyTarget = extractTypesOfKind(target, TypeFlags.Object | TypeFlags.Intersection | TypeFlags.Substitution); - if (objectOnlyTarget.flags & TypeFlags.Union) { - const result = typeRelatedToDiscriminatedType(source, objectOnlyTarget as UnionType); - if (result) { - return result; - } - } - } - } - return Ternary.False; - - function relateVariances(sourceTypeArguments: readonly Type[] | undefined, targetTypeArguments: readonly Type[] | undefined, variances: VarianceFlags[], intersectionState: IntersectionState) { - if (result = typeArgumentsRelatedTo(sourceTypeArguments, targetTypeArguments, variances, reportErrors, intersectionState)) { - return result; - } - if (some(variances, v => !!(v & VarianceFlags.AllowsStructuralFallback))) { - // If some type parameter was `Unmeasurable` or `Unreliable`, and we couldn't pass by assuming it was identical, then we - // have to allow a structural fallback check - // We elide the variance-based error elaborations, since those might not be too helpful, since we'll potentially - // be assuming identity of the type parameter. - originalErrorInfo = undefined; - resetErrorInfo(saveErrorInfo); - return undefined; - } - const allowStructuralFallback = targetTypeArguments && hasCovariantVoidArgument(targetTypeArguments, variances); - varianceCheckFailed = !allowStructuralFallback; - // The type arguments did not relate appropriately, but it may be because we have no variance - // information (in which case typeArgumentsRelatedTo defaulted to covariance for all type - // arguments). It might also be the case that the target type has a 'void' type argument for - // a covariant type parameter that is only used in return positions within the generic type - // (in which case any type argument is permitted on the source side). In those cases we proceed - // with a structural comparison. Otherwise, we know for certain the instantiations aren't - // related and we can return here. - if (variances !== emptyArray && !allowStructuralFallback) { - // In some cases generic types that are covariant in regular type checking mode become - // invariant in --strictFunctionTypes mode because one or more type parameters are used in - // both co- and contravariant positions. In order to make it easier to diagnose *why* such - // types are invariant, if any of the type parameters are invariant we reset the reported - // errors and instead force a structural comparison (which will include elaborations that - // reveal the reason). - // We can switch on `reportErrors` here, since varianceCheckFailed guarantees we return `False`, - // we can return `False` early here to skip calculating the structural error message we don't need. - if (varianceCheckFailed && !(reportErrors && some(variances, v => (v & VarianceFlags.VarianceMask) === VarianceFlags.Invariant))) { - return Ternary.False; - } - // We remember the original error information so we can restore it in case the structural - // comparison unexpectedly succeeds. This can happen when the structural comparison result - // is a Ternary.Maybe for example caused by the recursion depth limiter. - originalErrorInfo = errorInfo; - resetErrorInfo(saveErrorInfo); - } - } - } - - function reportUnmeasurableMarkers(p: TypeParameter) { - if (outofbandVarianceMarkerHandler && (p === markerSuperType || p === markerSubType || p === markerOtherType)) { - outofbandVarianceMarkerHandler(/*onlyUnreliable*/ false); - } - return p; - } - - function reportUnreliableMarkers(p: TypeParameter) { - if (outofbandVarianceMarkerHandler && (p === markerSuperType || p === markerSubType || p === markerOtherType)) { - outofbandVarianceMarkerHandler(/*onlyUnreliable*/ true); - } - return p; - } - - // A type [P in S]: X is related to a type [Q in T]: Y if T is related to S and X' is - // related to Y, where X' is an instantiation of X in which P is replaced with Q. Notice - // that S and T are contra-variant whereas X and Y are co-variant. - function mappedTypeRelatedTo(source: MappedType, target: MappedType, reportErrors: boolean): Ternary { - const modifiersRelated = relation === comparableRelation || (relation === identityRelation ? getMappedTypeModifiers(source) === getMappedTypeModifiers(target) : - getCombinedMappedTypeOptionality(source) <= getCombinedMappedTypeOptionality(target)); - if (modifiersRelated) { - let result: Ternary; - const targetConstraint = getConstraintTypeFromMappedType(target); - const sourceConstraint = instantiateType(getConstraintTypeFromMappedType(source), getCombinedMappedTypeOptionality(source) < 0 ? reportUnmeasurableMarkers : reportUnreliableMarkers); - if (result = isRelatedTo(targetConstraint, sourceConstraint, reportErrors)) { - const mapper = createTypeMapper([getTypeParameterFromMappedType(source)], [getTypeParameterFromMappedType(target)]); - return result & isRelatedTo(instantiateType(getTemplateTypeFromMappedType(source), mapper), getTemplateTypeFromMappedType(target), reportErrors); - } - } - return Ternary.False; - } - - function typeRelatedToDiscriminatedType(source: Type, target: UnionType) { - // 1. Generate the combinations of discriminant properties & types 'source' can satisfy. - // a. If the number of combinations is above a set limit, the comparison is too complex. - // 2. Filter 'target' to the subset of types whose discriminants exist in the matrix. - // a. If 'target' does not satisfy all discriminants in the matrix, 'source' is not related. - // 3. For each type in the filtered 'target', determine if all non-discriminant properties of - // 'target' are related to a property in 'source'. - // - // NOTE: See ~/tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignmentCompatWithDiscriminatedUnion.ts - // for examples. - - const sourceProperties = getPropertiesOfType(source); - const sourcePropertiesFiltered = findDiscriminantProperties(sourceProperties, target); - if (!sourcePropertiesFiltered) return Ternary.False; - - // Though we could compute the number of combinations as we generate - // the matrix, this would incur additional memory overhead due to - // array allocations. To reduce this overhead, we first compute - // the number of combinations to ensure we will not surpass our - // fixed limit before incurring the cost of any allocations: - let numCombinations = 1; - for (const sourceProperty of sourcePropertiesFiltered) { - numCombinations *= countTypes(getTypeOfSymbol(sourceProperty)); - if (numCombinations > 25) { - // We've reached the complexity limit. - return Ternary.False; - } - } - - // Compute the set of types for each discriminant property. - const sourceDiscriminantTypes: Type[][] = new Array(sourcePropertiesFiltered.length); - const excludedProperties = createUnderscoreEscapedMap(); - for (let i = 0; i < sourcePropertiesFiltered.length; i++) { - const sourceProperty = sourcePropertiesFiltered[i]; - const sourcePropertyType = getTypeOfSymbol(sourceProperty); - sourceDiscriminantTypes[i] = sourcePropertyType.flags & TypeFlags.Union - ? (sourcePropertyType as UnionType).types - : [sourcePropertyType]; - excludedProperties.set(sourceProperty.escapedName, true); - } - - // Match each combination of the cartesian product of discriminant properties to one or more - // constituents of 'target'. If any combination does not have a match then 'source' is not relatable. - const discriminantCombinations = cartesianProduct(sourceDiscriminantTypes); - const matchingTypes: Type[] = []; - for (const combination of discriminantCombinations) { - let hasMatch = false; - outer: for (const type of target.types) { - for (let i = 0; i < sourcePropertiesFiltered.length; i++) { - const sourceProperty = sourcePropertiesFiltered[i]; - const targetProperty = getPropertyOfType(type, sourceProperty.escapedName); - if (!targetProperty) continue outer; - if (sourceProperty === targetProperty) continue; - // We compare the source property to the target in the context of a single discriminant type. - const related = propertyRelatedTo(source, target, sourceProperty, targetProperty, _ => combination[i], /*reportErrors*/ false, IntersectionState.None); - // If the target property could not be found, or if the properties were not related, - // then this constituent is not a match. - if (!related) { - continue outer; - } - } - pushIfUnique(matchingTypes, type, equateValues); - hasMatch = true; - } - if (!hasMatch) { - // We failed to match any type for this combination. - return Ternary.False; - } - } - - // Compare the remaining non-discriminant properties of each match. - let result = Ternary.True; - for (const type of matchingTypes) { - result &= propertiesRelatedTo(source, type, /*reportErrors*/ false, excludedProperties, IntersectionState.None); - if (result) { - result &= signaturesRelatedTo(source, type, SignatureKind.Call, /*reportStructuralErrors*/ false); - if (result) { - result &= signaturesRelatedTo(source, type, SignatureKind.Construct, /*reportStructuralErrors*/ false); - if (result) { - result &= indexTypesRelatedTo(source, type, IndexKind.String, /*sourceIsPrimitive*/ false, /*reportStructuralErrors*/ false, IntersectionState.None); - if (result) { - result &= indexTypesRelatedTo(source, type, IndexKind.Number, /*sourceIsPrimitive*/ false, /*reportStructuralErrors*/ false, IntersectionState.None); - } - } - } - } - if (!result) { - return result; - } - } - return result; - } - - function excludeProperties(properties: Symbol[], excludedProperties: UnderscoreEscapedMap | undefined) { - if (!excludedProperties || properties.length === 0) return properties; - let result: Symbol[] | undefined; - for (let i = 0; i < properties.length; i++) { - if (!excludedProperties.has(properties[i].escapedName)) { - if (result) { - result.push(properties[i]); - } - } - else if (!result) { - result = properties.slice(0, i); - } - } - return result || properties; - } - - function isPropertySymbolTypeRelated(sourceProp: Symbol, targetProp: Symbol, getTypeOfSourceProperty: (sym: Symbol) => Type, reportErrors: boolean, intersectionState: IntersectionState): Ternary { - const targetIsOptional = strictNullChecks && !!(getCheckFlags(targetProp) & CheckFlags.Partial); - const source = getTypeOfSourceProperty(sourceProp); - if (getCheckFlags(targetProp) & CheckFlags.DeferredType && !getSymbolLinks(targetProp).type) { - // Rather than resolving (and normalizing) the type, relate constituent-by-constituent without performing normalization or seconadary passes - const links = getSymbolLinks(targetProp); - Debug.assertIsDefined(links.deferralParent); - Debug.assertIsDefined(links.deferralConstituents); - const unionParent = !!(links.deferralParent.flags & TypeFlags.Union); - let result = unionParent ? Ternary.False : Ternary.True; - const targetTypes = links.deferralConstituents; - for (const targetType of targetTypes) { - const related = isRelatedTo(source, targetType, /*reportErrors*/ false, /*headMessage*/ undefined, unionParent ? 0 : IntersectionState.Target); - if (!unionParent) { - if (!related) { - // Can't assign to a target individually - have to fallback to assigning to the _whole_ intersection (which forces normalization) - return isRelatedTo(source, addOptionality(getTypeOfSymbol(targetProp), targetIsOptional), reportErrors); - } - result &= related; - } - else { - if (related) { - return related; - } - } - } - if (unionParent && !result && targetIsOptional) { - result = isRelatedTo(source, undefinedType); - } - if (unionParent && !result && reportErrors) { - // The easiest way to get the right errors here is to un-defer (which may be costly) - // If it turns out this is too costly too often, we can replicate the error handling logic within - // typeRelatedToSomeType without the discriminatable type branch (as that requires a manifest union - // type on which to hand discriminable properties, which we are expressly trying to avoid here) - return isRelatedTo(source, addOptionality(getTypeOfSymbol(targetProp), targetIsOptional), reportErrors); - } - return result; - } - else { - return isRelatedTo(source, addOptionality(getTypeOfSymbol(targetProp), targetIsOptional), reportErrors, /*headMessage*/ undefined, intersectionState); - } - } - - function propertyRelatedTo(source: Type, target: Type, sourceProp: Symbol, targetProp: Symbol, getTypeOfSourceProperty: (sym: Symbol) => Type, reportErrors: boolean, intersectionState: IntersectionState): Ternary { - const sourcePropFlags = getDeclarationModifierFlagsFromSymbol(sourceProp); - const targetPropFlags = getDeclarationModifierFlagsFromSymbol(targetProp); - if (sourcePropFlags & ModifierFlags.Private || targetPropFlags & ModifierFlags.Private) { - const hasDifferingDeclarations = sourceProp.valueDeclaration !== targetProp.valueDeclaration; - if (getCheckFlags(sourceProp) & CheckFlags.ContainsPrivate && hasDifferingDeclarations) { - if (reportErrors) { - reportError(Diagnostics.Property_0_has_conflicting_declarations_and_is_inaccessible_in_type_1, symbolToString(sourceProp), typeToString(source)); - } - return Ternary.False; - } - if (hasDifferingDeclarations) { - if (reportErrors) { - if (sourcePropFlags & ModifierFlags.Private && targetPropFlags & ModifierFlags.Private) { - reportError(Diagnostics.Types_have_separate_declarations_of_a_private_property_0, symbolToString(targetProp)); - } - else { - reportError(Diagnostics.Property_0_is_private_in_type_1_but_not_in_type_2, symbolToString(targetProp), - typeToString(sourcePropFlags & ModifierFlags.Private ? source : target), - typeToString(sourcePropFlags & ModifierFlags.Private ? target : source)); - } - } - return Ternary.False; - } - } - else if (targetPropFlags & ModifierFlags.Protected) { - if (!isValidOverrideOf(sourceProp, targetProp)) { - if (reportErrors) { - reportError(Diagnostics.Property_0_is_protected_but_type_1_is_not_a_class_derived_from_2, symbolToString(targetProp), - typeToString(getDeclaringClass(sourceProp) || source), typeToString(getDeclaringClass(targetProp) || target)); - } - return Ternary.False; - } - } - else if (sourcePropFlags & ModifierFlags.Protected) { - if (reportErrors) { - reportError(Diagnostics.Property_0_is_protected_in_type_1_but_public_in_type_2, - symbolToString(targetProp), typeToString(source), typeToString(target)); - } - return Ternary.False; - } - // If the target comes from a partial union prop, allow `undefined` in the target type - const related = isPropertySymbolTypeRelated(sourceProp, targetProp, getTypeOfSourceProperty, reportErrors, intersectionState); - if (!related) { - if (reportErrors) { - reportIncompatibleError(Diagnostics.Types_of_property_0_are_incompatible, symbolToString(targetProp)); - } - return Ternary.False; - } - // When checking for comparability, be more lenient with optional properties. - if (relation !== comparableRelation && sourceProp.flags & SymbolFlags.Optional && !(targetProp.flags & SymbolFlags.Optional)) { - // TypeScript 1.0 spec (April 2014): 3.8.3 - // S is a subtype of a type T, and T is a supertype of S if ... - // S' and T are object types and, for each member M in T.. - // M is a property and S' contains a property N where - // if M is a required property, N is also a required property - // (M - property in T) - // (N - property in S) - if (reportErrors) { - reportError(Diagnostics.Property_0_is_optional_in_type_1_but_required_in_type_2, - symbolToString(targetProp), typeToString(source), typeToString(target)); - } - return Ternary.False; - } - return related; - } - - function reportUnmatchedProperty(source: Type, target: Type, unmatchedProperty: Symbol, requireOptionalProperties: boolean) { - let shouldSkipElaboration = false; - // give specific error in case where private names have the same description - if (unmatchedProperty.valueDeclaration - && isNamedDeclaration(unmatchedProperty.valueDeclaration) - && isPrivateIdentifier(unmatchedProperty.valueDeclaration.name) - && source.symbol - && source.symbol.flags & SymbolFlags.Class) { - const privateIdentifierDescription = unmatchedProperty.valueDeclaration.name.escapedText; - const symbolTableKey = getSymbolNameForPrivateIdentifier(source.symbol, privateIdentifierDescription); - if (symbolTableKey && getPropertyOfType(source, symbolTableKey)) { - const sourceName = getDeclarationName(source.symbol.valueDeclaration); - const targetName = getDeclarationName(target.symbol.valueDeclaration); - reportError( - Diagnostics.Property_0_in_type_1_refers_to_a_different_member_that_cannot_be_accessed_from_within_type_2, - diagnosticName(privateIdentifierDescription), - diagnosticName(sourceName.escapedText === "" ? anon : sourceName), - diagnosticName(targetName.escapedText === "" ? anon : targetName)); - return; - } - } - const props = arrayFrom(getUnmatchedProperties(source, target, requireOptionalProperties, /*matchDiscriminantProperties*/ false)); - if (!headMessage || (headMessage.code !== Diagnostics.Class_0_incorrectly_implements_interface_1.code && - headMessage.code !== Diagnostics.Class_0_incorrectly_implements_class_1_Did_you_mean_to_extend_1_and_inherit_its_members_as_a_subclass.code)) { - shouldSkipElaboration = true; // Retain top-level error for interface implementing issues, otherwise omit it - } - if (props.length === 1) { - const propName = symbolToString(unmatchedProperty); - reportError(Diagnostics.Property_0_is_missing_in_type_1_but_required_in_type_2, propName, ...getTypeNamesForErrorDisplay(source, target)); - if (length(unmatchedProperty.declarations)) { - associateRelatedInfo(createDiagnosticForNode(unmatchedProperty.declarations[0], Diagnostics._0_is_declared_here, propName)); - } - if (shouldSkipElaboration && errorInfo) { - overrideNextErrorInfo++; - } - } - else if (tryElaborateArrayLikeErrors(source, target, /*reportErrors*/ false)) { - if (props.length > 5) { // arbitrary cutoff for too-long list form - reportError(Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2_and_3_more, typeToString(source), typeToString(target), map(props.slice(0, 4), p => symbolToString(p)).join(", "), props.length - 4); - } - else { - reportError(Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2, typeToString(source), typeToString(target), map(props, p => symbolToString(p)).join(", ")); - } - if (shouldSkipElaboration && errorInfo) { - overrideNextErrorInfo++; - } - } - // No array like or unmatched property error - just issue top level error (errorInfo = undefined) + if (type.flags & TypeFlags.Void) { + context.approximateLength += 4; + return createKeywordTypeNode(SyntaxKind.VoidKeyword); } - - function propertiesRelatedTo(source: Type, target: Type, reportErrors: boolean, excludedProperties: UnderscoreEscapedMap | undefined, intersectionState: IntersectionState): Ternary { - - if (relation === identityRelation) { - return propertiesIdenticalTo(source, target, excludedProperties); - } - const requireOptionalProperties = (relation === subtypeRelation || relation === strictSubtypeRelation) && !isObjectLiteralType(source) && !isEmptyArrayLiteralType(source) && !isTupleType(source); - const unmatchedProperty = getUnmatchedProperty(source, target, requireOptionalProperties, /*matchDiscriminantProperties*/ false); - if (unmatchedProperty) { - if (reportErrors) { - reportUnmatchedProperty(source, target, unmatchedProperty, requireOptionalProperties); - } - return Ternary.False; - } - if (isObjectLiteralType(target)) { - for (const sourceProp of excludeProperties(getPropertiesOfType(source), excludedProperties)) { - if (!getPropertyOfObjectType(target, sourceProp.escapedName)) { - const sourceType = getTypeOfSymbol(sourceProp); - if (!(sourceType === undefinedType || sourceType === undefinedWideningType || sourceType === optionalType)) { - if (reportErrors) { - reportError(Diagnostics.Property_0_does_not_exist_on_type_1, symbolToString(sourceProp), typeToString(target)); - } - return Ternary.False; - } - } - } - } - let result = Ternary.True; - if (isTupleType(target)) { - const targetRestType = getRestTypeOfTupleType(target); - if (targetRestType) { - if (!isTupleType(source)) { - return Ternary.False; - } - const sourceRestType = getRestTypeOfTupleType(source); - if (sourceRestType && !isRelatedTo(sourceRestType, targetRestType, reportErrors)) { - if (reportErrors) { - reportError(Diagnostics.Rest_signatures_are_incompatible); - } - return Ternary.False; - } - const targetCount = getTypeReferenceArity(target) - 1; - const sourceCount = getTypeReferenceArity(source) - (sourceRestType ? 1 : 0); - const sourceTypeArguments = getTypeArguments(source); - for (let i = targetCount; i < sourceCount; i++) { - const related = isRelatedTo(sourceTypeArguments[i], targetRestType, reportErrors); - if (!related) { - if (reportErrors) { - reportError(Diagnostics.Property_0_is_incompatible_with_rest_element_type, "" + i); - } - return Ternary.False; - } - result &= related; - } - } - } - // We only call this for union target types when we're attempting to do excess property checking - in those cases, we want to get _all possible props_ - // from the target union, across all members - const properties = getPropertiesOfType(target); - const numericNamesOnly = isTupleType(source) && isTupleType(target); - for (const targetProp of excludeProperties(properties, excludedProperties)) { - const name = targetProp.escapedName; - if (!(targetProp.flags & SymbolFlags.Prototype) && (!numericNamesOnly || isNumericLiteralName(name) || name === "length")) { - const sourceProp = getPropertyOfType(source, name); - if (sourceProp && sourceProp !== targetProp) { - const related = propertyRelatedTo(source, target, sourceProp, targetProp, getTypeOfSymbol, reportErrors, intersectionState); - if (!related) { - return Ternary.False; - } - result &= related; - } - } - } - return result; + if (type.flags & TypeFlags.Undefined) { + context.approximateLength += 9; + return createKeywordTypeNode(SyntaxKind.UndefinedKeyword); } - - function propertiesIdenticalTo(source: Type, target: Type, excludedProperties: UnderscoreEscapedMap | undefined): Ternary { - if (!(source.flags & TypeFlags.Object && target.flags & TypeFlags.Object)) { - return Ternary.False; - } - const sourceProperties = excludeProperties(getPropertiesOfObjectType(source), excludedProperties); - const targetProperties = excludeProperties(getPropertiesOfObjectType(target), excludedProperties); - if (sourceProperties.length !== targetProperties.length) { - return Ternary.False; - } - let result = Ternary.True; - for (const sourceProp of sourceProperties) { - const targetProp = getPropertyOfObjectType(target, sourceProp.escapedName); - if (!targetProp) { - return Ternary.False; - } - const related = compareProperties(sourceProp, targetProp, isRelatedTo); - if (!related) { - return Ternary.False; - } - result &= related; - } - return result; + if (type.flags & TypeFlags.Null) { + context.approximateLength += 4; + return createKeywordTypeNode(SyntaxKind.NullKeyword); } - - function signaturesRelatedTo(source: Type, target: Type, kind: SignatureKind, reportErrors: boolean): Ternary { - if (relation === identityRelation) { - return signaturesIdenticalTo(source, target, kind); - } - if (target === anyFunctionType || source === anyFunctionType) { - return Ternary.True; - } - - const sourceIsJSConstructor = source.symbol && isJSConstructor(source.symbol.valueDeclaration); - const targetIsJSConstructor = target.symbol && isJSConstructor(target.symbol.valueDeclaration); - - const sourceSignatures = getSignaturesOfType(source, (sourceIsJSConstructor && kind === SignatureKind.Construct) ? - SignatureKind.Call : kind); - const targetSignatures = getSignaturesOfType(target, (targetIsJSConstructor && kind === SignatureKind.Construct) ? - SignatureKind.Call : kind); - - if (kind === SignatureKind.Construct && sourceSignatures.length && targetSignatures.length) { - if (isAbstractConstructorType(source) && !isAbstractConstructorType(target)) { - // An abstract constructor type is not assignable to a non-abstract constructor type - // as it would otherwise be possible to new an abstract class. Note that the assignability - // check we perform for an extends clause excludes construct signatures from the target, - // so this check never proceeds. - if (reportErrors) { - reportError(Diagnostics.Cannot_assign_an_abstract_constructor_type_to_a_non_abstract_constructor_type); - } - return Ternary.False; - } - if (!constructorVisibilitiesAreCompatible(sourceSignatures[0], targetSignatures[0], reportErrors)) { - return Ternary.False; + if (type.flags & TypeFlags.Never) { + context.approximateLength += 5; + return createKeywordTypeNode(SyntaxKind.NeverKeyword); + } + if (type.flags & TypeFlags.ESSymbol) { + context.approximateLength += 6; + return createKeywordTypeNode(SyntaxKind.SymbolKeyword); + } + if (type.flags & TypeFlags.NonPrimitive) { + context.approximateLength += 6; + return createKeywordTypeNode(SyntaxKind.ObjectKeyword); + } + if (isThisTypeParameter(type)) { + if (context.flags & NodeBuilderFlags.InObjectTypeLiteral) { + if (!context.encounteredError && !(context.flags & NodeBuilderFlags.AllowThisInObjectLiteral)) { + context.encounteredError = true; } - } - - let result = Ternary.True; - const saveErrorInfo = captureErrorCalculationState(); - const incompatibleReporter = kind === SignatureKind.Construct ? reportIncompatibleConstructSignatureReturn : reportIncompatibleCallSignatureReturn; - - if (getObjectFlags(source) & ObjectFlags.Instantiated && getObjectFlags(target) & ObjectFlags.Instantiated && source.symbol === target.symbol) { - // We have instantiations of the same anonymous type (which typically will be the type of a - // method). Simply do a pairwise comparison of the signatures in the two signature lists instead - // of the much more expensive N * M comparison matrix we explore below. We erase type parameters - // as they are known to always be the same. - for (let i = 0; i < targetSignatures.length; i++) { - const related = signatureRelatedTo(sourceSignatures[i], targetSignatures[i], /*erase*/ true, reportErrors, incompatibleReporter(sourceSignatures[i], targetSignatures[i])); - if (!related) { - return Ternary.False; - } - result &= related; + if (context.tracker.reportInaccessibleThisError) { + context.tracker.reportInaccessibleThisError(); } } - else if (sourceSignatures.length === 1 && targetSignatures.length === 1) { - // For simple functions (functions with a single signature) we only erase type parameters for - // the comparable relation. Otherwise, if the source signature is generic, we instantiate it - // in the context of the target signature before checking the relationship. Ideally we'd do - // this regardless of the number of signatures, but the potential costs are prohibitive due - // to the quadratic nature of the logic below. - const eraseGenerics = relation === comparableRelation || !!compilerOptions.noStrictGenericChecks; - result = signatureRelatedTo(sourceSignatures[0], targetSignatures[0], eraseGenerics, reportErrors, incompatibleReporter(sourceSignatures[0], targetSignatures[0])); + context.approximateLength += 4; + return createThis(); + } + if (!inTypeAlias && type.aliasSymbol && (context.flags & NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope || isTypeSymbolAccessible(type.aliasSymbol, context.enclosingDeclaration))) { + const typeArgumentNodes = mapToTypeNodes(type.aliasTypeArguments, context); + if (isReservedMemberName(type.aliasSymbol.escapedName) && !(type.aliasSymbol.flags & SymbolFlags.Class)) + return createTypeReferenceNode(createIdentifier(""), typeArgumentNodes); + return symbolToTypeNode(type.aliasSymbol, context, SymbolFlags.Type, typeArgumentNodes); + } + const objectFlags = getObjectFlags(type); + if (objectFlags & ObjectFlags.Reference) { + Debug.assert(!!(type.flags & TypeFlags.Object)); + return (type).node ? visitAndTransformType(type, typeReferenceToTypeNode) : typeReferenceToTypeNode((type)); + } + if (type.flags & TypeFlags.TypeParameter || objectFlags & ObjectFlags.ClassOrInterface) { + if (type.flags & TypeFlags.TypeParameter && contains(context.inferTypeParameters, type)) { + context.approximateLength += (symbolName(type.symbol).length + 6); + return createInferTypeNode(typeParameterToDeclarationWithConstraint((type as TypeParameter), context, /*constraintNode*/ undefined)); + } + if (context.flags & NodeBuilderFlags.GenerateNamesForShadowedTypeParams && + type.flags & TypeFlags.TypeParameter && + !isTypeSymbolAccessible(type.symbol, context.enclosingDeclaration)) { + const name = typeParameterToName(type, context); + context.approximateLength += idText(name).length; + return createTypeReferenceNode(createIdentifier(idText(name)), /*typeArguments*/ undefined); + } + // Ignore constraint/default when creating a usage (as opposed to declaration) of a type parameter. + return type.symbol + ? symbolToTypeNode(type.symbol, context, SymbolFlags.Type) + : createTypeReferenceNode(createIdentifier("?"), /*typeArguments*/ undefined); + } + if (type.flags & (TypeFlags.Union | TypeFlags.Intersection)) { + const types = type.flags & TypeFlags.Union ? formatUnionTypes((type).types) : (type).types; + if (length(types) === 1) { + return typeToTypeNodeHelper(types[0], context); + } + const typeNodes = mapToTypeNodes(types, context, /*isBareList*/ true); + if (typeNodes && typeNodes.length > 0) { + const unionOrIntersectionTypeNode = createUnionOrIntersectionTypeNode(type.flags & TypeFlags.Union ? SyntaxKind.UnionType : SyntaxKind.IntersectionType, typeNodes); + return unionOrIntersectionTypeNode; } else { - outer: for (const t of targetSignatures) { - // Only elaborate errors from the first failure - let shouldElaborateErrors = reportErrors; - for (const s of sourceSignatures) { - const related = signatureRelatedTo(s, t, /*erase*/ true, shouldElaborateErrors, incompatibleReporter(s, t)); - if (related) { - result &= related; - resetErrorInfo(saveErrorInfo); - continue outer; - } - shouldElaborateErrors = false; - } - - if (shouldElaborateErrors) { - reportError(Diagnostics.Type_0_provides_no_match_for_the_signature_1, - typeToString(source), - signatureToString(t, /*enclosingDeclaration*/ undefined, /*flags*/ undefined, kind)); - } - return Ternary.False; + if (!context.encounteredError && !(context.flags & NodeBuilderFlags.AllowEmptyUnionOrIntersection)) { + context.encounteredError = true; } + return undefined!; // TODO: GH#18217 } - return result; } - - function reportIncompatibleCallSignatureReturn(siga: Signature, sigb: Signature) { - if (siga.parameters.length === 0 && sigb.parameters.length === 0) { - return (source: Type, target: Type) => reportIncompatibleError(Diagnostics.Call_signatures_with_no_arguments_have_incompatible_return_types_0_and_1, typeToString(source), typeToString(target)); - } - return (source: Type, target: Type) => reportIncompatibleError(Diagnostics.Call_signature_return_types_0_and_1_are_incompatible, typeToString(source), typeToString(target)); + if (objectFlags & (ObjectFlags.Anonymous | ObjectFlags.Mapped)) { + Debug.assert(!!(type.flags & TypeFlags.Object)); + // The type is an object literal type. + return createAnonymousTypeNode((type)); } - - function reportIncompatibleConstructSignatureReturn(siga: Signature, sigb: Signature) { - if (siga.parameters.length === 0 && sigb.parameters.length === 0) { - return (source: Type, target: Type) => reportIncompatibleError(Diagnostics.Construct_signatures_with_no_arguments_have_incompatible_return_types_0_and_1, typeToString(source), typeToString(target)); - } - return (source: Type, target: Type) => reportIncompatibleError(Diagnostics.Construct_signature_return_types_0_and_1_are_incompatible, typeToString(source), typeToString(target)); + if (type.flags & TypeFlags.Index) { + const indexedType = (type).type; + context.approximateLength += 6; + const indexTypeNode = typeToTypeNodeHelper(indexedType, context); + return createTypeOperatorNode(indexTypeNode); } - - /** - * See signatureAssignableTo, compareSignaturesIdentical - */ - function signatureRelatedTo(source: Signature, target: Signature, erase: boolean, reportErrors: boolean, incompatibleReporter: (source: Type, target: Type) => void): Ternary { - return compareSignaturesRelated(erase ? getErasedSignature(source) : source, erase ? getErasedSignature(target) : target, - relation === strictSubtypeRelation ? SignatureCheckMode.StrictArity : 0, reportErrors, reportError, incompatibleReporter, isRelatedTo, reportUnreliableMarkers); + if (type.flags & TypeFlags.IndexedAccess) { + const objectTypeNode = typeToTypeNodeHelper((type).objectType, context); + const indexTypeNode = typeToTypeNodeHelper((type).indexType, context); + context.approximateLength += 2; + return createIndexedAccessTypeNode(objectTypeNode, indexTypeNode); } - - function signaturesIdenticalTo(source: Type, target: Type, kind: SignatureKind): Ternary { - const sourceSignatures = getSignaturesOfType(source, kind); - const targetSignatures = getSignaturesOfType(target, kind); - if (sourceSignatures.length !== targetSignatures.length) { - return Ternary.False; + if (type.flags & TypeFlags.Conditional) { + const checkTypeNode = typeToTypeNodeHelper((type).checkType, context); + const saveInferTypeParameters = context.inferTypeParameters; + context.inferTypeParameters = (type).root.inferTypeParameters; + const extendsTypeNode = typeToTypeNodeHelper((type).extendsType, context); + context.inferTypeParameters = saveInferTypeParameters; + const trueTypeNode = typeToTypeNodeHelper(getTrueTypeFromConditionalType((type)), context); + const falseTypeNode = typeToTypeNodeHelper(getFalseTypeFromConditionalType((type)), context); + context.approximateLength += 15; + return createConditionalTypeNode(checkTypeNode, extendsTypeNode, trueTypeNode, falseTypeNode); + } + if (type.flags & TypeFlags.Substitution) { + return typeToTypeNodeHelper((type).typeVariable, context); + } + return Debug.fail("Should be unreachable."); + function createMappedTypeNodeFromType(type: MappedType) { + Debug.assert(!!(type.flags & TypeFlags.Object)); + const readonlyToken = type.declaration.readonlyToken ? createToken(type.declaration.readonlyToken.kind) : undefined; + const questionToken = type.declaration.questionToken ? createToken(type.declaration.questionToken.kind) : undefined; + let appropriateConstraintTypeNode: TypeNode; + if (isMappedTypeWithKeyofConstraintDeclaration(type)) { + // We have a { [P in keyof T]: X } + // We do this to ensure we retain the toplevel keyof-ness of the type which may be lost due to keyof distribution during `getConstraintTypeFromMappedType` + appropriateConstraintTypeNode = createTypeOperatorNode(typeToTypeNodeHelper(getModifiersTypeFromMappedType(type), context)); } - let result = Ternary.True; - for (let i = 0; i < sourceSignatures.length; i++) { - const related = compareSignaturesIdentical(sourceSignatures[i], targetSignatures[i], /*partialMatch*/ false, /*ignoreThisTypes*/ false, /*ignoreReturnTypes*/ false, isRelatedTo); - if (!related) { - return Ternary.False; - } - result &= related; + else { + appropriateConstraintTypeNode = typeToTypeNodeHelper(getConstraintTypeFromMappedType(type), context); } - return result; + const typeParameterNode = typeParameterToDeclarationWithConstraint(getTypeParameterFromMappedType(type), context, appropriateConstraintTypeNode); + const templateTypeNode = typeToTypeNodeHelper(getTemplateTypeFromMappedType(type), context); + const mappedTypeNode = createMappedTypeNode(readonlyToken, typeParameterNode, questionToken, templateTypeNode); + context.approximateLength += 10; + return setEmitFlags(mappedTypeNode, EmitFlags.SingleLine); } - - function eachPropertyRelatedTo(source: Type, target: Type, kind: IndexKind, reportErrors: boolean): Ternary { - let result = Ternary.True; - const props = source.flags & TypeFlags.Intersection ? getPropertiesOfUnionOrIntersectionType(source) : getPropertiesOfObjectType(source); - for (const prop of props) { - // Skip over ignored JSX and symbol-named members - if (isIgnoredJsxProperty(source, prop)) { - continue; + function createAnonymousTypeNode(type: ObjectType): TypeNode { + const typeId = "" + type.id; + const symbol = type.symbol; + if (symbol) { + if (isJSConstructor(symbol.valueDeclaration)) { + // Instance and static types share the same symbol; only add 'typeof' for the static side. + const isInstanceType = type === getDeclaredTypeOfClassOrInterface(symbol) ? SymbolFlags.Type : SymbolFlags.Value; + return symbolToTypeNode(symbol, context, isInstanceType); + } + // Always use 'typeof T' for type of class, enum, and module objects + else if (symbol.flags & SymbolFlags.Class && !getBaseTypeVariableOfClass(symbol) && !(symbol.valueDeclaration.kind === SyntaxKind.ClassExpression && context.flags & NodeBuilderFlags.WriteClassExpressionAsTypeLiteral) || + symbol.flags & (SymbolFlags.Enum | SymbolFlags.ValueModule) || + shouldWriteTypeOfFunctionSymbol()) { + return symbolToTypeNode(symbol, context, SymbolFlags.Value); + } + else if (context.visitedTypes && context.visitedTypes.has(typeId)) { + // If type is an anonymous type literal in a type alias declaration, use type alias name + const typeAlias = getTypeAliasForTypeLiteral(type); + if (typeAlias) { + // The specified symbol flags need to be reinterpreted as type flags + return symbolToTypeNode(typeAlias, context, SymbolFlags.Type); + } + else { + return createElidedInformationPlaceholder(context); + } } - const nameType = getSymbolLinks(prop).nameType; - if (nameType && nameType.flags & TypeFlags.UniqueESSymbol) { - continue; + else { + return visitAndTransformType(type, createTypeNodeFromObjectType); } - if (kind === IndexKind.String || isNumericLiteralName(prop.escapedName)) { - const related = isRelatedTo(getTypeOfSymbol(prop), target, reportErrors); - if (!related) { - if (reportErrors) { - reportError(Diagnostics.Property_0_is_incompatible_with_index_signature, symbolToString(prop)); - } - return Ternary.False; - } - result &= related; + } + else { + // Anonymous types without a symbol are never circular. + return createTypeNodeFromObjectType(type); + } + function shouldWriteTypeOfFunctionSymbol() { + const isStaticMethodSymbol = !!(symbol.flags & SymbolFlags.Method) && // typeof static method + some(symbol.declarations, declaration => hasModifier(declaration, ModifierFlags.Static)); + const isNonLocalFunctionSymbol = !!(symbol.flags & SymbolFlags.Function) && + (symbol.parent || // is exported function symbol + forEach(symbol.declarations, declaration => declaration.parent.kind === SyntaxKind.SourceFile || declaration.parent.kind === SyntaxKind.ModuleBlock)); + if (isStaticMethodSymbol || isNonLocalFunctionSymbol) { + // typeof is allowed only for static/non local functions + return (!!(context.flags & NodeBuilderFlags.UseTypeOfFunction) || (context.visitedTypes && context.visitedTypes.has(typeId))) && // it is type of the symbol uses itself recursively + (!(context.flags & NodeBuilderFlags.UseStructuralFallback) || isValueSymbolAccessible(symbol, context.enclosingDeclaration)); // And the build is going to succeed without visibility error or there is no structural fallback allowed + } + } + } + function visitAndTransformType(type: ts.Type, transform: (type: ts.Type) => T) { + const typeId = "" + type.id; + const isConstructorObject = getObjectFlags(type) & ObjectFlags.Anonymous && type.symbol && type.symbol.flags & SymbolFlags.Class; + const id = getObjectFlags(type) & ObjectFlags.Reference && (type).node ? "N" + getNodeId(((type).node!)) : + type.symbol ? (isConstructorObject ? "+" : "") + getSymbolId(type.symbol) : + undefined; + // Since instantiations of the same anonymous type have the same symbol, tracking symbols instead + // of types allows us to catch circular references to instantiations of the same anonymous type + if (!context.visitedTypes) { + context.visitedTypes = createMap(); + } + if (id && !context.symbolDepth) { + context.symbolDepth = createMap(); + } + let depth: number | undefined; + if (id) { + depth = context.symbolDepth!.get(id) || 0; + if (depth > 10) { + return createElidedInformationPlaceholder(context); } + context.symbolDepth!.set(id, depth + 1); } - return result; - } - - function indexTypeRelatedTo(sourceType: Type, targetType: Type, reportErrors: boolean) { - const related = isRelatedTo(sourceType, targetType, reportErrors); - if (!related && reportErrors) { - reportError(Diagnostics.Index_signatures_are_incompatible); + context.visitedTypes.set(typeId, true); + const result = transform(type); + context.visitedTypes.delete(typeId); + if (id) { + context.symbolDepth!.set(id, depth!); } - return related; + return result; } - - function indexTypesRelatedTo(source: Type, target: Type, kind: IndexKind, sourceIsPrimitive: boolean, reportErrors: boolean, intersectionState: IntersectionState): Ternary { - if (relation === identityRelation) { - return indexTypesIdenticalTo(source, target, kind); + function createTypeNodeFromObjectType(type: ObjectType): TypeNode { + if (isGenericMappedType(type)) { + return createMappedTypeNodeFromType(type); + } + const resolved = resolveStructuredTypeMembers(type); + if (!resolved.properties.length && !resolved.stringIndexInfo && !resolved.numberIndexInfo) { + if (!resolved.callSignatures.length && !resolved.constructSignatures.length) { + context.approximateLength += 2; + return setEmitFlags(createTypeLiteralNode(/*members*/ undefined), EmitFlags.SingleLine); + } + if (resolved.callSignatures.length === 1 && !resolved.constructSignatures.length) { + const signature = resolved.callSignatures[0]; + const signatureNode = (signatureToSignatureDeclarationHelper(signature, SyntaxKind.FunctionType, context)); + return signatureNode; + } + if (resolved.constructSignatures.length === 1 && !resolved.callSignatures.length) { + const signature = resolved.constructSignatures[0]; + const signatureNode = (signatureToSignatureDeclarationHelper(signature, SyntaxKind.ConstructorType, context)); + return signatureNode; + } + } + const savedFlags = context.flags; + context.flags |= NodeBuilderFlags.InObjectTypeLiteral; + const members = createTypeNodesFromResolvedType(resolved); + context.flags = savedFlags; + const typeLiteralNode = createTypeLiteralNode(members); + context.approximateLength += 2; + return setEmitFlags(typeLiteralNode, (context.flags & NodeBuilderFlags.MultilineObjectLiterals) ? 0 : EmitFlags.SingleLine); + } + function typeReferenceToTypeNode(type: TypeReference) { + const typeArguments: readonly ts.Type[] = getTypeArguments(type); + if (type.target === globalArrayType || type.target === globalReadonlyArrayType) { + if (context.flags & NodeBuilderFlags.WriteArrayAsGenericType) { + const typeArgumentNode = typeToTypeNodeHelper(typeArguments[0], context); + return createTypeReferenceNode(type.target === globalArrayType ? "Array" : "ReadonlyArray", [typeArgumentNode]); + } + const elementType = typeToTypeNodeHelper(typeArguments[0], context); + const arrayType = createArrayTypeNode(elementType); + return type.target === globalArrayType ? arrayType : createTypeOperatorNode(SyntaxKind.ReadonlyKeyword, arrayType); + } + else if (type.target.objectFlags & ObjectFlags.Tuple) { + if (typeArguments.length > 0) { + const arity = getTypeReferenceArity(type); + const tupleConstituentNodes = mapToTypeNodes(typeArguments.slice(0, arity), context); + const hasRestElement = (type.target).hasRestElement; + if (tupleConstituentNodes) { + for (let i = (type.target).minLength; i < Math.min(arity, tupleConstituentNodes.length); i++) { + tupleConstituentNodes[i] = hasRestElement && i === arity - 1 ? + createRestTypeNode(createArrayTypeNode(tupleConstituentNodes[i])) : + createOptionalTypeNode(tupleConstituentNodes[i]); + } + const tupleTypeNode = createTupleTypeNode(tupleConstituentNodes); + return (type.target).readonly ? createTypeOperatorNode(SyntaxKind.ReadonlyKeyword, tupleTypeNode) : tupleTypeNode; + } + } + if (context.encounteredError || (context.flags & NodeBuilderFlags.AllowEmptyTuple)) { + const tupleTypeNode = createTupleTypeNode([]); + return (type.target).readonly ? createTypeOperatorNode(SyntaxKind.ReadonlyKeyword, tupleTypeNode) : tupleTypeNode; + } + context.encounteredError = true; + return undefined!; // TODO: GH#18217 } - const targetType = getIndexTypeOfType(target, kind); - if (!targetType || targetType.flags & TypeFlags.Any && !sourceIsPrimitive) { - // Index signature of type any permits assignment from everything but primitives - return Ternary.True; + else if (context.flags & NodeBuilderFlags.WriteClassExpressionAsTypeLiteral && + type.symbol.valueDeclaration && + isClassLike(type.symbol.valueDeclaration) && + !isValueSymbolAccessible(type.symbol, context.enclosingDeclaration)) { + return createAnonymousTypeNode(type); } - if (isGenericMappedType(source)) { - // A generic mapped type { [P in K]: T } is related to an index signature { [x: string]: U } - // if T is related to U. - return kind === IndexKind.String ? isRelatedTo(getTemplateTypeFromMappedType(source), targetType, reportErrors) : Ternary.False; - } - const indexType = getIndexTypeOfType(source, kind) || kind === IndexKind.Number && getIndexTypeOfType(source, IndexKind.String); - if (indexType) { - return indexTypeRelatedTo(indexType, targetType, reportErrors); - } - if (!(intersectionState & IntersectionState.Source) && isObjectTypeWithInferableIndex(source)) { - // Intersection constituents are never considered to have an inferred index signature - let related = eachPropertyRelatedTo(source, targetType, kind, reportErrors); - if (related && kind === IndexKind.String) { - const numberIndexType = getIndexTypeOfType(source, IndexKind.Number); - if (numberIndexType) { - related &= indexTypeRelatedTo(numberIndexType, targetType, reportErrors); + else { + const outerTypeParameters = type.target.outerTypeParameters; + let i = 0; + let resultType: TypeReferenceNode | ImportTypeNode | undefined; + if (outerTypeParameters) { + const length = outerTypeParameters.length; + while (i < length) { + // Find group of type arguments for type parameters with the same declaring container. + const start = i; + const parent = getParentSymbolOfTypeParameter(outerTypeParameters[i])!; + do { + i++; + } while (i < length && getParentSymbolOfTypeParameter(outerTypeParameters[i]) === parent); + // When type parameters are their own type arguments for the whole group (i.e. we have + // the default outer type arguments), we don't show the group. + if (!rangeEquals(outerTypeParameters, typeArguments, start, i)) { + const typeArgumentSlice = mapToTypeNodes(typeArguments.slice(start, i), context); + const flags = context.flags; + context.flags |= NodeBuilderFlags.ForbidIndexedAccessSymbolReferences; + const ref = (symbolToTypeNode(parent, context, SymbolFlags.Type, typeArgumentSlice) as TypeReferenceNode | ImportTypeNode); + context.flags = flags; + resultType = !resultType ? ref : appendReferenceToType(resultType, (ref as TypeReferenceNode)); + } } } - return related; - } - if (reportErrors) { - reportError(Diagnostics.Index_signature_is_missing_in_type_0, typeToString(source)); + let typeArgumentNodes: readonly TypeNode[] | undefined; + if (typeArguments.length > 0) { + const typeParameterCount = (type.target.typeParameters || emptyArray).length; + typeArgumentNodes = mapToTypeNodes(typeArguments.slice(i, typeParameterCount), context); + } + const flags = context.flags; + context.flags |= NodeBuilderFlags.ForbidIndexedAccessSymbolReferences; + const finalRef = symbolToTypeNode(type.symbol, context, SymbolFlags.Type, typeArgumentNodes); + context.flags = flags; + return !resultType ? finalRef : appendReferenceToType(resultType, (finalRef as TypeReferenceNode)); } - return Ternary.False; } - - function indexTypesIdenticalTo(source: Type, target: Type, indexKind: IndexKind): Ternary { - const targetInfo = getIndexInfoOfType(target, indexKind); - const sourceInfo = getIndexInfoOfType(source, indexKind); - if (!sourceInfo && !targetInfo) { - return Ternary.True; + function appendReferenceToType(root: TypeReferenceNode | ImportTypeNode, ref: TypeReferenceNode): TypeReferenceNode | ImportTypeNode { + if (isImportTypeNode(root)) { + // first shift type arguments + const innerParams = root.typeArguments; + if (root.qualifier) { + (isIdentifier(root.qualifier) ? root.qualifier : root.qualifier.right).typeArguments = innerParams; + } + root.typeArguments = ref.typeArguments; + // then move qualifiers + const ids = getAccessStack(ref); + for (const id of ids) { + root.qualifier = root.qualifier ? createQualifiedName(root.qualifier, id) : id; + } + return root; } - if (sourceInfo && targetInfo && sourceInfo.isReadonly === targetInfo.isReadonly) { - return isRelatedTo(sourceInfo.type, targetInfo.type); + else { + // first shift type arguments + const innerParams = root.typeArguments; + (isIdentifier(root.typeName) ? root.typeName : root.typeName.right).typeArguments = innerParams; + root.typeArguments = ref.typeArguments; + // then move qualifiers + const ids = getAccessStack(ref); + for (const id of ids) { + root.typeName = createQualifiedName(root.typeName, id); + } + return root; } - return Ternary.False; } - - function constructorVisibilitiesAreCompatible(sourceSignature: Signature, targetSignature: Signature, reportErrors: boolean) { - if (!sourceSignature.declaration || !targetSignature.declaration) { - return true; - } - - const sourceAccessibility = getSelectedModifierFlags(sourceSignature.declaration, ModifierFlags.NonPublicAccessibilityModifier); - const targetAccessibility = getSelectedModifierFlags(targetSignature.declaration, ModifierFlags.NonPublicAccessibilityModifier); - - // A public, protected and private signature is assignable to a private signature. - if (targetAccessibility === ModifierFlags.Private) { - return true; + function getAccessStack(ref: TypeReferenceNode): Identifier[] { + let state = ref.typeName; + const ids = []; + while (!isIdentifier(state)) { + ids.unshift(state.right); + state = state.left; } - - // A public and protected signature is assignable to a protected signature. - if (targetAccessibility === ModifierFlags.Protected && sourceAccessibility !== ModifierFlags.Private) { - return true; + ids.unshift(state); + return ids; + } + function createTypeNodesFromResolvedType(resolvedType: ResolvedType): TypeElement[] | undefined { + if (checkTruncationLength(context)) { + return [createPropertySignature(/*modifiers*/ undefined, "...", /*questionToken*/ undefined, /*type*/ undefined, /*initializer*/ undefined)]; } - - // Only a public signature is assignable to public signature. - if (targetAccessibility !== ModifierFlags.Protected && !sourceAccessibility) { - return true; + const typeElements: TypeElement[] = []; + for (const signature of resolvedType.callSignatures) { + typeElements.push((signatureToSignatureDeclarationHelper(signature, SyntaxKind.CallSignature, context))); } - - if (reportErrors) { - reportError(Diagnostics.Cannot_assign_a_0_constructor_type_to_a_1_constructor_type, visibilityToString(sourceAccessibility), visibilityToString(targetAccessibility)); + for (const signature of resolvedType.constructSignatures) { + typeElements.push((signatureToSignatureDeclarationHelper(signature, SyntaxKind.ConstructSignature, context))); } - - return false; - } - } - - function getBestMatchingType(source: Type, target: UnionOrIntersectionType, isRelatedTo = compareTypesAssignable) { - return findMatchingDiscriminantType(source, target, isRelatedTo) || - findMatchingTypeReferenceOrTypeAliasReference(source, target) || - findBestTypeForObjectLiteral(source, target) || - findBestTypeForInvokable(source, target) || - findMostOverlappyType(source, target); - } - - function discriminateTypeByDiscriminableItems(target: UnionType, discriminators: [() => Type, __String][], related: (source: Type, target: Type) => boolean | Ternary): Type | undefined; - function discriminateTypeByDiscriminableItems(target: UnionType, discriminators: [() => Type, __String][], related: (source: Type, target: Type) => boolean | Ternary, defaultValue: Type): Type; - function discriminateTypeByDiscriminableItems(target: UnionType, discriminators: [() => Type, __String][], related: (source: Type, target: Type) => boolean | Ternary, defaultValue?: Type) { - // undefined=unknown, true=discriminated, false=not discriminated - // The state of each type progresses from left to right. Discriminated types stop at 'true'. - const discriminable = target.types.map(_ => undefined) as (boolean | undefined)[]; - for (const [getDiscriminatingType, propertyName] of discriminators) { - let i = 0; - for (const type of target.types) { - const targetType = getTypeOfPropertyOfType(type, propertyName); - if (targetType && related(getDiscriminatingType(), targetType)) { - discriminable[i] = discriminable[i] === undefined ? true : discriminable[i]; + if (resolvedType.stringIndexInfo) { + let indexSignature: IndexSignatureDeclaration; + if (resolvedType.objectFlags & ObjectFlags.ReverseMapped) { + indexSignature = indexInfoToIndexSignatureDeclarationHelper(createIndexInfo(anyType, resolvedType.stringIndexInfo.isReadonly, resolvedType.stringIndexInfo.declaration), IndexKind.String, context); + indexSignature.type = createElidedInformationPlaceholder(context); } else { - discriminable[i] = false; + indexSignature = indexInfoToIndexSignatureDeclarationHelper(resolvedType.stringIndexInfo, IndexKind.String, context); } - i++; + typeElements.push(indexSignature); } - } - const match = discriminable.indexOf(/*searchElement*/ true); - // make sure exactly 1 matches before returning it - return match === -1 || discriminable.indexOf(/*searchElement*/ true, match + 1) !== -1 ? defaultValue : target.types[match]; - } - - function isFromSpreadAssignment(prop: Symbol, container: Symbol) { - return prop.valueDeclaration?.parent !== container.valueDeclaration; - } - - /** - * A type is 'weak' if it is an object type with at least one optional property - * and no required properties, call/construct signatures or index signatures - */ - function isWeakType(type: Type): boolean { - if (type.flags & TypeFlags.Object) { - const resolved = resolveStructuredTypeMembers(type); - return resolved.callSignatures.length === 0 && resolved.constructSignatures.length === 0 && - !resolved.stringIndexInfo && !resolved.numberIndexInfo && - resolved.properties.length > 0 && - every(resolved.properties, p => !!(p.flags & SymbolFlags.Optional)); - } - if (type.flags & TypeFlags.Intersection) { - return every((type).types, isWeakType); - } - return false; - } - - function hasCommonProperties(source: Type, target: Type, isComparingJsxAttributes: boolean) { - for (const prop of getPropertiesOfType(source)) { - if (isKnownProperty(target, prop.escapedName, isComparingJsxAttributes)) { - return true; + if (resolvedType.numberIndexInfo) { + typeElements.push(indexInfoToIndexSignatureDeclarationHelper(resolvedType.numberIndexInfo, IndexKind.Number, context)); } - } - return false; - } - - // Return a type reference where the source type parameter is replaced with the target marker - // type, and flag the result as a marker type reference. - function getMarkerTypeReference(type: GenericType, source: TypeParameter, target: Type) { - const result = createTypeReference(type, map(type.typeParameters, t => t === source ? target : t)); - result.objectFlags |= ObjectFlags.MarkerType; - return result; - } - - function getAliasVariances(symbol: Symbol) { - const links = getSymbolLinks(symbol); - return getVariancesWorker(links.typeParameters, links, (_links, param, marker) => { - const type = getTypeAliasInstantiation(symbol, instantiateTypes(links.typeParameters!, makeUnaryTypeMapper(param, marker))); - type.aliasTypeArgumentsContainsMarker = true; - return type; - }); - } - - // Return an array containing the variance of each type parameter. The variance is effectively - // a digest of the type comparisons that occur for each type argument when instantiations of the - // generic type are structurally compared. We infer the variance information by comparing - // instantiations of the generic type for type arguments with known relations. The function - // returns the emptyArray singleton when invoked recursively for the given generic type. - function getVariancesWorker(typeParameters: readonly TypeParameter[] = emptyArray, cache: TCache, createMarkerType: (input: TCache, param: TypeParameter, marker: Type) => Type): VarianceFlags[] { - let variances = cache.variances; - if (!variances) { - // The emptyArray singleton is used to signal a recursive invocation. - cache.variances = emptyArray; - variances = []; - for (const tp of typeParameters) { - let unmeasurable = false; - let unreliable = false; - const oldHandler = outofbandVarianceMarkerHandler; - outofbandVarianceMarkerHandler = (onlyUnreliable) => onlyUnreliable ? unreliable = true : unmeasurable = true; - // We first compare instantiations where the type parameter is replaced with - // marker types that have a known subtype relationship. From this we can infer - // invariance, covariance, contravariance or bivariance. - const typeWithSuper = createMarkerType(cache, tp, markerSuperType); - const typeWithSub = createMarkerType(cache, tp, markerSubType); - let variance = (isTypeAssignableTo(typeWithSub, typeWithSuper) ? VarianceFlags.Covariant : 0) | - (isTypeAssignableTo(typeWithSuper, typeWithSub) ? VarianceFlags.Contravariant : 0); - // If the instantiations appear to be related bivariantly it may be because the - // type parameter is independent (i.e. it isn't witnessed anywhere in the generic - // type). To determine this we compare instantiations where the type parameter is - // replaced with marker types that are known to be unrelated. - if (variance === VarianceFlags.Bivariant && isTypeAssignableTo(createMarkerType(cache, tp, markerOtherType), typeWithSuper)) { - variance = VarianceFlags.Independent; - } - outofbandVarianceMarkerHandler = oldHandler; - if (unmeasurable || unreliable) { - if (unmeasurable) { - variance |= VarianceFlags.Unmeasurable; + const properties = resolvedType.properties; + if (!properties) { + return typeElements; + } + let i = 0; + for (const propertySymbol of properties) { + i++; + if (context.flags & NodeBuilderFlags.WriteClassExpressionAsTypeLiteral) { + if (propertySymbol.flags & SymbolFlags.Prototype) { + continue; } - if (unreliable) { - variance |= VarianceFlags.Unreliable; + if (getDeclarationModifierFlagsFromSymbol(propertySymbol) & (ModifierFlags.Private | ModifierFlags.Protected) && context.tracker.reportPrivateInBaseOfClassExpression) { + context.tracker.reportPrivateInBaseOfClassExpression(unescapeLeadingUnderscores(propertySymbol.escapedName)); } } - variances.push(variance); + if (checkTruncationLength(context) && (i + 2 < properties.length - 1)) { + typeElements.push(createPropertySignature(/*modifiers*/ undefined, `... ${properties.length - i} more ...`, /*questionToken*/ undefined, /*type*/ undefined, /*initializer*/ undefined)); + addPropertyToElementList(properties[properties.length - 1], context, typeElements); + break; + } + addPropertyToElementList(propertySymbol, context, typeElements); } - cache.variances = variances; + return typeElements.length ? typeElements : undefined; } - return variances; } - - function getVariances(type: GenericType): VarianceFlags[] { - // Arrays and tuples are known to be covariant, no need to spend time computing this. - if (type === globalArrayType || type === globalReadonlyArrayType || type.objectFlags & ObjectFlags.Tuple) { - return arrayVariances; + function createElidedInformationPlaceholder(context: NodeBuilderContext) { + context.approximateLength += 3; + if (!(context.flags & NodeBuilderFlags.NoTruncation)) { + return createTypeReferenceNode(createIdentifier("..."), /*typeArguments*/ undefined); } - return getVariancesWorker(type.typeParameters, type, getMarkerTypeReference); + return createKeywordTypeNode(SyntaxKind.AnyKeyword); } - - // Return true if the given type reference has a 'void' type argument for a covariant type parameter. - // See comment at call in recursiveTypeRelatedTo for when this case matters. - function hasCovariantVoidArgument(typeArguments: readonly Type[], variances: VarianceFlags[]): boolean { - for (let i = 0; i < variances.length; i++) { - if ((variances[i] & VarianceFlags.VarianceMask) === VarianceFlags.Covariant && typeArguments[i].flags & TypeFlags.Void) { - return true; + function addPropertyToElementList(propertySymbol: ts.Symbol, context: NodeBuilderContext, typeElements: TypeElement[]) { + const propertyIsReverseMapped = !!(getCheckFlags(propertySymbol) & CheckFlags.ReverseMapped); + const propertyType = propertyIsReverseMapped && context.flags & NodeBuilderFlags.InReverseMappedType ? + anyType : getTypeOfSymbol(propertySymbol); + const saveEnclosingDeclaration = context.enclosingDeclaration; + context.enclosingDeclaration = undefined; + if (context.tracker.trackSymbol && getCheckFlags(propertySymbol) & CheckFlags.Late) { + const decl = first(propertySymbol.declarations); + if (hasLateBindableName(decl)) { + if (isBinaryExpression(decl)) { + const name = getNameOfDeclaration(decl); + if (name && isElementAccessExpression(name) && isPropertyAccessEntityNameExpression(name.argumentExpression)) { + trackComputedName(name.argumentExpression, saveEnclosingDeclaration, context); + } + } + else { + trackComputedName(decl.name.expression, saveEnclosingDeclaration, context); + } } } - return false; - } - - function isUnconstrainedTypeParameter(type: Type) { - return type.flags & TypeFlags.TypeParameter && !getConstraintOfTypeParameter(type); - } - - function isNonDeferredTypeReference(type: Type): type is TypeReference { - return !!(getObjectFlags(type) & ObjectFlags.Reference) && !(type).node; - } - - function isTypeReferenceWithGenericArguments(type: Type): boolean { - return isNonDeferredTypeReference(type) && some(getTypeArguments(type), t => isUnconstrainedTypeParameter(t) || isTypeReferenceWithGenericArguments(t)); - } - - /** - * getTypeReferenceId(A) returns "111=0-12=1" - * where A.id=111 and number.id=12 - */ - function getTypeReferenceId(type: TypeReference, typeParameters: Type[], depth = 0) { - let result = "" + type.target.id; - for (const t of getTypeArguments(type)) { - if (isUnconstrainedTypeParameter(t)) { - let index = typeParameters.indexOf(t); - if (index < 0) { - index = typeParameters.length; - typeParameters.push(t); - } - result += "=" + index; + context.enclosingDeclaration = saveEnclosingDeclaration; + const propertyName = getPropertyNameNodeForSymbol(propertySymbol, context); + context.approximateLength += (symbolName(propertySymbol).length + 1); + const optionalToken = propertySymbol.flags & SymbolFlags.Optional ? createToken(SyntaxKind.QuestionToken) : undefined; + if (propertySymbol.flags & (SymbolFlags.Function | SymbolFlags.Method) && !getPropertiesOfObjectType(propertyType).length && !isReadonlySymbol(propertySymbol)) { + const signatures = getSignaturesOfType(filterType(propertyType, t => !(t.flags & TypeFlags.Undefined)), SignatureKind.Call); + for (const signature of signatures) { + const methodDeclaration = (signatureToSignatureDeclarationHelper(signature, SyntaxKind.MethodSignature, context)); + methodDeclaration.name = propertyName; + methodDeclaration.questionToken = optionalToken; + typeElements.push(preserveCommentsOn(methodDeclaration)); } - else if (depth < 4 && isTypeReferenceWithGenericArguments(t)) { - result += "<" + getTypeReferenceId(t as TypeReference, typeParameters, depth + 1) + ">"; + } + else { + const savedFlags = context.flags; + context.flags |= propertyIsReverseMapped ? NodeBuilderFlags.InReverseMappedType : 0; + let propertyTypeNode: TypeNode; + if (propertyIsReverseMapped && !!(savedFlags & NodeBuilderFlags.InReverseMappedType)) { + propertyTypeNode = createElidedInformationPlaceholder(context); } else { - result += "-" + t.id; + propertyTypeNode = propertyType ? typeToTypeNodeHelper(propertyType, context) : createKeywordTypeNode(SyntaxKind.AnyKeyword); } + context.flags = savedFlags; + const modifiers = isReadonlySymbol(propertySymbol) ? [createToken(SyntaxKind.ReadonlyKeyword)] : undefined; + if (modifiers) { + context.approximateLength += 9; + } + const propertySignature = createPropertySignature(modifiers, propertyName, optionalToken, propertyTypeNode, + /*initializer*/ undefined); + typeElements.push(preserveCommentsOn(propertySignature)); } - return result; - } - - /** - * To improve caching, the relation key for two generic types uses the target's id plus ids of the type parameters. - * For other cases, the types ids are used. - */ - function getRelationKey(source: Type, target: Type, intersectionState: IntersectionState, relation: Map) { - if (relation === identityRelation && source.id > target.id) { - const temp = source; - source = target; - target = temp; - } - const postFix = intersectionState ? ":" + intersectionState : ""; - if (isTypeReferenceWithGenericArguments(source) && isTypeReferenceWithGenericArguments(target)) { - const typeParameters: Type[] = []; - return getTypeReferenceId(source, typeParameters) + "," + getTypeReferenceId(target, typeParameters) + postFix; - } - return source.id + "," + target.id + postFix; - } - - // Invoke the callback for each underlying property symbol of the given symbol and return the first - // value that isn't undefined. - function forEachProperty(prop: Symbol, callback: (p: Symbol) => T): T | undefined { - if (getCheckFlags(prop) & CheckFlags.Synthetic) { - for (const t of (prop).containingType!.types) { - const p = getPropertyOfType(t, prop.escapedName); - const result = p && forEachProperty(p, callback); - if (result) { - return result; + function preserveCommentsOn(node: T) { + if (some(propertySymbol.declarations, d => d.kind === SyntaxKind.JSDocPropertyTag)) { + const d = (find(propertySymbol.declarations, d => d.kind === SyntaxKind.JSDocPropertyTag)! as JSDocPropertyTag); + const commentText = d.comment; + if (commentText) { + setSyntheticLeadingComments(node, [{ kind: SyntaxKind.MultiLineCommentTrivia, text: "*\n * " + commentText.replace(/\n/g, "\n * ") + "\n ", pos: -1, end: -1, hasTrailingNewLine: true }]); } } - return undefined; + else if (propertySymbol.valueDeclaration) { + // Copy comments to node for declaration emit + setCommentRange(node, propertySymbol.valueDeclaration); + } + return node; } - return callback(prop); - } - - // Return the declaring class type of a property or undefined if property not declared in class - function getDeclaringClass(prop: Symbol) { - return prop.parent && prop.parent.flags & SymbolFlags.Class ? getDeclaredTypeOfSymbol(getParentOfSymbol(prop)!) : undefined; - } - - // Return true if some underlying source property is declared in a class that derives - // from the given base class. - function isPropertyInClassDerivedFrom(prop: Symbol, baseClass: Type | undefined) { - return forEachProperty(prop, sp => { - const sourceClass = getDeclaringClass(sp); - return sourceClass ? hasBaseType(sourceClass, baseClass) : false; - }); - } - - // Return true if source property is a valid override of protected parts of target property. - function isValidOverrideOf(sourceProp: Symbol, targetProp: Symbol) { - return !forEachProperty(targetProp, tp => getDeclarationModifierFlagsFromSymbol(tp) & ModifierFlags.Protected ? - !isPropertyInClassDerivedFrom(sourceProp, getDeclaringClass(tp)) : false); - } - - // Return true if the given class derives from each of the declaring classes of the protected - // constituents of the given property. - function isClassDerivedFromDeclaringClasses(checkClass: Type, prop: Symbol) { - return forEachProperty(prop, p => getDeclarationModifierFlagsFromSymbol(p) & ModifierFlags.Protected ? - !hasBaseType(checkClass, getDeclaringClass(p)) : false) ? undefined : checkClass; } - - // Return true if the given type is deeply nested. We consider this to be the case when structural type comparisons - // for 5 or more occurrences or instantiations of the type have been recorded on the given stack. It is possible, - // though highly unlikely, for this test to be true in a situation where a chain of instantiations is not infinitely - // expanding. Effectively, we will generate a false positive when two types are structurally equal to at least 5 - // levels, but unequal at some level beyond that. - // In addition, this will also detect when an indexed access has been chained off of 5 or more times (which is essentially - // the dual of the structural comparison), and likewise mark the type as deeply nested, potentially adding false positives - // for finite but deeply expanding indexed accesses (eg, for `Q[P1][P2][P3][P4][P5]`). - function isDeeplyNestedType(type: Type, stack: Type[], depth: number): boolean { - // We track all object types that have an associated symbol (representing the origin of the type) - if (depth >= 5 && type.flags & TypeFlags.Object && !isObjectOrArrayLiteralType(type)) { - const symbol = type.symbol; - if (symbol) { - let count = 0; - for (let i = 0; i < depth; i++) { - const t = stack[i]; - if (t.flags & TypeFlags.Object && t.symbol === symbol) { - count++; - if (count >= 5) return true; - } + function mapToTypeNodes(types: readonly ts.Type[] | undefined, context: NodeBuilderContext, isBareList?: boolean): TypeNode[] | undefined { + if (some(types)) { + if (checkTruncationLength(context)) { + if (!isBareList) { + return [createTypeReferenceNode("...", /*typeArguments*/ undefined)]; + } + else if (types.length > 2) { + return [ + typeToTypeNodeHelper(types[0], context), + createTypeReferenceNode(`... ${types.length - 2} more ...`, /*typeArguments*/ undefined), + typeToTypeNodeHelper(types[types.length - 1], context) + ]; } } - } - if (depth >= 5 && type.flags & TypeFlags.IndexedAccess) { - const root = getRootObjectTypeFromIndexedAccessChain(type); - let count = 0; - for (let i = 0; i < depth; i++) { - const t = stack[i]; - if (getRootObjectTypeFromIndexedAccessChain(t) === root) { - count++; - if (count >= 5) return true; + const result = []; + let i = 0; + for (const type of types) { + i++; + if (checkTruncationLength(context) && (i + 2 < types.length - 1)) { + result.push(createTypeReferenceNode(`... ${types.length - i} more ...`, /*typeArguments*/ undefined)); + const typeNode = typeToTypeNodeHelper(types[types.length - 1], context); + if (typeNode) { + result.push(typeNode); + } + break; + } + context.approximateLength += 2; // Account for whitespace + separator + const typeNode = typeToTypeNodeHelper(type, context); + if (typeNode) { + result.push(typeNode); } } + return result; } - return false; } - - /** - * Gets the leftmost object type in a chain of indexed accesses, eg, in A[P][Q], returns A - */ - function getRootObjectTypeFromIndexedAccessChain(type: Type) { - let t = type; - while (t.flags & TypeFlags.IndexedAccess) { - t = (t as IndexedAccessType).objectType; + function indexInfoToIndexSignatureDeclarationHelper(indexInfo: IndexInfo, kind: IndexKind, context: NodeBuilderContext): IndexSignatureDeclaration { + const name = getNameFromIndexInfo(indexInfo) || "x"; + const indexerTypeNode = createKeywordTypeNode(kind === IndexKind.String ? SyntaxKind.StringKeyword : SyntaxKind.NumberKeyword); + const indexingParameter = createParameter( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, name, + /*questionToken*/ undefined, indexerTypeNode, + /*initializer*/ undefined); + const typeNode = typeToTypeNodeHelper(indexInfo.type || anyType, context); + if (!indexInfo.type && !(context.flags & NodeBuilderFlags.AllowEmptyIndexInfoType)) { + context.encounteredError = true; + } + context.approximateLength += (name.length + 4); + return createIndexSignature( + /*decorators*/ undefined, indexInfo.isReadonly ? [createToken(SyntaxKind.ReadonlyKeyword)] : undefined, [indexingParameter], typeNode); + } + function signatureToSignatureDeclarationHelper(signature: ts.Signature, kind: SyntaxKind, context: NodeBuilderContext): SignatureDeclaration { + const suppressAny = context.flags & NodeBuilderFlags.SuppressAnyReturnType; + if (suppressAny) + context.flags &= ~NodeBuilderFlags.SuppressAnyReturnType; // suppress only toplevel `any`s + let typeParameters: TypeParameterDeclaration[] | undefined; + let typeArguments: TypeNode[] | undefined; + if (context.flags & NodeBuilderFlags.WriteTypeArgumentsOfSignature && signature.target && signature.mapper && signature.target.typeParameters) { + typeArguments = signature.target.typeParameters.map(parameter => typeToTypeNodeHelper(instantiateType(parameter, signature.mapper), context)); } - return t; - } - - function isPropertyIdenticalTo(sourceProp: Symbol, targetProp: Symbol): boolean { - return compareProperties(sourceProp, targetProp, compareTypesIdentical) !== Ternary.False; - } - - function compareProperties(sourceProp: Symbol, targetProp: Symbol, compareTypes: (source: Type, target: Type) => Ternary): Ternary { - // Two members are considered identical when - // - they are public properties with identical names, optionality, and types, - // - they are private or protected properties originating in the same declaration and having identical types - if (sourceProp === targetProp) { - return Ternary.True; + else { + typeParameters = signature.typeParameters && signature.typeParameters.map(parameter => typeParameterToDeclaration(parameter, context)); } - const sourcePropAccessibility = getDeclarationModifierFlagsFromSymbol(sourceProp) & ModifierFlags.NonPublicAccessibilityModifier; - const targetPropAccessibility = getDeclarationModifierFlagsFromSymbol(targetProp) & ModifierFlags.NonPublicAccessibilityModifier; - if (sourcePropAccessibility !== targetPropAccessibility) { - return Ternary.False; + const parameters = getExpandedParameters(signature).map(parameter => symbolToParameterDeclaration(parameter, context, kind === SyntaxKind.Constructor)); + if (signature.thisParameter) { + const thisParameter = symbolToParameterDeclaration(signature.thisParameter, context); + parameters.unshift(thisParameter); } - if (sourcePropAccessibility) { - if (getTargetSymbol(sourceProp) !== getTargetSymbol(targetProp)) { - return Ternary.False; - } + let returnTypeNode: TypeNode | undefined; + const typePredicate = getTypePredicateOfSignature(signature); + if (typePredicate) { + const assertsModifier = typePredicate.kind === TypePredicateKind.AssertsThis || typePredicate.kind === TypePredicateKind.AssertsIdentifier ? + createToken(SyntaxKind.AssertsKeyword) : + undefined; + const parameterName = typePredicate.kind === TypePredicateKind.Identifier || typePredicate.kind === TypePredicateKind.AssertsIdentifier ? + setEmitFlags(createIdentifier(typePredicate.parameterName), EmitFlags.NoAsciiEscaping) : + createThisTypeNode(); + const typeNode = typePredicate.type && typeToTypeNodeHelper(typePredicate.type, context); + returnTypeNode = createTypePredicateNodeWithModifier(assertsModifier, parameterName, typeNode); } else { - if ((sourceProp.flags & SymbolFlags.Optional) !== (targetProp.flags & SymbolFlags.Optional)) { - return Ternary.False; + const returnType = getReturnTypeOfSignature(signature); + if (returnType && !(suppressAny && isTypeAny(returnType))) { + returnTypeNode = typeToTypeNodeHelper(returnType, context); + } + else if (!suppressAny) { + returnTypeNode = createKeywordTypeNode(SyntaxKind.AnyKeyword); + } + } + context.approximateLength += 3; // Usually a signature contributes a few more characters than this, but 3 is the minimum + return createSignatureDeclaration(kind, typeParameters, parameters, returnTypeNode, typeArguments); + } + function typeParameterToDeclarationWithConstraint(type: TypeParameter, context: NodeBuilderContext, constraintNode: TypeNode | undefined): TypeParameterDeclaration { + const savedContextFlags = context.flags; + context.flags &= ~NodeBuilderFlags.WriteTypeParametersInQualifiedName; // Avoids potential infinite loop when building for a claimspace with a generic + const name = typeParameterToName(type, context); + const defaultParameter = getDefaultFromTypeParameter(type); + const defaultParameterNode = defaultParameter && typeToTypeNodeHelper(defaultParameter, context); + context.flags = savedContextFlags; + return createTypeParameterDeclaration(name, constraintNode, defaultParameterNode); + } + function typeParameterToDeclaration(type: TypeParameter, context: NodeBuilderContext, constraint = getConstraintOfTypeParameter(type)): TypeParameterDeclaration { + const constraintNode = constraint && typeToTypeNodeHelper(constraint, context); + return typeParameterToDeclarationWithConstraint(type, context, constraintNode); + } + function symbolToParameterDeclaration(parameterSymbol: ts.Symbol, context: NodeBuilderContext, preserveModifierFlags?: boolean): ParameterDeclaration { + let parameterDeclaration: ParameterDeclaration | JSDocParameterTag | undefined = getDeclarationOfKind(parameterSymbol, SyntaxKind.Parameter); + if (!parameterDeclaration && !isTransientSymbol(parameterSymbol)) { + parameterDeclaration = getDeclarationOfKind(parameterSymbol, SyntaxKind.JSDocParameterTag); + } + let parameterType = getTypeOfSymbol(parameterSymbol); + if (parameterDeclaration && isRequiredInitializedParameter(parameterDeclaration)) { + parameterType = getOptionalType(parameterType); + } + const parameterTypeNode = typeToTypeNodeHelper(parameterType, context); + const modifiers = !(context.flags & NodeBuilderFlags.OmitParameterModifiers) && preserveModifierFlags && parameterDeclaration && parameterDeclaration.modifiers ? parameterDeclaration.modifiers.map(getSynthesizedClone) : undefined; + const isRest = parameterDeclaration && isRestParameter(parameterDeclaration) || getCheckFlags(parameterSymbol) & CheckFlags.RestParameter; + const dotDotDotToken = isRest ? createToken(SyntaxKind.DotDotDotToken) : undefined; + const name = parameterDeclaration ? parameterDeclaration.name ? + parameterDeclaration.name.kind === SyntaxKind.Identifier ? setEmitFlags(getSynthesizedClone(parameterDeclaration.name), EmitFlags.NoAsciiEscaping) : + parameterDeclaration.name.kind === SyntaxKind.QualifiedName ? setEmitFlags(getSynthesizedClone(parameterDeclaration.name.right), EmitFlags.NoAsciiEscaping) : + cloneBindingName(parameterDeclaration.name) : + symbolName(parameterSymbol) : + symbolName(parameterSymbol); + const isOptional = parameterDeclaration && isOptionalParameter(parameterDeclaration) || getCheckFlags(parameterSymbol) & CheckFlags.OptionalParameter; + const questionToken = isOptional ? createToken(SyntaxKind.QuestionToken) : undefined; + const parameterNode = createParameter( + /*decorators*/ undefined, modifiers, dotDotDotToken, name, questionToken, parameterTypeNode, + /*initializer*/ undefined); + context.approximateLength += symbolName(parameterSymbol).length + 3; + return parameterNode; + function cloneBindingName(node: BindingName): BindingName { + return elideInitializerAndSetEmitFlags(node); + function elideInitializerAndSetEmitFlags(node: Node): Node { + if (context.tracker.trackSymbol && isComputedPropertyName(node) && isLateBindableName(node)) { + trackComputedName(node.expression, context.enclosingDeclaration, context); + } + const visited = (visitEachChild(node, elideInitializerAndSetEmitFlags, nullTransformationContext, /*nodesVisitor*/ undefined, elideInitializerAndSetEmitFlags)!); + const clone = nodeIsSynthesized(visited) ? visited : getSynthesizedClone(visited); + if (clone.kind === SyntaxKind.BindingElement) { + (clone).initializer = undefined; + } + return setEmitFlags(clone, EmitFlags.SingleLine | EmitFlags.NoAsciiEscaping); + } + } + } + function trackComputedName(accessExpression: EntityNameOrEntityNameExpression, enclosingDeclaration: Node | undefined, context: NodeBuilderContext) { + if (!context.tracker.trackSymbol) + return; + // get symbol of the first identifier of the entityName + const firstIdentifier = getFirstIdentifier(accessExpression); + const name = resolveName(firstIdentifier, firstIdentifier.escapedText, SymbolFlags.Value | SymbolFlags.ExportValue, /*nodeNotFoundErrorMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ true); + if (name) { + context.tracker.trackSymbol(name, enclosingDeclaration, SymbolFlags.Value); + } + } + function lookupSymbolChain(symbol: ts.Symbol, context: NodeBuilderContext, meaning: SymbolFlags, yieldModuleSymbol?: boolean) { + context.tracker.trackSymbol!(symbol, context.enclosingDeclaration, meaning); // TODO: GH#18217 + return lookupSymbolChainWorker(symbol, context, meaning, yieldModuleSymbol); + } + function lookupSymbolChainWorker(symbol: ts.Symbol, context: NodeBuilderContext, meaning: SymbolFlags, yieldModuleSymbol?: boolean) { + // Try to get qualified name if the symbol is not a type parameter and there is an enclosing declaration. + let chain: ts.Symbol[]; + const isTypeParameter = symbol.flags & SymbolFlags.TypeParameter; + if (!isTypeParameter && (context.enclosingDeclaration || context.flags & NodeBuilderFlags.UseFullyQualifiedType) && !(context.flags & NodeBuilderFlags.DoNotIncludeSymbolChain)) { + chain = Debug.checkDefined(getSymbolChain(symbol, meaning, /*endOfChain*/ true)); + Debug.assert(chain && chain.length > 0); + } + else { + chain = [symbol]; + } + return chain; + /** @param endOfChain Set to false for recursive calls; non-recursive calls should always output something. */ + function getSymbolChain(symbol: ts.Symbol, meaning: SymbolFlags, endOfChain: boolean): ts.Symbol[] | undefined { + let accessibleSymbolChain = getAccessibleSymbolChain(symbol, context.enclosingDeclaration, meaning, !!(context.flags & NodeBuilderFlags.UseOnlyExternalAliasing)); + let parentSpecifiers: (string | undefined)[]; + if (!accessibleSymbolChain || + needsQualification(accessibleSymbolChain[0], context.enclosingDeclaration, accessibleSymbolChain.length === 1 ? meaning : getQualifiedLeftMeaning(meaning))) { + // Go up and add our parent. + const parents = getContainersOfSymbol(accessibleSymbolChain ? accessibleSymbolChain[0] : symbol, context.enclosingDeclaration); + if (length(parents)) { + parentSpecifiers = parents!.map(symbol => some(symbol.declarations, hasNonGlobalAugmentationExternalModuleSymbol) + ? getSpecifierForModuleSymbol(symbol, context) + : undefined); + const indices = parents!.map((_, i) => i); + indices.sort(sortByBestName); + const sortedParents = indices.map(i => parents![i]); + for (const parent of sortedParents) { + const parentChain = getSymbolChain(parent, getQualifiedLeftMeaning(meaning), /*endOfChain*/ false); + if (parentChain) { + if (parent.exports && parent.exports.get(InternalSymbolName.ExportEquals) && + getSymbolIfSameReference((parent.exports.get(InternalSymbolName.ExportEquals)!), symbol)) { + // parentChain root _is_ symbol - symbol is a module export=, so it kinda looks like it's own parent + // No need to lookup an alias for the symbol in itself + accessibleSymbolChain = parentChain; + break; + } + accessibleSymbolChain = parentChain.concat(accessibleSymbolChain || [getAliasForSymbolInContainer(parent, symbol) || symbol]); + break; + } + } + } + } + if (accessibleSymbolChain) { + return accessibleSymbolChain; + } + if ( + // If this is the last part of outputting the symbol, always output. The cases apply only to parent symbols. + endOfChain || + // If a parent symbol is an anonymous type, don't write it. + !(symbol.flags & (SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral))) { + // If a parent symbol is an external module, don't write it. (We prefer just `x` vs `"foo/bar".x`.) + if (!endOfChain && !yieldModuleSymbol && !!forEach(symbol.declarations, hasNonGlobalAugmentationExternalModuleSymbol)) { + return; + } + return [symbol]; + } + function sortByBestName(a: number, b: number) { + const specifierA = parentSpecifiers[a]; + const specifierB = parentSpecifiers[b]; + if (specifierA && specifierB) { + const isBRelative = pathIsRelative(specifierB); + if (pathIsRelative(specifierA) === isBRelative) { + // Both relative or both non-relative, sort by number of parts + return countPathComponents(specifierA) - countPathComponents(specifierB); + } + if (isBRelative) { + // A is non-relative, B is relative: prefer A + return -1; + } + // A is relative, B is non-relative: prefer B + return 1; + } + return 0; } } - if (isReadonlySymbol(sourceProp) !== isReadonlySymbol(targetProp)) { - return Ternary.False; + } + function typeParametersToTypeParameterDeclarations(symbol: ts.Symbol, context: NodeBuilderContext) { + let typeParameterNodes: NodeArray | undefined; + const targetSymbol = getTargetSymbol(symbol); + if (targetSymbol.flags & (SymbolFlags.Class | SymbolFlags.Interface | SymbolFlags.TypeAlias)) { + typeParameterNodes = createNodeArray(map(getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol), tp => typeParameterToDeclaration(tp, context))); } - return compareTypes(getTypeOfSymbol(sourceProp), getTypeOfSymbol(targetProp)); + return typeParameterNodes; } - - function isMatchingSignature(source: Signature, target: Signature, partialMatch: boolean) { - const sourceParameterCount = getParameterCount(source); - const targetParameterCount = getParameterCount(target); - const sourceMinArgumentCount = getMinArgumentCount(source); - const targetMinArgumentCount = getMinArgumentCount(target); - const sourceHasRestParameter = hasEffectiveRestParameter(source); - const targetHasRestParameter = hasEffectiveRestParameter(target); - // A source signature matches a target signature if the two signatures have the same number of required, - // optional, and rest parameters. - if (sourceParameterCount === targetParameterCount && - sourceMinArgumentCount === targetMinArgumentCount && - sourceHasRestParameter === targetHasRestParameter) { - return true; + function lookupTypeParameterNodes(chain: ts.Symbol[], index: number, context: NodeBuilderContext) { + Debug.assert(chain && 0 <= index && index < chain.length); + const symbol = chain[index]; + const symbolId = "" + getSymbolId(symbol); + if (context.typeParameterSymbolList && context.typeParameterSymbolList.get(symbolId)) { + return undefined; } - // A source signature partially matches a target signature if the target signature has no fewer required - // parameters - if (partialMatch && sourceMinArgumentCount <= targetMinArgumentCount) { - return true; + (context.typeParameterSymbolList || (context.typeParameterSymbolList = createMap())).set(symbolId, true); + let typeParameterNodes: readonly TypeNode[] | readonly TypeParameterDeclaration[] | undefined; + if (context.flags & NodeBuilderFlags.WriteTypeParametersInQualifiedName && index < (chain.length - 1)) { + const parentSymbol = symbol; + const nextSymbol = chain[index + 1]; + if (getCheckFlags(nextSymbol) & CheckFlags.Instantiated) { + const params = getTypeParametersOfClassOrInterface(parentSymbol.flags & SymbolFlags.Alias ? resolveAlias(parentSymbol) : parentSymbol); + typeParameterNodes = mapToTypeNodes(map(params, ((nextSymbol as TransientSymbol).mapper!)), context); + } + else { + typeParameterNodes = typeParametersToTypeParameterDeclarations(symbol, context); + } } - return false; + return typeParameterNodes; } - /** - * See signatureRelatedTo, compareSignaturesIdentical + * Given A[B][C][D], finds A[B] */ - function compareSignaturesIdentical(source: Signature, target: Signature, partialMatch: boolean, ignoreThisTypes: boolean, ignoreReturnTypes: boolean, compareTypes: (s: Type, t: Type) => Ternary): Ternary { - // TODO (drosen): De-duplicate code between related functions. - if (source === target) { - return Ternary.True; + function getTopmostIndexedAccessType(top: IndexedAccessTypeNode): IndexedAccessTypeNode { + if (isIndexedAccessTypeNode(top.objectType)) { + return getTopmostIndexedAccessType(top.objectType); } - if (!(isMatchingSignature(source, target, partialMatch))) { - return Ternary.False; + return top; + } + function getSpecifierForModuleSymbol(symbol: ts.Symbol, context: NodeBuilderContext) { + const file = getDeclarationOfKind(symbol, SyntaxKind.SourceFile); + if (file && file.moduleName !== undefined) { + // Use the amd name if it is available + return file.moduleName; } - // Check that the two signatures have the same number of type parameters. - if (length(source.typeParameters) !== length(target.typeParameters)) { - return Ternary.False; + if (!file) { + if (context.tracker.trackReferencedAmbientModule) { + const ambientDecls = filter(symbol.declarations, isAmbientModule); + if (length(ambientDecls)) { + for (const decl of ambientDecls) { + context.tracker.trackReferencedAmbientModule(decl, symbol); + } + } + } + if (ambientModuleSymbolRegex.test(symbol.escapedName as string)) { + return (symbol.escapedName as string).substring(1, (symbol.escapedName as string).length - 1); + } } - // Check that type parameter constraints and defaults match. If they do, instantiate the source - // signature with the type parameters of the target signature and continue the comparison. - if (target.typeParameters) { - const mapper = createTypeMapper(source.typeParameters!, target.typeParameters); - for (let i = 0; i < target.typeParameters.length; i++) { - const s = source.typeParameters![i]; - const t = target.typeParameters[i]; - if (!(s === t || compareTypes(instantiateType(getConstraintFromTypeParameter(s), mapper) || unknownType, getConstraintFromTypeParameter(t) || unknownType) && - compareTypes(instantiateType(getDefaultFromTypeParameter(s), mapper) || unknownType, getDefaultFromTypeParameter(t) || unknownType))) { - return Ternary.False; + if (!context.enclosingDeclaration || !context.tracker.moduleResolverHost) { + // If there's no context declaration, we can't lookup a non-ambient specifier, so we just use the symbol name + if (ambientModuleSymbolRegex.test(symbol.escapedName as string)) { + return (symbol.escapedName as string).substring(1, (symbol.escapedName as string).length - 1); + } + return getSourceFileOfNode((getNonAugmentationDeclaration(symbol)!)).fileName; // A resolver may not be provided for baselines and errors - in those cases we use the fileName in full + } + const contextFile = getSourceFileOfNode(getOriginalNode(context.enclosingDeclaration)); + const links = getSymbolLinks(symbol); + let specifier = links.specifierCache && links.specifierCache.get(contextFile.path); + if (!specifier) { + const isBundle = (compilerOptions.out || compilerOptions.outFile); + // For declaration bundles, we need to generate absolute paths relative to the common source dir for imports, + // just like how the declaration emitter does for the ambient module declarations - we can easily accomplish this + // using the `baseUrl` compiler option (which we would otherwise never use in declaration emit) and a non-relative + // specifier preference + const { moduleResolverHost } = context.tracker; + const specifierCompilerOptions = isBundle ? { ...compilerOptions, baseUrl: moduleResolverHost.getCommonSourceDirectory() } : compilerOptions; + specifier = first(getModuleSpecifiers(symbol, specifierCompilerOptions, contextFile, moduleResolverHost, host.getSourceFiles(), { importModuleSpecifierPreference: isBundle ? "non-relative" : "relative" }, host.redirectTargetsMap)); + links.specifierCache = links.specifierCache || createMap(); + links.specifierCache.set(contextFile.path, specifier); + } + return specifier; + } + function symbolToTypeNode(symbol: ts.Symbol, context: NodeBuilderContext, meaning: SymbolFlags, overrideTypeArguments?: readonly TypeNode[]): TypeNode { + const chain = lookupSymbolChain(symbol, context, meaning, !(context.flags & NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope)); // If we're using aliases outside the current scope, dont bother with the module + const isTypeOf = meaning === SymbolFlags.Value; + if (some(chain[0].declarations, hasNonGlobalAugmentationExternalModuleSymbol)) { + // module is root, must use `ImportTypeNode` + const nonRootParts = chain.length > 1 ? createAccessFromSymbolChain(chain, chain.length - 1, 1) : undefined; + const typeParameterNodes = overrideTypeArguments || lookupTypeParameterNodes(chain, 0, context); + const specifier = getSpecifierForModuleSymbol(chain[0], context); + if (!(context.flags & NodeBuilderFlags.AllowNodeModulesRelativePaths) && getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs && specifier.indexOf("/node_modules/") >= 0) { + // If ultimately we can only name the symbol with a reference that dives into a `node_modules` folder, we should error + // since declaration files with these kinds of references are liable to fail when published :( + context.encounteredError = true; + if (context.tracker.reportLikelyUnsafeImportRequiredError) { + context.tracker.reportLikelyUnsafeImportRequiredError(specifier); } } - source = instantiateSignature(source, mapper, /*eraseTypeParameters*/ true); + const lit = createLiteralTypeNode(createLiteral(specifier)); + if (context.tracker.trackExternalModuleSymbolOfImportTypeNode) + context.tracker.trackExternalModuleSymbolOfImportTypeNode(chain[0]); + context.approximateLength += specifier.length + 10; // specifier + import("") + if (!nonRootParts || isEntityName(nonRootParts)) { + if (nonRootParts) { + const lastId = isIdentifier(nonRootParts) ? nonRootParts : nonRootParts.right; + lastId.typeArguments = undefined; + } + return createImportTypeNode(lit, (nonRootParts as EntityName), (typeParameterNodes as readonly TypeNode[]), isTypeOf); + } + else { + const splitNode = getTopmostIndexedAccessType(nonRootParts); + const qualifier = (splitNode.objectType as TypeReferenceNode).typeName; + return createIndexedAccessTypeNode(createImportTypeNode(lit, qualifier, (typeParameterNodes as readonly TypeNode[]), isTypeOf), splitNode.indexType); + } } - let result = Ternary.True; - if (!ignoreThisTypes) { - const sourceThisType = getThisTypeOfSignature(source); - if (sourceThisType) { - const targetThisType = getThisTypeOfSignature(target); - if (targetThisType) { - const related = compareTypes(sourceThisType, targetThisType); - if (!related) { - return Ternary.False; - } - result &= related; + const entityName = createAccessFromSymbolChain(chain, chain.length - 1, 0); + if (isIndexedAccessTypeNode(entityName)) { + return entityName; // Indexed accesses can never be `typeof` + } + if (isTypeOf) { + return createTypeQueryNode(entityName); + } + else { + const lastId = isIdentifier(entityName) ? entityName : entityName.right; + const lastTypeArgs = lastId.typeArguments; + lastId.typeArguments = undefined; + return createTypeReferenceNode(entityName, (lastTypeArgs as NodeArray)); + } + function createAccessFromSymbolChain(chain: ts.Symbol[], index: number, stopper: number): EntityName | IndexedAccessTypeNode { + const typeParameterNodes = index === (chain.length - 1) ? overrideTypeArguments : lookupTypeParameterNodes(chain, index, context); + const symbol = chain[index]; + const parent = chain[index - 1]; + let symbolName: string | undefined; + if (index === 0) { + context.flags |= NodeBuilderFlags.InInitialEntityName; + symbolName = getNameOfSymbolAsWritten(symbol, context); + context.approximateLength += (symbolName ? symbolName.length : 0) + 1; + context.flags ^= NodeBuilderFlags.InInitialEntityName; + } + else { + if (parent && getExportsOfSymbol(parent)) { + const exports = getExportsOfSymbol(parent); + forEachEntry(exports, (ex, name) => { + if (getSymbolIfSameReference(ex, symbol) && !isLateBoundName(name) && name !== InternalSymbolName.ExportEquals) { + symbolName = unescapeLeadingUnderscores(name); + return true; + } + }); + } + } + if (!symbolName) { + symbolName = getNameOfSymbolAsWritten(symbol, context); + } + context.approximateLength += symbolName.length + 1; + if (!(context.flags & NodeBuilderFlags.ForbidIndexedAccessSymbolReferences) && parent && + getMembersOfSymbol(parent) && getMembersOfSymbol(parent).get(symbol.escapedName) && + getSymbolIfSameReference(getMembersOfSymbol(parent).get(symbol.escapedName)!, symbol)) { + // Should use an indexed access + const LHS = createAccessFromSymbolChain(chain, index - 1, stopper); + if (isIndexedAccessTypeNode(LHS)) { + return createIndexedAccessTypeNode(LHS, createLiteralTypeNode(createLiteral(symbolName))); + } + else { + return createIndexedAccessTypeNode(createTypeReferenceNode(LHS, (typeParameterNodes as readonly TypeNode[])), createLiteralTypeNode(createLiteral(symbolName))); } } + const identifier = setEmitFlags(createIdentifier(symbolName, typeParameterNodes), EmitFlags.NoAsciiEscaping); + identifier.symbol = symbol; + if (index > stopper) { + const LHS = createAccessFromSymbolChain(chain, index - 1, stopper); + if (!isEntityName(LHS)) { + return Debug.fail("Impossible construct - an export of an indexed access cannot be reachable"); + } + return createQualifiedName(LHS, identifier); + } + return identifier; } - const targetLen = getParameterCount(target); - for (let i = 0; i < targetLen; i++) { - const s = getTypeAtPosition(source, i); - const t = getTypeAtPosition(target, i); - const related = compareTypes(t, s); - if (!related) { - return Ternary.False; + } + function typeParameterShadowsNameInScope(escapedName: __String, context: NodeBuilderContext) { + return !!resolveName(context.enclosingDeclaration, escapedName, SymbolFlags.Type, /*nameNotFoundArg*/ undefined, escapedName, /*isUse*/ false); + } + function typeParameterToName(type: TypeParameter, context: NodeBuilderContext) { + if (context.flags & NodeBuilderFlags.GenerateNamesForShadowedTypeParams && context.typeParameterNames) { + const cached = context.typeParameterNames.get("" + getTypeId(type)); + if (cached) { + return cached; } - result &= related; } - if (!ignoreReturnTypes) { - const sourceTypePredicate = getTypePredicateOfSignature(source); - const targetTypePredicate = getTypePredicateOfSignature(target); - result &= sourceTypePredicate || targetTypePredicate ? - compareTypePredicatesIdentical(sourceTypePredicate, targetTypePredicate, compareTypes) : - compareTypes(getReturnTypeOfSignature(source), getReturnTypeOfSignature(target)); + let result = symbolToName(type.symbol, context, SymbolFlags.Type, /*expectsIdentifier*/ true); + if (!(result.kind & SyntaxKind.Identifier)) { + return createIdentifier("(Missing type parameter)"); + } + if (context.flags & NodeBuilderFlags.GenerateNamesForShadowedTypeParams) { + const rawtext = result.escapedText as string; + let i = 0; + let text = rawtext; + while ((context.typeParameterNamesByText && context.typeParameterNamesByText.get(text)) || typeParameterShadowsNameInScope((text as __String), context)) { + i++; + text = `${rawtext}_${i}`; + } + if (text !== rawtext) { + result = createIdentifier(text, result.typeArguments); + } + (context.typeParameterNames || (context.typeParameterNames = createMap())).set("" + getTypeId(type), result); + (context.typeParameterNamesByText || (context.typeParameterNamesByText = createMap())).set((result.escapedText as string), true); } return result; } - - function compareTypePredicatesIdentical(source: TypePredicate | undefined, target: TypePredicate | undefined, compareTypes: (s: Type, t: Type) => Ternary): Ternary { - return !(source && target && typePredicateKindsMatch(source, target)) ? Ternary.False : - source.type === target.type ? Ternary.True : - source.type && target.type ? compareTypes(source.type, target.type) : - Ternary.False; - } - - function literalTypesWithSameBaseType(types: Type[]): boolean { - let commonBaseType: Type | undefined; - for (const t of types) { - const baseType = getBaseTypeOfLiteralType(t); - if (!commonBaseType) { - commonBaseType = baseType; + function symbolToName(symbol: ts.Symbol, context: NodeBuilderContext, meaning: SymbolFlags, expectsIdentifier: true): Identifier; + function symbolToName(symbol: ts.Symbol, context: NodeBuilderContext, meaning: SymbolFlags, expectsIdentifier: false): EntityName; + function symbolToName(symbol: ts.Symbol, context: NodeBuilderContext, meaning: SymbolFlags, expectsIdentifier: boolean): EntityName { + const chain = lookupSymbolChain(symbol, context, meaning); + if (expectsIdentifier && chain.length !== 1 + && !context.encounteredError + && !(context.flags & NodeBuilderFlags.AllowQualifedNameInPlaceOfIdentifier)) { + context.encounteredError = true; + } + return createEntityNameFromSymbolChain(chain, chain.length - 1); + function createEntityNameFromSymbolChain(chain: ts.Symbol[], index: number): EntityName { + const typeParameterNodes = lookupTypeParameterNodes(chain, index, context); + const symbol = chain[index]; + if (index === 0) { + context.flags |= NodeBuilderFlags.InInitialEntityName; } - if (baseType === t || baseType !== commonBaseType) { - return false; + const symbolName = getNameOfSymbolAsWritten(symbol, context); + if (index === 0) { + context.flags ^= NodeBuilderFlags.InInitialEntityName; } + const identifier = setEmitFlags(createIdentifier(symbolName, typeParameterNodes), EmitFlags.NoAsciiEscaping); + identifier.symbol = symbol; + return index > 0 ? createQualifiedName(createEntityNameFromSymbolChain(chain, index - 1), identifier) : identifier; } - return true; } - - // When the candidate types are all literal types with the same base type, return a union - // of those literal types. Otherwise, return the leftmost type for which no type to the - // right is a supertype. - function getSupertypeOrUnion(types: Type[]): Type { - return literalTypesWithSameBaseType(types) ? - getUnionType(types) : - reduceLeft(types, (s, t) => isTypeSubtypeOf(s, t) ? t : s)!; - } - - function getCommonSupertype(types: Type[]): Type { - if (!strictNullChecks) { - return getSupertypeOrUnion(types); + function symbolToExpression(symbol: ts.Symbol, context: NodeBuilderContext, meaning: SymbolFlags) { + const chain = lookupSymbolChain(symbol, context, meaning); + return createExpressionFromSymbolChain(chain, chain.length - 1); + function createExpressionFromSymbolChain(chain: ts.Symbol[], index: number): Expression { + const typeParameterNodes = lookupTypeParameterNodes(chain, index, context); + const symbol = chain[index]; + if (index === 0) { + context.flags |= NodeBuilderFlags.InInitialEntityName; + } + let symbolName = getNameOfSymbolAsWritten(symbol, context); + if (index === 0) { + context.flags ^= NodeBuilderFlags.InInitialEntityName; + } + let firstChar = symbolName.charCodeAt(0); + if (isSingleOrDoubleQuote(firstChar) && some(symbol.declarations, hasNonGlobalAugmentationExternalModuleSymbol)) { + return createLiteral(getSpecifierForModuleSymbol(symbol, context)); + } + const canUsePropertyAccess = firstChar === CharacterCodes.hash ? + symbolName.length > 1 && isIdentifierStart(symbolName.charCodeAt(1), languageVersion) : + isIdentifierStart(firstChar, languageVersion); + if (index === 0 || canUsePropertyAccess) { + const identifier = setEmitFlags(createIdentifier(symbolName, typeParameterNodes), EmitFlags.NoAsciiEscaping); + identifier.symbol = symbol; + return index > 0 ? createPropertyAccess(createExpressionFromSymbolChain(chain, index - 1), identifier) : identifier; + } + else { + if (firstChar === CharacterCodes.openBracket) { + symbolName = symbolName.substring(1, symbolName.length - 1); + firstChar = symbolName.charCodeAt(0); + } + let expression: Expression | undefined; + if (isSingleOrDoubleQuote(firstChar)) { + expression = createLiteral(symbolName.substring(1, symbolName.length - 1).replace(/\\./g, s => s.substring(1))); + (expression as StringLiteral).singleQuote = firstChar === CharacterCodes.singleQuote; + } + else if (("" + +symbolName) === symbolName) { + expression = createLiteral(+symbolName); + } + if (!expression) { + expression = setEmitFlags(createIdentifier(symbolName, typeParameterNodes), EmitFlags.NoAsciiEscaping); + expression.symbol = symbol; + } + return createElementAccess(createExpressionFromSymbolChain(chain, index - 1), expression); + } } - const primaryTypes = filter(types, t => !(t.flags & TypeFlags.Nullable)); - return primaryTypes.length ? - getNullableType(getSupertypeOrUnion(primaryTypes), getFalsyFlagsOfTypes(types) & TypeFlags.Nullable) : - getUnionType(types, UnionReduction.Subtype); - } - - // Return the leftmost type for which no type to the right is a subtype. - function getCommonSubtype(types: Type[]) { - return reduceLeft(types, (s, t) => isTypeSubtypeOf(t, s) ? t : s)!; - } - - function isArrayType(type: Type): boolean { - return !!(getObjectFlags(type) & ObjectFlags.Reference) && ((type).target === globalArrayType || (type).target === globalReadonlyArrayType); - } - - function isReadonlyArrayType(type: Type): boolean { - return !!(getObjectFlags(type) & ObjectFlags.Reference) && (type).target === globalReadonlyArrayType; - } - - function isMutableArrayOrTuple(type: Type): boolean { - return isArrayType(type) && !isReadonlyArrayType(type) || isTupleType(type) && !type.target.readonly; } - - function getElementTypeOfArrayType(type: Type): Type | undefined { - return isArrayType(type) ? getTypeArguments(type as TypeReference)[0] : undefined; - } - - function isArrayLikeType(type: Type): boolean { - // A type is array-like if it is a reference to the global Array or global ReadonlyArray type, - // or if it is not the undefined or null type and if it is assignable to ReadonlyArray - return isArrayType(type) || !(type.flags & TypeFlags.Nullable) && isTypeAssignableTo(type, anyReadonlyArrayType); - } - - function isEmptyArrayLiteralType(type: Type): boolean { - const elementType = isArrayType(type) ? getTypeArguments(type)[0] : undefined; - return elementType === undefinedWideningType || elementType === implicitNeverType; - } - - function isTupleLikeType(type: Type): boolean { - return isTupleType(type) || !!getPropertyOfType(type, "0" as __String); - } - - function isArrayOrTupleLikeType(type: Type): boolean { - return isArrayLikeType(type) || isTupleLikeType(type); + function isSingleQuotedStringNamed(d: Declaration) { + const name = getNameOfDeclaration(d); + if (name && isStringLiteral(name) && (name.singleQuote || + (!nodeIsSynthesized(name) && startsWith(getTextOfNode(name, /*includeTrivia*/ false), "'")))) { + return true; + } + return false; } - - function getTupleElementType(type: Type, index: number) { - const propType = getTypeOfPropertyOfType(type, "" + index as __String); - if (propType) { - return propType; + function getPropertyNameNodeForSymbol(symbol: ts.Symbol, context: NodeBuilderContext) { + const singleQuote = !!length(symbol.declarations) && every(symbol.declarations, isSingleQuotedStringNamed); + const fromNameType = getPropertyNameNodeForSymbolFromNameType(symbol, context, singleQuote); + if (fromNameType) { + return fromNameType; } - if (everyType(type, isTupleType)) { - return mapType(type, t => getRestTypeOfTupleType(t) || undefinedType); + if (isKnownSymbol(symbol)) { + return createComputedPropertyName(createPropertyAccess(createIdentifier("Symbol"), (symbol.escapedName as string).substr(3))); } - return undefined; - } - - function isNeitherUnitTypeNorNever(type: Type): boolean { - return !(type.flags & (TypeFlags.Unit | TypeFlags.Never)); - } - - function isUnitType(type: Type): boolean { - return !!(type.flags & TypeFlags.Unit); - } - - function isLiteralType(type: Type): boolean { - return type.flags & TypeFlags.Boolean ? true : - type.flags & TypeFlags.Union ? type.flags & TypeFlags.EnumLiteral ? true : every((type).types, isUnitType) : - isUnitType(type); - } - - function getBaseTypeOfLiteralType(type: Type): Type { - return type.flags & TypeFlags.EnumLiteral ? getBaseTypeOfEnumLiteralType(type) : - type.flags & TypeFlags.StringLiteral ? stringType : - type.flags & TypeFlags.NumberLiteral ? numberType : - type.flags & TypeFlags.BigIntLiteral ? bigintType : - type.flags & TypeFlags.BooleanLiteral ? booleanType : - type.flags & TypeFlags.Union ? getUnionType(sameMap((type).types, getBaseTypeOfLiteralType)) : - type; - } - - function getWidenedLiteralType(type: Type): Type { - return type.flags & TypeFlags.EnumLiteral && isFreshLiteralType(type) ? getBaseTypeOfEnumLiteralType(type) : - type.flags & TypeFlags.StringLiteral && isFreshLiteralType(type) ? stringType : - type.flags & TypeFlags.NumberLiteral && isFreshLiteralType(type) ? numberType : - type.flags & TypeFlags.BigIntLiteral && isFreshLiteralType(type) ? bigintType : - type.flags & TypeFlags.BooleanLiteral && isFreshLiteralType(type) ? booleanType : - type.flags & TypeFlags.Union ? getUnionType(sameMap((type).types, getWidenedLiteralType)) : - type; - } - - function getWidenedUniqueESSymbolType(type: Type): Type { - return type.flags & TypeFlags.UniqueESSymbol ? esSymbolType : - type.flags & TypeFlags.Union ? getUnionType(sameMap((type).types, getWidenedUniqueESSymbolType)) : - type; + const rawName = unescapeLeadingUnderscores(symbol.escapedName); + return createPropertyNameNodeForIdentifierOrLiteral(rawName, singleQuote); } - - function getWidenedLiteralLikeTypeForContextualType(type: Type, contextualType: Type | undefined) { - if (!isLiteralOfContextualType(type, contextualType)) { - type = getWidenedUniqueESSymbolType(getWidenedLiteralType(type)); + // See getNameForSymbolFromNameType for a stringy equivalent + function getPropertyNameNodeForSymbolFromNameType(symbol: ts.Symbol, context: NodeBuilderContext, singleQuote?: boolean) { + const nameType = getSymbolLinks(symbol).nameType; + if (nameType) { + if (nameType.flags & TypeFlags.StringOrNumberLiteral) { + const name = "" + (nameType).value; + if (!isIdentifierText(name, compilerOptions.target) && !isNumericLiteralName(name)) { + return createLiteral(name, !!singleQuote); + } + if (isNumericLiteralName(name) && startsWith(name, "-")) { + return createComputedPropertyName(createLiteral(+name)); + } + return createPropertyNameNodeForIdentifierOrLiteral(name); + } + if (nameType.flags & TypeFlags.UniqueESSymbol) { + return createComputedPropertyName(symbolToExpression((nameType).symbol, context, SymbolFlags.Value)); + } + } + } + function createPropertyNameNodeForIdentifierOrLiteral(name: string, singleQuote?: boolean) { + return isIdentifierText(name, compilerOptions.target) ? createIdentifier(name) : createLiteral(isNumericLiteralName(name) ? +name : name, !!singleQuote); + } + function cloneNodeBuilderContext(context: NodeBuilderContext): NodeBuilderContext { + const initial: NodeBuilderContext = { ...context }; + // Make type parameters created within this context not consume the name outside this context + // The symbol serializer ends up creating many sibling scopes that all need "separate" contexts when + // it comes to naming things - within a normal `typeToTypeNode` call, the node builder only ever descends + // through the type tree, so the only cases where we could have used distinct sibling scopes was when there + // were multiple generic overloads with similar generated type parameter names + // The effect: + // When we write out + // export const x: (x: T) => T + // export const y: (x: T) => T + // we write it out like that, rather than as + // export const x: (x: T) => T + // export const y: (x: T_1) => T_1 + if (initial.typeParameterNames) { + initial.typeParameterNames = cloneMap(initial.typeParameterNames); + } + if (initial.typeParameterNamesByText) { + initial.typeParameterNamesByText = cloneMap(initial.typeParameterNamesByText); + } + if (initial.typeParameterSymbolList) { + initial.typeParameterSymbolList = cloneMap(initial.typeParameterSymbolList); + } + return initial; + } + function symbolTableToDeclarationStatements(symbolTable: SymbolTable, context: NodeBuilderContext, bundled?: boolean): Statement[] { + const serializePropertySymbolForClass = makeSerializePropertySymbol(createProperty, SyntaxKind.MethodDeclaration, /*useAcessors*/ true); + const serializePropertySymbolForInterfaceWorker = makeSerializePropertySymbol((_decorators, mods, name, question, type, initializer) => createPropertySignature(mods, name, question, type, initializer), SyntaxKind.MethodSignature, /*useAcessors*/ false); + // TODO: Use `setOriginalNode` on original declaration names where possible so these declarations see some kind of + // declaration mapping + // We save the enclosing declaration off here so it's not adjusted by well-meaning declaration + // emit codepaths which want to apply more specific contexts (so we can still refer to the root real declaration + // we're trying to emit from later on) + const enclosingDeclaration = context.enclosingDeclaration!; + let results: Statement[] = []; + const visitedSymbols: ts.Map = createMap(); + let deferredPrivates: ts.Map | undefined; + const oldcontext = context; + context = { + ...oldcontext, + usedSymbolNames: createMap(), + remappedSymbolNames: createMap(), + tracker: { + ...oldcontext.tracker, + trackSymbol: (sym, decl, meaning) => { + const accessibleResult = isSymbolAccessible(sym, decl, meaning, /*computeALiases*/ false); + if (accessibleResult.accessibility === SymbolAccessibility.Accessible) { + // Lookup the root symbol of the chain of refs we'll use to access it and serialize it + const chain = lookupSymbolChainWorker(sym, context, meaning); + if (!(sym.flags & SymbolFlags.Property)) { + includePrivateSymbol(chain[0]); + } + } + else if (oldcontext.tracker && oldcontext.tracker.trackSymbol) { + oldcontext.tracker.trackSymbol(sym, decl, meaning); + } + } + } + }; + if (oldcontext.usedSymbolNames) { + oldcontext.usedSymbolNames.forEach((_, name) => { + context.usedSymbolNames!.set(name, true); + }); } - return type; - } - - function getWidenedLiteralLikeTypeForContextualReturnTypeIfNeeded(type: Type | undefined, contextualSignatureReturnType: Type | undefined, isAsync: boolean) { - if (type && isUnitType(type)) { - const contextualType = !contextualSignatureReturnType ? undefined : - isAsync ? getPromisedTypeOfPromise(contextualSignatureReturnType) : - contextualSignatureReturnType; - type = getWidenedLiteralLikeTypeForContextualType(type, contextualType); + forEachEntry(symbolTable, (symbol, name) => { + const baseName = unescapeLeadingUnderscores(name); + void getInternalSymbolName(symbol, baseName); // Called to cache values into `usedSymbolNames` and `remappedSymbolNames` + }); + let addingDeclare = !bundled; + const exportEquals = symbolTable.get(InternalSymbolName.ExportEquals); + if (exportEquals && symbolTable.size > 1 && exportEquals.flags & SymbolFlags.Alias) { + symbolTable = createSymbolTable(); + // Remove extraneous elements from root symbol table (they'll be mixed back in when the target of the `export=` is looked up) + symbolTable.set(InternalSymbolName.ExportEquals, exportEquals); + } + visitSymbolTable(symbolTable); + return mergeRedundantStatements(results); + function isIdentifierAndNotUndefined(node: Node | undefined): node is Identifier { + return !!node && node.kind === SyntaxKind.Identifier; + } + function getNamesOfDeclaration(statement: Statement): Identifier[] { + if (isVariableStatement(statement)) { + return filter(map(statement.declarationList.declarations, getNameOfDeclaration), isIdentifierAndNotUndefined); + } + return filter([getNameOfDeclaration((statement as DeclarationStatement))], isIdentifierAndNotUndefined); + } + function flattenExportAssignedNamespace(statements: Statement[]) { + const exportAssignment = find(statements, isExportAssignment); + const ns = find(statements, isModuleDeclaration); + if (ns && exportAssignment && exportAssignment.isExportEquals && + isIdentifier(exportAssignment.expression) && isIdentifier(ns.name) && idText(ns.name) === idText(exportAssignment.expression) && + ns.body && isModuleBlock(ns.body)) { + // Pass 0: Correct situations where a module has both an `export = ns` and multiple top-level exports by stripping the export modifiers from + // the top-level exports and exporting them in the targeted ns, as can occur when a js file has both typedefs and `module.export` assignments + const excessExports = filter(statements, s => !!(getModifierFlags(s) & ModifierFlags.Export)); + if (length(excessExports)) { + ns.body.statements = createNodeArray([...ns.body.statements, createExportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, createNamedExports(map(flatMap(excessExports, e => getNamesOfDeclaration(e)), id => createExportSpecifier(/*alias*/ undefined, id))), + /*moduleSpecifier*/ undefined)]); + } + // Pass 1: Flatten `export namespace _exports {} export = _exports;` so long as the `export=` only points at a single namespace declaration + if (!find(statements, s => s !== ns && nodeHasName(s, (ns.name as Identifier)))) { + results = []; + forEach(ns.body.statements, s => { + addResult(s, ModifierFlags.None); // Recalculates the ambient (and export, if applicable from above) flag + }); + statements = [...filter(statements, s => s !== ns && s !== exportAssignment), ...results]; + } + } + return statements; + } + function mergeExportDeclarations(statements: Statement[]) { + // Pass 2: Combine all `export {}` declarations + const exports = (filter(statements, d => isExportDeclaration(d) && !d.moduleSpecifier && !!d.exportClause && isNamedExports(d.exportClause)) as ExportDeclaration[]); + if (length(exports) > 1) { + const nonExports = filter(statements, d => !isExportDeclaration(d) || !!d.moduleSpecifier || !d.exportClause); + statements = [...nonExports, createExportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, createNamedExports(flatMap(exports, e => cast(e.exportClause, isNamedExports).elements)), + /*moduleSpecifier*/ undefined)]; + } + // Pass 2b: Also combine all `export {} from "..."` declarations as needed + const reexports = (filter(statements, d => isExportDeclaration(d) && !!d.moduleSpecifier && !!d.exportClause && isNamedExports(d.exportClause)) as ExportDeclaration[]); + if (length(reexports) > 1) { + const groups = group(reexports, decl => isStringLiteral((decl.moduleSpecifier!)) ? ">" + decl.moduleSpecifier.text : ">"); + if (groups.length !== reexports.length) { + for (const group of groups) { + if (group.length > 1) { + // remove group members from statements and then merge group members and add back to statements + statements = [ + ...filter(statements, s => group.indexOf((s as ExportDeclaration)) === -1), + createExportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, createNamedExports(flatMap(group, e => cast(e.exportClause, isNamedExports).elements)), group[0].moduleSpecifier) + ]; + } + } + } + } + return statements; } - return type; - } - - function getWidenedLiteralLikeTypeForContextualIterationTypeIfNeeded(type: Type | undefined, contextualSignatureReturnType: Type | undefined, kind: IterationTypeKind, isAsyncGenerator: boolean) { - if (type && isUnitType(type)) { - const contextualType = !contextualSignatureReturnType ? undefined : - getIterationTypeOfGeneratorFunctionReturnType(kind, contextualSignatureReturnType, isAsyncGenerator); - type = getWidenedLiteralLikeTypeForContextualType(type, contextualType); + function inlineExportModifiers(statements: Statement[]) { + // Pass 3: Move all `export {}`'s to `export` modifiers where possible + const exportDecl = (find(statements, d => isExportDeclaration(d) && !d.moduleSpecifier && !!d.exportClause) as ExportDeclaration | undefined); + if (exportDecl && exportDecl.exportClause && isNamedExports(exportDecl.exportClause)) { + const replacements = mapDefined(exportDecl.exportClause.elements, e => { + if (!e.propertyName) { + // export {name} - look thru `statements` for `name`, and if all results can take an `export` modifier, do so and filter it + const associated = filter(statements, s => nodeHasName(s, e.name)); + if (length(associated) && every(associated, canHaveExportModifier)) { + forEach(associated, addExportModifier); + return undefined; + } + } + return e; + }); + if (!length(replacements)) { + // all clauses removed, filter the export declaration + statements = filter(statements, s => s !== exportDecl); + } + else { + // some items filtered, others not - update the export declaration + // (mutating because why not, we're building a whole new tree here anyway) + exportDecl.exportClause.elements = createNodeArray(replacements); + } + } + return statements; + } + function mergeRedundantStatements(statements: Statement[]) { + statements = flattenExportAssignedNamespace(statements); + statements = mergeExportDeclarations(statements); + statements = inlineExportModifiers(statements); + // Not a cleanup, but as a final step: If there is a mix of `export` and non-`export` declarations, but no `export =` or `export {}` add a `export {};` so + // declaration privacy is respected. + if (enclosingDeclaration && + ((isSourceFile(enclosingDeclaration) && isExternalOrCommonJsModule(enclosingDeclaration)) || isModuleDeclaration(enclosingDeclaration)) && + (!some(statements, isExternalModuleIndicator) || (!hasScopeMarker(statements) && some(statements, needsScopeMarker)))) { + statements.push(createEmptyExports()); + } + return statements; + } + function canHaveExportModifier(node: Statement) { + return isEnumDeclaration(node) || + isVariableStatement(node) || + isFunctionDeclaration(node) || + isClassDeclaration(node) || + (isModuleDeclaration(node) && !isExternalModuleAugmentation(node) && !isGlobalScopeAugmentation(node)) || + isInterfaceDeclaration(node) || + isTypeDeclaration(node); + } + function addExportModifier(statement: Statement) { + const flags = (getModifierFlags(statement) | ModifierFlags.Export) & ~ModifierFlags.Ambient; + statement.modifiers = createNodeArray(createModifiersFromModifierFlags(flags)); + statement.modifierFlagsCache = 0; + } + function visitSymbolTable(symbolTable: SymbolTable, suppressNewPrivateContext?: boolean, propertyAsAlias?: boolean) { + const oldDeferredPrivates = deferredPrivates; + if (!suppressNewPrivateContext) { + deferredPrivates = createMap(); + } + symbolTable.forEach((symbol: ts.Symbol) => { + serializeSymbol(symbol, /*isPrivate*/ false, !!propertyAsAlias); + }); + if (!suppressNewPrivateContext) { + // deferredPrivates will be filled up by visiting the symbol table + // And will continue to iterate as elements are added while visited `deferredPrivates` + // (As that's how a map iterator is defined to work) + deferredPrivates!.forEach((symbol: ts.Symbol) => { + serializeSymbol(symbol, /*isPrivate*/ true, !!propertyAsAlias); + }); + } + deferredPrivates = oldDeferredPrivates; + } + function serializeSymbol(symbol: ts.Symbol, isPrivate: boolean, propertyAsAlias: boolean) { + // cache visited list based on merged symbol, since we want to use the unmerged top-level symbol, but + // still skip reserializing it if we encounter the merged product later on + const visitedSym = getMergedSymbol(symbol); + if (visitedSymbols.has("" + getSymbolId(visitedSym))) { + return; // Already printed + } + visitedSymbols.set("" + getSymbolId(visitedSym), true); + // Only actually serialize symbols within the correct enclosing declaration, otherwise do nothing with the out-of-context symbol + const skipMembershipCheck = !isPrivate; // We only call this on exported symbols when we know they're in the correct scope + if (skipMembershipCheck || (!!length(symbol.declarations) && some(symbol.declarations, d => !!findAncestor(d, n => n === enclosingDeclaration)))) { + const oldContext = context; + context = cloneNodeBuilderContext(context); + const result = serializeSymbolWorker(symbol, isPrivate, propertyAsAlias); + context = oldContext; + return result; + } } - return type; - } - - /** - * Check if a Type was written as a tuple type literal. - * Prefer using isTupleLikeType() unless the use of `elementTypes`/`getTypeArguments` is required. - */ - function isTupleType(type: Type): type is TupleTypeReference { - return !!(getObjectFlags(type) & ObjectFlags.Reference && (type).target.objectFlags & ObjectFlags.Tuple); - } - - function getRestTypeOfTupleType(type: TupleTypeReference) { - return type.target.hasRestElement ? getTypeArguments(type)[type.target.typeParameters!.length - 1] : undefined; - } - - function getRestArrayTypeOfTupleType(type: TupleTypeReference) { - const restType = getRestTypeOfTupleType(type); - return restType && createArrayType(restType); - } - - function getLengthOfTupleType(type: TupleTypeReference) { - return getTypeReferenceArity(type) - (type.target.hasRestElement ? 1 : 0); - } - - function isZeroBigInt({value}: BigIntLiteralType) { - return value.base10Value === "0"; - } - - function getFalsyFlagsOfTypes(types: Type[]): TypeFlags { - let result: TypeFlags = 0; - for (const t of types) { - result |= getFalsyFlags(t); + // Synthesize declarations for a symbol - might be an Interface, a Class, a Namespace, a Type, a Variable (const, let, or var), an Alias + // or a merge of some number of those. + // An interesting challenge is ensuring that when classes merge with namespaces and interfaces, is keeping + // each symbol in only one of the representations + // Also, synthesizing a default export of some kind + // If it's an alias: emit `export default ref` + // If it's a property: emit `export default _default` with a `_default` prop + // If it's a class/interface/function: emit a class/interface/function with a `default` modifier + // These forms can merge, eg (`export default 12; export default interface A {}`) + function serializeSymbolWorker(symbol: ts.Symbol, isPrivate: boolean, propertyAsAlias: boolean) { + const symbolName = unescapeLeadingUnderscores(symbol.escapedName); + const isDefault = symbol.escapedName === InternalSymbolName.Default; + if (isStringANonContextualKeyword(symbolName) && !isDefault) { + // Oh no. We cannot use this symbol's name as it's name... It's likely some jsdoc had an invalid name like `export` or `default` :( + context.encounteredError = true; + // TODO: Issue error via symbol tracker? + return; // If we need to emit a private with a keyword name, we're done for, since something else will try to refer to it by that name + } + const needsPostExportDefault = isDefault && !!(symbol.flags & SymbolFlags.ExportDoesNotSupportDefaultModifier + || (symbol.flags & SymbolFlags.Function && length(getPropertiesOfType(getTypeOfSymbol(symbol))))) && !(symbol.flags & SymbolFlags.Alias); // An alias symbol should preclude needing to make an alias ourselves + if (needsPostExportDefault) { + isPrivate = true; + } + const modifierFlags = (!isPrivate ? ModifierFlags.Export : 0) | (isDefault && !needsPostExportDefault ? ModifierFlags.Default : 0); + const isConstMergedWithNS = symbol.flags & SymbolFlags.Module && + symbol.flags & (SymbolFlags.BlockScopedVariable | SymbolFlags.FunctionScopedVariable | SymbolFlags.Property) && + symbol.escapedName !== InternalSymbolName.ExportEquals; + const isConstMergedWithNSPrintableAsSignatureMerge = isConstMergedWithNS && isTypeRepresentableAsFunctionNamespaceMerge(getTypeOfSymbol(symbol), symbol); + if (symbol.flags & (SymbolFlags.Function | SymbolFlags.Method) || isConstMergedWithNSPrintableAsSignatureMerge) { + serializeAsFunctionNamespaceMerge(getTypeOfSymbol(symbol), symbol, getInternalSymbolName(symbol, symbolName), modifierFlags); + } + if (symbol.flags & SymbolFlags.TypeAlias) { + serializeTypeAlias(symbol, symbolName, modifierFlags); + } + // Need to skip over export= symbols below - json source files get a single `Property` flagged + // symbol of name `export=` which needs to be handled like an alias. It's not great, but it is what it is. + if (symbol.flags & (SymbolFlags.BlockScopedVariable | SymbolFlags.FunctionScopedVariable | SymbolFlags.Property) + && symbol.escapedName !== InternalSymbolName.ExportEquals + && !(symbol.flags & SymbolFlags.Prototype) + && !(symbol.flags & SymbolFlags.Class) + && !isConstMergedWithNSPrintableAsSignatureMerge) { + serializeVariableOrProperty(symbol, symbolName, isPrivate, needsPostExportDefault, propertyAsAlias, modifierFlags); + } + if (symbol.flags & SymbolFlags.Enum) { + serializeEnum(symbol, symbolName, modifierFlags); + } + if (symbol.flags & SymbolFlags.Class) { + if (symbol.flags & SymbolFlags.Property) { + // Looks like a `module.exports.Sub = class {}` - if we serialize `symbol` as a class, the result will have no members, + // since the classiness is actually from the target of the effective alias the symbol is. yes. A BlockScopedVariable|Class|Property + // _really_ acts like an Alias, and none of a BlockScopedVariable, Class, or Property. This is the travesty of JS binding today. + serializeAsAlias(symbol, getInternalSymbolName(symbol, symbolName), modifierFlags); + } + else { + serializeAsClass(symbol, getInternalSymbolName(symbol, symbolName), modifierFlags); + } + } + if ((symbol.flags & (SymbolFlags.ValueModule | SymbolFlags.NamespaceModule) && (!isConstMergedWithNS || isTypeOnlyNamespace(symbol))) || isConstMergedWithNSPrintableAsSignatureMerge) { + serializeModule(symbol, symbolName, modifierFlags); + } + if (symbol.flags & SymbolFlags.Interface) { + serializeInterface(symbol, symbolName, modifierFlags); + } + if (symbol.flags & SymbolFlags.Alias) { + serializeAsAlias(symbol, getInternalSymbolName(symbol, symbolName), modifierFlags); + } + if (symbol.flags & SymbolFlags.Property && symbol.escapedName === InternalSymbolName.ExportEquals) { + serializeMaybeAliasAssignment(symbol); + } + if (symbol.flags & SymbolFlags.ExportStar) { + // synthesize export * from "moduleReference" + // Straightforward - only one thing to do - make an export declaration + for (const node of symbol.declarations) { + const resolvedModule = resolveExternalModuleName(node, ((node as ExportDeclaration).moduleSpecifier!)); + if (!resolvedModule) + continue; + addResult(createExportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, /*exportClause*/ undefined, createLiteral(getSpecifierForModuleSymbol(resolvedModule, context))), ModifierFlags.None); + } + } + if (needsPostExportDefault) { + addResult(createExportAssignment(/*decorators*/ undefined, /*modifiers*/ undefined, /*isExportAssignment*/ false, createIdentifier(getInternalSymbolName(symbol, symbolName))), ModifierFlags.None); + } } - return result; - } - - // Returns the String, Number, Boolean, StringLiteral, NumberLiteral, BooleanLiteral, Void, Undefined, or Null - // flags for the string, number, boolean, "", 0, false, void, undefined, or null types respectively. Returns - // no flags for all other types (including non-falsy literal types). - function getFalsyFlags(type: Type): TypeFlags { - return type.flags & TypeFlags.Union ? getFalsyFlagsOfTypes((type).types) : - type.flags & TypeFlags.StringLiteral ? (type).value === "" ? TypeFlags.StringLiteral : 0 : - type.flags & TypeFlags.NumberLiteral ? (type).value === 0 ? TypeFlags.NumberLiteral : 0 : - type.flags & TypeFlags.BigIntLiteral ? isZeroBigInt(type) ? TypeFlags.BigIntLiteral : 0 : - type.flags & TypeFlags.BooleanLiteral ? (type === falseType || type === regularFalseType) ? TypeFlags.BooleanLiteral : 0 : - type.flags & TypeFlags.PossiblyFalsy; - } - - function removeDefinitelyFalsyTypes(type: Type): Type { - return getFalsyFlags(type) & TypeFlags.DefinitelyFalsy ? - filterType(type, t => !(getFalsyFlags(t) & TypeFlags.DefinitelyFalsy)) : - type; - } - - function extractDefinitelyFalsyTypes(type: Type): Type { - return mapType(type, getDefinitelyFalsyPartOfType); - } - - function getDefinitelyFalsyPartOfType(type: Type): Type { - return type.flags & TypeFlags.String ? emptyStringType : - type.flags & TypeFlags.Number ? zeroType : - type.flags & TypeFlags.BigInt ? zeroBigIntType : - type === regularFalseType || - type === falseType || - type.flags & (TypeFlags.Void | TypeFlags.Undefined | TypeFlags.Null) || - type.flags & TypeFlags.StringLiteral && (type).value === "" || - type.flags & TypeFlags.NumberLiteral && (type).value === 0 || - type.flags & TypeFlags.BigIntLiteral && isZeroBigInt(type) ? type : - neverType; - } - - /** - * Add undefined or null or both to a type if they are missing. - * @param type - type to add undefined and/or null to if not present - * @param flags - Either TypeFlags.Undefined or TypeFlags.Null, or both - */ - function getNullableType(type: Type, flags: TypeFlags): Type { - const missing = (flags & ~type.flags) & (TypeFlags.Undefined | TypeFlags.Null); - return missing === 0 ? type : - missing === TypeFlags.Undefined ? getUnionType([type, undefinedType]) : - missing === TypeFlags.Null ? getUnionType([type, nullType]) : - getUnionType([type, undefinedType, nullType]); - } - - function getOptionalType(type: Type): Type { - Debug.assert(strictNullChecks); - return type.flags & TypeFlags.Undefined ? type : getUnionType([type, undefinedType]); - } - - function getGlobalNonNullableTypeInstantiation(type: Type) { - if (!deferredGlobalNonNullableTypeAlias) { - deferredGlobalNonNullableTypeAlias = getGlobalSymbol("NonNullable" as __String, SymbolFlags.TypeAlias, /*diagnostic*/ undefined) || unknownSymbol; + function includePrivateSymbol(symbol: ts.Symbol) { + if (some(symbol.declarations, isParameterDeclaration)) + return; + Debug.assertIsDefined(deferredPrivates); + getUnusedName(unescapeLeadingUnderscores(symbol.escapedName), symbol); // Call to cache unique name for symbol + deferredPrivates.set("" + getSymbolId(symbol), symbol); + } + function isExportingScope(enclosingDeclaration: Node) { + return ((isSourceFile(enclosingDeclaration) && (isExternalOrCommonJsModule(enclosingDeclaration) || isJsonSourceFile(enclosingDeclaration))) || + (isAmbientModule(enclosingDeclaration) && !isGlobalScopeAugmentation(enclosingDeclaration))); + } + // Prepends a `declare` and/or `export` modifier if the context requires it, and then adds `node` to `result` and returns `node` + // Note: This _mutates_ `node` without using `updateNode` - the assumption being that all nodes should be manufactured fresh by the node builder + function addResult(node: Statement, additionalModifierFlags: ModifierFlags) { + let newModifierFlags: ModifierFlags = ModifierFlags.None; + if (additionalModifierFlags & ModifierFlags.Export && + enclosingDeclaration && + isExportingScope(enclosingDeclaration) && + canHaveExportModifier(node)) { + // Classes, namespaces, variables, functions, interfaces, and types should all be `export`ed in a module context if not private + newModifierFlags |= ModifierFlags.Export; + } + if (addingDeclare && !(newModifierFlags & ModifierFlags.Export) && + (!enclosingDeclaration || !(enclosingDeclaration.flags & NodeFlags.Ambient)) && + (isEnumDeclaration(node) || isVariableStatement(node) || isFunctionDeclaration(node) || isClassDeclaration(node) || isModuleDeclaration(node))) { + // Classes, namespaces, variables, enums, and functions all need `declare` modifiers to be valid in a declaration file top-level scope + newModifierFlags |= ModifierFlags.Ambient; + } + if ((additionalModifierFlags & ModifierFlags.Default) && (isClassDeclaration(node) || isInterfaceDeclaration(node) || isFunctionDeclaration(node))) { + newModifierFlags |= ModifierFlags.Default; + } + if (newModifierFlags) { + node.modifiers = createNodeArray(createModifiersFromModifierFlags(newModifierFlags | getModifierFlags(node))); + node.modifierFlagsCache = 0; // Reset computed flags cache + } + results.push(node); + } + function serializeTypeAlias(symbol: ts.Symbol, symbolName: string, modifierFlags: ModifierFlags) { + const aliasType = getDeclaredTypeOfTypeAlias(symbol); + const typeParams = getSymbolLinks(symbol).typeParameters; + const typeParamDecls = map(typeParams, p => typeParameterToDeclaration(p, context)); + const jsdocAliasDecl = find(symbol.declarations, isJSDocTypeAlias); + const commentText = jsdocAliasDecl ? jsdocAliasDecl.comment || jsdocAliasDecl.parent.comment : undefined; + const oldFlags = context.flags; + context.flags |= NodeBuilderFlags.InTypeAlias; + addResult(setSyntheticLeadingComments(createTypeAliasDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, getInternalSymbolName(symbol, symbolName), typeParamDecls, typeToTypeNodeHelper(aliasType, context)), !commentText ? [] : [{ kind: SyntaxKind.MultiLineCommentTrivia, text: "*\n * " + commentText.replace(/\n/g, "\n * ") + "\n ", pos: -1, end: -1, hasTrailingNewLine: true }]), modifierFlags); + context.flags = oldFlags; + } + function serializeInterface(symbol: ts.Symbol, symbolName: string, modifierFlags: ModifierFlags) { + const interfaceType = getDeclaredTypeOfClassOrInterface(symbol); + const localParams = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol); + const typeParamDecls = map(localParams, p => typeParameterToDeclaration(p, context)); + const baseTypes = getBaseTypes(interfaceType); + const baseType = length(baseTypes) ? getIntersectionType(baseTypes) : undefined; + const members = flatMap(getPropertiesOfType(interfaceType), p => serializePropertySymbolForInterface(p, baseType)); + const callSignatures = (serializeSignatures(SignatureKind.Call, interfaceType, baseType, SyntaxKind.CallSignature) as CallSignatureDeclaration[]); + const constructSignatures = (serializeSignatures(SignatureKind.Construct, interfaceType, baseType, SyntaxKind.ConstructSignature) as ConstructSignatureDeclaration[]); + const indexSignatures = serializeIndexSignatures(interfaceType, baseType); + const heritageClauses = !length(baseTypes) ? undefined : [createHeritageClause(SyntaxKind.ExtendsKeyword, mapDefined(baseTypes, b => trySerializeAsTypeReference(b)))]; + addResult(createInterfaceDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, getInternalSymbolName(symbol, symbolName), typeParamDecls, heritageClauses, [...indexSignatures, ...constructSignatures, ...callSignatures, ...members]), modifierFlags); + } + function getNamespaceMembersForSerialization(symbol: ts.Symbol) { + return !symbol.exports ? [] : filter(arrayFrom((symbol.exports).values()), p => !((p.flags & SymbolFlags.Prototype) || (p.escapedName === "prototype"))); + } + function isTypeOnlyNamespace(symbol: ts.Symbol) { + return every(getNamespaceMembersForSerialization(symbol), m => !(resolveSymbol(m).flags & SymbolFlags.Value)); + } + function serializeModule(symbol: ts.Symbol, symbolName: string, modifierFlags: ModifierFlags) { + const members = getNamespaceMembersForSerialization(symbol); + // Split NS members up by declaration - members whose parent symbol is the ns symbol vs those whose is not (but were added in later via merging) + const locationMap = arrayToMultiMap(members, m => m.parent && m.parent === symbol ? "real" : "merged"); + const realMembers = locationMap.get("real") || emptyArray; + const mergedMembers = locationMap.get("merged") || emptyArray; + // TODO: `suppressNewPrivateContext` is questionable -we need to simply be emitting privates in whatever scope they were declared in, rather + // than whatever scope we traverse to them in. That's a bit of a complex rewrite, since we're not _actually_ tracking privates at all in advance, + // so we don't even have placeholders to fill in. + if (length(realMembers)) { + const localName = getInternalSymbolName(symbol, symbolName); + serializeAsNamespaceDeclaration(realMembers, localName, modifierFlags, !!(symbol.flags & (SymbolFlags.Function | SymbolFlags.Assignment))); + } + if (length(mergedMembers)) { + const localName = getInternalSymbolName(symbol, symbolName); + const nsBody = createModuleBlock([createExportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, createNamedExports(map(filter(mergedMembers, n => n.escapedName !== InternalSymbolName.ExportEquals), s => { + const name = unescapeLeadingUnderscores(s.escapedName); + const localName = getInternalSymbolName(s, name); + const aliasDecl = s.declarations && getDeclarationOfAliasSymbol(s); + const target = aliasDecl && getTargetOfAliasDeclaration(aliasDecl, /*dontRecursivelyResolve*/ true); + includePrivateSymbol(target || s); + const targetName = target ? getInternalSymbolName(target, unescapeLeadingUnderscores(target.escapedName)) : localName; + return createExportSpecifier(name === targetName ? undefined : targetName, name); + })))]); + addResult(createModuleDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, createIdentifier(localName), nsBody, NodeFlags.Namespace), ModifierFlags.None); + } + } + function serializeEnum(symbol: ts.Symbol, symbolName: string, modifierFlags: ModifierFlags) { + addResult(createEnumDeclaration( + /*decorators*/ undefined, createModifiersFromModifierFlags(isConstEnumSymbol(symbol) ? ModifierFlags.Const : 0), getInternalSymbolName(symbol, symbolName), map(filter(getPropertiesOfType(getTypeOfSymbol(symbol)), p => !!(p.flags & SymbolFlags.EnumMember)), p => { + // TODO: Handle computed names + // I hate that to get the initialized value we need to walk back to the declarations here; but there's no + // other way to get the possible const value of an enum member that I'm aware of, as the value is cached + // _on the declaration_, not on the declaration's symbol... + const initializedValue = p.declarations && p.declarations[0] && isEnumMember(p.declarations[0]) && getConstantValue((p.declarations[0] as EnumMember)); + return createEnumMember(unescapeLeadingUnderscores(p.escapedName), initializedValue === undefined ? undefined : createLiteral(initializedValue)); + })), modifierFlags); + } + function serializeVariableOrProperty(symbol: ts.Symbol, symbolName: string, isPrivate: boolean, needsPostExportDefault: boolean, propertyAsAlias: boolean | undefined, modifierFlags: ModifierFlags) { + if (propertyAsAlias) { + serializeMaybeAliasAssignment(symbol); + } + else { + const type = getTypeOfSymbol(symbol); + const localName = getInternalSymbolName(symbol, symbolName); + if (!(symbol.flags & SymbolFlags.Function) && isTypeRepresentableAsFunctionNamespaceMerge(type, symbol)) { + // If the type looks like a function declaration + ns could represent it, and it's type is sourced locally, rewrite it into a function declaration + ns + serializeAsFunctionNamespaceMerge(type, symbol, localName, modifierFlags); + } + else { + // A Class + Property merge is made for a `module.exports.Member = class {}`, and it doesn't serialize well as either a class _or_ a property symbol - in fact, _it behaves like an alias!_ + // `var` is `FunctionScopedVariable`, `const` and `let` are `BlockScopedVariable`, and `module.exports.thing =` is `Property` + const flags = !(symbol.flags & SymbolFlags.BlockScopedVariable) ? undefined + : isConstVariable(symbol) ? NodeFlags.Const + : NodeFlags.Let; + const name = (needsPostExportDefault || !(symbol.flags & SymbolFlags.Property)) ? localName : getUnusedName(localName, symbol); + let textRange: Node | undefined = symbol.declarations && find(symbol.declarations, d => isVariableDeclaration(d)); + if (textRange && isVariableDeclarationList(textRange.parent) && textRange.parent.declarations.length === 1) { + textRange = textRange.parent.parent; + } + const statement = setTextRange(createVariableStatement(/*modifiers*/ undefined, createVariableDeclarationList([ + createVariableDeclaration(name, serializeTypeForDeclaration(type, symbol)) + ], flags)), textRange); + addResult(statement, name !== localName ? modifierFlags & ~ModifierFlags.Export : modifierFlags); + if (name !== localName && !isPrivate) { + // We rename the variable declaration we generate for Property symbols since they may have a name which + // conflicts with a local declaration. For example, given input: + // ``` + // function g() {} + // module.exports.g = g + // ``` + // In such a situation, we have a local variable named `g`, and a separate exported variable named `g`. + // Naively, we would emit + // ``` + // function g() {} + // export const g: typeof g; + // ``` + // That's obviously incorrect - the `g` in the type annotation needs to refer to the local `g`, but + // the export declaration shadows it. + // To work around that, we instead write + // ``` + // function g() {} + // const g_1: typeof g; + // export { g_1 as g }; + // ``` + // To create an export named `g` that does _not_ shadow the local `g` + addResult(createExportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, createNamedExports([createExportSpecifier(name, localName)])), ModifierFlags.None); + } + } + } + } + function serializeAsFunctionNamespaceMerge(type: ts.Type, symbol: ts.Symbol, localName: string, modifierFlags: ModifierFlags) { + const signatures = getSignaturesOfType(type, SignatureKind.Call); + for (const sig of signatures) { + // Each overload becomes a separate function declaration, in order + const decl = (signatureToSignatureDeclarationHelper(sig, SyntaxKind.FunctionDeclaration, context) as FunctionDeclaration); + decl.name = createIdentifier(localName); + addResult(setTextRange(decl, sig.declaration), modifierFlags); + } + // Module symbol emit will take care of module-y members, provided it has exports + if (!(symbol.flags & (SymbolFlags.ValueModule | SymbolFlags.NamespaceModule) && !!symbol.exports && !!symbol.exports.size)) { + const props = filter(getPropertiesOfType(type), p => !((p.flags & SymbolFlags.Prototype) || (p.escapedName === "prototype"))); + serializeAsNamespaceDeclaration(props, localName, modifierFlags, /*suppressNewPrivateContext*/ true); + } + } + function serializeAsNamespaceDeclaration(props: readonly ts.Symbol[], localName: string, modifierFlags: ModifierFlags, suppressNewPrivateContext: boolean) { + if (length(props)) { + const localVsRemoteMap = arrayToMultiMap(props, p => !length(p.declarations) || some(p.declarations, d => getSourceFileOfNode(d) === getSourceFileOfNode((context.enclosingDeclaration!))) ? "local" : "remote"); + const localProps = localVsRemoteMap.get("local") || emptyArray; + // handle remote props first - we need to make an `import` declaration that points at the module containing each remote + // prop in the outermost scope (TODO: a namespace within a namespace would need to be appropriately handled by this) + // Example: + // import Foo_1 = require("./exporter"); + // export namespace ns { + // import Foo = Foo_1.Foo; + // export { Foo }; + // export const c: number; + // } + // This is needed because in JS, statements like `const x = require("./f")` support both type and value lookup, even if they're + // normally just value lookup (so it functions kinda like an alias even when it's not an alias) + // _Usually_, we'll simply print the top-level as an alias instead of a `var` in such situations, however is is theoretically + // possible to encounter a situation where a type has members from both the current file and other files - in those situations, + // emit akin to the above would be needed. + // Add a namespace + const fakespace = createModuleDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, createIdentifier(localName), createModuleBlock([]), NodeFlags.Namespace); + fakespace.flags ^= NodeFlags.Synthesized; // unset synthesized so it is usable as an enclosing declaration + fakespace.parent = (enclosingDeclaration as SourceFile | NamespaceDeclaration); + fakespace.locals = createSymbolTable(props); + fakespace.symbol = props[0].parent!; + const oldResults = results; + results = []; + const oldAddingDeclare = addingDeclare; + addingDeclare = false; + const subcontext = { ...context, enclosingDeclaration: fakespace }; + const oldContext = context; + context = subcontext; + // TODO: implement handling for the localVsRemoteMap.get("remote") - should be difficult to trigger (see comment above), as only interesting cross-file js merges should make this possible + visitSymbolTable(createSymbolTable(localProps), suppressNewPrivateContext, /*propertyAsAlias*/ true); + context = oldContext; + addingDeclare = oldAddingDeclare; + const declarations = results; + results = oldResults; + fakespace.flags ^= NodeFlags.Synthesized; // reset synthesized + fakespace.parent = undefined!; + fakespace.locals = undefined!; + fakespace.symbol = undefined!; + fakespace.body = createModuleBlock(declarations); + addResult(fakespace, modifierFlags); // namespaces can never be default exported + } + } + function serializeAsClass(symbol: ts.Symbol, localName: string, modifierFlags: ModifierFlags) { + const localParams = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol); + const typeParamDecls = map(localParams, p => typeParameterToDeclaration(p, context)); + const classType = getDeclaredTypeOfClassOrInterface(symbol); + const baseTypes = getBaseTypes(classType); + const implementsTypes = getImplementsTypes(classType); + const staticType = getTypeOfSymbol(symbol); + const staticBaseType = getBaseConstructorTypeOfClass((staticType as InterfaceType)); + const heritageClauses = [ + ...!length(baseTypes) ? [] : [createHeritageClause(SyntaxKind.ExtendsKeyword, map(baseTypes, b => serializeBaseType(b, staticBaseType, localName)))], + ...!length(implementsTypes) ? [] : [createHeritageClause(SyntaxKind.ImplementsKeyword, map(implementsTypes, b => serializeBaseType(b, staticBaseType, localName)))] + ]; + const symbolProps = getPropertiesOfType(classType); + const publicSymbolProps = filter(symbolProps, s => { + const valueDecl = s.valueDeclaration; + Debug.assertIsDefined(valueDecl); + return !(isNamedDeclaration(valueDecl) && isPrivateIdentifier(valueDecl.name)); + }); + const hasPrivateIdentifier = some(symbolProps, s => { + const valueDecl = s.valueDeclaration; + Debug.assertIsDefined(valueDecl); + return isNamedDeclaration(valueDecl) && isPrivateIdentifier(valueDecl.name); + }); + // Boil down all private properties into a single one. + const privateProperties = hasPrivateIdentifier ? + [createProperty( + /*decorators*/ undefined, + /*modifiers*/ undefined, createPrivateIdentifier("#private"), + /*questionOrExclamationToken*/ undefined, + /*type*/ undefined, + /*initializer*/ undefined)] : emptyArray; + const publicProperties = flatMap(publicSymbolProps, p => serializePropertySymbolForClass(p, /*isStatic*/ false, baseTypes[0])); + // Consider static members empty if symbol also has function or module meaning - function namespacey emit will handle statics + const staticMembers = symbol.flags & (SymbolFlags.Function | SymbolFlags.ValueModule) + ? [] + : flatMap(filter(getPropertiesOfType(staticType), p => !(p.flags & SymbolFlags.Prototype) && p.escapedName !== "prototype"), p => serializePropertySymbolForClass(p, /*isStatic*/ true, staticBaseType)); + const constructors = (serializeSignatures(SignatureKind.Construct, staticType, baseTypes[0], SyntaxKind.Constructor) as ConstructorDeclaration[]); + for (const c of constructors) { + // A constructor's return type and type parameters are supposed to be controlled by the enclosing class declaration + // `signatureToSignatureDeclarationHelper` appends them regardless, so for now we delete them here + c.type = undefined; + c.typeParameters = undefined; + } + const indexSignatures = serializeIndexSignatures(classType, baseTypes[0]); + addResult(setTextRange(createClassDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, localName, typeParamDecls, heritageClauses, [...indexSignatures, ...staticMembers, ...constructors, ...publicProperties, ...privateProperties]), symbol.declarations && filter(symbol.declarations, d => isClassDeclaration(d) || isClassExpression(d))[0]), modifierFlags); + } + function serializeAsAlias(symbol: ts.Symbol, localName: string, modifierFlags: ModifierFlags) { + // synthesize an alias, eg `export { symbolName as Name }` + // need to mark the alias `symbol` points at + // as something we need to serialize as a private declaration as well + const node = getDeclarationOfAliasSymbol(symbol); + if (!node) + return Debug.fail(); + const target = getMergedSymbol(getTargetOfAliasDeclaration(node, /*dontRecursivelyResolve*/ true)); + if (!target) { + return; + } + let verbatimTargetName = unescapeLeadingUnderscores(target.escapedName); + if (verbatimTargetName === InternalSymbolName.ExportEquals && (compilerOptions.esModuleInterop || compilerOptions.allowSyntheticDefaultImports)) { + // target refers to an `export=` symbol that was hoisted into a synthetic default - rename here to match + verbatimTargetName = InternalSymbolName.Default; + } + const targetName = getInternalSymbolName(target, verbatimTargetName); + includePrivateSymbol(target); // the target may be within the same scope - attempt to serialize it first + switch (node.kind) { + case SyntaxKind.ImportEqualsDeclaration: + // Could be a local `import localName = ns.member` or + // an external `import localName = require("whatever")` + const isLocalImport = !(target.flags & SymbolFlags.ValueModule); + addResult(createImportEqualsDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, createIdentifier(localName), isLocalImport + ? symbolToName(target, context, SymbolFlags.All, /*expectsIdentifier*/ false) + : createExternalModuleReference(createLiteral(getSpecifierForModuleSymbol(symbol, context)))), isLocalImport ? modifierFlags : ModifierFlags.None); + break; + case SyntaxKind.NamespaceExportDeclaration: + // export as namespace foo + // TODO: Not part of a file's local or export symbol tables + // Is bound into file.symbol.globalExports instead, which we don't currently traverse + addResult(createNamespaceExportDeclaration(idText((node as NamespaceExportDeclaration).name)), ModifierFlags.None); + break; + case SyntaxKind.ImportClause: + addResult(createImportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, createImportClause(createIdentifier(localName), /*namedBindings*/ undefined), + // We use `target.parent || target` below as `target.parent` is unset when the target is a module which has been export assigned + // And then made into a default by the `esModuleInterop` or `allowSyntheticDefaultImports` flag + // In such cases, the `target` refers to the module itself already + createLiteral(getSpecifierForModuleSymbol(target.parent || target, context))), ModifierFlags.None); + break; + case SyntaxKind.NamespaceImport: + addResult(createImportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, createImportClause(/*importClause*/ undefined, createNamespaceImport(createIdentifier(localName))), createLiteral(getSpecifierForModuleSymbol(target, context))), ModifierFlags.None); + break; + case SyntaxKind.NamespaceExport: + addResult(createExportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, createNamespaceExport(createIdentifier(localName)), createLiteral(getSpecifierForModuleSymbol(target, context))), ModifierFlags.None); + break; + case SyntaxKind.ImportSpecifier: + addResult(createImportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, createImportClause(/*importClause*/ undefined, createNamedImports([ + createImportSpecifier(localName !== verbatimTargetName ? createIdentifier(verbatimTargetName) : undefined, createIdentifier(localName)) + ])), createLiteral(getSpecifierForModuleSymbol(target.parent || target, context))), ModifierFlags.None); + break; + case SyntaxKind.ExportSpecifier: + // does not use localName because the symbol name in this case refers to the name in the exports table, + // which we must exactly preserve + const specifier = (node.parent.parent as ExportDeclaration).moduleSpecifier; + // targetName is only used when the target is local, as otherwise the target is an alias that points at + // another file + serializeExportSpecifier(unescapeLeadingUnderscores(symbol.escapedName), specifier ? verbatimTargetName : targetName, specifier && isStringLiteralLike(specifier) ? createLiteral(specifier.text) : undefined); + break; + case SyntaxKind.ExportAssignment: + serializeMaybeAliasAssignment(symbol); + break; + case SyntaxKind.BinaryExpression: + case SyntaxKind.PropertyAccessExpression: + // Could be best encoded as though an export specifier or as though an export assignment + // If name is default or export=, do an export assignment + // Otherwise do an export specifier + if (symbol.escapedName === InternalSymbolName.Default || symbol.escapedName === InternalSymbolName.ExportEquals) { + serializeMaybeAliasAssignment(symbol); + } + else { + serializeExportSpecifier(localName, targetName); + } + break; + default: + return Debug.failBadSyntaxKind(node, "Unhandled alias declaration kind in symbol serializer!"); + } } - // Use NonNullable global type alias if available to improve quick info/declaration emit - if (deferredGlobalNonNullableTypeAlias !== unknownSymbol) { - return getTypeAliasInstantiation(deferredGlobalNonNullableTypeAlias, [type]); + function serializeExportSpecifier(localName: string, targetName: string, specifier?: Expression) { + addResult(createExportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, createNamedExports([createExportSpecifier(localName !== targetName ? targetName : undefined, localName)]), specifier), ModifierFlags.None); } - return getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull); // Type alias unavailable, fall back to non-higher-order behavior - } - - function getNonNullableType(type: Type): Type { - return strictNullChecks ? getGlobalNonNullableTypeInstantiation(type) : type; - } - - function addOptionalTypeMarker(type: Type) { - return strictNullChecks ? getUnionType([type, optionalType]) : type; - } - - function isNotOptionalTypeMarker(type: Type) { - return type !== optionalType; - } - - function removeOptionalTypeMarker(type: Type): Type { - return strictNullChecks ? filterType(type, isNotOptionalTypeMarker) : type; - } - - function propagateOptionalTypeMarker(type: Type, node: OptionalChain, wasOptional: boolean) { - return wasOptional ? isOutermostOptionalChain(node) ? getOptionalType(type) : addOptionalTypeMarker(type) : type; - } - - function getOptionalExpressionType(exprType: Type, expression: Expression) { - return isExpressionOfOptionalChainRoot(expression) ? getNonNullableType(exprType) : - isOptionalChain(expression) ? removeOptionalTypeMarker(exprType) : - exprType; - } - - /** - * Is source potentially coercible to target type under `==`. - * Assumes that `source` is a constituent of a union, hence - * the boolean literal flag on the LHS, but not on the RHS. - * - * This does not fully replicate the semantics of `==`. The - * intention is to catch cases that are clearly not right. - * - * Comparing (string | number) to number should not remove the - * string element. - * - * Comparing (string | number) to 1 will remove the string - * element, though this is not sound. This is a pragmatic - * choice. - * - * @see narrowTypeByEquality - * - * @param source - * @param target - */ - function isCoercibleUnderDoubleEquals(source: Type, target: Type): boolean { - return ((source.flags & (TypeFlags.Number | TypeFlags.String | TypeFlags.BooleanLiteral)) !== 0) - && ((target.flags & (TypeFlags.Number | TypeFlags.String | TypeFlags.Boolean)) !== 0); - } - - /** - * Return true if type was inferred from an object literal, written as an object type literal, or is the shape of a module - * with no call or construct signatures. - */ - function isObjectTypeWithInferableIndex(type: Type): boolean { - return type.flags & TypeFlags.Intersection ? every((type).types, isObjectTypeWithInferableIndex) : - !!(type.symbol && (type.symbol.flags & (SymbolFlags.ObjectLiteral | SymbolFlags.TypeLiteral | SymbolFlags.Enum | SymbolFlags.ValueModule)) !== 0 && - !typeHasCallOrConstructSignatures(type)) || !!(getObjectFlags(type) & ObjectFlags.ReverseMapped && isObjectTypeWithInferableIndex((type as ReverseMappedType).source)); - } - - function createSymbolWithType(source: Symbol, type: Type | undefined) { - const symbol = createSymbol(source.flags, source.escapedName, getCheckFlags(source) & CheckFlags.Readonly); - symbol.declarations = source.declarations; - symbol.parent = source.parent; - symbol.type = type; - symbol.target = source; - if (source.valueDeclaration) { - symbol.valueDeclaration = source.valueDeclaration; - } - const nameType = getSymbolLinks(source).nameType; - if (nameType) { - symbol.nameType = nameType; + function serializeMaybeAliasAssignment(symbol: ts.Symbol) { + if (symbol.flags & SymbolFlags.Prototype) { + return; + } + const name = unescapeLeadingUnderscores(symbol.escapedName); + const isExportEquals = name === InternalSymbolName.ExportEquals; + const isDefault = name === InternalSymbolName.Default; + const isExportAssignment = isExportEquals || isDefault; + // synthesize export = ref + // ref should refer to either be a locally scoped symbol which we need to emit, or + // a reference to another namespace/module which we may need to emit an `import` statement for + const aliasDecl = symbol.declarations && getDeclarationOfAliasSymbol(symbol); + // serialize what the alias points to, preserve the declaration's initializer + const target = aliasDecl && getTargetOfAliasDeclaration(aliasDecl, /*dontRecursivelyResolve*/ true); + // If the target resolves and resolves to a thing defined in this file, emit as an alias, otherwise emit as a const + if (target && length(target.declarations) && some(target.declarations, d => getSourceFileOfNode(d) === getSourceFileOfNode(enclosingDeclaration))) { + // In case `target` refers to a namespace member, look at the declaration and serialize the leftmost symbol in it + // eg, `namespace A { export class B {} }; exports = A.B;` + // Technically, this is all that's required in the case where the assignment is an entity name expression + const expr = isExportAssignment ? getExportAssignmentExpression((aliasDecl as ExportAssignment | BinaryExpression)) : getPropertyAssignmentAliasLikeExpression((aliasDecl as ShorthandPropertyAssignment | PropertyAssignment | PropertyAccessExpression)); + const first = isEntityNameExpression(expr) ? getFirstNonModuleExportsIdentifier(expr) : undefined; + const referenced = first && resolveEntityName(first, SymbolFlags.All, /*ignoreErrors*/ true, /*dontResolveAlias*/ true, enclosingDeclaration); + if (referenced || target) { + includePrivateSymbol(referenced || target); + } + // We disable the context's symbol traker for the duration of this name serialization + // as, by virtue of being here, the name is required to print something, and we don't want to + // issue a visibility error on it. Only anonymous classes that an alias points at _would_ issue + // a visibility error here (as they're not visible within any scope), but we want to hoist them + // into the containing scope anyway, so we want to skip the visibility checks. + const oldTrack = context.tracker.trackSymbol; + context.tracker.trackSymbol = noop; + if (isExportAssignment) { + results.push(createExportAssignment( + /*decorators*/ undefined, + /*modifiers*/ undefined, isExportEquals, symbolToExpression(target, context, SymbolFlags.All))); + } + else { + if (first === expr) { + // serialize as `export {target as name}` + serializeExportSpecifier(name, idText(first)); + } + else if (isClassExpression(expr)) { + serializeExportSpecifier(name, getInternalSymbolName(target, symbolName(target))); + } + else { + // serialize as `import _Ref = t.arg.et; export { _Ref as name }` + const varName = getUnusedName(name, symbol); + addResult(createImportEqualsDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, createIdentifier(varName), symbolToName(target, context, SymbolFlags.All, /*expectsIdentifier*/ false)), ModifierFlags.None); + serializeExportSpecifier(name, varName); + } + } + context.tracker.trackSymbol = oldTrack; + } + else { + // serialize as an anonymous property declaration + const varName = getUnusedName(name, symbol); + // We have to use `getWidenedType` here since the object within a json file is unwidened within the file + // (Unwidened types can only exist in expression contexts and should never be serialized) + const typeToSerialize = getWidenedType(getTypeOfSymbol(symbol)); + if (isTypeRepresentableAsFunctionNamespaceMerge(typeToSerialize, symbol)) { + // If there are no index signatures and `typeToSerialize` is an object type, emit as a namespace instead of a const + serializeAsFunctionNamespaceMerge(typeToSerialize, symbol, varName, isExportAssignment ? ModifierFlags.None : ModifierFlags.Export); + } + else { + const statement = createVariableStatement(/*modifiers*/ undefined, createVariableDeclarationList([ + createVariableDeclaration(varName, serializeTypeForDeclaration(typeToSerialize, symbol)) + ], NodeFlags.Const)); + addResult(statement, name === varName ? ModifierFlags.Export : ModifierFlags.None); + } + if (isExportAssignment) { + results.push(createExportAssignment( + /*decorators*/ undefined, + /*modifiers*/ undefined, isExportEquals, createIdentifier(varName))); + } + else if (name !== varName) { + serializeExportSpecifier(name, varName); + } + } } - return symbol; - } - - function transformTypeOfMembers(type: Type, f: (propertyType: Type) => Type) { - const members = createSymbolTable(); - for (const property of getPropertiesOfObjectType(type)) { - const original = getTypeOfSymbol(property); - const updated = f(original); - members.set(property.escapedName, updated === original ? property : createSymbolWithType(property, updated)); + function isTypeRepresentableAsFunctionNamespaceMerge(typeToSerialize: ts.Type, hostSymbol: ts.Symbol) { + // Only object types which are not constructable, or indexable, whose members all come from the + // context source file, and whose property names are all valid identifiers and not late-bound, _and_ + // whose input is not type annotated (if the input symbol has an annotation we can reuse, we should prefer it) + const ctxSrc = getSourceFileOfNode(context.enclosingDeclaration); + return getObjectFlags(typeToSerialize) & (ObjectFlags.Anonymous | ObjectFlags.Mapped) && + !getIndexInfoOfType(typeToSerialize, IndexKind.String) && + !getIndexInfoOfType(typeToSerialize, IndexKind.Number) && + !!(length(getPropertiesOfType(typeToSerialize)) || length(getSignaturesOfType(typeToSerialize, SignatureKind.Call))) && + !length(getSignaturesOfType(typeToSerialize, SignatureKind.Construct)) && // TODO: could probably serialize as function + ns + class, now that that's OK + !getDeclarationWithTypeAnnotation(hostSymbol) && + !(typeToSerialize.symbol && some(typeToSerialize.symbol.declarations, d => getSourceFileOfNode(d) !== ctxSrc)) && + !some(getPropertiesOfType(typeToSerialize), p => isLateBoundName(p.escapedName)) && + !some(getPropertiesOfType(typeToSerialize), p => some(p.declarations, d => getSourceFileOfNode(d) !== ctxSrc)) && + every(getPropertiesOfType(typeToSerialize), p => isIdentifierText(symbolName(p), languageVersion) && !isStringAKeyword(symbolName(p))); } - return members; - } - - /** - * If the the provided object literal is subject to the excess properties check, - * create a new that is exempt. Recursively mark object literal members as exempt. - * Leave signatures alone since they are not subject to the check. - */ - function getRegularTypeOfObjectLiteral(type: Type): Type { - if (!(isObjectLiteralType(type) && getObjectFlags(type) & ObjectFlags.FreshLiteral)) { - return type; + function makeSerializePropertySymbol(createProperty: (decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, name: string | PropertyName, questionOrExclamationToken: QuestionToken | undefined, type: TypeNode | undefined, initializer: Expression | undefined) => T, methodKind: SyntaxKind, useAccessors: true): (p: ts.Symbol, isStatic: boolean, baseType: ts.Type | undefined) => (T | AccessorDeclaration | (T | AccessorDeclaration)[]); + function makeSerializePropertySymbol(createProperty: (decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, name: string | PropertyName, questionOrExclamationToken: QuestionToken | undefined, type: TypeNode | undefined, initializer: Expression | undefined) => T, methodKind: SyntaxKind, useAccessors: false): (p: ts.Symbol, isStatic: boolean, baseType: ts.Type | undefined) => (T | T[]); + function makeSerializePropertySymbol(createProperty: (decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, name: string | PropertyName, questionOrExclamationToken: QuestionToken | undefined, type: TypeNode | undefined, initializer: Expression | undefined) => T, methodKind: SyntaxKind, useAccessors: boolean): (p: ts.Symbol, isStatic: boolean, baseType: ts.Type | undefined) => (T | AccessorDeclaration | (T | AccessorDeclaration)[]) { + return function serializePropertySymbol(p: ts.Symbol, isStatic: boolean, baseType: ts.Type | undefined) { + const modifierFlags = getDeclarationModifierFlagsFromSymbol(p); + const isPrivate = !!(modifierFlags & ModifierFlags.Private); + if (isStatic && (p.flags & (SymbolFlags.Type | SymbolFlags.Namespace | SymbolFlags.Alias))) { + // Only value-only-meaning symbols can be correctly encoded as class statics, type/namespace/alias meaning symbols + // need to be merged namespace members + return []; + } + if (p.flags & SymbolFlags.Prototype || (baseType && getPropertyOfType(baseType, p.escapedName) + && isReadonlySymbol(getPropertyOfType(baseType, p.escapedName)!) === isReadonlySymbol(p) + && (p.flags & SymbolFlags.Optional) === (getPropertyOfType(baseType, p.escapedName)!.flags & SymbolFlags.Optional) + && isTypeIdenticalTo(getTypeOfSymbol(p), getTypeOfPropertyOfType(baseType, p.escapedName)!))) { + return []; + } + const flag = (modifierFlags & ~ModifierFlags.Async) | (isStatic ? ModifierFlags.Static : 0); + const name = getPropertyNameNodeForSymbol(p, context); + const firstPropertyLikeDecl = find(p.declarations, or(isPropertyDeclaration, isAccessor, isVariableDeclaration, isPropertySignature, isBinaryExpression, isPropertyAccessExpression)); + if (p.flags & SymbolFlags.Accessor && useAccessors) { + const result: AccessorDeclaration[] = []; + if (p.flags & SymbolFlags.SetAccessor) { + result.push(setTextRange(createSetAccessor( + /*decorators*/ undefined, createModifiersFromModifierFlags(flag), name, [createParameter( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, "arg", + /*questionToken*/ undefined, isPrivate ? undefined : serializeTypeForDeclaration(getTypeOfSymbol(p), p))], + /*body*/ undefined), find(p.declarations, isSetAccessor) || firstPropertyLikeDecl)); + } + if (p.flags & SymbolFlags.GetAccessor) { + const isPrivate = modifierFlags & ModifierFlags.Private; + result.push(setTextRange(createGetAccessor( + /*decorators*/ undefined, createModifiersFromModifierFlags(flag), name, [], isPrivate ? undefined : serializeTypeForDeclaration(getTypeOfSymbol(p), p), + /*body*/ undefined), find(p.declarations, isGetAccessor) || firstPropertyLikeDecl)); + } + return result; + } + // This is an else/if as accessors and properties can't merge in TS, but might in JS + // If this happens, we assume the accessor takes priority, as it imposes more constraints + else if (p.flags & (SymbolFlags.Property | SymbolFlags.Variable)) { + return setTextRange(createProperty( + /*decorators*/ undefined, createModifiersFromModifierFlags((isReadonlySymbol(p) ? ModifierFlags.Readonly : 0) | flag), name, p.flags & SymbolFlags.Optional ? createToken(SyntaxKind.QuestionToken) : undefined, isPrivate ? undefined : serializeTypeForDeclaration(getTypeOfSymbol(p), p), + // TODO: https://github.com/microsoft/TypeScript/pull/32372#discussion_r328386357 + // interface members can't have initializers, however class members _can_ + /*initializer*/ undefined), find(p.declarations, or(isPropertyDeclaration, isVariableDeclaration)) || firstPropertyLikeDecl); + } + if (p.flags & (SymbolFlags.Method | SymbolFlags.Function)) { + const type = getTypeOfSymbol(p); + const signatures = getSignaturesOfType(type, SignatureKind.Call); + if (flag & ModifierFlags.Private) { + return setTextRange(createProperty( + /*decorators*/ undefined, createModifiersFromModifierFlags((isReadonlySymbol(p) ? ModifierFlags.Readonly : 0) | flag), name, p.flags & SymbolFlags.Optional ? createToken(SyntaxKind.QuestionToken) : undefined, + /*type*/ undefined, + /*initializer*/ undefined), find(p.declarations, isFunctionLikeDeclaration) || signatures[0] && signatures[0].declaration || p.declarations[0]); + } + const results = []; + for (const sig of signatures) { + // Each overload becomes a separate method declaration, in order + const decl = (signatureToSignatureDeclarationHelper(sig, methodKind, context) as MethodDeclaration); + decl.name = name; // TODO: Clone + if (flag) { + decl.modifiers = createNodeArray(createModifiersFromModifierFlags(flag)); + } + if (p.flags & SymbolFlags.Optional) { + decl.questionToken = createToken(SyntaxKind.QuestionToken); + } + results.push(setTextRange(decl, sig.declaration)); + } + return results as unknown as T[]; + } + // The `Constructor`'s symbol isn't in the class's properties lists, obviously, since it's a signature on the static + return Debug.fail(`Unhandled class member kind! ${(p as any).__debugFlags || p.flags}`); + }; } - const regularType = (type).regularType; - if (regularType) { - return regularType; + function serializePropertySymbolForInterface(p: ts.Symbol, baseType: ts.Type | undefined) { + return serializePropertySymbolForInterfaceWorker(p, /*isStatic*/ false, baseType); } - - const resolved = type; - const members = transformTypeOfMembers(type, getRegularTypeOfObjectLiteral); - const regularNew = createAnonymousType(resolved.symbol, - members, - resolved.callSignatures, - resolved.constructSignatures, - resolved.stringIndexInfo, - resolved.numberIndexInfo); - regularNew.flags = resolved.flags; - regularNew.objectFlags |= resolved.objectFlags & ~ObjectFlags.FreshLiteral; - (type).regularType = regularNew; - return regularNew; - } - - function createWideningContext(parent: WideningContext | undefined, propertyName: __String | undefined, siblings: Type[] | undefined): WideningContext { - return { parent, propertyName, siblings, resolvedProperties: undefined }; - } - - function getSiblingsOfContext(context: WideningContext): Type[] { - if (!context.siblings) { - const siblings: Type[] = []; - for (const type of getSiblingsOfContext(context.parent!)) { - if (isObjectLiteralType(type)) { - const prop = getPropertyOfObjectType(type, context.propertyName!); - if (prop) { - forEachType(getTypeOfSymbol(prop), t => { - siblings.push(t); - }); + function getDeclarationWithTypeAnnotation(symbol: ts.Symbol) { + return symbol.declarations && find(symbol.declarations, s => !!getEffectiveTypeAnnotationNode(s) && !!findAncestor(s, n => n === enclosingDeclaration)); + } + /** + * Unlike `typeToTypeNodeHelper`, this handles setting up the `AllowUniqueESSymbolType` flag + * so a `unique symbol` is returned when appropriate for the input symbol, rather than `typeof sym` + */ + function serializeTypeForDeclaration(type: ts.Type, symbol: ts.Symbol) { + const declWithExistingAnnotation = getDeclarationWithTypeAnnotation(symbol); + if (declWithExistingAnnotation && !isFunctionLikeDeclaration(declWithExistingAnnotation)) { + // try to reuse the existing annotation + const existing = (getEffectiveTypeAnnotationNode(declWithExistingAnnotation)!); + const transformed = visitNode(existing, visitExistingNodeTreeSymbols); + return transformed === existing ? getMutableClone(existing) : transformed; + } + const oldFlags = context.flags; + if (type.flags & TypeFlags.UniqueESSymbol && + type.symbol === symbol) { + context.flags |= NodeBuilderFlags.AllowUniqueESSymbolType; + } + const result = typeToTypeNodeHelper(type, context); + context.flags = oldFlags; + return result; + function visitExistingNodeTreeSymbols(node: T): Node { + if (isJSDocAllType(node)) { + return createKeywordTypeNode(SyntaxKind.AnyKeyword); + } + if (isJSDocUnknownType(node)) { + return createKeywordTypeNode(SyntaxKind.UnknownKeyword); + } + if (isJSDocNullableType(node)) { + return createUnionTypeNode([visitNode(node.type, visitExistingNodeTreeSymbols), createKeywordTypeNode(SyntaxKind.NullKeyword)]); + } + if (isJSDocOptionalType(node)) { + return createUnionTypeNode([visitNode(node.type, visitExistingNodeTreeSymbols), createKeywordTypeNode(SyntaxKind.UndefinedKeyword)]); + } + if (isJSDocNonNullableType(node)) { + return visitNode(node.type, visitExistingNodeTreeSymbols); + } + if ((isExpressionWithTypeArguments(node) || isTypeReferenceNode(node)) && isJSDocIndexSignature(node)) { + return createTypeLiteralNode([createIndexSignature( + /*decorators*/ undefined, + /*modifiers*/ undefined, [createParameter( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotdotdotToken*/ undefined, "x", + /*questionToken*/ undefined, visitNode(node.typeArguments![0], visitExistingNodeTreeSymbols))], visitNode(node.typeArguments![1], visitExistingNodeTreeSymbols))]); + } + if (isJSDocFunctionType(node)) { + if (isJSDocConstructSignature(node)) { + let newTypeNode: TypeNode | undefined; + return createConstructorTypeNode(visitNodes(node.typeParameters, visitExistingNodeTreeSymbols), mapDefined(node.parameters, (p, i) => p.name && isIdentifier(p.name) && p.name.escapedText === "new" ? (newTypeNode = p.type, undefined) : createParameter( + /*decorators*/ undefined, + /*modifiers*/ undefined, p.dotDotDotToken, p.name || p.dotDotDotToken ? `args` : `arg${i}`, p.questionToken, visitNode(p.type, visitExistingNodeTreeSymbols), + /*initializer*/ undefined)), visitNode(newTypeNode || node.type, visitExistingNodeTreeSymbols)); + } + else { + return createFunctionTypeNode(visitNodes(node.typeParameters, visitExistingNodeTreeSymbols), map(node.parameters, (p, i) => createParameter( + /*decorators*/ undefined, + /*modifiers*/ undefined, p.dotDotDotToken, p.name || p.dotDotDotToken ? `args` : `arg${i}`, p.questionToken, visitNode(p.type, visitExistingNodeTreeSymbols), + /*initializer*/ undefined)), visitNode(node.type, visitExistingNodeTreeSymbols)); + } + } + if (isLiteralImportTypeNode(node)) { + return updateImportTypeNode(node, updateLiteralTypeNode(node.argument, rewriteModuleSpecifier(node, node.argument.literal)), node.qualifier, visitNodes(node.typeArguments, visitExistingNodeTreeSymbols, isTypeNode), node.isTypeOf); + } + if (isEntityName(node) || isEntityNameExpression(node)) { + const leftmost = getFirstIdentifier(node); + const sym = resolveEntityName(leftmost, SymbolFlags.All, /*ignoreErrors*/ true, /*dontResolveALias*/ true); + if (sym) { + includePrivateSymbol(sym); + if (isIdentifier(node) && sym.flags & SymbolFlags.TypeParameter) { + const name = typeParameterToName(getDeclaredTypeOfSymbol(sym), context); + if (idText(name) !== idText(node)) { + return name; + } + return node; + } + } + } + return visitEachChild(node, visitExistingNodeTreeSymbols, nullTransformationContext); + } + function rewriteModuleSpecifier(parent: ImportTypeNode, lit: StringLiteral) { + if (bundled) { + if (context.tracker && context.tracker.moduleResolverHost) { + const targetFile = getExternalModuleFileFromDeclaration(parent); + if (targetFile) { + const getCanonicalFileName = createGetCanonicalFileName(!!host.useCaseSensitiveFileNames); + const resolverHost = { + getCanonicalFileName, + getCurrentDirectory: context.tracker.moduleResolverHost.getCurrentDirectory ? () => context.tracker.moduleResolverHost!.getCurrentDirectory!() : () => "", + getCommonSourceDirectory: () => context.tracker.moduleResolverHost!.getCommonSourceDirectory() + }; + const newName = getResolvedExternalModuleName(resolverHost, targetFile); + return createLiteral(newName); + } + } + } + else { + if (context.tracker && context.tracker.trackExternalModuleSymbolOfImportTypeNode) { + const moduleSym = resolveExternalModuleNameWorker(lit, lit, /*moduleNotFoundError*/ undefined); + if (moduleSym) { + context.tracker.trackExternalModuleSymbolOfImportTypeNode(moduleSym); + } } } + return lit; } - context.siblings = siblings; } - return context.siblings; - } - - function getPropertiesOfContext(context: WideningContext): Symbol[] { - if (!context.resolvedProperties) { - const names = createMap() as UnderscoreEscapedMap; - for (const t of getSiblingsOfContext(context)) { - if (isObjectLiteralType(t) && !(getObjectFlags(t) & ObjectFlags.ContainsSpread)) { - for (const prop of getPropertiesOfType(t)) { - names.set(prop.escapedName, prop); + function serializeSignatures(kind: SignatureKind, input: ts.Type, baseType: ts.Type | undefined, outputKind: SyntaxKind) { + const signatures = getSignaturesOfType(input, kind); + if (kind === SignatureKind.Construct) { + if (!baseType && every(signatures, s => length(s.parameters) === 0)) { + return []; // No base type, every constructor is empty - elide the extraneous `constructor()` + } + if (baseType) { + // If there is a base type, if every signature in the class is identical to a signature in the baseType, elide all the declarations + const baseSigs = getSignaturesOfType(baseType, SignatureKind.Construct); + if (!length(baseSigs) && every(signatures, s => length(s.parameters) === 0)) { + return []; // Base had no explicit signatures, if all our signatures are also implicit, return an empty list } + if (baseSigs.length === signatures.length) { + let failed = false; + for (let i = 0; i < baseSigs.length; i++) { + if (!compareSignaturesIdentical(signatures[i], baseSigs[i], /*partialMatch*/ false, /*ignoreThisTypes*/ false, /*ignoreReturnTypes*/ true, compareTypesIdentical)) { + failed = true; + break; + } + } + if (!failed) { + return []; // Every signature was identical - elide constructor list as it is inherited + } + } + } + let privateProtected: ModifierFlags = 0; + for (const s of signatures) { + if (s.declaration) { + privateProtected |= getSelectedModifierFlags(s.declaration, ModifierFlags.Private | ModifierFlags.Protected); + } + } + if (privateProtected) { + return [setTextRange(createConstructor( + /*decorators*/ undefined, createModifiersFromModifierFlags(privateProtected), + /*parameters*/ [], + /*body*/ undefined), signatures[0].declaration)]; } } - context.resolvedProperties = arrayFrom(names.values()); - } - return context.resolvedProperties; - } - - function getWidenedProperty(prop: Symbol, context: WideningContext | undefined): Symbol { - if (!(prop.flags & SymbolFlags.Property)) { - // Since get accessors already widen their return value there is no need to - // widen accessor based properties here. - return prop; + const results = []; + for (const sig of signatures) { + // Each overload becomes a separate constructor declaration, in order + const decl = signatureToSignatureDeclarationHelper(sig, outputKind, context); + results.push(setTextRange(decl, sig.declaration)); + } + return results; } - const original = getTypeOfSymbol(prop); - const propContext = context && createWideningContext(context, prop.escapedName, /*siblings*/ undefined); - const widened = getWidenedTypeWithContext(original, propContext); - return widened === original ? prop : createSymbolWithType(prop, widened); - } - - function getUndefinedProperty(prop: Symbol) { - const cached = undefinedProperties.get(prop.escapedName); - if (cached) { - return cached; + function serializeIndexSignatures(input: ts.Type, baseType: ts.Type | undefined) { + const results: IndexSignatureDeclaration[] = []; + for (const type of [IndexKind.String, IndexKind.Number]) { + const info = getIndexInfoOfType(input, type); + if (info) { + if (baseType) { + const baseInfo = getIndexInfoOfType(baseType, type); + if (baseInfo) { + if (isTypeIdenticalTo(info.type, baseInfo.type)) { + continue; // elide identical index signatures + } + } + } + results.push(indexInfoToIndexSignatureDeclarationHelper(info, type, context)); + } + } + return results; } - const result = createSymbolWithType(prop, undefinedType); - result.flags |= SymbolFlags.Optional; - undefinedProperties.set(prop.escapedName, result); - return result; - } - - function getWidenedTypeOfObjectLiteral(type: Type, context: WideningContext | undefined): Type { - const members = createSymbolTable(); - for (const prop of getPropertiesOfObjectType(type)) { - members.set(prop.escapedName, getWidenedProperty(prop, context)); + function serializeBaseType(t: ts.Type, staticType: ts.Type, rootName: string) { + const ref = trySerializeAsTypeReference(t); + if (ref) { + return ref; + } + const tempName = getUnusedName(`${rootName}_base`); + const statement = createVariableStatement(/*modifiers*/ undefined, createVariableDeclarationList([ + createVariableDeclaration(tempName, typeToTypeNodeHelper(staticType, context)) + ], NodeFlags.Const)); + addResult(statement, ModifierFlags.None); + return createExpressionWithTypeArguments(/*typeArgs*/ undefined, createIdentifier(tempName)); + } + function trySerializeAsTypeReference(t: ts.Type) { + let typeArgs: TypeNode[] | undefined; + let reference: Expression | undefined; + // We don't use `isValueSymbolAccessible` below. since that considers alternative containers (like modules) + // which we can't write out in a syntactically valid way as an expression + if ((t as TypeReference).target && getAccessibleSymbolChain((t as TypeReference).target.symbol, enclosingDeclaration, SymbolFlags.Value, /*useOnlyExternalAliasing*/ false)) { + typeArgs = map(getTypeArguments((t as TypeReference)), t => typeToTypeNodeHelper(t, context)); + reference = symbolToExpression((t as TypeReference).target.symbol, context, SymbolFlags.Type); + } + else if (t.symbol && getAccessibleSymbolChain(t.symbol, enclosingDeclaration, SymbolFlags.Value, /*useOnlyExternalAliasing*/ false)) { + reference = symbolToExpression(t.symbol, context, SymbolFlags.Type); + } + if (reference) { + return createExpressionWithTypeArguments(typeArgs, reference); + } } - if (context) { - for (const prop of getPropertiesOfContext(context)) { - if (!members.has(prop.escapedName)) { - members.set(prop.escapedName, getUndefinedProperty(prop)); + function getUnusedName(input: string, symbol?: ts.Symbol): string { + if (symbol) { + if (context.remappedSymbolNames!.has("" + getSymbolId(symbol))) { + return context.remappedSymbolNames!.get("" + getSymbolId(symbol))!; } } - } - const stringIndexInfo = getIndexInfoOfType(type, IndexKind.String); - const numberIndexInfo = getIndexInfoOfType(type, IndexKind.Number); - const result = createAnonymousType(type.symbol, members, emptyArray, emptyArray, - stringIndexInfo && createIndexInfo(getWidenedType(stringIndexInfo.type), stringIndexInfo.isReadonly), - numberIndexInfo && createIndexInfo(getWidenedType(numberIndexInfo.type), numberIndexInfo.isReadonly)); - result.objectFlags |= (getObjectFlags(type) & (ObjectFlags.JSLiteral | ObjectFlags.NonInferrableType)); // Retain js literal flag through widening - return result; - } - - function getWidenedType(type: Type) { - return getWidenedTypeWithContext(type, /*context*/ undefined); - } - - function getWidenedTypeWithContext(type: Type, context: WideningContext | undefined): Type { - if (getObjectFlags(type) & ObjectFlags.RequiresWidening) { - if (context === undefined && type.widened) { - return type.widened; + if (symbol) { + input = getNameCandidateWorker(symbol, input); } - let result: Type | undefined; - if (type.flags & (TypeFlags.Any | TypeFlags.Nullable)) { - result = anyType; + let i = 0; + const original = input; + while (context.usedSymbolNames!.has(input)) { + i++; + input = `${original}_${i}`; } - else if (isObjectLiteralType(type)) { - result = getWidenedTypeOfObjectLiteral(type, context); + context.usedSymbolNames!.set(input, true); + if (symbol) { + context.remappedSymbolNames!.set("" + getSymbolId(symbol), input); } - else if (type.flags & TypeFlags.Union) { - const unionContext = context || createWideningContext(/*parent*/ undefined, /*propertyName*/ undefined, (type).types); - const widenedTypes = sameMap((type).types, t => t.flags & TypeFlags.Nullable ? t : getWidenedTypeWithContext(t, unionContext)); - // Widening an empty object literal transitions from a highly restrictive type to - // a highly inclusive one. For that reason we perform subtype reduction here if the - // union includes empty object types (e.g. reducing {} | string to just {}). - result = getUnionType(widenedTypes, some(widenedTypes, isEmptyObjectType) ? UnionReduction.Subtype : UnionReduction.Literal); + return input; + } + function getNameCandidateWorker(symbol: ts.Symbol, localName: string) { + if (localName === InternalSymbolName.Default || localName === InternalSymbolName.Class || localName === InternalSymbolName.Function) { + const flags = context.flags; + context.flags |= NodeBuilderFlags.InInitialEntityName; + const nameCandidate = getNameOfSymbolAsWritten(symbol, context); + context.flags = flags; + localName = nameCandidate.length > 0 && isSingleOrDoubleQuote(nameCandidate.charCodeAt(0)) ? stripQuotes(nameCandidate) : nameCandidate; } - else if (type.flags & TypeFlags.Intersection) { - result = getIntersectionType(sameMap((type).types, getWidenedType)); + if (localName === InternalSymbolName.Default) { + localName = "_default"; } - else if (isArrayType(type) || isTupleType(type)) { - result = createTypeReference((type).target, sameMap(getTypeArguments(type), getWidenedType)); + else if (localName === InternalSymbolName.ExportEquals) { + localName = "_exports"; } - if (result && context === undefined) { - type.widened = result; + localName = isIdentifierText(localName, languageVersion) && !isStringANonContextualKeyword(localName) ? localName : "_" + localName.replace(/[^a-zA-Z0-9]/g, "_"); + return localName; + } + function getInternalSymbolName(symbol: ts.Symbol, localName: string) { + if (context.remappedSymbolNames!.has("" + getSymbolId(symbol))) { + return context.remappedSymbolNames!.get("" + getSymbolId(symbol))!; } - return result || type; + localName = getNameCandidateWorker(symbol, localName); + // The result of this is going to be used as the symbol's name - lock it in, so `getUnusedName` will also pick it up + context.remappedSymbolNames!.set("" + getSymbolId(symbol), localName); + return localName; } - return type; } - - /** - * Reports implicit any errors that occur as a result of widening 'null' and 'undefined' - * to 'any'. A call to reportWideningErrorsInType is normally accompanied by a call to - * getWidenedType. But in some cases getWidenedType is called without reporting errors - * (type argument inference is an example). - * - * The return value indicates whether an error was in fact reported. The particular circumstances - * are on a best effort basis. Currently, if the null or undefined that causes widening is inside - * an object literal property (arbitrarily deeply), this function reports an error. If no error is - * reported, reportImplicitAnyError is a suitable fallback to report a general error. - */ - function reportWideningErrorsInType(type: Type): boolean { - let errorReported = false; - if (getObjectFlags(type) & ObjectFlags.ContainsWideningType) { - if (type.flags & TypeFlags.Union) { - if (some((type).types, isEmptyObjectType)) { - errorReported = true; - } - else { - for (const t of (type).types) { - if (reportWideningErrorsInType(t)) { - errorReported = true; - } + } + function typePredicateToString(typePredicate: TypePredicate, enclosingDeclaration?: Node, flags: TypeFormatFlags = TypeFormatFlags.UseAliasDefinedOutsideCurrentScope, writer?: EmitTextWriter): string { + return writer ? typePredicateToStringWorker(writer).getText() : usingSingleLineStringWriter(typePredicateToStringWorker); + function typePredicateToStringWorker(writer: EmitTextWriter) { + const predicate = createTypePredicateNodeWithModifier(typePredicate.kind === TypePredicateKind.AssertsThis || typePredicate.kind === TypePredicateKind.AssertsIdentifier ? createToken(SyntaxKind.AssertsKeyword) : undefined, typePredicate.kind === TypePredicateKind.Identifier || typePredicate.kind === TypePredicateKind.AssertsIdentifier ? createIdentifier(typePredicate.parameterName) : createThisTypeNode(), typePredicate.type && (nodeBuilder.typeToTypeNode(typePredicate.type, enclosingDeclaration, toNodeBuilderFlags(flags) | NodeBuilderFlags.IgnoreErrors | NodeBuilderFlags.WriteTypeParametersInQualifiedName)!) // TODO: GH#18217 + ); + const printer = createPrinter({ removeComments: true }); + const sourceFile = enclosingDeclaration && getSourceFileOfNode(enclosingDeclaration); + printer.writeNode(EmitHint.Unspecified, predicate, /*sourceFile*/ sourceFile, writer); + return writer; + } + } + function formatUnionTypes(types: readonly ts.Type[]): ts.Type[] { + const result: ts.Type[] = []; + let flags: TypeFlags = 0; + for (let i = 0; i < types.length; i++) { + const t = types[i]; + flags |= t.flags; + if (!(t.flags & TypeFlags.Nullable)) { + if (t.flags & (TypeFlags.BooleanLiteral | TypeFlags.EnumLiteral)) { + const baseType = t.flags & TypeFlags.BooleanLiteral ? booleanType : getBaseTypeOfEnumLiteralType((t)); + if (baseType.flags & TypeFlags.Union) { + const count = (baseType).types.length; + if (i + count <= types.length && getRegularTypeOfLiteralType(types[i + count - 1]) === getRegularTypeOfLiteralType((baseType).types[count - 1])) { + result.push(baseType); + i += count - 1; + continue; } } } - if (isArrayType(type) || isTupleType(type)) { - for (const t of getTypeArguments(type)) { - if (reportWideningErrorsInType(t)) { - errorReported = true; - } - } + result.push(t); + } + } + if (flags & TypeFlags.Null) + result.push(nullType); + if (flags & TypeFlags.Undefined) + result.push(undefinedType); + return result || types; + } + function visibilityToString(flags: ModifierFlags): string | undefined { + if (flags === ModifierFlags.Private) { + return "private"; + } + if (flags === ModifierFlags.Protected) { + return "protected"; + } + return "public"; + } + function getTypeAliasForTypeLiteral(type: ts.Type): ts.Symbol | undefined { + if (type.symbol && type.symbol.flags & SymbolFlags.TypeLiteral) { + const node = (findAncestor(type.symbol.declarations[0].parent, n => n.kind !== SyntaxKind.ParenthesizedType)!); + if (node.kind === SyntaxKind.TypeAliasDeclaration) { + return getSymbolOfNode(node); + } + } + return undefined; + } + function isTopLevelInExternalModuleAugmentation(node: Node): boolean { + return node && node.parent && + node.parent.kind === SyntaxKind.ModuleBlock && + isExternalModuleAugmentation(node.parent.parent); + } + interface NodeBuilderContext { + enclosingDeclaration: Node | undefined; + flags: NodeBuilderFlags; + tracker: SymbolTracker; + // State + encounteredError: boolean; + visitedTypes: ts.Map | undefined; + symbolDepth: ts.Map | undefined; + inferTypeParameters: TypeParameter[] | undefined; + approximateLength: number; + truncating?: boolean; + typeParameterSymbolList?: ts.Map; + typeParameterNames?: ts.Map; + typeParameterNamesByText?: ts.Map; + usedSymbolNames?: ts.Map; + remappedSymbolNames?: ts.Map; + } + function isDefaultBindingContext(location: Node) { + return location.kind === SyntaxKind.SourceFile || isAmbientModule(location); + } + function getNameOfSymbolFromNameType(symbol: ts.Symbol, context?: NodeBuilderContext) { + const nameType = getSymbolLinks(symbol).nameType; + if (nameType) { + if (nameType.flags & TypeFlags.StringOrNumberLiteral) { + const name = "" + (nameType).value; + if (!isIdentifierText(name, compilerOptions.target) && !isNumericLiteralName(name)) { + return `"${escapeString(name, CharacterCodes.doubleQuote)}"`; } - if (isObjectLiteralType(type)) { - for (const p of getPropertiesOfObjectType(type)) { - const t = getTypeOfSymbol(p); - if (getObjectFlags(t) & ObjectFlags.ContainsWideningType) { - if (!reportWideningErrorsInType(t)) { - error(p.valueDeclaration, Diagnostics.Object_literal_s_property_0_implicitly_has_an_1_type, symbolToString(p), typeToString(getWidenedType(t))); - } - errorReported = true; + if (isNumericLiteralName(name) && startsWith(name, "-")) { + return `[${name}]`; + } + return name; + } + if (nameType.flags & TypeFlags.UniqueESSymbol) { + return `[${getNameOfSymbolAsWritten((nameType).symbol, context)}]`; + } + } + } + /** + * Gets a human-readable name for a symbol. + * Should *not* be used for the right-hand side of a `.` -- use `symbolName(symbol)` for that instead. + * + * Unlike `symbolName(symbol)`, this will include quotes if the name is from a string literal. + * It will also use a representation of a number as written instead of a decimal form, e.g. `0o11` instead of `9`. + */ + function getNameOfSymbolAsWritten(symbol: ts.Symbol, context?: NodeBuilderContext): string { + if (context && symbol.escapedName === InternalSymbolName.Default && !(context.flags & NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope) && + // If it's not the first part of an entity name, it must print as `default` + (!(context.flags & NodeBuilderFlags.InInitialEntityName) || + // if the symbol is synthesized, it will only be referenced externally it must print as `default` + !symbol.declarations || + // if not in the same binding context (source file, module declaration), it must print as `default` + (context.enclosingDeclaration && findAncestor(symbol.declarations[0], isDefaultBindingContext) !== findAncestor(context.enclosingDeclaration, isDefaultBindingContext)))) { + return "default"; + } + if (symbol.declarations && symbol.declarations.length) { + let declaration = firstDefined(symbol.declarations, d => getNameOfDeclaration(d) ? d : undefined); // Try using a declaration with a name, first + const name = declaration && getNameOfDeclaration(declaration); + if (declaration && name) { + if (isCallExpression(declaration) && isBindableObjectDefinePropertyCall(declaration)) { + return symbolName(symbol); + } + if (isComputedPropertyName(name) && !(getCheckFlags(symbol) & CheckFlags.Late)) { + const nameType = getSymbolLinks(symbol).nameType; + if (nameType && nameType.flags & TypeFlags.StringOrNumberLiteral) { + // Computed property name isn't late bound, but has a well-known name type - use name type to generate a symbol name + const result = getNameOfSymbolFromNameType(symbol, context); + if (result !== undefined) { + return result; } } } + return declarationNameToString(name); } - return errorReported; - } - - function reportImplicitAny(declaration: Declaration, type: Type, wideningKind?: WideningKind) { - const typeAsString = typeToString(getWidenedType(type)); - if (isInJSFile(declaration) && !isCheckJsEnabledForFile(getSourceFileOfNode(declaration), compilerOptions)) { - // Only report implicit any errors/suggestions in TS and ts-check JS files - return; + if (!declaration) { + declaration = symbol.declarations[0]; // Declaration may be nameless, but we'll try anyway + } + if (declaration.parent && declaration.parent.kind === SyntaxKind.VariableDeclaration) { + return declarationNameToString((declaration.parent).name); } - let diagnostic: DiagnosticMessage; switch (declaration.kind) { - case SyntaxKind.BinaryExpression: - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.PropertySignature: - diagnostic = noImplicitAny ? Diagnostics.Member_0_implicitly_has_an_1_type : Diagnostics.Member_0_implicitly_has_an_1_type_but_a_better_type_may_be_inferred_from_usage; - break; - case SyntaxKind.Parameter: - const param = declaration as ParameterDeclaration; - if (isIdentifier(param.name) && - (isCallSignatureDeclaration(param.parent) || isMethodSignature(param.parent) || isFunctionTypeNode(param.parent)) && - param.parent.parameters.indexOf(param) > -1 && - (resolveName(param, param.name.escapedText, SymbolFlags.Type, undefined, param.name.escapedText, /*isUse*/ true) || - param.name.originalKeywordKind && isTypeNodeKind(param.name.originalKeywordKind))) { - const newName = "arg" + param.parent.parameters.indexOf(param); - errorOrSuggestion(noImplicitAny, declaration, Diagnostics.Parameter_has_a_name_but_no_type_Did_you_mean_0_Colon_1, newName, declarationNameToString(param.name)); - return; + case SyntaxKind.ClassExpression: + case SyntaxKind.FunctionExpression: + case SyntaxKind.ArrowFunction: + if (context && !context.encounteredError && !(context.flags & NodeBuilderFlags.AllowAnonymousIdentifier)) { + context.encounteredError = true; } - diagnostic = (declaration).dotDotDotToken ? - noImplicitAny ? Diagnostics.Rest_parameter_0_implicitly_has_an_any_type : Diagnostics.Rest_parameter_0_implicitly_has_an_any_type_but_a_better_type_may_be_inferred_from_usage : - noImplicitAny ? Diagnostics.Parameter_0_implicitly_has_an_1_type : Diagnostics.Parameter_0_implicitly_has_an_1_type_but_a_better_type_may_be_inferred_from_usage; - break; + return declaration.kind === SyntaxKind.ClassExpression ? "(Anonymous class)" : "(Anonymous function)"; + } + } + const name = getNameOfSymbolFromNameType(symbol, context); + return name !== undefined ? name : symbolName(symbol); + } + function isDeclarationVisible(node: Node): boolean { + if (node) { + const links = getNodeLinks(node); + if (links.isVisible === undefined) { + links.isVisible = !!determineIfDeclarationIsVisible(); + } + return links.isVisible; + } + return false; + function determineIfDeclarationIsVisible() { + switch (node.kind) { + case SyntaxKind.JSDocCallbackTag: + case SyntaxKind.JSDocTypedefTag: + case SyntaxKind.JSDocEnumTag: + // Top-level jsdoc type aliases are considered exported + // First parent is comment node, second is hosting declaration or token; we only care about those tokens or declarations whose parent is a source file + return !!(node.parent && node.parent.parent && node.parent.parent.parent && isSourceFile(node.parent.parent.parent)); case SyntaxKind.BindingElement: - diagnostic = Diagnostics.Binding_element_0_implicitly_has_an_1_type; - if (!noImplicitAny) { - // Don't issue a suggestion for binding elements since the codefix doesn't yet support them. - return; + return isDeclarationVisible(node.parent.parent); + case SyntaxKind.VariableDeclaration: + if (isBindingPattern((node as VariableDeclaration).name) && + !((node as VariableDeclaration).name as BindingPattern).elements.length) { + // If the binding pattern is empty, this variable declaration is not visible + return false; } - break; - case SyntaxKind.JSDocFunctionType: - error(declaration, Diagnostics.Function_type_which_lacks_return_type_annotation_implicitly_has_an_0_return_type, typeAsString); - return; + // falls through + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.TypeAliasDeclaration: case SyntaxKind.FunctionDeclaration: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.MethodSignature: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.ImportEqualsDeclaration: + // external module augmentation is always visible + if (isExternalModuleAugmentation(node)) { + return true; + } + const parent = getDeclarationContainer(node); + // If the node is not exported or it is not ambient module element (except import declaration) + if (!(getCombinedModifierFlags((node as Declaration)) & ModifierFlags.Export) && + !(node.kind !== SyntaxKind.ImportEqualsDeclaration && parent.kind !== SyntaxKind.SourceFile && parent.flags & NodeFlags.Ambient)) { + return isGlobalSourceFile(parent); + } + // Exported members/ambient module elements (exception import declaration) are visible if parent is visible + return isDeclarationVisible(parent); + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.PropertySignature: case SyntaxKind.GetAccessor: case SyntaxKind.SetAccessor: - case SyntaxKind.FunctionExpression: - case SyntaxKind.ArrowFunction: - if (noImplicitAny && !(declaration as NamedDeclaration).name) { - if (wideningKind === WideningKind.GeneratorYield) { - error(declaration, Diagnostics.Generator_implicitly_has_yield_type_0_because_it_does_not_yield_any_values_Consider_supplying_a_return_type_annotation, typeAsString); - } - else { - error(declaration, Diagnostics.Function_expression_which_lacks_return_type_annotation_implicitly_has_an_0_return_type, typeAsString); - } - return; - } - diagnostic = !noImplicitAny ? Diagnostics._0_implicitly_has_an_1_return_type_but_a_better_type_may_be_inferred_from_usage : - wideningKind === WideningKind.GeneratorYield ? Diagnostics._0_which_lacks_return_type_annotation_implicitly_has_an_1_yield_type : - Diagnostics._0_which_lacks_return_type_annotation_implicitly_has_an_1_return_type; - break; - case SyntaxKind.MappedType: - if (noImplicitAny) { - error(declaration, Diagnostics.Mapped_object_type_implicitly_has_an_any_template_type); + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: + if (hasModifier(node, ModifierFlags.Private | ModifierFlags.Protected)) { + // Private/protected properties/methods are not visible + return false; } - return; + // Public properties/methods are visible if its parents are visible, so: + // falls through + case SyntaxKind.Constructor: + case SyntaxKind.ConstructSignature: + case SyntaxKind.CallSignature: + case SyntaxKind.IndexSignature: + case SyntaxKind.Parameter: + case SyntaxKind.ModuleBlock: + case SyntaxKind.FunctionType: + case SyntaxKind.ConstructorType: + case SyntaxKind.TypeLiteral: + case SyntaxKind.TypeReference: + case SyntaxKind.ArrayType: + case SyntaxKind.TupleType: + case SyntaxKind.UnionType: + case SyntaxKind.IntersectionType: + case SyntaxKind.ParenthesizedType: + return isDeclarationVisible(node.parent); + // Default binding, import specifier and namespace import is visible + // only on demand so by default it is not visible + case SyntaxKind.ImportClause: + case SyntaxKind.NamespaceImport: + case SyntaxKind.ImportSpecifier: + return false; + // Type parameters are always visible + case SyntaxKind.TypeParameter: + // Source file and namespace export are always visible + // falls through + case SyntaxKind.SourceFile: + case SyntaxKind.NamespaceExportDeclaration: + return true; + // Export assignments do not create name bindings outside the module + case SyntaxKind.ExportAssignment: + return false; default: - diagnostic = noImplicitAny ? Diagnostics.Variable_0_implicitly_has_an_1_type : Diagnostics.Variable_0_implicitly_has_an_1_type_but_a_better_type_may_be_inferred_from_usage; + return false; } - errorOrSuggestion(noImplicitAny, declaration, diagnostic, declarationNameToString(getNameOfDeclaration(declaration)), typeAsString); } - - function reportErrorsFromWidening(declaration: Declaration, type: Type, wideningKind?: WideningKind) { - if (produceDiagnostics && noImplicitAny && getObjectFlags(type) & ObjectFlags.ContainsWideningType) { - // Report implicit any error within type if possible, otherwise report error on declaration - if (!reportWideningErrorsInType(type)) { - reportImplicitAny(declaration, type, wideningKind); + } + function collectLinkedAliases(node: Identifier, setVisibility?: boolean): Node[] | undefined { + let exportSymbol: ts.Symbol | undefined; + if (node.parent && node.parent.kind === SyntaxKind.ExportAssignment) { + exportSymbol = resolveName(node, node.escapedText, SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace | SymbolFlags.Alias, /*nameNotFoundMessage*/ undefined, node, /*isUse*/ false); + } + else if (node.parent.kind === SyntaxKind.ExportSpecifier) { + exportSymbol = getTargetOfExportSpecifier((node.parent), SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace | SymbolFlags.Alias); + } + let result: Node[] | undefined; + let visited: ts.Map | undefined; + if (exportSymbol) { + visited = createMap(); + visited.set("" + getSymbolId(exportSymbol), true); + buildVisibleNodeList(exportSymbol.declarations); + } + return result; + function buildVisibleNodeList(declarations: Declaration[]) { + forEach(declarations, declaration => { + const resultNode = getAnyImportSyntax(declaration) || declaration; + if (setVisibility) { + getNodeLinks(declaration).isVisible = true; + } + else { + result = result || []; + pushIfUnique(result, resultNode); + } + if (isInternalModuleImportEqualsDeclaration(declaration)) { + // Add the referenced top container visible + const internalModuleReference = (declaration.moduleReference); + const firstIdentifier = getFirstIdentifier(internalModuleReference); + const importSymbol = resolveName(declaration, firstIdentifier.escapedText, SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace, undefined, undefined, /*isUse*/ false); + const id = importSymbol && "" + getSymbolId(importSymbol); + if (importSymbol && !visited!.has(id!)) { + visited!.set(id!, true); + buildVisibleNodeList(importSymbol.declarations); + } } + }); + } + } + /** + * Push an entry on the type resolution stack. If an entry with the given target and the given property name + * is already on the stack, and no entries in between already have a type, then a circularity has occurred. + * In this case, the result values of the existing entry and all entries pushed after it are changed to false, + * and the value false is returned. Otherwise, the new entry is just pushed onto the stack, and true is returned. + * In order to see if the same query has already been done before, the target object and the propertyName both + * must match the one passed in. + * + * @param target The symbol, type, or signature whose type is being queried + * @param propertyName The property name that should be used to query the target for its type + */ + function pushTypeResolution(target: TypeSystemEntity, propertyName: TypeSystemPropertyName): boolean { + const resolutionCycleStartIndex = findResolutionCycleStartIndex(target, propertyName); + if (resolutionCycleStartIndex >= 0) { + // A cycle was found + const { length } = resolutionTargets; + for (let i = resolutionCycleStartIndex; i < length; i++) { + resolutionResults[i] = false; } + return false; } - - function applyToParameterTypes(source: Signature, target: Signature, callback: (s: Type, t: Type) => void) { - const sourceCount = getParameterCount(source); - const targetCount = getParameterCount(target); - const sourceRestType = getEffectiveRestType(source); - const targetRestType = getEffectiveRestType(target); - const targetNonRestCount = targetRestType ? targetCount - 1 : targetCount; - const paramCount = sourceRestType ? targetNonRestCount : Math.min(sourceCount, targetNonRestCount); - const sourceThisType = getThisTypeOfSignature(source); - if (sourceThisType) { - const targetThisType = getThisTypeOfSignature(target); - if (targetThisType) { - callback(sourceThisType, targetThisType); - } + resolutionTargets.push(target); + resolutionResults.push(/*items*/ true); + resolutionPropertyNames.push(propertyName); + return true; + } + function findResolutionCycleStartIndex(target: TypeSystemEntity, propertyName: TypeSystemPropertyName): number { + for (let i = resolutionTargets.length - 1; i >= 0; i--) { + if (hasType(resolutionTargets[i], resolutionPropertyNames[i])) { + return -1; } - for (let i = 0; i < paramCount; i++) { - callback(getTypeAtPosition(source, i), getTypeAtPosition(target, i)); + if (resolutionTargets[i] === target && resolutionPropertyNames[i] === propertyName) { + return i; } - if (targetRestType) { - callback(getRestTypeAtPosition(source, paramCount), targetRestType); + } + return -1; + } + function hasType(target: TypeSystemEntity, propertyName: TypeSystemPropertyName): boolean { + switch (propertyName) { + case TypeSystemPropertyName.Type: + return !!getSymbolLinks((target)).type; + case TypeSystemPropertyName.EnumTagType: + return !!(getNodeLinks((target as JSDocEnumTag)).resolvedEnumType); + case TypeSystemPropertyName.DeclaredType: + return !!getSymbolLinks((target)).declaredType; + case TypeSystemPropertyName.ResolvedBaseConstructorType: + return !!(target).resolvedBaseConstructorType; + case TypeSystemPropertyName.ResolvedReturnType: + return !!(target).resolvedReturnType; + case TypeSystemPropertyName.ImmediateBaseConstraint: + return !!(target).immediateBaseConstraint; + case TypeSystemPropertyName.ResolvedTypeArguments: + return !!(target as TypeReference).resolvedTypeArguments; + } + return Debug.assertNever(propertyName); + } + /** + * Pop an entry from the type resolution stack and return its associated result value. The result value will + * be true if no circularities were detected, or false if a circularity was found. + */ + function popTypeResolution(): boolean { + resolutionTargets.pop(); + resolutionPropertyNames.pop(); + return resolutionResults.pop()!; + } + function getDeclarationContainer(node: Node): Node { + return findAncestor(getRootDeclaration(node), node => { + switch (node.kind) { + case SyntaxKind.VariableDeclaration: + case SyntaxKind.VariableDeclarationList: + case SyntaxKind.ImportSpecifier: + case SyntaxKind.NamedImports: + case SyntaxKind.NamespaceImport: + case SyntaxKind.ImportClause: + return false; + default: + return true; } + })!.parent; + } + function getTypeOfPrototypeProperty(prototype: ts.Symbol): ts.Type { + // TypeScript 1.0 spec (April 2014): 8.4 + // Every class automatically contains a static property member named 'prototype', + // the type of which is an instantiation of the class type with type Any supplied as a type argument for each type parameter. + // It is an error to explicitly declare a static property member with the name 'prototype'. + const classType = (getDeclaredTypeOfSymbol(getParentOfSymbol(prototype)!)); + return classType.typeParameters ? createTypeReference((classType), map(classType.typeParameters, _ => anyType)) : classType; + } + // Return the type of the given property in the given type, or undefined if no such property exists + function getTypeOfPropertyOfType(type: ts.Type, name: __String): ts.Type | undefined { + const prop = getPropertyOfType(type, name); + return prop ? getTypeOfSymbol(prop) : undefined; + } + function getTypeOfPropertyOrIndexSignature(type: ts.Type, name: __String): ts.Type { + return getTypeOfPropertyOfType(type, name) || isNumericLiteralName(name) && getIndexTypeOfType(type, IndexKind.Number) || getIndexTypeOfType(type, IndexKind.String) || unknownType; + } + function isTypeAny(type: ts.Type | undefined) { + return type && (type.flags & TypeFlags.Any) !== 0; + } + // Return the type of a binding element parent. We check SymbolLinks first to see if a type has been + // assigned by contextual typing. + function getTypeForBindingElementParent(node: BindingElementGrandparent) { + const symbol = getSymbolOfNode(node); + return symbol && getSymbolLinks(symbol).type || getTypeForVariableLikeDeclaration(node, /*includeOptionality*/ false); + } + function isComputedNonLiteralName(name: PropertyName): boolean { + return name.kind === SyntaxKind.ComputedPropertyName && !isStringOrNumericLiteralLike(name.expression); + } + function getRestType(source: ts.Type, properties: PropertyName[], symbol: ts.Symbol | undefined): ts.Type { + source = filterType(source, t => !(t.flags & TypeFlags.Nullable)); + if (source.flags & TypeFlags.Never) { + return emptyObjectType; } - - function applyToReturnTypes(source: Signature, target: Signature, callback: (s: Type, t: Type) => void) { - const sourceTypePredicate = getTypePredicateOfSignature(source); - const targetTypePredicate = getTypePredicateOfSignature(target); - if (sourceTypePredicate && targetTypePredicate && typePredicateKindsMatch(sourceTypePredicate, targetTypePredicate) && sourceTypePredicate.type && targetTypePredicate.type) { - callback(sourceTypePredicate.type, targetTypePredicate.type); + if (source.flags & TypeFlags.Union) { + return mapType(source, t => getRestType(t, properties, symbol)); + } + const omitKeyType = getUnionType(map(properties, getLiteralTypeFromPropertyName)); + if (isGenericObjectType(source) || isGenericIndexType(omitKeyType)) { + if (omitKeyType.flags & TypeFlags.Never) { + return source; } - else { - callback(getReturnTypeOfSignature(source), getReturnTypeOfSignature(target)); + const omitTypeAlias = getGlobalOmitSymbol(); + if (!omitTypeAlias) { + return errorType; } + return getTypeAliasInstantiation(omitTypeAlias, [source, omitKeyType]); } - - function createInferenceContext(typeParameters: readonly TypeParameter[], signature: Signature | undefined, flags: InferenceFlags, compareTypes?: TypeComparer): InferenceContext { - return createInferenceContextWorker(typeParameters.map(createInferenceInfo), signature, flags, compareTypes || compareTypesAssignable); + const members = createSymbolTable(); + for (const prop of getPropertiesOfType(source)) { + if (!isTypeAssignableTo(getLiteralTypeFromProperty(prop, TypeFlags.StringOrNumberLiteralOrUnique), omitKeyType) + && !(getDeclarationModifierFlagsFromSymbol(prop) & (ModifierFlags.Private | ModifierFlags.Protected)) + && isSpreadableProperty(prop)) { + members.set(prop.escapedName, getSpreadSymbol(prop, /*readonly*/ false)); + } } - - function cloneInferenceContext(context: T, extraFlags: InferenceFlags = 0): InferenceContext | T & undefined { - return context && createInferenceContextWorker(map(context.inferences, cloneInferenceInfo), context.signature, context.flags | extraFlags, context.compareTypes); + const stringIndexInfo = getIndexInfoOfType(source, IndexKind.String); + const numberIndexInfo = getIndexInfoOfType(source, IndexKind.Number); + const result = createAnonymousType(symbol, members, emptyArray, emptyArray, stringIndexInfo, numberIndexInfo); + result.objectFlags |= ObjectFlags.ObjectRestType; + return result; + } + // Determine the control flow type associated with a destructuring declaration or assignment. The following + // forms of destructuring are possible: + // let { x } = obj; // BindingElement + // let [ x ] = obj; // BindingElement + // { x } = obj; // ShorthandPropertyAssignment + // { x: v } = obj; // PropertyAssignment + // [ x ] = obj; // Expression + // We construct a synthetic element access expression corresponding to 'obj.x' such that the control + // flow analyzer doesn't have to handle all the different syntactic forms. + function getFlowTypeOfDestructuring(node: BindingElement | PropertyAssignment | ShorthandPropertyAssignment | Expression, declaredType: ts.Type) { + const reference = getSyntheticElementAccess(node); + return reference ? getFlowTypeOfReference(reference, declaredType) : declaredType; + } + function getSyntheticElementAccess(node: BindingElement | PropertyAssignment | ShorthandPropertyAssignment | Expression): ElementAccessExpression | undefined { + const parentAccess = getParentElementAccess(node); + if (parentAccess && parentAccess.flowNode) { + const propName = getDestructuringPropertyName(node); + if (propName) { + const result = (createNode(SyntaxKind.ElementAccessExpression, node.pos, node.end)); + result.parent = node; + result.expression = (parentAccess); + const literal = (createNode(SyntaxKind.StringLiteral, node.pos, node.end)); + literal.parent = result; + literal.text = propName; + result.argumentExpression = literal; + result.flowNode = parentAccess.flowNode; + return result; + } } - - function createInferenceContextWorker(inferences: InferenceInfo[], signature: Signature | undefined, flags: InferenceFlags, compareTypes: TypeComparer): InferenceContext { - const context: InferenceContext = { - inferences, - signature, - flags, - compareTypes, - mapper: t => mapToInferredType(context, t, /*fix*/ true), - nonFixingMapper: t => mapToInferredType(context, t, /*fix*/ false), - }; - return context; + } + function getParentElementAccess(node: BindingElement | PropertyAssignment | ShorthandPropertyAssignment | Expression) { + const ancestor = node.parent.parent; + switch (ancestor.kind) { + case SyntaxKind.BindingElement: + case SyntaxKind.PropertyAssignment: + return getSyntheticElementAccess((ancestor)); + case SyntaxKind.ArrayLiteralExpression: + return getSyntheticElementAccess((node.parent)); + case SyntaxKind.VariableDeclaration: + return (ancestor).initializer; + case SyntaxKind.BinaryExpression: + return (ancestor).right; } - - function mapToInferredType(context: InferenceContext, t: Type, fix: boolean): Type { - const inferences = context.inferences; - for (let i = 0; i < inferences.length; i++) { - const inference = inferences[i]; - if (t === inference.typeParameter) { - if (fix && !inference.isFixed) { - clearCachedInferences(inferences); - inference.isFixed = true; + } + function getDestructuringPropertyName(node: BindingElement | PropertyAssignment | ShorthandPropertyAssignment | Expression) { + const parent = node.parent; + if (node.kind === SyntaxKind.BindingElement && parent.kind === SyntaxKind.ObjectBindingPattern) { + return getLiteralPropertyNameText((node).propertyName || ((node).name)); + } + if (node.kind === SyntaxKind.PropertyAssignment || node.kind === SyntaxKind.ShorthandPropertyAssignment) { + return getLiteralPropertyNameText((node).name); + } + return "" + (>(parent).elements).indexOf(node); + } + function getLiteralPropertyNameText(name: PropertyName) { + const type = getLiteralTypeFromPropertyName(name); + return type.flags & (TypeFlags.StringLiteral | TypeFlags.NumberLiteral) ? "" + (type).value : undefined; + } + /** Return the inferred type for a binding element */ + function getTypeForBindingElement(declaration: BindingElement): ts.Type | undefined { + const pattern = declaration.parent; + let parentType = getTypeForBindingElementParent(pattern.parent); + // If no type or an any type was inferred for parent, infer that for the binding element + if (!parentType || isTypeAny(parentType)) { + return parentType; + } + // Relax null check on ambient destructuring parameters, since the parameters have no implementation and are just documentation + if (strictNullChecks && declaration.flags & NodeFlags.Ambient && isParameterDeclaration(declaration)) { + parentType = getNonNullableType(parentType); + } + let type: ts.Type | undefined; + if (pattern.kind === SyntaxKind.ObjectBindingPattern) { + if (declaration.dotDotDotToken) { + if (parentType.flags & TypeFlags.Unknown || !isValidSpreadType(parentType)) { + error(declaration, Diagnostics.Rest_types_may_only_be_created_from_object_types); + return errorType; + } + const literalMembers: PropertyName[] = []; + for (const element of pattern.elements) { + if (!element.dotDotDotToken) { + literalMembers.push(element.propertyName || (element.name as Identifier)); } - return getInferredType(context, i); } + type = getRestType(parentType, literalMembers, declaration.symbol); } - return t; - } - - function clearCachedInferences(inferences: InferenceInfo[]) { - for (const inference of inferences) { - if (!inference.isFixed) { - inference.inferredType = undefined; - } + else { + // Use explicitly specified property name ({ p: xxx } form), or otherwise the implied name ({ p } form) + const name = declaration.propertyName || (declaration.name); + const indexType = getLiteralTypeFromPropertyName(name); + const declaredType = getConstraintForLocation(getIndexedAccessType(parentType, indexType, name), declaration.name); + type = getFlowTypeOfDestructuring(declaration, declaredType); } } - - function createInferenceInfo(typeParameter: TypeParameter): InferenceInfo { - return { - typeParameter, - candidates: undefined, - contraCandidates: undefined, - inferredType: undefined, - priority: undefined, - topLevel: true, - isFixed: false - }; + else { + // This elementType will be used if the specific property corresponding to this index is not + // present (aka the tuple element property). This call also checks that the parentType is in + // fact an iterable or array (depending on target language). + const elementType = checkIteratedTypeOrElementType(IterationUse.Destructuring, parentType, undefinedType, pattern); + const index = pattern.elements.indexOf(declaration); + if (declaration.dotDotDotToken) { + // If the parent is a tuple type, the rest element has a tuple type of the + // remaining tuple element types. Otherwise, the rest element has an array type with same + // element type as the parent type. + type = everyType(parentType, isTupleType) ? + mapType(parentType, t => sliceTupleType((t), index)) : + createArrayType(elementType); + } + else if (isArrayLikeType(parentType)) { + const indexType = getLiteralType(index); + const accessFlags = hasDefaultValue(declaration) ? AccessFlags.NoTupleBoundsCheck : 0; + const declaredType = getConstraintForLocation(getIndexedAccessTypeOrUndefined(parentType, indexType, declaration.name, accessFlags) || errorType, declaration.name); + type = getFlowTypeOfDestructuring(declaration, declaredType); + } + else { + type = elementType; + } } - - function cloneInferenceInfo(inference: InferenceInfo): InferenceInfo { - return { - typeParameter: inference.typeParameter, - candidates: inference.candidates && inference.candidates.slice(), - contraCandidates: inference.contraCandidates && inference.contraCandidates.slice(), - inferredType: inference.inferredType, - priority: inference.priority, - topLevel: inference.topLevel, - isFixed: inference.isFixed - }; + if (!declaration.initializer) { + return type; } - - function cloneInferredPartOfContext(context: InferenceContext): InferenceContext | undefined { - const inferences = filter(context.inferences, hasInferenceCandidates); - return inferences.length ? - createInferenceContextWorker(map(inferences, cloneInferenceInfo), context.signature, context.flags, context.compareTypes) : - undefined; + if (getEffectiveTypeAnnotationNode(walkUpBindingElementsAndPatterns(declaration))) { + // In strict null checking mode, if a default value of a non-undefined type is specified, remove + // undefined from the final type. + return strictNullChecks && !(getFalsyFlags(checkDeclarationInitializer(declaration)) & TypeFlags.Undefined) ? + getTypeWithFacts(type, TypeFacts.NEUndefined) : + type; } - - function getMapperFromContext(context: T): TypeMapper | T & undefined { - return context && context.mapper; + return widenTypeInferredFromInitializer(declaration, getUnionType([getTypeWithFacts(type, TypeFacts.NEUndefined), checkDeclarationInitializer(declaration)], UnionReduction.Subtype)); + } + function getTypeForDeclarationFromJSDocComment(declaration: Node) { + const jsdocType = getJSDocType(declaration); + if (jsdocType) { + return getTypeFromTypeNode(jsdocType); } - - // Return true if the given type could possibly reference a type parameter for which - // we perform type inference (i.e. a type parameter of a generic function). We cache - // results for union and intersection types for performance reasons. - function couldContainTypeVariables(type: Type): boolean { - const objectFlags = getObjectFlags(type); - if (objectFlags & ObjectFlags.CouldContainTypeVariablesComputed) { - return !!(objectFlags & ObjectFlags.CouldContainTypeVariables); + return undefined; + } + function isNullOrUndefined(node: Expression) { + const expr = skipParentheses(node); + return expr.kind === SyntaxKind.NullKeyword || expr.kind === SyntaxKind.Identifier && getResolvedSymbol((expr)) === undefinedSymbol; + } + function isEmptyArrayLiteral(node: Expression) { + const expr = skipParentheses(node); + return expr.kind === SyntaxKind.ArrayLiteralExpression && (expr).elements.length === 0; + } + function addOptionality(type: ts.Type, optional = true): ts.Type { + return strictNullChecks && optional ? getOptionalType(type) : type; + } + // Return the inferred type for a variable, parameter, or property declaration + function getTypeForVariableLikeDeclaration(declaration: ParameterDeclaration | PropertyDeclaration | PropertySignature | VariableDeclaration | BindingElement, includeOptionality: boolean): ts.Type | undefined { + // A variable declared in a for..in statement is of type string, or of type keyof T when the + // right hand expression is of a type parameter type. + if (isVariableDeclaration(declaration) && declaration.parent.parent.kind === SyntaxKind.ForInStatement) { + const indexType = getIndexType(getNonNullableTypeIfNeeded(checkExpression(declaration.parent.parent.expression))); + return indexType.flags & (TypeFlags.TypeParameter | TypeFlags.Index) ? getExtractStringType(indexType) : stringType; + } + if (isVariableDeclaration(declaration) && declaration.parent.parent.kind === SyntaxKind.ForOfStatement) { + // checkRightHandSideOfForOf will return undefined if the for-of expression type was + // missing properties/signatures required to get its iteratedType (like + // [Symbol.iterator] or next). This may be because we accessed properties from anyType, + // or it may have led to an error inside getElementTypeOfIterable. + const forOfStatement = declaration.parent.parent; + return checkRightHandSideOfForOf(forOfStatement.expression, forOfStatement.awaitModifier) || anyType; + } + if (isBindingPattern(declaration.parent)) { + return getTypeForBindingElement((declaration)); + } + const isOptional = includeOptionality && (isParameter(declaration) && isJSDocOptionalParameter(declaration) + || !isBindingElement(declaration) && !isVariableDeclaration(declaration) && !!declaration.questionToken); + // Use type from type annotation if one is present + const declaredType = tryGetTypeFromEffectiveTypeNode(declaration); + if (declaredType) { + return addOptionality(declaredType, isOptional); + } + if ((noImplicitAny || isInJSFile(declaration)) && + declaration.kind === SyntaxKind.VariableDeclaration && !isBindingPattern(declaration.name) && + !(getCombinedModifierFlags(declaration) & ModifierFlags.Export) && !(declaration.flags & NodeFlags.Ambient)) { + // If --noImplicitAny is on or the declaration is in a Javascript file, + // use control flow tracked 'any' type for non-ambient, non-exported var or let variables with no + // initializer or a 'null' or 'undefined' initializer. + if (!(getCombinedNodeFlags(declaration) & NodeFlags.Const) && (!declaration.initializer || isNullOrUndefined(declaration.initializer))) { + return autoType; + } + // Use control flow tracked 'any[]' type for non-ambient, non-exported variables with an empty array + // literal initializer. + if (declaration.initializer && isEmptyArrayLiteral(declaration.initializer)) { + return autoArrayType; + } + } + if (declaration.kind === SyntaxKind.Parameter) { + const func = (declaration.parent); + // For a parameter of a set accessor, use the type of the get accessor if one is present + if (func.kind === SyntaxKind.SetAccessor && !hasNonBindableDynamicName(func)) { + const getter = getDeclarationOfKind(getSymbolOfNode(declaration.parent), SyntaxKind.GetAccessor); + if (getter) { + const getterSignature = getSignatureFromDeclaration(getter); + const thisParameter = getAccessorThisParameter((func as AccessorDeclaration)); + if (thisParameter && declaration === thisParameter) { + // Use the type from the *getter* + Debug.assert(!thisParameter.type); + return getTypeOfSymbol(getterSignature.thisParameter!); + } + return getReturnTypeOfSignature(getterSignature); + } } - const result = !!(type.flags & TypeFlags.Instantiable || - objectFlags & ObjectFlags.Reference && ((type).node || forEach(getTypeArguments(type), couldContainTypeVariables)) || - objectFlags & ObjectFlags.Anonymous && type.symbol && type.symbol.flags & (SymbolFlags.Function | SymbolFlags.Method | SymbolFlags.Class | SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral) && type.symbol.declarations || - objectFlags & (ObjectFlags.Mapped | ObjectFlags.ObjectRestType) || - type.flags & TypeFlags.UnionOrIntersection && !(type.flags & TypeFlags.EnumLiteral) && some((type).types, couldContainTypeVariables)); - if (type.flags & TypeFlags.ObjectFlagsType) { - (type).objectFlags |= ObjectFlags.CouldContainTypeVariablesComputed | (result ? ObjectFlags.CouldContainTypeVariables : 0); + if (isInJSFile(declaration)) { + const typeTag = getJSDocType(func); + if (typeTag && isFunctionTypeNode(typeTag)) { + return getTypeAtPosition(getSignatureFromDeclaration(typeTag), func.parameters.indexOf(declaration)); + } + } + // Use contextual parameter type if one is available + const type = declaration.symbol.escapedName === InternalSymbolName.This ? getContextualThisParameterType(func) : getContextuallyTypedParameterType(declaration); + if (type) { + return addOptionality(type, isOptional); } - return result; } - - function isTypeParameterAtTopLevel(type: Type, typeParameter: TypeParameter): boolean { - return !!(type === typeParameter || - type.flags & TypeFlags.UnionOrIntersection && some((type).types, t => isTypeParameterAtTopLevel(t, typeParameter)) || - type.flags & TypeFlags.Conditional && ( - isTypeParameterAtTopLevel(getTrueTypeFromConditionalType(type), typeParameter) || - isTypeParameterAtTopLevel(getFalseTypeFromConditionalType(type), typeParameter))); + else if (isInJSFile(declaration)) { + const containerObjectType = getJSContainerObjectType(declaration, getSymbolOfNode(declaration), getDeclaredExpandoInitializer(declaration)); + if (containerObjectType) { + return containerObjectType; + } } - - /** Create an object with properties named in the string literal type. Every property has type `any` */ - function createEmptyObjectTypeFromStringLiteral(type: Type) { - const members = createSymbolTable(); - forEachType(type, t => { - if (!(t.flags & TypeFlags.StringLiteral)) { - return; - } - const name = escapeLeadingUnderscores((t as StringLiteralType).value); - const literalProp = createSymbol(SymbolFlags.Property, name); - literalProp.type = anyType; - if (t.symbol) { - literalProp.declarations = t.symbol.declarations; - literalProp.valueDeclaration = t.symbol.valueDeclaration; - } - members.set(name, literalProp); - }); - const indexInfo = type.flags & TypeFlags.String ? createIndexInfo(emptyObjectType, /*isReadonly*/ false) : undefined; - return createAnonymousType(undefined, members, emptyArray, emptyArray, indexInfo, undefined); + // Use the type of the initializer expression if one is present and the declaration is + // not a parameter of a contextually typed function + if (declaration.initializer) { + const type = widenTypeInferredFromInitializer(declaration, checkDeclarationInitializer(declaration)); + return addOptionality(type, isOptional); } - - /** - * Infer a suitable input type for a homomorphic mapped type { [P in keyof T]: X }. We construct - * an object type with the same set of properties as the source type, where the type of each - * property is computed by inferring from the source property type to X for the type - * variable T[P] (i.e. we treat the type T[P] as the type variable we're inferring for). - */ - function inferTypeForHomomorphicMappedType(source: Type, target: MappedType, constraint: IndexType): Type | undefined { - const key = source.id + "," + target.id + "," + constraint.id; - if (reverseMappedCache.has(key)) { - return reverseMappedCache.get(key); - } - reverseMappedCache.set(key, undefined); - const type = createReverseMappedType(source, target, constraint); - reverseMappedCache.set(key, type); - return type; + if (isJsxAttribute(declaration)) { + // if JSX attribute doesn't have initializer, by default the attribute will have boolean value of true. + // I.e is sugar for + return trueType; } - - // We consider a type to be partially inferable if it isn't marked non-inferable or if it is - // an object literal type with at least one property of an inferable type. For example, an object - // literal { a: 123, b: x => true } is marked non-inferable because it contains a context sensitive - // arrow function, but is considered partially inferable because property 'a' has an inferable type. - function isPartiallyInferableType(type: Type): boolean { - return !(getObjectFlags(type) & ObjectFlags.NonInferrableType) || - isObjectLiteralType(type) && some(getPropertiesOfType(type), prop => isPartiallyInferableType(getTypeOfSymbol(prop))); + // If the declaration specifies a binding pattern and is not a parameter of a contextually + // typed function, use the type implied by the binding pattern + if (isBindingPattern(declaration.name)) { + return getTypeFromBindingPattern(declaration.name, /*includePatternInType*/ false, /*reportErrors*/ true); } - - function createReverseMappedType(source: Type, target: MappedType, constraint: IndexType) { - // We consider a source type reverse mappable if it has a string index signature or if - // it has one or more properties and is of a partially inferable type. - if (!(getIndexInfoOfType(source, IndexKind.String) || getPropertiesOfType(source).length !== 0 && isPartiallyInferableType(source))) { - return undefined; - } - // For arrays and tuples we infer new arrays and tuples where the reverse mapping has been - // applied to the element type(s). - if (isArrayType(source)) { - return createArrayType(inferReverseMappedType(getTypeArguments(source)[0], target, constraint), isReadonlyArrayType(source)); + // No type specified and nothing can be inferred + return undefined; + } + function getWidenedTypeForAssignmentDeclaration(symbol: ts.Symbol, resolvedSymbol?: ts.Symbol) { + // function/class/{} initializers are themselves containers, so they won't merge in the same way as other initializers + const container = getAssignedExpandoInitializer(symbol.valueDeclaration); + if (container) { + const tag = getJSDocTypeTag(container); + if (tag && tag.typeExpression) { + return getTypeFromTypeNode(tag.typeExpression); + } + const containerObjectType = getJSContainerObjectType(symbol.valueDeclaration, symbol, container); + return containerObjectType || getWidenedLiteralType(checkExpressionCached(container)); + } + let definedInConstructor = false; + let definedInMethod = false; + let jsdocType: ts.Type | undefined; + let types: ts.Type[] | undefined; + for (const declaration of symbol.declarations) { + const expression = (isBinaryExpression(declaration) || isCallExpression(declaration)) ? declaration : + isAccessExpression(declaration) ? isBinaryExpression(declaration.parent) ? declaration.parent : declaration : + undefined; + if (!expression) { + continue; // Non-assignment declaration merged in (eg, an Identifier to mark the thing as a namespace) - skip over it and pull type info from elsewhere } - if (isTupleType(source)) { - const elementTypes = map(getTypeArguments(source), t => inferReverseMappedType(t, target, constraint)); - const minLength = getMappedTypeModifiers(target) & MappedTypeModifiers.IncludeOptional ? - getTypeReferenceArity(source) - (source.target.hasRestElement ? 1 : 0) : source.target.minLength; - return createTupleType(elementTypes, minLength, source.target.hasRestElement, source.target.readonly, source.target.associatedNames); - } - // For all other object types we infer a new object type where the reverse mapping has been - // applied to the type of each property. - const reversed = createObjectType(ObjectFlags.ReverseMapped | ObjectFlags.Anonymous, /*symbol*/ undefined) as ReverseMappedType; - reversed.source = source; - reversed.mappedType = target; - reversed.constraintType = constraint; - return reversed; - } - - function getTypeOfReverseMappedSymbol(symbol: ReverseMappedSymbol) { - return inferReverseMappedType(symbol.propertyType, symbol.mappedType, symbol.constraintType); - } - - function inferReverseMappedType(sourceType: Type, target: MappedType, constraint: IndexType): Type { - const typeParameter = getIndexedAccessType(constraint.type, getTypeParameterFromMappedType(target)); - const templateType = getTemplateTypeFromMappedType(target); - const inference = createInferenceInfo(typeParameter); - inferTypes([inference], sourceType, templateType); - return getTypeFromInference(inference) || unknownType; - } - - function* getUnmatchedProperties(source: Type, target: Type, requireOptionalProperties: boolean, matchDiscriminantProperties: boolean): IterableIterator { - const properties = getPropertiesOfType(target); - for (const targetProp of properties) { - // TODO: remove this when we support static private identifier fields and find other solutions to get privateNamesAndStaticFields test to pass - if (isStaticPrivateIdentifierProperty(targetProp)) { - continue; + const kind = isAccessExpression(expression) + ? getAssignmentDeclarationPropertyAccessKind(expression) + : getAssignmentDeclarationKind(expression); + if (kind === AssignmentDeclarationKind.ThisProperty) { + if (isDeclarationInConstructor(expression)) { + definedInConstructor = true; } - if (requireOptionalProperties || !(targetProp.flags & SymbolFlags.Optional || getCheckFlags(targetProp) & CheckFlags.Partial)) { - const sourceProp = getPropertyOfType(source, targetProp.escapedName); - if (!sourceProp) { - yield targetProp; - } - else if (matchDiscriminantProperties) { - const targetType = getTypeOfSymbol(targetProp); - if (targetType.flags & TypeFlags.Unit) { - const sourceType = getTypeOfSymbol(sourceProp); - if (!(sourceType.flags & TypeFlags.Any || getRegularTypeOfLiteralType(sourceType) === getRegularTypeOfLiteralType(targetType))) { - yield targetProp; - } - } - } + else { + definedInMethod = true; } } + if (!isCallExpression(expression)) { + jsdocType = getAnnotatedTypeForAssignmentDeclaration(jsdocType, expression, symbol, declaration); + } + if (!jsdocType) { + (types || (types = [])).push((isBinaryExpression(expression) || isCallExpression(expression)) ? getInitializerTypeFromAssignmentDeclaration(symbol, resolvedSymbol, expression, kind) : neverType); + } } - - function getUnmatchedProperty(source: Type, target: Type, requireOptionalProperties: boolean, matchDiscriminantProperties: boolean): Symbol | undefined { - const result = getUnmatchedProperties(source, target, requireOptionalProperties, matchDiscriminantProperties).next(); - if (!result.done) return result.value; - } - - function tupleTypesDefinitelyUnrelated(source: TupleTypeReference, target: TupleTypeReference) { - return target.target.minLength > source.target.minLength || - !getRestTypeOfTupleType(target) && (!!getRestTypeOfTupleType(source) || getLengthOfTupleType(target) < getLengthOfTupleType(source)); + let type = jsdocType; + if (!type) { + if (!length(types)) { + return errorType; // No types from any declarations :( + } + let constructorTypes = definedInConstructor ? getConstructorDefinedThisAssignmentTypes(types!, symbol.declarations) : undefined; + // use only the constructor types unless they were only assigned null | undefined (including widening variants) + if (definedInMethod) { + const propType = getTypeOfAssignmentDeclarationPropertyOfBaseType(symbol); + if (propType) { + (constructorTypes || (constructorTypes = [])).push(propType); + definedInConstructor = true; + } + } + const sourceTypes = some(constructorTypes, t => !!(t.flags & ~TypeFlags.Nullable)) ? constructorTypes : types; // TODO: GH#18217 + type = getUnionType((sourceTypes!), UnionReduction.Subtype); } - - function typesDefinitelyUnrelated(source: Type, target: Type) { - // Two tuple types with incompatible arities are definitely unrelated. - // Two object types that each have a property that is unmatched in the other are definitely unrelated. - return isTupleType(source) && isTupleType(target) && tupleTypesDefinitelyUnrelated(source, target) || - !!getUnmatchedProperty(source, target, /*requireOptionalProperties*/ false, /*matchDiscriminantProperties*/ true) && - !!getUnmatchedProperty(target, source, /*requireOptionalProperties*/ false, /*matchDiscriminantProperties*/ true); + const widened = getWidenedType(addOptionality(type, definedInMethod && !definedInConstructor)); + if (filterType(widened, t => !!(t.flags & ~TypeFlags.Nullable)) === neverType) { + reportImplicitAny(symbol.valueDeclaration, anyType); + return anyType; } - - function getTypeFromInference(inference: InferenceInfo) { - return inference.candidates ? getUnionType(inference.candidates, UnionReduction.Subtype) : - inference.contraCandidates ? getIntersectionType(inference.contraCandidates) : - undefined; + return widened; + } + function getJSContainerObjectType(decl: Node, symbol: ts.Symbol, init: Expression | undefined): ts.Type | undefined { + if (!isInJSFile(decl) || !init || !isObjectLiteralExpression(init) || init.properties.length) { + return undefined; } - - function hasSkipDirectInferenceFlag(node: Node) { - return !!getNodeLinks(node).skipDirectInference; + const exports = createSymbolTable(); + while (isBinaryExpression(decl) || isPropertyAccessExpression(decl)) { + const s = getSymbolOfNode(decl); + if (s && hasEntries(s.exports)) { + mergeSymbolTable(exports, s.exports); + } + decl = isBinaryExpression(decl) ? decl.parent : decl.parent.parent; } - - function isFromInferenceBlockedSource(type: Type) { - return !!(type.symbol && some(type.symbol.declarations, hasSkipDirectInferenceFlag)); + const s = getSymbolOfNode(decl); + if (s && hasEntries(s.exports)) { + mergeSymbolTable(exports, s.exports); } - - function inferTypes(inferences: InferenceInfo[], originalSource: Type, originalTarget: Type, priority: InferencePriority = 0, contravariant = false) { - let symbolStack: Symbol[]; - let visited: Map; - let bivariant = false; - let propagationType: Type; - let inferencePriority = InferencePriority.MaxValue; - let allowComplexConstraintInference = true; - inferFromTypes(originalSource, originalTarget); - - function inferFromTypes(source: Type, target: Type): void { - if (!couldContainTypeVariables(target)) { - return; - } - if (source === wildcardType) { - // We are inferring from an 'any' type. We want to infer this type for every type parameter - // referenced in the target type, so we record it as the propagation type and infer from the - // target to itself. Then, as we find candidates we substitute the propagation type. - const savePropagationType = propagationType; - propagationType = source; - inferFromTypes(target, target); - propagationType = savePropagationType; - return; - } - if (source.aliasSymbol && source.aliasTypeArguments && source.aliasSymbol === target.aliasSymbol) { - // Source and target are types originating in the same generic type alias declaration. - // Simply infer from source type arguments to target type arguments. - inferFromTypeArguments(source.aliasTypeArguments, target.aliasTypeArguments!, getAliasVariances(source.aliasSymbol)); - return; - } - if (source === target && source.flags & TypeFlags.UnionOrIntersection) { - // When source and target are the same union or intersection type, just relate each constituent - // type to itself. - for (const t of (source).types) { - inferFromTypes(t, t); - } - return; - } - if (target.flags & TypeFlags.Union) { - // First, infer between identically matching source and target constituents and remove the - // matching types. - const [tempSources, tempTargets] = inferFromMatchingTypes(source.flags & TypeFlags.Union ? (source).types : [source], (target).types, isTypeOrBaseIdenticalTo); - // Next, infer between closely matching source and target constituents and remove - // the matching types. Types closely match when they are instantiations of the same - // object type or instantiations of the same type alias. - const [sources, targets] = inferFromMatchingTypes(tempSources, tempTargets, isTypeCloselyMatchedBy); - if (targets.length === 0) { - return; - } - target = getUnionType(targets); - if (sources.length === 0) { - // All source constituents have been matched and there is nothing further to infer from. - // However, simply making no inferences is undesirable because it could ultimately mean - // inferring a type parameter constraint. Instead, make a lower priority inference from - // the full source to whatever remains in the target. For example, when inferring from - // string to 'string | T', make a lower priority inference of string for T. - inferWithPriority(source, target, InferencePriority.NakedTypeVariable); - return; - } - source = getUnionType(sources); - } - else if (target.flags & TypeFlags.Intersection && some((target).types, - t => !!getInferenceInfoForType(t) || (isGenericMappedType(t) && !!getInferenceInfoForType(getHomomorphicTypeVariable(t) || neverType)))) { - // We reduce intersection types only when they contain naked type parameters. For example, when - // inferring from 'string[] & { extra: any }' to 'string[] & T' we want to remove string[] and - // infer { extra: any } for T. But when inferring to 'string[] & Iterable' we want to keep the - // string[] on the source side and infer string for T. - // Likewise, we consider a homomorphic mapped type constrainted to the target type parameter as similar to a "naked type variable" - // in such scenarios. - if (!(source.flags & TypeFlags.Union)) { - // Infer between identically matching source and target constituents and remove the matching types. - const [sources, targets] = inferFromMatchingTypes(source.flags & TypeFlags.Intersection ? (source).types : [source], (target).types, isTypeIdenticalTo); - if (sources.length === 0 || targets.length === 0) { - return; - } - source = getIntersectionType(sources); - target = getIntersectionType(targets); - } - } - else if (target.flags & (TypeFlags.IndexedAccess | TypeFlags.Substitution)) { - target = getActualTypeVariable(target); - } - if (target.flags & TypeFlags.TypeVariable) { - // If target is a type parameter, make an inference, unless the source type contains - // the anyFunctionType (the wildcard type that's used to avoid contextually typing functions). - // Because the anyFunctionType is internal, it should not be exposed to the user by adding - // it as an inference candidate. Hopefully, a better candidate will come along that does - // not contain anyFunctionType when we come back to this argument for its second round - // of inference. Also, we exclude inferences for silentNeverType (which is used as a wildcard - // when constructing types from type parameters that had no inference candidates). - if (getObjectFlags(source) & ObjectFlags.NonInferrableType || source === nonInferrableAnyType || source === silentNeverType || - (priority & InferencePriority.ReturnType && (source === autoType || source === autoArrayType)) || isFromInferenceBlockedSource(source)) { - return; - } - const inference = getInferenceInfoForType(target); - if (inference) { - if (!inference.isFixed) { - if (inference.priority === undefined || priority < inference.priority) { - inference.candidates = undefined; - inference.contraCandidates = undefined; - inference.topLevel = true; - inference.priority = priority; - } - if (priority === inference.priority) { - const candidate = propagationType || source; - // We make contravariant inferences only if we are in a pure contravariant position, - // i.e. only if we have not descended into a bivariant position. - if (contravariant && !bivariant) { - if (!contains(inference.contraCandidates, candidate)) { - inference.contraCandidates = append(inference.contraCandidates, candidate); - clearCachedInferences(inferences); - } - } - else if (!contains(inference.candidates, candidate)) { - inference.candidates = append(inference.candidates, candidate); - clearCachedInferences(inferences); - } - } - if (!(priority & InferencePriority.ReturnType) && target.flags & TypeFlags.TypeParameter && inference.topLevel && !isTypeParameterAtTopLevel(originalTarget, target)) { - inference.topLevel = false; - clearCachedInferences(inferences); - } - } - inferencePriority = Math.min(inferencePriority, priority); - return; - } - else { - // Infer to the simplified version of an indexed access, if possible, to (hopefully) expose more bare type parameters to the inference engine - const simplified = getSimplifiedType(target, /*writing*/ false); - if (simplified !== target) { - invokeOnce(source, simplified, inferFromTypes); - } - else if (target.flags & TypeFlags.IndexedAccess) { - const indexType = getSimplifiedType((target as IndexedAccessType).indexType, /*writing*/ false); - // Generally simplifications of instantiable indexes are avoided to keep relationship checking correct, however if our target is an access, we can consider - // that key of that access to be "instantiated", since we're looking to find the infernce goal in any way we can. - if (indexType.flags & TypeFlags.Instantiable) { - const simplified = distributeIndexOverObjectType(getSimplifiedType((target as IndexedAccessType).objectType, /*writing*/ false), indexType, /*writing*/ false); - if (simplified && simplified !== target) { - invokeOnce(source, simplified, inferFromTypes); - } - } - } - } - } - if (getObjectFlags(source) & ObjectFlags.Reference && getObjectFlags(target) & ObjectFlags.Reference && ( - (source).target === (target).target || isArrayType(source) && isArrayType(target)) && - !((source).node && (target).node)) { - // If source and target are references to the same generic type, infer from type arguments - inferFromTypeArguments(getTypeArguments(source), getTypeArguments(target), getVariances((source).target)); - } - else if (source.flags & TypeFlags.Index && target.flags & TypeFlags.Index) { - contravariant = !contravariant; - inferFromTypes((source).type, (target).type); - contravariant = !contravariant; - } - else if ((isLiteralType(source) || source.flags & TypeFlags.String) && target.flags & TypeFlags.Index) { - const empty = createEmptyObjectTypeFromStringLiteral(source); - contravariant = !contravariant; - inferWithPriority(empty, (target as IndexType).type, InferencePriority.LiteralKeyof); - contravariant = !contravariant; - } - else if (source.flags & TypeFlags.IndexedAccess && target.flags & TypeFlags.IndexedAccess) { - inferFromTypes((source).objectType, (target).objectType); - inferFromTypes((source).indexType, (target).indexType); - } - else if (source.flags & TypeFlags.Conditional && target.flags & TypeFlags.Conditional) { - inferFromTypes((source).checkType, (target).checkType); - inferFromTypes((source).extendsType, (target).extendsType); - inferFromTypes(getTrueTypeFromConditionalType(source), getTrueTypeFromConditionalType(target)); - inferFromTypes(getFalseTypeFromConditionalType(source), getFalseTypeFromConditionalType(target)); - } - else if (target.flags & TypeFlags.Conditional) { - const savePriority = priority; - priority |= contravariant ? InferencePriority.ContravariantConditional : 0; - const targetTypes = [getTrueTypeFromConditionalType(target), getFalseTypeFromConditionalType(target)]; - inferToMultipleTypes(source, targetTypes, target.flags); - priority = savePriority; - } - else if (target.flags & TypeFlags.UnionOrIntersection) { - inferToMultipleTypes(source, (target).types, target.flags); - } - else if (source.flags & TypeFlags.Union) { - // Source is a union or intersection type, infer from each constituent type - const sourceTypes = (source).types; - for (const sourceType of sourceTypes) { - inferFromTypes(sourceType, target); - } - } - else { - if (!(priority & InferencePriority.NoConstraints && source.flags & (TypeFlags.Intersection | TypeFlags.Instantiable))) { - const apparentSource = getApparentType(source); - // getApparentType can return _any_ type, since an indexed access or conditional may simplify to any other type. - // If that occurs and it doesn't simplify to an object or intersection, we'll need to restart `inferFromTypes` - // with the simplified source. - if (apparentSource !== source && allowComplexConstraintInference && !(apparentSource.flags & (TypeFlags.Object | TypeFlags.Intersection))) { - // TODO: The `allowComplexConstraintInference` flag is a hack! This forbids inference from complex constraints within constraints! - // This isn't required algorithmically, but rather is used to lower the memory burden caused by performing inference - // that is _too good_ in projects with complicated constraints (eg, fp-ts). In such cases, if we did not limit ourselves - // here, we might produce more valid inferences for types, causing us to do more checks and perform more instantiations - // (in addition to the extra stack depth here) which, in turn, can push the already close process over its limit. - // TL;DR: If we ever become generally more memory efficient (or our resource budget ever increases), we should just - // remove this `allowComplexConstraintInference` flag. - allowComplexConstraintInference = false; - return inferFromTypes(apparentSource, target); - } - source = apparentSource; - } - if (source.flags & (TypeFlags.Object | TypeFlags.Intersection)) { - invokeOnce(source, target, inferFromObjectTypes); - } - } - if (source.flags & TypeFlags.Simplifiable) { - const simplified = getSimplifiedType(source, contravariant); - if (simplified !== source) { - inferFromTypes(simplified, target); - } - } + const type = createAnonymousType(symbol, exports, emptyArray, emptyArray, undefined, undefined); + type.objectFlags |= ObjectFlags.JSLiteral; + return type; + } + function getAnnotatedTypeForAssignmentDeclaration(declaredType: ts.Type | undefined, expression: Expression, symbol: ts.Symbol, declaration: Declaration) { + const typeNode = getEffectiveTypeAnnotationNode(expression.parent); + if (typeNode) { + const type = getWidenedType(getTypeFromTypeNode(typeNode)); + if (!declaredType) { + return type; } - - function inferWithPriority(source: Type, target: Type, newPriority: InferencePriority) { - const savePriority = priority; - priority |= newPriority; - inferFromTypes(source, target); - priority = savePriority; + else if (declaredType !== errorType && type !== errorType && !isTypeIdenticalTo(declaredType, type)) { + errorNextVariableOrPropertyDeclarationMustHaveSameType(/*firstDeclaration*/ undefined, declaredType, declaration, type); } - - function invokeOnce(source: Type, target: Type, action: (source: Type, target: Type) => void) { - const key = source.id + "," + target.id; - const status = visited && visited.get(key); - if (status !== undefined) { - inferencePriority = Math.min(inferencePriority, status); - return; - } - (visited || (visited = createMap())).set(key, InferencePriority.Circularity); - const saveInferencePriority = inferencePriority; - inferencePriority = InferencePriority.MaxValue; - action(source, target); - visited.set(key, inferencePriority); - inferencePriority = Math.min(inferencePriority, saveInferencePriority); + } + if (symbol.parent) { + const typeNode = getEffectiveTypeAnnotationNode(symbol.parent.valueDeclaration); + if (typeNode) { + return getTypeOfPropertyOfType(getTypeFromTypeNode(typeNode), symbol.escapedName); } - - function inferFromMatchingTypes(sources: Type[], targets: Type[], matches: (s: Type, t: Type) => boolean): [Type[], Type[]] { - let matchedSources: Type[] | undefined; - let matchedTargets: Type[] | undefined; - for (const t of targets) { - for (const s of sources) { - if (matches(s, t)) { - inferFromTypes(s, t); - matchedSources = appendIfUnique(matchedSources, s); - matchedTargets = appendIfUnique(matchedTargets, t); - } - } - } - return [ - matchedSources ? filter(sources, t => !contains(matchedSources, t)) : sources, - matchedTargets ? filter(targets, t => !contains(matchedTargets, t)) : targets, - ]; + } + return declaredType; + } + /** If we don't have an explicit JSDoc type, get the type from the initializer. */ + function getInitializerTypeFromAssignmentDeclaration(symbol: ts.Symbol, resolvedSymbol: ts.Symbol | undefined, expression: BinaryExpression | CallExpression, kind: AssignmentDeclarationKind) { + if (isCallExpression(expression)) { + if (resolvedSymbol) { + return getTypeOfSymbol(resolvedSymbol); // This shouldn't happen except under some hopefully forbidden merges of export assignments and object define assignments } - - function inferFromTypeArguments(sourceTypes: readonly Type[], targetTypes: readonly Type[], variances: readonly VarianceFlags[]) { - const count = sourceTypes.length < targetTypes.length ? sourceTypes.length : targetTypes.length; - for (let i = 0; i < count; i++) { - if (i < variances.length && (variances[i] & VarianceFlags.VarianceMask) === VarianceFlags.Contravariant) { - inferFromContravariantTypes(sourceTypes[i], targetTypes[i]); - } - else { - inferFromTypes(sourceTypes[i], targetTypes[i]); - } + const objectLitType = checkExpressionCached((expression as BindableObjectDefinePropertyCall).arguments[2]); + const valueType = getTypeOfPropertyOfType(objectLitType, ("value" as __String)); + if (valueType) { + return valueType; + } + const getFunc = getTypeOfPropertyOfType(objectLitType, ("get" as __String)); + if (getFunc) { + const getSig = getSingleCallSignature(getFunc); + if (getSig) { + return getReturnTypeOfSignature(getSig); } } - - function inferFromContravariantTypes(source: Type, target: Type) { - if (strictFunctionTypes || priority & InferencePriority.AlwaysStrict) { - contravariant = !contravariant; - inferFromTypes(source, target); - contravariant = !contravariant; + const setFunc = getTypeOfPropertyOfType(objectLitType, ("set" as __String)); + if (setFunc) { + const setSig = getSingleCallSignature(setFunc); + if (setSig) { + return getTypeOfFirstParameterOfSignature(setSig); + } + } + return anyType; + } + const type = resolvedSymbol ? getTypeOfSymbol(resolvedSymbol) : getWidenedLiteralType(checkExpressionCached(expression.right)); + if (type.flags & TypeFlags.Object && + kind === AssignmentDeclarationKind.ModuleExports && + symbol.escapedName === InternalSymbolName.ExportEquals) { + const exportedType = resolveStructuredTypeMembers((type as ObjectType)); + const members = createSymbolTable(); + copyEntries(exportedType.members, members); + if (resolvedSymbol && !resolvedSymbol.exports) { + resolvedSymbol.exports = createSymbolTable(); + } + (resolvedSymbol || symbol).exports!.forEach((s, name) => { + if (members.has(name)) { + const exportedMember = exportedType.members.get(name)!; + const union = createSymbol(s.flags | exportedMember.flags, name); + union.type = getUnionType([getTypeOfSymbol(s), getTypeOfSymbol(exportedMember)]); + members.set(name, union); } else { - inferFromTypes(source, target); + members.set(name, s); } + }); + const result = createAnonymousType(exportedType.symbol, members, exportedType.callSignatures, exportedType.constructSignatures, exportedType.stringIndexInfo, exportedType.numberIndexInfo); + result.objectFlags |= (getObjectFlags(type) & ObjectFlags.JSLiteral); // Propagate JSLiteral flag + return result; + } + if (isEmptyArrayLiteralType(type)) { + reportImplicitAny(expression, anyArrayType); + return anyArrayType; + } + return type; + } + function isDeclarationInConstructor(expression: Expression) { + const thisContainer = getThisContainer(expression, /*includeArrowFunctions*/ false); + // Properties defined in a constructor (or base constructor, or javascript constructor function) don't get undefined added. + // Function expressions that are assigned to the prototype count as methods. + return thisContainer.kind === SyntaxKind.Constructor || + thisContainer.kind === SyntaxKind.FunctionDeclaration || + (thisContainer.kind === SyntaxKind.FunctionExpression && !isPrototypePropertyAssignment(thisContainer.parent)); + } + function getConstructorDefinedThisAssignmentTypes(types: ts.Type[], declarations: Declaration[]): ts.Type[] | undefined { + Debug.assert(types.length === declarations.length); + return types.filter((_, i) => { + const declaration = declarations[i]; + const expression = isBinaryExpression(declaration) ? declaration : + isBinaryExpression(declaration.parent) ? declaration.parent : undefined; + return expression && isDeclarationInConstructor(expression); + }); + } + /** check for definition in base class if any declaration is in a class */ + function getTypeOfAssignmentDeclarationPropertyOfBaseType(property: ts.Symbol) { + const parentDeclaration = forEach(property.declarations, d => { + const parent = getThisContainer(d, /*includeArrowFunctions*/ false).parent; + return isClassLike(parent) && parent; + }); + if (parentDeclaration) { + const classType = (getDeclaredTypeOfSymbol(getSymbolOfNode(parentDeclaration)) as InterfaceType); + const baseClassType = classType && getBaseTypes(classType)[0]; + if (baseClassType) { + return getTypeOfPropertyOfType(baseClassType, property.escapedName); } - - function getInferenceInfoForType(type: Type) { - if (type.flags & TypeFlags.TypeVariable) { - for (const inference of inferences) { - if (type === inference.typeParameter) { - return inference; - } - } - } - return undefined; + } + } + // Return the type implied by a binding pattern element. This is the type of the initializer of the element if + // one is present. Otherwise, if the element is itself a binding pattern, it is the type implied by the binding + // pattern. Otherwise, it is the type any. + function getTypeFromBindingElement(element: BindingElement, includePatternInType?: boolean, reportErrors?: boolean): ts.Type { + if (element.initializer) { + // The type implied by a binding pattern is independent of context, so we check the initializer with no + // contextual type or, if the element itself is a binding pattern, with the type implied by that binding + // pattern. + const contextualType = isBindingPattern(element.name) ? getTypeFromBindingPattern(element.name, /*includePatternInType*/ true, /*reportErrors*/ false) : unknownType; + return addOptionality(widenTypeInferredFromInitializer(element, checkDeclarationInitializer(element, contextualType))); + } + if (isBindingPattern(element.name)) { + return getTypeFromBindingPattern(element.name, includePatternInType, reportErrors); + } + if (reportErrors && !declarationBelongsToPrivateAmbientMember(element)) { + reportImplicitAny(element, anyType); + } + // When we're including the pattern in the type (an indication we're obtaining a contextual type), we + // use the non-inferrable any type. Inference will never directly infer this type, but it is possible + // to infer a type that contains it, e.g. for a binding pattern like [foo] or { foo }. In such cases, + // widening of the binding pattern type substitutes a regular any for the non-inferrable any. + return includePatternInType ? nonInferrableAnyType : anyType; + } + // Return the type implied by an object binding pattern + function getTypeFromObjectBindingPattern(pattern: ObjectBindingPattern, includePatternInType: boolean, reportErrors: boolean): ts.Type { + const members = createSymbolTable(); + let stringIndexInfo: IndexInfo | undefined; + let objectFlags = ObjectFlags.ObjectLiteral | ObjectFlags.ContainsObjectOrArrayLiteral; + forEach(pattern.elements, e => { + const name = e.propertyName || (e.name); + if (e.dotDotDotToken) { + stringIndexInfo = createIndexInfo(anyType, /*isReadonly*/ false); + return; } - - function getSingleTypeVariableFromIntersectionTypes(types: Type[]) { - let typeVariable: Type | undefined; - for (const type of types) { - const t = type.flags & TypeFlags.Intersection && find((type).types, t => !!getInferenceInfoForType(t)); - if (!t || typeVariable && t !== typeVariable) { - return undefined; - } - typeVariable = t; - } - return typeVariable; + const exprType = getLiteralTypeFromPropertyName(name); + if (!isTypeUsableAsPropertyName(exprType)) { + // do not include computed properties in the implied type + objectFlags |= ObjectFlags.ObjectLiteralPatternWithComputedProperties; + return; } - - function inferToMultipleTypes(source: Type, targets: Type[], targetFlags: TypeFlags) { - let typeVariableCount = 0; - if (targetFlags & TypeFlags.Union) { - let nakedTypeVariable: Type | undefined; - const sources = source.flags & TypeFlags.Union ? (source).types : [source]; - const matched = new Array(sources.length); - let inferenceCircularity = false; - // First infer to types that are not naked type variables. For each source type we - // track whether inferences were made from that particular type to some target with - // equal priority (i.e. of equal quality) to what we would infer for a naked type - // parameter. - for (const t of targets) { - if (getInferenceInfoForType(t)) { - nakedTypeVariable = t; - typeVariableCount++; - } - else { - for (let i = 0; i < sources.length; i++) { - const saveInferencePriority = inferencePriority; - inferencePriority = InferencePriority.MaxValue; - inferFromTypes(sources[i], t); - if (inferencePriority === priority) matched[i] = true; - inferenceCircularity = inferenceCircularity || inferencePriority === InferencePriority.Circularity; - inferencePriority = Math.min(inferencePriority, saveInferencePriority); - } - } - } - if (typeVariableCount === 0) { - // If every target is an intersection of types containing a single naked type variable, - // make a lower priority inference to that type variable. This handles inferring from - // 'A | B' to 'T & (X | Y)' where we want to infer 'A | B' for T. - const intersectionTypeVariable = getSingleTypeVariableFromIntersectionTypes(targets); - if (intersectionTypeVariable) { - inferWithPriority(source, intersectionTypeVariable, InferencePriority.NakedTypeVariable); - } - return; - } - // If the target has a single naked type variable and no inference circularities were - // encountered above (meaning we explored the types fully), create a union of the source - // types from which no inferences have been made so far and infer from that union to the - // naked type variable. - if (typeVariableCount === 1 && !inferenceCircularity) { - const unmatched = flatMap(sources, (s, i) => matched[i] ? undefined : s); - if (unmatched.length) { - inferFromTypes(getUnionType(unmatched), nakedTypeVariable!); - return; - } - } + const text = getPropertyNameFromType(exprType); + const flags = SymbolFlags.Property | (e.initializer ? SymbolFlags.Optional : 0); + const symbol = createSymbol(flags, text); + symbol.type = getTypeFromBindingElement(e, includePatternInType, reportErrors); + symbol.bindingElement = e; + members.set(symbol.escapedName, symbol); + }); + const result = createAnonymousType(undefined, members, emptyArray, emptyArray, stringIndexInfo, undefined); + result.objectFlags |= objectFlags; + if (includePatternInType) { + result.pattern = pattern; + result.objectFlags |= ObjectFlags.ContainsObjectOrArrayLiteral; + } + return result; + } + // Return the type implied by an array binding pattern + function getTypeFromArrayBindingPattern(pattern: BindingPattern, includePatternInType: boolean, reportErrors: boolean): ts.Type { + const elements = pattern.elements; + const lastElement = lastOrUndefined(elements); + const hasRestElement = !!(lastElement && lastElement.kind === SyntaxKind.BindingElement && lastElement.dotDotDotToken); + if (elements.length === 0 || elements.length === 1 && hasRestElement) { + return languageVersion >= ScriptTarget.ES2015 ? createIterableType(anyType) : anyArrayType; + } + const elementTypes = map(elements, e => isOmittedExpression(e) ? anyType : getTypeFromBindingElement(e, includePatternInType, reportErrors)); + const minLength = findLastIndex(elements, e => !isOmittedExpression(e) && !hasDefaultValue(e), elements.length - (hasRestElement ? 2 : 1)) + 1; + let result = (createTupleType(elementTypes, minLength, hasRestElement)); + if (includePatternInType) { + result = cloneTypeReference(result); + result.pattern = pattern; + result.objectFlags |= ObjectFlags.ContainsObjectOrArrayLiteral; + } + return result; + } + // Return the type implied by a binding pattern. This is the type implied purely by the binding pattern itself + // and without regard to its context (i.e. without regard any type annotation or initializer associated with the + // declaration in which the binding pattern is contained). For example, the implied type of [x, y] is [any, any] + // and the implied type of { x, y: z = 1 } is { x: any; y: number; }. The type implied by a binding pattern is + // used as the contextual type of an initializer associated with the binding pattern. Also, for a destructuring + // parameter with no type annotation or initializer, the type implied by the binding pattern becomes the type of + // the parameter. + function getTypeFromBindingPattern(pattern: BindingPattern, includePatternInType = false, reportErrors = false): ts.Type { + return pattern.kind === SyntaxKind.ObjectBindingPattern + ? getTypeFromObjectBindingPattern(pattern, includePatternInType, reportErrors) + : getTypeFromArrayBindingPattern(pattern, includePatternInType, reportErrors); + } + // Return the type associated with a variable, parameter, or property declaration. In the simple case this is the type + // specified in a type annotation or inferred from an initializer. However, in the case of a destructuring declaration it + // is a bit more involved. For example: + // + // var [x, s = ""] = [1, "one"]; + // + // Here, the array literal [1, "one"] is contextually typed by the type [any, string], which is the implied type of the + // binding pattern [x, s = ""]. Because the contextual type is a tuple type, the resulting type of [1, "one"] is the + // tuple type [number, string]. Thus, the type inferred for 'x' is number and the type inferred for 's' is string. + function getWidenedTypeForVariableLikeDeclaration(declaration: ParameterDeclaration | PropertyDeclaration | PropertySignature | VariableDeclaration | BindingElement, reportErrors?: boolean): ts.Type { + return widenTypeForVariableLikeDeclaration(getTypeForVariableLikeDeclaration(declaration, /*includeOptionality*/ true), declaration, reportErrors); + } + function widenTypeForVariableLikeDeclaration(type: ts.Type | undefined, declaration: any, reportErrors?: boolean) { + if (type) { + if (reportErrors) { + reportErrorsFromWidening(declaration, type); + } + // always widen a 'unique symbol' type if the type was created for a different declaration. + if (type.flags & TypeFlags.UniqueESSymbol && (isBindingElement(declaration) || !declaration.type) && type.symbol !== getSymbolOfNode(declaration)) { + type = esSymbolType; + } + return getWidenedType(type); + } + // Rest parameters default to type any[], other parameters default to type any + type = isParameter(declaration) && declaration.dotDotDotToken ? anyArrayType : anyType; + // Report implicit any errors unless this is a private property within an ambient declaration + if (reportErrors) { + if (!declarationBelongsToPrivateAmbientMember(declaration)) { + reportImplicitAny(declaration, type); + } + } + return type; + } + function declarationBelongsToPrivateAmbientMember(declaration: VariableLikeDeclaration) { + const root = getRootDeclaration(declaration); + const memberDeclaration = root.kind === SyntaxKind.Parameter ? root.parent : root; + return isPrivateWithinAmbient(memberDeclaration); + } + function tryGetTypeFromEffectiveTypeNode(declaration: Declaration) { + const typeNode = getEffectiveTypeAnnotationNode(declaration); + if (typeNode) { + return getTypeFromTypeNode(typeNode); + } + } + function getTypeOfVariableOrParameterOrProperty(symbol: ts.Symbol): ts.Type { + const links = getSymbolLinks(symbol); + if (!links.type) { + const type = getTypeOfVariableOrParameterOrPropertyWorker(symbol); + // For a contextually typed parameter it is possible that a type has already + // been assigned (in assignTypeToParameterAndFixTypeParameters), and we want + // to preserve this type. + if (!links.type) { + links.type = type; + } + } + return links.type; + } + function getTypeOfVariableOrParameterOrPropertyWorker(symbol: ts.Symbol) { + // Handle prototype property + if (symbol.flags & SymbolFlags.Prototype) { + return getTypeOfPrototypeProperty(symbol); + } + // CommonsJS require and module both have type any. + if (symbol === requireSymbol) { + return anyType; + } + if (symbol.flags & SymbolFlags.ModuleExports) { + const fileSymbol = getSymbolOfNode(getSourceFileOfNode(symbol.valueDeclaration)); + const members = createSymbolTable(); + members.set(("exports" as __String), fileSymbol); + return createAnonymousType(symbol, members, emptyArray, emptyArray, undefined, undefined); + } + // Handle catch clause variables + const declaration = symbol.valueDeclaration; + if (isCatchClauseVariableDeclarationOrBindingElement(declaration)) { + return anyType; + } + // Handle export default expressions + if (isSourceFile(declaration) && isJsonSourceFile(declaration)) { + if (!declaration.statements.length) { + return emptyObjectType; + } + return getWidenedType(getWidenedLiteralType(checkExpression(declaration.statements[0].expression))); + } + // Handle variable, parameter or property + if (!pushTypeResolution(symbol, TypeSystemPropertyName.Type)) { + // Symbol is property of some kind that is merged with something - should use `getTypeOfFuncClassEnumModule` and not `getTypeOfVariableOrParameterOrProperty` + if (symbol.flags & SymbolFlags.ValueModule && !(symbol.flags & SymbolFlags.Assignment)) { + return getTypeOfFuncClassEnumModule(symbol); + } + return reportCircularityError(symbol); + } + let type: ts.Type | undefined; + if (declaration.kind === SyntaxKind.ExportAssignment) { + type = widenTypeForVariableLikeDeclaration(checkExpressionCached((declaration).expression), declaration); + } + else if (isBinaryExpression(declaration) || + (isInJSFile(declaration) && + (isCallExpression(declaration) || (isPropertyAccessExpression(declaration) || isBindableStaticElementAccessExpression(declaration)) && isBinaryExpression(declaration.parent)))) { + type = getWidenedTypeForAssignmentDeclaration(symbol); + } + else if (isJSDocPropertyLikeTag(declaration) + || isPropertyAccessExpression(declaration) + || isElementAccessExpression(declaration) + || isIdentifier(declaration) + || isStringLiteralLike(declaration) + || isNumericLiteral(declaration) + || isClassDeclaration(declaration) + || isFunctionDeclaration(declaration) + || (isMethodDeclaration(declaration) && !isObjectLiteralMethod(declaration)) + || isMethodSignature(declaration) + || isSourceFile(declaration)) { + // Symbol is property of some kind that is merged with something - should use `getTypeOfFuncClassEnumModule` and not `getTypeOfVariableOrParameterOrProperty` + if (symbol.flags & (SymbolFlags.Function | SymbolFlags.Method | SymbolFlags.Class | SymbolFlags.Enum | SymbolFlags.ValueModule)) { + return getTypeOfFuncClassEnumModule(symbol); + } + type = isBinaryExpression(declaration.parent) ? + getWidenedTypeForAssignmentDeclaration(symbol) : + tryGetTypeFromEffectiveTypeNode(declaration) || anyType; + } + else if (isPropertyAssignment(declaration)) { + type = tryGetTypeFromEffectiveTypeNode(declaration) || checkPropertyAssignment(declaration); + } + else if (isJsxAttribute(declaration)) { + type = tryGetTypeFromEffectiveTypeNode(declaration) || checkJsxAttribute(declaration); + } + else if (isShorthandPropertyAssignment(declaration)) { + type = tryGetTypeFromEffectiveTypeNode(declaration) || checkExpressionForMutableLocation(declaration.name, CheckMode.Normal); + } + else if (isObjectLiteralMethod(declaration)) { + type = tryGetTypeFromEffectiveTypeNode(declaration) || checkObjectLiteralMethod(declaration, CheckMode.Normal); + } + else if (isParameter(declaration) + || isPropertyDeclaration(declaration) + || isPropertySignature(declaration) + || isVariableDeclaration(declaration) + || isBindingElement(declaration)) { + type = getWidenedTypeForVariableLikeDeclaration(declaration, /*includeOptionality*/ true); + } + // getTypeOfSymbol dispatches some JS merges incorrectly because their symbol flags are not mutually exclusive. + // Re-dispatch based on valueDeclaration.kind instead. + else if (isEnumDeclaration(declaration)) { + type = getTypeOfFuncClassEnumModule(symbol); + } + else if (isEnumMember(declaration)) { + type = getTypeOfEnumMember(symbol); + } + else if (isAccessor(declaration)) { + type = resolveTypeOfAccessors(symbol); + } + else { + return Debug.fail("Unhandled declaration kind! " + Debug.formatSyntaxKind(declaration.kind) + " for " + Debug.formatSymbol(symbol)); + } + if (!popTypeResolution()) { + // Symbol is property of some kind that is merged with something - should use `getTypeOfFuncClassEnumModule` and not `getTypeOfVariableOrParameterOrProperty` + if (symbol.flags & SymbolFlags.ValueModule && !(symbol.flags & SymbolFlags.Assignment)) { + return getTypeOfFuncClassEnumModule(symbol); + } + return reportCircularityError(symbol); + } + return type; + } + function getAnnotatedAccessorTypeNode(accessor: AccessorDeclaration | undefined): TypeNode | undefined { + if (accessor) { + if (accessor.kind === SyntaxKind.GetAccessor) { + const getterTypeAnnotation = getEffectiveReturnTypeNode(accessor); + return getterTypeAnnotation; + } + else { + const setterTypeAnnotation = getEffectiveSetAccessorTypeAnnotationNode(accessor); + return setterTypeAnnotation; + } + } + return undefined; + } + function getAnnotatedAccessorType(accessor: AccessorDeclaration | undefined): ts.Type | undefined { + const node = getAnnotatedAccessorTypeNode(accessor); + return node && getTypeFromTypeNode(node); + } + function getAnnotatedAccessorThisParameter(accessor: AccessorDeclaration): ts.Symbol | undefined { + const parameter = getAccessorThisParameter(accessor); + return parameter && parameter.symbol; + } + function getThisTypeOfDeclaration(declaration: SignatureDeclaration): ts.Type | undefined { + return getThisTypeOfSignature(getSignatureFromDeclaration(declaration)); + } + function getTypeOfAccessors(symbol: ts.Symbol): ts.Type { + const links = getSymbolLinks(symbol); + return links.type || (links.type = getTypeOfAccessorsWorker(symbol)); + } + function getTypeOfAccessorsWorker(symbol: ts.Symbol): ts.Type { + if (!pushTypeResolution(symbol, TypeSystemPropertyName.Type)) { + return errorType; + } + let type = resolveTypeOfAccessors(symbol); + if (!popTypeResolution()) { + type = anyType; + if (noImplicitAny) { + const getter = getDeclarationOfKind(symbol, SyntaxKind.GetAccessor); + error(getter, Diagnostics._0_implicitly_has_return_type_any_because_it_does_not_have_a_return_type_annotation_and_is_referenced_directly_or_indirectly_in_one_of_its_return_expressions, symbolToString(symbol)); + } + } + return type; + } + function resolveTypeOfAccessors(symbol: ts.Symbol) { + const getter = getDeclarationOfKind(symbol, SyntaxKind.GetAccessor); + const setter = getDeclarationOfKind(symbol, SyntaxKind.SetAccessor); + if (getter && isInJSFile(getter)) { + const jsDocType = getTypeForDeclarationFromJSDocComment(getter); + if (jsDocType) { + return jsDocType; + } + } + // First try to see if the user specified a return type on the get-accessor. + const getterReturnType = getAnnotatedAccessorType(getter); + if (getterReturnType) { + return getterReturnType; + } + else { + // If the user didn't specify a return type, try to use the set-accessor's parameter type. + const setterParameterType = getAnnotatedAccessorType(setter); + if (setterParameterType) { + return setterParameterType; + } + else { + // If there are no specified types, try to infer it from the body of the get accessor if it exists. + if (getter && getter.body) { + return getReturnTypeFromBody(getter); } + // Otherwise, fall back to 'any'. else { - // We infer from types that are not naked type variables first so that inferences we - // make from nested naked type variables and given slightly higher priority by virtue - // of being first in the candidates array. - for (const t of targets) { - if (getInferenceInfoForType(t)) { - typeVariableCount++; - } - else { - inferFromTypes(source, t); + if (setter) { + if (!isPrivateWithinAmbient(setter)) { + errorOrSuggestion(noImplicitAny, setter, Diagnostics.Property_0_implicitly_has_type_any_because_its_set_accessor_lacks_a_parameter_type_annotation, symbolToString(symbol)); } } - } - // Inferences directly to naked type variables are given lower priority as they are - // less specific. For example, when inferring from Promise to T | Promise, - // we want to infer string for T, not Promise | string. For intersection types - // we only infer to single naked type variables. - if (targetFlags & TypeFlags.Intersection ? typeVariableCount === 1 : typeVariableCount > 0) { - for (const t of targets) { - if (getInferenceInfoForType(t)) { - inferWithPriority(source, t, InferencePriority.NakedTypeVariable); + else { + Debug.assert(!!getter, "there must exist a getter as we are current checking either setter or getter in this function"); + if (!isPrivateWithinAmbient(getter)) { + errorOrSuggestion(noImplicitAny, getter, Diagnostics.Property_0_implicitly_has_type_any_because_its_get_accessor_lacks_a_return_type_annotation, symbolToString(symbol)); } } + return anyType; } } - - function inferToMappedType(source: Type, target: MappedType, constraintType: Type): boolean { - if (constraintType.flags & TypeFlags.Union) { - let result = false; - for (const type of (constraintType as UnionType).types) { - result = inferToMappedType(source, target, type) || result; - } - return result; - } - if (constraintType.flags & TypeFlags.Index) { - // We're inferring from some source type S to a homomorphic mapped type { [P in keyof T]: X }, - // where T is a type variable. Use inferTypeForHomomorphicMappedType to infer a suitable source - // type and then make a secondary inference from that type to T. We make a secondary inference - // such that direct inferences to T get priority over inferences to Partial, for example. - const inference = getInferenceInfoForType((constraintType).type); - if (inference && !inference.isFixed && !isFromInferenceBlockedSource(source)) { - const inferredType = inferTypeForHomomorphicMappedType(source, target, constraintType); - if (inferredType) { - // We assign a lower priority to inferences made from types containing non-inferrable - // types because we may only have a partial result (i.e. we may have failed to make - // reverse inferences for some properties). - inferWithPriority(inferredType, inference.typeParameter, - getObjectFlags(source) & ObjectFlags.NonInferrableType ? - InferencePriority.PartialHomomorphicMappedType : - InferencePriority.HomomorphicMappedType); - } - } - return true; - } - if (constraintType.flags & TypeFlags.TypeParameter) { - // We're inferring from some source type S to a mapped type { [P in K]: X }, where K is a type - // parameter. First infer from 'keyof S' to K. - inferWithPriority(getIndexType(source), constraintType, InferencePriority.MappedTypeConstraint); - // If K is constrained to a type C, also infer to C. Thus, for a mapped type { [P in K]: X }, - // where K extends keyof T, we make the same inferences as for a homomorphic mapped type - // { [P in keyof T]: X }. This enables us to make meaningful inferences when the target is a - // Pick. - const extendedConstraint = getConstraintOfType(constraintType); - if (extendedConstraint && inferToMappedType(source, target, extendedConstraint)) { - return true; - } - // If no inferences can be made to K's constraint, infer from a union of the property types - // in the source to the template type X. - const propTypes = map(getPropertiesOfType(source), getTypeOfSymbol); - const stringIndexType = getIndexTypeOfType(source, IndexKind.String); - const numberIndexInfo = getNonEnumNumberIndexInfo(source); - const numberIndexType = numberIndexInfo && numberIndexInfo.type; - inferFromTypes(getUnionType(append(append(propTypes, stringIndexType), numberIndexType)), getTemplateTypeFromMappedType(target)); - return true; + } + } + function getBaseTypeVariableOfClass(symbol: ts.Symbol) { + const baseConstructorType = getBaseConstructorTypeOfClass(getDeclaredTypeOfClassOrInterface(symbol)); + return baseConstructorType.flags & TypeFlags.TypeVariable ? baseConstructorType : + baseConstructorType.flags & TypeFlags.Intersection ? find((baseConstructorType as IntersectionType).types, t => !!(t.flags & TypeFlags.TypeVariable)) : + undefined; + } + function getTypeOfFuncClassEnumModule(symbol: ts.Symbol): ts.Type { + let links = getSymbolLinks(symbol); + const originalLinks = links; + if (!links.type) { + const jsDeclaration = getDeclarationOfExpando(symbol.valueDeclaration); + if (jsDeclaration) { + const merged = mergeJSSymbols(symbol, getSymbolOfNode(jsDeclaration)); + if (merged) { + // note:we overwrite links because we just cloned the symbol + symbol = links = merged; } - return false; } - - function inferFromObjectTypes(source: Type, target: Type) { - // If we are already processing another target type with the same associated symbol (such as - // an instantiation of the same generic type), we do not explore this target as it would yield - // no further inferences. We exclude the static side of classes from this check since it shares - // its symbol with the instance side which would lead to false positives. - const isNonConstructorObject = target.flags & TypeFlags.Object && - !(getObjectFlags(target) & ObjectFlags.Anonymous && target.symbol && target.symbol.flags & SymbolFlags.Class); - const symbol = isNonConstructorObject ? target.symbol : undefined; - if (symbol) { - if (contains(symbolStack, symbol)) { - inferencePriority = InferencePriority.Circularity; - return; - } - (symbolStack || (symbolStack = [])).push(symbol); - inferFromObjectTypesWorker(source, target); - symbolStack.pop(); + originalLinks.type = links.type = getTypeOfFuncClassEnumModuleWorker(symbol); + } + return links.type; + } + function getTypeOfFuncClassEnumModuleWorker(symbol: ts.Symbol): ts.Type { + const declaration = symbol.valueDeclaration; + if (symbol.flags & SymbolFlags.Module && isShorthandAmbientModuleSymbol(symbol)) { + return anyType; + } + else if (declaration.kind === SyntaxKind.BinaryExpression || + isAccessExpression(declaration) && + declaration.parent.kind === SyntaxKind.BinaryExpression) { + return getWidenedTypeForAssignmentDeclaration(symbol); + } + else if (symbol.flags & SymbolFlags.ValueModule && declaration && isSourceFile(declaration) && declaration.commonJsModuleIndicator) { + const resolvedModule = resolveExternalModuleSymbol(symbol); + if (resolvedModule !== symbol) { + if (!pushTypeResolution(symbol, TypeSystemPropertyName.Type)) { + return errorType; } - else { - inferFromObjectTypesWorker(source, target); + const exportEquals = getMergedSymbol((symbol.exports!.get(InternalSymbolName.ExportEquals)!)); + const type = getWidenedTypeForAssignmentDeclaration(exportEquals, exportEquals === resolvedModule ? undefined : resolvedModule); + if (!popTypeResolution()) { + return reportCircularityError(symbol); } + return type; } - - function inferFromObjectTypesWorker(source: Type, target: Type) { - if (getObjectFlags(source) & ObjectFlags.Reference && getObjectFlags(target) & ObjectFlags.Reference && ( - (source).target === (target).target || isArrayType(source) && isArrayType(target))) { - // If source and target are references to the same generic type, infer from type arguments - inferFromTypeArguments(getTypeArguments(source), getTypeArguments(target), getVariances((source).target)); - return; - } - if (isGenericMappedType(source) && isGenericMappedType(target)) { - // The source and target types are generic types { [P in S]: X } and { [P in T]: Y }, so we infer - // from S to T and from X to Y. - inferFromTypes(getConstraintTypeFromMappedType(source), getConstraintTypeFromMappedType(target)); - inferFromTypes(getTemplateTypeFromMappedType(source), getTemplateTypeFromMappedType(target)); - } - if (getObjectFlags(target) & ObjectFlags.Mapped) { - const constraintType = getConstraintTypeFromMappedType(target); - if (inferToMappedType(source, target, constraintType)) { - return; - } - } - // Infer from the members of source and target only if the two types are possibly related - if (!typesDefinitelyUnrelated(source, target)) { - inferFromProperties(source, target); - inferFromSignatures(source, target, SignatureKind.Call); - inferFromSignatures(source, target, SignatureKind.Construct); - inferFromIndexTypes(source, target); - } + } + const type = createObjectType(ObjectFlags.Anonymous, symbol); + if (symbol.flags & SymbolFlags.Class) { + const baseTypeVariable = getBaseTypeVariableOfClass(symbol); + return baseTypeVariable ? getIntersectionType([type, baseTypeVariable]) : type; + } + else { + return strictNullChecks && symbol.flags & SymbolFlags.Optional ? getOptionalType(type) : type; + } + } + function getTypeOfEnumMember(symbol: ts.Symbol): ts.Type { + const links = getSymbolLinks(symbol); + return links.type || (links.type = getDeclaredTypeOfEnumMember(symbol)); + } + function getTypeOfAlias(symbol: ts.Symbol): ts.Type { + const links = getSymbolLinks(symbol); + if (!links.type) { + const targetSymbol = resolveAlias(symbol); + // It only makes sense to get the type of a value symbol. If the result of resolving + // the alias is not a value, then it has no type. To get the type associated with a + // type symbol, call getDeclaredTypeOfSymbol. + // This check is important because without it, a call to getTypeOfSymbol could end + // up recursively calling getTypeOfAlias, causing a stack overflow. + links.type = targetSymbol.flags & SymbolFlags.Value + ? getTypeOfSymbol(targetSymbol) + : errorType; + } + return links.type; + } + function getTypeOfInstantiatedSymbol(symbol: ts.Symbol): ts.Type { + const links = getSymbolLinks(symbol); + if (!links.type) { + if (!pushTypeResolution(symbol, TypeSystemPropertyName.Type)) { + return links.type = errorType; } - - function inferFromProperties(source: Type, target: Type) { - if (isArrayType(source) || isTupleType(source)) { - if (isTupleType(target)) { - const sourceLength = isTupleType(source) ? getLengthOfTupleType(source) : 0; - const targetLength = getLengthOfTupleType(target); - const sourceRestType = isTupleType(source) ? getRestTypeOfTupleType(source) : getElementTypeOfArrayType(source); - const targetRestType = getRestTypeOfTupleType(target); - const fixedLength = targetLength < sourceLength || sourceRestType ? targetLength : sourceLength; - for (let i = 0; i < fixedLength; i++) { - inferFromTypes(i < sourceLength ? getTypeArguments(source)[i] : sourceRestType!, getTypeArguments(target)[i]); - } - if (targetRestType) { - const types = fixedLength < sourceLength ? getTypeArguments(source).slice(fixedLength, sourceLength) : []; - if (sourceRestType) { - types.push(sourceRestType); - } - if (types.length) { - inferFromTypes(getUnionType(types), targetRestType); - } - } - return; - } - if (isArrayType(target)) { - inferFromIndexTypes(source, target); - return; - } - } - const properties = getPropertiesOfObjectType(target); - for (const targetProp of properties) { - const sourceProp = getPropertyOfType(source, targetProp.escapedName); - if (sourceProp) { - inferFromTypes(getTypeOfSymbol(sourceProp), getTypeOfSymbol(targetProp)); - } - } + let type = instantiateType(getTypeOfSymbol(links.target!), links.mapper); + if (!popTypeResolution()) { + type = reportCircularityError(symbol); } - - function inferFromSignatures(source: Type, target: Type, kind: SignatureKind) { - const sourceSignatures = getSignaturesOfType(source, kind); - const targetSignatures = getSignaturesOfType(target, kind); - const sourceLen = sourceSignatures.length; - const targetLen = targetSignatures.length; - const len = sourceLen < targetLen ? sourceLen : targetLen; - const skipParameters = !!(getObjectFlags(source) & ObjectFlags.NonInferrableType); - for (let i = 0; i < len; i++) { - inferFromSignature(getBaseSignature(sourceSignatures[sourceLen - len + i]), getBaseSignature(targetSignatures[targetLen - len + i]), skipParameters); - } + links.type = type; + } + return links.type; + } + function reportCircularityError(symbol: ts.Symbol) { + const declaration = (symbol.valueDeclaration); + // Check if variable has type annotation that circularly references the variable itself + if (getEffectiveTypeAnnotationNode(declaration)) { + error(symbol.valueDeclaration, Diagnostics._0_is_referenced_directly_or_indirectly_in_its_own_type_annotation, symbolToString(symbol)); + return errorType; + } + // Check if variable has initializer that circularly references the variable itself + if (noImplicitAny && (declaration.kind !== SyntaxKind.Parameter || (declaration).initializer)) { + error(symbol.valueDeclaration, Diagnostics._0_implicitly_has_type_any_because_it_does_not_have_a_type_annotation_and_is_referenced_directly_or_indirectly_in_its_own_initializer, symbolToString(symbol)); + } + // Circularities could also result from parameters in function expressions that end up + // having themselves as contextual types following type argument inference. In those cases + // we have already reported an implicit any error so we don't report anything here. + return anyType; + } + function getTypeOfSymbolWithDeferredType(symbol: ts.Symbol) { + const links = getSymbolLinks(symbol); + if (!links.type) { + Debug.assertIsDefined(links.deferralParent); + Debug.assertIsDefined(links.deferralConstituents); + links.type = links.deferralParent.flags & TypeFlags.Union ? getUnionType(links.deferralConstituents) : getIntersectionType(links.deferralConstituents); + } + return links.type; + } + function getTypeOfSymbol(symbol: ts.Symbol): ts.Type { + if (getCheckFlags(symbol) & CheckFlags.DeferredType) { + return getTypeOfSymbolWithDeferredType(symbol); + } + if (getCheckFlags(symbol) & CheckFlags.Instantiated) { + return getTypeOfInstantiatedSymbol(symbol); + } + if (getCheckFlags(symbol) & CheckFlags.ReverseMapped) { + return getTypeOfReverseMappedSymbol((symbol as ReverseMappedSymbol)); + } + if (symbol.flags & (SymbolFlags.Variable | SymbolFlags.Property)) { + return getTypeOfVariableOrParameterOrProperty(symbol); + } + if (symbol.flags & (SymbolFlags.Function | SymbolFlags.Method | SymbolFlags.Class | SymbolFlags.Enum | SymbolFlags.ValueModule)) { + return getTypeOfFuncClassEnumModule(symbol); + } + if (symbol.flags & SymbolFlags.EnumMember) { + return getTypeOfEnumMember(symbol); + } + if (symbol.flags & SymbolFlags.Accessor) { + return getTypeOfAccessors(symbol); + } + if (symbol.flags & SymbolFlags.Alias) { + return getTypeOfAlias(symbol); + } + return errorType; + } + function isReferenceToType(type: ts.Type, target: ts.Type) { + return type !== undefined + && target !== undefined + && (getObjectFlags(type) & ObjectFlags.Reference) !== 0 + && (type).target === target; + } + function getTargetType(type: ts.Type): ts.Type { + return getObjectFlags(type) & ObjectFlags.Reference ? (type).target : type; + } + // TODO: GH#18217 If `checkBase` is undefined, we should not call this because this will always return false. + function hasBaseType(type: ts.Type, checkBase: ts.Type | undefined) { + return check(type); + function check(type: ts.Type): boolean { + if (getObjectFlags(type) & (ObjectFlags.ClassOrInterface | ObjectFlags.Reference)) { + const target = (getTargetType(type)); + return target === checkBase || some(getBaseTypes(target), check); } - - function inferFromSignature(source: Signature, target: Signature, skipParameters: boolean) { - if (!skipParameters) { - const saveBivariant = bivariant; - const kind = target.declaration ? target.declaration.kind : SyntaxKind.Unknown; - // Once we descend into a bivariant signature we remain bivariant for all nested inferences - bivariant = bivariant || kind === SyntaxKind.MethodDeclaration || kind === SyntaxKind.MethodSignature || kind === SyntaxKind.Constructor; - applyToParameterTypes(source, target, inferFromContravariantTypes); - bivariant = saveBivariant; - } - applyToReturnTypes(source, target, inferFromTypes); + else if (type.flags & TypeFlags.Intersection) { + return some((type).types, check); } - - function inferFromIndexTypes(source: Type, target: Type) { - const targetStringIndexType = getIndexTypeOfType(target, IndexKind.String); - if (targetStringIndexType) { - const sourceIndexType = getIndexTypeOfType(source, IndexKind.String) || - getImplicitIndexTypeOfType(source, IndexKind.String); - if (sourceIndexType) { - inferFromTypes(sourceIndexType, targetStringIndexType); + return false; + } + } + // Appends the type parameters given by a list of declarations to a set of type parameters and returns the resulting set. + // The function allocates a new array if the input type parameter set is undefined, but otherwise it modifies the set + // in-place and returns the same array. + function appendTypeParameters(typeParameters: TypeParameter[] | undefined, declarations: readonly TypeParameterDeclaration[]): TypeParameter[] | undefined { + for (const declaration of declarations) { + typeParameters = appendIfUnique(typeParameters, getDeclaredTypeOfTypeParameter(getSymbolOfNode(declaration))); + } + return typeParameters; + } + // Return the outer type parameters of a node or undefined if the node has no outer type parameters. + function getOuterTypeParameters(node: Node, includeThisTypes?: boolean): TypeParameter[] | undefined { + while (true) { + node = node.parent; // TODO: GH#18217 Use SourceFile kind check instead + if (node && isBinaryExpression(node)) { + // prototype assignments get the outer type parameters of their constructor function + const assignmentKind = getAssignmentDeclarationKind(node); + if (assignmentKind === AssignmentDeclarationKind.Prototype || assignmentKind === AssignmentDeclarationKind.PrototypeProperty) { + const symbol = getSymbolOfNode(node.left); + if (symbol && symbol.parent && !findAncestor(symbol.parent.valueDeclaration, d => node === d)) { + node = symbol.parent.valueDeclaration; } } - const targetNumberIndexType = getIndexTypeOfType(target, IndexKind.Number); - if (targetNumberIndexType) { - const sourceIndexType = getIndexTypeOfType(source, IndexKind.Number) || - getIndexTypeOfType(source, IndexKind.String) || - getImplicitIndexTypeOfType(source, IndexKind.Number); - if (sourceIndexType) { - inferFromTypes(sourceIndexType, targetNumberIndexType); + } + if (!node) { + return undefined; + } + switch (node.kind) { + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ClassExpression: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.CallSignature: + case SyntaxKind.ConstructSignature: + case SyntaxKind.MethodSignature: + case SyntaxKind.FunctionType: + case SyntaxKind.ConstructorType: + case SyntaxKind.JSDocFunctionType: + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.FunctionExpression: + case SyntaxKind.ArrowFunction: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.JSDocTemplateTag: + case SyntaxKind.JSDocTypedefTag: + case SyntaxKind.JSDocEnumTag: + case SyntaxKind.JSDocCallbackTag: + case SyntaxKind.MappedType: + case SyntaxKind.ConditionalType: + const outerTypeParameters = getOuterTypeParameters(node, includeThisTypes); + if (node.kind === SyntaxKind.MappedType) { + return append(outerTypeParameters, getDeclaredTypeOfTypeParameter(getSymbolOfNode((node).typeParameter))); } - } + else if (node.kind === SyntaxKind.ConditionalType) { + return concatenate(outerTypeParameters, getInferTypeParameters((node))); + } + const outerAndOwnTypeParameters = appendTypeParameters(outerTypeParameters, getEffectiveTypeParameterDeclarations((node))); + const thisType = includeThisTypes && + (node.kind === SyntaxKind.ClassDeclaration || node.kind === SyntaxKind.ClassExpression || node.kind === SyntaxKind.InterfaceDeclaration || isJSConstructor(node)) && + getDeclaredTypeOfClassOrInterface(getSymbolOfNode((node as ClassLikeDeclaration | InterfaceDeclaration))).thisType; + return thisType ? append(outerAndOwnTypeParameters, thisType) : outerAndOwnTypeParameters; } } - - function isTypeOrBaseIdenticalTo(s: Type, t: Type) { - return isTypeIdenticalTo(s, t) || !!(t.flags & TypeFlags.String && s.flags & TypeFlags.StringLiteral || t.flags & TypeFlags.Number && s.flags & TypeFlags.NumberLiteral); - } - - function isTypeCloselyMatchedBy(s: Type, t: Type) { - return !!(s.flags & TypeFlags.Object && t.flags & TypeFlags.Object && s.symbol && s.symbol === t.symbol || - s.aliasSymbol && s.aliasTypeArguments && s.aliasSymbol === t.aliasSymbol); - } - - function hasPrimitiveConstraint(type: TypeParameter): boolean { - const constraint = getConstraintOfTypeParameter(type); - return !!constraint && maybeTypeOfKind(constraint.flags & TypeFlags.Conditional ? getDefaultConstraintOfConditionalType(constraint as ConditionalType) : constraint, TypeFlags.Primitive | TypeFlags.Index); + } + // The outer type parameters are those defined by enclosing generic classes, methods, or functions. + function getOuterTypeParametersOfClassOrInterface(symbol: ts.Symbol): TypeParameter[] | undefined { + const declaration = symbol.flags & SymbolFlags.Class ? symbol.valueDeclaration : getDeclarationOfKind(symbol, SyntaxKind.InterfaceDeclaration)!; + Debug.assert(!!declaration, "Class was missing valueDeclaration -OR- non-class had no interface declarations"); + return getOuterTypeParameters(declaration); + } + // The local type parameters are the combined set of type parameters from all declarations of the class, + // interface, or type alias. + function getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol: ts.Symbol): TypeParameter[] | undefined { + let result: TypeParameter[] | undefined; + for (const node of symbol.declarations) { + if (node.kind === SyntaxKind.InterfaceDeclaration || + node.kind === SyntaxKind.ClassDeclaration || + node.kind === SyntaxKind.ClassExpression || + isJSConstructor(node) || + isTypeAlias(node)) { + const declaration = (node); + result = appendTypeParameters(result, getEffectiveTypeParameterDeclarations(declaration)); + } } - - function isObjectLiteralType(type: Type) { - return !!(getObjectFlags(type) & ObjectFlags.ObjectLiteral); + return result; + } + // The full set of type parameters for a generic class or interface type consists of its outer type parameters plus + // its locally declared type parameters. + function getTypeParametersOfClassOrInterface(symbol: ts.Symbol): TypeParameter[] | undefined { + return concatenate(getOuterTypeParametersOfClassOrInterface(symbol), getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol)); + } + // A type is a mixin constructor if it has a single construct signature taking no type parameters and a single + // rest parameter of type any[]. + function isMixinConstructorType(type: ts.Type) { + const signatures = getSignaturesOfType(type, SignatureKind.Construct); + if (signatures.length === 1) { + const s = signatures[0]; + return !s.typeParameters && s.parameters.length === 1 && signatureHasRestParameter(s) && getElementTypeOfArrayType(getTypeOfParameter(s.parameters[0])) === anyType; + } + return false; + } + function isConstructorType(type: ts.Type): boolean { + if (getSignaturesOfType(type, SignatureKind.Construct).length > 0) { + return true; } - - function isObjectOrArrayLiteralType(type: Type) { - return !!(getObjectFlags(type) & (ObjectFlags.ObjectLiteral | ObjectFlags.ArrayLiteral)); + if (type.flags & TypeFlags.TypeVariable) { + const constraint = getBaseConstraintOfType(type); + return !!constraint && isMixinConstructorType(constraint); } - - function unionObjectAndArrayLiteralCandidates(candidates: Type[]): Type[] { - if (candidates.length > 1) { - const objectLiterals = filter(candidates, isObjectOrArrayLiteralType); - if (objectLiterals.length) { - const literalsType = getUnionType(objectLiterals, UnionReduction.Subtype); - return concatenate(filter(candidates, t => !isObjectOrArrayLiteralType(t)), [literalsType]); + return false; + } + function getBaseTypeNodeOfClass(type: InterfaceType): ExpressionWithTypeArguments | undefined { + return getEffectiveBaseTypeNode((type.symbol.valueDeclaration as ClassLikeDeclaration)); + } + function getConstructorsForTypeArguments(type: ts.Type, typeArgumentNodes: readonly TypeNode[] | undefined, location: Node): readonly ts.Signature[] { + const typeArgCount = length(typeArgumentNodes); + const isJavascript = isInJSFile(location); + return filter(getSignaturesOfType(type, SignatureKind.Construct), sig => (isJavascript || typeArgCount >= getMinTypeArgumentCount(sig.typeParameters)) && typeArgCount <= length(sig.typeParameters)); + } + function getInstantiatedConstructorsForTypeArguments(type: ts.Type, typeArgumentNodes: readonly TypeNode[] | undefined, location: Node): readonly ts.Signature[] { + const signatures = getConstructorsForTypeArguments(type, typeArgumentNodes, location); + const typeArguments = map(typeArgumentNodes, getTypeFromTypeNode); + return sameMap(signatures, sig => some(sig.typeParameters) ? getSignatureInstantiation(sig, typeArguments, isInJSFile(location)) : sig); + } + /** + * The base constructor of a class can resolve to + * * undefinedType if the class has no extends clause, + * * unknownType if an error occurred during resolution of the extends expression, + * * nullType if the extends expression is the null value, + * * anyType if the extends expression has type any, or + * * an object type with at least one construct signature. + */ + function getBaseConstructorTypeOfClass(type: InterfaceType): ts.Type { + if (!type.resolvedBaseConstructorType) { + const decl = (type.symbol.valueDeclaration); + const extended = getEffectiveBaseTypeNode(decl); + const baseTypeNode = getBaseTypeNodeOfClass(type); + if (!baseTypeNode) { + return type.resolvedBaseConstructorType = undefinedType; + } + if (!pushTypeResolution(type, TypeSystemPropertyName.ResolvedBaseConstructorType)) { + return errorType; + } + const baseConstructorType = checkExpression(baseTypeNode.expression); + if (extended && baseTypeNode !== extended) { + Debug.assert(!extended.typeArguments); // Because this is in a JS file, and baseTypeNode is in an @extends tag + checkExpression(extended.expression); + } + if (baseConstructorType.flags & (TypeFlags.Object | TypeFlags.Intersection)) { + // Resolving the members of a class requires us to resolve the base class of that class. + // We force resolution here such that we catch circularities now. + resolveStructuredTypeMembers((baseConstructorType)); + } + if (!popTypeResolution()) { + error(type.symbol.valueDeclaration, Diagnostics._0_is_referenced_directly_or_indirectly_in_its_own_base_expression, symbolToString(type.symbol)); + return type.resolvedBaseConstructorType = errorType; + } + if (!(baseConstructorType.flags & TypeFlags.Any) && baseConstructorType !== nullWideningType && !isConstructorType(baseConstructorType)) { + const err = error(baseTypeNode.expression, Diagnostics.Type_0_is_not_a_constructor_function_type, typeToString(baseConstructorType)); + if (baseConstructorType.flags & TypeFlags.TypeParameter) { + const constraint = getConstraintFromTypeParameter(baseConstructorType); + let ctorReturn: ts.Type = unknownType; + if (constraint) { + const ctorSig = getSignaturesOfType(constraint, SignatureKind.Construct); + if (ctorSig[0]) { + ctorReturn = getReturnTypeOfSignature(ctorSig[0]); + } + } + addRelatedInfo(err, createDiagnosticForNode(baseConstructorType.symbol.declarations[0], Diagnostics.Did_you_mean_for_0_to_be_constrained_to_type_new_args_Colon_any_1, symbolToString(baseConstructorType.symbol), typeToString(ctorReturn))); } + return type.resolvedBaseConstructorType = errorType; } - return candidates; + type.resolvedBaseConstructorType = baseConstructorType; } - - function getContravariantInference(inference: InferenceInfo) { - return inference.priority! & InferencePriority.PriorityImpliesCombination ? getIntersectionType(inference.contraCandidates!) : getCommonSubtype(inference.contraCandidates!); - } - - function getCovariantInference(inference: InferenceInfo, signature: Signature) { - // Extract all object and array literal types and replace them with a single widened and normalized type. - const candidates = unionObjectAndArrayLiteralCandidates(inference.candidates!); - // We widen inferred literal types if - // all inferences were made to top-level occurrences of the type parameter, and - // the type parameter has no constraint or its constraint includes no primitive or literal types, and - // the type parameter was fixed during inference or does not occur at top-level in the return type. - const primitiveConstraint = hasPrimitiveConstraint(inference.typeParameter); - const widenLiteralTypes = !primitiveConstraint && inference.topLevel && - (inference.isFixed || !isTypeParameterAtTopLevel(getReturnTypeOfSignature(signature), inference.typeParameter)); - const baseCandidates = primitiveConstraint ? sameMap(candidates, getRegularTypeOfLiteralType) : - widenLiteralTypes ? sameMap(candidates, getWidenedLiteralType) : - candidates; - // If all inferences were made from a position that implies a combined result, infer a union type. - // Otherwise, infer a common supertype. - const unwidenedType = inference.priority! & InferencePriority.PriorityImpliesCombination ? - getUnionType(baseCandidates, UnionReduction.Subtype) : - getCommonSupertype(baseCandidates); - return getWidenedType(unwidenedType); - } - - function getInferredType(context: InferenceContext, index: number): Type { - const inference = context.inferences[index]; - if (!inference.inferredType) { - let inferredType: Type | undefined; - const signature = context.signature; - if (signature) { - const inferredCovariantType = inference.candidates ? getCovariantInference(inference, signature) : undefined; - if (inference.contraCandidates) { - const inferredContravariantType = getContravariantInference(inference); - // If we have both co- and contra-variant inferences, we prefer the contra-variant inference - // unless the co-variant inference is a subtype and not 'never'. - inferredType = inferredCovariantType && !(inferredCovariantType.flags & TypeFlags.Never) && - isTypeSubtypeOf(inferredCovariantType, inferredContravariantType) ? - inferredCovariantType : inferredContravariantType; - } - else if (inferredCovariantType) { - inferredType = inferredCovariantType; - } - else if (context.flags & InferenceFlags.NoDefault) { - // We use silentNeverType as the wildcard that signals no inferences. - inferredType = silentNeverType; + return type.resolvedBaseConstructorType; + } + function getImplementsTypes(type: InterfaceType): BaseType[] { + let resolvedImplementsTypes: BaseType[] = emptyArray; + for (const declaration of type.symbol.declarations) { + const implementsTypeNodes = getEffectiveImplementsTypeNodes((declaration as ClassLikeDeclaration)); + if (!implementsTypeNodes) + continue; + for (const node of implementsTypeNodes) { + const implementsType = getTypeFromTypeNode(node); + if (implementsType !== errorType) { + if (resolvedImplementsTypes === emptyArray) { + resolvedImplementsTypes = [(implementsType)]; } else { - // Infer either the default or the empty object type when no inferences were - // made. It is important to remember that in this case, inference still - // succeeds, meaning there is no error for not having inference candidates. An - // inference error only occurs when there are *conflicting* candidates, i.e. - // candidates with no common supertype. - const defaultType = getDefaultFromTypeParameter(inference.typeParameter); - if (defaultType) { - // Instantiate the default type. Any forward reference to a type - // parameter should be instantiated to the empty object type. - inferredType = instantiateType(defaultType, combineTypeMappers(createBackreferenceMapper(context, index), context.nonFixingMapper)); - } + resolvedImplementsTypes.push(implementsType); } } - else { - inferredType = getTypeFromInference(inference); + } + } + return resolvedImplementsTypes; + } + function getBaseTypes(type: InterfaceType): BaseType[] { + if (!type.resolvedBaseTypes) { + if (type.objectFlags & ObjectFlags.Tuple) { + type.resolvedBaseTypes = [createArrayType(getUnionType(type.typeParameters || emptyArray), (type).readonly)]; + } + else if (type.symbol.flags & (SymbolFlags.Class | SymbolFlags.Interface)) { + if (type.symbol.flags & SymbolFlags.Class) { + resolveBaseTypesOfClass(type); } - - inference.inferredType = inferredType || getDefaultTypeArgumentType(!!(context.flags & InferenceFlags.AnyDefault)); - - const constraint = getConstraintOfTypeParameter(inference.typeParameter); - if (constraint) { - const instantiatedConstraint = instantiateType(constraint, context.nonFixingMapper); - if (!inferredType || !context.compareTypes(inferredType, getTypeWithThisArgument(instantiatedConstraint, inferredType))) { - inference.inferredType = inferredType = instantiatedConstraint; - } + if (type.symbol.flags & SymbolFlags.Interface) { + resolveBaseTypesOfInterface(type); } } - - return inference.inferredType; - } - - function getDefaultTypeArgumentType(isInJavaScriptFile: boolean): Type { - return isInJavaScriptFile ? anyType : unknownType; - } - - function getInferredTypes(context: InferenceContext): Type[] { - const result: Type[] = []; - for (let i = 0; i < context.inferences.length; i++) { - result.push(getInferredType(context, i)); + else { + Debug.fail("type must be class or interface"); } - return result; } - - // EXPRESSION TYPE CHECKING - - function getCannotFindNameDiagnosticForName(node: Identifier): DiagnosticMessage { - switch (node.escapedText) { - case "document": - case "console": - return Diagnostics.Cannot_find_name_0_Do_you_need_to_change_your_target_library_Try_changing_the_lib_compiler_option_to_include_dom; - case "$": - return compilerOptions.types - ? Diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_jQuery_Try_npm_i_types_Slashjquery_and_then_add_jquery_to_the_types_field_in_your_tsconfig - : Diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_jQuery_Try_npm_i_types_Slashjquery; - case "describe": - case "suite": - case "it": - case "test": - return compilerOptions.types - ? Diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_a_test_runner_Try_npm_i_types_Slashjest_or_npm_i_types_Slashmocha_and_then_add_jest_or_mocha_to_the_types_field_in_your_tsconfig - : Diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_a_test_runner_Try_npm_i_types_Slashjest_or_npm_i_types_Slashmocha; - case "process": - case "require": - case "Buffer": - case "module": - return compilerOptions.types - ? Diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_node_Try_npm_i_types_Slashnode_and_then_add_node_to_the_types_field_in_your_tsconfig - : Diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_node_Try_npm_i_types_Slashnode; - case "Map": - case "Set": - case "Promise": - case "Symbol": - case "WeakMap": - case "WeakSet": - case "Iterator": - case "AsyncIterator": - return Diagnostics.Cannot_find_name_0_Do_you_need_to_change_your_target_library_Try_changing_the_lib_compiler_option_to_es2015_or_later; - default: - if (node.parent.kind === SyntaxKind.ShorthandPropertyAssignment) { - return Diagnostics.No_value_exists_in_scope_for_the_shorthand_property_0_Either_declare_one_or_provide_an_initializer; - } - else { - return Diagnostics.Cannot_find_name_0; - } + return type.resolvedBaseTypes; + } + function resolveBaseTypesOfClass(type: InterfaceType) { + type.resolvedBaseTypes = resolvingEmptyArray; + const baseConstructorType = getApparentType(getBaseConstructorTypeOfClass(type)); + if (!(baseConstructorType.flags & (TypeFlags.Object | TypeFlags.Intersection | TypeFlags.Any))) { + return type.resolvedBaseTypes = emptyArray; + } + const baseTypeNode = getBaseTypeNodeOfClass(type)!; + let baseType: ts.Type; + const originalBaseType = baseConstructorType.symbol ? getDeclaredTypeOfSymbol(baseConstructorType.symbol) : undefined; + if (baseConstructorType.symbol && baseConstructorType.symbol.flags & SymbolFlags.Class && + areAllOuterTypeParametersApplied(originalBaseType!)) { + // When base constructor type is a class with no captured type arguments we know that the constructors all have the same type parameters as the + // class and all return the instance type of the class. There is no need for further checks and we can apply the + // type arguments in the same manner as a type reference to get the same error reporting experience. + baseType = getTypeFromClassOrInterfaceReference(baseTypeNode, baseConstructorType.symbol); + } + else if (baseConstructorType.flags & TypeFlags.Any) { + baseType = baseConstructorType; + } + else { + // The class derives from a "class-like" constructor function, check that we have at least one construct signature + // with a matching number of type parameters and use the return type of the first instantiated signature. Elsewhere + // we check that all instantiated signatures return the same type. + const constructors = getInstantiatedConstructorsForTypeArguments(baseConstructorType, baseTypeNode.typeArguments, baseTypeNode); + if (!constructors.length) { + error(baseTypeNode.expression, Diagnostics.No_base_constructor_has_the_specified_number_of_type_arguments); + return type.resolvedBaseTypes = emptyArray; } + baseType = getReturnTypeOfSignature(constructors[0]); } - - function getResolvedSymbol(node: Identifier): Symbol { - const links = getNodeLinks(node); - if (!links.resolvedSymbol) { - links.resolvedSymbol = !nodeIsMissing(node) && - resolveName( - node, - node.escapedText, - SymbolFlags.Value | SymbolFlags.ExportValue, - getCannotFindNameDiagnosticForName(node), - node, - !isWriteOnlyAccess(node), - /*excludeGlobals*/ false, - Diagnostics.Cannot_find_name_0_Did_you_mean_1) || unknownSymbol; - } - return links.resolvedSymbol; - } - - function isInTypeQuery(node: Node): boolean { - // TypeScript 1.0 spec (April 2014): 3.6.3 - // A type query consists of the keyword typeof followed by an expression. - // The expression is restricted to a single identifier or a sequence of identifiers separated by periods - return !!findAncestor( - node, - n => n.kind === SyntaxKind.TypeQuery ? true : n.kind === SyntaxKind.Identifier || n.kind === SyntaxKind.QualifiedName ? false : "quit"); + if (baseType === errorType) { + return type.resolvedBaseTypes = emptyArray; } - - // Return the flow cache key for a "dotted name" (i.e. a sequence of identifiers - // separated by dots). The key consists of the id of the symbol referenced by the - // leftmost identifier followed by zero or more property names separated by dots. - // The result is undefined if the reference isn't a dotted name. We prefix nodes - // occurring in an apparent type position with '@' because the control flow type - // of such nodes may be based on the apparent type instead of the declared type. - function getFlowCacheKey(node: Node, declaredType: Type, initialType: Type, flowContainer: Node | undefined): string | undefined { - switch (node.kind) { - case SyntaxKind.Identifier: - const symbol = getResolvedSymbol(node); - return symbol !== unknownSymbol ? `${flowContainer ? getNodeId(flowContainer) : "-1"}|${getTypeId(declaredType)}|${getTypeId(initialType)}|${isConstraintPosition(node) ? "@" : ""}${getSymbolId(symbol)}` : undefined; - case SyntaxKind.ThisKeyword: - return "0"; - case SyntaxKind.NonNullExpression: - case SyntaxKind.ParenthesizedExpression: - return getFlowCacheKey((node).expression, declaredType, initialType, flowContainer); - case SyntaxKind.PropertyAccessExpression: - case SyntaxKind.ElementAccessExpression: - const propName = getAccessedPropertyName(node); - if (propName !== undefined) { - const key = getFlowCacheKey((node).expression, declaredType, initialType, flowContainer); - return key && key + "." + propName; - } - } - return undefined; + if (!isValidBaseType(baseType)) { + error(baseTypeNode.expression, Diagnostics.Base_constructor_return_type_0_is_not_an_object_type_or_intersection_of_object_types_with_statically_known_members, typeToString(baseType)); + return type.resolvedBaseTypes = emptyArray; } - - function isMatchingReference(source: Node, target: Node): boolean { - switch (target.kind) { - case SyntaxKind.ParenthesizedExpression: - case SyntaxKind.NonNullExpression: - return isMatchingReference(source, (target as NonNullExpression | ParenthesizedExpression).expression); - } - switch (source.kind) { - case SyntaxKind.Identifier: - return target.kind === SyntaxKind.Identifier && getResolvedSymbol(source) === getResolvedSymbol(target) || - (target.kind === SyntaxKind.VariableDeclaration || target.kind === SyntaxKind.BindingElement) && - getExportSymbolOfValueSymbolIfExported(getResolvedSymbol(source)) === getSymbolOfNode(target); - case SyntaxKind.ThisKeyword: - return target.kind === SyntaxKind.ThisKeyword; - case SyntaxKind.SuperKeyword: - return target.kind === SyntaxKind.SuperKeyword; - case SyntaxKind.NonNullExpression: - case SyntaxKind.ParenthesizedExpression: - return isMatchingReference((source as NonNullExpression | ParenthesizedExpression).expression, target); - case SyntaxKind.PropertyAccessExpression: - case SyntaxKind.ElementAccessExpression: - return isAccessExpression(target) && - getAccessedPropertyName(source) === getAccessedPropertyName(target) && - isMatchingReference((source).expression, target.expression); - } - return false; + if (type === baseType || hasBaseType(baseType, type)) { + error(type.symbol.valueDeclaration, Diagnostics.Type_0_recursively_references_itself_as_a_base_type, typeToString(type, /*enclosingDeclaration*/ undefined, TypeFormatFlags.WriteArrayAsGenericType)); + return type.resolvedBaseTypes = emptyArray; } - - function getAccessedPropertyName(access: AccessExpression): __String | undefined { - return access.kind === SyntaxKind.PropertyAccessExpression ? access.name.escapedText : - isStringOrNumericLiteralLike(access.argumentExpression) ? escapeLeadingUnderscores(access.argumentExpression.text) : - undefined; + if (type.resolvedBaseTypes === resolvingEmptyArray) { + // Circular reference, likely through instantiation of default parameters + // (otherwise there'd be an error from hasBaseType) - this is fine, but `.members` should be reset + // as `getIndexedAccessType` via `instantiateType` via `getTypeFromClassOrInterfaceReference` forces a + // partial instantiation of the members without the base types fully resolved + type.members = undefined; } - - function containsMatchingReference(source: Node, target: Node) { - while (isAccessExpression(source)) { - source = source.expression; - if (isMatchingReference(source, target)) { - return true; + return type.resolvedBaseTypes = [baseType]; + } + function areAllOuterTypeParametersApplied(type: ts.Type): boolean { + // An unapplied type parameter has its symbol still the same as the matching argument symbol. + // Since parameters are applied outer-to-inner, only the last outer parameter needs to be checked. + const outerTypeParameters = (type).outerTypeParameters; + if (outerTypeParameters) { + const last = outerTypeParameters.length - 1; + const typeArguments = getTypeArguments((type)); + return outerTypeParameters[last].symbol !== typeArguments[last].symbol; + } + return true; + } + // A valid base type is `any`, an object type or intersection of object types. + function isValidBaseType(type: ts.Type): type is BaseType { + if (type.flags & TypeFlags.TypeParameter) { + const constraint = getBaseConstraintOfType(type); + if (constraint) { + return isValidBaseType(constraint); + } + } + // TODO: Given that we allow type parmeters here now, is this `!isGenericMappedType(type)` check really needed? + // There's no reason a `T` should be allowed while a `Readonly` should not. + return !!(type.flags & (TypeFlags.Object | TypeFlags.NonPrimitive | TypeFlags.Any)) && !isGenericMappedType(type) || + !!(type.flags & TypeFlags.Intersection) && every((type).types, isValidBaseType); + } + function resolveBaseTypesOfInterface(type: InterfaceType): void { + type.resolvedBaseTypes = type.resolvedBaseTypes || emptyArray; + for (const declaration of type.symbol.declarations) { + if (declaration.kind === SyntaxKind.InterfaceDeclaration && getInterfaceBaseTypeNodes((declaration))) { + for (const node of getInterfaceBaseTypeNodes((declaration))!) { + const baseType = getTypeFromTypeNode(node); + if (baseType !== errorType) { + if (isValidBaseType(baseType)) { + if (type !== baseType && !hasBaseType(baseType, type)) { + if (type.resolvedBaseTypes === emptyArray) { + type.resolvedBaseTypes = [(baseType)]; + } + else { + type.resolvedBaseTypes.push(baseType); + } + } + else { + error(declaration, Diagnostics.Type_0_recursively_references_itself_as_a_base_type, typeToString(type, /*enclosingDeclaration*/ undefined, TypeFormatFlags.WriteArrayAsGenericType)); + } + } + else { + error(node, Diagnostics.An_interface_can_only_extend_an_object_type_or_intersection_of_object_types_with_statically_known_members); + } + } } } - return false; } - - function optionalChainContainsReference(source: Node, target: Node) { - while (isOptionalChain(source)) { - source = source.expression; - if (isMatchingReference(source, target)) { - return true; + } + /** + * Returns true if the interface given by the symbol is free of "this" references. + * + * Specifically, the result is true if the interface itself contains no references + * to "this" in its body, if all base types are interfaces, + * and if none of the base interfaces have a "this" type. + */ + function isThislessInterface(symbol: ts.Symbol): boolean { + for (const declaration of symbol.declarations) { + if (declaration.kind === SyntaxKind.InterfaceDeclaration) { + if (declaration.flags & NodeFlags.ContainsThis) { + return false; } - } - return false; - } - - function isDiscriminantProperty(type: Type | undefined, name: __String) { - if (type && type.flags & TypeFlags.Union) { - const prop = getUnionOrIntersectionProperty(type, name); - if (prop && getCheckFlags(prop) & CheckFlags.SyntheticProperty) { - if ((prop).isDiscriminantProperty === undefined) { - (prop).isDiscriminantProperty = - ((prop).checkFlags & CheckFlags.Discriminant) === CheckFlags.Discriminant && - !maybeTypeOfKind(getTypeOfSymbol(prop), TypeFlags.Instantiable); + const baseTypeNodes = getInterfaceBaseTypeNodes((declaration)); + if (baseTypeNodes) { + for (const node of baseTypeNodes) { + if (isEntityNameExpression(node.expression)) { + const baseSymbol = resolveEntityName(node.expression, SymbolFlags.Type, /*ignoreErrors*/ true); + if (!baseSymbol || !(baseSymbol.flags & SymbolFlags.Interface) || getDeclaredTypeOfClassOrInterface(baseSymbol).thisType) { + return false; + } + } } - return !!(prop).isDiscriminantProperty; } } - return false; } - - function isSyntheticThisPropertyAccess(expr: Node) { - return isAccessExpression(expr) && expr.expression.kind === SyntaxKind.ThisKeyword && !!(expr.expression.flags & NodeFlags.Synthesized); - } - - function findDiscriminantProperties(sourceProperties: Symbol[], target: Type): Symbol[] | undefined { - let result: Symbol[] | undefined; - for (const sourceProperty of sourceProperties) { - if (isDiscriminantProperty(target, sourceProperty.escapedName)) { - if (result) { - result.push(sourceProperty); - continue; - } - result = [sourceProperty]; - } + return true; + } + function getDeclaredTypeOfClassOrInterface(symbol: ts.Symbol): InterfaceType { + let links = getSymbolLinks(symbol); + const originalLinks = links; + if (!links.declaredType) { + const kind = symbol.flags & SymbolFlags.Class ? ObjectFlags.Class : ObjectFlags.Interface; + const merged = mergeJSSymbols(symbol, getAssignedClassSymbol(symbol.valueDeclaration)); + if (merged) { + // note:we overwrite links because we just cloned the symbol + symbol = links = merged; + } + const type = originalLinks.declaredType = links.declaredType = (createObjectType(kind, symbol)); + const outerTypeParameters = getOuterTypeParametersOfClassOrInterface(symbol); + const localTypeParameters = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol); + // A class or interface is generic if it has type parameters or a "this" type. We always give classes a "this" type + // because it is not feasible to analyze all members to determine if the "this" type escapes the class (in particular, + // property types inferred from initializers and method return types inferred from return statements are very hard + // to exhaustively analyze). We give interfaces a "this" type if we can't definitely determine that they are free of + // "this" references. + if (outerTypeParameters || localTypeParameters || kind === ObjectFlags.Class || !isThislessInterface(symbol)) { + type.objectFlags |= ObjectFlags.Reference; + type.typeParameters = concatenate(outerTypeParameters, localTypeParameters); + type.outerTypeParameters = outerTypeParameters; + type.localTypeParameters = localTypeParameters; + (type).instantiations = createMap(); + (type).instantiations.set(getTypeListId(type.typeParameters), (type)); + (type).target = (type); + (type).resolvedTypeArguments = type.typeParameters; + type.thisType = createTypeParameter(symbol); + type.thisType.isThisType = true; + type.thisType.constraint = type; + } + } + return links.declaredType; + } + function getDeclaredTypeOfTypeAlias(symbol: ts.Symbol): ts.Type { + const links = getSymbolLinks(symbol); + if (!links.declaredType) { + // Note that we use the links object as the target here because the symbol object is used as the unique + // identity for resolution of the 'type' property in SymbolLinks. + if (!pushTypeResolution(symbol, TypeSystemPropertyName.DeclaredType)) { + return errorType; } - return result; - } - - function isOrContainsMatchingReference(source: Node, target: Node) { - return isMatchingReference(source, target) || containsMatchingReference(source, target); - } - - function hasMatchingArgument(callExpression: CallExpression, reference: Node) { - if (callExpression.arguments) { - for (const argument of callExpression.arguments) { - if (isOrContainsMatchingReference(reference, argument)) { - return true; - } + const declaration = Debug.checkDefined(find(symbol.declarations, isTypeAlias), "Type alias symbol with no valid declaration found"); + const typeNode = isJSDocTypeAlias(declaration) ? declaration.typeExpression : declaration.type; + // If typeNode is missing, we will error in checkJSDocTypedefTag. + let type = typeNode ? getTypeFromTypeNode(typeNode) : errorType; + if (popTypeResolution()) { + const typeParameters = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol); + if (typeParameters) { + // Initialize the instantiation cache for generic type aliases. The declared type corresponds to + // an instantiation of the type alias with the type parameters supplied as type arguments. + links.typeParameters = typeParameters; + links.instantiations = createMap(); + links.instantiations.set(getTypeListId(typeParameters), type); } } - if (callExpression.expression.kind === SyntaxKind.PropertyAccessExpression && - isOrContainsMatchingReference(reference, (callExpression.expression).expression)) { - return true; + else { + type = errorType; + error(isNamedDeclaration(declaration) ? declaration.name : declaration || declaration, Diagnostics.Type_alias_0_circularly_references_itself, symbolToString(symbol)); } - return false; + links.declaredType = type; } - - function getFlowNodeId(flow: FlowNode): number { - if (!flow.id || flow.id < 0) { - flow.id = nextFlowId; - nextFlowId++; - } - return flow.id; + return links.declaredType; + } + function isStringConcatExpression(expr: Node): boolean { + if (isStringLiteralLike(expr)) { + return true; } - - function typeMaybeAssignableTo(source: Type, target: Type) { - if (!(source.flags & TypeFlags.Union)) { - return isTypeAssignableTo(source, target); - } - for (const t of (source).types) { - if (isTypeAssignableTo(t, target)) { - return true; - } - } - return false; + else if (expr.kind === SyntaxKind.BinaryExpression) { + return isStringConcatExpression((expr).left) && isStringConcatExpression((expr).right); } - - // Remove those constituent types of declaredType to which no constituent type of assignedType is assignable. - // For example, when a variable of type number | string | boolean is assigned a value of type number | boolean, - // we remove type string. - function getAssignmentReducedType(declaredType: UnionType, assignedType: Type) { - if (declaredType !== assignedType) { - if (assignedType.flags & TypeFlags.Never) { - return assignedType; - } - let reducedType = filterType(declaredType, t => typeMaybeAssignableTo(assignedType, t)); - if (assignedType.flags & TypeFlags.BooleanLiteral && isFreshLiteralType(assignedType)) { - reducedType = mapType(reducedType, getFreshTypeOfLiteralType); // Ensure that if the assignment is a fresh type, that we narrow to fresh types - } - // Our crude heuristic produces an invalid result in some cases: see GH#26130. - // For now, when that happens, we give up and don't narrow at all. (This also - // means we'll never narrow for erroneous assignments where the assigned type - // is not assignable to the declared type.) - if (isTypeAssignableTo(assignedType, reducedType)) { - return reducedType; - } - } - return declaredType; + return false; + } + function isLiteralEnumMember(member: EnumMember) { + const expr = member.initializer; + if (!expr) { + return !(member.flags & NodeFlags.Ambient); + } + switch (expr.kind) { + case SyntaxKind.StringLiteral: + case SyntaxKind.NumericLiteral: + case SyntaxKind.NoSubstitutionTemplateLiteral: + return true; + case SyntaxKind.PrefixUnaryExpression: + return (expr).operator === SyntaxKind.MinusToken && + (expr).operand.kind === SyntaxKind.NumericLiteral; + case SyntaxKind.Identifier: + return nodeIsMissing(expr) || !!getSymbolOfNode(member.parent).exports!.get((expr).escapedText); + case SyntaxKind.BinaryExpression: + return isStringConcatExpression(expr); + default: + return false; } - - function getTypeFactsOfTypes(types: Type[]): TypeFacts { - let result: TypeFacts = TypeFacts.None; - for (const t of types) { - result |= getTypeFacts(t); + } + function getEnumKind(symbol: ts.Symbol): EnumKind { + const links = getSymbolLinks(symbol); + if (links.enumKind !== undefined) { + return links.enumKind; + } + let hasNonLiteralMember = false; + for (const declaration of symbol.declarations) { + if (declaration.kind === SyntaxKind.EnumDeclaration) { + for (const member of (declaration).members) { + if (member.initializer && isStringLiteralLike(member.initializer)) { + return links.enumKind = EnumKind.Literal; + } + if (!isLiteralEnumMember(member)) { + hasNonLiteralMember = true; + } + } } - return result; } - - function isFunctionObjectType(type: ObjectType): boolean { - // We do a quick check for a "bind" property before performing the more expensive subtype - // check. This gives us a quicker out in the common case where an object type is not a function. - const resolved = resolveStructuredTypeMembers(type); - return !!(resolved.callSignatures.length || resolved.constructSignatures.length || - resolved.members.get("bind" as __String) && isTypeSubtypeOf(type, globalFunctionType)); + return links.enumKind = hasNonLiteralMember ? EnumKind.Numeric : EnumKind.Literal; + } + function getBaseTypeOfEnumLiteralType(type: ts.Type) { + return type.flags & TypeFlags.EnumLiteral && !(type.flags & TypeFlags.Union) ? getDeclaredTypeOfSymbol(getParentOfSymbol(type.symbol)!) : type; + } + function getDeclaredTypeOfEnum(symbol: ts.Symbol): ts.Type { + const links = getSymbolLinks(symbol); + if (links.declaredType) { + return links.declaredType; } - - function getTypeFacts(type: Type): TypeFacts { - const flags = type.flags; - if (flags & TypeFlags.String) { - return strictNullChecks ? TypeFacts.StringStrictFacts : TypeFacts.StringFacts; - } - if (flags & TypeFlags.StringLiteral) { - const isEmpty = (type).value === ""; - return strictNullChecks ? - isEmpty ? TypeFacts.EmptyStringStrictFacts : TypeFacts.NonEmptyStringStrictFacts : - isEmpty ? TypeFacts.EmptyStringFacts : TypeFacts.NonEmptyStringFacts; - } - if (flags & (TypeFlags.Number | TypeFlags.Enum)) { - return strictNullChecks ? TypeFacts.NumberStrictFacts : TypeFacts.NumberFacts; - } - if (flags & TypeFlags.NumberLiteral) { - const isZero = (type).value === 0; - return strictNullChecks ? - isZero ? TypeFacts.ZeroNumberStrictFacts : TypeFacts.NonZeroNumberStrictFacts : - isZero ? TypeFacts.ZeroNumberFacts : TypeFacts.NonZeroNumberFacts; - } - if (flags & TypeFlags.BigInt) { - return strictNullChecks ? TypeFacts.BigIntStrictFacts : TypeFacts.BigIntFacts; - } - if (flags & TypeFlags.BigIntLiteral) { - const isZero = isZeroBigInt(type); - return strictNullChecks ? - isZero ? TypeFacts.ZeroBigIntStrictFacts : TypeFacts.NonZeroBigIntStrictFacts : - isZero ? TypeFacts.ZeroBigIntFacts : TypeFacts.NonZeroBigIntFacts; - } - if (flags & TypeFlags.Boolean) { - return strictNullChecks ? TypeFacts.BooleanStrictFacts : TypeFacts.BooleanFacts; - } - if (flags & TypeFlags.BooleanLike) { - return strictNullChecks ? - (type === falseType || type === regularFalseType) ? TypeFacts.FalseStrictFacts : TypeFacts.TrueStrictFacts : - (type === falseType || type === regularFalseType) ? TypeFacts.FalseFacts : TypeFacts.TrueFacts; - } - if (flags & TypeFlags.Object) { - return getObjectFlags(type) & ObjectFlags.Anonymous && isEmptyObjectType(type) ? - strictNullChecks ? TypeFacts.EmptyObjectStrictFacts : TypeFacts.EmptyObjectFacts : - isFunctionObjectType(type) ? - strictNullChecks ? TypeFacts.FunctionStrictFacts : TypeFacts.FunctionFacts : - strictNullChecks ? TypeFacts.ObjectStrictFacts : TypeFacts.ObjectFacts; - } - if (flags & (TypeFlags.Void | TypeFlags.Undefined)) { - return TypeFacts.UndefinedFacts; - } - if (flags & TypeFlags.Null) { - return TypeFacts.NullFacts; - } - if (flags & TypeFlags.ESSymbolLike) { - return strictNullChecks ? TypeFacts.SymbolStrictFacts : TypeFacts.SymbolFacts; - } - if (flags & TypeFlags.NonPrimitive) { - return strictNullChecks ? TypeFacts.ObjectStrictFacts : TypeFacts.ObjectFacts; - } - if (flags & TypeFlags.Instantiable) { - return getTypeFacts(getBaseConstraintOfType(type) || unknownType); + if (getEnumKind(symbol) === EnumKind.Literal) { + enumCount++; + const memberTypeList: ts.Type[] = []; + for (const declaration of symbol.declarations) { + if (declaration.kind === SyntaxKind.EnumDeclaration) { + for (const member of (declaration).members) { + const value = getEnumMemberValue(member); + const memberType = getFreshTypeOfLiteralType(getLiteralType(value !== undefined ? value : 0, enumCount, getSymbolOfNode(member))); + getSymbolLinks(getSymbolOfNode(member)).declaredType = memberType; + memberTypeList.push(getRegularTypeOfLiteralType(memberType)); + } + } } - if (flags & TypeFlags.UnionOrIntersection) { - return getTypeFactsOfTypes((type).types); + if (memberTypeList.length) { + const enumType = getUnionType(memberTypeList, UnionReduction.Literal, symbol, /*aliasTypeArguments*/ undefined); + if (enumType.flags & TypeFlags.Union) { + enumType.flags |= TypeFlags.EnumLiteral; + enumType.symbol = symbol; + } + return links.declaredType = enumType; } - return TypeFacts.All; - } - - function getTypeWithFacts(type: Type, include: TypeFacts) { - return filterType(type, t => (getTypeFacts(t) & include) !== 0); } - - function getTypeWithDefault(type: Type, defaultExpression: Expression) { - if (defaultExpression) { - const defaultType = getTypeOfExpression(defaultExpression); - return getUnionType([getTypeWithFacts(type, TypeFacts.NEUndefined), defaultType]); + const enumType = createType(TypeFlags.Enum); + enumType.symbol = symbol; + return links.declaredType = enumType; + } + function getDeclaredTypeOfEnumMember(symbol: ts.Symbol): ts.Type { + const links = getSymbolLinks(symbol); + if (!links.declaredType) { + const enumType = getDeclaredTypeOfEnum(getParentOfSymbol(symbol)!); + if (!links.declaredType) { + links.declaredType = enumType; } - return type; - } - - function getTypeOfDestructuredProperty(type: Type, name: PropertyName) { - const nameType = getLiteralTypeFromPropertyName(name); - if (!isTypeUsableAsPropertyName(nameType)) return errorType; - const text = getPropertyNameFromType(nameType); - return getConstraintForLocation(getTypeOfPropertyOfType(type, text), name) || - isNumericLiteralName(text) && getIndexTypeOfType(type, IndexKind.Number) || - getIndexTypeOfType(type, IndexKind.String) || - errorType; - } - - function getTypeOfDestructuredArrayElement(type: Type, index: number) { - return everyType(type, isTupleLikeType) && getTupleElementType(type, index) || - checkIteratedTypeOrElementType(IterationUse.Destructuring, type, undefinedType, /*errorNode*/ undefined) || - errorType; } - - function getTypeOfDestructuredSpreadExpression(type: Type) { - return createArrayType(checkIteratedTypeOrElementType(IterationUse.Destructuring, type, undefinedType, /*errorNode*/ undefined) || errorType); + return links.declaredType; + } + function getDeclaredTypeOfTypeParameter(symbol: ts.Symbol): TypeParameter { + const links = getSymbolLinks(symbol); + return links.declaredType || (links.declaredType = createTypeParameter(symbol)); + } + function getDeclaredTypeOfAlias(symbol: ts.Symbol): ts.Type { + const links = getSymbolLinks(symbol); + return links.declaredType || (links.declaredType = getDeclaredTypeOfSymbol(resolveAlias(symbol))); + } + function getDeclaredTypeOfSymbol(symbol: ts.Symbol): ts.Type { + return tryGetDeclaredTypeOfSymbol(symbol) || errorType; + } + function tryGetDeclaredTypeOfSymbol(symbol: ts.Symbol): ts.Type | undefined { + if (symbol.flags & (SymbolFlags.Class | SymbolFlags.Interface)) { + return getDeclaredTypeOfClassOrInterface(symbol); } - - function getAssignedTypeOfBinaryExpression(node: BinaryExpression): Type { - const isDestructuringDefaultAssignment = - node.parent.kind === SyntaxKind.ArrayLiteralExpression && isDestructuringAssignmentTarget(node.parent) || - node.parent.kind === SyntaxKind.PropertyAssignment && isDestructuringAssignmentTarget(node.parent.parent); - return isDestructuringDefaultAssignment ? - getTypeWithDefault(getAssignedType(node), node.right) : - getTypeOfExpression(node.right); + if (symbol.flags & SymbolFlags.TypeAlias) { + return getDeclaredTypeOfTypeAlias(symbol); } - - function isDestructuringAssignmentTarget(parent: Node) { - return parent.parent.kind === SyntaxKind.BinaryExpression && (parent.parent as BinaryExpression).left === parent || - parent.parent.kind === SyntaxKind.ForOfStatement && (parent.parent as ForOfStatement).initializer === parent; + if (symbol.flags & SymbolFlags.TypeParameter) { + return getDeclaredTypeOfTypeParameter(symbol); } - - function getAssignedTypeOfArrayLiteralElement(node: ArrayLiteralExpression, element: Expression): Type { - return getTypeOfDestructuredArrayElement(getAssignedType(node), node.elements.indexOf(element)); + if (symbol.flags & SymbolFlags.Enum) { + return getDeclaredTypeOfEnum(symbol); } - - function getAssignedTypeOfSpreadExpression(node: SpreadElement): Type { - return getTypeOfDestructuredSpreadExpression(getAssignedType(node.parent)); + if (symbol.flags & SymbolFlags.EnumMember) { + return getDeclaredTypeOfEnumMember(symbol); } - - function getAssignedTypeOfPropertyAssignment(node: PropertyAssignment | ShorthandPropertyAssignment): Type { - return getTypeOfDestructuredProperty(getAssignedType(node.parent), node.name); + if (symbol.flags & SymbolFlags.Alias) { + return getDeclaredTypeOfAlias(symbol); } - - function getAssignedTypeOfShorthandPropertyAssignment(node: ShorthandPropertyAssignment): Type { - return getTypeWithDefault(getAssignedTypeOfPropertyAssignment(node), node.objectAssignmentInitializer!); + return undefined; + } + /** + * A type is free of this references if it's the any, string, number, boolean, symbol, or void keyword, a string + * literal type, an array with an element type that is free of this references, or a type reference that is + * free of this references. + */ + function isThislessType(node: TypeNode): boolean { + switch (node.kind) { + case SyntaxKind.AnyKeyword: + case SyntaxKind.UnknownKeyword: + case SyntaxKind.StringKeyword: + case SyntaxKind.NumberKeyword: + case SyntaxKind.BigIntKeyword: + case SyntaxKind.BooleanKeyword: + case SyntaxKind.SymbolKeyword: + case SyntaxKind.ObjectKeyword: + case SyntaxKind.VoidKeyword: + case SyntaxKind.UndefinedKeyword: + case SyntaxKind.NullKeyword: + case SyntaxKind.NeverKeyword: + case SyntaxKind.LiteralType: + return true; + case SyntaxKind.ArrayType: + return isThislessType((node).elementType); + case SyntaxKind.TypeReference: + return !(node as TypeReferenceNode).typeArguments || (node as TypeReferenceNode).typeArguments!.every(isThislessType); } - - function getAssignedType(node: Expression): Type { - const { parent } = node; - switch (parent.kind) { - case SyntaxKind.ForInStatement: - return stringType; - case SyntaxKind.ForOfStatement: - return checkRightHandSideOfForOf((parent).expression, (parent).awaitModifier) || errorType; - case SyntaxKind.BinaryExpression: - return getAssignedTypeOfBinaryExpression(parent); - case SyntaxKind.DeleteExpression: - return undefinedType; - case SyntaxKind.ArrayLiteralExpression: - return getAssignedTypeOfArrayLiteralElement(parent, node); - case SyntaxKind.SpreadElement: - return getAssignedTypeOfSpreadExpression(parent); - case SyntaxKind.PropertyAssignment: - return getAssignedTypeOfPropertyAssignment(parent); - case SyntaxKind.ShorthandPropertyAssignment: - return getAssignedTypeOfShorthandPropertyAssignment(parent); + return false; + } + /** A type parameter is thisless if its constraint is thisless, or if it has no constraint. */ + function isThislessTypeParameter(node: TypeParameterDeclaration) { + const constraint = getEffectiveConstraintOfTypeParameter(node); + return !constraint || isThislessType(constraint); + } + /** + * A variable-like declaration is free of this references if it has a type annotation + * that is thisless, or if it has no type annotation and no initializer (and is thus of type any). + */ + function isThislessVariableLikeDeclaration(node: VariableLikeDeclaration): boolean { + const typeNode = getEffectiveTypeAnnotationNode(node); + return typeNode ? isThislessType(typeNode) : !hasInitializer(node); + } + /** + * A function-like declaration is considered free of `this` references if it has a return type + * annotation that is free of this references and if each parameter is thisless and if + * each type parameter (if present) is thisless. + */ + function isThislessFunctionLikeDeclaration(node: FunctionLikeDeclaration): boolean { + const returnType = getEffectiveReturnTypeNode(node); + const typeParameters = getEffectiveTypeParameterDeclarations(node); + return (node.kind === SyntaxKind.Constructor || (!!returnType && isThislessType(returnType))) && + node.parameters.every(isThislessVariableLikeDeclaration) && + typeParameters.every(isThislessTypeParameter); + } + /** + * Returns true if the class or interface member given by the symbol is free of "this" references. The + * function may return false for symbols that are actually free of "this" references because it is not + * feasible to perform a complete analysis in all cases. In particular, property members with types + * inferred from their initializers and function members with inferred return types are conservatively + * assumed not to be free of "this" references. + */ + function isThisless(symbol: ts.Symbol): boolean { + if (symbol.declarations && symbol.declarations.length === 1) { + const declaration = symbol.declarations[0]; + if (declaration) { + switch (declaration.kind) { + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.PropertySignature: + return isThislessVariableLikeDeclaration((declaration)); + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: + case SyntaxKind.Constructor: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + return isThislessFunctionLikeDeclaration((declaration)); + } } - return errorType; - } - - function getInitialTypeOfBindingElement(node: BindingElement): Type { - const pattern = node.parent; - const parentType = getInitialType(pattern.parent); - const type = pattern.kind === SyntaxKind.ObjectBindingPattern ? - getTypeOfDestructuredProperty(parentType, node.propertyName || node.name) : - !node.dotDotDotToken ? - getTypeOfDestructuredArrayElement(parentType, pattern.elements.indexOf(node)) : - getTypeOfDestructuredSpreadExpression(parentType); - return getTypeWithDefault(type, node.initializer!); - } - - function getTypeOfInitializer(node: Expression) { - // Return the cached type if one is available. If the type of the variable was inferred - // from its initializer, we'll already have cached the type. Otherwise we compute it now - // without caching such that transient types are reflected. - const links = getNodeLinks(node); - return links.resolvedType || getTypeOfExpression(node); } - - function getInitialTypeOfVariableDeclaration(node: VariableDeclaration) { - if (node.initializer) { - return getTypeOfInitializer(node.initializer); - } - if (node.parent.parent.kind === SyntaxKind.ForInStatement) { - return stringType; - } - if (node.parent.parent.kind === SyntaxKind.ForOfStatement) { - return checkRightHandSideOfForOf(node.parent.parent.expression, node.parent.parent.awaitModifier) || errorType; + return false; + } + // The mappingThisOnly flag indicates that the only type parameter being mapped is "this". When the flag is true, + // we check symbols to see if we can quickly conclude they are free of "this" references, thus needing no instantiation. + function createInstantiatedSymbolTable(symbols: ts.Symbol[], mapper: TypeMapper, mappingThisOnly: boolean): SymbolTable { + const result = createSymbolTable(); + for (const symbol of symbols) { + result.set(symbol.escapedName, mappingThisOnly && isThisless(symbol) ? symbol : instantiateSymbol(symbol, mapper)); + } + return result; + } + function addInheritedMembers(symbols: SymbolTable, baseSymbols: ts.Symbol[]) { + for (const s of baseSymbols) { + if (!symbols.has(s.escapedName) && !isStaticPrivateIdentifierProperty(s)) { + symbols.set(s.escapedName, s); } - return errorType; } - - function getInitialType(node: VariableDeclaration | BindingElement) { - return node.kind === SyntaxKind.VariableDeclaration ? - getInitialTypeOfVariableDeclaration(node) : - getInitialTypeOfBindingElement(node); - } - - function isEmptyArrayAssignment(node: VariableDeclaration | BindingElement | Expression) { - return node.kind === SyntaxKind.VariableDeclaration && (node).initializer && - isEmptyArrayLiteral((node).initializer!) || - node.kind !== SyntaxKind.BindingElement && node.parent.kind === SyntaxKind.BinaryExpression && - isEmptyArrayLiteral((node.parent).right); + } + function isStaticPrivateIdentifierProperty(s: ts.Symbol): boolean { + return !!s.valueDeclaration && isPrivateIdentifierPropertyDeclaration(s.valueDeclaration) && hasModifier(s.valueDeclaration, ModifierFlags.Static); + } + function resolveDeclaredMembers(type: InterfaceType): InterfaceTypeWithDeclaredMembers { + if (!(type).declaredProperties) { + const symbol = type.symbol; + const members = getMembersOfSymbol(symbol); + (type).declaredProperties = getNamedMembers(members); + // Start with signatures at empty array in case of recursive types + (type).declaredCallSignatures = emptyArray; + (type).declaredConstructSignatures = emptyArray; + (type).declaredCallSignatures = getSignaturesOfSymbol(members.get(InternalSymbolName.Call)); + (type).declaredConstructSignatures = getSignaturesOfSymbol(members.get(InternalSymbolName.New)); + (type).declaredStringIndexInfo = getIndexInfoOfSymbol(symbol, IndexKind.String); + (type).declaredNumberIndexInfo = getIndexInfoOfSymbol(symbol, IndexKind.Number); + } + return type; + } + /** + * Indicates whether a type can be used as a property name. + */ + function isTypeUsableAsPropertyName(type: ts.Type): type is StringLiteralType | NumberLiteralType | UniqueESSymbolType { + return !!(type.flags & TypeFlags.StringOrNumberLiteralOrUnique); + } + /** + * Indicates whether a declaration name is definitely late-bindable. + * A declaration name is only late-bindable if: + * - It is a `ComputedPropertyName`. + * - Its expression is an `Identifier` or either a `PropertyAccessExpression` an + * `ElementAccessExpression` consisting only of these same three types of nodes. + * - The type of its expression is a string or numeric literal type, or is a `unique symbol` type. + */ + function isLateBindableName(node: DeclarationName): node is LateBoundName { + if (!isComputedPropertyName(node) && !isElementAccessExpression(node)) { + return false; } - - function getReferenceCandidate(node: Expression): Expression { - switch (node.kind) { - case SyntaxKind.ParenthesizedExpression: - return getReferenceCandidate((node).expression); - case SyntaxKind.BinaryExpression: - switch ((node).operatorToken.kind) { - case SyntaxKind.EqualsToken: - return getReferenceCandidate((node).left); - case SyntaxKind.CommaToken: - return getReferenceCandidate((node).right); - } - } - return node; + const expr = isComputedPropertyName(node) ? node.expression : node.argumentExpression; + return isEntityNameExpression(expr) + && isTypeUsableAsPropertyName(isComputedPropertyName(node) ? checkComputedPropertyName(node) : checkExpressionCached(expr)); + } + function isLateBoundName(name: __String): boolean { + return (name as string).charCodeAt(0) === CharacterCodes._ && + (name as string).charCodeAt(1) === CharacterCodes._ && + (name as string).charCodeAt(2) === CharacterCodes.at; + } + /** + * Indicates whether a declaration has a late-bindable dynamic name. + */ + function hasLateBindableName(node: Declaration): node is LateBoundDeclaration | LateBoundBinaryExpressionDeclaration { + const name = getNameOfDeclaration(node); + return !!name && isLateBindableName(name); + } + /** + * Indicates whether a declaration has a dynamic name that cannot be late-bound. + */ + function hasNonBindableDynamicName(node: Declaration) { + return hasDynamicName(node) && !hasLateBindableName(node); + } + /** + * Indicates whether a declaration name is a dynamic name that cannot be late-bound. + */ + function isNonBindableDynamicName(node: DeclarationName) { + return isDynamicName(node) && !isLateBindableName(node); + } + /** + * Gets the symbolic name for a member from its type. + */ + function getPropertyNameFromType(type: StringLiteralType | NumberLiteralType | UniqueESSymbolType): __String { + if (type.flags & TypeFlags.UniqueESSymbol) { + return (type).escapedName; } - - function getReferenceRoot(node: Node): Node { - const { parent } = node; - return parent.kind === SyntaxKind.ParenthesizedExpression || - parent.kind === SyntaxKind.BinaryExpression && (parent).operatorToken.kind === SyntaxKind.EqualsToken && (parent).left === node || - parent.kind === SyntaxKind.BinaryExpression && (parent).operatorToken.kind === SyntaxKind.CommaToken && (parent).right === node ? - getReferenceRoot(parent) : node; + if (type.flags & (TypeFlags.StringLiteral | TypeFlags.NumberLiteral)) { + return escapeLeadingUnderscores("" + (type).value); } - - function getTypeOfSwitchClause(clause: CaseClause | DefaultClause) { - if (clause.kind === SyntaxKind.CaseClause) { - return getRegularTypeOfLiteralType(getTypeOfExpression(clause.expression)); + return Debug.fail(); + } + /** + * Adds a declaration to a late-bound dynamic member. This performs the same function for + * late-bound members that `addDeclarationToSymbol` in binder.ts performs for early-bound + * members. + */ + function addDeclarationToLateBoundSymbol(symbol: ts.Symbol, member: LateBoundDeclaration | BinaryExpression, symbolFlags: SymbolFlags) { + Debug.assert(!!(getCheckFlags(symbol) & CheckFlags.Late), "Expected a late-bound symbol."); + symbol.flags |= symbolFlags; + getSymbolLinks(member.symbol).lateSymbol = symbol; + if (!symbol.declarations) { + symbol.declarations = [member]; + } + else { + symbol.declarations.push(member); + } + if (symbolFlags & SymbolFlags.Value) { + if (!symbol.valueDeclaration || symbol.valueDeclaration.kind !== member.kind) { + symbol.valueDeclaration = member; } - return neverType; } - - function getSwitchClauseTypes(switchStatement: SwitchStatement): Type[] { - const links = getNodeLinks(switchStatement); - if (!links.switchTypes) { - links.switchTypes = []; - for (const clause of switchStatement.caseBlock.clauses) { - links.switchTypes.push(getTypeOfSwitchClause(clause)); + } + /** + * Performs late-binding of a dynamic member. This performs the same function for + * late-bound members that `declareSymbol` in binder.ts performs for early-bound + * members. + * + * If a symbol is a dynamic name from a computed property, we perform an additional "late" + * binding phase to attempt to resolve the name for the symbol from the type of the computed + * property's expression. If the type of the expression is a string-literal, numeric-literal, + * or unique symbol type, we can use that type as the name of the symbol. + * + * For example, given: + * + * const x = Symbol(); + * + * interface I { + * [x]: number; + * } + * + * The binder gives the property `[x]: number` a special symbol with the name "__computed". + * In the late-binding phase we can type-check the expression `x` and see that it has a + * unique symbol type which we can then use as the name of the member. This allows users + * to define custom symbols that can be used in the members of an object type. + * + * @param parent The containing symbol for the member. + * @param earlySymbols The early-bound symbols of the parent. + * @param lateSymbols The late-bound symbols of the parent. + * @param decl The member to bind. + */ + function lateBindMember(parent: ts.Symbol, earlySymbols: SymbolTable | undefined, lateSymbols: UnderscoreEscapedMap, decl: LateBoundDeclaration | LateBoundBinaryExpressionDeclaration) { + Debug.assert(!!decl.symbol, "The member is expected to have a symbol."); + const links = getNodeLinks(decl); + if (!links.resolvedSymbol) { + // In the event we attempt to resolve the late-bound name of this member recursively, + // fall back to the early-bound name of this member. + links.resolvedSymbol = decl.symbol; + const declName = isBinaryExpression(decl) ? decl.left : decl.name; + const type = isElementAccessExpression(declName) ? checkExpressionCached(declName.argumentExpression) : checkComputedPropertyName(declName); + if (isTypeUsableAsPropertyName(type)) { + const memberName = getPropertyNameFromType(type); + const symbolFlags = decl.symbol.flags; + // Get or add a late-bound symbol for the member. This allows us to merge late-bound accessor declarations. + let lateSymbol = lateSymbols.get(memberName); + if (!lateSymbol) + lateSymbols.set(memberName, lateSymbol = createSymbol(SymbolFlags.None, memberName, CheckFlags.Late)); + // Report an error if a late-bound member has the same name as an early-bound member, + // or if we have another early-bound symbol declaration with the same name and + // conflicting flags. + const earlySymbol = earlySymbols && earlySymbols.get(memberName); + if (lateSymbol.flags & getExcludedSymbolFlags(symbolFlags) || earlySymbol) { + // If we have an existing early-bound member, combine its declarations so that we can + // report an error at each declaration. + const declarations = earlySymbol ? concatenate(earlySymbol.declarations, lateSymbol.declarations) : lateSymbol.declarations; + const name = !(type.flags & TypeFlags.UniqueESSymbol) && unescapeLeadingUnderscores(memberName) || declarationNameToString(declName); + forEach(declarations, declaration => error(getNameOfDeclaration(declaration) || declaration, Diagnostics.Property_0_was_also_declared_here, name)); + error(declName || decl, Diagnostics.Duplicate_property_0, name); + lateSymbol = createSymbol(SymbolFlags.None, memberName, CheckFlags.Late); + } + lateSymbol.nameType = type; + addDeclarationToLateBoundSymbol(lateSymbol, decl, symbolFlags); + if (lateSymbol.parent) { + Debug.assert(lateSymbol.parent === parent, "Existing symbol parent should match new one"); + } + else { + lateSymbol.parent = parent; } + return links.resolvedSymbol = lateSymbol; } - return links.switchTypes; } - - // Get the types from all cases in a switch on `typeof`. An - // `undefined` element denotes an explicit `default` clause. - function getSwitchClauseTypeOfWitnesses(switchStatement: SwitchStatement): (string | undefined)[] { - const witnesses: (string | undefined)[] = []; - for (const clause of switchStatement.caseBlock.clauses) { - if (clause.kind === SyntaxKind.CaseClause) { - if (isStringLiteralLike(clause.expression)) { - witnesses.push(clause.expression.text); - continue; + return links.resolvedSymbol; + } + function getResolvedMembersOrExportsOfSymbol(symbol: ts.Symbol, resolutionKind: MembersOrExportsResolutionKind): UnderscoreEscapedMap { + const links = getSymbolLinks(symbol); + if (!links[resolutionKind]) { + const isStatic = resolutionKind === MembersOrExportsResolutionKind.resolvedExports; + const earlySymbols = !isStatic ? symbol.members : + symbol.flags & SymbolFlags.Module ? getExportsOfModuleWorker(symbol) : + symbol.exports; + // In the event we recursively resolve the members/exports of the symbol, we + // set the initial value of resolvedMembers/resolvedExports to the early-bound + // members/exports of the symbol. + links[resolutionKind] = earlySymbols || emptySymbols; + // fill in any as-yet-unresolved late-bound members. + const lateSymbols = (createSymbolTable() as UnderscoreEscapedMap); + for (const decl of symbol.declarations) { + const members = getMembersOfDeclaration(decl); + if (members) { + for (const member of members) { + if (isStatic === hasStaticModifier(member) && hasLateBindableName(member)) { + lateBindMember(symbol, earlySymbols, lateSymbols, member); + } } - return emptyArray; } - witnesses.push(/*explicitDefaultStatement*/ undefined); } - return witnesses; - } - - function eachTypeContainedIn(source: Type, types: Type[]) { - return source.flags & TypeFlags.Union ? !forEach((source).types, t => !contains(types, t)) : contains(types, source); - } - - function isTypeSubsetOf(source: Type, target: Type) { - return source === target || target.flags & TypeFlags.Union && isTypeSubsetOfUnion(source, target); - } - - function isTypeSubsetOfUnion(source: Type, target: UnionType) { - if (source.flags & TypeFlags.Union) { - for (const t of (source).types) { - if (!containsType(target.types, t)) { - return false; + const assignments = symbol.assignmentDeclarationMembers; + if (assignments) { + const decls = arrayFrom(assignments.values()); + for (const member of decls) { + const assignmentKind = getAssignmentDeclarationKind((member as BinaryExpression | CallExpression)); + const isInstanceMember = assignmentKind === AssignmentDeclarationKind.PrototypeProperty + || assignmentKind === AssignmentDeclarationKind.ThisProperty + || assignmentKind === AssignmentDeclarationKind.ObjectDefinePrototypeProperty + || assignmentKind === AssignmentDeclarationKind.Prototype; // A straight `Prototype` assignment probably can never have a computed name + if (isStatic === !isInstanceMember && hasLateBindableName(member)) { + lateBindMember(symbol, earlySymbols, lateSymbols, member); } } - return true; - } - if (source.flags & TypeFlags.EnumLiteral && getBaseTypeOfEnumLiteralType(source) === target) { - return true; } - return containsType(target.types, source); + links[resolutionKind] = combineSymbolTables(earlySymbols, lateSymbols) || emptySymbols; } - - function forEachType(type: Type, f: (t: Type) => T | undefined): T | undefined { - return type.flags & TypeFlags.Union ? forEach((type).types, f) : f(type); - } - - function everyType(type: Type, f: (t: Type) => boolean): boolean { - return type.flags & TypeFlags.Union ? every((type).types, f) : f(type); + return links[resolutionKind]!; + } + /** + * Gets a SymbolTable containing both the early- and late-bound members of a symbol. + * + * For a description of late-binding, see `lateBindMember`. + */ + function getMembersOfSymbol(symbol: ts.Symbol) { + return symbol.flags & SymbolFlags.LateBindingContainer + ? getResolvedMembersOrExportsOfSymbol(symbol, MembersOrExportsResolutionKind.resolvedMembers) + : symbol.members || emptySymbols; + } + /** + * If a symbol is the dynamic name of the member of an object type, get the late-bound + * symbol of the member. + * + * For a description of late-binding, see `lateBindMember`. + */ + function getLateBoundSymbol(symbol: ts.Symbol): ts.Symbol { + if (symbol.flags & SymbolFlags.ClassMember && symbol.escapedName === InternalSymbolName.Computed) { + const links = getSymbolLinks(symbol); + if (!links.lateSymbol && some(symbol.declarations, hasLateBindableName)) { + // force late binding of members/exports. This will set the late-bound symbol + const parent = getMergedSymbol(symbol.parent)!; + if (some(symbol.declarations, hasStaticModifier)) { + getExportsOfSymbol(parent); + } + else { + getMembersOfSymbol(parent); + } + } + return links.lateSymbol || (links.lateSymbol = symbol); } - - function filterType(type: Type, f: (t: Type) => boolean): Type { - if (type.flags & TypeFlags.Union) { - const types = (type).types; - const filtered = filter(types, f); - return filtered === types ? type : getUnionTypeFromSortedList(filtered, (type).objectFlags); + return symbol; + } + function getTypeWithThisArgument(type: ts.Type, thisArgument?: ts.Type, needApparentType?: boolean): ts.Type { + if (getObjectFlags(type) & ObjectFlags.Reference) { + const target = (type).target; + const typeArguments = getTypeArguments((type)); + if (length(target.typeParameters) === length(typeArguments)) { + const ref = createTypeReference(target, concatenate(typeArguments, [thisArgument || target.thisType!])); + return needApparentType ? getApparentType(ref) : ref; } - return f(type) ? type : neverType; } - - function countTypes(type: Type) { - return type.flags & TypeFlags.Union ? (type as UnionType).types.length : 1; + else if (type.flags & TypeFlags.Intersection) { + return getIntersectionType(map((type).types, t => getTypeWithThisArgument(t, thisArgument, needApparentType))); } - - // Apply a mapping function to a type and return the resulting type. If the source type - // is a union type, the mapping function is applied to each constituent type and a union - // of the resulting types is returned. - function mapType(type: Type, mapper: (t: Type) => Type, noReductions?: boolean): Type; - function mapType(type: Type, mapper: (t: Type) => Type | undefined, noReductions?: boolean): Type | undefined; - function mapType(type: Type, mapper: (t: Type) => Type | undefined, noReductions?: boolean): Type | undefined { - if (type.flags & TypeFlags.Never) { - return type; - } - if (!(type.flags & TypeFlags.Union)) { - return mapper(type); + return needApparentType ? getApparentType(type) : type; + } + function resolveObjectTypeMembers(type: ObjectType, source: InterfaceTypeWithDeclaredMembers, typeParameters: readonly TypeParameter[], typeArguments: readonly ts.Type[]) { + let mapper: TypeMapper; + let members: SymbolTable; + let callSignatures: readonly ts.Signature[]; + let constructSignatures: readonly ts.Signature[] | undefined; + let stringIndexInfo: IndexInfo | undefined; + let numberIndexInfo: IndexInfo | undefined; + if (rangeEquals(typeParameters, typeArguments, 0, typeParameters.length)) { + mapper = identityMapper; + members = source.symbol ? getMembersOfSymbol(source.symbol) : createSymbolTable(source.declaredProperties); + callSignatures = source.declaredCallSignatures; + constructSignatures = source.declaredConstructSignatures; + stringIndexInfo = source.declaredStringIndexInfo; + numberIndexInfo = source.declaredNumberIndexInfo; + } + else { + mapper = createTypeMapper(typeParameters, typeArguments); + members = createInstantiatedSymbolTable(source.declaredProperties, mapper, /*mappingThisOnly*/ typeParameters.length === 1); + callSignatures = instantiateSignatures(source.declaredCallSignatures, mapper); + constructSignatures = instantiateSignatures(source.declaredConstructSignatures, mapper); + stringIndexInfo = instantiateIndexInfo(source.declaredStringIndexInfo, mapper); + numberIndexInfo = instantiateIndexInfo(source.declaredNumberIndexInfo, mapper); + } + const baseTypes = getBaseTypes(source); + if (baseTypes.length) { + if (source.symbol && members === getMembersOfSymbol(source.symbol)) { + members = createSymbolTable(source.declaredProperties); } - let mappedTypes: Type[] | undefined; - for (const t of (type).types) { - const mapped = mapper(t); - if (mapped) { - if (!mappedTypes) { - mappedTypes = [mapped]; - } - else { - mappedTypes.push(mapped); - } + setStructuredTypeMembers(type, members, callSignatures, constructSignatures, stringIndexInfo, numberIndexInfo); + const thisArgument = lastOrUndefined(typeArguments); + for (const baseType of baseTypes) { + const instantiatedBaseType = thisArgument ? getTypeWithThisArgument(instantiateType(baseType, mapper), thisArgument) : baseType; + addInheritedMembers(members, getPropertiesOfType(instantiatedBaseType)); + callSignatures = concatenate(callSignatures, getSignaturesOfType(instantiatedBaseType, SignatureKind.Call)); + constructSignatures = concatenate(constructSignatures, getSignaturesOfType(instantiatedBaseType, SignatureKind.Construct)); + if (!stringIndexInfo) { + stringIndexInfo = instantiatedBaseType === anyType ? + createIndexInfo(anyType, /*isReadonly*/ false) : + getIndexInfoOfType(instantiatedBaseType, IndexKind.String); } + numberIndexInfo = numberIndexInfo || getIndexInfoOfType(instantiatedBaseType, IndexKind.Number); } - return mappedTypes && getUnionType(mappedTypes, noReductions ? UnionReduction.None : UnionReduction.Literal); - } - - function extractTypesOfKind(type: Type, kind: TypeFlags) { - return filterType(type, t => (t.flags & kind) !== 0); - } - - // Return a new type in which occurrences of the string and number primitive types in - // typeWithPrimitives have been replaced with occurrences of string literals and numeric - // literals in typeWithLiterals, respectively. - function replacePrimitivesWithLiterals(typeWithPrimitives: Type, typeWithLiterals: Type) { - if (isTypeSubsetOf(stringType, typeWithPrimitives) && maybeTypeOfKind(typeWithLiterals, TypeFlags.StringLiteral) || - isTypeSubsetOf(numberType, typeWithPrimitives) && maybeTypeOfKind(typeWithLiterals, TypeFlags.NumberLiteral) || - isTypeSubsetOf(bigintType, typeWithPrimitives) && maybeTypeOfKind(typeWithLiterals, TypeFlags.BigIntLiteral)) { - return mapType(typeWithPrimitives, t => - t.flags & TypeFlags.String ? extractTypesOfKind(typeWithLiterals, TypeFlags.String | TypeFlags.StringLiteral) : - t.flags & TypeFlags.Number ? extractTypesOfKind(typeWithLiterals, TypeFlags.Number | TypeFlags.NumberLiteral) : - t.flags & TypeFlags.BigInt ? extractTypesOfKind(typeWithLiterals, TypeFlags.BigInt | TypeFlags.BigIntLiteral) : t); - } - return typeWithPrimitives; - } - - function isIncomplete(flowType: FlowType) { - return flowType.flags === 0; - } - - function getTypeFromFlowType(flowType: FlowType) { - return flowType.flags === 0 ? (flowType).type : flowType; - } - - function createFlowType(type: Type, incomplete: boolean): FlowType { - return incomplete ? { flags: 0, type } : type; - } - - // An evolving array type tracks the element types that have so far been seen in an - // 'x.push(value)' or 'x[n] = value' operation along the control flow graph. Evolving - // array types are ultimately converted into manifest array types (using getFinalArrayType) - // and never escape the getFlowTypeOfReference function. - function createEvolvingArrayType(elementType: Type): EvolvingArrayType { - const result = createObjectType(ObjectFlags.EvolvingArray); - result.elementType = elementType; - return result; } - - function getEvolvingArrayType(elementType: Type): EvolvingArrayType { - return evolvingArrayTypes[elementType.id] || (evolvingArrayTypes[elementType.id] = createEvolvingArrayType(elementType)); + setStructuredTypeMembers(type, members, callSignatures, constructSignatures, stringIndexInfo, numberIndexInfo); + } + function resolveClassOrInterfaceMembers(type: InterfaceType): void { + resolveObjectTypeMembers(type, resolveDeclaredMembers(type), emptyArray, emptyArray); + } + function resolveTypeReferenceMembers(type: TypeReference): void { + const source = resolveDeclaredMembers(type.target); + const typeParameters = concatenate((source.typeParameters!), [source.thisType!]); + const typeArguments = getTypeArguments(type); + const paddedTypeArguments = typeArguments.length === typeParameters.length ? typeArguments : concatenate(typeArguments, [type]); + resolveObjectTypeMembers(type, source, typeParameters, paddedTypeArguments); + } + function createSignature(declaration: SignatureDeclaration | JSDocSignature | undefined, typeParameters: readonly TypeParameter[] | undefined, thisParameter: ts.Symbol | undefined, parameters: readonly ts.Symbol[], resolvedReturnType: ts.Type | undefined, resolvedTypePredicate: TypePredicate | undefined, minArgumentCount: number, flags: SignatureFlags): ts.Signature { + const sig = new Signature(checker, flags); + sig.declaration = declaration; + sig.typeParameters = typeParameters; + sig.parameters = parameters; + sig.thisParameter = thisParameter; + sig.resolvedReturnType = resolvedReturnType; + sig.resolvedTypePredicate = resolvedTypePredicate; + sig.minArgumentCount = minArgumentCount; + sig.target = undefined; + sig.mapper = undefined; + sig.unionSignatures = undefined; + return sig; + } + function cloneSignature(sig: ts.Signature): ts.Signature { + const result = createSignature(sig.declaration, sig.typeParameters, sig.thisParameter, sig.parameters, /*resolvedReturnType*/ undefined, + /*resolvedTypePredicate*/ undefined, sig.minArgumentCount, sig.flags & SignatureFlags.PropagatingFlags); + result.target = sig.target; + result.mapper = sig.mapper; + result.unionSignatures = sig.unionSignatures; + return result; + } + function createUnionSignature(signature: ts.Signature, unionSignatures: ts.Signature[]) { + const result = cloneSignature(signature); + result.unionSignatures = unionSignatures; + result.target = undefined; + result.mapper = undefined; + return result; + } + function getOptionalCallSignature(signature: ts.Signature, callChainFlags: SignatureFlags): ts.Signature { + if ((signature.flags & SignatureFlags.CallChainFlags) === callChainFlags) { + return signature; } - - // When adding evolving array element types we do not perform subtype reduction. Instead, - // we defer subtype reduction until the evolving array type is finalized into a manifest - // array type. - function addEvolvingArrayElementType(evolvingArrayType: EvolvingArrayType, node: Expression): EvolvingArrayType { - const elementType = getBaseTypeOfLiteralType(getContextFreeTypeOfExpression(node)); - return isTypeSubsetOf(elementType, evolvingArrayType.elementType) ? evolvingArrayType : getEvolvingArrayType(getUnionType([evolvingArrayType.elementType, elementType])); + if (!signature.optionalCallSignatureCache) { + signature.optionalCallSignatureCache = {}; } - - function createFinalArrayType(elementType: Type) { - return elementType.flags & TypeFlags.Never ? - autoArrayType : - createArrayType(elementType.flags & TypeFlags.Union ? - getUnionType((elementType).types, UnionReduction.Subtype) : - elementType); + const key = callChainFlags === SignatureFlags.IsInnerCallChain ? "inner" : "outer"; + return signature.optionalCallSignatureCache[key] + || (signature.optionalCallSignatureCache[key] = createOptionalCallSignature(signature, callChainFlags)); + } + function createOptionalCallSignature(signature: ts.Signature, callChainFlags: SignatureFlags) { + Debug.assert(callChainFlags === SignatureFlags.IsInnerCallChain || callChainFlags === SignatureFlags.IsOuterCallChain, "An optional call signature can either be for an inner call chain or an outer call chain, but not both."); + const result = cloneSignature(signature); + result.flags |= callChainFlags; + return result; + } + function getExpandedParameters(sig: ts.Signature): readonly ts.Symbol[] { + if (signatureHasRestParameter(sig)) { + const restIndex = sig.parameters.length - 1; + const restParameter = sig.parameters[restIndex]; + const restType = getTypeOfSymbol(restParameter); + if (isTupleType(restType)) { + const elementTypes = getTypeArguments(restType); + const minLength = restType.target.minLength; + const tupleRestIndex = restType.target.hasRestElement ? elementTypes.length - 1 : -1; + const restParams = map(elementTypes, (t, i) => { + const name = getParameterNameAtPosition(sig, restIndex + i); + const checkFlags = i === tupleRestIndex ? CheckFlags.RestParameter : + i >= minLength ? CheckFlags.OptionalParameter : 0; + const symbol = createSymbol(SymbolFlags.FunctionScopedVariable, name, checkFlags); + symbol.type = i === tupleRestIndex ? createArrayType(t) : t; + return symbol; + }); + return concatenate(sig.parameters.slice(0, restIndex), restParams); + } } - - // We perform subtype reduction upon obtaining the final array type from an evolving array type. - function getFinalArrayType(evolvingArrayType: EvolvingArrayType): Type { - return evolvingArrayType.finalArrayType || (evolvingArrayType.finalArrayType = createFinalArrayType(evolvingArrayType.elementType)); + return sig.parameters; + } + function getDefaultConstructSignatures(classType: InterfaceType): ts.Signature[] { + const baseConstructorType = getBaseConstructorTypeOfClass(classType); + const baseSignatures = getSignaturesOfType(baseConstructorType, SignatureKind.Construct); + if (baseSignatures.length === 0) { + return [createSignature(undefined, classType.localTypeParameters, undefined, emptyArray, classType, /*resolvedTypePredicate*/ undefined, 0, SignatureFlags.None)]; + } + const baseTypeNode = getBaseTypeNodeOfClass(classType)!; + const isJavaScript = isInJSFile(baseTypeNode); + const typeArguments = typeArgumentsFromTypeReferenceNode(baseTypeNode); + const typeArgCount = length(typeArguments); + const result: ts.Signature[] = []; + for (const baseSig of baseSignatures) { + const minTypeArgumentCount = getMinTypeArgumentCount(baseSig.typeParameters); + const typeParamCount = length(baseSig.typeParameters); + if (isJavaScript || typeArgCount >= minTypeArgumentCount && typeArgCount <= typeParamCount) { + const sig = typeParamCount ? createSignatureInstantiation(baseSig, fillMissingTypeArguments(typeArguments, baseSig.typeParameters, minTypeArgumentCount, isJavaScript)) : cloneSignature(baseSig); + sig.typeParameters = classType.localTypeParameters; + sig.resolvedReturnType = classType; + result.push(sig); + } + } + return result; + } + function findMatchingSignature(signatureList: readonly ts.Signature[], signature: ts.Signature, partialMatch: boolean, ignoreThisTypes: boolean, ignoreReturnTypes: boolean): ts.Signature | undefined { + for (const s of signatureList) { + if (compareSignaturesIdentical(s, signature, partialMatch, ignoreThisTypes, ignoreReturnTypes, partialMatch ? compareTypesSubtypeOf : compareTypesIdentical)) { + return s; + } } - - function finalizeEvolvingArrayType(type: Type): Type { - return getObjectFlags(type) & ObjectFlags.EvolvingArray ? getFinalArrayType(type) : type; + } + function findMatchingSignatures(signatureLists: readonly (readonly ts.Signature[])[], signature: ts.Signature, listIndex: number): ts.Signature[] | undefined { + if (signature.typeParameters) { + // We require an exact match for generic signatures, so we only return signatures from the first + // signature list and only if they have exact matches in the other signature lists. + if (listIndex > 0) { + return undefined; + } + for (let i = 1; i < signatureLists.length; i++) { + if (!findMatchingSignature(signatureLists[i], signature, /*partialMatch*/ false, /*ignoreThisTypes*/ false, /*ignoreReturnTypes*/ false)) { + return undefined; + } + } + return [signature]; } - - function getElementTypeOfEvolvingArrayType(type: Type) { - return getObjectFlags(type) & ObjectFlags.EvolvingArray ? (type).elementType : neverType; + let result: ts.Signature[] | undefined; + for (let i = 0; i < signatureLists.length; i++) { + // Allow matching non-generic signatures to have excess parameters and different return types. + // Prefer matching this types if possible. + const match = i === listIndex ? signature : findMatchingSignature(signatureLists[i], signature, /*partialMatch*/ true, /*ignoreThisTypes*/ false, /*ignoreReturnTypes*/ true); + if (!match) { + return undefined; + } + result = appendIfUnique(result, match); } - - function isEvolvingArrayTypeList(types: Type[]) { - let hasEvolvingArrayType = false; - for (const t of types) { - if (!(t.flags & TypeFlags.Never)) { - if (!(getObjectFlags(t) & ObjectFlags.EvolvingArray)) { - return false; + return result; + } + // The signatures of a union type are those signatures that are present in each of the constituent types. + // Generic signatures must match exactly, but non-generic signatures are allowed to have extra optional + // parameters and may differ in return types. When signatures differ in return types, the resulting return + // type is the union of the constituent return types. + function getUnionSignatures(signatureLists: readonly (readonly ts.Signature[])[]): ts.Signature[] { + let result: ts.Signature[] | undefined; + let indexWithLengthOverOne: number | undefined; + for (let i = 0; i < signatureLists.length; i++) { + if (signatureLists[i].length === 0) + return emptyArray; + if (signatureLists[i].length > 1) { + indexWithLengthOverOne = indexWithLengthOverOne === undefined ? i : -1; // -1 is a signal there are multiple overload sets + } + for (const signature of signatureLists[i]) { + // Only process signatures with parameter lists that aren't already in the result list + if (!result || !findMatchingSignature(result, signature, /*partialMatch*/ false, /*ignoreThisTypes*/ false, /*ignoreReturnTypes*/ true)) { + const unionSignatures = findMatchingSignatures(signatureLists, signature, i); + if (unionSignatures) { + let s = signature; + // Union the result types when more than one signature matches + if (unionSignatures.length > 1) { + let thisParameter = signature.thisParameter; + const firstThisParameterOfUnionSignatures = forEach(unionSignatures, sig => sig.thisParameter); + if (firstThisParameterOfUnionSignatures) { + const thisType = getIntersectionType(mapDefined(unionSignatures, sig => sig.thisParameter && getTypeOfSymbol(sig.thisParameter))); + thisParameter = createSymbolWithType(firstThisParameterOfUnionSignatures, thisType); + } + s = createUnionSignature(signature, unionSignatures); + s.thisParameter = thisParameter; + } + (result || (result = [])).push(s); } - hasEvolvingArrayType = true; } } - return hasEvolvingArrayType; } - - // At flow control branch or loop junctions, if the type along every antecedent code path - // is an evolving array type, we construct a combined evolving array type. Otherwise we - // finalize all evolving array types. - function getUnionOrEvolvingArrayType(types: Type[], subtypeReduction: UnionReduction) { - return isEvolvingArrayTypeList(types) ? - getEvolvingArrayType(getUnionType(map(types, getElementTypeOfEvolvingArrayType))) : - getUnionType(sameMap(types, finalizeEvolvingArrayType), subtypeReduction); + if (!length(result) && indexWithLengthOverOne !== -1) { + // No sufficiently similar signature existed to subsume all the other signatures in the union - time to see if we can make a single + // signature that handles all over them. We only do this when there are overloads in only one constituent. + // (Overloads are conditional in nature and having overloads in multiple constituents would necessitate making a power set of + // signatures from the type, whose ordering would be non-obvious) + const masterList = signatureLists[indexWithLengthOverOne !== undefined ? indexWithLengthOverOne : 0]; + let results: ts.Signature[] | undefined = masterList.slice(); + for (const signatures of signatureLists) { + if (signatures !== masterList) { + const signature = signatures[0]; + Debug.assert(!!signature, "getUnionSignatures bails early on empty signature lists and should not have empty lists on second pass"); + results = signature.typeParameters && some(results, s => !!s.typeParameters) ? undefined : map(results, sig => combineSignaturesOfUnionMembers(sig, signature)); + if (!results) { + break; + } + } + } + result = results; } - - // Return true if the given node is 'x' in an 'x.length', x.push(value)', 'x.unshift(value)' or - // 'x[n] = value' operation, where 'n' is an expression of type any, undefined, or a number-like type. - function isEvolvingArrayOperationTarget(node: Node) { - const root = getReferenceRoot(node); - const parent = root.parent; - const isLengthPushOrUnshift = isPropertyAccessExpression(parent) && ( - parent.name.escapedText === "length" || - parent.parent.kind === SyntaxKind.CallExpression - && isIdentifier(parent.name) - && isPushOrUnshiftIdentifier(parent.name)); - const isElementAssignment = parent.kind === SyntaxKind.ElementAccessExpression && - (parent).expression === root && - parent.parent.kind === SyntaxKind.BinaryExpression && - (parent.parent).operatorToken.kind === SyntaxKind.EqualsToken && - (parent.parent).left === parent && - !isAssignmentTarget(parent.parent) && - isTypeAssignableToKind(getTypeOfExpression((parent).argumentExpression), TypeFlags.NumberLike); - return isLengthPushOrUnshift || isElementAssignment; + return result || emptyArray; + } + function combineUnionThisParam(left: ts.Symbol | undefined, right: ts.Symbol | undefined): ts.Symbol | undefined { + if (!left || !right) { + return left || right; + } + // A signature `this` type might be a read or a write position... It's very possible that it should be invariant + // and we should refuse to merge signatures if there are `this` types and they do not match. However, so as to be + // permissive when calling, for now, we'll intersect the `this` types just like we do for param types in union signatures. + const thisType = getIntersectionType([getTypeOfSymbol(left), getTypeOfSymbol(right)]); + return createSymbolWithType(left, thisType); + } + function combineUnionParameters(left: ts.Signature, right: ts.Signature) { + const leftCount = getParameterCount(left); + const rightCount = getParameterCount(right); + const longest = leftCount >= rightCount ? left : right; + const shorter = longest === left ? right : left; + const longestCount = longest === left ? leftCount : rightCount; + const eitherHasEffectiveRest = (hasEffectiveRestParameter(left) || hasEffectiveRestParameter(right)); + const needsExtraRestElement = eitherHasEffectiveRest && !hasEffectiveRestParameter(longest); + const params = new Array(longestCount + (needsExtraRestElement ? 1 : 0)); + for (let i = 0; i < longestCount; i++) { + const longestParamType = tryGetTypeAtPosition(longest, i)!; + const shorterParamType = tryGetTypeAtPosition(shorter, i) || unknownType; + const unionParamType = getIntersectionType([longestParamType, shorterParamType]); + const isRestParam = eitherHasEffectiveRest && !needsExtraRestElement && i === (longestCount - 1); + const isOptional = i >= getMinArgumentCount(longest) && i >= getMinArgumentCount(shorter); + const leftName = i >= leftCount ? undefined : getParameterNameAtPosition(left, i); + const rightName = i >= rightCount ? undefined : getParameterNameAtPosition(right, i); + const paramName = leftName === rightName ? leftName : + !leftName ? rightName : + !rightName ? leftName : + undefined; + const paramSymbol = createSymbol(SymbolFlags.FunctionScopedVariable | (isOptional && !isRestParam ? SymbolFlags.Optional : 0), paramName || (`arg${i}` as __String)); + paramSymbol.type = isRestParam ? createArrayType(unionParamType) : unionParamType; + params[i] = paramSymbol; } - - function isDeclarationWithExplicitTypeAnnotation(declaration: Declaration | undefined) { - return !!(declaration && ( - declaration.kind === SyntaxKind.VariableDeclaration || declaration.kind === SyntaxKind.Parameter || - declaration.kind === SyntaxKind.PropertyDeclaration || declaration.kind === SyntaxKind.PropertySignature) && - getEffectiveTypeAnnotationNode(declaration as VariableDeclaration | ParameterDeclaration | PropertyDeclaration | PropertySignature)); + if (needsExtraRestElement) { + const restParamSymbol = createSymbol(SymbolFlags.FunctionScopedVariable, ("args" as __String)); + restParamSymbol.type = createArrayType(getTypeAtPosition(shorter, longestCount)); + params[longestCount] = restParamSymbol; } - - function getExplicitTypeOfSymbol(symbol: Symbol, diagnostic?: Diagnostic) { - if (symbol.flags & (SymbolFlags.Function | SymbolFlags.Method | SymbolFlags.Class | SymbolFlags.ValueModule)) { - return getTypeOfSymbol(symbol); - } - if (symbol.flags & (SymbolFlags.Variable | SymbolFlags.Property)) { - if (isDeclarationWithExplicitTypeAnnotation(symbol.valueDeclaration)) { - return getTypeOfSymbol(symbol); - } - if (diagnostic && symbol.valueDeclaration) { - addRelatedInfo(diagnostic, createDiagnosticForNode(symbol.valueDeclaration, Diagnostics._0_needs_an_explicit_type_annotation, symbolToString(symbol))); - } + return params; + } + function combineSignaturesOfUnionMembers(left: ts.Signature, right: ts.Signature): ts.Signature { + const declaration = left.declaration; + const params = combineUnionParameters(left, right); + const thisParam = combineUnionThisParam(left.thisParameter, right.thisParameter); + const minArgCount = Math.max(left.minArgumentCount, right.minArgumentCount); + const result = createSignature(declaration, left.typeParameters || right.typeParameters, thisParam, params, + /*resolvedReturnType*/ undefined, + /*resolvedTypePredicate*/ undefined, minArgCount, (left.flags | right.flags) & SignatureFlags.PropagatingFlags); + result.unionSignatures = concatenate(left.unionSignatures || [left], [right]); + return result; + } + function getUnionIndexInfo(types: readonly ts.Type[], kind: IndexKind): IndexInfo | undefined { + const indexTypes: ts.Type[] = []; + let isAnyReadonly = false; + for (const type of types) { + const indexInfo = getIndexInfoOfType(type, kind); + if (!indexInfo) { + return undefined; } + indexTypes.push(indexInfo.type); + isAnyReadonly = isAnyReadonly || indexInfo.isReadonly; } - - // We require the dotted function name in an assertion expression to be comprised of identifiers - // that reference function, method, class or value module symbols; or variable, property or - // parameter symbols with declarations that have explicit type annotations. Such references are - // resolvable with no possibility of triggering circularities in control flow analysis. - function getTypeOfDottedName(node: Expression, diagnostic: Diagnostic | undefined): Type | undefined { - if (!(node.flags & NodeFlags.InWithStatement)) { - switch (node.kind) { - case SyntaxKind.Identifier: - const symbol = getExportSymbolOfValueSymbolIfExported(getResolvedSymbol(node)); - return getExplicitTypeOfSymbol(symbol.flags & SymbolFlags.Alias ? resolveAlias(symbol) : symbol, diagnostic); - case SyntaxKind.ThisKeyword: - return getExplicitThisType(node); - case SyntaxKind.SuperKeyword: - return checkSuperExpression(node); - case SyntaxKind.PropertyAccessExpression: - const type = getTypeOfDottedName((node).expression, diagnostic); - const prop = type && getPropertyOfType(type, (node).name.escapedText); - return prop && getExplicitTypeOfSymbol(prop, diagnostic); - case SyntaxKind.ParenthesizedExpression: - return getTypeOfDottedName((node).expression, diagnostic); - } + return createIndexInfo(getUnionType(indexTypes, UnionReduction.Subtype), isAnyReadonly); + } + function resolveUnionTypeMembers(type: UnionType) { + // The members and properties collections are empty for union types. To get all properties of a union + // type use getPropertiesOfType (only the language service uses this). + const callSignatures = getUnionSignatures(map(type.types, t => t === globalFunctionType ? [unknownSignature] : getSignaturesOfType(t, SignatureKind.Call))); + const constructSignatures = getUnionSignatures(map(type.types, t => getSignaturesOfType(t, SignatureKind.Construct))); + const stringIndexInfo = getUnionIndexInfo(type.types, IndexKind.String); + const numberIndexInfo = getUnionIndexInfo(type.types, IndexKind.Number); + setStructuredTypeMembers(type, emptySymbols, callSignatures, constructSignatures, stringIndexInfo, numberIndexInfo); + } + function intersectTypes(type1: ts.Type, type2: ts.Type): ts.Type; + function intersectTypes(type1: ts.Type | undefined, type2: ts.Type | undefined): ts.Type | undefined; + function intersectTypes(type1: ts.Type | undefined, type2: ts.Type | undefined): ts.Type | undefined { + return !type1 ? type2 : !type2 ? type1 : getIntersectionType([type1, type2]); + } + function intersectIndexInfos(info1: IndexInfo | undefined, info2: IndexInfo | undefined): IndexInfo | undefined { + return !info1 ? info2 : !info2 ? info1 : createIndexInfo(getIntersectionType([info1.type, info2.type]), info1.isReadonly && info2.isReadonly); + } + function unionSpreadIndexInfos(info1: IndexInfo | undefined, info2: IndexInfo | undefined): IndexInfo | undefined { + return info1 && info2 && createIndexInfo(getUnionType([info1.type, info2.type]), info1.isReadonly || info2.isReadonly); + } + function findMixins(types: readonly ts.Type[]): readonly boolean[] { + const constructorTypeCount = countWhere(types, (t) => getSignaturesOfType(t, SignatureKind.Construct).length > 0); + const mixinFlags = map(types, isMixinConstructorType); + if (constructorTypeCount > 0 && constructorTypeCount === countWhere(mixinFlags, (b) => b)) { + const firstMixinIndex = mixinFlags.indexOf(/*searchElement*/ true); + mixinFlags[firstMixinIndex] = false; + } + return mixinFlags; + } + function includeMixinType(type: ts.Type, types: readonly ts.Type[], mixinFlags: readonly boolean[], index: number): ts.Type { + const mixedTypes: ts.Type[] = []; + for (let i = 0; i < types.length; i++) { + if (i === index) { + mixedTypes.push(type); + } + else if (mixinFlags[i]) { + mixedTypes.push(getReturnTypeOfSignature(getSignaturesOfType(types[i], SignatureKind.Construct)[0])); } } - - function getEffectsSignature(node: CallExpression) { - const links = getNodeLinks(node); - let signature = links.effectsSignature; - if (signature === undefined) { - // A call expression parented by an expression statement is a potential assertion. Other call - // expressions are potential type predicate function calls. In order to avoid triggering - // circularities in control flow analysis, we use getTypeOfDottedName when resolving the call - // target expression of an assertion. - let funcType: Type | undefined; - if (node.parent.kind === SyntaxKind.ExpressionStatement) { - funcType = getTypeOfDottedName(node.expression, /*diagnostic*/ undefined); - } - else if (node.expression.kind !== SyntaxKind.SuperKeyword) { - if (isOptionalChain(node)) { - funcType = checkNonNullType( - getOptionalExpressionType(checkExpression(node.expression), node.expression), - node.expression - ); - } - else { - funcType = checkNonNullExpression(node.expression); - } + return getIntersectionType(mixedTypes); + } + function resolveIntersectionTypeMembers(type: IntersectionType) { + // The members and properties collections are empty for intersection types. To get all properties of an + // intersection type use getPropertiesOfType (only the language service uses this). + let callSignatures: ts.Signature[] | undefined; + let constructSignatures: ts.Signature[] | undefined; + let stringIndexInfo: IndexInfo | undefined; + let numberIndexInfo: IndexInfo | undefined; + const types = type.types; + const mixinFlags = findMixins(types); + const mixinCount = countWhere(mixinFlags, (b) => b); + for (let i = 0; i < types.length; i++) { + const t = type.types[i]; + // When an intersection type contains mixin constructor types, the construct signatures from + // those types are discarded and their return types are mixed into the return types of all + // other construct signatures in the intersection type. For example, the intersection type + // '{ new(...args: any[]) => A } & { new(s: string) => B }' has a single construct signature + // 'new(s: string) => A & B'. + if (!mixinFlags[i]) { + let signatures = getSignaturesOfType(t, SignatureKind.Construct); + if (signatures.length && mixinCount > 0) { + signatures = map(signatures, s => { + const clone = cloneSignature(s); + clone.resolvedReturnType = includeMixinType(getReturnTypeOfSignature(s), types, mixinFlags, i); + return clone; + }); } - const signatures = getSignaturesOfType(funcType && getApparentType(funcType) || unknownType, SignatureKind.Call); - const candidate = signatures.length === 1 && !signatures[0].typeParameters ? signatures[0] : - some(signatures, hasTypePredicateOrNeverReturnType) ? getResolvedSignature(node) : - undefined; - signature = links.effectsSignature = candidate && hasTypePredicateOrNeverReturnType(candidate) ? candidate : unknownSignature; + constructSignatures = appendSignatures(constructSignatures, signatures); } - return signature === unknownSignature ? undefined : signature; - } - - function hasTypePredicateOrNeverReturnType(signature: Signature) { - return !!(getTypePredicateOfSignature(signature) || - signature.declaration && (getReturnTypeFromAnnotation(signature.declaration) || unknownType).flags & TypeFlags.Never); + callSignatures = appendSignatures(callSignatures, getSignaturesOfType(t, SignatureKind.Call)); + stringIndexInfo = intersectIndexInfos(stringIndexInfo, getIndexInfoOfType(t, IndexKind.String)); + numberIndexInfo = intersectIndexInfos(numberIndexInfo, getIndexInfoOfType(t, IndexKind.Number)); } - - function getTypePredicateArgument(predicate: TypePredicate, callExpression: CallExpression) { - if (predicate.kind === TypePredicateKind.Identifier || predicate.kind === TypePredicateKind.AssertsIdentifier) { - return callExpression.arguments[predicate.parameterIndex]; + setStructuredTypeMembers(type, emptySymbols, callSignatures || emptyArray, constructSignatures || emptyArray, stringIndexInfo, numberIndexInfo); + } + function appendSignatures(signatures: ts.Signature[] | undefined, newSignatures: readonly ts.Signature[]) { + for (const sig of newSignatures) { + if (!signatures || every(signatures, s => !compareSignaturesIdentical(s, sig, /*partialMatch*/ false, /*ignoreThisTypes*/ false, /*ignoreReturnTypes*/ false, compareTypesIdentical))) { + signatures = append(signatures, sig); } - const invokedExpression = skipParentheses(callExpression.expression); - return isAccessExpression(invokedExpression) ? skipParentheses(invokedExpression.expression) : undefined; - } - - function reportFlowControlError(node: Node) { - const block = findAncestor(node, isFunctionOrModuleBlock); - const sourceFile = getSourceFileOfNode(node); - const span = getSpanOfTokenAtPosition(sourceFile, block.statements.pos); - diagnostics.add(createFileDiagnostic(sourceFile, span.start, span.length, Diagnostics.The_containing_function_or_module_body_is_too_large_for_control_flow_analysis)); } - - function isReachableFlowNode(flow: FlowNode) { - const result = isReachableFlowNodeWorker(flow, /*noCacheCheck*/ false); - lastFlowNode = flow; - lastFlowNodeReachable = result; - return result; + return signatures; + } + /** + * Converts an AnonymousType to a ResolvedType. + */ + function resolveAnonymousTypeMembers(type: AnonymousType) { + const symbol = getMergedSymbol(type.symbol); + if (type.target) { + setStructuredTypeMembers(type, emptySymbols, emptyArray, emptyArray, undefined, undefined); + const members = createInstantiatedSymbolTable(getPropertiesOfObjectType(type.target), type.mapper!, /*mappingThisOnly*/ false); + const callSignatures = instantiateSignatures(getSignaturesOfType(type.target, SignatureKind.Call), (type.mapper!)); + const constructSignatures = instantiateSignatures(getSignaturesOfType(type.target, SignatureKind.Construct), (type.mapper!)); + const stringIndexInfo = instantiateIndexInfo(getIndexInfoOfType(type.target, IndexKind.String), (type.mapper!)); + const numberIndexInfo = instantiateIndexInfo(getIndexInfoOfType(type.target, IndexKind.Number), (type.mapper!)); + setStructuredTypeMembers(type, members, callSignatures, constructSignatures, stringIndexInfo, numberIndexInfo); } - - function isFalseExpression(expr: Expression): boolean { - const node = skipParentheses(expr); - return node.kind === SyntaxKind.FalseKeyword || node.kind === SyntaxKind.BinaryExpression && ( - (node).operatorToken.kind === SyntaxKind.AmpersandAmpersandToken && (isFalseExpression((node).left) || isFalseExpression((node).right)) || - (node).operatorToken.kind === SyntaxKind.BarBarToken && isFalseExpression((node).left) && isFalseExpression((node).right)); + else if (symbol.flags & SymbolFlags.TypeLiteral) { + setStructuredTypeMembers(type, emptySymbols, emptyArray, emptyArray, undefined, undefined); + const members = getMembersOfSymbol(symbol); + const callSignatures = getSignaturesOfSymbol(members.get(InternalSymbolName.Call)); + const constructSignatures = getSignaturesOfSymbol(members.get(InternalSymbolName.New)); + const stringIndexInfo = getIndexInfoOfSymbol(symbol, IndexKind.String); + const numberIndexInfo = getIndexInfoOfSymbol(symbol, IndexKind.Number); + setStructuredTypeMembers(type, members, callSignatures, constructSignatures, stringIndexInfo, numberIndexInfo); } - - function isReachableFlowNodeWorker(flow: FlowNode, noCacheCheck: boolean): boolean { - while (true) { - if (flow === lastFlowNode) { - return lastFlowNodeReachable; - } - const flags = flow.flags; - if (flags & FlowFlags.Shared) { - if (!noCacheCheck) { - const id = getFlowNodeId(flow); - const reachable = flowNodeReachable[id]; - return reachable !== undefined ? reachable : (flowNodeReachable[id] = isReachableFlowNodeWorker(flow, /*noCacheCheck*/ true)); - } - noCacheCheck = false; - } - if (flags & (FlowFlags.Assignment | FlowFlags.Condition | FlowFlags.ArrayMutation)) { - flow = (flow).antecedent; - } - else if (flags & FlowFlags.Call) { - const signature = getEffectsSignature((flow).node); - if (signature) { - const predicate = getTypePredicateOfSignature(signature); - if (predicate && predicate.kind === TypePredicateKind.AssertsIdentifier) { - const predicateArgument = (flow).node.arguments[predicate.parameterIndex]; - if (predicateArgument && isFalseExpression(predicateArgument)) { - return false; - } - } - if (getReturnTypeOfSignature(signature).flags & TypeFlags.Never) { - return false; + else { + // Combinations of function, class, enum and module + let members = emptySymbols; + let stringIndexInfo: IndexInfo | undefined; + if (symbol.exports) { + members = getExportsOfSymbol(symbol); + if (symbol === globalThisSymbol) { + const varsOnly = (createMap() as SymbolTable); + members.forEach(p => { + if (!(p.flags & SymbolFlags.BlockScoped)) { + varsOnly.set(p.escapedName, p); } - } - flow = (flow).antecedent; + }); + members = varsOnly; } - else if (flags & FlowFlags.BranchLabel) { - // A branching point is reachable if any branch is reachable. - return some((flow).antecedents, f => isReachableFlowNodeWorker(f, /*noCacheCheck*/ false)); + } + setStructuredTypeMembers(type, members, emptyArray, emptyArray, undefined, undefined); + if (symbol.flags & SymbolFlags.Class) { + const classType = getDeclaredTypeOfClassOrInterface(symbol); + const baseConstructorType = getBaseConstructorTypeOfClass(classType); + if (baseConstructorType.flags & (TypeFlags.Object | TypeFlags.Intersection | TypeFlags.TypeVariable)) { + members = createSymbolTable(getNamedMembers(members)); + addInheritedMembers(members, getPropertiesOfType(baseConstructorType)); } - else if (flags & FlowFlags.LoopLabel) { - // A loop is reachable if the control flow path that leads to the top is reachable. - flow = (flow).antecedents![0]; + else if (baseConstructorType === anyType) { + stringIndexInfo = createIndexInfo(anyType, /*isReadonly*/ false); } - else if (flags & FlowFlags.SwitchClause) { - // The control flow path representing an unmatched value in a switch statement with - // no default clause is unreachable if the switch statement is exhaustive. - if ((flow).clauseStart === (flow).clauseEnd && isExhaustiveSwitchStatement((flow).switchStatement)) { - return false; - } - flow = (flow).antecedent; + } + const numberIndexInfo = symbol.flags & SymbolFlags.Enum && (getDeclaredTypeOfSymbol(symbol).flags & TypeFlags.Enum || + some(type.properties, prop => !!(getTypeOfSymbol(prop).flags & TypeFlags.NumberLike))) ? enumNumberIndexInfo : undefined; + setStructuredTypeMembers(type, members, emptyArray, emptyArray, stringIndexInfo, numberIndexInfo); + // We resolve the members before computing the signatures because a signature may use + // typeof with a qualified name expression that circularly references the type we are + // in the process of resolving (see issue #6072). The temporarily empty signature list + // will never be observed because a qualified name can't reference signatures. + if (symbol.flags & (SymbolFlags.Function | SymbolFlags.Method)) { + type.callSignatures = getSignaturesOfSymbol(symbol); + } + // And likewise for construct signatures for classes + if (symbol.flags & SymbolFlags.Class) { + const classType = getDeclaredTypeOfClassOrInterface(symbol); + let constructSignatures = symbol.members ? getSignaturesOfSymbol(symbol.members.get(InternalSymbolName.Constructor)) : emptyArray; + if (symbol.flags & SymbolFlags.Function) { + constructSignatures = addRange(constructSignatures.slice(), mapDefined(type.callSignatures, sig => isJSConstructor(sig.declaration) ? + createSignature(sig.declaration, sig.typeParameters, sig.thisParameter, sig.parameters, classType, /*resolvedTypePredicate*/ undefined, sig.minArgumentCount, sig.flags & SignatureFlags.PropagatingFlags) : + undefined)); } - else if (flags & FlowFlags.ReduceLabel) { - // Cache is unreliable once we start adjusting labels - lastFlowNode = undefined; - const target = (flow).target; - const saveAntecedents = target.antecedents; - target.antecedents = (flow).antecedents; - const result = isReachableFlowNodeWorker((flow).antecedent, /*noCacheCheck*/ false); - target.antecedents = saveAntecedents; - return result; + if (!constructSignatures.length) { + constructSignatures = getDefaultConstructSignatures(classType); } - else { - return !(flags & FlowFlags.Unreachable); + type.constructSignatures = constructSignatures; + } + } + } + function resolveReverseMappedTypeMembers(type: ReverseMappedType) { + const indexInfo = getIndexInfoOfType(type.source, IndexKind.String); + const modifiers = getMappedTypeModifiers(type.mappedType); + const readonlyMask = modifiers & MappedTypeModifiers.IncludeReadonly ? false : true; + const optionalMask = modifiers & MappedTypeModifiers.IncludeOptional ? 0 : SymbolFlags.Optional; + const stringIndexInfo = indexInfo && createIndexInfo(inferReverseMappedType(indexInfo.type, type.mappedType, type.constraintType), readonlyMask && indexInfo.isReadonly); + const members = createSymbolTable(); + for (const prop of getPropertiesOfType(type.source)) { + const checkFlags = CheckFlags.ReverseMapped | (readonlyMask && isReadonlySymbol(prop) ? CheckFlags.Readonly : 0); + const inferredProp = (createSymbol(SymbolFlags.Property | prop.flags & optionalMask, prop.escapedName, checkFlags) as ReverseMappedSymbol); + inferredProp.declarations = prop.declarations; + inferredProp.nameType = getSymbolLinks(prop).nameType; + inferredProp.propertyType = getTypeOfSymbol(prop); + inferredProp.mappedType = type.mappedType; + inferredProp.constraintType = type.constraintType; + members.set(prop.escapedName, inferredProp); + } + setStructuredTypeMembers(type, members, emptyArray, emptyArray, stringIndexInfo, undefined); + } + // Return the lower bound of the key type in a mapped type. Intuitively, the lower + // bound includes those keys that are known to always be present, for example because + // because of constraints on type parameters (e.g. 'keyof T' for a constrained T). + function getLowerBoundOfKeyType(type: ts.Type): ts.Type { + if (type.flags & (TypeFlags.Any | TypeFlags.Primitive)) { + return type; + } + if (type.flags & TypeFlags.Index) { + return getIndexType(getApparentType((type).type)); + } + if (type.flags & TypeFlags.Conditional) { + if ((type).root.isDistributive) { + const checkType = (type).checkType; + const constraint = getLowerBoundOfKeyType(checkType); + if (constraint !== checkType) { + const mapper = makeUnaryTypeMapper((type).root.checkType, constraint); + return getConditionalTypeInstantiation((type), combineTypeMappers(mapper, (type).mapper)); } } + return type; } - - function getFlowTypeOfReference(reference: Node, declaredType: Type, initialType = declaredType, flowContainer?: Node, couldBeUninitialized?: boolean) { - let key: string | undefined; - let keySet = false; - let flowDepth = 0; - if (flowAnalysisDisabled) { - return errorType; + if (type.flags & TypeFlags.Union) { + return getUnionType(sameMap((type).types, getLowerBoundOfKeyType)); + } + if (type.flags & TypeFlags.Intersection) { + return getIntersectionType(sameMap((type).types, getLowerBoundOfKeyType)); + } + return neverType; + } + /** Resolve the members of a mapped type { [P in K]: T } */ + function resolveMappedTypeMembers(type: MappedType) { + const members: SymbolTable = createSymbolTable(); + let stringIndexInfo: IndexInfo | undefined; + let numberIndexInfo: IndexInfo | undefined; + // Resolve upfront such that recursive references see an empty object type. + setStructuredTypeMembers(type, emptySymbols, emptyArray, emptyArray, undefined, undefined); + // In { [P in K]: T }, we refer to P as the type parameter type, K as the constraint type, + // and T as the template type. + const typeParameter = getTypeParameterFromMappedType(type); + const constraintType = getConstraintTypeFromMappedType(type); + const templateType = getTemplateTypeFromMappedType((type.target) || type); + const modifiersType = getApparentType(getModifiersTypeFromMappedType(type)); // The 'T' in 'keyof T' + const templateModifiers = getMappedTypeModifiers(type); + const include = keyofStringsOnly ? TypeFlags.StringLiteral : TypeFlags.StringOrNumberLiteralOrUnique; + if (isMappedTypeWithKeyofConstraintDeclaration(type)) { + // We have a { [P in keyof T]: X } + for (const prop of getPropertiesOfType(modifiersType)) { + addMemberForKeyType(getLiteralTypeFromProperty(prop, include)); + } + if (modifiersType.flags & TypeFlags.Any || getIndexInfoOfType(modifiersType, IndexKind.String)) { + addMemberForKeyType(stringType); + } + if (!keyofStringsOnly && getIndexInfoOfType(modifiersType, IndexKind.Number)) { + addMemberForKeyType(numberType); + } + } + else { + forEachType(getLowerBoundOfKeyType(constraintType), addMemberForKeyType); + } + setStructuredTypeMembers(type, members, emptyArray, emptyArray, stringIndexInfo, numberIndexInfo); + function addMemberForKeyType(t: ts.Type) { + // Create a mapper from T to the current iteration type constituent. Then, if the + // mapped type is itself an instantiated type, combine the iteration mapper with the + // instantiation mapper. + const templateMapper = combineTypeMappers(type.mapper, createTypeMapper([typeParameter], [t])); + const propType = instantiateType(templateType, templateMapper); + // If the current iteration type constituent is a string literal type, create a property. + // Otherwise, for type string create a string index signature. + if (isTypeUsableAsPropertyName(t)) { + const propName = getPropertyNameFromType(t); + const modifiersProp = getPropertyOfType(modifiersType, propName); + const isOptional = !!(templateModifiers & MappedTypeModifiers.IncludeOptional || + !(templateModifiers & MappedTypeModifiers.ExcludeOptional) && modifiersProp && modifiersProp.flags & SymbolFlags.Optional); + const isReadonly = !!(templateModifiers & MappedTypeModifiers.IncludeReadonly || + !(templateModifiers & MappedTypeModifiers.ExcludeReadonly) && modifiersProp && isReadonlySymbol(modifiersProp)); + const prop = createSymbol(SymbolFlags.Property | (isOptional ? SymbolFlags.Optional : 0), propName, isReadonly ? CheckFlags.Readonly : 0); + // When creating an optional property in strictNullChecks mode, if 'undefined' isn't assignable to the + // type, we include 'undefined' in the type. Similarly, when creating a non-optional property in strictNullChecks + // mode, if the underlying property is optional we remove 'undefined' from the type. + prop.type = strictNullChecks && isOptional && !maybeTypeOfKind(propType, TypeFlags.Undefined | TypeFlags.Void) ? getOptionalType(propType) : + strictNullChecks && !isOptional && modifiersProp && modifiersProp.flags & SymbolFlags.Optional ? getTypeWithFacts(propType, TypeFacts.NEUndefined) : + propType; + if (modifiersProp) { + prop.syntheticOrigin = modifiersProp; + prop.declarations = modifiersProp.declarations; + } + prop.nameType = t; + members.set(propName, prop); } - if (!reference.flowNode || !couldBeUninitialized && !(declaredType.flags & TypeFlags.Narrowable)) { - return declaredType; + else if (t.flags & (TypeFlags.Any | TypeFlags.String)) { + stringIndexInfo = createIndexInfo(propType, !!(templateModifiers & MappedTypeModifiers.IncludeReadonly)); } - flowInvocationCount++; - const sharedFlowStart = sharedFlowCount; - const evolvedType = getTypeFromFlowType(getTypeAtFlowNode(reference.flowNode)); - sharedFlowCount = sharedFlowStart; - // When the reference is 'x' in an 'x.length', 'x.push(value)', 'x.unshift(value)' or x[n] = value' operation, - // we give type 'any[]' to 'x' instead of using the type determined by control flow analysis such that operations - // on empty arrays are possible without implicit any errors and new element types can be inferred without - // type mismatch errors. - const resultType = getObjectFlags(evolvedType) & ObjectFlags.EvolvingArray && isEvolvingArrayOperationTarget(reference) ? autoArrayType : finalizeEvolvingArrayType(evolvedType); - if (resultType === unreachableNeverType|| reference.parent && reference.parent.kind === SyntaxKind.NonNullExpression && getTypeWithFacts(resultType, TypeFacts.NEUndefinedOrNull).flags & TypeFlags.Never) { - return declaredType; + else if (t.flags & (TypeFlags.Number | TypeFlags.Enum)) { + numberIndexInfo = createIndexInfo(numberIndexInfo ? getUnionType([numberIndexInfo.type, propType]) : propType, !!(templateModifiers & MappedTypeModifiers.IncludeReadonly)); } - return resultType; - - function getOrSetCacheKey() { - if (keySet) { - return key; - } - keySet = true; - return key = getFlowCacheKey(reference, declaredType, initialType, flowContainer); + } + } + function getTypeParameterFromMappedType(type: MappedType) { + return type.typeParameter || + (type.typeParameter = getDeclaredTypeOfTypeParameter(getSymbolOfNode(type.declaration.typeParameter))); + } + function getConstraintTypeFromMappedType(type: MappedType) { + return type.constraintType || + (type.constraintType = getConstraintOfTypeParameter(getTypeParameterFromMappedType(type)) || errorType); + } + function getTemplateTypeFromMappedType(type: MappedType) { + return type.templateType || + (type.templateType = type.declaration.type ? + instantiateType(addOptionality(getTypeFromTypeNode(type.declaration.type), !!(getMappedTypeModifiers(type) & MappedTypeModifiers.IncludeOptional)), type.mapper || identityMapper) : + errorType); + } + function getConstraintDeclarationForMappedType(type: MappedType) { + return getEffectiveConstraintOfTypeParameter(type.declaration.typeParameter); + } + function isMappedTypeWithKeyofConstraintDeclaration(type: MappedType) { + const constraintDeclaration = getConstraintDeclarationForMappedType(type)!; // TODO: GH#18217 + return constraintDeclaration.kind === SyntaxKind.TypeOperator && + (constraintDeclaration).operator === SyntaxKind.KeyOfKeyword; + } + function getModifiersTypeFromMappedType(type: MappedType) { + if (!type.modifiersType) { + if (isMappedTypeWithKeyofConstraintDeclaration(type)) { + // If the constraint declaration is a 'keyof T' node, the modifiers type is T. We check + // AST nodes here because, when T is a non-generic type, the logic below eagerly resolves + // 'keyof T' to a literal union type and we can't recover T from that type. + type.modifiersType = instantiateType(getTypeFromTypeNode((getConstraintDeclarationForMappedType(type)).type), type.mapper || identityMapper); } - - function getTypeAtFlowNode(flow: FlowNode): FlowType { - if (flowDepth === 2000) { - // We have made 2000 recursive invocations. To avoid overflowing the call stack we report an error - // and disable further control flow analysis in the containing function or module body. - flowAnalysisDisabled = true; - reportFlowControlError(reference); - return errorType; - } - flowDepth++; - while (true) { - const flags = flow.flags; - if (flags & FlowFlags.Shared) { - // We cache results of flow type resolution for shared nodes that were previously visited in - // the same getFlowTypeOfReference invocation. A node is considered shared when it is the - // antecedent of more than one node. - for (let i = sharedFlowStart; i < sharedFlowCount; i++) { - if (sharedFlowNodes[i] === flow) { - flowDepth--; - return sharedFlowTypes[i]; - } - } - } - let type: FlowType | undefined; - if (flags & FlowFlags.Assignment) { - type = getTypeAtFlowAssignment(flow); - if (!type) { - flow = (flow).antecedent; - continue; - } - } - else if (flags & FlowFlags.Call) { - type = getTypeAtFlowCall(flow); - if (!type) { - flow = (flow).antecedent; - continue; - } - } - else if (flags & FlowFlags.Condition) { - type = getTypeAtFlowCondition(flow); - } - else if (flags & FlowFlags.SwitchClause) { - type = getTypeAtSwitchClause(flow); - } - else if (flags & FlowFlags.Label) { - if ((flow).antecedents!.length === 1) { - flow = (flow).antecedents![0]; - continue; - } - type = flags & FlowFlags.BranchLabel ? - getTypeAtFlowBranchLabel(flow) : - getTypeAtFlowLoopLabel(flow); - } - else if (flags & FlowFlags.ArrayMutation) { - type = getTypeAtFlowArrayMutation(flow); - if (!type) { - flow = (flow).antecedent; - continue; - } - } - else if (flags & FlowFlags.ReduceLabel) { - const target = (flow).target; - const saveAntecedents = target.antecedents; - target.antecedents = (flow).antecedents; - type = getTypeAtFlowNode((flow).antecedent); - target.antecedents = saveAntecedents; - } - else if (flags & FlowFlags.Start) { - // Check if we should continue with the control flow of the containing function. - const container = (flow).node; - if (container && container !== flowContainer && - reference.kind !== SyntaxKind.PropertyAccessExpression && - reference.kind !== SyntaxKind.ElementAccessExpression && - reference.kind !== SyntaxKind.ThisKeyword) { - flow = container.flowNode!; - continue; - } - // At the top of the flow we have the initial type. - type = initialType; - } - else { - // Unreachable code errors are reported in the binding phase. Here we - // simply return the non-auto declared type to reduce follow-on errors. - type = convertAutoToAny(declaredType); - } - if (flags & FlowFlags.Shared) { - // Record visited node and the associated type in the cache. - sharedFlowNodes[sharedFlowCount] = flow; - sharedFlowTypes[sharedFlowCount] = type; - sharedFlowCount++; - } - flowDepth--; - return type; + else { + // Otherwise, get the declared constraint type, and if the constraint type is a type parameter, + // get the constraint of that type parameter. If the resulting type is an indexed type 'keyof T', + // the modifiers type is T. Otherwise, the modifiers type is unknown. + const declaredType = (getTypeFromMappedTypeNode(type.declaration)); + const constraint = getConstraintTypeFromMappedType(declaredType); + const extendedConstraint = constraint && constraint.flags & TypeFlags.TypeParameter ? getConstraintOfTypeParameter((constraint)) : constraint; + type.modifiersType = extendedConstraint && extendedConstraint.flags & TypeFlags.Index ? instantiateType((extendedConstraint).type, type.mapper || identityMapper) : unknownType; + } + } + return type.modifiersType; + } + function getMappedTypeModifiers(type: MappedType): MappedTypeModifiers { + const declaration = type.declaration; + return (declaration.readonlyToken ? declaration.readonlyToken.kind === SyntaxKind.MinusToken ? MappedTypeModifiers.ExcludeReadonly : MappedTypeModifiers.IncludeReadonly : 0) | + (declaration.questionToken ? declaration.questionToken.kind === SyntaxKind.MinusToken ? MappedTypeModifiers.ExcludeOptional : MappedTypeModifiers.IncludeOptional : 0); + } + function getMappedTypeOptionality(type: MappedType): number { + const modifiers = getMappedTypeModifiers(type); + return modifiers & MappedTypeModifiers.ExcludeOptional ? -1 : modifiers & MappedTypeModifiers.IncludeOptional ? 1 : 0; + } + function getCombinedMappedTypeOptionality(type: MappedType): number { + const optionality = getMappedTypeOptionality(type); + const modifiersType = getModifiersTypeFromMappedType(type); + return optionality || (isGenericMappedType(modifiersType) ? getMappedTypeOptionality(modifiersType) : 0); + } + function isPartialMappedType(type: ts.Type) { + return !!(getObjectFlags(type) & ObjectFlags.Mapped && getMappedTypeModifiers((type)) & MappedTypeModifiers.IncludeOptional); + } + function isGenericMappedType(type: ts.Type): type is MappedType { + return !!(getObjectFlags(type) & ObjectFlags.Mapped) && isGenericIndexType(getConstraintTypeFromMappedType((type))); + } + function resolveStructuredTypeMembers(type: StructuredType): ResolvedType { + if (!(type).members) { + if (type.flags & TypeFlags.Object) { + if ((type).objectFlags & ObjectFlags.Reference) { + resolveTypeReferenceMembers((type)); + } + else if ((type).objectFlags & ObjectFlags.ClassOrInterface) { + resolveClassOrInterfaceMembers((type)); + } + else if ((type).objectFlags & ObjectFlags.ReverseMapped) { + resolveReverseMappedTypeMembers((type as ReverseMappedType)); + } + else if ((type).objectFlags & ObjectFlags.Anonymous) { + resolveAnonymousTypeMembers((type)); + } + else if ((type).objectFlags & ObjectFlags.Mapped) { + resolveMappedTypeMembers((type)); } } - - function getInitialOrAssignedType(flow: FlowAssignment) { - const node = flow.node; - return getConstraintForLocation(node.kind === SyntaxKind.VariableDeclaration || node.kind === SyntaxKind.BindingElement ? - getInitialType(node) : - getAssignedType(node), reference); + else if (type.flags & TypeFlags.Union) { + resolveUnionTypeMembers((type)); } - - function getTypeAtFlowAssignment(flow: FlowAssignment) { - const node = flow.node; - // Assignments only narrow the computed type if the declared type is a union type. Thus, we - // only need to evaluate the assigned type if the declared type is a union type. - if (isMatchingReference(reference, node)) { - if (!isReachableFlowNode(flow)) { - return unreachableNeverType; - } - if (getAssignmentTargetKind(node) === AssignmentKind.Compound) { - const flowType = getTypeAtFlowNode(flow.antecedent); - return createFlowType(getBaseTypeOfLiteralType(getTypeFromFlowType(flowType)), isIncomplete(flowType)); - } - if (declaredType === autoType || declaredType === autoArrayType) { - if (isEmptyArrayAssignment(node)) { - return getEvolvingArrayType(neverType); + else if (type.flags & TypeFlags.Intersection) { + resolveIntersectionTypeMembers((type)); + } + } + return type; + } + /** Return properties of an object type or an empty array for other types */ + function getPropertiesOfObjectType(type: ts.Type): ts.Symbol[] { + if (type.flags & TypeFlags.Object) { + return resolveStructuredTypeMembers((type)).properties; + } + return emptyArray; + } + /** If the given type is an object type and that type has a property by the given name, + * return the symbol for that property. Otherwise return undefined. + */ + function getPropertyOfObjectType(type: ts.Type, name: __String): ts.Symbol | undefined { + if (type.flags & TypeFlags.Object) { + const resolved = resolveStructuredTypeMembers((type)); + const symbol = resolved.members.get(name); + if (symbol && symbolIsValue(symbol)) { + return symbol; + } + } + } + function getPropertiesOfUnionOrIntersectionType(type: UnionOrIntersectionType): ts.Symbol[] { + if (!type.resolvedProperties) { + const members = createSymbolTable(); + for (const current of type.types) { + for (const prop of getPropertiesOfType(current)) { + if (!members.has(prop.escapedName)) { + const combinedProp = getPropertyOfUnionOrIntersectionType(type, prop.escapedName); + if (combinedProp) { + members.set(prop.escapedName, combinedProp); } - const assignedType = getBaseTypeOfLiteralType(getInitialOrAssignedType(flow)); - return isTypeAssignableTo(assignedType, declaredType) ? assignedType : anyArrayType; } - if (declaredType.flags & TypeFlags.Union) { - return getAssignmentReducedType(declaredType, getInitialOrAssignedType(flow)); - } - return declaredType; } - // We didn't have a direct match. However, if the reference is a dotted name, this - // may be an assignment to a left hand part of the reference. For example, for a - // reference 'x.y.z', we may be at an assignment to 'x.y' or 'x'. In that case, - // return the declared type. - if (containsMatchingReference(reference, node)) { - if (!isReachableFlowNode(flow)) { - return unreachableNeverType; - } - // A matching dotted name might also be an expando property on a function *expression*, - // in which case we continue control flow analysis back to the function's declaration - if (isVariableDeclaration(node) && (isInJSFile(node) || isVarConst(node))) { - const init = getDeclaredExpandoInitializer(node); - if (init && (init.kind === SyntaxKind.FunctionExpression || init.kind === SyntaxKind.ArrowFunction)) { - return getTypeAtFlowNode(flow.antecedent); - } - } - return declaredType; + // The properties of a union type are those that are present in all constituent types, so + // we only need to check the properties of the first type + if (type.flags & TypeFlags.Union) { + break; } - // for (const _ in ref) acts as a nonnull on ref - if (isVariableDeclaration(node) && node.parent.parent.kind === SyntaxKind.ForInStatement && isMatchingReference(reference, node.parent.parent.expression)) { - return getNonNullableTypeIfNeeded(getTypeFromFlowType(getTypeAtFlowNode(flow.antecedent))); + } + type.resolvedProperties = getNamedMembers(members); + } + return type.resolvedProperties; + } + function getPropertiesOfType(type: ts.Type): ts.Symbol[] { + type = getApparentType(type); + return type.flags & TypeFlags.UnionOrIntersection ? + getPropertiesOfUnionOrIntersectionType((type)) : + getPropertiesOfObjectType(type); + } + function isTypeInvalidDueToUnionDiscriminant(contextualType: ts.Type, obj: ObjectLiteralExpression | JsxAttributes): boolean { + const list = (obj.properties as NodeArray); + return list.some(property => { + const nameType = property.name && getLiteralTypeFromPropertyName(property.name); + const name = nameType && isTypeUsableAsPropertyName(nameType) ? getPropertyNameFromType(nameType) : undefined; + const expected = name === undefined ? undefined : getTypeOfPropertyOfType(contextualType, name); + return !!expected && isLiteralType(expected) && !isTypeAssignableTo(getTypeOfNode(property), expected); + }); + } + function getAllPossiblePropertiesOfTypes(types: readonly ts.Type[]): ts.Symbol[] { + const unionType = getUnionType(types); + if (!(unionType.flags & TypeFlags.Union)) { + return getAugmentedPropertiesOfType(unionType); + } + const props = createSymbolTable(); + for (const memberType of types) { + for (const { escapedName } of getAugmentedPropertiesOfType(memberType)) { + if (!props.has(escapedName)) { + const prop = createUnionOrIntersectionProperty((unionType as UnionType), escapedName); + // May be undefined if the property is private + if (prop) + props.set(escapedName, prop); } - // Assignment doesn't affect reference - return undefined; } - - function narrowTypeByAssertion(type: Type, expr: Expression): Type { - const node = skipParentheses(expr); - if (node.kind === SyntaxKind.FalseKeyword) { - return unreachableNeverType; + } + return arrayFrom(props.values()); + } + function getConstraintOfType(type: InstantiableType | UnionOrIntersectionType): ts.Type | undefined { + return type.flags & TypeFlags.TypeParameter ? getConstraintOfTypeParameter((type)) : + type.flags & TypeFlags.IndexedAccess ? getConstraintOfIndexedAccess((type)) : + type.flags & TypeFlags.Conditional ? getConstraintOfConditionalType((type)) : + getBaseConstraintOfType(type); + } + function getConstraintOfTypeParameter(typeParameter: TypeParameter): ts.Type | undefined { + return hasNonCircularBaseConstraint(typeParameter) ? getConstraintFromTypeParameter(typeParameter) : undefined; + } + function getConstraintOfIndexedAccess(type: IndexedAccessType) { + return hasNonCircularBaseConstraint(type) ? getConstraintFromIndexedAccess(type) : undefined; + } + function getSimplifiedTypeOrConstraint(type: ts.Type) { + const simplified = getSimplifiedType(type, /*writing*/ false); + return simplified !== type ? simplified : getConstraintOfType(type); + } + function getConstraintFromIndexedAccess(type: IndexedAccessType) { + const indexConstraint = getSimplifiedTypeOrConstraint(type.indexType); + if (indexConstraint && indexConstraint !== type.indexType) { + const indexedAccess = getIndexedAccessTypeOrUndefined(type.objectType, indexConstraint); + if (indexedAccess) { + return indexedAccess; + } + } + const objectConstraint = getSimplifiedTypeOrConstraint(type.objectType); + if (objectConstraint && objectConstraint !== type.objectType) { + return getIndexedAccessTypeOrUndefined(objectConstraint, type.indexType); + } + return undefined; + } + function getDefaultConstraintOfConditionalType(type: ConditionalType) { + if (!type.resolvedDefaultConstraint) { + // An `any` branch of a conditional type would normally be viral - specifically, without special handling here, + // a conditional type with a single branch of type `any` would be assignable to anything, since it's constraint would simplify to + // just `any`. This result is _usually_ unwanted - so instead here we elide an `any` branch from the constraint type, + // in effect treating `any` like `never` rather than `unknown` in this location. + const trueConstraint = getInferredTrueTypeFromConditionalType(type); + const falseConstraint = getFalseTypeFromConditionalType(type); + type.resolvedDefaultConstraint = isTypeAny(trueConstraint) ? falseConstraint : isTypeAny(falseConstraint) ? trueConstraint : getUnionType([trueConstraint, falseConstraint]); + } + return type.resolvedDefaultConstraint; + } + function getConstraintOfDistributiveConditionalType(type: ConditionalType): ts.Type | undefined { + // Check if we have a conditional type of the form 'T extends U ? X : Y', where T is a constrained + // type parameter. If so, create an instantiation of the conditional type where T is replaced + // with its constraint. We do this because if the constraint is a union type it will be distributed + // over the conditional type and possibly reduced. For example, 'T extends undefined ? never : T' + // removes 'undefined' from T. + // We skip returning a distributive constraint for a restrictive instantiation of a conditional type + // as the constraint for all type params (check type included) have been replace with `unknown`, which + // is going to produce even more false positive/negative results than the distribute constraint already does. + // Please note: the distributive constraint is a kludge for emulating what a negated type could to do filter + // a union - once negated types exist and are applied to the conditional false branch, this "constraint" + // likely doesn't need to exist. + if (type.root.isDistributive && type.restrictiveInstantiation !== type) { + const simplified = getSimplifiedType(type.checkType, /*writing*/ false); + const constraint = simplified === type.checkType ? getConstraintOfType(simplified) : simplified; + if (constraint && constraint !== type.checkType) { + const mapper = makeUnaryTypeMapper(type.root.checkType, constraint); + const instantiated = getConditionalTypeInstantiation(type, combineTypeMappers(mapper, type.mapper)); + if (!(instantiated.flags & TypeFlags.Never)) { + return instantiated; + } + } + } + return undefined; + } + function getConstraintFromConditionalType(type: ConditionalType) { + return getConstraintOfDistributiveConditionalType(type) || getDefaultConstraintOfConditionalType(type); + } + function getConstraintOfConditionalType(type: ConditionalType) { + return hasNonCircularBaseConstraint(type) ? getConstraintFromConditionalType(type) : undefined; + } + function getEffectiveConstraintOfIntersection(types: readonly ts.Type[], targetIsUnion: boolean) { + let constraints: ts.Type[] | undefined; + let hasDisjointDomainType = false; + for (const t of types) { + if (t.flags & TypeFlags.Instantiable) { + // We keep following constraints as long as we have an instantiable type that is known + // not to be circular or infinite (hence we stop on index access types). + let constraint = getConstraintOfType(t); + while (constraint && constraint.flags & (TypeFlags.TypeParameter | TypeFlags.Index | TypeFlags.Conditional)) { + constraint = getConstraintOfType(constraint); } - if (node.kind === SyntaxKind.BinaryExpression) { - if ((node).operatorToken.kind === SyntaxKind.AmpersandAmpersandToken) { - return narrowTypeByAssertion(narrowTypeByAssertion(type, (node).left), (node).right); - } - if ((node).operatorToken.kind === SyntaxKind.BarBarToken) { - return getUnionType([narrowTypeByAssertion(type, (node).left), narrowTypeByAssertion(type, (node).right)]); + if (constraint) { + constraints = append(constraints, constraint); + if (targetIsUnion) { + constraints = append(constraints, t); } } - return narrowType(type, node, /*assumeTrue*/ true); } - - function getTypeAtFlowCall(flow: FlowCall): FlowType | undefined { - const signature = getEffectsSignature(flow.node); - if (signature) { - const predicate = getTypePredicateOfSignature(signature); - if (predicate && (predicate.kind === TypePredicateKind.AssertsThis || predicate.kind === TypePredicateKind.AssertsIdentifier)) { - const flowType = getTypeAtFlowNode(flow.antecedent); - const type = getTypeFromFlowType(flowType); - const narrowedType = predicate.type ? narrowTypeByTypePredicate(type, predicate, flow.node, /*assumeTrue*/ true) : - predicate.kind === TypePredicateKind.AssertsIdentifier && predicate.parameterIndex >= 0 && predicate.parameterIndex < flow.node.arguments.length ? narrowTypeByAssertion(type, flow.node.arguments[predicate.parameterIndex]) : - type; - return narrowedType === type ? flowType : createFlowType(narrowedType, isIncomplete(flowType)); - } - if (getReturnTypeOfSignature(signature).flags & TypeFlags.Never) { - return unreachableNeverType; + else if (t.flags & TypeFlags.DisjointDomains) { + hasDisjointDomainType = true; + } + } + // If the target is a union type or if we are intersecting with types belonging to one of the + // disjoint domains, we may end up producing a constraint that hasn't been examined before. + if (constraints && (targetIsUnion || hasDisjointDomainType)) { + if (hasDisjointDomainType) { + // We add any types belong to one of the disjoint domains because they might cause the final + // intersection operation to reduce the union constraints. + for (const t of types) { + if (t.flags & TypeFlags.DisjointDomains) { + constraints = append(constraints, t); } } - return undefined; } - - function getTypeAtFlowArrayMutation(flow: FlowArrayMutation): FlowType | undefined { - if (declaredType === autoType || declaredType === autoArrayType) { - const node = flow.node; - const expr = node.kind === SyntaxKind.CallExpression ? - (node.expression).expression : - (node.left).expression; - if (isMatchingReference(reference, getReferenceCandidate(expr))) { - const flowType = getTypeAtFlowNode(flow.antecedent); - const type = getTypeFromFlowType(flowType); - if (getObjectFlags(type) & ObjectFlags.EvolvingArray) { - let evolvedType = type; - if (node.kind === SyntaxKind.CallExpression) { - for (const arg of node.arguments) { - evolvedType = addEvolvingArrayElementType(evolvedType, arg); - } - } - else { - // We must get the context free expression type so as to not recur in an uncached fashion on the LHS (which causes exponential blowup in compile time) - const indexType = getContextFreeTypeOfExpression((node.left).argumentExpression); - if (isTypeAssignableToKind(indexType, TypeFlags.NumberLike)) { - evolvedType = addEvolvingArrayElementType(evolvedType, node.right); - } + return getIntersectionType(constraints); + } + return undefined; + } + function getBaseConstraintOfType(type: ts.Type): ts.Type | undefined { + if (type.flags & (TypeFlags.InstantiableNonPrimitive | TypeFlags.UnionOrIntersection)) { + const constraint = getResolvedBaseConstraint((type)); + return constraint !== noConstraintType && constraint !== circularConstraintType ? constraint : undefined; + } + return type.flags & TypeFlags.Index ? keyofConstraintType : undefined; + } + /** + * This is similar to `getBaseConstraintOfType` except it returns the input type if there's no base constraint, instead of `undefined` + * It also doesn't map indexes to `string`, as where this is used this would be unneeded (and likely undesirable) + */ + function getBaseConstraintOrType(type: ts.Type) { + return getBaseConstraintOfType(type) || type; + } + function hasNonCircularBaseConstraint(type: InstantiableType): boolean { + return getResolvedBaseConstraint(type) !== circularConstraintType; + } + /** + * Return the resolved base constraint of a type variable. The noConstraintType singleton is returned if the + * type variable has no constraint, and the circularConstraintType singleton is returned if the constraint + * circularly references the type variable. + */ + function getResolvedBaseConstraint(type: InstantiableType | UnionOrIntersectionType): ts.Type { + let nonTerminating = false; + return type.resolvedBaseConstraint || + (type.resolvedBaseConstraint = getTypeWithThisArgument(getImmediateBaseConstraint(type), type)); + function getImmediateBaseConstraint(t: ts.Type): ts.Type { + if (!t.immediateBaseConstraint) { + if (!pushTypeResolution(t, TypeSystemPropertyName.ImmediateBaseConstraint)) { + return circularConstraintType; + } + if (constraintDepth >= 50) { + // We have reached 50 recursive invocations of getImmediateBaseConstraint and there is a + // very high likelihood we're dealing with an infinite generic type that perpetually generates + // new type identities as we descend into it. We stop the recursion here and mark this type + // and the outer types as having circular constraints. + error(currentNode, Diagnostics.Type_instantiation_is_excessively_deep_and_possibly_infinite); + nonTerminating = true; + return t.immediateBaseConstraint = noConstraintType; + } + constraintDepth++; + let result = computeBaseConstraint(getSimplifiedType(t, /*writing*/ false)); + constraintDepth--; + if (!popTypeResolution()) { + if (t.flags & TypeFlags.TypeParameter) { + const errorNode = getConstraintDeclaration((t)); + if (errorNode) { + const diagnostic = error(errorNode, Diagnostics.Type_parameter_0_has_a_circular_constraint, typeToString(t)); + if (currentNode && !isNodeDescendantOf(errorNode, currentNode) && !isNodeDescendantOf(currentNode, errorNode)) { + addRelatedInfo(diagnostic, createDiagnosticForNode(currentNode, Diagnostics.Circularity_originates_in_type_at_this_location)); } - return evolvedType === type ? flowType : createFlowType(evolvedType, isIncomplete(flowType)); } - return flowType; } + result = circularConstraintType; } - return undefined; - } - - function getTypeAtFlowCondition(flow: FlowCondition): FlowType { - const flowType = getTypeAtFlowNode(flow.antecedent); - const type = getTypeFromFlowType(flowType); - if (type.flags & TypeFlags.Never) { - return flowType; - } - // If we have an antecedent type (meaning we're reachable in some way), we first - // attempt to narrow the antecedent type. If that produces the never type, and if - // the antecedent type is incomplete (i.e. a transient type in a loop), then we - // take the type guard as an indication that control *could* reach here once we - // have the complete type. We proceed by switching to the silent never type which - // doesn't report errors when operators are applied to it. Note that this is the - // *only* place a silent never type is ever generated. - const assumeTrue = (flow.flags & FlowFlags.TrueCondition) !== 0; - const nonEvolvingType = finalizeEvolvingArrayType(type); - const narrowedType = narrowType(nonEvolvingType, flow.node, assumeTrue); - if (narrowedType === nonEvolvingType) { - return flowType; + if (nonTerminating) { + result = circularConstraintType; } - const incomplete = isIncomplete(flowType); - const resultType = incomplete && narrowedType.flags & TypeFlags.Never ? silentNeverType : narrowedType; - return createFlowType(resultType, incomplete); + t.immediateBaseConstraint = result || noConstraintType; } - - function getTypeAtSwitchClause(flow: FlowSwitchClause): FlowType { - const expr = flow.switchStatement.expression; - const flowType = getTypeAtFlowNode(flow.antecedent); - let type = getTypeFromFlowType(flowType); - if (isMatchingReference(reference, expr)) { - type = narrowTypeBySwitchOnDiscriminant(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd); - } - else if (expr.kind === SyntaxKind.TypeOfExpression && isMatchingReference(reference, (expr as TypeOfExpression).expression)) { - type = narrowBySwitchOnTypeOf(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd); - } - else { - if (strictNullChecks) { - if (optionalChainContainsReference(expr, reference)) { - type = narrowTypeBySwitchOptionalChainContainment(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd, - t => !(t.flags & (TypeFlags.Undefined | TypeFlags.Never))); - } - else if (expr.kind === SyntaxKind.TypeOfExpression && optionalChainContainsReference((expr as TypeOfExpression).expression, reference)) { - type = narrowTypeBySwitchOptionalChainContainment(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd, - t => !(t.flags & TypeFlags.Never || t.flags & TypeFlags.StringLiteral && (t).value === "undefined")); - } - } - if (isMatchingReferenceDiscriminant(expr, type)) { - type = narrowTypeByDiscriminant(type, expr as AccessExpression, - t => narrowTypeBySwitchOnDiscriminant(t, flow.switchStatement, flow.clauseStart, flow.clauseEnd)); + return t.immediateBaseConstraint; + } + function getBaseConstraint(t: ts.Type): ts.Type | undefined { + const c = getImmediateBaseConstraint(t); + return c !== noConstraintType && c !== circularConstraintType ? c : undefined; + } + function computeBaseConstraint(t: ts.Type): ts.Type | undefined { + if (t.flags & TypeFlags.TypeParameter) { + const constraint = getConstraintFromTypeParameter((t)); + return (t as TypeParameter).isThisType || !constraint ? + constraint : + getBaseConstraint(constraint); + } + if (t.flags & TypeFlags.UnionOrIntersection) { + const types = (t).types; + const baseTypes: ts.Type[] = []; + for (const type of types) { + const baseType = getBaseConstraint(type); + if (baseType) { + baseTypes.push(baseType); } } - return createFlowType(type, isIncomplete(flowType)); + return t.flags & TypeFlags.Union && baseTypes.length === types.length ? getUnionType(baseTypes) : + t.flags & TypeFlags.Intersection && baseTypes.length ? getIntersectionType(baseTypes) : + undefined; } - - function getTypeAtFlowBranchLabel(flow: FlowLabel): FlowType { - const antecedentTypes: Type[] = []; - let subtypeReduction = false; - let seenIncomplete = false; - let bypassFlow: FlowSwitchClause | undefined; - for (const antecedent of flow.antecedents!) { - if (!bypassFlow && antecedent.flags & FlowFlags.SwitchClause && (antecedent).clauseStart === (antecedent).clauseEnd) { - // The antecedent is the bypass branch of a potentially exhaustive switch statement. - bypassFlow = antecedent; - continue; + if (t.flags & TypeFlags.Index) { + return keyofConstraintType; + } + if (t.flags & TypeFlags.IndexedAccess) { + const baseObjectType = getBaseConstraint((t).objectType); + const baseIndexType = getBaseConstraint((t).indexType); + const baseIndexedAccess = baseObjectType && baseIndexType && getIndexedAccessTypeOrUndefined(baseObjectType, baseIndexType); + return baseIndexedAccess && getBaseConstraint(baseIndexedAccess); + } + if (t.flags & TypeFlags.Conditional) { + const constraint = getConstraintFromConditionalType((t)); + constraintDepth++; // Penalize repeating conditional types (this captures the recursion within getConstraintFromConditionalType and carries it forward) + const result = constraint && getBaseConstraint(constraint); + constraintDepth--; + return result; + } + if (t.flags & TypeFlags.Substitution) { + return getBaseConstraint((t).substitute); + } + return t; + } + } + function getApparentTypeOfIntersectionType(type: IntersectionType) { + return type.resolvedApparentType || (type.resolvedApparentType = getTypeWithThisArgument(type, type, /*apparentType*/ true)); + } + function getResolvedTypeParameterDefault(typeParameter: TypeParameter): ts.Type | undefined { + if (!typeParameter.default) { + if (typeParameter.target) { + const targetDefault = getResolvedTypeParameterDefault(typeParameter.target); + typeParameter.default = targetDefault ? instantiateType(targetDefault, typeParameter.mapper) : noConstraintType; + } + else { + // To block recursion, set the initial value to the resolvingDefaultType. + typeParameter.default = resolvingDefaultType; + const defaultDeclaration = typeParameter.symbol && forEach(typeParameter.symbol.declarations, decl => isTypeParameterDeclaration(decl) && decl.default); + const defaultType = defaultDeclaration ? getTypeFromTypeNode(defaultDeclaration) : noConstraintType; + if (typeParameter.default === resolvingDefaultType) { + // If we have not been called recursively, set the correct default type. + typeParameter.default = defaultType; + } + } + } + else if (typeParameter.default === resolvingDefaultType) { + // If we are called recursively for this type parameter, mark the default as circular. + typeParameter.default = circularConstraintType; + } + return typeParameter.default; + } + /** + * Gets the default type for a type parameter. + * + * If the type parameter is the result of an instantiation, this gets the instantiated + * default type of its target. If the type parameter has no default type or the default is + * circular, `undefined` is returned. + */ + function getDefaultFromTypeParameter(typeParameter: TypeParameter): ts.Type | undefined { + const defaultType = getResolvedTypeParameterDefault(typeParameter); + return defaultType !== noConstraintType && defaultType !== circularConstraintType ? defaultType : undefined; + } + function hasNonCircularTypeParameterDefault(typeParameter: TypeParameter) { + return getResolvedTypeParameterDefault(typeParameter) !== circularConstraintType; + } + /** + * Indicates whether the declaration of a typeParameter has a default type. + */ + function hasTypeParameterDefault(typeParameter: TypeParameter): boolean { + return !!(typeParameter.symbol && forEach(typeParameter.symbol.declarations, decl => isTypeParameterDeclaration(decl) && decl.default)); + } + function getApparentTypeOfMappedType(type: MappedType) { + return type.resolvedApparentType || (type.resolvedApparentType = getResolvedApparentTypeOfMappedType(type)); + } + function getResolvedApparentTypeOfMappedType(type: MappedType) { + const typeVariable = getHomomorphicTypeVariable(type); + if (typeVariable) { + const constraint = getConstraintOfTypeParameter(typeVariable); + if (constraint && (isArrayType(constraint) || isTupleType(constraint))) { + const mapper = makeUnaryTypeMapper(typeVariable, constraint); + return instantiateType(type, combineTypeMappers(mapper, type.mapper)); + } + } + return type; + } + /** + * For a type parameter, return the base constraint of the type parameter. For the string, number, + * boolean, and symbol primitive types, return the corresponding object types. Otherwise return the + * type itself. Note that the apparent type of a union type is the union type itself. + */ + function getApparentType(type: ts.Type): ts.Type { + const t = type.flags & TypeFlags.Instantiable ? getBaseConstraintOfType(type) || unknownType : type; + return getObjectFlags(t) & ObjectFlags.Mapped ? getApparentTypeOfMappedType((t)) : + t.flags & TypeFlags.Intersection ? getApparentTypeOfIntersectionType((t)) : + t.flags & TypeFlags.StringLike ? globalStringType : + t.flags & TypeFlags.NumberLike ? globalNumberType : + t.flags & TypeFlags.BigIntLike ? getGlobalBigIntType(/*reportErrors*/ languageVersion >= ScriptTarget.ESNext) : + t.flags & TypeFlags.BooleanLike ? globalBooleanType : + t.flags & TypeFlags.ESSymbolLike ? getGlobalESSymbolType(/*reportErrors*/ languageVersion >= ScriptTarget.ES2015) : + t.flags & TypeFlags.NonPrimitive ? emptyObjectType : + t.flags & TypeFlags.Index ? keyofConstraintType : + t.flags & TypeFlags.Unknown && !strictNullChecks ? emptyObjectType : + t; + } + function createUnionOrIntersectionProperty(containingType: UnionOrIntersectionType, name: __String): ts.Symbol | undefined { + const propSet = createMap(); + let indexTypes: ts.Type[] | undefined; + const isUnion = containingType.flags & TypeFlags.Union; + const excludeModifiers = isUnion ? ModifierFlags.NonPublicAccessibilityModifier : 0; + // Flags we want to propagate to the result if they exist in all source symbols + let optionalFlag = isUnion ? SymbolFlags.None : SymbolFlags.Optional; + let syntheticFlag = CheckFlags.SyntheticMethod; + let checkFlags = 0; + for (const current of containingType.types) { + const type = getApparentType(current); + if (type !== errorType) { + const prop = getPropertyOfType(type, name); + const modifiers = prop ? getDeclarationModifierFlagsFromSymbol(prop) : 0; + if (prop && !(modifiers & excludeModifiers)) { + if (isUnion) { + optionalFlag |= (prop.flags & SymbolFlags.Optional); } - const flowType = getTypeAtFlowNode(antecedent); - const type = getTypeFromFlowType(flowType); - // If the type at a particular antecedent path is the declared type and the - // reference is known to always be assigned (i.e. when declared and initial types - // are the same), there is no reason to process more antecedents since the only - // possible outcome is subtypes that will be removed in the final union type anyway. - if (type === declaredType && declaredType === initialType) { - return type; + else { + optionalFlag &= prop.flags; } - pushIfUnique(antecedentTypes, type); - // If an antecedent type is not a subset of the declared type, we need to perform - // subtype reduction. This happens when a "foreign" type is injected into the control - // flow using the instanceof operator or a user defined type predicate. - if (!isTypeSubsetOf(type, declaredType)) { - subtypeReduction = true; + const id = "" + getSymbolId(prop); + if (!propSet.has(id)) { + propSet.set(id, prop); } - if (isIncomplete(flowType)) { - seenIncomplete = true; + checkFlags |= (isReadonlySymbol(prop) ? CheckFlags.Readonly : 0) | + (!(modifiers & ModifierFlags.NonPublicAccessibilityModifier) ? CheckFlags.ContainsPublic : 0) | + (modifiers & ModifierFlags.Protected ? CheckFlags.ContainsProtected : 0) | + (modifiers & ModifierFlags.Private ? CheckFlags.ContainsPrivate : 0) | + (modifiers & ModifierFlags.Static ? CheckFlags.ContainsStatic : 0); + if (!isPrototypeProperty(prop)) { + syntheticFlag = CheckFlags.SyntheticProperty; } } - if (bypassFlow) { - const flowType = getTypeAtFlowNode(bypassFlow); - const type = getTypeFromFlowType(flowType); - // If the bypass flow contributes a type we haven't seen yet and the switch statement - // isn't exhaustive, process the bypass flow type. Since exhaustiveness checks increase - // the risk of circularities, we only want to perform them when they make a difference. - if (!contains(antecedentTypes, type) && !isExhaustiveSwitchStatement(bypassFlow.switchStatement)) { - if (type === declaredType && declaredType === initialType) { - return type; - } - antecedentTypes.push(type); - if (!isTypeSubsetOf(type, declaredType)) { - subtypeReduction = true; - } - if (isIncomplete(flowType)) { - seenIncomplete = true; - } + else if (isUnion) { + const indexInfo = !isLateBoundName(name) && (isNumericLiteralName(name) && getIndexInfoOfType(type, IndexKind.Number) || getIndexInfoOfType(type, IndexKind.String)); + if (indexInfo) { + checkFlags |= CheckFlags.WritePartial | (indexInfo.isReadonly ? CheckFlags.Readonly : 0); + indexTypes = append(indexTypes, isTupleType(type) ? getRestTypeOfTupleType(type) || undefinedType : indexInfo.type); } - } - return createFlowType(getUnionOrEvolvingArrayType(antecedentTypes, subtypeReduction ? UnionReduction.Subtype : UnionReduction.Literal), seenIncomplete); - } - - function getTypeAtFlowLoopLabel(flow: FlowLabel): FlowType { - // If we have previously computed the control flow type for the reference at - // this flow loop junction, return the cached type. - const id = getFlowNodeId(flow); - const cache = flowLoopCaches[id] || (flowLoopCaches[id] = createMap()); - const key = getOrSetCacheKey(); - if (!key) { - // No cache key is generated when binding patterns are in unnarrowable situations - return declaredType; - } - const cached = cache.get(key); - if (cached) { - return cached; - } - // If this flow loop junction and reference are already being processed, return - // the union of the types computed for each branch so far, marked as incomplete. - // It is possible to see an empty array in cases where loops are nested and the - // back edge of the outer loop reaches an inner loop that is already being analyzed. - // In such cases we restart the analysis of the inner loop, which will then see - // a non-empty in-process array for the outer loop and eventually terminate because - // the first antecedent of a loop junction is always the non-looping control flow - // path that leads to the top. - for (let i = flowLoopStart; i < flowLoopCount; i++) { - if (flowLoopNodes[i] === flow && flowLoopKeys[i] === key && flowLoopTypes[i].length) { - return createFlowType(getUnionOrEvolvingArrayType(flowLoopTypes[i], UnionReduction.Literal), /*incomplete*/ true); - } - } - // Add the flow loop junction and reference to the in-process stack and analyze - // each antecedent code path. - const antecedentTypes: Type[] = []; - let subtypeReduction = false; - let firstAntecedentType: FlowType | undefined; - for (const antecedent of flow.antecedents!) { - let flowType; - if (!firstAntecedentType) { - // The first antecedent of a loop junction is always the non-looping control - // flow path that leads to the top. - flowType = firstAntecedentType = getTypeAtFlowNode(antecedent); + else if (isObjectLiteralType(type)) { + checkFlags |= CheckFlags.WritePartial; + indexTypes = append(indexTypes, undefinedType); } else { - // All but the first antecedent are the looping control flow paths that lead - // back to the loop junction. We track these on the flow loop stack. - flowLoopNodes[flowLoopCount] = flow; - flowLoopKeys[flowLoopCount] = key; - flowLoopTypes[flowLoopCount] = antecedentTypes; - flowLoopCount++; - const saveFlowTypeCache = flowTypeCache; - flowTypeCache = undefined; - flowType = getTypeAtFlowNode(antecedent); - flowTypeCache = saveFlowTypeCache; - flowLoopCount--; - // If we see a value appear in the cache it is a sign that control flow analysis - // was restarted and completed by checkExpressionCached. We can simply pick up - // the resulting type and bail out. - const cached = cache.get(key); - if (cached) { - return cached; - } - } - const type = getTypeFromFlowType(flowType); - pushIfUnique(antecedentTypes, type); - // If an antecedent type is not a subset of the declared type, we need to perform - // subtype reduction. This happens when a "foreign" type is injected into the control - // flow using the instanceof operator or a user defined type predicate. - if (!isTypeSubsetOf(type, declaredType)) { - subtypeReduction = true; + checkFlags |= CheckFlags.ReadPartial; } - // If the type at a particular antecedent path is the declared type there is no - // reason to process more antecedents since the only possible outcome is subtypes - // that will be removed in the final union type anyway. - if (type === declaredType) { - break; - } - } - // The result is incomplete if the first antecedent (the non-looping control flow path) - // is incomplete. - const result = getUnionOrEvolvingArrayType(antecedentTypes, subtypeReduction ? UnionReduction.Subtype : UnionReduction.Literal); - if (isIncomplete(firstAntecedentType!)) { - return createFlowType(result, /*incomplete*/ true); - } - cache.set(key, result); - return result; - } - - function isMatchingReferenceDiscriminant(expr: Expression, computedType: Type) { - if (!(computedType.flags & TypeFlags.Union) || !isAccessExpression(expr)) { - return false; } - const name = getAccessedPropertyName(expr); - if (name === undefined) { - return false; - } - return isMatchingReference(reference, expr.expression) && isDiscriminantProperty(computedType, name); } - - function narrowTypeByDiscriminant(type: Type, access: AccessExpression, narrowType: (t: Type) => Type): Type { - const propName = getAccessedPropertyName(access); - if (propName === undefined) { - return type; - } - const propType = getTypeOfPropertyOfType(type, propName); - if (!propType) { - return type; - } - const narrowedPropType = narrowType(propType); - return filterType(type, t => { - const discriminantType = getTypeOfPropertyOrIndexSignature(t, propName); - return !(discriminantType.flags & TypeFlags.Never) && isTypeComparableTo(discriminantType, narrowedPropType); - }); + } + if (!propSet.size) { + return undefined; + } + const props = arrayFrom(propSet.values()); + if (props.length === 1 && !(checkFlags & CheckFlags.ReadPartial) && !indexTypes) { + return props[0]; + } + let declarations: Declaration[] | undefined; + let firstType: ts.Type | undefined; + let nameType: ts.Type | undefined; + const propTypes: ts.Type[] = []; + let firstValueDeclaration: Declaration | undefined; + let hasNonUniformValueDeclaration = false; + for (const prop of props) { + if (!firstValueDeclaration) { + firstValueDeclaration = prop.valueDeclaration; + } + else if (prop.valueDeclaration !== firstValueDeclaration) { + hasNonUniformValueDeclaration = true; + } + declarations = addRange(declarations, prop.declarations); + const type = getTypeOfSymbol(prop); + if (!firstType) { + firstType = type; + nameType = getSymbolLinks(prop).nameType; + } + else if (type !== firstType) { + checkFlags |= CheckFlags.HasNonUniformType; + } + if (isLiteralType(type)) { + checkFlags |= CheckFlags.HasLiteralType; + } + propTypes.push(type); + } + addRange(propTypes, indexTypes); + const result = createSymbol(SymbolFlags.Property | optionalFlag, name, syntheticFlag | checkFlags); + result.containingType = containingType; + if (!hasNonUniformValueDeclaration && firstValueDeclaration) { + result.valueDeclaration = firstValueDeclaration; + // Inherit information about parent type. + if (firstValueDeclaration.symbol.parent) { + result.parent = firstValueDeclaration.symbol.parent; + } + } + result.declarations = declarations!; + result.nameType = nameType; + if (propTypes.length > 2) { + // When `propTypes` has the potential to explode in size when normalized, defer normalization until absolutely needed + result.checkFlags |= CheckFlags.DeferredType; + result.deferralParent = containingType; + result.deferralConstituents = propTypes; + } + else { + result.type = isUnion ? getUnionType(propTypes) : getIntersectionType(propTypes); + } + return result; + } + // Return the symbol for a given property in a union or intersection type, or undefined if the property + // does not exist in any constituent type. Note that the returned property may only be present in some + // constituents, in which case the isPartial flag is set when the containing type is union type. We need + // these partial properties when identifying discriminant properties, but otherwise they are filtered out + // and do not appear to be present in the union type. + function getUnionOrIntersectionProperty(type: UnionOrIntersectionType, name: __String): ts.Symbol | undefined { + const properties = type.propertyCache || (type.propertyCache = createSymbolTable()); + let property = properties.get(name); + if (!property) { + property = createUnionOrIntersectionProperty(type, name); + if (property) { + properties.set(name, property); + } + } + return property; + } + function getPropertyOfUnionOrIntersectionType(type: UnionOrIntersectionType, name: __String): ts.Symbol | undefined { + const property = getUnionOrIntersectionProperty(type, name); + // We need to filter out partial properties in union types + return property && !(getCheckFlags(property) & CheckFlags.ReadPartial) ? property : undefined; + } + /** + * Return the symbol for the property with the given name in the given type. Creates synthetic union properties when + * necessary, maps primitive types and type parameters are to their apparent types, and augments with properties from + * Object and Function as appropriate. + * + * @param type a type to look up property from + * @param name a name of property to look up in a given type + */ + function getPropertyOfType(type: ts.Type, name: __String): ts.Symbol | undefined { + type = getApparentType(type); + if (type.flags & TypeFlags.Object) { + const resolved = resolveStructuredTypeMembers((type)); + const symbol = resolved.members.get(name); + if (symbol && symbolIsValue(symbol)) { + return symbol; } - - function narrowTypeByTruthiness(type: Type, expr: Expression, assumeTrue: boolean): Type { - if (isMatchingReference(reference, expr)) { - return getTypeWithFacts(type, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy); - } - if (strictNullChecks && assumeTrue && optionalChainContainsReference(expr, reference)) { - type = getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull); - } - if (isMatchingReferenceDiscriminant(expr, declaredType)) { - return narrowTypeByDiscriminant(type, expr, t => getTypeWithFacts(t, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy)); + const functionType = resolved === anyFunctionType ? globalFunctionType : + resolved.callSignatures.length ? globalCallableFunctionType : + resolved.constructSignatures.length ? globalNewableFunctionType : + undefined; + if (functionType) { + const symbol = getPropertyOfObjectType(functionType, name); + if (symbol) { + return symbol; } - return type; } - - function isTypePresencePossible(type: Type, propName: __String, assumeTrue: boolean) { - if (getIndexInfoOfType(type, IndexKind.String)) { - return true; - } - const prop = getPropertyOfType(type, propName); - if (prop) { - return prop.flags & SymbolFlags.Optional ? true : assumeTrue; + return getPropertyOfObjectType(globalObjectType, name); + } + if (type.flags & TypeFlags.UnionOrIntersection) { + return getPropertyOfUnionOrIntersectionType((type), name); + } + return undefined; + } + function getSignaturesOfStructuredType(type: ts.Type, kind: SignatureKind): readonly ts.Signature[] { + if (type.flags & TypeFlags.StructuredType) { + const resolved = resolveStructuredTypeMembers((type)); + return kind === SignatureKind.Call ? resolved.callSignatures : resolved.constructSignatures; + } + return emptyArray; + } + /** + * Return the signatures of the given kind in the given type. Creates synthetic union signatures when necessary and + * maps primitive types and type parameters are to their apparent types. + */ + function getSignaturesOfType(type: ts.Type, kind: SignatureKind): readonly ts.Signature[] { + return getSignaturesOfStructuredType(getApparentType(type), kind); + } + function getIndexInfoOfStructuredType(type: ts.Type, kind: IndexKind): IndexInfo | undefined { + if (type.flags & TypeFlags.StructuredType) { + const resolved = resolveStructuredTypeMembers((type)); + return kind === IndexKind.String ? resolved.stringIndexInfo : resolved.numberIndexInfo; + } + } + function getIndexTypeOfStructuredType(type: ts.Type, kind: IndexKind): ts.Type | undefined { + const info = getIndexInfoOfStructuredType(type, kind); + return info && info.type; + } + // Return the indexing info of the given kind in the given type. Creates synthetic union index types when necessary and + // maps primitive types and type parameters are to their apparent types. + function getIndexInfoOfType(type: ts.Type, kind: IndexKind): IndexInfo | undefined { + return getIndexInfoOfStructuredType(getApparentType(type), kind); + } + // Return the index type of the given kind in the given type. Creates synthetic union index types when necessary and + // maps primitive types and type parameters are to their apparent types. + function getIndexTypeOfType(type: ts.Type, kind: IndexKind): ts.Type | undefined { + return getIndexTypeOfStructuredType(getApparentType(type), kind); + } + function getImplicitIndexTypeOfType(type: ts.Type, kind: IndexKind): ts.Type | undefined { + if (isObjectTypeWithInferableIndex(type)) { + const propTypes: ts.Type[] = []; + for (const prop of getPropertiesOfType(type)) { + if (kind === IndexKind.String || isNumericLiteralName(prop.escapedName)) { + propTypes.push(getTypeOfSymbol(prop)); } - return !assumeTrue; } - - function narrowByInKeyword(type: Type, literal: LiteralExpression, assumeTrue: boolean) { - if (type.flags & (TypeFlags.Union | TypeFlags.Object) || isThisTypeParameter(type)) { - const propName = escapeLeadingUnderscores(literal.text); - return filterType(type, t => isTypePresencePossible(t, propName, assumeTrue)); - } - return type; + if (kind === IndexKind.String) { + append(propTypes, getIndexTypeOfType(type, IndexKind.Number)); } - - function narrowTypeByBinaryExpression(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type { - switch (expr.operatorToken.kind) { - case SyntaxKind.EqualsToken: - return narrowTypeByTruthiness(narrowType(type, expr.right, assumeTrue), expr.left, assumeTrue); - case SyntaxKind.EqualsEqualsToken: - case SyntaxKind.ExclamationEqualsToken: - case SyntaxKind.EqualsEqualsEqualsToken: - case SyntaxKind.ExclamationEqualsEqualsToken: - const operator = expr.operatorToken.kind; - const left = getReferenceCandidate(expr.left); - const right = getReferenceCandidate(expr.right); - if (left.kind === SyntaxKind.TypeOfExpression && isStringLiteralLike(right)) { - return narrowTypeByTypeof(type, left, operator, right, assumeTrue); - } - if (right.kind === SyntaxKind.TypeOfExpression && isStringLiteralLike(left)) { - return narrowTypeByTypeof(type, right, operator, left, assumeTrue); - } - if (isMatchingReference(reference, left)) { - return narrowTypeByEquality(type, operator, right, assumeTrue); - } - if (isMatchingReference(reference, right)) { - return narrowTypeByEquality(type, operator, left, assumeTrue); - } - if (strictNullChecks) { - if (optionalChainContainsReference(left, reference)) { - type = narrowTypeByOptionalChainContainment(type, operator, right, assumeTrue); - } - else if (optionalChainContainsReference(right, reference)) { - type = narrowTypeByOptionalChainContainment(type, operator, left, assumeTrue); - } - } - if (isMatchingReferenceDiscriminant(left, declaredType)) { - return narrowTypeByDiscriminant(type, left, t => narrowTypeByEquality(t, operator, right, assumeTrue)); - } - if (isMatchingReferenceDiscriminant(right, declaredType)) { - return narrowTypeByDiscriminant(type, right, t => narrowTypeByEquality(t, operator, left, assumeTrue)); - } - break; - case SyntaxKind.InstanceOfKeyword: - return narrowTypeByInstanceof(type, expr, assumeTrue); - case SyntaxKind.InKeyword: - const target = getReferenceCandidate(expr.right); - if (isStringLiteralLike(expr.left) && isMatchingReference(reference, target)) { - return narrowByInKeyword(type, expr.left, assumeTrue); - } - break; - case SyntaxKind.CommaToken: - return narrowType(type, expr.right, assumeTrue); - } - return type; + if (propTypes.length) { + return getUnionType(propTypes, UnionReduction.Subtype); } - - function narrowTypeByOptionalChainContainment(type: Type, operator: SyntaxKind, value: Expression, assumeTrue: boolean): Type { - // We are in a branch of obj?.foo === value (or any one of the other equality operators). We narrow obj as follows: - // When operator is === and type of value excludes undefined, null and undefined is removed from type of obj in true branch. - // When operator is !== and type of value excludes undefined, null and undefined is removed from type of obj in false branch. - // When operator is == and type of value excludes null and undefined, null and undefined is removed from type of obj in true branch. - // When operator is != and type of value excludes null and undefined, null and undefined is removed from type of obj in false branch. - // When operator is === and type of value is undefined, null and undefined is removed from type of obj in false branch. - // When operator is !== and type of value is undefined, null and undefined is removed from type of obj in true branch. - // When operator is == and type of value is null or undefined, null and undefined is removed from type of obj in false branch. - // When operator is != and type of value is null or undefined, null and undefined is removed from type of obj in true branch. - const equalsOperator = operator === SyntaxKind.EqualsEqualsToken || operator === SyntaxKind.EqualsEqualsEqualsToken; - const nullableFlags = operator === SyntaxKind.EqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsToken ? TypeFlags.Nullable : TypeFlags.Undefined; - const valueType = getTypeOfExpression(value); - // Note that we include any and unknown in the exclusion test because their domain includes null and undefined. - const removeNullable = equalsOperator !== assumeTrue && everyType(valueType, t => !!(t.flags & nullableFlags)) || - equalsOperator === assumeTrue && everyType(valueType, t => !(t.flags & (TypeFlags.AnyOrUnknown | nullableFlags))); - return removeNullable ? getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type; + } + return undefined; + } + // Return list of type parameters with duplicates removed (duplicate identifier errors are generated in the actual + // type checking functions). + function getTypeParametersFromDeclaration(declaration: DeclarationWithTypeParameters): TypeParameter[] | undefined { + let result: TypeParameter[] | undefined; + for (const node of getEffectiveTypeParameterDeclarations(declaration)) { + result = appendIfUnique(result, getDeclaredTypeOfTypeParameter(node.symbol)); + } + return result; + } + function symbolsToArray(symbols: SymbolTable): ts.Symbol[] { + const result: ts.Symbol[] = []; + symbols.forEach((symbol, id) => { + if (!isReservedMemberName(id)) { + result.push(symbol); } - - function narrowTypeByEquality(type: Type, operator: SyntaxKind, value: Expression, assumeTrue: boolean): Type { - if (type.flags & TypeFlags.Any) { - return type; - } - if (operator === SyntaxKind.ExclamationEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken) { - assumeTrue = !assumeTrue; - } - const valueType = getTypeOfExpression(value); - if ((type.flags & TypeFlags.Unknown) && assumeTrue && (operator === SyntaxKind.EqualsEqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken)) { - if (valueType.flags & (TypeFlags.Primitive | TypeFlags.NonPrimitive)) { - return valueType; - } - if (valueType.flags & TypeFlags.Object) { - return nonPrimitiveType; - } - return type; + }); + return result; + } + function isJSDocOptionalParameter(node: ParameterDeclaration) { + return isInJSFile(node) && ( + // node.type should only be a JSDocOptionalType when node is a parameter of a JSDocFunctionType + node.type && node.type.kind === SyntaxKind.JSDocOptionalType + || getJSDocParameterTags(node).some(({ isBracketed, typeExpression }) => isBracketed || !!typeExpression && typeExpression.type.kind === SyntaxKind.JSDocOptionalType)); + } + function tryFindAmbientModule(moduleName: string, withAugmentations: boolean) { + if (isExternalModuleNameRelative(moduleName)) { + return undefined; + } + const symbol = getSymbol(globals, ('"' + moduleName + '"' as __String), SymbolFlags.ValueModule); + // merged symbol is module declaration symbol combined with all augmentations + return symbol && withAugmentations ? getMergedSymbol(symbol) : symbol; + } + function isOptionalParameter(node: ParameterDeclaration | JSDocParameterTag) { + if (hasQuestionToken(node) || isOptionalJSDocParameterTag(node) || isJSDocOptionalParameter(node)) { + return true; + } + if (node.initializer) { + const signature = getSignatureFromDeclaration(node.parent); + const parameterIndex = node.parent.parameters.indexOf(node); + Debug.assert(parameterIndex >= 0); + return parameterIndex >= getMinArgumentCount(signature); + } + const iife = getImmediatelyInvokedFunctionExpression(node.parent); + if (iife) { + return !node.type && + !node.dotDotDotToken && + node.parent.parameters.indexOf(node) >= iife.arguments.length; + } + return false; + } + function isOptionalJSDocParameterTag(node: Node): node is JSDocParameterTag { + if (!isJSDocParameterTag(node)) { + return false; + } + const { isBracketed, typeExpression } = node; + return isBracketed || !!typeExpression && typeExpression.type.kind === SyntaxKind.JSDocOptionalType; + } + function createTypePredicate(kind: TypePredicateKind, parameterName: string | undefined, parameterIndex: number | undefined, type: ts.Type | undefined): TypePredicate { + return { kind, parameterName, parameterIndex, type } as TypePredicate; + } + /** + * Gets the minimum number of type arguments needed to satisfy all non-optional type + * parameters. + */ + function getMinTypeArgumentCount(typeParameters: readonly TypeParameter[] | undefined): number { + let minTypeArgumentCount = 0; + if (typeParameters) { + for (let i = 0; i < typeParameters.length; i++) { + if (!hasTypeParameterDefault(typeParameters[i])) { + minTypeArgumentCount = i + 1; } - if (valueType.flags & TypeFlags.Nullable) { - if (!strictNullChecks) { - return type; - } - const doubleEquals = operator === SyntaxKind.EqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsToken; - const facts = doubleEquals ? - assumeTrue ? TypeFacts.EQUndefinedOrNull : TypeFacts.NEUndefinedOrNull : - valueType.flags & TypeFlags.Null ? - assumeTrue ? TypeFacts.EQNull : TypeFacts.NENull : - assumeTrue ? TypeFacts.EQUndefined : TypeFacts.NEUndefined; - return getTypeWithFacts(type, facts); + } + } + return minTypeArgumentCount; + } + /** + * Fill in default types for unsupplied type arguments. If `typeArguments` is undefined + * when a default type is supplied, a new array will be created and returned. + * + * @param typeArguments The supplied type arguments. + * @param typeParameters The requested type parameters. + * @param minTypeArgumentCount The minimum number of required type arguments. + */ + function fillMissingTypeArguments(typeArguments: readonly ts.Type[], typeParameters: readonly TypeParameter[] | undefined, minTypeArgumentCount: number, isJavaScriptImplicitAny: boolean): ts.Type[]; + function fillMissingTypeArguments(typeArguments: readonly ts.Type[] | undefined, typeParameters: readonly TypeParameter[] | undefined, minTypeArgumentCount: number, isJavaScriptImplicitAny: boolean): ts.Type[] | undefined; + function fillMissingTypeArguments(typeArguments: readonly ts.Type[] | undefined, typeParameters: readonly TypeParameter[] | undefined, minTypeArgumentCount: number, isJavaScriptImplicitAny: boolean) { + const numTypeParameters = length(typeParameters); + if (!numTypeParameters) { + return []; + } + const numTypeArguments = length(typeArguments); + if (isJavaScriptImplicitAny || (numTypeArguments >= minTypeArgumentCount && numTypeArguments <= numTypeParameters)) { + const result = typeArguments ? typeArguments.slice() : []; + // Map invalid forward references in default types to the error type + for (let i = numTypeArguments; i < numTypeParameters; i++) { + result[i] = errorType; + } + const baseDefaultType = getDefaultTypeArgumentType(isJavaScriptImplicitAny); + for (let i = numTypeArguments; i < numTypeParameters; i++) { + let defaultType = getDefaultFromTypeParameter(typeParameters![i]); + if (isJavaScriptImplicitAny && defaultType && (isTypeIdenticalTo(defaultType, unknownType) || isTypeIdenticalTo(defaultType, emptyObjectType))) { + defaultType = anyType; + } + result[i] = defaultType ? instantiateType(defaultType, createTypeMapper(typeParameters!, result)) : baseDefaultType; + } + result.length = typeParameters!.length; + return result; + } + return typeArguments && typeArguments.slice(); + } + function getSignatureFromDeclaration(declaration: SignatureDeclaration | JSDocSignature): ts.Signature { + const links = getNodeLinks(declaration); + if (!links.resolvedSignature) { + const parameters: ts.Symbol[] = []; + let flags = SignatureFlags.None; + let minArgumentCount = 0; + let thisParameter: ts.Symbol | undefined; + let hasThisParameter = false; + const iife = getImmediatelyInvokedFunctionExpression(declaration); + const isJSConstructSignature = isJSDocConstructSignature(declaration); + const isUntypedSignatureInJSFile = !iife && + isInJSFile(declaration) && + isValueSignatureDeclaration(declaration) && + !hasJSDocParameterTags(declaration) && + !getJSDocType(declaration); + // If this is a JSDoc construct signature, then skip the first parameter in the + // parameter list. The first parameter represents the return type of the construct + // signature. + for (let i = isJSConstructSignature ? 1 : 0; i < declaration.parameters.length; i++) { + const param = declaration.parameters[i]; + let paramSymbol = param.symbol; + const type = isJSDocParameterTag(param) ? (param.typeExpression && param.typeExpression.type) : param.type; + // Include parameter symbol instead of property symbol in the signature + if (paramSymbol && !!(paramSymbol.flags & SymbolFlags.Property) && !isBindingPattern(param.name)) { + const resolvedSymbol = resolveName(param, paramSymbol.escapedName, SymbolFlags.Value, undefined, undefined, /*isUse*/ false); + paramSymbol = resolvedSymbol!; + } + if (i === 0 && paramSymbol.escapedName === InternalSymbolName.This) { + hasThisParameter = true; + thisParameter = param.symbol; } - if (type.flags & TypeFlags.NotUnionOrUnit) { - return type; + else { + parameters.push(paramSymbol); } - if (assumeTrue) { - const filterFn: (t: Type) => boolean = operator === SyntaxKind.EqualsEqualsToken ? - (t => areTypesComparable(t, valueType) || isCoercibleUnderDoubleEquals(t, valueType)) : - t => areTypesComparable(t, valueType); - const narrowedType = filterType(type, filterFn); - return narrowedType.flags & TypeFlags.Never ? type : replacePrimitivesWithLiterals(narrowedType, valueType); + if (type && type.kind === SyntaxKind.LiteralType) { + flags |= SignatureFlags.HasLiteralTypes; } - if (isUnitType(valueType)) { - const regularType = getRegularTypeOfLiteralType(valueType); - return filterType(type, t => getRegularTypeOfLiteralType(t) !== regularType); + // Record a new minimum argument count if this is not an optional parameter + const isOptionalParameter = isOptionalJSDocParameterTag(param) || + param.initializer || param.questionToken || param.dotDotDotToken || + iife && parameters.length > iife.arguments.length && !type || + isUntypedSignatureInJSFile || + isJSDocOptionalParameter(param); + if (!isOptionalParameter) { + minArgumentCount = parameters.length; } - return type; } - - function narrowTypeByTypeof(type: Type, typeOfExpr: TypeOfExpression, operator: SyntaxKind, literal: LiteralExpression, assumeTrue: boolean): Type { - // We have '==', '!=', '===', or !==' operator with 'typeof xxx' and string literal operands - if (operator === SyntaxKind.ExclamationEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken) { - assumeTrue = !assumeTrue; - } - const target = getReferenceCandidate(typeOfExpr.expression); - if (!isMatchingReference(reference, target)) { - if (strictNullChecks && optionalChainContainsReference(target, reference) && assumeTrue === (literal.text !== "undefined")) { - return getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull); - } - // For a reference of the form 'x.y', a 'typeof x === ...' type guard resets the - // narrowed type of 'y' to its declared type. - if (containsMatchingReference(reference, target)) { - return declaredType; - } - return type; + // If only one accessor includes a this-type annotation, the other behaves as if it had the same type annotation + if ((declaration.kind === SyntaxKind.GetAccessor || declaration.kind === SyntaxKind.SetAccessor) && + !hasNonBindableDynamicName(declaration) && + (!hasThisParameter || !thisParameter)) { + const otherKind = declaration.kind === SyntaxKind.GetAccessor ? SyntaxKind.SetAccessor : SyntaxKind.GetAccessor; + const other = getDeclarationOfKind(getSymbolOfNode(declaration), otherKind); + if (other) { + thisParameter = getAnnotatedAccessorThisParameter(other); } - if (type.flags & TypeFlags.Any && literal.text === "function") { - return type; + } + const classType = declaration.kind === SyntaxKind.Constructor ? + getDeclaredTypeOfClassOrInterface(getMergedSymbol((declaration.parent).symbol)) + : undefined; + const typeParameters = classType ? classType.localTypeParameters : getTypeParametersFromDeclaration(declaration); + if (hasRestParameter(declaration) || isInJSFile(declaration) && maybeAddJsSyntheticRestParameter(declaration, parameters)) { + flags |= SignatureFlags.HasRestParameter; + } + links.resolvedSignature = createSignature(declaration, typeParameters, thisParameter, parameters, + /*resolvedReturnType*/ undefined, /*resolvedTypePredicate*/ undefined, minArgumentCount, flags); + } + return links.resolvedSignature; + } + /** + * A JS function gets a synthetic rest parameter if it references `arguments` AND: + * 1. It has no parameters but at least one `@param` with a type that starts with `...` + * OR + * 2. It has at least one parameter, and the last parameter has a matching `@param` with a type that starts with `...` + */ + function maybeAddJsSyntheticRestParameter(declaration: SignatureDeclaration | JSDocSignature, parameters: ts.Symbol[]): boolean { + if (isJSDocSignature(declaration) || !containsArgumentsReference(declaration)) { + return false; + } + const lastParam = lastOrUndefined(declaration.parameters); + const lastParamTags = lastParam ? getJSDocParameterTags(lastParam) : getJSDocTags(declaration).filter(isJSDocParameterTag); + const lastParamVariadicType = firstDefined(lastParamTags, p => p.typeExpression && isJSDocVariadicType(p.typeExpression.type) ? p.typeExpression.type : undefined); + const syntheticArgsSymbol = createSymbol(SymbolFlags.Variable, ("args" as __String), CheckFlags.RestParameter); + syntheticArgsSymbol.type = lastParamVariadicType ? createArrayType(getTypeFromTypeNode(lastParamVariadicType.type)) : anyArrayType; + if (lastParamVariadicType) { + // Replace the last parameter with a rest parameter. + parameters.pop(); + } + parameters.push(syntheticArgsSymbol); + return true; + } + function getSignatureOfTypeTag(node: SignatureDeclaration | JSDocSignature) { + const typeTag = isInJSFile(node) ? getJSDocTypeTag(node) : undefined; + const signature = typeTag && typeTag.typeExpression && getSingleCallSignature(getTypeFromTypeNode(typeTag.typeExpression)); + return signature && getErasedSignature(signature); + } + function getReturnTypeOfTypeTag(node: SignatureDeclaration | JSDocSignature) { + const signature = getSignatureOfTypeTag(node); + return signature && getReturnTypeOfSignature(signature); + } + function containsArgumentsReference(declaration: SignatureDeclaration): boolean { + const links = getNodeLinks(declaration); + if (links.containsArgumentsReference === undefined) { + if (links.flags & NodeCheckFlags.CaptureArguments) { + links.containsArgumentsReference = true; + } + else { + links.containsArgumentsReference = traverse(((declaration as FunctionLikeDeclaration).body!)); + } + } + return links.containsArgumentsReference; + function traverse(node: Node): boolean { + if (!node) + return false; + switch (node.kind) { + case SyntaxKind.Identifier: + return (node).escapedText === "arguments" && isExpressionNode(node); + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + return (node).name!.kind === SyntaxKind.ComputedPropertyName + && traverse(((node).name!)); + default: + return !nodeStartsNewLexicalEnvironment(node) && !isPartOfTypeNode(node) && !!forEachChild(node, traverse); + } + } + } + function getSignaturesOfSymbol(symbol: ts.Symbol | undefined): ts.Signature[] { + if (!symbol) + return emptyArray; + const result: ts.Signature[] = []; + for (let i = 0; i < symbol.declarations.length; i++) { + const decl = symbol.declarations[i]; + if (!isFunctionLike(decl)) + continue; + // Don't include signature if node is the implementation of an overloaded function. A node is considered + // an implementation node if it has a body and the previous node is of the same kind and immediately + // precedes the implementation node (i.e. has the same parent and ends where the implementation starts). + if (i > 0 && (decl as FunctionLikeDeclaration).body) { + const previous = symbol.declarations[i - 1]; + if (decl.parent === previous.parent && decl.kind === previous.kind && decl.pos === previous.end) { + continue; } - const facts = assumeTrue ? - typeofEQFacts.get(literal.text) || TypeFacts.TypeofEQHostObject : - typeofNEFacts.get(literal.text) || TypeFacts.TypeofNEHostObject; - return getTypeWithFacts(assumeTrue ? mapType(type, narrowTypeForTypeof) : type, facts); - - function narrowTypeForTypeof(type: Type) { - if (type.flags & TypeFlags.Unknown && literal.text === "object") { - return getUnionType([nonPrimitiveType, nullType]); - } - // We narrow a non-union type to an exact primitive type if the non-union type - // is a supertype of that primitive type. For example, type 'any' can be narrowed - // to one of the primitive types. - const targetType = literal.text === "function" ? globalFunctionType : typeofTypesByName.get(literal.text); - if (targetType) { - if (isTypeSubtypeOf(type, targetType)) { - return type; - } - if (isTypeSubtypeOf(targetType, type)) { - return targetType; - } - if (type.flags & TypeFlags.Instantiable) { - const constraint = getBaseConstraintOfType(type) || anyType; - if (isTypeSubtypeOf(targetType, constraint)) { - return getIntersectionType([type, targetType]); - } - } + } + result.push(getSignatureFromDeclaration(decl)); + } + return result; + } + function resolveExternalModuleTypeByLiteral(name: StringLiteral) { + const moduleSym = resolveExternalModuleName(name, name); + if (moduleSym) { + const resolvedModuleSymbol = resolveExternalModuleSymbol(moduleSym); + if (resolvedModuleSymbol) { + return getTypeOfSymbol(resolvedModuleSymbol); + } + } + return anyType; + } + function getThisTypeOfSignature(signature: ts.Signature): ts.Type | undefined { + if (signature.thisParameter) { + return getTypeOfSymbol(signature.thisParameter); + } + } + function getTypePredicateOfSignature(signature: ts.Signature): TypePredicate | undefined { + if (!signature.resolvedTypePredicate) { + if (signature.target) { + const targetTypePredicate = getTypePredicateOfSignature(signature.target); + signature.resolvedTypePredicate = targetTypePredicate ? instantiateTypePredicate(targetTypePredicate, signature.mapper!) : noTypePredicate; + } + else if (signature.unionSignatures) { + signature.resolvedTypePredicate = getUnionTypePredicate(signature.unionSignatures) || noTypePredicate; + } + else { + const type = signature.declaration && getEffectiveReturnTypeNode(signature.declaration); + let jsdocPredicate: TypePredicate | undefined; + if (!type && isInJSFile(signature.declaration)) { + const jsdocSignature = getSignatureOfTypeTag(signature.declaration!); + if (jsdocSignature && signature !== jsdocSignature) { + jsdocPredicate = getTypePredicateOfSignature(jsdocSignature); } - return type; } + signature.resolvedTypePredicate = type && isTypePredicateNode(type) ? + createTypePredicateFromTypePredicateNode(type, signature) : + jsdocPredicate || noTypePredicate; } - - function narrowTypeBySwitchOptionalChainContainment(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number, clauseCheck: (type: Type) => boolean) { - const everyClauseChecks = clauseStart !== clauseEnd && every(getSwitchClauseTypes(switchStatement).slice(clauseStart, clauseEnd), clauseCheck); - return everyClauseChecks ? getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type; + Debug.assert(!!signature.resolvedTypePredicate); + } + return signature.resolvedTypePredicate === noTypePredicate ? undefined : signature.resolvedTypePredicate; + } + function createTypePredicateFromTypePredicateNode(node: TypePredicateNode, signature: ts.Signature): TypePredicate { + const parameterName = node.parameterName; + const type = node.type && getTypeFromTypeNode(node.type); + return parameterName.kind === SyntaxKind.ThisType ? + createTypePredicate(node.assertsModifier ? TypePredicateKind.AssertsThis : TypePredicateKind.This, /*parameterName*/ undefined, /*parameterIndex*/ undefined, type) : + createTypePredicate(node.assertsModifier ? TypePredicateKind.AssertsIdentifier : TypePredicateKind.Identifier, (parameterName.escapedText as string), findIndex(signature.parameters, p => p.escapedName === parameterName.escapedText), type); + } + function getReturnTypeOfSignature(signature: ts.Signature): ts.Type { + if (!signature.resolvedReturnType) { + if (!pushTypeResolution(signature, TypeSystemPropertyName.ResolvedReturnType)) { + return errorType; } - - function narrowTypeBySwitchOnDiscriminant(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number) { - // We only narrow if all case expressions specify - // values with unit types, except for the case where - // `type` is unknown. In this instance we map object - // types to the nonPrimitive type and narrow with that. - const switchTypes = getSwitchClauseTypes(switchStatement); - if (!switchTypes.length) { - return type; - } - const clauseTypes = switchTypes.slice(clauseStart, clauseEnd); - const hasDefaultClause = clauseStart === clauseEnd || contains(clauseTypes, neverType); - if ((type.flags & TypeFlags.Unknown) && !hasDefaultClause) { - let groundClauseTypes: Type[] | undefined; - for (let i = 0; i < clauseTypes.length; i += 1) { - const t = clauseTypes[i]; - if (t.flags & (TypeFlags.Primitive | TypeFlags.NonPrimitive)) { - if (groundClauseTypes !== undefined) { - groundClauseTypes.push(t); - } - } - else if (t.flags & TypeFlags.Object) { - if (groundClauseTypes === undefined) { - groundClauseTypes = clauseTypes.slice(0, i); - } - groundClauseTypes.push(nonPrimitiveType); + let type = signature.target ? instantiateType(getReturnTypeOfSignature(signature.target), signature.mapper) : + signature.unionSignatures ? getUnionType(map(signature.unionSignatures, getReturnTypeOfSignature), UnionReduction.Subtype) : + getReturnTypeFromAnnotation(signature.declaration!) || + (nodeIsMissing((signature.declaration).body) ? anyType : getReturnTypeFromBody((signature.declaration))); + if (signature.flags & SignatureFlags.IsInnerCallChain) { + type = addOptionalTypeMarker(type); + } + else if (signature.flags & SignatureFlags.IsOuterCallChain) { + type = getOptionalType(type); + } + if (!popTypeResolution()) { + if (signature.declaration) { + const typeNode = getEffectiveReturnTypeNode(signature.declaration); + if (typeNode) { + error(typeNode, Diagnostics.Return_type_annotation_circularly_references_itself); + } + else if (noImplicitAny) { + const declaration = (signature.declaration); + const name = getNameOfDeclaration(declaration); + if (name) { + error(name, Diagnostics._0_implicitly_has_return_type_any_because_it_does_not_have_a_return_type_annotation_and_is_referenced_directly_or_indirectly_in_one_of_its_return_expressions, declarationNameToString(name)); } else { - return type; + error(declaration, Diagnostics.Function_implicitly_has_return_type_any_because_it_does_not_have_a_return_type_annotation_and_is_referenced_directly_or_indirectly_in_one_of_its_return_expressions); } } - return getUnionType(groundClauseTypes === undefined ? clauseTypes : groundClauseTypes); - } - const discriminantType = getUnionType(clauseTypes); - const caseType = - discriminantType.flags & TypeFlags.Never ? neverType : - replacePrimitivesWithLiterals(filterType(type, t => areTypesComparable(discriminantType, t)), discriminantType); - if (!hasDefaultClause) { - return caseType; } - const defaultType = filterType(type, t => !(isUnitType(t) && contains(switchTypes, getRegularTypeOfLiteralType(t)))); - return caseType.flags & TypeFlags.Never ? defaultType : getUnionType([caseType, defaultType]); + type = anyType; } - - function getImpliedTypeFromTypeofCase(type: Type, text: string) { - switch (text) { - case "function": - return type.flags & TypeFlags.Any ? type : globalFunctionType; - case "object": - return type.flags & TypeFlags.Unknown ? getUnionType([nonPrimitiveType, nullType]) : type; - default: - return typeofTypesByName.get(text) || type; - } + signature.resolvedReturnType = type; + } + return signature.resolvedReturnType; + } + function getReturnTypeFromAnnotation(declaration: SignatureDeclaration | JSDocSignature) { + if (declaration.kind === SyntaxKind.Constructor) { + return getDeclaredTypeOfClassOrInterface(getMergedSymbol((declaration.parent).symbol)); + } + if (isJSDocConstructSignature(declaration)) { + return getTypeFromTypeNode(((declaration.parameters[0] as ParameterDeclaration).type!)); // TODO: GH#18217 + } + const typeNode = getEffectiveReturnTypeNode(declaration); + if (typeNode) { + return getTypeFromTypeNode(typeNode); + } + if (declaration.kind === SyntaxKind.GetAccessor && !hasNonBindableDynamicName(declaration)) { + const jsDocType = isInJSFile(declaration) && getTypeForDeclarationFromJSDocComment(declaration); + if (jsDocType) { + return jsDocType; } - - function narrowTypeForTypeofSwitch(candidate: Type) { - return (type: Type) => { - if (isTypeSubtypeOf(candidate, type)) { - return candidate; - } - if (type.flags & TypeFlags.Instantiable) { - const constraint = getBaseConstraintOfType(type) || anyType; - if (isTypeSubtypeOf(candidate, constraint)) { - return getIntersectionType([type, candidate]); - } - } - return type; - }; + const setter = getDeclarationOfKind(getSymbolOfNode(declaration), SyntaxKind.SetAccessor); + const setterType = getAnnotatedAccessorType(setter); + if (setterType) { + return setterType; } - - function narrowBySwitchOnTypeOf(type: Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number): Type { - const switchWitnesses = getSwitchClauseTypeOfWitnesses(switchStatement); - if (!switchWitnesses.length) { - return type; - } - // Equal start and end denotes implicit fallthrough; undefined marks explicit default clause - const defaultCaseLocation = findIndex(switchWitnesses, elem => elem === undefined); - const hasDefaultClause = clauseStart === clauseEnd || (defaultCaseLocation >= clauseStart && defaultCaseLocation < clauseEnd); - let clauseWitnesses: string[]; - let switchFacts: TypeFacts; - if (defaultCaseLocation > -1) { - // We no longer need the undefined denoting an - // explicit default case. Remove the undefined and - // fix-up clauseStart and clauseEnd. This means - // that we don't have to worry about undefined - // in the witness array. - const witnesses = switchWitnesses.filter(witness => witness !== undefined); - // The adjusted clause start and end after removing the `default` statement. - const fixedClauseStart = defaultCaseLocation < clauseStart ? clauseStart - 1 : clauseStart; - const fixedClauseEnd = defaultCaseLocation < clauseEnd ? clauseEnd - 1 : clauseEnd; - clauseWitnesses = witnesses.slice(fixedClauseStart, fixedClauseEnd); - switchFacts = getFactsFromTypeofSwitch(fixedClauseStart, fixedClauseEnd, witnesses, hasDefaultClause); - } - else { - clauseWitnesses = switchWitnesses.slice(clauseStart, clauseEnd); - switchFacts = getFactsFromTypeofSwitch(clauseStart, clauseEnd, switchWitnesses, hasDefaultClause); - } - if (hasDefaultClause) { - return filterType(type, t => (getTypeFacts(t) & switchFacts) === switchFacts); - } - /* - The implied type is the raw type suggested by a - value being caught in this clause. - - When the clause contains a default case we ignore - the implied type and try to narrow using any facts - we can learn: see `switchFacts`. - - Example: - switch (typeof x) { - case 'number': - case 'string': break; - default: break; - case 'number': - case 'boolean': break - } - - In the first clause (case `number` and `string`) the - implied type is number | string. - - In the default clause we de not compute an implied type. - - In the third clause (case `number` and `boolean`) - the naive implied type is number | boolean, however - we use the type facts to narrow the implied type to - boolean. We know that number cannot be selected - because it is caught in the first clause. - */ - let impliedType = getTypeWithFacts(getUnionType(clauseWitnesses.map(text => getImpliedTypeFromTypeofCase(type, text))), switchFacts); - if (impliedType.flags & TypeFlags.Union) { - impliedType = getAssignmentReducedType(impliedType as UnionType, getBaseConstraintOrType(type)); + } + return getReturnTypeOfTypeTag(declaration); + } + function isResolvingReturnTypeOfSignature(signature: ts.Signature) { + return !signature.resolvedReturnType && findResolutionCycleStartIndex(signature, TypeSystemPropertyName.ResolvedReturnType) >= 0; + } + function getRestTypeOfSignature(signature: ts.Signature): ts.Type { + return tryGetRestTypeOfSignature(signature) || anyType; + } + function tryGetRestTypeOfSignature(signature: ts.Signature): ts.Type | undefined { + if (signatureHasRestParameter(signature)) { + const sigRestType = getTypeOfSymbol(signature.parameters[signature.parameters.length - 1]); + const restType = isTupleType(sigRestType) ? getRestTypeOfTupleType(sigRestType) : sigRestType; + return restType && getIndexTypeOfType(restType, IndexKind.Number); + } + return undefined; + } + function getSignatureInstantiation(signature: ts.Signature, typeArguments: ts.Type[] | undefined, isJavascript: boolean, inferredTypeParameters?: readonly TypeParameter[]): ts.Signature { + const instantiatedSignature = getSignatureInstantiationWithoutFillingInTypeArguments(signature, fillMissingTypeArguments(typeArguments, signature.typeParameters, getMinTypeArgumentCount(signature.typeParameters), isJavascript)); + if (inferredTypeParameters) { + const returnSignature = getSingleCallOrConstructSignature(getReturnTypeOfSignature(instantiatedSignature)); + if (returnSignature) { + const newReturnSignature = cloneSignature(returnSignature); + newReturnSignature.typeParameters = inferredTypeParameters; + const newInstantiatedSignature = cloneSignature(instantiatedSignature); + newInstantiatedSignature.resolvedReturnType = getOrCreateTypeFromSignature(newReturnSignature); + return newInstantiatedSignature; + } + } + return instantiatedSignature; + } + function getSignatureInstantiationWithoutFillingInTypeArguments(signature: ts.Signature, typeArguments: readonly ts.Type[] | undefined): ts.Signature { + const instantiations = signature.instantiations || (signature.instantiations = createMap()); + const id = getTypeListId(typeArguments); + let instantiation = instantiations.get(id); + if (!instantiation) { + instantiations.set(id, instantiation = createSignatureInstantiation(signature, typeArguments)); + } + return instantiation; + } + function createSignatureInstantiation(signature: ts.Signature, typeArguments: readonly ts.Type[] | undefined): ts.Signature { + return instantiateSignature(signature, createSignatureTypeMapper(signature, typeArguments), /*eraseTypeParameters*/ true); + } + function createSignatureTypeMapper(signature: ts.Signature, typeArguments: readonly ts.Type[] | undefined): TypeMapper { + return createTypeMapper(signature.typeParameters!, typeArguments); + } + function getErasedSignature(signature: ts.Signature): ts.Signature { + return signature.typeParameters ? + signature.erasedSignatureCache || (signature.erasedSignatureCache = createErasedSignature(signature)) : + signature; + } + function createErasedSignature(signature: ts.Signature) { + // Create an instantiation of the signature where all type arguments are the any type. + return instantiateSignature(signature, createTypeEraser(signature.typeParameters!), /*eraseTypeParameters*/ true); + } + function getCanonicalSignature(signature: ts.Signature): ts.Signature { + return signature.typeParameters ? + signature.canonicalSignatureCache || (signature.canonicalSignatureCache = createCanonicalSignature(signature)) : + signature; + } + function createCanonicalSignature(signature: ts.Signature) { + // Create an instantiation of the signature where each unconstrained type parameter is replaced with + // its original. When a generic class or interface is instantiated, each generic method in the class or + // interface is instantiated with a fresh set of cloned type parameters (which we need to handle scenarios + // where different generations of the same type parameter are in scope). This leads to a lot of new type + // identities, and potentially a lot of work comparing those identities, so here we create an instantiation + // that uses the original type identities for all unconstrained type parameters. + return getSignatureInstantiation(signature, map(signature.typeParameters, tp => tp.target && !getConstraintOfTypeParameter(tp.target) ? tp.target : tp), isInJSFile(signature.declaration)); + } + function getBaseSignature(signature: ts.Signature) { + const typeParameters = signature.typeParameters; + if (typeParameters) { + const typeEraser = createTypeEraser(typeParameters); + const baseConstraints = map(typeParameters, tp => instantiateType(getBaseConstraintOfType(tp), typeEraser) || unknownType); + return instantiateSignature(signature, createTypeMapper(typeParameters, baseConstraints), /*eraseTypeParameters*/ true); + } + return signature; + } + function getOrCreateTypeFromSignature(signature: ts.Signature): ObjectType { + // There are two ways to declare a construct signature, one is by declaring a class constructor + // using the constructor keyword, and the other is declaring a bare construct signature in an + // object type literal or interface (using the new keyword). Each way of declaring a constructor + // will result in a different declaration kind. + if (!signature.isolatedSignatureType) { + const kind = signature.declaration ? signature.declaration.kind : SyntaxKind.Unknown; + const isConstructor = kind === SyntaxKind.Constructor || kind === SyntaxKind.ConstructSignature || kind === SyntaxKind.ConstructorType; + const type = createObjectType(ObjectFlags.Anonymous); + type.members = emptySymbols; + type.properties = emptyArray; + type.callSignatures = !isConstructor ? [signature] : emptyArray; + type.constructSignatures = isConstructor ? [signature] : emptyArray; + signature.isolatedSignatureType = type; + } + return signature.isolatedSignatureType; + } + function getIndexSymbol(symbol: ts.Symbol): ts.Symbol | undefined { + return symbol.members!.get(InternalSymbolName.Index); + } + function getIndexDeclarationOfSymbol(symbol: ts.Symbol, kind: IndexKind): IndexSignatureDeclaration | undefined { + const syntaxKind = kind === IndexKind.Number ? SyntaxKind.NumberKeyword : SyntaxKind.StringKeyword; + const indexSymbol = getIndexSymbol(symbol); + if (indexSymbol) { + for (const decl of indexSymbol.declarations) { + const node = cast(decl, isIndexSignatureDeclaration); + if (node.parameters.length === 1) { + const parameter = node.parameters[0]; + if (parameter.type && parameter.type.kind === syntaxKind) { + return node; + } } - return getTypeWithFacts(mapType(type, narrowTypeForTypeofSwitch(impliedType)), switchFacts); } - - function narrowTypeByInstanceof(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type { - const left = getReferenceCandidate(expr.left); - if (!isMatchingReference(reference, left)) { - if (assumeTrue && strictNullChecks && optionalChainContainsReference(left, reference)) { - return getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull); + } + return undefined; + } + function createIndexInfo(type: ts.Type, isReadonly: boolean, declaration?: IndexSignatureDeclaration): IndexInfo { + return { type, isReadonly, declaration }; + } + function getIndexInfoOfSymbol(symbol: ts.Symbol, kind: IndexKind): IndexInfo | undefined { + const declaration = getIndexDeclarationOfSymbol(symbol, kind); + if (declaration) { + return createIndexInfo(declaration.type ? getTypeFromTypeNode(declaration.type) : anyType, hasModifier(declaration, ModifierFlags.Readonly), declaration); + } + return undefined; + } + function getConstraintDeclaration(type: TypeParameter): TypeNode | undefined { + return mapDefined(filter(type.symbol && type.symbol.declarations, isTypeParameterDeclaration), getEffectiveConstraintOfTypeParameter)[0]; + } + function getInferredTypeParameterConstraint(typeParameter: TypeParameter) { + let inferences: ts.Type[] | undefined; + if (typeParameter.symbol) { + for (const declaration of typeParameter.symbol.declarations) { + if (declaration.parent.kind === SyntaxKind.InferType) { + // When an 'infer T' declaration is immediately contained in a type reference node + // (such as 'Foo'), T's constraint is inferred from the constraint of the + // corresponding type parameter in 'Foo'. When multiple 'infer T' declarations are + // present, we form an intersection of the inferred constraint types. + const grandParent = declaration.parent.parent; + if (grandParent.kind === SyntaxKind.TypeReference) { + const typeReference = (grandParent); + const typeParameters = getTypeParametersForTypeReference(typeReference); + if (typeParameters) { + const index = typeReference.typeArguments!.indexOf((declaration.parent)); + if (index < typeParameters.length) { + const declaredConstraint = getConstraintOfTypeParameter(typeParameters[index]); + if (declaredConstraint) { + // Type parameter constraints can reference other type parameters so + // constraints need to be instantiated. If instantiation produces the + // type parameter itself, we discard that inference. For example, in + // type Foo = [T, U]; + // type Bar = T extends Foo ? Foo : T; + // the instantiated constraint for U is X, so we discard that inference. + const mapper = createTypeMapper(typeParameters, getEffectiveTypeArguments(typeReference, typeParameters)); + const constraint = instantiateType(declaredConstraint, mapper); + if (constraint !== typeParameter) { + inferences = append(inferences, constraint); + } + } + } + } } - // For a reference of the form 'x.y', an 'x instanceof T' type guard resets the - // narrowed type of 'y' to its declared type. We do this because preceding 'x.y' - // references might reference a different 'y' property. However, we make an exception - // for property accesses where x is a synthetic 'this' expression, indicating that we - // were called from isPropertyInitializedInConstructor. Without this exception, - // initializations of 'this' properties that occur before a 'this instanceof XXX' - // check would not be considered. - if (containsMatchingReference(reference, left) && !isSyntheticThisPropertyAccess(reference)) { - return declaredType; + // When an 'infer T' declaration is immediately contained in a rest parameter + // declaration, we infer an 'unknown[]' constraint. + else if (grandParent.kind === SyntaxKind.Parameter && (grandParent).dotDotDotToken) { + inferences = append(inferences, createArrayType(unknownType)); } - return type; } - - // Check that right operand is a function type with a prototype property - const rightType = getTypeOfExpression(expr.right); - if (!isTypeDerivedFrom(rightType, globalFunctionType)) { - return type; - } - - let targetType: Type | undefined; - const prototypeProperty = getPropertyOfType(rightType, "prototype" as __String); - if (prototypeProperty) { - // Target type is type of the prototype property - const prototypePropertyType = getTypeOfSymbol(prototypeProperty); - if (!isTypeAny(prototypePropertyType)) { - targetType = prototypePropertyType; - } + } + } + return inferences && getIntersectionType(inferences); + } + /** This is a worker function. Use getConstraintOfTypeParameter which guards against circular constraints. */ + function getConstraintFromTypeParameter(typeParameter: TypeParameter): ts.Type | undefined { + if (!typeParameter.constraint) { + if (typeParameter.target) { + const targetConstraint = getConstraintOfTypeParameter(typeParameter.target); + typeParameter.constraint = targetConstraint ? instantiateType(targetConstraint, typeParameter.mapper) : noConstraintType; + } + else { + const constraintDeclaration = getConstraintDeclaration(typeParameter); + typeParameter.constraint = constraintDeclaration ? getTypeFromTypeNode(constraintDeclaration) : + getInferredTypeParameterConstraint(typeParameter) || noConstraintType; + } + } + return typeParameter.constraint === noConstraintType ? undefined : typeParameter.constraint; + } + function getParentSymbolOfTypeParameter(typeParameter: TypeParameter): ts.Symbol | undefined { + const tp = (getDeclarationOfKind(typeParameter.symbol, SyntaxKind.TypeParameter)!); + const host = isJSDocTemplateTag(tp.parent) ? getHostSignatureFromJSDoc(tp.parent) : tp.parent; + return host && getSymbolOfNode(host); + } + function getTypeListId(types: readonly ts.Type[] | undefined) { + let result = ""; + if (types) { + const length = types.length; + let i = 0; + while (i < length) { + const startId = types[i].id; + let count = 1; + while (i + count < length && types[i + count].id === startId + count) { + count++; } - - // Don't narrow from 'any' if the target type is exactly 'Object' or 'Function' - if (isTypeAny(type) && (targetType === globalObjectType || targetType === globalFunctionType)) { - return type; + if (result.length) { + result += ","; } - - if (!targetType) { - const constructSignatures = getSignaturesOfType(rightType, SignatureKind.Construct); - targetType = constructSignatures.length ? - getUnionType(map(constructSignatures, signature => getReturnTypeOfSignature(getErasedSignature(signature)))) : - emptyObjectType; + result += startId; + if (count > 1) { + result += ":" + count; } - - return getNarrowedType(type, targetType, assumeTrue, isTypeDerivedFrom); + i += count; } - - function getNarrowedType(type: Type, candidate: Type, assumeTrue: boolean, isRelated: (source: Type, target: Type) => boolean) { - if (!assumeTrue) { - return filterType(type, t => !isRelated(t, candidate)); - } - // If the current type is a union type, remove all constituents that couldn't be instances of - // the candidate type. If one or more constituents remain, return a union of those. - if (type.flags & TypeFlags.Union) { - const assignableType = filterType(type, t => isRelated(t, candidate)); - if (!(assignableType.flags & TypeFlags.Never)) { - return assignableType; - } - } - // If the candidate type is a subtype of the target type, narrow to the candidate type. - // Otherwise, if the target type is assignable to the candidate type, keep the target type. - // Otherwise, if the candidate type is assignable to the target type, narrow to the candidate - // type. Otherwise, the types are completely unrelated, so narrow to an intersection of the - // two types. - return isTypeSubtypeOf(candidate, type) ? candidate : - isTypeAssignableTo(type, candidate) ? type : - isTypeAssignableTo(candidate, type) ? candidate : - getIntersectionType([type, candidate]); + } + return result; + } + // This function is used to propagate certain flags when creating new object type references and union types. + // It is only necessary to do so if a constituent type might be the undefined type, the null type, the type + // of an object literal or the anyFunctionType. This is because there are operations in the type checker + // that care about the presence of such types at arbitrary depth in a containing type. + function getPropagatingFlagsOfTypes(types: readonly ts.Type[], excludeKinds: TypeFlags): ObjectFlags { + let result: ObjectFlags = 0; + for (const type of types) { + if (!(type.flags & excludeKinds)) { + result |= getObjectFlags(type); + } + } + return result & ObjectFlags.PropagatingFlags; + } + function createTypeReference(target: GenericType, typeArguments: readonly ts.Type[] | undefined): TypeReference { + const id = getTypeListId(typeArguments); + let type = target.instantiations.get(id); + if (!type) { + type = (createObjectType(ObjectFlags.Reference, target.symbol)); + target.instantiations.set(id, type); + type.objectFlags |= typeArguments ? getPropagatingFlagsOfTypes(typeArguments, /*excludeKinds*/ 0) : 0; + type.target = target; + type.resolvedTypeArguments = typeArguments; + } + return type; + } + function cloneTypeReference(source: TypeReference): TypeReference { + const type = (createType(source.flags)); + type.symbol = source.symbol; + type.objectFlags = source.objectFlags; + type.target = source.target; + type.resolvedTypeArguments = source.resolvedTypeArguments; + return type; + } + function createDeferredTypeReference(target: GenericType, node: TypeReferenceNode | ArrayTypeNode | TupleTypeNode, mapper?: TypeMapper): DeferredTypeReference { + const aliasSymbol = getAliasSymbolForTypeNode(node); + const aliasTypeArguments = getTypeArgumentsForAliasSymbol(aliasSymbol); + const type = (createObjectType(ObjectFlags.Reference, target.symbol)); + type.target = target; + type.node = node; + type.mapper = mapper; + type.aliasSymbol = aliasSymbol; + type.aliasTypeArguments = mapper ? instantiateTypes(aliasTypeArguments, mapper) : aliasTypeArguments; + return type; + } + function getTypeArguments(type: TypeReference): readonly ts.Type[] { + if (!type.resolvedTypeArguments) { + if (!pushTypeResolution(type, TypeSystemPropertyName.ResolvedTypeArguments)) { + return type.target.localTypeParameters?.map(() => errorType) || emptyArray; + } + const node = type.node; + const typeArguments = !node ? emptyArray : + node.kind === SyntaxKind.TypeReference ? concatenate(type.target.outerTypeParameters, getEffectiveTypeArguments(node, type.target.localTypeParameters!)) : + node.kind === SyntaxKind.ArrayType ? [getTypeFromTypeNode(node.elementType)] : + map(node.elementTypes, getTypeFromTypeNode); + if (popTypeResolution()) { + type.resolvedTypeArguments = type.mapper ? instantiateTypes(typeArguments, type.mapper) : typeArguments; } - - function narrowTypeByCallExpression(type: Type, callExpression: CallExpression, assumeTrue: boolean): Type { - if (hasMatchingArgument(callExpression, reference)) { - const signature = assumeTrue || !isCallChain(callExpression) ? getEffectsSignature(callExpression) : undefined; - const predicate = signature && getTypePredicateOfSignature(signature); - if (predicate && (predicate.kind === TypePredicateKind.This || predicate.kind === TypePredicateKind.Identifier)) { - return narrowTypeByTypePredicate(type, predicate, callExpression, assumeTrue); - } + else { + type.resolvedTypeArguments = type.target.localTypeParameters?.map(() => errorType) || emptyArray; + error(type.node || currentNode, type.target.symbol + ? Diagnostics.Type_arguments_for_0_circularly_reference_themselves + : Diagnostics.Tuple_type_arguments_circularly_reference_themselves, type.target.symbol && symbolToString(type.target.symbol)); + } + } + return type.resolvedTypeArguments; + } + function getTypeReferenceArity(type: TypeReference): number { + return length(type.target.typeParameters); + } + /** + * Get type from type-reference that reference to class or interface + */ + function getTypeFromClassOrInterfaceReference(node: NodeWithTypeArguments, symbol: ts.Symbol): ts.Type { + const type = (getDeclaredTypeOfSymbol(getMergedSymbol(symbol))); + const typeParameters = type.localTypeParameters; + if (typeParameters) { + const numTypeArguments = length(node.typeArguments); + const minTypeArgumentCount = getMinTypeArgumentCount(typeParameters); + const isJs = isInJSFile(node); + const isJsImplicitAny = !noImplicitAny && isJs; + if (!isJsImplicitAny && (numTypeArguments < minTypeArgumentCount || numTypeArguments > typeParameters.length)) { + const missingAugmentsTag = isJs && isExpressionWithTypeArguments(node) && !isJSDocAugmentsTag(node.parent); + const diag = minTypeArgumentCount === typeParameters.length ? + missingAugmentsTag ? + Diagnostics.Expected_0_type_arguments_provide_these_with_an_extends_tag : + Diagnostics.Generic_type_0_requires_1_type_argument_s : + missingAugmentsTag ? + Diagnostics.Expected_0_1_type_arguments_provide_these_with_an_extends_tag : + Diagnostics.Generic_type_0_requires_between_1_and_2_type_arguments; + const typeStr = typeToString(type, /*enclosingDeclaration*/ undefined, TypeFormatFlags.WriteArrayAsGenericType); + error(node, diag, typeStr, minTypeArgumentCount, typeParameters.length); + if (!isJs) { + // TODO: Adopt same permissive behavior in TS as in JS to reduce follow-on editing experience failures (requires editing fillMissingTypeArguments) + return errorType; } - return type; } - - function narrowTypeByTypePredicate(type: Type, predicate: TypePredicate, callExpression: CallExpression, assumeTrue: boolean): Type { - // Don't narrow from 'any' if the predicate type is exactly 'Object' or 'Function' - if (predicate.type && !(isTypeAny(type) && (predicate.type === globalObjectType || predicate.type === globalFunctionType))) { - const predicateArgument = getTypePredicateArgument(predicate, callExpression); - if (predicateArgument) { - if (isMatchingReference(reference, predicateArgument)) { - return getNarrowedType(type, predicate.type, assumeTrue, isTypeSubtypeOf); - } - if (strictNullChecks && assumeTrue && optionalChainContainsReference(predicateArgument, reference) && - !(getTypeFacts(predicate.type) & TypeFacts.EQUndefined)) { - return getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull); - } - if (containsMatchingReference(reference, predicateArgument)) { - return declaredType; - } + if (node.kind === SyntaxKind.TypeReference && isDeferredTypeReferenceNode((node), length(node.typeArguments) !== typeParameters.length)) { + return createDeferredTypeReference((type), (node), /*mapper*/ undefined); + } + // In a type reference, the outer type parameters of the referenced class or interface are automatically + // supplied as type arguments and the type reference only specifies arguments for the local type parameters + // of the class or interface. + const typeArguments = concatenate(type.outerTypeParameters, fillMissingTypeArguments(typeArgumentsFromTypeReferenceNode(node), typeParameters, minTypeArgumentCount, isJs)); + return createTypeReference((type), typeArguments); + } + return checkNoTypeArguments(node, symbol) ? type : errorType; + } + function getTypeAliasInstantiation(symbol: ts.Symbol, typeArguments: readonly ts.Type[] | undefined): ts.Type { + const type = getDeclaredTypeOfSymbol(symbol); + const links = getSymbolLinks(symbol); + const typeParameters = links.typeParameters!; + const id = getTypeListId(typeArguments); + let instantiation = links.instantiations!.get(id); + if (!instantiation) { + links.instantiations!.set(id, instantiation = instantiateType(type, createTypeMapper(typeParameters, fillMissingTypeArguments(typeArguments, typeParameters, getMinTypeArgumentCount(typeParameters), isInJSFile(symbol.valueDeclaration))))); + } + return instantiation; + } + /** + * Get type from reference to type alias. When a type alias is generic, the declared type of the type alias may include + * references to the type parameters of the alias. We replace those with the actual type arguments by instantiating the + * declared type. Instantiations are cached using the type identities of the type arguments as the key. + */ + function getTypeFromTypeAliasReference(node: NodeWithTypeArguments, symbol: ts.Symbol): ts.Type { + const type = getDeclaredTypeOfSymbol(symbol); + const typeParameters = getSymbolLinks(symbol).typeParameters; + if (typeParameters) { + const numTypeArguments = length(node.typeArguments); + const minTypeArgumentCount = getMinTypeArgumentCount(typeParameters); + if (numTypeArguments < minTypeArgumentCount || numTypeArguments > typeParameters.length) { + error(node, minTypeArgumentCount === typeParameters.length ? + Diagnostics.Generic_type_0_requires_1_type_argument_s : + Diagnostics.Generic_type_0_requires_between_1_and_2_type_arguments, symbolToString(symbol), minTypeArgumentCount, typeParameters.length); + return errorType; + } + return getTypeAliasInstantiation(symbol, typeArgumentsFromTypeReferenceNode(node)); + } + return checkNoTypeArguments(node, symbol) ? type : errorType; + } + function getTypeReferenceName(node: TypeReferenceType): EntityNameOrEntityNameExpression | undefined { + switch (node.kind) { + case SyntaxKind.TypeReference: + return node.typeName; + case SyntaxKind.ExpressionWithTypeArguments: + // We only support expressions that are simple qualified names. For other + // expressions this produces undefined. + const expr = node.expression; + if (isEntityNameExpression(expr)) { + return expr; + } + // fall through; + } + return undefined; + } + function resolveTypeReferenceName(typeReferenceName: EntityNameExpression | EntityName | undefined, meaning: SymbolFlags, ignoreErrors?: boolean) { + if (!typeReferenceName) { + return unknownSymbol; + } + return resolveEntityName(typeReferenceName, meaning, ignoreErrors) || unknownSymbol; + } + function getTypeReferenceType(node: NodeWithTypeArguments, symbol: ts.Symbol): ts.Type { + if (symbol === unknownSymbol) { + return errorType; + } + symbol = getExpandoSymbol(symbol) || symbol; + if (symbol.flags & (SymbolFlags.Class | SymbolFlags.Interface)) { + return getTypeFromClassOrInterfaceReference(node, symbol); + } + if (symbol.flags & SymbolFlags.TypeAlias) { + return getTypeFromTypeAliasReference(node, symbol); + } + // Get type from reference to named type that cannot be generic (enum or type parameter) + const res = tryGetDeclaredTypeOfSymbol(symbol); + if (res) { + return checkNoTypeArguments(node, symbol) ? + res.flags & TypeFlags.TypeParameter ? getConstrainedTypeVariable((res), node) : getRegularTypeOfLiteralType(res) : + errorType; + } + if (symbol.flags & SymbolFlags.Value && isJSDocTypeReference(node)) { + const jsdocType = getTypeFromJSDocValueReference(node, symbol); + if (jsdocType) { + return jsdocType; + } + else { + // Resolve the type reference as a Type for the purpose of reporting errors. + resolveTypeReferenceName(getTypeReferenceName(node), SymbolFlags.Type); + return getTypeOfSymbol(symbol); + } + } + return errorType; + } + /** + * A JSdoc TypeReference may be to a value, but resolve it as a type anyway. + * Note: If the value is imported from commonjs, it should really be an alias, + * but this function's special-case code fakes alias resolution as well. + */ + function getTypeFromJSDocValueReference(node: NodeWithTypeArguments, symbol: ts.Symbol): ts.Type | undefined { + const links = getNodeLinks(node); + if (!links.resolvedJSDocType) { + const valueType = getTypeOfSymbol(symbol); + let typeType = valueType; + if (symbol.valueDeclaration) { + const decl = getRootDeclaration(symbol.valueDeclaration); + let isRequireAlias = false; + if (isVariableDeclaration(decl) && decl.initializer) { + let expr = decl.initializer; + // skip past entity names, eg `require("x").a.b.c` + while (isPropertyAccessExpression(expr)) { + expr = expr.expression; } + isRequireAlias = isCallExpression(expr) && isRequireCall(expr, /*requireStringLiteralLikeArgument*/ true) && !!valueType.symbol; } - return type; - } - - // Narrow the given type based on the given expression having the assumed boolean value. The returned type - // will be a subtype or the same type as the argument. - function narrowType(type: Type, expr: Expression, assumeTrue: boolean): Type { - // for `a?.b`, we emulate a synthetic `a !== null && a !== undefined` condition for `a` - if (isExpressionOfOptionalChainRoot(expr) || - isBinaryExpression(expr.parent) && expr.parent.operatorToken.kind === SyntaxKind.QuestionQuestionToken && expr.parent.left === expr) { - return narrowTypeByOptionality(type, expr, assumeTrue); - } - switch (expr.kind) { - case SyntaxKind.Identifier: - case SyntaxKind.ThisKeyword: - case SyntaxKind.SuperKeyword: - case SyntaxKind.PropertyAccessExpression: - case SyntaxKind.ElementAccessExpression: - return narrowTypeByTruthiness(type, expr, assumeTrue); - case SyntaxKind.CallExpression: - return narrowTypeByCallExpression(type, expr, assumeTrue); - case SyntaxKind.ParenthesizedExpression: - return narrowType(type, (expr).expression, assumeTrue); - case SyntaxKind.BinaryExpression: - return narrowTypeByBinaryExpression(type, expr, assumeTrue); - case SyntaxKind.PrefixUnaryExpression: - if ((expr).operator === SyntaxKind.ExclamationToken) { - return narrowType(type, (expr).operand, !assumeTrue); - } - break; + const isImportTypeWithQualifier = node.kind === SyntaxKind.ImportType && (node as ImportTypeNode).qualifier; + // valueType might not have a symbol, eg, {import('./b').STRING_LITERAL} + if (valueType.symbol && (isRequireAlias || isImportTypeWithQualifier)) { + typeType = getTypeReferenceType(node, valueType.symbol); } - return type; } - - function narrowTypeByOptionality(type: Type, expr: Expression, assumePresent: boolean): Type { - if (isMatchingReference(reference, expr)) { - return getTypeWithFacts(type, assumePresent ? TypeFacts.NEUndefinedOrNull : TypeFacts.EQUndefinedOrNull); - } - if (isMatchingReferenceDiscriminant(expr, declaredType)) { - return narrowTypeByDiscriminant(type, expr, t => getTypeWithFacts(t, assumePresent ? TypeFacts.NEUndefinedOrNull : TypeFacts.EQUndefinedOrNull)); + links.resolvedJSDocType = typeType; + } + return links.resolvedJSDocType; + } + function getSubstitutionType(typeVariable: TypeVariable, substitute: ts.Type) { + if (substitute.flags & TypeFlags.AnyOrUnknown || substitute === typeVariable) { + return typeVariable; + } + const id = `${getTypeId(typeVariable)}>${getTypeId(substitute)}`; + const cached = substitutionTypes.get(id); + if (cached) { + return cached; + } + const result = (createType(TypeFlags.Substitution)); + result.typeVariable = typeVariable; + result.substitute = substitute; + substitutionTypes.set(id, result); + return result; + } + function isUnaryTupleTypeNode(node: TypeNode) { + return node.kind === SyntaxKind.TupleType && (node).elementTypes.length === 1; + } + function getImpliedConstraint(typeVariable: TypeVariable, checkNode: TypeNode, extendsNode: TypeNode): ts.Type | undefined { + return isUnaryTupleTypeNode(checkNode) && isUnaryTupleTypeNode(extendsNode) ? getImpliedConstraint(typeVariable, (checkNode).elementTypes[0], (extendsNode).elementTypes[0]) : + getActualTypeVariable(getTypeFromTypeNode(checkNode)) === typeVariable ? getTypeFromTypeNode(extendsNode) : + undefined; + } + function getConstrainedTypeVariable(typeVariable: TypeVariable, node: Node) { + let constraints: ts.Type[] | undefined; + while (node && !isStatement(node) && node.kind !== SyntaxKind.JSDocComment) { + const parent = node.parent; + if (parent.kind === SyntaxKind.ConditionalType && node === (parent).trueType) { + const constraint = getImpliedConstraint(typeVariable, (parent).checkType, (parent).extendsType); + if (constraint) { + constraints = append(constraints, constraint); } - return type; } + node = parent; } - - function getTypeOfSymbolAtLocation(symbol: Symbol, location: Node) { - symbol = symbol.exportSymbol || symbol; - - // If we have an identifier or a property access at the given location, if the location is - // an dotted name expression, and if the location is not an assignment target, obtain the type - // of the expression (which will reflect control flow analysis). If the expression indeed - // resolved to the given symbol, return the narrowed type. - if (location.kind === SyntaxKind.Identifier) { - if (isRightSideOfQualifiedNameOrPropertyAccess(location)) { - location = location.parent; - } - if (isExpressionNode(location) && !isAssignmentTarget(location)) { - const type = getTypeOfExpression(location); - if (getExportSymbolOfValueSymbolIfExported(getNodeLinks(location).resolvedSymbol) === symbol) { - return type; + return constraints ? getSubstitutionType(typeVariable, getIntersectionType(append(constraints, typeVariable))) : typeVariable; + } + function isJSDocTypeReference(node: Node): node is TypeReferenceNode { + return !!(node.flags & NodeFlags.JSDoc) && (node.kind === SyntaxKind.TypeReference || node.kind === SyntaxKind.ImportType); + } + function checkNoTypeArguments(node: NodeWithTypeArguments, symbol?: ts.Symbol) { + if (node.typeArguments) { + error(node, Diagnostics.Type_0_is_not_generic, symbol ? symbolToString(symbol) : (node).typeName ? declarationNameToString((node).typeName) : anon); + return false; + } + return true; + } + function getIntendedTypeFromJSDocTypeReference(node: TypeReferenceNode): ts.Type | undefined { + if (isIdentifier(node.typeName)) { + const typeArgs = node.typeArguments; + switch (node.typeName.escapedText) { + case "String": + checkNoTypeArguments(node); + return stringType; + case "Number": + checkNoTypeArguments(node); + return numberType; + case "Boolean": + checkNoTypeArguments(node); + return booleanType; + case "Void": + checkNoTypeArguments(node); + return voidType; + case "Undefined": + checkNoTypeArguments(node); + return undefinedType; + case "Null": + checkNoTypeArguments(node); + return nullType; + case "Function": + case "function": + checkNoTypeArguments(node); + return globalFunctionType; + case "array": + return (!typeArgs || !typeArgs.length) && !noImplicitAny ? anyArrayType : undefined; + case "promise": + return (!typeArgs || !typeArgs.length) && !noImplicitAny ? createPromiseType(anyType) : undefined; + case "Object": + if (typeArgs && typeArgs.length === 2) { + if (isJSDocIndexSignature(node)) { + const indexed = getTypeFromTypeNode(typeArgs[0]); + const target = getTypeFromTypeNode(typeArgs[1]); + const index = createIndexInfo(target, /*isReadonly*/ false); + return createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, indexed === stringType ? index : undefined, indexed === numberType ? index : undefined); + } + return anyType; + } + checkNoTypeArguments(node); + return !noImplicitAny ? anyType : undefined; + } + } + } + function getTypeFromJSDocNullableTypeNode(node: JSDocNullableType) { + const type = getTypeFromTypeNode(node.type); + return strictNullChecks ? getNullableType(type, TypeFlags.Null) : type; + } + function getTypeFromTypeReference(node: TypeReferenceType): ts.Type { + const links = getNodeLinks(node); + if (!links.resolvedType) { + // handle LS queries on the `const` in `x as const` by resolving to the type of `x` + if (isConstTypeReference(node) && isAssertionExpression(node.parent)) { + links.resolvedSymbol = unknownSymbol; + return links.resolvedType = checkExpressionCached(node.parent.expression); + } + let symbol: ts.Symbol | undefined; + let type: ts.Type | undefined; + const meaning = SymbolFlags.Type; + if (isJSDocTypeReference(node)) { + type = getIntendedTypeFromJSDocTypeReference(node); + if (!type) { + symbol = resolveTypeReferenceName(getTypeReferenceName(node), meaning, /*ignoreErrors*/ true); + if (symbol === unknownSymbol) { + symbol = resolveTypeReferenceName(getTypeReferenceName(node), meaning | SymbolFlags.Value); + } + else { + resolveTypeReferenceName(getTypeReferenceName(node), meaning); // Resolve again to mark errors, if any } + type = getTypeReferenceType(node, symbol); } } - // The location isn't a reference to the given symbol, meaning we're being asked - // a hypothetical question of what type the symbol would have if there was a reference - // to it at the given location. Since we have no control flow information for the - // hypothetical reference (control flow information is created and attached by the - // binder), we simply return the declared type of the symbol. - return getTypeOfSymbol(symbol); + if (!type) { + symbol = resolveTypeReferenceName(getTypeReferenceName(node), meaning); + type = getTypeReferenceType(node, symbol); + } + // Cache both the resolved symbol and the resolved type. The resolved symbol is needed when we check the + // type reference in checkTypeReferenceNode. + links.resolvedSymbol = symbol; + links.resolvedType = type; } - - function getControlFlowContainer(node: Node): Node { - return findAncestor(node.parent, node => - isFunctionLike(node) && !getImmediatelyInvokedFunctionExpression(node) || - node.kind === SyntaxKind.ModuleBlock || - node.kind === SyntaxKind.SourceFile || - node.kind === SyntaxKind.PropertyDeclaration)!; + return links.resolvedType; + } + function typeArgumentsFromTypeReferenceNode(node: NodeWithTypeArguments): ts.Type[] | undefined { + return map(node.typeArguments, getTypeFromTypeNode); + } + function getTypeFromTypeQueryNode(node: TypeQueryNode): ts.Type { + const links = getNodeLinks(node); + if (!links.resolvedType) { + // TypeScript 1.0 spec (April 2014): 3.6.3 + // The expression is processed as an identifier expression (section 4.3) + // or property access expression(section 4.10), + // the widened type(section 3.9) of which becomes the result. + links.resolvedType = getRegularTypeOfLiteralType(getWidenedType(checkExpression(node.exprName))); } - - // Check if a parameter is assigned anywhere within its declaring function. - function isParameterAssigned(symbol: Symbol) { - const func = getRootDeclaration(symbol.valueDeclaration).parent; - const links = getNodeLinks(func); - if (!(links.flags & NodeCheckFlags.AssignmentsMarked)) { - links.flags |= NodeCheckFlags.AssignmentsMarked; - if (!hasParentWithAssignmentsMarked(func)) { - markParameterAssignments(func); + return links.resolvedType; + } + function getTypeOfGlobalSymbol(symbol: ts.Symbol | undefined, arity: number): ObjectType { + function getTypeDeclaration(symbol: ts.Symbol): Declaration | undefined { + const declarations = symbol.declarations; + for (const declaration of declarations) { + switch (declaration.kind) { + case SyntaxKind.ClassDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.EnumDeclaration: + return declaration; } } - return symbol.isAssigned || false; } - - function hasParentWithAssignmentsMarked(node: Node) { - return !!findAncestor(node.parent, node => isFunctionLike(node) && !!(getNodeLinks(node).flags & NodeCheckFlags.AssignmentsMarked)); + if (!symbol) { + return arity ? emptyGenericType : emptyObjectType; } - - function markParameterAssignments(node: Node) { - if (node.kind === SyntaxKind.Identifier) { - if (isAssignmentTarget(node)) { - const symbol = getResolvedSymbol(node); - if (symbol.valueDeclaration && getRootDeclaration(symbol.valueDeclaration).kind === SyntaxKind.Parameter) { - symbol.isAssigned = true; - } - } - } - else { - forEachChild(node, markParameterAssignments); - } + const type = getDeclaredTypeOfSymbol(symbol); + if (!(type.flags & TypeFlags.Object)) { + error(getTypeDeclaration(symbol), Diagnostics.Global_type_0_must_be_a_class_or_interface_type, symbolName(symbol)); + return arity ? emptyGenericType : emptyObjectType; } - - function isConstVariable(symbol: Symbol) { - return symbol.flags & SymbolFlags.Variable && (getDeclarationNodeFlagsFromSymbol(symbol) & NodeFlags.Const) !== 0 && getTypeOfSymbol(symbol) !== autoArrayType; + if (length((type).typeParameters) !== arity) { + error(getTypeDeclaration(symbol), Diagnostics.Global_type_0_must_have_1_type_parameter_s, symbolName(symbol), arity); + return arity ? emptyGenericType : emptyObjectType; } - - /** remove undefined from the annotated type of a parameter when there is an initializer (that doesn't include undefined) */ - function removeOptionalityFromDeclaredType(declaredType: Type, declaration: VariableLikeDeclaration): Type { - if (pushTypeResolution(declaration.symbol, TypeSystemPropertyName.DeclaredType)) { - const annotationIncludesUndefined = strictNullChecks && - declaration.kind === SyntaxKind.Parameter && - declaration.initializer && - getFalsyFlags(declaredType) & TypeFlags.Undefined && - !(getFalsyFlags(checkExpression(declaration.initializer)) & TypeFlags.Undefined); - popTypeResolution(); - - return annotationIncludesUndefined ? getTypeWithFacts(declaredType, TypeFacts.NEUndefined) : declaredType; + return type; + } + function getGlobalValueSymbol(name: __String, reportErrors: boolean): ts.Symbol | undefined { + return getGlobalSymbol(name, SymbolFlags.Value, reportErrors ? Diagnostics.Cannot_find_global_value_0 : undefined); + } + function getGlobalTypeSymbol(name: __String, reportErrors: boolean): ts.Symbol | undefined { + return getGlobalSymbol(name, SymbolFlags.Type, reportErrors ? Diagnostics.Cannot_find_global_type_0 : undefined); + } + function getGlobalSymbol(name: __String, meaning: SymbolFlags, diagnostic: DiagnosticMessage | undefined): ts.Symbol | undefined { + // Don't track references for global symbols anyway, so value if `isReference` is arbitrary + return resolveName(undefined, name, meaning, diagnostic, name, /*isUse*/ false); + } + function getGlobalType(name: __String, arity: 0, reportErrors: boolean): ObjectType; + function getGlobalType(name: __String, arity: number, reportErrors: boolean): GenericType; + function getGlobalType(name: __String, arity: number, reportErrors: boolean): ObjectType | undefined { + const symbol = getGlobalTypeSymbol(name, reportErrors); + return symbol || reportErrors ? getTypeOfGlobalSymbol(symbol, arity) : undefined; + } + function getGlobalTypedPropertyDescriptorType() { + return deferredGlobalTypedPropertyDescriptorType || (deferredGlobalTypedPropertyDescriptorType = getGlobalType(("TypedPropertyDescriptor" as __String), /*arity*/ 1, /*reportErrors*/ true)) || emptyGenericType; + } + function getGlobalTemplateStringsArrayType() { + return deferredGlobalTemplateStringsArrayType || (deferredGlobalTemplateStringsArrayType = getGlobalType(("TemplateStringsArray" as __String), /*arity*/ 0, /*reportErrors*/ true)) || emptyObjectType; + } + function getGlobalImportMetaType() { + return deferredGlobalImportMetaType || (deferredGlobalImportMetaType = getGlobalType(("ImportMeta" as __String), /*arity*/ 0, /*reportErrors*/ true)) || emptyObjectType; + } + function getGlobalESSymbolConstructorSymbol(reportErrors: boolean) { + return deferredGlobalESSymbolConstructorSymbol || (deferredGlobalESSymbolConstructorSymbol = getGlobalValueSymbol(("Symbol" as __String), reportErrors)); + } + function getGlobalESSymbolType(reportErrors: boolean) { + return deferredGlobalESSymbolType || (deferredGlobalESSymbolType = getGlobalType(("Symbol" as __String), /*arity*/ 0, reportErrors)) || emptyObjectType; + } + function getGlobalPromiseType(reportErrors: boolean) { + return deferredGlobalPromiseType || (deferredGlobalPromiseType = getGlobalType(("Promise" as __String), /*arity*/ 1, reportErrors)) || emptyGenericType; + } + function getGlobalPromiseLikeType(reportErrors: boolean) { + return deferredGlobalPromiseLikeType || (deferredGlobalPromiseLikeType = getGlobalType(("PromiseLike" as __String), /*arity*/ 1, reportErrors)) || emptyGenericType; + } + function getGlobalPromiseConstructorSymbol(reportErrors: boolean): ts.Symbol | undefined { + return deferredGlobalPromiseConstructorSymbol || (deferredGlobalPromiseConstructorSymbol = getGlobalValueSymbol(("Promise" as __String), reportErrors)); + } + function getGlobalPromiseConstructorLikeType(reportErrors: boolean) { + return deferredGlobalPromiseConstructorLikeType || (deferredGlobalPromiseConstructorLikeType = getGlobalType(("PromiseConstructorLike" as __String), /*arity*/ 0, reportErrors)) || emptyObjectType; + } + function getGlobalAsyncIterableType(reportErrors: boolean) { + return deferredGlobalAsyncIterableType || (deferredGlobalAsyncIterableType = getGlobalType(("AsyncIterable" as __String), /*arity*/ 1, reportErrors)) || emptyGenericType; + } + function getGlobalAsyncIteratorType(reportErrors: boolean) { + return deferredGlobalAsyncIteratorType || (deferredGlobalAsyncIteratorType = getGlobalType(("AsyncIterator" as __String), /*arity*/ 3, reportErrors)) || emptyGenericType; + } + function getGlobalAsyncIterableIteratorType(reportErrors: boolean) { + return deferredGlobalAsyncIterableIteratorType || (deferredGlobalAsyncIterableIteratorType = getGlobalType(("AsyncIterableIterator" as __String), /*arity*/ 1, reportErrors)) || emptyGenericType; + } + function getGlobalAsyncGeneratorType(reportErrors: boolean) { + return deferredGlobalAsyncGeneratorType || (deferredGlobalAsyncGeneratorType = getGlobalType(("AsyncGenerator" as __String), /*arity*/ 3, reportErrors)) || emptyGenericType; + } + function getGlobalIterableType(reportErrors: boolean) { + return deferredGlobalIterableType || (deferredGlobalIterableType = getGlobalType(("Iterable" as __String), /*arity*/ 1, reportErrors)) || emptyGenericType; + } + function getGlobalIteratorType(reportErrors: boolean) { + return deferredGlobalIteratorType || (deferredGlobalIteratorType = getGlobalType(("Iterator" as __String), /*arity*/ 3, reportErrors)) || emptyGenericType; + } + function getGlobalIterableIteratorType(reportErrors: boolean) { + return deferredGlobalIterableIteratorType || (deferredGlobalIterableIteratorType = getGlobalType(("IterableIterator" as __String), /*arity*/ 1, reportErrors)) || emptyGenericType; + } + function getGlobalGeneratorType(reportErrors: boolean) { + return deferredGlobalGeneratorType || (deferredGlobalGeneratorType = getGlobalType(("Generator" as __String), /*arity*/ 3, reportErrors)) || emptyGenericType; + } + function getGlobalIteratorYieldResultType(reportErrors: boolean) { + return deferredGlobalIteratorYieldResultType || (deferredGlobalIteratorYieldResultType = getGlobalType(("IteratorYieldResult" as __String), /*arity*/ 1, reportErrors)) || emptyGenericType; + } + function getGlobalIteratorReturnResultType(reportErrors: boolean) { + return deferredGlobalIteratorReturnResultType || (deferredGlobalIteratorReturnResultType = getGlobalType(("IteratorReturnResult" as __String), /*arity*/ 1, reportErrors)) || emptyGenericType; + } + function getGlobalTypeOrUndefined(name: __String, arity = 0): ObjectType | undefined { + const symbol = getGlobalSymbol(name, SymbolFlags.Type, /*diagnostic*/ undefined); + return symbol && (getTypeOfGlobalSymbol(symbol, arity)); + } + function getGlobalExtractSymbol(): ts.Symbol { + return deferredGlobalExtractSymbol || (deferredGlobalExtractSymbol = (getGlobalSymbol(("Extract" as __String), SymbolFlags.TypeAlias, Diagnostics.Cannot_find_global_type_0)!)); // TODO: GH#18217 + } + function getGlobalOmitSymbol(): ts.Symbol { + return deferredGlobalOmitSymbol || (deferredGlobalOmitSymbol = (getGlobalSymbol(("Omit" as __String), SymbolFlags.TypeAlias, Diagnostics.Cannot_find_global_type_0)!)); // TODO: GH#18217 + } + function getGlobalBigIntType(reportErrors: boolean) { + return deferredGlobalBigIntType || (deferredGlobalBigIntType = getGlobalType(("BigInt" as __String), /*arity*/ 0, reportErrors)) || emptyObjectType; + } + /** + * Instantiates a global type that is generic with some element type, and returns that instantiation. + */ + function createTypeFromGenericGlobalType(genericGlobalType: GenericType, typeArguments: readonly ts.Type[]): ObjectType { + return genericGlobalType !== emptyGenericType ? createTypeReference(genericGlobalType, typeArguments) : emptyObjectType; + } + function createTypedPropertyDescriptorType(propertyType: ts.Type): ts.Type { + return createTypeFromGenericGlobalType(getGlobalTypedPropertyDescriptorType(), [propertyType]); + } + function createIterableType(iteratedType: ts.Type): ts.Type { + return createTypeFromGenericGlobalType(getGlobalIterableType(/*reportErrors*/ true), [iteratedType]); + } + function createArrayType(elementType: ts.Type, readonly?: boolean): ObjectType { + return createTypeFromGenericGlobalType(readonly ? globalReadonlyArrayType : globalArrayType, [elementType]); + } + function getArrayOrTupleTargetType(node: ArrayTypeNode | TupleTypeNode): GenericType { + const readonly = isReadonlyTypeOperator(node.parent); + if (node.kind === SyntaxKind.ArrayType || node.elementTypes.length === 1 && node.elementTypes[0].kind === SyntaxKind.RestType) { + return readonly ? globalReadonlyArrayType : globalArrayType; + } + const lastElement = lastOrUndefined(node.elementTypes); + const restElement = lastElement && lastElement.kind === SyntaxKind.RestType ? lastElement : undefined; + const minLength = findLastIndex(node.elementTypes, n => n.kind !== SyntaxKind.OptionalType && n !== restElement) + 1; + return getTupleTypeOfArity(node.elementTypes.length, minLength, !!restElement, readonly, /*associatedNames*/ undefined); + } + // Return true if the given type reference node is directly aliased or if it needs to be deferred + // because it is possibly contained in a circular chain of eagerly resolved types. + function isDeferredTypeReferenceNode(node: TypeReferenceNode | ArrayTypeNode | TupleTypeNode, hasDefaultTypeArguments?: boolean) { + return !!getAliasSymbolForTypeNode(node) || isResolvedByTypeAlias(node) && (node.kind === SyntaxKind.ArrayType ? mayResolveTypeAlias(node.elementType) : + node.kind === SyntaxKind.TupleType ? some(node.elementTypes, mayResolveTypeAlias) : + hasDefaultTypeArguments || some(node.typeArguments, mayResolveTypeAlias)); + } + // Return true when the given node is transitively contained in type constructs that eagerly + // resolve their constituent types. We include SyntaxKind.TypeReference because type arguments + // of type aliases are eagerly resolved. + function isResolvedByTypeAlias(node: Node): boolean { + const parent = node.parent; + switch (parent.kind) { + case SyntaxKind.ParenthesizedType: + case SyntaxKind.TypeReference: + case SyntaxKind.UnionType: + case SyntaxKind.IntersectionType: + case SyntaxKind.IndexedAccessType: + case SyntaxKind.ConditionalType: + case SyntaxKind.TypeOperator: + return isResolvedByTypeAlias(parent); + case SyntaxKind.TypeAliasDeclaration: + return true; + } + return false; + } + // Return true if resolving the given node (i.e. getTypeFromTypeNode) possibly causes resolution + // of a type alias. + function mayResolveTypeAlias(node: Node): boolean { + switch (node.kind) { + case SyntaxKind.TypeReference: + return isJSDocTypeReference(node) || !!(resolveTypeReferenceName((node).typeName, SymbolFlags.Type).flags & SymbolFlags.TypeAlias); + case SyntaxKind.TypeQuery: + return true; + case SyntaxKind.TypeOperator: + return (node).operator !== SyntaxKind.UniqueKeyword && mayResolveTypeAlias((node).type); + case SyntaxKind.ParenthesizedType: + case SyntaxKind.OptionalType: + case SyntaxKind.JSDocOptionalType: + case SyntaxKind.JSDocNullableType: + case SyntaxKind.JSDocNonNullableType: + case SyntaxKind.JSDocTypeExpression: + return mayResolveTypeAlias((node).type); + case SyntaxKind.RestType: + return (node).type.kind !== SyntaxKind.ArrayType || mayResolveTypeAlias(((node).type).elementType); + case SyntaxKind.UnionType: + case SyntaxKind.IntersectionType: + return some((node).types, mayResolveTypeAlias); + case SyntaxKind.IndexedAccessType: + return mayResolveTypeAlias((node).objectType) || mayResolveTypeAlias((node).indexType); + case SyntaxKind.ConditionalType: + return mayResolveTypeAlias((node).checkType) || mayResolveTypeAlias((node).extendsType) || + mayResolveTypeAlias((node).trueType) || mayResolveTypeAlias((node).falseType); + } + return false; + } + function getTypeFromArrayOrTupleTypeNode(node: ArrayTypeNode | TupleTypeNode): ts.Type { + const links = getNodeLinks(node); + if (!links.resolvedType) { + const target = getArrayOrTupleTargetType(node); + if (target === emptyGenericType) { + links.resolvedType = emptyObjectType; + } + else if (isDeferredTypeReferenceNode(node)) { + links.resolvedType = node.kind === SyntaxKind.TupleType && node.elementTypes.length === 0 ? target : + createDeferredTypeReference(target, node, /*mapper*/ undefined); } else { - reportCircularityError(declaration.symbol); - return declaredType; + const elementTypes = node.kind === SyntaxKind.ArrayType ? [getTypeFromTypeNode(node.elementType)] : map(node.elementTypes, getTypeFromTypeNode); + links.resolvedType = createTypeReference(target, elementTypes); } } - - function isConstraintPosition(node: Node) { - const parent = node.parent; - return parent.kind === SyntaxKind.PropertyAccessExpression || - parent.kind === SyntaxKind.CallExpression && (parent).expression === node || - parent.kind === SyntaxKind.ElementAccessExpression && (parent).expression === node || - parent.kind === SyntaxKind.BindingElement && (parent).name === node && !!(parent).initializer; + return links.resolvedType; + } + function isReadonlyTypeOperator(node: Node) { + return isTypeOperatorNode(node) && node.operator === SyntaxKind.ReadonlyKeyword; + } + // We represent tuple types as type references to synthesized generic interface types created by + // this function. The types are of the form: + // + // interface Tuple extends Array { 0: T0, 1: T1, 2: T2, ... } + // + // Note that the generic type created by this function has no symbol associated with it. The same + // is true for each of the synthesized type parameters. + function createTupleTypeOfArity(arity: number, minLength: number, hasRestElement: boolean, readonly: boolean, associatedNames: __String[] | undefined): TupleType { + let typeParameters: TypeParameter[] | undefined; + const properties: ts.Symbol[] = []; + const maxLength = hasRestElement ? arity - 1 : arity; + if (arity) { + typeParameters = new Array(arity); + for (let i = 0; i < arity; i++) { + const typeParameter = typeParameters[i] = createTypeParameter(); + if (i < maxLength) { + const property = createSymbol(SymbolFlags.Property | (i >= minLength ? SymbolFlags.Optional : 0), ("" + i as __String), readonly ? CheckFlags.Readonly : 0); + property.type = typeParameter; + properties.push(property); + } + } + } + const literalTypes = []; + for (let i = minLength; i <= maxLength; i++) + literalTypes.push(getLiteralType(i)); + const lengthSymbol = createSymbol(SymbolFlags.Property, ("length" as __String)); + lengthSymbol.type = hasRestElement ? numberType : getUnionType(literalTypes); + properties.push(lengthSymbol); + const type = (createObjectType(ObjectFlags.Tuple | ObjectFlags.Reference)); + type.typeParameters = typeParameters; + type.outerTypeParameters = undefined; + type.localTypeParameters = typeParameters; + type.instantiations = createMap(); + type.instantiations.set(getTypeListId(type.typeParameters), (type)); + type.target = (type); + type.resolvedTypeArguments = type.typeParameters; + type.thisType = createTypeParameter(); + type.thisType.isThisType = true; + type.thisType.constraint = type; + type.declaredProperties = properties; + type.declaredCallSignatures = emptyArray; + type.declaredConstructSignatures = emptyArray; + type.declaredStringIndexInfo = undefined; + type.declaredNumberIndexInfo = undefined; + type.minLength = minLength; + type.hasRestElement = hasRestElement; + type.readonly = readonly; + type.associatedNames = associatedNames; + return type; + } + function getTupleTypeOfArity(arity: number, minLength: number, hasRestElement: boolean, readonly: boolean, associatedNames?: __String[]): GenericType { + const key = arity + (hasRestElement ? "+" : ",") + minLength + (readonly ? "R" : "") + (associatedNames && associatedNames.length ? "," + associatedNames.join(",") : ""); + let type = tupleTypes.get(key); + if (!type) { + tupleTypes.set(key, type = createTupleTypeOfArity(arity, minLength, hasRestElement, readonly, associatedNames)); } - - function typeHasNullableConstraint(type: Type) { - return type.flags & TypeFlags.InstantiableNonPrimitive && maybeTypeOfKind(getBaseConstraintOfType(type) || unknownType, TypeFlags.Nullable); + return type; + } + function createTupleType(elementTypes: readonly ts.Type[], minLength = elementTypes.length, hasRestElement = false, readonly = false, associatedNames?: __String[]) { + const arity = elementTypes.length; + if (arity === 1 && hasRestElement) { + return createArrayType(elementTypes[0], readonly); } - - function getConstraintForLocation(type: Type, node: Node): Type; - function getConstraintForLocation(type: Type | undefined, node: Node): Type | undefined; - function getConstraintForLocation(type: Type, node: Node): Type | undefined { - // When a node is the left hand expression of a property access, element access, or call expression, - // and the type of the node includes type variables with constraints that are nullable, we fetch the - // apparent type of the node *before* performing control flow analysis such that narrowings apply to - // the constraint type. - if (type && isConstraintPosition(node) && forEachType(type, typeHasNullableConstraint)) { - return mapType(getWidenedType(type), getBaseConstraintOrType); - } - return type; + const tupleType = getTupleTypeOfArity(arity, minLength, arity > 0 && hasRestElement, readonly, associatedNames); + return elementTypes.length ? createTypeReference(tupleType, elementTypes) : tupleType; + } + function sliceTupleType(type: TupleTypeReference, index: number) { + const tuple = type.target; + if (tuple.hasRestElement) { + // don't slice off rest element + index = Math.min(index, getTypeReferenceArity(type) - 1); } - - function isExportOrExportExpression(location: Node) { - return !!findAncestor(location, e => e.parent && isExportAssignment(e.parent) && e.parent.expression === e && isEntityNameExpression(e)); + return createTupleType(getTypeArguments(type).slice(index), Math.max(0, tuple.minLength - index), tuple.hasRestElement, tuple.readonly, tuple.associatedNames && tuple.associatedNames.slice(index)); + } + function getTypeFromOptionalTypeNode(node: OptionalTypeNode): ts.Type { + const type = getTypeFromTypeNode(node.type); + return strictNullChecks ? getOptionalType(type) : type; + } + function getTypeId(type: ts.Type) { + return type.id; + } + function containsType(types: readonly ts.Type[], type: ts.Type): boolean { + return binarySearch(types, type, getTypeId, compareValues) >= 0; + } + function insertType(types: ts.Type[], type: ts.Type): boolean { + const index = binarySearch(types, type, getTypeId, compareValues); + if (index < 0) { + types.splice(~index, 0, type); + return true; } - - function markAliasReferenced(symbol: Symbol, location: Node) { - if (isNonLocalAlias(symbol, /*excludes*/ SymbolFlags.Value) && !isInTypeQuery(location) && !getTypeOnlyAliasDeclaration(symbol)) { - if (compilerOptions.preserveConstEnums && isExportOrExportExpression(location) || !isConstEnumOrConstEnumOnlyModule(resolveAlias(symbol))) { - markAliasSymbolAsReferenced(symbol); - } - else { - markConstEnumAliasAsReferenced(symbol); + return false; + } + function addTypeToUnion(typeSet: ts.Type[], includes: TypeFlags, type: ts.Type) { + const flags = type.flags; + if (flags & TypeFlags.Union) { + return addTypesToUnion(typeSet, includes, (type).types); + } + // We ignore 'never' types in unions + if (!(flags & TypeFlags.Never)) { + includes |= flags & TypeFlags.IncludesMask; + if (flags & TypeFlags.StructuredOrInstantiable) + includes |= TypeFlags.IncludesStructuredOrInstantiable; + if (type === wildcardType) + includes |= TypeFlags.IncludesWildcard; + if (!strictNullChecks && flags & TypeFlags.Nullable) { + if (!(getObjectFlags(type) & ObjectFlags.ContainsWideningType)) + includes |= TypeFlags.IncludesNonWideningType; + } + else { + const len = typeSet.length; + const index = len && type.id > typeSet[len - 1].id ? ~len : binarySearch(typeSet, type, getTypeId, compareValues); + if (index < 0) { + typeSet.splice(~index, 0, type); } } } - - function checkIdentifier(node: Identifier): Type { - const symbol = getResolvedSymbol(node); - if (symbol === unknownSymbol) { - return errorType; + return includes; + } + // Add the given types to the given type set. Order is preserved, duplicates are removed, + // and nested types of the given kind are flattened into the set. + function addTypesToUnion(typeSet: ts.Type[], includes: TypeFlags, types: readonly ts.Type[]): TypeFlags { + for (const type of types) { + includes = addTypeToUnion(typeSet, includes, type); + } + return includes; + } + function isSetOfLiteralsFromSameEnum(types: readonly ts.Type[]): boolean { + const first = types[0]; + if (first.flags & TypeFlags.EnumLiteral) { + const firstEnum = getParentOfSymbol(first.symbol); + for (let i = 1; i < types.length; i++) { + const other = types[i]; + if (!(other.flags & TypeFlags.EnumLiteral) || (firstEnum !== getParentOfSymbol(other.symbol))) { + return false; + } } - - // As noted in ECMAScript 6 language spec, arrow functions never have an arguments objects. - // Although in down-level emit of arrow function, we emit it using function expression which means that - // arguments objects will be bound to the inner object; emitting arrow function natively in ES6, arguments objects - // will be bound to non-arrow function that contain this arrow function. This results in inconsistent behavior. - // To avoid that we will give an error to users if they use arguments objects in arrow function so that they - // can explicitly bound arguments objects - if (symbol === argumentsSymbol) { - const container = getContainingFunction(node)!; - if (languageVersion < ScriptTarget.ES2015) { - if (container.kind === SyntaxKind.ArrowFunction) { - error(node, Diagnostics.The_arguments_object_cannot_be_referenced_in_an_arrow_function_in_ES3_and_ES5_Consider_using_a_standard_function_expression); + return true; + } + return false; + } + function removeSubtypes(types: ts.Type[], primitivesOnly: boolean): boolean { + const len = types.length; + if (len === 0 || isSetOfLiteralsFromSameEnum(types)) { + return true; + } + let i = len; + let count = 0; + while (i > 0) { + i--; + const source = types[i]; + for (const target of types) { + if (source !== target) { + if (count === 100000) { + // After 100000 subtype checks we estimate the remaining amount of work by assuming the + // same ratio of checks per element. If the estimated number of remaining type checks is + // greater than an upper limit we deem the union type too complex to represent. The + // upper limit is 25M for unions of primitives only, and 1M otherwise. This for example + // caps union types at 5000 unique literal types and 1000 unique object types. + const estimatedCount = (count / (len - i)) * len; + if (estimatedCount > (primitivesOnly ? 25000000 : 1000000)) { + error(currentNode, Diagnostics.Expression_produces_a_union_type_that_is_too_complex_to_represent); + return false; + } } - else if (hasModifier(container, ModifierFlags.Async)) { - error(node, Diagnostics.The_arguments_object_cannot_be_referenced_in_an_async_function_or_method_in_ES3_and_ES5_Consider_using_a_standard_function_or_method); + count++; + if (isTypeRelatedTo(source, target, strictSubtypeRelation) && (!(getObjectFlags(getTargetType(source)) & ObjectFlags.Class) || + !(getObjectFlags(getTargetType(target)) & ObjectFlags.Class) || + isTypeDerivedFrom(source, target))) { + orderedRemoveItemAt(types, i); + break; } } - - getNodeLinks(container).flags |= NodeCheckFlags.CaptureArguments; - return getTypeOfSymbol(symbol); } - - // We should only mark aliases as referenced if there isn't a local value declaration - // for the symbol. Also, don't mark any property access expression LHS - checkPropertyAccessExpression will handle that - if (!(node.parent && isPropertyAccessExpression(node.parent) && node.parent.expression === node)) { - markAliasReferenced(symbol, node); + } + return true; + } + function removeRedundantLiteralTypes(types: ts.Type[], includes: TypeFlags) { + let i = types.length; + while (i > 0) { + i--; + const t = types[i]; + const remove = t.flags & TypeFlags.StringLiteral && includes & TypeFlags.String || + t.flags & TypeFlags.NumberLiteral && includes & TypeFlags.Number || + t.flags & TypeFlags.BigIntLiteral && includes & TypeFlags.BigInt || + t.flags & TypeFlags.UniqueESSymbol && includes & TypeFlags.ESSymbol || + isFreshLiteralType(t) && containsType(types, (t).regularType); + if (remove) { + orderedRemoveItemAt(types, i); } - - const localOrExportSymbol = getExportSymbolOfValueSymbolIfExported(symbol); - let declaration: Declaration | undefined = localOrExportSymbol.valueDeclaration; - - if (localOrExportSymbol.flags & SymbolFlags.Class) { - // Due to the emit for class decorators, any reference to the class from inside of the class body - // must instead be rewritten to point to a temporary variable to avoid issues with the double-bind - // behavior of class names in ES6. - if (declaration.kind === SyntaxKind.ClassDeclaration - && nodeIsDecorated(declaration as ClassDeclaration)) { - let container = getContainingClass(node); - while (container !== undefined) { - if (container === declaration && container.name !== node) { - getNodeLinks(declaration).flags |= NodeCheckFlags.ClassWithConstructorReference; - getNodeLinks(node).flags |= NodeCheckFlags.ConstructorReferenceInClass; - break; - } - - container = getContainingClass(container); - } - } - else if (declaration.kind === SyntaxKind.ClassExpression) { - // When we emit a class expression with static members that contain a reference - // to the constructor in the initializer, we will need to substitute that - // binding with an alias as the class name is not in scope. - let container = getThisContainer(node, /*includeArrowFunctions*/ false); - while (container.kind !== SyntaxKind.SourceFile) { - if (container.parent === declaration) { - if (container.kind === SyntaxKind.PropertyDeclaration && hasModifier(container, ModifierFlags.Static)) { - getNodeLinks(declaration).flags |= NodeCheckFlags.ClassWithConstructorReference; - getNodeLinks(node).flags |= NodeCheckFlags.ConstructorReferenceInClass; - } - break; - } - - container = getThisContainer(container, /*includeArrowFunctions*/ false); - } - } + } + } + // We sort and deduplicate the constituent types based on object identity. If the subtypeReduction + // flag is specified we also reduce the constituent type set to only include types that aren't subtypes + // of other types. Subtype reduction is expensive for large union types and is possible only when union + // types are known not to circularly reference themselves (as is the case with union types created by + // expression constructs such as array literals and the || and ?: operators). Named types can + // circularly reference themselves and therefore cannot be subtype reduced during their declaration. + // For example, "type Item = string | (() => Item" is a named type that circularly references itself. + function getUnionType(types: readonly ts.Type[], unionReduction: UnionReduction = UnionReduction.Literal, aliasSymbol?: ts.Symbol, aliasTypeArguments?: readonly ts.Type[]): ts.Type { + if (types.length === 0) { + return neverType; + } + if (types.length === 1) { + return types[0]; + } + const typeSet: ts.Type[] = []; + const includes = addTypesToUnion(typeSet, 0, types); + if (unionReduction !== UnionReduction.None) { + if (includes & TypeFlags.AnyOrUnknown) { + return includes & TypeFlags.Any ? includes & TypeFlags.IncludesWildcard ? wildcardType : anyType : unknownType; } - - checkNestedBlockScopedBinding(node, symbol); - - const type = getConstraintForLocation(getTypeOfSymbol(localOrExportSymbol), node); - const assignmentKind = getAssignmentTargetKind(node); - - if (assignmentKind) { - if (!(localOrExportSymbol.flags & SymbolFlags.Variable) && - !(isInJSFile(node) && localOrExportSymbol.flags & SymbolFlags.ValueModule)) { - error(node, Diagnostics.Cannot_assign_to_0_because_it_is_not_a_variable, symbolToString(symbol)); - return errorType; - } - if (isReadonlySymbol(localOrExportSymbol)) { - if (localOrExportSymbol.flags & SymbolFlags.Variable) { - error(node, Diagnostics.Cannot_assign_to_0_because_it_is_a_constant, symbolToString(symbol)); + switch (unionReduction) { + case UnionReduction.Literal: + if (includes & (TypeFlags.Literal | TypeFlags.UniqueESSymbol)) { + removeRedundantLiteralTypes(typeSet, includes); } - else { - error(node, Diagnostics.Cannot_assign_to_0_because_it_is_a_read_only_property, symbolToString(symbol)); + break; + case UnionReduction.Subtype: + if (!removeSubtypes(typeSet, !(includes & TypeFlags.IncludesStructuredOrInstantiable))) { + return errorType; } - return errorType; - } + break; } - - const isAlias = localOrExportSymbol.flags & SymbolFlags.Alias; - - // We only narrow variables and parameters occurring in a non-assignment position. For all other - // entities we simply return the declared type. - if (localOrExportSymbol.flags & SymbolFlags.Variable) { - if (assignmentKind === AssignmentKind.Definite) { - return type; - } + if (typeSet.length === 0) { + return includes & TypeFlags.Null ? includes & TypeFlags.IncludesNonWideningType ? nullType : nullWideningType : + includes & TypeFlags.Undefined ? includes & TypeFlags.IncludesNonWideningType ? undefinedType : undefinedWideningType : + neverType; } - else if (isAlias) { - declaration = find(symbol.declarations, isSomeImportDeclaration); + } + return getUnionTypeFromSortedList(typeSet, includes & TypeFlags.NotPrimitiveUnion ? 0 : ObjectFlags.PrimitiveUnion, aliasSymbol, aliasTypeArguments); + } + function getUnionTypePredicate(signatures: readonly ts.Signature[]): TypePredicate | undefined { + let first: TypePredicate | undefined; + const types: ts.Type[] = []; + for (const sig of signatures) { + const pred = getTypePredicateOfSignature(sig); + if (!pred || pred.kind === TypePredicateKind.AssertsThis || pred.kind === TypePredicateKind.AssertsIdentifier) { + continue; + } + if (first) { + if (!typePredicateKindsMatch(first, pred)) { + // No common type predicate. + return undefined; + } } else { - return type; - } - - if (!declaration) { - return type; - } - - // The declaration container is the innermost function that encloses the declaration of the variable - // or parameter. The flow container is the innermost function starting with which we analyze the control - // flow graph to determine the control flow based type. - const isParameter = getRootDeclaration(declaration).kind === SyntaxKind.Parameter; - const declarationContainer = getControlFlowContainer(declaration); - let flowContainer = getControlFlowContainer(node); - const isOuterVariable = flowContainer !== declarationContainer; - const isSpreadDestructuringAssignmentTarget = node.parent && node.parent.parent && isSpreadAssignment(node.parent) && isDestructuringAssignmentTarget(node.parent.parent); - const isModuleExports = symbol.flags & SymbolFlags.ModuleExports; - // When the control flow originates in a function expression or arrow function and we are referencing - // a const variable or parameter from an outer function, we extend the origin of the control flow - // analysis to include the immediately enclosing function. - while (flowContainer !== declarationContainer && (flowContainer.kind === SyntaxKind.FunctionExpression || - flowContainer.kind === SyntaxKind.ArrowFunction || isObjectLiteralOrClassExpressionMethod(flowContainer)) && - (isConstVariable(localOrExportSymbol) || isParameter && !isParameterAssigned(localOrExportSymbol))) { - flowContainer = getControlFlowContainer(flowContainer); - } - // We only look for uninitialized variables in strict null checking mode, and only when we can analyze - // the entire control flow graph from the variable's declaration (i.e. when the flow container and - // declaration container are the same). - const assumeInitialized = isParameter || isAlias || isOuterVariable || isSpreadDestructuringAssignmentTarget || isModuleExports || isBindingElement(declaration) || - type !== autoType && type !== autoArrayType && (!strictNullChecks || (type.flags & (TypeFlags.AnyOrUnknown | TypeFlags.Void)) !== 0 || - isInTypeQuery(node) || node.parent.kind === SyntaxKind.ExportSpecifier) || - node.parent.kind === SyntaxKind.NonNullExpression || - declaration.kind === SyntaxKind.VariableDeclaration && (declaration).exclamationToken || - declaration.flags & NodeFlags.Ambient; - const initialType = assumeInitialized ? (isParameter ? removeOptionalityFromDeclaredType(type, declaration as VariableLikeDeclaration) : type) : - type === autoType || type === autoArrayType ? undefinedType : - getOptionalType(type); - const flowType = getFlowTypeOfReference(node, type, initialType, flowContainer, !assumeInitialized); - // A variable is considered uninitialized when it is possible to analyze the entire control flow graph - // from declaration to use, and when the variable's declared type doesn't include undefined but the - // control flow based type does include undefined. - if (!isEvolvingArrayOperationTarget(node) && (type === autoType || type === autoArrayType)) { - if (flowType === autoType || flowType === autoArrayType) { - if (noImplicitAny) { - error(getNameOfDeclaration(declaration), Diagnostics.Variable_0_implicitly_has_type_1_in_some_locations_where_its_type_cannot_be_determined, symbolToString(symbol), typeToString(flowType)); - error(node, Diagnostics.Variable_0_implicitly_has_an_1_type, symbolToString(symbol), typeToString(flowType)); - } - return convertAutoToAny(flowType); - } - } - else if (!assumeInitialized && !(getFalsyFlags(type) & TypeFlags.Undefined) && getFalsyFlags(flowType) & TypeFlags.Undefined) { - error(node, Diagnostics.Variable_0_is_used_before_being_assigned, symbolToString(symbol)); - // Return the declared type to reduce follow-on errors - return type; + first = pred; } - return assignmentKind ? getBaseTypeOfLiteralType(flowType) : flowType; + types.push(pred.type); } - - function isInsideFunction(node: Node, threshold: Node): boolean { - return !!findAncestor(node, n => n === threshold ? "quit" : isFunctionLike(n)); + if (!first) { + // No union signatures had a type predicate. + return undefined; } - - function getPartOfForStatementContainingNode(node: Node, container: ForStatement) { - return findAncestor(node, n => n === container ? "quit" : n === container.initializer || n === container.condition || n === container.incrementor || n === container.statement); + const unionType = getUnionType(types); + return createTypePredicate(first.kind, first.parameterName, first.parameterIndex, unionType); + } + function typePredicateKindsMatch(a: TypePredicate, b: TypePredicate): boolean { + return a.kind === b.kind && a.parameterIndex === b.parameterIndex; + } + // This function assumes the constituent type list is sorted and deduplicated. + function getUnionTypeFromSortedList(types: ts.Type[], objectFlags: ObjectFlags, aliasSymbol?: ts.Symbol, aliasTypeArguments?: readonly ts.Type[]): ts.Type { + if (types.length === 0) { + return neverType; } - - function checkNestedBlockScopedBinding(node: Identifier, symbol: Symbol): void { - if (languageVersion >= ScriptTarget.ES2015 || - (symbol.flags & (SymbolFlags.BlockScopedVariable | SymbolFlags.Class)) === 0 || - isSourceFile(symbol.valueDeclaration) || - symbol.valueDeclaration.parent.kind === SyntaxKind.CatchClause) { - return; + if (types.length === 1) { + return types[0]; + } + const id = getTypeListId(types); + let type = unionTypes.get(id); + if (!type) { + type = (createType(TypeFlags.Union)); + unionTypes.set(id, type); + type.objectFlags = objectFlags | getPropagatingFlagsOfTypes(types, /*excludeKinds*/ TypeFlags.Nullable); + type.types = types; + /* + Note: This is the alias symbol (or lack thereof) that we see when we first encounter this union type. + For aliases of identical unions, eg `type T = A | B; type U = A | B`, the symbol of the first alias encountered is the aliasSymbol. + (In the language service, the order may depend on the order in which a user takes actions, such as hovering over symbols.) + It's important that we create equivalent union types only once, so that's an unfortunate side effect. + */ + type.aliasSymbol = aliasSymbol; + type.aliasTypeArguments = aliasTypeArguments; + } + return type; + } + function getTypeFromUnionTypeNode(node: UnionTypeNode): ts.Type { + const links = getNodeLinks(node); + if (!links.resolvedType) { + const aliasSymbol = getAliasSymbolForTypeNode(node); + links.resolvedType = getUnionType(map(node.types, getTypeFromTypeNode), UnionReduction.Literal, aliasSymbol, getTypeArgumentsForAliasSymbol(aliasSymbol)); + } + return links.resolvedType; + } + function addTypeToIntersection(typeSet: ts.Map, includes: TypeFlags, type: ts.Type) { + const flags = type.flags; + if (flags & TypeFlags.Intersection) { + return addTypesToIntersection(typeSet, includes, (type).types); + } + if (isEmptyAnonymousObjectType(type)) { + if (!(includes & TypeFlags.IncludesEmptyObject)) { + includes |= TypeFlags.IncludesEmptyObject; + typeSet.set(type.id.toString(), type); } - - // 1. walk from the use site up to the declaration and check - // if there is anything function like between declaration and use-site (is binding/class is captured in function). - // 2. walk from the declaration up to the boundary of lexical environment and check - // if there is an iteration statement in between declaration and boundary (is binding/class declared inside iteration statement) - - const container = getEnclosingBlockScopeContainer(symbol.valueDeclaration); - const usedInFunction = isInsideFunction(node.parent, container); - let current = container; - - let containedInIterationStatement = false; - while (current && !nodeStartsNewLexicalEnvironment(current)) { - if (isIterationStatement(current, /*lookInLabeledStatements*/ false)) { - containedInIterationStatement = true; - break; - } - current = current.parent; + } + else { + if (flags & TypeFlags.AnyOrUnknown) { + if (type === wildcardType) + includes |= TypeFlags.IncludesWildcard; } - - if (containedInIterationStatement) { - if (usedInFunction) { - // mark iteration statement as containing block-scoped binding captured in some function - let capturesBlockScopeBindingInLoopBody = true; - if (isForStatement(container)) { - const varDeclList = getAncestor(symbol.valueDeclaration, SyntaxKind.VariableDeclarationList); - if (varDeclList && varDeclList.parent === container) { - const part = getPartOfForStatementContainingNode(node.parent, container); - if (part) { - const links = getNodeLinks(part); - links.flags |= NodeCheckFlags.ContainsCapturedBlockScopeBinding; - - const capturedBindings = links.capturedBlockScopeBindings || (links.capturedBlockScopeBindings = []); - pushIfUnique(capturedBindings, symbol); - - if (part === container.initializer) { - capturesBlockScopeBindingInLoopBody = false; // Initializer is outside of loop body - } - } - } - } - if (capturesBlockScopeBindingInLoopBody) { - getNodeLinks(current).flags |= NodeCheckFlags.LoopWithCapturedBlockScopedBinding; - } - } - - // mark variables that are declared in loop initializer and reassigned inside the body of ForStatement. - // if body of ForStatement will be converted to function then we'll need a extra machinery to propagate reassigned values back. - if (isForStatement(container)) { - const varDeclList = getAncestor(symbol.valueDeclaration, SyntaxKind.VariableDeclarationList); - if (varDeclList && varDeclList.parent === container && isAssignedInBodyOfForStatement(node, container)) { - getNodeLinks(symbol.valueDeclaration).flags |= NodeCheckFlags.NeedsLoopOutParameter; - } + else if ((strictNullChecks || !(flags & TypeFlags.Nullable)) && !typeSet.has(type.id.toString())) { + if (type.flags & TypeFlags.Unit && includes & TypeFlags.Unit) { + // We have seen two distinct unit types which means we should reduce to an + // empty intersection. Adding TypeFlags.NonPrimitive causes that to happen. + includes |= TypeFlags.NonPrimitive; } - - // set 'declared inside loop' bit on the block-scoped binding - getNodeLinks(symbol.valueDeclaration).flags |= NodeCheckFlags.BlockScopedBindingInLoop; - } - - if (usedInFunction) { - getNodeLinks(symbol.valueDeclaration).flags |= NodeCheckFlags.CapturedBlockScopedBinding; + typeSet.set(type.id.toString(), type); } + includes |= flags & TypeFlags.IncludesMask; } - - function isBindingCapturedByNode(node: Node, decl: VariableDeclaration | BindingElement) { - const links = getNodeLinks(node); - return !!links && contains(links.capturedBlockScopeBindings, getSymbolOfNode(decl)); + return includes; + } + // Add the given types to the given type set. Order is preserved, freshness is removed from literal + // types, duplicates are removed, and nested types of the given kind are flattened into the set. + function addTypesToIntersection(typeSet: ts.Map, includes: TypeFlags, types: readonly ts.Type[]) { + for (const type of types) { + includes = addTypeToIntersection(typeSet, includes, getRegularTypeOfLiteralType(type)); } - - function isAssignedInBodyOfForStatement(node: Identifier, container: ForStatement): boolean { - // skip parenthesized nodes - let current: Node = node; - while (current.parent.kind === SyntaxKind.ParenthesizedExpression) { - current = current.parent; - } - - // check if node is used as LHS in some assignment expression - let isAssigned = false; - if (isAssignmentTarget(current)) { - isAssigned = true; + return includes; + } + function removeRedundantPrimitiveTypes(types: ts.Type[], includes: TypeFlags) { + let i = types.length; + while (i > 0) { + i--; + const t = types[i]; + const remove = t.flags & TypeFlags.String && includes & TypeFlags.StringLiteral || + t.flags & TypeFlags.Number && includes & TypeFlags.NumberLiteral || + t.flags & TypeFlags.BigInt && includes & TypeFlags.BigIntLiteral || + t.flags & TypeFlags.ESSymbol && includes & TypeFlags.UniqueESSymbol; + if (remove) { + orderedRemoveItemAt(types, i); } - else if ((current.parent.kind === SyntaxKind.PrefixUnaryExpression || current.parent.kind === SyntaxKind.PostfixUnaryExpression)) { - const expr = current.parent; - isAssigned = expr.operator === SyntaxKind.PlusPlusToken || expr.operator === SyntaxKind.MinusMinusToken; + } + } + // Check that the given type has a match in every union. A given type is matched by + // an identical type, and a literal type is additionally matched by its corresponding + // primitive type. + function eachUnionContains(unionTypes: UnionType[], type: ts.Type) { + for (const u of unionTypes) { + if (!containsType(u.types, type)) { + const primitive = type.flags & TypeFlags.StringLiteral ? stringType : + type.flags & TypeFlags.NumberLiteral ? numberType : + type.flags & TypeFlags.BigIntLiteral ? bigintType : + type.flags & TypeFlags.UniqueESSymbol ? esSymbolType : + undefined; + if (!primitive || !containsType(u.types, primitive)) { + return false; + } } - - if (!isAssigned) { - return false; + } + return true; + } + function extractIrreducible(types: ts.Type[], flag: TypeFlags) { + if (every(types, t => !!(t.flags & TypeFlags.Union) && some((t as UnionType).types, tt => !!(tt.flags & flag)))) { + for (let i = 0; i < types.length; i++) { + types[i] = filterType(types[i], t => !(t.flags & flag)); } - - // at this point we know that node is the target of assignment - // now check that modification happens inside the statement part of the ForStatement - return !!findAncestor(current, n => n === container ? "quit" : n === container.statement); + return true; } - - function captureLexicalThis(node: Node, container: Node): void { - getNodeLinks(node).flags |= NodeCheckFlags.LexicalThis; - if (container.kind === SyntaxKind.PropertyDeclaration || container.kind === SyntaxKind.Constructor) { - const classNode = container.parent; - getNodeLinks(classNode).flags |= NodeCheckFlags.CaptureThis; + return false; + } + // If the given list of types contains more than one union of primitive types, replace the + // first with a union containing an intersection of those primitive types, then remove the + // other unions and return true. Otherwise, do nothing and return false. + function intersectUnionsOfPrimitiveTypes(types: ts.Type[]) { + let unionTypes: UnionType[] | undefined; + const index = findIndex(types, t => !!(getObjectFlags(t) & ObjectFlags.PrimitiveUnion)); + if (index < 0) { + return false; + } + let i = index + 1; + // Remove all but the first union of primitive types and collect them in + // the unionTypes array. + while (i < types.length) { + const t = types[i]; + if (getObjectFlags(t) & ObjectFlags.PrimitiveUnion) { + (unionTypes || (unionTypes = [(types[index])])).push((t)); + orderedRemoveItemAt(types, i); } else { - getNodeLinks(container).flags |= NodeCheckFlags.CaptureThis; + i++; } } - - function findFirstSuperCall(n: Node): SuperCall | undefined { - if (isSuperCall(n)) { - return n; - } - else if (isFunctionLike(n)) { - return undefined; - } - return forEachChild(n, findFirstSuperCall); + // Return false if there was only one union of primitive types + if (!unionTypes) { + return false; } - - /** - * Return a cached result if super-statement is already found. - * Otherwise, find a super statement in a given constructor function and cache the result in the node-links of the constructor - * - * @param constructor constructor-function to look for super statement - */ - function getSuperCallInConstructor(constructor: ConstructorDeclaration): SuperCall | undefined { - const links = getNodeLinks(constructor); - - // Only trying to find super-call if we haven't yet tried to find one. Once we try, we will record the result - if (links.hasSuperCall === undefined) { - links.superCall = findFirstSuperCall(constructor.body!); - links.hasSuperCall = links.superCall ? true : false; + // We have more than one union of primitive types, now intersect them. For each + // type in each union we check if the type is matched in every union and if so + // we include it in the result. + const checked: ts.Type[] = []; + const result: ts.Type[] = []; + for (const u of unionTypes) { + for (const t of u.types) { + if (insertType(checked, t)) { + if (eachUnionContains(unionTypes, t)) { + insertType(result, t); + } + } } - return links.superCall!; } - - /** - * Check if the given class-declaration extends null then return true. - * Otherwise, return false - * @param classDecl a class declaration to check if it extends null - */ - function classDeclarationExtendsNull(classDecl: ClassDeclaration): boolean { - const classSymbol = getSymbolOfNode(classDecl); - const classInstanceType = getDeclaredTypeOfSymbol(classSymbol); - const baseConstructorType = getBaseConstructorTypeOfClass(classInstanceType); - - return baseConstructorType === nullWideningType; + // Finally replace the first union with the result + types[index] = getUnionTypeFromSortedList(result, ObjectFlags.PrimitiveUnion); + return true; + } + function createIntersectionType(types: ts.Type[], aliasSymbol?: ts.Symbol, aliasTypeArguments?: readonly ts.Type[]) { + const result = (createType(TypeFlags.Intersection)); + result.objectFlags = getPropagatingFlagsOfTypes(types, /*excludeKinds*/ TypeFlags.Nullable); + result.types = types; + result.aliasSymbol = aliasSymbol; // See comment in `getUnionTypeFromSortedList`. + result.aliasTypeArguments = aliasTypeArguments; + return result; + } + // We normalize combinations of intersection and union types based on the distributive property of the '&' + // operator. Specifically, because X & (A | B) is equivalent to X & A | X & B, we can transform intersection + // types with union type constituents into equivalent union types with intersection type constituents and + // effectively ensure that union types are always at the top level in type representations. + // + // We do not perform structural deduplication on intersection types. Intersection types are created only by the & + // type operator and we can't reduce those because we want to support recursive intersection types. For example, + // a type alias of the form "type List = T & { next: List }" cannot be reduced during its declaration. + // Also, unlike union types, the order of the constituent types is preserved in order that overload resolution + // for intersections of types with signatures can be deterministic. + function getIntersectionType(types: readonly ts.Type[], aliasSymbol?: ts.Symbol, aliasTypeArguments?: readonly ts.Type[]): ts.Type { + const typeMembershipMap: ts.Map = createMap(); + const includes = addTypesToIntersection(typeMembershipMap, 0, types); + const typeSet: ts.Type[] = arrayFrom(typeMembershipMap.values()); + // An intersection type is considered empty if it contains + // the type never, or + // more than one unit type or, + // an object type and a nullable type (null or undefined), or + // a string-like type and a type known to be non-string-like, or + // a number-like type and a type known to be non-number-like, or + // a symbol-like type and a type known to be non-symbol-like, or + // a void-like type and a type known to be non-void-like, or + // a non-primitive type and a type known to be primitive. + if (includes & TypeFlags.Never || + strictNullChecks && includes & TypeFlags.Nullable && includes & (TypeFlags.Object | TypeFlags.NonPrimitive | TypeFlags.IncludesEmptyObject) || + includes & TypeFlags.NonPrimitive && includes & (TypeFlags.DisjointDomains & ~TypeFlags.NonPrimitive) || + includes & TypeFlags.StringLike && includes & (TypeFlags.DisjointDomains & ~TypeFlags.StringLike) || + includes & TypeFlags.NumberLike && includes & (TypeFlags.DisjointDomains & ~TypeFlags.NumberLike) || + includes & TypeFlags.BigIntLike && includes & (TypeFlags.DisjointDomains & ~TypeFlags.BigIntLike) || + includes & TypeFlags.ESSymbolLike && includes & (TypeFlags.DisjointDomains & ~TypeFlags.ESSymbolLike) || + includes & TypeFlags.VoidLike && includes & (TypeFlags.DisjointDomains & ~TypeFlags.VoidLike)) { + return neverType; } - - function checkThisBeforeSuper(node: Node, container: Node, diagnosticMessage: DiagnosticMessage) { - const containingClassDecl = container.parent; - const baseTypeNode = getClassExtendsHeritageElement(containingClassDecl); - - // If a containing class does not have extends clause or the class extends null - // skip checking whether super statement is called before "this" accessing. - if (baseTypeNode && !classDeclarationExtendsNull(containingClassDecl)) { - const superCall = getSuperCallInConstructor(container); - - // We should give an error in the following cases: - // - No super-call - // - "this" is accessing before super-call. - // i.e super(this) - // this.x; super(); - // We want to make sure that super-call is done before accessing "this" so that - // "this" is not accessed as a parameter of the super-call. - if (!superCall || superCall.end > node.pos) { - // In ES6, super inside constructor of class-declaration has to precede "this" accessing - error(node, diagnosticMessage); + if (includes & TypeFlags.Any) { + return includes & TypeFlags.IncludesWildcard ? wildcardType : anyType; + } + if (!strictNullChecks && includes & TypeFlags.Nullable) { + return includes & TypeFlags.Undefined ? undefinedType : nullType; + } + if (includes & TypeFlags.String && includes & TypeFlags.StringLiteral || + includes & TypeFlags.Number && includes & TypeFlags.NumberLiteral || + includes & TypeFlags.BigInt && includes & TypeFlags.BigIntLiteral || + includes & TypeFlags.ESSymbol && includes & TypeFlags.UniqueESSymbol) { + removeRedundantPrimitiveTypes(typeSet, includes); + } + if (includes & TypeFlags.IncludesEmptyObject && includes & TypeFlags.Object) { + orderedRemoveItemAt(typeSet, findIndex(typeSet, isEmptyAnonymousObjectType)); + } + if (typeSet.length === 0) { + return unknownType; + } + if (typeSet.length === 1) { + return typeSet[0]; + } + const id = getTypeListId(typeSet); + let result = intersectionTypes.get(id); + if (!result) { + if (includes & TypeFlags.Union) { + if (intersectUnionsOfPrimitiveTypes(typeSet)) { + // When the intersection creates a reduced set (which might mean that *all* union types have + // disappeared), we restart the operation to get a new set of combined flags. Once we have + // reduced we'll never reduce again, so this occurs at most once. + result = getIntersectionType(typeSet, aliasSymbol, aliasTypeArguments); + } + else if (extractIrreducible(typeSet, TypeFlags.Undefined)) { + result = getUnionType([getIntersectionType(typeSet), undefinedType], UnionReduction.Literal, aliasSymbol, aliasTypeArguments); + } + else if (extractIrreducible(typeSet, TypeFlags.Null)) { + result = getUnionType([getIntersectionType(typeSet), nullType], UnionReduction.Literal, aliasSymbol, aliasTypeArguments); + } + else { + // We are attempting to construct a type of the form X & (A | B) & Y. Transform this into a type of + // the form X & A & Y | X & B & Y and recursively reduce until no union type constituents remain. + // If the estimated size of the resulting union type exceeds 100000 constituents, report an error. + const size = reduceLeft(typeSet, (n, t) => n * (t.flags & TypeFlags.Union ? (t).types.length : 1), 1); + if (size >= 100000) { + error(currentNode, Diagnostics.Expression_produces_a_union_type_that_is_too_complex_to_represent); + return errorType; + } + const unionIndex = findIndex(typeSet, t => (t.flags & TypeFlags.Union) !== 0); + const unionType = (typeSet[unionIndex]); + result = getUnionType(map(unionType.types, t => getIntersectionType(replaceElement(typeSet, unionIndex, t))), UnionReduction.Literal, aliasSymbol, aliasTypeArguments); } } + else { + result = createIntersectionType(typeSet, aliasSymbol, aliasTypeArguments); + } + intersectionTypes.set(id, result); } - - function checkThisExpression(node: Node): Type { - // Stop at the first arrow function so that we can - // tell whether 'this' needs to be captured. - let container = getThisContainer(node, /* includeArrowFunctions */ true); - let capturedByArrowFunction = false; - - if (container.kind === SyntaxKind.Constructor) { - checkThisBeforeSuper(node, container, Diagnostics.super_must_be_called_before_accessing_this_in_the_constructor_of_a_derived_class); + return result; + } + function getTypeFromIntersectionTypeNode(node: IntersectionTypeNode): ts.Type { + const links = getNodeLinks(node); + if (!links.resolvedType) { + const aliasSymbol = getAliasSymbolForTypeNode(node); + links.resolvedType = getIntersectionType(map(node.types, getTypeFromTypeNode), aliasSymbol, getTypeArgumentsForAliasSymbol(aliasSymbol)); + } + return links.resolvedType; + } + function createIndexType(type: InstantiableType | UnionOrIntersectionType, stringsOnly: boolean) { + const result = (createType(TypeFlags.Index)); + result.type = type; + result.stringsOnly = stringsOnly; + return result; + } + function getIndexTypeForGenericType(type: InstantiableType | UnionOrIntersectionType, stringsOnly: boolean) { + return stringsOnly ? + type.resolvedStringIndexType || (type.resolvedStringIndexType = createIndexType(type, /*stringsOnly*/ true)) : + type.resolvedIndexType || (type.resolvedIndexType = createIndexType(type, /*stringsOnly*/ false)); + } + function getLiteralTypeFromPropertyName(name: PropertyName) { + if (isPrivateIdentifier(name)) { + return neverType; + } + return isIdentifier(name) ? getLiteralType(unescapeLeadingUnderscores(name.escapedText)) : + getRegularTypeOfLiteralType(isComputedPropertyName(name) ? checkComputedPropertyName(name) : checkExpression(name)); + } + function getBigIntLiteralType(node: BigIntLiteral): LiteralType { + return getLiteralType({ + negative: false, + base10Value: parsePseudoBigInt(node.text) + }); + } + function getLiteralTypeFromProperty(prop: ts.Symbol, include: TypeFlags) { + if (!(getDeclarationModifierFlagsFromSymbol(prop) & ModifierFlags.NonPublicAccessibilityModifier)) { + let type = getSymbolLinks(getLateBoundSymbol(prop)).nameType; + if (!type && !isKnownSymbol(prop)) { + if (prop.escapedName === InternalSymbolName.Default) { + type = getLiteralType("default"); + } + else { + const name = prop.valueDeclaration && (getNameOfDeclaration(prop.valueDeclaration) as PropertyName); + type = name && getLiteralTypeFromPropertyName(name) || getLiteralType(symbolName(prop)); + } } - - // Now skip arrow functions to get the "real" owner of 'this'. - if (container.kind === SyntaxKind.ArrowFunction) { - container = getThisContainer(container, /* includeArrowFunctions */ false); - capturedByArrowFunction = true; + if (type && type.flags & include) { + return type; } - - switch (container.kind) { - case SyntaxKind.ModuleDeclaration: - error(node, Diagnostics.this_cannot_be_referenced_in_a_module_or_namespace_body); - // do not return here so in case if lexical this is captured - it will be reflected in flags on NodeLinks - break; - case SyntaxKind.EnumDeclaration: - error(node, Diagnostics.this_cannot_be_referenced_in_current_location); - // do not return here so in case if lexical this is captured - it will be reflected in flags on NodeLinks - break; - case SyntaxKind.Constructor: - if (isInConstructorArgumentInitializer(node, container)) { - error(node, Diagnostics.this_cannot_be_referenced_in_constructor_arguments); - // do not return here so in case if lexical this is captured - it will be reflected in flags on NodeLinks - } + } + return neverType; + } + function getLiteralTypeFromProperties(type: ts.Type, include: TypeFlags) { + return getUnionType(map(getPropertiesOfType(type), p => getLiteralTypeFromProperty(p, include))); + } + function getNonEnumNumberIndexInfo(type: ts.Type) { + const numberIndexInfo = getIndexInfoOfType(type, IndexKind.Number); + return numberIndexInfo !== enumNumberIndexInfo ? numberIndexInfo : undefined; + } + function getIndexType(type: ts.Type, stringsOnly = keyofStringsOnly, noIndexSignatures?: boolean): ts.Type { + return type.flags & TypeFlags.Union ? getIntersectionType(map((type).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) : + type.flags & TypeFlags.Intersection ? getUnionType(map((type).types, t => getIndexType(t, stringsOnly, noIndexSignatures))) : + maybeTypeOfKind(type, TypeFlags.InstantiableNonPrimitive) ? getIndexTypeForGenericType((type), stringsOnly) : + getObjectFlags(type) & ObjectFlags.Mapped ? filterType(getConstraintTypeFromMappedType((type)), t => !(noIndexSignatures && t.flags & (TypeFlags.Any | TypeFlags.String))) : + type === wildcardType ? wildcardType : + type.flags & TypeFlags.Unknown ? neverType : + type.flags & (TypeFlags.Any | TypeFlags.Never) ? keyofConstraintType : + stringsOnly ? !noIndexSignatures && getIndexInfoOfType(type, IndexKind.String) ? stringType : getLiteralTypeFromProperties(type, TypeFlags.StringLiteral) : + !noIndexSignatures && getIndexInfoOfType(type, IndexKind.String) ? getUnionType([stringType, numberType, getLiteralTypeFromProperties(type, TypeFlags.UniqueESSymbol)]) : + getNonEnumNumberIndexInfo(type) ? getUnionType([numberType, getLiteralTypeFromProperties(type, TypeFlags.StringLiteral | TypeFlags.UniqueESSymbol)]) : + getLiteralTypeFromProperties(type, TypeFlags.StringOrNumberLiteralOrUnique); + } + function getExtractStringType(type: ts.Type) { + if (keyofStringsOnly) { + return type; + } + const extractTypeAlias = getGlobalExtractSymbol(); + return extractTypeAlias ? getTypeAliasInstantiation(extractTypeAlias, [type, stringType]) : stringType; + } + function getIndexTypeOrString(type: ts.Type): ts.Type { + const indexType = getExtractStringType(getIndexType(type)); + return indexType.flags & TypeFlags.Never ? stringType : indexType; + } + function getTypeFromTypeOperatorNode(node: TypeOperatorNode): ts.Type { + const links = getNodeLinks(node); + if (!links.resolvedType) { + switch (node.operator) { + case SyntaxKind.KeyOfKeyword: + links.resolvedType = getIndexType(getTypeFromTypeNode(node.type)); break; - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.PropertySignature: - if (hasModifier(container, ModifierFlags.Static) && !(compilerOptions.target === ScriptTarget.ESNext && compilerOptions.useDefineForClassFields)) { - error(node, Diagnostics.this_cannot_be_referenced_in_a_static_property_initializer); - // do not return here so in case if lexical this is captured - it will be reflected in flags on NodeLinks - } + case SyntaxKind.UniqueKeyword: + links.resolvedType = node.type.kind === SyntaxKind.SymbolKeyword + ? getESSymbolLikeTypeForNode(walkUpParenthesizedTypes(node.parent)) + : errorType; break; - case SyntaxKind.ComputedPropertyName: - error(node, Diagnostics.this_cannot_be_referenced_in_a_computed_property_name); + case SyntaxKind.ReadonlyKeyword: + links.resolvedType = getTypeFromTypeNode(node.type); break; + default: + throw Debug.assertNever(node.operator); } - - // When targeting es6, mark that we'll need to capture `this` in its lexically bound scope. - if (capturedByArrowFunction && languageVersion < ScriptTarget.ES2015) { - captureLexicalThis(node, container); + } + return links.resolvedType; + } + function createIndexedAccessType(objectType: ts.Type, indexType: ts.Type) { + const type = (createType(TypeFlags.IndexedAccess)); + type.objectType = objectType; + type.indexType = indexType; + return type; + } + /** + * Returns if a type is or consists of a JSLiteral object type + * In addition to objects which are directly literals, + * * unions where every element is a jsliteral + * * intersections where at least one element is a jsliteral + * * and instantiable types constrained to a jsliteral + * Should all count as literals and not print errors on access or assignment of possibly existing properties. + * This mirrors the behavior of the index signature propagation, to which this behaves similarly (but doesn't affect assignability or inference). + */ + function isJSLiteralType(type: ts.Type): boolean { + if (noImplicitAny) { + return false; // Flag is meaningless under `noImplicitAny` mode + } + if (getObjectFlags(type) & ObjectFlags.JSLiteral) { + return true; + } + if (type.flags & TypeFlags.Union) { + return every((type as UnionType).types, isJSLiteralType); + } + if (type.flags & TypeFlags.Intersection) { + return some((type as IntersectionType).types, isJSLiteralType); + } + if (type.flags & TypeFlags.Instantiable) { + return isJSLiteralType(getResolvedBaseConstraint(type)); + } + return false; + } + function getPropertyNameFromIndex(indexType: ts.Type, accessNode: StringLiteral | Identifier | PrivateIdentifier | ObjectBindingPattern | ArrayBindingPattern | ComputedPropertyName | NumericLiteral | IndexedAccessTypeNode | ElementAccessExpression | SyntheticExpression | undefined) { + const accessExpression = accessNode && accessNode.kind === SyntaxKind.ElementAccessExpression ? accessNode : undefined; + return isTypeUsableAsPropertyName(indexType) ? + getPropertyNameFromType(indexType) : + accessExpression && checkThatExpressionIsProperSymbolReference(accessExpression.argumentExpression, indexType, /*reportError*/ false) ? + getPropertyNameForKnownSymbolName(idText((accessExpression.argumentExpression).name)) : + accessNode && isPropertyName(accessNode) ? + // late bound names are handled in the first branch, so here we only need to handle normal names + getPropertyNameForPropertyNameNode(accessNode) : + undefined; + } + function getPropertyTypeForIndexType(originalObjectType: ts.Type, objectType: ts.Type, indexType: ts.Type, fullIndexType: ts.Type, suppressNoImplicitAnyError: boolean, accessNode: ElementAccessExpression | IndexedAccessTypeNode | PropertyName | BindingName | SyntheticExpression | undefined, accessFlags: AccessFlags) { + const accessExpression = accessNode && accessNode.kind === SyntaxKind.ElementAccessExpression ? accessNode : undefined; + const propName = accessNode && isPrivateIdentifier(accessNode) ? undefined : getPropertyNameFromIndex(indexType, accessNode); + if (propName !== undefined) { + const prop = getPropertyOfType(objectType, propName); + if (prop) { + if (accessExpression) { + markPropertyAsReferenced(prop, accessExpression, /*isThisAccess*/ accessExpression.expression.kind === SyntaxKind.ThisKeyword); + if (isAssignmentToReadonlyEntity(accessExpression, prop, getAssignmentTargetKind(accessExpression))) { + error(accessExpression.argumentExpression, Diagnostics.Cannot_assign_to_0_because_it_is_a_read_only_property, symbolToString(prop)); + return undefined; + } + if (accessFlags & AccessFlags.CacheSymbol) { + getNodeLinks(accessNode!).resolvedSymbol = prop; + } + } + const propType = getTypeOfSymbol(prop); + return accessExpression && getAssignmentTargetKind(accessExpression) !== AssignmentKind.Definite ? + getFlowTypeOfReference(accessExpression, propType) : + propType; } - - const type = tryGetThisTypeAt(node, /*includeGlobalThis*/ true, container); - if (noImplicitThis) { - const globalThisType = getTypeOfSymbol(globalThisSymbol); - if (type === globalThisType && capturedByArrowFunction) { - error(node, Diagnostics.The_containing_arrow_function_captures_the_global_value_of_this); - } - else if (!type) { - // With noImplicitThis, functions may not reference 'this' if it has type 'any' - const diag = error(node, Diagnostics.this_implicitly_has_type_any_because_it_does_not_have_a_type_annotation); - if (!isSourceFile(container)) { - const outsideThis = tryGetThisTypeAt(container); - if (outsideThis && outsideThis !== globalThisType) { - addRelatedInfo(diag, createDiagnosticForNode(container, Diagnostics.An_outer_value_of_this_is_shadowed_by_this_container)); - } + if (everyType(objectType, isTupleType) && isNumericLiteralName(propName) && +propName >= 0) { + if (accessNode && everyType(objectType, t => !(t).target.hasRestElement) && !(accessFlags & AccessFlags.NoTupleBoundsCheck)) { + const indexNode = getIndexNodeForAccessExpression(accessNode); + if (isTupleType(objectType)) { + error(indexNode, Diagnostics.Tuple_type_0_of_length_1_has_no_element_at_index_2, typeToString(objectType), getTypeReferenceArity(objectType), unescapeLeadingUnderscores(propName)); + } + else { + error(indexNode, Diagnostics.Property_0_does_not_exist_on_type_1, unescapeLeadingUnderscores(propName), typeToString(objectType)); } } + errorIfWritingToReadonlyIndex(getIndexInfoOfType(objectType, IndexKind.Number)); + return mapType(objectType, t => getRestTypeOfTupleType((t)) || undefinedType); } - return type || anyType; } - - function tryGetThisTypeAt(node: Node, includeGlobalThis = true, container = getThisContainer(node, /*includeArrowFunctions*/ false)): Type | undefined { - const isInJS = isInJSFile(node); - if (isFunctionLike(container) && - (!isInParameterInitializerBeforeContainingFunction(node) || getThisParameter(container))) { - // Note: a parameter initializer should refer to class-this unless function-this is explicitly annotated. - // If this is a function in a JS file, it might be a class method. - const className = getClassNameFromPrototypeMethod(container); - if (isInJS && className) { - const classSymbol = checkExpression(className).symbol; - if (classSymbol && classSymbol.members && (classSymbol.flags & SymbolFlags.Function)) { - const classType = (getDeclaredTypeOfSymbol(classSymbol) as InterfaceType).thisType!; - return getFlowTypeOfReference(node, classType); - } - } - // Check if it's a constructor definition, can be either a variable decl or function decl - // i.e. - // * /** @constructor */ function [name]() { ... } - // * /** @constructor */ var x = function() { ... } - else if (isInJS && - (container.kind === SyntaxKind.FunctionExpression || container.kind === SyntaxKind.FunctionDeclaration) && - getJSDocClassTag(container)) { - const classType = (getDeclaredTypeOfSymbol(getMergedSymbol(container.symbol)) as InterfaceType).thisType!; - return getFlowTypeOfReference(node, classType); + if (!(indexType.flags & TypeFlags.Nullable) && isTypeAssignableToKind(indexType, TypeFlags.StringLike | TypeFlags.NumberLike | TypeFlags.ESSymbolLike)) { + if (objectType.flags & (TypeFlags.Any | TypeFlags.Never)) { + return objectType; + } + const stringIndexInfo = getIndexInfoOfType(objectType, IndexKind.String); + const indexInfo = isTypeAssignableToKind(indexType, TypeFlags.NumberLike) && getIndexInfoOfType(objectType, IndexKind.Number) || stringIndexInfo; + if (indexInfo) { + if (accessFlags & AccessFlags.NoIndexSignatures && indexInfo === stringIndexInfo) { + if (accessExpression) { + error(accessExpression, Diagnostics.Type_0_cannot_be_used_to_index_type_1, typeToString(indexType), typeToString(originalObjectType)); + } + return undefined; } - - const thisType = getThisTypeOfDeclaration(container) || getContextualThisParameterType(container); - if (thisType) { - return getFlowTypeOfReference(node, thisType); + if (accessNode && !isTypeAssignableToKind(indexType, TypeFlags.String | TypeFlags.Number)) { + const indexNode = getIndexNodeForAccessExpression(accessNode); + error(indexNode, Diagnostics.Type_0_cannot_be_used_as_an_index_type, typeToString(indexType)); + return indexInfo.type; } + errorIfWritingToReadonlyIndex(indexInfo); + return indexInfo.type; } - - if (isClassLike(container.parent)) { - const symbol = getSymbolOfNode(container.parent); - const type = hasModifier(container, ModifierFlags.Static) ? getTypeOfSymbol(symbol) : (getDeclaredTypeOfSymbol(symbol) as InterfaceType).thisType!; - return getFlowTypeOfReference(node, type); + if (indexType.flags & TypeFlags.Never) { + return neverType; } - - if (isInJS) { - const type = getTypeForThisExpressionFromJSDoc(container); - if (type && type !== errorType) { - return getFlowTypeOfReference(node, type); - } + if (isJSLiteralType(objectType)) { + return anyType; } - if (isSourceFile(container)) { - // look up in the source file's locals or exports - if (container.commonJsModuleIndicator) { - const fileSymbol = getSymbolOfNode(container); - return fileSymbol && getTypeOfSymbol(fileSymbol); + if (accessExpression && !isConstEnumObjectType(objectType)) { + if (objectType.symbol === globalThisSymbol && propName !== undefined && globalThisSymbol.exports!.has(propName) && (globalThisSymbol.exports!.get(propName)!.flags & SymbolFlags.BlockScoped)) { + error(accessExpression, Diagnostics.Property_0_does_not_exist_on_type_1, unescapeLeadingUnderscores(propName), typeToString(objectType)); } - else if (includeGlobalThis) { - return getTypeOfSymbol(globalThisSymbol); + else if (noImplicitAny && !compilerOptions.suppressImplicitAnyIndexErrors && !suppressNoImplicitAnyError) { + if (propName !== undefined && typeHasStaticProperty(propName, objectType)) { + error(accessExpression, Diagnostics.Property_0_is_a_static_member_of_type_1, (propName as string), typeToString(objectType)); + } + else if (getIndexTypeOfType(objectType, IndexKind.Number)) { + error(accessExpression.argumentExpression, Diagnostics.Element_implicitly_has_an_any_type_because_index_expression_is_not_of_type_number); + } + else { + let suggestion: string | undefined; + if (propName !== undefined && (suggestion = getSuggestionForNonexistentProperty(propName as string, objectType))) { + if (suggestion !== undefined) { + error(accessExpression.argumentExpression, Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_2, (propName as string), typeToString(objectType), suggestion); + } + } + else { + const suggestion = getSuggestionForNonexistentIndexSignature(objectType, accessExpression, indexType); + if (suggestion !== undefined) { + error(accessExpression, Diagnostics.Element_implicitly_has_an_any_type_because_type_0_has_no_index_signature_Did_you_mean_to_call_1, typeToString(objectType), suggestion); + } + else { + let errorInfo: DiagnosticMessageChain | undefined; + if (indexType.flags & TypeFlags.EnumLiteral) { + errorInfo = chainDiagnosticMessages(/* details */ undefined, Diagnostics.Property_0_does_not_exist_on_type_1, "[" + typeToString(indexType) + "]", typeToString(objectType)); + } + else if (indexType.flags & TypeFlags.UniqueESSymbol) { + const symbolName = getFullyQualifiedName((indexType as UniqueESSymbolType).symbol, accessExpression); + errorInfo = chainDiagnosticMessages(/* details */ undefined, Diagnostics.Property_0_does_not_exist_on_type_1, "[" + symbolName + "]", typeToString(objectType)); + } + else if (indexType.flags & TypeFlags.StringLiteral) { + errorInfo = chainDiagnosticMessages(/* details */ undefined, Diagnostics.Property_0_does_not_exist_on_type_1, (indexType as StringLiteralType).value, typeToString(objectType)); + } + else if (indexType.flags & TypeFlags.NumberLiteral) { + errorInfo = chainDiagnosticMessages(/* details */ undefined, Diagnostics.Property_0_does_not_exist_on_type_1, (indexType as NumberLiteralType).value, typeToString(objectType)); + } + else if (indexType.flags & (TypeFlags.Number | TypeFlags.String)) { + errorInfo = chainDiagnosticMessages(/* details */ undefined, Diagnostics.No_index_signature_with_a_parameter_of_type_0_was_found_on_type_1, typeToString(indexType), typeToString(objectType)); + } + errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Element_implicitly_has_an_any_type_because_expression_of_type_0_can_t_be_used_to_index_type_1, typeToString(fullIndexType), typeToString(objectType)); + diagnostics.add(createDiagnosticForNodeFromMessageChain(accessExpression, errorInfo)); + } + } + } } + return undefined; } } - - function getExplicitThisType(node: Expression) { - const container = getThisContainer(node, /*includeArrowFunctions*/ false); - if (isFunctionLike(container)) { - const signature = getSignatureFromDeclaration(container); - if (signature.thisParameter) { - return getExplicitTypeOfSymbol(signature.thisParameter); - } + if (isJSLiteralType(objectType)) { + return anyType; + } + if (accessNode) { + const indexNode = getIndexNodeForAccessExpression(accessNode); + if (indexType.flags & (TypeFlags.StringLiteral | TypeFlags.NumberLiteral)) { + error(indexNode, Diagnostics.Property_0_does_not_exist_on_type_1, "" + (indexType).value, typeToString(objectType)); + } + else if (indexType.flags & (TypeFlags.String | TypeFlags.Number)) { + error(indexNode, Diagnostics.Type_0_has_no_matching_index_signature_for_type_1, typeToString(objectType), typeToString(indexType)); } - if (isClassLike(container.parent)) { - const symbol = getSymbolOfNode(container.parent); - return hasModifier(container, ModifierFlags.Static) ? getTypeOfSymbol(symbol) : (getDeclaredTypeOfSymbol(symbol) as InterfaceType).thisType!; + else { + error(indexNode, Diagnostics.Type_0_cannot_be_used_as_an_index_type, typeToString(indexType)); } } - - function getClassNameFromPrototypeMethod(container: Node) { - // Check if it's the RHS of a x.prototype.y = function [name]() { .... } - if (container.kind === SyntaxKind.FunctionExpression && - isBinaryExpression(container.parent) && - getAssignmentDeclarationKind(container.parent) === AssignmentDeclarationKind.PrototypeProperty) { - // Get the 'x' of 'x.prototype.y = container' - return ((container.parent // x.prototype.y = container - .left as PropertyAccessExpression) // x.prototype.y - .expression as PropertyAccessExpression) // x.prototype - .expression; // x - } - // x.prototype = { method() { } } - else if (container.kind === SyntaxKind.MethodDeclaration && - container.parent.kind === SyntaxKind.ObjectLiteralExpression && - isBinaryExpression(container.parent.parent) && - getAssignmentDeclarationKind(container.parent.parent) === AssignmentDeclarationKind.Prototype) { - return (container.parent.parent.left as PropertyAccessExpression).expression; - } - // x.prototype = { method: function() { } } - else if (container.kind === SyntaxKind.FunctionExpression && - container.parent.kind === SyntaxKind.PropertyAssignment && - container.parent.parent.kind === SyntaxKind.ObjectLiteralExpression && - isBinaryExpression(container.parent.parent.parent) && - getAssignmentDeclarationKind(container.parent.parent.parent) === AssignmentDeclarationKind.Prototype) { - return (container.parent.parent.parent.left as PropertyAccessExpression).expression; - } - // Object.defineProperty(x, "method", { value: function() { } }); - // Object.defineProperty(x, "method", { set: (x: () => void) => void }); - // Object.defineProperty(x, "method", { get: () => function() { }) }); - else if (container.kind === SyntaxKind.FunctionExpression && - isPropertyAssignment(container.parent) && - isIdentifier(container.parent.name) && - (container.parent.name.escapedText === "value" || container.parent.name.escapedText === "get" || container.parent.name.escapedText === "set") && - isObjectLiteralExpression(container.parent.parent) && - isCallExpression(container.parent.parent.parent) && - container.parent.parent.parent.arguments[2] === container.parent.parent && - getAssignmentDeclarationKind(container.parent.parent.parent) === AssignmentDeclarationKind.ObjectDefinePrototypeProperty) { - return (container.parent.parent.parent.arguments[0] as PropertyAccessExpression).expression; - } - // Object.defineProperty(x, "method", { value() { } }); - // Object.defineProperty(x, "method", { set(x: () => void) {} }); - // Object.defineProperty(x, "method", { get() { return () => {} } }); - else if (isMethodDeclaration(container) && - isIdentifier(container.name) && - (container.name.escapedText === "value" || container.name.escapedText === "get" || container.name.escapedText === "set") && - isObjectLiteralExpression(container.parent) && - isCallExpression(container.parent.parent) && - container.parent.parent.arguments[2] === container.parent && - getAssignmentDeclarationKind(container.parent.parent) === AssignmentDeclarationKind.ObjectDefinePrototypeProperty) { - return (container.parent.parent.arguments[0] as PropertyAccessExpression).expression; + if (isTypeAny(indexType)) { + return indexType; + } + return undefined; + function errorIfWritingToReadonlyIndex(indexInfo: IndexInfo | undefined): void { + if (indexInfo && indexInfo.isReadonly && accessExpression && (isAssignmentTarget(accessExpression) || isDeleteTarget(accessExpression))) { + error(accessExpression, Diagnostics.Index_signature_in_type_0_only_permits_reading, typeToString(objectType)); } } - - function getTypeForThisExpressionFromJSDoc(node: Node) { - const jsdocType = getJSDocType(node); - if (jsdocType && jsdocType.kind === SyntaxKind.JSDocFunctionType) { - const jsDocFunctionType = jsdocType; - if (jsDocFunctionType.parameters.length > 0 && - jsDocFunctionType.parameters[0].name && - (jsDocFunctionType.parameters[0].name as Identifier).escapedText === InternalSymbolName.This) { - return getTypeFromTypeNode(jsDocFunctionType.parameters[0].type!); - } + } + function getIndexNodeForAccessExpression(accessNode: ElementAccessExpression | IndexedAccessTypeNode | PropertyName | BindingName | SyntheticExpression) { + return accessNode.kind === SyntaxKind.ElementAccessExpression ? accessNode.argumentExpression : + accessNode.kind === SyntaxKind.IndexedAccessType ? accessNode.indexType : + accessNode.kind === SyntaxKind.ComputedPropertyName ? accessNode.expression : + accessNode; + } + function isGenericObjectType(type: ts.Type): boolean { + if (type.flags & TypeFlags.UnionOrIntersection) { + if (!((type).objectFlags & ObjectFlags.IsGenericObjectTypeComputed)) { + (type).objectFlags |= ObjectFlags.IsGenericObjectTypeComputed | + (some((type).types, isGenericObjectType) ? ObjectFlags.IsGenericObjectType : 0); } - const thisTag = getJSDocThisTag(node); - if (thisTag && thisTag.typeExpression) { - return getTypeFromTypeNode(thisTag.typeExpression); + return !!((type).objectFlags & ObjectFlags.IsGenericObjectType); + } + return !!(type.flags & TypeFlags.InstantiableNonPrimitive) || isGenericMappedType(type); + } + function isGenericIndexType(type: ts.Type): boolean { + if (type.flags & TypeFlags.UnionOrIntersection) { + if (!((type).objectFlags & ObjectFlags.IsGenericIndexTypeComputed)) { + (type).objectFlags |= ObjectFlags.IsGenericIndexTypeComputed | + (some((type).types, isGenericIndexType) ? ObjectFlags.IsGenericIndexType : 0); } + return !!((type).objectFlags & ObjectFlags.IsGenericIndexType); } - - function isInConstructorArgumentInitializer(node: Node, constructorDecl: Node): boolean { - return !!findAncestor(node, n => isFunctionLikeDeclaration(n) ? "quit" : n.kind === SyntaxKind.Parameter && n.parent === constructorDecl); + return !!(type.flags & (TypeFlags.InstantiableNonPrimitive | TypeFlags.Index)); + } + function isThisTypeParameter(type: ts.Type): boolean { + return !!(type.flags & TypeFlags.TypeParameter && (type).isThisType); + } + function getSimplifiedType(type: ts.Type, writing: boolean): ts.Type { + return type.flags & TypeFlags.IndexedAccess ? getSimplifiedIndexedAccessType((type), writing) : + type.flags & TypeFlags.Conditional ? getSimplifiedConditionalType((type), writing) : + type; + } + function distributeIndexOverObjectType(objectType: ts.Type, indexType: ts.Type, writing: boolean) { + // (T | U)[K] -> T[K] | U[K] (reading) + // (T | U)[K] -> T[K] & U[K] (writing) + // (T & U)[K] -> T[K] & U[K] + if (objectType.flags & TypeFlags.UnionOrIntersection) { + const types = map((objectType as UnionOrIntersectionType).types, t => getSimplifiedType(getIndexedAccessType(t, indexType), writing)); + return objectType.flags & TypeFlags.Intersection || writing ? getIntersectionType(types) : getUnionType(types); } - - function checkSuperExpression(node: Node): Type { - const isCallExpression = node.parent.kind === SyntaxKind.CallExpression && (node.parent).expression === node; - - let container = getSuperContainer(node, /*stopOnFunctions*/ true); - let needToCaptureLexicalThis = false; - - // adjust the container reference in case if super is used inside arrow functions with arbitrarily deep nesting - if (!isCallExpression) { - while (container && container.kind === SyntaxKind.ArrowFunction) { - container = getSuperContainer(container, /*stopOnFunctions*/ true); - needToCaptureLexicalThis = languageVersion < ScriptTarget.ES2015; - } - } - - const canUseSuperExpression = isLegalUsageOfSuperExpression(container); - let nodeCheckFlag: NodeCheckFlags = 0; - - if (!canUseSuperExpression) { - // issue more specific error if super is used in computed property name - // class A { foo() { return "1" }} - // class B { - // [super.foo()]() {} - // } - const current = findAncestor(node, n => n === container ? "quit" : n.kind === SyntaxKind.ComputedPropertyName); - if (current && current.kind === SyntaxKind.ComputedPropertyName) { - error(node, Diagnostics.super_cannot_be_referenced_in_a_computed_property_name); - } - else if (isCallExpression) { - error(node, Diagnostics.Super_calls_are_not_permitted_outside_constructors_or_in_nested_functions_inside_constructors); - } - else if (!container || !container.parent || !(isClassLike(container.parent) || container.parent.kind === SyntaxKind.ObjectLiteralExpression)) { - error(node, Diagnostics.super_can_only_be_referenced_in_members_of_derived_classes_or_object_literal_expressions); - } - else { - error(node, Diagnostics.super_property_access_is_permitted_only_in_a_constructor_member_function_or_member_accessor_of_a_derived_class); - } - return errorType; + } + function distributeObjectOverIndexType(objectType: ts.Type, indexType: ts.Type, writing: boolean) { + // T[A | B] -> T[A] | T[B] (reading) + // T[A | B] -> T[A] & T[B] (writing) + if (indexType.flags & TypeFlags.Union) { + const types = map((indexType as UnionType).types, t => getSimplifiedType(getIndexedAccessType(objectType, t), writing)); + return writing ? getIntersectionType(types) : getUnionType(types); + } + } + function unwrapSubstitution(type: ts.Type): ts.Type { + if (type.flags & TypeFlags.Substitution) { + return (type as SubstitutionType).substitute; + } + return type; + } + // Transform an indexed access to a simpler form, if possible. Return the simpler form, or return + // the type itself if no transformation is possible. The writing flag indicates that the type is + // the target of an assignment. + function getSimplifiedIndexedAccessType(type: IndexedAccessType, writing: boolean): ts.Type { + const cache = writing ? "simplifiedForWriting" : "simplifiedForReading"; + if (type[cache]) { + return type[cache] === circularConstraintType ? type : type[cache]!; + } + type[cache] = circularConstraintType; + // We recursively simplify the object type as it may in turn be an indexed access type. For example, with + // '{ [P in T]: { [Q in U]: number } }[T][U]' we want to first simplify the inner indexed access type. + const objectType = unwrapSubstitution(getSimplifiedType(type.objectType, writing)); + const indexType = getSimplifiedType(type.indexType, writing); + // T[A | B] -> T[A] | T[B] (reading) + // T[A | B] -> T[A] & T[B] (writing) + const distributedOverIndex = distributeObjectOverIndexType(objectType, indexType, writing); + if (distributedOverIndex) { + return type[cache] = distributedOverIndex; + } + // Only do the inner distributions if the index can no longer be instantiated to cause index distribution again + if (!(indexType.flags & TypeFlags.Instantiable)) { + // (T | U)[K] -> T[K] | U[K] (reading) + // (T | U)[K] -> T[K] & U[K] (writing) + // (T & U)[K] -> T[K] & U[K] + const distributedOverObject = distributeIndexOverObjectType(objectType, indexType, writing); + if (distributedOverObject) { + return type[cache] = distributedOverObject; } - - if (!isCallExpression && container.kind === SyntaxKind.Constructor) { - checkThisBeforeSuper(node, container, Diagnostics.super_must_be_called_before_accessing_a_property_of_super_in_the_constructor_of_a_derived_class); + } + // So ultimately (reading): + // ((A & B) | C)[K1 | K2] -> ((A & B) | C)[K1] | ((A & B) | C)[K2] -> (A & B)[K1] | C[K1] | (A & B)[K2] | C[K2] -> (A[K1] & B[K1]) | C[K1] | (A[K2] & B[K2]) | C[K2] + // If the object type is a mapped type { [P in K]: E }, where K is generic, instantiate E using a mapper + // that substitutes the index type for P. For example, for an index access { [P in K]: Box }[X], we + // construct the type Box. + if (isGenericMappedType(objectType)) { + return type[cache] = mapType(substituteIndexedMappedType(objectType, type.indexType), t => getSimplifiedType(t, writing)); + } + return type[cache] = type; + } + function getSimplifiedConditionalType(type: ConditionalType, writing: boolean) { + const checkType = type.checkType; + const extendsType = type.extendsType; + const trueType = getTrueTypeFromConditionalType(type); + const falseType = getFalseTypeFromConditionalType(type); + // Simplifications for types of the form `T extends U ? T : never` and `T extends U ? never : T`. + if (falseType.flags & TypeFlags.Never && getActualTypeVariable(trueType) === getActualTypeVariable(checkType)) { + if (checkType.flags & TypeFlags.Any || isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(extendsType))) { // Always true + return getSimplifiedType(trueType, writing); + } + else if (isIntersectionEmpty(checkType, extendsType)) { // Always false + return neverType; } - - if (hasModifier(container, ModifierFlags.Static) || isCallExpression) { - nodeCheckFlag = NodeCheckFlags.SuperStatic; + } + else if (trueType.flags & TypeFlags.Never && getActualTypeVariable(falseType) === getActualTypeVariable(checkType)) { + if (!(checkType.flags & TypeFlags.Any) && isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(extendsType))) { // Always true + return neverType; } - else { - nodeCheckFlag = NodeCheckFlags.SuperInstance; + else if (checkType.flags & TypeFlags.Any || isIntersectionEmpty(checkType, extendsType)) { // Always false + return getSimplifiedType(falseType, writing); } - - getNodeLinks(node).flags |= nodeCheckFlag; - - // Due to how we emit async functions, we need to specialize the emit for an async method that contains a `super` reference. - // This is due to the fact that we emit the body of an async function inside of a generator function. As generator - // functions cannot reference `super`, we emit a helper inside of the method body, but outside of the generator. This helper - // uses an arrow function, which is permitted to reference `super`. - // - // There are two primary ways we can access `super` from within an async method. The first is getting the value of a property - // or indexed access on super, either as part of a right-hand-side expression or call expression. The second is when setting the value - // of a property or indexed access, either as part of an assignment expression or destructuring assignment. - // - // The simplest case is reading a value, in which case we will emit something like the following: - // - // // ts - // ... - // async asyncMethod() { - // let x = await super.asyncMethod(); - // return x; - // } - // ... - // - // // js - // ... - // asyncMethod() { - // const _super = Object.create(null, { - // asyncMethod: { get: () => super.asyncMethod }, - // }); - // return __awaiter(this, arguments, Promise, function *() { - // let x = yield _super.asyncMethod.call(this); - // return x; - // }); - // } - // ... - // - // The more complex case is when we wish to assign a value, especially as part of a destructuring assignment. As both cases - // are legal in ES6, but also likely less frequent, we only emit setters if there is an assignment: - // - // // ts - // ... - // async asyncMethod(ar: Promise) { - // [super.a, super.b] = await ar; - // } - // ... - // - // // js - // ... - // asyncMethod(ar) { - // const _super = Object.create(null, { - // a: { get: () => super.a, set: (v) => super.a = v }, - // b: { get: () => super.b, set: (v) => super.b = v } - // }; - // return __awaiter(this, arguments, Promise, function *() { - // [_super.a, _super.b] = yield ar; - // }); - // } - // ... - // - // Creating an object that has getter and setters instead of just an accessor function is required for destructuring assignments - // as a call expression cannot be used as the target of a destructuring assignment while a property access can. - // - // For element access expressions (`super[x]`), we emit a generic helper that forwards the element access in both situations. - if (container.kind === SyntaxKind.MethodDeclaration && hasModifier(container, ModifierFlags.Async)) { - if (isSuperProperty(node.parent) && isAssignmentTarget(node.parent)) { - getNodeLinks(container).flags |= NodeCheckFlags.AsyncMethodWithSuperBinding; - } - else { - getNodeLinks(container).flags |= NodeCheckFlags.AsyncMethodWithSuper; - } + } + return type; + } + /** + * Invokes union simplification logic to determine if an intersection is considered empty as a union constituent + */ + function isIntersectionEmpty(type1: ts.Type, type2: ts.Type) { + return !!(getUnionType([intersectTypes(type1, type2), neverType]).flags & TypeFlags.Never); + } + function substituteIndexedMappedType(objectType: MappedType, index: ts.Type) { + const mapper = createTypeMapper([getTypeParameterFromMappedType(objectType)], [index]); + const templateMapper = combineTypeMappers(objectType.mapper, mapper); + return instantiateType(getTemplateTypeFromMappedType(objectType), templateMapper); + } + function getIndexedAccessType(objectType: ts.Type, indexType: ts.Type, accessNode?: ElementAccessExpression | IndexedAccessTypeNode | PropertyName | BindingName | SyntheticExpression): ts.Type { + return getIndexedAccessTypeOrUndefined(objectType, indexType, accessNode, AccessFlags.None) || (accessNode ? errorType : unknownType); + } + function getIndexedAccessTypeOrUndefined(objectType: ts.Type, indexType: ts.Type, accessNode?: ElementAccessExpression | IndexedAccessTypeNode | PropertyName | BindingName | SyntheticExpression, accessFlags = AccessFlags.None): ts.Type | undefined { + if (objectType === wildcardType || indexType === wildcardType) { + return wildcardType; + } + // If the object type has a string index signature and no other members we know that the result will + // always be the type of that index signature and we can simplify accordingly. + if (isStringIndexSignatureOnlyType(objectType) && !(indexType.flags & TypeFlags.Nullable) && isTypeAssignableToKind(indexType, TypeFlags.String | TypeFlags.Number)) { + indexType = stringType; + } + // If the index type is generic, or if the object type is generic and doesn't originate in an expression, + // we are performing a higher-order index access where we cannot meaningfully access the properties of the + // object type. Note that for a generic T and a non-generic K, we eagerly resolve T[K] if it originates in + // an expression. This is to preserve backwards compatibility. For example, an element access 'this["foo"]' + // has always been resolved eagerly using the constraint type of 'this' at the given location. + if (isGenericIndexType(indexType) || !(accessNode && accessNode.kind !== SyntaxKind.IndexedAccessType) && isGenericObjectType(objectType)) { + if (objectType.flags & TypeFlags.AnyOrUnknown) { + return objectType; } - - if (needToCaptureLexicalThis) { - // call expressions are allowed only in constructors so they should always capture correct 'this' - // super property access expressions can also appear in arrow functions - - // in this case they should also use correct lexical this - captureLexicalThis(node.parent, container); + // Defer the operation by creating an indexed access type. + const id = objectType.id + "," + indexType.id; + let type = indexedAccessTypes.get(id); + if (!type) { + indexedAccessTypes.set(id, type = createIndexedAccessType(objectType, indexType)); } - - if (container.parent.kind === SyntaxKind.ObjectLiteralExpression) { - if (languageVersion < ScriptTarget.ES2015) { - error(node, Diagnostics.super_is_only_allowed_in_members_of_object_literal_expressions_when_option_target_is_ES2015_or_higher); - return errorType; + return type; + } + // In the following we resolve T[K] to the type of the property in T selected by K. + // We treat boolean as different from other unions to improve errors; + // skipping straight to getPropertyTypeForIndexType gives errors with 'boolean' instead of 'true'. + const apparentObjectType = getApparentType(objectType); + if (indexType.flags & TypeFlags.Union && !(indexType.flags & TypeFlags.Boolean)) { + const propTypes: ts.Type[] = []; + let wasMissingProp = false; + for (const t of (indexType).types) { + const propType = getPropertyTypeForIndexType(objectType, apparentObjectType, t, indexType, wasMissingProp, accessNode, accessFlags); + if (propType) { + propTypes.push(propType); + } + else if (!accessNode) { + // If there's no error node, we can immeditely stop, since error reporting is off + return undefined; } else { - // for object literal assume that type of 'super' is 'any' - return anyType; + // Otherwise we set a flag and return at the end of the loop so we still mark all errors + wasMissingProp = true; } } - - // at this point the only legal case for parent is ClassLikeDeclaration - const classLikeDeclaration = container.parent; - if (!getClassExtendsHeritageElement(classLikeDeclaration)) { - error(node, Diagnostics.super_can_only_be_referenced_in_a_derived_class); - return errorType; + if (wasMissingProp) { + return undefined; } - - const classType = getDeclaredTypeOfSymbol(getSymbolOfNode(classLikeDeclaration)); - const baseClassType = classType && getBaseTypes(classType)[0]; - if (!baseClassType) { - return errorType; + return accessFlags & AccessFlags.Writing ? getIntersectionType(propTypes) : getUnionType(propTypes); + } + return getPropertyTypeForIndexType(objectType, apparentObjectType, indexType, indexType, /* supressNoImplicitAnyError */ false, accessNode, accessFlags | AccessFlags.CacheSymbol); + } + function getTypeFromIndexedAccessTypeNode(node: IndexedAccessTypeNode) { + const links = getNodeLinks(node); + if (!links.resolvedType) { + const objectType = getTypeFromTypeNode(node.objectType); + const indexType = getTypeFromTypeNode(node.indexType); + const resolved = getIndexedAccessType(objectType, indexType, node); + links.resolvedType = resolved.flags & TypeFlags.IndexedAccess && + (resolved).objectType === objectType && + (resolved).indexType === indexType ? + getConstrainedTypeVariable((resolved), node) : resolved; + } + return links.resolvedType; + } + function getTypeFromMappedTypeNode(node: MappedTypeNode): ts.Type { + const links = getNodeLinks(node); + if (!links.resolvedType) { + const type = (createObjectType(ObjectFlags.Mapped, node.symbol)); + type.declaration = node; + type.aliasSymbol = getAliasSymbolForTypeNode(node); + type.aliasTypeArguments = getTypeArgumentsForAliasSymbol(type.aliasSymbol); + links.resolvedType = type; + // Eagerly resolve the constraint type which forces an error if the constraint type circularly + // references itself through one or more type aliases. + getConstraintTypeFromMappedType(type); + } + return links.resolvedType; + } + function getActualTypeVariable(type: ts.Type): ts.Type { + if (type.flags & TypeFlags.Substitution) { + return (type).typeVariable; + } + if (type.flags & TypeFlags.IndexedAccess && ((type).objectType.flags & TypeFlags.Substitution || + (type).indexType.flags & TypeFlags.Substitution)) { + return getIndexedAccessType(getActualTypeVariable((type).objectType), getActualTypeVariable((type).indexType)); + } + return type; + } + function getConditionalType(root: ConditionalRoot, mapper: TypeMapper | undefined): ts.Type { + const checkType = instantiateType(root.checkType, mapper); + const extendsType = instantiateType(root.extendsType, mapper); + if (checkType === wildcardType || extendsType === wildcardType) { + return wildcardType; + } + const checkTypeInstantiable = isGenericObjectType(checkType) || isGenericIndexType(checkType); + let combinedMapper: TypeMapper | undefined; + if (root.inferTypeParameters) { + const context = createInferenceContext(root.inferTypeParameters, /*signature*/ undefined, InferenceFlags.None); + // We skip inference of the possible `infer` types unles the `extendsType` _is_ an infer type + // if it was, it's trivial to say that extendsType = checkType, however such a pattern is used to + // "reset" the type being build up during constraint calculation and avoid making an apparently "infinite" constraint + // so in those cases we refain from performing inference and retain the uninfered type parameter + if (!checkTypeInstantiable || !some(root.inferTypeParameters, t => t === extendsType)) { + // We don't want inferences from constraints as they may cause us to eagerly resolve the + // conditional type instead of deferring resolution. Also, we always want strict function + // types rules (i.e. proper contravariance) for inferences. + inferTypes(context.inferences, checkType, extendsType, InferencePriority.NoConstraints | InferencePriority.AlwaysStrict); + } + combinedMapper = combineTypeMappers(mapper, context.mapper); + } + // Instantiate the extends type including inferences for 'infer T' type parameters + const inferredExtendsType = combinedMapper ? instantiateType(root.extendsType, combinedMapper) : extendsType; + // We attempt to resolve the conditional type only when the check and extends types are non-generic + if (!checkTypeInstantiable && !isGenericObjectType(inferredExtendsType) && !isGenericIndexType(inferredExtendsType)) { + if (inferredExtendsType.flags & TypeFlags.AnyOrUnknown) { + return instantiateType(root.trueType, combinedMapper || mapper); + } + // Return union of trueType and falseType for 'any' since it matches anything + if (checkType.flags & TypeFlags.Any) { + return getUnionType([instantiateType(root.trueType, combinedMapper || mapper), instantiateType(root.falseType, mapper)]); + } + // Return falseType for a definitely false extends check. We check an instantiations of the two + // types with type parameters mapped to the wildcard type, the most permissive instantiations + // possible (the wildcard type is assignable to and from all types). If those are not related, + // then no instantiations will be and we can just return the false branch type. + if (!isTypeAssignableTo(getPermissiveInstantiation(checkType), getPermissiveInstantiation(inferredExtendsType))) { + return instantiateType(root.falseType, mapper); + } + // Return trueType for a definitely true extends check. We check instantiations of the two + // types with type parameters mapped to their restrictive form, i.e. a form of the type parameter + // that has no constraint. This ensures that, for example, the type + // type Foo = T extends { x: string } ? string : number + // doesn't immediately resolve to 'string' instead of being deferred. + if (isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(inferredExtendsType))) { + return instantiateType(root.trueType, combinedMapper || mapper); + } + } + // Return a deferred type for a check that is neither definitely true nor definitely false + const erasedCheckType = getActualTypeVariable(checkType); + const result = (createType(TypeFlags.Conditional)); + result.root = root; + result.checkType = erasedCheckType; + result.extendsType = extendsType; + result.mapper = mapper; + result.combinedMapper = combinedMapper; + result.aliasSymbol = root.aliasSymbol; + result.aliasTypeArguments = instantiateTypes(root.aliasTypeArguments, mapper!); // TODO: GH#18217 + return result; + } + function getTrueTypeFromConditionalType(type: ConditionalType) { + return type.resolvedTrueType || (type.resolvedTrueType = instantiateType(type.root.trueType, type.mapper)); + } + function getFalseTypeFromConditionalType(type: ConditionalType) { + return type.resolvedFalseType || (type.resolvedFalseType = instantiateType(type.root.falseType, type.mapper)); + } + function getInferredTrueTypeFromConditionalType(type: ConditionalType) { + return type.resolvedInferredTrueType || (type.resolvedInferredTrueType = type.combinedMapper ? instantiateType(type.root.trueType, type.combinedMapper) : getTrueTypeFromConditionalType(type)); + } + function getInferTypeParameters(node: ConditionalTypeNode): TypeParameter[] | undefined { + let result: TypeParameter[] | undefined; + if (node.locals) { + node.locals.forEach(symbol => { + if (symbol.flags & SymbolFlags.TypeParameter) { + result = append(result, getDeclaredTypeOfSymbol(symbol)); + } + }); + } + return result; + } + function getTypeFromConditionalTypeNode(node: ConditionalTypeNode): ts.Type { + const links = getNodeLinks(node); + if (!links.resolvedType) { + const checkType = getTypeFromTypeNode(node.checkType); + const aliasSymbol = getAliasSymbolForTypeNode(node); + const aliasTypeArguments = getTypeArgumentsForAliasSymbol(aliasSymbol); + const allOuterTypeParameters = getOuterTypeParameters(node, /*includeThisTypes*/ true); + const outerTypeParameters = aliasTypeArguments ? allOuterTypeParameters : filter(allOuterTypeParameters, tp => isTypeParameterPossiblyReferenced(tp, node)); + const root: ConditionalRoot = { + node, + checkType, + extendsType: getTypeFromTypeNode(node.extendsType), + trueType: getTypeFromTypeNode(node.trueType), + falseType: getTypeFromTypeNode(node.falseType), + isDistributive: !!(checkType.flags & TypeFlags.TypeParameter), + inferTypeParameters: getInferTypeParameters(node), + outerTypeParameters, + instantiations: undefined, + aliasSymbol, + aliasTypeArguments + }; + links.resolvedType = getConditionalType(root, /*mapper*/ undefined); + if (outerTypeParameters) { + root.instantiations = createMap(); + root.instantiations.set(getTypeListId(outerTypeParameters), links.resolvedType); } - - if (container.kind === SyntaxKind.Constructor && isInConstructorArgumentInitializer(node, container)) { - // issue custom error message for super property access in constructor arguments (to be aligned with old compiler) - error(node, Diagnostics.super_cannot_be_referenced_in_constructor_arguments); - return errorType; + } + return links.resolvedType; + } + function getTypeFromInferTypeNode(node: InferTypeNode): ts.Type { + const links = getNodeLinks(node); + if (!links.resolvedType) { + links.resolvedType = getDeclaredTypeOfTypeParameter(getSymbolOfNode(node.typeParameter)); + } + return links.resolvedType; + } + function getIdentifierChain(node: EntityName): Identifier[] { + if (isIdentifier(node)) { + return [node]; + } + else { + return append(getIdentifierChain(node.left), node.right); + } + } + function getTypeFromImportTypeNode(node: ImportTypeNode): ts.Type { + const links = getNodeLinks(node); + if (!links.resolvedType) { + if (node.isTypeOf && node.typeArguments) { // Only the non-typeof form can make use of type arguments + error(node, Diagnostics.Type_arguments_cannot_be_used_here); + links.resolvedSymbol = unknownSymbol; + return links.resolvedType = errorType; + } + if (!isLiteralImportTypeNode(node)) { + error(node.argument, Diagnostics.String_literal_expected); + links.resolvedSymbol = unknownSymbol; + return links.resolvedType = errorType; + } + const targetMeaning = node.isTypeOf ? SymbolFlags.Value : node.flags & NodeFlags.JSDoc ? SymbolFlags.Value | SymbolFlags.Type : SymbolFlags.Type; + // TODO: Future work: support unions/generics/whatever via a deferred import-type + const innerModuleSymbol = resolveExternalModuleName(node, node.argument.literal); + if (!innerModuleSymbol) { + links.resolvedSymbol = unknownSymbol; + return links.resolvedType = errorType; + } + const moduleSymbol = resolveExternalModuleSymbol(innerModuleSymbol, /*dontResolveAlias*/ false); + if (!nodeIsMissing(node.qualifier)) { + const nameStack: Identifier[] = getIdentifierChain(node.qualifier!); + let currentNamespace = moduleSymbol; + let current: Identifier | undefined; + while (current = nameStack.shift()) { + const meaning = nameStack.length ? SymbolFlags.Namespace : targetMeaning; + const next = getSymbol(getExportsOfSymbol(getMergedSymbol(resolveSymbol(currentNamespace))), current.escapedText, meaning); + if (!next) { + error(current, Diagnostics.Namespace_0_has_no_exported_member_1, getFullyQualifiedName(currentNamespace), declarationNameToString(current)); + return links.resolvedType = errorType; + } + getNodeLinks(current).resolvedSymbol = next; + getNodeLinks(current.parent).resolvedSymbol = next; + currentNamespace = next; + } + links.resolvedType = resolveImportSymbolType(node, links, currentNamespace, targetMeaning); } - - return nodeCheckFlag === NodeCheckFlags.SuperStatic - ? getBaseConstructorTypeOfClass(classType) - : getTypeWithThisArgument(baseClassType, classType.thisType); - - function isLegalUsageOfSuperExpression(container: Node): boolean { - if (!container) { - return false; - } - - if (isCallExpression) { - // TS 1.0 SPEC (April 2014): 4.8.1 - // Super calls are only permitted in constructors of derived classes - return container.kind === SyntaxKind.Constructor; + else { + if (moduleSymbol.flags & targetMeaning) { + links.resolvedType = resolveImportSymbolType(node, links, moduleSymbol, targetMeaning); } else { - // TS 1.0 SPEC (April 2014) - // 'super' property access is allowed - // - In a constructor, instance member function, instance member accessor, or instance member variable initializer where this references a derived class instance - // - In a static member function or static member accessor - - // topmost container must be something that is directly nested in the class declaration\object literal expression - if (isClassLike(container.parent) || container.parent.kind === SyntaxKind.ObjectLiteralExpression) { - if (hasModifier(container, ModifierFlags.Static)) { - return container.kind === SyntaxKind.MethodDeclaration || - container.kind === SyntaxKind.MethodSignature || - container.kind === SyntaxKind.GetAccessor || - container.kind === SyntaxKind.SetAccessor; - } - else { - return container.kind === SyntaxKind.MethodDeclaration || - container.kind === SyntaxKind.MethodSignature || - container.kind === SyntaxKind.GetAccessor || - container.kind === SyntaxKind.SetAccessor || - container.kind === SyntaxKind.PropertyDeclaration || - container.kind === SyntaxKind.PropertySignature || - container.kind === SyntaxKind.Constructor; - } - } + const errorMessage = targetMeaning === SymbolFlags.Value + ? Diagnostics.Module_0_does_not_refer_to_a_value_but_is_used_as_a_value_here + : Diagnostics.Module_0_does_not_refer_to_a_type_but_is_used_as_a_type_here_Did_you_mean_typeof_import_0; + error(node, errorMessage, node.argument.literal.text); + links.resolvedSymbol = unknownSymbol; + links.resolvedType = errorType; } - - return false; } } - - function getContainingObjectLiteral(func: SignatureDeclaration): ObjectLiteralExpression | undefined { - return (func.kind === SyntaxKind.MethodDeclaration || - func.kind === SyntaxKind.GetAccessor || - func.kind === SyntaxKind.SetAccessor) && func.parent.kind === SyntaxKind.ObjectLiteralExpression ? func.parent : - func.kind === SyntaxKind.FunctionExpression && func.parent.kind === SyntaxKind.PropertyAssignment ? func.parent.parent : - undefined; - } - - function getThisTypeArgument(type: Type): Type | undefined { - return getObjectFlags(type) & ObjectFlags.Reference && (type).target === globalThisType ? getTypeArguments(type)[0] : undefined; + return links.resolvedType; + } + function resolveImportSymbolType(node: ImportTypeNode, links: NodeLinks, symbol: ts.Symbol, meaning: SymbolFlags) { + const resolvedSymbol = resolveSymbol(symbol); + links.resolvedSymbol = resolvedSymbol; + if (meaning === SymbolFlags.Value) { + return getTypeOfSymbol(symbol); // intentionally doesn't use resolved symbol so type is cached as expected on the alias } - - function getThisTypeFromContextualType(type: Type): Type | undefined { - return mapType(type, t => { - return t.flags & TypeFlags.Intersection ? forEach((t).types, getThisTypeArgument) : getThisTypeArgument(t); - }); + else { + return getTypeReferenceType(node, resolvedSymbol); // getTypeReferenceType doesn't handle aliases - it must get the resolved symbol } - - function getContextualThisParameterType(func: SignatureDeclaration): Type | undefined { - if (func.kind === SyntaxKind.ArrowFunction) { - return undefined; + } + function getTypeFromTypeLiteralOrFunctionOrConstructorTypeNode(node: TypeNode): ts.Type { + const links = getNodeLinks(node); + if (!links.resolvedType) { + // Deferred resolution of members is handled by resolveObjectTypeMembers + const aliasSymbol = getAliasSymbolForTypeNode(node); + if (getMembersOfSymbol(node.symbol).size === 0 && !aliasSymbol) { + links.resolvedType = emptyTypeLiteralType; } - if (isContextSensitiveFunctionOrObjectLiteralMethod(func)) { - const contextualSignature = getContextualSignature(func); - if (contextualSignature) { - const thisParameter = contextualSignature.thisParameter; - if (thisParameter) { - return getTypeOfSymbol(thisParameter); - } - } - } - const inJs = isInJSFile(func); - if (noImplicitThis || inJs) { - const containingLiteral = getContainingObjectLiteral(func); - if (containingLiteral) { - // We have an object literal method. Check if the containing object literal has a contextual type - // that includes a ThisType. If so, T is the contextual type for 'this'. We continue looking in - // any directly enclosing object literals. - const contextualType = getApparentTypeOfContextualType(containingLiteral); - let literal = containingLiteral; - let type = contextualType; - while (type) { - const thisType = getThisTypeFromContextualType(type); - if (thisType) { - return instantiateType(thisType, getMapperFromContext(getInferenceContext(containingLiteral))); - } - if (literal.parent.kind !== SyntaxKind.PropertyAssignment) { - break; - } - literal = literal.parent.parent; - type = getApparentTypeOfContextualType(literal); - } - // There was no contextual ThisType for the containing object literal, so the contextual type - // for 'this' is the non-null form of the contextual type for the containing object literal or - // the type of the object literal itself. - return getWidenedType(contextualType ? getNonNullableType(contextualType) : checkExpressionCached(containingLiteral)); - } - // In an assignment of the form 'obj.xxx = function(...)' or 'obj[xxx] = function(...)', the - // contextual type for 'this' is 'obj'. - const parent = walkUpParenthesizedExpressions(func.parent); - if (parent.kind === SyntaxKind.BinaryExpression && (parent).operatorToken.kind === SyntaxKind.EqualsToken) { - const target = (parent).left; - if (isAccessExpression(target)) { - const { expression } = target; - // Don't contextually type `this` as `exports` in `exports.Point = function(x, y) { this.x = x; this.y = y; }` - if (inJs && isIdentifier(expression)) { - const sourceFile = getSourceFileOfNode(parent); - if (sourceFile.commonJsModuleIndicator && getResolvedSymbol(expression) === sourceFile.symbol) { - return undefined; - } - } - - return getWidenedType(checkExpressionCached(expression)); - } + else { + let type = createObjectType(ObjectFlags.Anonymous, node.symbol); + type.aliasSymbol = aliasSymbol; + type.aliasTypeArguments = getTypeArgumentsForAliasSymbol(aliasSymbol); + if (isJSDocTypeLiteral(node) && node.isArrayType) { + type = createArrayType(type); } + links.resolvedType = type; } - return undefined; } - - // Return contextual type of parameter or undefined if no contextual type is available - function getContextuallyTypedParameterType(parameter: ParameterDeclaration): Type | undefined { - const func = parameter.parent; - if (!isContextSensitiveFunctionOrObjectLiteralMethod(func)) { - return undefined; + return links.resolvedType; + } + function getAliasSymbolForTypeNode(node: Node) { + let host = node.parent; + while (isParenthesizedTypeNode(host) || isTypeOperatorNode(host) && host.operator === SyntaxKind.ReadonlyKeyword) { + host = host.parent; + } + return isTypeAlias(host) ? getSymbolOfNode(host) : undefined; + } + function getTypeArgumentsForAliasSymbol(symbol: ts.Symbol | undefined) { + return symbol ? getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol) : undefined; + } + function isNonGenericObjectType(type: ts.Type) { + return !!(type.flags & TypeFlags.Object) && !isGenericMappedType(type); + } + function isEmptyObjectTypeOrSpreadsIntoEmptyObject(type: ts.Type) { + return isEmptyObjectType(type) || !!(type.flags & (TypeFlags.Null | TypeFlags.Undefined | TypeFlags.BooleanLike | TypeFlags.NumberLike | TypeFlags.BigIntLike | TypeFlags.StringLike | TypeFlags.EnumLike | TypeFlags.NonPrimitive | TypeFlags.Index)); + } + function isSinglePropertyAnonymousObjectType(type: ts.Type) { + return !!(type.flags & TypeFlags.Object) && + !!(getObjectFlags(type) & ObjectFlags.Anonymous) && + (length(getPropertiesOfType(type)) === 1 || every(getPropertiesOfType(type), p => !!(p.flags & SymbolFlags.Optional))); + } + function tryMergeUnionOfObjectTypeAndEmptyObject(type: UnionType, readonly: boolean): ts.Type | undefined { + if (type.types.length === 2) { + const firstType = type.types[0]; + const secondType = type.types[1]; + if (every(type.types, isEmptyObjectTypeOrSpreadsIntoEmptyObject)) { + return isEmptyObjectType(firstType) ? firstType : isEmptyObjectType(secondType) ? secondType : emptyObjectType; } - const iife = getImmediatelyInvokedFunctionExpression(func); - if (iife && iife.arguments) { - const args = getEffectiveCallArguments(iife); - const indexOfParameter = func.parameters.indexOf(parameter); - if (parameter.dotDotDotToken) { - return getSpreadArgumentType(args, indexOfParameter, args.length, anyType, /*context*/ undefined); - } - const links = getNodeLinks(iife); - const cached = links.resolvedSignature; - links.resolvedSignature = anySignature; - const type = indexOfParameter < args.length ? - getWidenedLiteralType(checkExpression(args[indexOfParameter])) : - parameter.initializer ? undefined : undefinedWideningType; - links.resolvedSignature = cached; - return type; + if (isEmptyObjectTypeOrSpreadsIntoEmptyObject(firstType) && isSinglePropertyAnonymousObjectType(secondType)) { + return getAnonymousPartialType(secondType); } - const contextualSignature = getContextualSignature(func); - if (contextualSignature) { - const index = func.parameters.indexOf(parameter) - (getThisParameter(func) ? 1 : 0); - return parameter.dotDotDotToken && lastOrUndefined(func.parameters) === parameter ? - getRestTypeAtPosition(contextualSignature, index) : - tryGetTypeAtPosition(contextualSignature, index); + if (isEmptyObjectTypeOrSpreadsIntoEmptyObject(secondType) && isSinglePropertyAnonymousObjectType(firstType)) { + return getAnonymousPartialType(firstType); } } - - function getContextualTypeForVariableLikeDeclaration(declaration: VariableLikeDeclaration): Type | undefined { - const typeNode = getEffectiveTypeAnnotationNode(declaration); - if (typeNode) { - return getTypeFromTypeNode(typeNode); + function getAnonymousPartialType(type: ts.Type) { + // gets the type as if it had been spread, but where everything in the spread is made optional + const members = createSymbolTable(); + for (const prop of getPropertiesOfType(type)) { + if (getDeclarationModifierFlagsFromSymbol(prop) & (ModifierFlags.Private | ModifierFlags.Protected)) { + // do nothing, skip privates + } + else if (isSpreadableProperty(prop)) { + const isSetonlyAccessor = prop.flags & SymbolFlags.SetAccessor && !(prop.flags & SymbolFlags.GetAccessor); + const flags = SymbolFlags.Property | SymbolFlags.Optional; + const result = createSymbol(flags, prop.escapedName, readonly ? CheckFlags.Readonly : 0); + result.type = isSetonlyAccessor ? undefinedType : getTypeOfSymbol(prop); + result.declarations = prop.declarations; + result.nameType = getSymbolLinks(prop).nameType; + result.syntheticOrigin = prop; + members.set(prop.escapedName, result); + } + } + const spread = createAnonymousType(type.symbol, members, emptyArray, emptyArray, getIndexInfoOfType(type, IndexKind.String), getIndexInfoOfType(type, IndexKind.Number)); + spread.objectFlags |= ObjectFlags.ObjectLiteral | ObjectFlags.ContainsObjectOrArrayLiteral; + return spread; + } + } + /** + * Since the source of spread types are object literals, which are not binary, + * this function should be called in a left folding style, with left = previous result of getSpreadType + * and right = the new element to be spread. + */ + function getSpreadType(left: ts.Type, right: ts.Type, symbol: ts.Symbol | undefined, objectFlags: ObjectFlags, readonly: boolean, isParentTypeNullable?: boolean): ts.Type { + if (left.flags & TypeFlags.Any || right.flags & TypeFlags.Any) { + return anyType; + } + if (left.flags & TypeFlags.Unknown || right.flags & TypeFlags.Unknown) { + return unknownType; + } + if (left.flags & TypeFlags.Never) { + return right; + } + if (right.flags & TypeFlags.Never) { + return left; + } + if (left.flags & TypeFlags.Union) { + const merged = tryMergeUnionOfObjectTypeAndEmptyObject((left as UnionType), readonly); + if (merged) { + return getSpreadType(merged, right, symbol, objectFlags, readonly, isParentTypeNullable); } - switch (declaration.kind) { - case SyntaxKind.Parameter: - return getContextuallyTypedParameterType(declaration); - case SyntaxKind.BindingElement: - return getContextualTypeForBindingElement(declaration); - // By default, do nothing and return undefined - only parameters and binding elements have context implied by a parent + return mapType(left, t => getSpreadType(t, right, symbol, objectFlags, readonly, isParentTypeNullable)); + } + if (right.flags & TypeFlags.Union) { + const merged = tryMergeUnionOfObjectTypeAndEmptyObject((right as UnionType), readonly); + if (merged) { + return getSpreadType(left, merged, symbol, objectFlags, readonly, maybeTypeOfKind(right, TypeFlags.Nullable)); } + return mapType(right, t => getSpreadType(left, t, symbol, objectFlags, readonly, maybeTypeOfKind(right, TypeFlags.Nullable))); } - - function getContextualTypeForBindingElement(declaration: BindingElement): Type | undefined { - const parent = declaration.parent.parent; - const name = declaration.propertyName || declaration.name; - const parentType = getContextualTypeForVariableLikeDeclaration(parent) || - parent.kind !== SyntaxKind.BindingElement && parent.initializer && checkDeclarationInitializer(parent); - if (parentType && !isBindingPattern(name) && !isComputedNonLiteralName(name)) { - const nameType = getLiteralTypeFromPropertyName(name); - if (isTypeUsableAsPropertyName(nameType)) { - const text = getPropertyNameFromType(nameType); - return getTypeOfPropertyOfType(parentType, text); + if (right.flags & (TypeFlags.BooleanLike | TypeFlags.NumberLike | TypeFlags.BigIntLike | TypeFlags.StringLike | TypeFlags.EnumLike | TypeFlags.NonPrimitive | TypeFlags.Index)) { + return left; + } + if (isGenericObjectType(left) || isGenericObjectType(right)) { + if (isEmptyObjectType(left)) { + return right; + } + // When the left type is an intersection, we may need to merge the last constituent of the + // intersection with the right type. For example when the left type is 'T & { a: string }' + // and the right type is '{ b: string }' we produce 'T & { a: string, b: string }'. + if (left.flags & TypeFlags.Intersection) { + const types = (left).types; + const lastLeft = types[types.length - 1]; + if (isNonGenericObjectType(lastLeft) && isNonGenericObjectType(right)) { + return getIntersectionType(concatenate(types.slice(0, types.length - 1), [getSpreadType(lastLeft, right, symbol, objectFlags, readonly)])); + } + } + return getIntersectionType([left, right]); + } + const members = createSymbolTable(); + const skippedPrivateMembers = createUnderscoreEscapedMap(); + let stringIndexInfo: IndexInfo | undefined; + let numberIndexInfo: IndexInfo | undefined; + if (left === emptyObjectType) { + // for the first spread element, left === emptyObjectType, so take the right's string indexer + stringIndexInfo = getIndexInfoOfType(right, IndexKind.String); + numberIndexInfo = getIndexInfoOfType(right, IndexKind.Number); + } + else { + stringIndexInfo = unionSpreadIndexInfos(getIndexInfoOfType(left, IndexKind.String), getIndexInfoOfType(right, IndexKind.String)); + numberIndexInfo = unionSpreadIndexInfos(getIndexInfoOfType(left, IndexKind.Number), getIndexInfoOfType(right, IndexKind.Number)); + } + for (const rightProp of getPropertiesOfType(right)) { + if (getDeclarationModifierFlagsFromSymbol(rightProp) & (ModifierFlags.Private | ModifierFlags.Protected)) { + skippedPrivateMembers.set(rightProp.escapedName, true); + } + else if (isSpreadableProperty(rightProp)) { + members.set(rightProp.escapedName, getSpreadSymbol(rightProp, readonly)); + } + } + for (const leftProp of getPropertiesOfType(left)) { + if (skippedPrivateMembers.has(leftProp.escapedName) || !isSpreadableProperty(leftProp)) { + continue; + } + if (members.has(leftProp.escapedName)) { + const rightProp = members.get(leftProp.escapedName)!; + const rightType = getTypeOfSymbol(rightProp); + if (rightProp.flags & SymbolFlags.Optional) { + const declarations = concatenate(leftProp.declarations, rightProp.declarations); + const flags = SymbolFlags.Property | (leftProp.flags & SymbolFlags.Optional); + const result = createSymbol(flags, leftProp.escapedName); + result.type = getUnionType([getTypeOfSymbol(leftProp), getTypeWithFacts(rightType, TypeFacts.NEUndefined)]); + result.leftSpread = leftProp; + result.rightSpread = rightProp; + result.declarations = declarations; + result.nameType = getSymbolLinks(leftProp).nameType; + members.set(leftProp.escapedName, result); + } + else if (strictNullChecks && + !isParentTypeNullable && + symbol && + !isFromSpreadAssignment(leftProp, symbol) && + isFromSpreadAssignment(rightProp, symbol) && + !maybeTypeOfKind(rightType, TypeFlags.Nullable)) { + error(leftProp.valueDeclaration, Diagnostics._0_is_specified_more_than_once_so_this_usage_will_be_overwritten, unescapeLeadingUnderscores(leftProp.escapedName)); } } + else { + members.set(leftProp.escapedName, getSpreadSymbol(leftProp, readonly)); + } } - - // In a variable, parameter or property declaration with a type annotation, - // the contextual type of an initializer expression is the type of the variable, parameter or property. - // Otherwise, in a parameter declaration of a contextually typed function expression, - // the contextual type of an initializer expression is the contextual type of the parameter. - // Otherwise, in a variable or parameter declaration with a binding pattern name, - // the contextual type of an initializer expression is the type implied by the binding pattern. - // Otherwise, in a binding pattern inside a variable or parameter declaration, - // the contextual type of an initializer expression is the type annotation of the containing declaration, if present. - function getContextualTypeForInitializerExpression(node: Expression): Type | undefined { - const declaration = node.parent; - if (hasInitializer(declaration) && node === declaration.initializer) { - const result = getContextualTypeForVariableLikeDeclaration(declaration); - if (result) { + const spread = createAnonymousType(symbol, members, emptyArray, emptyArray, getIndexInfoWithReadonly(stringIndexInfo, readonly), getIndexInfoWithReadonly(numberIndexInfo, readonly)); + spread.objectFlags |= ObjectFlags.ObjectLiteral | ObjectFlags.ContainsObjectOrArrayLiteral | ObjectFlags.ContainsSpread | objectFlags; + return spread; + } + /** We approximate own properties as non-methods plus methods that are inside the object literal */ + function isSpreadableProperty(prop: ts.Symbol): boolean { + return !some(prop.declarations, isPrivateIdentifierPropertyDeclaration) && + (!(prop.flags & (SymbolFlags.Method | SymbolFlags.GetAccessor | SymbolFlags.SetAccessor)) || + !prop.declarations.some(decl => isClassLike(decl.parent))); + } + function getSpreadSymbol(prop: ts.Symbol, readonly: boolean) { + const isSetonlyAccessor = prop.flags & SymbolFlags.SetAccessor && !(prop.flags & SymbolFlags.GetAccessor); + if (!isSetonlyAccessor && readonly === isReadonlySymbol(prop)) { + return prop; + } + const flags = SymbolFlags.Property | (prop.flags & SymbolFlags.Optional); + const result = createSymbol(flags, prop.escapedName, readonly ? CheckFlags.Readonly : 0); + result.type = isSetonlyAccessor ? undefinedType : getTypeOfSymbol(prop); + result.declarations = prop.declarations; + result.nameType = getSymbolLinks(prop).nameType; + result.syntheticOrigin = prop; + return result; + } + function getIndexInfoWithReadonly(info: IndexInfo | undefined, readonly: boolean) { + return info && info.isReadonly !== readonly ? createIndexInfo(info.type, readonly, info.declaration) : info; + } + function createLiteralType(flags: TypeFlags, value: string | number | PseudoBigInt, symbol: ts.Symbol | undefined) { + const type = (createType(flags)); + type.symbol = symbol!; + type.value = value; + return type; + } + function getFreshTypeOfLiteralType(type: ts.Type): ts.Type { + if (type.flags & TypeFlags.Literal) { + if (!(type).freshType) { + const freshType = createLiteralType(type.flags, (type).value, (type).symbol); + freshType.regularType = (type); + freshType.freshType = freshType; + (type).freshType = freshType; + } + return (type).freshType; + } + return type; + } + function getRegularTypeOfLiteralType(type: ts.Type): ts.Type { + return type.flags & TypeFlags.Literal ? (type).regularType : + type.flags & TypeFlags.Union ? getUnionType(sameMap((type).types, getRegularTypeOfLiteralType)) : + type; + } + function isFreshLiteralType(type: ts.Type) { + return !!(type.flags & TypeFlags.Literal) && (type).freshType === type; + } + function getLiteralType(value: string | number | PseudoBigInt, enumId?: number, symbol?: ts.Symbol) { + // We store all literal types in a single map with keys of the form '#NNN' and '@SSS', + // where NNN is the text representation of a numeric literal and SSS are the characters + // of a string literal. For literal enum members we use 'EEE#NNN' and 'EEE@SSS', where + // EEE is a unique id for the containing enum type. + const qualifier = typeof value === "number" ? "#" : typeof value === "string" ? "@" : "n"; + const key = (enumId ? enumId : "") + qualifier + (typeof value === "object" ? pseudoBigIntToString(value) : value); + let type = literalTypes.get(key); + if (!type) { + const flags = (typeof value === "number" ? TypeFlags.NumberLiteral : + typeof value === "string" ? TypeFlags.StringLiteral : TypeFlags.BigIntLiteral) | + (enumId ? TypeFlags.EnumLiteral : 0); + literalTypes.set(key, type = createLiteralType(flags, value, symbol)); + type.regularType = type; + } + return type; + } + function getTypeFromLiteralTypeNode(node: LiteralTypeNode): ts.Type { + const links = getNodeLinks(node); + if (!links.resolvedType) { + links.resolvedType = getRegularTypeOfLiteralType(checkExpression(node.literal)); + } + return links.resolvedType; + } + function createUniqueESSymbolType(symbol: ts.Symbol) { + const type = (createType(TypeFlags.UniqueESSymbol)); + type.symbol = symbol; + type.escapedName = (`__@${type.symbol.escapedName}@${getSymbolId(type.symbol)}` as __String); + return type; + } + function getESSymbolLikeTypeForNode(node: Node) { + if (isValidESSymbolDeclaration(node)) { + const symbol = getSymbolOfNode(node); + const links = getSymbolLinks(symbol); + return links.uniqueESSymbolType || (links.uniqueESSymbolType = createUniqueESSymbolType(symbol)); + } + return esSymbolType; + } + function getThisType(node: Node): ts.Type { + const container = getThisContainer(node, /*includeArrowFunctions*/ false); + const parent = container && container.parent; + if (parent && (isClassLike(parent) || parent.kind === SyntaxKind.InterfaceDeclaration)) { + if (!hasModifier(container, ModifierFlags.Static) && + (!isConstructorDeclaration(container) || isNodeDescendantOf(node, container.body))) { + return getDeclaredTypeOfClassOrInterface(getSymbolOfNode((parent as ClassLikeDeclaration | InterfaceDeclaration))).thisType!; + } + } + // inside x.prototype = { ... } + if (parent && isObjectLiteralExpression(parent) && isBinaryExpression(parent.parent) && getAssignmentDeclarationKind(parent.parent) === AssignmentDeclarationKind.Prototype) { + return getDeclaredTypeOfClassOrInterface(getSymbolOfNode(parent.parent.left)!.parent!).thisType!; + } + // /** @return {this} */ + // x.prototype.m = function() { ... } + const host = node.flags & NodeFlags.JSDoc ? getHostSignatureFromJSDoc(node) : undefined; + if (host && isFunctionExpression(host) && isBinaryExpression(host.parent) && getAssignmentDeclarationKind(host.parent) === AssignmentDeclarationKind.PrototypeProperty) { + return getDeclaredTypeOfClassOrInterface(getSymbolOfNode(host.parent.left)!.parent!).thisType!; + } + // inside constructor function C() { ... } + if (isJSConstructor(container) && isNodeDescendantOf(node, container.body)) { + return getDeclaredTypeOfClassOrInterface(getSymbolOfNode(container)).thisType!; + } + error(node, Diagnostics.A_this_type_is_available_only_in_a_non_static_member_of_a_class_or_interface); + return errorType; + } + function getTypeFromThisTypeNode(node: ThisExpression | ThisTypeNode): ts.Type { + const links = getNodeLinks(node); + if (!links.resolvedType) { + links.resolvedType = getThisType(node); + } + return links.resolvedType; + } + function getTypeFromTypeNode(node: TypeNode): ts.Type { + switch (node.kind) { + case SyntaxKind.AnyKeyword: + case SyntaxKind.JSDocAllType: + case SyntaxKind.JSDocUnknownType: + return anyType; + case SyntaxKind.UnknownKeyword: + return unknownType; + case SyntaxKind.StringKeyword: + return stringType; + case SyntaxKind.NumberKeyword: + return numberType; + case SyntaxKind.BigIntKeyword: + return bigintType; + case SyntaxKind.BooleanKeyword: + return booleanType; + case SyntaxKind.SymbolKeyword: + return esSymbolType; + case SyntaxKind.VoidKeyword: + return voidType; + case SyntaxKind.UndefinedKeyword: + return undefinedType; + case SyntaxKind.NullKeyword: + return nullType; + case SyntaxKind.NeverKeyword: + return neverType; + case SyntaxKind.ObjectKeyword: + return node.flags & NodeFlags.JavaScriptFile && !noImplicitAny ? anyType : nonPrimitiveType; + case SyntaxKind.ThisType: + case SyntaxKind.ThisKeyword: + return getTypeFromThisTypeNode((node as ThisExpression | ThisTypeNode)); + case SyntaxKind.LiteralType: + return getTypeFromLiteralTypeNode((node)); + case SyntaxKind.TypeReference: + return getTypeFromTypeReference((node)); + case SyntaxKind.TypePredicate: + return (node).assertsModifier ? voidType : booleanType; + case SyntaxKind.ExpressionWithTypeArguments: + return getTypeFromTypeReference((node)); + case SyntaxKind.TypeQuery: + return getTypeFromTypeQueryNode((node)); + case SyntaxKind.ArrayType: + case SyntaxKind.TupleType: + return getTypeFromArrayOrTupleTypeNode((node)); + case SyntaxKind.OptionalType: + return getTypeFromOptionalTypeNode((node)); + case SyntaxKind.UnionType: + return getTypeFromUnionTypeNode((node)); + case SyntaxKind.IntersectionType: + return getTypeFromIntersectionTypeNode((node)); + case SyntaxKind.JSDocNullableType: + return getTypeFromJSDocNullableTypeNode((node)); + case SyntaxKind.JSDocOptionalType: + return addOptionality(getTypeFromTypeNode((node as JSDocOptionalType).type)); + case SyntaxKind.ParenthesizedType: + case SyntaxKind.JSDocNonNullableType: + case SyntaxKind.JSDocTypeExpression: + return getTypeFromTypeNode((node).type); + case SyntaxKind.RestType: + return getElementTypeOfArrayType(getTypeFromTypeNode((node).type)) || errorType; + case SyntaxKind.JSDocVariadicType: + return getTypeFromJSDocVariadicType((node as JSDocVariadicType)); + case SyntaxKind.FunctionType: + case SyntaxKind.ConstructorType: + case SyntaxKind.TypeLiteral: + case SyntaxKind.JSDocTypeLiteral: + case SyntaxKind.JSDocFunctionType: + case SyntaxKind.JSDocSignature: + return getTypeFromTypeLiteralOrFunctionOrConstructorTypeNode(node); + case SyntaxKind.TypeOperator: + return getTypeFromTypeOperatorNode((node)); + case SyntaxKind.IndexedAccessType: + return getTypeFromIndexedAccessTypeNode((node)); + case SyntaxKind.MappedType: + return getTypeFromMappedTypeNode((node)); + case SyntaxKind.ConditionalType: + return getTypeFromConditionalTypeNode((node)); + case SyntaxKind.InferType: + return getTypeFromInferTypeNode((node)); + case SyntaxKind.ImportType: + return getTypeFromImportTypeNode((node)); + // This function assumes that an identifier or qualified name is a type expression + // Callers should first ensure this by calling isTypeNode + case SyntaxKind.Identifier: + case SyntaxKind.QualifiedName: + const symbol = getSymbolAtLocation(node); + return symbol ? getDeclaredTypeOfSymbol(symbol) : errorType; + default: + return errorType; + } + } + function instantiateList(items: readonly T[], mapper: TypeMapper, instantiator: (item: T, mapper: TypeMapper) => T): readonly T[]; + function instantiateList(items: readonly T[] | undefined, mapper: TypeMapper, instantiator: (item: T, mapper: TypeMapper) => T): readonly T[] | undefined; + function instantiateList(items: readonly T[] | undefined, mapper: TypeMapper, instantiator: (item: T, mapper: TypeMapper) => T): readonly T[] | undefined { + if (items && items.length) { + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const mapped = instantiator(item, mapper); + if (item !== mapped) { + const result = i === 0 ? [] : items.slice(0, i); + result.push(mapped); + for (i++; i < items.length; i++) { + result.push(instantiator(items[i], mapper)); + } return result; } - if (isBindingPattern(declaration.name)) { // This is less a contextual type and more an implied shape - in some cases, this may be undesirable - return getTypeFromBindingPattern(declaration.name, /*includePatternInType*/ true, /*reportErrors*/ false); - } } - return undefined; } - - function getContextualTypeForReturnExpression(node: Expression): Type | undefined { - const func = getContainingFunction(node); - if (func) { - const functionFlags = getFunctionFlags(func); - if (functionFlags & FunctionFlags.Generator) { // AsyncGenerator function or Generator function - return undefined; + return items; + } + function instantiateTypes(types: readonly ts.Type[], mapper: TypeMapper): readonly ts.Type[]; + function instantiateTypes(types: readonly ts.Type[] | undefined, mapper: TypeMapper): readonly ts.Type[] | undefined; + function instantiateTypes(types: readonly ts.Type[] | undefined, mapper: TypeMapper): readonly ts.Type[] | undefined { + return instantiateList(types, mapper, instantiateType); + } + function instantiateSignatures(signatures: readonly ts.Signature[], mapper: TypeMapper): readonly ts.Signature[] { + return instantiateList(signatures, mapper, instantiateSignature); + } + function makeUnaryTypeMapper(source: ts.Type, target: ts.Type) { + return (t: ts.Type) => t === source ? target : t; + } + function makeBinaryTypeMapper(source1: ts.Type, target1: ts.Type, source2: ts.Type, target2: ts.Type) { + return (t: ts.Type) => t === source1 ? target1 : t === source2 ? target2 : t; + } + function makeArrayTypeMapper(sources: readonly ts.Type[], targets: readonly ts.Type[] | undefined) { + return (t: ts.Type) => { + for (let i = 0; i < sources.length; i++) { + if (t === sources[i]) { + return targets ? targets[i] : anyType; } - - const contextualReturnType = getContextualReturnType(func); - if (contextualReturnType) { - if (functionFlags & FunctionFlags.Async) { // Async function - const contextualAwaitedType = getAwaitedTypeOfPromise(contextualReturnType); - return contextualAwaitedType && getUnionType([contextualAwaitedType, createPromiseLikeType(contextualAwaitedType)]); + } + return t; + }; + } + function createTypeMapper(sources: readonly TypeParameter[], targets: readonly ts.Type[] | undefined): TypeMapper { + Debug.assert(targets === undefined || sources.length === targets.length); + return sources.length === 1 ? makeUnaryTypeMapper(sources[0], targets ? targets[0] : anyType) : + sources.length === 2 ? makeBinaryTypeMapper(sources[0], targets ? targets[0] : anyType, sources[1], targets ? targets[1] : anyType) : + makeArrayTypeMapper(sources, targets); + } + function createTypeEraser(sources: readonly TypeParameter[]): TypeMapper { + return createTypeMapper(sources, /*targets*/ undefined); + } + /** + * Maps forward-references to later types parameters to the empty object type. + * This is used during inference when instantiating type parameter defaults. + */ + function createBackreferenceMapper(context: InferenceContext, index: number): TypeMapper { + return t => findIndex(context.inferences, info => info.typeParameter === t) >= index ? unknownType : t; + } + function combineTypeMappers(mapper1: TypeMapper | undefined, mapper2: TypeMapper): TypeMapper; + function combineTypeMappers(mapper1: TypeMapper, mapper2: TypeMapper | undefined): TypeMapper; + function combineTypeMappers(mapper1: TypeMapper, mapper2: TypeMapper): TypeMapper { + if (!mapper1) + return mapper2; + if (!mapper2) + return mapper1; + return t => instantiateType(mapper1(t), mapper2); + } + function createReplacementMapper(source: ts.Type, target: ts.Type, baseMapper: TypeMapper): TypeMapper { + return t => t === source ? target : baseMapper(t); + } + function permissiveMapper(type: ts.Type) { + return type.flags & TypeFlags.TypeParameter ? wildcardType : type; + } + function getRestrictiveTypeParameter(tp: TypeParameter) { + return tp.constraint === unknownType ? tp : tp.restrictiveInstantiation || (tp.restrictiveInstantiation = createTypeParameter(tp.symbol), + (tp.restrictiveInstantiation as TypeParameter).constraint = unknownType, + tp.restrictiveInstantiation); + } + function restrictiveMapper(type: ts.Type) { + return type.flags & TypeFlags.TypeParameter ? getRestrictiveTypeParameter((type)) : type; + } + function cloneTypeParameter(typeParameter: TypeParameter): TypeParameter { + const result = createTypeParameter(typeParameter.symbol); + result.target = typeParameter; + return result; + } + function instantiateTypePredicate(predicate: TypePredicate, mapper: TypeMapper): TypePredicate { + return createTypePredicate(predicate.kind, predicate.parameterName, predicate.parameterIndex, instantiateType(predicate.type, mapper)); + } + function instantiateSignature(signature: ts.Signature, mapper: TypeMapper, eraseTypeParameters?: boolean): ts.Signature { + let freshTypeParameters: TypeParameter[] | undefined; + if (signature.typeParameters && !eraseTypeParameters) { + // First create a fresh set of type parameters, then include a mapping from the old to the + // new type parameters in the mapper function. Finally store this mapper in the new type + // parameters such that we can use it when instantiating constraints. + freshTypeParameters = map(signature.typeParameters, cloneTypeParameter); + mapper = combineTypeMappers(createTypeMapper(signature.typeParameters, freshTypeParameters), mapper); + for (const tp of freshTypeParameters) { + tp.mapper = mapper; + } + } + // Don't compute resolvedReturnType and resolvedTypePredicate now, + // because using `mapper` now could trigger inferences to become fixed. (See `createInferenceContext`.) + // See GH#17600. + const result = createSignature(signature.declaration, freshTypeParameters, signature.thisParameter && instantiateSymbol(signature.thisParameter, mapper), instantiateList(signature.parameters, mapper, instantiateSymbol), + /*resolvedReturnType*/ undefined, + /*resolvedTypePredicate*/ undefined, signature.minArgumentCount, signature.flags & SignatureFlags.PropagatingFlags); + result.target = signature; + result.mapper = mapper; + return result; + } + function instantiateSymbol(symbol: ts.Symbol, mapper: TypeMapper): ts.Symbol { + const links = getSymbolLinks(symbol); + if (links.type && !maybeTypeOfKind(links.type, TypeFlags.Object | TypeFlags.Instantiable)) { + // If the type of the symbol is already resolved, and if that type could not possibly + // be affected by instantiation, simply return the symbol itself. + return symbol; + } + if (getCheckFlags(symbol) & CheckFlags.Instantiated) { + // If symbol being instantiated is itself a instantiation, fetch the original target and combine the + // type mappers. This ensures that original type identities are properly preserved and that aliases + // always reference a non-aliases. + symbol = links.target!; + mapper = combineTypeMappers(links.mapper, mapper); + } + // Keep the flags from the symbol we're instantiating. Mark that is instantiated, and + // also transient so that we can just store data on it directly. + const result = createSymbol(symbol.flags, symbol.escapedName, CheckFlags.Instantiated | getCheckFlags(symbol) & (CheckFlags.Readonly | CheckFlags.Late | CheckFlags.OptionalParameter | CheckFlags.RestParameter)); + result.declarations = symbol.declarations; + result.parent = symbol.parent; + result.target = symbol; + result.mapper = mapper; + if (symbol.valueDeclaration) { + result.valueDeclaration = symbol.valueDeclaration; + } + if (links.nameType) { + result.nameType = links.nameType; + } + return result; + } + function getObjectTypeInstantiation(type: AnonymousType | DeferredTypeReference, mapper: TypeMapper) { + const target = type.objectFlags & ObjectFlags.Instantiated ? type.target! : type; + const node = type.objectFlags & ObjectFlags.Reference ? (type).node! : type.symbol.declarations[0]; + const links = getNodeLinks(node); + let typeParameters = links.outerTypeParameters; + if (!typeParameters) { + // The first time an anonymous type is instantiated we compute and store a list of the type + // parameters that are in scope (and therefore potentially referenced). For type literals that + // aren't the right hand side of a generic type alias declaration we optimize by reducing the + // set of type parameters to those that are possibly referenced in the literal. + let declaration = node; + if (isInJSFile(declaration)) { + const paramTag = findAncestor(declaration, isJSDocParameterTag); + if (paramTag) { + const paramSymbol = getParameterSymbolFromJSDoc(paramTag); + if (paramSymbol) { + declaration = paramSymbol.valueDeclaration; } - return contextualReturnType; // Regular function } } - return undefined; + let outerTypeParameters = getOuterTypeParameters(declaration, /*includeThisTypes*/ true); + if (isJSConstructor(declaration)) { + const templateTagParameters = getTypeParametersFromDeclaration((declaration as DeclarationWithTypeParameters)); + outerTypeParameters = addRange(outerTypeParameters, templateTagParameters); + } + typeParameters = outerTypeParameters || emptyArray; + typeParameters = (target.objectFlags & ObjectFlags.Reference || target.symbol.flags & SymbolFlags.TypeLiteral) && !target.aliasTypeArguments ? + filter(typeParameters, tp => isTypeParameterPossiblyReferenced(tp, declaration)) : + typeParameters; + links.outerTypeParameters = typeParameters; + if (typeParameters.length) { + links.instantiations = createMap(); + links.instantiations.set(getTypeListId(typeParameters), target); + } } - - function getContextualTypeForAwaitOperand(node: AwaitExpression): Type | undefined { - const contextualType = getContextualType(node); - if (contextualType) { - const contextualAwaitedType = getAwaitedType(contextualType); - return contextualAwaitedType && getUnionType([contextualAwaitedType, createPromiseLikeType(contextualAwaitedType)]); + if (typeParameters.length) { + // We are instantiating an anonymous type that has one or more type parameters in scope. Apply the + // mapper to the type parameters to produce the effective list of type arguments, and compute the + // instantiation cache key from the type IDs of the type arguments. + const typeArguments = map(typeParameters, combineTypeMappers(type.mapper, mapper)); + const id = getTypeListId(typeArguments); + let result = links.instantiations!.get(id); + if (!result) { + const newMapper = createTypeMapper(typeParameters, typeArguments); + result = target.objectFlags & ObjectFlags.Reference ? createDeferredTypeReference((type).target, (type).node, newMapper) : + target.objectFlags & ObjectFlags.Mapped ? instantiateMappedType((target), newMapper) : + instantiateAnonymousType(target, newMapper); + links.instantiations!.set(id, result); } - return undefined; + return result; } - - function getContextualTypeForYieldOperand(node: YieldExpression): Type | undefined { - const func = getContainingFunction(node); - if (func) { - const functionFlags = getFunctionFlags(func); - const contextualReturnType = getContextualReturnType(func); - if (contextualReturnType) { - return node.asteriskToken - ? contextualReturnType - : getIterationTypeOfGeneratorFunctionReturnType(IterationTypeKind.Yield, contextualReturnType, (functionFlags & FunctionFlags.Async) !== 0); + return type; + } + function maybeTypeParameterReference(node: Node) { + return !(node.kind === SyntaxKind.QualifiedName || + node.parent.kind === SyntaxKind.TypeReference && (node.parent).typeArguments && node === (node.parent).typeName || + node.parent.kind === SyntaxKind.ImportType && (node.parent as ImportTypeNode).typeArguments && node === (node.parent as ImportTypeNode).qualifier); + } + function isTypeParameterPossiblyReferenced(tp: TypeParameter, node: Node) { + // If the type parameter doesn't have exactly one declaration, if there are invening statement blocks + // between the node and the type parameter declaration, if the node contains actual references to the + // type parameter, or if the node contains type queries, we consider the type parameter possibly referenced. + if (tp.symbol && tp.symbol.declarations && tp.symbol.declarations.length === 1) { + const container = tp.symbol.declarations[0].parent; + for (let n = node; n !== container; n = n.parent) { + if (!n || n.kind === SyntaxKind.Block || n.kind === SyntaxKind.ConditionalType && forEachChild((n).extendsType, containsReference)) { + return true; } } - - return undefined; + return !!forEachChild(node, containsReference); } - - function isInParameterInitializerBeforeContainingFunction(node: Node) { - let inBindingInitializer = false; - while (node.parent && !isFunctionLike(node.parent)) { - if (isParameter(node.parent) && (inBindingInitializer || node.parent.initializer === node)) { + return true; + function containsReference(node: Node): boolean { + switch (node.kind) { + case SyntaxKind.ThisType: + return !!tp.isThisType; + case SyntaxKind.Identifier: + return !tp.isThisType && isPartOfTypeNode(node) && maybeTypeParameterReference(node) && + getTypeFromTypeNode((node)) === tp; + case SyntaxKind.TypeQuery: return true; - } - if (isBindingElement(node.parent) && node.parent.initializer === node) { - inBindingInitializer = true; - } - - node = node.parent; } - - return false; + return !!forEachChild(node, containsReference); } - - function getContextualIterationType(kind: IterationTypeKind, functionDecl: SignatureDeclaration): Type | undefined { - const isAsync = !!(getFunctionFlags(functionDecl) & FunctionFlags.Async); - const contextualReturnType = getContextualReturnType(functionDecl); - if (contextualReturnType) { - return getIterationTypeOfGeneratorFunctionReturnType(kind, contextualReturnType, isAsync) - || undefined; + } + function getHomomorphicTypeVariable(type: MappedType) { + const constraintType = getConstraintTypeFromMappedType(type); + if (constraintType.flags & TypeFlags.Index) { + const typeVariable = getActualTypeVariable((constraintType).type); + if (typeVariable.flags & TypeFlags.TypeParameter) { + return typeVariable; } - - return undefined; } - - function getContextualReturnType(functionDecl: SignatureDeclaration): Type | undefined { - // If the containing function has a return type annotation, is a constructor, or is a get accessor whose - // corresponding set accessor has a type annotation, return statements in the function are contextually typed - const returnType = getReturnTypeFromAnnotation(functionDecl); - if (returnType) { - return returnType; - } - // Otherwise, if the containing function is contextually typed by a function type with exactly one call signature - // and that call signature is non-generic, return statements are contextually typed by the return type of the signature - const signature = getContextualSignatureForFunctionLikeDeclaration(functionDecl); - if (signature && !isResolvingReturnTypeOfSignature(signature)) { - return getReturnTypeOfSignature(signature); + return undefined; + } + function instantiateMappedType(type: MappedType, mapper: TypeMapper): ts.Type { + // For a homomorphic mapped type { [P in keyof T]: X }, where T is some type variable, the mapping + // operation depends on T as follows: + // * If T is a primitive type no mapping is performed and the result is simply T. + // * If T is a union type we distribute the mapped type over the union. + // * If T is an array we map to an array where the element type has been transformed. + // * If T is a tuple we map to a tuple where the element types have been transformed. + // * Otherwise we map to an object type where the type of each property has been transformed. + // For example, when T is instantiated to a union type A | B, we produce { [P in keyof A]: X } | + // { [P in keyof B]: X }, and when when T is instantiated to a union type A | undefined, we produce + // { [P in keyof A]: X } | undefined. + const typeVariable = getHomomorphicTypeVariable(type); + if (typeVariable) { + const mappedTypeVariable = instantiateType(typeVariable, mapper); + if (typeVariable !== mappedTypeVariable) { + return mapType(mappedTypeVariable, t => { + if (t.flags & (TypeFlags.AnyOrUnknown | TypeFlags.InstantiableNonPrimitive | TypeFlags.Object | TypeFlags.Intersection) && t !== wildcardType && t !== errorType) { + const replacementMapper = createReplacementMapper(typeVariable, t, mapper); + return isArrayType(t) ? instantiateMappedArrayType(t, type, replacementMapper) : + isTupleType(t) ? instantiateMappedTupleType(t, type, replacementMapper) : + instantiateAnonymousType(type, replacementMapper); + } + return t; + }); } - return undefined; } - - // In a typed function call, an argument or substitution expression is contextually typed by the type of the corresponding parameter. - function getContextualTypeForArgument(callTarget: CallLikeExpression, arg: Expression): Type | undefined { - const args = getEffectiveCallArguments(callTarget); - const argIndex = args.indexOf(arg); // -1 for e.g. the expression of a CallExpression, or the tag of a TaggedTemplateExpression - return argIndex === -1 ? undefined : getContextualTypeForArgumentAtIndex(callTarget, argIndex); + return instantiateAnonymousType(type, mapper); + } + function getModifiedReadonlyState(state: boolean, modifiers: MappedTypeModifiers) { + return modifiers & MappedTypeModifiers.IncludeReadonly ? true : modifiers & MappedTypeModifiers.ExcludeReadonly ? false : state; + } + function instantiateMappedArrayType(arrayType: ts.Type, mappedType: MappedType, mapper: TypeMapper) { + const elementType = instantiateMappedTypeTemplate(mappedType, numberType, /*isOptional*/ true, mapper); + return elementType === errorType ? errorType : + createArrayType(elementType, getModifiedReadonlyState(isReadonlyArrayType(arrayType), getMappedTypeModifiers(mappedType))); + } + function instantiateMappedTupleType(tupleType: TupleTypeReference, mappedType: MappedType, mapper: TypeMapper) { + const minLength = tupleType.target.minLength; + const elementTypes = map(getTypeArguments(tupleType), (_, i) => instantiateMappedTypeTemplate(mappedType, getLiteralType("" + i), i >= minLength, mapper)); + const modifiers = getMappedTypeModifiers(mappedType); + const newMinLength = modifiers & MappedTypeModifiers.IncludeOptional ? 0 : + modifiers & MappedTypeModifiers.ExcludeOptional ? getTypeReferenceArity(tupleType) - (tupleType.target.hasRestElement ? 1 : 0) : + minLength; + const newReadonly = getModifiedReadonlyState(tupleType.target.readonly, modifiers); + return contains(elementTypes, errorType) ? errorType : + createTupleType(elementTypes, newMinLength, tupleType.target.hasRestElement, newReadonly, tupleType.target.associatedNames); + } + function instantiateMappedTypeTemplate(type: MappedType, key: ts.Type, isOptional: boolean, mapper: TypeMapper) { + const templateMapper = combineTypeMappers(mapper, createTypeMapper([getTypeParameterFromMappedType(type)], [key])); + const propType = instantiateType(getTemplateTypeFromMappedType((type.target) || type), templateMapper); + const modifiers = getMappedTypeModifiers(type); + return strictNullChecks && modifiers & MappedTypeModifiers.IncludeOptional && !maybeTypeOfKind(propType, TypeFlags.Undefined | TypeFlags.Void) ? getOptionalType(propType) : + strictNullChecks && modifiers & MappedTypeModifiers.ExcludeOptional && isOptional ? getTypeWithFacts(propType, TypeFacts.NEUndefined) : + propType; + } + function instantiateAnonymousType(type: AnonymousType, mapper: TypeMapper): AnonymousType { + const result = (createObjectType(type.objectFlags | ObjectFlags.Instantiated, type.symbol)); + if (type.objectFlags & ObjectFlags.Mapped) { + (result).declaration = (type).declaration; + // C.f. instantiateSignature + const origTypeParameter = getTypeParameterFromMappedType((type)); + const freshTypeParameter = cloneTypeParameter(origTypeParameter); + (result).typeParameter = freshTypeParameter; + mapper = combineTypeMappers(makeUnaryTypeMapper(origTypeParameter, freshTypeParameter), mapper); + freshTypeParameter.mapper = mapper; + } + result.target = type; + result.mapper = mapper; + result.aliasSymbol = type.aliasSymbol; + result.aliasTypeArguments = instantiateTypes(type.aliasTypeArguments, mapper); + return result; + } + function getConditionalTypeInstantiation(type: ConditionalType, mapper: TypeMapper): ts.Type { + const root = type.root; + if (root.outerTypeParameters) { + // We are instantiating a conditional type that has one or more type parameters in scope. Apply the + // mapper to the type parameters to produce the effective list of type arguments, and compute the + // instantiation cache key from the type IDs of the type arguments. + const typeArguments = map(root.outerTypeParameters, mapper); + const id = getTypeListId(typeArguments); + let result = root.instantiations!.get(id); + if (!result) { + const newMapper = createTypeMapper(root.outerTypeParameters, typeArguments); + result = instantiateConditionalType(root, newMapper); + root.instantiations!.set(id, result); + } + return result; } - - function getContextualTypeForArgumentAtIndex(callTarget: CallLikeExpression, argIndex: number): Type { - // If we're already in the process of resolving the given signature, don't resolve again as - // that could cause infinite recursion. Instead, return anySignature. - const signature = getNodeLinks(callTarget).resolvedSignature === resolvingSignature ? resolvingSignature : getResolvedSignature(callTarget); - - if (isJsxOpeningLikeElement(callTarget) && argIndex === 0) { - return getEffectiveFirstArgumentForJsxSignature(signature, callTarget); + return type; + } + function instantiateConditionalType(root: ConditionalRoot, mapper: TypeMapper): ts.Type { + // Check if we have a conditional type where the check type is a naked type parameter. If so, + // the conditional type is distributive over union types and when T is instantiated to a union + // type A | B, we produce (A extends U ? X : Y) | (B extends U ? X : Y). + if (root.isDistributive) { + const checkType = (root.checkType); + const instantiatedType = mapper(checkType); + if (checkType !== instantiatedType && instantiatedType.flags & (TypeFlags.Union | TypeFlags.Never)) { + return mapType(instantiatedType, t => getConditionalType(root, createReplacementMapper(checkType, t, mapper))); + } + } + return getConditionalType(root, mapper); + } + function instantiateType(type: ts.Type, mapper: TypeMapper | undefined): ts.Type; + function instantiateType(type: ts.Type | undefined, mapper: TypeMapper | undefined): ts.Type | undefined; + function instantiateType(type: ts.Type | undefined, mapper: TypeMapper | undefined): ts.Type | undefined { + if (!type || !mapper || mapper === identityMapper) { + return type; + } + if (instantiationDepth === 50 || instantiationCount >= 5000000) { + // We have reached 50 recursive type instantiations and there is a very high likelyhood we're dealing + // with a combination of infinite generic types that perpetually generate new type identities. We stop + // the recursion here by yielding the error type. + error(currentNode, Diagnostics.Type_instantiation_is_excessively_deep_and_possibly_infinite); + return errorType; + } + totalInstantiationCount++; + instantiationCount++; + instantiationDepth++; + const result = instantiateTypeWorker(type, mapper); + instantiationDepth--; + return result; + } + function instantiateTypeWorker(type: ts.Type, mapper: TypeMapper): ts.Type { + const flags = type.flags; + if (flags & TypeFlags.TypeParameter) { + return mapper(type); + } + if (flags & TypeFlags.Object) { + const objectFlags = (type).objectFlags; + if (objectFlags & ObjectFlags.Anonymous) { + // If the anonymous type originates in a declaration of a function, method, class, or + // interface, in an object type literal, or in an object literal expression, we may need + // to instantiate the type because it might reference a type parameter. + return couldContainTypeVariables(type) ? + getObjectTypeInstantiation((type), mapper) : type; + } + if (objectFlags & ObjectFlags.Mapped) { + return getObjectTypeInstantiation((type), mapper); + } + if (objectFlags & ObjectFlags.Reference) { + if ((type).node) { + return getObjectTypeInstantiation((type), mapper); + } + const resolvedTypeArguments = (type).resolvedTypeArguments; + const newTypeArguments = instantiateTypes(resolvedTypeArguments, mapper); + return newTypeArguments !== resolvedTypeArguments ? createTypeReference((type).target, newTypeArguments) : type; } - return getTypeAtPosition(signature, argIndex); + return type; } - - function getContextualTypeForSubstitutionExpression(template: TemplateExpression, substitutionExpression: Expression) { - if (template.parent.kind === SyntaxKind.TaggedTemplateExpression) { - return getContextualTypeForArgument(template.parent, substitutionExpression); + if ((flags & TypeFlags.Intersection) || (flags & TypeFlags.Union && !(flags & TypeFlags.Primitive))) { + if (!couldContainTypeVariables(type)) { + return type; } - - return undefined; + const types = (type).types; + const newTypes = instantiateTypes(types, mapper); + return newTypes === types + ? type + : (flags & TypeFlags.Intersection) + ? getIntersectionType(newTypes, type.aliasSymbol, instantiateTypes(type.aliasTypeArguments, mapper)) + : getUnionType(newTypes, UnionReduction.Literal, type.aliasSymbol, instantiateTypes(type.aliasTypeArguments, mapper)); } - - function getContextualTypeForBinaryOperand(node: Expression, contextFlags?: ContextFlags): Type | undefined { - const binaryExpression = node.parent; - const { left, operatorToken, right } = binaryExpression; - switch (operatorToken.kind) { - case SyntaxKind.EqualsToken: - if (node !== right) { - return undefined; - } - const contextSensitive = getIsContextSensitiveAssignmentOrContextType(binaryExpression); - if (!contextSensitive) { - return undefined; - } - return contextSensitive === true ? getTypeOfExpression(left) : contextSensitive; - case SyntaxKind.BarBarToken: - case SyntaxKind.QuestionQuestionToken: - // When an || expression has a contextual type, the operands are contextually typed by that type, except - // when that type originates in a binding pattern, the right operand is contextually typed by the type of - // the left operand. When an || expression has no contextual type, the right operand is contextually typed - // by the type of the left operand, except for the special case of Javascript declarations of the form - // `namespace.prop = namespace.prop || {}`. - const type = getContextualType(binaryExpression, contextFlags); - return node === right && (type && type.pattern || !type && !isDefaultedExpandoInitializer(binaryExpression)) ? - getTypeOfExpression(left) : type; - case SyntaxKind.AmpersandAmpersandToken: - case SyntaxKind.CommaToken: - return node === right ? getContextualType(binaryExpression, contextFlags) : undefined; - default: - return undefined; + if (flags & TypeFlags.Index) { + return getIndexType(instantiateType((type).type, mapper)); + } + if (flags & TypeFlags.IndexedAccess) { + return getIndexedAccessType(instantiateType((type).objectType, mapper), instantiateType((type).indexType, mapper)); + } + if (flags & TypeFlags.Conditional) { + return getConditionalTypeInstantiation((type), combineTypeMappers((type).mapper, mapper)); + } + if (flags & TypeFlags.Substitution) { + const maybeVariable = instantiateType((type).typeVariable, mapper); + if (maybeVariable.flags & TypeFlags.TypeVariable) { + return getSubstitutionType((maybeVariable as TypeVariable), instantiateType((type).substitute, mapper)); + } + else { + const sub = instantiateType((type).substitute, mapper); + if (sub.flags & TypeFlags.AnyOrUnknown || isTypeAssignableTo(getRestrictiveInstantiation(maybeVariable), getRestrictiveInstantiation(sub))) { + return maybeVariable; + } + return sub; } } - - // In an assignment expression, the right operand is contextually typed by the type of the left operand. - // Don't do this for assignment declarations unless there is a type tag on the assignment, to avoid circularity from checking the right operand. - function getIsContextSensitiveAssignmentOrContextType(binaryExpression: BinaryExpression): boolean | Type { - const kind = getAssignmentDeclarationKind(binaryExpression); - switch (kind) { - case AssignmentDeclarationKind.None: + return type; + } + function getPermissiveInstantiation(type: ts.Type) { + return type.flags & (TypeFlags.Primitive | TypeFlags.AnyOrUnknown | TypeFlags.Never) ? type : + type.permissiveInstantiation || (type.permissiveInstantiation = instantiateType(type, permissiveMapper)); + } + function getRestrictiveInstantiation(type: ts.Type) { + if (type.flags & (TypeFlags.Primitive | TypeFlags.AnyOrUnknown | TypeFlags.Never)) { + return type; + } + if (type.restrictiveInstantiation) { + return type.restrictiveInstantiation; + } + type.restrictiveInstantiation = instantiateType(type, restrictiveMapper); + // We set the following so we don't attempt to set the restrictive instance of a restrictive instance + // which is redundant - we'll produce new type identities, but all type params have already been mapped. + // This also gives us a way to detect restrictive instances upon comparisons and _disable_ the "distributeive constraint" + // assignability check for them, which is distinctly unsafe, as once you have a restrctive instance, all the type parameters + // are constrained to `unknown` and produce tons of false positives/negatives! + type.restrictiveInstantiation.restrictiveInstantiation = type.restrictiveInstantiation; + return type.restrictiveInstantiation; + } + function instantiateIndexInfo(info: IndexInfo | undefined, mapper: TypeMapper): IndexInfo | undefined { + return info && createIndexInfo(instantiateType(info.type, mapper), info.isReadonly, info.declaration); + } + // Returns true if the given expression contains (at any level of nesting) a function or arrow expression + // that is subject to contextual typing. + function isContextSensitive(node: Expression | MethodDeclaration | ObjectLiteralElementLike | JsxAttributeLike | JsxChild): boolean { + Debug.assert(node.kind !== SyntaxKind.MethodDeclaration || isObjectLiteralMethod(node)); + switch (node.kind) { + case SyntaxKind.FunctionExpression: + case SyntaxKind.ArrowFunction: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.FunctionDeclaration: // Function declarations can have context when annotated with a jsdoc @type + return isContextSensitiveFunctionLikeDeclaration((node)); + case SyntaxKind.ObjectLiteralExpression: + return some((node).properties, isContextSensitive); + case SyntaxKind.ArrayLiteralExpression: + return some((node).elements, isContextSensitive); + case SyntaxKind.ConditionalExpression: + return isContextSensitive((node).whenTrue) || + isContextSensitive((node).whenFalse); + case SyntaxKind.BinaryExpression: + return ((node).operatorToken.kind === SyntaxKind.BarBarToken || (node).operatorToken.kind === SyntaxKind.QuestionQuestionToken) && + (isContextSensitive((node).left) || isContextSensitive((node).right)); + case SyntaxKind.PropertyAssignment: + return isContextSensitive((node).initializer); + case SyntaxKind.ParenthesizedExpression: + return isContextSensitive((node).expression); + case SyntaxKind.JsxAttributes: + return some((node).properties, isContextSensitive) || isJsxOpeningElement(node.parent) && some(node.parent.parent.children, isContextSensitive); + case SyntaxKind.JsxAttribute: { + // If there is no initializer, JSX attribute has a boolean value of true which is not context sensitive. + const { initializer } = (node as JsxAttribute); + return !!initializer && isContextSensitive(initializer); + } + case SyntaxKind.JsxExpression: { + // It is possible to that node.expression is undefined (e.g
) + const { expression } = (node as JsxExpression); + return !!expression && isContextSensitive(expression); + } + } + return false; + } + function isContextSensitiveFunctionLikeDeclaration(node: FunctionLikeDeclaration): boolean { + return (!isFunctionDeclaration(node) || isInJSFile(node) && !!getTypeForDeclarationFromJSDocComment(node)) && + (hasContextSensitiveParameters(node) || hasContextSensitiveReturnExpression(node)); + } + function hasContextSensitiveParameters(node: FunctionLikeDeclaration) { + // Functions with type parameters are not context sensitive. + if (!node.typeParameters) { + // Functions with any parameters that lack type annotations are context sensitive. + if (some(node.parameters, p => !getEffectiveTypeAnnotationNode(p))) { + return true; + } + if (node.kind !== SyntaxKind.ArrowFunction) { + // If the first parameter is not an explicit 'this' parameter, then the function has + // an implicit 'this' parameter which is subject to contextual typing. + const parameter = firstOrUndefined(node.parameters); + if (!(parameter && parameterIsThisKeyword(parameter))) { return true; - case AssignmentDeclarationKind.Property: - case AssignmentDeclarationKind.ExportsProperty: - case AssignmentDeclarationKind.Prototype: - case AssignmentDeclarationKind.PrototypeProperty: - // If `binaryExpression.left` was assigned a symbol, then this is a new declaration; otherwise it is an assignment to an existing declaration. - // See `bindStaticPropertyAssignment` in `binder.ts`. - if (!binaryExpression.left.symbol) { - return true; - } - else { - const decl = binaryExpression.left.symbol.valueDeclaration; - if (!decl) { - return false; - } - const lhs = cast(binaryExpression.left, isAccessExpression); - const overallAnnotation = getEffectiveTypeAnnotationNode(decl); - if (overallAnnotation) { - return getTypeFromTypeNode(overallAnnotation); - } - else if (isIdentifier(lhs.expression)) { - const id = lhs.expression; - const parentSymbol = resolveName(id, id.escapedText, SymbolFlags.Value, undefined, id.escapedText, /*isUse*/ true); - if (parentSymbol) { - const annotated = getEffectiveTypeAnnotationNode(parentSymbol.valueDeclaration); - if (annotated) { - const nameStr = getElementOrPropertyAccessName(lhs); - if (nameStr !== undefined) { - const type = getTypeOfPropertyOfContextualType(getTypeFromTypeNode(annotated), nameStr); - return type || false; - } - } - return false; - } - } - return !isInJSFile(decl); - } - case AssignmentDeclarationKind.ModuleExports: - case AssignmentDeclarationKind.ThisProperty: - if (!binaryExpression.symbol) return true; - if (binaryExpression.symbol.valueDeclaration) { - const annotated = getEffectiveTypeAnnotationNode(binaryExpression.symbol.valueDeclaration); - if (annotated) { - const type = getTypeFromTypeNode(annotated); - if (type) { - return type; - } - } - } - if (kind === AssignmentDeclarationKind.ModuleExports) return false; - const thisAccess = cast(binaryExpression.left, isAccessExpression); - if (!isObjectLiteralMethod(getThisContainer(thisAccess.expression, /*includeArrowFunctions*/ false))) { - return false; - } - const thisType = checkThisExpression(thisAccess.expression); - const nameStr = getElementOrPropertyAccessName(thisAccess); - return nameStr !== undefined && thisType && getTypeOfPropertyOfContextualType(thisType, nameStr) || false; - case AssignmentDeclarationKind.ObjectDefinePropertyValue: - case AssignmentDeclarationKind.ObjectDefinePropertyExports: - case AssignmentDeclarationKind.ObjectDefinePrototypeProperty: - return Debug.fail("Does not apply"); - default: - return Debug.assertNever(kind); + } } } - - function getTypeOfPropertyOfContextualType(type: Type, name: __String) { - return mapType(type, t => { - if (isGenericMappedType(t)) { - const constraint = getConstraintTypeFromMappedType(t); - const constraintOfConstraint = getBaseConstraintOfType(constraint) || constraint; - const propertyNameType = getLiteralType(unescapeLeadingUnderscores(name)); - if (isTypeAssignableTo(propertyNameType, constraintOfConstraint)) { - return substituteIndexedMappedType(t, propertyNameType); - } - } - else if (t.flags & TypeFlags.StructuredType) { - const prop = getPropertyOfType(t, name); - if (prop) { - return getTypeOfSymbol(prop); - } - if (isTupleType(t)) { - const restType = getRestTypeOfTupleType(t); - if (restType && isNumericLiteralName(name) && +name >= 0) { - return restType; - } - } - return isNumericLiteralName(name) && getIndexTypeOfContextualType(t, IndexKind.Number) || - getIndexTypeOfContextualType(t, IndexKind.String); - } - return undefined; - }, /*noReductions*/ true); + return false; + } + function hasContextSensitiveReturnExpression(node: FunctionLikeDeclaration) { + // TODO(anhans): A block should be context-sensitive if it has a context-sensitive return value. + return !getEffectiveReturnTypeNode(node) && !!node.body && node.body.kind !== SyntaxKind.Block && isContextSensitive(node.body); + } + function isContextSensitiveFunctionOrObjectLiteralMethod(func: Node): func is FunctionExpression | ArrowFunction | MethodDeclaration { + return (isInJSFile(func) && isFunctionDeclaration(func) || isFunctionExpressionOrArrowFunction(func) || isObjectLiteralMethod(func)) && + isContextSensitiveFunctionLikeDeclaration(func); + } + function getTypeWithoutSignatures(type: ts.Type): ts.Type { + if (type.flags & TypeFlags.Object) { + const resolved = resolveStructuredTypeMembers((type)); + if (resolved.constructSignatures.length || resolved.callSignatures.length) { + const result = createObjectType(ObjectFlags.Anonymous, type.symbol); + result.members = resolved.members; + result.properties = resolved.properties; + result.callSignatures = emptyArray; + result.constructSignatures = emptyArray; + return result; + } + } + else if (type.flags & TypeFlags.Intersection) { + return getIntersectionType(map((type).types, getTypeWithoutSignatures)); + } + return type; + } + // TYPE CHECKING + function isTypeIdenticalTo(source: ts.Type, target: ts.Type): boolean { + return isTypeRelatedTo(source, target, identityRelation); + } + function compareTypesIdentical(source: ts.Type, target: ts.Type): Ternary { + return isTypeRelatedTo(source, target, identityRelation) ? Ternary.True : Ternary.False; + } + function compareTypesAssignable(source: ts.Type, target: ts.Type): Ternary { + return isTypeRelatedTo(source, target, assignableRelation) ? Ternary.True : Ternary.False; + } + function compareTypesSubtypeOf(source: ts.Type, target: ts.Type): Ternary { + return isTypeRelatedTo(source, target, subtypeRelation) ? Ternary.True : Ternary.False; + } + function isTypeSubtypeOf(source: ts.Type, target: ts.Type): boolean { + return isTypeRelatedTo(source, target, subtypeRelation); + } + function isTypeAssignableTo(source: ts.Type, target: ts.Type): boolean { + return isTypeRelatedTo(source, target, assignableRelation); + } + // An object type S is considered to be derived from an object type T if + // S is a union type and every constituent of S is derived from T, + // T is a union type and S is derived from at least one constituent of T, or + // S is a type variable with a base constraint that is derived from T, + // T is one of the global types Object and Function and S is a subtype of T, or + // T occurs directly or indirectly in an 'extends' clause of S. + // Note that this check ignores type parameters and only considers the + // inheritance hierarchy. + function isTypeDerivedFrom(source: ts.Type, target: ts.Type): boolean { + return source.flags & TypeFlags.Union ? every((source).types, t => isTypeDerivedFrom(t, target)) : + target.flags & TypeFlags.Union ? some((target).types, t => isTypeDerivedFrom(source, t)) : + source.flags & TypeFlags.InstantiableNonPrimitive ? isTypeDerivedFrom(getBaseConstraintOfType(source) || unknownType, target) : + target === globalObjectType ? !!(source.flags & (TypeFlags.Object | TypeFlags.NonPrimitive)) : + target === globalFunctionType ? !!(source.flags & TypeFlags.Object) && isFunctionObjectType((source as ObjectType)) : + hasBaseType(source, getTargetType(target)); + } + /** + * This is *not* a bi-directional relationship. + * If one needs to check both directions for comparability, use a second call to this function or 'checkTypeComparableTo'. + * + * A type S is comparable to a type T if some (but not necessarily all) of the possible values of S are also possible values of T. + * It is used to check following cases: + * - the types of the left and right sides of equality/inequality operators (`===`, `!==`, `==`, `!=`). + * - the types of `case` clause expressions and their respective `switch` expressions. + * - the type of an expression in a type assertion with the type being asserted. + */ + function isTypeComparableTo(source: ts.Type, target: ts.Type): boolean { + return isTypeRelatedTo(source, target, comparableRelation); + } + function areTypesComparable(type1: ts.Type, type2: ts.Type): boolean { + return isTypeComparableTo(type1, type2) || isTypeComparableTo(type2, type1); + } + function checkTypeAssignableTo(source: ts.Type, target: ts.Type, errorNode: Node | undefined, headMessage?: DiagnosticMessage, containingMessageChain?: () => DiagnosticMessageChain | undefined, errorOutputObject?: { + errors?: Diagnostic[]; + }): boolean { + return checkTypeRelatedTo(source, target, assignableRelation, errorNode, headMessage, containingMessageChain, errorOutputObject); + } + /** + * Like `checkTypeAssignableTo`, but if it would issue an error, instead performs structural comparisons of the types using the given expression node to + * attempt to issue more specific errors on, for example, specific object literal properties or tuple members. + */ + function checkTypeAssignableToAndOptionallyElaborate(source: ts.Type, target: ts.Type, errorNode: Node | undefined, expr: Expression | undefined, headMessage?: DiagnosticMessage, containingMessageChain?: () => DiagnosticMessageChain | undefined): boolean { + return checkTypeRelatedToAndOptionallyElaborate(source, target, assignableRelation, errorNode, expr, headMessage, containingMessageChain, /*errorOutputContainer*/ undefined); + } + function checkTypeRelatedToAndOptionallyElaborate(source: ts.Type, target: ts.Type, relation: ts.Map, errorNode: Node | undefined, expr: Expression | undefined, headMessage: DiagnosticMessage | undefined, containingMessageChain: (() => DiagnosticMessageChain | undefined) | undefined, errorOutputContainer: { + errors?: Diagnostic[]; + skipLogging?: boolean; + } | undefined): boolean { + if (isTypeRelatedTo(source, target, relation)) + return true; + if (!errorNode || !elaborateError(expr, source, target, relation, headMessage, containingMessageChain, errorOutputContainer)) { + return checkTypeRelatedTo(source, target, relation, errorNode, headMessage, containingMessageChain, errorOutputContainer); + } + return false; + } + function isOrHasGenericConditional(type: ts.Type): boolean { + return !!(type.flags & TypeFlags.Conditional || (type.flags & TypeFlags.Intersection && some((type as IntersectionType).types, isOrHasGenericConditional))); + } + function elaborateError(node: Expression | undefined, source: ts.Type, target: ts.Type, relation: ts.Map, headMessage: DiagnosticMessage | undefined, containingMessageChain: (() => DiagnosticMessageChain | undefined) | undefined, errorOutputContainer: { + errors?: Diagnostic[]; + skipLogging?: boolean; + } | undefined): boolean { + if (!node || isOrHasGenericConditional(target)) + return false; + if (!checkTypeRelatedTo(source, target, relation, /*errorNode*/ undefined) + && elaborateDidYouMeanToCallOrConstruct(node, source, target, relation, headMessage, containingMessageChain, errorOutputContainer)) { + return true; + } + switch (node.kind) { + case SyntaxKind.JsxExpression: + case SyntaxKind.ParenthesizedExpression: + return elaborateError((node as ParenthesizedExpression | JsxExpression).expression, source, target, relation, headMessage, containingMessageChain, errorOutputContainer); + case SyntaxKind.BinaryExpression: + switch ((node as BinaryExpression).operatorToken.kind) { + case SyntaxKind.EqualsToken: + case SyntaxKind.CommaToken: + return elaborateError((node as BinaryExpression).right, source, target, relation, headMessage, containingMessageChain, errorOutputContainer); + } + break; + case SyntaxKind.ObjectLiteralExpression: + return elaborateObjectLiteral((node as ObjectLiteralExpression), source, target, relation, containingMessageChain, errorOutputContainer); + case SyntaxKind.ArrayLiteralExpression: + return elaborateArrayLiteral((node as ArrayLiteralExpression), source, target, relation, containingMessageChain, errorOutputContainer); + case SyntaxKind.JsxAttributes: + return elaborateJsxComponents((node as JsxAttributes), source, target, relation, containingMessageChain, errorOutputContainer); + case SyntaxKind.ArrowFunction: + return elaborateArrowFunction((node as ArrowFunction), source, target, relation, containingMessageChain, errorOutputContainer); + } + return false; + } + function elaborateDidYouMeanToCallOrConstruct(node: Expression, source: ts.Type, target: ts.Type, relation: ts.Map, headMessage: DiagnosticMessage | undefined, containingMessageChain: (() => DiagnosticMessageChain | undefined) | undefined, errorOutputContainer: { + errors?: Diagnostic[]; + skipLogging?: boolean; + } | undefined): boolean { + const callSignatures = getSignaturesOfType(source, SignatureKind.Call); + const constructSignatures = getSignaturesOfType(source, SignatureKind.Construct); + for (const signatures of [constructSignatures, callSignatures]) { + if (some(signatures, s => { + const returnType = getReturnTypeOfSignature(s); + return !(returnType.flags & (TypeFlags.Any | TypeFlags.Never)) && checkTypeRelatedTo(returnType, target, relation, /*errorNode*/ undefined); + })) { + const resultObj: { + errors?: Diagnostic[]; + } = errorOutputContainer || {}; + checkTypeAssignableTo(source, target, node, headMessage, containingMessageChain, resultObj); + const diagnostic = resultObj.errors![resultObj.errors!.length - 1]; + addRelatedInfo(diagnostic, createDiagnosticForNode(node, signatures === constructSignatures ? Diagnostics.Did_you_mean_to_use_new_with_this_expression : Diagnostics.Did_you_mean_to_call_this_expression)); + return true; + } } - - function getIndexTypeOfContextualType(type: Type, kind: IndexKind) { - return mapType(type, t => getIndexTypeOfStructuredType(t, kind), /*noReductions*/ true); + return false; + } + function elaborateArrowFunction(node: ArrowFunction, source: ts.Type, target: ts.Type, relation: ts.Map, containingMessageChain: (() => DiagnosticMessageChain | undefined) | undefined, errorOutputContainer: { + errors?: Diagnostic[]; + skipLogging?: boolean; + } | undefined): boolean { + // Don't elaborate blocks + if (isBlock(node.body)) { + return false; } - - // In an object literal contextually typed by a type T, the contextual type of a property assignment is the type of - // the matching property in T, if one exists. Otherwise, it is the type of the numeric index signature in T, if one - // exists. Otherwise, it is the type of the string index signature in T, if one exists. - function getContextualTypeForObjectLiteralMethod(node: MethodDeclaration, contextFlags?: ContextFlags): Type | undefined { - Debug.assert(isObjectLiteralMethod(node)); - if (node.flags & NodeFlags.InWithStatement) { - // We cannot answer semantic questions within a with block, do not proceed any further - return undefined; - } - return getContextualTypeForObjectLiteralElement(node, contextFlags); + // Or functions with annotated parameter types + if (some(node.parameters, ts.hasType)) { + return false; } - - function getContextualTypeForObjectLiteralElement(element: ObjectLiteralElementLike, contextFlags?: ContextFlags) { - const objectLiteral = element.parent; - const type = getApparentTypeOfContextualType(objectLiteral, contextFlags); - if (type) { - if (!hasNonBindableDynamicName(element)) { - // For a (non-symbol) computed property, there is no reason to look up the name - // in the type. It will just be "__computed", which does not appear in any - // SymbolTable. - const symbolName = getSymbolOfNode(element).escapedName; - const propertyType = getTypeOfPropertyOfContextualType(type, symbolName); - if (propertyType) { - return propertyType; - } + const sourceSig = getSingleCallSignature(source); + if (!sourceSig) { + return false; + } + const targetSignatures = getSignaturesOfType(target, SignatureKind.Call); + if (!length(targetSignatures)) { + return false; + } + const returnExpression = node.body; + const sourceReturn = getReturnTypeOfSignature(sourceSig); + const targetReturn = getUnionType(map(targetSignatures, getReturnTypeOfSignature)); + if (!checkTypeRelatedTo(sourceReturn, targetReturn, relation, /*errorNode*/ undefined)) { + const elaborated = returnExpression && elaborateError(returnExpression, sourceReturn, targetReturn, relation, /*headMessage*/ undefined, containingMessageChain, errorOutputContainer); + if (elaborated) { + return elaborated; + } + const resultObj: { + errors?: Diagnostic[]; + } = errorOutputContainer || {}; + checkTypeRelatedTo(sourceReturn, targetReturn, relation, returnExpression, /*message*/ undefined, containingMessageChain, resultObj); + if (resultObj.errors) { + if (target.symbol && length(target.symbol.declarations)) { + addRelatedInfo(resultObj.errors[resultObj.errors.length - 1], createDiagnosticForNode(target.symbol.declarations[0], Diagnostics.The_expected_type_comes_from_the_return_type_of_this_signature)); + } + if ((getFunctionFlags(node) & FunctionFlags.Async) === 0 + // exclude cases where source itself is promisy - this way we don't make a suggestion when relating + // an IPromise and a Promise that are slightly different + && !getTypeOfPropertyOfType(sourceReturn, ("then" as __String)) + && checkTypeRelatedTo(createPromiseType(sourceReturn), targetReturn, relation, /*errorNode*/ undefined)) { + addRelatedInfo(resultObj.errors[resultObj.errors.length - 1], createDiagnosticForNode(node, Diagnostics.Did_you_mean_to_mark_this_function_as_async)); } - return isNumericName(element.name!) && getIndexTypeOfContextualType(type, IndexKind.Number) || - getIndexTypeOfContextualType(type, IndexKind.String); + return true; } - return undefined; } - - // In an array literal contextually typed by a type T, the contextual type of an element expression at index N is - // the type of the property with the numeric name N in T, if one exists. Otherwise, if T has a numeric index signature, - // it is the type of the numeric index signature in T. Otherwise, in ES6 and higher, the contextual type is the iterated - // type of T. - function getContextualTypeForElementExpression(arrayContextualType: Type | undefined, index: number): Type | undefined { - return arrayContextualType && ( - getTypeOfPropertyOfContextualType(arrayContextualType, "" + index as __String) - || getIteratedTypeOrElementType(IterationUse.Element, arrayContextualType, undefinedType, /*errorNode*/ undefined, /*checkAssignability*/ false)); - } - - // In a contextually typed conditional expression, the true/false expressions are contextually typed by the same type. - function getContextualTypeForConditionalOperand(node: Expression, contextFlags?: ContextFlags): Type | undefined { - const conditional = node.parent; - return node === conditional.whenTrue || node === conditional.whenFalse ? getContextualType(conditional, contextFlags) : undefined; + return false; + } + function getBestMatchIndexedAccessTypeOrUndefined(source: ts.Type, target: ts.Type, nameType: ts.Type) { + const idx = getIndexedAccessTypeOrUndefined(target, nameType); + if (idx) { + return idx; } - - function getContextualTypeForChildJsxExpression(node: JsxElement, child: JsxChild) { - const attributesType = getApparentTypeOfContextualType(node.openingElement.tagName); - // JSX expression is in children of JSX Element, we will look for an "children" atttribute (we get the name from JSX.ElementAttributesProperty) - const jsxChildrenPropertyName = getJsxElementChildrenPropertyName(getJsxNamespaceAt(node)); - if (!(attributesType && !isTypeAny(attributesType) && jsxChildrenPropertyName && jsxChildrenPropertyName !== "")) { - return undefined; + if (target.flags & TypeFlags.Union) { + const best = getBestMatchingType(source, (target as UnionType)); + if (best) { + return getIndexedAccessTypeOrUndefined(best, nameType); } - const realChildren = getSemanticJsxChildren(node.children); - const childIndex = realChildren.indexOf(child); - const childFieldType = getTypeOfPropertyOfContextualType(attributesType, jsxChildrenPropertyName); - return childFieldType && (realChildren.length === 1 ? childFieldType : mapType(childFieldType, t => { - if (isArrayLikeType(t)) { - return getIndexedAccessType(t, getLiteralType(childIndex)); + } + } + type ElaborationIterator = IterableIterator<{ + errorNode: Node; + innerExpression: Expression | undefined; + nameType: ts.Type; + errorMessage?: DiagnosticMessage | undefined; + }>; + /** + * For every element returned from the iterator, checks that element to issue an error on a property of that element's type + * If that element would issue an error, we first attempt to dive into that element's inner expression and issue a more specific error by recuring into `elaborateError` + * Otherwise, we issue an error on _every_ element which fail the assignability check + */ + function elaborateElementwise(iterator: ElaborationIterator, source: ts.Type, target: ts.Type, relation: ts.Map, containingMessageChain: (() => DiagnosticMessageChain | undefined) | undefined, errorOutputContainer: { + errors?: Diagnostic[]; + skipLogging?: boolean; + } | undefined) { + // Assignability failure - check each prop individually, and if that fails, fall back on the bad error span + let reportedError = false; + for (let status = iterator.next(); !status.done; status = iterator.next()) { + const { errorNode: prop, innerExpression: next, nameType, errorMessage } = status.value; + const targetPropType = getBestMatchIndexedAccessTypeOrUndefined(source, target, nameType); + if (!targetPropType || targetPropType.flags & TypeFlags.IndexedAccess) + continue; // Don't elaborate on indexes on generic variables + const sourcePropType = getIndexedAccessTypeOrUndefined(source, nameType); + if (sourcePropType && !checkTypeRelatedTo(sourcePropType, targetPropType, relation, /*errorNode*/ undefined)) { + const elaborated = next && elaborateError(next, sourcePropType, targetPropType, relation, /*headMessage*/ undefined, containingMessageChain, errorOutputContainer); + if (elaborated) { + reportedError = true; } else { - return t; + // Issue error on the prop itself, since the prop couldn't elaborate the error + const resultObj: { + errors?: Diagnostic[]; + } = errorOutputContainer || {}; + // Use the expression type, if available + const specificSource = next ? checkExpressionForMutableLocation(next, CheckMode.Normal, sourcePropType) : sourcePropType; + const result = checkTypeRelatedTo(specificSource, targetPropType, relation, prop, errorMessage, containingMessageChain, resultObj); + if (result && specificSource !== sourcePropType) { + // If for whatever reason the expression type doesn't yield an error, make sure we still issue an error on the sourcePropType + checkTypeRelatedTo(sourcePropType, targetPropType, relation, prop, errorMessage, containingMessageChain, resultObj); + } + if (resultObj.errors) { + const reportedDiag = resultObj.errors[resultObj.errors.length - 1]; + const propertyName = isTypeUsableAsPropertyName(nameType) ? getPropertyNameFromType(nameType) : undefined; + const targetProp = propertyName !== undefined ? getPropertyOfType(target, propertyName) : undefined; + let issuedElaboration = false; + if (!targetProp) { + const indexInfo = isTypeAssignableToKind(nameType, TypeFlags.NumberLike) && getIndexInfoOfType(target, IndexKind.Number) || + getIndexInfoOfType(target, IndexKind.String) || + undefined; + if (indexInfo && indexInfo.declaration && !getSourceFileOfNode(indexInfo.declaration).hasNoDefaultLib) { + issuedElaboration = true; + addRelatedInfo(reportedDiag, createDiagnosticForNode(indexInfo.declaration, Diagnostics.The_expected_type_comes_from_this_index_signature)); + } + } + if (!issuedElaboration && (targetProp && length(targetProp.declarations) || target.symbol && length(target.symbol.declarations))) { + const targetNode = targetProp && length(targetProp.declarations) ? targetProp.declarations[0] : target.symbol.declarations[0]; + if (!getSourceFileOfNode(targetNode).hasNoDefaultLib) { + addRelatedInfo(reportedDiag, createDiagnosticForNode(targetNode, Diagnostics.The_expected_type_comes_from_property_0_which_is_declared_here_on_type_1, propertyName && !(nameType.flags & TypeFlags.UniqueESSymbol) ? unescapeLeadingUnderscores(propertyName) : typeToString(nameType), typeToString(target))); + } + } + } + reportedError = true; } - }, /*noReductions*/ true)); + } } - - function getContextualTypeForJsxExpression(node: JsxExpression): Type | undefined { - const exprParent = node.parent; - return isJsxAttributeLike(exprParent) - ? getContextualType(node) - : isJsxElement(exprParent) - ? getContextualTypeForChildJsxExpression(exprParent, node) - : undefined; + return reportedError; + } + function* generateJsxAttributes(node: JsxAttributes): ElaborationIterator { + if (!length(node.properties)) + return; + for (const prop of node.properties) { + if (isJsxSpreadAttribute(prop)) + continue; + yield { errorNode: prop.name, innerExpression: prop.initializer, nameType: getLiteralType(idText(prop.name)) }; } - - function getContextualTypeForJsxAttribute(attribute: JsxAttribute | JsxSpreadAttribute): Type | undefined { - // When we trying to resolve JsxOpeningLikeElement as a stateless function element, we will already give its attributes a contextual type - // which is a type of the parameter of the signature we are trying out. - // If there is no contextual type (e.g. we are trying to resolve stateful component), get attributes type from resolving element's tagName - if (isJsxAttribute(attribute)) { - const attributesType = getApparentTypeOfContextualType(attribute.parent); - if (!attributesType || isTypeAny(attributesType)) { - return undefined; - } - return getTypeOfPropertyOfContextualType(attributesType, attribute.name.escapedText); + } + function* generateJsxChildren(node: JsxElement, getInvalidTextDiagnostic: () => DiagnosticMessage): ElaborationIterator { + if (!length(node.children)) + return; + let memberOffset = 0; + for (let i = 0; i < node.children.length; i++) { + const child = node.children[i]; + const nameType = getLiteralType(i - memberOffset); + const elem = getElaborationElementForJsxChild(child, nameType, getInvalidTextDiagnostic); + if (elem) { + yield elem; } else { - return getContextualType(attribute.parent); - } - } - - // Return true if the given expression is possibly a discriminant value. We limit the kinds of - // expressions we check to those that don't depend on their contextual type in order not to cause - // recursive (and possibly infinite) invocations of getContextualType. - function isPossiblyDiscriminantValue(node: Expression): boolean { - switch (node.kind) { - case SyntaxKind.StringLiteral: - case SyntaxKind.NumericLiteral: - case SyntaxKind.BigIntLiteral: - case SyntaxKind.NoSubstitutionTemplateLiteral: - case SyntaxKind.TrueKeyword: - case SyntaxKind.FalseKeyword: - case SyntaxKind.NullKeyword: - case SyntaxKind.Identifier: - case SyntaxKind.UndefinedKeyword: - return true; - case SyntaxKind.PropertyAccessExpression: - case SyntaxKind.ParenthesizedExpression: - return isPossiblyDiscriminantValue((node).expression); - case SyntaxKind.JsxExpression: - return !(node as JsxExpression).expression || isPossiblyDiscriminantValue((node as JsxExpression).expression!); + memberOffset++; } - return false; - } - - function discriminateContextualTypeByObjectMembers(node: ObjectLiteralExpression, contextualType: UnionType) { - return discriminateTypeByDiscriminableItems(contextualType, - map( - filter(node.properties, p => !!p.symbol && p.kind === SyntaxKind.PropertyAssignment && isPossiblyDiscriminantValue(p.initializer) && isDiscriminantProperty(contextualType, p.symbol.escapedName)), - prop => ([() => checkExpression((prop as PropertyAssignment).initializer), prop.symbol.escapedName] as [() => Type, __String]) - ), - isTypeAssignableTo, - contextualType - ); } - - function discriminateContextualTypeByJSXAttributes(node: JsxAttributes, contextualType: UnionType) { - return discriminateTypeByDiscriminableItems(contextualType, - map( - filter(node.properties, p => !!p.symbol && p.kind === SyntaxKind.JsxAttribute && isDiscriminantProperty(contextualType, p.symbol.escapedName) && (!p.initializer || isPossiblyDiscriminantValue(p.initializer))), - prop => ([!(prop as JsxAttribute).initializer ? (() => trueType) : (() => checkExpression((prop as JsxAttribute).initializer!)), prop.symbol.escapedName] as [() => Type, __String]) - ), - isTypeAssignableTo, - contextualType - ); + } + function getElaborationElementForJsxChild(child: JsxChild, nameType: LiteralType, getInvalidTextDiagnostic: () => DiagnosticMessage) { + switch (child.kind) { + case SyntaxKind.JsxExpression: + // child is of the type of the expression + return { errorNode: child, innerExpression: child.expression, nameType }; + case SyntaxKind.JsxText: + if (child.containsOnlyTriviaWhiteSpaces) { + break; // Whitespace only jsx text isn't real jsx text + } + // child is a string + return { errorNode: child, innerExpression: undefined, nameType, errorMessage: getInvalidTextDiagnostic() }; + case SyntaxKind.JsxElement: + case SyntaxKind.JsxSelfClosingElement: + case SyntaxKind.JsxFragment: + // child is of type JSX.Element + return { errorNode: child, innerExpression: child, nameType }; + default: + return Debug.assertNever(child, "Found invalid jsx child"); } - - // Return the contextual type for a given expression node. During overload resolution, a contextual type may temporarily - // be "pushed" onto a node using the contextualType property. - function getApparentTypeOfContextualType(node: Expression | MethodDeclaration, contextFlags?: ContextFlags): Type | undefined { - const contextualType = isObjectLiteralMethod(node) ? - getContextualTypeForObjectLiteralMethod(node, contextFlags) : - getContextualType(node, contextFlags); - const instantiatedType = instantiateContextualType(contextualType, node, contextFlags); - if (instantiatedType && !(contextFlags && contextFlags & ContextFlags.NoConstraints && instantiatedType.flags & TypeFlags.TypeVariable)) { - const apparentType = mapType(instantiatedType, getApparentType, /*noReductions*/ true); - if (apparentType.flags & TypeFlags.Union) { - if (isObjectLiteralExpression(node)) { - return discriminateContextualTypeByObjectMembers(node, apparentType as UnionType); - } - else if (isJsxAttributes(node)) { - return discriminateContextualTypeByJSXAttributes(node, apparentType as UnionType); + } + function getSemanticJsxChildren(children: NodeArray) { + return filter(children, i => !isJsxText(i) || !i.containsOnlyTriviaWhiteSpaces); + } + function elaborateJsxComponents(node: JsxAttributes, source: ts.Type, target: ts.Type, relation: ts.Map, containingMessageChain: (() => DiagnosticMessageChain | undefined) | undefined, errorOutputContainer: { + errors?: Diagnostic[]; + skipLogging?: boolean; + } | undefined) { + let result = elaborateElementwise(generateJsxAttributes(node), source, target, relation, containingMessageChain, errorOutputContainer); + let invalidTextDiagnostic: DiagnosticMessage | undefined; + if (isJsxOpeningElement(node.parent) && isJsxElement(node.parent.parent)) { + const containingElement = node.parent.parent; + const childPropName = getJsxElementChildrenPropertyName(getJsxNamespaceAt(node)); + const childrenPropName = childPropName === undefined ? "children" : unescapeLeadingUnderscores(childPropName); + const childrenNameType = getLiteralType(childrenPropName); + const childrenTargetType = getIndexedAccessType(target, childrenNameType); + const validChildren = getSemanticJsxChildren(containingElement.children); + if (!length(validChildren)) { + return result; + } + const moreThanOneRealChildren = length(validChildren) > 1; + const arrayLikeTargetParts = filterType(childrenTargetType, isArrayOrTupleLikeType); + const nonArrayLikeTargetParts = filterType(childrenTargetType, t => !isArrayOrTupleLikeType(t)); + if (moreThanOneRealChildren) { + if (arrayLikeTargetParts !== neverType) { + const realSource = createTupleType(checkJsxChildren(containingElement, CheckMode.Normal)); + const children = generateJsxChildren(containingElement, getInvalidTextualChildDiagnostic); + result = elaborateElementwise(children, realSource, arrayLikeTargetParts, relation, containingMessageChain, errorOutputContainer) || result; + } + else if (!isTypeRelatedTo(getIndexedAccessType(source, childrenNameType), childrenTargetType, relation)) { + // arity mismatch + result = true; + const diag = error(containingElement.openingElement.tagName, Diagnostics.This_JSX_tag_s_0_prop_expects_a_single_child_of_type_1_but_multiple_children_were_provided, childrenPropName, typeToString(childrenTargetType)); + if (errorOutputContainer && errorOutputContainer.skipLogging) { + (errorOutputContainer.errors || (errorOutputContainer.errors = [])).push(diag); } } - return apparentType; } - } - - // If the given contextual type contains instantiable types and if a mapper representing - // return type inferences is available, instantiate those types using that mapper. - function instantiateContextualType(contextualType: Type | undefined, node: Node, contextFlags?: ContextFlags): Type | undefined { - if (contextualType && maybeTypeOfKind(contextualType, TypeFlags.Instantiable)) { - const inferenceContext = getInferenceContext(node); - // If no inferences have been made, nothing is gained from instantiating as type parameters - // would just be replaced with their defaults similar to the apparent type. - if (inferenceContext && some(inferenceContext.inferences, hasInferenceCandidates)) { - // For contextual signatures we incorporate all inferences made so far, e.g. from return - // types as well as arguments to the left in a function call. - if (contextFlags && contextFlags & ContextFlags.Signature) { - return instantiateInstantiableTypes(contextualType, inferenceContext.nonFixingMapper); + else { + if (nonArrayLikeTargetParts !== neverType) { + const child = validChildren[0]; + const elem = getElaborationElementForJsxChild(child, childrenNameType, getInvalidTextualChildDiagnostic); + if (elem) { + result = elaborateElementwise((function* () { yield elem; })(), source, target, relation, containingMessageChain, errorOutputContainer) || result; } - // For other purposes (e.g. determining whether to produce literal types) we only - // incorporate inferences made from the return type in a function call. - if (inferenceContext.returnMapper) { - return instantiateInstantiableTypes(contextualType, inferenceContext.returnMapper); + } + else if (!isTypeRelatedTo(getIndexedAccessType(source, childrenNameType), childrenTargetType, relation)) { + // arity mismatch + result = true; + const diag = error(containingElement.openingElement.tagName, Diagnostics.This_JSX_tag_s_0_prop_expects_type_1_which_requires_multiple_children_but_only_a_single_child_was_provided, childrenPropName, typeToString(childrenTargetType)); + if (errorOutputContainer && errorOutputContainer.skipLogging) { + (errorOutputContainer.errors || (errorOutputContainer.errors = [])).push(diag); } } } - return contextualType; } - - // This function is similar to instantiateType, except that (a) it only instantiates types that - // are classified as instantiable (i.e. it doesn't instantiate object types), and (b) it performs - // no reductions on instantiated union types. - function instantiateInstantiableTypes(type: Type, mapper: TypeMapper): Type { - if (type.flags & TypeFlags.Instantiable) { - return instantiateType(type, mapper); - } - if (type.flags & TypeFlags.Union) { - return getUnionType(map((type).types, t => instantiateInstantiableTypes(t, mapper)), UnionReduction.None); - } - if (type.flags & TypeFlags.Intersection) { - return getIntersectionType(map((type).types, t => instantiateInstantiableTypes(t, mapper))); - } - return type; - } - - /** - * Whoa! Do you really want to use this function? - * - * Unless you're trying to get the *non-apparent* type for a - * value-literal type or you're authoring relevant portions of this algorithm, - * you probably meant to use 'getApparentTypeOfContextualType'. - * Otherwise this may not be very useful. - * - * In cases where you *are* working on this function, you should understand - * when it is appropriate to use 'getContextualType' and 'getApparentTypeOfContextualType'. - * - * - Use 'getContextualType' when you are simply going to propagate the result to the expression. - * - Use 'getApparentTypeOfContextualType' when you're going to need the members of the type. - * - * @param node the expression whose contextual type will be returned. - * @returns the contextual type of an expression. - */ - function getContextualType(node: Expression, contextFlags?: ContextFlags): Type | undefined { - if (node.flags & NodeFlags.InWithStatement) { - // We cannot answer semantic questions within a with block, do not proceed any further - return undefined; - } - if (node.contextualType) { - return node.contextualType; - } - const { parent } = node; - switch (parent.kind) { - case SyntaxKind.VariableDeclaration: - case SyntaxKind.Parameter: - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.PropertySignature: - case SyntaxKind.BindingElement: - return getContextualTypeForInitializerExpression(node); - case SyntaxKind.ArrowFunction: - case SyntaxKind.ReturnStatement: - return getContextualTypeForReturnExpression(node); - case SyntaxKind.YieldExpression: - return getContextualTypeForYieldOperand(parent); - case SyntaxKind.AwaitExpression: - return getContextualTypeForAwaitOperand(parent); - case SyntaxKind.CallExpression: - if ((parent).expression.kind === SyntaxKind.ImportKeyword) { - return stringType; - } - /* falls through */ - case SyntaxKind.NewExpression: - return getContextualTypeForArgument(parent, node); - case SyntaxKind.TypeAssertionExpression: - case SyntaxKind.AsExpression: - return isConstTypeReference((parent).type) ? undefined : getTypeFromTypeNode((parent).type); - case SyntaxKind.BinaryExpression: - return getContextualTypeForBinaryOperand(node, contextFlags); - case SyntaxKind.PropertyAssignment: - case SyntaxKind.ShorthandPropertyAssignment: - return getContextualTypeForObjectLiteralElement(parent, contextFlags); - case SyntaxKind.SpreadAssignment: - return getApparentTypeOfContextualType(parent.parent as ObjectLiteralExpression, contextFlags); - case SyntaxKind.ArrayLiteralExpression: { - const arrayLiteral = parent; - const type = getApparentTypeOfContextualType(arrayLiteral, contextFlags); - return getContextualTypeForElementExpression(type, indexOfNode(arrayLiteral.elements, node)); - } - case SyntaxKind.ConditionalExpression: - return getContextualTypeForConditionalOperand(node, contextFlags); - case SyntaxKind.TemplateSpan: - Debug.assert(parent.parent.kind === SyntaxKind.TemplateExpression); - return getContextualTypeForSubstitutionExpression(parent.parent, node); - case SyntaxKind.ParenthesizedExpression: { - // Like in `checkParenthesizedExpression`, an `/** @type {xyz} */` comment before a parenthesized expression acts as a type cast. - const tag = isInJSFile(parent) ? getJSDocTypeTag(parent) : undefined; - return tag ? getTypeFromTypeNode(tag.typeExpression.type) : getContextualType(parent, contextFlags); - } - case SyntaxKind.JsxExpression: - return getContextualTypeForJsxExpression(parent); - case SyntaxKind.JsxAttribute: - case SyntaxKind.JsxSpreadAttribute: - return getContextualTypeForJsxAttribute(parent); - case SyntaxKind.JsxOpeningElement: - case SyntaxKind.JsxSelfClosingElement: - return getContextualJsxElementAttributesType(parent, contextFlags); + return result; + function getInvalidTextualChildDiagnostic() { + if (!invalidTextDiagnostic) { + const tagNameText = getTextOfNode(node.parent.tagName); + const childPropName = getJsxElementChildrenPropertyName(getJsxNamespaceAt(node)); + const childrenPropName = childPropName === undefined ? "children" : unescapeLeadingUnderscores(childPropName); + const childrenTargetType = getIndexedAccessType(target, getLiteralType(childrenPropName)); + const diagnostic = Diagnostics._0_components_don_t_accept_text_as_child_elements_Text_in_JSX_has_the_type_string_but_the_expected_type_of_1_is_2; + invalidTextDiagnostic = { ...diagnostic, key: "!!ALREADY FORMATTED!!", message: formatMessage(/*_dummy*/ undefined, diagnostic, tagNameText, childrenPropName, typeToString(childrenTargetType)) }; } - return undefined; + return invalidTextDiagnostic; } - - function getInferenceContext(node: Node) { - const ancestor = findAncestor(node, n => !!n.inferenceContext); - return ancestor && ancestor.inferenceContext!; + } + function* generateLimitedTupleElements(node: ArrayLiteralExpression, target: ts.Type): ElaborationIterator { + const len = length(node.elements); + if (!len) + return; + for (let i = 0; i < len; i++) { + // Skip elements which do not exist in the target - a length error on the tuple overall is likely better than an error on a mismatched index signature + if (isTupleLikeType(target) && !getPropertyOfType(target, (("" + i) as __String))) + continue; + const elem = node.elements[i]; + if (isOmittedExpression(elem)) + continue; + const nameType = getLiteralType(i); + yield { errorNode: elem, innerExpression: elem, nameType }; } - - function getContextualJsxElementAttributesType(node: JsxOpeningLikeElement, contextFlags?: ContextFlags) { - if (isJsxOpeningElement(node) && node.parent.contextualType && contextFlags !== ContextFlags.Completions) { - // Contextually applied type is moved from attributes up to the outer jsx attributes so when walking up from the children they get hit - // _However_ to hit them from the _attributes_ we must look for them here; otherwise we'll used the declared type - // (as below) instead! - return node.parent.contextualType; + } + function elaborateArrayLiteral(node: ArrayLiteralExpression, source: ts.Type, target: ts.Type, relation: ts.Map, containingMessageChain: (() => DiagnosticMessageChain | undefined) | undefined, errorOutputContainer: { + errors?: Diagnostic[]; + skipLogging?: boolean; + } | undefined) { + if (target.flags & TypeFlags.Primitive) + return false; + if (isTupleLikeType(source)) { + return elaborateElementwise(generateLimitedTupleElements(node, target), source, target, relation, containingMessageChain, errorOutputContainer); + } + // recreate a tuple from the elements, if possible + // Since we're re-doing the expression type, we need to reapply the contextual type + const oldContext = node.contextualType; + node.contextualType = target; + try { + const tupleizedType = checkArrayLiteral(node, CheckMode.Contextual, /*forceTuple*/ true); + node.contextualType = oldContext; + if (isTupleLikeType(tupleizedType)) { + return elaborateElementwise(generateLimitedTupleElements(node, target), tupleizedType, target, relation, containingMessageChain, errorOutputContainer); } - return getContextualTypeForArgumentAtIndex(node, 0); + return false; } - - function getEffectiveFirstArgumentForJsxSignature(signature: Signature, node: JsxOpeningLikeElement) { - return getJsxReferenceKind(node) !== JsxReferenceKind.Component - ? getJsxPropsTypeFromCallSignature(signature, node) - : getJsxPropsTypeFromClassType(signature, node); + finally { + node.contextualType = oldContext; } - - function getJsxPropsTypeFromCallSignature(sig: Signature, context: JsxOpeningLikeElement) { - let propsType = getTypeOfFirstParameterOfSignatureWithFallback(sig, unknownType); - propsType = getJsxManagedAttributesFromLocatedAttributes(context, getJsxNamespaceAt(context), propsType); - const intrinsicAttribs = getJsxType(JsxNames.IntrinsicAttributes, context); - if (intrinsicAttribs !== errorType) { - propsType = intersectTypes(intrinsicAttribs, propsType); + } + function* generateObjectLiteralElements(node: ObjectLiteralExpression): ElaborationIterator { + if (!length(node.properties)) + return; + for (const prop of node.properties) { + if (isSpreadAssignment(prop)) + continue; + const type = getLiteralTypeFromProperty(getSymbolOfNode(prop), TypeFlags.StringOrNumberLiteralOrUnique); + if (!type || (type.flags & TypeFlags.Never)) { + continue; + } + switch (prop.kind) { + case SyntaxKind.SetAccessor: + case SyntaxKind.GetAccessor: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.ShorthandPropertyAssignment: + yield { errorNode: prop.name, innerExpression: undefined, nameType: type }; + break; + case SyntaxKind.PropertyAssignment: + yield { errorNode: prop.name, innerExpression: prop.initializer, nameType: type, errorMessage: isComputedNonLiteralName(prop.name) ? Diagnostics.Type_of_computed_property_s_value_is_0_which_is_not_assignable_to_type_1 : undefined }; + break; + default: + Debug.assertNever(prop); } - return propsType; } - - function getJsxPropsTypeForSignatureFromMember(sig: Signature, forcedLookupLocation: __String) { - if (sig.unionSignatures) { - // JSX Elements using the legacy `props`-field based lookup (eg, react class components) need to treat the `props` member as an input - // instead of an output position when resolving the signature. We need to go back to the input signatures of the composite signature, - // get the type of `props` on each return type individually, and then _intersect them_, rather than union them (as would normally occur - // for a union signature). It's an unfortunate quirk of looking in the output of the signature for the type we want to use for the input. - // The default behavior of `getTypeOfFirstParameterOfSignatureWithFallback` when no `props` member name is defined is much more sane. - const results: Type[] = []; - for (const signature of sig.unionSignatures) { - const instance = getReturnTypeOfSignature(signature); - if (isTypeAny(instance)) { - return instance; - } - const propType = getTypeOfPropertyOfType(instance, forcedLookupLocation); - if (!propType) { - return; + } + function elaborateObjectLiteral(node: ObjectLiteralExpression, source: ts.Type, target: ts.Type, relation: ts.Map, containingMessageChain: (() => DiagnosticMessageChain | undefined) | undefined, errorOutputContainer: { + errors?: Diagnostic[]; + skipLogging?: boolean; + } | undefined) { + if (target.flags & TypeFlags.Primitive) + return false; + return elaborateElementwise(generateObjectLiteralElements(node), source, target, relation, containingMessageChain, errorOutputContainer); + } + /** + * This is *not* a bi-directional relationship. + * If one needs to check both directions for comparability, use a second call to this function or 'isTypeComparableTo'. + */ + function checkTypeComparableTo(source: ts.Type, target: ts.Type, errorNode: Node, headMessage?: DiagnosticMessage, containingMessageChain?: () => DiagnosticMessageChain | undefined): boolean { + return checkTypeRelatedTo(source, target, comparableRelation, errorNode, headMessage, containingMessageChain); + } + function isSignatureAssignableTo(source: ts.Signature, target: ts.Signature, ignoreReturnTypes: boolean): boolean { + return compareSignaturesRelated(source, target, ignoreReturnTypes ? SignatureCheckMode.IgnoreReturnTypes : 0, /*reportErrors*/ false, + /*errorReporter*/ undefined, /*errorReporter*/ undefined, compareTypesAssignable, /*reportUnreliableMarkers*/ undefined) !== Ternary.False; + } + type ErrorReporter = (message: DiagnosticMessage, arg0?: string, arg1?: string) => void; + /** + * Returns true if `s` is `(...args: any[]) => any` or `(this: any, ...args: any[]) => any` + */ + function isAnySignature(s: ts.Signature) { + return !s.typeParameters && (!s.thisParameter || isTypeAny(getTypeOfParameter(s.thisParameter))) && s.parameters.length === 1 && + signatureHasRestParameter(s) && (getTypeOfParameter(s.parameters[0]) === anyArrayType || isTypeAny(getTypeOfParameter(s.parameters[0]))) && + isTypeAny(getReturnTypeOfSignature(s)); + } + /** + * See signatureRelatedTo, compareSignaturesIdentical + */ + function compareSignaturesRelated(source: ts.Signature, target: ts.Signature, checkMode: SignatureCheckMode, reportErrors: boolean, errorReporter: ErrorReporter | undefined, incompatibleErrorReporter: ((source: ts.Type, target: ts.Type) => void) | undefined, compareTypes: TypeComparer, reportUnreliableMarkers: TypeMapper | undefined): Ternary { + // TODO (drosen): De-duplicate code between related functions. + if (source === target) { + return Ternary.True; + } + if (isAnySignature(target)) { + return Ternary.True; + } + const targetCount = getParameterCount(target); + const sourceHasMoreParameters = !hasEffectiveRestParameter(target) && + (checkMode & SignatureCheckMode.StrictArity ? hasEffectiveRestParameter(source) || getParameterCount(source) > targetCount : getMinArgumentCount(source) > targetCount); + if (sourceHasMoreParameters) { + return Ternary.False; + } + if (source.typeParameters && source.typeParameters !== target.typeParameters) { + target = getCanonicalSignature(target); + source = instantiateSignatureInContextOf(source, target, /*inferenceContext*/ undefined, compareTypes); + } + const sourceCount = getParameterCount(source); + const sourceRestType = getNonArrayRestType(source); + const targetRestType = getNonArrayRestType(target); + if (sourceRestType || targetRestType) { + void instantiateType(sourceRestType || targetRestType, reportUnreliableMarkers); + } + if (sourceRestType && targetRestType && sourceCount !== targetCount) { + // We're not able to relate misaligned complex rest parameters + return Ternary.False; + } + const kind = target.declaration ? target.declaration.kind : SyntaxKind.Unknown; + const strictVariance = !(checkMode & SignatureCheckMode.Callback) && strictFunctionTypes && kind !== SyntaxKind.MethodDeclaration && + kind !== SyntaxKind.MethodSignature && kind !== SyntaxKind.Constructor; + let result = Ternary.True; + const sourceThisType = getThisTypeOfSignature(source); + if (sourceThisType && sourceThisType !== voidType) { + const targetThisType = getThisTypeOfSignature(target); + if (targetThisType) { + // void sources are assignable to anything. + const related = !strictVariance && compareTypes(sourceThisType, targetThisType, /*reportErrors*/ false) + || compareTypes(targetThisType, sourceThisType, reportErrors); + if (!related) { + if (reportErrors) { + errorReporter!(Diagnostics.The_this_types_of_each_signature_are_incompatible); } - results.push(propType); + return Ternary.False; } - return getIntersectionType(results); + result &= related; } - const instanceType = getReturnTypeOfSignature(sig); - return isTypeAny(instanceType) ? instanceType : getTypeOfPropertyOfType(instanceType, forcedLookupLocation); } - - function getStaticTypeOfReferencedJsxConstructor(context: JsxOpeningLikeElement) { - if (isJsxIntrinsicIdentifier(context.tagName)) { - const result = getIntrinsicAttributesTypeFromJsxOpeningLikeElement(context); - const fakeSignature = createSignatureForJSXIntrinsic(context, result); - return getOrCreateTypeFromSignature(fakeSignature); - } - const tagType = checkExpressionCached(context.tagName); - if (tagType.flags & TypeFlags.StringLiteral) { - const result = getIntrinsicAttributesTypeFromStringLiteralType(tagType as StringLiteralType, context); - if (!result) { - return errorType; + const paramCount = sourceRestType || targetRestType ? Math.min(sourceCount, targetCount) : Math.max(sourceCount, targetCount); + const restIndex = sourceRestType || targetRestType ? paramCount - 1 : -1; + for (let i = 0; i < paramCount; i++) { + const sourceType = i === restIndex ? getRestTypeAtPosition(source, i) : getTypeAtPosition(source, i); + const targetType = i === restIndex ? getRestTypeAtPosition(target, i) : getTypeAtPosition(target, i); + // In order to ensure that any generic type Foo is at least co-variant with respect to T no matter + // how Foo uses T, we need to relate parameters bi-variantly (given that parameters are input positions, + // they naturally relate only contra-variantly). However, if the source and target parameters both have + // function types with a single call signature, we know we are relating two callback parameters. In + // that case it is sufficient to only relate the parameters of the signatures co-variantly because, + // similar to return values, callback parameters are output positions. This means that a Promise, + // where T is used only in callback parameter positions, will be co-variant (as opposed to bi-variant) + // with respect to T. + const sourceSig = checkMode & SignatureCheckMode.Callback ? undefined : getSingleCallSignature(getNonNullableType(sourceType)); + const targetSig = checkMode & SignatureCheckMode.Callback ? undefined : getSingleCallSignature(getNonNullableType(targetType)); + const callbacks = sourceSig && targetSig && !getTypePredicateOfSignature(sourceSig) && !getTypePredicateOfSignature(targetSig) && + (getFalsyFlags(sourceType) & TypeFlags.Nullable) === (getFalsyFlags(targetType) & TypeFlags.Nullable); + let related = callbacks ? + compareSignaturesRelated(targetSig!, sourceSig!, (checkMode & SignatureCheckMode.StrictArity) | (strictVariance ? SignatureCheckMode.StrictCallback : SignatureCheckMode.BivariantCallback), reportErrors, errorReporter, incompatibleErrorReporter, compareTypes, reportUnreliableMarkers) : + !(checkMode & SignatureCheckMode.Callback) && !strictVariance && compareTypes(sourceType, targetType, /*reportErrors*/ false) || compareTypes(targetType, sourceType, reportErrors); + // With strict arity, (x: number | undefined) => void is a subtype of (x?: number | undefined) => void + if (related && checkMode & SignatureCheckMode.StrictArity && i >= getMinArgumentCount(source) && i < getMinArgumentCount(target) && compareTypes(sourceType, targetType, /*reportErrors*/ false)) { + related = Ternary.False; + } + if (!related) { + if (reportErrors) { + errorReporter!(Diagnostics.Types_of_parameters_0_and_1_are_incompatible, unescapeLeadingUnderscores(getParameterNameAtPosition(source, i)), unescapeLeadingUnderscores(getParameterNameAtPosition(target, i))); } - const fakeSignature = createSignatureForJSXIntrinsic(context, result); - return getOrCreateTypeFromSignature(fakeSignature); + return Ternary.False; } - return tagType; + result &= related; } - - function getJsxManagedAttributesFromLocatedAttributes(context: JsxOpeningLikeElement, ns: Symbol, attributesType: Type) { - const managedSym = getJsxLibraryManagedAttributes(ns); - if (managedSym) { - const declaredManagedType = getDeclaredTypeOfSymbol(managedSym); - const ctorType = getStaticTypeOfReferencedJsxConstructor(context); - if (length((declaredManagedType as GenericType).typeParameters) >= 2) { - const args = fillMissingTypeArguments([ctorType, attributesType], (declaredManagedType as GenericType).typeParameters, 2, isInJSFile(context)); - return createTypeReference((declaredManagedType as GenericType), args); - } - else if (length(declaredManagedType.aliasTypeArguments) >= 2) { - const args = fillMissingTypeArguments([ctorType, attributesType], declaredManagedType.aliasTypeArguments, 2, isInJSFile(context)); - return getTypeAliasInstantiation(declaredManagedType.aliasSymbol!, args); - } + if (!(checkMode & SignatureCheckMode.IgnoreReturnTypes)) { + // If a signature resolution is already in-flight, skip issuing a circularity error + // here and just use the `any` type directly + const targetReturnType = isResolvingReturnTypeOfSignature(target) ? anyType + : target.declaration && isJSConstructor(target.declaration) ? getDeclaredTypeOfClassOrInterface(getMergedSymbol(target.declaration.symbol)) + : getReturnTypeOfSignature(target); + if (targetReturnType === voidType) { + return result; } - return attributesType; - } - - function getJsxPropsTypeFromClassType(sig: Signature, context: JsxOpeningLikeElement) { - const ns = getJsxNamespaceAt(context); - const forcedLookupLocation = getJsxElementPropertiesName(ns); - let attributesType = forcedLookupLocation === undefined - // If there is no type ElementAttributesProperty, return the type of the first parameter of the signature, which should be the props type - ? getTypeOfFirstParameterOfSignatureWithFallback(sig, unknownType) - : forcedLookupLocation === "" - // If there is no e.g. 'props' member in ElementAttributesProperty, use the element class type instead - ? getReturnTypeOfSignature(sig) - // Otherwise get the type of the property on the signature return type - : getJsxPropsTypeForSignatureFromMember(sig, forcedLookupLocation); - - if (!attributesType) { - // There is no property named 'props' on this instance type - if (!!forcedLookupLocation && !!length(context.attributes.properties)) { - error(context, Diagnostics.JSX_element_class_does_not_support_attributes_because_it_does_not_have_a_0_property, unescapeLeadingUnderscores(forcedLookupLocation)); + const sourceReturnType = isResolvingReturnTypeOfSignature(source) ? anyType + : source.declaration && isJSConstructor(source.declaration) ? getDeclaredTypeOfClassOrInterface(getMergedSymbol(source.declaration.symbol)) + : getReturnTypeOfSignature(source); + // The following block preserves behavior forbidding boolean returning functions from being assignable to type guard returning functions + const targetTypePredicate = getTypePredicateOfSignature(target); + if (targetTypePredicate) { + const sourceTypePredicate = getTypePredicateOfSignature(source); + if (sourceTypePredicate) { + result &= compareTypePredicateRelatedTo(sourceTypePredicate, targetTypePredicate, reportErrors, errorReporter, compareTypes); + } + else if (isIdentifierTypePredicate(targetTypePredicate)) { + if (reportErrors) { + errorReporter!(Diagnostics.Signature_0_must_be_a_type_predicate, signatureToString(source)); + } + return Ternary.False; } - return unknownType; - } - - attributesType = getJsxManagedAttributesFromLocatedAttributes(context, ns, attributesType); - - if (isTypeAny(attributesType)) { - // Props is of type 'any' or unknown - return attributesType; } else { - // Normal case -- add in IntrinsicClassElements and IntrinsicElements - let apparentAttributesType = attributesType; - const intrinsicClassAttribs = getJsxType(JsxNames.IntrinsicClassAttributes, context); - if (intrinsicClassAttribs !== errorType) { - const typeParams = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(intrinsicClassAttribs.symbol); - const hostClassType = getReturnTypeOfSignature(sig); - apparentAttributesType = intersectTypes( - typeParams - ? createTypeReference(intrinsicClassAttribs, fillMissingTypeArguments([hostClassType], typeParams, getMinTypeArgumentCount(typeParams), isInJSFile(context))) - : intrinsicClassAttribs, - apparentAttributesType - ); - } - - const intrinsicAttribs = getJsxType(JsxNames.IntrinsicAttributes, context); - if (intrinsicAttribs !== errorType) { - apparentAttributesType = intersectTypes(intrinsicAttribs, apparentAttributesType); + // When relating callback signatures, we still need to relate return types bi-variantly as otherwise + // the containing type wouldn't be co-variant. For example, interface Foo { add(cb: () => T): void } + // wouldn't be co-variant for T without this rule. + result &= checkMode & SignatureCheckMode.BivariantCallback && compareTypes(targetReturnType, sourceReturnType, /*reportErrors*/ false) || + compareTypes(sourceReturnType, targetReturnType, reportErrors); + if (!result && reportErrors && incompatibleErrorReporter) { + incompatibleErrorReporter(sourceReturnType, targetReturnType); } - - return apparentAttributesType; } } - - // If the given type is an object or union type with a single signature, and if that signature has at - // least as many parameters as the given function, return the signature. Otherwise return undefined. - function getContextualCallSignature(type: Type, node: SignatureDeclaration): Signature | undefined { - const signatures = getSignaturesOfType(type, SignatureKind.Call); - if (signatures.length === 1) { - const signature = signatures[0]; - if (!isAritySmaller(signature, node)) { - return signature; - } + return result; + } + function compareTypePredicateRelatedTo(source: TypePredicate, target: TypePredicate, reportErrors: boolean, errorReporter: ErrorReporter | undefined, compareTypes: (s: ts.Type, t: ts.Type, reportErrors?: boolean) => Ternary): Ternary { + if (source.kind !== target.kind) { + if (reportErrors) { + errorReporter!(Diagnostics.A_this_based_type_guard_is_not_compatible_with_a_parameter_based_type_guard); + errorReporter!(Diagnostics.Type_predicate_0_is_not_assignable_to_1, typePredicateToString(source), typePredicateToString(target)); } + return Ternary.False; } - - /** If the contextual signature has fewer parameters than the function expression, do not use it */ - function isAritySmaller(signature: Signature, target: SignatureDeclaration) { - let targetParameterCount = 0; - for (; targetParameterCount < target.parameters.length; targetParameterCount++) { - const param = target.parameters[targetParameterCount]; - if (param.initializer || param.questionToken || param.dotDotDotToken || isJSDocOptionalParameter(param)) { - break; + if (source.kind === TypePredicateKind.Identifier || source.kind === TypePredicateKind.AssertsIdentifier) { + if (source.parameterIndex !== (target as IdentifierTypePredicate).parameterIndex) { + if (reportErrors) { + errorReporter!(Diagnostics.Parameter_0_is_not_in_the_same_position_as_parameter_1, source.parameterName, (target as IdentifierTypePredicate).parameterName); + errorReporter!(Diagnostics.Type_predicate_0_is_not_assignable_to_1, typePredicateToString(source), typePredicateToString(target)); } + return Ternary.False; } - if (target.parameters.length && parameterIsThisKeyword(target.parameters[0])) { - targetParameterCount--; - } - return !hasEffectiveRestParameter(signature) && getParameterCount(signature) < targetParameterCount; } - - function isFunctionExpressionOrArrowFunction(node: Node): node is FunctionExpression | ArrowFunction { - return node.kind === SyntaxKind.FunctionExpression || node.kind === SyntaxKind.ArrowFunction; - } - - function getContextualSignatureForFunctionLikeDeclaration(node: FunctionLikeDeclaration): Signature | undefined { - // Only function expressions, arrow functions, and object literal methods are contextually typed. - return isFunctionExpressionOrArrowFunction(node) || isObjectLiteralMethod(node) - ? getContextualSignature(node) - : undefined; + const related = source.type === target.type ? Ternary.True : + source.type && target.type ? compareTypes(source.type, target.type, reportErrors) : + Ternary.False; + if (related === Ternary.False && reportErrors) { + errorReporter!(Diagnostics.Type_predicate_0_is_not_assignable_to_1, typePredicateToString(source), typePredicateToString(target)); } - - // Return the contextual signature for a given expression node. A contextual type provides a - // contextual signature if it has a single call signature and if that call signature is non-generic. - // If the contextual type is a union type, get the signature from each type possible and if they are - // all identical ignoring their return type, the result is same signature but with return type as - // union type of return types from these signatures - function getContextualSignature(node: FunctionExpression | ArrowFunction | MethodDeclaration): Signature | undefined { - Debug.assert(node.kind !== SyntaxKind.MethodDeclaration || isObjectLiteralMethod(node)); - const typeTagSignature = getSignatureOfTypeTag(node); - if (typeTagSignature) { - return typeTagSignature; - } - const type = getApparentTypeOfContextualType(node, ContextFlags.Signature); - if (!type) { - return undefined; - } - if (!(type.flags & TypeFlags.Union)) { - return getContextualCallSignature(type, node); - } - let signatureList: Signature[] | undefined; - const types = (type).types; - for (const current of types) { - const signature = getContextualCallSignature(current, node); - if (signature) { - if (!signatureList) { - // This signature will contribute to contextual union signature - signatureList = [signature]; - } - else if (!compareSignaturesIdentical(signatureList[0], signature, /*partialMatch*/ false, /*ignoreThisTypes*/ true, /*ignoreReturnTypes*/ true, compareTypesIdentical)) { - // Signatures aren't identical, do not use - return undefined; - } - else { - // Use this signature for contextual union signature - signatureList.push(signature); - } - } - } - // Result is union of signatures collected (return type is union of return types of this signature set) - if (signatureList) { - return signatureList.length === 1 ? signatureList[0] : createUnionSignature(signatureList[0], signatureList); - } + return related; + } + function isImplementationCompatibleWithOverload(implementation: ts.Signature, overload: ts.Signature): boolean { + const erasedSource = getErasedSignature(implementation); + const erasedTarget = getErasedSignature(overload); + // First see if the return types are compatible in either direction. + const sourceReturnType = getReturnTypeOfSignature(erasedSource); + const targetReturnType = getReturnTypeOfSignature(erasedTarget); + if (targetReturnType === voidType + || isTypeRelatedTo(targetReturnType, sourceReturnType, assignableRelation) + || isTypeRelatedTo(sourceReturnType, targetReturnType, assignableRelation)) { + return isSignatureAssignableTo(erasedSource, erasedTarget, /*ignoreReturnTypes*/ true); + } + return false; + } + function isEmptyResolvedType(t: ResolvedType) { + return t !== anyFunctionType && + t.properties.length === 0 && + t.callSignatures.length === 0 && + t.constructSignatures.length === 0 && + !t.stringIndexInfo && + !t.numberIndexInfo; + } + function isEmptyObjectType(type: ts.Type): boolean { + return type.flags & TypeFlags.Object ? !isGenericMappedType(type) && isEmptyResolvedType(resolveStructuredTypeMembers((type))) : + type.flags & TypeFlags.NonPrimitive ? true : + type.flags & TypeFlags.Union ? some((type).types, isEmptyObjectType) : + type.flags & TypeFlags.Intersection ? every((type).types, isEmptyObjectType) : + false; + } + function isEmptyAnonymousObjectType(type: ts.Type) { + return !!(getObjectFlags(type) & ObjectFlags.Anonymous) && isEmptyObjectType(type); + } + function isStringIndexSignatureOnlyType(type: ts.Type): boolean { + return type.flags & TypeFlags.Object && !isGenericMappedType(type) && getPropertiesOfType(type).length === 0 && getIndexInfoOfType(type, IndexKind.String) && !getIndexInfoOfType(type, IndexKind.Number) || + type.flags & TypeFlags.UnionOrIntersection && every((type).types, isStringIndexSignatureOnlyType) || + false; + } + function isEnumTypeRelatedTo(sourceSymbol: ts.Symbol, targetSymbol: ts.Symbol, errorReporter?: ErrorReporter) { + if (sourceSymbol === targetSymbol) { + return true; } - - function checkSpreadExpression(node: SpreadElement, checkMode?: CheckMode): Type { - if (languageVersion < ScriptTarget.ES2015) { - checkExternalEmitHelpers(node, compilerOptions.downlevelIteration ? ExternalEmitHelpers.SpreadIncludes : ExternalEmitHelpers.SpreadArrays); - } - - const arrayOrIterableType = checkExpression(node.expression, checkMode); - return checkIteratedTypeOrElementType(IterationUse.Spread, arrayOrIterableType, undefinedType, node.expression); + const id = getSymbolId(sourceSymbol) + "," + getSymbolId(targetSymbol); + const entry = enumRelation.get(id); + if (entry !== undefined && !(!(entry & RelationComparisonResult.Reported) && entry & RelationComparisonResult.Failed && errorReporter)) { + return !!(entry & RelationComparisonResult.Succeeded); } - - function hasDefaultValue(node: BindingElement | Expression): boolean { - return (node.kind === SyntaxKind.BindingElement && !!(node).initializer) || - (node.kind === SyntaxKind.BinaryExpression && (node).operatorToken.kind === SyntaxKind.EqualsToken); + if (sourceSymbol.escapedName !== targetSymbol.escapedName || !(sourceSymbol.flags & SymbolFlags.RegularEnum) || !(targetSymbol.flags & SymbolFlags.RegularEnum)) { + enumRelation.set(id, RelationComparisonResult.Failed | RelationComparisonResult.Reported); + return false; } - - function checkArrayLiteral(node: ArrayLiteralExpression, checkMode: CheckMode | undefined, forceTuple: boolean | undefined): Type { - const elements = node.elements; - const elementCount = elements.length; - const elementTypes: Type[] = []; - let hasEndingSpreadElement = false; - let hasNonEndingSpreadElement = false; - const contextualType = getApparentTypeOfContextualType(node); - const inDestructuringPattern = isAssignmentTarget(node); - const inConstContext = isConstContext(node); - for (let i = 0; i < elementCount; i++) { - const e = elements[i]; - const spread = e.kind === SyntaxKind.SpreadElement && (e).expression; - const spreadType = spread && checkExpression(spread, checkMode, forceTuple); - if (spreadType && isTupleType(spreadType)) { - elementTypes.push(...getTypeArguments(spreadType)); - if (spreadType.target.hasRestElement) { - if (i === elementCount - 1) hasEndingSpreadElement = true; - else hasNonEndingSpreadElement = true; - } - } - else { - if (inDestructuringPattern && spreadType) { - // Given the following situation: - // var c: {}; - // [...c] = ["", 0]; - // - // c is represented in the tree as a spread element in an array literal. - // But c really functions as a rest element, and its purpose is to provide - // a contextual type for the right hand side of the assignment. Therefore, - // instead of calling checkExpression on "...c", which will give an error - // if c is not iterable/array-like, we need to act as if we are trying to - // get the contextual element type from it. So we do something similar to - // getContextualTypeForElementExpression, which will crucially not error - // if there is no index type / iterated type. - const restElementType = getIndexTypeOfType(spreadType, IndexKind.Number) || - getIteratedTypeOrElementType(IterationUse.Destructuring, spreadType, undefinedType, /*errorNode*/ undefined, /*checkAssignability*/ false); - if (restElementType) { - elementTypes.push(restElementType); - } + const targetEnumType = getTypeOfSymbol(targetSymbol); + for (const property of getPropertiesOfType(getTypeOfSymbol(sourceSymbol))) { + if (property.flags & SymbolFlags.EnumMember) { + const targetProperty = getPropertyOfType(targetEnumType, property.escapedName); + if (!targetProperty || !(targetProperty.flags & SymbolFlags.EnumMember)) { + if (errorReporter) { + errorReporter(Diagnostics.Property_0_is_missing_in_type_1, symbolName(property), typeToString(getDeclaredTypeOfSymbol(targetSymbol), /*enclosingDeclaration*/ undefined, TypeFormatFlags.UseFullyQualifiedType)); + enumRelation.set(id, RelationComparisonResult.Failed | RelationComparisonResult.Reported); } else { - const elementContextualType = getContextualTypeForElementExpression(contextualType, elementTypes.length); - const type = checkExpressionForMutableLocation(e, checkMode, elementContextualType, forceTuple); - elementTypes.push(type); + enumRelation.set(id, RelationComparisonResult.Failed); } - if (spread) { // tuples are done above, so these are only arrays - if (i === elementCount - 1) hasEndingSpreadElement = true; - else hasNonEndingSpreadElement = true; - } - } - } - if (!hasNonEndingSpreadElement) { - const minLength = elementTypes.length - (hasEndingSpreadElement ? 1 : 0); - // If array literal is actually a destructuring pattern, mark it as an implied type. We do this such - // that we get the same behavior for "var [x, y] = []" and "[x, y] = []". - let tupleResult; - if (inDestructuringPattern && minLength > 0) { - const type = cloneTypeReference(createTupleType(elementTypes, minLength, hasEndingSpreadElement)); - type.pattern = node; - return type; - } - else if (tupleResult = getArrayLiteralTupleTypeIfApplicable(elementTypes, contextualType, hasEndingSpreadElement, elementTypes.length, inConstContext)) { - return createArrayLiteralType(tupleResult); - } - else if (forceTuple) { - return createArrayLiteralType(createTupleType(elementTypes, minLength, hasEndingSpreadElement)); + return false; } } - return createArrayLiteralType(createArrayType(elementTypes.length ? - getUnionType(elementTypes, UnionReduction.Subtype) : - strictNullChecks ? implicitNeverType : undefinedWideningType, inConstContext)); - } - - function createArrayLiteralType(type: ObjectType) { - if (!(getObjectFlags(type) & ObjectFlags.Reference)) { - return type; - } - let literalType = (type).literalType; - if (!literalType) { - literalType = (type).literalType = cloneTypeReference(type); - literalType.objectFlags |= ObjectFlags.ArrayLiteral | ObjectFlags.ContainsObjectOrArrayLiteral; - } - return literalType; } - - function getArrayLiteralTupleTypeIfApplicable(elementTypes: Type[], contextualType: Type | undefined, hasRestElement: boolean, elementCount = elementTypes.length, readonly = false) { - // Infer a tuple type when the contextual type is or contains a tuple-like type - if (readonly || (contextualType && forEachType(contextualType, isTupleLikeType))) { - return createTupleType(elementTypes, elementCount - (hasRestElement ? 1 : 0), hasRestElement, readonly); - } + enumRelation.set(id, RelationComparisonResult.Succeeded); + return true; + } + function isSimpleTypeRelatedTo(source: ts.Type, target: ts.Type, relation: ts.Map, errorReporter?: ErrorReporter) { + const s = source.flags; + const t = target.flags; + if (t & TypeFlags.AnyOrUnknown || s & TypeFlags.Never || source === wildcardType) + return true; + if (t & TypeFlags.Never) + return false; + if (s & TypeFlags.StringLike && t & TypeFlags.String) + return true; + if (s & TypeFlags.StringLiteral && s & TypeFlags.EnumLiteral && + t & TypeFlags.StringLiteral && !(t & TypeFlags.EnumLiteral) && + (source).value === (target).value) + return true; + if (s & TypeFlags.NumberLike && t & TypeFlags.Number) + return true; + if (s & TypeFlags.NumberLiteral && s & TypeFlags.EnumLiteral && + t & TypeFlags.NumberLiteral && !(t & TypeFlags.EnumLiteral) && + (source).value === (target).value) + return true; + if (s & TypeFlags.BigIntLike && t & TypeFlags.BigInt) + return true; + if (s & TypeFlags.BooleanLike && t & TypeFlags.Boolean) + return true; + if (s & TypeFlags.ESSymbolLike && t & TypeFlags.ESSymbol) + return true; + if (s & TypeFlags.Enum && t & TypeFlags.Enum && isEnumTypeRelatedTo(source.symbol, target.symbol, errorReporter)) + return true; + if (s & TypeFlags.EnumLiteral && t & TypeFlags.EnumLiteral) { + if (s & TypeFlags.Union && t & TypeFlags.Union && isEnumTypeRelatedTo(source.symbol, target.symbol, errorReporter)) + return true; + if (s & TypeFlags.Literal && t & TypeFlags.Literal && + (source).value === (target).value && + isEnumTypeRelatedTo(getParentOfSymbol(source.symbol)!, getParentOfSymbol(target.symbol)!, errorReporter)) + return true; } - - function isNumericName(name: DeclarationName): boolean { - switch (name.kind) { - case SyntaxKind.ComputedPropertyName: - return isNumericComputedName(name); - case SyntaxKind.Identifier: - return isNumericLiteralName(name.escapedText); - case SyntaxKind.NumericLiteral: - case SyntaxKind.StringLiteral: - return isNumericLiteralName(name.text); - default: - return false; - } + if (s & TypeFlags.Undefined && (!strictNullChecks || t & (TypeFlags.Undefined | TypeFlags.Void))) + return true; + if (s & TypeFlags.Null && (!strictNullChecks || t & TypeFlags.Null)) + return true; + if (s & TypeFlags.Object && t & TypeFlags.NonPrimitive) + return true; + if (relation === assignableRelation || relation === comparableRelation) { + if (s & TypeFlags.Any) + return true; + // Type number or any numeric literal type is assignable to any numeric enum type or any + // numeric enum literal type. This rule exists for backwards compatibility reasons because + // bit-flag enum types sometimes look like literal enum types with numeric literal values. + if (s & (TypeFlags.Number | TypeFlags.NumberLiteral) && !(s & TypeFlags.EnumLiteral) && (t & TypeFlags.Enum || t & TypeFlags.NumberLiteral && t & TypeFlags.EnumLiteral)) + return true; } - - function isNumericComputedName(name: ComputedPropertyName): boolean { - // It seems odd to consider an expression of type Any to result in a numeric name, - // but this behavior is consistent with checkIndexedAccess - return isTypeAssignableToKind(checkComputedPropertyName(name), TypeFlags.NumberLike); + return false; + } + function isTypeRelatedTo(source: ts.Type, target: ts.Type, relation: ts.Map) { + if (isFreshLiteralType(source)) { + source = (source).regularType; } - - function isInfinityOrNaNString(name: string | __String): boolean { - return name === "Infinity" || name === "-Infinity" || name === "NaN"; + if (isFreshLiteralType(target)) { + target = (target).regularType; } - - function isNumericLiteralName(name: string | __String) { - // The intent of numeric names is that - // - they are names with text in a numeric form, and that - // - setting properties/indexing with them is always equivalent to doing so with the numeric literal 'numLit', - // acquired by applying the abstract 'ToNumber' operation on the name's text. - // - // The subtlety is in the latter portion, as we cannot reliably say that anything that looks like a numeric literal is a numeric name. - // In fact, it is the case that the text of the name must be equal to 'ToString(numLit)' for this to hold. - // - // Consider the property name '"0xF00D"'. When one indexes with '0xF00D', they are actually indexing with the value of 'ToString(0xF00D)' - // according to the ECMAScript specification, so it is actually as if the user indexed with the string '"61453"'. - // Thus, the text of all numeric literals equivalent to '61543' such as '0xF00D', '0xf00D', '0170015', etc. are not valid numeric names - // because their 'ToString' representation is not equal to their original text. - // This is motivated by ECMA-262 sections 9.3.1, 9.8.1, 11.1.5, and 11.2.1. - // - // Here, we test whether 'ToString(ToNumber(name))' is exactly equal to 'name'. - // The '+' prefix operator is equivalent here to applying the abstract ToNumber operation. - // Applying the 'toString()' method on a number gives us the abstract ToString operation on a number. - // - // Note that this accepts the values 'Infinity', '-Infinity', and 'NaN', and that this is intentional. - // This is desired behavior, because when indexing with them as numeric entities, you are indexing - // with the strings '"Infinity"', '"-Infinity"', and '"NaN"' respectively. - return (+name).toString() === name; + if (source === target) { + return true; } - - function checkComputedPropertyName(node: ComputedPropertyName): Type { - const links = getNodeLinks(node.expression); - if (!links.resolvedType) { - links.resolvedType = checkExpression(node.expression); - // This will allow types number, string, symbol or any. It will also allow enums, the unknown - // type, and any union of these types (like string | number). - if (links.resolvedType.flags & TypeFlags.Nullable || - !isTypeAssignableToKind(links.resolvedType, TypeFlags.StringLike | TypeFlags.NumberLike | TypeFlags.ESSymbolLike) && - !isTypeAssignableTo(links.resolvedType, stringNumberSymbolType)) { - error(node, Diagnostics.A_computed_property_name_must_be_of_type_string_number_symbol_or_any); - } - else { - checkThatExpressionIsProperSymbolReference(node.expression, links.resolvedType, /*reportError*/ true); - } + if (relation !== identityRelation) { + if (relation === comparableRelation && !(target.flags & TypeFlags.Never) && isSimpleTypeRelatedTo(target, source, relation) || isSimpleTypeRelatedTo(source, target, relation)) { + return true; } - - return links.resolvedType; } - - function getObjectLiteralIndexInfo(node: ObjectLiteralExpression, offset: number, properties: Symbol[], kind: IndexKind): IndexInfo { - const propTypes: Type[] = []; - for (let i = 0; i < properties.length; i++) { - if (kind === IndexKind.String || isNumericName(node.properties[i + offset].name!)) { - propTypes.push(getTypeOfSymbol(properties[i])); - } + else { + if (!(source.flags === target.flags && source.flags & TypeFlags.Substructure)) + return false; + } + if (source.flags & TypeFlags.Object && target.flags & TypeFlags.Object) { + const related = relation.get(getRelationKey(source, target, IntersectionState.None, relation)); + if (related !== undefined) { + return !!(related & RelationComparisonResult.Succeeded); } - const unionType = propTypes.length ? getUnionType(propTypes, UnionReduction.Subtype) : undefinedType; - return createIndexInfo(unionType, isConstContext(node)); } - - function getImmediateAliasedSymbol(symbol: Symbol): Symbol | undefined { - Debug.assert((symbol.flags & SymbolFlags.Alias) !== 0, "Should only get Alias here."); - const links = getSymbolLinks(symbol); - if (!links.immediateTarget) { - const node = getDeclarationOfAliasSymbol(symbol); - if (!node) return Debug.fail(); - links.immediateTarget = getTargetOfAliasDeclaration(node, /*dontRecursivelyResolve*/ true); + if (source.flags & TypeFlags.StructuredOrInstantiable || target.flags & TypeFlags.StructuredOrInstantiable) { + return checkTypeRelatedTo(source, target, relation, /*errorNode*/ undefined); + } + return false; + } + function isIgnoredJsxProperty(source: ts.Type, sourceProp: ts.Symbol) { + return getObjectFlags(source) & ObjectFlags.JsxAttributes && !isUnhyphenatedJsxName(sourceProp.escapedName); + } + function getNormalizedType(type: ts.Type, writing: boolean): ts.Type { + do { + const t = isFreshLiteralType(type) ? (type).regularType : + getObjectFlags(type) & ObjectFlags.Reference && (type).node ? createTypeReference((type).target, getTypeArguments((type))) : + type.flags & TypeFlags.Substitution ? writing ? (type).typeVariable : (type).substitute : + type.flags & TypeFlags.Simplifiable ? getSimplifiedType(type, writing) : + type; + if (t === type) + break; + type = t; + } while (true); + return type; + } + /** + * Checks if 'source' is related to 'target' (e.g.: is a assignable to). + * @param source The left-hand-side of the relation. + * @param target The right-hand-side of the relation. + * @param relation The relation considered. One of 'identityRelation', 'subtypeRelation', 'assignableRelation', or 'comparableRelation'. + * Used as both to determine which checks are performed and as a cache of previously computed results. + * @param errorNode The suggested node upon which all errors will be reported, if defined. This may or may not be the actual node used. + * @param headMessage If the error chain should be prepended by a head message, then headMessage will be used. + * @param containingMessageChain A chain of errors to prepend any new errors found. + * @param errorOutputContainer Return the diagnostic. Do not log if 'skipLogging' is truthy. + */ + function checkTypeRelatedTo(source: ts.Type, target: ts.Type, relation: ts.Map, errorNode: Node | undefined, headMessage?: DiagnosticMessage, containingMessageChain?: () => DiagnosticMessageChain | undefined, errorOutputContainer?: { + errors?: Diagnostic[]; + skipLogging?: boolean; + }): boolean { + let errorInfo: DiagnosticMessageChain | undefined; + let relatedInfo: [DiagnosticRelatedInformation, ...DiagnosticRelatedInformation[]] | undefined; + let maybeKeys: string[]; + let sourceStack: ts.Type[]; + let targetStack: ts.Type[]; + let maybeCount = 0; + let depth = 0; + let expandingFlags = ExpandingFlags.None; + let overflow = false; + let overrideNextErrorInfo = 0; // How many `reportRelationError` calls should be skipped in the elaboration pyramid + let lastSkippedInfo: [ts.Type, ts.Type] | undefined; + let incompatibleStack: [DiagnosticMessage, (string | number)?, (string | number)?, (string | number)?, (string | number)?][] = []; + Debug.assert(relation !== identityRelation || !errorNode, "no error reporting in identity checking"); + const result = isRelatedTo(source, target, /*reportErrors*/ !!errorNode, headMessage); + if (incompatibleStack.length) { + reportIncompatibleStack(); + } + if (overflow) { + const diag = error(errorNode, Diagnostics.Excessive_stack_depth_comparing_types_0_and_1, typeToString(source), typeToString(target)); + if (errorOutputContainer) { + (errorOutputContainer.errors || (errorOutputContainer.errors = [])).push(diag); + } + } + else if (errorInfo) { + if (containingMessageChain) { + const chain = containingMessageChain(); + if (chain) { + concatenateDiagnosticMessageChains(chain, errorInfo); + errorInfo = chain; + } + } + let relatedInformation: DiagnosticRelatedInformation[] | undefined; + // Check if we should issue an extra diagnostic to produce a quickfix for a slightly incorrect import statement + if (headMessage && errorNode && !result && source.symbol) { + const links = getSymbolLinks(source.symbol); + if (links.originatingImport && !isImportCall(links.originatingImport)) { + const helpfulRetry = checkTypeRelatedTo(getTypeOfSymbol(links.target!), target, relation, /*errorNode*/ undefined); + if (helpfulRetry) { + // Likely an incorrect import. Issue a helpful diagnostic to produce a quickfix to change the import + const diag = createDiagnosticForNode(links.originatingImport, Diagnostics.Type_originates_at_this_import_A_namespace_style_import_cannot_be_called_or_constructed_and_will_cause_a_failure_at_runtime_Consider_using_a_default_import_or_import_require_here_instead); + relatedInformation = append(relatedInformation, diag); // Cause the error to appear with the error that triggered it + } + } + } + const diag = createDiagnosticForNodeFromMessageChain((errorNode!), errorInfo, relatedInformation); + if (relatedInfo) { + addRelatedInfo(diag, ...relatedInfo); + } + if (errorOutputContainer) { + (errorOutputContainer.errors || (errorOutputContainer.errors = [])).push(diag); + } + if (!errorOutputContainer || !errorOutputContainer.skipLogging) { + diagnostics.add(diag); } - - return links.immediateTarget; } - - function checkObjectLiteral(node: ObjectLiteralExpression, checkMode?: CheckMode): Type { - const inDestructuringPattern = isAssignmentTarget(node); - // Grammar checking - checkGrammarObjectLiteralExpression(node, inDestructuringPattern); - - let propertiesTable: SymbolTable; - let propertiesArray: Symbol[] = []; - let spread: Type = emptyObjectType; - - const contextualType = getApparentTypeOfContextualType(node); - const contextualTypeHasPattern = contextualType && contextualType.pattern && - (contextualType.pattern.kind === SyntaxKind.ObjectBindingPattern || contextualType.pattern.kind === SyntaxKind.ObjectLiteralExpression); - const inConstContext = isConstContext(node); - const checkFlags = inConstContext ? CheckFlags.Readonly : 0; - const isInJavascript = isInJSFile(node) && !isInJsonFile(node); - const enumTag = getJSDocEnumTag(node); - const isJSObjectLiteral = !contextualType && isInJavascript && !enumTag; - let objectFlags: ObjectFlags = freshObjectLiteralFlag; - let patternWithComputedProperties = false; - let hasComputedStringProperty = false; - let hasComputedNumberProperty = false; - propertiesTable = createSymbolTable(); - - let offset = 0; - for (let i = 0; i < node.properties.length; i++) { - const memberDecl = node.properties[i]; - let member = getSymbolOfNode(memberDecl); - const computedNameType = memberDecl.name && memberDecl.name.kind === SyntaxKind.ComputedPropertyName && !isWellKnownSymbolSyntactically(memberDecl.name.expression) ? - checkComputedPropertyName(memberDecl.name) : undefined; - if (memberDecl.kind === SyntaxKind.PropertyAssignment || - memberDecl.kind === SyntaxKind.ShorthandPropertyAssignment || - isObjectLiteralMethod(memberDecl)) { - let type = memberDecl.kind === SyntaxKind.PropertyAssignment ? checkPropertyAssignment(memberDecl, checkMode) : - memberDecl.kind === SyntaxKind.ShorthandPropertyAssignment ? checkExpressionForMutableLocation(memberDecl.name, checkMode) : - checkObjectLiteralMethod(memberDecl, checkMode); - if (isInJavascript) { - const jsDocType = getTypeForDeclarationFromJSDocComment(memberDecl); - if (jsDocType) { - checkTypeAssignableTo(type, jsDocType, memberDecl); - type = jsDocType; - } - else if (enumTag && enumTag.typeExpression) { - checkTypeAssignableTo(type, getTypeFromTypeNode(enumTag.typeExpression), memberDecl); - } - } - objectFlags |= getObjectFlags(type) & ObjectFlags.PropagatingFlags; - const nameType = computedNameType && isTypeUsableAsPropertyName(computedNameType) ? computedNameType : undefined; - const prop = nameType ? - createSymbol(SymbolFlags.Property | member.flags, getPropertyNameFromType(nameType), checkFlags | CheckFlags.Late) : - createSymbol(SymbolFlags.Property | member.flags, member.escapedName, checkFlags); - if (nameType) { - prop.nameType = nameType; - } - - if (inDestructuringPattern) { - // If object literal is an assignment pattern and if the assignment pattern specifies a default value - // for the property, make the property optional. - const isOptional = - (memberDecl.kind === SyntaxKind.PropertyAssignment && hasDefaultValue(memberDecl.initializer)) || - (memberDecl.kind === SyntaxKind.ShorthandPropertyAssignment && memberDecl.objectAssignmentInitializer); - if (isOptional) { - prop.flags |= SymbolFlags.Optional; - } - } - else if (contextualTypeHasPattern && !(getObjectFlags(contextualType!) & ObjectFlags.ObjectLiteralPatternWithComputedProperties)) { - // If object literal is contextually typed by the implied type of a binding pattern, and if the - // binding pattern specifies a default value for the property, make the property optional. - const impliedProp = getPropertyOfType(contextualType!, member.escapedName); - if (impliedProp) { - prop.flags |= impliedProp.flags & SymbolFlags.Optional; - } - - else if (!compilerOptions.suppressExcessPropertyErrors && !getIndexInfoOfType(contextualType!, IndexKind.String)) { - error(memberDecl.name, Diagnostics.Object_literal_may_only_specify_known_properties_and_0_does_not_exist_in_type_1, - symbolToString(member), typeToString(contextualType!)); + if (errorNode && errorOutputContainer && errorOutputContainer.skipLogging && result === Ternary.False) { + Debug.assert(!!errorOutputContainer.errors, "missed opportunity to interact with error."); + } + return result !== Ternary.False; + function resetErrorInfo(saved: ReturnType) { + errorInfo = saved.errorInfo; + lastSkippedInfo = saved.lastSkippedInfo; + incompatibleStack = saved.incompatibleStack; + overrideNextErrorInfo = saved.overrideNextErrorInfo; + relatedInfo = saved.relatedInfo; + } + function captureErrorCalculationState() { + return { + errorInfo, + lastSkippedInfo, + incompatibleStack: incompatibleStack.slice(), + overrideNextErrorInfo, + relatedInfo: !relatedInfo ? undefined : relatedInfo.slice() as ([DiagnosticRelatedInformation, ...DiagnosticRelatedInformation[]] | undefined) + }; + } + function reportIncompatibleError(message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number) { + overrideNextErrorInfo++; // Suppress the next relation error + lastSkippedInfo = undefined; // Reset skipped info cache + incompatibleStack.push([message, arg0, arg1, arg2, arg3]); + } + function reportIncompatibleStack() { + const stack = incompatibleStack; + incompatibleStack = []; + const info = lastSkippedInfo; + lastSkippedInfo = undefined; + if (stack.length === 1) { + reportError(...stack[0]); + if (info) { + // Actually do the last relation error + reportRelationError(/*headMessage*/ undefined, ...info); + } + return; + } + // The first error will be the innermost, while the last will be the outermost - so by popping off the end, + // we can build from left to right + let path = ""; + const secondaryRootErrors: typeof incompatibleStack = []; + while (stack.length) { + const [msg, ...args] = stack.pop()!; + switch (msg.code) { + case Diagnostics.Types_of_property_0_are_incompatible.code: { + // Parenthesize a `new` if there is one + if (path.indexOf("new ") === 0) { + path = `(${path})`; + } + const str = "" + args[0]; + // If leading, just print back the arg (irrespective of if it's a valid identifier) + if (path.length === 0) { + path = `${str}`; + } + // Otherwise write a dotted name if possible + else if (isIdentifierText(str, compilerOptions.target)) { + path = `${path}.${str}`; + } + // Failing that, check if the name is already a computed name + else if (str[0] === "[" && str[str.length - 1] === "]") { + path = `${path}${str}`; + } + // And finally write out a computed name as a last resort + else { + path = `${path}[${str}]`; } + break; } - - prop.declarations = member.declarations; - prop.parent = member.parent; - if (member.valueDeclaration) { - prop.valueDeclaration = member.valueDeclaration; - } - - prop.type = type; - prop.target = member; - member = prop; - } - else if (memberDecl.kind === SyntaxKind.SpreadAssignment) { - if (languageVersion < ScriptTarget.ES2015) { - checkExternalEmitHelpers(memberDecl, ExternalEmitHelpers.Assign); - } - if (propertiesArray.length > 0) { - spread = getSpreadType(spread, createObjectLiteralType(), node.symbol, objectFlags, inConstContext); - propertiesArray = []; - propertiesTable = createSymbolTable(); - hasComputedStringProperty = false; - hasComputedNumberProperty = false; - } - const type = checkExpression(memberDecl.expression); - if (!isValidSpreadType(type)) { - error(memberDecl, Diagnostics.Spread_types_may_only_be_created_from_object_types); - return errorType; - } - spread = getSpreadType(spread, type, node.symbol, objectFlags, inConstContext); - offset = i + 1; - continue; - } - else { - // TypeScript 1.0 spec (April 2014) - // A get accessor declaration is processed in the same manner as - // an ordinary function declaration(section 6.1) with no parameters. - // A set accessor declaration is processed in the same manner - // as an ordinary function declaration with a single parameter and a Void return type. - Debug.assert(memberDecl.kind === SyntaxKind.GetAccessor || memberDecl.kind === SyntaxKind.SetAccessor); - checkNodeDeferred(memberDecl); - } - - if (computedNameType && !(computedNameType.flags & TypeFlags.StringOrNumberLiteralOrUnique)) { - if (isTypeAssignableTo(computedNameType, stringNumberSymbolType)) { - if (isTypeAssignableTo(computedNameType, numberType)) { - hasComputedNumberProperty = true; + case Diagnostics.Call_signature_return_types_0_and_1_are_incompatible.code: + case Diagnostics.Construct_signature_return_types_0_and_1_are_incompatible.code: + case Diagnostics.Call_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code: + case Diagnostics.Construct_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code: { + if (path.length === 0) { + // Don't flatten signature compatability errors at the start of a chain - instead prefer + // to unify (the with no arguments bit is excessive for printback) and print them back + let mappedMsg = msg; + if (msg.code === Diagnostics.Call_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code) { + mappedMsg = Diagnostics.Call_signature_return_types_0_and_1_are_incompatible; + } + else if (msg.code === Diagnostics.Construct_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code) { + mappedMsg = Diagnostics.Construct_signature_return_types_0_and_1_are_incompatible; + } + secondaryRootErrors.unshift([mappedMsg, args[0], args[1]]); } else { - hasComputedStringProperty = true; - } - if (inDestructuringPattern) { - patternWithComputedProperties = true; + const prefix = (msg.code === Diagnostics.Construct_signature_return_types_0_and_1_are_incompatible.code || + msg.code === Diagnostics.Construct_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code) + ? "new " + : ""; + const params = (msg.code === Diagnostics.Call_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code || + msg.code === Diagnostics.Construct_signatures_with_no_arguments_have_incompatible_return_types_0_and_1.code) + ? "" + : "..."; + path = `${prefix}${path}(${params})`; } + break; } + default: + return Debug.fail(`Unhandled Diagnostic: ${msg.code}`); } - else { - propertiesTable.set(member.escapedName, member); - } - propertiesArray.push(member); } - - // If object literal is contextually typed by the implied type of a binding pattern, augment the result - // type with those properties for which the binding pattern specifies a default value. - // If the object literal is spread into another object literal, skip this step and let the top-level object - // literal handle it instead. - if (contextualTypeHasPattern && node.parent.kind !== SyntaxKind.SpreadAssignment) { - for (const prop of getPropertiesOfType(contextualType!)) { - if (!propertiesTable.get(prop.escapedName) && !getPropertyOfType(spread, prop.escapedName)) { - if (!(prop.flags & SymbolFlags.Optional)) { - error(prop.valueDeclaration || (prop).bindingElement, - Diagnostics.Initializer_provides_no_value_for_this_binding_element_and_the_binding_element_has_no_default_value); - } - propertiesTable.set(prop.escapedName, prop); - propertiesArray.push(prop); - } - } + if (path) { + reportError(path[path.length - 1] === ")" + ? Diagnostics.The_types_returned_by_0_are_incompatible_between_these_types + : Diagnostics.The_types_of_0_are_incompatible_between_these_types, path); } - - if (spread !== emptyObjectType) { - if (propertiesArray.length > 0) { - spread = getSpreadType(spread, createObjectLiteralType(), node.symbol, objectFlags, inConstContext); - propertiesArray = []; - propertiesTable = createSymbolTable(); - hasComputedStringProperty = false; - hasComputedNumberProperty = false; - } - // remap the raw emptyObjectType fed in at the top into a fresh empty object literal type, unique to this use site - return mapType(spread, t => t === emptyObjectType ? createObjectLiteralType() : t); + else { + // Remove the innermost secondary error as it will duplicate the error already reported by `reportRelationError` on entry + secondaryRootErrors.shift(); } - - return createObjectLiteralType(); - - function createObjectLiteralType() { - const stringIndexInfo = hasComputedStringProperty ? getObjectLiteralIndexInfo(node, offset, propertiesArray, IndexKind.String) : undefined; - const numberIndexInfo = hasComputedNumberProperty ? getObjectLiteralIndexInfo(node, offset, propertiesArray, IndexKind.Number) : undefined; - const result = createAnonymousType(node.symbol, propertiesTable, emptyArray, emptyArray, stringIndexInfo, numberIndexInfo); - result.objectFlags |= objectFlags | ObjectFlags.ObjectLiteral | ObjectFlags.ContainsObjectOrArrayLiteral; - if (isJSObjectLiteral) { - result.objectFlags |= ObjectFlags.JSLiteral; - } - if (patternWithComputedProperties) { - result.objectFlags |= ObjectFlags.ObjectLiteralPatternWithComputedProperties; - } - if (inDestructuringPattern) { - result.pattern = node; - } - return result; + for (const [msg, ...args] of secondaryRootErrors) { + const originalValue = msg.elidedInCompatabilityPyramid; + msg.elidedInCompatabilityPyramid = false; // Teporarily override elision to ensure error is reported + reportError(msg, ...args); + msg.elidedInCompatabilityPyramid = originalValue; } - } - - function isValidSpreadType(type: Type): boolean { - if (type.flags & TypeFlags.Instantiable) { - const constraint = getBaseConstraintOfType(type); - if (constraint !== undefined) { - return isValidSpreadType(constraint); - } + if (info) { + // Actually do the last relation error + reportRelationError(/*headMessage*/ undefined, ...info); } - return !!(type.flags & (TypeFlags.Any | TypeFlags.NonPrimitive | TypeFlags.Object | TypeFlags.InstantiableNonPrimitive) || - getFalsyFlags(type) & TypeFlags.DefinitelyFalsy && isValidSpreadType(removeDefinitelyFalsyTypes(type)) || - type.flags & TypeFlags.UnionOrIntersection && every((type).types, isValidSpreadType)); - } - - function checkJsxSelfClosingElementDeferred(node: JsxSelfClosingElement) { - checkJsxOpeningLikeElementOrOpeningFragment(node); - resolveUntypedCall(node); // ensure type arguments and parameters are typechecked, even if there is an arity error } - - function checkJsxSelfClosingElement(node: JsxSelfClosingElement, _checkMode: CheckMode | undefined): Type { - checkNodeDeferred(node); - return getJsxElementTypeAt(node) || anyType; + function reportError(message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): void { + Debug.assert(!!errorNode); + if (incompatibleStack.length) + reportIncompatibleStack(); + if (message.elidedInCompatabilityPyramid) + return; + errorInfo = chainDiagnosticMessages(errorInfo, message, arg0, arg1, arg2, arg3); } - - function checkJsxElementDeferred(node: JsxElement) { - // Check attributes - checkJsxOpeningLikeElementOrOpeningFragment(node.openingElement); - - // Perform resolution on the closing tag so that rename/go to definition/etc work - if (isJsxIntrinsicIdentifier(node.closingElement.tagName)) { - getIntrinsicTagSymbol(node.closingElement); + function associateRelatedInfo(info: DiagnosticRelatedInformation) { + Debug.assert(!!errorInfo); + if (!relatedInfo) { + relatedInfo = [info]; } else { - checkExpression(node.closingElement.tagName); + relatedInfo.push(info); } - - checkJsxChildren(node); - } - - function checkJsxElement(node: JsxElement, _checkMode: CheckMode | undefined): Type { - checkNodeDeferred(node); - - return getJsxElementTypeAt(node) || anyType; } - - function checkJsxFragment(node: JsxFragment): Type { - checkJsxOpeningLikeElementOrOpeningFragment(node.openingFragment); - - if (compilerOptions.jsx === JsxEmit.React && (compilerOptions.jsxFactory || getSourceFileOfNode(node).pragmas.has("jsx"))) { - error(node, compilerOptions.jsxFactory - ? Diagnostics.JSX_fragment_is_not_supported_when_using_jsxFactory - : Diagnostics.JSX_fragment_is_not_supported_when_using_an_inline_JSX_factory_pragma); + function reportRelationError(message: DiagnosticMessage | undefined, source: ts.Type, target: ts.Type) { + if (incompatibleStack.length) + reportIncompatibleStack(); + const [sourceType, targetType] = getTypeNamesForErrorDisplay(source, target); + if (target.flags & TypeFlags.TypeParameter && target.immediateBaseConstraint !== undefined && isTypeAssignableTo(source, target.immediateBaseConstraint)) { + reportError(Diagnostics._0_is_assignable_to_the_constraint_of_type_1_but_1_could_be_instantiated_with_a_different_subtype_of_constraint_2, sourceType, targetType, typeToString(target.immediateBaseConstraint)); } - - checkJsxChildren(node); - return getJsxElementTypeAt(node) || anyType; + if (!message) { + if (relation === comparableRelation) { + message = Diagnostics.Type_0_is_not_comparable_to_type_1; + } + else if (sourceType === targetType) { + message = Diagnostics.Type_0_is_not_assignable_to_type_1_Two_different_types_with_this_name_exist_but_they_are_unrelated; + } + else { + message = Diagnostics.Type_0_is_not_assignable_to_type_1; + } + } + reportError(message, sourceType, targetType); } - - /** - * Returns true iff the JSX element name would be a valid JS identifier, ignoring restrictions about keywords not being identifiers - */ - function isUnhyphenatedJsxName(name: string | __String) { - // - is the only character supported in JSX attribute names that isn't valid in JavaScript identifiers - return !stringContains(name as string, "-"); + function tryElaborateErrorsForPrimitivesAndObjects(source: ts.Type, target: ts.Type) { + const sourceType = symbolValueDeclarationIsContextSensitive(source.symbol) ? typeToString(source, source.symbol.valueDeclaration) : typeToString(source); + const targetType = symbolValueDeclarationIsContextSensitive(target.symbol) ? typeToString(target, target.symbol.valueDeclaration) : typeToString(target); + if ((globalStringType === source && stringType === target) || + (globalNumberType === source && numberType === target) || + (globalBooleanType === source && booleanType === target) || + (getGlobalESSymbolType(/*reportErrors*/ false) === source && esSymbolType === target)) { + reportError(Diagnostics._0_is_a_primitive_but_1_is_a_wrapper_object_Prefer_using_0_when_possible, targetType, sourceType); + } } - /** - * Returns true iff React would emit this tag name as a string rather than an identifier or qualified name + * Try and elaborate array and tuple errors. Returns false + * if we have found an elaboration, or we should ignore + * any other elaborations when relating the `source` and + * `target` types. */ - function isJsxIntrinsicIdentifier(tagName: JsxTagNameExpression): boolean { - return tagName.kind === SyntaxKind.Identifier && isIntrinsicJsxName(tagName.escapedText); - } - - function checkJsxAttribute(node: JsxAttribute, checkMode?: CheckMode) { - return node.initializer - ? checkExpressionForMutableLocation(node.initializer, checkMode) - : trueType; // is sugar for + function tryElaborateArrayLikeErrors(source: ts.Type, target: ts.Type, reportErrors: boolean): boolean { + /** + * The spec for elaboration is: + * - If the source is a readonly tuple and the target is a mutable array or tuple, elaborate on mutability and skip property elaborations. + * - If the source is a tuple then skip property elaborations if the target is an array or tuple. + * - If the source is a readonly array and the target is a mutable array or tuple, elaborate on mutability and skip property elaborations. + * - If the source an array then skip property elaborations if the target is a tuple. + */ + if (isTupleType(source)) { + if (source.target.readonly && isMutableArrayOrTuple(target)) { + if (reportErrors) { + reportError(Diagnostics.The_type_0_is_readonly_and_cannot_be_assigned_to_the_mutable_type_1, typeToString(source), typeToString(target)); + } + return false; + } + return isTupleType(target) || isArrayType(target); + } + if (isReadonlyArrayType(source) && isMutableArrayOrTuple(target)) { + if (reportErrors) { + reportError(Diagnostics.The_type_0_is_readonly_and_cannot_be_assigned_to_the_mutable_type_1, typeToString(source), typeToString(target)); + } + return false; + } + if (isTupleType(target)) { + return isArrayType(source); + } + return true; } - /** - * Get attributes type of the JSX opening-like element. The result is from resolving "attributes" property of the opening-like element. - * - * @param openingLikeElement a JSX opening-like element - * @param filter a function to remove attributes that will not participate in checking whether attributes are assignable - * @return an anonymous type (similar to the one returned by checkObjectLiteral) in which its properties are attributes property. - * @remarks Because this function calls getSpreadType, it needs to use the same checks as checkObjectLiteral, - * which also calls getSpreadType. + * Compare two types and return + * * Ternary.True if they are related with no assumptions, + * * Ternary.Maybe if they are related with assumptions of other relationships, or + * * Ternary.False if they are not related. */ - function createJsxAttributesTypeFromAttributesProperty(openingLikeElement: JsxOpeningLikeElement, checkMode: CheckMode | undefined) { - const attributes = openingLikeElement.attributes; - let attributesTable = createSymbolTable(); - let spread: Type = emptyJsxObjectType; - let hasSpreadAnyType = false; - let typeToIntersect: Type | undefined; - let explicitlySpecifyChildrenAttribute = false; - let objectFlags: ObjectFlags = ObjectFlags.JsxAttributes; - const jsxChildrenPropertyName = getJsxElementChildrenPropertyName(getJsxNamespaceAt(openingLikeElement)); - - for (const attributeDecl of attributes.properties) { - const member = attributeDecl.symbol; - if (isJsxAttribute(attributeDecl)) { - const exprType = checkJsxAttribute(attributeDecl, checkMode); - objectFlags |= getObjectFlags(exprType) & ObjectFlags.PropagatingFlags; - - const attributeSymbol = createSymbol(SymbolFlags.Property | SymbolFlags.Transient | member.flags, member.escapedName); - attributeSymbol.declarations = member.declarations; - attributeSymbol.parent = member.parent; - if (member.valueDeclaration) { - attributeSymbol.valueDeclaration = member.valueDeclaration; - } - attributeSymbol.type = exprType; - attributeSymbol.target = member; - attributesTable.set(attributeSymbol.escapedName, attributeSymbol); - if (attributeDecl.name.escapedText === jsxChildrenPropertyName) { - explicitlySpecifyChildrenAttribute = true; - } + function isRelatedTo(originalSource: ts.Type, originalTarget: ts.Type, reportErrors = false, headMessage?: DiagnosticMessage, intersectionState = IntersectionState.None): Ternary { + // Normalize the source and target types: Turn fresh literal types into regular literal types, + // turn deferred type references into regular type references, simplify indexed access and + // conditional types, and resolve substitution types to either the substitution (on the source + // side) or the type variable (on the target side). + let source = getNormalizedType(originalSource, /*writing*/ false); + let target = getNormalizedType(originalTarget, /*writing*/ true); + if (source === target) + return Ternary.True; + if (relation === identityRelation) { + return isIdenticalTo(source, target); + } + // Try to see if we're relating something like `Foo` -> `Bar | null | undefined`. + // If so, reporting the `null` and `undefined` in the type is hardly useful. + // First, see if we're even relating an object type to a union. + // Then see if the target is stripped down to a single non-union type. + // Note + // * We actually want to remove null and undefined naively here (rather than using getNonNullableType), + // since we don't want to end up with a worse error like "`Foo` is not assignable to `NonNullable`" + // when dealing with generics. + // * We also don't deal with primitive source types, since we already halt elaboration below. + if (target.flags & TypeFlags.Union && source.flags & TypeFlags.Object && + (target as UnionType).types.length <= 3 && maybeTypeOfKind(target, TypeFlags.Nullable)) { + const nullStrippedTarget = extractTypesOfKind(target, ~TypeFlags.Nullable); + if (!(nullStrippedTarget.flags & (TypeFlags.Union | TypeFlags.Never))) { + if (source === nullStrippedTarget) + return Ternary.True; + target = nullStrippedTarget; } - else { - Debug.assert(attributeDecl.kind === SyntaxKind.JsxSpreadAttribute); - if (attributesTable.size > 0) { - spread = getSpreadType(spread, createJsxAttributesType(), attributes.symbol, objectFlags, /*readonly*/ false); - attributesTable = createSymbolTable(); - } - const exprType = checkExpressionCached(attributeDecl.expression, checkMode); - if (isTypeAny(exprType)) { - hasSpreadAnyType = true; + } + if (relation === comparableRelation && !(target.flags & TypeFlags.Never) && isSimpleTypeRelatedTo(target, source, relation) || + isSimpleTypeRelatedTo(source, target, relation, reportErrors ? reportError : undefined)) + return Ternary.True; + const isComparingJsxAttributes = !!(getObjectFlags(source) & ObjectFlags.JsxAttributes); + const isPerformingExcessPropertyChecks = !(intersectionState & IntersectionState.Target) && (isObjectLiteralType(source) && getObjectFlags(source) & ObjectFlags.FreshLiteral); + if (isPerformingExcessPropertyChecks) { + if (hasExcessProperties((source), target, reportErrors)) { + if (reportErrors) { + reportRelationError(headMessage, source, target); } - if (isValidSpreadType(exprType)) { - spread = getSpreadType(spread, exprType, attributes.symbol, objectFlags, /*readonly*/ false); + return Ternary.False; + } + } + const isPerformingCommonPropertyChecks = relation !== comparableRelation && !(intersectionState & IntersectionState.Target) && + source.flags & (TypeFlags.Primitive | TypeFlags.Object | TypeFlags.Intersection) && source !== globalObjectType && + target.flags & (TypeFlags.Object | TypeFlags.Intersection) && isWeakType(target) && + (getPropertiesOfType(source).length > 0 || typeHasCallOrConstructSignatures(source)); + if (isPerformingCommonPropertyChecks && !hasCommonProperties(source, target, isComparingJsxAttributes)) { + if (reportErrors) { + const calls = getSignaturesOfType(source, SignatureKind.Call); + const constructs = getSignaturesOfType(source, SignatureKind.Construct); + if (calls.length > 0 && isRelatedTo(getReturnTypeOfSignature(calls[0]), target, /*reportErrors*/ false) || + constructs.length > 0 && isRelatedTo(getReturnTypeOfSignature(constructs[0]), target, /*reportErrors*/ false)) { + reportError(Diagnostics.Value_of_type_0_has_no_properties_in_common_with_type_1_Did_you_mean_to_call_it, typeToString(source), typeToString(target)); } else { - typeToIntersect = typeToIntersect ? getIntersectionType([typeToIntersect, exprType]) : exprType; + reportError(Diagnostics.Type_0_has_no_properties_in_common_with_type_1, typeToString(source), typeToString(target)); } } + return Ternary.False; } - - if (!hasSpreadAnyType) { - if (attributesTable.size > 0) { - spread = getSpreadType(spread, createJsxAttributesType(), attributes.symbol, objectFlags, /*readonly*/ false); - } + let result = Ternary.False; + const saveErrorInfo = captureErrorCalculationState(); + // Note that these checks are specifically ordered to produce correct results. In particular, + // we need to deconstruct unions before intersections (because unions are always at the top), + // and we need to handle "each" relations before "some" relations for the same kind of type. + if (source.flags & TypeFlags.Union) { + result = relation === comparableRelation ? + someTypeRelatedToType((source as UnionType), target, reportErrors && !(source.flags & TypeFlags.Primitive), intersectionState) : + eachTypeRelatedToType((source as UnionType), target, reportErrors && !(source.flags & TypeFlags.Primitive)); } - - // Handle children attribute - const parent = openingLikeElement.parent.kind === SyntaxKind.JsxElement ? openingLikeElement.parent as JsxElement : undefined; - // We have to check that openingElement of the parent is the one we are visiting as this may not be true for selfClosingElement - if (parent && parent.openingElement === openingLikeElement && parent.children.length > 0) { - const childrenTypes: Type[] = checkJsxChildren(parent, checkMode); - - if (!hasSpreadAnyType && jsxChildrenPropertyName && jsxChildrenPropertyName !== "") { - // Error if there is a attribute named "children" explicitly specified and children element. - // This is because children element will overwrite the value from attributes. - // Note: we will not warn "children" attribute overwritten if "children" attribute is specified in object spread. - if (explicitlySpecifyChildrenAttribute) { - error(attributes, Diagnostics._0_are_specified_twice_The_attribute_named_0_will_be_overwritten, unescapeLeadingUnderscores(jsxChildrenPropertyName)); - } - - const contextualType = getApparentTypeOfContextualType(openingLikeElement.attributes); - const childrenContextualType = contextualType && getTypeOfPropertyOfContextualType(contextualType, jsxChildrenPropertyName); - // If there are children in the body of JSX element, create dummy attribute "children" with the union of children types so that it will pass the attribute checking process - const childrenPropSymbol = createSymbol(SymbolFlags.Property | SymbolFlags.Transient, jsxChildrenPropertyName); - childrenPropSymbol.type = childrenTypes.length === 1 ? - childrenTypes[0] : - (getArrayLiteralTupleTypeIfApplicable(childrenTypes, childrenContextualType, /*hasRestElement*/ false) || createArrayType(getUnionType(childrenTypes))); - // Fake up a property declaration for the children - childrenPropSymbol.valueDeclaration = createPropertySignature(/*modifiers*/ undefined, unescapeLeadingUnderscores(jsxChildrenPropertyName), /*questionToken*/ undefined, /*type*/ undefined, /*initializer*/ undefined); - childrenPropSymbol.valueDeclaration.parent = attributes; - childrenPropSymbol.valueDeclaration.symbol = childrenPropSymbol; - const childPropMap = createSymbolTable(); - childPropMap.set(jsxChildrenPropertyName, childrenPropSymbol); - spread = getSpreadType(spread, createAnonymousType(attributes.symbol, childPropMap, emptyArray, emptyArray, /*stringIndexInfo*/ undefined, /*numberIndexInfo*/ undefined), - attributes.symbol, objectFlags, /*readonly*/ false); - + else { + if (target.flags & TypeFlags.Union) { + result = typeRelatedToSomeType(getRegularTypeOfObjectLiteral(source), (target), reportErrors && !(source.flags & TypeFlags.Primitive) && !(target.flags & TypeFlags.Primitive)); } - } - - if (hasSpreadAnyType) { - return anyType; - } - if (typeToIntersect && spread !== emptyJsxObjectType) { - return getIntersectionType([typeToIntersect, spread]); - } - return typeToIntersect || (spread === emptyJsxObjectType ? createJsxAttributesType() : spread); - - /** - * Create anonymous type from given attributes symbol table. - * @param symbol a symbol of JsxAttributes containing attributes corresponding to attributesTable - * @param attributesTable a symbol table of attributes property - */ - function createJsxAttributesType() { - objectFlags |= freshObjectLiteralFlag; - const result = createAnonymousType(attributes.symbol, attributesTable, emptyArray, emptyArray, /*stringIndexInfo*/ undefined, /*numberIndexInfo*/ undefined); - result.objectFlags |= objectFlags | ObjectFlags.ObjectLiteral | ObjectFlags.ContainsObjectOrArrayLiteral; - return result; - } - } - - function checkJsxChildren(node: JsxElement | JsxFragment, checkMode?: CheckMode) { - const childrenTypes: Type[] = []; - for (const child of node.children) { - // In React, JSX text that contains only whitespaces will be ignored so we don't want to type-check that - // because then type of children property will have constituent of string type. - if (child.kind === SyntaxKind.JsxText) { - if (!child.containsOnlyTriviaWhiteSpaces) { - childrenTypes.push(stringType); + else if (target.flags & TypeFlags.Intersection) { + result = typeRelatedToEachType(getRegularTypeOfObjectLiteral(source), (target as IntersectionType), reportErrors, IntersectionState.Target); + if (result && (isPerformingExcessPropertyChecks || isPerformingCommonPropertyChecks)) { + // Validate against excess props using the original `source` + if (!propertiesRelatedTo(source, target, reportErrors, /*excludedProperties*/ undefined, IntersectionState.None)) { + return Ternary.False; + } } } - else { - childrenTypes.push(checkExpressionForMutableLocation(child, checkMode)); + else if (source.flags & TypeFlags.Intersection) { + // Check to see if any constituents of the intersection are immediately related to the target. + // + // Don't report errors though. Checking whether a constituent is related to the source is not actually + // useful and leads to some confusing error messages. Instead it is better to let the below checks + // take care of this, or to not elaborate at all. For instance, + // + // - For an object type (such as 'C = A & B'), users are usually more interested in structural errors. + // + // - For a union type (such as '(A | B) = (C & D)'), it's better to hold onto the whole intersection + // than to report that 'D' is not assignable to 'A' or 'B'. + // + // - For a primitive type or type parameter (such as 'number = A & B') there is no point in + // breaking the intersection apart. + result = someTypeRelatedToType((source), target, /*reportErrors*/ false, IntersectionState.Source); + } + if (!result && (source.flags & TypeFlags.StructuredOrInstantiable || target.flags & TypeFlags.StructuredOrInstantiable)) { + if (result = recursiveTypeRelatedTo(source, target, reportErrors, intersectionState)) { + resetErrorInfo(saveErrorInfo); + } } } - return childrenTypes; - } - - /** - * Check attributes property of opening-like element. This function is called during chooseOverload to get call signature of a JSX opening-like element. - * (See "checkApplicableSignatureForJsxOpeningLikeElement" for how the function is used) - * @param node a JSXAttributes to be resolved of its type - */ - function checkJsxAttributes(node: JsxAttributes, checkMode: CheckMode | undefined) { - return createJsxAttributesTypeFromAttributesProperty(node.parent, checkMode); - } - - function getJsxType(name: __String, location: Node | undefined) { - const namespace = getJsxNamespaceAt(location); - const exports = namespace && getExportsOfSymbol(namespace); - const typeSymbol = exports && getSymbol(exports, name, SymbolFlags.Type); - return typeSymbol ? getDeclaredTypeOfSymbol(typeSymbol) : errorType; - } - - /** - * Looks up an intrinsic tag name and returns a symbol that either points to an intrinsic - * property (in which case nodeLinks.jsxFlags will be IntrinsicNamedElement) or an intrinsic - * string index signature (in which case nodeLinks.jsxFlags will be IntrinsicIndexedElement). - * May also return unknownSymbol if both of these lookups fail. - */ - function getIntrinsicTagSymbol(node: JsxOpeningLikeElement | JsxClosingElement): Symbol { - const links = getNodeLinks(node); - if (!links.resolvedSymbol) { - const intrinsicElementsType = getJsxType(JsxNames.IntrinsicElements, node); - if (intrinsicElementsType !== errorType) { - // Property case - if (!isIdentifier(node.tagName)) return Debug.fail(); - const intrinsicProp = getPropertyOfType(intrinsicElementsType, node.tagName.escapedText); - if (intrinsicProp) { - links.jsxFlags |= JsxFlags.IntrinsicNamedElement; - return links.resolvedSymbol = intrinsicProp; + if (!result && source.flags & (TypeFlags.Intersection | TypeFlags.TypeParameter)) { + // The combined constraint of an intersection type is the intersection of the constraints of + // the constituents. When an intersection type contains instantiable types with union type + // constraints, there are situations where we need to examine the combined constraint. One is + // when the target is a union type. Another is when the intersection contains types belonging + // to one of the disjoint domains. For example, given type variables T and U, each with the + // constraint 'string | number', the combined constraint of 'T & U' is 'string | number' and + // we need to check this constraint against a union on the target side. Also, given a type + // variable V constrained to 'string | number', 'V & number' has a combined constraint of + // 'string & number | number & number' which reduces to just 'number'. + // This also handles type parameters, as a type parameter with a union constraint compared against a union + // needs to have its constraint hoisted into an intersection with said type parameter, this way + // the type param can be compared with itself in the target (with the influence of its constraint to match other parts) + // For example, if `T extends 1 | 2` and `U extends 2 | 3` and we compare `T & U` to `T & U & (1 | 2 | 3)` + const constraint = getEffectiveConstraintOfIntersection(source.flags & TypeFlags.Intersection ? (source).types : [source], !!(target.flags & TypeFlags.Union)); + if (constraint && (source.flags & TypeFlags.Intersection || target.flags & TypeFlags.Union)) { + if (everyType(constraint, c => c !== source)) { // Skip comparison if expansion contains the source itself + // TODO: Stack errors so we get a pyramid for the "normal" comparison above, _and_ a second for this + if (result = isRelatedTo(constraint, target, /*reportErrors*/ false, /*headMessage*/ undefined, intersectionState)) { + resetErrorInfo(saveErrorInfo); + } } - - // Intrinsic string indexer case - const indexSignatureType = getIndexTypeOfType(intrinsicElementsType, IndexKind.String); - if (indexSignatureType) { - links.jsxFlags |= JsxFlags.IntrinsicIndexedElement; - return links.resolvedSymbol = intrinsicElementsType.symbol; + } + } + if (!result && reportErrors) { + source = originalSource.aliasSymbol ? originalSource : source; + target = originalTarget.aliasSymbol ? originalTarget : target; + let maybeSuppress = overrideNextErrorInfo > 0; + if (maybeSuppress) { + overrideNextErrorInfo--; + } + if (source.flags & TypeFlags.Object && target.flags & TypeFlags.Object) { + const currentError = errorInfo; + tryElaborateArrayLikeErrors(source, target, reportErrors); + if (errorInfo !== currentError) { + maybeSuppress = !!errorInfo; } - - // Wasn't found - error(node, Diagnostics.Property_0_does_not_exist_on_type_1, idText(node.tagName), "JSX." + JsxNames.IntrinsicElements); - return links.resolvedSymbol = unknownSymbol; } - else { - if (noImplicitAny) { - error(node, Diagnostics.JSX_element_implicitly_has_type_any_because_no_interface_JSX_0_exists, unescapeLeadingUnderscores(JsxNames.IntrinsicElements)); + if (source.flags & TypeFlags.Object && target.flags & TypeFlags.Primitive) { + tryElaborateErrorsForPrimitivesAndObjects(source, target); + } + else if (source.symbol && source.flags & TypeFlags.Object && globalObjectType === source) { + reportError(Diagnostics.The_Object_type_is_assignable_to_very_few_other_types_Did_you_mean_to_use_the_any_type_instead); + } + else if (isComparingJsxAttributes && target.flags & TypeFlags.Intersection) { + const targetTypes = (target as IntersectionType).types; + const intrinsicAttributes = getJsxType(JsxNames.IntrinsicAttributes, errorNode); + const intrinsicClassAttributes = getJsxType(JsxNames.IntrinsicClassAttributes, errorNode); + if (intrinsicAttributes !== errorType && intrinsicClassAttributes !== errorType && + (contains(targetTypes, intrinsicAttributes) || contains(targetTypes, intrinsicClassAttributes))) { + // do not report top error + return result; } - return links.resolvedSymbol = unknownSymbol; } + if (!headMessage && maybeSuppress) { + lastSkippedInfo = [source, target]; + // Used by, eg, missing property checking to replace the top-level message with a more informative one + return result; + } + reportRelationError(headMessage, source, target); } - return links.resolvedSymbol; + return result; } - - function getJsxNamespaceAt(location: Node | undefined): Symbol { - const links = location && getNodeLinks(location); - if (links && links.jsxNamespace) { - return links.jsxNamespace; - } - if (!links || links.jsxNamespace !== false) { - const namespaceName = getJsxNamespace(location); - const resolvedNamespace = resolveName(location, namespaceName, SymbolFlags.Namespace, /*diagnosticMessage*/ undefined, namespaceName, /*isUse*/ false); - if (resolvedNamespace) { - const candidate = resolveSymbol(getSymbol(getExportsOfSymbol(resolveSymbol(resolvedNamespace)), JsxNames.JSX, SymbolFlags.Namespace)); - if (candidate) { - if (links) { - links.jsxNamespace = candidate; + function isIdenticalTo(source: ts.Type, target: ts.Type): Ternary { + const flags = source.flags & target.flags; + if (!(flags & TypeFlags.Substructure)) { + return Ternary.False; + } + if (flags & TypeFlags.UnionOrIntersection) { + let result = eachTypeRelatedToSomeType((source), (target)); + if (result) { + result &= eachTypeRelatedToSomeType((target), (source)); + } + return result; + } + return recursiveTypeRelatedTo(source, target, /*reportErrors*/ false, IntersectionState.None); + } + function getTypeOfPropertyInTypes(types: ts.Type[], name: __String) { + const appendPropType = (propTypes: ts.Type[] | undefined, type: ts.Type) => { + type = getApparentType(type); + const prop = type.flags & TypeFlags.UnionOrIntersection ? getPropertyOfUnionOrIntersectionType((type), name) : getPropertyOfObjectType(type, name); + const propType = prop && getTypeOfSymbol(prop) || isNumericLiteralName(name) && getIndexTypeOfType(type, IndexKind.Number) || getIndexTypeOfType(type, IndexKind.String) || undefinedType; + return append(propTypes, propType); + }; + return getUnionType(reduceLeft(types, appendPropType, /*initial*/ undefined) || emptyArray); + } + function hasExcessProperties(source: FreshObjectLiteralType, target: ts.Type, reportErrors: boolean): boolean { + if (!isExcessPropertyCheckTarget(target) || !noImplicitAny && getObjectFlags(target) & ObjectFlags.JSLiteral) { + return false; // Disable excess property checks on JS literals to simulate having an implicit "index signature" - but only outside of noImplicitAny + } + const isComparingJsxAttributes = !!(getObjectFlags(source) & ObjectFlags.JsxAttributes); + if ((relation === assignableRelation || relation === comparableRelation) && + (isTypeSubsetOf(globalObjectType, target) || (!isComparingJsxAttributes && isEmptyObjectType(target)))) { + return false; + } + let reducedTarget = target; + let checkTypes: ts.Type[] | undefined; + if (target.flags & TypeFlags.Union) { + reducedTarget = findMatchingDiscriminantType(source, (target), isRelatedTo) || filterPrimitivesIfContainsNonPrimitive((target)); + checkTypes = reducedTarget.flags & TypeFlags.Union ? (reducedTarget).types : [reducedTarget]; + } + for (const prop of getPropertiesOfType(source)) { + if (shouldCheckAsExcessProperty(prop, source.symbol) && !isIgnoredJsxProperty(source, prop)) { + if (!isKnownProperty(reducedTarget, prop.escapedName, isComparingJsxAttributes)) { + if (reportErrors) { + // Report error in terms of object types in the target as those are the only ones + // we check in isKnownProperty. + const errorTarget = filterType(reducedTarget, isExcessPropertyCheckTarget); + // We know *exactly* where things went wrong when comparing the types. + // Use this property as the error node as this will be more helpful in + // reasoning about what went wrong. + if (!errorNode) + return Debug.fail(); + if (isJsxAttributes(errorNode) || isJsxOpeningLikeElement(errorNode) || isJsxOpeningLikeElement(errorNode.parent)) { + // JsxAttributes has an object-literal flag and undergo same type-assignablity check as normal object-literal. + // However, using an object-literal error message will be very confusing to the users so we give different a message. + // TODO: Spelling suggestions for excess jsx attributes (needs new diagnostic messages) + if (prop.valueDeclaration && isJsxAttribute(prop.valueDeclaration) && getSourceFileOfNode(errorNode) === getSourceFileOfNode(prop.valueDeclaration.name)) { + // Note that extraneous children (as in `extra`) don't pass this check, + // since `children` is a SyntaxKind.PropertySignature instead of a SyntaxKind.JsxAttribute. + errorNode = prop.valueDeclaration.name; + } + reportError(Diagnostics.Property_0_does_not_exist_on_type_1, symbolToString(prop), typeToString(errorTarget)); + } + else { + // use the property's value declaration if the property is assigned inside the literal itself + const objectLiteralDeclaration = source.symbol && firstOrUndefined(source.symbol.declarations); + let suggestion; + if (prop.valueDeclaration && findAncestor(prop.valueDeclaration, d => d === objectLiteralDeclaration) && getSourceFileOfNode(objectLiteralDeclaration) === getSourceFileOfNode(errorNode)) { + const propDeclaration = (prop.valueDeclaration as ObjectLiteralElementLike); + Debug.assertNode(propDeclaration, isObjectLiteralElementLike); + errorNode = propDeclaration; + const name = propDeclaration.name!; + if (isIdentifier(name)) { + suggestion = getSuggestionForNonexistentProperty(name, errorTarget); + } + } + if (suggestion !== undefined) { + reportError(Diagnostics.Object_literal_may_only_specify_known_properties_but_0_does_not_exist_in_type_1_Did_you_mean_to_write_2, symbolToString(prop), typeToString(errorTarget), suggestion); + } + else { + reportError(Diagnostics.Object_literal_may_only_specify_known_properties_and_0_does_not_exist_in_type_1, symbolToString(prop), typeToString(errorTarget)); + } + } } - return candidate; + return true; } - if (links) { - links.jsxNamespace = false; + if (checkTypes && !isRelatedTo(getTypeOfSymbol(prop), getTypeOfPropertyInTypes(checkTypes, prop.escapedName), reportErrors)) { + if (reportErrors) { + reportIncompatibleError(Diagnostics.Types_of_property_0_are_incompatible, symbolToString(prop)); + } + return true; } } } - // JSX global fallback - return getGlobalSymbol(JsxNames.JSX, SymbolFlags.Namespace, /*diagnosticMessage*/ undefined)!; // TODO: GH#18217 + return false; } - - /** - * Look into JSX namespace and then look for container with matching name as nameOfAttribPropContainer. - * Get a single property from that container if existed. Report an error if there are more than one property. - * - * @param nameOfAttribPropContainer a string of value JsxNames.ElementAttributesPropertyNameContainer or JsxNames.ElementChildrenAttributeNameContainer - * if other string is given or the container doesn't exist, return undefined. - */ - function getNameFromJsxElementAttributesContainer(nameOfAttribPropContainer: __String, jsxNamespace: Symbol): __String | undefined { - // JSX.ElementAttributesProperty | JSX.ElementChildrenAttribute [symbol] - const jsxElementAttribPropInterfaceSym = jsxNamespace && getSymbol(jsxNamespace.exports!, nameOfAttribPropContainer, SymbolFlags.Type); - // JSX.ElementAttributesProperty | JSX.ElementChildrenAttribute [type] - const jsxElementAttribPropInterfaceType = jsxElementAttribPropInterfaceSym && getDeclaredTypeOfSymbol(jsxElementAttribPropInterfaceSym); - // The properties of JSX.ElementAttributesProperty | JSX.ElementChildrenAttribute - const propertiesOfJsxElementAttribPropInterface = jsxElementAttribPropInterfaceType && getPropertiesOfType(jsxElementAttribPropInterfaceType); - if (propertiesOfJsxElementAttribPropInterface) { - // Element Attributes has zero properties, so the element attributes type will be the class instance type - if (propertiesOfJsxElementAttribPropInterface.length === 0) { - return "" as __String; - } - // Element Attributes has one property, so the element attributes type will be the type of the corresponding - // property of the class instance type - else if (propertiesOfJsxElementAttribPropInterface.length === 1) { - return propertiesOfJsxElementAttribPropInterface[0].escapedName; - } - else if (propertiesOfJsxElementAttribPropInterface.length > 1) { - // More than one property on ElementAttributesProperty is an error - error(jsxElementAttribPropInterfaceSym!.declarations[0], Diagnostics.The_global_type_JSX_0_may_not_have_more_than_one_property, unescapeLeadingUnderscores(nameOfAttribPropContainer)); + function shouldCheckAsExcessProperty(prop: ts.Symbol, container: ts.Symbol) { + return prop.valueDeclaration && container.valueDeclaration && prop.valueDeclaration.parent === container.valueDeclaration; + } + function eachTypeRelatedToSomeType(source: UnionOrIntersectionType, target: UnionOrIntersectionType): Ternary { + let result = Ternary.True; + const sourceTypes = source.types; + for (const sourceType of sourceTypes) { + const related = typeRelatedToSomeType(sourceType, target, /*reportErrors*/ false); + if (!related) { + return Ternary.False; } + result &= related; } - return undefined; - } - - function getJsxLibraryManagedAttributes(jsxNamespace: Symbol) { - // JSX.LibraryManagedAttributes [symbol] - return jsxNamespace && getSymbol(jsxNamespace.exports!, JsxNames.LibraryManagedAttributes, SymbolFlags.Type); - } - - /// e.g. "props" for React.d.ts, - /// or 'undefined' if ElementAttributesProperty doesn't exist (which means all - /// non-intrinsic elements' attributes type is 'any'), - /// or '' if it has 0 properties (which means every - /// non-intrinsic elements' attributes type is the element instance type) - function getJsxElementPropertiesName(jsxNamespace: Symbol) { - return getNameFromJsxElementAttributesContainer(JsxNames.ElementAttributesPropertyNameContainer, jsxNamespace); - } - - function getJsxElementChildrenPropertyName(jsxNamespace: Symbol): __String | undefined { - return getNameFromJsxElementAttributesContainer(JsxNames.ElementChildrenAttributeNameContainer, jsxNamespace); + return result; } - - function getUninstantiatedJsxSignaturesOfType(elementType: Type, caller: JsxOpeningLikeElement): readonly Signature[] { - if (elementType.flags & TypeFlags.String) { - return [anySignature]; + function typeRelatedToSomeType(source: ts.Type, target: UnionOrIntersectionType, reportErrors: boolean): Ternary { + const targetTypes = target.types; + if (target.flags & TypeFlags.Union && containsType(targetTypes, source)) { + return Ternary.True; } - else if (elementType.flags & TypeFlags.StringLiteral) { - const intrinsicType = getIntrinsicAttributesTypeFromStringLiteralType(elementType as StringLiteralType, caller); - if (!intrinsicType) { - error(caller, Diagnostics.Property_0_does_not_exist_on_type_1, (elementType as StringLiteralType).value, "JSX." + JsxNames.IntrinsicElements); - return emptyArray; - } - else { - const fakeSignature = createSignatureForJSXIntrinsic(caller, intrinsicType); - return [fakeSignature]; + for (const type of targetTypes) { + const related = isRelatedTo(source, type, /*reportErrors*/ false); + if (related) { + return related; } } - const apparentElemType = getApparentType(elementType); - // Resolve the signatures, preferring constructor - let signatures = getSignaturesOfType(apparentElemType, SignatureKind.Construct); - if (signatures.length === 0) { - // No construct signatures, try call signatures - signatures = getSignaturesOfType(apparentElemType, SignatureKind.Call); - } - if (signatures.length === 0 && apparentElemType.flags & TypeFlags.Union) { - // If each member has some combination of new/call signatures; make a union signature list for those - signatures = getUnionSignatures(map((apparentElemType as UnionType).types, t => getUninstantiatedJsxSignaturesOfType(t, caller))); + if (reportErrors) { + const bestMatchingType = getBestMatchingType(source, target, isRelatedTo); + isRelatedTo(source, bestMatchingType || targetTypes[targetTypes.length - 1], /*reportErrors*/ true); } - return signatures; + return Ternary.False; } - - function getIntrinsicAttributesTypeFromStringLiteralType(type: StringLiteralType, location: Node): Type | undefined { - // If the elemType is a stringLiteral type, we can then provide a check to make sure that the string literal type is one of the Jsx intrinsic element type - // For example: - // var CustomTag: "h1" = "h1"; - // Hello World - const intrinsicElementsType = getJsxType(JsxNames.IntrinsicElements, location); - if (intrinsicElementsType !== errorType) { - const stringLiteralTypeName = type.value; - const intrinsicProp = getPropertyOfType(intrinsicElementsType, escapeLeadingUnderscores(stringLiteralTypeName)); - if (intrinsicProp) { - return getTypeOfSymbol(intrinsicProp); - } - const indexSignatureType = getIndexTypeOfType(intrinsicElementsType, IndexKind.String); - if (indexSignatureType) { - return indexSignatureType; + function typeRelatedToEachType(source: ts.Type, target: IntersectionType, reportErrors: boolean, intersectionState: IntersectionState): Ternary { + let result = Ternary.True; + const targetTypes = target.types; + for (const targetType of targetTypes) { + const related = isRelatedTo(source, targetType, reportErrors, /*headMessage*/ undefined, intersectionState); + if (!related) { + return Ternary.False; } - return undefined; + result &= related; } - // If we need to report an error, we already done so here. So just return any to prevent any more error downstream - return anyType; + return result; } - - function checkJsxReturnAssignableToAppropriateBound(refKind: JsxReferenceKind, elemInstanceType: Type, openingLikeElement: Node) { - if (refKind === JsxReferenceKind.Function) { - const sfcReturnConstraint = getJsxStatelessElementTypeAt(openingLikeElement); - if (sfcReturnConstraint) { - checkTypeRelatedTo(elemInstanceType, sfcReturnConstraint, assignableRelation, openingLikeElement, Diagnostics.JSX_element_type_0_is_not_a_constructor_function_for_JSX_elements); - } + function someTypeRelatedToType(source: UnionOrIntersectionType, target: ts.Type, reportErrors: boolean, intersectionState: IntersectionState): Ternary { + const sourceTypes = source.types; + if (source.flags & TypeFlags.Union && containsType(sourceTypes, target)) { + return Ternary.True; } - else if (refKind === JsxReferenceKind.Component) { - const classConstraint = getJsxElementClassTypeAt(openingLikeElement); - if (classConstraint) { - // Issue an error if this return type isn't assignable to JSX.ElementClass or JSX.Element, failing that - checkTypeRelatedTo(elemInstanceType, classConstraint, assignableRelation, openingLikeElement, Diagnostics.JSX_element_type_0_is_not_a_constructor_function_for_JSX_elements); + const len = sourceTypes.length; + for (let i = 0; i < len; i++) { + const related = isRelatedTo(sourceTypes[i], target, reportErrors && i === len - 1, /*headMessage*/ undefined, intersectionState); + if (related) { + return related; } } - else { // Mixed - const sfcReturnConstraint = getJsxStatelessElementTypeAt(openingLikeElement); - const classConstraint = getJsxElementClassTypeAt(openingLikeElement); - if (!sfcReturnConstraint || !classConstraint) { - return; + return Ternary.False; + } + function eachTypeRelatedToType(source: UnionOrIntersectionType, target: ts.Type, reportErrors: boolean): Ternary { + let result = Ternary.True; + const sourceTypes = source.types; + for (const sourceType of sourceTypes) { + const related = isRelatedTo(sourceType, target, reportErrors); + if (!related) { + return Ternary.False; } - const combined = getUnionType([sfcReturnConstraint, classConstraint]); - checkTypeRelatedTo(elemInstanceType, combined, assignableRelation, openingLikeElement, Diagnostics.JSX_element_type_0_is_not_a_constructor_function_for_JSX_elements); + result &= related; } + return result; } - - /** - * Get attributes type of the given intrinsic opening-like Jsx element by resolving the tag name. - * The function is intended to be called from a function which has checked that the opening element is an intrinsic element. - * @param node an intrinsic JSX opening-like element - */ - function getIntrinsicAttributesTypeFromJsxOpeningLikeElement(node: JsxOpeningLikeElement): Type { - Debug.assert(isJsxIntrinsicIdentifier(node.tagName)); - const links = getNodeLinks(node); - if (!links.resolvedJsxElementAttributesType) { - const symbol = getIntrinsicTagSymbol(node); - if (links.jsxFlags & JsxFlags.IntrinsicNamedElement) { - return links.resolvedJsxElementAttributesType = getTypeOfSymbol(symbol); + function typeArgumentsRelatedTo(sources: readonly ts.Type[] = emptyArray, targets: readonly ts.Type[] = emptyArray, variances: readonly VarianceFlags[] = emptyArray, reportErrors: boolean, intersectionState: IntersectionState): Ternary { + if (sources.length !== targets.length && relation === identityRelation) { + return Ternary.False; + } + const length = sources.length <= targets.length ? sources.length : targets.length; + let result = Ternary.True; + for (let i = 0; i < length; i++) { + // When variance information isn't available we default to covariance. This happens + // in the process of computing variance information for recursive types and when + // comparing 'this' type arguments. + const varianceFlags = i < variances.length ? variances[i] : VarianceFlags.Covariant; + const variance = varianceFlags & VarianceFlags.VarianceMask; + // We ignore arguments for independent type parameters (because they're never witnessed). + if (variance !== VarianceFlags.Independent) { + const s = sources[i]; + const t = targets[i]; + let related = Ternary.True; + if (varianceFlags & VarianceFlags.Unmeasurable) { + // Even an `Unmeasurable` variance works out without a structural check if the source and target are _identical_. + // We can't simply assume invariance, because `Unmeasurable` marks nonlinear relations, for example, a relation tained by + // the `-?` modifier in a mapped type (where, no matter how the inputs are related, the outputs still might not be) + related = relation === identityRelation ? isRelatedTo(s, t, /*reportErrors*/ false) : compareTypesIdentical(s, t); + } + else if (variance === VarianceFlags.Covariant) { + related = isRelatedTo(s, t, reportErrors, /*headMessage*/ undefined, intersectionState); + } + else if (variance === VarianceFlags.Contravariant) { + related = isRelatedTo(t, s, reportErrors, /*headMessage*/ undefined, intersectionState); + } + else if (variance === VarianceFlags.Bivariant) { + // In the bivariant case we first compare contravariantly without reporting + // errors. Then, if that doesn't succeed, we compare covariantly with error + // reporting. Thus, error elaboration will be based on the the covariant check, + // which is generally easier to reason about. + related = isRelatedTo(t, s, /*reportErrors*/ false); + if (!related) { + related = isRelatedTo(s, t, reportErrors, /*headMessage*/ undefined, intersectionState); + } + } + else { + // In the invariant case we first compare covariantly, and only when that + // succeeds do we proceed to compare contravariantly. Thus, error elaboration + // will typically be based on the covariant check. + related = isRelatedTo(s, t, reportErrors, /*headMessage*/ undefined, intersectionState); + if (related) { + related &= isRelatedTo(t, s, reportErrors, /*headMessage*/ undefined, intersectionState); + } + } + if (!related) { + return Ternary.False; + } + result &= related; } - else if (links.jsxFlags & JsxFlags.IntrinsicIndexedElement) { - return links.resolvedJsxElementAttributesType = - getIndexTypeOfType(getDeclaredTypeOfSymbol(symbol), IndexKind.String)!; + } + return result; + } + // Determine if possibly recursive types are related. First, check if the result is already available in the global cache. + // Second, check if we have already started a comparison of the given two types in which case we assume the result to be true. + // Third, check if both types are part of deeply nested chains of generic type instantiations and if so assume the types are + // equal and infinitely expanding. Fourth, if we have reached a depth of 100 nested comparisons, assume we have runaway recursion + // and issue an error. Otherwise, actually compare the structure of the two types. + function recursiveTypeRelatedTo(source: ts.Type, target: ts.Type, reportErrors: boolean, intersectionState: IntersectionState): Ternary { + if (overflow) { + return Ternary.False; + } + const id = getRelationKey(source, target, intersectionState, relation); + const entry = relation.get(id); + if (entry !== undefined) { + if (reportErrors && entry & RelationComparisonResult.Failed && !(entry & RelationComparisonResult.Reported)) { + // We are elaborating errors and the cached result is an unreported failure. The result will be reported + // as a failure, and should be updated as a reported failure by the bottom of this function. } else { - return links.resolvedJsxElementAttributesType = errorType; + if (outofbandVarianceMarkerHandler) { + // We're in the middle of variance checking - integrate any unmeasurable/unreliable flags from this cached component + const saved = entry & RelationComparisonResult.ReportsMask; + if (saved & RelationComparisonResult.ReportsUnmeasurable) { + instantiateType(source, reportUnmeasurableMarkers); + } + if (saved & RelationComparisonResult.ReportsUnreliable) { + instantiateType(source, reportUnreliableMarkers); + } + } + return entry & RelationComparisonResult.Succeeded ? Ternary.True : Ternary.False; } } - return links.resolvedJsxElementAttributesType; - } - - function getJsxElementClassTypeAt(location: Node): Type | undefined { - const type = getJsxType(JsxNames.ElementClass, location); - if (type === errorType) return undefined; - return type; - } - - function getJsxElementTypeAt(location: Node): Type { - return getJsxType(JsxNames.Element, location); - } - - function getJsxStatelessElementTypeAt(location: Node): Type | undefined { - const jsxElementType = getJsxElementTypeAt(location); - if (jsxElementType) { - return getUnionType([jsxElementType, nullType]); - } - } - - /** - * Returns all the properties of the Jsx.IntrinsicElements interface - */ - function getJsxIntrinsicTagNamesAt(location: Node): Symbol[] { - const intrinsics = getJsxType(JsxNames.IntrinsicElements, location); - return intrinsics ? getPropertiesOfType(intrinsics) : emptyArray; - } - - function checkJsxPreconditions(errorNode: Node) { - // Preconditions for using JSX - if ((compilerOptions.jsx || JsxEmit.None) === JsxEmit.None) { - error(errorNode, Diagnostics.Cannot_use_JSX_unless_the_jsx_flag_is_provided); + if (!maybeKeys) { + maybeKeys = []; + sourceStack = []; + targetStack = []; } - - if (getJsxElementTypeAt(errorNode) === undefined) { - if (noImplicitAny) { - error(errorNode, Diagnostics.JSX_element_implicitly_has_type_any_because_the_global_type_JSX_Element_does_not_exist); + else { + for (let i = 0; i < maybeCount; i++) { + // If source and target are already being compared, consider them related with assumptions + if (id === maybeKeys[i]) { + return Ternary.Maybe; + } + } + if (depth === 100) { + overflow = true; + return Ternary.False; } } - } - - function checkJsxOpeningLikeElementOrOpeningFragment(node: JsxOpeningLikeElement | JsxOpeningFragment) { - const isNodeOpeningLikeElement = isJsxOpeningLikeElement(node); - - if (isNodeOpeningLikeElement) { - checkGrammarJsxElement(node); - } - checkJsxPreconditions(node); - // The reactNamespace/jsxFactory's root symbol should be marked as 'used' so we don't incorrectly elide its import. - // And if there is no reactNamespace/jsxFactory's symbol in scope when targeting React emit, we should issue an error. - const reactRefErr = diagnostics && compilerOptions.jsx === JsxEmit.React ? Diagnostics.Cannot_find_name_0 : undefined; - const reactNamespace = getJsxNamespace(node); - const reactLocation = isNodeOpeningLikeElement ? (node).tagName : node; - const reactSym = resolveName(reactLocation, reactNamespace, SymbolFlags.Value, reactRefErr, reactNamespace, /*isUse*/ true); - if (reactSym) { - // Mark local symbol as referenced here because it might not have been marked - // if jsx emit was not react as there wont be error being emitted - reactSym.isReferenced = SymbolFlags.All; - - // If react symbol is alias, mark it as refereced - if (reactSym.flags & SymbolFlags.Alias && !getTypeOnlyAliasDeclaration(reactSym)) { - markAliasSymbolAsReferenced(reactSym); + const maybeStart = maybeCount; + maybeKeys[maybeCount] = id; + maybeCount++; + sourceStack[depth] = source; + targetStack[depth] = target; + depth++; + const saveExpandingFlags = expandingFlags; + if (!(expandingFlags & ExpandingFlags.Source) && isDeeplyNestedType(source, sourceStack, depth)) + expandingFlags |= ExpandingFlags.Source; + if (!(expandingFlags & ExpandingFlags.Target) && isDeeplyNestedType(target, targetStack, depth)) + expandingFlags |= ExpandingFlags.Target; + let originalHandler: typeof outofbandVarianceMarkerHandler; + let propagatingVarianceFlags: RelationComparisonResult = 0; + if (outofbandVarianceMarkerHandler) { + originalHandler = outofbandVarianceMarkerHandler; + outofbandVarianceMarkerHandler = onlyUnreliable => { + propagatingVarianceFlags |= onlyUnreliable ? RelationComparisonResult.ReportsUnreliable : RelationComparisonResult.ReportsUnmeasurable; + return originalHandler!(onlyUnreliable); + }; + } + const result = expandingFlags !== ExpandingFlags.Both ? structuredTypeRelatedTo(source, target, reportErrors, intersectionState) : Ternary.Maybe; + if (outofbandVarianceMarkerHandler) { + outofbandVarianceMarkerHandler = originalHandler; + } + expandingFlags = saveExpandingFlags; + depth--; + if (result) { + if (result === Ternary.True || depth === 0) { + // If result is definitely true, record all maybe keys as having succeeded + for (let i = maybeStart; i < maybeCount; i++) { + relation.set(maybeKeys[i], RelationComparisonResult.Succeeded | propagatingVarianceFlags); + } + maybeCount = maybeStart; } } - - if (isNodeOpeningLikeElement) { - const sig = getResolvedSignature(node as JsxOpeningLikeElement); - checkJsxReturnAssignableToAppropriateBound(getJsxReferenceKind(node as JsxOpeningLikeElement), getReturnTypeOfSignature(sig), node); + else { + // A false result goes straight into global cache (when something is false under + // assumptions it will also be false without assumptions) + relation.set(id, (reportErrors ? RelationComparisonResult.Reported : 0) | RelationComparisonResult.Failed | propagatingVarianceFlags); + maybeCount = maybeStart; } + return result; } - - /** - * Check if a property with the given name is known anywhere in the given type. In an object type, a property - * is considered known if - * 1. the object type is empty and the check is for assignability, or - * 2. if the object type has index signatures, or - * 3. if the property is actually declared in the object type - * (this means that 'toString', for example, is not usually a known property). - * 4. In a union or intersection type, - * a property is considered known if it is known in any constituent type. - * @param targetType a type to search a given name in - * @param name a property name to search - * @param isComparingJsxAttributes a boolean flag indicating whether we are searching in JsxAttributesType - */ - function isKnownProperty(targetType: Type, name: __String, isComparingJsxAttributes: boolean): boolean { - if (targetType.flags & TypeFlags.Object) { - const resolved = resolveStructuredTypeMembers(targetType as ObjectType); - if (resolved.stringIndexInfo || - resolved.numberIndexInfo && isNumericLiteralName(name) || - getPropertyOfObjectType(targetType, name) || - isComparingJsxAttributes && !isUnhyphenatedJsxName(name)) { - // For JSXAttributes, if the attribute has a hyphenated name, consider that the attribute to be known. - return true; + function structuredTypeRelatedTo(source: ts.Type, target: ts.Type, reportErrors: boolean, intersectionState: IntersectionState): Ternary { + const flags = source.flags & target.flags; + if (relation === identityRelation && !(flags & TypeFlags.Object)) { + if (flags & TypeFlags.Index) { + return isRelatedTo((source).type, (target).type, /*reportErrors*/ false); + } + let result = Ternary.False; + if (flags & TypeFlags.IndexedAccess) { + if (result = isRelatedTo((source).objectType, (target).objectType, /*reportErrors*/ false)) { + if (result &= isRelatedTo((source).indexType, (target).indexType, /*reportErrors*/ false)) { + return result; + } + } + } + if (flags & TypeFlags.Conditional) { + if ((source).root.isDistributive === (target).root.isDistributive) { + if (result = isRelatedTo((source).checkType, (target).checkType, /*reportErrors*/ false)) { + if (result &= isRelatedTo((source).extendsType, (target).extendsType, /*reportErrors*/ false)) { + if (result &= isRelatedTo(getTrueTypeFromConditionalType((source)), getTrueTypeFromConditionalType((target)), /*reportErrors*/ false)) { + if (result &= isRelatedTo(getFalseTypeFromConditionalType((source)), getFalseTypeFromConditionalType((target)), /*reportErrors*/ false)) { + return result; + } + } + } + } + } + } + if (flags & TypeFlags.Substitution) { + return isRelatedTo((source).substitute, (target).substitute, /*reportErrors*/ false); } + return Ternary.False; } - else if (targetType.flags & TypeFlags.UnionOrIntersection && isExcessPropertyCheckTarget(targetType)) { - for (const t of (targetType as UnionOrIntersectionType).types) { - if (isKnownProperty(t, name, isComparingJsxAttributes)) { - return true; + let result: Ternary; + let originalErrorInfo: DiagnosticMessageChain | undefined; + let varianceCheckFailed = false; + const saveErrorInfo = captureErrorCalculationState(); + // We limit alias variance probing to only object and conditional types since their alias behavior + // is more predictable than other, interned types, which may or may not have an alias depending on + // the order in which things were checked. + if (source.flags & (TypeFlags.Object | TypeFlags.Conditional) && source.aliasSymbol && + source.aliasTypeArguments && source.aliasSymbol === target.aliasSymbol && + !(source.aliasTypeArgumentsContainsMarker || target.aliasTypeArgumentsContainsMarker)) { + const variances = getAliasVariances(source.aliasSymbol); + if (variances === emptyArray) { + return Ternary.Maybe; + } + const varianceResult = relateVariances(source.aliasTypeArguments, target.aliasTypeArguments, variances, intersectionState); + if (varianceResult !== undefined) { + return varianceResult; + } + } + if (target.flags & TypeFlags.TypeParameter) { + // A source type { [P in Q]: X } is related to a target type T if keyof T is related to Q and X is related to T[Q]. + if (getObjectFlags(source) & ObjectFlags.Mapped && isRelatedTo(getIndexType(target), getConstraintTypeFromMappedType((source)))) { + if (!(getMappedTypeModifiers((source)) & MappedTypeModifiers.IncludeOptional)) { + const templateType = getTemplateTypeFromMappedType((source)); + const indexedAccessType = getIndexedAccessType(target, getTypeParameterFromMappedType((source))); + if (result = isRelatedTo(templateType, indexedAccessType, reportErrors)) { + return result; + } } } } - return false; - } - - function isExcessPropertyCheckTarget(type: Type): boolean { - return !!(type.flags & TypeFlags.Object && !(getObjectFlags(type) & ObjectFlags.ObjectLiteralPatternWithComputedProperties) || - type.flags & TypeFlags.NonPrimitive || - type.flags & TypeFlags.Union && some((type).types, isExcessPropertyCheckTarget) || - type.flags & TypeFlags.Intersection && every((type).types, isExcessPropertyCheckTarget)); - } - - function checkJsxExpression(node: JsxExpression, checkMode?: CheckMode) { - checkGrammarJsxExpression(node); - if (node.expression) { - const type = checkExpression(node.expression, checkMode); - if (node.dotDotDotToken && type !== anyType && !isArrayType(type)) { - error(node, Diagnostics.JSX_spread_child_must_be_an_array_type); + else if (target.flags & TypeFlags.Index) { + // A keyof S is related to a keyof T if T is related to S. + if (source.flags & TypeFlags.Index) { + if (result = isRelatedTo((target).type, (source).type, /*reportErrors*/ false)) { + return result; + } + } + // A type S is assignable to keyof T if S is assignable to keyof C, where C is the + // simplified form of T or, if T doesn't simplify, the constraint of T. + const constraint = getSimplifiedTypeOrConstraint((target).type); + if (constraint) { + // We require Ternary.True here such that circular constraints don't cause + // false positives. For example, given 'T extends { [K in keyof T]: string }', + // 'keyof T' has itself as its constraint and produces a Ternary.Maybe when + // related to other types. + if (isRelatedTo(source, getIndexType(constraint, (target as IndexType).stringsOnly), reportErrors) === Ternary.True) { + return Ternary.True; + } } - return type; } - else { - return errorType; + else if (target.flags & TypeFlags.IndexedAccess) { + // A type S is related to a type T[K] if S is related to C, where C is the base + // constraint of T[K] for writing. + if (relation !== identityRelation) { + const objectType = (target).objectType; + const indexType = (target).indexType; + const baseObjectType = getBaseConstraintOfType(objectType) || objectType; + const baseIndexType = getBaseConstraintOfType(indexType) || indexType; + if (!isGenericObjectType(baseObjectType) && !isGenericIndexType(baseIndexType)) { + const accessFlags = AccessFlags.Writing | (baseObjectType !== objectType ? AccessFlags.NoIndexSignatures : 0); + const constraint = getIndexedAccessTypeOrUndefined(baseObjectType, baseIndexType, /*accessNode*/ undefined, accessFlags); + if (constraint && (result = isRelatedTo(source, constraint, reportErrors))) { + return result; + } + } + } } - } - - function getDeclarationNodeFlagsFromSymbol(s: Symbol): NodeFlags { - return s.valueDeclaration ? getCombinedNodeFlags(s.valueDeclaration) : 0; - } - - /** - * Return whether this symbol is a member of a prototype somewhere - * Note that this is not tracked well within the compiler, so the answer may be incorrect. - */ - function isPrototypeProperty(symbol: Symbol) { - if (symbol.flags & SymbolFlags.Method || getCheckFlags(symbol) & CheckFlags.SyntheticMethod) { - return true; + else if (isGenericMappedType(target)) { + // A source type T is related to a target type { [P in X]: T[P] } + const template = getTemplateTypeFromMappedType(target); + const modifiers = getMappedTypeModifiers(target); + if (!(modifiers & MappedTypeModifiers.ExcludeOptional)) { + if (template.flags & TypeFlags.IndexedAccess && (template).objectType === source && + (template).indexType === getTypeParameterFromMappedType(target)) { + return Ternary.True; + } + if (!isGenericMappedType(source)) { + const targetConstraint = getConstraintTypeFromMappedType(target); + const sourceKeys = getIndexType(source, /*stringsOnly*/ undefined, /*noIndexSignatures*/ true); + const includeOptional = modifiers & MappedTypeModifiers.IncludeOptional; + const filteredByApplicability = includeOptional ? intersectTypes(targetConstraint, sourceKeys) : undefined; + // A source type T is related to a target type { [P in Q]: X } if Q is related to keyof T and T[Q] is related to X. + // A source type T is related to a target type { [P in Q]?: X } if some constituent Q' of Q is related to keyof T and T[Q'] is related to X. + if (includeOptional + ? !(filteredByApplicability!.flags & TypeFlags.Never) + : isRelatedTo(targetConstraint, sourceKeys)) { + const typeParameter = getTypeParameterFromMappedType(target); + const indexingType = filteredByApplicability ? getIntersectionType([filteredByApplicability, typeParameter]) : typeParameter; + const indexedAccessType = getIndexedAccessType(source, indexingType); + const templateType = getTemplateTypeFromMappedType(target); + if (result = isRelatedTo(indexedAccessType, templateType, reportErrors)) { + return result; + } + } + originalErrorInfo = errorInfo; + resetErrorInfo(saveErrorInfo); + } + } } - if (isInJSFile(symbol.valueDeclaration)) { - const parent = symbol.valueDeclaration.parent; - return parent && isBinaryExpression(parent) && - getAssignmentDeclarationKind(parent) === AssignmentDeclarationKind.PrototypeProperty; + if (source.flags & TypeFlags.TypeVariable) { + if (source.flags & TypeFlags.IndexedAccess && target.flags & TypeFlags.IndexedAccess) { + // A type S[K] is related to a type T[J] if S is related to T and K is related to J. + if (result = isRelatedTo((source).objectType, (target).objectType, reportErrors)) { + result &= isRelatedTo((source).indexType, (target).indexType, reportErrors); + } + if (result) { + resetErrorInfo(saveErrorInfo); + return result; + } + } + else { + const constraint = getConstraintOfType((source)); + if (!constraint || (source.flags & TypeFlags.TypeParameter && constraint.flags & TypeFlags.Any)) { + // A type variable with no constraint is not related to the non-primitive object type. + if (result = isRelatedTo(emptyObjectType, extractTypesOfKind(target, ~TypeFlags.NonPrimitive))) { + resetErrorInfo(saveErrorInfo); + return result; + } + } + // hi-speed no-this-instantiation check (less accurate, but avoids costly `this`-instantiation when the constraint will suffice), see #28231 for report on why this is needed + else if (result = isRelatedTo(constraint, target, /*reportErrors*/ false, /*headMessage*/ undefined, intersectionState)) { + resetErrorInfo(saveErrorInfo); + return result; + } + // slower, fuller, this-instantiated check (necessary when comparing raw `this` types from base classes), see `subclassWithPolymorphicThisIsAssignable.ts` test for example + else if (result = isRelatedTo(getTypeWithThisArgument(constraint, source), target, reportErrors, /*headMessage*/ undefined, intersectionState)) { + resetErrorInfo(saveErrorInfo); + return result; + } + } } - } - - /** - * Check whether the requested property access is valid. - * Returns true if node is a valid property access, and false otherwise. - * @param node The node to be checked. - * @param isSuper True if the access is from `super.`. - * @param type The type of the object whose property is being accessed. (Not the type of the property.) - * @param prop The symbol for the property being accessed. - */ - function checkPropertyAccessibility( - node: PropertyAccessExpression | QualifiedName | PropertyAccessExpression | VariableDeclaration | ParameterDeclaration | ImportTypeNode | PropertyAssignment | ShorthandPropertyAssignment | BindingElement, - isSuper: boolean, type: Type, prop: Symbol): boolean { - const flags = getDeclarationModifierFlagsFromSymbol(prop); - const errorNode = node.kind === SyntaxKind.QualifiedName ? node.right : node.kind === SyntaxKind.ImportType ? node : node.name; - - if (getCheckFlags(prop) & CheckFlags.ContainsPrivate) { - // Synthetic property with private constituent property - error(errorNode, Diagnostics.Property_0_has_conflicting_declarations_and_is_inaccessible_in_type_1, symbolToString(prop), typeToString(type)); - return false; + else if (source.flags & TypeFlags.Index) { + if (result = isRelatedTo(keyofConstraintType, target, reportErrors)) { + resetErrorInfo(saveErrorInfo); + return result; + } } - - if (isSuper) { - // TS 1.0 spec (April 2014): 4.8.2 - // - In a constructor, instance member function, instance member accessor, or - // instance member variable initializer where this references a derived class instance, - // a super property access is permitted and must specify a public instance member function of the base class. - // - In a static member function or static member accessor - // where this references the constructor function object of a derived class, - // a super property access is permitted and must specify a public static member function of the base class. - if (languageVersion < ScriptTarget.ES2015) { - if (symbolHasNonMethodDeclaration(prop)) { - error(errorNode, Diagnostics.Only_public_and_protected_methods_of_the_base_class_are_accessible_via_the_super_keyword); - return false; + else if (source.flags & TypeFlags.Conditional) { + if (target.flags & TypeFlags.Conditional) { + // Two conditional types 'T1 extends U1 ? X1 : Y1' and 'T2 extends U2 ? X2 : Y2' are related if + // one of T1 and T2 is related to the other, U1 and U2 are identical types, X1 is related to X2, + // and Y1 is related to Y2. + const sourceParams = (source as ConditionalType).root.inferTypeParameters; + let sourceExtends = (source).extendsType; + let mapper: TypeMapper | undefined; + if (sourceParams) { + // If the source has infer type parameters, we instantiate them in the context of the target + const ctx = createInferenceContext(sourceParams, /*signature*/ undefined, InferenceFlags.None, isRelatedTo); + inferTypes(ctx.inferences, (target).extendsType, sourceExtends, InferencePriority.NoConstraints | InferencePriority.AlwaysStrict); + sourceExtends = instantiateType(sourceExtends, ctx.mapper); + mapper = ctx.mapper; + } + if (isTypeIdenticalTo(sourceExtends, (target).extendsType) && + (isRelatedTo((source).checkType, (target).checkType) || isRelatedTo((target).checkType, (source).checkType))) { + if (result = isRelatedTo(instantiateType(getTrueTypeFromConditionalType((source)), mapper), getTrueTypeFromConditionalType((target)), reportErrors)) { + result &= isRelatedTo(getFalseTypeFromConditionalType((source)), getFalseTypeFromConditionalType((target)), reportErrors); + } + if (result) { + resetErrorInfo(saveErrorInfo); + return result; + } } } - if (flags & ModifierFlags.Abstract) { - // A method cannot be accessed in a super property access if the method is abstract. - // This error could mask a private property access error. But, a member - // cannot simultaneously be private and abstract, so this will trigger an - // additional error elsewhere. - error(errorNode, Diagnostics.Abstract_method_0_in_class_1_cannot_be_accessed_via_super_expression, symbolToString(prop), typeToString(getDeclaringClass(prop)!)); - return false; + else { + const distributiveConstraint = getConstraintOfDistributiveConditionalType((source)); + if (distributiveConstraint) { + if (result = isRelatedTo(distributiveConstraint, target, reportErrors)) { + resetErrorInfo(saveErrorInfo); + return result; + } + } + const defaultConstraint = getDefaultConstraintOfConditionalType((source)); + if (defaultConstraint) { + if (result = isRelatedTo(defaultConstraint, target, reportErrors)) { + resetErrorInfo(saveErrorInfo); + return result; + } + } } } - - // Referencing abstract properties within their own constructors is not allowed - if ((flags & ModifierFlags.Abstract) && isThisProperty(node) && symbolHasNonMethodDeclaration(prop)) { - const declaringClassDeclaration = getClassLikeDeclarationOfSymbol(getParentOfSymbol(prop)!); - if (declaringClassDeclaration && isNodeUsedDuringClassInitialization(node)) { - error(errorNode, Diagnostics.Abstract_property_0_in_class_1_cannot_be_accessed_in_the_constructor, symbolToString(prop), getTextOfIdentifierOrLiteral(declaringClassDeclaration.name!)); // TODO: GH#18217 - return false; + else { + // An empty object type is related to any mapped type that includes a '?' modifier. + if (relation !== subtypeRelation && relation !== strictSubtypeRelation && isPartialMappedType(target) && isEmptyObjectType(source)) { + return Ternary.True; + } + if (isGenericMappedType(target)) { + if (isGenericMappedType(source)) { + if (result = mappedTypeRelatedTo(source, target, reportErrors)) { + resetErrorInfo(saveErrorInfo); + return result; + } + } + return Ternary.False; + } + const sourceIsPrimitive = !!(source.flags & TypeFlags.Primitive); + if (relation !== identityRelation) { + source = getApparentType(source); + } + else if (isGenericMappedType(source)) { + return Ternary.False; + } + if (getObjectFlags(source) & ObjectFlags.Reference && getObjectFlags(target) & ObjectFlags.Reference && (source).target === (target).target && + !(getObjectFlags(source) & ObjectFlags.MarkerType || getObjectFlags(target) & ObjectFlags.MarkerType)) { + // We have type references to the same generic type, and the type references are not marker + // type references (which are intended by be compared structurally). Obtain the variance + // information for the type parameters and relate the type arguments accordingly. + const variances = getVariances((source).target); + // We return Ternary.Maybe for a recursive invocation of getVariances (signalled by emptyArray). This + // effectively means we measure variance only from type parameter occurrences that aren't nested in + // recursive instantiations of the generic type. + if (variances === emptyArray) { + return Ternary.Maybe; + } + const varianceResult = relateVariances(getTypeArguments((source)), getTypeArguments((target)), variances, intersectionState); + if (varianceResult !== undefined) { + return varianceResult; + } + } + else if (isReadonlyArrayType(target) ? isArrayType(source) || isTupleType(source) : isArrayType(target) && isTupleType(source) && !source.target.readonly) { + if (relation !== identityRelation) { + return isRelatedTo(getIndexTypeOfType(source, IndexKind.Number) || anyType, getIndexTypeOfType(target, IndexKind.Number) || anyType, reportErrors); + } + else { + // By flags alone, we know that the `target` is a readonly array while the source is a normal array or tuple + // or `target` is an array and source is a tuple - in both cases the types cannot be identical, by construction + return Ternary.False; + } + } + // Consider a fresh empty object literal type "closed" under the subtype relationship - this way `{} <- {[idx: string]: any} <- fresh({})` + // and not `{} <- fresh({}) <- {[idx: string]: any}` + else if ((relation === subtypeRelation || relation === strictSubtypeRelation) && isEmptyObjectType(target) && getObjectFlags(target) & ObjectFlags.FreshLiteral && !isEmptyObjectType(source)) { + return Ternary.False; + } + // Even if relationship doesn't hold for unions, intersections, or generic type references, + // it may hold in a structural comparison. + // In a check of the form X = A & B, we will have previously checked if A relates to X or B relates + // to X. Failing both of those we want to check if the aggregation of A and B's members structurally + // relates to X. Thus, we include intersection types on the source side here. + if (source.flags & (TypeFlags.Object | TypeFlags.Intersection) && target.flags & TypeFlags.Object) { + // Report structural errors only if we haven't reported any errors yet + const reportStructuralErrors = reportErrors && errorInfo === saveErrorInfo.errorInfo && !sourceIsPrimitive; + result = propertiesRelatedTo(source, target, reportStructuralErrors, /*excludedProperties*/ undefined, intersectionState); + if (result) { + result &= signaturesRelatedTo(source, target, SignatureKind.Call, reportStructuralErrors); + if (result) { + result &= signaturesRelatedTo(source, target, SignatureKind.Construct, reportStructuralErrors); + if (result) { + result &= indexTypesRelatedTo(source, target, IndexKind.String, sourceIsPrimitive, reportStructuralErrors, intersectionState); + if (result) { + result &= indexTypesRelatedTo(source, target, IndexKind.Number, sourceIsPrimitive, reportStructuralErrors, intersectionState); + } + } + } + } + if (varianceCheckFailed && result) { + errorInfo = originalErrorInfo || errorInfo || saveErrorInfo.errorInfo; // Use variance error (there is no structural one) and return false + } + else if (result) { + return result; + } + } + // If S is an object type and T is a discriminated union, S may be related to T if + // there exists a constituent of T for every combination of the discriminants of S + // with respect to T. We do not report errors here, as we will use the existing + // error result from checking each constituent of the union. + if (source.flags & (TypeFlags.Object | TypeFlags.Intersection) && target.flags & TypeFlags.Union) { + const objectOnlyTarget = extractTypesOfKind(target, TypeFlags.Object | TypeFlags.Intersection | TypeFlags.Substitution); + if (objectOnlyTarget.flags & TypeFlags.Union) { + const result = typeRelatedToDiscriminatedType(source, (objectOnlyTarget as UnionType)); + if (result) { + return result; + } + } } } - - if (isPropertyAccessExpression(node) && isPrivateIdentifier(node.name)) { - if (!getContainingClass(node)) { - error(errorNode, Diagnostics.Private_identifiers_are_not_allowed_outside_class_bodies); - return false; + return Ternary.False; + function relateVariances(sourceTypeArguments: readonly ts.Type[] | undefined, targetTypeArguments: readonly ts.Type[] | undefined, variances: VarianceFlags[], intersectionState: IntersectionState) { + if (result = typeArgumentsRelatedTo(sourceTypeArguments, targetTypeArguments, variances, reportErrors, intersectionState)) { + return result; } - return true; - } - - // Public properties are otherwise accessible. - if (!(flags & ModifierFlags.NonPublicAccessibilityModifier)) { - return true; - } - - // Property is known to be private or protected at this point - - // Private property is accessible if the property is within the declaring class - if (flags & ModifierFlags.Private) { - const declaringClassDeclaration = getClassLikeDeclarationOfSymbol(getParentOfSymbol(prop)!)!; - if (!isNodeWithinClass(node, declaringClassDeclaration)) { - error(errorNode, Diagnostics.Property_0_is_private_and_only_accessible_within_class_1, symbolToString(prop), typeToString(getDeclaringClass(prop)!)); - return false; + if (some(variances, v => !!(v & VarianceFlags.AllowsStructuralFallback))) { + // If some type parameter was `Unmeasurable` or `Unreliable`, and we couldn't pass by assuming it was identical, then we + // have to allow a structural fallback check + // We elide the variance-based error elaborations, since those might not be too helpful, since we'll potentially + // be assuming identity of the type parameter. + originalErrorInfo = undefined; + resetErrorInfo(saveErrorInfo); + return undefined; } - return true; - } - - // Property is known to be protected at this point - - // All protected properties of a supertype are accessible in a super access - if (isSuper) { - return true; - } - - // Find the first enclosing class that has the declaring classes of the protected constituents - // of the property as base classes - let enclosingClass = forEachEnclosingClass(node, enclosingDeclaration => { - const enclosingClass = getDeclaredTypeOfSymbol(getSymbolOfNode(enclosingDeclaration)!); - return isClassDerivedFromDeclaringClasses(enclosingClass, prop) ? enclosingClass : undefined; - }); - // A protected property is accessible if the property is within the declaring class or classes derived from it - if (!enclosingClass) { - // allow PropertyAccessibility if context is in function with this parameter - // static member access is disallow - let thisParameter: ParameterDeclaration | undefined; - if (flags & ModifierFlags.Static || !(thisParameter = getThisParameterFromNodeContext(node)) || !thisParameter.type) { - error(errorNode, Diagnostics.Property_0_is_protected_and_only_accessible_within_class_1_and_its_subclasses, symbolToString(prop), typeToString(getDeclaringClass(prop) || type)); - return false; + const allowStructuralFallback = targetTypeArguments && hasCovariantVoidArgument(targetTypeArguments, variances); + varianceCheckFailed = !allowStructuralFallback; + // The type arguments did not relate appropriately, but it may be because we have no variance + // information (in which case typeArgumentsRelatedTo defaulted to covariance for all type + // arguments). It might also be the case that the target type has a 'void' type argument for + // a covariant type parameter that is only used in return positions within the generic type + // (in which case any type argument is permitted on the source side). In those cases we proceed + // with a structural comparison. Otherwise, we know for certain the instantiations aren't + // related and we can return here. + if (variances !== emptyArray && !allowStructuralFallback) { + // In some cases generic types that are covariant in regular type checking mode become + // invariant in --strictFunctionTypes mode because one or more type parameters are used in + // both co- and contravariant positions. In order to make it easier to diagnose *why* such + // types are invariant, if any of the type parameters are invariant we reset the reported + // errors and instead force a structural comparison (which will include elaborations that + // reveal the reason). + // We can switch on `reportErrors` here, since varianceCheckFailed guarantees we return `False`, + // we can return `False` early here to skip calculating the structural error message we don't need. + if (varianceCheckFailed && !(reportErrors && some(variances, v => (v & VarianceFlags.VarianceMask) === VarianceFlags.Invariant))) { + return Ternary.False; + } + // We remember the original error information so we can restore it in case the structural + // comparison unexpectedly succeeds. This can happen when the structural comparison result + // is a Ternary.Maybe for example caused by the recursion depth limiter. + originalErrorInfo = errorInfo; + resetErrorInfo(saveErrorInfo); } - - const thisType = getTypeFromTypeNode(thisParameter.type); - enclosingClass = (((thisType.flags & TypeFlags.TypeParameter) ? getConstraintOfTypeParameter(thisType) : thisType) as TypeReference).target; - } - // No further restrictions for static properties - if (flags & ModifierFlags.Static) { - return true; - } - if (type.flags & TypeFlags.TypeParameter) { - // get the original type -- represented as the type constraint of the 'this' type - type = (type as TypeParameter).isThisType ? getConstraintOfTypeParameter(type)! : getBaseConstraintOfType(type)!; // TODO: GH#18217 Use a different variable that's allowed to be undefined - } - if (!type || !hasBaseType(type, enclosingClass)) { - error(errorNode, Diagnostics.Property_0_is_protected_and_only_accessible_through_an_instance_of_class_1, symbolToString(prop), typeToString(enclosingClass)); - return false; - } - return true; - } - - function getThisParameterFromNodeContext(node: Node) { - const thisContainer = getThisContainer(node, /* includeArrowFunctions */ false); - return thisContainer && isFunctionLike(thisContainer) ? getThisParameter(thisContainer) : undefined; - } - - function symbolHasNonMethodDeclaration(symbol: Symbol) { - return !!forEachProperty(symbol, prop => !(prop.flags & SymbolFlags.Method)); - } - - function checkNonNullExpression(node: Expression | QualifiedName) { - return checkNonNullType(checkExpression(node), node); - } - - function isNullableType(type: Type) { - return !!((strictNullChecks ? getFalsyFlags(type) : type.flags) & TypeFlags.Nullable); - } - - function getNonNullableTypeIfNeeded(type: Type) { - return isNullableType(type) ? getNonNullableType(type) : type; - } - - function reportObjectPossiblyNullOrUndefinedError(node: Node, flags: TypeFlags) { - error(node, flags & TypeFlags.Undefined ? flags & TypeFlags.Null ? - Diagnostics.Object_is_possibly_null_or_undefined : - Diagnostics.Object_is_possibly_undefined : - Diagnostics.Object_is_possibly_null - ); - } - - function reportCannotInvokePossiblyNullOrUndefinedError(node: Node, flags: TypeFlags) { - error(node, flags & TypeFlags.Undefined ? flags & TypeFlags.Null ? - Diagnostics.Cannot_invoke_an_object_which_is_possibly_null_or_undefined : - Diagnostics.Cannot_invoke_an_object_which_is_possibly_undefined : - Diagnostics.Cannot_invoke_an_object_which_is_possibly_null - ); - } - - function checkNonNullTypeWithReporter( - type: Type, - node: Node, - reportError: (node: Node, kind: TypeFlags) => void - ): Type { - if (strictNullChecks && type.flags & TypeFlags.Unknown) { - error(node, Diagnostics.Object_is_of_type_unknown); - return errorType; - } - const kind = (strictNullChecks ? getFalsyFlags(type) : type.flags) & TypeFlags.Nullable; - if (kind) { - reportError(node, kind); - const t = getNonNullableType(type); - return t.flags & (TypeFlags.Nullable | TypeFlags.Never) ? errorType : t; } - return type; - } - - function checkNonNullType(type: Type, node: Node) { - return checkNonNullTypeWithReporter(type, node, reportObjectPossiblyNullOrUndefinedError); } - - function checkNonNullNonVoidType(type: Type, node: Node): Type { - const nonNullType = checkNonNullType(type, node); - if (nonNullType !== errorType && nonNullType.flags & TypeFlags.Void) { - error(node, Diagnostics.Object_is_possibly_undefined); + function reportUnmeasurableMarkers(p: TypeParameter) { + if (outofbandVarianceMarkerHandler && (p === markerSuperType || p === markerSubType || p === markerOtherType)) { + outofbandVarianceMarkerHandler(/*onlyUnreliable*/ false); } - return nonNullType; + return p; } - - function checkPropertyAccessExpression(node: PropertyAccessExpression) { - return node.flags & NodeFlags.OptionalChain ? checkPropertyAccessChain(node as PropertyAccessChain) : - checkPropertyAccessExpressionOrQualifiedName(node, node.expression, checkNonNullExpression(node.expression), node.name); - } - - function checkPropertyAccessChain(node: PropertyAccessChain) { - const leftType = checkExpression(node.expression); - const nonOptionalType = getOptionalExpressionType(leftType, node.expression); - return propagateOptionalTypeMarker(checkPropertyAccessExpressionOrQualifiedName(node, node.expression, checkNonNullType(nonOptionalType, node.expression), node.name), node, nonOptionalType !== leftType); - } - - function checkQualifiedName(node: QualifiedName) { - return checkPropertyAccessExpressionOrQualifiedName(node, node.left, checkNonNullExpression(node.left), node.right); - } - - function isMethodAccessForCall(node: Node) { - while (node.parent.kind === SyntaxKind.ParenthesizedExpression) { - node = node.parent; + function reportUnreliableMarkers(p: TypeParameter) { + if (outofbandVarianceMarkerHandler && (p === markerSuperType || p === markerSubType || p === markerOtherType)) { + outofbandVarianceMarkerHandler(/*onlyUnreliable*/ true); } - return isCallOrNewExpression(node.parent) && node.parent.expression === node; + return p; } - - // Lookup the private identifier lexically. - function lookupSymbolForPrivateIdentifierDeclaration(propName: __String, location: Node): Symbol | undefined { - for (let containingClass = getContainingClass(location); !!containingClass; containingClass = getContainingClass(containingClass)) { - const { symbol } = containingClass; - const name = getSymbolNameForPrivateIdentifier(symbol, propName); - const prop = (symbol.members && symbol.members.get(name)) || (symbol.exports && symbol.exports.get(name)); - if (prop) { - return prop; + // A type [P in S]: X is related to a type [Q in T]: Y if T is related to S and X' is + // related to Y, where X' is an instantiation of X in which P is replaced with Q. Notice + // that S and T are contra-variant whereas X and Y are co-variant. + function mappedTypeRelatedTo(source: MappedType, target: MappedType, reportErrors: boolean): Ternary { + const modifiersRelated = relation === comparableRelation || (relation === identityRelation ? getMappedTypeModifiers(source) === getMappedTypeModifiers(target) : + getCombinedMappedTypeOptionality(source) <= getCombinedMappedTypeOptionality(target)); + if (modifiersRelated) { + let result: Ternary; + const targetConstraint = getConstraintTypeFromMappedType(target); + const sourceConstraint = instantiateType(getConstraintTypeFromMappedType(source), getCombinedMappedTypeOptionality(source) < 0 ? reportUnmeasurableMarkers : reportUnreliableMarkers); + if (result = isRelatedTo(targetConstraint, sourceConstraint, reportErrors)) { + const mapper = createTypeMapper([getTypeParameterFromMappedType(source)], [getTypeParameterFromMappedType(target)]); + return result & isRelatedTo(instantiateType(getTemplateTypeFromMappedType(source), mapper), getTemplateTypeFromMappedType(target), reportErrors); + } + } + return Ternary.False; + } + function typeRelatedToDiscriminatedType(source: ts.Type, target: UnionType) { + // 1. Generate the combinations of discriminant properties & types 'source' can satisfy. + // a. If the number of combinations is above a set limit, the comparison is too complex. + // 2. Filter 'target' to the subset of types whose discriminants exist in the matrix. + // a. If 'target' does not satisfy all discriminants in the matrix, 'source' is not related. + // 3. For each type in the filtered 'target', determine if all non-discriminant properties of + // 'target' are related to a property in 'source'. + // + // NOTE: See ~/tests/cases/conformance/types/typeRelationships/assignmentCompatibility/assignmentCompatWithDiscriminatedUnion.ts + // for examples. + const sourceProperties = getPropertiesOfType(source); + const sourcePropertiesFiltered = findDiscriminantProperties(sourceProperties, target); + if (!sourcePropertiesFiltered) + return Ternary.False; + // Though we could compute the number of combinations as we generate + // the matrix, this would incur additional memory overhead due to + // array allocations. To reduce this overhead, we first compute + // the number of combinations to ensure we will not surpass our + // fixed limit before incurring the cost of any allocations: + let numCombinations = 1; + for (const sourceProperty of sourcePropertiesFiltered) { + numCombinations *= countTypes(getTypeOfSymbol(sourceProperty)); + if (numCombinations > 25) { + // We've reached the complexity limit. + return Ternary.False; } } - } - - function getPrivateIdentifierPropertyOfType(leftType: Type, lexicallyScopedIdentifier: Symbol): Symbol | undefined { - return getPropertyOfType(leftType, lexicallyScopedIdentifier.escapedName); - } - - function checkPrivateIdentifierPropertyAccess(leftType: Type, right: PrivateIdentifier, lexicallyScopedIdentifier: Symbol | undefined): boolean { - // Either the identifier could not be looked up in the lexical scope OR the lexically scoped identifier did not exist on the type. - // Find a private identifier with the same description on the type. - let propertyOnType: Symbol | undefined; - const properties = getPropertiesOfType(leftType); - if (properties) { - forEach(properties, (symbol: Symbol) => { - const decl = symbol.valueDeclaration; - if (decl && isNamedDeclaration(decl) && isPrivateIdentifier(decl.name) && decl.name.escapedText === right.escapedText) { - propertyOnType = symbol; - return true; + // Compute the set of types for each discriminant property. + const sourceDiscriminantTypes: ts.Type[][] = new Array(sourcePropertiesFiltered.length); + const excludedProperties = createUnderscoreEscapedMap(); + for (let i = 0; i < sourcePropertiesFiltered.length; i++) { + const sourceProperty = sourcePropertiesFiltered[i]; + const sourcePropertyType = getTypeOfSymbol(sourceProperty); + sourceDiscriminantTypes[i] = sourcePropertyType.flags & TypeFlags.Union + ? (sourcePropertyType as UnionType).types + : [sourcePropertyType]; + excludedProperties.set(sourceProperty.escapedName, true); + } + // Match each combination of the cartesian product of discriminant properties to one or more + // constituents of 'target'. If any combination does not have a match then 'source' is not relatable. + const discriminantCombinations = cartesianProduct(sourceDiscriminantTypes); + const matchingTypes: ts.Type[] = []; + for (const combination of discriminantCombinations) { + let hasMatch = false; + outer: for (const type of target.types) { + for (let i = 0; i < sourcePropertiesFiltered.length; i++) { + const sourceProperty = sourcePropertiesFiltered[i]; + const targetProperty = getPropertyOfType(type, sourceProperty.escapedName); + if (!targetProperty) + continue outer; + if (sourceProperty === targetProperty) + continue; + // We compare the source property to the target in the context of a single discriminant type. + const related = propertyRelatedTo(source, target, sourceProperty, targetProperty, _ => combination[i], /*reportErrors*/ false, IntersectionState.None); + // If the target property could not be found, or if the properties were not related, + // then this constituent is not a match. + if (!related) { + continue outer; + } } - }); + pushIfUnique(matchingTypes, type, equateValues); + hasMatch = true; + } + if (!hasMatch) { + // We failed to match any type for this combination. + return Ternary.False; + } } - const diagName = diagnosticName(right); - if (propertyOnType) { - const typeValueDecl = propertyOnType.valueDeclaration; - const typeClass = getContainingClass(typeValueDecl); - Debug.assert(!!typeClass); - // We found a private identifier property with the same description. - // Either: - // - There is a lexically scoped private identifier AND it shadows the one we found on the type. - // - It is an attempt to access the private identifier outside of the class. - if (lexicallyScopedIdentifier) { - const lexicalValueDecl = lexicallyScopedIdentifier.valueDeclaration; - const lexicalClass = getContainingClass(lexicalValueDecl); - Debug.assert(!!lexicalClass); - if (findAncestor(lexicalClass, n => typeClass === n)) { - const diagnostic = error( - right, - Diagnostics.The_property_0_cannot_be_accessed_on_type_1_within_this_class_because_it_is_shadowed_by_another_private_identifier_with_the_same_spelling, - diagName, - typeToString(leftType) - ); - - addRelatedInfo( - diagnostic, - createDiagnosticForNode( - lexicalValueDecl, - Diagnostics.The_shadowing_declaration_of_0_is_defined_here, - diagName - ), - createDiagnosticForNode( - typeValueDecl, - Diagnostics.The_declaration_of_0_that_you_probably_intended_to_use_is_defined_here, - diagName - ) - ); - return true; + // Compare the remaining non-discriminant properties of each match. + let result = Ternary.True; + for (const type of matchingTypes) { + result &= propertiesRelatedTo(source, type, /*reportErrors*/ false, excludedProperties, IntersectionState.None); + if (result) { + result &= signaturesRelatedTo(source, type, SignatureKind.Call, /*reportStructuralErrors*/ false); + if (result) { + result &= signaturesRelatedTo(source, type, SignatureKind.Construct, /*reportStructuralErrors*/ false); + if (result) { + result &= indexTypesRelatedTo(source, type, IndexKind.String, /*sourceIsPrimitive*/ false, /*reportStructuralErrors*/ false, IntersectionState.None); + if (result) { + result &= indexTypesRelatedTo(source, type, IndexKind.Number, /*sourceIsPrimitive*/ false, /*reportStructuralErrors*/ false, IntersectionState.None); + } + } } } - error( - right, - Diagnostics.Property_0_is_not_accessible_outside_class_1_because_it_has_a_private_identifier, - diagName, - diagnosticName(typeClass.name || anon) - ); - return true; + if (!result) { + return result; + } } - return false; + return result; } - - function checkPropertyAccessExpressionOrQualifiedName(node: PropertyAccessExpression | QualifiedName, left: Expression | QualifiedName, leftType: Type, right: Identifier | PrivateIdentifier) { - const parentSymbol = getNodeLinks(left).resolvedSymbol; - const assignmentKind = getAssignmentTargetKind(node); - const apparentType = getApparentType(assignmentKind !== AssignmentKind.None || isMethodAccessForCall(node) ? getWidenedType(leftType) : leftType); - if (isPrivateIdentifier(right)) { - checkExternalEmitHelpers(node, ExternalEmitHelpers.ClassPrivateFieldGet); - } - const isAnyLike = isTypeAny(apparentType) || apparentType === silentNeverType; - let prop: Symbol | undefined; - if (isPrivateIdentifier(right)) { - const lexicallyScopedSymbol = lookupSymbolForPrivateIdentifierDeclaration(right.escapedText, right); - if (isAnyLike) { - if (lexicallyScopedSymbol) { - return apparentType; - } - if (!getContainingClass(right)) { - grammarErrorOnNode(right, Diagnostics.Private_identifiers_are_not_allowed_outside_class_bodies); - return anyType; + function excludeProperties(properties: ts.Symbol[], excludedProperties: UnderscoreEscapedMap | undefined) { + if (!excludedProperties || properties.length === 0) + return properties; + let result: ts.Symbol[] | undefined; + for (let i = 0; i < properties.length; i++) { + if (!excludedProperties.has(properties[i].escapedName)) { + if (result) { + result.push(properties[i]); } } - prop = lexicallyScopedSymbol ? getPrivateIdentifierPropertyOfType(leftType, lexicallyScopedSymbol) : undefined; - // Check for private-identifier-specific shadowing and lexical-scoping errors. - if (!prop && checkPrivateIdentifierPropertyAccess(leftType, right, lexicallyScopedSymbol)) { - return errorType; + else if (!result) { + result = properties.slice(0, i); } } - else { - if (isAnyLike) { - if (isIdentifier(left) && parentSymbol) { - markAliasReferenced(parentSymbol, node); + return result || properties; + } + function isPropertySymbolTypeRelated(sourceProp: ts.Symbol, targetProp: ts.Symbol, getTypeOfSourceProperty: (sym: ts.Symbol) => ts.Type, reportErrors: boolean, intersectionState: IntersectionState): Ternary { + const targetIsOptional = strictNullChecks && !!(getCheckFlags(targetProp) & CheckFlags.Partial); + const source = getTypeOfSourceProperty(sourceProp); + if (getCheckFlags(targetProp) & CheckFlags.DeferredType && !getSymbolLinks(targetProp).type) { + // Rather than resolving (and normalizing) the type, relate constituent-by-constituent without performing normalization or seconadary passes + const links = getSymbolLinks(targetProp); + Debug.assertIsDefined(links.deferralParent); + Debug.assertIsDefined(links.deferralConstituents); + const unionParent = !!(links.deferralParent.flags & TypeFlags.Union); + let result = unionParent ? Ternary.False : Ternary.True; + const targetTypes = links.deferralConstituents; + for (const targetType of targetTypes) { + const related = isRelatedTo(source, targetType, /*reportErrors*/ false, /*headMessage*/ undefined, unionParent ? 0 : IntersectionState.Target); + if (!unionParent) { + if (!related) { + // Can't assign to a target individually - have to fallback to assigning to the _whole_ intersection (which forces normalization) + return isRelatedTo(source, addOptionality(getTypeOfSymbol(targetProp), targetIsOptional), reportErrors); + } + result &= related; } - return apparentType; + else { + if (related) { + return related; + } + } + } + if (unionParent && !result && targetIsOptional) { + result = isRelatedTo(source, undefinedType); } - prop = getPropertyOfType(apparentType, right.escapedText); + if (unionParent && !result && reportErrors) { + // The easiest way to get the right errors here is to un-defer (which may be costly) + // If it turns out this is too costly too often, we can replicate the error handling logic within + // typeRelatedToSomeType without the discriminatable type branch (as that requires a manifest union + // type on which to hand discriminable properties, which we are expressly trying to avoid here) + return isRelatedTo(source, addOptionality(getTypeOfSymbol(targetProp), targetIsOptional), reportErrors); + } + return result; } - if (isIdentifier(left) && parentSymbol && !(prop && isConstEnumOrConstEnumOnlyModule(prop))) { - markAliasReferenced(parentSymbol, node); + else { + return isRelatedTo(source, addOptionality(getTypeOfSymbol(targetProp), targetIsOptional), reportErrors, /*headMessage*/ undefined, intersectionState); } - - let propType: Type; - if (!prop) { - const indexInfo = !isPrivateIdentifier(right) && (assignmentKind === AssignmentKind.None || !isGenericObjectType(leftType) || isThisTypeParameter(leftType)) ? getIndexInfoOfType(apparentType, IndexKind.String) : undefined; - if (!(indexInfo && indexInfo.type)) { - if (isJSLiteralType(leftType)) { - return anyType; + } + function propertyRelatedTo(source: ts.Type, target: ts.Type, sourceProp: ts.Symbol, targetProp: ts.Symbol, getTypeOfSourceProperty: (sym: ts.Symbol) => ts.Type, reportErrors: boolean, intersectionState: IntersectionState): Ternary { + const sourcePropFlags = getDeclarationModifierFlagsFromSymbol(sourceProp); + const targetPropFlags = getDeclarationModifierFlagsFromSymbol(targetProp); + if (sourcePropFlags & ModifierFlags.Private || targetPropFlags & ModifierFlags.Private) { + const hasDifferingDeclarations = sourceProp.valueDeclaration !== targetProp.valueDeclaration; + if (getCheckFlags(sourceProp) & CheckFlags.ContainsPrivate && hasDifferingDeclarations) { + if (reportErrors) { + reportError(Diagnostics.Property_0_has_conflicting_declarations_and_is_inaccessible_in_type_1, symbolToString(sourceProp), typeToString(source)); } - if (leftType.symbol === globalThisSymbol) { - if (globalThisSymbol.exports!.has(right.escapedText) && (globalThisSymbol.exports!.get(right.escapedText)!.flags & SymbolFlags.BlockScoped)) { - error(right, Diagnostics.Property_0_does_not_exist_on_type_1, unescapeLeadingUnderscores(right.escapedText), typeToString(leftType)); + return Ternary.False; + } + if (hasDifferingDeclarations) { + if (reportErrors) { + if (sourcePropFlags & ModifierFlags.Private && targetPropFlags & ModifierFlags.Private) { + reportError(Diagnostics.Types_have_separate_declarations_of_a_private_property_0, symbolToString(targetProp)); } - else if (noImplicitAny) { - error(right, Diagnostics.Element_implicitly_has_an_any_type_because_type_0_has_no_index_signature, typeToString(leftType)); + else { + reportError(Diagnostics.Property_0_is_private_in_type_1_but_not_in_type_2, symbolToString(targetProp), typeToString(sourcePropFlags & ModifierFlags.Private ? source : target), typeToString(sourcePropFlags & ModifierFlags.Private ? target : source)); } - return anyType; } - if (right.escapedText && !checkAndReportErrorForExtendingInterface(node)) { - reportNonexistentProperty(right, isThisTypeParameter(leftType) ? apparentType : leftType); + return Ternary.False; + } + } + else if (targetPropFlags & ModifierFlags.Protected) { + if (!isValidOverrideOf(sourceProp, targetProp)) { + if (reportErrors) { + reportError(Diagnostics.Property_0_is_protected_but_type_1_is_not_a_class_derived_from_2, symbolToString(targetProp), typeToString(getDeclaringClass(sourceProp) || source), typeToString(getDeclaringClass(targetProp) || target)); } - return errorType; + return Ternary.False; } - if (indexInfo.isReadonly && (isAssignmentTarget(node) || isDeleteTarget(node))) { - error(node, Diagnostics.Index_signature_in_type_0_only_permits_reading, typeToString(apparentType)); + } + else if (sourcePropFlags & ModifierFlags.Protected) { + if (reportErrors) { + reportError(Diagnostics.Property_0_is_protected_in_type_1_but_public_in_type_2, symbolToString(targetProp), typeToString(source), typeToString(target)); } - propType = indexInfo.type; + return Ternary.False; } - else { - checkPropertyNotUsedBeforeDeclaration(prop, node, right); - markPropertyAsReferenced(prop, node, left.kind === SyntaxKind.ThisKeyword); - getNodeLinks(node).resolvedSymbol = prop; - checkPropertyAccessibility(node, left.kind === SyntaxKind.SuperKeyword, apparentType, prop); - if (isAssignmentToReadonlyEntity(node as Expression, prop, assignmentKind)) { - error(right, Diagnostics.Cannot_assign_to_0_because_it_is_a_read_only_property, idText(right)); - return errorType; + // If the target comes from a partial union prop, allow `undefined` in the target type + const related = isPropertySymbolTypeRelated(sourceProp, targetProp, getTypeOfSourceProperty, reportErrors, intersectionState); + if (!related) { + if (reportErrors) { + reportIncompatibleError(Diagnostics.Types_of_property_0_are_incompatible, symbolToString(targetProp)); } - propType = getConstraintForLocation(getTypeOfSymbol(prop), node); + return Ternary.False; } - return getFlowTypeOfAccessExpression(node, prop, propType, right); - } - - function getFlowTypeOfAccessExpression(node: ElementAccessExpression | PropertyAccessExpression | QualifiedName, prop: Symbol | undefined, propType: Type, errorNode: Node) { - // Only compute control flow type if this is a property access expression that isn't an - // assignment target, and the referenced property was declared as a variable, property, - // accessor, or optional method. - const assignmentKind = getAssignmentTargetKind(node); - if (!isAccessExpression(node) || - assignmentKind === AssignmentKind.Definite || - prop && !(prop.flags & (SymbolFlags.Variable | SymbolFlags.Property | SymbolFlags.Accessor)) && !(prop.flags & SymbolFlags.Method && propType.flags & TypeFlags.Union)) { - return propType; - } - // If strict null checks and strict property initialization checks are enabled, if we have - // a this.xxx property access, if the property is an instance property without an initializer, - // and if we are in a constructor of the same class as the property declaration, assume that - // the property is uninitialized at the top of the control flow. - let assumeUninitialized = false; - if (strictNullChecks && strictPropertyInitialization && node.expression.kind === SyntaxKind.ThisKeyword) { - const declaration = prop && prop.valueDeclaration; - if (declaration && isInstancePropertyWithoutInitializer(declaration)) { - const flowContainer = getControlFlowContainer(node); - if (flowContainer.kind === SyntaxKind.Constructor && flowContainer.parent === declaration.parent && !(declaration.flags & NodeFlags.Ambient)) { - assumeUninitialized = true; - } - } - } - else if (strictNullChecks && prop && prop.valueDeclaration && - isPropertyAccessExpression(prop.valueDeclaration) && - getAssignmentDeclarationPropertyAccessKind(prop.valueDeclaration) && - getControlFlowContainer(node) === getControlFlowContainer(prop.valueDeclaration)) { - assumeUninitialized = true; - } - const flowType = getFlowTypeOfReference(node, propType, assumeUninitialized ? getOptionalType(propType) : propType); - if (assumeUninitialized && !(getFalsyFlags(propType) & TypeFlags.Undefined) && getFalsyFlags(flowType) & TypeFlags.Undefined) { - error(errorNode, Diagnostics.Property_0_is_used_before_being_assigned, symbolToString(prop!)); // TODO: GH#18217 - // Return the declared type to reduce follow-on errors - return propType; - } - return assignmentKind ? getBaseTypeOfLiteralType(flowType) : flowType; - } - - function checkPropertyNotUsedBeforeDeclaration(prop: Symbol, node: PropertyAccessExpression | QualifiedName, right: Identifier | PrivateIdentifier): void { - const { valueDeclaration } = prop; - if (!valueDeclaration || getSourceFileOfNode(node).isDeclarationFile) { - return; + // When checking for comparability, be more lenient with optional properties. + if (relation !== comparableRelation && sourceProp.flags & SymbolFlags.Optional && !(targetProp.flags & SymbolFlags.Optional)) { + // TypeScript 1.0 spec (April 2014): 3.8.3 + // S is a subtype of a type T, and T is a supertype of S if ... + // S' and T are object types and, for each member M in T.. + // M is a property and S' contains a property N where + // if M is a required property, N is also a required property + // (M - property in T) + // (N - property in S) + if (reportErrors) { + reportError(Diagnostics.Property_0_is_optional_in_type_1_but_required_in_type_2, symbolToString(targetProp), typeToString(source), typeToString(target)); + } + return Ternary.False; } - - let diagnosticMessage; - const declarationName = idText(right); - if (isInPropertyInitializer(node) - && !(isAccessExpression(node) && isAccessExpression(node.expression)) - && !isBlockScopedNameDeclaredBeforeUse(valueDeclaration, right) - && !isPropertyDeclaredInAncestorClass(prop)) { - diagnosticMessage = error(right, Diagnostics.Property_0_is_used_before_its_initialization, declarationName); - } - else if (valueDeclaration.kind === SyntaxKind.ClassDeclaration && - node.parent.kind !== SyntaxKind.TypeReference && - !(valueDeclaration.flags & NodeFlags.Ambient) && - !isBlockScopedNameDeclaredBeforeUse(valueDeclaration, right)) { - diagnosticMessage = error(right, Diagnostics.Class_0_used_before_its_declaration, declarationName); + return related; + } + function reportUnmatchedProperty(source: ts.Type, target: ts.Type, unmatchedProperty: ts.Symbol, requireOptionalProperties: boolean) { + let shouldSkipElaboration = false; + // give specific error in case where private names have the same description + if (unmatchedProperty.valueDeclaration + && isNamedDeclaration(unmatchedProperty.valueDeclaration) + && isPrivateIdentifier(unmatchedProperty.valueDeclaration.name) + && source.symbol + && source.symbol.flags & SymbolFlags.Class) { + const privateIdentifierDescription = unmatchedProperty.valueDeclaration.name.escapedText; + const symbolTableKey = getSymbolNameForPrivateIdentifier(source.symbol, privateIdentifierDescription); + if (symbolTableKey && getPropertyOfType(source, symbolTableKey)) { + const sourceName = getDeclarationName(source.symbol.valueDeclaration); + const targetName = getDeclarationName(target.symbol.valueDeclaration); + reportError(Diagnostics.Property_0_in_type_1_refers_to_a_different_member_that_cannot_be_accessed_from_within_type_2, diagnosticName(privateIdentifierDescription), diagnosticName(sourceName.escapedText === "" ? anon : sourceName), diagnosticName(targetName.escapedText === "" ? anon : targetName)); + return; + } } - - if (diagnosticMessage) { - addRelatedInfo(diagnosticMessage, - createDiagnosticForNode(valueDeclaration, Diagnostics._0_is_declared_here, declarationName) - ); + const props = arrayFrom(getUnmatchedProperties(source, target, requireOptionalProperties, /*matchDiscriminantProperties*/ false)); + if (!headMessage || (headMessage.code !== Diagnostics.Class_0_incorrectly_implements_interface_1.code && + headMessage.code !== Diagnostics.Class_0_incorrectly_implements_class_1_Did_you_mean_to_extend_1_and_inherit_its_members_as_a_subclass.code)) { + shouldSkipElaboration = true; // Retain top-level error for interface implementing issues, otherwise omit it } - } - - function isInPropertyInitializer(node: Node): boolean { - return !!findAncestor(node, node => { - switch (node.kind) { - case SyntaxKind.PropertyDeclaration: - return true; - case SyntaxKind.PropertyAssignment: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - case SyntaxKind.SpreadAssignment: - case SyntaxKind.ComputedPropertyName: - case SyntaxKind.TemplateSpan: - case SyntaxKind.JsxExpression: - case SyntaxKind.JsxAttribute: - case SyntaxKind.JsxAttributes: - case SyntaxKind.JsxSpreadAttribute: - case SyntaxKind.JsxOpeningElement: - case SyntaxKind.ExpressionWithTypeArguments: - case SyntaxKind.HeritageClause: - return false; - default: - return isExpressionNode(node) ? false : "quit"; + if (props.length === 1) { + const propName = symbolToString(unmatchedProperty); + reportError(Diagnostics.Property_0_is_missing_in_type_1_but_required_in_type_2, propName, ...getTypeNamesForErrorDisplay(source, target)); + if (length(unmatchedProperty.declarations)) { + associateRelatedInfo(createDiagnosticForNode(unmatchedProperty.declarations[0], Diagnostics._0_is_declared_here, propName)); + } + if (shouldSkipElaboration && errorInfo) { + overrideNextErrorInfo++; } - }); - } - - /** - * It's possible that "prop.valueDeclaration" is a local declaration, but the property was also declared in a superclass. - * In that case we won't consider it used before its declaration, because it gets its value from the superclass' declaration. - */ - function isPropertyDeclaredInAncestorClass(prop: Symbol): boolean { - if (!(prop.parent!.flags & SymbolFlags.Class)) { - return false; } - let classType: InterfaceType | undefined = getTypeOfSymbol(prop.parent!) as InterfaceType; - while (true) { - classType = classType.symbol && getSuperClass(classType) as InterfaceType | undefined; - if (!classType) { - return false; + else if (tryElaborateArrayLikeErrors(source, target, /*reportErrors*/ false)) { + if (props.length > 5) { // arbitrary cutoff for too-long list form + reportError(Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2_and_3_more, typeToString(source), typeToString(target), map(props.slice(0, 4), p => symbolToString(p)).join(", "), props.length - 4); } - const superProperty = getPropertyOfType(classType, prop.escapedName); - if (superProperty && superProperty.valueDeclaration) { - return true; + else { + reportError(Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2, typeToString(source), typeToString(target), map(props, p => symbolToString(p)).join(", ")); + } + if (shouldSkipElaboration && errorInfo) { + overrideNextErrorInfo++; } } + // No array like or unmatched property error - just issue top level error (errorInfo = undefined) } - - function getSuperClass(classType: InterfaceType): Type | undefined { - const x = getBaseTypes(classType); - if (x.length === 0) { - return undefined; + function propertiesRelatedTo(source: ts.Type, target: ts.Type, reportErrors: boolean, excludedProperties: UnderscoreEscapedMap | undefined, intersectionState: IntersectionState): Ternary { + if (relation === identityRelation) { + return propertiesIdenticalTo(source, target, excludedProperties); } - return getIntersectionType(x); - } - - function reportNonexistentProperty(propNode: Identifier | PrivateIdentifier, containingType: Type) { - let errorInfo: DiagnosticMessageChain | undefined; - let relatedInfo: Diagnostic | undefined; - if (!isPrivateIdentifier(propNode) && containingType.flags & TypeFlags.Union && !(containingType.flags & TypeFlags.Primitive)) { - for (const subtype of (containingType as UnionType).types) { - if (!getPropertyOfType(subtype, propNode.escapedText) && !getIndexInfoOfType(subtype, IndexKind.String)) { - errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_does_not_exist_on_type_1, declarationNameToString(propNode), typeToString(subtype)); - break; - } + const requireOptionalProperties = (relation === subtypeRelation || relation === strictSubtypeRelation) && !isObjectLiteralType(source) && !isEmptyArrayLiteralType(source) && !isTupleType(source); + const unmatchedProperty = getUnmatchedProperty(source, target, requireOptionalProperties, /*matchDiscriminantProperties*/ false); + if (unmatchedProperty) { + if (reportErrors) { + reportUnmatchedProperty(source, target, unmatchedProperty, requireOptionalProperties); } + return Ternary.False; } - if (typeHasStaticProperty(propNode.escapedText, containingType)) { - errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_is_a_static_member_of_type_1, declarationNameToString(propNode), typeToString(containingType)); - } - else { - const promisedType = getPromisedTypeOfPromise(containingType); - if (promisedType && getPropertyOfType(promisedType, propNode.escapedText)) { - errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_does_not_exist_on_type_1, declarationNameToString(propNode), typeToString(containingType)); - relatedInfo = createDiagnosticForNode(propNode, Diagnostics.Did_you_forget_to_use_await); + if (isObjectLiteralType(target)) { + for (const sourceProp of excludeProperties(getPropertiesOfType(source), excludedProperties)) { + if (!getPropertyOfObjectType(target, sourceProp.escapedName)) { + const sourceType = getTypeOfSymbol(sourceProp); + if (!(sourceType === undefinedType || sourceType === undefinedWideningType || sourceType === optionalType)) { + if (reportErrors) { + reportError(Diagnostics.Property_0_does_not_exist_on_type_1, symbolToString(sourceProp), typeToString(target)); + } + return Ternary.False; + } + } } - else { - const suggestion = getSuggestedSymbolForNonexistentProperty(propNode, containingType); - if (suggestion !== undefined) { - const suggestedName = symbolName(suggestion); - errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_2, declarationNameToString(propNode), typeToString(containingType), suggestedName); - relatedInfo = suggestion.valueDeclaration && createDiagnosticForNode(suggestion.valueDeclaration, Diagnostics._0_is_declared_here, suggestedName); + } + let result = Ternary.True; + if (isTupleType(target)) { + const targetRestType = getRestTypeOfTupleType(target); + if (targetRestType) { + if (!isTupleType(source)) { + return Ternary.False; } - else { - errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_does_not_exist_on_type_1, declarationNameToString(propNode), typeToString(containingType)); + const sourceRestType = getRestTypeOfTupleType(source); + if (sourceRestType && !isRelatedTo(sourceRestType, targetRestType, reportErrors)) { + if (reportErrors) { + reportError(Diagnostics.Rest_signatures_are_incompatible); + } + return Ternary.False; + } + const targetCount = getTypeReferenceArity(target) - 1; + const sourceCount = getTypeReferenceArity(source) - (sourceRestType ? 1 : 0); + const sourceTypeArguments = getTypeArguments((source)); + for (let i = targetCount; i < sourceCount; i++) { + const related = isRelatedTo(sourceTypeArguments[i], targetRestType, reportErrors); + if (!related) { + if (reportErrors) { + reportError(Diagnostics.Property_0_is_incompatible_with_rest_element_type, "" + i); + } + return Ternary.False; + } + result &= related; } } } - const resultDiagnostic = createDiagnosticForNodeFromMessageChain(propNode, errorInfo); - if (relatedInfo) { - addRelatedInfo(resultDiagnostic, relatedInfo); + // We only call this for union target types when we're attempting to do excess property checking - in those cases, we want to get _all possible props_ + // from the target union, across all members + const properties = getPropertiesOfType(target); + const numericNamesOnly = isTupleType(source) && isTupleType(target); + for (const targetProp of excludeProperties(properties, excludedProperties)) { + const name = targetProp.escapedName; + if (!(targetProp.flags & SymbolFlags.Prototype) && (!numericNamesOnly || isNumericLiteralName(name) || name === "length")) { + const sourceProp = getPropertyOfType(source, name); + if (sourceProp && sourceProp !== targetProp) { + const related = propertyRelatedTo(source, target, sourceProp, targetProp, getTypeOfSymbol, reportErrors, intersectionState); + if (!related) { + return Ternary.False; + } + result &= related; + } + } } - diagnostics.add(resultDiagnostic); - } - - function typeHasStaticProperty(propName: __String, containingType: Type): boolean { - const prop = containingType.symbol && getPropertyOfType(getTypeOfSymbol(containingType.symbol), propName); - return prop !== undefined && prop.valueDeclaration && hasModifier(prop.valueDeclaration, ModifierFlags.Static); - } - - function getSuggestedSymbolForNonexistentProperty(name: Identifier | PrivateIdentifier | string, containingType: Type): Symbol | undefined { - return getSpellingSuggestionForName(isString(name) ? name : idText(name), getPropertiesOfType(containingType), SymbolFlags.Value); - } - - function getSuggestionForNonexistentProperty(name: Identifier | PrivateIdentifier | string, containingType: Type): string | undefined { - const suggestion = getSuggestedSymbolForNonexistentProperty(name, containingType); - return suggestion && symbolName(suggestion); - } - - function getSuggestedSymbolForNonexistentSymbol(location: Node | undefined, outerName: __String, meaning: SymbolFlags): Symbol | undefined { - Debug.assert(outerName !== undefined, "outername should always be defined"); - const result = resolveNameHelper(location, outerName, meaning, /*nameNotFoundMessage*/ undefined, outerName, /*isUse*/ false, /*excludeGlobals*/ false, (symbols, name, meaning) => { - Debug.assertEqual(outerName, name, "name should equal outerName"); - const symbol = getSymbol(symbols, name, meaning); - // Sometimes the symbol is found when location is a return type of a function: `typeof x` and `x` is declared in the body of the function - // So the table *contains* `x` but `x` isn't actually in scope. - // However, resolveNameHelper will continue and call this callback again, so we'll eventually get a correct suggestion. - return symbol || getSpellingSuggestionForName(unescapeLeadingUnderscores(name), arrayFrom(symbols.values()), meaning); - }); return result; } - - function getSuggestionForNonexistentSymbol(location: Node | undefined, outerName: __String, meaning: SymbolFlags): string | undefined { - const symbolResult = getSuggestedSymbolForNonexistentSymbol(location, outerName, meaning); - return symbolResult && symbolName(symbolResult); - } - - function getSuggestedSymbolForNonexistentModule(name: Identifier, targetModule: Symbol): Symbol | undefined { - return targetModule.exports && getSpellingSuggestionForName(idText(name), getExportsOfModuleAsArray(targetModule), SymbolFlags.ModuleMember); - } - - function getSuggestionForNonexistentExport(name: Identifier, targetModule: Symbol): string | undefined { - const suggestion = getSuggestedSymbolForNonexistentModule(name, targetModule); - return suggestion && symbolName(suggestion); - } - - function getSuggestionForNonexistentIndexSignature(objectType: Type, expr: ElementAccessExpression, keyedType: Type): string | undefined { - // check if object type has setter or getter - function hasProp(name: "set" | "get") { - const prop = getPropertyOfObjectType(objectType, <__String>name); - if (prop) { - const s = getSingleCallSignature(getTypeOfSymbol(prop)); - return !!s && getMinArgumentCount(s) >= 1 && isTypeAssignableTo(keyedType, getTypeAtPosition(s, 0)); - } - return false; - }; - - const suggestedMethod = isAssignmentTarget(expr) ? "set" : "get"; - if (!hasProp(suggestedMethod)) { - return undefined; - } - - let suggestion = tryGetPropertyAccessOrIdentifierToString(expr.expression); - if (suggestion === undefined) { - suggestion = suggestedMethod; + function propertiesIdenticalTo(source: ts.Type, target: ts.Type, excludedProperties: UnderscoreEscapedMap | undefined): Ternary { + if (!(source.flags & TypeFlags.Object && target.flags & TypeFlags.Object)) { + return Ternary.False; } - else { - suggestion += "." + suggestedMethod; + const sourceProperties = excludeProperties(getPropertiesOfObjectType(source), excludedProperties); + const targetProperties = excludeProperties(getPropertiesOfObjectType(target), excludedProperties); + if (sourceProperties.length !== targetProperties.length) { + return Ternary.False; } - - return suggestion; - } - - /** - * Given a name and a list of symbols whose names are *not* equal to the name, return a spelling suggestion if there is one that is close enough. - * Names less than length 3 only check for case-insensitive equality, not levenshtein distance. - * - * If there is a candidate that's the same except for case, return that. - * If there is a candidate that's within one edit of the name, return that. - * Otherwise, return the candidate with the smallest Levenshtein distance, - * except for candidates: - * * With no name - * * Whose meaning doesn't match the `meaning` parameter. - * * Whose length differs from the target name by more than 0.34 of the length of the name. - * * Whose levenshtein distance is more than 0.4 of the length of the name - * (0.4 allows 1 substitution/transposition for every 5 characters, - * and 1 insertion/deletion at 3 characters) - */ - function getSpellingSuggestionForName(name: string, symbols: Symbol[], meaning: SymbolFlags): Symbol | undefined { - return getSpellingSuggestion(name, symbols, getCandidateName); - function getCandidateName(candidate: Symbol) { - const candidateName = symbolName(candidate); - return !startsWith(candidateName, "\"") && candidate.flags & meaning ? candidateName : undefined; + let result = Ternary.True; + for (const sourceProp of sourceProperties) { + const targetProp = getPropertyOfObjectType(target, sourceProp.escapedName); + if (!targetProp) { + return Ternary.False; + } + const related = compareProperties(sourceProp, targetProp, isRelatedTo); + if (!related) { + return Ternary.False; + } + result &= related; } + return result; } - - function markPropertyAsReferenced(prop: Symbol, nodeForCheckWriteOnly: Node | undefined, isThisAccess: boolean) { - - const valueDeclaration = prop && (prop.flags & SymbolFlags.ClassMember) && prop.valueDeclaration; - if (!valueDeclaration) { - return; + function signaturesRelatedTo(source: ts.Type, target: ts.Type, kind: SignatureKind, reportErrors: boolean): Ternary { + if (relation === identityRelation) { + return signaturesIdenticalTo(source, target, kind); } - const hasPrivateModifier = hasModifier(valueDeclaration, ModifierFlags.Private); - const hasPrivateIdentifier = isNamedDeclaration(prop.valueDeclaration) && isPrivateIdentifier(prop.valueDeclaration.name); - if (!hasPrivateModifier && !hasPrivateIdentifier) { - return; - } - if (nodeForCheckWriteOnly && isWriteOnlyAccess(nodeForCheckWriteOnly) && !(prop.flags & SymbolFlags.SetAccessor && !(prop.flags & SymbolFlags.GetAccessor))) { - return; + if (target === anyFunctionType || source === anyFunctionType) { + return Ternary.True; } - - if (isThisAccess) { - // Find any FunctionLikeDeclaration because those create a new 'this' binding. But this should only matter for methods (or getters/setters). - const containingMethod = findAncestor(nodeForCheckWriteOnly, isFunctionLikeDeclaration); - if (containingMethod && containingMethod.symbol === prop) { - return; + const sourceIsJSConstructor = source.symbol && isJSConstructor(source.symbol.valueDeclaration); + const targetIsJSConstructor = target.symbol && isJSConstructor(target.symbol.valueDeclaration); + const sourceSignatures = getSignaturesOfType(source, (sourceIsJSConstructor && kind === SignatureKind.Construct) ? + SignatureKind.Call : kind); + const targetSignatures = getSignaturesOfType(target, (targetIsJSConstructor && kind === SignatureKind.Construct) ? + SignatureKind.Call : kind); + if (kind === SignatureKind.Construct && sourceSignatures.length && targetSignatures.length) { + if (isAbstractConstructorType(source) && !isAbstractConstructorType(target)) { + // An abstract constructor type is not assignable to a non-abstract constructor type + // as it would otherwise be possible to new an abstract class. Note that the assignability + // check we perform for an extends clause excludes construct signatures from the target, + // so this check never proceeds. + if (reportErrors) { + reportError(Diagnostics.Cannot_assign_an_abstract_constructor_type_to_a_non_abstract_constructor_type); + } + return Ternary.False; + } + if (!constructorVisibilitiesAreCompatible(sourceSignatures[0], targetSignatures[0], reportErrors)) { + return Ternary.False; } } - - (getCheckFlags(prop) & CheckFlags.Instantiated ? getSymbolLinks(prop).target : prop)!.isReferenced = SymbolFlags.All; - } - - function isValidPropertyAccess(node: PropertyAccessExpression | QualifiedName | ImportTypeNode, propertyName: __String): boolean { - switch (node.kind) { - case SyntaxKind.PropertyAccessExpression: - return isValidPropertyAccessWithType(node, node.expression.kind === SyntaxKind.SuperKeyword, propertyName, getWidenedType(checkExpression(node.expression))); - case SyntaxKind.QualifiedName: - return isValidPropertyAccessWithType(node, /*isSuper*/ false, propertyName, getWidenedType(checkExpression(node.left))); - case SyntaxKind.ImportType: - return isValidPropertyAccessWithType(node, /*isSuper*/ false, propertyName, getTypeFromTypeNode(node)); + let result = Ternary.True; + const saveErrorInfo = captureErrorCalculationState(); + const incompatibleReporter = kind === SignatureKind.Construct ? reportIncompatibleConstructSignatureReturn : reportIncompatibleCallSignatureReturn; + if (getObjectFlags(source) & ObjectFlags.Instantiated && getObjectFlags(target) & ObjectFlags.Instantiated && source.symbol === target.symbol) { + // We have instantiations of the same anonymous type (which typically will be the type of a + // method). Simply do a pairwise comparison of the signatures in the two signature lists instead + // of the much more expensive N * M comparison matrix we explore below. We erase type parameters + // as they are known to always be the same. + for (let i = 0; i < targetSignatures.length; i++) { + const related = signatureRelatedTo(sourceSignatures[i], targetSignatures[i], /*erase*/ true, reportErrors, incompatibleReporter(sourceSignatures[i], targetSignatures[i])); + if (!related) { + return Ternary.False; + } + result &= related; + } } - } - - function isValidPropertyAccessForCompletions(node: PropertyAccessExpression | ImportTypeNode | QualifiedName, type: Type, property: Symbol): boolean { - return isValidPropertyAccessWithType(node, node.kind === SyntaxKind.PropertyAccessExpression && node.expression.kind === SyntaxKind.SuperKeyword, property.escapedName, type); - // Previously we validated the 'this' type of methods but this adversely affected performance. See #31377 for more context. - } - - function isValidPropertyAccessWithType( - node: PropertyAccessExpression | QualifiedName | ImportTypeNode, - isSuper: boolean, - propertyName: __String, - type: Type): boolean { - - if (type === errorType || isTypeAny(type)) { - return true; + else if (sourceSignatures.length === 1 && targetSignatures.length === 1) { + // For simple functions (functions with a single signature) we only erase type parameters for + // the comparable relation. Otherwise, if the source signature is generic, we instantiate it + // in the context of the target signature before checking the relationship. Ideally we'd do + // this regardless of the number of signatures, but the potential costs are prohibitive due + // to the quadratic nature of the logic below. + const eraseGenerics = relation === comparableRelation || !!compilerOptions.noStrictGenericChecks; + result = signatureRelatedTo(sourceSignatures[0], targetSignatures[0], eraseGenerics, reportErrors, incompatibleReporter(sourceSignatures[0], targetSignatures[0])); } - const prop = getPropertyOfType(type, propertyName); - if (prop) { - if (isPropertyAccessExpression(node) && prop.valueDeclaration && isPrivateIdentifierPropertyDeclaration(prop.valueDeclaration)) { - const declClass = getContainingClass(prop.valueDeclaration); - return !isOptionalChain(node) && !!findAncestor(node, parent => parent === declClass); + else { + outer: for (const t of targetSignatures) { + // Only elaborate errors from the first failure + let shouldElaborateErrors = reportErrors; + for (const s of sourceSignatures) { + const related = signatureRelatedTo(s, t, /*erase*/ true, shouldElaborateErrors, incompatibleReporter(s, t)); + if (related) { + result &= related; + resetErrorInfo(saveErrorInfo); + continue outer; + } + shouldElaborateErrors = false; + } + if (shouldElaborateErrors) { + reportError(Diagnostics.Type_0_provides_no_match_for_the_signature_1, typeToString(source), signatureToString(t, /*enclosingDeclaration*/ undefined, /*flags*/ undefined, kind)); + } + return Ternary.False; } - return checkPropertyAccessibility(node, isSuper, type, prop); } - // In js files properties of unions are allowed in completion - return isInJSFile(node) && (type.flags & TypeFlags.Union) !== 0 && (type).types.some(elementType => isValidPropertyAccessWithType(node, isSuper, propertyName, elementType)); + return result; } - - /** - * Return the symbol of the for-in variable declared or referenced by the given for-in statement. - */ - function getForInVariableSymbol(node: ForInStatement): Symbol | undefined { - const initializer = node.initializer; - if (initializer.kind === SyntaxKind.VariableDeclarationList) { - const variable = (initializer).declarations[0]; - if (variable && !isBindingPattern(variable.name)) { - return getSymbolOfNode(variable); - } + function reportIncompatibleCallSignatureReturn(siga: ts.Signature, sigb: ts.Signature) { + if (siga.parameters.length === 0 && sigb.parameters.length === 0) { + return (source: ts.Type, target: ts.Type) => reportIncompatibleError(Diagnostics.Call_signatures_with_no_arguments_have_incompatible_return_types_0_and_1, typeToString(source), typeToString(target)); } - else if (initializer.kind === SyntaxKind.Identifier) { - return getResolvedSymbol(initializer); + return (source: ts.Type, target: ts.Type) => reportIncompatibleError(Diagnostics.Call_signature_return_types_0_and_1_are_incompatible, typeToString(source), typeToString(target)); + } + function reportIncompatibleConstructSignatureReturn(siga: ts.Signature, sigb: ts.Signature) { + if (siga.parameters.length === 0 && sigb.parameters.length === 0) { + return (source: ts.Type, target: ts.Type) => reportIncompatibleError(Diagnostics.Construct_signatures_with_no_arguments_have_incompatible_return_types_0_and_1, typeToString(source), typeToString(target)); } - return undefined; + return (source: ts.Type, target: ts.Type) => reportIncompatibleError(Diagnostics.Construct_signature_return_types_0_and_1_are_incompatible, typeToString(source), typeToString(target)); } - /** - * Return true if the given type is considered to have numeric property names. + * See signatureAssignableTo, compareSignaturesIdentical */ - function hasNumericPropertyNames(type: Type) { - return getIndexTypeOfType(type, IndexKind.Number) && !getIndexTypeOfType(type, IndexKind.String); + function signatureRelatedTo(source: ts.Signature, target: ts.Signature, erase: boolean, reportErrors: boolean, incompatibleReporter: (source: ts.Type, target: ts.Type) => void): Ternary { + return compareSignaturesRelated(erase ? getErasedSignature(source) : source, erase ? getErasedSignature(target) : target, relation === strictSubtypeRelation ? SignatureCheckMode.StrictArity : 0, reportErrors, reportError, incompatibleReporter, isRelatedTo, reportUnreliableMarkers); } - - /** - * Return true if given node is an expression consisting of an identifier (possibly parenthesized) - * that references a for-in variable for an object with numeric property names. - */ - function isForInVariableForNumericPropertyNames(expr: Expression) { - const e = skipParentheses(expr); - if (e.kind === SyntaxKind.Identifier) { - const symbol = getResolvedSymbol(e); - if (symbol.flags & SymbolFlags.Variable) { - let child: Node = expr; - let node = expr.parent; - while (node) { - if (node.kind === SyntaxKind.ForInStatement && - child === (node).statement && - getForInVariableSymbol(node) === symbol && - hasNumericPropertyNames(getTypeOfExpression((node).expression))) { - return true; + function signaturesIdenticalTo(source: ts.Type, target: ts.Type, kind: SignatureKind): Ternary { + const sourceSignatures = getSignaturesOfType(source, kind); + const targetSignatures = getSignaturesOfType(target, kind); + if (sourceSignatures.length !== targetSignatures.length) { + return Ternary.False; + } + let result = Ternary.True; + for (let i = 0; i < sourceSignatures.length; i++) { + const related = compareSignaturesIdentical(sourceSignatures[i], targetSignatures[i], /*partialMatch*/ false, /*ignoreThisTypes*/ false, /*ignoreReturnTypes*/ false, isRelatedTo); + if (!related) { + return Ternary.False; + } + result &= related; + } + return result; + } + function eachPropertyRelatedTo(source: ts.Type, target: ts.Type, kind: IndexKind, reportErrors: boolean): Ternary { + let result = Ternary.True; + const props = source.flags & TypeFlags.Intersection ? getPropertiesOfUnionOrIntersectionType((source)) : getPropertiesOfObjectType(source); + for (const prop of props) { + // Skip over ignored JSX and symbol-named members + if (isIgnoredJsxProperty(source, prop)) { + continue; + } + const nameType = getSymbolLinks(prop).nameType; + if (nameType && nameType.flags & TypeFlags.UniqueESSymbol) { + continue; + } + if (kind === IndexKind.String || isNumericLiteralName(prop.escapedName)) { + const related = isRelatedTo(getTypeOfSymbol(prop), target, reportErrors); + if (!related) { + if (reportErrors) { + reportError(Diagnostics.Property_0_is_incompatible_with_index_signature, symbolToString(prop)); } - child = node; - node = node.parent; + return Ternary.False; } + result &= related; } } - return false; - } - - function checkIndexedAccess(node: ElementAccessExpression): Type { - return node.flags & NodeFlags.OptionalChain ? checkElementAccessChain(node as ElementAccessChain) : - checkElementAccessExpression(node, checkNonNullExpression(node.expression)); + return result; } - - function checkElementAccessChain(node: ElementAccessChain) { - const exprType = checkExpression(node.expression); - const nonOptionalType = getOptionalExpressionType(exprType, node.expression); - return propagateOptionalTypeMarker(checkElementAccessExpression(node, checkNonNullType(nonOptionalType, node.expression)), node, nonOptionalType !== exprType); + function indexTypeRelatedTo(sourceType: ts.Type, targetType: ts.Type, reportErrors: boolean) { + const related = isRelatedTo(sourceType, targetType, reportErrors); + if (!related && reportErrors) { + reportError(Diagnostics.Index_signatures_are_incompatible); + } + return related; } - - function checkElementAccessExpression(node: ElementAccessExpression, exprType: Type): Type { - const objectType = getAssignmentTargetKind(node) !== AssignmentKind.None || isMethodAccessForCall(node) ? getWidenedType(exprType) : exprType; - const indexExpression = node.argumentExpression; - const indexType = checkExpression(indexExpression); - - if (objectType === errorType || objectType === silentNeverType) { - return objectType; + function indexTypesRelatedTo(source: ts.Type, target: ts.Type, kind: IndexKind, sourceIsPrimitive: boolean, reportErrors: boolean, intersectionState: IntersectionState): Ternary { + if (relation === identityRelation) { + return indexTypesIdenticalTo(source, target, kind); } - - if (isConstEnumObjectType(objectType) && !isStringLiteralLike(indexExpression)) { - error(indexExpression, Diagnostics.A_const_enum_member_can_only_be_accessed_using_a_string_literal); - return errorType; + const targetType = getIndexTypeOfType(target, kind); + if (!targetType || targetType.flags & TypeFlags.Any && !sourceIsPrimitive) { + // Index signature of type any permits assignment from everything but primitives + return Ternary.True; } - - const effectiveIndexType = isForInVariableForNumericPropertyNames(indexExpression) ? numberType : indexType; - const accessFlags = isAssignmentTarget(node) ? - AccessFlags.Writing | (isGenericObjectType(objectType) && !isThisTypeParameter(objectType) ? AccessFlags.NoIndexSignatures : 0) : - AccessFlags.None; - const indexedAccessType = getIndexedAccessTypeOrUndefined(objectType, effectiveIndexType, node, accessFlags) || errorType; - return checkIndexedAccessIndexType(getFlowTypeOfAccessExpression(node, indexedAccessType.symbol, indexedAccessType, indexExpression), node); - } - - function checkThatExpressionIsProperSymbolReference(expression: Expression, expressionType: Type, reportError: boolean): boolean { - if (expressionType === errorType) { - // There is already an error, so no need to report one. - return false; + if (isGenericMappedType(source)) { + // A generic mapped type { [P in K]: T } is related to an index signature { [x: string]: U } + // if T is related to U. + return kind === IndexKind.String ? isRelatedTo(getTemplateTypeFromMappedType(source), targetType, reportErrors) : Ternary.False; } - - if (!isWellKnownSymbolSyntactically(expression)) { - return false; + const indexType = getIndexTypeOfType(source, kind) || kind === IndexKind.Number && getIndexTypeOfType(source, IndexKind.String); + if (indexType) { + return indexTypeRelatedTo(indexType, targetType, reportErrors); } - - // Make sure the property type is the primitive symbol type - if ((expressionType.flags & TypeFlags.ESSymbolLike) === 0) { - if (reportError) { - error(expression, Diagnostics.A_computed_property_name_of_the_form_0_must_be_of_type_symbol, getTextOfNode(expression)); + if (!(intersectionState & IntersectionState.Source) && isObjectTypeWithInferableIndex(source)) { + // Intersection constituents are never considered to have an inferred index signature + let related = eachPropertyRelatedTo(source, targetType, kind, reportErrors); + if (related && kind === IndexKind.String) { + const numberIndexType = getIndexTypeOfType(source, IndexKind.Number); + if (numberIndexType) { + related &= indexTypeRelatedTo(numberIndexType, targetType, reportErrors); + } } - return false; + return related; } - - // The name is Symbol., so make sure Symbol actually resolves to the - // global Symbol object - const leftHandSide = (expression).expression; - const leftHandSideSymbol = getResolvedSymbol(leftHandSide); - if (!leftHandSideSymbol) { - return false; + if (reportErrors) { + reportError(Diagnostics.Index_signature_is_missing_in_type_0, typeToString(source)); } - - const globalESSymbol = getGlobalESSymbolConstructorSymbol(/*reportErrors*/ true); - if (!globalESSymbol) { - // Already errored when we tried to look up the symbol - return false; + return Ternary.False; + } + function indexTypesIdenticalTo(source: ts.Type, target: ts.Type, indexKind: IndexKind): Ternary { + const targetInfo = getIndexInfoOfType(target, indexKind); + const sourceInfo = getIndexInfoOfType(source, indexKind); + if (!sourceInfo && !targetInfo) { + return Ternary.True; } - - if (leftHandSideSymbol !== globalESSymbol) { - if (reportError) { - error(leftHandSide, Diagnostics.Symbol_reference_does_not_refer_to_the_global_Symbol_constructor_object); - } - return false; + if (sourceInfo && targetInfo && sourceInfo.isReadonly === targetInfo.isReadonly) { + return isRelatedTo(sourceInfo.type, targetInfo.type); } - - return true; - } - - function callLikeExpressionMayHaveTypeArguments(node: CallLikeExpression): node is CallExpression | NewExpression | TaggedTemplateExpression | JsxOpeningElement { - return isCallOrNewExpression(node) || isTaggedTemplateExpression(node) || isJsxOpeningLikeElement(node); + return Ternary.False; } - - function resolveUntypedCall(node: CallLikeExpression): Signature { - if (callLikeExpressionMayHaveTypeArguments(node)) { - // Check type arguments even though we will give an error that untyped calls may not accept type arguments. - // This gets us diagnostics for the type arguments and marks them as referenced. - forEach(node.typeArguments, checkSourceElement); + function constructorVisibilitiesAreCompatible(sourceSignature: ts.Signature, targetSignature: ts.Signature, reportErrors: boolean) { + if (!sourceSignature.declaration || !targetSignature.declaration) { + return true; } - - if (node.kind === SyntaxKind.TaggedTemplateExpression) { - checkExpression(node.template); + const sourceAccessibility = getSelectedModifierFlags(sourceSignature.declaration, ModifierFlags.NonPublicAccessibilityModifier); + const targetAccessibility = getSelectedModifierFlags(targetSignature.declaration, ModifierFlags.NonPublicAccessibilityModifier); + // A public, protected and private signature is assignable to a private signature. + if (targetAccessibility === ModifierFlags.Private) { + return true; } - else if (isJsxOpeningLikeElement(node)) { - checkExpression(node.attributes); + // A public and protected signature is assignable to a protected signature. + if (targetAccessibility === ModifierFlags.Protected && sourceAccessibility !== ModifierFlags.Private) { + return true; } - else if (node.kind !== SyntaxKind.Decorator) { - forEach((node).arguments, argument => { - checkExpression(argument); - }); + // Only a public signature is assignable to public signature. + if (targetAccessibility !== ModifierFlags.Protected && !sourceAccessibility) { + return true; } - return anySignature; - } - - function resolveErrorCall(node: CallLikeExpression): Signature { - resolveUntypedCall(node); - return unknownSignature; + if (reportErrors) { + reportError(Diagnostics.Cannot_assign_a_0_constructor_type_to_a_1_constructor_type, visibilityToString(sourceAccessibility), visibilityToString(targetAccessibility)); + } + return false; } - - // Re-order candidate signatures into the result array. Assumes the result array to be empty. - // The candidate list orders groups in reverse, but within a group signatures are kept in declaration order - // A nit here is that we reorder only signatures that belong to the same symbol, - // so order how inherited signatures are processed is still preserved. - // interface A { (x: string): void } - // interface B extends A { (x: 'foo'): string } - // const b: B; - // b('foo') // <- here overloads should be processed as [(x:'foo'): string, (x: string): void] - function reorderCandidates(signatures: readonly Signature[], result: Signature[], callChainFlags: SignatureFlags): void { - let lastParent: Node | undefined; - let lastSymbol: Symbol | undefined; - let cutoffIndex = 0; - let index: number | undefined; - let specializedIndex = -1; - let spliceIndex: number; - Debug.assert(!result.length); - for (const signature of signatures) { - const symbol = signature.declaration && getSymbolOfNode(signature.declaration); - const parent = signature.declaration && signature.declaration.parent; - if (!lastSymbol || symbol === lastSymbol) { - if (lastParent && parent === lastParent) { - index = index! + 1; - } - else { - lastParent = parent; - index = cutoffIndex; - } - } - else { - // current declaration belongs to a different symbol - // set cutoffIndex so re-orderings in the future won't change result set from 0 to cutoffIndex - index = cutoffIndex = result.length; - lastParent = parent; - } - lastSymbol = symbol; - - // specialized signatures always need to be placed before non-specialized signatures regardless - // of the cutoff position; see GH#1133 - if (signatureHasLiteralTypes(signature)) { - specializedIndex++; - spliceIndex = specializedIndex; - // The cutoff index always needs to be greater than or equal to the specialized signature index - // in order to prevent non-specialized signatures from being added before a specialized - // signature. - cutoffIndex++; + } + function getBestMatchingType(source: ts.Type, target: UnionOrIntersectionType, isRelatedTo = compareTypesAssignable) { + return findMatchingDiscriminantType(source, target, isRelatedTo) || + findMatchingTypeReferenceOrTypeAliasReference(source, target) || + findBestTypeForObjectLiteral(source, target) || + findBestTypeForInvokable(source, target) || + findMostOverlappyType(source, target); + } + function discriminateTypeByDiscriminableItems(target: UnionType, discriminators: [() => ts.Type, __String][], related: (source: ts.Type, target: ts.Type) => boolean | Ternary): ts.Type | undefined; + function discriminateTypeByDiscriminableItems(target: UnionType, discriminators: [() => ts.Type, __String][], related: (source: ts.Type, target: ts.Type) => boolean | Ternary, defaultValue: ts.Type): ts.Type; + function discriminateTypeByDiscriminableItems(target: UnionType, discriminators: [() => ts.Type, __String][], related: (source: ts.Type, target: ts.Type) => boolean | Ternary, defaultValue?: ts.Type) { + // undefined=unknown, true=discriminated, false=not discriminated + // The state of each type progresses from left to right. Discriminated types stop at 'true'. + const discriminable = target.types.map(_ => undefined) as (boolean | undefined)[]; + for (const [getDiscriminatingType, propertyName] of discriminators) { + let i = 0; + for (const type of target.types) { + const targetType = getTypeOfPropertyOfType(type, propertyName); + if (targetType && related(getDiscriminatingType(), targetType)) { + discriminable[i] = discriminable[i] === undefined ? true : discriminable[i]; } else { - spliceIndex = index; + discriminable[i] = false; } - - result.splice(spliceIndex, 0, callChainFlags ? getOptionalCallSignature(signature, callChainFlags) : signature); + i++; } } - - function isSpreadArgument(arg: Expression | undefined): arg is Expression { - return !!arg && (arg.kind === SyntaxKind.SpreadElement || arg.kind === SyntaxKind.SyntheticExpression && (arg).isSpread); + const match = discriminable.indexOf(/*searchElement*/ true); + // make sure exactly 1 matches before returning it + return match === -1 || discriminable.indexOf(/*searchElement*/ true, match + 1) !== -1 ? defaultValue : target.types[match]; + } + function isFromSpreadAssignment(prop: ts.Symbol, container: ts.Symbol) { + return prop.valueDeclaration?.parent !== container.valueDeclaration; + } + /** + * A type is 'weak' if it is an object type with at least one optional property + * and no required properties, call/construct signatures or index signatures + */ + function isWeakType(type: ts.Type): boolean { + if (type.flags & TypeFlags.Object) { + const resolved = resolveStructuredTypeMembers((type)); + return resolved.callSignatures.length === 0 && resolved.constructSignatures.length === 0 && + !resolved.stringIndexInfo && !resolved.numberIndexInfo && + resolved.properties.length > 0 && + every(resolved.properties, p => !!(p.flags & SymbolFlags.Optional)); + } + if (type.flags & TypeFlags.Intersection) { + return every((type).types, isWeakType); + } + return false; + } + function hasCommonProperties(source: ts.Type, target: ts.Type, isComparingJsxAttributes: boolean) { + for (const prop of getPropertiesOfType(source)) { + if (isKnownProperty(target, prop.escapedName, isComparingJsxAttributes)) { + return true; + } } - - function getSpreadArgumentIndex(args: readonly Expression[]): number { - return findIndex(args, isSpreadArgument); + return false; + } + // Return a type reference where the source type parameter is replaced with the target marker + // type, and flag the result as a marker type reference. + function getMarkerTypeReference(type: GenericType, source: TypeParameter, target: ts.Type) { + const result = createTypeReference(type, map(type.typeParameters, t => t === source ? target : t)); + result.objectFlags |= ObjectFlags.MarkerType; + return result; + } + function getAliasVariances(symbol: ts.Symbol) { + const links = getSymbolLinks(symbol); + return getVariancesWorker(links.typeParameters, links, (_links, param, marker) => { + const type = getTypeAliasInstantiation(symbol, instantiateTypes(links.typeParameters!, makeUnaryTypeMapper(param, marker))); + type.aliasTypeArgumentsContainsMarker = true; + return type; + }); + } + // Return an array containing the variance of each type parameter. The variance is effectively + // a digest of the type comparisons that occur for each type argument when instantiations of the + // generic type are structurally compared. We infer the variance information by comparing + // instantiations of the generic type for type arguments with known relations. The function + // returns the emptyArray singleton when invoked recursively for the given generic type. + function getVariancesWorker(typeParameters: readonly TypeParameter[] = emptyArray, cache: TCache, createMarkerType: (input: TCache, param: TypeParameter, marker: ts.Type) => ts.Type): VarianceFlags[] { + let variances = cache.variances; + if (!variances) { + // The emptyArray singleton is used to signal a recursive invocation. + cache.variances = emptyArray; + variances = []; + for (const tp of typeParameters) { + let unmeasurable = false; + let unreliable = false; + const oldHandler = outofbandVarianceMarkerHandler; + outofbandVarianceMarkerHandler = (onlyUnreliable) => onlyUnreliable ? unreliable = true : unmeasurable = true; + // We first compare instantiations where the type parameter is replaced with + // marker types that have a known subtype relationship. From this we can infer + // invariance, covariance, contravariance or bivariance. + const typeWithSuper = createMarkerType(cache, tp, markerSuperType); + const typeWithSub = createMarkerType(cache, tp, markerSubType); + let variance = (isTypeAssignableTo(typeWithSub, typeWithSuper) ? VarianceFlags.Covariant : 0) | + (isTypeAssignableTo(typeWithSuper, typeWithSub) ? VarianceFlags.Contravariant : 0); + // If the instantiations appear to be related bivariantly it may be because the + // type parameter is independent (i.e. it isn't witnessed anywhere in the generic + // type). To determine this we compare instantiations where the type parameter is + // replaced with marker types that are known to be unrelated. + if (variance === VarianceFlags.Bivariant && isTypeAssignableTo(createMarkerType(cache, tp, markerOtherType), typeWithSuper)) { + variance = VarianceFlags.Independent; + } + outofbandVarianceMarkerHandler = oldHandler; + if (unmeasurable || unreliable) { + if (unmeasurable) { + variance |= VarianceFlags.Unmeasurable; + } + if (unreliable) { + variance |= VarianceFlags.Unreliable; + } + } + variances.push(variance); + } + cache.variances = variances; + } + return variances; + } + function getVariances(type: GenericType): VarianceFlags[] { + // Arrays and tuples are known to be covariant, no need to spend time computing this. + if (type === globalArrayType || type === globalReadonlyArrayType || type.objectFlags & ObjectFlags.Tuple) { + return arrayVariances; } - - function acceptsVoid(t: Type): boolean { - return !!(t.flags & TypeFlags.Void); + return getVariancesWorker(type.typeParameters, type, getMarkerTypeReference); + } + // Return true if the given type reference has a 'void' type argument for a covariant type parameter. + // See comment at call in recursiveTypeRelatedTo for when this case matters. + function hasCovariantVoidArgument(typeArguments: readonly ts.Type[], variances: VarianceFlags[]): boolean { + for (let i = 0; i < variances.length; i++) { + if ((variances[i] & VarianceFlags.VarianceMask) === VarianceFlags.Covariant && typeArguments[i].flags & TypeFlags.Void) { + return true; + } } - - function hasCorrectArity(node: CallLikeExpression, args: readonly Expression[], signature: Signature, signatureHelpTrailingComma = false) { - let argCount: number; - let callIsIncomplete = false; // In incomplete call we want to be lenient when we have too few arguments - let effectiveParameterCount = getParameterCount(signature); - let effectiveMinimumArguments = getMinArgumentCount(signature); - - if (node.kind === SyntaxKind.TaggedTemplateExpression) { - argCount = args.length; - if (node.template.kind === SyntaxKind.TemplateExpression) { - // If a tagged template expression lacks a tail literal, the call is incomplete. - // Specifically, a template only can end in a TemplateTail or a Missing literal. - const lastSpan = last(node.template.templateSpans); // we should always have at least one span. - callIsIncomplete = nodeIsMissing(lastSpan.literal) || !!lastSpan.literal.isUnterminated; - } - else { - // If the template didn't end in a backtick, or its beginning occurred right prior to EOF, - // then this might actually turn out to be a TemplateHead in the future; - // so we consider the call to be incomplete. - const templateLiteral = node.template; - Debug.assert(templateLiteral.kind === SyntaxKind.NoSubstitutionTemplateLiteral); - callIsIncomplete = !!templateLiteral.isUnterminated; - } + return false; + } + function isUnconstrainedTypeParameter(type: ts.Type) { + return type.flags & TypeFlags.TypeParameter && !getConstraintOfTypeParameter((type)); + } + function isNonDeferredTypeReference(type: ts.Type): type is TypeReference { + return !!(getObjectFlags(type) & ObjectFlags.Reference) && !(type).node; + } + function isTypeReferenceWithGenericArguments(type: ts.Type): boolean { + return isNonDeferredTypeReference(type) && some(getTypeArguments(type), t => isUnconstrainedTypeParameter(t) || isTypeReferenceWithGenericArguments(t)); + } + /** + * getTypeReferenceId(A) returns "111=0-12=1" + * where A.id=111 and number.id=12 + */ + function getTypeReferenceId(type: TypeReference, typeParameters: ts.Type[], depth = 0) { + let result = "" + type.target.id; + for (const t of getTypeArguments(type)) { + if (isUnconstrainedTypeParameter(t)) { + let index = typeParameters.indexOf(t); + if (index < 0) { + index = typeParameters.length; + typeParameters.push(t); + } + result += "=" + index; + } + else if (depth < 4 && isTypeReferenceWithGenericArguments(t)) { + result += "<" + getTypeReferenceId((t as TypeReference), typeParameters, depth + 1) + ">"; } - else if (node.kind === SyntaxKind.Decorator) { - argCount = getDecoratorArgumentCount(node, signature); + else { + result += "-" + t.id; } - else if (isJsxOpeningLikeElement(node)) { - callIsIncomplete = node.attributes.end === node.end; - if (callIsIncomplete) { - return true; + } + return result; + } + /** + * To improve caching, the relation key for two generic types uses the target's id plus ids of the type parameters. + * For other cases, the types ids are used. + */ + function getRelationKey(source: ts.Type, target: ts.Type, intersectionState: IntersectionState, relation: ts.Map) { + if (relation === identityRelation && source.id > target.id) { + const temp = source; + source = target; + target = temp; + } + const postFix = intersectionState ? ":" + intersectionState : ""; + if (isTypeReferenceWithGenericArguments(source) && isTypeReferenceWithGenericArguments(target)) { + const typeParameters: ts.Type[] = []; + return getTypeReferenceId((source), typeParameters) + "," + getTypeReferenceId((target), typeParameters) + postFix; + } + return source.id + "," + target.id + postFix; + } + // Invoke the callback for each underlying property symbol of the given symbol and return the first + // value that isn't undefined. + function forEachProperty(prop: ts.Symbol, callback: (p: ts.Symbol) => T): T | undefined { + if (getCheckFlags(prop) & CheckFlags.Synthetic) { + for (const t of (prop).containingType!.types) { + const p = getPropertyOfType(t, prop.escapedName); + const result = p && forEachProperty(p, callback); + if (result) { + return result; } - argCount = effectiveMinimumArguments === 0 ? args.length : 1; - effectiveParameterCount = args.length === 0 ? effectiveParameterCount : 1; // class may have argumentless ctor functions - still resolve ctor and compare vs props member type - effectiveMinimumArguments = Math.min(effectiveMinimumArguments, 1); // sfc may specify context argument - handled by framework and not typechecked } - else { - if (!node.arguments) { - // This only happens when we have something of the form: 'new C' - Debug.assert(node.kind === SyntaxKind.NewExpression); - return getMinArgumentCount(signature) === 0; + return undefined; + } + return callback(prop); + } + // Return the declaring class type of a property or undefined if property not declared in class + function getDeclaringClass(prop: ts.Symbol) { + return prop.parent && prop.parent.flags & SymbolFlags.Class ? getDeclaredTypeOfSymbol(getParentOfSymbol(prop)!) : undefined; + } + // Return true if some underlying source property is declared in a class that derives + // from the given base class. + function isPropertyInClassDerivedFrom(prop: ts.Symbol, baseClass: ts.Type | undefined) { + return forEachProperty(prop, sp => { + const sourceClass = getDeclaringClass(sp); + return sourceClass ? hasBaseType(sourceClass, baseClass) : false; + }); + } + // Return true if source property is a valid override of protected parts of target property. + function isValidOverrideOf(sourceProp: ts.Symbol, targetProp: ts.Symbol) { + return !forEachProperty(targetProp, tp => getDeclarationModifierFlagsFromSymbol(tp) & ModifierFlags.Protected ? + !isPropertyInClassDerivedFrom(sourceProp, getDeclaringClass(tp)) : false); + } + // Return true if the given class derives from each of the declaring classes of the protected + // constituents of the given property. + function isClassDerivedFromDeclaringClasses(checkClass: ts.Type, prop: ts.Symbol) { + return forEachProperty(prop, p => getDeclarationModifierFlagsFromSymbol(p) & ModifierFlags.Protected ? + !hasBaseType(checkClass, getDeclaringClass(p)) : false) ? undefined : checkClass; + } + // Return true if the given type is deeply nested. We consider this to be the case when structural type comparisons + // for 5 or more occurrences or instantiations of the type have been recorded on the given stack. It is possible, + // though highly unlikely, for this test to be true in a situation where a chain of instantiations is not infinitely + // expanding. Effectively, we will generate a false positive when two types are structurally equal to at least 5 + // levels, but unequal at some level beyond that. + // In addition, this will also detect when an indexed access has been chained off of 5 or more times (which is essentially + // the dual of the structural comparison), and likewise mark the type as deeply nested, potentially adding false positives + // for finite but deeply expanding indexed accesses (eg, for `Q[P1][P2][P3][P4][P5]`). + function isDeeplyNestedType(type: ts.Type, stack: ts.Type[], depth: number): boolean { + // We track all object types that have an associated symbol (representing the origin of the type) + if (depth >= 5 && type.flags & TypeFlags.Object && !isObjectOrArrayLiteralType(type)) { + const symbol = type.symbol; + if (symbol) { + let count = 0; + for (let i = 0; i < depth; i++) { + const t = stack[i]; + if (t.flags & TypeFlags.Object && t.symbol === symbol) { + count++; + if (count >= 5) + return true; + } } - - argCount = signatureHelpTrailingComma ? args.length + 1 : args.length; - - // If we are missing the close parenthesis, the call is incomplete. - callIsIncomplete = node.arguments.end === node.end; - - // If a spread argument is present, check that it corresponds to a rest parameter or at least that it's in the valid range. - const spreadArgIndex = getSpreadArgumentIndex(args); - if (spreadArgIndex >= 0) { - return spreadArgIndex >= getMinArgumentCount(signature) && (hasEffectiveRestParameter(signature) || spreadArgIndex < getParameterCount(signature)); + } + } + if (depth >= 5 && type.flags & TypeFlags.IndexedAccess) { + const root = getRootObjectTypeFromIndexedAccessChain(type); + let count = 0; + for (let i = 0; i < depth; i++) { + const t = stack[i]; + if (getRootObjectTypeFromIndexedAccessChain(t) === root) { + count++; + if (count >= 5) + return true; } } - - // Too many arguments implies incorrect arity. - if (!hasEffectiveRestParameter(signature) && argCount > effectiveParameterCount) { - return false; + } + return false; + } + /** + * Gets the leftmost object type in a chain of indexed accesses, eg, in A[P][Q], returns A + */ + function getRootObjectTypeFromIndexedAccessChain(type: ts.Type) { + let t = type; + while (t.flags & TypeFlags.IndexedAccess) { + t = (t as IndexedAccessType).objectType; + } + return t; + } + function isPropertyIdenticalTo(sourceProp: ts.Symbol, targetProp: ts.Symbol): boolean { + return compareProperties(sourceProp, targetProp, compareTypesIdentical) !== Ternary.False; + } + function compareProperties(sourceProp: ts.Symbol, targetProp: ts.Symbol, compareTypes: (source: ts.Type, target: ts.Type) => Ternary): Ternary { + // Two members are considered identical when + // - they are public properties with identical names, optionality, and types, + // - they are private or protected properties originating in the same declaration and having identical types + if (sourceProp === targetProp) { + return Ternary.True; + } + const sourcePropAccessibility = getDeclarationModifierFlagsFromSymbol(sourceProp) & ModifierFlags.NonPublicAccessibilityModifier; + const targetPropAccessibility = getDeclarationModifierFlagsFromSymbol(targetProp) & ModifierFlags.NonPublicAccessibilityModifier; + if (sourcePropAccessibility !== targetPropAccessibility) { + return Ternary.False; + } + if (sourcePropAccessibility) { + if (getTargetSymbol(sourceProp) !== getTargetSymbol(targetProp)) { + return Ternary.False; } - - // If the call is incomplete, we should skip the lower bound check. - // JSX signatures can have extra parameters provided by the library which we don't check - if (callIsIncomplete || argCount >= effectiveMinimumArguments) { - return true; + } + else { + if ((sourceProp.flags & SymbolFlags.Optional) !== (targetProp.flags & SymbolFlags.Optional)) { + return Ternary.False; } - for (let i = argCount; i < effectiveMinimumArguments; i++) { - const type = getTypeAtPosition(signature, i); - if (filterType(type, acceptsVoid).flags & TypeFlags.Never) { - return false; + } + if (isReadonlySymbol(sourceProp) !== isReadonlySymbol(targetProp)) { + return Ternary.False; + } + return compareTypes(getTypeOfSymbol(sourceProp), getTypeOfSymbol(targetProp)); + } + function isMatchingSignature(source: ts.Signature, target: ts.Signature, partialMatch: boolean) { + const sourceParameterCount = getParameterCount(source); + const targetParameterCount = getParameterCount(target); + const sourceMinArgumentCount = getMinArgumentCount(source); + const targetMinArgumentCount = getMinArgumentCount(target); + const sourceHasRestParameter = hasEffectiveRestParameter(source); + const targetHasRestParameter = hasEffectiveRestParameter(target); + // A source signature matches a target signature if the two signatures have the same number of required, + // optional, and rest parameters. + if (sourceParameterCount === targetParameterCount && + sourceMinArgumentCount === targetMinArgumentCount && + sourceHasRestParameter === targetHasRestParameter) { + return true; + } + // A source signature partially matches a target signature if the target signature has no fewer required + // parameters + if (partialMatch && sourceMinArgumentCount <= targetMinArgumentCount) { + return true; + } + return false; + } + /** + * See signatureRelatedTo, compareSignaturesIdentical + */ + function compareSignaturesIdentical(source: ts.Signature, target: ts.Signature, partialMatch: boolean, ignoreThisTypes: boolean, ignoreReturnTypes: boolean, compareTypes: (s: ts.Type, t: ts.Type) => Ternary): Ternary { + // TODO (drosen): De-duplicate code between related functions. + if (source === target) { + return Ternary.True; + } + if (!(isMatchingSignature(source, target, partialMatch))) { + return Ternary.False; + } + // Check that the two signatures have the same number of type parameters. + if (length(source.typeParameters) !== length(target.typeParameters)) { + return Ternary.False; + } + // Check that type parameter constraints and defaults match. If they do, instantiate the source + // signature with the type parameters of the target signature and continue the comparison. + if (target.typeParameters) { + const mapper = createTypeMapper(source.typeParameters!, target.typeParameters); + for (let i = 0; i < target.typeParameters.length; i++) { + const s = source.typeParameters![i]; + const t = target.typeParameters[i]; + if (!(s === t || compareTypes(instantiateType(getConstraintFromTypeParameter(s), mapper) || unknownType, getConstraintFromTypeParameter(t) || unknownType) && + compareTypes(instantiateType(getDefaultFromTypeParameter(s), mapper) || unknownType, getDefaultFromTypeParameter(t) || unknownType))) { + return Ternary.False; } } - return true; + source = instantiateSignature(source, mapper, /*eraseTypeParameters*/ true); + } + let result = Ternary.True; + if (!ignoreThisTypes) { + const sourceThisType = getThisTypeOfSignature(source); + if (sourceThisType) { + const targetThisType = getThisTypeOfSignature(target); + if (targetThisType) { + const related = compareTypes(sourceThisType, targetThisType); + if (!related) { + return Ternary.False; + } + result &= related; + } + } + } + const targetLen = getParameterCount(target); + for (let i = 0; i < targetLen; i++) { + const s = getTypeAtPosition(source, i); + const t = getTypeAtPosition(target, i); + const related = compareTypes(t, s); + if (!related) { + return Ternary.False; + } + result &= related; + } + if (!ignoreReturnTypes) { + const sourceTypePredicate = getTypePredicateOfSignature(source); + const targetTypePredicate = getTypePredicateOfSignature(target); + result &= sourceTypePredicate || targetTypePredicate ? + compareTypePredicatesIdentical(sourceTypePredicate, targetTypePredicate, compareTypes) : + compareTypes(getReturnTypeOfSignature(source), getReturnTypeOfSignature(target)); + } + return result; + } + function compareTypePredicatesIdentical(source: TypePredicate | undefined, target: TypePredicate | undefined, compareTypes: (s: ts.Type, t: ts.Type) => Ternary): Ternary { + return !(source && target && typePredicateKindsMatch(source, target)) ? Ternary.False : + source.type === target.type ? Ternary.True : + source.type && target.type ? compareTypes(source.type, target.type) : + Ternary.False; + } + function literalTypesWithSameBaseType(types: ts.Type[]): boolean { + let commonBaseType: ts.Type | undefined; + for (const t of types) { + const baseType = getBaseTypeOfLiteralType(t); + if (!commonBaseType) { + commonBaseType = baseType; + } + if (baseType === t || baseType !== commonBaseType) { + return false; + } + } + return true; + } + // When the candidate types are all literal types with the same base type, return a union + // of those literal types. Otherwise, return the leftmost type for which no type to the + // right is a supertype. + function getSupertypeOrUnion(types: ts.Type[]): ts.Type { + return literalTypesWithSameBaseType(types) ? + getUnionType(types) : + reduceLeft(types, (s, t) => isTypeSubtypeOf(s, t) ? t : s)!; + } + function getCommonSupertype(types: ts.Type[]): ts.Type { + if (!strictNullChecks) { + return getSupertypeOrUnion(types); + } + const primaryTypes = filter(types, t => !(t.flags & TypeFlags.Nullable)); + return primaryTypes.length ? + getNullableType(getSupertypeOrUnion(primaryTypes), getFalsyFlagsOfTypes(types) & TypeFlags.Nullable) : + getUnionType(types, UnionReduction.Subtype); + } + // Return the leftmost type for which no type to the right is a subtype. + function getCommonSubtype(types: ts.Type[]) { + return reduceLeft(types, (s, t) => isTypeSubtypeOf(t, s) ? t : s)!; + } + function isArrayType(type: ts.Type): boolean { + return !!(getObjectFlags(type) & ObjectFlags.Reference) && ((type).target === globalArrayType || (type).target === globalReadonlyArrayType); + } + function isReadonlyArrayType(type: ts.Type): boolean { + return !!(getObjectFlags(type) & ObjectFlags.Reference) && (type).target === globalReadonlyArrayType; + } + function isMutableArrayOrTuple(type: ts.Type): boolean { + return isArrayType(type) && !isReadonlyArrayType(type) || isTupleType(type) && !type.target.readonly; + } + function getElementTypeOfArrayType(type: ts.Type): ts.Type | undefined { + return isArrayType(type) ? getTypeArguments((type as TypeReference))[0] : undefined; + } + function isArrayLikeType(type: ts.Type): boolean { + // A type is array-like if it is a reference to the global Array or global ReadonlyArray type, + // or if it is not the undefined or null type and if it is assignable to ReadonlyArray + return isArrayType(type) || !(type.flags & TypeFlags.Nullable) && isTypeAssignableTo(type, anyReadonlyArrayType); + } + function isEmptyArrayLiteralType(type: ts.Type): boolean { + const elementType = isArrayType(type) ? getTypeArguments((type))[0] : undefined; + return elementType === undefinedWideningType || elementType === implicitNeverType; + } + function isTupleLikeType(type: ts.Type): boolean { + return isTupleType(type) || !!getPropertyOfType(type, ("0" as __String)); + } + function isArrayOrTupleLikeType(type: ts.Type): boolean { + return isArrayLikeType(type) || isTupleLikeType(type); + } + function getTupleElementType(type: ts.Type, index: number) { + const propType = getTypeOfPropertyOfType(type, ("" + index as __String)); + if (propType) { + return propType; + } + if (everyType(type, isTupleType)) { + return mapType(type, t => getRestTypeOfTupleType((t)) || undefinedType); + } + return undefined; + } + function isNeitherUnitTypeNorNever(type: ts.Type): boolean { + return !(type.flags & (TypeFlags.Unit | TypeFlags.Never)); + } + function isUnitType(type: ts.Type): boolean { + return !!(type.flags & TypeFlags.Unit); + } + function isLiteralType(type: ts.Type): boolean { + return type.flags & TypeFlags.Boolean ? true : + type.flags & TypeFlags.Union ? type.flags & TypeFlags.EnumLiteral ? true : every((type).types, isUnitType) : + isUnitType(type); + } + function getBaseTypeOfLiteralType(type: ts.Type): ts.Type { + return type.flags & TypeFlags.EnumLiteral ? getBaseTypeOfEnumLiteralType((type)) : + type.flags & TypeFlags.StringLiteral ? stringType : + type.flags & TypeFlags.NumberLiteral ? numberType : + type.flags & TypeFlags.BigIntLiteral ? bigintType : + type.flags & TypeFlags.BooleanLiteral ? booleanType : + type.flags & TypeFlags.Union ? getUnionType(sameMap((type).types, getBaseTypeOfLiteralType)) : + type; + } + function getWidenedLiteralType(type: ts.Type): ts.Type { + return type.flags & TypeFlags.EnumLiteral && isFreshLiteralType(type) ? getBaseTypeOfEnumLiteralType((type)) : + type.flags & TypeFlags.StringLiteral && isFreshLiteralType(type) ? stringType : + type.flags & TypeFlags.NumberLiteral && isFreshLiteralType(type) ? numberType : + type.flags & TypeFlags.BigIntLiteral && isFreshLiteralType(type) ? bigintType : + type.flags & TypeFlags.BooleanLiteral && isFreshLiteralType(type) ? booleanType : + type.flags & TypeFlags.Union ? getUnionType(sameMap((type).types, getWidenedLiteralType)) : + type; + } + function getWidenedUniqueESSymbolType(type: ts.Type): ts.Type { + return type.flags & TypeFlags.UniqueESSymbol ? esSymbolType : + type.flags & TypeFlags.Union ? getUnionType(sameMap((type).types, getWidenedUniqueESSymbolType)) : + type; + } + function getWidenedLiteralLikeTypeForContextualType(type: ts.Type, contextualType: ts.Type | undefined) { + if (!isLiteralOfContextualType(type, contextualType)) { + type = getWidenedUniqueESSymbolType(getWidenedLiteralType(type)); } - - function hasCorrectTypeArgumentArity(signature: Signature, typeArguments: NodeArray | undefined) { - // If the user supplied type arguments, but the number of type arguments does not match - // the declared number of type parameters, the call has an incorrect arity. - const numTypeParameters = length(signature.typeParameters); - const minTypeArgumentCount = getMinTypeArgumentCount(signature.typeParameters); - return !some(typeArguments) || - (typeArguments.length >= minTypeArgumentCount && typeArguments.length <= numTypeParameters); + return type; + } + function getWidenedLiteralLikeTypeForContextualReturnTypeIfNeeded(type: ts.Type | undefined, contextualSignatureReturnType: ts.Type | undefined, isAsync: boolean) { + if (type && isUnitType(type)) { + const contextualType = !contextualSignatureReturnType ? undefined : + isAsync ? getPromisedTypeOfPromise(contextualSignatureReturnType) : + contextualSignatureReturnType; + type = getWidenedLiteralLikeTypeForContextualType(type, contextualType); } - - // If type has a single call signature and no other members, return that signature. Otherwise, return undefined. - function getSingleCallSignature(type: Type): Signature | undefined { - return getSingleSignature(type, SignatureKind.Call, /*allowMembers*/ false); + return type; + } + function getWidenedLiteralLikeTypeForContextualIterationTypeIfNeeded(type: ts.Type | undefined, contextualSignatureReturnType: ts.Type | undefined, kind: IterationTypeKind, isAsyncGenerator: boolean) { + if (type && isUnitType(type)) { + const contextualType = !contextualSignatureReturnType ? undefined : + getIterationTypeOfGeneratorFunctionReturnType(kind, contextualSignatureReturnType, isAsyncGenerator); + type = getWidenedLiteralLikeTypeForContextualType(type, contextualType); } - - function getSingleCallOrConstructSignature(type: Type): Signature | undefined { - return getSingleSignature(type, SignatureKind.Call, /*allowMembers*/ false) || - getSingleSignature(type, SignatureKind.Construct, /*allowMembers*/ false); + return type; + } + /** + * Check if a Type was written as a tuple type literal. + * Prefer using isTupleLikeType() unless the use of `elementTypes`/`getTypeArguments` is required. + */ + function isTupleType(type: ts.Type): type is TupleTypeReference { + return !!(getObjectFlags(type) & ObjectFlags.Reference && (type).target.objectFlags & ObjectFlags.Tuple); + } + function getRestTypeOfTupleType(type: TupleTypeReference) { + return type.target.hasRestElement ? getTypeArguments(type)[type.target.typeParameters!.length - 1] : undefined; + } + function getRestArrayTypeOfTupleType(type: TupleTypeReference) { + const restType = getRestTypeOfTupleType(type); + return restType && createArrayType(restType); + } + function getLengthOfTupleType(type: TupleTypeReference) { + return getTypeReferenceArity(type) - (type.target.hasRestElement ? 1 : 0); + } + function isZeroBigInt({ value }: BigIntLiteralType) { + return value.base10Value === "0"; + } + function getFalsyFlagsOfTypes(types: ts.Type[]): TypeFlags { + let result: TypeFlags = 0; + for (const t of types) { + result |= getFalsyFlags(t); } - - function getSingleSignature(type: Type, kind: SignatureKind, allowMembers: boolean): Signature | undefined { - if (type.flags & TypeFlags.Object) { - const resolved = resolveStructuredTypeMembers(type); - if (allowMembers || resolved.properties.length === 0 && !resolved.stringIndexInfo && !resolved.numberIndexInfo) { - if (kind === SignatureKind.Call && resolved.callSignatures.length === 1 && resolved.constructSignatures.length === 0) { - return resolved.callSignatures[0]; - } - if (kind === SignatureKind.Construct && resolved.constructSignatures.length === 1 && resolved.callSignatures.length === 0) { - return resolved.constructSignatures[0]; - } - } - } - return undefined; + return result; + } + // Returns the String, Number, Boolean, StringLiteral, NumberLiteral, BooleanLiteral, Void, Undefined, or Null + // flags for the string, number, boolean, "", 0, false, void, undefined, or null types respectively. Returns + // no flags for all other types (including non-falsy literal types). + function getFalsyFlags(type: ts.Type): TypeFlags { + return type.flags & TypeFlags.Union ? getFalsyFlagsOfTypes((type).types) : + type.flags & TypeFlags.StringLiteral ? (type).value === "" ? TypeFlags.StringLiteral : 0 : + type.flags & TypeFlags.NumberLiteral ? (type).value === 0 ? TypeFlags.NumberLiteral : 0 : + type.flags & TypeFlags.BigIntLiteral ? isZeroBigInt((type)) ? TypeFlags.BigIntLiteral : 0 : + type.flags & TypeFlags.BooleanLiteral ? (type === falseType || type === regularFalseType) ? TypeFlags.BooleanLiteral : 0 : + type.flags & TypeFlags.PossiblyFalsy; + } + function removeDefinitelyFalsyTypes(type: ts.Type): ts.Type { + return getFalsyFlags(type) & TypeFlags.DefinitelyFalsy ? + filterType(type, t => !(getFalsyFlags(t) & TypeFlags.DefinitelyFalsy)) : + type; + } + function extractDefinitelyFalsyTypes(type: ts.Type): ts.Type { + return mapType(type, getDefinitelyFalsyPartOfType); + } + function getDefinitelyFalsyPartOfType(type: ts.Type): ts.Type { + return type.flags & TypeFlags.String ? emptyStringType : + type.flags & TypeFlags.Number ? zeroType : + type.flags & TypeFlags.BigInt ? zeroBigIntType : + type === regularFalseType || + type === falseType || + type.flags & (TypeFlags.Void | TypeFlags.Undefined | TypeFlags.Null) || + type.flags & TypeFlags.StringLiteral && (type).value === "" || + type.flags & TypeFlags.NumberLiteral && (type).value === 0 || + type.flags & TypeFlags.BigIntLiteral && isZeroBigInt((type)) ? type : + neverType; + } + /** + * Add undefined or null or both to a type if they are missing. + * @param type - type to add undefined and/or null to if not present + * @param flags - Either TypeFlags.Undefined or TypeFlags.Null, or both + */ + function getNullableType(type: ts.Type, flags: TypeFlags): ts.Type { + const missing = (flags & ~type.flags) & (TypeFlags.Undefined | TypeFlags.Null); + return missing === 0 ? type : + missing === TypeFlags.Undefined ? getUnionType([type, undefinedType]) : + missing === TypeFlags.Null ? getUnionType([type, nullType]) : + getUnionType([type, undefinedType, nullType]); + } + function getOptionalType(type: ts.Type): ts.Type { + Debug.assert(strictNullChecks); + return type.flags & TypeFlags.Undefined ? type : getUnionType([type, undefinedType]); + } + function getGlobalNonNullableTypeInstantiation(type: ts.Type) { + if (!deferredGlobalNonNullableTypeAlias) { + deferredGlobalNonNullableTypeAlias = getGlobalSymbol(("NonNullable" as __String), SymbolFlags.TypeAlias, /*diagnostic*/ undefined) || unknownSymbol; } - - // Instantiate a generic signature in the context of a non-generic signature (section 3.8.5 in TypeScript spec) - function instantiateSignatureInContextOf(signature: Signature, contextualSignature: Signature, inferenceContext?: InferenceContext, compareTypes?: TypeComparer): Signature { - const context = createInferenceContext(signature.typeParameters!, signature, InferenceFlags.None, compareTypes); - // We clone the inferenceContext to avoid fixing. For example, when the source signature is (x: T) => T[] and - // the contextual signature is (...args: A) => B, we want to infer the element type of A's constraint (say 'any') - // for T but leave it possible to later infer '[any]' back to A. - const restType = getEffectiveRestType(contextualSignature); - const mapper = inferenceContext && (restType && restType.flags & TypeFlags.TypeParameter ? inferenceContext.nonFixingMapper : inferenceContext.mapper); - const sourceSignature = mapper ? instantiateSignature(contextualSignature, mapper) : contextualSignature; - applyToParameterTypes(sourceSignature, signature, (source, target) => { - // Type parameters from outer context referenced by source type are fixed by instantiation of the source type - inferTypes(context.inferences, source, target); - }); - if (!inferenceContext) { - applyToReturnTypes(contextualSignature, signature, (source, target) => { - inferTypes(context.inferences, source, target, InferencePriority.ReturnType); - }); - } - return getSignatureInstantiation(signature, getInferredTypes(context), isInJSFile(contextualSignature.declaration)); + // Use NonNullable global type alias if available to improve quick info/declaration emit + if (deferredGlobalNonNullableTypeAlias !== unknownSymbol) { + return getTypeAliasInstantiation(deferredGlobalNonNullableTypeAlias, [type]); } - - function inferJsxTypeArguments(node: JsxOpeningLikeElement, signature: Signature, checkMode: CheckMode, context: InferenceContext): Type[] { - const paramType = getEffectiveFirstArgumentForJsxSignature(signature, node); - const checkAttrType = checkExpressionWithContextualType(node.attributes, paramType, context, checkMode); - inferTypes(context.inferences, checkAttrType, paramType); - return getInferredTypes(context); + return getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull); // Type alias unavailable, fall back to non-higher-order behavior + } + function getNonNullableType(type: ts.Type): ts.Type { + return strictNullChecks ? getGlobalNonNullableTypeInstantiation(type) : type; + } + function addOptionalTypeMarker(type: ts.Type) { + return strictNullChecks ? getUnionType([type, optionalType]) : type; + } + function isNotOptionalTypeMarker(type: ts.Type) { + return type !== optionalType; + } + function removeOptionalTypeMarker(type: ts.Type): ts.Type { + return strictNullChecks ? filterType(type, isNotOptionalTypeMarker) : type; + } + function propagateOptionalTypeMarker(type: ts.Type, node: OptionalChain, wasOptional: boolean) { + return wasOptional ? isOutermostOptionalChain(node) ? getOptionalType(type) : addOptionalTypeMarker(type) : type; + } + function getOptionalExpressionType(exprType: ts.Type, expression: Expression) { + return isExpressionOfOptionalChainRoot(expression) ? getNonNullableType(exprType) : + isOptionalChain(expression) ? removeOptionalTypeMarker(exprType) : + exprType; + } + /** + * Is source potentially coercible to target type under `==`. + * Assumes that `source` is a constituent of a union, hence + * the boolean literal flag on the LHS, but not on the RHS. + * + * This does not fully replicate the semantics of `==`. The + * intention is to catch cases that are clearly not right. + * + * Comparing (string | number) to number should not remove the + * string element. + * + * Comparing (string | number) to 1 will remove the string + * element, though this is not sound. This is a pragmatic + * choice. + * + * @see narrowTypeByEquality + * + * @param source + * @param target + */ + function isCoercibleUnderDoubleEquals(source: ts.Type, target: ts.Type): boolean { + return ((source.flags & (TypeFlags.Number | TypeFlags.String | TypeFlags.BooleanLiteral)) !== 0) + && ((target.flags & (TypeFlags.Number | TypeFlags.String | TypeFlags.Boolean)) !== 0); + } + /** + * Return true if type was inferred from an object literal, written as an object type literal, or is the shape of a module + * with no call or construct signatures. + */ + function isObjectTypeWithInferableIndex(type: ts.Type): boolean { + return type.flags & TypeFlags.Intersection ? every((type).types, isObjectTypeWithInferableIndex) : + !!(type.symbol && (type.symbol.flags & (SymbolFlags.ObjectLiteral | SymbolFlags.TypeLiteral | SymbolFlags.Enum | SymbolFlags.ValueModule)) !== 0 && + !typeHasCallOrConstructSignatures(type)) || !!(getObjectFlags(type) & ObjectFlags.ReverseMapped && isObjectTypeWithInferableIndex((type as ReverseMappedType).source)); + } + function createSymbolWithType(source: ts.Symbol, type: ts.Type | undefined) { + const symbol = createSymbol(source.flags, source.escapedName, getCheckFlags(source) & CheckFlags.Readonly); + symbol.declarations = source.declarations; + symbol.parent = source.parent; + symbol.type = type; + symbol.target = source; + if (source.valueDeclaration) { + symbol.valueDeclaration = source.valueDeclaration; + } + const nameType = getSymbolLinks(source).nameType; + if (nameType) { + symbol.nameType = nameType; + } + return symbol; + } + function transformTypeOfMembers(type: ts.Type, f: (propertyType: ts.Type) => ts.Type) { + const members = createSymbolTable(); + for (const property of getPropertiesOfObjectType(type)) { + const original = getTypeOfSymbol(property); + const updated = f(original); + members.set(property.escapedName, updated === original ? property : createSymbolWithType(property, updated)); + } + return members; + } + /** + * If the the provided object literal is subject to the excess properties check, + * create a new that is exempt. Recursively mark object literal members as exempt. + * Leave signatures alone since they are not subject to the check. + */ + function getRegularTypeOfObjectLiteral(type: ts.Type): ts.Type { + if (!(isObjectLiteralType(type) && getObjectFlags(type) & ObjectFlags.FreshLiteral)) { + return type; } - - function inferTypeArguments(node: CallLikeExpression, signature: Signature, args: readonly Expression[], checkMode: CheckMode, context: InferenceContext): Type[] { - if (isJsxOpeningLikeElement(node)) { - return inferJsxTypeArguments(node, signature, checkMode, context); - } - - // If a contextual type is available, infer from that type to the return type of the call expression. For - // example, given a 'function wrap(cb: (x: T) => U): (x: T) => U' and a call expression - // 'let f: (x: string) => number = wrap(s => s.length)', we infer from the declared type of 'f' to the - // return type of 'wrap'. - if (node.kind !== SyntaxKind.Decorator) { - const contextualType = getContextualType(node); - if (contextualType) { - // We clone the inference context to avoid disturbing a resolution in progress for an - // outer call expression. Effectively we just want a snapshot of whatever has been - // inferred for any outer call expression so far. - const outerContext = getInferenceContext(node); - const outerMapper = getMapperFromContext(cloneInferenceContext(outerContext, InferenceFlags.NoDefault)); - const instantiatedType = instantiateType(contextualType, outerMapper); - // If the contextual type is a generic function type with a single call signature, we - // instantiate the type with its own type parameters and type arguments. This ensures that - // the type parameters are not erased to type any during type inference such that they can - // be inferred as actual types from the contextual type. For example: - // declare function arrayMap(f: (x: T) => U): (a: T[]) => U[]; - // const boxElements: (a: A[]) => { value: A }[] = arrayMap(value => ({ value })); - // Above, the type of the 'value' parameter is inferred to be 'A'. - const contextualSignature = getSingleCallSignature(instantiatedType); - const inferenceSourceType = contextualSignature && contextualSignature.typeParameters ? - getOrCreateTypeFromSignature(getSignatureInstantiationWithoutFillingInTypeArguments(contextualSignature, contextualSignature.typeParameters)) : - instantiatedType; - const inferenceTargetType = getReturnTypeOfSignature(signature); - // Inferences made from return types have lower priority than all other inferences. - inferTypes(context.inferences, inferenceSourceType, inferenceTargetType, InferencePriority.ReturnType); - // Create a type mapper for instantiating generic contextual types using the inferences made - // from the return type. We need a separate inference pass here because (a) instantiation of - // the source type uses the outer context's return mapper (which excludes inferences made from - // outer arguments), and (b) we don't want any further inferences going into this context. - const returnContext = createInferenceContext(signature.typeParameters!, signature, context.flags); - const returnSourceType = instantiateType(contextualType, outerContext && outerContext.returnMapper); - inferTypes(returnContext.inferences, returnSourceType, inferenceTargetType); - context.returnMapper = some(returnContext.inferences, hasInferenceCandidates) ? getMapperFromContext(cloneInferredPartOfContext(returnContext)) : undefined; + const regularType = (type).regularType; + if (regularType) { + return regularType; + } + const resolved = (type); + const members = transformTypeOfMembers(type, getRegularTypeOfObjectLiteral); + const regularNew = createAnonymousType(resolved.symbol, members, resolved.callSignatures, resolved.constructSignatures, resolved.stringIndexInfo, resolved.numberIndexInfo); + regularNew.flags = resolved.flags; + regularNew.objectFlags |= resolved.objectFlags & ~ObjectFlags.FreshLiteral; + (type).regularType = regularNew; + return regularNew; + } + function createWideningContext(parent: WideningContext | undefined, propertyName: __String | undefined, siblings: ts.Type[] | undefined): WideningContext { + return { parent, propertyName, siblings, resolvedProperties: undefined }; + } + function getSiblingsOfContext(context: WideningContext): ts.Type[] { + if (!context.siblings) { + const siblings: ts.Type[] = []; + for (const type of getSiblingsOfContext(context.parent!)) { + if (isObjectLiteralType(type)) { + const prop = getPropertyOfObjectType(type, context.propertyName!); + if (prop) { + forEachType(getTypeOfSymbol(prop), t => { + siblings.push(t); + }); + } } } - - const thisType = getThisTypeOfSignature(signature); - if (thisType) { - const thisArgumentNode = getThisArgumentOfCall(node); - const thisArgumentType = thisArgumentNode ? checkExpression(thisArgumentNode) : voidType; - inferTypes(context.inferences, thisArgumentType, thisType); - } - - const restType = getNonArrayRestType(signature); - const argCount = restType ? Math.min(getParameterCount(signature) - 1, args.length) : args.length; - for (let i = 0; i < argCount; i++) { - const arg = args[i]; - if (arg.kind !== SyntaxKind.OmittedExpression) { - const paramType = getTypeAtPosition(signature, i); - const argType = checkExpressionWithContextualType(arg, paramType, context, checkMode); - inferTypes(context.inferences, argType, paramType); + context.siblings = siblings; + } + return context.siblings; + } + function getPropertiesOfContext(context: WideningContext): ts.Symbol[] { + if (!context.resolvedProperties) { + const names = (createMap() as UnderscoreEscapedMap); + for (const t of getSiblingsOfContext(context)) { + if (isObjectLiteralType(t) && !(getObjectFlags(t) & ObjectFlags.ContainsSpread)) { + for (const prop of getPropertiesOfType(t)) { + names.set(prop.escapedName, prop); + } } } - - if (restType) { - const spreadType = getSpreadArgumentType(args, argCount, args.length, restType, context); - inferTypes(context.inferences, spreadType, restType); - } - - return getInferredTypes(context); - } - - function getArrayifiedType(type: Type) { - return type.flags & TypeFlags.Union ? mapType(type, getArrayifiedType) : - type.flags & (TypeFlags.Any | TypeFlags.Instantiable) || isMutableArrayOrTuple(type) ? type : - isTupleType(type) ? createTupleType(getTypeArguments(type), type.target.minLength, type.target.hasRestElement, /*readonly*/ false, type.target.associatedNames) : - createArrayType(getIndexedAccessType(type, numberType)); + context.resolvedProperties = arrayFrom(names.values()); } - - function getSpreadArgumentType(args: readonly Expression[], index: number, argCount: number, restType: Type, context: InferenceContext | undefined) { - if (index >= argCount - 1) { - const arg = args[argCount - 1]; - if (isSpreadArgument(arg)) { - // We are inferring from a spread expression in the last argument position, i.e. both the parameter - // and the argument are ...x forms. - return arg.kind === SyntaxKind.SyntheticExpression ? - createArrayType((arg).type) : - getArrayifiedType(checkExpressionWithContextualType((arg).expression, restType, context, CheckMode.Normal)); - } - } - const types = []; - let spreadIndex = -1; - for (let i = index; i < argCount; i++) { - const contextualType = getIndexedAccessType(restType, getLiteralType(i - index)); - const argType = checkExpressionWithContextualType(args[i], contextualType, context, CheckMode.Normal); - if (spreadIndex < 0 && isSpreadArgument(args[i])) { - spreadIndex = i - index; - } - const hasPrimitiveContextualType = maybeTypeOfKind(contextualType, TypeFlags.Primitive | TypeFlags.Index); - types.push(hasPrimitiveContextualType ? getRegularTypeOfLiteralType(argType) : getWidenedLiteralType(argType)); - } - return spreadIndex < 0 ? - createTupleType(types) : - createTupleType(append(types.slice(0, spreadIndex), getUnionType(types.slice(spreadIndex))), spreadIndex, /*hasRestElement*/ true); + return context.resolvedProperties; + } + function getWidenedProperty(prop: ts.Symbol, context: WideningContext | undefined): ts.Symbol { + if (!(prop.flags & SymbolFlags.Property)) { + // Since get accessors already widen their return value there is no need to + // widen accessor based properties here. + return prop; + } + const original = getTypeOfSymbol(prop); + const propContext = context && createWideningContext(context, prop.escapedName, /*siblings*/ undefined); + const widened = getWidenedTypeWithContext(original, propContext); + return widened === original ? prop : createSymbolWithType(prop, widened); + } + function getUndefinedProperty(prop: ts.Symbol) { + const cached = undefinedProperties.get(prop.escapedName); + if (cached) { + return cached; + } + const result = createSymbolWithType(prop, undefinedType); + result.flags |= SymbolFlags.Optional; + undefinedProperties.set(prop.escapedName, result); + return result; + } + function getWidenedTypeOfObjectLiteral(type: ts.Type, context: WideningContext | undefined): ts.Type { + const members = createSymbolTable(); + for (const prop of getPropertiesOfObjectType(type)) { + members.set(prop.escapedName, getWidenedProperty(prop, context)); } - - function checkTypeArguments(signature: Signature, typeArgumentNodes: readonly TypeNode[], reportErrors: boolean, headMessage?: DiagnosticMessage): Type[] | undefined { - const isJavascript = isInJSFile(signature.declaration); - const typeParameters = signature.typeParameters!; - const typeArgumentTypes = fillMissingTypeArguments(map(typeArgumentNodes, getTypeFromTypeNode), typeParameters, getMinTypeArgumentCount(typeParameters), isJavascript); - let mapper: TypeMapper | undefined; - for (let i = 0; i < typeArgumentNodes.length; i++) { - Debug.assert(typeParameters[i] !== undefined, "Should not call checkTypeArguments with too many type arguments"); - const constraint = getConstraintOfTypeParameter(typeParameters[i]); - if (constraint) { - const errorInfo = reportErrors && headMessage ? (() => chainDiagnosticMessages(/*details*/ undefined, Diagnostics.Type_0_does_not_satisfy_the_constraint_1)) : undefined; - const typeArgumentHeadMessage = headMessage || Diagnostics.Type_0_does_not_satisfy_the_constraint_1; - if (!mapper) { - mapper = createTypeMapper(typeParameters, typeArgumentTypes); - } - const typeArgument = typeArgumentTypes[i]; - if (!checkTypeAssignableTo( - typeArgument, - getTypeWithThisArgument(instantiateType(constraint, mapper), typeArgument), - reportErrors ? typeArgumentNodes[i] : undefined, - typeArgumentHeadMessage, - errorInfo)) { - return undefined; - } + if (context) { + for (const prop of getPropertiesOfContext(context)) { + if (!members.has(prop.escapedName)) { + members.set(prop.escapedName, getUndefinedProperty(prop)); } } - return typeArgumentTypes; } - - function getJsxReferenceKind(node: JsxOpeningLikeElement): JsxReferenceKind { - if (isJsxIntrinsicIdentifier(node.tagName)) { - return JsxReferenceKind.Mixed; + const stringIndexInfo = getIndexInfoOfType(type, IndexKind.String); + const numberIndexInfo = getIndexInfoOfType(type, IndexKind.Number); + const result = createAnonymousType(type.symbol, members, emptyArray, emptyArray, stringIndexInfo && createIndexInfo(getWidenedType(stringIndexInfo.type), stringIndexInfo.isReadonly), numberIndexInfo && createIndexInfo(getWidenedType(numberIndexInfo.type), numberIndexInfo.isReadonly)); + result.objectFlags |= (getObjectFlags(type) & (ObjectFlags.JSLiteral | ObjectFlags.NonInferrableType)); // Retain js literal flag through widening + return result; + } + function getWidenedType(type: ts.Type) { + return getWidenedTypeWithContext(type, /*context*/ undefined); + } + function getWidenedTypeWithContext(type: ts.Type, context: WideningContext | undefined): ts.Type { + if (getObjectFlags(type) & ObjectFlags.RequiresWidening) { + if (context === undefined && type.widened) { + return type.widened; + } + let result: ts.Type | undefined; + if (type.flags & (TypeFlags.Any | TypeFlags.Nullable)) { + result = anyType; + } + else if (isObjectLiteralType(type)) { + result = getWidenedTypeOfObjectLiteral(type, context); + } + else if (type.flags & TypeFlags.Union) { + const unionContext = context || createWideningContext(/*parent*/ undefined, /*propertyName*/ undefined, (type).types); + const widenedTypes = sameMap((type).types, t => t.flags & TypeFlags.Nullable ? t : getWidenedTypeWithContext(t, unionContext)); + // Widening an empty object literal transitions from a highly restrictive type to + // a highly inclusive one. For that reason we perform subtype reduction here if the + // union includes empty object types (e.g. reducing {} | string to just {}). + result = getUnionType(widenedTypes, some(widenedTypes, isEmptyObjectType) ? UnionReduction.Subtype : UnionReduction.Literal); } - const tagType = getApparentType(checkExpression(node.tagName)); - if (length(getSignaturesOfType(tagType, SignatureKind.Construct))) { - return JsxReferenceKind.Component; + else if (type.flags & TypeFlags.Intersection) { + result = getIntersectionType(sameMap((type).types, getWidenedType)); } - if (length(getSignaturesOfType(tagType, SignatureKind.Call))) { - return JsxReferenceKind.Function; + else if (isArrayType(type) || isTupleType(type)) { + result = createTypeReference((type).target, sameMap(getTypeArguments((type)), getWidenedType)); } - return JsxReferenceKind.Mixed; + if (result && context === undefined) { + type.widened = result; + } + return result || type; } - - /** - * Check if the given signature can possibly be a signature called by the JSX opening-like element. - * @param node a JSX opening-like element we are trying to figure its call signature - * @param signature a candidate signature we are trying whether it is a call signature - * @param relation a relationship to check parameter and argument type - */ - function checkApplicableSignatureForJsxOpeningLikeElement( - node: JsxOpeningLikeElement, - signature: Signature, - relation: Map, - checkMode: CheckMode, - reportErrors: boolean, - containingMessageChain: (() => DiagnosticMessageChain | undefined) | undefined, - errorOutputContainer: { errors?: Diagnostic[], skipLogging?: boolean } - ) { - // Stateless function components can have maximum of three arguments: "props", "context", and "updater". - // However "context" and "updater" are implicit and can't be specify by users. Only the first parameter, props, - // can be specified by users through attributes property. - const paramType = getEffectiveFirstArgumentForJsxSignature(signature, node); - const attributesType = checkExpressionWithContextualType(node.attributes, paramType, /*inferenceContext*/ undefined, checkMode); - return checkTagNameDoesNotExpectTooManyArguments() && checkTypeRelatedToAndOptionallyElaborate( - attributesType, - paramType, - relation, - reportErrors ? node.tagName : undefined, - node.attributes, - /*headMessage*/ undefined, - containingMessageChain, - errorOutputContainer); - - function checkTagNameDoesNotExpectTooManyArguments(): boolean { - const tagType = isJsxOpeningElement(node) || isJsxSelfClosingElement(node) && !isJsxIntrinsicIdentifier(node.tagName) ? checkExpression(node.tagName) : undefined; - if (!tagType) { - return true; - } - const tagCallSignatures = getSignaturesOfType(tagType, SignatureKind.Call); - if (!length(tagCallSignatures)) { - return true; - } - const factory = getJsxFactoryEntity(node); - if (!factory) { - return true; - } - const factorySymbol = resolveEntityName(factory, SymbolFlags.Value, /*ignoreErrors*/ true, /*dontResolveAlias*/ false, node); - if (!factorySymbol) { - return true; - } - - const factoryType = getTypeOfSymbol(factorySymbol); - const callSignatures = getSignaturesOfType(factoryType, SignatureKind.Call); - if (!length(callSignatures)) { - return true; + return type; + } + /** + * Reports implicit any errors that occur as a result of widening 'null' and 'undefined' + * to 'any'. A call to reportWideningErrorsInType is normally accompanied by a call to + * getWidenedType. But in some cases getWidenedType is called without reporting errors + * (type argument inference is an example). + * + * The return value indicates whether an error was in fact reported. The particular circumstances + * are on a best effort basis. Currently, if the null or undefined that causes widening is inside + * an object literal property (arbitrarily deeply), this function reports an error. If no error is + * reported, reportImplicitAnyError is a suitable fallback to report a general error. + */ + function reportWideningErrorsInType(type: ts.Type): boolean { + let errorReported = false; + if (getObjectFlags(type) & ObjectFlags.ContainsWideningType) { + if (type.flags & TypeFlags.Union) { + if (some((type).types, isEmptyObjectType)) { + errorReported = true; } - - let hasFirstParamSignatures = false; - let maxParamCount = 0; - // Check that _some_ first parameter expects a FC-like thing, and that some overload of the SFC expects an acceptable number of arguments - for (const sig of callSignatures) { - const firstparam = getTypeAtPosition(sig, 0); - const signaturesOfParam = getSignaturesOfType(firstparam, SignatureKind.Call); - if (!length(signaturesOfParam)) continue; - for (const paramSig of signaturesOfParam) { - hasFirstParamSignatures = true; - if (hasEffectiveRestParameter(paramSig)) { - return true; // some signature has a rest param, so function components can have an arbitrary number of arguments - } - const paramCount = getParameterCount(paramSig); - if (paramCount > maxParamCount) { - maxParamCount = paramCount; + else { + for (const t of (type).types) { + if (reportWideningErrorsInType(t)) { + errorReported = true; } } } - if (!hasFirstParamSignatures) { - // Not a single signature had a first parameter which expected a signature - for back compat, and - // to guard against generic factories which won't have signatures directly, do not error - return true; - } - let absoluteMinArgCount = Infinity; - for (const tagSig of tagCallSignatures) { - const tagRequiredArgCount = getMinArgumentCount(tagSig); - if (tagRequiredArgCount < absoluteMinArgCount) { - absoluteMinArgCount = tagRequiredArgCount; + } + if (isArrayType(type) || isTupleType(type)) { + for (const t of getTypeArguments((type))) { + if (reportWideningErrorsInType(t)) { + errorReported = true; } } - if (absoluteMinArgCount <= maxParamCount) { - return true; // some signature accepts the number of arguments the function component provides - } - - if (reportErrors) { - const diag = createDiagnosticForNode(node.tagName, Diagnostics.Tag_0_expects_at_least_1_arguments_but_the_JSX_factory_2_provides_at_most_3, entityNameToString(node.tagName), absoluteMinArgCount, entityNameToString(factory), maxParamCount); - const tagNameDeclaration = getSymbolAtLocation(node.tagName)?.valueDeclaration; - if (tagNameDeclaration) { - addRelatedInfo(diag, createDiagnosticForNode(tagNameDeclaration, Diagnostics._0_is_declared_here, entityNameToString(node.tagName))); - } - if (errorOutputContainer && errorOutputContainer.skipLogging) { - (errorOutputContainer.errors || (errorOutputContainer.errors = [])).push(diag); - } - if (!errorOutputContainer.skipLogging) { - diagnostics.add(diag); + } + if (isObjectLiteralType(type)) { + for (const p of getPropertiesOfObjectType(type)) { + const t = getTypeOfSymbol(p); + if (getObjectFlags(t) & ObjectFlags.ContainsWideningType) { + if (!reportWideningErrorsInType(t)) { + error(p.valueDeclaration, Diagnostics.Object_literal_s_property_0_implicitly_has_an_1_type, symbolToString(p), typeToString(getWidenedType(t))); + } + errorReported = true; } } - return false; } } - - function getSignatureApplicabilityError( - node: CallLikeExpression, - args: readonly Expression[], - signature: Signature, - relation: Map, - checkMode: CheckMode, - reportErrors: boolean, - containingMessageChain: (() => DiagnosticMessageChain | undefined) | undefined, - ): readonly Diagnostic[] | undefined { - - const errorOutputContainer: { errors?: Diagnostic[], skipLogging?: boolean } = { errors: undefined, skipLogging: true }; - if (isJsxOpeningLikeElement(node)) { - if (!checkApplicableSignatureForJsxOpeningLikeElement(node, signature, relation, checkMode, reportErrors, containingMessageChain, errorOutputContainer)) { - Debug.assert(!reportErrors || !!errorOutputContainer.errors, "jsx should have errors when reporting errors"); - return errorOutputContainer.errors || emptyArray; - } - return undefined; - } - const thisType = getThisTypeOfSignature(signature); - if (thisType && thisType !== voidType && node.kind !== SyntaxKind.NewExpression) { - // If the called expression is not of the form `x.f` or `x["f"]`, then sourceType = voidType - // If the signature's 'this' type is voidType, then the check is skipped -- anything is compatible. - // If the expression is a new expression, then the check is skipped. - const thisArgumentNode = getThisArgumentOfCall(node); - let thisArgumentType: Type; - if (thisArgumentNode) { - thisArgumentType = checkExpression(thisArgumentNode); - if (isOptionalChainRoot(thisArgumentNode.parent)) { - thisArgumentType = getNonNullableType(thisArgumentType); - } - else if (isOptionalChain(thisArgumentNode.parent)) { - thisArgumentType = removeOptionalTypeMarker(thisArgumentType); - } - } - else { - thisArgumentType = voidType; - } - - const errorNode = reportErrors ? (thisArgumentNode || node) : undefined; - const headMessage = Diagnostics.The_this_context_of_type_0_is_not_assignable_to_method_s_this_of_type_1; - if (!checkTypeRelatedTo(thisArgumentType, thisType, relation, errorNode, headMessage, containingMessageChain, errorOutputContainer)) { - Debug.assert(!reportErrors || !!errorOutputContainer.errors, "this parameter should have errors when reporting errors"); - return errorOutputContainer.errors || emptyArray; + return errorReported; + } + function reportImplicitAny(declaration: Declaration, type: ts.Type, wideningKind?: WideningKind) { + const typeAsString = typeToString(getWidenedType(type)); + if (isInJSFile(declaration) && !isCheckJsEnabledForFile(getSourceFileOfNode(declaration), compilerOptions)) { + // Only report implicit any errors/suggestions in TS and ts-check JS files + return; + } + let diagnostic: DiagnosticMessage; + switch (declaration.kind) { + case SyntaxKind.BinaryExpression: + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.PropertySignature: + diagnostic = noImplicitAny ? Diagnostics.Member_0_implicitly_has_an_1_type : Diagnostics.Member_0_implicitly_has_an_1_type_but_a_better_type_may_be_inferred_from_usage; + break; + case SyntaxKind.Parameter: + const param = (declaration as ParameterDeclaration); + if (isIdentifier(param.name) && + (isCallSignatureDeclaration(param.parent) || isMethodSignature(param.parent) || isFunctionTypeNode(param.parent)) && + param.parent.parameters.indexOf(param) > -1 && + (resolveName(param, param.name.escapedText, SymbolFlags.Type, undefined, param.name.escapedText, /*isUse*/ true) || + param.name.originalKeywordKind && isTypeNodeKind(param.name.originalKeywordKind))) { + const newName = "arg" + param.parent.parameters.indexOf(param); + errorOrSuggestion(noImplicitAny, declaration, Diagnostics.Parameter_has_a_name_but_no_type_Did_you_mean_0_Colon_1, newName, declarationNameToString(param.name)); + return; } - } - const headMessage = Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1; - const restType = getNonArrayRestType(signature); - const argCount = restType ? Math.min(getParameterCount(signature) - 1, args.length) : args.length; - for (let i = 0; i < argCount; i++) { - const arg = args[i]; - if (arg.kind !== SyntaxKind.OmittedExpression) { - const paramType = getTypeAtPosition(signature, i); - const argType = checkExpressionWithContextualType(arg, paramType, /*inferenceContext*/ undefined, checkMode); - // If one or more arguments are still excluded (as indicated by CheckMode.SkipContextSensitive), - // we obtain the regular type of any object literal arguments because we may not have inferred complete - // parameter types yet and therefore excess property checks may yield false positives (see #17041). - const checkArgType = checkMode & CheckMode.SkipContextSensitive ? getRegularTypeOfObjectLiteral(argType) : argType; - if (!checkTypeRelatedToAndOptionallyElaborate(checkArgType, paramType, relation, reportErrors ? arg : undefined, arg, headMessage, containingMessageChain, errorOutputContainer)) { - Debug.assert(!reportErrors || !!errorOutputContainer.errors, "parameter should have errors when reporting errors"); - maybeAddMissingAwaitInfo(arg, checkArgType, paramType); - return errorOutputContainer.errors || emptyArray; - } - } - } - if (restType) { - const spreadType = getSpreadArgumentType(args, argCount, args.length, restType, /*context*/ undefined); - const errorNode = reportErrors ? argCount < args.length ? args[argCount] : node : undefined; - if (!checkTypeRelatedTo(spreadType, restType, relation, errorNode, headMessage, /*containingMessageChain*/ undefined, errorOutputContainer)) { - Debug.assert(!reportErrors || !!errorOutputContainer.errors, "rest parameter should have errors when reporting errors"); - maybeAddMissingAwaitInfo(errorNode, spreadType, restType); - return errorOutputContainer.errors || emptyArray; + diagnostic = (declaration).dotDotDotToken ? + noImplicitAny ? Diagnostics.Rest_parameter_0_implicitly_has_an_any_type : Diagnostics.Rest_parameter_0_implicitly_has_an_any_type_but_a_better_type_may_be_inferred_from_usage : + noImplicitAny ? Diagnostics.Parameter_0_implicitly_has_an_1_type : Diagnostics.Parameter_0_implicitly_has_an_1_type_but_a_better_type_may_be_inferred_from_usage; + break; + case SyntaxKind.BindingElement: + diagnostic = Diagnostics.Binding_element_0_implicitly_has_an_1_type; + if (!noImplicitAny) { + // Don't issue a suggestion for binding elements since the codefix doesn't yet support them. + return; } - } - return undefined; - - function maybeAddMissingAwaitInfo(errorNode: Node | undefined, source: Type, target: Type) { - if (errorNode && reportErrors && errorOutputContainer.errors && errorOutputContainer.errors.length) { - // Bail if target is Promise-like---something else is wrong - if (getAwaitedTypeOfPromise(target)) { - return; + break; + case SyntaxKind.JSDocFunctionType: + error(declaration, Diagnostics.Function_type_which_lacks_return_type_annotation_implicitly_has_an_0_return_type, typeAsString); + return; + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + case SyntaxKind.FunctionExpression: + case SyntaxKind.ArrowFunction: + if (noImplicitAny && !(declaration as NamedDeclaration).name) { + if (wideningKind === WideningKind.GeneratorYield) { + error(declaration, Diagnostics.Generator_implicitly_has_yield_type_0_because_it_does_not_yield_any_values_Consider_supplying_a_return_type_annotation, typeAsString); } - const awaitedTypeOfSource = getAwaitedTypeOfPromise(source); - if (awaitedTypeOfSource && isTypeRelatedTo(awaitedTypeOfSource, target, relation)) { - addRelatedInfo(errorOutputContainer.errors[0], createDiagnosticForNode(errorNode, Diagnostics.Did_you_forget_to_use_await)); + else { + error(declaration, Diagnostics.Function_expression_which_lacks_return_type_annotation_implicitly_has_an_0_return_type, typeAsString); } + return; } - } - } - - /** - * Returns the this argument in calls like x.f(...) and x[f](...). Undefined otherwise. - */ - function getThisArgumentOfCall(node: CallLikeExpression): LeftHandSideExpression | undefined { - if (node.kind === SyntaxKind.CallExpression) { - const callee = skipOuterExpressions(node.expression); - if (isAccessExpression(callee)) { - return callee.expression; - } - } - } - - function createSyntheticExpression(parent: Node, type: Type, isSpread?: boolean) { - const result = createNode(SyntaxKind.SyntheticExpression, parent.pos, parent.end); - result.parent = parent; - result.type = type; - result.isSpread = isSpread || false; - return result; - } - - /** - * Returns the effective arguments for an expression that works like a function invocation. - */ - function getEffectiveCallArguments(node: CallLikeExpression): readonly Expression[] { - if (node.kind === SyntaxKind.TaggedTemplateExpression) { - const template = node.template; - const args: Expression[] = [createSyntheticExpression(template, getGlobalTemplateStringsArrayType())]; - if (template.kind === SyntaxKind.TemplateExpression) { - forEach(template.templateSpans, span => { - args.push(span.expression); - }); - } - return args; - } - if (node.kind === SyntaxKind.Decorator) { - return getEffectiveDecoratorArguments(node); - } - if (isJsxOpeningLikeElement(node)) { - return node.attributes.properties.length > 0 || (isJsxOpeningElement(node) && node.parent.children.length > 0) ? [node.attributes] : emptyArray; - } - const args = node.arguments || emptyArray; - const length = args.length; - if (length && isSpreadArgument(args[length - 1]) && getSpreadArgumentIndex(args) === length - 1) { - // We have a spread argument in the last position and no other spread arguments. If the type - // of the argument is a tuple type, spread the tuple elements into the argument list. We can - // call checkExpressionCached because spread expressions never have a contextual type. - const spreadArgument = args[length - 1]; - const type = flowLoopCount ? checkExpression(spreadArgument.expression) : checkExpressionCached(spreadArgument.expression); - if (isTupleType(type)) { - const typeArguments = getTypeArguments(type); - const restIndex = type.target.hasRestElement ? typeArguments.length - 1 : -1; - const syntheticArgs = map(typeArguments, (t, i) => createSyntheticExpression(spreadArgument, t, /*isSpread*/ i === restIndex)); - return concatenate(args.slice(0, length - 1), syntheticArgs); + diagnostic = !noImplicitAny ? Diagnostics._0_implicitly_has_an_1_return_type_but_a_better_type_may_be_inferred_from_usage : + wideningKind === WideningKind.GeneratorYield ? Diagnostics._0_which_lacks_return_type_annotation_implicitly_has_an_1_yield_type : + Diagnostics._0_which_lacks_return_type_annotation_implicitly_has_an_1_return_type; + break; + case SyntaxKind.MappedType: + if (noImplicitAny) { + error(declaration, Diagnostics.Mapped_object_type_implicitly_has_an_any_template_type); } - } - return args; + return; + default: + diagnostic = noImplicitAny ? Diagnostics.Variable_0_implicitly_has_an_1_type : Diagnostics.Variable_0_implicitly_has_an_1_type_but_a_better_type_may_be_inferred_from_usage; } - - /** - * Returns the synthetic argument list for a decorator invocation. - */ - function getEffectiveDecoratorArguments(node: Decorator): readonly Expression[] { - const parent = node.parent; - const expr = node.expression; - switch (parent.kind) { - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ClassExpression: - // For a class decorator, the `target` is the type of the class (e.g. the - // "static" or "constructor" side of the class). - return [ - createSyntheticExpression(expr, getTypeOfSymbol(getSymbolOfNode(parent))) - ]; - case SyntaxKind.Parameter: - // A parameter declaration decorator will have three arguments (see - // `ParameterDecorator` in core.d.ts). - const func = parent.parent; - return [ - createSyntheticExpression(expr, parent.parent.kind === SyntaxKind.Constructor ? getTypeOfSymbol(getSymbolOfNode(func)) : errorType), - createSyntheticExpression(expr, anyType), - createSyntheticExpression(expr, numberType) - ]; - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - // A method or accessor declaration decorator will have two or three arguments (see - // `PropertyDecorator` and `MethodDecorator` in core.d.ts). If we are emitting decorators - // for ES3, we will only pass two arguments. - const hasPropDesc = parent.kind !== SyntaxKind.PropertyDeclaration && languageVersion !== ScriptTarget.ES3; - return [ - createSyntheticExpression(expr, getParentTypeOfClassElement(parent)), - createSyntheticExpression(expr, getClassElementPropertyKeyType(parent)), - createSyntheticExpression(expr, hasPropDesc ? createTypedPropertyDescriptorType(getTypeOfNode(parent)) : anyType) - ]; + errorOrSuggestion(noImplicitAny, declaration, diagnostic, declarationNameToString(getNameOfDeclaration(declaration)), typeAsString); + } + function reportErrorsFromWidening(declaration: Declaration, type: ts.Type, wideningKind?: WideningKind) { + if (produceDiagnostics && noImplicitAny && getObjectFlags(type) & ObjectFlags.ContainsWideningType) { + // Report implicit any error within type if possible, otherwise report error on declaration + if (!reportWideningErrorsInType(type)) { + reportImplicitAny(declaration, type, wideningKind); } - return Debug.fail(); } - - /** - * Returns the argument count for a decorator node that works like a function invocation. - */ - function getDecoratorArgumentCount(node: Decorator, signature: Signature) { - switch (node.parent.kind) { - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ClassExpression: - return 1; - case SyntaxKind.PropertyDeclaration: - return 2; - case SyntaxKind.MethodDeclaration: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - // For ES3 or decorators with only two parameters we supply only two arguments - return languageVersion === ScriptTarget.ES3 || signature.parameters.length <= 2 ? 2 : 3; - case SyntaxKind.Parameter: - return 3; - default: - return Debug.fail(); - } + } + function applyToParameterTypes(source: ts.Signature, target: ts.Signature, callback: (s: ts.Type, t: ts.Type) => void) { + const sourceCount = getParameterCount(source); + const targetCount = getParameterCount(target); + const sourceRestType = getEffectiveRestType(source); + const targetRestType = getEffectiveRestType(target); + const targetNonRestCount = targetRestType ? targetCount - 1 : targetCount; + const paramCount = sourceRestType ? targetNonRestCount : Math.min(sourceCount, targetNonRestCount); + const sourceThisType = getThisTypeOfSignature(source); + if (sourceThisType) { + const targetThisType = getThisTypeOfSignature(target); + if (targetThisType) { + callback(sourceThisType, targetThisType); + } + } + for (let i = 0; i < paramCount; i++) { + callback(getTypeAtPosition(source, i), getTypeAtPosition(target, i)); + } + if (targetRestType) { + callback(getRestTypeAtPosition(source, paramCount), targetRestType); } - function getDiagnosticSpanForCallNode(node: CallExpression, doNotIncludeArguments?: boolean) { - let start: number; - let length: number; - const sourceFile = getSourceFileOfNode(node); - - if (isPropertyAccessExpression(node.expression)) { - const nameSpan = getErrorSpanForNode(sourceFile, node.expression.name); - start = nameSpan.start; - length = doNotIncludeArguments ? nameSpan.length : node.end - start; - } - else { - const expressionSpan = getErrorSpanForNode(sourceFile, node.expression); - start = expressionSpan.start; - length = doNotIncludeArguments ? expressionSpan.length : node.end - start; - } - return { start, length, sourceFile }; + } + function applyToReturnTypes(source: ts.Signature, target: ts.Signature, callback: (s: ts.Type, t: ts.Type) => void) { + const sourceTypePredicate = getTypePredicateOfSignature(source); + const targetTypePredicate = getTypePredicateOfSignature(target); + if (sourceTypePredicate && targetTypePredicate && typePredicateKindsMatch(sourceTypePredicate, targetTypePredicate) && sourceTypePredicate.type && targetTypePredicate.type) { + callback(sourceTypePredicate.type, targetTypePredicate.type); } - function getDiagnosticForCallNode(node: CallLikeExpression, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): DiagnosticWithLocation { - if (isCallExpression(node)) { - const { sourceFile, start, length } = getDiagnosticSpanForCallNode(node); - return createFileDiagnostic(sourceFile, start, length, message, arg0, arg1, arg2, arg3); - } - else { - return createDiagnosticForNode(node, message, arg0, arg1, arg2, arg3); - } + else { + callback(getReturnTypeOfSignature(source), getReturnTypeOfSignature(target)); } - - function getArgumentArityError(node: CallLikeExpression, signatures: readonly Signature[], args: readonly Expression[]) { - let min = Number.POSITIVE_INFINITY; - let max = Number.NEGATIVE_INFINITY; - let belowArgCount = Number.NEGATIVE_INFINITY; - let aboveArgCount = Number.POSITIVE_INFINITY; - - let argCount = args.length; - let closestSignature: Signature | undefined; - for (const sig of signatures) { - const minCount = getMinArgumentCount(sig); - const maxCount = getParameterCount(sig); - if (minCount < argCount && minCount > belowArgCount) belowArgCount = minCount; - if (argCount < maxCount && maxCount < aboveArgCount) aboveArgCount = maxCount; - if (minCount < min) { - min = minCount; - closestSignature = sig; - } - max = Math.max(max, maxCount); - } - - const hasRestParameter = some(signatures, hasEffectiveRestParameter); - const paramRange = hasRestParameter ? min : - min < max ? min + "-" + max : - min; - const hasSpreadArgument = getSpreadArgumentIndex(args) > -1; - if (argCount <= max && hasSpreadArgument) { - argCount--; - } - - let spanArray: NodeArray; - let related: DiagnosticWithLocation | undefined; - - const error = hasRestParameter || hasSpreadArgument ? hasRestParameter && hasSpreadArgument ? Diagnostics.Expected_at_least_0_arguments_but_got_1_or_more : - hasRestParameter ? Diagnostics.Expected_at_least_0_arguments_but_got_1 : - Diagnostics.Expected_0_arguments_but_got_1_or_more : Diagnostics.Expected_0_arguments_but_got_1; - - if (closestSignature && getMinArgumentCount(closestSignature) > argCount && closestSignature.declaration) { - const paramDecl = closestSignature.declaration.parameters[closestSignature.thisParameter ? argCount + 1 : argCount]; - if (paramDecl) { - related = createDiagnosticForNode( - paramDecl, - isBindingPattern(paramDecl.name) ? Diagnostics.An_argument_matching_this_binding_pattern_was_not_provided : Diagnostics.An_argument_for_0_was_not_provided, - !paramDecl.name ? argCount : !isBindingPattern(paramDecl.name) ? idText(getFirstIdentifier(paramDecl.name)) : undefined - ); + } + function createInferenceContext(typeParameters: readonly TypeParameter[], signature: ts.Signature | undefined, flags: InferenceFlags, compareTypes?: TypeComparer): InferenceContext { + return createInferenceContextWorker(typeParameters.map(createInferenceInfo), signature, flags, compareTypes || compareTypesAssignable); + } + function cloneInferenceContext(context: T, extraFlags: InferenceFlags = 0): InferenceContext | (T & undefined) { + return context && createInferenceContextWorker(map(context.inferences, cloneInferenceInfo), context.signature, context.flags | extraFlags, context.compareTypes); + } + function createInferenceContextWorker(inferences: InferenceInfo[], signature: ts.Signature | undefined, flags: InferenceFlags, compareTypes: TypeComparer): InferenceContext { + const context: InferenceContext = { + inferences, + signature, + flags, + compareTypes, + mapper: t => mapToInferredType(context, t, /*fix*/ true), + nonFixingMapper: t => mapToInferredType(context, t, /*fix*/ false), + }; + return context; + } + function mapToInferredType(context: InferenceContext, t: ts.Type, fix: boolean): ts.Type { + const inferences = context.inferences; + for (let i = 0; i < inferences.length; i++) { + const inference = inferences[i]; + if (t === inference.typeParameter) { + if (fix && !inference.isFixed) { + clearCachedInferences(inferences); + inference.isFixed = true; } + return getInferredType(context, i); } - if (min < argCount && argCount < max) { - return getDiagnosticForCallNode(node, Diagnostics.No_overload_expects_0_arguments_but_overloads_do_exist_that_expect_either_1_or_2_arguments, argCount, belowArgCount, aboveArgCount); - } - - if (!hasSpreadArgument && argCount < min) { - const diagnostic = getDiagnosticForCallNode(node, error, paramRange, argCount); - return related ? addRelatedInfo(diagnostic, related) : diagnostic; - } - - if (hasRestParameter || hasSpreadArgument) { - spanArray = createNodeArray(args); - if (hasSpreadArgument && argCount) { - const nextArg = elementAt(args, getSpreadArgumentIndex(args) + 1) || undefined; - spanArray = createNodeArray(args.slice(max > argCount && nextArg ? args.indexOf(nextArg) : Math.min(max, args.length - 1))); - } + } + return t; + } + function clearCachedInferences(inferences: InferenceInfo[]) { + for (const inference of inferences) { + if (!inference.isFixed) { + inference.inferredType = undefined; } - else { - spanArray = createNodeArray(args.slice(max)); + } + } + function createInferenceInfo(typeParameter: TypeParameter): InferenceInfo { + return { + typeParameter, + candidates: undefined, + contraCandidates: undefined, + inferredType: undefined, + priority: undefined, + topLevel: true, + isFixed: false + }; + } + function cloneInferenceInfo(inference: InferenceInfo): InferenceInfo { + return { + typeParameter: inference.typeParameter, + candidates: inference.candidates && inference.candidates.slice(), + contraCandidates: inference.contraCandidates && inference.contraCandidates.slice(), + inferredType: inference.inferredType, + priority: inference.priority, + topLevel: inference.topLevel, + isFixed: inference.isFixed + }; + } + function cloneInferredPartOfContext(context: InferenceContext): InferenceContext | undefined { + const inferences = filter(context.inferences, hasInferenceCandidates); + return inferences.length ? + createInferenceContextWorker(map(inferences, cloneInferenceInfo), context.signature, context.flags, context.compareTypes) : + undefined; + } + function getMapperFromContext(context: T): TypeMapper | (T & undefined) { + return context && context.mapper; + } + // Return true if the given type could possibly reference a type parameter for which + // we perform type inference (i.e. a type parameter of a generic function). We cache + // results for union and intersection types for performance reasons. + function couldContainTypeVariables(type: ts.Type): boolean { + const objectFlags = getObjectFlags(type); + if (objectFlags & ObjectFlags.CouldContainTypeVariablesComputed) { + return !!(objectFlags & ObjectFlags.CouldContainTypeVariables); + } + const result = !!(type.flags & TypeFlags.Instantiable || + objectFlags & ObjectFlags.Reference && ((type).node || forEach(getTypeArguments((type)), couldContainTypeVariables)) || + objectFlags & ObjectFlags.Anonymous && type.symbol && type.symbol.flags & (SymbolFlags.Function | SymbolFlags.Method | SymbolFlags.Class | SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral) && type.symbol.declarations || + objectFlags & (ObjectFlags.Mapped | ObjectFlags.ObjectRestType) || + type.flags & TypeFlags.UnionOrIntersection && !(type.flags & TypeFlags.EnumLiteral) && some((type).types, couldContainTypeVariables)); + if (type.flags & TypeFlags.ObjectFlagsType) { + (type).objectFlags |= ObjectFlags.CouldContainTypeVariablesComputed | (result ? ObjectFlags.CouldContainTypeVariables : 0); + } + return result; + } + function isTypeParameterAtTopLevel(type: ts.Type, typeParameter: TypeParameter): boolean { + return !!(type === typeParameter || + type.flags & TypeFlags.UnionOrIntersection && some((type).types, t => isTypeParameterAtTopLevel(t, typeParameter)) || + type.flags & TypeFlags.Conditional && (isTypeParameterAtTopLevel(getTrueTypeFromConditionalType((type)), typeParameter) || + isTypeParameterAtTopLevel(getFalseTypeFromConditionalType((type)), typeParameter))); + } + /** Create an object with properties named in the string literal type. Every property has type `any` */ + function createEmptyObjectTypeFromStringLiteral(type: ts.Type) { + const members = createSymbolTable(); + forEachType(type, t => { + if (!(t.flags & TypeFlags.StringLiteral)) { + return; } - - spanArray.pos = first(spanArray).pos; - spanArray.end = last(spanArray).end; - if (spanArray.end === spanArray.pos) { - spanArray.end++; + const name = escapeLeadingUnderscores((t as StringLiteralType).value); + const literalProp = createSymbol(SymbolFlags.Property, name); + literalProp.type = anyType; + if (t.symbol) { + literalProp.declarations = t.symbol.declarations; + literalProp.valueDeclaration = t.symbol.valueDeclaration; } - const diagnostic = createDiagnosticForNodeArray( - getSourceFileOfNode(node), spanArray, error, paramRange, argCount); - return related ? addRelatedInfo(diagnostic, related) : diagnostic; + members.set(name, literalProp); + }); + const indexInfo = type.flags & TypeFlags.String ? createIndexInfo(emptyObjectType, /*isReadonly*/ false) : undefined; + return createAnonymousType(undefined, members, emptyArray, emptyArray, indexInfo, undefined); + } + /** + * Infer a suitable input type for a homomorphic mapped type { [P in keyof T]: X }. We construct + * an object type with the same set of properties as the source type, where the type of each + * property is computed by inferring from the source property type to X for the type + * variable T[P] (i.e. we treat the type T[P] as the type variable we're inferring for). + */ + function inferTypeForHomomorphicMappedType(source: ts.Type, target: MappedType, constraint: IndexType): ts.Type | undefined { + const key = source.id + "," + target.id + "," + constraint.id; + if (reverseMappedCache.has(key)) { + return reverseMappedCache.get(key); + } + reverseMappedCache.set(key, undefined); + const type = createReverseMappedType(source, target, constraint); + reverseMappedCache.set(key, type); + return type; + } + // We consider a type to be partially inferable if it isn't marked non-inferable or if it is + // an object literal type with at least one property of an inferable type. For example, an object + // literal { a: 123, b: x => true } is marked non-inferable because it contains a context sensitive + // arrow function, but is considered partially inferable because property 'a' has an inferable type. + function isPartiallyInferableType(type: ts.Type): boolean { + return !(getObjectFlags(type) & ObjectFlags.NonInferrableType) || + isObjectLiteralType(type) && some(getPropertiesOfType(type), prop => isPartiallyInferableType(getTypeOfSymbol(prop))); + } + function createReverseMappedType(source: ts.Type, target: MappedType, constraint: IndexType) { + // We consider a source type reverse mappable if it has a string index signature or if + // it has one or more properties and is of a partially inferable type. + if (!(getIndexInfoOfType(source, IndexKind.String) || getPropertiesOfType(source).length !== 0 && isPartiallyInferableType(source))) { + return undefined; } - - function getTypeArgumentArityError(node: Node, signatures: readonly Signature[], typeArguments: NodeArray) { - const argCount = typeArguments.length; - // No overloads exist - if (signatures.length === 1) { - const sig = signatures[0]; - const min = getMinTypeArgumentCount(sig.typeParameters); - const max = length(sig.typeParameters); - return createDiagnosticForNodeArray(getSourceFileOfNode(node), typeArguments, Diagnostics.Expected_0_type_arguments_but_got_1, min < max ? min + "-" + max : min , argCount); + // For arrays and tuples we infer new arrays and tuples where the reverse mapping has been + // applied to the element type(s). + if (isArrayType(source)) { + return createArrayType(inferReverseMappedType(getTypeArguments((source))[0], target, constraint), isReadonlyArrayType(source)); + } + if (isTupleType(source)) { + const elementTypes = map(getTypeArguments(source), t => inferReverseMappedType(t, target, constraint)); + const minLength = getMappedTypeModifiers(target) & MappedTypeModifiers.IncludeOptional ? + getTypeReferenceArity(source) - (source.target.hasRestElement ? 1 : 0) : source.target.minLength; + return createTupleType(elementTypes, minLength, source.target.hasRestElement, source.target.readonly, source.target.associatedNames); + } + // For all other object types we infer a new object type where the reverse mapping has been + // applied to the type of each property. + const reversed = (createObjectType(ObjectFlags.ReverseMapped | ObjectFlags.Anonymous, /*symbol*/ undefined) as ReverseMappedType); + reversed.source = source; + reversed.mappedType = target; + reversed.constraintType = constraint; + return reversed; + } + function getTypeOfReverseMappedSymbol(symbol: ReverseMappedSymbol) { + return inferReverseMappedType(symbol.propertyType, symbol.mappedType, symbol.constraintType); + } + function inferReverseMappedType(sourceType: ts.Type, target: MappedType, constraint: IndexType): ts.Type { + const typeParameter = (getIndexedAccessType(constraint.type, getTypeParameterFromMappedType(target))); + const templateType = getTemplateTypeFromMappedType(target); + const inference = createInferenceInfo(typeParameter); + inferTypes([inference], sourceType, templateType); + return getTypeFromInference(inference) || unknownType; + } + function* getUnmatchedProperties(source: ts.Type, target: ts.Type, requireOptionalProperties: boolean, matchDiscriminantProperties: boolean): IterableIterator { + const properties = getPropertiesOfType(target); + for (const targetProp of properties) { + // TODO: remove this when we support static private identifier fields and find other solutions to get privateNamesAndStaticFields test to pass + if (isStaticPrivateIdentifierProperty(targetProp)) { + continue; } - // Overloads exist - let belowArgCount = -Infinity; - let aboveArgCount = Infinity; - for (const sig of signatures) { - const min = getMinTypeArgumentCount(sig.typeParameters); - const max = length(sig.typeParameters); - if (min > argCount) { - aboveArgCount = Math.min(aboveArgCount, min); + if (requireOptionalProperties || !(targetProp.flags & SymbolFlags.Optional || getCheckFlags(targetProp) & CheckFlags.Partial)) { + const sourceProp = getPropertyOfType(source, targetProp.escapedName); + if (!sourceProp) { + yield targetProp; } - else if (max < argCount) { - belowArgCount = Math.max(belowArgCount, max); + else if (matchDiscriminantProperties) { + const targetType = getTypeOfSymbol(targetProp); + if (targetType.flags & TypeFlags.Unit) { + const sourceType = getTypeOfSymbol(sourceProp); + if (!(sourceType.flags & TypeFlags.Any || getRegularTypeOfLiteralType(sourceType) === getRegularTypeOfLiteralType(targetType))) { + yield targetProp; + } + } } } - if (belowArgCount !== -Infinity && aboveArgCount !== Infinity) { - return createDiagnosticForNodeArray(getSourceFileOfNode(node), typeArguments, Diagnostics.No_overload_expects_0_type_arguments_but_overloads_do_exist_that_expect_either_1_or_2_type_arguments, argCount, belowArgCount, aboveArgCount); - } - return createDiagnosticForNodeArray(getSourceFileOfNode(node), typeArguments, Diagnostics.Expected_0_type_arguments_but_got_1, belowArgCount === -Infinity ? aboveArgCount : belowArgCount, argCount); } - - function resolveCall(node: CallLikeExpression, signatures: readonly Signature[], candidatesOutArray: Signature[] | undefined, checkMode: CheckMode, callChainFlags: SignatureFlags, fallbackError?: DiagnosticMessage): Signature { - const isTaggedTemplate = node.kind === SyntaxKind.TaggedTemplateExpression; - const isDecorator = node.kind === SyntaxKind.Decorator; - const isJsxOpeningOrSelfClosingElement = isJsxOpeningLikeElement(node); - const reportErrors = !candidatesOutArray; - - let typeArguments: NodeArray | undefined; - - if (!isDecorator) { - typeArguments = (node).typeArguments; - - // We already perform checking on the type arguments on the class declaration itself. - if (isTaggedTemplate || isJsxOpeningOrSelfClosingElement || (node).expression.kind !== SyntaxKind.SuperKeyword) { - forEach(typeArguments, checkSourceElement); - } - } - - const candidates = candidatesOutArray || []; - // reorderCandidates fills up the candidates array directly - reorderCandidates(signatures, candidates, callChainFlags); - if (!candidates.length) { - if (reportErrors) { - diagnostics.add(getDiagnosticForCallNode(node, Diagnostics.Call_target_does_not_contain_any_signatures)); - } - return resolveErrorCall(node); - } - - const args = getEffectiveCallArguments(node); - - // The excludeArgument array contains true for each context sensitive argument (an argument - // is context sensitive it is susceptible to a one-time permanent contextual typing). - // - // The idea is that we will perform type argument inference & assignability checking once - // without using the susceptible parameters that are functions, and once more for those - // parameters, contextually typing each as we go along. - // - // For a tagged template, then the first argument be 'undefined' if necessary because it - // represents a TemplateStringsArray. - // - // For a decorator, no arguments are susceptible to contextual typing due to the fact - // decorators are applied to a declaration by the emitter, and not to an expression. - const isSingleNonGenericCandidate = candidates.length === 1 && !candidates[0].typeParameters; - let argCheckMode = !isDecorator && !isSingleNonGenericCandidate && some(args, isContextSensitive) ? CheckMode.SkipContextSensitive : CheckMode.Normal; - - // The following variables are captured and modified by calls to chooseOverload. - // If overload resolution or type argument inference fails, we want to report the - // best error possible. The best error is one which says that an argument was not - // assignable to a parameter. This implies that everything else about the overload - // was fine. So if there is any overload that is only incorrect because of an - // argument, we will report an error on that one. - // - // function foo(s: string): void; - // function foo(n: number): void; // Report argument error on this overload - // function foo(): void; - // foo(true); - // - // If none of the overloads even made it that far, there are two possibilities. - // There was a problem with type arguments for some overload, in which case - // report an error on that. Or none of the overloads even had correct arity, - // in which case give an arity error. - // - // function foo(x: T): void; // Report type argument error - // function foo(): void; - // foo(0); - // - let candidatesForArgumentError: Signature[] | undefined; - let candidateForArgumentArityError: Signature | undefined; - let candidateForTypeArgumentError: Signature | undefined; - let result: Signature | undefined; - - // If we are in signature help, a trailing comma indicates that we intend to provide another argument, - // so we will only accept overloads with arity at least 1 higher than the current number of provided arguments. - const signatureHelpTrailingComma = - !!(checkMode & CheckMode.IsForSignatureHelp) && node.kind === SyntaxKind.CallExpression && node.arguments.hasTrailingComma; - - // Section 4.12.1: - // if the candidate list contains one or more signatures for which the type of each argument - // expression is a subtype of each corresponding parameter type, the return type of the first - // of those signatures becomes the return type of the function call. - // Otherwise, the return type of the first signature in the candidate list becomes the return - // type of the function call. - // - // Whether the call is an error is determined by assignability of the arguments. The subtype pass - // is just important for choosing the best signature. So in the case where there is only one - // signature, the subtype pass is useless. So skipping it is an optimization. - if (candidates.length > 1) { - result = chooseOverload(candidates, subtypeRelation, signatureHelpTrailingComma); + } + function getUnmatchedProperty(source: ts.Type, target: ts.Type, requireOptionalProperties: boolean, matchDiscriminantProperties: boolean): ts.Symbol | undefined { + const result = getUnmatchedProperties(source, target, requireOptionalProperties, matchDiscriminantProperties).next(); + if (!result.done) + return result.value; + } + function tupleTypesDefinitelyUnrelated(source: TupleTypeReference, target: TupleTypeReference) { + return target.target.minLength > source.target.minLength || + !getRestTypeOfTupleType(target) && (!!getRestTypeOfTupleType(source) || getLengthOfTupleType(target) < getLengthOfTupleType(source)); + } + function typesDefinitelyUnrelated(source: ts.Type, target: ts.Type) { + // Two tuple types with incompatible arities are definitely unrelated. + // Two object types that each have a property that is unmatched in the other are definitely unrelated. + return isTupleType(source) && isTupleType(target) && tupleTypesDefinitelyUnrelated(source, target) || + !!getUnmatchedProperty(source, target, /*requireOptionalProperties*/ false, /*matchDiscriminantProperties*/ true) && + !!getUnmatchedProperty(target, source, /*requireOptionalProperties*/ false, /*matchDiscriminantProperties*/ true); + } + function getTypeFromInference(inference: InferenceInfo) { + return inference.candidates ? getUnionType(inference.candidates, UnionReduction.Subtype) : + inference.contraCandidates ? getIntersectionType(inference.contraCandidates) : + undefined; + } + function hasSkipDirectInferenceFlag(node: Node) { + return !!getNodeLinks(node).skipDirectInference; + } + function isFromInferenceBlockedSource(type: ts.Type) { + return !!(type.symbol && some(type.symbol.declarations, hasSkipDirectInferenceFlag)); + } + function inferTypes(inferences: InferenceInfo[], originalSource: ts.Type, originalTarget: ts.Type, priority: InferencePriority = 0, contravariant = false) { + let symbolStack: ts.Symbol[]; + let visited: ts.Map; + let bivariant = false; + let propagationType: ts.Type; + let inferencePriority = InferencePriority.MaxValue; + let allowComplexConstraintInference = true; + inferFromTypes(originalSource, originalTarget); + function inferFromTypes(source: ts.Type, target: ts.Type): void { + if (!couldContainTypeVariables(target)) { + return; } - if (!result) { - result = chooseOverload(candidates, assignableRelation, signatureHelpTrailingComma); + if (source === wildcardType) { + // We are inferring from an 'any' type. We want to infer this type for every type parameter + // referenced in the target type, so we record it as the propagation type and infer from the + // target to itself. Then, as we find candidates we substitute the propagation type. + const savePropagationType = propagationType; + propagationType = source; + inferFromTypes(target, target); + propagationType = savePropagationType; + return; } - if (result) { - return result; + if (source.aliasSymbol && source.aliasTypeArguments && source.aliasSymbol === target.aliasSymbol) { + // Source and target are types originating in the same generic type alias declaration. + // Simply infer from source type arguments to target type arguments. + inferFromTypeArguments(source.aliasTypeArguments, target.aliasTypeArguments!, getAliasVariances(source.aliasSymbol)); + return; } - - // No signatures were applicable. Now report errors based on the last applicable signature with - // no arguments excluded from assignability checks. - // If candidate is undefined, it means that no candidates had a suitable arity. In that case, - // skip the checkApplicableSignature check. - if (reportErrors) { - if (candidatesForArgumentError) { - if (candidatesForArgumentError.length === 1 || candidatesForArgumentError.length > 3) { - const last = candidatesForArgumentError[candidatesForArgumentError.length - 1]; - let chain: DiagnosticMessageChain | undefined; - if (candidatesForArgumentError.length > 3) { - chain = chainDiagnosticMessages(chain, Diagnostics.The_last_overload_gave_the_following_error); - chain = chainDiagnosticMessages(chain, Diagnostics.No_overload_matches_this_call); - } - const diags = getSignatureApplicabilityError(node, args, last, assignableRelation, CheckMode.Normal, /*reportErrors*/ true, () => chain); - if (diags) { - for (const d of diags) { - if (last.declaration && candidatesForArgumentError.length > 3) { - addRelatedInfo(d, createDiagnosticForNode(last.declaration, Diagnostics.The_last_overload_is_declared_here)); - } - diagnostics.add(d); - } - } - else { - Debug.fail("No error for last overload signature"); - } - } - else { - const allDiagnostics: (readonly DiagnosticRelatedInformation[])[] = []; - let max = 0; - let min = Number.MAX_VALUE; - let minIndex = 0; - let i = 0; - for (const c of candidatesForArgumentError) { - const chain = () => chainDiagnosticMessages(/*details*/ undefined, Diagnostics.Overload_0_of_1_2_gave_the_following_error, i + 1, candidates.length, signatureToString(c)); - const diags = getSignatureApplicabilityError(node, args, c, assignableRelation, CheckMode.Normal, /*reportErrors*/ true, chain); - if (diags) { - if (diags.length <= min) { - min = diags.length; - minIndex = i; - } - max = Math.max(max, diags.length); - allDiagnostics.push(diags); - } - else { - Debug.fail("No error for 3 or fewer overload signatures"); - } - i++; - } - - const diags = max > 1 ? allDiagnostics[minIndex] : flatten(allDiagnostics); - Debug.assert(diags.length > 0, "No errors reported for 3 or fewer overload signatures"); - const chain = chainDiagnosticMessages( - map(diags, d => typeof d.messageText === "string" ? (d as DiagnosticMessageChain) : d.messageText), - Diagnostics.No_overload_matches_this_call); - const related = flatMap(diags, d => (d as Diagnostic).relatedInformation) as DiagnosticRelatedInformation[]; - if (every(diags, d => d.start === diags[0].start && d.length === diags[0].length && d.file === diags[0].file)) { - const { file, start, length } = diags[0]; - diagnostics.add({ file, start, length, code: chain.code, category: chain.category, messageText: chain, relatedInformation: related }); - } - else { - diagnostics.add(createDiagnosticForNodeFromMessageChain(node, chain, related)); - } - } + if (source === target && source.flags & TypeFlags.UnionOrIntersection) { + // When source and target are the same union or intersection type, just relate each constituent + // type to itself. + for (const t of (source).types) { + inferFromTypes(t, t); } - else if (candidateForArgumentArityError) { - diagnostics.add(getArgumentArityError(node, [candidateForArgumentArityError], args)); + return; + } + if (target.flags & TypeFlags.Union) { + // First, infer between identically matching source and target constituents and remove the + // matching types. + const [tempSources, tempTargets] = inferFromMatchingTypes(source.flags & TypeFlags.Union ? (source).types : [source], (target).types, isTypeOrBaseIdenticalTo); + // Next, infer between closely matching source and target constituents and remove + // the matching types. Types closely match when they are instantiations of the same + // object type or instantiations of the same type alias. + const [sources, targets] = inferFromMatchingTypes(tempSources, tempTargets, isTypeCloselyMatchedBy); + if (targets.length === 0) { + return; } - else if (candidateForTypeArgumentError) { - checkTypeArguments(candidateForTypeArgumentError, (node as CallExpression | TaggedTemplateExpression | JsxOpeningLikeElement).typeArguments!, /*reportErrors*/ true, fallbackError); + target = getUnionType(targets); + if (sources.length === 0) { + // All source constituents have been matched and there is nothing further to infer from. + // However, simply making no inferences is undesirable because it could ultimately mean + // inferring a type parameter constraint. Instead, make a lower priority inference from + // the full source to whatever remains in the target. For example, when inferring from + // string to 'string | T', make a lower priority inference of string for T. + inferWithPriority(source, target, InferencePriority.NakedTypeVariable); + return; } - else { - const signaturesWithCorrectTypeArgumentArity = filter(signatures, s => hasCorrectTypeArgumentArity(s, typeArguments)); - if (signaturesWithCorrectTypeArgumentArity.length === 0) { - diagnostics.add(getTypeArgumentArityError(node, signatures, typeArguments!)); - } - else if (!isDecorator) { - diagnostics.add(getArgumentArityError(node, signaturesWithCorrectTypeArgumentArity, args)); - } - else if (fallbackError) { - diagnostics.add(getDiagnosticForCallNode(node, fallbackError)); + source = getUnionType(sources); + } + else if (target.flags & TypeFlags.Intersection && some((target).types, t => !!getInferenceInfoForType(t) || (isGenericMappedType(t) && !!getInferenceInfoForType(getHomomorphicTypeVariable(t) || neverType)))) { + // We reduce intersection types only when they contain naked type parameters. For example, when + // inferring from 'string[] & { extra: any }' to 'string[] & T' we want to remove string[] and + // infer { extra: any } for T. But when inferring to 'string[] & Iterable' we want to keep the + // string[] on the source side and infer string for T. + // Likewise, we consider a homomorphic mapped type constrainted to the target type parameter as similar to a "naked type variable" + // in such scenarios. + if (!(source.flags & TypeFlags.Union)) { + // Infer between identically matching source and target constituents and remove the matching types. + const [sources, targets] = inferFromMatchingTypes(source.flags & TypeFlags.Intersection ? (source).types : [source], (target).types, isTypeIdenticalTo); + if (sources.length === 0 || targets.length === 0) { + return; } + source = getIntersectionType(sources); + target = getIntersectionType(targets); } } - - return getCandidateForOverloadFailure(node, candidates, args, !!candidatesOutArray); - - function chooseOverload(candidates: Signature[], relation: Map, signatureHelpTrailingComma = false) { - candidatesForArgumentError = undefined; - candidateForArgumentArityError = undefined; - candidateForTypeArgumentError = undefined; - - if (isSingleNonGenericCandidate) { - const candidate = candidates[0]; - if (some(typeArguments) || !hasCorrectArity(node, args, candidate, signatureHelpTrailingComma)) { - return undefined; - } - if (getSignatureApplicabilityError(node, args, candidate, relation, CheckMode.Normal, /*reportErrors*/ false, /*containingMessageChain*/ undefined)) { - candidatesForArgumentError = [candidate]; - return undefined; - } - return candidate; + else if (target.flags & (TypeFlags.IndexedAccess | TypeFlags.Substitution)) { + target = getActualTypeVariable(target); + } + if (target.flags & TypeFlags.TypeVariable) { + // If target is a type parameter, make an inference, unless the source type contains + // the anyFunctionType (the wildcard type that's used to avoid contextually typing functions). + // Because the anyFunctionType is internal, it should not be exposed to the user by adding + // it as an inference candidate. Hopefully, a better candidate will come along that does + // not contain anyFunctionType when we come back to this argument for its second round + // of inference. Also, we exclude inferences for silentNeverType (which is used as a wildcard + // when constructing types from type parameters that had no inference candidates). + if (getObjectFlags(source) & ObjectFlags.NonInferrableType || source === nonInferrableAnyType || source === silentNeverType || + (priority & InferencePriority.ReturnType && (source === autoType || source === autoArrayType)) || isFromInferenceBlockedSource(source)) { + return; } - - for (let candidateIndex = 0; candidateIndex < candidates.length; candidateIndex++) { - const candidate = candidates[candidateIndex]; - if (!hasCorrectTypeArgumentArity(candidate, typeArguments) || !hasCorrectArity(node, args, candidate, signatureHelpTrailingComma)) { - continue; - } - - let checkCandidate: Signature; - let inferenceContext: InferenceContext | undefined; - - if (candidate.typeParameters) { - let typeArgumentTypes: Type[] | undefined; - if (some(typeArguments)) { - typeArgumentTypes = checkTypeArguments(candidate, typeArguments, /*reportErrors*/ false); - if (!typeArgumentTypes) { - candidateForTypeArgumentError = candidate; - continue; + const inference = getInferenceInfoForType(target); + if (inference) { + if (!inference.isFixed) { + if (inference.priority === undefined || priority < inference.priority) { + inference.candidates = undefined; + inference.contraCandidates = undefined; + inference.topLevel = true; + inference.priority = priority; + } + if (priority === inference.priority) { + const candidate = propagationType || source; + // We make contravariant inferences only if we are in a pure contravariant position, + // i.e. only if we have not descended into a bivariant position. + if (contravariant && !bivariant) { + if (!contains(inference.contraCandidates, candidate)) { + inference.contraCandidates = append(inference.contraCandidates, candidate); + clearCachedInferences(inferences); + } + } + else if (!contains(inference.candidates, candidate)) { + inference.candidates = append(inference.candidates, candidate); + clearCachedInferences(inferences); } } - else { - inferenceContext = createInferenceContext(candidate.typeParameters, candidate, /*flags*/ isInJSFile(node) ? InferenceFlags.AnyDefault : InferenceFlags.None); - typeArgumentTypes = inferTypeArguments(node, candidate, args, argCheckMode | CheckMode.SkipGenericFunctions, inferenceContext); - argCheckMode |= inferenceContext.flags & InferenceFlags.SkippedGenericFunction ? CheckMode.SkipGenericFunctions : CheckMode.Normal; - } - checkCandidate = getSignatureInstantiation(candidate, typeArgumentTypes, isInJSFile(candidate.declaration), inferenceContext && inferenceContext.inferredTypeParameters); - // If the original signature has a generic rest type, instantiation may produce a - // signature with different arity and we need to perform another arity check. - if (getNonArrayRestType(candidate) && !hasCorrectArity(node, args, checkCandidate, signatureHelpTrailingComma)) { - candidateForArgumentArityError = checkCandidate; - continue; + if (!(priority & InferencePriority.ReturnType) && target.flags & TypeFlags.TypeParameter && inference.topLevel && !isTypeParameterAtTopLevel(originalTarget, (target))) { + inference.topLevel = false; + clearCachedInferences(inferences); } } - else { - checkCandidate = candidate; - } - if (getSignatureApplicabilityError(node, args, checkCandidate, relation, argCheckMode, /*reportErrors*/ false, /*containingMessageChain*/ undefined)) { - // Give preference to error candidates that have no rest parameters (as they are more specific) - (candidatesForArgumentError || (candidatesForArgumentError = [])).push(checkCandidate); - continue; - } - if (argCheckMode) { - // If one or more context sensitive arguments were excluded, we start including - // them now (and keeping do so for any subsequent candidates) and perform a second - // round of type inference and applicability checking for this particular candidate. - argCheckMode = CheckMode.Normal; - if (inferenceContext) { - const typeArgumentTypes = inferTypeArguments(node, candidate, args, argCheckMode, inferenceContext); - checkCandidate = getSignatureInstantiation(candidate, typeArgumentTypes, isInJSFile(candidate.declaration), inferenceContext && inferenceContext.inferredTypeParameters); - // If the original signature has a generic rest type, instantiation may produce a - // signature with different arity and we need to perform another arity check. - if (getNonArrayRestType(candidate) && !hasCorrectArity(node, args, checkCandidate, signatureHelpTrailingComma)) { - candidateForArgumentArityError = checkCandidate; - continue; + inferencePriority = Math.min(inferencePriority, priority); + return; + } + else { + // Infer to the simplified version of an indexed access, if possible, to (hopefully) expose more bare type parameters to the inference engine + const simplified = getSimplifiedType(target, /*writing*/ false); + if (simplified !== target) { + invokeOnce(source, simplified, inferFromTypes); + } + else if (target.flags & TypeFlags.IndexedAccess) { + const indexType = getSimplifiedType((target as IndexedAccessType).indexType, /*writing*/ false); + // Generally simplifications of instantiable indexes are avoided to keep relationship checking correct, however if our target is an access, we can consider + // that key of that access to be "instantiated", since we're looking to find the infernce goal in any way we can. + if (indexType.flags & TypeFlags.Instantiable) { + const simplified = distributeIndexOverObjectType(getSimplifiedType((target as IndexedAccessType).objectType, /*writing*/ false), indexType, /*writing*/ false); + if (simplified && simplified !== target) { + invokeOnce(source, simplified, inferFromTypes); } } - if (getSignatureApplicabilityError(node, args, checkCandidate, relation, argCheckMode, /*reportErrors*/ false, /*containingMessageChain*/ undefined)) { - // Give preference to error candidates that have no rest parameters (as they are more specific) - (candidatesForArgumentError || (candidatesForArgumentError = [])).push(checkCandidate); - continue; - } } - candidates[candidateIndex] = checkCandidate; - return checkCandidate; } - - return undefined; } - } - - // No signature was applicable. We have already reported the errors for the invalid signature. - function getCandidateForOverloadFailure( - node: CallLikeExpression, - candidates: Signature[], - args: readonly Expression[], - hasCandidatesOutArray: boolean, - ): Signature { - Debug.assert(candidates.length > 0); // Else should not have called this. - checkNodeDeferred(node); - // Normally we will combine overloads. Skip this if they have type parameters since that's hard to combine. - // Don't do this if there is a `candidatesOutArray`, - // because then we want the chosen best candidate to be one of the overloads, not a combination. - return hasCandidatesOutArray || candidates.length === 1 || candidates.some(c => !!c.typeParameters) - ? pickLongestCandidateSignature(node, candidates, args) - : createUnionOfSignaturesForOverloadFailure(candidates); - } - - function createUnionOfSignaturesForOverloadFailure(candidates: readonly Signature[]): Signature { - const thisParameters = mapDefined(candidates, c => c.thisParameter); - let thisParameter: Symbol | undefined; - if (thisParameters.length) { - thisParameter = createCombinedSymbolFromTypes(thisParameters, thisParameters.map(getTypeOfParameter)); - } - const { min: minArgumentCount, max: maxNonRestParam } = minAndMax(candidates, getNumNonRestParameters); - const parameters: Symbol[] = []; - for (let i = 0; i < maxNonRestParam; i++) { - const symbols = mapDefined(candidates, s => signatureHasRestParameter(s) ? - i < s.parameters.length - 1 ? s.parameters[i] : last(s.parameters) : - i < s.parameters.length ? s.parameters[i] : undefined); - Debug.assert(symbols.length !== 0); - parameters.push(createCombinedSymbolFromTypes(symbols, mapDefined(candidates, candidate => tryGetTypeAtPosition(candidate, i)))); - } - const restParameterSymbols = mapDefined(candidates, c => signatureHasRestParameter(c) ? last(c.parameters) : undefined); - let flags = SignatureFlags.None; - if (restParameterSymbols.length !== 0) { - const type = createArrayType(getUnionType(mapDefined(candidates, tryGetRestTypeOfSignature), UnionReduction.Subtype)); - parameters.push(createCombinedSymbolForOverloadFailure(restParameterSymbols, type)); - flags |= SignatureFlags.HasRestParameter; + if (getObjectFlags(source) & ObjectFlags.Reference && getObjectFlags(target) & ObjectFlags.Reference && ((source).target === (target).target || isArrayType(source) && isArrayType(target)) && + !((source).node && (target).node)) { + // If source and target are references to the same generic type, infer from type arguments + inferFromTypeArguments(getTypeArguments((source)), getTypeArguments((target)), getVariances((source).target)); } - if (candidates.some(signatureHasLiteralTypes)) { - flags |= SignatureFlags.HasLiteralTypes; + else if (source.flags & TypeFlags.Index && target.flags & TypeFlags.Index) { + contravariant = !contravariant; + inferFromTypes((source).type, (target).type); + contravariant = !contravariant; } - return createSignature( - candidates[0].declaration, - /*typeParameters*/ undefined, // Before calling this we tested for `!candidates.some(c => !!c.typeParameters)`. - thisParameter, - parameters, - /*resolvedReturnType*/ getIntersectionType(candidates.map(getReturnTypeOfSignature)), - /*typePredicate*/ undefined, - minArgumentCount, - flags); - } - - function getNumNonRestParameters(signature: Signature): number { - const numParams = signature.parameters.length; - return signatureHasRestParameter(signature) ? numParams - 1 : numParams; - } - - function createCombinedSymbolFromTypes(sources: readonly Symbol[], types: Type[]): Symbol { - return createCombinedSymbolForOverloadFailure(sources, getUnionType(types, UnionReduction.Subtype)); - } - - function createCombinedSymbolForOverloadFailure(sources: readonly Symbol[], type: Type): Symbol { - // This function is currently only used for erroneous overloads, so it's good enough to just use the first source. - return createSymbolWithType(first(sources), type); - } - - function pickLongestCandidateSignature(node: CallLikeExpression, candidates: Signature[], args: readonly Expression[]): Signature { - // Pick the longest signature. This way we can get a contextual type for cases like: - // declare function f(a: { xa: number; xb: number; }, b: number); - // f({ | - // Also, use explicitly-supplied type arguments if they are provided, so we can get a contextual signature in cases like: - // declare function f(k: keyof T); - // f(" - const bestIndex = getLongestCandidateIndex(candidates, apparentArgumentCount === undefined ? args.length : apparentArgumentCount); - const candidate = candidates[bestIndex]; - const { typeParameters } = candidate; - if (!typeParameters) { - return candidate; + else if ((isLiteralType(source) || source.flags & TypeFlags.String) && target.flags & TypeFlags.Index) { + const empty = createEmptyObjectTypeFromStringLiteral(source); + contravariant = !contravariant; + inferWithPriority(empty, (target as IndexType).type, InferencePriority.LiteralKeyof); + contravariant = !contravariant; } - - const typeArgumentNodes: readonly TypeNode[] | undefined = callLikeExpressionMayHaveTypeArguments(node) ? node.typeArguments : undefined; - const instantiated = typeArgumentNodes - ? createSignatureInstantiation(candidate, getTypeArgumentsFromNodes(typeArgumentNodes, typeParameters, isInJSFile(node))) - : inferSignatureInstantiationForOverloadFailure(node, typeParameters, candidate, args); - candidates[bestIndex] = instantiated; - return instantiated; - } - - function getTypeArgumentsFromNodes(typeArgumentNodes: readonly TypeNode[], typeParameters: readonly TypeParameter[], isJs: boolean): readonly Type[] { - const typeArguments = typeArgumentNodes.map(getTypeOfNode); - while (typeArguments.length > typeParameters.length) { - typeArguments.pop(); + else if (source.flags & TypeFlags.IndexedAccess && target.flags & TypeFlags.IndexedAccess) { + inferFromTypes((source).objectType, (target).objectType); + inferFromTypes((source).indexType, (target).indexType); } - while (typeArguments.length < typeParameters.length) { - typeArguments.push(getConstraintOfTypeParameter(typeParameters[typeArguments.length]) || getDefaultTypeArgumentType(isJs)); + else if (source.flags & TypeFlags.Conditional && target.flags & TypeFlags.Conditional) { + inferFromTypes((source).checkType, (target).checkType); + inferFromTypes((source).extendsType, (target).extendsType); + inferFromTypes(getTrueTypeFromConditionalType((source)), getTrueTypeFromConditionalType((target))); + inferFromTypes(getFalseTypeFromConditionalType((source)), getFalseTypeFromConditionalType((target))); } - return typeArguments; - } - - function inferSignatureInstantiationForOverloadFailure(node: CallLikeExpression, typeParameters: readonly TypeParameter[], candidate: Signature, args: readonly Expression[]): Signature { - const inferenceContext = createInferenceContext(typeParameters, candidate, /*flags*/ isInJSFile(node) ? InferenceFlags.AnyDefault : InferenceFlags.None); - const typeArgumentTypes = inferTypeArguments(node, candidate, args, CheckMode.SkipContextSensitive | CheckMode.SkipGenericFunctions, inferenceContext); - return createSignatureInstantiation(candidate, typeArgumentTypes); - } - - function getLongestCandidateIndex(candidates: Signature[], argsCount: number): number { - let maxParamsIndex = -1; - let maxParams = -1; - - for (let i = 0; i < candidates.length; i++) { - const candidate = candidates[i]; - const paramCount = getParameterCount(candidate); - if (hasEffectiveRestParameter(candidate) || paramCount >= argsCount) { - return i; - } - if (paramCount > maxParams) { - maxParams = paramCount; - maxParamsIndex = i; + else if (target.flags & TypeFlags.Conditional) { + const savePriority = priority; + priority |= contravariant ? InferencePriority.ContravariantConditional : 0; + const targetTypes = [getTrueTypeFromConditionalType((target)), getFalseTypeFromConditionalType((target))]; + inferToMultipleTypes(source, targetTypes, target.flags); + priority = savePriority; + } + else if (target.flags & TypeFlags.UnionOrIntersection) { + inferToMultipleTypes(source, (target).types, target.flags); + } + else if (source.flags & TypeFlags.Union) { + // Source is a union or intersection type, infer from each constituent type + const sourceTypes = (source).types; + for (const sourceType of sourceTypes) { + inferFromTypes(sourceType, target); } } - - return maxParamsIndex; + else { + if (!(priority & InferencePriority.NoConstraints && source.flags & (TypeFlags.Intersection | TypeFlags.Instantiable))) { + const apparentSource = getApparentType(source); + // getApparentType can return _any_ type, since an indexed access or conditional may simplify to any other type. + // If that occurs and it doesn't simplify to an object or intersection, we'll need to restart `inferFromTypes` + // with the simplified source. + if (apparentSource !== source && allowComplexConstraintInference && !(apparentSource.flags & (TypeFlags.Object | TypeFlags.Intersection))) { + // TODO: The `allowComplexConstraintInference` flag is a hack! This forbids inference from complex constraints within constraints! + // This isn't required algorithmically, but rather is used to lower the memory burden caused by performing inference + // that is _too good_ in projects with complicated constraints (eg, fp-ts). In such cases, if we did not limit ourselves + // here, we might produce more valid inferences for types, causing us to do more checks and perform more instantiations + // (in addition to the extra stack depth here) which, in turn, can push the already close process over its limit. + // TL;DR: If we ever become generally more memory efficient (or our resource budget ever increases), we should just + // remove this `allowComplexConstraintInference` flag. + allowComplexConstraintInference = false; + return inferFromTypes(apparentSource, target); + } + source = apparentSource; + } + if (source.flags & (TypeFlags.Object | TypeFlags.Intersection)) { + invokeOnce(source, target, inferFromObjectTypes); + } + } + if (source.flags & TypeFlags.Simplifiable) { + const simplified = getSimplifiedType(source, contravariant); + if (simplified !== source) { + inferFromTypes(simplified, target); + } + } + } + function inferWithPriority(source: ts.Type, target: ts.Type, newPriority: InferencePriority) { + const savePriority = priority; + priority |= newPriority; + inferFromTypes(source, target); + priority = savePriority; + } + function invokeOnce(source: ts.Type, target: ts.Type, action: (source: ts.Type, target: ts.Type) => void) { + const key = source.id + "," + target.id; + const status = visited && visited.get(key); + if (status !== undefined) { + inferencePriority = Math.min(inferencePriority, status); + return; + } + (visited || (visited = createMap())).set(key, InferencePriority.Circularity); + const saveInferencePriority = inferencePriority; + inferencePriority = InferencePriority.MaxValue; + action(source, target); + visited.set(key, inferencePriority); + inferencePriority = Math.min(inferencePriority, saveInferencePriority); } - - function resolveCallExpression(node: CallExpression, candidatesOutArray: Signature[] | undefined, checkMode: CheckMode): Signature { - if (node.expression.kind === SyntaxKind.SuperKeyword) { - const superType = checkSuperExpression(node.expression); - if (isTypeAny(superType)) { - for (const arg of node.arguments) { - checkExpression(arg); // Still visit arguments so they get marked for visibility, etc + function inferFromMatchingTypes(sources: ts.Type[], targets: ts.Type[], matches: (s: ts.Type, t: ts.Type) => boolean): [ts.Type[], ts.Type[]] { + let matchedSources: ts.Type[] | undefined; + let matchedTargets: ts.Type[] | undefined; + for (const t of targets) { + for (const s of sources) { + if (matches(s, t)) { + inferFromTypes(s, t); + matchedSources = appendIfUnique(matchedSources, s); + matchedTargets = appendIfUnique(matchedTargets, t); } - return anySignature; } - if (superType !== errorType) { - // In super call, the candidate signatures are the matching arity signatures of the base constructor function instantiated - // with the type arguments specified in the extends clause. - const baseTypeNode = getEffectiveBaseTypeNode(getContainingClass(node)!); - if (baseTypeNode) { - const baseConstructors = getInstantiatedConstructorsForTypeArguments(superType, baseTypeNode.typeArguments, baseTypeNode); - return resolveCall(node, baseConstructors, candidatesOutArray, checkMode, SignatureFlags.None); - } + } + return [ + matchedSources ? filter(sources, t => !contains(matchedSources, t)) : sources, + matchedTargets ? filter(targets, t => !contains(matchedTargets, t)) : targets, + ]; + } + function inferFromTypeArguments(sourceTypes: readonly ts.Type[], targetTypes: readonly ts.Type[], variances: readonly VarianceFlags[]) { + const count = sourceTypes.length < targetTypes.length ? sourceTypes.length : targetTypes.length; + for (let i = 0; i < count; i++) { + if (i < variances.length && (variances[i] & VarianceFlags.VarianceMask) === VarianceFlags.Contravariant) { + inferFromContravariantTypes(sourceTypes[i], targetTypes[i]); + } + else { + inferFromTypes(sourceTypes[i], targetTypes[i]); } - return resolveUntypedCall(node); } - - let callChainFlags: SignatureFlags; - let funcType = checkExpression(node.expression); - if (isCallChain(node)) { - const nonOptionalType = getOptionalExpressionType(funcType, node.expression); - callChainFlags = nonOptionalType === funcType ? SignatureFlags.None : - isOutermostOptionalChain(node) ? SignatureFlags.IsOuterCallChain : - SignatureFlags.IsInnerCallChain; - funcType = nonOptionalType; + } + function inferFromContravariantTypes(source: ts.Type, target: ts.Type) { + if (strictFunctionTypes || priority & InferencePriority.AlwaysStrict) { + contravariant = !contravariant; + inferFromTypes(source, target); + contravariant = !contravariant; } else { - callChainFlags = SignatureFlags.None; - } - - funcType = checkNonNullTypeWithReporter( - funcType, - node.expression, - reportCannotInvokePossiblyNullOrUndefinedError - ); - - if (funcType === silentNeverType) { - return silentNeverSignature; + inferFromTypes(source, target); } - - const apparentType = getApparentType(funcType); - if (apparentType === errorType) { - // Another error has already been reported - return resolveErrorCall(node); + } + function getInferenceInfoForType(type: ts.Type) { + if (type.flags & TypeFlags.TypeVariable) { + for (const inference of inferences) { + if (type === inference.typeParameter) { + return inference; + } + } } - - // Technically, this signatures list may be incomplete. We are taking the apparent type, - // but we are not including call signatures that may have been added to the Object or - // Function interface, since they have none by default. This is a bit of a leap of faith - // that the user will not add any. - const callSignatures = getSignaturesOfType(apparentType, SignatureKind.Call); - const numConstructSignatures = getSignaturesOfType(apparentType, SignatureKind.Construct).length; - - // TS 1.0 Spec: 4.12 - // In an untyped function call no TypeArgs are permitted, Args can be any argument list, no contextual - // types are provided for the argument expressions, and the result is always of type Any. - if (isUntypedFunctionCall(funcType, apparentType, callSignatures.length, numConstructSignatures)) { - // The unknownType indicates that an error already occurred (and was reported). No - // need to report another error in this case. - if (funcType !== errorType && node.typeArguments) { - error(node, Diagnostics.Untyped_function_calls_may_not_accept_type_arguments); - } - return resolveUntypedCall(node); - } - // If FuncExpr's apparent type(section 3.8.1) is a function type, the call is a typed function call. - // TypeScript employs overload resolution in typed function calls in order to support functions - // with multiple call signatures. - if (!callSignatures.length) { - if (numConstructSignatures) { - error(node, Diagnostics.Value_of_type_0_is_not_callable_Did_you_mean_to_include_new, typeToString(funcType)); + return undefined; + } + function getSingleTypeVariableFromIntersectionTypes(types: ts.Type[]) { + let typeVariable: ts.Type | undefined; + for (const type of types) { + const t = type.flags & TypeFlags.Intersection && find((type).types, t => !!getInferenceInfoForType(t)); + if (!t || typeVariable && t !== typeVariable) { + return undefined; } - else { - let relatedInformation: DiagnosticRelatedInformation | undefined; - if (node.arguments.length === 1) { - const text = getSourceFileOfNode(node).text; - if (isLineBreak(text.charCodeAt(skipTrivia(text, node.expression.end, /* stopAfterLineBreak */ true) - 1))) { - relatedInformation = createDiagnosticForNode(node.expression, Diagnostics.Are_you_missing_a_semicolon); + typeVariable = t; + } + return typeVariable; + } + function inferToMultipleTypes(source: ts.Type, targets: ts.Type[], targetFlags: TypeFlags) { + let typeVariableCount = 0; + if (targetFlags & TypeFlags.Union) { + let nakedTypeVariable: ts.Type | undefined; + const sources = source.flags & TypeFlags.Union ? (source).types : [source]; + const matched = new Array(sources.length); + let inferenceCircularity = false; + // First infer to types that are not naked type variables. For each source type we + // track whether inferences were made from that particular type to some target with + // equal priority (i.e. of equal quality) to what we would infer for a naked type + // parameter. + for (const t of targets) { + if (getInferenceInfoForType(t)) { + nakedTypeVariable = t; + typeVariableCount++; + } + else { + for (let i = 0; i < sources.length; i++) { + const saveInferencePriority = inferencePriority; + inferencePriority = InferencePriority.MaxValue; + inferFromTypes(sources[i], t); + if (inferencePriority === priority) + matched[i] = true; + inferenceCircularity = inferenceCircularity || inferencePriority === InferencePriority.Circularity; + inferencePriority = Math.min(inferencePriority, saveInferencePriority); } } - invocationError(node.expression, apparentType, SignatureKind.Call, relatedInformation); } - return resolveErrorCall(node); - } - // When a call to a generic function is an argument to an outer call to a generic function for which - // inference is in process, we have a choice to make. If the inner call relies on inferences made from - // its contextual type to its return type, deferring the inner call processing allows the best possible - // contextual type to accumulate. But if the outer call relies on inferences made from the return type of - // the inner call, the inner call should be processed early. There's no sure way to know which choice is - // right (only a full unification algorithm can determine that), so we resort to the following heuristic: - // If no type arguments are specified in the inner call and at least one call signature is generic and - // returns a function type, we choose to defer processing. This narrowly permits function composition - // operators to flow inferences through return types, but otherwise processes calls right away. We - // use the resolvingSignature singleton to indicate that we deferred processing. This result will be - // propagated out and eventually turned into nonInferrableType (a type that is assignable to anything and - // from which we never make inferences). - if (checkMode & CheckMode.SkipGenericFunctions && !node.typeArguments && callSignatures.some(isGenericFunctionReturningFunction)) { - skippedGenericFunction(node, checkMode); - return resolvingSignature; - } - // If the function is explicitly marked with `@class`, then it must be constructed. - if (callSignatures.some(sig => isInJSFile(sig.declaration) && !!getJSDocClassTag(sig.declaration!))) { - error(node, Diagnostics.Value_of_type_0_is_not_callable_Did_you_mean_to_include_new, typeToString(funcType)); - return resolveErrorCall(node); - } - - return resolveCall(node, callSignatures, candidatesOutArray, checkMode, callChainFlags); - } - - function isGenericFunctionReturningFunction(signature: Signature) { - return !!(signature.typeParameters && isFunctionType(getReturnTypeOfSignature(signature))); - } - - /** - * TS 1.0 spec: 4.12 - * If FuncExpr is of type Any, or of an object type that has no call or construct signatures - * but is a subtype of the Function interface, the call is an untyped function call. - */ - function isUntypedFunctionCall(funcType: Type, apparentFuncType: Type, numCallSignatures: number, numConstructSignatures: number): boolean { - // We exclude union types because we may have a union of function types that happen to have no common signatures. - return isTypeAny(funcType) || isTypeAny(apparentFuncType) && !!(funcType.flags & TypeFlags.TypeParameter) || - !numCallSignatures && !numConstructSignatures && !(apparentFuncType.flags & (TypeFlags.Union | TypeFlags.Never)) && isTypeAssignableTo(funcType, globalFunctionType); - } - - function resolveNewExpression(node: NewExpression, candidatesOutArray: Signature[] | undefined, checkMode: CheckMode): Signature { - if (node.arguments && languageVersion < ScriptTarget.ES5) { - const spreadIndex = getSpreadArgumentIndex(node.arguments); - if (spreadIndex >= 0) { - error(node.arguments[spreadIndex], Diagnostics.Spread_operator_in_new_expressions_is_only_available_when_targeting_ECMAScript_5_and_higher); + if (typeVariableCount === 0) { + // If every target is an intersection of types containing a single naked type variable, + // make a lower priority inference to that type variable. This handles inferring from + // 'A | B' to 'T & (X | Y)' where we want to infer 'A | B' for T. + const intersectionTypeVariable = getSingleTypeVariableFromIntersectionTypes(targets); + if (intersectionTypeVariable) { + inferWithPriority(source, intersectionTypeVariable, InferencePriority.NakedTypeVariable); + } + return; } - } - - let expressionType = checkNonNullExpression(node.expression); - if (expressionType === silentNeverType) { - return silentNeverSignature; - } - - // If expressionType's apparent type(section 3.8.1) is an object type with one or - // more construct signatures, the expression is processed in the same manner as a - // function call, but using the construct signatures as the initial set of candidate - // signatures for overload resolution. The result type of the function call becomes - // the result type of the operation. - expressionType = getApparentType(expressionType); - if (expressionType === errorType) { - // Another error has already been reported - return resolveErrorCall(node); - } - - // TS 1.0 spec: 4.11 - // If expressionType is of type Any, Args can be any argument - // list and the result of the operation is of type Any. - if (isTypeAny(expressionType)) { - if (node.typeArguments) { - error(node, Diagnostics.Untyped_function_calls_may_not_accept_type_arguments); + // If the target has a single naked type variable and no inference circularities were + // encountered above (meaning we explored the types fully), create a union of the source + // types from which no inferences have been made so far and infer from that union to the + // naked type variable. + if (typeVariableCount === 1 && !inferenceCircularity) { + const unmatched = flatMap(sources, (s, i) => matched[i] ? undefined : s); + if (unmatched.length) { + inferFromTypes(getUnionType(unmatched), nakedTypeVariable!); + return; + } } - return resolveUntypedCall(node); } - - // Technically, this signatures list may be incomplete. We are taking the apparent type, - // but we are not including construct signatures that may have been added to the Object or - // Function interface, since they have none by default. This is a bit of a leap of faith - // that the user will not add any. - const constructSignatures = getSignaturesOfType(expressionType, SignatureKind.Construct); - if (constructSignatures.length) { - if (!isConstructorAccessible(node, constructSignatures[0])) { - return resolveErrorCall(node); - } - // If the expression is a class of abstract type, then it cannot be instantiated. - // Note, only class declarations can be declared abstract. - // In the case of a merged class-module or class-interface declaration, - // only the class declaration node will have the Abstract flag set. - const valueDecl = expressionType.symbol && getClassLikeDeclarationOfSymbol(expressionType.symbol); - if (valueDecl && hasModifier(valueDecl, ModifierFlags.Abstract)) { - error(node, Diagnostics.Cannot_create_an_instance_of_an_abstract_class); - return resolveErrorCall(node); + else { + // We infer from types that are not naked type variables first so that inferences we + // make from nested naked type variables and given slightly higher priority by virtue + // of being first in the candidates array. + for (const t of targets) { + if (getInferenceInfoForType(t)) { + typeVariableCount++; + } + else { + inferFromTypes(source, t); + } } - - return resolveCall(node, constructSignatures, candidatesOutArray, checkMode, SignatureFlags.None); } - - // If expressionType's apparent type is an object type with no construct signatures but - // one or more call signatures, the expression is processed as a function call. A compile-time - // error occurs if the result of the function call is not Void. The type of the result of the - // operation is Any. It is an error to have a Void this type. - const callSignatures = getSignaturesOfType(expressionType, SignatureKind.Call); - if (callSignatures.length) { - const signature = resolveCall(node, callSignatures, candidatesOutArray, checkMode, SignatureFlags.None); - if (!noImplicitAny) { - if (signature.declaration && !isJSConstructor(signature.declaration) && getReturnTypeOfSignature(signature) !== voidType) { - error(node, Diagnostics.Only_a_void_function_can_be_called_with_the_new_keyword); - } - if (getThisTypeOfSignature(signature) === voidType) { - error(node, Diagnostics.A_function_that_is_called_with_the_new_keyword_cannot_have_a_this_type_that_is_void); + // Inferences directly to naked type variables are given lower priority as they are + // less specific. For example, when inferring from Promise to T | Promise, + // we want to infer string for T, not Promise | string. For intersection types + // we only infer to single naked type variables. + if (targetFlags & TypeFlags.Intersection ? typeVariableCount === 1 : typeVariableCount > 0) { + for (const t of targets) { + if (getInferenceInfoForType(t)) { + inferWithPriority(source, t, InferencePriority.NakedTypeVariable); } } - return signature; } - - invocationError(node.expression, expressionType, SignatureKind.Construct); - return resolveErrorCall(node); } - - function typeHasProtectedAccessibleBase(target: Symbol, type: InterfaceType): boolean { - const baseTypes = getBaseTypes(type); - if (!length(baseTypes)) { - return false; + function inferToMappedType(source: ts.Type, target: MappedType, constraintType: ts.Type): boolean { + if (constraintType.flags & TypeFlags.Union) { + let result = false; + for (const type of (constraintType as UnionType).types) { + result = inferToMappedType(source, target, type) || result; + } + return result; } - const firstBase = baseTypes[0]; - if (firstBase.flags & TypeFlags.Intersection) { - const types = (firstBase as IntersectionType).types; - const mixinFlags = findMixins(types); - let i = 0; - for (const intersectionMember of (firstBase as IntersectionType).types) { - // We want to ignore mixin ctors - if (!mixinFlags[i]) { - if (getObjectFlags(intersectionMember) & (ObjectFlags.Class | ObjectFlags.Interface)) { - if (intersectionMember.symbol === target) { - return true; - } - if (typeHasProtectedAccessibleBase(target, intersectionMember as InterfaceType)) { - return true; - } - } + if (constraintType.flags & TypeFlags.Index) { + // We're inferring from some source type S to a homomorphic mapped type { [P in keyof T]: X }, + // where T is a type variable. Use inferTypeForHomomorphicMappedType to infer a suitable source + // type and then make a secondary inference from that type to T. We make a secondary inference + // such that direct inferences to T get priority over inferences to Partial, for example. + const inference = getInferenceInfoForType((constraintType).type); + if (inference && !inference.isFixed && !isFromInferenceBlockedSource(source)) { + const inferredType = inferTypeForHomomorphicMappedType(source, target, (constraintType)); + if (inferredType) { + // We assign a lower priority to inferences made from types containing non-inferrable + // types because we may only have a partial result (i.e. we may have failed to make + // reverse inferences for some properties). + inferWithPriority(inferredType, inference.typeParameter, getObjectFlags(source) & ObjectFlags.NonInferrableType ? + InferencePriority.PartialHomomorphicMappedType : + InferencePriority.HomomorphicMappedType); } - i++; } - return false; + return true; } - if (firstBase.symbol === target) { + if (constraintType.flags & TypeFlags.TypeParameter) { + // We're inferring from some source type S to a mapped type { [P in K]: X }, where K is a type + // parameter. First infer from 'keyof S' to K. + inferWithPriority(getIndexType(source), constraintType, InferencePriority.MappedTypeConstraint); + // If K is constrained to a type C, also infer to C. Thus, for a mapped type { [P in K]: X }, + // where K extends keyof T, we make the same inferences as for a homomorphic mapped type + // { [P in keyof T]: X }. This enables us to make meaningful inferences when the target is a + // Pick. + const extendedConstraint = getConstraintOfType(constraintType); + if (extendedConstraint && inferToMappedType(source, target, extendedConstraint)) { + return true; + } + // If no inferences can be made to K's constraint, infer from a union of the property types + // in the source to the template type X. + const propTypes = map(getPropertiesOfType(source), getTypeOfSymbol); + const stringIndexType = getIndexTypeOfType(source, IndexKind.String); + const numberIndexInfo = getNonEnumNumberIndexInfo(source); + const numberIndexType = numberIndexInfo && numberIndexInfo.type; + inferFromTypes(getUnionType(append(append(propTypes, stringIndexType), numberIndexType)), getTemplateTypeFromMappedType(target)); return true; } - return typeHasProtectedAccessibleBase(target, firstBase as InterfaceType); + return false; } - - function isConstructorAccessible(node: NewExpression, signature: Signature) { - if (!signature || !signature.declaration) { - return true; + function inferFromObjectTypes(source: ts.Type, target: ts.Type) { + // If we are already processing another target type with the same associated symbol (such as + // an instantiation of the same generic type), we do not explore this target as it would yield + // no further inferences. We exclude the static side of classes from this check since it shares + // its symbol with the instance side which would lead to false positives. + const isNonConstructorObject = target.flags & TypeFlags.Object && + !(getObjectFlags(target) & ObjectFlags.Anonymous && target.symbol && target.symbol.flags & SymbolFlags.Class); + const symbol = isNonConstructorObject ? target.symbol : undefined; + if (symbol) { + if (contains(symbolStack, symbol)) { + inferencePriority = InferencePriority.Circularity; + return; + } + (symbolStack || (symbolStack = [])).push(symbol); + inferFromObjectTypesWorker(source, target); + symbolStack.pop(); } - - const declaration = signature.declaration; - const modifiers = getSelectedModifierFlags(declaration, ModifierFlags.NonPublicAccessibilityModifier); - - // (1) Public constructors and (2) constructor functions are always accessible. - if (!modifiers || declaration.kind !== SyntaxKind.Constructor) { - return true; + else { + inferFromObjectTypesWorker(source, target); } - - const declaringClassDeclaration = getClassLikeDeclarationOfSymbol(declaration.parent.symbol)!; - const declaringClass = getDeclaredTypeOfSymbol(declaration.parent.symbol); - - // A private or protected constructor can only be instantiated within its own class (or a subclass, for protected) - if (!isNodeWithinClass(node, declaringClassDeclaration)) { - const containingClass = getContainingClass(node); - if (containingClass && modifiers & ModifierFlags.Protected) { - const containingType = getTypeOfNode(containingClass); - if (typeHasProtectedAccessibleBase(declaration.parent.symbol, containingType as InterfaceType)) { - return true; - } - } - if (modifiers & ModifierFlags.Private) { - error(node, Diagnostics.Constructor_of_class_0_is_private_and_only_accessible_within_the_class_declaration, typeToString(declaringClass)); - } - if (modifiers & ModifierFlags.Protected) { - error(node, Diagnostics.Constructor_of_class_0_is_protected_and_only_accessible_within_the_class_declaration, typeToString(declaringClass)); + } + function inferFromObjectTypesWorker(source: ts.Type, target: ts.Type) { + if (getObjectFlags(source) & ObjectFlags.Reference && getObjectFlags(target) & ObjectFlags.Reference && ((source).target === (target).target || isArrayType(source) && isArrayType(target))) { + // If source and target are references to the same generic type, infer from type arguments + inferFromTypeArguments(getTypeArguments((source)), getTypeArguments((target)), getVariances((source).target)); + return; + } + if (isGenericMappedType(source) && isGenericMappedType(target)) { + // The source and target types are generic types { [P in S]: X } and { [P in T]: Y }, so we infer + // from S to T and from X to Y. + inferFromTypes(getConstraintTypeFromMappedType(source), getConstraintTypeFromMappedType(target)); + inferFromTypes(getTemplateTypeFromMappedType(source), getTemplateTypeFromMappedType(target)); + } + if (getObjectFlags(target) & ObjectFlags.Mapped) { + const constraintType = getConstraintTypeFromMappedType((target)); + if (inferToMappedType(source, (target), constraintType)) { + return; } - return false; } - - return true; + // Infer from the members of source and target only if the two types are possibly related + if (!typesDefinitelyUnrelated(source, target)) { + inferFromProperties(source, target); + inferFromSignatures(source, target, SignatureKind.Call); + inferFromSignatures(source, target, SignatureKind.Construct); + inferFromIndexTypes(source, target); + } } - - function invocationErrorDetails(apparentType: Type, kind: SignatureKind): { messageChain: DiagnosticMessageChain, relatedMessage: DiagnosticMessage | undefined } { - let errorInfo: DiagnosticMessageChain | undefined; - const isCall = kind === SignatureKind.Call; - const awaitedType = getAwaitedType(apparentType); - const maybeMissingAwait = awaitedType && getSignaturesOfType(awaitedType, kind).length > 0; - if (apparentType.flags & TypeFlags.Union) { - const types = (apparentType as UnionType).types; - let hasSignatures = false; - for (const constituent of types) { - const signatures = getSignaturesOfType(constituent, kind); - if (signatures.length !== 0) { - hasSignatures = true; - if (errorInfo) { - // Bail early if we already have an error, no chance of "No constituent of type is callable" - break; - } + function inferFromProperties(source: ts.Type, target: ts.Type) { + if (isArrayType(source) || isTupleType(source)) { + if (isTupleType(target)) { + const sourceLength = isTupleType(source) ? getLengthOfTupleType(source) : 0; + const targetLength = getLengthOfTupleType(target); + const sourceRestType = isTupleType(source) ? getRestTypeOfTupleType(source) : getElementTypeOfArrayType(source); + const targetRestType = getRestTypeOfTupleType(target); + const fixedLength = targetLength < sourceLength || sourceRestType ? targetLength : sourceLength; + for (let i = 0; i < fixedLength; i++) { + inferFromTypes(i < sourceLength ? getTypeArguments((source))[i] : sourceRestType!, getTypeArguments(target)[i]); } - else { - // Error on the first non callable constituent only - if (!errorInfo) { - errorInfo = chainDiagnosticMessages( - errorInfo, - isCall ? - Diagnostics.Type_0_has_no_call_signatures : - Diagnostics.Type_0_has_no_construct_signatures, - typeToString(constituent) - ); - errorInfo = chainDiagnosticMessages( - errorInfo, - isCall ? - Diagnostics.Not_all_constituents_of_type_0_are_callable : - Diagnostics.Not_all_constituents_of_type_0_are_constructable, - typeToString(apparentType) - ); + if (targetRestType) { + const types = fixedLength < sourceLength ? getTypeArguments((source)).slice(fixedLength, sourceLength) : []; + if (sourceRestType) { + types.push(sourceRestType); } - if (hasSignatures) { - // Bail early if we already found a siganture, no chance of "No constituent of type is callable" - break; + if (types.length) { + inferFromTypes(getUnionType(types), targetRestType); } } + return; } - if (!hasSignatures) { - errorInfo = chainDiagnosticMessages( - /* detials */ undefined, - isCall ? - Diagnostics.No_constituent_of_type_0_is_callable : - Diagnostics.No_constituent_of_type_0_is_constructable, - typeToString(apparentType) - ); - } - if (!errorInfo) { - errorInfo = chainDiagnosticMessages( - errorInfo, - isCall ? - Diagnostics.Each_member_of_the_union_type_0_has_signatures_but_none_of_those_signatures_are_compatible_with_each_other : - Diagnostics.Each_member_of_the_union_type_0_has_construct_signatures_but_none_of_those_signatures_are_compatible_with_each_other, - typeToString(apparentType) - ); + if (isArrayType(target)) { + inferFromIndexTypes(source, target); + return; } } - else { - errorInfo = chainDiagnosticMessages( - errorInfo, - isCall ? - Diagnostics.Type_0_has_no_call_signatures : - Diagnostics.Type_0_has_no_construct_signatures, - typeToString(apparentType) - ); + const properties = getPropertiesOfObjectType(target); + for (const targetProp of properties) { + const sourceProp = getPropertyOfType(source, targetProp.escapedName); + if (sourceProp) { + inferFromTypes(getTypeOfSymbol(sourceProp), getTypeOfSymbol(targetProp)); + } } - return { - messageChain: chainDiagnosticMessages( - errorInfo, - isCall ? Diagnostics.This_expression_is_not_callable : Diagnostics.This_expression_is_not_constructable - ), - relatedMessage: maybeMissingAwait ? Diagnostics.Did_you_forget_to_use_await : undefined, - }; } - function invocationError(errorTarget: Node, apparentType: Type, kind: SignatureKind, relatedInformation?: DiagnosticRelatedInformation) { - const { messageChain, relatedMessage: relatedInfo } = invocationErrorDetails(apparentType, kind); - const diagnostic = createDiagnosticForNodeFromMessageChain(errorTarget, messageChain); - if (relatedInfo) { - addRelatedInfo(diagnostic, createDiagnosticForNode(errorTarget, relatedInfo)); + function inferFromSignatures(source: ts.Type, target: ts.Type, kind: SignatureKind) { + const sourceSignatures = getSignaturesOfType(source, kind); + const targetSignatures = getSignaturesOfType(target, kind); + const sourceLen = sourceSignatures.length; + const targetLen = targetSignatures.length; + const len = sourceLen < targetLen ? sourceLen : targetLen; + const skipParameters = !!(getObjectFlags(source) & ObjectFlags.NonInferrableType); + for (let i = 0; i < len; i++) { + inferFromSignature(getBaseSignature(sourceSignatures[sourceLen - len + i]), getBaseSignature(targetSignatures[targetLen - len + i]), skipParameters); } - if (isCallExpression(errorTarget.parent)) { - const { start, length } = getDiagnosticSpanForCallNode(errorTarget.parent, /* doNotIncludeArguments */ true); - diagnostic.start = start; - diagnostic.length = length; + } + function inferFromSignature(source: ts.Signature, target: ts.Signature, skipParameters: boolean) { + if (!skipParameters) { + const saveBivariant = bivariant; + const kind = target.declaration ? target.declaration.kind : SyntaxKind.Unknown; + // Once we descend into a bivariant signature we remain bivariant for all nested inferences + bivariant = bivariant || kind === SyntaxKind.MethodDeclaration || kind === SyntaxKind.MethodSignature || kind === SyntaxKind.Constructor; + applyToParameterTypes(source, target, inferFromContravariantTypes); + bivariant = saveBivariant; } - diagnostics.add(diagnostic); - invocationErrorRecovery(apparentType, kind, relatedInformation ? addRelatedInfo(diagnostic, relatedInformation) : diagnostic); + applyToReturnTypes(source, target, inferFromTypes); } - - function invocationErrorRecovery(apparentType: Type, kind: SignatureKind, diagnostic: Diagnostic) { - if (!apparentType.symbol) { - return; + function inferFromIndexTypes(source: ts.Type, target: ts.Type) { + const targetStringIndexType = getIndexTypeOfType(target, IndexKind.String); + if (targetStringIndexType) { + const sourceIndexType = getIndexTypeOfType(source, IndexKind.String) || + getImplicitIndexTypeOfType(source, IndexKind.String); + if (sourceIndexType) { + inferFromTypes(sourceIndexType, targetStringIndexType); + } } - const importNode = getSymbolLinks(apparentType.symbol).originatingImport; - // Create a diagnostic on the originating import if possible onto which we can attach a quickfix - // An import call expression cannot be rewritten into another form to correct the error - the only solution is to use `.default` at the use-site - if (importNode && !isImportCall(importNode)) { - const sigs = getSignaturesOfType(getTypeOfSymbol(getSymbolLinks(apparentType.symbol).target!), kind); - if (!sigs || !sigs.length) return; - - addRelatedInfo(diagnostic, - createDiagnosticForNode(importNode, Diagnostics.Type_originates_at_this_import_A_namespace_style_import_cannot_be_called_or_constructed_and_will_cause_a_failure_at_runtime_Consider_using_a_default_import_or_import_require_here_instead) - ); + const targetNumberIndexType = getIndexTypeOfType(target, IndexKind.Number); + if (targetNumberIndexType) { + const sourceIndexType = getIndexTypeOfType(source, IndexKind.Number) || + getIndexTypeOfType(source, IndexKind.String) || + getImplicitIndexTypeOfType(source, IndexKind.Number); + if (sourceIndexType) { + inferFromTypes(sourceIndexType, targetNumberIndexType); + } } } - - function resolveTaggedTemplateExpression(node: TaggedTemplateExpression, candidatesOutArray: Signature[] | undefined, checkMode: CheckMode): Signature { - const tagType = checkExpression(node.tag); - const apparentType = getApparentType(tagType); - - if (apparentType === errorType) { - // Another error has already been reported - return resolveErrorCall(node); + } + function isTypeOrBaseIdenticalTo(s: ts.Type, t: ts.Type) { + return isTypeIdenticalTo(s, t) || !!(t.flags & TypeFlags.String && s.flags & TypeFlags.StringLiteral || t.flags & TypeFlags.Number && s.flags & TypeFlags.NumberLiteral); + } + function isTypeCloselyMatchedBy(s: ts.Type, t: ts.Type) { + return !!(s.flags & TypeFlags.Object && t.flags & TypeFlags.Object && s.symbol && s.symbol === t.symbol || + s.aliasSymbol && s.aliasTypeArguments && s.aliasSymbol === t.aliasSymbol); + } + function hasPrimitiveConstraint(type: TypeParameter): boolean { + const constraint = getConstraintOfTypeParameter(type); + return !!constraint && maybeTypeOfKind(constraint.flags & TypeFlags.Conditional ? getDefaultConstraintOfConditionalType((constraint as ConditionalType)) : constraint, TypeFlags.Primitive | TypeFlags.Index); + } + function isObjectLiteralType(type: ts.Type) { + return !!(getObjectFlags(type) & ObjectFlags.ObjectLiteral); + } + function isObjectOrArrayLiteralType(type: ts.Type) { + return !!(getObjectFlags(type) & (ObjectFlags.ObjectLiteral | ObjectFlags.ArrayLiteral)); + } + function unionObjectAndArrayLiteralCandidates(candidates: ts.Type[]): ts.Type[] { + if (candidates.length > 1) { + const objectLiterals = filter(candidates, isObjectOrArrayLiteralType); + if (objectLiterals.length) { + const literalsType = getUnionType(objectLiterals, UnionReduction.Subtype); + return concatenate(filter(candidates, t => !isObjectOrArrayLiteralType(t)), [literalsType]); } - - const callSignatures = getSignaturesOfType(apparentType, SignatureKind.Call); - const numConstructSignatures = getSignaturesOfType(apparentType, SignatureKind.Construct).length; - - if (isUntypedFunctionCall(tagType, apparentType, callSignatures.length, numConstructSignatures)) { - return resolveUntypedCall(node); + } + return candidates; + } + function getContravariantInference(inference: InferenceInfo) { + return (inference.priority!) & InferencePriority.PriorityImpliesCombination ? getIntersectionType(inference.contraCandidates!) : getCommonSubtype(inference.contraCandidates!); + } + function getCovariantInference(inference: InferenceInfo, signature: ts.Signature) { + // Extract all object and array literal types and replace them with a single widened and normalized type. + const candidates = unionObjectAndArrayLiteralCandidates(inference.candidates!); + // We widen inferred literal types if + // all inferences were made to top-level occurrences of the type parameter, and + // the type parameter has no constraint or its constraint includes no primitive or literal types, and + // the type parameter was fixed during inference or does not occur at top-level in the return type. + const primitiveConstraint = hasPrimitiveConstraint(inference.typeParameter); + const widenLiteralTypes = !primitiveConstraint && inference.topLevel && + (inference.isFixed || !isTypeParameterAtTopLevel(getReturnTypeOfSignature(signature), inference.typeParameter)); + const baseCandidates = primitiveConstraint ? sameMap(candidates, getRegularTypeOfLiteralType) : + widenLiteralTypes ? sameMap(candidates, getWidenedLiteralType) : + candidates; + // If all inferences were made from a position that implies a combined result, infer a union type. + // Otherwise, infer a common supertype. + const unwidenedType = (inference.priority!) & InferencePriority.PriorityImpliesCombination ? + getUnionType(baseCandidates, UnionReduction.Subtype) : + getCommonSupertype(baseCandidates); + return getWidenedType(unwidenedType); + } + function getInferredType(context: InferenceContext, index: number): ts.Type { + const inference = context.inferences[index]; + if (!inference.inferredType) { + let inferredType: ts.Type | undefined; + const signature = context.signature; + if (signature) { + const inferredCovariantType = inference.candidates ? getCovariantInference(inference, signature) : undefined; + if (inference.contraCandidates) { + const inferredContravariantType = getContravariantInference(inference); + // If we have both co- and contra-variant inferences, we prefer the contra-variant inference + // unless the co-variant inference is a subtype and not 'never'. + inferredType = inferredCovariantType && !(inferredCovariantType.flags & TypeFlags.Never) && + isTypeSubtypeOf(inferredCovariantType, inferredContravariantType) ? + inferredCovariantType : inferredContravariantType; + } + else if (inferredCovariantType) { + inferredType = inferredCovariantType; + } + else if (context.flags & InferenceFlags.NoDefault) { + // We use silentNeverType as the wildcard that signals no inferences. + inferredType = silentNeverType; + } + else { + // Infer either the default or the empty object type when no inferences were + // made. It is important to remember that in this case, inference still + // succeeds, meaning there is no error for not having inference candidates. An + // inference error only occurs when there are *conflicting* candidates, i.e. + // candidates with no common supertype. + const defaultType = getDefaultFromTypeParameter(inference.typeParameter); + if (defaultType) { + // Instantiate the default type. Any forward reference to a type + // parameter should be instantiated to the empty object type. + inferredType = instantiateType(defaultType, combineTypeMappers(createBackreferenceMapper(context, index), context.nonFixingMapper)); + } + } } - - if (!callSignatures.length) { - invocationError(node.tag, apparentType, SignatureKind.Call); - return resolveErrorCall(node); + else { + inferredType = getTypeFromInference(inference); + } + inference.inferredType = inferredType || getDefaultTypeArgumentType(!!(context.flags & InferenceFlags.AnyDefault)); + const constraint = getConstraintOfTypeParameter(inference.typeParameter); + if (constraint) { + const instantiatedConstraint = instantiateType(constraint, context.nonFixingMapper); + if (!inferredType || !context.compareTypes(inferredType, getTypeWithThisArgument(instantiatedConstraint, inferredType))) { + inference.inferredType = inferredType = instantiatedConstraint; + } } - - return resolveCall(node, callSignatures, candidatesOutArray, checkMode, SignatureFlags.None); } - - /** - * Gets the localized diagnostic head message to use for errors when resolving a decorator as a call expression. - */ - function getDiagnosticHeadMessageForDecoratorResolution(node: Decorator) { - switch (node.parent.kind) { - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ClassExpression: - return Diagnostics.Unable_to_resolve_signature_of_class_decorator_when_called_as_an_expression; - - case SyntaxKind.Parameter: - return Diagnostics.Unable_to_resolve_signature_of_parameter_decorator_when_called_as_an_expression; - - case SyntaxKind.PropertyDeclaration: - return Diagnostics.Unable_to_resolve_signature_of_property_decorator_when_called_as_an_expression; - - case SyntaxKind.MethodDeclaration: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - return Diagnostics.Unable_to_resolve_signature_of_method_decorator_when_called_as_an_expression; - - default: - return Debug.fail(); + return inference.inferredType; + } + function getDefaultTypeArgumentType(isInJavaScriptFile: boolean): ts.Type { + return isInJavaScriptFile ? anyType : unknownType; + } + function getInferredTypes(context: InferenceContext): ts.Type[] { + const result: ts.Type[] = []; + for (let i = 0; i < context.inferences.length; i++) { + result.push(getInferredType(context, i)); + } + return result; + } + // EXPRESSION TYPE CHECKING + function getCannotFindNameDiagnosticForName(node: Identifier): DiagnosticMessage { + switch (node.escapedText) { + case "document": + case "console": + return Diagnostics.Cannot_find_name_0_Do_you_need_to_change_your_target_library_Try_changing_the_lib_compiler_option_to_include_dom; + case "$": + return compilerOptions.types + ? Diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_jQuery_Try_npm_i_types_Slashjquery_and_then_add_jquery_to_the_types_field_in_your_tsconfig + : Diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_jQuery_Try_npm_i_types_Slashjquery; + case "describe": + case "suite": + case "it": + case "test": + return compilerOptions.types + ? Diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_a_test_runner_Try_npm_i_types_Slashjest_or_npm_i_types_Slashmocha_and_then_add_jest_or_mocha_to_the_types_field_in_your_tsconfig + : Diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_a_test_runner_Try_npm_i_types_Slashjest_or_npm_i_types_Slashmocha; + case "process": + case "require": + case "Buffer": + case "module": + return compilerOptions.types + ? Diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_node_Try_npm_i_types_Slashnode_and_then_add_node_to_the_types_field_in_your_tsconfig + : Diagnostics.Cannot_find_name_0_Do_you_need_to_install_type_definitions_for_node_Try_npm_i_types_Slashnode; + case "Map": + case "Set": + case "Promise": + case "Symbol": + case "WeakMap": + case "WeakSet": + case "Iterator": + case "AsyncIterator": + return Diagnostics.Cannot_find_name_0_Do_you_need_to_change_your_target_library_Try_changing_the_lib_compiler_option_to_es2015_or_later; + default: + if (node.parent.kind === SyntaxKind.ShorthandPropertyAssignment) { + return Diagnostics.No_value_exists_in_scope_for_the_shorthand_property_0_Either_declare_one_or_provide_an_initializer; + } + else { + return Diagnostics.Cannot_find_name_0; + } + } + } + function getResolvedSymbol(node: Identifier): ts.Symbol { + const links = getNodeLinks(node); + if (!links.resolvedSymbol) { + links.resolvedSymbol = !nodeIsMissing(node) && + resolveName(node, node.escapedText, SymbolFlags.Value | SymbolFlags.ExportValue, getCannotFindNameDiagnosticForName(node), node, !isWriteOnlyAccess(node), + /*excludeGlobals*/ false, Diagnostics.Cannot_find_name_0_Did_you_mean_1) || unknownSymbol; + } + return links.resolvedSymbol; + } + function isInTypeQuery(node: Node): boolean { + // TypeScript 1.0 spec (April 2014): 3.6.3 + // A type query consists of the keyword typeof followed by an expression. + // The expression is restricted to a single identifier or a sequence of identifiers separated by periods + return !!findAncestor(node, n => n.kind === SyntaxKind.TypeQuery ? true : n.kind === SyntaxKind.Identifier || n.kind === SyntaxKind.QualifiedName ? false : "quit"); + } + // Return the flow cache key for a "dotted name" (i.e. a sequence of identifiers + // separated by dots). The key consists of the id of the symbol referenced by the + // leftmost identifier followed by zero or more property names separated by dots. + // The result is undefined if the reference isn't a dotted name. We prefix nodes + // occurring in an apparent type position with '@' because the control flow type + // of such nodes may be based on the apparent type instead of the declared type. + function getFlowCacheKey(node: Node, declaredType: ts.Type, initialType: ts.Type, flowContainer: Node | undefined): string | undefined { + switch (node.kind) { + case SyntaxKind.Identifier: + const symbol = getResolvedSymbol((node)); + return symbol !== unknownSymbol ? `${flowContainer ? getNodeId(flowContainer) : "-1"}|${getTypeId(declaredType)}|${getTypeId(initialType)}|${isConstraintPosition(node) ? "@" : ""}${getSymbolId(symbol)}` : undefined; + case SyntaxKind.ThisKeyword: + return "0"; + case SyntaxKind.NonNullExpression: + case SyntaxKind.ParenthesizedExpression: + return getFlowCacheKey((node).expression, declaredType, initialType, flowContainer); + case SyntaxKind.PropertyAccessExpression: + case SyntaxKind.ElementAccessExpression: + const propName = getAccessedPropertyName((node)); + if (propName !== undefined) { + const key = getFlowCacheKey((node).expression, declaredType, initialType, flowContainer); + return key && key + "." + propName; + } + } + return undefined; + } + function isMatchingReference(source: Node, target: Node): boolean { + switch (target.kind) { + case SyntaxKind.ParenthesizedExpression: + case SyntaxKind.NonNullExpression: + return isMatchingReference(source, (target as NonNullExpression | ParenthesizedExpression).expression); + } + switch (source.kind) { + case SyntaxKind.Identifier: + return target.kind === SyntaxKind.Identifier && getResolvedSymbol((source)) === getResolvedSymbol((target)) || + (target.kind === SyntaxKind.VariableDeclaration || target.kind === SyntaxKind.BindingElement) && + getExportSymbolOfValueSymbolIfExported(getResolvedSymbol((source))) === getSymbolOfNode(target); + case SyntaxKind.ThisKeyword: + return target.kind === SyntaxKind.ThisKeyword; + case SyntaxKind.SuperKeyword: + return target.kind === SyntaxKind.SuperKeyword; + case SyntaxKind.NonNullExpression: + case SyntaxKind.ParenthesizedExpression: + return isMatchingReference((source as NonNullExpression | ParenthesizedExpression).expression, target); + case SyntaxKind.PropertyAccessExpression: + case SyntaxKind.ElementAccessExpression: + return isAccessExpression(target) && + getAccessedPropertyName((source)) === getAccessedPropertyName(target) && + isMatchingReference((source).expression, target.expression); + } + return false; + } + function getAccessedPropertyName(access: AccessExpression): __String | undefined { + return access.kind === SyntaxKind.PropertyAccessExpression ? access.name.escapedText : + isStringOrNumericLiteralLike(access.argumentExpression) ? escapeLeadingUnderscores(access.argumentExpression.text) : + undefined; + } + function containsMatchingReference(source: Node, target: Node) { + while (isAccessExpression(source)) { + source = source.expression; + if (isMatchingReference(source, target)) { + return true; } } - - /** - * Resolves a decorator as if it were a call expression. - */ - function resolveDecorator(node: Decorator, candidatesOutArray: Signature[] | undefined, checkMode: CheckMode): Signature { - const funcType = checkExpression(node.expression); - const apparentType = getApparentType(funcType); - if (apparentType === errorType) { - return resolveErrorCall(node); + return false; + } + function optionalChainContainsReference(source: Node, target: Node) { + while (isOptionalChain(source)) { + source = source.expression; + if (isMatchingReference(source, target)) { + return true; } - - const callSignatures = getSignaturesOfType(apparentType, SignatureKind.Call); - const numConstructSignatures = getSignaturesOfType(apparentType, SignatureKind.Construct).length; - if (isUntypedFunctionCall(funcType, apparentType, callSignatures.length, numConstructSignatures)) { - return resolveUntypedCall(node); + } + return false; + } + function isDiscriminantProperty(type: ts.Type | undefined, name: __String) { + if (type && type.flags & TypeFlags.Union) { + const prop = getUnionOrIntersectionProperty((type), name); + if (prop && getCheckFlags(prop) & CheckFlags.SyntheticProperty) { + if ((prop).isDiscriminantProperty === undefined) { + (prop).isDiscriminantProperty = + ((prop).checkFlags & CheckFlags.Discriminant) === CheckFlags.Discriminant && + !maybeTypeOfKind(getTypeOfSymbol(prop), TypeFlags.Instantiable); + } + return !!(prop).isDiscriminantProperty; } - - if (isPotentiallyUncalledDecorator(node, callSignatures)) { - const nodeStr = getTextOfNode(node.expression, /*includeTrivia*/ false); - error(node, Diagnostics._0_accepts_too_few_arguments_to_be_used_as_a_decorator_here_Did_you_mean_to_call_it_first_and_write_0, nodeStr); - return resolveErrorCall(node); + } + return false; + } + function isSyntheticThisPropertyAccess(expr: Node) { + return isAccessExpression(expr) && expr.expression.kind === SyntaxKind.ThisKeyword && !!(expr.expression.flags & NodeFlags.Synthesized); + } + function findDiscriminantProperties(sourceProperties: ts.Symbol[], target: ts.Type): ts.Symbol[] | undefined { + let result: ts.Symbol[] | undefined; + for (const sourceProperty of sourceProperties) { + if (isDiscriminantProperty(target, sourceProperty.escapedName)) { + if (result) { + result.push(sourceProperty); + continue; + } + result = [sourceProperty]; } - - const headMessage = getDiagnosticHeadMessageForDecoratorResolution(node); - if (!callSignatures.length) { - const errorDetails = invocationErrorDetails(apparentType, SignatureKind.Call); - const messageChain = chainDiagnosticMessages(errorDetails.messageChain, headMessage); - const diag = createDiagnosticForNodeFromMessageChain(node.expression, messageChain); - if (errorDetails.relatedMessage) { - addRelatedInfo(diag, createDiagnosticForNode(node.expression, errorDetails.relatedMessage)); + } + return result; + } + function isOrContainsMatchingReference(source: Node, target: Node) { + return isMatchingReference(source, target) || containsMatchingReference(source, target); + } + function hasMatchingArgument(callExpression: CallExpression, reference: Node) { + if (callExpression.arguments) { + for (const argument of callExpression.arguments) { + if (isOrContainsMatchingReference(reference, argument)) { + return true; } - diagnostics.add(diag); - invocationErrorRecovery(apparentType, SignatureKind.Call, diag); - return resolveErrorCall(node); } - - return resolveCall(node, callSignatures, candidatesOutArray, checkMode, SignatureFlags.None, headMessage); } - - function createSignatureForJSXIntrinsic(node: JsxOpeningLikeElement, result: Type): Signature { - const namespace = getJsxNamespaceAt(node); - const exports = namespace && getExportsOfSymbol(namespace); - // We fake up a SFC signature for each intrinsic, however a more specific per-element signature drawn from the JSX declaration - // file would probably be preferable. - const typeSymbol = exports && getSymbol(exports, JsxNames.Element, SymbolFlags.Type); - const returnNode = typeSymbol && nodeBuilder.symbolToEntityName(typeSymbol, SymbolFlags.Type, node); - const declaration = createFunctionTypeNode(/*typeParameters*/ undefined, - [createParameter(/*decorators*/ undefined, /*modifiers*/ undefined, /*dotdotdot*/ undefined, "props", /*questionMark*/ undefined, nodeBuilder.typeToTypeNode(result, node))], - returnNode ? createTypeReferenceNode(returnNode, /*typeArguments*/ undefined) : createKeywordTypeNode(SyntaxKind.AnyKeyword) - ); - const parameterSymbol = createSymbol(SymbolFlags.FunctionScopedVariable, "props" as __String); - parameterSymbol.type = result; - return createSignature( - declaration, - /*typeParameters*/ undefined, - /*thisParameter*/ undefined, - [parameterSymbol], - typeSymbol ? getDeclaredTypeOfSymbol(typeSymbol) : errorType, - /*returnTypePredicate*/ undefined, - 1, - SignatureFlags.None - ); + if (callExpression.expression.kind === SyntaxKind.PropertyAccessExpression && + isOrContainsMatchingReference(reference, (callExpression.expression).expression)) { + return true; } - - function resolveJsxOpeningLikeElement(node: JsxOpeningLikeElement, candidatesOutArray: Signature[] | undefined, checkMode: CheckMode): Signature { - if (isJsxIntrinsicIdentifier(node.tagName)) { - const result = getIntrinsicAttributesTypeFromJsxOpeningLikeElement(node); - const fakeSignature = createSignatureForJSXIntrinsic(node, result); - checkTypeAssignableToAndOptionallyElaborate(checkExpressionWithContextualType(node.attributes, getEffectiveFirstArgumentForJsxSignature(fakeSignature, node), /*mapper*/ undefined, CheckMode.Normal), result, node.tagName, node.attributes); - return fakeSignature; - } - const exprTypes = checkExpression(node.tagName); - const apparentType = getApparentType(exprTypes); - if (apparentType === errorType) { - return resolveErrorCall(node); + return false; + } + function getFlowNodeId(flow: FlowNode): number { + if (!flow.id || flow.id < 0) { + flow.id = nextFlowId; + nextFlowId++; + } + return flow.id; + } + function typeMaybeAssignableTo(source: ts.Type, target: ts.Type) { + if (!(source.flags & TypeFlags.Union)) { + return isTypeAssignableTo(source, target); + } + for (const t of (source).types) { + if (isTypeAssignableTo(t, target)) { + return true; } - - const signatures = getUninstantiatedJsxSignaturesOfType(exprTypes, node); - if (isUntypedFunctionCall(exprTypes, apparentType, signatures.length, /*constructSignatures*/ 0)) { - return resolveUntypedCall(node); + } + return false; + } + // Remove those constituent types of declaredType to which no constituent type of assignedType is assignable. + // For example, when a variable of type number | string | boolean is assigned a value of type number | boolean, + // we remove type string. + function getAssignmentReducedType(declaredType: UnionType, assignedType: ts.Type) { + if (declaredType !== assignedType) { + if (assignedType.flags & TypeFlags.Never) { + return assignedType; + } + let reducedType = filterType(declaredType, t => typeMaybeAssignableTo(assignedType, t)); + if (assignedType.flags & TypeFlags.BooleanLiteral && isFreshLiteralType(assignedType)) { + reducedType = mapType(reducedType, getFreshTypeOfLiteralType); // Ensure that if the assignment is a fresh type, that we narrow to fresh types + } + // Our crude heuristic produces an invalid result in some cases: see GH#26130. + // For now, when that happens, we give up and don't narrow at all. (This also + // means we'll never narrow for erroneous assignments where the assigned type + // is not assignable to the declared type.) + if (isTypeAssignableTo(assignedType, reducedType)) { + return reducedType; + } + } + return declaredType; + } + function getTypeFactsOfTypes(types: ts.Type[]): TypeFacts { + let result: TypeFacts = TypeFacts.None; + for (const t of types) { + result |= getTypeFacts(t); + } + return result; + } + function isFunctionObjectType(type: ObjectType): boolean { + // We do a quick check for a "bind" property before performing the more expensive subtype + // check. This gives us a quicker out in the common case where an object type is not a function. + const resolved = resolveStructuredTypeMembers(type); + return !!(resolved.callSignatures.length || resolved.constructSignatures.length || + resolved.members.get(("bind" as __String)) && isTypeSubtypeOf(type, globalFunctionType)); + } + function getTypeFacts(type: ts.Type): TypeFacts { + const flags = type.flags; + if (flags & TypeFlags.String) { + return strictNullChecks ? TypeFacts.StringStrictFacts : TypeFacts.StringFacts; + } + if (flags & TypeFlags.StringLiteral) { + const isEmpty = (type).value === ""; + return strictNullChecks ? + isEmpty ? TypeFacts.EmptyStringStrictFacts : TypeFacts.NonEmptyStringStrictFacts : + isEmpty ? TypeFacts.EmptyStringFacts : TypeFacts.NonEmptyStringFacts; + } + if (flags & (TypeFlags.Number | TypeFlags.Enum)) { + return strictNullChecks ? TypeFacts.NumberStrictFacts : TypeFacts.NumberFacts; + } + if (flags & TypeFlags.NumberLiteral) { + const isZero = (type).value === 0; + return strictNullChecks ? + isZero ? TypeFacts.ZeroNumberStrictFacts : TypeFacts.NonZeroNumberStrictFacts : + isZero ? TypeFacts.ZeroNumberFacts : TypeFacts.NonZeroNumberFacts; + } + if (flags & TypeFlags.BigInt) { + return strictNullChecks ? TypeFacts.BigIntStrictFacts : TypeFacts.BigIntFacts; + } + if (flags & TypeFlags.BigIntLiteral) { + const isZero = isZeroBigInt((type)); + return strictNullChecks ? + isZero ? TypeFacts.ZeroBigIntStrictFacts : TypeFacts.NonZeroBigIntStrictFacts : + isZero ? TypeFacts.ZeroBigIntFacts : TypeFacts.NonZeroBigIntFacts; + } + if (flags & TypeFlags.Boolean) { + return strictNullChecks ? TypeFacts.BooleanStrictFacts : TypeFacts.BooleanFacts; + } + if (flags & TypeFlags.BooleanLike) { + return strictNullChecks ? + (type === falseType || type === regularFalseType) ? TypeFacts.FalseStrictFacts : TypeFacts.TrueStrictFacts : + (type === falseType || type === regularFalseType) ? TypeFacts.FalseFacts : TypeFacts.TrueFacts; + } + if (flags & TypeFlags.Object) { + return getObjectFlags(type) & ObjectFlags.Anonymous && isEmptyObjectType((type)) ? + strictNullChecks ? TypeFacts.EmptyObjectStrictFacts : TypeFacts.EmptyObjectFacts : + isFunctionObjectType((type)) ? + strictNullChecks ? TypeFacts.FunctionStrictFacts : TypeFacts.FunctionFacts : + strictNullChecks ? TypeFacts.ObjectStrictFacts : TypeFacts.ObjectFacts; + } + if (flags & (TypeFlags.Void | TypeFlags.Undefined)) { + return TypeFacts.UndefinedFacts; + } + if (flags & TypeFlags.Null) { + return TypeFacts.NullFacts; + } + if (flags & TypeFlags.ESSymbolLike) { + return strictNullChecks ? TypeFacts.SymbolStrictFacts : TypeFacts.SymbolFacts; + } + if (flags & TypeFlags.NonPrimitive) { + return strictNullChecks ? TypeFacts.ObjectStrictFacts : TypeFacts.ObjectFacts; + } + if (flags & TypeFlags.Instantiable) { + return getTypeFacts(getBaseConstraintOfType(type) || unknownType); + } + if (flags & TypeFlags.UnionOrIntersection) { + return getTypeFactsOfTypes((type).types); + } + return TypeFacts.All; + } + function getTypeWithFacts(type: ts.Type, include: TypeFacts) { + return filterType(type, t => (getTypeFacts(t) & include) !== 0); + } + function getTypeWithDefault(type: ts.Type, defaultExpression: Expression) { + if (defaultExpression) { + const defaultType = getTypeOfExpression(defaultExpression); + return getUnionType([getTypeWithFacts(type, TypeFacts.NEUndefined), defaultType]); + } + return type; + } + function getTypeOfDestructuredProperty(type: ts.Type, name: PropertyName) { + const nameType = getLiteralTypeFromPropertyName(name); + if (!isTypeUsableAsPropertyName(nameType)) + return errorType; + const text = getPropertyNameFromType(nameType); + return getConstraintForLocation(getTypeOfPropertyOfType(type, text), name) || + isNumericLiteralName(text) && getIndexTypeOfType(type, IndexKind.Number) || + getIndexTypeOfType(type, IndexKind.String) || + errorType; + } + function getTypeOfDestructuredArrayElement(type: ts.Type, index: number) { + return everyType(type, isTupleLikeType) && getTupleElementType(type, index) || + checkIteratedTypeOrElementType(IterationUse.Destructuring, type, undefinedType, /*errorNode*/ undefined) || + errorType; + } + function getTypeOfDestructuredSpreadExpression(type: ts.Type) { + return createArrayType(checkIteratedTypeOrElementType(IterationUse.Destructuring, type, undefinedType, /*errorNode*/ undefined) || errorType); + } + function getAssignedTypeOfBinaryExpression(node: BinaryExpression): ts.Type { + const isDestructuringDefaultAssignment = node.parent.kind === SyntaxKind.ArrayLiteralExpression && isDestructuringAssignmentTarget(node.parent) || + node.parent.kind === SyntaxKind.PropertyAssignment && isDestructuringAssignmentTarget(node.parent.parent); + return isDestructuringDefaultAssignment ? + getTypeWithDefault(getAssignedType(node), node.right) : + getTypeOfExpression(node.right); + } + function isDestructuringAssignmentTarget(parent: Node) { + return parent.parent.kind === SyntaxKind.BinaryExpression && (parent.parent as BinaryExpression).left === parent || + parent.parent.kind === SyntaxKind.ForOfStatement && (parent.parent as ForOfStatement).initializer === parent; + } + function getAssignedTypeOfArrayLiteralElement(node: ArrayLiteralExpression, element: Expression): ts.Type { + return getTypeOfDestructuredArrayElement(getAssignedType(node), node.elements.indexOf(element)); + } + function getAssignedTypeOfSpreadExpression(node: SpreadElement): ts.Type { + return getTypeOfDestructuredSpreadExpression(getAssignedType((node.parent))); + } + function getAssignedTypeOfPropertyAssignment(node: PropertyAssignment | ShorthandPropertyAssignment): ts.Type { + return getTypeOfDestructuredProperty(getAssignedType(node.parent), node.name); + } + function getAssignedTypeOfShorthandPropertyAssignment(node: ShorthandPropertyAssignment): ts.Type { + return getTypeWithDefault(getAssignedTypeOfPropertyAssignment(node), node.objectAssignmentInitializer!); + } + function getAssignedType(node: Expression): ts.Type { + const { parent } = node; + switch (parent.kind) { + case SyntaxKind.ForInStatement: + return stringType; + case SyntaxKind.ForOfStatement: + return checkRightHandSideOfForOf((parent).expression, (parent).awaitModifier) || errorType; + case SyntaxKind.BinaryExpression: + return getAssignedTypeOfBinaryExpression((parent)); + case SyntaxKind.DeleteExpression: + return undefinedType; + case SyntaxKind.ArrayLiteralExpression: + return getAssignedTypeOfArrayLiteralElement((parent), node); + case SyntaxKind.SpreadElement: + return getAssignedTypeOfSpreadExpression((parent)); + case SyntaxKind.PropertyAssignment: + return getAssignedTypeOfPropertyAssignment((parent)); + case SyntaxKind.ShorthandPropertyAssignment: + return getAssignedTypeOfShorthandPropertyAssignment((parent)); + } + return errorType; + } + function getInitialTypeOfBindingElement(node: BindingElement): ts.Type { + const pattern = node.parent; + const parentType = getInitialType((pattern.parent)); + const type = pattern.kind === SyntaxKind.ObjectBindingPattern ? + getTypeOfDestructuredProperty(parentType, node.propertyName || (node.name)) : + !node.dotDotDotToken ? + getTypeOfDestructuredArrayElement(parentType, pattern.elements.indexOf(node)) : + getTypeOfDestructuredSpreadExpression(parentType); + return getTypeWithDefault(type, node.initializer!); + } + function getTypeOfInitializer(node: Expression) { + // Return the cached type if one is available. If the type of the variable was inferred + // from its initializer, we'll already have cached the type. Otherwise we compute it now + // without caching such that transient types are reflected. + const links = getNodeLinks(node); + return links.resolvedType || getTypeOfExpression(node); + } + function getInitialTypeOfVariableDeclaration(node: VariableDeclaration) { + if (node.initializer) { + return getTypeOfInitializer(node.initializer); + } + if (node.parent.parent.kind === SyntaxKind.ForInStatement) { + return stringType; + } + if (node.parent.parent.kind === SyntaxKind.ForOfStatement) { + return checkRightHandSideOfForOf(node.parent.parent.expression, node.parent.parent.awaitModifier) || errorType; + } + return errorType; + } + function getInitialType(node: VariableDeclaration | BindingElement) { + return node.kind === SyntaxKind.VariableDeclaration ? + getInitialTypeOfVariableDeclaration(node) : + getInitialTypeOfBindingElement(node); + } + function isEmptyArrayAssignment(node: VariableDeclaration | BindingElement | Expression) { + return node.kind === SyntaxKind.VariableDeclaration && (node).initializer && + isEmptyArrayLiteral(((node).initializer!)) || + node.kind !== SyntaxKind.BindingElement && node.parent.kind === SyntaxKind.BinaryExpression && + isEmptyArrayLiteral((node.parent).right); + } + function getReferenceCandidate(node: Expression): Expression { + switch (node.kind) { + case SyntaxKind.ParenthesizedExpression: + return getReferenceCandidate((node).expression); + case SyntaxKind.BinaryExpression: + switch ((node).operatorToken.kind) { + case SyntaxKind.EqualsToken: + return getReferenceCandidate((node).left); + case SyntaxKind.CommaToken: + return getReferenceCandidate((node).right); + } + } + return node; + } + function getReferenceRoot(node: Node): Node { + const { parent } = node; + return parent.kind === SyntaxKind.ParenthesizedExpression || + parent.kind === SyntaxKind.BinaryExpression && (parent).operatorToken.kind === SyntaxKind.EqualsToken && (parent).left === node || + parent.kind === SyntaxKind.BinaryExpression && (parent).operatorToken.kind === SyntaxKind.CommaToken && (parent).right === node ? + getReferenceRoot(parent) : node; + } + function getTypeOfSwitchClause(clause: CaseClause | DefaultClause) { + if (clause.kind === SyntaxKind.CaseClause) { + return getRegularTypeOfLiteralType(getTypeOfExpression(clause.expression)); + } + return neverType; + } + function getSwitchClauseTypes(switchStatement: SwitchStatement): ts.Type[] { + const links = getNodeLinks(switchStatement); + if (!links.switchTypes) { + links.switchTypes = []; + for (const clause of switchStatement.caseBlock.clauses) { + links.switchTypes.push(getTypeOfSwitchClause(clause)); } - - if (signatures.length === 0) { - // We found no signatures at all, which is an error - error(node.tagName, Diagnostics.JSX_element_type_0_does_not_have_any_construct_or_call_signatures, getTextOfNode(node.tagName)); - return resolveErrorCall(node); + } + return links.switchTypes; + } + // Get the types from all cases in a switch on `typeof`. An + // `undefined` element denotes an explicit `default` clause. + function getSwitchClauseTypeOfWitnesses(switchStatement: SwitchStatement): (string | undefined)[] { + const witnesses: (string | undefined)[] = []; + for (const clause of switchStatement.caseBlock.clauses) { + if (clause.kind === SyntaxKind.CaseClause) { + if (isStringLiteralLike(clause.expression)) { + witnesses.push(clause.expression.text); + continue; + } + return emptyArray; } - - return resolveCall(node, signatures, candidatesOutArray, checkMode, SignatureFlags.None); + witnesses.push(/*explicitDefaultStatement*/ undefined); } - - /** - * Sometimes, we have a decorator that could accept zero arguments, - * but is receiving too many arguments as part of the decorator invocation. - * In those cases, a user may have meant to *call* the expression before using it as a decorator. - */ - function isPotentiallyUncalledDecorator(decorator: Decorator, signatures: readonly Signature[]) { - return signatures.length && every(signatures, signature => - signature.minArgumentCount === 0 && - !signatureHasRestParameter(signature) && - signature.parameters.length < getDecoratorArgumentCount(decorator, signature)); + return witnesses; + } + function eachTypeContainedIn(source: ts.Type, types: ts.Type[]) { + return source.flags & TypeFlags.Union ? !forEach((source).types, t => !contains(types, t)) : contains(types, source); + } + function isTypeSubsetOf(source: ts.Type, target: ts.Type) { + return source === target || target.flags & TypeFlags.Union && isTypeSubsetOfUnion(source, (target)); + } + function isTypeSubsetOfUnion(source: ts.Type, target: UnionType) { + if (source.flags & TypeFlags.Union) { + for (const t of (source).types) { + if (!containsType(target.types, t)) { + return false; + } + } + return true; } - - function resolveSignature(node: CallLikeExpression, candidatesOutArray: Signature[] | undefined, checkMode: CheckMode): Signature { - switch (node.kind) { - case SyntaxKind.CallExpression: - return resolveCallExpression(node, candidatesOutArray, checkMode); - case SyntaxKind.NewExpression: - return resolveNewExpression(node, candidatesOutArray, checkMode); - case SyntaxKind.TaggedTemplateExpression: - return resolveTaggedTemplateExpression(node, candidatesOutArray, checkMode); - case SyntaxKind.Decorator: - return resolveDecorator(node, candidatesOutArray, checkMode); - case SyntaxKind.JsxOpeningElement: - case SyntaxKind.JsxSelfClosingElement: - return resolveJsxOpeningLikeElement(node, candidatesOutArray, checkMode); + if (source.flags & TypeFlags.EnumLiteral && getBaseTypeOfEnumLiteralType((source)) === target) { + return true; + } + return containsType(target.types, source); + } + function forEachType(type: ts.Type, f: (t: ts.Type) => T | undefined): T | undefined { + return type.flags & TypeFlags.Union ? forEach((type).types, f) : f(type); + } + function everyType(type: ts.Type, f: (t: ts.Type) => boolean): boolean { + return type.flags & TypeFlags.Union ? every((type).types, f) : f(type); + } + function filterType(type: ts.Type, f: (t: ts.Type) => boolean): ts.Type { + if (type.flags & TypeFlags.Union) { + const types = (type).types; + const filtered = filter(types, f); + return filtered === types ? type : getUnionTypeFromSortedList(filtered, (type).objectFlags); + } + return f(type) ? type : neverType; + } + function countTypes(type: ts.Type) { + return type.flags & TypeFlags.Union ? (type as UnionType).types.length : 1; + } + // Apply a mapping function to a type and return the resulting type. If the source type + // is a union type, the mapping function is applied to each constituent type and a union + // of the resulting types is returned. + function mapType(type: ts.Type, mapper: (t: ts.Type) => ts.Type, noReductions?: boolean): ts.Type; + function mapType(type: ts.Type, mapper: (t: ts.Type) => ts.Type | undefined, noReductions?: boolean): ts.Type | undefined; + function mapType(type: ts.Type, mapper: (t: ts.Type) => ts.Type | undefined, noReductions?: boolean): ts.Type | undefined { + if (type.flags & TypeFlags.Never) { + return type; + } + if (!(type.flags & TypeFlags.Union)) { + return mapper(type); + } + let mappedTypes: ts.Type[] | undefined; + for (const t of (type).types) { + const mapped = mapper(t); + if (mapped) { + if (!mappedTypes) { + mappedTypes = [mapped]; + } + else { + mappedTypes.push(mapped); + } + } + } + return mappedTypes && getUnionType(mappedTypes, noReductions ? UnionReduction.None : UnionReduction.Literal); + } + function extractTypesOfKind(type: ts.Type, kind: TypeFlags) { + return filterType(type, t => (t.flags & kind) !== 0); + } + // Return a new type in which occurrences of the string and number primitive types in + // typeWithPrimitives have been replaced with occurrences of string literals and numeric + // literals in typeWithLiterals, respectively. + function replacePrimitivesWithLiterals(typeWithPrimitives: ts.Type, typeWithLiterals: ts.Type) { + if (isTypeSubsetOf(stringType, typeWithPrimitives) && maybeTypeOfKind(typeWithLiterals, TypeFlags.StringLiteral) || + isTypeSubsetOf(numberType, typeWithPrimitives) && maybeTypeOfKind(typeWithLiterals, TypeFlags.NumberLiteral) || + isTypeSubsetOf(bigintType, typeWithPrimitives) && maybeTypeOfKind(typeWithLiterals, TypeFlags.BigIntLiteral)) { + return mapType(typeWithPrimitives, t => t.flags & TypeFlags.String ? extractTypesOfKind(typeWithLiterals, TypeFlags.String | TypeFlags.StringLiteral) : + t.flags & TypeFlags.Number ? extractTypesOfKind(typeWithLiterals, TypeFlags.Number | TypeFlags.NumberLiteral) : + t.flags & TypeFlags.BigInt ? extractTypesOfKind(typeWithLiterals, TypeFlags.BigInt | TypeFlags.BigIntLiteral) : t); + } + return typeWithPrimitives; + } + function isIncomplete(flowType: FlowType) { + return flowType.flags === 0; + } + function getTypeFromFlowType(flowType: FlowType) { + return flowType.flags === 0 ? (flowType).type : flowType; + } + function createFlowType(type: ts.Type, incomplete: boolean): FlowType { + return incomplete ? { flags: 0, type } : type; + } + // An evolving array type tracks the element types that have so far been seen in an + // 'x.push(value)' or 'x[n] = value' operation along the control flow graph. Evolving + // array types are ultimately converted into manifest array types (using getFinalArrayType) + // and never escape the getFlowTypeOfReference function. + function createEvolvingArrayType(elementType: ts.Type): EvolvingArrayType { + const result = (createObjectType(ObjectFlags.EvolvingArray)); + result.elementType = elementType; + return result; + } + function getEvolvingArrayType(elementType: ts.Type): EvolvingArrayType { + return evolvingArrayTypes[elementType.id] || (evolvingArrayTypes[elementType.id] = createEvolvingArrayType(elementType)); + } + // When adding evolving array element types we do not perform subtype reduction. Instead, + // we defer subtype reduction until the evolving array type is finalized into a manifest + // array type. + function addEvolvingArrayElementType(evolvingArrayType: EvolvingArrayType, node: Expression): EvolvingArrayType { + const elementType = getBaseTypeOfLiteralType(getContextFreeTypeOfExpression(node)); + return isTypeSubsetOf(elementType, evolvingArrayType.elementType) ? evolvingArrayType : getEvolvingArrayType(getUnionType([evolvingArrayType.elementType, elementType])); + } + function createFinalArrayType(elementType: ts.Type) { + return elementType.flags & TypeFlags.Never ? + autoArrayType : + createArrayType(elementType.flags & TypeFlags.Union ? + getUnionType((elementType).types, UnionReduction.Subtype) : + elementType); + } + // We perform subtype reduction upon obtaining the final array type from an evolving array type. + function getFinalArrayType(evolvingArrayType: EvolvingArrayType): ts.Type { + return evolvingArrayType.finalArrayType || (evolvingArrayType.finalArrayType = createFinalArrayType(evolvingArrayType.elementType)); + } + function finalizeEvolvingArrayType(type: ts.Type): ts.Type { + return getObjectFlags(type) & ObjectFlags.EvolvingArray ? getFinalArrayType((type)) : type; + } + function getElementTypeOfEvolvingArrayType(type: ts.Type) { + return getObjectFlags(type) & ObjectFlags.EvolvingArray ? (type).elementType : neverType; + } + function isEvolvingArrayTypeList(types: ts.Type[]) { + let hasEvolvingArrayType = false; + for (const t of types) { + if (!(t.flags & TypeFlags.Never)) { + if (!(getObjectFlags(t) & ObjectFlags.EvolvingArray)) { + return false; + } + hasEvolvingArrayType = true; } - throw Debug.assertNever(node, "Branch in 'resolveSignature' should be unreachable."); } - - /** - * Resolve a signature of a given call-like expression. - * @param node a call-like expression to try resolve a signature for - * @param candidatesOutArray an array of signature to be filled in by the function. It is passed by signature help in the language service; - * the function will fill it up with appropriate candidate signatures - * @return a signature of the call-like expression or undefined if one can't be found - */ - function getResolvedSignature(node: CallLikeExpression, candidatesOutArray?: Signature[] | undefined, checkMode?: CheckMode): Signature { - const links = getNodeLinks(node); - // If getResolvedSignature has already been called, we will have cached the resolvedSignature. - // However, it is possible that either candidatesOutArray was not passed in the first time, - // or that a different candidatesOutArray was passed in. Therefore, we need to redo the work - // to correctly fill the candidatesOutArray. - const cached = links.resolvedSignature; - if (cached && cached !== resolvingSignature && !candidatesOutArray) { - return cached; + return hasEvolvingArrayType; + } + // At flow control branch or loop junctions, if the type along every antecedent code path + // is an evolving array type, we construct a combined evolving array type. Otherwise we + // finalize all evolving array types. + function getUnionOrEvolvingArrayType(types: ts.Type[], subtypeReduction: UnionReduction) { + return isEvolvingArrayTypeList(types) ? + getEvolvingArrayType(getUnionType(map(types, getElementTypeOfEvolvingArrayType))) : + getUnionType(sameMap(types, finalizeEvolvingArrayType), subtypeReduction); + } + // Return true if the given node is 'x' in an 'x.length', x.push(value)', 'x.unshift(value)' or + // 'x[n] = value' operation, where 'n' is an expression of type any, undefined, or a number-like type. + function isEvolvingArrayOperationTarget(node: Node) { + const root = getReferenceRoot(node); + const parent = root.parent; + const isLengthPushOrUnshift = isPropertyAccessExpression(parent) && (parent.name.escapedText === "length" || + parent.parent.kind === SyntaxKind.CallExpression + && isIdentifier(parent.name) + && isPushOrUnshiftIdentifier(parent.name)); + const isElementAssignment = parent.kind === SyntaxKind.ElementAccessExpression && + (parent).expression === root && + parent.parent.kind === SyntaxKind.BinaryExpression && + (parent.parent).operatorToken.kind === SyntaxKind.EqualsToken && + (parent.parent).left === parent && + !isAssignmentTarget(parent.parent) && + isTypeAssignableToKind(getTypeOfExpression((parent).argumentExpression), TypeFlags.NumberLike); + return isLengthPushOrUnshift || isElementAssignment; + } + function isDeclarationWithExplicitTypeAnnotation(declaration: Declaration | undefined) { + return !!(declaration && (declaration.kind === SyntaxKind.VariableDeclaration || declaration.kind === SyntaxKind.Parameter || + declaration.kind === SyntaxKind.PropertyDeclaration || declaration.kind === SyntaxKind.PropertySignature) && + getEffectiveTypeAnnotationNode((declaration as VariableDeclaration | ParameterDeclaration | PropertyDeclaration | PropertySignature))); + } + function getExplicitTypeOfSymbol(symbol: ts.Symbol, diagnostic?: Diagnostic) { + if (symbol.flags & (SymbolFlags.Function | SymbolFlags.Method | SymbolFlags.Class | SymbolFlags.ValueModule)) { + return getTypeOfSymbol(symbol); + } + if (symbol.flags & (SymbolFlags.Variable | SymbolFlags.Property)) { + if (isDeclarationWithExplicitTypeAnnotation(symbol.valueDeclaration)) { + return getTypeOfSymbol(symbol); } - links.resolvedSignature = resolvingSignature; - const result = resolveSignature(node, candidatesOutArray, checkMode || CheckMode.Normal); - // When CheckMode.SkipGenericFunctions is set we use resolvingSignature to indicate that call - // resolution should be deferred. - if (result !== resolvingSignature) { - // If signature resolution originated in control flow type analysis (for example to compute the - // assigned type in a flow assignment) we don't cache the result as it may be based on temporary - // types from the control flow analysis. - links.resolvedSignature = flowLoopStart === flowLoopCount ? result : cached; + if (diagnostic && symbol.valueDeclaration) { + addRelatedInfo(diagnostic, createDiagnosticForNode(symbol.valueDeclaration, Diagnostics._0_needs_an_explicit_type_annotation, symbolToString(symbol))); } - return result; } - - /** - * Indicates whether a declaration can be treated as a constructor in a JavaScript - * file. - */ - function isJSConstructor(node: Node | undefined): node is FunctionDeclaration | FunctionExpression { - if (!node || !isInJSFile(node)) { - return false; - } - const func = isFunctionDeclaration(node) || isFunctionExpression(node) ? node : - isVariableDeclaration(node) && node.initializer && isFunctionExpression(node.initializer) ? node.initializer : - undefined; - if (func) { - // If the node has a @class tag, treat it like a constructor. - if (getJSDocClassTag(node)) return true; - - // If the symbol of the node has members, treat it like a constructor. - const symbol = getSymbolOfNode(func); - return !!symbol && hasEntries(symbol.members); + } + // We require the dotted function name in an assertion expression to be comprised of identifiers + // that reference function, method, class or value module symbols; or variable, property or + // parameter symbols with declarations that have explicit type annotations. Such references are + // resolvable with no possibility of triggering circularities in control flow analysis. + function getTypeOfDottedName(node: Expression, diagnostic: Diagnostic | undefined): ts.Type | undefined { + if (!(node.flags & NodeFlags.InWithStatement)) { + switch (node.kind) { + case SyntaxKind.Identifier: + const symbol = getExportSymbolOfValueSymbolIfExported(getResolvedSymbol((node))); + return getExplicitTypeOfSymbol(symbol.flags & SymbolFlags.Alias ? resolveAlias(symbol) : symbol, diagnostic); + case SyntaxKind.ThisKeyword: + return getExplicitThisType(node); + case SyntaxKind.SuperKeyword: + return checkSuperExpression(node); + case SyntaxKind.PropertyAccessExpression: + const type = getTypeOfDottedName((node).expression, diagnostic); + const prop = type && getPropertyOfType(type, (node).name.escapedText); + return prop && getExplicitTypeOfSymbol(prop, diagnostic); + case SyntaxKind.ParenthesizedExpression: + return getTypeOfDottedName((node).expression, diagnostic); } - return false; } - - function mergeJSSymbols(target: Symbol, source: Symbol | undefined) { - if (source) { - const links = getSymbolLinks(source); - if (!links.inferredClassSymbol || !links.inferredClassSymbol.has("" + getSymbolId(target))) { - const inferred = isTransientSymbol(target) ? target : cloneSymbol(target) as TransientSymbol; - inferred.exports = inferred.exports || createSymbolTable(); - inferred.members = inferred.members || createSymbolTable(); - inferred.flags |= source.flags & SymbolFlags.Class; - if (hasEntries(source.exports)) { - mergeSymbolTable(inferred.exports, source.exports); - } - if (hasEntries(source.members)) { - mergeSymbolTable(inferred.members, source.members); - } - (links.inferredClassSymbol || (links.inferredClassSymbol = createMap())).set("" + getSymbolId(inferred), inferred); - return inferred; + } + function getEffectsSignature(node: CallExpression) { + const links = getNodeLinks(node); + let signature = links.effectsSignature; + if (signature === undefined) { + // A call expression parented by an expression statement is a potential assertion. Other call + // expressions are potential type predicate function calls. In order to avoid triggering + // circularities in control flow analysis, we use getTypeOfDottedName when resolving the call + // target expression of an assertion. + let funcType: ts.Type | undefined; + if (node.parent.kind === SyntaxKind.ExpressionStatement) { + funcType = getTypeOfDottedName(node.expression, /*diagnostic*/ undefined); + } + else if (node.expression.kind !== SyntaxKind.SuperKeyword) { + if (isOptionalChain(node)) { + funcType = checkNonNullType(getOptionalExpressionType(checkExpression(node.expression), node.expression), node.expression); + } + else { + funcType = checkNonNullExpression(node.expression); } - return links.inferredClassSymbol.get("" + getSymbolId(target)); } + const signatures = getSignaturesOfType(funcType && getApparentType(funcType) || unknownType, SignatureKind.Call); + const candidate = signatures.length === 1 && !signatures[0].typeParameters ? signatures[0] : + some(signatures, hasTypePredicateOrNeverReturnType) ? getResolvedSignature(node) : + undefined; + signature = links.effectsSignature = candidate && hasTypePredicateOrNeverReturnType(candidate) ? candidate : unknownSignature; } - - function getAssignedClassSymbol(decl: Declaration): Symbol | undefined { - const assignmentSymbol = decl && decl.parent && - (isFunctionDeclaration(decl) && getSymbolOfNode(decl) || - isBinaryExpression(decl.parent) && getSymbolOfNode(decl.parent.left) || - isVariableDeclaration(decl.parent) && getSymbolOfNode(decl.parent)); - const prototype = assignmentSymbol && assignmentSymbol.exports && assignmentSymbol.exports.get("prototype" as __String); - const init = prototype && prototype.valueDeclaration && getAssignedJSPrototype(prototype.valueDeclaration); - return init ? getSymbolOfNode(init) : undefined; + return signature === unknownSignature ? undefined : signature; + } + function hasTypePredicateOrNeverReturnType(signature: ts.Signature) { + return !!(getTypePredicateOfSignature(signature) || + signature.declaration && (getReturnTypeFromAnnotation(signature.declaration) || unknownType).flags & TypeFlags.Never); + } + function getTypePredicateArgument(predicate: TypePredicate, callExpression: CallExpression) { + if (predicate.kind === TypePredicateKind.Identifier || predicate.kind === TypePredicateKind.AssertsIdentifier) { + return callExpression.arguments[predicate.parameterIndex]; } - - function getAssignedJSPrototype(node: Node) { - if (!node.parent) { - return false; - } - let parent: Node = node.parent; - while (parent && parent.kind === SyntaxKind.PropertyAccessExpression) { - parent = parent.parent; - } - if (parent && isBinaryExpression(parent) && isPrototypeAccess(parent.left) && parent.operatorToken.kind === SyntaxKind.EqualsToken) { - const right = getInitializerOfBinaryExpression(parent); - return isObjectLiteralExpression(right) && right; + const invokedExpression = skipParentheses(callExpression.expression); + return isAccessExpression(invokedExpression) ? skipParentheses(invokedExpression.expression) : undefined; + } + function reportFlowControlError(node: Node) { + const block = (findAncestor(node, isFunctionOrModuleBlock)); + const sourceFile = getSourceFileOfNode(node); + const span = getSpanOfTokenAtPosition(sourceFile, block.statements.pos); + diagnostics.add(createFileDiagnostic(sourceFile, span.start, span.length, Diagnostics.The_containing_function_or_module_body_is_too_large_for_control_flow_analysis)); + } + function isReachableFlowNode(flow: FlowNode) { + const result = isReachableFlowNodeWorker(flow, /*noCacheCheck*/ false); + lastFlowNode = flow; + lastFlowNodeReachable = result; + return result; + } + function isFalseExpression(expr: Expression): boolean { + const node = skipParentheses(expr); + return node.kind === SyntaxKind.FalseKeyword || node.kind === SyntaxKind.BinaryExpression && ((node).operatorToken.kind === SyntaxKind.AmpersandAmpersandToken && (isFalseExpression((node).left) || isFalseExpression((node).right)) || + (node).operatorToken.kind === SyntaxKind.BarBarToken && isFalseExpression((node).left) && isFalseExpression((node).right)); + } + function isReachableFlowNodeWorker(flow: FlowNode, noCacheCheck: boolean): boolean { + while (true) { + if (flow === lastFlowNode) { + return lastFlowNodeReachable; } - } - - /** - * Syntactically and semantically checks a call or new expression. - * @param node The call/new expression to be checked. - * @returns On success, the expression's signature's return type. On failure, anyType. - */ - function checkCallExpression(node: CallExpression | NewExpression, checkMode?: CheckMode): Type { - if (!checkGrammarTypeArguments(node, node.typeArguments)) checkGrammarArguments(node.arguments); - - const signature = getResolvedSignature(node, /*candidatesOutArray*/ undefined, checkMode); - if (signature === resolvingSignature) { - // CheckMode.SkipGenericFunctions is enabled and this is a call to a generic function that - // returns a function type. We defer checking and return nonInferrableType. - return nonInferrableType; + const flags = flow.flags; + if (flags & FlowFlags.Shared) { + if (!noCacheCheck) { + const id = getFlowNodeId(flow); + const reachable = flowNodeReachable[id]; + return reachable !== undefined ? reachable : (flowNodeReachable[id] = isReachableFlowNodeWorker(flow, /*noCacheCheck*/ true)); + } + noCacheCheck = false; } - - if (node.expression.kind === SyntaxKind.SuperKeyword) { - return voidType; + if (flags & (FlowFlags.Assignment | FlowFlags.Condition | FlowFlags.ArrayMutation)) { + flow = (flow).antecedent; } - - if (node.kind === SyntaxKind.NewExpression) { - const declaration = signature.declaration; - - if (declaration && - declaration.kind !== SyntaxKind.Constructor && - declaration.kind !== SyntaxKind.ConstructSignature && - declaration.kind !== SyntaxKind.ConstructorType && - !isJSDocConstructSignature(declaration) && - !isJSConstructor(declaration)) { - - // When resolved signature is a call signature (and not a construct signature) the result type is any - if (noImplicitAny) { - error(node, Diagnostics.new_expression_whose_target_lacks_a_construct_signature_implicitly_has_an_any_type); + else if (flags & FlowFlags.Call) { + const signature = getEffectsSignature((flow).node); + if (signature) { + const predicate = getTypePredicateOfSignature(signature); + if (predicate && predicate.kind === TypePredicateKind.AssertsIdentifier) { + const predicateArgument = (flow).node.arguments[predicate.parameterIndex]; + if (predicateArgument && isFalseExpression(predicateArgument)) { + return false; + } + } + if (getReturnTypeOfSignature(signature).flags & TypeFlags.Never) { + return false; } - return anyType; } + flow = (flow).antecedent; } - - // In JavaScript files, calls to any identifier 'require' are treated as external module imports - if (isInJSFile(node) && isCommonJsRequire(node)) { - return resolveExternalModuleTypeByLiteral(node.arguments![0] as StringLiteral); - } - - const returnType = getReturnTypeOfSignature(signature); - // Treat any call to the global 'Symbol' function that is part of a const variable or readonly property - // as a fresh unique symbol literal type. - if (returnType.flags & TypeFlags.ESSymbolLike && isSymbolOrSymbolForCall(node)) { - return getESSymbolLikeTypeForNode(walkUpParenthesizedExpressions(node.parent)); + else if (flags & FlowFlags.BranchLabel) { + // A branching point is reachable if any branch is reachable. + return some((flow).antecedents, f => isReachableFlowNodeWorker(f, /*noCacheCheck*/ false)); } - if (node.kind === SyntaxKind.CallExpression && node.parent.kind === SyntaxKind.ExpressionStatement && - returnType.flags & TypeFlags.Void && getTypePredicateOfSignature(signature)) { - if (!isDottedName(node.expression)) { - error(node.expression, Diagnostics.Assertions_require_the_call_target_to_be_an_identifier_or_qualified_name); - } - else if (!getEffectsSignature(node)) { - const diagnostic = error(node.expression, Diagnostics.Assertions_require_every_name_in_the_call_target_to_be_declared_with_an_explicit_type_annotation); - getTypeOfDottedName(node.expression, diagnostic); - } + else if (flags & FlowFlags.LoopLabel) { + // A loop is reachable if the control flow path that leads to the top is reachable. + flow = (flow).antecedents![0]; } - - if (isInJSFile(node)) { - const decl = getDeclarationOfExpando(node); - if (decl) { - const jsSymbol = getSymbolOfNode(decl); - if (jsSymbol && hasEntries(jsSymbol.exports)) { - const jsAssignmentType = createAnonymousType(jsSymbol, jsSymbol.exports, emptyArray, emptyArray, undefined, undefined); - jsAssignmentType.objectFlags |= ObjectFlags.JSLiteral; - return getIntersectionType([returnType, jsAssignmentType]); - } + else if (flags & FlowFlags.SwitchClause) { + // The control flow path representing an unmatched value in a switch statement with + // no default clause is unreachable if the switch statement is exhaustive. + if ((flow).clauseStart === (flow).clauseEnd && isExhaustiveSwitchStatement((flow).switchStatement)) { + return false; } + flow = (flow).antecedent; } - - return returnType; - } - - function isSymbolOrSymbolForCall(node: Node) { - if (!isCallExpression(node)) return false; - let left = node.expression; - if (isPropertyAccessExpression(left) && left.name.escapedText === "for") { - left = left.expression; - } - if (!isIdentifier(left) || left.escapedText !== "Symbol") { - return false; + else if (flags & FlowFlags.ReduceLabel) { + // Cache is unreliable once we start adjusting labels + lastFlowNode = undefined; + const target = (flow).target; + const saveAntecedents = target.antecedents; + target.antecedents = (flow).antecedents; + const result = isReachableFlowNodeWorker((flow).antecedent, /*noCacheCheck*/ false); + target.antecedents = saveAntecedents; + return result; } - - // make sure `Symbol` is the global symbol - const globalESSymbol = getGlobalESSymbolConstructorSymbol(/*reportErrors*/ false); - if (!globalESSymbol) { - return false; + else { + return !(flags & FlowFlags.Unreachable); } - - return globalESSymbol === resolveName(left, "Symbol" as __String, SymbolFlags.Value, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false); } - - function checkImportCallExpression(node: ImportCall): Type { - // Check grammar of dynamic import - if (!checkGrammarArguments(node.arguments)) checkGrammarImportCallExpression(node); - - if (node.arguments.length === 0) { - return createPromiseReturnType(node, anyType); - } - const specifier = node.arguments[0]; - const specifierType = checkExpressionCached(specifier); - // Even though multiple arguments is grammatically incorrect, type-check extra arguments for completion - for (let i = 1; i < node.arguments.length; ++i) { - checkExpressionCached(node.arguments[i]); + } + function getFlowTypeOfReference(reference: Node, declaredType: ts.Type, initialType = declaredType, flowContainer?: Node, couldBeUninitialized?: boolean) { + let key: string | undefined; + let keySet = false; + let flowDepth = 0; + if (flowAnalysisDisabled) { + return errorType; + } + if (!reference.flowNode || !couldBeUninitialized && !(declaredType.flags & TypeFlags.Narrowable)) { + return declaredType; + } + flowInvocationCount++; + const sharedFlowStart = sharedFlowCount; + const evolvedType = getTypeFromFlowType(getTypeAtFlowNode(reference.flowNode)); + sharedFlowCount = sharedFlowStart; + // When the reference is 'x' in an 'x.length', 'x.push(value)', 'x.unshift(value)' or x[n] = value' operation, + // we give type 'any[]' to 'x' instead of using the type determined by control flow analysis such that operations + // on empty arrays are possible without implicit any errors and new element types can be inferred without + // type mismatch errors. + const resultType = getObjectFlags(evolvedType) & ObjectFlags.EvolvingArray && isEvolvingArrayOperationTarget(reference) ? autoArrayType : finalizeEvolvingArrayType(evolvedType); + if (resultType === unreachableNeverType || reference.parent && reference.parent.kind === SyntaxKind.NonNullExpression && getTypeWithFacts(resultType, TypeFacts.NEUndefinedOrNull).flags & TypeFlags.Never) { + return declaredType; + } + return resultType; + function getOrSetCacheKey() { + if (keySet) { + return key; } - - if (specifierType.flags & TypeFlags.Undefined || specifierType.flags & TypeFlags.Null || !isTypeAssignableTo(specifierType, stringType)) { - error(specifier, Diagnostics.Dynamic_import_s_specifier_must_be_of_type_string_but_here_has_type_0, typeToString(specifierType)); + keySet = true; + return key = getFlowCacheKey(reference, declaredType, initialType, flowContainer); + } + function getTypeAtFlowNode(flow: FlowNode): FlowType { + if (flowDepth === 2000) { + // We have made 2000 recursive invocations. To avoid overflowing the call stack we report an error + // and disable further control flow analysis in the containing function or module body. + flowAnalysisDisabled = true; + reportFlowControlError(reference); + return errorType; } - - // resolveExternalModuleName will return undefined if the moduleReferenceExpression is not a string literal - const moduleSymbol = resolveExternalModuleName(node, specifier); - if (moduleSymbol) { - const esModuleSymbol = resolveESModuleSymbol(moduleSymbol, specifier, /*dontRecursivelyResolve*/ true, /*suppressUsageError*/ false); - if (esModuleSymbol) { - return createPromiseReturnType(node, getTypeWithSyntheticDefaultImportType(getTypeOfSymbol(esModuleSymbol), esModuleSymbol, moduleSymbol)); + flowDepth++; + while (true) { + const flags = flow.flags; + if (flags & FlowFlags.Shared) { + // We cache results of flow type resolution for shared nodes that were previously visited in + // the same getFlowTypeOfReference invocation. A node is considered shared when it is the + // antecedent of more than one node. + for (let i = sharedFlowStart; i < sharedFlowCount; i++) { + if (sharedFlowNodes[i] === flow) { + flowDepth--; + return sharedFlowTypes[i]; + } + } } - } - return createPromiseReturnType(node, anyType); - } - - function getTypeWithSyntheticDefaultImportType(type: Type, symbol: Symbol, originalSymbol: Symbol): Type { - if (allowSyntheticDefaultImports && type && type !== errorType) { - const synthType = type as SyntheticDefaultModuleType; - if (!synthType.syntheticType) { - const file = find(originalSymbol.declarations, isSourceFile); - const hasSyntheticDefault = canHaveSyntheticDefault(file, originalSymbol, /*dontResolveAlias*/ false); - if (hasSyntheticDefault) { - const memberTable = createSymbolTable(); - const newSymbol = createSymbol(SymbolFlags.Alias, InternalSymbolName.Default); - newSymbol.nameType = getLiteralType("default"); - newSymbol.target = resolveSymbol(symbol); - memberTable.set(InternalSymbolName.Default, newSymbol); - const anonymousSymbol = createSymbol(SymbolFlags.TypeLiteral, InternalSymbolName.Type); - const defaultContainingObject = createAnonymousType(anonymousSymbol, memberTable, emptyArray, emptyArray, /*stringIndexInfo*/ undefined, /*numberIndexInfo*/ undefined); - anonymousSymbol.type = defaultContainingObject; - synthType.syntheticType = isValidSpreadType(type) ? getSpreadType(type, defaultContainingObject, anonymousSymbol, /*objectFlags*/ 0, /*readonly*/ false) : defaultContainingObject; + let type: FlowType | undefined; + if (flags & FlowFlags.Assignment) { + type = getTypeAtFlowAssignment((flow)); + if (!type) { + flow = (flow).antecedent; + continue; } - else { - synthType.syntheticType = type; + } + else if (flags & FlowFlags.Call) { + type = getTypeAtFlowCall((flow)); + if (!type) { + flow = (flow).antecedent; + continue; } } - return synthType.syntheticType; - } - return type; - } - - function isCommonJsRequire(node: Node): boolean { - if (!isRequireCall(node, /*checkArgumentIsStringLiteralLike*/ true)) { - return false; - } - - // Make sure require is not a local function - if (!isIdentifier(node.expression)) return Debug.fail(); - const resolvedRequire = resolveName(node.expression, node.expression.escapedText, SymbolFlags.Value, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ true)!; // TODO: GH#18217 - if (resolvedRequire === requireSymbol) { - return true; - } - // project includes symbol named 'require' - make sure that it is ambient and local non-alias - if (resolvedRequire.flags & SymbolFlags.Alias) { - return false; - } - - const targetDeclarationKind = resolvedRequire.flags & SymbolFlags.Function - ? SyntaxKind.FunctionDeclaration - : resolvedRequire.flags & SymbolFlags.Variable - ? SyntaxKind.VariableDeclaration - : SyntaxKind.Unknown; - if (targetDeclarationKind !== SyntaxKind.Unknown) { - const decl = getDeclarationOfKind(resolvedRequire, targetDeclarationKind)!; - // function/variable declaration should be ambient - return !!decl && !!(decl.flags & NodeFlags.Ambient); - } - return false; - } - - function checkTaggedTemplateExpression(node: TaggedTemplateExpression): Type { - if (!checkGrammarTaggedTemplateChain(node)) checkGrammarTypeArguments(node, node.typeArguments); - if (languageVersion < ScriptTarget.ES2015) { - checkExternalEmitHelpers(node, ExternalEmitHelpers.MakeTemplateObject); + else if (flags & FlowFlags.Condition) { + type = getTypeAtFlowCondition((flow)); + } + else if (flags & FlowFlags.SwitchClause) { + type = getTypeAtSwitchClause((flow)); + } + else if (flags & FlowFlags.Label) { + if ((flow).antecedents!.length === 1) { + flow = (flow).antecedents![0]; + continue; + } + type = flags & FlowFlags.BranchLabel ? + getTypeAtFlowBranchLabel((flow)) : + getTypeAtFlowLoopLabel((flow)); + } + else if (flags & FlowFlags.ArrayMutation) { + type = getTypeAtFlowArrayMutation((flow)); + if (!type) { + flow = (flow).antecedent; + continue; + } + } + else if (flags & FlowFlags.ReduceLabel) { + const target = (flow).target; + const saveAntecedents = target.antecedents; + target.antecedents = (flow).antecedents; + type = getTypeAtFlowNode((flow).antecedent); + target.antecedents = saveAntecedents; + } + else if (flags & FlowFlags.Start) { + // Check if we should continue with the control flow of the containing function. + const container = (flow).node; + if (container && container !== flowContainer && + reference.kind !== SyntaxKind.PropertyAccessExpression && + reference.kind !== SyntaxKind.ElementAccessExpression && + reference.kind !== SyntaxKind.ThisKeyword) { + flow = container.flowNode!; + continue; + } + // At the top of the flow we have the initial type. + type = initialType; + } + else { + // Unreachable code errors are reported in the binding phase. Here we + // simply return the non-auto declared type to reduce follow-on errors. + type = convertAutoToAny(declaredType); + } + if (flags & FlowFlags.Shared) { + // Record visited node and the associated type in the cache. + sharedFlowNodes[sharedFlowCount] = flow; + sharedFlowTypes[sharedFlowCount] = type; + sharedFlowCount++; + } + flowDepth--; + return type; } - return getReturnTypeOfSignature(getResolvedSignature(node)); } - - function checkAssertion(node: AssertionExpression) { - return checkAssertionWorker(node, node.type, node.expression); + function getInitialOrAssignedType(flow: FlowAssignment) { + const node = flow.node; + return getConstraintForLocation(node.kind === SyntaxKind.VariableDeclaration || node.kind === SyntaxKind.BindingElement ? + getInitialType((node)) : + getAssignedType(node), reference); } - - function isValidConstAssertionArgument(node: Node): boolean { - switch (node.kind) { - case SyntaxKind.StringLiteral: - case SyntaxKind.NoSubstitutionTemplateLiteral: - case SyntaxKind.NumericLiteral: - case SyntaxKind.BigIntLiteral: - case SyntaxKind.TrueKeyword: - case SyntaxKind.FalseKeyword: - case SyntaxKind.ArrayLiteralExpression: - case SyntaxKind.ObjectLiteralExpression: - return true; - case SyntaxKind.ParenthesizedExpression: - return isValidConstAssertionArgument((node).expression); - case SyntaxKind.PrefixUnaryExpression: - const op = (node).operator; - const arg = (node).operand; - return op === SyntaxKind.MinusToken && (arg.kind === SyntaxKind.NumericLiteral || arg.kind === SyntaxKind.BigIntLiteral) || - op === SyntaxKind.PlusToken && arg.kind === SyntaxKind.NumericLiteral; - case SyntaxKind.PropertyAccessExpression: - case SyntaxKind.ElementAccessExpression: - const expr = (node).expression; - if (isIdentifier(expr)) { - let symbol = getSymbolAtLocation(expr); - if (symbol && symbol.flags & SymbolFlags.Alias) { - symbol = resolveAlias(symbol); - } - return !!(symbol && (symbol.flags & SymbolFlags.Enum) && getEnumKind(symbol) === EnumKind.Literal); + function getTypeAtFlowAssignment(flow: FlowAssignment) { + const node = flow.node; + // Assignments only narrow the computed type if the declared type is a union type. Thus, we + // only need to evaluate the assigned type if the declared type is a union type. + if (isMatchingReference(reference, node)) { + if (!isReachableFlowNode(flow)) { + return unreachableNeverType; + } + if (getAssignmentTargetKind(node) === AssignmentKind.Compound) { + const flowType = getTypeAtFlowNode(flow.antecedent); + return createFlowType(getBaseTypeOfLiteralType(getTypeFromFlowType(flowType)), isIncomplete(flowType)); + } + if (declaredType === autoType || declaredType === autoArrayType) { + if (isEmptyArrayAssignment(node)) { + return getEvolvingArrayType(neverType); } - } - return false; - } - - function checkAssertionWorker(errNode: Node, type: TypeNode, expression: UnaryExpression | Expression, checkMode?: CheckMode) { - let exprType = checkExpression(expression, checkMode); - if (isConstTypeReference(type)) { - if (!isValidConstAssertionArgument(expression)) { - error(expression, Diagnostics.A_const_assertions_can_only_be_applied_to_references_to_enum_members_or_string_number_boolean_array_or_object_literals); + const assignedType = getBaseTypeOfLiteralType(getInitialOrAssignedType(flow)); + return isTypeAssignableTo(assignedType, declaredType) ? assignedType : anyArrayType; } - return getRegularTypeOfLiteralType(exprType); - } - checkSourceElement(type); - exprType = getRegularTypeOfObjectLiteral(getBaseTypeOfLiteralType(exprType)); - const targetType = getTypeFromTypeNode(type); - if (produceDiagnostics && targetType !== errorType) { - const widenedType = getWidenedType(exprType); - if (!isTypeComparableTo(targetType, widenedType)) { - checkTypeComparableTo(exprType, targetType, errNode, - Diagnostics.Conversion_of_type_0_to_type_1_may_be_a_mistake_because_neither_type_sufficiently_overlaps_with_the_other_If_this_was_intentional_convert_the_expression_to_unknown_first); + if (declaredType.flags & TypeFlags.Union) { + return getAssignmentReducedType((declaredType), getInitialOrAssignedType(flow)); } + return declaredType; } - return targetType; - } - - function checkNonNullAssertion(node: NonNullExpression) { - return getNonNullableType(checkExpression(node.expression)); - } - - function checkMetaProperty(node: MetaProperty): Type { - checkGrammarMetaProperty(node); - - if (node.keywordToken === SyntaxKind.NewKeyword) { - return checkNewTargetMetaProperty(node); + // We didn't have a direct match. However, if the reference is a dotted name, this + // may be an assignment to a left hand part of the reference. For example, for a + // reference 'x.y.z', we may be at an assignment to 'x.y' or 'x'. In that case, + // return the declared type. + if (containsMatchingReference(reference, node)) { + if (!isReachableFlowNode(flow)) { + return unreachableNeverType; + } + // A matching dotted name might also be an expando property on a function *expression*, + // in which case we continue control flow analysis back to the function's declaration + if (isVariableDeclaration(node) && (isInJSFile(node) || isVarConst(node))) { + const init = getDeclaredExpandoInitializer(node); + if (init && (init.kind === SyntaxKind.FunctionExpression || init.kind === SyntaxKind.ArrowFunction)) { + return getTypeAtFlowNode(flow.antecedent); + } + } + return declaredType; } - - if (node.keywordToken === SyntaxKind.ImportKeyword) { - return checkImportMetaProperty(node); + // for (const _ in ref) acts as a nonnull on ref + if (isVariableDeclaration(node) && node.parent.parent.kind === SyntaxKind.ForInStatement && isMatchingReference(reference, node.parent.parent.expression)) { + return getNonNullableTypeIfNeeded(getTypeFromFlowType(getTypeAtFlowNode(flow.antecedent))); } - - return Debug.assertNever(node.keywordToken); + // Assignment doesn't affect reference + return undefined; } - - function checkNewTargetMetaProperty(node: MetaProperty) { - const container = getNewTargetContainer(node); - if (!container) { - error(node, Diagnostics.Meta_property_0_is_only_allowed_in_the_body_of_a_function_declaration_function_expression_or_constructor, "new.target"); - return errorType; - } - else if (container.kind === SyntaxKind.Constructor) { - const symbol = getSymbolOfNode(container.parent as ClassLikeDeclaration); - return getTypeOfSymbol(symbol); + function narrowTypeByAssertion(type: ts.Type, expr: Expression): ts.Type { + const node = skipParentheses(expr); + if (node.kind === SyntaxKind.FalseKeyword) { + return unreachableNeverType; } - else { - const symbol = getSymbolOfNode(container)!; - return getTypeOfSymbol(symbol); + if (node.kind === SyntaxKind.BinaryExpression) { + if ((node).operatorToken.kind === SyntaxKind.AmpersandAmpersandToken) { + return narrowTypeByAssertion(narrowTypeByAssertion(type, (node).left), (node).right); + } + if ((node).operatorToken.kind === SyntaxKind.BarBarToken) { + return getUnionType([narrowTypeByAssertion(type, (node).left), narrowTypeByAssertion(type, (node).right)]); + } } + return narrowType(type, node, /*assumeTrue*/ true); } - - function checkImportMetaProperty(node: MetaProperty) { - if (moduleKind !== ModuleKind.ESNext && moduleKind !== ModuleKind.System) { - error(node, Diagnostics.The_import_meta_meta_property_is_only_allowed_when_the_module_option_is_esnext_or_system); + function getTypeAtFlowCall(flow: FlowCall): FlowType | undefined { + const signature = getEffectsSignature(flow.node); + if (signature) { + const predicate = getTypePredicateOfSignature(signature); + if (predicate && (predicate.kind === TypePredicateKind.AssertsThis || predicate.kind === TypePredicateKind.AssertsIdentifier)) { + const flowType = getTypeAtFlowNode(flow.antecedent); + const type = getTypeFromFlowType(flowType); + const narrowedType = predicate.type ? narrowTypeByTypePredicate(type, predicate, flow.node, /*assumeTrue*/ true) : + predicate.kind === TypePredicateKind.AssertsIdentifier && predicate.parameterIndex >= 0 && predicate.parameterIndex < flow.node.arguments.length ? narrowTypeByAssertion(type, flow.node.arguments[predicate.parameterIndex]) : + type; + return narrowedType === type ? flowType : createFlowType(narrowedType, isIncomplete(flowType)); + } + if (getReturnTypeOfSignature(signature).flags & TypeFlags.Never) { + return unreachableNeverType; + } } - const file = getSourceFileOfNode(node); - Debug.assert(!!(file.flags & NodeFlags.PossiblyContainsImportMeta), "Containing file is missing import meta node flag."); - Debug.assert(!!file.externalModuleIndicator, "Containing file should be a module."); - return node.name.escapedText === "meta" ? getGlobalImportMetaType() : errorType; + return undefined; } - - function getTypeOfParameter(symbol: Symbol) { - const type = getTypeOfSymbol(symbol); - if (strictNullChecks) { - const declaration = symbol.valueDeclaration; - if (declaration && hasInitializer(declaration)) { - return getOptionalType(type); + function getTypeAtFlowArrayMutation(flow: FlowArrayMutation): FlowType | undefined { + if (declaredType === autoType || declaredType === autoArrayType) { + const node = flow.node; + const expr = node.kind === SyntaxKind.CallExpression ? + (node.expression).expression : + (node.left).expression; + if (isMatchingReference(reference, getReferenceCandidate(expr))) { + const flowType = getTypeAtFlowNode(flow.antecedent); + const type = getTypeFromFlowType(flowType); + if (getObjectFlags(type) & ObjectFlags.EvolvingArray) { + let evolvedType = (type); + if (node.kind === SyntaxKind.CallExpression) { + for (const arg of node.arguments) { + evolvedType = addEvolvingArrayElementType(evolvedType, arg); + } + } + else { + // We must get the context free expression type so as to not recur in an uncached fashion on the LHS (which causes exponential blowup in compile time) + const indexType = getContextFreeTypeOfExpression((node.left).argumentExpression); + if (isTypeAssignableToKind(indexType, TypeFlags.NumberLike)) { + evolvedType = addEvolvingArrayElementType(evolvedType, node.right); + } + } + return evolvedType === type ? flowType : createFlowType(evolvedType, isIncomplete(flowType)); + } + return flowType; } } - return type; + return undefined; } - - function getParameterNameAtPosition(signature: Signature, pos: number) { - const paramCount = signature.parameters.length - (signatureHasRestParameter(signature) ? 1 : 0); - if (pos < paramCount) { - return signature.parameters[pos].escapedName; + function getTypeAtFlowCondition(flow: FlowCondition): FlowType { + const flowType = getTypeAtFlowNode(flow.antecedent); + const type = getTypeFromFlowType(flowType); + if (type.flags & TypeFlags.Never) { + return flowType; + } + // If we have an antecedent type (meaning we're reachable in some way), we first + // attempt to narrow the antecedent type. If that produces the never type, and if + // the antecedent type is incomplete (i.e. a transient type in a loop), then we + // take the type guard as an indication that control *could* reach here once we + // have the complete type. We proceed by switching to the silent never type which + // doesn't report errors when operators are applied to it. Note that this is the + // *only* place a silent never type is ever generated. + const assumeTrue = (flow.flags & FlowFlags.TrueCondition) !== 0; + const nonEvolvingType = finalizeEvolvingArrayType(type); + const narrowedType = narrowType(nonEvolvingType, flow.node, assumeTrue); + if (narrowedType === nonEvolvingType) { + return flowType; + } + const incomplete = isIncomplete(flowType); + const resultType = incomplete && narrowedType.flags & TypeFlags.Never ? silentNeverType : narrowedType; + return createFlowType(resultType, incomplete); + } + function getTypeAtSwitchClause(flow: FlowSwitchClause): FlowType { + const expr = flow.switchStatement.expression; + const flowType = getTypeAtFlowNode(flow.antecedent); + let type = getTypeFromFlowType(flowType); + if (isMatchingReference(reference, expr)) { + type = narrowTypeBySwitchOnDiscriminant(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd); + } + else if (expr.kind === SyntaxKind.TypeOfExpression && isMatchingReference(reference, (expr as TypeOfExpression).expression)) { + type = narrowBySwitchOnTypeOf(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd); } - const restParameter = signature.parameters[paramCount] || unknownSymbol; - const restType = getTypeOfSymbol(restParameter); - if (isTupleType(restType)) { - const associatedNames = ((restType).target).associatedNames; - const index = pos - paramCount; - return associatedNames && associatedNames[index] || restParameter.escapedName + "_" + index as __String; + else { + if (strictNullChecks) { + if (optionalChainContainsReference(expr, reference)) { + type = narrowTypeBySwitchOptionalChainContainment(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd, t => !(t.flags & (TypeFlags.Undefined | TypeFlags.Never))); + } + else if (expr.kind === SyntaxKind.TypeOfExpression && optionalChainContainsReference((expr as TypeOfExpression).expression, reference)) { + type = narrowTypeBySwitchOptionalChainContainment(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd, t => !(t.flags & TypeFlags.Never || t.flags & TypeFlags.StringLiteral && (t).value === "undefined")); + } + } + if (isMatchingReferenceDiscriminant(expr, type)) { + type = narrowTypeByDiscriminant(type, (expr as AccessExpression), t => narrowTypeBySwitchOnDiscriminant(t, flow.switchStatement, flow.clauseStart, flow.clauseEnd)); + } } - return restParameter.escapedName; - } - - function getTypeAtPosition(signature: Signature, pos: number): Type { - return tryGetTypeAtPosition(signature, pos) || anyType; + return createFlowType(type, isIncomplete(flowType)); } - - function tryGetTypeAtPosition(signature: Signature, pos: number): Type | undefined { - const paramCount = signature.parameters.length - (signatureHasRestParameter(signature) ? 1 : 0); - if (pos < paramCount) { - return getTypeOfParameter(signature.parameters[pos]); + function getTypeAtFlowBranchLabel(flow: FlowLabel): FlowType { + const antecedentTypes: ts.Type[] = []; + let subtypeReduction = false; + let seenIncomplete = false; + let bypassFlow: FlowSwitchClause | undefined; + for (const antecedent of flow.antecedents!) { + if (!bypassFlow && antecedent.flags & FlowFlags.SwitchClause && (antecedent).clauseStart === (antecedent).clauseEnd) { + // The antecedent is the bypass branch of a potentially exhaustive switch statement. + bypassFlow = (antecedent); + continue; + } + const flowType = getTypeAtFlowNode(antecedent); + const type = getTypeFromFlowType(flowType); + // If the type at a particular antecedent path is the declared type and the + // reference is known to always be assigned (i.e. when declared and initial types + // are the same), there is no reason to process more antecedents since the only + // possible outcome is subtypes that will be removed in the final union type anyway. + if (type === declaredType && declaredType === initialType) { + return type; + } + pushIfUnique(antecedentTypes, type); + // If an antecedent type is not a subset of the declared type, we need to perform + // subtype reduction. This happens when a "foreign" type is injected into the control + // flow using the instanceof operator or a user defined type predicate. + if (!isTypeSubsetOf(type, declaredType)) { + subtypeReduction = true; + } + if (isIncomplete(flowType)) { + seenIncomplete = true; + } } - if (signatureHasRestParameter(signature)) { - // We want to return the value undefined for an out of bounds parameter position, - // so we need to check bounds here before calling getIndexedAccessType (which - // otherwise would return the type 'undefined'). - const restType = getTypeOfSymbol(signature.parameters[paramCount]); - const index = pos - paramCount; - if (!isTupleType(restType) || restType.target.hasRestElement || index < getTypeArguments(restType).length) { - return getIndexedAccessType(restType, getLiteralType(index)); + if (bypassFlow) { + const flowType = getTypeAtFlowNode(bypassFlow); + const type = getTypeFromFlowType(flowType); + // If the bypass flow contributes a type we haven't seen yet and the switch statement + // isn't exhaustive, process the bypass flow type. Since exhaustiveness checks increase + // the risk of circularities, we only want to perform them when they make a difference. + if (!contains(antecedentTypes, type) && !isExhaustiveSwitchStatement(bypassFlow.switchStatement)) { + if (type === declaredType && declaredType === initialType) { + return type; + } + antecedentTypes.push(type); + if (!isTypeSubsetOf(type, declaredType)) { + subtypeReduction = true; + } + if (isIncomplete(flowType)) { + seenIncomplete = true; + } } } - return undefined; - } - - function getRestTypeAtPosition(source: Signature, pos: number): Type { - const paramCount = getParameterCount(source); - const restType = getEffectiveRestType(source); - const nonRestCount = paramCount - (restType ? 1 : 0); - if (restType && pos === nonRestCount) { - return restType; - } - const types = []; - const names = []; - for (let i = pos; i < nonRestCount; i++) { - types.push(getTypeAtPosition(source, i)); - names.push(getParameterNameAtPosition(source, i)); - } - if (restType) { - types.push(getIndexedAccessType(restType, numberType)); - names.push(getParameterNameAtPosition(source, nonRestCount)); - } - const minArgumentCount = getMinArgumentCount(source); - const minLength = minArgumentCount < pos ? 0 : minArgumentCount - pos; - return createTupleType(types, minLength, !!restType, /*readonly*/ false, names); + return createFlowType(getUnionOrEvolvingArrayType(antecedentTypes, subtypeReduction ? UnionReduction.Subtype : UnionReduction.Literal), seenIncomplete); } - - function getParameterCount(signature: Signature) { - const length = signature.parameters.length; - if (signatureHasRestParameter(signature)) { - const restType = getTypeOfSymbol(signature.parameters[length - 1]); - if (isTupleType(restType)) { - return length + getTypeArguments(restType).length - 1; - } + function getTypeAtFlowLoopLabel(flow: FlowLabel): FlowType { + // If we have previously computed the control flow type for the reference at + // this flow loop junction, return the cached type. + const id = getFlowNodeId(flow); + const cache = flowLoopCaches[id] || (flowLoopCaches[id] = createMap()); + const key = getOrSetCacheKey(); + if (!key) { + // No cache key is generated when binding patterns are in unnarrowable situations + return declaredType; + } + const cached = cache.get(key); + if (cached) { + return cached; } - return length; - } - - function getMinArgumentCount(signature: Signature) { - if (signatureHasRestParameter(signature)) { - const restType = getTypeOfSymbol(signature.parameters[signature.parameters.length - 1]); - if (isTupleType(restType)) { - const minLength = restType.target.minLength; - if (minLength > 0) { - return signature.parameters.length - 1 + minLength; + // If this flow loop junction and reference are already being processed, return + // the union of the types computed for each branch so far, marked as incomplete. + // It is possible to see an empty array in cases where loops are nested and the + // back edge of the outer loop reaches an inner loop that is already being analyzed. + // In such cases we restart the analysis of the inner loop, which will then see + // a non-empty in-process array for the outer loop and eventually terminate because + // the first antecedent of a loop junction is always the non-looping control flow + // path that leads to the top. + for (let i = flowLoopStart; i < flowLoopCount; i++) { + if (flowLoopNodes[i] === flow && flowLoopKeys[i] === key && flowLoopTypes[i].length) { + return createFlowType(getUnionOrEvolvingArrayType(flowLoopTypes[i], UnionReduction.Literal), /*incomplete*/ true); + } + } + // Add the flow loop junction and reference to the in-process stack and analyze + // each antecedent code path. + const antecedentTypes: ts.Type[] = []; + let subtypeReduction = false; + let firstAntecedentType: FlowType | undefined; + for (const antecedent of flow.antecedents!) { + let flowType; + if (!firstAntecedentType) { + // The first antecedent of a loop junction is always the non-looping control + // flow path that leads to the top. + flowType = firstAntecedentType = getTypeAtFlowNode(antecedent); + } + else { + // All but the first antecedent are the looping control flow paths that lead + // back to the loop junction. We track these on the flow loop stack. + flowLoopNodes[flowLoopCount] = flow; + flowLoopKeys[flowLoopCount] = key; + flowLoopTypes[flowLoopCount] = antecedentTypes; + flowLoopCount++; + const saveFlowTypeCache = flowTypeCache; + flowTypeCache = undefined; + flowType = getTypeAtFlowNode(antecedent); + flowTypeCache = saveFlowTypeCache; + flowLoopCount--; + // If we see a value appear in the cache it is a sign that control flow analysis + // was restarted and completed by checkExpressionCached. We can simply pick up + // the resulting type and bail out. + const cached = cache.get(key); + if (cached) { + return cached; } } + const type = getTypeFromFlowType(flowType); + pushIfUnique(antecedentTypes, type); + // If an antecedent type is not a subset of the declared type, we need to perform + // subtype reduction. This happens when a "foreign" type is injected into the control + // flow using the instanceof operator or a user defined type predicate. + if (!isTypeSubsetOf(type, declaredType)) { + subtypeReduction = true; + } + // If the type at a particular antecedent path is the declared type there is no + // reason to process more antecedents since the only possible outcome is subtypes + // that will be removed in the final union type anyway. + if (type === declaredType) { + break; + } } - return signature.minArgumentCount; - } - - function hasEffectiveRestParameter(signature: Signature) { - if (signatureHasRestParameter(signature)) { - const restType = getTypeOfSymbol(signature.parameters[signature.parameters.length - 1]); - return !isTupleType(restType) || restType.target.hasRestElement; + // The result is incomplete if the first antecedent (the non-looping control flow path) + // is incomplete. + const result = getUnionOrEvolvingArrayType(antecedentTypes, subtypeReduction ? UnionReduction.Subtype : UnionReduction.Literal); + if (isIncomplete(firstAntecedentType!)) { + return createFlowType(result, /*incomplete*/ true); } - return false; + cache.set(key, result); + return result; } - - function getEffectiveRestType(signature: Signature) { - if (signatureHasRestParameter(signature)) { - const restType = getTypeOfSymbol(signature.parameters[signature.parameters.length - 1]); - return isTupleType(restType) ? getRestArrayTypeOfTupleType(restType) : restType; + function isMatchingReferenceDiscriminant(expr: Expression, computedType: ts.Type) { + if (!(computedType.flags & TypeFlags.Union) || !isAccessExpression(expr)) { + return false; } - return undefined; - } - - function getNonArrayRestType(signature: Signature) { - const restType = getEffectiveRestType(signature); - return restType && !isArrayType(restType) && !isTypeAny(restType) ? restType : undefined; - } - - function getTypeOfFirstParameterOfSignature(signature: Signature) { - return getTypeOfFirstParameterOfSignatureWithFallback(signature, neverType); - } - - function getTypeOfFirstParameterOfSignatureWithFallback(signature: Signature, fallbackType: Type) { - return signature.parameters.length > 0 ? getTypeAtPosition(signature, 0) : fallbackType; + const name = getAccessedPropertyName(expr); + if (name === undefined) { + return false; + } + return isMatchingReference(reference, expr.expression) && isDiscriminantProperty(computedType, name); } - - function inferFromAnnotatedParameters(signature: Signature, context: Signature, inferenceContext: InferenceContext) { - const len = signature.parameters.length - (signatureHasRestParameter(signature) ? 1 : 0); - for (let i = 0; i < len; i++) { - const declaration = signature.parameters[i].valueDeclaration; - if (declaration.type) { - const typeNode = getEffectiveTypeAnnotationNode(declaration); - if (typeNode) { - inferTypes(inferenceContext.inferences, getTypeFromTypeNode(typeNode), getTypeAtPosition(context, i)); - } - } + function narrowTypeByDiscriminant(type: ts.Type, access: AccessExpression, narrowType: (t: ts.Type) => ts.Type): ts.Type { + const propName = getAccessedPropertyName(access); + if (propName === undefined) { + return type; } - const restType = getEffectiveRestType(context); - if (restType && restType.flags & TypeFlags.TypeParameter) { - // The contextual signature has a generic rest parameter. We first instantiate the contextual - // signature (without fixing type parameters) and assign types to contextually typed parameters. - const instantiatedContext = instantiateSignature(context, inferenceContext.nonFixingMapper); - assignContextualParameterTypes(signature, instantiatedContext); - // We then infer from a tuple type representing the parameters that correspond to the contextual - // rest parameter. - const restPos = getParameterCount(context) - 1; - inferTypes(inferenceContext.inferences, getRestTypeAtPosition(signature, restPos), restType); + const propType = getTypeOfPropertyOfType(type, propName); + if (!propType) { + return type; } + const narrowedPropType = narrowType(propType); + return filterType(type, t => { + const discriminantType = getTypeOfPropertyOrIndexSignature(t, propName); + return !(discriminantType.flags & TypeFlags.Never) && isTypeComparableTo(discriminantType, narrowedPropType); + }); } - - function assignContextualParameterTypes(signature: Signature, context: Signature) { - signature.typeParameters = context.typeParameters; - if (context.thisParameter) { - const parameter = signature.thisParameter; - if (!parameter || parameter.valueDeclaration && !(parameter.valueDeclaration).type) { - if (!parameter) { - signature.thisParameter = createSymbolWithType(context.thisParameter, /*type*/ undefined); - } - assignParameterType(signature.thisParameter!, getTypeOfSymbol(context.thisParameter)); - } + function narrowTypeByTruthiness(type: ts.Type, expr: Expression, assumeTrue: boolean): ts.Type { + if (isMatchingReference(reference, expr)) { + return getTypeWithFacts(type, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy); } - const len = signature.parameters.length - (signatureHasRestParameter(signature) ? 1 : 0); - for (let i = 0; i < len; i++) { - const parameter = signature.parameters[i]; - if (!getEffectiveTypeAnnotationNode(parameter.valueDeclaration)) { - const contextualParameterType = tryGetTypeAtPosition(context, i); - assignParameterType(parameter, contextualParameterType); - } + if (strictNullChecks && assumeTrue && optionalChainContainsReference(expr, reference)) { + type = getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull); } - if (signatureHasRestParameter(signature)) { - // parameter might be a transient symbol generated by use of `arguments` in the function body. - const parameter = last(signature.parameters); - if (isTransientSymbol(parameter) || !getEffectiveTypeAnnotationNode(parameter.valueDeclaration)) { - const contextualParameterType = getRestTypeAtPosition(context, len); - assignParameterType(parameter, contextualParameterType); - } + if (isMatchingReferenceDiscriminant(expr, declaredType)) { + return narrowTypeByDiscriminant(type, (expr), t => getTypeWithFacts(t, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy)); } + return type; } - - function assignNonContextualParameterTypes(signature: Signature) { - if (signature.thisParameter) { - assignParameterType(signature.thisParameter); + function isTypePresencePossible(type: ts.Type, propName: __String, assumeTrue: boolean) { + if (getIndexInfoOfType(type, IndexKind.String)) { + return true; } - for (const parameter of signature.parameters) { - assignParameterType(parameter); + const prop = getPropertyOfType(type, propName); + if (prop) { + return prop.flags & SymbolFlags.Optional ? true : assumeTrue; } + return !assumeTrue; } - - function assignParameterType(parameter: Symbol, type?: Type) { - const links = getSymbolLinks(parameter); - if (!links.type) { - const declaration = parameter.valueDeclaration as ParameterDeclaration; - links.type = type || getWidenedTypeForVariableLikeDeclaration(declaration, /*includeOptionality*/ true); - if (declaration.name.kind !== SyntaxKind.Identifier) { - // if inference didn't come up with anything but unknown, fall back to the binding pattern if present. - if (links.type === unknownType) { - links.type = getTypeFromBindingPattern(declaration.name); - } - assignBindingElementTypes(declaration.name); - } + function narrowByInKeyword(type: ts.Type, literal: LiteralExpression, assumeTrue: boolean) { + if (type.flags & (TypeFlags.Union | TypeFlags.Object) || isThisTypeParameter(type)) { + const propName = escapeLeadingUnderscores(literal.text); + return filterType(type, t => isTypePresencePossible(t, propName, assumeTrue)); } + return type; } - - // When contextual typing assigns a type to a parameter that contains a binding pattern, we also need to push - // the destructured type into the contained binding elements. - function assignBindingElementTypes(pattern: BindingPattern) { - for (const element of pattern.elements) { - if (!isOmittedExpression(element)) { - if (element.name.kind === SyntaxKind.Identifier) { - getSymbolLinks(getSymbolOfNode(element)).type = getTypeForBindingElement(element); + function narrowTypeByBinaryExpression(type: ts.Type, expr: BinaryExpression, assumeTrue: boolean): ts.Type { + switch (expr.operatorToken.kind) { + case SyntaxKind.EqualsToken: + return narrowTypeByTruthiness(narrowType(type, expr.right, assumeTrue), expr.left, assumeTrue); + case SyntaxKind.EqualsEqualsToken: + case SyntaxKind.ExclamationEqualsToken: + case SyntaxKind.EqualsEqualsEqualsToken: + case SyntaxKind.ExclamationEqualsEqualsToken: + const operator = expr.operatorToken.kind; + const left = getReferenceCandidate(expr.left); + const right = getReferenceCandidate(expr.right); + if (left.kind === SyntaxKind.TypeOfExpression && isStringLiteralLike(right)) { + return narrowTypeByTypeof(type, (left), operator, right, assumeTrue); } - else { - assignBindingElementTypes(element.name); + if (right.kind === SyntaxKind.TypeOfExpression && isStringLiteralLike(left)) { + return narrowTypeByTypeof(type, (right), operator, left, assumeTrue); } - } + if (isMatchingReference(reference, left)) { + return narrowTypeByEquality(type, operator, right, assumeTrue); + } + if (isMatchingReference(reference, right)) { + return narrowTypeByEquality(type, operator, left, assumeTrue); + } + if (strictNullChecks) { + if (optionalChainContainsReference(left, reference)) { + type = narrowTypeByOptionalChainContainment(type, operator, right, assumeTrue); + } + else if (optionalChainContainsReference(right, reference)) { + type = narrowTypeByOptionalChainContainment(type, operator, left, assumeTrue); + } + } + if (isMatchingReferenceDiscriminant(left, declaredType)) { + return narrowTypeByDiscriminant(type, (left), t => narrowTypeByEquality(t, operator, right, assumeTrue)); + } + if (isMatchingReferenceDiscriminant(right, declaredType)) { + return narrowTypeByDiscriminant(type, (right), t => narrowTypeByEquality(t, operator, left, assumeTrue)); + } + break; + case SyntaxKind.InstanceOfKeyword: + return narrowTypeByInstanceof(type, expr, assumeTrue); + case SyntaxKind.InKeyword: + const target = getReferenceCandidate(expr.right); + if (isStringLiteralLike(expr.left) && isMatchingReference(reference, target)) { + return narrowByInKeyword(type, expr.left, assumeTrue); + } + break; + case SyntaxKind.CommaToken: + return narrowType(type, expr.right, assumeTrue); } + return type; } - - function createPromiseType(promisedType: Type): Type { - // creates a `Promise` type where `T` is the promisedType argument - const globalPromiseType = getGlobalPromiseType(/*reportErrors*/ true); - if (globalPromiseType !== emptyGenericType) { - // if the promised type is itself a promise, get the underlying type; otherwise, fallback to the promised type - promisedType = getAwaitedType(promisedType) || unknownType; - return createTypeReference(globalPromiseType, [promisedType]); + function narrowTypeByOptionalChainContainment(type: ts.Type, operator: SyntaxKind, value: Expression, assumeTrue: boolean): ts.Type { + // We are in a branch of obj?.foo === value (or any one of the other equality operators). We narrow obj as follows: + // When operator is === and type of value excludes undefined, null and undefined is removed from type of obj in true branch. + // When operator is !== and type of value excludes undefined, null and undefined is removed from type of obj in false branch. + // When operator is == and type of value excludes null and undefined, null and undefined is removed from type of obj in true branch. + // When operator is != and type of value excludes null and undefined, null and undefined is removed from type of obj in false branch. + // When operator is === and type of value is undefined, null and undefined is removed from type of obj in false branch. + // When operator is !== and type of value is undefined, null and undefined is removed from type of obj in true branch. + // When operator is == and type of value is null or undefined, null and undefined is removed from type of obj in false branch. + // When operator is != and type of value is null or undefined, null and undefined is removed from type of obj in true branch. + const equalsOperator = operator === SyntaxKind.EqualsEqualsToken || operator === SyntaxKind.EqualsEqualsEqualsToken; + const nullableFlags = operator === SyntaxKind.EqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsToken ? TypeFlags.Nullable : TypeFlags.Undefined; + const valueType = getTypeOfExpression(value); + // Note that we include any and unknown in the exclusion test because their domain includes null and undefined. + const removeNullable = equalsOperator !== assumeTrue && everyType(valueType, t => !!(t.flags & nullableFlags)) || + equalsOperator === assumeTrue && everyType(valueType, t => !(t.flags & (TypeFlags.AnyOrUnknown | nullableFlags))); + return removeNullable ? getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type; + } + function narrowTypeByEquality(type: ts.Type, operator: SyntaxKind, value: Expression, assumeTrue: boolean): ts.Type { + if (type.flags & TypeFlags.Any) { + return type; } - - return unknownType; - } - - function createPromiseLikeType(promisedType: Type): Type { - // creates a `PromiseLike` type where `T` is the promisedType argument - const globalPromiseLikeType = getGlobalPromiseLikeType(/*reportErrors*/ true); - if (globalPromiseLikeType !== emptyGenericType) { - // if the promised type is itself a promise, get the underlying type; otherwise, fallback to the promised type - promisedType = getAwaitedType(promisedType) || unknownType; - return createTypeReference(globalPromiseLikeType, [promisedType]); + if (operator === SyntaxKind.ExclamationEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken) { + assumeTrue = !assumeTrue; } - - return unknownType; - } - - function createPromiseReturnType(func: FunctionLikeDeclaration | ImportCall, promisedType: Type) { - const promiseType = createPromiseType(promisedType); - if (promiseType === unknownType) { - error(func, isImportCall(func) ? - Diagnostics.A_dynamic_import_call_returns_a_Promise_Make_sure_you_have_a_declaration_for_Promise_or_include_ES2015_in_your_lib_option : - Diagnostics.An_async_function_or_method_must_return_a_Promise_Make_sure_you_have_a_declaration_for_Promise_or_include_ES2015_in_your_lib_option); - return errorType; + const valueType = getTypeOfExpression(value); + if ((type.flags & TypeFlags.Unknown) && assumeTrue && (operator === SyntaxKind.EqualsEqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken)) { + if (valueType.flags & (TypeFlags.Primitive | TypeFlags.NonPrimitive)) { + return valueType; + } + if (valueType.flags & TypeFlags.Object) { + return nonPrimitiveType; + } + return type; } - else if (!getGlobalPromiseConstructorSymbol(/*reportErrors*/ true)) { - error(func, isImportCall(func) ? - Diagnostics.A_dynamic_import_call_in_ES5_SlashES3_requires_the_Promise_constructor_Make_sure_you_have_a_declaration_for_the_Promise_constructor_or_include_ES2015_in_your_lib_option : - Diagnostics.An_async_function_or_method_in_ES5_SlashES3_requires_the_Promise_constructor_Make_sure_you_have_a_declaration_for_the_Promise_constructor_or_include_ES2015_in_your_lib_option); + if (valueType.flags & TypeFlags.Nullable) { + if (!strictNullChecks) { + return type; + } + const doubleEquals = operator === SyntaxKind.EqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsToken; + const facts = doubleEquals ? + assumeTrue ? TypeFacts.EQUndefinedOrNull : TypeFacts.NEUndefinedOrNull : + valueType.flags & TypeFlags.Null ? + assumeTrue ? TypeFacts.EQNull : TypeFacts.NENull : + assumeTrue ? TypeFacts.EQUndefined : TypeFacts.NEUndefined; + return getTypeWithFacts(type, facts); } - - return promiseType; - } - - function getReturnTypeFromBody(func: FunctionLikeDeclaration, checkMode?: CheckMode): Type { - if (!func.body) { - return errorType; + if (type.flags & TypeFlags.NotUnionOrUnit) { + return type; } - - const functionFlags = getFunctionFlags(func); - const isAsync = (functionFlags & FunctionFlags.Async) !== 0; - const isGenerator = (functionFlags & FunctionFlags.Generator) !== 0; - - let returnType: Type | undefined; - let yieldType: Type | undefined; - let nextType: Type | undefined; - let fallbackReturnType: Type = voidType; - if (func.body.kind !== SyntaxKind.Block) { // Async or normal arrow function - returnType = checkExpressionCached(func.body, checkMode && checkMode & ~CheckMode.SkipGenericFunctions); - if (isAsync) { - // From within an async function you can return either a non-promise value or a promise. Any - // Promise/A+ compatible implementation will always assimilate any foreign promise, so the - // return type of the body should be unwrapped to its awaited type, which we will wrap in - // the native Promise type later in this function. - returnType = checkAwaitedType(returnType, /*errorNode*/ func, Diagnostics.The_return_type_of_an_async_function_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member); - } + if (assumeTrue) { + const filterFn: (t: ts.Type) => boolean = operator === SyntaxKind.EqualsEqualsToken ? + (t => areTypesComparable(t, valueType) || isCoercibleUnderDoubleEquals(t, valueType)) : + t => areTypesComparable(t, valueType); + const narrowedType = filterType(type, filterFn); + return narrowedType.flags & TypeFlags.Never ? type : replacePrimitivesWithLiterals(narrowedType, valueType); + } + if (isUnitType(valueType)) { + const regularType = getRegularTypeOfLiteralType(valueType); + return filterType(type, t => getRegularTypeOfLiteralType(t) !== regularType); } - else if (isGenerator) { // Generator or AsyncGenerator function - const returnTypes = checkAndAggregateReturnExpressionTypes(func, checkMode); - if (!returnTypes) { - fallbackReturnType = neverType; + return type; + } + function narrowTypeByTypeof(type: ts.Type, typeOfExpr: TypeOfExpression, operator: SyntaxKind, literal: LiteralExpression, assumeTrue: boolean): ts.Type { + // We have '==', '!=', '===', or !==' operator with 'typeof xxx' and string literal operands + if (operator === SyntaxKind.ExclamationEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken) { + assumeTrue = !assumeTrue; + } + const target = getReferenceCandidate(typeOfExpr.expression); + if (!isMatchingReference(reference, target)) { + if (strictNullChecks && optionalChainContainsReference(target, reference) && assumeTrue === (literal.text !== "undefined")) { + return getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull); } - else if (returnTypes.length > 0) { - returnType = getUnionType(returnTypes, UnionReduction.Subtype); + // For a reference of the form 'x.y', a 'typeof x === ...' type guard resets the + // narrowed type of 'y' to its declared type. + if (containsMatchingReference(reference, target)) { + return declaredType; } - const { yieldTypes, nextTypes } = checkAndAggregateYieldOperandTypes(func, checkMode); - yieldType = some(yieldTypes) ? getUnionType(yieldTypes, UnionReduction.Subtype) : undefined; - nextType = some(nextTypes) ? getIntersectionType(nextTypes) : undefined; + return type; } - else { // Async or normal function - const types = checkAndAggregateReturnExpressionTypes(func, checkMode); - if (!types) { - // For an async function, the return type will not be never, but rather a Promise for never. - return functionFlags & FunctionFlags.Async - ? createPromiseReturnType(func, neverType) // Async function - : neverType; // Normal function - } - if (types.length === 0) { - // For an async function, the return type will not be void, but rather a Promise for void. - return functionFlags & FunctionFlags.Async - ? createPromiseReturnType(func, voidType) // Async function - : voidType; // Normal function + if (type.flags & TypeFlags.Any && literal.text === "function") { + return type; + } + const facts = assumeTrue ? + typeofEQFacts.get(literal.text) || TypeFacts.TypeofEQHostObject : + typeofNEFacts.get(literal.text) || TypeFacts.TypeofNEHostObject; + return getTypeWithFacts(assumeTrue ? mapType(type, narrowTypeForTypeof) : type, facts); + function narrowTypeForTypeof(type: ts.Type) { + if (type.flags & TypeFlags.Unknown && literal.text === "object") { + return getUnionType([nonPrimitiveType, nullType]); + } + // We narrow a non-union type to an exact primitive type if the non-union type + // is a supertype of that primitive type. For example, type 'any' can be narrowed + // to one of the primitive types. + const targetType = literal.text === "function" ? globalFunctionType : typeofTypesByName.get(literal.text); + if (targetType) { + if (isTypeSubtypeOf(type, targetType)) { + return type; + } + if (isTypeSubtypeOf(targetType, type)) { + return targetType; + } + if (type.flags & TypeFlags.Instantiable) { + const constraint = getBaseConstraintOfType(type) || anyType; + if (isTypeSubtypeOf(targetType, constraint)) { + return getIntersectionType([type, targetType]); + } + } } - - // Return a union of the return expression types. - returnType = getUnionType(types, UnionReduction.Subtype); + return type; } - - if (returnType || yieldType || nextType) { - const contextualSignature = getContextualSignatureForFunctionLikeDeclaration(func); - if (!contextualSignature) { - if (yieldType) reportErrorsFromWidening(func, yieldType, WideningKind.GeneratorYield); - if (returnType) reportErrorsFromWidening(func, returnType); - if (nextType) reportErrorsFromWidening(func, nextType); - } - if (returnType && isUnitType(returnType) || - yieldType && isUnitType(yieldType) || - nextType && isUnitType(nextType)) { - const contextualType = !contextualSignature ? undefined : - contextualSignature === getSignatureFromDeclaration(func) ? isGenerator ? undefined : returnType : - instantiateContextualType(getReturnTypeOfSignature(contextualSignature), func); - if (isGenerator) { - yieldType = getWidenedLiteralLikeTypeForContextualIterationTypeIfNeeded(yieldType, contextualType, IterationTypeKind.Yield, isAsync); - returnType = getWidenedLiteralLikeTypeForContextualIterationTypeIfNeeded(returnType, contextualType, IterationTypeKind.Return, isAsync); - nextType = getWidenedLiteralLikeTypeForContextualIterationTypeIfNeeded(nextType, contextualType, IterationTypeKind.Next, isAsync); + } + function narrowTypeBySwitchOptionalChainContainment(type: ts.Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number, clauseCheck: (type: ts.Type) => boolean) { + const everyClauseChecks = clauseStart !== clauseEnd && every(getSwitchClauseTypes(switchStatement).slice(clauseStart, clauseEnd), clauseCheck); + return everyClauseChecks ? getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type; + } + function narrowTypeBySwitchOnDiscriminant(type: ts.Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number) { + // We only narrow if all case expressions specify + // values with unit types, except for the case where + // `type` is unknown. In this instance we map object + // types to the nonPrimitive type and narrow with that. + const switchTypes = getSwitchClauseTypes(switchStatement); + if (!switchTypes.length) { + return type; + } + const clauseTypes = switchTypes.slice(clauseStart, clauseEnd); + const hasDefaultClause = clauseStart === clauseEnd || contains(clauseTypes, neverType); + if ((type.flags & TypeFlags.Unknown) && !hasDefaultClause) { + let groundClauseTypes: ts.Type[] | undefined; + for (let i = 0; i < clauseTypes.length; i += 1) { + const t = clauseTypes[i]; + if (t.flags & (TypeFlags.Primitive | TypeFlags.NonPrimitive)) { + if (groundClauseTypes !== undefined) { + groundClauseTypes.push(t); + } + } + else if (t.flags & TypeFlags.Object) { + if (groundClauseTypes === undefined) { + groundClauseTypes = clauseTypes.slice(0, i); + } + groundClauseTypes.push(nonPrimitiveType); } else { - returnType = getWidenedLiteralLikeTypeForContextualReturnTypeIfNeeded(returnType, contextualType, isAsync); + return type; } } - - if (yieldType) yieldType = getWidenedType(yieldType); - if (returnType) returnType = getWidenedType(returnType); - if (nextType) nextType = getWidenedType(nextType); + return getUnionType(groundClauseTypes === undefined ? clauseTypes : groundClauseTypes); } - - if (isGenerator) { - return createGeneratorReturnType( - yieldType || neverType, - returnType || fallbackReturnType, - nextType || getContextualIterationType(IterationTypeKind.Next, func) || unknownType, - isAsync); - } - else { - // From within an async function you can return either a non-promise value or a promise. Any - // Promise/A+ compatible implementation will always assimilate any foreign promise, so the - // return type of the body is awaited type of the body, wrapped in a native Promise type. - return isAsync - ? createPromiseType(returnType || fallbackReturnType) - : returnType || fallbackReturnType; + const discriminantType = getUnionType(clauseTypes); + const caseType = discriminantType.flags & TypeFlags.Never ? neverType : + replacePrimitivesWithLiterals(filterType(type, t => areTypesComparable(discriminantType, t)), discriminantType); + if (!hasDefaultClause) { + return caseType; } + const defaultType = filterType(type, t => !(isUnitType(t) && contains(switchTypes, getRegularTypeOfLiteralType(t)))); + return caseType.flags & TypeFlags.Never ? defaultType : getUnionType([caseType, defaultType]); } - - function createGeneratorReturnType(yieldType: Type, returnType: Type, nextType: Type, isAsyncGenerator: boolean) { - const resolver = isAsyncGenerator ? asyncIterationTypesResolver : syncIterationTypesResolver; - const globalGeneratorType = resolver.getGlobalGeneratorType(/*reportErrors*/ false); - yieldType = resolver.resolveIterationType(yieldType, /*errorNode*/ undefined) || unknownType; - returnType = resolver.resolveIterationType(returnType, /*errorNode*/ undefined) || unknownType; - nextType = resolver.resolveIterationType(nextType, /*errorNode*/ undefined) || unknownType; - if (globalGeneratorType === emptyGenericType) { - // Fall back to the global IterableIterator if returnType is assignable to the expected return iteration - // type of IterableIterator, and the expected next iteration type of IterableIterator is assignable to - // nextType. - const globalType = resolver.getGlobalIterableIteratorType(/*reportErrors*/ false); - const iterationTypes = globalType !== emptyGenericType ? getIterationTypesOfGlobalIterableType(globalType, resolver) : undefined; - const iterableIteratorReturnType = iterationTypes ? iterationTypes.returnType : anyType; - const iterableIteratorNextType = iterationTypes ? iterationTypes.nextType : undefinedType; - if (isTypeAssignableTo(returnType, iterableIteratorReturnType) && - isTypeAssignableTo(iterableIteratorNextType, nextType)) { - if (globalType !== emptyGenericType) { - return createTypeFromGenericGlobalType(globalType, [yieldType]); - } - - // The global IterableIterator type doesn't exist, so report an error - resolver.getGlobalIterableIteratorType(/*reportErrors*/ true); - return emptyObjectType; - } - - // The global Generator type doesn't exist, so report an error - resolver.getGlobalGeneratorType(/*reportErrors*/ true); - return emptyObjectType; + function getImpliedTypeFromTypeofCase(type: ts.Type, text: string) { + switch (text) { + case "function": + return type.flags & TypeFlags.Any ? type : globalFunctionType; + case "object": + return type.flags & TypeFlags.Unknown ? getUnionType([nonPrimitiveType, nullType]) : type; + default: + return typeofTypesByName.get(text) || type; } - - return createTypeFromGenericGlobalType(globalGeneratorType, [yieldType, returnType, nextType]); } - - function checkAndAggregateYieldOperandTypes(func: FunctionLikeDeclaration, checkMode: CheckMode | undefined) { - const yieldTypes: Type[] = []; - const nextTypes: Type[] = []; - const isAsync = (getFunctionFlags(func) & FunctionFlags.Async) !== 0; - forEachYieldExpression(func.body, yieldExpression => { - const yieldExpressionType = yieldExpression.expression ? checkExpression(yieldExpression.expression, checkMode) : undefinedWideningType; - pushIfUnique(yieldTypes, getYieldedTypeOfYieldExpression(yieldExpression, yieldExpressionType, anyType, isAsync)); - let nextType: Type | undefined; - if (yieldExpression.asteriskToken) { - const iterationTypes = getIterationTypesOfIterable( - yieldExpressionType, - isAsync ? IterationUse.AsyncYieldStar : IterationUse.YieldStar, - yieldExpression.expression); - nextType = iterationTypes && iterationTypes.nextType; + function narrowTypeForTypeofSwitch(candidate: ts.Type) { + return (type: ts.Type) => { + if (isTypeSubtypeOf(candidate, type)) { + return candidate; } - else { - nextType = getContextualType(yieldExpression); + if (type.flags & TypeFlags.Instantiable) { + const constraint = getBaseConstraintOfType(type) || anyType; + if (isTypeSubtypeOf(candidate, constraint)) { + return getIntersectionType([type, candidate]); + } } - if (nextType) pushIfUnique(nextTypes, nextType); - }); - return { yieldTypes, nextTypes }; - } - - function getYieldedTypeOfYieldExpression(node: YieldExpression, expressionType: Type, sentType: Type, isAsync: boolean): Type | undefined { - const errorNode = node.expression || node; - // A `yield*` expression effectively yields everything that its operand yields - const yieldedType = node.asteriskToken ? checkIteratedTypeOrElementType(isAsync ? IterationUse.AsyncYieldStar : IterationUse.YieldStar, expressionType, sentType, errorNode) : expressionType; - return !isAsync ? yieldedType : getAwaitedType(yieldedType, errorNode, node.asteriskToken - ? Diagnostics.Type_of_iterated_elements_of_a_yield_Asterisk_operand_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member - : Diagnostics.Type_of_yield_operand_in_an_async_generator_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member); + return type; + }; } - - /** - * Collect the TypeFacts learned from a typeof switch with - * total clauses `witnesses`, and the active clause ranging - * from `start` to `end`. Parameter `hasDefault` denotes - * whether the active clause contains a default clause. - */ - function getFactsFromTypeofSwitch(start: number, end: number, witnesses: string[], hasDefault: boolean): TypeFacts { - let facts: TypeFacts = TypeFacts.None; - // When in the default we only collect inequality facts - // because default is 'in theory' a set of infinite - // equalities. - if (hasDefault) { - // Value is not equal to any types after the active clause. - for (let i = end; i < witnesses.length; i++) { - facts |= typeofNEFacts.get(witnesses[i]) || TypeFacts.TypeofNEHostObject; - } - // Remove inequalities for types that appear in the - // active clause because they appear before other - // types collected so far. - for (let i = start; i < end; i++) { - facts &= ~(typeofNEFacts.get(witnesses[i]) || 0); - } - // Add inequalities for types before the active clause unconditionally. - for (let i = 0; i < start; i++) { - facts |= typeofNEFacts.get(witnesses[i]) || TypeFacts.TypeofNEHostObject; - } - } - // When in an active clause without default the set of - // equalities is finite. + function narrowBySwitchOnTypeOf(type: ts.Type, switchStatement: SwitchStatement, clauseStart: number, clauseEnd: number): ts.Type { + const switchWitnesses = getSwitchClauseTypeOfWitnesses(switchStatement); + if (!switchWitnesses.length) { + return type; + } + // Equal start and end denotes implicit fallthrough; undefined marks explicit default clause + const defaultCaseLocation = findIndex(switchWitnesses, elem => elem === undefined); + const hasDefaultClause = clauseStart === clauseEnd || (defaultCaseLocation >= clauseStart && defaultCaseLocation < clauseEnd); + let clauseWitnesses: string[]; + let switchFacts: TypeFacts; + if (defaultCaseLocation > -1) { + // We no longer need the undefined denoting an + // explicit default case. Remove the undefined and + // fix-up clauseStart and clauseEnd. This means + // that we don't have to worry about undefined + // in the witness array. + const witnesses = switchWitnesses.filter(witness => witness !== undefined); + // The adjusted clause start and end after removing the `default` statement. + const fixedClauseStart = defaultCaseLocation < clauseStart ? clauseStart - 1 : clauseStart; + const fixedClauseEnd = defaultCaseLocation < clauseEnd ? clauseEnd - 1 : clauseEnd; + clauseWitnesses = witnesses.slice(fixedClauseStart, fixedClauseEnd); + switchFacts = getFactsFromTypeofSwitch(fixedClauseStart, fixedClauseEnd, witnesses, hasDefaultClause); + } else { - // Add equalities for all types in the active clause. - for (let i = start; i < end; i++) { - facts |= typeofEQFacts.get(witnesses[i]) || TypeFacts.TypeofEQHostObject; - } - // Remove equalities for types that appear before the - // active clause. - for (let i = 0; i < start; i++) { - facts &= ~(typeofEQFacts.get(witnesses[i]) || 0); + clauseWitnesses = switchWitnesses.slice(clauseStart, clauseEnd); + switchFacts = getFactsFromTypeofSwitch(clauseStart, clauseEnd, switchWitnesses, hasDefaultClause); + } + if (hasDefaultClause) { + return filterType(type, t => (getTypeFacts(t) & switchFacts) === switchFacts); + } + /* + The implied type is the raw type suggested by a + value being caught in this clause. + + When the clause contains a default case we ignore + the implied type and try to narrow using any facts + we can learn: see `switchFacts`. + + Example: + switch (typeof x) { + case 'number': + case 'string': break; + default: break; + case 'number': + case 'boolean': break + } + + In the first clause (case `number` and `string`) the + implied type is number | string. + + In the default clause we de not compute an implied type. + + In the third clause (case `number` and `boolean`) + the naive implied type is number | boolean, however + we use the type facts to narrow the implied type to + boolean. We know that number cannot be selected + because it is caught in the first clause. + */ + let impliedType = getTypeWithFacts(getUnionType(clauseWitnesses.map(text => getImpliedTypeFromTypeofCase(type, text))), switchFacts); + if (impliedType.flags & TypeFlags.Union) { + impliedType = getAssignmentReducedType((impliedType as UnionType), getBaseConstraintOrType(type)); + } + return getTypeWithFacts(mapType(type, narrowTypeForTypeofSwitch(impliedType)), switchFacts); + } + function narrowTypeByInstanceof(type: ts.Type, expr: BinaryExpression, assumeTrue: boolean): ts.Type { + const left = getReferenceCandidate(expr.left); + if (!isMatchingReference(reference, left)) { + if (assumeTrue && strictNullChecks && optionalChainContainsReference(left, reference)) { + return getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull); + } + // For a reference of the form 'x.y', an 'x instanceof T' type guard resets the + // narrowed type of 'y' to its declared type. We do this because preceding 'x.y' + // references might reference a different 'y' property. However, we make an exception + // for property accesses where x is a synthetic 'this' expression, indicating that we + // were called from isPropertyInitializedInConstructor. Without this exception, + // initializations of 'this' properties that occur before a 'this instanceof XXX' + // check would not be considered. + if (containsMatchingReference(reference, left) && !isSyntheticThisPropertyAccess(reference)) { + return declaredType; } + return type; } - return facts; - } - - function isExhaustiveSwitchStatement(node: SwitchStatement): boolean { - const links = getNodeLinks(node); - return links.isExhaustive !== undefined ? links.isExhaustive : (links.isExhaustive = computeExhaustiveSwitchStatement(node)); - } - - function computeExhaustiveSwitchStatement(node: SwitchStatement): boolean { - if (node.expression.kind === SyntaxKind.TypeOfExpression) { - const operandType = getTypeOfExpression((node.expression as TypeOfExpression).expression); - // This cast is safe because the switch is possibly exhaustive and does not contain a default case, so there can be no undefined. - const witnesses = getSwitchClauseTypeOfWitnesses(node); - // notEqualFacts states that the type of the switched value is not equal to every type in the switch. - const notEqualFacts = getFactsFromTypeofSwitch(0, 0, witnesses, /*hasDefault*/ true); - const type = getBaseConstraintOfType(operandType) || operandType; - return !!(filterType(type, t => (getTypeFacts(t) & notEqualFacts) === notEqualFacts).flags & TypeFlags.Never); - } - const type = getTypeOfExpression(node.expression); - if (!isLiteralType(type)) { - return false; - } - const switchTypes = getSwitchClauseTypes(node); - if (!switchTypes.length || some(switchTypes, isNeitherUnitTypeNorNever)) { - return false; + // Check that right operand is a function type with a prototype property + const rightType = getTypeOfExpression(expr.right); + if (!isTypeDerivedFrom(rightType, globalFunctionType)) { + return type; } - return eachTypeContainedIn(mapType(type, getRegularTypeOfLiteralType), switchTypes); - } - - function functionHasImplicitReturn(func: FunctionLikeDeclaration) { - return func.endFlowNode && isReachableFlowNode(func.endFlowNode); - } - - /** NOTE: Return value of `[]` means a different thing than `undefined`. `[]` means func returns `void`, `undefined` means it returns `never`. */ - function checkAndAggregateReturnExpressionTypes(func: FunctionLikeDeclaration, checkMode: CheckMode | undefined): Type[] | undefined { - const functionFlags = getFunctionFlags(func); - const aggregatedTypes: Type[] = []; - let hasReturnWithNoExpression = functionHasImplicitReturn(func); - let hasReturnOfTypeNever = false; - forEachReturnStatement(func.body, returnStatement => { - const expr = returnStatement.expression; - if (expr) { - let type = checkExpressionCached(expr, checkMode && checkMode & ~CheckMode.SkipGenericFunctions); - if (functionFlags & FunctionFlags.Async) { - // From within an async function you can return either a non-promise value or a promise. Any - // Promise/A+ compatible implementation will always assimilate any foreign promise, so the - // return type of the body should be unwrapped to its awaited type, which should be wrapped in - // the native Promise type by the caller. - type = checkAwaitedType(type, func, Diagnostics.The_return_type_of_an_async_function_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member); - } - if (type.flags & TypeFlags.Never) { - hasReturnOfTypeNever = true; - } - pushIfUnique(aggregatedTypes, type); - } - else { - hasReturnWithNoExpression = true; + let targetType: ts.Type | undefined; + const prototypeProperty = getPropertyOfType(rightType, ("prototype" as __String)); + if (prototypeProperty) { + // Target type is type of the prototype property + const prototypePropertyType = getTypeOfSymbol(prototypeProperty); + if (!isTypeAny(prototypePropertyType)) { + targetType = prototypePropertyType; } - }); - if (aggregatedTypes.length === 0 && !hasReturnWithNoExpression && (hasReturnOfTypeNever || mayReturnNever(func))) { - return undefined; } - if (strictNullChecks && aggregatedTypes.length && hasReturnWithNoExpression && - !(isJSConstructor(func) && aggregatedTypes.some(t => t.symbol === func.symbol))) { - // Javascript "callable constructors", containing eg `if (!(this instanceof A)) return new A()` should not add undefined - pushIfUnique(aggregatedTypes, undefinedType); + // Don't narrow from 'any' if the target type is exactly 'Object' or 'Function' + if (isTypeAny(type) && (targetType === globalObjectType || targetType === globalFunctionType)) { + return type; } - return aggregatedTypes; - } - function mayReturnNever(func: FunctionLikeDeclaration): boolean { - switch (func.kind) { - case SyntaxKind.FunctionExpression: - case SyntaxKind.ArrowFunction: - return true; - case SyntaxKind.MethodDeclaration: - return func.parent.kind === SyntaxKind.ObjectLiteralExpression; - default: - return false; + if (!targetType) { + const constructSignatures = getSignaturesOfType(rightType, SignatureKind.Construct); + targetType = constructSignatures.length ? + getUnionType(map(constructSignatures, signature => getReturnTypeOfSignature(getErasedSignature(signature)))) : + emptyObjectType; } + return getNarrowedType(type, targetType, assumeTrue, isTypeDerivedFrom); } - - /** - * TypeScript Specification 1.0 (6.3) - July 2014 - * An explicitly typed function whose return type isn't the Void type, - * the Any type, or a union type containing the Void or Any type as a constituent - * must have at least one return statement somewhere in its body. - * An exception to this rule is if the function implementation consists of a single 'throw' statement. - * - * @param returnType - return type of the function, can be undefined if return type is not explicitly specified - */ - function checkAllCodePathsInNonVoidFunctionReturnOrThrow(func: FunctionLikeDeclaration | MethodSignature, returnType: Type | undefined): void { - if (!produceDiagnostics) { - return; - } - - const functionFlags = getFunctionFlags(func); - const type = returnType && getReturnOrPromisedType(returnType, functionFlags); - - // Functions with with an explicitly specified 'void' or 'any' return type don't need any return expressions. - if (type && maybeTypeOfKind(type, TypeFlags.Any | TypeFlags.Void)) { - return; - } - - // If all we have is a function signature, or an arrow function with an expression body, then there is nothing to check. - // also if HasImplicitReturn flag is not set this means that all codepaths in function body end with return or throw - if (func.kind === SyntaxKind.MethodSignature || nodeIsMissing(func.body) || func.body!.kind !== SyntaxKind.Block || !functionHasImplicitReturn(func)) { - return; - } - - const hasExplicitReturn = func.flags & NodeFlags.HasExplicitReturn; - - if (type && type.flags & TypeFlags.Never) { - error(getEffectiveReturnTypeNode(func), Diagnostics.A_function_returning_never_cannot_have_a_reachable_end_point); - } - else if (type && !hasExplicitReturn) { - // minimal check: function has syntactic return type annotation and no explicit return statements in the body - // this function does not conform to the specification. - // NOTE: having returnType !== undefined is a precondition for entering this branch so func.type will always be present - error(getEffectiveReturnTypeNode(func), Diagnostics.A_function_whose_declared_type_is_neither_void_nor_any_must_return_a_value); + function getNarrowedType(type: ts.Type, candidate: ts.Type, assumeTrue: boolean, isRelated: (source: ts.Type, target: ts.Type) => boolean) { + if (!assumeTrue) { + return filterType(type, t => !isRelated(t, candidate)); } - else if (type && strictNullChecks && !isTypeAssignableTo(undefinedType, type)) { - error(getEffectiveReturnTypeNode(func) || func, Diagnostics.Function_lacks_ending_return_statement_and_return_type_does_not_include_undefined); + // If the current type is a union type, remove all constituents that couldn't be instances of + // the candidate type. If one or more constituents remain, return a union of those. + if (type.flags & TypeFlags.Union) { + const assignableType = filterType(type, t => isRelated(t, candidate)); + if (!(assignableType.flags & TypeFlags.Never)) { + return assignableType; + } + } + // If the candidate type is a subtype of the target type, narrow to the candidate type. + // Otherwise, if the target type is assignable to the candidate type, keep the target type. + // Otherwise, if the candidate type is assignable to the target type, narrow to the candidate + // type. Otherwise, the types are completely unrelated, so narrow to an intersection of the + // two types. + return isTypeSubtypeOf(candidate, type) ? candidate : + isTypeAssignableTo(type, candidate) ? type : + isTypeAssignableTo(candidate, type) ? candidate : + getIntersectionType([type, candidate]); + } + function narrowTypeByCallExpression(type: ts.Type, callExpression: CallExpression, assumeTrue: boolean): ts.Type { + if (hasMatchingArgument(callExpression, reference)) { + const signature = assumeTrue || !isCallChain(callExpression) ? getEffectsSignature(callExpression) : undefined; + const predicate = signature && getTypePredicateOfSignature(signature); + if (predicate && (predicate.kind === TypePredicateKind.This || predicate.kind === TypePredicateKind.Identifier)) { + return narrowTypeByTypePredicate(type, predicate, callExpression, assumeTrue); + } } - else if (compilerOptions.noImplicitReturns) { - if (!type) { - // If return type annotation is omitted check if function has any explicit return statements. - // If it does not have any - its inferred return type is void - don't do any checks. - // Otherwise get inferred return type from function body and report error only if it is not void / anytype - if (!hasExplicitReturn) { - return; + return type; + } + function narrowTypeByTypePredicate(type: ts.Type, predicate: TypePredicate, callExpression: CallExpression, assumeTrue: boolean): ts.Type { + // Don't narrow from 'any' if the predicate type is exactly 'Object' or 'Function' + if (predicate.type && !(isTypeAny(type) && (predicate.type === globalObjectType || predicate.type === globalFunctionType))) { + const predicateArgument = getTypePredicateArgument(predicate, callExpression); + if (predicateArgument) { + if (isMatchingReference(reference, predicateArgument)) { + return getNarrowedType(type, predicate.type, assumeTrue, isTypeSubtypeOf); } - const inferredReturnType = getReturnTypeOfSignature(getSignatureFromDeclaration(func)); - if (isUnwrappedReturnTypeVoidOrAny(func, inferredReturnType)) { - return; + if (strictNullChecks && assumeTrue && optionalChainContainsReference(predicateArgument, reference) && + !(getTypeFacts(predicate.type) & TypeFacts.EQUndefined)) { + return getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull); + } + if (containsMatchingReference(reference, predicateArgument)) { + return declaredType; } } - error(getEffectiveReturnTypeNode(func) || func, Diagnostics.Not_all_code_paths_return_a_value); } + return type; } - - function checkFunctionExpressionOrObjectLiteralMethod(node: FunctionExpression | ArrowFunction | MethodDeclaration, checkMode?: CheckMode): Type { - Debug.assert(node.kind !== SyntaxKind.MethodDeclaration || isObjectLiteralMethod(node)); - checkNodeDeferred(node); - - // The identityMapper object is used to indicate that function expressions are wildcards - if (checkMode && checkMode & CheckMode.SkipContextSensitive && isContextSensitive(node)) { - // Skip parameters, return signature with return type that retains noncontextual parts so inferences can still be drawn in an early stage - if (!getEffectiveReturnTypeNode(node) && !hasContextSensitiveParameters(node)) { - // Return plain anyFunctionType if there is no possibility we'll make inferences from the return type - const contextualSignature = getContextualSignature(node); - if (contextualSignature && couldContainTypeVariables(getReturnTypeOfSignature(contextualSignature))) { - const links = getNodeLinks(node); - if (links.contextFreeType) { - return links.contextFreeType; - } - const returnType = getReturnTypeFromBody(node, checkMode); - const returnOnlySignature = createSignature(undefined, undefined, undefined, emptyArray, returnType, /*resolvedTypePredicate*/ undefined, 0, SignatureFlags.None); - const returnOnlyType = createAnonymousType(node.symbol, emptySymbols, [returnOnlySignature], emptyArray, undefined, undefined); - returnOnlyType.objectFlags |= ObjectFlags.NonInferrableType; - return links.contextFreeType = returnOnlyType; + // Narrow the given type based on the given expression having the assumed boolean value. The returned type + // will be a subtype or the same type as the argument. + function narrowType(type: ts.Type, expr: Expression, assumeTrue: boolean): ts.Type { + // for `a?.b`, we emulate a synthetic `a !== null && a !== undefined` condition for `a` + if (isExpressionOfOptionalChainRoot(expr) || + isBinaryExpression(expr.parent) && expr.parent.operatorToken.kind === SyntaxKind.QuestionQuestionToken && expr.parent.left === expr) { + return narrowTypeByOptionality(type, expr, assumeTrue); + } + switch (expr.kind) { + case SyntaxKind.Identifier: + case SyntaxKind.ThisKeyword: + case SyntaxKind.SuperKeyword: + case SyntaxKind.PropertyAccessExpression: + case SyntaxKind.ElementAccessExpression: + return narrowTypeByTruthiness(type, expr, assumeTrue); + case SyntaxKind.CallExpression: + return narrowTypeByCallExpression(type, (expr), assumeTrue); + case SyntaxKind.ParenthesizedExpression: + return narrowType(type, (expr).expression, assumeTrue); + case SyntaxKind.BinaryExpression: + return narrowTypeByBinaryExpression(type, (expr), assumeTrue); + case SyntaxKind.PrefixUnaryExpression: + if ((expr).operator === SyntaxKind.ExclamationToken) { + return narrowType(type, (expr).operand, !assumeTrue); } - } - return anyFunctionType; + break; } - - // Grammar checking - const hasGrammarError = checkGrammarFunctionLikeDeclaration(node); - if (!hasGrammarError && node.kind === SyntaxKind.FunctionExpression) { - checkGrammarForGenerator(node); + return type; + } + function narrowTypeByOptionality(type: ts.Type, expr: Expression, assumePresent: boolean): ts.Type { + if (isMatchingReference(reference, expr)) { + return getTypeWithFacts(type, assumePresent ? TypeFacts.NEUndefinedOrNull : TypeFacts.EQUndefinedOrNull); } - - contextuallyCheckFunctionExpressionOrObjectLiteralMethod(node, checkMode); - - return getTypeOfSymbol(getSymbolOfNode(node)); + if (isMatchingReferenceDiscriminant(expr, declaredType)) { + return narrowTypeByDiscriminant(type, (expr), t => getTypeWithFacts(t, assumePresent ? TypeFacts.NEUndefinedOrNull : TypeFacts.EQUndefinedOrNull)); + } + return type; } - - function contextuallyCheckFunctionExpressionOrObjectLiteralMethod(node: FunctionExpression | ArrowFunction | MethodDeclaration, checkMode?: CheckMode) { - const links = getNodeLinks(node); - // Check if function expression is contextually typed and assign parameter types if so. - if (!(links.flags & NodeCheckFlags.ContextChecked)) { - const contextualSignature = getContextualSignature(node); - // If a type check is started at a function expression that is an argument of a function call, obtaining the - // contextual type may recursively get back to here during overload resolution of the call. If so, we will have - // already assigned contextual types. - if (!(links.flags & NodeCheckFlags.ContextChecked)) { - links.flags |= NodeCheckFlags.ContextChecked; - const signature = firstOrUndefined(getSignaturesOfType(getTypeOfSymbol(getSymbolOfNode(node)), SignatureKind.Call)); - if (!signature) { - return; - } - if (isContextSensitive(node)) { - if (contextualSignature) { - const inferenceContext = getInferenceContext(node); - if (checkMode && checkMode & CheckMode.Inferential) { - inferFromAnnotatedParameters(signature, contextualSignature, inferenceContext!); - } - const instantiatedContextualSignature = inferenceContext ? - instantiateSignature(contextualSignature, inferenceContext.mapper) : contextualSignature; - assignContextualParameterTypes(signature, instantiatedContextualSignature); - } - else { - // Force resolution of all parameter types such that the absence of a contextual type is consistently reflected. - assignNonContextualParameterTypes(signature); - } - } - if (contextualSignature && !getReturnTypeFromAnnotation(node) && !signature.resolvedReturnType) { - const returnType = getReturnTypeFromBody(node, checkMode); - if (!signature.resolvedReturnType) { - signature.resolvedReturnType = returnType; - } - } - checkSignatureDeclaration(node); + } + function getTypeOfSymbolAtLocation(symbol: ts.Symbol, location: Node) { + symbol = symbol.exportSymbol || symbol; + // If we have an identifier or a property access at the given location, if the location is + // an dotted name expression, and if the location is not an assignment target, obtain the type + // of the expression (which will reflect control flow analysis). If the expression indeed + // resolved to the given symbol, return the narrowed type. + if (location.kind === SyntaxKind.Identifier) { + if (isRightSideOfQualifiedNameOrPropertyAccess(location)) { + location = location.parent; + } + if (isExpressionNode(location) && !isAssignmentTarget(location)) { + const type = getTypeOfExpression((location)); + if (getExportSymbolOfValueSymbolIfExported(getNodeLinks(location).resolvedSymbol) === symbol) { + return type; } } } - - function getReturnOrPromisedType(type: Type | undefined, functionFlags: FunctionFlags) { - const isGenerator = !!(functionFlags & FunctionFlags.Generator); - const isAsync = !!(functionFlags & FunctionFlags.Async); - return type && isGenerator ? getIterationTypeOfGeneratorFunctionReturnType(IterationTypeKind.Return, type, isAsync) || errorType : - type && isAsync ? getAwaitedType(type) || errorType : - type; + // The location isn't a reference to the given symbol, meaning we're being asked + // a hypothetical question of what type the symbol would have if there was a reference + // to it at the given location. Since we have no control flow information for the + // hypothetical reference (control flow information is created and attached by the + // binder), we simply return the declared type of the symbol. + return getTypeOfSymbol(symbol); + } + function getControlFlowContainer(node: Node): Node { + return findAncestor(node.parent, node => isFunctionLike(node) && !getImmediatelyInvokedFunctionExpression(node) || + node.kind === SyntaxKind.ModuleBlock || + node.kind === SyntaxKind.SourceFile || + node.kind === SyntaxKind.PropertyDeclaration)!; + } + // Check if a parameter is assigned anywhere within its declaring function. + function isParameterAssigned(symbol: ts.Symbol) { + const func = (getRootDeclaration(symbol.valueDeclaration).parent); + const links = getNodeLinks(func); + if (!(links.flags & NodeCheckFlags.AssignmentsMarked)) { + links.flags |= NodeCheckFlags.AssignmentsMarked; + if (!hasParentWithAssignmentsMarked(func)) { + markParameterAssignments(func); + } } - - function checkFunctionExpressionOrObjectLiteralMethodDeferred(node: ArrowFunction | FunctionExpression | MethodDeclaration) { - Debug.assert(node.kind !== SyntaxKind.MethodDeclaration || isObjectLiteralMethod(node)); - - const functionFlags = getFunctionFlags(node); - const returnType = getReturnTypeFromAnnotation(node); - checkAllCodePathsInNonVoidFunctionReturnOrThrow(node, returnType); - - if (node.body) { - if (!getEffectiveReturnTypeNode(node)) { - // There are some checks that are only performed in getReturnTypeFromBody, that may produce errors - // we need. An example is the noImplicitAny errors resulting from widening the return expression - // of a function. Because checking of function expression bodies is deferred, there was never an - // appropriate time to do this during the main walk of the file (see the comment at the top of - // checkFunctionExpressionBodies). So it must be done now. - getReturnTypeOfSignature(getSignatureFromDeclaration(node)); - } - - if (node.body.kind === SyntaxKind.Block) { - checkSourceElement(node.body); - } - else { - // From within an async function you can return either a non-promise value or a promise. Any - // Promise/A+ compatible implementation will always assimilate any foreign promise, so we - // should not be checking assignability of a promise to the return type. Instead, we need to - // check assignability of the awaited type of the expression body against the promised type of - // its return type annotation. - const exprType = checkExpression(node.body); - const returnOrPromisedType = getReturnOrPromisedType(returnType, functionFlags); - if (returnOrPromisedType) { - if ((functionFlags & FunctionFlags.AsyncGenerator) === FunctionFlags.Async) { // Async function - const awaitedType = checkAwaitedType(exprType, node.body, Diagnostics.The_return_type_of_an_async_function_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member); - checkTypeAssignableToAndOptionallyElaborate(awaitedType, returnOrPromisedType, node.body, node.body); - } - else { // Normal function - checkTypeAssignableToAndOptionallyElaborate(exprType, returnOrPromisedType, node.body, node.body); - } - } + return symbol.isAssigned || false; + } + function hasParentWithAssignmentsMarked(node: Node) { + return !!findAncestor(node.parent, node => isFunctionLike(node) && !!(getNodeLinks(node).flags & NodeCheckFlags.AssignmentsMarked)); + } + function markParameterAssignments(node: Node) { + if (node.kind === SyntaxKind.Identifier) { + if (isAssignmentTarget(node)) { + const symbol = getResolvedSymbol((node)); + if (symbol.valueDeclaration && getRootDeclaration(symbol.valueDeclaration).kind === SyntaxKind.Parameter) { + symbol.isAssigned = true; } } } - - function checkArithmeticOperandType(operand: Node, type: Type, diagnostic: DiagnosticMessage, isAwaitValid = false): boolean { - if (!isTypeAssignableTo(type, numberOrBigIntType)) { - const awaitedType = isAwaitValid && getAwaitedTypeOfPromise(type); - errorAndMaybeSuggestAwait( - operand, - !!awaitedType && isTypeAssignableTo(awaitedType, numberOrBigIntType), - diagnostic); - return false; - } - return true; + else { + forEachChild(node, markParameterAssignments); } - - function isReadonlyAssignmentDeclaration(d: Declaration) { - if (!isCallExpression(d)) { - return false; + } + function isConstVariable(symbol: ts.Symbol) { + return symbol.flags & SymbolFlags.Variable && (getDeclarationNodeFlagsFromSymbol(symbol) & NodeFlags.Const) !== 0 && getTypeOfSymbol(symbol) !== autoArrayType; + } + /** remove undefined from the annotated type of a parameter when there is an initializer (that doesn't include undefined) */ + function removeOptionalityFromDeclaredType(declaredType: ts.Type, declaration: VariableLikeDeclaration): ts.Type { + if (pushTypeResolution(declaration.symbol, TypeSystemPropertyName.DeclaredType)) { + const annotationIncludesUndefined = strictNullChecks && + declaration.kind === SyntaxKind.Parameter && + declaration.initializer && + getFalsyFlags(declaredType) & TypeFlags.Undefined && + !(getFalsyFlags(checkExpression(declaration.initializer)) & TypeFlags.Undefined); + popTypeResolution(); + return annotationIncludesUndefined ? getTypeWithFacts(declaredType, TypeFacts.NEUndefined) : declaredType; + } + else { + reportCircularityError(declaration.symbol); + return declaredType; + } + } + function isConstraintPosition(node: Node) { + const parent = node.parent; + return parent.kind === SyntaxKind.PropertyAccessExpression || + parent.kind === SyntaxKind.CallExpression && (parent).expression === node || + parent.kind === SyntaxKind.ElementAccessExpression && (parent).expression === node || + parent.kind === SyntaxKind.BindingElement && (parent).name === node && !!(parent).initializer; + } + function typeHasNullableConstraint(type: ts.Type) { + return type.flags & TypeFlags.InstantiableNonPrimitive && maybeTypeOfKind(getBaseConstraintOfType(type) || unknownType, TypeFlags.Nullable); + } + function getConstraintForLocation(type: ts.Type, node: Node): ts.Type; + function getConstraintForLocation(type: ts.Type | undefined, node: Node): ts.Type | undefined; + function getConstraintForLocation(type: ts.Type, node: Node): ts.Type | undefined { + // When a node is the left hand expression of a property access, element access, or call expression, + // and the type of the node includes type variables with constraints that are nullable, we fetch the + // apparent type of the node *before* performing control flow analysis such that narrowings apply to + // the constraint type. + if (type && isConstraintPosition(node) && forEachType(type, typeHasNullableConstraint)) { + return mapType(getWidenedType(type), getBaseConstraintOrType); + } + return type; + } + function isExportOrExportExpression(location: Node) { + return !!findAncestor(location, e => e.parent && isExportAssignment(e.parent) && e.parent.expression === e && isEntityNameExpression(e)); + } + function markAliasReferenced(symbol: ts.Symbol, location: Node) { + if (isNonLocalAlias(symbol, /*excludes*/ SymbolFlags.Value) && !isInTypeQuery(location) && !getTypeOnlyAliasDeclaration(symbol)) { + if (compilerOptions.preserveConstEnums && isExportOrExportExpression(location) || !isConstEnumOrConstEnumOnlyModule(resolveAlias(symbol))) { + markAliasSymbolAsReferenced(symbol); } - if (!isBindableObjectDefinePropertyCall(d)) { - return false; + else { + markConstEnumAliasAsReferenced(symbol); } - const objectLitType = checkExpressionCached(d.arguments[2]); - const valueType = getTypeOfPropertyOfType(objectLitType, "value" as __String); - if (valueType) { - const writableProp = getPropertyOfType(objectLitType, "writable" as __String); - const writableType = writableProp && getTypeOfSymbol(writableProp); - if (!writableType || writableType === falseType || writableType === regularFalseType) { - return true; + } + } + function checkIdentifier(node: Identifier): ts.Type { + const symbol = getResolvedSymbol(node); + if (symbol === unknownSymbol) { + return errorType; + } + // As noted in ECMAScript 6 language spec, arrow functions never have an arguments objects. + // Although in down-level emit of arrow function, we emit it using function expression which means that + // arguments objects will be bound to the inner object; emitting arrow function natively in ES6, arguments objects + // will be bound to non-arrow function that contain this arrow function. This results in inconsistent behavior. + // To avoid that we will give an error to users if they use arguments objects in arrow function so that they + // can explicitly bound arguments objects + if (symbol === argumentsSymbol) { + const container = (getContainingFunction(node)!); + if (languageVersion < ScriptTarget.ES2015) { + if (container.kind === SyntaxKind.ArrowFunction) { + error(node, Diagnostics.The_arguments_object_cannot_be_referenced_in_an_arrow_function_in_ES3_and_ES5_Consider_using_a_standard_function_expression); } - // We include this definition whereupon we walk back and check the type at the declaration because - // The usual definition of `Object.defineProperty` will _not_ cause literal types to be preserved in the - // argument types, should the type be contextualized by the call itself. - if (writableProp && writableProp.valueDeclaration && isPropertyAssignment(writableProp.valueDeclaration)) { - const initializer = writableProp.valueDeclaration.initializer; - const rawOriginalType = checkExpression(initializer); - if (rawOriginalType === falseType || rawOriginalType === regularFalseType) { - return true; - } + else if (hasModifier(container, ModifierFlags.Async)) { + error(node, Diagnostics.The_arguments_object_cannot_be_referenced_in_an_async_function_or_method_in_ES3_and_ES5_Consider_using_a_standard_function_or_method); } - return false; } - const setProp = getPropertyOfType(objectLitType, "set" as __String); - return !setProp; - } - - function isReadonlySymbol(symbol: Symbol): boolean { - // The following symbols are considered read-only: - // Properties with a 'readonly' modifier - // Variables declared with 'const' - // Get accessors without matching set accessors - // Enum members - // Object.defineProperty assignments with writable false or no setter - // Unions and intersections of the above (unions and intersections eagerly set isReadonly on creation) - return !!(getCheckFlags(symbol) & CheckFlags.Readonly || - symbol.flags & SymbolFlags.Property && getDeclarationModifierFlagsFromSymbol(symbol) & ModifierFlags.Readonly || - symbol.flags & SymbolFlags.Variable && getDeclarationNodeFlagsFromSymbol(symbol) & NodeFlags.Const || - symbol.flags & SymbolFlags.Accessor && !(symbol.flags & SymbolFlags.SetAccessor) || - symbol.flags & SymbolFlags.EnumMember || - some(symbol.declarations, isReadonlyAssignmentDeclaration) - ); + getNodeLinks(container).flags |= NodeCheckFlags.CaptureArguments; + return getTypeOfSymbol(symbol); } - - function isAssignmentToReadonlyEntity(expr: Expression, symbol: Symbol, assignmentKind: AssignmentKind) { - if (assignmentKind === AssignmentKind.None) { - // no assigment means it doesn't matter whether the entity is readonly - return false; - } - if (isReadonlySymbol(symbol)) { - // Allow assignments to readonly properties within constructors of the same class declaration. - if (symbol.flags & SymbolFlags.Property && - isAccessExpression(expr) && - expr.expression.kind === SyntaxKind.ThisKeyword) { - // Look for if this is the constructor for the class that `symbol` is a property of. - const ctor = getContainingFunction(expr); - if (!(ctor && ctor.kind === SyntaxKind.Constructor)) { - return true; - } - if (symbol.valueDeclaration) { - const isAssignmentDeclaration = isBinaryExpression(symbol.valueDeclaration); - const isLocalPropertyDeclaration = ctor.parent === symbol.valueDeclaration.parent; - const isLocalParameterProperty = ctor === symbol.valueDeclaration.parent; - const isLocalThisPropertyAssignment = isAssignmentDeclaration && symbol.parent?.valueDeclaration === ctor.parent; - const isLocalThisPropertyAssignmentConstructorFunction = isAssignmentDeclaration && symbol.parent?.valueDeclaration === ctor; - const isWriteableSymbol = - isLocalPropertyDeclaration - || isLocalParameterProperty - || isLocalThisPropertyAssignment - || isLocalThisPropertyAssignmentConstructorFunction; - return !isWriteableSymbol; + // We should only mark aliases as referenced if there isn't a local value declaration + // for the symbol. Also, don't mark any property access expression LHS - checkPropertyAccessExpression will handle that + if (!(node.parent && isPropertyAccessExpression(node.parent) && node.parent.expression === node)) { + markAliasReferenced(symbol, node); + } + const localOrExportSymbol = getExportSymbolOfValueSymbolIfExported(symbol); + let declaration: Declaration | undefined = localOrExportSymbol.valueDeclaration; + if (localOrExportSymbol.flags & SymbolFlags.Class) { + // Due to the emit for class decorators, any reference to the class from inside of the class body + // must instead be rewritten to point to a temporary variable to avoid issues with the double-bind + // behavior of class names in ES6. + if (declaration.kind === SyntaxKind.ClassDeclaration + && nodeIsDecorated((declaration as ClassDeclaration))) { + let container = getContainingClass(node); + while (container !== undefined) { + if (container === declaration && container.name !== node) { + getNodeLinks(declaration).flags |= NodeCheckFlags.ClassWithConstructorReference; + getNodeLinks(node).flags |= NodeCheckFlags.ConstructorReferenceInClass; + break; } + container = getContainingClass(container); } - return true; } - if (isAccessExpression(expr)) { - // references through namespace import should be readonly - const node = skipParentheses(expr.expression); - if (node.kind === SyntaxKind.Identifier) { - const symbol = getNodeLinks(node).resolvedSymbol!; - if (symbol.flags & SymbolFlags.Alias) { - const declaration = getDeclarationOfAliasSymbol(symbol); - return !!declaration && declaration.kind === SyntaxKind.NamespaceImport; + else if (declaration.kind === SyntaxKind.ClassExpression) { + // When we emit a class expression with static members that contain a reference + // to the constructor in the initializer, we will need to substitute that + // binding with an alias as the class name is not in scope. + let container = getThisContainer(node, /*includeArrowFunctions*/ false); + while (container.kind !== SyntaxKind.SourceFile) { + if (container.parent === declaration) { + if (container.kind === SyntaxKind.PropertyDeclaration && hasModifier(container, ModifierFlags.Static)) { + getNodeLinks(declaration).flags |= NodeCheckFlags.ClassWithConstructorReference; + getNodeLinks(node).flags |= NodeCheckFlags.ConstructorReferenceInClass; + } + break; } + container = getThisContainer(container, /*includeArrowFunctions*/ false); } } - return false; } - - function checkReferenceExpression(expr: Expression, invalidReferenceMessage: DiagnosticMessage, invalidOptionalChainMessage: DiagnosticMessage): boolean { - // References are combinations of identifiers, parentheses, and property accesses. - const node = skipOuterExpressions(expr, OuterExpressionKinds.Assertions | OuterExpressionKinds.Parentheses); - if (node.kind !== SyntaxKind.Identifier && !isAccessExpression(node)) { - error(expr, invalidReferenceMessage); - return false; + checkNestedBlockScopedBinding(node, symbol); + const type = getConstraintForLocation(getTypeOfSymbol(localOrExportSymbol), node); + const assignmentKind = getAssignmentTargetKind(node); + if (assignmentKind) { + if (!(localOrExportSymbol.flags & SymbolFlags.Variable) && + !(isInJSFile(node) && localOrExportSymbol.flags & SymbolFlags.ValueModule)) { + error(node, Diagnostics.Cannot_assign_to_0_because_it_is_not_a_variable, symbolToString(symbol)); + return errorType; } - if (node.flags & NodeFlags.OptionalChain) { - error(expr, invalidOptionalChainMessage); - return false; + if (isReadonlySymbol(localOrExportSymbol)) { + if (localOrExportSymbol.flags & SymbolFlags.Variable) { + error(node, Diagnostics.Cannot_assign_to_0_because_it_is_a_constant, symbolToString(symbol)); + } + else { + error(node, Diagnostics.Cannot_assign_to_0_because_it_is_a_read_only_property, symbolToString(symbol)); + } + return errorType; } - return true; } - - function checkDeleteExpression(node: DeleteExpression): Type { - checkExpression(node.expression); - const expr = skipParentheses(node.expression); - if (!isAccessExpression(expr)) { - error(expr, Diagnostics.The_operand_of_a_delete_operator_must_be_a_property_reference); - return booleanType; - } - if (expr.kind === SyntaxKind.PropertyAccessExpression && isPrivateIdentifier((expr as PropertyAccessExpression).name)) { - error(expr, Diagnostics.The_operand_of_a_delete_operator_cannot_be_a_private_identifier); - } - const links = getNodeLinks(expr); - const symbol = getExportSymbolOfValueSymbolIfExported(links.resolvedSymbol); - if (symbol && isReadonlySymbol(symbol)) { - error(expr, Diagnostics.The_operand_of_a_delete_operator_cannot_be_a_read_only_property); + const isAlias = localOrExportSymbol.flags & SymbolFlags.Alias; + // We only narrow variables and parameters occurring in a non-assignment position. For all other + // entities we simply return the declared type. + if (localOrExportSymbol.flags & SymbolFlags.Variable) { + if (assignmentKind === AssignmentKind.Definite) { + return type; } - return booleanType; } - - function checkTypeOfExpression(node: TypeOfExpression): Type { - checkExpression(node.expression); - return typeofType; + else if (isAlias) { + declaration = find(symbol.declarations, isSomeImportDeclaration); } - - function checkVoidExpression(node: VoidExpression): Type { - checkExpression(node.expression); - return undefinedWideningType; + else { + return type; } - - function isTopLevelAwait(node: AwaitExpression) { - const container = getThisContainer(node, /*includeArrowFunctions*/ true); - return isSourceFile(container); + if (!declaration) { + return type; } - - function checkAwaitExpression(node: AwaitExpression): Type { - // Grammar checking - if (produceDiagnostics) { - if (!(node.flags & NodeFlags.AwaitContext)) { - if (isTopLevelAwait(node)) { - const sourceFile = getSourceFileOfNode(node); - if (!hasParseDiagnostics(sourceFile)) { - let span: TextSpan | undefined; - if (!isEffectiveExternalModule(sourceFile, compilerOptions)) { - if (!span) span = getSpanOfTokenAtPosition(sourceFile, node.pos); - const diagnostic = createFileDiagnostic(sourceFile, span.start, span.length, - Diagnostics.await_expressions_are_only_allowed_at_the_top_level_of_a_file_when_that_file_is_a_module_but_this_file_has_no_imports_or_exports_Consider_adding_an_empty_export_to_make_this_file_a_module); - diagnostics.add(diagnostic); - } - if ((moduleKind !== ModuleKind.ESNext && moduleKind !== ModuleKind.System) || languageVersion < ScriptTarget.ES2017) { - span = getSpanOfTokenAtPosition(sourceFile, node.pos); - const diagnostic = createFileDiagnostic(sourceFile, span.start, span.length, - Diagnostics.Top_level_await_expressions_are_only_allowed_when_the_module_option_is_set_to_esnext_or_system_and_the_target_option_is_set_to_es2017_or_higher); - diagnostics.add(diagnostic); - } - } - } - else { - // use of 'await' in non-async function - const sourceFile = getSourceFileOfNode(node); - if (!hasParseDiagnostics(sourceFile)) { - const span = getSpanOfTokenAtPosition(sourceFile, node.pos); - const diagnostic = createFileDiagnostic(sourceFile, span.start, span.length, Diagnostics.await_expressions_are_only_allowed_within_async_functions_and_at_the_top_levels_of_modules); - const func = getContainingFunction(node); - if (func && func.kind !== SyntaxKind.Constructor && (getFunctionFlags(func) & FunctionFlags.Async) === 0) { - const relatedInfo = createDiagnosticForNode(func, Diagnostics.Did_you_mean_to_mark_this_function_as_async); - addRelatedInfo(diagnostic, relatedInfo); + // The declaration container is the innermost function that encloses the declaration of the variable + // or parameter. The flow container is the innermost function starting with which we analyze the control + // flow graph to determine the control flow based type. + const isParameter = getRootDeclaration(declaration).kind === SyntaxKind.Parameter; + const declarationContainer = getControlFlowContainer(declaration); + let flowContainer = getControlFlowContainer(node); + const isOuterVariable = flowContainer !== declarationContainer; + const isSpreadDestructuringAssignmentTarget = node.parent && node.parent.parent && isSpreadAssignment(node.parent) && isDestructuringAssignmentTarget(node.parent.parent); + const isModuleExports = symbol.flags & SymbolFlags.ModuleExports; + // When the control flow originates in a function expression or arrow function and we are referencing + // a const variable or parameter from an outer function, we extend the origin of the control flow + // analysis to include the immediately enclosing function. + while (flowContainer !== declarationContainer && (flowContainer.kind === SyntaxKind.FunctionExpression || + flowContainer.kind === SyntaxKind.ArrowFunction || isObjectLiteralOrClassExpressionMethod(flowContainer)) && + (isConstVariable(localOrExportSymbol) || isParameter && !isParameterAssigned(localOrExportSymbol))) { + flowContainer = getControlFlowContainer(flowContainer); + } + // We only look for uninitialized variables in strict null checking mode, and only when we can analyze + // the entire control flow graph from the variable's declaration (i.e. when the flow container and + // declaration container are the same). + const assumeInitialized = isParameter || isAlias || isOuterVariable || isSpreadDestructuringAssignmentTarget || isModuleExports || isBindingElement(declaration) || + type !== autoType && type !== autoArrayType && (!strictNullChecks || (type.flags & (TypeFlags.AnyOrUnknown | TypeFlags.Void)) !== 0 || + isInTypeQuery(node) || node.parent.kind === SyntaxKind.ExportSpecifier) || + node.parent.kind === SyntaxKind.NonNullExpression || + declaration.kind === SyntaxKind.VariableDeclaration && (declaration).exclamationToken || + declaration.flags & NodeFlags.Ambient; + const initialType = assumeInitialized ? (isParameter ? removeOptionalityFromDeclaredType(type, (declaration as VariableLikeDeclaration)) : type) : + type === autoType || type === autoArrayType ? undefinedType : + getOptionalType(type); + const flowType = getFlowTypeOfReference(node, type, initialType, flowContainer, !assumeInitialized); + // A variable is considered uninitialized when it is possible to analyze the entire control flow graph + // from declaration to use, and when the variable's declared type doesn't include undefined but the + // control flow based type does include undefined. + if (!isEvolvingArrayOperationTarget(node) && (type === autoType || type === autoArrayType)) { + if (flowType === autoType || flowType === autoArrayType) { + if (noImplicitAny) { + error(getNameOfDeclaration(declaration), Diagnostics.Variable_0_implicitly_has_type_1_in_some_locations_where_its_type_cannot_be_determined, symbolToString(symbol), typeToString(flowType)); + error(node, Diagnostics.Variable_0_implicitly_has_an_1_type, symbolToString(symbol), typeToString(flowType)); + } + return convertAutoToAny(flowType); + } + } + else if (!assumeInitialized && !(getFalsyFlags(type) & TypeFlags.Undefined) && getFalsyFlags(flowType) & TypeFlags.Undefined) { + error(node, Diagnostics.Variable_0_is_used_before_being_assigned, symbolToString(symbol)); + // Return the declared type to reduce follow-on errors + return type; + } + return assignmentKind ? getBaseTypeOfLiteralType(flowType) : flowType; + } + function isInsideFunction(node: Node, threshold: Node): boolean { + return !!findAncestor(node, n => n === threshold ? "quit" : isFunctionLike(n)); + } + function getPartOfForStatementContainingNode(node: Node, container: ForStatement) { + return findAncestor(node, n => n === container ? "quit" : n === container.initializer || n === container.condition || n === container.incrementor || n === container.statement); + } + function checkNestedBlockScopedBinding(node: Identifier, symbol: ts.Symbol): void { + if (languageVersion >= ScriptTarget.ES2015 || + (symbol.flags & (SymbolFlags.BlockScopedVariable | SymbolFlags.Class)) === 0 || + isSourceFile(symbol.valueDeclaration) || + symbol.valueDeclaration.parent.kind === SyntaxKind.CatchClause) { + return; + } + // 1. walk from the use site up to the declaration and check + // if there is anything function like between declaration and use-site (is binding/class is captured in function). + // 2. walk from the declaration up to the boundary of lexical environment and check + // if there is an iteration statement in between declaration and boundary (is binding/class declared inside iteration statement) + const container = getEnclosingBlockScopeContainer(symbol.valueDeclaration); + const usedInFunction = isInsideFunction(node.parent, container); + let current = container; + let containedInIterationStatement = false; + while (current && !nodeStartsNewLexicalEnvironment(current)) { + if (isIterationStatement(current, /*lookInLabeledStatements*/ false)) { + containedInIterationStatement = true; + break; + } + current = current.parent; + } + if (containedInIterationStatement) { + if (usedInFunction) { + // mark iteration statement as containing block-scoped binding captured in some function + let capturesBlockScopeBindingInLoopBody = true; + if (isForStatement(container)) { + const varDeclList = getAncestor(symbol.valueDeclaration, SyntaxKind.VariableDeclarationList); + if (varDeclList && varDeclList.parent === container) { + const part = getPartOfForStatementContainingNode(node.parent, container); + if (part) { + const links = getNodeLinks(part); + links.flags |= NodeCheckFlags.ContainsCapturedBlockScopeBinding; + const capturedBindings = links.capturedBlockScopeBindings || (links.capturedBlockScopeBindings = []); + pushIfUnique(capturedBindings, symbol); + if (part === container.initializer) { + capturesBlockScopeBindingInLoopBody = false; // Initializer is outside of loop body } - diagnostics.add(diagnostic); } } } - - if (isInParameterInitializerBeforeContainingFunction(node)) { - error(node, Diagnostics.await_expressions_cannot_be_used_in_a_parameter_initializer); + if (capturesBlockScopeBindingInLoopBody) { + getNodeLinks(current).flags |= NodeCheckFlags.LoopWithCapturedBlockScopedBinding; } } - - const operandType = checkExpression(node.expression); - const awaitedType = checkAwaitedType(operandType, node, Diagnostics.Type_of_await_operand_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member); - if (awaitedType === operandType && awaitedType !== errorType && !(operandType.flags & TypeFlags.AnyOrUnknown)) { - addErrorOrSuggestion(/*isError*/ false, createDiagnosticForNode(node, Diagnostics.await_has_no_effect_on_the_type_of_this_expression)); + // mark variables that are declared in loop initializer and reassigned inside the body of ForStatement. + // if body of ForStatement will be converted to function then we'll need a extra machinery to propagate reassigned values back. + if (isForStatement(container)) { + const varDeclList = getAncestor(symbol.valueDeclaration, SyntaxKind.VariableDeclarationList); + if (varDeclList && varDeclList.parent === container && isAssignedInBodyOfForStatement(node, container)) { + getNodeLinks(symbol.valueDeclaration).flags |= NodeCheckFlags.NeedsLoopOutParameter; + } } - return awaitedType; + // set 'declared inside loop' bit on the block-scoped binding + getNodeLinks(symbol.valueDeclaration).flags |= NodeCheckFlags.BlockScopedBindingInLoop; } - - function checkPrefixUnaryExpression(node: PrefixUnaryExpression): Type { - const operandType = checkExpression(node.operand); - if (operandType === silentNeverType) { - return silentNeverType; - } - switch (node.operand.kind) { - case SyntaxKind.NumericLiteral: - switch (node.operator) { - case SyntaxKind.MinusToken: - return getFreshTypeOfLiteralType(getLiteralType(-(node.operand as NumericLiteral).text)); - case SyntaxKind.PlusToken: - return getFreshTypeOfLiteralType(getLiteralType(+(node.operand as NumericLiteral).text)); - } - break; - case SyntaxKind.BigIntLiteral: - if (node.operator === SyntaxKind.MinusToken) { - return getFreshTypeOfLiteralType(getLiteralType({ - negative: true, - base10Value: parsePseudoBigInt((node.operand as BigIntLiteral).text) - })); - } - } - switch (node.operator) { - case SyntaxKind.PlusToken: - case SyntaxKind.MinusToken: - case SyntaxKind.TildeToken: - checkNonNullType(operandType, node.operand); - if (maybeTypeOfKind(operandType, TypeFlags.ESSymbolLike)) { - error(node.operand, Diagnostics.The_0_operator_cannot_be_applied_to_type_symbol, tokenToString(node.operator)); - } - if (node.operator === SyntaxKind.PlusToken) { - if (maybeTypeOfKind(operandType, TypeFlags.BigIntLike)) { - error(node.operand, Diagnostics.Operator_0_cannot_be_applied_to_type_1, tokenToString(node.operator), typeToString(getBaseTypeOfLiteralType(operandType))); - } - return numberType; - } - return getUnaryResultType(operandType); - case SyntaxKind.ExclamationToken: - checkTruthinessExpression(node.operand); - const facts = getTypeFacts(operandType) & (TypeFacts.Truthy | TypeFacts.Falsy); - return facts === TypeFacts.Truthy ? falseType : - facts === TypeFacts.Falsy ? trueType : - booleanType; - case SyntaxKind.PlusPlusToken: - case SyntaxKind.MinusMinusToken: - const ok = checkArithmeticOperandType(node.operand, checkNonNullType(operandType, node.operand), - Diagnostics.An_arithmetic_operand_must_be_of_type_any_number_bigint_or_an_enum_type); - if (ok) { - // run check only if former checks succeeded to avoid reporting cascading errors - checkReferenceExpression( - node.operand, - Diagnostics.The_operand_of_an_increment_or_decrement_operator_must_be_a_variable_or_a_property_access, - Diagnostics.The_operand_of_an_increment_or_decrement_operator_may_not_be_an_optional_property_access); - } - return getUnaryResultType(operandType); - } - return errorType; + if (usedInFunction) { + getNodeLinks(symbol.valueDeclaration).flags |= NodeCheckFlags.CapturedBlockScopedBinding; } - - function checkPostfixUnaryExpression(node: PostfixUnaryExpression): Type { - const operandType = checkExpression(node.operand); - if (operandType === silentNeverType) { - return silentNeverType; - } - const ok = checkArithmeticOperandType( - node.operand, - checkNonNullType(operandType, node.operand), - Diagnostics.An_arithmetic_operand_must_be_of_type_any_number_bigint_or_an_enum_type); - if (ok) { - // run check only if former checks succeeded to avoid reporting cascading errors - checkReferenceExpression( - node.operand, - Diagnostics.The_operand_of_an_increment_or_decrement_operator_must_be_a_variable_or_a_property_access, - Diagnostics.The_operand_of_an_increment_or_decrement_operator_may_not_be_an_optional_property_access); - } - return getUnaryResultType(operandType); + } + function isBindingCapturedByNode(node: Node, decl: VariableDeclaration | BindingElement) { + const links = getNodeLinks(node); + return !!links && contains(links.capturedBlockScopeBindings, getSymbolOfNode(decl)); + } + function isAssignedInBodyOfForStatement(node: Identifier, container: ForStatement): boolean { + // skip parenthesized nodes + let current: Node = node; + while (current.parent.kind === SyntaxKind.ParenthesizedExpression) { + current = current.parent; + } + // check if node is used as LHS in some assignment expression + let isAssigned = false; + if (isAssignmentTarget(current)) { + isAssigned = true; + } + else if ((current.parent.kind === SyntaxKind.PrefixUnaryExpression || current.parent.kind === SyntaxKind.PostfixUnaryExpression)) { + const expr = (current.parent); + isAssigned = expr.operator === SyntaxKind.PlusPlusToken || expr.operator === SyntaxKind.MinusMinusToken; + } + if (!isAssigned) { + return false; } - - function getUnaryResultType(operandType: Type): Type { - if (maybeTypeOfKind(operandType, TypeFlags.BigIntLike)) { - return isTypeAssignableToKind(operandType, TypeFlags.AnyOrUnknown) || maybeTypeOfKind(operandType, TypeFlags.NumberLike) - ? numberOrBigIntType - : bigintType; + // at this point we know that node is the target of assignment + // now check that modification happens inside the statement part of the ForStatement + return !!findAncestor(current, n => n === container ? "quit" : n === container.statement); + } + function captureLexicalThis(node: Node, container: Node): void { + getNodeLinks(node).flags |= NodeCheckFlags.LexicalThis; + if (container.kind === SyntaxKind.PropertyDeclaration || container.kind === SyntaxKind.Constructor) { + const classNode = container.parent; + getNodeLinks(classNode).flags |= NodeCheckFlags.CaptureThis; + } + else { + getNodeLinks(container).flags |= NodeCheckFlags.CaptureThis; + } + } + function findFirstSuperCall(n: Node): SuperCall | undefined { + if (isSuperCall(n)) { + return n; + } + else if (isFunctionLike(n)) { + return undefined; + } + return forEachChild(n, findFirstSuperCall); + } + /** + * Return a cached result if super-statement is already found. + * Otherwise, find a super statement in a given constructor function and cache the result in the node-links of the constructor + * + * @param constructor constructor-function to look for super statement + */ + function getSuperCallInConstructor(constructor: ConstructorDeclaration): SuperCall | undefined { + const links = getNodeLinks(constructor); + // Only trying to find super-call if we haven't yet tried to find one. Once we try, we will record the result + if (links.hasSuperCall === undefined) { + links.superCall = findFirstSuperCall(constructor.body!); + links.hasSuperCall = links.superCall ? true : false; + } + return links.superCall!; + } + /** + * Check if the given class-declaration extends null then return true. + * Otherwise, return false + * @param classDecl a class declaration to check if it extends null + */ + function classDeclarationExtendsNull(classDecl: ClassDeclaration): boolean { + const classSymbol = getSymbolOfNode(classDecl); + const classInstanceType = (getDeclaredTypeOfSymbol(classSymbol)); + const baseConstructorType = getBaseConstructorTypeOfClass(classInstanceType); + return baseConstructorType === nullWideningType; + } + function checkThisBeforeSuper(node: Node, container: Node, diagnosticMessage: DiagnosticMessage) { + const containingClassDecl = (container.parent); + const baseTypeNode = getClassExtendsHeritageElement(containingClassDecl); + // If a containing class does not have extends clause or the class extends null + // skip checking whether super statement is called before "this" accessing. + if (baseTypeNode && !classDeclarationExtendsNull(containingClassDecl)) { + const superCall = getSuperCallInConstructor((container)); + // We should give an error in the following cases: + // - No super-call + // - "this" is accessing before super-call. + // i.e super(this) + // this.x; super(); + // We want to make sure that super-call is done before accessing "this" so that + // "this" is not accessed as a parameter of the super-call. + if (!superCall || superCall.end > node.pos) { + // In ES6, super inside constructor of class-declaration has to precede "this" accessing + error(node, diagnosticMessage); } - // If it's not a bigint type, implicit coercion will result in a number - return numberType; } - - // Return true if type might be of the given kind. A union or intersection type might be of a given - // kind if at least one constituent type is of the given kind. - function maybeTypeOfKind(type: Type, kind: TypeFlags): boolean { - if (type.flags & kind) { - return true; + } + function checkThisExpression(node: Node): ts.Type { + // Stop at the first arrow function so that we can + // tell whether 'this' needs to be captured. + let container = getThisContainer(node, /* includeArrowFunctions */ true); + let capturedByArrowFunction = false; + if (container.kind === SyntaxKind.Constructor) { + checkThisBeforeSuper(node, container, Diagnostics.super_must_be_called_before_accessing_this_in_the_constructor_of_a_derived_class); + } + // Now skip arrow functions to get the "real" owner of 'this'. + if (container.kind === SyntaxKind.ArrowFunction) { + container = getThisContainer(container, /* includeArrowFunctions */ false); + capturedByArrowFunction = true; + } + switch (container.kind) { + case SyntaxKind.ModuleDeclaration: + error(node, Diagnostics.this_cannot_be_referenced_in_a_module_or_namespace_body); + // do not return here so in case if lexical this is captured - it will be reflected in flags on NodeLinks + break; + case SyntaxKind.EnumDeclaration: + error(node, Diagnostics.this_cannot_be_referenced_in_current_location); + // do not return here so in case if lexical this is captured - it will be reflected in flags on NodeLinks + break; + case SyntaxKind.Constructor: + if (isInConstructorArgumentInitializer(node, container)) { + error(node, Diagnostics.this_cannot_be_referenced_in_constructor_arguments); + // do not return here so in case if lexical this is captured - it will be reflected in flags on NodeLinks + } + break; + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.PropertySignature: + if (hasModifier(container, ModifierFlags.Static) && !(compilerOptions.target === ScriptTarget.ESNext && compilerOptions.useDefineForClassFields)) { + error(node, Diagnostics.this_cannot_be_referenced_in_a_static_property_initializer); + // do not return here so in case if lexical this is captured - it will be reflected in flags on NodeLinks + } + break; + case SyntaxKind.ComputedPropertyName: + error(node, Diagnostics.this_cannot_be_referenced_in_a_computed_property_name); + break; + } + // When targeting es6, mark that we'll need to capture `this` in its lexically bound scope. + if (capturedByArrowFunction && languageVersion < ScriptTarget.ES2015) { + captureLexicalThis(node, container); + } + const type = tryGetThisTypeAt(node, /*includeGlobalThis*/ true, container); + if (noImplicitThis) { + const globalThisType = getTypeOfSymbol(globalThisSymbol); + if (type === globalThisType && capturedByArrowFunction) { + error(node, Diagnostics.The_containing_arrow_function_captures_the_global_value_of_this); } - if (type.flags & TypeFlags.UnionOrIntersection) { - const types = (type).types; - for (const t of types) { - if (maybeTypeOfKind(t, kind)) { - return true; + else if (!type) { + // With noImplicitThis, functions may not reference 'this' if it has type 'any' + const diag = error(node, Diagnostics.this_implicitly_has_type_any_because_it_does_not_have_a_type_annotation); + if (!isSourceFile(container)) { + const outsideThis = tryGetThisTypeAt(container); + if (outsideThis && outsideThis !== globalThisType) { + addRelatedInfo(diag, createDiagnosticForNode(container, Diagnostics.An_outer_value_of_this_is_shadowed_by_this_container)); } } } - return false; } - - function isTypeAssignableToKind(source: Type, kind: TypeFlags, strict?: boolean): boolean { - if (source.flags & kind) { - return true; + return type || anyType; + } + function tryGetThisTypeAt(node: Node, includeGlobalThis = true, container = getThisContainer(node, /*includeArrowFunctions*/ false)): ts.Type | undefined { + const isInJS = isInJSFile(node); + if (isFunctionLike(container) && + (!isInParameterInitializerBeforeContainingFunction(node) || getThisParameter(container))) { + // Note: a parameter initializer should refer to class-this unless function-this is explicitly annotated. + // If this is a function in a JS file, it might be a class method. + const className = getClassNameFromPrototypeMethod(container); + if (isInJS && className) { + const classSymbol = checkExpression(className).symbol; + if (classSymbol && classSymbol.members && (classSymbol.flags & SymbolFlags.Function)) { + const classType = ((getDeclaredTypeOfSymbol(classSymbol) as InterfaceType).thisType!); + return getFlowTypeOfReference(node, classType); + } } - if (strict && source.flags & (TypeFlags.AnyOrUnknown | TypeFlags.Void | TypeFlags.Undefined | TypeFlags.Null)) { - return false; + // Check if it's a constructor definition, can be either a variable decl or function decl + // i.e. + // * /** @constructor */ function [name]() { ... } + // * /** @constructor */ var x = function() { ... } + else if (isInJS && + (container.kind === SyntaxKind.FunctionExpression || container.kind === SyntaxKind.FunctionDeclaration) && + getJSDocClassTag(container)) { + const classType = ((getDeclaredTypeOfSymbol(getMergedSymbol(container.symbol)) as InterfaceType).thisType!); + return getFlowTypeOfReference(node, classType); + } + const thisType = getThisTypeOfDeclaration(container) || getContextualThisParameterType(container); + if (thisType) { + return getFlowTypeOfReference(node, thisType); } - return !!(kind & TypeFlags.NumberLike) && isTypeAssignableTo(source, numberType) || - !!(kind & TypeFlags.BigIntLike) && isTypeAssignableTo(source, bigintType) || - !!(kind & TypeFlags.StringLike) && isTypeAssignableTo(source, stringType) || - !!(kind & TypeFlags.BooleanLike) && isTypeAssignableTo(source, booleanType) || - !!(kind & TypeFlags.Void) && isTypeAssignableTo(source, voidType) || - !!(kind & TypeFlags.Never) && isTypeAssignableTo(source, neverType) || - !!(kind & TypeFlags.Null) && isTypeAssignableTo(source, nullType) || - !!(kind & TypeFlags.Undefined) && isTypeAssignableTo(source, undefinedType) || - !!(kind & TypeFlags.ESSymbol) && isTypeAssignableTo(source, esSymbolType) || - !!(kind & TypeFlags.NonPrimitive) && isTypeAssignableTo(source, nonPrimitiveType); - } - - function allTypesAssignableToKind(source: Type, kind: TypeFlags, strict?: boolean): boolean { - return source.flags & TypeFlags.Union ? - every((source as UnionType).types, subType => allTypesAssignableToKind(subType, kind, strict)) : - isTypeAssignableToKind(source, kind, strict); - } - - function isConstEnumObjectType(type: Type): boolean { - return !!(getObjectFlags(type) & ObjectFlags.Anonymous) && !!type.symbol && isConstEnumSymbol(type.symbol); } - - function isConstEnumSymbol(symbol: Symbol): boolean { - return (symbol.flags & SymbolFlags.ConstEnum) !== 0; + if (isClassLike(container.parent)) { + const symbol = getSymbolOfNode(container.parent); + const type = hasModifier(container, ModifierFlags.Static) ? getTypeOfSymbol(symbol) : (getDeclaredTypeOfSymbol(symbol) as InterfaceType).thisType!; + return getFlowTypeOfReference(node, type); } - - function checkInstanceOfExpression(left: Expression, right: Expression, leftType: Type, rightType: Type): Type { - if (leftType === silentNeverType || rightType === silentNeverType) { - return silentNeverType; - } - // TypeScript 1.0 spec (April 2014): 4.15.4 - // The instanceof operator requires the left operand to be of type Any, an object type, or a type parameter type, - // and the right operand to be of type Any, a subtype of the 'Function' interface type, or have a call or construct signature. - // The result is always of the Boolean primitive type. - // NOTE: do not raise error if leftType is unknown as related error was already reported - if (!isTypeAny(leftType) && - allTypesAssignableToKind(leftType, TypeFlags.Primitive)) { - error(left, Diagnostics.The_left_hand_side_of_an_instanceof_expression_must_be_of_type_any_an_object_type_or_a_type_parameter); - } - // NOTE: do not raise error if right is unknown as related error was already reported - if (!(isTypeAny(rightType) || typeHasCallOrConstructSignatures(rightType) || isTypeSubtypeOf(rightType, globalFunctionType))) { - error(right, Diagnostics.The_right_hand_side_of_an_instanceof_expression_must_be_of_type_any_or_of_a_type_assignable_to_the_Function_interface_type); + if (isInJS) { + const type = getTypeForThisExpressionFromJSDoc(container); + if (type && type !== errorType) { + return getFlowTypeOfReference(node, type); } - return booleanType; } - - function checkInExpression(left: Expression, right: Expression, leftType: Type, rightType: Type): Type { - if (leftType === silentNeverType || rightType === silentNeverType) { - return silentNeverType; - } - leftType = checkNonNullType(leftType, left); - rightType = checkNonNullType(rightType, right); - // TypeScript 1.0 spec (April 2014): 4.15.5 - // The in operator requires the left operand to be of type Any, the String primitive type, or the Number primitive type, - // and the right operand to be of type Any, an object type, or a type parameter type. - // The result is always of the Boolean primitive type. - if (!(isTypeComparableTo(leftType, stringType) || isTypeAssignableToKind(leftType, TypeFlags.NumberLike | TypeFlags.ESSymbolLike))) { - error(left, Diagnostics.The_left_hand_side_of_an_in_expression_must_be_of_type_any_string_number_or_symbol); - } - if (!allTypesAssignableToKind(rightType, TypeFlags.NonPrimitive | TypeFlags.InstantiableNonPrimitive)) { - error(right, Diagnostics.The_right_hand_side_of_an_in_expression_must_be_of_type_any_an_object_type_or_a_type_parameter); + if (isSourceFile(container)) { + // look up in the source file's locals or exports + if (container.commonJsModuleIndicator) { + const fileSymbol = getSymbolOfNode(container); + return fileSymbol && getTypeOfSymbol(fileSymbol); } - return booleanType; - } - - function checkObjectLiteralAssignment(node: ObjectLiteralExpression, sourceType: Type, rightIsThis?: boolean): Type { - const properties = node.properties; - if (strictNullChecks && properties.length === 0) { - return checkNonNullType(sourceType, node); + else if (includeGlobalThis) { + return getTypeOfSymbol(globalThisSymbol); } - for (let i = 0; i < properties.length; i++) { - checkObjectLiteralDestructuringPropertyAssignment(node, sourceType, i, properties, rightIsThis); + } + } + function getExplicitThisType(node: Expression) { + const container = getThisContainer(node, /*includeArrowFunctions*/ false); + if (isFunctionLike(container)) { + const signature = getSignatureFromDeclaration(container); + if (signature.thisParameter) { + return getExplicitTypeOfSymbol(signature.thisParameter); } - return sourceType; } - - /** Note: If property cannot be a SpreadAssignment, then allProperties does not need to be provided */ - function checkObjectLiteralDestructuringPropertyAssignment(node: ObjectLiteralExpression, objectLiteralType: Type, propertyIndex: number, allProperties?: NodeArray, rightIsThis = false) { - const properties = node.properties; - const property = properties[propertyIndex]; - if (property.kind === SyntaxKind.PropertyAssignment || property.kind === SyntaxKind.ShorthandPropertyAssignment) { - const name = property.name; - const exprType = getLiteralTypeFromPropertyName(name); - if (isTypeUsableAsPropertyName(exprType)) { - const text = getPropertyNameFromType(exprType); - const prop = getPropertyOfType(objectLiteralType, text); - if (prop) { - markPropertyAsReferenced(prop, property, rightIsThis); - checkPropertyAccessibility(property, /*isSuper*/ false, objectLiteralType, prop); - } - } - const elementType = getIndexedAccessType(objectLiteralType, exprType, name); - const type = getFlowTypeOfDestructuring(property, elementType); - return checkDestructuringAssignment(property.kind === SyntaxKind.ShorthandPropertyAssignment ? property : property.initializer, type); + if (isClassLike(container.parent)) { + const symbol = getSymbolOfNode(container.parent); + return hasModifier(container, ModifierFlags.Static) ? getTypeOfSymbol(symbol) : (getDeclaredTypeOfSymbol(symbol) as InterfaceType).thisType!; + } + } + function getClassNameFromPrototypeMethod(container: Node) { + // Check if it's the RHS of a x.prototype.y = function [name]() { .... } + if (container.kind === SyntaxKind.FunctionExpression && + isBinaryExpression(container.parent) && + getAssignmentDeclarationKind(container.parent) === AssignmentDeclarationKind.PrototypeProperty) { + // Get the 'x' of 'x.prototype.y = container' + return ((container.parent // x.prototype.y = container + .left as PropertyAccessExpression) // x.prototype.y + .expression as PropertyAccessExpression) // x.prototype + .expression; // x + } + // x.prototype = { method() { } } + else if (container.kind === SyntaxKind.MethodDeclaration && + container.parent.kind === SyntaxKind.ObjectLiteralExpression && + isBinaryExpression(container.parent.parent) && + getAssignmentDeclarationKind(container.parent.parent) === AssignmentDeclarationKind.Prototype) { + return (container.parent.parent.left as PropertyAccessExpression).expression; + } + // x.prototype = { method: function() { } } + else if (container.kind === SyntaxKind.FunctionExpression && + container.parent.kind === SyntaxKind.PropertyAssignment && + container.parent.parent.kind === SyntaxKind.ObjectLiteralExpression && + isBinaryExpression(container.parent.parent.parent) && + getAssignmentDeclarationKind(container.parent.parent.parent) === AssignmentDeclarationKind.Prototype) { + return (container.parent.parent.parent.left as PropertyAccessExpression).expression; + } + // Object.defineProperty(x, "method", { value: function() { } }); + // Object.defineProperty(x, "method", { set: (x: () => void) => void }); + // Object.defineProperty(x, "method", { get: () => function() { }) }); + else if (container.kind === SyntaxKind.FunctionExpression && + isPropertyAssignment(container.parent) && + isIdentifier(container.parent.name) && + (container.parent.name.escapedText === "value" || container.parent.name.escapedText === "get" || container.parent.name.escapedText === "set") && + isObjectLiteralExpression(container.parent.parent) && + isCallExpression(container.parent.parent.parent) && + container.parent.parent.parent.arguments[2] === container.parent.parent && + getAssignmentDeclarationKind(container.parent.parent.parent) === AssignmentDeclarationKind.ObjectDefinePrototypeProperty) { + return (container.parent.parent.parent.arguments[0] as PropertyAccessExpression).expression; + } + // Object.defineProperty(x, "method", { value() { } }); + // Object.defineProperty(x, "method", { set(x: () => void) {} }); + // Object.defineProperty(x, "method", { get() { return () => {} } }); + else if (isMethodDeclaration(container) && + isIdentifier(container.name) && + (container.name.escapedText === "value" || container.name.escapedText === "get" || container.name.escapedText === "set") && + isObjectLiteralExpression(container.parent) && + isCallExpression(container.parent.parent) && + container.parent.parent.arguments[2] === container.parent && + getAssignmentDeclarationKind(container.parent.parent) === AssignmentDeclarationKind.ObjectDefinePrototypeProperty) { + return (container.parent.parent.arguments[0] as PropertyAccessExpression).expression; + } + } + function getTypeForThisExpressionFromJSDoc(node: Node) { + const jsdocType = getJSDocType(node); + if (jsdocType && jsdocType.kind === SyntaxKind.JSDocFunctionType) { + const jsDocFunctionType = (jsdocType); + if (jsDocFunctionType.parameters.length > 0 && + jsDocFunctionType.parameters[0].name && + (jsDocFunctionType.parameters[0].name as Identifier).escapedText === InternalSymbolName.This) { + return getTypeFromTypeNode(jsDocFunctionType.parameters[0].type!); } - else if (property.kind === SyntaxKind.SpreadAssignment) { - if (propertyIndex < properties.length - 1) { - error(property, Diagnostics.A_rest_element_must_be_last_in_a_destructuring_pattern); - } - else { - if (languageVersion < ScriptTarget.ESNext) { - checkExternalEmitHelpers(property, ExternalEmitHelpers.Rest); - } - const nonRestNames: PropertyName[] = []; - if (allProperties) { - for (const otherProperty of allProperties) { - if (!isSpreadAssignment(otherProperty)) { - nonRestNames.push(otherProperty.name); - } - } - } - const type = getRestType(objectLiteralType, nonRestNames, objectLiteralType.symbol); - checkGrammarForDisallowedTrailingComma(allProperties, Diagnostics.A_rest_parameter_or_binding_pattern_may_not_have_a_trailing_comma); - return checkDestructuringAssignment(property.expression, type); - } + } + const thisTag = getJSDocThisTag(node); + if (thisTag && thisTag.typeExpression) { + return getTypeFromTypeNode(thisTag.typeExpression); + } + } + function isInConstructorArgumentInitializer(node: Node, constructorDecl: Node): boolean { + return !!findAncestor(node, n => isFunctionLikeDeclaration(n) ? "quit" : n.kind === SyntaxKind.Parameter && n.parent === constructorDecl); + } + function checkSuperExpression(node: Node): ts.Type { + const isCallExpression = node.parent.kind === SyntaxKind.CallExpression && (node.parent).expression === node; + let container = getSuperContainer(node, /*stopOnFunctions*/ true); + let needToCaptureLexicalThis = false; + // adjust the container reference in case if super is used inside arrow functions with arbitrarily deep nesting + if (!isCallExpression) { + while (container && container.kind === SyntaxKind.ArrowFunction) { + container = getSuperContainer(container, /*stopOnFunctions*/ true); + needToCaptureLexicalThis = languageVersion < ScriptTarget.ES2015; + } + } + const canUseSuperExpression = isLegalUsageOfSuperExpression(container); + let nodeCheckFlag: NodeCheckFlags = 0; + if (!canUseSuperExpression) { + // issue more specific error if super is used in computed property name + // class A { foo() { return "1" }} + // class B { + // [super.foo()]() {} + // } + const current = findAncestor(node, n => n === container ? "quit" : n.kind === SyntaxKind.ComputedPropertyName); + if (current && current.kind === SyntaxKind.ComputedPropertyName) { + error(node, Diagnostics.super_cannot_be_referenced_in_a_computed_property_name); + } + else if (isCallExpression) { + error(node, Diagnostics.Super_calls_are_not_permitted_outside_constructors_or_in_nested_functions_inside_constructors); + } + else if (!container || !container.parent || !(isClassLike(container.parent) || container.parent.kind === SyntaxKind.ObjectLiteralExpression)) { + error(node, Diagnostics.super_can_only_be_referenced_in_members_of_derived_classes_or_object_literal_expressions); } else { - error(property, Diagnostics.Property_assignment_expected); + error(node, Diagnostics.super_property_access_is_permitted_only_in_a_constructor_member_function_or_member_accessor_of_a_derived_class); } + return errorType; } - - function checkArrayLiteralAssignment(node: ArrayLiteralExpression, sourceType: Type, checkMode?: CheckMode): Type { - const elements = node.elements; - if (languageVersion < ScriptTarget.ES2015 && compilerOptions.downlevelIteration) { - checkExternalEmitHelpers(node, ExternalEmitHelpers.Read); - } - // This elementType will be used if the specific property corresponding to this index is not - // present (aka the tuple element property). This call also checks that the parentType is in - // fact an iterable or array (depending on target language). - const elementType = checkIteratedTypeOrElementType(IterationUse.Destructuring, sourceType, undefinedType, node) || errorType; - for (let i = 0; i < elements.length; i++) { - checkArrayLiteralDestructuringElementAssignment(node, sourceType, i, elementType, checkMode); - } - return sourceType; + if (!isCallExpression && container.kind === SyntaxKind.Constructor) { + checkThisBeforeSuper(node, container, Diagnostics.super_must_be_called_before_accessing_a_property_of_super_in_the_constructor_of_a_derived_class); } - - function checkArrayLiteralDestructuringElementAssignment(node: ArrayLiteralExpression, sourceType: Type, - elementIndex: number, elementType: Type, checkMode?: CheckMode) { - const elements = node.elements; - const element = elements[elementIndex]; - if (element.kind !== SyntaxKind.OmittedExpression) { - if (element.kind !== SyntaxKind.SpreadElement) { - const indexType = getLiteralType(elementIndex); - if (isArrayLikeType(sourceType)) { - // We create a synthetic expression so that getIndexedAccessType doesn't get confused - // when the element is a SyntaxKind.ElementAccessExpression. - const accessFlags = hasDefaultValue(element) ? AccessFlags.NoTupleBoundsCheck : 0; - const elementType = getIndexedAccessTypeOrUndefined(sourceType, indexType, createSyntheticExpression(element, indexType), accessFlags) || errorType; - const assignedType = hasDefaultValue(element) ? getTypeWithFacts(elementType, TypeFacts.NEUndefined) : elementType; - const type = getFlowTypeOfDestructuring(element, assignedType); - return checkDestructuringAssignment(element, type, checkMode); - } - return checkDestructuringAssignment(element, elementType, checkMode); - } - if (elementIndex < elements.length - 1) { - error(element, Diagnostics.A_rest_element_must_be_last_in_a_destructuring_pattern); - } - else { - const restExpression = (element).expression; - if (restExpression.kind === SyntaxKind.BinaryExpression && (restExpression).operatorToken.kind === SyntaxKind.EqualsToken) { - error((restExpression).operatorToken, Diagnostics.A_rest_element_cannot_have_an_initializer); - } - else { - checkGrammarForDisallowedTrailingComma(node.elements, Diagnostics.A_rest_parameter_or_binding_pattern_may_not_have_a_trailing_comma); - const type = everyType(sourceType, isTupleType) ? - mapType(sourceType, t => sliceTupleType(t, elementIndex)) : - createArrayType(elementType); - return checkDestructuringAssignment(restExpression, type, checkMode); - } - } - } - return undefined; + if (hasModifier(container, ModifierFlags.Static) || isCallExpression) { + nodeCheckFlag = NodeCheckFlags.SuperStatic; } - - function checkDestructuringAssignment(exprOrAssignment: Expression | ShorthandPropertyAssignment, sourceType: Type, checkMode?: CheckMode, rightIsThis?: boolean): Type { - let target: Expression; - if (exprOrAssignment.kind === SyntaxKind.ShorthandPropertyAssignment) { - const prop = exprOrAssignment; - if (prop.objectAssignmentInitializer) { - // In strict null checking mode, if a default value of a non-undefined type is specified, remove - // undefined from the final type. - if (strictNullChecks && - !(getFalsyFlags(checkExpression(prop.objectAssignmentInitializer)) & TypeFlags.Undefined)) { - sourceType = getTypeWithFacts(sourceType, TypeFacts.NEUndefined); - } - checkBinaryLikeExpression(prop.name, prop.equalsToken!, prop.objectAssignmentInitializer, checkMode); - } - target = (exprOrAssignment).name; + else { + nodeCheckFlag = NodeCheckFlags.SuperInstance; + } + getNodeLinks(node).flags |= nodeCheckFlag; + // Due to how we emit async functions, we need to specialize the emit for an async method that contains a `super` reference. + // This is due to the fact that we emit the body of an async function inside of a generator function. As generator + // functions cannot reference `super`, we emit a helper inside of the method body, but outside of the generator. This helper + // uses an arrow function, which is permitted to reference `super`. + // + // There are two primary ways we can access `super` from within an async method. The first is getting the value of a property + // or indexed access on super, either as part of a right-hand-side expression or call expression. The second is when setting the value + // of a property or indexed access, either as part of an assignment expression or destructuring assignment. + // + // The simplest case is reading a value, in which case we will emit something like the following: + // + // // ts + // ... + // async asyncMethod() { + // let x = await super.asyncMethod(); + // return x; + // } + // ... + // + // // js + // ... + // asyncMethod() { + // const _super = Object.create(null, { + // asyncMethod: { get: () => super.asyncMethod }, + // }); + // return __awaiter(this, arguments, Promise, function *() { + // let x = yield _super.asyncMethod.call(this); + // return x; + // }); + // } + // ... + // + // The more complex case is when we wish to assign a value, especially as part of a destructuring assignment. As both cases + // are legal in ES6, but also likely less frequent, we only emit setters if there is an assignment: + // + // // ts + // ... + // async asyncMethod(ar: Promise) { + // [super.a, super.b] = await ar; + // } + // ... + // + // // js + // ... + // asyncMethod(ar) { + // const _super = Object.create(null, { + // a: { get: () => super.a, set: (v) => super.a = v }, + // b: { get: () => super.b, set: (v) => super.b = v } + // }; + // return __awaiter(this, arguments, Promise, function *() { + // [_super.a, _super.b] = yield ar; + // }); + // } + // ... + // + // Creating an object that has getter and setters instead of just an accessor function is required for destructuring assignments + // as a call expression cannot be used as the target of a destructuring assignment while a property access can. + // + // For element access expressions (`super[x]`), we emit a generic helper that forwards the element access in both situations. + if (container.kind === SyntaxKind.MethodDeclaration && hasModifier(container, ModifierFlags.Async)) { + if (isSuperProperty(node.parent) && isAssignmentTarget(node.parent)) { + getNodeLinks(container).flags |= NodeCheckFlags.AsyncMethodWithSuperBinding; } else { - target = exprOrAssignment; - } - - if (target.kind === SyntaxKind.BinaryExpression && (target).operatorToken.kind === SyntaxKind.EqualsToken) { - checkBinaryExpression(target, checkMode); - target = (target).left; + getNodeLinks(container).flags |= NodeCheckFlags.AsyncMethodWithSuper; } - if (target.kind === SyntaxKind.ObjectLiteralExpression) { - return checkObjectLiteralAssignment(target, sourceType, rightIsThis); + } + if (needToCaptureLexicalThis) { + // call expressions are allowed only in constructors so they should always capture correct 'this' + // super property access expressions can also appear in arrow functions - + // in this case they should also use correct lexical this + captureLexicalThis(node.parent, container); + } + if (container.parent.kind === SyntaxKind.ObjectLiteralExpression) { + if (languageVersion < ScriptTarget.ES2015) { + error(node, Diagnostics.super_is_only_allowed_in_members_of_object_literal_expressions_when_option_target_is_ES2015_or_higher); + return errorType; } - if (target.kind === SyntaxKind.ArrayLiteralExpression) { - return checkArrayLiteralAssignment(target, sourceType, checkMode); + else { + // for object literal assume that type of 'super' is 'any' + return anyType; } - return checkReferenceAssignment(target, sourceType, checkMode); } - - function checkReferenceAssignment(target: Expression, sourceType: Type, checkMode?: CheckMode): Type { - const targetType = checkExpression(target, checkMode); - const error = target.parent.kind === SyntaxKind.SpreadAssignment ? - Diagnostics.The_target_of_an_object_rest_assignment_must_be_a_variable_or_a_property_access : - Diagnostics.The_left_hand_side_of_an_assignment_expression_must_be_a_variable_or_a_property_access; - const optionalError = target.parent.kind === SyntaxKind.SpreadAssignment ? - Diagnostics.The_target_of_an_object_rest_assignment_may_not_be_an_optional_property_access : - Diagnostics.The_left_hand_side_of_an_assignment_expression_may_not_be_an_optional_property_access; - if (checkReferenceExpression(target, error, optionalError)) { - checkTypeAssignableToAndOptionallyElaborate(sourceType, targetType, target, target); - } - if (isPrivateIdentifierPropertyAccessExpression(target)) { - checkExternalEmitHelpers(target.parent, ExternalEmitHelpers.ClassPrivateFieldSet); - } - return sourceType; + // at this point the only legal case for parent is ClassLikeDeclaration + const classLikeDeclaration = (container.parent); + if (!getClassExtendsHeritageElement(classLikeDeclaration)) { + error(node, Diagnostics.super_can_only_be_referenced_in_a_derived_class); + return errorType; } - - /** - * This is a *shallow* check: An expression is side-effect-free if the - * evaluation of the expression *itself* cannot produce side effects. - * For example, x++ / 3 is side-effect free because the / operator - * does not have side effects. - * The intent is to "smell test" an expression for correctness in positions where - * its value is discarded (e.g. the left side of the comma operator). - */ - function isSideEffectFree(node: Node): boolean { - node = skipParentheses(node); - switch (node.kind) { - case SyntaxKind.Identifier: - case SyntaxKind.StringLiteral: - case SyntaxKind.RegularExpressionLiteral: - case SyntaxKind.TaggedTemplateExpression: - case SyntaxKind.TemplateExpression: - case SyntaxKind.NoSubstitutionTemplateLiteral: - case SyntaxKind.NumericLiteral: - case SyntaxKind.BigIntLiteral: - case SyntaxKind.TrueKeyword: - case SyntaxKind.FalseKeyword: - case SyntaxKind.NullKeyword: - case SyntaxKind.UndefinedKeyword: - case SyntaxKind.FunctionExpression: - case SyntaxKind.ClassExpression: - case SyntaxKind.ArrowFunction: - case SyntaxKind.ArrayLiteralExpression: - case SyntaxKind.ObjectLiteralExpression: - case SyntaxKind.TypeOfExpression: - case SyntaxKind.NonNullExpression: - case SyntaxKind.JsxSelfClosingElement: - case SyntaxKind.JsxElement: - return true; - - case SyntaxKind.ConditionalExpression: - return isSideEffectFree((node as ConditionalExpression).whenTrue) && - isSideEffectFree((node as ConditionalExpression).whenFalse); - - case SyntaxKind.BinaryExpression: - if (isAssignmentOperator((node as BinaryExpression).operatorToken.kind)) { - return false; + const classType = (getDeclaredTypeOfSymbol(getSymbolOfNode(classLikeDeclaration))); + const baseClassType = classType && getBaseTypes(classType)[0]; + if (!baseClassType) { + return errorType; + } + if (container.kind === SyntaxKind.Constructor && isInConstructorArgumentInitializer(node, container)) { + // issue custom error message for super property access in constructor arguments (to be aligned with old compiler) + error(node, Diagnostics.super_cannot_be_referenced_in_constructor_arguments); + return errorType; + } + return nodeCheckFlag === NodeCheckFlags.SuperStatic + ? getBaseConstructorTypeOfClass(classType) + : getTypeWithThisArgument(baseClassType, classType.thisType); + function isLegalUsageOfSuperExpression(container: Node): boolean { + if (!container) { + return false; + } + if (isCallExpression) { + // TS 1.0 SPEC (April 2014): 4.8.1 + // Super calls are only permitted in constructors of derived classes + return container.kind === SyntaxKind.Constructor; + } + else { + // TS 1.0 SPEC (April 2014) + // 'super' property access is allowed + // - In a constructor, instance member function, instance member accessor, or instance member variable initializer where this references a derived class instance + // - In a static member function or static member accessor + // topmost container must be something that is directly nested in the class declaration\object literal expression + if (isClassLike(container.parent) || container.parent.kind === SyntaxKind.ObjectLiteralExpression) { + if (hasModifier(container, ModifierFlags.Static)) { + return container.kind === SyntaxKind.MethodDeclaration || + container.kind === SyntaxKind.MethodSignature || + container.kind === SyntaxKind.GetAccessor || + container.kind === SyntaxKind.SetAccessor; } - return isSideEffectFree((node as BinaryExpression).left) && - isSideEffectFree((node as BinaryExpression).right); - - case SyntaxKind.PrefixUnaryExpression: - case SyntaxKind.PostfixUnaryExpression: - // Unary operators ~, !, +, and - have no side effects. - // The rest do. - switch ((node as PrefixUnaryExpression).operator) { - case SyntaxKind.ExclamationToken: - case SyntaxKind.PlusToken: - case SyntaxKind.MinusToken: - case SyntaxKind.TildeToken: - return true; + else { + return container.kind === SyntaxKind.MethodDeclaration || + container.kind === SyntaxKind.MethodSignature || + container.kind === SyntaxKind.GetAccessor || + container.kind === SyntaxKind.SetAccessor || + container.kind === SyntaxKind.PropertyDeclaration || + container.kind === SyntaxKind.PropertySignature || + container.kind === SyntaxKind.Constructor; } - return false; - - // Some forms listed here for clarity - case SyntaxKind.VoidExpression: // Explicit opt-out - case SyntaxKind.TypeAssertionExpression: // Not SEF, but can produce useful type warnings - case SyntaxKind.AsExpression: // Not SEF, but can produce useful type warnings - default: - return false; + } } + return false; } - - function isTypeEqualityComparableTo(source: Type, target: Type) { - return (target.flags & TypeFlags.Nullable) !== 0 || isTypeComparableTo(source, target); - } - - const enum CheckBinaryExpressionState { - MaybeCheckLeft, - CheckRight, - FinishCheck + } + function getContainingObjectLiteral(func: SignatureDeclaration): ObjectLiteralExpression | undefined { + return (func.kind === SyntaxKind.MethodDeclaration || + func.kind === SyntaxKind.GetAccessor || + func.kind === SyntaxKind.SetAccessor) && func.parent.kind === SyntaxKind.ObjectLiteralExpression ? func.parent : + func.kind === SyntaxKind.FunctionExpression && func.parent.kind === SyntaxKind.PropertyAssignment ? func.parent.parent : + undefined; + } + function getThisTypeArgument(type: ts.Type): ts.Type | undefined { + return getObjectFlags(type) & ObjectFlags.Reference && (type).target === globalThisType ? getTypeArguments((type))[0] : undefined; + } + function getThisTypeFromContextualType(type: ts.Type): ts.Type | undefined { + return mapType(type, t => { + return t.flags & TypeFlags.Intersection ? forEach((t).types, getThisTypeArgument) : getThisTypeArgument(t); + }); + } + function getContextualThisParameterType(func: SignatureDeclaration): ts.Type | undefined { + if (func.kind === SyntaxKind.ArrowFunction) { + return undefined; } - - function checkBinaryExpression(node: BinaryExpression, checkMode?: CheckMode) { - const workStacks: { - expr: BinaryExpression[], - state: CheckBinaryExpressionState[], - leftType: (Type | undefined)[] - } = { - expr: [node], - state: [CheckBinaryExpressionState.MaybeCheckLeft], - leftType: [undefined] - }; - let stackIndex = 0; - let lastResult: Type | undefined; - while (stackIndex >= 0) { - node = workStacks.expr[stackIndex]; - switch (workStacks.state[stackIndex]) { - case CheckBinaryExpressionState.MaybeCheckLeft: { - if (isInJSFile(node) && getAssignedExpandoInitializer(node)) { - finishInvocation(checkExpression(node.right, checkMode)); - break; - } - checkGrammarNullishCoalesceWithLogicalExpression(node); - const operator = node.operatorToken.kind; - if (operator === SyntaxKind.EqualsToken && (node.left.kind === SyntaxKind.ObjectLiteralExpression || node.left.kind === SyntaxKind.ArrayLiteralExpression)) { - finishInvocation(checkDestructuringAssignment(node.left, checkExpression(node.right, checkMode), checkMode, node.right.kind === SyntaxKind.ThisKeyword)); - break; - } - advanceState(CheckBinaryExpressionState.CheckRight); - maybeCheckExpression(node.left); + if (isContextSensitiveFunctionOrObjectLiteralMethod(func)) { + const contextualSignature = getContextualSignature(func); + if (contextualSignature) { + const thisParameter = contextualSignature.thisParameter; + if (thisParameter) { + return getTypeOfSymbol(thisParameter); + } + } + } + const inJs = isInJSFile(func); + if (noImplicitThis || inJs) { + const containingLiteral = getContainingObjectLiteral(func); + if (containingLiteral) { + // We have an object literal method. Check if the containing object literal has a contextual type + // that includes a ThisType. If so, T is the contextual type for 'this'. We continue looking in + // any directly enclosing object literals. + const contextualType = getApparentTypeOfContextualType(containingLiteral); + let literal = containingLiteral; + let type = contextualType; + while (type) { + const thisType = getThisTypeFromContextualType(type); + if (thisType) { + return instantiateType(thisType, getMapperFromContext(getInferenceContext(containingLiteral))); + } + if (literal.parent.kind !== SyntaxKind.PropertyAssignment) { break; } - case CheckBinaryExpressionState.CheckRight: { - const leftType = lastResult!; - workStacks.leftType[stackIndex] = leftType; - const operator = node.operatorToken.kind; - if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.BarBarToken || operator === SyntaxKind.QuestionQuestionToken) { - checkTruthinessOfType(leftType, node.left); + literal = (literal.parent.parent); + type = getApparentTypeOfContextualType(literal); + } + // There was no contextual ThisType for the containing object literal, so the contextual type + // for 'this' is the non-null form of the contextual type for the containing object literal or + // the type of the object literal itself. + return getWidenedType(contextualType ? getNonNullableType(contextualType) : checkExpressionCached(containingLiteral)); + } + // In an assignment of the form 'obj.xxx = function(...)' or 'obj[xxx] = function(...)', the + // contextual type for 'this' is 'obj'. + const parent = walkUpParenthesizedExpressions(func.parent); + if (parent.kind === SyntaxKind.BinaryExpression && (parent).operatorToken.kind === SyntaxKind.EqualsToken) { + const target = (parent).left; + if (isAccessExpression(target)) { + const { expression } = target; + // Don't contextually type `this` as `exports` in `exports.Point = function(x, y) { this.x = x; this.y = y; }` + if (inJs && isIdentifier(expression)) { + const sourceFile = getSourceFileOfNode(parent); + if (sourceFile.commonJsModuleIndicator && getResolvedSymbol(expression) === sourceFile.symbol) { + return undefined; } - advanceState(CheckBinaryExpressionState.FinishCheck); - maybeCheckExpression(node.right); - break; - } - case CheckBinaryExpressionState.FinishCheck: { - const leftType = workStacks.leftType[stackIndex]!; - const rightType = lastResult!; - finishInvocation(checkBinaryLikeExpressionWorker(node.left, node.operatorToken, node.right, leftType, rightType, node)); - break; } - default: return Debug.fail(`Invalid state ${workStacks.state[stackIndex]} for checkBinaryExpression`); + return getWidenedType(checkExpressionCached(expression)); } } - - return lastResult!; - - function finishInvocation(result: Type) { - lastResult = result; - stackIndex--; + } + return undefined; + } + // Return contextual type of parameter or undefined if no contextual type is available + function getContextuallyTypedParameterType(parameter: ParameterDeclaration): ts.Type | undefined { + const func = parameter.parent; + if (!isContextSensitiveFunctionOrObjectLiteralMethod(func)) { + return undefined; + } + const iife = getImmediatelyInvokedFunctionExpression(func); + if (iife && iife.arguments) { + const args = getEffectiveCallArguments(iife); + const indexOfParameter = func.parameters.indexOf(parameter); + if (parameter.dotDotDotToken) { + return getSpreadArgumentType(args, indexOfParameter, args.length, anyType, /*context*/ undefined); } - - /** - * Note that `advanceState` sets the _current_ head state, and that `maybeCheckExpression` potentially pushes on a new - * head state; so `advanceState` must be called before any `maybeCheckExpression` during a state's execution. - */ - function advanceState(nextState: CheckBinaryExpressionState) { - workStacks.state[stackIndex] = nextState; + const links = getNodeLinks(iife); + const cached = links.resolvedSignature; + links.resolvedSignature = anySignature; + const type = indexOfParameter < args.length ? + getWidenedLiteralType(checkExpression(args[indexOfParameter])) : + parameter.initializer ? undefined : undefinedWideningType; + links.resolvedSignature = cached; + return type; + } + const contextualSignature = getContextualSignature(func); + if (contextualSignature) { + const index = func.parameters.indexOf(parameter) - (getThisParameter(func) ? 1 : 0); + return parameter.dotDotDotToken && lastOrUndefined(func.parameters) === parameter ? + getRestTypeAtPosition(contextualSignature, index) : + tryGetTypeAtPosition(contextualSignature, index); + } + } + function getContextualTypeForVariableLikeDeclaration(declaration: VariableLikeDeclaration): ts.Type | undefined { + const typeNode = getEffectiveTypeAnnotationNode(declaration); + if (typeNode) { + return getTypeFromTypeNode(typeNode); + } + switch (declaration.kind) { + case SyntaxKind.Parameter: + return getContextuallyTypedParameterType(declaration); + case SyntaxKind.BindingElement: + return getContextualTypeForBindingElement(declaration); + // By default, do nothing and return undefined - only parameters and binding elements have context implied by a parent + } + } + function getContextualTypeForBindingElement(declaration: BindingElement): ts.Type | undefined { + const parent = declaration.parent.parent; + const name = declaration.propertyName || declaration.name; + const parentType = getContextualTypeForVariableLikeDeclaration(parent) || + parent.kind !== SyntaxKind.BindingElement && parent.initializer && checkDeclarationInitializer(parent); + if (parentType && !isBindingPattern(name) && !isComputedNonLiteralName(name)) { + const nameType = getLiteralTypeFromPropertyName(name); + if (isTypeUsableAsPropertyName(nameType)) { + const text = getPropertyNameFromType(nameType); + return getTypeOfPropertyOfType(parentType, text); } - - function maybeCheckExpression(node: Expression) { - if (isBinaryExpression(node)) { - stackIndex++; - workStacks.expr[stackIndex] = node; - workStacks.state[stackIndex] = CheckBinaryExpressionState.MaybeCheckLeft; - workStacks.leftType[stackIndex] = undefined; - } - else { - lastResult = checkExpression(node, checkMode); - } + } + } + // In a variable, parameter or property declaration with a type annotation, + // the contextual type of an initializer expression is the type of the variable, parameter or property. + // Otherwise, in a parameter declaration of a contextually typed function expression, + // the contextual type of an initializer expression is the contextual type of the parameter. + // Otherwise, in a variable or parameter declaration with a binding pattern name, + // the contextual type of an initializer expression is the type implied by the binding pattern. + // Otherwise, in a binding pattern inside a variable or parameter declaration, + // the contextual type of an initializer expression is the type annotation of the containing declaration, if present. + function getContextualTypeForInitializerExpression(node: Expression): ts.Type | undefined { + const declaration = (node.parent); + if (hasInitializer(declaration) && node === declaration.initializer) { + const result = getContextualTypeForVariableLikeDeclaration(declaration); + if (result) { + return result; + } + if (isBindingPattern(declaration.name)) { // This is less a contextual type and more an implied shape - in some cases, this may be undesirable + return getTypeFromBindingPattern(declaration.name, /*includePatternInType*/ true, /*reportErrors*/ false); } } - - function checkGrammarNullishCoalesceWithLogicalExpression(node: BinaryExpression) { - const { left, operatorToken, right } = node; - if (operatorToken.kind === SyntaxKind.QuestionQuestionToken) { - if (isBinaryExpression(left) && (left.operatorToken.kind === SyntaxKind.BarBarToken || left.operatorToken.kind === SyntaxKind.AmpersandAmpersandToken)) { - grammarErrorOnNode(left, Diagnostics._0_and_1_operations_cannot_be_mixed_without_parentheses, tokenToString(left.operatorToken.kind), tokenToString(operatorToken.kind)); - } - if (isBinaryExpression(right) && (right.operatorToken.kind === SyntaxKind.BarBarToken || right.operatorToken.kind === SyntaxKind.AmpersandAmpersandToken)) { - grammarErrorOnNode(right, Diagnostics._0_and_1_operations_cannot_be_mixed_without_parentheses, tokenToString(right.operatorToken.kind), tokenToString(operatorToken.kind)); + return undefined; + } + function getContextualTypeForReturnExpression(node: Expression): ts.Type | undefined { + const func = getContainingFunction(node); + if (func) { + const functionFlags = getFunctionFlags(func); + if (functionFlags & FunctionFlags.Generator) { // AsyncGenerator function or Generator function + return undefined; + } + const contextualReturnType = getContextualReturnType(func); + if (contextualReturnType) { + if (functionFlags & FunctionFlags.Async) { // Async function + const contextualAwaitedType = getAwaitedTypeOfPromise(contextualReturnType); + return contextualAwaitedType && getUnionType([contextualAwaitedType, createPromiseLikeType(contextualAwaitedType)]); } + return contextualReturnType; // Regular function } } - - // Note that this and `checkBinaryExpression` above should behave mostly the same, except this elides some - // expression-wide checks and does not use a work stack to fold nested binary expressions into the same callstack frame - function checkBinaryLikeExpression(left: Expression, operatorToken: Node, right: Expression, checkMode?: CheckMode, errorNode?: Node): Type { - const operator = operatorToken.kind; - if (operator === SyntaxKind.EqualsToken && (left.kind === SyntaxKind.ObjectLiteralExpression || left.kind === SyntaxKind.ArrayLiteralExpression)) { - return checkDestructuringAssignment(left, checkExpression(right, checkMode), checkMode, right.kind === SyntaxKind.ThisKeyword); + return undefined; + } + function getContextualTypeForAwaitOperand(node: AwaitExpression): ts.Type | undefined { + const contextualType = getContextualType(node); + if (contextualType) { + const contextualAwaitedType = getAwaitedType(contextualType); + return contextualAwaitedType && getUnionType([contextualAwaitedType, createPromiseLikeType(contextualAwaitedType)]); + } + return undefined; + } + function getContextualTypeForYieldOperand(node: YieldExpression): ts.Type | undefined { + const func = getContainingFunction(node); + if (func) { + const functionFlags = getFunctionFlags(func); + const contextualReturnType = getContextualReturnType(func); + if (contextualReturnType) { + return node.asteriskToken + ? contextualReturnType + : getIterationTypeOfGeneratorFunctionReturnType(IterationTypeKind.Yield, contextualReturnType, (functionFlags & FunctionFlags.Async) !== 0); } - let leftType: Type; - if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.BarBarToken || operator === SyntaxKind.QuestionQuestionToken) { - leftType = checkTruthinessExpression(left, checkMode); + } + return undefined; + } + function isInParameterInitializerBeforeContainingFunction(node: Node) { + let inBindingInitializer = false; + while (node.parent && !isFunctionLike(node.parent)) { + if (isParameter(node.parent) && (inBindingInitializer || node.parent.initializer === node)) { + return true; } - else { - leftType = checkExpression(left, checkMode); + if (isBindingElement(node.parent) && node.parent.initializer === node) { + inBindingInitializer = true; } - - const rightType = checkExpression(right, checkMode); - return checkBinaryLikeExpressionWorker(left, operatorToken, right, leftType, rightType, errorNode); + node = node.parent; } - - function checkBinaryLikeExpressionWorker( - left: Expression, - operatorToken: Node, - right: Expression, - leftType: Type, - rightType: Type, - errorNode?: Node - ): Type { - const operator = operatorToken.kind; - switch (operator) { - case SyntaxKind.AsteriskToken: - case SyntaxKind.AsteriskAsteriskToken: - case SyntaxKind.AsteriskEqualsToken: - case SyntaxKind.AsteriskAsteriskEqualsToken: - case SyntaxKind.SlashToken: - case SyntaxKind.SlashEqualsToken: - case SyntaxKind.PercentToken: - case SyntaxKind.PercentEqualsToken: - case SyntaxKind.MinusToken: - case SyntaxKind.MinusEqualsToken: - case SyntaxKind.LessThanLessThanToken: - case SyntaxKind.LessThanLessThanEqualsToken: - case SyntaxKind.GreaterThanGreaterThanToken: - case SyntaxKind.GreaterThanGreaterThanEqualsToken: - case SyntaxKind.GreaterThanGreaterThanGreaterThanToken: - case SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: - case SyntaxKind.BarToken: - case SyntaxKind.BarEqualsToken: - case SyntaxKind.CaretToken: - case SyntaxKind.CaretEqualsToken: - case SyntaxKind.AmpersandToken: - case SyntaxKind.AmpersandEqualsToken: - if (leftType === silentNeverType || rightType === silentNeverType) { - return silentNeverType; - } - - leftType = checkNonNullType(leftType, left); - rightType = checkNonNullType(rightType, right); - - let suggestedOperator: SyntaxKind | undefined; - // if a user tries to apply a bitwise operator to 2 boolean operands - // try and return them a helpful suggestion - if ((leftType.flags & TypeFlags.BooleanLike) && - (rightType.flags & TypeFlags.BooleanLike) && - (suggestedOperator = getSuggestedBooleanOperator(operatorToken.kind)) !== undefined) { - error(errorNode || operatorToken, Diagnostics.The_0_operator_is_not_allowed_for_boolean_types_Consider_using_1_instead, tokenToString(operatorToken.kind), tokenToString(suggestedOperator)); - return numberType; + return false; + } + function getContextualIterationType(kind: IterationTypeKind, functionDecl: SignatureDeclaration): ts.Type | undefined { + const isAsync = !!(getFunctionFlags(functionDecl) & FunctionFlags.Async); + const contextualReturnType = getContextualReturnType(functionDecl); + if (contextualReturnType) { + return getIterationTypeOfGeneratorFunctionReturnType(kind, contextualReturnType, isAsync) + || undefined; + } + return undefined; + } + function getContextualReturnType(functionDecl: SignatureDeclaration): ts.Type | undefined { + // If the containing function has a return type annotation, is a constructor, or is a get accessor whose + // corresponding set accessor has a type annotation, return statements in the function are contextually typed + const returnType = getReturnTypeFromAnnotation(functionDecl); + if (returnType) { + return returnType; + } + // Otherwise, if the containing function is contextually typed by a function type with exactly one call signature + // and that call signature is non-generic, return statements are contextually typed by the return type of the signature + const signature = getContextualSignatureForFunctionLikeDeclaration((functionDecl)); + if (signature && !isResolvingReturnTypeOfSignature(signature)) { + return getReturnTypeOfSignature(signature); + } + return undefined; + } + // In a typed function call, an argument or substitution expression is contextually typed by the type of the corresponding parameter. + function getContextualTypeForArgument(callTarget: CallLikeExpression, arg: Expression): ts.Type | undefined { + const args = getEffectiveCallArguments(callTarget); + const argIndex = args.indexOf(arg); // -1 for e.g. the expression of a CallExpression, or the tag of a TaggedTemplateExpression + return argIndex === -1 ? undefined : getContextualTypeForArgumentAtIndex(callTarget, argIndex); + } + function getContextualTypeForArgumentAtIndex(callTarget: CallLikeExpression, argIndex: number): ts.Type { + // If we're already in the process of resolving the given signature, don't resolve again as + // that could cause infinite recursion. Instead, return anySignature. + const signature = getNodeLinks(callTarget).resolvedSignature === resolvingSignature ? resolvingSignature : getResolvedSignature(callTarget); + if (isJsxOpeningLikeElement(callTarget) && argIndex === 0) { + return getEffectiveFirstArgumentForJsxSignature(signature, callTarget); + } + return getTypeAtPosition(signature, argIndex); + } + function getContextualTypeForSubstitutionExpression(template: TemplateExpression, substitutionExpression: Expression) { + if (template.parent.kind === SyntaxKind.TaggedTemplateExpression) { + return getContextualTypeForArgument((template.parent), substitutionExpression); + } + return undefined; + } + function getContextualTypeForBinaryOperand(node: Expression, contextFlags?: ContextFlags): ts.Type | undefined { + const binaryExpression = (node.parent); + const { left, operatorToken, right } = binaryExpression; + switch (operatorToken.kind) { + case SyntaxKind.EqualsToken: + if (node !== right) { + return undefined; + } + const contextSensitive = getIsContextSensitiveAssignmentOrContextType(binaryExpression); + if (!contextSensitive) { + return undefined; + } + return contextSensitive === true ? getTypeOfExpression(left) : contextSensitive; + case SyntaxKind.BarBarToken: + case SyntaxKind.QuestionQuestionToken: + // When an || expression has a contextual type, the operands are contextually typed by that type, except + // when that type originates in a binding pattern, the right operand is contextually typed by the type of + // the left operand. When an || expression has no contextual type, the right operand is contextually typed + // by the type of the left operand, except for the special case of Javascript declarations of the form + // `namespace.prop = namespace.prop || {}`. + const type = getContextualType(binaryExpression, contextFlags); + return node === right && (type && type.pattern || !type && !isDefaultedExpandoInitializer(binaryExpression)) ? + getTypeOfExpression(left) : type; + case SyntaxKind.AmpersandAmpersandToken: + case SyntaxKind.CommaToken: + return node === right ? getContextualType(binaryExpression, contextFlags) : undefined; + default: + return undefined; + } + } + // In an assignment expression, the right operand is contextually typed by the type of the left operand. + // Don't do this for assignment declarations unless there is a type tag on the assignment, to avoid circularity from checking the right operand. + function getIsContextSensitiveAssignmentOrContextType(binaryExpression: BinaryExpression): boolean | ts.Type { + const kind = getAssignmentDeclarationKind(binaryExpression); + switch (kind) { + case AssignmentDeclarationKind.None: + return true; + case AssignmentDeclarationKind.Property: + case AssignmentDeclarationKind.ExportsProperty: + case AssignmentDeclarationKind.Prototype: + case AssignmentDeclarationKind.PrototypeProperty: + // If `binaryExpression.left` was assigned a symbol, then this is a new declaration; otherwise it is an assignment to an existing declaration. + // See `bindStaticPropertyAssignment` in `binder.ts`. + if (!binaryExpression.left.symbol) { + return true; + } + else { + const decl = binaryExpression.left.symbol.valueDeclaration; + if (!decl) { + return false; } - else { - // otherwise just check each operand separately and report errors as normal - const leftOk = checkArithmeticOperandType(left, leftType, Diagnostics.The_left_hand_side_of_an_arithmetic_operation_must_be_of_type_any_number_bigint_or_an_enum_type, /*isAwaitValid*/ true); - const rightOk = checkArithmeticOperandType(right, rightType, Diagnostics.The_right_hand_side_of_an_arithmetic_operation_must_be_of_type_any_number_bigint_or_an_enum_type, /*isAwaitValid*/ true); - let resultType: Type; - // If both are any or unknown, allow operation; assume it will resolve to number - if ((isTypeAssignableToKind(leftType, TypeFlags.AnyOrUnknown) && isTypeAssignableToKind(rightType, TypeFlags.AnyOrUnknown)) || - // Or, if neither could be bigint, implicit coercion results in a number result - !(maybeTypeOfKind(leftType, TypeFlags.BigIntLike) || maybeTypeOfKind(rightType, TypeFlags.BigIntLike)) - ) { - resultType = numberType; - } - // At least one is assignable to bigint, so check that both are - else if (bothAreBigIntLike(leftType, rightType)) { - switch (operator) { - case SyntaxKind.GreaterThanGreaterThanGreaterThanToken: - case SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: - reportOperatorError(); + const lhs = cast(binaryExpression.left, isAccessExpression); + const overallAnnotation = getEffectiveTypeAnnotationNode(decl); + if (overallAnnotation) { + return getTypeFromTypeNode(overallAnnotation); + } + else if (isIdentifier(lhs.expression)) { + const id = lhs.expression; + const parentSymbol = resolveName(id, id.escapedText, SymbolFlags.Value, undefined, id.escapedText, /*isUse*/ true); + if (parentSymbol) { + const annotated = getEffectiveTypeAnnotationNode(parentSymbol.valueDeclaration); + if (annotated) { + const nameStr = getElementOrPropertyAccessName(lhs); + if (nameStr !== undefined) { + const type = getTypeOfPropertyOfContextualType(getTypeFromTypeNode(annotated), nameStr); + return type || false; + } } - resultType = bigintType; - } - // Exactly one of leftType/rightType is assignable to bigint - else { - reportOperatorError(bothAreBigIntLike); - resultType = errorType; - } - if (leftOk && rightOk) { - checkAssignmentOperator(resultType); - } - return resultType; - } - case SyntaxKind.PlusToken: - case SyntaxKind.PlusEqualsToken: - if (leftType === silentNeverType || rightType === silentNeverType) { - return silentNeverType; - } - - if (!isTypeAssignableToKind(leftType, TypeFlags.StringLike) && !isTypeAssignableToKind(rightType, TypeFlags.StringLike)) { - leftType = checkNonNullType(leftType, left); - rightType = checkNonNullType(rightType, right); - } - - let resultType: Type | undefined; - if (isTypeAssignableToKind(leftType, TypeFlags.NumberLike, /*strict*/ true) && isTypeAssignableToKind(rightType, TypeFlags.NumberLike, /*strict*/ true)) { - // Operands of an enum type are treated as having the primitive type Number. - // If both operands are of the Number primitive type, the result is of the Number primitive type. - resultType = numberType; - } - else if (isTypeAssignableToKind(leftType, TypeFlags.BigIntLike, /*strict*/ true) && isTypeAssignableToKind(rightType, TypeFlags.BigIntLike, /*strict*/ true)) { - // If both operands are of the BigInt primitive type, the result is of the BigInt primitive type. - resultType = bigintType; - } - else if (isTypeAssignableToKind(leftType, TypeFlags.StringLike, /*strict*/ true) || isTypeAssignableToKind(rightType, TypeFlags.StringLike, /*strict*/ true)) { - // If one or both operands are of the String primitive type, the result is of the String primitive type. - resultType = stringType; - } - else if (isTypeAny(leftType) || isTypeAny(rightType)) { - // Otherwise, the result is of type Any. - // NOTE: unknown type here denotes error type. Old compiler treated this case as any type so do we. - resultType = leftType === errorType || rightType === errorType ? errorType : anyType; - } - - // Symbols are not allowed at all in arithmetic expressions - if (resultType && !checkForDisallowedESSymbolOperand(operator)) { - return resultType; - } - - if (!resultType) { - // Types that have a reasonably good chance of being a valid operand type. - // If both types have an awaited type of one of these, we'll assume the user - // might be missing an await without doing an exhaustive check that inserting - // await(s) will actually be a completely valid binary expression. - const closeEnoughKind = TypeFlags.NumberLike | TypeFlags.BigIntLike | TypeFlags.StringLike | TypeFlags.AnyOrUnknown; - reportOperatorError((left, right) => - isTypeAssignableToKind(left, closeEnoughKind) && - isTypeAssignableToKind(right, closeEnoughKind)); - return anyType; - } - - if (operator === SyntaxKind.PlusEqualsToken) { - checkAssignmentOperator(resultType); - } - return resultType; - case SyntaxKind.LessThanToken: - case SyntaxKind.GreaterThanToken: - case SyntaxKind.LessThanEqualsToken: - case SyntaxKind.GreaterThanEqualsToken: - if (checkForDisallowedESSymbolOperand(operator)) { - leftType = getBaseTypeOfLiteralType(checkNonNullType(leftType, left)); - rightType = getBaseTypeOfLiteralType(checkNonNullType(rightType, right)); - reportOperatorErrorUnless((left, right) => - isTypeComparableTo(left, right) || isTypeComparableTo(right, left) || ( - isTypeAssignableTo(left, numberOrBigIntType) && isTypeAssignableTo(right, numberOrBigIntType))); - } - return booleanType; - case SyntaxKind.EqualsEqualsToken: - case SyntaxKind.ExclamationEqualsToken: - case SyntaxKind.EqualsEqualsEqualsToken: - case SyntaxKind.ExclamationEqualsEqualsToken: - reportOperatorErrorUnless((left, right) => isTypeEqualityComparableTo(left, right) || isTypeEqualityComparableTo(right, left)); - return booleanType; - - case SyntaxKind.InstanceOfKeyword: - return checkInstanceOfExpression(left, right, leftType, rightType); - case SyntaxKind.InKeyword: - return checkInExpression(left, right, leftType, rightType); - case SyntaxKind.AmpersandAmpersandToken: - return getTypeFacts(leftType) & TypeFacts.Truthy ? - getUnionType([extractDefinitelyFalsyTypes(strictNullChecks ? leftType : getBaseTypeOfLiteralType(rightType)), rightType]) : - leftType; - case SyntaxKind.BarBarToken: - return getTypeFacts(leftType) & TypeFacts.Falsy ? - getUnionType([removeDefinitelyFalsyTypes(leftType), rightType], UnionReduction.Subtype) : - leftType; - case SyntaxKind.QuestionQuestionToken: - return getTypeFacts(leftType) & TypeFacts.EQUndefinedOrNull ? - getUnionType([getNonNullableType(leftType), rightType], UnionReduction.Subtype) : - leftType; - case SyntaxKind.EqualsToken: - const declKind = isBinaryExpression(left.parent) ? getAssignmentDeclarationKind(left.parent) : AssignmentDeclarationKind.None; - checkAssignmentDeclaration(declKind, rightType); - if (isAssignmentDeclaration(declKind)) { - if (!(rightType.flags & TypeFlags.Object) || - declKind !== AssignmentDeclarationKind.ModuleExports && - declKind !== AssignmentDeclarationKind.Prototype && - !isEmptyObjectType(rightType) && - !isFunctionObjectType(rightType as ObjectType) && - !(getObjectFlags(rightType) & ObjectFlags.Class)) { - // don't check assignability of module.exports=, C.prototype=, or expando types because they will necessarily be incomplete - checkAssignmentOperator(rightType); + return false; } - return leftType; - } - else { - checkAssignmentOperator(rightType); - return getRegularTypeOfObjectLiteral(rightType); - } - case SyntaxKind.CommaToken: - if (!compilerOptions.allowUnreachableCode && isSideEffectFree(left) && !isEvalNode(right)) { - error(left, Diagnostics.Left_side_of_comma_operator_is_unused_and_has_no_side_effects); } - return rightType; - - default: - return Debug.fail(); - } - - function bothAreBigIntLike(left: Type, right: Type): boolean { - return isTypeAssignableToKind(left, TypeFlags.BigIntLike) && isTypeAssignableToKind(right, TypeFlags.BigIntLike); - } - - function checkAssignmentDeclaration(kind: AssignmentDeclarationKind, rightType: Type) { - if (kind === AssignmentDeclarationKind.ModuleExports) { - for (const prop of getPropertiesOfObjectType(rightType)) { - const propType = getTypeOfSymbol(prop); - if (propType.symbol && propType.symbol.flags & SymbolFlags.Class) { - const name = prop.escapedName; - const symbol = resolveName(prop.valueDeclaration, name, SymbolFlags.Type, undefined, name, /*isUse*/ false); - if (symbol && symbol.declarations.some(isJSDocTypedefTag)) { - grammarErrorOnNode(symbol.declarations[0], Diagnostics.Duplicate_identifier_0, unescapeLeadingUnderscores(name)); - return grammarErrorOnNode(prop.valueDeclaration, Diagnostics.Duplicate_identifier_0, unescapeLeadingUnderscores(name)); - } + return !isInJSFile(decl); + } + case AssignmentDeclarationKind.ModuleExports: + case AssignmentDeclarationKind.ThisProperty: + if (!binaryExpression.symbol) + return true; + if (binaryExpression.symbol.valueDeclaration) { + const annotated = getEffectiveTypeAnnotationNode(binaryExpression.symbol.valueDeclaration); + if (annotated) { + const type = getTypeFromTypeNode(annotated); + if (type) { + return type; } } } - } - - function isEvalNode(node: Expression) { - return node.kind === SyntaxKind.Identifier && (node as Identifier).escapedText === "eval"; - } - - // Return true if there was no error, false if there was an error. - function checkForDisallowedESSymbolOperand(operator: SyntaxKind): boolean { - const offendingSymbolOperand = - maybeTypeOfKind(leftType, TypeFlags.ESSymbolLike) ? left : - maybeTypeOfKind(rightType, TypeFlags.ESSymbolLike) ? right : - undefined; - - if (offendingSymbolOperand) { - error(offendingSymbolOperand, Diagnostics.The_0_operator_cannot_be_applied_to_type_symbol, tokenToString(operator)); + if (kind === AssignmentDeclarationKind.ModuleExports) + return false; + const thisAccess = cast(binaryExpression.left, isAccessExpression); + if (!isObjectLiteralMethod(getThisContainer(thisAccess.expression, /*includeArrowFunctions*/ false))) { return false; } - - return true; - } - - function getSuggestedBooleanOperator(operator: SyntaxKind): SyntaxKind | undefined { - switch (operator) { - case SyntaxKind.BarToken: - case SyntaxKind.BarEqualsToken: - return SyntaxKind.BarBarToken; - case SyntaxKind.CaretToken: - case SyntaxKind.CaretEqualsToken: - return SyntaxKind.ExclamationEqualsEqualsToken; - case SyntaxKind.AmpersandToken: - case SyntaxKind.AmpersandEqualsToken: - return SyntaxKind.AmpersandAmpersandToken; - default: - return undefined; + const thisType = checkThisExpression(thisAccess.expression); + const nameStr = getElementOrPropertyAccessName(thisAccess); + return nameStr !== undefined && thisType && getTypeOfPropertyOfContextualType(thisType, nameStr) || false; + case AssignmentDeclarationKind.ObjectDefinePropertyValue: + case AssignmentDeclarationKind.ObjectDefinePropertyExports: + case AssignmentDeclarationKind.ObjectDefinePrototypeProperty: + return Debug.fail("Does not apply"); + default: + return Debug.assertNever(kind); + } + } + function getTypeOfPropertyOfContextualType(type: ts.Type, name: __String) { + return mapType(type, t => { + if (isGenericMappedType(t)) { + const constraint = getConstraintTypeFromMappedType(t); + const constraintOfConstraint = getBaseConstraintOfType(constraint) || constraint; + const propertyNameType = getLiteralType(unescapeLeadingUnderscores(name)); + if (isTypeAssignableTo(propertyNameType, constraintOfConstraint)) { + return substituteIndexedMappedType(t, propertyNameType); + } + } + else if (t.flags & TypeFlags.StructuredType) { + const prop = getPropertyOfType(t, name); + if (prop) { + return getTypeOfSymbol(prop); } - } - - function checkAssignmentOperator(valueType: Type): void { - if (produceDiagnostics && isAssignmentOperator(operator)) { - // TypeScript 1.0 spec (April 2014): 4.17 - // An assignment of the form - // VarExpr = ValueExpr - // requires VarExpr to be classified as a reference - // A compound assignment furthermore requires VarExpr to be classified as a reference (section 4.1) - // and the type of the non-compound operation to be assignable to the type of VarExpr. - - if (checkReferenceExpression(left, - Diagnostics.The_left_hand_side_of_an_assignment_expression_must_be_a_variable_or_a_property_access, - Diagnostics.The_left_hand_side_of_an_assignment_expression_may_not_be_an_optional_property_access) - && (!isIdentifier(left) || unescapeLeadingUnderscores(left.escapedText) !== "exports")) { - // to avoid cascading errors check assignability only if 'isReference' check succeeded and no errors were reported - checkTypeAssignableToAndOptionallyElaborate(valueType, leftType, left, right); + if (isTupleType(t)) { + const restType = getRestTypeOfTupleType(t); + if (restType && isNumericLiteralName(name) && +name >= 0) { + return restType; } } + return isNumericLiteralName(name) && getIndexTypeOfContextualType(t, IndexKind.Number) || + getIndexTypeOfContextualType(t, IndexKind.String); } - - function isAssignmentDeclaration(kind: AssignmentDeclarationKind) { - switch (kind) { - case AssignmentDeclarationKind.ModuleExports: - return true; - case AssignmentDeclarationKind.ExportsProperty: - case AssignmentDeclarationKind.Property: - case AssignmentDeclarationKind.Prototype: - case AssignmentDeclarationKind.PrototypeProperty: - case AssignmentDeclarationKind.ThisProperty: - const symbol = getSymbolOfNode(left); - const init = getAssignedExpandoInitializer(right); - return init && isObjectLiteralExpression(init) && - symbol && hasEntries(symbol.exports); - default: - return false; - } - } - - /** - * Returns true if an error is reported - */ - function reportOperatorErrorUnless(typesAreCompatible: (left: Type, right: Type) => boolean): boolean { - if (!typesAreCompatible(leftType, rightType)) { - reportOperatorError(typesAreCompatible); - return true; - } - return false; - } - - function reportOperatorError(isRelated?: (left: Type, right: Type) => boolean) { - let wouldWorkWithAwait = false; - const errNode = errorNode || operatorToken; - if (isRelated) { - const awaitedLeftType = getAwaitedType(leftType); - const awaitedRightType = getAwaitedType(rightType); - wouldWorkWithAwait = !(awaitedLeftType === leftType && awaitedRightType === rightType) - && !!(awaitedLeftType && awaitedRightType) - && isRelated(awaitedLeftType, awaitedRightType); - } - - let effectiveLeft = leftType; - let effectiveRight = rightType; - if (!wouldWorkWithAwait && isRelated) { - [effectiveLeft, effectiveRight] = getBaseTypesIfUnrelated(leftType, rightType, isRelated); - } - const [leftStr, rightStr] = getTypeNamesForErrorDisplay(effectiveLeft, effectiveRight); - if (!tryGiveBetterPrimaryError(errNode, wouldWorkWithAwait, leftStr, rightStr)) { - errorAndMaybeSuggestAwait( - errNode, - wouldWorkWithAwait, - Diagnostics.Operator_0_cannot_be_applied_to_types_1_and_2, - tokenToString(operatorToken.kind), - leftStr, - rightStr, - ); - } - } - - function tryGiveBetterPrimaryError(errNode: Node, maybeMissingAwait: boolean, leftStr: string, rightStr: string) { - let typeName: string | undefined; - switch (operatorToken.kind) { - case SyntaxKind.EqualsEqualsEqualsToken: - case SyntaxKind.EqualsEqualsToken: - typeName = "false"; - break; - case SyntaxKind.ExclamationEqualsEqualsToken: - case SyntaxKind.ExclamationEqualsToken: - typeName = "true"; - } - - if (typeName) { - return errorAndMaybeSuggestAwait( - errNode, - maybeMissingAwait, - Diagnostics.This_condition_will_always_return_0_since_the_types_1_and_2_have_no_overlap, - typeName, leftStr, rightStr); - } - - return undefined; - } + return undefined; + }, /*noReductions*/ true); + } + function getIndexTypeOfContextualType(type: ts.Type, kind: IndexKind) { + return mapType(type, t => getIndexTypeOfStructuredType(t, kind), /*noReductions*/ true); + } + // In an object literal contextually typed by a type T, the contextual type of a property assignment is the type of + // the matching property in T, if one exists. Otherwise, it is the type of the numeric index signature in T, if one + // exists. Otherwise, it is the type of the string index signature in T, if one exists. + function getContextualTypeForObjectLiteralMethod(node: MethodDeclaration, contextFlags?: ContextFlags): ts.Type | undefined { + Debug.assert(isObjectLiteralMethod(node)); + if (node.flags & NodeFlags.InWithStatement) { + // We cannot answer semantic questions within a with block, do not proceed any further + return undefined; } - - function getBaseTypesIfUnrelated(leftType: Type, rightType: Type, isRelated: (left: Type, right: Type) => boolean): [Type, Type] { - let effectiveLeft = leftType; - let effectiveRight = rightType; - const leftBase = getBaseTypeOfLiteralType(leftType); - const rightBase = getBaseTypeOfLiteralType(rightType); - if (!isRelated(leftBase, rightBase)) { - effectiveLeft = leftBase; - effectiveRight = rightBase; - } - return [ effectiveLeft, effectiveRight ]; + return getContextualTypeForObjectLiteralElement(node, contextFlags); + } + function getContextualTypeForObjectLiteralElement(element: ObjectLiteralElementLike, contextFlags?: ContextFlags) { + const objectLiteral = (element.parent); + const type = getApparentTypeOfContextualType(objectLiteral, contextFlags); + if (type) { + if (!hasNonBindableDynamicName(element)) { + // For a (non-symbol) computed property, there is no reason to look up the name + // in the type. It will just be "__computed", which does not appear in any + // SymbolTable. + const symbolName = getSymbolOfNode(element).escapedName; + const propertyType = getTypeOfPropertyOfContextualType(type, symbolName); + if (propertyType) { + return propertyType; + } + } + return isNumericName(element.name!) && getIndexTypeOfContextualType(type, IndexKind.Number) || + getIndexTypeOfContextualType(type, IndexKind.String); + } + return undefined; + } + // In an array literal contextually typed by a type T, the contextual type of an element expression at index N is + // the type of the property with the numeric name N in T, if one exists. Otherwise, if T has a numeric index signature, + // it is the type of the numeric index signature in T. Otherwise, in ES6 and higher, the contextual type is the iterated + // type of T. + function getContextualTypeForElementExpression(arrayContextualType: ts.Type | undefined, index: number): ts.Type | undefined { + return arrayContextualType && (getTypeOfPropertyOfContextualType(arrayContextualType, ("" + index as __String)) + || getIteratedTypeOrElementType(IterationUse.Element, arrayContextualType, undefinedType, /*errorNode*/ undefined, /*checkAssignability*/ false)); + } + // In a contextually typed conditional expression, the true/false expressions are contextually typed by the same type. + function getContextualTypeForConditionalOperand(node: Expression, contextFlags?: ContextFlags): ts.Type | undefined { + const conditional = (node.parent); + return node === conditional.whenTrue || node === conditional.whenFalse ? getContextualType(conditional, contextFlags) : undefined; + } + function getContextualTypeForChildJsxExpression(node: JsxElement, child: JsxChild) { + const attributesType = getApparentTypeOfContextualType(node.openingElement.tagName); + // JSX expression is in children of JSX Element, we will look for an "children" atttribute (we get the name from JSX.ElementAttributesProperty) + const jsxChildrenPropertyName = getJsxElementChildrenPropertyName(getJsxNamespaceAt(node)); + if (!(attributesType && !isTypeAny(attributesType) && jsxChildrenPropertyName && jsxChildrenPropertyName !== "")) { + return undefined; } - - function checkYieldExpression(node: YieldExpression): Type { - // Grammar checking - if (produceDiagnostics) { - if (!(node.flags & NodeFlags.YieldContext)) { - grammarErrorOnFirstToken(node, Diagnostics.A_yield_expression_is_only_allowed_in_a_generator_body); - } - - if (isInParameterInitializerBeforeContainingFunction(node)) { - error(node, Diagnostics.yield_expressions_cannot_be_used_in_a_parameter_initializer); - } - } - - const func = getContainingFunction(node); - if (!func) return anyType; - const functionFlags = getFunctionFlags(func); - - if (!(functionFlags & FunctionFlags.Generator)) { - // If the user's code is syntactically correct, the func should always have a star. After all, we are in a yield context. - return anyType; - } - - const isAsync = (functionFlags & FunctionFlags.Async) !== 0; - if (node.asteriskToken) { - // Async generator functions prior to ESNext require the __await, __asyncDelegator, - // and __asyncValues helpers - if (isAsync && languageVersion < ScriptTarget.ESNext) { - checkExternalEmitHelpers(node, ExternalEmitHelpers.AsyncDelegatorIncludes); - } - - // Generator functions prior to ES2015 require the __values helper - if (!isAsync && languageVersion < ScriptTarget.ES2015 && compilerOptions.downlevelIteration) { - checkExternalEmitHelpers(node, ExternalEmitHelpers.Values); - } + const realChildren = getSemanticJsxChildren(node.children); + const childIndex = realChildren.indexOf(child); + const childFieldType = getTypeOfPropertyOfContextualType(attributesType, jsxChildrenPropertyName); + return childFieldType && (realChildren.length === 1 ? childFieldType : mapType(childFieldType, t => { + if (isArrayLikeType(t)) { + return getIndexedAccessType(t, getLiteralType(childIndex)); } - - // There is no point in doing an assignability check if the function - // has no explicit return type because the return type is directly computed - // from the yield expressions. - const returnType = getReturnTypeFromAnnotation(func); - const iterationTypes = returnType && getIterationTypesOfGeneratorFunctionReturnType(returnType, isAsync); - const signatureYieldType = iterationTypes && iterationTypes.yieldType || anyType; - const signatureNextType = iterationTypes && iterationTypes.nextType || anyType; - const resolvedSignatureNextType = isAsync ? getAwaitedType(signatureNextType) || anyType : signatureNextType; - const yieldExpressionType = node.expression ? checkExpression(node.expression) : undefinedWideningType; - const yieldedType = getYieldedTypeOfYieldExpression(node, yieldExpressionType, resolvedSignatureNextType, isAsync); - if (returnType && yieldedType) { - checkTypeAssignableToAndOptionallyElaborate(yieldedType, signatureYieldType, node.expression || node, node.expression); - } - - if (node.asteriskToken) { - const use = isAsync ? IterationUse.AsyncYieldStar : IterationUse.YieldStar; - return getIterationTypeOfIterable(use, IterationTypeKind.Return, yieldExpressionType, node.expression) - || anyType; + else { + return t; } - else if (returnType) { - return getIterationTypeOfGeneratorFunctionReturnType(IterationTypeKind.Next, returnType, isAsync) - || anyType; + }, /*noReductions*/ true)); + } + function getContextualTypeForJsxExpression(node: JsxExpression): ts.Type | undefined { + const exprParent = node.parent; + return isJsxAttributeLike(exprParent) + ? getContextualType(node) + : isJsxElement(exprParent) + ? getContextualTypeForChildJsxExpression(exprParent, node) + : undefined; + } + function getContextualTypeForJsxAttribute(attribute: JsxAttribute | JsxSpreadAttribute): ts.Type | undefined { + // When we trying to resolve JsxOpeningLikeElement as a stateless function element, we will already give its attributes a contextual type + // which is a type of the parameter of the signature we are trying out. + // If there is no contextual type (e.g. we are trying to resolve stateful component), get attributes type from resolving element's tagName + if (isJsxAttribute(attribute)) { + const attributesType = getApparentTypeOfContextualType(attribute.parent); + if (!attributesType || isTypeAny(attributesType)) { + return undefined; } - - return getContextualIterationType(IterationTypeKind.Next, func) || anyType; + return getTypeOfPropertyOfContextualType(attributesType, attribute.name.escapedText); } - - function checkConditionalExpression(node: ConditionalExpression, checkMode?: CheckMode): Type { - const type = checkTruthinessExpression(node.condition); - checkTestingKnownTruthyCallableType(node.condition, node.whenTrue, type); - const type1 = checkExpression(node.whenTrue, checkMode); - const type2 = checkExpression(node.whenFalse, checkMode); - return getUnionType([type1, type2], UnionReduction.Subtype); + else { + return getContextualType(attribute.parent); } - - function checkTemplateExpression(node: TemplateExpression): Type { - // We just want to check each expressions, but we are unconcerned with - // the type of each expression, as any value may be coerced into a string. - // It is worth asking whether this is what we really want though. - // A place where we actually *are* concerned with the expressions' types are - // in tagged templates. - forEach(node.templateSpans, templateSpan => { - if (maybeTypeOfKind(checkExpression(templateSpan.expression), TypeFlags.ESSymbolLike)) { - error(templateSpan.expression, Diagnostics.Implicit_conversion_of_a_symbol_to_a_string_will_fail_at_runtime_Consider_wrapping_this_expression_in_String); - } - }); - - return stringType; + } + // Return true if the given expression is possibly a discriminant value. We limit the kinds of + // expressions we check to those that don't depend on their contextual type in order not to cause + // recursive (and possibly infinite) invocations of getContextualType. + function isPossiblyDiscriminantValue(node: Expression): boolean { + switch (node.kind) { + case SyntaxKind.StringLiteral: + case SyntaxKind.NumericLiteral: + case SyntaxKind.BigIntLiteral: + case SyntaxKind.NoSubstitutionTemplateLiteral: + case SyntaxKind.TrueKeyword: + case SyntaxKind.FalseKeyword: + case SyntaxKind.NullKeyword: + case SyntaxKind.Identifier: + case SyntaxKind.UndefinedKeyword: + return true; + case SyntaxKind.PropertyAccessExpression: + case SyntaxKind.ParenthesizedExpression: + return isPossiblyDiscriminantValue((node).expression); + case SyntaxKind.JsxExpression: + return !(node as JsxExpression).expression || isPossiblyDiscriminantValue(((node as JsxExpression).expression!)); } - - function getContextNode(node: Expression): Node { - if (node.kind === SyntaxKind.JsxAttributes && !isJsxSelfClosingElement(node.parent)) { - return node.parent.parent; // Needs to be the root JsxElement, so it encompasses the attributes _and_ the children (which are essentially part of the attributes) + return false; + } + function discriminateContextualTypeByObjectMembers(node: ObjectLiteralExpression, contextualType: UnionType) { + return discriminateTypeByDiscriminableItems(contextualType, map(filter(node.properties, p => !!p.symbol && p.kind === SyntaxKind.PropertyAssignment && isPossiblyDiscriminantValue(p.initializer) && isDiscriminantProperty(contextualType, p.symbol.escapedName)), prop => ([() => checkExpression((prop as PropertyAssignment).initializer), prop.symbol.escapedName] as [() => ts.Type, __String])), isTypeAssignableTo, contextualType); + } + function discriminateContextualTypeByJSXAttributes(node: JsxAttributes, contextualType: UnionType) { + return discriminateTypeByDiscriminableItems(contextualType, map(filter(node.properties, p => !!p.symbol && p.kind === SyntaxKind.JsxAttribute && isDiscriminantProperty(contextualType, p.symbol.escapedName) && (!p.initializer || isPossiblyDiscriminantValue(p.initializer))), prop => ([!(prop as JsxAttribute).initializer ? (() => trueType) : (() => checkExpression(((prop as JsxAttribute).initializer!))), prop.symbol.escapedName] as [() => ts.Type, __String])), isTypeAssignableTo, contextualType); + } + // Return the contextual type for a given expression node. During overload resolution, a contextual type may temporarily + // be "pushed" onto a node using the contextualType property. + function getApparentTypeOfContextualType(node: Expression | MethodDeclaration, contextFlags?: ContextFlags): ts.Type | undefined { + const contextualType = isObjectLiteralMethod(node) ? + getContextualTypeForObjectLiteralMethod(node, contextFlags) : + getContextualType(node, contextFlags); + const instantiatedType = instantiateContextualType(contextualType, node, contextFlags); + if (instantiatedType && !(contextFlags && contextFlags & ContextFlags.NoConstraints && instantiatedType.flags & TypeFlags.TypeVariable)) { + const apparentType = mapType(instantiatedType, getApparentType, /*noReductions*/ true); + if (apparentType.flags & TypeFlags.Union) { + if (isObjectLiteralExpression(node)) { + return discriminateContextualTypeByObjectMembers(node, (apparentType as UnionType)); + } + else if (isJsxAttributes(node)) { + return discriminateContextualTypeByJSXAttributes(node, (apparentType as UnionType)); + } } - return node; + return apparentType; } - - function checkExpressionWithContextualType(node: Expression, contextualType: Type, inferenceContext: InferenceContext | undefined, checkMode: CheckMode): Type { - const context = getContextNode(node); - const saveContextualType = context.contextualType; - const saveInferenceContext = context.inferenceContext; - try { - context.contextualType = contextualType; - context.inferenceContext = inferenceContext; - const type = checkExpression(node, checkMode | CheckMode.Contextual | (inferenceContext ? CheckMode.Inferential : 0)); - // We strip literal freshness when an appropriate contextual type is present such that contextually typed - // literals always preserve their literal types (otherwise they might widen during type inference). An alternative - // here would be to not mark contextually typed literals as fresh in the first place. - const result = maybeTypeOfKind(type, TypeFlags.Literal) && isLiteralOfContextualType(type, instantiateContextualType(contextualType, node)) ? - getRegularTypeOfLiteralType(type) : type; - return result; - } - finally { - // In the event our operation is canceled or some other exception occurs, reset the contextual type - // so that we do not accidentally hold onto an instance of the checker, as a Type created in the services layer - // may hold onto the checker that created it. - context.contextualType = saveContextualType; - context.inferenceContext = saveInferenceContext; - } + } + // If the given contextual type contains instantiable types and if a mapper representing + // return type inferences is available, instantiate those types using that mapper. + function instantiateContextualType(contextualType: ts.Type | undefined, node: Node, contextFlags?: ContextFlags): ts.Type | undefined { + if (contextualType && maybeTypeOfKind(contextualType, TypeFlags.Instantiable)) { + const inferenceContext = getInferenceContext(node); + // If no inferences have been made, nothing is gained from instantiating as type parameters + // would just be replaced with their defaults similar to the apparent type. + if (inferenceContext && some(inferenceContext.inferences, hasInferenceCandidates)) { + // For contextual signatures we incorporate all inferences made so far, e.g. from return + // types as well as arguments to the left in a function call. + if (contextFlags && contextFlags & ContextFlags.Signature) { + return instantiateInstantiableTypes(contextualType, inferenceContext.nonFixingMapper); + } + // For other purposes (e.g. determining whether to produce literal types) we only + // incorporate inferences made from the return type in a function call. + if (inferenceContext.returnMapper) { + return instantiateInstantiableTypes(contextualType, inferenceContext.returnMapper); + } + } + } + return contextualType; + } + // This function is similar to instantiateType, except that (a) it only instantiates types that + // are classified as instantiable (i.e. it doesn't instantiate object types), and (b) it performs + // no reductions on instantiated union types. + function instantiateInstantiableTypes(type: ts.Type, mapper: TypeMapper): ts.Type { + if (type.flags & TypeFlags.Instantiable) { + return instantiateType(type, mapper); } - - function checkExpressionCached(node: Expression | QualifiedName, checkMode?: CheckMode): Type { - const links = getNodeLinks(node); - if (!links.resolvedType) { - if (checkMode && checkMode !== CheckMode.Normal) { - return checkExpression(node, checkMode); - } - // When computing a type that we're going to cache, we need to ignore any ongoing control flow - // analysis because variables may have transient types in indeterminable states. Moving flowLoopStart - // to the top of the stack ensures all transient types are computed from a known point. - const saveFlowLoopStart = flowLoopStart; - const saveFlowTypeCache = flowTypeCache; - flowLoopStart = flowLoopCount; - flowTypeCache = undefined; - links.resolvedType = checkExpression(node, checkMode); - flowTypeCache = saveFlowTypeCache; - flowLoopStart = saveFlowLoopStart; - } - return links.resolvedType; + if (type.flags & TypeFlags.Union) { + return getUnionType(map((type).types, t => instantiateInstantiableTypes(t, mapper)), UnionReduction.None); } - - function isTypeAssertion(node: Expression) { - node = skipParentheses(node); - return node.kind === SyntaxKind.TypeAssertionExpression || node.kind === SyntaxKind.AsExpression; + if (type.flags & TypeFlags.Intersection) { + return getIntersectionType(map((type).types, t => instantiateInstantiableTypes(t, mapper))); } - - function checkDeclarationInitializer(declaration: HasExpressionInitializer, contextualType?: Type | undefined) { - const initializer = getEffectiveInitializer(declaration)!; - const type = getQuickTypeOfExpression(initializer) || - (contextualType ? checkExpressionWithContextualType(initializer, contextualType, /*inferenceContext*/ undefined, CheckMode.Normal) : checkExpressionCached(initializer)); - return isParameter(declaration) && declaration.name.kind === SyntaxKind.ArrayBindingPattern && - isTupleType(type) && !type.target.hasRestElement && getTypeReferenceArity(type) < declaration.name.elements.length ? - padTupleType(type, declaration.name) : type; + return type; + } + /** + * Whoa! Do you really want to use this function? + * + * Unless you're trying to get the *non-apparent* type for a + * value-literal type or you're authoring relevant portions of this algorithm, + * you probably meant to use 'getApparentTypeOfContextualType'. + * Otherwise this may not be very useful. + * + * In cases where you *are* working on this function, you should understand + * when it is appropriate to use 'getContextualType' and 'getApparentTypeOfContextualType'. + * + * - Use 'getContextualType' when you are simply going to propagate the result to the expression. + * - Use 'getApparentTypeOfContextualType' when you're going to need the members of the type. + * + * @param node the expression whose contextual type will be returned. + * @returns the contextual type of an expression. + */ + function getContextualType(node: Expression, contextFlags?: ContextFlags): ts.Type | undefined { + if (node.flags & NodeFlags.InWithStatement) { + // We cannot answer semantic questions within a with block, do not proceed any further + return undefined; } - - function padTupleType(type: TupleTypeReference, pattern: ArrayBindingPattern) { - const patternElements = pattern.elements; - const arity = getTypeReferenceArity(type); - const elementTypes = arity ? getTypeArguments(type).slice() : []; - for (let i = arity; i < patternElements.length; i++) { - const e = patternElements[i]; - if (i < patternElements.length - 1 || !(e.kind === SyntaxKind.BindingElement && e.dotDotDotToken)) { - elementTypes.push(!isOmittedExpression(e) && hasDefaultValue(e) ? getTypeFromBindingElement(e, /*includePatternInType*/ false, /*reportErrors*/ false) : anyType); - if (!isOmittedExpression(e) && !hasDefaultValue(e)) { - reportImplicitAny(e, anyType); - } + if (node.contextualType) { + return node.contextualType; + } + const { parent } = node; + switch (parent.kind) { + case SyntaxKind.VariableDeclaration: + case SyntaxKind.Parameter: + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.PropertySignature: + case SyntaxKind.BindingElement: + return getContextualTypeForInitializerExpression(node); + case SyntaxKind.ArrowFunction: + case SyntaxKind.ReturnStatement: + return getContextualTypeForReturnExpression(node); + case SyntaxKind.YieldExpression: + return getContextualTypeForYieldOperand((parent)); + case SyntaxKind.AwaitExpression: + return getContextualTypeForAwaitOperand((parent)); + case SyntaxKind.CallExpression: + if ((parent).expression.kind === SyntaxKind.ImportKeyword) { + return stringType; + } + /* falls through */ + case SyntaxKind.NewExpression: + return getContextualTypeForArgument((parent), node); + case SyntaxKind.TypeAssertionExpression: + case SyntaxKind.AsExpression: + return isConstTypeReference((parent).type) ? undefined : getTypeFromTypeNode((parent).type); + case SyntaxKind.BinaryExpression: + return getContextualTypeForBinaryOperand(node, contextFlags); + case SyntaxKind.PropertyAssignment: + case SyntaxKind.ShorthandPropertyAssignment: + return getContextualTypeForObjectLiteralElement((parent), contextFlags); + case SyntaxKind.SpreadAssignment: + return getApparentTypeOfContextualType((parent.parent as ObjectLiteralExpression), contextFlags); + case SyntaxKind.ArrayLiteralExpression: { + const arrayLiteral = (parent); + const type = getApparentTypeOfContextualType(arrayLiteral, contextFlags); + return getContextualTypeForElementExpression(type, indexOfNode(arrayLiteral.elements, node)); + } + case SyntaxKind.ConditionalExpression: + return getContextualTypeForConditionalOperand(node, contextFlags); + case SyntaxKind.TemplateSpan: + Debug.assert(parent.parent.kind === SyntaxKind.TemplateExpression); + return getContextualTypeForSubstitutionExpression((parent.parent), node); + case SyntaxKind.ParenthesizedExpression: { + // Like in `checkParenthesizedExpression`, an `/** @type {xyz} */` comment before a parenthesized expression acts as a type cast. + const tag = isInJSFile(parent) ? getJSDocTypeTag(parent) : undefined; + return tag ? getTypeFromTypeNode(tag.typeExpression.type) : getContextualType((parent), contextFlags); + } + case SyntaxKind.JsxExpression: + return getContextualTypeForJsxExpression((parent)); + case SyntaxKind.JsxAttribute: + case SyntaxKind.JsxSpreadAttribute: + return getContextualTypeForJsxAttribute((parent)); + case SyntaxKind.JsxOpeningElement: + case SyntaxKind.JsxSelfClosingElement: + return getContextualJsxElementAttributesType((parent), contextFlags); + } + return undefined; + } + function getInferenceContext(node: Node) { + const ancestor = findAncestor(node, n => !!n.inferenceContext); + return ancestor && ancestor.inferenceContext!; + } + function getContextualJsxElementAttributesType(node: JsxOpeningLikeElement, contextFlags?: ContextFlags) { + if (isJsxOpeningElement(node) && node.parent.contextualType && contextFlags !== ContextFlags.Completions) { + // Contextually applied type is moved from attributes up to the outer jsx attributes so when walking up from the children they get hit + // _However_ to hit them from the _attributes_ we must look for them here; otherwise we'll used the declared type + // (as below) instead! + return node.parent.contextualType; + } + return getContextualTypeForArgumentAtIndex(node, 0); + } + function getEffectiveFirstArgumentForJsxSignature(signature: ts.Signature, node: JsxOpeningLikeElement) { + return getJsxReferenceKind(node) !== JsxReferenceKind.Component + ? getJsxPropsTypeFromCallSignature(signature, node) + : getJsxPropsTypeFromClassType(signature, node); + } + function getJsxPropsTypeFromCallSignature(sig: ts.Signature, context: JsxOpeningLikeElement) { + let propsType = getTypeOfFirstParameterOfSignatureWithFallback(sig, unknownType); + propsType = getJsxManagedAttributesFromLocatedAttributes(context, getJsxNamespaceAt(context), propsType); + const intrinsicAttribs = getJsxType(JsxNames.IntrinsicAttributes, context); + if (intrinsicAttribs !== errorType) { + propsType = intersectTypes(intrinsicAttribs, propsType); + } + return propsType; + } + function getJsxPropsTypeForSignatureFromMember(sig: ts.Signature, forcedLookupLocation: __String) { + if (sig.unionSignatures) { + // JSX Elements using the legacy `props`-field based lookup (eg, react class components) need to treat the `props` member as an input + // instead of an output position when resolving the signature. We need to go back to the input signatures of the composite signature, + // get the type of `props` on each return type individually, and then _intersect them_, rather than union them (as would normally occur + // for a union signature). It's an unfortunate quirk of looking in the output of the signature for the type we want to use for the input. + // The default behavior of `getTypeOfFirstParameterOfSignatureWithFallback` when no `props` member name is defined is much more sane. + const results: ts.Type[] = []; + for (const signature of sig.unionSignatures) { + const instance = getReturnTypeOfSignature(signature); + if (isTypeAny(instance)) { + return instance; + } + const propType = getTypeOfPropertyOfType(instance, forcedLookupLocation); + if (!propType) { + return; } + results.push(propType); } - return createTupleType(elementTypes, type.target.minLength, /*hasRestElement*/ false, type.target.readonly); + return getIntersectionType(results); } - - function widenTypeInferredFromInitializer(declaration: HasExpressionInitializer, type: Type) { - const widened = getCombinedNodeFlags(declaration) & NodeFlags.Const || isDeclarationReadonly(declaration) ? type : getWidenedLiteralType(type); - if (isInJSFile(declaration)) { - if (widened.flags & TypeFlags.Nullable) { - reportImplicitAny(declaration, anyType); - return anyType; - } - else if (isEmptyArrayLiteralType(widened)) { - reportImplicitAny(declaration, anyArrayType); - return anyArrayType; - } + const instanceType = getReturnTypeOfSignature(sig); + return isTypeAny(instanceType) ? instanceType : getTypeOfPropertyOfType(instanceType, forcedLookupLocation); + } + function getStaticTypeOfReferencedJsxConstructor(context: JsxOpeningLikeElement) { + if (isJsxIntrinsicIdentifier(context.tagName)) { + const result = getIntrinsicAttributesTypeFromJsxOpeningLikeElement(context); + const fakeSignature = createSignatureForJSXIntrinsic(context, result); + return getOrCreateTypeFromSignature(fakeSignature); + } + const tagType = checkExpressionCached(context.tagName); + if (tagType.flags & TypeFlags.StringLiteral) { + const result = getIntrinsicAttributesTypeFromStringLiteralType((tagType as StringLiteralType), context); + if (!result) { + return errorType; } - return widened; + const fakeSignature = createSignatureForJSXIntrinsic(context, result); + return getOrCreateTypeFromSignature(fakeSignature); } - - function isLiteralOfContextualType(candidateType: Type, contextualType: Type | undefined): boolean { - if (contextualType) { - if (contextualType.flags & TypeFlags.UnionOrIntersection) { - const types = (contextualType).types; - return some(types, t => isLiteralOfContextualType(candidateType, t)); - } - if (contextualType.flags & TypeFlags.InstantiableNonPrimitive) { - // If the contextual type is a type variable constrained to a primitive type, consider - // this a literal context for literals of that primitive type. For example, given a - // type parameter 'T extends string', infer string literal types for T. - const constraint = getBaseConstraintOfType(contextualType) || unknownType; - return maybeTypeOfKind(constraint, TypeFlags.String) && maybeTypeOfKind(candidateType, TypeFlags.StringLiteral) || - maybeTypeOfKind(constraint, TypeFlags.Number) && maybeTypeOfKind(candidateType, TypeFlags.NumberLiteral) || - maybeTypeOfKind(constraint, TypeFlags.BigInt) && maybeTypeOfKind(candidateType, TypeFlags.BigIntLiteral) || - maybeTypeOfKind(constraint, TypeFlags.ESSymbol) && maybeTypeOfKind(candidateType, TypeFlags.UniqueESSymbol) || - isLiteralOfContextualType(candidateType, constraint); - } - // If the contextual type is a literal of a particular primitive type, we consider this a - // literal context for all literals of that primitive type. - return !!(contextualType.flags & (TypeFlags.StringLiteral | TypeFlags.Index) && maybeTypeOfKind(candidateType, TypeFlags.StringLiteral) || - contextualType.flags & TypeFlags.NumberLiteral && maybeTypeOfKind(candidateType, TypeFlags.NumberLiteral) || - contextualType.flags & TypeFlags.BigIntLiteral && maybeTypeOfKind(candidateType, TypeFlags.BigIntLiteral) || - contextualType.flags & TypeFlags.BooleanLiteral && maybeTypeOfKind(candidateType, TypeFlags.BooleanLiteral) || - contextualType.flags & TypeFlags.UniqueESSymbol && maybeTypeOfKind(candidateType, TypeFlags.UniqueESSymbol)); + return tagType; + } + function getJsxManagedAttributesFromLocatedAttributes(context: JsxOpeningLikeElement, ns: ts.Symbol, attributesType: ts.Type) { + const managedSym = getJsxLibraryManagedAttributes(ns); + if (managedSym) { + const declaredManagedType = getDeclaredTypeOfSymbol(managedSym); + const ctorType = getStaticTypeOfReferencedJsxConstructor(context); + if (length((declaredManagedType as GenericType).typeParameters) >= 2) { + const args = fillMissingTypeArguments([ctorType, attributesType], (declaredManagedType as GenericType).typeParameters, 2, isInJSFile(context)); + return createTypeReference((declaredManagedType as GenericType), args); + } + else if (length(declaredManagedType.aliasTypeArguments) >= 2) { + const args = fillMissingTypeArguments([ctorType, attributesType], declaredManagedType.aliasTypeArguments, 2, isInJSFile(context)); + return getTypeAliasInstantiation(declaredManagedType.aliasSymbol!, args); } - return false; } - - function isConstContext(node: Expression): boolean { - const parent = node.parent; - return isAssertionExpression(parent) && isConstTypeReference(parent.type) || - (isParenthesizedExpression(parent) || isArrayLiteralExpression(parent) || isSpreadElement(parent)) && isConstContext(parent) || - (isPropertyAssignment(parent) || isShorthandPropertyAssignment(parent)) && isConstContext(parent.parent); + return attributesType; + } + function getJsxPropsTypeFromClassType(sig: ts.Signature, context: JsxOpeningLikeElement) { + const ns = getJsxNamespaceAt(context); + const forcedLookupLocation = getJsxElementPropertiesName(ns); + let attributesType = forcedLookupLocation === undefined + // If there is no type ElementAttributesProperty, return the type of the first parameter of the signature, which should be the props type + ? getTypeOfFirstParameterOfSignatureWithFallback(sig, unknownType) + : forcedLookupLocation === "" + // If there is no e.g. 'props' member in ElementAttributesProperty, use the element class type instead + ? getReturnTypeOfSignature(sig) + // Otherwise get the type of the property on the signature return type + : getJsxPropsTypeForSignatureFromMember(sig, forcedLookupLocation); + if (!attributesType) { + // There is no property named 'props' on this instance type + if (!!forcedLookupLocation && !!length(context.attributes.properties)) { + error(context, Diagnostics.JSX_element_class_does_not_support_attributes_because_it_does_not_have_a_0_property, unescapeLeadingUnderscores(forcedLookupLocation)); + } + return unknownType; } - - function checkExpressionForMutableLocation(node: Expression, checkMode: CheckMode | undefined, contextualType?: Type, forceTuple?: boolean): Type { - const type = checkExpression(node, checkMode, forceTuple); - return isConstContext(node) ? getRegularTypeOfLiteralType(type) : - isTypeAssertion(node) ? type : - getWidenedLiteralLikeTypeForContextualType(type, instantiateContextualType(arguments.length === 2 ? getContextualType(node) : contextualType, node)); + attributesType = getJsxManagedAttributesFromLocatedAttributes(context, ns, attributesType); + if (isTypeAny(attributesType)) { + // Props is of type 'any' or unknown + return attributesType; } - - function checkPropertyAssignment(node: PropertyAssignment, checkMode?: CheckMode): Type { - // Do not use hasDynamicName here, because that returns false for well known symbols. - // We want to perform checkComputedPropertyName for all computed properties, including - // well known symbols. - if (node.name.kind === SyntaxKind.ComputedPropertyName) { - checkComputedPropertyName(node.name); + else { + // Normal case -- add in IntrinsicClassElements and IntrinsicElements + let apparentAttributesType = attributesType; + const intrinsicClassAttribs = getJsxType(JsxNames.IntrinsicClassAttributes, context); + if (intrinsicClassAttribs !== errorType) { + const typeParams = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(intrinsicClassAttribs.symbol); + const hostClassType = getReturnTypeOfSignature(sig); + apparentAttributesType = intersectTypes(typeParams + ? createTypeReference((intrinsicClassAttribs), fillMissingTypeArguments([hostClassType], typeParams, getMinTypeArgumentCount(typeParams), isInJSFile(context))) + : intrinsicClassAttribs, apparentAttributesType); } - - return checkExpressionForMutableLocation(node.initializer, checkMode); - } - - function checkObjectLiteralMethod(node: MethodDeclaration, checkMode?: CheckMode): Type { - // Grammar checking - checkGrammarMethod(node); - - // Do not use hasDynamicName here, because that returns false for well known symbols. - // We want to perform checkComputedPropertyName for all computed properties, including - // well known symbols. - if (node.name.kind === SyntaxKind.ComputedPropertyName) { - checkComputedPropertyName(node.name); + const intrinsicAttribs = getJsxType(JsxNames.IntrinsicAttributes, context); + if (intrinsicAttribs !== errorType) { + apparentAttributesType = intersectTypes(intrinsicAttribs, apparentAttributesType); } - - const uninstantiatedType = checkFunctionExpressionOrObjectLiteralMethod(node, checkMode); - return instantiateTypeWithSingleGenericCallSignature(node, uninstantiatedType, checkMode); + return apparentAttributesType; } - - function instantiateTypeWithSingleGenericCallSignature(node: Expression | MethodDeclaration | QualifiedName, type: Type, checkMode?: CheckMode) { - if (checkMode && checkMode & (CheckMode.Inferential | CheckMode.SkipGenericFunctions)) { - const callSignature = getSingleSignature(type, SignatureKind.Call, /*allowMembers*/ true); - const constructSignature = getSingleSignature(type, SignatureKind.Construct, /*allowMembers*/ true); - const signature = callSignature || constructSignature; - if (signature && signature.typeParameters) { - const contextualType = getApparentTypeOfContextualType(node, ContextFlags.NoConstraints); - if (contextualType) { - const contextualSignature = getSingleSignature(getNonNullableType(contextualType), callSignature ? SignatureKind.Call : SignatureKind.Construct, /*allowMembers*/ false); - if (contextualSignature && !contextualSignature.typeParameters) { - if (checkMode & CheckMode.SkipGenericFunctions) { - skippedGenericFunction(node, checkMode); - return anyFunctionType; - } - const context = getInferenceContext(node)!; - // We have an expression that is an argument of a generic function for which we are performing - // type argument inference. The expression is of a function type with a single generic call - // signature and a contextual function type with a single non-generic call signature. Now check - // if the outer function returns a function type with a single non-generic call signature and - // if some of the outer function type parameters have no inferences so far. If so, we can - // potentially add inferred type parameters to the outer function return type. - const returnType = context.signature && getReturnTypeOfSignature(context.signature); - const returnSignature = returnType && getSingleCallOrConstructSignature(returnType); - if (returnSignature && !returnSignature.typeParameters && !every(context.inferences, hasInferenceCandidates)) { - // Instantiate the signature with its own type parameters as type arguments, possibly - // renaming the type parameters to ensure they have unique names. - const uniqueTypeParameters = getUniqueTypeParameters(context, signature.typeParameters); - const instantiatedSignature = getSignatureInstantiationWithoutFillingInTypeArguments(signature, uniqueTypeParameters); - // Infer from the parameters of the instantiated signature to the parameters of the - // contextual signature starting with an empty set of inference candidates. - const inferences = map(context.inferences, info => createInferenceInfo(info.typeParameter)); - applyToParameterTypes(instantiatedSignature, contextualSignature, (source, target) => { - inferTypes(inferences, source, target, /*priority*/ 0, /*contravariant*/ true); - }); - if (some(inferences, hasInferenceCandidates)) { - // We have inference candidates, indicating that one or more type parameters are referenced - // in the parameter types of the contextual signature. Now also infer from the return type. - applyToReturnTypes(instantiatedSignature, contextualSignature, (source, target) => { - inferTypes(inferences, source, target); - }); - // If the type parameters for which we produced candidates do not have any inferences yet, - // we adopt the new inference candidates and add the type parameters of the expression type - // to the set of inferred type parameters for the outer function return type. - if (!hasOverlappingInferences(context.inferences, inferences)) { - mergeInferences(context.inferences, inferences); - context.inferredTypeParameters = concatenate(context.inferredTypeParameters, uniqueTypeParameters); - return getOrCreateTypeFromSignature(instantiatedSignature); - } - } - } - return getOrCreateTypeFromSignature(instantiateSignatureInContextOf(signature, contextualSignature, context)); - } - } - } + } + // If the given type is an object or union type with a single signature, and if that signature has at + // least as many parameters as the given function, return the signature. Otherwise return undefined. + function getContextualCallSignature(type: ts.Type, node: SignatureDeclaration): ts.Signature | undefined { + const signatures = getSignaturesOfType(type, SignatureKind.Call); + if (signatures.length === 1) { + const signature = signatures[0]; + if (!isAritySmaller(signature, node)) { + return signature; } - return type; } - - function skippedGenericFunction(node: Node, checkMode: CheckMode) { - if (checkMode & CheckMode.Inferential) { - // We have skipped a generic function during inferential typing. Obtain the inference context and - // indicate this has occurred such that we know a second pass of inference is be needed. - const context = getInferenceContext(node)!; - context.flags |= InferenceFlags.SkippedGenericFunction; + } + /** If the contextual signature has fewer parameters than the function expression, do not use it */ + function isAritySmaller(signature: ts.Signature, target: SignatureDeclaration) { + let targetParameterCount = 0; + for (; targetParameterCount < target.parameters.length; targetParameterCount++) { + const param = target.parameters[targetParameterCount]; + if (param.initializer || param.questionToken || param.dotDotDotToken || isJSDocOptionalParameter(param)) { + break; } } - - function hasInferenceCandidates(info: InferenceInfo) { - return !!(info.candidates || info.contraCandidates); + if (target.parameters.length && parameterIsThisKeyword(target.parameters[0])) { + targetParameterCount--; } - - function hasOverlappingInferences(a: InferenceInfo[], b: InferenceInfo[]) { - for (let i = 0; i < a.length; i++) { - if (hasInferenceCandidates(a[i]) && hasInferenceCandidates(b[i])) { - return true; - } - } - return false; + return !hasEffectiveRestParameter(signature) && getParameterCount(signature) < targetParameterCount; + } + function isFunctionExpressionOrArrowFunction(node: Node): node is FunctionExpression | ArrowFunction { + return node.kind === SyntaxKind.FunctionExpression || node.kind === SyntaxKind.ArrowFunction; + } + function getContextualSignatureForFunctionLikeDeclaration(node: FunctionLikeDeclaration): ts.Signature | undefined { + // Only function expressions, arrow functions, and object literal methods are contextually typed. + return isFunctionExpressionOrArrowFunction(node) || isObjectLiteralMethod(node) + ? getContextualSignature((node)) + : undefined; + } + // Return the contextual signature for a given expression node. A contextual type provides a + // contextual signature if it has a single call signature and if that call signature is non-generic. + // If the contextual type is a union type, get the signature from each type possible and if they are + // all identical ignoring their return type, the result is same signature but with return type as + // union type of return types from these signatures + function getContextualSignature(node: FunctionExpression | ArrowFunction | MethodDeclaration): ts.Signature | undefined { + Debug.assert(node.kind !== SyntaxKind.MethodDeclaration || isObjectLiteralMethod(node)); + const typeTagSignature = getSignatureOfTypeTag(node); + if (typeTagSignature) { + return typeTagSignature; + } + const type = getApparentTypeOfContextualType(node, ContextFlags.Signature); + if (!type) { + return undefined; } - - function mergeInferences(target: InferenceInfo[], source: InferenceInfo[]) { - for (let i = 0; i < target.length; i++) { - if (!hasInferenceCandidates(target[i]) && hasInferenceCandidates(source[i])) { - target[i] = source[i]; - } - } + if (!(type.flags & TypeFlags.Union)) { + return getContextualCallSignature(type, node); } - - function getUniqueTypeParameters(context: InferenceContext, typeParameters: readonly TypeParameter[]): readonly TypeParameter[] { - const result: TypeParameter[] = []; - let oldTypeParameters: TypeParameter[] | undefined; - let newTypeParameters: TypeParameter[] | undefined; - for (const tp of typeParameters) { - const name = tp.symbol.escapedName; - if (hasTypeParameterByName(context.inferredTypeParameters, name) || hasTypeParameterByName(result, name)) { - const newName = getUniqueTypeParameterName(concatenate(context.inferredTypeParameters, result), name); - const symbol = createSymbol(SymbolFlags.TypeParameter, newName); - const newTypeParameter = createTypeParameter(symbol); - newTypeParameter.target = tp; - oldTypeParameters = append(oldTypeParameters, tp); - newTypeParameters = append(newTypeParameters, newTypeParameter); - result.push(newTypeParameter); + let signatureList: ts.Signature[] | undefined; + const types = (type).types; + for (const current of types) { + const signature = getContextualCallSignature(current, node); + if (signature) { + if (!signatureList) { + // This signature will contribute to contextual union signature + signatureList = [signature]; } - else { - result.push(tp); - } - } - if (newTypeParameters) { - const mapper = createTypeMapper(oldTypeParameters!, newTypeParameters); - for (const tp of newTypeParameters) { - tp.mapper = mapper; + else if (!compareSignaturesIdentical(signatureList[0], signature, /*partialMatch*/ false, /*ignoreThisTypes*/ true, /*ignoreReturnTypes*/ true, compareTypesIdentical)) { + // Signatures aren't identical, do not use + return undefined; } - } - return result; - } - - function hasTypeParameterByName(typeParameters: readonly TypeParameter[] | undefined, name: __String) { - return some(typeParameters, tp => tp.symbol.escapedName === name); - } - - function getUniqueTypeParameterName(typeParameters: readonly TypeParameter[], baseName: __String) { - let len = (baseName).length; - while (len > 1 && (baseName).charCodeAt(len - 1) >= CharacterCodes._0 && (baseName).charCodeAt(len - 1) <= CharacterCodes._9) len--; - const s = (baseName).slice(0, len); - for (let index = 1; true; index++) { - const augmentedName = <__String>(s + index); - if (!hasTypeParameterByName(typeParameters, augmentedName)) { - return augmentedName; + else { + // Use this signature for contextual union signature + signatureList.push(signature); } } } - - function getReturnTypeOfSingleNonGenericCallSignature(funcType: Type) { - const signature = getSingleCallSignature(funcType); - if (signature && !signature.typeParameters) { - return getReturnTypeOfSignature(signature); - } - } - - function getReturnTypeOfSingleNonGenericSignatureOfCallChain(expr: CallChain) { - const funcType = checkExpression(expr.expression); - const nonOptionalType = getOptionalExpressionType(funcType, expr.expression); - const returnType = getReturnTypeOfSingleNonGenericCallSignature(funcType); - return returnType && propagateOptionalTypeMarker(returnType, expr, nonOptionalType !== funcType); + // Result is union of signatures collected (return type is union of return types of this signature set) + if (signatureList) { + return signatureList.length === 1 ? signatureList[0] : createUnionSignature(signatureList[0], signatureList); } - - /** - * Returns the type of an expression. Unlike checkExpression, this function is simply concerned - * with computing the type and may not fully check all contained sub-expressions for errors. - */ - function getTypeOfExpression(node: Expression) { - // Don't bother caching types that require no flow analysis and are quick to compute. - const quickType = getQuickTypeOfExpression(node); - if (quickType) { - return quickType; - } - // If a type has been cached for the node, return it. - if (node.flags & NodeFlags.TypeCached && flowTypeCache) { - const cachedType = flowTypeCache[getNodeId(node)]; - if (cachedType) { - return cachedType; - } - } - const startInvocationCount = flowInvocationCount; - const type = checkExpression(node); - // If control flow analysis was required to determine the type, it is worth caching. - if (flowInvocationCount !== startInvocationCount) { - const cache = flowTypeCache || (flowTypeCache = []); - cache[getNodeId(node)] = type; - node.flags |= NodeFlags.TypeCached; - } - return type; + } + function checkSpreadExpression(node: SpreadElement, checkMode?: CheckMode): ts.Type { + if (languageVersion < ScriptTarget.ES2015) { + checkExternalEmitHelpers(node, compilerOptions.downlevelIteration ? ExternalEmitHelpers.SpreadIncludes : ExternalEmitHelpers.SpreadArrays); } - - function getQuickTypeOfExpression(node: Expression) { - const expr = skipParentheses(node); - // Optimize for the common case of a call to a function with a single non-generic call - // signature where we can just fetch the return type without checking the arguments. - if (isCallExpression(expr) && expr.expression.kind !== SyntaxKind.SuperKeyword && !isRequireCall(expr, /*checkArgumentIsStringLiteralLike*/ true) && !isSymbolOrSymbolForCall(expr)) { - const type = isCallChain(expr) ? getReturnTypeOfSingleNonGenericSignatureOfCallChain(expr) : - getReturnTypeOfSingleNonGenericCallSignature(checkNonNullExpression(expr.expression)); - if (type) { - return type; + const arrayOrIterableType = checkExpression(node.expression, checkMode); + return checkIteratedTypeOrElementType(IterationUse.Spread, arrayOrIterableType, undefinedType, node.expression); + } + function hasDefaultValue(node: BindingElement | Expression): boolean { + return (node.kind === SyntaxKind.BindingElement && !!(node).initializer) || + (node.kind === SyntaxKind.BinaryExpression && (node).operatorToken.kind === SyntaxKind.EqualsToken); + } + function checkArrayLiteral(node: ArrayLiteralExpression, checkMode: CheckMode | undefined, forceTuple: boolean | undefined): ts.Type { + const elements = node.elements; + const elementCount = elements.length; + const elementTypes: ts.Type[] = []; + let hasEndingSpreadElement = false; + let hasNonEndingSpreadElement = false; + const contextualType = getApparentTypeOfContextualType(node); + const inDestructuringPattern = isAssignmentTarget(node); + const inConstContext = isConstContext(node); + for (let i = 0; i < elementCount; i++) { + const e = elements[i]; + const spread = e.kind === SyntaxKind.SpreadElement && (e).expression; + const spreadType = spread && checkExpression(spread, checkMode, forceTuple); + if (spreadType && isTupleType(spreadType)) { + elementTypes.push(...getTypeArguments(spreadType)); + if (spreadType.target.hasRestElement) { + if (i === elementCount - 1) + hasEndingSpreadElement = true; + else + hasNonEndingSpreadElement = true; } } - else if (isAssertionExpression(expr) && !isConstTypeReference(expr.type)) { - return getTypeFromTypeNode((expr).type); - } - else if (node.kind === SyntaxKind.NumericLiteral || node.kind === SyntaxKind.StringLiteral || - node.kind === SyntaxKind.TrueKeyword || node.kind === SyntaxKind.FalseKeyword) { - return checkExpression(node); - } - return undefined; - } - - /** - * Returns the type of an expression. Unlike checkExpression, this function is simply concerned - * with computing the type and may not fully check all contained sub-expressions for errors. - * It is intended for uses where you know there is no contextual type, - * and requesting the contextual type might cause a circularity or other bad behaviour. - * It sets the contextual type of the node to any before calling getTypeOfExpression. - */ - function getContextFreeTypeOfExpression(node: Expression) { - const links = getNodeLinks(node); - if (links.contextFreeType) { - return links.contextFreeType; - } - const saveContextualType = node.contextualType; - node.contextualType = anyType; - try { - const type = links.contextFreeType = checkExpression(node, CheckMode.SkipContextSensitive); + else { + if (inDestructuringPattern && spreadType) { + // Given the following situation: + // var c: {}; + // [...c] = ["", 0]; + // + // c is represented in the tree as a spread element in an array literal. + // But c really functions as a rest element, and its purpose is to provide + // a contextual type for the right hand side of the assignment. Therefore, + // instead of calling checkExpression on "...c", which will give an error + // if c is not iterable/array-like, we need to act as if we are trying to + // get the contextual element type from it. So we do something similar to + // getContextualTypeForElementExpression, which will crucially not error + // if there is no index type / iterated type. + const restElementType = getIndexTypeOfType(spreadType, IndexKind.Number) || + getIteratedTypeOrElementType(IterationUse.Destructuring, spreadType, undefinedType, /*errorNode*/ undefined, /*checkAssignability*/ false); + if (restElementType) { + elementTypes.push(restElementType); + } + } + else { + const elementContextualType = getContextualTypeForElementExpression(contextualType, elementTypes.length); + const type = checkExpressionForMutableLocation(e, checkMode, elementContextualType, forceTuple); + elementTypes.push(type); + } + if (spread) { // tuples are done above, so these are only arrays + if (i === elementCount - 1) + hasEndingSpreadElement = true; + else + hasNonEndingSpreadElement = true; + } + } + } + if (!hasNonEndingSpreadElement) { + const minLength = elementTypes.length - (hasEndingSpreadElement ? 1 : 0); + // If array literal is actually a destructuring pattern, mark it as an implied type. We do this such + // that we get the same behavior for "var [x, y] = []" and "[x, y] = []". + let tupleResult; + if (inDestructuringPattern && minLength > 0) { + const type = cloneTypeReference((createTupleType(elementTypes, minLength, hasEndingSpreadElement))); + type.pattern = node; return type; } - finally { - // In the event our operation is canceled or some other exception occurs, reset the contextual type - // so that we do not accidentally hold onto an instance of the checker, as a Type created in the services layer - // may hold onto the checker that created it. - node.contextualType = saveContextualType; + else if (tupleResult = getArrayLiteralTupleTypeIfApplicable(elementTypes, contextualType, hasEndingSpreadElement, elementTypes.length, inConstContext)) { + return createArrayLiteralType(tupleResult); } - } - - function checkExpression(node: Expression | QualifiedName, checkMode?: CheckMode, forceTuple?: boolean): Type { - const saveCurrentNode = currentNode; - currentNode = node; - instantiationCount = 0; - const uninstantiatedType = checkExpressionWorker(node, checkMode, forceTuple); - const type = instantiateTypeWithSingleGenericCallSignature(node, uninstantiatedType, checkMode); - if (isConstEnumObjectType(type)) { - checkConstEnumAccess(node, type); + else if (forceTuple) { + return createArrayLiteralType(createTupleType(elementTypes, minLength, hasEndingSpreadElement)); } - currentNode = saveCurrentNode; + } + return createArrayLiteralType(createArrayType(elementTypes.length ? + getUnionType(elementTypes, UnionReduction.Subtype) : + strictNullChecks ? implicitNeverType : undefinedWideningType, inConstContext)); + } + function createArrayLiteralType(type: ObjectType) { + if (!(getObjectFlags(type) & ObjectFlags.Reference)) { return type; } - - function checkConstEnumAccess(node: Expression | QualifiedName, type: Type) { - // enum object type for const enums are only permitted in: - // - 'left' in property access - // - 'object' in indexed access - // - target in rhs of import statement - const ok = - (node.parent.kind === SyntaxKind.PropertyAccessExpression && (node.parent).expression === node) || - (node.parent.kind === SyntaxKind.ElementAccessExpression && (node.parent).expression === node) || - ((node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.QualifiedName) && isInRightSideOfImportOrExportAssignment(node) || - (node.parent.kind === SyntaxKind.TypeQuery && (node.parent).exprName === node)) || - (node.parent.kind === SyntaxKind.ExportSpecifier); // We allow reexporting const enums - - if (!ok) { - error(node, Diagnostics.const_enums_can_only_be_used_in_property_or_index_access_expressions_or_the_right_hand_side_of_an_import_declaration_or_export_assignment_or_type_query); - } - - if (compilerOptions.isolatedModules) { - Debug.assert(!!(type.symbol.flags & SymbolFlags.ConstEnum)); - const constEnumDeclaration = type.symbol.valueDeclaration as EnumDeclaration; - if (constEnumDeclaration.flags & NodeFlags.Ambient) { - error(node, Diagnostics.Cannot_access_ambient_const_enums_when_the_isolatedModules_flag_is_provided); - } - } + let literalType = (type).literalType; + if (!literalType) { + literalType = (type).literalType = cloneTypeReference((type)); + literalType.objectFlags |= ObjectFlags.ArrayLiteral | ObjectFlags.ContainsObjectOrArrayLiteral; } - - function checkParenthesizedExpression(node: ParenthesizedExpression, checkMode?: CheckMode): Type { - const tag = isInJSFile(node) ? getJSDocTypeTag(node) : undefined; - if (tag) { - return checkAssertionWorker(tag, tag.typeExpression.type, node.expression, checkMode); - } - return checkExpression(node.expression, checkMode); + return literalType; + } + function getArrayLiteralTupleTypeIfApplicable(elementTypes: ts.Type[], contextualType: ts.Type | undefined, hasRestElement: boolean, elementCount = elementTypes.length, readonly = false) { + // Infer a tuple type when the contextual type is or contains a tuple-like type + if (readonly || (contextualType && forEachType(contextualType, isTupleLikeType))) { + return createTupleType(elementTypes, elementCount - (hasRestElement ? 1 : 0), hasRestElement, readonly); } - - function checkExpressionWorker(node: Expression | QualifiedName, checkMode: CheckMode | undefined, forceTuple?: boolean): Type { - const kind = node.kind; - if (cancellationToken) { - // Only bother checking on a few construct kinds. We don't want to be excessively - // hitting the cancellation token on every node we check. - switch (kind) { - case SyntaxKind.ClassExpression: - case SyntaxKind.FunctionExpression: - case SyntaxKind.ArrowFunction: - cancellationToken.throwIfCancellationRequested(); - } - } - switch (kind) { - case SyntaxKind.Identifier: - return checkIdentifier(node); - case SyntaxKind.ThisKeyword: - return checkThisExpression(node); - case SyntaxKind.SuperKeyword: - return checkSuperExpression(node); - case SyntaxKind.NullKeyword: - return nullWideningType; - case SyntaxKind.NoSubstitutionTemplateLiteral: - case SyntaxKind.StringLiteral: - return getFreshTypeOfLiteralType(getLiteralType((node as StringLiteralLike).text)); - case SyntaxKind.NumericLiteral: - checkGrammarNumericLiteral(node as NumericLiteral); - return getFreshTypeOfLiteralType(getLiteralType(+(node as NumericLiteral).text)); - case SyntaxKind.BigIntLiteral: - checkGrammarBigIntLiteral(node as BigIntLiteral); - return getFreshTypeOfLiteralType(getBigIntLiteralType(node as BigIntLiteral)); - case SyntaxKind.TrueKeyword: - return trueType; - case SyntaxKind.FalseKeyword: - return falseType; - case SyntaxKind.TemplateExpression: - return checkTemplateExpression(node); - case SyntaxKind.RegularExpressionLiteral: - return globalRegExpType; - case SyntaxKind.ArrayLiteralExpression: - return checkArrayLiteral(node, checkMode, forceTuple); - case SyntaxKind.ObjectLiteralExpression: - return checkObjectLiteral(node, checkMode); - case SyntaxKind.PropertyAccessExpression: - return checkPropertyAccessExpression(node); - case SyntaxKind.QualifiedName: - return checkQualifiedName(node); - case SyntaxKind.ElementAccessExpression: - return checkIndexedAccess(node); - case SyntaxKind.CallExpression: - if ((node).expression.kind === SyntaxKind.ImportKeyword) { - return checkImportCallExpression(node); - } - // falls through - case SyntaxKind.NewExpression: - return checkCallExpression(node, checkMode); - case SyntaxKind.TaggedTemplateExpression: - return checkTaggedTemplateExpression(node); - case SyntaxKind.ParenthesizedExpression: - return checkParenthesizedExpression(node, checkMode); - case SyntaxKind.ClassExpression: - return checkClassExpression(node); - case SyntaxKind.FunctionExpression: - case SyntaxKind.ArrowFunction: - return checkFunctionExpressionOrObjectLiteralMethod(node, checkMode); - case SyntaxKind.TypeOfExpression: - return checkTypeOfExpression(node); - case SyntaxKind.TypeAssertionExpression: - case SyntaxKind.AsExpression: - return checkAssertion(node); - case SyntaxKind.NonNullExpression: - return checkNonNullAssertion(node); - case SyntaxKind.MetaProperty: - return checkMetaProperty(node); - case SyntaxKind.DeleteExpression: - return checkDeleteExpression(node); - case SyntaxKind.VoidExpression: - return checkVoidExpression(node); - case SyntaxKind.AwaitExpression: - return checkAwaitExpression(node); - case SyntaxKind.PrefixUnaryExpression: - return checkPrefixUnaryExpression(node); - case SyntaxKind.PostfixUnaryExpression: - return checkPostfixUnaryExpression(node); - case SyntaxKind.BinaryExpression: - return checkBinaryExpression(node, checkMode); - case SyntaxKind.ConditionalExpression: - return checkConditionalExpression(node, checkMode); - case SyntaxKind.SpreadElement: - return checkSpreadExpression(node, checkMode); - case SyntaxKind.OmittedExpression: - return undefinedWideningType; - case SyntaxKind.YieldExpression: - return checkYieldExpression(node); - case SyntaxKind.SyntheticExpression: - return (node).type; - case SyntaxKind.JsxExpression: - return checkJsxExpression(node, checkMode); - case SyntaxKind.JsxElement: - return checkJsxElement(node, checkMode); - case SyntaxKind.JsxSelfClosingElement: - return checkJsxSelfClosingElement(node, checkMode); - case SyntaxKind.JsxFragment: - return checkJsxFragment(node); - case SyntaxKind.JsxAttributes: - return checkJsxAttributes(node, checkMode); - case SyntaxKind.JsxOpeningElement: - Debug.fail("Shouldn't ever directly check a JsxOpeningElement"); - } - return errorType; + } + function isNumericName(name: DeclarationName): boolean { + switch (name.kind) { + case SyntaxKind.ComputedPropertyName: + return isNumericComputedName(name); + case SyntaxKind.Identifier: + return isNumericLiteralName(name.escapedText); + case SyntaxKind.NumericLiteral: + case SyntaxKind.StringLiteral: + return isNumericLiteralName(name.text); + default: + return false; } - - // DECLARATION AND STATEMENT TYPE CHECKING - - function checkTypeParameter(node: TypeParameterDeclaration) { - // Grammar Checking - if (node.expression) { - grammarErrorOnFirstToken(node.expression, Diagnostics.Type_expected); - } - - checkSourceElement(node.constraint); - checkSourceElement(node.default); - const typeParameter = getDeclaredTypeOfTypeParameter(getSymbolOfNode(node)); - // Resolve base constraint to reveal circularity errors - getBaseConstraintOfType(typeParameter); - if (!hasNonCircularTypeParameterDefault(typeParameter)) { - error(node.default, Diagnostics.Type_parameter_0_has_a_circular_default, typeToString(typeParameter)); + } + function isNumericComputedName(name: ComputedPropertyName): boolean { + // It seems odd to consider an expression of type Any to result in a numeric name, + // but this behavior is consistent with checkIndexedAccess + return isTypeAssignableToKind(checkComputedPropertyName(name), TypeFlags.NumberLike); + } + function isInfinityOrNaNString(name: string | __String): boolean { + return name === "Infinity" || name === "-Infinity" || name === "NaN"; + } + function isNumericLiteralName(name: string | __String) { + // The intent of numeric names is that + // - they are names with text in a numeric form, and that + // - setting properties/indexing with them is always equivalent to doing so with the numeric literal 'numLit', + // acquired by applying the abstract 'ToNumber' operation on the name's text. + // + // The subtlety is in the latter portion, as we cannot reliably say that anything that looks like a numeric literal is a numeric name. + // In fact, it is the case that the text of the name must be equal to 'ToString(numLit)' for this to hold. + // + // Consider the property name '"0xF00D"'. When one indexes with '0xF00D', they are actually indexing with the value of 'ToString(0xF00D)' + // according to the ECMAScript specification, so it is actually as if the user indexed with the string '"61453"'. + // Thus, the text of all numeric literals equivalent to '61543' such as '0xF00D', '0xf00D', '0170015', etc. are not valid numeric names + // because their 'ToString' representation is not equal to their original text. + // This is motivated by ECMA-262 sections 9.3.1, 9.8.1, 11.1.5, and 11.2.1. + // + // Here, we test whether 'ToString(ToNumber(name))' is exactly equal to 'name'. + // The '+' prefix operator is equivalent here to applying the abstract ToNumber operation. + // Applying the 'toString()' method on a number gives us the abstract ToString operation on a number. + // + // Note that this accepts the values 'Infinity', '-Infinity', and 'NaN', and that this is intentional. + // This is desired behavior, because when indexing with them as numeric entities, you are indexing + // with the strings '"Infinity"', '"-Infinity"', and '"NaN"' respectively. + return (+name).toString() === name; + } + function checkComputedPropertyName(node: ComputedPropertyName): ts.Type { + const links = getNodeLinks(node.expression); + if (!links.resolvedType) { + links.resolvedType = checkExpression(node.expression); + // This will allow types number, string, symbol or any. It will also allow enums, the unknown + // type, and any union of these types (like string | number). + if (links.resolvedType.flags & TypeFlags.Nullable || + !isTypeAssignableToKind(links.resolvedType, TypeFlags.StringLike | TypeFlags.NumberLike | TypeFlags.ESSymbolLike) && + !isTypeAssignableTo(links.resolvedType, stringNumberSymbolType)) { + error(node, Diagnostics.A_computed_property_name_must_be_of_type_string_number_symbol_or_any); } - const constraintType = getConstraintOfTypeParameter(typeParameter); - const defaultType = getDefaultFromTypeParameter(typeParameter); - if (constraintType && defaultType) { - checkTypeAssignableTo(defaultType, getTypeWithThisArgument(instantiateType(constraintType, makeUnaryTypeMapper(typeParameter, defaultType)), defaultType), node.default, Diagnostics.Type_0_does_not_satisfy_the_constraint_1); + else { + checkThatExpressionIsProperSymbolReference(node.expression, links.resolvedType, /*reportError*/ true); } - if (produceDiagnostics) { - checkTypeNameIsReserved(node.name, Diagnostics.Type_parameter_name_cannot_be_0); + } + return links.resolvedType; + } + function getObjectLiteralIndexInfo(node: ObjectLiteralExpression, offset: number, properties: ts.Symbol[], kind: IndexKind): IndexInfo { + const propTypes: ts.Type[] = []; + for (let i = 0; i < properties.length; i++) { + if (kind === IndexKind.String || isNumericName(node.properties[i + offset].name!)) { + propTypes.push(getTypeOfSymbol(properties[i])); } } - - function checkParameter(node: ParameterDeclaration) { - // Grammar checking - // It is a SyntaxError if the Identifier "eval" or the Identifier "arguments" occurs as the - // Identifier in a PropertySetParameterList of a PropertyAssignment that is contained in strict code - // or if its FunctionBody is strict code(11.1.5). - checkGrammarDecoratorsAndModifiers(node); - - checkVariableLikeDeclaration(node); - const func = getContainingFunction(node)!; - if (hasModifier(node, ModifierFlags.ParameterPropertyModifier)) { - if (!(func.kind === SyntaxKind.Constructor && nodeIsPresent(func.body))) { - error(node, Diagnostics.A_parameter_property_is_only_allowed_in_a_constructor_implementation); + const unionType = propTypes.length ? getUnionType(propTypes, UnionReduction.Subtype) : undefinedType; + return createIndexInfo(unionType, isConstContext(node)); + } + function getImmediateAliasedSymbol(symbol: ts.Symbol): ts.Symbol | undefined { + Debug.assert((symbol.flags & SymbolFlags.Alias) !== 0, "Should only get Alias here."); + const links = getSymbolLinks(symbol); + if (!links.immediateTarget) { + const node = getDeclarationOfAliasSymbol(symbol); + if (!node) + return Debug.fail(); + links.immediateTarget = getTargetOfAliasDeclaration(node, /*dontRecursivelyResolve*/ true); + } + return links.immediateTarget; + } + function checkObjectLiteral(node: ObjectLiteralExpression, checkMode?: CheckMode): ts.Type { + const inDestructuringPattern = isAssignmentTarget(node); + // Grammar checking + checkGrammarObjectLiteralExpression(node, inDestructuringPattern); + let propertiesTable: SymbolTable; + let propertiesArray: ts.Symbol[] = []; + let spread: ts.Type = emptyObjectType; + const contextualType = getApparentTypeOfContextualType(node); + const contextualTypeHasPattern = contextualType && contextualType.pattern && + (contextualType.pattern.kind === SyntaxKind.ObjectBindingPattern || contextualType.pattern.kind === SyntaxKind.ObjectLiteralExpression); + const inConstContext = isConstContext(node); + const checkFlags = inConstContext ? CheckFlags.Readonly : 0; + const isInJavascript = isInJSFile(node) && !isInJsonFile(node); + const enumTag = getJSDocEnumTag(node); + const isJSObjectLiteral = !contextualType && isInJavascript && !enumTag; + let objectFlags: ObjectFlags = freshObjectLiteralFlag; + let patternWithComputedProperties = false; + let hasComputedStringProperty = false; + let hasComputedNumberProperty = false; + propertiesTable = createSymbolTable(); + let offset = 0; + for (let i = 0; i < node.properties.length; i++) { + const memberDecl = node.properties[i]; + let member = getSymbolOfNode(memberDecl); + const computedNameType = memberDecl.name && memberDecl.name.kind === SyntaxKind.ComputedPropertyName && !isWellKnownSymbolSyntactically(memberDecl.name.expression) ? + checkComputedPropertyName(memberDecl.name) : undefined; + if (memberDecl.kind === SyntaxKind.PropertyAssignment || + memberDecl.kind === SyntaxKind.ShorthandPropertyAssignment || + isObjectLiteralMethod(memberDecl)) { + let type = memberDecl.kind === SyntaxKind.PropertyAssignment ? checkPropertyAssignment(memberDecl, checkMode) : + memberDecl.kind === SyntaxKind.ShorthandPropertyAssignment ? checkExpressionForMutableLocation(memberDecl.name, checkMode) : + checkObjectLiteralMethod(memberDecl, checkMode); + if (isInJavascript) { + const jsDocType = getTypeForDeclarationFromJSDocComment(memberDecl); + if (jsDocType) { + checkTypeAssignableTo(type, jsDocType, memberDecl); + type = jsDocType; + } + else if (enumTag && enumTag.typeExpression) { + checkTypeAssignableTo(type, getTypeFromTypeNode(enumTag.typeExpression), memberDecl); + } } - } - if (node.questionToken && isBindingPattern(node.name) && (func as FunctionLikeDeclaration).body) { - error(node, Diagnostics.A_binding_pattern_parameter_cannot_be_optional_in_an_implementation_signature); - } - if (node.name && isIdentifier(node.name) && (node.name.escapedText === "this" || node.name.escapedText === "new")) { - if (func.parameters.indexOf(node) !== 0) { - error(node, Diagnostics.A_0_parameter_must_be_the_first_parameter, node.name.escapedText as string); + objectFlags |= getObjectFlags(type) & ObjectFlags.PropagatingFlags; + const nameType = computedNameType && isTypeUsableAsPropertyName(computedNameType) ? computedNameType : undefined; + const prop = nameType ? + createSymbol(SymbolFlags.Property | member.flags, getPropertyNameFromType(nameType), checkFlags | CheckFlags.Late) : + createSymbol(SymbolFlags.Property | member.flags, member.escapedName, checkFlags); + if (nameType) { + prop.nameType = nameType; } - if (func.kind === SyntaxKind.Constructor || func.kind === SyntaxKind.ConstructSignature || func.kind === SyntaxKind.ConstructorType) { - error(node, Diagnostics.A_constructor_cannot_have_a_this_parameter); + if (inDestructuringPattern) { + // If object literal is an assignment pattern and if the assignment pattern specifies a default value + // for the property, make the property optional. + const isOptional = (memberDecl.kind === SyntaxKind.PropertyAssignment && hasDefaultValue(memberDecl.initializer)) || + (memberDecl.kind === SyntaxKind.ShorthandPropertyAssignment && memberDecl.objectAssignmentInitializer); + if (isOptional) { + prop.flags |= SymbolFlags.Optional; + } } - if (func.kind === SyntaxKind.ArrowFunction) { - error(node, Diagnostics.An_arrow_function_cannot_have_a_this_parameter); + else if (contextualTypeHasPattern && !(getObjectFlags((contextualType!)) & ObjectFlags.ObjectLiteralPatternWithComputedProperties)) { + // If object literal is contextually typed by the implied type of a binding pattern, and if the + // binding pattern specifies a default value for the property, make the property optional. + const impliedProp = getPropertyOfType(contextualType!, member.escapedName); + if (impliedProp) { + prop.flags |= impliedProp.flags & SymbolFlags.Optional; + } + else if (!compilerOptions.suppressExcessPropertyErrors && !getIndexInfoOfType((contextualType!), IndexKind.String)) { + error(memberDecl.name, Diagnostics.Object_literal_may_only_specify_known_properties_and_0_does_not_exist_in_type_1, symbolToString(member), typeToString(contextualType!)); + } } + prop.declarations = member.declarations; + prop.parent = member.parent; + if (member.valueDeclaration) { + prop.valueDeclaration = member.valueDeclaration; + } + prop.type = type; + prop.target = member; + member = prop; } - - // Only check rest parameter type if it's not a binding pattern. Since binding patterns are - // not allowed in a rest parameter, we already have an error from checkGrammarParameterList. - if (node.dotDotDotToken && !isBindingPattern(node.name) && !isTypeAssignableTo(getTypeOfSymbol(node.symbol), anyReadonlyArrayType)) { - error(node, Diagnostics.A_rest_parameter_must_be_of_an_array_type); - } - } - - function checkTypePredicate(node: TypePredicateNode): void { - const parent = getTypePredicateParent(node); - if (!parent) { - // The parent must not be valid. - error(node, Diagnostics.A_type_predicate_is_only_allowed_in_return_type_position_for_functions_and_methods); - return; - } - - const signature = getSignatureFromDeclaration(parent); - const typePredicate = getTypePredicateOfSignature(signature); - if (!typePredicate) { - return; - } - - checkSourceElement(node.type); - - const { parameterName } = node; - if (typePredicate.kind === TypePredicateKind.This || typePredicate.kind === TypePredicateKind.AssertsThis) { - getTypeFromThisTypeNode(parameterName as ThisTypeNode); + else if (memberDecl.kind === SyntaxKind.SpreadAssignment) { + if (languageVersion < ScriptTarget.ES2015) { + checkExternalEmitHelpers(memberDecl, ExternalEmitHelpers.Assign); + } + if (propertiesArray.length > 0) { + spread = getSpreadType(spread, createObjectLiteralType(), node.symbol, objectFlags, inConstContext); + propertiesArray = []; + propertiesTable = createSymbolTable(); + hasComputedStringProperty = false; + hasComputedNumberProperty = false; + } + const type = checkExpression(memberDecl.expression); + if (!isValidSpreadType(type)) { + error(memberDecl, Diagnostics.Spread_types_may_only_be_created_from_object_types); + return errorType; + } + spread = getSpreadType(spread, type, node.symbol, objectFlags, inConstContext); + offset = i + 1; + continue; } else { - if (typePredicate.parameterIndex >= 0) { - if (signatureHasRestParameter(signature) && typePredicate.parameterIndex === signature.parameters.length - 1) { - error(parameterName, Diagnostics.A_type_predicate_cannot_reference_a_rest_parameter); + // TypeScript 1.0 spec (April 2014) + // A get accessor declaration is processed in the same manner as + // an ordinary function declaration(section 6.1) with no parameters. + // A set accessor declaration is processed in the same manner + // as an ordinary function declaration with a single parameter and a Void return type. + Debug.assert(memberDecl.kind === SyntaxKind.GetAccessor || memberDecl.kind === SyntaxKind.SetAccessor); + checkNodeDeferred(memberDecl); + } + if (computedNameType && !(computedNameType.flags & TypeFlags.StringOrNumberLiteralOrUnique)) { + if (isTypeAssignableTo(computedNameType, stringNumberSymbolType)) { + if (isTypeAssignableTo(computedNameType, numberType)) { + hasComputedNumberProperty = true; } else { - if (typePredicate.type) { - const leadingError = () => chainDiagnosticMessages(/*details*/ undefined, Diagnostics.A_type_predicate_s_type_must_be_assignable_to_its_parameter_s_type); - checkTypeAssignableTo(typePredicate.type, - getTypeOfSymbol(signature.parameters[typePredicate.parameterIndex]), - node.type, - /*headMessage*/ undefined, - leadingError); - } - } - } - else if (parameterName) { - let hasReportedError = false; - for (const { name } of parent.parameters) { - if (isBindingPattern(name) && - checkIfTypePredicateVariableIsDeclaredInBindingPattern(name, parameterName, typePredicate.parameterName)) { - hasReportedError = true; - break; - } + hasComputedStringProperty = true; } - if (!hasReportedError) { - error(node.parameterName, Diagnostics.Cannot_find_parameter_0, typePredicate.parameterName); + if (inDestructuringPattern) { + patternWithComputedProperties = true; } } } - } - - function getTypePredicateParent(node: Node): SignatureDeclaration | undefined { - switch (node.parent.kind) { - case SyntaxKind.ArrowFunction: - case SyntaxKind.CallSignature: - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.FunctionExpression: - case SyntaxKind.FunctionType: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.MethodSignature: - const parent = node.parent; - if (node === parent.type) { - return parent; - } + else { + propertiesTable.set(member.escapedName, member); } + propertiesArray.push(member); } - - function checkIfTypePredicateVariableIsDeclaredInBindingPattern( - pattern: BindingPattern, - predicateVariableNode: Node, - predicateVariableName: string) { - for (const element of pattern.elements) { - if (isOmittedExpression(element)) { - continue; - } - - const name = element.name; - if (name.kind === SyntaxKind.Identifier && name.escapedText === predicateVariableName) { - error(predicateVariableNode, - Diagnostics.A_type_predicate_cannot_reference_element_0_in_a_binding_pattern, - predicateVariableName); - return true; - } - else if (name.kind === SyntaxKind.ArrayBindingPattern || name.kind === SyntaxKind.ObjectBindingPattern) { - if (checkIfTypePredicateVariableIsDeclaredInBindingPattern( - name, - predicateVariableNode, - predicateVariableName)) { - return true; + // If object literal is contextually typed by the implied type of a binding pattern, augment the result + // type with those properties for which the binding pattern specifies a default value. + // If the object literal is spread into another object literal, skip this step and let the top-level object + // literal handle it instead. + if (contextualTypeHasPattern && node.parent.kind !== SyntaxKind.SpreadAssignment) { + for (const prop of getPropertiesOfType(contextualType!)) { + if (!propertiesTable.get(prop.escapedName) && !getPropertyOfType(spread, prop.escapedName)) { + if (!(prop.flags & SymbolFlags.Optional)) { + error(prop.valueDeclaration || (prop).bindingElement, Diagnostics.Initializer_provides_no_value_for_this_binding_element_and_the_binding_element_has_no_default_value); } + propertiesTable.set(prop.escapedName, prop); + propertiesArray.push(prop); } } } - - function checkSignatureDeclaration(node: SignatureDeclaration) { - // Grammar checking - if (node.kind === SyntaxKind.IndexSignature) { - checkGrammarIndexSignature(node); + if (spread !== emptyObjectType) { + if (propertiesArray.length > 0) { + spread = getSpreadType(spread, createObjectLiteralType(), node.symbol, objectFlags, inConstContext); + propertiesArray = []; + propertiesTable = createSymbolTable(); + hasComputedStringProperty = false; + hasComputedNumberProperty = false; } - // TODO (yuisu): Remove this check in else-if when SyntaxKind.Construct is moved and ambient context is handled - else if (node.kind === SyntaxKind.FunctionType || node.kind === SyntaxKind.FunctionDeclaration || node.kind === SyntaxKind.ConstructorType || - node.kind === SyntaxKind.CallSignature || node.kind === SyntaxKind.Constructor || - node.kind === SyntaxKind.ConstructSignature) { - checkGrammarFunctionLikeDeclaration(node); + // remap the raw emptyObjectType fed in at the top into a fresh empty object literal type, unique to this use site + return mapType(spread, t => t === emptyObjectType ? createObjectLiteralType() : t); + } + return createObjectLiteralType(); + function createObjectLiteralType() { + const stringIndexInfo = hasComputedStringProperty ? getObjectLiteralIndexInfo(node, offset, propertiesArray, IndexKind.String) : undefined; + const numberIndexInfo = hasComputedNumberProperty ? getObjectLiteralIndexInfo(node, offset, propertiesArray, IndexKind.Number) : undefined; + const result = createAnonymousType(node.symbol, propertiesTable, emptyArray, emptyArray, stringIndexInfo, numberIndexInfo); + result.objectFlags |= objectFlags | ObjectFlags.ObjectLiteral | ObjectFlags.ContainsObjectOrArrayLiteral; + if (isJSObjectLiteral) { + result.objectFlags |= ObjectFlags.JSLiteral; } - - const functionFlags = getFunctionFlags(node); - if (!(functionFlags & FunctionFlags.Invalid)) { - // Async generators prior to ESNext require the __await and __asyncGenerator helpers - if ((functionFlags & FunctionFlags.AsyncGenerator) === FunctionFlags.AsyncGenerator && languageVersion < ScriptTarget.ESNext) { - checkExternalEmitHelpers(node, ExternalEmitHelpers.AsyncGeneratorIncludes); - } - - // Async functions prior to ES2017 require the __awaiter helper - if ((functionFlags & FunctionFlags.AsyncGenerator) === FunctionFlags.Async && languageVersion < ScriptTarget.ES2017) { - checkExternalEmitHelpers(node, ExternalEmitHelpers.Awaiter); - } - - // Generator functions, Async functions, and Async Generator functions prior to - // ES2015 require the __generator helper - if ((functionFlags & FunctionFlags.AsyncGenerator) !== FunctionFlags.Normal && languageVersion < ScriptTarget.ES2015) { - checkExternalEmitHelpers(node, ExternalEmitHelpers.Generator); - } + if (patternWithComputedProperties) { + result.objectFlags |= ObjectFlags.ObjectLiteralPatternWithComputedProperties; } - - checkTypeParameters(node.typeParameters); - - forEach(node.parameters, checkParameter); - - // TODO(rbuckton): Should we start checking JSDoc types? - if (node.type) { - checkSourceElement(node.type); + if (inDestructuringPattern) { + result.pattern = node; } - - if (produceDiagnostics) { - checkCollisionWithArgumentsInGeneratedCode(node); - const returnTypeNode = getEffectiveReturnTypeNode(node); - if (noImplicitAny && !returnTypeNode) { - switch (node.kind) { - case SyntaxKind.ConstructSignature: - error(node, Diagnostics.Construct_signature_which_lacks_return_type_annotation_implicitly_has_an_any_return_type); - break; - case SyntaxKind.CallSignature: - error(node, Diagnostics.Call_signature_which_lacks_return_type_annotation_implicitly_has_an_any_return_type); - break; - } - } - - if (returnTypeNode) { - const functionFlags = getFunctionFlags(node); - if ((functionFlags & (FunctionFlags.Invalid | FunctionFlags.Generator)) === FunctionFlags.Generator) { - const returnType = getTypeFromTypeNode(returnTypeNode); - if (returnType === voidType) { - error(returnTypeNode, Diagnostics.A_generator_cannot_have_a_void_type_annotation); - } - else { - // Naively, one could check that Generator is assignable to the return type annotation. - // However, that would not catch the error in the following case. - // - // interface BadGenerator extends Iterable, Iterator { } - // function* g(): BadGenerator { } // Iterable and Iterator have different types! - // - const generatorYieldType = getIterationTypeOfGeneratorFunctionReturnType(IterationTypeKind.Yield, returnType, (functionFlags & FunctionFlags.Async) !== 0) || anyType; - const generatorReturnType = getIterationTypeOfGeneratorFunctionReturnType(IterationTypeKind.Return, returnType, (functionFlags & FunctionFlags.Async) !== 0) || generatorYieldType; - const generatorNextType = getIterationTypeOfGeneratorFunctionReturnType(IterationTypeKind.Next, returnType, (functionFlags & FunctionFlags.Async) !== 0) || unknownType; - const generatorInstantiation = createGeneratorReturnType(generatorYieldType, generatorReturnType, generatorNextType, !!(functionFlags & FunctionFlags.Async)); - checkTypeAssignableTo(generatorInstantiation, returnType, returnTypeNode); - } - } - else if ((functionFlags & FunctionFlags.AsyncGenerator) === FunctionFlags.Async) { - checkAsyncFunctionReturnType(node, returnTypeNode); - } - } - if (node.kind !== SyntaxKind.IndexSignature && node.kind !== SyntaxKind.JSDocFunctionType) { - registerForUnusedIdentifiersCheck(node); - } + return result; + } + } + function isValidSpreadType(type: ts.Type): boolean { + if (type.flags & TypeFlags.Instantiable) { + const constraint = getBaseConstraintOfType(type); + if (constraint !== undefined) { + return isValidSpreadType(constraint); } } - - function checkClassForDuplicateDeclarations(node: ClassLikeDeclaration) { - const instanceNames = createUnderscoreEscapedMap(); - const staticNames = createUnderscoreEscapedMap(); - // instance and static private identifiers share the same scope - const privateIdentifiers = createUnderscoreEscapedMap(); - for (const member of node.members) { - if (member.kind === SyntaxKind.Constructor) { - for (const param of (member as ConstructorDeclaration).parameters) { - if (isParameterPropertyDeclaration(param, member) && !isBindingPattern(param.name)) { - addName(instanceNames, param.name, param.name.escapedText, DeclarationMeaning.GetOrSetAccessor); - } - } - } - else { - const isStatic = hasModifier(member, ModifierFlags.Static); - const name = member.name; - if (!name) { - return; - } - const names = - isPrivateIdentifier(name) ? privateIdentifiers : - isStatic ? staticNames : - instanceNames; - const memberName = name && getPropertyNameForPropertyNameNode(name); - if (memberName) { - switch (member.kind) { - case SyntaxKind.GetAccessor: - addName(names, name, memberName, DeclarationMeaning.GetAccessor); - break; - - case SyntaxKind.SetAccessor: - addName(names, name, memberName, DeclarationMeaning.SetAccessor); - break; - - case SyntaxKind.PropertyDeclaration: - addName(names, name, memberName, DeclarationMeaning.GetOrSetAccessor); - break; - - case SyntaxKind.MethodDeclaration: - addName(names, name, memberName, DeclarationMeaning.Method); - break; - } - } + return !!(type.flags & (TypeFlags.Any | TypeFlags.NonPrimitive | TypeFlags.Object | TypeFlags.InstantiableNonPrimitive) || + getFalsyFlags(type) & TypeFlags.DefinitelyFalsy && isValidSpreadType(removeDefinitelyFalsyTypes(type)) || + type.flags & TypeFlags.UnionOrIntersection && every((type).types, isValidSpreadType)); + } + function checkJsxSelfClosingElementDeferred(node: JsxSelfClosingElement) { + checkJsxOpeningLikeElementOrOpeningFragment(node); + resolveUntypedCall(node); // ensure type arguments and parameters are typechecked, even if there is an arity error + } + function checkJsxSelfClosingElement(node: JsxSelfClosingElement, _checkMode: CheckMode | undefined): ts.Type { + checkNodeDeferred(node); + return getJsxElementTypeAt(node) || anyType; + } + function checkJsxElementDeferred(node: JsxElement) { + // Check attributes + checkJsxOpeningLikeElementOrOpeningFragment(node.openingElement); + // Perform resolution on the closing tag so that rename/go to definition/etc work + if (isJsxIntrinsicIdentifier(node.closingElement.tagName)) { + getIntrinsicTagSymbol(node.closingElement); + } + else { + checkExpression(node.closingElement.tagName); + } + checkJsxChildren(node); + } + function checkJsxElement(node: JsxElement, _checkMode: CheckMode | undefined): ts.Type { + checkNodeDeferred(node); + return getJsxElementTypeAt(node) || anyType; + } + function checkJsxFragment(node: JsxFragment): ts.Type { + checkJsxOpeningLikeElementOrOpeningFragment(node.openingFragment); + if (compilerOptions.jsx === JsxEmit.React && (compilerOptions.jsxFactory || getSourceFileOfNode(node).pragmas.has("jsx"))) { + error(node, compilerOptions.jsxFactory + ? Diagnostics.JSX_fragment_is_not_supported_when_using_jsxFactory + : Diagnostics.JSX_fragment_is_not_supported_when_using_an_inline_JSX_factory_pragma); + } + checkJsxChildren(node); + return getJsxElementTypeAt(node) || anyType; + } + /** + * Returns true iff the JSX element name would be a valid JS identifier, ignoring restrictions about keywords not being identifiers + */ + function isUnhyphenatedJsxName(name: string | __String) { + // - is the only character supported in JSX attribute names that isn't valid in JavaScript identifiers + return !stringContains((name as string), "-"); + } + /** + * Returns true iff React would emit this tag name as a string rather than an identifier or qualified name + */ + function isJsxIntrinsicIdentifier(tagName: JsxTagNameExpression): boolean { + return tagName.kind === SyntaxKind.Identifier && isIntrinsicJsxName(tagName.escapedText); + } + function checkJsxAttribute(node: JsxAttribute, checkMode?: CheckMode) { + return node.initializer + ? checkExpressionForMutableLocation(node.initializer, checkMode) + : trueType; // is sugar for + } + /** + * Get attributes type of the JSX opening-like element. The result is from resolving "attributes" property of the opening-like element. + * + * @param openingLikeElement a JSX opening-like element + * @param filter a function to remove attributes that will not participate in checking whether attributes are assignable + * @return an anonymous type (similar to the one returned by checkObjectLiteral) in which its properties are attributes property. + * @remarks Because this function calls getSpreadType, it needs to use the same checks as checkObjectLiteral, + * which also calls getSpreadType. + */ + function createJsxAttributesTypeFromAttributesProperty(openingLikeElement: JsxOpeningLikeElement, checkMode: CheckMode | undefined) { + const attributes = openingLikeElement.attributes; + let attributesTable = createSymbolTable(); + let spread: ts.Type = emptyJsxObjectType; + let hasSpreadAnyType = false; + let typeToIntersect: ts.Type | undefined; + let explicitlySpecifyChildrenAttribute = false; + let objectFlags: ObjectFlags = ObjectFlags.JsxAttributes; + const jsxChildrenPropertyName = getJsxElementChildrenPropertyName(getJsxNamespaceAt(openingLikeElement)); + for (const attributeDecl of attributes.properties) { + const member = attributeDecl.symbol; + if (isJsxAttribute(attributeDecl)) { + const exprType = checkJsxAttribute(attributeDecl, checkMode); + objectFlags |= getObjectFlags(exprType) & ObjectFlags.PropagatingFlags; + const attributeSymbol = createSymbol(SymbolFlags.Property | SymbolFlags.Transient | member.flags, member.escapedName); + attributeSymbol.declarations = member.declarations; + attributeSymbol.parent = member.parent; + if (member.valueDeclaration) { + attributeSymbol.valueDeclaration = member.valueDeclaration; + } + attributeSymbol.type = exprType; + attributeSymbol.target = member; + attributesTable.set(attributeSymbol.escapedName, attributeSymbol); + if (attributeDecl.name.escapedText === jsxChildrenPropertyName) { + explicitlySpecifyChildrenAttribute = true; } } - - function addName(names: UnderscoreEscapedMap, location: Node, name: __String, meaning: DeclarationMeaning) { - const prev = names.get(name); - if (prev) { - if (prev & DeclarationMeaning.Method) { - if (meaning !== DeclarationMeaning.Method) { - error(location, Diagnostics.Duplicate_identifier_0, getTextOfNode(location)); - } - } - else if (prev & meaning) { - error(location, Diagnostics.Duplicate_identifier_0, getTextOfNode(location)); - } - else { - names.set(name, prev | meaning); - } + else { + Debug.assert(attributeDecl.kind === SyntaxKind.JsxSpreadAttribute); + if (attributesTable.size > 0) { + spread = getSpreadType(spread, createJsxAttributesType(), attributes.symbol, objectFlags, /*readonly*/ false); + attributesTable = createSymbolTable(); } - else { - names.set(name, meaning); + const exprType = checkExpressionCached(attributeDecl.expression, checkMode); + if (isTypeAny(exprType)) { + hasSpreadAnyType = true; } - } + if (isValidSpreadType(exprType)) { + spread = getSpreadType(spread, exprType, attributes.symbol, objectFlags, /*readonly*/ false); + } + else { + typeToIntersect = typeToIntersect ? getIntersectionType([typeToIntersect, exprType]) : exprType; + } + } + } + if (!hasSpreadAnyType) { + if (attributesTable.size > 0) { + spread = getSpreadType(spread, createJsxAttributesType(), attributes.symbol, objectFlags, /*readonly*/ false); + } + } + // Handle children attribute + const parent = openingLikeElement.parent.kind === SyntaxKind.JsxElement ? openingLikeElement.parent as JsxElement : undefined; + // We have to check that openingElement of the parent is the one we are visiting as this may not be true for selfClosingElement + if (parent && parent.openingElement === openingLikeElement && parent.children.length > 0) { + const childrenTypes: ts.Type[] = checkJsxChildren(parent, checkMode); + if (!hasSpreadAnyType && jsxChildrenPropertyName && jsxChildrenPropertyName !== "") { + // Error if there is a attribute named "children" explicitly specified and children element. + // This is because children element will overwrite the value from attributes. + // Note: we will not warn "children" attribute overwritten if "children" attribute is specified in object spread. + if (explicitlySpecifyChildrenAttribute) { + error(attributes, Diagnostics._0_are_specified_twice_The_attribute_named_0_will_be_overwritten, unescapeLeadingUnderscores(jsxChildrenPropertyName)); + } + const contextualType = getApparentTypeOfContextualType(openingLikeElement.attributes); + const childrenContextualType = contextualType && getTypeOfPropertyOfContextualType(contextualType, jsxChildrenPropertyName); + // If there are children in the body of JSX element, create dummy attribute "children" with the union of children types so that it will pass the attribute checking process + const childrenPropSymbol = createSymbol(SymbolFlags.Property | SymbolFlags.Transient, jsxChildrenPropertyName); + childrenPropSymbol.type = childrenTypes.length === 1 ? + childrenTypes[0] : + (getArrayLiteralTupleTypeIfApplicable(childrenTypes, childrenContextualType, /*hasRestElement*/ false) || createArrayType(getUnionType(childrenTypes))); + // Fake up a property declaration for the children + childrenPropSymbol.valueDeclaration = createPropertySignature(/*modifiers*/ undefined, unescapeLeadingUnderscores(jsxChildrenPropertyName), /*questionToken*/ undefined, /*type*/ undefined, /*initializer*/ undefined); + childrenPropSymbol.valueDeclaration.parent = attributes; + childrenPropSymbol.valueDeclaration.symbol = childrenPropSymbol; + const childPropMap = createSymbolTable(); + childPropMap.set(jsxChildrenPropertyName, childrenPropSymbol); + spread = getSpreadType(spread, createAnonymousType(attributes.symbol, childPropMap, emptyArray, emptyArray, /*stringIndexInfo*/ undefined, /*numberIndexInfo*/ undefined), attributes.symbol, objectFlags, /*readonly*/ false); + } + } + if (hasSpreadAnyType) { + return anyType; } - + if (typeToIntersect && spread !== emptyJsxObjectType) { + return getIntersectionType([typeToIntersect, spread]); + } + return typeToIntersect || (spread === emptyJsxObjectType ? createJsxAttributesType() : spread); /** - * Static members being set on a constructor function may conflict with built-in properties - * of Function. Esp. in ECMAScript 5 there are non-configurable and non-writable - * built-in properties. This check issues a transpile error when a class has a static - * member with the same name as a non-writable built-in property. - * - * @see http://www.ecma-international.org/ecma-262/5.1/#sec-15.3.3 - * @see http://www.ecma-international.org/ecma-262/5.1/#sec-15.3.5 - * @see http://www.ecma-international.org/ecma-262/6.0/#sec-properties-of-the-function-constructor - * @see http://www.ecma-international.org/ecma-262/6.0/#sec-function-instances + * Create anonymous type from given attributes symbol table. + * @param symbol a symbol of JsxAttributes containing attributes corresponding to attributesTable + * @param attributesTable a symbol table of attributes property */ - function checkClassForStaticPropertyNameConflicts(node: ClassLikeDeclaration) { - for (const member of node.members) { - const memberNameNode = member.name; - const isStatic = hasModifier(member, ModifierFlags.Static); - if (isStatic && memberNameNode) { - const memberName = getPropertyNameForPropertyNameNode(memberNameNode); - switch (memberName) { - case "name": - case "length": - case "caller": - case "arguments": - case "prototype": - const message = Diagnostics.Static_property_0_conflicts_with_built_in_property_Function_0_of_constructor_function_1; - const className = getNameOfSymbolAsWritten(getSymbolOfNode(node)); - error(memberNameNode, message, memberName, className); - break; - } - } - } + function createJsxAttributesType() { + objectFlags |= freshObjectLiteralFlag; + const result = createAnonymousType(attributes.symbol, attributesTable, emptyArray, emptyArray, /*stringIndexInfo*/ undefined, /*numberIndexInfo*/ undefined); + result.objectFlags |= objectFlags | ObjectFlags.ObjectLiteral | ObjectFlags.ContainsObjectOrArrayLiteral; + return result; } - - function checkObjectTypeForDuplicateDeclarations(node: TypeLiteralNode | InterfaceDeclaration) { - const names = createMap(); - for (const member of node.members) { - if (member.kind === SyntaxKind.PropertySignature) { - let memberName: string; - const name = member.name!; - switch (name.kind) { - case SyntaxKind.StringLiteral: - case SyntaxKind.NumericLiteral: - memberName = name.text; - break; - case SyntaxKind.Identifier: - memberName = idText(name); - break; - default: - continue; - } - - if (names.get(memberName)) { - error(getNameOfDeclaration(member.symbol.valueDeclaration), Diagnostics.Duplicate_identifier_0, memberName); - error(member.name, Diagnostics.Duplicate_identifier_0, memberName); - } - else { - names.set(memberName, true); - } + } + function checkJsxChildren(node: JsxElement | JsxFragment, checkMode?: CheckMode) { + const childrenTypes: ts.Type[] = []; + for (const child of node.children) { + // In React, JSX text that contains only whitespaces will be ignored so we don't want to type-check that + // because then type of children property will have constituent of string type. + if (child.kind === SyntaxKind.JsxText) { + if (!child.containsOnlyTriviaWhiteSpaces) { + childrenTypes.push(stringType); } } + else { + childrenTypes.push(checkExpressionForMutableLocation(child, checkMode)); + } } - - function checkTypeForDuplicateIndexSignatures(node: Node) { - if (node.kind === SyntaxKind.InterfaceDeclaration) { - const nodeSymbol = getSymbolOfNode(node as InterfaceDeclaration); - // in case of merging interface declaration it is possible that we'll enter this check procedure several times for every declaration - // to prevent this run check only for the first declaration of a given kind - if (nodeSymbol.declarations.length > 0 && nodeSymbol.declarations[0] !== node) { - return; + return childrenTypes; + } + /** + * Check attributes property of opening-like element. This function is called during chooseOverload to get call signature of a JSX opening-like element. + * (See "checkApplicableSignatureForJsxOpeningLikeElement" for how the function is used) + * @param node a JSXAttributes to be resolved of its type + */ + function checkJsxAttributes(node: JsxAttributes, checkMode: CheckMode | undefined) { + return createJsxAttributesTypeFromAttributesProperty(node.parent, checkMode); + } + function getJsxType(name: __String, location: Node | undefined) { + const namespace = getJsxNamespaceAt(location); + const exports = namespace && getExportsOfSymbol(namespace); + const typeSymbol = exports && getSymbol(exports, name, SymbolFlags.Type); + return typeSymbol ? getDeclaredTypeOfSymbol(typeSymbol) : errorType; + } + /** + * Looks up an intrinsic tag name and returns a symbol that either points to an intrinsic + * property (in which case nodeLinks.jsxFlags will be IntrinsicNamedElement) or an intrinsic + * string index signature (in which case nodeLinks.jsxFlags will be IntrinsicIndexedElement). + * May also return unknownSymbol if both of these lookups fail. + */ + function getIntrinsicTagSymbol(node: JsxOpeningLikeElement | JsxClosingElement): ts.Symbol { + const links = getNodeLinks(node); + if (!links.resolvedSymbol) { + const intrinsicElementsType = getJsxType(JsxNames.IntrinsicElements, node); + if (intrinsicElementsType !== errorType) { + // Property case + if (!isIdentifier(node.tagName)) + return Debug.fail(); + const intrinsicProp = getPropertyOfType(intrinsicElementsType, node.tagName.escapedText); + if (intrinsicProp) { + links.jsxFlags |= JsxFlags.IntrinsicNamedElement; + return links.resolvedSymbol = intrinsicProp; + } + // Intrinsic string indexer case + const indexSignatureType = getIndexTypeOfType(intrinsicElementsType, IndexKind.String); + if (indexSignatureType) { + links.jsxFlags |= JsxFlags.IntrinsicIndexedElement; + return links.resolvedSymbol = intrinsicElementsType.symbol; } + // Wasn't found + error(node, Diagnostics.Property_0_does_not_exist_on_type_1, idText(node.tagName), "JSX." + JsxNames.IntrinsicElements); + return links.resolvedSymbol = unknownSymbol; } - - // TypeScript 1.0 spec (April 2014) - // 3.7.4: An object type can contain at most one string index signature and one numeric index signature. - // 8.5: A class declaration can have at most one string index member declaration and one numeric index member declaration - const indexSymbol = getIndexSymbol(getSymbolOfNode(node)!); - if (indexSymbol) { - let seenNumericIndexer = false; - let seenStringIndexer = false; - for (const decl of indexSymbol.declarations) { - const declaration = decl; - if (declaration.parameters.length === 1 && declaration.parameters[0].type) { - switch (declaration.parameters[0].type.kind) { - case SyntaxKind.StringKeyword: - if (!seenStringIndexer) { - seenStringIndexer = true; - } - else { - error(declaration, Diagnostics.Duplicate_string_index_signature); - } - break; - case SyntaxKind.NumberKeyword: - if (!seenNumericIndexer) { - seenNumericIndexer = true; - } - else { - error(declaration, Diagnostics.Duplicate_number_index_signature); - } - break; - } - } + else { + if (noImplicitAny) { + error(node, Diagnostics.JSX_element_implicitly_has_type_any_because_no_interface_JSX_0_exists, unescapeLeadingUnderscores(JsxNames.IntrinsicElements)); } + return links.resolvedSymbol = unknownSymbol; } } - - function checkPropertyDeclaration(node: PropertyDeclaration | PropertySignature) { - // Grammar checking - if (!checkGrammarDecoratorsAndModifiers(node) && !checkGrammarProperty(node)) checkGrammarComputedPropertyName(node.name); - checkVariableLikeDeclaration(node); - - // Private class fields transformation relies on WeakMaps. - if (isPrivateIdentifier(node.name) && languageVersion < ScriptTarget.ESNext) { - for (let lexicalScope = getEnclosingBlockScopeContainer(node); !!lexicalScope; lexicalScope = getEnclosingBlockScopeContainer(lexicalScope)) { - getNodeLinks(lexicalScope).flags |= NodeCheckFlags.ContainsClassWithPrivateIdentifiers; + return links.resolvedSymbol; + } + function getJsxNamespaceAt(location: Node | undefined): ts.Symbol { + const links = location && getNodeLinks(location); + if (links && links.jsxNamespace) { + return links.jsxNamespace; + } + if (!links || links.jsxNamespace !== false) { + const namespaceName = getJsxNamespace(location); + const resolvedNamespace = resolveName(location, namespaceName, SymbolFlags.Namespace, /*diagnosticMessage*/ undefined, namespaceName, /*isUse*/ false); + if (resolvedNamespace) { + const candidate = resolveSymbol(getSymbol(getExportsOfSymbol(resolveSymbol(resolvedNamespace)), JsxNames.JSX, SymbolFlags.Namespace)); + if (candidate) { + if (links) { + links.jsxNamespace = candidate; + } + return candidate; + } + if (links) { + links.jsxNamespace = false; } } } - - function checkPropertySignature(node: PropertySignature) { - if (isPrivateIdentifier(node.name)) { - error(node, Diagnostics.Private_identifiers_are_not_allowed_outside_class_bodies); + // JSX global fallback + return getGlobalSymbol(JsxNames.JSX, SymbolFlags.Namespace, /*diagnosticMessage*/ undefined)!; // TODO: GH#18217 + } + /** + * Look into JSX namespace and then look for container with matching name as nameOfAttribPropContainer. + * Get a single property from that container if existed. Report an error if there are more than one property. + * + * @param nameOfAttribPropContainer a string of value JsxNames.ElementAttributesPropertyNameContainer or JsxNames.ElementChildrenAttributeNameContainer + * if other string is given or the container doesn't exist, return undefined. + */ + function getNameFromJsxElementAttributesContainer(nameOfAttribPropContainer: __String, jsxNamespace: ts.Symbol): __String | undefined { + // JSX.ElementAttributesProperty | JSX.ElementChildrenAttribute [symbol] + const jsxElementAttribPropInterfaceSym = jsxNamespace && getSymbol((jsxNamespace.exports!), nameOfAttribPropContainer, SymbolFlags.Type); + // JSX.ElementAttributesProperty | JSX.ElementChildrenAttribute [type] + const jsxElementAttribPropInterfaceType = jsxElementAttribPropInterfaceSym && getDeclaredTypeOfSymbol(jsxElementAttribPropInterfaceSym); + // The properties of JSX.ElementAttributesProperty | JSX.ElementChildrenAttribute + const propertiesOfJsxElementAttribPropInterface = jsxElementAttribPropInterfaceType && getPropertiesOfType(jsxElementAttribPropInterfaceType); + if (propertiesOfJsxElementAttribPropInterface) { + // Element Attributes has zero properties, so the element attributes type will be the class instance type + if (propertiesOfJsxElementAttribPropInterface.length === 0) { + return "" as __String; + } + // Element Attributes has one property, so the element attributes type will be the type of the corresponding + // property of the class instance type + else if (propertiesOfJsxElementAttribPropInterface.length === 1) { + return propertiesOfJsxElementAttribPropInterface[0].escapedName; + } + else if (propertiesOfJsxElementAttribPropInterface.length > 1) { + // More than one property on ElementAttributesProperty is an error + error(jsxElementAttribPropInterfaceSym!.declarations[0], Diagnostics.The_global_type_JSX_0_may_not_have_more_than_one_property, unescapeLeadingUnderscores(nameOfAttribPropContainer)); + } + } + return undefined; + } + function getJsxLibraryManagedAttributes(jsxNamespace: ts.Symbol) { + // JSX.LibraryManagedAttributes [symbol] + return jsxNamespace && getSymbol((jsxNamespace.exports!), JsxNames.LibraryManagedAttributes, SymbolFlags.Type); + } + /// e.g. "props" for React.d.ts, + /// or 'undefined' if ElementAttributesProperty doesn't exist (which means all + /// non-intrinsic elements' attributes type is 'any'), + /// or '' if it has 0 properties (which means every + /// non-intrinsic elements' attributes type is the element instance type) + function getJsxElementPropertiesName(jsxNamespace: ts.Symbol) { + return getNameFromJsxElementAttributesContainer(JsxNames.ElementAttributesPropertyNameContainer, jsxNamespace); + } + function getJsxElementChildrenPropertyName(jsxNamespace: ts.Symbol): __String | undefined { + return getNameFromJsxElementAttributesContainer(JsxNames.ElementChildrenAttributeNameContainer, jsxNamespace); + } + function getUninstantiatedJsxSignaturesOfType(elementType: ts.Type, caller: JsxOpeningLikeElement): readonly ts.Signature[] { + if (elementType.flags & TypeFlags.String) { + return [anySignature]; + } + else if (elementType.flags & TypeFlags.StringLiteral) { + const intrinsicType = getIntrinsicAttributesTypeFromStringLiteralType((elementType as StringLiteralType), caller); + if (!intrinsicType) { + error(caller, Diagnostics.Property_0_does_not_exist_on_type_1, (elementType as StringLiteralType).value, "JSX." + JsxNames.IntrinsicElements); + return emptyArray; } - return checkPropertyDeclaration(node); - } - - function checkMethodDeclaration(node: MethodDeclaration | MethodSignature) { - // Grammar checking - if (!checkGrammarMethod(node)) checkGrammarComputedPropertyName(node.name); - - if (isPrivateIdentifier(node.name)) { - error(node, Diagnostics.A_method_cannot_be_named_with_a_private_identifier); + else { + const fakeSignature = createSignatureForJSXIntrinsic(caller, intrinsicType); + return [fakeSignature]; } - - // Grammar checking for modifiers is done inside the function checkGrammarFunctionLikeDeclaration - checkFunctionOrMethodDeclaration(node); - - // Abstract methods cannot have an implementation. - // Extra checks are to avoid reporting multiple errors relating to the "abstractness" of the node. - if (hasModifier(node, ModifierFlags.Abstract) && node.kind === SyntaxKind.MethodDeclaration && node.body) { - error(node, Diagnostics.Method_0_cannot_have_an_implementation_because_it_is_marked_abstract, declarationNameToString(node.name)); + } + const apparentElemType = getApparentType(elementType); + // Resolve the signatures, preferring constructor + let signatures = getSignaturesOfType(apparentElemType, SignatureKind.Construct); + if (signatures.length === 0) { + // No construct signatures, try call signatures + signatures = getSignaturesOfType(apparentElemType, SignatureKind.Call); + } + if (signatures.length === 0 && apparentElemType.flags & TypeFlags.Union) { + // If each member has some combination of new/call signatures; make a union signature list for those + signatures = getUnionSignatures(map((apparentElemType as UnionType).types, t => getUninstantiatedJsxSignaturesOfType(t, caller))); + } + return signatures; + } + function getIntrinsicAttributesTypeFromStringLiteralType(type: StringLiteralType, location: Node): ts.Type | undefined { + // If the elemType is a stringLiteral type, we can then provide a check to make sure that the string literal type is one of the Jsx intrinsic element type + // For example: + // var CustomTag: "h1" = "h1"; + // Hello World + const intrinsicElementsType = getJsxType(JsxNames.IntrinsicElements, location); + if (intrinsicElementsType !== errorType) { + const stringLiteralTypeName = type.value; + const intrinsicProp = getPropertyOfType(intrinsicElementsType, escapeLeadingUnderscores(stringLiteralTypeName)); + if (intrinsicProp) { + return getTypeOfSymbol(intrinsicProp); + } + const indexSignatureType = getIndexTypeOfType(intrinsicElementsType, IndexKind.String); + if (indexSignatureType) { + return indexSignatureType; } + return undefined; } - - function checkConstructorDeclaration(node: ConstructorDeclaration) { - // Grammar check on signature of constructor and modifier of the constructor is done in checkSignatureDeclaration function. - checkSignatureDeclaration(node); - // Grammar check for checking only related to constructorDeclaration - if (!checkGrammarConstructorTypeParameters(node)) checkGrammarConstructorTypeAnnotation(node); - - checkSourceElement(node.body); - - const symbol = getSymbolOfNode(node); - const firstDeclaration = getDeclarationOfKind(symbol, node.kind); - - // Only type check the symbol once - if (node === firstDeclaration) { - checkFunctionOrConstructorSymbol(symbol); + // If we need to report an error, we already done so here. So just return any to prevent any more error downstream + return anyType; + } + function checkJsxReturnAssignableToAppropriateBound(refKind: JsxReferenceKind, elemInstanceType: ts.Type, openingLikeElement: Node) { + if (refKind === JsxReferenceKind.Function) { + const sfcReturnConstraint = getJsxStatelessElementTypeAt(openingLikeElement); + if (sfcReturnConstraint) { + checkTypeRelatedTo(elemInstanceType, sfcReturnConstraint, assignableRelation, openingLikeElement, Diagnostics.JSX_element_type_0_is_not_a_constructor_function_for_JSX_elements); } - - // exit early in the case of signature - super checks are not relevant to them - if (nodeIsMissing(node.body)) { - return; + } + else if (refKind === JsxReferenceKind.Component) { + const classConstraint = getJsxElementClassTypeAt(openingLikeElement); + if (classConstraint) { + // Issue an error if this return type isn't assignable to JSX.ElementClass or JSX.Element, failing that + checkTypeRelatedTo(elemInstanceType, classConstraint, assignableRelation, openingLikeElement, Diagnostics.JSX_element_type_0_is_not_a_constructor_function_for_JSX_elements); } - - if (!produceDiagnostics) { + } + else { // Mixed + const sfcReturnConstraint = getJsxStatelessElementTypeAt(openingLikeElement); + const classConstraint = getJsxElementClassTypeAt(openingLikeElement); + if (!sfcReturnConstraint || !classConstraint) { return; } - - function isInstancePropertyWithInitializerOrPrivateIdentifierProperty(n: Node): boolean { - if (isPrivateIdentifierPropertyDeclaration(n)) { - return true; - } - return n.kind === SyntaxKind.PropertyDeclaration && - !hasModifier(n, ModifierFlags.Static) && - !!(n).initializer; + const combined = getUnionType([sfcReturnConstraint, classConstraint]); + checkTypeRelatedTo(elemInstanceType, combined, assignableRelation, openingLikeElement, Diagnostics.JSX_element_type_0_is_not_a_constructor_function_for_JSX_elements); + } + } + /** + * Get attributes type of the given intrinsic opening-like Jsx element by resolving the tag name. + * The function is intended to be called from a function which has checked that the opening element is an intrinsic element. + * @param node an intrinsic JSX opening-like element + */ + function getIntrinsicAttributesTypeFromJsxOpeningLikeElement(node: JsxOpeningLikeElement): ts.Type { + Debug.assert(isJsxIntrinsicIdentifier(node.tagName)); + const links = getNodeLinks(node); + if (!links.resolvedJsxElementAttributesType) { + const symbol = getIntrinsicTagSymbol(node); + if (links.jsxFlags & JsxFlags.IntrinsicNamedElement) { + return links.resolvedJsxElementAttributesType = getTypeOfSymbol(symbol); + } + else if (links.jsxFlags & JsxFlags.IntrinsicIndexedElement) { + return links.resolvedJsxElementAttributesType = + (getIndexTypeOfType(getDeclaredTypeOfSymbol(symbol), IndexKind.String)!); } - - // TS 1.0 spec (April 2014): 8.3.2 - // Constructors of classes with no extends clause may not contain super calls, whereas - // constructors of derived classes must contain at least one super call somewhere in their function body. - const containingClassDecl = node.parent; - if (getClassExtendsHeritageElement(containingClassDecl)) { - captureLexicalThis(node.parent, containingClassDecl); - const classExtendsNull = classDeclarationExtendsNull(containingClassDecl); - const superCall = getSuperCallInConstructor(node); - if (superCall) { - if (classExtendsNull) { - error(superCall, Diagnostics.A_constructor_cannot_contain_a_super_call_when_its_class_extends_null); - } - - // The first statement in the body of a constructor (excluding prologue directives) must be a super call - // if both of the following are true: - // - The containing class is a derived class. - // - The constructor declares parameter properties - // or the containing class declares instance member variables with initializers. - const superCallShouldBeFirst = - some((node.parent).members, isInstancePropertyWithInitializerOrPrivateIdentifierProperty) || - some(node.parameters, p => hasModifier(p, ModifierFlags.ParameterPropertyModifier)); - - // Skip past any prologue directives to find the first statement - // to ensure that it was a super call. - if (superCallShouldBeFirst) { - const statements = node.body!.statements; - let superCallStatement: ExpressionStatement | undefined; - - for (const statement of statements) { - if (statement.kind === SyntaxKind.ExpressionStatement && isSuperCall((statement).expression)) { - superCallStatement = statement; - break; - } - if (!isPrologueDirective(statement)) { - break; - } - } - if (!superCallStatement) { - error(node, Diagnostics.A_super_call_must_be_the_first_statement_in_the_constructor_when_a_class_contains_initialized_properties_parameter_properties_or_private_identifiers); - } - } - } - else if (!classExtendsNull) { - error(node, Diagnostics.Constructors_for_derived_classes_must_contain_a_super_call); - } + else { + return links.resolvedJsxElementAttributesType = errorType; } } - - function checkAccessorDeclaration(node: AccessorDeclaration) { - if (produceDiagnostics) { - // Grammar checking accessors - if (!checkGrammarFunctionLikeDeclaration(node) && !checkGrammarAccessor(node)) checkGrammarComputedPropertyName(node.name); - - checkDecorators(node); - checkSignatureDeclaration(node); - if (node.kind === SyntaxKind.GetAccessor) { - if (!(node.flags & NodeFlags.Ambient) && nodeIsPresent(node.body) && (node.flags & NodeFlags.HasImplicitReturn)) { - if (!(node.flags & NodeFlags.HasExplicitReturn)) { - error(node.name, Diagnostics.A_get_accessor_must_return_a_value); - } - } - } - // Do not use hasDynamicName here, because that returns false for well known symbols. - // We want to perform checkComputedPropertyName for all computed properties, including - // well known symbols. - if (node.name.kind === SyntaxKind.ComputedPropertyName) { - checkComputedPropertyName(node.name); - } - if (isPrivateIdentifier(node.name)) { - error(node.name, Diagnostics.An_accessor_cannot_be_named_with_a_private_identifier); - } - if (!hasNonBindableDynamicName(node)) { - // TypeScript 1.0 spec (April 2014): 8.4.3 - // Accessors for the same member name must specify the same accessibility. - const otherKind = node.kind === SyntaxKind.GetAccessor ? SyntaxKind.SetAccessor : SyntaxKind.GetAccessor; - const otherAccessor = getDeclarationOfKind(getSymbolOfNode(node), otherKind); - if (otherAccessor) { - const nodeFlags = getModifierFlags(node); - const otherFlags = getModifierFlags(otherAccessor); - if ((nodeFlags & ModifierFlags.AccessibilityModifier) !== (otherFlags & ModifierFlags.AccessibilityModifier)) { - error(node.name, Diagnostics.Getter_and_setter_accessors_do_not_agree_in_visibility); - } - if ((nodeFlags & ModifierFlags.Abstract) !== (otherFlags & ModifierFlags.Abstract)) { - error(node.name, Diagnostics.Accessors_must_both_be_abstract_or_non_abstract); - } - - // TypeScript 1.0 spec (April 2014): 4.5 - // If both accessors include type annotations, the specified types must be identical. - checkAccessorDeclarationTypesIdentical(node, otherAccessor, getAnnotatedAccessorType, Diagnostics.get_and_set_accessor_must_have_the_same_type); - checkAccessorDeclarationTypesIdentical(node, otherAccessor, getThisTypeOfDeclaration, Diagnostics.get_and_set_accessor_must_have_the_same_this_type); - } - } - const returnType = getTypeOfAccessors(getSymbolOfNode(node)); - if (node.kind === SyntaxKind.GetAccessor) { - checkAllCodePathsInNonVoidFunctionReturnOrThrow(node, returnType); - } - } - checkSourceElement(node.body); + return links.resolvedJsxElementAttributesType; + } + function getJsxElementClassTypeAt(location: Node): ts.Type | undefined { + const type = getJsxType(JsxNames.ElementClass, location); + if (type === errorType) + return undefined; + return type; + } + function getJsxElementTypeAt(location: Node): ts.Type { + return getJsxType(JsxNames.Element, location); + } + function getJsxStatelessElementTypeAt(location: Node): ts.Type | undefined { + const jsxElementType = getJsxElementTypeAt(location); + if (jsxElementType) { + return getUnionType([jsxElementType, nullType]); } - - function checkAccessorDeclarationTypesIdentical(first: AccessorDeclaration, second: AccessorDeclaration, getAnnotatedType: (a: AccessorDeclaration) => Type | undefined, message: DiagnosticMessage) { - const firstType = getAnnotatedType(first); - const secondType = getAnnotatedType(second); - if (firstType && secondType && !isTypeIdenticalTo(firstType, secondType)) { - error(first, message); - } + } + /** + * Returns all the properties of the Jsx.IntrinsicElements interface + */ + function getJsxIntrinsicTagNamesAt(location: Node): ts.Symbol[] { + const intrinsics = getJsxType(JsxNames.IntrinsicElements, location); + return intrinsics ? getPropertiesOfType(intrinsics) : emptyArray; + } + function checkJsxPreconditions(errorNode: Node) { + // Preconditions for using JSX + if ((compilerOptions.jsx || JsxEmit.None) === JsxEmit.None) { + error(errorNode, Diagnostics.Cannot_use_JSX_unless_the_jsx_flag_is_provided); } - - function checkMissingDeclaration(node: Node) { - checkDecorators(node); + if (getJsxElementTypeAt(errorNode) === undefined) { + if (noImplicitAny) { + error(errorNode, Diagnostics.JSX_element_implicitly_has_type_any_because_the_global_type_JSX_Element_does_not_exist); + } } - - function getEffectiveTypeArguments(node: TypeReferenceNode | ExpressionWithTypeArguments, typeParameters: readonly TypeParameter[]): Type[] { - return fillMissingTypeArguments(map(node.typeArguments!, getTypeFromTypeNode), typeParameters, - getMinTypeArgumentCount(typeParameters), isInJSFile(node)); + } + function checkJsxOpeningLikeElementOrOpeningFragment(node: JsxOpeningLikeElement | JsxOpeningFragment) { + const isNodeOpeningLikeElement = isJsxOpeningLikeElement(node); + if (isNodeOpeningLikeElement) { + checkGrammarJsxElement((node)); + } + checkJsxPreconditions(node); + // The reactNamespace/jsxFactory's root symbol should be marked as 'used' so we don't incorrectly elide its import. + // And if there is no reactNamespace/jsxFactory's symbol in scope when targeting React emit, we should issue an error. + const reactRefErr = diagnostics && compilerOptions.jsx === JsxEmit.React ? Diagnostics.Cannot_find_name_0 : undefined; + const reactNamespace = getJsxNamespace(node); + const reactLocation = isNodeOpeningLikeElement ? (node).tagName : node; + const reactSym = resolveName(reactLocation, reactNamespace, SymbolFlags.Value, reactRefErr, reactNamespace, /*isUse*/ true); + if (reactSym) { + // Mark local symbol as referenced here because it might not have been marked + // if jsx emit was not react as there wont be error being emitted + reactSym.isReferenced = SymbolFlags.All; + // If react symbol is alias, mark it as refereced + if (reactSym.flags & SymbolFlags.Alias && !getTypeOnlyAliasDeclaration(reactSym)) { + markAliasSymbolAsReferenced(reactSym); + } + } + if (isNodeOpeningLikeElement) { + const sig = getResolvedSignature((node as JsxOpeningLikeElement)); + checkJsxReturnAssignableToAppropriateBound(getJsxReferenceKind((node as JsxOpeningLikeElement)), getReturnTypeOfSignature(sig), node); } - - function checkTypeArgumentConstraints(node: TypeReferenceNode | ExpressionWithTypeArguments, typeParameters: readonly TypeParameter[]): boolean { - let typeArguments: Type[] | undefined; - let mapper: TypeMapper | undefined; - let result = true; - for (let i = 0; i < typeParameters.length; i++) { - const constraint = getConstraintOfTypeParameter(typeParameters[i]); - if (constraint) { - if (!typeArguments) { - typeArguments = getEffectiveTypeArguments(node, typeParameters); - mapper = createTypeMapper(typeParameters, typeArguments); - } - result = result && checkTypeAssignableTo( - typeArguments[i], - instantiateType(constraint, mapper), - node.typeArguments![i], - Diagnostics.Type_0_does_not_satisfy_the_constraint_1); - } + } + /** + * Check if a property with the given name is known anywhere in the given type. In an object type, a property + * is considered known if + * 1. the object type is empty and the check is for assignability, or + * 2. if the object type has index signatures, or + * 3. if the property is actually declared in the object type + * (this means that 'toString', for example, is not usually a known property). + * 4. In a union or intersection type, + * a property is considered known if it is known in any constituent type. + * @param targetType a type to search a given name in + * @param name a property name to search + * @param isComparingJsxAttributes a boolean flag indicating whether we are searching in JsxAttributesType + */ + function isKnownProperty(targetType: ts.Type, name: __String, isComparingJsxAttributes: boolean): boolean { + if (targetType.flags & TypeFlags.Object) { + const resolved = resolveStructuredTypeMembers((targetType as ObjectType)); + if (resolved.stringIndexInfo || + resolved.numberIndexInfo && isNumericLiteralName(name) || + getPropertyOfObjectType(targetType, name) || + isComparingJsxAttributes && !isUnhyphenatedJsxName(name)) { + // For JSXAttributes, if the attribute has a hyphenated name, consider that the attribute to be known. + return true; } - return result; } - - function getTypeParametersForTypeReference(node: TypeReferenceNode | ExpressionWithTypeArguments) { - const type = getTypeFromTypeReference(node); - if (type !== errorType) { - const symbol = getNodeLinks(node).resolvedSymbol; - if (symbol) { - return symbol.flags & SymbolFlags.TypeAlias && getSymbolLinks(symbol).typeParameters || - (getObjectFlags(type) & ObjectFlags.Reference ? (type).target.localTypeParameters : undefined); + else if (targetType.flags & TypeFlags.UnionOrIntersection && isExcessPropertyCheckTarget(targetType)) { + for (const t of (targetType as UnionOrIntersectionType).types) { + if (isKnownProperty(t, name, isComparingJsxAttributes)) { + return true; } } - return undefined; } - - function checkTypeReferenceNode(node: TypeReferenceNode | ExpressionWithTypeArguments) { - checkGrammarTypeArguments(node, node.typeArguments); - if (node.kind === SyntaxKind.TypeReference && node.typeName.jsdocDotPos !== undefined && !isInJSFile(node) && !isInJSDoc(node)) { - grammarErrorAtPos(node, node.typeName.jsdocDotPos, 1, Diagnostics.JSDoc_types_can_only_be_used_inside_documentation_comments); - } - forEach(node.typeArguments, checkSourceElement); - const type = getTypeFromTypeReference(node); - if (type !== errorType) { - if (node.typeArguments && produceDiagnostics) { - const typeParameters = getTypeParametersForTypeReference(node); - if (typeParameters) { - checkTypeArgumentConstraints(node, typeParameters); - } - } - if (type.flags & TypeFlags.Enum && getNodeLinks(node).resolvedSymbol!.flags & SymbolFlags.EnumMember) { - error(node, Diagnostics.Enum_type_0_has_members_with_initializers_that_are_not_literals, typeToString(type)); - } + return false; + } + function isExcessPropertyCheckTarget(type: ts.Type): boolean { + return !!(type.flags & TypeFlags.Object && !(getObjectFlags(type) & ObjectFlags.ObjectLiteralPatternWithComputedProperties) || + type.flags & TypeFlags.NonPrimitive || + type.flags & TypeFlags.Union && some((type).types, isExcessPropertyCheckTarget) || + type.flags & TypeFlags.Intersection && every((type).types, isExcessPropertyCheckTarget)); + } + function checkJsxExpression(node: JsxExpression, checkMode?: CheckMode) { + checkGrammarJsxExpression(node); + if (node.expression) { + const type = checkExpression(node.expression, checkMode); + if (node.dotDotDotToken && type !== anyType && !isArrayType(type)) { + error(node, Diagnostics.JSX_spread_child_must_be_an_array_type); } + return type; } - - function getTypeArgumentConstraint(node: TypeNode): Type | undefined { - const typeReferenceNode = tryCast(node.parent, isTypeReferenceType); - if (!typeReferenceNode) return undefined; - const typeParameters = getTypeParametersForTypeReference(typeReferenceNode)!; // TODO: GH#18217 - const constraint = getConstraintOfTypeParameter(typeParameters[typeReferenceNode.typeArguments!.indexOf(node)]); - return constraint && instantiateType(constraint, createTypeMapper(typeParameters, getEffectiveTypeArguments(typeReferenceNode, typeParameters))); + else { + return errorType; } - - function checkTypeQuery(node: TypeQueryNode) { - getTypeFromTypeQueryNode(node); + } + function getDeclarationNodeFlagsFromSymbol(s: ts.Symbol): NodeFlags { + return s.valueDeclaration ? getCombinedNodeFlags(s.valueDeclaration) : 0; + } + /** + * Return whether this symbol is a member of a prototype somewhere + * Note that this is not tracked well within the compiler, so the answer may be incorrect. + */ + function isPrototypeProperty(symbol: ts.Symbol) { + if (symbol.flags & SymbolFlags.Method || getCheckFlags(symbol) & CheckFlags.SyntheticMethod) { + return true; } - - function checkTypeLiteral(node: TypeLiteralNode) { - forEach(node.members, checkSourceElement); - if (produceDiagnostics) { - const type = getTypeFromTypeLiteralOrFunctionOrConstructorTypeNode(node); - checkIndexConstraints(type); - checkTypeForDuplicateIndexSignatures(node); - checkObjectTypeForDuplicateDeclarations(node); - } + if (isInJSFile(symbol.valueDeclaration)) { + const parent = symbol.valueDeclaration.parent; + return parent && isBinaryExpression(parent) && + getAssignmentDeclarationKind(parent) === AssignmentDeclarationKind.PrototypeProperty; } - - function checkArrayType(node: ArrayTypeNode) { - checkSourceElement(node.elementType); + } + /** + * Check whether the requested property access is valid. + * Returns true if node is a valid property access, and false otherwise. + * @param node The node to be checked. + * @param isSuper True if the access is from `super.`. + * @param type The type of the object whose property is being accessed. (Not the type of the property.) + * @param prop The symbol for the property being accessed. + */ + function checkPropertyAccessibility(node: PropertyAccessExpression | QualifiedName | PropertyAccessExpression | VariableDeclaration | ParameterDeclaration | ImportTypeNode | PropertyAssignment | ShorthandPropertyAssignment | BindingElement, isSuper: boolean, type: ts.Type, prop: ts.Symbol): boolean { + const flags = getDeclarationModifierFlagsFromSymbol(prop); + const errorNode = node.kind === SyntaxKind.QualifiedName ? node.right : node.kind === SyntaxKind.ImportType ? node : node.name; + if (getCheckFlags(prop) & CheckFlags.ContainsPrivate) { + // Synthetic property with private constituent property + error(errorNode, Diagnostics.Property_0_has_conflicting_declarations_and_is_inaccessible_in_type_1, symbolToString(prop), typeToString(type)); + return false; } - - function checkTupleType(node: TupleTypeNode) { - const elementTypes = node.elementTypes; - let seenOptionalElement = false; - for (let i = 0; i < elementTypes.length; i++) { - const e = elementTypes[i]; - if (e.kind === SyntaxKind.RestType) { - if (i !== elementTypes.length - 1) { - grammarErrorOnNode(e, Diagnostics.A_rest_element_must_be_last_in_a_tuple_type); - break; - } - if (!isArrayType(getTypeFromTypeNode((e).type))) { - error(e, Diagnostics.A_rest_element_type_must_be_an_array_type); - } - } - else if (e.kind === SyntaxKind.OptionalType) { - seenOptionalElement = true; - } - else if (seenOptionalElement) { - grammarErrorOnNode(e, Diagnostics.A_required_element_cannot_follow_an_optional_element); - break; + if (isSuper) { + // TS 1.0 spec (April 2014): 4.8.2 + // - In a constructor, instance member function, instance member accessor, or + // instance member variable initializer where this references a derived class instance, + // a super property access is permitted and must specify a public instance member function of the base class. + // - In a static member function or static member accessor + // where this references the constructor function object of a derived class, + // a super property access is permitted and must specify a public static member function of the base class. + if (languageVersion < ScriptTarget.ES2015) { + if (symbolHasNonMethodDeclaration(prop)) { + error(errorNode, Diagnostics.Only_public_and_protected_methods_of_the_base_class_are_accessible_via_the_super_keyword); + return false; } } - forEach(node.elementTypes, checkSourceElement); - } - - function checkUnionOrIntersectionType(node: UnionOrIntersectionTypeNode) { - forEach(node.types, checkSourceElement); - } - - function checkIndexedAccessIndexType(type: Type, accessNode: IndexedAccessTypeNode | ElementAccessExpression) { - if (!(type.flags & TypeFlags.IndexedAccess)) { - return type; + if (flags & ModifierFlags.Abstract) { + // A method cannot be accessed in a super property access if the method is abstract. + // This error could mask a private property access error. But, a member + // cannot simultaneously be private and abstract, so this will trigger an + // additional error elsewhere. + error(errorNode, Diagnostics.Abstract_method_0_in_class_1_cannot_be_accessed_via_super_expression, symbolToString(prop), typeToString(getDeclaringClass(prop)!)); + return false; } - // Check if the index type is assignable to 'keyof T' for the object type. - const objectType = (type).objectType; - const indexType = (type).indexType; - if (isTypeAssignableTo(indexType, getIndexType(objectType, /*stringsOnly*/ false))) { - if (accessNode.kind === SyntaxKind.ElementAccessExpression && isAssignmentTarget(accessNode) && - getObjectFlags(objectType) & ObjectFlags.Mapped && getMappedTypeModifiers(objectType) & MappedTypeModifiers.IncludeReadonly) { - error(accessNode, Diagnostics.Index_signature_in_type_0_only_permits_reading, typeToString(objectType)); - } - return type; + } + // Referencing abstract properties within their own constructors is not allowed + if ((flags & ModifierFlags.Abstract) && isThisProperty(node) && symbolHasNonMethodDeclaration(prop)) { + const declaringClassDeclaration = getClassLikeDeclarationOfSymbol((getParentOfSymbol(prop)!)); + if (declaringClassDeclaration && isNodeUsedDuringClassInitialization(node)) { + error(errorNode, Diagnostics.Abstract_property_0_in_class_1_cannot_be_accessed_in_the_constructor, symbolToString(prop), getTextOfIdentifierOrLiteral((declaringClassDeclaration.name!))); // TODO: GH#18217 + return false; } - // Check if we're indexing with a numeric type and if either object or index types - // is a generic type with a constraint that has a numeric index signature. - const apparentObjectType = getApparentType(objectType); - if (getIndexInfoOfType(apparentObjectType, IndexKind.Number) && isTypeAssignableToKind(indexType, TypeFlags.NumberLike)) { - return type; + } + if (isPropertyAccessExpression(node) && isPrivateIdentifier(node.name)) { + if (!getContainingClass(node)) { + error(errorNode, Diagnostics.Private_identifiers_are_not_allowed_outside_class_bodies); + return false; } - if (isGenericObjectType(objectType)) { - const propertyName = getPropertyNameFromIndex(indexType, accessNode); - if (propertyName) { - const propertySymbol = forEachType(apparentObjectType, t => getPropertyOfType(t, propertyName)); - if (propertySymbol && getDeclarationModifierFlagsFromSymbol(propertySymbol) & ModifierFlags.NonPublicAccessibilityModifier) { - error(accessNode, Diagnostics.Private_or_protected_member_0_cannot_be_accessed_on_a_type_parameter, unescapeLeadingUnderscores(propertyName)); - return errorType; - } - } + return true; + } + // Public properties are otherwise accessible. + if (!(flags & ModifierFlags.NonPublicAccessibilityModifier)) { + return true; + } + // Property is known to be private or protected at this point + // Private property is accessible if the property is within the declaring class + if (flags & ModifierFlags.Private) { + const declaringClassDeclaration = (getClassLikeDeclarationOfSymbol((getParentOfSymbol(prop)!))!); + if (!isNodeWithinClass(node, declaringClassDeclaration)) { + error(errorNode, Diagnostics.Property_0_is_private_and_only_accessible_within_class_1, symbolToString(prop), typeToString(getDeclaringClass(prop)!)); + return false; } - error(accessNode, Diagnostics.Type_0_cannot_be_used_to_index_type_1, typeToString(indexType), typeToString(objectType)); - return errorType; + return true; } - - function checkIndexedAccessType(node: IndexedAccessTypeNode) { - checkSourceElement(node.objectType); - checkSourceElement(node.indexType); - checkIndexedAccessIndexType(getTypeFromIndexedAccessTypeNode(node), node); + // Property is known to be protected at this point + // All protected properties of a supertype are accessible in a super access + if (isSuper) { + return true; } - - function checkMappedType(node: MappedTypeNode) { - checkSourceElement(node.typeParameter); - checkSourceElement(node.type); - - if (!node.type) { - reportImplicitAny(node, anyType); + // Find the first enclosing class that has the declaring classes of the protected constituents + // of the property as base classes + let enclosingClass = forEachEnclosingClass(node, enclosingDeclaration => { + const enclosingClass = (getDeclaredTypeOfSymbol(getSymbolOfNode(enclosingDeclaration)!)); + return isClassDerivedFromDeclaringClasses(enclosingClass, prop) ? enclosingClass : undefined; + }); + // A protected property is accessible if the property is within the declaring class or classes derived from it + if (!enclosingClass) { + // allow PropertyAccessibility if context is in function with this parameter + // static member access is disallow + let thisParameter: ParameterDeclaration | undefined; + if (flags & ModifierFlags.Static || !(thisParameter = getThisParameterFromNodeContext(node)) || !thisParameter.type) { + error(errorNode, Diagnostics.Property_0_is_protected_and_only_accessible_within_class_1_and_its_subclasses, symbolToString(prop), typeToString(getDeclaringClass(prop) || type)); + return false; } - - const type = getTypeFromMappedTypeNode(node); - const constraintType = getConstraintTypeFromMappedType(type); - checkTypeAssignableTo(constraintType, keyofConstraintType, getEffectiveConstraintOfTypeParameter(node.typeParameter)); + const thisType = getTypeFromTypeNode(thisParameter.type); + enclosingClass = (((thisType.flags & TypeFlags.TypeParameter) ? getConstraintOfTypeParameter((thisType)) : thisType) as TypeReference).target; } - - function checkThisType(node: ThisTypeNode) { - getTypeFromThisTypeNode(node); + // No further restrictions for static properties + if (flags & ModifierFlags.Static) { + return true; } - - function checkTypeOperator(node: TypeOperatorNode) { - checkGrammarTypeOperatorNode(node); - checkSourceElement(node.type); + if (type.flags & TypeFlags.TypeParameter) { + // get the original type -- represented as the type constraint of the 'this' type + type = (type as TypeParameter).isThisType ? getConstraintOfTypeParameter((type))! : getBaseConstraintOfType((type))!; // TODO: GH#18217 Use a different variable that's allowed to be undefined } - - function checkConditionalType(node: ConditionalTypeNode) { - forEachChild(node, checkSourceElement); + if (!type || !hasBaseType(type, enclosingClass)) { + error(errorNode, Diagnostics.Property_0_is_protected_and_only_accessible_through_an_instance_of_class_1, symbolToString(prop), typeToString(enclosingClass)); + return false; } - - function checkInferType(node: InferTypeNode) { - if (!findAncestor(node, n => n.parent && n.parent.kind === SyntaxKind.ConditionalType && (n.parent).extendsType === n)) { - grammarErrorOnNode(node, Diagnostics.infer_declarations_are_only_permitted_in_the_extends_clause_of_a_conditional_type); - } - checkSourceElement(node.typeParameter); - registerForUnusedIdentifiersCheck(node); + return true; + } + function getThisParameterFromNodeContext(node: Node) { + const thisContainer = getThisContainer(node, /* includeArrowFunctions */ false); + return thisContainer && isFunctionLike(thisContainer) ? getThisParameter(thisContainer) : undefined; + } + function symbolHasNonMethodDeclaration(symbol: ts.Symbol) { + return !!forEachProperty(symbol, prop => !(prop.flags & SymbolFlags.Method)); + } + function checkNonNullExpression(node: Expression | QualifiedName) { + return checkNonNullType(checkExpression(node), node); + } + function isNullableType(type: ts.Type) { + return !!((strictNullChecks ? getFalsyFlags(type) : type.flags) & TypeFlags.Nullable); + } + function getNonNullableTypeIfNeeded(type: ts.Type) { + return isNullableType(type) ? getNonNullableType(type) : type; + } + function reportObjectPossiblyNullOrUndefinedError(node: Node, flags: TypeFlags) { + error(node, flags & TypeFlags.Undefined ? flags & TypeFlags.Null ? + Diagnostics.Object_is_possibly_null_or_undefined : + Diagnostics.Object_is_possibly_undefined : + Diagnostics.Object_is_possibly_null); + } + function reportCannotInvokePossiblyNullOrUndefinedError(node: Node, flags: TypeFlags) { + error(node, flags & TypeFlags.Undefined ? flags & TypeFlags.Null ? + Diagnostics.Cannot_invoke_an_object_which_is_possibly_null_or_undefined : + Diagnostics.Cannot_invoke_an_object_which_is_possibly_undefined : + Diagnostics.Cannot_invoke_an_object_which_is_possibly_null); + } + function checkNonNullTypeWithReporter(type: ts.Type, node: Node, reportError: (node: Node, kind: TypeFlags) => void): ts.Type { + if (strictNullChecks && type.flags & TypeFlags.Unknown) { + error(node, Diagnostics.Object_is_of_type_unknown); + return errorType; } - - function checkImportType(node: ImportTypeNode) { - checkSourceElement(node.argument); - getTypeFromTypeNode(node); + const kind = (strictNullChecks ? getFalsyFlags(type) : type.flags) & TypeFlags.Nullable; + if (kind) { + reportError(node, kind); + const t = getNonNullableType(type); + return t.flags & (TypeFlags.Nullable | TypeFlags.Never) ? errorType : t; } - - function isPrivateWithinAmbient(node: Node): boolean { - return (hasModifier(node, ModifierFlags.Private) || isPrivateIdentifierPropertyDeclaration(node)) && !!(node.flags & NodeFlags.Ambient); + return type; + } + function checkNonNullType(type: ts.Type, node: Node) { + return checkNonNullTypeWithReporter(type, node, reportObjectPossiblyNullOrUndefinedError); + } + function checkNonNullNonVoidType(type: ts.Type, node: Node): ts.Type { + const nonNullType = checkNonNullType(type, node); + if (nonNullType !== errorType && nonNullType.flags & TypeFlags.Void) { + error(node, Diagnostics.Object_is_possibly_undefined); } - - function getEffectiveDeclarationFlags(n: Declaration, flagsToCheck: ModifierFlags): ModifierFlags { - let flags = getCombinedModifierFlags(n); - - // children of classes (even ambient classes) should not be marked as ambient or export - // because those flags have no useful semantics there. - if (n.parent.kind !== SyntaxKind.InterfaceDeclaration && - n.parent.kind !== SyntaxKind.ClassDeclaration && - n.parent.kind !== SyntaxKind.ClassExpression && - n.flags & NodeFlags.Ambient) { - if (!(flags & ModifierFlags.Ambient) && !(isModuleBlock(n.parent) && isModuleDeclaration(n.parent.parent) && isGlobalScopeAugmentation(n.parent.parent))) { - // It is nested in an ambient context, which means it is automatically exported - flags |= ModifierFlags.Export; - } - flags |= ModifierFlags.Ambient; - } - - return flags & flagsToCheck; + return nonNullType; + } + function checkPropertyAccessExpression(node: PropertyAccessExpression) { + return node.flags & NodeFlags.OptionalChain ? checkPropertyAccessChain((node as PropertyAccessChain)) : + checkPropertyAccessExpressionOrQualifiedName(node, node.expression, checkNonNullExpression(node.expression), node.name); + } + function checkPropertyAccessChain(node: PropertyAccessChain) { + const leftType = checkExpression(node.expression); + const nonOptionalType = getOptionalExpressionType(leftType, node.expression); + return propagateOptionalTypeMarker(checkPropertyAccessExpressionOrQualifiedName(node, node.expression, checkNonNullType(nonOptionalType, node.expression), node.name), node, nonOptionalType !== leftType); + } + function checkQualifiedName(node: QualifiedName) { + return checkPropertyAccessExpressionOrQualifiedName(node, node.left, checkNonNullExpression(node.left), node.right); + } + function isMethodAccessForCall(node: Node) { + while (node.parent.kind === SyntaxKind.ParenthesizedExpression) { + node = node.parent; } - - function checkFunctionOrConstructorSymbol(symbol: Symbol): void { - if (!produceDiagnostics) { - return; + return isCallOrNewExpression(node.parent) && node.parent.expression === node; + } + // Lookup the private identifier lexically. + function lookupSymbolForPrivateIdentifierDeclaration(propName: __String, location: Node): ts.Symbol | undefined { + for (let containingClass = getContainingClass(location); !!containingClass; containingClass = getContainingClass(containingClass)) { + const { symbol } = containingClass; + const name = getSymbolNameForPrivateIdentifier(symbol, propName); + const prop = (symbol.members && symbol.members.get(name)) || (symbol.exports && symbol.exports.get(name)); + if (prop) { + return prop; } - - function getCanonicalOverload(overloads: Declaration[], implementation: FunctionLikeDeclaration | undefined): Declaration { - // Consider the canonical set of flags to be the flags of the bodyDeclaration or the first declaration - // Error on all deviations from this canonical set of flags - // The caveat is that if some overloads are defined in lib.d.ts, we don't want to - // report the errors on those. To achieve this, we will say that the implementation is - // the canonical signature only if it is in the same container as the first overload - const implementationSharesContainerWithFirstOverload = implementation !== undefined && implementation.parent === overloads[0].parent; - return implementationSharesContainerWithFirstOverload ? implementation! : overloads[0]; + } + } + function getPrivateIdentifierPropertyOfType(leftType: ts.Type, lexicallyScopedIdentifier: ts.Symbol): ts.Symbol | undefined { + return getPropertyOfType(leftType, lexicallyScopedIdentifier.escapedName); + } + function checkPrivateIdentifierPropertyAccess(leftType: ts.Type, right: PrivateIdentifier, lexicallyScopedIdentifier: ts.Symbol | undefined): boolean { + // Either the identifier could not be looked up in the lexical scope OR the lexically scoped identifier did not exist on the type. + // Find a private identifier with the same description on the type. + let propertyOnType: ts.Symbol | undefined; + const properties = getPropertiesOfType(leftType); + if (properties) { + forEach(properties, (symbol: ts.Symbol) => { + const decl = symbol.valueDeclaration; + if (decl && isNamedDeclaration(decl) && isPrivateIdentifier(decl.name) && decl.name.escapedText === right.escapedText) { + propertyOnType = symbol; + return true; + } + }); + } + const diagName = diagnosticName(right); + if (propertyOnType) { + const typeValueDecl = propertyOnType.valueDeclaration; + const typeClass = getContainingClass(typeValueDecl); + Debug.assert(!!typeClass); + // We found a private identifier property with the same description. + // Either: + // - There is a lexically scoped private identifier AND it shadows the one we found on the type. + // - It is an attempt to access the private identifier outside of the class. + if (lexicallyScopedIdentifier) { + const lexicalValueDecl = lexicallyScopedIdentifier.valueDeclaration; + const lexicalClass = getContainingClass(lexicalValueDecl); + Debug.assert(!!lexicalClass); + if (findAncestor(lexicalClass, n => typeClass === n)) { + const diagnostic = error(right, Diagnostics.The_property_0_cannot_be_accessed_on_type_1_within_this_class_because_it_is_shadowed_by_another_private_identifier_with_the_same_spelling, diagName, typeToString(leftType)); + addRelatedInfo(diagnostic, createDiagnosticForNode(lexicalValueDecl, Diagnostics.The_shadowing_declaration_of_0_is_defined_here, diagName), createDiagnosticForNode(typeValueDecl, Diagnostics.The_declaration_of_0_that_you_probably_intended_to_use_is_defined_here, diagName)); + return true; + } } - - function checkFlagAgreementBetweenOverloads(overloads: Declaration[], implementation: FunctionLikeDeclaration | undefined, flagsToCheck: ModifierFlags, someOverloadFlags: ModifierFlags, allOverloadFlags: ModifierFlags): void { - // Error if some overloads have a flag that is not shared by all overloads. To find the - // deviations, we XOR someOverloadFlags with allOverloadFlags - const someButNotAllOverloadFlags = someOverloadFlags ^ allOverloadFlags; - if (someButNotAllOverloadFlags !== 0) { - const canonicalFlags = getEffectiveDeclarationFlags(getCanonicalOverload(overloads, implementation), flagsToCheck); - - forEach(overloads, o => { - const deviation = getEffectiveDeclarationFlags(o, flagsToCheck) ^ canonicalFlags; - if (deviation & ModifierFlags.Export) { - error(getNameOfDeclaration(o), Diagnostics.Overload_signatures_must_all_be_exported_or_non_exported); - } - else if (deviation & ModifierFlags.Ambient) { - error(getNameOfDeclaration(o), Diagnostics.Overload_signatures_must_all_be_ambient_or_non_ambient); - } - else if (deviation & (ModifierFlags.Private | ModifierFlags.Protected)) { - error(getNameOfDeclaration(o) || o, Diagnostics.Overload_signatures_must_all_be_public_private_or_protected); - } - else if (deviation & ModifierFlags.Abstract) { - error(getNameOfDeclaration(o), Diagnostics.Overload_signatures_must_all_be_abstract_or_non_abstract); - } - }); + error(right, Diagnostics.Property_0_is_not_accessible_outside_class_1_because_it_has_a_private_identifier, diagName, diagnosticName(typeClass.name || anon)); + return true; + } + return false; + } + function checkPropertyAccessExpressionOrQualifiedName(node: PropertyAccessExpression | QualifiedName, left: Expression | QualifiedName, leftType: ts.Type, right: Identifier | PrivateIdentifier) { + const parentSymbol = getNodeLinks(left).resolvedSymbol; + const assignmentKind = getAssignmentTargetKind(node); + const apparentType = getApparentType(assignmentKind !== AssignmentKind.None || isMethodAccessForCall(node) ? getWidenedType(leftType) : leftType); + if (isPrivateIdentifier(right)) { + checkExternalEmitHelpers(node, ExternalEmitHelpers.ClassPrivateFieldGet); + } + const isAnyLike = isTypeAny(apparentType) || apparentType === silentNeverType; + let prop: ts.Symbol | undefined; + if (isPrivateIdentifier(right)) { + const lexicallyScopedSymbol = lookupSymbolForPrivateIdentifierDeclaration(right.escapedText, right); + if (isAnyLike) { + if (lexicallyScopedSymbol) { + return apparentType; + } + if (!getContainingClass(right)) { + grammarErrorOnNode(right, Diagnostics.Private_identifiers_are_not_allowed_outside_class_bodies); + return anyType; } } - - function checkQuestionTokenAgreementBetweenOverloads(overloads: Declaration[], implementation: FunctionLikeDeclaration | undefined, someHaveQuestionToken: boolean, allHaveQuestionToken: boolean): void { - if (someHaveQuestionToken !== allHaveQuestionToken) { - const canonicalHasQuestionToken = hasQuestionToken(getCanonicalOverload(overloads, implementation)); - forEach(overloads, o => { - const deviation = hasQuestionToken(o) !== canonicalHasQuestionToken; - if (deviation) { - error(getNameOfDeclaration(o), Diagnostics.Overload_signatures_must_all_be_optional_or_required); - } - }); + prop = lexicallyScopedSymbol ? getPrivateIdentifierPropertyOfType(leftType, lexicallyScopedSymbol) : undefined; + // Check for private-identifier-specific shadowing and lexical-scoping errors. + if (!prop && checkPrivateIdentifierPropertyAccess(leftType, right, lexicallyScopedSymbol)) { + return errorType; + } + } + else { + if (isAnyLike) { + if (isIdentifier(left) && parentSymbol) { + markAliasReferenced(parentSymbol, node); } + return apparentType; } - - const flagsToCheck: ModifierFlags = ModifierFlags.Export | ModifierFlags.Ambient | ModifierFlags.Private | ModifierFlags.Protected | ModifierFlags.Abstract; - let someNodeFlags: ModifierFlags = ModifierFlags.None; - let allNodeFlags = flagsToCheck; - let someHaveQuestionToken = false; - let allHaveQuestionToken = true; - let hasOverloads = false; - let bodyDeclaration: FunctionLikeDeclaration | undefined; - let lastSeenNonAmbientDeclaration: FunctionLikeDeclaration | undefined; - let previousDeclaration: SignatureDeclaration | undefined; - - const declarations = symbol.declarations; - const isConstructor = (symbol.flags & SymbolFlags.Constructor) !== 0; - - function reportImplementationExpectedError(node: SignatureDeclaration): void { - if (node.name && nodeIsMissing(node.name)) { - return; + prop = getPropertyOfType(apparentType, right.escapedText); + } + if (isIdentifier(left) && parentSymbol && !(prop && isConstEnumOrConstEnumOnlyModule(prop))) { + markAliasReferenced(parentSymbol, node); + } + let propType: ts.Type; + if (!prop) { + const indexInfo = !isPrivateIdentifier(right) && (assignmentKind === AssignmentKind.None || !isGenericObjectType(leftType) || isThisTypeParameter(leftType)) ? getIndexInfoOfType(apparentType, IndexKind.String) : undefined; + if (!(indexInfo && indexInfo.type)) { + if (isJSLiteralType(leftType)) { + return anyType; } - - let seen = false; - const subsequentNode = forEachChild(node.parent, c => { - if (seen) { - return c; - } - else { - seen = c === node; + if (leftType.symbol === globalThisSymbol) { + if (globalThisSymbol.exports!.has(right.escapedText) && (globalThisSymbol.exports!.get(right.escapedText)!.flags & SymbolFlags.BlockScoped)) { + error(right, Diagnostics.Property_0_does_not_exist_on_type_1, unescapeLeadingUnderscores(right.escapedText), typeToString(leftType)); } - }); - // We may be here because of some extra nodes between overloads that could not be parsed into a valid node. - // In this case the subsequent node is not really consecutive (.pos !== node.end), and we must ignore it here. - if (subsequentNode && subsequentNode.pos === node.end) { - if (subsequentNode.kind === node.kind) { - const errorNode: Node = (subsequentNode).name || subsequentNode; - const subsequentName = (subsequentNode).name; - if (node.name && subsequentName && ( - // both are private identifiers - isPrivateIdentifier(node.name) && isPrivateIdentifier(subsequentName) && node.name.escapedText === subsequentName.escapedText || - // Both are computed property names - // TODO: GH#17345: These are methods, so handle computed name case. (`Always allowing computed property names is *not* the correct behavior!) - isComputedPropertyName(node.name) && isComputedPropertyName(subsequentName) || - // Both are literal property names that are the same. - isPropertyNameLiteral(node.name) && isPropertyNameLiteral(subsequentName) && - getEscapedTextOfIdentifierOrLiteral(node.name) === getEscapedTextOfIdentifierOrLiteral(subsequentName) - )) { - const reportError = - (node.kind === SyntaxKind.MethodDeclaration || node.kind === SyntaxKind.MethodSignature) && - hasModifier(node, ModifierFlags.Static) !== hasModifier(subsequentNode, ModifierFlags.Static); - // we can get here in two cases - // 1. mixed static and instance class members - // 2. something with the same name was defined before the set of overloads that prevents them from merging - // here we'll report error only for the first case since for second we should already report error in binder - if (reportError) { - const diagnostic = hasModifier(node, ModifierFlags.Static) ? Diagnostics.Function_overload_must_be_static : Diagnostics.Function_overload_must_not_be_static; - error(errorNode, diagnostic); - } - return; - } - if (nodeIsPresent((subsequentNode).body)) { - error(errorNode, Diagnostics.Function_implementation_name_must_be_0, declarationNameToString(node.name)); - return; - } + else if (noImplicitAny) { + error(right, Diagnostics.Element_implicitly_has_an_any_type_because_type_0_has_no_index_signature, typeToString(leftType)); } + return anyType; } - const errorNode: Node = node.name || node; - if (isConstructor) { - error(errorNode, Diagnostics.Constructor_implementation_is_missing); - } - else { - // Report different errors regarding non-consecutive blocks of declarations depending on whether - // the node in question is abstract. - if (hasModifier(node, ModifierFlags.Abstract)) { - error(errorNode, Diagnostics.All_declarations_of_an_abstract_method_must_be_consecutive); - } - else { - error(errorNode, Diagnostics.Function_implementation_is_missing_or_not_immediately_following_the_declaration); - } + if (right.escapedText && !checkAndReportErrorForExtendingInterface(node)) { + reportNonexistentProperty(right, isThisTypeParameter(leftType) ? apparentType : leftType); } + return errorType; } - - let duplicateFunctionDeclaration = false; - let multipleConstructorImplementation = false; - let hasNonAmbientClass = false; - for (const current of declarations) { - const node = current; - const inAmbientContext = node.flags & NodeFlags.Ambient; - const inAmbientContextOrInterface = node.parent.kind === SyntaxKind.InterfaceDeclaration || node.parent.kind === SyntaxKind.TypeLiteral || inAmbientContext; - if (inAmbientContextOrInterface) { - // check if declarations are consecutive only if they are non-ambient - // 1. ambient declarations can be interleaved - // i.e. this is legal - // declare function foo(); - // declare function bar(); - // declare function foo(); - // 2. mixing ambient and non-ambient declarations is a separate error that will be reported - do not want to report an extra one - previousDeclaration = undefined; + if (indexInfo.isReadonly && (isAssignmentTarget(node) || isDeleteTarget(node))) { + error(node, Diagnostics.Index_signature_in_type_0_only_permits_reading, typeToString(apparentType)); + } + propType = indexInfo.type; + } + else { + checkPropertyNotUsedBeforeDeclaration(prop, node, right); + markPropertyAsReferenced(prop, node, left.kind === SyntaxKind.ThisKeyword); + getNodeLinks(node).resolvedSymbol = prop; + checkPropertyAccessibility(node, left.kind === SyntaxKind.SuperKeyword, apparentType, prop); + if (isAssignmentToReadonlyEntity((node as Expression), prop, assignmentKind)) { + error(right, Diagnostics.Cannot_assign_to_0_because_it_is_a_read_only_property, idText(right)); + return errorType; + } + propType = getConstraintForLocation(getTypeOfSymbol(prop), node); + } + return getFlowTypeOfAccessExpression(node, prop, propType, right); + } + function getFlowTypeOfAccessExpression(node: ElementAccessExpression | PropertyAccessExpression | QualifiedName, prop: ts.Symbol | undefined, propType: ts.Type, errorNode: Node) { + // Only compute control flow type if this is a property access expression that isn't an + // assignment target, and the referenced property was declared as a variable, property, + // accessor, or optional method. + const assignmentKind = getAssignmentTargetKind(node); + if (!isAccessExpression(node) || + assignmentKind === AssignmentKind.Definite || + prop && !(prop.flags & (SymbolFlags.Variable | SymbolFlags.Property | SymbolFlags.Accessor)) && !(prop.flags & SymbolFlags.Method && propType.flags & TypeFlags.Union)) { + return propType; + } + // If strict null checks and strict property initialization checks are enabled, if we have + // a this.xxx property access, if the property is an instance property without an initializer, + // and if we are in a constructor of the same class as the property declaration, assume that + // the property is uninitialized at the top of the control flow. + let assumeUninitialized = false; + if (strictNullChecks && strictPropertyInitialization && node.expression.kind === SyntaxKind.ThisKeyword) { + const declaration = prop && prop.valueDeclaration; + if (declaration && isInstancePropertyWithoutInitializer(declaration)) { + const flowContainer = getControlFlowContainer(node); + if (flowContainer.kind === SyntaxKind.Constructor && flowContainer.parent === declaration.parent && !(declaration.flags & NodeFlags.Ambient)) { + assumeUninitialized = true; + } + } + } + else if (strictNullChecks && prop && prop.valueDeclaration && + isPropertyAccessExpression(prop.valueDeclaration) && + getAssignmentDeclarationPropertyAccessKind(prop.valueDeclaration) && + getControlFlowContainer(node) === getControlFlowContainer(prop.valueDeclaration)) { + assumeUninitialized = true; + } + const flowType = getFlowTypeOfReference(node, propType, assumeUninitialized ? getOptionalType(propType) : propType); + if (assumeUninitialized && !(getFalsyFlags(propType) & TypeFlags.Undefined) && getFalsyFlags(flowType) & TypeFlags.Undefined) { + error(errorNode, Diagnostics.Property_0_is_used_before_being_assigned, symbolToString(prop!)); // TODO: GH#18217 + // Return the declared type to reduce follow-on errors + return propType; + } + return assignmentKind ? getBaseTypeOfLiteralType(flowType) : flowType; + } + function checkPropertyNotUsedBeforeDeclaration(prop: ts.Symbol, node: PropertyAccessExpression | QualifiedName, right: Identifier | PrivateIdentifier): void { + const { valueDeclaration } = prop; + if (!valueDeclaration || getSourceFileOfNode(node).isDeclarationFile) { + return; + } + let diagnosticMessage; + const declarationName = idText(right); + if (isInPropertyInitializer(node) + && !(isAccessExpression(node) && isAccessExpression(node.expression)) + && !isBlockScopedNameDeclaredBeforeUse(valueDeclaration, right) + && !isPropertyDeclaredInAncestorClass(prop)) { + diagnosticMessage = error(right, Diagnostics.Property_0_is_used_before_its_initialization, declarationName); + } + else if (valueDeclaration.kind === SyntaxKind.ClassDeclaration && + node.parent.kind !== SyntaxKind.TypeReference && + !(valueDeclaration.flags & NodeFlags.Ambient) && + !isBlockScopedNameDeclaredBeforeUse(valueDeclaration, right)) { + diagnosticMessage = error(right, Diagnostics.Class_0_used_before_its_declaration, declarationName); + } + if (diagnosticMessage) { + addRelatedInfo(diagnosticMessage, createDiagnosticForNode(valueDeclaration, Diagnostics._0_is_declared_here, declarationName)); + } + } + function isInPropertyInitializer(node: Node): boolean { + return !!findAncestor(node, node => { + switch (node.kind) { + case SyntaxKind.PropertyDeclaration: + return true; + case SyntaxKind.PropertyAssignment: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + case SyntaxKind.SpreadAssignment: + case SyntaxKind.ComputedPropertyName: + case SyntaxKind.TemplateSpan: + case SyntaxKind.JsxExpression: + case SyntaxKind.JsxAttribute: + case SyntaxKind.JsxAttributes: + case SyntaxKind.JsxSpreadAttribute: + case SyntaxKind.JsxOpeningElement: + case SyntaxKind.ExpressionWithTypeArguments: + case SyntaxKind.HeritageClause: + return false; + default: + return isExpressionNode(node) ? false : "quit"; + } + }); + } + /** + * It's possible that "prop.valueDeclaration" is a local declaration, but the property was also declared in a superclass. + * In that case we won't consider it used before its declaration, because it gets its value from the superclass' declaration. + */ + function isPropertyDeclaredInAncestorClass(prop: ts.Symbol): boolean { + if (!(prop.parent!.flags & SymbolFlags.Class)) { + return false; + } + let classType: InterfaceType | undefined = (getTypeOfSymbol(prop.parent!) as InterfaceType); + while (true) { + classType = classType.symbol && (getSuperClass(classType) as InterfaceType | undefined); + if (!classType) { + return false; + } + const superProperty = getPropertyOfType(classType, prop.escapedName); + if (superProperty && superProperty.valueDeclaration) { + return true; + } + } + } + function getSuperClass(classType: InterfaceType): ts.Type | undefined { + const x = getBaseTypes(classType); + if (x.length === 0) { + return undefined; + } + return getIntersectionType(x); + } + function reportNonexistentProperty(propNode: Identifier | PrivateIdentifier, containingType: ts.Type) { + let errorInfo: DiagnosticMessageChain | undefined; + let relatedInfo: Diagnostic | undefined; + if (!isPrivateIdentifier(propNode) && containingType.flags & TypeFlags.Union && !(containingType.flags & TypeFlags.Primitive)) { + for (const subtype of (containingType as UnionType).types) { + if (!getPropertyOfType(subtype, propNode.escapedText) && !getIndexInfoOfType(subtype, IndexKind.String)) { + errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_does_not_exist_on_type_1, declarationNameToString(propNode), typeToString(subtype)); + break; } - - if ((node.kind === SyntaxKind.ClassDeclaration || node.kind === SyntaxKind.ClassExpression) && !inAmbientContext) { - hasNonAmbientClass = true; + } + } + if (typeHasStaticProperty(propNode.escapedText, containingType)) { + errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_is_a_static_member_of_type_1, declarationNameToString(propNode), typeToString(containingType)); + } + else { + const promisedType = getPromisedTypeOfPromise(containingType); + if (promisedType && getPropertyOfType(promisedType, propNode.escapedText)) { + errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_does_not_exist_on_type_1, declarationNameToString(propNode), typeToString(containingType)); + relatedInfo = createDiagnosticForNode(propNode, Diagnostics.Did_you_forget_to_use_await); + } + else { + const suggestion = getSuggestedSymbolForNonexistentProperty(propNode, containingType); + if (suggestion !== undefined) { + const suggestedName = symbolName(suggestion); + errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_2, declarationNameToString(propNode), typeToString(containingType), suggestedName); + relatedInfo = suggestion.valueDeclaration && createDiagnosticForNode(suggestion.valueDeclaration, Diagnostics._0_is_declared_here, suggestedName); } - - if (node.kind === SyntaxKind.FunctionDeclaration || node.kind === SyntaxKind.MethodDeclaration || node.kind === SyntaxKind.MethodSignature || node.kind === SyntaxKind.Constructor) { - const currentNodeFlags = getEffectiveDeclarationFlags(node, flagsToCheck); - someNodeFlags |= currentNodeFlags; - allNodeFlags &= currentNodeFlags; - someHaveQuestionToken = someHaveQuestionToken || hasQuestionToken(node); - allHaveQuestionToken = allHaveQuestionToken && hasQuestionToken(node); - - if (nodeIsPresent((node as FunctionLikeDeclaration).body) && bodyDeclaration) { - if (isConstructor) { - multipleConstructorImplementation = true; - } - else { - duplicateFunctionDeclaration = true; - } - } - else if (previousDeclaration && previousDeclaration.parent === node.parent && previousDeclaration.end !== node.pos) { - reportImplementationExpectedError(previousDeclaration); - } - - if (nodeIsPresent((node as FunctionLikeDeclaration).body)) { - if (!bodyDeclaration) { - bodyDeclaration = node as FunctionLikeDeclaration; - } - } - else { - hasOverloads = true; - } - - previousDeclaration = node; - - if (!inAmbientContextOrInterface) { - lastSeenNonAmbientDeclaration = node as FunctionLikeDeclaration; - } + else { + errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_does_not_exist_on_type_1, declarationNameToString(propNode), typeToString(containingType)); } } - - if (multipleConstructorImplementation) { - forEach(declarations, declaration => { - error(declaration, Diagnostics.Multiple_constructor_implementations_are_not_allowed); - }); + } + const resultDiagnostic = createDiagnosticForNodeFromMessageChain(propNode, errorInfo); + if (relatedInfo) { + addRelatedInfo(resultDiagnostic, relatedInfo); + } + diagnostics.add(resultDiagnostic); + } + function typeHasStaticProperty(propName: __String, containingType: ts.Type): boolean { + const prop = containingType.symbol && getPropertyOfType(getTypeOfSymbol(containingType.symbol), propName); + return prop !== undefined && prop.valueDeclaration && hasModifier(prop.valueDeclaration, ModifierFlags.Static); + } + function getSuggestedSymbolForNonexistentProperty(name: Identifier | PrivateIdentifier | string, containingType: ts.Type): ts.Symbol | undefined { + return getSpellingSuggestionForName(isString(name) ? name : idText(name), getPropertiesOfType(containingType), SymbolFlags.Value); + } + function getSuggestionForNonexistentProperty(name: Identifier | PrivateIdentifier | string, containingType: ts.Type): string | undefined { + const suggestion = getSuggestedSymbolForNonexistentProperty(name, containingType); + return suggestion && symbolName(suggestion); + } + function getSuggestedSymbolForNonexistentSymbol(location: Node | undefined, outerName: __String, meaning: SymbolFlags): ts.Symbol | undefined { + Debug.assert(outerName !== undefined, "outername should always be defined"); + const result = resolveNameHelper(location, outerName, meaning, /*nameNotFoundMessage*/ undefined, outerName, /*isUse*/ false, /*excludeGlobals*/ false, (symbols, name, meaning) => { + Debug.assertEqual(outerName, name, "name should equal outerName"); + const symbol = getSymbol(symbols, name, meaning); + // Sometimes the symbol is found when location is a return type of a function: `typeof x` and `x` is declared in the body of the function + // So the table *contains* `x` but `x` isn't actually in scope. + // However, resolveNameHelper will continue and call this callback again, so we'll eventually get a correct suggestion. + return symbol || getSpellingSuggestionForName(unescapeLeadingUnderscores(name), arrayFrom(symbols.values()), meaning); + }); + return result; + } + function getSuggestionForNonexistentSymbol(location: Node | undefined, outerName: __String, meaning: SymbolFlags): string | undefined { + const symbolResult = getSuggestedSymbolForNonexistentSymbol(location, outerName, meaning); + return symbolResult && symbolName(symbolResult); + } + function getSuggestedSymbolForNonexistentModule(name: Identifier, targetModule: ts.Symbol): ts.Symbol | undefined { + return targetModule.exports && getSpellingSuggestionForName(idText(name), getExportsOfModuleAsArray(targetModule), SymbolFlags.ModuleMember); + } + function getSuggestionForNonexistentExport(name: Identifier, targetModule: ts.Symbol): string | undefined { + const suggestion = getSuggestedSymbolForNonexistentModule(name, targetModule); + return suggestion && symbolName(suggestion); + } + function getSuggestionForNonexistentIndexSignature(objectType: ts.Type, expr: ElementAccessExpression, keyedType: ts.Type): string | undefined { + // check if object type has setter or getter + function hasProp(name: "set" | "get") { + const prop = getPropertyOfObjectType(objectType, (<__String>name)); + if (prop) { + const s = getSingleCallSignature(getTypeOfSymbol(prop)); + return !!s && getMinArgumentCount(s) >= 1 && isTypeAssignableTo(keyedType, getTypeAtPosition(s, 0)); } - - if (duplicateFunctionDeclaration) { - forEach(declarations, declaration => { - error(getNameOfDeclaration(declaration), Diagnostics.Duplicate_function_implementation); - }); + return false; + } + ; + const suggestedMethod = isAssignmentTarget(expr) ? "set" : "get"; + if (!hasProp(suggestedMethod)) { + return undefined; + } + let suggestion = tryGetPropertyAccessOrIdentifierToString(expr.expression); + if (suggestion === undefined) { + suggestion = suggestedMethod; + } + else { + suggestion += "." + suggestedMethod; + } + return suggestion; + } + /** + * Given a name and a list of symbols whose names are *not* equal to the name, return a spelling suggestion if there is one that is close enough. + * Names less than length 3 only check for case-insensitive equality, not levenshtein distance. + * + * If there is a candidate that's the same except for case, return that. + * If there is a candidate that's within one edit of the name, return that. + * Otherwise, return the candidate with the smallest Levenshtein distance, + * except for candidates: + * * With no name + * * Whose meaning doesn't match the `meaning` parameter. + * * Whose length differs from the target name by more than 0.34 of the length of the name. + * * Whose levenshtein distance is more than 0.4 of the length of the name + * (0.4 allows 1 substitution/transposition for every 5 characters, + * and 1 insertion/deletion at 3 characters) + */ + function getSpellingSuggestionForName(name: string, symbols: ts.Symbol[], meaning: SymbolFlags): ts.Symbol | undefined { + return getSpellingSuggestion(name, symbols, getCandidateName); + function getCandidateName(candidate: ts.Symbol) { + const candidateName = symbolName(candidate); + return !startsWith(candidateName, "\"") && candidate.flags & meaning ? candidateName : undefined; + } + } + function markPropertyAsReferenced(prop: ts.Symbol, nodeForCheckWriteOnly: Node | undefined, isThisAccess: boolean) { + const valueDeclaration = prop && (prop.flags & SymbolFlags.ClassMember) && prop.valueDeclaration; + if (!valueDeclaration) { + return; + } + const hasPrivateModifier = hasModifier(valueDeclaration, ModifierFlags.Private); + const hasPrivateIdentifier = isNamedDeclaration(prop.valueDeclaration) && isPrivateIdentifier(prop.valueDeclaration.name); + if (!hasPrivateModifier && !hasPrivateIdentifier) { + return; + } + if (nodeForCheckWriteOnly && isWriteOnlyAccess(nodeForCheckWriteOnly) && !(prop.flags & SymbolFlags.SetAccessor && !(prop.flags & SymbolFlags.GetAccessor))) { + return; + } + if (isThisAccess) { + // Find any FunctionLikeDeclaration because those create a new 'this' binding. But this should only matter for methods (or getters/setters). + const containingMethod = findAncestor(nodeForCheckWriteOnly, isFunctionLikeDeclaration); + if (containingMethod && containingMethod.symbol === prop) { + return; } - - if (hasNonAmbientClass && !isConstructor && symbol.flags & SymbolFlags.Function) { - // A non-ambient class cannot be an implementation for a non-constructor function/class merge - // TODO: The below just replicates our older error from when classes and functions were - // entirely unable to merge - a more helpful message like "Class declaration cannot implement overload list" - // might be warranted. :shrug: - forEach(declarations, declaration => { - addDuplicateDeclarationError(getNameOfDeclaration(declaration) || declaration, Diagnostics.Duplicate_identifier_0, symbolName(symbol), filter(declarations, d => d !== declaration)); - }); + } + (getCheckFlags(prop) & CheckFlags.Instantiated ? getSymbolLinks(prop).target : prop)!.isReferenced = SymbolFlags.All; + } + function isValidPropertyAccess(node: PropertyAccessExpression | QualifiedName | ImportTypeNode, propertyName: __String): boolean { + switch (node.kind) { + case SyntaxKind.PropertyAccessExpression: + return isValidPropertyAccessWithType(node, node.expression.kind === SyntaxKind.SuperKeyword, propertyName, getWidenedType(checkExpression(node.expression))); + case SyntaxKind.QualifiedName: + return isValidPropertyAccessWithType(node, /*isSuper*/ false, propertyName, getWidenedType(checkExpression(node.left))); + case SyntaxKind.ImportType: + return isValidPropertyAccessWithType(node, /*isSuper*/ false, propertyName, getTypeFromTypeNode(node)); + } + } + function isValidPropertyAccessForCompletions(node: PropertyAccessExpression | ImportTypeNode | QualifiedName, type: ts.Type, property: ts.Symbol): boolean { + return isValidPropertyAccessWithType(node, node.kind === SyntaxKind.PropertyAccessExpression && node.expression.kind === SyntaxKind.SuperKeyword, property.escapedName, type); + // Previously we validated the 'this' type of methods but this adversely affected performance. See #31377 for more context. + } + function isValidPropertyAccessWithType(node: PropertyAccessExpression | QualifiedName | ImportTypeNode, isSuper: boolean, propertyName: __String, type: ts.Type): boolean { + if (type === errorType || isTypeAny(type)) { + return true; + } + const prop = getPropertyOfType(type, propertyName); + if (prop) { + if (isPropertyAccessExpression(node) && prop.valueDeclaration && isPrivateIdentifierPropertyDeclaration(prop.valueDeclaration)) { + const declClass = getContainingClass(prop.valueDeclaration); + return !isOptionalChain(node) && !!findAncestor(node, parent => parent === declClass); } - - // Abstract methods can't have an implementation -- in particular, they don't need one. - if (lastSeenNonAmbientDeclaration && !lastSeenNonAmbientDeclaration.body && - !hasModifier(lastSeenNonAmbientDeclaration, ModifierFlags.Abstract) && !lastSeenNonAmbientDeclaration.questionToken) { - reportImplementationExpectedError(lastSeenNonAmbientDeclaration); + return checkPropertyAccessibility(node, isSuper, type, prop); + } + // In js files properties of unions are allowed in completion + return isInJSFile(node) && (type.flags & TypeFlags.Union) !== 0 && (type).types.some(elementType => isValidPropertyAccessWithType(node, isSuper, propertyName, elementType)); + } + /** + * Return the symbol of the for-in variable declared or referenced by the given for-in statement. + */ + function getForInVariableSymbol(node: ForInStatement): ts.Symbol | undefined { + const initializer = node.initializer; + if (initializer.kind === SyntaxKind.VariableDeclarationList) { + const variable = (initializer).declarations[0]; + if (variable && !isBindingPattern(variable.name)) { + return getSymbolOfNode(variable); } - - if (hasOverloads) { - checkFlagAgreementBetweenOverloads(declarations, bodyDeclaration, flagsToCheck, someNodeFlags, allNodeFlags); - checkQuestionTokenAgreementBetweenOverloads(declarations, bodyDeclaration, someHaveQuestionToken, allHaveQuestionToken); - - if (bodyDeclaration) { - const signatures = getSignaturesOfSymbol(symbol); - const bodySignature = getSignatureFromDeclaration(bodyDeclaration); - for (const signature of signatures) { - if (!isImplementationCompatibleWithOverload(bodySignature, signature)) { - addRelatedInfo( - error(signature.declaration, Diagnostics.This_overload_signature_is_not_compatible_with_its_implementation_signature), - createDiagnosticForNode(bodyDeclaration, Diagnostics.The_implementation_signature_is_declared_here) - ); - break; - } + } + else if (initializer.kind === SyntaxKind.Identifier) { + return getResolvedSymbol((initializer)); + } + return undefined; + } + /** + * Return true if the given type is considered to have numeric property names. + */ + function hasNumericPropertyNames(type: ts.Type) { + return getIndexTypeOfType(type, IndexKind.Number) && !getIndexTypeOfType(type, IndexKind.String); + } + /** + * Return true if given node is an expression consisting of an identifier (possibly parenthesized) + * that references a for-in variable for an object with numeric property names. + */ + function isForInVariableForNumericPropertyNames(expr: Expression) { + const e = skipParentheses(expr); + if (e.kind === SyntaxKind.Identifier) { + const symbol = getResolvedSymbol((e)); + if (symbol.flags & SymbolFlags.Variable) { + let child: Node = expr; + let node = expr.parent; + while (node) { + if (node.kind === SyntaxKind.ForInStatement && + child === (node).statement && + getForInVariableSymbol((node)) === symbol && + hasNumericPropertyNames(getTypeOfExpression((node).expression))) { + return true; } + child = node; + node = node.parent; } } } - - function checkExportsOnMergedDeclarations(node: Declaration): void { - if (!produceDiagnostics) { - return; - } - - // if localSymbol is defined on node then node itself is exported - check is required - let symbol = node.localSymbol; - if (!symbol) { - // local symbol is undefined => this declaration is non-exported. - // however symbol might contain other declarations that are exported - symbol = getSymbolOfNode(node)!; - if (!symbol.exportSymbol) { - // this is a pure local symbol (all declarations are non-exported) - no need to check anything - return; - } + return false; + } + function checkIndexedAccess(node: ElementAccessExpression): ts.Type { + return node.flags & NodeFlags.OptionalChain ? checkElementAccessChain((node as ElementAccessChain)) : + checkElementAccessExpression(node, checkNonNullExpression(node.expression)); + } + function checkElementAccessChain(node: ElementAccessChain) { + const exprType = checkExpression(node.expression); + const nonOptionalType = getOptionalExpressionType(exprType, node.expression); + return propagateOptionalTypeMarker(checkElementAccessExpression(node, checkNonNullType(nonOptionalType, node.expression)), node, nonOptionalType !== exprType); + } + function checkElementAccessExpression(node: ElementAccessExpression, exprType: ts.Type): ts.Type { + const objectType = getAssignmentTargetKind(node) !== AssignmentKind.None || isMethodAccessForCall(node) ? getWidenedType(exprType) : exprType; + const indexExpression = node.argumentExpression; + const indexType = checkExpression(indexExpression); + if (objectType === errorType || objectType === silentNeverType) { + return objectType; + } + if (isConstEnumObjectType(objectType) && !isStringLiteralLike(indexExpression)) { + error(indexExpression, Diagnostics.A_const_enum_member_can_only_be_accessed_using_a_string_literal); + return errorType; + } + const effectiveIndexType = isForInVariableForNumericPropertyNames(indexExpression) ? numberType : indexType; + const accessFlags = isAssignmentTarget(node) ? + AccessFlags.Writing | (isGenericObjectType(objectType) && !isThisTypeParameter(objectType) ? AccessFlags.NoIndexSignatures : 0) : + AccessFlags.None; + const indexedAccessType = getIndexedAccessTypeOrUndefined(objectType, effectiveIndexType, node, accessFlags) || errorType; + return checkIndexedAccessIndexType(getFlowTypeOfAccessExpression(node, indexedAccessType.symbol, indexedAccessType, indexExpression), node); + } + function checkThatExpressionIsProperSymbolReference(expression: Expression, expressionType: ts.Type, reportError: boolean): boolean { + if (expressionType === errorType) { + // There is already an error, so no need to report one. + return false; + } + if (!isWellKnownSymbolSyntactically(expression)) { + return false; + } + // Make sure the property type is the primitive symbol type + if ((expressionType.flags & TypeFlags.ESSymbolLike) === 0) { + if (reportError) { + error(expression, Diagnostics.A_computed_property_name_of_the_form_0_must_be_of_type_symbol, getTextOfNode(expression)); } - - // run the check only for the first declaration in the list - if (getDeclarationOfKind(symbol, node.kind) !== node) { - return; + return false; + } + // The name is Symbol., so make sure Symbol actually resolves to the + // global Symbol object + const leftHandSide = ((expression).expression); + const leftHandSideSymbol = getResolvedSymbol(leftHandSide); + if (!leftHandSideSymbol) { + return false; + } + const globalESSymbol = getGlobalESSymbolConstructorSymbol(/*reportErrors*/ true); + if (!globalESSymbol) { + // Already errored when we tried to look up the symbol + return false; + } + if (leftHandSideSymbol !== globalESSymbol) { + if (reportError) { + error(leftHandSide, Diagnostics.Symbol_reference_does_not_refer_to_the_global_Symbol_constructor_object); } - - let exportedDeclarationSpaces = DeclarationSpaces.None; - let nonExportedDeclarationSpaces = DeclarationSpaces.None; - let defaultExportedDeclarationSpaces = DeclarationSpaces.None; - for (const d of symbol.declarations) { - const declarationSpaces = getDeclarationSpaces(d); - const effectiveDeclarationFlags = getEffectiveDeclarationFlags(d, ModifierFlags.Export | ModifierFlags.Default); - - if (effectiveDeclarationFlags & ModifierFlags.Export) { - if (effectiveDeclarationFlags & ModifierFlags.Default) { - defaultExportedDeclarationSpaces |= declarationSpaces; - } - else { - exportedDeclarationSpaces |= declarationSpaces; - } + return false; + } + return true; + } + function callLikeExpressionMayHaveTypeArguments(node: CallLikeExpression): node is CallExpression | NewExpression | TaggedTemplateExpression | JsxOpeningElement { + return isCallOrNewExpression(node) || isTaggedTemplateExpression(node) || isJsxOpeningLikeElement(node); + } + function resolveUntypedCall(node: CallLikeExpression): ts.Signature { + if (callLikeExpressionMayHaveTypeArguments(node)) { + // Check type arguments even though we will give an error that untyped calls may not accept type arguments. + // This gets us diagnostics for the type arguments and marks them as referenced. + forEach(node.typeArguments, checkSourceElement); + } + if (node.kind === SyntaxKind.TaggedTemplateExpression) { + checkExpression(node.template); + } + else if (isJsxOpeningLikeElement(node)) { + checkExpression(node.attributes); + } + else if (node.kind !== SyntaxKind.Decorator) { + forEach((node).arguments, argument => { + checkExpression(argument); + }); + } + return anySignature; + } + function resolveErrorCall(node: CallLikeExpression): ts.Signature { + resolveUntypedCall(node); + return unknownSignature; + } + // Re-order candidate signatures into the result array. Assumes the result array to be empty. + // The candidate list orders groups in reverse, but within a group signatures are kept in declaration order + // A nit here is that we reorder only signatures that belong to the same symbol, + // so order how inherited signatures are processed is still preserved. + // interface A { (x: string): void } + // interface B extends A { (x: 'foo'): string } + // const b: B; + // b('foo') // <- here overloads should be processed as [(x:'foo'): string, (x: string): void] + function reorderCandidates(signatures: readonly ts.Signature[], result: ts.Signature[], callChainFlags: SignatureFlags): void { + let lastParent: Node | undefined; + let lastSymbol: ts.Symbol | undefined; + let cutoffIndex = 0; + let index: number | undefined; + let specializedIndex = -1; + let spliceIndex: number; + Debug.assert(!result.length); + for (const signature of signatures) { + const symbol = signature.declaration && getSymbolOfNode(signature.declaration); + const parent = signature.declaration && signature.declaration.parent; + if (!lastSymbol || symbol === lastSymbol) { + if (lastParent && parent === lastParent) { + index = index! + 1; } else { - nonExportedDeclarationSpaces |= declarationSpaces; + lastParent = parent; + index = cutoffIndex; } } - - // Spaces for anything not declared a 'default export'. - const nonDefaultExportedDeclarationSpaces = exportedDeclarationSpaces | nonExportedDeclarationSpaces; - - const commonDeclarationSpacesForExportsAndLocals = exportedDeclarationSpaces & nonExportedDeclarationSpaces; - const commonDeclarationSpacesForDefaultAndNonDefault = defaultExportedDeclarationSpaces & nonDefaultExportedDeclarationSpaces; - - if (commonDeclarationSpacesForExportsAndLocals || commonDeclarationSpacesForDefaultAndNonDefault) { - // declaration spaces for exported and non-exported declarations intersect - for (const d of symbol.declarations) { - const declarationSpaces = getDeclarationSpaces(d); - - const name = getNameOfDeclaration(d); - // Only error on the declarations that contributed to the intersecting spaces. - if (declarationSpaces & commonDeclarationSpacesForDefaultAndNonDefault) { - error(name, Diagnostics.Merged_declaration_0_cannot_include_a_default_export_declaration_Consider_adding_a_separate_export_default_0_declaration_instead, declarationNameToString(name)); - } - else if (declarationSpaces & commonDeclarationSpacesForExportsAndLocals) { - error(name, Diagnostics.Individual_declarations_in_merged_declaration_0_must_be_all_exported_or_all_local, declarationNameToString(name)); - } - } + else { + // current declaration belongs to a different symbol + // set cutoffIndex so re-orderings in the future won't change result set from 0 to cutoffIndex + index = cutoffIndex = result.length; + lastParent = parent; + } + lastSymbol = symbol; + // specialized signatures always need to be placed before non-specialized signatures regardless + // of the cutoff position; see GH#1133 + if (signatureHasLiteralTypes(signature)) { + specializedIndex++; + spliceIndex = specializedIndex; + // The cutoff index always needs to be greater than or equal to the specialized signature index + // in order to prevent non-specialized signatures from being added before a specialized + // signature. + cutoffIndex++; } - - function getDeclarationSpaces(decl: Declaration): DeclarationSpaces { - let d = decl as Node; - switch (d.kind) { - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.TypeAliasDeclaration: - - // A jsdoc typedef and callback are, by definition, type aliases. - // falls through - case SyntaxKind.JSDocTypedefTag: - case SyntaxKind.JSDocCallbackTag: - case SyntaxKind.JSDocEnumTag: - return DeclarationSpaces.ExportType; - case SyntaxKind.ModuleDeclaration: - return isAmbientModule(d as ModuleDeclaration) || getModuleInstanceState(d as ModuleDeclaration) !== ModuleInstanceState.NonInstantiated - ? DeclarationSpaces.ExportNamespace | DeclarationSpaces.ExportValue - : DeclarationSpaces.ExportNamespace; - case SyntaxKind.ClassDeclaration: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.EnumMember: - return DeclarationSpaces.ExportType | DeclarationSpaces.ExportValue; - case SyntaxKind.SourceFile: - return DeclarationSpaces.ExportType | DeclarationSpaces.ExportValue | DeclarationSpaces.ExportNamespace; - case SyntaxKind.ExportAssignment: - // Export assigned entity name expressions act as aliases and should fall through, otherwise they export values - if (!isEntityNameExpression((d as ExportAssignment).expression)) { - return DeclarationSpaces.ExportValue; - } - d = (d as ExportAssignment).expression; - - // The below options all declare an Alias, which is allowed to merge with other values within the importing module. - // falls through - case SyntaxKind.ImportEqualsDeclaration: - case SyntaxKind.NamespaceImport: - case SyntaxKind.ImportClause: - let result = DeclarationSpaces.None; - const target = resolveAlias(getSymbolOfNode(d)!); - forEach(target.declarations, d => { result |= getDeclarationSpaces(d); }); - return result; - case SyntaxKind.VariableDeclaration: - case SyntaxKind.BindingElement: - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.ImportSpecifier: // https://github.com/Microsoft/TypeScript/pull/7591 - case SyntaxKind.Identifier: // https://github.com/microsoft/TypeScript/issues/36098 - // Identifiers are used as declarations of assignment declarations whose parents may be - // SyntaxKind.CallExpression - `Object.defineProperty(thing, "aField", {value: 42});` - // SyntaxKind.ElementAccessExpression - `thing["aField"] = 42;` or `thing["aField"];` (with a doc comment on it) - // or SyntaxKind.PropertyAccessExpression - `thing.aField = 42;` - // all of which are pretty much always values, or at least imply a value meaning. - // It may be apprpriate to treat these as aliases in the future. - return DeclarationSpaces.ExportValue; - default: - return Debug.failBadSyntaxKind(d); - } + else { + spliceIndex = index; } + result.splice(spliceIndex, 0, callChainFlags ? getOptionalCallSignature(signature, callChainFlags) : signature); } - - function getAwaitedTypeOfPromise(type: Type, errorNode?: Node, diagnosticMessage?: DiagnosticMessage, arg0?: string | number): Type | undefined { - const promisedType = getPromisedTypeOfPromise(type, errorNode); - return promisedType && getAwaitedType(promisedType, errorNode, diagnosticMessage, arg0); - } - - /** - * Gets the "promised type" of a promise. - * @param type The type of the promise. - * @remarks The "promised type" of a type is the type of the "value" parameter of the "onfulfilled" callback. - */ - function getPromisedTypeOfPromise(promise: Type, errorNode?: Node): Type | undefined { - // - // { // promise - // then( // thenFunction - // onfulfilled: ( // onfulfilledParameterType - // value: T // valueParameterType - // ) => any - // ): any; - // } - // - - if (isTypeAny(promise)) { - return undefined; - } - - const typeAsPromise = promise; - if (typeAsPromise.promisedTypeOfPromise) { - return typeAsPromise.promisedTypeOfPromise; - } - - if (isReferenceToType(promise, getGlobalPromiseType(/*reportErrors*/ false))) { - return typeAsPromise.promisedTypeOfPromise = getTypeArguments(promise)[0]; + } + function isSpreadArgument(arg: Expression | undefined): arg is Expression { + return !!arg && (arg.kind === SyntaxKind.SpreadElement || arg.kind === SyntaxKind.SyntheticExpression && (arg).isSpread); + } + function getSpreadArgumentIndex(args: readonly Expression[]): number { + return findIndex(args, isSpreadArgument); + } + function acceptsVoid(t: ts.Type): boolean { + return !!(t.flags & TypeFlags.Void); + } + function hasCorrectArity(node: CallLikeExpression, args: readonly Expression[], signature: ts.Signature, signatureHelpTrailingComma = false) { + let argCount: number; + let callIsIncomplete = false; // In incomplete call we want to be lenient when we have too few arguments + let effectiveParameterCount = getParameterCount(signature); + let effectiveMinimumArguments = getMinArgumentCount(signature); + if (node.kind === SyntaxKind.TaggedTemplateExpression) { + argCount = args.length; + if (node.template.kind === SyntaxKind.TemplateExpression) { + // If a tagged template expression lacks a tail literal, the call is incomplete. + // Specifically, a template only can end in a TemplateTail or a Missing literal. + const lastSpan = last(node.template.templateSpans); // we should always have at least one span. + callIsIncomplete = nodeIsMissing(lastSpan.literal) || !!lastSpan.literal.isUnterminated; } - - const thenFunction = getTypeOfPropertyOfType(promise, "then" as __String)!; // TODO: GH#18217 - if (isTypeAny(thenFunction)) { - return undefined; + else { + // If the template didn't end in a backtick, or its beginning occurred right prior to EOF, + // then this might actually turn out to be a TemplateHead in the future; + // so we consider the call to be incomplete. + const templateLiteral = (node.template); + Debug.assert(templateLiteral.kind === SyntaxKind.NoSubstitutionTemplateLiteral); + callIsIncomplete = !!templateLiteral.isUnterminated; } - - const thenSignatures = thenFunction ? getSignaturesOfType(thenFunction, SignatureKind.Call) : emptyArray; - if (thenSignatures.length === 0) { - if (errorNode) { - error(errorNode, Diagnostics.A_promise_must_have_a_then_method); - } - return undefined; + } + else if (node.kind === SyntaxKind.Decorator) { + argCount = getDecoratorArgumentCount(node, signature); + } + else if (isJsxOpeningLikeElement(node)) { + callIsIncomplete = node.attributes.end === node.end; + if (callIsIncomplete) { + return true; } - - const onfulfilledParameterType = getTypeWithFacts(getUnionType(map(thenSignatures, getTypeOfFirstParameterOfSignature)), TypeFacts.NEUndefinedOrNull); - if (isTypeAny(onfulfilledParameterType)) { - return undefined; + argCount = effectiveMinimumArguments === 0 ? args.length : 1; + effectiveParameterCount = args.length === 0 ? effectiveParameterCount : 1; // class may have argumentless ctor functions - still resolve ctor and compare vs props member type + effectiveMinimumArguments = Math.min(effectiveMinimumArguments, 1); // sfc may specify context argument - handled by framework and not typechecked + } + else { + if (!node.arguments) { + // This only happens when we have something of the form: 'new C' + Debug.assert(node.kind === SyntaxKind.NewExpression); + return getMinArgumentCount(signature) === 0; } - - const onfulfilledParameterSignatures = getSignaturesOfType(onfulfilledParameterType, SignatureKind.Call); - if (onfulfilledParameterSignatures.length === 0) { - if (errorNode) { - error(errorNode, Diagnostics.The_first_parameter_of_the_then_method_of_a_promise_must_be_a_callback); - } - return undefined; + argCount = signatureHelpTrailingComma ? args.length + 1 : args.length; + // If we are missing the close parenthesis, the call is incomplete. + callIsIncomplete = node.arguments.end === node.end; + // If a spread argument is present, check that it corresponds to a rest parameter or at least that it's in the valid range. + const spreadArgIndex = getSpreadArgumentIndex(args); + if (spreadArgIndex >= 0) { + return spreadArgIndex >= getMinArgumentCount(signature) && (hasEffectiveRestParameter(signature) || spreadArgIndex < getParameterCount(signature)); } - - return typeAsPromise.promisedTypeOfPromise = getUnionType(map(onfulfilledParameterSignatures, getTypeOfFirstParameterOfSignature), UnionReduction.Subtype); } - - /** - * Gets the "awaited type" of a type. - * @param type The type to await. - * @remarks The "awaited type" of an expression is its "promised type" if the expression is a - * Promise-like type; otherwise, it is the type of the expression. This is used to reflect - * The runtime behavior of the `await` keyword. - */ - function checkAwaitedType(type: Type, errorNode: Node, diagnosticMessage: DiagnosticMessage, arg0?: string | number): Type { - const awaitedType = getAwaitedType(type, errorNode, diagnosticMessage, arg0); - return awaitedType || errorType; + // Too many arguments implies incorrect arity. + if (!hasEffectiveRestParameter(signature) && argCount > effectiveParameterCount) { + return false; } - - function getAwaitedType(type: Type, errorNode?: Node, diagnosticMessage?: DiagnosticMessage, arg0?: string | number): Type | undefined { - const typeAsAwaitable = type; - if (typeAsAwaitable.awaitedTypeOfType) { - return typeAsAwaitable.awaitedTypeOfType; - } - - if (isTypeAny(type)) { - return typeAsAwaitable.awaitedTypeOfType = type; + // If the call is incomplete, we should skip the lower bound check. + // JSX signatures can have extra parameters provided by the library which we don't check + if (callIsIncomplete || argCount >= effectiveMinimumArguments) { + return true; + } + for (let i = argCount; i < effectiveMinimumArguments; i++) { + const type = getTypeAtPosition(signature, i); + if (filterType(type, acceptsVoid).flags & TypeFlags.Never) { + return false; } - - if (type.flags & TypeFlags.Union) { - let types: Type[] | undefined; - for (const constituentType of (type).types) { - types = append(types, getAwaitedType(constituentType, errorNode, diagnosticMessage, arg0)); + } + return true; + } + function hasCorrectTypeArgumentArity(signature: ts.Signature, typeArguments: NodeArray | undefined) { + // If the user supplied type arguments, but the number of type arguments does not match + // the declared number of type parameters, the call has an incorrect arity. + const numTypeParameters = length(signature.typeParameters); + const minTypeArgumentCount = getMinTypeArgumentCount(signature.typeParameters); + return !some(typeArguments) || + (typeArguments.length >= minTypeArgumentCount && typeArguments.length <= numTypeParameters); + } + // If type has a single call signature and no other members, return that signature. Otherwise, return undefined. + function getSingleCallSignature(type: ts.Type): ts.Signature | undefined { + return getSingleSignature(type, SignatureKind.Call, /*allowMembers*/ false); + } + function getSingleCallOrConstructSignature(type: ts.Type): ts.Signature | undefined { + return getSingleSignature(type, SignatureKind.Call, /*allowMembers*/ false) || + getSingleSignature(type, SignatureKind.Construct, /*allowMembers*/ false); + } + function getSingleSignature(type: ts.Type, kind: SignatureKind, allowMembers: boolean): ts.Signature | undefined { + if (type.flags & TypeFlags.Object) { + const resolved = resolveStructuredTypeMembers((type)); + if (allowMembers || resolved.properties.length === 0 && !resolved.stringIndexInfo && !resolved.numberIndexInfo) { + if (kind === SignatureKind.Call && resolved.callSignatures.length === 1 && resolved.constructSignatures.length === 0) { + return resolved.callSignatures[0]; } - - if (!types) { - return undefined; + if (kind === SignatureKind.Construct && resolved.constructSignatures.length === 1 && resolved.callSignatures.length === 0) { + return resolved.constructSignatures[0]; } - - return typeAsAwaitable.awaitedTypeOfType = getUnionType(types); } - - const promisedType = getPromisedTypeOfPromise(type); - if (promisedType) { - if (type.id === promisedType.id || awaitedTypeStack.indexOf(promisedType.id) >= 0) { - // Verify that we don't have a bad actor in the form of a promise whose - // promised type is the same as the promise type, or a mutually recursive - // promise. If so, we return undefined as we cannot guess the shape. If this - // were the actual case in the JavaScript, this Promise would never resolve. - // - // An example of a bad actor with a singly-recursive promise type might - // be: - // - // interface BadPromise { - // then( - // onfulfilled: (value: BadPromise) => any, - // onrejected: (error: any) => any): BadPromise; - // } - // The above interface will pass the PromiseLike check, and return a - // promised type of `BadPromise`. Since this is a self reference, we - // don't want to keep recursing ad infinitum. - // - // An example of a bad actor in the form of a mutually-recursive - // promise type might be: - // - // interface BadPromiseA { - // then( - // onfulfilled: (value: BadPromiseB) => any, - // onrejected: (error: any) => any): BadPromiseB; - // } - // - // interface BadPromiseB { - // then( - // onfulfilled: (value: BadPromiseA) => any, - // onrejected: (error: any) => any): BadPromiseA; - // } - // - if (errorNode) { - error(errorNode, Diagnostics.Type_is_referenced_directly_or_indirectly_in_the_fulfillment_callback_of_its_own_then_method); - } - return undefined; - } - - // Keep track of the type we're about to unwrap to avoid bad recursive promise types. - // See the comments above for more information. - awaitedTypeStack.push(type.id); - const awaitedType = getAwaitedType(promisedType, errorNode, diagnosticMessage, arg0); - awaitedTypeStack.pop(); - - if (!awaitedType) { + } + return undefined; + } + // Instantiate a generic signature in the context of a non-generic signature (section 3.8.5 in TypeScript spec) + function instantiateSignatureInContextOf(signature: ts.Signature, contextualSignature: ts.Signature, inferenceContext?: InferenceContext, compareTypes?: TypeComparer): ts.Signature { + const context = createInferenceContext((signature.typeParameters!), signature, InferenceFlags.None, compareTypes); + // We clone the inferenceContext to avoid fixing. For example, when the source signature is (x: T) => T[] and + // the contextual signature is (...args: A) => B, we want to infer the element type of A's constraint (say 'any') + // for T but leave it possible to later infer '[any]' back to A. + const restType = getEffectiveRestType(contextualSignature); + const mapper = inferenceContext && (restType && restType.flags & TypeFlags.TypeParameter ? inferenceContext.nonFixingMapper : inferenceContext.mapper); + const sourceSignature = mapper ? instantiateSignature(contextualSignature, mapper) : contextualSignature; + applyToParameterTypes(sourceSignature, signature, (source, target) => { + // Type parameters from outer context referenced by source type are fixed by instantiation of the source type + inferTypes(context.inferences, source, target); + }); + if (!inferenceContext) { + applyToReturnTypes(contextualSignature, signature, (source, target) => { + inferTypes(context.inferences, source, target, InferencePriority.ReturnType); + }); + } + return getSignatureInstantiation(signature, getInferredTypes(context), isInJSFile(contextualSignature.declaration)); + } + function inferJsxTypeArguments(node: JsxOpeningLikeElement, signature: ts.Signature, checkMode: CheckMode, context: InferenceContext): ts.Type[] { + const paramType = getEffectiveFirstArgumentForJsxSignature(signature, node); + const checkAttrType = checkExpressionWithContextualType(node.attributes, paramType, context, checkMode); + inferTypes(context.inferences, checkAttrType, paramType); + return getInferredTypes(context); + } + function inferTypeArguments(node: CallLikeExpression, signature: ts.Signature, args: readonly Expression[], checkMode: CheckMode, context: InferenceContext): ts.Type[] { + if (isJsxOpeningLikeElement(node)) { + return inferJsxTypeArguments(node, signature, checkMode, context); + } + // If a contextual type is available, infer from that type to the return type of the call expression. For + // example, given a 'function wrap(cb: (x: T) => U): (x: T) => U' and a call expression + // 'let f: (x: string) => number = wrap(s => s.length)', we infer from the declared type of 'f' to the + // return type of 'wrap'. + if (node.kind !== SyntaxKind.Decorator) { + const contextualType = getContextualType(node); + if (contextualType) { + // We clone the inference context to avoid disturbing a resolution in progress for an + // outer call expression. Effectively we just want a snapshot of whatever has been + // inferred for any outer call expression so far. + const outerContext = getInferenceContext(node); + const outerMapper = getMapperFromContext(cloneInferenceContext(outerContext, InferenceFlags.NoDefault)); + const instantiatedType = instantiateType(contextualType, outerMapper); + // If the contextual type is a generic function type with a single call signature, we + // instantiate the type with its own type parameters and type arguments. This ensures that + // the type parameters are not erased to type any during type inference such that they can + // be inferred as actual types from the contextual type. For example: + // declare function arrayMap(f: (x: T) => U): (a: T[]) => U[]; + // const boxElements: (a: A[]) => { value: A }[] = arrayMap(value => ({ value })); + // Above, the type of the 'value' parameter is inferred to be 'A'. + const contextualSignature = getSingleCallSignature(instantiatedType); + const inferenceSourceType = contextualSignature && contextualSignature.typeParameters ? + getOrCreateTypeFromSignature(getSignatureInstantiationWithoutFillingInTypeArguments(contextualSignature, contextualSignature.typeParameters)) : + instantiatedType; + const inferenceTargetType = getReturnTypeOfSignature(signature); + // Inferences made from return types have lower priority than all other inferences. + inferTypes(context.inferences, inferenceSourceType, inferenceTargetType, InferencePriority.ReturnType); + // Create a type mapper for instantiating generic contextual types using the inferences made + // from the return type. We need a separate inference pass here because (a) instantiation of + // the source type uses the outer context's return mapper (which excludes inferences made from + // outer arguments), and (b) we don't want any further inferences going into this context. + const returnContext = createInferenceContext(signature.typeParameters!, signature, context.flags); + const returnSourceType = instantiateType(contextualType, outerContext && outerContext.returnMapper); + inferTypes(returnContext.inferences, returnSourceType, inferenceTargetType); + context.returnMapper = some(returnContext.inferences, hasInferenceCandidates) ? getMapperFromContext(cloneInferredPartOfContext(returnContext)) : undefined; + } + } + const thisType = getThisTypeOfSignature(signature); + if (thisType) { + const thisArgumentNode = getThisArgumentOfCall(node); + const thisArgumentType = thisArgumentNode ? checkExpression(thisArgumentNode) : voidType; + inferTypes(context.inferences, thisArgumentType, thisType); + } + const restType = getNonArrayRestType(signature); + const argCount = restType ? Math.min(getParameterCount(signature) - 1, args.length) : args.length; + for (let i = 0; i < argCount; i++) { + const arg = args[i]; + if (arg.kind !== SyntaxKind.OmittedExpression) { + const paramType = getTypeAtPosition(signature, i); + const argType = checkExpressionWithContextualType(arg, paramType, context, checkMode); + inferTypes(context.inferences, argType, paramType); + } + } + if (restType) { + const spreadType = getSpreadArgumentType(args, argCount, args.length, restType, context); + inferTypes(context.inferences, spreadType, restType); + } + return getInferredTypes(context); + } + function getArrayifiedType(type: ts.Type) { + return type.flags & TypeFlags.Union ? mapType(type, getArrayifiedType) : + type.flags & (TypeFlags.Any | TypeFlags.Instantiable) || isMutableArrayOrTuple(type) ? type : + isTupleType(type) ? createTupleType(getTypeArguments(type), type.target.minLength, type.target.hasRestElement, /*readonly*/ false, type.target.associatedNames) : + createArrayType(getIndexedAccessType(type, numberType)); + } + function getSpreadArgumentType(args: readonly Expression[], index: number, argCount: number, restType: ts.Type, context: InferenceContext | undefined) { + if (index >= argCount - 1) { + const arg = args[argCount - 1]; + if (isSpreadArgument(arg)) { + // We are inferring from a spread expression in the last argument position, i.e. both the parameter + // and the argument are ...x forms. + return arg.kind === SyntaxKind.SyntheticExpression ? + createArrayType((arg).type) : + getArrayifiedType(checkExpressionWithContextualType((arg).expression, restType, context, CheckMode.Normal)); + } + } + const types = []; + let spreadIndex = -1; + for (let i = index; i < argCount; i++) { + const contextualType = getIndexedAccessType(restType, getLiteralType(i - index)); + const argType = checkExpressionWithContextualType(args[i], contextualType, context, CheckMode.Normal); + if (spreadIndex < 0 && isSpreadArgument(args[i])) { + spreadIndex = i - index; + } + const hasPrimitiveContextualType = maybeTypeOfKind(contextualType, TypeFlags.Primitive | TypeFlags.Index); + types.push(hasPrimitiveContextualType ? getRegularTypeOfLiteralType(argType) : getWidenedLiteralType(argType)); + } + return spreadIndex < 0 ? + createTupleType(types) : + createTupleType(append(types.slice(0, spreadIndex), getUnionType(types.slice(spreadIndex))), spreadIndex, /*hasRestElement*/ true); + } + function checkTypeArguments(signature: ts.Signature, typeArgumentNodes: readonly TypeNode[], reportErrors: boolean, headMessage?: DiagnosticMessage): ts.Type[] | undefined { + const isJavascript = isInJSFile(signature.declaration); + const typeParameters = signature.typeParameters!; + const typeArgumentTypes = fillMissingTypeArguments(map(typeArgumentNodes, getTypeFromTypeNode), typeParameters, getMinTypeArgumentCount(typeParameters), isJavascript); + let mapper: TypeMapper | undefined; + for (let i = 0; i < typeArgumentNodes.length; i++) { + Debug.assert(typeParameters[i] !== undefined, "Should not call checkTypeArguments with too many type arguments"); + const constraint = getConstraintOfTypeParameter(typeParameters[i]); + if (constraint) { + const errorInfo = reportErrors && headMessage ? (() => chainDiagnosticMessages(/*details*/ undefined, Diagnostics.Type_0_does_not_satisfy_the_constraint_1)) : undefined; + const typeArgumentHeadMessage = headMessage || Diagnostics.Type_0_does_not_satisfy_the_constraint_1; + if (!mapper) { + mapper = createTypeMapper(typeParameters, typeArgumentTypes); + } + const typeArgument = typeArgumentTypes[i]; + if (!checkTypeAssignableTo(typeArgument, getTypeWithThisArgument(instantiateType(constraint, mapper), typeArgument), reportErrors ? typeArgumentNodes[i] : undefined, typeArgumentHeadMessage, errorInfo)) { return undefined; } - - return typeAsAwaitable.awaitedTypeOfType = awaitedType; - } - - // The type was not a promise, so it could not be unwrapped any further. - // As long as the type does not have a callable "then" property, it is - // safe to return the type; otherwise, an error will be reported in - // the call to getNonThenableType and we will return undefined. - // - // An example of a non-promise "thenable" might be: - // - // await { then(): void {} } - // - // The "thenable" does not match the minimal definition for a promise. When - // a Promise/A+-compatible or ES6 promise tries to adopt this value, the promise - // will never settle. We treat this as an error to help flag an early indicator - // of a runtime problem. If the user wants to return this value from an async - // function, they would need to wrap it in some other value. If they want it to - // be treated as a promise, they can cast to . - const thenFunction = getTypeOfPropertyOfType(type, "then" as __String); - if (thenFunction && getSignaturesOfType(thenFunction, SignatureKind.Call).length > 0) { - if (errorNode) { - if (!diagnosticMessage) return Debug.fail(); - error(errorNode, diagnosticMessage, arg0); - } - return undefined; } - - return typeAsAwaitable.awaitedTypeOfType = type; } - - /** - * Checks the return type of an async function to ensure it is a compatible - * Promise implementation. - * - * This checks that an async function has a valid Promise-compatible return type. - * An async function has a valid Promise-compatible return type if the resolved value - * of the return type has a construct signature that takes in an `initializer` function - * that in turn supplies a `resolve` function as one of its arguments and results in an - * object with a callable `then` signature. - * - * @param node The signature to check - */ - function checkAsyncFunctionReturnType(node: FunctionLikeDeclaration | MethodSignature, returnTypeNode: TypeNode) { - // As part of our emit for an async function, we will need to emit the entity name of - // the return type annotation as an expression. To meet the necessary runtime semantics - // for __awaiter, we must also check that the type of the declaration (e.g. the static - // side or "constructor" of the promise type) is compatible `PromiseConstructorLike`. - // - // An example might be (from lib.es6.d.ts): - // - // interface Promise { ... } - // interface PromiseConstructor { - // new (...): Promise; - // } - // declare var Promise: PromiseConstructor; - // - // When an async function declares a return type annotation of `Promise`, we - // need to get the type of the `Promise` variable declaration above, which would - // be `PromiseConstructor`. - // - // The same case applies to a class: - // - // declare class Promise { - // constructor(...); - // then(...): Promise; - // } - // - const returnType = getTypeFromTypeNode(returnTypeNode); - - if (languageVersion >= ScriptTarget.ES2015) { - if (returnType === errorType) { - return; - } - const globalPromiseType = getGlobalPromiseType(/*reportErrors*/ true); - if (globalPromiseType !== emptyGenericType && !isReferenceToType(returnType, globalPromiseType)) { - // The promise type was not a valid type reference to the global promise type, so we - // report an error and return the unknown type. - error(returnTypeNode, Diagnostics.The_return_type_of_an_async_function_or_method_must_be_the_global_Promise_T_type); - return; - } - } - else { - // Always mark the type node as referenced if it points to a value - markTypeNodeAsReferenced(returnTypeNode); - - if (returnType === errorType) { - return; - } - - const promiseConstructorName = getEntityNameFromTypeNode(returnTypeNode); - if (promiseConstructorName === undefined) { - error(returnTypeNode, Diagnostics.Type_0_is_not_a_valid_async_function_return_type_in_ES5_SlashES3_because_it_does_not_refer_to_a_Promise_compatible_constructor_value, typeToString(returnType)); - return; - } - - const promiseConstructorSymbol = resolveEntityName(promiseConstructorName, SymbolFlags.Value, /*ignoreErrors*/ true); - const promiseConstructorType = promiseConstructorSymbol ? getTypeOfSymbol(promiseConstructorSymbol) : errorType; - if (promiseConstructorType === errorType) { - if (promiseConstructorName.kind === SyntaxKind.Identifier && promiseConstructorName.escapedText === "Promise" && getTargetType(returnType) === getGlobalPromiseType(/*reportErrors*/ false)) { - error(returnTypeNode, Diagnostics.An_async_function_or_method_in_ES5_SlashES3_requires_the_Promise_constructor_Make_sure_you_have_a_declaration_for_the_Promise_constructor_or_include_ES2015_in_your_lib_option); - } - else { - error(returnTypeNode, Diagnostics.Type_0_is_not_a_valid_async_function_return_type_in_ES5_SlashES3_because_it_does_not_refer_to_a_Promise_compatible_constructor_value, entityNameToString(promiseConstructorName)); - } - return; - } - - const globalPromiseConstructorLikeType = getGlobalPromiseConstructorLikeType(/*reportErrors*/ true); - if (globalPromiseConstructorLikeType === emptyObjectType) { - // If we couldn't resolve the global PromiseConstructorLike type we cannot verify - // compatibility with __awaiter. - error(returnTypeNode, Diagnostics.Type_0_is_not_a_valid_async_function_return_type_in_ES5_SlashES3_because_it_does_not_refer_to_a_Promise_compatible_constructor_value, entityNameToString(promiseConstructorName)); - return; - } - - if (!checkTypeAssignableTo(promiseConstructorType, globalPromiseConstructorLikeType, returnTypeNode, - Diagnostics.Type_0_is_not_a_valid_async_function_return_type_in_ES5_SlashES3_because_it_does_not_refer_to_a_Promise_compatible_constructor_value)) { - return; - } - - // Verify there is no local declaration that could collide with the promise constructor. - const rootName = promiseConstructorName && getFirstIdentifier(promiseConstructorName); - const collidingSymbol = getSymbol(node.locals!, rootName.escapedText, SymbolFlags.Value); - if (collidingSymbol) { - error(collidingSymbol.valueDeclaration, Diagnostics.Duplicate_identifier_0_Compiler_uses_declaration_1_to_support_async_functions, - idText(rootName), - entityNameToString(promiseConstructorName)); - return; - } - } - checkAwaitedType(returnType, node, Diagnostics.The_return_type_of_an_async_function_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member); + return typeArgumentTypes; + } + function getJsxReferenceKind(node: JsxOpeningLikeElement): JsxReferenceKind { + if (isJsxIntrinsicIdentifier(node.tagName)) { + return JsxReferenceKind.Mixed; } - - /** Check a decorator */ - function checkDecorator(node: Decorator): void { - const signature = getResolvedSignature(node); - const returnType = getReturnTypeOfSignature(signature); - if (returnType.flags & TypeFlags.Any) { - return; - } - - let expectedReturnType: Type; - const headMessage = getDiagnosticHeadMessageForDecoratorResolution(node); - let errorInfo: DiagnosticMessageChain | undefined; - switch (node.parent.kind) { - case SyntaxKind.ClassDeclaration: - const classSymbol = getSymbolOfNode(node.parent); - const classConstructorType = getTypeOfSymbol(classSymbol); - expectedReturnType = getUnionType([classConstructorType, voidType]); - break; - - case SyntaxKind.Parameter: - expectedReturnType = voidType; - errorInfo = chainDiagnosticMessages( - /*details*/ undefined, - Diagnostics.The_return_type_of_a_parameter_decorator_function_must_be_either_void_or_any); - - break; - - case SyntaxKind.PropertyDeclaration: - expectedReturnType = voidType; - errorInfo = chainDiagnosticMessages( - /*details*/ undefined, - Diagnostics.The_return_type_of_a_property_decorator_function_must_be_either_void_or_any); - break; - - case SyntaxKind.MethodDeclaration: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - const methodType = getTypeOfNode(node.parent); - const descriptorType = createTypedPropertyDescriptorType(methodType); - expectedReturnType = getUnionType([descriptorType, voidType]); - break; - - default: - return Debug.fail(); - } - - checkTypeAssignableTo( - returnType, - expectedReturnType, - node, - headMessage, - () => errorInfo); + const tagType = getApparentType(checkExpression(node.tagName)); + if (length(getSignaturesOfType(tagType, SignatureKind.Construct))) { + return JsxReferenceKind.Component; } - - /** - * If a TypeNode can be resolved to a value symbol imported from an external module, it is - * marked as referenced to prevent import elision. - */ - function markTypeNodeAsReferenced(node: TypeNode) { - markEntityNameOrEntityExpressionAsReference(node && getEntityNameFromTypeNode(node)); + if (length(getSignaturesOfType(tagType, SignatureKind.Call))) { + return JsxReferenceKind.Function; } - - function markEntityNameOrEntityExpressionAsReference(typeName: EntityNameOrEntityNameExpression | undefined) { - if (!typeName) return; - - const rootName = getFirstIdentifier(typeName); - const meaning = (typeName.kind === SyntaxKind.Identifier ? SymbolFlags.Type : SymbolFlags.Namespace) | SymbolFlags.Alias; - const rootSymbol = resolveName(rootName, rootName.escapedText, meaning, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isRefernce*/ true); - if (rootSymbol - && rootSymbol.flags & SymbolFlags.Alias - && symbolIsValue(rootSymbol) - && !isConstEnumOrConstEnumOnlyModule(resolveAlias(rootSymbol)) - && !getTypeOnlyAliasDeclaration(rootSymbol)) { - markAliasSymbolAsReferenced(rootSymbol); + return JsxReferenceKind.Mixed; + } + /** + * Check if the given signature can possibly be a signature called by the JSX opening-like element. + * @param node a JSX opening-like element we are trying to figure its call signature + * @param signature a candidate signature we are trying whether it is a call signature + * @param relation a relationship to check parameter and argument type + */ + function checkApplicableSignatureForJsxOpeningLikeElement(node: JsxOpeningLikeElement, signature: ts.Signature, relation: ts.Map, checkMode: CheckMode, reportErrors: boolean, containingMessageChain: (() => DiagnosticMessageChain | undefined) | undefined, errorOutputContainer: { + errors?: Diagnostic[]; + skipLogging?: boolean; + }) { + // Stateless function components can have maximum of three arguments: "props", "context", and "updater". + // However "context" and "updater" are implicit and can't be specify by users. Only the first parameter, props, + // can be specified by users through attributes property. + const paramType = getEffectiveFirstArgumentForJsxSignature(signature, node); + const attributesType = checkExpressionWithContextualType(node.attributes, paramType, /*inferenceContext*/ undefined, checkMode); + return checkTagNameDoesNotExpectTooManyArguments() && checkTypeRelatedToAndOptionallyElaborate(attributesType, paramType, relation, reportErrors ? node.tagName : undefined, node.attributes, + /*headMessage*/ undefined, containingMessageChain, errorOutputContainer); + function checkTagNameDoesNotExpectTooManyArguments(): boolean { + const tagType = isJsxOpeningElement(node) || isJsxSelfClosingElement(node) && !isJsxIntrinsicIdentifier(node.tagName) ? checkExpression(node.tagName) : undefined; + if (!tagType) { + return true; } - } - - /** - * This function marks the type used for metadata decorator as referenced if it is import - * from external module. - * This is different from markTypeNodeAsReferenced because it tries to simplify type nodes in - * union and intersection type - * @param node - */ - function markDecoratorMedataDataTypeNodeAsReferenced(node: TypeNode | undefined): void { - const entityName = getEntityNameForDecoratorMetadata(node); - if (entityName && isEntityName(entityName)) { - markEntityNameOrEntityExpressionAsReference(entityName); + const tagCallSignatures = getSignaturesOfType(tagType, SignatureKind.Call); + if (!length(tagCallSignatures)) { + return true; } - } - - function getEntityNameForDecoratorMetadata(node: TypeNode | undefined): EntityName | undefined { - if (node) { - switch (node.kind) { - case SyntaxKind.IntersectionType: - case SyntaxKind.UnionType: - return getEntityNameForDecoratorMetadataFromTypeList((node).types); - - case SyntaxKind.ConditionalType: - return getEntityNameForDecoratorMetadataFromTypeList([(node).trueType, (node).falseType]); - - case SyntaxKind.ParenthesizedType: - return getEntityNameForDecoratorMetadata((node).type); - - case SyntaxKind.TypeReference: - return (node).typeName; + const factory = getJsxFactoryEntity(node); + if (!factory) { + return true; + } + const factorySymbol = resolveEntityName(factory, SymbolFlags.Value, /*ignoreErrors*/ true, /*dontResolveAlias*/ false, node); + if (!factorySymbol) { + return true; + } + const factoryType = getTypeOfSymbol(factorySymbol); + const callSignatures = getSignaturesOfType(factoryType, SignatureKind.Call); + if (!length(callSignatures)) { + return true; + } + let hasFirstParamSignatures = false; + let maxParamCount = 0; + // Check that _some_ first parameter expects a FC-like thing, and that some overload of the SFC expects an acceptable number of arguments + for (const sig of callSignatures) { + const firstparam = getTypeAtPosition(sig, 0); + const signaturesOfParam = getSignaturesOfType(firstparam, SignatureKind.Call); + if (!length(signaturesOfParam)) + continue; + for (const paramSig of signaturesOfParam) { + hasFirstParamSignatures = true; + if (hasEffectiveRestParameter(paramSig)) { + return true; // some signature has a rest param, so function components can have an arbitrary number of arguments + } + const paramCount = getParameterCount(paramSig); + if (paramCount > maxParamCount) { + maxParamCount = paramCount; + } } } - } - - function getEntityNameForDecoratorMetadataFromTypeList(types: readonly TypeNode[]): EntityName | undefined { - let commonEntityName: EntityName | undefined; - for (let typeNode of types) { - while (typeNode.kind === SyntaxKind.ParenthesizedType) { - typeNode = (typeNode as ParenthesizedTypeNode).type; // Skip parens if need be - } - if (typeNode.kind === SyntaxKind.NeverKeyword) { - continue; // Always elide `never` from the union/intersection if possible - } - if (!strictNullChecks && (typeNode.kind === SyntaxKind.NullKeyword || typeNode.kind === SyntaxKind.UndefinedKeyword)) { - continue; // Elide null and undefined from unions for metadata, just like what we did prior to the implementation of strict null checks - } - const individualEntityName = getEntityNameForDecoratorMetadata(typeNode); - if (!individualEntityName) { - // Individual is something like string number - // So it would be serialized to either that type or object - // Safe to return here - return undefined; + if (!hasFirstParamSignatures) { + // Not a single signature had a first parameter which expected a signature - for back compat, and + // to guard against generic factories which won't have signatures directly, do not error + return true; + } + let absoluteMinArgCount = Infinity; + for (const tagSig of tagCallSignatures) { + const tagRequiredArgCount = getMinArgumentCount(tagSig); + if (tagRequiredArgCount < absoluteMinArgCount) { + absoluteMinArgCount = tagRequiredArgCount; } - - if (commonEntityName) { - // Note this is in sync with the transformation that happens for type node. - // Keep this in sync with serializeUnionOrIntersectionType - // Verify if they refer to same entity and is identifier - // return undefined if they dont match because we would emit object - if (!isIdentifier(commonEntityName) || - !isIdentifier(individualEntityName) || - commonEntityName.escapedText !== individualEntityName.escapedText) { - return undefined; - } + } + if (absoluteMinArgCount <= maxParamCount) { + return true; // some signature accepts the number of arguments the function component provides + } + if (reportErrors) { + const diag = createDiagnosticForNode(node.tagName, Diagnostics.Tag_0_expects_at_least_1_arguments_but_the_JSX_factory_2_provides_at_most_3, entityNameToString(node.tagName), absoluteMinArgCount, entityNameToString(factory), maxParamCount); + const tagNameDeclaration = getSymbolAtLocation(node.tagName)?.valueDeclaration; + if (tagNameDeclaration) { + addRelatedInfo(diag, createDiagnosticForNode(tagNameDeclaration, Diagnostics._0_is_declared_here, entityNameToString(node.tagName))); } - else { - commonEntityName = individualEntityName; + if (errorOutputContainer && errorOutputContainer.skipLogging) { + (errorOutputContainer.errors || (errorOutputContainer.errors = [])).push(diag); + } + if (!errorOutputContainer.skipLogging) { + diagnostics.add(diag); } } - return commonEntityName; - } - - function getParameterTypeNodeForDecoratorCheck(node: ParameterDeclaration): TypeNode | undefined { - const typeNode = getEffectiveTypeAnnotationNode(node); - return isRestParameter(node) ? getRestParameterElementType(typeNode) : typeNode; + return false; } - - /** Check the decorators of a node */ - function checkDecorators(node: Node): void { - if (!node.decorators) { - return; + } + function getSignatureApplicabilityError(node: CallLikeExpression, args: readonly Expression[], signature: ts.Signature, relation: ts.Map, checkMode: CheckMode, reportErrors: boolean, containingMessageChain: (() => DiagnosticMessageChain | undefined) | undefined): readonly Diagnostic[] | undefined { + const errorOutputContainer: { + errors?: Diagnostic[]; + skipLogging?: boolean; + } = { errors: undefined, skipLogging: true }; + if (isJsxOpeningLikeElement(node)) { + if (!checkApplicableSignatureForJsxOpeningLikeElement(node, signature, relation, checkMode, reportErrors, containingMessageChain, errorOutputContainer)) { + Debug.assert(!reportErrors || !!errorOutputContainer.errors, "jsx should have errors when reporting errors"); + return errorOutputContainer.errors || emptyArray; } - - // skip this check for nodes that cannot have decorators. These should have already had an error reported by - // checkGrammarDecorators. - if (!nodeCanBeDecorated(node, node.parent, node.parent.parent)) { - return; + return undefined; + } + const thisType = getThisTypeOfSignature(signature); + if (thisType && thisType !== voidType && node.kind !== SyntaxKind.NewExpression) { + // If the called expression is not of the form `x.f` or `x["f"]`, then sourceType = voidType + // If the signature's 'this' type is voidType, then the check is skipped -- anything is compatible. + // If the expression is a new expression, then the check is skipped. + const thisArgumentNode = getThisArgumentOfCall(node); + let thisArgumentType: ts.Type; + if (thisArgumentNode) { + thisArgumentType = checkExpression(thisArgumentNode); + if (isOptionalChainRoot(thisArgumentNode.parent)) { + thisArgumentType = getNonNullableType(thisArgumentType); + } + else if (isOptionalChain(thisArgumentNode.parent)) { + thisArgumentType = removeOptionalTypeMarker(thisArgumentType); + } } - - if (!compilerOptions.experimentalDecorators) { - error(node, Diagnostics.Experimental_support_for_decorators_is_a_feature_that_is_subject_to_change_in_a_future_release_Set_the_experimentalDecorators_option_in_your_tsconfig_or_jsconfig_to_remove_this_warning); + else { + thisArgumentType = voidType; + } + const errorNode = reportErrors ? (thisArgumentNode || node) : undefined; + const headMessage = Diagnostics.The_this_context_of_type_0_is_not_assignable_to_method_s_this_of_type_1; + if (!checkTypeRelatedTo(thisArgumentType, thisType, relation, errorNode, headMessage, containingMessageChain, errorOutputContainer)) { + Debug.assert(!reportErrors || !!errorOutputContainer.errors, "this parameter should have errors when reporting errors"); + return errorOutputContainer.errors || emptyArray; + } + } + const headMessage = Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1; + const restType = getNonArrayRestType(signature); + const argCount = restType ? Math.min(getParameterCount(signature) - 1, args.length) : args.length; + for (let i = 0; i < argCount; i++) { + const arg = args[i]; + if (arg.kind !== SyntaxKind.OmittedExpression) { + const paramType = getTypeAtPosition(signature, i); + const argType = checkExpressionWithContextualType(arg, paramType, /*inferenceContext*/ undefined, checkMode); + // If one or more arguments are still excluded (as indicated by CheckMode.SkipContextSensitive), + // we obtain the regular type of any object literal arguments because we may not have inferred complete + // parameter types yet and therefore excess property checks may yield false positives (see #17041). + const checkArgType = checkMode & CheckMode.SkipContextSensitive ? getRegularTypeOfObjectLiteral(argType) : argType; + if (!checkTypeRelatedToAndOptionallyElaborate(checkArgType, paramType, relation, reportErrors ? arg : undefined, arg, headMessage, containingMessageChain, errorOutputContainer)) { + Debug.assert(!reportErrors || !!errorOutputContainer.errors, "parameter should have errors when reporting errors"); + maybeAddMissingAwaitInfo(arg, checkArgType, paramType); + return errorOutputContainer.errors || emptyArray; + } } - - const firstDecorator = node.decorators[0]; - checkExternalEmitHelpers(firstDecorator, ExternalEmitHelpers.Decorate); - if (node.kind === SyntaxKind.Parameter) { - checkExternalEmitHelpers(firstDecorator, ExternalEmitHelpers.Param); + } + if (restType) { + const spreadType = getSpreadArgumentType(args, argCount, args.length, restType, /*context*/ undefined); + const errorNode = reportErrors ? argCount < args.length ? args[argCount] : node : undefined; + if (!checkTypeRelatedTo(spreadType, restType, relation, errorNode, headMessage, /*containingMessageChain*/ undefined, errorOutputContainer)) { + Debug.assert(!reportErrors || !!errorOutputContainer.errors, "rest parameter should have errors when reporting errors"); + maybeAddMissingAwaitInfo(errorNode, spreadType, restType); + return errorOutputContainer.errors || emptyArray; } - - if (compilerOptions.emitDecoratorMetadata) { - checkExternalEmitHelpers(firstDecorator, ExternalEmitHelpers.Metadata); - - // we only need to perform these checks if we are emitting serialized type metadata for the target of a decorator. - switch (node.kind) { - case SyntaxKind.ClassDeclaration: - const constructor = getFirstConstructorWithBody(node); - if (constructor) { - for (const parameter of constructor.parameters) { - markDecoratorMedataDataTypeNodeAsReferenced(getParameterTypeNodeForDecoratorCheck(parameter)); - } - } - break; - - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - const otherKind = node.kind === SyntaxKind.GetAccessor ? SyntaxKind.SetAccessor : SyntaxKind.GetAccessor; - const otherAccessor = getDeclarationOfKind(getSymbolOfNode(node as AccessorDeclaration), otherKind); - markDecoratorMedataDataTypeNodeAsReferenced(getAnnotatedAccessorTypeNode(node as AccessorDeclaration) || otherAccessor && getAnnotatedAccessorTypeNode(otherAccessor)); - break; - case SyntaxKind.MethodDeclaration: - for (const parameter of (node).parameters) { - markDecoratorMedataDataTypeNodeAsReferenced(getParameterTypeNodeForDecoratorCheck(parameter)); - } - - markDecoratorMedataDataTypeNodeAsReferenced(getEffectiveReturnTypeNode(node)); - break; - - case SyntaxKind.PropertyDeclaration: - markDecoratorMedataDataTypeNodeAsReferenced(getEffectiveTypeAnnotationNode(node)); - break; - - case SyntaxKind.Parameter: - markDecoratorMedataDataTypeNodeAsReferenced(getParameterTypeNodeForDecoratorCheck(node)); - const containingSignature = (node as ParameterDeclaration).parent; - for (const parameter of containingSignature.parameters) { - markDecoratorMedataDataTypeNodeAsReferenced(getParameterTypeNodeForDecoratorCheck(parameter)); - } - break; + } + return undefined; + function maybeAddMissingAwaitInfo(errorNode: Node | undefined, source: ts.Type, target: ts.Type) { + if (errorNode && reportErrors && errorOutputContainer.errors && errorOutputContainer.errors.length) { + // Bail if target is Promise-like---something else is wrong + if (getAwaitedTypeOfPromise(target)) { + return; + } + const awaitedTypeOfSource = getAwaitedTypeOfPromise(source); + if (awaitedTypeOfSource && isTypeRelatedTo(awaitedTypeOfSource, target, relation)) { + addRelatedInfo(errorOutputContainer.errors[0], createDiagnosticForNode(errorNode, Diagnostics.Did_you_forget_to_use_await)); } } - - forEach(node.decorators, checkDecorator); } - - function checkFunctionDeclaration(node: FunctionDeclaration): void { - if (produceDiagnostics) { - checkFunctionOrMethodDeclaration(node); - checkGrammarForGenerator(node); - checkCollisionWithRequireExportsInGeneratedCode(node, node.name!); - checkCollisionWithGlobalPromiseInGeneratedCode(node, node.name!); + } + /** + * Returns the this argument in calls like x.f(...) and x[f](...). Undefined otherwise. + */ + function getThisArgumentOfCall(node: CallLikeExpression): LeftHandSideExpression | undefined { + if (node.kind === SyntaxKind.CallExpression) { + const callee = skipOuterExpressions(node.expression); + if (isAccessExpression(callee)) { + return callee.expression; } } - - function checkJSDocTypeAliasTag(node: JSDocTypedefTag | JSDocCallbackTag) { - if (!node.typeExpression) { - // If the node had `@property` tags, `typeExpression` would have been set to the first property tag. - error(node.name, Diagnostics.JSDoc_typedef_tag_should_either_have_a_type_annotation_or_be_followed_by_property_or_member_tags); + } + function createSyntheticExpression(parent: Node, type: ts.Type, isSpread?: boolean) { + const result = (createNode(SyntaxKind.SyntheticExpression, parent.pos, parent.end)); + result.parent = parent; + result.type = type; + result.isSpread = isSpread || false; + return result; + } + /** + * Returns the effective arguments for an expression that works like a function invocation. + */ + function getEffectiveCallArguments(node: CallLikeExpression): readonly Expression[] { + if (node.kind === SyntaxKind.TaggedTemplateExpression) { + const template = node.template; + const args: Expression[] = [createSyntheticExpression(template, getGlobalTemplateStringsArrayType())]; + if (template.kind === SyntaxKind.TemplateExpression) { + forEach(template.templateSpans, span => { + args.push(span.expression); + }); } - - if (node.name) { - checkTypeNameIsReserved(node.name, Diagnostics.Type_alias_name_cannot_be_0); + return args; + } + if (node.kind === SyntaxKind.Decorator) { + return getEffectiveDecoratorArguments(node); + } + if (isJsxOpeningLikeElement(node)) { + return node.attributes.properties.length > 0 || (isJsxOpeningElement(node) && node.parent.children.length > 0) ? [node.attributes] : emptyArray; + } + const args = node.arguments || emptyArray; + const length = args.length; + if (length && isSpreadArgument(args[length - 1]) && getSpreadArgumentIndex(args) === length - 1) { + // We have a spread argument in the last position and no other spread arguments. If the type + // of the argument is a tuple type, spread the tuple elements into the argument list. We can + // call checkExpressionCached because spread expressions never have a contextual type. + const spreadArgument = (args[length - 1]); + const type = flowLoopCount ? checkExpression(spreadArgument.expression) : checkExpressionCached(spreadArgument.expression); + if (isTupleType(type)) { + const typeArguments = getTypeArguments((type)); + const restIndex = type.target.hasRestElement ? typeArguments.length - 1 : -1; + const syntheticArgs = map(typeArguments, (t, i) => createSyntheticExpression(spreadArgument, t, /*isSpread*/ i === restIndex)); + return concatenate(args.slice(0, length - 1), syntheticArgs); } - checkSourceElement(node.typeExpression); } - - function checkJSDocTemplateTag(node: JSDocTemplateTag): void { - checkSourceElement(node.constraint); - for (const tp of node.typeParameters) { - checkSourceElement(tp); + return args; + } + /** + * Returns the synthetic argument list for a decorator invocation. + */ + function getEffectiveDecoratorArguments(node: Decorator): readonly Expression[] { + const parent = node.parent; + const expr = node.expression; + switch (parent.kind) { + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ClassExpression: + // For a class decorator, the `target` is the type of the class (e.g. the + // "static" or "constructor" side of the class). + return [ + createSyntheticExpression(expr, getTypeOfSymbol(getSymbolOfNode(parent))) + ]; + case SyntaxKind.Parameter: + // A parameter declaration decorator will have three arguments (see + // `ParameterDecorator` in core.d.ts). + const func = (parent.parent); + return [ + createSyntheticExpression(expr, parent.parent.kind === SyntaxKind.Constructor ? getTypeOfSymbol(getSymbolOfNode(func)) : errorType), + createSyntheticExpression(expr, anyType), + createSyntheticExpression(expr, numberType) + ]; + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + // A method or accessor declaration decorator will have two or three arguments (see + // `PropertyDecorator` and `MethodDecorator` in core.d.ts). If we are emitting decorators + // for ES3, we will only pass two arguments. + const hasPropDesc = parent.kind !== SyntaxKind.PropertyDeclaration && languageVersion !== ScriptTarget.ES3; + return [ + createSyntheticExpression(expr, getParentTypeOfClassElement((parent))), + createSyntheticExpression(expr, getClassElementPropertyKeyType((parent))), + createSyntheticExpression(expr, hasPropDesc ? createTypedPropertyDescriptorType(getTypeOfNode(parent)) : anyType) + ]; + } + return Debug.fail(); + } + /** + * Returns the argument count for a decorator node that works like a function invocation. + */ + function getDecoratorArgumentCount(node: Decorator, signature: ts.Signature) { + switch (node.parent.kind) { + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ClassExpression: + return 1; + case SyntaxKind.PropertyDeclaration: + return 2; + case SyntaxKind.MethodDeclaration: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + // For ES3 or decorators with only two parameters we supply only two arguments + return languageVersion === ScriptTarget.ES3 || signature.parameters.length <= 2 ? 2 : 3; + case SyntaxKind.Parameter: + return 3; + default: + return Debug.fail(); + } + } + function getDiagnosticSpanForCallNode(node: CallExpression, doNotIncludeArguments?: boolean) { + let start: number; + let length: number; + const sourceFile = getSourceFileOfNode(node); + if (isPropertyAccessExpression(node.expression)) { + const nameSpan = getErrorSpanForNode(sourceFile, node.expression.name); + start = nameSpan.start; + length = doNotIncludeArguments ? nameSpan.length : node.end - start; + } + else { + const expressionSpan = getErrorSpanForNode(sourceFile, node.expression); + start = expressionSpan.start; + length = doNotIncludeArguments ? expressionSpan.length : node.end - start; + } + return { start, length, sourceFile }; + } + function getDiagnosticForCallNode(node: CallLikeExpression, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): DiagnosticWithLocation { + if (isCallExpression(node)) { + const { sourceFile, start, length } = getDiagnosticSpanForCallNode(node); + return createFileDiagnostic(sourceFile, start, length, message, arg0, arg1, arg2, arg3); + } + else { + return createDiagnosticForNode(node, message, arg0, arg1, arg2, arg3); + } + } + function getArgumentArityError(node: CallLikeExpression, signatures: readonly ts.Signature[], args: readonly Expression[]) { + let min = Number.POSITIVE_INFINITY; + let max = Number.NEGATIVE_INFINITY; + let belowArgCount = Number.NEGATIVE_INFINITY; + let aboveArgCount = Number.POSITIVE_INFINITY; + let argCount = args.length; + let closestSignature: ts.Signature | undefined; + for (const sig of signatures) { + const minCount = getMinArgumentCount(sig); + const maxCount = getParameterCount(sig); + if (minCount < argCount && minCount > belowArgCount) + belowArgCount = minCount; + if (argCount < maxCount && maxCount < aboveArgCount) + aboveArgCount = maxCount; + if (minCount < min) { + min = minCount; + closestSignature = sig; + } + max = Math.max(max, maxCount); + } + const hasRestParameter = some(signatures, hasEffectiveRestParameter); + const paramRange = hasRestParameter ? min : + min < max ? min + "-" + max : + min; + const hasSpreadArgument = getSpreadArgumentIndex(args) > -1; + if (argCount <= max && hasSpreadArgument) { + argCount--; + } + let spanArray: NodeArray; + let related: DiagnosticWithLocation | undefined; + const error = hasRestParameter || hasSpreadArgument ? hasRestParameter && hasSpreadArgument ? Diagnostics.Expected_at_least_0_arguments_but_got_1_or_more : + hasRestParameter ? Diagnostics.Expected_at_least_0_arguments_but_got_1 : + Diagnostics.Expected_0_arguments_but_got_1_or_more : Diagnostics.Expected_0_arguments_but_got_1; + if (closestSignature && getMinArgumentCount(closestSignature) > argCount && closestSignature.declaration) { + const paramDecl = closestSignature.declaration.parameters[closestSignature.thisParameter ? argCount + 1 : argCount]; + if (paramDecl) { + related = createDiagnosticForNode(paramDecl, isBindingPattern(paramDecl.name) ? Diagnostics.An_argument_matching_this_binding_pattern_was_not_provided : Diagnostics.An_argument_for_0_was_not_provided, !paramDecl.name ? argCount : !isBindingPattern(paramDecl.name) ? idText(getFirstIdentifier(paramDecl.name)) : undefined); } } - - function checkJSDocTypeTag(node: JSDocTypeTag) { - checkSourceElement(node.typeExpression); + if (min < argCount && argCount < max) { + return getDiagnosticForCallNode(node, Diagnostics.No_overload_expects_0_arguments_but_overloads_do_exist_that_expect_either_1_or_2_arguments, argCount, belowArgCount, aboveArgCount); } - - function checkJSDocParameterTag(node: JSDocParameterTag) { - checkSourceElement(node.typeExpression); - if (!getParameterSymbolFromJSDoc(node)) { - const decl = getHostSignatureFromJSDoc(node); - // don't issue an error for invalid hosts -- just functions -- - // and give a better error message when the host function mentions `arguments` - // but the tag doesn't have an array type - if (decl) { - const i = getJSDocTags(decl).filter(isJSDocParameterTag).indexOf(node); - if (i > -1 && i < decl.parameters.length && isBindingPattern(decl.parameters[i].name)) { - return; + if (!hasSpreadArgument && argCount < min) { + const diagnostic = getDiagnosticForCallNode(node, error, paramRange, argCount); + return related ? addRelatedInfo(diagnostic, related) : diagnostic; + } + if (hasRestParameter || hasSpreadArgument) { + spanArray = createNodeArray(args); + if (hasSpreadArgument && argCount) { + const nextArg = elementAt(args, getSpreadArgumentIndex(args) + 1) || undefined; + spanArray = createNodeArray(args.slice(max > argCount && nextArg ? args.indexOf(nextArg) : Math.min(max, args.length - 1))); + } + } + else { + spanArray = createNodeArray(args.slice(max)); + } + spanArray.pos = first(spanArray).pos; + spanArray.end = last(spanArray).end; + if (spanArray.end === spanArray.pos) { + spanArray.end++; + } + const diagnostic = createDiagnosticForNodeArray(getSourceFileOfNode(node), spanArray, error, paramRange, argCount); + return related ? addRelatedInfo(diagnostic, related) : diagnostic; + } + function getTypeArgumentArityError(node: Node, signatures: readonly ts.Signature[], typeArguments: NodeArray) { + const argCount = typeArguments.length; + // No overloads exist + if (signatures.length === 1) { + const sig = signatures[0]; + const min = getMinTypeArgumentCount(sig.typeParameters); + const max = length(sig.typeParameters); + return createDiagnosticForNodeArray(getSourceFileOfNode(node), typeArguments, Diagnostics.Expected_0_type_arguments_but_got_1, min < max ? min + "-" + max : min, argCount); + } + // Overloads exist + let belowArgCount = -Infinity; + let aboveArgCount = Infinity; + for (const sig of signatures) { + const min = getMinTypeArgumentCount(sig.typeParameters); + const max = length(sig.typeParameters); + if (min > argCount) { + aboveArgCount = Math.min(aboveArgCount, min); + } + else if (max < argCount) { + belowArgCount = Math.max(belowArgCount, max); + } + } + if (belowArgCount !== -Infinity && aboveArgCount !== Infinity) { + return createDiagnosticForNodeArray(getSourceFileOfNode(node), typeArguments, Diagnostics.No_overload_expects_0_type_arguments_but_overloads_do_exist_that_expect_either_1_or_2_type_arguments, argCount, belowArgCount, aboveArgCount); + } + return createDiagnosticForNodeArray(getSourceFileOfNode(node), typeArguments, Diagnostics.Expected_0_type_arguments_but_got_1, belowArgCount === -Infinity ? aboveArgCount : belowArgCount, argCount); + } + function resolveCall(node: CallLikeExpression, signatures: readonly ts.Signature[], candidatesOutArray: ts.Signature[] | undefined, checkMode: CheckMode, callChainFlags: SignatureFlags, fallbackError?: DiagnosticMessage): ts.Signature { + const isTaggedTemplate = node.kind === SyntaxKind.TaggedTemplateExpression; + const isDecorator = node.kind === SyntaxKind.Decorator; + const isJsxOpeningOrSelfClosingElement = isJsxOpeningLikeElement(node); + const reportErrors = !candidatesOutArray; + let typeArguments: NodeArray | undefined; + if (!isDecorator) { + typeArguments = (node).typeArguments; + // We already perform checking on the type arguments on the class declaration itself. + if (isTaggedTemplate || isJsxOpeningOrSelfClosingElement || (node).expression.kind !== SyntaxKind.SuperKeyword) { + forEach(typeArguments, checkSourceElement); + } + } + const candidates = candidatesOutArray || []; + // reorderCandidates fills up the candidates array directly + reorderCandidates(signatures, candidates, callChainFlags); + if (!candidates.length) { + if (reportErrors) { + diagnostics.add(getDiagnosticForCallNode(node, Diagnostics.Call_target_does_not_contain_any_signatures)); + } + return resolveErrorCall(node); + } + const args = getEffectiveCallArguments(node); + // The excludeArgument array contains true for each context sensitive argument (an argument + // is context sensitive it is susceptible to a one-time permanent contextual typing). + // + // The idea is that we will perform type argument inference & assignability checking once + // without using the susceptible parameters that are functions, and once more for those + // parameters, contextually typing each as we go along. + // + // For a tagged template, then the first argument be 'undefined' if necessary because it + // represents a TemplateStringsArray. + // + // For a decorator, no arguments are susceptible to contextual typing due to the fact + // decorators are applied to a declaration by the emitter, and not to an expression. + const isSingleNonGenericCandidate = candidates.length === 1 && !candidates[0].typeParameters; + let argCheckMode = !isDecorator && !isSingleNonGenericCandidate && some(args, isContextSensitive) ? CheckMode.SkipContextSensitive : CheckMode.Normal; + // The following variables are captured and modified by calls to chooseOverload. + // If overload resolution or type argument inference fails, we want to report the + // best error possible. The best error is one which says that an argument was not + // assignable to a parameter. This implies that everything else about the overload + // was fine. So if there is any overload that is only incorrect because of an + // argument, we will report an error on that one. + // + // function foo(s: string): void; + // function foo(n: number): void; // Report argument error on this overload + // function foo(): void; + // foo(true); + // + // If none of the overloads even made it that far, there are two possibilities. + // There was a problem with type arguments for some overload, in which case + // report an error on that. Or none of the overloads even had correct arity, + // in which case give an arity error. + // + // function foo(x: T): void; // Report type argument error + // function foo(): void; + // foo(0); + // + let candidatesForArgumentError: ts.Signature[] | undefined; + let candidateForArgumentArityError: ts.Signature | undefined; + let candidateForTypeArgumentError: ts.Signature | undefined; + let result: ts.Signature | undefined; + // If we are in signature help, a trailing comma indicates that we intend to provide another argument, + // so we will only accept overloads with arity at least 1 higher than the current number of provided arguments. + const signatureHelpTrailingComma = !!(checkMode & CheckMode.IsForSignatureHelp) && node.kind === SyntaxKind.CallExpression && node.arguments.hasTrailingComma; + // Section 4.12.1: + // if the candidate list contains one or more signatures for which the type of each argument + // expression is a subtype of each corresponding parameter type, the return type of the first + // of those signatures becomes the return type of the function call. + // Otherwise, the return type of the first signature in the candidate list becomes the return + // type of the function call. + // + // Whether the call is an error is determined by assignability of the arguments. The subtype pass + // is just important for choosing the best signature. So in the case where there is only one + // signature, the subtype pass is useless. So skipping it is an optimization. + if (candidates.length > 1) { + result = chooseOverload(candidates, subtypeRelation, signatureHelpTrailingComma); + } + if (!result) { + result = chooseOverload(candidates, assignableRelation, signatureHelpTrailingComma); + } + if (result) { + return result; + } + // No signatures were applicable. Now report errors based on the last applicable signature with + // no arguments excluded from assignability checks. + // If candidate is undefined, it means that no candidates had a suitable arity. In that case, + // skip the checkApplicableSignature check. + if (reportErrors) { + if (candidatesForArgumentError) { + if (candidatesForArgumentError.length === 1 || candidatesForArgumentError.length > 3) { + const last = candidatesForArgumentError[candidatesForArgumentError.length - 1]; + let chain: DiagnosticMessageChain | undefined; + if (candidatesForArgumentError.length > 3) { + chain = chainDiagnosticMessages(chain, Diagnostics.The_last_overload_gave_the_following_error); + chain = chainDiagnosticMessages(chain, Diagnostics.No_overload_matches_this_call); + } + const diags = getSignatureApplicabilityError(node, args, last, assignableRelation, CheckMode.Normal, /*reportErrors*/ true, () => chain); + if (diags) { + for (const d of diags) { + if (last.declaration && candidatesForArgumentError.length > 3) { + addRelatedInfo(d, createDiagnosticForNode(last.declaration, Diagnostics.The_last_overload_is_declared_here)); + } + diagnostics.add(d); + } + } + else { + Debug.fail("No error for last overload signature"); } - if (!containsArgumentsReference(decl)) { - if (isQualifiedName(node.name)) { - error(node.name, - Diagnostics.Qualified_name_0_is_not_allowed_without_a_leading_param_object_1, - entityNameToString(node.name), - entityNameToString(node.name.left)); + } + else { + const allDiagnostics: (readonly DiagnosticRelatedInformation[])[] = []; + let max = 0; + let min = Number.MAX_VALUE; + let minIndex = 0; + let i = 0; + for (const c of candidatesForArgumentError) { + const chain = () => chainDiagnosticMessages(/*details*/ undefined, Diagnostics.Overload_0_of_1_2_gave_the_following_error, i + 1, candidates.length, signatureToString(c)); + const diags = getSignatureApplicabilityError(node, args, c, assignableRelation, CheckMode.Normal, /*reportErrors*/ true, chain); + if (diags) { + if (diags.length <= min) { + min = diags.length; + minIndex = i; + } + max = Math.max(max, diags.length); + allDiagnostics.push(diags); } else { - error(node.name, - Diagnostics.JSDoc_param_tag_has_name_0_but_there_is_no_parameter_with_that_name, - idText(node.name)); + Debug.fail("No error for 3 or fewer overload signatures"); } + i++; + } + const diags = max > 1 ? allDiagnostics[minIndex] : flatten(allDiagnostics); + Debug.assert(diags.length > 0, "No errors reported for 3 or fewer overload signatures"); + const chain = chainDiagnosticMessages(map(diags, d => typeof d.messageText === "string" ? (d as DiagnosticMessageChain) : d.messageText), Diagnostics.No_overload_matches_this_call); + const related = (flatMap(diags, d => (d as Diagnostic).relatedInformation) as DiagnosticRelatedInformation[]); + if (every(diags, d => d.start === diags[0].start && d.length === diags[0].length && d.file === diags[0].file)) { + const { file, start, length } = diags[0]; + diagnostics.add({ file, start, length, code: chain.code, category: chain.category, messageText: chain, relatedInformation: related }); } - else if (findLast(getJSDocTags(decl), isJSDocParameterTag) === node && - node.typeExpression && node.typeExpression.type && - !isArrayType(getTypeFromTypeNode(node.typeExpression.type))) { - error(node.name, - Diagnostics.JSDoc_param_tag_has_name_0_but_there_is_no_parameter_with_that_name_It_would_match_arguments_if_it_had_an_array_type, - idText(node.name.kind === SyntaxKind.QualifiedName ? node.name.right : node.name)); + else { + diagnostics.add(createDiagnosticForNodeFromMessageChain(node, chain, related)); } } } - } - - function checkJSDocFunctionType(node: JSDocFunctionType): void { - if (produceDiagnostics && !node.type && !isJSDocConstructSignature(node)) { - reportImplicitAny(node, anyType); - } - checkSignatureDeclaration(node); - } - - function checkJSDocImplementsTag(node: JSDocImplementsTag): void { - const classLike = getJSDocHost(node); - if (!isClassDeclaration(classLike) && !isClassExpression(classLike)) { - error(classLike, Diagnostics.JSDoc_0_is_not_attached_to_a_class, idText(node.tagName)); - return; - } - } - function checkJSDocAugmentsTag(node: JSDocAugmentsTag): void { - const classLike = getJSDocHost(node); - if (!isClassDeclaration(classLike) && !isClassExpression(classLike)) { - error(classLike, Diagnostics.JSDoc_0_is_not_attached_to_a_class, idText(node.tagName)); - return; + else if (candidateForArgumentArityError) { + diagnostics.add(getArgumentArityError(node, [candidateForArgumentArityError], args)); } - - const augmentsTags = getJSDocTags(classLike).filter(isJSDocAugmentsTag); - Debug.assert(augmentsTags.length > 0); - if (augmentsTags.length > 1) { - error(augmentsTags[1], Diagnostics.Class_declarations_cannot_have_more_than_one_augments_or_extends_tag); + else if (candidateForTypeArgumentError) { + checkTypeArguments(candidateForTypeArgumentError, ((node as CallExpression | TaggedTemplateExpression | JsxOpeningLikeElement).typeArguments!), /*reportErrors*/ true, fallbackError); } - - const name = getIdentifierFromEntityNameExpression(node.class.expression); - const extend = getClassExtendsHeritageElement(classLike); - if (extend) { - const className = getIdentifierFromEntityNameExpression(extend.expression); - if (className && name.escapedText !== className.escapedText) { - error(name, Diagnostics.JSDoc_0_1_does_not_match_the_extends_2_clause, idText(node.tagName), idText(name), idText(className)); + else { + const signaturesWithCorrectTypeArgumentArity = filter(signatures, s => hasCorrectTypeArgumentArity(s, typeArguments)); + if (signaturesWithCorrectTypeArgumentArity.length === 0) { + diagnostics.add(getTypeArgumentArityError(node, signatures, typeArguments!)); + } + else if (!isDecorator) { + diagnostics.add(getArgumentArityError(node, signaturesWithCorrectTypeArgumentArity, args)); + } + else if (fallbackError) { + diagnostics.add(getDiagnosticForCallNode(node, fallbackError)); } } } - - function getIdentifierFromEntityNameExpression(node: Identifier | PropertyAccessExpression): Identifier | PrivateIdentifier; - function getIdentifierFromEntityNameExpression(node: Expression): Identifier | PrivateIdentifier | undefined; - function getIdentifierFromEntityNameExpression(node: Expression): Identifier | PrivateIdentifier | undefined { - switch (node.kind) { - case SyntaxKind.Identifier: - return node as Identifier; - case SyntaxKind.PropertyAccessExpression: - return (node as PropertyAccessExpression).name; - default: + return getCandidateForOverloadFailure(node, candidates, args, !!candidatesOutArray); + function chooseOverload(candidates: ts.Signature[], relation: ts.Map, signatureHelpTrailingComma = false) { + candidatesForArgumentError = undefined; + candidateForArgumentArityError = undefined; + candidateForTypeArgumentError = undefined; + if (isSingleNonGenericCandidate) { + const candidate = candidates[0]; + if (some(typeArguments) || !hasCorrectArity(node, args, candidate, signatureHelpTrailingComma)) { return undefined; - } - } - - function checkFunctionOrMethodDeclaration(node: FunctionDeclaration | MethodDeclaration | MethodSignature): void { - checkDecorators(node); - checkSignatureDeclaration(node); - const functionFlags = getFunctionFlags(node); - - // Do not use hasDynamicName here, because that returns false for well known symbols. - // We want to perform checkComputedPropertyName for all computed properties, including - // well known symbols. - if (node.name && node.name.kind === SyntaxKind.ComputedPropertyName) { - // This check will account for methods in class/interface declarations, - // as well as accessors in classes/object literals - checkComputedPropertyName(node.name); - } - - if (!hasNonBindableDynamicName(node)) { - // first we want to check the local symbol that contain this declaration - // - if node.localSymbol !== undefined - this is current declaration is exported and localSymbol points to the local symbol - // - if node.localSymbol === undefined - this node is non-exported so we can just pick the result of getSymbolOfNode - const symbol = getSymbolOfNode(node); - const localSymbol = node.localSymbol || symbol; - - // Since the javascript won't do semantic analysis like typescript, - // if the javascript file comes before the typescript file and both contain same name functions, - // checkFunctionOrConstructorSymbol wouldn't be called if we didnt ignore javascript function. - const firstDeclaration = find(localSymbol.declarations, - // Get first non javascript function declaration - declaration => declaration.kind === node.kind && !(declaration.flags & NodeFlags.JavaScriptFile)); - - // Only type check the symbol once - if (node === firstDeclaration) { - checkFunctionOrConstructorSymbol(localSymbol); } - - if (symbol.parent) { - // run check once for the first declaration - if (getDeclarationOfKind(symbol, node.kind) === node) { - // run check on export symbol to check that modifiers agree across all exported declarations - checkFunctionOrConstructorSymbol(symbol); - } + if (getSignatureApplicabilityError(node, args, candidate, relation, CheckMode.Normal, /*reportErrors*/ false, /*containingMessageChain*/ undefined)) { + candidatesForArgumentError = [candidate]; + return undefined; } + return candidate; } - - const body = node.kind === SyntaxKind.MethodSignature ? undefined : node.body; - checkSourceElement(body); - checkAllCodePathsInNonVoidFunctionReturnOrThrow(node, getReturnTypeFromAnnotation(node)); - - if (produceDiagnostics && !getEffectiveReturnTypeNode(node)) { - // Report an implicit any error if there is no body, no explicit return type, and node is not a private method - // in an ambient context - if (nodeIsMissing(body) && !isPrivateWithinAmbient(node)) { - reportImplicitAny(node, anyType); + for (let candidateIndex = 0; candidateIndex < candidates.length; candidateIndex++) { + const candidate = candidates[candidateIndex]; + if (!hasCorrectTypeArgumentArity(candidate, typeArguments) || !hasCorrectArity(node, args, candidate, signatureHelpTrailingComma)) { + continue; } - - if (functionFlags & FunctionFlags.Generator && nodeIsPresent(body)) { - // A generator with a body and no type annotation can still cause errors. It can error if the - // yielded values have no common supertype, or it can give an implicit any error if it has no - // yielded values. The only way to trigger these errors is to try checking its return type. - getReturnTypeOfSignature(getSignatureFromDeclaration(node)); + let checkCandidate: ts.Signature; + let inferenceContext: InferenceContext | undefined; + if (candidate.typeParameters) { + let typeArgumentTypes: ts.Type[] | undefined; + if (some(typeArguments)) { + typeArgumentTypes = checkTypeArguments(candidate, typeArguments, /*reportErrors*/ false); + if (!typeArgumentTypes) { + candidateForTypeArgumentError = candidate; + continue; + } + } + else { + inferenceContext = createInferenceContext(candidate.typeParameters, candidate, /*flags*/ isInJSFile(node) ? InferenceFlags.AnyDefault : InferenceFlags.None); + typeArgumentTypes = inferTypeArguments(node, candidate, args, argCheckMode | CheckMode.SkipGenericFunctions, inferenceContext); + argCheckMode |= inferenceContext.flags & InferenceFlags.SkippedGenericFunction ? CheckMode.SkipGenericFunctions : CheckMode.Normal; + } + checkCandidate = getSignatureInstantiation(candidate, typeArgumentTypes, isInJSFile(candidate.declaration), inferenceContext && inferenceContext.inferredTypeParameters); + // If the original signature has a generic rest type, instantiation may produce a + // signature with different arity and we need to perform another arity check. + if (getNonArrayRestType(candidate) && !hasCorrectArity(node, args, checkCandidate, signatureHelpTrailingComma)) { + candidateForArgumentArityError = checkCandidate; + continue; + } } - } - - // A js function declaration can have a @type tag instead of a return type node, but that type must have a call signature - if (isInJSFile(node)) { - const typeTag = getJSDocTypeTag(node); - if (typeTag && typeTag.typeExpression && !getContextualCallSignature(getTypeFromTypeNode(typeTag.typeExpression), node)) { - error(typeTag, Diagnostics.The_type_of_a_function_declaration_must_match_the_function_s_signature); + else { + checkCandidate = candidate; } - } - } - - function registerForUnusedIdentifiersCheck(node: PotentiallyUnusedIdentifier): void { - // May be in a call such as getTypeOfNode that happened to call this. But potentiallyUnusedIdentifiers is only defined in the scope of `checkSourceFile`. - if (produceDiagnostics) { - const sourceFile = getSourceFileOfNode(node); - let potentiallyUnusedIdentifiers = allPotentiallyUnusedIdentifiers.get(sourceFile.path); - if (!potentiallyUnusedIdentifiers) { - potentiallyUnusedIdentifiers = []; - allPotentiallyUnusedIdentifiers.set(sourceFile.path, potentiallyUnusedIdentifiers); + if (getSignatureApplicabilityError(node, args, checkCandidate, relation, argCheckMode, /*reportErrors*/ false, /*containingMessageChain*/ undefined)) { + // Give preference to error candidates that have no rest parameters (as they are more specific) + (candidatesForArgumentError || (candidatesForArgumentError = [])).push(checkCandidate); + continue; } - // TODO: GH#22580 - // Debug.assert(addToSeen(seenPotentiallyUnusedIdentifiers, getNodeId(node)), "Adding potentially-unused identifier twice"); - potentiallyUnusedIdentifiers.push(node); - } - } - - type PotentiallyUnusedIdentifier = - | SourceFile | ModuleDeclaration | ClassLikeDeclaration | InterfaceDeclaration - | Block | CaseBlock | ForStatement | ForInStatement | ForOfStatement - | Exclude | TypeAliasDeclaration - | InferTypeNode; - - function checkUnusedIdentifiers(potentiallyUnusedIdentifiers: readonly PotentiallyUnusedIdentifier[], addDiagnostic: AddUnusedDiagnostic) { - for (const node of potentiallyUnusedIdentifiers) { - switch (node.kind) { - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ClassExpression: - checkUnusedClassMembers(node, addDiagnostic); - checkUnusedTypeParameters(node, addDiagnostic); - break; - case SyntaxKind.SourceFile: - case SyntaxKind.ModuleDeclaration: - case SyntaxKind.Block: - case SyntaxKind.CaseBlock: - case SyntaxKind.ForStatement: - case SyntaxKind.ForInStatement: - case SyntaxKind.ForOfStatement: - checkUnusedLocalsAndParameters(node, addDiagnostic); - break; - case SyntaxKind.Constructor: - case SyntaxKind.FunctionExpression: - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.ArrowFunction: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - if (node.body) { // Don't report unused parameters in overloads - checkUnusedLocalsAndParameters(node, addDiagnostic); + if (argCheckMode) { + // If one or more context sensitive arguments were excluded, we start including + // them now (and keeping do so for any subsequent candidates) and perform a second + // round of type inference and applicability checking for this particular candidate. + argCheckMode = CheckMode.Normal; + if (inferenceContext) { + const typeArgumentTypes = inferTypeArguments(node, candidate, args, argCheckMode, inferenceContext); + checkCandidate = getSignatureInstantiation(candidate, typeArgumentTypes, isInJSFile(candidate.declaration), inferenceContext && inferenceContext.inferredTypeParameters); + // If the original signature has a generic rest type, instantiation may produce a + // signature with different arity and we need to perform another arity check. + if (getNonArrayRestType(candidate) && !hasCorrectArity(node, args, checkCandidate, signatureHelpTrailingComma)) { + candidateForArgumentArityError = checkCandidate; + continue; } - checkUnusedTypeParameters(node, addDiagnostic); - break; - case SyntaxKind.MethodSignature: - case SyntaxKind.CallSignature: - case SyntaxKind.ConstructSignature: - case SyntaxKind.FunctionType: - case SyntaxKind.ConstructorType: - case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.InterfaceDeclaration: - checkUnusedTypeParameters(node, addDiagnostic); - break; - case SyntaxKind.InferType: - checkUnusedInferTypeParameter(node, addDiagnostic); - break; - default: - Debug.assertNever(node, "Node should not have been registered for unused identifiers check"); + } + if (getSignatureApplicabilityError(node, args, checkCandidate, relation, argCheckMode, /*reportErrors*/ false, /*containingMessageChain*/ undefined)) { + // Give preference to error candidates that have no rest parameters (as they are more specific) + (candidatesForArgumentError || (candidatesForArgumentError = [])).push(checkCandidate); + continue; + } } + candidates[candidateIndex] = checkCandidate; + return checkCandidate; } + return undefined; } - - function errorUnusedLocal(declaration: Declaration, name: string, addDiagnostic: AddUnusedDiagnostic) { - const node = getNameOfDeclaration(declaration) || declaration; - const message = isTypeDeclaration(declaration) ? Diagnostics._0_is_declared_but_never_used : Diagnostics._0_is_declared_but_its_value_is_never_read; - addDiagnostic(declaration, UnusedKind.Local, createDiagnosticForNode(node, message, name)); + } + // No signature was applicable. We have already reported the errors for the invalid signature. + function getCandidateForOverloadFailure(node: CallLikeExpression, candidates: ts.Signature[], args: readonly Expression[], hasCandidatesOutArray: boolean): ts.Signature { + Debug.assert(candidates.length > 0); // Else should not have called this. + checkNodeDeferred(node); + // Normally we will combine overloads. Skip this if they have type parameters since that's hard to combine. + // Don't do this if there is a `candidatesOutArray`, + // because then we want the chosen best candidate to be one of the overloads, not a combination. + return hasCandidatesOutArray || candidates.length === 1 || candidates.some(c => !!c.typeParameters) + ? pickLongestCandidateSignature(node, candidates, args) + : createUnionOfSignaturesForOverloadFailure(candidates); + } + function createUnionOfSignaturesForOverloadFailure(candidates: readonly ts.Signature[]): ts.Signature { + const thisParameters = mapDefined(candidates, c => c.thisParameter); + let thisParameter: ts.Symbol | undefined; + if (thisParameters.length) { + thisParameter = createCombinedSymbolFromTypes(thisParameters, thisParameters.map(getTypeOfParameter)); + } + const { min: minArgumentCount, max: maxNonRestParam } = minAndMax(candidates, getNumNonRestParameters); + const parameters: ts.Symbol[] = []; + for (let i = 0; i < maxNonRestParam; i++) { + const symbols = mapDefined(candidates, s => signatureHasRestParameter(s) ? + i < s.parameters.length - 1 ? s.parameters[i] : last(s.parameters) : + i < s.parameters.length ? s.parameters[i] : undefined); + Debug.assert(symbols.length !== 0); + parameters.push(createCombinedSymbolFromTypes(symbols, mapDefined(candidates, candidate => tryGetTypeAtPosition(candidate, i)))); + } + const restParameterSymbols = mapDefined(candidates, c => signatureHasRestParameter(c) ? last(c.parameters) : undefined); + let flags = SignatureFlags.None; + if (restParameterSymbols.length !== 0) { + const type = createArrayType(getUnionType(mapDefined(candidates, tryGetRestTypeOfSignature), UnionReduction.Subtype)); + parameters.push(createCombinedSymbolForOverloadFailure(restParameterSymbols, type)); + flags |= SignatureFlags.HasRestParameter; + } + if (candidates.some(signatureHasLiteralTypes)) { + flags |= SignatureFlags.HasLiteralTypes; + } + return createSignature(candidates[0].declaration, + /*typeParameters*/ undefined, // Before calling this we tested for `!candidates.some(c => !!c.typeParameters)`. + thisParameter, parameters, + /*resolvedReturnType*/ getIntersectionType(candidates.map(getReturnTypeOfSignature)), + /*typePredicate*/ undefined, minArgumentCount, flags); + } + function getNumNonRestParameters(signature: ts.Signature): number { + const numParams = signature.parameters.length; + return signatureHasRestParameter(signature) ? numParams - 1 : numParams; + } + function createCombinedSymbolFromTypes(sources: readonly ts.Symbol[], types: ts.Type[]): ts.Symbol { + return createCombinedSymbolForOverloadFailure(sources, getUnionType(types, UnionReduction.Subtype)); + } + function createCombinedSymbolForOverloadFailure(sources: readonly ts.Symbol[], type: ts.Type): ts.Symbol { + // This function is currently only used for erroneous overloads, so it's good enough to just use the first source. + return createSymbolWithType(first(sources), type); + } + function pickLongestCandidateSignature(node: CallLikeExpression, candidates: ts.Signature[], args: readonly Expression[]): ts.Signature { + // Pick the longest signature. This way we can get a contextual type for cases like: + // declare function f(a: { xa: number; xb: number; }, b: number); + // f({ | + // Also, use explicitly-supplied type arguments if they are provided, so we can get a contextual signature in cases like: + // declare function f(k: keyof T); + // f(" + const bestIndex = getLongestCandidateIndex(candidates, apparentArgumentCount === undefined ? args.length : apparentArgumentCount); + const candidate = candidates[bestIndex]; + const { typeParameters } = candidate; + if (!typeParameters) { + return candidate; + } + const typeArgumentNodes: readonly TypeNode[] | undefined = callLikeExpressionMayHaveTypeArguments(node) ? node.typeArguments : undefined; + const instantiated = typeArgumentNodes + ? createSignatureInstantiation(candidate, getTypeArgumentsFromNodes(typeArgumentNodes, typeParameters, isInJSFile(node))) + : inferSignatureInstantiationForOverloadFailure(node, typeParameters, candidate, args); + candidates[bestIndex] = instantiated; + return instantiated; + } + function getTypeArgumentsFromNodes(typeArgumentNodes: readonly TypeNode[], typeParameters: readonly TypeParameter[], isJs: boolean): readonly ts.Type[] { + const typeArguments = typeArgumentNodes.map(getTypeOfNode); + while (typeArguments.length > typeParameters.length) { + typeArguments.pop(); } - - function isIdentifierThatStartsWithUnderscore(node: Node) { - return isIdentifier(node) && idText(node).charCodeAt(0) === CharacterCodes._; + while (typeArguments.length < typeParameters.length) { + typeArguments.push(getConstraintOfTypeParameter(typeParameters[typeArguments.length]) || getDefaultTypeArgumentType(isJs)); } - - function checkUnusedClassMembers(node: ClassDeclaration | ClassExpression, addDiagnostic: AddUnusedDiagnostic): void { - for (const member of node.members) { - switch (member.kind) { - case SyntaxKind.MethodDeclaration: - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - if (member.kind === SyntaxKind.SetAccessor && member.symbol.flags & SymbolFlags.GetAccessor) { - // Already would have reported an error on the getter. - break; - } - const symbol = getSymbolOfNode(member); - if (!symbol.isReferenced - && (hasModifier(member, ModifierFlags.Private) || isNamedDeclaration(member) && isPrivateIdentifier(member.name)) - && !(member.flags & NodeFlags.Ambient)) { - addDiagnostic(member, UnusedKind.Local, createDiagnosticForNode(member.name!, Diagnostics._0_is_declared_but_its_value_is_never_read, symbolToString(symbol))); - } - break; - case SyntaxKind.Constructor: - for (const parameter of (member).parameters) { - if (!parameter.symbol.isReferenced && hasModifier(parameter, ModifierFlags.Private)) { - addDiagnostic(parameter, UnusedKind.Local, createDiagnosticForNode(parameter.name, Diagnostics.Property_0_is_declared_but_its_value_is_never_read, symbolName(parameter.symbol))); - } - } - break; - case SyntaxKind.IndexSignature: - case SyntaxKind.SemicolonClassElement: - // Can't be private - break; - default: - Debug.fail(); - } + return typeArguments; + } + function inferSignatureInstantiationForOverloadFailure(node: CallLikeExpression, typeParameters: readonly TypeParameter[], candidate: ts.Signature, args: readonly Expression[]): ts.Signature { + const inferenceContext = createInferenceContext(typeParameters, candidate, /*flags*/ isInJSFile(node) ? InferenceFlags.AnyDefault : InferenceFlags.None); + const typeArgumentTypes = inferTypeArguments(node, candidate, args, CheckMode.SkipContextSensitive | CheckMode.SkipGenericFunctions, inferenceContext); + return createSignatureInstantiation(candidate, typeArgumentTypes); + } + function getLongestCandidateIndex(candidates: ts.Signature[], argsCount: number): number { + let maxParamsIndex = -1; + let maxParams = -1; + for (let i = 0; i < candidates.length; i++) { + const candidate = candidates[i]; + const paramCount = getParameterCount(candidate); + if (hasEffectiveRestParameter(candidate) || paramCount >= argsCount) { + return i; } - } - - function checkUnusedInferTypeParameter(node: InferTypeNode, addDiagnostic: AddUnusedDiagnostic): void { - const { typeParameter } = node; - if (isTypeParameterUnused(typeParameter)) { - addDiagnostic(node, UnusedKind.Parameter, createDiagnosticForNode(node, Diagnostics._0_is_declared_but_its_value_is_never_read, idText(typeParameter.name))); + if (paramCount > maxParams) { + maxParams = paramCount; + maxParamsIndex = i; } } - - function checkUnusedTypeParameters(node: ClassLikeDeclaration | SignatureDeclaration | InterfaceDeclaration | TypeAliasDeclaration, addDiagnostic: AddUnusedDiagnostic): void { - // Only report errors on the last declaration for the type parameter container; - // this ensures that all uses have been accounted for. - if (last(getSymbolOfNode(node).declarations) !== node) return; - - const typeParameters = getEffectiveTypeParameterDeclarations(node); - const seenParentsWithEveryUnused = new NodeSet(); - - for (const typeParameter of typeParameters) { - if (!isTypeParameterUnused(typeParameter)) continue; - - const name = idText(typeParameter.name); - const { parent } = typeParameter; - if (parent.kind !== SyntaxKind.InferType && parent.typeParameters!.every(isTypeParameterUnused)) { - if (seenParentsWithEveryUnused.tryAdd(parent)) { - const range = isJSDocTemplateTag(parent) - // Whole @template tag - ? rangeOfNode(parent) - // Include the `<>` in the error message - : rangeOfTypeParameters(parent.typeParameters!); - const only = typeParameters.length === 1; - const message = only ? Diagnostics._0_is_declared_but_its_value_is_never_read : Diagnostics.All_type_parameters_are_unused; - const arg0 = only ? name : undefined; - addDiagnostic(typeParameter, UnusedKind.Parameter, createFileDiagnostic(getSourceFileOfNode(parent), range.pos, range.end - range.pos, message, arg0)); + return maxParamsIndex; + } + function resolveCallExpression(node: CallExpression, candidatesOutArray: ts.Signature[] | undefined, checkMode: CheckMode): ts.Signature { + if (node.expression.kind === SyntaxKind.SuperKeyword) { + const superType = checkSuperExpression(node.expression); + if (isTypeAny(superType)) { + for (const arg of node.arguments) { + checkExpression(arg); // Still visit arguments so they get marked for visibility, etc + } + return anySignature; + } + if (superType !== errorType) { + // In super call, the candidate signatures are the matching arity signatures of the base constructor function instantiated + // with the type arguments specified in the extends clause. + const baseTypeNode = getEffectiveBaseTypeNode((getContainingClass(node)!)); + if (baseTypeNode) { + const baseConstructors = getInstantiatedConstructorsForTypeArguments(superType, baseTypeNode.typeArguments, baseTypeNode); + return resolveCall(node, baseConstructors, candidatesOutArray, checkMode, SignatureFlags.None); + } + } + return resolveUntypedCall(node); + } + let callChainFlags: SignatureFlags; + let funcType = checkExpression(node.expression); + if (isCallChain(node)) { + const nonOptionalType = getOptionalExpressionType(funcType, node.expression); + callChainFlags = nonOptionalType === funcType ? SignatureFlags.None : + isOutermostOptionalChain(node) ? SignatureFlags.IsOuterCallChain : + SignatureFlags.IsInnerCallChain; + funcType = nonOptionalType; + } + else { + callChainFlags = SignatureFlags.None; + } + funcType = checkNonNullTypeWithReporter(funcType, node.expression, reportCannotInvokePossiblyNullOrUndefinedError); + if (funcType === silentNeverType) { + return silentNeverSignature; + } + const apparentType = getApparentType(funcType); + if (apparentType === errorType) { + // Another error has already been reported + return resolveErrorCall(node); + } + // Technically, this signatures list may be incomplete. We are taking the apparent type, + // but we are not including call signatures that may have been added to the Object or + // Function interface, since they have none by default. This is a bit of a leap of faith + // that the user will not add any. + const callSignatures = getSignaturesOfType(apparentType, SignatureKind.Call); + const numConstructSignatures = getSignaturesOfType(apparentType, SignatureKind.Construct).length; + // TS 1.0 Spec: 4.12 + // In an untyped function call no TypeArgs are permitted, Args can be any argument list, no contextual + // types are provided for the argument expressions, and the result is always of type Any. + if (isUntypedFunctionCall(funcType, apparentType, callSignatures.length, numConstructSignatures)) { + // The unknownType indicates that an error already occurred (and was reported). No + // need to report another error in this case. + if (funcType !== errorType && node.typeArguments) { + error(node, Diagnostics.Untyped_function_calls_may_not_accept_type_arguments); + } + return resolveUntypedCall(node); + } + // If FuncExpr's apparent type(section 3.8.1) is a function type, the call is a typed function call. + // TypeScript employs overload resolution in typed function calls in order to support functions + // with multiple call signatures. + if (!callSignatures.length) { + if (numConstructSignatures) { + error(node, Diagnostics.Value_of_type_0_is_not_callable_Did_you_mean_to_include_new, typeToString(funcType)); + } + else { + let relatedInformation: DiagnosticRelatedInformation | undefined; + if (node.arguments.length === 1) { + const text = getSourceFileOfNode(node).text; + if (isLineBreak(text.charCodeAt(skipTrivia(text, node.expression.end, /* stopAfterLineBreak */ true) - 1))) { + relatedInformation = createDiagnosticForNode(node.expression, Diagnostics.Are_you_missing_a_semicolon); } } - else { - addDiagnostic(typeParameter, UnusedKind.Parameter, createDiagnosticForNode(typeParameter, Diagnostics._0_is_declared_but_its_value_is_never_read, name)); - } + invocationError(node.expression, apparentType, SignatureKind.Call, relatedInformation); } + return resolveErrorCall(node); } - function isTypeParameterUnused(typeParameter: TypeParameterDeclaration): boolean { - return !(getMergedSymbol(typeParameter.symbol).isReferenced! & SymbolFlags.TypeParameter) && !isIdentifierThatStartsWithUnderscore(typeParameter.name); + // When a call to a generic function is an argument to an outer call to a generic function for which + // inference is in process, we have a choice to make. If the inner call relies on inferences made from + // its contextual type to its return type, deferring the inner call processing allows the best possible + // contextual type to accumulate. But if the outer call relies on inferences made from the return type of + // the inner call, the inner call should be processed early. There's no sure way to know which choice is + // right (only a full unification algorithm can determine that), so we resort to the following heuristic: + // If no type arguments are specified in the inner call and at least one call signature is generic and + // returns a function type, we choose to defer processing. This narrowly permits function composition + // operators to flow inferences through return types, but otherwise processes calls right away. We + // use the resolvingSignature singleton to indicate that we deferred processing. This result will be + // propagated out and eventually turned into nonInferrableType (a type that is assignable to anything and + // from which we never make inferences). + if (checkMode & CheckMode.SkipGenericFunctions && !node.typeArguments && callSignatures.some(isGenericFunctionReturningFunction)) { + skippedGenericFunction(node, checkMode); + return resolvingSignature; + } + // If the function is explicitly marked with `@class`, then it must be constructed. + if (callSignatures.some(sig => isInJSFile(sig.declaration) && !!getJSDocClassTag((sig.declaration!)))) { + error(node, Diagnostics.Value_of_type_0_is_not_callable_Did_you_mean_to_include_new, typeToString(funcType)); + return resolveErrorCall(node); } - - function addToGroup(map: Map<[K, V[]]>, key: K, value: V, getKey: (key: K) => number | string): void { - const keyString = String(getKey(key)); - const group = map.get(keyString); - if (group) { - group[1].push(value); - } - else { - map.set(keyString, [key, [value]]); + return resolveCall(node, callSignatures, candidatesOutArray, checkMode, callChainFlags); + } + function isGenericFunctionReturningFunction(signature: ts.Signature) { + return !!(signature.typeParameters && isFunctionType(getReturnTypeOfSignature(signature))); + } + /** + * TS 1.0 spec: 4.12 + * If FuncExpr is of type Any, or of an object type that has no call or construct signatures + * but is a subtype of the Function interface, the call is an untyped function call. + */ + function isUntypedFunctionCall(funcType: ts.Type, apparentFuncType: ts.Type, numCallSignatures: number, numConstructSignatures: number): boolean { + // We exclude union types because we may have a union of function types that happen to have no common signatures. + return isTypeAny(funcType) || isTypeAny(apparentFuncType) && !!(funcType.flags & TypeFlags.TypeParameter) || + !numCallSignatures && !numConstructSignatures && !(apparentFuncType.flags & (TypeFlags.Union | TypeFlags.Never)) && isTypeAssignableTo(funcType, globalFunctionType); + } + function resolveNewExpression(node: NewExpression, candidatesOutArray: ts.Signature[] | undefined, checkMode: CheckMode): ts.Signature { + if (node.arguments && languageVersion < ScriptTarget.ES5) { + const spreadIndex = getSpreadArgumentIndex(node.arguments); + if (spreadIndex >= 0) { + error(node.arguments[spreadIndex], Diagnostics.Spread_operator_in_new_expressions_is_only_available_when_targeting_ECMAScript_5_and_higher); + } + } + let expressionType = checkNonNullExpression(node.expression); + if (expressionType === silentNeverType) { + return silentNeverSignature; + } + // If expressionType's apparent type(section 3.8.1) is an object type with one or + // more construct signatures, the expression is processed in the same manner as a + // function call, but using the construct signatures as the initial set of candidate + // signatures for overload resolution. The result type of the function call becomes + // the result type of the operation. + expressionType = getApparentType(expressionType); + if (expressionType === errorType) { + // Another error has already been reported + return resolveErrorCall(node); + } + // TS 1.0 spec: 4.11 + // If expressionType is of type Any, Args can be any argument + // list and the result of the operation is of type Any. + if (isTypeAny(expressionType)) { + if (node.typeArguments) { + error(node, Diagnostics.Untyped_function_calls_may_not_accept_type_arguments); } + return resolveUntypedCall(node); } - - function tryGetRootParameterDeclaration(node: Node): ParameterDeclaration | undefined { - return tryCast(getRootDeclaration(node), isParameter); + // Technically, this signatures list may be incomplete. We are taking the apparent type, + // but we are not including construct signatures that may have been added to the Object or + // Function interface, since they have none by default. This is a bit of a leap of faith + // that the user will not add any. + const constructSignatures = getSignaturesOfType(expressionType, SignatureKind.Construct); + if (constructSignatures.length) { + if (!isConstructorAccessible(node, constructSignatures[0])) { + return resolveErrorCall(node); + } + // If the expression is a class of abstract type, then it cannot be instantiated. + // Note, only class declarations can be declared abstract. + // In the case of a merged class-module or class-interface declaration, + // only the class declaration node will have the Abstract flag set. + const valueDecl = expressionType.symbol && getClassLikeDeclarationOfSymbol(expressionType.symbol); + if (valueDecl && hasModifier(valueDecl, ModifierFlags.Abstract)) { + error(node, Diagnostics.Cannot_create_an_instance_of_an_abstract_class); + return resolveErrorCall(node); + } + return resolveCall(node, constructSignatures, candidatesOutArray, checkMode, SignatureFlags.None); } - - function checkUnusedLocalsAndParameters(nodeWithLocals: Node, addDiagnostic: AddUnusedDiagnostic): void { - // Ideally we could use the ImportClause directly as a key, but must wait until we have full ES6 maps. So must store key along with value. - const unusedImports = createMap<[ImportClause, ImportedDeclaration[]]>(); - const unusedDestructures = createMap<[ObjectBindingPattern, BindingElement[]]>(); - const unusedVariables = createMap<[VariableDeclarationList, VariableDeclaration[]]>(); - nodeWithLocals.locals!.forEach(local => { - // If it's purely a type parameter, ignore, will be checked in `checkUnusedTypeParameters`. - // If it's a type parameter merged with a parameter, check if the parameter-side is used. - if (local.flags & SymbolFlags.TypeParameter ? !(local.flags & SymbolFlags.Variable && !(local.isReferenced! & SymbolFlags.Variable)) : local.isReferenced || local.exportSymbol) { - return; + // If expressionType's apparent type is an object type with no construct signatures but + // one or more call signatures, the expression is processed as a function call. A compile-time + // error occurs if the result of the function call is not Void. The type of the result of the + // operation is Any. It is an error to have a Void this type. + const callSignatures = getSignaturesOfType(expressionType, SignatureKind.Call); + if (callSignatures.length) { + const signature = resolveCall(node, callSignatures, candidatesOutArray, checkMode, SignatureFlags.None); + if (!noImplicitAny) { + if (signature.declaration && !isJSConstructor(signature.declaration) && getReturnTypeOfSignature(signature) !== voidType) { + error(node, Diagnostics.Only_a_void_function_can_be_called_with_the_new_keyword); } - - for (const declaration of local.declarations) { - if (isAmbientModule(declaration) || - (isVariableDeclaration(declaration) && isForInOrOfStatement(declaration.parent.parent) || isImportedDeclaration(declaration)) && isIdentifierThatStartsWithUnderscore(declaration.name!)) { - continue; - } - - if (isImportedDeclaration(declaration)) { - addToGroup(unusedImports, importClauseFromImported(declaration), declaration, getNodeId); - } - else if (isBindingElement(declaration) && isObjectBindingPattern(declaration.parent)) { - // In `{ a, ...b }, `a` is considered used since it removes a property from `b`. `b` may still be unused though. - const lastElement = last(declaration.parent.elements); - if (declaration === lastElement || !last(declaration.parent.elements).dotDotDotToken) { - addToGroup(unusedDestructures, declaration.parent, declaration, getNodeId); - } - } - else if (isVariableDeclaration(declaration)) { - addToGroup(unusedVariables, declaration.parent, declaration, getNodeId); - } - else { - const parameter = local.valueDeclaration && tryGetRootParameterDeclaration(local.valueDeclaration); - const name = local.valueDeclaration && getNameOfDeclaration(local.valueDeclaration); - if (parameter && name) { - if (!isParameterPropertyDeclaration(parameter, parameter.parent) && !parameterIsThisKeyword(parameter) && !isIdentifierThatStartsWithUnderscore(name)) { - addDiagnostic(parameter, UnusedKind.Parameter, createDiagnosticForNode(name, Diagnostics._0_is_declared_but_its_value_is_never_read, symbolName(local))); - } + if (getThisTypeOfSignature(signature) === voidType) { + error(node, Diagnostics.A_function_that_is_called_with_the_new_keyword_cannot_have_a_this_type_that_is_void); + } + } + return signature; + } + invocationError(node.expression, expressionType, SignatureKind.Construct); + return resolveErrorCall(node); + } + function typeHasProtectedAccessibleBase(target: ts.Symbol, type: InterfaceType): boolean { + const baseTypes = getBaseTypes(type); + if (!length(baseTypes)) { + return false; + } + const firstBase = baseTypes[0]; + if (firstBase.flags & TypeFlags.Intersection) { + const types = (firstBase as IntersectionType).types; + const mixinFlags = findMixins(types); + let i = 0; + for (const intersectionMember of (firstBase as IntersectionType).types) { + // We want to ignore mixin ctors + if (!mixinFlags[i]) { + if (getObjectFlags(intersectionMember) & (ObjectFlags.Class | ObjectFlags.Interface)) { + if (intersectionMember.symbol === target) { + return true; } - else { - errorUnusedLocal(declaration, symbolName(local), addDiagnostic); + if (typeHasProtectedAccessibleBase(target, (intersectionMember as InterfaceType))) { + return true; } } } - }); - unusedImports.forEach(([importClause, unuseds]) => { - const importDecl = importClause.parent; - const nDeclarations = (importClause.name ? 1 : 0) + - (importClause.namedBindings ? - (importClause.namedBindings.kind === SyntaxKind.NamespaceImport ? 1 : importClause.namedBindings.elements.length) - : 0); - if (nDeclarations === unuseds.length) { - addDiagnostic(importDecl, UnusedKind.Local, unuseds.length === 1 - ? createDiagnosticForNode(importDecl, Diagnostics._0_is_declared_but_its_value_is_never_read, idText(first(unuseds).name!)) - : createDiagnosticForNode(importDecl, Diagnostics.All_imports_in_import_declaration_are_unused)); - } - else { - for (const unused of unuseds) errorUnusedLocal(unused, idText(unused.name!), addDiagnostic); - } - }); - unusedDestructures.forEach(([bindingPattern, bindingElements]) => { - const kind = tryGetRootParameterDeclaration(bindingPattern.parent) ? UnusedKind.Parameter : UnusedKind.Local; - if (bindingPattern.elements.length === bindingElements.length) { - if (bindingElements.length === 1 && bindingPattern.parent.kind === SyntaxKind.VariableDeclaration && bindingPattern.parent.parent.kind === SyntaxKind.VariableDeclarationList) { - addToGroup(unusedVariables, bindingPattern.parent.parent, bindingPattern.parent, getNodeId); - } - else { - addDiagnostic(bindingPattern, kind, bindingElements.length === 1 - ? createDiagnosticForNode(bindingPattern, Diagnostics._0_is_declared_but_its_value_is_never_read, bindingNameText(first(bindingElements).name)) - : createDiagnosticForNode(bindingPattern, Diagnostics.All_destructured_elements_are_unused)); - } + i++; + } + return false; + } + if (firstBase.symbol === target) { + return true; + } + return typeHasProtectedAccessibleBase(target, (firstBase as InterfaceType)); + } + function isConstructorAccessible(node: NewExpression, signature: ts.Signature) { + if (!signature || !signature.declaration) { + return true; + } + const declaration = signature.declaration; + const modifiers = getSelectedModifierFlags(declaration, ModifierFlags.NonPublicAccessibilityModifier); + // (1) Public constructors and (2) constructor functions are always accessible. + if (!modifiers || declaration.kind !== SyntaxKind.Constructor) { + return true; + } + const declaringClassDeclaration = (getClassLikeDeclarationOfSymbol(declaration.parent.symbol)!); + const declaringClass = (getDeclaredTypeOfSymbol(declaration.parent.symbol)); + // A private or protected constructor can only be instantiated within its own class (or a subclass, for protected) + if (!isNodeWithinClass(node, declaringClassDeclaration)) { + const containingClass = getContainingClass(node); + if (containingClass && modifiers & ModifierFlags.Protected) { + const containingType = getTypeOfNode(containingClass); + if (typeHasProtectedAccessibleBase(declaration.parent.symbol, (containingType as InterfaceType))) { + return true; } - else { - for (const e of bindingElements) { - addDiagnostic(e, kind, createDiagnosticForNode(e, Diagnostics._0_is_declared_but_its_value_is_never_read, bindingNameText(e.name))); + } + if (modifiers & ModifierFlags.Private) { + error(node, Diagnostics.Constructor_of_class_0_is_private_and_only_accessible_within_the_class_declaration, typeToString(declaringClass)); + } + if (modifiers & ModifierFlags.Protected) { + error(node, Diagnostics.Constructor_of_class_0_is_protected_and_only_accessible_within_the_class_declaration, typeToString(declaringClass)); + } + return false; + } + return true; + } + function invocationErrorDetails(apparentType: ts.Type, kind: SignatureKind): { + messageChain: DiagnosticMessageChain; + relatedMessage: DiagnosticMessage | undefined; + } { + let errorInfo: DiagnosticMessageChain | undefined; + const isCall = kind === SignatureKind.Call; + const awaitedType = getAwaitedType(apparentType); + const maybeMissingAwait = awaitedType && getSignaturesOfType(awaitedType, kind).length > 0; + if (apparentType.flags & TypeFlags.Union) { + const types = (apparentType as UnionType).types; + let hasSignatures = false; + for (const constituent of types) { + const signatures = getSignaturesOfType(constituent, kind); + if (signatures.length !== 0) { + hasSignatures = true; + if (errorInfo) { + // Bail early if we already have an error, no chance of "No constituent of type is callable" + break; } } - }); - unusedVariables.forEach(([declarationList, declarations]) => { - if (declarationList.declarations.length === declarations.length) { - addDiagnostic(declarationList, UnusedKind.Local, declarations.length === 1 - ? createDiagnosticForNode(first(declarations).name, Diagnostics._0_is_declared_but_its_value_is_never_read, bindingNameText(first(declarations).name)) - : createDiagnosticForNode(declarationList.parent.kind === SyntaxKind.VariableStatement ? declarationList.parent : declarationList, Diagnostics.All_variables_are_unused)); - } else { - for (const decl of declarations) { - addDiagnostic(decl, UnusedKind.Local, createDiagnosticForNode(decl, Diagnostics._0_is_declared_but_its_value_is_never_read, bindingNameText(decl.name))); + // Error on the first non callable constituent only + if (!errorInfo) { + errorInfo = chainDiagnosticMessages(errorInfo, isCall ? + Diagnostics.Type_0_has_no_call_signatures : + Diagnostics.Type_0_has_no_construct_signatures, typeToString(constituent)); + errorInfo = chainDiagnosticMessages(errorInfo, isCall ? + Diagnostics.Not_all_constituents_of_type_0_are_callable : + Diagnostics.Not_all_constituents_of_type_0_are_constructable, typeToString(apparentType)); + } + if (hasSignatures) { + // Bail early if we already found a siganture, no chance of "No constituent of type is callable" + break; } } - }); + } + if (!hasSignatures) { + errorInfo = chainDiagnosticMessages( + /* detials */ undefined, isCall ? + Diagnostics.No_constituent_of_type_0_is_callable : + Diagnostics.No_constituent_of_type_0_is_constructable, typeToString(apparentType)); + } + if (!errorInfo) { + errorInfo = chainDiagnosticMessages(errorInfo, isCall ? + Diagnostics.Each_member_of_the_union_type_0_has_signatures_but_none_of_those_signatures_are_compatible_with_each_other : + Diagnostics.Each_member_of_the_union_type_0_has_construct_signatures_but_none_of_those_signatures_are_compatible_with_each_other, typeToString(apparentType)); + } } - - function bindingNameText(name: BindingName): string { - switch (name.kind) { - case SyntaxKind.Identifier: - return idText(name); - case SyntaxKind.ArrayBindingPattern: - case SyntaxKind.ObjectBindingPattern: - return bindingNameText(cast(first(name.elements), isBindingElement).name); - default: - return Debug.assertNever(name); + else { + errorInfo = chainDiagnosticMessages(errorInfo, isCall ? + Diagnostics.Type_0_has_no_call_signatures : + Diagnostics.Type_0_has_no_construct_signatures, typeToString(apparentType)); + } + return { + messageChain: chainDiagnosticMessages(errorInfo, isCall ? Diagnostics.This_expression_is_not_callable : Diagnostics.This_expression_is_not_constructable), + relatedMessage: maybeMissingAwait ? Diagnostics.Did_you_forget_to_use_await : undefined, + }; + } + function invocationError(errorTarget: Node, apparentType: ts.Type, kind: SignatureKind, relatedInformation?: DiagnosticRelatedInformation) { + const { messageChain, relatedMessage: relatedInfo } = invocationErrorDetails(apparentType, kind); + const diagnostic = createDiagnosticForNodeFromMessageChain(errorTarget, messageChain); + if (relatedInfo) { + addRelatedInfo(diagnostic, createDiagnosticForNode(errorTarget, relatedInfo)); + } + if (isCallExpression(errorTarget.parent)) { + const { start, length } = getDiagnosticSpanForCallNode(errorTarget.parent, /* doNotIncludeArguments */ true); + diagnostic.start = start; + diagnostic.length = length; + } + diagnostics.add(diagnostic); + invocationErrorRecovery(apparentType, kind, relatedInformation ? addRelatedInfo(diagnostic, relatedInformation) : diagnostic); + } + function invocationErrorRecovery(apparentType: ts.Type, kind: SignatureKind, diagnostic: Diagnostic) { + if (!apparentType.symbol) { + return; + } + const importNode = getSymbolLinks(apparentType.symbol).originatingImport; + // Create a diagnostic on the originating import if possible onto which we can attach a quickfix + // An import call expression cannot be rewritten into another form to correct the error - the only solution is to use `.default` at the use-site + if (importNode && !isImportCall(importNode)) { + const sigs = getSignaturesOfType(getTypeOfSymbol(getSymbolLinks(apparentType.symbol).target!), kind); + if (!sigs || !sigs.length) + return; + addRelatedInfo(diagnostic, createDiagnosticForNode(importNode, Diagnostics.Type_originates_at_this_import_A_namespace_style_import_cannot_be_called_or_constructed_and_will_cause_a_failure_at_runtime_Consider_using_a_default_import_or_import_require_here_instead)); + } + } + function resolveTaggedTemplateExpression(node: TaggedTemplateExpression, candidatesOutArray: ts.Signature[] | undefined, checkMode: CheckMode): ts.Signature { + const tagType = checkExpression(node.tag); + const apparentType = getApparentType(tagType); + if (apparentType === errorType) { + // Another error has already been reported + return resolveErrorCall(node); + } + const callSignatures = getSignaturesOfType(apparentType, SignatureKind.Call); + const numConstructSignatures = getSignaturesOfType(apparentType, SignatureKind.Construct).length; + if (isUntypedFunctionCall(tagType, apparentType, callSignatures.length, numConstructSignatures)) { + return resolveUntypedCall(node); + } + if (!callSignatures.length) { + invocationError(node.tag, apparentType, SignatureKind.Call); + return resolveErrorCall(node); + } + return resolveCall(node, callSignatures, candidatesOutArray, checkMode, SignatureFlags.None); + } + /** + * Gets the localized diagnostic head message to use for errors when resolving a decorator as a call expression. + */ + function getDiagnosticHeadMessageForDecoratorResolution(node: Decorator) { + switch (node.parent.kind) { + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ClassExpression: + return Diagnostics.Unable_to_resolve_signature_of_class_decorator_when_called_as_an_expression; + case SyntaxKind.Parameter: + return Diagnostics.Unable_to_resolve_signature_of_parameter_decorator_when_called_as_an_expression; + case SyntaxKind.PropertyDeclaration: + return Diagnostics.Unable_to_resolve_signature_of_property_decorator_when_called_as_an_expression; + case SyntaxKind.MethodDeclaration: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + return Diagnostics.Unable_to_resolve_signature_of_method_decorator_when_called_as_an_expression; + default: + return Debug.fail(); + } + } + /** + * Resolves a decorator as if it were a call expression. + */ + function resolveDecorator(node: Decorator, candidatesOutArray: ts.Signature[] | undefined, checkMode: CheckMode): ts.Signature { + const funcType = checkExpression(node.expression); + const apparentType = getApparentType(funcType); + if (apparentType === errorType) { + return resolveErrorCall(node); + } + const callSignatures = getSignaturesOfType(apparentType, SignatureKind.Call); + const numConstructSignatures = getSignaturesOfType(apparentType, SignatureKind.Construct).length; + if (isUntypedFunctionCall(funcType, apparentType, callSignatures.length, numConstructSignatures)) { + return resolveUntypedCall(node); + } + if (isPotentiallyUncalledDecorator(node, callSignatures)) { + const nodeStr = getTextOfNode(node.expression, /*includeTrivia*/ false); + error(node, Diagnostics._0_accepts_too_few_arguments_to_be_used_as_a_decorator_here_Did_you_mean_to_call_it_first_and_write_0, nodeStr); + return resolveErrorCall(node); + } + const headMessage = getDiagnosticHeadMessageForDecoratorResolution(node); + if (!callSignatures.length) { + const errorDetails = invocationErrorDetails(apparentType, SignatureKind.Call); + const messageChain = chainDiagnosticMessages(errorDetails.messageChain, headMessage); + const diag = createDiagnosticForNodeFromMessageChain(node.expression, messageChain); + if (errorDetails.relatedMessage) { + addRelatedInfo(diag, createDiagnosticForNode(node.expression, errorDetails.relatedMessage)); } + diagnostics.add(diag); + invocationErrorRecovery(apparentType, SignatureKind.Call, diag); + return resolveErrorCall(node); } - - type ImportedDeclaration = ImportClause | ImportSpecifier | NamespaceImport; - function isImportedDeclaration(node: Node): node is ImportedDeclaration { - return node.kind === SyntaxKind.ImportClause || node.kind === SyntaxKind.ImportSpecifier || node.kind === SyntaxKind.NamespaceImport; + return resolveCall(node, callSignatures, candidatesOutArray, checkMode, SignatureFlags.None, headMessage); + } + function createSignatureForJSXIntrinsic(node: JsxOpeningLikeElement, result: ts.Type): ts.Signature { + const namespace = getJsxNamespaceAt(node); + const exports = namespace && getExportsOfSymbol(namespace); + // We fake up a SFC signature for each intrinsic, however a more specific per-element signature drawn from the JSX declaration + // file would probably be preferable. + const typeSymbol = exports && getSymbol(exports, JsxNames.Element, SymbolFlags.Type); + const returnNode = typeSymbol && nodeBuilder.symbolToEntityName(typeSymbol, SymbolFlags.Type, node); + const declaration = createFunctionTypeNode(/*typeParameters*/ undefined, [createParameter(/*decorators*/ undefined, /*modifiers*/ undefined, /*dotdotdot*/ undefined, "props", /*questionMark*/ undefined, nodeBuilder.typeToTypeNode(result, node))], returnNode ? createTypeReferenceNode(returnNode, /*typeArguments*/ undefined) : createKeywordTypeNode(SyntaxKind.AnyKeyword)); + const parameterSymbol = createSymbol(SymbolFlags.FunctionScopedVariable, ("props" as __String)); + parameterSymbol.type = result; + return createSignature(declaration, + /*typeParameters*/ undefined, + /*thisParameter*/ undefined, [parameterSymbol], typeSymbol ? getDeclaredTypeOfSymbol(typeSymbol) : errorType, + /*returnTypePredicate*/ undefined, 1, SignatureFlags.None); + } + function resolveJsxOpeningLikeElement(node: JsxOpeningLikeElement, candidatesOutArray: ts.Signature[] | undefined, checkMode: CheckMode): ts.Signature { + if (isJsxIntrinsicIdentifier(node.tagName)) { + const result = getIntrinsicAttributesTypeFromJsxOpeningLikeElement(node); + const fakeSignature = createSignatureForJSXIntrinsic(node, result); + checkTypeAssignableToAndOptionallyElaborate(checkExpressionWithContextualType(node.attributes, getEffectiveFirstArgumentForJsxSignature(fakeSignature, node), /*mapper*/ undefined, CheckMode.Normal), result, node.tagName, node.attributes); + return fakeSignature; + } + const exprTypes = checkExpression(node.tagName); + const apparentType = getApparentType(exprTypes); + if (apparentType === errorType) { + return resolveErrorCall(node); } - function importClauseFromImported(decl: ImportedDeclaration): ImportClause { - return decl.kind === SyntaxKind.ImportClause ? decl : decl.kind === SyntaxKind.NamespaceImport ? decl.parent : decl.parent.parent; + const signatures = getUninstantiatedJsxSignaturesOfType(exprTypes, node); + if (isUntypedFunctionCall(exprTypes, apparentType, signatures.length, /*constructSignatures*/ 0)) { + return resolveUntypedCall(node); } - - function checkBlock(node: Block) { - // Grammar checking for SyntaxKind.Block - if (node.kind === SyntaxKind.Block) { - checkGrammarStatementInAmbientContext(node); + if (signatures.length === 0) { + // We found no signatures at all, which is an error + error(node.tagName, Diagnostics.JSX_element_type_0_does_not_have_any_construct_or_call_signatures, getTextOfNode(node.tagName)); + return resolveErrorCall(node); + } + return resolveCall(node, signatures, candidatesOutArray, checkMode, SignatureFlags.None); + } + /** + * Sometimes, we have a decorator that could accept zero arguments, + * but is receiving too many arguments as part of the decorator invocation. + * In those cases, a user may have meant to *call* the expression before using it as a decorator. + */ + function isPotentiallyUncalledDecorator(decorator: Decorator, signatures: readonly ts.Signature[]) { + return signatures.length && every(signatures, signature => signature.minArgumentCount === 0 && + !signatureHasRestParameter(signature) && + signature.parameters.length < getDecoratorArgumentCount(decorator, signature)); + } + function resolveSignature(node: CallLikeExpression, candidatesOutArray: ts.Signature[] | undefined, checkMode: CheckMode): ts.Signature { + switch (node.kind) { + case SyntaxKind.CallExpression: + return resolveCallExpression(node, candidatesOutArray, checkMode); + case SyntaxKind.NewExpression: + return resolveNewExpression(node, candidatesOutArray, checkMode); + case SyntaxKind.TaggedTemplateExpression: + return resolveTaggedTemplateExpression(node, candidatesOutArray, checkMode); + case SyntaxKind.Decorator: + return resolveDecorator(node, candidatesOutArray, checkMode); + case SyntaxKind.JsxOpeningElement: + case SyntaxKind.JsxSelfClosingElement: + return resolveJsxOpeningLikeElement(node, candidatesOutArray, checkMode); + } + throw Debug.assertNever(node, "Branch in 'resolveSignature' should be unreachable."); + } + /** + * Resolve a signature of a given call-like expression. + * @param node a call-like expression to try resolve a signature for + * @param candidatesOutArray an array of signature to be filled in by the function. It is passed by signature help in the language service; + * the function will fill it up with appropriate candidate signatures + * @return a signature of the call-like expression or undefined if one can't be found + */ + function getResolvedSignature(node: CallLikeExpression, candidatesOutArray?: ts.Signature[] | undefined, checkMode?: CheckMode): ts.Signature { + const links = getNodeLinks(node); + // If getResolvedSignature has already been called, we will have cached the resolvedSignature. + // However, it is possible that either candidatesOutArray was not passed in the first time, + // or that a different candidatesOutArray was passed in. Therefore, we need to redo the work + // to correctly fill the candidatesOutArray. + const cached = links.resolvedSignature; + if (cached && cached !== resolvingSignature && !candidatesOutArray) { + return cached; + } + links.resolvedSignature = resolvingSignature; + const result = resolveSignature(node, candidatesOutArray, checkMode || CheckMode.Normal); + // When CheckMode.SkipGenericFunctions is set we use resolvingSignature to indicate that call + // resolution should be deferred. + if (result !== resolvingSignature) { + // If signature resolution originated in control flow type analysis (for example to compute the + // assigned type in a flow assignment) we don't cache the result as it may be based on temporary + // types from the control flow analysis. + links.resolvedSignature = flowLoopStart === flowLoopCount ? result : cached; + } + return result; + } + /** + * Indicates whether a declaration can be treated as a constructor in a JavaScript + * file. + */ + function isJSConstructor(node: Node | undefined): node is FunctionDeclaration | FunctionExpression { + if (!node || !isInJSFile(node)) { + return false; + } + const func = isFunctionDeclaration(node) || isFunctionExpression(node) ? node : + isVariableDeclaration(node) && node.initializer && isFunctionExpression(node.initializer) ? node.initializer : + undefined; + if (func) { + // If the node has a @class tag, treat it like a constructor. + if (getJSDocClassTag(node)) + return true; + // If the symbol of the node has members, treat it like a constructor. + const symbol = getSymbolOfNode(func); + return !!symbol && hasEntries(symbol.members); + } + return false; + } + function mergeJSSymbols(target: ts.Symbol, source: ts.Symbol | undefined) { + if (source) { + const links = getSymbolLinks(source); + if (!links.inferredClassSymbol || !links.inferredClassSymbol.has("" + getSymbolId(target))) { + const inferred = isTransientSymbol(target) ? target : cloneSymbol(target) as TransientSymbol; + inferred.exports = inferred.exports || createSymbolTable(); + inferred.members = inferred.members || createSymbolTable(); + inferred.flags |= source.flags & SymbolFlags.Class; + if (hasEntries(source.exports)) { + mergeSymbolTable(inferred.exports, source.exports); + } + if (hasEntries(source.members)) { + mergeSymbolTable(inferred.members, source.members); + } + (links.inferredClassSymbol || (links.inferredClassSymbol = createMap())).set("" + getSymbolId(inferred), inferred); + return inferred; } - if (isFunctionOrModuleBlock(node)) { - const saveFlowAnalysisDisabled = flowAnalysisDisabled; - forEach(node.statements, checkSourceElement); - flowAnalysisDisabled = saveFlowAnalysisDisabled; + return links.inferredClassSymbol.get("" + getSymbolId(target)); + } + } + function getAssignedClassSymbol(decl: Declaration): ts.Symbol | undefined { + const assignmentSymbol = decl && decl.parent && + (isFunctionDeclaration(decl) && getSymbolOfNode(decl) || + isBinaryExpression(decl.parent) && getSymbolOfNode(decl.parent.left) || + isVariableDeclaration(decl.parent) && getSymbolOfNode(decl.parent)); + const prototype = assignmentSymbol && assignmentSymbol.exports && assignmentSymbol.exports.get(("prototype" as __String)); + const init = prototype && prototype.valueDeclaration && getAssignedJSPrototype(prototype.valueDeclaration); + return init ? getSymbolOfNode(init) : undefined; + } + function getAssignedJSPrototype(node: Node) { + if (!node.parent) { + return false; + } + let parent: Node = node.parent; + while (parent && parent.kind === SyntaxKind.PropertyAccessExpression) { + parent = parent.parent; + } + if (parent && isBinaryExpression(parent) && isPrototypeAccess(parent.left) && parent.operatorToken.kind === SyntaxKind.EqualsToken) { + const right = getInitializerOfBinaryExpression(parent); + return isObjectLiteralExpression(right) && right; + } + } + /** + * Syntactically and semantically checks a call or new expression. + * @param node The call/new expression to be checked. + * @returns On success, the expression's signature's return type. On failure, anyType. + */ + function checkCallExpression(node: CallExpression | NewExpression, checkMode?: CheckMode): ts.Type { + if (!checkGrammarTypeArguments(node, node.typeArguments)) + checkGrammarArguments(node.arguments); + const signature = getResolvedSignature(node, /*candidatesOutArray*/ undefined, checkMode); + if (signature === resolvingSignature) { + // CheckMode.SkipGenericFunctions is enabled and this is a call to a generic function that + // returns a function type. We defer checking and return nonInferrableType. + return nonInferrableType; + } + if (node.expression.kind === SyntaxKind.SuperKeyword) { + return voidType; + } + if (node.kind === SyntaxKind.NewExpression) { + const declaration = signature.declaration; + if (declaration && + declaration.kind !== SyntaxKind.Constructor && + declaration.kind !== SyntaxKind.ConstructSignature && + declaration.kind !== SyntaxKind.ConstructorType && + !isJSDocConstructSignature(declaration) && + !isJSConstructor(declaration)) { + // When resolved signature is a call signature (and not a construct signature) the result type is any + if (noImplicitAny) { + error(node, Diagnostics.new_expression_whose_target_lacks_a_construct_signature_implicitly_has_an_any_type); + } + return anyType; } - else { - forEach(node.statements, checkSourceElement); + } + // In JavaScript files, calls to any identifier 'require' are treated as external module imports + if (isInJSFile(node) && isCommonJsRequire(node)) { + return resolveExternalModuleTypeByLiteral((node.arguments![0] as StringLiteral)); + } + const returnType = getReturnTypeOfSignature(signature); + // Treat any call to the global 'Symbol' function that is part of a const variable or readonly property + // as a fresh unique symbol literal type. + if (returnType.flags & TypeFlags.ESSymbolLike && isSymbolOrSymbolForCall(node)) { + return getESSymbolLikeTypeForNode(walkUpParenthesizedExpressions(node.parent)); + } + if (node.kind === SyntaxKind.CallExpression && node.parent.kind === SyntaxKind.ExpressionStatement && + returnType.flags & TypeFlags.Void && getTypePredicateOfSignature(signature)) { + if (!isDottedName(node.expression)) { + error(node.expression, Diagnostics.Assertions_require_the_call_target_to_be_an_identifier_or_qualified_name); } - if (node.locals) { - registerForUnusedIdentifiersCheck(node); + else if (!getEffectsSignature(node)) { + const diagnostic = error(node.expression, Diagnostics.Assertions_require_every_name_in_the_call_target_to_be_declared_with_an_explicit_type_annotation); + getTypeOfDottedName(node.expression, diagnostic); } } - - function checkCollisionWithArgumentsInGeneratedCode(node: SignatureDeclaration) { - // no rest parameters \ declaration context \ overload - no codegen impact - if (languageVersion >= ScriptTarget.ES2015 || compilerOptions.noEmit || !hasRestParameter(node) || node.flags & NodeFlags.Ambient || nodeIsMissing((node).body)) { - return; - } - - forEach(node.parameters, p => { - if (p.name && !isBindingPattern(p.name) && p.name.escapedText === argumentsSymbol.escapedName) { - error(p, Diagnostics.Duplicate_identifier_arguments_Compiler_uses_arguments_to_initialize_rest_parameters); + if (isInJSFile(node)) { + const decl = getDeclarationOfExpando(node); + if (decl) { + const jsSymbol = getSymbolOfNode(decl); + if (jsSymbol && hasEntries(jsSymbol.exports)) { + const jsAssignmentType = createAnonymousType(jsSymbol, jsSymbol.exports, emptyArray, emptyArray, undefined, undefined); + jsAssignmentType.objectFlags |= ObjectFlags.JSLiteral; + return getIntersectionType([returnType, jsAssignmentType]); } - }); + } } - - function needCollisionCheckForIdentifier(node: Node, identifier: Identifier | undefined, name: string): boolean { - if (!(identifier && identifier.escapedText === name)) { - return false; + return returnType; + } + function isSymbolOrSymbolForCall(node: Node) { + if (!isCallExpression(node)) + return false; + let left = node.expression; + if (isPropertyAccessExpression(left) && left.name.escapedText === "for") { + left = left.expression; + } + if (!isIdentifier(left) || left.escapedText !== "Symbol") { + return false; + } + // make sure `Symbol` is the global symbol + const globalESSymbol = getGlobalESSymbolConstructorSymbol(/*reportErrors*/ false); + if (!globalESSymbol) { + return false; + } + return globalESSymbol === resolveName(left, ("Symbol" as __String), SymbolFlags.Value, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false); + } + function checkImportCallExpression(node: ImportCall): ts.Type { + // Check grammar of dynamic import + if (!checkGrammarArguments(node.arguments)) + checkGrammarImportCallExpression(node); + if (node.arguments.length === 0) { + return createPromiseReturnType(node, anyType); + } + const specifier = node.arguments[0]; + const specifierType = checkExpressionCached(specifier); + // Even though multiple arguments is grammatically incorrect, type-check extra arguments for completion + for (let i = 1; i < node.arguments.length; ++i) { + checkExpressionCached(node.arguments[i]); + } + if (specifierType.flags & TypeFlags.Undefined || specifierType.flags & TypeFlags.Null || !isTypeAssignableTo(specifierType, stringType)) { + error(specifier, Diagnostics.Dynamic_import_s_specifier_must_be_of_type_string_but_here_has_type_0, typeToString(specifierType)); + } + // resolveExternalModuleName will return undefined if the moduleReferenceExpression is not a string literal + const moduleSymbol = resolveExternalModuleName(node, specifier); + if (moduleSymbol) { + const esModuleSymbol = resolveESModuleSymbol(moduleSymbol, specifier, /*dontRecursivelyResolve*/ true, /*suppressUsageError*/ false); + if (esModuleSymbol) { + return createPromiseReturnType(node, getTypeWithSyntheticDefaultImportType(getTypeOfSymbol(esModuleSymbol), esModuleSymbol, moduleSymbol)); } - - if (node.kind === SyntaxKind.PropertyDeclaration || - node.kind === SyntaxKind.PropertySignature || - node.kind === SyntaxKind.MethodDeclaration || - node.kind === SyntaxKind.MethodSignature || - node.kind === SyntaxKind.GetAccessor || - node.kind === SyntaxKind.SetAccessor) { - // it is ok to have member named '_super' or '_this' - member access is always qualified - return false; + } + return createPromiseReturnType(node, anyType); + } + function getTypeWithSyntheticDefaultImportType(type: ts.Type, symbol: ts.Symbol, originalSymbol: ts.Symbol): ts.Type { + if (allowSyntheticDefaultImports && type && type !== errorType) { + const synthType = (type as SyntheticDefaultModuleType); + if (!synthType.syntheticType) { + const file = find(originalSymbol.declarations, isSourceFile); + const hasSyntheticDefault = canHaveSyntheticDefault(file, originalSymbol, /*dontResolveAlias*/ false); + if (hasSyntheticDefault) { + const memberTable = createSymbolTable(); + const newSymbol = createSymbol(SymbolFlags.Alias, InternalSymbolName.Default); + newSymbol.nameType = getLiteralType("default"); + newSymbol.target = resolveSymbol(symbol); + memberTable.set(InternalSymbolName.Default, newSymbol); + const anonymousSymbol = createSymbol(SymbolFlags.TypeLiteral, InternalSymbolName.Type); + const defaultContainingObject = createAnonymousType(anonymousSymbol, memberTable, emptyArray, emptyArray, /*stringIndexInfo*/ undefined, /*numberIndexInfo*/ undefined); + anonymousSymbol.type = defaultContainingObject; + synthType.syntheticType = isValidSpreadType(type) ? getSpreadType(type, defaultContainingObject, anonymousSymbol, /*objectFlags*/ 0, /*readonly*/ false) : defaultContainingObject; + } + else { + synthType.syntheticType = type; + } } - - if (node.flags & NodeFlags.Ambient) { - // ambient context - no codegen impact - return false; + return synthType.syntheticType; + } + return type; + } + function isCommonJsRequire(node: Node): boolean { + if (!isRequireCall(node, /*checkArgumentIsStringLiteralLike*/ true)) { + return false; + } + // Make sure require is not a local function + if (!isIdentifier(node.expression)) + return Debug.fail(); + const resolvedRequire = (resolveName(node.expression, node.expression.escapedText, SymbolFlags.Value, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ true)!); // TODO: GH#18217 + if (resolvedRequire === requireSymbol) { + return true; + } + // project includes symbol named 'require' - make sure that it is ambient and local non-alias + if (resolvedRequire.flags & SymbolFlags.Alias) { + return false; + } + const targetDeclarationKind = resolvedRequire.flags & SymbolFlags.Function + ? SyntaxKind.FunctionDeclaration + : resolvedRequire.flags & SymbolFlags.Variable + ? SyntaxKind.VariableDeclaration + : SyntaxKind.Unknown; + if (targetDeclarationKind !== SyntaxKind.Unknown) { + const decl = (getDeclarationOfKind(resolvedRequire, targetDeclarationKind)!); + // function/variable declaration should be ambient + return !!decl && !!(decl.flags & NodeFlags.Ambient); + } + return false; + } + function checkTaggedTemplateExpression(node: TaggedTemplateExpression): ts.Type { + if (!checkGrammarTaggedTemplateChain(node)) + checkGrammarTypeArguments(node, node.typeArguments); + if (languageVersion < ScriptTarget.ES2015) { + checkExternalEmitHelpers(node, ExternalEmitHelpers.MakeTemplateObject); + } + return getReturnTypeOfSignature(getResolvedSignature(node)); + } + function checkAssertion(node: AssertionExpression) { + return checkAssertionWorker(node, node.type, node.expression); + } + function isValidConstAssertionArgument(node: Node): boolean { + switch (node.kind) { + case SyntaxKind.StringLiteral: + case SyntaxKind.NoSubstitutionTemplateLiteral: + case SyntaxKind.NumericLiteral: + case SyntaxKind.BigIntLiteral: + case SyntaxKind.TrueKeyword: + case SyntaxKind.FalseKeyword: + case SyntaxKind.ArrayLiteralExpression: + case SyntaxKind.ObjectLiteralExpression: + return true; + case SyntaxKind.ParenthesizedExpression: + return isValidConstAssertionArgument((node).expression); + case SyntaxKind.PrefixUnaryExpression: + const op = (node).operator; + const arg = (node).operand; + return op === SyntaxKind.MinusToken && (arg.kind === SyntaxKind.NumericLiteral || arg.kind === SyntaxKind.BigIntLiteral) || + op === SyntaxKind.PlusToken && arg.kind === SyntaxKind.NumericLiteral; + case SyntaxKind.PropertyAccessExpression: + case SyntaxKind.ElementAccessExpression: + const expr = (node).expression; + if (isIdentifier(expr)) { + let symbol = getSymbolAtLocation(expr); + if (symbol && symbol.flags & SymbolFlags.Alias) { + symbol = resolveAlias(symbol); + } + return !!(symbol && (symbol.flags & SymbolFlags.Enum) && getEnumKind(symbol) === EnumKind.Literal); + } + } + return false; + } + function checkAssertionWorker(errNode: Node, type: TypeNode, expression: UnaryExpression | Expression, checkMode?: CheckMode) { + let exprType = checkExpression(expression, checkMode); + if (isConstTypeReference(type)) { + if (!isValidConstAssertionArgument(expression)) { + error(expression, Diagnostics.A_const_assertions_can_only_be_applied_to_references_to_enum_members_or_string_number_boolean_array_or_object_literals); } - - const root = getRootDeclaration(node); - if (root.kind === SyntaxKind.Parameter && nodeIsMissing((root.parent).body)) { - // just an overload - no codegen impact - return false; + return getRegularTypeOfLiteralType(exprType); + } + checkSourceElement(type); + exprType = getRegularTypeOfObjectLiteral(getBaseTypeOfLiteralType(exprType)); + const targetType = getTypeFromTypeNode(type); + if (produceDiagnostics && targetType !== errorType) { + const widenedType = getWidenedType(exprType); + if (!isTypeComparableTo(targetType, widenedType)) { + checkTypeComparableTo(exprType, targetType, errNode, Diagnostics.Conversion_of_type_0_to_type_1_may_be_a_mistake_because_neither_type_sufficiently_overlaps_with_the_other_If_this_was_intentional_convert_the_expression_to_unknown_first); } - - return true; } - - // this function will run after checking the source file so 'CaptureThis' is correct for all nodes - function checkIfThisIsCapturedInEnclosingScope(node: Node): void { - findAncestor(node, current => { - if (getNodeCheckFlags(current) & NodeCheckFlags.CaptureThis) { - const isDeclaration = node.kind !== SyntaxKind.Identifier; - if (isDeclaration) { - error(getNameOfDeclaration(node), Diagnostics.Duplicate_identifier_this_Compiler_uses_variable_declaration_this_to_capture_this_reference); - } - else { - error(node, Diagnostics.Expression_resolves_to_variable_declaration_this_that_compiler_uses_to_capture_this_reference); - } - return true; - } - return false; - }); + return targetType; + } + function checkNonNullAssertion(node: NonNullExpression) { + return getNonNullableType(checkExpression(node.expression)); + } + function checkMetaProperty(node: MetaProperty): ts.Type { + checkGrammarMetaProperty(node); + if (node.keywordToken === SyntaxKind.NewKeyword) { + return checkNewTargetMetaProperty(node); } - - function checkIfNewTargetIsCapturedInEnclosingScope(node: Node): void { - findAncestor(node, current => { - if (getNodeCheckFlags(current) & NodeCheckFlags.CaptureNewTarget) { - const isDeclaration = node.kind !== SyntaxKind.Identifier; - if (isDeclaration) { - error(getNameOfDeclaration(node), Diagnostics.Duplicate_identifier_newTarget_Compiler_uses_variable_declaration_newTarget_to_capture_new_target_meta_property_reference); - } - else { - error(node, Diagnostics.Expression_resolves_to_variable_declaration_newTarget_that_compiler_uses_to_capture_new_target_meta_property_reference); - } - return true; - } - return false; - }); + if (node.keywordToken === SyntaxKind.ImportKeyword) { + return checkImportMetaProperty(node); + } + return Debug.assertNever(node.keywordToken); + } + function checkNewTargetMetaProperty(node: MetaProperty) { + const container = getNewTargetContainer(node); + if (!container) { + error(node, Diagnostics.Meta_property_0_is_only_allowed_in_the_body_of_a_function_declaration_function_expression_or_constructor, "new.target"); + return errorType; } - - function checkWeakMapCollision(node: Node) { - const enclosingBlockScope = getEnclosingBlockScopeContainer(node); - if (getNodeCheckFlags(enclosingBlockScope) & NodeCheckFlags.ContainsClassWithPrivateIdentifiers) { - error(node, Diagnostics.Compiler_reserves_name_0_when_emitting_private_identifier_downlevel, "WeakMap"); - } + else if (container.kind === SyntaxKind.Constructor) { + const symbol = getSymbolOfNode((container.parent as ClassLikeDeclaration)); + return getTypeOfSymbol(symbol); } - - function checkCollisionWithRequireExportsInGeneratedCode(node: Node, name: Identifier) { - // No need to check for require or exports for ES6 modules and later - if (moduleKind >= ModuleKind.ES2015 || compilerOptions.noEmit) { - return; - } - - if (!needCollisionCheckForIdentifier(node, name, "require") && !needCollisionCheckForIdentifier(node, name, "exports")) { - return; + else { + const symbol = getSymbolOfNode(container)!; + return getTypeOfSymbol(symbol); + } + } + function checkImportMetaProperty(node: MetaProperty) { + if (moduleKind !== ModuleKind.ESNext && moduleKind !== ModuleKind.System) { + error(node, Diagnostics.The_import_meta_meta_property_is_only_allowed_when_the_module_option_is_esnext_or_system); + } + const file = getSourceFileOfNode(node); + Debug.assert(!!(file.flags & NodeFlags.PossiblyContainsImportMeta), "Containing file is missing import meta node flag."); + Debug.assert(!!file.externalModuleIndicator, "Containing file should be a module."); + return node.name.escapedText === "meta" ? getGlobalImportMetaType() : errorType; + } + function getTypeOfParameter(symbol: ts.Symbol) { + const type = getTypeOfSymbol(symbol); + if (strictNullChecks) { + const declaration = symbol.valueDeclaration; + if (declaration && hasInitializer(declaration)) { + return getOptionalType(type); } - - // Uninstantiated modules shouldnt do this check - if (isModuleDeclaration(node) && getModuleInstanceState(node) !== ModuleInstanceState.Instantiated) { - return; + } + return type; + } + function getParameterNameAtPosition(signature: ts.Signature, pos: number) { + const paramCount = signature.parameters.length - (signatureHasRestParameter(signature) ? 1 : 0); + if (pos < paramCount) { + return signature.parameters[pos].escapedName; + } + const restParameter = signature.parameters[paramCount] || unknownSymbol; + const restType = getTypeOfSymbol(restParameter); + if (isTupleType(restType)) { + const associatedNames = ((restType).target).associatedNames; + const index = pos - paramCount; + return associatedNames && associatedNames[index] || (restParameter.escapedName + "_" + index as __String); + } + return restParameter.escapedName; + } + function getTypeAtPosition(signature: ts.Signature, pos: number): ts.Type { + return tryGetTypeAtPosition(signature, pos) || anyType; + } + function tryGetTypeAtPosition(signature: ts.Signature, pos: number): ts.Type | undefined { + const paramCount = signature.parameters.length - (signatureHasRestParameter(signature) ? 1 : 0); + if (pos < paramCount) { + return getTypeOfParameter(signature.parameters[pos]); + } + if (signatureHasRestParameter(signature)) { + // We want to return the value undefined for an out of bounds parameter position, + // so we need to check bounds here before calling getIndexedAccessType (which + // otherwise would return the type 'undefined'). + const restType = getTypeOfSymbol(signature.parameters[paramCount]); + const index = pos - paramCount; + if (!isTupleType(restType) || restType.target.hasRestElement || index < getTypeArguments(restType).length) { + return getIndexedAccessType(restType, getLiteralType(index)); + } + } + return undefined; + } + function getRestTypeAtPosition(source: ts.Signature, pos: number): ts.Type { + const paramCount = getParameterCount(source); + const restType = getEffectiveRestType(source); + const nonRestCount = paramCount - (restType ? 1 : 0); + if (restType && pos === nonRestCount) { + return restType; + } + const types = []; + const names = []; + for (let i = pos; i < nonRestCount; i++) { + types.push(getTypeAtPosition(source, i)); + names.push(getParameterNameAtPosition(source, i)); + } + if (restType) { + types.push(getIndexedAccessType(restType, numberType)); + names.push(getParameterNameAtPosition(source, nonRestCount)); + } + const minArgumentCount = getMinArgumentCount(source); + const minLength = minArgumentCount < pos ? 0 : minArgumentCount - pos; + return createTupleType(types, minLength, !!restType, /*readonly*/ false, names); + } + function getParameterCount(signature: ts.Signature) { + const length = signature.parameters.length; + if (signatureHasRestParameter(signature)) { + const restType = getTypeOfSymbol(signature.parameters[length - 1]); + if (isTupleType(restType)) { + return length + getTypeArguments(restType).length - 1; } - - // In case of variable declaration, node.parent is variable statement so look at the variable statement's parent - const parent = getDeclarationContainer(node); - if (parent.kind === SyntaxKind.SourceFile && isExternalOrCommonJsModule(parent)) { - // If the declaration happens to be in external module, report error that require and exports are reserved keywords - error(name, Diagnostics.Duplicate_identifier_0_Compiler_reserves_name_1_in_top_level_scope_of_a_module, - declarationNameToString(name), declarationNameToString(name)); + } + return length; + } + function getMinArgumentCount(signature: ts.Signature) { + if (signatureHasRestParameter(signature)) { + const restType = getTypeOfSymbol(signature.parameters[signature.parameters.length - 1]); + if (isTupleType(restType)) { + const minLength = restType.target.minLength; + if (minLength > 0) { + return signature.parameters.length - 1 + minLength; + } } } - - function checkCollisionWithGlobalPromiseInGeneratedCode(node: Node, name: Identifier): void { - if (languageVersion >= ScriptTarget.ES2017 || compilerOptions.noEmit || !needCollisionCheckForIdentifier(node, name, "Promise")) { - return; + return signature.minArgumentCount; + } + function hasEffectiveRestParameter(signature: ts.Signature) { + if (signatureHasRestParameter(signature)) { + const restType = getTypeOfSymbol(signature.parameters[signature.parameters.length - 1]); + return !isTupleType(restType) || restType.target.hasRestElement; + } + return false; + } + function getEffectiveRestType(signature: ts.Signature) { + if (signatureHasRestParameter(signature)) { + const restType = getTypeOfSymbol(signature.parameters[signature.parameters.length - 1]); + return isTupleType(restType) ? getRestArrayTypeOfTupleType(restType) : restType; + } + return undefined; + } + function getNonArrayRestType(signature: ts.Signature) { + const restType = getEffectiveRestType(signature); + return restType && !isArrayType(restType) && !isTypeAny(restType) ? restType : undefined; + } + function getTypeOfFirstParameterOfSignature(signature: ts.Signature) { + return getTypeOfFirstParameterOfSignatureWithFallback(signature, neverType); + } + function getTypeOfFirstParameterOfSignatureWithFallback(signature: ts.Signature, fallbackType: ts.Type) { + return signature.parameters.length > 0 ? getTypeAtPosition(signature, 0) : fallbackType; + } + function inferFromAnnotatedParameters(signature: ts.Signature, context: ts.Signature, inferenceContext: InferenceContext) { + const len = signature.parameters.length - (signatureHasRestParameter(signature) ? 1 : 0); + for (let i = 0; i < len; i++) { + const declaration = (signature.parameters[i].valueDeclaration); + if (declaration.type) { + const typeNode = getEffectiveTypeAnnotationNode(declaration); + if (typeNode) { + inferTypes(inferenceContext.inferences, getTypeFromTypeNode(typeNode), getTypeAtPosition(context, i)); + } } - - // Uninstantiated modules shouldnt do this check - if (isModuleDeclaration(node) && getModuleInstanceState(node) !== ModuleInstanceState.Instantiated) { - return; + } + const restType = getEffectiveRestType(context); + if (restType && restType.flags & TypeFlags.TypeParameter) { + // The contextual signature has a generic rest parameter. We first instantiate the contextual + // signature (without fixing type parameters) and assign types to contextually typed parameters. + const instantiatedContext = instantiateSignature(context, inferenceContext.nonFixingMapper); + assignContextualParameterTypes(signature, instantiatedContext); + // We then infer from a tuple type representing the parameters that correspond to the contextual + // rest parameter. + const restPos = getParameterCount(context) - 1; + inferTypes(inferenceContext.inferences, getRestTypeAtPosition(signature, restPos), restType); + } + } + function assignContextualParameterTypes(signature: ts.Signature, context: ts.Signature) { + signature.typeParameters = context.typeParameters; + if (context.thisParameter) { + const parameter = signature.thisParameter; + if (!parameter || parameter.valueDeclaration && !(parameter.valueDeclaration).type) { + if (!parameter) { + signature.thisParameter = createSymbolWithType(context.thisParameter, /*type*/ undefined); + } + assignParameterType(signature.thisParameter!, getTypeOfSymbol(context.thisParameter)); } - - // In case of variable declaration, node.parent is variable statement so look at the variable statement's parent - const parent = getDeclarationContainer(node); - if (parent.kind === SyntaxKind.SourceFile && isExternalOrCommonJsModule(parent) && parent.flags & NodeFlags.HasAsyncFunctions) { - // If the declaration happens to be in external module, report error that Promise is a reserved identifier. - error(name, Diagnostics.Duplicate_identifier_0_Compiler_reserves_name_1_in_top_level_scope_of_a_module_containing_async_functions, - declarationNameToString(name), declarationNameToString(name)); + } + const len = signature.parameters.length - (signatureHasRestParameter(signature) ? 1 : 0); + for (let i = 0; i < len; i++) { + const parameter = signature.parameters[i]; + if (!getEffectiveTypeAnnotationNode((parameter.valueDeclaration))) { + const contextualParameterType = tryGetTypeAtPosition(context, i); + assignParameterType(parameter, contextualParameterType); } } - - function checkVarDeclaredNamesNotShadowed(node: VariableDeclaration | BindingElement) { - // - ScriptBody : StatementList - // It is a Syntax Error if any element of the LexicallyDeclaredNames of StatementList - // also occurs in the VarDeclaredNames of StatementList. - - // - Block : { StatementList } - // It is a Syntax Error if any element of the LexicallyDeclaredNames of StatementList - // also occurs in the VarDeclaredNames of StatementList. - - // Variable declarations are hoisted to the top of their function scope. They can shadow - // block scoped declarations, which bind tighter. this will not be flagged as duplicate definition - // by the binder as the declaration scope is different. - // A non-initialized declaration is a no-op as the block declaration will resolve before the var - // declaration. the problem is if the declaration has an initializer. this will act as a write to the - // block declared value. this is fine for let, but not const. - // Only consider declarations with initializers, uninitialized const declarations will not - // step on a let/const variable. - // Do not consider const and const declarations, as duplicate block-scoped declarations - // are handled by the binder. - // We are only looking for const declarations that step on let\const declarations from a - // different scope. e.g.: - // { - // const x = 0; // localDeclarationSymbol obtained after name resolution will correspond to this declaration - // const x = 0; // symbol for this declaration will be 'symbol' - // } - - // skip block-scoped variables and parameters - if ((getCombinedNodeFlags(node) & NodeFlags.BlockScoped) !== 0 || isParameterDeclaration(node)) { - return; + if (signatureHasRestParameter(signature)) { + // parameter might be a transient symbol generated by use of `arguments` in the function body. + const parameter = last(signature.parameters); + if (isTransientSymbol(parameter) || !getEffectiveTypeAnnotationNode((parameter.valueDeclaration))) { + const contextualParameterType = getRestTypeAtPosition(context, len); + assignParameterType(parameter, contextualParameterType); } - - // skip variable declarations that don't have initializers - // NOTE: in ES6 spec initializer is required in variable declarations where name is binding pattern - // so we'll always treat binding elements as initialized - if (node.kind === SyntaxKind.VariableDeclaration && !node.initializer) { - return; + } + } + function assignNonContextualParameterTypes(signature: ts.Signature) { + if (signature.thisParameter) { + assignParameterType(signature.thisParameter); + } + for (const parameter of signature.parameters) { + assignParameterType(parameter); + } + } + function assignParameterType(parameter: ts.Symbol, type?: ts.Type) { + const links = getSymbolLinks(parameter); + if (!links.type) { + const declaration = (parameter.valueDeclaration as ParameterDeclaration); + links.type = type || getWidenedTypeForVariableLikeDeclaration(declaration, /*includeOptionality*/ true); + if (declaration.name.kind !== SyntaxKind.Identifier) { + // if inference didn't come up with anything but unknown, fall back to the binding pattern if present. + if (links.type === unknownType) { + links.type = getTypeFromBindingPattern(declaration.name); + } + assignBindingElementTypes(declaration.name); } - - const symbol = getSymbolOfNode(node); - if (symbol.flags & SymbolFlags.FunctionScopedVariable) { - if (!isIdentifier(node.name)) return Debug.fail(); - const localDeclarationSymbol = resolveName(node, node.name.escapedText, SymbolFlags.Variable, /*nodeNotFoundErrorMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false); - if (localDeclarationSymbol && - localDeclarationSymbol !== symbol && - localDeclarationSymbol.flags & SymbolFlags.BlockScopedVariable) { - if (getDeclarationNodeFlagsFromSymbol(localDeclarationSymbol) & NodeFlags.BlockScoped) { - const varDeclList = getAncestor(localDeclarationSymbol.valueDeclaration, SyntaxKind.VariableDeclarationList)!; - const container = - varDeclList.parent.kind === SyntaxKind.VariableStatement && varDeclList.parent.parent - ? varDeclList.parent.parent - : undefined; - - // names of block-scoped and function scoped variables can collide only - // if block scoped variable is defined in the function\module\source file scope (because of variable hoisting) - const namesShareScope = - container && - (container.kind === SyntaxKind.Block && isFunctionLike(container.parent) || - container.kind === SyntaxKind.ModuleBlock || - container.kind === SyntaxKind.ModuleDeclaration || - container.kind === SyntaxKind.SourceFile); - - // here we know that function scoped variable is shadowed by block scoped one - // if they are defined in the same scope - binder has already reported redeclaration error - // otherwise if variable has an initializer - show error that initialization will fail - // since LHS will be block scoped name instead of function scoped - if (!namesShareScope) { - const name = symbolToString(localDeclarationSymbol); - error(node, Diagnostics.Cannot_initialize_outer_scoped_variable_0_in_the_same_scope_as_block_scoped_declaration_1, name, name); - } - } + } + } + // When contextual typing assigns a type to a parameter that contains a binding pattern, we also need to push + // the destructured type into the contained binding elements. + function assignBindingElementTypes(pattern: BindingPattern) { + for (const element of pattern.elements) { + if (!isOmittedExpression(element)) { + if (element.name.kind === SyntaxKind.Identifier) { + getSymbolLinks(getSymbolOfNode(element)).type = getTypeForBindingElement(element); + } + else { + assignBindingElementTypes(element.name); } } } - - function convertAutoToAny(type: Type) { - return type === autoType ? anyType : type === autoArrayType ? anyArrayType : type; + } + function createPromiseType(promisedType: ts.Type): ts.Type { + // creates a `Promise` type where `T` is the promisedType argument + const globalPromiseType = getGlobalPromiseType(/*reportErrors*/ true); + if (globalPromiseType !== emptyGenericType) { + // if the promised type is itself a promise, get the underlying type; otherwise, fallback to the promised type + promisedType = getAwaitedType(promisedType) || unknownType; + return createTypeReference(globalPromiseType, [promisedType]); + } + return unknownType; + } + function createPromiseLikeType(promisedType: ts.Type): ts.Type { + // creates a `PromiseLike` type where `T` is the promisedType argument + const globalPromiseLikeType = getGlobalPromiseLikeType(/*reportErrors*/ true); + if (globalPromiseLikeType !== emptyGenericType) { + // if the promised type is itself a promise, get the underlying type; otherwise, fallback to the promised type + promisedType = getAwaitedType(promisedType) || unknownType; + return createTypeReference(globalPromiseLikeType, [promisedType]); + } + return unknownType; + } + function createPromiseReturnType(func: FunctionLikeDeclaration | ImportCall, promisedType: ts.Type) { + const promiseType = createPromiseType(promisedType); + if (promiseType === unknownType) { + error(func, isImportCall(func) ? + Diagnostics.A_dynamic_import_call_returns_a_Promise_Make_sure_you_have_a_declaration_for_Promise_or_include_ES2015_in_your_lib_option : + Diagnostics.An_async_function_or_method_must_return_a_Promise_Make_sure_you_have_a_declaration_for_Promise_or_include_ES2015_in_your_lib_option); + return errorType; } - - // Check variable, parameter, or property declaration - function checkVariableLikeDeclaration(node: ParameterDeclaration | PropertyDeclaration | PropertySignature | VariableDeclaration | BindingElement) { - checkDecorators(node); - if (!isBindingElement(node)) { - checkSourceElement(node.type); + else if (!getGlobalPromiseConstructorSymbol(/*reportErrors*/ true)) { + error(func, isImportCall(func) ? + Diagnostics.A_dynamic_import_call_in_ES5_SlashES3_requires_the_Promise_constructor_Make_sure_you_have_a_declaration_for_the_Promise_constructor_or_include_ES2015_in_your_lib_option : + Diagnostics.An_async_function_or_method_in_ES5_SlashES3_requires_the_Promise_constructor_Make_sure_you_have_a_declaration_for_the_Promise_constructor_or_include_ES2015_in_your_lib_option); + } + return promiseType; + } + function getReturnTypeFromBody(func: FunctionLikeDeclaration, checkMode?: CheckMode): ts.Type { + if (!func.body) { + return errorType; + } + const functionFlags = getFunctionFlags(func); + const isAsync = (functionFlags & FunctionFlags.Async) !== 0; + const isGenerator = (functionFlags & FunctionFlags.Generator) !== 0; + let returnType: ts.Type | undefined; + let yieldType: ts.Type | undefined; + let nextType: ts.Type | undefined; + let fallbackReturnType: ts.Type = voidType; + if (func.body.kind !== SyntaxKind.Block) { // Async or normal arrow function + returnType = checkExpressionCached(func.body, checkMode && checkMode & ~CheckMode.SkipGenericFunctions); + if (isAsync) { + // From within an async function you can return either a non-promise value or a promise. Any + // Promise/A+ compatible implementation will always assimilate any foreign promise, so the + // return type of the body should be unwrapped to its awaited type, which we will wrap in + // the native Promise type later in this function. + returnType = checkAwaitedType(returnType, /*errorNode*/ func, Diagnostics.The_return_type_of_an_async_function_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member); } - - // JSDoc `function(string, string): string` syntax results in parameters with no name - if (!node.name) { - return; + } + else if (isGenerator) { // Generator or AsyncGenerator function + const returnTypes = checkAndAggregateReturnExpressionTypes(func, checkMode); + if (!returnTypes) { + fallbackReturnType = neverType; } - // For a computed property, just check the initializer and exit - // Do not use hasDynamicName here, because that returns false for well known symbols. - // We want to perform checkComputedPropertyName for all computed properties, including - // well known symbols. - if (node.name.kind === SyntaxKind.ComputedPropertyName) { - checkComputedPropertyName(node.name); - if (node.initializer) { - checkExpressionCached(node.initializer); - } + else if (returnTypes.length > 0) { + returnType = getUnionType(returnTypes, UnionReduction.Subtype); } - - if (node.kind === SyntaxKind.BindingElement) { - if (node.parent.kind === SyntaxKind.ObjectBindingPattern && languageVersion < ScriptTarget.ESNext) { - checkExternalEmitHelpers(node, ExternalEmitHelpers.Rest); - } - // check computed properties inside property names of binding elements - if (node.propertyName && node.propertyName.kind === SyntaxKind.ComputedPropertyName) { - checkComputedPropertyName(node.propertyName); + const { yieldTypes, nextTypes } = checkAndAggregateYieldOperandTypes(func, checkMode); + yieldType = some(yieldTypes) ? getUnionType(yieldTypes, UnionReduction.Subtype) : undefined; + nextType = some(nextTypes) ? getIntersectionType(nextTypes) : undefined; + } + else { // Async or normal function + const types = checkAndAggregateReturnExpressionTypes(func, checkMode); + if (!types) { + // For an async function, the return type will not be never, but rather a Promise for never. + return functionFlags & FunctionFlags.Async + ? createPromiseReturnType(func, neverType) // Async function + : neverType; // Normal function + } + if (types.length === 0) { + // For an async function, the return type will not be void, but rather a Promise for void. + return functionFlags & FunctionFlags.Async + ? createPromiseReturnType(func, voidType) // Async function + : voidType; // Normal function + } + // Return a union of the return expression types. + returnType = getUnionType(types, UnionReduction.Subtype); + } + if (returnType || yieldType || nextType) { + const contextualSignature = getContextualSignatureForFunctionLikeDeclaration(func); + if (!contextualSignature) { + if (yieldType) + reportErrorsFromWidening(func, yieldType, WideningKind.GeneratorYield); + if (returnType) + reportErrorsFromWidening(func, returnType); + if (nextType) + reportErrorsFromWidening(func, nextType); + } + if (returnType && isUnitType(returnType) || + yieldType && isUnitType(yieldType) || + nextType && isUnitType(nextType)) { + const contextualType = !contextualSignature ? undefined : + contextualSignature === getSignatureFromDeclaration(func) ? isGenerator ? undefined : returnType : + instantiateContextualType(getReturnTypeOfSignature(contextualSignature), func); + if (isGenerator) { + yieldType = getWidenedLiteralLikeTypeForContextualIterationTypeIfNeeded(yieldType, contextualType, IterationTypeKind.Yield, isAsync); + returnType = getWidenedLiteralLikeTypeForContextualIterationTypeIfNeeded(returnType, contextualType, IterationTypeKind.Return, isAsync); + nextType = getWidenedLiteralLikeTypeForContextualIterationTypeIfNeeded(nextType, contextualType, IterationTypeKind.Next, isAsync); } - - // check private/protected variable access - const parent = node.parent.parent; - const parentType = getTypeForBindingElementParent(parent); - const name = node.propertyName || node.name; - if (parentType && !isBindingPattern(name)) { - const exprType = getLiteralTypeFromPropertyName(name); - if (isTypeUsableAsPropertyName(exprType)) { - const nameText = getPropertyNameFromType(exprType); - const property = getPropertyOfType(parentType, nameText); - if (property) { - markPropertyAsReferenced(property, /*nodeForCheckWriteOnly*/ undefined, /*isThisAccess*/ false); // A destructuring is never a write-only reference. - checkPropertyAccessibility(parent, !!parent.initializer && parent.initializer.kind === SyntaxKind.SuperKeyword, parentType, property); - } - } + else { + returnType = getWidenedLiteralLikeTypeForContextualReturnTypeIfNeeded(returnType, contextualType, isAsync); } } - - // For a binding pattern, check contained binding elements - if (isBindingPattern(node.name)) { - if (node.name.kind === SyntaxKind.ArrayBindingPattern && languageVersion < ScriptTarget.ES2015 && compilerOptions.downlevelIteration) { - checkExternalEmitHelpers(node, ExternalEmitHelpers.Read); - } - - forEach(node.name.elements, checkSourceElement); + if (yieldType) + yieldType = getWidenedType(yieldType); + if (returnType) + returnType = getWidenedType(returnType); + if (nextType) + nextType = getWidenedType(nextType); + } + if (isGenerator) { + return createGeneratorReturnType(yieldType || neverType, returnType || fallbackReturnType, nextType || getContextualIterationType(IterationTypeKind.Next, func) || unknownType, isAsync); + } + else { + // From within an async function you can return either a non-promise value or a promise. Any + // Promise/A+ compatible implementation will always assimilate any foreign promise, so the + // return type of the body is awaited type of the body, wrapped in a native Promise type. + return isAsync + ? createPromiseType(returnType || fallbackReturnType) + : returnType || fallbackReturnType; + } + } + function createGeneratorReturnType(yieldType: ts.Type, returnType: ts.Type, nextType: ts.Type, isAsyncGenerator: boolean) { + const resolver = isAsyncGenerator ? asyncIterationTypesResolver : syncIterationTypesResolver; + const globalGeneratorType = resolver.getGlobalGeneratorType(/*reportErrors*/ false); + yieldType = resolver.resolveIterationType(yieldType, /*errorNode*/ undefined) || unknownType; + returnType = resolver.resolveIterationType(returnType, /*errorNode*/ undefined) || unknownType; + nextType = resolver.resolveIterationType(nextType, /*errorNode*/ undefined) || unknownType; + if (globalGeneratorType === emptyGenericType) { + // Fall back to the global IterableIterator if returnType is assignable to the expected return iteration + // type of IterableIterator, and the expected next iteration type of IterableIterator is assignable to + // nextType. + const globalType = resolver.getGlobalIterableIteratorType(/*reportErrors*/ false); + const iterationTypes = globalType !== emptyGenericType ? getIterationTypesOfGlobalIterableType(globalType, resolver) : undefined; + const iterableIteratorReturnType = iterationTypes ? iterationTypes.returnType : anyType; + const iterableIteratorNextType = iterationTypes ? iterationTypes.nextType : undefinedType; + if (isTypeAssignableTo(returnType, iterableIteratorReturnType) && + isTypeAssignableTo(iterableIteratorNextType, nextType)) { + if (globalType !== emptyGenericType) { + return createTypeFromGenericGlobalType(globalType, [yieldType]); + } + // The global IterableIterator type doesn't exist, so report an error + resolver.getGlobalIterableIteratorType(/*reportErrors*/ true); + return emptyObjectType; } - // For a parameter declaration with an initializer, error and exit if the containing function doesn't have a body - if (node.initializer && getRootDeclaration(node).kind === SyntaxKind.Parameter && nodeIsMissing((getContainingFunction(node) as FunctionLikeDeclaration).body)) { - error(node, Diagnostics.A_parameter_initializer_is_only_allowed_in_a_function_or_constructor_implementation); - return; + // The global Generator type doesn't exist, so report an error + resolver.getGlobalGeneratorType(/*reportErrors*/ true); + return emptyObjectType; + } + return createTypeFromGenericGlobalType(globalGeneratorType, [yieldType, returnType, nextType]); + } + function checkAndAggregateYieldOperandTypes(func: FunctionLikeDeclaration, checkMode: CheckMode | undefined) { + const yieldTypes: ts.Type[] = []; + const nextTypes: ts.Type[] = []; + const isAsync = (getFunctionFlags(func) & FunctionFlags.Async) !== 0; + forEachYieldExpression((func.body), yieldExpression => { + const yieldExpressionType = yieldExpression.expression ? checkExpression(yieldExpression.expression, checkMode) : undefinedWideningType; + pushIfUnique(yieldTypes, getYieldedTypeOfYieldExpression(yieldExpression, yieldExpressionType, anyType, isAsync)); + let nextType: ts.Type | undefined; + if (yieldExpression.asteriskToken) { + const iterationTypes = getIterationTypesOfIterable(yieldExpressionType, isAsync ? IterationUse.AsyncYieldStar : IterationUse.YieldStar, yieldExpression.expression); + nextType = iterationTypes && iterationTypes.nextType; } - // For a binding pattern, validate the initializer and exit - if (isBindingPattern(node.name)) { - const needCheckInitializer = node.initializer && node.parent.parent.kind !== SyntaxKind.ForInStatement; - const needCheckWidenedType = node.name.elements.length === 0; - if (needCheckInitializer || needCheckWidenedType) { - // Don't validate for-in initializer as it is already an error - const widenedType = getWidenedTypeForVariableLikeDeclaration(node); - if (needCheckInitializer) { - const initializerType = checkExpressionCached(node.initializer!); - if (strictNullChecks && needCheckWidenedType) { - checkNonNullNonVoidType(initializerType, node); - } - else { - checkTypeAssignableToAndOptionallyElaborate(initializerType, getWidenedTypeForVariableLikeDeclaration(node), node, node.initializer); - } - } - // check the binding pattern with empty elements - if (needCheckWidenedType) { - if (isArrayBindingPattern(node.name)) { - checkIteratedTypeOrElementType(IterationUse.Destructuring, widenedType, undefinedType, node); - } - else if (strictNullChecks) { - checkNonNullNonVoidType(widenedType, node); - } - } - } - return; + else { + nextType = getContextualType(yieldExpression); } - const symbol = getSymbolOfNode(node); - const type = convertAutoToAny(getTypeOfSymbol(symbol)); - if (node === symbol.valueDeclaration) { - // Node is the primary declaration of the symbol, just validate the initializer - // Don't validate for-in initializer as it is already an error - const initializer = getEffectiveInitializer(node); - if (initializer) { - const isJSObjectLiteralInitializer = isInJSFile(node) && - isObjectLiteralExpression(initializer) && - (initializer.properties.length === 0 || isPrototypeAccess(node.name)) && - hasEntries(symbol.exports); - if (!isJSObjectLiteralInitializer && node.parent.parent.kind !== SyntaxKind.ForInStatement) { - checkTypeAssignableToAndOptionallyElaborate(checkExpressionCached(initializer), type, node, initializer, /*headMessage*/ undefined); - } + if (nextType) + pushIfUnique(nextTypes, nextType); + }); + return { yieldTypes, nextTypes }; + } + function getYieldedTypeOfYieldExpression(node: YieldExpression, expressionType: ts.Type, sentType: ts.Type, isAsync: boolean): ts.Type | undefined { + const errorNode = node.expression || node; + // A `yield*` expression effectively yields everything that its operand yields + const yieldedType = node.asteriskToken ? checkIteratedTypeOrElementType(isAsync ? IterationUse.AsyncYieldStar : IterationUse.YieldStar, expressionType, sentType, errorNode) : expressionType; + return !isAsync ? yieldedType : getAwaitedType(yieldedType, errorNode, node.asteriskToken + ? Diagnostics.Type_of_iterated_elements_of_a_yield_Asterisk_operand_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member + : Diagnostics.Type_of_yield_operand_in_an_async_generator_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member); + } + /** + * Collect the TypeFacts learned from a typeof switch with + * total clauses `witnesses`, and the active clause ranging + * from `start` to `end`. Parameter `hasDefault` denotes + * whether the active clause contains a default clause. + */ + function getFactsFromTypeofSwitch(start: number, end: number, witnesses: string[], hasDefault: boolean): TypeFacts { + let facts: TypeFacts = TypeFacts.None; + // When in the default we only collect inequality facts + // because default is 'in theory' a set of infinite + // equalities. + if (hasDefault) { + // Value is not equal to any types after the active clause. + for (let i = end; i < witnesses.length; i++) { + facts |= typeofNEFacts.get(witnesses[i]) || TypeFacts.TypeofNEHostObject; + } + // Remove inequalities for types that appear in the + // active clause because they appear before other + // types collected so far. + for (let i = start; i < end; i++) { + facts &= ~(typeofNEFacts.get(witnesses[i]) || 0); + } + // Add inequalities for types before the active clause unconditionally. + for (let i = 0; i < start; i++) { + facts |= typeofNEFacts.get(witnesses[i]) || TypeFacts.TypeofNEHostObject; + } + } + // When in an active clause without default the set of + // equalities is finite. + else { + // Add equalities for all types in the active clause. + for (let i = start; i < end; i++) { + facts |= typeofEQFacts.get(witnesses[i]) || TypeFacts.TypeofEQHostObject; + } + // Remove equalities for types that appear before the + // active clause. + for (let i = 0; i < start; i++) { + facts &= ~(typeofEQFacts.get(witnesses[i]) || 0); + } + } + return facts; + } + function isExhaustiveSwitchStatement(node: SwitchStatement): boolean { + const links = getNodeLinks(node); + return links.isExhaustive !== undefined ? links.isExhaustive : (links.isExhaustive = computeExhaustiveSwitchStatement(node)); + } + function computeExhaustiveSwitchStatement(node: SwitchStatement): boolean { + if (node.expression.kind === SyntaxKind.TypeOfExpression) { + const operandType = getTypeOfExpression((node.expression as TypeOfExpression).expression); + // This cast is safe because the switch is possibly exhaustive and does not contain a default case, so there can be no undefined. + const witnesses = getSwitchClauseTypeOfWitnesses(node); + // notEqualFacts states that the type of the switched value is not equal to every type in the switch. + const notEqualFacts = getFactsFromTypeofSwitch(0, 0, witnesses, /*hasDefault*/ true); + const type = getBaseConstraintOfType(operandType) || operandType; + return !!(filterType(type, t => (getTypeFacts(t) & notEqualFacts) === notEqualFacts).flags & TypeFlags.Never); + } + const type = getTypeOfExpression(node.expression); + if (!isLiteralType(type)) { + return false; + } + const switchTypes = getSwitchClauseTypes(node); + if (!switchTypes.length || some(switchTypes, isNeitherUnitTypeNorNever)) { + return false; + } + return eachTypeContainedIn(mapType(type, getRegularTypeOfLiteralType), switchTypes); + } + function functionHasImplicitReturn(func: FunctionLikeDeclaration) { + return func.endFlowNode && isReachableFlowNode(func.endFlowNode); + } + /** NOTE: Return value of `[]` means a different thing than `undefined`. `[]` means func returns `void`, `undefined` means it returns `never`. */ + function checkAndAggregateReturnExpressionTypes(func: FunctionLikeDeclaration, checkMode: CheckMode | undefined): ts.Type[] | undefined { + const functionFlags = getFunctionFlags(func); + const aggregatedTypes: ts.Type[] = []; + let hasReturnWithNoExpression = functionHasImplicitReturn(func); + let hasReturnOfTypeNever = false; + forEachReturnStatement((func.body), returnStatement => { + const expr = returnStatement.expression; + if (expr) { + let type = checkExpressionCached(expr, checkMode && checkMode & ~CheckMode.SkipGenericFunctions); + if (functionFlags & FunctionFlags.Async) { + // From within an async function you can return either a non-promise value or a promise. Any + // Promise/A+ compatible implementation will always assimilate any foreign promise, so the + // return type of the body should be unwrapped to its awaited type, which should be wrapped in + // the native Promise type by the caller. + type = checkAwaitedType(type, func, Diagnostics.The_return_type_of_an_async_function_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member); } - if (symbol.declarations.length > 1) { - if (some(symbol.declarations, d => d !== node && isVariableLike(d) && !areDeclarationFlagsIdentical(d, node))) { - error(node.name, Diagnostics.All_declarations_of_0_must_have_identical_modifiers, declarationNameToString(node.name)); - } + if (type.flags & TypeFlags.Never) { + hasReturnOfTypeNever = true; } + pushIfUnique(aggregatedTypes, type); } else { - // Node is a secondary declaration, check that type is identical to primary declaration and check that - // initializer is consistent with type associated with the node - const declarationType = convertAutoToAny(getWidenedTypeForVariableLikeDeclaration(node)); - - if (type !== errorType && declarationType !== errorType && - !isTypeIdenticalTo(type, declarationType) && - !(symbol.flags & SymbolFlags.Assignment)) { - errorNextVariableOrPropertyDeclarationMustHaveSameType(symbol.valueDeclaration, type, node, declarationType); - } - if (node.initializer) { - checkTypeAssignableToAndOptionallyElaborate(checkExpressionCached(node.initializer), declarationType, node, node.initializer, /*headMessage*/ undefined); - } - if (!areDeclarationFlagsIdentical(node, symbol.valueDeclaration)) { - error(node.name, Diagnostics.All_declarations_of_0_must_have_identical_modifiers, declarationNameToString(node.name)); - } - } - if (node.kind !== SyntaxKind.PropertyDeclaration && node.kind !== SyntaxKind.PropertySignature) { - // We know we don't have a binding pattern or computed name here - checkExportsOnMergedDeclarations(node); - if (node.kind === SyntaxKind.VariableDeclaration || node.kind === SyntaxKind.BindingElement) { - checkVarDeclaredNamesNotShadowed(node); - } - checkCollisionWithRequireExportsInGeneratedCode(node, node.name); - checkCollisionWithGlobalPromiseInGeneratedCode(node, node.name); - if (!compilerOptions.noEmit && languageVersion < ScriptTarget.ESNext && needCollisionCheckForIdentifier(node, node.name, "WeakMap")) { - potentialWeakMapCollisions.push(node); - } + hasReturnWithNoExpression = true; } + }); + if (aggregatedTypes.length === 0 && !hasReturnWithNoExpression && (hasReturnOfTypeNever || mayReturnNever(func))) { + return undefined; } - - function errorNextVariableOrPropertyDeclarationMustHaveSameType(firstDeclaration: Declaration | undefined, firstType: Type, nextDeclaration: Declaration, nextType: Type): void { - const nextDeclarationName = getNameOfDeclaration(nextDeclaration); - const message = nextDeclaration.kind === SyntaxKind.PropertyDeclaration || nextDeclaration.kind === SyntaxKind.PropertySignature - ? Diagnostics.Subsequent_property_declarations_must_have_the_same_type_Property_0_must_be_of_type_1_but_here_has_type_2 - : Diagnostics.Subsequent_variable_declarations_must_have_the_same_type_Variable_0_must_be_of_type_1_but_here_has_type_2; - const declName = declarationNameToString(nextDeclarationName); - const err = error( - nextDeclarationName, - message, - declName, - typeToString(firstType), - typeToString(nextType) - ); - if (firstDeclaration) { - addRelatedInfo(err, - createDiagnosticForNode(firstDeclaration, Diagnostics._0_was_also_declared_here, declName) - ); - } + if (strictNullChecks && aggregatedTypes.length && hasReturnWithNoExpression && + !(isJSConstructor(func) && aggregatedTypes.some(t => t.symbol === func.symbol))) { + // Javascript "callable constructors", containing eg `if (!(this instanceof A)) return new A()` should not add undefined + pushIfUnique(aggregatedTypes, undefinedType); } - - function areDeclarationFlagsIdentical(left: Declaration, right: Declaration) { - if ((left.kind === SyntaxKind.Parameter && right.kind === SyntaxKind.VariableDeclaration) || - (left.kind === SyntaxKind.VariableDeclaration && right.kind === SyntaxKind.Parameter)) { - // Differences in optionality between parameters and variables are allowed. + return aggregatedTypes; + } + function mayReturnNever(func: FunctionLikeDeclaration): boolean { + switch (func.kind) { + case SyntaxKind.FunctionExpression: + case SyntaxKind.ArrowFunction: return true; - } - - if (hasQuestionToken(left) !== hasQuestionToken(right)) { + case SyntaxKind.MethodDeclaration: + return func.parent.kind === SyntaxKind.ObjectLiteralExpression; + default: return false; - } - - const interestingFlags = ModifierFlags.Private | - ModifierFlags.Protected | - ModifierFlags.Async | - ModifierFlags.Abstract | - ModifierFlags.Readonly | - ModifierFlags.Static; - - return getSelectedModifierFlags(left, interestingFlags) === getSelectedModifierFlags(right, interestingFlags); - } - - function checkVariableDeclaration(node: VariableDeclaration) { - checkGrammarVariableDeclaration(node); - return checkVariableLikeDeclaration(node); } - - function checkBindingElement(node: BindingElement) { - checkGrammarBindingElement(node); - return checkVariableLikeDeclaration(node); + } + /** + * TypeScript Specification 1.0 (6.3) - July 2014 + * An explicitly typed function whose return type isn't the Void type, + * the Any type, or a union type containing the Void or Any type as a constituent + * must have at least one return statement somewhere in its body. + * An exception to this rule is if the function implementation consists of a single 'throw' statement. + * + * @param returnType - return type of the function, can be undefined if return type is not explicitly specified + */ + function checkAllCodePathsInNonVoidFunctionReturnOrThrow(func: FunctionLikeDeclaration | MethodSignature, returnType: ts.Type | undefined): void { + if (!produceDiagnostics) { + return; + } + const functionFlags = getFunctionFlags(func); + const type = returnType && getReturnOrPromisedType(returnType, functionFlags); + // Functions with with an explicitly specified 'void' or 'any' return type don't need any return expressions. + if (type && maybeTypeOfKind(type, TypeFlags.Any | TypeFlags.Void)) { + return; + } + // If all we have is a function signature, or an arrow function with an expression body, then there is nothing to check. + // also if HasImplicitReturn flag is not set this means that all codepaths in function body end with return or throw + if (func.kind === SyntaxKind.MethodSignature || nodeIsMissing(func.body) || func.body!.kind !== SyntaxKind.Block || !functionHasImplicitReturn(func)) { + return; + } + const hasExplicitReturn = func.flags & NodeFlags.HasExplicitReturn; + if (type && type.flags & TypeFlags.Never) { + error(getEffectiveReturnTypeNode(func), Diagnostics.A_function_returning_never_cannot_have_a_reachable_end_point); + } + else if (type && !hasExplicitReturn) { + // minimal check: function has syntactic return type annotation and no explicit return statements in the body + // this function does not conform to the specification. + // NOTE: having returnType !== undefined is a precondition for entering this branch so func.type will always be present + error(getEffectiveReturnTypeNode(func), Diagnostics.A_function_whose_declared_type_is_neither_void_nor_any_must_return_a_value); + } + else if (type && strictNullChecks && !isTypeAssignableTo(undefinedType, type)) { + error(getEffectiveReturnTypeNode(func) || func, Diagnostics.Function_lacks_ending_return_statement_and_return_type_does_not_include_undefined); + } + else if (compilerOptions.noImplicitReturns) { + if (!type) { + // If return type annotation is omitted check if function has any explicit return statements. + // If it does not have any - its inferred return type is void - don't do any checks. + // Otherwise get inferred return type from function body and report error only if it is not void / anytype + if (!hasExplicitReturn) { + return; + } + const inferredReturnType = getReturnTypeOfSignature(getSignatureFromDeclaration(func)); + if (isUnwrappedReturnTypeVoidOrAny(func, inferredReturnType)) { + return; + } + } + error(getEffectiveReturnTypeNode(func) || func, Diagnostics.Not_all_code_paths_return_a_value); } - - function checkVariableStatement(node: VariableStatement) { - // Grammar checking - if (!checkGrammarDecoratorsAndModifiers(node) && !checkGrammarVariableDeclarationList(node.declarationList)) checkGrammarForDisallowedLetOrConstStatement(node); - forEach(node.declarationList.declarations, checkSourceElement); + } + function checkFunctionExpressionOrObjectLiteralMethod(node: FunctionExpression | ArrowFunction | MethodDeclaration, checkMode?: CheckMode): ts.Type { + Debug.assert(node.kind !== SyntaxKind.MethodDeclaration || isObjectLiteralMethod(node)); + checkNodeDeferred(node); + // The identityMapper object is used to indicate that function expressions are wildcards + if (checkMode && checkMode & CheckMode.SkipContextSensitive && isContextSensitive(node)) { + // Skip parameters, return signature with return type that retains noncontextual parts so inferences can still be drawn in an early stage + if (!getEffectiveReturnTypeNode(node) && !hasContextSensitiveParameters(node)) { + // Return plain anyFunctionType if there is no possibility we'll make inferences from the return type + const contextualSignature = getContextualSignature(node); + if (contextualSignature && couldContainTypeVariables(getReturnTypeOfSignature(contextualSignature))) { + const links = getNodeLinks(node); + if (links.contextFreeType) { + return links.contextFreeType; + } + const returnType = getReturnTypeFromBody(node, checkMode); + const returnOnlySignature = createSignature(undefined, undefined, undefined, emptyArray, returnType, /*resolvedTypePredicate*/ undefined, 0, SignatureFlags.None); + const returnOnlyType = createAnonymousType(node.symbol, emptySymbols, [returnOnlySignature], emptyArray, undefined, undefined); + returnOnlyType.objectFlags |= ObjectFlags.NonInferrableType; + return links.contextFreeType = returnOnlyType; + } + } + return anyFunctionType; } - - function checkExpressionStatement(node: ExpressionStatement) { - // Grammar checking - checkGrammarStatementInAmbientContext(node); - - checkExpression(node.expression); + // Grammar checking + const hasGrammarError = checkGrammarFunctionLikeDeclaration(node); + if (!hasGrammarError && node.kind === SyntaxKind.FunctionExpression) { + checkGrammarForGenerator(node); } - - function checkIfStatement(node: IfStatement) { - // Grammar checking - checkGrammarStatementInAmbientContext(node); - const type = checkTruthinessExpression(node.expression); - checkTestingKnownTruthyCallableType(node.expression, node.thenStatement, type); - checkSourceElement(node.thenStatement); - - if (node.thenStatement.kind === SyntaxKind.EmptyStatement) { - error(node.thenStatement, Diagnostics.The_body_of_an_if_statement_cannot_be_the_empty_statement); + contextuallyCheckFunctionExpressionOrObjectLiteralMethod(node, checkMode); + return getTypeOfSymbol(getSymbolOfNode(node)); + } + function contextuallyCheckFunctionExpressionOrObjectLiteralMethod(node: FunctionExpression | ArrowFunction | MethodDeclaration, checkMode?: CheckMode) { + const links = getNodeLinks(node); + // Check if function expression is contextually typed and assign parameter types if so. + if (!(links.flags & NodeCheckFlags.ContextChecked)) { + const contextualSignature = getContextualSignature(node); + // If a type check is started at a function expression that is an argument of a function call, obtaining the + // contextual type may recursively get back to here during overload resolution of the call. If so, we will have + // already assigned contextual types. + if (!(links.flags & NodeCheckFlags.ContextChecked)) { + links.flags |= NodeCheckFlags.ContextChecked; + const signature = firstOrUndefined(getSignaturesOfType(getTypeOfSymbol(getSymbolOfNode(node)), SignatureKind.Call)); + if (!signature) { + return; + } + if (isContextSensitive(node)) { + if (contextualSignature) { + const inferenceContext = getInferenceContext(node); + if (checkMode && checkMode & CheckMode.Inferential) { + inferFromAnnotatedParameters(signature, contextualSignature, inferenceContext!); + } + const instantiatedContextualSignature = inferenceContext ? + instantiateSignature(contextualSignature, inferenceContext.mapper) : contextualSignature; + assignContextualParameterTypes(signature, instantiatedContextualSignature); + } + else { + // Force resolution of all parameter types such that the absence of a contextual type is consistently reflected. + assignNonContextualParameterTypes(signature); + } + } + if (contextualSignature && !getReturnTypeFromAnnotation(node) && !signature.resolvedReturnType) { + const returnType = getReturnTypeFromBody(node, checkMode); + if (!signature.resolvedReturnType) { + signature.resolvedReturnType = returnType; + } + } + checkSignatureDeclaration(node); } - - checkSourceElement(node.elseStatement); } - - function checkTestingKnownTruthyCallableType(condExpr: Expression, body: Statement | Expression, type: Type) { - if (!strictNullChecks) { - return; - } - - const testedNode = isIdentifier(condExpr) - ? condExpr - : isPropertyAccessExpression(condExpr) - ? condExpr.name - : undefined; - - if (!testedNode) { - return; - } - - const possiblyFalsy = getFalsyFlags(type); - if (possiblyFalsy) { - return; - } - - // While it technically should be invalid for any known-truthy value - // to be tested, we de-scope to functions unrefenced in the block as a - // heuristic to identify the most common bugs. There are too many - // false positives for values sourced from type definitions without - // strictNullChecks otherwise. - const callSignatures = getSignaturesOfType(type, SignatureKind.Call); - if (callSignatures.length === 0) { - return; - } - - const testedFunctionSymbol = getSymbolAtLocation(testedNode); - if (!testedFunctionSymbol) { - return; + } + function getReturnOrPromisedType(type: ts.Type | undefined, functionFlags: FunctionFlags) { + const isGenerator = !!(functionFlags & FunctionFlags.Generator); + const isAsync = !!(functionFlags & FunctionFlags.Async); + return type && isGenerator ? getIterationTypeOfGeneratorFunctionReturnType(IterationTypeKind.Return, type, isAsync) || errorType : + type && isAsync ? getAwaitedType(type) || errorType : + type; + } + function checkFunctionExpressionOrObjectLiteralMethodDeferred(node: ArrowFunction | FunctionExpression | MethodDeclaration) { + Debug.assert(node.kind !== SyntaxKind.MethodDeclaration || isObjectLiteralMethod(node)); + const functionFlags = getFunctionFlags(node); + const returnType = getReturnTypeFromAnnotation(node); + checkAllCodePathsInNonVoidFunctionReturnOrThrow(node, returnType); + if (node.body) { + if (!getEffectiveReturnTypeNode(node)) { + // There are some checks that are only performed in getReturnTypeFromBody, that may produce errors + // we need. An example is the noImplicitAny errors resulting from widening the return expression + // of a function. Because checking of function expression bodies is deferred, there was never an + // appropriate time to do this during the main walk of the file (see the comment at the top of + // checkFunctionExpressionBodies). So it must be done now. + getReturnTypeOfSignature(getSignatureFromDeclaration(node)); + } + if (node.body.kind === SyntaxKind.Block) { + checkSourceElement(node.body); } - - const functionIsUsedInBody = forEachChild(body, function check(childNode): boolean | undefined { - if (isIdentifier(childNode)) { - const childSymbol = getSymbolAtLocation(childNode); - if (childSymbol && childSymbol.id === testedFunctionSymbol.id) { - return true; + else { + // From within an async function you can return either a non-promise value or a promise. Any + // Promise/A+ compatible implementation will always assimilate any foreign promise, so we + // should not be checking assignability of a promise to the return type. Instead, we need to + // check assignability of the awaited type of the expression body against the promised type of + // its return type annotation. + const exprType = checkExpression(node.body); + const returnOrPromisedType = getReturnOrPromisedType(returnType, functionFlags); + if (returnOrPromisedType) { + if ((functionFlags & FunctionFlags.AsyncGenerator) === FunctionFlags.Async) { // Async function + const awaitedType = checkAwaitedType(exprType, node.body, Diagnostics.The_return_type_of_an_async_function_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member); + checkTypeAssignableToAndOptionallyElaborate(awaitedType, returnOrPromisedType, node.body, node.body); + } + else { // Normal function + checkTypeAssignableToAndOptionallyElaborate(exprType, returnOrPromisedType, node.body, node.body); } } - - return forEachChild(childNode, check); - }); - - if (!functionIsUsedInBody) { - error(condExpr, Diagnostics.This_condition_will_always_return_true_since_the_function_is_always_defined_Did_you_mean_to_call_it_instead); } } - - function checkDoStatement(node: DoStatement) { - // Grammar checking - checkGrammarStatementInAmbientContext(node); - - checkSourceElement(node.statement); - checkTruthinessExpression(node.expression); - } - - function checkWhileStatement(node: WhileStatement) { - // Grammar checking - checkGrammarStatementInAmbientContext(node); - - checkTruthinessExpression(node.expression); - checkSourceElement(node.statement); + } + function checkArithmeticOperandType(operand: Node, type: ts.Type, diagnostic: DiagnosticMessage, isAwaitValid = false): boolean { + if (!isTypeAssignableTo(type, numberOrBigIntType)) { + const awaitedType = isAwaitValid && getAwaitedTypeOfPromise(type); + errorAndMaybeSuggestAwait(operand, !!awaitedType && isTypeAssignableTo(awaitedType, numberOrBigIntType), diagnostic); + return false; } - - function checkTruthinessOfType(type: Type, node: Node) { - if (type.flags & TypeFlags.Void) { - error(node, Diagnostics.An_expression_of_type_void_cannot_be_tested_for_truthiness); - } - return type; + return true; + } + function isReadonlyAssignmentDeclaration(d: Declaration) { + if (!isCallExpression(d)) { + return false; } - - function checkTruthinessExpression(node: Expression, checkMode?: CheckMode) { - return checkTruthinessOfType(checkExpression(node, checkMode), node); + if (!isBindableObjectDefinePropertyCall(d)) { + return false; } - - function checkForStatement(node: ForStatement) { - // Grammar checking - if (!checkGrammarStatementInAmbientContext(node)) { - if (node.initializer && node.initializer.kind === SyntaxKind.VariableDeclarationList) { - checkGrammarVariableDeclarationList(node.initializer); + const objectLitType = checkExpressionCached(d.arguments[2]); + const valueType = getTypeOfPropertyOfType(objectLitType, ("value" as __String)); + if (valueType) { + const writableProp = getPropertyOfType(objectLitType, ("writable" as __String)); + const writableType = writableProp && getTypeOfSymbol(writableProp); + if (!writableType || writableType === falseType || writableType === regularFalseType) { + return true; + } + // We include this definition whereupon we walk back and check the type at the declaration because + // The usual definition of `Object.defineProperty` will _not_ cause literal types to be preserved in the + // argument types, should the type be contextualized by the call itself. + if (writableProp && writableProp.valueDeclaration && isPropertyAssignment(writableProp.valueDeclaration)) { + const initializer = writableProp.valueDeclaration.initializer; + const rawOriginalType = checkExpression(initializer); + if (rawOriginalType === falseType || rawOriginalType === regularFalseType) { + return true; } } - - if (node.initializer) { - if (node.initializer.kind === SyntaxKind.VariableDeclarationList) { - forEach((node.initializer).declarations, checkVariableDeclaration); + return false; + } + const setProp = getPropertyOfType(objectLitType, ("set" as __String)); + return !setProp; + } + function isReadonlySymbol(symbol: ts.Symbol): boolean { + // The following symbols are considered read-only: + // Properties with a 'readonly' modifier + // Variables declared with 'const' + // Get accessors without matching set accessors + // Enum members + // Object.defineProperty assignments with writable false or no setter + // Unions and intersections of the above (unions and intersections eagerly set isReadonly on creation) + return !!(getCheckFlags(symbol) & CheckFlags.Readonly || + symbol.flags & SymbolFlags.Property && getDeclarationModifierFlagsFromSymbol(symbol) & ModifierFlags.Readonly || + symbol.flags & SymbolFlags.Variable && getDeclarationNodeFlagsFromSymbol(symbol) & NodeFlags.Const || + symbol.flags & SymbolFlags.Accessor && !(symbol.flags & SymbolFlags.SetAccessor) || + symbol.flags & SymbolFlags.EnumMember || + some(symbol.declarations, isReadonlyAssignmentDeclaration)); + } + function isAssignmentToReadonlyEntity(expr: Expression, symbol: ts.Symbol, assignmentKind: AssignmentKind) { + if (assignmentKind === AssignmentKind.None) { + // no assigment means it doesn't matter whether the entity is readonly + return false; + } + if (isReadonlySymbol(symbol)) { + // Allow assignments to readonly properties within constructors of the same class declaration. + if (symbol.flags & SymbolFlags.Property && + isAccessExpression(expr) && + expr.expression.kind === SyntaxKind.ThisKeyword) { + // Look for if this is the constructor for the class that `symbol` is a property of. + const ctor = getContainingFunction(expr); + if (!(ctor && ctor.kind === SyntaxKind.Constructor)) { + return true; } - else { - checkExpression(node.initializer); + if (symbol.valueDeclaration) { + const isAssignmentDeclaration = isBinaryExpression(symbol.valueDeclaration); + const isLocalPropertyDeclaration = ctor.parent === symbol.valueDeclaration.parent; + const isLocalParameterProperty = ctor === symbol.valueDeclaration.parent; + const isLocalThisPropertyAssignment = isAssignmentDeclaration && symbol.parent?.valueDeclaration === ctor.parent; + const isLocalThisPropertyAssignmentConstructorFunction = isAssignmentDeclaration && symbol.parent?.valueDeclaration === ctor; + const isWriteableSymbol = isLocalPropertyDeclaration + || isLocalParameterProperty + || isLocalThisPropertyAssignment + || isLocalThisPropertyAssignmentConstructorFunction; + return !isWriteableSymbol; } } - - if (node.condition) checkTruthinessExpression(node.condition); - if (node.incrementor) checkExpression(node.incrementor); - checkSourceElement(node.statement); - if (node.locals) { - registerForUnusedIdentifiersCheck(node); - } + return true; } - - function checkForOfStatement(node: ForOfStatement): void { - checkGrammarForInOrForOfStatement(node); - - if (node.awaitModifier) { - const functionFlags = getFunctionFlags(getContainingFunction(node)); - if ((functionFlags & (FunctionFlags.Invalid | FunctionFlags.Async)) === FunctionFlags.Async && languageVersion < ScriptTarget.ESNext) { - // for..await..of in an async function or async generator function prior to ESNext requires the __asyncValues helper - checkExternalEmitHelpers(node, ExternalEmitHelpers.ForAwaitOfIncludes); + if (isAccessExpression(expr)) { + // references through namespace import should be readonly + const node = skipParentheses(expr.expression); + if (node.kind === SyntaxKind.Identifier) { + const symbol = getNodeLinks(node).resolvedSymbol!; + if (symbol.flags & SymbolFlags.Alias) { + const declaration = getDeclarationOfAliasSymbol(symbol); + return !!declaration && declaration.kind === SyntaxKind.NamespaceImport; } } - else if (compilerOptions.downlevelIteration && languageVersion < ScriptTarget.ES2015) { - // for..of prior to ES2015 requires the __values helper when downlevelIteration is enabled - checkExternalEmitHelpers(node, ExternalEmitHelpers.ForOfIncludes); - } - - // Check the LHS and RHS - // If the LHS is a declaration, just check it as a variable declaration, which will in turn check the RHS - // via checkRightHandSideOfForOf. - // If the LHS is an expression, check the LHS, as a destructuring assignment or as a reference. - // Then check that the RHS is assignable to it. - if (node.initializer.kind === SyntaxKind.VariableDeclarationList) { - checkForInOrForOfVariableDeclaration(node); - } - else { - const varExpr = node.initializer; - const iteratedType = checkRightHandSideOfForOf(node.expression, node.awaitModifier); - - // There may be a destructuring assignment on the left side - if (varExpr.kind === SyntaxKind.ArrayLiteralExpression || varExpr.kind === SyntaxKind.ObjectLiteralExpression) { - // iteratedType may be undefined. In this case, we still want to check the structure of - // varExpr, in particular making sure it's a valid LeftHandSideExpression. But we'd like - // to short circuit the type relation checking as much as possible, so we pass the unknownType. - checkDestructuringAssignment(varExpr, iteratedType || errorType); + } + return false; + } + function checkReferenceExpression(expr: Expression, invalidReferenceMessage: DiagnosticMessage, invalidOptionalChainMessage: DiagnosticMessage): boolean { + // References are combinations of identifiers, parentheses, and property accesses. + const node = skipOuterExpressions(expr, OuterExpressionKinds.Assertions | OuterExpressionKinds.Parentheses); + if (node.kind !== SyntaxKind.Identifier && !isAccessExpression(node)) { + error(expr, invalidReferenceMessage); + return false; + } + if (node.flags & NodeFlags.OptionalChain) { + error(expr, invalidOptionalChainMessage); + return false; + } + return true; + } + function checkDeleteExpression(node: DeleteExpression): ts.Type { + checkExpression(node.expression); + const expr = skipParentheses(node.expression); + if (!isAccessExpression(expr)) { + error(expr, Diagnostics.The_operand_of_a_delete_operator_must_be_a_property_reference); + return booleanType; + } + if (expr.kind === SyntaxKind.PropertyAccessExpression && isPrivateIdentifier((expr as PropertyAccessExpression).name)) { + error(expr, Diagnostics.The_operand_of_a_delete_operator_cannot_be_a_private_identifier); + } + const links = getNodeLinks(expr); + const symbol = getExportSymbolOfValueSymbolIfExported(links.resolvedSymbol); + if (symbol && isReadonlySymbol(symbol)) { + error(expr, Diagnostics.The_operand_of_a_delete_operator_cannot_be_a_read_only_property); + } + return booleanType; + } + function checkTypeOfExpression(node: TypeOfExpression): ts.Type { + checkExpression(node.expression); + return typeofType; + } + function checkVoidExpression(node: VoidExpression): ts.Type { + checkExpression(node.expression); + return undefinedWideningType; + } + function isTopLevelAwait(node: AwaitExpression) { + const container = getThisContainer(node, /*includeArrowFunctions*/ true); + return isSourceFile(container); + } + function checkAwaitExpression(node: AwaitExpression): ts.Type { + // Grammar checking + if (produceDiagnostics) { + if (!(node.flags & NodeFlags.AwaitContext)) { + if (isTopLevelAwait(node)) { + const sourceFile = getSourceFileOfNode(node); + if (!hasParseDiagnostics(sourceFile)) { + let span: TextSpan | undefined; + if (!isEffectiveExternalModule(sourceFile, compilerOptions)) { + if (!span) + span = getSpanOfTokenAtPosition(sourceFile, node.pos); + const diagnostic = createFileDiagnostic(sourceFile, span.start, span.length, Diagnostics.await_expressions_are_only_allowed_at_the_top_level_of_a_file_when_that_file_is_a_module_but_this_file_has_no_imports_or_exports_Consider_adding_an_empty_export_to_make_this_file_a_module); + diagnostics.add(diagnostic); + } + if ((moduleKind !== ModuleKind.ESNext && moduleKind !== ModuleKind.System) || languageVersion < ScriptTarget.ES2017) { + span = getSpanOfTokenAtPosition(sourceFile, node.pos); + const diagnostic = createFileDiagnostic(sourceFile, span.start, span.length, Diagnostics.Top_level_await_expressions_are_only_allowed_when_the_module_option_is_set_to_esnext_or_system_and_the_target_option_is_set_to_es2017_or_higher); + diagnostics.add(diagnostic); + } + } } else { - const leftType = checkExpression(varExpr); - checkReferenceExpression( - varExpr, - Diagnostics.The_left_hand_side_of_a_for_of_statement_must_be_a_variable_or_a_property_access, - Diagnostics.The_left_hand_side_of_a_for_of_statement_may_not_be_an_optional_property_access); - - // iteratedType will be undefined if the rightType was missing properties/signatures - // required to get its iteratedType (like [Symbol.iterator] or next). This may be - // because we accessed properties from anyType, or it may have led to an error inside - // getElementTypeOfIterable. - if (iteratedType) { - checkTypeAssignableToAndOptionallyElaborate(iteratedType, leftType, varExpr, node.expression); + // use of 'await' in non-async function + const sourceFile = getSourceFileOfNode(node); + if (!hasParseDiagnostics(sourceFile)) { + const span = getSpanOfTokenAtPosition(sourceFile, node.pos); + const diagnostic = createFileDiagnostic(sourceFile, span.start, span.length, Diagnostics.await_expressions_are_only_allowed_within_async_functions_and_at_the_top_levels_of_modules); + const func = getContainingFunction(node); + if (func && func.kind !== SyntaxKind.Constructor && (getFunctionFlags(func) & FunctionFlags.Async) === 0) { + const relatedInfo = createDiagnosticForNode(func, Diagnostics.Did_you_mean_to_mark_this_function_as_async); + addRelatedInfo(diagnostic, relatedInfo); + } + diagnostics.add(diagnostic); } } } - - checkSourceElement(node.statement); - if (node.locals) { - registerForUnusedIdentifiersCheck(node); + if (isInParameterInitializerBeforeContainingFunction(node)) { + error(node, Diagnostics.await_expressions_cannot_be_used_in_a_parameter_initializer); } } - - function checkForInStatement(node: ForInStatement) { - // Grammar checking - checkGrammarForInOrForOfStatement(node); - - const rightType = getNonNullableTypeIfNeeded(checkExpression(node.expression)); - // TypeScript 1.0 spec (April 2014): 5.4 - // In a 'for-in' statement of the form - // for (let VarDecl in Expr) Statement - // VarDecl must be a variable declaration without a type annotation that declares a variable of type Any, - // and Expr must be an expression of type Any, an object type, or a type parameter type. - if (node.initializer.kind === SyntaxKind.VariableDeclarationList) { - const variable = (node.initializer).declarations[0]; - if (variable && isBindingPattern(variable.name)) { - error(variable.name, Diagnostics.The_left_hand_side_of_a_for_in_statement_cannot_be_a_destructuring_pattern); - } - checkForInOrForOfVariableDeclaration(node); - } - else { - // In a 'for-in' statement of the form - // for (Var in Expr) Statement - // Var must be an expression classified as a reference of type Any or the String primitive type, - // and Expr must be an expression of type Any, an object type, or a type parameter type. - const varExpr = node.initializer; - const leftType = checkExpression(varExpr); - if (varExpr.kind === SyntaxKind.ArrayLiteralExpression || varExpr.kind === SyntaxKind.ObjectLiteralExpression) { - error(varExpr, Diagnostics.The_left_hand_side_of_a_for_in_statement_cannot_be_a_destructuring_pattern); + const operandType = checkExpression(node.expression); + const awaitedType = checkAwaitedType(operandType, node, Diagnostics.Type_of_await_operand_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member); + if (awaitedType === operandType && awaitedType !== errorType && !(operandType.flags & TypeFlags.AnyOrUnknown)) { + addErrorOrSuggestion(/*isError*/ false, createDiagnosticForNode(node, Diagnostics.await_has_no_effect_on_the_type_of_this_expression)); + } + return awaitedType; + } + function checkPrefixUnaryExpression(node: PrefixUnaryExpression): ts.Type { + const operandType = checkExpression(node.operand); + if (operandType === silentNeverType) { + return silentNeverType; + } + switch (node.operand.kind) { + case SyntaxKind.NumericLiteral: + switch (node.operator) { + case SyntaxKind.MinusToken: + return getFreshTypeOfLiteralType(getLiteralType(-(node.operand as NumericLiteral).text)); + case SyntaxKind.PlusToken: + return getFreshTypeOfLiteralType(getLiteralType(+(node.operand as NumericLiteral).text)); + } + break; + case SyntaxKind.BigIntLiteral: + if (node.operator === SyntaxKind.MinusToken) { + return getFreshTypeOfLiteralType(getLiteralType({ + negative: true, + base10Value: parsePseudoBigInt((node.operand as BigIntLiteral).text) + })); + } + } + switch (node.operator) { + case SyntaxKind.PlusToken: + case SyntaxKind.MinusToken: + case SyntaxKind.TildeToken: + checkNonNullType(operandType, node.operand); + if (maybeTypeOfKind(operandType, TypeFlags.ESSymbolLike)) { + error(node.operand, Diagnostics.The_0_operator_cannot_be_applied_to_type_symbol, tokenToString(node.operator)); + } + if (node.operator === SyntaxKind.PlusToken) { + if (maybeTypeOfKind(operandType, TypeFlags.BigIntLike)) { + error(node.operand, Diagnostics.Operator_0_cannot_be_applied_to_type_1, tokenToString(node.operator), typeToString(getBaseTypeOfLiteralType(operandType))); + } + return numberType; } - else if (!isTypeAssignableTo(getIndexTypeOrString(rightType), leftType)) { - error(varExpr, Diagnostics.The_left_hand_side_of_a_for_in_statement_must_be_of_type_string_or_any); + return getUnaryResultType(operandType); + case SyntaxKind.ExclamationToken: + checkTruthinessExpression(node.operand); + const facts = getTypeFacts(operandType) & (TypeFacts.Truthy | TypeFacts.Falsy); + return facts === TypeFacts.Truthy ? falseType : + facts === TypeFacts.Falsy ? trueType : + booleanType; + case SyntaxKind.PlusPlusToken: + case SyntaxKind.MinusMinusToken: + const ok = checkArithmeticOperandType(node.operand, checkNonNullType(operandType, node.operand), Diagnostics.An_arithmetic_operand_must_be_of_type_any_number_bigint_or_an_enum_type); + if (ok) { + // run check only if former checks succeeded to avoid reporting cascading errors + checkReferenceExpression(node.operand, Diagnostics.The_operand_of_an_increment_or_decrement_operator_must_be_a_variable_or_a_property_access, Diagnostics.The_operand_of_an_increment_or_decrement_operator_may_not_be_an_optional_property_access); } - else { - // run check only former check succeeded to avoid cascading errors - checkReferenceExpression( - varExpr, - Diagnostics.The_left_hand_side_of_a_for_in_statement_must_be_a_variable_or_a_property_access, - Diagnostics.The_left_hand_side_of_a_for_in_statement_may_not_be_an_optional_property_access); + return getUnaryResultType(operandType); + } + return errorType; + } + function checkPostfixUnaryExpression(node: PostfixUnaryExpression): ts.Type { + const operandType = checkExpression(node.operand); + if (operandType === silentNeverType) { + return silentNeverType; + } + const ok = checkArithmeticOperandType(node.operand, checkNonNullType(operandType, node.operand), Diagnostics.An_arithmetic_operand_must_be_of_type_any_number_bigint_or_an_enum_type); + if (ok) { + // run check only if former checks succeeded to avoid reporting cascading errors + checkReferenceExpression(node.operand, Diagnostics.The_operand_of_an_increment_or_decrement_operator_must_be_a_variable_or_a_property_access, Diagnostics.The_operand_of_an_increment_or_decrement_operator_may_not_be_an_optional_property_access); + } + return getUnaryResultType(operandType); + } + function getUnaryResultType(operandType: ts.Type): ts.Type { + if (maybeTypeOfKind(operandType, TypeFlags.BigIntLike)) { + return isTypeAssignableToKind(operandType, TypeFlags.AnyOrUnknown) || maybeTypeOfKind(operandType, TypeFlags.NumberLike) + ? numberOrBigIntType + : bigintType; + } + // If it's not a bigint type, implicit coercion will result in a number + return numberType; + } + // Return true if type might be of the given kind. A union or intersection type might be of a given + // kind if at least one constituent type is of the given kind. + function maybeTypeOfKind(type: ts.Type, kind: TypeFlags): boolean { + if (type.flags & kind) { + return true; + } + if (type.flags & TypeFlags.UnionOrIntersection) { + const types = (type).types; + for (const t of types) { + if (maybeTypeOfKind(t, kind)) { + return true; } } - - // unknownType is returned i.e. if node.expression is identifier whose name cannot be resolved - // in this case error about missing name is already reported - do not report extra one - if (rightType === neverType || !isTypeAssignableToKind(rightType, TypeFlags.NonPrimitive | TypeFlags.InstantiableNonPrimitive)) { - error(node.expression, Diagnostics.The_right_hand_side_of_a_for_in_statement_must_be_of_type_any_an_object_type_or_a_type_parameter_but_here_has_type_0, typeToString(rightType)); - } - - checkSourceElement(node.statement); - if (node.locals) { - registerForUnusedIdentifiersCheck(node); - } } - - function checkForInOrForOfVariableDeclaration(iterationStatement: ForInOrOfStatement): void { - const variableDeclarationList = iterationStatement.initializer; - // checkGrammarForInOrForOfStatement will check that there is exactly one declaration. - if (variableDeclarationList.declarations.length >= 1) { - const decl = variableDeclarationList.declarations[0]; - checkVariableDeclaration(decl); - } + return false; + } + function isTypeAssignableToKind(source: ts.Type, kind: TypeFlags, strict?: boolean): boolean { + if (source.flags & kind) { + return true; } - - function checkRightHandSideOfForOf(rhsExpression: Expression, awaitModifier: AwaitKeywordToken | undefined): Type { - const expressionType = checkNonNullExpression(rhsExpression); - const use = awaitModifier ? IterationUse.ForAwaitOf : IterationUse.ForOf; - return checkIteratedTypeOrElementType(use, expressionType, undefinedType, rhsExpression); + if (strict && source.flags & (TypeFlags.AnyOrUnknown | TypeFlags.Void | TypeFlags.Undefined | TypeFlags.Null)) { + return false; } - - function checkIteratedTypeOrElementType(use: IterationUse, inputType: Type, sentType: Type, errorNode: Node | undefined): Type { - if (isTypeAny(inputType)) { - return inputType; - } - - return getIteratedTypeOrElementType(use, inputType, sentType, errorNode, /*checkAssignability*/ true) || anyType; + return !!(kind & TypeFlags.NumberLike) && isTypeAssignableTo(source, numberType) || + !!(kind & TypeFlags.BigIntLike) && isTypeAssignableTo(source, bigintType) || + !!(kind & TypeFlags.StringLike) && isTypeAssignableTo(source, stringType) || + !!(kind & TypeFlags.BooleanLike) && isTypeAssignableTo(source, booleanType) || + !!(kind & TypeFlags.Void) && isTypeAssignableTo(source, voidType) || + !!(kind & TypeFlags.Never) && isTypeAssignableTo(source, neverType) || + !!(kind & TypeFlags.Null) && isTypeAssignableTo(source, nullType) || + !!(kind & TypeFlags.Undefined) && isTypeAssignableTo(source, undefinedType) || + !!(kind & TypeFlags.ESSymbol) && isTypeAssignableTo(source, esSymbolType) || + !!(kind & TypeFlags.NonPrimitive) && isTypeAssignableTo(source, nonPrimitiveType); + } + function allTypesAssignableToKind(source: ts.Type, kind: TypeFlags, strict?: boolean): boolean { + return source.flags & TypeFlags.Union ? + every((source as UnionType).types, subType => allTypesAssignableToKind(subType, kind, strict)) : + isTypeAssignableToKind(source, kind, strict); + } + function isConstEnumObjectType(type: ts.Type): boolean { + return !!(getObjectFlags(type) & ObjectFlags.Anonymous) && !!type.symbol && isConstEnumSymbol(type.symbol); + } + function isConstEnumSymbol(symbol: ts.Symbol): boolean { + return (symbol.flags & SymbolFlags.ConstEnum) !== 0; + } + function checkInstanceOfExpression(left: Expression, right: Expression, leftType: ts.Type, rightType: ts.Type): ts.Type { + if (leftType === silentNeverType || rightType === silentNeverType) { + return silentNeverType; + } + // TypeScript 1.0 spec (April 2014): 4.15.4 + // The instanceof operator requires the left operand to be of type Any, an object type, or a type parameter type, + // and the right operand to be of type Any, a subtype of the 'Function' interface type, or have a call or construct signature. + // The result is always of the Boolean primitive type. + // NOTE: do not raise error if leftType is unknown as related error was already reported + if (!isTypeAny(leftType) && + allTypesAssignableToKind(leftType, TypeFlags.Primitive)) { + error(left, Diagnostics.The_left_hand_side_of_an_instanceof_expression_must_be_of_type_any_an_object_type_or_a_type_parameter); + } + // NOTE: do not raise error if right is unknown as related error was already reported + if (!(isTypeAny(rightType) || typeHasCallOrConstructSignatures(rightType) || isTypeSubtypeOf(rightType, globalFunctionType))) { + error(right, Diagnostics.The_right_hand_side_of_an_instanceof_expression_must_be_of_type_any_or_of_a_type_assignable_to_the_Function_interface_type); + } + return booleanType; + } + function checkInExpression(left: Expression, right: Expression, leftType: ts.Type, rightType: ts.Type): ts.Type { + if (leftType === silentNeverType || rightType === silentNeverType) { + return silentNeverType; + } + leftType = checkNonNullType(leftType, left); + rightType = checkNonNullType(rightType, right); + // TypeScript 1.0 spec (April 2014): 4.15.5 + // The in operator requires the left operand to be of type Any, the String primitive type, or the Number primitive type, + // and the right operand to be of type Any, an object type, or a type parameter type. + // The result is always of the Boolean primitive type. + if (!(isTypeComparableTo(leftType, stringType) || isTypeAssignableToKind(leftType, TypeFlags.NumberLike | TypeFlags.ESSymbolLike))) { + error(left, Diagnostics.The_left_hand_side_of_an_in_expression_must_be_of_type_any_string_number_or_symbol); + } + if (!allTypesAssignableToKind(rightType, TypeFlags.NonPrimitive | TypeFlags.InstantiableNonPrimitive)) { + error(right, Diagnostics.The_right_hand_side_of_an_in_expression_must_be_of_type_any_an_object_type_or_a_type_parameter); + } + return booleanType; + } + function checkObjectLiteralAssignment(node: ObjectLiteralExpression, sourceType: ts.Type, rightIsThis?: boolean): ts.Type { + const properties = node.properties; + if (strictNullChecks && properties.length === 0) { + return checkNonNullType(sourceType, node); } - - /** - * When consuming an iterable type in a for..of, spread, or iterator destructuring assignment - * we want to get the iterated type of an iterable for ES2015 or later, or the iterated type - * of a iterable (if defined globally) or element type of an array like for ES2015 or earlier. - */ - function getIteratedTypeOrElementType(use: IterationUse, inputType: Type, sentType: Type, errorNode: Node | undefined, checkAssignability: boolean): Type | undefined { - const allowAsyncIterables = (use & IterationUse.AllowsAsyncIterablesFlag) !== 0; - if (inputType === neverType) { - reportTypeNotIterableError(errorNode!, inputType, allowAsyncIterables); // TODO: GH#18217 - return undefined; - } - - const uplevelIteration = languageVersion >= ScriptTarget.ES2015; - const downlevelIteration = !uplevelIteration && compilerOptions.downlevelIteration; - - // Get the iterated type of an `Iterable` or `IterableIterator` only in ES2015 - // or higher, when inside of an async generator or for-await-if, or when - // downlevelIteration is requested. - if (uplevelIteration || downlevelIteration || allowAsyncIterables) { - // We only report errors for an invalid iterable type in ES2015 or higher. - const iterationTypes = getIterationTypesOfIterable(inputType, use, uplevelIteration ? errorNode : undefined); - if (checkAssignability) { - if (iterationTypes) { - const diagnostic = - use & IterationUse.ForOfFlag ? Diagnostics.Cannot_iterate_value_because_the_next_method_of_its_iterator_expects_type_1_but_for_of_will_always_send_0 : - use & IterationUse.SpreadFlag ? Diagnostics.Cannot_iterate_value_because_the_next_method_of_its_iterator_expects_type_1_but_array_spread_will_always_send_0 : - use & IterationUse.DestructuringFlag ? Diagnostics.Cannot_iterate_value_because_the_next_method_of_its_iterator_expects_type_1_but_array_destructuring_will_always_send_0 : - use & IterationUse.YieldStarFlag ? Diagnostics.Cannot_delegate_iteration_to_value_because_the_next_method_of_its_iterator_expects_type_1_but_the_containing_generator_will_always_send_0 : - undefined; - if (diagnostic) { - checkTypeAssignableTo(sentType, iterationTypes.nextType, errorNode, diagnostic); - } - } - } - if (iterationTypes || uplevelIteration) { - return iterationTypes && iterationTypes.yieldType; + for (let i = 0; i < properties.length; i++) { + checkObjectLiteralDestructuringPropertyAssignment(node, sourceType, i, properties, rightIsThis); + } + return sourceType; + } + /** Note: If property cannot be a SpreadAssignment, then allProperties does not need to be provided */ + function checkObjectLiteralDestructuringPropertyAssignment(node: ObjectLiteralExpression, objectLiteralType: ts.Type, propertyIndex: number, allProperties?: NodeArray, rightIsThis = false) { + const properties = node.properties; + const property = properties[propertyIndex]; + if (property.kind === SyntaxKind.PropertyAssignment || property.kind === SyntaxKind.ShorthandPropertyAssignment) { + const name = property.name; + const exprType = getLiteralTypeFromPropertyName(name); + if (isTypeUsableAsPropertyName(exprType)) { + const text = getPropertyNameFromType(exprType); + const prop = getPropertyOfType(objectLiteralType, text); + if (prop) { + markPropertyAsReferenced(prop, property, rightIsThis); + checkPropertyAccessibility(property, /*isSuper*/ false, objectLiteralType, prop); } } - - let arrayType = inputType; - let reportedError = false; - let hasStringConstituent = false; - - // If strings are permitted, remove any string-like constituents from the array type. - // This allows us to find other non-string element types from an array unioned with - // a string. - if (use & IterationUse.AllowsStringInputFlag) { - if (arrayType.flags & TypeFlags.Union) { - // After we remove all types that are StringLike, we will know if there was a string constituent - // based on whether the result of filter is a new array. - const arrayTypes = (inputType).types; - const filteredTypes = filter(arrayTypes, t => !(t.flags & TypeFlags.StringLike)); - if (filteredTypes !== arrayTypes) { - arrayType = getUnionType(filteredTypes, UnionReduction.Subtype); - } - } - else if (arrayType.flags & TypeFlags.StringLike) { - arrayType = neverType; + const elementType = getIndexedAccessType(objectLiteralType, exprType, name); + const type = getFlowTypeOfDestructuring(property, elementType); + return checkDestructuringAssignment(property.kind === SyntaxKind.ShorthandPropertyAssignment ? property : property.initializer, type); + } + else if (property.kind === SyntaxKind.SpreadAssignment) { + if (propertyIndex < properties.length - 1) { + error(property, Diagnostics.A_rest_element_must_be_last_in_a_destructuring_pattern); + } + else { + if (languageVersion < ScriptTarget.ESNext) { + checkExternalEmitHelpers(property, ExternalEmitHelpers.Rest); } - - hasStringConstituent = arrayType !== inputType; - if (hasStringConstituent) { - if (languageVersion < ScriptTarget.ES5) { - if (errorNode) { - error(errorNode, Diagnostics.Using_a_string_in_a_for_of_statement_is_only_supported_in_ECMAScript_5_and_higher); - reportedError = true; + const nonRestNames: PropertyName[] = []; + if (allProperties) { + for (const otherProperty of allProperties) { + if (!isSpreadAssignment(otherProperty)) { + nonRestNames.push(otherProperty.name); } } - - // Now that we've removed all the StringLike types, if no constituents remain, then the entire - // arrayOrStringType was a string. - if (arrayType.flags & TypeFlags.Never) { - return stringType; - } - } - } - - if (!isArrayLikeType(arrayType)) { - if (errorNode && !reportedError) { - // Which error we report depends on whether we allow strings or if there was a - // string constituent. For example, if the input type is number | string, we - // want to say that number is not an array type. But if the input was just - // number and string input is allowed, we want to say that number is not an - // array type or a string type. - const yieldType = getIterationTypeOfIterable(use, IterationTypeKind.Yield, inputType, /*errorNode*/ undefined); - const [defaultDiagnostic, maybeMissingAwait]: [DiagnosticMessage, boolean] = !(use & IterationUse.AllowsStringInputFlag) || hasStringConstituent - ? downlevelIteration - ? [Diagnostics.Type_0_is_not_an_array_type_or_does_not_have_a_Symbol_iterator_method_that_returns_an_iterator, true] - : yieldType - ? [Diagnostics.Type_0_is_not_an_array_type_or_a_string_type_Use_compiler_option_downlevelIteration_to_allow_iterating_of_iterators, false] - : [Diagnostics.Type_0_is_not_an_array_type, true] - : downlevelIteration - ? [Diagnostics.Type_0_is_not_an_array_type_or_a_string_type_or_does_not_have_a_Symbol_iterator_method_that_returns_an_iterator, true] - : yieldType - ? [Diagnostics.Type_0_is_not_an_array_type_or_a_string_type_Use_compiler_option_downlevelIteration_to_allow_iterating_of_iterators, false] - : [Diagnostics.Type_0_is_not_an_array_type_or_a_string_type, true]; - errorAndMaybeSuggestAwait( - errorNode, - maybeMissingAwait && !!getAwaitedTypeOfPromise(arrayType), - defaultDiagnostic, - typeToString(arrayType)); - } - return hasStringConstituent ? stringType : undefined; - } - - const arrayElementType = getIndexTypeOfType(arrayType, IndexKind.Number); - if (hasStringConstituent && arrayElementType) { - // This is just an optimization for the case where arrayOrStringType is string | string[] - if (arrayElementType.flags & TypeFlags.StringLike) { - return stringType; } - - return getUnionType([arrayElementType, stringType], UnionReduction.Subtype); + const type = getRestType(objectLiteralType, nonRestNames, objectLiteralType.symbol); + checkGrammarForDisallowedTrailingComma(allProperties, Diagnostics.A_rest_parameter_or_binding_pattern_may_not_have_a_trailing_comma); + return checkDestructuringAssignment(property.expression, type); } - - return arrayElementType; } - - /** - * Gets the requested "iteration type" from an `Iterable`-like or `AsyncIterable`-like type. - */ - function getIterationTypeOfIterable(use: IterationUse, typeKind: IterationTypeKind, inputType: Type, errorNode: Node | undefined): Type | undefined { - if (isTypeAny(inputType)) { - return undefined; - } - - const iterationTypes = getIterationTypesOfIterable(inputType, use, errorNode); - return iterationTypes && iterationTypes[getIterationTypesKeyFromIterationTypeKind(typeKind)]; + else { + error(property, Diagnostics.Property_assignment_expected); } - - function createIterationTypes(yieldType: Type = neverType, returnType: Type = neverType, nextType: Type = unknownType): IterationTypes { - // `yieldType` and `returnType` are defaulted to `neverType` they each will be combined - // via `getUnionType` when merging iteration types. `nextType` is defined as `unknownType` - // as it is combined via `getIntersectionType` when merging iteration types. - - // Use the cache only for intrinsic types to keep it small as they are likely to be - // more frequently created (i.e. `Iterator`). Iteration types - // are also cached on the type they are requested for, so we shouldn't need to maintain - // the cache for less-frequently used types. - if (yieldType.flags & TypeFlags.Intrinsic && - returnType.flags & (TypeFlags.Any | TypeFlags.Never | TypeFlags.Unknown | TypeFlags.Void | TypeFlags.Undefined) && - nextType.flags & (TypeFlags.Any | TypeFlags.Never | TypeFlags.Unknown | TypeFlags.Void | TypeFlags.Undefined)) { - const id = getTypeListId([yieldType, returnType, nextType]); - let iterationTypes = iterationTypesCache.get(id); - if (!iterationTypes) { - iterationTypes = { yieldType, returnType, nextType }; - iterationTypesCache.set(id, iterationTypes); - } - return iterationTypes; + } + function checkArrayLiteralAssignment(node: ArrayLiteralExpression, sourceType: ts.Type, checkMode?: CheckMode): ts.Type { + const elements = node.elements; + if (languageVersion < ScriptTarget.ES2015 && compilerOptions.downlevelIteration) { + checkExternalEmitHelpers(node, ExternalEmitHelpers.Read); + } + // This elementType will be used if the specific property corresponding to this index is not + // present (aka the tuple element property). This call also checks that the parentType is in + // fact an iterable or array (depending on target language). + const elementType = checkIteratedTypeOrElementType(IterationUse.Destructuring, sourceType, undefinedType, node) || errorType; + for (let i = 0; i < elements.length; i++) { + checkArrayLiteralDestructuringElementAssignment(node, sourceType, i, elementType, checkMode); + } + return sourceType; + } + function checkArrayLiteralDestructuringElementAssignment(node: ArrayLiteralExpression, sourceType: ts.Type, elementIndex: number, elementType: ts.Type, checkMode?: CheckMode) { + const elements = node.elements; + const element = elements[elementIndex]; + if (element.kind !== SyntaxKind.OmittedExpression) { + if (element.kind !== SyntaxKind.SpreadElement) { + const indexType = getLiteralType(elementIndex); + if (isArrayLikeType(sourceType)) { + // We create a synthetic expression so that getIndexedAccessType doesn't get confused + // when the element is a SyntaxKind.ElementAccessExpression. + const accessFlags = hasDefaultValue(element) ? AccessFlags.NoTupleBoundsCheck : 0; + const elementType = getIndexedAccessTypeOrUndefined(sourceType, indexType, createSyntheticExpression(element, indexType), accessFlags) || errorType; + const assignedType = hasDefaultValue(element) ? getTypeWithFacts(elementType, TypeFacts.NEUndefined) : elementType; + const type = getFlowTypeOfDestructuring(element, assignedType); + return checkDestructuringAssignment(element, type, checkMode); + } + return checkDestructuringAssignment(element, elementType, checkMode); + } + if (elementIndex < elements.length - 1) { + error(element, Diagnostics.A_rest_element_must_be_last_in_a_destructuring_pattern); } - return { yieldType, returnType, nextType }; - } - - /** - * Combines multiple `IterationTypes` records. - * - * If `array` is empty or all elements are missing or are references to `noIterationTypes`, - * then `noIterationTypes` is returned. Otherwise, an `IterationTypes` record is returned - * for the combined iteration types. - */ - function combineIterationTypes(array: (IterationTypes | undefined)[]) { - let yieldTypes: Type[] | undefined; - let returnTypes: Type[] | undefined; - let nextTypes: Type[] | undefined; - for (const iterationTypes of array) { - if (iterationTypes === undefined || iterationTypes === noIterationTypes) { - continue; + else { + const restExpression = (element).expression; + if (restExpression.kind === SyntaxKind.BinaryExpression && (restExpression).operatorToken.kind === SyntaxKind.EqualsToken) { + error((restExpression).operatorToken, Diagnostics.A_rest_element_cannot_have_an_initializer); } - if (iterationTypes === anyIterationTypes) { - return anyIterationTypes; + else { + checkGrammarForDisallowedTrailingComma(node.elements, Diagnostics.A_rest_parameter_or_binding_pattern_may_not_have_a_trailing_comma); + const type = everyType(sourceType, isTupleType) ? + mapType(sourceType, t => sliceTupleType((t), elementIndex)) : + createArrayType(elementType); + return checkDestructuringAssignment(restExpression, type, checkMode); } - yieldTypes = append(yieldTypes, iterationTypes.yieldType); - returnTypes = append(returnTypes, iterationTypes.returnType); - nextTypes = append(nextTypes, iterationTypes.nextType); } - if (yieldTypes || returnTypes || nextTypes) { - return createIterationTypes( - yieldTypes && getUnionType(yieldTypes), - returnTypes && getUnionType(returnTypes), - nextTypes && getIntersectionType(nextTypes)); + } + return undefined; + } + function checkDestructuringAssignment(exprOrAssignment: Expression | ShorthandPropertyAssignment, sourceType: ts.Type, checkMode?: CheckMode, rightIsThis?: boolean): ts.Type { + let target: Expression; + if (exprOrAssignment.kind === SyntaxKind.ShorthandPropertyAssignment) { + const prop = (exprOrAssignment); + if (prop.objectAssignmentInitializer) { + // In strict null checking mode, if a default value of a non-undefined type is specified, remove + // undefined from the final type. + if (strictNullChecks && + !(getFalsyFlags(checkExpression(prop.objectAssignmentInitializer)) & TypeFlags.Undefined)) { + sourceType = getTypeWithFacts(sourceType, TypeFacts.NEUndefined); + } + checkBinaryLikeExpression(prop.name, prop.equalsToken!, prop.objectAssignmentInitializer, checkMode); } - return noIterationTypes; + target = (exprOrAssignment).name; } - - function getCachedIterationTypes(type: Type, cacheKey: MatchingKeys) { - return (type as IterableOrIteratorType)[cacheKey]; + else { + target = exprOrAssignment; } - - function setCachedIterationTypes(type: Type, cacheKey: MatchingKeys, cachedTypes: IterationTypes) { - return (type as IterableOrIteratorType)[cacheKey] = cachedTypes; + if (target.kind === SyntaxKind.BinaryExpression && (target).operatorToken.kind === SyntaxKind.EqualsToken) { + checkBinaryExpression((target), checkMode); + target = (target).left; } - - /** - * Gets the *yield*, *return*, and *next* types from an `Iterable`-like or `AsyncIterable`-like type. - * - * At every level that involves analyzing return types of signatures, we union the return types of all the signatures. - * - * Another thing to note is that at any step of this process, we could run into a dead end, - * meaning either the property is missing, or we run into the anyType. If either of these things - * happens, we return `undefined` to signal that we could not find the iteration type. If a property - * is missing, and the previous step did not result in `any`, then we also give an error if the - * caller requested it. Then the caller can decide what to do in the case where there is no iterated - * type. - * - * For a **for-of** statement, `yield*` (in a normal generator), spread, array - * destructuring, or normal generator we will only ever look for a `[Symbol.iterator]()` - * method. - * - * For an async generator we will only ever look at the `[Symbol.asyncIterator]()` method. - * - * For a **for-await-of** statement or a `yield*` in an async generator we will look for - * the `[Symbol.asyncIterator]()` method first, and then the `[Symbol.iterator]()` method. - */ - function getIterationTypesOfIterable(type: Type, use: IterationUse, errorNode: Node | undefined) { - if (isTypeAny(type)) { - return anyIterationTypes; - } - - if (!(type.flags & TypeFlags.Union)) { - const iterationTypes = getIterationTypesOfIterableWorker(type, use, errorNode); - if (iterationTypes === noIterationTypes) { - if (errorNode) { - reportTypeNotIterableError(errorNode, type, !!(use & IterationUse.AllowsAsyncIterablesFlag)); + if (target.kind === SyntaxKind.ObjectLiteralExpression) { + return checkObjectLiteralAssignment((target), sourceType, rightIsThis); + } + if (target.kind === SyntaxKind.ArrayLiteralExpression) { + return checkArrayLiteralAssignment((target), sourceType, checkMode); + } + return checkReferenceAssignment(target, sourceType, checkMode); + } + function checkReferenceAssignment(target: Expression, sourceType: ts.Type, checkMode?: CheckMode): ts.Type { + const targetType = checkExpression(target, checkMode); + const error = target.parent.kind === SyntaxKind.SpreadAssignment ? + Diagnostics.The_target_of_an_object_rest_assignment_must_be_a_variable_or_a_property_access : + Diagnostics.The_left_hand_side_of_an_assignment_expression_must_be_a_variable_or_a_property_access; + const optionalError = target.parent.kind === SyntaxKind.SpreadAssignment ? + Diagnostics.The_target_of_an_object_rest_assignment_may_not_be_an_optional_property_access : + Diagnostics.The_left_hand_side_of_an_assignment_expression_may_not_be_an_optional_property_access; + if (checkReferenceExpression(target, error, optionalError)) { + checkTypeAssignableToAndOptionallyElaborate(sourceType, targetType, target, target); + } + if (isPrivateIdentifierPropertyAccessExpression(target)) { + checkExternalEmitHelpers(target.parent, ExternalEmitHelpers.ClassPrivateFieldSet); + } + return sourceType; + } + /** + * This is a *shallow* check: An expression is side-effect-free if the + * evaluation of the expression *itself* cannot produce side effects. + * For example, x++ / 3 is side-effect free because the / operator + * does not have side effects. + * The intent is to "smell test" an expression for correctness in positions where + * its value is discarded (e.g. the left side of the comma operator). + */ + function isSideEffectFree(node: Node): boolean { + node = skipParentheses(node); + switch (node.kind) { + case SyntaxKind.Identifier: + case SyntaxKind.StringLiteral: + case SyntaxKind.RegularExpressionLiteral: + case SyntaxKind.TaggedTemplateExpression: + case SyntaxKind.TemplateExpression: + case SyntaxKind.NoSubstitutionTemplateLiteral: + case SyntaxKind.NumericLiteral: + case SyntaxKind.BigIntLiteral: + case SyntaxKind.TrueKeyword: + case SyntaxKind.FalseKeyword: + case SyntaxKind.NullKeyword: + case SyntaxKind.UndefinedKeyword: + case SyntaxKind.FunctionExpression: + case SyntaxKind.ClassExpression: + case SyntaxKind.ArrowFunction: + case SyntaxKind.ArrayLiteralExpression: + case SyntaxKind.ObjectLiteralExpression: + case SyntaxKind.TypeOfExpression: + case SyntaxKind.NonNullExpression: + case SyntaxKind.JsxSelfClosingElement: + case SyntaxKind.JsxElement: + return true; + case SyntaxKind.ConditionalExpression: + return isSideEffectFree((node as ConditionalExpression).whenTrue) && + isSideEffectFree((node as ConditionalExpression).whenFalse); + case SyntaxKind.BinaryExpression: + if (isAssignmentOperator((node as BinaryExpression).operatorToken.kind)) { + return false; + } + return isSideEffectFree((node as BinaryExpression).left) && + isSideEffectFree((node as BinaryExpression).right); + case SyntaxKind.PrefixUnaryExpression: + case SyntaxKind.PostfixUnaryExpression: + // Unary operators ~, !, +, and - have no side effects. + // The rest do. + switch ((node as PrefixUnaryExpression).operator) { + case SyntaxKind.ExclamationToken: + case SyntaxKind.PlusToken: + case SyntaxKind.MinusToken: + case SyntaxKind.TildeToken: + return true; + } + return false; + // Some forms listed here for clarity + case SyntaxKind.VoidExpression: // Explicit opt-out + case SyntaxKind.TypeAssertionExpression: // Not SEF, but can produce useful type warnings + case SyntaxKind.AsExpression: // Not SEF, but can produce useful type warnings + default: + return false; + } + } + function isTypeEqualityComparableTo(source: ts.Type, target: ts.Type) { + return (target.flags & TypeFlags.Nullable) !== 0 || isTypeComparableTo(source, target); + } + const enum CheckBinaryExpressionState { + MaybeCheckLeft, + CheckRight, + FinishCheck + } + function checkBinaryExpression(node: BinaryExpression, checkMode?: CheckMode) { + const workStacks: { + expr: BinaryExpression[]; + state: CheckBinaryExpressionState[]; + leftType: (ts.Type | undefined)[]; + } = { + expr: [node], + state: [CheckBinaryExpressionState.MaybeCheckLeft], + leftType: [undefined] + }; + let stackIndex = 0; + let lastResult: ts.Type | undefined; + while (stackIndex >= 0) { + node = workStacks.expr[stackIndex]; + switch (workStacks.state[stackIndex]) { + case CheckBinaryExpressionState.MaybeCheckLeft: { + if (isInJSFile(node) && getAssignedExpandoInitializer(node)) { + finishInvocation(checkExpression(node.right, checkMode)); + break; } - return undefined; + checkGrammarNullishCoalesceWithLogicalExpression(node); + const operator = node.operatorToken.kind; + if (operator === SyntaxKind.EqualsToken && (node.left.kind === SyntaxKind.ObjectLiteralExpression || node.left.kind === SyntaxKind.ArrayLiteralExpression)) { + finishInvocation(checkDestructuringAssignment(node.left, checkExpression(node.right, checkMode), checkMode, node.right.kind === SyntaxKind.ThisKeyword)); + break; + } + advanceState(CheckBinaryExpressionState.CheckRight); + maybeCheckExpression(node.left); + break; } - return iterationTypes; - } - - const cacheKey = use & IterationUse.AllowsAsyncIterablesFlag ? "iterationTypesOfAsyncIterable" : "iterationTypesOfIterable"; - const cachedTypes = getCachedIterationTypes(type, cacheKey); - if (cachedTypes) return cachedTypes === noIterationTypes ? undefined : cachedTypes; - - let allIterationTypes: IterationTypes[] | undefined; - for (const constituent of (type as UnionType).types) { - const iterationTypes = getIterationTypesOfIterableWorker(constituent, use, errorNode); - if (iterationTypes === noIterationTypes) { - if (errorNode) { - reportTypeNotIterableError(errorNode, type, !!(use & IterationUse.AllowsAsyncIterablesFlag)); - errorNode = undefined; + case CheckBinaryExpressionState.CheckRight: { + const leftType = lastResult!; + workStacks.leftType[stackIndex] = leftType; + const operator = node.operatorToken.kind; + if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.BarBarToken || operator === SyntaxKind.QuestionQuestionToken) { + checkTruthinessOfType(leftType, node.left); } + advanceState(CheckBinaryExpressionState.FinishCheck); + maybeCheckExpression(node.right); + break; } - else { - allIterationTypes = append(allIterationTypes, iterationTypes); + case CheckBinaryExpressionState.FinishCheck: { + const leftType = workStacks.leftType[stackIndex]!; + const rightType = lastResult!; + finishInvocation(checkBinaryLikeExpressionWorker(node.left, node.operatorToken, node.right, leftType, rightType, node)); + break; } + default: return Debug.fail(`Invalid state ${workStacks.state[stackIndex]} for checkBinaryExpression`); } - - const iterationTypes = allIterationTypes ? combineIterationTypes(allIterationTypes) : noIterationTypes; - setCachedIterationTypes(type, cacheKey, iterationTypes); - return iterationTypes === noIterationTypes ? undefined : iterationTypes; } - - function getAsyncFromSyncIterationTypes(iterationTypes: IterationTypes, errorNode: Node | undefined) { - if (iterationTypes === noIterationTypes) return noIterationTypes; - if (iterationTypes === anyIterationTypes) return anyIterationTypes; - const { yieldType, returnType, nextType } = iterationTypes; - return createIterationTypes( - getAwaitedType(yieldType, errorNode) || anyType, - getAwaitedType(returnType, errorNode) || anyType, - nextType); + return lastResult!; + function finishInvocation(result: ts.Type) { + lastResult = result; + stackIndex--; } - /** - * Gets the *yield*, *return*, and *next* types from a non-union type. - * - * If we are unable to find the *yield*, *return*, and *next* types, `noIterationTypes` is - * returned to indicate to the caller that it should report an error. Otherwise, an - * `IterationTypes` record is returned. - * - * NOTE: You probably don't want to call this directly and should be calling - * `getIterationTypesOfIterable` instead. + * Note that `advanceState` sets the _current_ head state, and that `maybeCheckExpression` potentially pushes on a new + * head state; so `advanceState` must be called before any `maybeCheckExpression` during a state's execution. */ - function getIterationTypesOfIterableWorker(type: Type, use: IterationUse, errorNode: Node | undefined) { - if (isTypeAny(type)) { - return anyIterationTypes; + function advanceState(nextState: CheckBinaryExpressionState) { + workStacks.state[stackIndex] = nextState; + } + function maybeCheckExpression(node: Expression) { + if (isBinaryExpression(node)) { + stackIndex++; + workStacks.expr[stackIndex] = node; + workStacks.state[stackIndex] = CheckBinaryExpressionState.MaybeCheckLeft; + workStacks.leftType[stackIndex] = undefined; } - - if (use & IterationUse.AllowsAsyncIterablesFlag) { - const iterationTypes = - getIterationTypesOfIterableCached(type, asyncIterationTypesResolver) || - getIterationTypesOfIterableFast(type, asyncIterationTypesResolver); - if (iterationTypes) { - return iterationTypes; - } + else { + lastResult = checkExpression(node, checkMode); } - - if (use & IterationUse.AllowsSyncIterablesFlag) { - const iterationTypes = - getIterationTypesOfIterableCached(type, syncIterationTypesResolver) || - getIterationTypesOfIterableFast(type, syncIterationTypesResolver); - if (iterationTypes) { - if (use & IterationUse.AllowsAsyncIterablesFlag) { - // for a sync iterable in an async context, only use the cached types if they are valid. - if (iterationTypes !== noIterationTypes) { - return setCachedIterationTypes(type, "iterationTypesOfAsyncIterable", getAsyncFromSyncIterationTypes(iterationTypes, errorNode)); + } + } + function checkGrammarNullishCoalesceWithLogicalExpression(node: BinaryExpression) { + const { left, operatorToken, right } = node; + if (operatorToken.kind === SyntaxKind.QuestionQuestionToken) { + if (isBinaryExpression(left) && (left.operatorToken.kind === SyntaxKind.BarBarToken || left.operatorToken.kind === SyntaxKind.AmpersandAmpersandToken)) { + grammarErrorOnNode(left, Diagnostics._0_and_1_operations_cannot_be_mixed_without_parentheses, tokenToString(left.operatorToken.kind), tokenToString(operatorToken.kind)); + } + if (isBinaryExpression(right) && (right.operatorToken.kind === SyntaxKind.BarBarToken || right.operatorToken.kind === SyntaxKind.AmpersandAmpersandToken)) { + grammarErrorOnNode(right, Diagnostics._0_and_1_operations_cannot_be_mixed_without_parentheses, tokenToString(right.operatorToken.kind), tokenToString(operatorToken.kind)); + } + } + } + // Note that this and `checkBinaryExpression` above should behave mostly the same, except this elides some + // expression-wide checks and does not use a work stack to fold nested binary expressions into the same callstack frame + function checkBinaryLikeExpression(left: Expression, operatorToken: Node, right: Expression, checkMode?: CheckMode, errorNode?: Node): ts.Type { + const operator = operatorToken.kind; + if (operator === SyntaxKind.EqualsToken && (left.kind === SyntaxKind.ObjectLiteralExpression || left.kind === SyntaxKind.ArrayLiteralExpression)) { + return checkDestructuringAssignment(left, checkExpression(right, checkMode), checkMode, right.kind === SyntaxKind.ThisKeyword); + } + let leftType: ts.Type; + if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.BarBarToken || operator === SyntaxKind.QuestionQuestionToken) { + leftType = checkTruthinessExpression(left, checkMode); + } + else { + leftType = checkExpression(left, checkMode); + } + const rightType = checkExpression(right, checkMode); + return checkBinaryLikeExpressionWorker(left, operatorToken, right, leftType, rightType, errorNode); + } + function checkBinaryLikeExpressionWorker(left: Expression, operatorToken: Node, right: Expression, leftType: ts.Type, rightType: ts.Type, errorNode?: Node): ts.Type { + const operator = operatorToken.kind; + switch (operator) { + case SyntaxKind.AsteriskToken: + case SyntaxKind.AsteriskAsteriskToken: + case SyntaxKind.AsteriskEqualsToken: + case SyntaxKind.AsteriskAsteriskEqualsToken: + case SyntaxKind.SlashToken: + case SyntaxKind.SlashEqualsToken: + case SyntaxKind.PercentToken: + case SyntaxKind.PercentEqualsToken: + case SyntaxKind.MinusToken: + case SyntaxKind.MinusEqualsToken: + case SyntaxKind.LessThanLessThanToken: + case SyntaxKind.LessThanLessThanEqualsToken: + case SyntaxKind.GreaterThanGreaterThanToken: + case SyntaxKind.GreaterThanGreaterThanEqualsToken: + case SyntaxKind.GreaterThanGreaterThanGreaterThanToken: + case SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: + case SyntaxKind.BarToken: + case SyntaxKind.BarEqualsToken: + case SyntaxKind.CaretToken: + case SyntaxKind.CaretEqualsToken: + case SyntaxKind.AmpersandToken: + case SyntaxKind.AmpersandEqualsToken: + if (leftType === silentNeverType || rightType === silentNeverType) { + return silentNeverType; + } + leftType = checkNonNullType(leftType, left); + rightType = checkNonNullType(rightType, right); + let suggestedOperator: SyntaxKind | undefined; + // if a user tries to apply a bitwise operator to 2 boolean operands + // try and return them a helpful suggestion + if ((leftType.flags & TypeFlags.BooleanLike) && + (rightType.flags & TypeFlags.BooleanLike) && + (suggestedOperator = getSuggestedBooleanOperator(operatorToken.kind)) !== undefined) { + error(errorNode || operatorToken, Diagnostics.The_0_operator_is_not_allowed_for_boolean_types_Consider_using_1_instead, tokenToString(operatorToken.kind), tokenToString(suggestedOperator)); + return numberType; + } + else { + // otherwise just check each operand separately and report errors as normal + const leftOk = checkArithmeticOperandType(left, leftType, Diagnostics.The_left_hand_side_of_an_arithmetic_operation_must_be_of_type_any_number_bigint_or_an_enum_type, /*isAwaitValid*/ true); + const rightOk = checkArithmeticOperandType(right, rightType, Diagnostics.The_right_hand_side_of_an_arithmetic_operation_must_be_of_type_any_number_bigint_or_an_enum_type, /*isAwaitValid*/ true); + let resultType: ts.Type; + // If both are any or unknown, allow operation; assume it will resolve to number + if ((isTypeAssignableToKind(leftType, TypeFlags.AnyOrUnknown) && isTypeAssignableToKind(rightType, TypeFlags.AnyOrUnknown)) || + // Or, if neither could be bigint, implicit coercion results in a number result + !(maybeTypeOfKind(leftType, TypeFlags.BigIntLike) || maybeTypeOfKind(rightType, TypeFlags.BigIntLike))) { + resultType = numberType; + } + // At least one is assignable to bigint, so check that both are + else if (bothAreBigIntLike(leftType, rightType)) { + switch (operator) { + case SyntaxKind.GreaterThanGreaterThanGreaterThanToken: + case SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: + reportOperatorError(); } + resultType = bigintType; } + // Exactly one of leftType/rightType is assignable to bigint else { - return iterationTypes; + reportOperatorError(bothAreBigIntLike); + resultType = errorType; + } + if (leftOk && rightOk) { + checkAssignmentOperator(resultType); } + return resultType; } - } - - if (use & IterationUse.AllowsAsyncIterablesFlag) { - const iterationTypes = getIterationTypesOfIterableSlow(type, asyncIterationTypesResolver, errorNode); - if (iterationTypes !== noIterationTypes) { - return iterationTypes; + case SyntaxKind.PlusToken: + case SyntaxKind.PlusEqualsToken: + if (leftType === silentNeverType || rightType === silentNeverType) { + return silentNeverType; } - } - - if (use & IterationUse.AllowsSyncIterablesFlag) { - const iterationTypes = getIterationTypesOfIterableSlow(type, syncIterationTypesResolver, errorNode); - if (iterationTypes !== noIterationTypes) { - if (use & IterationUse.AllowsAsyncIterablesFlag) { - return setCachedIterationTypes(type, "iterationTypesOfAsyncIterable", iterationTypes - ? getAsyncFromSyncIterationTypes(iterationTypes, errorNode) - : noIterationTypes); + if (!isTypeAssignableToKind(leftType, TypeFlags.StringLike) && !isTypeAssignableToKind(rightType, TypeFlags.StringLike)) { + leftType = checkNonNullType(leftType, left); + rightType = checkNonNullType(rightType, right); + } + let resultType: ts.Type | undefined; + if (isTypeAssignableToKind(leftType, TypeFlags.NumberLike, /*strict*/ true) && isTypeAssignableToKind(rightType, TypeFlags.NumberLike, /*strict*/ true)) { + // Operands of an enum type are treated as having the primitive type Number. + // If both operands are of the Number primitive type, the result is of the Number primitive type. + resultType = numberType; + } + else if (isTypeAssignableToKind(leftType, TypeFlags.BigIntLike, /*strict*/ true) && isTypeAssignableToKind(rightType, TypeFlags.BigIntLike, /*strict*/ true)) { + // If both operands are of the BigInt primitive type, the result is of the BigInt primitive type. + resultType = bigintType; + } + else if (isTypeAssignableToKind(leftType, TypeFlags.StringLike, /*strict*/ true) || isTypeAssignableToKind(rightType, TypeFlags.StringLike, /*strict*/ true)) { + // If one or both operands are of the String primitive type, the result is of the String primitive type. + resultType = stringType; + } + else if (isTypeAny(leftType) || isTypeAny(rightType)) { + // Otherwise, the result is of type Any. + // NOTE: unknown type here denotes error type. Old compiler treated this case as any type so do we. + resultType = leftType === errorType || rightType === errorType ? errorType : anyType; + } + // Symbols are not allowed at all in arithmetic expressions + if (resultType && !checkForDisallowedESSymbolOperand(operator)) { + return resultType; + } + if (!resultType) { + // Types that have a reasonably good chance of being a valid operand type. + // If both types have an awaited type of one of these, we'll assume the user + // might be missing an await without doing an exhaustive check that inserting + // await(s) will actually be a completely valid binary expression. + const closeEnoughKind = TypeFlags.NumberLike | TypeFlags.BigIntLike | TypeFlags.StringLike | TypeFlags.AnyOrUnknown; + reportOperatorError((left, right) => isTypeAssignableToKind(left, closeEnoughKind) && + isTypeAssignableToKind(right, closeEnoughKind)); + return anyType; + } + if (operator === SyntaxKind.PlusEqualsToken) { + checkAssignmentOperator(resultType); + } + return resultType; + case SyntaxKind.LessThanToken: + case SyntaxKind.GreaterThanToken: + case SyntaxKind.LessThanEqualsToken: + case SyntaxKind.GreaterThanEqualsToken: + if (checkForDisallowedESSymbolOperand(operator)) { + leftType = getBaseTypeOfLiteralType(checkNonNullType(leftType, left)); + rightType = getBaseTypeOfLiteralType(checkNonNullType(rightType, right)); + reportOperatorErrorUnless((left, right) => isTypeComparableTo(left, right) || isTypeComparableTo(right, left) || (isTypeAssignableTo(left, numberOrBigIntType) && isTypeAssignableTo(right, numberOrBigIntType))); + } + return booleanType; + case SyntaxKind.EqualsEqualsToken: + case SyntaxKind.ExclamationEqualsToken: + case SyntaxKind.EqualsEqualsEqualsToken: + case SyntaxKind.ExclamationEqualsEqualsToken: + reportOperatorErrorUnless((left, right) => isTypeEqualityComparableTo(left, right) || isTypeEqualityComparableTo(right, left)); + return booleanType; + case SyntaxKind.InstanceOfKeyword: + return checkInstanceOfExpression(left, right, leftType, rightType); + case SyntaxKind.InKeyword: + return checkInExpression(left, right, leftType, rightType); + case SyntaxKind.AmpersandAmpersandToken: + return getTypeFacts(leftType) & TypeFacts.Truthy ? + getUnionType([extractDefinitelyFalsyTypes(strictNullChecks ? leftType : getBaseTypeOfLiteralType(rightType)), rightType]) : + leftType; + case SyntaxKind.BarBarToken: + return getTypeFacts(leftType) & TypeFacts.Falsy ? + getUnionType([removeDefinitelyFalsyTypes(leftType), rightType], UnionReduction.Subtype) : + leftType; + case SyntaxKind.QuestionQuestionToken: + return getTypeFacts(leftType) & TypeFacts.EQUndefinedOrNull ? + getUnionType([getNonNullableType(leftType), rightType], UnionReduction.Subtype) : + leftType; + case SyntaxKind.EqualsToken: + const declKind = isBinaryExpression(left.parent) ? getAssignmentDeclarationKind(left.parent) : AssignmentDeclarationKind.None; + checkAssignmentDeclaration(declKind, rightType); + if (isAssignmentDeclaration(declKind)) { + if (!(rightType.flags & TypeFlags.Object) || + declKind !== AssignmentDeclarationKind.ModuleExports && + declKind !== AssignmentDeclarationKind.Prototype && + !isEmptyObjectType(rightType) && + !isFunctionObjectType((rightType as ObjectType)) && + !(getObjectFlags(rightType) & ObjectFlags.Class)) { + // don't check assignability of module.exports=, C.prototype=, or expando types because they will necessarily be incomplete + checkAssignmentOperator(rightType); } - else { - return iterationTypes; + return leftType; + } + else { + checkAssignmentOperator(rightType); + return getRegularTypeOfObjectLiteral(rightType); + } + case SyntaxKind.CommaToken: + if (!compilerOptions.allowUnreachableCode && isSideEffectFree(left) && !isEvalNode(right)) { + error(left, Diagnostics.Left_side_of_comma_operator_is_unused_and_has_no_side_effects); + } + return rightType; + default: + return Debug.fail(); + } + function bothAreBigIntLike(left: ts.Type, right: ts.Type): boolean { + return isTypeAssignableToKind(left, TypeFlags.BigIntLike) && isTypeAssignableToKind(right, TypeFlags.BigIntLike); + } + function checkAssignmentDeclaration(kind: AssignmentDeclarationKind, rightType: ts.Type) { + if (kind === AssignmentDeclarationKind.ModuleExports) { + for (const prop of getPropertiesOfObjectType(rightType)) { + const propType = getTypeOfSymbol(prop); + if (propType.symbol && propType.symbol.flags & SymbolFlags.Class) { + const name = prop.escapedName; + const symbol = resolveName(prop.valueDeclaration, name, SymbolFlags.Type, undefined, name, /*isUse*/ false); + if (symbol && symbol.declarations.some(isJSDocTypedefTag)) { + grammarErrorOnNode(symbol.declarations[0], Diagnostics.Duplicate_identifier_0, unescapeLeadingUnderscores(name)); + return grammarErrorOnNode(prop.valueDeclaration, Diagnostics.Duplicate_identifier_0, unescapeLeadingUnderscores(name)); + } } } } - - return noIterationTypes; } - - /** - * Gets the *yield*, *return*, and *next* types of an `Iterable`-like or - * `AsyncIterable`-like type from the cache. - * - * NOTE: You probably don't want to call this directly and should be calling - * `getIterationTypesOfIterable` instead. - */ - function getIterationTypesOfIterableCached(type: Type, resolver: IterationTypesResolver) { - return getCachedIterationTypes(type, resolver.iterableCacheKey); + function isEvalNode(node: Expression) { + return node.kind === SyntaxKind.Identifier && (node as Identifier).escapedText === "eval"; } - - function getIterationTypesOfGlobalIterableType(globalType: Type, resolver: IterationTypesResolver) { - const globalIterationTypes = - getIterationTypesOfIterableCached(globalType, resolver) || - getIterationTypesOfIterableSlow(globalType, resolver, /*errorNode*/ undefined); - return globalIterationTypes === noIterationTypes ? defaultIterationTypes : globalIterationTypes; + // Return true if there was no error, false if there was an error. + function checkForDisallowedESSymbolOperand(operator: SyntaxKind): boolean { + const offendingSymbolOperand = maybeTypeOfKind(leftType, TypeFlags.ESSymbolLike) ? left : + maybeTypeOfKind(rightType, TypeFlags.ESSymbolLike) ? right : + undefined; + if (offendingSymbolOperand) { + error(offendingSymbolOperand, Diagnostics.The_0_operator_cannot_be_applied_to_type_symbol, tokenToString(operator)); + return false; + } + return true; } - - /** - * Gets the *yield*, *return*, and *next* types of an `Iterable`-like or `AsyncIterable`-like - * type from from common heuristics. - * - * If we previously analyzed this type and found no iteration types, `noIterationTypes` is - * returned. If we found iteration types, an `IterationTypes` record is returned. - * Otherwise, we return `undefined` to indicate to the caller it should perform a more - * exhaustive analysis. - * - * NOTE: You probably don't want to call this directly and should be calling - * `getIterationTypesOfIterable` instead. - */ - function getIterationTypesOfIterableFast(type: Type, resolver: IterationTypesResolver) { - // As an optimization, if the type is an instantiation of one of the following global types, then - // just grab its related type argument: - // - `Iterable` or `AsyncIterable` - // - `IterableIterator` or `AsyncIterableIterator` - let globalType: Type; - if (isReferenceToType(type, globalType = resolver.getGlobalIterableType(/*reportErrors*/ false)) || - isReferenceToType(type, globalType = resolver.getGlobalIterableIteratorType(/*reportErrors*/ false))) { - const [yieldType] = getTypeArguments(type as GenericType); - // The "return" and "next" types of `Iterable` and `IterableIterator` are defined by the - // iteration types of their `[Symbol.iterator]()` method. The same is true for their async cousins. - // While we define these as `any` and `undefined` in our libs by default, a custom lib *could* use - // different definitions. - const { returnType, nextType } = getIterationTypesOfGlobalIterableType(globalType, resolver); - return setCachedIterationTypes(type, resolver.iterableCacheKey, createIterationTypes(yieldType, returnType, nextType)); + function getSuggestedBooleanOperator(operator: SyntaxKind): SyntaxKind | undefined { + switch (operator) { + case SyntaxKind.BarToken: + case SyntaxKind.BarEqualsToken: + return SyntaxKind.BarBarToken; + case SyntaxKind.CaretToken: + case SyntaxKind.CaretEqualsToken: + return SyntaxKind.ExclamationEqualsEqualsToken; + case SyntaxKind.AmpersandToken: + case SyntaxKind.AmpersandEqualsToken: + return SyntaxKind.AmpersandAmpersandToken; + default: + return undefined; } - - // As an optimization, if the type is an instantiation of the following global type, then - // just grab its related type arguments: - // - `Generator` or `AsyncGenerator` - if (isReferenceToType(type, resolver.getGlobalGeneratorType(/*reportErrors*/ false))) { - const [yieldType, returnType, nextType] = getTypeArguments(type as GenericType); - return setCachedIterationTypes(type, resolver.iterableCacheKey, createIterationTypes(yieldType, returnType, nextType)); + } + function checkAssignmentOperator(valueType: ts.Type): void { + if (produceDiagnostics && isAssignmentOperator(operator)) { + // TypeScript 1.0 spec (April 2014): 4.17 + // An assignment of the form + // VarExpr = ValueExpr + // requires VarExpr to be classified as a reference + // A compound assignment furthermore requires VarExpr to be classified as a reference (section 4.1) + // and the type of the non-compound operation to be assignable to the type of VarExpr. + if (checkReferenceExpression(left, Diagnostics.The_left_hand_side_of_an_assignment_expression_must_be_a_variable_or_a_property_access, Diagnostics.The_left_hand_side_of_an_assignment_expression_may_not_be_an_optional_property_access) + && (!isIdentifier(left) || unescapeLeadingUnderscores(left.escapedText) !== "exports")) { + // to avoid cascading errors check assignability only if 'isReference' check succeeded and no errors were reported + checkTypeAssignableToAndOptionallyElaborate(valueType, leftType, left, right); + } + } + } + function isAssignmentDeclaration(kind: AssignmentDeclarationKind) { + switch (kind) { + case AssignmentDeclarationKind.ModuleExports: + return true; + case AssignmentDeclarationKind.ExportsProperty: + case AssignmentDeclarationKind.Property: + case AssignmentDeclarationKind.Prototype: + case AssignmentDeclarationKind.PrototypeProperty: + case AssignmentDeclarationKind.ThisProperty: + const symbol = getSymbolOfNode(left); + const init = getAssignedExpandoInitializer(right); + return init && isObjectLiteralExpression(init) && + symbol && hasEntries(symbol.exports); + default: + return false; } } - /** - * Gets the *yield*, *return*, and *next* types of an `Iterable`-like or `AsyncIterable`-like - * type from its members. - * - * If we successfully found the *yield*, *return*, and *next* types, an `IterationTypes` - * record is returned. Otherwise, `noIterationTypes` is returned. - * - * NOTE: You probably don't want to call this directly and should be calling - * `getIterationTypesOfIterable` instead. + * Returns true if an error is reported */ - function getIterationTypesOfIterableSlow(type: Type, resolver: IterationTypesResolver, errorNode: Node | undefined) { - const method = getPropertyOfType(type, getPropertyNameForKnownSymbolName(resolver.iteratorSymbolName)); - const methodType = method && !(method.flags & SymbolFlags.Optional) ? getTypeOfSymbol(method) : undefined; - if (isTypeAny(methodType)) { - return setCachedIterationTypes(type, resolver.iterableCacheKey, anyIterationTypes); + function reportOperatorErrorUnless(typesAreCompatible: (left: ts.Type, right: ts.Type) => boolean): boolean { + if (!typesAreCompatible(leftType, rightType)) { + reportOperatorError(typesAreCompatible); + return true; } - - const signatures = methodType ? getSignaturesOfType(methodType, SignatureKind.Call) : undefined; - if (!some(signatures)) { - return setCachedIterationTypes(type, resolver.iterableCacheKey, noIterationTypes); + return false; + } + function reportOperatorError(isRelated?: (left: ts.Type, right: ts.Type) => boolean) { + let wouldWorkWithAwait = false; + const errNode = errorNode || operatorToken; + if (isRelated) { + const awaitedLeftType = getAwaitedType(leftType); + const awaitedRightType = getAwaitedType(rightType); + wouldWorkWithAwait = !(awaitedLeftType === leftType && awaitedRightType === rightType) + && !!(awaitedLeftType && awaitedRightType) + && isRelated(awaitedLeftType, awaitedRightType); + } + let effectiveLeft = leftType; + let effectiveRight = rightType; + if (!wouldWorkWithAwait && isRelated) { + [effectiveLeft, effectiveRight] = getBaseTypesIfUnrelated(leftType, rightType, isRelated); + } + const [leftStr, rightStr] = getTypeNamesForErrorDisplay(effectiveLeft, effectiveRight); + if (!tryGiveBetterPrimaryError(errNode, wouldWorkWithAwait, leftStr, rightStr)) { + errorAndMaybeSuggestAwait(errNode, wouldWorkWithAwait, Diagnostics.Operator_0_cannot_be_applied_to_types_1_and_2, tokenToString(operatorToken.kind), leftStr, rightStr); } - - const iteratorType = getUnionType(map(signatures, getReturnTypeOfSignature), UnionReduction.Subtype); - const iterationTypes = getIterationTypesOfIterator(iteratorType, resolver, errorNode) ?? noIterationTypes; - return setCachedIterationTypes(type, resolver.iterableCacheKey, iterationTypes); } - - function reportTypeNotIterableError(errorNode: Node, type: Type, allowAsyncIterables: boolean): void { - const message = allowAsyncIterables - ? Diagnostics.Type_0_must_have_a_Symbol_asyncIterator_method_that_returns_an_async_iterator - : Diagnostics.Type_0_must_have_a_Symbol_iterator_method_that_returns_an_iterator; - errorAndMaybeSuggestAwait(errorNode, !!getAwaitedTypeOfPromise(type), message, typeToString(type)); + function tryGiveBetterPrimaryError(errNode: Node, maybeMissingAwait: boolean, leftStr: string, rightStr: string) { + let typeName: string | undefined; + switch (operatorToken.kind) { + case SyntaxKind.EqualsEqualsEqualsToken: + case SyntaxKind.EqualsEqualsToken: + typeName = "false"; + break; + case SyntaxKind.ExclamationEqualsEqualsToken: + case SyntaxKind.ExclamationEqualsToken: + typeName = "true"; + } + if (typeName) { + return errorAndMaybeSuggestAwait(errNode, maybeMissingAwait, Diagnostics.This_condition_will_always_return_0_since_the_types_1_and_2_have_no_overlap, typeName, leftStr, rightStr); + } + return undefined; } - - /** - * Gets the *yield*, *return*, and *next* types from an `Iterator`-like or `AsyncIterator`-like type. - * - * If we successfully found the *yield*, *return*, and *next* types, an `IterationTypes` - * record is returned. Otherwise, `undefined` is returned. - */ - function getIterationTypesOfIterator(type: Type, resolver: IterationTypesResolver, errorNode: Node | undefined) { - if (isTypeAny(type)) { - return anyIterationTypes; + } + function getBaseTypesIfUnrelated(leftType: ts.Type, rightType: ts.Type, isRelated: (left: ts.Type, right: ts.Type) => boolean): [ts.Type, ts.Type] { + let effectiveLeft = leftType; + let effectiveRight = rightType; + const leftBase = getBaseTypeOfLiteralType(leftType); + const rightBase = getBaseTypeOfLiteralType(rightType); + if (!isRelated(leftBase, rightBase)) { + effectiveLeft = leftBase; + effectiveRight = rightBase; + } + return [effectiveLeft, effectiveRight]; + } + function checkYieldExpression(node: YieldExpression): ts.Type { + // Grammar checking + if (produceDiagnostics) { + if (!(node.flags & NodeFlags.YieldContext)) { + grammarErrorOnFirstToken(node, Diagnostics.A_yield_expression_is_only_allowed_in_a_generator_body); + } + if (isInParameterInitializerBeforeContainingFunction(node)) { + error(node, Diagnostics.yield_expressions_cannot_be_used_in_a_parameter_initializer); } - - const iterationTypes = - getIterationTypesOfIteratorCached(type, resolver) || - getIterationTypesOfIteratorFast(type, resolver) || - getIterationTypesOfIteratorSlow(type, resolver, errorNode); - return iterationTypes === noIterationTypes ? undefined : iterationTypes; } - - /** - * Gets the iteration types of an `Iterator`-like or `AsyncIterator`-like type from the - * cache. - * - * NOTE: You probably don't want to call this directly and should be calling - * `getIterationTypesOfIterator` instead. - */ - function getIterationTypesOfIteratorCached(type: Type, resolver: IterationTypesResolver) { - return getCachedIterationTypes(type, resolver.iteratorCacheKey); + const func = getContainingFunction(node); + if (!func) + return anyType; + const functionFlags = getFunctionFlags(func); + if (!(functionFlags & FunctionFlags.Generator)) { + // If the user's code is syntactically correct, the func should always have a star. After all, we are in a yield context. + return anyType; } - - /** - * Gets the iteration types of an `Iterator`-like or `AsyncIterator`-like type from the - * cache or from common heuristics. - * - * If we previously analyzed this type and found no iteration types, `noIterationTypes` is - * returned. If we found iteration types, an `IterationTypes` record is returned. - * Otherwise, we return `undefined` to indicate to the caller it should perform a more - * exhaustive analysis. - * - * NOTE: You probably don't want to call this directly and should be calling - * `getIterationTypesOfIterator` instead. - */ - function getIterationTypesOfIteratorFast(type: Type, resolver: IterationTypesResolver) { - // As an optimization, if the type is an instantiation of one of the following global types, - // then just grab its related type argument: - // - `IterableIterator` or `AsyncIterableIterator` - // - `Iterator` or `AsyncIterator` - // - `Generator` or `AsyncGenerator` - const globalType = resolver.getGlobalIterableIteratorType(/*reportErrors*/ false); - if (isReferenceToType(type, globalType)) { - const [yieldType] = getTypeArguments(type as GenericType); - // The "return" and "next" types of `IterableIterator` and `AsyncIterableIterator` are defined by the - // iteration types of their `next`, `return`, and `throw` methods. While we define these as `any` - // and `undefined` in our libs by default, a custom lib *could* use different definitions. - const globalIterationTypes = - getIterationTypesOfIteratorCached(globalType, resolver) || - getIterationTypesOfIteratorSlow(globalType, resolver, /*errorNode*/ undefined); - const { returnType, nextType } = globalIterationTypes === noIterationTypes ? defaultIterationTypes : globalIterationTypes; - return setCachedIterationTypes(type, resolver.iteratorCacheKey, createIterationTypes(yieldType, returnType, nextType)); - } - if (isReferenceToType(type, resolver.getGlobalIteratorType(/*reportErrors*/ false)) || - isReferenceToType(type, resolver.getGlobalGeneratorType(/*reportErrors*/ false))) { - const [yieldType, returnType, nextType] = getTypeArguments(type as GenericType); - return setCachedIterationTypes(type, resolver.iteratorCacheKey, createIterationTypes(yieldType, returnType, nextType)); + const isAsync = (functionFlags & FunctionFlags.Async) !== 0; + if (node.asteriskToken) { + // Async generator functions prior to ESNext require the __await, __asyncDelegator, + // and __asyncValues helpers + if (isAsync && languageVersion < ScriptTarget.ESNext) { + checkExternalEmitHelpers(node, ExternalEmitHelpers.AsyncDelegatorIncludes); + } + // Generator functions prior to ES2015 require the __values helper + if (!isAsync && languageVersion < ScriptTarget.ES2015 && compilerOptions.downlevelIteration) { + checkExternalEmitHelpers(node, ExternalEmitHelpers.Values); + } + } + // There is no point in doing an assignability check if the function + // has no explicit return type because the return type is directly computed + // from the yield expressions. + const returnType = getReturnTypeFromAnnotation(func); + const iterationTypes = returnType && getIterationTypesOfGeneratorFunctionReturnType(returnType, isAsync); + const signatureYieldType = iterationTypes && iterationTypes.yieldType || anyType; + const signatureNextType = iterationTypes && iterationTypes.nextType || anyType; + const resolvedSignatureNextType = isAsync ? getAwaitedType(signatureNextType) || anyType : signatureNextType; + const yieldExpressionType = node.expression ? checkExpression(node.expression) : undefinedWideningType; + const yieldedType = getYieldedTypeOfYieldExpression(node, yieldExpressionType, resolvedSignatureNextType, isAsync); + if (returnType && yieldedType) { + checkTypeAssignableToAndOptionallyElaborate(yieldedType, signatureYieldType, node.expression || node, node.expression); + } + if (node.asteriskToken) { + const use = isAsync ? IterationUse.AsyncYieldStar : IterationUse.YieldStar; + return getIterationTypeOfIterable(use, IterationTypeKind.Return, yieldExpressionType, node.expression) + || anyType; + } + else if (returnType) { + return getIterationTypeOfGeneratorFunctionReturnType(IterationTypeKind.Next, returnType, isAsync) + || anyType; + } + return getContextualIterationType(IterationTypeKind.Next, func) || anyType; + } + function checkConditionalExpression(node: ConditionalExpression, checkMode?: CheckMode): ts.Type { + const type = checkTruthinessExpression(node.condition); + checkTestingKnownTruthyCallableType(node.condition, node.whenTrue, type); + const type1 = checkExpression(node.whenTrue, checkMode); + const type2 = checkExpression(node.whenFalse, checkMode); + return getUnionType([type1, type2], UnionReduction.Subtype); + } + function checkTemplateExpression(node: TemplateExpression): ts.Type { + // We just want to check each expressions, but we are unconcerned with + // the type of each expression, as any value may be coerced into a string. + // It is worth asking whether this is what we really want though. + // A place where we actually *are* concerned with the expressions' types are + // in tagged templates. + forEach(node.templateSpans, templateSpan => { + if (maybeTypeOfKind(checkExpression(templateSpan.expression), TypeFlags.ESSymbolLike)) { + error(templateSpan.expression, Diagnostics.Implicit_conversion_of_a_symbol_to_a_string_will_fail_at_runtime_Consider_wrapping_this_expression_in_String); } + }); + return stringType; + } + function getContextNode(node: Expression): Node { + if (node.kind === SyntaxKind.JsxAttributes && !isJsxSelfClosingElement(node.parent)) { + return node.parent.parent; // Needs to be the root JsxElement, so it encompasses the attributes _and_ the children (which are essentially part of the attributes) } - - function isIteratorResult(type: Type, kind: IterationTypeKind.Yield | IterationTypeKind.Return) { - // From https://tc39.github.io/ecma262/#sec-iteratorresult-interface: - // > [done] is the result status of an iterator `next` method call. If the end of the iterator was reached `done` is `true`. - // > If the end was not reached `done` is `false` and a value is available. - // > If a `done` property (either own or inherited) does not exist, it is consider to have the value `false`. - const doneType = getTypeOfPropertyOfType(type, "done" as __String) || falseType; - return isTypeAssignableTo(kind === IterationTypeKind.Yield ? falseType : trueType, doneType); - } - - function isYieldIteratorResult(type: Type) { - return isIteratorResult(type, IterationTypeKind.Yield); + return node; + } + function checkExpressionWithContextualType(node: Expression, contextualType: ts.Type, inferenceContext: InferenceContext | undefined, checkMode: CheckMode): ts.Type { + const context = getContextNode(node); + const saveContextualType = context.contextualType; + const saveInferenceContext = context.inferenceContext; + try { + context.contextualType = contextualType; + context.inferenceContext = inferenceContext; + const type = checkExpression(node, checkMode | CheckMode.Contextual | (inferenceContext ? CheckMode.Inferential : 0)); + // We strip literal freshness when an appropriate contextual type is present such that contextually typed + // literals always preserve their literal types (otherwise they might widen during type inference). An alternative + // here would be to not mark contextually typed literals as fresh in the first place. + const result = maybeTypeOfKind(type, TypeFlags.Literal) && isLiteralOfContextualType(type, instantiateContextualType(contextualType, node)) ? + getRegularTypeOfLiteralType(type) : type; + return result; } - - function isReturnIteratorResult(type: Type) { - return isIteratorResult(type, IterationTypeKind.Return); + finally { + // In the event our operation is canceled or some other exception occurs, reset the contextual type + // so that we do not accidentally hold onto an instance of the checker, as a Type created in the services layer + // may hold onto the checker that created it. + context.contextualType = saveContextualType; + context.inferenceContext = saveInferenceContext; } - - /** - * Gets the *yield* and *return* types of an `IteratorResult`-like type. - * - * If we are unable to determine a *yield* or a *return* type, `noIterationTypes` is - * returned to indicate to the caller that it should handle the error. Otherwise, an - * `IterationTypes` record is returned. - */ - function getIterationTypesOfIteratorResult(type: Type) { - if (isTypeAny(type)) { - return anyIterationTypes; - } - - const cachedTypes = getCachedIterationTypes(type, "iterationTypesOfIteratorResult"); - if (cachedTypes) { - return cachedTypes; - } - - // As an optimization, if the type is an instantiation of one of the global `IteratorYieldResult` - // or `IteratorReturnResult` types, then just grab its type argument. - if (isReferenceToType(type, getGlobalIteratorYieldResultType(/*reportErrors*/ false))) { - const yieldType = getTypeArguments(type as GenericType)[0]; - return setCachedIterationTypes(type, "iterationTypesOfIteratorResult", createIterationTypes(yieldType, /*returnType*/ undefined, /*nextType*/ undefined)); - } - if (isReferenceToType(type, getGlobalIteratorReturnResultType(/*reportErrors*/ false))) { - const returnType = getTypeArguments(type as GenericType)[0]; - return setCachedIterationTypes(type, "iterationTypesOfIteratorResult", createIterationTypes(/*yieldType*/ undefined, returnType, /*nextType*/ undefined)); - } - - // Choose any constituents that can produce the requested iteration type. - const yieldIteratorResult = filterType(type, isYieldIteratorResult); - const yieldType = yieldIteratorResult !== neverType ? getTypeOfPropertyOfType(yieldIteratorResult, "value" as __String) : undefined; - - const returnIteratorResult = filterType(type, isReturnIteratorResult); - const returnType = returnIteratorResult !== neverType ? getTypeOfPropertyOfType(returnIteratorResult, "value" as __String) : undefined; - - if (!yieldType && !returnType) { - return setCachedIterationTypes(type, "iterationTypesOfIteratorResult", noIterationTypes); + } + function checkExpressionCached(node: Expression | QualifiedName, checkMode?: CheckMode): ts.Type { + const links = getNodeLinks(node); + if (!links.resolvedType) { + if (checkMode && checkMode !== CheckMode.Normal) { + return checkExpression(node, checkMode); + } + // When computing a type that we're going to cache, we need to ignore any ongoing control flow + // analysis because variables may have transient types in indeterminable states. Moving flowLoopStart + // to the top of the stack ensures all transient types are computed from a known point. + const saveFlowLoopStart = flowLoopStart; + const saveFlowTypeCache = flowTypeCache; + flowLoopStart = flowLoopCount; + flowTypeCache = undefined; + links.resolvedType = checkExpression(node, checkMode); + flowTypeCache = saveFlowTypeCache; + flowLoopStart = saveFlowLoopStart; + } + return links.resolvedType; + } + function isTypeAssertion(node: Expression) { + node = skipParentheses(node); + return node.kind === SyntaxKind.TypeAssertionExpression || node.kind === SyntaxKind.AsExpression; + } + function checkDeclarationInitializer(declaration: HasExpressionInitializer, contextualType?: ts.Type | undefined) { + const initializer = (getEffectiveInitializer(declaration)!); + const type = getQuickTypeOfExpression(initializer) || + (contextualType ? checkExpressionWithContextualType(initializer, contextualType, /*inferenceContext*/ undefined, CheckMode.Normal) : checkExpressionCached(initializer)); + return isParameter(declaration) && declaration.name.kind === SyntaxKind.ArrayBindingPattern && + isTupleType(type) && !type.target.hasRestElement && getTypeReferenceArity(type) < declaration.name.elements.length ? + padTupleType(type, declaration.name) : type; + } + function padTupleType(type: TupleTypeReference, pattern: ArrayBindingPattern) { + const patternElements = pattern.elements; + const arity = getTypeReferenceArity(type); + const elementTypes = arity ? getTypeArguments(type).slice() : []; + for (let i = arity; i < patternElements.length; i++) { + const e = patternElements[i]; + if (i < patternElements.length - 1 || !(e.kind === SyntaxKind.BindingElement && e.dotDotDotToken)) { + elementTypes.push(!isOmittedExpression(e) && hasDefaultValue(e) ? getTypeFromBindingElement(e, /*includePatternInType*/ false, /*reportErrors*/ false) : anyType); + if (!isOmittedExpression(e) && !hasDefaultValue(e)) { + reportImplicitAny(e, anyType); + } } - - // From https://tc39.github.io/ecma262/#sec-iteratorresult-interface - // > ... If the iterator does not have a return value, `value` is `undefined`. In that case, the - // > `value` property may be absent from the conforming object if it does not inherit an explicit - // > `value` property. - return setCachedIterationTypes(type, "iterationTypesOfIteratorResult", createIterationTypes(yieldType, returnType || voidType, /*nextType*/ undefined)); } - - /** - * Gets the *yield*, *return*, and *next* types of a the `next()`, `return()`, or - * `throw()` method of an `Iterator`-like or `AsyncIterator`-like type. - * - * If we successfully found the *yield*, *return*, and *next* types, an `IterationTypes` - * record is returned. Otherwise, we return `undefined`. - */ - function getIterationTypesOfMethod(type: Type, resolver: IterationTypesResolver, methodName: "next" | "return" | "throw", errorNode: Node | undefined): IterationTypes | undefined { - const method = getPropertyOfType(type, methodName as __String); - - // Ignore 'return' or 'throw' if they are missing. - if (!method && methodName !== "next") { - return undefined; + return createTupleType(elementTypes, type.target.minLength, /*hasRestElement*/ false, type.target.readonly); + } + function widenTypeInferredFromInitializer(declaration: HasExpressionInitializer, type: ts.Type) { + const widened = getCombinedNodeFlags(declaration) & NodeFlags.Const || isDeclarationReadonly(declaration) ? type : getWidenedLiteralType(type); + if (isInJSFile(declaration)) { + if (widened.flags & TypeFlags.Nullable) { + reportImplicitAny(declaration, anyType); + return anyType; } - - const methodType = method && !(methodName === "next" && (method.flags & SymbolFlags.Optional)) - ? methodName === "next" ? getTypeOfSymbol(method) : getTypeWithFacts(getTypeOfSymbol(method), TypeFacts.NEUndefinedOrNull) - : undefined; - - if (isTypeAny(methodType)) { - // `return()` and `throw()` don't provide a *next* type. - return methodName === "next" ? anyIterationTypes : anyIterationTypesExceptNext; + else if (isEmptyArrayLiteralType(widened)) { + reportImplicitAny(declaration, anyArrayType); + return anyArrayType; } - - // Both async and non-async iterators *must* have a `next` method. - const methodSignatures = methodType ? getSignaturesOfType(methodType, SignatureKind.Call) : emptyArray; - if (methodSignatures.length === 0) { - if (errorNode) { - const diagnostic = methodName === "next" - ? resolver.mustHaveANextMethodDiagnostic - : resolver.mustBeAMethodDiagnostic; - error(errorNode, diagnostic, methodName); + } + return widened; + } + function isLiteralOfContextualType(candidateType: ts.Type, contextualType: ts.Type | undefined): boolean { + if (contextualType) { + if (contextualType.flags & TypeFlags.UnionOrIntersection) { + const types = (contextualType).types; + return some(types, t => isLiteralOfContextualType(candidateType, t)); + } + if (contextualType.flags & TypeFlags.InstantiableNonPrimitive) { + // If the contextual type is a type variable constrained to a primitive type, consider + // this a literal context for literals of that primitive type. For example, given a + // type parameter 'T extends string', infer string literal types for T. + const constraint = getBaseConstraintOfType(contextualType) || unknownType; + return maybeTypeOfKind(constraint, TypeFlags.String) && maybeTypeOfKind(candidateType, TypeFlags.StringLiteral) || + maybeTypeOfKind(constraint, TypeFlags.Number) && maybeTypeOfKind(candidateType, TypeFlags.NumberLiteral) || + maybeTypeOfKind(constraint, TypeFlags.BigInt) && maybeTypeOfKind(candidateType, TypeFlags.BigIntLiteral) || + maybeTypeOfKind(constraint, TypeFlags.ESSymbol) && maybeTypeOfKind(candidateType, TypeFlags.UniqueESSymbol) || + isLiteralOfContextualType(candidateType, constraint); + } + // If the contextual type is a literal of a particular primitive type, we consider this a + // literal context for all literals of that primitive type. + return !!(contextualType.flags & (TypeFlags.StringLiteral | TypeFlags.Index) && maybeTypeOfKind(candidateType, TypeFlags.StringLiteral) || + contextualType.flags & TypeFlags.NumberLiteral && maybeTypeOfKind(candidateType, TypeFlags.NumberLiteral) || + contextualType.flags & TypeFlags.BigIntLiteral && maybeTypeOfKind(candidateType, TypeFlags.BigIntLiteral) || + contextualType.flags & TypeFlags.BooleanLiteral && maybeTypeOfKind(candidateType, TypeFlags.BooleanLiteral) || + contextualType.flags & TypeFlags.UniqueESSymbol && maybeTypeOfKind(candidateType, TypeFlags.UniqueESSymbol)); + } + return false; + } + function isConstContext(node: Expression): boolean { + const parent = node.parent; + return isAssertionExpression(parent) && isConstTypeReference(parent.type) || + (isParenthesizedExpression(parent) || isArrayLiteralExpression(parent) || isSpreadElement(parent)) && isConstContext(parent) || + (isPropertyAssignment(parent) || isShorthandPropertyAssignment(parent)) && isConstContext(parent.parent); + } + function checkExpressionForMutableLocation(node: Expression, checkMode: CheckMode | undefined, contextualType?: ts.Type, forceTuple?: boolean): ts.Type { + const type = checkExpression(node, checkMode, forceTuple); + return isConstContext(node) ? getRegularTypeOfLiteralType(type) : + isTypeAssertion(node) ? type : + getWidenedLiteralLikeTypeForContextualType(type, instantiateContextualType(arguments.length === 2 ? getContextualType(node) : contextualType, node)); + } + function checkPropertyAssignment(node: PropertyAssignment, checkMode?: CheckMode): ts.Type { + // Do not use hasDynamicName here, because that returns false for well known symbols. + // We want to perform checkComputedPropertyName for all computed properties, including + // well known symbols. + if (node.name.kind === SyntaxKind.ComputedPropertyName) { + checkComputedPropertyName(node.name); + } + return checkExpressionForMutableLocation(node.initializer, checkMode); + } + function checkObjectLiteralMethod(node: MethodDeclaration, checkMode?: CheckMode): ts.Type { + // Grammar checking + checkGrammarMethod(node); + // Do not use hasDynamicName here, because that returns false for well known symbols. + // We want to perform checkComputedPropertyName for all computed properties, including + // well known symbols. + if (node.name.kind === SyntaxKind.ComputedPropertyName) { + checkComputedPropertyName(node.name); + } + const uninstantiatedType = checkFunctionExpressionOrObjectLiteralMethod(node, checkMode); + return instantiateTypeWithSingleGenericCallSignature(node, uninstantiatedType, checkMode); + } + function instantiateTypeWithSingleGenericCallSignature(node: Expression | MethodDeclaration | QualifiedName, type: ts.Type, checkMode?: CheckMode) { + if (checkMode && checkMode & (CheckMode.Inferential | CheckMode.SkipGenericFunctions)) { + const callSignature = getSingleSignature(type, SignatureKind.Call, /*allowMembers*/ true); + const constructSignature = getSingleSignature(type, SignatureKind.Construct, /*allowMembers*/ true); + const signature = callSignature || constructSignature; + if (signature && signature.typeParameters) { + const contextualType = getApparentTypeOfContextualType((node), ContextFlags.NoConstraints); + if (contextualType) { + const contextualSignature = getSingleSignature(getNonNullableType(contextualType), callSignature ? SignatureKind.Call : SignatureKind.Construct, /*allowMembers*/ false); + if (contextualSignature && !contextualSignature.typeParameters) { + if (checkMode & CheckMode.SkipGenericFunctions) { + skippedGenericFunction(node, checkMode); + return anyFunctionType; + } + const context = getInferenceContext(node)!; + // We have an expression that is an argument of a generic function for which we are performing + // type argument inference. The expression is of a function type with a single generic call + // signature and a contextual function type with a single non-generic call signature. Now check + // if the outer function returns a function type with a single non-generic call signature and + // if some of the outer function type parameters have no inferences so far. If so, we can + // potentially add inferred type parameters to the outer function return type. + const returnType = context.signature && getReturnTypeOfSignature(context.signature); + const returnSignature = returnType && getSingleCallOrConstructSignature(returnType); + if (returnSignature && !returnSignature.typeParameters && !every(context.inferences, hasInferenceCandidates)) { + // Instantiate the signature with its own type parameters as type arguments, possibly + // renaming the type parameters to ensure they have unique names. + const uniqueTypeParameters = getUniqueTypeParameters(context, signature.typeParameters); + const instantiatedSignature = getSignatureInstantiationWithoutFillingInTypeArguments(signature, uniqueTypeParameters); + // Infer from the parameters of the instantiated signature to the parameters of the + // contextual signature starting with an empty set of inference candidates. + const inferences = map(context.inferences, info => createInferenceInfo(info.typeParameter)); + applyToParameterTypes(instantiatedSignature, contextualSignature, (source, target) => { + inferTypes(inferences, source, target, /*priority*/ 0, /*contravariant*/ true); + }); + if (some(inferences, hasInferenceCandidates)) { + // We have inference candidates, indicating that one or more type parameters are referenced + // in the parameter types of the contextual signature. Now also infer from the return type. + applyToReturnTypes(instantiatedSignature, contextualSignature, (source, target) => { + inferTypes(inferences, source, target); + }); + // If the type parameters for which we produced candidates do not have any inferences yet, + // we adopt the new inference candidates and add the type parameters of the expression type + // to the set of inferred type parameters for the outer function return type. + if (!hasOverlappingInferences(context.inferences, inferences)) { + mergeInferences(context.inferences, inferences); + context.inferredTypeParameters = concatenate(context.inferredTypeParameters, uniqueTypeParameters); + return getOrCreateTypeFromSignature(instantiatedSignature); + } + } + } + return getOrCreateTypeFromSignature(instantiateSignatureInContextOf(signature, contextualSignature, context)); + } } - return methodName === "next" ? anyIterationTypes : undefined; } - - // Extract the first parameter and return type of each signature. - let methodParameterTypes: Type[] | undefined; - let methodReturnTypes: Type[] | undefined; - for (const signature of methodSignatures) { - if (methodName !== "throw" && some(signature.parameters)) { - methodParameterTypes = append(methodParameterTypes, getTypeAtPosition(signature, 0)); - } - methodReturnTypes = append(methodReturnTypes, getReturnTypeOfSignature(signature)); + } + return type; + } + function skippedGenericFunction(node: Node, checkMode: CheckMode) { + if (checkMode & CheckMode.Inferential) { + // We have skipped a generic function during inferential typing. Obtain the inference context and + // indicate this has occurred such that we know a second pass of inference is be needed. + const context = getInferenceContext(node)!; + context.flags |= InferenceFlags.SkippedGenericFunction; + } + } + function hasInferenceCandidates(info: InferenceInfo) { + return !!(info.candidates || info.contraCandidates); + } + function hasOverlappingInferences(a: InferenceInfo[], b: InferenceInfo[]) { + for (let i = 0; i < a.length; i++) { + if (hasInferenceCandidates(a[i]) && hasInferenceCandidates(b[i])) { + return true; } - - // Resolve the *next* or *return* type from the first parameter of a `next()` or - // `return()` method, respectively. - let returnTypes: Type[] | undefined; - let nextType: Type | undefined; - if (methodName !== "throw") { - const methodParameterType = methodParameterTypes ? getUnionType(methodParameterTypes) : unknownType; - if (methodName === "next") { - // The value of `next(value)` is *not* awaited by async generators - nextType = methodParameterType; - } - else if (methodName === "return") { - // The value of `return(value)` *is* awaited by async generators - const resolvedMethodParameterType = resolver.resolveIterationType(methodParameterType, errorNode) || anyType; - returnTypes = append(returnTypes, resolvedMethodParameterType); - } + } + return false; + } + function mergeInferences(target: InferenceInfo[], source: InferenceInfo[]) { + for (let i = 0; i < target.length; i++) { + if (!hasInferenceCandidates(target[i]) && hasInferenceCandidates(source[i])) { + target[i] = source[i]; } - - // Resolve the *yield* and *return* types from the return type of the method (i.e. `IteratorResult`) - let yieldType: Type; - const methodReturnType = methodReturnTypes ? getUnionType(methodReturnTypes, UnionReduction.Subtype) : neverType; - const resolvedMethodReturnType = resolver.resolveIterationType(methodReturnType, errorNode) || anyType; - const iterationTypes = getIterationTypesOfIteratorResult(resolvedMethodReturnType); - if (iterationTypes === noIterationTypes) { - if (errorNode) { - error(errorNode, resolver.mustHaveAValueDiagnostic, methodName); - } - yieldType = anyType; - returnTypes = append(returnTypes, anyType); + } + } + function getUniqueTypeParameters(context: InferenceContext, typeParameters: readonly TypeParameter[]): readonly TypeParameter[] { + const result: TypeParameter[] = []; + let oldTypeParameters: TypeParameter[] | undefined; + let newTypeParameters: TypeParameter[] | undefined; + for (const tp of typeParameters) { + const name = tp.symbol.escapedName; + if (hasTypeParameterByName(context.inferredTypeParameters, name) || hasTypeParameterByName(result, name)) { + const newName = getUniqueTypeParameterName(concatenate(context.inferredTypeParameters, result), name); + const symbol = createSymbol(SymbolFlags.TypeParameter, newName); + const newTypeParameter = createTypeParameter(symbol); + newTypeParameter.target = tp; + oldTypeParameters = append(oldTypeParameters, tp); + newTypeParameters = append(newTypeParameters, newTypeParameter); + result.push(newTypeParameter); } else { - yieldType = iterationTypes.yieldType; - returnTypes = append(returnTypes, iterationTypes.returnType); + result.push(tp); } - - return createIterationTypes(yieldType, getUnionType(returnTypes), nextType); } - - /** - * Gets the *yield*, *return*, and *next* types of an `Iterator`-like or `AsyncIterator`-like - * type from its members. - * - * If we successfully found the *yield*, *return*, and *next* types, an `IterationTypes` - * record is returned. Otherwise, `noIterationTypes` is returned. - * - * NOTE: You probably don't want to call this directly and should be calling - * `getIterationTypesOfIterator` instead. - */ - function getIterationTypesOfIteratorSlow(type: Type, resolver: IterationTypesResolver, errorNode: Node | undefined) { - const iterationTypes = combineIterationTypes([ - getIterationTypesOfMethod(type, resolver, "next", errorNode), - getIterationTypesOfMethod(type, resolver, "return", errorNode), - getIterationTypesOfMethod(type, resolver, "throw", errorNode), - ]); - return setCachedIterationTypes(type, resolver.iteratorCacheKey, iterationTypes); + if (newTypeParameters) { + const mapper = createTypeMapper(oldTypeParameters!, newTypeParameters); + for (const tp of newTypeParameters) { + tp.mapper = mapper; + } } - - /** - * Gets the requested "iteration type" from a type that is either `Iterable`-like, `Iterator`-like, - * `IterableIterator`-like, or `Generator`-like (for a non-async generator); or `AsyncIterable`-like, - * `AsyncIterator`-like, `AsyncIterableIterator`-like, or `AsyncGenerator`-like (for an async generator). - */ - function getIterationTypeOfGeneratorFunctionReturnType(kind: IterationTypeKind, returnType: Type, isAsyncGenerator: boolean): Type | undefined { - if (isTypeAny(returnType)) { - return undefined; + return result; + } + function hasTypeParameterByName(typeParameters: readonly TypeParameter[] | undefined, name: __String) { + return some(typeParameters, tp => tp.symbol.escapedName === name); + } + function getUniqueTypeParameterName(typeParameters: readonly TypeParameter[], baseName: __String) { + let len = (baseName).length; + while (len > 1 && (baseName).charCodeAt(len - 1) >= CharacterCodes._0 && (baseName).charCodeAt(len - 1) <= CharacterCodes._9) + len--; + const s = (baseName).slice(0, len); + for (let index = 1; true; index++) { + const augmentedName = (<__String>(s + index)); + if (!hasTypeParameterByName(typeParameters, augmentedName)) { + return augmentedName; } - - const iterationTypes = getIterationTypesOfGeneratorFunctionReturnType(returnType, isAsyncGenerator); - return iterationTypes && iterationTypes[getIterationTypesKeyFromIterationTypeKind(kind)]; } - - function getIterationTypesOfGeneratorFunctionReturnType(type: Type, isAsyncGenerator: boolean) { - if (isTypeAny(type)) { - return anyIterationTypes; + } + function getReturnTypeOfSingleNonGenericCallSignature(funcType: ts.Type) { + const signature = getSingleCallSignature(funcType); + if (signature && !signature.typeParameters) { + return getReturnTypeOfSignature(signature); + } + } + function getReturnTypeOfSingleNonGenericSignatureOfCallChain(expr: CallChain) { + const funcType = checkExpression(expr.expression); + const nonOptionalType = getOptionalExpressionType(funcType, expr.expression); + const returnType = getReturnTypeOfSingleNonGenericCallSignature(funcType); + return returnType && propagateOptionalTypeMarker(returnType, expr, nonOptionalType !== funcType); + } + /** + * Returns the type of an expression. Unlike checkExpression, this function is simply concerned + * with computing the type and may not fully check all contained sub-expressions for errors. + */ + function getTypeOfExpression(node: Expression) { + // Don't bother caching types that require no flow analysis and are quick to compute. + const quickType = getQuickTypeOfExpression(node); + if (quickType) { + return quickType; + } + // If a type has been cached for the node, return it. + if (node.flags & NodeFlags.TypeCached && flowTypeCache) { + const cachedType = flowTypeCache[getNodeId(node)]; + if (cachedType) { + return cachedType; + } + } + const startInvocationCount = flowInvocationCount; + const type = checkExpression(node); + // If control flow analysis was required to determine the type, it is worth caching. + if (flowInvocationCount !== startInvocationCount) { + const cache = flowTypeCache || (flowTypeCache = []); + cache[getNodeId(node)] = type; + node.flags |= NodeFlags.TypeCached; + } + return type; + } + function getQuickTypeOfExpression(node: Expression) { + const expr = skipParentheses(node); + // Optimize for the common case of a call to a function with a single non-generic call + // signature where we can just fetch the return type without checking the arguments. + if (isCallExpression(expr) && expr.expression.kind !== SyntaxKind.SuperKeyword && !isRequireCall(expr, /*checkArgumentIsStringLiteralLike*/ true) && !isSymbolOrSymbolForCall(expr)) { + const type = isCallChain(expr) ? getReturnTypeOfSingleNonGenericSignatureOfCallChain(expr) : + getReturnTypeOfSingleNonGenericCallSignature(checkNonNullExpression(expr.expression)); + if (type) { + return type; } - - const use = isAsyncGenerator ? IterationUse.AsyncGeneratorReturnType : IterationUse.GeneratorReturnType; - const resolver = isAsyncGenerator ? asyncIterationTypesResolver : syncIterationTypesResolver; - return getIterationTypesOfIterable(type, use, /*errorNode*/ undefined) || - getIterationTypesOfIterator(type, resolver, /*errorNode*/ undefined); } - - function checkBreakOrContinueStatement(node: BreakOrContinueStatement) { - // Grammar checking - if (!checkGrammarStatementInAmbientContext(node)) checkGrammarBreakOrContinueStatement(node); - - // TODO: Check that target label is valid + else if (isAssertionExpression(expr) && !isConstTypeReference(expr.type)) { + return getTypeFromTypeNode((expr).type); } - - function unwrapReturnType(returnType: Type, functionFlags: FunctionFlags) { - const isGenerator = !!(functionFlags & FunctionFlags.Generator); - const isAsync = !!(functionFlags & FunctionFlags.Async); - return isGenerator ? getIterationTypeOfGeneratorFunctionReturnType(IterationTypeKind.Return, returnType, isAsync) || errorType : - isAsync ? getPromisedTypeOfPromise(returnType) || errorType : - returnType; + else if (node.kind === SyntaxKind.NumericLiteral || node.kind === SyntaxKind.StringLiteral || + node.kind === SyntaxKind.TrueKeyword || node.kind === SyntaxKind.FalseKeyword) { + return checkExpression(node); } - - function isUnwrappedReturnTypeVoidOrAny(func: SignatureDeclaration, returnType: Type): boolean { - const unwrappedReturnType = unwrapReturnType(returnType, getFunctionFlags(func)); - return !!unwrappedReturnType && maybeTypeOfKind(unwrappedReturnType, TypeFlags.Void | TypeFlags.AnyOrUnknown); + return undefined; + } + /** + * Returns the type of an expression. Unlike checkExpression, this function is simply concerned + * with computing the type and may not fully check all contained sub-expressions for errors. + * It is intended for uses where you know there is no contextual type, + * and requesting the contextual type might cause a circularity or other bad behaviour. + * It sets the contextual type of the node to any before calling getTypeOfExpression. + */ + function getContextFreeTypeOfExpression(node: Expression) { + const links = getNodeLinks(node); + if (links.contextFreeType) { + return links.contextFreeType; + } + const saveContextualType = node.contextualType; + node.contextualType = anyType; + try { + const type = links.contextFreeType = checkExpression(node, CheckMode.SkipContextSensitive); + return type; } - - function checkReturnStatement(node: ReturnStatement) { - // Grammar checking - if (checkGrammarStatementInAmbientContext(node)) { - return; - } - - const func = getContainingFunction(node); - if (!func) { - grammarErrorOnFirstToken(node, Diagnostics.A_return_statement_can_only_be_used_within_a_function_body); - return; + finally { + // In the event our operation is canceled or some other exception occurs, reset the contextual type + // so that we do not accidentally hold onto an instance of the checker, as a Type created in the services layer + // may hold onto the checker that created it. + node.contextualType = saveContextualType; + } + } + function checkExpression(node: Expression | QualifiedName, checkMode?: CheckMode, forceTuple?: boolean): ts.Type { + const saveCurrentNode = currentNode; + currentNode = node; + instantiationCount = 0; + const uninstantiatedType = checkExpressionWorker(node, checkMode, forceTuple); + const type = instantiateTypeWithSingleGenericCallSignature(node, uninstantiatedType, checkMode); + if (isConstEnumObjectType(type)) { + checkConstEnumAccess(node, type); + } + currentNode = saveCurrentNode; + return type; + } + function checkConstEnumAccess(node: Expression | QualifiedName, type: ts.Type) { + // enum object type for const enums are only permitted in: + // - 'left' in property access + // - 'object' in indexed access + // - target in rhs of import statement + const ok = (node.parent.kind === SyntaxKind.PropertyAccessExpression && (node.parent).expression === node) || + (node.parent.kind === SyntaxKind.ElementAccessExpression && (node.parent).expression === node) || + ((node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.QualifiedName) && isInRightSideOfImportOrExportAssignment((node)) || + (node.parent.kind === SyntaxKind.TypeQuery && (node.parent).exprName === node)) || + (node.parent.kind === SyntaxKind.ExportSpecifier); // We allow reexporting const enums + if (!ok) { + error(node, Diagnostics.const_enums_can_only_be_used_in_property_or_index_access_expressions_or_the_right_hand_side_of_an_import_declaration_or_export_assignment_or_type_query); + } + if (compilerOptions.isolatedModules) { + Debug.assert(!!(type.symbol.flags & SymbolFlags.ConstEnum)); + const constEnumDeclaration = (type.symbol.valueDeclaration as EnumDeclaration); + if (constEnumDeclaration.flags & NodeFlags.Ambient) { + error(node, Diagnostics.Cannot_access_ambient_const_enums_when_the_isolatedModules_flag_is_provided); } - - const signature = getSignatureFromDeclaration(func); - const returnType = getReturnTypeOfSignature(signature); - const functionFlags = getFunctionFlags(func); - if (strictNullChecks || node.expression || returnType.flags & TypeFlags.Never) { - const exprType = node.expression ? checkExpressionCached(node.expression) : undefinedType; - if (func.kind === SyntaxKind.SetAccessor) { - if (node.expression) { - error(node, Diagnostics.Setters_cannot_return_a_value); - } - } - else if (func.kind === SyntaxKind.Constructor) { - if (node.expression && !checkTypeAssignableToAndOptionallyElaborate(exprType, returnType, node, node.expression)) { - error(node, Diagnostics.Return_type_of_constructor_signature_must_be_assignable_to_the_instance_type_of_the_class); - } - } - else if (getReturnTypeFromAnnotation(func)) { - const unwrappedReturnType = unwrapReturnType(returnType, functionFlags); - const unwrappedExprType = functionFlags & FunctionFlags.Async - ? checkAwaitedType(exprType, node, Diagnostics.The_return_type_of_an_async_function_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member) - : exprType; - if (unwrappedReturnType) { - // If the function has a return type, but promisedType is - // undefined, an error will be reported in checkAsyncFunctionReturnType - // so we don't need to report one here. - checkTypeAssignableToAndOptionallyElaborate(unwrappedExprType, unwrappedReturnType, node, node.expression); - } - } + } + } + function checkParenthesizedExpression(node: ParenthesizedExpression, checkMode?: CheckMode): ts.Type { + const tag = isInJSFile(node) ? getJSDocTypeTag(node) : undefined; + if (tag) { + return checkAssertionWorker(tag, tag.typeExpression.type, node.expression, checkMode); + } + return checkExpression(node.expression, checkMode); + } + function checkExpressionWorker(node: Expression | QualifiedName, checkMode: CheckMode | undefined, forceTuple?: boolean): ts.Type { + const kind = node.kind; + if (cancellationToken) { + // Only bother checking on a few construct kinds. We don't want to be excessively + // hitting the cancellation token on every node we check. + switch (kind) { + case SyntaxKind.ClassExpression: + case SyntaxKind.FunctionExpression: + case SyntaxKind.ArrowFunction: + cancellationToken.throwIfCancellationRequested(); } - else if (func.kind !== SyntaxKind.Constructor && compilerOptions.noImplicitReturns && !isUnwrappedReturnTypeVoidOrAny(func, returnType)) { - // The function has a return type, but the return statement doesn't have an expression. - error(node, Diagnostics.Not_all_code_paths_return_a_value); + } + switch (kind) { + case SyntaxKind.Identifier: + return checkIdentifier((node)); + case SyntaxKind.ThisKeyword: + return checkThisExpression(node); + case SyntaxKind.SuperKeyword: + return checkSuperExpression(node); + case SyntaxKind.NullKeyword: + return nullWideningType; + case SyntaxKind.NoSubstitutionTemplateLiteral: + case SyntaxKind.StringLiteral: + return getFreshTypeOfLiteralType(getLiteralType((node as StringLiteralLike).text)); + case SyntaxKind.NumericLiteral: + checkGrammarNumericLiteral((node as NumericLiteral)); + return getFreshTypeOfLiteralType(getLiteralType(+(node as NumericLiteral).text)); + case SyntaxKind.BigIntLiteral: + checkGrammarBigIntLiteral((node as BigIntLiteral)); + return getFreshTypeOfLiteralType(getBigIntLiteralType((node as BigIntLiteral))); + case SyntaxKind.TrueKeyword: + return trueType; + case SyntaxKind.FalseKeyword: + return falseType; + case SyntaxKind.TemplateExpression: + return checkTemplateExpression((node)); + case SyntaxKind.RegularExpressionLiteral: + return globalRegExpType; + case SyntaxKind.ArrayLiteralExpression: + return checkArrayLiteral((node), checkMode, forceTuple); + case SyntaxKind.ObjectLiteralExpression: + return checkObjectLiteral((node), checkMode); + case SyntaxKind.PropertyAccessExpression: + return checkPropertyAccessExpression((node)); + case SyntaxKind.QualifiedName: + return checkQualifiedName((node)); + case SyntaxKind.ElementAccessExpression: + return checkIndexedAccess((node)); + case SyntaxKind.CallExpression: + if ((node).expression.kind === SyntaxKind.ImportKeyword) { + return checkImportCallExpression((node)); + } + // falls through + case SyntaxKind.NewExpression: + return checkCallExpression((node), checkMode); + case SyntaxKind.TaggedTemplateExpression: + return checkTaggedTemplateExpression((node)); + case SyntaxKind.ParenthesizedExpression: + return checkParenthesizedExpression((node), checkMode); + case SyntaxKind.ClassExpression: + return checkClassExpression((node)); + case SyntaxKind.FunctionExpression: + case SyntaxKind.ArrowFunction: + return checkFunctionExpressionOrObjectLiteralMethod((node), checkMode); + case SyntaxKind.TypeOfExpression: + return checkTypeOfExpression((node)); + case SyntaxKind.TypeAssertionExpression: + case SyntaxKind.AsExpression: + return checkAssertion((node)); + case SyntaxKind.NonNullExpression: + return checkNonNullAssertion((node)); + case SyntaxKind.MetaProperty: + return checkMetaProperty((node)); + case SyntaxKind.DeleteExpression: + return checkDeleteExpression((node)); + case SyntaxKind.VoidExpression: + return checkVoidExpression((node)); + case SyntaxKind.AwaitExpression: + return checkAwaitExpression((node)); + case SyntaxKind.PrefixUnaryExpression: + return checkPrefixUnaryExpression((node)); + case SyntaxKind.PostfixUnaryExpression: + return checkPostfixUnaryExpression((node)); + case SyntaxKind.BinaryExpression: + return checkBinaryExpression((node), checkMode); + case SyntaxKind.ConditionalExpression: + return checkConditionalExpression((node), checkMode); + case SyntaxKind.SpreadElement: + return checkSpreadExpression((node), checkMode); + case SyntaxKind.OmittedExpression: + return undefinedWideningType; + case SyntaxKind.YieldExpression: + return checkYieldExpression((node)); + case SyntaxKind.SyntheticExpression: + return (node).type; + case SyntaxKind.JsxExpression: + return checkJsxExpression((node), checkMode); + case SyntaxKind.JsxElement: + return checkJsxElement((node), checkMode); + case SyntaxKind.JsxSelfClosingElement: + return checkJsxSelfClosingElement((node), checkMode); + case SyntaxKind.JsxFragment: + return checkJsxFragment((node)); + case SyntaxKind.JsxAttributes: + return checkJsxAttributes((node), checkMode); + case SyntaxKind.JsxOpeningElement: + Debug.fail("Shouldn't ever directly check a JsxOpeningElement"); + } + return errorType; + } + // DECLARATION AND STATEMENT TYPE CHECKING + function checkTypeParameter(node: TypeParameterDeclaration) { + // Grammar Checking + if (node.expression) { + grammarErrorOnFirstToken(node.expression, Diagnostics.Type_expected); + } + checkSourceElement(node.constraint); + checkSourceElement(node.default); + const typeParameter = getDeclaredTypeOfTypeParameter(getSymbolOfNode(node)); + // Resolve base constraint to reveal circularity errors + getBaseConstraintOfType(typeParameter); + if (!hasNonCircularTypeParameterDefault(typeParameter)) { + error(node.default, Diagnostics.Type_parameter_0_has_a_circular_default, typeToString(typeParameter)); + } + const constraintType = getConstraintOfTypeParameter(typeParameter); + const defaultType = getDefaultFromTypeParameter(typeParameter); + if (constraintType && defaultType) { + checkTypeAssignableTo(defaultType, getTypeWithThisArgument(instantiateType(constraintType, makeUnaryTypeMapper(typeParameter, defaultType)), defaultType), node.default, Diagnostics.Type_0_does_not_satisfy_the_constraint_1); + } + if (produceDiagnostics) { + checkTypeNameIsReserved(node.name, Diagnostics.Type_parameter_name_cannot_be_0); + } + } + function checkParameter(node: ParameterDeclaration) { + // Grammar checking + // It is a SyntaxError if the Identifier "eval" or the Identifier "arguments" occurs as the + // Identifier in a PropertySetParameterList of a PropertyAssignment that is contained in strict code + // or if its FunctionBody is strict code(11.1.5). + checkGrammarDecoratorsAndModifiers(node); + checkVariableLikeDeclaration(node); + const func = (getContainingFunction(node)!); + if (hasModifier(node, ModifierFlags.ParameterPropertyModifier)) { + if (!(func.kind === SyntaxKind.Constructor && nodeIsPresent(func.body))) { + error(node, Diagnostics.A_parameter_property_is_only_allowed_in_a_constructor_implementation); } } - - function checkWithStatement(node: WithStatement) { - // Grammar checking for withStatement - if (!checkGrammarStatementInAmbientContext(node)) { - if (node.flags & NodeFlags.AwaitContext) { - grammarErrorOnFirstToken(node, Diagnostics.with_statements_are_not_allowed_in_an_async_function_block); - } + if (node.questionToken && isBindingPattern(node.name) && (func as FunctionLikeDeclaration).body) { + error(node, Diagnostics.A_binding_pattern_parameter_cannot_be_optional_in_an_implementation_signature); + } + if (node.name && isIdentifier(node.name) && (node.name.escapedText === "this" || node.name.escapedText === "new")) { + if (func.parameters.indexOf(node) !== 0) { + error(node, Diagnostics.A_0_parameter_must_be_the_first_parameter, (node.name.escapedText as string)); } - - checkExpression(node.expression); - - const sourceFile = getSourceFileOfNode(node); - if (!hasParseDiagnostics(sourceFile)) { - const start = getSpanOfTokenAtPosition(sourceFile, node.pos).start; - const end = node.statement.pos; - grammarErrorAtPos(sourceFile, start, end - start, Diagnostics.The_with_statement_is_not_supported_All_symbols_in_a_with_block_will_have_type_any); + if (func.kind === SyntaxKind.Constructor || func.kind === SyntaxKind.ConstructSignature || func.kind === SyntaxKind.ConstructorType) { + error(node, Diagnostics.A_constructor_cannot_have_a_this_parameter); + } + if (func.kind === SyntaxKind.ArrowFunction) { + error(node, Diagnostics.An_arrow_function_cannot_have_a_this_parameter); } } - - function checkSwitchStatement(node: SwitchStatement) { - // Grammar checking - checkGrammarStatementInAmbientContext(node); - - let firstDefaultClause: CaseOrDefaultClause; - let hasDuplicateDefaultClause = false; - - const expressionType = checkExpression(node.expression); - const expressionIsLiteral = isLiteralType(expressionType); - forEach(node.caseBlock.clauses, clause => { - // Grammar check for duplicate default clauses, skip if we already report duplicate default clause - if (clause.kind === SyntaxKind.DefaultClause && !hasDuplicateDefaultClause) { - if (firstDefaultClause === undefined) { - firstDefaultClause = clause; + // Only check rest parameter type if it's not a binding pattern. Since binding patterns are + // not allowed in a rest parameter, we already have an error from checkGrammarParameterList. + if (node.dotDotDotToken && !isBindingPattern(node.name) && !isTypeAssignableTo(getTypeOfSymbol(node.symbol), anyReadonlyArrayType)) { + error(node, Diagnostics.A_rest_parameter_must_be_of_an_array_type); + } + } + function checkTypePredicate(node: TypePredicateNode): void { + const parent = getTypePredicateParent(node); + if (!parent) { + // The parent must not be valid. + error(node, Diagnostics.A_type_predicate_is_only_allowed_in_return_type_position_for_functions_and_methods); + return; + } + const signature = getSignatureFromDeclaration(parent); + const typePredicate = getTypePredicateOfSignature(signature); + if (!typePredicate) { + return; + } + checkSourceElement(node.type); + const { parameterName } = node; + if (typePredicate.kind === TypePredicateKind.This || typePredicate.kind === TypePredicateKind.AssertsThis) { + getTypeFromThisTypeNode((parameterName as ThisTypeNode)); + } + else { + if (typePredicate.parameterIndex >= 0) { + if (signatureHasRestParameter(signature) && typePredicate.parameterIndex === signature.parameters.length - 1) { + error(parameterName, Diagnostics.A_type_predicate_cannot_reference_a_rest_parameter); + } + else { + if (typePredicate.type) { + const leadingError = () => chainDiagnosticMessages(/*details*/ undefined, Diagnostics.A_type_predicate_s_type_must_be_assignable_to_its_parameter_s_type); + checkTypeAssignableTo(typePredicate.type, getTypeOfSymbol(signature.parameters[typePredicate.parameterIndex]), node.type, + /*headMessage*/ undefined, leadingError); } - else { - grammarErrorOnNode(clause, Diagnostics.A_default_clause_cannot_appear_more_than_once_in_a_switch_statement); - hasDuplicateDefaultClause = true; + } + } + else if (parameterName) { + let hasReportedError = false; + for (const { name } of parent.parameters) { + if (isBindingPattern(name) && + checkIfTypePredicateVariableIsDeclaredInBindingPattern(name, parameterName, typePredicate.parameterName)) { + hasReportedError = true; + break; } } - - if (produceDiagnostics && clause.kind === SyntaxKind.CaseClause) { - // TypeScript 1.0 spec (April 2014): 5.9 - // In a 'switch' statement, each 'case' expression must be of a type that is comparable - // to or from the type of the 'switch' expression. - let caseType = checkExpression(clause.expression); - const caseIsLiteral = isLiteralType(caseType); - let comparedExpressionType = expressionType; - if (!caseIsLiteral || !expressionIsLiteral) { - caseType = caseIsLiteral ? getBaseTypeOfLiteralType(caseType) : caseType; - comparedExpressionType = getBaseTypeOfLiteralType(expressionType); - } - if (!isTypeEqualityComparableTo(comparedExpressionType, caseType)) { - // expressionType is not comparable to caseType, try the reversed check and report errors if it fails - checkTypeComparableTo(caseType, comparedExpressionType, clause.expression, /*headMessage*/ undefined); - } - } - forEach(clause.statements, checkSourceElement); - if (compilerOptions.noFallthroughCasesInSwitch && clause.fallthroughFlowNode && isReachableFlowNode(clause.fallthroughFlowNode)) { - error(clause, Diagnostics.Fallthrough_case_in_switch); + if (!hasReportedError) { + error(node.parameterName, Diagnostics.Cannot_find_parameter_0, typePredicate.parameterName); } - }); - if (node.caseBlock.locals) { - registerForUnusedIdentifiersCheck(node.caseBlock); } } - - function checkLabeledStatement(node: LabeledStatement) { - // Grammar checking - if (!checkGrammarStatementInAmbientContext(node)) { - findAncestor(node.parent, current => { - if (isFunctionLike(current)) { - return "quit"; - } - if (current.kind === SyntaxKind.LabeledStatement && (current).label.escapedText === node.label.escapedText) { - grammarErrorOnNode(node.label, Diagnostics.Duplicate_label_0, getTextOfNode(node.label)); - return true; - } - return false; - }); + } + function getTypePredicateParent(node: Node): SignatureDeclaration | undefined { + switch (node.parent.kind) { + case SyntaxKind.ArrowFunction: + case SyntaxKind.CallSignature: + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.FunctionExpression: + case SyntaxKind.FunctionType: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: + const parent = (node.parent); + if (node === parent.type) { + return parent; + } + } + } + function checkIfTypePredicateVariableIsDeclaredInBindingPattern(pattern: BindingPattern, predicateVariableNode: Node, predicateVariableName: string) { + for (const element of pattern.elements) { + if (isOmittedExpression(element)) { + continue; + } + const name = element.name; + if (name.kind === SyntaxKind.Identifier && name.escapedText === predicateVariableName) { + error(predicateVariableNode, Diagnostics.A_type_predicate_cannot_reference_element_0_in_a_binding_pattern, predicateVariableName); + return true; + } + else if (name.kind === SyntaxKind.ArrayBindingPattern || name.kind === SyntaxKind.ObjectBindingPattern) { + if (checkIfTypePredicateVariableIsDeclaredInBindingPattern(name, predicateVariableNode, predicateVariableName)) { + return true; + } } - - // ensure that label is unique - checkSourceElement(node.statement); } - - function checkThrowStatement(node: ThrowStatement) { - // Grammar checking - if (!checkGrammarStatementInAmbientContext(node)) { - if (node.expression === undefined) { - grammarErrorAfterFirstToken(node, Diagnostics.Line_break_not_permitted_here); + } + function checkSignatureDeclaration(node: SignatureDeclaration) { + // Grammar checking + if (node.kind === SyntaxKind.IndexSignature) { + checkGrammarIndexSignature((node)); + } + // TODO (yuisu): Remove this check in else-if when SyntaxKind.Construct is moved and ambient context is handled + else if (node.kind === SyntaxKind.FunctionType || node.kind === SyntaxKind.FunctionDeclaration || node.kind === SyntaxKind.ConstructorType || + node.kind === SyntaxKind.CallSignature || node.kind === SyntaxKind.Constructor || + node.kind === SyntaxKind.ConstructSignature) { + checkGrammarFunctionLikeDeclaration((node)); + } + const functionFlags = getFunctionFlags((node)); + if (!(functionFlags & FunctionFlags.Invalid)) { + // Async generators prior to ESNext require the __await and __asyncGenerator helpers + if ((functionFlags & FunctionFlags.AsyncGenerator) === FunctionFlags.AsyncGenerator && languageVersion < ScriptTarget.ESNext) { + checkExternalEmitHelpers(node, ExternalEmitHelpers.AsyncGeneratorIncludes); + } + // Async functions prior to ES2017 require the __awaiter helper + if ((functionFlags & FunctionFlags.AsyncGenerator) === FunctionFlags.Async && languageVersion < ScriptTarget.ES2017) { + checkExternalEmitHelpers(node, ExternalEmitHelpers.Awaiter); + } + // Generator functions, Async functions, and Async Generator functions prior to + // ES2015 require the __generator helper + if ((functionFlags & FunctionFlags.AsyncGenerator) !== FunctionFlags.Normal && languageVersion < ScriptTarget.ES2015) { + checkExternalEmitHelpers(node, ExternalEmitHelpers.Generator); + } + } + checkTypeParameters(node.typeParameters); + forEach(node.parameters, checkParameter); + // TODO(rbuckton): Should we start checking JSDoc types? + if (node.type) { + checkSourceElement(node.type); + } + if (produceDiagnostics) { + checkCollisionWithArgumentsInGeneratedCode(node); + const returnTypeNode = getEffectiveReturnTypeNode(node); + if (noImplicitAny && !returnTypeNode) { + switch (node.kind) { + case SyntaxKind.ConstructSignature: + error(node, Diagnostics.Construct_signature_which_lacks_return_type_annotation_implicitly_has_an_any_return_type); + break; + case SyntaxKind.CallSignature: + error(node, Diagnostics.Call_signature_which_lacks_return_type_annotation_implicitly_has_an_any_return_type); + break; } } - - if (node.expression) { - checkExpression(node.expression); - } - } - - function checkTryStatement(node: TryStatement) { - // Grammar checking - checkGrammarStatementInAmbientContext(node); - - checkBlock(node.tryBlock); - const catchClause = node.catchClause; - if (catchClause) { - // Grammar checking - if (catchClause.variableDeclaration) { - if (catchClause.variableDeclaration.type) { - grammarErrorOnFirstToken(catchClause.variableDeclaration.type, Diagnostics.Catch_clause_variable_cannot_have_a_type_annotation); - } - else if (catchClause.variableDeclaration.initializer) { - grammarErrorOnFirstToken(catchClause.variableDeclaration.initializer, Diagnostics.Catch_clause_variable_cannot_have_an_initializer); + if (returnTypeNode) { + const functionFlags = getFunctionFlags((node)); + if ((functionFlags & (FunctionFlags.Invalid | FunctionFlags.Generator)) === FunctionFlags.Generator) { + const returnType = getTypeFromTypeNode(returnTypeNode); + if (returnType === voidType) { + error(returnTypeNode, Diagnostics.A_generator_cannot_have_a_void_type_annotation); } else { - const blockLocals = catchClause.block.locals; - if (blockLocals) { - forEachKey(catchClause.locals!, caughtName => { - const blockLocal = blockLocals.get(caughtName); - if (blockLocal && (blockLocal.flags & SymbolFlags.BlockScopedVariable) !== 0) { - grammarErrorOnNode(blockLocal.valueDeclaration, Diagnostics.Cannot_redeclare_identifier_0_in_catch_clause, caughtName); - } - }); - } + // Naively, one could check that Generator is assignable to the return type annotation. + // However, that would not catch the error in the following case. + // + // interface BadGenerator extends Iterable, Iterator { } + // function* g(): BadGenerator { } // Iterable and Iterator have different types! + // + const generatorYieldType = getIterationTypeOfGeneratorFunctionReturnType(IterationTypeKind.Yield, returnType, (functionFlags & FunctionFlags.Async) !== 0) || anyType; + const generatorReturnType = getIterationTypeOfGeneratorFunctionReturnType(IterationTypeKind.Return, returnType, (functionFlags & FunctionFlags.Async) !== 0) || generatorYieldType; + const generatorNextType = getIterationTypeOfGeneratorFunctionReturnType(IterationTypeKind.Next, returnType, (functionFlags & FunctionFlags.Async) !== 0) || unknownType; + const generatorInstantiation = createGeneratorReturnType(generatorYieldType, generatorReturnType, generatorNextType, !!(functionFlags & FunctionFlags.Async)); + checkTypeAssignableTo(generatorInstantiation, returnType, returnTypeNode); } } - - checkBlock(catchClause.block); + else if ((functionFlags & FunctionFlags.AsyncGenerator) === FunctionFlags.Async) { + checkAsyncFunctionReturnType((node), returnTypeNode); + } } - - if (node.finallyBlock) { - checkBlock(node.finallyBlock); + if (node.kind !== SyntaxKind.IndexSignature && node.kind !== SyntaxKind.JSDocFunctionType) { + registerForUnusedIdentifiersCheck(node); } } - - function checkIndexConstraints(type: Type) { - const declaredNumberIndexer = getIndexDeclarationOfSymbol(type.symbol, IndexKind.Number); - const declaredStringIndexer = getIndexDeclarationOfSymbol(type.symbol, IndexKind.String); - - const stringIndexType = getIndexTypeOfType(type, IndexKind.String); - const numberIndexType = getIndexTypeOfType(type, IndexKind.Number); - - if (stringIndexType || numberIndexType) { - forEach(getPropertiesOfObjectType(type), prop => { - const propType = getTypeOfSymbol(prop); - checkIndexConstraintForProperty(prop, propType, type, declaredStringIndexer, stringIndexType, IndexKind.String); - checkIndexConstraintForProperty(prop, propType, type, declaredNumberIndexer, numberIndexType, IndexKind.Number); - }); - - const classDeclaration = type.symbol.valueDeclaration; - if (getObjectFlags(type) & ObjectFlags.Class && isClassLike(classDeclaration)) { - for (const member of classDeclaration.members) { - // Only process instance properties with computed names here. - // Static properties cannot be in conflict with indexers, - // and properties with literal names were already checked. - if (!hasModifier(member, ModifierFlags.Static) && hasNonBindableDynamicName(member)) { - const symbol = getSymbolOfNode(member); - const propType = getTypeOfSymbol(symbol); - checkIndexConstraintForProperty(symbol, propType, type, declaredStringIndexer, stringIndexType, IndexKind.String); - checkIndexConstraintForProperty(symbol, propType, type, declaredNumberIndexer, numberIndexType, IndexKind.Number); - } + } + function checkClassForDuplicateDeclarations(node: ClassLikeDeclaration) { + const instanceNames = createUnderscoreEscapedMap(); + const staticNames = createUnderscoreEscapedMap(); + // instance and static private identifiers share the same scope + const privateIdentifiers = createUnderscoreEscapedMap(); + for (const member of node.members) { + if (member.kind === SyntaxKind.Constructor) { + for (const param of (member as ConstructorDeclaration).parameters) { + if (isParameterPropertyDeclaration(param, member) && !isBindingPattern(param.name)) { + addName(instanceNames, param.name, param.name.escapedText, DeclarationMeaning.GetOrSetAccessor); } } } - - let errorNode: Node | undefined; - if (stringIndexType && numberIndexType) { - errorNode = declaredNumberIndexer || declaredStringIndexer; - // condition 'errorNode === undefined' may appear if types does not declare nor string neither number indexer - if (!errorNode && (getObjectFlags(type) & ObjectFlags.Interface)) { - const someBaseTypeHasBothIndexers = forEach(getBaseTypes(type), base => getIndexTypeOfType(base, IndexKind.String) && getIndexTypeOfType(base, IndexKind.Number)); - errorNode = someBaseTypeHasBothIndexers ? undefined : type.symbol.declarations[0]; - } - } - - if (errorNode && !isTypeAssignableTo(numberIndexType!, stringIndexType!)) { // TODO: GH#18217 - error(errorNode, Diagnostics.Numeric_index_type_0_is_not_assignable_to_string_index_type_1, - typeToString(numberIndexType!), typeToString(stringIndexType!)); - } - - function checkIndexConstraintForProperty( - prop: Symbol, - propertyType: Type, - containingType: Type, - indexDeclaration: Declaration | undefined, - indexType: Type | undefined, - indexKind: IndexKind): void { - - // ESSymbol properties apply to neither string nor numeric indexers. - if (!indexType || isKnownSymbol(prop)) { - return; - } - - const propDeclaration = prop.valueDeclaration; - const name = propDeclaration && getNameOfDeclaration(propDeclaration); - - // index is numeric and property name is not valid numeric literal - if (indexKind === IndexKind.Number && !(name ? isNumericName(name) : isNumericLiteralName(prop.escapedName))) { + else { + const isStatic = hasModifier(member, ModifierFlags.Static); + const name = member.name; + if (!name) { return; } - - // perform property check if property or indexer is declared in 'type' - // this allows us to rule out cases when both property and indexer are inherited from the base class - let errorNode: Node | undefined; - if (propDeclaration && name && - (propDeclaration.kind === SyntaxKind.BinaryExpression || - name.kind === SyntaxKind.ComputedPropertyName || - prop.parent === containingType.symbol)) { - errorNode = propDeclaration; - } - else if (indexDeclaration) { - errorNode = indexDeclaration; - } - else if (getObjectFlags(containingType) & ObjectFlags.Interface) { - // for interfaces property and indexer might be inherited from different bases - // check if any base class already has both property and indexer. - // check should be performed only if 'type' is the first type that brings property\indexer together - const someBaseClassHasBothPropertyAndIndexer = forEach(getBaseTypes(containingType), base => getPropertyOfObjectType(base, prop.escapedName) && getIndexTypeOfType(base, indexKind)); - errorNode = someBaseClassHasBothPropertyAndIndexer ? undefined : containingType.symbol.declarations[0]; - } - - if (errorNode && !isTypeAssignableTo(propertyType, indexType)) { - const errorMessage = - indexKind === IndexKind.String - ? Diagnostics.Property_0_of_type_1_is_not_assignable_to_string_index_type_2 - : Diagnostics.Property_0_of_type_1_is_not_assignable_to_numeric_index_type_2; - error(errorNode, errorMessage, symbolToString(prop), typeToString(propertyType), typeToString(indexType)); + const names = isPrivateIdentifier(name) ? privateIdentifiers : + isStatic ? staticNames : + instanceNames; + const memberName = name && getPropertyNameForPropertyNameNode(name); + if (memberName) { + switch (member.kind) { + case SyntaxKind.GetAccessor: + addName(names, name, memberName, DeclarationMeaning.GetAccessor); + break; + case SyntaxKind.SetAccessor: + addName(names, name, memberName, DeclarationMeaning.SetAccessor); + break; + case SyntaxKind.PropertyDeclaration: + addName(names, name, memberName, DeclarationMeaning.GetOrSetAccessor); + break; + case SyntaxKind.MethodDeclaration: + addName(names, name, memberName, DeclarationMeaning.Method); + break; + } } } } - - function checkTypeNameIsReserved(name: Identifier, message: DiagnosticMessage): void { - // TS 1.0 spec (April 2014): 3.6.1 - // The predefined type keywords are reserved and cannot be used as names of user defined types. - switch (name.escapedText) { - case "any": - case "unknown": - case "number": - case "bigint": - case "boolean": - case "string": - case "symbol": - case "void": - case "object": - error(name, message, name.escapedText as string); + function addName(names: UnderscoreEscapedMap, location: Node, name: __String, meaning: DeclarationMeaning) { + const prev = names.get(name); + if (prev) { + if (prev & DeclarationMeaning.Method) { + if (meaning !== DeclarationMeaning.Method) { + error(location, Diagnostics.Duplicate_identifier_0, getTextOfNode(location)); + } + } + else if (prev & meaning) { + error(location, Diagnostics.Duplicate_identifier_0, getTextOfNode(location)); + } + else { + names.set(name, prev | meaning); + } } - } - - /** - * The name cannot be used as 'Object' of user defined types with special target. - */ - function checkClassNameCollisionWithObject(name: Identifier): void { - if (languageVersion === ScriptTarget.ES5 && name.escapedText === "Object" - && moduleKind < ModuleKind.ES2015) { - error(name, Diagnostics.Class_name_cannot_be_Object_when_targeting_ES5_with_module_0, ModuleKind[moduleKind]); // https://github.com/Microsoft/TypeScript/issues/17494 + else { + names.set(name, meaning); } } - - /** - * Check each type parameter and check that type parameters have no duplicate type parameter declarations - */ - function checkTypeParameters(typeParameterDeclarations: readonly TypeParameterDeclaration[] | undefined) { - if (typeParameterDeclarations) { - let seenDefault = false; - for (let i = 0; i < typeParameterDeclarations.length; i++) { - const node = typeParameterDeclarations[i]; - checkTypeParameter(node); - - if (produceDiagnostics) { - if (node.default) { - seenDefault = true; - checkTypeParametersNotReferenced(node.default, typeParameterDeclarations, i); - } - else if (seenDefault) { - error(node, Diagnostics.Required_type_parameters_may_not_follow_optional_type_parameters); - } - for (let j = 0; j < i; j++) { - if (typeParameterDeclarations[j].symbol === node.symbol) { - error(node.name, Diagnostics.Duplicate_identifier_0, declarationNameToString(node.name)); - } - } - } + } + /** + * Static members being set on a constructor function may conflict with built-in properties + * of Function. Esp. in ECMAScript 5 there are non-configurable and non-writable + * built-in properties. This check issues a transpile error when a class has a static + * member with the same name as a non-writable built-in property. + * + * @see http://www.ecma-international.org/ecma-262/5.1/#sec-15.3.3 + * @see http://www.ecma-international.org/ecma-262/5.1/#sec-15.3.5 + * @see http://www.ecma-international.org/ecma-262/6.0/#sec-properties-of-the-function-constructor + * @see http://www.ecma-international.org/ecma-262/6.0/#sec-function-instances + */ + function checkClassForStaticPropertyNameConflicts(node: ClassLikeDeclaration) { + for (const member of node.members) { + const memberNameNode = member.name; + const isStatic = hasModifier(member, ModifierFlags.Static); + if (isStatic && memberNameNode) { + const memberName = getPropertyNameForPropertyNameNode(memberNameNode); + switch (memberName) { + case "name": + case "length": + case "caller": + case "arguments": + case "prototype": + const message = Diagnostics.Static_property_0_conflicts_with_built_in_property_Function_0_of_constructor_function_1; + const className = getNameOfSymbolAsWritten(getSymbolOfNode(node)); + error(memberNameNode, message, memberName, className); + break; } } } - - /** Check that type parameter defaults only reference previously declared type parameters */ - function checkTypeParametersNotReferenced(root: TypeNode, typeParameters: readonly TypeParameterDeclaration[], index: number) { - visit(root); - function visit(node: Node) { - if (node.kind === SyntaxKind.TypeReference) { - const type = getTypeFromTypeReference(node); - if (type.flags & TypeFlags.TypeParameter) { - for (let i = index; i < typeParameters.length; i++) { - if (type.symbol === getSymbolOfNode(typeParameters[i])) { - error(node, Diagnostics.Type_parameter_defaults_can_only_reference_previously_declared_type_parameters); - } - } - } + } + function checkObjectTypeForDuplicateDeclarations(node: TypeLiteralNode | InterfaceDeclaration) { + const names = createMap(); + for (const member of node.members) { + if (member.kind === SyntaxKind.PropertySignature) { + let memberName: string; + const name = member.name!; + switch (name.kind) { + case SyntaxKind.StringLiteral: + case SyntaxKind.NumericLiteral: + memberName = name.text; + break; + case SyntaxKind.Identifier: + memberName = idText(name); + break; + default: + continue; + } + if (names.get(memberName)) { + error(getNameOfDeclaration(member.symbol.valueDeclaration), Diagnostics.Duplicate_identifier_0, memberName); + error(member.name, Diagnostics.Duplicate_identifier_0, memberName); + } + else { + names.set(memberName, true); } - forEachChild(node, visit); } } - - /** Check that type parameter lists are identical across multiple declarations */ - function checkTypeParameterListsIdentical(symbol: Symbol) { - if (symbol.declarations.length === 1) { + } + function checkTypeForDuplicateIndexSignatures(node: Node) { + if (node.kind === SyntaxKind.InterfaceDeclaration) { + const nodeSymbol = getSymbolOfNode((node as InterfaceDeclaration)); + // in case of merging interface declaration it is possible that we'll enter this check procedure several times for every declaration + // to prevent this run check only for the first declaration of a given kind + if (nodeSymbol.declarations.length > 0 && nodeSymbol.declarations[0] !== node) { return; } - - const links = getSymbolLinks(symbol); - if (!links.typeParametersChecked) { - links.typeParametersChecked = true; - const declarations = getClassOrInterfaceDeclarationsOfSymbol(symbol); - if (declarations.length <= 1) { - return; - } - - const type = getDeclaredTypeOfSymbol(symbol); - if (!areTypeParametersIdentical(declarations, type.localTypeParameters!)) { - // Report an error on every conflicting declaration. - const name = symbolToString(symbol); - for (const declaration of declarations) { - error(declaration.name, Diagnostics.All_declarations_of_0_must_have_identical_type_parameters, name); + } + // TypeScript 1.0 spec (April 2014) + // 3.7.4: An object type can contain at most one string index signature and one numeric index signature. + // 8.5: A class declaration can have at most one string index member declaration and one numeric index member declaration + const indexSymbol = getIndexSymbol(getSymbolOfNode(node)!); + if (indexSymbol) { + let seenNumericIndexer = false; + let seenStringIndexer = false; + for (const decl of indexSymbol.declarations) { + const declaration = (decl); + if (declaration.parameters.length === 1 && declaration.parameters[0].type) { + switch (declaration.parameters[0].type.kind) { + case SyntaxKind.StringKeyword: + if (!seenStringIndexer) { + seenStringIndexer = true; + } + else { + error(declaration, Diagnostics.Duplicate_string_index_signature); + } + break; + case SyntaxKind.NumberKeyword: + if (!seenNumericIndexer) { + seenNumericIndexer = true; + } + else { + error(declaration, Diagnostics.Duplicate_number_index_signature); + } + break; } } } } - - function areTypeParametersIdentical(declarations: readonly (ClassDeclaration | InterfaceDeclaration)[], targetParameters: TypeParameter[]) { - const maxTypeArgumentCount = length(targetParameters); - const minTypeArgumentCount = getMinTypeArgumentCount(targetParameters); - - for (const declaration of declarations) { - // If this declaration has too few or too many type parameters, we report an error - const sourceParameters = getEffectiveTypeParameterDeclarations(declaration); - const numTypeParameters = sourceParameters.length; - if (numTypeParameters < minTypeArgumentCount || numTypeParameters > maxTypeArgumentCount) { - return false; - } - - for (let i = 0; i < numTypeParameters; i++) { - const source = sourceParameters[i]; - const target = targetParameters[i]; - - // If the type parameter node does not have the same as the resolved type - // parameter at this position, we report an error. - if (source.name.escapedText !== target.symbol.escapedName) { - return false; - } - - // If the type parameter node does not have an identical constraint as the resolved - // type parameter at this position, we report an error. - const constraint = getEffectiveConstraintOfTypeParameter(source); - const sourceConstraint = constraint && getTypeFromTypeNode(constraint); - const targetConstraint = getConstraintOfTypeParameter(target); - // relax check if later interface augmentation has no constraint, it's more broad and is OK to merge with - // a more constrained interface (this could be generalized to a full hierarchy check, but that's maybe overkill) - if (sourceConstraint && targetConstraint && !isTypeIdenticalTo(sourceConstraint, targetConstraint)) { - return false; - } - - // If the type parameter node has a default and it is not identical to the default - // for the type parameter at this position, we report an error. - const sourceDefault = source.default && getTypeFromTypeNode(source.default); - const targetDefault = getDefaultFromTypeParameter(target); - if (sourceDefault && targetDefault && !isTypeIdenticalTo(sourceDefault, targetDefault)) { - return false; - } - } + } + function checkPropertyDeclaration(node: PropertyDeclaration | PropertySignature) { + // Grammar checking + if (!checkGrammarDecoratorsAndModifiers(node) && !checkGrammarProperty(node)) + checkGrammarComputedPropertyName(node.name); + checkVariableLikeDeclaration(node); + // Private class fields transformation relies on WeakMaps. + if (isPrivateIdentifier(node.name) && languageVersion < ScriptTarget.ESNext) { + for (let lexicalScope = getEnclosingBlockScopeContainer(node); !!lexicalScope; lexicalScope = getEnclosingBlockScopeContainer(lexicalScope)) { + getNodeLinks(lexicalScope).flags |= NodeCheckFlags.ContainsClassWithPrivateIdentifiers; } - - return true; } - - function checkClassExpression(node: ClassExpression): Type { - checkClassLikeDeclaration(node); - checkNodeDeferred(node); - return getTypeOfSymbol(getSymbolOfNode(node)); + } + function checkPropertySignature(node: PropertySignature) { + if (isPrivateIdentifier(node.name)) { + error(node, Diagnostics.Private_identifiers_are_not_allowed_outside_class_bodies); } - - function checkClassExpressionDeferred(node: ClassExpression) { - forEach(node.members, checkSourceElement); - registerForUnusedIdentifiersCheck(node); + return checkPropertyDeclaration(node); + } + function checkMethodDeclaration(node: MethodDeclaration | MethodSignature) { + // Grammar checking + if (!checkGrammarMethod(node)) + checkGrammarComputedPropertyName(node.name); + if (isPrivateIdentifier(node.name)) { + error(node, Diagnostics.A_method_cannot_be_named_with_a_private_identifier); + } + // Grammar checking for modifiers is done inside the function checkGrammarFunctionLikeDeclaration + checkFunctionOrMethodDeclaration(node); + // Abstract methods cannot have an implementation. + // Extra checks are to avoid reporting multiple errors relating to the "abstractness" of the node. + if (hasModifier(node, ModifierFlags.Abstract) && node.kind === SyntaxKind.MethodDeclaration && node.body) { + error(node, Diagnostics.Method_0_cannot_have_an_implementation_because_it_is_marked_abstract, declarationNameToString(node.name)); } - - function checkClassDeclaration(node: ClassDeclaration) { - if (!node.name && !hasModifier(node, ModifierFlags.Default)) { - grammarErrorOnFirstToken(node, Diagnostics.A_class_declaration_without_the_default_modifier_must_have_a_name); + } + function checkConstructorDeclaration(node: ConstructorDeclaration) { + // Grammar check on signature of constructor and modifier of the constructor is done in checkSignatureDeclaration function. + checkSignatureDeclaration(node); + // Grammar check for checking only related to constructorDeclaration + if (!checkGrammarConstructorTypeParameters(node)) + checkGrammarConstructorTypeAnnotation(node); + checkSourceElement(node.body); + const symbol = getSymbolOfNode(node); + const firstDeclaration = getDeclarationOfKind(symbol, node.kind); + // Only type check the symbol once + if (node === firstDeclaration) { + checkFunctionOrConstructorSymbol(symbol); + } + // exit early in the case of signature - super checks are not relevant to them + if (nodeIsMissing(node.body)) { + return; + } + if (!produceDiagnostics) { + return; + } + function isInstancePropertyWithInitializerOrPrivateIdentifierProperty(n: Node): boolean { + if (isPrivateIdentifierPropertyDeclaration(n)) { + return true; + } + return n.kind === SyntaxKind.PropertyDeclaration && + !hasModifier(n, ModifierFlags.Static) && + !!(n).initializer; + } + // TS 1.0 spec (April 2014): 8.3.2 + // Constructors of classes with no extends clause may not contain super calls, whereas + // constructors of derived classes must contain at least one super call somewhere in their function body. + const containingClassDecl = (node.parent); + if (getClassExtendsHeritageElement(containingClassDecl)) { + captureLexicalThis(node.parent, containingClassDecl); + const classExtendsNull = classDeclarationExtendsNull(containingClassDecl); + const superCall = getSuperCallInConstructor(node); + if (superCall) { + if (classExtendsNull) { + error(superCall, Diagnostics.A_constructor_cannot_contain_a_super_call_when_its_class_extends_null); + } + // The first statement in the body of a constructor (excluding prologue directives) must be a super call + // if both of the following are true: + // - The containing class is a derived class. + // - The constructor declares parameter properties + // or the containing class declares instance member variables with initializers. + const superCallShouldBeFirst = some((node.parent).members, isInstancePropertyWithInitializerOrPrivateIdentifierProperty) || + some(node.parameters, p => hasModifier(p, ModifierFlags.ParameterPropertyModifier)); + // Skip past any prologue directives to find the first statement + // to ensure that it was a super call. + if (superCallShouldBeFirst) { + const statements = node.body!.statements; + let superCallStatement: ExpressionStatement | undefined; + for (const statement of statements) { + if (statement.kind === SyntaxKind.ExpressionStatement && isSuperCall((statement).expression)) { + superCallStatement = (statement); + break; + } + if (!isPrologueDirective(statement)) { + break; + } + } + if (!superCallStatement) { + error(node, Diagnostics.A_super_call_must_be_the_first_statement_in_the_constructor_when_a_class_contains_initialized_properties_parameter_properties_or_private_identifiers); + } + } + } + else if (!classExtendsNull) { + error(node, Diagnostics.Constructors_for_derived_classes_must_contain_a_super_call); } - checkClassLikeDeclaration(node); - forEach(node.members, checkSourceElement); - - registerForUnusedIdentifiersCheck(node); } - - function checkClassLikeDeclaration(node: ClassLikeDeclaration) { - checkGrammarClassLikeDeclaration(node); + } + function checkAccessorDeclaration(node: AccessorDeclaration) { + if (produceDiagnostics) { + // Grammar checking accessors + if (!checkGrammarFunctionLikeDeclaration(node) && !checkGrammarAccessor(node)) + checkGrammarComputedPropertyName(node.name); checkDecorators(node); - if (node.name) { - checkTypeNameIsReserved(node.name, Diagnostics.Class_name_cannot_be_0); - checkCollisionWithRequireExportsInGeneratedCode(node, node.name); - checkCollisionWithGlobalPromiseInGeneratedCode(node, node.name); - if (!(node.flags & NodeFlags.Ambient)) { - checkClassNameCollisionWithObject(node.name); + checkSignatureDeclaration(node); + if (node.kind === SyntaxKind.GetAccessor) { + if (!(node.flags & NodeFlags.Ambient) && nodeIsPresent(node.body) && (node.flags & NodeFlags.HasImplicitReturn)) { + if (!(node.flags & NodeFlags.HasExplicitReturn)) { + error(node.name, Diagnostics.A_get_accessor_must_return_a_value); + } } } - checkTypeParameters(getEffectiveTypeParameterDeclarations(node)); - checkExportsOnMergedDeclarations(node); - const symbol = getSymbolOfNode(node); - const type = getDeclaredTypeOfSymbol(symbol); - const typeWithThis = getTypeWithThisArgument(type); - const staticType = getTypeOfSymbol(symbol); - checkTypeParameterListsIdentical(symbol); - checkClassForDuplicateDeclarations(node); - - // Only check for reserved static identifiers on non-ambient context. - if (!(node.flags & NodeFlags.Ambient)) { - checkClassForStaticPropertyNameConflicts(node); + // Do not use hasDynamicName here, because that returns false for well known symbols. + // We want to perform checkComputedPropertyName for all computed properties, including + // well known symbols. + if (node.name.kind === SyntaxKind.ComputedPropertyName) { + checkComputedPropertyName(node.name); } - - const baseTypeNode = getEffectiveBaseTypeNode(node); - if (baseTypeNode) { - forEach(baseTypeNode.typeArguments, checkSourceElement); - if (languageVersion < ScriptTarget.ES2015) { - checkExternalEmitHelpers(baseTypeNode.parent, ExternalEmitHelpers.Extends); - } - // check both @extends and extends if both are specified. - const extendsNode = getClassExtendsHeritageElement(node); - if (extendsNode && extendsNode !== baseTypeNode) { - checkExpression(extendsNode.expression); - } - - const baseTypes = getBaseTypes(type); - if (baseTypes.length && produceDiagnostics) { - const baseType = baseTypes[0]; - const baseConstructorType = getBaseConstructorTypeOfClass(type); - const staticBaseType = getApparentType(baseConstructorType); - checkBaseTypeAccessibility(staticBaseType, baseTypeNode); - checkSourceElement(baseTypeNode.expression); - if (some(baseTypeNode.typeArguments)) { - forEach(baseTypeNode.typeArguments, checkSourceElement); - for (const constructor of getConstructorsForTypeArguments(staticBaseType, baseTypeNode.typeArguments, baseTypeNode)) { - if (!checkTypeArgumentConstraints(baseTypeNode, constructor.typeParameters!)) { - break; - } - } - } - const baseWithThis = getTypeWithThisArgument(baseType, type.thisType); - if (!checkTypeAssignableTo(typeWithThis, baseWithThis, /*errorNode*/ undefined)) { - issueMemberSpecificError(node, typeWithThis, baseWithThis, Diagnostics.Class_0_incorrectly_extends_base_class_1); - } - else { - // Report static side error only when instance type is assignable - checkTypeAssignableTo(staticType, getTypeWithoutSignatures(staticBaseType), node.name || node, - Diagnostics.Class_static_side_0_incorrectly_extends_base_class_static_side_1); - } - if (baseConstructorType.flags & TypeFlags.TypeVariable && !isMixinConstructorType(staticType)) { - error(node.name || node, Diagnostics.A_mixin_class_must_have_a_constructor_with_a_single_rest_parameter_of_type_any); + if (isPrivateIdentifier(node.name)) { + error(node.name, Diagnostics.An_accessor_cannot_be_named_with_a_private_identifier); + } + if (!hasNonBindableDynamicName(node)) { + // TypeScript 1.0 spec (April 2014): 8.4.3 + // Accessors for the same member name must specify the same accessibility. + const otherKind = node.kind === SyntaxKind.GetAccessor ? SyntaxKind.SetAccessor : SyntaxKind.GetAccessor; + const otherAccessor = getDeclarationOfKind(getSymbolOfNode(node), otherKind); + if (otherAccessor) { + const nodeFlags = getModifierFlags(node); + const otherFlags = getModifierFlags(otherAccessor); + if ((nodeFlags & ModifierFlags.AccessibilityModifier) !== (otherFlags & ModifierFlags.AccessibilityModifier)) { + error(node.name, Diagnostics.Getter_and_setter_accessors_do_not_agree_in_visibility); } - - if (!(staticBaseType.symbol && staticBaseType.symbol.flags & SymbolFlags.Class) && !(baseConstructorType.flags & TypeFlags.TypeVariable)) { - // When the static base type is a "class-like" constructor function (but not actually a class), we verify - // that all instantiated base constructor signatures return the same type. - const constructors = getInstantiatedConstructorsForTypeArguments(staticBaseType, baseTypeNode.typeArguments, baseTypeNode); - if (forEach(constructors, sig => !isJSConstructor(sig.declaration) && !isTypeIdenticalTo(getReturnTypeOfSignature(sig), baseType))) { - error(baseTypeNode.expression, Diagnostics.Base_constructors_must_all_have_the_same_return_type); - } + if ((nodeFlags & ModifierFlags.Abstract) !== (otherFlags & ModifierFlags.Abstract)) { + error(node.name, Diagnostics.Accessors_must_both_be_abstract_or_non_abstract); } - checkKindsOfPropertyMemberOverrides(type, baseType); + // TypeScript 1.0 spec (April 2014): 4.5 + // If both accessors include type annotations, the specified types must be identical. + checkAccessorDeclarationTypesIdentical(node, otherAccessor, getAnnotatedAccessorType, Diagnostics.get_and_set_accessor_must_have_the_same_type); + checkAccessorDeclarationTypesIdentical(node, otherAccessor, getThisTypeOfDeclaration, Diagnostics.get_and_set_accessor_must_have_the_same_this_type); } } - - const implementedTypeNodes = getEffectiveImplementsTypeNodes(node); - if (implementedTypeNodes) { - for (const typeRefNode of implementedTypeNodes) { - if (!isEntityNameExpression(typeRefNode.expression)) { - error(typeRefNode.expression, Diagnostics.A_class_can_only_implement_an_identifier_Slashqualified_name_with_optional_type_arguments); - } - checkTypeReferenceNode(typeRefNode); - if (produceDiagnostics) { - const t = getTypeFromTypeNode(typeRefNode); - if (t !== errorType) { - if (isValidBaseType(t)) { - const genericDiag = t.symbol && t.symbol.flags & SymbolFlags.Class ? - Diagnostics.Class_0_incorrectly_implements_class_1_Did_you_mean_to_extend_1_and_inherit_its_members_as_a_subclass : - Diagnostics.Class_0_incorrectly_implements_interface_1; - const baseWithThis = getTypeWithThisArgument(t, type.thisType); - if (!checkTypeAssignableTo(typeWithThis, baseWithThis, /*errorNode*/ undefined)) { - issueMemberSpecificError(node, typeWithThis, baseWithThis, genericDiag); - } - } - else { - error(typeRefNode, Diagnostics.A_class_can_only_implement_an_object_type_or_intersection_of_object_types_with_statically_known_members); - } - } - } + const returnType = getTypeOfAccessors(getSymbolOfNode(node)); + if (node.kind === SyntaxKind.GetAccessor) { + checkAllCodePathsInNonVoidFunctionReturnOrThrow(node, returnType); + } + } + checkSourceElement(node.body); + } + function checkAccessorDeclarationTypesIdentical(first: AccessorDeclaration, second: AccessorDeclaration, getAnnotatedType: (a: AccessorDeclaration) => ts.Type | undefined, message: DiagnosticMessage) { + const firstType = getAnnotatedType(first); + const secondType = getAnnotatedType(second); + if (firstType && secondType && !isTypeIdenticalTo(firstType, secondType)) { + error(first, message); + } + } + function checkMissingDeclaration(node: Node) { + checkDecorators(node); + } + function getEffectiveTypeArguments(node: TypeReferenceNode | ExpressionWithTypeArguments, typeParameters: readonly TypeParameter[]): ts.Type[] { + return fillMissingTypeArguments(map((node.typeArguments!), getTypeFromTypeNode), typeParameters, getMinTypeArgumentCount(typeParameters), isInJSFile(node)); + } + function checkTypeArgumentConstraints(node: TypeReferenceNode | ExpressionWithTypeArguments, typeParameters: readonly TypeParameter[]): boolean { + let typeArguments: ts.Type[] | undefined; + let mapper: TypeMapper | undefined; + let result = true; + for (let i = 0; i < typeParameters.length; i++) { + const constraint = getConstraintOfTypeParameter(typeParameters[i]); + if (constraint) { + if (!typeArguments) { + typeArguments = getEffectiveTypeArguments(node, typeParameters); + mapper = createTypeMapper(typeParameters, typeArguments); } + result = result && checkTypeAssignableTo(typeArguments[i], instantiateType(constraint, mapper), node.typeArguments![i], Diagnostics.Type_0_does_not_satisfy_the_constraint_1); } - - if (produceDiagnostics) { - checkIndexConstraints(type); - checkTypeForDuplicateIndexSignatures(node); - checkPropertyInitialization(node); + } + return result; + } + function getTypeParametersForTypeReference(node: TypeReferenceNode | ExpressionWithTypeArguments) { + const type = getTypeFromTypeReference(node); + if (type !== errorType) { + const symbol = getNodeLinks(node).resolvedSymbol; + if (symbol) { + return symbol.flags & SymbolFlags.TypeAlias && getSymbolLinks(symbol).typeParameters || + (getObjectFlags(type) & ObjectFlags.Reference ? (type).target.localTypeParameters : undefined); } } - - function issueMemberSpecificError(node: ClassLikeDeclaration, typeWithThis: Type, baseWithThis: Type, broadDiag: DiagnosticMessage) { - // iterate over all implemented properties and issue errors on each one which isn't compatible, rather than the class as a whole, if possible - let issuedMemberError = false; - for (const member of node.members) { - if (hasStaticModifier(member)) { - continue; - } - const declaredProp = member.name && getSymbolAtLocation(member.name) || getSymbolAtLocation(member); - if (declaredProp) { - const prop = getPropertyOfType(typeWithThis, declaredProp.escapedName); - const baseProp = getPropertyOfType(baseWithThis, declaredProp.escapedName); - if (prop && baseProp) { - const rootChain = () => chainDiagnosticMessages( - /*details*/ undefined, - Diagnostics.Property_0_in_type_1_is_not_assignable_to_the_same_property_in_base_type_2, - symbolToString(declaredProp), - typeToString(typeWithThis), - typeToString(baseWithThis) - ); - if (!checkTypeAssignableTo(getTypeOfSymbol(prop), getTypeOfSymbol(baseProp), member.name || member, /*message*/ undefined, rootChain)) { - issuedMemberError = true; - } - } + return undefined; + } + function checkTypeReferenceNode(node: TypeReferenceNode | ExpressionWithTypeArguments) { + checkGrammarTypeArguments(node, node.typeArguments); + if (node.kind === SyntaxKind.TypeReference && node.typeName.jsdocDotPos !== undefined && !isInJSFile(node) && !isInJSDoc(node)) { + grammarErrorAtPos(node, node.typeName.jsdocDotPos, 1, Diagnostics.JSDoc_types_can_only_be_used_inside_documentation_comments); + } + forEach(node.typeArguments, checkSourceElement); + const type = getTypeFromTypeReference(node); + if (type !== errorType) { + if (node.typeArguments && produceDiagnostics) { + const typeParameters = getTypeParametersForTypeReference(node); + if (typeParameters) { + checkTypeArgumentConstraints(node, typeParameters); } } - if (!issuedMemberError) { - // check again with diagnostics to generate a less-specific error - checkTypeAssignableTo(typeWithThis, baseWithThis, node.name || node, broadDiag); + if (type.flags & TypeFlags.Enum && getNodeLinks(node).resolvedSymbol!.flags & SymbolFlags.EnumMember) { + error(node, Diagnostics.Enum_type_0_has_members_with_initializers_that_are_not_literals, typeToString(type)); } } - - function checkBaseTypeAccessibility(type: Type, node: ExpressionWithTypeArguments) { - const signatures = getSignaturesOfType(type, SignatureKind.Construct); - if (signatures.length) { - const declaration = signatures[0].declaration; - if (declaration && hasModifier(declaration, ModifierFlags.Private)) { - const typeClassDeclaration = getClassLikeDeclarationOfSymbol(type.symbol)!; - if (!isNodeWithinClass(node, typeClassDeclaration)) { - error(node, Diagnostics.Cannot_extend_a_class_0_Class_constructor_is_marked_as_private, getFullyQualifiedName(type.symbol)); - } + } + function getTypeArgumentConstraint(node: TypeNode): ts.Type | undefined { + const typeReferenceNode = tryCast(node.parent, isTypeReferenceType); + if (!typeReferenceNode) + return undefined; + const typeParameters = getTypeParametersForTypeReference(typeReferenceNode)!; // TODO: GH#18217 + const constraint = getConstraintOfTypeParameter(typeParameters[typeReferenceNode.typeArguments!.indexOf(node)]); + return constraint && instantiateType(constraint, createTypeMapper(typeParameters, getEffectiveTypeArguments(typeReferenceNode, typeParameters))); + } + function checkTypeQuery(node: TypeQueryNode) { + getTypeFromTypeQueryNode(node); + } + function checkTypeLiteral(node: TypeLiteralNode) { + forEach(node.members, checkSourceElement); + if (produceDiagnostics) { + const type = getTypeFromTypeLiteralOrFunctionOrConstructorTypeNode(node); + checkIndexConstraints(type); + checkTypeForDuplicateIndexSignatures(node); + checkObjectTypeForDuplicateDeclarations(node); + } + } + function checkArrayType(node: ArrayTypeNode) { + checkSourceElement(node.elementType); + } + function checkTupleType(node: TupleTypeNode) { + const elementTypes = node.elementTypes; + let seenOptionalElement = false; + for (let i = 0; i < elementTypes.length; i++) { + const e = elementTypes[i]; + if (e.kind === SyntaxKind.RestType) { + if (i !== elementTypes.length - 1) { + grammarErrorOnNode(e, Diagnostics.A_rest_element_must_be_last_in_a_tuple_type); + break; } + if (!isArrayType(getTypeFromTypeNode((e).type))) { + error(e, Diagnostics.A_rest_element_type_must_be_an_array_type); + } + } + else if (e.kind === SyntaxKind.OptionalType) { + seenOptionalElement = true; + } + else if (seenOptionalElement) { + grammarErrorOnNode(e, Diagnostics.A_required_element_cannot_follow_an_optional_element); + break; } } - - function getTargetSymbol(s: Symbol) { - // if symbol is instantiated its flags are not copied from the 'target' - // so we'll need to get back original 'target' symbol to work with correct set of flags - return getCheckFlags(s) & CheckFlags.Instantiated ? (s).target! : s; + forEach(node.elementTypes, checkSourceElement); + } + function checkUnionOrIntersectionType(node: UnionOrIntersectionTypeNode) { + forEach(node.types, checkSourceElement); + } + function checkIndexedAccessIndexType(type: ts.Type, accessNode: IndexedAccessTypeNode | ElementAccessExpression) { + if (!(type.flags & TypeFlags.IndexedAccess)) { + return type; } - - function getClassOrInterfaceDeclarationsOfSymbol(symbol: Symbol) { - return filter(symbol.declarations, (d: Declaration): d is ClassDeclaration | InterfaceDeclaration => - d.kind === SyntaxKind.ClassDeclaration || d.kind === SyntaxKind.InterfaceDeclaration); + // Check if the index type is assignable to 'keyof T' for the object type. + const objectType = (type).objectType; + const indexType = (type).indexType; + if (isTypeAssignableTo(indexType, getIndexType(objectType, /*stringsOnly*/ false))) { + if (accessNode.kind === SyntaxKind.ElementAccessExpression && isAssignmentTarget(accessNode) && + getObjectFlags(objectType) & ObjectFlags.Mapped && getMappedTypeModifiers((objectType)) & MappedTypeModifiers.IncludeReadonly) { + error(accessNode, Diagnostics.Index_signature_in_type_0_only_permits_reading, typeToString(objectType)); + } + return type; } - - function checkKindsOfPropertyMemberOverrides(type: InterfaceType, baseType: BaseType): void { - // TypeScript 1.0 spec (April 2014): 8.2.3 - // A derived class inherits all members from its base class it doesn't override. - // Inheritance means that a derived class implicitly contains all non - overridden members of the base class. - // Both public and private property members are inherited, but only public property members can be overridden. - // A property member in a derived class is said to override a property member in a base class - // when the derived class property member has the same name and kind(instance or static) - // as the base class property member. - // The type of an overriding property member must be assignable(section 3.8.4) - // to the type of the overridden property member, or otherwise a compile - time error occurs. - // Base class instance member functions can be overridden by derived class instance member functions, - // but not by other kinds of members. - // Base class instance member variables and accessors can be overridden by - // derived class instance member variables and accessors, but not by other kinds of members. - - // NOTE: assignability is checked in checkClassDeclaration - const baseProperties = getPropertiesOfType(baseType); - basePropertyCheck: for (const baseProperty of baseProperties) { - const base = getTargetSymbol(baseProperty); - - if (base.flags & SymbolFlags.Prototype) { - continue; - } - const baseSymbol = getPropertyOfObjectType(type, base.escapedName); - if (!baseSymbol) { - continue; + // Check if we're indexing with a numeric type and if either object or index types + // is a generic type with a constraint that has a numeric index signature. + const apparentObjectType = getApparentType(objectType); + if (getIndexInfoOfType(apparentObjectType, IndexKind.Number) && isTypeAssignableToKind(indexType, TypeFlags.NumberLike)) { + return type; + } + if (isGenericObjectType(objectType)) { + const propertyName = getPropertyNameFromIndex(indexType, accessNode); + if (propertyName) { + const propertySymbol = forEachType(apparentObjectType, t => getPropertyOfType(t, propertyName)); + if (propertySymbol && getDeclarationModifierFlagsFromSymbol(propertySymbol) & ModifierFlags.NonPublicAccessibilityModifier) { + error(accessNode, Diagnostics.Private_or_protected_member_0_cannot_be_accessed_on_a_type_parameter, unescapeLeadingUnderscores(propertyName)); + return errorType; } - const derived = getTargetSymbol(baseSymbol); - const baseDeclarationFlags = getDeclarationModifierFlagsFromSymbol(base); - - Debug.assert(!!derived, "derived should point to something, even if it is the base class' declaration."); - - // In order to resolve whether the inherited method was overridden in the base class or not, - // we compare the Symbols obtained. Since getTargetSymbol returns the symbol on the *uninstantiated* - // type declaration, derived and base resolve to the same symbol even in the case of generic classes. - if (derived === base) { - // derived class inherits base without override/redeclaration - const derivedClassDecl = getClassLikeDeclarationOfSymbol(type.symbol)!; - - // It is an error to inherit an abstract member without implementing it or being declared abstract. - // If there is no declaration for the derived class (as in the case of class expressions), - // then the class cannot be declared abstract. - if (baseDeclarationFlags & ModifierFlags.Abstract && (!derivedClassDecl || !hasModifier(derivedClassDecl, ModifierFlags.Abstract))) { - // Searches other base types for a declaration that would satisfy the inherited abstract member. - // (The class may have more than one base type via declaration merging with an interface with the - // same name.) - for (const otherBaseType of getBaseTypes(type)) { - if (otherBaseType === baseType) continue; - const baseSymbol = getPropertyOfObjectType(otherBaseType, base.escapedName); - const derivedElsewhere = baseSymbol && getTargetSymbol(baseSymbol); - if (derivedElsewhere && derivedElsewhere !== base) { - continue basePropertyCheck; - } - } - - if (derivedClassDecl.kind === SyntaxKind.ClassExpression) { - error(derivedClassDecl, Diagnostics.Non_abstract_class_expression_does_not_implement_inherited_abstract_member_0_from_class_1, - symbolToString(baseProperty), typeToString(baseType)); - } - else { - error(derivedClassDecl, Diagnostics.Non_abstract_class_0_does_not_implement_inherited_abstract_member_1_from_class_2, - typeToString(type), symbolToString(baseProperty), typeToString(baseType)); - } + } + } + error(accessNode, Diagnostics.Type_0_cannot_be_used_to_index_type_1, typeToString(indexType), typeToString(objectType)); + return errorType; + } + function checkIndexedAccessType(node: IndexedAccessTypeNode) { + checkSourceElement(node.objectType); + checkSourceElement(node.indexType); + checkIndexedAccessIndexType(getTypeFromIndexedAccessTypeNode(node), node); + } + function checkMappedType(node: MappedTypeNode) { + checkSourceElement(node.typeParameter); + checkSourceElement(node.type); + if (!node.type) { + reportImplicitAny(node, anyType); + } + const type = (getTypeFromMappedTypeNode(node)); + const constraintType = getConstraintTypeFromMappedType(type); + checkTypeAssignableTo(constraintType, keyofConstraintType, getEffectiveConstraintOfTypeParameter(node.typeParameter)); + } + function checkThisType(node: ThisTypeNode) { + getTypeFromThisTypeNode(node); + } + function checkTypeOperator(node: TypeOperatorNode) { + checkGrammarTypeOperatorNode(node); + checkSourceElement(node.type); + } + function checkConditionalType(node: ConditionalTypeNode) { + forEachChild(node, checkSourceElement); + } + function checkInferType(node: InferTypeNode) { + if (!findAncestor(node, n => n.parent && n.parent.kind === SyntaxKind.ConditionalType && (n.parent).extendsType === n)) { + grammarErrorOnNode(node, Diagnostics.infer_declarations_are_only_permitted_in_the_extends_clause_of_a_conditional_type); + } + checkSourceElement(node.typeParameter); + registerForUnusedIdentifiersCheck(node); + } + function checkImportType(node: ImportTypeNode) { + checkSourceElement(node.argument); + getTypeFromTypeNode(node); + } + function isPrivateWithinAmbient(node: Node): boolean { + return (hasModifier(node, ModifierFlags.Private) || isPrivateIdentifierPropertyDeclaration(node)) && !!(node.flags & NodeFlags.Ambient); + } + function getEffectiveDeclarationFlags(n: Declaration, flagsToCheck: ModifierFlags): ModifierFlags { + let flags = getCombinedModifierFlags(n); + // children of classes (even ambient classes) should not be marked as ambient or export + // because those flags have no useful semantics there. + if (n.parent.kind !== SyntaxKind.InterfaceDeclaration && + n.parent.kind !== SyntaxKind.ClassDeclaration && + n.parent.kind !== SyntaxKind.ClassExpression && + n.flags & NodeFlags.Ambient) { + if (!(flags & ModifierFlags.Ambient) && !(isModuleBlock(n.parent) && isModuleDeclaration(n.parent.parent) && isGlobalScopeAugmentation(n.parent.parent))) { + // It is nested in an ambient context, which means it is automatically exported + flags |= ModifierFlags.Export; + } + flags |= ModifierFlags.Ambient; + } + return flags & flagsToCheck; + } + function checkFunctionOrConstructorSymbol(symbol: ts.Symbol): void { + if (!produceDiagnostics) { + return; + } + function getCanonicalOverload(overloads: Declaration[], implementation: FunctionLikeDeclaration | undefined): Declaration { + // Consider the canonical set of flags to be the flags of the bodyDeclaration or the first declaration + // Error on all deviations from this canonical set of flags + // The caveat is that if some overloads are defined in lib.d.ts, we don't want to + // report the errors on those. To achieve this, we will say that the implementation is + // the canonical signature only if it is in the same container as the first overload + const implementationSharesContainerWithFirstOverload = implementation !== undefined && implementation.parent === overloads[0].parent; + return implementationSharesContainerWithFirstOverload ? implementation! : overloads[0]; + } + function checkFlagAgreementBetweenOverloads(overloads: Declaration[], implementation: FunctionLikeDeclaration | undefined, flagsToCheck: ModifierFlags, someOverloadFlags: ModifierFlags, allOverloadFlags: ModifierFlags): void { + // Error if some overloads have a flag that is not shared by all overloads. To find the + // deviations, we XOR someOverloadFlags with allOverloadFlags + const someButNotAllOverloadFlags = someOverloadFlags ^ allOverloadFlags; + if (someButNotAllOverloadFlags !== 0) { + const canonicalFlags = getEffectiveDeclarationFlags(getCanonicalOverload(overloads, implementation), flagsToCheck); + forEach(overloads, o => { + const deviation = getEffectiveDeclarationFlags(o, flagsToCheck) ^ canonicalFlags; + if (deviation & ModifierFlags.Export) { + error(getNameOfDeclaration(o), Diagnostics.Overload_signatures_must_all_be_exported_or_non_exported); + } + else if (deviation & ModifierFlags.Ambient) { + error(getNameOfDeclaration(o), Diagnostics.Overload_signatures_must_all_be_ambient_or_non_ambient); + } + else if (deviation & (ModifierFlags.Private | ModifierFlags.Protected)) { + error(getNameOfDeclaration(o) || o, Diagnostics.Overload_signatures_must_all_be_public_private_or_protected); + } + else if (deviation & ModifierFlags.Abstract) { + error(getNameOfDeclaration(o), Diagnostics.Overload_signatures_must_all_be_abstract_or_non_abstract); + } + }); + } + } + function checkQuestionTokenAgreementBetweenOverloads(overloads: Declaration[], implementation: FunctionLikeDeclaration | undefined, someHaveQuestionToken: boolean, allHaveQuestionToken: boolean): void { + if (someHaveQuestionToken !== allHaveQuestionToken) { + const canonicalHasQuestionToken = hasQuestionToken(getCanonicalOverload(overloads, implementation)); + forEach(overloads, o => { + const deviation = hasQuestionToken(o) !== canonicalHasQuestionToken; + if (deviation) { + error(getNameOfDeclaration(o), Diagnostics.Overload_signatures_must_all_be_optional_or_required); } + }); + } + } + const flagsToCheck: ModifierFlags = ModifierFlags.Export | ModifierFlags.Ambient | ModifierFlags.Private | ModifierFlags.Protected | ModifierFlags.Abstract; + let someNodeFlags: ModifierFlags = ModifierFlags.None; + let allNodeFlags = flagsToCheck; + let someHaveQuestionToken = false; + let allHaveQuestionToken = true; + let hasOverloads = false; + let bodyDeclaration: FunctionLikeDeclaration | undefined; + let lastSeenNonAmbientDeclaration: FunctionLikeDeclaration | undefined; + let previousDeclaration: SignatureDeclaration | undefined; + const declarations = symbol.declarations; + const isConstructor = (symbol.flags & SymbolFlags.Constructor) !== 0; + function reportImplementationExpectedError(node: SignatureDeclaration): void { + if (node.name && nodeIsMissing(node.name)) { + return; + } + let seen = false; + const subsequentNode = forEachChild(node.parent, c => { + if (seen) { + return c; } else { - // derived overrides base. - const derivedDeclarationFlags = getDeclarationModifierFlagsFromSymbol(derived); - if (baseDeclarationFlags & ModifierFlags.Private || derivedDeclarationFlags & ModifierFlags.Private) { - // either base or derived property is private - not override, skip it - continue; - } - - let errorMessage: DiagnosticMessage; - const basePropertyFlags = base.flags & SymbolFlags.PropertyOrAccessor; - const derivedPropertyFlags = derived.flags & SymbolFlags.PropertyOrAccessor; - if (basePropertyFlags && derivedPropertyFlags) { - // property/accessor is overridden with property/accessor - if (!compilerOptions.useDefineForClassFields - || baseDeclarationFlags & ModifierFlags.Abstract && !(base.valueDeclaration && isPropertyDeclaration(base.valueDeclaration) && base.valueDeclaration.initializer) - || base.valueDeclaration && base.valueDeclaration.parent.kind === SyntaxKind.InterfaceDeclaration - || derived.valueDeclaration && isBinaryExpression(derived.valueDeclaration)) { - // when the base property is abstract or from an interface, base/derived flags don't need to match - // same when the derived property is from an assignment - continue; - } - - const overriddenInstanceProperty = basePropertyFlags !== SymbolFlags.Property && derivedPropertyFlags === SymbolFlags.Property; - const overriddenInstanceAccessor = basePropertyFlags === SymbolFlags.Property && derivedPropertyFlags !== SymbolFlags.Property; - if (overriddenInstanceProperty || overriddenInstanceAccessor) { - const errorMessage = overriddenInstanceProperty ? - Diagnostics._0_is_defined_as_an_accessor_in_class_1_but_is_overridden_here_in_2_as_an_instance_property : - Diagnostics._0_is_defined_as_a_property_in_class_1_but_is_overridden_here_in_2_as_an_accessor; - error(getNameOfDeclaration(derived.valueDeclaration) || derived.valueDeclaration, errorMessage, symbolToString(base), typeToString(baseType), typeToString(type)); - } - else { - const uninitialized = find(derived.declarations, d => d.kind === SyntaxKind.PropertyDeclaration && !(d as PropertyDeclaration).initializer); - if (uninitialized - && !(derived.flags & SymbolFlags.Transient) - && !(baseDeclarationFlags & ModifierFlags.Abstract) - && !(derivedDeclarationFlags & ModifierFlags.Abstract) - && !derived.declarations.some(d => !!(d.flags & NodeFlags.Ambient))) { - const constructor = findConstructorDeclaration(getClassLikeDeclarationOfSymbol(type.symbol)!); - const propName = (uninitialized as PropertyDeclaration).name; - if ((uninitialized as PropertyDeclaration).exclamationToken - || !constructor - || !isIdentifier(propName) - || !strictNullChecks - || !isPropertyInitializedInConstructor(propName, type, constructor)) { - const errorMessage = Diagnostics.Property_0_will_overwrite_the_base_property_in_1_If_this_is_intentional_add_an_initializer_Otherwise_add_a_declare_modifier_or_remove_the_redundant_declaration; - error(getNameOfDeclaration(derived.valueDeclaration) || derived.valueDeclaration, errorMessage, symbolToString(base), typeToString(baseType)); - } - } - } - - // correct case - continue; - } - else if (isPrototypeProperty(base)) { - if (isPrototypeProperty(derived) || derived.flags & SymbolFlags.Property) { - // method is overridden with method or property -- correct case - continue; - } - else { - Debug.assert(!!(derived.flags & SymbolFlags.Accessor)); - errorMessage = Diagnostics.Class_0_defines_instance_member_function_1_but_extended_class_2_defines_it_as_instance_member_accessor; + seen = c === node; + } + }); + // We may be here because of some extra nodes between overloads that could not be parsed into a valid node. + // In this case the subsequent node is not really consecutive (.pos !== node.end), and we must ignore it here. + if (subsequentNode && subsequentNode.pos === node.end) { + if (subsequentNode.kind === node.kind) { + const errorNode: Node = (subsequentNode).name || subsequentNode; + const subsequentName = (subsequentNode).name; + if (node.name && subsequentName && ( + // both are private identifiers + isPrivateIdentifier(node.name) && isPrivateIdentifier(subsequentName) && node.name.escapedText === subsequentName.escapedText || + // Both are computed property names + // TODO: GH#17345: These are methods, so handle computed name case. (`Always allowing computed property names is *not* the correct behavior!) + isComputedPropertyName(node.name) && isComputedPropertyName(subsequentName) || + // Both are literal property names that are the same. + isPropertyNameLiteral(node.name) && isPropertyNameLiteral(subsequentName) && + getEscapedTextOfIdentifierOrLiteral(node.name) === getEscapedTextOfIdentifierOrLiteral(subsequentName))) { + const reportError = (node.kind === SyntaxKind.MethodDeclaration || node.kind === SyntaxKind.MethodSignature) && + hasModifier(node, ModifierFlags.Static) !== hasModifier(subsequentNode, ModifierFlags.Static); + // we can get here in two cases + // 1. mixed static and instance class members + // 2. something with the same name was defined before the set of overloads that prevents them from merging + // here we'll report error only for the first case since for second we should already report error in binder + if (reportError) { + const diagnostic = hasModifier(node, ModifierFlags.Static) ? Diagnostics.Function_overload_must_be_static : Diagnostics.Function_overload_must_not_be_static; + error(errorNode, diagnostic); } + return; } - else if (base.flags & SymbolFlags.Accessor) { - errorMessage = Diagnostics.Class_0_defines_instance_member_accessor_1_but_extended_class_2_defines_it_as_instance_member_function; - } - else { - errorMessage = Diagnostics.Class_0_defines_instance_member_property_1_but_extended_class_2_defines_it_as_instance_member_function; + if (nodeIsPresent((subsequentNode).body)) { + error(errorNode, Diagnostics.Function_implementation_name_must_be_0, declarationNameToString(node.name)); + return; } - - error(getNameOfDeclaration(derived.valueDeclaration) || derived.valueDeclaration, errorMessage, typeToString(baseType), symbolToString(base), typeToString(type)); } } - } - - function checkInheritedPropertiesAreIdentical(type: InterfaceType, typeNode: Node): boolean { - const baseTypes = getBaseTypes(type); - if (baseTypes.length < 2) { - return true; + const errorNode: Node = node.name || node; + if (isConstructor) { + error(errorNode, Diagnostics.Constructor_implementation_is_missing); } - - interface InheritanceInfoMap { prop: Symbol; containingType: Type; } - const seen = createUnderscoreEscapedMap(); - forEach(resolveDeclaredMembers(type).declaredProperties, p => { seen.set(p.escapedName, { prop: p, containingType: type }); }); - let ok = true; - - for (const base of baseTypes) { - const properties = getPropertiesOfType(getTypeWithThisArgument(base, type.thisType)); - for (const prop of properties) { - const existing = seen.get(prop.escapedName); - if (!existing) { - seen.set(prop.escapedName, { prop, containingType: base }); + else { + // Report different errors regarding non-consecutive blocks of declarations depending on whether + // the node in question is abstract. + if (hasModifier(node, ModifierFlags.Abstract)) { + error(errorNode, Diagnostics.All_declarations_of_an_abstract_method_must_be_consecutive); + } + else { + error(errorNode, Diagnostics.Function_implementation_is_missing_or_not_immediately_following_the_declaration); + } + } + } + let duplicateFunctionDeclaration = false; + let multipleConstructorImplementation = false; + let hasNonAmbientClass = false; + for (const current of declarations) { + const node = (current); + const inAmbientContext = node.flags & NodeFlags.Ambient; + const inAmbientContextOrInterface = node.parent.kind === SyntaxKind.InterfaceDeclaration || node.parent.kind === SyntaxKind.TypeLiteral || inAmbientContext; + if (inAmbientContextOrInterface) { + // check if declarations are consecutive only if they are non-ambient + // 1. ambient declarations can be interleaved + // i.e. this is legal + // declare function foo(); + // declare function bar(); + // declare function foo(); + // 2. mixing ambient and non-ambient declarations is a separate error that will be reported - do not want to report an extra one + previousDeclaration = undefined; + } + if ((node.kind === SyntaxKind.ClassDeclaration || node.kind === SyntaxKind.ClassExpression) && !inAmbientContext) { + hasNonAmbientClass = true; + } + if (node.kind === SyntaxKind.FunctionDeclaration || node.kind === SyntaxKind.MethodDeclaration || node.kind === SyntaxKind.MethodSignature || node.kind === SyntaxKind.Constructor) { + const currentNodeFlags = getEffectiveDeclarationFlags(node, flagsToCheck); + someNodeFlags |= currentNodeFlags; + allNodeFlags &= currentNodeFlags; + someHaveQuestionToken = someHaveQuestionToken || hasQuestionToken(node); + allHaveQuestionToken = allHaveQuestionToken && hasQuestionToken(node); + if (nodeIsPresent((node as FunctionLikeDeclaration).body) && bodyDeclaration) { + if (isConstructor) { + multipleConstructorImplementation = true; } else { - const isInheritedProperty = existing.containingType !== type; - if (isInheritedProperty && !isPropertyIdenticalTo(existing.prop, prop)) { - ok = false; - - const typeName1 = typeToString(existing.containingType); - const typeName2 = typeToString(base); - - let errorInfo = chainDiagnosticMessages(/*details*/ undefined, Diagnostics.Named_property_0_of_types_1_and_2_are_not_identical, symbolToString(prop), typeName1, typeName2); - errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Interface_0_cannot_simultaneously_extend_types_1_and_2, typeToString(type), typeName1, typeName2); - diagnostics.add(createDiagnosticForNodeFromMessageChain(typeNode, errorInfo)); - } + duplicateFunctionDeclaration = true; } } - } - - return ok; - } - - function checkPropertyInitialization(node: ClassLikeDeclaration) { - if (!strictNullChecks || !strictPropertyInitialization || node.flags & NodeFlags.Ambient) { - return; - } - const constructor = findConstructorDeclaration(node); - for (const member of node.members) { - if (getModifierFlags(member) & ModifierFlags.Ambient) { - continue; + else if (previousDeclaration && previousDeclaration.parent === node.parent && previousDeclaration.end !== node.pos) { + reportImplementationExpectedError(previousDeclaration); } - if (isInstancePropertyWithoutInitializer(member)) { - const propName = (member).name; - if (isIdentifier(propName) || isPrivateIdentifier(propName)) { - const type = getTypeOfSymbol(getSymbolOfNode(member)); - if (!(type.flags & TypeFlags.AnyOrUnknown || getFalsyFlags(type) & TypeFlags.Undefined)) { - if (!constructor || !isPropertyInitializedInConstructor(propName, type, constructor)) { - error(member.name, Diagnostics.Property_0_has_no_initializer_and_is_not_definitely_assigned_in_the_constructor, declarationNameToString(propName)); - } - } + if (nodeIsPresent((node as FunctionLikeDeclaration).body)) { + if (!bodyDeclaration) { + bodyDeclaration = (node as FunctionLikeDeclaration); } } + else { + hasOverloads = true; + } + previousDeclaration = node; + if (!inAmbientContextOrInterface) { + lastSeenNonAmbientDeclaration = (node as FunctionLikeDeclaration); + } } } - - function isInstancePropertyWithoutInitializer(node: Node) { - return node.kind === SyntaxKind.PropertyDeclaration && - !hasModifier(node, ModifierFlags.Static | ModifierFlags.Abstract) && - !(node).exclamationToken && - !(node).initializer; + if (multipleConstructorImplementation) { + forEach(declarations, declaration => { + error(declaration, Diagnostics.Multiple_constructor_implementations_are_not_allowed); + }); } - - function isPropertyInitializedInConstructor(propName: Identifier | PrivateIdentifier, propType: Type, constructor: ConstructorDeclaration) { - const reference = createPropertyAccess(createThis(), propName); - reference.expression.parent = reference; - reference.parent = constructor; - reference.flowNode = constructor.returnFlowNode; - const flowType = getFlowTypeOfReference(reference, propType, getOptionalType(propType)); - return !(getFalsyFlags(flowType) & TypeFlags.Undefined); + if (duplicateFunctionDeclaration) { + forEach(declarations, declaration => { + error(getNameOfDeclaration(declaration), Diagnostics.Duplicate_function_implementation); + }); } - - function checkInterfaceDeclaration(node: InterfaceDeclaration) { - // Grammar checking - if (!checkGrammarDecoratorsAndModifiers(node)) checkGrammarInterfaceDeclaration(node); - - checkTypeParameters(node.typeParameters); - if (produceDiagnostics) { - checkTypeNameIsReserved(node.name, Diagnostics.Interface_name_cannot_be_0); - - checkExportsOnMergedDeclarations(node); - const symbol = getSymbolOfNode(node); - checkTypeParameterListsIdentical(symbol); - - // Only check this symbol once - const firstInterfaceDecl = getDeclarationOfKind(symbol, SyntaxKind.InterfaceDeclaration); - if (node === firstInterfaceDecl) { - const type = getDeclaredTypeOfSymbol(symbol); - const typeWithThis = getTypeWithThisArgument(type); - // run subsequent checks only if first set succeeded - if (checkInheritedPropertiesAreIdentical(type, node.name)) { - for (const baseType of getBaseTypes(type)) { - checkTypeAssignableTo(typeWithThis, getTypeWithThisArgument(baseType, type.thisType), node.name, Diagnostics.Interface_0_incorrectly_extends_interface_1); - } - checkIndexConstraints(type); + if (hasNonAmbientClass && !isConstructor && symbol.flags & SymbolFlags.Function) { + // A non-ambient class cannot be an implementation for a non-constructor function/class merge + // TODO: The below just replicates our older error from when classes and functions were + // entirely unable to merge - a more helpful message like "Class declaration cannot implement overload list" + // might be warranted. :shrug: + forEach(declarations, declaration => { + addDuplicateDeclarationError(getNameOfDeclaration(declaration) || declaration, Diagnostics.Duplicate_identifier_0, symbolName(symbol), filter(declarations, d => d !== declaration)); + }); + } + // Abstract methods can't have an implementation -- in particular, they don't need one. + if (lastSeenNonAmbientDeclaration && !lastSeenNonAmbientDeclaration.body && + !hasModifier(lastSeenNonAmbientDeclaration, ModifierFlags.Abstract) && !lastSeenNonAmbientDeclaration.questionToken) { + reportImplementationExpectedError(lastSeenNonAmbientDeclaration); + } + if (hasOverloads) { + checkFlagAgreementBetweenOverloads(declarations, bodyDeclaration, flagsToCheck, someNodeFlags, allNodeFlags); + checkQuestionTokenAgreementBetweenOverloads(declarations, bodyDeclaration, someHaveQuestionToken, allHaveQuestionToken); + if (bodyDeclaration) { + const signatures = getSignaturesOfSymbol(symbol); + const bodySignature = getSignatureFromDeclaration(bodyDeclaration); + for (const signature of signatures) { + if (!isImplementationCompatibleWithOverload(bodySignature, signature)) { + addRelatedInfo(error(signature.declaration, Diagnostics.This_overload_signature_is_not_compatible_with_its_implementation_signature), createDiagnosticForNode(bodyDeclaration, Diagnostics.The_implementation_signature_is_declared_here)); + break; } } - checkObjectTypeForDuplicateDeclarations(node); } - forEach(getInterfaceBaseTypeNodes(node), heritageElement => { - if (!isEntityNameExpression(heritageElement.expression)) { - error(heritageElement.expression, Diagnostics.An_interface_can_only_extend_an_identifier_Slashqualified_name_with_optional_type_arguments); - } - checkTypeReferenceNode(heritageElement); - }); - - forEach(node.members, checkSourceElement); - - if (produceDiagnostics) { - checkTypeForDuplicateIndexSignatures(node); - registerForUnusedIdentifiersCheck(node); + } + } + function checkExportsOnMergedDeclarations(node: Declaration): void { + if (!produceDiagnostics) { + return; + } + // if localSymbol is defined on node then node itself is exported - check is required + let symbol = node.localSymbol; + if (!symbol) { + // local symbol is undefined => this declaration is non-exported. + // however symbol might contain other declarations that are exported + symbol = getSymbolOfNode(node)!; + if (!symbol.exportSymbol) { + // this is a pure local symbol (all declarations are non-exported) - no need to check anything + return; } } - - function checkTypeAliasDeclaration(node: TypeAliasDeclaration) { - // Grammar checking - checkGrammarDecoratorsAndModifiers(node); - - checkTypeNameIsReserved(node.name, Diagnostics.Type_alias_name_cannot_be_0); - checkExportsOnMergedDeclarations(node); - checkTypeParameters(node.typeParameters); - checkSourceElement(node.type); - registerForUnusedIdentifiersCheck(node); + // run the check only for the first declaration in the list + if (getDeclarationOfKind(symbol, node.kind) !== node) { + return; } - - function computeEnumMemberValues(node: EnumDeclaration) { - const nodeLinks = getNodeLinks(node); - if (!(nodeLinks.flags & NodeCheckFlags.EnumValuesComputed)) { - nodeLinks.flags |= NodeCheckFlags.EnumValuesComputed; - let autoValue: number | undefined = 0; - for (const member of node.members) { - const value = computeMemberValue(member, autoValue); - getNodeLinks(member).enumMemberValue = value; - autoValue = typeof value === "number" ? value + 1 : undefined; + let exportedDeclarationSpaces = DeclarationSpaces.None; + let nonExportedDeclarationSpaces = DeclarationSpaces.None; + let defaultExportedDeclarationSpaces = DeclarationSpaces.None; + for (const d of symbol.declarations) { + const declarationSpaces = getDeclarationSpaces(d); + const effectiveDeclarationFlags = getEffectiveDeclarationFlags(d, ModifierFlags.Export | ModifierFlags.Default); + if (effectiveDeclarationFlags & ModifierFlags.Export) { + if (effectiveDeclarationFlags & ModifierFlags.Default) { + defaultExportedDeclarationSpaces |= declarationSpaces; } - } - } - - function computeMemberValue(member: EnumMember, autoValue: number | undefined) { - if (isComputedNonLiteralName(member.name)) { - error(member.name, Diagnostics.Computed_property_names_are_not_allowed_in_enums); - } - else { - const text = getTextOfPropertyName(member.name); - if (isNumericLiteralName(text) && !isInfinityOrNaNString(text)) { - error(member.name, Diagnostics.An_enum_member_cannot_have_a_numeric_name); + else { + exportedDeclarationSpaces |= declarationSpaces; } } - if (member.initializer) { - return computeConstantValue(member); + else { + nonExportedDeclarationSpaces |= declarationSpaces; } - // In ambient non-const numeric enum declarations, enum members without initializers are - // considered computed members (as opposed to having auto-incremented values). - if (member.parent.flags & NodeFlags.Ambient && !isEnumConst(member.parent) && getEnumKind(getSymbolOfNode(member.parent)) === EnumKind.Numeric) { - return undefined; + } + // Spaces for anything not declared a 'default export'. + const nonDefaultExportedDeclarationSpaces = exportedDeclarationSpaces | nonExportedDeclarationSpaces; + const commonDeclarationSpacesForExportsAndLocals = exportedDeclarationSpaces & nonExportedDeclarationSpaces; + const commonDeclarationSpacesForDefaultAndNonDefault = defaultExportedDeclarationSpaces & nonDefaultExportedDeclarationSpaces; + if (commonDeclarationSpacesForExportsAndLocals || commonDeclarationSpacesForDefaultAndNonDefault) { + // declaration spaces for exported and non-exported declarations intersect + for (const d of symbol.declarations) { + const declarationSpaces = getDeclarationSpaces(d); + const name = getNameOfDeclaration(d); + // Only error on the declarations that contributed to the intersecting spaces. + if (declarationSpaces & commonDeclarationSpacesForDefaultAndNonDefault) { + error(name, Diagnostics.Merged_declaration_0_cannot_include_a_default_export_declaration_Consider_adding_a_separate_export_default_0_declaration_instead, declarationNameToString(name)); + } + else if (declarationSpaces & commonDeclarationSpacesForExportsAndLocals) { + error(name, Diagnostics.Individual_declarations_in_merged_declaration_0_must_be_all_exported_or_all_local, declarationNameToString(name)); + } } - // If the member declaration specifies no value, the member is considered a constant enum member. - // If the member is the first member in the enum declaration, it is assigned the value zero. - // Otherwise, it is assigned the value of the immediately preceding member plus one, and an error - // occurs if the immediately preceding member is not a constant enum member. - if (autoValue !== undefined) { - return autoValue; + } + function getDeclarationSpaces(decl: Declaration): DeclarationSpaces { + let d = (decl as Node); + switch (d.kind) { + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.TypeAliasDeclaration: + // A jsdoc typedef and callback are, by definition, type aliases. + // falls through + case SyntaxKind.JSDocTypedefTag: + case SyntaxKind.JSDocCallbackTag: + case SyntaxKind.JSDocEnumTag: + return DeclarationSpaces.ExportType; + case SyntaxKind.ModuleDeclaration: + return isAmbientModule((d as ModuleDeclaration)) || getModuleInstanceState((d as ModuleDeclaration)) !== ModuleInstanceState.NonInstantiated + ? DeclarationSpaces.ExportNamespace | DeclarationSpaces.ExportValue + : DeclarationSpaces.ExportNamespace; + case SyntaxKind.ClassDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.EnumMember: + return DeclarationSpaces.ExportType | DeclarationSpaces.ExportValue; + case SyntaxKind.SourceFile: + return DeclarationSpaces.ExportType | DeclarationSpaces.ExportValue | DeclarationSpaces.ExportNamespace; + case SyntaxKind.ExportAssignment: + // Export assigned entity name expressions act as aliases and should fall through, otherwise they export values + if (!isEntityNameExpression((d as ExportAssignment).expression)) { + return DeclarationSpaces.ExportValue; + } + d = (d as ExportAssignment).expression; + // The below options all declare an Alias, which is allowed to merge with other values within the importing module. + // falls through + case SyntaxKind.ImportEqualsDeclaration: + case SyntaxKind.NamespaceImport: + case SyntaxKind.ImportClause: + let result = DeclarationSpaces.None; + const target = resolveAlias(getSymbolOfNode(d)!); + forEach(target.declarations, d => { result |= getDeclarationSpaces(d); }); + return result; + case SyntaxKind.VariableDeclaration: + case SyntaxKind.BindingElement: + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.ImportSpecifier: // https://github.com/Microsoft/TypeScript/pull/7591 + case SyntaxKind.Identifier: // https://github.com/microsoft/TypeScript/issues/36098 + // Identifiers are used as declarations of assignment declarations whose parents may be + // SyntaxKind.CallExpression - `Object.defineProperty(thing, "aField", {value: 42});` + // SyntaxKind.ElementAccessExpression - `thing["aField"] = 42;` or `thing["aField"];` (with a doc comment on it) + // or SyntaxKind.PropertyAccessExpression - `thing.aField = 42;` + // all of which are pretty much always values, or at least imply a value meaning. + // It may be apprpriate to treat these as aliases in the future. + return DeclarationSpaces.ExportValue; + default: + return Debug.failBadSyntaxKind(d); } - error(member.name, Diagnostics.Enum_member_must_have_initializer); + } + } + function getAwaitedTypeOfPromise(type: ts.Type, errorNode?: Node, diagnosticMessage?: DiagnosticMessage, arg0?: string | number): ts.Type | undefined { + const promisedType = getPromisedTypeOfPromise(type, errorNode); + return promisedType && getAwaitedType(promisedType, errorNode, diagnosticMessage, arg0); + } + /** + * Gets the "promised type" of a promise. + * @param type The type of the promise. + * @remarks The "promised type" of a type is the type of the "value" parameter of the "onfulfilled" callback. + */ + function getPromisedTypeOfPromise(promise: ts.Type, errorNode?: Node): ts.Type | undefined { + // + // { // promise + // then( // thenFunction + // onfulfilled: ( // onfulfilledParameterType + // value: T // valueParameterType + // ) => any + // ): any; + // } + // + if (isTypeAny(promise)) { return undefined; } - - function computeConstantValue(member: EnumMember): string | number | undefined { - const enumKind = getEnumKind(getSymbolOfNode(member.parent)); - const isConstEnum = isEnumConst(member.parent); - const initializer = member.initializer!; - const value = enumKind === EnumKind.Literal && !isLiteralEnumMember(member) ? undefined : evaluate(initializer); - if (value !== undefined) { - if (isConstEnum && typeof value === "number" && !isFinite(value)) { - error(initializer, isNaN(value) ? - Diagnostics.const_enum_member_initializer_was_evaluated_to_disallowed_value_NaN : - Diagnostics.const_enum_member_initializer_was_evaluated_to_a_non_finite_value); - } - } - else if (enumKind === EnumKind.Literal) { - error(initializer, Diagnostics.Computed_values_are_not_permitted_in_an_enum_with_string_valued_members); - return 0; + const typeAsPromise = (promise); + if (typeAsPromise.promisedTypeOfPromise) { + return typeAsPromise.promisedTypeOfPromise; + } + if (isReferenceToType(promise, getGlobalPromiseType(/*reportErrors*/ false))) { + return typeAsPromise.promisedTypeOfPromise = getTypeArguments((promise))[0]; + } + const thenFunction = (getTypeOfPropertyOfType(promise, ("then" as __String))!); // TODO: GH#18217 + if (isTypeAny(thenFunction)) { + return undefined; + } + const thenSignatures = thenFunction ? getSignaturesOfType(thenFunction, SignatureKind.Call) : emptyArray; + if (thenSignatures.length === 0) { + if (errorNode) { + error(errorNode, Diagnostics.A_promise_must_have_a_then_method); } - else if (isConstEnum) { - error(initializer, Diagnostics.const_enum_member_initializers_can_only_contain_literal_values_and_other_computed_enum_values); + return undefined; + } + const onfulfilledParameterType = getTypeWithFacts(getUnionType(map(thenSignatures, getTypeOfFirstParameterOfSignature)), TypeFacts.NEUndefinedOrNull); + if (isTypeAny(onfulfilledParameterType)) { + return undefined; + } + const onfulfilledParameterSignatures = getSignaturesOfType(onfulfilledParameterType, SignatureKind.Call); + if (onfulfilledParameterSignatures.length === 0) { + if (errorNode) { + error(errorNode, Diagnostics.The_first_parameter_of_the_then_method_of_a_promise_must_be_a_callback); } - else if (member.parent.flags & NodeFlags.Ambient) { - error(initializer, Diagnostics.In_ambient_enum_declarations_member_initializer_must_be_constant_expression); + return undefined; + } + return typeAsPromise.promisedTypeOfPromise = getUnionType(map(onfulfilledParameterSignatures, getTypeOfFirstParameterOfSignature), UnionReduction.Subtype); + } + /** + * Gets the "awaited type" of a type. + * @param type The type to await. + * @remarks The "awaited type" of an expression is its "promised type" if the expression is a + * Promise-like type; otherwise, it is the type of the expression. This is used to reflect + * The runtime behavior of the `await` keyword. + */ + function checkAwaitedType(type: ts.Type, errorNode: Node, diagnosticMessage: DiagnosticMessage, arg0?: string | number): ts.Type { + const awaitedType = getAwaitedType(type, errorNode, diagnosticMessage, arg0); + return awaitedType || errorType; + } + function getAwaitedType(type: ts.Type, errorNode?: Node, diagnosticMessage?: DiagnosticMessage, arg0?: string | number): ts.Type | undefined { + const typeAsAwaitable = (type); + if (typeAsAwaitable.awaitedTypeOfType) { + return typeAsAwaitable.awaitedTypeOfType; + } + if (isTypeAny(type)) { + return typeAsAwaitable.awaitedTypeOfType = type; + } + if (type.flags & TypeFlags.Union) { + let types: ts.Type[] | undefined; + for (const constituentType of (type).types) { + types = append(types, getAwaitedType(constituentType, errorNode, diagnosticMessage, arg0)); } - else { - // Only here do we need to check that the initializer is assignable to the enum type. - checkTypeAssignableTo(checkExpression(initializer), getDeclaredTypeOfSymbol(getSymbolOfNode(member.parent)), initializer, /*headMessage*/ undefined); + if (!types) { + return undefined; } - return value; - - function evaluate(expr: Expression): string | number | undefined { - switch (expr.kind) { - case SyntaxKind.PrefixUnaryExpression: - const value = evaluate((expr).operand); - if (typeof value === "number") { - switch ((expr).operator) { - case SyntaxKind.PlusToken: return value; - case SyntaxKind.MinusToken: return -value; - case SyntaxKind.TildeToken: return ~value; - } - } - break; - case SyntaxKind.BinaryExpression: - const left = evaluate((expr).left); - const right = evaluate((expr).right); - if (typeof left === "number" && typeof right === "number") { - switch ((expr).operatorToken.kind) { - case SyntaxKind.BarToken: return left | right; - case SyntaxKind.AmpersandToken: return left & right; - case SyntaxKind.GreaterThanGreaterThanToken: return left >> right; - case SyntaxKind.GreaterThanGreaterThanGreaterThanToken: return left >>> right; - case SyntaxKind.LessThanLessThanToken: return left << right; - case SyntaxKind.CaretToken: return left ^ right; - case SyntaxKind.AsteriskToken: return left * right; - case SyntaxKind.SlashToken: return left / right; - case SyntaxKind.PlusToken: return left + right; - case SyntaxKind.MinusToken: return left - right; - case SyntaxKind.PercentToken: return left % right; - case SyntaxKind.AsteriskAsteriskToken: return left ** right; - } - } - else if (typeof left === "string" && typeof right === "string" && (expr).operatorToken.kind === SyntaxKind.PlusToken) { - return left + right; - } - break; - case SyntaxKind.StringLiteral: - case SyntaxKind.NoSubstitutionTemplateLiteral: - return (expr).text; - case SyntaxKind.NumericLiteral: - checkGrammarNumericLiteral(expr); - return +(expr).text; - case SyntaxKind.ParenthesizedExpression: - return evaluate((expr).expression); - case SyntaxKind.Identifier: - const identifier = expr; - if (isInfinityOrNaNString(identifier.escapedText)) { - return +(identifier.escapedText); - } - return nodeIsMissing(expr) ? 0 : evaluateEnumMember(expr, getSymbolOfNode(member.parent), identifier.escapedText); - case SyntaxKind.ElementAccessExpression: - case SyntaxKind.PropertyAccessExpression: - const ex = expr; - if (isConstantMemberAccess(ex)) { - const type = getTypeOfExpression(ex.expression); - if (type.symbol && type.symbol.flags & SymbolFlags.Enum) { - let name: __String; - if (ex.kind === SyntaxKind.PropertyAccessExpression) { - name = ex.name.escapedText; - } - else { - name = escapeLeadingUnderscores(cast(ex.argumentExpression, isLiteralExpression).text); - } - return evaluateEnumMember(expr, type.symbol, name); - } - } - break; + return typeAsAwaitable.awaitedTypeOfType = getUnionType(types); + } + const promisedType = getPromisedTypeOfPromise(type); + if (promisedType) { + if (type.id === promisedType.id || awaitedTypeStack.indexOf(promisedType.id) >= 0) { + // Verify that we don't have a bad actor in the form of a promise whose + // promised type is the same as the promise type, or a mutually recursive + // promise. If so, we return undefined as we cannot guess the shape. If this + // were the actual case in the JavaScript, this Promise would never resolve. + // + // An example of a bad actor with a singly-recursive promise type might + // be: + // + // interface BadPromise { + // then( + // onfulfilled: (value: BadPromise) => any, + // onrejected: (error: any) => any): BadPromise; + // } + // The above interface will pass the PromiseLike check, and return a + // promised type of `BadPromise`. Since this is a self reference, we + // don't want to keep recursing ad infinitum. + // + // An example of a bad actor in the form of a mutually-recursive + // promise type might be: + // + // interface BadPromiseA { + // then( + // onfulfilled: (value: BadPromiseB) => any, + // onrejected: (error: any) => any): BadPromiseB; + // } + // + // interface BadPromiseB { + // then( + // onfulfilled: (value: BadPromiseA) => any, + // onrejected: (error: any) => any): BadPromiseA; + // } + // + if (errorNode) { + error(errorNode, Diagnostics.Type_is_referenced_directly_or_indirectly_in_the_fulfillment_callback_of_its_own_then_method); } return undefined; } - - function evaluateEnumMember(expr: Expression, enumSymbol: Symbol, name: __String) { - const memberSymbol = enumSymbol.exports!.get(name); - if (memberSymbol) { - const declaration = memberSymbol.valueDeclaration; - if (declaration !== member) { - if (isBlockScopedNameDeclaredBeforeUse(declaration, member)) { - return getEnumMemberValue(declaration as EnumMember); - } - error(expr, Diagnostics.A_member_initializer_in_a_enum_declaration_cannot_reference_members_declared_after_it_including_members_defined_in_other_enums); - return 0; - } - } + // Keep track of the type we're about to unwrap to avoid bad recursive promise types. + // See the comments above for more information. + awaitedTypeStack.push(type.id); + const awaitedType = getAwaitedType(promisedType, errorNode, diagnosticMessage, arg0); + awaitedTypeStack.pop(); + if (!awaitedType) { return undefined; } + return typeAsAwaitable.awaitedTypeOfType = awaitedType; } - - function isConstantMemberAccess(node: Expression): boolean { - return node.kind === SyntaxKind.Identifier || - node.kind === SyntaxKind.PropertyAccessExpression && isConstantMemberAccess((node).expression) || - node.kind === SyntaxKind.ElementAccessExpression && isConstantMemberAccess((node).expression) && - isStringLiteralLike((node).argumentExpression); + // The type was not a promise, so it could not be unwrapped any further. + // As long as the type does not have a callable "then" property, it is + // safe to return the type; otherwise, an error will be reported in + // the call to getNonThenableType and we will return undefined. + // + // An example of a non-promise "thenable" might be: + // + // await { then(): void {} } + // + // The "thenable" does not match the minimal definition for a promise. When + // a Promise/A+-compatible or ES6 promise tries to adopt this value, the promise + // will never settle. We treat this as an error to help flag an early indicator + // of a runtime problem. If the user wants to return this value from an async + // function, they would need to wrap it in some other value. If they want it to + // be treated as a promise, they can cast to . + const thenFunction = getTypeOfPropertyOfType(type, ("then" as __String)); + if (thenFunction && getSignaturesOfType(thenFunction, SignatureKind.Call).length > 0) { + if (errorNode) { + if (!diagnosticMessage) + return Debug.fail(); + error(errorNode, diagnosticMessage, arg0); + } + return undefined; } - - function checkEnumDeclaration(node: EnumDeclaration) { - if (!produceDiagnostics) { + return typeAsAwaitable.awaitedTypeOfType = type; + } + /** + * Checks the return type of an async function to ensure it is a compatible + * Promise implementation. + * + * This checks that an async function has a valid Promise-compatible return type. + * An async function has a valid Promise-compatible return type if the resolved value + * of the return type has a construct signature that takes in an `initializer` function + * that in turn supplies a `resolve` function as one of its arguments and results in an + * object with a callable `then` signature. + * + * @param node The signature to check + */ + function checkAsyncFunctionReturnType(node: FunctionLikeDeclaration | MethodSignature, returnTypeNode: TypeNode) { + // As part of our emit for an async function, we will need to emit the entity name of + // the return type annotation as an expression. To meet the necessary runtime semantics + // for __awaiter, we must also check that the type of the declaration (e.g. the static + // side or "constructor" of the promise type) is compatible `PromiseConstructorLike`. + // + // An example might be (from lib.es6.d.ts): + // + // interface Promise { ... } + // interface PromiseConstructor { + // new (...): Promise; + // } + // declare var Promise: PromiseConstructor; + // + // When an async function declares a return type annotation of `Promise`, we + // need to get the type of the `Promise` variable declaration above, which would + // be `PromiseConstructor`. + // + // The same case applies to a class: + // + // declare class Promise { + // constructor(...); + // then(...): Promise; + // } + // + const returnType = getTypeFromTypeNode(returnTypeNode); + if (languageVersion >= ScriptTarget.ES2015) { + if (returnType === errorType) { return; } - - // Grammar checking - checkGrammarDecoratorsAndModifiers(node); - - checkTypeNameIsReserved(node.name, Diagnostics.Enum_name_cannot_be_0); - checkCollisionWithRequireExportsInGeneratedCode(node, node.name); - checkCollisionWithGlobalPromiseInGeneratedCode(node, node.name); - checkExportsOnMergedDeclarations(node); - node.members.forEach(checkEnumMember); - - computeEnumMemberValues(node); - - // Spec 2014 - Section 9.3: - // It isn't possible for one enum declaration to continue the automatic numbering sequence of another, - // and when an enum type has multiple declarations, only one declaration is permitted to omit a value - // for the first member. - // - // Only perform this check once per symbol - const enumSymbol = getSymbolOfNode(node); - const firstDeclaration = getDeclarationOfKind(enumSymbol, node.kind); - if (node === firstDeclaration) { - if (enumSymbol.declarations.length > 1) { - const enumIsConst = isEnumConst(node); - // check that const is placed\omitted on all enum declarations - forEach(enumSymbol.declarations, decl => { - if (isEnumDeclaration(decl) && isEnumConst(decl) !== enumIsConst) { - error(getNameOfDeclaration(decl), Diagnostics.Enum_declarations_must_all_be_const_or_non_const); - } - }); - } - - let seenEnumMissingInitialInitializer = false; - forEach(enumSymbol.declarations, declaration => { - // return true if we hit a violation of the rule, false otherwise - if (declaration.kind !== SyntaxKind.EnumDeclaration) { - return false; - } - - const enumDeclaration = declaration; - if (!enumDeclaration.members.length) { - return false; - } - - const firstEnumMember = enumDeclaration.members[0]; - if (!firstEnumMember.initializer) { - if (seenEnumMissingInitialInitializer) { - error(firstEnumMember.name, Diagnostics.In_an_enum_with_multiple_declarations_only_one_declaration_can_omit_an_initializer_for_its_first_enum_element); - } - else { - seenEnumMissingInitialInitializer = true; - } - } - }); + const globalPromiseType = getGlobalPromiseType(/*reportErrors*/ true); + if (globalPromiseType !== emptyGenericType && !isReferenceToType(returnType, globalPromiseType)) { + // The promise type was not a valid type reference to the global promise type, so we + // report an error and return the unknown type. + error(returnTypeNode, Diagnostics.The_return_type_of_an_async_function_or_method_must_be_the_global_Promise_T_type); + return; } } - - function checkEnumMember(node: EnumMember) { - if (isPrivateIdentifier(node.name)) { - error(node, Diagnostics.An_enum_member_cannot_be_named_with_a_private_identifier); + else { + // Always mark the type node as referenced if it points to a value + markTypeNodeAsReferenced(returnTypeNode); + if (returnType === errorType) { + return; + } + const promiseConstructorName = getEntityNameFromTypeNode(returnTypeNode); + if (promiseConstructorName === undefined) { + error(returnTypeNode, Diagnostics.Type_0_is_not_a_valid_async_function_return_type_in_ES5_SlashES3_because_it_does_not_refer_to_a_Promise_compatible_constructor_value, typeToString(returnType)); + return; + } + const promiseConstructorSymbol = resolveEntityName(promiseConstructorName, SymbolFlags.Value, /*ignoreErrors*/ true); + const promiseConstructorType = promiseConstructorSymbol ? getTypeOfSymbol(promiseConstructorSymbol) : errorType; + if (promiseConstructorType === errorType) { + if (promiseConstructorName.kind === SyntaxKind.Identifier && promiseConstructorName.escapedText === "Promise" && getTargetType(returnType) === getGlobalPromiseType(/*reportErrors*/ false)) { + error(returnTypeNode, Diagnostics.An_async_function_or_method_in_ES5_SlashES3_requires_the_Promise_constructor_Make_sure_you_have_a_declaration_for_the_Promise_constructor_or_include_ES2015_in_your_lib_option); + } + else { + error(returnTypeNode, Diagnostics.Type_0_is_not_a_valid_async_function_return_type_in_ES5_SlashES3_because_it_does_not_refer_to_a_Promise_compatible_constructor_value, entityNameToString(promiseConstructorName)); + } + return; + } + const globalPromiseConstructorLikeType = getGlobalPromiseConstructorLikeType(/*reportErrors*/ true); + if (globalPromiseConstructorLikeType === emptyObjectType) { + // If we couldn't resolve the global PromiseConstructorLike type we cannot verify + // compatibility with __awaiter. + error(returnTypeNode, Diagnostics.Type_0_is_not_a_valid_async_function_return_type_in_ES5_SlashES3_because_it_does_not_refer_to_a_Promise_compatible_constructor_value, entityNameToString(promiseConstructorName)); + return; + } + if (!checkTypeAssignableTo(promiseConstructorType, globalPromiseConstructorLikeType, returnTypeNode, Diagnostics.Type_0_is_not_a_valid_async_function_return_type_in_ES5_SlashES3_because_it_does_not_refer_to_a_Promise_compatible_constructor_value)) { + return; + } + // Verify there is no local declaration that could collide with the promise constructor. + const rootName = promiseConstructorName && getFirstIdentifier(promiseConstructorName); + const collidingSymbol = getSymbol((node.locals!), rootName.escapedText, SymbolFlags.Value); + if (collidingSymbol) { + error(collidingSymbol.valueDeclaration, Diagnostics.Duplicate_identifier_0_Compiler_uses_declaration_1_to_support_async_functions, idText(rootName), entityNameToString(promiseConstructorName)); + return; } } - - function getFirstNonAmbientClassOrFunctionDeclaration(symbol: Symbol): Declaration | undefined { - const declarations = symbol.declarations; - for (const declaration of declarations) { - if ((declaration.kind === SyntaxKind.ClassDeclaration || - (declaration.kind === SyntaxKind.FunctionDeclaration && nodeIsPresent((declaration).body))) && - !(declaration.flags & NodeFlags.Ambient)) { - return declaration; - } + checkAwaitedType(returnType, node, Diagnostics.The_return_type_of_an_async_function_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member); + } + /** Check a decorator */ + function checkDecorator(node: Decorator): void { + const signature = getResolvedSignature(node); + const returnType = getReturnTypeOfSignature(signature); + if (returnType.flags & TypeFlags.Any) { + return; + } + let expectedReturnType: ts.Type; + const headMessage = getDiagnosticHeadMessageForDecoratorResolution(node); + let errorInfo: DiagnosticMessageChain | undefined; + switch (node.parent.kind) { + case SyntaxKind.ClassDeclaration: + const classSymbol = getSymbolOfNode(node.parent); + const classConstructorType = getTypeOfSymbol(classSymbol); + expectedReturnType = getUnionType([classConstructorType, voidType]); + break; + case SyntaxKind.Parameter: + expectedReturnType = voidType; + errorInfo = chainDiagnosticMessages( + /*details*/ undefined, Diagnostics.The_return_type_of_a_parameter_decorator_function_must_be_either_void_or_any); + break; + case SyntaxKind.PropertyDeclaration: + expectedReturnType = voidType; + errorInfo = chainDiagnosticMessages( + /*details*/ undefined, Diagnostics.The_return_type_of_a_property_decorator_function_must_be_either_void_or_any); + break; + case SyntaxKind.MethodDeclaration: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + const methodType = getTypeOfNode(node.parent); + const descriptorType = createTypedPropertyDescriptorType(methodType); + expectedReturnType = getUnionType([descriptorType, voidType]); + break; + default: + return Debug.fail(); + } + checkTypeAssignableTo(returnType, expectedReturnType, node, headMessage, () => errorInfo); + } + /** + * If a TypeNode can be resolved to a value symbol imported from an external module, it is + * marked as referenced to prevent import elision. + */ + function markTypeNodeAsReferenced(node: TypeNode) { + markEntityNameOrEntityExpressionAsReference(node && getEntityNameFromTypeNode(node)); + } + function markEntityNameOrEntityExpressionAsReference(typeName: EntityNameOrEntityNameExpression | undefined) { + if (!typeName) + return; + const rootName = getFirstIdentifier(typeName); + const meaning = (typeName.kind === SyntaxKind.Identifier ? SymbolFlags.Type : SymbolFlags.Namespace) | SymbolFlags.Alias; + const rootSymbol = resolveName(rootName, rootName.escapedText, meaning, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isRefernce*/ true); + if (rootSymbol + && rootSymbol.flags & SymbolFlags.Alias + && symbolIsValue(rootSymbol) + && !isConstEnumOrConstEnumOnlyModule(resolveAlias(rootSymbol)) + && !getTypeOnlyAliasDeclaration(rootSymbol)) { + markAliasSymbolAsReferenced(rootSymbol); + } + } + /** + * This function marks the type used for metadata decorator as referenced if it is import + * from external module. + * This is different from markTypeNodeAsReferenced because it tries to simplify type nodes in + * union and intersection type + * @param node + */ + function markDecoratorMedataDataTypeNodeAsReferenced(node: TypeNode | undefined): void { + const entityName = getEntityNameForDecoratorMetadata(node); + if (entityName && isEntityName(entityName)) { + markEntityNameOrEntityExpressionAsReference(entityName); + } + } + function getEntityNameForDecoratorMetadata(node: TypeNode | undefined): EntityName | undefined { + if (node) { + switch (node.kind) { + case SyntaxKind.IntersectionType: + case SyntaxKind.UnionType: + return getEntityNameForDecoratorMetadataFromTypeList((node).types); + case SyntaxKind.ConditionalType: + return getEntityNameForDecoratorMetadataFromTypeList([(node).trueType, (node).falseType]); + case SyntaxKind.ParenthesizedType: + return getEntityNameForDecoratorMetadata((node).type); + case SyntaxKind.TypeReference: + return (node).typeName; } - return undefined; } - - function inSameLexicalScope(node1: Node, node2: Node) { - const container1 = getEnclosingBlockScopeContainer(node1); - const container2 = getEnclosingBlockScopeContainer(node2); - if (isGlobalSourceFile(container1)) { - return isGlobalSourceFile(container2); + } + function getEntityNameForDecoratorMetadataFromTypeList(types: readonly TypeNode[]): EntityName | undefined { + let commonEntityName: EntityName | undefined; + for (let typeNode of types) { + while (typeNode.kind === SyntaxKind.ParenthesizedType) { + typeNode = (typeNode as ParenthesizedTypeNode).type; // Skip parens if need be + } + if (typeNode.kind === SyntaxKind.NeverKeyword) { + continue; // Always elide `never` from the union/intersection if possible + } + if (!strictNullChecks && (typeNode.kind === SyntaxKind.NullKeyword || typeNode.kind === SyntaxKind.UndefinedKeyword)) { + continue; // Elide null and undefined from unions for metadata, just like what we did prior to the implementation of strict null checks + } + const individualEntityName = getEntityNameForDecoratorMetadata(typeNode); + if (!individualEntityName) { + // Individual is something like string number + // So it would be serialized to either that type or object + // Safe to return here + return undefined; } - else if (isGlobalSourceFile(container2)) { - return false; + if (commonEntityName) { + // Note this is in sync with the transformation that happens for type node. + // Keep this in sync with serializeUnionOrIntersectionType + // Verify if they refer to same entity and is identifier + // return undefined if they dont match because we would emit object + if (!isIdentifier(commonEntityName) || + !isIdentifier(individualEntityName) || + commonEntityName.escapedText !== individualEntityName.escapedText) { + return undefined; + } } else { - return container1 === container2; + commonEntityName = individualEntityName; } } - - function checkModuleDeclaration(node: ModuleDeclaration) { - if (produceDiagnostics) { - // Grammar checking - const isGlobalAugmentation = isGlobalScopeAugmentation(node); - const inAmbientContext = node.flags & NodeFlags.Ambient; - if (isGlobalAugmentation && !inAmbientContext) { - error(node.name, Diagnostics.Augmentations_for_the_global_scope_should_have_declare_modifier_unless_they_appear_in_already_ambient_context); - } - - const isAmbientExternalModule = isAmbientModule(node); - const contextErrorMessage = isAmbientExternalModule - ? Diagnostics.An_ambient_module_declaration_is_only_allowed_at_the_top_level_in_a_file - : Diagnostics.A_namespace_declaration_is_only_allowed_in_a_namespace_or_module; - if (checkGrammarModuleElementContext(node, contextErrorMessage)) { - // If we hit a module declaration in an illegal context, just bail out to avoid cascading errors. - return; - } - - if (!checkGrammarDecoratorsAndModifiers(node)) { - if (!inAmbientContext && node.name.kind === SyntaxKind.StringLiteral) { - grammarErrorOnNode(node.name, Diagnostics.Only_ambient_modules_can_use_quoted_names); - } - } - - if (isIdentifier(node.name)) { - checkCollisionWithRequireExportsInGeneratedCode(node, node.name); - checkCollisionWithGlobalPromiseInGeneratedCode(node, node.name); - } - - checkExportsOnMergedDeclarations(node); - const symbol = getSymbolOfNode(node); - - // The following checks only apply on a non-ambient instantiated module declaration. - if (symbol.flags & SymbolFlags.ValueModule - && !inAmbientContext - && symbol.declarations.length > 1 - && isInstantiatedModule(node, !!compilerOptions.preserveConstEnums || !!compilerOptions.isolatedModules)) { - const firstNonAmbientClassOrFunc = getFirstNonAmbientClassOrFunctionDeclaration(symbol); - if (firstNonAmbientClassOrFunc) { - if (getSourceFileOfNode(node) !== getSourceFileOfNode(firstNonAmbientClassOrFunc)) { - error(node.name, Diagnostics.A_namespace_declaration_cannot_be_in_a_different_file_from_a_class_or_function_with_which_it_is_merged); - } - else if (node.pos < firstNonAmbientClassOrFunc.pos) { - error(node.name, Diagnostics.A_namespace_declaration_cannot_be_located_prior_to_a_class_or_function_with_which_it_is_merged); + return commonEntityName; + } + function getParameterTypeNodeForDecoratorCheck(node: ParameterDeclaration): TypeNode | undefined { + const typeNode = getEffectiveTypeAnnotationNode(node); + return isRestParameter(node) ? getRestParameterElementType(typeNode) : typeNode; + } + /** Check the decorators of a node */ + function checkDecorators(node: Node): void { + if (!node.decorators) { + return; + } + // skip this check for nodes that cannot have decorators. These should have already had an error reported by + // checkGrammarDecorators. + if (!nodeCanBeDecorated(node, node.parent, node.parent.parent)) { + return; + } + if (!compilerOptions.experimentalDecorators) { + error(node, Diagnostics.Experimental_support_for_decorators_is_a_feature_that_is_subject_to_change_in_a_future_release_Set_the_experimentalDecorators_option_in_your_tsconfig_or_jsconfig_to_remove_this_warning); + } + const firstDecorator = node.decorators[0]; + checkExternalEmitHelpers(firstDecorator, ExternalEmitHelpers.Decorate); + if (node.kind === SyntaxKind.Parameter) { + checkExternalEmitHelpers(firstDecorator, ExternalEmitHelpers.Param); + } + if (compilerOptions.emitDecoratorMetadata) { + checkExternalEmitHelpers(firstDecorator, ExternalEmitHelpers.Metadata); + // we only need to perform these checks if we are emitting serialized type metadata for the target of a decorator. + switch (node.kind) { + case SyntaxKind.ClassDeclaration: + const constructor = getFirstConstructorWithBody((node)); + if (constructor) { + for (const parameter of constructor.parameters) { + markDecoratorMedataDataTypeNodeAsReferenced(getParameterTypeNodeForDecoratorCheck(parameter)); } } - - // if the module merges with a class declaration in the same lexical scope, - // we need to track this to ensure the correct emit. - const mergedClass = getDeclarationOfKind(symbol, SyntaxKind.ClassDeclaration); - if (mergedClass && - inSameLexicalScope(node, mergedClass)) { - getNodeLinks(node).flags |= NodeCheckFlags.LexicalModuleMergesWithClass; + break; + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + const otherKind = node.kind === SyntaxKind.GetAccessor ? SyntaxKind.SetAccessor : SyntaxKind.GetAccessor; + const otherAccessor = getDeclarationOfKind(getSymbolOfNode((node as AccessorDeclaration)), otherKind); + markDecoratorMedataDataTypeNodeAsReferenced(getAnnotatedAccessorTypeNode((node as AccessorDeclaration)) || otherAccessor && getAnnotatedAccessorTypeNode(otherAccessor)); + break; + case SyntaxKind.MethodDeclaration: + for (const parameter of (node).parameters) { + markDecoratorMedataDataTypeNodeAsReferenced(getParameterTypeNodeForDecoratorCheck(parameter)); } - } - - if (isAmbientExternalModule) { - if (isExternalModuleAugmentation(node)) { - // body of the augmentation should be checked for consistency only if augmentation was applied to its target (either global scope or module) - // otherwise we'll be swamped in cascading errors. - // We can detect if augmentation was applied using following rules: - // - augmentation for a global scope is always applied - // - augmentation for some external module is applied if symbol for augmentation is merged (it was combined with target module). - const checkBody = isGlobalAugmentation || (getSymbolOfNode(node).flags & SymbolFlags.Transient); - if (checkBody && node.body) { - for (const statement of node.body.statements) { - checkModuleAugmentationElement(statement, isGlobalAugmentation); - } - } + markDecoratorMedataDataTypeNodeAsReferenced(getEffectiveReturnTypeNode((node))); + break; + case SyntaxKind.PropertyDeclaration: + markDecoratorMedataDataTypeNodeAsReferenced(getEffectiveTypeAnnotationNode((node))); + break; + case SyntaxKind.Parameter: + markDecoratorMedataDataTypeNodeAsReferenced(getParameterTypeNodeForDecoratorCheck((node))); + const containingSignature = (node as ParameterDeclaration).parent; + for (const parameter of containingSignature.parameters) { + markDecoratorMedataDataTypeNodeAsReferenced(getParameterTypeNodeForDecoratorCheck(parameter)); } - else if (isGlobalSourceFile(node.parent)) { - if (isGlobalAugmentation) { - error(node.name, Diagnostics.Augmentations_for_the_global_scope_can_only_be_directly_nested_in_external_modules_or_ambient_module_declarations); - } - else if (isExternalModuleNameRelative(getTextOfIdentifierOrLiteral(node.name))) { - error(node.name, Diagnostics.Ambient_module_declaration_cannot_specify_relative_module_name); - } + break; + } + } + forEach(node.decorators, checkDecorator); + } + function checkFunctionDeclaration(node: FunctionDeclaration): void { + if (produceDiagnostics) { + checkFunctionOrMethodDeclaration(node); + checkGrammarForGenerator(node); + checkCollisionWithRequireExportsInGeneratedCode(node, node.name!); + checkCollisionWithGlobalPromiseInGeneratedCode(node, node.name!); + } + } + function checkJSDocTypeAliasTag(node: JSDocTypedefTag | JSDocCallbackTag) { + if (!node.typeExpression) { + // If the node had `@property` tags, `typeExpression` would have been set to the first property tag. + error(node.name, Diagnostics.JSDoc_typedef_tag_should_either_have_a_type_annotation_or_be_followed_by_property_or_member_tags); + } + if (node.name) { + checkTypeNameIsReserved(node.name, Diagnostics.Type_alias_name_cannot_be_0); + } + checkSourceElement(node.typeExpression); + } + function checkJSDocTemplateTag(node: JSDocTemplateTag): void { + checkSourceElement(node.constraint); + for (const tp of node.typeParameters) { + checkSourceElement(tp); + } + } + function checkJSDocTypeTag(node: JSDocTypeTag) { + checkSourceElement(node.typeExpression); + } + function checkJSDocParameterTag(node: JSDocParameterTag) { + checkSourceElement(node.typeExpression); + if (!getParameterSymbolFromJSDoc(node)) { + const decl = getHostSignatureFromJSDoc(node); + // don't issue an error for invalid hosts -- just functions -- + // and give a better error message when the host function mentions `arguments` + // but the tag doesn't have an array type + if (decl) { + const i = getJSDocTags(decl).filter(isJSDocParameterTag).indexOf(node); + if (i > -1 && i < decl.parameters.length && isBindingPattern(decl.parameters[i].name)) { + return; + } + if (!containsArgumentsReference(decl)) { + if (isQualifiedName(node.name)) { + error(node.name, Diagnostics.Qualified_name_0_is_not_allowed_without_a_leading_param_object_1, entityNameToString(node.name), entityNameToString(node.name.left)); } else { - if (isGlobalAugmentation) { - error(node.name, Diagnostics.Augmentations_for_the_global_scope_can_only_be_directly_nested_in_external_modules_or_ambient_module_declarations); - } - else { - // Node is not an augmentation and is not located on the script level. - // This means that this is declaration of ambient module that is located in other module or namespace which is prohibited. - error(node.name, Diagnostics.Ambient_modules_cannot_be_nested_in_other_modules_or_namespaces); - } + error(node.name, Diagnostics.JSDoc_param_tag_has_name_0_but_there_is_no_parameter_with_that_name, idText(node.name)); } } + else if (findLast(getJSDocTags(decl), isJSDocParameterTag) === node && + node.typeExpression && node.typeExpression.type && + !isArrayType(getTypeFromTypeNode(node.typeExpression.type))) { + error(node.name, Diagnostics.JSDoc_param_tag_has_name_0_but_there_is_no_parameter_with_that_name_It_would_match_arguments_if_it_had_an_array_type, idText(node.name.kind === SyntaxKind.QualifiedName ? node.name.right : node.name)); + } } - - if (node.body) { - checkSourceElement(node.body); - if (!isGlobalScopeAugmentation(node)) { - registerForUnusedIdentifiersCheck(node); + } + } + function checkJSDocFunctionType(node: JSDocFunctionType): void { + if (produceDiagnostics && !node.type && !isJSDocConstructSignature(node)) { + reportImplicitAny(node, anyType); + } + checkSignatureDeclaration(node); + } + function checkJSDocImplementsTag(node: JSDocImplementsTag): void { + const classLike = getJSDocHost(node); + if (!isClassDeclaration(classLike) && !isClassExpression(classLike)) { + error(classLike, Diagnostics.JSDoc_0_is_not_attached_to_a_class, idText(node.tagName)); + return; + } + } + function checkJSDocAugmentsTag(node: JSDocAugmentsTag): void { + const classLike = getJSDocHost(node); + if (!isClassDeclaration(classLike) && !isClassExpression(classLike)) { + error(classLike, Diagnostics.JSDoc_0_is_not_attached_to_a_class, idText(node.tagName)); + return; + } + const augmentsTags = getJSDocTags(classLike).filter(isJSDocAugmentsTag); + Debug.assert(augmentsTags.length > 0); + if (augmentsTags.length > 1) { + error(augmentsTags[1], Diagnostics.Class_declarations_cannot_have_more_than_one_augments_or_extends_tag); + } + const name = getIdentifierFromEntityNameExpression(node.class.expression); + const extend = getClassExtendsHeritageElement(classLike); + if (extend) { + const className = getIdentifierFromEntityNameExpression(extend.expression); + if (className && name.escapedText !== className.escapedText) { + error(name, Diagnostics.JSDoc_0_1_does_not_match_the_extends_2_clause, idText(node.tagName), idText(name), idText(className)); + } + } + } + function getIdentifierFromEntityNameExpression(node: Identifier | PropertyAccessExpression): Identifier | PrivateIdentifier; + function getIdentifierFromEntityNameExpression(node: Expression): Identifier | PrivateIdentifier | undefined; + function getIdentifierFromEntityNameExpression(node: Expression): Identifier | PrivateIdentifier | undefined { + switch (node.kind) { + case SyntaxKind.Identifier: + return node as Identifier; + case SyntaxKind.PropertyAccessExpression: + return (node as PropertyAccessExpression).name; + default: + return undefined; + } + } + function checkFunctionOrMethodDeclaration(node: FunctionDeclaration | MethodDeclaration | MethodSignature): void { + checkDecorators(node); + checkSignatureDeclaration(node); + const functionFlags = getFunctionFlags(node); + // Do not use hasDynamicName here, because that returns false for well known symbols. + // We want to perform checkComputedPropertyName for all computed properties, including + // well known symbols. + if (node.name && node.name.kind === SyntaxKind.ComputedPropertyName) { + // This check will account for methods in class/interface declarations, + // as well as accessors in classes/object literals + checkComputedPropertyName(node.name); + } + if (!hasNonBindableDynamicName(node)) { + // first we want to check the local symbol that contain this declaration + // - if node.localSymbol !== undefined - this is current declaration is exported and localSymbol points to the local symbol + // - if node.localSymbol === undefined - this node is non-exported so we can just pick the result of getSymbolOfNode + const symbol = getSymbolOfNode(node); + const localSymbol = node.localSymbol || symbol; + // Since the javascript won't do semantic analysis like typescript, + // if the javascript file comes before the typescript file and both contain same name functions, + // checkFunctionOrConstructorSymbol wouldn't be called if we didnt ignore javascript function. + const firstDeclaration = find(localSymbol.declarations, + // Get first non javascript function declaration + declaration => declaration.kind === node.kind && !(declaration.flags & NodeFlags.JavaScriptFile)); + // Only type check the symbol once + if (node === firstDeclaration) { + checkFunctionOrConstructorSymbol(localSymbol); + } + if (symbol.parent) { + // run check once for the first declaration + if (getDeclarationOfKind(symbol, node.kind) === node) { + // run check on export symbol to check that modifiers agree across all exported declarations + checkFunctionOrConstructorSymbol(symbol); } } } - - function checkModuleAugmentationElement(node: Node, isGlobalAugmentation: boolean): void { + const body = node.kind === SyntaxKind.MethodSignature ? undefined : node.body; + checkSourceElement(body); + checkAllCodePathsInNonVoidFunctionReturnOrThrow(node, getReturnTypeFromAnnotation(node)); + if (produceDiagnostics && !getEffectiveReturnTypeNode(node)) { + // Report an implicit any error if there is no body, no explicit return type, and node is not a private method + // in an ambient context + if (nodeIsMissing(body) && !isPrivateWithinAmbient(node)) { + reportImplicitAny(node, anyType); + } + if (functionFlags & FunctionFlags.Generator && nodeIsPresent(body)) { + // A generator with a body and no type annotation can still cause errors. It can error if the + // yielded values have no common supertype, or it can give an implicit any error if it has no + // yielded values. The only way to trigger these errors is to try checking its return type. + getReturnTypeOfSignature(getSignatureFromDeclaration(node)); + } + } + // A js function declaration can have a @type tag instead of a return type node, but that type must have a call signature + if (isInJSFile(node)) { + const typeTag = getJSDocTypeTag(node); + if (typeTag && typeTag.typeExpression && !getContextualCallSignature(getTypeFromTypeNode(typeTag.typeExpression), node)) { + error(typeTag, Diagnostics.The_type_of_a_function_declaration_must_match_the_function_s_signature); + } + } + } + function registerForUnusedIdentifiersCheck(node: PotentiallyUnusedIdentifier): void { + // May be in a call such as getTypeOfNode that happened to call this. But potentiallyUnusedIdentifiers is only defined in the scope of `checkSourceFile`. + if (produceDiagnostics) { + const sourceFile = getSourceFileOfNode(node); + let potentiallyUnusedIdentifiers = allPotentiallyUnusedIdentifiers.get(sourceFile.path); + if (!potentiallyUnusedIdentifiers) { + potentiallyUnusedIdentifiers = []; + allPotentiallyUnusedIdentifiers.set(sourceFile.path, potentiallyUnusedIdentifiers); + } + // TODO: GH#22580 + // Debug.assert(addToSeen(seenPotentiallyUnusedIdentifiers, getNodeId(node)), "Adding potentially-unused identifier twice"); + potentiallyUnusedIdentifiers.push(node); + } + } + type PotentiallyUnusedIdentifier = SourceFile | ModuleDeclaration | ClassLikeDeclaration | InterfaceDeclaration | Block | CaseBlock | ForStatement | ForInStatement | ForOfStatement | Exclude | TypeAliasDeclaration | InferTypeNode; + function checkUnusedIdentifiers(potentiallyUnusedIdentifiers: readonly PotentiallyUnusedIdentifier[], addDiagnostic: AddUnusedDiagnostic) { + for (const node of potentiallyUnusedIdentifiers) { switch (node.kind) { - case SyntaxKind.VariableStatement: - // error each individual name in variable statement instead of marking the entire variable statement - for (const decl of (node).declarationList.declarations) { - checkModuleAugmentationElement(decl, isGlobalAugmentation); + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ClassExpression: + checkUnusedClassMembers(node, addDiagnostic); + checkUnusedTypeParameters(node, addDiagnostic); + break; + case SyntaxKind.SourceFile: + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.Block: + case SyntaxKind.CaseBlock: + case SyntaxKind.ForStatement: + case SyntaxKind.ForInStatement: + case SyntaxKind.ForOfStatement: + checkUnusedLocalsAndParameters(node, addDiagnostic); + break; + case SyntaxKind.Constructor: + case SyntaxKind.FunctionExpression: + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.ArrowFunction: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + if (node.body) { // Don't report unused parameters in overloads + checkUnusedLocalsAndParameters(node, addDiagnostic); } + checkUnusedTypeParameters(node, addDiagnostic); break; - case SyntaxKind.ExportAssignment: - case SyntaxKind.ExportDeclaration: - grammarErrorOnFirstToken(node, Diagnostics.Exports_and_export_assignments_are_not_permitted_in_module_augmentations); + case SyntaxKind.MethodSignature: + case SyntaxKind.CallSignature: + case SyntaxKind.ConstructSignature: + case SyntaxKind.FunctionType: + case SyntaxKind.ConstructorType: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.InterfaceDeclaration: + checkUnusedTypeParameters(node, addDiagnostic); break; - case SyntaxKind.ImportEqualsDeclaration: - case SyntaxKind.ImportDeclaration: - grammarErrorOnFirstToken(node, Diagnostics.Imports_are_not_permitted_in_module_augmentations_Consider_moving_them_to_the_enclosing_external_module); + case SyntaxKind.InferType: + checkUnusedInferTypeParameter(node, addDiagnostic); break; - case SyntaxKind.BindingElement: - case SyntaxKind.VariableDeclaration: - const name = (node).name; - if (isBindingPattern(name)) { - for (const el of name.elements) { - // mark individual names in binding pattern - checkModuleAugmentationElement(el, isGlobalAugmentation); - } + default: + Debug.assertNever(node, "Node should not have been registered for unused identifiers check"); + } + } + } + function errorUnusedLocal(declaration: Declaration, name: string, addDiagnostic: AddUnusedDiagnostic) { + const node = getNameOfDeclaration(declaration) || declaration; + const message = isTypeDeclaration(declaration) ? Diagnostics._0_is_declared_but_never_used : Diagnostics._0_is_declared_but_its_value_is_never_read; + addDiagnostic(declaration, UnusedKind.Local, createDiagnosticForNode(node, message, name)); + } + function isIdentifierThatStartsWithUnderscore(node: Node) { + return isIdentifier(node) && idText(node).charCodeAt(0) === CharacterCodes._; + } + function checkUnusedClassMembers(node: ClassDeclaration | ClassExpression, addDiagnostic: AddUnusedDiagnostic): void { + for (const member of node.members) { + switch (member.kind) { + case SyntaxKind.MethodDeclaration: + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + if (member.kind === SyntaxKind.SetAccessor && member.symbol.flags & SymbolFlags.GetAccessor) { + // Already would have reported an error on the getter. break; } - // falls through - case SyntaxKind.ClassDeclaration: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.ModuleDeclaration: - case SyntaxKind.TypeAliasDeclaration: - if (isGlobalAugmentation) { - return; + const symbol = getSymbolOfNode(member); + if (!symbol.isReferenced + && (hasModifier(member, ModifierFlags.Private) || isNamedDeclaration(member) && isPrivateIdentifier(member.name)) + && !(member.flags & NodeFlags.Ambient)) { + addDiagnostic(member, UnusedKind.Local, createDiagnosticForNode((member.name!), Diagnostics._0_is_declared_but_its_value_is_never_read, symbolToString(symbol))); } - const symbol = getSymbolOfNode(node); - if (symbol) { - // module augmentations cannot introduce new names on the top level scope of the module - // this is done it two steps - // 1. quick check - if symbol for node is not merged - this is local symbol to this augmentation - report error - // 2. main check - report error if value declaration of the parent symbol is module augmentation) - let reportError = !(symbol.flags & SymbolFlags.Transient); - if (!reportError) { - // symbol should not originate in augmentation - reportError = !!symbol.parent && isExternalModuleAugmentation(symbol.parent.declarations[0]); + break; + case SyntaxKind.Constructor: + for (const parameter of (member).parameters) { + if (!parameter.symbol.isReferenced && hasModifier(parameter, ModifierFlags.Private)) { + addDiagnostic(parameter, UnusedKind.Local, createDiagnosticForNode(parameter.name, Diagnostics.Property_0_is_declared_but_its_value_is_never_read, symbolName(parameter.symbol))); } } break; + case SyntaxKind.IndexSignature: + case SyntaxKind.SemicolonClassElement: + // Can't be private + break; + default: + Debug.fail(); } } - - function getFirstNonModuleExportsIdentifier(node: EntityNameOrEntityNameExpression): Identifier { - switch (node.kind) { - case SyntaxKind.Identifier: - return node; - case SyntaxKind.QualifiedName: - do { - node = node.left; - } while (node.kind !== SyntaxKind.Identifier); - return node; - case SyntaxKind.PropertyAccessExpression: - do { - if (isModuleExportsAccessExpression(node.expression) && !isPrivateIdentifier(node.name)) { - return node.name; - } - node = node.expression; - } while (node.kind !== SyntaxKind.Identifier); - return node; - } + } + function checkUnusedInferTypeParameter(node: InferTypeNode, addDiagnostic: AddUnusedDiagnostic): void { + const { typeParameter } = node; + if (isTypeParameterUnused(typeParameter)) { + addDiagnostic(node, UnusedKind.Parameter, createDiagnosticForNode(node, Diagnostics._0_is_declared_but_its_value_is_never_read, idText(typeParameter.name))); } - - function checkExternalImportOrExportDeclaration(node: ImportDeclaration | ImportEqualsDeclaration | ExportDeclaration): boolean { - const moduleName = getExternalModuleName(node); - if (!moduleName || nodeIsMissing(moduleName)) { - // Should be a parse error. - return false; - } - if (!isStringLiteral(moduleName)) { - error(moduleName, Diagnostics.String_literal_expected); - return false; - } - const inAmbientExternalModule = node.parent.kind === SyntaxKind.ModuleBlock && isAmbientModule(node.parent.parent); - if (node.parent.kind !== SyntaxKind.SourceFile && !inAmbientExternalModule) { - error(moduleName, node.kind === SyntaxKind.ExportDeclaration ? - Diagnostics.Export_declarations_are_not_permitted_in_a_namespace : - Diagnostics.Import_declarations_in_a_namespace_cannot_reference_a_module); - return false; - } - if (inAmbientExternalModule && isExternalModuleNameRelative(moduleName.text)) { - // we have already reported errors on top level imports\exports in external module augmentations in checkModuleDeclaration - // no need to do this again. - if (!isTopLevelInExternalModuleAugmentation(node)) { - // TypeScript 1.0 spec (April 2013): 12.1.6 - // An ExternalImportDeclaration in an AmbientExternalModuleDeclaration may reference - // other external modules only through top - level external module names. - // Relative external module names are not permitted. - error(node, Diagnostics.Import_or_export_declaration_in_an_ambient_module_declaration_cannot_reference_module_through_relative_module_name); - return false; + } + function checkUnusedTypeParameters(node: ClassLikeDeclaration | SignatureDeclaration | InterfaceDeclaration | TypeAliasDeclaration, addDiagnostic: AddUnusedDiagnostic): void { + // Only report errors on the last declaration for the type parameter container; + // this ensures that all uses have been accounted for. + if (last(getSymbolOfNode(node).declarations) !== node) + return; + const typeParameters = getEffectiveTypeParameterDeclarations(node); + const seenParentsWithEveryUnused = new NodeSet(); + for (const typeParameter of typeParameters) { + if (!isTypeParameterUnused(typeParameter)) + continue; + const name = idText(typeParameter.name); + const { parent } = typeParameter; + if (parent.kind !== SyntaxKind.InferType && parent.typeParameters!.every(isTypeParameterUnused)) { + if (seenParentsWithEveryUnused.tryAdd(parent)) { + const range = isJSDocTemplateTag(parent) + // Whole @template tag + ? rangeOfNode(parent) + // Include the `<>` in the error message + : rangeOfTypeParameters((parent.typeParameters!)); + const only = typeParameters.length === 1; + const message = only ? Diagnostics._0_is_declared_but_its_value_is_never_read : Diagnostics.All_type_parameters_are_unused; + const arg0 = only ? name : undefined; + addDiagnostic(typeParameter, UnusedKind.Parameter, createFileDiagnostic(getSourceFileOfNode(parent), range.pos, range.end - range.pos, message, arg0)); } } - return true; - } - - function checkAliasSymbol(node: ImportEqualsDeclaration | ImportClause | NamespaceImport | ImportSpecifier | ExportSpecifier | NamespaceExport) { - let symbol = getSymbolOfNode(node); - const target = resolveAlias(symbol); - - const shouldSkipWithJSExpandoTargets = symbol.flags & SymbolFlags.Assignment; - if (!shouldSkipWithJSExpandoTargets && target !== unknownSymbol) { - // For external modules symbol represents local symbol for an alias. - // This local symbol will merge any other local declarations (excluding other aliases) - // and symbol.flags will contains combined representation for all merged declaration. - // Based on symbol.flags we can compute a set of excluded meanings (meaning that resolved alias should not have, - // otherwise it will conflict with some local declaration). Note that in addition to normal flags we include matching SymbolFlags.Export* - // in order to prevent collisions with declarations that were exported from the current module (they still contribute to local names). - symbol = getMergedSymbol(symbol.exportSymbol || symbol); - const excludedMeanings = - (symbol.flags & (SymbolFlags.Value | SymbolFlags.ExportValue) ? SymbolFlags.Value : 0) | - (symbol.flags & SymbolFlags.Type ? SymbolFlags.Type : 0) | - (symbol.flags & SymbolFlags.Namespace ? SymbolFlags.Namespace : 0); - if (target.flags & excludedMeanings) { - const message = node.kind === SyntaxKind.ExportSpecifier ? - Diagnostics.Export_declaration_conflicts_with_exported_declaration_of_0 : - Diagnostics.Import_declaration_conflicts_with_local_declaration_of_0; - error(node, message, symbolToString(symbol)); - } - - // Don't allow to re-export something with no value side when `--isolatedModules` is set. - if (compilerOptions.isolatedModules - && node.kind === SyntaxKind.ExportSpecifier - && !node.parent.parent.isTypeOnly - && !(target.flags & SymbolFlags.Value) - && !(node.flags & NodeFlags.Ambient)) { - error(node, Diagnostics.Re_exporting_a_type_when_the_isolatedModules_flag_is_provided_requires_using_export_type); - } + else { + addDiagnostic(typeParameter, UnusedKind.Parameter, createDiagnosticForNode(typeParameter, Diagnostics._0_is_declared_but_its_value_is_never_read, name)); } } - - function checkImportBinding(node: ImportEqualsDeclaration | ImportClause | NamespaceImport | ImportSpecifier) { - checkCollisionWithRequireExportsInGeneratedCode(node, node.name!); - checkCollisionWithGlobalPromiseInGeneratedCode(node, node.name!); - checkAliasSymbol(node); + } + function isTypeParameterUnused(typeParameter: TypeParameterDeclaration): boolean { + return !((getMergedSymbol(typeParameter.symbol).isReferenced!) & SymbolFlags.TypeParameter) && !isIdentifierThatStartsWithUnderscore(typeParameter.name); + } + function addToGroup(map: ts.Map<[K, V[]]>, key: K, value: V, getKey: (key: K) => number | string): void { + const keyString = String(getKey(key)); + const group = map.get(keyString); + if (group) { + group[1].push(value); } - - function checkImportDeclaration(node: ImportDeclaration) { - if (checkGrammarModuleElementContext(node, Diagnostics.An_import_declaration_can_only_be_used_in_a_namespace_or_module)) { - // If we hit an import declaration in an illegal context, just bail out to avoid cascading errors. + else { + map.set(keyString, [key, [value]]); + } + } + function tryGetRootParameterDeclaration(node: Node): ParameterDeclaration | undefined { + return tryCast(getRootDeclaration(node), isParameter); + } + function checkUnusedLocalsAndParameters(nodeWithLocals: Node, addDiagnostic: AddUnusedDiagnostic): void { + // Ideally we could use the ImportClause directly as a key, but must wait until we have full ES6 maps. So must store key along with value. + const unusedImports = createMap<[ImportClause, ImportedDeclaration[]]>(); + const unusedDestructures = createMap<[ObjectBindingPattern, BindingElement[]]>(); + const unusedVariables = createMap<[VariableDeclarationList, VariableDeclaration[]]>(); + nodeWithLocals.locals!.forEach(local => { + // If it's purely a type parameter, ignore, will be checked in `checkUnusedTypeParameters`. + // If it's a type parameter merged with a parameter, check if the parameter-side is used. + if (local.flags & SymbolFlags.TypeParameter ? !(local.flags & SymbolFlags.Variable && !((local.isReferenced!) & SymbolFlags.Variable)) : local.isReferenced || local.exportSymbol) { return; } - if (!checkGrammarDecoratorsAndModifiers(node) && hasModifiers(node)) { - grammarErrorOnFirstToken(node, Diagnostics.An_import_declaration_cannot_have_modifiers); - } - if (checkExternalImportOrExportDeclaration(node)) { - const importClause = node.importClause; - if (importClause && !checkGrammarImportClause(importClause)) { - if (importClause.name) { - checkImportBinding(importClause); + for (const declaration of local.declarations) { + if (isAmbientModule(declaration) || + (isVariableDeclaration(declaration) && isForInOrOfStatement(declaration.parent.parent) || isImportedDeclaration(declaration)) && isIdentifierThatStartsWithUnderscore(declaration.name!)) { + continue; + } + if (isImportedDeclaration(declaration)) { + addToGroup(unusedImports, importClauseFromImported(declaration), declaration, getNodeId); + } + else if (isBindingElement(declaration) && isObjectBindingPattern(declaration.parent)) { + // In `{ a, ...b }, `a` is considered used since it removes a property from `b`. `b` may still be unused though. + const lastElement = last(declaration.parent.elements); + if (declaration === lastElement || !last(declaration.parent.elements).dotDotDotToken) { + addToGroup(unusedDestructures, declaration.parent, declaration, getNodeId); } - if (importClause.namedBindings) { - if (importClause.namedBindings.kind === SyntaxKind.NamespaceImport) { - checkImportBinding(importClause.namedBindings); - } - else { - const moduleExisted = resolveExternalModuleName(node, node.moduleSpecifier); - if (moduleExisted) { - forEach(importClause.namedBindings.elements, checkImportBinding); - } + } + else if (isVariableDeclaration(declaration)) { + addToGroup(unusedVariables, declaration.parent, declaration, getNodeId); + } + else { + const parameter = local.valueDeclaration && tryGetRootParameterDeclaration(local.valueDeclaration); + const name = local.valueDeclaration && getNameOfDeclaration(local.valueDeclaration); + if (parameter && name) { + if (!isParameterPropertyDeclaration(parameter, parameter.parent) && !parameterIsThisKeyword(parameter) && !isIdentifierThatStartsWithUnderscore(name)) { + addDiagnostic(parameter, UnusedKind.Parameter, createDiagnosticForNode(name, Diagnostics._0_is_declared_but_its_value_is_never_read, symbolName(local))); } } + else { + errorUnusedLocal(declaration, symbolName(local), addDiagnostic); + } + } + } + }); + unusedImports.forEach(([importClause, unuseds]) => { + const importDecl = importClause.parent; + const nDeclarations = (importClause.name ? 1 : 0) + + (importClause.namedBindings ? + (importClause.namedBindings.kind === SyntaxKind.NamespaceImport ? 1 : importClause.namedBindings.elements.length) + : 0); + if (nDeclarations === unuseds.length) { + addDiagnostic(importDecl, UnusedKind.Local, unuseds.length === 1 + ? createDiagnosticForNode(importDecl, Diagnostics._0_is_declared_but_its_value_is_never_read, idText((first(unuseds).name!))) + : createDiagnosticForNode(importDecl, Diagnostics.All_imports_in_import_declaration_are_unused)); + } + else { + for (const unused of unuseds) + errorUnusedLocal(unused, idText((unused.name!)), addDiagnostic); + } + }); + unusedDestructures.forEach(([bindingPattern, bindingElements]) => { + const kind = tryGetRootParameterDeclaration(bindingPattern.parent) ? UnusedKind.Parameter : UnusedKind.Local; + if (bindingPattern.elements.length === bindingElements.length) { + if (bindingElements.length === 1 && bindingPattern.parent.kind === SyntaxKind.VariableDeclaration && bindingPattern.parent.parent.kind === SyntaxKind.VariableDeclarationList) { + addToGroup(unusedVariables, bindingPattern.parent.parent, bindingPattern.parent, getNodeId); + } + else { + addDiagnostic(bindingPattern, kind, bindingElements.length === 1 + ? createDiagnosticForNode(bindingPattern, Diagnostics._0_is_declared_but_its_value_is_never_read, bindingNameText(first(bindingElements).name)) + : createDiagnosticForNode(bindingPattern, Diagnostics.All_destructured_elements_are_unused)); + } + } + else { + for (const e of bindingElements) { + addDiagnostic(e, kind, createDiagnosticForNode(e, Diagnostics._0_is_declared_but_its_value_is_never_read, bindingNameText(e.name))); + } + } + }); + unusedVariables.forEach(([declarationList, declarations]) => { + if (declarationList.declarations.length === declarations.length) { + addDiagnostic(declarationList, UnusedKind.Local, declarations.length === 1 + ? createDiagnosticForNode(first(declarations).name, Diagnostics._0_is_declared_but_its_value_is_never_read, bindingNameText(first(declarations).name)) + : createDiagnosticForNode(declarationList.parent.kind === SyntaxKind.VariableStatement ? declarationList.parent : declarationList, Diagnostics.All_variables_are_unused)); + } + else { + for (const decl of declarations) { + addDiagnostic(decl, UnusedKind.Local, createDiagnosticForNode(decl, Diagnostics._0_is_declared_but_its_value_is_never_read, bindingNameText(decl.name))); } } + }); + } + function bindingNameText(name: BindingName): string { + switch (name.kind) { + case SyntaxKind.Identifier: + return idText(name); + case SyntaxKind.ArrayBindingPattern: + case SyntaxKind.ObjectBindingPattern: + return bindingNameText(cast(first(name.elements), isBindingElement).name); + default: + return Debug.assertNever(name); } - - function checkImportEqualsDeclaration(node: ImportEqualsDeclaration) { - if (checkGrammarModuleElementContext(node, Diagnostics.An_import_declaration_can_only_be_used_in_a_namespace_or_module)) { - // If we hit an import declaration in an illegal context, just bail out to avoid cascading errors. - return; + } + type ImportedDeclaration = ImportClause | ImportSpecifier | NamespaceImport; + function isImportedDeclaration(node: Node): node is ImportedDeclaration { + return node.kind === SyntaxKind.ImportClause || node.kind === SyntaxKind.ImportSpecifier || node.kind === SyntaxKind.NamespaceImport; + } + function importClauseFromImported(decl: ImportedDeclaration): ImportClause { + return decl.kind === SyntaxKind.ImportClause ? decl : decl.kind === SyntaxKind.NamespaceImport ? decl.parent : decl.parent.parent; + } + function checkBlock(node: Block) { + // Grammar checking for SyntaxKind.Block + if (node.kind === SyntaxKind.Block) { + checkGrammarStatementInAmbientContext(node); + } + if (isFunctionOrModuleBlock(node)) { + const saveFlowAnalysisDisabled = flowAnalysisDisabled; + forEach(node.statements, checkSourceElement); + flowAnalysisDisabled = saveFlowAnalysisDisabled; + } + else { + forEach(node.statements, checkSourceElement); + } + if (node.locals) { + registerForUnusedIdentifiersCheck(node); + } + } + function checkCollisionWithArgumentsInGeneratedCode(node: SignatureDeclaration) { + // no rest parameters \ declaration context \ overload - no codegen impact + if (languageVersion >= ScriptTarget.ES2015 || compilerOptions.noEmit || !hasRestParameter(node) || node.flags & NodeFlags.Ambient || nodeIsMissing((node).body)) { + return; + } + forEach(node.parameters, p => { + if (p.name && !isBindingPattern(p.name) && p.name.escapedText === argumentsSymbol.escapedName) { + error(p, Diagnostics.Duplicate_identifier_arguments_Compiler_uses_arguments_to_initialize_rest_parameters); } - - checkGrammarDecoratorsAndModifiers(node); - if (isInternalModuleImportEqualsDeclaration(node) || checkExternalImportOrExportDeclaration(node)) { - checkImportBinding(node); - if (hasModifier(node, ModifierFlags.Export)) { - markExportAsReferenced(node); - } - if (node.moduleReference.kind !== SyntaxKind.ExternalModuleReference) { - const target = resolveAlias(getSymbolOfNode(node)); - if (target !== unknownSymbol) { - if (target.flags & SymbolFlags.Value) { - // Target is a value symbol, check that it is not hidden by a local declaration with the same name - const moduleName = getFirstIdentifier(node.moduleReference); - if (!(resolveEntityName(moduleName, SymbolFlags.Value | SymbolFlags.Namespace)!.flags & SymbolFlags.Namespace)) { - error(moduleName, Diagnostics.Module_0_is_hidden_by_a_local_declaration_with_the_same_name, declarationNameToString(moduleName)); - } - } - if (target.flags & SymbolFlags.Type) { - checkTypeNameIsReserved(node.name, Diagnostics.Import_name_cannot_be_0); - } - } + }); + } + function needCollisionCheckForIdentifier(node: Node, identifier: Identifier | undefined, name: string): boolean { + if (!(identifier && identifier.escapedText === name)) { + return false; + } + if (node.kind === SyntaxKind.PropertyDeclaration || + node.kind === SyntaxKind.PropertySignature || + node.kind === SyntaxKind.MethodDeclaration || + node.kind === SyntaxKind.MethodSignature || + node.kind === SyntaxKind.GetAccessor || + node.kind === SyntaxKind.SetAccessor) { + // it is ok to have member named '_super' or '_this' - member access is always qualified + return false; + } + if (node.flags & NodeFlags.Ambient) { + // ambient context - no codegen impact + return false; + } + const root = getRootDeclaration(node); + if (root.kind === SyntaxKind.Parameter && nodeIsMissing((root.parent).body)) { + // just an overload - no codegen impact + return false; + } + return true; + } + // this function will run after checking the source file so 'CaptureThis' is correct for all nodes + function checkIfThisIsCapturedInEnclosingScope(node: Node): void { + findAncestor(node, current => { + if (getNodeCheckFlags(current) & NodeCheckFlags.CaptureThis) { + const isDeclaration = node.kind !== SyntaxKind.Identifier; + if (isDeclaration) { + error(getNameOfDeclaration((node)), Diagnostics.Duplicate_identifier_this_Compiler_uses_variable_declaration_this_to_capture_this_reference); } else { - if (moduleKind >= ModuleKind.ES2015 && !(node.flags & NodeFlags.Ambient)) { - // Import equals declaration is deprecated in es6 or above - grammarErrorOnNode(node, Diagnostics.Import_assignment_cannot_be_used_when_targeting_ECMAScript_modules_Consider_using_import_Asterisk_as_ns_from_mod_import_a_from_mod_import_d_from_mod_or_another_module_format_instead); - } + error(node, Diagnostics.Expression_resolves_to_variable_declaration_this_that_compiler_uses_to_capture_this_reference); } + return true; } - } - - function checkExportDeclaration(node: ExportDeclaration) { - if (checkGrammarModuleElementContext(node, Diagnostics.An_export_declaration_can_only_be_used_in_a_module)) { - // If we hit an export in an illegal context, just bail out to avoid cascading errors. - return; - } - - if (!checkGrammarDecoratorsAndModifiers(node) && hasModifiers(node)) { - grammarErrorOnFirstToken(node, Diagnostics.An_export_declaration_cannot_have_modifiers); - } - - if (node.moduleSpecifier && node.exportClause && isNamedExports(node.exportClause) && length(node.exportClause.elements) && languageVersion === ScriptTarget.ES3) { - checkExternalEmitHelpers(node, ExternalEmitHelpers.CreateBinding); - } - - checkGrammarExportDeclaration(node); - if (!node.moduleSpecifier || checkExternalImportOrExportDeclaration(node)) { - if (node.exportClause) { - // export { x, y } - // export { x, y } from "foo" - if (isNamedExports(node.exportClause)) { - forEach(node.exportClause.elements, checkExportSpecifier); - } - else if(!isNamespaceExport(node.exportClause)) { - checkImportBinding(node.exportClause); - } - - const inAmbientExternalModule = node.parent.kind === SyntaxKind.ModuleBlock && isAmbientModule(node.parent.parent); - const inAmbientNamespaceDeclaration = !inAmbientExternalModule && node.parent.kind === SyntaxKind.ModuleBlock && - !node.moduleSpecifier && node.flags & NodeFlags.Ambient; - if (node.parent.kind !== SyntaxKind.SourceFile && !inAmbientExternalModule && !inAmbientNamespaceDeclaration) { - error(node, Diagnostics.Export_declarations_are_not_permitted_in_a_namespace); - } + return false; + }); + } + function checkIfNewTargetIsCapturedInEnclosingScope(node: Node): void { + findAncestor(node, current => { + if (getNodeCheckFlags(current) & NodeCheckFlags.CaptureNewTarget) { + const isDeclaration = node.kind !== SyntaxKind.Identifier; + if (isDeclaration) { + error(getNameOfDeclaration((node)), Diagnostics.Duplicate_identifier_newTarget_Compiler_uses_variable_declaration_newTarget_to_capture_new_target_meta_property_reference); } else { - // export * from "foo" - const moduleSymbol = resolveExternalModuleName(node, node.moduleSpecifier!); - if (moduleSymbol && hasExportAssignmentSymbol(moduleSymbol)) { - error(node.moduleSpecifier, Diagnostics.Module_0_uses_export_and_cannot_be_used_with_export_Asterisk, symbolToString(moduleSymbol)); - } - - if (moduleKind !== ModuleKind.System && moduleKind < ModuleKind.ES2015) { - checkExternalEmitHelpers(node, ExternalEmitHelpers.ExportStar); - } + error(node, Diagnostics.Expression_resolves_to_variable_declaration_newTarget_that_compiler_uses_to_capture_new_target_meta_property_reference); } + return true; } + return false; + }); + } + function checkWeakMapCollision(node: Node) { + const enclosingBlockScope = getEnclosingBlockScopeContainer(node); + if (getNodeCheckFlags(enclosingBlockScope) & NodeCheckFlags.ContainsClassWithPrivateIdentifiers) { + error(node, Diagnostics.Compiler_reserves_name_0_when_emitting_private_identifier_downlevel, "WeakMap"); } - - function checkGrammarExportDeclaration(node: ExportDeclaration): boolean { - const isTypeOnlyExportStar = node.isTypeOnly && node.exportClause?.kind !== SyntaxKind.NamedExports; - if (isTypeOnlyExportStar) { - grammarErrorOnNode(node, Diagnostics.Only_named_exports_may_use_export_type); - } - return !isTypeOnlyExportStar; + } + function checkCollisionWithRequireExportsInGeneratedCode(node: Node, name: Identifier) { + // No need to check for require or exports for ES6 modules and later + if (moduleKind >= ModuleKind.ES2015 || compilerOptions.noEmit) { + return; } - - function checkGrammarModuleElementContext(node: Statement, errorMessage: DiagnosticMessage): boolean { - const isInAppropriateContext = node.parent.kind === SyntaxKind.SourceFile || node.parent.kind === SyntaxKind.ModuleBlock || node.parent.kind === SyntaxKind.ModuleDeclaration; - if (!isInAppropriateContext) { - grammarErrorOnFirstToken(node, errorMessage); - } - return !isInAppropriateContext; + if (!needCollisionCheckForIdentifier(node, name, "require") && !needCollisionCheckForIdentifier(node, name, "exports")) { + return; } - - function importClauseContainsReferencedImport(importClause: ImportClause) { - return forEachImportClauseDeclaration(importClause, declaration => { - return !!getSymbolOfNode(declaration).isReferenced; - }); + // Uninstantiated modules shouldnt do this check + if (isModuleDeclaration(node) && getModuleInstanceState(node) !== ModuleInstanceState.Instantiated) { + return; } - - function importClauseContainsConstEnumUsedAsValue(importClause: ImportClause) { - return forEachImportClauseDeclaration(importClause, declaration => { - return !!getSymbolLinks(getSymbolOfNode(declaration)).constEnumReferenced; - }); + // In case of variable declaration, node.parent is variable statement so look at the variable statement's parent + const parent = getDeclarationContainer(node); + if (parent.kind === SyntaxKind.SourceFile && isExternalOrCommonJsModule((parent))) { + // If the declaration happens to be in external module, report error that require and exports are reserved keywords + error(name, Diagnostics.Duplicate_identifier_0_Compiler_reserves_name_1_in_top_level_scope_of_a_module, declarationNameToString(name), declarationNameToString(name)); } - - function checkImportsForTypeOnlyConversion(sourceFile: SourceFile) { - for (const statement of sourceFile.statements) { - if ( - isImportDeclaration(statement) && - statement.importClause && - !statement.importClause.isTypeOnly && - importClauseContainsReferencedImport(statement.importClause) && - !isReferencedAliasDeclaration(statement.importClause, /*checkChildren*/ true) && - !importClauseContainsConstEnumUsedAsValue(statement.importClause) - ) { - error( - statement, - Diagnostics.This_import_is_never_used_as_a_value_and_must_use_import_type_because_the_importsNotUsedAsValues_is_set_to_error); - } - } + } + function checkCollisionWithGlobalPromiseInGeneratedCode(node: Node, name: Identifier): void { + if (languageVersion >= ScriptTarget.ES2017 || compilerOptions.noEmit || !needCollisionCheckForIdentifier(node, name, "Promise")) { + return; } - - function checkExportSpecifier(node: ExportSpecifier) { - checkAliasSymbol(node); - if (getEmitDeclarations(compilerOptions)) { - collectLinkedAliases(node.propertyName || node.name, /*setVisibility*/ true); - } - if (!node.parent.parent.moduleSpecifier) { - const exportedName = node.propertyName || node.name; - // find immediate value referenced by exported name (SymbolFlags.Alias is set so we don't chase down aliases) - const symbol = resolveName(exportedName, exportedName.escapedText, SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace | SymbolFlags.Alias, - /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ true); - if (symbol && (symbol === undefinedSymbol || symbol === globalThisSymbol || isGlobalSourceFile(getDeclarationContainer(symbol.declarations[0])))) { - error(exportedName, Diagnostics.Cannot_export_0_Only_local_declarations_can_be_exported_from_a_module, idText(exportedName)); - } - else { - markExportAsReferenced(node); - const target = symbol && (symbol.flags & SymbolFlags.Alias ? resolveAlias(symbol) : symbol); - if (!target || target === unknownSymbol || target.flags & SymbolFlags.Value) { - checkExpressionCached(node.propertyName || node.name); + // Uninstantiated modules shouldnt do this check + if (isModuleDeclaration(node) && getModuleInstanceState(node) !== ModuleInstanceState.Instantiated) { + return; + } + // In case of variable declaration, node.parent is variable statement so look at the variable statement's parent + const parent = getDeclarationContainer(node); + if (parent.kind === SyntaxKind.SourceFile && isExternalOrCommonJsModule((parent)) && parent.flags & NodeFlags.HasAsyncFunctions) { + // If the declaration happens to be in external module, report error that Promise is a reserved identifier. + error(name, Diagnostics.Duplicate_identifier_0_Compiler_reserves_name_1_in_top_level_scope_of_a_module_containing_async_functions, declarationNameToString(name), declarationNameToString(name)); + } + } + function checkVarDeclaredNamesNotShadowed(node: VariableDeclaration | BindingElement) { + // - ScriptBody : StatementList + // It is a Syntax Error if any element of the LexicallyDeclaredNames of StatementList + // also occurs in the VarDeclaredNames of StatementList. + // - Block : { StatementList } + // It is a Syntax Error if any element of the LexicallyDeclaredNames of StatementList + // also occurs in the VarDeclaredNames of StatementList. + // Variable declarations are hoisted to the top of their function scope. They can shadow + // block scoped declarations, which bind tighter. this will not be flagged as duplicate definition + // by the binder as the declaration scope is different. + // A non-initialized declaration is a no-op as the block declaration will resolve before the var + // declaration. the problem is if the declaration has an initializer. this will act as a write to the + // block declared value. this is fine for let, but not const. + // Only consider declarations with initializers, uninitialized const declarations will not + // step on a let/const variable. + // Do not consider const and const declarations, as duplicate block-scoped declarations + // are handled by the binder. + // We are only looking for const declarations that step on let\const declarations from a + // different scope. e.g.: + // { + // const x = 0; // localDeclarationSymbol obtained after name resolution will correspond to this declaration + // const x = 0; // symbol for this declaration will be 'symbol' + // } + // skip block-scoped variables and parameters + if ((getCombinedNodeFlags(node) & NodeFlags.BlockScoped) !== 0 || isParameterDeclaration(node)) { + return; + } + // skip variable declarations that don't have initializers + // NOTE: in ES6 spec initializer is required in variable declarations where name is binding pattern + // so we'll always treat binding elements as initialized + if (node.kind === SyntaxKind.VariableDeclaration && !node.initializer) { + return; + } + const symbol = getSymbolOfNode(node); + if (symbol.flags & SymbolFlags.FunctionScopedVariable) { + if (!isIdentifier(node.name)) + return Debug.fail(); + const localDeclarationSymbol = resolveName(node, node.name.escapedText, SymbolFlags.Variable, /*nodeNotFoundErrorMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false); + if (localDeclarationSymbol && + localDeclarationSymbol !== symbol && + localDeclarationSymbol.flags & SymbolFlags.BlockScopedVariable) { + if (getDeclarationNodeFlagsFromSymbol(localDeclarationSymbol) & NodeFlags.BlockScoped) { + const varDeclList = (getAncestor(localDeclarationSymbol.valueDeclaration, SyntaxKind.VariableDeclarationList)!); + const container = varDeclList.parent.kind === SyntaxKind.VariableStatement && varDeclList.parent.parent + ? varDeclList.parent.parent + : undefined; + // names of block-scoped and function scoped variables can collide only + // if block scoped variable is defined in the function\module\source file scope (because of variable hoisting) + const namesShareScope = container && + (container.kind === SyntaxKind.Block && isFunctionLike(container.parent) || + container.kind === SyntaxKind.ModuleBlock || + container.kind === SyntaxKind.ModuleDeclaration || + container.kind === SyntaxKind.SourceFile); + // here we know that function scoped variable is shadowed by block scoped one + // if they are defined in the same scope - binder has already reported redeclaration error + // otherwise if variable has an initializer - show error that initialization will fail + // since LHS will be block scoped name instead of function scoped + if (!namesShareScope) { + const name = symbolToString(localDeclarationSymbol); + error(node, Diagnostics.Cannot_initialize_outer_scoped_variable_0_in_the_same_scope_as_block_scoped_declaration_1, name, name); } } } } - - function checkExportAssignment(node: ExportAssignment) { - if (checkGrammarModuleElementContext(node, Diagnostics.An_export_assignment_can_only_be_used_in_a_module)) { - // If we hit an export assignment in an illegal context, just bail out to avoid cascading errors. - return; + } + function convertAutoToAny(type: ts.Type) { + return type === autoType ? anyType : type === autoArrayType ? anyArrayType : type; + } + // Check variable, parameter, or property declaration + function checkVariableLikeDeclaration(node: ParameterDeclaration | PropertyDeclaration | PropertySignature | VariableDeclaration | BindingElement) { + checkDecorators(node); + if (!isBindingElement(node)) { + checkSourceElement(node.type); + } + // JSDoc `function(string, string): string` syntax results in parameters with no name + if (!node.name) { + return; + } + // For a computed property, just check the initializer and exit + // Do not use hasDynamicName here, because that returns false for well known symbols. + // We want to perform checkComputedPropertyName for all computed properties, including + // well known symbols. + if (node.name.kind === SyntaxKind.ComputedPropertyName) { + checkComputedPropertyName(node.name); + if (node.initializer) { + checkExpressionCached(node.initializer); } - - const container = node.parent.kind === SyntaxKind.SourceFile ? node.parent : node.parent.parent; - if (container.kind === SyntaxKind.ModuleDeclaration && !isAmbientModule(container)) { - if (node.isExportEquals) { - error(node, Diagnostics.An_export_assignment_cannot_be_used_in_a_namespace); - } - else { - error(node, Diagnostics.A_default_export_can_only_be_used_in_an_ECMAScript_style_module); - } - - return; + } + if (node.kind === SyntaxKind.BindingElement) { + if (node.parent.kind === SyntaxKind.ObjectBindingPattern && languageVersion < ScriptTarget.ESNext) { + checkExternalEmitHelpers(node, ExternalEmitHelpers.Rest); } - // Grammar checking - if (!checkGrammarDecoratorsAndModifiers(node) && hasModifiers(node)) { - grammarErrorOnFirstToken(node, Diagnostics.An_export_assignment_cannot_have_modifiers); + // check computed properties inside property names of binding elements + if (node.propertyName && node.propertyName.kind === SyntaxKind.ComputedPropertyName) { + checkComputedPropertyName(node.propertyName); } - if (node.expression.kind === SyntaxKind.Identifier) { - const id = node.expression as Identifier; - const sym = resolveEntityName(id, SymbolFlags.All, /*ignoreErrors*/ true, /*dontResolveAlias*/ true, node); - if (sym) { - - markAliasReferenced(sym, id); - // If not a value, we're interpreting the identifier as a type export, along the lines of (`export { Id as default }`) - const target = sym.flags & SymbolFlags.Alias ? resolveAlias(sym) : sym; - if (target === unknownSymbol || target.flags & SymbolFlags.Value) { - // However if it is a value, we need to check it's being used correctly - checkExpressionCached(node.expression); + // check private/protected variable access + const parent = node.parent.parent; + const parentType = getTypeForBindingElementParent(parent); + const name = node.propertyName || node.name; + if (parentType && !isBindingPattern(name)) { + const exprType = getLiteralTypeFromPropertyName(name); + if (isTypeUsableAsPropertyName(exprType)) { + const nameText = getPropertyNameFromType(exprType); + const property = getPropertyOfType(parentType, nameText); + if (property) { + markPropertyAsReferenced(property, /*nodeForCheckWriteOnly*/ undefined, /*isThisAccess*/ false); // A destructuring is never a write-only reference. + checkPropertyAccessibility(parent, !!parent.initializer && parent.initializer.kind === SyntaxKind.SuperKeyword, parentType, property); } } - - if (getEmitDeclarations(compilerOptions)) { - collectLinkedAliases(node.expression as Identifier, /*setVisibility*/ true); - } } - else { - checkExpressionCached(node.expression); - } - - checkExternalModuleExports(container); - - if ((node.flags & NodeFlags.Ambient) && !isEntityNameExpression(node.expression)) { - grammarErrorOnNode(node.expression, Diagnostics.The_expression_of_an_export_assignment_must_be_an_identifier_or_qualified_name_in_an_ambient_context); + } + // For a binding pattern, check contained binding elements + if (isBindingPattern(node.name)) { + if (node.name.kind === SyntaxKind.ArrayBindingPattern && languageVersion < ScriptTarget.ES2015 && compilerOptions.downlevelIteration) { + checkExternalEmitHelpers(node, ExternalEmitHelpers.Read); } - - if (node.isExportEquals && !(node.flags & NodeFlags.Ambient)) { - if (moduleKind >= ModuleKind.ES2015) { - // export assignment is not supported in es6 modules - grammarErrorOnNode(node, Diagnostics.Export_assignment_cannot_be_used_when_targeting_ECMAScript_modules_Consider_using_export_default_or_another_module_format_instead); + forEach(node.name.elements, checkSourceElement); + } + // For a parameter declaration with an initializer, error and exit if the containing function doesn't have a body + if (node.initializer && getRootDeclaration(node).kind === SyntaxKind.Parameter && nodeIsMissing((getContainingFunction(node) as FunctionLikeDeclaration).body)) { + error(node, Diagnostics.A_parameter_initializer_is_only_allowed_in_a_function_or_constructor_implementation); + return; + } + // For a binding pattern, validate the initializer and exit + if (isBindingPattern(node.name)) { + const needCheckInitializer = node.initializer && node.parent.parent.kind !== SyntaxKind.ForInStatement; + const needCheckWidenedType = node.name.elements.length === 0; + if (needCheckInitializer || needCheckWidenedType) { + // Don't validate for-in initializer as it is already an error + const widenedType = getWidenedTypeForVariableLikeDeclaration(node); + if (needCheckInitializer) { + const initializerType = checkExpressionCached(node.initializer!); + if (strictNullChecks && needCheckWidenedType) { + checkNonNullNonVoidType(initializerType, node); + } + else { + checkTypeAssignableToAndOptionallyElaborate(initializerType, getWidenedTypeForVariableLikeDeclaration(node), node, node.initializer); + } } - else if (moduleKind === ModuleKind.System) { - // system modules does not support export assignment - grammarErrorOnNode(node, Diagnostics.Export_assignment_is_not_supported_when_module_flag_is_system); + // check the binding pattern with empty elements + if (needCheckWidenedType) { + if (isArrayBindingPattern(node.name)) { + checkIteratedTypeOrElementType(IterationUse.Destructuring, widenedType, undefinedType, node); + } + else if (strictNullChecks) { + checkNonNullNonVoidType(widenedType, node); + } } } + return; } - - function hasExportedMembers(moduleSymbol: Symbol) { - return forEachEntry(moduleSymbol.exports!, (_, id) => id !== "export="); - } - - function checkExternalModuleExports(node: SourceFile | ModuleDeclaration) { - const moduleSymbol = getSymbolOfNode(node); - const links = getSymbolLinks(moduleSymbol); - if (!links.exportsChecked) { - const exportEqualsSymbol = moduleSymbol.exports!.get("export=" as __String); - if (exportEqualsSymbol && hasExportedMembers(moduleSymbol)) { - const declaration = getDeclarationOfAliasSymbol(exportEqualsSymbol) || exportEqualsSymbol.valueDeclaration; - if (!isTopLevelInExternalModuleAugmentation(declaration) && !isInJSFile(declaration)) { - error(declaration, Diagnostics.An_export_assignment_cannot_be_used_in_a_module_with_other_exported_elements); - } - } - // Checks for export * conflicts - const exports = getExportsOfModule(moduleSymbol); - if (exports) { - exports.forEach(({ declarations, flags }, id) => { - if (id === "__export") { - return; - } - // ECMA262: 15.2.1.1 It is a Syntax Error if the ExportedNames of ModuleItemList contains any duplicate entries. - // (TS Exceptions: namespaces, function overloads, enums, and interfaces) - if (flags & (SymbolFlags.Namespace | SymbolFlags.Interface | SymbolFlags.Enum)) { - return; - } - const exportedDeclarationsCount = countWhere(declarations, isNotOverloadAndNotAccessor); - if (flags & SymbolFlags.TypeAlias && exportedDeclarationsCount <= 2) { - // it is legal to merge type alias with other values - // so count should be either 1 (just type alias) or 2 (type alias + merged value) - return; - } - if (exportedDeclarationsCount > 1) { - for (const declaration of declarations) { - if (isNotOverload(declaration)) { - diagnostics.add(createDiagnosticForNode(declaration, Diagnostics.Cannot_redeclare_exported_variable_0, unescapeLeadingUnderscores(id))); - } - } - } - }); + const symbol = getSymbolOfNode(node); + const type = convertAutoToAny(getTypeOfSymbol(symbol)); + if (node === symbol.valueDeclaration) { + // Node is the primary declaration of the symbol, just validate the initializer + // Don't validate for-in initializer as it is already an error + const initializer = getEffectiveInitializer(node); + if (initializer) { + const isJSObjectLiteralInitializer = isInJSFile(node) && + isObjectLiteralExpression(initializer) && + (initializer.properties.length === 0 || isPrototypeAccess(node.name)) && + hasEntries(symbol.exports); + if (!isJSObjectLiteralInitializer && node.parent.parent.kind !== SyntaxKind.ForInStatement) { + checkTypeAssignableToAndOptionallyElaborate(checkExpressionCached(initializer), type, node, initializer, /*headMessage*/ undefined); } - links.exportsChecked = true; } - } - - function checkSourceElement(node: Node | undefined): void { - if (node) { - const saveCurrentNode = currentNode; - currentNode = node; - instantiationCount = 0; - checkSourceElementWorker(node); - currentNode = saveCurrentNode; + if (symbol.declarations.length > 1) { + if (some(symbol.declarations, d => d !== node && isVariableLike(d) && !areDeclarationFlagsIdentical(d, node))) { + error(node.name, Diagnostics.All_declarations_of_0_must_have_identical_modifiers, declarationNameToString(node.name)); + } } } - - function checkSourceElementWorker(node: Node): void { - if (isInJSFile(node)) { - forEach((node as JSDocContainer).jsDoc, ({ tags }) => forEach(tags, checkSourceElement)); - } - - const kind = node.kind; - if (cancellationToken) { - // Only bother checking on a few construct kinds. We don't want to be excessively - // hitting the cancellation token on every node we check. - switch (kind) { - case SyntaxKind.ModuleDeclaration: - case SyntaxKind.ClassDeclaration: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.FunctionDeclaration: - cancellationToken.throwIfCancellationRequested(); - } + else { + // Node is a secondary declaration, check that type is identical to primary declaration and check that + // initializer is consistent with type associated with the node + const declarationType = convertAutoToAny(getWidenedTypeForVariableLikeDeclaration(node)); + if (type !== errorType && declarationType !== errorType && + !isTypeIdenticalTo(type, declarationType) && + !(symbol.flags & SymbolFlags.Assignment)) { + errorNextVariableOrPropertyDeclarationMustHaveSameType(symbol.valueDeclaration, type, node, declarationType); } - if (kind >= SyntaxKind.FirstStatement && kind <= SyntaxKind.LastStatement && node.flowNode && !isReachableFlowNode(node.flowNode)) { - errorOrSuggestion(compilerOptions.allowUnreachableCode === false, node, Diagnostics.Unreachable_code_detected); + if (node.initializer) { + checkTypeAssignableToAndOptionallyElaborate(checkExpressionCached(node.initializer), declarationType, node, node.initializer, /*headMessage*/ undefined); } - - switch (kind) { - case SyntaxKind.TypeParameter: - return checkTypeParameter(node); - case SyntaxKind.Parameter: - return checkParameter(node); - case SyntaxKind.PropertyDeclaration: - return checkPropertyDeclaration(node); - case SyntaxKind.PropertySignature: - return checkPropertySignature(node); - case SyntaxKind.FunctionType: - case SyntaxKind.ConstructorType: - case SyntaxKind.CallSignature: - case SyntaxKind.ConstructSignature: - case SyntaxKind.IndexSignature: - return checkSignatureDeclaration(node); - case SyntaxKind.MethodDeclaration: - case SyntaxKind.MethodSignature: - return checkMethodDeclaration(node); - case SyntaxKind.Constructor: - return checkConstructorDeclaration(node); - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - return checkAccessorDeclaration(node); - case SyntaxKind.TypeReference: - return checkTypeReferenceNode(node); - case SyntaxKind.TypePredicate: - return checkTypePredicate(node); - case SyntaxKind.TypeQuery: - return checkTypeQuery(node); - case SyntaxKind.TypeLiteral: - return checkTypeLiteral(node); - case SyntaxKind.ArrayType: - return checkArrayType(node); - case SyntaxKind.TupleType: - return checkTupleType(node); - case SyntaxKind.UnionType: - case SyntaxKind.IntersectionType: - return checkUnionOrIntersectionType(node); - case SyntaxKind.ParenthesizedType: - case SyntaxKind.OptionalType: - case SyntaxKind.RestType: - return checkSourceElement((node).type); - case SyntaxKind.ThisType: - return checkThisType(node); - case SyntaxKind.TypeOperator: - return checkTypeOperator(node); - case SyntaxKind.ConditionalType: - return checkConditionalType(node); - case SyntaxKind.InferType: - return checkInferType(node); - case SyntaxKind.ImportType: - return checkImportType(node); - case SyntaxKind.JSDocAugmentsTag: - return checkJSDocAugmentsTag(node as JSDocAugmentsTag); - case SyntaxKind.JSDocImplementsTag: - return checkJSDocImplementsTag(node as JSDocImplementsTag); - case SyntaxKind.JSDocTypedefTag: - case SyntaxKind.JSDocCallbackTag: - case SyntaxKind.JSDocEnumTag: - return checkJSDocTypeAliasTag(node as JSDocTypedefTag); - case SyntaxKind.JSDocTemplateTag: - return checkJSDocTemplateTag(node as JSDocTemplateTag); - case SyntaxKind.JSDocTypeTag: - return checkJSDocTypeTag(node as JSDocTypeTag); - case SyntaxKind.JSDocParameterTag: - return checkJSDocParameterTag(node as JSDocParameterTag); - case SyntaxKind.JSDocFunctionType: - checkJSDocFunctionType(node as JSDocFunctionType); - // falls through - case SyntaxKind.JSDocNonNullableType: - case SyntaxKind.JSDocNullableType: - case SyntaxKind.JSDocAllType: - case SyntaxKind.JSDocUnknownType: - case SyntaxKind.JSDocTypeLiteral: - checkJSDocTypeIsInJsFile(node); - forEachChild(node, checkSourceElement); - return; - case SyntaxKind.JSDocVariadicType: - checkJSDocVariadicType(node as JSDocVariadicType); - return; - case SyntaxKind.JSDocTypeExpression: - return checkSourceElement((node as JSDocTypeExpression).type); - case SyntaxKind.IndexedAccessType: - return checkIndexedAccessType(node); - case SyntaxKind.MappedType: - return checkMappedType(node); - case SyntaxKind.FunctionDeclaration: - return checkFunctionDeclaration(node); - case SyntaxKind.Block: - case SyntaxKind.ModuleBlock: - return checkBlock(node); - case SyntaxKind.VariableStatement: - return checkVariableStatement(node); - case SyntaxKind.ExpressionStatement: - return checkExpressionStatement(node); - case SyntaxKind.IfStatement: - return checkIfStatement(node); - case SyntaxKind.DoStatement: - return checkDoStatement(node); - case SyntaxKind.WhileStatement: - return checkWhileStatement(node); - case SyntaxKind.ForStatement: - return checkForStatement(node); - case SyntaxKind.ForInStatement: - return checkForInStatement(node); - case SyntaxKind.ForOfStatement: - return checkForOfStatement(node); - case SyntaxKind.ContinueStatement: - case SyntaxKind.BreakStatement: - return checkBreakOrContinueStatement(node); - case SyntaxKind.ReturnStatement: - return checkReturnStatement(node); - case SyntaxKind.WithStatement: - return checkWithStatement(node); - case SyntaxKind.SwitchStatement: - return checkSwitchStatement(node); - case SyntaxKind.LabeledStatement: - return checkLabeledStatement(node); - case SyntaxKind.ThrowStatement: - return checkThrowStatement(node); - case SyntaxKind.TryStatement: - return checkTryStatement(node); - case SyntaxKind.VariableDeclaration: - return checkVariableDeclaration(node); - case SyntaxKind.BindingElement: - return checkBindingElement(node); - case SyntaxKind.ClassDeclaration: - return checkClassDeclaration(node); - case SyntaxKind.InterfaceDeclaration: - return checkInterfaceDeclaration(node); - case SyntaxKind.TypeAliasDeclaration: - return checkTypeAliasDeclaration(node); - case SyntaxKind.EnumDeclaration: - return checkEnumDeclaration(node); - case SyntaxKind.ModuleDeclaration: - return checkModuleDeclaration(node); - case SyntaxKind.ImportDeclaration: - return checkImportDeclaration(node); - case SyntaxKind.ImportEqualsDeclaration: - return checkImportEqualsDeclaration(node); - case SyntaxKind.ExportDeclaration: - return checkExportDeclaration(node); - case SyntaxKind.ExportAssignment: - return checkExportAssignment(node); - case SyntaxKind.EmptyStatement: - case SyntaxKind.DebuggerStatement: - checkGrammarStatementInAmbientContext(node); - return; - case SyntaxKind.MissingDeclaration: - return checkMissingDeclaration(node); + if (!areDeclarationFlagsIdentical(node, symbol.valueDeclaration)) { + error(node.name, Diagnostics.All_declarations_of_0_must_have_identical_modifiers, declarationNameToString(node.name)); } } - - function checkJSDocTypeIsInJsFile(node: Node): void { - if (!isInJSFile(node)) { - grammarErrorOnNode(node, Diagnostics.JSDoc_types_can_only_be_used_inside_documentation_comments); + if (node.kind !== SyntaxKind.PropertyDeclaration && node.kind !== SyntaxKind.PropertySignature) { + // We know we don't have a binding pattern or computed name here + checkExportsOnMergedDeclarations(node); + if (node.kind === SyntaxKind.VariableDeclaration || node.kind === SyntaxKind.BindingElement) { + checkVarDeclaredNamesNotShadowed(node); + } + checkCollisionWithRequireExportsInGeneratedCode(node, node.name); + checkCollisionWithGlobalPromiseInGeneratedCode(node, node.name); + if (!compilerOptions.noEmit && languageVersion < ScriptTarget.ESNext && needCollisionCheckForIdentifier(node, node.name, "WeakMap")) { + potentialWeakMapCollisions.push(node); } } - - function checkJSDocVariadicType(node: JSDocVariadicType): void { - checkJSDocTypeIsInJsFile(node); - checkSourceElement(node.type); - - // Only legal location is in the *last* parameter tag or last parameter of a JSDoc function. - const { parent } = node; - if (isParameter(parent) && isJSDocFunctionType(parent.parent)) { - if (last(parent.parent.parameters) !== parent) { - error(node, Diagnostics.A_rest_parameter_must_be_last_in_a_parameter_list); + } + function errorNextVariableOrPropertyDeclarationMustHaveSameType(firstDeclaration: Declaration | undefined, firstType: ts.Type, nextDeclaration: Declaration, nextType: ts.Type): void { + const nextDeclarationName = getNameOfDeclaration(nextDeclaration); + const message = nextDeclaration.kind === SyntaxKind.PropertyDeclaration || nextDeclaration.kind === SyntaxKind.PropertySignature + ? Diagnostics.Subsequent_property_declarations_must_have_the_same_type_Property_0_must_be_of_type_1_but_here_has_type_2 + : Diagnostics.Subsequent_variable_declarations_must_have_the_same_type_Variable_0_must_be_of_type_1_but_here_has_type_2; + const declName = declarationNameToString(nextDeclarationName); + const err = error(nextDeclarationName, message, declName, typeToString(firstType), typeToString(nextType)); + if (firstDeclaration) { + addRelatedInfo(err, createDiagnosticForNode(firstDeclaration, Diagnostics._0_was_also_declared_here, declName)); + } + } + function areDeclarationFlagsIdentical(left: Declaration, right: Declaration) { + if ((left.kind === SyntaxKind.Parameter && right.kind === SyntaxKind.VariableDeclaration) || + (left.kind === SyntaxKind.VariableDeclaration && right.kind === SyntaxKind.Parameter)) { + // Differences in optionality between parameters and variables are allowed. + return true; + } + if (hasQuestionToken(left) !== hasQuestionToken(right)) { + return false; + } + const interestingFlags = ModifierFlags.Private | + ModifierFlags.Protected | + ModifierFlags.Async | + ModifierFlags.Abstract | + ModifierFlags.Readonly | + ModifierFlags.Static; + return getSelectedModifierFlags(left, interestingFlags) === getSelectedModifierFlags(right, interestingFlags); + } + function checkVariableDeclaration(node: VariableDeclaration) { + checkGrammarVariableDeclaration(node); + return checkVariableLikeDeclaration(node); + } + function checkBindingElement(node: BindingElement) { + checkGrammarBindingElement(node); + return checkVariableLikeDeclaration(node); + } + function checkVariableStatement(node: VariableStatement) { + // Grammar checking + if (!checkGrammarDecoratorsAndModifiers(node) && !checkGrammarVariableDeclarationList(node.declarationList)) + checkGrammarForDisallowedLetOrConstStatement(node); + forEach(node.declarationList.declarations, checkSourceElement); + } + function checkExpressionStatement(node: ExpressionStatement) { + // Grammar checking + checkGrammarStatementInAmbientContext(node); + checkExpression(node.expression); + } + function checkIfStatement(node: IfStatement) { + // Grammar checking + checkGrammarStatementInAmbientContext(node); + const type = checkTruthinessExpression(node.expression); + checkTestingKnownTruthyCallableType(node.expression, node.thenStatement, type); + checkSourceElement(node.thenStatement); + if (node.thenStatement.kind === SyntaxKind.EmptyStatement) { + error(node.thenStatement, Diagnostics.The_body_of_an_if_statement_cannot_be_the_empty_statement); + } + checkSourceElement(node.elseStatement); + } + function checkTestingKnownTruthyCallableType(condExpr: Expression, body: Statement | Expression, type: ts.Type) { + if (!strictNullChecks) { + return; + } + const testedNode = isIdentifier(condExpr) + ? condExpr + : isPropertyAccessExpression(condExpr) + ? condExpr.name + : undefined; + if (!testedNode) { + return; + } + const possiblyFalsy = getFalsyFlags(type); + if (possiblyFalsy) { + return; + } + // While it technically should be invalid for any known-truthy value + // to be tested, we de-scope to functions unrefenced in the block as a + // heuristic to identify the most common bugs. There are too many + // false positives for values sourced from type definitions without + // strictNullChecks otherwise. + const callSignatures = getSignaturesOfType(type, SignatureKind.Call); + if (callSignatures.length === 0) { + return; + } + const testedFunctionSymbol = getSymbolAtLocation(testedNode); + if (!testedFunctionSymbol) { + return; + } + const functionIsUsedInBody = forEachChild(body, function check(childNode): boolean | undefined { + if (isIdentifier(childNode)) { + const childSymbol = getSymbolAtLocation(childNode); + if (childSymbol && childSymbol.id === testedFunctionSymbol.id) { + return true; } - return; - } - - if (!isJSDocTypeExpression(parent)) { - error(node, Diagnostics.JSDoc_may_only_appear_in_the_last_parameter_of_a_signature); - } - - const paramTag = node.parent.parent; - if (!isJSDocParameterTag(paramTag)) { - error(node, Diagnostics.JSDoc_may_only_appear_in_the_last_parameter_of_a_signature); - return; - } - - const param = getParameterSymbolFromJSDoc(paramTag); - if (!param) { - // We will error in `checkJSDocParameterTag`. - return; - } - - const host = getHostSignatureFromJSDoc(paramTag); - if (!host || last(host.parameters).symbol !== param) { - error(node, Diagnostics.A_rest_parameter_must_be_last_in_a_parameter_list); } + return forEachChild(childNode, check); + }); + if (!functionIsUsedInBody) { + error(condExpr, Diagnostics.This_condition_will_always_return_true_since_the_function_is_always_defined_Did_you_mean_to_call_it_instead); } - - function getTypeFromJSDocVariadicType(node: JSDocVariadicType): Type { - const type = getTypeFromTypeNode(node.type); - const { parent } = node; - const paramTag = node.parent.parent; - if (isJSDocTypeExpression(node.parent) && isJSDocParameterTag(paramTag)) { - // Else we will add a diagnostic, see `checkJSDocVariadicType`. - const host = getHostSignatureFromJSDoc(paramTag); - if (host) { - /* - Only return an array type if the corresponding parameter is marked as a rest parameter, or if there are no parameters. - So in the following situation we will not create an array type: - /** @param {...number} a * / - function f(a) {} - Because `a` will just be of type `number | undefined`. A synthetic `...args` will also be added, which *will* get an array type. - */ - const lastParamDeclaration = lastOrUndefined(host.parameters); - const symbol = getParameterSymbolFromJSDoc(paramTag); - if (!lastParamDeclaration || - symbol && lastParamDeclaration.symbol === symbol && isRestParameter(lastParamDeclaration)) { - return createArrayType(type); - } - } - } - if (isParameter(parent) && isJSDocFunctionType(parent.parent)) { - return createArrayType(type); - } - return addOptionality(type); + } + function checkDoStatement(node: DoStatement) { + // Grammar checking + checkGrammarStatementInAmbientContext(node); + checkSourceElement(node.statement); + checkTruthinessExpression(node.expression); + } + function checkWhileStatement(node: WhileStatement) { + // Grammar checking + checkGrammarStatementInAmbientContext(node); + checkTruthinessExpression(node.expression); + checkSourceElement(node.statement); + } + function checkTruthinessOfType(type: ts.Type, node: Node) { + if (type.flags & TypeFlags.Void) { + error(node, Diagnostics.An_expression_of_type_void_cannot_be_tested_for_truthiness); } - - // Function and class expression bodies are checked after all statements in the enclosing body. This is - // to ensure constructs like the following are permitted: - // const foo = function () { - // const s = foo(); - // return "hello"; - // } - // Here, performing a full type check of the body of the function expression whilst in the process of - // determining the type of foo would cause foo to be given type any because of the recursive reference. - // Delaying the type check of the body ensures foo has been assigned a type. - function checkNodeDeferred(node: Node) { - const enclosingFile = getSourceFileOfNode(node); - const links = getNodeLinks(enclosingFile); - if (!(links.flags & NodeCheckFlags.TypeChecked)) { - links.deferredNodes = links.deferredNodes || createMap(); - const id = "" + getNodeId(node); - links.deferredNodes.set(id, node); + return type; + } + function checkTruthinessExpression(node: Expression, checkMode?: CheckMode) { + return checkTruthinessOfType(checkExpression(node, checkMode), node); + } + function checkForStatement(node: ForStatement) { + // Grammar checking + if (!checkGrammarStatementInAmbientContext(node)) { + if (node.initializer && node.initializer.kind === SyntaxKind.VariableDeclarationList) { + checkGrammarVariableDeclarationList((node.initializer)); } } - - function checkDeferredNodes(context: SourceFile) { - const links = getNodeLinks(context); - if (links.deferredNodes) { - links.deferredNodes.forEach(checkDeferredNode); + if (node.initializer) { + if (node.initializer.kind === SyntaxKind.VariableDeclarationList) { + forEach((node.initializer).declarations, checkVariableDeclaration); + } + else { + checkExpression(node.initializer); } } - - function checkDeferredNode(node: Node) { - const saveCurrentNode = currentNode; - currentNode = node; - instantiationCount = 0; - switch (node.kind) { - case SyntaxKind.CallExpression: - case SyntaxKind.NewExpression: - case SyntaxKind.TaggedTemplateExpression: - case SyntaxKind.Decorator: - case SyntaxKind.JsxOpeningElement: - // These node kinds are deferred checked when overload resolution fails - // To save on work, we ensure the arguments are checked just once, in - // a deferred way - resolveUntypedCall(node as CallLikeExpression); - break; - case SyntaxKind.FunctionExpression: - case SyntaxKind.ArrowFunction: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.MethodSignature: - checkFunctionExpressionOrObjectLiteralMethodDeferred(node); - break; - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - checkAccessorDeclaration(node); - break; - case SyntaxKind.ClassExpression: - checkClassExpressionDeferred(node); - break; - case SyntaxKind.JsxSelfClosingElement: - checkJsxSelfClosingElementDeferred(node); - break; - case SyntaxKind.JsxElement: - checkJsxElementDeferred(node); - break; + if (node.condition) + checkTruthinessExpression(node.condition); + if (node.incrementor) + checkExpression(node.incrementor); + checkSourceElement(node.statement); + if (node.locals) { + registerForUnusedIdentifiersCheck(node); + } + } + function checkForOfStatement(node: ForOfStatement): void { + checkGrammarForInOrForOfStatement(node); + if (node.awaitModifier) { + const functionFlags = getFunctionFlags(getContainingFunction(node)); + if ((functionFlags & (FunctionFlags.Invalid | FunctionFlags.Async)) === FunctionFlags.Async && languageVersion < ScriptTarget.ESNext) { + // for..await..of in an async function or async generator function prior to ESNext requires the __asyncValues helper + checkExternalEmitHelpers(node, ExternalEmitHelpers.ForAwaitOfIncludes); + } + } + else if (compilerOptions.downlevelIteration && languageVersion < ScriptTarget.ES2015) { + // for..of prior to ES2015 requires the __values helper when downlevelIteration is enabled + checkExternalEmitHelpers(node, ExternalEmitHelpers.ForOfIncludes); + } + // Check the LHS and RHS + // If the LHS is a declaration, just check it as a variable declaration, which will in turn check the RHS + // via checkRightHandSideOfForOf. + // If the LHS is an expression, check the LHS, as a destructuring assignment or as a reference. + // Then check that the RHS is assignable to it. + if (node.initializer.kind === SyntaxKind.VariableDeclarationList) { + checkForInOrForOfVariableDeclaration(node); + } + else { + const varExpr = node.initializer; + const iteratedType = checkRightHandSideOfForOf(node.expression, node.awaitModifier); + // There may be a destructuring assignment on the left side + if (varExpr.kind === SyntaxKind.ArrayLiteralExpression || varExpr.kind === SyntaxKind.ObjectLiteralExpression) { + // iteratedType may be undefined. In this case, we still want to check the structure of + // varExpr, in particular making sure it's a valid LeftHandSideExpression. But we'd like + // to short circuit the type relation checking as much as possible, so we pass the unknownType. + checkDestructuringAssignment(varExpr, iteratedType || errorType); + } + else { + const leftType = checkExpression(varExpr); + checkReferenceExpression(varExpr, Diagnostics.The_left_hand_side_of_a_for_of_statement_must_be_a_variable_or_a_property_access, Diagnostics.The_left_hand_side_of_a_for_of_statement_may_not_be_an_optional_property_access); + // iteratedType will be undefined if the rightType was missing properties/signatures + // required to get its iteratedType (like [Symbol.iterator] or next). This may be + // because we accessed properties from anyType, or it may have led to an error inside + // getElementTypeOfIterable. + if (iteratedType) { + checkTypeAssignableToAndOptionallyElaborate(iteratedType, leftType, varExpr, node.expression); + } } - currentNode = saveCurrentNode; } - - function checkSourceFile(node: SourceFile) { - performance.mark("beforeCheck"); - checkSourceFileWorker(node); - performance.mark("afterCheck"); - performance.measure("Check", "beforeCheck", "afterCheck"); + checkSourceElement(node.statement); + if (node.locals) { + registerForUnusedIdentifiersCheck(node); } - - function unusedIsError(kind: UnusedKind, isAmbient: boolean): boolean { - if (isAmbient) { - return false; + } + function checkForInStatement(node: ForInStatement) { + // Grammar checking + checkGrammarForInOrForOfStatement(node); + const rightType = getNonNullableTypeIfNeeded(checkExpression(node.expression)); + // TypeScript 1.0 spec (April 2014): 5.4 + // In a 'for-in' statement of the form + // for (let VarDecl in Expr) Statement + // VarDecl must be a variable declaration without a type annotation that declares a variable of type Any, + // and Expr must be an expression of type Any, an object type, or a type parameter type. + if (node.initializer.kind === SyntaxKind.VariableDeclarationList) { + const variable = (node.initializer).declarations[0]; + if (variable && isBindingPattern(variable.name)) { + error(variable.name, Diagnostics.The_left_hand_side_of_a_for_in_statement_cannot_be_a_destructuring_pattern); + } + checkForInOrForOfVariableDeclaration(node); + } + else { + // In a 'for-in' statement of the form + // for (Var in Expr) Statement + // Var must be an expression classified as a reference of type Any or the String primitive type, + // and Expr must be an expression of type Any, an object type, or a type parameter type. + const varExpr = node.initializer; + const leftType = checkExpression(varExpr); + if (varExpr.kind === SyntaxKind.ArrayLiteralExpression || varExpr.kind === SyntaxKind.ObjectLiteralExpression) { + error(varExpr, Diagnostics.The_left_hand_side_of_a_for_in_statement_cannot_be_a_destructuring_pattern); } - switch (kind) { - case UnusedKind.Local: - return !!compilerOptions.noUnusedLocals; - case UnusedKind.Parameter: - return !!compilerOptions.noUnusedParameters; - default: - return Debug.assertNever(kind); + else if (!isTypeAssignableTo(getIndexTypeOrString(rightType), leftType)) { + error(varExpr, Diagnostics.The_left_hand_side_of_a_for_in_statement_must_be_of_type_string_or_any); + } + else { + // run check only former check succeeded to avoid cascading errors + checkReferenceExpression(varExpr, Diagnostics.The_left_hand_side_of_a_for_in_statement_must_be_a_variable_or_a_property_access, Diagnostics.The_left_hand_side_of_a_for_in_statement_may_not_be_an_optional_property_access); } } - - function getPotentiallyUnusedIdentifiers(sourceFile: SourceFile): readonly PotentiallyUnusedIdentifier[] { - return allPotentiallyUnusedIdentifiers.get(sourceFile.path) || emptyArray; + // unknownType is returned i.e. if node.expression is identifier whose name cannot be resolved + // in this case error about missing name is already reported - do not report extra one + if (rightType === neverType || !isTypeAssignableToKind(rightType, TypeFlags.NonPrimitive | TypeFlags.InstantiableNonPrimitive)) { + error(node.expression, Diagnostics.The_right_hand_side_of_a_for_in_statement_must_be_of_type_any_an_object_type_or_a_type_parameter_but_here_has_type_0, typeToString(rightType)); } - - // Fully type check a source file and collect the relevant diagnostics. - function checkSourceFileWorker(node: SourceFile) { - const links = getNodeLinks(node); - if (!(links.flags & NodeCheckFlags.TypeChecked)) { - if (skipTypeChecking(node, compilerOptions, host)) { - return; + checkSourceElement(node.statement); + if (node.locals) { + registerForUnusedIdentifiersCheck(node); + } + } + function checkForInOrForOfVariableDeclaration(iterationStatement: ForInOrOfStatement): void { + const variableDeclarationList = (iterationStatement.initializer); + // checkGrammarForInOrForOfStatement will check that there is exactly one declaration. + if (variableDeclarationList.declarations.length >= 1) { + const decl = variableDeclarationList.declarations[0]; + checkVariableDeclaration(decl); + } + } + function checkRightHandSideOfForOf(rhsExpression: Expression, awaitModifier: AwaitKeywordToken | undefined): ts.Type { + const expressionType = checkNonNullExpression(rhsExpression); + const use = awaitModifier ? IterationUse.ForAwaitOf : IterationUse.ForOf; + return checkIteratedTypeOrElementType(use, expressionType, undefinedType, rhsExpression); + } + function checkIteratedTypeOrElementType(use: IterationUse, inputType: ts.Type, sentType: ts.Type, errorNode: Node | undefined): ts.Type { + if (isTypeAny(inputType)) { + return inputType; + } + return getIteratedTypeOrElementType(use, inputType, sentType, errorNode, /*checkAssignability*/ true) || anyType; + } + /** + * When consuming an iterable type in a for..of, spread, or iterator destructuring assignment + * we want to get the iterated type of an iterable for ES2015 or later, or the iterated type + * of a iterable (if defined globally) or element type of an array like for ES2015 or earlier. + */ + function getIteratedTypeOrElementType(use: IterationUse, inputType: ts.Type, sentType: ts.Type, errorNode: Node | undefined, checkAssignability: boolean): ts.Type | undefined { + const allowAsyncIterables = (use & IterationUse.AllowsAsyncIterablesFlag) !== 0; + if (inputType === neverType) { + reportTypeNotIterableError(errorNode!, inputType, allowAsyncIterables); // TODO: GH#18217 + return undefined; + } + const uplevelIteration = languageVersion >= ScriptTarget.ES2015; + const downlevelIteration = !uplevelIteration && compilerOptions.downlevelIteration; + // Get the iterated type of an `Iterable` or `IterableIterator` only in ES2015 + // or higher, when inside of an async generator or for-await-if, or when + // downlevelIteration is requested. + if (uplevelIteration || downlevelIteration || allowAsyncIterables) { + // We only report errors for an invalid iterable type in ES2015 or higher. + const iterationTypes = getIterationTypesOfIterable(inputType, use, uplevelIteration ? errorNode : undefined); + if (checkAssignability) { + if (iterationTypes) { + const diagnostic = use & IterationUse.ForOfFlag ? Diagnostics.Cannot_iterate_value_because_the_next_method_of_its_iterator_expects_type_1_but_for_of_will_always_send_0 : + use & IterationUse.SpreadFlag ? Diagnostics.Cannot_iterate_value_because_the_next_method_of_its_iterator_expects_type_1_but_array_spread_will_always_send_0 : + use & IterationUse.DestructuringFlag ? Diagnostics.Cannot_iterate_value_because_the_next_method_of_its_iterator_expects_type_1_but_array_destructuring_will_always_send_0 : + use & IterationUse.YieldStarFlag ? Diagnostics.Cannot_delegate_iteration_to_value_because_the_next_method_of_its_iterator_expects_type_1_but_the_containing_generator_will_always_send_0 : + undefined; + if (diagnostic) { + checkTypeAssignableTo(sentType, iterationTypes.nextType, errorNode, diagnostic); + } } - - // Grammar checking - checkGrammarSourceFile(node); - - clear(potentialThisCollisions); - clear(potentialNewTargetCollisions); - clear(potentialWeakMapCollisions); - - forEach(node.statements, checkSourceElement); - checkSourceElement(node.endOfFileToken); - - checkDeferredNodes(node); - - if (isExternalOrCommonJsModule(node)) { - registerForUnusedIdentifiersCheck(node); + } + if (iterationTypes || uplevelIteration) { + return iterationTypes && iterationTypes.yieldType; + } + } + let arrayType = inputType; + let reportedError = false; + let hasStringConstituent = false; + // If strings are permitted, remove any string-like constituents from the array type. + // This allows us to find other non-string element types from an array unioned with + // a string. + if (use & IterationUse.AllowsStringInputFlag) { + if (arrayType.flags & TypeFlags.Union) { + // After we remove all types that are StringLike, we will know if there was a string constituent + // based on whether the result of filter is a new array. + const arrayTypes = (inputType).types; + const filteredTypes = filter(arrayTypes, t => !(t.flags & TypeFlags.StringLike)); + if (filteredTypes !== arrayTypes) { + arrayType = getUnionType(filteredTypes, UnionReduction.Subtype); } - - if (!node.isDeclarationFile && (compilerOptions.noUnusedLocals || compilerOptions.noUnusedParameters)) { - checkUnusedIdentifiers(getPotentiallyUnusedIdentifiers(node), (containingNode, kind, diag) => { - if (!containsParseError(containingNode) && unusedIsError(kind, !!(containingNode.flags & NodeFlags.Ambient))) { - diagnostics.add(diag); - } - }); + } + else if (arrayType.flags & TypeFlags.StringLike) { + arrayType = neverType; + } + hasStringConstituent = arrayType !== inputType; + if (hasStringConstituent) { + if (languageVersion < ScriptTarget.ES5) { + if (errorNode) { + error(errorNode, Diagnostics.Using_a_string_in_a_for_of_statement_is_only_supported_in_ECMAScript_5_and_higher); + reportedError = true; + } } - - if (compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error && - !node.isDeclarationFile && - isExternalModule(node) - ) { - checkImportsForTypeOnlyConversion(node); + // Now that we've removed all the StringLike types, if no constituents remain, then the entire + // arrayOrStringType was a string. + if (arrayType.flags & TypeFlags.Never) { + return stringType; } - - if (isExternalOrCommonJsModule(node)) { - checkExternalModuleExports(node); + } + } + if (!isArrayLikeType(arrayType)) { + if (errorNode && !reportedError) { + // Which error we report depends on whether we allow strings or if there was a + // string constituent. For example, if the input type is number | string, we + // want to say that number is not an array type. But if the input was just + // number and string input is allowed, we want to say that number is not an + // array type or a string type. + const yieldType = getIterationTypeOfIterable(use, IterationTypeKind.Yield, inputType, /*errorNode*/ undefined); + const [defaultDiagnostic, maybeMissingAwait]: [DiagnosticMessage, boolean] = !(use & IterationUse.AllowsStringInputFlag) || hasStringConstituent + ? downlevelIteration + ? [Diagnostics.Type_0_is_not_an_array_type_or_does_not_have_a_Symbol_iterator_method_that_returns_an_iterator, true] + : yieldType + ? [Diagnostics.Type_0_is_not_an_array_type_or_a_string_type_Use_compiler_option_downlevelIteration_to_allow_iterating_of_iterators, false] + : [Diagnostics.Type_0_is_not_an_array_type, true] + : downlevelIteration + ? [Diagnostics.Type_0_is_not_an_array_type_or_a_string_type_or_does_not_have_a_Symbol_iterator_method_that_returns_an_iterator, true] + : yieldType + ? [Diagnostics.Type_0_is_not_an_array_type_or_a_string_type_Use_compiler_option_downlevelIteration_to_allow_iterating_of_iterators, false] + : [Diagnostics.Type_0_is_not_an_array_type_or_a_string_type, true]; + errorAndMaybeSuggestAwait(errorNode, maybeMissingAwait && !!getAwaitedTypeOfPromise(arrayType), defaultDiagnostic, typeToString(arrayType)); + } + return hasStringConstituent ? stringType : undefined; + } + const arrayElementType = getIndexTypeOfType(arrayType, IndexKind.Number); + if (hasStringConstituent && arrayElementType) { + // This is just an optimization for the case where arrayOrStringType is string | string[] + if (arrayElementType.flags & TypeFlags.StringLike) { + return stringType; + } + return getUnionType([arrayElementType, stringType], UnionReduction.Subtype); + } + return arrayElementType; + } + /** + * Gets the requested "iteration type" from an `Iterable`-like or `AsyncIterable`-like type. + */ + function getIterationTypeOfIterable(use: IterationUse, typeKind: IterationTypeKind, inputType: ts.Type, errorNode: Node | undefined): ts.Type | undefined { + if (isTypeAny(inputType)) { + return undefined; + } + const iterationTypes = getIterationTypesOfIterable(inputType, use, errorNode); + return iterationTypes && iterationTypes[getIterationTypesKeyFromIterationTypeKind(typeKind)]; + } + function createIterationTypes(yieldType: ts.Type = neverType, returnType: ts.Type = neverType, nextType: ts.Type = unknownType): IterationTypes { + // `yieldType` and `returnType` are defaulted to `neverType` they each will be combined + // via `getUnionType` when merging iteration types. `nextType` is defined as `unknownType` + // as it is combined via `getIntersectionType` when merging iteration types. + // Use the cache only for intrinsic types to keep it small as they are likely to be + // more frequently created (i.e. `Iterator`). Iteration types + // are also cached on the type they are requested for, so we shouldn't need to maintain + // the cache for less-frequently used types. + if (yieldType.flags & TypeFlags.Intrinsic && + returnType.flags & (TypeFlags.Any | TypeFlags.Never | TypeFlags.Unknown | TypeFlags.Void | TypeFlags.Undefined) && + nextType.flags & (TypeFlags.Any | TypeFlags.Never | TypeFlags.Unknown | TypeFlags.Void | TypeFlags.Undefined)) { + const id = getTypeListId([yieldType, returnType, nextType]); + let iterationTypes = iterationTypesCache.get(id); + if (!iterationTypes) { + iterationTypes = { yieldType, returnType, nextType }; + iterationTypesCache.set(id, iterationTypes); + } + return iterationTypes; + } + return { yieldType, returnType, nextType }; + } + /** + * Combines multiple `IterationTypes` records. + * + * If `array` is empty or all elements are missing or are references to `noIterationTypes`, + * then `noIterationTypes` is returned. Otherwise, an `IterationTypes` record is returned + * for the combined iteration types. + */ + function combineIterationTypes(array: (IterationTypes | undefined)[]) { + let yieldTypes: ts.Type[] | undefined; + let returnTypes: ts.Type[] | undefined; + let nextTypes: ts.Type[] | undefined; + for (const iterationTypes of array) { + if (iterationTypes === undefined || iterationTypes === noIterationTypes) { + continue; + } + if (iterationTypes === anyIterationTypes) { + return anyIterationTypes; + } + yieldTypes = append(yieldTypes, iterationTypes.yieldType); + returnTypes = append(returnTypes, iterationTypes.returnType); + nextTypes = append(nextTypes, iterationTypes.nextType); + } + if (yieldTypes || returnTypes || nextTypes) { + return createIterationTypes(yieldTypes && getUnionType(yieldTypes), returnTypes && getUnionType(returnTypes), nextTypes && getIntersectionType(nextTypes)); + } + return noIterationTypes; + } + function getCachedIterationTypes(type: ts.Type, cacheKey: MatchingKeys) { + return (type as IterableOrIteratorType)[cacheKey]; + } + function setCachedIterationTypes(type: ts.Type, cacheKey: MatchingKeys, cachedTypes: IterationTypes) { + return (type as IterableOrIteratorType)[cacheKey] = cachedTypes; + } + /** + * Gets the *yield*, *return*, and *next* types from an `Iterable`-like or `AsyncIterable`-like type. + * + * At every level that involves analyzing return types of signatures, we union the return types of all the signatures. + * + * Another thing to note is that at any step of this process, we could run into a dead end, + * meaning either the property is missing, or we run into the anyType. If either of these things + * happens, we return `undefined` to signal that we could not find the iteration type. If a property + * is missing, and the previous step did not result in `any`, then we also give an error if the + * caller requested it. Then the caller can decide what to do in the case where there is no iterated + * type. + * + * For a **for-of** statement, `yield*` (in a normal generator), spread, array + * destructuring, or normal generator we will only ever look for a `[Symbol.iterator]()` + * method. + * + * For an async generator we will only ever look at the `[Symbol.asyncIterator]()` method. + * + * For a **for-await-of** statement or a `yield*` in an async generator we will look for + * the `[Symbol.asyncIterator]()` method first, and then the `[Symbol.iterator]()` method. + */ + function getIterationTypesOfIterable(type: ts.Type, use: IterationUse, errorNode: Node | undefined) { + if (isTypeAny(type)) { + return anyIterationTypes; + } + if (!(type.flags & TypeFlags.Union)) { + const iterationTypes = getIterationTypesOfIterableWorker(type, use, errorNode); + if (iterationTypes === noIterationTypes) { + if (errorNode) { + reportTypeNotIterableError(errorNode, type, !!(use & IterationUse.AllowsAsyncIterablesFlag)); } - - if (potentialThisCollisions.length) { - forEach(potentialThisCollisions, checkIfThisIsCapturedInEnclosingScope); - clear(potentialThisCollisions); + return undefined; + } + return iterationTypes; + } + const cacheKey = use & IterationUse.AllowsAsyncIterablesFlag ? "iterationTypesOfAsyncIterable" : "iterationTypesOfIterable"; + const cachedTypes = getCachedIterationTypes(type, cacheKey); + if (cachedTypes) + return cachedTypes === noIterationTypes ? undefined : cachedTypes; + let allIterationTypes: IterationTypes[] | undefined; + for (const constituent of (type as UnionType).types) { + const iterationTypes = getIterationTypesOfIterableWorker(constituent, use, errorNode); + if (iterationTypes === noIterationTypes) { + if (errorNode) { + reportTypeNotIterableError(errorNode, type, !!(use & IterationUse.AllowsAsyncIterablesFlag)); + errorNode = undefined; } - - if (potentialNewTargetCollisions.length) { - forEach(potentialNewTargetCollisions, checkIfNewTargetIsCapturedInEnclosingScope); - clear(potentialNewTargetCollisions); + } + else { + allIterationTypes = append(allIterationTypes, iterationTypes); + } + } + const iterationTypes = allIterationTypes ? combineIterationTypes(allIterationTypes) : noIterationTypes; + setCachedIterationTypes(type, cacheKey, iterationTypes); + return iterationTypes === noIterationTypes ? undefined : iterationTypes; + } + function getAsyncFromSyncIterationTypes(iterationTypes: IterationTypes, errorNode: Node | undefined) { + if (iterationTypes === noIterationTypes) + return noIterationTypes; + if (iterationTypes === anyIterationTypes) + return anyIterationTypes; + const { yieldType, returnType, nextType } = iterationTypes; + return createIterationTypes(getAwaitedType(yieldType, errorNode) || anyType, getAwaitedType(returnType, errorNode) || anyType, nextType); + } + /** + * Gets the *yield*, *return*, and *next* types from a non-union type. + * + * If we are unable to find the *yield*, *return*, and *next* types, `noIterationTypes` is + * returned to indicate to the caller that it should report an error. Otherwise, an + * `IterationTypes` record is returned. + * + * NOTE: You probably don't want to call this directly and should be calling + * `getIterationTypesOfIterable` instead. + */ + function getIterationTypesOfIterableWorker(type: ts.Type, use: IterationUse, errorNode: Node | undefined) { + if (isTypeAny(type)) { + return anyIterationTypes; + } + if (use & IterationUse.AllowsAsyncIterablesFlag) { + const iterationTypes = getIterationTypesOfIterableCached(type, asyncIterationTypesResolver) || + getIterationTypesOfIterableFast(type, asyncIterationTypesResolver); + if (iterationTypes) { + return iterationTypes; + } + } + if (use & IterationUse.AllowsSyncIterablesFlag) { + const iterationTypes = getIterationTypesOfIterableCached(type, syncIterationTypesResolver) || + getIterationTypesOfIterableFast(type, syncIterationTypesResolver); + if (iterationTypes) { + if (use & IterationUse.AllowsAsyncIterablesFlag) { + // for a sync iterable in an async context, only use the cached types if they are valid. + if (iterationTypes !== noIterationTypes) { + return setCachedIterationTypes(type, "iterationTypesOfAsyncIterable", getAsyncFromSyncIterationTypes(iterationTypes, errorNode)); + } } - - if (potentialWeakMapCollisions.length) { - forEach(potentialWeakMapCollisions, checkWeakMapCollision); - clear(potentialWeakMapCollisions); + else { + return iterationTypes; } - - links.flags |= NodeCheckFlags.TypeChecked; } } - - function getDiagnostics(sourceFile: SourceFile, ct: CancellationToken): Diagnostic[] { - try { - // Record the cancellation token so it can be checked later on during checkSourceElement. - // Do this in a finally block so we can ensure that it gets reset back to nothing after - // this call is done. - cancellationToken = ct; - return getDiagnosticsWorker(sourceFile); - } - finally { - cancellationToken = undefined; + if (use & IterationUse.AllowsAsyncIterablesFlag) { + const iterationTypes = getIterationTypesOfIterableSlow(type, asyncIterationTypesResolver, errorNode); + if (iterationTypes !== noIterationTypes) { + return iterationTypes; } } - - function getDiagnosticsWorker(sourceFile: SourceFile): Diagnostic[] { - throwIfNonDiagnosticsProducing(); - if (sourceFile) { - // Some global diagnostics are deferred until they are needed and - // may not be reported in the first call to getGlobalDiagnostics. - // We should catch these changes and report them. - const previousGlobalDiagnostics = diagnostics.getGlobalDiagnostics(); - const previousGlobalDiagnosticsSize = previousGlobalDiagnostics.length; - - checkSourceFile(sourceFile); - - const semanticDiagnostics = diagnostics.getDiagnostics(sourceFile.fileName); - const currentGlobalDiagnostics = diagnostics.getGlobalDiagnostics(); - if (currentGlobalDiagnostics !== previousGlobalDiagnostics) { - // If the arrays are not the same reference, new diagnostics were added. - const deferredGlobalDiagnostics = relativeComplement(previousGlobalDiagnostics, currentGlobalDiagnostics, compareDiagnostics); - return concatenate(deferredGlobalDiagnostics, semanticDiagnostics); - } - else if (previousGlobalDiagnosticsSize === 0 && currentGlobalDiagnostics.length > 0) { - // If the arrays are the same reference, but the length has changed, a single - // new diagnostic was added as DiagnosticCollection attempts to reuse the - // same array. - return concatenate(currentGlobalDiagnostics, semanticDiagnostics); + if (use & IterationUse.AllowsSyncIterablesFlag) { + const iterationTypes = getIterationTypesOfIterableSlow(type, syncIterationTypesResolver, errorNode); + if (iterationTypes !== noIterationTypes) { + if (use & IterationUse.AllowsAsyncIterablesFlag) { + return setCachedIterationTypes(type, "iterationTypesOfAsyncIterable", iterationTypes + ? getAsyncFromSyncIterationTypes(iterationTypes, errorNode) + : noIterationTypes); + } + else { + return iterationTypes; } - - return semanticDiagnostics; } - - // Global diagnostics are always added when a file is not provided to - // getDiagnostics - forEach(host.getSourceFiles(), checkSourceFile); - return diagnostics.getDiagnostics(); } - - function getGlobalDiagnostics(): Diagnostic[] { - throwIfNonDiagnosticsProducing(); - return diagnostics.getGlobalDiagnostics(); + return noIterationTypes; + } + /** + * Gets the *yield*, *return*, and *next* types of an `Iterable`-like or + * `AsyncIterable`-like type from the cache. + * + * NOTE: You probably don't want to call this directly and should be calling + * `getIterationTypesOfIterable` instead. + */ + function getIterationTypesOfIterableCached(type: ts.Type, resolver: IterationTypesResolver) { + return getCachedIterationTypes(type, resolver.iterableCacheKey); + } + function getIterationTypesOfGlobalIterableType(globalType: ts.Type, resolver: IterationTypesResolver) { + const globalIterationTypes = getIterationTypesOfIterableCached(globalType, resolver) || + getIterationTypesOfIterableSlow(globalType, resolver, /*errorNode*/ undefined); + return globalIterationTypes === noIterationTypes ? defaultIterationTypes : globalIterationTypes; + } + /** + * Gets the *yield*, *return*, and *next* types of an `Iterable`-like or `AsyncIterable`-like + * type from from common heuristics. + * + * If we previously analyzed this type and found no iteration types, `noIterationTypes` is + * returned. If we found iteration types, an `IterationTypes` record is returned. + * Otherwise, we return `undefined` to indicate to the caller it should perform a more + * exhaustive analysis. + * + * NOTE: You probably don't want to call this directly and should be calling + * `getIterationTypesOfIterable` instead. + */ + function getIterationTypesOfIterableFast(type: ts.Type, resolver: IterationTypesResolver) { + // As an optimization, if the type is an instantiation of one of the following global types, then + // just grab its related type argument: + // - `Iterable` or `AsyncIterable` + // - `IterableIterator` or `AsyncIterableIterator` + let globalType: ts.Type; + if (isReferenceToType(type, globalType = resolver.getGlobalIterableType(/*reportErrors*/ false)) || + isReferenceToType(type, globalType = resolver.getGlobalIterableIteratorType(/*reportErrors*/ false))) { + const [yieldType] = getTypeArguments((type as GenericType)); + // The "return" and "next" types of `Iterable` and `IterableIterator` are defined by the + // iteration types of their `[Symbol.iterator]()` method. The same is true for their async cousins. + // While we define these as `any` and `undefined` in our libs by default, a custom lib *could* use + // different definitions. + const { returnType, nextType } = getIterationTypesOfGlobalIterableType(globalType, resolver); + return setCachedIterationTypes(type, resolver.iterableCacheKey, createIterationTypes(yieldType, returnType, nextType)); + } + // As an optimization, if the type is an instantiation of the following global type, then + // just grab its related type arguments: + // - `Generator` or `AsyncGenerator` + if (isReferenceToType(type, resolver.getGlobalGeneratorType(/*reportErrors*/ false))) { + const [yieldType, returnType, nextType] = getTypeArguments((type as GenericType)); + return setCachedIterationTypes(type, resolver.iterableCacheKey, createIterationTypes(yieldType, returnType, nextType)); } - - function throwIfNonDiagnosticsProducing() { - if (!produceDiagnostics) { - throw new Error("Trying to get diagnostics from a type checker that does not produce them."); - } + } + /** + * Gets the *yield*, *return*, and *next* types of an `Iterable`-like or `AsyncIterable`-like + * type from its members. + * + * If we successfully found the *yield*, *return*, and *next* types, an `IterationTypes` + * record is returned. Otherwise, `noIterationTypes` is returned. + * + * NOTE: You probably don't want to call this directly and should be calling + * `getIterationTypesOfIterable` instead. + */ + function getIterationTypesOfIterableSlow(type: ts.Type, resolver: IterationTypesResolver, errorNode: Node | undefined) { + const method = getPropertyOfType(type, getPropertyNameForKnownSymbolName(resolver.iteratorSymbolName)); + const methodType = method && !(method.flags & SymbolFlags.Optional) ? getTypeOfSymbol(method) : undefined; + if (isTypeAny(methodType)) { + return setCachedIterationTypes(type, resolver.iterableCacheKey, anyIterationTypes); + } + const signatures = methodType ? getSignaturesOfType(methodType, SignatureKind.Call) : undefined; + if (!some(signatures)) { + return setCachedIterationTypes(type, resolver.iterableCacheKey, noIterationTypes); + } + const iteratorType = getUnionType(map(signatures, getReturnTypeOfSignature), UnionReduction.Subtype); + const iterationTypes = getIterationTypesOfIterator(iteratorType, resolver, errorNode) ?? noIterationTypes; + return setCachedIterationTypes(type, resolver.iterableCacheKey, iterationTypes); + } + function reportTypeNotIterableError(errorNode: Node, type: ts.Type, allowAsyncIterables: boolean): void { + const message = allowAsyncIterables + ? Diagnostics.Type_0_must_have_a_Symbol_asyncIterator_method_that_returns_an_async_iterator + : Diagnostics.Type_0_must_have_a_Symbol_iterator_method_that_returns_an_iterator; + errorAndMaybeSuggestAwait(errorNode, !!getAwaitedTypeOfPromise(type), message, typeToString(type)); + } + /** + * Gets the *yield*, *return*, and *next* types from an `Iterator`-like or `AsyncIterator`-like type. + * + * If we successfully found the *yield*, *return*, and *next* types, an `IterationTypes` + * record is returned. Otherwise, `undefined` is returned. + */ + function getIterationTypesOfIterator(type: ts.Type, resolver: IterationTypesResolver, errorNode: Node | undefined) { + if (isTypeAny(type)) { + return anyIterationTypes; + } + const iterationTypes = getIterationTypesOfIteratorCached(type, resolver) || + getIterationTypesOfIteratorFast(type, resolver) || + getIterationTypesOfIteratorSlow(type, resolver, errorNode); + return iterationTypes === noIterationTypes ? undefined : iterationTypes; + } + /** + * Gets the iteration types of an `Iterator`-like or `AsyncIterator`-like type from the + * cache. + * + * NOTE: You probably don't want to call this directly and should be calling + * `getIterationTypesOfIterator` instead. + */ + function getIterationTypesOfIteratorCached(type: ts.Type, resolver: IterationTypesResolver) { + return getCachedIterationTypes(type, resolver.iteratorCacheKey); + } + /** + * Gets the iteration types of an `Iterator`-like or `AsyncIterator`-like type from the + * cache or from common heuristics. + * + * If we previously analyzed this type and found no iteration types, `noIterationTypes` is + * returned. If we found iteration types, an `IterationTypes` record is returned. + * Otherwise, we return `undefined` to indicate to the caller it should perform a more + * exhaustive analysis. + * + * NOTE: You probably don't want to call this directly and should be calling + * `getIterationTypesOfIterator` instead. + */ + function getIterationTypesOfIteratorFast(type: ts.Type, resolver: IterationTypesResolver) { + // As an optimization, if the type is an instantiation of one of the following global types, + // then just grab its related type argument: + // - `IterableIterator` or `AsyncIterableIterator` + // - `Iterator` or `AsyncIterator` + // - `Generator` or `AsyncGenerator` + const globalType = resolver.getGlobalIterableIteratorType(/*reportErrors*/ false); + if (isReferenceToType(type, globalType)) { + const [yieldType] = getTypeArguments((type as GenericType)); + // The "return" and "next" types of `IterableIterator` and `AsyncIterableIterator` are defined by the + // iteration types of their `next`, `return`, and `throw` methods. While we define these as `any` + // and `undefined` in our libs by default, a custom lib *could* use different definitions. + const globalIterationTypes = getIterationTypesOfIteratorCached(globalType, resolver) || + getIterationTypesOfIteratorSlow(globalType, resolver, /*errorNode*/ undefined); + const { returnType, nextType } = globalIterationTypes === noIterationTypes ? defaultIterationTypes : globalIterationTypes; + return setCachedIterationTypes(type, resolver.iteratorCacheKey, createIterationTypes(yieldType, returnType, nextType)); + } + if (isReferenceToType(type, resolver.getGlobalIteratorType(/*reportErrors*/ false)) || + isReferenceToType(type, resolver.getGlobalGeneratorType(/*reportErrors*/ false))) { + const [yieldType, returnType, nextType] = getTypeArguments((type as GenericType)); + return setCachedIterationTypes(type, resolver.iteratorCacheKey, createIterationTypes(yieldType, returnType, nextType)); } - - // Language service support - - function getSymbolsInScope(location: Node, meaning: SymbolFlags): Symbol[] { - if (location.flags & NodeFlags.InWithStatement) { - // We cannot answer semantic questions within a with block, do not proceed any further - return []; - } - - const symbols = createSymbolTable(); - let isStatic = false; - - populateSymbols(); - - symbols.delete(InternalSymbolName.This); // Not a symbol, a keyword - return symbolsToArray(symbols); - - function populateSymbols() { - while (location) { - if (location.locals && !isGlobalSourceFile(location)) { - copySymbols(location.locals, meaning); - } - - switch (location.kind) { - case SyntaxKind.SourceFile: - if (!isExternalOrCommonJsModule(location)) break; - // falls through - case SyntaxKind.ModuleDeclaration: - copySymbols(getSymbolOfNode(location as ModuleDeclaration | SourceFile).exports!, meaning & SymbolFlags.ModuleMember); - break; - case SyntaxKind.EnumDeclaration: - copySymbols(getSymbolOfNode(location as EnumDeclaration).exports!, meaning & SymbolFlags.EnumMember); - break; - case SyntaxKind.ClassExpression: - const className = (location as ClassExpression).name; - if (className) { - copySymbol(location.symbol, meaning); - } - - // this fall-through is necessary because we would like to handle - // type parameter inside class expression similar to how we handle it in classDeclaration and interface Declaration. - // falls through - case SyntaxKind.ClassDeclaration: - case SyntaxKind.InterfaceDeclaration: - // If we didn't come from static member of class or interface, - // add the type parameters into the symbol table - // (type parameters of classDeclaration/classExpression and interface are in member property of the symbol. - // Note: that the memberFlags come from previous iteration. - if (!isStatic) { - copySymbols(getMembersOfSymbol(getSymbolOfNode(location as ClassDeclaration | InterfaceDeclaration)), meaning & SymbolFlags.Type); - } - break; - case SyntaxKind.FunctionExpression: - const funcName = (location as FunctionExpression).name; - if (funcName) { - copySymbol(location.symbol, meaning); - } - break; - } - - if (introducesArgumentsExoticObject(location)) { - copySymbol(argumentsSymbol, meaning); - } - - isStatic = hasModifier(location, ModifierFlags.Static); - location = location.parent; + } + function isIteratorResult(type: ts.Type, kind: IterationTypeKind.Yield | IterationTypeKind.Return) { + // From https://tc39.github.io/ecma262/#sec-iteratorresult-interface: + // > [done] is the result status of an iterator `next` method call. If the end of the iterator was reached `done` is `true`. + // > If the end was not reached `done` is `false` and a value is available. + // > If a `done` property (either own or inherited) does not exist, it is consider to have the value `false`. + const doneType = getTypeOfPropertyOfType(type, ("done" as __String)) || falseType; + return isTypeAssignableTo(kind === IterationTypeKind.Yield ? falseType : trueType, doneType); + } + function isYieldIteratorResult(type: ts.Type) { + return isIteratorResult(type, IterationTypeKind.Yield); + } + function isReturnIteratorResult(type: ts.Type) { + return isIteratorResult(type, IterationTypeKind.Return); + } + /** + * Gets the *yield* and *return* types of an `IteratorResult`-like type. + * + * If we are unable to determine a *yield* or a *return* type, `noIterationTypes` is + * returned to indicate to the caller that it should handle the error. Otherwise, an + * `IterationTypes` record is returned. + */ + function getIterationTypesOfIteratorResult(type: ts.Type) { + if (isTypeAny(type)) { + return anyIterationTypes; + } + const cachedTypes = getCachedIterationTypes(type, "iterationTypesOfIteratorResult"); + if (cachedTypes) { + return cachedTypes; + } + // As an optimization, if the type is an instantiation of one of the global `IteratorYieldResult` + // or `IteratorReturnResult` types, then just grab its type argument. + if (isReferenceToType(type, getGlobalIteratorYieldResultType(/*reportErrors*/ false))) { + const yieldType = getTypeArguments((type as GenericType))[0]; + return setCachedIterationTypes(type, "iterationTypesOfIteratorResult", createIterationTypes(yieldType, /*returnType*/ undefined, /*nextType*/ undefined)); + } + if (isReferenceToType(type, getGlobalIteratorReturnResultType(/*reportErrors*/ false))) { + const returnType = getTypeArguments((type as GenericType))[0]; + return setCachedIterationTypes(type, "iterationTypesOfIteratorResult", createIterationTypes(/*yieldType*/ undefined, returnType, /*nextType*/ undefined)); + } + // Choose any constituents that can produce the requested iteration type. + const yieldIteratorResult = filterType(type, isYieldIteratorResult); + const yieldType = yieldIteratorResult !== neverType ? getTypeOfPropertyOfType(yieldIteratorResult, ("value" as __String)) : undefined; + const returnIteratorResult = filterType(type, isReturnIteratorResult); + const returnType = returnIteratorResult !== neverType ? getTypeOfPropertyOfType(returnIteratorResult, ("value" as __String)) : undefined; + if (!yieldType && !returnType) { + return setCachedIterationTypes(type, "iterationTypesOfIteratorResult", noIterationTypes); + } + // From https://tc39.github.io/ecma262/#sec-iteratorresult-interface + // > ... If the iterator does not have a return value, `value` is `undefined`. In that case, the + // > `value` property may be absent from the conforming object if it does not inherit an explicit + // > `value` property. + return setCachedIterationTypes(type, "iterationTypesOfIteratorResult", createIterationTypes(yieldType, returnType || voidType, /*nextType*/ undefined)); + } + /** + * Gets the *yield*, *return*, and *next* types of a the `next()`, `return()`, or + * `throw()` method of an `Iterator`-like or `AsyncIterator`-like type. + * + * If we successfully found the *yield*, *return*, and *next* types, an `IterationTypes` + * record is returned. Otherwise, we return `undefined`. + */ + function getIterationTypesOfMethod(type: ts.Type, resolver: IterationTypesResolver, methodName: "next" | "return" | "throw", errorNode: Node | undefined): IterationTypes | undefined { + const method = getPropertyOfType(type, (methodName as __String)); + // Ignore 'return' or 'throw' if they are missing. + if (!method && methodName !== "next") { + return undefined; + } + const methodType = method && !(methodName === "next" && (method.flags & SymbolFlags.Optional)) + ? methodName === "next" ? getTypeOfSymbol(method) : getTypeWithFacts(getTypeOfSymbol(method), TypeFacts.NEUndefinedOrNull) + : undefined; + if (isTypeAny(methodType)) { + // `return()` and `throw()` don't provide a *next* type. + return methodName === "next" ? anyIterationTypes : anyIterationTypesExceptNext; + } + // Both async and non-async iterators *must* have a `next` method. + const methodSignatures = methodType ? getSignaturesOfType(methodType, SignatureKind.Call) : emptyArray; + if (methodSignatures.length === 0) { + if (errorNode) { + const diagnostic = methodName === "next" + ? resolver.mustHaveANextMethodDiagnostic + : resolver.mustBeAMethodDiagnostic; + error(errorNode, diagnostic, methodName); + } + return methodName === "next" ? anyIterationTypes : undefined; + } + // Extract the first parameter and return type of each signature. + let methodParameterTypes: ts.Type[] | undefined; + let methodReturnTypes: ts.Type[] | undefined; + for (const signature of methodSignatures) { + if (methodName !== "throw" && some(signature.parameters)) { + methodParameterTypes = append(methodParameterTypes, getTypeAtPosition(signature, 0)); + } + methodReturnTypes = append(methodReturnTypes, getReturnTypeOfSignature(signature)); + } + // Resolve the *next* or *return* type from the first parameter of a `next()` or + // `return()` method, respectively. + let returnTypes: ts.Type[] | undefined; + let nextType: ts.Type | undefined; + if (methodName !== "throw") { + const methodParameterType = methodParameterTypes ? getUnionType(methodParameterTypes) : unknownType; + if (methodName === "next") { + // The value of `next(value)` is *not* awaited by async generators + nextType = methodParameterType; + } + else if (methodName === "return") { + // The value of `return(value)` *is* awaited by async generators + const resolvedMethodParameterType = resolver.resolveIterationType(methodParameterType, errorNode) || anyType; + returnTypes = append(returnTypes, resolvedMethodParameterType); + } + } + // Resolve the *yield* and *return* types from the return type of the method (i.e. `IteratorResult`) + let yieldType: ts.Type; + const methodReturnType = methodReturnTypes ? getUnionType(methodReturnTypes, UnionReduction.Subtype) : neverType; + const resolvedMethodReturnType = resolver.resolveIterationType(methodReturnType, errorNode) || anyType; + const iterationTypes = getIterationTypesOfIteratorResult(resolvedMethodReturnType); + if (iterationTypes === noIterationTypes) { + if (errorNode) { + error(errorNode, resolver.mustHaveAValueDiagnostic, methodName); + } + yieldType = anyType; + returnTypes = append(returnTypes, anyType); + } + else { + yieldType = iterationTypes.yieldType; + returnTypes = append(returnTypes, iterationTypes.returnType); + } + return createIterationTypes(yieldType, getUnionType(returnTypes), nextType); + } + /** + * Gets the *yield*, *return*, and *next* types of an `Iterator`-like or `AsyncIterator`-like + * type from its members. + * + * If we successfully found the *yield*, *return*, and *next* types, an `IterationTypes` + * record is returned. Otherwise, `noIterationTypes` is returned. + * + * NOTE: You probably don't want to call this directly and should be calling + * `getIterationTypesOfIterator` instead. + */ + function getIterationTypesOfIteratorSlow(type: ts.Type, resolver: IterationTypesResolver, errorNode: Node | undefined) { + const iterationTypes = combineIterationTypes([ + getIterationTypesOfMethod(type, resolver, "next", errorNode), + getIterationTypesOfMethod(type, resolver, "return", errorNode), + getIterationTypesOfMethod(type, resolver, "throw", errorNode), + ]); + return setCachedIterationTypes(type, resolver.iteratorCacheKey, iterationTypes); + } + /** + * Gets the requested "iteration type" from a type that is either `Iterable`-like, `Iterator`-like, + * `IterableIterator`-like, or `Generator`-like (for a non-async generator); or `AsyncIterable`-like, + * `AsyncIterator`-like, `AsyncIterableIterator`-like, or `AsyncGenerator`-like (for an async generator). + */ + function getIterationTypeOfGeneratorFunctionReturnType(kind: IterationTypeKind, returnType: ts.Type, isAsyncGenerator: boolean): ts.Type | undefined { + if (isTypeAny(returnType)) { + return undefined; + } + const iterationTypes = getIterationTypesOfGeneratorFunctionReturnType(returnType, isAsyncGenerator); + return iterationTypes && iterationTypes[getIterationTypesKeyFromIterationTypeKind(kind)]; + } + function getIterationTypesOfGeneratorFunctionReturnType(type: ts.Type, isAsyncGenerator: boolean) { + if (isTypeAny(type)) { + return anyIterationTypes; + } + const use = isAsyncGenerator ? IterationUse.AsyncGeneratorReturnType : IterationUse.GeneratorReturnType; + const resolver = isAsyncGenerator ? asyncIterationTypesResolver : syncIterationTypesResolver; + return getIterationTypesOfIterable(type, use, /*errorNode*/ undefined) || + getIterationTypesOfIterator(type, resolver, /*errorNode*/ undefined); + } + function checkBreakOrContinueStatement(node: BreakOrContinueStatement) { + // Grammar checking + if (!checkGrammarStatementInAmbientContext(node)) + checkGrammarBreakOrContinueStatement(node); + // TODO: Check that target label is valid + } + function unwrapReturnType(returnType: ts.Type, functionFlags: FunctionFlags) { + const isGenerator = !!(functionFlags & FunctionFlags.Generator); + const isAsync = !!(functionFlags & FunctionFlags.Async); + return isGenerator ? getIterationTypeOfGeneratorFunctionReturnType(IterationTypeKind.Return, returnType, isAsync) || errorType : + isAsync ? getPromisedTypeOfPromise(returnType) || errorType : + returnType; + } + function isUnwrappedReturnTypeVoidOrAny(func: SignatureDeclaration, returnType: ts.Type): boolean { + const unwrappedReturnType = unwrapReturnType(returnType, getFunctionFlags(func)); + return !!unwrappedReturnType && maybeTypeOfKind(unwrappedReturnType, TypeFlags.Void | TypeFlags.AnyOrUnknown); + } + function checkReturnStatement(node: ReturnStatement) { + // Grammar checking + if (checkGrammarStatementInAmbientContext(node)) { + return; + } + const func = getContainingFunction(node); + if (!func) { + grammarErrorOnFirstToken(node, Diagnostics.A_return_statement_can_only_be_used_within_a_function_body); + return; + } + const signature = getSignatureFromDeclaration(func); + const returnType = getReturnTypeOfSignature(signature); + const functionFlags = getFunctionFlags(func); + if (strictNullChecks || node.expression || returnType.flags & TypeFlags.Never) { + const exprType = node.expression ? checkExpressionCached(node.expression) : undefinedType; + if (func.kind === SyntaxKind.SetAccessor) { + if (node.expression) { + error(node, Diagnostics.Setters_cannot_return_a_value); } - - copySymbols(globals, meaning); } - - /** - * Copy the given symbol into symbol tables if the symbol has the given meaning - * and it doesn't already existed in the symbol table - * @param key a key for storing in symbol table; if undefined, use symbol.name - * @param symbol the symbol to be added into symbol table - * @param meaning meaning of symbol to filter by before adding to symbol table - */ - function copySymbol(symbol: Symbol, meaning: SymbolFlags): void { - if (getCombinedLocalAndExportSymbolFlags(symbol) & meaning) { - const id = symbol.escapedName; - // We will copy all symbol regardless of its reserved name because - // symbolsToArray will check whether the key is a reserved name and - // it will not copy symbol with reserved name to the array - if (!symbols.has(id)) { - symbols.set(id, symbol); - } + else if (func.kind === SyntaxKind.Constructor) { + if (node.expression && !checkTypeAssignableToAndOptionallyElaborate(exprType, returnType, node, node.expression)) { + error(node, Diagnostics.Return_type_of_constructor_signature_must_be_assignable_to_the_instance_type_of_the_class); } } - - function copySymbols(source: SymbolTable, meaning: SymbolFlags): void { - if (meaning) { - source.forEach(symbol => { - copySymbol(symbol, meaning); - }); + else if (getReturnTypeFromAnnotation(func)) { + const unwrappedReturnType = unwrapReturnType(returnType, functionFlags); + const unwrappedExprType = functionFlags & FunctionFlags.Async + ? checkAwaitedType(exprType, node, Diagnostics.The_return_type_of_an_async_function_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member) + : exprType; + if (unwrappedReturnType) { + // If the function has a return type, but promisedType is + // undefined, an error will be reported in checkAsyncFunctionReturnType + // so we don't need to report one here. + checkTypeAssignableToAndOptionallyElaborate(unwrappedExprType, unwrappedReturnType, node, node.expression); } } } - - function isTypeDeclarationName(name: Node): boolean { - return name.kind === SyntaxKind.Identifier && - isTypeDeclaration(name.parent) && - name.parent.name === name; + else if (func.kind !== SyntaxKind.Constructor && compilerOptions.noImplicitReturns && !isUnwrappedReturnTypeVoidOrAny(func, returnType)) { + // The function has a return type, but the return statement doesn't have an expression. + error(node, Diagnostics.Not_all_code_paths_return_a_value); } - - function isTypeDeclaration(node: Node): node is TypeParameterDeclaration | ClassDeclaration | InterfaceDeclaration | TypeAliasDeclaration | EnumDeclaration | ImportClause | ImportSpecifier | ExportSpecifier { - switch (node.kind) { - case SyntaxKind.TypeParameter: - case SyntaxKind.ClassDeclaration: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.EnumDeclaration: - return true; - case SyntaxKind.ImportClause: - return (node as ImportClause).isTypeOnly; - case SyntaxKind.ImportSpecifier: - case SyntaxKind.ExportSpecifier: - return (node as ImportSpecifier | ExportSpecifier).parent.parent.isTypeOnly; - default: - return false; + } + function checkWithStatement(node: WithStatement) { + // Grammar checking for withStatement + if (!checkGrammarStatementInAmbientContext(node)) { + if (node.flags & NodeFlags.AwaitContext) { + grammarErrorOnFirstToken(node, Diagnostics.with_statements_are_not_allowed_in_an_async_function_block); } } - - // True if the given identifier is part of a type reference - function isTypeReferenceIdentifier(node: EntityName): boolean { - while (node.parent.kind === SyntaxKind.QualifiedName) { - node = node.parent as QualifiedName; - } - - return node.parent.kind === SyntaxKind.TypeReference; + checkExpression(node.expression); + const sourceFile = getSourceFileOfNode(node); + if (!hasParseDiagnostics(sourceFile)) { + const start = getSpanOfTokenAtPosition(sourceFile, node.pos).start; + const end = node.statement.pos; + grammarErrorAtPos(sourceFile, start, end - start, Diagnostics.The_with_statement_is_not_supported_All_symbols_in_a_with_block_will_have_type_any); } - - function isHeritageClauseElementIdentifier(node: Node): boolean { - while (node.parent.kind === SyntaxKind.PropertyAccessExpression) { - node = node.parent; + } + function checkSwitchStatement(node: SwitchStatement) { + // Grammar checking + checkGrammarStatementInAmbientContext(node); + let firstDefaultClause: CaseOrDefaultClause; + let hasDuplicateDefaultClause = false; + const expressionType = checkExpression(node.expression); + const expressionIsLiteral = isLiteralType(expressionType); + forEach(node.caseBlock.clauses, clause => { + // Grammar check for duplicate default clauses, skip if we already report duplicate default clause + if (clause.kind === SyntaxKind.DefaultClause && !hasDuplicateDefaultClause) { + if (firstDefaultClause === undefined) { + firstDefaultClause = clause; + } + else { + grammarErrorOnNode(clause, Diagnostics.A_default_clause_cannot_appear_more_than_once_in_a_switch_statement); + hasDuplicateDefaultClause = true; + } } - - return node.parent.kind === SyntaxKind.ExpressionWithTypeArguments; - } - - function forEachEnclosingClass(node: Node, callback: (node: Node) => T | undefined): T | undefined { - let result: T | undefined; - - while (true) { - node = getContainingClass(node)!; - if (!node) break; - if (result = callback(node)) break; + if (produceDiagnostics && clause.kind === SyntaxKind.CaseClause) { + // TypeScript 1.0 spec (April 2014): 5.9 + // In a 'switch' statement, each 'case' expression must be of a type that is comparable + // to or from the type of the 'switch' expression. + let caseType = checkExpression(clause.expression); + const caseIsLiteral = isLiteralType(caseType); + let comparedExpressionType = expressionType; + if (!caseIsLiteral || !expressionIsLiteral) { + caseType = caseIsLiteral ? getBaseTypeOfLiteralType(caseType) : caseType; + comparedExpressionType = getBaseTypeOfLiteralType(expressionType); + } + if (!isTypeEqualityComparableTo(comparedExpressionType, caseType)) { + // expressionType is not comparable to caseType, try the reversed check and report errors if it fails + checkTypeComparableTo(caseType, comparedExpressionType, clause.expression, /*headMessage*/ undefined); + } } - - return result; + forEach(clause.statements, checkSourceElement); + if (compilerOptions.noFallthroughCasesInSwitch && clause.fallthroughFlowNode && isReachableFlowNode(clause.fallthroughFlowNode)) { + error(clause, Diagnostics.Fallthrough_case_in_switch); + } + }); + if (node.caseBlock.locals) { + registerForUnusedIdentifiersCheck(node.caseBlock); } - - function isNodeUsedDuringClassInitialization(node: Node) { - return !!findAncestor(node, element => { - if (isConstructorDeclaration(element) && nodeIsPresent(element.body) || isPropertyDeclaration(element)) { - return true; - } - else if (isClassLike(element) || isFunctionLikeDeclaration(element)) { + } + function checkLabeledStatement(node: LabeledStatement) { + // Grammar checking + if (!checkGrammarStatementInAmbientContext(node)) { + findAncestor(node.parent, current => { + if (isFunctionLike(current)) { return "quit"; } - + if (current.kind === SyntaxKind.LabeledStatement && (current).label.escapedText === node.label.escapedText) { + grammarErrorOnNode(node.label, Diagnostics.Duplicate_label_0, getTextOfNode(node.label)); + return true; + } return false; }); } - - function isNodeWithinClass(node: Node, classDeclaration: ClassLikeDeclaration) { - return !!forEachEnclosingClass(node, n => n === classDeclaration); + // ensure that label is unique + checkSourceElement(node.statement); + } + function checkThrowStatement(node: ThrowStatement) { + // Grammar checking + if (!checkGrammarStatementInAmbientContext(node)) { + if (node.expression === undefined) { + grammarErrorAfterFirstToken(node, Diagnostics.Line_break_not_permitted_here); + } } - - function getLeftSideOfImportEqualsOrExportAssignment(nodeOnRightSide: EntityName): ImportEqualsDeclaration | ExportAssignment | undefined { - while (nodeOnRightSide.parent.kind === SyntaxKind.QualifiedName) { - nodeOnRightSide = nodeOnRightSide.parent; + if (node.expression) { + checkExpression(node.expression); + } + } + function checkTryStatement(node: TryStatement) { + // Grammar checking + checkGrammarStatementInAmbientContext(node); + checkBlock(node.tryBlock); + const catchClause = node.catchClause; + if (catchClause) { + // Grammar checking + if (catchClause.variableDeclaration) { + if (catchClause.variableDeclaration.type) { + grammarErrorOnFirstToken(catchClause.variableDeclaration.type, Diagnostics.Catch_clause_variable_cannot_have_a_type_annotation); + } + else if (catchClause.variableDeclaration.initializer) { + grammarErrorOnFirstToken(catchClause.variableDeclaration.initializer, Diagnostics.Catch_clause_variable_cannot_have_an_initializer); + } + else { + const blockLocals = catchClause.block.locals; + if (blockLocals) { + forEachKey((catchClause.locals!), caughtName => { + const blockLocal = blockLocals.get(caughtName); + if (blockLocal && (blockLocal.flags & SymbolFlags.BlockScopedVariable) !== 0) { + grammarErrorOnNode(blockLocal.valueDeclaration, Diagnostics.Cannot_redeclare_identifier_0_in_catch_clause, caughtName); + } + }); + } + } } - - if (nodeOnRightSide.parent.kind === SyntaxKind.ImportEqualsDeclaration) { - return (nodeOnRightSide.parent).moduleReference === nodeOnRightSide ? nodeOnRightSide.parent : undefined; + checkBlock(catchClause.block); + } + if (node.finallyBlock) { + checkBlock(node.finallyBlock); + } + } + function checkIndexConstraints(type: ts.Type) { + const declaredNumberIndexer = getIndexDeclarationOfSymbol(type.symbol, IndexKind.Number); + const declaredStringIndexer = getIndexDeclarationOfSymbol(type.symbol, IndexKind.String); + const stringIndexType = getIndexTypeOfType(type, IndexKind.String); + const numberIndexType = getIndexTypeOfType(type, IndexKind.Number); + if (stringIndexType || numberIndexType) { + forEach(getPropertiesOfObjectType(type), prop => { + const propType = getTypeOfSymbol(prop); + checkIndexConstraintForProperty(prop, propType, type, declaredStringIndexer, stringIndexType, IndexKind.String); + checkIndexConstraintForProperty(prop, propType, type, declaredNumberIndexer, numberIndexType, IndexKind.Number); + }); + const classDeclaration = type.symbol.valueDeclaration; + if (getObjectFlags(type) & ObjectFlags.Class && isClassLike(classDeclaration)) { + for (const member of classDeclaration.members) { + // Only process instance properties with computed names here. + // Static properties cannot be in conflict with indexers, + // and properties with literal names were already checked. + if (!hasModifier(member, ModifierFlags.Static) && hasNonBindableDynamicName(member)) { + const symbol = getSymbolOfNode(member); + const propType = getTypeOfSymbol(symbol); + checkIndexConstraintForProperty(symbol, propType, type, declaredStringIndexer, stringIndexType, IndexKind.String); + checkIndexConstraintForProperty(symbol, propType, type, declaredNumberIndexer, numberIndexType, IndexKind.Number); + } + } } - - if (nodeOnRightSide.parent.kind === SyntaxKind.ExportAssignment) { - return (nodeOnRightSide.parent).expression === nodeOnRightSide ? nodeOnRightSide.parent : undefined; + } + let errorNode: Node | undefined; + if (stringIndexType && numberIndexType) { + errorNode = declaredNumberIndexer || declaredStringIndexer; + // condition 'errorNode === undefined' may appear if types does not declare nor string neither number indexer + if (!errorNode && (getObjectFlags(type) & ObjectFlags.Interface)) { + const someBaseTypeHasBothIndexers = forEach(getBaseTypes((type)), base => getIndexTypeOfType(base, IndexKind.String) && getIndexTypeOfType(base, IndexKind.Number)); + errorNode = someBaseTypeHasBothIndexers ? undefined : type.symbol.declarations[0]; } - - return undefined; } - - function isInRightSideOfImportOrExportAssignment(node: EntityName) { - return getLeftSideOfImportEqualsOrExportAssignment(node) !== undefined; + if (errorNode && !isTypeAssignableTo(numberIndexType!, stringIndexType!)) { // TODO: GH#18217 + error(errorNode, Diagnostics.Numeric_index_type_0_is_not_assignable_to_string_index_type_1, typeToString(numberIndexType!), typeToString(stringIndexType!)); } - - function getSpecialPropertyAssignmentSymbolFromEntityName(entityName: EntityName | PropertyAccessExpression) { - const specialPropertyAssignmentKind = getAssignmentDeclarationKind(entityName.parent.parent as BinaryExpression); - switch (specialPropertyAssignmentKind) { - case AssignmentDeclarationKind.ExportsProperty: - case AssignmentDeclarationKind.PrototypeProperty: - return getSymbolOfNode(entityName.parent); - case AssignmentDeclarationKind.ThisProperty: - case AssignmentDeclarationKind.ModuleExports: - case AssignmentDeclarationKind.Property: - return getSymbolOfNode(entityName.parent.parent); + function checkIndexConstraintForProperty(prop: ts.Symbol, propertyType: ts.Type, containingType: ts.Type, indexDeclaration: Declaration | undefined, indexType: ts.Type | undefined, indexKind: IndexKind): void { + // ESSymbol properties apply to neither string nor numeric indexers. + if (!indexType || isKnownSymbol(prop)) { + return; } - } - - function isImportTypeQualifierPart(node: EntityName): ImportTypeNode | undefined { - let parent = node.parent; - while (isQualifiedName(parent)) { - node = parent; - parent = parent.parent; + const propDeclaration = prop.valueDeclaration; + const name = propDeclaration && getNameOfDeclaration(propDeclaration); + // index is numeric and property name is not valid numeric literal + if (indexKind === IndexKind.Number && !(name ? isNumericName(name) : isNumericLiteralName(prop.escapedName))) { + return; + } + // perform property check if property or indexer is declared in 'type' + // this allows us to rule out cases when both property and indexer are inherited from the base class + let errorNode: Node | undefined; + if (propDeclaration && name && + (propDeclaration.kind === SyntaxKind.BinaryExpression || + name.kind === SyntaxKind.ComputedPropertyName || + prop.parent === containingType.symbol)) { + errorNode = propDeclaration; } - if (parent && parent.kind === SyntaxKind.ImportType && (parent as ImportTypeNode).qualifier === node) { - return parent as ImportTypeNode; + else if (indexDeclaration) { + errorNode = indexDeclaration; } - return undefined; + else if (getObjectFlags(containingType) & ObjectFlags.Interface) { + // for interfaces property and indexer might be inherited from different bases + // check if any base class already has both property and indexer. + // check should be performed only if 'type' is the first type that brings property\indexer together + const someBaseClassHasBothPropertyAndIndexer = forEach(getBaseTypes((containingType)), base => getPropertyOfObjectType(base, prop.escapedName) && getIndexTypeOfType(base, indexKind)); + errorNode = someBaseClassHasBothPropertyAndIndexer ? undefined : containingType.symbol.declarations[0]; + } + if (errorNode && !isTypeAssignableTo(propertyType, indexType)) { + const errorMessage = indexKind === IndexKind.String + ? Diagnostics.Property_0_of_type_1_is_not_assignable_to_string_index_type_2 + : Diagnostics.Property_0_of_type_1_is_not_assignable_to_numeric_index_type_2; + error(errorNode, errorMessage, symbolToString(prop), typeToString(propertyType), typeToString(indexType)); + } + } + } + function checkTypeNameIsReserved(name: Identifier, message: DiagnosticMessage): void { + // TS 1.0 spec (April 2014): 3.6.1 + // The predefined type keywords are reserved and cannot be used as names of user defined types. + switch (name.escapedText) { + case "any": + case "unknown": + case "number": + case "bigint": + case "boolean": + case "string": + case "symbol": + case "void": + case "object": + error(name, message, name.escapedText as string); } - - function getSymbolOfNameOrPropertyAccessExpression(name: EntityName | PrivateIdentifier | PropertyAccessExpression): Symbol | undefined { - if (isDeclarationName(name)) { - return getSymbolOfNode(name.parent); - } - - if (isInJSFile(name) && - name.parent.kind === SyntaxKind.PropertyAccessExpression && - name.parent === (name.parent.parent as BinaryExpression).left) { - // Check if this is a special property assignment - if (!isPrivateIdentifier(name)) { - const specialPropertyAssignmentSymbol = getSpecialPropertyAssignmentSymbolFromEntityName(name); - if (specialPropertyAssignmentSymbol) { - return specialPropertyAssignmentSymbol; + } + /** + * The name cannot be used as 'Object' of user defined types with special target. + */ + function checkClassNameCollisionWithObject(name: Identifier): void { + if (languageVersion === ScriptTarget.ES5 && name.escapedText === "Object" + && moduleKind < ModuleKind.ES2015) { + error(name, Diagnostics.Class_name_cannot_be_Object_when_targeting_ES5_with_module_0, ModuleKind[moduleKind]); // https://github.com/Microsoft/TypeScript/issues/17494 + } + } + /** + * Check each type parameter and check that type parameters have no duplicate type parameter declarations + */ + function checkTypeParameters(typeParameterDeclarations: readonly TypeParameterDeclaration[] | undefined) { + if (typeParameterDeclarations) { + let seenDefault = false; + for (let i = 0; i < typeParameterDeclarations.length; i++) { + const node = typeParameterDeclarations[i]; + checkTypeParameter(node); + if (produceDiagnostics) { + if (node.default) { + seenDefault = true; + checkTypeParametersNotReferenced(node.default, typeParameterDeclarations, i); + } + else if (seenDefault) { + error(node, Diagnostics.Required_type_parameters_may_not_follow_optional_type_parameters); + } + for (let j = 0; j < i; j++) { + if (typeParameterDeclarations[j].symbol === node.symbol) { + error(node.name, Diagnostics.Duplicate_identifier_0, declarationNameToString(node.name)); + } } } } - - if (name.parent.kind === SyntaxKind.ExportAssignment && isEntityNameExpression(name)) { - // Even an entity name expression that doesn't resolve as an entityname may still typecheck as a property access expression - const success = resolveEntityName(name, - /*all meanings*/ SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace | SymbolFlags.Alias, /*ignoreErrors*/ true); - if (success && success !== unknownSymbol) { - return success; + } + } + /** Check that type parameter defaults only reference previously declared type parameters */ + function checkTypeParametersNotReferenced(root: TypeNode, typeParameters: readonly TypeParameterDeclaration[], index: number) { + visit(root); + function visit(node: Node) { + if (node.kind === SyntaxKind.TypeReference) { + const type = getTypeFromTypeReference((node)); + if (type.flags & TypeFlags.TypeParameter) { + for (let i = index; i < typeParameters.length; i++) { + if (type.symbol === getSymbolOfNode(typeParameters[i])) { + error(node, Diagnostics.Type_parameter_defaults_can_only_reference_previously_declared_type_parameters); + } + } } } - else if (!isPropertyAccessExpression(name) && !isPrivateIdentifier(name) && isInRightSideOfImportOrExportAssignment(name)) { - // Since we already checked for ExportAssignment, this really could only be an Import - const importEqualsDeclaration = getAncestor(name, SyntaxKind.ImportEqualsDeclaration); - Debug.assert(importEqualsDeclaration !== undefined); - return getSymbolOfPartOfRightHandSideOfImportEquals(name, /*dontResolveAlias*/ true); + forEachChild(node, visit); + } + } + /** Check that type parameter lists are identical across multiple declarations */ + function checkTypeParameterListsIdentical(symbol: ts.Symbol) { + if (symbol.declarations.length === 1) { + return; + } + const links = getSymbolLinks(symbol); + if (!links.typeParametersChecked) { + links.typeParametersChecked = true; + const declarations = getClassOrInterfaceDeclarationsOfSymbol(symbol); + if (declarations.length <= 1) { + return; } - - if (!isPropertyAccessExpression(name) && !isPrivateIdentifier(name)) { - const possibleImportNode = isImportTypeQualifierPart(name); - if (possibleImportNode) { - getTypeFromTypeNode(possibleImportNode); - const sym = getNodeLinks(name).resolvedSymbol; - return sym === unknownSymbol ? undefined : sym; + const type = (getDeclaredTypeOfSymbol(symbol)); + if (!areTypeParametersIdentical(declarations, type.localTypeParameters!)) { + // Report an error on every conflicting declaration. + const name = symbolToString(symbol); + for (const declaration of declarations) { + error(declaration.name, Diagnostics.All_declarations_of_0_must_have_identical_type_parameters, name); } } - - while (isRightSideOfQualifiedNameOrPropertyAccess(name)) { - name = name.parent; + } + } + function areTypeParametersIdentical(declarations: readonly (ClassDeclaration | InterfaceDeclaration)[], targetParameters: TypeParameter[]) { + const maxTypeArgumentCount = length(targetParameters); + const minTypeArgumentCount = getMinTypeArgumentCount(targetParameters); + for (const declaration of declarations) { + // If this declaration has too few or too many type parameters, we report an error + const sourceParameters = getEffectiveTypeParameterDeclarations(declaration); + const numTypeParameters = sourceParameters.length; + if (numTypeParameters < minTypeArgumentCount || numTypeParameters > maxTypeArgumentCount) { + return false; } - - if (isHeritageClauseElementIdentifier(name)) { - let meaning = SymbolFlags.None; - // In an interface or class, we're definitely interested in a type. - if (name.parent.kind === SyntaxKind.ExpressionWithTypeArguments) { - meaning = SymbolFlags.Type; - - // In a class 'extends' clause we are also looking for a value. - if (isExpressionWithTypeArgumentsInClassExtendsClause(name.parent)) { - meaning |= SymbolFlags.Value; - } + for (let i = 0; i < numTypeParameters; i++) { + const source = sourceParameters[i]; + const target = targetParameters[i]; + // If the type parameter node does not have the same as the resolved type + // parameter at this position, we report an error. + if (source.name.escapedText !== target.symbol.escapedName) { + return false; } - else { - meaning = SymbolFlags.Namespace; + // If the type parameter node does not have an identical constraint as the resolved + // type parameter at this position, we report an error. + const constraint = getEffectiveConstraintOfTypeParameter(source); + const sourceConstraint = constraint && getTypeFromTypeNode(constraint); + const targetConstraint = getConstraintOfTypeParameter(target); + // relax check if later interface augmentation has no constraint, it's more broad and is OK to merge with + // a more constrained interface (this could be generalized to a full hierarchy check, but that's maybe overkill) + if (sourceConstraint && targetConstraint && !isTypeIdenticalTo(sourceConstraint, targetConstraint)) { + return false; } - - meaning |= SymbolFlags.Alias; - const entityNameSymbol = isEntityNameExpression(name) ? resolveEntityName(name, meaning) : undefined; - if (entityNameSymbol) { - return entityNameSymbol; + // If the type parameter node has a default and it is not identical to the default + // for the type parameter at this position, we report an error. + const sourceDefault = source.default && getTypeFromTypeNode(source.default); + const targetDefault = getDefaultFromTypeParameter(target); + if (sourceDefault && targetDefault && !isTypeIdenticalTo(sourceDefault, targetDefault)) { + return false; } } - - if (name.parent.kind === SyntaxKind.JSDocParameterTag) { - return getParameterSymbolFromJSDoc(name.parent as JSDocParameterTag); + } + return true; + } + function checkClassExpression(node: ClassExpression): ts.Type { + checkClassLikeDeclaration(node); + checkNodeDeferred(node); + return getTypeOfSymbol(getSymbolOfNode(node)); + } + function checkClassExpressionDeferred(node: ClassExpression) { + forEach(node.members, checkSourceElement); + registerForUnusedIdentifiersCheck(node); + } + function checkClassDeclaration(node: ClassDeclaration) { + if (!node.name && !hasModifier(node, ModifierFlags.Default)) { + grammarErrorOnFirstToken(node, Diagnostics.A_class_declaration_without_the_default_modifier_must_have_a_name); + } + checkClassLikeDeclaration(node); + forEach(node.members, checkSourceElement); + registerForUnusedIdentifiersCheck(node); + } + function checkClassLikeDeclaration(node: ClassLikeDeclaration) { + checkGrammarClassLikeDeclaration(node); + checkDecorators(node); + if (node.name) { + checkTypeNameIsReserved(node.name, Diagnostics.Class_name_cannot_be_0); + checkCollisionWithRequireExportsInGeneratedCode(node, node.name); + checkCollisionWithGlobalPromiseInGeneratedCode(node, node.name); + if (!(node.flags & NodeFlags.Ambient)) { + checkClassNameCollisionWithObject(node.name); + } + } + checkTypeParameters(getEffectiveTypeParameterDeclarations(node)); + checkExportsOnMergedDeclarations(node); + const symbol = getSymbolOfNode(node); + const type = (getDeclaredTypeOfSymbol(symbol)); + const typeWithThis = getTypeWithThisArgument(type); + const staticType = (getTypeOfSymbol(symbol)); + checkTypeParameterListsIdentical(symbol); + checkClassForDuplicateDeclarations(node); + // Only check for reserved static identifiers on non-ambient context. + if (!(node.flags & NodeFlags.Ambient)) { + checkClassForStaticPropertyNameConflicts(node); + } + const baseTypeNode = getEffectiveBaseTypeNode(node); + if (baseTypeNode) { + forEach(baseTypeNode.typeArguments, checkSourceElement); + if (languageVersion < ScriptTarget.ES2015) { + checkExternalEmitHelpers(baseTypeNode.parent, ExternalEmitHelpers.Extends); } - - if (name.parent.kind === SyntaxKind.TypeParameter && name.parent.parent.kind === SyntaxKind.JSDocTemplateTag) { - Debug.assert(!isInJSFile(name)); // Otherwise `isDeclarationName` would have been true. - const typeParameter = getTypeParameterFromJsDoc(name.parent as TypeParameterDeclaration & { parent: JSDocTemplateTag }); - return typeParameter && typeParameter.symbol; + // check both @extends and extends if both are specified. + const extendsNode = getClassExtendsHeritageElement(node); + if (extendsNode && extendsNode !== baseTypeNode) { + checkExpression(extendsNode.expression); } - - if (isExpressionNode(name)) { - if (nodeIsMissing(name)) { - // Missing entity name. - return undefined; - } - - if (name.kind === SyntaxKind.Identifier) { - if (isJSXTagName(name) && isJsxIntrinsicIdentifier(name)) { - const symbol = getIntrinsicTagSymbol(name.parent); - return symbol === unknownSymbol ? undefined : symbol; + const baseTypes = getBaseTypes(type); + if (baseTypes.length && produceDiagnostics) { + const baseType = baseTypes[0]; + const baseConstructorType = getBaseConstructorTypeOfClass(type); + const staticBaseType = getApparentType(baseConstructorType); + checkBaseTypeAccessibility(staticBaseType, baseTypeNode); + checkSourceElement(baseTypeNode.expression); + if (some(baseTypeNode.typeArguments)) { + forEach(baseTypeNode.typeArguments, checkSourceElement); + for (const constructor of getConstructorsForTypeArguments(staticBaseType, baseTypeNode.typeArguments, baseTypeNode)) { + if (!checkTypeArgumentConstraints(baseTypeNode, constructor.typeParameters!)) { + break; + } } - - return resolveEntityName(name, SymbolFlags.Value, /*ignoreErrors*/ false, /*dontResolveAlias*/ true); } - else if (name.kind === SyntaxKind.PropertyAccessExpression || name.kind === SyntaxKind.QualifiedName) { - const links = getNodeLinks(name); - if (links.resolvedSymbol) { - return links.resolvedSymbol; - } - - if (name.kind === SyntaxKind.PropertyAccessExpression) { - checkPropertyAccessExpression(name); - } - else { - checkQualifiedName(name); + const baseWithThis = getTypeWithThisArgument(baseType, type.thisType); + if (!checkTypeAssignableTo(typeWithThis, baseWithThis, /*errorNode*/ undefined)) { + issueMemberSpecificError(node, typeWithThis, baseWithThis, Diagnostics.Class_0_incorrectly_extends_base_class_1); + } + else { + // Report static side error only when instance type is assignable + checkTypeAssignableTo(staticType, getTypeWithoutSignatures(staticBaseType), node.name || node, Diagnostics.Class_static_side_0_incorrectly_extends_base_class_static_side_1); + } + if (baseConstructorType.flags & TypeFlags.TypeVariable && !isMixinConstructorType(staticType)) { + error(node.name || node, Diagnostics.A_mixin_class_must_have_a_constructor_with_a_single_rest_parameter_of_type_any); + } + if (!(staticBaseType.symbol && staticBaseType.symbol.flags & SymbolFlags.Class) && !(baseConstructorType.flags & TypeFlags.TypeVariable)) { + // When the static base type is a "class-like" constructor function (but not actually a class), we verify + // that all instantiated base constructor signatures return the same type. + const constructors = getInstantiatedConstructorsForTypeArguments(staticBaseType, baseTypeNode.typeArguments, baseTypeNode); + if (forEach(constructors, sig => !isJSConstructor(sig.declaration) && !isTypeIdenticalTo(getReturnTypeOfSignature(sig), baseType))) { + error(baseTypeNode.expression, Diagnostics.Base_constructors_must_all_have_the_same_return_type); + } + } + checkKindsOfPropertyMemberOverrides(type, baseType); + } + } + const implementedTypeNodes = getEffectiveImplementsTypeNodes(node); + if (implementedTypeNodes) { + for (const typeRefNode of implementedTypeNodes) { + if (!isEntityNameExpression(typeRefNode.expression)) { + error(typeRefNode.expression, Diagnostics.A_class_can_only_implement_an_identifier_Slashqualified_name_with_optional_type_arguments); + } + checkTypeReferenceNode(typeRefNode); + if (produceDiagnostics) { + const t = getTypeFromTypeNode(typeRefNode); + if (t !== errorType) { + if (isValidBaseType(t)) { + const genericDiag = t.symbol && t.symbol.flags & SymbolFlags.Class ? + Diagnostics.Class_0_incorrectly_implements_class_1_Did_you_mean_to_extend_1_and_inherit_its_members_as_a_subclass : + Diagnostics.Class_0_incorrectly_implements_interface_1; + const baseWithThis = getTypeWithThisArgument(t, type.thisType); + if (!checkTypeAssignableTo(typeWithThis, baseWithThis, /*errorNode*/ undefined)) { + issueMemberSpecificError(node, typeWithThis, baseWithThis, genericDiag); + } + } + else { + error(typeRefNode, Diagnostics.A_class_can_only_implement_an_object_type_or_intersection_of_object_types_with_statically_known_members); + } } - return links.resolvedSymbol; } } - else if (isTypeReferenceIdentifier(name)) { - const meaning = name.parent.kind === SyntaxKind.TypeReference ? SymbolFlags.Type : SymbolFlags.Namespace; - return resolveEntityName(name, meaning, /*ignoreErrors*/ false, /*dontResolveAlias*/ true); - } - - if (name.parent.kind === SyntaxKind.TypePredicate) { - return resolveEntityName(name, /*meaning*/ SymbolFlags.FunctionScopedVariable); - } - - // Do we want to return undefined here? - return undefined; } - - function getSymbolAtLocation(node: Node, ignoreErrors?: boolean): Symbol | undefined { - if (node.kind === SyntaxKind.SourceFile) { - return isExternalModule(node) ? getMergedSymbol(node.symbol) : undefined; - } - const { parent } = node; - const grandParent = parent.parent; - - if (node.flags & NodeFlags.InWithStatement) { - // We cannot answer semantic questions within a with block, do not proceed any further - return undefined; + if (produceDiagnostics) { + checkIndexConstraints(type); + checkTypeForDuplicateIndexSignatures(node); + checkPropertyInitialization(node); + } + } + function issueMemberSpecificError(node: ClassLikeDeclaration, typeWithThis: ts.Type, baseWithThis: ts.Type, broadDiag: DiagnosticMessage) { + // iterate over all implemented properties and issue errors on each one which isn't compatible, rather than the class as a whole, if possible + let issuedMemberError = false; + for (const member of node.members) { + if (hasStaticModifier(member)) { + continue; } - - if (isDeclarationNameOrImportPropertyName(node)) { - // This is a declaration, call getSymbolOfNode - const parentSymbol = getSymbolOfNode(parent)!; - return isImportOrExportSpecifier(node.parent) && node.parent.propertyName === node - ? getImmediateAliasedSymbol(parentSymbol) - : parentSymbol; + const declaredProp = member.name && getSymbolAtLocation(member.name) || getSymbolAtLocation(member); + if (declaredProp) { + const prop = getPropertyOfType(typeWithThis, declaredProp.escapedName); + const baseProp = getPropertyOfType(baseWithThis, declaredProp.escapedName); + if (prop && baseProp) { + const rootChain = () => chainDiagnosticMessages( + /*details*/ undefined, Diagnostics.Property_0_in_type_1_is_not_assignable_to_the_same_property_in_base_type_2, symbolToString(declaredProp), typeToString(typeWithThis), typeToString(baseWithThis)); + if (!checkTypeAssignableTo(getTypeOfSymbol(prop), getTypeOfSymbol(baseProp), member.name || member, /*message*/ undefined, rootChain)) { + issuedMemberError = true; + } + } } - else if (isLiteralComputedPropertyDeclarationName(node)) { - return getSymbolOfNode(parent.parent); + } + if (!issuedMemberError) { + // check again with diagnostics to generate a less-specific error + checkTypeAssignableTo(typeWithThis, baseWithThis, node.name || node, broadDiag); + } + } + function checkBaseTypeAccessibility(type: ts.Type, node: ExpressionWithTypeArguments) { + const signatures = getSignaturesOfType(type, SignatureKind.Construct); + if (signatures.length) { + const declaration = signatures[0].declaration; + if (declaration && hasModifier(declaration, ModifierFlags.Private)) { + const typeClassDeclaration = (getClassLikeDeclarationOfSymbol(type.symbol)!); + if (!isNodeWithinClass(node, typeClassDeclaration)) { + error(node, Diagnostics.Cannot_extend_a_class_0_Class_constructor_is_marked_as_private, getFullyQualifiedName(type.symbol)); + } } - - if (node.kind === SyntaxKind.Identifier) { - if (isInRightSideOfImportOrExportAssignment(node)) { - return getSymbolOfNameOrPropertyAccessExpression(node); - } - else if (parent.kind === SyntaxKind.BindingElement && - grandParent.kind === SyntaxKind.ObjectBindingPattern && - node === (parent).propertyName) { - const typeOfPattern = getTypeOfNode(grandParent); - const propertyDeclaration = getPropertyOfType(typeOfPattern, (node).escapedText); - - if (propertyDeclaration) { - return propertyDeclaration; + } + } + function getTargetSymbol(s: ts.Symbol) { + // if symbol is instantiated its flags are not copied from the 'target' + // so we'll need to get back original 'target' symbol to work with correct set of flags + return getCheckFlags(s) & CheckFlags.Instantiated ? (s).target! : s; + } + function getClassOrInterfaceDeclarationsOfSymbol(symbol: ts.Symbol) { + return filter(symbol.declarations, (d: Declaration): d is ClassDeclaration | InterfaceDeclaration => d.kind === SyntaxKind.ClassDeclaration || d.kind === SyntaxKind.InterfaceDeclaration); + } + function checkKindsOfPropertyMemberOverrides(type: InterfaceType, baseType: BaseType): void { + // TypeScript 1.0 spec (April 2014): 8.2.3 + // A derived class inherits all members from its base class it doesn't override. + // Inheritance means that a derived class implicitly contains all non - overridden members of the base class. + // Both public and private property members are inherited, but only public property members can be overridden. + // A property member in a derived class is said to override a property member in a base class + // when the derived class property member has the same name and kind(instance or static) + // as the base class property member. + // The type of an overriding property member must be assignable(section 3.8.4) + // to the type of the overridden property member, or otherwise a compile - time error occurs. + // Base class instance member functions can be overridden by derived class instance member functions, + // but not by other kinds of members. + // Base class instance member variables and accessors can be overridden by + // derived class instance member variables and accessors, but not by other kinds of members. + // NOTE: assignability is checked in checkClassDeclaration + const baseProperties = getPropertiesOfType(baseType); + basePropertyCheck: for (const baseProperty of baseProperties) { + const base = getTargetSymbol(baseProperty); + if (base.flags & SymbolFlags.Prototype) { + continue; + } + const baseSymbol = getPropertyOfObjectType(type, base.escapedName); + if (!baseSymbol) { + continue; + } + const derived = getTargetSymbol(baseSymbol); + const baseDeclarationFlags = getDeclarationModifierFlagsFromSymbol(base); + Debug.assert(!!derived, "derived should point to something, even if it is the base class' declaration."); + // In order to resolve whether the inherited method was overridden in the base class or not, + // we compare the Symbols obtained. Since getTargetSymbol returns the symbol on the *uninstantiated* + // type declaration, derived and base resolve to the same symbol even in the case of generic classes. + if (derived === base) { + // derived class inherits base without override/redeclaration + const derivedClassDecl = (getClassLikeDeclarationOfSymbol(type.symbol)!); + // It is an error to inherit an abstract member without implementing it or being declared abstract. + // If there is no declaration for the derived class (as in the case of class expressions), + // then the class cannot be declared abstract. + if (baseDeclarationFlags & ModifierFlags.Abstract && (!derivedClassDecl || !hasModifier(derivedClassDecl, ModifierFlags.Abstract))) { + // Searches other base types for a declaration that would satisfy the inherited abstract member. + // (The class may have more than one base type via declaration merging with an interface with the + // same name.) + for (const otherBaseType of getBaseTypes(type)) { + if (otherBaseType === baseType) + continue; + const baseSymbol = getPropertyOfObjectType(otherBaseType, base.escapedName); + const derivedElsewhere = baseSymbol && getTargetSymbol(baseSymbol); + if (derivedElsewhere && derivedElsewhere !== base) { + continue basePropertyCheck; + } + } + if (derivedClassDecl.kind === SyntaxKind.ClassExpression) { + error(derivedClassDecl, Diagnostics.Non_abstract_class_expression_does_not_implement_inherited_abstract_member_0_from_class_1, symbolToString(baseProperty), typeToString(baseType)); + } + else { + error(derivedClassDecl, Diagnostics.Non_abstract_class_0_does_not_implement_inherited_abstract_member_1_from_class_2, typeToString(type), symbolToString(baseProperty), typeToString(baseType)); } } } - - switch (node.kind) { - case SyntaxKind.Identifier: - case SyntaxKind.PrivateIdentifier: - case SyntaxKind.PropertyAccessExpression: - case SyntaxKind.QualifiedName: - return getSymbolOfNameOrPropertyAccessExpression(node); - - case SyntaxKind.ThisKeyword: - const container = getThisContainer(node, /*includeArrowFunctions*/ false); - if (isFunctionLike(container)) { - const sig = getSignatureFromDeclaration(container); - if (sig.thisParameter) { - return sig.thisParameter; - } + else { + // derived overrides base. + const derivedDeclarationFlags = getDeclarationModifierFlagsFromSymbol(derived); + if (baseDeclarationFlags & ModifierFlags.Private || derivedDeclarationFlags & ModifierFlags.Private) { + // either base or derived property is private - not override, skip it + continue; + } + let errorMessage: DiagnosticMessage; + const basePropertyFlags = base.flags & SymbolFlags.PropertyOrAccessor; + const derivedPropertyFlags = derived.flags & SymbolFlags.PropertyOrAccessor; + if (basePropertyFlags && derivedPropertyFlags) { + // property/accessor is overridden with property/accessor + if (!compilerOptions.useDefineForClassFields + || baseDeclarationFlags & ModifierFlags.Abstract && !(base.valueDeclaration && isPropertyDeclaration(base.valueDeclaration) && base.valueDeclaration.initializer) + || base.valueDeclaration && base.valueDeclaration.parent.kind === SyntaxKind.InterfaceDeclaration + || derived.valueDeclaration && isBinaryExpression(derived.valueDeclaration)) { + // when the base property is abstract or from an interface, base/derived flags don't need to match + // same when the derived property is from an assignment + continue; } - if (isInExpressionContext(node)) { - return checkExpression(node as Expression).symbol; + const overriddenInstanceProperty = basePropertyFlags !== SymbolFlags.Property && derivedPropertyFlags === SymbolFlags.Property; + const overriddenInstanceAccessor = basePropertyFlags === SymbolFlags.Property && derivedPropertyFlags !== SymbolFlags.Property; + if (overriddenInstanceProperty || overriddenInstanceAccessor) { + const errorMessage = overriddenInstanceProperty ? + Diagnostics._0_is_defined_as_an_accessor_in_class_1_but_is_overridden_here_in_2_as_an_instance_property : + Diagnostics._0_is_defined_as_a_property_in_class_1_but_is_overridden_here_in_2_as_an_accessor; + error(getNameOfDeclaration(derived.valueDeclaration) || derived.valueDeclaration, errorMessage, symbolToString(base), typeToString(baseType), typeToString(type)); } - // falls through - - case SyntaxKind.ThisType: - return getTypeFromThisTypeNode(node as ThisExpression | ThisTypeNode).symbol; - - case SyntaxKind.SuperKeyword: - return checkExpression(node as Expression).symbol; - - case SyntaxKind.ConstructorKeyword: - // constructor keyword for an overload, should take us to the definition if it exist - const constructorDeclaration = node.parent; - if (constructorDeclaration && constructorDeclaration.kind === SyntaxKind.Constructor) { - return (constructorDeclaration.parent).symbol; + else { + const uninitialized = find(derived.declarations, d => d.kind === SyntaxKind.PropertyDeclaration && !(d as PropertyDeclaration).initializer); + if (uninitialized + && !(derived.flags & SymbolFlags.Transient) + && !(baseDeclarationFlags & ModifierFlags.Abstract) + && !(derivedDeclarationFlags & ModifierFlags.Abstract) + && !derived.declarations.some(d => !!(d.flags & NodeFlags.Ambient))) { + const constructor = findConstructorDeclaration((getClassLikeDeclarationOfSymbol(type.symbol)!)); + const propName = (uninitialized as PropertyDeclaration).name; + if ((uninitialized as PropertyDeclaration).exclamationToken + || !constructor + || !isIdentifier(propName) + || !strictNullChecks + || !isPropertyInitializedInConstructor(propName, type, constructor)) { + const errorMessage = Diagnostics.Property_0_will_overwrite_the_base_property_in_1_If_this_is_intentional_add_an_initializer_Otherwise_add_a_declare_modifier_or_remove_the_redundant_declaration; + error(getNameOfDeclaration(derived.valueDeclaration) || derived.valueDeclaration, errorMessage, symbolToString(base), typeToString(baseType)); + } + } } - return undefined; - - case SyntaxKind.StringLiteral: - case SyntaxKind.NoSubstitutionTemplateLiteral: - // 1). import x = require("./mo/*gotToDefinitionHere*/d") - // 2). External module name in an import declaration - // 3). Dynamic import call or require in javascript - // 4). type A = import("./f/*gotToDefinitionHere*/oo") - if ((isExternalModuleImportEqualsDeclaration(node.parent.parent) && getExternalModuleImportEqualsDeclarationExpression(node.parent.parent) === node) || - ((node.parent.kind === SyntaxKind.ImportDeclaration || node.parent.kind === SyntaxKind.ExportDeclaration) && (node.parent).moduleSpecifier === node) || - ((isInJSFile(node) && isRequireCall(node.parent, /*checkArgumentIsStringLiteralLike*/ false)) || isImportCall(node.parent)) || - (isLiteralTypeNode(node.parent) && isLiteralImportTypeNode(node.parent.parent) && node.parent.parent.argument === node.parent) - ) { - return resolveExternalModuleName(node, node, ignoreErrors); - } - if (isCallExpression(parent) && isBindableObjectDefinePropertyCall(parent) && parent.arguments[1] === node) { - return getSymbolOfNode(parent); + // correct case + continue; + } + else if (isPrototypeProperty(base)) { + if (isPrototypeProperty(derived) || derived.flags & SymbolFlags.Property) { + // method is overridden with method or property -- correct case + continue; } - // falls through - - case SyntaxKind.NumericLiteral: - // index access - const objectType = isElementAccessExpression(parent) - ? parent.argumentExpression === node ? getTypeOfExpression(parent.expression) : undefined - : isLiteralTypeNode(parent) && isIndexedAccessTypeNode(grandParent) - ? getTypeFromTypeNode(grandParent.objectType) - : undefined; - return objectType && getPropertyOfType(objectType, escapeLeadingUnderscores((node as StringLiteral | NumericLiteral).text)); - - case SyntaxKind.DefaultKeyword: - case SyntaxKind.FunctionKeyword: - case SyntaxKind.EqualsGreaterThanToken: - case SyntaxKind.ClassKeyword: - return getSymbolOfNode(node.parent); - case SyntaxKind.ImportType: - return isLiteralImportTypeNode(node) ? getSymbolAtLocation(node.argument.literal, ignoreErrors) : undefined; - - case SyntaxKind.ExportKeyword: - return isExportAssignment(node.parent) ? Debug.checkDefined(node.parent.symbol) : undefined; - - default: - return undefined; + else { + Debug.assert(!!(derived.flags & SymbolFlags.Accessor)); + errorMessage = Diagnostics.Class_0_defines_instance_member_function_1_but_extended_class_2_defines_it_as_instance_member_accessor; + } + } + else if (base.flags & SymbolFlags.Accessor) { + errorMessage = Diagnostics.Class_0_defines_instance_member_accessor_1_but_extended_class_2_defines_it_as_instance_member_function; + } + else { + errorMessage = Diagnostics.Class_0_defines_instance_member_property_1_but_extended_class_2_defines_it_as_instance_member_function; + } + error(getNameOfDeclaration(derived.valueDeclaration) || derived.valueDeclaration, errorMessage, typeToString(baseType), symbolToString(base), typeToString(type)); } } - - function getShorthandAssignmentValueSymbol(location: Node): Symbol | undefined { - if (location && location.kind === SyntaxKind.ShorthandPropertyAssignment) { - return resolveEntityName((location).name, SymbolFlags.Value | SymbolFlags.Alias); - } - return undefined; + } + function checkInheritedPropertiesAreIdentical(type: InterfaceType, typeNode: Node): boolean { + const baseTypes = getBaseTypes(type); + if (baseTypes.length < 2) { + return true; } - - /** Returns the target of an export specifier without following aliases */ - function getExportSpecifierLocalTargetSymbol(node: ExportSpecifier): Symbol | undefined { - return node.parent.parent.moduleSpecifier ? - getExternalModuleMember(node.parent.parent, node) : - resolveEntityName(node.propertyName || node.name, SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace | SymbolFlags.Alias); + interface InheritanceInfoMap { + prop: ts.Symbol; + containingType: ts.Type; } - - function getTypeOfNode(node: Node): Type { - if (node.flags & NodeFlags.InWithStatement) { - // We cannot answer semantic questions within a with block, do not proceed any further - return errorType; - } - - const classDecl = tryGetClassImplementingOrExtendingExpressionWithTypeArguments(node); - const classType = classDecl && getDeclaredTypeOfClassOrInterface(getSymbolOfNode(classDecl.class)); - if (isPartOfTypeNode(node)) { - const typeFromTypeNode = getTypeFromTypeNode(node); - return classType ? getTypeWithThisArgument(typeFromTypeNode, classType.thisType) : typeFromTypeNode; - } - - if (isExpressionNode(node)) { - return getRegularTypeOfExpression(node); - } - - if (classType && !classDecl!.isImplements) { - // A SyntaxKind.ExpressionWithTypeArguments is considered a type node, except when it occurs in the - // extends clause of a class. We handle that case here. - const baseType = firstOrUndefined(getBaseTypes(classType)); - return baseType ? getTypeWithThisArgument(baseType, classType.thisType) : errorType; - } - - if (isTypeDeclaration(node)) { - // In this case, we call getSymbolOfNode instead of getSymbolAtLocation because it is a declaration - const symbol = getSymbolOfNode(node); - return getDeclaredTypeOfSymbol(symbol); - } - - if (isTypeDeclarationName(node)) { - const symbol = getSymbolAtLocation(node); - return symbol ? getDeclaredTypeOfSymbol(symbol) : errorType; + const seen = createUnderscoreEscapedMap(); + forEach(resolveDeclaredMembers(type).declaredProperties, p => { seen.set(p.escapedName, { prop: p, containingType: type }); }); + let ok = true; + for (const base of baseTypes) { + const properties = getPropertiesOfType(getTypeWithThisArgument(base, type.thisType)); + for (const prop of properties) { + const existing = seen.get(prop.escapedName); + if (!existing) { + seen.set(prop.escapedName, { prop, containingType: base }); + } + else { + const isInheritedProperty = existing.containingType !== type; + if (isInheritedProperty && !isPropertyIdenticalTo(existing.prop, prop)) { + ok = false; + const typeName1 = typeToString(existing.containingType); + const typeName2 = typeToString(base); + let errorInfo = chainDiagnosticMessages(/*details*/ undefined, Diagnostics.Named_property_0_of_types_1_and_2_are_not_identical, symbolToString(prop), typeName1, typeName2); + errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Interface_0_cannot_simultaneously_extend_types_1_and_2, typeToString(type), typeName1, typeName2); + diagnostics.add(createDiagnosticForNodeFromMessageChain(typeNode, errorInfo)); + } + } } - - if (isDeclaration(node)) { - // In this case, we call getSymbolOfNode instead of getSymbolAtLocation because it is a declaration - const symbol = getSymbolOfNode(node); - return getTypeOfSymbol(symbol); + } + return ok; + } + function checkPropertyInitialization(node: ClassLikeDeclaration) { + if (!strictNullChecks || !strictPropertyInitialization || node.flags & NodeFlags.Ambient) { + return; + } + const constructor = findConstructorDeclaration(node); + for (const member of node.members) { + if (getModifierFlags(member) & ModifierFlags.Ambient) { + continue; } - - if (isDeclarationNameOrImportPropertyName(node)) { - const symbol = getSymbolAtLocation(node); - if (symbol) { - return getTypeOfSymbol(symbol); + if (isInstancePropertyWithoutInitializer(member)) { + const propName = (member).name; + if (isIdentifier(propName) || isPrivateIdentifier(propName)) { + const type = getTypeOfSymbol(getSymbolOfNode(member)); + if (!(type.flags & TypeFlags.AnyOrUnknown || getFalsyFlags(type) & TypeFlags.Undefined)) { + if (!constructor || !isPropertyInitializedInConstructor(propName, type, constructor)) { + error(member.name, Diagnostics.Property_0_has_no_initializer_and_is_not_definitely_assigned_in_the_constructor, declarationNameToString(propName)); + } + } } - return errorType; } - - if (isBindingPattern(node)) { - return getTypeForVariableLikeDeclaration(node.parent, /*includeOptionality*/ true) || errorType; - } - - if (isInRightSideOfImportOrExportAssignment(node)) { - const symbol = getSymbolAtLocation(node); - if (symbol) { - const declaredType = getDeclaredTypeOfSymbol(symbol); - return declaredType !== errorType ? declaredType : getTypeOfSymbol(symbol); + } + } + function isInstancePropertyWithoutInitializer(node: Node) { + return node.kind === SyntaxKind.PropertyDeclaration && + !hasModifier(node, ModifierFlags.Static | ModifierFlags.Abstract) && + !(node).exclamationToken && + !(node).initializer; + } + function isPropertyInitializedInConstructor(propName: Identifier | PrivateIdentifier, propType: ts.Type, constructor: ConstructorDeclaration) { + const reference = createPropertyAccess(createThis(), propName); + reference.expression.parent = reference; + reference.parent = constructor; + reference.flowNode = constructor.returnFlowNode; + const flowType = getFlowTypeOfReference(reference, propType, getOptionalType(propType)); + return !(getFalsyFlags(flowType) & TypeFlags.Undefined); + } + function checkInterfaceDeclaration(node: InterfaceDeclaration) { + // Grammar checking + if (!checkGrammarDecoratorsAndModifiers(node)) + checkGrammarInterfaceDeclaration(node); + checkTypeParameters(node.typeParameters); + if (produceDiagnostics) { + checkTypeNameIsReserved(node.name, Diagnostics.Interface_name_cannot_be_0); + checkExportsOnMergedDeclarations(node); + const symbol = getSymbolOfNode(node); + checkTypeParameterListsIdentical(symbol); + // Only check this symbol once + const firstInterfaceDecl = getDeclarationOfKind(symbol, SyntaxKind.InterfaceDeclaration); + if (node === firstInterfaceDecl) { + const type = (getDeclaredTypeOfSymbol(symbol)); + const typeWithThis = getTypeWithThisArgument(type); + // run subsequent checks only if first set succeeded + if (checkInheritedPropertiesAreIdentical(type, node.name)) { + for (const baseType of getBaseTypes(type)) { + checkTypeAssignableTo(typeWithThis, getTypeWithThisArgument(baseType, type.thisType), node.name, Diagnostics.Interface_0_incorrectly_extends_interface_1); + } + checkIndexConstraints(type); } } - - return errorType; + checkObjectTypeForDuplicateDeclarations(node); } - - // Gets the type of object literal or array literal of destructuring assignment. - // { a } from - // for ( { a } of elems) { - // } - // [ a ] from - // [a] = [ some array ...] - function getTypeOfAssignmentPattern(expr: AssignmentPattern): Type | undefined { - Debug.assert(expr.kind === SyntaxKind.ObjectLiteralExpression || expr.kind === SyntaxKind.ArrayLiteralExpression); - // If this is from "for of" - // for ( { a } of elems) { - // } - if (expr.parent.kind === SyntaxKind.ForOfStatement) { - const iteratedType = checkRightHandSideOfForOf((expr.parent).expression, (expr.parent).awaitModifier); - return checkDestructuringAssignment(expr, iteratedType || errorType); - } - // If this is from "for" initializer - // for ({a } = elems[0];.....) { } - if (expr.parent.kind === SyntaxKind.BinaryExpression) { - const iteratedType = getTypeOfExpression((expr.parent).right); - return checkDestructuringAssignment(expr, iteratedType || errorType); - } - // If this is from nested object binding pattern - // for ({ skills: { primary, secondary } } = multiRobot, i = 0; i < 1; i++) { - if (expr.parent.kind === SyntaxKind.PropertyAssignment) { - const node = cast(expr.parent.parent, isObjectLiteralExpression); - const typeOfParentObjectLiteral = getTypeOfAssignmentPattern(node) || errorType; - const propertyIndex = indexOfNode(node.properties, expr.parent); - return checkObjectLiteralDestructuringPropertyAssignment(node, typeOfParentObjectLiteral, propertyIndex); - } - // Array literal assignment - array destructuring pattern - const node = cast(expr.parent, isArrayLiteralExpression); - // [{ property1: p1, property2 }] = elems; - const typeOfArrayLiteral = getTypeOfAssignmentPattern(node) || errorType; - const elementType = checkIteratedTypeOrElementType(IterationUse.Destructuring, typeOfArrayLiteral, undefinedType, expr.parent) || errorType; - return checkArrayLiteralDestructuringElementAssignment(node, typeOfArrayLiteral, node.elements.indexOf(expr), elementType); + forEach(getInterfaceBaseTypeNodes(node), heritageElement => { + if (!isEntityNameExpression(heritageElement.expression)) { + error(heritageElement.expression, Diagnostics.An_interface_can_only_extend_an_identifier_Slashqualified_name_with_optional_type_arguments); + } + checkTypeReferenceNode(heritageElement); + }); + forEach(node.members, checkSourceElement); + if (produceDiagnostics) { + checkTypeForDuplicateIndexSignatures(node); + registerForUnusedIdentifiersCheck(node); } - - // Gets the property symbol corresponding to the property in destructuring assignment - // 'property1' from - // for ( { property1: a } of elems) { - // } - // 'property1' at location 'a' from: - // [a] = [ property1, property2 ] - function getPropertySymbolOfDestructuringAssignment(location: Identifier) { - // Get the type of the object or array literal and then look for property of given name in the type - const typeOfObjectLiteral = getTypeOfAssignmentPattern(cast(location.parent.parent, isAssignmentPattern)); - return typeOfObjectLiteral && getPropertyOfType(typeOfObjectLiteral, location.escapedText); + } + function checkTypeAliasDeclaration(node: TypeAliasDeclaration) { + // Grammar checking + checkGrammarDecoratorsAndModifiers(node); + checkTypeNameIsReserved(node.name, Diagnostics.Type_alias_name_cannot_be_0); + checkExportsOnMergedDeclarations(node); + checkTypeParameters(node.typeParameters); + checkSourceElement(node.type); + registerForUnusedIdentifiersCheck(node); + } + function computeEnumMemberValues(node: EnumDeclaration) { + const nodeLinks = getNodeLinks(node); + if (!(nodeLinks.flags & NodeCheckFlags.EnumValuesComputed)) { + nodeLinks.flags |= NodeCheckFlags.EnumValuesComputed; + let autoValue: number | undefined = 0; + for (const member of node.members) { + const value = computeMemberValue(member, autoValue); + getNodeLinks(member).enumMemberValue = value; + autoValue = typeof value === "number" ? value + 1 : undefined; + } } - - function getRegularTypeOfExpression(expr: Expression): Type { - if (isRightSideOfQualifiedNameOrPropertyAccess(expr)) { - expr = expr.parent; + } + function computeMemberValue(member: EnumMember, autoValue: number | undefined) { + if (isComputedNonLiteralName(member.name)) { + error(member.name, Diagnostics.Computed_property_names_are_not_allowed_in_enums); + } + else { + const text = getTextOfPropertyName(member.name); + if (isNumericLiteralName(text) && !isInfinityOrNaNString(text)) { + error(member.name, Diagnostics.An_enum_member_cannot_have_a_numeric_name); } - return getRegularTypeOfLiteralType(getTypeOfExpression(expr)); } - - /** - * Gets either the static or instance type of a class element, based on - * whether the element is declared as "static". - */ - function getParentTypeOfClassElement(node: ClassElement) { - const classSymbol = getSymbolOfNode(node.parent)!; - return hasModifier(node, ModifierFlags.Static) - ? getTypeOfSymbol(classSymbol) - : getDeclaredTypeOfSymbol(classSymbol); + if (member.initializer) { + return computeConstantValue(member); } - - function getClassElementPropertyKeyType(element: ClassElement) { - const name = element.name!; - switch (name.kind) { - case SyntaxKind.Identifier: - return getLiteralType(idText(name)); - case SyntaxKind.NumericLiteral: + // In ambient non-const numeric enum declarations, enum members without initializers are + // considered computed members (as opposed to having auto-incremented values). + if (member.parent.flags & NodeFlags.Ambient && !isEnumConst(member.parent) && getEnumKind(getSymbolOfNode(member.parent)) === EnumKind.Numeric) { + return undefined; + } + // If the member declaration specifies no value, the member is considered a constant enum member. + // If the member is the first member in the enum declaration, it is assigned the value zero. + // Otherwise, it is assigned the value of the immediately preceding member plus one, and an error + // occurs if the immediately preceding member is not a constant enum member. + if (autoValue !== undefined) { + return autoValue; + } + error(member.name, Diagnostics.Enum_member_must_have_initializer); + return undefined; + } + function computeConstantValue(member: EnumMember): string | number | undefined { + const enumKind = getEnumKind(getSymbolOfNode(member.parent)); + const isConstEnum = isEnumConst(member.parent); + const initializer = member.initializer!; + const value = enumKind === EnumKind.Literal && !isLiteralEnumMember(member) ? undefined : evaluate(initializer); + if (value !== undefined) { + if (isConstEnum && typeof value === "number" && !isFinite(value)) { + error(initializer, isNaN(value) ? + Diagnostics.const_enum_member_initializer_was_evaluated_to_disallowed_value_NaN : + Diagnostics.const_enum_member_initializer_was_evaluated_to_a_non_finite_value); + } + } + else if (enumKind === EnumKind.Literal) { + error(initializer, Diagnostics.Computed_values_are_not_permitted_in_an_enum_with_string_valued_members); + return 0; + } + else if (isConstEnum) { + error(initializer, Diagnostics.const_enum_member_initializers_can_only_contain_literal_values_and_other_computed_enum_values); + } + else if (member.parent.flags & NodeFlags.Ambient) { + error(initializer, Diagnostics.In_ambient_enum_declarations_member_initializer_must_be_constant_expression); + } + else { + // Only here do we need to check that the initializer is assignable to the enum type. + checkTypeAssignableTo(checkExpression(initializer), getDeclaredTypeOfSymbol(getSymbolOfNode(member.parent)), initializer, /*headMessage*/ undefined); + } + return value; + function evaluate(expr: Expression): string | number | undefined { + switch (expr.kind) { + case SyntaxKind.PrefixUnaryExpression: + const value = evaluate((expr).operand); + if (typeof value === "number") { + switch ((expr).operator) { + case SyntaxKind.PlusToken: return value; + case SyntaxKind.MinusToken: return -value; + case SyntaxKind.TildeToken: return ~value; + } + } + break; + case SyntaxKind.BinaryExpression: + const left = evaluate((expr).left); + const right = evaluate((expr).right); + if (typeof left === "number" && typeof right === "number") { + switch ((expr).operatorToken.kind) { + case SyntaxKind.BarToken: return left | right; + case SyntaxKind.AmpersandToken: return left & right; + case SyntaxKind.GreaterThanGreaterThanToken: return left >> right; + case SyntaxKind.GreaterThanGreaterThanGreaterThanToken: return left >>> right; + case SyntaxKind.LessThanLessThanToken: return left << right; + case SyntaxKind.CaretToken: return left ^ right; + case SyntaxKind.AsteriskToken: return left * right; + case SyntaxKind.SlashToken: return left / right; + case SyntaxKind.PlusToken: return left + right; + case SyntaxKind.MinusToken: return left - right; + case SyntaxKind.PercentToken: return left % right; + case SyntaxKind.AsteriskAsteriskToken: return left ** right; + } + } + else if (typeof left === "string" && typeof right === "string" && (expr).operatorToken.kind === SyntaxKind.PlusToken) { + return left + right; + } + break; case SyntaxKind.StringLiteral: - return getLiteralType(name.text); - case SyntaxKind.ComputedPropertyName: - const nameType = checkComputedPropertyName(name); - return isTypeAssignableToKind(nameType, TypeFlags.ESSymbolLike) ? nameType : stringType; - default: - return Debug.fail("Unsupported property name."); + case SyntaxKind.NoSubstitutionTemplateLiteral: + return (expr).text; + case SyntaxKind.NumericLiteral: + checkGrammarNumericLiteral((expr)); + return +(expr).text; + case SyntaxKind.ParenthesizedExpression: + return evaluate((expr).expression); + case SyntaxKind.Identifier: + const identifier = (expr); + if (isInfinityOrNaNString(identifier.escapedText)) { + return +(identifier.escapedText); + } + return nodeIsMissing(expr) ? 0 : evaluateEnumMember(expr, getSymbolOfNode(member.parent), identifier.escapedText); + case SyntaxKind.ElementAccessExpression: + case SyntaxKind.PropertyAccessExpression: + const ex = (expr); + if (isConstantMemberAccess(ex)) { + const type = getTypeOfExpression(ex.expression); + if (type.symbol && type.symbol.flags & SymbolFlags.Enum) { + let name: __String; + if (ex.kind === SyntaxKind.PropertyAccessExpression) { + name = ex.name.escapedText; + } + else { + name = escapeLeadingUnderscores(cast(ex.argumentExpression, isLiteralExpression).text); + } + return evaluateEnumMember(expr, type.symbol, name); + } + } + break; } + return undefined; } - - // Return the list of properties of the given type, augmented with properties from Function - // if the type has call or construct signatures - function getAugmentedPropertiesOfType(type: Type): Symbol[] { - type = getApparentType(type); - const propsByName = createSymbolTable(getPropertiesOfType(type)); - const functionType = getSignaturesOfType(type, SignatureKind.Call).length ? globalCallableFunctionType : - getSignaturesOfType(type, SignatureKind.Construct).length ? globalNewableFunctionType : - undefined; - if (functionType) { - forEach(getPropertiesOfType(functionType), p => { - if (!propsByName.has(p.escapedName)) { - propsByName.set(p.escapedName, p); + function evaluateEnumMember(expr: Expression, enumSymbol: ts.Symbol, name: __String) { + const memberSymbol = enumSymbol.exports!.get(name); + if (memberSymbol) { + const declaration = memberSymbol.valueDeclaration; + if (declaration !== member) { + if (isBlockScopedNameDeclaredBeforeUse(declaration, member)) { + return getEnumMemberValue((declaration as EnumMember)); + } + error(expr, Diagnostics.A_member_initializer_in_a_enum_declaration_cannot_reference_members_declared_after_it_including_members_defined_in_other_enums); + return 0; + } + } + return undefined; + } + } + function isConstantMemberAccess(node: Expression): boolean { + return node.kind === SyntaxKind.Identifier || + node.kind === SyntaxKind.PropertyAccessExpression && isConstantMemberAccess((node).expression) || + node.kind === SyntaxKind.ElementAccessExpression && isConstantMemberAccess((node).expression) && + isStringLiteralLike((node).argumentExpression); + } + function checkEnumDeclaration(node: EnumDeclaration) { + if (!produceDiagnostics) { + return; + } + // Grammar checking + checkGrammarDecoratorsAndModifiers(node); + checkTypeNameIsReserved(node.name, Diagnostics.Enum_name_cannot_be_0); + checkCollisionWithRequireExportsInGeneratedCode(node, node.name); + checkCollisionWithGlobalPromiseInGeneratedCode(node, node.name); + checkExportsOnMergedDeclarations(node); + node.members.forEach(checkEnumMember); + computeEnumMemberValues(node); + // Spec 2014 - Section 9.3: + // It isn't possible for one enum declaration to continue the automatic numbering sequence of another, + // and when an enum type has multiple declarations, only one declaration is permitted to omit a value + // for the first member. + // + // Only perform this check once per symbol + const enumSymbol = getSymbolOfNode(node); + const firstDeclaration = getDeclarationOfKind(enumSymbol, node.kind); + if (node === firstDeclaration) { + if (enumSymbol.declarations.length > 1) { + const enumIsConst = isEnumConst(node); + // check that const is placed\omitted on all enum declarations + forEach(enumSymbol.declarations, decl => { + if (isEnumDeclaration(decl) && isEnumConst(decl) !== enumIsConst) { + error(getNameOfDeclaration(decl), Diagnostics.Enum_declarations_must_all_be_const_or_non_const); + } + }); + } + let seenEnumMissingInitialInitializer = false; + forEach(enumSymbol.declarations, declaration => { + // return true if we hit a violation of the rule, false otherwise + if (declaration.kind !== SyntaxKind.EnumDeclaration) { + return false; + } + const enumDeclaration = (declaration); + if (!enumDeclaration.members.length) { + return false; + } + const firstEnumMember = enumDeclaration.members[0]; + if (!firstEnumMember.initializer) { + if (seenEnumMissingInitialInitializer) { + error(firstEnumMember.name, Diagnostics.In_an_enum_with_multiple_declarations_only_one_declaration_can_omit_an_initializer_for_its_first_enum_element); + } + else { + seenEnumMissingInitialInitializer = true; } - }); - } - return getNamedMembers(propsByName); + } + }); } - - function typeHasCallOrConstructSignatures(type: Type): boolean { - return ts.typeHasCallOrConstructSignatures(type, checker); + } + function checkEnumMember(node: EnumMember) { + if (isPrivateIdentifier(node.name)) { + error(node, Diagnostics.An_enum_member_cannot_be_named_with_a_private_identifier); } - - function getRootSymbols(symbol: Symbol): readonly Symbol[] { - const roots = getImmediateRootSymbols(symbol); - return roots ? flatMap(roots, getRootSymbols) : [symbol]; - } - function getImmediateRootSymbols(symbol: Symbol): readonly Symbol[] | undefined { - if (getCheckFlags(symbol) & CheckFlags.Synthetic) { - return mapDefined(getSymbolLinks(symbol).containingType!.types, type => getPropertyOfType(type, symbol.escapedName)); - } - else if (symbol.flags & SymbolFlags.Transient) { - const { leftSpread, rightSpread, syntheticOrigin } = symbol as TransientSymbol; - return leftSpread ? [leftSpread, rightSpread!] - : syntheticOrigin ? [syntheticOrigin] - : singleElementArray(tryGetAliasTarget(symbol)); + } + function getFirstNonAmbientClassOrFunctionDeclaration(symbol: ts.Symbol): Declaration | undefined { + const declarations = symbol.declarations; + for (const declaration of declarations) { + if ((declaration.kind === SyntaxKind.ClassDeclaration || + (declaration.kind === SyntaxKind.FunctionDeclaration && nodeIsPresent((declaration).body))) && + !(declaration.flags & NodeFlags.Ambient)) { + return declaration; } - return undefined; } - function tryGetAliasTarget(symbol: Symbol): Symbol | undefined { - let target: Symbol | undefined; - let next: Symbol | undefined = symbol; - while (next = getSymbolLinks(next).target) { - target = next; - } - return target; + return undefined; + } + function inSameLexicalScope(node1: Node, node2: Node) { + const container1 = getEnclosingBlockScopeContainer(node1); + const container2 = getEnclosingBlockScopeContainer(node2); + if (isGlobalSourceFile(container1)) { + return isGlobalSourceFile(container2); } - - // Emitter support - - function isArgumentsLocalBinding(nodeIn: Identifier): boolean { - if (!isGeneratedIdentifier(nodeIn)) { - const node = getParseTreeNode(nodeIn, isIdentifier); - if (node) { - const isPropertyName = node.parent.kind === SyntaxKind.PropertyAccessExpression && (node.parent).name === node; - return !isPropertyName && getReferencedValueSymbol(node) === argumentsSymbol; - } - } - + else if (isGlobalSourceFile(container2)) { return false; } - - function moduleExportsSomeValue(moduleReferenceExpression: Expression): boolean { - let moduleSymbol = resolveExternalModuleName(moduleReferenceExpression.parent, moduleReferenceExpression); - if (!moduleSymbol || isShorthandAmbientModuleSymbol(moduleSymbol)) { - // If the module is not found or is shorthand, assume that it may export a value. - return true; + else { + return container1 === container2; + } + } + function checkModuleDeclaration(node: ModuleDeclaration) { + if (produceDiagnostics) { + // Grammar checking + const isGlobalAugmentation = isGlobalScopeAugmentation(node); + const inAmbientContext = node.flags & NodeFlags.Ambient; + if (isGlobalAugmentation && !inAmbientContext) { + error(node.name, Diagnostics.Augmentations_for_the_global_scope_should_have_declare_modifier_unless_they_appear_in_already_ambient_context); + } + const isAmbientExternalModule = isAmbientModule(node); + const contextErrorMessage = isAmbientExternalModule + ? Diagnostics.An_ambient_module_declaration_is_only_allowed_at_the_top_level_in_a_file + : Diagnostics.A_namespace_declaration_is_only_allowed_in_a_namespace_or_module; + if (checkGrammarModuleElementContext(node, contextErrorMessage)) { + // If we hit a module declaration in an illegal context, just bail out to avoid cascading errors. + return; } - - const hasExportAssignment = hasExportAssignmentSymbol(moduleSymbol); - // if module has export assignment then 'resolveExternalModuleSymbol' will return resolved symbol for export assignment - // otherwise it will return moduleSymbol itself - moduleSymbol = resolveExternalModuleSymbol(moduleSymbol); - - const symbolLinks = getSymbolLinks(moduleSymbol); - if (symbolLinks.exportsSomeValue === undefined) { - // for export assignments - check if resolved symbol for RHS is itself a value - // otherwise - check if at least one export is value - symbolLinks.exportsSomeValue = hasExportAssignment - ? !!(moduleSymbol.flags & SymbolFlags.Value) - : forEachEntry(getExportsOfModule(moduleSymbol), isValue); + if (!checkGrammarDecoratorsAndModifiers(node)) { + if (!inAmbientContext && node.name.kind === SyntaxKind.StringLiteral) { + grammarErrorOnNode(node.name, Diagnostics.Only_ambient_modules_can_use_quoted_names); + } } - - return symbolLinks.exportsSomeValue!; - - function isValue(s: Symbol): boolean { - s = resolveSymbol(s); - return s && !!(s.flags & SymbolFlags.Value); + if (isIdentifier(node.name)) { + checkCollisionWithRequireExportsInGeneratedCode(node, node.name); + checkCollisionWithGlobalPromiseInGeneratedCode(node, node.name); } - } - - function isNameOfModuleOrEnumDeclaration(node: Identifier) { - return isModuleOrEnumDeclaration(node.parent) && node === node.parent.name; - } - - // When resolved as an expression identifier, if the given node references an exported entity, return the declaration - // node of the exported entity's container. Otherwise, return undefined. - function getReferencedExportContainer(nodeIn: Identifier, prefixLocals?: boolean): SourceFile | ModuleDeclaration | EnumDeclaration | undefined { - const node = getParseTreeNode(nodeIn, isIdentifier); - if (node) { - // When resolving the export container for the name of a module or enum - // declaration, we need to start resolution at the declaration's container. - // Otherwise, we could incorrectly resolve the export container as the - // declaration if it contains an exported member with the same name. - let symbol = getReferencedValueSymbol(node, /*startInDeclarationContainer*/ isNameOfModuleOrEnumDeclaration(node)); - if (symbol) { - if (symbol.flags & SymbolFlags.ExportValue) { - // If we reference an exported entity within the same module declaration, then whether - // we prefix depends on the kind of entity. SymbolFlags.ExportHasLocal encompasses all the - // kinds that we do NOT prefix. - const exportSymbol = getMergedSymbol(symbol.exportSymbol!); - if (!prefixLocals && exportSymbol.flags & SymbolFlags.ExportHasLocal && !(exportSymbol.flags & SymbolFlags.Variable)) { - return undefined; - } - symbol = exportSymbol; - } - const parentSymbol = getParentOfSymbol(symbol); - if (parentSymbol) { - if (parentSymbol.flags & SymbolFlags.ValueModule && parentSymbol.valueDeclaration.kind === SyntaxKind.SourceFile) { - const symbolFile = parentSymbol.valueDeclaration; - const referenceFile = getSourceFileOfNode(node); - // If `node` accesses an export and that export isn't in the same file, then symbol is a namespace export, so return undefined. - const symbolIsUmdExport = symbolFile !== referenceFile; - return symbolIsUmdExport ? undefined : symbolFile; - } - return findAncestor(node.parent, (n): n is ModuleDeclaration | EnumDeclaration => isModuleOrEnumDeclaration(n) && getSymbolOfNode(n) === parentSymbol); + checkExportsOnMergedDeclarations(node); + const symbol = getSymbolOfNode(node); + // The following checks only apply on a non-ambient instantiated module declaration. + if (symbol.flags & SymbolFlags.ValueModule + && !inAmbientContext + && symbol.declarations.length > 1 + && isInstantiatedModule(node, !!compilerOptions.preserveConstEnums || !!compilerOptions.isolatedModules)) { + const firstNonAmbientClassOrFunc = getFirstNonAmbientClassOrFunctionDeclaration(symbol); + if (firstNonAmbientClassOrFunc) { + if (getSourceFileOfNode(node) !== getSourceFileOfNode(firstNonAmbientClassOrFunc)) { + error(node.name, Diagnostics.A_namespace_declaration_cannot_be_in_a_different_file_from_a_class_or_function_with_which_it_is_merged); + } + else if (node.pos < firstNonAmbientClassOrFunc.pos) { + error(node.name, Diagnostics.A_namespace_declaration_cannot_be_located_prior_to_a_class_or_function_with_which_it_is_merged); + } + } + // if the module merges with a class declaration in the same lexical scope, + // we need to track this to ensure the correct emit. + const mergedClass = getDeclarationOfKind(symbol, SyntaxKind.ClassDeclaration); + if (mergedClass && + inSameLexicalScope(node, mergedClass)) { + getNodeLinks(node).flags |= NodeCheckFlags.LexicalModuleMergesWithClass; + } + } + if (isAmbientExternalModule) { + if (isExternalModuleAugmentation(node)) { + // body of the augmentation should be checked for consistency only if augmentation was applied to its target (either global scope or module) + // otherwise we'll be swamped in cascading errors. + // We can detect if augmentation was applied using following rules: + // - augmentation for a global scope is always applied + // - augmentation for some external module is applied if symbol for augmentation is merged (it was combined with target module). + const checkBody = isGlobalAugmentation || (getSymbolOfNode(node).flags & SymbolFlags.Transient); + if (checkBody && node.body) { + for (const statement of node.body.statements) { + checkModuleAugmentationElement(statement, isGlobalAugmentation); + } + } + } + else if (isGlobalSourceFile(node.parent)) { + if (isGlobalAugmentation) { + error(node.name, Diagnostics.Augmentations_for_the_global_scope_can_only_be_directly_nested_in_external_modules_or_ambient_module_declarations); + } + else if (isExternalModuleNameRelative(getTextOfIdentifierOrLiteral(node.name))) { + error(node.name, Diagnostics.Ambient_module_declaration_cannot_specify_relative_module_name); } } - } - } - - // When resolved as an expression identifier, if the given node references an import, return the declaration of - // that import. Otherwise, return undefined. - function getReferencedImportDeclaration(nodeIn: Identifier): Declaration | undefined { - const node = getParseTreeNode(nodeIn, isIdentifier); - if (node) { - const symbol = getReferencedValueSymbol(node); - // We should only get the declaration of an alias if there isn't a local value - // declaration for the symbol - if (isNonLocalAlias(symbol, /*excludes*/ SymbolFlags.Value) && !getTypeOnlyAliasDeclaration(symbol)) { - return getDeclarationOfAliasSymbol(symbol); + else { + if (isGlobalAugmentation) { + error(node.name, Diagnostics.Augmentations_for_the_global_scope_can_only_be_directly_nested_in_external_modules_or_ambient_module_declarations); + } + else { + // Node is not an augmentation and is not located on the script level. + // This means that this is declaration of ambient module that is located in other module or namespace which is prohibited. + error(node.name, Diagnostics.Ambient_modules_cannot_be_nested_in_other_modules_or_namespaces); + } } } - - return undefined; - } - - function isSymbolOfDestructuredElementOfCatchBinding(symbol: Symbol) { - return isBindingElement(symbol.valueDeclaration) - && walkUpBindingElementsAndPatterns(symbol.valueDeclaration).parent.kind === SyntaxKind.CatchClause; } - - function isSymbolOfDeclarationWithCollidingName(symbol: Symbol): boolean { - if (symbol.flags & SymbolFlags.BlockScoped && !isSourceFile(symbol.valueDeclaration)) { - const links = getSymbolLinks(symbol); - if (links.isDeclarationWithCollidingName === undefined) { - const container = getEnclosingBlockScopeContainer(symbol.valueDeclaration); - if (isStatementWithLocals(container) || isSymbolOfDestructuredElementOfCatchBinding(symbol)) { - const nodeLinks = getNodeLinks(symbol.valueDeclaration); - if (resolveName(container.parent, symbol.escapedName, SymbolFlags.Value, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false)) { - // redeclaration - always should be renamed - links.isDeclarationWithCollidingName = true; - } - else if (nodeLinks.flags & NodeCheckFlags.CapturedBlockScopedBinding) { - // binding is captured in the function - // should be renamed if: - // - binding is not top level - top level bindings never collide with anything - // AND - // - binding is not declared in loop, should be renamed to avoid name reuse across siblings - // let a, b - // { let x = 1; a = () => x; } - // { let x = 100; b = () => x; } - // console.log(a()); // should print '1' - // console.log(b()); // should print '100' - // OR - // - binding is declared inside loop but not in inside initializer of iteration statement or directly inside loop body - // * variables from initializer are passed to rewritten loop body as parameters so they are not captured directly - // * variables that are declared immediately in loop body will become top level variable after loop is rewritten and thus - // they will not collide with anything - const isDeclaredInLoop = nodeLinks.flags & NodeCheckFlags.BlockScopedBindingInLoop; - const inLoopInitializer = isIterationStatement(container, /*lookInLabeledStatements*/ false); - const inLoopBodyBlock = container.kind === SyntaxKind.Block && isIterationStatement(container.parent, /*lookInLabeledStatements*/ false); - - links.isDeclarationWithCollidingName = !isBlockScopedContainerTopLevel(container) && (!isDeclaredInLoop || (!inLoopInitializer && !inLoopBodyBlock)); - } - else { - links.isDeclarationWithCollidingName = false; - } - } - } - return links.isDeclarationWithCollidingName!; + if (node.body) { + checkSourceElement(node.body); + if (!isGlobalScopeAugmentation(node)) { + registerForUnusedIdentifiersCheck(node); } - return false; } - - // When resolved as an expression identifier, if the given node references a nested block scoped entity with - // a name that either hides an existing name or might hide it when compiled downlevel, - // return the declaration of that entity. Otherwise, return undefined. - function getReferencedDeclarationWithCollidingName(nodeIn: Identifier): Declaration | undefined { - if (!isGeneratedIdentifier(nodeIn)) { - const node = getParseTreeNode(nodeIn, isIdentifier); - if (node) { - const symbol = getReferencedValueSymbol(node); - if (symbol && isSymbolOfDeclarationWithCollidingName(symbol)) { - return symbol.valueDeclaration; + } + function checkModuleAugmentationElement(node: Node, isGlobalAugmentation: boolean): void { + switch (node.kind) { + case SyntaxKind.VariableStatement: + // error each individual name in variable statement instead of marking the entire variable statement + for (const decl of (node).declarationList.declarations) { + checkModuleAugmentationElement(decl, isGlobalAugmentation); + } + break; + case SyntaxKind.ExportAssignment: + case SyntaxKind.ExportDeclaration: + grammarErrorOnFirstToken(node, Diagnostics.Exports_and_export_assignments_are_not_permitted_in_module_augmentations); + break; + case SyntaxKind.ImportEqualsDeclaration: + case SyntaxKind.ImportDeclaration: + grammarErrorOnFirstToken(node, Diagnostics.Imports_are_not_permitted_in_module_augmentations_Consider_moving_them_to_the_enclosing_external_module); + break; + case SyntaxKind.BindingElement: + case SyntaxKind.VariableDeclaration: + const name = (node).name; + if (isBindingPattern(name)) { + for (const el of name.elements) { + // mark individual names in binding pattern + checkModuleAugmentationElement(el, isGlobalAugmentation); } + break; + } + // falls through + case SyntaxKind.ClassDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.TypeAliasDeclaration: + if (isGlobalAugmentation) { + return; } - } - - return undefined; - } - - // Return true if the given node is a declaration of a nested block scoped entity with a name that either hides an - // existing name or might hide a name when compiled downlevel - function isDeclarationWithCollidingName(nodeIn: Declaration): boolean { - const node = getParseTreeNode(nodeIn, isDeclaration); - if (node) { const symbol = getSymbolOfNode(node); if (symbol) { - return isSymbolOfDeclarationWithCollidingName(symbol); + // module augmentations cannot introduce new names on the top level scope of the module + // this is done it two steps + // 1. quick check - if symbol for node is not merged - this is local symbol to this augmentation - report error + // 2. main check - report error if value declaration of the parent symbol is module augmentation) + let reportError = !(symbol.flags & SymbolFlags.Transient); + if (!reportError) { + // symbol should not originate in augmentation + reportError = !!symbol.parent && isExternalModuleAugmentation(symbol.parent.declarations[0]); + } } - } - + break; + } + } + function getFirstNonModuleExportsIdentifier(node: EntityNameOrEntityNameExpression): Identifier { + switch (node.kind) { + case SyntaxKind.Identifier: + return node; + case SyntaxKind.QualifiedName: + do { + node = node.left; + } while (node.kind !== SyntaxKind.Identifier); + return node; + case SyntaxKind.PropertyAccessExpression: + do { + if (isModuleExportsAccessExpression(node.expression) && !isPrivateIdentifier(node.name)) { + return node.name; + } + node = node.expression; + } while (node.kind !== SyntaxKind.Identifier); + return node; + } + } + function checkExternalImportOrExportDeclaration(node: ImportDeclaration | ImportEqualsDeclaration | ExportDeclaration): boolean { + const moduleName = getExternalModuleName(node); + if (!moduleName || nodeIsMissing(moduleName)) { + // Should be a parse error. return false; } - - function isValueAliasDeclaration(node: Node): boolean { - switch (node.kind) { - case SyntaxKind.ImportEqualsDeclaration: - return isAliasResolvedToValue(getSymbolOfNode(node) || unknownSymbol); - case SyntaxKind.ImportClause: - case SyntaxKind.NamespaceImport: - case SyntaxKind.ImportSpecifier: - case SyntaxKind.ExportSpecifier: - const symbol = getSymbolOfNode(node) || unknownSymbol; - return isAliasResolvedToValue(symbol) && !getTypeOnlyAliasDeclaration(symbol); - case SyntaxKind.ExportDeclaration: - const exportClause = (node).exportClause; - return !!exportClause && ( - isNamespaceExport(exportClause) || - some(exportClause.elements, isValueAliasDeclaration) - ); - case SyntaxKind.ExportAssignment: - return (node).expression && (node).expression.kind === SyntaxKind.Identifier ? - isAliasResolvedToValue(getSymbolOfNode(node) || unknownSymbol) : - true; - } + if (!isStringLiteral(moduleName)) { + error(moduleName, Diagnostics.String_literal_expected); return false; } - - function isTopLevelValueImportEqualsWithEntityName(nodeIn: ImportEqualsDeclaration): boolean { - const node = getParseTreeNode(nodeIn, isImportEqualsDeclaration); - if (node === undefined || node.parent.kind !== SyntaxKind.SourceFile || !isInternalModuleImportEqualsDeclaration(node)) { - // parent is not source file or it is not reference to internal module + const inAmbientExternalModule = node.parent.kind === SyntaxKind.ModuleBlock && isAmbientModule(node.parent.parent); + if (node.parent.kind !== SyntaxKind.SourceFile && !inAmbientExternalModule) { + error(moduleName, node.kind === SyntaxKind.ExportDeclaration ? + Diagnostics.Export_declarations_are_not_permitted_in_a_namespace : + Diagnostics.Import_declarations_in_a_namespace_cannot_reference_a_module); + return false; + } + if (inAmbientExternalModule && isExternalModuleNameRelative(moduleName.text)) { + // we have already reported errors on top level imports\exports in external module augmentations in checkModuleDeclaration + // no need to do this again. + if (!isTopLevelInExternalModuleAugmentation(node)) { + // TypeScript 1.0 spec (April 2013): 12.1.6 + // An ExternalImportDeclaration in an AmbientExternalModuleDeclaration may reference + // other external modules only through top - level external module names. + // Relative external module names are not permitted. + error(node, Diagnostics.Import_or_export_declaration_in_an_ambient_module_declaration_cannot_reference_module_through_relative_module_name); return false; } - - const isValue = isAliasResolvedToValue(getSymbolOfNode(node)); - return isValue && node.moduleReference && !nodeIsMissing(node.moduleReference); } - - function isAliasResolvedToValue(symbol: Symbol): boolean { - const target = resolveAlias(symbol); - if (target === unknownSymbol) { - return true; + return true; + } + function checkAliasSymbol(node: ImportEqualsDeclaration | ImportClause | NamespaceImport | ImportSpecifier | ExportSpecifier | NamespaceExport) { + let symbol = getSymbolOfNode(node); + const target = resolveAlias(symbol); + const shouldSkipWithJSExpandoTargets = symbol.flags & SymbolFlags.Assignment; + if (!shouldSkipWithJSExpandoTargets && target !== unknownSymbol) { + // For external modules symbol represents local symbol for an alias. + // This local symbol will merge any other local declarations (excluding other aliases) + // and symbol.flags will contains combined representation for all merged declaration. + // Based on symbol.flags we can compute a set of excluded meanings (meaning that resolved alias should not have, + // otherwise it will conflict with some local declaration). Note that in addition to normal flags we include matching SymbolFlags.Export* + // in order to prevent collisions with declarations that were exported from the current module (they still contribute to local names). + symbol = getMergedSymbol(symbol.exportSymbol || symbol); + const excludedMeanings = (symbol.flags & (SymbolFlags.Value | SymbolFlags.ExportValue) ? SymbolFlags.Value : 0) | + (symbol.flags & SymbolFlags.Type ? SymbolFlags.Type : 0) | + (symbol.flags & SymbolFlags.Namespace ? SymbolFlags.Namespace : 0); + if (target.flags & excludedMeanings) { + const message = node.kind === SyntaxKind.ExportSpecifier ? + Diagnostics.Export_declaration_conflicts_with_exported_declaration_of_0 : + Diagnostics.Import_declaration_conflicts_with_local_declaration_of_0; + error(node, message, symbolToString(symbol)); + } + // Don't allow to re-export something with no value side when `--isolatedModules` is set. + if (compilerOptions.isolatedModules + && node.kind === SyntaxKind.ExportSpecifier + && !node.parent.parent.isTypeOnly + && !(target.flags & SymbolFlags.Value) + && !(node.flags & NodeFlags.Ambient)) { + error(node, Diagnostics.Re_exporting_a_type_when_the_isolatedModules_flag_is_provided_requires_using_export_type); } - // const enums and modules that contain only const enums are not considered values from the emit perspective - // unless 'preserveConstEnums' option is set to true - return !!(target.flags & SymbolFlags.Value) && - (compilerOptions.preserveConstEnums || !isConstEnumOrConstEnumOnlyModule(target)); } - - function isConstEnumOrConstEnumOnlyModule(s: Symbol): boolean { - return isConstEnumSymbol(s) || !!s.constEnumOnlyModule; + } + function checkImportBinding(node: ImportEqualsDeclaration | ImportClause | NamespaceImport | ImportSpecifier) { + checkCollisionWithRequireExportsInGeneratedCode(node, node.name!); + checkCollisionWithGlobalPromiseInGeneratedCode(node, node.name!); + checkAliasSymbol(node); + } + function checkImportDeclaration(node: ImportDeclaration) { + if (checkGrammarModuleElementContext(node, Diagnostics.An_import_declaration_can_only_be_used_in_a_namespace_or_module)) { + // If we hit an import declaration in an illegal context, just bail out to avoid cascading errors. + return; } - - function isReferencedAliasDeclaration(node: Node, checkChildren?: boolean): boolean { - if (isAliasSymbolDeclaration(node)) { - const symbol = getSymbolOfNode(node); - if (symbol && getSymbolLinks(symbol).referenced) { - return true; + if (!checkGrammarDecoratorsAndModifiers(node) && hasModifiers(node)) { + grammarErrorOnFirstToken(node, Diagnostics.An_import_declaration_cannot_have_modifiers); + } + if (checkExternalImportOrExportDeclaration(node)) { + const importClause = node.importClause; + if (importClause && !checkGrammarImportClause(importClause)) { + if (importClause.name) { + checkImportBinding(importClause); } - const target = getSymbolLinks(symbol!).target; // TODO: GH#18217 - if (target && getModifierFlags(node) & ModifierFlags.Export && - target.flags & SymbolFlags.Value && - (compilerOptions.preserveConstEnums || !isConstEnumOrConstEnumOnlyModule(target))) { - // An `export import ... =` of a value symbol is always considered referenced - return true; + if (importClause.namedBindings) { + if (importClause.namedBindings.kind === SyntaxKind.NamespaceImport) { + checkImportBinding(importClause.namedBindings); + } + else { + const moduleExisted = resolveExternalModuleName(node, node.moduleSpecifier); + if (moduleExisted) { + forEach(importClause.namedBindings.elements, checkImportBinding); + } + } } } - - if (checkChildren) { - return !!forEachChild(node, node => isReferencedAliasDeclaration(node, checkChildren)); - } - return false; } - - function isImplementationOfOverload(node: SignatureDeclaration) { - if (nodeIsPresent((node as FunctionLikeDeclaration).body)) { - if (isGetAccessor(node) || isSetAccessor(node)) return false; // Get or set accessors can never be overload implementations, but can have up to 2 signatures - const symbol = getSymbolOfNode(node); - const signaturesOfSymbol = getSignaturesOfSymbol(symbol); - // If this function body corresponds to function with multiple signature, it is implementation of overload - // e.g.: function foo(a: string): string; - // function foo(a: number): number; - // function foo(a: any) { // This is implementation of the overloads - // return a; - // } - return signaturesOfSymbol.length > 1 || - // If there is single signature for the symbol, it is overload if that signature isn't coming from the node - // e.g.: function foo(a: string): string; - // function foo(a: any) { // This is implementation of the overloads - // return a; - // } - (signaturesOfSymbol.length === 1 && signaturesOfSymbol[0].declaration !== node); + } + function checkImportEqualsDeclaration(node: ImportEqualsDeclaration) { + if (checkGrammarModuleElementContext(node, Diagnostics.An_import_declaration_can_only_be_used_in_a_namespace_or_module)) { + // If we hit an import declaration in an illegal context, just bail out to avoid cascading errors. + return; + } + checkGrammarDecoratorsAndModifiers(node); + if (isInternalModuleImportEqualsDeclaration(node) || checkExternalImportOrExportDeclaration(node)) { + checkImportBinding(node); + if (hasModifier(node, ModifierFlags.Export)) { + markExportAsReferenced(node); + } + if (node.moduleReference.kind !== SyntaxKind.ExternalModuleReference) { + const target = resolveAlias(getSymbolOfNode(node)); + if (target !== unknownSymbol) { + if (target.flags & SymbolFlags.Value) { + // Target is a value symbol, check that it is not hidden by a local declaration with the same name + const moduleName = getFirstIdentifier(node.moduleReference); + if (!(resolveEntityName(moduleName, SymbolFlags.Value | SymbolFlags.Namespace)!.flags & SymbolFlags.Namespace)) { + error(moduleName, Diagnostics.Module_0_is_hidden_by_a_local_declaration_with_the_same_name, declarationNameToString(moduleName)); + } + } + if (target.flags & SymbolFlags.Type) { + checkTypeNameIsReserved(node.name, Diagnostics.Import_name_cannot_be_0); + } + } + } + else { + if (moduleKind >= ModuleKind.ES2015 && !(node.flags & NodeFlags.Ambient)) { + // Import equals declaration is deprecated in es6 or above + grammarErrorOnNode(node, Diagnostics.Import_assignment_cannot_be_used_when_targeting_ECMAScript_modules_Consider_using_import_Asterisk_as_ns_from_mod_import_a_from_mod_import_d_from_mod_or_another_module_format_instead); + } } - return false; } - - function isRequiredInitializedParameter(parameter: ParameterDeclaration | JSDocParameterTag): boolean { - return !!strictNullChecks && - !isOptionalParameter(parameter) && - !isJSDocParameterTag(parameter) && - !!parameter.initializer && - !hasModifier(parameter, ModifierFlags.ParameterPropertyModifier); + } + function checkExportDeclaration(node: ExportDeclaration) { + if (checkGrammarModuleElementContext(node, Diagnostics.An_export_declaration_can_only_be_used_in_a_module)) { + // If we hit an export in an illegal context, just bail out to avoid cascading errors. + return; } - - function isOptionalUninitializedParameterProperty(parameter: ParameterDeclaration) { - return strictNullChecks && - isOptionalParameter(parameter) && - !parameter.initializer && - hasModifier(parameter, ModifierFlags.ParameterPropertyModifier); + if (!checkGrammarDecoratorsAndModifiers(node) && hasModifiers(node)) { + grammarErrorOnFirstToken(node, Diagnostics.An_export_declaration_cannot_have_modifiers); } - - function isExpandoFunctionDeclaration(node: Declaration): boolean { - const declaration = getParseTreeNode(node, isFunctionDeclaration); - if (!declaration) { - return false; - } - const symbol = getSymbolOfNode(declaration); - if (!symbol || !(symbol.flags & SymbolFlags.Function)) { - return false; - } - return !!forEachEntry(getExportsOfSymbol(symbol), p => p.flags & SymbolFlags.Value && p.valueDeclaration && isPropertyAccessExpression(p.valueDeclaration)); + if (node.moduleSpecifier && node.exportClause && isNamedExports(node.exportClause) && length(node.exportClause.elements) && languageVersion === ScriptTarget.ES3) { + checkExternalEmitHelpers(node, ExternalEmitHelpers.CreateBinding); } - - function getPropertiesOfContainerFunction(node: Declaration): Symbol[] { - const declaration = getParseTreeNode(node, isFunctionDeclaration); - if (!declaration) { - return emptyArray; + checkGrammarExportDeclaration(node); + if (!node.moduleSpecifier || checkExternalImportOrExportDeclaration(node)) { + if (node.exportClause) { + // export { x, y } + // export { x, y } from "foo" + if (isNamedExports(node.exportClause)) { + forEach(node.exportClause.elements, checkExportSpecifier); + } + else if (!isNamespaceExport(node.exportClause)) { + checkImportBinding(node.exportClause); + } + const inAmbientExternalModule = node.parent.kind === SyntaxKind.ModuleBlock && isAmbientModule(node.parent.parent); + const inAmbientNamespaceDeclaration = !inAmbientExternalModule && node.parent.kind === SyntaxKind.ModuleBlock && + !node.moduleSpecifier && node.flags & NodeFlags.Ambient; + if (node.parent.kind !== SyntaxKind.SourceFile && !inAmbientExternalModule && !inAmbientNamespaceDeclaration) { + error(node, Diagnostics.Export_declarations_are_not_permitted_in_a_namespace); + } + } + else { + // export * from "foo" + const moduleSymbol = resolveExternalModuleName(node, node.moduleSpecifier!); + if (moduleSymbol && hasExportAssignmentSymbol(moduleSymbol)) { + error(node.moduleSpecifier, Diagnostics.Module_0_uses_export_and_cannot_be_used_with_export_Asterisk, symbolToString(moduleSymbol)); + } + if (moduleKind !== ModuleKind.System && moduleKind < ModuleKind.ES2015) { + checkExternalEmitHelpers(node, ExternalEmitHelpers.ExportStar); + } } - const symbol = getSymbolOfNode(declaration); - return symbol && getPropertiesOfType(getTypeOfSymbol(symbol)) || emptyArray; } - - function getNodeCheckFlags(node: Node): NodeCheckFlags { - return getNodeLinks(node).flags || 0; + } + function checkGrammarExportDeclaration(node: ExportDeclaration): boolean { + const isTypeOnlyExportStar = node.isTypeOnly && node.exportClause?.kind !== SyntaxKind.NamedExports; + if (isTypeOnlyExportStar) { + grammarErrorOnNode(node, Diagnostics.Only_named_exports_may_use_export_type); } - - function getEnumMemberValue(node: EnumMember): string | number | undefined { - computeEnumMemberValues(node.parent); - return getNodeLinks(node).enumMemberValue; + return !isTypeOnlyExportStar; + } + function checkGrammarModuleElementContext(node: Statement, errorMessage: DiagnosticMessage): boolean { + const isInAppropriateContext = node.parent.kind === SyntaxKind.SourceFile || node.parent.kind === SyntaxKind.ModuleBlock || node.parent.kind === SyntaxKind.ModuleDeclaration; + if (!isInAppropriateContext) { + grammarErrorOnFirstToken(node, errorMessage); } - - function canHaveConstantValue(node: Node): node is EnumMember | AccessExpression { - switch (node.kind) { - case SyntaxKind.EnumMember: - case SyntaxKind.PropertyAccessExpression: - case SyntaxKind.ElementAccessExpression: - return true; + return !isInAppropriateContext; + } + function importClauseContainsReferencedImport(importClause: ImportClause) { + return forEachImportClauseDeclaration(importClause, declaration => { + return !!getSymbolOfNode(declaration).isReferenced; + }); + } + function importClauseContainsConstEnumUsedAsValue(importClause: ImportClause) { + return forEachImportClauseDeclaration(importClause, declaration => { + return !!getSymbolLinks(getSymbolOfNode(declaration)).constEnumReferenced; + }); + } + function checkImportsForTypeOnlyConversion(sourceFile: SourceFile) { + for (const statement of sourceFile.statements) { + if (isImportDeclaration(statement) && + statement.importClause && + !statement.importClause.isTypeOnly && + importClauseContainsReferencedImport(statement.importClause) && + !isReferencedAliasDeclaration(statement.importClause, /*checkChildren*/ true) && + !importClauseContainsConstEnumUsedAsValue(statement.importClause)) { + error(statement, Diagnostics.This_import_is_never_used_as_a_value_and_must_use_import_type_because_the_importsNotUsedAsValues_is_set_to_error); } - return false; } - - function getConstantValue(node: EnumMember | AccessExpression): string | number | undefined { - if (node.kind === SyntaxKind.EnumMember) { - return getEnumMemberValue(node); + } + function checkExportSpecifier(node: ExportSpecifier) { + checkAliasSymbol(node); + if (getEmitDeclarations(compilerOptions)) { + collectLinkedAliases(node.propertyName || node.name, /*setVisibility*/ true); + } + if (!node.parent.parent.moduleSpecifier) { + const exportedName = node.propertyName || node.name; + // find immediate value referenced by exported name (SymbolFlags.Alias is set so we don't chase down aliases) + const symbol = resolveName(exportedName, exportedName.escapedText, SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace | SymbolFlags.Alias, + /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ true); + if (symbol && (symbol === undefinedSymbol || symbol === globalThisSymbol || isGlobalSourceFile(getDeclarationContainer(symbol.declarations[0])))) { + error(exportedName, Diagnostics.Cannot_export_0_Only_local_declarations_can_be_exported_from_a_module, idText(exportedName)); } - - const symbol = getNodeLinks(node).resolvedSymbol; - if (symbol && (symbol.flags & SymbolFlags.EnumMember)) { - // inline property\index accesses only for const enums - const member = symbol.valueDeclaration as EnumMember; - if (isEnumConst(member.parent)) { - return getEnumMemberValue(member); + else { + markExportAsReferenced(node); + const target = symbol && (symbol.flags & SymbolFlags.Alias ? resolveAlias(symbol) : symbol); + if (!target || target === unknownSymbol || target.flags & SymbolFlags.Value) { + checkExpressionCached(node.propertyName || node.name); } } - - return undefined; } - - function isFunctionType(type: Type): boolean { - return !!(type.flags & TypeFlags.Object) && getSignaturesOfType(type, SignatureKind.Call).length > 0; + } + function checkExportAssignment(node: ExportAssignment) { + if (checkGrammarModuleElementContext(node, Diagnostics.An_export_assignment_can_only_be_used_in_a_module)) { + // If we hit an export assignment in an illegal context, just bail out to avoid cascading errors. + return; } - - function getTypeReferenceSerializationKind(typeNameIn: EntityName, location?: Node): TypeReferenceSerializationKind { - // ensure both `typeName` and `location` are parse tree nodes. - const typeName = getParseTreeNode(typeNameIn, isEntityName); - if (!typeName) return TypeReferenceSerializationKind.Unknown; - - if (location) { - location = getParseTreeNode(location); - if (!location) return TypeReferenceSerializationKind.Unknown; + const container = node.parent.kind === SyntaxKind.SourceFile ? node.parent : node.parent.parent; + if (container.kind === SyntaxKind.ModuleDeclaration && !isAmbientModule(container)) { + if (node.isExportEquals) { + error(node, Diagnostics.An_export_assignment_cannot_be_used_in_a_namespace); } - - // Resolve the symbol as a value to ensure the type can be reached at runtime during emit. - const valueSymbol = resolveEntityName(typeName, SymbolFlags.Value, /*ignoreErrors*/ true, /*dontResolveAlias*/ false, location); - - // Resolve the symbol as a type so that we can provide a more useful hint for the type serializer. - const typeSymbol = resolveEntityName(typeName, SymbolFlags.Type, /*ignoreErrors*/ true, /*dontResolveAlias*/ false, location); - if (valueSymbol && valueSymbol === typeSymbol) { - const globalPromiseSymbol = getGlobalPromiseConstructorSymbol(/*reportErrors*/ false); - if (globalPromiseSymbol && valueSymbol === globalPromiseSymbol) { - return TypeReferenceSerializationKind.Promise; - } - - const constructorType = getTypeOfSymbol(valueSymbol); - if (constructorType && isConstructorType(constructorType)) { - return TypeReferenceSerializationKind.TypeWithConstructSignatureAndValue; - } + else { + error(node, Diagnostics.A_default_export_can_only_be_used_in_an_ECMAScript_style_module); } - - // We might not be able to resolve type symbol so use unknown type in that case (eg error case) - if (!typeSymbol) { - return TypeReferenceSerializationKind.Unknown; + return; + } + // Grammar checking + if (!checkGrammarDecoratorsAndModifiers(node) && hasModifiers(node)) { + grammarErrorOnFirstToken(node, Diagnostics.An_export_assignment_cannot_have_modifiers); + } + if (node.expression.kind === SyntaxKind.Identifier) { + const id = (node.expression as Identifier); + const sym = resolveEntityName(id, SymbolFlags.All, /*ignoreErrors*/ true, /*dontResolveAlias*/ true, node); + if (sym) { + markAliasReferenced(sym, id); + // If not a value, we're interpreting the identifier as a type export, along the lines of (`export { Id as default }`) + const target = sym.flags & SymbolFlags.Alias ? resolveAlias(sym) : sym; + if (target === unknownSymbol || target.flags & SymbolFlags.Value) { + // However if it is a value, we need to check it's being used correctly + checkExpressionCached(node.expression); + } } - const type = getDeclaredTypeOfSymbol(typeSymbol); - if (type === errorType) { - return TypeReferenceSerializationKind.Unknown; + if (getEmitDeclarations(compilerOptions)) { + collectLinkedAliases((node.expression as Identifier), /*setVisibility*/ true); } - else if (type.flags & TypeFlags.AnyOrUnknown) { - return TypeReferenceSerializationKind.ObjectType; + } + else { + checkExpressionCached(node.expression); + } + checkExternalModuleExports(container); + if ((node.flags & NodeFlags.Ambient) && !isEntityNameExpression(node.expression)) { + grammarErrorOnNode(node.expression, Diagnostics.The_expression_of_an_export_assignment_must_be_an_identifier_or_qualified_name_in_an_ambient_context); + } + if (node.isExportEquals && !(node.flags & NodeFlags.Ambient)) { + if (moduleKind >= ModuleKind.ES2015) { + // export assignment is not supported in es6 modules + grammarErrorOnNode(node, Diagnostics.Export_assignment_cannot_be_used_when_targeting_ECMAScript_modules_Consider_using_export_default_or_another_module_format_instead); } - else if (isTypeAssignableToKind(type, TypeFlags.Void | TypeFlags.Nullable | TypeFlags.Never)) { - return TypeReferenceSerializationKind.VoidNullableOrNeverType; + else if (moduleKind === ModuleKind.System) { + // system modules does not support export assignment + grammarErrorOnNode(node, Diagnostics.Export_assignment_is_not_supported_when_module_flag_is_system); } - else if (isTypeAssignableToKind(type, TypeFlags.BooleanLike)) { - return TypeReferenceSerializationKind.BooleanType; + } + } + function hasExportedMembers(moduleSymbol: ts.Symbol) { + return forEachEntry((moduleSymbol.exports!), (_, id) => id !== "export="); + } + function checkExternalModuleExports(node: SourceFile | ModuleDeclaration) { + const moduleSymbol = getSymbolOfNode(node); + const links = getSymbolLinks(moduleSymbol); + if (!links.exportsChecked) { + const exportEqualsSymbol = moduleSymbol.exports!.get(("export=" as __String)); + if (exportEqualsSymbol && hasExportedMembers(moduleSymbol)) { + const declaration = getDeclarationOfAliasSymbol(exportEqualsSymbol) || exportEqualsSymbol.valueDeclaration; + if (!isTopLevelInExternalModuleAugmentation(declaration) && !isInJSFile(declaration)) { + error(declaration, Diagnostics.An_export_assignment_cannot_be_used_in_a_module_with_other_exported_elements); + } + } + // Checks for export * conflicts + const exports = getExportsOfModule(moduleSymbol); + if (exports) { + exports.forEach(({ declarations, flags }, id) => { + if (id === "__export") { + return; + } + // ECMA262: 15.2.1.1 It is a Syntax Error if the ExportedNames of ModuleItemList contains any duplicate entries. + // (TS Exceptions: namespaces, function overloads, enums, and interfaces) + if (flags & (SymbolFlags.Namespace | SymbolFlags.Interface | SymbolFlags.Enum)) { + return; + } + const exportedDeclarationsCount = countWhere(declarations, isNotOverloadAndNotAccessor); + if (flags & SymbolFlags.TypeAlias && exportedDeclarationsCount <= 2) { + // it is legal to merge type alias with other values + // so count should be either 1 (just type alias) or 2 (type alias + merged value) + return; + } + if (exportedDeclarationsCount > 1) { + for (const declaration of declarations) { + if (isNotOverload(declaration)) { + diagnostics.add(createDiagnosticForNode(declaration, Diagnostics.Cannot_redeclare_exported_variable_0, unescapeLeadingUnderscores(id))); + } + } + } + }); } - else if (isTypeAssignableToKind(type, TypeFlags.NumberLike)) { - return TypeReferenceSerializationKind.NumberLikeType; + links.exportsChecked = true; + } + } + function checkSourceElement(node: Node | undefined): void { + if (node) { + const saveCurrentNode = currentNode; + currentNode = node; + instantiationCount = 0; + checkSourceElementWorker(node); + currentNode = saveCurrentNode; + } + } + function checkSourceElementWorker(node: Node): void { + if (isInJSFile(node)) { + forEach((node as JSDocContainer).jsDoc, ({ tags }) => forEach(tags, checkSourceElement)); + } + const kind = node.kind; + if (cancellationToken) { + // Only bother checking on a few construct kinds. We don't want to be excessively + // hitting the cancellation token on every node we check. + switch (kind) { + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.FunctionDeclaration: + cancellationToken.throwIfCancellationRequested(); } - else if (isTypeAssignableToKind(type, TypeFlags.BigIntLike)) { - return TypeReferenceSerializationKind.BigIntLikeType; + } + if (kind >= SyntaxKind.FirstStatement && kind <= SyntaxKind.LastStatement && node.flowNode && !isReachableFlowNode(node.flowNode)) { + errorOrSuggestion(compilerOptions.allowUnreachableCode === false, node, Diagnostics.Unreachable_code_detected); + } + switch (kind) { + case SyntaxKind.TypeParameter: + return checkTypeParameter((node)); + case SyntaxKind.Parameter: + return checkParameter((node)); + case SyntaxKind.PropertyDeclaration: + return checkPropertyDeclaration((node)); + case SyntaxKind.PropertySignature: + return checkPropertySignature((node)); + case SyntaxKind.FunctionType: + case SyntaxKind.ConstructorType: + case SyntaxKind.CallSignature: + case SyntaxKind.ConstructSignature: + case SyntaxKind.IndexSignature: + return checkSignatureDeclaration((node)); + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: + return checkMethodDeclaration((node)); + case SyntaxKind.Constructor: + return checkConstructorDeclaration((node)); + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + return checkAccessorDeclaration((node)); + case SyntaxKind.TypeReference: + return checkTypeReferenceNode((node)); + case SyntaxKind.TypePredicate: + return checkTypePredicate((node)); + case SyntaxKind.TypeQuery: + return checkTypeQuery((node)); + case SyntaxKind.TypeLiteral: + return checkTypeLiteral((node)); + case SyntaxKind.ArrayType: + return checkArrayType((node)); + case SyntaxKind.TupleType: + return checkTupleType((node)); + case SyntaxKind.UnionType: + case SyntaxKind.IntersectionType: + return checkUnionOrIntersectionType((node)); + case SyntaxKind.ParenthesizedType: + case SyntaxKind.OptionalType: + case SyntaxKind.RestType: + return checkSourceElement((node).type); + case SyntaxKind.ThisType: + return checkThisType((node)); + case SyntaxKind.TypeOperator: + return checkTypeOperator((node)); + case SyntaxKind.ConditionalType: + return checkConditionalType((node)); + case SyntaxKind.InferType: + return checkInferType((node)); + case SyntaxKind.ImportType: + return checkImportType((node)); + case SyntaxKind.JSDocAugmentsTag: + return checkJSDocAugmentsTag((node as JSDocAugmentsTag)); + case SyntaxKind.JSDocImplementsTag: + return checkJSDocImplementsTag((node as JSDocImplementsTag)); + case SyntaxKind.JSDocTypedefTag: + case SyntaxKind.JSDocCallbackTag: + case SyntaxKind.JSDocEnumTag: + return checkJSDocTypeAliasTag((node as JSDocTypedefTag)); + case SyntaxKind.JSDocTemplateTag: + return checkJSDocTemplateTag((node as JSDocTemplateTag)); + case SyntaxKind.JSDocTypeTag: + return checkJSDocTypeTag((node as JSDocTypeTag)); + case SyntaxKind.JSDocParameterTag: + return checkJSDocParameterTag((node as JSDocParameterTag)); + case SyntaxKind.JSDocFunctionType: + checkJSDocFunctionType((node as JSDocFunctionType)); + // falls through + case SyntaxKind.JSDocNonNullableType: + case SyntaxKind.JSDocNullableType: + case SyntaxKind.JSDocAllType: + case SyntaxKind.JSDocUnknownType: + case SyntaxKind.JSDocTypeLiteral: + checkJSDocTypeIsInJsFile(node); + forEachChild(node, checkSourceElement); + return; + case SyntaxKind.JSDocVariadicType: + checkJSDocVariadicType((node as JSDocVariadicType)); + return; + case SyntaxKind.JSDocTypeExpression: + return checkSourceElement((node as JSDocTypeExpression).type); + case SyntaxKind.IndexedAccessType: + return checkIndexedAccessType((node)); + case SyntaxKind.MappedType: + return checkMappedType((node)); + case SyntaxKind.FunctionDeclaration: + return checkFunctionDeclaration((node)); + case SyntaxKind.Block: + case SyntaxKind.ModuleBlock: + return checkBlock((node)); + case SyntaxKind.VariableStatement: + return checkVariableStatement((node)); + case SyntaxKind.ExpressionStatement: + return checkExpressionStatement((node)); + case SyntaxKind.IfStatement: + return checkIfStatement((node)); + case SyntaxKind.DoStatement: + return checkDoStatement((node)); + case SyntaxKind.WhileStatement: + return checkWhileStatement((node)); + case SyntaxKind.ForStatement: + return checkForStatement((node)); + case SyntaxKind.ForInStatement: + return checkForInStatement((node)); + case SyntaxKind.ForOfStatement: + return checkForOfStatement((node)); + case SyntaxKind.ContinueStatement: + case SyntaxKind.BreakStatement: + return checkBreakOrContinueStatement((node)); + case SyntaxKind.ReturnStatement: + return checkReturnStatement((node)); + case SyntaxKind.WithStatement: + return checkWithStatement((node)); + case SyntaxKind.SwitchStatement: + return checkSwitchStatement((node)); + case SyntaxKind.LabeledStatement: + return checkLabeledStatement((node)); + case SyntaxKind.ThrowStatement: + return checkThrowStatement((node)); + case SyntaxKind.TryStatement: + return checkTryStatement((node)); + case SyntaxKind.VariableDeclaration: + return checkVariableDeclaration((node)); + case SyntaxKind.BindingElement: + return checkBindingElement((node)); + case SyntaxKind.ClassDeclaration: + return checkClassDeclaration((node)); + case SyntaxKind.InterfaceDeclaration: + return checkInterfaceDeclaration((node)); + case SyntaxKind.TypeAliasDeclaration: + return checkTypeAliasDeclaration((node)); + case SyntaxKind.EnumDeclaration: + return checkEnumDeclaration((node)); + case SyntaxKind.ModuleDeclaration: + return checkModuleDeclaration((node)); + case SyntaxKind.ImportDeclaration: + return checkImportDeclaration((node)); + case SyntaxKind.ImportEqualsDeclaration: + return checkImportEqualsDeclaration((node)); + case SyntaxKind.ExportDeclaration: + return checkExportDeclaration((node)); + case SyntaxKind.ExportAssignment: + return checkExportAssignment((node)); + case SyntaxKind.EmptyStatement: + case SyntaxKind.DebuggerStatement: + checkGrammarStatementInAmbientContext(node); + return; + case SyntaxKind.MissingDeclaration: + return checkMissingDeclaration(node); + } + } + function checkJSDocTypeIsInJsFile(node: Node): void { + if (!isInJSFile(node)) { + grammarErrorOnNode(node, Diagnostics.JSDoc_types_can_only_be_used_inside_documentation_comments); + } + } + function checkJSDocVariadicType(node: JSDocVariadicType): void { + checkJSDocTypeIsInJsFile(node); + checkSourceElement(node.type); + // Only legal location is in the *last* parameter tag or last parameter of a JSDoc function. + const { parent } = node; + if (isParameter(parent) && isJSDocFunctionType(parent.parent)) { + if (last(parent.parent.parameters) !== parent) { + error(node, Diagnostics.A_rest_parameter_must_be_last_in_a_parameter_list); } - else if (isTypeAssignableToKind(type, TypeFlags.StringLike)) { - return TypeReferenceSerializationKind.StringLikeType; + return; + } + if (!isJSDocTypeExpression(parent)) { + error(node, Diagnostics.JSDoc_may_only_appear_in_the_last_parameter_of_a_signature); + } + const paramTag = node.parent.parent; + if (!isJSDocParameterTag(paramTag)) { + error(node, Diagnostics.JSDoc_may_only_appear_in_the_last_parameter_of_a_signature); + return; + } + const param = getParameterSymbolFromJSDoc(paramTag); + if (!param) { + // We will error in `checkJSDocParameterTag`. + return; + } + const host = getHostSignatureFromJSDoc(paramTag); + if (!host || last(host.parameters).symbol !== param) { + error(node, Diagnostics.A_rest_parameter_must_be_last_in_a_parameter_list); + } + } + function getTypeFromJSDocVariadicType(node: JSDocVariadicType): ts.Type { + const type = getTypeFromTypeNode(node.type); + const { parent } = node; + const paramTag = node.parent.parent; + if (isJSDocTypeExpression(node.parent) && isJSDocParameterTag(paramTag)) { + // Else we will add a diagnostic, see `checkJSDocVariadicType`. + const host = getHostSignatureFromJSDoc(paramTag); + if (host) { + /* + Only return an array type if the corresponding parameter is marked as a rest parameter, or if there are no parameters. + So in the following situation we will not create an array type: + /** @param {...number} a * / + function f(a) {} + Because `a` will just be of type `number | undefined`. A synthetic `...args` will also be added, which *will* get an array type. + */ + const lastParamDeclaration = lastOrUndefined(host.parameters); + const symbol = getParameterSymbolFromJSDoc(paramTag); + if (!lastParamDeclaration || + symbol && lastParamDeclaration.symbol === symbol && isRestParameter(lastParamDeclaration)) { + return createArrayType(type); + } } - else if (isTupleType(type)) { - return TypeReferenceSerializationKind.ArrayLikeType; + } + if (isParameter(parent) && isJSDocFunctionType(parent.parent)) { + return createArrayType(type); + } + return addOptionality(type); + } + // Function and class expression bodies are checked after all statements in the enclosing body. This is + // to ensure constructs like the following are permitted: + // const foo = function () { + // const s = foo(); + // return "hello"; + // } + // Here, performing a full type check of the body of the function expression whilst in the process of + // determining the type of foo would cause foo to be given type any because of the recursive reference. + // Delaying the type check of the body ensures foo has been assigned a type. + function checkNodeDeferred(node: Node) { + const enclosingFile = getSourceFileOfNode(node); + const links = getNodeLinks(enclosingFile); + if (!(links.flags & NodeCheckFlags.TypeChecked)) { + links.deferredNodes = links.deferredNodes || createMap(); + const id = "" + getNodeId(node); + links.deferredNodes.set(id, node); + } + } + function checkDeferredNodes(context: SourceFile) { + const links = getNodeLinks(context); + if (links.deferredNodes) { + links.deferredNodes.forEach(checkDeferredNode); + } + } + function checkDeferredNode(node: Node) { + const saveCurrentNode = currentNode; + currentNode = node; + instantiationCount = 0; + switch (node.kind) { + case SyntaxKind.CallExpression: + case SyntaxKind.NewExpression: + case SyntaxKind.TaggedTemplateExpression: + case SyntaxKind.Decorator: + case SyntaxKind.JsxOpeningElement: + // These node kinds are deferred checked when overload resolution fails + // To save on work, we ensure the arguments are checked just once, in + // a deferred way + resolveUntypedCall((node as CallLikeExpression)); + break; + case SyntaxKind.FunctionExpression: + case SyntaxKind.ArrowFunction: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: + checkFunctionExpressionOrObjectLiteralMethodDeferred((node)); + break; + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + checkAccessorDeclaration((node)); + break; + case SyntaxKind.ClassExpression: + checkClassExpressionDeferred((node)); + break; + case SyntaxKind.JsxSelfClosingElement: + checkJsxSelfClosingElementDeferred((node)); + break; + case SyntaxKind.JsxElement: + checkJsxElementDeferred((node)); + break; + } + currentNode = saveCurrentNode; + } + function checkSourceFile(node: SourceFile) { + mark("beforeCheck"); + checkSourceFileWorker(node); + mark("afterCheck"); + measure("Check", "beforeCheck", "afterCheck"); + } + function unusedIsError(kind: UnusedKind, isAmbient: boolean): boolean { + if (isAmbient) { + return false; + } + switch (kind) { + case UnusedKind.Local: + return !!compilerOptions.noUnusedLocals; + case UnusedKind.Parameter: + return !!compilerOptions.noUnusedParameters; + default: + return Debug.assertNever(kind); + } + } + function getPotentiallyUnusedIdentifiers(sourceFile: SourceFile): readonly PotentiallyUnusedIdentifier[] { + return allPotentiallyUnusedIdentifiers.get(sourceFile.path) || emptyArray; + } + // Fully type check a source file and collect the relevant diagnostics. + function checkSourceFileWorker(node: SourceFile) { + const links = getNodeLinks(node); + if (!(links.flags & NodeCheckFlags.TypeChecked)) { + if (skipTypeChecking(node, compilerOptions, host)) { + return; } - else if (isTypeAssignableToKind(type, TypeFlags.ESSymbolLike)) { - return TypeReferenceSerializationKind.ESSymbolType; + // Grammar checking + checkGrammarSourceFile(node); + clear(potentialThisCollisions); + clear(potentialNewTargetCollisions); + clear(potentialWeakMapCollisions); + forEach(node.statements, checkSourceElement); + checkSourceElement(node.endOfFileToken); + checkDeferredNodes(node); + if (isExternalOrCommonJsModule(node)) { + registerForUnusedIdentifiersCheck(node); } - else if (isFunctionType(type)) { - return TypeReferenceSerializationKind.TypeWithCallSignature; + if (!node.isDeclarationFile && (compilerOptions.noUnusedLocals || compilerOptions.noUnusedParameters)) { + checkUnusedIdentifiers(getPotentiallyUnusedIdentifiers(node), (containingNode, kind, diag) => { + if (!containsParseError(containingNode) && unusedIsError(kind, !!(containingNode.flags & NodeFlags.Ambient))) { + diagnostics.add(diag); + } + }); } - else if (isArrayType(type)) { - return TypeReferenceSerializationKind.ArrayLikeType; + if (compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error && + !node.isDeclarationFile && + isExternalModule(node)) { + checkImportsForTypeOnlyConversion(node); } - else { - return TypeReferenceSerializationKind.ObjectType; + if (isExternalOrCommonJsModule(node)) { + checkExternalModuleExports(node); } - } - - function createTypeOfDeclaration(declarationIn: AccessorDeclaration | VariableLikeDeclaration | PropertyAccessExpression, enclosingDeclaration: Node, flags: NodeBuilderFlags, tracker: SymbolTracker, addUndefined?: boolean) { - const declaration = getParseTreeNode(declarationIn, isVariableLikeOrAccessor); - if (!declaration) { - return createToken(SyntaxKind.AnyKeyword) as KeywordTypeNode; + if (potentialThisCollisions.length) { + forEach(potentialThisCollisions, checkIfThisIsCapturedInEnclosingScope); + clear(potentialThisCollisions); } - // Get type of the symbol if this is the valid symbol otherwise get type at location - const symbol = getSymbolOfNode(declaration); - let type = symbol && !(symbol.flags & (SymbolFlags.TypeLiteral | SymbolFlags.Signature)) - ? getWidenedLiteralType(getTypeOfSymbol(symbol)) - : errorType; - if (type.flags & TypeFlags.UniqueESSymbol && - type.symbol === symbol) { - flags |= NodeBuilderFlags.AllowUniqueESSymbolType; + if (potentialNewTargetCollisions.length) { + forEach(potentialNewTargetCollisions, checkIfNewTargetIsCapturedInEnclosingScope); + clear(potentialNewTargetCollisions); } - if (addUndefined) { - type = getOptionalType(type); + if (potentialWeakMapCollisions.length) { + forEach(potentialWeakMapCollisions, checkWeakMapCollision); + clear(potentialWeakMapCollisions); } - return nodeBuilder.typeToTypeNode(type, enclosingDeclaration, flags | NodeBuilderFlags.MultilineObjectLiterals, tracker); + links.flags |= NodeCheckFlags.TypeChecked; } - - function createReturnTypeOfSignatureDeclaration(signatureDeclarationIn: SignatureDeclaration, enclosingDeclaration: Node, flags: NodeBuilderFlags, tracker: SymbolTracker) { - const signatureDeclaration = getParseTreeNode(signatureDeclarationIn, isFunctionLike); - if (!signatureDeclaration) { - return createToken(SyntaxKind.AnyKeyword) as KeywordTypeNode; - } - const signature = getSignatureFromDeclaration(signatureDeclaration); - return nodeBuilder.typeToTypeNode(getReturnTypeOfSignature(signature), enclosingDeclaration, flags | NodeBuilderFlags.MultilineObjectLiterals, tracker); + } + function getDiagnostics(sourceFile: SourceFile, ct: CancellationToken): Diagnostic[] { + try { + // Record the cancellation token so it can be checked later on during checkSourceElement. + // Do this in a finally block so we can ensure that it gets reset back to nothing after + // this call is done. + cancellationToken = ct; + return getDiagnosticsWorker(sourceFile); } - - function createTypeOfExpression(exprIn: Expression, enclosingDeclaration: Node, flags: NodeBuilderFlags, tracker: SymbolTracker) { - const expr = getParseTreeNode(exprIn, isExpression); - if (!expr) { - return createToken(SyntaxKind.AnyKeyword) as KeywordTypeNode; - } - const type = getWidenedType(getRegularTypeOfExpression(expr)); - return nodeBuilder.typeToTypeNode(type, enclosingDeclaration, flags | NodeBuilderFlags.MultilineObjectLiterals, tracker); + finally { + cancellationToken = undefined; } - - function hasGlobalName(name: string): boolean { - return globals.has(escapeLeadingUnderscores(name)); + } + function getDiagnosticsWorker(sourceFile: SourceFile): Diagnostic[] { + throwIfNonDiagnosticsProducing(); + if (sourceFile) { + // Some global diagnostics are deferred until they are needed and + // may not be reported in the first call to getGlobalDiagnostics. + // We should catch these changes and report them. + const previousGlobalDiagnostics = diagnostics.getGlobalDiagnostics(); + const previousGlobalDiagnosticsSize = previousGlobalDiagnostics.length; + checkSourceFile(sourceFile); + const semanticDiagnostics = diagnostics.getDiagnostics(sourceFile.fileName); + const currentGlobalDiagnostics = diagnostics.getGlobalDiagnostics(); + if (currentGlobalDiagnostics !== previousGlobalDiagnostics) { + // If the arrays are not the same reference, new diagnostics were added. + const deferredGlobalDiagnostics = relativeComplement(previousGlobalDiagnostics, currentGlobalDiagnostics, compareDiagnostics); + return concatenate(deferredGlobalDiagnostics, semanticDiagnostics); + } + else if (previousGlobalDiagnosticsSize === 0 && currentGlobalDiagnostics.length > 0) { + // If the arrays are the same reference, but the length has changed, a single + // new diagnostic was added as DiagnosticCollection attempts to reuse the + // same array. + return concatenate(currentGlobalDiagnostics, semanticDiagnostics); + } + return semanticDiagnostics; + } + // Global diagnostics are always added when a file is not provided to + // getDiagnostics + forEach(host.getSourceFiles(), checkSourceFile); + return diagnostics.getDiagnostics(); + } + function getGlobalDiagnostics(): Diagnostic[] { + throwIfNonDiagnosticsProducing(); + return diagnostics.getGlobalDiagnostics(); + } + function throwIfNonDiagnosticsProducing() { + if (!produceDiagnostics) { + throw new Error("Trying to get diagnostics from a type checker that does not produce them."); } - - function getReferencedValueSymbol(reference: Identifier, startInDeclarationContainer?: boolean): Symbol | undefined { - const resolvedSymbol = getNodeLinks(reference).resolvedSymbol; - if (resolvedSymbol) { - return resolvedSymbol; - } - - let location: Node = reference; - if (startInDeclarationContainer) { - // When resolving the name of a declaration as a value, we need to start resolution - // at a point outside of the declaration. - const parent = reference.parent; - if (isDeclaration(parent) && reference === parent.name) { - location = getDeclarationContainer(parent); + } + // Language service support + function getSymbolsInScope(location: Node, meaning: SymbolFlags): ts.Symbol[] { + if (location.flags & NodeFlags.InWithStatement) { + // We cannot answer semantic questions within a with block, do not proceed any further + return []; + } + const symbols = createSymbolTable(); + let isStatic = false; + populateSymbols(); + symbols.delete(InternalSymbolName.This); // Not a symbol, a keyword + return symbolsToArray(symbols); + function populateSymbols() { + while (location) { + if (location.locals && !isGlobalSourceFile(location)) { + copySymbols(location.locals, meaning); + } + switch (location.kind) { + case SyntaxKind.SourceFile: + if (!isExternalOrCommonJsModule((location))) + break; + // falls through + case SyntaxKind.ModuleDeclaration: + copySymbols((getSymbolOfNode((location as ModuleDeclaration | SourceFile)).exports!), meaning & SymbolFlags.ModuleMember); + break; + case SyntaxKind.EnumDeclaration: + copySymbols((getSymbolOfNode((location as EnumDeclaration)).exports!), meaning & SymbolFlags.EnumMember); + break; + case SyntaxKind.ClassExpression: + const className = (location as ClassExpression).name; + if (className) { + copySymbol(location.symbol, meaning); + } + // this fall-through is necessary because we would like to handle + // type parameter inside class expression similar to how we handle it in classDeclaration and interface Declaration. + // falls through + case SyntaxKind.ClassDeclaration: + case SyntaxKind.InterfaceDeclaration: + // If we didn't come from static member of class or interface, + // add the type parameters into the symbol table + // (type parameters of classDeclaration/classExpression and interface are in member property of the symbol. + // Note: that the memberFlags come from previous iteration. + if (!isStatic) { + copySymbols(getMembersOfSymbol(getSymbolOfNode((location as ClassDeclaration | InterfaceDeclaration))), meaning & SymbolFlags.Type); + } + break; + case SyntaxKind.FunctionExpression: + const funcName = (location as FunctionExpression).name; + if (funcName) { + copySymbol(location.symbol, meaning); + } + break; + } + if (introducesArgumentsExoticObject(location)) { + copySymbol(argumentsSymbol, meaning); } + isStatic = hasModifier(location, ModifierFlags.Static); + location = location.parent; } - - return resolveName(location, reference.escapedText, SymbolFlags.Value | SymbolFlags.ExportValue | SymbolFlags.Alias, /*nodeNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ true); + copySymbols(globals, meaning); } - - function getReferencedValueDeclaration(referenceIn: Identifier): Declaration | undefined { - if (!isGeneratedIdentifier(referenceIn)) { - const reference = getParseTreeNode(referenceIn, isIdentifier); - if (reference) { - const symbol = getReferencedValueSymbol(reference); - if (symbol) { - return getExportSymbolOfValueSymbolIfExported(symbol).valueDeclaration; - } + /** + * Copy the given symbol into symbol tables if the symbol has the given meaning + * and it doesn't already existed in the symbol table + * @param key a key for storing in symbol table; if undefined, use symbol.name + * @param symbol the symbol to be added into symbol table + * @param meaning meaning of symbol to filter by before adding to symbol table + */ + function copySymbol(symbol: ts.Symbol, meaning: SymbolFlags): void { + if (getCombinedLocalAndExportSymbolFlags(symbol) & meaning) { + const id = symbol.escapedName; + // We will copy all symbol regardless of its reserved name because + // symbolsToArray will check whether the key is a reserved name and + // it will not copy symbol with reserved name to the array + if (!symbols.has(id)) { + symbols.set(id, symbol); } } - - return undefined; } - - function isLiteralConstDeclaration(node: VariableDeclaration | PropertyDeclaration | PropertySignature | ParameterDeclaration): boolean { - if (isDeclarationReadonly(node) || isVariableDeclaration(node) && isVarConst(node)) { - return isFreshLiteralType(getTypeOfSymbol(getSymbolOfNode(node))); + function copySymbols(source: SymbolTable, meaning: SymbolFlags): void { + if (meaning) { + source.forEach(symbol => { + copySymbol(symbol, meaning); + }); } - return false; } - - function literalTypeToNode(type: FreshableType, enclosing: Node, tracker: SymbolTracker): Expression { - const enumResult = type.flags & TypeFlags.EnumLiteral ? nodeBuilder.symbolToExpression(type.symbol, SymbolFlags.Value, enclosing, /*flags*/ undefined, tracker) - : type === trueType ? createTrue() : type === falseType && createFalse(); - return enumResult || createLiteral((type as LiteralType).value); + } + function isTypeDeclarationName(name: Node): boolean { + return name.kind === SyntaxKind.Identifier && + isTypeDeclaration(name.parent) && + name.parent.name === name; + } + function isTypeDeclaration(node: Node): node is TypeParameterDeclaration | ClassDeclaration | InterfaceDeclaration | TypeAliasDeclaration | EnumDeclaration | ImportClause | ImportSpecifier | ExportSpecifier { + switch (node.kind) { + case SyntaxKind.TypeParameter: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.EnumDeclaration: + return true; + case SyntaxKind.ImportClause: + return (node as ImportClause).isTypeOnly; + case SyntaxKind.ImportSpecifier: + case SyntaxKind.ExportSpecifier: + return (node as ImportSpecifier | ExportSpecifier).parent.parent.isTypeOnly; + default: + return false; } - - function createLiteralConstValue(node: VariableDeclaration | PropertyDeclaration | PropertySignature | ParameterDeclaration, tracker: SymbolTracker) { - const type = getTypeOfSymbol(getSymbolOfNode(node)); - return literalTypeToNode(type, node, tracker); + } + // True if the given identifier is part of a type reference + function isTypeReferenceIdentifier(node: EntityName): boolean { + while (node.parent.kind === SyntaxKind.QualifiedName) { + node = (node.parent as QualifiedName); } - - function getJsxFactoryEntity(location: Node) { - return location ? (getJsxNamespace(location), (getSourceFileOfNode(location).localJsxFactory || _jsxFactoryEntity)) : _jsxFactoryEntity; + return node.parent.kind === SyntaxKind.TypeReference; + } + function isHeritageClauseElementIdentifier(node: Node): boolean { + while (node.parent.kind === SyntaxKind.PropertyAccessExpression) { + node = node.parent; } - - function createResolver(): EmitResolver { - // this variable and functions that use it are deliberately moved here from the outer scope - // to avoid scope pollution - const resolvedTypeReferenceDirectives = host.getResolvedTypeReferenceDirectives(); - let fileToDirective: Map; - if (resolvedTypeReferenceDirectives) { - // populate reverse mapping: file path -> type reference directive that was resolved to this file - fileToDirective = createMap(); - resolvedTypeReferenceDirectives.forEach((resolvedDirective, key) => { - if (!resolvedDirective || !resolvedDirective.resolvedFileName) { - return; - } - const file = host.getSourceFile(resolvedDirective.resolvedFileName)!; - // Add the transitive closure of path references loaded by this file (as long as they are not) - // part of an existing type reference. - addReferencedFilesToTypeDirective(file, key); - }); - } - - return { - getReferencedExportContainer, - getReferencedImportDeclaration, - getReferencedDeclarationWithCollidingName, - isDeclarationWithCollidingName, - isValueAliasDeclaration: node => { - node = getParseTreeNode(node); - // Synthesized nodes are always treated like values. - return node ? isValueAliasDeclaration(node) : true; - }, - hasGlobalName, - isReferencedAliasDeclaration: (node, checkChildren?) => { - node = getParseTreeNode(node); - // Synthesized nodes are always treated as referenced. - return node ? isReferencedAliasDeclaration(node, checkChildren) : true; - }, - getNodeCheckFlags: node => { - node = getParseTreeNode(node); - return node ? getNodeCheckFlags(node) : 0; - }, - isTopLevelValueImportEqualsWithEntityName, - isDeclarationVisible, - isImplementationOfOverload, - isRequiredInitializedParameter, - isOptionalUninitializedParameterProperty, - isExpandoFunctionDeclaration, - getPropertiesOfContainerFunction, - createTypeOfDeclaration, - createReturnTypeOfSignatureDeclaration, - createTypeOfExpression, - createLiteralConstValue, - isSymbolAccessible, - isEntityNameVisible, - getConstantValue: nodeIn => { - const node = getParseTreeNode(nodeIn, canHaveConstantValue); - return node ? getConstantValue(node) : undefined; - }, - collectLinkedAliases, - getReferencedValueDeclaration, - getTypeReferenceSerializationKind, - isOptionalParameter, - moduleExportsSomeValue, - isArgumentsLocalBinding, - getExternalModuleFileFromDeclaration, - getTypeReferenceDirectivesForEntityName, - getTypeReferenceDirectivesForSymbol, - isLiteralConstDeclaration, - isLateBound: (nodeIn: Declaration): nodeIn is LateBoundDeclaration => { - const node = getParseTreeNode(nodeIn, isDeclaration); - const symbol = node && getSymbolOfNode(node); - return !!(symbol && getCheckFlags(symbol) & CheckFlags.Late); - }, - getJsxFactoryEntity, - getAllAccessorDeclarations(accessor: AccessorDeclaration): AllAccessorDeclarations { - accessor = getParseTreeNode(accessor, isGetOrSetAccessorDeclaration)!; // TODO: GH#18217 - const otherKind = accessor.kind === SyntaxKind.SetAccessor ? SyntaxKind.GetAccessor : SyntaxKind.SetAccessor; - const otherAccessor = getDeclarationOfKind(getSymbolOfNode(accessor), otherKind); - const firstAccessor = otherAccessor && (otherAccessor.pos < accessor.pos) ? otherAccessor : accessor; - const secondAccessor = otherAccessor && (otherAccessor.pos < accessor.pos) ? accessor : otherAccessor; - const setAccessor = accessor.kind === SyntaxKind.SetAccessor ? accessor : otherAccessor as SetAccessorDeclaration; - const getAccessor = accessor.kind === SyntaxKind.GetAccessor ? accessor : otherAccessor as GetAccessorDeclaration; - return { - firstAccessor, - secondAccessor, - setAccessor, - getAccessor - }; - }, - getSymbolOfExternalModuleSpecifier: moduleName => resolveExternalModuleNameWorker(moduleName, moduleName, /*moduleNotFoundError*/ undefined), - isBindingCapturedByNode: (node, decl) => { - const parseNode = getParseTreeNode(node); - const parseDecl = getParseTreeNode(decl); - return !!parseNode && !!parseDecl && (isVariableDeclaration(parseDecl) || isBindingElement(parseDecl)) && isBindingCapturedByNode(parseNode, parseDecl); - }, - getDeclarationStatementsForSourceFile: (node, flags, tracker, bundled) => { - const n = getParseTreeNode(node) as SourceFile; - Debug.assert(n && n.kind === SyntaxKind.SourceFile, "Non-sourcefile node passed into getDeclarationsForSourceFile"); - const sym = getSymbolOfNode(node); - if (!sym) { - return !node.locals ? [] : nodeBuilder.symbolTableToDeclarationStatements(node.locals, node, flags, tracker, bundled); - } - return !sym.exports ? [] : nodeBuilder.symbolTableToDeclarationStatements(sym.exports, node, flags, tracker, bundled); - } - }; - - function isInHeritageClause(node: PropertyAccessEntityNameExpression) { - return node.parent && node.parent.kind === SyntaxKind.ExpressionWithTypeArguments && node.parent.parent && node.parent.parent.kind === SyntaxKind.HeritageClause; + return node.parent.kind === SyntaxKind.ExpressionWithTypeArguments; + } + function forEachEnclosingClass(node: Node, callback: (node: Node) => T | undefined): T | undefined { + let result: T | undefined; + while (true) { + node = (getContainingClass(node)!); + if (!node) + break; + if (result = callback(node)) + break; + } + return result; + } + function isNodeUsedDuringClassInitialization(node: Node) { + return !!findAncestor(node, element => { + if (isConstructorDeclaration(element) && nodeIsPresent(element.body) || isPropertyDeclaration(element)) { + return true; } - - // defined here to avoid outer scope pollution - function getTypeReferenceDirectivesForEntityName(node: EntityNameOrEntityNameExpression): string[] | undefined { - // program does not have any files with type reference directives - bail out - if (!fileToDirective) { - return undefined; - } - // property access can only be used as values, or types when within an expression with type arguments inside a heritage clause - // qualified names can only be used as types\namespaces - // identifiers are treated as values only if they appear in type queries - let meaning = SymbolFlags.Type | SymbolFlags.Namespace; - if ((node.kind === SyntaxKind.Identifier && isInTypeQuery(node)) || (node.kind === SyntaxKind.PropertyAccessExpression && !isInHeritageClause(node))) { - meaning = SymbolFlags.Value | SymbolFlags.ExportValue; - } - - const symbol = resolveEntityName(node, meaning, /*ignoreErrors*/ true); - return symbol && symbol !== unknownSymbol ? getTypeReferenceDirectivesForSymbol(symbol, meaning) : undefined; + else if (isClassLike(element) || isFunctionLikeDeclaration(element)) { + return "quit"; } - - // defined here to avoid outer scope pollution - function getTypeReferenceDirectivesForSymbol(symbol: Symbol, meaning?: SymbolFlags): string[] | undefined { - // program does not have any files with type reference directives - bail out - if (!fileToDirective) { - return undefined; - } - if (!isSymbolFromTypeDeclarationFile(symbol)) { - return undefined; - } - // check what declarations in the symbol can contribute to the target meaning - let typeReferenceDirectives: string[] | undefined; - for (const decl of symbol.declarations) { - // check meaning of the local symbol to see if declaration needs to be analyzed further - if (decl.symbol && decl.symbol.flags & meaning!) { - const file = getSourceFileOfNode(decl); - const typeReferenceDirective = fileToDirective.get(file.path); - if (typeReferenceDirective) { - (typeReferenceDirectives || (typeReferenceDirectives = [])).push(typeReferenceDirective); - } - else { - // found at least one entry that does not originate from type reference directive - return undefined; - } - } + return false; + }); + } + function isNodeWithinClass(node: Node, classDeclaration: ClassLikeDeclaration) { + return !!forEachEnclosingClass(node, n => n === classDeclaration); + } + function getLeftSideOfImportEqualsOrExportAssignment(nodeOnRightSide: EntityName): ImportEqualsDeclaration | ExportAssignment | undefined { + while (nodeOnRightSide.parent.kind === SyntaxKind.QualifiedName) { + nodeOnRightSide = (nodeOnRightSide.parent); + } + if (nodeOnRightSide.parent.kind === SyntaxKind.ImportEqualsDeclaration) { + return (nodeOnRightSide.parent).moduleReference === nodeOnRightSide ? nodeOnRightSide.parent : undefined; + } + if (nodeOnRightSide.parent.kind === SyntaxKind.ExportAssignment) { + return (nodeOnRightSide.parent).expression === (nodeOnRightSide) ? nodeOnRightSide.parent : undefined; + } + return undefined; + } + function isInRightSideOfImportOrExportAssignment(node: EntityName) { + return getLeftSideOfImportEqualsOrExportAssignment(node) !== undefined; + } + function getSpecialPropertyAssignmentSymbolFromEntityName(entityName: EntityName | PropertyAccessExpression) { + const specialPropertyAssignmentKind = getAssignmentDeclarationKind((entityName.parent.parent as BinaryExpression)); + switch (specialPropertyAssignmentKind) { + case AssignmentDeclarationKind.ExportsProperty: + case AssignmentDeclarationKind.PrototypeProperty: + return getSymbolOfNode(entityName.parent); + case AssignmentDeclarationKind.ThisProperty: + case AssignmentDeclarationKind.ModuleExports: + case AssignmentDeclarationKind.Property: + return getSymbolOfNode(entityName.parent.parent); + } + } + function isImportTypeQualifierPart(node: EntityName): ImportTypeNode | undefined { + let parent = node.parent; + while (isQualifiedName(parent)) { + node = parent; + parent = parent.parent; + } + if (parent && parent.kind === SyntaxKind.ImportType && (parent as ImportTypeNode).qualifier === node) { + return parent as ImportTypeNode; + } + return undefined; + } + function getSymbolOfNameOrPropertyAccessExpression(name: EntityName | PrivateIdentifier | PropertyAccessExpression): ts.Symbol | undefined { + if (isDeclarationName(name)) { + return getSymbolOfNode(name.parent); + } + if (isInJSFile(name) && + name.parent.kind === SyntaxKind.PropertyAccessExpression && + name.parent === (name.parent.parent as BinaryExpression).left) { + // Check if this is a special property assignment + if (!isPrivateIdentifier(name)) { + const specialPropertyAssignmentSymbol = getSpecialPropertyAssignmentSymbolFromEntityName(name); + if (specialPropertyAssignmentSymbol) { + return specialPropertyAssignmentSymbol; + } + } + } + if (name.parent.kind === SyntaxKind.ExportAssignment && isEntityNameExpression(name)) { + // Even an entity name expression that doesn't resolve as an entityname may still typecheck as a property access expression + const success = resolveEntityName(name, + /*all meanings*/ SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace | SymbolFlags.Alias, /*ignoreErrors*/ true); + if (success && success !== unknownSymbol) { + return success; + } + } + else if (!isPropertyAccessExpression(name) && !isPrivateIdentifier(name) && isInRightSideOfImportOrExportAssignment(name)) { + // Since we already checked for ExportAssignment, this really could only be an Import + const importEqualsDeclaration = getAncestor(name, SyntaxKind.ImportEqualsDeclaration); + Debug.assert(importEqualsDeclaration !== undefined); + return getSymbolOfPartOfRightHandSideOfImportEquals(name, /*dontResolveAlias*/ true); + } + if (!isPropertyAccessExpression(name) && !isPrivateIdentifier(name)) { + const possibleImportNode = isImportTypeQualifierPart(name); + if (possibleImportNode) { + getTypeFromTypeNode(possibleImportNode); + const sym = getNodeLinks(name).resolvedSymbol; + return sym === unknownSymbol ? undefined : sym; + } + } + while (isRightSideOfQualifiedNameOrPropertyAccess(name)) { + name = (name.parent); + } + if (isHeritageClauseElementIdentifier(name)) { + let meaning = SymbolFlags.None; + // In an interface or class, we're definitely interested in a type. + if (name.parent.kind === SyntaxKind.ExpressionWithTypeArguments) { + meaning = SymbolFlags.Type; + // In a class 'extends' clause we are also looking for a value. + if (isExpressionWithTypeArgumentsInClassExtendsClause(name.parent)) { + meaning |= SymbolFlags.Value; } - return typeReferenceDirectives; } - - function isSymbolFromTypeDeclarationFile(symbol: Symbol): boolean { - // bail out if symbol does not have associated declarations (i.e. this is transient symbol created for property in binding pattern) - if (!symbol.declarations) { - return false; - } - - // walk the parent chain for symbols to make sure that top level parent symbol is in the global scope - // external modules cannot define or contribute to type declaration files - let current = symbol; - while (true) { - const parent = getParentOfSymbol(current); - if (parent) { - current = parent; - } - else { - break; - } - } - - if (current.valueDeclaration && current.valueDeclaration.kind === SyntaxKind.SourceFile && current.flags & SymbolFlags.ValueModule) { - return false; - } - - // check that at least one declaration of top level symbol originates from type declaration file - for (const decl of symbol.declarations) { - const file = getSourceFileOfNode(decl); - if (fileToDirective.has(file.path)) { - return true; - } - } - return false; + else { + meaning = SymbolFlags.Namespace; } - - function addReferencedFilesToTypeDirective(file: SourceFile, key: string) { - if (fileToDirective.has(file.path)) return; - fileToDirective.set(file.path, key); - for (const { fileName } of file.referencedFiles) { - const resolvedFile = resolveTripleslashReference(fileName, file.originalFileName); - const referencedFile = host.getSourceFile(resolvedFile); - if (referencedFile) { - addReferencedFilesToTypeDirective(referencedFile, key); - } - } + meaning |= SymbolFlags.Alias; + const entityNameSymbol = isEntityNameExpression(name) ? resolveEntityName(name, meaning) : undefined; + if (entityNameSymbol) { + return entityNameSymbol; } } - - function getExternalModuleFileFromDeclaration(declaration: AnyImportOrReExport | ModuleDeclaration | ImportTypeNode): SourceFile | undefined { - const specifier = declaration.kind === SyntaxKind.ModuleDeclaration ? tryCast(declaration.name, isStringLiteral) : getExternalModuleName(declaration); - const moduleSymbol = resolveExternalModuleNameWorker(specifier!, specifier!, /*moduleNotFoundError*/ undefined); // TODO: GH#18217 - if (!moduleSymbol) { - return undefined; - } - return getDeclarationOfKind(moduleSymbol, SyntaxKind.SourceFile); + if (name.parent.kind === SyntaxKind.JSDocParameterTag) { + return getParameterSymbolFromJSDoc((name.parent as JSDocParameterTag)); } - - function initializeTypeChecker() { - // Bind all source files and propagate errors - for (const file of host.getSourceFiles()) { - bindSourceFile(file, compilerOptions); + if (name.parent.kind === SyntaxKind.TypeParameter && name.parent.parent.kind === SyntaxKind.JSDocTemplateTag) { + Debug.assert(!isInJSFile(name)); // Otherwise `isDeclarationName` would have been true. + const typeParameter = getTypeParameterFromJsDoc((name.parent as TypeParameterDeclaration & { + parent: JSDocTemplateTag; + })); + return typeParameter && typeParameter.symbol; + } + if (isExpressionNode(name)) { + if (nodeIsMissing(name)) { + // Missing entity name. + return undefined; } - - amalgamatedDuplicates = createMap(); - - // Initialize global symbol table - let augmentations: (readonly (StringLiteral | Identifier)[])[] | undefined; - for (const file of host.getSourceFiles()) { - if (file.redirectInfo) { - continue; - } - if (!isExternalOrCommonJsModule(file)) { - // It is an error for a non-external-module (i.e. script) to declare its own `globalThis`. - // We can't use `builtinGlobals` for this due to synthetic expando-namespace generation in JS files. - const fileGlobalThisSymbol = file.locals!.get("globalThis" as __String); - if (fileGlobalThisSymbol) { - for (const declaration of fileGlobalThisSymbol.declarations) { - diagnostics.add(createDiagnosticForNode(declaration, Diagnostics.Declaration_name_conflicts_with_built_in_global_identifier_0, "globalThis")); - } - } - mergeSymbolTable(globals, file.locals!); - } - if (file.jsGlobalAugmentations) { - mergeSymbolTable(globals, file.jsGlobalAugmentations); - } - if (file.patternAmbientModules && file.patternAmbientModules.length) { - patternAmbientModules = concatenate(patternAmbientModules, file.patternAmbientModules); + if (name.kind === SyntaxKind.Identifier) { + if (isJSXTagName(name) && isJsxIntrinsicIdentifier(name)) { + const symbol = getIntrinsicTagSymbol((name.parent)); + return symbol === unknownSymbol ? undefined : symbol; } - if (file.moduleAugmentations.length) { - (augmentations || (augmentations = [])).push(file.moduleAugmentations); + return resolveEntityName(name, SymbolFlags.Value, /*ignoreErrors*/ false, /*dontResolveAlias*/ true); + } + else if (name.kind === SyntaxKind.PropertyAccessExpression || name.kind === SyntaxKind.QualifiedName) { + const links = getNodeLinks(name); + if (links.resolvedSymbol) { + return links.resolvedSymbol; } - if (file.symbol && file.symbol.globalExports) { - // Merge in UMD exports with first-in-wins semantics (see #9771) - const source = file.symbol.globalExports; - source.forEach((sourceSymbol, id) => { - if (!globals.has(id)) { - globals.set(id, sourceSymbol); - } - }); + if (name.kind === SyntaxKind.PropertyAccessExpression) { + checkPropertyAccessExpression(name); } - } - - // We do global augmentations separately from module augmentations (and before creating global types) because they - // 1. Affect global types. We won't have the correct global types until global augmentations are merged. Also, - // 2. Module augmentation instantiation requires creating the type of a module, which, in turn, can require - // checking for an export or property on the module (if export=) which, in turn, can fall back to the - // apparent type of the module - either globalObjectType or globalFunctionType - which wouldn't exist if we - // did module augmentations prior to finalizing the global types. - if (augmentations) { - // merge _global_ module augmentations. - // this needs to be done after global symbol table is initialized to make sure that all ambient modules are indexed - for (const list of augmentations) { - for (const augmentation of list) { - if (!isGlobalScopeAugmentation(augmentation.parent as ModuleDeclaration)) continue; - mergeModuleAugmentation(augmentation); - } + else { + checkQualifiedName(name); } + return links.resolvedSymbol; } - - // Setup global builtins - addToSymbolTable(globals, builtinGlobals, Diagnostics.Declaration_name_conflicts_with_built_in_global_identifier_0); - - getSymbolLinks(undefinedSymbol).type = undefinedWideningType; - getSymbolLinks(argumentsSymbol).type = getGlobalType("IArguments" as __String, /*arity*/ 0, /*reportErrors*/ true); - getSymbolLinks(unknownSymbol).type = errorType; - getSymbolLinks(globalThisSymbol).type = createObjectType(ObjectFlags.Anonymous, globalThisSymbol); - - // Initialize special types - globalArrayType = getGlobalType("Array" as __String, /*arity*/ 1, /*reportErrors*/ true); - globalObjectType = getGlobalType("Object" as __String, /*arity*/ 0, /*reportErrors*/ true); - globalFunctionType = getGlobalType("Function" as __String, /*arity*/ 0, /*reportErrors*/ true); - globalCallableFunctionType = strictBindCallApply && getGlobalType("CallableFunction" as __String, /*arity*/ 0, /*reportErrors*/ true) || globalFunctionType; - globalNewableFunctionType = strictBindCallApply && getGlobalType("NewableFunction" as __String, /*arity*/ 0, /*reportErrors*/ true) || globalFunctionType; - globalStringType = getGlobalType("String" as __String, /*arity*/ 0, /*reportErrors*/ true); - globalNumberType = getGlobalType("Number" as __String, /*arity*/ 0, /*reportErrors*/ true); - globalBooleanType = getGlobalType("Boolean" as __String, /*arity*/ 0, /*reportErrors*/ true); - globalRegExpType = getGlobalType("RegExp" as __String, /*arity*/ 0, /*reportErrors*/ true); - anyArrayType = createArrayType(anyType); - - autoArrayType = createArrayType(autoType); - if (autoArrayType === emptyObjectType) { - // autoArrayType is used as a marker, so even if global Array type is not defined, it needs to be a unique type - autoArrayType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined); + } + else if (isTypeReferenceIdentifier((name))) { + const meaning = name.parent.kind === SyntaxKind.TypeReference ? SymbolFlags.Type : SymbolFlags.Namespace; + return resolveEntityName((name), meaning, /*ignoreErrors*/ false, /*dontResolveAlias*/ true); + } + if (name.parent.kind === SyntaxKind.TypePredicate) { + return resolveEntityName((name), /*meaning*/ SymbolFlags.FunctionScopedVariable); + } + // Do we want to return undefined here? + return undefined; + } + function getSymbolAtLocation(node: Node, ignoreErrors?: boolean): ts.Symbol | undefined { + if (node.kind === SyntaxKind.SourceFile) { + return isExternalModule((node)) ? getMergedSymbol(node.symbol) : undefined; + } + const { parent } = node; + const grandParent = parent.parent; + if (node.flags & NodeFlags.InWithStatement) { + // We cannot answer semantic questions within a with block, do not proceed any further + return undefined; + } + if (isDeclarationNameOrImportPropertyName(node)) { + // This is a declaration, call getSymbolOfNode + const parentSymbol = getSymbolOfNode(parent)!; + return isImportOrExportSpecifier(node.parent) && node.parent.propertyName === node + ? getImmediateAliasedSymbol(parentSymbol) + : parentSymbol; + } + else if (isLiteralComputedPropertyDeclarationName(node)) { + return getSymbolOfNode(parent.parent); + } + if (node.kind === SyntaxKind.Identifier) { + if (isInRightSideOfImportOrExportAssignment((node))) { + return getSymbolOfNameOrPropertyAccessExpression((node)); } - - globalReadonlyArrayType = getGlobalTypeOrUndefined("ReadonlyArray" as __String, /*arity*/ 1) || globalArrayType; - anyReadonlyArrayType = globalReadonlyArrayType ? createTypeFromGenericGlobalType(globalReadonlyArrayType, [anyType]) : anyArrayType; - globalThisType = getGlobalTypeOrUndefined("ThisType" as __String, /*arity*/ 1); - - if (augmentations) { - // merge _nonglobal_ module augmentations. - // this needs to be done after global symbol table is initialized to make sure that all ambient modules are indexed - for (const list of augmentations) { - for (const augmentation of list) { - if (isGlobalScopeAugmentation(augmentation.parent as ModuleDeclaration)) continue; - mergeModuleAugmentation(augmentation); - } + else if (parent.kind === SyntaxKind.BindingElement && + grandParent.kind === SyntaxKind.ObjectBindingPattern && + node === (parent).propertyName) { + const typeOfPattern = getTypeOfNode(grandParent); + const propertyDeclaration = getPropertyOfType(typeOfPattern, (node).escapedText); + if (propertyDeclaration) { + return propertyDeclaration; } } - - amalgamatedDuplicates.forEach(({ firstFile, secondFile, conflictingSymbols }) => { - // If not many things conflict, issue individual errors - if (conflictingSymbols.size < 8) { - conflictingSymbols.forEach(({ isBlockScoped, firstFileLocations, secondFileLocations }, symbolName) => { - const message = isBlockScoped ? Diagnostics.Cannot_redeclare_block_scoped_variable_0 : Diagnostics.Duplicate_identifier_0; - for (const node of firstFileLocations) { - addDuplicateDeclarationError(node, message, symbolName, secondFileLocations); - } - for (const node of secondFileLocations) { - addDuplicateDeclarationError(node, message, symbolName, firstFileLocations); - } - }); - } - else { - // Otherwise issue top-level error since the files appear very identical in terms of what they contain - const list = arrayFrom(conflictingSymbols.keys()).join(", "); - diagnostics.add(addRelatedInfo( - createDiagnosticForNode(firstFile, Diagnostics.Definitions_of_the_following_identifiers_conflict_with_those_in_another_file_Colon_0, list), - createDiagnosticForNode(secondFile, Diagnostics.Conflicts_are_in_this_file) - )); - diagnostics.add(addRelatedInfo( - createDiagnosticForNode(secondFile, Diagnostics.Definitions_of_the_following_identifiers_conflict_with_those_in_another_file_Colon_0, list), - createDiagnosticForNode(firstFile, Diagnostics.Conflicts_are_in_this_file) - )); - } - }); - amalgamatedDuplicates = undefined; } - - function checkExternalEmitHelpers(location: Node, helpers: ExternalEmitHelpers) { - if ((requestedExternalEmitHelpers & helpers) !== helpers && compilerOptions.importHelpers) { - const sourceFile = getSourceFileOfNode(location); - if (isEffectiveExternalModule(sourceFile, compilerOptions) && !(location.flags & NodeFlags.Ambient)) { - const helpersModule = resolveHelpersModule(sourceFile, location); - if (helpersModule !== unknownSymbol) { - const uncheckedHelpers = helpers & ~requestedExternalEmitHelpers; - for (let helper = ExternalEmitHelpers.FirstEmitHelper; helper <= ExternalEmitHelpers.LastEmitHelper; helper <<= 1) { - if (uncheckedHelpers & helper) { - const name = getHelperName(helper); - const symbol = getSymbol(helpersModule.exports!, escapeLeadingUnderscores(name), SymbolFlags.Value); - if (!symbol) { - error(location, Diagnostics.This_syntax_requires_an_imported_helper_named_1_which_does_not_exist_in_0_Consider_upgrading_your_version_of_0, externalHelpersModuleNameText, name); - } - } - } - } - requestedExternalEmitHelpers |= helpers; + switch (node.kind) { + case SyntaxKind.Identifier: + case SyntaxKind.PrivateIdentifier: + case SyntaxKind.PropertyAccessExpression: + case SyntaxKind.QualifiedName: + return getSymbolOfNameOrPropertyAccessExpression((node)); + case SyntaxKind.ThisKeyword: + const container = getThisContainer(node, /*includeArrowFunctions*/ false); + if (isFunctionLike(container)) { + const sig = getSignatureFromDeclaration(container); + if (sig.thisParameter) { + return sig.thisParameter; + } + } + if (isInExpressionContext(node)) { + return checkExpression((node as Expression)).symbol; + } + // falls through + case SyntaxKind.ThisType: + return getTypeFromThisTypeNode((node as ThisExpression | ThisTypeNode)).symbol; + case SyntaxKind.SuperKeyword: + return checkExpression((node as Expression)).symbol; + case SyntaxKind.ConstructorKeyword: + // constructor keyword for an overload, should take us to the definition if it exist + const constructorDeclaration = node.parent; + if (constructorDeclaration && constructorDeclaration.kind === SyntaxKind.Constructor) { + return (constructorDeclaration.parent).symbol; } + return undefined; + case SyntaxKind.StringLiteral: + case SyntaxKind.NoSubstitutionTemplateLiteral: + // 1). import x = require("./mo/*gotToDefinitionHere*/d") + // 2). External module name in an import declaration + // 3). Dynamic import call or require in javascript + // 4). type A = import("./f/*gotToDefinitionHere*/oo") + if ((isExternalModuleImportEqualsDeclaration(node.parent.parent) && getExternalModuleImportEqualsDeclarationExpression(node.parent.parent) === node) || + ((node.parent.kind === SyntaxKind.ImportDeclaration || node.parent.kind === SyntaxKind.ExportDeclaration) && (node.parent).moduleSpecifier === node) || + ((isInJSFile(node) && isRequireCall(node.parent, /*checkArgumentIsStringLiteralLike*/ false)) || isImportCall(node.parent)) || + (isLiteralTypeNode(node.parent) && isLiteralImportTypeNode(node.parent.parent) && node.parent.parent.argument === node.parent)) { + return resolveExternalModuleName(node, (node), ignoreErrors); + } + if (isCallExpression(parent) && isBindableObjectDefinePropertyCall(parent) && parent.arguments[1] === node) { + return getSymbolOfNode(parent); + } + // falls through + case SyntaxKind.NumericLiteral: + // index access + const objectType = isElementAccessExpression(parent) + ? parent.argumentExpression === node ? getTypeOfExpression(parent.expression) : undefined + : isLiteralTypeNode(parent) && isIndexedAccessTypeNode(grandParent) + ? getTypeFromTypeNode(grandParent.objectType) + : undefined; + return objectType && getPropertyOfType(objectType, escapeLeadingUnderscores((node as StringLiteral | NumericLiteral).text)); + case SyntaxKind.DefaultKeyword: + case SyntaxKind.FunctionKeyword: + case SyntaxKind.EqualsGreaterThanToken: + case SyntaxKind.ClassKeyword: + return getSymbolOfNode(node.parent); + case SyntaxKind.ImportType: + return isLiteralImportTypeNode(node) ? getSymbolAtLocation(node.argument.literal, ignoreErrors) : undefined; + case SyntaxKind.ExportKeyword: + return isExportAssignment(node.parent) ? Debug.checkDefined(node.parent.symbol) : undefined; + default: + return undefined; + } + } + function getShorthandAssignmentValueSymbol(location: Node): ts.Symbol | undefined { + if (location && location.kind === SyntaxKind.ShorthandPropertyAssignment) { + return resolveEntityName((location).name, SymbolFlags.Value | SymbolFlags.Alias); + } + return undefined; + } + /** Returns the target of an export specifier without following aliases */ + function getExportSpecifierLocalTargetSymbol(node: ExportSpecifier): ts.Symbol | undefined { + return node.parent.parent.moduleSpecifier ? + getExternalModuleMember(node.parent.parent, node) : + resolveEntityName(node.propertyName || node.name, SymbolFlags.Value | SymbolFlags.Type | SymbolFlags.Namespace | SymbolFlags.Alias); + } + function getTypeOfNode(node: Node): ts.Type { + if (node.flags & NodeFlags.InWithStatement) { + // We cannot answer semantic questions within a with block, do not proceed any further + return errorType; + } + const classDecl = tryGetClassImplementingOrExtendingExpressionWithTypeArguments(node); + const classType = classDecl && getDeclaredTypeOfClassOrInterface(getSymbolOfNode(classDecl.class)); + if (isPartOfTypeNode(node)) { + const typeFromTypeNode = getTypeFromTypeNode((node)); + return classType ? getTypeWithThisArgument(typeFromTypeNode, classType.thisType) : typeFromTypeNode; + } + if (isExpressionNode(node)) { + return getRegularTypeOfExpression((node)); + } + if (classType && !classDecl!.isImplements) { + // A SyntaxKind.ExpressionWithTypeArguments is considered a type node, except when it occurs in the + // extends clause of a class. We handle that case here. + const baseType = firstOrUndefined(getBaseTypes(classType)); + return baseType ? getTypeWithThisArgument(baseType, classType.thisType) : errorType; + } + if (isTypeDeclaration(node)) { + // In this case, we call getSymbolOfNode instead of getSymbolAtLocation because it is a declaration + const symbol = getSymbolOfNode(node); + return getDeclaredTypeOfSymbol(symbol); + } + if (isTypeDeclarationName(node)) { + const symbol = getSymbolAtLocation(node); + return symbol ? getDeclaredTypeOfSymbol(symbol) : errorType; + } + if (isDeclaration(node)) { + // In this case, we call getSymbolOfNode instead of getSymbolAtLocation because it is a declaration + const symbol = getSymbolOfNode(node); + return getTypeOfSymbol(symbol); + } + if (isDeclarationNameOrImportPropertyName(node)) { + const symbol = getSymbolAtLocation(node); + if (symbol) { + return getTypeOfSymbol(symbol); } + return errorType; } - - function getHelperName(helper: ExternalEmitHelpers) { - switch (helper) { - case ExternalEmitHelpers.Extends: return "__extends"; - case ExternalEmitHelpers.Assign: return "__assign"; - case ExternalEmitHelpers.Rest: return "__rest"; - case ExternalEmitHelpers.Decorate: return "__decorate"; - case ExternalEmitHelpers.Metadata: return "__metadata"; - case ExternalEmitHelpers.Param: return "__param"; - case ExternalEmitHelpers.Awaiter: return "__awaiter"; - case ExternalEmitHelpers.Generator: return "__generator"; - case ExternalEmitHelpers.Values: return "__values"; - case ExternalEmitHelpers.Read: return "__read"; - case ExternalEmitHelpers.Spread: return "__spread"; - case ExternalEmitHelpers.SpreadArrays: return "__spreadArrays"; - case ExternalEmitHelpers.Await: return "__await"; - case ExternalEmitHelpers.AsyncGenerator: return "__asyncGenerator"; - case ExternalEmitHelpers.AsyncDelegator: return "__asyncDelegator"; - case ExternalEmitHelpers.AsyncValues: return "__asyncValues"; - case ExternalEmitHelpers.ExportStar: return "__exportStar"; - case ExternalEmitHelpers.MakeTemplateObject: return "__makeTemplateObject"; - case ExternalEmitHelpers.ClassPrivateFieldGet: return "__classPrivateFieldGet"; - case ExternalEmitHelpers.ClassPrivateFieldSet: return "__classPrivateFieldSet"; - case ExternalEmitHelpers.CreateBinding: return "__createBinding"; - default: return Debug.fail("Unrecognized helper"); + if (isBindingPattern(node)) { + return getTypeForVariableLikeDeclaration(node.parent, /*includeOptionality*/ true) || errorType; + } + if (isInRightSideOfImportOrExportAssignment((node))) { + const symbol = getSymbolAtLocation(node); + if (symbol) { + const declaredType = getDeclaredTypeOfSymbol(symbol); + return declaredType !== errorType ? declaredType : getTypeOfSymbol(symbol); } } - - function resolveHelpersModule(node: SourceFile, errorNode: Node) { - if (!externalHelpersModule) { - externalHelpersModule = resolveExternalModule(node, externalHelpersModuleNameText, Diagnostics.This_syntax_requires_an_imported_helper_but_module_0_cannot_be_found, errorNode) || unknownSymbol; + return errorType; + } + // Gets the type of object literal or array literal of destructuring assignment. + // { a } from + // for ( { a } of elems) { + // } + // [ a ] from + // [a] = [ some array ...] + function getTypeOfAssignmentPattern(expr: AssignmentPattern): ts.Type | undefined { + Debug.assert(expr.kind === SyntaxKind.ObjectLiteralExpression || expr.kind === SyntaxKind.ArrayLiteralExpression); + // If this is from "for of" + // for ( { a } of elems) { + // } + if (expr.parent.kind === SyntaxKind.ForOfStatement) { + const iteratedType = checkRightHandSideOfForOf((expr.parent).expression, (expr.parent).awaitModifier); + return checkDestructuringAssignment(expr, iteratedType || errorType); + } + // If this is from "for" initializer + // for ({a } = elems[0];.....) { } + if (expr.parent.kind === SyntaxKind.BinaryExpression) { + const iteratedType = getTypeOfExpression((expr.parent).right); + return checkDestructuringAssignment(expr, iteratedType || errorType); + } + // If this is from nested object binding pattern + // for ({ skills: { primary, secondary } } = multiRobot, i = 0; i < 1; i++) { + if (expr.parent.kind === SyntaxKind.PropertyAssignment) { + const node = cast(expr.parent.parent, isObjectLiteralExpression); + const typeOfParentObjectLiteral = getTypeOfAssignmentPattern(node) || errorType; + const propertyIndex = indexOfNode(node.properties, expr.parent); + return checkObjectLiteralDestructuringPropertyAssignment(node, typeOfParentObjectLiteral, propertyIndex); + } + // Array literal assignment - array destructuring pattern + const node = cast(expr.parent, isArrayLiteralExpression); + // [{ property1: p1, property2 }] = elems; + const typeOfArrayLiteral = getTypeOfAssignmentPattern(node) || errorType; + const elementType = checkIteratedTypeOrElementType(IterationUse.Destructuring, typeOfArrayLiteral, undefinedType, expr.parent) || errorType; + return checkArrayLiteralDestructuringElementAssignment(node, typeOfArrayLiteral, node.elements.indexOf(expr), elementType); + } + // Gets the property symbol corresponding to the property in destructuring assignment + // 'property1' from + // for ( { property1: a } of elems) { + // } + // 'property1' at location 'a' from: + // [a] = [ property1, property2 ] + function getPropertySymbolOfDestructuringAssignment(location: Identifier) { + // Get the type of the object or array literal and then look for property of given name in the type + const typeOfObjectLiteral = getTypeOfAssignmentPattern(cast(location.parent.parent, isAssignmentPattern)); + return typeOfObjectLiteral && getPropertyOfType(typeOfObjectLiteral, location.escapedText); + } + function getRegularTypeOfExpression(expr: Expression): ts.Type { + if (isRightSideOfQualifiedNameOrPropertyAccess(expr)) { + expr = (expr.parent); + } + return getRegularTypeOfLiteralType(getTypeOfExpression(expr)); + } + /** + * Gets either the static or instance type of a class element, based on + * whether the element is declared as "static". + */ + function getParentTypeOfClassElement(node: ClassElement) { + const classSymbol = getSymbolOfNode(node.parent)!; + return hasModifier(node, ModifierFlags.Static) + ? getTypeOfSymbol(classSymbol) + : getDeclaredTypeOfSymbol(classSymbol); + } + function getClassElementPropertyKeyType(element: ClassElement) { + const name = element.name!; + switch (name.kind) { + case SyntaxKind.Identifier: + return getLiteralType(idText(name)); + case SyntaxKind.NumericLiteral: + case SyntaxKind.StringLiteral: + return getLiteralType(name.text); + case SyntaxKind.ComputedPropertyName: + const nameType = checkComputedPropertyName(name); + return isTypeAssignableToKind(nameType, TypeFlags.ESSymbolLike) ? nameType : stringType; + default: + return Debug.fail("Unsupported property name."); + } + } + // Return the list of properties of the given type, augmented with properties from Function + // if the type has call or construct signatures + function getAugmentedPropertiesOfType(type: ts.Type): ts.Symbol[] { + type = getApparentType(type); + const propsByName = createSymbolTable(getPropertiesOfType(type)); + const functionType = getSignaturesOfType(type, SignatureKind.Call).length ? globalCallableFunctionType : + getSignaturesOfType(type, SignatureKind.Construct).length ? globalNewableFunctionType : + undefined; + if (functionType) { + forEach(getPropertiesOfType(functionType), p => { + if (!propsByName.has(p.escapedName)) { + propsByName.set(p.escapedName, p); + } + }); + } + return getNamedMembers(propsByName); + } + function typeHasCallOrConstructSignatures(type: ts.Type): boolean { + return ts.typeHasCallOrConstructSignatures(type, checker); + } + function getRootSymbols(symbol: ts.Symbol): readonly ts.Symbol[] { + const roots = getImmediateRootSymbols(symbol); + return roots ? flatMap(roots, getRootSymbols) : [symbol]; + } + function getImmediateRootSymbols(symbol: ts.Symbol): readonly ts.Symbol[] | undefined { + if (getCheckFlags(symbol) & CheckFlags.Synthetic) { + return mapDefined(getSymbolLinks(symbol).containingType!.types, type => getPropertyOfType(type, symbol.escapedName)); + } + else if (symbol.flags & SymbolFlags.Transient) { + const { leftSpread, rightSpread, syntheticOrigin } = (symbol as TransientSymbol); + return leftSpread ? [leftSpread, rightSpread!] + : syntheticOrigin ? [syntheticOrigin] + : singleElementArray(tryGetAliasTarget(symbol)); + } + return undefined; + } + function tryGetAliasTarget(symbol: ts.Symbol): ts.Symbol | undefined { + let target: ts.Symbol | undefined; + let next: ts.Symbol | undefined = symbol; + while (next = getSymbolLinks(next).target) { + target = next; + } + return target; + } + // Emitter support + function isArgumentsLocalBinding(nodeIn: Identifier): boolean { + if (!isGeneratedIdentifier(nodeIn)) { + const node = getParseTreeNode(nodeIn, isIdentifier); + if (node) { + const isPropertyName = node.parent.kind === SyntaxKind.PropertyAccessExpression && (node.parent).name === node; + return !isPropertyName && getReferencedValueSymbol(node) === argumentsSymbol; } - return externalHelpersModule; } - - // GRAMMAR CHECKING - function checkGrammarDecoratorsAndModifiers(node: Node): boolean { - return checkGrammarDecorators(node) || checkGrammarModifiers(node); + return false; + } + function moduleExportsSomeValue(moduleReferenceExpression: Expression): boolean { + let moduleSymbol = resolveExternalModuleName(moduleReferenceExpression.parent, moduleReferenceExpression); + if (!moduleSymbol || isShorthandAmbientModuleSymbol(moduleSymbol)) { + // If the module is not found or is shorthand, assume that it may export a value. + return true; } - - function checkGrammarDecorators(node: Node): boolean { - if (!node.decorators) { - return false; - } - if (!nodeCanBeDecorated(node, node.parent, node.parent.parent)) { - if (node.kind === SyntaxKind.MethodDeclaration && !nodeIsPresent((node).body)) { - return grammarErrorOnFirstToken(node, Diagnostics.A_decorator_can_only_decorate_a_method_implementation_not_an_overload); - } - else { - return grammarErrorOnFirstToken(node, Diagnostics.Decorators_are_not_valid_here); + const hasExportAssignment = hasExportAssignmentSymbol(moduleSymbol); + // if module has export assignment then 'resolveExternalModuleSymbol' will return resolved symbol for export assignment + // otherwise it will return moduleSymbol itself + moduleSymbol = resolveExternalModuleSymbol(moduleSymbol); + const symbolLinks = getSymbolLinks(moduleSymbol); + if (symbolLinks.exportsSomeValue === undefined) { + // for export assignments - check if resolved symbol for RHS is itself a value + // otherwise - check if at least one export is value + symbolLinks.exportsSomeValue = hasExportAssignment + ? !!(moduleSymbol.flags & SymbolFlags.Value) + : forEachEntry(getExportsOfModule(moduleSymbol), isValue); + } + return symbolLinks.exportsSomeValue!; + function isValue(s: ts.Symbol): boolean { + s = resolveSymbol(s); + return s && !!(s.flags & SymbolFlags.Value); + } + } + function isNameOfModuleOrEnumDeclaration(node: Identifier) { + return isModuleOrEnumDeclaration(node.parent) && node === node.parent.name; + } + // When resolved as an expression identifier, if the given node references an exported entity, return the declaration + // node of the exported entity's container. Otherwise, return undefined. + function getReferencedExportContainer(nodeIn: Identifier, prefixLocals?: boolean): SourceFile | ModuleDeclaration | EnumDeclaration | undefined { + const node = getParseTreeNode(nodeIn, isIdentifier); + if (node) { + // When resolving the export container for the name of a module or enum + // declaration, we need to start resolution at the declaration's container. + // Otherwise, we could incorrectly resolve the export container as the + // declaration if it contains an exported member with the same name. + let symbol = getReferencedValueSymbol(node, /*startInDeclarationContainer*/ isNameOfModuleOrEnumDeclaration(node)); + if (symbol) { + if (symbol.flags & SymbolFlags.ExportValue) { + // If we reference an exported entity within the same module declaration, then whether + // we prefix depends on the kind of entity. SymbolFlags.ExportHasLocal encompasses all the + // kinds that we do NOT prefix. + const exportSymbol = getMergedSymbol(symbol.exportSymbol!); + if (!prefixLocals && exportSymbol.flags & SymbolFlags.ExportHasLocal && !(exportSymbol.flags & SymbolFlags.Variable)) { + return undefined; + } + symbol = exportSymbol; } - } - else if (node.kind === SyntaxKind.GetAccessor || node.kind === SyntaxKind.SetAccessor) { - const accessors = getAllAccessorDeclarations((node.parent).members, node); - if (accessors.firstAccessor.decorators && node === accessors.secondAccessor) { - return grammarErrorOnFirstToken(node, Diagnostics.Decorators_cannot_be_applied_to_multiple_get_Slashset_accessors_of_the_same_name); + const parentSymbol = getParentOfSymbol(symbol); + if (parentSymbol) { + if (parentSymbol.flags & SymbolFlags.ValueModule && parentSymbol.valueDeclaration.kind === SyntaxKind.SourceFile) { + const symbolFile = (parentSymbol.valueDeclaration); + const referenceFile = getSourceFileOfNode(node); + // If `node` accesses an export and that export isn't in the same file, then symbol is a namespace export, so return undefined. + const symbolIsUmdExport = symbolFile !== referenceFile; + return symbolIsUmdExport ? undefined : symbolFile; + } + return findAncestor(node.parent, (n): n is ModuleDeclaration | EnumDeclaration => isModuleOrEnumDeclaration(n) && getSymbolOfNode(n) === parentSymbol); } } - return false; } - - function checkGrammarModifiers(node: Node): boolean { - const quickResult = reportObviousModifierErrors(node); - if (quickResult !== undefined) { - return quickResult; - } - - let lastStatic: Node | undefined, lastDeclare: Node | undefined, lastAsync: Node | undefined, lastReadonly: Node | undefined; - let flags = ModifierFlags.None; - for (const modifier of node.modifiers!) { - if (modifier.kind !== SyntaxKind.ReadonlyKeyword) { - if (node.kind === SyntaxKind.PropertySignature || node.kind === SyntaxKind.MethodSignature) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_appear_on_a_type_member, tokenToString(modifier.kind)); + } + // When resolved as an expression identifier, if the given node references an import, return the declaration of + // that import. Otherwise, return undefined. + function getReferencedImportDeclaration(nodeIn: Identifier): Declaration | undefined { + const node = getParseTreeNode(nodeIn, isIdentifier); + if (node) { + const symbol = getReferencedValueSymbol(node); + // We should only get the declaration of an alias if there isn't a local value + // declaration for the symbol + if (isNonLocalAlias(symbol, /*excludes*/ SymbolFlags.Value) && !getTypeOnlyAliasDeclaration(symbol)) { + return getDeclarationOfAliasSymbol(symbol); + } + } + return undefined; + } + function isSymbolOfDestructuredElementOfCatchBinding(symbol: ts.Symbol) { + return isBindingElement(symbol.valueDeclaration) + && walkUpBindingElementsAndPatterns(symbol.valueDeclaration).parent.kind === SyntaxKind.CatchClause; + } + function isSymbolOfDeclarationWithCollidingName(symbol: ts.Symbol): boolean { + if (symbol.flags & SymbolFlags.BlockScoped && !isSourceFile(symbol.valueDeclaration)) { + const links = getSymbolLinks(symbol); + if (links.isDeclarationWithCollidingName === undefined) { + const container = getEnclosingBlockScopeContainer(symbol.valueDeclaration); + if (isStatementWithLocals(container) || isSymbolOfDestructuredElementOfCatchBinding(symbol)) { + const nodeLinks = getNodeLinks(symbol.valueDeclaration); + if (resolveName(container.parent, symbol.escapedName, SymbolFlags.Value, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false)) { + // redeclaration - always should be renamed + links.isDeclarationWithCollidingName = true; + } + else if (nodeLinks.flags & NodeCheckFlags.CapturedBlockScopedBinding) { + // binding is captured in the function + // should be renamed if: + // - binding is not top level - top level bindings never collide with anything + // AND + // - binding is not declared in loop, should be renamed to avoid name reuse across siblings + // let a, b + // { let x = 1; a = () => x; } + // { let x = 100; b = () => x; } + // console.log(a()); // should print '1' + // console.log(b()); // should print '100' + // OR + // - binding is declared inside loop but not in inside initializer of iteration statement or directly inside loop body + // * variables from initializer are passed to rewritten loop body as parameters so they are not captured directly + // * variables that are declared immediately in loop body will become top level variable after loop is rewritten and thus + // they will not collide with anything + const isDeclaredInLoop = nodeLinks.flags & NodeCheckFlags.BlockScopedBindingInLoop; + const inLoopInitializer = isIterationStatement(container, /*lookInLabeledStatements*/ false); + const inLoopBodyBlock = container.kind === SyntaxKind.Block && isIterationStatement(container.parent, /*lookInLabeledStatements*/ false); + links.isDeclarationWithCollidingName = !isBlockScopedContainerTopLevel(container) && (!isDeclaredInLoop || (!inLoopInitializer && !inLoopBodyBlock)); } - if (node.kind === SyntaxKind.IndexSignature) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_appear_on_an_index_signature, tokenToString(modifier.kind)); + else { + links.isDeclarationWithCollidingName = false; } } - switch (modifier.kind) { - case SyntaxKind.ConstKeyword: - if (node.kind !== SyntaxKind.EnumDeclaration) { - return grammarErrorOnNode(node, Diagnostics.A_class_member_cannot_have_the_0_keyword, tokenToString(SyntaxKind.ConstKeyword)); - } - break; - case SyntaxKind.PublicKeyword: - case SyntaxKind.ProtectedKeyword: - case SyntaxKind.PrivateKeyword: - const text = visibilityToString(modifierToFlag(modifier.kind)); - - if (flags & ModifierFlags.AccessibilityModifier) { - return grammarErrorOnNode(modifier, Diagnostics.Accessibility_modifier_already_seen); - } - else if (flags & ModifierFlags.Static) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_must_precede_1_modifier, text, "static"); - } - else if (flags & ModifierFlags.Readonly) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_must_precede_1_modifier, text, "readonly"); - } - else if (flags & ModifierFlags.Async) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_must_precede_1_modifier, text, "async"); - } - else if (node.parent.kind === SyntaxKind.ModuleBlock || node.parent.kind === SyntaxKind.SourceFile) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_appear_on_a_module_or_namespace_element, text); - } - else if (flags & ModifierFlags.Abstract) { - if (modifier.kind === SyntaxKind.PrivateKeyword) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_be_used_with_1_modifier, text, "abstract"); - } - else { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_must_precede_1_modifier, text, "abstract"); - } - } - else if (isPrivateIdentifierPropertyDeclaration(node)) { - return grammarErrorOnNode(modifier, Diagnostics.An_accessibility_modifier_cannot_be_used_with_a_private_identifier); - } - flags |= modifierToFlag(modifier.kind); - break; - - case SyntaxKind.StaticKeyword: - if (flags & ModifierFlags.Static) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_already_seen, "static"); - } - else if (flags & ModifierFlags.Readonly) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_must_precede_1_modifier, "static", "readonly"); - } - else if (flags & ModifierFlags.Async) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_must_precede_1_modifier, "static", "async"); - } - else if (node.parent.kind === SyntaxKind.ModuleBlock || node.parent.kind === SyntaxKind.SourceFile) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_appear_on_a_module_or_namespace_element, "static"); - } - else if (node.kind === SyntaxKind.Parameter) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_appear_on_a_parameter, "static"); - } - else if (flags & ModifierFlags.Abstract) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_be_used_with_1_modifier, "static", "abstract"); - } - else if (isPrivateIdentifierPropertyDeclaration(node)) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_be_used_with_a_private_identifier, "static"); - } - flags |= ModifierFlags.Static; - lastStatic = modifier; - break; - - case SyntaxKind.ReadonlyKeyword: - if (flags & ModifierFlags.Readonly) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_already_seen, "readonly"); - } - else if (node.kind !== SyntaxKind.PropertyDeclaration && node.kind !== SyntaxKind.PropertySignature && node.kind !== SyntaxKind.IndexSignature && node.kind !== SyntaxKind.Parameter) { - // If node.kind === SyntaxKind.Parameter, checkParameter report an error if it's not a parameter property. - return grammarErrorOnNode(modifier, Diagnostics.readonly_modifier_can_only_appear_on_a_property_declaration_or_index_signature); - } - flags |= ModifierFlags.Readonly; - lastReadonly = modifier; - break; - - case SyntaxKind.ExportKeyword: - if (flags & ModifierFlags.Export) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_already_seen, "export"); - } - else if (flags & ModifierFlags.Ambient) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_must_precede_1_modifier, "export", "declare"); - } - else if (flags & ModifierFlags.Abstract) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_must_precede_1_modifier, "export", "abstract"); - } - else if (flags & ModifierFlags.Async) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_must_precede_1_modifier, "export", "async"); - } - else if (isClassLike(node.parent)) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_appear_on_a_class_element, "export"); - } - else if (node.kind === SyntaxKind.Parameter) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_appear_on_a_parameter, "export"); - } - flags |= ModifierFlags.Export; - break; - case SyntaxKind.DefaultKeyword: - const container = node.parent.kind === SyntaxKind.SourceFile ? node.parent : node.parent.parent; - if (container.kind === SyntaxKind.ModuleDeclaration && !isAmbientModule(container)) { - return grammarErrorOnNode(modifier, Diagnostics.A_default_export_can_only_be_used_in_an_ECMAScript_style_module); - } - - flags |= ModifierFlags.Default; - break; - case SyntaxKind.DeclareKeyword: - if (flags & ModifierFlags.Ambient) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_already_seen, "declare"); - } - else if (flags & ModifierFlags.Async) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_be_used_in_an_ambient_context, "async"); - } - else if (isClassLike(node.parent) && !isPropertyDeclaration(node)) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_appear_on_a_class_element, "declare"); - } - else if (node.kind === SyntaxKind.Parameter) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_appear_on_a_parameter, "declare"); - } - else if ((node.parent.flags & NodeFlags.Ambient) && node.parent.kind === SyntaxKind.ModuleBlock) { - return grammarErrorOnNode(modifier, Diagnostics.A_declare_modifier_cannot_be_used_in_an_already_ambient_context); - } - else if (isPrivateIdentifierPropertyDeclaration(node)) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_be_used_with_a_private_identifier, "declare"); - } - flags |= ModifierFlags.Ambient; - lastDeclare = modifier; - break; - - case SyntaxKind.AbstractKeyword: - if (flags & ModifierFlags.Abstract) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_already_seen, "abstract"); - } - if (node.kind !== SyntaxKind.ClassDeclaration) { - if (node.kind !== SyntaxKind.MethodDeclaration && - node.kind !== SyntaxKind.PropertyDeclaration && - node.kind !== SyntaxKind.GetAccessor && - node.kind !== SyntaxKind.SetAccessor) { - return grammarErrorOnNode(modifier, Diagnostics.abstract_modifier_can_only_appear_on_a_class_method_or_property_declaration); - } - if (!(node.parent.kind === SyntaxKind.ClassDeclaration && hasModifier(node.parent, ModifierFlags.Abstract))) { - return grammarErrorOnNode(modifier, Diagnostics.Abstract_methods_can_only_appear_within_an_abstract_class); - } - if (flags & ModifierFlags.Static) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_be_used_with_1_modifier, "static", "abstract"); - } - if (flags & ModifierFlags.Private) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_be_used_with_1_modifier, "private", "abstract"); - } - } - if (isNamedDeclaration(node) && node.name.kind === SyntaxKind.PrivateIdentifier) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_be_used_with_a_private_identifier, "abstract"); - } - - flags |= ModifierFlags.Abstract; - break; - - case SyntaxKind.AsyncKeyword: - if (flags & ModifierFlags.Async) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_already_seen, "async"); - } - else if (flags & ModifierFlags.Ambient || node.parent.flags & NodeFlags.Ambient) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_be_used_in_an_ambient_context, "async"); - } - else if (node.kind === SyntaxKind.Parameter) { - return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_appear_on_a_parameter, "async"); - } - flags |= ModifierFlags.Async; - lastAsync = modifier; - break; - } } - - if (node.kind === SyntaxKind.Constructor) { - if (flags & ModifierFlags.Static) { - return grammarErrorOnNode(lastStatic!, Diagnostics._0_modifier_cannot_appear_on_a_constructor_declaration, "static"); - } - if (flags & ModifierFlags.Abstract) { - return grammarErrorOnNode(lastStatic!, Diagnostics._0_modifier_cannot_appear_on_a_constructor_declaration, "abstract"); // TODO: GH#18217 - } - else if (flags & ModifierFlags.Async) { - return grammarErrorOnNode(lastAsync!, Diagnostics._0_modifier_cannot_appear_on_a_constructor_declaration, "async"); - } - else if (flags & ModifierFlags.Readonly) { - return grammarErrorOnNode(lastReadonly!, Diagnostics._0_modifier_cannot_appear_on_a_constructor_declaration, "readonly"); + return links.isDeclarationWithCollidingName!; + } + return false; + } + // When resolved as an expression identifier, if the given node references a nested block scoped entity with + // a name that either hides an existing name or might hide it when compiled downlevel, + // return the declaration of that entity. Otherwise, return undefined. + function getReferencedDeclarationWithCollidingName(nodeIn: Identifier): Declaration | undefined { + if (!isGeneratedIdentifier(nodeIn)) { + const node = getParseTreeNode(nodeIn, isIdentifier); + if (node) { + const symbol = getReferencedValueSymbol(node); + if (symbol && isSymbolOfDeclarationWithCollidingName(symbol)) { + return symbol.valueDeclaration; } - return false; } - else if ((node.kind === SyntaxKind.ImportDeclaration || node.kind === SyntaxKind.ImportEqualsDeclaration) && flags & ModifierFlags.Ambient) { - return grammarErrorOnNode(lastDeclare!, Diagnostics.A_0_modifier_cannot_be_used_with_an_import_declaration, "declare"); - } - else if (node.kind === SyntaxKind.Parameter && (flags & ModifierFlags.ParameterPropertyModifier) && isBindingPattern((node).name)) { - return grammarErrorOnNode(node, Diagnostics.A_parameter_property_may_not_be_declared_using_a_binding_pattern); + } + return undefined; + } + // Return true if the given node is a declaration of a nested block scoped entity with a name that either hides an + // existing name or might hide a name when compiled downlevel + function isDeclarationWithCollidingName(nodeIn: Declaration): boolean { + const node = getParseTreeNode(nodeIn, isDeclaration); + if (node) { + const symbol = getSymbolOfNode(node); + if (symbol) { + return isSymbolOfDeclarationWithCollidingName(symbol); } - else if (node.kind === SyntaxKind.Parameter && (flags & ModifierFlags.ParameterPropertyModifier) && (node).dotDotDotToken) { - return grammarErrorOnNode(node, Diagnostics.A_parameter_property_cannot_be_declared_using_a_rest_parameter); + } + return false; + } + function isValueAliasDeclaration(node: Node): boolean { + switch (node.kind) { + case SyntaxKind.ImportEqualsDeclaration: + return isAliasResolvedToValue(getSymbolOfNode(node) || unknownSymbol); + case SyntaxKind.ImportClause: + case SyntaxKind.NamespaceImport: + case SyntaxKind.ImportSpecifier: + case SyntaxKind.ExportSpecifier: + const symbol = getSymbolOfNode(node) || unknownSymbol; + return isAliasResolvedToValue(symbol) && !getTypeOnlyAliasDeclaration(symbol); + case SyntaxKind.ExportDeclaration: + const exportClause = (node).exportClause; + return !!exportClause && (isNamespaceExport(exportClause) || + some(exportClause.elements, isValueAliasDeclaration)); + case SyntaxKind.ExportAssignment: + return (node).expression && (node).expression.kind === SyntaxKind.Identifier ? + isAliasResolvedToValue(getSymbolOfNode(node) || unknownSymbol) : + true; + } + return false; + } + function isTopLevelValueImportEqualsWithEntityName(nodeIn: ImportEqualsDeclaration): boolean { + const node = getParseTreeNode(nodeIn, isImportEqualsDeclaration); + if (node === undefined || node.parent.kind !== SyntaxKind.SourceFile || !isInternalModuleImportEqualsDeclaration(node)) { + // parent is not source file or it is not reference to internal module + return false; + } + const isValue = isAliasResolvedToValue(getSymbolOfNode(node)); + return isValue && node.moduleReference && !nodeIsMissing(node.moduleReference); + } + function isAliasResolvedToValue(symbol: ts.Symbol): boolean { + const target = resolveAlias(symbol); + if (target === unknownSymbol) { + return true; + } + // const enums and modules that contain only const enums are not considered values from the emit perspective + // unless 'preserveConstEnums' option is set to true + return !!(target.flags & SymbolFlags.Value) && + (compilerOptions.preserveConstEnums || !isConstEnumOrConstEnumOnlyModule(target)); + } + function isConstEnumOrConstEnumOnlyModule(s: ts.Symbol): boolean { + return isConstEnumSymbol(s) || !!s.constEnumOnlyModule; + } + function isReferencedAliasDeclaration(node: Node, checkChildren?: boolean): boolean { + if (isAliasSymbolDeclaration(node)) { + const symbol = getSymbolOfNode(node); + if (symbol && getSymbolLinks(symbol).referenced) { + return true; } - if (flags & ModifierFlags.Async) { - return checkGrammarAsyncModifier(node, lastAsync!); + const target = getSymbolLinks(symbol!).target; // TODO: GH#18217 + if (target && getModifierFlags(node) & ModifierFlags.Export && + target.flags & SymbolFlags.Value && + (compilerOptions.preserveConstEnums || !isConstEnumOrConstEnumOnlyModule(target))) { + // An `export import ... =` of a value symbol is always considered referenced + return true; } + } + if (checkChildren) { + return !!forEachChild(node, node => isReferencedAliasDeclaration(node, checkChildren)); + } + return false; + } + function isImplementationOfOverload(node: SignatureDeclaration) { + if (nodeIsPresent((node as FunctionLikeDeclaration).body)) { + if (isGetAccessor(node) || isSetAccessor(node)) + return false; // Get or set accessors can never be overload implementations, but can have up to 2 signatures + const symbol = getSymbolOfNode(node); + const signaturesOfSymbol = getSignaturesOfSymbol(symbol); + // If this function body corresponds to function with multiple signature, it is implementation of overload + // e.g.: function foo(a: string): string; + // function foo(a: number): number; + // function foo(a: any) { // This is implementation of the overloads + // return a; + // } + return signaturesOfSymbol.length > 1 || + // If there is single signature for the symbol, it is overload if that signature isn't coming from the node + // e.g.: function foo(a: string): string; + // function foo(a: any) { // This is implementation of the overloads + // return a; + // } + (signaturesOfSymbol.length === 1 && signaturesOfSymbol[0].declaration !== node); + } + return false; + } + function isRequiredInitializedParameter(parameter: ParameterDeclaration | JSDocParameterTag): boolean { + return !!strictNullChecks && + !isOptionalParameter(parameter) && + !isJSDocParameterTag(parameter) && + !!parameter.initializer && + !hasModifier(parameter, ModifierFlags.ParameterPropertyModifier); + } + function isOptionalUninitializedParameterProperty(parameter: ParameterDeclaration) { + return strictNullChecks && + isOptionalParameter(parameter) && + !parameter.initializer && + hasModifier(parameter, ModifierFlags.ParameterPropertyModifier); + } + function isExpandoFunctionDeclaration(node: Declaration): boolean { + const declaration = getParseTreeNode(node, isFunctionDeclaration); + if (!declaration) { return false; } - - /** - * true | false: Early return this value from checkGrammarModifiers. - * undefined: Need to do full checking on the modifiers. - */ - function reportObviousModifierErrors(node: Node): boolean | undefined { - return !node.modifiers - ? false - : shouldReportBadModifier(node) - ? grammarErrorOnFirstToken(node, Diagnostics.Modifiers_cannot_appear_here) - : undefined; - } - function shouldReportBadModifier(node: Node): boolean { - switch (node.kind) { - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - case SyntaxKind.Constructor: - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.PropertySignature: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.MethodSignature: - case SyntaxKind.IndexSignature: - case SyntaxKind.ModuleDeclaration: - case SyntaxKind.ImportDeclaration: - case SyntaxKind.ImportEqualsDeclaration: - case SyntaxKind.ExportDeclaration: - case SyntaxKind.ExportAssignment: - case SyntaxKind.FunctionExpression: - case SyntaxKind.ArrowFunction: - case SyntaxKind.Parameter: - return false; - default: - if (node.parent.kind === SyntaxKind.ModuleBlock || node.parent.kind === SyntaxKind.SourceFile) { - return false; - } - switch (node.kind) { - case SyntaxKind.FunctionDeclaration: - return nodeHasAnyModifiersExcept(node, SyntaxKind.AsyncKeyword); - case SyntaxKind.ClassDeclaration: - return nodeHasAnyModifiersExcept(node, SyntaxKind.AbstractKeyword); - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.VariableStatement: - case SyntaxKind.TypeAliasDeclaration: - return true; - case SyntaxKind.EnumDeclaration: - return nodeHasAnyModifiersExcept(node, SyntaxKind.ConstKeyword); - default: - Debug.fail(); - return false; - } - } + const symbol = getSymbolOfNode(declaration); + if (!symbol || !(symbol.flags & SymbolFlags.Function)) { + return false; + } + return !!forEachEntry(getExportsOfSymbol(symbol), p => p.flags & SymbolFlags.Value && p.valueDeclaration && isPropertyAccessExpression(p.valueDeclaration)); + } + function getPropertiesOfContainerFunction(node: Declaration): ts.Symbol[] { + const declaration = getParseTreeNode(node, isFunctionDeclaration); + if (!declaration) { + return emptyArray; } - function nodeHasAnyModifiersExcept(node: Node, allowedModifier: SyntaxKind): boolean { - return node.modifiers!.length > 1 || node.modifiers![0].kind !== allowedModifier; + const symbol = getSymbolOfNode(declaration); + return symbol && getPropertiesOfType(getTypeOfSymbol(symbol)) || emptyArray; + } + function getNodeCheckFlags(node: Node): NodeCheckFlags { + return getNodeLinks(node).flags || 0; + } + function getEnumMemberValue(node: EnumMember): string | number | undefined { + computeEnumMemberValues(node.parent); + return getNodeLinks(node).enumMemberValue; + } + function canHaveConstantValue(node: Node): node is EnumMember | AccessExpression { + switch (node.kind) { + case SyntaxKind.EnumMember: + case SyntaxKind.PropertyAccessExpression: + case SyntaxKind.ElementAccessExpression: + return true; } - - function checkGrammarAsyncModifier(node: Node, asyncModifier: Node): boolean { - switch (node.kind) { - case SyntaxKind.MethodDeclaration: - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.FunctionExpression: - case SyntaxKind.ArrowFunction: - return false; + return false; + } + function getConstantValue(node: EnumMember | AccessExpression): string | number | undefined { + if (node.kind === SyntaxKind.EnumMember) { + return getEnumMemberValue(node); + } + const symbol = getNodeLinks(node).resolvedSymbol; + if (symbol && (symbol.flags & SymbolFlags.EnumMember)) { + // inline property\index accesses only for const enums + const member = (symbol.valueDeclaration as EnumMember); + if (isEnumConst(member.parent)) { + return getEnumMemberValue(member); } - - return grammarErrorOnNode(asyncModifier, Diagnostics._0_modifier_cannot_be_used_here, "async"); } - - function checkGrammarForDisallowedTrailingComma(list: NodeArray | undefined, diag = Diagnostics.Trailing_comma_not_allowed): boolean { - if (list && list.hasTrailingComma) { - return grammarErrorAtPos(list[0], list.end - ",".length, ",".length, diag); + return undefined; + } + function isFunctionType(type: ts.Type): boolean { + return !!(type.flags & TypeFlags.Object) && getSignaturesOfType(type, SignatureKind.Call).length > 0; + } + function getTypeReferenceSerializationKind(typeNameIn: EntityName, location?: Node): TypeReferenceSerializationKind { + // ensure both `typeName` and `location` are parse tree nodes. + const typeName = getParseTreeNode(typeNameIn, isEntityName); + if (!typeName) + return TypeReferenceSerializationKind.Unknown; + if (location) { + location = getParseTreeNode(location); + if (!location) + return TypeReferenceSerializationKind.Unknown; + } + // Resolve the symbol as a value to ensure the type can be reached at runtime during emit. + const valueSymbol = resolveEntityName(typeName, SymbolFlags.Value, /*ignoreErrors*/ true, /*dontResolveAlias*/ false, location); + // Resolve the symbol as a type so that we can provide a more useful hint for the type serializer. + const typeSymbol = resolveEntityName(typeName, SymbolFlags.Type, /*ignoreErrors*/ true, /*dontResolveAlias*/ false, location); + if (valueSymbol && valueSymbol === typeSymbol) { + const globalPromiseSymbol = getGlobalPromiseConstructorSymbol(/*reportErrors*/ false); + if (globalPromiseSymbol && valueSymbol === globalPromiseSymbol) { + return TypeReferenceSerializationKind.Promise; + } + const constructorType = getTypeOfSymbol(valueSymbol); + if (constructorType && isConstructorType(constructorType)) { + return TypeReferenceSerializationKind.TypeWithConstructSignatureAndValue; } - return false; } - - function checkGrammarTypeParameterList(typeParameters: NodeArray | undefined, file: SourceFile): boolean { - if (typeParameters && typeParameters.length === 0) { - const start = typeParameters.pos - "<".length; - const end = skipTrivia(file.text, typeParameters.end) + ">".length; - return grammarErrorAtPos(file, start, end - start, Diagnostics.Type_parameter_list_cannot_be_empty); + // We might not be able to resolve type symbol so use unknown type in that case (eg error case) + if (!typeSymbol) { + return TypeReferenceSerializationKind.Unknown; + } + const type = getDeclaredTypeOfSymbol(typeSymbol); + if (type === errorType) { + return TypeReferenceSerializationKind.Unknown; + } + else if (type.flags & TypeFlags.AnyOrUnknown) { + return TypeReferenceSerializationKind.ObjectType; + } + else if (isTypeAssignableToKind(type, TypeFlags.Void | TypeFlags.Nullable | TypeFlags.Never)) { + return TypeReferenceSerializationKind.VoidNullableOrNeverType; + } + else if (isTypeAssignableToKind(type, TypeFlags.BooleanLike)) { + return TypeReferenceSerializationKind.BooleanType; + } + else if (isTypeAssignableToKind(type, TypeFlags.NumberLike)) { + return TypeReferenceSerializationKind.NumberLikeType; + } + else if (isTypeAssignableToKind(type, TypeFlags.BigIntLike)) { + return TypeReferenceSerializationKind.BigIntLikeType; + } + else if (isTypeAssignableToKind(type, TypeFlags.StringLike)) { + return TypeReferenceSerializationKind.StringLikeType; + } + else if (isTupleType(type)) { + return TypeReferenceSerializationKind.ArrayLikeType; + } + else if (isTypeAssignableToKind(type, TypeFlags.ESSymbolLike)) { + return TypeReferenceSerializationKind.ESSymbolType; + } + else if (isFunctionType(type)) { + return TypeReferenceSerializationKind.TypeWithCallSignature; + } + else if (isArrayType(type)) { + return TypeReferenceSerializationKind.ArrayLikeType; + } + else { + return TypeReferenceSerializationKind.ObjectType; + } + } + function createTypeOfDeclaration(declarationIn: AccessorDeclaration | VariableLikeDeclaration | PropertyAccessExpression, enclosingDeclaration: Node, flags: NodeBuilderFlags, tracker: SymbolTracker, addUndefined?: boolean) { + const declaration = getParseTreeNode(declarationIn, isVariableLikeOrAccessor); + if (!declaration) { + return createToken(SyntaxKind.AnyKeyword) as KeywordTypeNode; + } + // Get type of the symbol if this is the valid symbol otherwise get type at location + const symbol = getSymbolOfNode(declaration); + let type = symbol && !(symbol.flags & (SymbolFlags.TypeLiteral | SymbolFlags.Signature)) + ? getWidenedLiteralType(getTypeOfSymbol(symbol)) + : errorType; + if (type.flags & TypeFlags.UniqueESSymbol && + type.symbol === symbol) { + flags |= NodeBuilderFlags.AllowUniqueESSymbolType; + } + if (addUndefined) { + type = getOptionalType(type); + } + return nodeBuilder.typeToTypeNode(type, enclosingDeclaration, flags | NodeBuilderFlags.MultilineObjectLiterals, tracker); + } + function createReturnTypeOfSignatureDeclaration(signatureDeclarationIn: SignatureDeclaration, enclosingDeclaration: Node, flags: NodeBuilderFlags, tracker: SymbolTracker) { + const signatureDeclaration = getParseTreeNode(signatureDeclarationIn, isFunctionLike); + if (!signatureDeclaration) { + return createToken(SyntaxKind.AnyKeyword) as KeywordTypeNode; + } + const signature = getSignatureFromDeclaration(signatureDeclaration); + return nodeBuilder.typeToTypeNode(getReturnTypeOfSignature(signature), enclosingDeclaration, flags | NodeBuilderFlags.MultilineObjectLiterals, tracker); + } + function createTypeOfExpression(exprIn: Expression, enclosingDeclaration: Node, flags: NodeBuilderFlags, tracker: SymbolTracker) { + const expr = getParseTreeNode(exprIn, isExpression); + if (!expr) { + return createToken(SyntaxKind.AnyKeyword) as KeywordTypeNode; + } + const type = getWidenedType(getRegularTypeOfExpression(expr)); + return nodeBuilder.typeToTypeNode(type, enclosingDeclaration, flags | NodeBuilderFlags.MultilineObjectLiterals, tracker); + } + function hasGlobalName(name: string): boolean { + return globals.has(escapeLeadingUnderscores(name)); + } + function getReferencedValueSymbol(reference: Identifier, startInDeclarationContainer?: boolean): ts.Symbol | undefined { + const resolvedSymbol = getNodeLinks(reference).resolvedSymbol; + if (resolvedSymbol) { + return resolvedSymbol; + } + let location: Node = reference; + if (startInDeclarationContainer) { + // When resolving the name of a declaration as a value, we need to start resolution + // at a point outside of the declaration. + const parent = reference.parent; + if (isDeclaration(parent) && reference === parent.name) { + location = getDeclarationContainer(parent); } - return false; } - - function checkGrammarParameterList(parameters: NodeArray) { - let seenOptionalParameter = false; - const parameterCount = parameters.length; - - for (let i = 0; i < parameterCount; i++) { - const parameter = parameters[i]; - if (parameter.dotDotDotToken) { - if (i !== (parameterCount - 1)) { - return grammarErrorOnNode(parameter.dotDotDotToken, Diagnostics.A_rest_parameter_must_be_last_in_a_parameter_list); - } - if (!(parameter.flags & NodeFlags.Ambient)) { // Allow `...foo,` in ambient declarations; see GH#23070 - checkGrammarForDisallowedTrailingComma(parameters, Diagnostics.A_rest_parameter_or_binding_pattern_may_not_have_a_trailing_comma); - } - - if (parameter.questionToken) { - return grammarErrorOnNode(parameter.questionToken, Diagnostics.A_rest_parameter_cannot_be_optional); - } - - if (parameter.initializer) { - return grammarErrorOnNode(parameter.name, Diagnostics.A_rest_parameter_cannot_have_an_initializer); - } + return resolveName(location, reference.escapedText, SymbolFlags.Value | SymbolFlags.ExportValue | SymbolFlags.Alias, /*nodeNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ true); + } + function getReferencedValueDeclaration(referenceIn: Identifier): Declaration | undefined { + if (!isGeneratedIdentifier(referenceIn)) { + const reference = getParseTreeNode(referenceIn, isIdentifier); + if (reference) { + const symbol = getReferencedValueSymbol(reference); + if (symbol) { + return getExportSymbolOfValueSymbolIfExported(symbol).valueDeclaration; } - else if (parameter.questionToken) { - seenOptionalParameter = true; - - if (parameter.initializer) { - return grammarErrorOnNode(parameter.name, Diagnostics.Parameter_cannot_have_question_mark_and_initializer); - } + } + } + return undefined; + } + function isLiteralConstDeclaration(node: VariableDeclaration | PropertyDeclaration | PropertySignature | ParameterDeclaration): boolean { + if (isDeclarationReadonly(node) || isVariableDeclaration(node) && isVarConst(node)) { + return isFreshLiteralType(getTypeOfSymbol(getSymbolOfNode(node))); + } + return false; + } + function literalTypeToNode(type: FreshableType, enclosing: Node, tracker: SymbolTracker): Expression { + const enumResult = type.flags & TypeFlags.EnumLiteral ? nodeBuilder.symbolToExpression(type.symbol, SymbolFlags.Value, enclosing, /*flags*/ undefined, tracker) + : type === trueType ? createTrue() : type === falseType && createFalse(); + return enumResult || createLiteral((type as LiteralType).value); + } + function createLiteralConstValue(node: VariableDeclaration | PropertyDeclaration | PropertySignature | ParameterDeclaration, tracker: SymbolTracker) { + const type = getTypeOfSymbol(getSymbolOfNode(node)); + return literalTypeToNode((type), node, tracker); + } + function getJsxFactoryEntity(location: Node) { + return location ? (getJsxNamespace(location), (getSourceFileOfNode(location).localJsxFactory || _jsxFactoryEntity)) : _jsxFactoryEntity; + } + function createResolver(): EmitResolver { + // this variable and functions that use it are deliberately moved here from the outer scope + // to avoid scope pollution + const resolvedTypeReferenceDirectives = host.getResolvedTypeReferenceDirectives(); + let fileToDirective: ts.Map; + if (resolvedTypeReferenceDirectives) { + // populate reverse mapping: file path -> type reference directive that was resolved to this file + fileToDirective = createMap(); + resolvedTypeReferenceDirectives.forEach((resolvedDirective, key) => { + if (!resolvedDirective || !resolvedDirective.resolvedFileName) { + return; } - else if (seenOptionalParameter && !parameter.initializer) { - return grammarErrorOnNode(parameter.name, Diagnostics.A_required_parameter_cannot_follow_an_optional_parameter); + const file = host.getSourceFile(resolvedDirective.resolvedFileName)!; + // Add the transitive closure of path references loaded by this file (as long as they are not) + // part of an existing type reference. + addReferencedFilesToTypeDirective(file, key); + }); + } + return { + getReferencedExportContainer, + getReferencedImportDeclaration, + getReferencedDeclarationWithCollidingName, + isDeclarationWithCollidingName, + isValueAliasDeclaration: node => { + node = getParseTreeNode(node); + // Synthesized nodes are always treated like values. + return node ? isValueAliasDeclaration(node) : true; + }, + hasGlobalName, + isReferencedAliasDeclaration: (node, checkChildren?) => { + node = getParseTreeNode(node); + // Synthesized nodes are always treated as referenced. + return node ? isReferencedAliasDeclaration(node, checkChildren) : true; + }, + getNodeCheckFlags: node => { + node = getParseTreeNode(node); + return node ? getNodeCheckFlags(node) : 0; + }, + isTopLevelValueImportEqualsWithEntityName, + isDeclarationVisible, + isImplementationOfOverload, + isRequiredInitializedParameter, + isOptionalUninitializedParameterProperty, + isExpandoFunctionDeclaration, + getPropertiesOfContainerFunction, + createTypeOfDeclaration, + createReturnTypeOfSignatureDeclaration, + createTypeOfExpression, + createLiteralConstValue, + isSymbolAccessible, + isEntityNameVisible, + getConstantValue: nodeIn => { + const node = getParseTreeNode(nodeIn, canHaveConstantValue); + return node ? getConstantValue(node) : undefined; + }, + collectLinkedAliases, + getReferencedValueDeclaration, + getTypeReferenceSerializationKind, + isOptionalParameter, + moduleExportsSomeValue, + isArgumentsLocalBinding, + getExternalModuleFileFromDeclaration, + getTypeReferenceDirectivesForEntityName, + getTypeReferenceDirectivesForSymbol, + isLiteralConstDeclaration, + isLateBound: (nodeIn: Declaration): nodeIn is LateBoundDeclaration => { + const node = getParseTreeNode(nodeIn, isDeclaration); + const symbol = node && getSymbolOfNode(node); + return !!(symbol && getCheckFlags(symbol) & CheckFlags.Late); + }, + getJsxFactoryEntity, + getAllAccessorDeclarations(accessor: AccessorDeclaration): AllAccessorDeclarations { + accessor = (getParseTreeNode(accessor, isGetOrSetAccessorDeclaration)!); // TODO: GH#18217 + const otherKind = accessor.kind === SyntaxKind.SetAccessor ? SyntaxKind.GetAccessor : SyntaxKind.SetAccessor; + const otherAccessor = getDeclarationOfKind(getSymbolOfNode(accessor), otherKind); + const firstAccessor = otherAccessor && (otherAccessor.pos < accessor.pos) ? otherAccessor : accessor; + const secondAccessor = otherAccessor && (otherAccessor.pos < accessor.pos) ? accessor : otherAccessor; + const setAccessor = accessor.kind === SyntaxKind.SetAccessor ? accessor : otherAccessor as SetAccessorDeclaration; + const getAccessor = accessor.kind === SyntaxKind.GetAccessor ? accessor : otherAccessor as GetAccessorDeclaration; + return { + firstAccessor, + secondAccessor, + setAccessor, + getAccessor + }; + }, + getSymbolOfExternalModuleSpecifier: moduleName => resolveExternalModuleNameWorker(moduleName, moduleName, /*moduleNotFoundError*/ undefined), + isBindingCapturedByNode: (node, decl) => { + const parseNode = getParseTreeNode(node); + const parseDecl = getParseTreeNode(decl); + return !!parseNode && !!parseDecl && (isVariableDeclaration(parseDecl) || isBindingElement(parseDecl)) && isBindingCapturedByNode(parseNode, parseDecl); + }, + getDeclarationStatementsForSourceFile: (node, flags, tracker, bundled) => { + const n = (getParseTreeNode(node) as SourceFile); + Debug.assert(n && n.kind === SyntaxKind.SourceFile, "Non-sourcefile node passed into getDeclarationsForSourceFile"); + const sym = getSymbolOfNode(node); + if (!sym) { + return !node.locals ? [] : nodeBuilder.symbolTableToDeclarationStatements(node.locals, node, flags, tracker, bundled); } + return !sym.exports ? [] : nodeBuilder.symbolTableToDeclarationStatements(sym.exports, node, flags, tracker, bundled); } + }; + function isInHeritageClause(node: PropertyAccessEntityNameExpression) { + return node.parent && node.parent.kind === SyntaxKind.ExpressionWithTypeArguments && node.parent.parent && node.parent.parent.kind === SyntaxKind.HeritageClause; } - - function getNonSimpleParameters(parameters: readonly ParameterDeclaration[]): readonly ParameterDeclaration[] { - return filter(parameters, parameter => !!parameter.initializer || isBindingPattern(parameter.name) || isRestParameter(parameter)); - } - - function checkGrammarForUseStrictSimpleParameterList(node: FunctionLikeDeclaration): boolean { - if (languageVersion >= ScriptTarget.ES2016) { - const useStrictDirective = node.body && isBlock(node.body) && findUseStrictPrologue(node.body.statements); - if (useStrictDirective) { - const nonSimpleParameters = getNonSimpleParameters(node.parameters); - if (length(nonSimpleParameters)) { - forEach(nonSimpleParameters, parameter => { - addRelatedInfo( - error(parameter, Diagnostics.This_parameter_is_not_allowed_with_use_strict_directive), - createDiagnosticForNode(useStrictDirective, Diagnostics.use_strict_directive_used_here) - ); - }); - - const diagnostics = nonSimpleParameters.map((parameter, index) => ( - index === 0 ? createDiagnosticForNode(parameter, Diagnostics.Non_simple_parameter_declared_here) : createDiagnosticForNode(parameter, Diagnostics.and_here) - )) as [DiagnosticWithLocation, ...DiagnosticWithLocation[]]; - addRelatedInfo(error(useStrictDirective, Diagnostics.use_strict_directive_cannot_be_used_with_non_simple_parameter_list), ...diagnostics); - return true; + // defined here to avoid outer scope pollution + function getTypeReferenceDirectivesForEntityName(node: EntityNameOrEntityNameExpression): string[] | undefined { + // program does not have any files with type reference directives - bail out + if (!fileToDirective) { + return undefined; + } + // property access can only be used as values, or types when within an expression with type arguments inside a heritage clause + // qualified names can only be used as types\namespaces + // identifiers are treated as values only if they appear in type queries + let meaning = SymbolFlags.Type | SymbolFlags.Namespace; + if ((node.kind === SyntaxKind.Identifier && isInTypeQuery(node)) || (node.kind === SyntaxKind.PropertyAccessExpression && !isInHeritageClause(node))) { + meaning = SymbolFlags.Value | SymbolFlags.ExportValue; + } + const symbol = resolveEntityName(node, meaning, /*ignoreErrors*/ true); + return symbol && symbol !== unknownSymbol ? getTypeReferenceDirectivesForSymbol(symbol, meaning) : undefined; + } + // defined here to avoid outer scope pollution + function getTypeReferenceDirectivesForSymbol(symbol: ts.Symbol, meaning?: SymbolFlags): string[] | undefined { + // program does not have any files with type reference directives - bail out + if (!fileToDirective) { + return undefined; + } + if (!isSymbolFromTypeDeclarationFile(symbol)) { + return undefined; + } + // check what declarations in the symbol can contribute to the target meaning + let typeReferenceDirectives: string[] | undefined; + for (const decl of symbol.declarations) { + // check meaning of the local symbol to see if declaration needs to be analyzed further + if (decl.symbol && decl.symbol.flags & meaning!) { + const file = getSourceFileOfNode(decl); + const typeReferenceDirective = fileToDirective.get(file.path); + if (typeReferenceDirective) { + (typeReferenceDirectives || (typeReferenceDirectives = [])).push(typeReferenceDirective); + } + else { + // found at least one entry that does not originate from type reference directive + return undefined; } } } - return false; - } - - function checkGrammarFunctionLikeDeclaration(node: FunctionLikeDeclaration | MethodSignature): boolean { - // Prevent cascading error by short-circuit - const file = getSourceFileOfNode(node); - return checkGrammarDecoratorsAndModifiers(node) || checkGrammarTypeParameterList(node.typeParameters, file) || - checkGrammarParameterList(node.parameters) || checkGrammarArrowFunction(node, file) || - (isFunctionLikeDeclaration(node) && checkGrammarForUseStrictSimpleParameterList(node)); - } - - function checkGrammarClassLikeDeclaration(node: ClassLikeDeclaration): boolean { - const file = getSourceFileOfNode(node); - return checkGrammarClassDeclarationHeritageClauses(node) || checkGrammarTypeParameterList(node.typeParameters, file); + return typeReferenceDirectives; } - - function checkGrammarArrowFunction(node: Node, file: SourceFile): boolean { - if (!isArrowFunction(node)) { + function isSymbolFromTypeDeclarationFile(symbol: ts.Symbol): boolean { + // bail out if symbol does not have associated declarations (i.e. this is transient symbol created for property in binding pattern) + if (!symbol.declarations) { return false; } - - const { equalsGreaterThanToken } = node; - const startLine = getLineAndCharacterOfPosition(file, equalsGreaterThanToken.pos).line; - const endLine = getLineAndCharacterOfPosition(file, equalsGreaterThanToken.end).line; - return startLine !== endLine && grammarErrorOnNode(equalsGreaterThanToken, Diagnostics.Line_terminator_not_permitted_before_arrow); - } - - function checkGrammarIndexSignatureParameters(node: SignatureDeclaration): boolean { - const parameter = node.parameters[0]; - if (node.parameters.length !== 1) { - if (parameter) { - return grammarErrorOnNode(parameter.name, Diagnostics.An_index_signature_must_have_exactly_one_parameter); + // walk the parent chain for symbols to make sure that top level parent symbol is in the global scope + // external modules cannot define or contribute to type declaration files + let current = symbol; + while (true) { + const parent = getParentOfSymbol(current); + if (parent) { + current = parent; } else { - return grammarErrorOnNode(node, Diagnostics.An_index_signature_must_have_exactly_one_parameter); + break; } } - if (parameter.dotDotDotToken) { - return grammarErrorOnNode(parameter.dotDotDotToken, Diagnostics.An_index_signature_cannot_have_a_rest_parameter); - } - if (hasModifiers(parameter)) { - return grammarErrorOnNode(parameter.name, Diagnostics.An_index_signature_parameter_cannot_have_an_accessibility_modifier); - } - if (parameter.questionToken) { - return grammarErrorOnNode(parameter.questionToken, Diagnostics.An_index_signature_parameter_cannot_have_a_question_mark); - } - if (parameter.initializer) { - return grammarErrorOnNode(parameter.name, Diagnostics.An_index_signature_parameter_cannot_have_an_initializer); - } - if (!parameter.type) { - return grammarErrorOnNode(parameter.name, Diagnostics.An_index_signature_parameter_must_have_a_type_annotation); + if (current.valueDeclaration && current.valueDeclaration.kind === SyntaxKind.SourceFile && current.flags & SymbolFlags.ValueModule) { + return false; } - if (parameter.type.kind !== SyntaxKind.StringKeyword && parameter.type.kind !== SyntaxKind.NumberKeyword) { - const type = getTypeFromTypeNode(parameter.type); - - if (type.flags & TypeFlags.String || type.flags & TypeFlags.Number) { - return grammarErrorOnNode(parameter.name, - Diagnostics.An_index_signature_parameter_type_cannot_be_a_type_alias_Consider_writing_0_Colon_1_Colon_2_instead, - getTextOfNode(parameter.name), - typeToString(type), - typeToString(node.type ? getTypeFromTypeNode(node.type) : anyType)); - } - - if (type.flags & TypeFlags.Union && allTypesAssignableToKind(type, TypeFlags.StringOrNumberLiteral, /*strict*/ true)) { - return grammarErrorOnNode(parameter.name, - Diagnostics.An_index_signature_parameter_type_cannot_be_a_union_type_Consider_using_a_mapped_object_type_instead); + // check that at least one declaration of top level symbol originates from type declaration file + for (const decl of symbol.declarations) { + const file = getSourceFileOfNode(decl); + if (fileToDirective.has(file.path)) { + return true; } - - return grammarErrorOnNode(parameter.name, Diagnostics.An_index_signature_parameter_type_must_be_either_string_or_number); - } - if (!node.type) { - return grammarErrorOnNode(node, Diagnostics.An_index_signature_must_have_a_type_annotation); } return false; } - - function checkGrammarIndexSignature(node: SignatureDeclaration) { - // Prevent cascading error by short-circuit - return checkGrammarDecoratorsAndModifiers(node) || checkGrammarIndexSignatureParameters(node); - } - - function checkGrammarForAtLeastOneTypeArgument(node: Node, typeArguments: NodeArray | undefined): boolean { - if (typeArguments && typeArguments.length === 0) { - const sourceFile = getSourceFileOfNode(node); - const start = typeArguments.pos - "<".length; - const end = skipTrivia(sourceFile.text, typeArguments.end) + ">".length; - return grammarErrorAtPos(sourceFile, start, end - start, Diagnostics.Type_argument_list_cannot_be_empty); + function addReferencedFilesToTypeDirective(file: SourceFile, key: string) { + if (fileToDirective.has(file.path)) + return; + fileToDirective.set(file.path, key); + for (const { fileName } of file.referencedFiles) { + const resolvedFile = resolveTripleslashReference(fileName, file.originalFileName); + const referencedFile = host.getSourceFile(resolvedFile); + if (referencedFile) { + addReferencedFilesToTypeDirective(referencedFile, key); + } } - return false; } - - function checkGrammarTypeArguments(node: Node, typeArguments: NodeArray | undefined): boolean { - return checkGrammarForDisallowedTrailingComma(typeArguments) || - checkGrammarForAtLeastOneTypeArgument(node, typeArguments); + } + function getExternalModuleFileFromDeclaration(declaration: AnyImportOrReExport | ModuleDeclaration | ImportTypeNode): SourceFile | undefined { + const specifier = declaration.kind === SyntaxKind.ModuleDeclaration ? tryCast(declaration.name, isStringLiteral) : getExternalModuleName(declaration); + const moduleSymbol = resolveExternalModuleNameWorker(specifier!, specifier!, /*moduleNotFoundError*/ undefined); // TODO: GH#18217 + if (!moduleSymbol) { + return undefined; } - - function checkGrammarTaggedTemplateChain(node: TaggedTemplateExpression): boolean { - if (node.questionDotToken || node.flags & NodeFlags.OptionalChain) { - return grammarErrorOnNode(node.template, Diagnostics.Tagged_template_expressions_are_not_permitted_in_an_optional_chain); - } - return false; + return getDeclarationOfKind(moduleSymbol, SyntaxKind.SourceFile); + } + function initializeTypeChecker() { + // Bind all source files and propagate errors + for (const file of host.getSourceFiles()) { + bindSourceFile(file, compilerOptions); } - - function checkGrammarForOmittedArgument(args: NodeArray | undefined): boolean { - if (args) { - for (const arg of args) { - if (arg.kind === SyntaxKind.OmittedExpression) { - return grammarErrorAtPos(arg, arg.pos, 0, Diagnostics.Argument_expression_expected); + amalgamatedDuplicates = createMap(); + // Initialize global symbol table + let augmentations: (readonly (StringLiteral | Identifier)[])[] | undefined; + for (const file of host.getSourceFiles()) { + if (file.redirectInfo) { + continue; + } + if (!isExternalOrCommonJsModule(file)) { + // It is an error for a non-external-module (i.e. script) to declare its own `globalThis`. + // We can't use `builtinGlobals` for this due to synthetic expando-namespace generation in JS files. + const fileGlobalThisSymbol = file.locals!.get(("globalThis" as __String)); + if (fileGlobalThisSymbol) { + for (const declaration of fileGlobalThisSymbol.declarations) { + diagnostics.add(createDiagnosticForNode(declaration, Diagnostics.Declaration_name_conflicts_with_built_in_global_identifier_0, "globalThis")); } } + mergeSymbolTable(globals, file.locals!); } - return false; - } - - function checkGrammarArguments(args: NodeArray | undefined): boolean { - return checkGrammarForOmittedArgument(args); - } - - function checkGrammarHeritageClause(node: HeritageClause): boolean { - const types = node.types; - if (checkGrammarForDisallowedTrailingComma(types)) { - return true; + if (file.jsGlobalAugmentations) { + mergeSymbolTable(globals, file.jsGlobalAugmentations); + } + if (file.patternAmbientModules && file.patternAmbientModules.length) { + patternAmbientModules = concatenate(patternAmbientModules, file.patternAmbientModules); } - if (types && types.length === 0) { - const listType = tokenToString(node.token); - return grammarErrorAtPos(node, types.pos, 0, Diagnostics._0_list_cannot_be_empty, listType); + if (file.moduleAugmentations.length) { + (augmentations || (augmentations = [])).push(file.moduleAugmentations); + } + if (file.symbol && file.symbol.globalExports) { + // Merge in UMD exports with first-in-wins semantics (see #9771) + const source = file.symbol.globalExports; + source.forEach((sourceSymbol, id) => { + if (!globals.has(id)) { + globals.set(id, sourceSymbol); + } + }); } - return some(types, checkGrammarExpressionWithTypeArguments); } - - function checkGrammarExpressionWithTypeArguments(node: ExpressionWithTypeArguments) { - return checkGrammarTypeArguments(node, node.typeArguments); + // We do global augmentations separately from module augmentations (and before creating global types) because they + // 1. Affect global types. We won't have the correct global types until global augmentations are merged. Also, + // 2. Module augmentation instantiation requires creating the type of a module, which, in turn, can require + // checking for an export or property on the module (if export=) which, in turn, can fall back to the + // apparent type of the module - either globalObjectType or globalFunctionType - which wouldn't exist if we + // did module augmentations prior to finalizing the global types. + if (augmentations) { + // merge _global_ module augmentations. + // this needs to be done after global symbol table is initialized to make sure that all ambient modules are indexed + for (const list of augmentations) { + for (const augmentation of list) { + if (!isGlobalScopeAugmentation((augmentation.parent as ModuleDeclaration))) + continue; + mergeModuleAugmentation(augmentation); + } + } + } + // Setup global builtins + addToSymbolTable(globals, builtinGlobals, Diagnostics.Declaration_name_conflicts_with_built_in_global_identifier_0); + getSymbolLinks(undefinedSymbol).type = undefinedWideningType; + getSymbolLinks(argumentsSymbol).type = getGlobalType(("IArguments" as __String), /*arity*/ 0, /*reportErrors*/ true); + getSymbolLinks(unknownSymbol).type = errorType; + getSymbolLinks(globalThisSymbol).type = createObjectType(ObjectFlags.Anonymous, globalThisSymbol); + // Initialize special types + globalArrayType = getGlobalType(("Array" as __String), /*arity*/ 1, /*reportErrors*/ true); + globalObjectType = getGlobalType(("Object" as __String), /*arity*/ 0, /*reportErrors*/ true); + globalFunctionType = getGlobalType(("Function" as __String), /*arity*/ 0, /*reportErrors*/ true); + globalCallableFunctionType = strictBindCallApply && getGlobalType(("CallableFunction" as __String), /*arity*/ 0, /*reportErrors*/ true) || globalFunctionType; + globalNewableFunctionType = strictBindCallApply && getGlobalType(("NewableFunction" as __String), /*arity*/ 0, /*reportErrors*/ true) || globalFunctionType; + globalStringType = getGlobalType(("String" as __String), /*arity*/ 0, /*reportErrors*/ true); + globalNumberType = getGlobalType(("Number" as __String), /*arity*/ 0, /*reportErrors*/ true); + globalBooleanType = getGlobalType(("Boolean" as __String), /*arity*/ 0, /*reportErrors*/ true); + globalRegExpType = getGlobalType(("RegExp" as __String), /*arity*/ 0, /*reportErrors*/ true); + anyArrayType = createArrayType(anyType); + autoArrayType = createArrayType(autoType); + if (autoArrayType === emptyObjectType) { + // autoArrayType is used as a marker, so even if global Array type is not defined, it needs to be a unique type + autoArrayType = createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, undefined, undefined); + } + globalReadonlyArrayType = (getGlobalTypeOrUndefined(("ReadonlyArray" as __String), /*arity*/ 1)) || globalArrayType; + anyReadonlyArrayType = globalReadonlyArrayType ? createTypeFromGenericGlobalType(globalReadonlyArrayType, [anyType]) : anyArrayType; + globalThisType = (getGlobalTypeOrUndefined(("ThisType" as __String), /*arity*/ 1)); + if (augmentations) { + // merge _nonglobal_ module augmentations. + // this needs to be done after global symbol table is initialized to make sure that all ambient modules are indexed + for (const list of augmentations) { + for (const augmentation of list) { + if (isGlobalScopeAugmentation((augmentation.parent as ModuleDeclaration))) + continue; + mergeModuleAugmentation(augmentation); + } + } } - - function checkGrammarClassDeclarationHeritageClauses(node: ClassLikeDeclaration) { - let seenExtendsClause = false; - let seenImplementsClause = false; - - if (!checkGrammarDecoratorsAndModifiers(node) && node.heritageClauses) { - for (const heritageClause of node.heritageClauses) { - if (heritageClause.token === SyntaxKind.ExtendsKeyword) { - if (seenExtendsClause) { - return grammarErrorOnFirstToken(heritageClause, Diagnostics.extends_clause_already_seen); - } - - if (seenImplementsClause) { - return grammarErrorOnFirstToken(heritageClause, Diagnostics.extends_clause_must_precede_implements_clause); - } - - if (heritageClause.types.length > 1) { - return grammarErrorOnFirstToken(heritageClause.types[1], Diagnostics.Classes_can_only_extend_a_single_class); - } - - seenExtendsClause = true; + amalgamatedDuplicates.forEach(({ firstFile, secondFile, conflictingSymbols }) => { + // If not many things conflict, issue individual errors + if (conflictingSymbols.size < 8) { + conflictingSymbols.forEach(({ isBlockScoped, firstFileLocations, secondFileLocations }, symbolName) => { + const message = isBlockScoped ? Diagnostics.Cannot_redeclare_block_scoped_variable_0 : Diagnostics.Duplicate_identifier_0; + for (const node of firstFileLocations) { + addDuplicateDeclarationError(node, message, symbolName, secondFileLocations); } - else { - Debug.assert(heritageClause.token === SyntaxKind.ImplementsKeyword); - if (seenImplementsClause) { - return grammarErrorOnFirstToken(heritageClause, Diagnostics.implements_clause_already_seen); - } - - seenImplementsClause = true; + for (const node of secondFileLocations) { + addDuplicateDeclarationError(node, message, symbolName, firstFileLocations); } - - // Grammar checking heritageClause inside class declaration - checkGrammarHeritageClause(heritageClause); - } + }); } - } - - function checkGrammarInterfaceDeclaration(node: InterfaceDeclaration) { - let seenExtendsClause = false; - - if (node.heritageClauses) { - for (const heritageClause of node.heritageClauses) { - if (heritageClause.token === SyntaxKind.ExtendsKeyword) { - if (seenExtendsClause) { - return grammarErrorOnFirstToken(heritageClause, Diagnostics.extends_clause_already_seen); + else { + // Otherwise issue top-level error since the files appear very identical in terms of what they contain + const list = arrayFrom(conflictingSymbols.keys()).join(", "); + diagnostics.add(addRelatedInfo(createDiagnosticForNode(firstFile, Diagnostics.Definitions_of_the_following_identifiers_conflict_with_those_in_another_file_Colon_0, list), createDiagnosticForNode(secondFile, Diagnostics.Conflicts_are_in_this_file))); + diagnostics.add(addRelatedInfo(createDiagnosticForNode(secondFile, Diagnostics.Definitions_of_the_following_identifiers_conflict_with_those_in_another_file_Colon_0, list), createDiagnosticForNode(firstFile, Diagnostics.Conflicts_are_in_this_file))); + } + }); + amalgamatedDuplicates = undefined; + } + function checkExternalEmitHelpers(location: Node, helpers: ExternalEmitHelpers) { + if ((requestedExternalEmitHelpers & helpers) !== helpers && compilerOptions.importHelpers) { + const sourceFile = getSourceFileOfNode(location); + if (isEffectiveExternalModule(sourceFile, compilerOptions) && !(location.flags & NodeFlags.Ambient)) { + const helpersModule = resolveHelpersModule(sourceFile, location); + if (helpersModule !== unknownSymbol) { + const uncheckedHelpers = helpers & ~requestedExternalEmitHelpers; + for (let helper = ExternalEmitHelpers.FirstEmitHelper; helper <= ExternalEmitHelpers.LastEmitHelper; helper <<= 1) { + if (uncheckedHelpers & helper) { + const name = getHelperName(helper); + const symbol = getSymbol((helpersModule.exports!), escapeLeadingUnderscores(name), SymbolFlags.Value); + if (!symbol) { + error(location, Diagnostics.This_syntax_requires_an_imported_helper_named_1_which_does_not_exist_in_0_Consider_upgrading_your_version_of_0, externalHelpersModuleNameText, name); + } } - - seenExtendsClause = true; - } - else { - Debug.assert(heritageClause.token === SyntaxKind.ImplementsKeyword); - return grammarErrorOnFirstToken(heritageClause, Diagnostics.Interface_declaration_cannot_have_implements_clause); } - - // Grammar checking heritageClause inside class declaration - checkGrammarHeritageClause(heritageClause); } + requestedExternalEmitHelpers |= helpers; } + } + } + function getHelperName(helper: ExternalEmitHelpers) { + switch (helper) { + case ExternalEmitHelpers.Extends: return "__extends"; + case ExternalEmitHelpers.Assign: return "__assign"; + case ExternalEmitHelpers.Rest: return "__rest"; + case ExternalEmitHelpers.Decorate: return "__decorate"; + case ExternalEmitHelpers.Metadata: return "__metadata"; + case ExternalEmitHelpers.Param: return "__param"; + case ExternalEmitHelpers.Awaiter: return "__awaiter"; + case ExternalEmitHelpers.Generator: return "__generator"; + case ExternalEmitHelpers.Values: return "__values"; + case ExternalEmitHelpers.Read: return "__read"; + case ExternalEmitHelpers.Spread: return "__spread"; + case ExternalEmitHelpers.SpreadArrays: return "__spreadArrays"; + case ExternalEmitHelpers.Await: return "__await"; + case ExternalEmitHelpers.AsyncGenerator: return "__asyncGenerator"; + case ExternalEmitHelpers.AsyncDelegator: return "__asyncDelegator"; + case ExternalEmitHelpers.AsyncValues: return "__asyncValues"; + case ExternalEmitHelpers.ExportStar: return "__exportStar"; + case ExternalEmitHelpers.MakeTemplateObject: return "__makeTemplateObject"; + case ExternalEmitHelpers.ClassPrivateFieldGet: return "__classPrivateFieldGet"; + case ExternalEmitHelpers.ClassPrivateFieldSet: return "__classPrivateFieldSet"; + case ExternalEmitHelpers.CreateBinding: return "__createBinding"; + default: return Debug.fail("Unrecognized helper"); + } + } + function resolveHelpersModule(node: SourceFile, errorNode: Node) { + if (!externalHelpersModule) { + externalHelpersModule = resolveExternalModule(node, externalHelpersModuleNameText, Diagnostics.This_syntax_requires_an_imported_helper_but_module_0_cannot_be_found, errorNode) || unknownSymbol; + } + return externalHelpersModule; + } + // GRAMMAR CHECKING + function checkGrammarDecoratorsAndModifiers(node: Node): boolean { + return checkGrammarDecorators(node) || checkGrammarModifiers(node); + } + function checkGrammarDecorators(node: Node): boolean { + if (!node.decorators) { return false; } - - function checkGrammarComputedPropertyName(node: Node): boolean { - // If node is not a computedPropertyName, just skip the grammar checking - if (node.kind !== SyntaxKind.ComputedPropertyName) { - return false; + if (!nodeCanBeDecorated(node, node.parent, node.parent.parent)) { + if (node.kind === SyntaxKind.MethodDeclaration && !nodeIsPresent((node).body)) { + return grammarErrorOnFirstToken(node, Diagnostics.A_decorator_can_only_decorate_a_method_implementation_not_an_overload); } - - const computedPropertyName = node; - if (computedPropertyName.expression.kind === SyntaxKind.BinaryExpression && (computedPropertyName.expression).operatorToken.kind === SyntaxKind.CommaToken) { - return grammarErrorOnNode(computedPropertyName.expression, Diagnostics.A_comma_expression_is_not_allowed_in_a_computed_property_name); + else { + return grammarErrorOnFirstToken(node, Diagnostics.Decorators_are_not_valid_here); } - return false; } - - function checkGrammarForGenerator(node: FunctionLikeDeclaration) { - if (node.asteriskToken) { - Debug.assert( - node.kind === SyntaxKind.FunctionDeclaration || - node.kind === SyntaxKind.FunctionExpression || - node.kind === SyntaxKind.MethodDeclaration); - if (node.flags & NodeFlags.Ambient) { - return grammarErrorOnNode(node.asteriskToken, Diagnostics.Generators_are_not_allowed_in_an_ambient_context); - } - if (!node.body) { - return grammarErrorOnNode(node.asteriskToken, Diagnostics.An_overload_signature_cannot_be_declared_as_a_generator); - } + else if (node.kind === SyntaxKind.GetAccessor || node.kind === SyntaxKind.SetAccessor) { + const accessors = getAllAccessorDeclarations((node.parent).members, (node)); + if (accessors.firstAccessor.decorators && node === accessors.secondAccessor) { + return grammarErrorOnFirstToken(node, Diagnostics.Decorators_cannot_be_applied_to_multiple_get_Slashset_accessors_of_the_same_name); } } - - function checkGrammarForInvalidQuestionMark(questionToken: QuestionToken | undefined, message: DiagnosticMessage): boolean { - return !!questionToken && grammarErrorOnNode(questionToken, message); - } - - function checkGrammarForInvalidExclamationToken(exclamationToken: ExclamationToken | undefined, message: DiagnosticMessage): boolean { - return !!exclamationToken && grammarErrorOnNode(exclamationToken, message); + return false; + } + function checkGrammarModifiers(node: Node): boolean { + const quickResult = reportObviousModifierErrors(node); + if (quickResult !== undefined) { + return quickResult; } - - function checkGrammarObjectLiteralExpression(node: ObjectLiteralExpression, inDestructuring: boolean) { - const seen = createUnderscoreEscapedMap(); - - for (const prop of node.properties) { - if (prop.kind === SyntaxKind.SpreadAssignment) { - if (inDestructuring) { - // a rest property cannot be destructured any further - const expression = skipParentheses(prop.expression); - if (isArrayLiteralExpression(expression) || isObjectLiteralExpression(expression)) { - return grammarErrorOnNode(prop.expression, Diagnostics.A_rest_element_cannot_contain_a_binding_pattern); - } - } - continue; - } - const name = prop.name; - if (name.kind === SyntaxKind.ComputedPropertyName) { - // If the name is not a ComputedPropertyName, the grammar checking will skip it - checkGrammarComputedPropertyName(name); - } - - if (prop.kind === SyntaxKind.ShorthandPropertyAssignment && !inDestructuring && prop.objectAssignmentInitializer) { - // having objectAssignmentInitializer is only valid in ObjectAssignmentPattern - // outside of destructuring it is a syntax error - return grammarErrorOnNode(prop.equalsToken!, Diagnostics.can_only_be_used_in_an_object_literal_property_inside_a_destructuring_assignment); + let lastStatic: Node | undefined, lastDeclare: Node | undefined, lastAsync: Node | undefined, lastReadonly: Node | undefined; + let flags = ModifierFlags.None; + for (const modifier of node.modifiers!) { + if (modifier.kind !== SyntaxKind.ReadonlyKeyword) { + if (node.kind === SyntaxKind.PropertySignature || node.kind === SyntaxKind.MethodSignature) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_appear_on_a_type_member, tokenToString(modifier.kind)); } - - if (name.kind === SyntaxKind.PrivateIdentifier) { - return grammarErrorOnNode(name, Diagnostics.Private_identifiers_are_not_allowed_outside_class_bodies); + if (node.kind === SyntaxKind.IndexSignature) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_appear_on_an_index_signature, tokenToString(modifier.kind)); } - - // Modifiers are never allowed on properties except for 'async' on a method declaration - if (prop.modifiers) { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion - for (const mod of prop.modifiers!) { // TODO: GH#19955 - if (mod.kind !== SyntaxKind.AsyncKeyword || prop.kind !== SyntaxKind.MethodDeclaration) { - grammarErrorOnNode(mod, Diagnostics._0_modifier_cannot_be_used_here, getTextOfNode(mod)); - } + } + switch (modifier.kind) { + case SyntaxKind.ConstKeyword: + if (node.kind !== SyntaxKind.EnumDeclaration) { + return grammarErrorOnNode(node, Diagnostics.A_class_member_cannot_have_the_0_keyword, tokenToString(SyntaxKind.ConstKeyword)); } - } - - // ECMA-262 11.1.5 Object Initializer - // If previous is not undefined then throw a SyntaxError exception if any of the following conditions are true - // a.This production is contained in strict code and IsDataDescriptor(previous) is true and - // IsDataDescriptor(propId.descriptor) is true. - // b.IsDataDescriptor(previous) is true and IsAccessorDescriptor(propId.descriptor) is true. - // c.IsAccessorDescriptor(previous) is true and IsDataDescriptor(propId.descriptor) is true. - // d.IsAccessorDescriptor(previous) is true and IsAccessorDescriptor(propId.descriptor) is true - // and either both previous and propId.descriptor have[[Get]] fields or both previous and propId.descriptor have[[Set]] fields - let currentKind: DeclarationMeaning; - switch (prop.kind) { - case SyntaxKind.ShorthandPropertyAssignment: - checkGrammarForInvalidExclamationToken(prop.exclamationToken, Diagnostics.A_definite_assignment_assertion_is_not_permitted_in_this_context); - // falls through - case SyntaxKind.PropertyAssignment: - // Grammar checking for computedPropertyName and shorthandPropertyAssignment - checkGrammarForInvalidQuestionMark(prop.questionToken, Diagnostics.An_object_member_cannot_be_declared_optional); - if (name.kind === SyntaxKind.NumericLiteral) { - checkGrammarNumericLiteral(name); + break; + case SyntaxKind.PublicKeyword: + case SyntaxKind.ProtectedKeyword: + case SyntaxKind.PrivateKeyword: + const text = visibilityToString(modifierToFlag(modifier.kind)); + if (flags & ModifierFlags.AccessibilityModifier) { + return grammarErrorOnNode(modifier, Diagnostics.Accessibility_modifier_already_seen); + } + else if (flags & ModifierFlags.Static) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_must_precede_1_modifier, text, "static"); + } + else if (flags & ModifierFlags.Readonly) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_must_precede_1_modifier, text, "readonly"); + } + else if (flags & ModifierFlags.Async) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_must_precede_1_modifier, text, "async"); + } + else if (node.parent.kind === SyntaxKind.ModuleBlock || node.parent.kind === SyntaxKind.SourceFile) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_appear_on_a_module_or_namespace_element, text); + } + else if (flags & ModifierFlags.Abstract) { + if (modifier.kind === SyntaxKind.PrivateKeyword) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_be_used_with_1_modifier, text, "abstract"); + } + else { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_must_precede_1_modifier, text, "abstract"); } - currentKind = DeclarationMeaning.PropertyAssignment; - break; - case SyntaxKind.MethodDeclaration: - currentKind = DeclarationMeaning.Method; - break; - case SyntaxKind.GetAccessor: - currentKind = DeclarationMeaning.GetAccessor; - break; - case SyntaxKind.SetAccessor: - currentKind = DeclarationMeaning.SetAccessor; - break; - default: - throw Debug.assertNever(prop, "Unexpected syntax kind:" + (prop).kind); - } - - if (!inDestructuring) { - const effectiveName = getPropertyNameForPropertyNameNode(name); - if (effectiveName === undefined) { - continue; } - - const existingKind = seen.get(effectiveName); - if (!existingKind) { - seen.set(effectiveName, currentKind); + else if (isPrivateIdentifierPropertyDeclaration(node)) { + return grammarErrorOnNode(modifier, Diagnostics.An_accessibility_modifier_cannot_be_used_with_a_private_identifier); } - else { - if ((currentKind & DeclarationMeaning.PropertyAssignmentOrMethod) && (existingKind & DeclarationMeaning.PropertyAssignmentOrMethod)) { - grammarErrorOnNode(name, Diagnostics.Duplicate_identifier_0, getTextOfNode(name)); + flags |= modifierToFlag(modifier.kind); + break; + case SyntaxKind.StaticKeyword: + if (flags & ModifierFlags.Static) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_already_seen, "static"); + } + else if (flags & ModifierFlags.Readonly) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_must_precede_1_modifier, "static", "readonly"); + } + else if (flags & ModifierFlags.Async) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_must_precede_1_modifier, "static", "async"); + } + else if (node.parent.kind === SyntaxKind.ModuleBlock || node.parent.kind === SyntaxKind.SourceFile) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_appear_on_a_module_or_namespace_element, "static"); + } + else if (node.kind === SyntaxKind.Parameter) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_appear_on_a_parameter, "static"); + } + else if (flags & ModifierFlags.Abstract) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_be_used_with_1_modifier, "static", "abstract"); + } + else if (isPrivateIdentifierPropertyDeclaration(node)) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_be_used_with_a_private_identifier, "static"); + } + flags |= ModifierFlags.Static; + lastStatic = modifier; + break; + case SyntaxKind.ReadonlyKeyword: + if (flags & ModifierFlags.Readonly) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_already_seen, "readonly"); + } + else if (node.kind !== SyntaxKind.PropertyDeclaration && node.kind !== SyntaxKind.PropertySignature && node.kind !== SyntaxKind.IndexSignature && node.kind !== SyntaxKind.Parameter) { + // If node.kind === SyntaxKind.Parameter, checkParameter report an error if it's not a parameter property. + return grammarErrorOnNode(modifier, Diagnostics.readonly_modifier_can_only_appear_on_a_property_declaration_or_index_signature); + } + flags |= ModifierFlags.Readonly; + lastReadonly = modifier; + break; + case SyntaxKind.ExportKeyword: + if (flags & ModifierFlags.Export) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_already_seen, "export"); + } + else if (flags & ModifierFlags.Ambient) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_must_precede_1_modifier, "export", "declare"); + } + else if (flags & ModifierFlags.Abstract) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_must_precede_1_modifier, "export", "abstract"); + } + else if (flags & ModifierFlags.Async) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_must_precede_1_modifier, "export", "async"); + } + else if (isClassLike(node.parent)) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_appear_on_a_class_element, "export"); + } + else if (node.kind === SyntaxKind.Parameter) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_appear_on_a_parameter, "export"); + } + flags |= ModifierFlags.Export; + break; + case SyntaxKind.DefaultKeyword: + const container = node.parent.kind === SyntaxKind.SourceFile ? node.parent : node.parent.parent; + if (container.kind === SyntaxKind.ModuleDeclaration && !isAmbientModule(container)) { + return grammarErrorOnNode(modifier, Diagnostics.A_default_export_can_only_be_used_in_an_ECMAScript_style_module); + } + flags |= ModifierFlags.Default; + break; + case SyntaxKind.DeclareKeyword: + if (flags & ModifierFlags.Ambient) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_already_seen, "declare"); + } + else if (flags & ModifierFlags.Async) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_be_used_in_an_ambient_context, "async"); + } + else if (isClassLike(node.parent) && !isPropertyDeclaration(node)) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_appear_on_a_class_element, "declare"); + } + else if (node.kind === SyntaxKind.Parameter) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_appear_on_a_parameter, "declare"); + } + else if ((node.parent.flags & NodeFlags.Ambient) && node.parent.kind === SyntaxKind.ModuleBlock) { + return grammarErrorOnNode(modifier, Diagnostics.A_declare_modifier_cannot_be_used_in_an_already_ambient_context); + } + else if (isPrivateIdentifierPropertyDeclaration(node)) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_be_used_with_a_private_identifier, "declare"); + } + flags |= ModifierFlags.Ambient; + lastDeclare = modifier; + break; + case SyntaxKind.AbstractKeyword: + if (flags & ModifierFlags.Abstract) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_already_seen, "abstract"); + } + if (node.kind !== SyntaxKind.ClassDeclaration) { + if (node.kind !== SyntaxKind.MethodDeclaration && + node.kind !== SyntaxKind.PropertyDeclaration && + node.kind !== SyntaxKind.GetAccessor && + node.kind !== SyntaxKind.SetAccessor) { + return grammarErrorOnNode(modifier, Diagnostics.abstract_modifier_can_only_appear_on_a_class_method_or_property_declaration); } - else if ((currentKind & DeclarationMeaning.GetOrSetAccessor) && (existingKind & DeclarationMeaning.GetOrSetAccessor)) { - if (existingKind !== DeclarationMeaning.GetOrSetAccessor && currentKind !== existingKind) { - seen.set(effectiveName, currentKind | existingKind); - } - else { - return grammarErrorOnNode(name, Diagnostics.An_object_literal_cannot_have_multiple_get_Slashset_accessors_with_the_same_name); - } + if (!(node.parent.kind === SyntaxKind.ClassDeclaration && hasModifier(node.parent, ModifierFlags.Abstract))) { + return grammarErrorOnNode(modifier, Diagnostics.Abstract_methods_can_only_appear_within_an_abstract_class); } - else { - return grammarErrorOnNode(name, Diagnostics.An_object_literal_cannot_have_property_and_accessor_with_the_same_name); + if (flags & ModifierFlags.Static) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_be_used_with_1_modifier, "static", "abstract"); + } + if (flags & ModifierFlags.Private) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_be_used_with_1_modifier, "private", "abstract"); } } - } + if (isNamedDeclaration(node) && node.name.kind === SyntaxKind.PrivateIdentifier) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_be_used_with_a_private_identifier, "abstract"); + } + flags |= ModifierFlags.Abstract; + break; + case SyntaxKind.AsyncKeyword: + if (flags & ModifierFlags.Async) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_already_seen, "async"); + } + else if (flags & ModifierFlags.Ambient || node.parent.flags & NodeFlags.Ambient) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_be_used_in_an_ambient_context, "async"); + } + else if (node.kind === SyntaxKind.Parameter) { + return grammarErrorOnNode(modifier, Diagnostics._0_modifier_cannot_appear_on_a_parameter, "async"); + } + flags |= ModifierFlags.Async; + lastAsync = modifier; + break; } } - - function checkGrammarJsxElement(node: JsxOpeningLikeElement) { - checkGrammarTypeArguments(node, node.typeArguments); - const seen = createUnderscoreEscapedMap(); - - for (const attr of node.attributes.properties) { - if (attr.kind === SyntaxKind.JsxSpreadAttribute) { - continue; - } - - const { name, initializer } = attr; - if (!seen.get(name.escapedText)) { - seen.set(name.escapedText, true); - } - else { - return grammarErrorOnNode(name, Diagnostics.JSX_elements_cannot_have_multiple_attributes_with_the_same_name); - } - - if (initializer && initializer.kind === SyntaxKind.JsxExpression && !initializer.expression) { - return grammarErrorOnNode(initializer, Diagnostics.JSX_attributes_must_only_be_assigned_a_non_empty_expression); - } + if (node.kind === SyntaxKind.Constructor) { + if (flags & ModifierFlags.Static) { + return grammarErrorOnNode((lastStatic!), Diagnostics._0_modifier_cannot_appear_on_a_constructor_declaration, "static"); } - } - - function checkGrammarJsxExpression(node: JsxExpression) { - if (node.expression && isCommaSequence(node.expression)) { - return grammarErrorOnNode(node.expression, Diagnostics.JSX_expressions_may_not_use_the_comma_operator_Did_you_mean_to_write_an_array); + if (flags & ModifierFlags.Abstract) { + return grammarErrorOnNode((lastStatic!), Diagnostics._0_modifier_cannot_appear_on_a_constructor_declaration, "abstract"); // TODO: GH#18217 } - } - - function checkGrammarForInOrForOfStatement(forInOrOfStatement: ForInOrOfStatement): boolean { - if (checkGrammarStatementInAmbientContext(forInOrOfStatement)) { - return true; + else if (flags & ModifierFlags.Async) { + return grammarErrorOnNode((lastAsync!), Diagnostics._0_modifier_cannot_appear_on_a_constructor_declaration, "async"); } - - if (forInOrOfStatement.kind === SyntaxKind.ForOfStatement && forInOrOfStatement.awaitModifier) { - if ((forInOrOfStatement.flags & NodeFlags.AwaitContext) === NodeFlags.None) { - // use of 'for-await-of' in non-async function - const sourceFile = getSourceFileOfNode(forInOrOfStatement); - if (!hasParseDiagnostics(sourceFile)) { - const diagnostic = createDiagnosticForNode(forInOrOfStatement.awaitModifier, Diagnostics.A_for_await_of_statement_is_only_allowed_within_an_async_function_or_async_generator); - const func = getContainingFunction(forInOrOfStatement); - if (func && func.kind !== SyntaxKind.Constructor) { - Debug.assert((getFunctionFlags(func) & FunctionFlags.Async) === 0, "Enclosing function should never be an async function."); - const relatedInfo = createDiagnosticForNode(func, Diagnostics.Did_you_mean_to_mark_this_function_as_async); - addRelatedInfo(diagnostic, relatedInfo); - } - diagnostics.add(diagnostic); - return true; - } + else if (flags & ModifierFlags.Readonly) { + return grammarErrorOnNode((lastReadonly!), Diagnostics._0_modifier_cannot_appear_on_a_constructor_declaration, "readonly"); + } + return false; + } + else if ((node.kind === SyntaxKind.ImportDeclaration || node.kind === SyntaxKind.ImportEqualsDeclaration) && flags & ModifierFlags.Ambient) { + return grammarErrorOnNode((lastDeclare!), Diagnostics.A_0_modifier_cannot_be_used_with_an_import_declaration, "declare"); + } + else if (node.kind === SyntaxKind.Parameter && (flags & ModifierFlags.ParameterPropertyModifier) && isBindingPattern((node).name)) { + return grammarErrorOnNode(node, Diagnostics.A_parameter_property_may_not_be_declared_using_a_binding_pattern); + } + else if (node.kind === SyntaxKind.Parameter && (flags & ModifierFlags.ParameterPropertyModifier) && (node).dotDotDotToken) { + return grammarErrorOnNode(node, Diagnostics.A_parameter_property_cannot_be_declared_using_a_rest_parameter); + } + if (flags & ModifierFlags.Async) { + return checkGrammarAsyncModifier(node, lastAsync!); + } + return false; + } + /** + * true | false: Early return this value from checkGrammarModifiers. + * undefined: Need to do full checking on the modifiers. + */ + function reportObviousModifierErrors(node: Node): boolean | undefined { + return !node.modifiers + ? false + : shouldReportBadModifier(node) + ? grammarErrorOnFirstToken(node, Diagnostics.Modifiers_cannot_appear_here) + : undefined; + } + function shouldReportBadModifier(node: Node): boolean { + switch (node.kind) { + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + case SyntaxKind.Constructor: + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.PropertySignature: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: + case SyntaxKind.IndexSignature: + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.ImportDeclaration: + case SyntaxKind.ImportEqualsDeclaration: + case SyntaxKind.ExportDeclaration: + case SyntaxKind.ExportAssignment: + case SyntaxKind.FunctionExpression: + case SyntaxKind.ArrowFunction: + case SyntaxKind.Parameter: + return false; + default: + if (node.parent.kind === SyntaxKind.ModuleBlock || node.parent.kind === SyntaxKind.SourceFile) { return false; } - } - - if (forInOrOfStatement.initializer.kind === SyntaxKind.VariableDeclarationList) { - const variableList = forInOrOfStatement.initializer; - if (!checkGrammarVariableDeclarationList(variableList)) { - const declarations = variableList.declarations; - - // declarations.length can be zero if there is an error in variable declaration in for-of or for-in - // See http://www.ecma-international.org/ecma-262/6.0/#sec-for-in-and-for-of-statements for details - // For example: - // var let = 10; - // for (let of [1,2,3]) {} // this is invalid ES6 syntax - // for (let in [1,2,3]) {} // this is invalid ES6 syntax - // We will then want to skip on grammar checking on variableList declaration - if (!declarations.length) { - return false; - } - - if (declarations.length > 1) { - const diagnostic = forInOrOfStatement.kind === SyntaxKind.ForInStatement - ? Diagnostics.Only_a_single_variable_declaration_is_allowed_in_a_for_in_statement - : Diagnostics.Only_a_single_variable_declaration_is_allowed_in_a_for_of_statement; - return grammarErrorOnFirstToken(variableList.declarations[1], diagnostic); - } - const firstDeclaration = declarations[0]; - - if (firstDeclaration.initializer) { - const diagnostic = forInOrOfStatement.kind === SyntaxKind.ForInStatement - ? Diagnostics.The_variable_declaration_of_a_for_in_statement_cannot_have_an_initializer - : Diagnostics.The_variable_declaration_of_a_for_of_statement_cannot_have_an_initializer; - return grammarErrorOnNode(firstDeclaration.name, diagnostic); - } - if (firstDeclaration.type) { - const diagnostic = forInOrOfStatement.kind === SyntaxKind.ForInStatement - ? Diagnostics.The_left_hand_side_of_a_for_in_statement_cannot_use_a_type_annotation - : Diagnostics.The_left_hand_side_of_a_for_of_statement_cannot_use_a_type_annotation; - return grammarErrorOnNode(firstDeclaration, diagnostic); - } + switch (node.kind) { + case SyntaxKind.FunctionDeclaration: + return nodeHasAnyModifiersExcept(node, SyntaxKind.AsyncKeyword); + case SyntaxKind.ClassDeclaration: + return nodeHasAnyModifiersExcept(node, SyntaxKind.AbstractKeyword); + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.VariableStatement: + case SyntaxKind.TypeAliasDeclaration: + return true; + case SyntaxKind.EnumDeclaration: + return nodeHasAnyModifiersExcept(node, SyntaxKind.ConstKeyword); + default: + Debug.fail(); + return false; + } + } + } + function nodeHasAnyModifiersExcept(node: Node, allowedModifier: SyntaxKind): boolean { + return node.modifiers!.length > 1 || node.modifiers![0].kind !== allowedModifier; + } + function checkGrammarAsyncModifier(node: Node, asyncModifier: Node): boolean { + switch (node.kind) { + case SyntaxKind.MethodDeclaration: + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.FunctionExpression: + case SyntaxKind.ArrowFunction: + return false; + } + return grammarErrorOnNode(asyncModifier, Diagnostics._0_modifier_cannot_be_used_here, "async"); + } + function checkGrammarForDisallowedTrailingComma(list: NodeArray | undefined, diag = Diagnostics.Trailing_comma_not_allowed): boolean { + if (list && list.hasTrailingComma) { + return grammarErrorAtPos(list[0], list.end - ",".length, ",".length, diag); + } + return false; + } + function checkGrammarTypeParameterList(typeParameters: NodeArray | undefined, file: SourceFile): boolean { + if (typeParameters && typeParameters.length === 0) { + const start = typeParameters.pos - "<".length; + const end = skipTrivia(file.text, typeParameters.end) + ">".length; + return grammarErrorAtPos(file, start, end - start, Diagnostics.Type_parameter_list_cannot_be_empty); + } + return false; + } + function checkGrammarParameterList(parameters: NodeArray) { + let seenOptionalParameter = false; + const parameterCount = parameters.length; + for (let i = 0; i < parameterCount; i++) { + const parameter = parameters[i]; + if (parameter.dotDotDotToken) { + if (i !== (parameterCount - 1)) { + return grammarErrorOnNode(parameter.dotDotDotToken, Diagnostics.A_rest_parameter_must_be_last_in_a_parameter_list); + } + if (!(parameter.flags & NodeFlags.Ambient)) { // Allow `...foo,` in ambient declarations; see GH#23070 + checkGrammarForDisallowedTrailingComma(parameters, Diagnostics.A_rest_parameter_or_binding_pattern_may_not_have_a_trailing_comma); } - } - - return false; - } - - function checkGrammarAccessor(accessor: AccessorDeclaration): boolean { - if (!(accessor.flags & NodeFlags.Ambient)) { - if (languageVersion < ScriptTarget.ES5) { - return grammarErrorOnNode(accessor.name, Diagnostics.Accessors_are_only_available_when_targeting_ECMAScript_5_and_higher); + if (parameter.questionToken) { + return grammarErrorOnNode(parameter.questionToken, Diagnostics.A_rest_parameter_cannot_be_optional); } - if (accessor.body === undefined && !hasModifier(accessor, ModifierFlags.Abstract)) { - return grammarErrorAtPos(accessor, accessor.end - 1, ";".length, Diagnostics._0_expected, "{"); + if (parameter.initializer) { + return grammarErrorOnNode(parameter.name, Diagnostics.A_rest_parameter_cannot_have_an_initializer); } } - if (accessor.body && hasModifier(accessor, ModifierFlags.Abstract)) { - return grammarErrorOnNode(accessor, Diagnostics.An_abstract_accessor_cannot_have_an_implementation); - } - if (accessor.typeParameters) { - return grammarErrorOnNode(accessor.name, Diagnostics.An_accessor_cannot_have_type_parameters); + else if (parameter.questionToken) { + seenOptionalParameter = true; + if (parameter.initializer) { + return grammarErrorOnNode(parameter.name, Diagnostics.Parameter_cannot_have_question_mark_and_initializer); + } } - if (!doesAccessorHaveCorrectParameterCount(accessor)) { - return grammarErrorOnNode(accessor.name, - accessor.kind === SyntaxKind.GetAccessor ? - Diagnostics.A_get_accessor_cannot_have_parameters : - Diagnostics.A_set_accessor_must_have_exactly_one_parameter); + else if (seenOptionalParameter && !parameter.initializer) { + return grammarErrorOnNode(parameter.name, Diagnostics.A_required_parameter_cannot_follow_an_optional_parameter); } - if (accessor.kind === SyntaxKind.SetAccessor) { - if (accessor.type) { - return grammarErrorOnNode(accessor.name, Diagnostics.A_set_accessor_cannot_have_a_return_type_annotation); - } - const parameter = Debug.checkDefined(getSetAccessorValueParameter(accessor), "Return value does not match parameter count assertion."); - if (parameter.dotDotDotToken) { - return grammarErrorOnNode(parameter.dotDotDotToken, Diagnostics.A_set_accessor_cannot_have_rest_parameter); - } - if (parameter.questionToken) { - return grammarErrorOnNode(parameter.questionToken, Diagnostics.A_set_accessor_cannot_have_an_optional_parameter); - } - if (parameter.initializer) { - return grammarErrorOnNode(accessor.name, Diagnostics.A_set_accessor_parameter_cannot_have_an_initializer); + } + } + function getNonSimpleParameters(parameters: readonly ParameterDeclaration[]): readonly ParameterDeclaration[] { + return filter(parameters, parameter => !!parameter.initializer || isBindingPattern(parameter.name) || isRestParameter(parameter)); + } + function checkGrammarForUseStrictSimpleParameterList(node: FunctionLikeDeclaration): boolean { + if (languageVersion >= ScriptTarget.ES2016) { + const useStrictDirective = node.body && isBlock(node.body) && findUseStrictPrologue(node.body.statements); + if (useStrictDirective) { + const nonSimpleParameters = getNonSimpleParameters(node.parameters); + if (length(nonSimpleParameters)) { + forEach(nonSimpleParameters, parameter => { + addRelatedInfo(error(parameter, Diagnostics.This_parameter_is_not_allowed_with_use_strict_directive), createDiagnosticForNode(useStrictDirective, Diagnostics.use_strict_directive_used_here)); + }); + const diagnostics = (nonSimpleParameters.map((parameter, index) => (index === 0 ? createDiagnosticForNode(parameter, Diagnostics.Non_simple_parameter_declared_here) : createDiagnosticForNode(parameter, Diagnostics.and_here))) as [DiagnosticWithLocation, ...DiagnosticWithLocation[]]); + addRelatedInfo(error(useStrictDirective, Diagnostics.use_strict_directive_cannot_be_used_with_non_simple_parameter_list), ...diagnostics); + return true; } } - return false; } - - /** Does the accessor have the right number of parameters? - * A get accessor has no parameters or a single `this` parameter. - * A set accessor has one parameter or a `this` parameter and one more parameter. - */ - function doesAccessorHaveCorrectParameterCount(accessor: AccessorDeclaration) { - return getAccessorThisParameter(accessor) || accessor.parameters.length === (accessor.kind === SyntaxKind.GetAccessor ? 0 : 1); + return false; + } + function checkGrammarFunctionLikeDeclaration(node: FunctionLikeDeclaration | MethodSignature): boolean { + // Prevent cascading error by short-circuit + const file = getSourceFileOfNode(node); + return checkGrammarDecoratorsAndModifiers(node) || checkGrammarTypeParameterList(node.typeParameters, file) || + checkGrammarParameterList(node.parameters) || checkGrammarArrowFunction(node, file) || + (isFunctionLikeDeclaration(node) && checkGrammarForUseStrictSimpleParameterList(node)); + } + function checkGrammarClassLikeDeclaration(node: ClassLikeDeclaration): boolean { + const file = getSourceFileOfNode(node); + return checkGrammarClassDeclarationHeritageClauses(node) || checkGrammarTypeParameterList(node.typeParameters, file); + } + function checkGrammarArrowFunction(node: Node, file: SourceFile): boolean { + if (!isArrowFunction(node)) { + return false; } - - function getAccessorThisParameter(accessor: AccessorDeclaration): ParameterDeclaration | undefined { - if (accessor.parameters.length === (accessor.kind === SyntaxKind.GetAccessor ? 1 : 2)) { - return getThisParameter(accessor); + const { equalsGreaterThanToken } = node; + const startLine = getLineAndCharacterOfPosition(file, equalsGreaterThanToken.pos).line; + const endLine = getLineAndCharacterOfPosition(file, equalsGreaterThanToken.end).line; + return startLine !== endLine && grammarErrorOnNode(equalsGreaterThanToken, Diagnostics.Line_terminator_not_permitted_before_arrow); + } + function checkGrammarIndexSignatureParameters(node: SignatureDeclaration): boolean { + const parameter = node.parameters[0]; + if (node.parameters.length !== 1) { + if (parameter) { + return grammarErrorOnNode(parameter.name, Diagnostics.An_index_signature_must_have_exactly_one_parameter); + } + else { + return grammarErrorOnNode(node, Diagnostics.An_index_signature_must_have_exactly_one_parameter); } } - - function checkGrammarTypeOperatorNode(node: TypeOperatorNode) { - if (node.operator === SyntaxKind.UniqueKeyword) { - if (node.type.kind !== SyntaxKind.SymbolKeyword) { - return grammarErrorOnNode(node.type, Diagnostics._0_expected, tokenToString(SyntaxKind.SymbolKeyword)); - } - - const parent = walkUpParenthesizedTypes(node.parent); - switch (parent.kind) { - case SyntaxKind.VariableDeclaration: - const decl = parent as VariableDeclaration; - if (decl.name.kind !== SyntaxKind.Identifier) { - return grammarErrorOnNode(node, Diagnostics.unique_symbol_types_may_not_be_used_on_a_variable_declaration_with_a_binding_name); - } - if (!isVariableDeclarationInVariableStatement(decl)) { - return grammarErrorOnNode(node, Diagnostics.unique_symbol_types_are_only_allowed_on_variables_in_a_variable_statement); - } - if (!(decl.parent.flags & NodeFlags.Const)) { - return grammarErrorOnNode((parent).name, Diagnostics.A_variable_whose_type_is_a_unique_symbol_type_must_be_const); - } - break; - - case SyntaxKind.PropertyDeclaration: - if (!hasModifier(parent, ModifierFlags.Static) || - !hasModifier(parent, ModifierFlags.Readonly)) { - return grammarErrorOnNode((parent).name, Diagnostics.A_property_of_a_class_whose_type_is_a_unique_symbol_type_must_be_both_static_and_readonly); - } - break; - - case SyntaxKind.PropertySignature: - if (!hasModifier(parent, ModifierFlags.Readonly)) { - return grammarErrorOnNode((parent).name, Diagnostics.A_property_of_an_interface_or_type_literal_whose_type_is_a_unique_symbol_type_must_be_readonly); - } - break; - - default: - return grammarErrorOnNode(node, Diagnostics.unique_symbol_types_are_not_allowed_here); - } + if (parameter.dotDotDotToken) { + return grammarErrorOnNode(parameter.dotDotDotToken, Diagnostics.An_index_signature_cannot_have_a_rest_parameter); + } + if (hasModifiers(parameter)) { + return grammarErrorOnNode(parameter.name, Diagnostics.An_index_signature_parameter_cannot_have_an_accessibility_modifier); + } + if (parameter.questionToken) { + return grammarErrorOnNode(parameter.questionToken, Diagnostics.An_index_signature_parameter_cannot_have_a_question_mark); + } + if (parameter.initializer) { + return grammarErrorOnNode(parameter.name, Diagnostics.An_index_signature_parameter_cannot_have_an_initializer); + } + if (!parameter.type) { + return grammarErrorOnNode(parameter.name, Diagnostics.An_index_signature_parameter_must_have_a_type_annotation); + } + if (parameter.type.kind !== SyntaxKind.StringKeyword && parameter.type.kind !== SyntaxKind.NumberKeyword) { + const type = getTypeFromTypeNode(parameter.type); + if (type.flags & TypeFlags.String || type.flags & TypeFlags.Number) { + return grammarErrorOnNode(parameter.name, Diagnostics.An_index_signature_parameter_type_cannot_be_a_type_alias_Consider_writing_0_Colon_1_Colon_2_instead, getTextOfNode(parameter.name), typeToString(type), typeToString(node.type ? getTypeFromTypeNode(node.type) : anyType)); } - else if (node.operator === SyntaxKind.ReadonlyKeyword) { - if (node.type.kind !== SyntaxKind.ArrayType && node.type.kind !== SyntaxKind.TupleType) { - return grammarErrorOnFirstToken(node, Diagnostics.readonly_type_modifier_is_only_permitted_on_array_and_tuple_literal_types, tokenToString(SyntaxKind.SymbolKeyword)); - } + if (type.flags & TypeFlags.Union && allTypesAssignableToKind(type, TypeFlags.StringOrNumberLiteral, /*strict*/ true)) { + return grammarErrorOnNode(parameter.name, Diagnostics.An_index_signature_parameter_type_cannot_be_a_union_type_Consider_using_a_mapped_object_type_instead); } + return grammarErrorOnNode(parameter.name, Diagnostics.An_index_signature_parameter_type_must_be_either_string_or_number); } - - function checkGrammarForInvalidDynamicName(node: DeclarationName, message: DiagnosticMessage) { - if (isNonBindableDynamicName(node)) { - return grammarErrorOnNode(node, message); - } + if (!node.type) { + return grammarErrorOnNode(node, Diagnostics.An_index_signature_must_have_a_type_annotation); } - - function checkGrammarMethod(node: MethodDeclaration | MethodSignature) { - if (checkGrammarFunctionLikeDeclaration(node)) { - return true; + return false; + } + function checkGrammarIndexSignature(node: SignatureDeclaration) { + // Prevent cascading error by short-circuit + return checkGrammarDecoratorsAndModifiers(node) || checkGrammarIndexSignatureParameters(node); + } + function checkGrammarForAtLeastOneTypeArgument(node: Node, typeArguments: NodeArray | undefined): boolean { + if (typeArguments && typeArguments.length === 0) { + const sourceFile = getSourceFileOfNode(node); + const start = typeArguments.pos - "<".length; + const end = skipTrivia(sourceFile.text, typeArguments.end) + ">".length; + return grammarErrorAtPos(sourceFile, start, end - start, Diagnostics.Type_argument_list_cannot_be_empty); + } + return false; + } + function checkGrammarTypeArguments(node: Node, typeArguments: NodeArray | undefined): boolean { + return checkGrammarForDisallowedTrailingComma(typeArguments) || + checkGrammarForAtLeastOneTypeArgument(node, typeArguments); + } + function checkGrammarTaggedTemplateChain(node: TaggedTemplateExpression): boolean { + if (node.questionDotToken || node.flags & NodeFlags.OptionalChain) { + return grammarErrorOnNode(node.template, Diagnostics.Tagged_template_expressions_are_not_permitted_in_an_optional_chain); + } + return false; + } + function checkGrammarForOmittedArgument(args: NodeArray | undefined): boolean { + if (args) { + for (const arg of args) { + if (arg.kind === SyntaxKind.OmittedExpression) { + return grammarErrorAtPos(arg, arg.pos, 0, Diagnostics.Argument_expression_expected); + } } - - if (node.kind === SyntaxKind.MethodDeclaration) { - if (node.parent.kind === SyntaxKind.ObjectLiteralExpression) { - // We only disallow modifier on a method declaration if it is a property of object-literal-expression - if (node.modifiers && !(node.modifiers.length === 1 && first(node.modifiers).kind === SyntaxKind.AsyncKeyword)) { - return grammarErrorOnFirstToken(node, Diagnostics.Modifiers_cannot_appear_here); - } - else if (checkGrammarForInvalidQuestionMark(node.questionToken, Diagnostics.An_object_member_cannot_be_declared_optional)) { - return true; + } + return false; + } + function checkGrammarArguments(args: NodeArray | undefined): boolean { + return checkGrammarForOmittedArgument(args); + } + function checkGrammarHeritageClause(node: HeritageClause): boolean { + const types = node.types; + if (checkGrammarForDisallowedTrailingComma(types)) { + return true; + } + if (types && types.length === 0) { + const listType = tokenToString(node.token); + return grammarErrorAtPos(node, types.pos, 0, Diagnostics._0_list_cannot_be_empty, listType); + } + return some(types, checkGrammarExpressionWithTypeArguments); + } + function checkGrammarExpressionWithTypeArguments(node: ExpressionWithTypeArguments) { + return checkGrammarTypeArguments(node, node.typeArguments); + } + function checkGrammarClassDeclarationHeritageClauses(node: ClassLikeDeclaration) { + let seenExtendsClause = false; + let seenImplementsClause = false; + if (!checkGrammarDecoratorsAndModifiers(node) && node.heritageClauses) { + for (const heritageClause of node.heritageClauses) { + if (heritageClause.token === SyntaxKind.ExtendsKeyword) { + if (seenExtendsClause) { + return grammarErrorOnFirstToken(heritageClause, Diagnostics.extends_clause_already_seen); } - else if (checkGrammarForInvalidExclamationToken(node.exclamationToken, Diagnostics.A_definite_assignment_assertion_is_not_permitted_in_this_context)) { - return true; + if (seenImplementsClause) { + return grammarErrorOnFirstToken(heritageClause, Diagnostics.extends_clause_must_precede_implements_clause); } - else if (node.body === undefined) { - return grammarErrorAtPos(node, node.end - 1, ";".length, Diagnostics._0_expected, "{"); + if (heritageClause.types.length > 1) { + return grammarErrorOnFirstToken(heritageClause.types[1], Diagnostics.Classes_can_only_extend_a_single_class); } + seenExtendsClause = true; } - if (checkGrammarForGenerator(node)) { - return true; + else { + Debug.assert(heritageClause.token === SyntaxKind.ImplementsKeyword); + if (seenImplementsClause) { + return grammarErrorOnFirstToken(heritageClause, Diagnostics.implements_clause_already_seen); + } + seenImplementsClause = true; } + // Grammar checking heritageClause inside class declaration + checkGrammarHeritageClause(heritageClause); } - - if (isClassLike(node.parent)) { - // Technically, computed properties in ambient contexts is disallowed - // for property declarations and accessors too, not just methods. - // However, property declarations disallow computed names in general, - // and accessors are not allowed in ambient contexts in general, - // so this error only really matters for methods. - if (node.flags & NodeFlags.Ambient) { - return checkGrammarForInvalidDynamicName(node.name, Diagnostics.A_computed_property_name_in_an_ambient_context_must_refer_to_an_expression_whose_type_is_a_literal_type_or_a_unique_symbol_type); + } + } + function checkGrammarInterfaceDeclaration(node: InterfaceDeclaration) { + let seenExtendsClause = false; + if (node.heritageClauses) { + for (const heritageClause of node.heritageClauses) { + if (heritageClause.token === SyntaxKind.ExtendsKeyword) { + if (seenExtendsClause) { + return grammarErrorOnFirstToken(heritageClause, Diagnostics.extends_clause_already_seen); + } + seenExtendsClause = true; } - else if (node.kind === SyntaxKind.MethodDeclaration && !node.body) { - return checkGrammarForInvalidDynamicName(node.name, Diagnostics.A_computed_property_name_in_a_method_overload_must_refer_to_an_expression_whose_type_is_a_literal_type_or_a_unique_symbol_type); + else { + Debug.assert(heritageClause.token === SyntaxKind.ImplementsKeyword); + return grammarErrorOnFirstToken(heritageClause, Diagnostics.Interface_declaration_cannot_have_implements_clause); } + // Grammar checking heritageClause inside class declaration + checkGrammarHeritageClause(heritageClause); } - else if (node.parent.kind === SyntaxKind.InterfaceDeclaration) { - return checkGrammarForInvalidDynamicName(node.name, Diagnostics.A_computed_property_name_in_an_interface_must_refer_to_an_expression_whose_type_is_a_literal_type_or_a_unique_symbol_type); + } + return false; + } + function checkGrammarComputedPropertyName(node: Node): boolean { + // If node is not a computedPropertyName, just skip the grammar checking + if (node.kind !== SyntaxKind.ComputedPropertyName) { + return false; + } + const computedPropertyName = (node); + if (computedPropertyName.expression.kind === SyntaxKind.BinaryExpression && (computedPropertyName.expression).operatorToken.kind === SyntaxKind.CommaToken) { + return grammarErrorOnNode(computedPropertyName.expression, Diagnostics.A_comma_expression_is_not_allowed_in_a_computed_property_name); + } + return false; + } + function checkGrammarForGenerator(node: FunctionLikeDeclaration) { + if (node.asteriskToken) { + Debug.assert(node.kind === SyntaxKind.FunctionDeclaration || + node.kind === SyntaxKind.FunctionExpression || + node.kind === SyntaxKind.MethodDeclaration); + if (node.flags & NodeFlags.Ambient) { + return grammarErrorOnNode(node.asteriskToken, Diagnostics.Generators_are_not_allowed_in_an_ambient_context); } - else if (node.parent.kind === SyntaxKind.TypeLiteral) { - return checkGrammarForInvalidDynamicName(node.name, Diagnostics.A_computed_property_name_in_a_type_literal_must_refer_to_an_expression_whose_type_is_a_literal_type_or_a_unique_symbol_type); + if (!node.body) { + return grammarErrorOnNode(node.asteriskToken, Diagnostics.An_overload_signature_cannot_be_declared_as_a_generator); } } - - function checkGrammarBreakOrContinueStatement(node: BreakOrContinueStatement): boolean { - let current: Node = node; - while (current) { - if (isFunctionLike(current)) { - return grammarErrorOnNode(node, Diagnostics.Jump_target_cannot_cross_function_boundary); + } + function checkGrammarForInvalidQuestionMark(questionToken: QuestionToken | undefined, message: DiagnosticMessage): boolean { + return !!questionToken && grammarErrorOnNode(questionToken, message); + } + function checkGrammarForInvalidExclamationToken(exclamationToken: ExclamationToken | undefined, message: DiagnosticMessage): boolean { + return !!exclamationToken && grammarErrorOnNode(exclamationToken, message); + } + function checkGrammarObjectLiteralExpression(node: ObjectLiteralExpression, inDestructuring: boolean) { + const seen = createUnderscoreEscapedMap(); + for (const prop of node.properties) { + if (prop.kind === SyntaxKind.SpreadAssignment) { + if (inDestructuring) { + // a rest property cannot be destructured any further + const expression = skipParentheses(prop.expression); + if (isArrayLiteralExpression(expression) || isObjectLiteralExpression(expression)) { + return grammarErrorOnNode(prop.expression, Diagnostics.A_rest_element_cannot_contain_a_binding_pattern); + } + } + continue; + } + const name = prop.name; + if (name.kind === SyntaxKind.ComputedPropertyName) { + // If the name is not a ComputedPropertyName, the grammar checking will skip it + checkGrammarComputedPropertyName(name); + } + if (prop.kind === SyntaxKind.ShorthandPropertyAssignment && !inDestructuring && prop.objectAssignmentInitializer) { + // having objectAssignmentInitializer is only valid in ObjectAssignmentPattern + // outside of destructuring it is a syntax error + return grammarErrorOnNode((prop.equalsToken!), Diagnostics.can_only_be_used_in_an_object_literal_property_inside_a_destructuring_assignment); + } + if (name.kind === SyntaxKind.PrivateIdentifier) { + return grammarErrorOnNode(name, Diagnostics.Private_identifiers_are_not_allowed_outside_class_bodies); + } + // Modifiers are never allowed on properties except for 'async' on a method declaration + if (prop.modifiers) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + for (const mod of prop.modifiers!) { // TODO: GH#19955 + if (mod.kind !== SyntaxKind.AsyncKeyword || prop.kind !== SyntaxKind.MethodDeclaration) { + grammarErrorOnNode(mod, Diagnostics._0_modifier_cannot_be_used_here, getTextOfNode(mod)); + } + } + } + // ECMA-262 11.1.5 Object Initializer + // If previous is not undefined then throw a SyntaxError exception if any of the following conditions are true + // a.This production is contained in strict code and IsDataDescriptor(previous) is true and + // IsDataDescriptor(propId.descriptor) is true. + // b.IsDataDescriptor(previous) is true and IsAccessorDescriptor(propId.descriptor) is true. + // c.IsAccessorDescriptor(previous) is true and IsDataDescriptor(propId.descriptor) is true. + // d.IsAccessorDescriptor(previous) is true and IsAccessorDescriptor(propId.descriptor) is true + // and either both previous and propId.descriptor have[[Get]] fields or both previous and propId.descriptor have[[Set]] fields + let currentKind: DeclarationMeaning; + switch (prop.kind) { + case SyntaxKind.ShorthandPropertyAssignment: + checkGrammarForInvalidExclamationToken(prop.exclamationToken, Diagnostics.A_definite_assignment_assertion_is_not_permitted_in_this_context); + // falls through + case SyntaxKind.PropertyAssignment: + // Grammar checking for computedPropertyName and shorthandPropertyAssignment + checkGrammarForInvalidQuestionMark(prop.questionToken, Diagnostics.An_object_member_cannot_be_declared_optional); + if (name.kind === SyntaxKind.NumericLiteral) { + checkGrammarNumericLiteral(name); + } + currentKind = DeclarationMeaning.PropertyAssignment; + break; + case SyntaxKind.MethodDeclaration: + currentKind = DeclarationMeaning.Method; + break; + case SyntaxKind.GetAccessor: + currentKind = DeclarationMeaning.GetAccessor; + break; + case SyntaxKind.SetAccessor: + currentKind = DeclarationMeaning.SetAccessor; + break; + default: + throw Debug.assertNever(prop, "Unexpected syntax kind:" + (prop).kind); + } + if (!inDestructuring) { + const effectiveName = getPropertyNameForPropertyNameNode(name); + if (effectiveName === undefined) { + continue; } - - switch (current.kind) { - case SyntaxKind.LabeledStatement: - if (node.label && (current).label.escapedText === node.label.escapedText) { - // found matching label - verify that label usage is correct - // continue can only target labels that are on iteration statements - const isMisplacedContinueLabel = node.kind === SyntaxKind.ContinueStatement - && !isIterationStatement((current).statement, /*lookInLabeledStatement*/ true); - - if (isMisplacedContinueLabel) { - return grammarErrorOnNode(node, Diagnostics.A_continue_statement_can_only_jump_to_a_label_of_an_enclosing_iteration_statement); - } - - return false; - } - break; - case SyntaxKind.SwitchStatement: - if (node.kind === SyntaxKind.BreakStatement && !node.label) { - // unlabeled break within switch statement - ok - return false; + const existingKind = seen.get(effectiveName); + if (!existingKind) { + seen.set(effectiveName, currentKind); + } + else { + if ((currentKind & DeclarationMeaning.PropertyAssignmentOrMethod) && (existingKind & DeclarationMeaning.PropertyAssignmentOrMethod)) { + grammarErrorOnNode(name, Diagnostics.Duplicate_identifier_0, getTextOfNode(name)); + } + else if ((currentKind & DeclarationMeaning.GetOrSetAccessor) && (existingKind & DeclarationMeaning.GetOrSetAccessor)) { + if (existingKind !== DeclarationMeaning.GetOrSetAccessor && currentKind !== existingKind) { + seen.set(effectiveName, currentKind | existingKind); } - break; - default: - if (isIterationStatement(current, /*lookInLabeledStatement*/ false) && !node.label) { - // unlabeled break or continue within iteration statement - ok - return false; + else { + return grammarErrorOnNode(name, Diagnostics.An_object_literal_cannot_have_multiple_get_Slashset_accessors_with_the_same_name); } - break; + } + else { + return grammarErrorOnNode(name, Diagnostics.An_object_literal_cannot_have_property_and_accessor_with_the_same_name); + } } - - current = current.parent; } - - if (node.label) { - const message = node.kind === SyntaxKind.BreakStatement - ? Diagnostics.A_break_statement_can_only_jump_to_a_label_of_an_enclosing_statement - : Diagnostics.A_continue_statement_can_only_jump_to_a_label_of_an_enclosing_iteration_statement; - - return grammarErrorOnNode(node, message); + } + } + function checkGrammarJsxElement(node: JsxOpeningLikeElement) { + checkGrammarTypeArguments(node, node.typeArguments); + const seen = createUnderscoreEscapedMap(); + for (const attr of node.attributes.properties) { + if (attr.kind === SyntaxKind.JsxSpreadAttribute) { + continue; + } + const { name, initializer } = attr; + if (!seen.get(name.escapedText)) { + seen.set(name.escapedText, true); } else { - const message = node.kind === SyntaxKind.BreakStatement - ? Diagnostics.A_break_statement_can_only_be_used_within_an_enclosing_iteration_or_switch_statement - : Diagnostics.A_continue_statement_can_only_be_used_within_an_enclosing_iteration_statement; - return grammarErrorOnNode(node, message); + return grammarErrorOnNode(name, Diagnostics.JSX_elements_cannot_have_multiple_attributes_with_the_same_name); } - } - - function checkGrammarBindingElement(node: BindingElement) { - if (node.dotDotDotToken) { - const elements = node.parent.elements; - if (node !== last(elements)) { - return grammarErrorOnNode(node, Diagnostics.A_rest_element_must_be_last_in_a_destructuring_pattern); - } - checkGrammarForDisallowedTrailingComma(elements, Diagnostics.A_rest_parameter_or_binding_pattern_may_not_have_a_trailing_comma); - - if (node.propertyName) { - return grammarErrorOnNode(node.name, Diagnostics.A_rest_element_cannot_have_a_property_name); - } - - if (node.initializer) { - // Error on equals token which immediately precedes the initializer - return grammarErrorAtPos(node, node.initializer.pos - 1, 1, Diagnostics.A_rest_element_cannot_have_an_initializer); - } + if (initializer && initializer.kind === SyntaxKind.JsxExpression && !initializer.expression) { + return grammarErrorOnNode(initializer, Diagnostics.JSX_attributes_must_only_be_assigned_a_non_empty_expression); } } - - function isStringOrNumberLiteralExpression(expr: Expression) { - return isStringOrNumericLiteralLike(expr) || - expr.kind === SyntaxKind.PrefixUnaryExpression && (expr).operator === SyntaxKind.MinusToken && - (expr).operand.kind === SyntaxKind.NumericLiteral; + } + function checkGrammarJsxExpression(node: JsxExpression) { + if (node.expression && isCommaSequence(node.expression)) { + return grammarErrorOnNode(node.expression, Diagnostics.JSX_expressions_may_not_use_the_comma_operator_Did_you_mean_to_write_an_array); } - - function isBigIntLiteralExpression(expr: Expression) { - return expr.kind === SyntaxKind.BigIntLiteral || - expr.kind === SyntaxKind.PrefixUnaryExpression && (expr).operator === SyntaxKind.MinusToken && - (expr).operand.kind === SyntaxKind.BigIntLiteral; + } + function checkGrammarForInOrForOfStatement(forInOrOfStatement: ForInOrOfStatement): boolean { + if (checkGrammarStatementInAmbientContext(forInOrOfStatement)) { + return true; } - - function isSimpleLiteralEnumReference(expr: Expression) { - if ((isPropertyAccessExpression(expr) || (isElementAccessExpression(expr) && isStringOrNumberLiteralExpression(expr.argumentExpression))) && - isEntityNameExpression(expr.expression)) { - return !!(checkExpressionCached(expr).flags & TypeFlags.EnumLiteral); + if (forInOrOfStatement.kind === SyntaxKind.ForOfStatement && forInOrOfStatement.awaitModifier) { + if ((forInOrOfStatement.flags & NodeFlags.AwaitContext) === NodeFlags.None) { + // use of 'for-await-of' in non-async function + const sourceFile = getSourceFileOfNode(forInOrOfStatement); + if (!hasParseDiagnostics(sourceFile)) { + const diagnostic = createDiagnosticForNode(forInOrOfStatement.awaitModifier, Diagnostics.A_for_await_of_statement_is_only_allowed_within_an_async_function_or_async_generator); + const func = getContainingFunction(forInOrOfStatement); + if (func && func.kind !== SyntaxKind.Constructor) { + Debug.assert((getFunctionFlags(func) & FunctionFlags.Async) === 0, "Enclosing function should never be an async function."); + const relatedInfo = createDiagnosticForNode(func, Diagnostics.Did_you_mean_to_mark_this_function_as_async); + addRelatedInfo(diagnostic, relatedInfo); + } + diagnostics.add(diagnostic); + return true; + } + return false; } } - - function checkAmbientInitializer(node: VariableDeclaration | PropertyDeclaration | PropertySignature) { - const {initializer} = node; - if (initializer) { - const isInvalidInitializer = !( - isStringOrNumberLiteralExpression(initializer) || - isSimpleLiteralEnumReference(initializer) || - initializer.kind === SyntaxKind.TrueKeyword || initializer.kind === SyntaxKind.FalseKeyword || - isBigIntLiteralExpression(initializer) - ); - const isConstOrReadonly = isDeclarationReadonly(node) || isVariableDeclaration(node) && isVarConst(node); - if (isConstOrReadonly && !node.type) { - if (isInvalidInitializer) { - return grammarErrorOnNode(initializer, Diagnostics.A_const_initializer_in_an_ambient_context_must_be_a_string_or_numeric_literal_or_literal_enum_reference); - } - } - else { - return grammarErrorOnNode(initializer, Diagnostics.Initializers_are_not_allowed_in_ambient_contexts); + if (forInOrOfStatement.initializer.kind === SyntaxKind.VariableDeclarationList) { + const variableList = (forInOrOfStatement.initializer); + if (!checkGrammarVariableDeclarationList(variableList)) { + const declarations = variableList.declarations; + // declarations.length can be zero if there is an error in variable declaration in for-of or for-in + // See http://www.ecma-international.org/ecma-262/6.0/#sec-for-in-and-for-of-statements for details + // For example: + // var let = 10; + // for (let of [1,2,3]) {} // this is invalid ES6 syntax + // for (let in [1,2,3]) {} // this is invalid ES6 syntax + // We will then want to skip on grammar checking on variableList declaration + if (!declarations.length) { + return false; } - if (!isConstOrReadonly || isInvalidInitializer) { - return grammarErrorOnNode(initializer, Diagnostics.Initializers_are_not_allowed_in_ambient_contexts); + if (declarations.length > 1) { + const diagnostic = forInOrOfStatement.kind === SyntaxKind.ForInStatement + ? Diagnostics.Only_a_single_variable_declaration_is_allowed_in_a_for_in_statement + : Diagnostics.Only_a_single_variable_declaration_is_allowed_in_a_for_of_statement; + return grammarErrorOnFirstToken(variableList.declarations[1], diagnostic); } - } - } - - function checkGrammarVariableDeclaration(node: VariableDeclaration) { - if (node.parent.parent.kind !== SyntaxKind.ForInStatement && node.parent.parent.kind !== SyntaxKind.ForOfStatement) { - if (node.flags & NodeFlags.Ambient) { - checkAmbientInitializer(node); + const firstDeclaration = declarations[0]; + if (firstDeclaration.initializer) { + const diagnostic = forInOrOfStatement.kind === SyntaxKind.ForInStatement + ? Diagnostics.The_variable_declaration_of_a_for_in_statement_cannot_have_an_initializer + : Diagnostics.The_variable_declaration_of_a_for_of_statement_cannot_have_an_initializer; + return grammarErrorOnNode(firstDeclaration.name, diagnostic); } - else if (!node.initializer) { - if (isBindingPattern(node.name) && !isBindingPattern(node.parent)) { - return grammarErrorOnNode(node, Diagnostics.A_destructuring_declaration_must_have_an_initializer); - } - if (isVarConst(node)) { - return grammarErrorOnNode(node, Diagnostics.const_declarations_must_be_initialized); - } + if (firstDeclaration.type) { + const diagnostic = forInOrOfStatement.kind === SyntaxKind.ForInStatement + ? Diagnostics.The_left_hand_side_of_a_for_in_statement_cannot_use_a_type_annotation + : Diagnostics.The_left_hand_side_of_a_for_of_statement_cannot_use_a_type_annotation; + return grammarErrorOnNode(firstDeclaration, diagnostic); } } - - if (node.exclamationToken && (node.parent.parent.kind !== SyntaxKind.VariableStatement || !node.type || node.initializer || node.flags & NodeFlags.Ambient)) { - return grammarErrorOnNode(node.exclamationToken, Diagnostics.Definite_assignment_assertions_can_only_be_used_along_with_a_type_annotation); + } + return false; + } + function checkGrammarAccessor(accessor: AccessorDeclaration): boolean { + if (!(accessor.flags & NodeFlags.Ambient)) { + if (languageVersion < ScriptTarget.ES5) { + return grammarErrorOnNode(accessor.name, Diagnostics.Accessors_are_only_available_when_targeting_ECMAScript_5_and_higher); } - - const moduleKind = getEmitModuleKind(compilerOptions); - - if (moduleKind < ModuleKind.ES2015 && moduleKind !== ModuleKind.System && !compilerOptions.noEmit && - !(node.parent.parent.flags & NodeFlags.Ambient) && hasModifier(node.parent.parent, ModifierFlags.Export)) { - checkESModuleMarker(node.name); + if (accessor.body === undefined && !hasModifier(accessor, ModifierFlags.Abstract)) { + return grammarErrorAtPos(accessor, accessor.end - 1, ";".length, Diagnostics._0_expected, "{"); } - - const checkLetConstNames = (isLet(node) || isVarConst(node)); - - // 1. LexicalDeclaration : LetOrConst BindingList ; - // It is a Syntax Error if the BoundNames of BindingList contains "let". - // 2. ForDeclaration: ForDeclaration : LetOrConst ForBinding - // It is a Syntax Error if the BoundNames of ForDeclaration contains "let". - - // It is a SyntaxError if a VariableDeclaration or VariableDeclarationNoIn occurs within strict code - // and its Identifier is eval or arguments - return checkLetConstNames && checkGrammarNameInLetOrConstDeclarations(node.name); } - - function checkESModuleMarker(name: Identifier | BindingPattern): boolean { - if (name.kind === SyntaxKind.Identifier) { - if (idText(name) === "__esModule") { - return grammarErrorOnNode(name, Diagnostics.Identifier_expected_esModule_is_reserved_as_an_exported_marker_when_transforming_ECMAScript_modules); - } + if (accessor.body && hasModifier(accessor, ModifierFlags.Abstract)) { + return grammarErrorOnNode(accessor, Diagnostics.An_abstract_accessor_cannot_have_an_implementation); + } + if (accessor.typeParameters) { + return grammarErrorOnNode(accessor.name, Diagnostics.An_accessor_cannot_have_type_parameters); + } + if (!doesAccessorHaveCorrectParameterCount(accessor)) { + return grammarErrorOnNode(accessor.name, accessor.kind === SyntaxKind.GetAccessor ? + Diagnostics.A_get_accessor_cannot_have_parameters : + Diagnostics.A_set_accessor_must_have_exactly_one_parameter); + } + if (accessor.kind === SyntaxKind.SetAccessor) { + if (accessor.type) { + return grammarErrorOnNode(accessor.name, Diagnostics.A_set_accessor_cannot_have_a_return_type_annotation); } - else { - const elements = name.elements; - for (const element of elements) { - if (!isOmittedExpression(element)) { - return checkESModuleMarker(element.name); - } - } + const parameter = Debug.checkDefined(getSetAccessorValueParameter(accessor), "Return value does not match parameter count assertion."); + if (parameter.dotDotDotToken) { + return grammarErrorOnNode(parameter.dotDotDotToken, Diagnostics.A_set_accessor_cannot_have_rest_parameter); } - return false; - } - - function checkGrammarNameInLetOrConstDeclarations(name: Identifier | BindingPattern): boolean { - if (name.kind === SyntaxKind.Identifier) { - if (name.originalKeywordKind === SyntaxKind.LetKeyword) { - return grammarErrorOnNode(name, Diagnostics.let_is_not_allowed_to_be_used_as_a_name_in_let_or_const_declarations); - } + if (parameter.questionToken) { + return grammarErrorOnNode(parameter.questionToken, Diagnostics.A_set_accessor_cannot_have_an_optional_parameter); } - else { - const elements = name.elements; - for (const element of elements) { - if (!isOmittedExpression(element)) { - checkGrammarNameInLetOrConstDeclarations(element.name); - } - } + if (parameter.initializer) { + return grammarErrorOnNode(accessor.name, Diagnostics.A_set_accessor_parameter_cannot_have_an_initializer); } - return false; } - - function checkGrammarVariableDeclarationList(declarationList: VariableDeclarationList): boolean { - const declarations = declarationList.declarations; - if (checkGrammarForDisallowedTrailingComma(declarationList.declarations)) { - return true; + return false; + } + /** Does the accessor have the right number of parameters? + * A get accessor has no parameters or a single `this` parameter. + * A set accessor has one parameter or a `this` parameter and one more parameter. + */ + function doesAccessorHaveCorrectParameterCount(accessor: AccessorDeclaration) { + return getAccessorThisParameter(accessor) || accessor.parameters.length === (accessor.kind === SyntaxKind.GetAccessor ? 0 : 1); + } + function getAccessorThisParameter(accessor: AccessorDeclaration): ParameterDeclaration | undefined { + if (accessor.parameters.length === (accessor.kind === SyntaxKind.GetAccessor ? 1 : 2)) { + return getThisParameter(accessor); + } + } + function checkGrammarTypeOperatorNode(node: TypeOperatorNode) { + if (node.operator === SyntaxKind.UniqueKeyword) { + if (node.type.kind !== SyntaxKind.SymbolKeyword) { + return grammarErrorOnNode(node.type, Diagnostics._0_expected, tokenToString(SyntaxKind.SymbolKeyword)); } - - if (!declarationList.declarations.length) { - return grammarErrorAtPos(declarationList, declarations.pos, declarations.end - declarations.pos, Diagnostics.Variable_declaration_list_cannot_be_empty); + const parent = walkUpParenthesizedTypes(node.parent); + switch (parent.kind) { + case SyntaxKind.VariableDeclaration: + const decl = (parent as VariableDeclaration); + if (decl.name.kind !== SyntaxKind.Identifier) { + return grammarErrorOnNode(node, Diagnostics.unique_symbol_types_may_not_be_used_on_a_variable_declaration_with_a_binding_name); + } + if (!isVariableDeclarationInVariableStatement(decl)) { + return grammarErrorOnNode(node, Diagnostics.unique_symbol_types_are_only_allowed_on_variables_in_a_variable_statement); + } + if (!(decl.parent.flags & NodeFlags.Const)) { + return grammarErrorOnNode((parent).name, Diagnostics.A_variable_whose_type_is_a_unique_symbol_type_must_be_const); + } + break; + case SyntaxKind.PropertyDeclaration: + if (!hasModifier(parent, ModifierFlags.Static) || + !hasModifier(parent, ModifierFlags.Readonly)) { + return grammarErrorOnNode((parent).name, Diagnostics.A_property_of_a_class_whose_type_is_a_unique_symbol_type_must_be_both_static_and_readonly); + } + break; + case SyntaxKind.PropertySignature: + if (!hasModifier(parent, ModifierFlags.Readonly)) { + return grammarErrorOnNode((parent).name, Diagnostics.A_property_of_an_interface_or_type_literal_whose_type_is_a_unique_symbol_type_must_be_readonly); + } + break; + default: + return grammarErrorOnNode(node, Diagnostics.unique_symbol_types_are_not_allowed_here); } - return false; } - - function allowLetAndConstDeclarations(parent: Node): boolean { - switch (parent.kind) { - case SyntaxKind.IfStatement: - case SyntaxKind.DoStatement: - case SyntaxKind.WhileStatement: - case SyntaxKind.WithStatement: - case SyntaxKind.ForStatement: - case SyntaxKind.ForInStatement: - case SyntaxKind.ForOfStatement: - return false; - case SyntaxKind.LabeledStatement: - return allowLetAndConstDeclarations(parent.parent); + else if (node.operator === SyntaxKind.ReadonlyKeyword) { + if (node.type.kind !== SyntaxKind.ArrayType && node.type.kind !== SyntaxKind.TupleType) { + return grammarErrorOnFirstToken(node, Diagnostics.readonly_type_modifier_is_only_permitted_on_array_and_tuple_literal_types, tokenToString(SyntaxKind.SymbolKeyword)); } - + } + } + function checkGrammarForInvalidDynamicName(node: DeclarationName, message: DiagnosticMessage) { + if (isNonBindableDynamicName(node)) { + return grammarErrorOnNode(node, message); + } + } + function checkGrammarMethod(node: MethodDeclaration | MethodSignature) { + if (checkGrammarFunctionLikeDeclaration(node)) { return true; } - - function checkGrammarForDisallowedLetOrConstStatement(node: VariableStatement) { - if (!allowLetAndConstDeclarations(node.parent)) { - if (isLet(node.declarationList)) { - return grammarErrorOnNode(node, Diagnostics.let_declarations_can_only_be_declared_inside_a_block); + if (node.kind === SyntaxKind.MethodDeclaration) { + if (node.parent.kind === SyntaxKind.ObjectLiteralExpression) { + // We only disallow modifier on a method declaration if it is a property of object-literal-expression + if (node.modifiers && !(node.modifiers.length === 1 && first(node.modifiers).kind === SyntaxKind.AsyncKeyword)) { + return grammarErrorOnFirstToken(node, Diagnostics.Modifiers_cannot_appear_here); + } + else if (checkGrammarForInvalidQuestionMark(node.questionToken, Diagnostics.An_object_member_cannot_be_declared_optional)) { + return true; + } + else if (checkGrammarForInvalidExclamationToken(node.exclamationToken, Diagnostics.A_definite_assignment_assertion_is_not_permitted_in_this_context)) { + return true; } - else if (isVarConst(node.declarationList)) { - return grammarErrorOnNode(node, Diagnostics.const_declarations_can_only_be_declared_inside_a_block); + else if (node.body === undefined) { + return grammarErrorAtPos(node, node.end - 1, ";".length, Diagnostics._0_expected, "{"); } } - } - - function checkGrammarMetaProperty(node: MetaProperty) { - const escapedText = node.name.escapedText; - switch (node.keywordToken) { - case SyntaxKind.NewKeyword: - if (escapedText !== "target") { - return grammarErrorOnNode(node.name, Diagnostics._0_is_not_a_valid_meta_property_for_keyword_1_Did_you_mean_2, node.name.escapedText, tokenToString(node.keywordToken), "target"); + if (checkGrammarForGenerator(node)) { + return true; + } + } + if (isClassLike(node.parent)) { + // Technically, computed properties in ambient contexts is disallowed + // for property declarations and accessors too, not just methods. + // However, property declarations disallow computed names in general, + // and accessors are not allowed in ambient contexts in general, + // so this error only really matters for methods. + if (node.flags & NodeFlags.Ambient) { + return checkGrammarForInvalidDynamicName(node.name, Diagnostics.A_computed_property_name_in_an_ambient_context_must_refer_to_an_expression_whose_type_is_a_literal_type_or_a_unique_symbol_type); + } + else if (node.kind === SyntaxKind.MethodDeclaration && !node.body) { + return checkGrammarForInvalidDynamicName(node.name, Diagnostics.A_computed_property_name_in_a_method_overload_must_refer_to_an_expression_whose_type_is_a_literal_type_or_a_unique_symbol_type); + } + } + else if (node.parent.kind === SyntaxKind.InterfaceDeclaration) { + return checkGrammarForInvalidDynamicName(node.name, Diagnostics.A_computed_property_name_in_an_interface_must_refer_to_an_expression_whose_type_is_a_literal_type_or_a_unique_symbol_type); + } + else if (node.parent.kind === SyntaxKind.TypeLiteral) { + return checkGrammarForInvalidDynamicName(node.name, Diagnostics.A_computed_property_name_in_a_type_literal_must_refer_to_an_expression_whose_type_is_a_literal_type_or_a_unique_symbol_type); + } + } + function checkGrammarBreakOrContinueStatement(node: BreakOrContinueStatement): boolean { + let current: Node = node; + while (current) { + if (isFunctionLike(current)) { + return grammarErrorOnNode(node, Diagnostics.Jump_target_cannot_cross_function_boundary); + } + switch (current.kind) { + case SyntaxKind.LabeledStatement: + if (node.label && (current).label.escapedText === node.label.escapedText) { + // found matching label - verify that label usage is correct + // continue can only target labels that are on iteration statements + const isMisplacedContinueLabel = node.kind === SyntaxKind.ContinueStatement + && !isIterationStatement((current).statement, /*lookInLabeledStatement*/ true); + if (isMisplacedContinueLabel) { + return grammarErrorOnNode(node, Diagnostics.A_continue_statement_can_only_jump_to_a_label_of_an_enclosing_iteration_statement); + } + return false; + } + break; + case SyntaxKind.SwitchStatement: + if (node.kind === SyntaxKind.BreakStatement && !node.label) { + // unlabeled break within switch statement - ok + return false; } break; - case SyntaxKind.ImportKeyword: - if (escapedText !== "meta") { - return grammarErrorOnNode(node.name, Diagnostics._0_is_not_a_valid_meta_property_for_keyword_1_Did_you_mean_2, node.name.escapedText, tokenToString(node.keywordToken), "meta"); + default: + if (isIterationStatement(current, /*lookInLabeledStatement*/ false) && !node.label) { + // unlabeled break or continue within iteration statement - ok + return false; } break; } + current = current.parent; } - - function hasParseDiagnostics(sourceFile: SourceFile): boolean { - return sourceFile.parseDiagnostics.length > 0; + if (node.label) { + const message = node.kind === SyntaxKind.BreakStatement + ? Diagnostics.A_break_statement_can_only_jump_to_a_label_of_an_enclosing_statement + : Diagnostics.A_continue_statement_can_only_jump_to_a_label_of_an_enclosing_iteration_statement; + return grammarErrorOnNode(node, message); } - - function grammarErrorOnFirstToken(node: Node, message: DiagnosticMessage, arg0?: any, arg1?: any, arg2?: any): boolean { - const sourceFile = getSourceFileOfNode(node); - if (!hasParseDiagnostics(sourceFile)) { - const span = getSpanOfTokenAtPosition(sourceFile, node.pos); - diagnostics.add(createFileDiagnostic(sourceFile, span.start, span.length, message, arg0, arg1, arg2)); - return true; - } - return false; + else { + const message = node.kind === SyntaxKind.BreakStatement + ? Diagnostics.A_break_statement_can_only_be_used_within_an_enclosing_iteration_or_switch_statement + : Diagnostics.A_continue_statement_can_only_be_used_within_an_enclosing_iteration_statement; + return grammarErrorOnNode(node, message); } - - function grammarErrorAtPos(nodeForSourceFile: Node, start: number, length: number, message: DiagnosticMessage, arg0?: any, arg1?: any, arg2?: any): boolean { - const sourceFile = getSourceFileOfNode(nodeForSourceFile); - if (!hasParseDiagnostics(sourceFile)) { - diagnostics.add(createFileDiagnostic(sourceFile, start, length, message, arg0, arg1, arg2)); - return true; + } + function checkGrammarBindingElement(node: BindingElement) { + if (node.dotDotDotToken) { + const elements = node.parent.elements; + if (node !== last(elements)) { + return grammarErrorOnNode(node, Diagnostics.A_rest_element_must_be_last_in_a_destructuring_pattern); } - return false; - } - - function grammarErrorOnNode(node: Node, message: DiagnosticMessage, arg0?: any, arg1?: any, arg2?: any): boolean { - const sourceFile = getSourceFileOfNode(node); - if (!hasParseDiagnostics(sourceFile)) { - diagnostics.add(createDiagnosticForNode(node, message, arg0, arg1, arg2)); - return true; + checkGrammarForDisallowedTrailingComma(elements, Diagnostics.A_rest_parameter_or_binding_pattern_may_not_have_a_trailing_comma); + if (node.propertyName) { + return grammarErrorOnNode(node.name, Diagnostics.A_rest_element_cannot_have_a_property_name); } - return false; - } - - function checkGrammarConstructorTypeParameters(node: ConstructorDeclaration) { - const jsdocTypeParameters = isInJSFile(node) ? getJSDocTypeParameterDeclarations(node) : undefined; - const range = node.typeParameters || jsdocTypeParameters && firstOrUndefined(jsdocTypeParameters); - if (range) { - const pos = range.pos === range.end ? range.pos : skipTrivia(getSourceFileOfNode(node).text, range.pos); - return grammarErrorAtPos(node, pos, range.end - pos, Diagnostics.Type_parameters_cannot_appear_on_a_constructor_declaration); + if (node.initializer) { + // Error on equals token which immediately precedes the initializer + return grammarErrorAtPos(node, node.initializer.pos - 1, 1, Diagnostics.A_rest_element_cannot_have_an_initializer); } } - - function checkGrammarConstructorTypeAnnotation(node: ConstructorDeclaration) { - const type = getEffectiveReturnTypeNode(node); - if (type) { - return grammarErrorOnNode(type, Diagnostics.Type_annotation_cannot_appear_on_a_constructor_declaration); - } + } + function isStringOrNumberLiteralExpression(expr: Expression) { + return isStringOrNumericLiteralLike(expr) || + expr.kind === SyntaxKind.PrefixUnaryExpression && (expr).operator === SyntaxKind.MinusToken && + (expr).operand.kind === SyntaxKind.NumericLiteral; + } + function isBigIntLiteralExpression(expr: Expression) { + return expr.kind === SyntaxKind.BigIntLiteral || + expr.kind === SyntaxKind.PrefixUnaryExpression && (expr).operator === SyntaxKind.MinusToken && + (expr).operand.kind === SyntaxKind.BigIntLiteral; + } + function isSimpleLiteralEnumReference(expr: Expression) { + if ((isPropertyAccessExpression(expr) || (isElementAccessExpression(expr) && isStringOrNumberLiteralExpression(expr.argumentExpression))) && + isEntityNameExpression(expr.expression)) { + return !!(checkExpressionCached(expr).flags & TypeFlags.EnumLiteral); } - - function checkGrammarProperty(node: PropertyDeclaration | PropertySignature) { - if (isClassLike(node.parent)) { - if (isStringLiteral(node.name) && node.name.text === "constructor") { - return grammarErrorOnNode(node.name, Diagnostics.Classes_may_not_have_a_field_named_constructor); - } - if (checkGrammarForInvalidDynamicName(node.name, Diagnostics.A_computed_property_name_in_a_class_property_declaration_must_refer_to_an_expression_whose_type_is_a_literal_type_or_a_unique_symbol_type)) { - return true; - } - if (languageVersion < ScriptTarget.ES2015 && isPrivateIdentifier(node.name)) { - return grammarErrorOnNode(node.name, Diagnostics.Private_identifiers_are_only_available_when_targeting_ECMAScript_2015_and_higher); + } + function checkAmbientInitializer(node: VariableDeclaration | PropertyDeclaration | PropertySignature) { + const { initializer } = node; + if (initializer) { + const isInvalidInitializer = !(isStringOrNumberLiteralExpression(initializer) || + isSimpleLiteralEnumReference(initializer) || + initializer.kind === SyntaxKind.TrueKeyword || initializer.kind === SyntaxKind.FalseKeyword || + isBigIntLiteralExpression(initializer)); + const isConstOrReadonly = isDeclarationReadonly(node) || isVariableDeclaration(node) && isVarConst(node); + if (isConstOrReadonly && !node.type) { + if (isInvalidInitializer) { + return grammarErrorOnNode(initializer, Diagnostics.A_const_initializer_in_an_ambient_context_must_be_a_string_or_numeric_literal_or_literal_enum_reference); } } - else if (node.parent.kind === SyntaxKind.InterfaceDeclaration) { - if (checkGrammarForInvalidDynamicName(node.name, Diagnostics.A_computed_property_name_in_an_interface_must_refer_to_an_expression_whose_type_is_a_literal_type_or_a_unique_symbol_type)) { - return true; - } - if (node.initializer) { - return grammarErrorOnNode(node.initializer, Diagnostics.An_interface_property_cannot_have_an_initializer); - } + else { + return grammarErrorOnNode(initializer, Diagnostics.Initializers_are_not_allowed_in_ambient_contexts); } - else if (node.parent.kind === SyntaxKind.TypeLiteral) { - if (checkGrammarForInvalidDynamicName(node.name, Diagnostics.A_computed_property_name_in_a_type_literal_must_refer_to_an_expression_whose_type_is_a_literal_type_or_a_unique_symbol_type)) { - return true; - } - if (node.initializer) { - return grammarErrorOnNode(node.initializer, Diagnostics.A_type_literal_property_cannot_have_an_initializer); - } + if (!isConstOrReadonly || isInvalidInitializer) { + return grammarErrorOnNode(initializer, Diagnostics.Initializers_are_not_allowed_in_ambient_contexts); } - + } + } + function checkGrammarVariableDeclaration(node: VariableDeclaration) { + if (node.parent.parent.kind !== SyntaxKind.ForInStatement && node.parent.parent.kind !== SyntaxKind.ForOfStatement) { if (node.flags & NodeFlags.Ambient) { checkAmbientInitializer(node); } - - if (isPropertyDeclaration(node) && node.exclamationToken && (!isClassLike(node.parent) || !node.type || node.initializer || - node.flags & NodeFlags.Ambient || hasModifier(node, ModifierFlags.Static | ModifierFlags.Abstract))) { - return grammarErrorOnNode(node.exclamationToken, Diagnostics.A_definite_assignment_assertion_is_not_permitted_in_this_context); + else if (!node.initializer) { + if (isBindingPattern(node.name) && !isBindingPattern(node.parent)) { + return grammarErrorOnNode(node, Diagnostics.A_destructuring_declaration_must_have_an_initializer); + } + if (isVarConst(node)) { + return grammarErrorOnNode(node, Diagnostics.const_declarations_must_be_initialized); + } } } - - function checkGrammarTopLevelElementForRequiredDeclareModifier(node: Node): boolean { - // A declare modifier is required for any top level .d.ts declaration except export=, export default, export as namespace - // interfaces and imports categories: - // - // DeclarationElement: - // ExportAssignment - // export_opt InterfaceDeclaration - // export_opt TypeAliasDeclaration - // export_opt ImportDeclaration - // export_opt ExternalImportDeclaration - // export_opt AmbientDeclaration - // - // TODO: The spec needs to be amended to reflect this grammar. - if (node.kind === SyntaxKind.InterfaceDeclaration || - node.kind === SyntaxKind.TypeAliasDeclaration || - node.kind === SyntaxKind.ImportDeclaration || - node.kind === SyntaxKind.ImportEqualsDeclaration || - node.kind === SyntaxKind.ExportDeclaration || - node.kind === SyntaxKind.ExportAssignment || - node.kind === SyntaxKind.NamespaceExportDeclaration || - hasModifier(node, ModifierFlags.Ambient | ModifierFlags.Export | ModifierFlags.Default)) { - return false; + if (node.exclamationToken && (node.parent.parent.kind !== SyntaxKind.VariableStatement || !node.type || node.initializer || node.flags & NodeFlags.Ambient)) { + return grammarErrorOnNode(node.exclamationToken, Diagnostics.Definite_assignment_assertions_can_only_be_used_along_with_a_type_annotation); + } + const moduleKind = getEmitModuleKind(compilerOptions); + if (moduleKind < ModuleKind.ES2015 && moduleKind !== ModuleKind.System && !compilerOptions.noEmit && + !(node.parent.parent.flags & NodeFlags.Ambient) && hasModifier(node.parent.parent, ModifierFlags.Export)) { + checkESModuleMarker(node.name); + } + const checkLetConstNames = (isLet(node) || isVarConst(node)); + // 1. LexicalDeclaration : LetOrConst BindingList ; + // It is a Syntax Error if the BoundNames of BindingList contains "let". + // 2. ForDeclaration: ForDeclaration : LetOrConst ForBinding + // It is a Syntax Error if the BoundNames of ForDeclaration contains "let". + // It is a SyntaxError if a VariableDeclaration or VariableDeclarationNoIn occurs within strict code + // and its Identifier is eval or arguments + return checkLetConstNames && checkGrammarNameInLetOrConstDeclarations(node.name); + } + function checkESModuleMarker(name: Identifier | BindingPattern): boolean { + if (name.kind === SyntaxKind.Identifier) { + if (idText(name) === "__esModule") { + return grammarErrorOnNode(name, Diagnostics.Identifier_expected_esModule_is_reserved_as_an_exported_marker_when_transforming_ECMAScript_modules); } - - return grammarErrorOnFirstToken(node, Diagnostics.Top_level_declarations_in_d_ts_files_must_start_with_either_a_declare_or_export_modifier); } - - function checkGrammarTopLevelElementsForRequiredDeclareModifier(file: SourceFile): boolean { - for (const decl of file.statements) { - if (isDeclaration(decl) || decl.kind === SyntaxKind.VariableStatement) { - if (checkGrammarTopLevelElementForRequiredDeclareModifier(decl)) { - return true; - } + else { + const elements = name.elements; + for (const element of elements) { + if (!isOmittedExpression(element)) { + return checkESModuleMarker(element.name); } } - return false; - } - - function checkGrammarSourceFile(node: SourceFile): boolean { - return !!(node.flags & NodeFlags.Ambient) && checkGrammarTopLevelElementsForRequiredDeclareModifier(node); } - - function checkGrammarStatementInAmbientContext(node: Node): boolean { - if (node.flags & NodeFlags.Ambient) { - // Find containing block which is either Block, ModuleBlock, SourceFile - const links = getNodeLinks(node); - if (!links.hasReportedStatementInAmbientContext && (isFunctionLike(node.parent) || isAccessor(node.parent))) { - return getNodeLinks(node).hasReportedStatementInAmbientContext = grammarErrorOnFirstToken(node, Diagnostics.An_implementation_cannot_be_declared_in_ambient_contexts); - } - - // We are either parented by another statement, or some sort of block. - // If we're in a block, we only want to really report an error once - // to prevent noisiness. So use a bit on the block to indicate if - // this has already been reported, and don't report if it has. - // - if (node.parent.kind === SyntaxKind.Block || node.parent.kind === SyntaxKind.ModuleBlock || node.parent.kind === SyntaxKind.SourceFile) { - const links = getNodeLinks(node.parent); - // Check if the containing block ever report this error - if (!links.hasReportedStatementInAmbientContext) { - return links.hasReportedStatementInAmbientContext = grammarErrorOnFirstToken(node, Diagnostics.Statements_are_not_allowed_in_ambient_contexts); - } - } - else { - // We must be parented by a statement. If so, there's no need - // to report the error as our parent will have already done it. - // Debug.assert(isStatement(node.parent)); - } + return false; + } + function checkGrammarNameInLetOrConstDeclarations(name: Identifier | BindingPattern): boolean { + if (name.kind === SyntaxKind.Identifier) { + if (name.originalKeywordKind === SyntaxKind.LetKeyword) { + return grammarErrorOnNode(name, Diagnostics.let_is_not_allowed_to_be_used_as_a_name_in_let_or_const_declarations); } - return false; } - - function checkGrammarNumericLiteral(node: NumericLiteral): boolean { - // Grammar checking - if (node.numericLiteralFlags & TokenFlags.Octal) { - let diagnosticMessage: DiagnosticMessage | undefined; - if (languageVersion >= ScriptTarget.ES5) { - diagnosticMessage = Diagnostics.Octal_literals_are_not_available_when_targeting_ECMAScript_5_and_higher_Use_the_syntax_0; - } - else if (isChildOfNodeWithKind(node, SyntaxKind.LiteralType)) { - diagnosticMessage = Diagnostics.Octal_literal_types_must_use_ES2015_syntax_Use_the_syntax_0; - } - else if (isChildOfNodeWithKind(node, SyntaxKind.EnumMember)) { - diagnosticMessage = Diagnostics.Octal_literals_are_not_allowed_in_enums_members_initializer_Use_the_syntax_0; - } - if (diagnosticMessage) { - const withMinus = isPrefixUnaryExpression(node.parent) && node.parent.operator === SyntaxKind.MinusToken; - const literal = (withMinus ? "-" : "") + "0o" + node.text; - return grammarErrorOnNode(withMinus ? node.parent : node, diagnosticMessage, literal); + else { + const elements = name.elements; + for (const element of elements) { + if (!isOmittedExpression(element)) { + checkGrammarNameInLetOrConstDeclarations(element.name); } } - - // Realism (size) checking - checkNumericLiteralValueSize(node); - - return false; } - - function checkNumericLiteralValueSize(node: NumericLiteral) { - // Scientific notation (e.g. 2e54 and 1e00000000010) can't be converted to bigint - // Literals with 15 or fewer characters aren't long enough to reach past 2^53 - 1 - // Fractional numbers (e.g. 9000000000000000.001) are inherently imprecise anyway - if (node.numericLiteralFlags & TokenFlags.Scientific || node.text.length <= 15 || node.text.indexOf(".") !== -1) { - return; + return false; + } + function checkGrammarVariableDeclarationList(declarationList: VariableDeclarationList): boolean { + const declarations = declarationList.declarations; + if (checkGrammarForDisallowedTrailingComma(declarationList.declarations)) { + return true; + } + if (!declarationList.declarations.length) { + return grammarErrorAtPos(declarationList, declarations.pos, declarations.end - declarations.pos, Diagnostics.Variable_declaration_list_cannot_be_empty); + } + return false; + } + function allowLetAndConstDeclarations(parent: Node): boolean { + switch (parent.kind) { + case SyntaxKind.IfStatement: + case SyntaxKind.DoStatement: + case SyntaxKind.WhileStatement: + case SyntaxKind.WithStatement: + case SyntaxKind.ForStatement: + case SyntaxKind.ForInStatement: + case SyntaxKind.ForOfStatement: + return false; + case SyntaxKind.LabeledStatement: + return allowLetAndConstDeclarations(parent.parent); + } + return true; + } + function checkGrammarForDisallowedLetOrConstStatement(node: VariableStatement) { + if (!allowLetAndConstDeclarations(node.parent)) { + if (isLet(node.declarationList)) { + return grammarErrorOnNode(node, Diagnostics.let_declarations_can_only_be_declared_inside_a_block); } - - // We can't rely on the runtime to accurately store and compare extremely large numeric values - // Even for internal use, we use getTextOfNode: https://github.com/microsoft/TypeScript/issues/33298 - // Thus, if the runtime claims a too-large number is lower than Number.MAX_SAFE_INTEGER, - // it's likely addition operations on it will fail too - const apparentValue = +getTextOfNode(node); - if (apparentValue <= 2 ** 53 - 1 && apparentValue + 1 > apparentValue) { - return; + else if (isVarConst(node.declarationList)) { + return grammarErrorOnNode(node, Diagnostics.const_declarations_can_only_be_declared_inside_a_block); } - - addErrorOrSuggestion(/*isError*/ false, createDiagnosticForNode(node, Diagnostics.Numeric_literals_with_absolute_values_equal_to_2_53_or_greater_are_too_large_to_be_represented_accurately_as_integers)); } - - function checkGrammarBigIntLiteral(node: BigIntLiteral): boolean { - const literalType = isLiteralTypeNode(node.parent) || - isPrefixUnaryExpression(node.parent) && isLiteralTypeNode(node.parent.parent); - if (!literalType) { - if (languageVersion < ScriptTarget.ES2020) { - if (grammarErrorOnNode(node, Diagnostics.BigInt_literals_are_not_available_when_targeting_lower_than_ES2020)) { - return true; - } + } + function checkGrammarMetaProperty(node: MetaProperty) { + const escapedText = node.name.escapedText; + switch (node.keywordToken) { + case SyntaxKind.NewKeyword: + if (escapedText !== "target") { + return grammarErrorOnNode(node.name, Diagnostics._0_is_not_a_valid_meta_property_for_keyword_1_Did_you_mean_2, node.name.escapedText, tokenToString(node.keywordToken), "target"); } - } - return false; + break; + case SyntaxKind.ImportKeyword: + if (escapedText !== "meta") { + return grammarErrorOnNode(node.name, Diagnostics._0_is_not_a_valid_meta_property_for_keyword_1_Did_you_mean_2, node.name.escapedText, tokenToString(node.keywordToken), "meta"); + } + break; } - - function grammarErrorAfterFirstToken(node: Node, message: DiagnosticMessage, arg0?: any, arg1?: any, arg2?: any): boolean { - const sourceFile = getSourceFileOfNode(node); - if (!hasParseDiagnostics(sourceFile)) { - const span = getSpanOfTokenAtPosition(sourceFile, node.pos); - diagnostics.add(createFileDiagnostic(sourceFile, textSpanEnd(span), /*length*/ 0, message, arg0, arg1, arg2)); + } + function hasParseDiagnostics(sourceFile: SourceFile): boolean { + return sourceFile.parseDiagnostics.length > 0; + } + function grammarErrorOnFirstToken(node: Node, message: DiagnosticMessage, arg0?: any, arg1?: any, arg2?: any): boolean { + const sourceFile = getSourceFileOfNode(node); + if (!hasParseDiagnostics(sourceFile)) { + const span = getSpanOfTokenAtPosition(sourceFile, node.pos); + diagnostics.add(createFileDiagnostic(sourceFile, span.start, span.length, message, arg0, arg1, arg2)); + return true; + } + return false; + } + function grammarErrorAtPos(nodeForSourceFile: Node, start: number, length: number, message: DiagnosticMessage, arg0?: any, arg1?: any, arg2?: any): boolean { + const sourceFile = getSourceFileOfNode(nodeForSourceFile); + if (!hasParseDiagnostics(sourceFile)) { + diagnostics.add(createFileDiagnostic(sourceFile, start, length, message, arg0, arg1, arg2)); + return true; + } + return false; + } + function grammarErrorOnNode(node: Node, message: DiagnosticMessage, arg0?: any, arg1?: any, arg2?: any): boolean { + const sourceFile = getSourceFileOfNode(node); + if (!hasParseDiagnostics(sourceFile)) { + diagnostics.add(createDiagnosticForNode(node, message, arg0, arg1, arg2)); + return true; + } + return false; + } + function checkGrammarConstructorTypeParameters(node: ConstructorDeclaration) { + const jsdocTypeParameters = isInJSFile(node) ? getJSDocTypeParameterDeclarations(node) : undefined; + const range = node.typeParameters || jsdocTypeParameters && firstOrUndefined(jsdocTypeParameters); + if (range) { + const pos = range.pos === range.end ? range.pos : skipTrivia(getSourceFileOfNode(node).text, range.pos); + return grammarErrorAtPos(node, pos, range.end - pos, Diagnostics.Type_parameters_cannot_appear_on_a_constructor_declaration); + } + } + function checkGrammarConstructorTypeAnnotation(node: ConstructorDeclaration) { + const type = getEffectiveReturnTypeNode(node); + if (type) { + return grammarErrorOnNode(type, Diagnostics.Type_annotation_cannot_appear_on_a_constructor_declaration); + } + } + function checkGrammarProperty(node: PropertyDeclaration | PropertySignature) { + if (isClassLike(node.parent)) { + if (isStringLiteral(node.name) && node.name.text === "constructor") { + return grammarErrorOnNode(node.name, Diagnostics.Classes_may_not_have_a_field_named_constructor); + } + if (checkGrammarForInvalidDynamicName(node.name, Diagnostics.A_computed_property_name_in_a_class_property_declaration_must_refer_to_an_expression_whose_type_is_a_literal_type_or_a_unique_symbol_type)) { return true; } - return false; + if (languageVersion < ScriptTarget.ES2015 && isPrivateIdentifier(node.name)) { + return grammarErrorOnNode(node.name, Diagnostics.Private_identifiers_are_only_available_when_targeting_ECMAScript_2015_and_higher); + } } - - function getAmbientModules(): Symbol[] { - if (!ambientModulesCache) { - ambientModulesCache = []; - globals.forEach((global, sym) => { - // No need to `unescapeLeadingUnderscores`, an escaped symbol is never an ambient module. - if (ambientModuleSymbolRegex.test(sym as string)) { - ambientModulesCache!.push(global); - } - }); + else if (node.parent.kind === SyntaxKind.InterfaceDeclaration) { + if (checkGrammarForInvalidDynamicName(node.name, Diagnostics.A_computed_property_name_in_an_interface_must_refer_to_an_expression_whose_type_is_a_literal_type_or_a_unique_symbol_type)) { + return true; + } + if (node.initializer) { + return grammarErrorOnNode(node.initializer, Diagnostics.An_interface_property_cannot_have_an_initializer); } - return ambientModulesCache; } - - function checkGrammarImportClause(node: ImportClause): boolean { - if (node.isTypeOnly && node.name && node.namedBindings) { - return grammarErrorOnNode(node, Diagnostics.A_type_only_import_can_specify_a_default_import_or_named_bindings_but_not_both); + else if (node.parent.kind === SyntaxKind.TypeLiteral) { + if (checkGrammarForInvalidDynamicName(node.name, Diagnostics.A_computed_property_name_in_a_type_literal_must_refer_to_an_expression_whose_type_is_a_literal_type_or_a_unique_symbol_type)) { + return true; + } + if (node.initializer) { + return grammarErrorOnNode(node.initializer, Diagnostics.A_type_literal_property_cannot_have_an_initializer); } + } + if (node.flags & NodeFlags.Ambient) { + checkAmbientInitializer(node); + } + if (isPropertyDeclaration(node) && node.exclamationToken && (!isClassLike(node.parent) || !node.type || node.initializer || + node.flags & NodeFlags.Ambient || hasModifier(node, ModifierFlags.Static | ModifierFlags.Abstract))) { + return grammarErrorOnNode(node.exclamationToken, Diagnostics.A_definite_assignment_assertion_is_not_permitted_in_this_context); + } + } + function checkGrammarTopLevelElementForRequiredDeclareModifier(node: Node): boolean { + // A declare modifier is required for any top level .d.ts declaration except export=, export default, export as namespace + // interfaces and imports categories: + // + // DeclarationElement: + // ExportAssignment + // export_opt InterfaceDeclaration + // export_opt TypeAliasDeclaration + // export_opt ImportDeclaration + // export_opt ExternalImportDeclaration + // export_opt AmbientDeclaration + // + // TODO: The spec needs to be amended to reflect this grammar. + if (node.kind === SyntaxKind.InterfaceDeclaration || + node.kind === SyntaxKind.TypeAliasDeclaration || + node.kind === SyntaxKind.ImportDeclaration || + node.kind === SyntaxKind.ImportEqualsDeclaration || + node.kind === SyntaxKind.ExportDeclaration || + node.kind === SyntaxKind.ExportAssignment || + node.kind === SyntaxKind.NamespaceExportDeclaration || + hasModifier(node, ModifierFlags.Ambient | ModifierFlags.Export | ModifierFlags.Default)) { return false; } - - function checkGrammarImportCallExpression(node: ImportCall): boolean { - if (moduleKind === ModuleKind.ES2015) { - return grammarErrorOnNode(node, Diagnostics.Dynamic_imports_are_only_supported_when_the_module_flag_is_set_to_es2020_esnext_commonjs_amd_system_or_umd); + return grammarErrorOnFirstToken(node, Diagnostics.Top_level_declarations_in_d_ts_files_must_start_with_either_a_declare_or_export_modifier); + } + function checkGrammarTopLevelElementsForRequiredDeclareModifier(file: SourceFile): boolean { + for (const decl of file.statements) { + if (isDeclaration(decl) || decl.kind === SyntaxKind.VariableStatement) { + if (checkGrammarTopLevelElementForRequiredDeclareModifier(decl)) { + return true; + } } - - if (node.typeArguments) { - return grammarErrorOnNode(node, Diagnostics.Dynamic_import_cannot_have_type_arguments); + } + return false; + } + function checkGrammarSourceFile(node: SourceFile): boolean { + return !!(node.flags & NodeFlags.Ambient) && checkGrammarTopLevelElementsForRequiredDeclareModifier(node); + } + function checkGrammarStatementInAmbientContext(node: Node): boolean { + if (node.flags & NodeFlags.Ambient) { + // Find containing block which is either Block, ModuleBlock, SourceFile + const links = getNodeLinks(node); + if (!links.hasReportedStatementInAmbientContext && (isFunctionLike(node.parent) || isAccessor(node.parent))) { + return getNodeLinks(node).hasReportedStatementInAmbientContext = grammarErrorOnFirstToken(node, Diagnostics.An_implementation_cannot_be_declared_in_ambient_contexts); } - - const nodeArguments = node.arguments; - if (nodeArguments.length !== 1) { - return grammarErrorOnNode(node, Diagnostics.Dynamic_import_must_have_one_specifier_as_an_argument); + // We are either parented by another statement, or some sort of block. + // If we're in a block, we only want to really report an error once + // to prevent noisiness. So use a bit on the block to indicate if + // this has already been reported, and don't report if it has. + // + if (node.parent.kind === SyntaxKind.Block || node.parent.kind === SyntaxKind.ModuleBlock || node.parent.kind === SyntaxKind.SourceFile) { + const links = getNodeLinks(node.parent); + // Check if the containing block ever report this error + if (!links.hasReportedStatementInAmbientContext) { + return links.hasReportedStatementInAmbientContext = grammarErrorOnFirstToken(node, Diagnostics.Statements_are_not_allowed_in_ambient_contexts); + } } - checkGrammarForDisallowedTrailingComma(nodeArguments); - // see: parseArgumentOrArrayLiteralElement...we use this function which parse arguments of callExpression to parse specifier for dynamic import. - // parseArgumentOrArrayLiteralElement allows spread element to be in an argument list which is not allowed as specifier in dynamic import. - if (isSpreadElement(nodeArguments[0])) { - return grammarErrorOnNode(nodeArguments[0], Diagnostics.Specifier_of_dynamic_import_cannot_be_spread_element); + else { + // We must be parented by a statement. If so, there's no need + // to report the error as our parent will have already done it. + // Debug.assert(isStatement(node.parent)); } - return false; } - - function findMatchingTypeReferenceOrTypeAliasReference(source: Type, unionTarget: UnionOrIntersectionType) { - const sourceObjectFlags = getObjectFlags(source); - if (sourceObjectFlags & (ObjectFlags.Reference | ObjectFlags.Anonymous) && unionTarget.flags & TypeFlags.Union) { - return find(unionTarget.types, target => { - if (target.flags & TypeFlags.Object) { - const overlapObjFlags = sourceObjectFlags & getObjectFlags(target); - if (overlapObjFlags & ObjectFlags.Reference) { - return (source as TypeReference).target === (target as TypeReference).target; - } - if (overlapObjFlags & ObjectFlags.Anonymous) { - return !!(source as AnonymousType).aliasSymbol && (source as AnonymousType).aliasSymbol === (target as AnonymousType).aliasSymbol; - } - } - return false; - }); + return false; + } + function checkGrammarNumericLiteral(node: NumericLiteral): boolean { + // Grammar checking + if (node.numericLiteralFlags & TokenFlags.Octal) { + let diagnosticMessage: DiagnosticMessage | undefined; + if (languageVersion >= ScriptTarget.ES5) { + diagnosticMessage = Diagnostics.Octal_literals_are_not_available_when_targeting_ECMAScript_5_and_higher_Use_the_syntax_0; } - } - - function findBestTypeForObjectLiteral(source: Type, unionTarget: UnionOrIntersectionType) { - if (getObjectFlags(source) & ObjectFlags.ObjectLiteral && forEachType(unionTarget, isArrayLikeType)) { - return find(unionTarget.types, t => !isArrayLikeType(t)); + else if (isChildOfNodeWithKind(node, SyntaxKind.LiteralType)) { + diagnosticMessage = Diagnostics.Octal_literal_types_must_use_ES2015_syntax_Use_the_syntax_0; } - } - - function findBestTypeForInvokable(source: Type, unionTarget: UnionOrIntersectionType) { - let signatureKind = SignatureKind.Call; - const hasSignatures = getSignaturesOfType(source, signatureKind).length > 0 || - (signatureKind = SignatureKind.Construct, getSignaturesOfType(source, signatureKind).length > 0); - if (hasSignatures) { - return find(unionTarget.types, t => getSignaturesOfType(t, signatureKind).length > 0); + else if (isChildOfNodeWithKind(node, SyntaxKind.EnumMember)) { + diagnosticMessage = Diagnostics.Octal_literals_are_not_allowed_in_enums_members_initializer_Use_the_syntax_0; + } + if (diagnosticMessage) { + const withMinus = isPrefixUnaryExpression(node.parent) && node.parent.operator === SyntaxKind.MinusToken; + const literal = (withMinus ? "-" : "") + "0o" + node.text; + return grammarErrorOnNode(withMinus ? node.parent : node, diagnosticMessage, literal); } } - - function findMostOverlappyType(source: Type, unionTarget: UnionOrIntersectionType) { - let bestMatch: Type | undefined; - let matchingCount = 0; - for (const target of unionTarget.types) { - const overlap = getIntersectionType([getIndexType(source), getIndexType(target)]); - if (overlap.flags & TypeFlags.Index) { - // perfect overlap of keys - bestMatch = target; - matchingCount = Infinity; - } - else if (overlap.flags & TypeFlags.Union) { - // We only want to account for literal types otherwise. - // If we have a union of index types, it seems likely that we - // needed to elaborate between two generic mapped types anyway. - const len = length(filter((overlap as UnionType).types, isUnitType)); - if (len >= matchingCount) { - bestMatch = target; - matchingCount = len; - } - } - else if (isUnitType(overlap) && 1 >= matchingCount) { - bestMatch = target; - matchingCount = 1; + // Realism (size) checking + checkNumericLiteralValueSize(node); + return false; + } + function checkNumericLiteralValueSize(node: NumericLiteral) { + // Scientific notation (e.g. 2e54 and 1e00000000010) can't be converted to bigint + // Literals with 15 or fewer characters aren't long enough to reach past 2^53 - 1 + // Fractional numbers (e.g. 9000000000000000.001) are inherently imprecise anyway + if (node.numericLiteralFlags & TokenFlags.Scientific || node.text.length <= 15 || node.text.indexOf(".") !== -1) { + return; + } + // We can't rely on the runtime to accurately store and compare extremely large numeric values + // Even for internal use, we use getTextOfNode: https://github.com/microsoft/TypeScript/issues/33298 + // Thus, if the runtime claims a too-large number is lower than Number.MAX_SAFE_INTEGER, + // it's likely addition operations on it will fail too + const apparentValue = +getTextOfNode(node); + if (apparentValue <= 2 ** 53 - 1 && apparentValue + 1 > apparentValue) { + return; + } + addErrorOrSuggestion(/*isError*/ false, createDiagnosticForNode(node, Diagnostics.Numeric_literals_with_absolute_values_equal_to_2_53_or_greater_are_too_large_to_be_represented_accurately_as_integers)); + } + function checkGrammarBigIntLiteral(node: BigIntLiteral): boolean { + const literalType = isLiteralTypeNode(node.parent) || + isPrefixUnaryExpression(node.parent) && isLiteralTypeNode(node.parent.parent); + if (!literalType) { + if (languageVersion < ScriptTarget.ES2020) { + if (grammarErrorOnNode(node, Diagnostics.BigInt_literals_are_not_available_when_targeting_lower_than_ES2020)) { + return true; } } - return bestMatch; } - - function filterPrimitivesIfContainsNonPrimitive(type: UnionType) { - if (maybeTypeOfKind(type, TypeFlags.NonPrimitive)) { - const result = filterType(type, t => !(t.flags & TypeFlags.Primitive)); - if (!(result.flags & TypeFlags.Never)) { - return result; + return false; + } + function grammarErrorAfterFirstToken(node: Node, message: DiagnosticMessage, arg0?: any, arg1?: any, arg2?: any): boolean { + const sourceFile = getSourceFileOfNode(node); + if (!hasParseDiagnostics(sourceFile)) { + const span = getSpanOfTokenAtPosition(sourceFile, node.pos); + diagnostics.add(createFileDiagnostic(sourceFile, textSpanEnd(span), /*length*/ 0, message, arg0, arg1, arg2)); + return true; + } + return false; + } + function getAmbientModules(): ts.Symbol[] { + if (!ambientModulesCache) { + ambientModulesCache = []; + globals.forEach((global, sym) => { + // No need to `unescapeLeadingUnderscores`, an escaped symbol is never an ambient module. + if (ambientModuleSymbolRegex.test(sym as string)) { + ambientModulesCache!.push(global); } - } - return type; + }); } - - // Keep this up-to-date with the same logic within `getApparentTypeOfContextualType`, since they should behave similarly - function findMatchingDiscriminantType(source: Type, target: Type, isRelatedTo: (source: Type, target: Type) => Ternary) { - if (target.flags & TypeFlags.Union && source.flags & (TypeFlags.Intersection | TypeFlags.Object)) { - const sourceProperties = getPropertiesOfType(source); - if (sourceProperties) { - const sourcePropertiesFiltered = findDiscriminantProperties(sourceProperties, target); - if (sourcePropertiesFiltered) { - return discriminateTypeByDiscriminableItems(target, map(sourcePropertiesFiltered, p => ([() => getTypeOfSymbol(p), p.escapedName] as [() => Type, __String])), isRelatedTo); + return ambientModulesCache; + } + function checkGrammarImportClause(node: ImportClause): boolean { + if (node.isTypeOnly && node.name && node.namedBindings) { + return grammarErrorOnNode(node, Diagnostics.A_type_only_import_can_specify_a_default_import_or_named_bindings_but_not_both); + } + return false; + } + function checkGrammarImportCallExpression(node: ImportCall): boolean { + if (moduleKind === ModuleKind.ES2015) { + return grammarErrorOnNode(node, Diagnostics.Dynamic_imports_are_only_supported_when_the_module_flag_is_set_to_es2020_esnext_commonjs_amd_system_or_umd); + } + if (node.typeArguments) { + return grammarErrorOnNode(node, Diagnostics.Dynamic_import_cannot_have_type_arguments); + } + const nodeArguments = node.arguments; + if (nodeArguments.length !== 1) { + return grammarErrorOnNode(node, Diagnostics.Dynamic_import_must_have_one_specifier_as_an_argument); + } + checkGrammarForDisallowedTrailingComma(nodeArguments); + // see: parseArgumentOrArrayLiteralElement...we use this function which parse arguments of callExpression to parse specifier for dynamic import. + // parseArgumentOrArrayLiteralElement allows spread element to be in an argument list which is not allowed as specifier in dynamic import. + if (isSpreadElement(nodeArguments[0])) { + return grammarErrorOnNode(nodeArguments[0], Diagnostics.Specifier_of_dynamic_import_cannot_be_spread_element); + } + return false; + } + function findMatchingTypeReferenceOrTypeAliasReference(source: ts.Type, unionTarget: UnionOrIntersectionType) { + const sourceObjectFlags = getObjectFlags(source); + if (sourceObjectFlags & (ObjectFlags.Reference | ObjectFlags.Anonymous) && unionTarget.flags & TypeFlags.Union) { + return find(unionTarget.types, target => { + if (target.flags & TypeFlags.Object) { + const overlapObjFlags = sourceObjectFlags & getObjectFlags(target); + if (overlapObjFlags & ObjectFlags.Reference) { + return (source as TypeReference).target === (target as TypeReference).target; + } + if (overlapObjFlags & ObjectFlags.Anonymous) { + return !!(source as AnonymousType).aliasSymbol && (source as AnonymousType).aliasSymbol === (target as AnonymousType).aliasSymbol; } } - } - return undefined; + return false; + }); } } - - function isNotAccessor(declaration: Declaration): boolean { - // Accessors check for their own matching duplicates, and in contexts where they are valid, there are already duplicate identifier checks - return !isAccessor(declaration); - } - - function isNotOverload(declaration: Declaration): boolean { - return (declaration.kind !== SyntaxKind.FunctionDeclaration && declaration.kind !== SyntaxKind.MethodDeclaration) || - !!(declaration as FunctionDeclaration).body; + function findBestTypeForObjectLiteral(source: ts.Type, unionTarget: UnionOrIntersectionType) { + if (getObjectFlags(source) & ObjectFlags.ObjectLiteral && forEachType(unionTarget, isArrayLikeType)) { + return find(unionTarget.types, t => !isArrayLikeType(t)); + } } - - /** Like 'isDeclarationName', but returns true for LHS of `import { x as y }` or `export { x as y }`. */ - function isDeclarationNameOrImportPropertyName(name: Node): boolean { - switch (name.parent.kind) { - case SyntaxKind.ImportSpecifier: - case SyntaxKind.ExportSpecifier: - return isIdentifier(name); - default: - return isDeclarationName(name); + function findBestTypeForInvokable(source: ts.Type, unionTarget: UnionOrIntersectionType) { + let signatureKind = SignatureKind.Call; + const hasSignatures = getSignaturesOfType(source, signatureKind).length > 0 || + (signatureKind = SignatureKind.Construct, getSignaturesOfType(source, signatureKind).length > 0); + if (hasSignatures) { + return find(unionTarget.types, t => getSignaturesOfType(t, signatureKind).length > 0); } } - - function isSomeImportDeclaration(decl: Node): boolean { - switch (decl.kind) { - case SyntaxKind.ImportClause: // For default import - case SyntaxKind.ImportEqualsDeclaration: - case SyntaxKind.NamespaceImport: - case SyntaxKind.ImportSpecifier: // For rename import `x as y` - return true; - case SyntaxKind.Identifier: - // For regular import, `decl` is an Identifier under the ImportSpecifier. - return decl.parent.kind === SyntaxKind.ImportSpecifier; - default: - return false; + function findMostOverlappyType(source: ts.Type, unionTarget: UnionOrIntersectionType) { + let bestMatch: ts.Type | undefined; + let matchingCount = 0; + for (const target of unionTarget.types) { + const overlap = getIntersectionType([getIndexType(source), getIndexType(target)]); + if (overlap.flags & TypeFlags.Index) { + // perfect overlap of keys + bestMatch = target; + matchingCount = Infinity; + } + else if (overlap.flags & TypeFlags.Union) { + // We only want to account for literal types otherwise. + // If we have a union of index types, it seems likely that we + // needed to elaborate between two generic mapped types anyway. + const len = length(filter((overlap as UnionType).types, isUnitType)); + if (len >= matchingCount) { + bestMatch = target; + matchingCount = len; + } + } + else if (isUnitType(overlap) && 1 >= matchingCount) { + bestMatch = target; + matchingCount = 1; + } } + return bestMatch; } - - namespace JsxNames { - export const JSX = "JSX" as __String; - export const IntrinsicElements = "IntrinsicElements" as __String; - export const ElementClass = "ElementClass" as __String; - export const ElementAttributesPropertyNameContainer = "ElementAttributesProperty" as __String; // TODO: Deprecate and remove support - export const ElementChildrenAttributeNameContainer = "ElementChildrenAttribute" as __String; - export const Element = "Element" as __String; - export const IntrinsicAttributes = "IntrinsicAttributes" as __String; - export const IntrinsicClassAttributes = "IntrinsicClassAttributes" as __String; - export const LibraryManagedAttributes = "LibraryManagedAttributes" as __String; + function filterPrimitivesIfContainsNonPrimitive(type: UnionType) { + if (maybeTypeOfKind(type, TypeFlags.NonPrimitive)) { + const result = filterType(type, t => !(t.flags & TypeFlags.Primitive)); + if (!(result.flags & TypeFlags.Never)) { + return result; + } + } + return type; } - - function getIterationTypesKeyFromIterationTypeKind(typeKind: IterationTypeKind) { - switch (typeKind) { - case IterationTypeKind.Yield: return "yieldType"; - case IterationTypeKind.Return: return "returnType"; - case IterationTypeKind.Next: return "nextType"; + // Keep this up-to-date with the same logic within `getApparentTypeOfContextualType`, since they should behave similarly + function findMatchingDiscriminantType(source: ts.Type, target: ts.Type, isRelatedTo: (source: ts.Type, target: ts.Type) => Ternary) { + if (target.flags & TypeFlags.Union && source.flags & (TypeFlags.Intersection | TypeFlags.Object)) { + const sourceProperties = getPropertiesOfType(source); + if (sourceProperties) { + const sourcePropertiesFiltered = findDiscriminantProperties(sourceProperties, target); + if (sourcePropertiesFiltered) { + return discriminateTypeByDiscriminableItems((target), map(sourcePropertiesFiltered, p => ([() => getTypeOfSymbol(p), p.escapedName] as [() => ts.Type, __String])), isRelatedTo); + } + } } + return undefined; } - - export function signatureHasRestParameter(s: Signature) { - return !!(s.flags & SignatureFlags.HasRestParameter); +} +/* @internal */ +function isNotAccessor(declaration: Declaration): boolean { + // Accessors check for their own matching duplicates, and in contexts where they are valid, there are already duplicate identifier checks + return !isAccessor(declaration); +} +/* @internal */ +function isNotOverload(declaration: Declaration): boolean { + return (declaration.kind !== SyntaxKind.FunctionDeclaration && declaration.kind !== SyntaxKind.MethodDeclaration) || + !!(declaration as FunctionDeclaration).body; +} +/** Like 'isDeclarationName', but returns true for LHS of `import { x as y }` or `export { x as y }`. */ +/* @internal */ +function isDeclarationNameOrImportPropertyName(name: Node): boolean { + switch (name.parent.kind) { + case SyntaxKind.ImportSpecifier: + case SyntaxKind.ExportSpecifier: + return isIdentifier(name); + default: + return isDeclarationName(name); } - - export function signatureHasLiteralTypes(s: Signature) { - return !!(s.flags & SignatureFlags.HasLiteralTypes); +} +/* @internal */ +function isSomeImportDeclaration(decl: Node): boolean { + switch (decl.kind) { + case SyntaxKind.ImportClause: // For default import + case SyntaxKind.ImportEqualsDeclaration: + case SyntaxKind.NamespaceImport: + case SyntaxKind.ImportSpecifier: // For rename import `x as y` + return true; + case SyntaxKind.Identifier: + // For regular import, `decl` is an Identifier under the ImportSpecifier. + return decl.parent.kind === SyntaxKind.ImportSpecifier; + default: + return false; } - +} +/* @internal */ +namespace JsxNames { + export const JSX = ("JSX" as __String); + export const IntrinsicElements = ("IntrinsicElements" as __String); + export const ElementClass = ("ElementClass" as __String); + export const ElementAttributesPropertyNameContainer = ("ElementAttributesProperty" as __String); // TODO: Deprecate and remove support + export const ElementChildrenAttributeNameContainer = ("ElementChildrenAttribute" as __String); + export const Element = ("Element" as __String); + export const IntrinsicAttributes = ("IntrinsicAttributes" as __String); + export const IntrinsicClassAttributes = ("IntrinsicClassAttributes" as __String); + export const LibraryManagedAttributes = ("LibraryManagedAttributes" as __String); +} +/* @internal */ +function getIterationTypesKeyFromIterationTypeKind(typeKind: IterationTypeKind) { + switch (typeKind) { + case IterationTypeKind.Yield: return "yieldType"; + case IterationTypeKind.Return: return "returnType"; + case IterationTypeKind.Next: return "nextType"; + } +} +/* @internal */ +export function signatureHasRestParameter(s: Signature) { + return !!(s.flags & SignatureFlags.HasRestParameter); +} +/* @internal */ +export function signatureHasLiteralTypes(s: Signature) { + return !!(s.flags & SignatureFlags.HasLiteralTypes); } diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 72bc87d4eee9d..e46923609055b 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -1,3211 +1,2874 @@ -namespace ts { - /* @internal */ - export const compileOnSaveCommandLineOption: CommandLineOption = { name: "compileOnSave", type: "boolean" }; - - // NOTE: The order here is important to default lib ordering as entries will have the same - // order in the generated program (see `getDefaultLibPriority` in program.ts). This - // order also affects overload resolution when a type declared in one lib is - // augmented in another lib. - const libEntries: [string, string][] = [ - // JavaScript only - ["es5", "lib.es5.d.ts"], - ["es6", "lib.es2015.d.ts"], - ["es2015", "lib.es2015.d.ts"], - ["es7", "lib.es2016.d.ts"], - ["es2016", "lib.es2016.d.ts"], - ["es2017", "lib.es2017.d.ts"], - ["es2018", "lib.es2018.d.ts"], - ["es2019", "lib.es2019.d.ts"], - ["es2020", "lib.es2020.d.ts"], - ["esnext", "lib.esnext.d.ts"], - // Host only - ["dom", "lib.dom.d.ts"], - ["dom.iterable", "lib.dom.iterable.d.ts"], - ["webworker", "lib.webworker.d.ts"], - ["webworker.importscripts", "lib.webworker.importscripts.d.ts"], - ["scripthost", "lib.scripthost.d.ts"], - // ES2015 Or ESNext By-feature options - ["es2015.core", "lib.es2015.core.d.ts"], - ["es2015.collection", "lib.es2015.collection.d.ts"], - ["es2015.generator", "lib.es2015.generator.d.ts"], - ["es2015.iterable", "lib.es2015.iterable.d.ts"], - ["es2015.promise", "lib.es2015.promise.d.ts"], - ["es2015.proxy", "lib.es2015.proxy.d.ts"], - ["es2015.reflect", "lib.es2015.reflect.d.ts"], - ["es2015.symbol", "lib.es2015.symbol.d.ts"], - ["es2015.symbol.wellknown", "lib.es2015.symbol.wellknown.d.ts"], - ["es2016.array.include", "lib.es2016.array.include.d.ts"], - ["es2017.object", "lib.es2017.object.d.ts"], - ["es2017.sharedmemory", "lib.es2017.sharedmemory.d.ts"], - ["es2017.string", "lib.es2017.string.d.ts"], - ["es2017.intl", "lib.es2017.intl.d.ts"], - ["es2017.typedarrays", "lib.es2017.typedarrays.d.ts"], - ["es2018.asyncgenerator", "lib.es2018.asyncgenerator.d.ts"], - ["es2018.asynciterable", "lib.es2018.asynciterable.d.ts"], - ["es2018.intl", "lib.es2018.intl.d.ts"], - ["es2018.promise", "lib.es2018.promise.d.ts"], - ["es2018.regexp", "lib.es2018.regexp.d.ts"], - ["es2019.array", "lib.es2019.array.d.ts"], - ["es2019.object", "lib.es2019.object.d.ts"], - ["es2019.string", "lib.es2019.string.d.ts"], - ["es2019.symbol", "lib.es2019.symbol.d.ts"], - ["es2020.bigint", "lib.es2020.bigint.d.ts"], - ["es2020.promise", "lib.es2020.promise.d.ts"], - ["es2020.string", "lib.es2020.string.d.ts"], - ["es2020.symbol.wellknown", "lib.es2020.symbol.wellknown.d.ts"], - ["esnext.array", "lib.es2019.array.d.ts"], - ["esnext.symbol", "lib.es2019.symbol.d.ts"], - ["esnext.asynciterable", "lib.es2018.asynciterable.d.ts"], - ["esnext.intl", "lib.esnext.intl.d.ts"], - ["esnext.bigint", "lib.es2020.bigint.d.ts"] - ]; - - /** - * An array of supported "lib" reference file names used to determine the order for inclusion - * when referenced, as well as for spelling suggestions. This ensures the correct ordering for - * overload resolution when a type declared in one lib is extended by another. - */ - /* @internal */ - export const libs = libEntries.map(entry => entry[0]); - - /** - * A map of lib names to lib files. This map is used both for parsing the "lib" command line - * option as well as for resolving lib reference directives. - */ - /* @internal */ - export const libMap = createMapFromEntries(libEntries); - - // Watch related options - /* @internal */ - export const optionsForWatch: CommandLineOption[] = [ - { - name: "watchFile", - type: createMapFromTemplate({ - fixedpollinginterval: WatchFileKind.FixedPollingInterval, - prioritypollinginterval: WatchFileKind.PriorityPollingInterval, - dynamicprioritypolling: WatchFileKind.DynamicPriorityPolling, - usefsevents: WatchFileKind.UseFsEvents, - usefseventsonparentdirectory: WatchFileKind.UseFsEventsOnParentDirectory, - }), - category: Diagnostics.Advanced_Options, - description: Diagnostics.Specify_strategy_for_watching_file_Colon_FixedPollingInterval_default_PriorityPollingInterval_DynamicPriorityPolling_UseFsEvents_UseFsEventsOnParentDirectory, - }, - { - name: "watchDirectory", - type: createMapFromTemplate({ - usefsevents: WatchDirectoryKind.UseFsEvents, - fixedpollinginterval: WatchDirectoryKind.FixedPollingInterval, - dynamicprioritypolling: WatchDirectoryKind.DynamicPriorityPolling, - }), - category: Diagnostics.Advanced_Options, - description: Diagnostics.Specify_strategy_for_watching_directory_on_platforms_that_don_t_support_recursive_watching_natively_Colon_UseFsEvents_default_FixedPollingInterval_DynamicPriorityPolling, - }, - { - name: "fallbackPolling", - type: createMapFromTemplate({ - fixedinterval: PollingWatchKind.FixedInterval, - priorityinterval: PollingWatchKind.PriorityInterval, - dynamicpriority: PollingWatchKind.DynamicPriority, - }), - category: Diagnostics.Advanced_Options, - description: Diagnostics.Specify_strategy_for_creating_a_polling_watch_when_it_fails_to_create_using_file_system_events_Colon_FixedInterval_default_PriorityInterval_DynamicPriority, - }, - { - name: "synchronousWatchDirectory", - type: "boolean", - category: Diagnostics.Advanced_Options, - description: Diagnostics.Synchronously_call_callbacks_and_update_the_state_of_directory_watchers_on_platforms_that_don_t_support_recursive_watching_natively, - }, - ]; - - /* @internal */ - export const commonOptionsWithBuild: CommandLineOption[] = [ - { - name: "help", - shortName: "h", - type: "boolean", - showInSimplifiedHelpView: true, - category: Diagnostics.Command_line_Options, - description: Diagnostics.Print_this_message, - }, - { - name: "help", - shortName: "?", - type: "boolean" - }, - { - name: "watch", - shortName: "w", - type: "boolean", - showInSimplifiedHelpView: true, - category: Diagnostics.Command_line_Options, - description: Diagnostics.Watch_input_files, - }, - { - name: "preserveWatchOutput", - type: "boolean", - showInSimplifiedHelpView: false, - category: Diagnostics.Command_line_Options, - description: Diagnostics.Whether_to_keep_outdated_console_output_in_watch_mode_instead_of_clearing_the_screen, - }, - { - name: "listFiles", - type: "boolean", - category: Diagnostics.Advanced_Options, - description: Diagnostics.Print_names_of_files_part_of_the_compilation - }, - { - name: "listEmittedFiles", - type: "boolean", - category: Diagnostics.Advanced_Options, - description: Diagnostics.Print_names_of_generated_files_part_of_the_compilation - }, - { - name: "pretty", - type: "boolean", - showInSimplifiedHelpView: true, - category: Diagnostics.Command_line_Options, - description: Diagnostics.Stylize_errors_and_messages_using_color_and_context_experimental - }, - - { - name: "traceResolution", - type: "boolean", - category: Diagnostics.Advanced_Options, - description: Diagnostics.Enable_tracing_of_the_name_resolution_process - }, - { - name: "diagnostics", - type: "boolean", - category: Diagnostics.Advanced_Options, - description: Diagnostics.Show_diagnostic_information - }, - { - name: "extendedDiagnostics", - type: "boolean", - category: Diagnostics.Advanced_Options, - description: Diagnostics.Show_verbose_diagnostic_information - }, - { - name: "generateCpuProfile", - type: "string", - isFilePath: true, - paramType: Diagnostics.FILE_OR_DIRECTORY, - category: Diagnostics.Advanced_Options, - description: Diagnostics.Generates_a_CPU_profile - }, - { - name: "incremental", - shortName: "i", - type: "boolean", - category: Diagnostics.Basic_Options, - description: Diagnostics.Enable_incremental_compilation, - transpileOptionValue: undefined - }, - { - name: "assumeChangesOnlyAffectDirectDependencies", - type: "boolean", - affectsSemanticDiagnostics: true, - affectsEmit: true, - category: Diagnostics.Advanced_Options, - description: Diagnostics.Have_recompiles_in_incremental_and_watch_assume_that_changes_within_a_file_will_only_affect_files_directly_depending_on_it - }, - { - name: "locale", - type: "string", - category: Diagnostics.Advanced_Options, - description: Diagnostics.The_locale_used_when_displaying_messages_to_the_user_e_g_en_us - }, - ]; - - /* @internal */ - export const optionDeclarations: CommandLineOption[] = [ - // CommandLine only options - ...commonOptionsWithBuild, - { - name: "all", - type: "boolean", - showInSimplifiedHelpView: true, - category: Diagnostics.Command_line_Options, - description: Diagnostics.Show_all_compiler_options, - }, - { - name: "version", - shortName: "v", - type: "boolean", - showInSimplifiedHelpView: true, - category: Diagnostics.Command_line_Options, - description: Diagnostics.Print_the_compiler_s_version, - }, - { - name: "init", - type: "boolean", - showInSimplifiedHelpView: true, - category: Diagnostics.Command_line_Options, - description: Diagnostics.Initializes_a_TypeScript_project_and_creates_a_tsconfig_json_file, - }, - { - name: "project", - shortName: "p", - type: "string", - isFilePath: true, - showInSimplifiedHelpView: true, - category: Diagnostics.Command_line_Options, - paramType: Diagnostics.FILE_OR_DIRECTORY, - description: Diagnostics.Compile_the_project_given_the_path_to_its_configuration_file_or_to_a_folder_with_a_tsconfig_json, - }, - { - name: "build", - type: "boolean", - shortName: "b", - showInSimplifiedHelpView: true, - category: Diagnostics.Command_line_Options, - description: Diagnostics.Build_one_or_more_projects_and_their_dependencies_if_out_of_date - }, - { - name: "showConfig", - type: "boolean", - category: Diagnostics.Command_line_Options, - isCommandLineOnly: true, - description: Diagnostics.Print_the_final_configuration_instead_of_building - }, - { - name: "listFilesOnly", - type: "boolean", - category: Diagnostics.Command_line_Options, - affectsSemanticDiagnostics: true, - affectsEmit: true, - isCommandLineOnly: true, - description: Diagnostics.Print_names_of_files_that_are_part_of_the_compilation_and_then_stop_processing - }, - - // Basic - { - name: "target", - shortName: "t", - type: createMapFromTemplate({ - es3: ScriptTarget.ES3, - es5: ScriptTarget.ES5, - es6: ScriptTarget.ES2015, - es2015: ScriptTarget.ES2015, - es2016: ScriptTarget.ES2016, - es2017: ScriptTarget.ES2017, - es2018: ScriptTarget.ES2018, - es2019: ScriptTarget.ES2019, - es2020: ScriptTarget.ES2020, - esnext: ScriptTarget.ESNext, - }), - affectsSourceFile: true, - affectsModuleResolution: true, - affectsEmit: true, - paramType: Diagnostics.VERSION, - showInSimplifiedHelpView: true, - category: Diagnostics.Basic_Options, - description: Diagnostics.Specify_ECMAScript_target_version_Colon_ES3_default_ES5_ES2015_ES2016_ES2017_ES2018_ES2019_ES2020_or_ESNEXT, - }, - { - name: "module", - shortName: "m", - type: createMapFromTemplate({ - none: ModuleKind.None, - commonjs: ModuleKind.CommonJS, - amd: ModuleKind.AMD, - system: ModuleKind.System, - umd: ModuleKind.UMD, - es6: ModuleKind.ES2015, - es2015: ModuleKind.ES2015, - es2020: ModuleKind.ES2020, - esnext: ModuleKind.ESNext - }), - affectsModuleResolution: true, - affectsEmit: true, - paramType: Diagnostics.KIND, - showInSimplifiedHelpView: true, - category: Diagnostics.Basic_Options, - description: Diagnostics.Specify_module_code_generation_Colon_none_commonjs_amd_system_umd_es2015_es2020_or_ESNext, - }, - { +import { CommandLineOption, createMapFromEntries, createMapFromTemplate, WatchFileKind, Diagnostics, WatchDirectoryKind, PollingWatchKind, ScriptTarget, ModuleKind, JsxEmit, ImportsNotUsedAsValues, ModuleResolutionKind, NewLineKind, hasProperty, createMap, forEach, CompilerOptions, TypeAcquisition, CommandLineOptionOfCustomType, Diagnostic, createCompilerDiagnostic, DiagnosticMessage, arrayFrom, Push, CommandLineOptionOfListType, startsWith, map, mapDefined, CompilerOptionsValue, TsConfigSourceFile, DidYouMeanOptionsDiagnostics, getSpellingSuggestion, WatchOptions, CharacterCodes, sys, ParsedCommandLine, BuildOptions, ParseConfigHost, parseJsonText, toPath, createGetCanonicalFileName, getNormalizedAbsolutePath, getDirectoryPath, isString, arrayToMap, TsConfigOnlyOption, PropertyName, Expression, JsonSourceFile, ObjectLiteralExpression, SyntaxKind, createDiagnosticForNodeInSourceFile, getTextOfPropertyName, unescapeLeadingUnderscores, NodeArray, filter, StringLiteral, NumericLiteral, PrefixUnaryExpression, ArrayLiteralExpression, Node, isStringLiteral, isStringDoubleQuoted, isArray, ProjectReference, getRelativePathFromFile, length, getFileMatcherPatterns, getRegexFromPattern, forEachEntry, extend, Debug, createMultiMap, getLocaleSpecificMessage, Path, FileExtensionInfo, normalizeSlashes, ExpandResult, firstDefined, getTsConfigPropArray, ConfigFileSpecs, filterMutate, assign, isRootedDiskPath, endsWith, Extension, nodeModuleNameResolver, combinePaths, toFileNameLowerCase, getBaseFileName, convertToRelativePath, identity, normalizePath, getSupportedExtensions, getSuppoertedExtensionsWithJsonIfResolveJsonModule, fileExtensionIs, getRegularExpressionsForWildcards, emptyArray, findIndex, getTsConfigPropArrayElementValue, MapLike, WatchDirectoryFlags, getRegularExpressionForWildcard, containsPath, isImplicitGlob, getExtensionPriority, adjustExtensionPriority, ExtensionPriority, changeExtension, getNextLowestExtensionPriority } from "./ts"; +import * as ts from "./ts"; +/* @internal */ +export const compileOnSaveCommandLineOption: CommandLineOption = { name: "compileOnSave", type: "boolean" }; +// NOTE: The order here is important to default lib ordering as entries will have the same +// order in the generated program (see `getDefaultLibPriority` in program.ts). This +// order also affects overload resolution when a type declared in one lib is +// augmented in another lib. +const libEntries: [string, string][] = [ + // JavaScript only + ["es5", "lib.es5.d.ts"], + ["es6", "lib.es2015.d.ts"], + ["es2015", "lib.es2015.d.ts"], + ["es7", "lib.es2016.d.ts"], + ["es2016", "lib.es2016.d.ts"], + ["es2017", "lib.es2017.d.ts"], + ["es2018", "lib.es2018.d.ts"], + ["es2019", "lib.es2019.d.ts"], + ["es2020", "lib.es2020.d.ts"], + ["esnext", "lib.esnext.d.ts"], + // Host only + ["dom", "lib.dom.d.ts"], + ["dom.iterable", "lib.dom.iterable.d.ts"], + ["webworker", "lib.webworker.d.ts"], + ["webworker.importscripts", "lib.webworker.importscripts.d.ts"], + ["scripthost", "lib.scripthost.d.ts"], + // ES2015 Or ESNext By-feature options + ["es2015.core", "lib.es2015.core.d.ts"], + ["es2015.collection", "lib.es2015.collection.d.ts"], + ["es2015.generator", "lib.es2015.generator.d.ts"], + ["es2015.iterable", "lib.es2015.iterable.d.ts"], + ["es2015.promise", "lib.es2015.promise.d.ts"], + ["es2015.proxy", "lib.es2015.proxy.d.ts"], + ["es2015.reflect", "lib.es2015.reflect.d.ts"], + ["es2015.symbol", "lib.es2015.symbol.d.ts"], + ["es2015.symbol.wellknown", "lib.es2015.symbol.wellknown.d.ts"], + ["es2016.array.include", "lib.es2016.array.include.d.ts"], + ["es2017.object", "lib.es2017.object.d.ts"], + ["es2017.sharedmemory", "lib.es2017.sharedmemory.d.ts"], + ["es2017.string", "lib.es2017.string.d.ts"], + ["es2017.intl", "lib.es2017.intl.d.ts"], + ["es2017.typedarrays", "lib.es2017.typedarrays.d.ts"], + ["es2018.asyncgenerator", "lib.es2018.asyncgenerator.d.ts"], + ["es2018.asynciterable", "lib.es2018.asynciterable.d.ts"], + ["es2018.intl", "lib.es2018.intl.d.ts"], + ["es2018.promise", "lib.es2018.promise.d.ts"], + ["es2018.regexp", "lib.es2018.regexp.d.ts"], + ["es2019.array", "lib.es2019.array.d.ts"], + ["es2019.object", "lib.es2019.object.d.ts"], + ["es2019.string", "lib.es2019.string.d.ts"], + ["es2019.symbol", "lib.es2019.symbol.d.ts"], + ["es2020.bigint", "lib.es2020.bigint.d.ts"], + ["es2020.promise", "lib.es2020.promise.d.ts"], + ["es2020.string", "lib.es2020.string.d.ts"], + ["es2020.symbol.wellknown", "lib.es2020.symbol.wellknown.d.ts"], + ["esnext.array", "lib.es2019.array.d.ts"], + ["esnext.symbol", "lib.es2019.symbol.d.ts"], + ["esnext.asynciterable", "lib.es2018.asynciterable.d.ts"], + ["esnext.intl", "lib.esnext.intl.d.ts"], + ["esnext.bigint", "lib.es2020.bigint.d.ts"] +]; +/** + * An array of supported "lib" reference file names used to determine the order for inclusion + * when referenced, as well as for spelling suggestions. This ensures the correct ordering for + * overload resolution when a type declared in one lib is extended by another. + */ +/* @internal */ +export const libs = libEntries.map(entry => entry[0]); +/** + * A map of lib names to lib files. This map is used both for parsing the "lib" command line + * option as well as for resolving lib reference directives. + */ +/* @internal */ +export const libMap = createMapFromEntries(libEntries); +// Watch related options +/* @internal */ +export const optionsForWatch: CommandLineOption[] = [ + { + name: "watchFile", + type: createMapFromTemplate({ + fixedpollinginterval: WatchFileKind.FixedPollingInterval, + prioritypollinginterval: WatchFileKind.PriorityPollingInterval, + dynamicprioritypolling: WatchFileKind.DynamicPriorityPolling, + usefsevents: WatchFileKind.UseFsEvents, + usefseventsonparentdirectory: WatchFileKind.UseFsEventsOnParentDirectory, + }), + category: Diagnostics.Advanced_Options, + description: Diagnostics.Specify_strategy_for_watching_file_Colon_FixedPollingInterval_default_PriorityPollingInterval_DynamicPriorityPolling_UseFsEvents_UseFsEventsOnParentDirectory, + }, + { + name: "watchDirectory", + type: createMapFromTemplate({ + usefsevents: WatchDirectoryKind.UseFsEvents, + fixedpollinginterval: WatchDirectoryKind.FixedPollingInterval, + dynamicprioritypolling: WatchDirectoryKind.DynamicPriorityPolling, + }), + category: Diagnostics.Advanced_Options, + description: Diagnostics.Specify_strategy_for_watching_directory_on_platforms_that_don_t_support_recursive_watching_natively_Colon_UseFsEvents_default_FixedPollingInterval_DynamicPriorityPolling, + }, + { + name: "fallbackPolling", + type: createMapFromTemplate({ + fixedinterval: PollingWatchKind.FixedInterval, + priorityinterval: PollingWatchKind.PriorityInterval, + dynamicpriority: PollingWatchKind.DynamicPriority, + }), + category: Diagnostics.Advanced_Options, + description: Diagnostics.Specify_strategy_for_creating_a_polling_watch_when_it_fails_to_create_using_file_system_events_Colon_FixedInterval_default_PriorityInterval_DynamicPriority, + }, + { + name: "synchronousWatchDirectory", + type: "boolean", + category: Diagnostics.Advanced_Options, + description: Diagnostics.Synchronously_call_callbacks_and_update_the_state_of_directory_watchers_on_platforms_that_don_t_support_recursive_watching_natively, + }, +]; +/* @internal */ +export const commonOptionsWithBuild: CommandLineOption[] = [ + { + name: "help", + shortName: "h", + type: "boolean", + showInSimplifiedHelpView: true, + category: Diagnostics.Command_line_Options, + description: Diagnostics.Print_this_message, + }, + { + name: "help", + shortName: "?", + type: "boolean" + }, + { + name: "watch", + shortName: "w", + type: "boolean", + showInSimplifiedHelpView: true, + category: Diagnostics.Command_line_Options, + description: Diagnostics.Watch_input_files, + }, + { + name: "preserveWatchOutput", + type: "boolean", + showInSimplifiedHelpView: false, + category: Diagnostics.Command_line_Options, + description: Diagnostics.Whether_to_keep_outdated_console_output_in_watch_mode_instead_of_clearing_the_screen, + }, + { + name: "listFiles", + type: "boolean", + category: Diagnostics.Advanced_Options, + description: Diagnostics.Print_names_of_files_part_of_the_compilation + }, + { + name: "listEmittedFiles", + type: "boolean", + category: Diagnostics.Advanced_Options, + description: Diagnostics.Print_names_of_generated_files_part_of_the_compilation + }, + { + name: "pretty", + type: "boolean", + showInSimplifiedHelpView: true, + category: Diagnostics.Command_line_Options, + description: Diagnostics.Stylize_errors_and_messages_using_color_and_context_experimental + }, + { + name: "traceResolution", + type: "boolean", + category: Diagnostics.Advanced_Options, + description: Diagnostics.Enable_tracing_of_the_name_resolution_process + }, + { + name: "diagnostics", + type: "boolean", + category: Diagnostics.Advanced_Options, + description: Diagnostics.Show_diagnostic_information + }, + { + name: "extendedDiagnostics", + type: "boolean", + category: Diagnostics.Advanced_Options, + description: Diagnostics.Show_verbose_diagnostic_information + }, + { + name: "generateCpuProfile", + type: "string", + isFilePath: true, + paramType: Diagnostics.FILE_OR_DIRECTORY, + category: Diagnostics.Advanced_Options, + description: Diagnostics.Generates_a_CPU_profile + }, + { + name: "incremental", + shortName: "i", + type: "boolean", + category: Diagnostics.Basic_Options, + description: Diagnostics.Enable_incremental_compilation, + transpileOptionValue: undefined + }, + { + name: "assumeChangesOnlyAffectDirectDependencies", + type: "boolean", + affectsSemanticDiagnostics: true, + affectsEmit: true, + category: Diagnostics.Advanced_Options, + description: Diagnostics.Have_recompiles_in_incremental_and_watch_assume_that_changes_within_a_file_will_only_affect_files_directly_depending_on_it + }, + { + name: "locale", + type: "string", + category: Diagnostics.Advanced_Options, + description: Diagnostics.The_locale_used_when_displaying_messages_to_the_user_e_g_en_us + }, +]; +/* @internal */ +export const optionDeclarations: CommandLineOption[] = [ + // CommandLine only options + ...commonOptionsWithBuild, + { + name: "all", + type: "boolean", + showInSimplifiedHelpView: true, + category: Diagnostics.Command_line_Options, + description: Diagnostics.Show_all_compiler_options, + }, + { + name: "version", + shortName: "v", + type: "boolean", + showInSimplifiedHelpView: true, + category: Diagnostics.Command_line_Options, + description: Diagnostics.Print_the_compiler_s_version, + }, + { + name: "init", + type: "boolean", + showInSimplifiedHelpView: true, + category: Diagnostics.Command_line_Options, + description: Diagnostics.Initializes_a_TypeScript_project_and_creates_a_tsconfig_json_file, + }, + { + name: "project", + shortName: "p", + type: "string", + isFilePath: true, + showInSimplifiedHelpView: true, + category: Diagnostics.Command_line_Options, + paramType: Diagnostics.FILE_OR_DIRECTORY, + description: Diagnostics.Compile_the_project_given_the_path_to_its_configuration_file_or_to_a_folder_with_a_tsconfig_json, + }, + { + name: "build", + type: "boolean", + shortName: "b", + showInSimplifiedHelpView: true, + category: Diagnostics.Command_line_Options, + description: Diagnostics.Build_one_or_more_projects_and_their_dependencies_if_out_of_date + }, + { + name: "showConfig", + type: "boolean", + category: Diagnostics.Command_line_Options, + isCommandLineOnly: true, + description: Diagnostics.Print_the_final_configuration_instead_of_building + }, + { + name: "listFilesOnly", + type: "boolean", + category: Diagnostics.Command_line_Options, + affectsSemanticDiagnostics: true, + affectsEmit: true, + isCommandLineOnly: true, + description: Diagnostics.Print_names_of_files_that_are_part_of_the_compilation_and_then_stop_processing + }, + // Basic + { + name: "target", + shortName: "t", + type: createMapFromTemplate({ + es3: ScriptTarget.ES3, + es5: ScriptTarget.ES5, + es6: ScriptTarget.ES2015, + es2015: ScriptTarget.ES2015, + es2016: ScriptTarget.ES2016, + es2017: ScriptTarget.ES2017, + es2018: ScriptTarget.ES2018, + es2019: ScriptTarget.ES2019, + es2020: ScriptTarget.ES2020, + esnext: ScriptTarget.ESNext, + }), + affectsSourceFile: true, + affectsModuleResolution: true, + affectsEmit: true, + paramType: Diagnostics.VERSION, + showInSimplifiedHelpView: true, + category: Diagnostics.Basic_Options, + description: Diagnostics.Specify_ECMAScript_target_version_Colon_ES3_default_ES5_ES2015_ES2016_ES2017_ES2018_ES2019_ES2020_or_ESNEXT, + }, + { + name: "module", + shortName: "m", + type: createMapFromTemplate({ + none: ModuleKind.None, + commonjs: ModuleKind.CommonJS, + amd: ModuleKind.AMD, + system: ModuleKind.System, + umd: ModuleKind.UMD, + es6: ModuleKind.ES2015, + es2015: ModuleKind.ES2015, + es2020: ModuleKind.ES2020, + esnext: ModuleKind.ESNext + }), + affectsModuleResolution: true, + affectsEmit: true, + paramType: Diagnostics.KIND, + showInSimplifiedHelpView: true, + category: Diagnostics.Basic_Options, + description: Diagnostics.Specify_module_code_generation_Colon_none_commonjs_amd_system_umd_es2015_es2020_or_ESNext, + }, + { + name: "lib", + type: "list", + element: { name: "lib", - type: "list", - element: { - name: "lib", - type: libMap - }, - affectsModuleResolution: true, - showInSimplifiedHelpView: true, - category: Diagnostics.Basic_Options, - description: Diagnostics.Specify_library_files_to_be_included_in_the_compilation, - transpileOptionValue: undefined - }, - { - name: "allowJs", - type: "boolean", - affectsModuleResolution: true, - showInSimplifiedHelpView: true, - category: Diagnostics.Basic_Options, - description: Diagnostics.Allow_javascript_files_to_be_compiled - }, - { - name: "checkJs", - type: "boolean", - category: Diagnostics.Basic_Options, - description: Diagnostics.Report_errors_in_js_files - }, - { - name: "jsx", - type: createMapFromTemplate({ - "preserve": JsxEmit.Preserve, - "react-native": JsxEmit.ReactNative, - "react": JsxEmit.React - }), - affectsSourceFile: true, - paramType: Diagnostics.KIND, - showInSimplifiedHelpView: true, - category: Diagnostics.Basic_Options, - description: Diagnostics.Specify_JSX_code_generation_Colon_preserve_react_native_or_react, - }, - { - name: "declaration", - shortName: "d", - type: "boolean", - affectsEmit: true, - showInSimplifiedHelpView: true, - category: Diagnostics.Basic_Options, - description: Diagnostics.Generates_corresponding_d_ts_file, - transpileOptionValue: undefined - }, - { - name: "declarationMap", - type: "boolean", - affectsEmit: true, - showInSimplifiedHelpView: true, - category: Diagnostics.Basic_Options, - description: Diagnostics.Generates_a_sourcemap_for_each_corresponding_d_ts_file, - transpileOptionValue: undefined - }, - { - name: "emitDeclarationOnly", - type: "boolean", - affectsEmit: true, - category: Diagnostics.Advanced_Options, - description: Diagnostics.Only_emit_d_ts_declaration_files, - transpileOptionValue: undefined - }, - { - name: "sourceMap", - type: "boolean", - affectsEmit: true, - showInSimplifiedHelpView: true, - category: Diagnostics.Basic_Options, - description: Diagnostics.Generates_corresponding_map_file, - }, - { - name: "outFile", - type: "string", - affectsEmit: true, - isFilePath: true, - paramType: Diagnostics.FILE, - showInSimplifiedHelpView: true, - category: Diagnostics.Basic_Options, - description: Diagnostics.Concatenate_and_emit_output_to_single_file, - transpileOptionValue: undefined - }, - { - name: "outDir", - type: "string", - affectsEmit: true, - isFilePath: true, - paramType: Diagnostics.DIRECTORY, - showInSimplifiedHelpView: true, - category: Diagnostics.Basic_Options, - description: Diagnostics.Redirect_output_structure_to_the_directory, - }, - { - name: "rootDir", - type: "string", - affectsEmit: true, - isFilePath: true, - paramType: Diagnostics.LOCATION, - category: Diagnostics.Basic_Options, - description: Diagnostics.Specify_the_root_directory_of_input_files_Use_to_control_the_output_directory_structure_with_outDir, - }, - { - name: "composite", - type: "boolean", - affectsEmit: true, - isTSConfigOnly: true, - category: Diagnostics.Basic_Options, - description: Diagnostics.Enable_project_compilation, - transpileOptionValue: undefined - }, - { - name: "tsBuildInfoFile", - type: "string", - affectsEmit: true, - isFilePath: true, - paramType: Diagnostics.FILE, - category: Diagnostics.Basic_Options, - description: Diagnostics.Specify_file_to_store_incremental_compilation_information, - transpileOptionValue: undefined - }, - { - name: "removeComments", - type: "boolean", - affectsEmit: true, - showInSimplifiedHelpView: true, - category: Diagnostics.Basic_Options, - description: Diagnostics.Do_not_emit_comments_to_output, - }, - { - name: "noEmit", - type: "boolean", - affectsEmit: true, - showInSimplifiedHelpView: true, - category: Diagnostics.Basic_Options, - description: Diagnostics.Do_not_emit_outputs, - transpileOptionValue: undefined - }, - { - name: "importHelpers", - type: "boolean", - affectsEmit: true, - category: Diagnostics.Basic_Options, - description: Diagnostics.Import_emit_helpers_from_tslib - }, - { - name: "importsNotUsedAsValues", - type: createMapFromTemplate({ - remove: ImportsNotUsedAsValues.Remove, - preserve: ImportsNotUsedAsValues.Preserve, - error: ImportsNotUsedAsValues.Error - }), - affectsEmit: true, - affectsSemanticDiagnostics: true, - category: Diagnostics.Advanced_Options, - description: Diagnostics.Specify_emit_Slashchecking_behavior_for_imports_that_are_only_used_for_types - }, - { - name: "downlevelIteration", - type: "boolean", - affectsEmit: true, - category: Diagnostics.Basic_Options, - description: Diagnostics.Provide_full_support_for_iterables_in_for_of_spread_and_destructuring_when_targeting_ES5_or_ES3 - }, - { - name: "isolatedModules", - type: "boolean", - category: Diagnostics.Basic_Options, - description: Diagnostics.Transpile_each_file_as_a_separate_module_similar_to_ts_transpileModule, - transpileOptionValue: true - }, - - // Strict Type Checks - { - name: "strict", - type: "boolean", - showInSimplifiedHelpView: true, - category: Diagnostics.Strict_Type_Checking_Options, - description: Diagnostics.Enable_all_strict_type_checking_options - }, - { - name: "noImplicitAny", - type: "boolean", - affectsSemanticDiagnostics: true, - strictFlag: true, - showInSimplifiedHelpView: true, - category: Diagnostics.Strict_Type_Checking_Options, - description: Diagnostics.Raise_error_on_expressions_and_declarations_with_an_implied_any_type - }, - { - name: "strictNullChecks", - type: "boolean", - affectsSemanticDiagnostics: true, - strictFlag: true, - showInSimplifiedHelpView: true, - category: Diagnostics.Strict_Type_Checking_Options, - description: Diagnostics.Enable_strict_null_checks - }, - { - name: "strictFunctionTypes", - type: "boolean", - affectsSemanticDiagnostics: true, - strictFlag: true, - showInSimplifiedHelpView: true, - category: Diagnostics.Strict_Type_Checking_Options, - description: Diagnostics.Enable_strict_checking_of_function_types - }, - { - name: "strictBindCallApply", - type: "boolean", - strictFlag: true, - showInSimplifiedHelpView: true, - category: Diagnostics.Strict_Type_Checking_Options, - description: Diagnostics.Enable_strict_bind_call_and_apply_methods_on_functions - }, - { - name: "strictPropertyInitialization", - type: "boolean", - affectsSemanticDiagnostics: true, - strictFlag: true, - showInSimplifiedHelpView: true, - category: Diagnostics.Strict_Type_Checking_Options, - description: Diagnostics.Enable_strict_checking_of_property_initialization_in_classes - }, - { - name: "noImplicitThis", - type: "boolean", - affectsSemanticDiagnostics: true, - strictFlag: true, - showInSimplifiedHelpView: true, - category: Diagnostics.Strict_Type_Checking_Options, - description: Diagnostics.Raise_error_on_this_expressions_with_an_implied_any_type, - }, - { - name: "alwaysStrict", - type: "boolean", - affectsSourceFile: true, - strictFlag: true, - showInSimplifiedHelpView: true, - category: Diagnostics.Strict_Type_Checking_Options, - description: Diagnostics.Parse_in_strict_mode_and_emit_use_strict_for_each_source_file - }, - - // Additional Checks - { - name: "noUnusedLocals", - type: "boolean", - affectsSemanticDiagnostics: true, - showInSimplifiedHelpView: true, - category: Diagnostics.Additional_Checks, - description: Diagnostics.Report_errors_on_unused_locals, - }, - { - name: "noUnusedParameters", - type: "boolean", - affectsSemanticDiagnostics: true, - showInSimplifiedHelpView: true, - category: Diagnostics.Additional_Checks, - description: Diagnostics.Report_errors_on_unused_parameters, - }, - { - name: "noImplicitReturns", - type: "boolean", - affectsSemanticDiagnostics: true, - showInSimplifiedHelpView: true, - category: Diagnostics.Additional_Checks, - description: Diagnostics.Report_error_when_not_all_code_paths_in_function_return_a_value - }, - { - name: "noFallthroughCasesInSwitch", - type: "boolean", - affectsBindDiagnostics: true, - affectsSemanticDiagnostics: true, - showInSimplifiedHelpView: true, - category: Diagnostics.Additional_Checks, - description: Diagnostics.Report_errors_for_fallthrough_cases_in_switch_statement - }, - - // Module Resolution - { - name: "moduleResolution", - type: createMapFromTemplate({ - node: ModuleResolutionKind.NodeJs, - classic: ModuleResolutionKind.Classic, - }), - affectsModuleResolution: true, - paramType: Diagnostics.STRATEGY, - category: Diagnostics.Module_Resolution_Options, - description: Diagnostics.Specify_module_resolution_strategy_Colon_node_Node_js_or_classic_TypeScript_pre_1_6, - }, - { - name: "baseUrl", - type: "string", - affectsModuleResolution: true, - isFilePath: true, - category: Diagnostics.Module_Resolution_Options, - description: Diagnostics.Base_directory_to_resolve_non_absolute_module_names - }, - { - // this option can only be specified in tsconfig.json - // use type = object to copy the value as-is - name: "paths", - type: "object", - affectsModuleResolution: true, - isTSConfigOnly: true, - category: Diagnostics.Module_Resolution_Options, - description: Diagnostics.A_series_of_entries_which_re_map_imports_to_lookup_locations_relative_to_the_baseUrl, - transpileOptionValue: undefined - }, - { - // this option can only be specified in tsconfig.json - // use type = object to copy the value as-is + type: libMap + }, + affectsModuleResolution: true, + showInSimplifiedHelpView: true, + category: Diagnostics.Basic_Options, + description: Diagnostics.Specify_library_files_to_be_included_in_the_compilation, + transpileOptionValue: undefined + }, + { + name: "allowJs", + type: "boolean", + affectsModuleResolution: true, + showInSimplifiedHelpView: true, + category: Diagnostics.Basic_Options, + description: Diagnostics.Allow_javascript_files_to_be_compiled + }, + { + name: "checkJs", + type: "boolean", + category: Diagnostics.Basic_Options, + description: Diagnostics.Report_errors_in_js_files + }, + { + name: "jsx", + type: createMapFromTemplate({ + "preserve": JsxEmit.Preserve, + "react-native": JsxEmit.ReactNative, + "react": JsxEmit.React + }), + affectsSourceFile: true, + paramType: Diagnostics.KIND, + showInSimplifiedHelpView: true, + category: Diagnostics.Basic_Options, + description: Diagnostics.Specify_JSX_code_generation_Colon_preserve_react_native_or_react, + }, + { + name: "declaration", + shortName: "d", + type: "boolean", + affectsEmit: true, + showInSimplifiedHelpView: true, + category: Diagnostics.Basic_Options, + description: Diagnostics.Generates_corresponding_d_ts_file, + transpileOptionValue: undefined + }, + { + name: "declarationMap", + type: "boolean", + affectsEmit: true, + showInSimplifiedHelpView: true, + category: Diagnostics.Basic_Options, + description: Diagnostics.Generates_a_sourcemap_for_each_corresponding_d_ts_file, + transpileOptionValue: undefined + }, + { + name: "emitDeclarationOnly", + type: "boolean", + affectsEmit: true, + category: Diagnostics.Advanced_Options, + description: Diagnostics.Only_emit_d_ts_declaration_files, + transpileOptionValue: undefined + }, + { + name: "sourceMap", + type: "boolean", + affectsEmit: true, + showInSimplifiedHelpView: true, + category: Diagnostics.Basic_Options, + description: Diagnostics.Generates_corresponding_map_file, + }, + { + name: "outFile", + type: "string", + affectsEmit: true, + isFilePath: true, + paramType: Diagnostics.FILE, + showInSimplifiedHelpView: true, + category: Diagnostics.Basic_Options, + description: Diagnostics.Concatenate_and_emit_output_to_single_file, + transpileOptionValue: undefined + }, + { + name: "outDir", + type: "string", + affectsEmit: true, + isFilePath: true, + paramType: Diagnostics.DIRECTORY, + showInSimplifiedHelpView: true, + category: Diagnostics.Basic_Options, + description: Diagnostics.Redirect_output_structure_to_the_directory, + }, + { + name: "rootDir", + type: "string", + affectsEmit: true, + isFilePath: true, + paramType: Diagnostics.LOCATION, + category: Diagnostics.Basic_Options, + description: Diagnostics.Specify_the_root_directory_of_input_files_Use_to_control_the_output_directory_structure_with_outDir, + }, + { + name: "composite", + type: "boolean", + affectsEmit: true, + isTSConfigOnly: true, + category: Diagnostics.Basic_Options, + description: Diagnostics.Enable_project_compilation, + transpileOptionValue: undefined + }, + { + name: "tsBuildInfoFile", + type: "string", + affectsEmit: true, + isFilePath: true, + paramType: Diagnostics.FILE, + category: Diagnostics.Basic_Options, + description: Diagnostics.Specify_file_to_store_incremental_compilation_information, + transpileOptionValue: undefined + }, + { + name: "removeComments", + type: "boolean", + affectsEmit: true, + showInSimplifiedHelpView: true, + category: Diagnostics.Basic_Options, + description: Diagnostics.Do_not_emit_comments_to_output, + }, + { + name: "noEmit", + type: "boolean", + affectsEmit: true, + showInSimplifiedHelpView: true, + category: Diagnostics.Basic_Options, + description: Diagnostics.Do_not_emit_outputs, + transpileOptionValue: undefined + }, + { + name: "importHelpers", + type: "boolean", + affectsEmit: true, + category: Diagnostics.Basic_Options, + description: Diagnostics.Import_emit_helpers_from_tslib + }, + { + name: "importsNotUsedAsValues", + type: createMapFromTemplate({ + remove: ImportsNotUsedAsValues.Remove, + preserve: ImportsNotUsedAsValues.Preserve, + error: ImportsNotUsedAsValues.Error + }), + affectsEmit: true, + affectsSemanticDiagnostics: true, + category: Diagnostics.Advanced_Options, + description: Diagnostics.Specify_emit_Slashchecking_behavior_for_imports_that_are_only_used_for_types + }, + { + name: "downlevelIteration", + type: "boolean", + affectsEmit: true, + category: Diagnostics.Basic_Options, + description: Diagnostics.Provide_full_support_for_iterables_in_for_of_spread_and_destructuring_when_targeting_ES5_or_ES3 + }, + { + name: "isolatedModules", + type: "boolean", + category: Diagnostics.Basic_Options, + description: Diagnostics.Transpile_each_file_as_a_separate_module_similar_to_ts_transpileModule, + transpileOptionValue: true + }, + // Strict Type Checks + { + name: "strict", + type: "boolean", + showInSimplifiedHelpView: true, + category: Diagnostics.Strict_Type_Checking_Options, + description: Diagnostics.Enable_all_strict_type_checking_options + }, + { + name: "noImplicitAny", + type: "boolean", + affectsSemanticDiagnostics: true, + strictFlag: true, + showInSimplifiedHelpView: true, + category: Diagnostics.Strict_Type_Checking_Options, + description: Diagnostics.Raise_error_on_expressions_and_declarations_with_an_implied_any_type + }, + { + name: "strictNullChecks", + type: "boolean", + affectsSemanticDiagnostics: true, + strictFlag: true, + showInSimplifiedHelpView: true, + category: Diagnostics.Strict_Type_Checking_Options, + description: Diagnostics.Enable_strict_null_checks + }, + { + name: "strictFunctionTypes", + type: "boolean", + affectsSemanticDiagnostics: true, + strictFlag: true, + showInSimplifiedHelpView: true, + category: Diagnostics.Strict_Type_Checking_Options, + description: Diagnostics.Enable_strict_checking_of_function_types + }, + { + name: "strictBindCallApply", + type: "boolean", + strictFlag: true, + showInSimplifiedHelpView: true, + category: Diagnostics.Strict_Type_Checking_Options, + description: Diagnostics.Enable_strict_bind_call_and_apply_methods_on_functions + }, + { + name: "strictPropertyInitialization", + type: "boolean", + affectsSemanticDiagnostics: true, + strictFlag: true, + showInSimplifiedHelpView: true, + category: Diagnostics.Strict_Type_Checking_Options, + description: Diagnostics.Enable_strict_checking_of_property_initialization_in_classes + }, + { + name: "noImplicitThis", + type: "boolean", + affectsSemanticDiagnostics: true, + strictFlag: true, + showInSimplifiedHelpView: true, + category: Diagnostics.Strict_Type_Checking_Options, + description: Diagnostics.Raise_error_on_this_expressions_with_an_implied_any_type, + }, + { + name: "alwaysStrict", + type: "boolean", + affectsSourceFile: true, + strictFlag: true, + showInSimplifiedHelpView: true, + category: Diagnostics.Strict_Type_Checking_Options, + description: Diagnostics.Parse_in_strict_mode_and_emit_use_strict_for_each_source_file + }, + // Additional Checks + { + name: "noUnusedLocals", + type: "boolean", + affectsSemanticDiagnostics: true, + showInSimplifiedHelpView: true, + category: Diagnostics.Additional_Checks, + description: Diagnostics.Report_errors_on_unused_locals, + }, + { + name: "noUnusedParameters", + type: "boolean", + affectsSemanticDiagnostics: true, + showInSimplifiedHelpView: true, + category: Diagnostics.Additional_Checks, + description: Diagnostics.Report_errors_on_unused_parameters, + }, + { + name: "noImplicitReturns", + type: "boolean", + affectsSemanticDiagnostics: true, + showInSimplifiedHelpView: true, + category: Diagnostics.Additional_Checks, + description: Diagnostics.Report_error_when_not_all_code_paths_in_function_return_a_value + }, + { + name: "noFallthroughCasesInSwitch", + type: "boolean", + affectsBindDiagnostics: true, + affectsSemanticDiagnostics: true, + showInSimplifiedHelpView: true, + category: Diagnostics.Additional_Checks, + description: Diagnostics.Report_errors_for_fallthrough_cases_in_switch_statement + }, + // Module Resolution + { + name: "moduleResolution", + type: createMapFromTemplate({ + node: ModuleResolutionKind.NodeJs, + classic: ModuleResolutionKind.Classic, + }), + affectsModuleResolution: true, + paramType: Diagnostics.STRATEGY, + category: Diagnostics.Module_Resolution_Options, + description: Diagnostics.Specify_module_resolution_strategy_Colon_node_Node_js_or_classic_TypeScript_pre_1_6, + }, + { + name: "baseUrl", + type: "string", + affectsModuleResolution: true, + isFilePath: true, + category: Diagnostics.Module_Resolution_Options, + description: Diagnostics.Base_directory_to_resolve_non_absolute_module_names + }, + { + // this option can only be specified in tsconfig.json + // use type = object to copy the value as-is + name: "paths", + type: "object", + affectsModuleResolution: true, + isTSConfigOnly: true, + category: Diagnostics.Module_Resolution_Options, + description: Diagnostics.A_series_of_entries_which_re_map_imports_to_lookup_locations_relative_to_the_baseUrl, + transpileOptionValue: undefined + }, + { + // this option can only be specified in tsconfig.json + // use type = object to copy the value as-is + name: "rootDirs", + type: "list", + isTSConfigOnly: true, + element: { name: "rootDirs", - type: "list", - isTSConfigOnly: true, - element: { - name: "rootDirs", - type: "string", - isFilePath: true - }, - affectsModuleResolution: true, - category: Diagnostics.Module_Resolution_Options, - description: Diagnostics.List_of_root_folders_whose_combined_content_represents_the_structure_of_the_project_at_runtime, - transpileOptionValue: undefined - }, - { - name: "typeRoots", - type: "list", - element: { - name: "typeRoots", - type: "string", - isFilePath: true - }, - affectsModuleResolution: true, - category: Diagnostics.Module_Resolution_Options, - description: Diagnostics.List_of_folders_to_include_type_definitions_from - }, - { - name: "types", - type: "list", - element: { - name: "types", - type: "string" - }, - affectsModuleResolution: true, - showInSimplifiedHelpView: true, - category: Diagnostics.Module_Resolution_Options, - description: Diagnostics.Type_declaration_files_to_be_included_in_compilation, - transpileOptionValue: undefined - }, - { - name: "allowSyntheticDefaultImports", - type: "boolean", - affectsSemanticDiagnostics: true, - category: Diagnostics.Module_Resolution_Options, - description: Diagnostics.Allow_default_imports_from_modules_with_no_default_export_This_does_not_affect_code_emit_just_typechecking - }, - { - name: "esModuleInterop", - type: "boolean", - affectsSemanticDiagnostics: true, - affectsEmit: true, - showInSimplifiedHelpView: true, - category: Diagnostics.Module_Resolution_Options, - description: Diagnostics.Enables_emit_interoperability_between_CommonJS_and_ES_Modules_via_creation_of_namespace_objects_for_all_imports_Implies_allowSyntheticDefaultImports - }, - { - name: "preserveSymlinks", - type: "boolean", - category: Diagnostics.Module_Resolution_Options, - description: Diagnostics.Do_not_resolve_the_real_path_of_symlinks, - }, - { - name: "allowUmdGlobalAccess", - type: "boolean", - affectsSemanticDiagnostics: true, - category: Diagnostics.Module_Resolution_Options, - description: Diagnostics.Allow_accessing_UMD_globals_from_modules, - }, - - // Source Maps - { - name: "sourceRoot", - type: "string", - affectsEmit: true, - paramType: Diagnostics.LOCATION, - category: Diagnostics.Source_Map_Options, - description: Diagnostics.Specify_the_location_where_debugger_should_locate_TypeScript_files_instead_of_source_locations, - }, - { - name: "mapRoot", - type: "string", - affectsEmit: true, - paramType: Diagnostics.LOCATION, - category: Diagnostics.Source_Map_Options, - description: Diagnostics.Specify_the_location_where_debugger_should_locate_map_files_instead_of_generated_locations, - }, - { - name: "inlineSourceMap", - type: "boolean", - affectsEmit: true, - category: Diagnostics.Source_Map_Options, - description: Diagnostics.Emit_a_single_file_with_source_maps_instead_of_having_a_separate_file - }, - { - name: "inlineSources", - type: "boolean", - affectsEmit: true, - category: Diagnostics.Source_Map_Options, - description: Diagnostics.Emit_the_source_alongside_the_sourcemaps_within_a_single_file_requires_inlineSourceMap_or_sourceMap_to_be_set - }, - - // Experimental - { - name: "experimentalDecorators", - type: "boolean", - affectsSemanticDiagnostics: true, - category: Diagnostics.Experimental_Options, - description: Diagnostics.Enables_experimental_support_for_ES7_decorators - }, - { - name: "emitDecoratorMetadata", - type: "boolean", - affectsSemanticDiagnostics: true, - affectsEmit: true, - category: Diagnostics.Experimental_Options, - description: Diagnostics.Enables_experimental_support_for_emitting_type_metadata_for_decorators - }, - - // Advanced - { - name: "jsxFactory", - type: "string", - category: Diagnostics.Advanced_Options, - description: Diagnostics.Specify_the_JSX_factory_function_to_use_when_targeting_react_JSX_emit_e_g_React_createElement_or_h - }, - { - name: "resolveJsonModule", - type: "boolean", - affectsModuleResolution: true, - category: Diagnostics.Advanced_Options, - description: Diagnostics.Include_modules_imported_with_json_extension - }, - - { - name: "out", - type: "string", - affectsEmit: true, - isFilePath: false, // This is intentionally broken to support compatability with existing tsconfig files - // for correct behaviour, please use outFile - category: Diagnostics.Advanced_Options, - paramType: Diagnostics.FILE, - description: Diagnostics.Deprecated_Use_outFile_instead_Concatenate_and_emit_output_to_single_file, - transpileOptionValue: undefined - }, - { - name: "reactNamespace", - type: "string", - affectsEmit: true, - category: Diagnostics.Advanced_Options, - description: Diagnostics.Deprecated_Use_jsxFactory_instead_Specify_the_object_invoked_for_createElement_when_targeting_react_JSX_emit - }, - { - name: "skipDefaultLibCheck", - type: "boolean", - category: Diagnostics.Advanced_Options, - description: Diagnostics.Deprecated_Use_skipLibCheck_instead_Skip_type_checking_of_default_library_declaration_files - }, - { - name: "charset", type: "string", - category: Diagnostics.Advanced_Options, - description: Diagnostics.The_character_set_of_the_input_files - }, - { - name: "emitBOM", - type: "boolean", - affectsEmit: true, - category: Diagnostics.Advanced_Options, - description: Diagnostics.Emit_a_UTF_8_Byte_Order_Mark_BOM_in_the_beginning_of_output_files - }, - { - name: "newLine", - type: createMapFromTemplate({ - crlf: NewLineKind.CarriageReturnLineFeed, - lf: NewLineKind.LineFeed - }), - affectsEmit: true, - paramType: Diagnostics.NEWLINE, - category: Diagnostics.Advanced_Options, - description: Diagnostics.Specify_the_end_of_line_sequence_to_be_used_when_emitting_files_Colon_CRLF_dos_or_LF_unix, - }, - { - name: "noErrorTruncation", - type: "boolean", - affectsSemanticDiagnostics: true, - category: Diagnostics.Advanced_Options, - description: Diagnostics.Do_not_truncate_error_messages - }, - { - name: "noLib", - type: "boolean", - affectsModuleResolution: true, - category: Diagnostics.Advanced_Options, - description: Diagnostics.Do_not_include_the_default_library_file_lib_d_ts, - // We are not returning a sourceFile for lib file when asked by the program, - // so pass --noLib to avoid reporting a file not found error. - transpileOptionValue: true - }, - { - name: "noResolve", - type: "boolean", - affectsModuleResolution: true, - category: Diagnostics.Advanced_Options, - description: Diagnostics.Do_not_add_triple_slash_references_or_imported_modules_to_the_list_of_compiled_files, - // We are not doing a full typecheck, we are not resolving the whole context, - // so pass --noResolve to avoid reporting missing file errors. - transpileOptionValue: true - }, - { - name: "stripInternal", - type: "boolean", - affectsEmit: true, - category: Diagnostics.Advanced_Options, - description: Diagnostics.Do_not_emit_declarations_for_code_that_has_an_internal_annotation, - }, - { - name: "disableSizeLimit", - type: "boolean", - affectsSourceFile: true, - category: Diagnostics.Advanced_Options, - description: Diagnostics.Disable_size_limitations_on_JavaScript_projects - }, - { - name: "disableSourceOfProjectReferenceRedirect", - type: "boolean", - isTSConfigOnly: true, - category: Diagnostics.Advanced_Options, - description: Diagnostics.Disable_use_of_source_files_instead_of_declaration_files_from_referenced_projects - }, - { - name: "disableSolutionSearching", - type: "boolean", - isTSConfigOnly: true, - category: Diagnostics.Advanced_Options, - description: Diagnostics.Disable_solution_searching_for_this_project - }, - { - name: "noImplicitUseStrict", - type: "boolean", - affectsSemanticDiagnostics: true, - category: Diagnostics.Advanced_Options, - description: Diagnostics.Do_not_emit_use_strict_directives_in_module_output - }, - { - name: "noEmitHelpers", - type: "boolean", - affectsEmit: true, - category: Diagnostics.Advanced_Options, - description: Diagnostics.Do_not_generate_custom_helper_functions_like_extends_in_compiled_output - }, - { - name: "noEmitOnError", - type: "boolean", - affectsEmit: true, - category: Diagnostics.Advanced_Options, - description: Diagnostics.Do_not_emit_outputs_if_any_errors_were_reported, - transpileOptionValue: undefined - }, - { - name: "preserveConstEnums", - type: "boolean", - affectsEmit: true, - category: Diagnostics.Advanced_Options, - description: Diagnostics.Do_not_erase_const_enum_declarations_in_generated_code - }, - { - name: "declarationDir", + isFilePath: true + }, + affectsModuleResolution: true, + category: Diagnostics.Module_Resolution_Options, + description: Diagnostics.List_of_root_folders_whose_combined_content_represents_the_structure_of_the_project_at_runtime, + transpileOptionValue: undefined + }, + { + name: "typeRoots", + type: "list", + element: { + name: "typeRoots", type: "string", - affectsEmit: true, - isFilePath: true, - paramType: Diagnostics.DIRECTORY, - category: Diagnostics.Advanced_Options, - description: Diagnostics.Output_directory_for_generated_declaration_files, - transpileOptionValue: undefined - }, - { - name: "skipLibCheck", - type: "boolean", - category: Diagnostics.Advanced_Options, - description: Diagnostics.Skip_type_checking_of_declaration_files, - }, - { - name: "allowUnusedLabels", - type: "boolean", - affectsBindDiagnostics: true, - affectsSemanticDiagnostics: true, - category: Diagnostics.Advanced_Options, - description: Diagnostics.Do_not_report_errors_on_unused_labels - }, - { - name: "allowUnreachableCode", - type: "boolean", - affectsBindDiagnostics: true, - affectsSemanticDiagnostics: true, - category: Diagnostics.Advanced_Options, - description: Diagnostics.Do_not_report_errors_on_unreachable_code - }, - { - name: "suppressExcessPropertyErrors", - type: "boolean", - affectsSemanticDiagnostics: true, - category: Diagnostics.Advanced_Options, - description: Diagnostics.Suppress_excess_property_checks_for_object_literals, - }, - { - name: "suppressImplicitAnyIndexErrors", - type: "boolean", - affectsSemanticDiagnostics: true, - category: Diagnostics.Advanced_Options, - description: Diagnostics.Suppress_noImplicitAny_errors_for_indexing_objects_lacking_index_signatures, - }, - { - name: "forceConsistentCasingInFileNames", - type: "boolean", - affectsModuleResolution: true, - category: Diagnostics.Advanced_Options, - description: Diagnostics.Disallow_inconsistently_cased_references_to_the_same_file - }, - { - name: "maxNodeModuleJsDepth", - type: "number", - affectsModuleResolution: true, - category: Diagnostics.Advanced_Options, - description: Diagnostics.The_maximum_dependency_depth_to_search_under_node_modules_and_load_JavaScript_files - }, - { - name: "noStrictGenericChecks", - type: "boolean", - affectsSemanticDiagnostics: true, - category: Diagnostics.Advanced_Options, - description: Diagnostics.Disable_strict_checking_of_generic_signatures_in_function_types, - }, - { - name: "useDefineForClassFields", - type: "boolean", - affectsSemanticDiagnostics: true, - affectsEmit: true, - category: Diagnostics.Advanced_Options, - description: Diagnostics.Emit_class_fields_with_Define_instead_of_Set, - }, - { - name: "keyofStringsOnly", - type: "boolean", - category: Diagnostics.Advanced_Options, - description: Diagnostics.Resolve_keyof_to_string_valued_property_names_only_no_numbers_or_symbols, - }, - { - // A list of plugins to load in the language service - name: "plugins", - type: "list", - isTSConfigOnly: true, - element: { - name: "plugin", - type: "object" - }, - description: Diagnostics.List_of_language_service_plugins - }, - ]; - - /* @internal */ - export const semanticDiagnosticsOptionDeclarations: readonly CommandLineOption[] = - optionDeclarations.filter(option => !!option.affectsSemanticDiagnostics); - - /* @internal */ - export const affectsEmitOptionDeclarations: readonly CommandLineOption[] = - optionDeclarations.filter(option => !!option.affectsEmit); - - /* @internal */ - export const moduleResolutionOptionDeclarations: readonly CommandLineOption[] = - optionDeclarations.filter(option => !!option.affectsModuleResolution); - - /* @internal */ - export const sourceFileAffectingCompilerOptions: readonly CommandLineOption[] = optionDeclarations.filter(option => - !!option.affectsSourceFile || !!option.affectsModuleResolution || !!option.affectsBindDiagnostics); - - /* @internal */ - export const transpileOptionValueCompilerOptions: readonly CommandLineOption[] = optionDeclarations.filter(option => - hasProperty(option, "transpileOptionValue")); - - /* @internal */ - export const buildOpts: CommandLineOption[] = [ - ...commonOptionsWithBuild, - { - name: "verbose", - shortName: "v", - category: Diagnostics.Command_line_Options, - description: Diagnostics.Enable_verbose_logging, - type: "boolean" - }, - { - name: "dry", - shortName: "d", - category: Diagnostics.Command_line_Options, - description: Diagnostics.Show_what_would_be_built_or_deleted_if_specified_with_clean, - type: "boolean" - }, - { - name: "force", - shortName: "f", - category: Diagnostics.Command_line_Options, - description: Diagnostics.Build_all_projects_including_those_that_appear_to_be_up_to_date, - type: "boolean" - }, - { - name: "clean", - category: Diagnostics.Command_line_Options, - description: Diagnostics.Delete_the_outputs_of_all_projects, - type: "boolean" - } - ]; - - /* @internal */ - export const typeAcquisitionDeclarations: CommandLineOption[] = [ - { - /* @deprecated typingOptions.enableAutoDiscovery - * Use typeAcquisition.enable instead. - */ - name: "enableAutoDiscovery", - type: "boolean", - }, - { - name: "enable", - type: "boolean", - }, - { + isFilePath: true + }, + affectsModuleResolution: true, + category: Diagnostics.Module_Resolution_Options, + description: Diagnostics.List_of_folders_to_include_type_definitions_from + }, + { + name: "types", + type: "list", + element: { + name: "types", + type: "string" + }, + affectsModuleResolution: true, + showInSimplifiedHelpView: true, + category: Diagnostics.Module_Resolution_Options, + description: Diagnostics.Type_declaration_files_to_be_included_in_compilation, + transpileOptionValue: undefined + }, + { + name: "allowSyntheticDefaultImports", + type: "boolean", + affectsSemanticDiagnostics: true, + category: Diagnostics.Module_Resolution_Options, + description: Diagnostics.Allow_default_imports_from_modules_with_no_default_export_This_does_not_affect_code_emit_just_typechecking + }, + { + name: "esModuleInterop", + type: "boolean", + affectsSemanticDiagnostics: true, + affectsEmit: true, + showInSimplifiedHelpView: true, + category: Diagnostics.Module_Resolution_Options, + description: Diagnostics.Enables_emit_interoperability_between_CommonJS_and_ES_Modules_via_creation_of_namespace_objects_for_all_imports_Implies_allowSyntheticDefaultImports + }, + { + name: "preserveSymlinks", + type: "boolean", + category: Diagnostics.Module_Resolution_Options, + description: Diagnostics.Do_not_resolve_the_real_path_of_symlinks, + }, + { + name: "allowUmdGlobalAccess", + type: "boolean", + affectsSemanticDiagnostics: true, + category: Diagnostics.Module_Resolution_Options, + description: Diagnostics.Allow_accessing_UMD_globals_from_modules, + }, + // Source Maps + { + name: "sourceRoot", + type: "string", + affectsEmit: true, + paramType: Diagnostics.LOCATION, + category: Diagnostics.Source_Map_Options, + description: Diagnostics.Specify_the_location_where_debugger_should_locate_TypeScript_files_instead_of_source_locations, + }, + { + name: "mapRoot", + type: "string", + affectsEmit: true, + paramType: Diagnostics.LOCATION, + category: Diagnostics.Source_Map_Options, + description: Diagnostics.Specify_the_location_where_debugger_should_locate_map_files_instead_of_generated_locations, + }, + { + name: "inlineSourceMap", + type: "boolean", + affectsEmit: true, + category: Diagnostics.Source_Map_Options, + description: Diagnostics.Emit_a_single_file_with_source_maps_instead_of_having_a_separate_file + }, + { + name: "inlineSources", + type: "boolean", + affectsEmit: true, + category: Diagnostics.Source_Map_Options, + description: Diagnostics.Emit_the_source_alongside_the_sourcemaps_within_a_single_file_requires_inlineSourceMap_or_sourceMap_to_be_set + }, + // Experimental + { + name: "experimentalDecorators", + type: "boolean", + affectsSemanticDiagnostics: true, + category: Diagnostics.Experimental_Options, + description: Diagnostics.Enables_experimental_support_for_ES7_decorators + }, + { + name: "emitDecoratorMetadata", + type: "boolean", + affectsSemanticDiagnostics: true, + affectsEmit: true, + category: Diagnostics.Experimental_Options, + description: Diagnostics.Enables_experimental_support_for_emitting_type_metadata_for_decorators + }, + // Advanced + { + name: "jsxFactory", + type: "string", + category: Diagnostics.Advanced_Options, + description: Diagnostics.Specify_the_JSX_factory_function_to_use_when_targeting_react_JSX_emit_e_g_React_createElement_or_h + }, + { + name: "resolveJsonModule", + type: "boolean", + affectsModuleResolution: true, + category: Diagnostics.Advanced_Options, + description: Diagnostics.Include_modules_imported_with_json_extension + }, + { + name: "out", + type: "string", + affectsEmit: true, + isFilePath: false, + // for correct behaviour, please use outFile + category: Diagnostics.Advanced_Options, + paramType: Diagnostics.FILE, + description: Diagnostics.Deprecated_Use_outFile_instead_Concatenate_and_emit_output_to_single_file, + transpileOptionValue: undefined + }, + { + name: "reactNamespace", + type: "string", + affectsEmit: true, + category: Diagnostics.Advanced_Options, + description: Diagnostics.Deprecated_Use_jsxFactory_instead_Specify_the_object_invoked_for_createElement_when_targeting_react_JSX_emit + }, + { + name: "skipDefaultLibCheck", + type: "boolean", + category: Diagnostics.Advanced_Options, + description: Diagnostics.Deprecated_Use_skipLibCheck_instead_Skip_type_checking_of_default_library_declaration_files + }, + { + name: "charset", + type: "string", + category: Diagnostics.Advanced_Options, + description: Diagnostics.The_character_set_of_the_input_files + }, + { + name: "emitBOM", + type: "boolean", + affectsEmit: true, + category: Diagnostics.Advanced_Options, + description: Diagnostics.Emit_a_UTF_8_Byte_Order_Mark_BOM_in_the_beginning_of_output_files + }, + { + name: "newLine", + type: createMapFromTemplate({ + crlf: NewLineKind.CarriageReturnLineFeed, + lf: NewLineKind.LineFeed + }), + affectsEmit: true, + paramType: Diagnostics.NEWLINE, + category: Diagnostics.Advanced_Options, + description: Diagnostics.Specify_the_end_of_line_sequence_to_be_used_when_emitting_files_Colon_CRLF_dos_or_LF_unix, + }, + { + name: "noErrorTruncation", + type: "boolean", + affectsSemanticDiagnostics: true, + category: Diagnostics.Advanced_Options, + description: Diagnostics.Do_not_truncate_error_messages + }, + { + name: "noLib", + type: "boolean", + affectsModuleResolution: true, + category: Diagnostics.Advanced_Options, + description: Diagnostics.Do_not_include_the_default_library_file_lib_d_ts, + // We are not returning a sourceFile for lib file when asked by the program, + // so pass --noLib to avoid reporting a file not found error. + transpileOptionValue: true + }, + { + name: "noResolve", + type: "boolean", + affectsModuleResolution: true, + category: Diagnostics.Advanced_Options, + description: Diagnostics.Do_not_add_triple_slash_references_or_imported_modules_to_the_list_of_compiled_files, + // We are not doing a full typecheck, we are not resolving the whole context, + // so pass --noResolve to avoid reporting missing file errors. + transpileOptionValue: true + }, + { + name: "stripInternal", + type: "boolean", + affectsEmit: true, + category: Diagnostics.Advanced_Options, + description: Diagnostics.Do_not_emit_declarations_for_code_that_has_an_internal_annotation, + }, + { + name: "disableSizeLimit", + type: "boolean", + affectsSourceFile: true, + category: Diagnostics.Advanced_Options, + description: Diagnostics.Disable_size_limitations_on_JavaScript_projects + }, + { + name: "disableSourceOfProjectReferenceRedirect", + type: "boolean", + isTSConfigOnly: true, + category: Diagnostics.Advanced_Options, + description: Diagnostics.Disable_use_of_source_files_instead_of_declaration_files_from_referenced_projects + }, + { + name: "disableSolutionSearching", + type: "boolean", + isTSConfigOnly: true, + category: Diagnostics.Advanced_Options, + description: Diagnostics.Disable_solution_searching_for_this_project + }, + { + name: "noImplicitUseStrict", + type: "boolean", + affectsSemanticDiagnostics: true, + category: Diagnostics.Advanced_Options, + description: Diagnostics.Do_not_emit_use_strict_directives_in_module_output + }, + { + name: "noEmitHelpers", + type: "boolean", + affectsEmit: true, + category: Diagnostics.Advanced_Options, + description: Diagnostics.Do_not_generate_custom_helper_functions_like_extends_in_compiled_output + }, + { + name: "noEmitOnError", + type: "boolean", + affectsEmit: true, + category: Diagnostics.Advanced_Options, + description: Diagnostics.Do_not_emit_outputs_if_any_errors_were_reported, + transpileOptionValue: undefined + }, + { + name: "preserveConstEnums", + type: "boolean", + affectsEmit: true, + category: Diagnostics.Advanced_Options, + description: Diagnostics.Do_not_erase_const_enum_declarations_in_generated_code + }, + { + name: "declarationDir", + type: "string", + affectsEmit: true, + isFilePath: true, + paramType: Diagnostics.DIRECTORY, + category: Diagnostics.Advanced_Options, + description: Diagnostics.Output_directory_for_generated_declaration_files, + transpileOptionValue: undefined + }, + { + name: "skipLibCheck", + type: "boolean", + category: Diagnostics.Advanced_Options, + description: Diagnostics.Skip_type_checking_of_declaration_files, + }, + { + name: "allowUnusedLabels", + type: "boolean", + affectsBindDiagnostics: true, + affectsSemanticDiagnostics: true, + category: Diagnostics.Advanced_Options, + description: Diagnostics.Do_not_report_errors_on_unused_labels + }, + { + name: "allowUnreachableCode", + type: "boolean", + affectsBindDiagnostics: true, + affectsSemanticDiagnostics: true, + category: Diagnostics.Advanced_Options, + description: Diagnostics.Do_not_report_errors_on_unreachable_code + }, + { + name: "suppressExcessPropertyErrors", + type: "boolean", + affectsSemanticDiagnostics: true, + category: Diagnostics.Advanced_Options, + description: Diagnostics.Suppress_excess_property_checks_for_object_literals, + }, + { + name: "suppressImplicitAnyIndexErrors", + type: "boolean", + affectsSemanticDiagnostics: true, + category: Diagnostics.Advanced_Options, + description: Diagnostics.Suppress_noImplicitAny_errors_for_indexing_objects_lacking_index_signatures, + }, + { + name: "forceConsistentCasingInFileNames", + type: "boolean", + affectsModuleResolution: true, + category: Diagnostics.Advanced_Options, + description: Diagnostics.Disallow_inconsistently_cased_references_to_the_same_file + }, + { + name: "maxNodeModuleJsDepth", + type: "number", + affectsModuleResolution: true, + category: Diagnostics.Advanced_Options, + description: Diagnostics.The_maximum_dependency_depth_to_search_under_node_modules_and_load_JavaScript_files + }, + { + name: "noStrictGenericChecks", + type: "boolean", + affectsSemanticDiagnostics: true, + category: Diagnostics.Advanced_Options, + description: Diagnostics.Disable_strict_checking_of_generic_signatures_in_function_types, + }, + { + name: "useDefineForClassFields", + type: "boolean", + affectsSemanticDiagnostics: true, + affectsEmit: true, + category: Diagnostics.Advanced_Options, + description: Diagnostics.Emit_class_fields_with_Define_instead_of_Set, + }, + { + name: "keyofStringsOnly", + type: "boolean", + category: Diagnostics.Advanced_Options, + description: Diagnostics.Resolve_keyof_to_string_valued_property_names_only_no_numbers_or_symbols, + }, + { + // A list of plugins to load in the language service + name: "plugins", + type: "list", + isTSConfigOnly: true, + element: { + name: "plugin", + type: "object" + }, + description: Diagnostics.List_of_language_service_plugins + }, +]; +/* @internal */ +export const semanticDiagnosticsOptionDeclarations: readonly CommandLineOption[] = optionDeclarations.filter(option => !!option.affectsSemanticDiagnostics); +/* @internal */ +export const affectsEmitOptionDeclarations: readonly CommandLineOption[] = optionDeclarations.filter(option => !!option.affectsEmit); +/* @internal */ +export const moduleResolutionOptionDeclarations: readonly CommandLineOption[] = optionDeclarations.filter(option => !!option.affectsModuleResolution); +/* @internal */ +export const sourceFileAffectingCompilerOptions: readonly CommandLineOption[] = optionDeclarations.filter(option => !!option.affectsSourceFile || !!option.affectsModuleResolution || !!option.affectsBindDiagnostics); +/* @internal */ +export const transpileOptionValueCompilerOptions: readonly CommandLineOption[] = optionDeclarations.filter(option => hasProperty(option, "transpileOptionValue")); +/* @internal */ +export const buildOpts: CommandLineOption[] = [ + ...commonOptionsWithBuild, + { + name: "verbose", + shortName: "v", + category: Diagnostics.Command_line_Options, + description: Diagnostics.Enable_verbose_logging, + type: "boolean" + }, + { + name: "dry", + shortName: "d", + category: Diagnostics.Command_line_Options, + description: Diagnostics.Show_what_would_be_built_or_deleted_if_specified_with_clean, + type: "boolean" + }, + { + name: "force", + shortName: "f", + category: Diagnostics.Command_line_Options, + description: Diagnostics.Build_all_projects_including_those_that_appear_to_be_up_to_date, + type: "boolean" + }, + { + name: "clean", + category: Diagnostics.Command_line_Options, + description: Diagnostics.Delete_the_outputs_of_all_projects, + type: "boolean" + } +]; +/* @internal */ +export const typeAcquisitionDeclarations: CommandLineOption[] = [ + { + /* @deprecated typingOptions.enableAutoDiscovery + * Use typeAcquisition.enable instead. + */ + name: "enableAutoDiscovery", + type: "boolean", + }, + { + name: "enable", + type: "boolean", + }, + { + name: "include", + type: "list", + element: { name: "include", - type: "list", - element: { - name: "include", - type: "string" - } - }, - { - name: "exclude", - type: "list", - element: { - name: "exclude", - type: "string" - } - } - ]; - - /* @internal */ - export interface OptionsNameMap { - optionsNameMap: Map; - shortOptionNames: Map; - } - - /*@internal*/ - export function createOptionNameMap(optionDeclarations: readonly CommandLineOption[]): OptionsNameMap { - const optionsNameMap = createMap(); - const shortOptionNames = createMap(); - forEach(optionDeclarations, option => { - optionsNameMap.set(option.name.toLowerCase(), option); - if (option.shortName) { - shortOptionNames.set(option.shortName, option.name); - } - }); - - return { optionsNameMap, shortOptionNames }; - } - - let optionsNameMapCache: OptionsNameMap; - - /* @internal */ - export function getOptionsNameMap(): OptionsNameMap { - return optionsNameMapCache || (optionsNameMapCache = createOptionNameMap(optionDeclarations)); - } - - /* @internal */ - export const defaultInitCompilerOptions: CompilerOptions = { - module: ModuleKind.CommonJS, - target: ScriptTarget.ES5, - strict: true, - esModuleInterop: true, - forceConsistentCasingInFileNames: true - }; - - /* @internal */ - export function convertEnableAutoDiscoveryToEnable(typeAcquisition: TypeAcquisition): TypeAcquisition { - // Convert deprecated typingOptions.enableAutoDiscovery to typeAcquisition.enable - if (typeAcquisition && typeAcquisition.enableAutoDiscovery !== undefined && typeAcquisition.enable === undefined) { - return { - enable: typeAcquisition.enableAutoDiscovery, - include: typeAcquisition.include || [], - exclude: typeAcquisition.exclude || [] - }; - } - return typeAcquisition; - } - - /* @internal */ - export function createCompilerDiagnosticForInvalidCustomType(opt: CommandLineOptionOfCustomType): Diagnostic { - return createDiagnosticForInvalidCustomType(opt, createCompilerDiagnostic); - } - - function createDiagnosticForInvalidCustomType(opt: CommandLineOptionOfCustomType, createDiagnostic: (message: DiagnosticMessage, arg0: string, arg1: string) => Diagnostic): Diagnostic { - const namesOfType = arrayFrom(opt.type.keys()).map(key => `'${key}'`).join(", "); - return createDiagnostic(Diagnostics.Argument_for_0_option_must_be_Colon_1, `--${opt.name}`, namesOfType); - } - - /* @internal */ - export function parseCustomTypeOption(opt: CommandLineOptionOfCustomType, value: string, errors: Push) { - return convertJsonOptionOfCustomType(opt, trimString(value || ""), errors); - } - - /* @internal */ - export function parseListTypeOption(opt: CommandLineOptionOfListType, value = "", errors: Push): (string | number)[] | undefined { - value = trimString(value); - if (startsWith(value, "-")) { - return undefined; - } - if (value === "") { - return []; + type: "string" } - const values = value.split(","); - switch (opt.element.type) { - case "number": - return map(values, parseInt); - case "string": - return map(values, v => v || ""); - default: - return mapDefined(values, v => parseCustomTypeOption(opt.element, v, errors)); + }, + { + name: "exclude", + type: "list", + element: { + name: "exclude", + type: "string" } } - - /*@internal*/ - export interface OptionsBase { - [option: string]: CompilerOptionsValue | TsConfigSourceFile | undefined; - } - - /*@internal*/ - export interface ParseCommandLineWorkerDiagnostics extends DidYouMeanOptionsDiagnostics { - getOptionsNameMap: () => OptionsNameMap; - optionTypeMismatchDiagnostic: DiagnosticMessage; - } - - function getOptionName(option: CommandLineOption) { - return option.name; - } - - function createUnknownOptionError( - unknownOption: string, - diagnostics: DidYouMeanOptionsDiagnostics, - createDiagnostics: (message: DiagnosticMessage, arg0: string, arg1?: string) => Diagnostic, - unknownOptionErrorText?: string - ) { - const possibleOption = getSpellingSuggestion(unknownOption, diagnostics.optionDeclarations, getOptionName); - return possibleOption ? - createDiagnostics(diagnostics.unknownDidYouMeanDiagnostic, unknownOptionErrorText || unknownOption, possibleOption.name) : - createDiagnostics(diagnostics.unknownOptionDiagnostic, unknownOptionErrorText || unknownOption); - } - - /*@internal*/ - export function parseCommandLineWorker( - diagnostics: ParseCommandLineWorkerDiagnostics, - commandLine: readonly string[], - readFile?: (path: string) => string | undefined) { - const options = {} as OptionsBase; - let watchOptions: WatchOptions | undefined; - const fileNames: string[] = []; - const errors: Diagnostic[] = []; - - parseStrings(commandLine); +]; +/* @internal */ +export interface OptionsNameMap { + optionsNameMap: ts.Map; + shortOptionNames: ts.Map; +} +/*@internal*/ +export function createOptionNameMap(optionDeclarations: readonly CommandLineOption[]): OptionsNameMap { + const optionsNameMap = createMap(); + const shortOptionNames = createMap(); + forEach(optionDeclarations, option => { + optionsNameMap.set(option.name.toLowerCase(), option); + if (option.shortName) { + shortOptionNames.set(option.shortName, option.name); + } + }); + return { optionsNameMap, shortOptionNames }; +} +let optionsNameMapCache: OptionsNameMap; +/* @internal */ +export function getOptionsNameMap(): OptionsNameMap { + return optionsNameMapCache || (optionsNameMapCache = createOptionNameMap(optionDeclarations)); +} +/* @internal */ +export const defaultInitCompilerOptions: CompilerOptions = { + module: ModuleKind.CommonJS, + target: ScriptTarget.ES5, + strict: true, + esModuleInterop: true, + forceConsistentCasingInFileNames: true +}; +/* @internal */ +export function convertEnableAutoDiscoveryToEnable(typeAcquisition: TypeAcquisition): TypeAcquisition { + // Convert deprecated typingOptions.enableAutoDiscovery to typeAcquisition.enable + if (typeAcquisition && typeAcquisition.enableAutoDiscovery !== undefined && typeAcquisition.enable === undefined) { return { - options, - watchOptions, - fileNames, - errors + enable: typeAcquisition.enableAutoDiscovery, + include: typeAcquisition.include || [], + exclude: typeAcquisition.exclude || [] }; - - function parseStrings(args: readonly string[]) { - let i = 0; - while (i < args.length) { - const s = args[i]; - i++; - if (s.charCodeAt(0) === CharacterCodes.at) { - parseResponseFile(s.slice(1)); + } + return typeAcquisition; +} +/* @internal */ +export function createCompilerDiagnosticForInvalidCustomType(opt: CommandLineOptionOfCustomType): Diagnostic { + return createDiagnosticForInvalidCustomType(opt, createCompilerDiagnostic); +} +function createDiagnosticForInvalidCustomType(opt: CommandLineOptionOfCustomType, createDiagnostic: (message: DiagnosticMessage, arg0: string, arg1: string) => Diagnostic): Diagnostic { + const namesOfType = arrayFrom(opt.type.keys()).map(key => `'${key}'`).join(", "); + return createDiagnostic(Diagnostics.Argument_for_0_option_must_be_Colon_1, `--${opt.name}`, namesOfType); +} +/* @internal */ +export function parseCustomTypeOption(opt: CommandLineOptionOfCustomType, value: string, errors: Push) { + return convertJsonOptionOfCustomType(opt, trimString(value || ""), errors); +} +/* @internal */ +export function parseListTypeOption(opt: CommandLineOptionOfListType, value = "", errors: Push): (string | number)[] | undefined { + value = trimString(value); + if (startsWith(value, "-")) { + return undefined; + } + if (value === "") { + return []; + } + const values = value.split(","); + switch (opt.element.type) { + case "number": + return map(values, parseInt); + case "string": + return map(values, v => v || ""); + default: + return mapDefined(values, v => parseCustomTypeOption((opt.element), v, errors)); + } +} +/*@internal*/ +export interface OptionsBase { + [option: string]: CompilerOptionsValue | TsConfigSourceFile | undefined; +} +/*@internal*/ +export interface ParseCommandLineWorkerDiagnostics extends DidYouMeanOptionsDiagnostics { + getOptionsNameMap: () => OptionsNameMap; + optionTypeMismatchDiagnostic: DiagnosticMessage; +} +function getOptionName(option: CommandLineOption) { + return option.name; +} +function createUnknownOptionError(unknownOption: string, diagnostics: DidYouMeanOptionsDiagnostics, createDiagnostics: (message: DiagnosticMessage, arg0: string, arg1?: string) => Diagnostic, unknownOptionErrorText?: string) { + const possibleOption = getSpellingSuggestion(unknownOption, diagnostics.optionDeclarations, getOptionName); + return possibleOption ? + createDiagnostics(diagnostics.unknownDidYouMeanDiagnostic, unknownOptionErrorText || unknownOption, possibleOption.name) : + createDiagnostics(diagnostics.unknownOptionDiagnostic, unknownOptionErrorText || unknownOption); +} +/*@internal*/ +export function parseCommandLineWorker(diagnostics: ParseCommandLineWorkerDiagnostics, commandLine: readonly string[], readFile?: (path: string) => string | undefined) { + const options = {} as OptionsBase; + let watchOptions: WatchOptions | undefined; + const fileNames: string[] = []; + const errors: Diagnostic[] = []; + parseStrings(commandLine); + return { + options, + watchOptions, + fileNames, + errors + }; + function parseStrings(args: readonly string[]) { + let i = 0; + while (i < args.length) { + const s = args[i]; + i++; + if (s.charCodeAt(0) === CharacterCodes.at) { + parseResponseFile(s.slice(1)); + } + else if (s.charCodeAt(0) === CharacterCodes.minus) { + const inputOptionName = s.slice(s.charCodeAt(1) === CharacterCodes.minus ? 2 : 1); + const opt = getOptionDeclarationFromName(diagnostics.getOptionsNameMap, inputOptionName, /*allowShort*/ true); + if (opt) { + i = parseOptionValue(args, i, diagnostics, opt, options, errors); } - else if (s.charCodeAt(0) === CharacterCodes.minus) { - const inputOptionName = s.slice(s.charCodeAt(1) === CharacterCodes.minus ? 2 : 1); - const opt = getOptionDeclarationFromName(diagnostics.getOptionsNameMap, inputOptionName, /*allowShort*/ true); - if (opt) { - i = parseOptionValue(args, i, diagnostics, opt, options, errors); + else { + const watchOpt = getOptionDeclarationFromName(watchOptionsDidYouMeanDiagnostics.getOptionsNameMap, inputOptionName, /*allowShort*/ true); + if (watchOpt) { + i = parseOptionValue(args, i, watchOptionsDidYouMeanDiagnostics, watchOpt, watchOptions || (watchOptions = {}), errors); } else { - const watchOpt = getOptionDeclarationFromName(watchOptionsDidYouMeanDiagnostics.getOptionsNameMap, inputOptionName, /*allowShort*/ true); - if (watchOpt) { - i = parseOptionValue(args, i, watchOptionsDidYouMeanDiagnostics, watchOpt, watchOptions || (watchOptions = {}), errors); - } - else { - errors.push(createUnknownOptionError(inputOptionName, diagnostics, createCompilerDiagnostic, s)); - } + errors.push(createUnknownOptionError(inputOptionName, diagnostics, createCompilerDiagnostic, s)); } } - else { - fileNames.push(s); - } } - } - - function parseResponseFile(fileName: string) { - const text = readFile ? readFile(fileName) : sys.readFile(fileName); - - if (!text) { - errors.push(createCompilerDiagnostic(Diagnostics.File_0_not_found, fileName)); - return; + else { + fileNames.push(s); } - - const args: string[] = []; - let pos = 0; - while (true) { - while (pos < text.length && text.charCodeAt(pos) <= CharacterCodes.space) pos++; - if (pos >= text.length) break; - const start = pos; - if (text.charCodeAt(start) === CharacterCodes.doubleQuote) { + } + } + function parseResponseFile(fileName: string) { + const text = readFile ? readFile(fileName) : sys.readFile(fileName); + if (!text) { + errors.push(createCompilerDiagnostic(Diagnostics.File_0_not_found, fileName)); + return; + } + const args: string[] = []; + let pos = 0; + while (true) { + while (pos < text.length && text.charCodeAt(pos) <= CharacterCodes.space) + pos++; + if (pos >= text.length) + break; + const start = pos; + if (text.charCodeAt(start) === CharacterCodes.doubleQuote) { + pos++; + while (pos < text.length && text.charCodeAt(pos) !== CharacterCodes.doubleQuote) + pos++; + if (pos < text.length) { + args.push(text.substring(start + 1, pos)); pos++; - while (pos < text.length && text.charCodeAt(pos) !== CharacterCodes.doubleQuote) pos++; - if (pos < text.length) { - args.push(text.substring(start + 1, pos)); - pos++; - } - else { - errors.push(createCompilerDiagnostic(Diagnostics.Unterminated_quoted_string_in_response_file_0, fileName)); - } } else { - while (text.charCodeAt(pos) > CharacterCodes.space) pos++; - args.push(text.substring(start, pos)); + errors.push(createCompilerDiagnostic(Diagnostics.Unterminated_quoted_string_in_response_file_0, fileName)); } } - parseStrings(args); + else { + while (text.charCodeAt(pos) > CharacterCodes.space) + pos++; + args.push(text.substring(start, pos)); + } } + parseStrings(args); } - - function parseOptionValue( - args: readonly string[], - i: number, - diagnostics: ParseCommandLineWorkerDiagnostics, - opt: CommandLineOption, - options: OptionsBase, - errors: Diagnostic[] - ) { - if (opt.isTSConfigOnly) { - const optValue = args[i]; - if (optValue === "null") { - options[opt.name] = undefined; +} +function parseOptionValue(args: readonly string[], i: number, diagnostics: ParseCommandLineWorkerDiagnostics, opt: CommandLineOption, options: OptionsBase, errors: Diagnostic[]) { + if (opt.isTSConfigOnly) { + const optValue = args[i]; + if (optValue === "null") { + options[opt.name] = undefined; + i++; + } + else if (opt.type === "boolean") { + if (optValue === "false") { + options[opt.name] = false; i++; } - else if (opt.type === "boolean") { - if (optValue === "false") { - options[opt.name] = false; - i++; - } - else { - if (optValue === "true") i++; - errors.push(createCompilerDiagnostic(Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_false_or_null_on_command_line, opt.name)); - } - } else { - errors.push(createCompilerDiagnostic(Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_null_on_command_line, opt.name)); - if (optValue && !startsWith(optValue, "-")) i++; + if (optValue === "true") + i++; + errors.push(createCompilerDiagnostic(Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_false_or_null_on_command_line, opt.name)); } } else { - // Check to see if no argument was provided (e.g. "--locale" is the last command-line argument). - if (!args[i] && opt.type !== "boolean") { - errors.push(createCompilerDiagnostic(diagnostics.optionTypeMismatchDiagnostic, opt.name, getCompilerOptionValueTypeString(opt))); - } - - if (args[i] !== "null") { - switch (opt.type) { - case "number": - options[opt.name] = parseInt(args[i]); - i++; - break; - case "boolean": - // boolean flag has optional value true, false, others - const optValue = args[i]; - options[opt.name] = optValue !== "false"; - // consume next argument as boolean flag value - if (optValue === "false" || optValue === "true") { - i++; - } - break; - case "string": - options[opt.name] = args[i] || ""; - i++; - break; - case "list": - const result = parseListTypeOption(opt, args[i], errors); - options[opt.name] = result || []; - if (result) { - i++; - } - break; - // If not a primitive, the possible types are specified in what is effectively a map of options. - default: - options[opt.name] = parseCustomTypeOption(opt, args[i], errors); - i++; - break; - } - } - else { - options[opt.name] = undefined; + errors.push(createCompilerDiagnostic(Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_null_on_command_line, opt.name)); + if (optValue && !startsWith(optValue, "-")) i++; - } - } - return i; - } - - /*@internal*/ - export const compilerOptionsDidYouMeanDiagnostics: ParseCommandLineWorkerDiagnostics = { - getOptionsNameMap, - optionDeclarations, - unknownOptionDiagnostic: Diagnostics.Unknown_compiler_option_0, - unknownDidYouMeanDiagnostic: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1, - optionTypeMismatchDiagnostic: Diagnostics.Compiler_option_0_expects_an_argument - }; - export function parseCommandLine(commandLine: readonly string[], readFile?: (path: string) => string | undefined): ParsedCommandLine { - return parseCommandLineWorker(compilerOptionsDidYouMeanDiagnostics, commandLine, readFile); - } - - /** @internal */ - export function getOptionFromName(optionName: string, allowShort?: boolean): CommandLineOption | undefined { - return getOptionDeclarationFromName(getOptionsNameMap, optionName, allowShort); - } - - function getOptionDeclarationFromName(getOptionNameMap: () => OptionsNameMap, optionName: string, allowShort = false): CommandLineOption | undefined { - optionName = optionName.toLowerCase(); - const { optionsNameMap, shortOptionNames } = getOptionNameMap(); - // Try to translate short option names to their full equivalents. - if (allowShort) { - const short = shortOptionNames.get(optionName); - if (short !== undefined) { - optionName = short; - } - } - return optionsNameMap.get(optionName); - } - - /*@internal*/ - export interface ParsedBuildCommand { - buildOptions: BuildOptions; - watchOptions: WatchOptions | undefined; - projects: string[]; - errors: Diagnostic[]; - } - - let buildOptionsNameMapCache: OptionsNameMap; - function getBuildOptionsNameMap(): OptionsNameMap { - return buildOptionsNameMapCache || (buildOptionsNameMapCache = createOptionNameMap(buildOpts)); - } - - const buildOptionsDidYouMeanDiagnostics: ParseCommandLineWorkerDiagnostics = { - getOptionsNameMap: getBuildOptionsNameMap, - optionDeclarations: buildOpts, - unknownOptionDiagnostic: Diagnostics.Unknown_build_option_0, - unknownDidYouMeanDiagnostic: Diagnostics.Unknown_build_option_0_Did_you_mean_1, - optionTypeMismatchDiagnostic: Diagnostics.Build_option_0_requires_a_value_of_type_1 - }; - - /*@internal*/ - export function parseBuildCommand(args: readonly string[]): ParsedBuildCommand { - const { options, watchOptions, fileNames: projects, errors } = parseCommandLineWorker( - buildOptionsDidYouMeanDiagnostics, - args - ); - const buildOptions = options as BuildOptions; - - if (projects.length === 0) { - // tsc -b invoked with no extra arguments; act as if invoked with "tsc -b ." - projects.push("."); } - - // Nonsensical combinations - if (buildOptions.clean && buildOptions.force) { - errors.push(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "force")); - } - if (buildOptions.clean && buildOptions.verbose) { - errors.push(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "verbose")); - } - if (buildOptions.clean && buildOptions.watch) { - errors.push(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "watch")); - } - if (buildOptions.watch && buildOptions.dry) { - errors.push(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "watch", "dry")); - } - - return { buildOptions, watchOptions, projects, errors }; - } - - /* @internal */ - export function getDiagnosticText(_message: DiagnosticMessage, ..._args: any[]): string { - const diagnostic = createCompilerDiagnostic.apply(undefined, arguments); - return diagnostic.messageText; - } - - export type DiagnosticReporter = (diagnostic: Diagnostic) => void; - /** - * Reports config file diagnostics - */ - export interface ConfigFileDiagnosticsReporter { - /** - * Reports unrecoverable error when parsing config file - */ - onUnRecoverableConfigFileDiagnostic: DiagnosticReporter; - } - - /** - * Interface extending ParseConfigHost to support ParseConfigFile that reads config file and reports errors - */ - export interface ParseConfigFileHost extends ParseConfigHost, ConfigFileDiagnosticsReporter { - getCurrentDirectory(): string; } - - /** - * Reads the config file, reports errors if any and exits if the config file cannot be found - */ - export function getParsedCommandLineOfConfigFile( - configFileName: string, - optionsToExtend: CompilerOptions, - host: ParseConfigFileHost, - extendedConfigCache?: Map, - watchOptionsToExtend?: WatchOptions - ): ParsedCommandLine | undefined { - let configFileText: string | undefined; - try { - configFileText = host.readFile(configFileName); + else { + // Check to see if no argument was provided (e.g. "--locale" is the last command-line argument). + if (!args[i] && opt.type !== "boolean") { + errors.push(createCompilerDiagnostic(diagnostics.optionTypeMismatchDiagnostic, opt.name, getCompilerOptionValueTypeString(opt))); } - catch (e) { - const error = createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, configFileName, e.message); - host.onUnRecoverableConfigFileDiagnostic(error); - return undefined; + if (args[i] !== "null") { + switch (opt.type) { + case "number": + options[opt.name] = parseInt(args[i]); + i++; + break; + case "boolean": + // boolean flag has optional value true, false, others + const optValue = args[i]; + options[opt.name] = optValue !== "false"; + // consume next argument as boolean flag value + if (optValue === "false" || optValue === "true") { + i++; + } + break; + case "string": + options[opt.name] = args[i] || ""; + i++; + break; + case "list": + const result = parseListTypeOption(opt, args[i], errors); + options[opt.name] = result || []; + if (result) { + i++; + } + break; + // If not a primitive, the possible types are specified in what is effectively a map of options. + default: + options[opt.name] = parseCustomTypeOption((opt), args[i], errors); + i++; + break; + } } - if (!configFileText) { - const error = createCompilerDiagnostic(Diagnostics.File_0_not_found, configFileName); - host.onUnRecoverableConfigFileDiagnostic(error); - return undefined; + else { + options[opt.name] = undefined; + i++; } - - const result = parseJsonText(configFileName, configFileText); - const cwd = host.getCurrentDirectory(); - result.path = toPath(configFileName, cwd, createGetCanonicalFileName(host.useCaseSensitiveFileNames)); - result.resolvedPath = result.path; - result.originalFileName = result.fileName; - return parseJsonSourceFileConfigFileContent( - result, - host, - getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd), - optionsToExtend, - getNormalizedAbsolutePath(configFileName, cwd), - /*resolutionStack*/ undefined, - /*extraFileExtension*/ undefined, - extendedConfigCache, - watchOptionsToExtend - ); - } - - /** - * Read tsconfig.json file - * @param fileName The path to the config file - */ - export function readConfigFile(fileName: string, readFile: (path: string) => string | undefined): { config?: any; error?: Diagnostic } { - const textOrDiagnostic = tryReadFile(fileName, readFile); - return isString(textOrDiagnostic) ? parseConfigFileTextToJson(fileName, textOrDiagnostic) : { config: {}, error: textOrDiagnostic }; } - - /** - * Parse the text of the tsconfig.json file - * @param fileName The path to the config file - * @param jsonText The text of the config file - */ - export function parseConfigFileTextToJson(fileName: string, jsonText: string): { config?: any; error?: Diagnostic } { - const jsonSourceFile = parseJsonText(fileName, jsonText); - return { - config: convertToObject(jsonSourceFile, jsonSourceFile.parseDiagnostics), - error: jsonSourceFile.parseDiagnostics.length ? jsonSourceFile.parseDiagnostics[0] : undefined - }; + return i; +} +/*@internal*/ +export const compilerOptionsDidYouMeanDiagnostics: ParseCommandLineWorkerDiagnostics = { + getOptionsNameMap, + optionDeclarations, + unknownOptionDiagnostic: Diagnostics.Unknown_compiler_option_0, + unknownDidYouMeanDiagnostic: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1, + optionTypeMismatchDiagnostic: Diagnostics.Compiler_option_0_expects_an_argument +}; +export function parseCommandLine(commandLine: readonly string[], readFile?: (path: string) => string | undefined): ParsedCommandLine { + return parseCommandLineWorker(compilerOptionsDidYouMeanDiagnostics, commandLine, readFile); +} +/** @internal */ +export function getOptionFromName(optionName: string, allowShort?: boolean): CommandLineOption | undefined { + return getOptionDeclarationFromName(getOptionsNameMap, optionName, allowShort); +} +function getOptionDeclarationFromName(getOptionNameMap: () => OptionsNameMap, optionName: string, allowShort = false): CommandLineOption | undefined { + optionName = optionName.toLowerCase(); + const { optionsNameMap, shortOptionNames } = getOptionNameMap(); + // Try to translate short option names to their full equivalents. + if (allowShort) { + const short = shortOptionNames.get(optionName); + if (short !== undefined) { + optionName = short; + } } - + return optionsNameMap.get(optionName); +} +/*@internal*/ +export interface ParsedBuildCommand { + buildOptions: BuildOptions; + watchOptions: WatchOptions | undefined; + projects: string[]; + errors: Diagnostic[]; +} +let buildOptionsNameMapCache: OptionsNameMap; +function getBuildOptionsNameMap(): OptionsNameMap { + return buildOptionsNameMapCache || (buildOptionsNameMapCache = createOptionNameMap(buildOpts)); +} +const buildOptionsDidYouMeanDiagnostics: ParseCommandLineWorkerDiagnostics = { + getOptionsNameMap: getBuildOptionsNameMap, + optionDeclarations: buildOpts, + unknownOptionDiagnostic: Diagnostics.Unknown_build_option_0, + unknownDidYouMeanDiagnostic: Diagnostics.Unknown_build_option_0_Did_you_mean_1, + optionTypeMismatchDiagnostic: Diagnostics.Build_option_0_requires_a_value_of_type_1 +}; +/*@internal*/ +export function parseBuildCommand(args: readonly string[]): ParsedBuildCommand { + const { options, watchOptions, fileNames: projects, errors } = parseCommandLineWorker(buildOptionsDidYouMeanDiagnostics, args); + const buildOptions = (options as BuildOptions); + if (projects.length === 0) { + // tsc -b invoked with no extra arguments; act as if invoked with "tsc -b ." + projects.push("."); + } + // Nonsensical combinations + if (buildOptions.clean && buildOptions.force) { + errors.push(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "force")); + } + if (buildOptions.clean && buildOptions.verbose) { + errors.push(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "verbose")); + } + if (buildOptions.clean && buildOptions.watch) { + errors.push(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "watch")); + } + if (buildOptions.watch && buildOptions.dry) { + errors.push(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "watch", "dry")); + } + return { buildOptions, watchOptions, projects, errors }; +} +/* @internal */ +export function getDiagnosticText(_message: DiagnosticMessage, ..._args: any[]): string { + const diagnostic = createCompilerDiagnostic.apply(undefined, arguments); + return diagnostic.messageText; +} +export type DiagnosticReporter = (diagnostic: Diagnostic) => void; +/** + * Reports config file diagnostics + */ +export interface ConfigFileDiagnosticsReporter { /** - * Read tsconfig.json file - * @param fileName The path to the config file + * Reports unrecoverable error when parsing config file */ - export function readJsonConfigFile(fileName: string, readFile: (path: string) => string | undefined): TsConfigSourceFile { - const textOrDiagnostic = tryReadFile(fileName, readFile); - return isString(textOrDiagnostic) ? parseJsonText(fileName, textOrDiagnostic) : { parseDiagnostics: [textOrDiagnostic] }; - } - - function tryReadFile(fileName: string, readFile: (path: string) => string | undefined): string | Diagnostic { - let text: string | undefined; - try { - text = readFile(fileName); - } - catch (e) { - return createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, fileName, e.message); - } - return text === undefined ? createCompilerDiagnostic(Diagnostics.The_specified_path_does_not_exist_Colon_0, fileName) : text; + onUnRecoverableConfigFileDiagnostic: DiagnosticReporter; +} +/** + * Interface extending ParseConfigHost to support ParseConfigFile that reads config file and reports errors + */ +export interface ParseConfigFileHost extends ParseConfigHost, ConfigFileDiagnosticsReporter { + getCurrentDirectory(): string; +} +/** + * Reads the config file, reports errors if any and exits if the config file cannot be found + */ +export function getParsedCommandLineOfConfigFile(configFileName: string, optionsToExtend: CompilerOptions, host: ParseConfigFileHost, extendedConfigCache?: ts.Map, watchOptionsToExtend?: WatchOptions): ParsedCommandLine | undefined { + let configFileText: string | undefined; + try { + configFileText = host.readFile(configFileName); + } + catch (e) { + const error = createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, configFileName, e.message); + host.onUnRecoverableConfigFileDiagnostic(error); + return undefined; } - - function commandLineOptionsToMap(options: readonly CommandLineOption[]) { - return arrayToMap(options, getOptionName); + if (!configFileText) { + const error = createCompilerDiagnostic(Diagnostics.File_0_not_found, configFileName); + host.onUnRecoverableConfigFileDiagnostic(error); + return undefined; } - - const typeAcquisitionDidYouMeanDiagnostics: DidYouMeanOptionsDiagnostics = { - optionDeclarations: typeAcquisitionDeclarations, - unknownOptionDiagnostic: Diagnostics.Unknown_type_acquisition_option_0, - unknownDidYouMeanDiagnostic: Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1, - }; - - let watchOptionsNameMapCache: OptionsNameMap; - function getWatchOptionsNameMap(): OptionsNameMap { - return watchOptionsNameMapCache || (watchOptionsNameMapCache = createOptionNameMap(optionsForWatch)); - } - const watchOptionsDidYouMeanDiagnostics: ParseCommandLineWorkerDiagnostics = { - getOptionsNameMap: getWatchOptionsNameMap, - optionDeclarations: optionsForWatch, - unknownOptionDiagnostic: Diagnostics.Unknown_watch_option_0, - unknownDidYouMeanDiagnostic: Diagnostics.Unknown_watch_option_0_Did_you_mean_1, - optionTypeMismatchDiagnostic: Diagnostics.Watch_option_0_requires_a_value_of_type_1 + const result = parseJsonText(configFileName, configFileText); + const cwd = host.getCurrentDirectory(); + result.path = toPath(configFileName, cwd, createGetCanonicalFileName(host.useCaseSensitiveFileNames)); + result.resolvedPath = result.path; + result.originalFileName = result.fileName; + return parseJsonSourceFileConfigFileContent(result, host, getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd), optionsToExtend, getNormalizedAbsolutePath(configFileName, cwd), + /*resolutionStack*/ undefined, + /*extraFileExtension*/ undefined, extendedConfigCache, watchOptionsToExtend); +} +/** + * Read tsconfig.json file + * @param fileName The path to the config file + */ +export function readConfigFile(fileName: string, readFile: (path: string) => string | undefined): { + config?: any; + error?: Diagnostic; +} { + const textOrDiagnostic = tryReadFile(fileName, readFile); + return isString(textOrDiagnostic) ? parseConfigFileTextToJson(fileName, textOrDiagnostic) : { config: {}, error: textOrDiagnostic }; +} +/** + * Parse the text of the tsconfig.json file + * @param fileName The path to the config file + * @param jsonText The text of the config file + */ +export function parseConfigFileTextToJson(fileName: string, jsonText: string): { + config?: any; + error?: Diagnostic; +} { + const jsonSourceFile = parseJsonText(fileName, jsonText); + return { + config: convertToObject(jsonSourceFile, jsonSourceFile.parseDiagnostics), + error: jsonSourceFile.parseDiagnostics.length ? jsonSourceFile.parseDiagnostics[0] : undefined }; - - let commandLineCompilerOptionsMapCache: Map; - function getCommandLineCompilerOptionsMap() { - return commandLineCompilerOptionsMapCache || (commandLineCompilerOptionsMapCache = commandLineOptionsToMap(optionDeclarations)); - } - let commandLineWatchOptionsMapCache: Map; - function getCommandLineWatchOptionsMap() { - return commandLineWatchOptionsMapCache || (commandLineWatchOptionsMapCache = commandLineOptionsToMap(optionsForWatch)); - } - let commandLineTypeAcquisitionMapCache: Map; - function getCommandLineTypeAcquisitionMap() { - return commandLineTypeAcquisitionMapCache || (commandLineTypeAcquisitionMapCache = commandLineOptionsToMap(typeAcquisitionDeclarations)); - } - - let _tsconfigRootOptions: TsConfigOnlyOption; - function getTsconfigRootOptionsMap() { - if (_tsconfigRootOptions === undefined) { - _tsconfigRootOptions = { - name: undefined!, // should never be needed since this is root - type: "object", - elementOptions: commandLineOptionsToMap([ - { - name: "compilerOptions", - type: "object", - elementOptions: getCommandLineCompilerOptionsMap(), - extraKeyDiagnostics: compilerOptionsDidYouMeanDiagnostics, - }, - { - name: "watchOptions", - type: "object", - elementOptions: getCommandLineWatchOptionsMap(), - extraKeyDiagnostics: watchOptionsDidYouMeanDiagnostics, - }, - { - name: "typingOptions", - type: "object", - elementOptions: getCommandLineTypeAcquisitionMap(), - extraKeyDiagnostics: typeAcquisitionDidYouMeanDiagnostics, - }, - { - name: "typeAcquisition", - type: "object", - elementOptions: getCommandLineTypeAcquisitionMap(), - extraKeyDiagnostics: typeAcquisitionDidYouMeanDiagnostics - }, - { - name: "extends", - type: "string" - }, - { +} +/** + * Read tsconfig.json file + * @param fileName The path to the config file + */ +export function readJsonConfigFile(fileName: string, readFile: (path: string) => string | undefined): TsConfigSourceFile { + const textOrDiagnostic = tryReadFile(fileName, readFile); + return isString(textOrDiagnostic) ? parseJsonText(fileName, textOrDiagnostic) : { parseDiagnostics: [textOrDiagnostic] }; +} +function tryReadFile(fileName: string, readFile: (path: string) => string | undefined): string | Diagnostic { + let text: string | undefined; + try { + text = readFile(fileName); + } + catch (e) { + return createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, fileName, e.message); + } + return text === undefined ? createCompilerDiagnostic(Diagnostics.The_specified_path_does_not_exist_Colon_0, fileName) : text; +} +function commandLineOptionsToMap(options: readonly CommandLineOption[]) { + return arrayToMap(options, getOptionName); +} +const typeAcquisitionDidYouMeanDiagnostics: DidYouMeanOptionsDiagnostics = { + optionDeclarations: typeAcquisitionDeclarations, + unknownOptionDiagnostic: Diagnostics.Unknown_type_acquisition_option_0, + unknownDidYouMeanDiagnostic: Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1, +}; +let watchOptionsNameMapCache: OptionsNameMap; +function getWatchOptionsNameMap(): OptionsNameMap { + return watchOptionsNameMapCache || (watchOptionsNameMapCache = createOptionNameMap(optionsForWatch)); +} +const watchOptionsDidYouMeanDiagnostics: ParseCommandLineWorkerDiagnostics = { + getOptionsNameMap: getWatchOptionsNameMap, + optionDeclarations: optionsForWatch, + unknownOptionDiagnostic: Diagnostics.Unknown_watch_option_0, + unknownDidYouMeanDiagnostic: Diagnostics.Unknown_watch_option_0_Did_you_mean_1, + optionTypeMismatchDiagnostic: Diagnostics.Watch_option_0_requires_a_value_of_type_1 +}; +let commandLineCompilerOptionsMapCache: ts.Map; +function getCommandLineCompilerOptionsMap() { + return commandLineCompilerOptionsMapCache || (commandLineCompilerOptionsMapCache = commandLineOptionsToMap(optionDeclarations)); +} +let commandLineWatchOptionsMapCache: ts.Map; +function getCommandLineWatchOptionsMap() { + return commandLineWatchOptionsMapCache || (commandLineWatchOptionsMapCache = commandLineOptionsToMap(optionsForWatch)); +} +let commandLineTypeAcquisitionMapCache: ts.Map; +function getCommandLineTypeAcquisitionMap() { + return commandLineTypeAcquisitionMapCache || (commandLineTypeAcquisitionMapCache = commandLineOptionsToMap(typeAcquisitionDeclarations)); +} +let _tsconfigRootOptions: TsConfigOnlyOption; +function getTsconfigRootOptionsMap() { + if (_tsconfigRootOptions === undefined) { + _tsconfigRootOptions = { + name: undefined!, + type: "object", + elementOptions: commandLineOptionsToMap([ + { + name: "compilerOptions", + type: "object", + elementOptions: getCommandLineCompilerOptionsMap(), + extraKeyDiagnostics: compilerOptionsDidYouMeanDiagnostics, + }, + { + name: "watchOptions", + type: "object", + elementOptions: getCommandLineWatchOptionsMap(), + extraKeyDiagnostics: watchOptionsDidYouMeanDiagnostics, + }, + { + name: "typingOptions", + type: "object", + elementOptions: getCommandLineTypeAcquisitionMap(), + extraKeyDiagnostics: typeAcquisitionDidYouMeanDiagnostics, + }, + { + name: "typeAcquisition", + type: "object", + elementOptions: getCommandLineTypeAcquisitionMap(), + extraKeyDiagnostics: typeAcquisitionDidYouMeanDiagnostics + }, + { + name: "extends", + type: "string" + }, + { + name: "references", + type: "list", + element: { name: "references", - type: "list", - element: { - name: "references", - type: "object" - } - }, - { + type: "object" + } + }, + { + name: "files", + type: "list", + element: { name: "files", - type: "list", - element: { - name: "files", - type: "string" - } - }, - { + type: "string" + } + }, + { + name: "include", + type: "list", + element: { name: "include", - type: "list", - element: { - name: "include", - type: "string" - } - }, - { + type: "string" + } + }, + { + name: "exclude", + type: "list", + element: { name: "exclude", - type: "list", - element: { - name: "exclude", - type: "string" - } - }, - compileOnSaveCommandLineOption - ]) - }; - } - return _tsconfigRootOptions; - } - - /*@internal*/ - interface JsonConversionNotifier { - /** - * Notifies parent option object is being set with the optionKey and a valid optionValue - * Currently it notifies only if there is element with type object (parentOption) and - * has element's option declarations map associated with it - * @param parentOption parent option name in which the option and value are being set - * @param option option declaration which is being set with the value - * @param value value of the option - */ - onSetValidOptionKeyValueInParent(parentOption: string, option: CommandLineOption, value: CompilerOptionsValue): void; - /** - * Notify when valid root key value option is being set - * @param key option key - * @param keyNode node corresponding to node in the source file - * @param value computed value of the key - * @param ValueNode node corresponding to value in the source file - */ - onSetValidOptionKeyValueInRoot(key: string, keyNode: PropertyName, value: CompilerOptionsValue, valueNode: Expression): void; - /** - * Notify when unknown root key value option is being set - * @param key option key - * @param keyNode node corresponding to node in the source file - * @param value computed value of the key - * @param ValueNode node corresponding to value in the source file - */ - onSetUnknownOptionKeyValueInRoot(key: string, keyNode: PropertyName, value: CompilerOptionsValue, valueNode: Expression): void; + type: "string" + } + }, + compileOnSaveCommandLineOption + ]) + }; } - + return _tsconfigRootOptions; +} +/*@internal*/ +interface JsonConversionNotifier { /** - * Convert the json syntax tree into the json value + * Notifies parent option object is being set with the optionKey and a valid optionValue + * Currently it notifies only if there is element with type object (parentOption) and + * has element's option declarations map associated with it + * @param parentOption parent option name in which the option and value are being set + * @param option option declaration which is being set with the value + * @param value value of the option */ - export function convertToObject(sourceFile: JsonSourceFile, errors: Push): any { - return convertToObjectWorker(sourceFile, errors, /*returnValue*/ true, /*knownRootOptions*/ undefined, /*jsonConversionNotifier*/ undefined); - } - + onSetValidOptionKeyValueInParent(parentOption: string, option: CommandLineOption, value: CompilerOptionsValue): void; /** - * Convert the json syntax tree into the json value and report errors - * This returns the json value (apart from checking errors) only if returnValue provided is true. - * Otherwise it just checks the errors and returns undefined + * Notify when valid root key value option is being set + * @param key option key + * @param keyNode node corresponding to node in the source file + * @param value computed value of the key + * @param ValueNode node corresponding to value in the source file */ - /*@internal*/ - export function convertToObjectWorker( - sourceFile: JsonSourceFile, - errors: Push, - returnValue: boolean, - knownRootOptions: CommandLineOption | undefined, - jsonConversionNotifier: JsonConversionNotifier | undefined): any { - if (!sourceFile.statements.length) { - return returnValue ? {} : undefined; - } - - return convertPropertyValueToJson(sourceFile.statements[0].expression, knownRootOptions); - - function isRootOptionMap(knownOptions: Map | undefined) { - return knownRootOptions && (knownRootOptions as TsConfigOnlyOption).elementOptions === knownOptions; - } - - function convertObjectLiteralExpressionToJson( - node: ObjectLiteralExpression, - knownOptions: Map | undefined, - extraKeyDiagnostics: DidYouMeanOptionsDiagnostics | undefined, - parentOption: string | undefined - ): any { - const result: any = returnValue ? {} : undefined; - for (const element of node.properties) { - if (element.kind !== SyntaxKind.PropertyAssignment) { - errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element, Diagnostics.Property_assignment_expected)); - continue; - } - - if (element.questionToken) { - errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element.questionToken, Diagnostics.The_0_modifier_can_only_be_used_in_TypeScript_files, "?")); + onSetValidOptionKeyValueInRoot(key: string, keyNode: PropertyName, value: CompilerOptionsValue, valueNode: Expression): void; + /** + * Notify when unknown root key value option is being set + * @param key option key + * @param keyNode node corresponding to node in the source file + * @param value computed value of the key + * @param ValueNode node corresponding to value in the source file + */ + onSetUnknownOptionKeyValueInRoot(key: string, keyNode: PropertyName, value: CompilerOptionsValue, valueNode: Expression): void; +} +/** + * Convert the json syntax tree into the json value + */ +export function convertToObject(sourceFile: JsonSourceFile, errors: Push): any { + return convertToObjectWorker(sourceFile, errors, /*returnValue*/ true, /*knownRootOptions*/ undefined, /*jsonConversionNotifier*/ undefined); +} +/** + * Convert the json syntax tree into the json value and report errors + * This returns the json value (apart from checking errors) only if returnValue provided is true. + * Otherwise it just checks the errors and returns undefined + */ +/*@internal*/ +export function convertToObjectWorker(sourceFile: JsonSourceFile, errors: Push, returnValue: boolean, knownRootOptions: CommandLineOption | undefined, jsonConversionNotifier: JsonConversionNotifier | undefined): any { + if (!sourceFile.statements.length) { + return returnValue ? {} : undefined; + } + return convertPropertyValueToJson(sourceFile.statements[0].expression, knownRootOptions); + function isRootOptionMap(knownOptions: ts.Map | undefined) { + return knownRootOptions && (knownRootOptions as TsConfigOnlyOption).elementOptions === knownOptions; + } + function convertObjectLiteralExpressionToJson(node: ObjectLiteralExpression, knownOptions: ts.Map | undefined, extraKeyDiagnostics: DidYouMeanOptionsDiagnostics | undefined, parentOption: string | undefined): any { + const result: any = returnValue ? {} : undefined; + for (const element of node.properties) { + if (element.kind !== SyntaxKind.PropertyAssignment) { + errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element, Diagnostics.Property_assignment_expected)); + continue; + } + if (element.questionToken) { + errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element.questionToken, Diagnostics.The_0_modifier_can_only_be_used_in_TypeScript_files, "?")); + } + if (!isDoubleQuotedString(element.name)) { + errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element.name, Diagnostics.String_literal_with_double_quotes_expected)); + } + const textOfKey = getTextOfPropertyName(element.name); + const keyText = textOfKey && unescapeLeadingUnderscores(textOfKey); + const option = keyText && knownOptions ? knownOptions.get(keyText) : undefined; + if (keyText && extraKeyDiagnostics && !option) { + if (knownOptions) { + errors.push(createUnknownOptionError(keyText, extraKeyDiagnostics, (message, arg0, arg1) => createDiagnosticForNodeInSourceFile(sourceFile, element.name, message, arg0, arg1))); } - if (!isDoubleQuotedString(element.name)) { - errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element.name, Diagnostics.String_literal_with_double_quotes_expected)); + else { + errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element.name, extraKeyDiagnostics.unknownOptionDiagnostic, keyText)); } - - const textOfKey = getTextOfPropertyName(element.name); - const keyText = textOfKey && unescapeLeadingUnderscores(textOfKey); - const option = keyText && knownOptions ? knownOptions.get(keyText) : undefined; - if (keyText && extraKeyDiagnostics && !option) { - if (knownOptions) { - errors.push(createUnknownOptionError( - keyText, - extraKeyDiagnostics, - (message, arg0, arg1) => createDiagnosticForNodeInSourceFile(sourceFile, element.name, message, arg0, arg1) - )); - } - else { - errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element.name, extraKeyDiagnostics.unknownOptionDiagnostic, keyText)); - } + } + const value = convertPropertyValueToJson(element.initializer, option); + if (typeof keyText !== "undefined") { + if (returnValue) { + result[keyText] = value; } - const value = convertPropertyValueToJson(element.initializer, option); - if (typeof keyText !== "undefined") { - if (returnValue) { - result[keyText] = value; + // Notify key value set, if user asked for it + if (jsonConversionNotifier && + // Current callbacks are only on known parent option or if we are setting values in the root + (parentOption || isRootOptionMap(knownOptions))) { + const isValidOptionValue = isCompilerOptionsValue(option, value); + if (parentOption) { + if (isValidOptionValue) { + // Notify option set in the parent if its a valid option value + jsonConversionNotifier.onSetValidOptionKeyValueInParent(parentOption, option!, value); + } } - // Notify key value set, if user asked for it - if (jsonConversionNotifier && - // Current callbacks are only on known parent option or if we are setting values in the root - (parentOption || isRootOptionMap(knownOptions))) { - const isValidOptionValue = isCompilerOptionsValue(option, value); - if (parentOption) { - if (isValidOptionValue) { - // Notify option set in the parent if its a valid option value - jsonConversionNotifier.onSetValidOptionKeyValueInParent(parentOption, option!, value); - } + else if (isRootOptionMap(knownOptions)) { + if (isValidOptionValue) { + // Notify about the valid root key value being set + jsonConversionNotifier.onSetValidOptionKeyValueInRoot(keyText, element.name, value, element.initializer); } - else if (isRootOptionMap(knownOptions)) { - if (isValidOptionValue) { - // Notify about the valid root key value being set - jsonConversionNotifier.onSetValidOptionKeyValueInRoot(keyText, element.name, value, element.initializer); - } - else if (!option) { - // Notify about the unknown root key value being set - jsonConversionNotifier.onSetUnknownOptionKeyValueInRoot(keyText, element.name, value, element.initializer); - } + else if (!option) { + // Notify about the unknown root key value being set + jsonConversionNotifier.onSetUnknownOptionKeyValueInRoot(keyText, element.name, value, element.initializer); } } } } - return result; } - - function convertArrayLiteralExpressionToJson( - elements: NodeArray, - elementOption: CommandLineOption | undefined - ): any[] | void { - if (!returnValue) { - return elements.forEach(element => convertPropertyValueToJson(element, elementOption)); - } - - // Filter out invalid values - return filter(elements.map(element => convertPropertyValueToJson(element, elementOption)), v => v !== undefined); + return result; + } + function convertArrayLiteralExpressionToJson(elements: NodeArray, elementOption: CommandLineOption | undefined): any[] | void { + if (!returnValue) { + return elements.forEach(element => convertPropertyValueToJson(element, elementOption)); } - - function convertPropertyValueToJson(valueExpression: Expression, option: CommandLineOption | undefined): any { - switch (valueExpression.kind) { - case SyntaxKind.TrueKeyword: - reportInvalidOptionValue(option && option.type !== "boolean"); - return true; - - case SyntaxKind.FalseKeyword: - reportInvalidOptionValue(option && option.type !== "boolean"); - return false; - - case SyntaxKind.NullKeyword: - reportInvalidOptionValue(option && option.name === "extends"); // "extends" is the only option we don't allow null/undefined for - return null; // eslint-disable-line no-null/no-null - - case SyntaxKind.StringLiteral: - if (!isDoubleQuotedString(valueExpression)) { - errors.push(createDiagnosticForNodeInSourceFile(sourceFile, valueExpression, Diagnostics.String_literal_with_double_quotes_expected)); - } - reportInvalidOptionValue(option && (isString(option.type) && option.type !== "string")); - const text = (valueExpression).text; - if (option && !isString(option.type)) { - const customOption = option; - // Validate custom option type - if (!customOption.type.has(text.toLowerCase())) { - errors.push( - createDiagnosticForInvalidCustomType( - customOption, - (message, arg0, arg1) => createDiagnosticForNodeInSourceFile(sourceFile, valueExpression, message, arg0, arg1) - ) - ); - } - } - return text; - - case SyntaxKind.NumericLiteral: - reportInvalidOptionValue(option && option.type !== "number"); - return Number((valueExpression).text); - - case SyntaxKind.PrefixUnaryExpression: - if ((valueExpression).operator !== SyntaxKind.MinusToken || (valueExpression).operand.kind !== SyntaxKind.NumericLiteral) { - break; // not valid JSON syntax - } - reportInvalidOptionValue(option && option.type !== "number"); - return -Number(((valueExpression).operand).text); - - case SyntaxKind.ObjectLiteralExpression: - reportInvalidOptionValue(option && option.type !== "object"); - const objectLiteralExpression = valueExpression; - - // Currently having element option declaration in the tsconfig with type "object" - // determines if it needs onSetValidOptionKeyValueInParent callback or not - // At moment there are only "compilerOptions", "typeAcquisition" and "typingOptions" - // that satifies it and need it to modify options set in them (for normalizing file paths) - // vs what we set in the json - // If need arises, we can modify this interface and callbacks as needed - if (option) { - const { elementOptions, extraKeyDiagnostics, name: optionName } = option; - return convertObjectLiteralExpressionToJson(objectLiteralExpression, - elementOptions, extraKeyDiagnostics, optionName); - } - else { - return convertObjectLiteralExpressionToJson( - objectLiteralExpression, /* knownOptions*/ undefined, - /*extraKeyDiagnosticMessage */ undefined, /*parentOption*/ undefined); + // Filter out invalid values + return filter(elements.map(element => convertPropertyValueToJson(element, elementOption)), v => v !== undefined); + } + function convertPropertyValueToJson(valueExpression: Expression, option: CommandLineOption | undefined): any { + switch (valueExpression.kind) { + case SyntaxKind.TrueKeyword: + reportInvalidOptionValue(option && option.type !== "boolean"); + return true; + case SyntaxKind.FalseKeyword: + reportInvalidOptionValue(option && option.type !== "boolean"); + return false; + case SyntaxKind.NullKeyword: + reportInvalidOptionValue(option && option.name === "extends"); // "extends" is the only option we don't allow null/undefined for + return null; // eslint-disable-line no-null/no-null + case SyntaxKind.StringLiteral: + if (!isDoubleQuotedString(valueExpression)) { + errors.push(createDiagnosticForNodeInSourceFile(sourceFile, valueExpression, Diagnostics.String_literal_with_double_quotes_expected)); + } + reportInvalidOptionValue(option && (isString(option.type) && option.type !== "string")); + const text = (valueExpression).text; + if (option && !isString(option.type)) { + const customOption = (option); + // Validate custom option type + if (!customOption.type.has(text.toLowerCase())) { + errors.push(createDiagnosticForInvalidCustomType(customOption, (message, arg0, arg1) => createDiagnosticForNodeInSourceFile(sourceFile, valueExpression, message, arg0, arg1))); } - - case SyntaxKind.ArrayLiteralExpression: - reportInvalidOptionValue(option && option.type !== "list"); - return convertArrayLiteralExpressionToJson( - (valueExpression).elements, - option && (option).element); - } - - // Not in expected format - if (option) { - reportInvalidOptionValue(/*isError*/ true); - } - else { - errors.push(createDiagnosticForNodeInSourceFile(sourceFile, valueExpression, Diagnostics.Property_value_can_only_be_string_literal_numeric_literal_true_false_null_object_literal_or_array_literal)); - } - - return undefined; - - function reportInvalidOptionValue(isError: boolean | undefined) { - if (isError) { - errors.push(createDiagnosticForNodeInSourceFile(sourceFile, valueExpression, Diagnostics.Compiler_option_0_requires_a_value_of_type_1, option!.name, getCompilerOptionValueTypeString(option!))); } - } - } - - function isDoubleQuotedString(node: Node): boolean { - return isStringLiteral(node) && isStringDoubleQuoted(node, sourceFile); + return text; + case SyntaxKind.NumericLiteral: + reportInvalidOptionValue(option && option.type !== "number"); + return Number((valueExpression).text); + case SyntaxKind.PrefixUnaryExpression: + if ((valueExpression).operator !== SyntaxKind.MinusToken || (valueExpression).operand.kind !== SyntaxKind.NumericLiteral) { + break; // not valid JSON syntax + } + reportInvalidOptionValue(option && option.type !== "number"); + return -Number(((valueExpression).operand).text); + case SyntaxKind.ObjectLiteralExpression: + reportInvalidOptionValue(option && option.type !== "object"); + const objectLiteralExpression = (valueExpression); + // Currently having element option declaration in the tsconfig with type "object" + // determines if it needs onSetValidOptionKeyValueInParent callback or not + // At moment there are only "compilerOptions", "typeAcquisition" and "typingOptions" + // that satifies it and need it to modify options set in them (for normalizing file paths) + // vs what we set in the json + // If need arises, we can modify this interface and callbacks as needed + if (option) { + const { elementOptions, extraKeyDiagnostics, name: optionName } = (option); + return convertObjectLiteralExpressionToJson(objectLiteralExpression, elementOptions, extraKeyDiagnostics, optionName); + } + else { + return convertObjectLiteralExpressionToJson(objectLiteralExpression, /* knownOptions*/ undefined, + /*extraKeyDiagnosticMessage */ undefined, /*parentOption*/ undefined); + } + case SyntaxKind.ArrayLiteralExpression: + reportInvalidOptionValue(option && option.type !== "list"); + return convertArrayLiteralExpressionToJson((valueExpression).elements, option && (option).element); } - } - - function getCompilerOptionValueTypeString(option: CommandLineOption) { - return option.type === "list" ? - "Array" : - isString(option.type) ? option.type : "string"; - } - - function isCompilerOptionsValue(option: CommandLineOption | undefined, value: any): value is CompilerOptionsValue { + // Not in expected format if (option) { - if (isNullOrUndefined(value)) return true; // All options are undefinable/nullable - if (option.type === "list") { - return isArray(value); + reportInvalidOptionValue(/*isError*/ true); + } + else { + errors.push(createDiagnosticForNodeInSourceFile(sourceFile, valueExpression, Diagnostics.Property_value_can_only_be_string_literal_numeric_literal_true_false_null_object_literal_or_array_literal)); + } + return undefined; + function reportInvalidOptionValue(isError: boolean | undefined) { + if (isError) { + errors.push(createDiagnosticForNodeInSourceFile(sourceFile, valueExpression, Diagnostics.Compiler_option_0_requires_a_value_of_type_1, option!.name, getCompilerOptionValueTypeString(option!))); } - const expectedType = isString(option.type) ? option.type : "string"; - return typeof value === expectedType; } - return false; } - - /** @internal */ - export interface TSConfig { - compilerOptions: CompilerOptions; - compileOnSave: boolean | undefined; - exclude?: readonly string[]; - files: readonly string[] | undefined; - include?: readonly string[]; - references: readonly ProjectReference[] | undefined; - } - - /** @internal */ - export interface ConvertToTSConfigHost { - getCurrentDirectory(): string; - useCaseSensitiveFileNames: boolean; - } - - /** - * Generate an uncommented, complete tsconfig for use with "--showConfig" - * @param configParseResult options to be generated into tsconfig.json - * @param configFileName name of the parsed config file - output paths will be generated relative to this - * @param host provides current directory and case sensitivity services - */ - /** @internal */ - export function convertToTSConfig(configParseResult: ParsedCommandLine, configFileName: string, host: ConvertToTSConfigHost): TSConfig { - const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); - const files = map( - filter( - configParseResult.fileNames, - (!configParseResult.configFileSpecs || !configParseResult.configFileSpecs.validatedIncludeSpecs) ? _ => true : matchesSpecs( - configFileName, - configParseResult.configFileSpecs.validatedIncludeSpecs, - configParseResult.configFileSpecs.validatedExcludeSpecs, - host, - ) - ), - f => getRelativePathFromFile(getNormalizedAbsolutePath(configFileName, host.getCurrentDirectory()), getNormalizedAbsolutePath(f, host.getCurrentDirectory()), getCanonicalFileName) - ); - const optionMap = serializeCompilerOptions(configParseResult.options, { configFilePath: getNormalizedAbsolutePath(configFileName, host.getCurrentDirectory()), useCaseSensitiveFileNames: host.useCaseSensitiveFileNames }); - const watchOptionMap = configParseResult.watchOptions && serializeWatchOptions(configParseResult.watchOptions); - const config = { - compilerOptions: { - ...optionMapToObject(optionMap), - showConfig: undefined, - configFile: undefined, - configFilePath: undefined, - help: undefined, - init: undefined, - listFiles: undefined, - listEmittedFiles: undefined, - project: undefined, - build: undefined, - version: undefined, - }, - watchOptions: watchOptionMap && optionMapToObject(watchOptionMap), - references: map(configParseResult.projectReferences, r => ({ ...r, path: r.originalPath ? r.originalPath : "", originalPath: undefined })), - files: length(files) ? files : undefined, - ...(configParseResult.configFileSpecs ? { - include: filterSameAsDefaultInclude(configParseResult.configFileSpecs.validatedIncludeSpecs), - exclude: configParseResult.configFileSpecs.validatedExcludeSpecs - } : {}), - compileOnSave: !!configParseResult.compileOnSave ? true : undefined - }; - return config; + function isDoubleQuotedString(node: Node): boolean { + return isStringLiteral(node) && isStringDoubleQuoted(node, sourceFile); } - - function optionMapToObject(optionMap: Map): object { - return { - ...arrayFrom(optionMap.entries()).reduce((prev, cur) => ({ ...prev, [cur[0]]: cur[1] }), {}), - }; +} +function getCompilerOptionValueTypeString(option: CommandLineOption) { + return option.type === "list" ? + "Array" : + isString(option.type) ? option.type : "string"; +} +function isCompilerOptionsValue(option: CommandLineOption | undefined, value: any): value is CompilerOptionsValue { + if (option) { + if (isNullOrUndefined(value)) + return true; // All options are undefinable/nullable + if (option.type === "list") { + return isArray(value); + } + const expectedType = isString(option.type) ? option.type : "string"; + return typeof value === expectedType; } - - function filterSameAsDefaultInclude(specs: readonly string[] | undefined) { - if (!length(specs)) return undefined; - if (length(specs) !== 1) return specs; - if (specs![0] === "**/*") return undefined; + return false; +} +/** @internal */ +export interface TSConfig { + compilerOptions: CompilerOptions; + compileOnSave: boolean | undefined; + exclude?: readonly string[]; + files: readonly string[] | undefined; + include?: readonly string[]; + references: readonly ProjectReference[] | undefined; +} +/** @internal */ +export interface ConvertToTSConfigHost { + getCurrentDirectory(): string; + useCaseSensitiveFileNames: boolean; +} +/** + * Generate an uncommented, complete tsconfig for use with "--showConfig" + * @param configParseResult options to be generated into tsconfig.json + * @param configFileName name of the parsed config file - output paths will be generated relative to this + * @param host provides current directory and case sensitivity services + */ +/** @internal */ +export function convertToTSConfig(configParseResult: ParsedCommandLine, configFileName: string, host: ConvertToTSConfigHost): TSConfig { + const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); + const files = map(filter(configParseResult.fileNames, (!configParseResult.configFileSpecs || !configParseResult.configFileSpecs.validatedIncludeSpecs) ? _ => true : matchesSpecs(configFileName, configParseResult.configFileSpecs.validatedIncludeSpecs, configParseResult.configFileSpecs.validatedExcludeSpecs, host)), f => getRelativePathFromFile(getNormalizedAbsolutePath(configFileName, host.getCurrentDirectory()), getNormalizedAbsolutePath(f, host.getCurrentDirectory()), getCanonicalFileName)); + const optionMap = serializeCompilerOptions(configParseResult.options, { configFilePath: getNormalizedAbsolutePath(configFileName, host.getCurrentDirectory()), useCaseSensitiveFileNames: host.useCaseSensitiveFileNames }); + const watchOptionMap = configParseResult.watchOptions && serializeWatchOptions(configParseResult.watchOptions); + const config = { + compilerOptions: { + ...optionMapToObject(optionMap), + showConfig: undefined, + configFile: undefined, + configFilePath: undefined, + help: undefined, + init: undefined, + listFiles: undefined, + listEmittedFiles: undefined, + project: undefined, + build: undefined, + version: undefined, + }, + watchOptions: watchOptionMap && optionMapToObject(watchOptionMap), + references: map(configParseResult.projectReferences, r => ({ ...r, path: r.originalPath ? r.originalPath : "", originalPath: undefined })), + files: length(files) ? files : undefined, + ...(configParseResult.configFileSpecs ? { + include: filterSameAsDefaultInclude(configParseResult.configFileSpecs.validatedIncludeSpecs), + exclude: configParseResult.configFileSpecs.validatedExcludeSpecs + } : {}), + compileOnSave: !!configParseResult.compileOnSave ? true : undefined + }; + return config; +} +function optionMapToObject(optionMap: ts.Map): object { + return { + ...arrayFrom(optionMap.entries()).reduce((prev, cur) => ({ ...prev, [cur[0]]: cur[1] }), {}), + }; +} +function filterSameAsDefaultInclude(specs: readonly string[] | undefined) { + if (!length(specs)) + return undefined; + if (length(specs) !== 1) return specs; - } - - function matchesSpecs(path: string, includeSpecs: readonly string[] | undefined, excludeSpecs: readonly string[] | undefined, host: ConvertToTSConfigHost): (path: string) => boolean { - if (!includeSpecs) return _ => true; - const patterns = getFileMatcherPatterns(path, excludeSpecs, includeSpecs, host.useCaseSensitiveFileNames, host.getCurrentDirectory()); - const excludeRe = patterns.excludePattern && getRegexFromPattern(patterns.excludePattern, host.useCaseSensitiveFileNames); - const includeRe = patterns.includeFilePattern && getRegexFromPattern(patterns.includeFilePattern, host.useCaseSensitiveFileNames); - if (includeRe) { - if (excludeRe) { - return path => !(includeRe.test(path) && !excludeRe.test(path)); - } - return path => !includeRe.test(path); - } - if (excludeRe) { - return path => excludeRe.test(path); - } + if (specs![0] === "**/*") + return undefined; + return specs; +} +function matchesSpecs(path: string, includeSpecs: readonly string[] | undefined, excludeSpecs: readonly string[] | undefined, host: ConvertToTSConfigHost): (path: string) => boolean { + if (!includeSpecs) return _ => true; - } - - function getCustomTypeMapOfCommandLineOption(optionDefinition: CommandLineOption): Map | undefined { - if (optionDefinition.type === "string" || optionDefinition.type === "number" || optionDefinition.type === "boolean" || optionDefinition.type === "object") { - // this is of a type CommandLineOptionOfPrimitiveType - return undefined; - } - else if (optionDefinition.type === "list") { - return getCustomTypeMapOfCommandLineOption(optionDefinition.element); - } - else { - return (optionDefinition).type; + const patterns = getFileMatcherPatterns(path, excludeSpecs, includeSpecs, host.useCaseSensitiveFileNames, host.getCurrentDirectory()); + const excludeRe = patterns.excludePattern && getRegexFromPattern(patterns.excludePattern, host.useCaseSensitiveFileNames); + const includeRe = patterns.includeFilePattern && getRegexFromPattern(patterns.includeFilePattern, host.useCaseSensitiveFileNames); + if (includeRe) { + if (excludeRe) { + return path => !(includeRe.test(path) && !excludeRe.test(path)); } + return path => !includeRe.test(path); } - - function getNameOfCompilerOptionValue(value: CompilerOptionsValue, customTypeMap: Map): string | undefined { - // There is a typeMap associated with this command-line option so use it to map value back to its name - return forEachEntry(customTypeMap, (mapValue, key) => { - if (mapValue === value) { - return key; - } - }); + if (excludeRe) { + return path => excludeRe.test(path); } - - function serializeCompilerOptions( - options: CompilerOptions, - pathOptions?: { configFilePath: string, useCaseSensitiveFileNames: boolean } - ): Map { - return serializeOptionBaseObject(options, getOptionsNameMap(), pathOptions); - } - - function serializeWatchOptions(options: WatchOptions) { - return serializeOptionBaseObject(options, getWatchOptionsNameMap()); - } - - function serializeOptionBaseObject( - options: OptionsBase, - { optionsNameMap }: OptionsNameMap, - pathOptions?: { configFilePath: string, useCaseSensitiveFileNames: boolean } - ): Map { - const result = createMap(); - const getCanonicalFileName = pathOptions && createGetCanonicalFileName(pathOptions.useCaseSensitiveFileNames); - - for (const name in options) { - if (hasProperty(options, name)) { - // tsconfig only options cannot be specified via command line, - // so we can assume that only types that can appear here string | number | boolean - if (optionsNameMap.has(name) && optionsNameMap.get(name)!.category === Diagnostics.Command_line_Options) { - continue; - } - const value = options[name]; - const optionDefinition = optionsNameMap.get(name.toLowerCase()); - if (optionDefinition) { - const customTypeMap = getCustomTypeMapOfCommandLineOption(optionDefinition); - if (!customTypeMap) { - // There is no map associated with this compiler option then use the value as-is - // This is the case if the value is expect to be string, number, boolean or list of string - if (pathOptions && optionDefinition.isFilePath) { - result.set(name, getRelativePathFromFile(pathOptions.configFilePath, getNormalizedAbsolutePath(value as string, getDirectoryPath(pathOptions.configFilePath)), getCanonicalFileName!)); - } - else { - result.set(name, value); - } - } - else { - if (optionDefinition.type === "list") { - result.set(name, (value as readonly (string | number)[]).map(element => getNameOfCompilerOptionValue(element, customTypeMap)!)); // TODO: GH#18217 - } - else { - // There is a typeMap associated with this command-line option so use it to map value back to its name - result.set(name, getNameOfCompilerOptionValue(value, customTypeMap)); - } - } - } - } - } - return result; - } - - /** - * Generate tsconfig configuration when running command line "--init" - * @param options commandlineOptions to be generated into tsconfig.json - * @param fileNames array of filenames to be generated into tsconfig.json - */ - /* @internal */ - export function generateTSConfig(options: CompilerOptions, fileNames: readonly string[], newLine: string): string { - const compilerOptions = extend(options, defaultInitCompilerOptions); - const compilerOptionsMap = serializeCompilerOptions(compilerOptions); - return writeConfigurations(); - - function getDefaultValueForOption(option: CommandLineOption) { - switch (option.type) { - case "number": - return 1; - case "boolean": - return true; - case "string": - return option.isFilePath ? "./" : ""; - case "list": - return []; - case "object": - return {}; - default: - const iterResult = option.type.keys().next(); - if (!iterResult.done) return iterResult.value; - return Debug.fail("Expected 'option.type' to have entries."); - } - } - - function makePadding(paddingLength: number): string { - return Array(paddingLength + 1).join(" "); - } - - function isAllowedOption({ category, name }: CommandLineOption): boolean { - // Skip options which do not have a category or have category `Command_line_Options` - // Exclude all possible `Advanced_Options` in tsconfig.json which were NOT defined in command line - return category !== undefined - && category !== Diagnostics.Command_line_Options - && (category !== Diagnostics.Advanced_Options || compilerOptionsMap.has(name)); + return _ => true; +} +function getCustomTypeMapOfCommandLineOption(optionDefinition: CommandLineOption): ts.Map | undefined { + if (optionDefinition.type === "string" || optionDefinition.type === "number" || optionDefinition.type === "boolean" || optionDefinition.type === "object") { + // this is of a type CommandLineOptionOfPrimitiveType + return undefined; + } + else if (optionDefinition.type === "list") { + return getCustomTypeMapOfCommandLineOption(optionDefinition.element); + } + else { + return (optionDefinition).type; + } +} +function getNameOfCompilerOptionValue(value: CompilerOptionsValue, customTypeMap: ts.Map): string | undefined { + // There is a typeMap associated with this command-line option so use it to map value back to its name + return forEachEntry(customTypeMap, (mapValue, key) => { + if (mapValue === value) { + return key; } - - function writeConfigurations() { - // Filter applicable options to place in the file - const categorizedOptions = createMultiMap(); - for (const option of optionDeclarations) { - const { category } = option; - - if (isAllowedOption(option)) { - categorizedOptions.add(getLocaleSpecificMessage(category!), option); - } + }); +} +function serializeCompilerOptions(options: CompilerOptions, pathOptions?: { + configFilePath: string; + useCaseSensitiveFileNames: boolean; +}): ts.Map { + return serializeOptionBaseObject(options, getOptionsNameMap(), pathOptions); +} +function serializeWatchOptions(options: WatchOptions) { + return serializeOptionBaseObject(options, getWatchOptionsNameMap()); +} +function serializeOptionBaseObject(options: OptionsBase, { optionsNameMap }: OptionsNameMap, pathOptions?: { + configFilePath: string; + useCaseSensitiveFileNames: boolean; +}): ts.Map { + const result = createMap(); + const getCanonicalFileName = pathOptions && createGetCanonicalFileName(pathOptions.useCaseSensitiveFileNames); + for (const name in options) { + if (hasProperty(options, name)) { + // tsconfig only options cannot be specified via command line, + // so we can assume that only types that can appear here string | number | boolean + if (optionsNameMap.has(name) && optionsNameMap.get(name)!.category === Diagnostics.Command_line_Options) { + continue; } - - // Serialize all options and their descriptions - let marginLength = 0; - let seenKnownKeys = 0; - const nameColumn: string[] = []; - const descriptionColumn: string[] = []; - categorizedOptions.forEach((options, category) => { - if (nameColumn.length !== 0) { - nameColumn.push(""); - descriptionColumn.push(""); - } - nameColumn.push(`/* ${category} */`); - descriptionColumn.push(""); - for (const option of options) { - let optionName; - if (compilerOptionsMap.has(option.name)) { - optionName = `"${option.name}": ${JSON.stringify(compilerOptionsMap.get(option.name))}${(seenKnownKeys += 1) === compilerOptionsMap.size ? "" : ","}`; + const value = (options[name]); + const optionDefinition = optionsNameMap.get(name.toLowerCase()); + if (optionDefinition) { + const customTypeMap = getCustomTypeMapOfCommandLineOption(optionDefinition); + if (!customTypeMap) { + // There is no map associated with this compiler option then use the value as-is + // This is the case if the value is expect to be string, number, boolean or list of string + if (pathOptions && optionDefinition.isFilePath) { + result.set(name, getRelativePathFromFile(pathOptions.configFilePath, getNormalizedAbsolutePath((value as string), getDirectoryPath(pathOptions.configFilePath)), (getCanonicalFileName!))); } else { - optionName = `// "${option.name}": ${JSON.stringify(getDefaultValueForOption(option))},`; + result.set(name, value); } - nameColumn.push(optionName); - descriptionColumn.push(`/* ${option.description && getLocaleSpecificMessage(option.description) || option.name} */`); - marginLength = Math.max(optionName.length, marginLength); } - }); - - // Write the output - const tab = makePadding(2); - const result: string[] = []; - result.push(`{`); - result.push(`${tab}"compilerOptions": {`); - // Print out each row, aligning all the descriptions on the same column. - for (let i = 0; i < nameColumn.length; i++) { - const optionName = nameColumn[i]; - const description = descriptionColumn[i]; - result.push(optionName && `${tab}${tab}${optionName}${description && (makePadding(marginLength - optionName.length + 2) + description)}`); - } - if (fileNames.length) { - result.push(`${tab}},`); - result.push(`${tab}"files": [`); - for (let i = 0; i < fileNames.length; i++) { - result.push(`${tab}${tab}${JSON.stringify(fileNames[i])}${i === fileNames.length - 1 ? "" : ","}`); + else { + if (optionDefinition.type === "list") { + result.set(name, (value as readonly (string | number)[]).map(element => getNameOfCompilerOptionValue(element, customTypeMap)!)); // TODO: GH#18217 + } + else { + // There is a typeMap associated with this command-line option so use it to map value back to its name + result.set(name, getNameOfCompilerOptionValue(value, customTypeMap)); + } } - result.push(`${tab}]`); - } - else { - result.push(`${tab}}`); } - result.push(`}`); - - return result.join(newLine) + newLine; } } - - /* @internal */ - export function convertToOptionsWithAbsolutePaths(options: CompilerOptions, toAbsolutePath: (path: string) => string) { - const result: CompilerOptions = {}; - const optionsNameMap = getOptionsNameMap().optionsNameMap; - - for (const name in options) { - if (hasProperty(options, name)) { - result[name] = convertToOptionValueWithAbsolutePaths( - optionsNameMap.get(name.toLowerCase()), - options[name] as CompilerOptionsValue, - toAbsolutePath - ); + return result; +} +/** + * Generate tsconfig configuration when running command line "--init" + * @param options commandlineOptions to be generated into tsconfig.json + * @param fileNames array of filenames to be generated into tsconfig.json + */ +/* @internal */ +export function generateTSConfig(options: CompilerOptions, fileNames: readonly string[], newLine: string): string { + const compilerOptions = extend(options, defaultInitCompilerOptions); + const compilerOptionsMap = serializeCompilerOptions(compilerOptions); + return writeConfigurations(); + function getDefaultValueForOption(option: CommandLineOption) { + switch (option.type) { + case "number": + return 1; + case "boolean": + return true; + case "string": + return option.isFilePath ? "./" : ""; + case "list": + return []; + case "object": + return {}; + default: + const iterResult = option.type.keys().next(); + if (!iterResult.done) + return iterResult.value; + return Debug.fail("Expected 'option.type' to have entries."); + } + } + function makePadding(paddingLength: number): string { + return Array(paddingLength + 1).join(" "); + } + function isAllowedOption({ category, name }: CommandLineOption): boolean { + // Skip options which do not have a category or have category `Command_line_Options` + // Exclude all possible `Advanced_Options` in tsconfig.json which were NOT defined in command line + return category !== undefined + && category !== Diagnostics.Command_line_Options + && (category !== Diagnostics.Advanced_Options || compilerOptionsMap.has(name)); + } + function writeConfigurations() { + // Filter applicable options to place in the file + const categorizedOptions = createMultiMap(); + for (const option of optionDeclarations) { + const { category } = option; + if (isAllowedOption(option)) { + categorizedOptions.add(getLocaleSpecificMessage((category!)), option); } } - if (result.configFilePath) { - result.configFilePath = toAbsolutePath(result.configFilePath); - } - return result; - } - - function convertToOptionValueWithAbsolutePaths(option: CommandLineOption | undefined, value: CompilerOptionsValue, toAbsolutePath: (path: string) => string) { - if (option && !isNullOrUndefined(value)) { - if (option.type === "list") { - const values = value as readonly (string | number)[]; - if (option.element.isFilePath && values.length) { - return values.map(toAbsolutePath); + // Serialize all options and their descriptions + let marginLength = 0; + let seenKnownKeys = 0; + const nameColumn: string[] = []; + const descriptionColumn: string[] = []; + categorizedOptions.forEach((options, category) => { + if (nameColumn.length !== 0) { + nameColumn.push(""); + descriptionColumn.push(""); + } + nameColumn.push(`/* ${category} */`); + descriptionColumn.push(""); + for (const option of options) { + let optionName; + if (compilerOptionsMap.has(option.name)) { + optionName = `"${option.name}": ${JSON.stringify(compilerOptionsMap.get(option.name))}${(seenKnownKeys += 1) === compilerOptionsMap.size ? "" : ","}`; + } + else { + optionName = `// "${option.name}": ${JSON.stringify(getDefaultValueForOption(option))},`; } + nameColumn.push(optionName); + descriptionColumn.push(`/* ${option.description && getLocaleSpecificMessage(option.description) || option.name} */`); + marginLength = Math.max(optionName.length, marginLength); } - else if (option.isFilePath) { - return toAbsolutePath(value as string); + }); + // Write the output + const tab = makePadding(2); + const result: string[] = []; + result.push(`{`); + result.push(`${tab}"compilerOptions": {`); + // Print out each row, aligning all the descriptions on the same column. + for (let i = 0; i < nameColumn.length; i++) { + const optionName = nameColumn[i]; + const description = descriptionColumn[i]; + result.push(optionName && `${tab}${tab}${optionName}${description && (makePadding(marginLength - optionName.length + 2) + description)}`); + } + if (fileNames.length) { + result.push(`${tab}},`); + result.push(`${tab}"files": [`); + for (let i = 0; i < fileNames.length; i++) { + result.push(`${tab}${tab}${JSON.stringify(fileNames[i])}${i === fileNames.length - 1 ? "" : ","}`); } + result.push(`${tab}]`); } - return value; - } - - /** - * Parse the contents of a config file (tsconfig.json). - * @param json The contents of the config file to parse - * @param host Instance of ParseConfigHost used to enumerate files in folder. - * @param basePath A root directory to resolve relative path entries in the config - * file to. e.g. outDir - */ - export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: readonly FileExtensionInfo[], extendedConfigCache?: Map, existingWatchOptions?: WatchOptions): ParsedCommandLine { - return parseJsonConfigFileContentWorker(json, /*sourceFile*/ undefined, host, basePath, existingOptions, existingWatchOptions, configFileName, resolutionStack, extraFileExtensions, extendedConfigCache); + else { + result.push(`${tab}}`); + } + result.push(`}`); + return result.join(newLine) + newLine; } - - /** - * Parse the contents of a config file (tsconfig.json). - * @param jsonNode The contents of the config file to parse - * @param host Instance of ParseConfigHost used to enumerate files in folder. - * @param basePath A root directory to resolve relative path entries in the config - * file to. e.g. outDir - */ - export function parseJsonSourceFileConfigFileContent(sourceFile: TsConfigSourceFile, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: readonly FileExtensionInfo[], extendedConfigCache?: Map, existingWatchOptions?: WatchOptions): ParsedCommandLine { - return parseJsonConfigFileContentWorker(/*json*/ undefined, sourceFile, host, basePath, existingOptions, existingWatchOptions, configFileName, resolutionStack, extraFileExtensions, extendedConfigCache); - } - - /*@internal*/ - export function setConfigFileInOptions(options: CompilerOptions, configFile: TsConfigSourceFile | undefined) { - if (configFile) { - Object.defineProperty(options, "configFile", { enumerable: false, writable: false, value: configFile }); +} +/* @internal */ +export function convertToOptionsWithAbsolutePaths(options: CompilerOptions, toAbsolutePath: (path: string) => string) { + const result: CompilerOptions = {}; + const optionsNameMap = getOptionsNameMap().optionsNameMap; + for (const name in options) { + if (hasProperty(options, name)) { + result[name] = convertToOptionValueWithAbsolutePaths(optionsNameMap.get(name.toLowerCase()), (options[name] as CompilerOptionsValue), toAbsolutePath); } } - - function isNullOrUndefined(x: any): x is null | undefined { - return x === undefined || x === null; // eslint-disable-line no-null/no-null + if (result.configFilePath) { + result.configFilePath = toAbsolutePath(result.configFilePath); } - - function directoryOfCombinedPath(fileName: string, basePath: string) { - // Use the `getNormalizedAbsolutePath` function to avoid canonicalizing the path, as it must remain noncanonical - // until consistent casing errors are reported - return getDirectoryPath(getNormalizedAbsolutePath(fileName, basePath)); + return result; +} +function convertToOptionValueWithAbsolutePaths(option: CommandLineOption | undefined, value: CompilerOptionsValue, toAbsolutePath: (path: string) => string) { + if (option && !isNullOrUndefined(value)) { + if (option.type === "list") { + const values = value as readonly (string | number)[]; + if (option.element.isFilePath && values.length) { + return values.map(toAbsolutePath); + } + } + else if (option.isFilePath) { + return toAbsolutePath(value as string); + } } - - /** - * Parse the contents of a config file from json or json source file (tsconfig.json). - * @param json The contents of the config file to parse - * @param sourceFile sourceFile corresponding to the Json - * @param host Instance of ParseConfigHost used to enumerate files in folder. - * @param basePath A root directory to resolve relative path entries in the config - * file to. e.g. outDir - * @param resolutionStack Only present for backwards-compatibility. Should be empty. - */ - function parseJsonConfigFileContentWorker( - json: any, - sourceFile: TsConfigSourceFile | undefined, - host: ParseConfigHost, - basePath: string, - existingOptions: CompilerOptions = {}, - existingWatchOptions: WatchOptions | undefined, - configFileName?: string, - resolutionStack: Path[] = [], - extraFileExtensions: readonly FileExtensionInfo[] = [], - extendedConfigCache?: Map - ): ParsedCommandLine { - Debug.assert((json === undefined && sourceFile !== undefined) || (json !== undefined && sourceFile === undefined)); - const errors: Diagnostic[] = []; - - const parsedConfig = parseConfig(json, sourceFile, host, basePath, configFileName, resolutionStack, errors, extendedConfigCache); - const { raw } = parsedConfig; - const options = extend(existingOptions, parsedConfig.options || {}); - const watchOptions = existingWatchOptions && parsedConfig.watchOptions ? - extend(existingWatchOptions, parsedConfig.watchOptions) : - parsedConfig.watchOptions || existingWatchOptions; - - options.configFilePath = configFileName && normalizeSlashes(configFileName); - setConfigFileInOptions(options, sourceFile); - let projectReferences: ProjectReference[] | undefined; - const { fileNames, wildcardDirectories, spec } = getFileNames(); - return { - options, - watchOptions, - fileNames, - projectReferences, - typeAcquisition: parsedConfig.typeAcquisition || getDefaultTypeAcquisition(), - raw, - errors, - wildcardDirectories, - compileOnSave: !!raw.compileOnSave, - configFileSpecs: spec - }; - - function getFileNames(): ExpandResult { - let filesSpecs: readonly string[] | undefined; - if (hasProperty(raw, "files") && !isNullOrUndefined(raw.files)) { - if (isArray(raw.files)) { - filesSpecs = raw.files; - const hasReferences = hasProperty(raw, "references") && !isNullOrUndefined(raw.references); - const hasZeroOrNoReferences = !hasReferences || raw.references.length === 0; - const hasExtends = hasProperty(raw, "extends"); - if (filesSpecs.length === 0 && hasZeroOrNoReferences && !hasExtends) { - if (sourceFile) { - const fileName = configFileName || "tsconfig.json"; - const diagnosticMessage = Diagnostics.The_files_list_in_config_file_0_is_empty; - const nodeValue = firstDefined(getTsConfigPropArray(sourceFile, "files"), property => property.initializer); - const error = nodeValue - ? createDiagnosticForNodeInSourceFile(sourceFile, nodeValue, diagnosticMessage, fileName) - : createCompilerDiagnostic(diagnosticMessage, fileName); - errors.push(error); - } - else { - createCompilerDiagnosticOnlyIfJson(Diagnostics.The_files_list_in_config_file_0_is_empty, configFileName || "tsconfig.json"); - } + return value; +} +/** + * Parse the contents of a config file (tsconfig.json). + * @param json The contents of the config file to parse + * @param host Instance of ParseConfigHost used to enumerate files in folder. + * @param basePath A root directory to resolve relative path entries in the config + * file to. e.g. outDir + */ +export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: readonly FileExtensionInfo[], extendedConfigCache?: ts.Map, existingWatchOptions?: WatchOptions): ParsedCommandLine { + return parseJsonConfigFileContentWorker(json, /*sourceFile*/ undefined, host, basePath, existingOptions, existingWatchOptions, configFileName, resolutionStack, extraFileExtensions, extendedConfigCache); +} +/** + * Parse the contents of a config file (tsconfig.json). + * @param jsonNode The contents of the config file to parse + * @param host Instance of ParseConfigHost used to enumerate files in folder. + * @param basePath A root directory to resolve relative path entries in the config + * file to. e.g. outDir + */ +export function parseJsonSourceFileConfigFileContent(sourceFile: TsConfigSourceFile, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[], extraFileExtensions?: readonly FileExtensionInfo[], extendedConfigCache?: ts.Map, existingWatchOptions?: WatchOptions): ParsedCommandLine { + return parseJsonConfigFileContentWorker(/*json*/ undefined, sourceFile, host, basePath, existingOptions, existingWatchOptions, configFileName, resolutionStack, extraFileExtensions, extendedConfigCache); +} +/*@internal*/ +export function setConfigFileInOptions(options: CompilerOptions, configFile: TsConfigSourceFile | undefined) { + if (configFile) { + Object.defineProperty(options, "configFile", { enumerable: false, writable: false, value: configFile }); + } +} +function isNullOrUndefined(x: any): x is null | undefined { + return x === undefined || x === null; // eslint-disable-line no-null/no-null +} +function directoryOfCombinedPath(fileName: string, basePath: string) { + // Use the `getNormalizedAbsolutePath` function to avoid canonicalizing the path, as it must remain noncanonical + // until consistent casing errors are reported + return getDirectoryPath(getNormalizedAbsolutePath(fileName, basePath)); +} +/** + * Parse the contents of a config file from json or json source file (tsconfig.json). + * @param json The contents of the config file to parse + * @param sourceFile sourceFile corresponding to the Json + * @param host Instance of ParseConfigHost used to enumerate files in folder. + * @param basePath A root directory to resolve relative path entries in the config + * file to. e.g. outDir + * @param resolutionStack Only present for backwards-compatibility. Should be empty. + */ +function parseJsonConfigFileContentWorker(json: any, sourceFile: TsConfigSourceFile | undefined, host: ParseConfigHost, basePath: string, existingOptions: CompilerOptions = {}, existingWatchOptions: WatchOptions | undefined, configFileName?: string, resolutionStack: Path[] = [], extraFileExtensions: readonly FileExtensionInfo[] = [], extendedConfigCache?: ts.Map): ParsedCommandLine { + Debug.assert((json === undefined && sourceFile !== undefined) || (json !== undefined && sourceFile === undefined)); + const errors: Diagnostic[] = []; + const parsedConfig = parseConfig(json, sourceFile, host, basePath, configFileName, resolutionStack, errors, extendedConfigCache); + const { raw } = parsedConfig; + const options = extend(existingOptions, parsedConfig.options || {}); + const watchOptions = existingWatchOptions && parsedConfig.watchOptions ? + extend(existingWatchOptions, parsedConfig.watchOptions) : + parsedConfig.watchOptions || existingWatchOptions; + options.configFilePath = configFileName && normalizeSlashes(configFileName); + setConfigFileInOptions(options, sourceFile); + let projectReferences: ProjectReference[] | undefined; + const { fileNames, wildcardDirectories, spec } = getFileNames(); + return { + options, + watchOptions, + fileNames, + projectReferences, + typeAcquisition: parsedConfig.typeAcquisition || getDefaultTypeAcquisition(), + raw, + errors, + wildcardDirectories, + compileOnSave: !!raw.compileOnSave, + configFileSpecs: spec + }; + function getFileNames(): ExpandResult { + let filesSpecs: readonly string[] | undefined; + if (hasProperty(raw, "files") && !isNullOrUndefined(raw.files)) { + if (isArray(raw.files)) { + filesSpecs = raw.files; + const hasReferences = hasProperty(raw, "references") && !isNullOrUndefined(raw.references); + const hasZeroOrNoReferences = !hasReferences || raw.references.length === 0; + const hasExtends = hasProperty(raw, "extends"); + if (filesSpecs.length === 0 && hasZeroOrNoReferences && !hasExtends) { + if (sourceFile) { + const fileName = configFileName || "tsconfig.json"; + const diagnosticMessage = Diagnostics.The_files_list_in_config_file_0_is_empty; + const nodeValue = firstDefined(getTsConfigPropArray(sourceFile, "files"), property => property.initializer); + const error = nodeValue + ? createDiagnosticForNodeInSourceFile(sourceFile, nodeValue, diagnosticMessage, fileName) + : createCompilerDiagnostic(diagnosticMessage, fileName); + errors.push(error); + } + else { + createCompilerDiagnosticOnlyIfJson(Diagnostics.The_files_list_in_config_file_0_is_empty, configFileName || "tsconfig.json"); } - } - else { - createCompilerDiagnosticOnlyIfJson(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "files", "Array"); } } - - let includeSpecs: readonly string[] | undefined; - if (hasProperty(raw, "include") && !isNullOrUndefined(raw.include)) { - if (isArray(raw.include)) { - includeSpecs = raw.include; - } - else { - createCompilerDiagnosticOnlyIfJson(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "include", "Array"); - } + else { + createCompilerDiagnosticOnlyIfJson(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "files", "Array"); } - - let excludeSpecs: readonly string[] | undefined; - if (hasProperty(raw, "exclude") && !isNullOrUndefined(raw.exclude)) { - if (isArray(raw.exclude)) { - excludeSpecs = raw.exclude; - } - else { - createCompilerDiagnosticOnlyIfJson(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "exclude", "Array"); - } + } + let includeSpecs: readonly string[] | undefined; + if (hasProperty(raw, "include") && !isNullOrUndefined(raw.include)) { + if (isArray(raw.include)) { + includeSpecs = raw.include; } - else if (raw.compilerOptions) { - const outDir = raw.compilerOptions.outDir; - const declarationDir = raw.compilerOptions.declarationDir; - - if (outDir || declarationDir) { - excludeSpecs = [outDir, declarationDir].filter(d => !!d); - } + else { + createCompilerDiagnosticOnlyIfJson(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "include", "Array"); } - - if (filesSpecs === undefined && includeSpecs === undefined) { - includeSpecs = ["**/*"]; + } + let excludeSpecs: readonly string[] | undefined; + if (hasProperty(raw, "exclude") && !isNullOrUndefined(raw.exclude)) { + if (isArray(raw.exclude)) { + excludeSpecs = raw.exclude; } - - const result = matchFileNames(filesSpecs, includeSpecs, excludeSpecs, configFileName ? directoryOfCombinedPath(configFileName, basePath) : basePath, options, host, errors, extraFileExtensions, sourceFile); - if (shouldReportNoInputFiles(result, canJsonReportNoInutFiles(raw), resolutionStack)) { - errors.push(getErrorForNoInputFiles(result.spec, configFileName)); + else { + createCompilerDiagnosticOnlyIfJson(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "exclude", "Array"); } - - if (hasProperty(raw, "references") && !isNullOrUndefined(raw.references)) { - if (isArray(raw.references)) { - for (const ref of raw.references) { - if (typeof ref.path !== "string") { - createCompilerDiagnosticOnlyIfJson(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "reference.path", "string"); - } - else { - (projectReferences || (projectReferences = [])).push({ - path: getNormalizedAbsolutePath(ref.path, basePath), - originalPath: ref.path, - prepend: ref.prepend, - circular: ref.circular - }); - } + } + else if (raw.compilerOptions) { + const outDir = raw.compilerOptions.outDir; + const declarationDir = raw.compilerOptions.declarationDir; + if (outDir || declarationDir) { + excludeSpecs = [outDir, declarationDir].filter(d => !!d); + } + } + if (filesSpecs === undefined && includeSpecs === undefined) { + includeSpecs = ["**/*"]; + } + const result = matchFileNames(filesSpecs, includeSpecs, excludeSpecs, configFileName ? directoryOfCombinedPath(configFileName, basePath) : basePath, options, host, errors, extraFileExtensions, sourceFile); + if (shouldReportNoInputFiles(result, canJsonReportNoInutFiles(raw), resolutionStack)) { + errors.push(getErrorForNoInputFiles(result.spec, configFileName)); + } + if (hasProperty(raw, "references") && !isNullOrUndefined(raw.references)) { + if (isArray(raw.references)) { + for (const ref of raw.references) { + if (typeof ref.path !== "string") { + createCompilerDiagnosticOnlyIfJson(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "reference.path", "string"); + } + else { + (projectReferences || (projectReferences = [])).push({ + path: getNormalizedAbsolutePath(ref.path, basePath), + originalPath: ref.path, + prepend: ref.prepend, + circular: ref.circular + }); } - } - else { - createCompilerDiagnosticOnlyIfJson(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "references", "Array"); } } - - return result; - } - - function createCompilerDiagnosticOnlyIfJson(message: DiagnosticMessage, arg0?: string, arg1?: string) { - if (!sourceFile) { - errors.push(createCompilerDiagnostic(message, arg0, arg1)); + else { + createCompilerDiagnosticOnlyIfJson(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "references", "Array"); } } + return result; } - - function isErrorNoInputFiles(error: Diagnostic) { - return error.code === Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code; - } - - function getErrorForNoInputFiles({ includeSpecs, excludeSpecs }: ConfigFileSpecs, configFileName: string | undefined) { - return createCompilerDiagnostic( - Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, - configFileName || "tsconfig.json", - JSON.stringify(includeSpecs || []), - JSON.stringify(excludeSpecs || [])); - } - - function shouldReportNoInputFiles(result: ExpandResult, canJsonReportNoInutFiles: boolean, resolutionStack?: Path[]) { - return result.fileNames.length === 0 && canJsonReportNoInutFiles && (!resolutionStack || resolutionStack.length === 0); - } - - /*@internal*/ - export function canJsonReportNoInutFiles(raw: any) { - return !hasProperty(raw, "files") && !hasProperty(raw, "references"); - } - - /*@internal*/ - export function updateErrorForNoInputFiles(result: ExpandResult, configFileName: string, configFileSpecs: ConfigFileSpecs, configParseDiagnostics: Diagnostic[], canJsonReportNoInutFiles: boolean) { - const existingErrors = configParseDiagnostics.length; - if (shouldReportNoInputFiles(result, canJsonReportNoInutFiles)) { - configParseDiagnostics.push(getErrorForNoInputFiles(configFileSpecs, configFileName)); - } - else { - filterMutate(configParseDiagnostics, error => !isErrorNoInputFiles(error)); + function createCompilerDiagnosticOnlyIfJson(message: DiagnosticMessage, arg0?: string, arg1?: string) { + if (!sourceFile) { + errors.push(createCompilerDiagnostic(message, arg0, arg1)); } - return existingErrors !== configParseDiagnostics.length; - } - - export interface ParsedTsconfig { - raw: any; - options?: CompilerOptions; - watchOptions?: WatchOptions; - typeAcquisition?: TypeAcquisition; - /** - * Note that the case of the config path has not yet been normalized, as no files have been imported into the project yet - */ - extendedConfigPath?: string; } - - function isSuccessfulParsedTsconfig(value: ParsedTsconfig) { - return !!value.options; +} +function isErrorNoInputFiles(error: Diagnostic) { + return error.code === Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code; +} +function getErrorForNoInputFiles({ includeSpecs, excludeSpecs }: ConfigFileSpecs, configFileName: string | undefined) { + return createCompilerDiagnostic(Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, configFileName || "tsconfig.json", JSON.stringify(includeSpecs || []), JSON.stringify(excludeSpecs || [])); +} +function shouldReportNoInputFiles(result: ExpandResult, canJsonReportNoInutFiles: boolean, resolutionStack?: Path[]) { + return result.fileNames.length === 0 && canJsonReportNoInutFiles && (!resolutionStack || resolutionStack.length === 0); +} +/*@internal*/ +export function canJsonReportNoInutFiles(raw: any) { + return !hasProperty(raw, "files") && !hasProperty(raw, "references"); +} +/*@internal*/ +export function updateErrorForNoInputFiles(result: ExpandResult, configFileName: string, configFileSpecs: ConfigFileSpecs, configParseDiagnostics: Diagnostic[], canJsonReportNoInutFiles: boolean) { + const existingErrors = configParseDiagnostics.length; + if (shouldReportNoInputFiles(result, canJsonReportNoInutFiles)) { + configParseDiagnostics.push(getErrorForNoInputFiles(configFileSpecs, configFileName)); + } + else { + filterMutate(configParseDiagnostics, error => !isErrorNoInputFiles(error)); } - + return existingErrors !== configParseDiagnostics.length; +} +export interface ParsedTsconfig { + raw: any; + options?: CompilerOptions; + watchOptions?: WatchOptions; + typeAcquisition?: TypeAcquisition; /** - * This *just* extracts options/include/exclude/files out of a config file. - * It does *not* resolve the included files. + * Note that the case of the config path has not yet been normalized, as no files have been imported into the project yet */ - function parseConfig( - json: any, - sourceFile: TsConfigSourceFile | undefined, - host: ParseConfigHost, - basePath: string, - configFileName: string | undefined, - resolutionStack: string[], - errors: Push, - extendedConfigCache?: Map - ): ParsedTsconfig { - basePath = normalizeSlashes(basePath); - const resolvedPath = getNormalizedAbsolutePath(configFileName || "", basePath); - - if (resolutionStack.indexOf(resolvedPath) >= 0) { - errors.push(createCompilerDiagnostic(Diagnostics.Circularity_detected_while_resolving_configuration_Colon_0, [...resolutionStack, resolvedPath].join(" -> "))); - return { raw: json || convertToObject(sourceFile!, errors) }; - } - - const ownConfig = json ? - parseOwnConfigOfJson(json, host, basePath, configFileName, errors) : - parseOwnConfigOfJsonSourceFile(sourceFile!, host, basePath, configFileName, errors); - - if (ownConfig.extendedConfigPath) { - // copy the resolution stack so it is never reused between branches in potential diamond-problem scenarios. - resolutionStack = resolutionStack.concat([resolvedPath]); - const extendedConfig = getExtendedConfig(sourceFile, ownConfig.extendedConfigPath, host, basePath, resolutionStack, errors, extendedConfigCache); - if (extendedConfig && isSuccessfulParsedTsconfig(extendedConfig)) { - const baseRaw = extendedConfig.raw; - const raw = ownConfig.raw; - const setPropertyInRawIfNotUndefined = (propertyName: string) => { - const value = raw[propertyName] || baseRaw[propertyName]; - if (value) { - raw[propertyName] = value; - } - }; - setPropertyInRawIfNotUndefined("include"); - setPropertyInRawIfNotUndefined("exclude"); - setPropertyInRawIfNotUndefined("files"); - if (raw.compileOnSave === undefined) { - raw.compileOnSave = baseRaw.compileOnSave; + extendedConfigPath?: string; +} +function isSuccessfulParsedTsconfig(value: ParsedTsconfig) { + return !!value.options; +} +/** + * This *just* extracts options/include/exclude/files out of a config file. + * It does *not* resolve the included files. + */ +function parseConfig(json: any, sourceFile: TsConfigSourceFile | undefined, host: ParseConfigHost, basePath: string, configFileName: string | undefined, resolutionStack: string[], errors: Push, extendedConfigCache?: ts.Map): ParsedTsconfig { + basePath = normalizeSlashes(basePath); + const resolvedPath = getNormalizedAbsolutePath(configFileName || "", basePath); + if (resolutionStack.indexOf(resolvedPath) >= 0) { + errors.push(createCompilerDiagnostic(Diagnostics.Circularity_detected_while_resolving_configuration_Colon_0, [...resolutionStack, resolvedPath].join(" -> "))); + return { raw: json || convertToObject(sourceFile!, errors) }; + } + const ownConfig = json ? + parseOwnConfigOfJson(json, host, basePath, configFileName, errors) : + parseOwnConfigOfJsonSourceFile(sourceFile!, host, basePath, configFileName, errors); + if (ownConfig.extendedConfigPath) { + // copy the resolution stack so it is never reused between branches in potential diamond-problem scenarios. + resolutionStack = resolutionStack.concat([resolvedPath]); + const extendedConfig = getExtendedConfig(sourceFile, ownConfig.extendedConfigPath, host, basePath, resolutionStack, errors, extendedConfigCache); + if (extendedConfig && isSuccessfulParsedTsconfig(extendedConfig)) { + const baseRaw = extendedConfig.raw; + const raw = ownConfig.raw; + const setPropertyInRawIfNotUndefined = (propertyName: string) => { + const value = raw[propertyName] || baseRaw[propertyName]; + if (value) { + raw[propertyName] = value; } - ownConfig.options = assign({}, extendedConfig.options, ownConfig.options); - ownConfig.watchOptions = ownConfig.watchOptions && extendedConfig.watchOptions ? - assign({}, extendedConfig.watchOptions, ownConfig.watchOptions) : - ownConfig.watchOptions || extendedConfig.watchOptions; - // TODO extend type typeAcquisition + }; + setPropertyInRawIfNotUndefined("include"); + setPropertyInRawIfNotUndefined("exclude"); + setPropertyInRawIfNotUndefined("files"); + if (raw.compileOnSave === undefined) { + raw.compileOnSave = baseRaw.compileOnSave; } + ownConfig.options = assign({}, extendedConfig.options, ownConfig.options); + ownConfig.watchOptions = ownConfig.watchOptions && extendedConfig.watchOptions ? + assign({}, extendedConfig.watchOptions, ownConfig.watchOptions) : + ownConfig.watchOptions || extendedConfig.watchOptions; + // TODO extend type typeAcquisition } - - return ownConfig; - } - - function parseOwnConfigOfJson( - json: any, - host: ParseConfigHost, - basePath: string, - configFileName: string | undefined, - errors: Push - ): ParsedTsconfig { - if (hasProperty(json, "excludes")) { - errors.push(createCompilerDiagnostic(Diagnostics.Unknown_option_excludes_Did_you_mean_exclude)); + } + return ownConfig; +} +function parseOwnConfigOfJson(json: any, host: ParseConfigHost, basePath: string, configFileName: string | undefined, errors: Push): ParsedTsconfig { + if (hasProperty(json, "excludes")) { + errors.push(createCompilerDiagnostic(Diagnostics.Unknown_option_excludes_Did_you_mean_exclude)); + } + const options = convertCompilerOptionsFromJsonWorker(json.compilerOptions, basePath, errors, configFileName); + // typingOptions has been deprecated and is only supported for backward compatibility purposes. + // It should be removed in future releases - use typeAcquisition instead. + const typeAcquisition = convertTypeAcquisitionFromJsonWorker(json.typeAcquisition || json.typingOptions, basePath, errors, configFileName); + const watchOptions = convertWatchOptionsFromJsonWorker(json.watchOptions, basePath, errors); + json.compileOnSave = convertCompileOnSaveOptionFromJson(json, basePath, errors); + let extendedConfigPath: string | undefined; + if (json.extends) { + if (!isString(json.extends)) { + errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "extends", "string")); } - - const options = convertCompilerOptionsFromJsonWorker(json.compilerOptions, basePath, errors, configFileName); - // typingOptions has been deprecated and is only supported for backward compatibility purposes. - // It should be removed in future releases - use typeAcquisition instead. - const typeAcquisition = convertTypeAcquisitionFromJsonWorker(json.typeAcquisition || json.typingOptions, basePath, errors, configFileName); - const watchOptions = convertWatchOptionsFromJsonWorker(json.watchOptions, basePath, errors); - json.compileOnSave = convertCompileOnSaveOptionFromJson(json, basePath, errors); - let extendedConfigPath: string | undefined; - - if (json.extends) { - if (!isString(json.extends)) { - errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "extends", "string")); - } - else { - const newBase = configFileName ? directoryOfCombinedPath(configFileName, basePath) : basePath; - extendedConfigPath = getExtendsConfigPath(json.extends, host, newBase, errors, createCompilerDiagnostic); - } + else { + const newBase = configFileName ? directoryOfCombinedPath(configFileName, basePath) : basePath; + extendedConfigPath = getExtendsConfigPath(json.extends, host, newBase, errors, createCompilerDiagnostic); } - return { raw: json, options, watchOptions, typeAcquisition, extendedConfigPath }; - } - - function parseOwnConfigOfJsonSourceFile( - sourceFile: TsConfigSourceFile, - host: ParseConfigHost, - basePath: string, - configFileName: string | undefined, - errors: Push - ): ParsedTsconfig { - const options = getDefaultCompilerOptions(configFileName); - let typeAcquisition: TypeAcquisition | undefined, typingOptionstypeAcquisition: TypeAcquisition | undefined; - let watchOptions: WatchOptions | undefined; - let extendedConfigPath: string | undefined; - - const optionsIterator: JsonConversionNotifier = { - onSetValidOptionKeyValueInParent(parentOption: string, option: CommandLineOption, value: CompilerOptionsValue) { - let currentOption; - switch (parentOption) { - case "compilerOptions": - currentOption = options; - break; - case "watchOptions": - currentOption = (watchOptions || (watchOptions = {})); - break; - case "typeAcquisition": - currentOption = (typeAcquisition || (typeAcquisition = getDefaultTypeAcquisition(configFileName))); - break; - case "typingOptions": - currentOption = (typingOptionstypeAcquisition || (typingOptionstypeAcquisition = getDefaultTypeAcquisition(configFileName))); - break; - default: - Debug.fail("Unknown option"); - } - - currentOption[option.name] = normalizeOptionValue(option, basePath, value); - }, - onSetValidOptionKeyValueInRoot(key: string, _keyNode: PropertyName, value: CompilerOptionsValue, valueNode: Expression) { - switch (key) { - case "extends": - const newBase = configFileName ? directoryOfCombinedPath(configFileName, basePath) : basePath; - extendedConfigPath = getExtendsConfigPath( - value, - host, - newBase, - errors, - (message, arg0) => - createDiagnosticForNodeInSourceFile(sourceFile, valueNode, message, arg0) - ); - return; - } - }, - onSetUnknownOptionKeyValueInRoot(key: string, keyNode: PropertyName, _value: CompilerOptionsValue, _valueNode: Expression) { - if (key === "excludes") { - errors.push(createDiagnosticForNodeInSourceFile(sourceFile, keyNode, Diagnostics.Unknown_option_excludes_Did_you_mean_exclude)); - } + } + return { raw: json, options, watchOptions, typeAcquisition, extendedConfigPath }; +} +function parseOwnConfigOfJsonSourceFile(sourceFile: TsConfigSourceFile, host: ParseConfigHost, basePath: string, configFileName: string | undefined, errors: Push): ParsedTsconfig { + const options = getDefaultCompilerOptions(configFileName); + let typeAcquisition: TypeAcquisition | undefined, typingOptionstypeAcquisition: TypeAcquisition | undefined; + let watchOptions: WatchOptions | undefined; + let extendedConfigPath: string | undefined; + const optionsIterator: JsonConversionNotifier = { + onSetValidOptionKeyValueInParent(parentOption: string, option: CommandLineOption, value: CompilerOptionsValue) { + let currentOption; + switch (parentOption) { + case "compilerOptions": + currentOption = options; + break; + case "watchOptions": + currentOption = (watchOptions || (watchOptions = {})); + break; + case "typeAcquisition": + currentOption = (typeAcquisition || (typeAcquisition = getDefaultTypeAcquisition(configFileName))); + break; + case "typingOptions": + currentOption = (typingOptionstypeAcquisition || (typingOptionstypeAcquisition = getDefaultTypeAcquisition(configFileName))); + break; + default: + Debug.fail("Unknown option"); } - }; - const json = convertToObjectWorker(sourceFile, errors, /*returnValue*/ true, getTsconfigRootOptionsMap(), optionsIterator); - - if (!typeAcquisition) { - if (typingOptionstypeAcquisition) { - typeAcquisition = (typingOptionstypeAcquisition.enableAutoDiscovery !== undefined) ? - { - enable: typingOptionstypeAcquisition.enableAutoDiscovery, - include: typingOptionstypeAcquisition.include, - exclude: typingOptionstypeAcquisition.exclude - } : - typingOptionstypeAcquisition; + currentOption[option.name] = normalizeOptionValue(option, basePath, value); + }, + onSetValidOptionKeyValueInRoot(key: string, _keyNode: PropertyName, value: CompilerOptionsValue, valueNode: Expression) { + switch (key) { + case "extends": + const newBase = configFileName ? directoryOfCombinedPath(configFileName, basePath) : basePath; + extendedConfigPath = getExtendsConfigPath((value), host, newBase, errors, (message, arg0) => createDiagnosticForNodeInSourceFile(sourceFile, valueNode, message, arg0)); + return; } - else { - typeAcquisition = getDefaultTypeAcquisition(configFileName); + }, + onSetUnknownOptionKeyValueInRoot(key: string, keyNode: PropertyName, _value: CompilerOptionsValue, _valueNode: Expression) { + if (key === "excludes") { + errors.push(createDiagnosticForNodeInSourceFile(sourceFile, keyNode, Diagnostics.Unknown_option_excludes_Did_you_mean_exclude)); } } - - return { raw: json, options, watchOptions, typeAcquisition, extendedConfigPath }; - } - - function getExtendsConfigPath( - extendedConfig: string, - host: ParseConfigHost, - basePath: string, - errors: Push, - createDiagnostic: (message: DiagnosticMessage, arg1?: string) => Diagnostic) { - extendedConfig = normalizeSlashes(extendedConfig); - if (isRootedDiskPath(extendedConfig) || startsWith(extendedConfig, "./") || startsWith(extendedConfig, "../")) { - let extendedConfigPath = getNormalizedAbsolutePath(extendedConfig, basePath); - if (!host.fileExists(extendedConfigPath) && !endsWith(extendedConfigPath, Extension.Json)) { - extendedConfigPath = `${extendedConfigPath}.json`; - if (!host.fileExists(extendedConfigPath)) { - errors.push(createDiagnostic(Diagnostics.File_0_not_found, extendedConfig)); - return undefined; - } - } - return extendedConfigPath; + }; + const json = convertToObjectWorker(sourceFile, errors, /*returnValue*/ true, getTsconfigRootOptionsMap(), optionsIterator); + if (!typeAcquisition) { + if (typingOptionstypeAcquisition) { + typeAcquisition = (typingOptionstypeAcquisition.enableAutoDiscovery !== undefined) ? + { + enable: typingOptionstypeAcquisition.enableAutoDiscovery, + include: typingOptionstypeAcquisition.include, + exclude: typingOptionstypeAcquisition.exclude + } : + typingOptionstypeAcquisition; } - // If the path isn't a rooted or relative path, resolve like a module - const resolved = nodeModuleNameResolver(extendedConfig, combinePaths(basePath, "tsconfig.json"), { moduleResolution: ModuleResolutionKind.NodeJs }, host, /*cache*/ undefined, /*projectRefs*/ undefined, /*lookupConfig*/ true); - if (resolved.resolvedModule) { - return resolved.resolvedModule.resolvedFileName; + else { + typeAcquisition = getDefaultTypeAcquisition(configFileName); } - errors.push(createDiagnostic(Diagnostics.File_0_not_found, extendedConfig)); - return undefined; } - - export interface ExtendedConfigCacheEntry { - extendedResult: TsConfigSourceFile; - extendedConfig: ParsedTsconfig | undefined; - } - - function getExtendedConfig( - sourceFile: TsConfigSourceFile | undefined, - extendedConfigPath: string, - host: ParseConfigHost, - basePath: string, - resolutionStack: string[], - errors: Push, - extendedConfigCache?: Map - ): ParsedTsconfig | undefined { - const path = host.useCaseSensitiveFileNames ? extendedConfigPath : toFileNameLowerCase(extendedConfigPath); - let value: ExtendedConfigCacheEntry | undefined; - let extendedResult: TsConfigSourceFile; - let extendedConfig: ParsedTsconfig | undefined; - if (extendedConfigCache && (value = extendedConfigCache.get(path))) { - ({ extendedResult, extendedConfig } = value); - } - else { - extendedResult = readJsonConfigFile(extendedConfigPath, path => host.readFile(path)); - if (!extendedResult.parseDiagnostics.length) { - const extendedDirname = getDirectoryPath(extendedConfigPath); - extendedConfig = parseConfig(/*json*/ undefined, extendedResult, host, extendedDirname, - getBaseFileName(extendedConfigPath), resolutionStack, errors, extendedConfigCache); - - if (isSuccessfulParsedTsconfig(extendedConfig)) { - // Update the paths to reflect base path - const relativeDifference = convertToRelativePath(extendedDirname, basePath, identity); - const updatePath = (path: string) => isRootedDiskPath(path) ? path : combinePaths(relativeDifference, path); - const mapPropertiesInRawIfNotUndefined = (propertyName: string) => { - if (raw[propertyName]) { - raw[propertyName] = map(raw[propertyName], updatePath); - } - }; - - const { raw } = extendedConfig; - mapPropertiesInRawIfNotUndefined("include"); - mapPropertiesInRawIfNotUndefined("exclude"); - mapPropertiesInRawIfNotUndefined("files"); - } - } - if (extendedConfigCache) { - extendedConfigCache.set(path, { extendedResult, extendedConfig }); + return { raw: json, options, watchOptions, typeAcquisition, extendedConfigPath }; +} +function getExtendsConfigPath(extendedConfig: string, host: ParseConfigHost, basePath: string, errors: Push, createDiagnostic: (message: DiagnosticMessage, arg1?: string) => Diagnostic) { + extendedConfig = normalizeSlashes(extendedConfig); + if (isRootedDiskPath(extendedConfig) || startsWith(extendedConfig, "./") || startsWith(extendedConfig, "../")) { + let extendedConfigPath = getNormalizedAbsolutePath(extendedConfig, basePath); + if (!host.fileExists(extendedConfigPath) && !endsWith(extendedConfigPath, Extension.Json)) { + extendedConfigPath = `${extendedConfigPath}.json`; + if (!host.fileExists(extendedConfigPath)) { + errors.push(createDiagnostic(Diagnostics.File_0_not_found, extendedConfig)); + return undefined; } } - if (sourceFile) { - sourceFile.extendedSourceFiles = [extendedResult.fileName]; - if (extendedResult.extendedSourceFiles) { - sourceFile.extendedSourceFiles.push(...extendedResult.extendedSourceFiles); + return extendedConfigPath; + } + // If the path isn't a rooted or relative path, resolve like a module + const resolved = nodeModuleNameResolver(extendedConfig, combinePaths(basePath, "tsconfig.json"), { moduleResolution: ModuleResolutionKind.NodeJs }, host, /*cache*/ undefined, /*projectRefs*/ undefined, /*lookupConfig*/ true); + if (resolved.resolvedModule) { + return resolved.resolvedModule.resolvedFileName; + } + errors.push(createDiagnostic(Diagnostics.File_0_not_found, extendedConfig)); + return undefined; +} +export interface ExtendedConfigCacheEntry { + extendedResult: TsConfigSourceFile; + extendedConfig: ParsedTsconfig | undefined; +} +function getExtendedConfig(sourceFile: TsConfigSourceFile | undefined, extendedConfigPath: string, host: ParseConfigHost, basePath: string, resolutionStack: string[], errors: Push, extendedConfigCache?: ts.Map): ParsedTsconfig | undefined { + const path = host.useCaseSensitiveFileNames ? extendedConfigPath : toFileNameLowerCase(extendedConfigPath); + let value: ExtendedConfigCacheEntry | undefined; + let extendedResult: TsConfigSourceFile; + let extendedConfig: ParsedTsconfig | undefined; + if (extendedConfigCache && (value = extendedConfigCache.get(path))) { + ({ extendedResult, extendedConfig } = value); + } + else { + extendedResult = readJsonConfigFile(extendedConfigPath, path => host.readFile(path)); + if (!extendedResult.parseDiagnostics.length) { + const extendedDirname = getDirectoryPath(extendedConfigPath); + extendedConfig = parseConfig(/*json*/ undefined, extendedResult, host, extendedDirname, getBaseFileName(extendedConfigPath), resolutionStack, errors, extendedConfigCache); + if (isSuccessfulParsedTsconfig(extendedConfig)) { + // Update the paths to reflect base path + const relativeDifference = convertToRelativePath(extendedDirname, basePath, identity); + const updatePath = (path: string) => isRootedDiskPath(path) ? path : combinePaths(relativeDifference, path); + const mapPropertiesInRawIfNotUndefined = (propertyName: string) => { + if (raw[propertyName]) { + raw[propertyName] = map(raw[propertyName], updatePath); + } + }; + const { raw } = extendedConfig; + mapPropertiesInRawIfNotUndefined("include"); + mapPropertiesInRawIfNotUndefined("exclude"); + mapPropertiesInRawIfNotUndefined("files"); } } - if (extendedResult.parseDiagnostics.length) { - errors.push(...extendedResult.parseDiagnostics); - return undefined; + if (extendedConfigCache) { + extendedConfigCache.set(path, { extendedResult, extendedConfig }); } - return extendedConfig!; } - - function convertCompileOnSaveOptionFromJson(jsonOption: any, basePath: string, errors: Push): boolean { - if (!hasProperty(jsonOption, compileOnSaveCommandLineOption.name)) { - return false; - } - const result = convertJsonOption(compileOnSaveCommandLineOption, jsonOption.compileOnSave, basePath, errors); - return typeof result === "boolean" && result; - } - - export function convertCompilerOptionsFromJson(jsonOptions: any, basePath: string, configFileName?: string): { options: CompilerOptions, errors: Diagnostic[] } { - const errors: Diagnostic[] = []; - const options = convertCompilerOptionsFromJsonWorker(jsonOptions, basePath, errors, configFileName); - return { options, errors }; - } - - export function convertTypeAcquisitionFromJson(jsonOptions: any, basePath: string, configFileName?: string): { options: TypeAcquisition, errors: Diagnostic[] } { - const errors: Diagnostic[] = []; - const options = convertTypeAcquisitionFromJsonWorker(jsonOptions, basePath, errors, configFileName); - return { options, errors }; - } - - function getDefaultCompilerOptions(configFileName?: string) { - const options: CompilerOptions = configFileName && getBaseFileName(configFileName) === "jsconfig.json" - ? { allowJs: true, maxNodeModuleJsDepth: 2, allowSyntheticDefaultImports: true, skipLibCheck: true, noEmit: true } - : {}; - return options; - } - - function convertCompilerOptionsFromJsonWorker(jsonOptions: any, - basePath: string, errors: Push, configFileName?: string): CompilerOptions { - - const options = getDefaultCompilerOptions(configFileName); - convertOptionsFromJson(getCommandLineCompilerOptionsMap(), jsonOptions, basePath, options, compilerOptionsDidYouMeanDiagnostics, errors); - if (configFileName) { - options.configFilePath = normalizeSlashes(configFileName); - } - return options; - } - - function getDefaultTypeAcquisition(configFileName?: string): TypeAcquisition { - return { enable: !!configFileName && getBaseFileName(configFileName) === "jsconfig.json", include: [], exclude: [] }; - } - - function convertTypeAcquisitionFromJsonWorker(jsonOptions: any, - basePath: string, errors: Push, configFileName?: string): TypeAcquisition { - - const options = getDefaultTypeAcquisition(configFileName); - const typeAcquisition = convertEnableAutoDiscoveryToEnable(jsonOptions); - - convertOptionsFromJson(getCommandLineTypeAcquisitionMap(), typeAcquisition, basePath, options, typeAcquisitionDidYouMeanDiagnostics, errors); - return options; - } - - function convertWatchOptionsFromJsonWorker(jsonOptions: any, basePath: string, errors: Push): WatchOptions | undefined { - return convertOptionsFromJson(getCommandLineWatchOptionsMap(), jsonOptions, basePath, /*defaultOptions*/ undefined, watchOptionsDidYouMeanDiagnostics, errors); - } - - function convertOptionsFromJson(optionsNameMap: Map, jsonOptions: any, basePath: string, - defaultOptions: undefined, diagnostics: DidYouMeanOptionsDiagnostics, errors: Push): WatchOptions | undefined; - function convertOptionsFromJson(optionsNameMap: Map, jsonOptions: any, basePath: string, - defaultOptions: CompilerOptions | TypeAcquisition, diagnostics: DidYouMeanOptionsDiagnostics, errors: Push): CompilerOptions | TypeAcquisition; - function convertOptionsFromJson(optionsNameMap: Map, jsonOptions: any, basePath: string, - defaultOptions: CompilerOptions | TypeAcquisition | WatchOptions | undefined, diagnostics: DidYouMeanOptionsDiagnostics, errors: Push) { - - if (!jsonOptions) { - return; - } - - for (const id in jsonOptions) { - const opt = optionsNameMap.get(id); - if (opt) { - (defaultOptions || (defaultOptions = {}))[opt.name] = convertJsonOption(opt, jsonOptions[id], basePath, errors); - } - else { - errors.push(createUnknownOptionError(id, diagnostics, createCompilerDiagnostic)); - } + if (sourceFile) { + sourceFile.extendedSourceFiles = [extendedResult.fileName]; + if (extendedResult.extendedSourceFiles) { + sourceFile.extendedSourceFiles.push(...extendedResult.extendedSourceFiles); } - return defaultOptions; - } - - function convertJsonOption(opt: CommandLineOption, value: any, basePath: string, errors: Push): CompilerOptionsValue { - if (isCompilerOptionsValue(opt, value)) { - const optType = opt.type; - if (optType === "list" && isArray(value)) { - return convertJsonOptionOfListType(opt, value, basePath, errors); - } - else if (!isString(optType)) { - return convertJsonOptionOfCustomType(opt, value, errors); - } - return normalizeNonListOptionValue(opt, basePath, value); + } + if (extendedResult.parseDiagnostics.length) { + errors.push(...extendedResult.parseDiagnostics); + return undefined; + } + return extendedConfig!; +} +function convertCompileOnSaveOptionFromJson(jsonOption: any, basePath: string, errors: Push): boolean { + if (!hasProperty(jsonOption, compileOnSaveCommandLineOption.name)) { + return false; + } + const result = convertJsonOption(compileOnSaveCommandLineOption, jsonOption.compileOnSave, basePath, errors); + return typeof result === "boolean" && result; +} +export function convertCompilerOptionsFromJson(jsonOptions: any, basePath: string, configFileName?: string): { + options: CompilerOptions; + errors: Diagnostic[]; +} { + const errors: Diagnostic[] = []; + const options = convertCompilerOptionsFromJsonWorker(jsonOptions, basePath, errors, configFileName); + return { options, errors }; +} +export function convertTypeAcquisitionFromJson(jsonOptions: any, basePath: string, configFileName?: string): { + options: TypeAcquisition; + errors: Diagnostic[]; +} { + const errors: Diagnostic[] = []; + const options = convertTypeAcquisitionFromJsonWorker(jsonOptions, basePath, errors, configFileName); + return { options, errors }; +} +function getDefaultCompilerOptions(configFileName?: string) { + const options: CompilerOptions = configFileName && getBaseFileName(configFileName) === "jsconfig.json" + ? { allowJs: true, maxNodeModuleJsDepth: 2, allowSyntheticDefaultImports: true, skipLibCheck: true, noEmit: true } + : {}; + return options; +} +function convertCompilerOptionsFromJsonWorker(jsonOptions: any, basePath: string, errors: Push, configFileName?: string): CompilerOptions { + const options = getDefaultCompilerOptions(configFileName); + convertOptionsFromJson(getCommandLineCompilerOptionsMap(), jsonOptions, basePath, options, compilerOptionsDidYouMeanDiagnostics, errors); + if (configFileName) { + options.configFilePath = normalizeSlashes(configFileName); + } + return options; +} +function getDefaultTypeAcquisition(configFileName?: string): TypeAcquisition { + return { enable: !!configFileName && getBaseFileName(configFileName) === "jsconfig.json", include: [], exclude: [] }; +} +function convertTypeAcquisitionFromJsonWorker(jsonOptions: any, basePath: string, errors: Push, configFileName?: string): TypeAcquisition { + const options = getDefaultTypeAcquisition(configFileName); + const typeAcquisition = convertEnableAutoDiscoveryToEnable(jsonOptions); + convertOptionsFromJson(getCommandLineTypeAcquisitionMap(), typeAcquisition, basePath, options, typeAcquisitionDidYouMeanDiagnostics, errors); + return options; +} +function convertWatchOptionsFromJsonWorker(jsonOptions: any, basePath: string, errors: Push): WatchOptions | undefined { + return convertOptionsFromJson(getCommandLineWatchOptionsMap(), jsonOptions, basePath, /*defaultOptions*/ undefined, watchOptionsDidYouMeanDiagnostics, errors); +} +function convertOptionsFromJson(optionsNameMap: ts.Map, jsonOptions: any, basePath: string, defaultOptions: undefined, diagnostics: DidYouMeanOptionsDiagnostics, errors: Push): WatchOptions | undefined; +function convertOptionsFromJson(optionsNameMap: ts.Map, jsonOptions: any, basePath: string, defaultOptions: CompilerOptions | TypeAcquisition, diagnostics: DidYouMeanOptionsDiagnostics, errors: Push): CompilerOptions | TypeAcquisition; +function convertOptionsFromJson(optionsNameMap: ts.Map, jsonOptions: any, basePath: string, defaultOptions: CompilerOptions | TypeAcquisition | WatchOptions | undefined, diagnostics: DidYouMeanOptionsDiagnostics, errors: Push) { + if (!jsonOptions) { + return; + } + for (const id in jsonOptions) { + const opt = optionsNameMap.get(id); + if (opt) { + (defaultOptions || (defaultOptions = {}))[opt.name] = convertJsonOption(opt, jsonOptions[id], basePath, errors); } else { - errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, opt.name, getCompilerOptionValueTypeString(opt))); + errors.push(createUnknownOptionError(id, diagnostics, createCompilerDiagnostic)); } } - - function normalizeOptionValue(option: CommandLineOption, basePath: string, value: any): CompilerOptionsValue { - if (isNullOrUndefined(value)) return undefined; - if (option.type === "list") { - const listOption = option; - if (listOption.element.isFilePath || !isString(listOption.element.type)) { - return filter(map(value, v => normalizeOptionValue(listOption.element, basePath, v)), v => !!v); - } - return value; + return defaultOptions; +} +function convertJsonOption(opt: CommandLineOption, value: any, basePath: string, errors: Push): CompilerOptionsValue { + if (isCompilerOptionsValue(opt, value)) { + const optType = opt.type; + if (optType === "list" && isArray(value)) { + return convertJsonOptionOfListType((opt), value, basePath, errors); } - else if (!isString(option.type)) { - return option.type.get(isString(value) ? value.toLowerCase() : value); + else if (!isString(optType)) { + return convertJsonOptionOfCustomType((opt), (value), errors); } - return normalizeNonListOptionValue(option, basePath, value); - } - - function normalizeNonListOptionValue(option: CommandLineOption, basePath: string, value: any): CompilerOptionsValue { - if (option.isFilePath) { - value = getNormalizedAbsolutePath(value, basePath); - if (value === "") { - value = "."; - } + return normalizeNonListOptionValue(opt, basePath, value); + } + else { + errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, opt.name, getCompilerOptionValueTypeString(opt))); + } +} +function normalizeOptionValue(option: CommandLineOption, basePath: string, value: any): CompilerOptionsValue { + if (isNullOrUndefined(value)) + return undefined; + if (option.type === "list") { + const listOption = option; + if (listOption.element.isFilePath || !isString(listOption.element.type)) { + return filter(map(value, v => normalizeOptionValue(listOption.element, basePath, v)), v => !!v); } return value; } - - function convertJsonOptionOfCustomType(opt: CommandLineOptionOfCustomType, value: string, errors: Push) { - if (isNullOrUndefined(value)) return undefined; - const key = value.toLowerCase(); - const val = opt.type.get(key); - if (val !== undefined) { - return val; - } - else { - errors.push(createCompilerDiagnosticForInvalidCustomType(opt)); + else if (!isString(option.type)) { + return option.type.get(isString(value) ? value.toLowerCase() : value); + } + return normalizeNonListOptionValue(option, basePath, value); +} +function normalizeNonListOptionValue(option: CommandLineOption, basePath: string, value: any): CompilerOptionsValue { + if (option.isFilePath) { + value = getNormalizedAbsolutePath(value, basePath); + if (value === "") { + value = "."; } } - - function convertJsonOptionOfListType(option: CommandLineOptionOfListType, values: readonly any[], basePath: string, errors: Push): any[] { - return filter(map(values, v => convertJsonOption(option.element, v, basePath, errors)), v => !!v); + return value; +} +function convertJsonOptionOfCustomType(opt: CommandLineOptionOfCustomType, value: string, errors: Push) { + if (isNullOrUndefined(value)) + return undefined; + const key = value.toLowerCase(); + const val = opt.type.get(key); + if (val !== undefined) { + return val; } - - function trimString(s: string) { - return typeof s.trim === "function" ? s.trim() : s.replace(/^[\s]+|[\s]+$/g, ""); + else { + errors.push(createCompilerDiagnosticForInvalidCustomType(opt)); } - - /** - * Tests for a path that ends in a recursive directory wildcard. - * Matches **, \**, **\, and \**\, but not a**b. - * - * NOTE: used \ in place of / above to avoid issues with multiline comments. - * - * Breakdown: - * (^|\/) # matches either the beginning of the string or a directory separator. - * \*\* # matches the recursive directory wildcard "**". - * \/?$ # matches an optional trailing directory separator at the end of the string. - */ - const invalidTrailingRecursionPattern = /(^|\/)\*\*\/?$/; - - /** - * Tests for a path where .. appears after a recursive directory wildcard. - * Matches **\..\*, **\a\..\*, and **\.., but not ..\**\* - * - * NOTE: used \ in place of / above to avoid issues with multiline comments. - * - * Breakdown: - * (^|\/) # matches either the beginning of the string or a directory separator. - * \*\*\/ # matches a recursive directory wildcard "**" followed by a directory separator. - * (.*\/)? # optionally matches any number of characters followed by a directory separator. - * \.\. # matches a parent directory path component ".." - * ($|\/) # matches either the end of the string or a directory separator. - */ - const invalidDotDotAfterRecursiveWildcardPattern = /(^|\/)\*\*\/(.*\/)?\.\.($|\/)/; - - /** - * Tests for a path containing a wildcard character in a directory component of the path. - * Matches \*\, \?\, and \a*b\, but not \a\ or \a\*. - * - * NOTE: used \ in place of / above to avoid issues with multiline comments. - * - * Breakdown: - * \/ # matches a directory separator. - * [^/]*? # matches any number of characters excluding directory separators (non-greedy). - * [*?] # matches either a wildcard character (* or ?) - * [^/]* # matches any number of characters excluding directory separators (greedy). - * \/ # matches a directory separator. - */ - const watchRecursivePattern = /\/[^/]*?[*?][^/]*\//; - - /** - * Matches the portion of a wildcard path that does not contain wildcards. - * Matches \a of \a\*, or \a\b\c of \a\b\c\?\d. - * - * NOTE: used \ in place of / above to avoid issues with multiline comments. - * - * Breakdown: - * ^ # matches the beginning of the string - * [^*?]* # matches any number of non-wildcard characters - * (?=\/[^/]*[*?]) # lookahead that matches a directory separator followed by - * # a path component that contains at least one wildcard character (* or ?). - */ - const wildcardDirectoryPattern = /^[^*?]*(?=\/[^/]*[*?])/; - - /** - * Expands an array of file specifications. - * - * @param filesSpecs The literal file names to include. - * @param includeSpecs The wildcard file specifications to include. - * @param excludeSpecs The wildcard file specifications to exclude. - * @param basePath The base path for any relative file specifications. - * @param options Compiler options. - * @param host The host used to resolve files and directories. - * @param errors An array for diagnostic reporting. - */ - function matchFileNames( - filesSpecs: readonly string[] | undefined, - includeSpecs: readonly string[] | undefined, - excludeSpecs: readonly string[] | undefined, - basePath: string, - options: CompilerOptions, - host: ParseConfigHost, - errors: Push, - extraFileExtensions: readonly FileExtensionInfo[], - jsonSourceFile: TsConfigSourceFile | undefined - ): ExpandResult { - basePath = normalizePath(basePath); - let validatedIncludeSpecs: readonly string[] | undefined, validatedExcludeSpecs: readonly string[] | undefined; - - // The exclude spec list is converted into a regular expression, which allows us to quickly - // test whether a file or directory should be excluded before recursively traversing the - // file system. - - if (includeSpecs) { - validatedIncludeSpecs = validateSpecs(includeSpecs, errors, /*allowTrailingRecursion*/ false, jsonSourceFile, "include"); - } - - if (excludeSpecs) { - validatedExcludeSpecs = validateSpecs(excludeSpecs, errors, /*allowTrailingRecursion*/ true, jsonSourceFile, "exclude"); - } - - // Wildcard directories (provided as part of a wildcard path) are stored in a - // file map that marks whether it was a regular wildcard match (with a `*` or `?` token), - // or a recursive directory. This information is used by filesystem watchers to monitor for - // new entries in these paths. - const wildcardDirectories = getWildcardDirectories(validatedIncludeSpecs, validatedExcludeSpecs, basePath, host.useCaseSensitiveFileNames); - - const spec: ConfigFileSpecs = { filesSpecs, includeSpecs, excludeSpecs, validatedIncludeSpecs, validatedExcludeSpecs, wildcardDirectories }; - return getFileNamesFromConfigSpecs(spec, basePath, options, host, extraFileExtensions); - } - - /** - * Gets the file names from the provided config file specs that contain, files, include, exclude and - * other properties needed to resolve the file names - * @param spec The config file specs extracted with file names to include, wildcards to include/exclude and other details - * @param basePath The base path for any relative file specifications. - * @param options Compiler options. - * @param host The host used to resolve files and directories. - * @param extraFileExtensions optionaly file extra file extension information from host - */ - /* @internal */ - export function getFileNamesFromConfigSpecs(spec: ConfigFileSpecs, basePath: string, options: CompilerOptions, host: ParseConfigHost, extraFileExtensions: readonly FileExtensionInfo[] = []): ExpandResult { - basePath = normalizePath(basePath); - - const keyMapper = createGetCanonicalFileName(host.useCaseSensitiveFileNames); - - // Literal file names (provided via the "files" array in tsconfig.json) are stored in a - // file map with a possibly case insensitive key. We use this map later when when including - // wildcard paths. - const literalFileMap = createMap(); - - // Wildcard paths (provided via the "includes" array in tsconfig.json) are stored in a - // file map with a possibly case insensitive key. We use this map to store paths matched - // via wildcard, and to handle extension priority. - const wildcardFileMap = createMap(); - - // Wildcard paths of json files (provided via the "includes" array in tsconfig.json) are stored in a - // file map with a possibly case insensitive key. We use this map to store paths matched - // via wildcard of *.json kind - const wildCardJsonFileMap = createMap(); - const { filesSpecs, validatedIncludeSpecs, validatedExcludeSpecs, wildcardDirectories } = spec; - - // Rather than requery this for each file and filespec, we query the supported extensions - // once and store it on the expansion context. - const supportedExtensions = getSupportedExtensions(options, extraFileExtensions); - const supportedExtensionsWithJsonIfResolveJsonModule = getSuppoertedExtensionsWithJsonIfResolveJsonModule(options, supportedExtensions); - - // Literal files are always included verbatim. An "include" or "exclude" specification cannot - // remove a literal file. - if (filesSpecs) { - for (const fileName of filesSpecs) { - const file = getNormalizedAbsolutePath(fileName, basePath); - literalFileMap.set(keyMapper(file), file); - } - } - - let jsonOnlyIncludeRegexes: readonly RegExp[] | undefined; - if (validatedIncludeSpecs && validatedIncludeSpecs.length > 0) { - for (const file of host.readDirectory(basePath, supportedExtensionsWithJsonIfResolveJsonModule, validatedExcludeSpecs, validatedIncludeSpecs, /*depth*/ undefined)) { - if (fileExtensionIs(file, Extension.Json)) { - // Valid only if *.json specified - if (!jsonOnlyIncludeRegexes) { - const includes = validatedIncludeSpecs.filter(s => endsWith(s, Extension.Json)); - const includeFilePatterns = map(getRegularExpressionsForWildcards(includes, basePath, "files"), pattern => `^${pattern}$`); - jsonOnlyIncludeRegexes = includeFilePatterns ? includeFilePatterns.map(pattern => getRegexFromPattern(pattern, host.useCaseSensitiveFileNames)) : emptyArray; - } - const includeIndex = findIndex(jsonOnlyIncludeRegexes, re => re.test(file)); - if (includeIndex !== -1) { - const key = keyMapper(file); - if (!literalFileMap.has(key) && !wildCardJsonFileMap.has(key)) { - wildCardJsonFileMap.set(key, file); - } - } - continue; - } - // If we have already included a literal or wildcard path with a - // higher priority extension, we should skip this file. - // - // This handles cases where we may encounter both .ts and - // .d.ts (or .js if "allowJs" is enabled) in the same - // directory when they are compilation outputs. - if (hasFileWithHigherPriorityExtension(file, literalFileMap, wildcardFileMap, supportedExtensions, keyMapper)) { - continue; +} +function convertJsonOptionOfListType(option: CommandLineOptionOfListType, values: readonly any[], basePath: string, errors: Push): any[] { + return filter(map(values, v => convertJsonOption(option.element, v, basePath, errors)), v => !!v); +} +function trimString(s: string) { + return typeof s.trim === "function" ? s.trim() : s.replace(/^[\s]+|[\s]+$/g, ""); +} +/** + * Tests for a path that ends in a recursive directory wildcard. + * Matches **, \**, **\, and \**\, but not a**b. + * + * NOTE: used \ in place of / above to avoid issues with multiline comments. + * + * Breakdown: + * (^|\/) # matches either the beginning of the string or a directory separator. + * \*\* # matches the recursive directory wildcard "**". + * \/?$ # matches an optional trailing directory separator at the end of the string. + */ +const invalidTrailingRecursionPattern = /(^|\/)\*\*\/?$/; +/** + * Tests for a path where .. appears after a recursive directory wildcard. + * Matches **\..\*, **\a\..\*, and **\.., but not ..\**\* + * + * NOTE: used \ in place of / above to avoid issues with multiline comments. + * + * Breakdown: + * (^|\/) # matches either the beginning of the string or a directory separator. + * \*\*\/ # matches a recursive directory wildcard "**" followed by a directory separator. + * (.*\/)? # optionally matches any number of characters followed by a directory separator. + * \.\. # matches a parent directory path component ".." + * ($|\/) # matches either the end of the string or a directory separator. + */ +const invalidDotDotAfterRecursiveWildcardPattern = /(^|\/)\*\*\/(.*\/)?\.\.($|\/)/; +/** + * Tests for a path containing a wildcard character in a directory component of the path. + * Matches \*\, \?\, and \a*b\, but not \a\ or \a\*. + * + * NOTE: used \ in place of / above to avoid issues with multiline comments. + * + * Breakdown: + * \/ # matches a directory separator. + * [^/]*? # matches any number of characters excluding directory separators (non-greedy). + * [*?] # matches either a wildcard character (* or ?) + * [^/]* # matches any number of characters excluding directory separators (greedy). + * \/ # matches a directory separator. + */ +const watchRecursivePattern = /\/[^/]*?[*?][^/]*\//; +/** + * Matches the portion of a wildcard path that does not contain wildcards. + * Matches \a of \a\*, or \a\b\c of \a\b\c\?\d. + * + * NOTE: used \ in place of / above to avoid issues with multiline comments. + * + * Breakdown: + * ^ # matches the beginning of the string + * [^*?]* # matches any number of non-wildcard characters + * (?=\/[^/]*[*?]) # lookahead that matches a directory separator followed by + * # a path component that contains at least one wildcard character (* or ?). + */ +const wildcardDirectoryPattern = /^[^*?]*(?=\/[^/]*[*?])/; +/** + * Expands an array of file specifications. + * + * @param filesSpecs The literal file names to include. + * @param includeSpecs The wildcard file specifications to include. + * @param excludeSpecs The wildcard file specifications to exclude. + * @param basePath The base path for any relative file specifications. + * @param options Compiler options. + * @param host The host used to resolve files and directories. + * @param errors An array for diagnostic reporting. + */ +function matchFileNames(filesSpecs: readonly string[] | undefined, includeSpecs: readonly string[] | undefined, excludeSpecs: readonly string[] | undefined, basePath: string, options: CompilerOptions, host: ParseConfigHost, errors: Push, extraFileExtensions: readonly FileExtensionInfo[], jsonSourceFile: TsConfigSourceFile | undefined): ExpandResult { + basePath = normalizePath(basePath); + let validatedIncludeSpecs: readonly string[] | undefined, validatedExcludeSpecs: readonly string[] | undefined; + // The exclude spec list is converted into a regular expression, which allows us to quickly + // test whether a file or directory should be excluded before recursively traversing the + // file system. + if (includeSpecs) { + validatedIncludeSpecs = validateSpecs(includeSpecs, errors, /*allowTrailingRecursion*/ false, jsonSourceFile, "include"); + } + if (excludeSpecs) { + validatedExcludeSpecs = validateSpecs(excludeSpecs, errors, /*allowTrailingRecursion*/ true, jsonSourceFile, "exclude"); + } + // Wildcard directories (provided as part of a wildcard path) are stored in a + // file map that marks whether it was a regular wildcard match (with a `*` or `?` token), + // or a recursive directory. This information is used by filesystem watchers to monitor for + // new entries in these paths. + const wildcardDirectories = getWildcardDirectories(validatedIncludeSpecs, validatedExcludeSpecs, basePath, host.useCaseSensitiveFileNames); + const spec: ConfigFileSpecs = { filesSpecs, includeSpecs, excludeSpecs, validatedIncludeSpecs, validatedExcludeSpecs, wildcardDirectories }; + return getFileNamesFromConfigSpecs(spec, basePath, options, host, extraFileExtensions); +} +/** + * Gets the file names from the provided config file specs that contain, files, include, exclude and + * other properties needed to resolve the file names + * @param spec The config file specs extracted with file names to include, wildcards to include/exclude and other details + * @param basePath The base path for any relative file specifications. + * @param options Compiler options. + * @param host The host used to resolve files and directories. + * @param extraFileExtensions optionaly file extra file extension information from host + */ +/* @internal */ +export function getFileNamesFromConfigSpecs(spec: ConfigFileSpecs, basePath: string, options: CompilerOptions, host: ParseConfigHost, extraFileExtensions: readonly FileExtensionInfo[] = []): ExpandResult { + basePath = normalizePath(basePath); + const keyMapper = createGetCanonicalFileName(host.useCaseSensitiveFileNames); + // Literal file names (provided via the "files" array in tsconfig.json) are stored in a + // file map with a possibly case insensitive key. We use this map later when when including + // wildcard paths. + const literalFileMap = createMap(); + // Wildcard paths (provided via the "includes" array in tsconfig.json) are stored in a + // file map with a possibly case insensitive key. We use this map to store paths matched + // via wildcard, and to handle extension priority. + const wildcardFileMap = createMap(); + // Wildcard paths of json files (provided via the "includes" array in tsconfig.json) are stored in a + // file map with a possibly case insensitive key. We use this map to store paths matched + // via wildcard of *.json kind + const wildCardJsonFileMap = createMap(); + const { filesSpecs, validatedIncludeSpecs, validatedExcludeSpecs, wildcardDirectories } = spec; + // Rather than requery this for each file and filespec, we query the supported extensions + // once and store it on the expansion context. + const supportedExtensions = getSupportedExtensions(options, extraFileExtensions); + const supportedExtensionsWithJsonIfResolveJsonModule = getSuppoertedExtensionsWithJsonIfResolveJsonModule(options, supportedExtensions); + // Literal files are always included verbatim. An "include" or "exclude" specification cannot + // remove a literal file. + if (filesSpecs) { + for (const fileName of filesSpecs) { + const file = getNormalizedAbsolutePath(fileName, basePath); + literalFileMap.set(keyMapper(file), file); + } + } + let jsonOnlyIncludeRegexes: readonly RegExp[] | undefined; + if (validatedIncludeSpecs && validatedIncludeSpecs.length > 0) { + for (const file of host.readDirectory(basePath, supportedExtensionsWithJsonIfResolveJsonModule, validatedExcludeSpecs, validatedIncludeSpecs, /*depth*/ undefined)) { + if (fileExtensionIs(file, Extension.Json)) { + // Valid only if *.json specified + if (!jsonOnlyIncludeRegexes) { + const includes = validatedIncludeSpecs.filter(s => endsWith(s, Extension.Json)); + const includeFilePatterns = map(getRegularExpressionsForWildcards(includes, basePath, "files"), pattern => `^${pattern}$`); + jsonOnlyIncludeRegexes = includeFilePatterns ? includeFilePatterns.map(pattern => getRegexFromPattern(pattern, host.useCaseSensitiveFileNames)) : emptyArray; } - - // We may have included a wildcard path with a lower priority - // extension due to the user-defined order of entries in the - // "include" array. If there is a lower priority extension in the - // same directory, we should remove it. - removeWildcardFilesWithLowerPriorityExtension(file, wildcardFileMap, supportedExtensions, keyMapper); - - const key = keyMapper(file); - if (!literalFileMap.has(key) && !wildcardFileMap.has(key)) { - wildcardFileMap.set(key, file); + const includeIndex = findIndex(jsonOnlyIncludeRegexes, re => re.test(file)); + if (includeIndex !== -1) { + const key = keyMapper(file); + if (!literalFileMap.has(key) && !wildCardJsonFileMap.has(key)) { + wildCardJsonFileMap.set(key, file); + } } + continue; } - } - - const literalFiles = arrayFrom(literalFileMap.values()); - const wildcardFiles = arrayFrom(wildcardFileMap.values()); - - return { - fileNames: literalFiles.concat(wildcardFiles, arrayFrom(wildCardJsonFileMap.values())), - wildcardDirectories, - spec - }; - } - - function validateSpecs(specs: readonly string[], errors: Push, allowTrailingRecursion: boolean, jsonSourceFile: TsConfigSourceFile | undefined, specKey: string): readonly string[] { - return specs.filter(spec => { - const diag = specToDiagnostic(spec, allowTrailingRecursion); - if (diag !== undefined) { - errors.push(createDiagnostic(diag, spec)); + // If we have already included a literal or wildcard path with a + // higher priority extension, we should skip this file. + // + // This handles cases where we may encounter both .ts and + // .d.ts (or .js if "allowJs" is enabled) in the same + // directory when they are compilation outputs. + if (hasFileWithHigherPriorityExtension(file, literalFileMap, wildcardFileMap, supportedExtensions, keyMapper)) { + continue; + } + // We may have included a wildcard path with a lower priority + // extension due to the user-defined order of entries in the + // "include" array. If there is a lower priority extension in the + // same directory, we should remove it. + removeWildcardFilesWithLowerPriorityExtension(file, wildcardFileMap, supportedExtensions, keyMapper); + const key = keyMapper(file); + if (!literalFileMap.has(key) && !wildcardFileMap.has(key)) { + wildcardFileMap.set(key, file); } - return diag === undefined; - }); - - function createDiagnostic(message: DiagnosticMessage, spec: string): Diagnostic { - const element = getTsConfigPropArrayElementValue(jsonSourceFile, specKey, spec); - return element ? - createDiagnosticForNodeInSourceFile(jsonSourceFile!, element, message, spec) : - createCompilerDiagnostic(message, spec); } } - - function specToDiagnostic(spec: string, allowTrailingRecursion: boolean): DiagnosticMessage | undefined { - if (!allowTrailingRecursion && invalidTrailingRecursionPattern.test(spec)) { - return Diagnostics.File_specification_cannot_end_in_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0; - } - else if (invalidDotDotAfterRecursiveWildcardPattern.test(spec)) { - return Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0; - } + const literalFiles = arrayFrom(literalFileMap.values()); + const wildcardFiles = arrayFrom(wildcardFileMap.values()); + return { + fileNames: literalFiles.concat(wildcardFiles, arrayFrom(wildCardJsonFileMap.values())), + wildcardDirectories, + spec + }; +} +function validateSpecs(specs: readonly string[], errors: Push, allowTrailingRecursion: boolean, jsonSourceFile: TsConfigSourceFile | undefined, specKey: string): readonly string[] { + return specs.filter(spec => { + const diag = specToDiagnostic(spec, allowTrailingRecursion); + if (diag !== undefined) { + errors.push(createDiagnostic(diag, spec)); + } + return diag === undefined; + }); + function createDiagnostic(message: DiagnosticMessage, spec: string): Diagnostic { + const element = getTsConfigPropArrayElementValue(jsonSourceFile, specKey, spec); + return element ? + createDiagnosticForNodeInSourceFile((jsonSourceFile!), element, message, spec) : + createCompilerDiagnostic(message, spec); } - - /** - * Gets directories in a set of include patterns that should be watched for changes. - */ - function getWildcardDirectories(include: readonly string[] | undefined, exclude: readonly string[] | undefined, path: string, useCaseSensitiveFileNames: boolean): MapLike { - // We watch a directory recursively if it contains a wildcard anywhere in a directory segment - // of the pattern: - // - // /a/b/**/d - Watch /a/b recursively to catch changes to any d in any subfolder recursively - // /a/b/*/d - Watch /a/b recursively to catch any d in any immediate subfolder, even if a new subfolder is added - // /a/b - Watch /a/b recursively to catch changes to anything in any recursive subfoler - // - // We watch a directory without recursion if it contains a wildcard in the file segment of - // the pattern: - // - // /a/b/* - Watch /a/b directly to catch any new file - // /a/b/a?z - Watch /a/b directly to catch any new file matching a?z - const rawExcludeRegex = getRegularExpressionForWildcard(exclude, path, "exclude"); - const excludeRegex = rawExcludeRegex && new RegExp(rawExcludeRegex, useCaseSensitiveFileNames ? "" : "i"); - const wildcardDirectories: MapLike = {}; - if (include !== undefined) { - const recursiveKeys: string[] = []; - for (const file of include) { - const spec = normalizePath(combinePaths(path, file)); - if (excludeRegex && excludeRegex.test(spec)) { - continue; - } - - const match = getWildcardDirectoryFromSpec(spec, useCaseSensitiveFileNames); - if (match) { - const { key, flags } = match; - const existingFlags = wildcardDirectories[key]; - if (existingFlags === undefined || existingFlags < flags) { - wildcardDirectories[key] = flags; - if (flags === WatchDirectoryFlags.Recursive) { - recursiveKeys.push(key); - } +} +function specToDiagnostic(spec: string, allowTrailingRecursion: boolean): DiagnosticMessage | undefined { + if (!allowTrailingRecursion && invalidTrailingRecursionPattern.test(spec)) { + return Diagnostics.File_specification_cannot_end_in_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0; + } + else if (invalidDotDotAfterRecursiveWildcardPattern.test(spec)) { + return Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0; + } +} +/** + * Gets directories in a set of include patterns that should be watched for changes. + */ +function getWildcardDirectories(include: readonly string[] | undefined, exclude: readonly string[] | undefined, path: string, useCaseSensitiveFileNames: boolean): MapLike { + // We watch a directory recursively if it contains a wildcard anywhere in a directory segment + // of the pattern: + // + // /a/b/**/d - Watch /a/b recursively to catch changes to any d in any subfolder recursively + // /a/b/*/d - Watch /a/b recursively to catch any d in any immediate subfolder, even if a new subfolder is added + // /a/b - Watch /a/b recursively to catch changes to anything in any recursive subfoler + // + // We watch a directory without recursion if it contains a wildcard in the file segment of + // the pattern: + // + // /a/b/* - Watch /a/b directly to catch any new file + // /a/b/a?z - Watch /a/b directly to catch any new file matching a?z + const rawExcludeRegex = getRegularExpressionForWildcard(exclude, path, "exclude"); + const excludeRegex = rawExcludeRegex && new RegExp(rawExcludeRegex, useCaseSensitiveFileNames ? "" : "i"); + const wildcardDirectories: MapLike = {}; + if (include !== undefined) { + const recursiveKeys: string[] = []; + for (const file of include) { + const spec = normalizePath(combinePaths(path, file)); + if (excludeRegex && excludeRegex.test(spec)) { + continue; + } + const match = getWildcardDirectoryFromSpec(spec, useCaseSensitiveFileNames); + if (match) { + const { key, flags } = match; + const existingFlags = wildcardDirectories[key]; + if (existingFlags === undefined || existingFlags < flags) { + wildcardDirectories[key] = flags; + if (flags === WatchDirectoryFlags.Recursive) { + recursiveKeys.push(key); } } } - - // Remove any subpaths under an existing recursively watched directory. - for (const key in wildcardDirectories) { - if (hasProperty(wildcardDirectories, key)) { - for (const recursiveKey of recursiveKeys) { - if (key !== recursiveKey && containsPath(recursiveKey, key, path, !useCaseSensitiveFileNames)) { - delete wildcardDirectories[key]; - } + } + // Remove any subpaths under an existing recursively watched directory. + for (const key in wildcardDirectories) { + if (hasProperty(wildcardDirectories, key)) { + for (const recursiveKey of recursiveKeys) { + if (key !== recursiveKey && containsPath(recursiveKey, key, path, !useCaseSensitiveFileNames)) { + delete wildcardDirectories[key]; } } } } - - return wildcardDirectories; - } - - function getWildcardDirectoryFromSpec(spec: string, useCaseSensitiveFileNames: boolean): { key: string, flags: WatchDirectoryFlags } | undefined { - const match = wildcardDirectoryPattern.exec(spec); - if (match) { - return { - key: useCaseSensitiveFileNames ? match[0] : toFileNameLowerCase(match[0]), - flags: watchRecursivePattern.test(spec) ? WatchDirectoryFlags.Recursive : WatchDirectoryFlags.None - }; - } - if (isImplicitGlob(spec)) { - return { key: spec, flags: WatchDirectoryFlags.Recursive }; - } - return undefined; } - - /** - * Determines whether a literal or wildcard file has already been included that has a higher - * extension priority. - * - * @param file The path to the file. - * @param extensionPriority The priority of the extension. - * @param context The expansion context. - */ - function hasFileWithHigherPriorityExtension(file: string, literalFiles: Map, wildcardFiles: Map, extensions: readonly string[], keyMapper: (value: string) => string) { - const extensionPriority = getExtensionPriority(file, extensions); - const adjustedExtensionPriority = adjustExtensionPriority(extensionPriority, extensions); - for (let i = ExtensionPriority.Highest; i < adjustedExtensionPriority; i++) { - const higherPriorityExtension = extensions[i]; - const higherPriorityPath = keyMapper(changeExtension(file, higherPriorityExtension)); - if (literalFiles.has(higherPriorityPath) || wildcardFiles.has(higherPriorityPath)) { - return true; - } - } - - return false; + return wildcardDirectories; +} +function getWildcardDirectoryFromSpec(spec: string, useCaseSensitiveFileNames: boolean): { + key: string; + flags: WatchDirectoryFlags; +} | undefined { + const match = wildcardDirectoryPattern.exec(spec); + if (match) { + return { + key: useCaseSensitiveFileNames ? match[0] : toFileNameLowerCase(match[0]), + flags: watchRecursivePattern.test(spec) ? WatchDirectoryFlags.Recursive : WatchDirectoryFlags.None + }; } - - /** - * Removes files included via wildcard expansion with a lower extension priority that have - * already been included. - * - * @param file The path to the file. - * @param extensionPriority The priority of the extension. - * @param context The expansion context. - */ - function removeWildcardFilesWithLowerPriorityExtension(file: string, wildcardFiles: Map, extensions: readonly string[], keyMapper: (value: string) => string) { - const extensionPriority = getExtensionPriority(file, extensions); - const nextExtensionPriority = getNextLowestExtensionPriority(extensionPriority, extensions); - for (let i = nextExtensionPriority; i < extensions.length; i++) { - const lowerPriorityExtension = extensions[i]; - const lowerPriorityPath = keyMapper(changeExtension(file, lowerPriorityExtension)); - wildcardFiles.delete(lowerPriorityPath); - } + if (isImplicitGlob(spec)) { + return { key: spec, flags: WatchDirectoryFlags.Recursive }; } - - /** - * Produces a cleaned version of compiler options with personally identifying info (aka, paths) removed. - * Also converts enum values back to strings. - */ - /* @internal */ - export function convertCompilerOptionsForTelemetry(opts: CompilerOptions): CompilerOptions { - const out: CompilerOptions = {}; - for (const key in opts) { - if (opts.hasOwnProperty(key)) { - const type = getOptionFromName(key); - if (type !== undefined) { // Ignore unknown options - out[key] = getOptionValueWithEmptyStrings(opts[key], type); - } + return undefined; +} +/** + * Determines whether a literal or wildcard file has already been included that has a higher + * extension priority. + * + * @param file The path to the file. + * @param extensionPriority The priority of the extension. + * @param context The expansion context. + */ +function hasFileWithHigherPriorityExtension(file: string, literalFiles: ts.Map, wildcardFiles: ts.Map, extensions: readonly string[], keyMapper: (value: string) => string) { + const extensionPriority = getExtensionPriority(file, extensions); + const adjustedExtensionPriority = adjustExtensionPriority(extensionPriority, extensions); + for (let i = ExtensionPriority.Highest; i < adjustedExtensionPriority; i++) { + const higherPriorityExtension = extensions[i]; + const higherPriorityPath = keyMapper(changeExtension(file, higherPriorityExtension)); + if (literalFiles.has(higherPriorityPath) || wildcardFiles.has(higherPriorityPath)) { + return true; + } + } + return false; +} +/** + * Removes files included via wildcard expansion with a lower extension priority that have + * already been included. + * + * @param file The path to the file. + * @param extensionPriority The priority of the extension. + * @param context The expansion context. + */ +function removeWildcardFilesWithLowerPriorityExtension(file: string, wildcardFiles: ts.Map, extensions: readonly string[], keyMapper: (value: string) => string) { + const extensionPriority = getExtensionPriority(file, extensions); + const nextExtensionPriority = getNextLowestExtensionPriority(extensionPriority, extensions); + for (let i = nextExtensionPriority; i < extensions.length; i++) { + const lowerPriorityExtension = extensions[i]; + const lowerPriorityPath = keyMapper(changeExtension(file, lowerPriorityExtension)); + wildcardFiles.delete(lowerPriorityPath); + } +} +/** + * Produces a cleaned version of compiler options with personally identifying info (aka, paths) removed. + * Also converts enum values back to strings. + */ +/* @internal */ +export function convertCompilerOptionsForTelemetry(opts: CompilerOptions): CompilerOptions { + const out: CompilerOptions = {}; + for (const key in opts) { + if (opts.hasOwnProperty(key)) { + const type = getOptionFromName(key); + if (type !== undefined) { // Ignore unknown options + out[key] = getOptionValueWithEmptyStrings(opts[key], type); } } - return out; } - - function getOptionValueWithEmptyStrings(value: any, option: CommandLineOption): {} { - switch (option.type) { - case "object": // "paths". Can't get any useful information from the value since we blank out strings, so just return "". - return ""; - case "string": // Could be any arbitrary string -- use empty string instead. - return ""; - case "number": // Allow numbers, but be sure to check it's actually a number. - return typeof value === "number" ? value : ""; - case "boolean": - return typeof value === "boolean" ? value : ""; - case "list": - const elementType = option.element; - return isArray(value) ? value.map(v => getOptionValueWithEmptyStrings(v, elementType)) : ""; - default: - return forEachEntry(option.type, (optionEnumValue, optionStringValue) => { - if (optionEnumValue === value) { - return optionStringValue; - } - })!; // TODO: GH#18217 - } + return out; +} +function getOptionValueWithEmptyStrings(value: any, option: CommandLineOption): {} { + switch (option.type) { + case "object": // "paths". Can't get any useful information from the value since we blank out strings, so just return "". + return ""; + case "string": // Could be any arbitrary string -- use empty string instead. + return ""; + case "number": // Allow numbers, but be sure to check it's actually a number. + return typeof value === "number" ? value : ""; + case "boolean": + return typeof value === "boolean" ? value : ""; + case "list": + const elementType = option.element; + return isArray(value) ? value.map(v => getOptionValueWithEmptyStrings(v, elementType)) : ""; + default: + return forEachEntry(option.type, (optionEnumValue, optionStringValue) => { + if (optionEnumValue === value) { + return optionStringValue; + } + })!; // TODO: GH#18217 } } diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 1e8326621d111..8540b9ef1a2c6 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1,348 +1,405 @@ - -/* @internal */ -namespace ts { - - export const emptyArray: never[] = [] as never[]; - - /** Create a new map. */ - export function createMap(): Map { - return new Map(); - } - - /** Create a new map from an array of entries. */ - export function createMapFromEntries(entries: [string, T][]): Map { - const map = createMap(); - for (const [key, value] of entries) { - map.set(key, value); - } - return map; - } - - /** Create a new map from a template object is provided, the map will copy entries from it. */ - export function createMapFromTemplate(template: MapLike): Map { - const map: Map = new Map(); - - // Copies keys/values from template. Note that for..in will not throw if - // template is undefined, and instead will just exit the loop. - for (const key in template) { - if (hasOwnProperty.call(template, key)) { - map.set(key, template[key]); - } - } - - return map; +import { MapLike, Debug, EqualityComparer, Comparer, SortedReadonlyArray, Comparison, SortedArray, Push, UnderscoreEscapedMap, __String, TextSpan } from "./ts"; +import * as ts from "./ts"; +/* @internal */ +export const emptyArray: never[] = [] as never[]; +/** Create a new map. */ +/* @internal */ +export function createMap(): ts.Map { + return new ts.Map(); +} +/** Create a new map from an array of entries. */ +/* @internal */ +export function createMapFromEntries(entries: [string, T][]): ts.Map { + const map = createMap(); + for (const [key, value] of entries) { + map.set(key, value); } - - export function length(array: readonly any[] | undefined): number { - return array ? array.length : 0; + return map; +} +/** Create a new map from a template object is provided, the map will copy entries from it. */ +/* @internal */ +export function createMapFromTemplate(template: MapLike): ts.Map { + const map: ts.Map = new ts.Map(); + // Copies keys/values from template. Note that for..in will not throw if + // template is undefined, and instead will just exit the loop. + for (const key in template) { + if (hasOwnProperty.call(template, key)) { + map.set(key, template[key]); + } } - - /** - * 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 forEach(array: readonly T[] | undefined, callback: (element: T, index: number) => U | undefined): U | undefined { - if (array) { - for (let i = 0; i < array.length; i++) { - const result = callback(array[i], i); - if (result) { - return result; - } + return map; +} +/* @internal */ +export function length(array: readonly any[] | undefined): number { + return array ? array.length : 0; +} +/** + * 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. + */ +/* @internal */ +export function forEach(array: readonly T[] | undefined, callback: (element: T, index: number) => U | undefined): U | undefined { + if (array) { + for (let i = 0; i < array.length; i++) { + const result = callback(array[i], i); + if (result) { + return result; } } - return undefined; } - - /** - * Like `forEach`, but iterates in reverse order. - */ - export function forEachRight(array: readonly T[] | undefined, callback: (element: T, index: number) => U | undefined): U | undefined { - if (array) { - for (let i = array.length - 1; i >= 0; i--) { - const result = callback(array[i], i); - if (result) { - return result; - } + return undefined; +} +/** + * Like `forEach`, but iterates in reverse order. + */ +/* @internal */ +export function forEachRight(array: readonly T[] | undefined, callback: (element: T, index: number) => U | undefined): U | undefined { + if (array) { + for (let i = array.length - 1; i >= 0; i--) { + const result = callback(array[i], i); + if (result) { + return result; } } + } + return undefined; +} +/** Like `forEach`, but suitable for use with numbers and strings (which may be falsy). */ +/* @internal */ +export function firstDefined(array: readonly T[] | undefined, callback: (element: T, index: number) => U | undefined): U | undefined { + if (array === undefined) { return undefined; } - - /** Like `forEach`, but suitable for use with numbers and strings (which may be falsy). */ - export function firstDefined(array: readonly T[] | undefined, callback: (element: T, index: number) => U | undefined): U | undefined { - if (array === undefined) { + for (let i = 0; i < array.length; i++) { + const result = callback(array[i], i); + if (result !== undefined) { + return result; + } + } + return undefined; +} +/* @internal */ +export function firstDefinedIterator(iter: ts.Iterator, callback: (element: T) => U | undefined): U | undefined { + while (true) { + const iterResult = iter.next(); + if (iterResult.done) { return undefined; } - - for (let i = 0; i < array.length; i++) { - const result = callback(array[i], i); - if (result !== undefined) { - return result; - } + const result = callback(iterResult.value); + if (result !== undefined) { + return result; } - return undefined; } - - export function firstDefinedIterator(iter: Iterator, callback: (element: T) => U | undefined): U | undefined { - while (true) { - const iterResult = iter.next(); - if (iterResult.done) { - return undefined; - } - const result = callback(iterResult.value); - if (result !== undefined) { - return result; +} +/* @internal */ +export function zipWith(arrayA: readonly T[], arrayB: readonly U[], callback: (a: T, b: U, index: number) => V): V[] { + const result: V[] = []; + Debug.assertEqual(arrayA.length, arrayB.length); + for (let i = 0; i < arrayA.length; i++) { + result.push(callback(arrayA[i], arrayB[i], i)); + } + return result; +} +/* @internal */ +export function zipToIterator(arrayA: readonly T[], arrayB: readonly U[]): ts.Iterator<[T, U]> { + Debug.assertEqual(arrayA.length, arrayB.length); + let i = 0; + return { + next() { + if (i === arrayA.length) { + return { value: undefined as never, done: true }; } + i++; + return { value: [arrayA[i - 1], arrayB[i - 1]] as [T, U], done: false }; } + }; +} +/* @internal */ +export function zipToMap(keys: readonly string[], values: readonly T[]): ts.Map { + Debug.assert(keys.length === values.length); + const map = createMap(); + for (let i = 0; i < keys.length; ++i) { + map.set(keys[i], values[i]); } - - export function zipWith(arrayA: readonly T[], arrayB: readonly U[], callback: (a: T, b: U, index: number) => V): V[] { - const result: V[] = []; - Debug.assertEqual(arrayA.length, arrayB.length); - for (let i = 0; i < arrayA.length; i++) { - result.push(callback(arrayA[i], arrayB[i], i)); + return map; +} +/** + * Iterates through `array` by index and performs the callback on each element of array until the callback + * returns a falsey value, then returns false. + * If no such value is found, the callback is applied to each element of array and `true` is returned. + */ +/* @internal */ +export function every(array: readonly T[], callback: (element: T, index: number) => boolean): boolean { + if (array) { + for (let i = 0; i < array.length; i++) { + if (!callback(array[i], i)) { + return false; + } } - return result; } - - export function zipToIterator(arrayA: readonly T[], arrayB: readonly U[]): Iterator<[T, U]> { - Debug.assertEqual(arrayA.length, arrayB.length); - let i = 0; - return { - next() { - if (i === arrayA.length) { - return { value: undefined as never, done: true }; - } - i++; - return { value: [arrayA[i - 1], arrayB[i - 1]] as [T, U], done: false }; - } - }; + return true; +} +/** Works like Array.prototype.find, returning `undefined` if no element satisfying the predicate is found. */ +/* @internal */ +export function find(array: readonly T[], predicate: (element: T, index: number) => element is U): U | undefined; +/* @internal */ +export function find(array: readonly T[], predicate: (element: T, index: number) => boolean): T | undefined; +/* @internal */ +export function find(array: readonly T[], predicate: (element: T, index: number) => boolean): T | undefined { + for (let i = 0; i < array.length; i++) { + const value = array[i]; + if (predicate(value, i)) { + return value; + } } - - export function zipToMap(keys: readonly string[], values: readonly T[]): Map { - Debug.assert(keys.length === values.length); - const map = createMap(); - for (let i = 0; i < keys.length; ++i) { - map.set(keys[i], values[i]); + return undefined; +} +/* @internal */ +export function findLast(array: readonly T[], predicate: (element: T, index: number) => element is U): U | undefined; +/* @internal */ +export function findLast(array: readonly T[], predicate: (element: T, index: number) => boolean): T | undefined; +/* @internal */ +export function findLast(array: readonly T[], predicate: (element: T, index: number) => boolean): T | undefined { + for (let i = array.length - 1; i >= 0; i--) { + const value = array[i]; + if (predicate(value, i)) { + return value; } - return map; } - - /** - * Iterates through `array` by index and performs the callback on each element of array until the callback - * returns a falsey value, then returns false. - * If no such value is found, the callback is applied to each element of array and `true` is returned. - */ - export function every(array: readonly T[], callback: (element: T, index: number) => boolean): boolean { - if (array) { - for (let i = 0; i < array.length; i++) { - if (!callback(array[i], i)) { - return false; - } - } + return undefined; +} +/** Works like Array.prototype.findIndex, returning `-1` if no element satisfying the predicate is found. */ +/* @internal */ +export function findIndex(array: readonly T[], predicate: (element: T, index: number) => boolean, startIndex?: number): number { + for (let i = startIndex || 0; i < array.length; i++) { + if (predicate(array[i], i)) { + return i; } - - return true; } - - /** Works like Array.prototype.find, returning `undefined` if no element satisfying the predicate is found. */ - export function find(array: readonly T[], predicate: (element: T, index: number) => element is U): U | undefined; - export function find(array: readonly T[], predicate: (element: T, index: number) => boolean): T | undefined; - export function find(array: readonly T[], predicate: (element: T, index: number) => boolean): T | undefined { - for (let i = 0; i < array.length; i++) { - const value = array[i]; - if (predicate(value, i)) { - return value; - } + return -1; +} +/* @internal */ +export function findLastIndex(array: readonly T[], predicate: (element: T, index: number) => boolean, startIndex?: number): number { + for (let i = startIndex === undefined ? array.length - 1 : startIndex; i >= 0; i--) { + if (predicate(array[i], i)) { + return i; } - return undefined; } - - export function findLast(array: readonly T[], predicate: (element: T, index: number) => element is U): U | undefined; - export function findLast(array: readonly T[], predicate: (element: T, index: number) => boolean): T | undefined; - export function findLast(array: readonly T[], predicate: (element: T, index: number) => boolean): T | undefined { - for (let i = array.length - 1; i >= 0; i--) { - const value = array[i]; - if (predicate(value, i)) { - return value; - } + return -1; +} +/** + * Returns the first truthy result of `callback`, or else fails. + * This is like `forEach`, but never returns undefined. + */ +/* @internal */ +export function findMap(array: readonly T[], callback: (element: T, index: number) => U | undefined): U { + for (let i = 0; i < array.length; i++) { + const result = callback(array[i], i); + if (result) { + return result; } - return undefined; } - - /** Works like Array.prototype.findIndex, returning `-1` if no element satisfying the predicate is found. */ - export function findIndex(array: readonly T[], predicate: (element: T, index: number) => boolean, startIndex?: number): number { - for (let i = startIndex || 0; i < array.length; i++) { - if (predicate(array[i], i)) { - return i; + return Debug.fail(); +} +/* @internal */ +export function contains(array: readonly T[] | undefined, value: T, equalityComparer: EqualityComparer = equateValues): boolean { + if (array) { + for (const v of array) { + if (equalityComparer(v, value)) { + return true; } } - return -1; } - - export function findLastIndex(array: readonly T[], predicate: (element: T, index: number) => boolean, startIndex?: number): number { - for (let i = startIndex === undefined ? array.length - 1 : startIndex; i >= 0; i--) { - if (predicate(array[i], i)) { - return i; - } + return false; +} +/* @internal */ +export function arraysEqual(a: readonly T[], b: readonly T[], equalityComparer: EqualityComparer = equateValues): boolean { + return a.length === b.length && a.every((x, i) => equalityComparer(x, b[i])); +} +/* @internal */ +export function indexOfAnyCharCode(text: string, charCodes: readonly number[], start?: number): number { + for (let i = start || 0; i < text.length; i++) { + if (contains(charCodes, text.charCodeAt(i))) { + return i; } - return -1; } - - /** - * Returns the first truthy result of `callback`, or else fails. - * This is like `forEach`, but never returns undefined. - */ - export function findMap(array: readonly T[], callback: (element: T, index: number) => U | undefined): U { + return -1; +} +/* @internal */ +export function countWhere(array: readonly T[], predicate: (x: T, i: number) => boolean): number { + let count = 0; + if (array) { for (let i = 0; i < array.length; i++) { - const result = callback(array[i], i); - if (result) { - return result; + const v = array[i]; + if (predicate(v, i)) { + count++; } } - return Debug.fail(); } - - export function contains(array: readonly T[] | undefined, value: T, equalityComparer: EqualityComparer = equateValues): boolean { - if (array) { - for (const v of array) { - if (equalityComparer(v, value)) { - return true; + return count; +} +/** + * Filters an array by a predicate function. Returns the same array instance if the predicate is + * true for all elements, otherwise returns a new array instance containing the filtered subset. + */ +/* @internal */ +export function filter(array: T[], f: (x: T) => x is U): U[]; +/* @internal */ +export function filter(array: T[], f: (x: T) => boolean): T[]; +/* @internal */ +export function filter(array: readonly T[], f: (x: T) => x is U): readonly U[]; +/* @internal */ +export function filter(array: readonly T[], f: (x: T) => boolean): readonly T[]; +/* @internal */ +export function filter(array: T[] | undefined, f: (x: T) => x is U): U[] | undefined; +/* @internal */ +export function filter(array: T[] | undefined, f: (x: T) => boolean): T[] | undefined; +/* @internal */ +export function filter(array: readonly T[] | undefined, f: (x: T) => x is U): readonly U[] | undefined; +/* @internal */ +export function filter(array: readonly T[] | undefined, f: (x: T) => boolean): readonly T[] | undefined; +/* @internal */ +export function filter(array: readonly T[] | undefined, f: (x: T) => boolean): readonly T[] | undefined { + if (array) { + const len = array.length; + let i = 0; + while (i < len && f(array[i])) + i++; + if (i < len) { + const result = array.slice(0, i); + i++; + while (i < len) { + const item = array[i]; + if (f(item)) { + result.push(item); } + i++; } + return result; } - return false; - } - - export function arraysEqual(a: readonly T[], b: readonly T[], equalityComparer: EqualityComparer = equateValues): boolean { - return a.length === b.length && a.every((x, i) => equalityComparer(x, b[i])); } - - export function indexOfAnyCharCode(text: string, charCodes: readonly number[], start?: number): number { - for (let i = start || 0; i < text.length; i++) { - if (contains(charCodes, text.charCodeAt(i))) { - return i; - } + return array; +} +/* @internal */ +export function filterMutate(array: T[], f: (x: T, i: number, array: T[]) => boolean): void { + let outIndex = 0; + for (let i = 0; i < array.length; i++) { + if (f(array[i], i, array)) { + array[outIndex] = array[i]; + outIndex++; } - return -1; } - - export function countWhere(array: readonly T[], predicate: (x: T, i: number) => boolean): number { - let count = 0; - if (array) { - for (let i = 0; i < array.length; i++) { - const v = array[i]; - if (predicate(v, i)) { - count++; - } - } + array.length = outIndex; +} +/* @internal */ +export function clear(array: {}[]): void { + array.length = 0; +} +/* @internal */ +export function map(array: readonly T[], f: (x: T, i: number) => U): U[]; +/* @internal */ +export function map(array: readonly T[] | undefined, f: (x: T, i: number) => U): U[] | undefined; +/* @internal */ +export function map(array: readonly T[] | undefined, f: (x: T, i: number) => U): U[] | undefined { + let result: U[] | undefined; + if (array) { + result = []; + for (let i = 0; i < array.length; i++) { + result.push(f(array[i], i)); } - return count; } - - /** - * Filters an array by a predicate function. Returns the same array instance if the predicate is - * true for all elements, otherwise returns a new array instance containing the filtered subset. - */ - export function filter(array: T[], f: (x: T) => x is U): U[]; - export function filter(array: T[], f: (x: T) => boolean): T[]; - export function filter(array: readonly T[], f: (x: T) => x is U): readonly U[]; - export function filter(array: readonly T[], f: (x: T) => boolean): readonly T[]; - export function filter(array: T[] | undefined, f: (x: T) => x is U): U[] | undefined; - export function filter(array: T[] | undefined, f: (x: T) => boolean): T[] | undefined; - export function filter(array: readonly T[] | undefined, f: (x: T) => x is U): readonly U[] | undefined; - export function filter(array: readonly T[] | undefined, f: (x: T) => boolean): readonly T[] | undefined; - export function filter(array: readonly T[] | undefined, f: (x: T) => boolean): readonly T[] | undefined { - if (array) { - const len = array.length; - let i = 0; - while (i < len && f(array[i])) i++; - if (i < len) { + return result; +} +/* @internal */ +export function mapIterator(iter: ts.Iterator, mapFn: (x: T) => U): ts.Iterator { + return { + next() { + const iterRes = iter.next(); + return iterRes.done ? iterRes as { + done: true; + value: never; + } : { value: mapFn(iterRes.value), done: false }; + } + }; +} +// Maps from T to T and avoids allocation if all elements map to themselves +/* @internal */ +export function sameMap(array: T[], f: (x: T, i: number) => T): T[]; +/* @internal */ +export function sameMap(array: readonly T[], f: (x: T, i: number) => T): readonly T[]; +/* @internal */ +export function sameMap(array: T[] | undefined, f: (x: T, i: number) => T): T[] | undefined; +/* @internal */ +export function sameMap(array: readonly T[] | undefined, f: (x: T, i: number) => T): readonly T[] | undefined; +/* @internal */ +export function sameMap(array: readonly T[] | undefined, f: (x: T, i: number) => T): readonly T[] | undefined { + if (array) { + for (let i = 0; i < array.length; i++) { + const item = array[i]; + const mapped = f(item, i); + if (item !== mapped) { const result = array.slice(0, i); - i++; - while (i < len) { - const item = array[i]; - if (f(item)) { - result.push(item); - } - i++; + result.push(mapped); + for (i++; i < array.length; i++) { + result.push(f(array[i], i)); } return result; } } - return array; } - - export function filterMutate(array: T[], f: (x: T, i: number, array: T[]) => boolean): void { - let outIndex = 0; - for (let i = 0; i < array.length; i++) { - if (f(array[i], i, array)) { - array[outIndex] = array[i]; - outIndex++; + return array; +} +/** + * Flattens an array containing a mix of array or non-array elements. + * + * @param array The array to flatten. + */ +/* @internal */ +export function flatten(array: T[][] | readonly (T | readonly T[] | undefined)[]): T[] { + const result = []; + for (const v of array) { + if (v) { + if (isArray(v)) { + addRange(result, v); } - } - array.length = outIndex; - } - - export function clear(array: {}[]): void { - array.length = 0; - } - - export function map(array: readonly T[], f: (x: T, i: number) => U): U[]; - export function map(array: readonly T[] | undefined, f: (x: T, i: number) => U): U[] | undefined; - export function map(array: readonly T[] | undefined, f: (x: T, i: number) => U): U[] | undefined { - let result: U[] | undefined; - if (array) { - result = []; - for (let i = 0; i < array.length; i++) { - result.push(f(array[i], i)); + else { + result.push(v); } } - return result; } - - - export function mapIterator(iter: Iterator, mapFn: (x: T) => U): Iterator { - return { - next() { - const iterRes = iter.next(); - return iterRes.done ? iterRes as { done: true, value: never } : { value: mapFn(iterRes.value), done: false }; - } - }; - } - - // Maps from T to T and avoids allocation if all elements map to themselves - export function sameMap(array: T[], f: (x: T, i: number) => T): T[]; - export function sameMap(array: readonly T[], f: (x: T, i: number) => T): readonly T[]; - export function sameMap(array: T[] | undefined, f: (x: T, i: number) => T): T[] | undefined; - export function sameMap(array: readonly T[] | undefined, f: (x: T, i: number) => T): readonly T[] | undefined; - export function sameMap(array: readonly T[] | undefined, f: (x: T, i: number) => T): readonly T[] | undefined { - if (array) { - for (let i = 0; i < array.length; i++) { - const item = array[i]; - const mapped = f(item, i); - if (item !== mapped) { - const result = array.slice(0, i); - result.push(mapped); - for (i++; i < array.length; i++) { - result.push(f(array[i], i)); - } - return result; + return result; +} +/** + * Maps an array. If the mapped value is an array, it is spread into the result. + * + * @param array The array to map. + * @param mapfn The callback used to map the result into one or more values. + */ +/* @internal */ +export function flatMap(array: readonly T[] | undefined, mapfn: (x: T, i: number) => U | readonly U[] | undefined): readonly U[] { + let result: U[] | undefined; + if (array) { + for (let i = 0; i < array.length; i++) { + const v = mapfn(array[i], i); + if (v) { + if (isArray(v)) { + result = addRange(result, v); + } + else { + result = append(result, v); } } } - return array; } - - /** - * Flattens an array containing a mix of array or non-array elements. - * - * @param array The array to flatten. - */ - export function flatten(array: T[][] | readonly (T | readonly T[] | undefined)[]): T[] { - const result = []; - for (const v of array) { + return result || emptyArray; +} +/* @internal */ +export function flatMapToMutable(array: readonly T[] | undefined, mapfn: (x: T, i: number) => U | readonly U[] | undefined): U[] { + const result: U[] = []; + if (array) { + for (let i = 0; i < array.length; i++) { + const v = mapfn(array[i], i); if (v) { if (isArray(v)) { addRange(result, v); @@ -352,572 +409,552 @@ namespace ts { } } } - return result; } - - /** - * Maps an array. If the mapped value is an array, it is spread into the result. - * - * @param array The array to map. - * @param mapfn The callback used to map the result into one or more values. - */ - export function flatMap(array: readonly T[] | undefined, mapfn: (x: T, i: number) => U | readonly U[] | undefined): readonly U[] { - let result: U[] | undefined; - if (array) { - for (let i = 0; i < array.length; i++) { - const v = mapfn(array[i], i); - if (v) { - if (isArray(v)) { - result = addRange(result, v); - } - else { - result = append(result, v); - } + return result; +} +/* @internal */ +export function flatMapIterator(iter: ts.Iterator, mapfn: (x: T) => readonly U[] | ts.Iterator | undefined): ts.Iterator { + const first = iter.next(); + if (first.done) { + return emptyIterator; + } + let currentIter = getIterator(first.value); + return { + next() { + while (true) { + const currentRes = currentIter.next(); + if (!currentRes.done) { + return currentRes; + } + const iterRes = iter.next(); + if (iterRes.done) { + return iterRes as { + done: true; + value: never; + }; } + currentIter = getIterator(iterRes.value); } - } - return result || emptyArray; + }, + }; + function getIterator(x: T): ts.Iterator { + const res = mapfn(x); + return res === undefined ? emptyIterator : isArray(res) ? arrayIterator(res) : res; } - - export function flatMapToMutable(array: readonly T[] | undefined, mapfn: (x: T, i: number) => U | readonly U[] | undefined): U[] { - const result: U[] = []; - if (array) { - for (let i = 0; i < array.length; i++) { - const v = mapfn(array[i], i); - if (v) { - if (isArray(v)) { - addRange(result, v); - } - else { - result.push(v); - } +} +/** + * Maps an array. If the mapped value is an array, it is spread into the result. + * Avoids allocation if all elements map to themselves. + * + * @param array The array to map. + * @param mapfn The callback used to map the result into one or more values. + */ +/* @internal */ +export function sameFlatMap(array: T[], mapfn: (x: T, i: number) => T | readonly T[]): T[]; +/* @internal */ +export function sameFlatMap(array: readonly T[], mapfn: (x: T, i: number) => T | readonly T[]): readonly T[]; +/* @internal */ +export function sameFlatMap(array: T[], mapfn: (x: T, i: number) => T | T[]): T[] { + let result: T[] | undefined; + if (array) { + for (let i = 0; i < array.length; i++) { + const item = array[i]; + const mapped = mapfn(item, i); + if (result || item !== mapped || isArray(mapped)) { + if (!result) { + result = array.slice(0, i); } - } - } - return result; - } - - export function flatMapIterator(iter: Iterator, mapfn: (x: T) => readonly U[] | Iterator | undefined): Iterator { - const first = iter.next(); - if (first.done) { - return emptyIterator; - } - let currentIter = getIterator(first.value); - return { - next() { - while (true) { - const currentRes = currentIter.next(); - if (!currentRes.done) { - return currentRes; - } - const iterRes = iter.next(); - if (iterRes.done) { - return iterRes as { done: true, value: never }; - } - currentIter = getIterator(iterRes.value); + if (isArray(mapped)) { + addRange(result, mapped); } - }, - }; - - function getIterator(x: T): Iterator { - const res = mapfn(x); - return res === undefined ? emptyIterator : isArray(res) ? arrayIterator(res) : res; - } - } - - /** - * Maps an array. If the mapped value is an array, it is spread into the result. - * Avoids allocation if all elements map to themselves. - * - * @param array The array to map. - * @param mapfn The callback used to map the result into one or more values. - */ - export function sameFlatMap(array: T[], mapfn: (x: T, i: number) => T | readonly T[]): T[]; - export function sameFlatMap(array: readonly T[], mapfn: (x: T, i: number) => T | readonly T[]): readonly T[]; - export function sameFlatMap(array: T[], mapfn: (x: T, i: number) => T | T[]): T[] { - let result: T[] | undefined; - if (array) { - for (let i = 0; i < array.length; i++) { - const item = array[i]; - const mapped = mapfn(item, i); - if (result || item !== mapped || isArray(mapped)) { - if (!result) { - result = array.slice(0, i); - } - if (isArray(mapped)) { - addRange(result, mapped); - } - else { - result.push(mapped); - } + else { + result.push(mapped); } } } - return result || array; } - - export function mapAllOrFail(array: readonly T[], mapFn: (x: T, i: number) => U | undefined): U[] | undefined { - const result: U[] = []; + return result || array; +} +/* @internal */ +export function mapAllOrFail(array: readonly T[], mapFn: (x: T, i: number) => U | undefined): U[] | undefined { + const result: U[] = []; + for (let i = 0; i < array.length; i++) { + const mapped = mapFn(array[i], i); + if (mapped === undefined) { + return undefined; + } + result.push(mapped); + } + return result; +} +/* @internal */ +export function mapDefined(array: readonly T[] | undefined, mapFn: (x: T, i: number) => U | undefined): U[] { + const result: U[] = []; + if (array) { for (let i = 0; i < array.length; i++) { const mapped = mapFn(array[i], i); - if (mapped === undefined) { - return undefined; + if (mapped !== undefined) { + result.push(mapped); } - result.push(mapped); } - return result; } - - export function mapDefined(array: readonly T[] | undefined, mapFn: (x: T, i: number) => U | undefined): U[] { - const result: U[] = []; - if (array) { - for (let i = 0; i < array.length; i++) { - const mapped = mapFn(array[i], i); - if (mapped !== undefined) { - result.push(mapped); + return result; +} +/* @internal */ +export function mapDefinedIterator(iter: ts.Iterator, mapFn: (x: T) => U | undefined): ts.Iterator { + return { + next() { + while (true) { + const res = iter.next(); + if (res.done) { + return res as { + done: true; + value: never; + }; } - } - } - return result; - } - - export function mapDefinedIterator(iter: Iterator, mapFn: (x: T) => U | undefined): Iterator { - return { - next() { - while (true) { - const res = iter.next(); - if (res.done) { - return res as { done: true, value: never }; - } - const value = mapFn(res.value); - if (value !== undefined) { - return { value, done: false }; - } + const value = mapFn(res.value); + if (value !== undefined) { + return { value, done: false }; } } - }; - } - - export function mapDefinedMap(map: ReadonlyMap, mapValue: (value: T, key: string) => U | undefined, mapKey: (key: string) => string = identity): Map { - const result = createMap(); - map.forEach((value, key) => { - const mapped = mapValue(value, key); - if (mapped !== undefined) { - result.set(mapKey(key), mapped); - } - }); - return result; - } - - export const emptyIterator: Iterator = { next: () => ({ value: undefined as never, done: true }) }; - - export function singleIterator(value: T): Iterator { - let done = false; - return { - next() { - const wasDone = done; - done = true; - return wasDone ? { value: undefined as never, done: true } : { value, done: false }; - } - }; - } - - /** - * Maps contiguous spans of values with the same key. - * - * @param array The array to map. - * @param keyfn A callback used to select the key for an element. - * @param mapfn A callback used to map a contiguous chunk of values to a single value. - */ - export function spanMap(array: readonly T[], keyfn: (x: T, i: number) => K, mapfn: (chunk: T[], key: K, start: number, end: number) => U): U[]; - export function spanMap(array: readonly T[] | undefined, keyfn: (x: T, i: number) => K, mapfn: (chunk: T[], key: K, start: number, end: number) => U): U[] | undefined; - export function spanMap(array: readonly T[] | undefined, keyfn: (x: T, i: number) => K, mapfn: (chunk: T[], key: K, start: number, end: number) => U): U[] | undefined { - let result: U[] | undefined; - if (array) { - result = []; - const len = array.length; - let previousKey: K | undefined; - let key: K | undefined; - let start = 0; - let pos = 0; - while (start < len) { - while (pos < len) { - const value = array[pos]; - key = keyfn(value, pos); - if (pos === 0) { - previousKey = key; - } - else if (key !== previousKey) { - break; - } - - pos++; + } + }; +} +/* @internal */ +export function mapDefinedMap(map: ts.ReadonlyMap, mapValue: (value: T, key: string) => U | undefined, mapKey: (key: string) => string = identity): ts.Map { + const result = createMap(); + map.forEach((value, key) => { + const mapped = mapValue(value, key); + if (mapped !== undefined) { + result.set(mapKey(key), mapped); + } + }); + return result; +} +/* @internal */ +export const emptyIterator: ts.Iterator = { next: () => ({ value: undefined as never, done: true }) }; +/* @internal */ +export function singleIterator(value: T): ts.Iterator { + let done = false; + return { + next() { + const wasDone = done; + done = true; + return wasDone ? { value: undefined as never, done: true } : { value, done: false }; + } + }; +} +/** + * Maps contiguous spans of values with the same key. + * + * @param array The array to map. + * @param keyfn A callback used to select the key for an element. + * @param mapfn A callback used to map a contiguous chunk of values to a single value. + */ +/* @internal */ +export function spanMap(array: readonly T[], keyfn: (x: T, i: number) => K, mapfn: (chunk: T[], key: K, start: number, end: number) => U): U[]; +/* @internal */ +export function spanMap(array: readonly T[] | undefined, keyfn: (x: T, i: number) => K, mapfn: (chunk: T[], key: K, start: number, end: number) => U): U[] | undefined; +/* @internal */ +export function spanMap(array: readonly T[] | undefined, keyfn: (x: T, i: number) => K, mapfn: (chunk: T[], key: K, start: number, end: number) => U): U[] | undefined { + let result: U[] | undefined; + if (array) { + result = []; + const len = array.length; + let previousKey: K | undefined; + let key: K | undefined; + let start = 0; + let pos = 0; + while (start < len) { + while (pos < len) { + const value = array[pos]; + key = keyfn(value, pos); + if (pos === 0) { + previousKey = key; } - - if (start < pos) { - const v = mapfn(array.slice(start, pos), previousKey!, start, pos); - if (v) { - result.push(v); - } - - start = pos; + else if (key !== previousKey) { + break; } - - previousKey = key; pos++; } - } - - return result; - } - - export function mapEntries(map: ReadonlyMap, f: (key: string, value: T) => [string, U]): Map; - export function mapEntries(map: ReadonlyMap | undefined, f: (key: string, value: T) => [string, U]): Map | undefined; - export function mapEntries(map: ReadonlyMap | undefined, f: (key: string, value: T) => [string, U]): Map | undefined { - if (!map) { - return undefined; - } - - const result = createMap(); - map.forEach((value, key) => { - const [newKey, newValue] = f(key, value); - result.set(newKey, newValue); - }); - return result; - } - export function some(array: readonly T[] | undefined): array is readonly T[]; - export function some(array: readonly T[] | undefined, predicate: (value: T) => boolean): boolean; - export function some(array: readonly T[] | undefined, predicate?: (value: T) => boolean): boolean { - if (array) { - if (predicate) { - for (const v of array) { - if (predicate(v)) { - return true; - } + if (start < pos) { + const v = mapfn(array.slice(start, pos), previousKey!, start, pos); + if (v) { + result.push(v); } + start = pos; } - else { - return array.length > 0; - } + previousKey = key; + pos++; } - return false; } - - /** Calls the callback with (start, afterEnd) index pairs for each range where 'pred' is true. */ - export function getRangesWhere(arr: readonly T[], pred: (t: T) => boolean, cb: (start: number, afterEnd: number) => void): void { - let start: number | undefined; - for (let i = 0; i < arr.length; i++) { - if (pred(arr[i])) { - start = start === undefined ? i : start; - } - else { - if (start !== undefined) { - cb(start, i); - start = undefined; + return result; +} +/* @internal */ +export function mapEntries(map: ts.ReadonlyMap, f: (key: string, value: T) => [string, U]): ts.Map; +/* @internal */ +export function mapEntries(map: ts.ReadonlyMap | undefined, f: (key: string, value: T) => [string, U]): ts.Map | undefined; +/* @internal */ +export function mapEntries(map: ts.ReadonlyMap | undefined, f: (key: string, value: T) => [string, U]): ts.Map | undefined { + if (!map) { + return undefined; + } + const result = createMap(); + map.forEach((value, key) => { + const [newKey, newValue] = f(key, value); + result.set(newKey, newValue); + }); + return result; +} +/* @internal */ +export function some(array: readonly T[] | undefined): array is readonly T[]; +/* @internal */ +export function some(array: readonly T[] | undefined, predicate: (value: T) => boolean): boolean; +/* @internal */ +export function some(array: readonly T[] | undefined, predicate?: (value: T) => boolean): boolean { + if (array) { + if (predicate) { + for (const v of array) { + if (predicate(v)) { + return true; } } } - if (start !== undefined) cb(start, arr.length); - } - - export function concatenate(array1: T[], array2: T[]): T[]; - export function concatenate(array1: readonly T[], array2: readonly T[]): readonly T[]; - export function concatenate(array1: T[] | undefined, array2: T[] | undefined): T[]; - export function concatenate(array1: readonly T[] | undefined, array2: readonly T[] | undefined): readonly T[]; - export function concatenate(array1: T[], array2: T[]): T[] { - if (!some(array2)) return array1; - if (!some(array1)) return array2; - return [...array1, ...array2]; - } - - function selectIndex(_: unknown, i: number) { - return i; - } - - export function indicesOf(array: readonly unknown[]): number[] { - return array.map(selectIndex); - } - - function deduplicateRelational(array: readonly T[], equalityComparer: EqualityComparer, comparer: Comparer) { - // Perform a stable sort of the array. This ensures the first entry in a list of - // duplicates remains the first entry in the result. - const indices = indicesOf(array); - stableSortIndices(array, indices, comparer); - - let last = array[indices[0]]; - const deduplicated: number[] = [indices[0]]; - for (let i = 1; i < indices.length; i++) { - const index = indices[i]; - const item = array[index]; - if (!equalityComparer(last, item)) { - deduplicated.push(index); - last = item; - } + else { + return array.length > 0; } - - // restore original order - deduplicated.sort(); - return deduplicated.map(i => array[i]); } - - function deduplicateEquality(array: readonly T[], equalityComparer: EqualityComparer) { - const result: T[] = []; - for (const item of array) { - pushIfUnique(result, item, equalityComparer); + return false; +} +/** Calls the callback with (start, afterEnd) index pairs for each range where 'pred' is true. */ +/* @internal */ +export function getRangesWhere(arr: readonly T[], pred: (t: T) => boolean, cb: (start: number, afterEnd: number) => void): void { + let start: number | undefined; + for (let i = 0; i < arr.length; i++) { + if (pred(arr[i])) { + start = start === undefined ? i : start; } - return result; - } - - /** - * Deduplicates an unsorted array. - * @param equalityComparer An `EqualityComparer` used to determine if two values are duplicates. - * @param comparer An optional `Comparer` used to sort entries before comparison, though the - * result will remain in the original order in `array`. - */ - export function deduplicate(array: readonly T[], equalityComparer: EqualityComparer, comparer?: Comparer): T[] { - return array.length === 0 ? [] : - array.length === 1 ? array.slice() : - comparer ? deduplicateRelational(array, equalityComparer, comparer) : - deduplicateEquality(array, equalityComparer); - } - - /** - * Deduplicates an array that has already been sorted. - */ - function deduplicateSorted(array: SortedReadonlyArray, comparer: EqualityComparer | Comparer): SortedReadonlyArray { - if (array.length === 0) return emptyArray as any as SortedReadonlyArray; - - let last = array[0]; - const deduplicated: T[] = [last]; - for (let i = 1; i < array.length; i++) { - const next = array[i]; - switch (comparer(next, last)) { - // equality comparison - case true: - - // relational comparison - // falls through - case Comparison.EqualTo: - continue; - - case Comparison.LessThan: - // If `array` is sorted, `next` should **never** be less than `last`. - return Debug.fail("Array is unsorted."); + else { + if (start !== undefined) { + cb(start, i); + start = undefined; } - - deduplicated.push(last = next); } - - return deduplicated as any as SortedReadonlyArray; } - - export function insertSorted(array: SortedArray, insert: T, compare: Comparer): void { - if (array.length === 0) { - array.push(insert); - return; - } - - const insertIndex = binarySearch(array, insert, identity, compare); - if (insertIndex < 0) { - array.splice(~insertIndex, 0, insert); - } + if (start !== undefined) + cb(start, arr.length); +} +/* @internal */ +export function concatenate(array1: T[], array2: T[]): T[]; +/* @internal */ +export function concatenate(array1: readonly T[], array2: readonly T[]): readonly T[]; +/* @internal */ +export function concatenate(array1: T[] | undefined, array2: T[] | undefined): T[]; +/* @internal */ +export function concatenate(array1: readonly T[] | undefined, array2: readonly T[] | undefined): readonly T[]; +/* @internal */ +export function concatenate(array1: T[], array2: T[]): T[] { + if (!some(array2)) + return array1; + if (!some(array1)) + return array2; + return [...array1, ...array2]; +} +/* @internal */ +function selectIndex(_: unknown, i: number) { + return i; +} +/* @internal */ +export function indicesOf(array: readonly unknown[]): number[] { + return array.map(selectIndex); +} +/* @internal */ +function deduplicateRelational(array: readonly T[], equalityComparer: EqualityComparer, comparer: Comparer) { + // Perform a stable sort of the array. This ensures the first entry in a list of + // duplicates remains the first entry in the result. + const indices = indicesOf(array); + stableSortIndices(array, indices, comparer); + let last = array[indices[0]]; + const deduplicated: number[] = [indices[0]]; + for (let i = 1; i < indices.length; i++) { + const index = indices[i]; + const item = array[index]; + if (!equalityComparer(last, item)) { + deduplicated.push(index); + last = item; + } + } + // restore original order + deduplicated.sort(); + return deduplicated.map(i => array[i]); +} +/* @internal */ +function deduplicateEquality(array: readonly T[], equalityComparer: EqualityComparer) { + const result: T[] = []; + for (const item of array) { + pushIfUnique(result, item, equalityComparer); + } + return result; +} +/** + * Deduplicates an unsorted array. + * @param equalityComparer An `EqualityComparer` used to determine if two values are duplicates. + * @param comparer An optional `Comparer` used to sort entries before comparison, though the + * result will remain in the original order in `array`. + */ +/* @internal */ +export function deduplicate(array: readonly T[], equalityComparer: EqualityComparer, comparer?: Comparer): T[] { + return array.length === 0 ? [] : + array.length === 1 ? array.slice() : + comparer ? deduplicateRelational(array, equalityComparer, comparer) : + deduplicateEquality(array, equalityComparer); +} +/** + * Deduplicates an array that has already been sorted. + */ +/* @internal */ +function deduplicateSorted(array: SortedReadonlyArray, comparer: EqualityComparer | Comparer): SortedReadonlyArray { + if (array.length === 0) + return emptyArray as any as SortedReadonlyArray; + let last = array[0]; + const deduplicated: T[] = [last]; + for (let i = 1; i < array.length; i++) { + const next = array[i]; + switch (comparer(next, last)) { + // equality comparison + case true: + // relational comparison + // falls through + case Comparison.EqualTo: + continue; + case Comparison.LessThan: + // If `array` is sorted, `next` should **never** be less than `last`. + return Debug.fail("Array is unsorted."); + } + deduplicated.push(last = next); + } + return deduplicated as any as SortedReadonlyArray; +} +/* @internal */ +export function insertSorted(array: SortedArray, insert: T, compare: Comparer): void { + if (array.length === 0) { + array.push(insert); + return; } - - export function sortAndDeduplicate(array: readonly string[]): SortedReadonlyArray; - export function sortAndDeduplicate(array: readonly T[], comparer: Comparer, equalityComparer?: EqualityComparer): SortedReadonlyArray; - export function sortAndDeduplicate(array: readonly T[], comparer?: Comparer, equalityComparer?: EqualityComparer): SortedReadonlyArray { - return deduplicateSorted(sort(array, comparer), equalityComparer || comparer || compareStringsCaseSensitive as any as Comparer); + const insertIndex = binarySearch(array, insert, identity, compare); + if (insertIndex < 0) { + array.splice(~insertIndex, 0, insert); } - - export function arrayIsEqualTo(array1: readonly T[] | undefined, array2: readonly T[] | undefined, equalityComparer: (a: T, b: T, index: number) => boolean = equateValues): boolean { - if (!array1 || !array2) { - return array1 === array2; - } - - if (array1.length !== array2.length) { +} +/* @internal */ +export function sortAndDeduplicate(array: readonly string[]): SortedReadonlyArray; +/* @internal */ +export function sortAndDeduplicate(array: readonly T[], comparer: Comparer, equalityComparer?: EqualityComparer): SortedReadonlyArray; +/* @internal */ +export function sortAndDeduplicate(array: readonly T[], comparer?: Comparer, equalityComparer?: EqualityComparer): SortedReadonlyArray { + return deduplicateSorted(sort(array, comparer), equalityComparer || comparer || (compareStringsCaseSensitive as any as Comparer)); +} +/* @internal */ +export function arrayIsEqualTo(array1: readonly T[] | undefined, array2: readonly T[] | undefined, equalityComparer: (a: T, b: T, index: number) => boolean = equateValues): boolean { + if (!array1 || !array2) { + return array1 === array2; + } + if (array1.length !== array2.length) { + return false; + } + for (let i = 0; i < array1.length; i++) { + if (!equalityComparer(array1[i], array2[i], i)) { return false; } - - for (let i = 0; i < array1.length; i++) { - if (!equalityComparer(array1[i], array2[i], i)) { - return false; - } - } - - return true; } - - /** - * Compacts an array, removing any falsey elements. - */ - export function compact(array: (T | undefined | null | false | 0 | "")[]): T[]; - export function compact(array: readonly (T | undefined | null | false | 0 | "")[]): readonly T[]; - // ESLint thinks these can be combined with the above - they cannot; they'd produce higher-priority inferences and prevent the falsey types from being stripped - export function compact(array: T[]): T[]; // eslint-disable-line @typescript-eslint/unified-signatures - export function compact(array: readonly T[]): readonly T[]; // eslint-disable-line @typescript-eslint/unified-signatures - export function compact(array: T[]): T[] { - let result: T[] | undefined; - if (array) { - for (let i = 0; i < array.length; i++) { - const v = array[i]; - if (result || !v) { - if (!result) { - result = array.slice(0, i); - } - if (v) { - result.push(v); - } + return true; +} +/** + * Compacts an array, removing any falsey elements. + */ +/* @internal */ +export function compact(array: (T | undefined | null | false | 0 | "")[]): T[]; +/* @internal */ +export function compact(array: readonly (T | undefined | null | false | 0 | "")[]): readonly T[]; +// ESLint thinks these can be combined with the above - they cannot; they'd produce higher-priority inferences and prevent the falsey types from being stripped +/* @internal */ +export function compact(array: T[]): T[]; // eslint-disable-line @typescript-eslint/unified-signatures +/* @internal */ +export function compact(array: readonly T[]): readonly T[]; // eslint-disable-line @typescript-eslint/unified-signatures +/* @internal */ +export function compact(array: T[]): T[] { + let result: T[] | undefined; + if (array) { + for (let i = 0; i < array.length; i++) { + const v = array[i]; + if (result || !v) { + if (!result) { + result = array.slice(0, i); + } + if (v) { + result.push(v); } } } - return result || array; } - - /** - * Gets the relative complement of `arrayA` with respect to `arrayB`, returning the elements that - * are not present in `arrayA` but are present in `arrayB`. Assumes both arrays are sorted - * based on the provided comparer. - */ - export function relativeComplement(arrayA: T[] | undefined, arrayB: T[] | undefined, comparer: Comparer): T[] | undefined { - if (!arrayB || !arrayA || arrayB.length === 0 || arrayA.length === 0) return arrayB; - const result: T[] = []; - loopB: for (let offsetA = 0, offsetB = 0; offsetB < arrayB.length; offsetB++) { - if (offsetB > 0) { - // Ensure `arrayB` is properly sorted. - Debug.assertGreaterThanOrEqual(comparer(arrayB[offsetB], arrayB[offsetB - 1]), Comparison.EqualTo); - } - - loopA: for (const startA = offsetA; offsetA < arrayA.length; offsetA++) { - if (offsetA > startA) { - // Ensure `arrayA` is properly sorted. We only need to perform this check if - // `offsetA` has changed since we entered the loop. - Debug.assertGreaterThanOrEqual(comparer(arrayA[offsetA], arrayA[offsetA - 1]), Comparison.EqualTo); - } - - switch (comparer(arrayB[offsetB], arrayA[offsetA])) { - case Comparison.LessThan: - // If B is less than A, B does not exist in arrayA. Add B to the result and - // move to the next element in arrayB without changing the current position - // in arrayA. - result.push(arrayB[offsetB]); - continue loopB; - case Comparison.EqualTo: - // If B is equal to A, B exists in arrayA. Move to the next element in - // arrayB without adding B to the result or changing the current position - // in arrayA. - continue loopB; - case Comparison.GreaterThan: - // If B is greater than A, we need to keep looking for B in arrayA. Move to - // the next element in arrayA and recheck. - continue loopA; - } + return result || array; +} +/** + * Gets the relative complement of `arrayA` with respect to `arrayB`, returning the elements that + * are not present in `arrayA` but are present in `arrayB`. Assumes both arrays are sorted + * based on the provided comparer. + */ +/* @internal */ +export function relativeComplement(arrayA: T[] | undefined, arrayB: T[] | undefined, comparer: Comparer): T[] | undefined { + if (!arrayB || !arrayA || arrayB.length === 0 || arrayA.length === 0) + return arrayB; + const result: T[] = []; + loopB: for (let offsetA = 0, offsetB = 0; offsetB < arrayB.length; offsetB++) { + if (offsetB > 0) { + // Ensure `arrayB` is properly sorted. + Debug.assertGreaterThanOrEqual(comparer(arrayB[offsetB], arrayB[offsetB - 1]), Comparison.EqualTo); + } + loopA: for (const startA = offsetA; offsetA < arrayA.length; offsetA++) { + if (offsetA > startA) { + // Ensure `arrayA` is properly sorted. We only need to perform this check if + // `offsetA` has changed since we entered the loop. + Debug.assertGreaterThanOrEqual(comparer(arrayA[offsetA], arrayA[offsetA - 1]), Comparison.EqualTo); + } + switch (comparer(arrayB[offsetB], arrayA[offsetA])) { + case Comparison.LessThan: + // If B is less than A, B does not exist in arrayA. Add B to the result and + // move to the next element in arrayB without changing the current position + // in arrayA. + result.push(arrayB[offsetB]); + continue loopB; + case Comparison.EqualTo: + // If B is equal to A, B exists in arrayA. Move to the next element in + // arrayB without adding B to the result or changing the current position + // in arrayA. + continue loopB; + case Comparison.GreaterThan: + // If B is greater than A, we need to keep looking for B in arrayA. Move to + // the next element in arrayA and recheck. + continue loopA; } } - return result; } - - export function sum, K extends string>(array: readonly T[], prop: K): number { - let result = 0; - for (const v of array) { - result += v[prop]; - } - return result; + return result; +} +/* @internal */ +export function sum, K extends string>(array: readonly T[], prop: K): number { + let result = 0; + for (const v of array) { + result += v[prop]; } - - /** - * Appends a value to an array, returning the array. - * - * @param to The array to which `value` is to be appended. If `to` is `undefined`, a new array - * is created if `value` was appended. - * @param value The value to append to the array. If `value` is `undefined`, nothing is - * appended. - */ - export function append[number] | undefined>(to: TArray, value: TValue): [undefined, undefined] extends [TArray, TValue] ? TArray : NonNullable[number][]; - export function append(to: T[], value: T | undefined): T[]; - export function append(to: T[] | undefined, value: T): T[]; - export function append(to: T[] | undefined, value: T | undefined): T[] | undefined; - export function append(to: Push, value: T | undefined): void; - export function append(to: T[], value: T | undefined): T[] | undefined { - if (value === undefined) return to; - if (to === undefined) return [value]; - to.push(value); + return result; +} +/** + * Appends a value to an array, returning the array. + * + * @param to The array to which `value` is to be appended. If `to` is `undefined`, a new array + * is created if `value` was appended. + * @param value The value to append to the array. If `value` is `undefined`, nothing is + * appended. + */ +/* @internal */ +export function append[number] | undefined>(to: TArray, value: TValue): [undefined, undefined] extends [TArray, TValue] ? TArray : NonNullable[number][]; +/* @internal */ +export function append(to: T[], value: T | undefined): T[]; +/* @internal */ +export function append(to: T[] | undefined, value: T): T[]; +/* @internal */ +export function append(to: T[] | undefined, value: T | undefined): T[] | undefined; +/* @internal */ +export function append(to: Push, value: T | undefined): void; +/* @internal */ +export function append(to: T[], value: T | undefined): T[] | undefined { + if (value === undefined) return to; - } - - /** - * Gets the actual offset into an array for a relative offset. Negative offsets indicate a - * position offset from the end of the array. - */ - function toOffset(array: readonly any[], offset: number) { - return offset < 0 ? array.length + offset : offset; - } - - /** - * Appends a range of value to an array, returning the array. - * - * @param to The array to which `value` is to be appended. If `to` is `undefined`, a new array - * is created if `value` was appended. - * @param from The values to append to the array. If `from` is `undefined`, nothing is - * appended. If an element of `from` is `undefined`, that element is not appended. - * @param start The offset in `from` at which to start copying values. - * @param end The offset in `from` at which to stop copying values (non-inclusive). - */ - export function addRange(to: T[], from: readonly T[] | undefined, start?: number, end?: number): T[]; - export function addRange(to: T[] | undefined, from: readonly T[] | undefined, start?: number, end?: number): T[] | undefined; - export function addRange(to: T[] | undefined, from: readonly T[] | undefined, start?: number, end?: number): T[] | undefined { - if (from === undefined || from.length === 0) return to; - if (to === undefined) return from.slice(start, end); - start = start === undefined ? 0 : toOffset(from, start); - end = end === undefined ? from.length : toOffset(from, end); - for (let i = start; i < end && i < from.length; i++) { - if (from[i] !== undefined) { - to.push(from[i]); - } - } + if (to === undefined) + return [value]; + to.push(value); + return to; +} +/** + * Gets the actual offset into an array for a relative offset. Negative offsets indicate a + * position offset from the end of the array. + */ +/* @internal */ +function toOffset(array: readonly any[], offset: number) { + return offset < 0 ? array.length + offset : offset; +} +/** + * Appends a range of value to an array, returning the array. + * + * @param to The array to which `value` is to be appended. If `to` is `undefined`, a new array + * is created if `value` was appended. + * @param from The values to append to the array. If `from` is `undefined`, nothing is + * appended. If an element of `from` is `undefined`, that element is not appended. + * @param start The offset in `from` at which to start copying values. + * @param end The offset in `from` at which to stop copying values (non-inclusive). + */ +/* @internal */ +export function addRange(to: T[], from: readonly T[] | undefined, start?: number, end?: number): T[]; +/* @internal */ +export function addRange(to: T[] | undefined, from: readonly T[] | undefined, start?: number, end?: number): T[] | undefined; +/* @internal */ +export function addRange(to: T[] | undefined, from: readonly T[] | undefined, start?: number, end?: number): T[] | undefined { + if (from === undefined || from.length === 0) return to; - } - - /** - * @return Whether the value was added. - */ - export function pushIfUnique(array: T[], toAdd: T, equalityComparer?: EqualityComparer): boolean { - if (contains(array, toAdd, equalityComparer)) { - return false; - } - else { - array.push(toAdd); - return true; + if (to === undefined) + return from.slice(start, end); + start = start === undefined ? 0 : toOffset(from, start); + end = end === undefined ? from.length : toOffset(from, end); + for (let i = start; i < end && i < from.length; i++) { + if (from[i] !== undefined) { + to.push(from[i]); } } - - /** - * Unlike `pushIfUnique`, this can take `undefined` as an input, and returns a new array. - */ - export function appendIfUnique(array: T[] | undefined, toAdd: T, equalityComparer?: EqualityComparer): T[] { - if (array) { - pushIfUnique(array, toAdd, equalityComparer); - return array; - } - else { - return [toAdd]; - } + return to; +} +/** + * @return Whether the value was added. + */ +/* @internal */ +export function pushIfUnique(array: T[], toAdd: T, equalityComparer?: EqualityComparer): boolean { + if (contains(array, toAdd, equalityComparer)) { + return false; } - - function stableSortIndices(array: readonly T[], indices: number[], comparer: Comparer) { - // sort indices by value then position - indices.sort((x, y) => comparer(array[x], array[y]) || compareValues(x, y)); + else { + array.push(toAdd); + return true; } - - /** - * Returns a new sorted array. - */ - export function sort(array: readonly T[], comparer?: Comparer): SortedReadonlyArray { - return (array.length === 0 ? array : array.slice().sort(comparer)) as SortedReadonlyArray; +} +/** + * Unlike `pushIfUnique`, this can take `undefined` as an input, and returns a new array. + */ +/* @internal */ +export function appendIfUnique(array: T[] | undefined, toAdd: T, equalityComparer?: EqualityComparer): T[] { + if (array) { + pushIfUnique(array, toAdd, equalityComparer); + return array; } - - export function arrayIterator(array: readonly T[]): Iterator { - let i = 0; - return { next: () => { + else { + return [toAdd]; + } +} +/* @internal */ +function stableSortIndices(array: readonly T[], indices: number[], comparer: Comparer) { + // sort indices by value then position + indices.sort((x, y) => comparer(array[x], array[y]) || compareValues(x, y)); +} +/** + * Returns a new sorted array. + */ +/* @internal */ +export function sort(array: readonly T[], comparer?: Comparer): SortedReadonlyArray { + return (array.length === 0 ? array : array.slice().sort(comparer)) as SortedReadonlyArray; +} +/* @internal */ +export function arrayIterator(array: readonly T[]): ts.Iterator { + let i = 0; + return { next: () => { if (i === array.length) { return { value: undefined as never, done: true }; } @@ -925,1145 +962,1159 @@ namespace ts { i++; return { value: array[i - 1], done: false }; } - }}; - } - - export function arrayReverseIterator(array: readonly T[]): Iterator { - let i = array.length; - return { - next: () => { - if (i === 0) { - return { value: undefined as never, done: true }; - } - else { - i--; - return { value: array[i], done: false }; - } + } }; +} +/* @internal */ +export function arrayReverseIterator(array: readonly T[]): ts.Iterator { + let i = array.length; + return { + next: () => { + if (i === 0) { + return { value: undefined as never, done: true }; } - }; - } - - /** - * Stable sort of an array. Elements equal to each other maintain their relative position in the array. - */ - export function stableSort(array: readonly T[], comparer: Comparer): SortedReadonlyArray { - const indices = indicesOf(array); - stableSortIndices(array, indices, comparer); - return indices.map(i => array[i]) as SortedArray as SortedReadonlyArray; - } - - export function rangeEquals(array1: readonly T[], array2: readonly T[], pos: number, end: number) { - while (pos < end) { - if (array1[pos] !== array2[pos]) { - return false; + else { + i--; + return { value: array[i], done: false }; } - pos++; } - return true; - } - - /** - * Returns the element at a specific offset in an array if non-empty, `undefined` otherwise. - * A negative offset indicates the element should be retrieved from the end of the array. - */ - export function elementAt(array: readonly T[] | undefined, offset: number): T | undefined { - if (array) { - offset = toOffset(array, offset); - if (offset < array.length) { - return array[offset]; - } + }; +} +/** + * Stable sort of an array. Elements equal to each other maintain their relative position in the array. + */ +/* @internal */ +export function stableSort(array: readonly T[], comparer: Comparer): SortedReadonlyArray { + const indices = indicesOf(array); + stableSortIndices(array, indices, comparer); + return indices.map(i => array[i]) as SortedArray as SortedReadonlyArray; +} +/* @internal */ +export function rangeEquals(array1: readonly T[], array2: readonly T[], pos: number, end: number) { + while (pos < end) { + if (array1[pos] !== array2[pos]) { + return false; } - return undefined; - } - - /** - * Returns the first element of an array if non-empty, `undefined` otherwise. - */ - export function firstOrUndefined(array: readonly T[]): T | undefined { - return array.length === 0 ? undefined : array[0]; - } - - export function first(array: readonly T[]): T { - Debug.assert(array.length !== 0); - return array[0]; - } - - /** - * Returns the last element of an array if non-empty, `undefined` otherwise. - */ - export function lastOrUndefined(array: readonly T[]): T | undefined { - return array.length === 0 ? undefined : array[array.length - 1]; - } - - export function last(array: readonly T[]): T { - Debug.assert(array.length !== 0); - return array[array.length - 1]; - } - - /** - * Returns the only element of an array if it contains only one element, `undefined` otherwise. - */ - export function singleOrUndefined(array: readonly T[] | undefined): T | undefined { - return array && array.length === 1 - ? array[0] - : undefined; + pos++; } - - /** - * Returns the only element of an array if it contains only one element; otherwise, returns the - * array. - */ - export function singleOrMany(array: T[]): T | T[]; - export function singleOrMany(array: readonly T[]): T | readonly T[]; - export function singleOrMany(array: T[] | undefined): T | T[] | undefined; - export function singleOrMany(array: readonly T[] | undefined): T | readonly T[] | undefined; - export function singleOrMany(array: readonly T[] | undefined): T | readonly T[] | undefined { - return array && array.length === 1 - ? array[0] - : array; - } - - export function replaceElement(array: readonly T[], index: number, value: T): T[] { - const result = array.slice(0); - result[index] = value; - return result; - } - - /** - * Performs a binary search, finding the index at which `value` occurs in `array`. - * If no such index is found, returns the 2's-complement of first index at which - * `array[index]` exceeds `value`. - * @param array A sorted array whose first element must be no larger than number - * @param value The value to be searched for in the array. - * @param keySelector A callback used to select the search key from `value` and each element of - * `array`. - * @param keyComparer A callback used to compare two keys in a sorted array. - * @param offset An offset into `array` at which to start the search. - */ - export function binarySearch(array: readonly T[], value: T, keySelector: (v: T) => U, keyComparer: Comparer, offset?: number): number { - return binarySearchKey(array, keySelector(value), keySelector, keyComparer, offset); - } - - /** - * Performs a binary search, finding the index at which an object with `key` occurs in `array`. - * If no such index is found, returns the 2's-complement of first index at which - * `array[index]` exceeds `key`. - * @param array A sorted array whose first element must be no larger than number - * @param key The key to be searched for in the array. - * @param keySelector A callback used to select the search key from each element of `array`. - * @param keyComparer A callback used to compare two keys in a sorted array. - * @param offset An offset into `array` at which to start the search. - */ - export function binarySearchKey(array: readonly T[], key: U, keySelector: (v: T) => U, keyComparer: Comparer, offset?: number): number { - if (!some(array)) { - return -1; - } - - let low = offset || 0; - let high = array.length - 1; - while (low <= high) { - const middle = low + ((high - low) >> 1); - const midKey = keySelector(array[middle]); - switch (keyComparer(midKey, key)) { - case Comparison.LessThan: - low = middle + 1; - break; - case Comparison.EqualTo: - return middle; - case Comparison.GreaterThan: - high = middle - 1; - break; - } - } - - return ~low; - } - - export function reduceLeft(array: readonly T[] | undefined, f: (memo: U, value: T, i: number) => U, initial: U, start?: number, count?: number): U; - export function reduceLeft(array: readonly T[], f: (memo: T, value: T, i: number) => T): T | undefined; - export function reduceLeft(array: T[], f: (memo: T, value: T, i: number) => T, initial?: T, start?: number, count?: number): T | undefined { - if (array && array.length > 0) { - const size = array.length; - if (size > 0) { - let pos = start === undefined || start < 0 ? 0 : start; - const end = count === undefined || pos + count > size - 1 ? size - 1 : pos + count; - let result: T; - if (arguments.length <= 2) { - result = array[pos]; - pos++; - } - else { - result = initial!; - } - while (pos <= end) { - result = f(result, array[pos], pos); - pos++; - } - return result; - } + return true; +} +/** + * Returns the element at a specific offset in an array if non-empty, `undefined` otherwise. + * A negative offset indicates the element should be retrieved from the end of the array. + */ +/* @internal */ +export function elementAt(array: readonly T[] | undefined, offset: number): T | undefined { + if (array) { + offset = toOffset(array, offset); + if (offset < array.length) { + return array[offset]; } - return initial; - } - - const hasOwnProperty = Object.prototype.hasOwnProperty; - - /** - * Indicates whether a map-like contains an own property with the specified key. - * - * @param map A map-like. - * @param key A property key. - */ - export function hasProperty(map: MapLike, key: string): boolean { - return hasOwnProperty.call(map, key); - } - - /** - * Gets the value of an owned property in a map-like. - * - * @param map A map-like. - * @param key A property key. - */ - export function getProperty(map: MapLike, key: string): T | undefined { - return hasOwnProperty.call(map, key) ? map[key] : undefined; } - - /** - * Gets the owned, enumerable property keys of a map-like. - */ - export function getOwnKeys(map: MapLike): string[] { - const keys: string[] = []; - for (const key in map) { - if (hasOwnProperty.call(map, key)) { - keys.push(key); - } - } - - return keys; + return undefined; +} +/** + * Returns the first element of an array if non-empty, `undefined` otherwise. + */ +/* @internal */ +export function firstOrUndefined(array: readonly T[]): T | undefined { + return array.length === 0 ? undefined : array[0]; +} +/* @internal */ +export function first(array: readonly T[]): T { + Debug.assert(array.length !== 0); + return array[0]; +} +/** + * Returns the last element of an array if non-empty, `undefined` otherwise. + */ +/* @internal */ +export function lastOrUndefined(array: readonly T[]): T | undefined { + return array.length === 0 ? undefined : array[array.length - 1]; +} +/* @internal */ +export function last(array: readonly T[]): T { + Debug.assert(array.length !== 0); + return array[array.length - 1]; +} +/** + * Returns the only element of an array if it contains only one element, `undefined` otherwise. + */ +/* @internal */ +export function singleOrUndefined(array: readonly T[] | undefined): T | undefined { + return array && array.length === 1 + ? array[0] + : undefined; +} +/** + * Returns the only element of an array if it contains only one element; otherwise, returns the + * array. + */ +/* @internal */ +export function singleOrMany(array: T[]): T | T[]; +/* @internal */ +export function singleOrMany(array: readonly T[]): T | readonly T[]; +/* @internal */ +export function singleOrMany(array: T[] | undefined): T | T[] | undefined; +/* @internal */ +export function singleOrMany(array: readonly T[] | undefined): T | readonly T[] | undefined; +/* @internal */ +export function singleOrMany(array: readonly T[] | undefined): T | readonly T[] | undefined { + return array && array.length === 1 + ? array[0] + : array; +} +/* @internal */ +export function replaceElement(array: readonly T[], index: number, value: T): T[] { + const result = array.slice(0); + result[index] = value; + return result; +} +/** + * Performs a binary search, finding the index at which `value` occurs in `array`. + * If no such index is found, returns the 2's-complement of first index at which + * `array[index]` exceeds `value`. + * @param array A sorted array whose first element must be no larger than number + * @param value The value to be searched for in the array. + * @param keySelector A callback used to select the search key from `value` and each element of + * `array`. + * @param keyComparer A callback used to compare two keys in a sorted array. + * @param offset An offset into `array` at which to start the search. + */ +/* @internal */ +export function binarySearch(array: readonly T[], value: T, keySelector: (v: T) => U, keyComparer: Comparer, offset?: number): number { + return binarySearchKey(array, keySelector(value), keySelector, keyComparer, offset); +} +/** + * Performs a binary search, finding the index at which an object with `key` occurs in `array`. + * If no such index is found, returns the 2's-complement of first index at which + * `array[index]` exceeds `key`. + * @param array A sorted array whose first element must be no larger than number + * @param key The key to be searched for in the array. + * @param keySelector A callback used to select the search key from each element of `array`. + * @param keyComparer A callback used to compare two keys in a sorted array. + * @param offset An offset into `array` at which to start the search. + */ +/* @internal */ +export function binarySearchKey(array: readonly T[], key: U, keySelector: (v: T) => U, keyComparer: Comparer, offset?: number): number { + if (!some(array)) { + return -1; } - - export function getAllKeys(obj: object): string[] { - const result: string[] = []; - do { - const names = Object.getOwnPropertyNames(obj); - for (const name of names) { - pushIfUnique(result, name); + let low = offset || 0; + let high = array.length - 1; + while (low <= high) { + const middle = low + ((high - low) >> 1); + const midKey = keySelector(array[middle]); + switch (keyComparer(midKey, key)) { + case Comparison.LessThan: + low = middle + 1; + break; + case Comparison.EqualTo: + return middle; + case Comparison.GreaterThan: + high = middle - 1; + break; + } + } + return ~low; +} +/* @internal */ +export function reduceLeft(array: readonly T[] | undefined, f: (memo: U, value: T, i: number) => U, initial: U, start?: number, count?: number): U; +/* @internal */ +export function reduceLeft(array: readonly T[], f: (memo: T, value: T, i: number) => T): T | undefined; +/* @internal */ +export function reduceLeft(array: T[], f: (memo: T, value: T, i: number) => T, initial?: T, start?: number, count?: number): T | undefined { + if (array && array.length > 0) { + const size = array.length; + if (size > 0) { + let pos = start === undefined || start < 0 ? 0 : start; + const end = count === undefined || pos + count > size - 1 ? size - 1 : pos + count; + let result: T; + if (arguments.length <= 2) { + result = array[pos]; + pos++; } - } while (obj = Object.getPrototypeOf(obj)); - return result; - } - - export function getOwnValues(sparseArray: T[]): T[] { - const values: T[] = []; - for (const key in sparseArray) { - if (hasOwnProperty.call(sparseArray, key)) { - values.push(sparseArray[key]); + else { + result = initial!; } - } - - return values; - } - - /** Shims `Array.from`. */ - export function arrayFrom(iterator: Iterator | IterableIterator, map: (t: T) => U): U[]; - export function arrayFrom(iterator: Iterator | IterableIterator): T[]; - export function arrayFrom(iterator: Iterator | IterableIterator, map?: (t: T) => U): (T | U)[] { - const result: (T | U)[] = []; - for (let iterResult = iterator.next(); !iterResult.done; iterResult = iterator.next()) { - result.push(map ? map(iterResult.value) : iterResult.value); - } - return result; - } - - - export function assign(t: T, ...args: (T | undefined)[]) { - for (const arg of args) { - if (arg === undefined) continue; - for (const p in arg) { - if (hasProperty(arg, p)) { - t[p] = arg[p]; - } + while (pos <= end) { + result = f(result, array[pos], pos); + pos++; } + return result; } - return t; } - - /** - * Performs a shallow equality comparison of the contents of two map-likes. - * - * @param left A map-like whose properties should be compared. - * @param right A map-like whose properties should be compared. - */ - export function equalOwnProperties(left: MapLike | undefined, right: MapLike | undefined, equalityComparer: EqualityComparer = equateValues) { - if (left === right) return true; - if (!left || !right) return false; - for (const key in left) { - if (hasOwnProperty.call(left, key)) { - if (!hasOwnProperty.call(right, key)) return false; - if (!equalityComparer(left[key], right[key])) return false; - } - } - - for (const key in right) { - if (hasOwnProperty.call(right, key)) { - if (!hasOwnProperty.call(left, key)) return false; - } + return initial; +} +/* @internal */ +const hasOwnProperty = Object.prototype.hasOwnProperty; +/** + * Indicates whether a map-like contains an own property with the specified key. + * + * @param map A map-like. + * @param key A property key. + */ +/* @internal */ +export function hasProperty(map: MapLike, key: string): boolean { + return hasOwnProperty.call(map, key); +} +/** + * Gets the value of an owned property in a map-like. + * + * @param map A map-like. + * @param key A property key. + */ +/* @internal */ +export function getProperty(map: MapLike, key: string): T | undefined { + return hasOwnProperty.call(map, key) ? map[key] : undefined; +} +/** + * Gets the owned, enumerable property keys of a map-like. + */ +/* @internal */ +export function getOwnKeys(map: MapLike): string[] { + const keys: string[] = []; + for (const key in map) { + if (hasOwnProperty.call(map, key)) { + keys.push(key); } - - return true; } - - /** - * Creates a map from the elements of an array. - * - * @param array the array of input elements. - * @param makeKey a function that produces a key for a given element. - * - * This function makes no effort to avoid collisions; if any two elements produce - * the same key with the given 'makeKey' function, then the element with the higher - * index in the array will be the one associated with the produced key. - */ - export function arrayToMap(array: readonly T[], makeKey: (value: T) => string | undefined): Map; - export function arrayToMap(array: readonly T[], makeKey: (value: T) => string | undefined, makeValue: (value: T) => U): Map; - export function arrayToMap(array: readonly T[], makeKey: (value: T) => string | undefined, makeValue: (value: T) => T | U = identity): Map { - const result = createMap(); - for (const value of array) { - const key = makeKey(value); - if (key !== undefined) result.set(key, makeValue(value)); - } - return result; - } - - export function arrayToNumericMap(array: readonly T[], makeKey: (value: T) => number): T[]; - export function arrayToNumericMap(array: readonly T[], makeKey: (value: T) => number, makeValue: (value: T) => U): U[]; - export function arrayToNumericMap(array: readonly T[], makeKey: (value: T) => number, makeValue: (value: T) => T | U = identity): (T | U)[] { - const result: (T | U)[] = []; - for (const value of array) { - result[makeKey(value)] = makeValue(value); - } - return result; - } - - export function arrayToMultiMap(values: readonly T[], makeKey: (value: T) => string): MultiMap; - export function arrayToMultiMap(values: readonly T[], makeKey: (value: T) => string, makeValue: (value: T) => U): MultiMap; - export function arrayToMultiMap(values: readonly T[], makeKey: (value: T) => string, makeValue: (value: T) => T | U = identity): MultiMap { - const result = createMultiMap(); - for (const value of values) { - result.add(makeKey(value), makeValue(value)); - } - return result; - } - - export function group(values: readonly T[], getGroupId: (value: T) => string): readonly (readonly T[])[]; - export function group(values: readonly T[], getGroupId: (value: T) => string, resultSelector: (values: readonly T[]) => R): R[]; - export function group(values: readonly T[], getGroupId: (value: T) => string, resultSelector: (values: readonly T[]) => readonly T[] = identity): readonly (readonly T[])[] { - return arrayFrom(arrayToMultiMap(values, getGroupId).values(), resultSelector); - } - - export function clone(object: T): T { - const result: any = {}; - for (const id in object) { - if (hasOwnProperty.call(object, id)) { - result[id] = (object)[id]; - } + return keys; +} +/* @internal */ +export function getAllKeys(obj: object): string[] { + const result: string[] = []; + do { + const names = Object.getOwnPropertyNames(obj); + for (const name of names) { + pushIfUnique(result, name); + } + } while (obj = Object.getPrototypeOf(obj)); + return result; +} +/* @internal */ +export function getOwnValues(sparseArray: T[]): T[] { + const values: T[] = []; + for (const key in sparseArray) { + if (hasOwnProperty.call(sparseArray, key)) { + values.push(sparseArray[key]); } - return result; } - - /** - * Creates a new object by adding the own properties of `second`, then the own properties of `first`. - * - * NOTE: This means that if a property exists in both `first` and `second`, the property in `first` will be chosen. - */ - export function extend(first: T1, second: T2): T1 & T2 { - const result: T1 & T2 = {}; - for (const id in second) { - if (hasOwnProperty.call(second, id)) { - (result as any)[id] = (second as any)[id]; - } - } - - for (const id in first) { - if (hasOwnProperty.call(first, id)) { - (result as any)[id] = (first as any)[id]; - } - } - - return result; + return values; +} +/** Shims `Array.from`. */ +/* @internal */ +export function arrayFrom(iterator: ts.Iterator | IterableIterator, map: (t: T) => U): U[]; +/* @internal */ +export function arrayFrom(iterator: ts.Iterator | IterableIterator): T[]; +/* @internal */ +export function arrayFrom(iterator: ts.Iterator | IterableIterator, map?: (t: T) => U): (T | U)[] { + const result: (T | U)[] = []; + for (let iterResult = iterator.next(); !iterResult.done; iterResult = iterator.next()) { + result.push(map ? map(iterResult.value) : iterResult.value); } - - export function copyProperties(first: T1, second: T2) { - for (const id in second) { - if (hasOwnProperty.call(second, id)) { - (first as any)[id] = second[id]; + return result; +} +/* @internal */ +export function assign(t: T, ...args: (T | undefined)[]) { + for (const arg of args) { + if (arg === undefined) + continue; + for (const p in arg) { + if (hasProperty(arg, p)) { + t[p] = arg[p]; } } } - - export function maybeBind(obj: T, fn: ((this: T, ...args: A) => R) | undefined): ((...args: A) => R) | undefined { - return fn ? fn.bind(obj) : undefined; - } - - export function mapMap(map: Map, f: (t: T, key: string) => [string, U]): Map; - export function mapMap(map: UnderscoreEscapedMap, f: (t: T, key: __String) => [string, U]): Map; - export function mapMap(map: Map | UnderscoreEscapedMap, f: ((t: T, key: string) => [string, U]) | ((t: T, key: __String) => [string, U])): Map { - const result = createMap(); - map.forEach((t: T, key: string & __String) => result.set(...(f(t, key)))); - return result; - } - - export interface MultiMap extends Map { - /** - * Adds the value to an array of values associated with the key, and returns the array. - * Creates the array if it does not already exist. - */ - add(key: string, value: T): T[]; - /** - * Removes a value from an array of values associated with the key. - * Does not preserve the order of those values. - * Does nothing if `key` is not in `map`, or `value` is not in `map[key]`. - */ - remove(key: string, value: T): void; - } - - export function createMultiMap(): MultiMap { - const map = createMap() as MultiMap; - map.add = multiMapAdd; - map.remove = multiMapRemove; - return map; - } - function multiMapAdd(this: MultiMap, key: string, value: T) { - let values = this.get(key); - if (values) { - values.push(value); - } - else { - this.set(key, values = [value]); + return t; +} +/** + * Performs a shallow equality comparison of the contents of two map-likes. + * + * @param left A map-like whose properties should be compared. + * @param right A map-like whose properties should be compared. + */ +/* @internal */ +export function equalOwnProperties(left: MapLike | undefined, right: MapLike | undefined, equalityComparer: EqualityComparer = equateValues) { + if (left === right) + return true; + if (!left || !right) + return false; + for (const key in left) { + if (hasOwnProperty.call(left, key)) { + if (!hasOwnProperty.call(right, key)) + return false; + if (!equalityComparer(left[key], right[key])) + return false; } - return values; } - function multiMapRemove(this: MultiMap, key: string, value: T) { - const values = this.get(key); - if (values) { - unorderedRemoveItem(values, value); - if (!values.length) { - this.delete(key); - } + for (const key in right) { + if (hasOwnProperty.call(right, key)) { + if (!hasOwnProperty.call(left, key)) + return false; } } - - /** - * Tests whether a value is an array. - */ - export function isArray(value: any): value is readonly {}[] { - return Array.isArray ? Array.isArray(value) : value instanceof Array; - } - - export function toArray(value: T | T[]): T[]; - export function toArray(value: T | readonly T[]): readonly T[]; - export function toArray(value: T | T[]): T[] { - return isArray(value) ? value : [value]; + return true; +} +/** + * Creates a map from the elements of an array. + * + * @param array the array of input elements. + * @param makeKey a function that produces a key for a given element. + * + * This function makes no effort to avoid collisions; if any two elements produce + * the same key with the given 'makeKey' function, then the element with the higher + * index in the array will be the one associated with the produced key. + */ +/* @internal */ +export function arrayToMap(array: readonly T[], makeKey: (value: T) => string | undefined): ts.Map; +/* @internal */ +export function arrayToMap(array: readonly T[], makeKey: (value: T) => string | undefined, makeValue: (value: T) => U): ts.Map; +/* @internal */ +export function arrayToMap(array: readonly T[], makeKey: (value: T) => string | undefined, makeValue: (value: T) => T | U = identity): ts.Map { + const result = createMap(); + for (const value of array) { + const key = makeKey(value); + if (key !== undefined) + result.set(key, makeValue(value)); + } + return result; +} +/* @internal */ +export function arrayToNumericMap(array: readonly T[], makeKey: (value: T) => number): T[]; +/* @internal */ +export function arrayToNumericMap(array: readonly T[], makeKey: (value: T) => number, makeValue: (value: T) => U): U[]; +/* @internal */ +export function arrayToNumericMap(array: readonly T[], makeKey: (value: T) => number, makeValue: (value: T) => T | U = identity): (T | U)[] { + const result: (T | U)[] = []; + for (const value of array) { + result[makeKey(value)] = makeValue(value); } - - /** - * Tests whether a value is string - */ - export function isString(text: unknown): text is string { - return typeof text === "string"; - } - export function isNumber(x: unknown): x is number { - return typeof x === "number"; - } - - export function tryCast(value: TIn | undefined, test: (value: TIn) => value is TOut): TOut | undefined; - export function tryCast(value: T, test: (value: T) => boolean): T | undefined; - export function tryCast(value: T, test: (value: T) => boolean): T | undefined { - return value !== undefined && test(value) ? value : undefined; - } - - export function cast(value: TIn | undefined, test: (value: TIn) => value is TOut): TOut { - if (value !== undefined && test(value)) return value; - - return Debug.fail(`Invalid cast. The supplied value ${value} did not pass the test '${Debug.getFunctionName(test)}'.`); - } - - /** Does nothing. */ - export function noop(_?: {} | null | undefined): void { } - - /** Do nothing and return false */ - export function returnFalse(): false { return false; } - - /** Do nothing and return true */ - export function returnTrue(): true { return true; } - - /** Do nothing and return undefined */ - export function returnUndefined(): undefined { return undefined; } - - /** Returns its argument. */ - export function identity(x: T) { return x; } - - /** Returns lower case string */ - export function toLowerCase(x: string) { return x.toLowerCase(); } - - // We convert the file names to lower case as key for file name on case insensitive file system - // While doing so we need to handle special characters (eg \u0130) to ensure that we dont convert - // it to lower case, fileName with its lowercase form can exist along side it. - // Handle special characters and make those case sensitive instead - // - // |-#--|-Unicode--|-Char code-|-Desc-------------------------------------------------------------------| - // | 1. | i | 105 | Ascii i | - // | 2. | I | 73 | Ascii I | - // |-------- Special characters ------------------------------------------------------------------------| - // | 3. | \u0130 | 304 | Uppper case I with dot above | - // | 4. | i,\u0307 | 105,775 | i, followed by 775: Lower case of (3rd item) | - // | 5. | I,\u0307 | 73,775 | I, followed by 775: Upper case of (4th item), lower case is (4th item) | - // | 6. | \u0131 | 305 | Lower case i without dot, upper case is I (2nd item) | - // | 7. | \u00DF | 223 | Lower case sharp s | - // - // Because item 3 is special where in its lowercase character has its own - // upper case form we cant convert its case. - // Rest special characters are either already in lower case format or - // they have corresponding upper case character so they dont need special handling - // - // But to avoid having to do string building for most common cases, also ignore - // a-z, 0-9, \u0131, \u00DF, \, /, ., : and space - const fileNameLowerCaseRegExp = /[^\u0130\u0131\u00DFa-z0-9\\/:\-_\. ]+/g; - /** - * Case insensitive file systems have descripencies in how they handle some characters (eg. turkish Upper case I with dot on top - \u0130) - * This function is used in places where we want to make file name as a key on these systems - * It is possible on mac to be able to refer to file name with I with dot on top as a fileName with its lower case form - * But on windows we cannot. Windows can have fileName with I with dot on top next to its lower case and they can not each be referred with the lowercase forms - * Technically we would want this function to be platform sepcific as well but - * our api has till now only taken caseSensitive as the only input and just for some characters we dont want to update API and ensure all customers use those api - * We could use upper case and we would still need to deal with the descripencies but - * we want to continue using lower case since in most cases filenames are lowercasewe and wont need any case changes and avoid having to store another string for the key - * So for this function purpose, we go ahead and assume character I with dot on top it as case sensitive since its very unlikely to use lower case form of that special character - */ - export function toFileNameLowerCase(x: string) { - return fileNameLowerCaseRegExp.test(x) ? - x.replace(fileNameLowerCaseRegExp, toLowerCase) : - x; - } - - /** Throws an error because a function is not implemented. */ - export function notImplemented(): never { - throw new Error("Not implemented"); - } - - export function memoize(callback: () => T): () => T { - let value: T; - return () => { - if (callback) { - value = callback(); - callback = undefined!; - } - return value; - }; + return result; +} +/* @internal */ +export function arrayToMultiMap(values: readonly T[], makeKey: (value: T) => string): MultiMap; +/* @internal */ +export function arrayToMultiMap(values: readonly T[], makeKey: (value: T) => string, makeValue: (value: T) => U): MultiMap; +/* @internal */ +export function arrayToMultiMap(values: readonly T[], makeKey: (value: T) => string, makeValue: (value: T) => T | U = identity): MultiMap { + const result = createMultiMap(); + for (const value of values) { + result.add(makeKey(value), makeValue(value)); } - - /** - * High-order function, composes functions. Note that functions are composed inside-out; - * for example, `compose(a, b)` is the equivalent of `x => b(a(x))`. - * - * @param args The functions to compose. - */ - export function compose(...args: ((t: T) => T)[]): (t: T) => T; - export function compose(a: (t: T) => T, b: (t: T) => T, c: (t: T) => T, d: (t: T) => T, e: (t: T) => T): (t: T) => T { - if (!!e) { - const args: ((t: T) => T)[] = []; - for (let i = 0; i < arguments.length; i++) { - args[i] = arguments[i]; - } - - return t => reduceLeft(args, (u, f) => f(u), t); - } - else if (d) { - return t => d(c(b(a(t)))); - } - else if (c) { - return t => c(b(a(t))); - } - else if (b) { - return t => b(a(t)); + return result; +} +/* @internal */ +export function group(values: readonly T[], getGroupId: (value: T) => string): readonly (readonly T[])[]; +/* @internal */ +export function group(values: readonly T[], getGroupId: (value: T) => string, resultSelector: (values: readonly T[]) => R): R[]; +/* @internal */ +export function group(values: readonly T[], getGroupId: (value: T) => string, resultSelector: (values: readonly T[]) => readonly T[] = identity): readonly (readonly T[])[] { + return arrayFrom(arrayToMultiMap(values, getGroupId).values(), resultSelector); +} +/* @internal */ +export function clone(object: T): T { + const result: any = {}; + for (const id in object) { + if (hasOwnProperty.call(object, id)) { + result[id] = (object)[id]; } - else if (a) { - return t => a(t); + } + return result; +} +/** + * Creates a new object by adding the own properties of `second`, then the own properties of `first`. + * + * NOTE: This means that if a property exists in both `first` and `second`, the property in `first` will be chosen. + */ +/* @internal */ +export function extend(first: T1, second: T2): T1 & T2 { + const result: T1 & T2 = {}; + for (const id in second) { + if (hasOwnProperty.call(second, id)) { + (result as any)[id] = (second as any)[id]; } - else { - return t => t; + } + for (const id in first) { + if (hasOwnProperty.call(first, id)) { + (result as any)[id] = (first as any)[id]; } } - - export const enum AssertionLevel { - None = 0, - Normal = 1, - Aggressive = 2, - VeryAggressive = 3, + return result; +} +/* @internal */ +export function copyProperties(first: T1, second: T2) { + for (const id in second) { + if (hasOwnProperty.call(second, id)) { + (first as any)[id] = second[id]; + } } - +} +/* @internal */ +export function maybeBind(obj: T, fn: ((this: T, ...args: A) => R) | undefined): ((...args: A) => R) | undefined { + return fn ? fn.bind(obj) : undefined; +} +/* @internal */ +export function mapMap(map: ts.Map, f: (t: T, key: string) => [string, U]): ts.Map; +/* @internal */ +export function mapMap(map: UnderscoreEscapedMap, f: (t: T, key: __String) => [string, U]): ts.Map; +/* @internal */ +export function mapMap(map: ts.Map | UnderscoreEscapedMap, f: ((t: T, key: string) => [string, U]) | ((t: T, key: __String) => [string, U])): ts.Map { + const result = createMap(); + map.forEach((t: T, key: string & __String) => result.set(...(f(t, key)))); + return result; +} +/* @internal */ +export interface MultiMap extends ts.Map { /** - * Safer version of `Function` which should not be called. - * Every function should be assignable to this, but this should not be assignable to every function. + * Adds the value to an array of values associated with the key, and returns the array. + * Creates the array if it does not already exist. */ - export type AnyFunction = (...args: never[]) => void; - export type AnyConstructor = new (...args: unknown[]) => unknown; - - export function equateValues(a: T, b: T) { - return a === b; - } - + add(key: string, value: T): T[]; /** - * Compare the equality of two strings using a case-sensitive ordinal comparison. - * - * Case-sensitive comparisons compare both strings one code-point at a time using the integer - * value of each code-point after applying `toUpperCase` to each string. We always map both - * strings to their upper-case form as some unicode characters do not properly round-trip to - * lowercase (such as `ẞ` (German sharp capital s)). + * Removes a value from an array of values associated with the key. + * Does not preserve the order of those values. + * Does nothing if `key` is not in `map`, or `value` is not in `map[key]`. */ - export function equateStringsCaseInsensitive(a: string, b: string) { - return a === b - || a !== undefined - && b !== undefined - && a.toUpperCase() === b.toUpperCase(); + remove(key: string, value: T): void; +} +/* @internal */ +export function createMultiMap(): MultiMap { + const map = createMap() as MultiMap; + map.add = multiMapAdd; + map.remove = multiMapRemove; + return map; +} +/* @internal */ +function multiMapAdd(this: MultiMap, key: string, value: T) { + let values = this.get(key); + if (values) { + values.push(value); } - - /** - * Compare the equality of two strings using a case-sensitive ordinal comparison. - * - * Case-sensitive comparisons compare both strings one code-point at a time using the - * integer value of each code-point. - */ - export function equateStringsCaseSensitive(a: string, b: string) { - return equateValues(a, b); - } - - function compareComparableValues(a: string | undefined, b: string | undefined): Comparison; - function compareComparableValues(a: number | undefined, b: number | undefined): Comparison; - function compareComparableValues(a: string | number | undefined, b: string | number | undefined) { - return a === b ? Comparison.EqualTo : - a === undefined ? Comparison.LessThan : - b === undefined ? Comparison.GreaterThan : - a < b ? Comparison.LessThan : - Comparison.GreaterThan; + else { + this.set(key, values = [value]); } - - /** - * Compare two numeric values for their order relative to each other. - * To compare strings, use any of the `compareStrings` functions. - */ - export function compareValues(a: number | undefined, b: number | undefined): Comparison { - return compareComparableValues(a, b); + return values; +} +/* @internal */ +function multiMapRemove(this: MultiMap, key: string, value: T) { + const values = this.get(key); + if (values) { + unorderedRemoveItem(values, value); + if (!values.length) { + this.delete(key); + } } - - /** - * Compare two TextSpans, first by `start`, then by `length`. - */ - export function compareTextSpans(a: Partial | undefined, b: Partial | undefined): Comparison { - return compareValues(a?.start, b?.start) || compareValues(a?.length, b?.length); +} +/** + * Tests whether a value is an array. + */ +/* @internal */ +export function isArray(value: any): value is readonly {}[] { + return Array.isArray ? Array.isArray(value) : value instanceof Array; +} +/* @internal */ +export function toArray(value: T | T[]): T[]; +/* @internal */ +export function toArray(value: T | readonly T[]): readonly T[]; +/* @internal */ +export function toArray(value: T | T[]): T[] { + return isArray(value) ? value : [value]; +} +/** + * Tests whether a value is string + */ +/* @internal */ +export function isString(text: unknown): text is string { + return typeof text === "string"; +} +/* @internal */ +export function isNumber(x: unknown): x is number { + return typeof x === "number"; +} +/* @internal */ +export function tryCast(value: TIn | undefined, test: (value: TIn) => value is TOut): TOut | undefined; +/* @internal */ +export function tryCast(value: T, test: (value: T) => boolean): T | undefined; +/* @internal */ +export function tryCast(value: T, test: (value: T) => boolean): T | undefined { + return value !== undefined && test(value) ? value : undefined; +} +/* @internal */ +export function cast(value: TIn | undefined, test: (value: TIn) => value is TOut): TOut { + if (value !== undefined && test(value)) + return value; + return Debug.fail(`Invalid cast. The supplied value ${value} did not pass the test '${Debug.getFunctionName(test)}'.`); +} +/** Does nothing. */ +/* @internal */ +export function noop(_?: {} | null | undefined): void { } +/** Do nothing and return false */ +/* @internal */ +export function returnFalse(): false { return false; } +/** Do nothing and return true */ +/* @internal */ +export function returnTrue(): true { return true; } +/** Do nothing and return undefined */ +/* @internal */ +export function returnUndefined(): undefined { return undefined; } +/** Returns its argument. */ +/* @internal */ +export function identity(x: T) { return x; } +/** Returns lower case string */ +/* @internal */ +export function toLowerCase(x: string) { return x.toLowerCase(); } +// We convert the file names to lower case as key for file name on case insensitive file system +// While doing so we need to handle special characters (eg \u0130) to ensure that we dont convert +// it to lower case, fileName with its lowercase form can exist along side it. +// Handle special characters and make those case sensitive instead +// +// |-#--|-Unicode--|-Char code-|-Desc-------------------------------------------------------------------| +// | 1. | i | 105 | Ascii i | +// | 2. | I | 73 | Ascii I | +// |-------- Special characters ------------------------------------------------------------------------| +// | 3. | \u0130 | 304 | Uppper case I with dot above | +// | 4. | i,\u0307 | 105,775 | i, followed by 775: Lower case of (3rd item) | +// | 5. | I,\u0307 | 73,775 | I, followed by 775: Upper case of (4th item), lower case is (4th item) | +// | 6. | \u0131 | 305 | Lower case i without dot, upper case is I (2nd item) | +// | 7. | \u00DF | 223 | Lower case sharp s | +// +// Because item 3 is special where in its lowercase character has its own +// upper case form we cant convert its case. +// Rest special characters are either already in lower case format or +// they have corresponding upper case character so they dont need special handling +// +// But to avoid having to do string building for most common cases, also ignore +// a-z, 0-9, \u0131, \u00DF, \, /, ., : and space +/* @internal */ +const fileNameLowerCaseRegExp = /[^\u0130\u0131\u00DFa-z0-9\\/:\-_\. ]+/g; +/** + * Case insensitive file systems have descripencies in how they handle some characters (eg. turkish Upper case I with dot on top - \u0130) + * This function is used in places where we want to make file name as a key on these systems + * It is possible on mac to be able to refer to file name with I with dot on top as a fileName with its lower case form + * But on windows we cannot. Windows can have fileName with I with dot on top next to its lower case and they can not each be referred with the lowercase forms + * Technically we would want this function to be platform sepcific as well but + * our api has till now only taken caseSensitive as the only input and just for some characters we dont want to update API and ensure all customers use those api + * We could use upper case and we would still need to deal with the descripencies but + * we want to continue using lower case since in most cases filenames are lowercasewe and wont need any case changes and avoid having to store another string for the key + * So for this function purpose, we go ahead and assume character I with dot on top it as case sensitive since its very unlikely to use lower case form of that special character + */ +/* @internal */ +export function toFileNameLowerCase(x: string) { + return fileNameLowerCaseRegExp.test(x) ? + x.replace(fileNameLowerCaseRegExp, toLowerCase) : + x; +} +/** Throws an error because a function is not implemented. */ +/* @internal */ +export function notImplemented(): never { + throw new Error("Not implemented"); +} +/* @internal */ +export function memoize(callback: () => T): () => T { + let value: T; + return () => { + if (callback) { + value = callback(); + callback = undefined!; + } + return value; + }; +} +/** + * High-order function, composes functions. Note that functions are composed inside-out; + * for example, `compose(a, b)` is the equivalent of `x => b(a(x))`. + * + * @param args The functions to compose. + */ +/* @internal */ +export function compose(...args: ((t: T) => T)[]): (t: T) => T; +/* @internal */ +export function compose(a: (t: T) => T, b: (t: T) => T, c: (t: T) => T, d: (t: T) => T, e: (t: T) => T): (t: T) => T { + if (!!e) { + const args: ((t: T) => T)[] = []; + for (let i = 0; i < arguments.length; i++) { + args[i] = arguments[i]; + } + return t => reduceLeft(args, (u, f) => f(u), t); } - - export function min(a: T, b: T, compare: Comparer): T { - return compare(a, b) === Comparison.LessThan ? a : b; + else if (d) { + return t => d(c(b(a(t)))); } - - /** - * Compare two strings using a case-insensitive ordinal comparison. - * - * Ordinal comparisons are based on the difference between the unicode code points of both - * strings. Characters with multiple unicode representations are considered unequal. Ordinal - * comparisons provide predictable ordering, but place "a" after "B". - * - * Case-insensitive comparisons compare both strings one code-point at a time using the integer - * value of each code-point after applying `toUpperCase` to each string. We always map both - * strings to their upper-case form as some unicode characters do not properly round-trip to - * lowercase (such as `ẞ` (German sharp capital s)). - */ - export function compareStringsCaseInsensitive(a: string, b: string) { - if (a === b) return Comparison.EqualTo; - if (a === undefined) return Comparison.LessThan; - if (b === undefined) return Comparison.GreaterThan; - a = a.toUpperCase(); - b = b.toUpperCase(); - return a < b ? Comparison.LessThan : a > b ? Comparison.GreaterThan : Comparison.EqualTo; - } - - /** - * Compare two strings using a case-sensitive ordinal comparison. - * - * Ordinal comparisons are based on the difference between the unicode code points of both - * strings. Characters with multiple unicode representations are considered unequal. Ordinal - * comparisons provide predictable ordering, but place "a" after "B". - * - * Case-sensitive comparisons compare both strings one code-point at a time using the integer - * value of each code-point. - */ - export function compareStringsCaseSensitive(a: string | undefined, b: string | undefined): Comparison { - return compareComparableValues(a, b); + else if (c) { + return t => c(b(a(t))); } - - export function getStringComparer(ignoreCase?: boolean) { - return ignoreCase ? compareStringsCaseInsensitive : compareStringsCaseSensitive; + else if (b) { + return t => b(a(t)); } - - /** - * Creates a string comparer for use with string collation in the UI. - */ - const createUIStringComparer = (() => { - let defaultComparer: Comparer | undefined; - let enUSComparer: Comparer | undefined; - - const stringComparerFactory = getStringComparerFactory(); - return createStringComparer; - - function compareWithCallback(a: string | undefined, b: string | undefined, comparer: (a: string, b: string) => number) { - if (a === b) return Comparison.EqualTo; - if (a === undefined) return Comparison.LessThan; - if (b === undefined) return Comparison.GreaterThan; - const value = comparer(a, b); - return value < 0 ? Comparison.LessThan : value > 0 ? Comparison.GreaterThan : Comparison.EqualTo; - } - - function createIntlCollatorStringComparer(locale: string | undefined): Comparer { - // Intl.Collator.prototype.compare is bound to the collator. See NOTE in - // http://www.ecma-international.org/ecma-402/2.0/#sec-Intl.Collator.prototype.compare - const comparer = new Intl.Collator(locale, { usage: "sort", sensitivity: "variant" }).compare; - return (a, b) => compareWithCallback(a, b, comparer); - } - - function createLocaleCompareStringComparer(locale: string | undefined): Comparer { - // if the locale is not the default locale (`undefined`), use the fallback comparer. - if (locale !== undefined) return createFallbackStringComparer(); - - return (a, b) => compareWithCallback(a, b, compareStrings); - - function compareStrings(a: string, b: string) { - return a.localeCompare(b); - } - } - - function createFallbackStringComparer(): Comparer { - // An ordinal comparison puts "A" after "b", but for the UI we want "A" before "b". - // We first sort case insensitively. So "Aaa" will come before "baa". - // Then we sort case sensitively, so "aaa" will come before "Aaa". - // - // For case insensitive comparisons we always map both strings to their - // upper-case form as some unicode characters do not properly round-trip to - // lowercase (such as `ẞ` (German sharp capital s)). - return (a, b) => compareWithCallback(a, b, compareDictionaryOrder); - - function compareDictionaryOrder(a: string, b: string) { - return compareStrings(a.toUpperCase(), b.toUpperCase()) || compareStrings(a, b); - } - - function compareStrings(a: string, b: string) { - return a < b ? Comparison.LessThan : a > b ? Comparison.GreaterThan : Comparison.EqualTo; - } - } - - function getStringComparerFactory() { - // If the host supports Intl, we use it for comparisons using the default locale. - if (typeof Intl === "object" && typeof Intl.Collator === "function") { - return createIntlCollatorStringComparer; - } - - // If the host does not support Intl, we fall back to localeCompare. - // localeCompare in Node v0.10 is just an ordinal comparison, so don't use it. - if (typeof String.prototype.localeCompare === "function" && - typeof String.prototype.toLocaleUpperCase === "function" && - "a".localeCompare("B") < 0) { - return createLocaleCompareStringComparer; - } - - // Otherwise, fall back to ordinal comparison: - return createFallbackStringComparer; - } - - function createStringComparer(locale: string | undefined) { - // Hold onto common string comparers. This avoids constantly reallocating comparers during - // tests. - if (locale === undefined) { - return defaultComparer || (defaultComparer = stringComparerFactory(locale)); - } - else if (locale === "en-US") { - return enUSComparer || (enUSComparer = stringComparerFactory(locale)); - } - else { - return stringComparerFactory(locale); - } - } - })(); - - let uiComparerCaseSensitive: Comparer | undefined; - let uiLocale: string | undefined; - - export function getUILocale() { - return uiLocale; + else if (a) { + return t => a(t); } - - export function setUILocale(value: string | undefined) { - if (uiLocale !== value) { - uiLocale = value; - uiComparerCaseSensitive = undefined; - } + else { + return t => t; } - - /** - * Compare two strings in a using the case-sensitive sort behavior of the UI locale. - * - * Ordering is not predictable between different host locales, but is best for displaying - * ordered data for UI presentation. Characters with multiple unicode representations may - * be considered equal. - * - * Case-sensitive comparisons compare strings that differ in base characters, or - * accents/diacritic marks, or case as unequal. - */ - export function compareStringsCaseSensitiveUI(a: string, b: string) { - const comparer = uiComparerCaseSensitive || (uiComparerCaseSensitive = createUIStringComparer(uiLocale)); - return comparer(a, b); - } - - export function compareProperties(a: T | undefined, b: T | undefined, key: K, comparer: Comparer): Comparison { - return a === b ? Comparison.EqualTo : - a === undefined ? Comparison.LessThan : +} +/* @internal */ +export const enum AssertionLevel { + None = 0, + Normal = 1, + Aggressive = 2, + VeryAggressive = 3 +} +/** + * Safer version of `Function` which should not be called. + * Every function should be assignable to this, but this should not be assignable to every function. + */ +/* @internal */ +export type AnyFunction = (...args: never[]) => void; +/* @internal */ +export type AnyConstructor = new (...args: unknown[]) => unknown; +/* @internal */ +export function equateValues(a: T, b: T) { + return a === b; +} +/** + * Compare the equality of two strings using a case-sensitive ordinal comparison. + * + * Case-sensitive comparisons compare both strings one code-point at a time using the integer + * value of each code-point after applying `toUpperCase` to each string. We always map both + * strings to their upper-case form as some unicode characters do not properly round-trip to + * lowercase (such as `ẞ` (German sharp capital s)). + */ +/* @internal */ +export function equateStringsCaseInsensitive(a: string, b: string) { + return a === b + || a !== undefined + && b !== undefined + && a.toUpperCase() === b.toUpperCase(); +} +/** + * Compare the equality of two strings using a case-sensitive ordinal comparison. + * + * Case-sensitive comparisons compare both strings one code-point at a time using the + * integer value of each code-point. + */ +/* @internal */ +export function equateStringsCaseSensitive(a: string, b: string) { + return equateValues(a, b); +} +/* @internal */ +function compareComparableValues(a: string | undefined, b: string | undefined): Comparison; +/* @internal */ +function compareComparableValues(a: number | undefined, b: number | undefined): Comparison; +/* @internal */ +function compareComparableValues(a: string | number | undefined, b: string | number | undefined) { + return a === b ? Comparison.EqualTo : + a === undefined ? Comparison.LessThan : b === undefined ? Comparison.GreaterThan : - comparer(a[key], b[key]); + a < b ? Comparison.LessThan : + Comparison.GreaterThan; +} +/** + * Compare two numeric values for their order relative to each other. + * To compare strings, use any of the `compareStrings` functions. + */ +/* @internal */ +export function compareValues(a: number | undefined, b: number | undefined): Comparison { + return compareComparableValues(a, b); +} +/** + * Compare two TextSpans, first by `start`, then by `length`. + */ +/* @internal */ +export function compareTextSpans(a: Partial | undefined, b: Partial | undefined): Comparison { + return compareValues(a?.start, b?.start) || compareValues(a?.length, b?.length); +} +/* @internal */ +export function min(a: T, b: T, compare: Comparer): T { + return compare(a, b) === Comparison.LessThan ? a : b; +} +/** + * Compare two strings using a case-insensitive ordinal comparison. + * + * Ordinal comparisons are based on the difference between the unicode code points of both + * strings. Characters with multiple unicode representations are considered unequal. Ordinal + * comparisons provide predictable ordering, but place "a" after "B". + * + * Case-insensitive comparisons compare both strings one code-point at a time using the integer + * value of each code-point after applying `toUpperCase` to each string. We always map both + * strings to their upper-case form as some unicode characters do not properly round-trip to + * lowercase (such as `ẞ` (German sharp capital s)). + */ +/* @internal */ +export function compareStringsCaseInsensitive(a: string, b: string) { + if (a === b) + return Comparison.EqualTo; + if (a === undefined) + return Comparison.LessThan; + if (b === undefined) + return Comparison.GreaterThan; + a = a.toUpperCase(); + b = b.toUpperCase(); + return a < b ? Comparison.LessThan : a > b ? Comparison.GreaterThan : Comparison.EqualTo; +} +/** + * Compare two strings using a case-sensitive ordinal comparison. + * + * Ordinal comparisons are based on the difference between the unicode code points of both + * strings. Characters with multiple unicode representations are considered unequal. Ordinal + * comparisons provide predictable ordering, but place "a" after "B". + * + * Case-sensitive comparisons compare both strings one code-point at a time using the integer + * value of each code-point. + */ +/* @internal */ +export function compareStringsCaseSensitive(a: string | undefined, b: string | undefined): Comparison { + return compareComparableValues(a, b); +} +/* @internal */ +export function getStringComparer(ignoreCase?: boolean) { + return ignoreCase ? compareStringsCaseInsensitive : compareStringsCaseSensitive; +} +/** + * Creates a string comparer for use with string collation in the UI. + */ +/* @internal */ +const createUIStringComparer = (() => { + let defaultComparer: Comparer | undefined; + let enUSComparer: Comparer | undefined; + const stringComparerFactory = getStringComparerFactory(); + return createStringComparer; + function compareWithCallback(a: string | undefined, b: string | undefined, comparer: (a: string, b: string) => number) { + if (a === b) + return Comparison.EqualTo; + if (a === undefined) + return Comparison.LessThan; + if (b === undefined) + return Comparison.GreaterThan; + const value = comparer(a, b); + return value < 0 ? Comparison.LessThan : value > 0 ? Comparison.GreaterThan : Comparison.EqualTo; + } + function createIntlCollatorStringComparer(locale: string | undefined): Comparer { + // Intl.Collator.prototype.compare is bound to the collator. See NOTE in + // http://www.ecma-international.org/ecma-402/2.0/#sec-Intl.Collator.prototype.compare + const comparer = new Intl.Collator(locale, { usage: "sort", sensitivity: "variant" }).compare; + return (a, b) => compareWithCallback(a, b, comparer); + } + function createLocaleCompareStringComparer(locale: string | undefined): Comparer { + // if the locale is not the default locale (`undefined`), use the fallback comparer. + if (locale !== undefined) + return createFallbackStringComparer(); + return (a, b) => compareWithCallback(a, b, compareStrings); + function compareStrings(a: string, b: string) { + return a.localeCompare(b); + } + } + function createFallbackStringComparer(): Comparer { + // An ordinal comparison puts "A" after "b", but for the UI we want "A" before "b". + // We first sort case insensitively. So "Aaa" will come before "baa". + // Then we sort case sensitively, so "aaa" will come before "Aaa". + // + // For case insensitive comparisons we always map both strings to their + // upper-case form as some unicode characters do not properly round-trip to + // lowercase (such as `ẞ` (German sharp capital s)). + return (a, b) => compareWithCallback(a, b, compareDictionaryOrder); + function compareDictionaryOrder(a: string, b: string) { + return compareStrings(a.toUpperCase(), b.toUpperCase()) || compareStrings(a, b); + } + function compareStrings(a: string, b: string) { + return a < b ? Comparison.LessThan : a > b ? Comparison.GreaterThan : Comparison.EqualTo; + } + } + function getStringComparerFactory() { + // If the host supports Intl, we use it for comparisons using the default locale. + if (typeof Intl === "object" && typeof Intl.Collator === "function") { + return createIntlCollatorStringComparer; + } + // If the host does not support Intl, we fall back to localeCompare. + // localeCompare in Node v0.10 is just an ordinal comparison, so don't use it. + if (typeof String.prototype.localeCompare === "function" && + typeof String.prototype.toLocaleUpperCase === "function" && + "a".localeCompare("B") < 0) { + return createLocaleCompareStringComparer; + } + // Otherwise, fall back to ordinal comparison: + return createFallbackStringComparer; + } + function createStringComparer(locale: string | undefined) { + // Hold onto common string comparers. This avoids constantly reallocating comparers during + // tests. + if (locale === undefined) { + return defaultComparer || (defaultComparer = stringComparerFactory(locale)); + } + else if (locale === "en-US") { + return enUSComparer || (enUSComparer = stringComparerFactory(locale)); + } + else { + return stringComparerFactory(locale); + } } - - /** True is greater than false. */ - export function compareBooleans(a: boolean, b: boolean): Comparison { - return compareValues(a ? 1 : 0, b ? 1 : 0); +})(); +/* @internal */ +let uiComparerCaseSensitive: Comparer | undefined; +/* @internal */ +let uiLocale: string | undefined; +/* @internal */ +export function getUILocale() { + return uiLocale; +} +/* @internal */ +export function setUILocale(value: string | undefined) { + if (uiLocale !== value) { + uiLocale = value; + uiComparerCaseSensitive = undefined; } - - /** - * Given a name and a list of names that are *not* equal to the name, return a spelling suggestion if there is one that is close enough. - * Names less than length 3 only check for case-insensitive equality, not Levenshtein distance. - * - * If there is a candidate that's the same except for case, return that. - * If there is a candidate that's within one edit of the name, return that. - * Otherwise, return the candidate with the smallest Levenshtein distance, - * except for candidates: - * * With no name - * * Whose length differs from the target name by more than 0.34 of the length of the name. - * * Whose levenshtein distance is more than 0.4 of the length of the name - * (0.4 allows 1 substitution/transposition for every 5 characters, - * and 1 insertion/deletion at 3 characters) - */ - export function getSpellingSuggestion(name: string, candidates: T[], getName: (candidate: T) => string | undefined): T | undefined { - const maximumLengthDifference = Math.min(2, Math.floor(name.length * 0.34)); - let bestDistance = Math.floor(name.length * 0.4) + 1; // If the best result isn't better than this, don't bother. - let bestCandidate: T | undefined; - let justCheckExactMatches = false; - const nameLowerCase = name.toLowerCase(); - for (const candidate of candidates) { - const candidateName = getName(candidate); - if (candidateName !== undefined && Math.abs(candidateName.length - nameLowerCase.length) <= maximumLengthDifference) { - const candidateNameLowerCase = candidateName.toLowerCase(); - if (candidateNameLowerCase === nameLowerCase) { - if (candidateName === name) { - continue; - } - return candidate; - } - if (justCheckExactMatches) { - continue; - } - if (candidateName.length < 3) { - // Don't bother, user would have noticed a 2-character name having an extra character - continue; - } - // Only care about a result better than the best so far. - const distance = levenshteinWithMax(nameLowerCase, candidateNameLowerCase, bestDistance - 1); - if (distance === undefined) { +} +/** + * Compare two strings in a using the case-sensitive sort behavior of the UI locale. + * + * Ordering is not predictable between different host locales, but is best for displaying + * ordered data for UI presentation. Characters with multiple unicode representations may + * be considered equal. + * + * Case-sensitive comparisons compare strings that differ in base characters, or + * accents/diacritic marks, or case as unequal. + */ +/* @internal */ +export function compareStringsCaseSensitiveUI(a: string, b: string) { + const comparer = uiComparerCaseSensitive || (uiComparerCaseSensitive = createUIStringComparer(uiLocale)); + return comparer(a, b); +} +/* @internal */ +export function compareProperties(a: T | undefined, b: T | undefined, key: K, comparer: Comparer): Comparison { + return a === b ? Comparison.EqualTo : + a === undefined ? Comparison.LessThan : + b === undefined ? Comparison.GreaterThan : + comparer(a[key], b[key]); +} +/** True is greater than false. */ +/* @internal */ +export function compareBooleans(a: boolean, b: boolean): Comparison { + return compareValues(a ? 1 : 0, b ? 1 : 0); +} +/** + * Given a name and a list of names that are *not* equal to the name, return a spelling suggestion if there is one that is close enough. + * Names less than length 3 only check for case-insensitive equality, not Levenshtein distance. + * + * If there is a candidate that's the same except for case, return that. + * If there is a candidate that's within one edit of the name, return that. + * Otherwise, return the candidate with the smallest Levenshtein distance, + * except for candidates: + * * With no name + * * Whose length differs from the target name by more than 0.34 of the length of the name. + * * Whose levenshtein distance is more than 0.4 of the length of the name + * (0.4 allows 1 substitution/transposition for every 5 characters, + * and 1 insertion/deletion at 3 characters) + */ +/* @internal */ +export function getSpellingSuggestion(name: string, candidates: T[], getName: (candidate: T) => string | undefined): T | undefined { + const maximumLengthDifference = Math.min(2, Math.floor(name.length * 0.34)); + let bestDistance = Math.floor(name.length * 0.4) + 1; // If the best result isn't better than this, don't bother. + let bestCandidate: T | undefined; + let justCheckExactMatches = false; + const nameLowerCase = name.toLowerCase(); + for (const candidate of candidates) { + const candidateName = getName(candidate); + if (candidateName !== undefined && Math.abs(candidateName.length - nameLowerCase.length) <= maximumLengthDifference) { + const candidateNameLowerCase = candidateName.toLowerCase(); + if (candidateNameLowerCase === nameLowerCase) { + if (candidateName === name) { continue; } - if (distance < 3) { - justCheckExactMatches = true; - bestCandidate = candidate; - } - else { - Debug.assert(distance < bestDistance); // Else `levenshteinWithMax` should return undefined - bestDistance = distance; - bestCandidate = candidate; - } + return candidate; } - } - return bestCandidate; - } - - function levenshteinWithMax(s1: string, s2: string, max: number): number | undefined { - let previous = new Array(s2.length + 1); - let current = new Array(s2.length + 1); - /** Represents any value > max. We don't care about the particular value. */ - const big = max + 1; - - for (let i = 0; i <= s2.length; i++) { - previous[i] = i; - } - - for (let i = 1; i <= s1.length; i++) { - const c1 = s1.charCodeAt(i - 1); - const minJ = i > max ? i - max : 1; - const maxJ = s2.length > max + i ? max + i : s2.length; - current[0] = i; - /** Smallest value of the matrix in the ith column. */ - let colMin = i; - for (let j = 1; j < minJ; j++) { - current[j] = big; + if (justCheckExactMatches) { + continue; + } + if (candidateName.length < 3) { + // Don't bother, user would have noticed a 2-character name having an extra character + continue; } - for (let j = minJ; j <= maxJ; j++) { - const dist = c1 === s2.charCodeAt(j - 1) - ? previous[j - 1] - : Math.min(/*delete*/ previous[j] + 1, /*insert*/ current[j - 1] + 1, /*substitute*/ previous[j - 1] + 2); - current[j] = dist; - colMin = Math.min(colMin, dist); + // Only care about a result better than the best so far. + const distance = levenshteinWithMax(nameLowerCase, candidateNameLowerCase, bestDistance - 1); + if (distance === undefined) { + continue; } - for (let j = maxJ + 1; j <= s2.length; j++) { - current[j] = big; + if (distance < 3) { + justCheckExactMatches = true; + bestCandidate = candidate; } - if (colMin > max) { - // Give up -- everything in this column is > max and it can't get better in future columns. - return undefined; + else { + Debug.assert(distance < bestDistance); // Else `levenshteinWithMax` should return undefined + bestDistance = distance; + bestCandidate = candidate; } - - const temp = previous; - previous = current; - current = temp; } - - const res = previous[s2.length]; - return res > max ? undefined : res; } - - export function endsWith(str: string, suffix: string): boolean { - const expectedPos = str.length - suffix.length; - return expectedPos >= 0 && str.indexOf(suffix, expectedPos) === expectedPos; - } - - export function removeSuffix(str: string, suffix: string): string { - return endsWith(str, suffix) ? str.slice(0, str.length - suffix.length) : str; + return bestCandidate; +} +/* @internal */ +function levenshteinWithMax(s1: string, s2: string, max: number): number | undefined { + let previous = new Array(s2.length + 1); + let current = new Array(s2.length + 1); + /** Represents any value > max. We don't care about the particular value. */ + const big = max + 1; + for (let i = 0; i <= s2.length; i++) { + previous[i] = i; + } + for (let i = 1; i <= s1.length; i++) { + const c1 = s1.charCodeAt(i - 1); + const minJ = i > max ? i - max : 1; + const maxJ = s2.length > max + i ? max + i : s2.length; + current[0] = i; + /** Smallest value of the matrix in the ith column. */ + let colMin = i; + for (let j = 1; j < minJ; j++) { + current[j] = big; + } + for (let j = minJ; j <= maxJ; j++) { + const dist = c1 === s2.charCodeAt(j - 1) + ? previous[j - 1] + : Math.min(/*delete*/ previous[j] + 1, /*insert*/ current[j - 1] + 1, /*substitute*/ previous[j - 1] + 2); + current[j] = dist; + colMin = Math.min(colMin, dist); + } + for (let j = maxJ + 1; j <= s2.length; j++) { + current[j] = big; + } + if (colMin > max) { + // Give up -- everything in this column is > max and it can't get better in future columns. + return undefined; + } + const temp = previous; + previous = current; + current = temp; } - - export function tryRemoveSuffix(str: string, suffix: string): string | undefined { - return endsWith(str, suffix) ? str.slice(0, str.length - suffix.length) : undefined; + const res = previous[s2.length]; + return res > max ? undefined : res; +} +/* @internal */ +export function endsWith(str: string, suffix: string): boolean { + const expectedPos = str.length - suffix.length; + return expectedPos >= 0 && str.indexOf(suffix, expectedPos) === expectedPos; +} +/* @internal */ +export function removeSuffix(str: string, suffix: string): string { + return endsWith(str, suffix) ? str.slice(0, str.length - suffix.length) : str; +} +/* @internal */ +export function tryRemoveSuffix(str: string, suffix: string): string | undefined { + return endsWith(str, suffix) ? str.slice(0, str.length - suffix.length) : undefined; +} +/* @internal */ +export function stringContains(str: string, substring: string): boolean { + return str.indexOf(substring) !== -1; +} +/** + * Takes a string like "jquery-min.4.2.3" and returns "jquery" + */ +/* @internal */ +export function removeMinAndVersionNumbers(fileName: string) { + // Match a "." or "-" followed by a version number or 'min' at the end of the name + const trailingMinOrVersion = /[.-]((min)|(\d+(\.\d+)*))$/; + // The "min" or version may both be present, in either order, so try applying the above twice. + return fileName.replace(trailingMinOrVersion, "").replace(trailingMinOrVersion, ""); +} +/** Remove an item from an array, moving everything to its right one space left. */ +/* @internal */ +export function orderedRemoveItem(array: T[], item: T): boolean { + for (let i = 0; i < array.length; i++) { + if (array[i] === item) { + orderedRemoveItemAt(array, i); + return true; + } } - - export function stringContains(str: string, substring: string): boolean { - return str.indexOf(substring) !== -1; + return false; +} +/** Remove an item by index from an array, moving everything to its right one space left. */ +/* @internal */ +export function orderedRemoveItemAt(array: T[], index: number): void { + // This seems to be faster than either `array.splice(i, 1)` or `array.copyWithin(i, i+ 1)`. + for (let i = index; i < array.length - 1; i++) { + array[i] = array[i + 1]; } - - /** - * Takes a string like "jquery-min.4.2.3" and returns "jquery" - */ - export function removeMinAndVersionNumbers(fileName: string) { - // Match a "." or "-" followed by a version number or 'min' at the end of the name - const trailingMinOrVersion = /[.-]((min)|(\d+(\.\d+)*))$/; - - // The "min" or version may both be present, in either order, so try applying the above twice. - return fileName.replace(trailingMinOrVersion, "").replace(trailingMinOrVersion, ""); - } - - /** Remove an item from an array, moving everything to its right one space left. */ - export function orderedRemoveItem(array: T[], item: T): boolean { - for (let i = 0; i < array.length; i++) { - if (array[i] === item) { - orderedRemoveItemAt(array, i); - return true; - } + array.pop(); +} +/* @internal */ +export function unorderedRemoveItemAt(array: T[], index: number): void { + // Fill in the "hole" left at `index`. + array[index] = array[array.length - 1]; + array.pop(); +} +/** Remove the *first* occurrence of `item` from the array. */ +/* @internal */ +export function unorderedRemoveItem(array: T[], item: T) { + return unorderedRemoveFirstItemWhere(array, element => element === item); +} +/** Remove the *first* element satisfying `predicate`. */ +/* @internal */ +function unorderedRemoveFirstItemWhere(array: T[], predicate: (element: T) => boolean) { + for (let i = 0; i < array.length; i++) { + if (predicate(array[i])) { + unorderedRemoveItemAt(array, i); + return true; } - return false; } - - /** Remove an item by index from an array, moving everything to its right one space left. */ - export function orderedRemoveItemAt(array: T[], index: number): void { - // This seems to be faster than either `array.splice(i, 1)` or `array.copyWithin(i, i+ 1)`. - for (let i = index; i < array.length - 1; i++) { - array[i] = array[i + 1]; - } - array.pop(); - } - - export function unorderedRemoveItemAt(array: T[], index: number): void { - // Fill in the "hole" left at `index`. - array[index] = array[array.length - 1]; - array.pop(); - } - - /** Remove the *first* occurrence of `item` from the array. */ - export function unorderedRemoveItem(array: T[], item: T) { - return unorderedRemoveFirstItemWhere(array, element => element === item); - } - - /** Remove the *first* element satisfying `predicate`. */ - function unorderedRemoveFirstItemWhere(array: T[], predicate: (element: T) => boolean) { - for (let i = 0; i < array.length; i++) { - if (predicate(array[i])) { - unorderedRemoveItemAt(array, i); + return false; +} +/* @internal */ +export type GetCanonicalFileName = (fileName: string) => string; +/* @internal */ +export function createGetCanonicalFileName(useCaseSensitiveFileNames: boolean): GetCanonicalFileName { + return useCaseSensitiveFileNames ? identity : toFileNameLowerCase; +} +/** Represents a "prefix*suffix" pattern. */ +/* @internal */ +export interface Pattern { + prefix: string; + suffix: string; +} +/* @internal */ +export function patternText({ prefix, suffix }: Pattern): string { + return `${prefix}*${suffix}`; +} +/** + * Given that candidate matches pattern, returns the text matching the '*'. + * E.g.: matchedText(tryParsePattern("foo*baz"), "foobarbaz") === "bar" + */ +/* @internal */ +export function matchedText(pattern: Pattern, candidate: string): string { + Debug.assert(isPatternMatch(pattern, candidate)); + return candidate.substring(pattern.prefix.length, candidate.length - pattern.suffix.length); +} +/** Return the object corresponding to the best pattern to match `candidate`. */ +/* @internal */ +export function findBestPatternMatch(values: readonly T[], getPattern: (value: T) => Pattern, candidate: string): T | undefined { + let matchedValue: T | undefined; + // use length of prefix as betterness criteria + let longestMatchPrefixLength = -1; + for (const v of values) { + const pattern = getPattern(v); + if (isPatternMatch(pattern, candidate) && pattern.prefix.length > longestMatchPrefixLength) { + longestMatchPrefixLength = pattern.prefix.length; + matchedValue = v; + } + } + return matchedValue; +} +/* @internal */ +export function startsWith(str: string, prefix: string): boolean { + return str.lastIndexOf(prefix, 0) === 0; +} +/* @internal */ +export function removePrefix(str: string, prefix: string): string { + return startsWith(str, prefix) ? str.substr(prefix.length) : str; +} +/* @internal */ +export function tryRemovePrefix(str: string, prefix: string, getCanonicalFileName: GetCanonicalFileName = identity): string | undefined { + return startsWith(getCanonicalFileName(str), getCanonicalFileName(prefix)) ? str.substring(prefix.length) : undefined; +} +/* @internal */ +function isPatternMatch({ prefix, suffix }: Pattern, candidate: string) { + return candidate.length >= prefix.length + suffix.length && + startsWith(candidate, prefix) && + endsWith(candidate, suffix); +} +/* @internal */ +export function and(f: (arg: T) => boolean, g: (arg: T) => boolean) { + return (arg: T) => f(arg) && g(arg); +} +/* @internal */ +export function or(...fs: ((...args: T) => boolean)[]): (...args: T) => boolean { + return (...args) => { + for (const f of fs) { + if (f(...args)) { return true; } } return false; - } - - export type GetCanonicalFileName = (fileName: string) => string; - export function createGetCanonicalFileName(useCaseSensitiveFileNames: boolean): GetCanonicalFileName { - return useCaseSensitiveFileNames ? identity : toFileNameLowerCase; - } - - /** Represents a "prefix*suffix" pattern. */ - export interface Pattern { - prefix: string; - suffix: string; - } - - export function patternText({ prefix, suffix }: Pattern): string { - return `${prefix}*${suffix}`; - } - - /** - * Given that candidate matches pattern, returns the text matching the '*'. - * E.g.: matchedText(tryParsePattern("foo*baz"), "foobarbaz") === "bar" - */ - export function matchedText(pattern: Pattern, candidate: string): string { - Debug.assert(isPatternMatch(pattern, candidate)); - return candidate.substring(pattern.prefix.length, candidate.length - pattern.suffix.length); - } - - /** Return the object corresponding to the best pattern to match `candidate`. */ - export function findBestPatternMatch(values: readonly T[], getPattern: (value: T) => Pattern, candidate: string): T | undefined { - let matchedValue: T | undefined; - // use length of prefix as betterness criteria - let longestMatchPrefixLength = -1; - - for (const v of values) { - const pattern = getPattern(v); - if (isPatternMatch(pattern, candidate) && pattern.prefix.length > longestMatchPrefixLength) { - longestMatchPrefixLength = pattern.prefix.length; - matchedValue = v; - } + }; +} +/* @internal */ +export function not(fn: (...args: T) => boolean): (...args: T) => boolean { + return (...args) => !fn(...args); +} +/* @internal */ +export function assertType(_: T): void { } +/* @internal */ +export function singleElementArray(t: T | undefined): T[] | undefined { + return t === undefined ? undefined : [t]; +} +/* @internal */ +export function enumerateInsertsAndDeletes(newItems: readonly T[], oldItems: readonly U[], comparer: (a: T, b: U) => Comparison, inserted: (newItem: T) => void, deleted: (oldItem: U) => void, unchanged?: (oldItem: U, newItem: T) => void) { + unchanged = unchanged || noop; + let newIndex = 0; + let oldIndex = 0; + const newLen = newItems.length; + const oldLen = oldItems.length; + while (newIndex < newLen && oldIndex < oldLen) { + const newItem = newItems[newIndex]; + const oldItem = oldItems[oldIndex]; + const compareResult = comparer(newItem, oldItem); + if (compareResult === Comparison.LessThan) { + inserted(newItem); + newIndex++; + } + else if (compareResult === Comparison.GreaterThan) { + deleted(oldItem); + oldIndex++; + } + else { + unchanged(oldItem, newItem); + newIndex++; + oldIndex++; } - - return matchedValue; - } - - export function startsWith(str: string, prefix: string): boolean { - return str.lastIndexOf(prefix, 0) === 0; - } - - export function removePrefix(str: string, prefix: string): string { - return startsWith(str, prefix) ? str.substr(prefix.length) : str; } - - export function tryRemovePrefix(str: string, prefix: string, getCanonicalFileName: GetCanonicalFileName = identity): string | undefined { - return startsWith(getCanonicalFileName(str), getCanonicalFileName(prefix)) ? str.substring(prefix.length) : undefined; + while (newIndex < newLen) { + inserted(newItems[newIndex++]); } - - function isPatternMatch({ prefix, suffix }: Pattern, candidate: string) { - return candidate.length >= prefix.length + suffix.length && - startsWith(candidate, prefix) && - endsWith(candidate, suffix); + while (oldIndex < oldLen) { + deleted(oldItems[oldIndex++]); } - - export function and(f: (arg: T) => boolean, g: (arg: T) => boolean) { - return (arg: T) => f(arg) && g(arg); +} +/* @internal */ +export function fill(length: number, cb: (index: number) => T): T[] { + const result = Array(length); + for (let i = 0; i < length; i++) { + result[i] = cb(i); } - - export function or(...fs: ((...args: T) => boolean)[]): (...args: T) => boolean { - return (...args) => { - for (const f of fs) { - if (f(...args)) { - return true; - } - } - return false; - }; - } - - export function not(fn: (...args: T) => boolean): (...args: T) => boolean { - return (...args) => !fn(...args); - } - - export function assertType(_: T): void { } - - export function singleElementArray(t: T | undefined): T[] | undefined { - return t === undefined ? undefined : [t]; - } - - export function enumerateInsertsAndDeletes(newItems: readonly T[], oldItems: readonly U[], comparer: (a: T, b: U) => Comparison, inserted: (newItem: T) => void, deleted: (oldItem: U) => void, unchanged?: (oldItem: U, newItem: T) => void) { - unchanged = unchanged || noop; - let newIndex = 0; - let oldIndex = 0; - const newLen = newItems.length; - const oldLen = oldItems.length; - while (newIndex < newLen && oldIndex < oldLen) { - const newItem = newItems[newIndex]; - const oldItem = oldItems[oldIndex]; - const compareResult = comparer(newItem, oldItem); - if (compareResult === Comparison.LessThan) { - inserted(newItem); - newIndex++; - } - else if (compareResult === Comparison.GreaterThan) { - deleted(oldItem); - oldIndex++; - } - else { - unchanged(oldItem, newItem); - newIndex++; - oldIndex++; - } - } - while (newIndex < newLen) { - inserted(newItems[newIndex++]); + return result; +} +/* @internal */ +export function cartesianProduct(arrays: readonly T[][]) { + const result: T[][] = []; + cartesianProductWorker(arrays, result, /*outer*/ undefined, 0); + return result; +} +/* @internal */ +function cartesianProductWorker(arrays: readonly (readonly T[])[], result: (readonly T[])[], outer: readonly T[] | undefined, index: number) { + for (const element of arrays[index]) { + let inner: T[]; + if (outer) { + inner = outer.slice(); + inner.push(element); } - while (oldIndex < oldLen) { - deleted(oldItems[oldIndex++]); + else { + inner = [element]; } - } - - export function fill(length: number, cb: (index: number) => T): T[] { - const result = Array(length); - for (let i = 0; i < length; i++) { - result[i] = cb(i); + if (index === arrays.length - 1) { + result.push(inner); } - return result; - } - - export function cartesianProduct(arrays: readonly T[][]) { - const result: T[][] = []; - cartesianProductWorker(arrays, result, /*outer*/ undefined, 0); - return result; - } - - function cartesianProductWorker(arrays: readonly (readonly T[])[], result: (readonly T[])[], outer: readonly T[] | undefined, index: number) { - for (const element of arrays[index]) { - let inner: T[]; - if (outer) { - inner = outer.slice(); - inner.push(element); - } - else { - inner = [element]; - } - if (index === arrays.length - 1) { - result.push(inner); - } - else { - cartesianProductWorker(arrays, result, inner, index + 1); - } + else { + cartesianProductWorker(arrays, result, inner, index + 1); } } - - export function padLeft(s: string, length: number) { - while (s.length < length) { - s = " " + s; - } - return s; +} +/* @internal */ +export function padLeft(s: string, length: number) { + while (s.length < length) { + s = " " + s; } - - export function padRight(s: string, length: number) { - while (s.length < length) { - s = s + " "; - } - - return s; + return s; +} +/* @internal */ +export function padRight(s: string, length: number) { + while (s.length < length) { + s = s + " "; } + return s; } diff --git a/src/compiler/corePublic.ts b/src/compiler/corePublic.ts index c3a258cd84edb..0c1c1246108f7 100644 --- a/src/compiler/corePublic.ts +++ b/src/compiler/corePublic.ts @@ -1,96 +1,88 @@ -namespace ts { - // WARNING: The script `configurePrerelease.ts` uses a regexp to parse out these values. - // If changing the text in this section, be sure to test `configurePrerelease` too. - export const versionMajorMinor = "3.9"; - /** The version of the TypeScript compiler release */ - export const version = `${versionMajorMinor}.0-dev`; - - /** - * Type of objects whose values are all of the same type. - * The `in` and `for-in` operators can *not* be safely used, - * since `Object.prototype` may be modified by outside code. - */ - export interface MapLike { - [index: string]: T; +import { createMapShim } from "./ts"; +// WARNING: The script `configurePrerelease.ts` uses a regexp to parse out these values. +// If changing the text in this section, be sure to test `configurePrerelease` too. +export const versionMajorMinor = "3.9"; +/** The version of the TypeScript compiler release */ +export const version = `${versionMajorMinor}.0-dev`; +/** + * Type of objects whose values are all of the same type. + * The `in` and `for-in` operators can *not* be safely used, + * since `Object.prototype` may be modified by outside code. + */ +export interface MapLike { + [index: string]: T; +} +export interface SortedReadonlyArray extends ReadonlyArray { + " __sortedArrayBrand": any; +} +export interface SortedArray extends Array { + " __sortedArrayBrand": any; +} +/** ES6 Map interface, only read methods included. */ +export interface ReadonlyMap { + get(key: string): T | undefined; + has(key: string): boolean; + forEach(action: (value: T, key: string) => void): void; + readonly size: number; + keys(): Iterator; + values(): Iterator; + entries(): Iterator<[string, T]>; +} +/** ES6 Map interface. */ +export interface Map extends ReadonlyMap { + set(key: string, value: T): this; + delete(key: string): boolean; + clear(): void; +} +/* @internal */ +export interface MapConstructor { + // eslint-disable-next-line @typescript-eslint/prefer-function-type + new (): Map; +} +/** + * Returns the native Map implementation if it is available and compatible (i.e. supports iteration). + */ +/* @internal */ +export function tryGetNativeMap(): MapConstructor | undefined { + // Internet Explorer's Map doesn't support iteration, so don't use it. + // Natives + // NOTE: TS doesn't strictly allow in-line declares, but if we suppress the error, the declaration + // is still used for typechecking _and_ correctly elided, which is out goal, as this prevents us from + // needing to pollute an outer scope with a declaration of `Map` just to satisfy the checks in this function + //@ts-ignore + declare const Map: (new () => Map) | undefined; + // eslint-disable-next-line no-in-operator + return typeof Map !== "undefined" && "entries" in Map.prototype ? Map : undefined; +} +/* @internal */ +export const Map: MapConstructor = tryGetNativeMap() || (() => { + // NOTE: createMapShim will be defined for typescriptServices.js but not for tsc.js, so we must test for it. + if (typeof createMapShim === "function") { + return createMapShim(); } - - export interface SortedReadonlyArray extends ReadonlyArray { - " __sortedArrayBrand": any; - } - - export interface SortedArray extends Array { - " __sortedArrayBrand": any; - } - - /** ES6 Map interface, only read methods included. */ - export interface ReadonlyMap { - get(key: string): T | undefined; - has(key: string): boolean; - forEach(action: (value: T, key: string) => void): void; - readonly size: number; - keys(): Iterator; - values(): Iterator; - entries(): Iterator<[string, T]>; - } - - /** ES6 Map interface. */ - export interface Map extends ReadonlyMap { - set(key: string, value: T): this; - delete(key: string): boolean; - clear(): void; - } - - /* @internal */ - export interface MapConstructor { - // eslint-disable-next-line @typescript-eslint/prefer-function-type - new (): Map; - } - - /** - * Returns the native Map implementation if it is available and compatible (i.e. supports iteration). - */ - /* @internal */ - export function tryGetNativeMap(): MapConstructor | undefined { - // Internet Explorer's Map doesn't support iteration, so don't use it. - // Natives - // NOTE: TS doesn't strictly allow in-line declares, but if we suppress the error, the declaration - // is still used for typechecking _and_ correctly elided, which is out goal, as this prevents us from - // needing to pollute an outer scope with a declaration of `Map` just to satisfy the checks in this function - //@ts-ignore - declare const Map: (new () => Map) | undefined; - // eslint-disable-next-line no-in-operator - return typeof Map !== "undefined" && "entries" in Map.prototype ? Map : undefined; - } - - /* @internal */ - export const Map: MapConstructor = tryGetNativeMap() || (() => { - // NOTE: createMapShim will be defined for typescriptServices.js but not for tsc.js, so we must test for it. - if (typeof createMapShim === "function") { - return createMapShim(); - } - throw new Error("TypeScript requires an environment that provides a compatible native Map implementation."); - })(); - - /** ES6 Iterator type. */ - export interface Iterator { - next(): { value: T, done?: false } | { value: never, done: true }; - } - - /** Array that is only intended to be pushed to, never read. */ - export interface Push { - push(...values: T[]): void; - } - - /* @internal */ - export type EqualityComparer = (a: T, b: T) => boolean; - - /* @internal */ - export type Comparer = (a: T, b: T) => Comparison; - - /* @internal */ - export const enum Comparison { - LessThan = -1, - EqualTo = 0, - GreaterThan = 1 - } -} \ No newline at end of file + throw new Error("TypeScript requires an environment that provides a compatible native Map implementation."); +})(); +/** ES6 Iterator type. */ +export interface Iterator { + next(): { + value: T; + done?: false; + } | { + value: never; + done: true; + }; +} +/** Array that is only intended to be pushed to, never read. */ +export interface Push { + push(...values: T[]): void; +} +/* @internal */ +export type EqualityComparer = (a: T, b: T) => boolean; +/* @internal */ +export type Comparer = (a: T, b: T) => Comparison; +/* @internal */ +export const enum Comparison { + LessThan = -1, + EqualTo = 0, + GreaterThan = 1 +} diff --git a/src/compiler/debug.ts b/src/compiler/debug.ts index 660f136133510..689f6b192063c 100644 --- a/src/compiler/debug.ts +++ b/src/compiler/debug.ts @@ -1,414 +1,341 @@ +import { AssertionLevel, MatchingKeys, AnyFunction, getOwnKeys, noop, Node, NodeArray, hasProperty, every, SyntaxKind, Symbol, unescapeLeadingUnderscores, map, stableSort, compareValues, NodeFlags, ModifierFlags, TransformFlags, EmitFlags, SymbolFlags, TypeFlags, ObjectFlags, FlowNode, objectAllocator, Type, ObjectType, getModifierFlagsNoCache, isParseTreeNode, getEmitFlags, nodeIsSynthesized, getParseTreeNode, getSourceFileOfNode, getSourceTextOfNodeFromSourceFile, sys, getDirectoryPath, resolvePath, RequireResult } from "./ts"; +import * as ts from "./ts"; /* @internal */ -namespace ts { - export namespace Debug { - let currentAssertionLevel = AssertionLevel.None; - - // eslint-disable-next-line prefer-const - export let isDebugging = false; - - type AssertionKeys = MatchingKeys; - - const assertionCache: Partial> = {}; - - export function getAssertionLevel() { - return currentAssertionLevel; - } - - export function setAssertionLevel(level: AssertionLevel) { - const prevAssertionLevel = currentAssertionLevel; - currentAssertionLevel = level; - - if (level > prevAssertionLevel) { - // restore assertion functions for the current assertion level (see `shouldAssertFunction`). - for (const key of getOwnKeys(assertionCache) as AssertionKeys[]) { - const cachedFunc = assertionCache[key]; - if (cachedFunc !== undefined && Debug[key] !== cachedFunc.assertion && level >= cachedFunc.level) { - (Debug as any)[key] = cachedFunc; - assertionCache[key] = undefined; - } - } - } - } - - export function shouldAssert(level: AssertionLevel): boolean { - return currentAssertionLevel >= level; - } - - /** - * Tests whether an assertion function should be executed. If it shouldn't, it is cached and replaced with `ts.noop`. - * Replaced assertion functions are restored when `Debug.setAssertionLevel` is set to a high enough level. - * @param level The minimum assertion level required. - * @param name The name of the current assertion function. - */ - function shouldAssertFunction(level: AssertionLevel, name: K): boolean { - if (!shouldAssert(level)) { - assertionCache[name] = { level, assertion: Debug[name] }; - (Debug as any)[name] = noop; - return false; - } - return true; - } - - export function fail(message?: string, stackCrawlMark?: AnyFunction): never { - debugger; - const e = new Error(message ? `Debug Failure. ${message}` : "Debug Failure."); - if ((Error).captureStackTrace) { - (Error).captureStackTrace(e, stackCrawlMark || fail); - } - throw e; - } - - export function failBadSyntaxKind(node: Node, message?: string, stackCrawlMark?: AnyFunction): never { - return fail( - `${message || "Unexpected node."}\r\nNode ${formatSyntaxKind(node.kind)} was unexpected.`, - stackCrawlMark || failBadSyntaxKind); - } - - export function assert(expression: boolean, message?: string, verboseDebugInfo?: string | (() => string), stackCrawlMark?: AnyFunction): asserts expression { - if (!expression) { - message = message ? `False expression: ${message}` : "False expression."; - if (verboseDebugInfo) { - message += "\r\nVerbose Debug Information: " + (typeof verboseDebugInfo === "string" ? verboseDebugInfo : verboseDebugInfo()); +export namespace Debug { + let currentAssertionLevel = AssertionLevel.None; + // eslint-disable-next-line prefer-const + export let isDebugging = false; + type AssertionKeys = MatchingKeys; + const assertionCache: Partial> = {}; + export function getAssertionLevel() { + return currentAssertionLevel; + } + export function setAssertionLevel(level: AssertionLevel) { + const prevAssertionLevel = currentAssertionLevel; + currentAssertionLevel = level; + if (level > prevAssertionLevel) { + // restore assertion functions for the current assertion level (see `shouldAssertFunction`). + for (const key of getOwnKeys(assertionCache) as AssertionKeys[]) { + const cachedFunc = assertionCache[key]; + if (cachedFunc !== undefined && Debug[key] !== cachedFunc.assertion && level >= cachedFunc.level) { + (Debug as any)[key] = cachedFunc; + assertionCache[key] = undefined; } - fail(message, stackCrawlMark || assert); } } - - export function assertEqual(a: T, b: T, msg?: string, msg2?: string, stackCrawlMark?: AnyFunction): void { - if (a !== b) { - const message = msg ? msg2 ? `${msg} ${msg2}` : msg : ""; - fail(`Expected ${a} === ${b}. ${message}`, stackCrawlMark || assertEqual); - } + } + export function shouldAssert(level: AssertionLevel): boolean { + return currentAssertionLevel >= level; + } + /** + * Tests whether an assertion function should be executed. If it shouldn't, it is cached and replaced with `ts.noop`. + * Replaced assertion functions are restored when `Debug.setAssertionLevel` is set to a high enough level. + * @param level The minimum assertion level required. + * @param name The name of the current assertion function. + */ + function shouldAssertFunction(level: AssertionLevel, name: K): boolean { + if (!shouldAssert(level)) { + assertionCache[name] = { level, assertion: Debug[name] }; + (Debug as any)[name] = noop; + return false; + } + return true; + } + export function fail(message?: string, stackCrawlMark?: AnyFunction): never { + debugger; + const e = new Error(message ? `Debug Failure. ${message}` : "Debug Failure."); + if ((Error).captureStackTrace) { + (Error).captureStackTrace(e, stackCrawlMark || fail); } - - export function assertLessThan(a: number, b: number, msg?: string, stackCrawlMark?: AnyFunction): void { - if (a >= b) { - fail(`Expected ${a} < ${b}. ${msg || ""}`, stackCrawlMark || assertLessThan); + throw e; + } + export function failBadSyntaxKind(node: Node, message?: string, stackCrawlMark?: AnyFunction): never { + return fail(`${message || "Unexpected node."}\r\nNode ${formatSyntaxKind(node.kind)} was unexpected.`, stackCrawlMark || failBadSyntaxKind); + } + export function assert(expression: boolean, message?: string, verboseDebugInfo?: string | (() => string), stackCrawlMark?: AnyFunction): asserts expression { + if (!expression) { + message = message ? `False expression: ${message}` : "False expression."; + if (verboseDebugInfo) { + message += "\r\nVerbose Debug Information: " + (typeof verboseDebugInfo === "string" ? verboseDebugInfo : verboseDebugInfo()); } + fail(message, stackCrawlMark || assert); } - - export function assertLessThanOrEqual(a: number, b: number, stackCrawlMark?: AnyFunction): void { - if (a > b) { - fail(`Expected ${a} <= ${b}`, stackCrawlMark || assertLessThanOrEqual); - } + } + export function assertEqual(a: T, b: T, msg?: string, msg2?: string, stackCrawlMark?: AnyFunction): void { + if (a !== b) { + const message = msg ? msg2 ? `${msg} ${msg2}` : msg : ""; + fail(`Expected ${a} === ${b}. ${message}`, stackCrawlMark || assertEqual); } - - export function assertGreaterThanOrEqual(a: number, b: number, stackCrawlMark?: AnyFunction): void { - if (a < b) { - fail(`Expected ${a} >= ${b}`, stackCrawlMark || assertGreaterThanOrEqual); - } + } + export function assertLessThan(a: number, b: number, msg?: string, stackCrawlMark?: AnyFunction): void { + if (a >= b) { + fail(`Expected ${a} < ${b}. ${msg || ""}`, stackCrawlMark || assertLessThan); } - - export function assertIsDefined(value: T, message?: string, stackCrawlMark?: AnyFunction): asserts value is NonNullable { - // eslint-disable-next-line no-null/no-null - if (value === undefined || value === null) { - fail(message, stackCrawlMark || assertIsDefined); - } + } + export function assertLessThanOrEqual(a: number, b: number, stackCrawlMark?: AnyFunction): void { + if (a > b) { + fail(`Expected ${a} <= ${b}`, stackCrawlMark || assertLessThanOrEqual); } - - export function checkDefined(value: T | null | undefined, message?: string, stackCrawlMark?: AnyFunction): T { - assertIsDefined(value, message, stackCrawlMark || checkDefined); - return value; + } + export function assertGreaterThanOrEqual(a: number, b: number, stackCrawlMark?: AnyFunction): void { + if (a < b) { + fail(`Expected ${a} >= ${b}`, stackCrawlMark || assertGreaterThanOrEqual); } - - /** - * @deprecated Use `checkDefined` to check whether a value is defined inline. Use `assertIsDefined` to check whether - * a value is defined at the statement level. - */ - export const assertDefined = checkDefined; - - export function assertEachIsDefined(value: NodeArray, message?: string, stackCrawlMark?: AnyFunction): asserts value is NodeArray; - export function assertEachIsDefined(value: readonly T[], message?: string, stackCrawlMark?: AnyFunction): asserts value is readonly NonNullable[]; - export function assertEachIsDefined(value: readonly T[], message?: string, stackCrawlMark?: AnyFunction) { - for (const v of value) { - assertIsDefined(v, message, stackCrawlMark || assertEachIsDefined); - } + } + export function assertIsDefined(value: T, message?: string, stackCrawlMark?: AnyFunction): asserts value is NonNullable { + // eslint-disable-next-line no-null/no-null + if (value === undefined || value === null) { + fail(message, stackCrawlMark || assertIsDefined); } - - export function checkEachDefined(value: A, message?: string, stackCrawlMark?: AnyFunction): A { - assertEachIsDefined(value, message, stackCrawlMark || checkEachDefined); - return value; + } + export function checkDefined(value: T | null | undefined, message?: string, stackCrawlMark?: AnyFunction): T { + assertIsDefined(value, message, stackCrawlMark || checkDefined); + return value; + } + /** + * @deprecated Use `checkDefined` to check whether a value is defined inline. Use `assertIsDefined` to check whether + * a value is defined at the statement level. + */ + export const assertDefined = checkDefined; + export function assertEachIsDefined(value: NodeArray, message?: string, stackCrawlMark?: AnyFunction): asserts value is NodeArray; + export function assertEachIsDefined(value: readonly T[], message?: string, stackCrawlMark?: AnyFunction): asserts value is readonly NonNullable[]; + export function assertEachIsDefined(value: readonly T[], message?: string, stackCrawlMark?: AnyFunction) { + for (const v of value) { + assertIsDefined(v, message, stackCrawlMark || assertEachIsDefined); } - - /** - * @deprecated Use `checkEachDefined` to check whether the elements of an array are defined inline. Use `assertEachIsDefined` to check whether - * the elements of an array are defined at the statement level. - */ - export const assertEachDefined = checkEachDefined; - - export function assertNever(member: never, message = "Illegal value:", stackCrawlMark?: AnyFunction): never { - const detail = typeof member === "object" && hasProperty(member, "kind") && hasProperty(member, "pos") && formatSyntaxKind ? "SyntaxKind: " + formatSyntaxKind((member as Node).kind) : JSON.stringify(member); - return fail(`${message} ${detail}`, stackCrawlMark || assertNever); + } + export function checkEachDefined(value: A, message?: string, stackCrawlMark?: AnyFunction): A { + assertEachIsDefined(value, message, stackCrawlMark || checkEachDefined); + return value; + } + /** + * @deprecated Use `checkEachDefined` to check whether the elements of an array are defined inline. Use `assertEachIsDefined` to check whether + * the elements of an array are defined at the statement level. + */ + export const assertEachDefined = checkEachDefined; + export function assertNever(member: never, message = "Illegal value:", stackCrawlMark?: AnyFunction): never { + const detail = typeof member === "object" && hasProperty(member, "kind") && hasProperty(member, "pos") && formatSyntaxKind ? "SyntaxKind: " + formatSyntaxKind((member as Node).kind) : JSON.stringify(member); + return fail(`${message} ${detail}`, stackCrawlMark || assertNever); + } + export function assertEachNode(nodes: NodeArray, test: (node: T) => node is U, message?: string, stackCrawlMark?: AnyFunction): asserts nodes is NodeArray; + export function assertEachNode(nodes: readonly T[], test: (node: T) => node is U, message?: string, stackCrawlMark?: AnyFunction): asserts nodes is readonly U[]; + export function assertEachNode(nodes: readonly Node[], test: (node: Node) => boolean, message?: string, stackCrawlMark?: AnyFunction): void; + export function assertEachNode(nodes: readonly Node[], test: (node: Node) => boolean, message?: string, stackCrawlMark?: AnyFunction) { + if (shouldAssertFunction(AssertionLevel.Normal, "assertEachNode")) { + assert(test === undefined || every(nodes, test), message || "Unexpected node.", () => `Node array did not pass test '${getFunctionName(test)}'.`, stackCrawlMark || assertEachNode); } - - export function assertEachNode(nodes: NodeArray, test: (node: T) => node is U, message?: string, stackCrawlMark?: AnyFunction): asserts nodes is NodeArray; - export function assertEachNode(nodes: readonly T[], test: (node: T) => node is U, message?: string, stackCrawlMark?: AnyFunction): asserts nodes is readonly U[]; - export function assertEachNode(nodes: readonly Node[], test: (node: Node) => boolean, message?: string, stackCrawlMark?: AnyFunction): void; - export function assertEachNode(nodes: readonly Node[], test: (node: Node) => boolean, message?: string, stackCrawlMark?: AnyFunction) { - if (shouldAssertFunction(AssertionLevel.Normal, "assertEachNode")) { - assert( - test === undefined || every(nodes, test), - message || "Unexpected node.", - () => `Node array did not pass test '${getFunctionName(test)}'.`, - stackCrawlMark || assertEachNode); - } + } + export function assertNode(node: T | undefined, test: (node: T) => node is U, message?: string, stackCrawlMark?: AnyFunction): asserts node is U; + export function assertNode(node: Node | undefined, test: ((node: Node) => boolean) | undefined, message?: string, stackCrawlMark?: AnyFunction): void; + export function assertNode(node: Node | undefined, test: ((node: Node) => boolean) | undefined, message?: string, stackCrawlMark?: AnyFunction) { + if (shouldAssertFunction(AssertionLevel.Normal, "assertNode")) { + assert(node !== undefined && (test === undefined || test(node)), message || "Unexpected node.", () => `Node ${formatSyntaxKind(node!.kind)} did not pass test '${getFunctionName(test!)}'.`, stackCrawlMark || assertNode); } - - export function assertNode(node: T | undefined, test: (node: T) => node is U, message?: string, stackCrawlMark?: AnyFunction): asserts node is U; - export function assertNode(node: Node | undefined, test: ((node: Node) => boolean) | undefined, message?: string, stackCrawlMark?: AnyFunction): void; - export function assertNode(node: Node | undefined, test: ((node: Node) => boolean) | undefined, message?: string, stackCrawlMark?: AnyFunction) { - if (shouldAssertFunction(AssertionLevel.Normal, "assertNode")) { - assert( - node !== undefined && (test === undefined || test(node)), - message || "Unexpected node.", - () => `Node ${formatSyntaxKind(node!.kind)} did not pass test '${getFunctionName(test!)}'.`, - stackCrawlMark || assertNode); - } + } + export function assertNotNode(node: T | undefined, test: (node: T) => node is U, message?: string, stackCrawlMark?: AnyFunction): asserts node is Exclude; + export function assertNotNode(node: Node | undefined, test: ((node: Node) => boolean) | undefined, message?: string, stackCrawlMark?: AnyFunction): void; + export function assertNotNode(node: Node | undefined, test: ((node: Node) => boolean) | undefined, message?: string, stackCrawlMark?: AnyFunction) { + if (shouldAssertFunction(AssertionLevel.Normal, "assertNotNode")) { + assert(node === undefined || test === undefined || !test(node), message || "Unexpected node.", () => `Node ${formatSyntaxKind(node!.kind)} should not have passed test '${getFunctionName(test!)}'.`, stackCrawlMark || assertNotNode); } - - export function assertNotNode(node: T | undefined, test: (node: T) => node is U, message?: string, stackCrawlMark?: AnyFunction): asserts node is Exclude; - export function assertNotNode(node: Node | undefined, test: ((node: Node) => boolean) | undefined, message?: string, stackCrawlMark?: AnyFunction): void; - export function assertNotNode(node: Node | undefined, test: ((node: Node) => boolean) | undefined, message?: string, stackCrawlMark?: AnyFunction) { - if (shouldAssertFunction(AssertionLevel.Normal, "assertNotNode")) { - assert( - node === undefined || test === undefined || !test(node), - message || "Unexpected node.", - () => `Node ${formatSyntaxKind(node!.kind)} should not have passed test '${getFunctionName(test!)}'.`, - stackCrawlMark || assertNotNode); - } + } + export function assertOptionalNode(node: T, test: (node: T) => node is U, message?: string, stackCrawlMark?: AnyFunction): asserts node is U; + export function assertOptionalNode(node: T | undefined, test: (node: T) => node is U, message?: string, stackCrawlMark?: AnyFunction): asserts node is U | undefined; + export function assertOptionalNode(node: Node | undefined, test: ((node: Node) => boolean) | undefined, message?: string, stackCrawlMark?: AnyFunction): void; + export function assertOptionalNode(node: Node | undefined, test: ((node: Node) => boolean) | undefined, message?: string, stackCrawlMark?: AnyFunction) { + if (shouldAssertFunction(AssertionLevel.Normal, "assertOptionalNode")) { + assert(test === undefined || node === undefined || test(node), message || "Unexpected node.", () => `Node ${formatSyntaxKind(node!.kind)} did not pass test '${getFunctionName(test!)}'.`, stackCrawlMark || assertOptionalNode); } - - export function assertOptionalNode(node: T, test: (node: T) => node is U, message?: string, stackCrawlMark?: AnyFunction): asserts node is U; - export function assertOptionalNode(node: T | undefined, test: (node: T) => node is U, message?: string, stackCrawlMark?: AnyFunction): asserts node is U | undefined; - export function assertOptionalNode(node: Node | undefined, test: ((node: Node) => boolean) | undefined, message?: string, stackCrawlMark?: AnyFunction): void; - export function assertOptionalNode(node: Node | undefined, test: ((node: Node) => boolean) | undefined, message?: string, stackCrawlMark?: AnyFunction) { - if (shouldAssertFunction(AssertionLevel.Normal, "assertOptionalNode")) { - assert( - test === undefined || node === undefined || test(node), - message || "Unexpected node.", - () => `Node ${formatSyntaxKind(node!.kind)} did not pass test '${getFunctionName(test!)}'.`, - stackCrawlMark || assertOptionalNode); - } + } + export function assertOptionalToken(node: T, kind: K, message?: string, stackCrawlMark?: AnyFunction): asserts node is Extract; + export function assertOptionalToken(node: T | undefined, kind: K, message?: string, stackCrawlMark?: AnyFunction): asserts node is Extract | undefined; + export function assertOptionalToken(node: Node | undefined, kind: SyntaxKind | undefined, message?: string, stackCrawlMark?: AnyFunction): void; + export function assertOptionalToken(node: Node | undefined, kind: SyntaxKind | undefined, message?: string, stackCrawlMark?: AnyFunction) { + if (shouldAssertFunction(AssertionLevel.Normal, "assertOptionalToken")) { + assert(kind === undefined || node === undefined || node.kind === kind, message || "Unexpected node.", () => `Node ${formatSyntaxKind(node!.kind)} was not a '${formatSyntaxKind(kind)}' token.`, stackCrawlMark || assertOptionalToken); } - - export function assertOptionalToken(node: T, kind: K, message?: string, stackCrawlMark?: AnyFunction): asserts node is Extract; - export function assertOptionalToken(node: T | undefined, kind: K, message?: string, stackCrawlMark?: AnyFunction): asserts node is Extract | undefined; - export function assertOptionalToken(node: Node | undefined, kind: SyntaxKind | undefined, message?: string, stackCrawlMark?: AnyFunction): void; - export function assertOptionalToken(node: Node | undefined, kind: SyntaxKind | undefined, message?: string, stackCrawlMark?: AnyFunction) { - if (shouldAssertFunction(AssertionLevel.Normal, "assertOptionalToken")) { - assert( - kind === undefined || node === undefined || node.kind === kind, - message || "Unexpected node.", - () => `Node ${formatSyntaxKind(node!.kind)} was not a '${formatSyntaxKind(kind)}' token.`, - stackCrawlMark || assertOptionalToken); - } + } + export function assertMissingNode(node: Node | undefined, message?: string, stackCrawlMark?: AnyFunction): asserts node is undefined; + export function assertMissingNode(node: Node | undefined, message?: string, stackCrawlMark?: AnyFunction) { + if (shouldAssertFunction(AssertionLevel.Normal, "assertMissingNode")) { + assert(node === undefined, message || "Unexpected node.", () => `Node ${formatSyntaxKind(node!.kind)} was unexpected'.`, stackCrawlMark || assertMissingNode); } - - export function assertMissingNode(node: Node | undefined, message?: string, stackCrawlMark?: AnyFunction): asserts node is undefined; - export function assertMissingNode(node: Node | undefined, message?: string, stackCrawlMark?: AnyFunction) { - if (shouldAssertFunction(AssertionLevel.Normal, "assertMissingNode")) { - assert( - node === undefined, - message || "Unexpected node.", - () => `Node ${formatSyntaxKind(node!.kind)} was unexpected'.`, - stackCrawlMark || assertMissingNode); - } + } + export function getFunctionName(func: AnyFunction) { + if (typeof func !== "function") { + return ""; } - - export function getFunctionName(func: AnyFunction) { - if (typeof func !== "function") { - return ""; - } - else if (func.hasOwnProperty("name")) { - return (func).name; - } - else { - const text = Function.prototype.toString.call(func); - const match = /^function\s+([\w\$]+)\s*\(/.exec(text); - return match ? match[1] : ""; - } + else if (func.hasOwnProperty("name")) { + return (func).name; } - - export function formatSymbol(symbol: Symbol): string { - return `{ name: ${unescapeLeadingUnderscores(symbol.escapedName)}; flags: ${formatSymbolFlags(symbol.flags)}; declarations: ${map(symbol.declarations, node => formatSyntaxKind(node.kind))} }`; + else { + const text = Function.prototype.toString.call(func); + const match = /^function\s+([\w\$]+)\s*\(/.exec(text); + return match ? match[1] : ""; } - - /** - * Formats an enum value as a string for debugging and debug assertions. - */ - export function formatEnum(value = 0, enumObject: any, isFlags?: boolean) { - const members = getEnumMembers(enumObject); - if (value === 0) { - return members.length > 0 && members[0][0] === 0 ? members[0][1] : "0"; - } - if (isFlags) { - let result = ""; - let remainingFlags = value; - for (const [enumValue, enumName] of members) { - if (enumValue > value) { - break; - } - if (enumValue !== 0 && enumValue & value) { - result = `${result}${result ? "|" : ""}${enumName}`; - remainingFlags &= ~enumValue; - } + } + export function formatSymbol(symbol: Symbol): string { + return `{ name: ${unescapeLeadingUnderscores(symbol.escapedName)}; flags: ${formatSymbolFlags(symbol.flags)}; declarations: ${map(symbol.declarations, node => formatSyntaxKind(node.kind))} }`; + } + /** + * Formats an enum value as a string for debugging and debug assertions. + */ + export function formatEnum(value = 0, enumObject: any, isFlags?: boolean) { + const members = getEnumMembers(enumObject); + if (value === 0) { + return members.length > 0 && members[0][0] === 0 ? members[0][1] : "0"; + } + if (isFlags) { + let result = ""; + let remainingFlags = value; + for (const [enumValue, enumName] of members) { + if (enumValue > value) { + break; } - if (remainingFlags === 0) { - return result; + if (enumValue !== 0 && enumValue & value) { + result = `${result}${result ? "|" : ""}${enumName}`; + remainingFlags &= ~enumValue; } } - else { - for (const [enumValue, enumName] of members) { - if (enumValue === value) { - return enumName; - } - } + if (remainingFlags === 0) { + return result; } - return value.toString(); } - - function getEnumMembers(enumObject: any) { - const result: [number, string][] = []; - for (const name in enumObject) { - const value = enumObject[name]; - if (typeof value === "number") { - result.push([value, name]); + else { + for (const [enumValue, enumName] of members) { + if (enumValue === value) { + return enumName; } } - - return stableSort<[number, string]>(result, (x, y) => compareValues(x[0], y[0])); } - - export function formatSyntaxKind(kind: SyntaxKind | undefined): string { - return formatEnum(kind, (ts).SyntaxKind, /*isFlags*/ false); - } - - export function formatNodeFlags(flags: NodeFlags | undefined): string { - return formatEnum(flags, (ts).NodeFlags, /*isFlags*/ true); - } - - export function formatModifierFlags(flags: ModifierFlags | undefined): string { - return formatEnum(flags, (ts).ModifierFlags, /*isFlags*/ true); - } - - export function formatTransformFlags(flags: TransformFlags | undefined): string { - return formatEnum(flags, (ts).TransformFlags, /*isFlags*/ true); - } - - export function formatEmitFlags(flags: EmitFlags | undefined): string { - return formatEnum(flags, (ts).EmitFlags, /*isFlags*/ true); - } - - export function formatSymbolFlags(flags: SymbolFlags | undefined): string { - return formatEnum(flags, (ts).SymbolFlags, /*isFlags*/ true); - } - - export function formatTypeFlags(flags: TypeFlags | undefined): string { - return formatEnum(flags, (ts).TypeFlags, /*isFlags*/ true); - } - - export function formatObjectFlags(flags: ObjectFlags | undefined): string { - return formatEnum(flags, (ts).ObjectFlags, /*isFlags*/ true); - } - - let isDebugInfoEnabled = false; - - interface ExtendedDebugModule { - init(_ts: typeof ts): void; - formatControlFlowGraph(flowNode: FlowNode): string; - } - - let extendedDebugModule: ExtendedDebugModule | undefined; - - function extendedDebug() { - enableDebugInfo(); - if (!extendedDebugModule) { - throw new Error("Debugging helpers could not be loaded."); + return value.toString(); + } + function getEnumMembers(enumObject: any) { + const result: [number, string][] = []; + for (const name in enumObject) { + const value = enumObject[name]; + if (typeof value === "number") { + result.push([value, name]); } - return extendedDebugModule; - } - - export function printControlFlowGraph(flowNode: FlowNode) { - return console.log(formatControlFlowGraph(flowNode)); } - - export function formatControlFlowGraph(flowNode: FlowNode) { - return extendedDebug().formatControlFlowGraph(flowNode); + return stableSort<[number, string]>(result, (x, y) => compareValues(x[0], y[0])); + } + export function formatSyntaxKind(kind: SyntaxKind | undefined): string { + return formatEnum(kind, (ts).SyntaxKind, /*isFlags*/ false); + } + export function formatNodeFlags(flags: NodeFlags | undefined): string { + return formatEnum(flags, (ts).NodeFlags, /*isFlags*/ true); + } + export function formatModifierFlags(flags: ModifierFlags | undefined): string { + return formatEnum(flags, (ts).ModifierFlags, /*isFlags*/ true); + } + export function formatTransformFlags(flags: TransformFlags | undefined): string { + return formatEnum(flags, (ts).TransformFlags, /*isFlags*/ true); + } + export function formatEmitFlags(flags: EmitFlags | undefined): string { + return formatEnum(flags, (ts).EmitFlags, /*isFlags*/ true); + } + export function formatSymbolFlags(flags: SymbolFlags | undefined): string { + return formatEnum(flags, (ts).SymbolFlags, /*isFlags*/ true); + } + export function formatTypeFlags(flags: TypeFlags | undefined): string { + return formatEnum(flags, (ts).TypeFlags, /*isFlags*/ true); + } + export function formatObjectFlags(flags: ObjectFlags | undefined): string { + return formatEnum(flags, (ts).ObjectFlags, /*isFlags*/ true); + } + let isDebugInfoEnabled = false; + interface ExtendedDebugModule { + init(_ts: typeof ts): void; + formatControlFlowGraph(flowNode: FlowNode): string; + } + let extendedDebugModule: ExtendedDebugModule | undefined; + function extendedDebug() { + enableDebugInfo(); + if (!extendedDebugModule) { + throw new Error("Debugging helpers could not be loaded."); } - - export function attachFlowNodeDebugInfo(flowNode: FlowNode) { - if (isDebugInfoEnabled) { - if (!("__debugFlowFlags" in flowNode)) { // eslint-disable-line no-in-operator - Object.defineProperties(flowNode, { - __debugFlowFlags: { get(this: FlowNode) { return formatEnum(this.flags, (ts as any).FlowFlags, /*isFlags*/ true); } }, - __debugToString: { value(this: FlowNode) { return formatControlFlowGraph(this); } } - }); - } + return extendedDebugModule; + } + export function printControlFlowGraph(flowNode: FlowNode) { + return console.log(formatControlFlowGraph(flowNode)); + } + export function formatControlFlowGraph(flowNode: FlowNode) { + return extendedDebug().formatControlFlowGraph(flowNode); + } + export function attachFlowNodeDebugInfo(flowNode: FlowNode) { + if (isDebugInfoEnabled) { + if (!("__debugFlowFlags" in flowNode)) { // eslint-disable-line no-in-operator + Object.defineProperties(flowNode, { + __debugFlowFlags: { get(this: FlowNode) { return formatEnum(this.flags, (ts as any).FlowFlags, /*isFlags*/ true); } }, + __debugToString: { value(this: FlowNode) { return formatControlFlowGraph(this); } } + }); } } - - /** - * Injects debug information into frequently used types. - */ - export function enableDebugInfo() { - if (isDebugInfoEnabled) return; - - // Add additional properties in debug mode to assist with debugging. - Object.defineProperties(objectAllocator.getSymbolConstructor().prototype, { - __debugFlags: { get(this: Symbol) { return formatSymbolFlags(this.flags); } } - }); - - Object.defineProperties(objectAllocator.getTypeConstructor().prototype, { - __debugFlags: { get(this: Type) { return formatTypeFlags(this.flags); } }, - __debugObjectFlags: { get(this: Type) { return this.flags & TypeFlags.Object ? formatObjectFlags((this).objectFlags) : ""; } }, - __debugTypeToString: { value(this: Type) { return this.checker.typeToString(this); } }, - }); - - const nodeConstructors = [ - objectAllocator.getNodeConstructor(), - objectAllocator.getIdentifierConstructor(), - objectAllocator.getTokenConstructor(), - objectAllocator.getSourceFileConstructor() - ]; - - for (const ctor of nodeConstructors) { - if (!ctor.prototype.hasOwnProperty("__debugKind")) { - Object.defineProperties(ctor.prototype, { - __debugKind: { get(this: Node) { return formatSyntaxKind(this.kind); } }, - __debugNodeFlags: { get(this: Node) { return formatNodeFlags(this.flags); } }, - __debugModifierFlags: { get(this: Node) { return formatModifierFlags(getModifierFlagsNoCache(this)); } }, - __debugTransformFlags: { get(this: Node) { return formatTransformFlags(this.transformFlags); } }, - __debugIsParseTreeNode: { get(this: Node) { return isParseTreeNode(this); } }, - __debugEmitFlags: { get(this: Node) { return formatEmitFlags(getEmitFlags(this)); } }, - __debugGetText: { - value(this: Node, includeTrivia?: boolean) { - if (nodeIsSynthesized(this)) return ""; - const parseNode = getParseTreeNode(this); - const sourceFile = parseNode && getSourceFileOfNode(parseNode); - return sourceFile ? getSourceTextOfNodeFromSourceFile(sourceFile, parseNode, includeTrivia) : ""; - } + } + /** + * Injects debug information into frequently used types. + */ + export function enableDebugInfo() { + if (isDebugInfoEnabled) + return; + // Add additional properties in debug mode to assist with debugging. + Object.defineProperties(objectAllocator.getSymbolConstructor().prototype, { + __debugFlags: { get(this: Symbol) { return formatSymbolFlags(this.flags); } } + }); + Object.defineProperties(objectAllocator.getTypeConstructor().prototype, { + __debugFlags: { get(this: Type) { return formatTypeFlags(this.flags); } }, + __debugObjectFlags: { get(this: Type) { return this.flags & TypeFlags.Object ? formatObjectFlags((this).objectFlags) : ""; } }, + __debugTypeToString: { value(this: Type) { return this.checker.typeToString(this); } }, + }); + const nodeConstructors = [ + objectAllocator.getNodeConstructor(), + objectAllocator.getIdentifierConstructor(), + objectAllocator.getTokenConstructor(), + objectAllocator.getSourceFileConstructor() + ]; + for (const ctor of nodeConstructors) { + if (!ctor.prototype.hasOwnProperty("__debugKind")) { + Object.defineProperties(ctor.prototype, { + __debugKind: { get(this: Node) { return formatSyntaxKind(this.kind); } }, + __debugNodeFlags: { get(this: Node) { return formatNodeFlags(this.flags); } }, + __debugModifierFlags: { get(this: Node) { return formatModifierFlags(getModifierFlagsNoCache(this)); } }, + __debugTransformFlags: { get(this: Node) { return formatTransformFlags(this.transformFlags); } }, + __debugIsParseTreeNode: { get(this: Node) { return isParseTreeNode(this); } }, + __debugEmitFlags: { get(this: Node) { return formatEmitFlags(getEmitFlags(this)); } }, + __debugGetText: { + value(this: Node, includeTrivia?: boolean) { + if (nodeIsSynthesized(this)) + return ""; + const parseNode = getParseTreeNode(this); + const sourceFile = parseNode && getSourceFileOfNode(parseNode); + return sourceFile ? getSourceTextOfNodeFromSourceFile(sourceFile, parseNode, includeTrivia) : ""; } - }); - } - } - - // attempt to load extended debugging information - try { - if (sys && sys.require) { - const basePath = getDirectoryPath(resolvePath(sys.getExecutingFilePath())); - const result = sys.require(basePath, "./compiler-debug") as RequireResult; - if (!result.error) { - result.module.init(ts); - extendedDebugModule = result.module; } - } + }); } - catch { - // do nothing + } + // attempt to load extended debugging information + try { + if (sys && sys.require) { + const basePath = getDirectoryPath(resolvePath(sys.getExecutingFilePath())); + const result = (sys.require(basePath, "./compiler-debug") as RequireResult); + if (!result.error) { + result.module.init(ts); + extendedDebugModule = result.module; + } } - - isDebugInfoEnabled = true; } - + catch { + // do nothing + } + isDebugInfoEnabled = true; } } diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index adbe97212e2b0..ed451f6eb664c 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -1,5287 +1,4649 @@ -namespace ts { - const brackets = createBracketsMap(); - const syntheticParent: TextRange = { pos: -1, end: -1 }; - - /*@internal*/ - export function isBuildInfoFile(file: string) { - return fileExtensionIs(file, Extension.TsBuildInfo); - } - - /*@internal*/ - /** - * Iterates over the source files that are expected to have an emit output. - * - * @param host An EmitHost. - * @param action The action to execute. - * @param sourceFilesOrTargetSourceFile - * If an array, the full list of source files to emit. - * Else, calls `getSourceFilesToEmit` with the (optional) target source file to determine the list of source files to emit. - */ - export function forEachEmittedFile( - host: EmitHost, action: (emitFileNames: EmitFileNames, sourceFileOrBundle: SourceFile | Bundle | undefined) => T, - sourceFilesOrTargetSourceFile?: readonly SourceFile[] | SourceFile, - forceDtsEmit = false, - onlyBuildInfo?: boolean, - includeBuildInfo?: boolean) { - const sourceFiles = isArray(sourceFilesOrTargetSourceFile) ? sourceFilesOrTargetSourceFile : getSourceFilesToEmit(host, sourceFilesOrTargetSourceFile, forceDtsEmit); - const options = host.getCompilerOptions(); - if (options.outFile || options.out) { - const prepends = host.getPrependNodes(); - if (sourceFiles.length || prepends.length) { - const bundle = createBundle(sourceFiles, prepends); - const result = action(getOutputPathsFor(bundle, host, forceDtsEmit), bundle); +import { TextRange, fileExtensionIs, Extension, EmitHost, EmitFileNames, SourceFile, Bundle, isArray, getSourceFilesToEmit, createBundle, CompilerOptions, isIncrementalCompilation, removeFileExtension, resolvePath, getRelativePathFromDirectory, combinePaths, getBaseFileName, getEmitDeclarations, getAreDeclarationMapsEnabled, SyntaxKind, getOwnEmitOutputFilePath, isJsonSourceFile, comparePaths, Comparison, getDeclarationEmitOutputFilePath, JsxEmit, isSourceFileJS, LanguageVariant, ParsedCommandLine, getDirectoryPath, Debug, changeExtension, emptyArray, normalizePath, contains, EmitResolver, EmitTransformers, EmitResult, SourceMapEmitResult, createDiagnosticCollection, getNewLineCharacter, createTextWriter, BundleBuildInfo, ExportedModulesFromDeclarationEmit, isBundle, getNormalizedAbsolutePath, ensurePathIsNonModuleName, writeFile, transformNodes, PrinterOptions, isSourceFile, filter, isSourceFileNotJson, length, Node, isExportAssignment, Identifier, isExportSpecifier, forEachChild, Printer, SourceMapGenerator, createSourceMapGenerator, normalizeSlashes, ensureTrailingDirectorySeparator, getSourceFilePathInNewDir, getRootLength, base64encode, sys, getRelativePathToDirectoryOrUrl, BuildInfo, notImplemented, LateBoundDeclaration, OutputFile, ModuleResolutionHost, createNode, createNodeArray, forEach, PrologueDirective, StringLiteral, ProjectReference, CustomTransformers, createInputFiles, createPrependNodes, memoize, returnUndefined, returnFalse, createMultiMap, getTransformers, PrintHandlers, noEmitNotification, noEmitSubstitution, getEmitModuleKind, createMap, EmitTextWriter, BundleFileInfo, BundleFileTextLikeKind, BundleFileSectionKind, SourceMapSource, EmitHint, isIdentifier, isExpression, UnparsedSource, ListFormat, NodeArray, TypeNode, lastOrUndefined, isDeclaration, isVariableStatement, isInternalDeclaration, isBundleFileTextLike, BundleFileTextLike, getTrailingSemicolonDeferringWriter, getLineStarts, Expression, JsxExpression, isStringLiteral, isInJsonFile, cast, isTypeParameterDeclaration, isEmptyStatement, isKeyword, LiteralExpression, UnparsedNode, UnparsedTextLike, UnparsedSyntheticReference, PrivateIdentifier, QualifiedName, ComputedPropertyName, TypeParameterDeclaration, ParameterDeclaration, Decorator, PropertySignature, PropertyDeclaration, MethodSignature, MethodDeclaration, ConstructorDeclaration, AccessorDeclaration, CallSignatureDeclaration, ConstructSignatureDeclaration, IndexSignatureDeclaration, TypePredicateNode, TypeReferenceNode, FunctionTypeNode, JSDocFunctionType, ConstructorTypeNode, TypeQueryNode, TypeLiteralNode, ArrayTypeNode, TupleTypeNode, OptionalTypeNode, UnionTypeNode, IntersectionTypeNode, ConditionalTypeNode, InferTypeNode, ParenthesizedTypeNode, ExpressionWithTypeArguments, TypeOperatorNode, IndexedAccessTypeNode, MappedTypeNode, LiteralTypeNode, ImportTypeNode, JSDocNullableType, JSDocNonNullableType, JSDocOptionalType, RestTypeNode, JSDocVariadicType, ObjectBindingPattern, ArrayBindingPattern, BindingElement, TemplateSpan, Block, VariableStatement, ExpressionStatement, IfStatement, DoStatement, WhileStatement, ForStatement, ForInStatement, ForOfStatement, ContinueStatement, BreakStatement, ReturnStatement, WithStatement, SwitchStatement, LabeledStatement, ThrowStatement, TryStatement, DebuggerStatement, VariableDeclaration, VariableDeclarationList, FunctionDeclaration, ClassDeclaration, InterfaceDeclaration, TypeAliasDeclaration, EnumDeclaration, ModuleDeclaration, ModuleBlock, CaseBlock, NamespaceExportDeclaration, ImportEqualsDeclaration, ImportDeclaration, ImportClause, NamespaceImport, NamespaceExport, NamedImports, ImportSpecifier, ExportAssignment, ExportDeclaration, NamedExports, ExportSpecifier, ExternalModuleReference, JsxText, JsxOpeningElement, JsxClosingElement, JsxAttribute, JsxAttributes, JsxSpreadAttribute, CaseClause, DefaultClause, HeritageClause, CatchClause, PropertyAssignment, ShorthandPropertyAssignment, SpreadAssignment, EnumMember, JSDocPropertyLikeTag, JSDocTypeTag, JSDocImplementsTag, JSDocAugmentsTag, JSDocTemplateTag, JSDocTypedefTag, JSDocCallbackTag, JSDocSignature, JSDocTypeLiteral, JSDocTag, JSDoc, isToken, NumericLiteral, BigIntLiteral, ArrayLiteralExpression, ObjectLiteralExpression, PropertyAccessExpression, ElementAccessExpression, CallExpression, NewExpression, TaggedTemplateExpression, TypeAssertion, ParenthesizedExpression, FunctionExpression, ArrowFunction, DeleteExpression, TypeOfExpression, VoidExpression, AwaitExpression, PrefixUnaryExpression, PostfixUnaryExpression, BinaryExpression, ConditionalExpression, TemplateExpression, YieldExpression, SpreadElement, ClassExpression, AsExpression, NonNullExpression, MetaProperty, JsxElement, JsxSelfClosingElement, JsxFragment, PartiallyEmittedExpression, CommaListExpression, ModuleKind, getExternalHelpersModuleName, isUnparsedSource, hasRecordedExternalHelpers, getEmitHelpers, stableSort, compareEmitHelpers, LiteralLikeNode, isTemplateLiteralKind, UnparsedPrepend, clone, EntityName, getEmitFlags, EmitFlags, ScriptTarget, getDotOrQuestionDotToken, skipPartiallyEmittedExpressions, isNumericLiteral, stringContains, tokenToString, isAccessExpression, getConstantValue, isBinaryExpression, BlockLike, nodeIsSynthesized, isBlock, getParseTreeNode, skipTrivia, positionsAreOnSameLine, isLet, isVarConst, FunctionLikeDeclaration, SignatureDeclaration, rangeIsOnSingleLine, Statement, NodeFlags, ModuleReference, NamedImportsOrExports, ImportOrExportSpecifier, JsxOpeningFragment, isJsxOpeningElement, JsxClosingFragment, isJsxClosingElement, JsxTagNameExpression, rangeStartPositionsAreOnSameLine, getCommentRange, JSDocThisTag, JSDocEnumTag, JSDocReturnTag, JSDocTypeExpression, isPrologueDirective, FileReference, findIndex, UnparsedPrologue, SourceFilePrologueInfo, SourceFilePrologueDirective, getShebang, Modifier, isFunctionLike, singleOrUndefined, isArrowFunction, some, Symbol, guessIndentation, positionIsSynthesized, rangeEndIsOnSameLineAsRangeStart, getStartsOnNewLine, rangeEndPositionsAreOnSameLine, isGeneratedIdentifier, isPrivateIdentifier, getSourceFileOfNode, getOriginalNode, idText, isLiteralExpression, getSourceTextOfNodeFromSourceFile, escapeJsxAttributeString, escapeString, escapeNonAsciiString, getLiteralText, ForInOrOfStatement, CaseOrDefaultClause, NamedDeclaration, BindingPattern, DeclarationName, isBindingPattern, GeneratedIdentifier, GeneratedIdentifierFlags, getNodeId, isNodeDescendantOf, escapeLeadingUnderscores, SymbolFlags, CharacterCodes, getExternalModuleName, makeIdentifierFromModuleName, getSyntheticLeadingComments, getSyntheticTrailingComments, SynthesizedComment, computeLineStarts, writeCommentRange, isJSDocLikeText, isPinnedComment, emitNewLineBeforeLeadingCommentOfPosition, forEachLeadingCommentRange, forEachTrailingCommentRange, last, emitDetachedComments, isRecognizedTripleSlashComment, tryParseRawSourceMap, isUnparsedPrepend, isUnparsedNode, getSourceMapRange, getLineAndCharacterOfPosition } from "./ts"; +import { createTimer, createTimerIf } from "./ts.performance"; +import * as ts from "./ts"; +const brackets = createBracketsMap(); +const syntheticParent: TextRange = { pos: -1, end: -1 }; +/*@internal*/ +export function isBuildInfoFile(file: string) { + return fileExtensionIs(file, Extension.TsBuildInfo); +} +/*@internal*/ +/** + * Iterates over the source files that are expected to have an emit output. + * + * @param host An EmitHost. + * @param action The action to execute. + * @param sourceFilesOrTargetSourceFile + * If an array, the full list of source files to emit. + * Else, calls `getSourceFilesToEmit` with the (optional) target source file to determine the list of source files to emit. + */ +export function forEachEmittedFile(host: EmitHost, action: (emitFileNames: EmitFileNames, sourceFileOrBundle: SourceFile | Bundle | undefined) => T, sourceFilesOrTargetSourceFile?: readonly SourceFile[] | SourceFile, forceDtsEmit = false, onlyBuildInfo?: boolean, includeBuildInfo?: boolean) { + const sourceFiles = isArray(sourceFilesOrTargetSourceFile) ? sourceFilesOrTargetSourceFile : getSourceFilesToEmit(host, sourceFilesOrTargetSourceFile, forceDtsEmit); + const options = host.getCompilerOptions(); + if (options.outFile || options.out) { + const prepends = host.getPrependNodes(); + if (sourceFiles.length || prepends.length) { + const bundle = createBundle(sourceFiles, prepends); + const result = action(getOutputPathsFor(bundle, host, forceDtsEmit), bundle); + if (result) { + return result; + } + } + } + else { + if (!onlyBuildInfo) { + for (const sourceFile of sourceFiles) { + const result = action(getOutputPathsFor(sourceFile, host, forceDtsEmit), sourceFile); if (result) { return result; } } } - else { - if (!onlyBuildInfo) { - for (const sourceFile of sourceFiles) { - const result = action(getOutputPathsFor(sourceFile, host, forceDtsEmit), sourceFile); - if (result) { - return result; - } - } - } - if (includeBuildInfo) { - const buildInfoPath = getTsBuildInfoEmitOutputFilePath(host.getCompilerOptions()); - if (buildInfoPath) return action({ buildInfoPath }, /*sourceFileOrBundle*/ undefined); - } + if (includeBuildInfo) { + const buildInfoPath = getTsBuildInfoEmitOutputFilePath(host.getCompilerOptions()); + if (buildInfoPath) + return action({ buildInfoPath }, /*sourceFileOrBundle*/ undefined); } } - - export function getTsBuildInfoEmitOutputFilePath(options: CompilerOptions) { - const configFile = options.configFilePath; - if (!isIncrementalCompilation(options)) return undefined; - if (options.tsBuildInfoFile) return options.tsBuildInfoFile; - const outPath = options.outFile || options.out; - let buildInfoExtensionLess: string; - if (outPath) { - buildInfoExtensionLess = removeFileExtension(outPath); - } - else { - if (!configFile) return undefined; - const configFileExtensionLess = removeFileExtension(configFile); - buildInfoExtensionLess = options.outDir ? - options.rootDir ? - resolvePath(options.outDir, getRelativePathFromDirectory(options.rootDir, configFileExtensionLess, /*ignoreCase*/ true)) : - combinePaths(options.outDir, getBaseFileName(configFileExtensionLess)) : - configFileExtensionLess; - } - return buildInfoExtensionLess + Extension.TsBuildInfo; - } - - /*@internal*/ - export function getOutputPathsForBundle(options: CompilerOptions, forceDtsPaths: boolean): EmitFileNames { - const outPath = options.outFile || options.out!; - const jsFilePath = options.emitDeclarationOnly ? undefined : outPath; - const sourceMapFilePath = jsFilePath && getSourceMapFilePath(jsFilePath, options); - const declarationFilePath = (forceDtsPaths || getEmitDeclarations(options)) ? removeFileExtension(outPath) + Extension.Dts : undefined; +} +export function getTsBuildInfoEmitOutputFilePath(options: CompilerOptions) { + const configFile = options.configFilePath; + if (!isIncrementalCompilation(options)) + return undefined; + if (options.tsBuildInfoFile) + return options.tsBuildInfoFile; + const outPath = options.outFile || options.out; + let buildInfoExtensionLess: string; + if (outPath) { + buildInfoExtensionLess = removeFileExtension(outPath); + } + else { + if (!configFile) + return undefined; + const configFileExtensionLess = removeFileExtension(configFile); + buildInfoExtensionLess = options.outDir ? + options.rootDir ? + resolvePath(options.outDir, getRelativePathFromDirectory(options.rootDir, configFileExtensionLess, /*ignoreCase*/ true)) : + combinePaths(options.outDir, getBaseFileName(configFileExtensionLess)) : + configFileExtensionLess; + } + return buildInfoExtensionLess + Extension.TsBuildInfo; +} +/*@internal*/ +export function getOutputPathsForBundle(options: CompilerOptions, forceDtsPaths: boolean): EmitFileNames { + const outPath = options.outFile || options.out!; + const jsFilePath = options.emitDeclarationOnly ? undefined : outPath; + const sourceMapFilePath = jsFilePath && getSourceMapFilePath(jsFilePath, options); + const declarationFilePath = (forceDtsPaths || getEmitDeclarations(options)) ? removeFileExtension(outPath) + Extension.Dts : undefined; + const declarationMapPath = declarationFilePath && getAreDeclarationMapsEnabled(options) ? declarationFilePath + ".map" : undefined; + const buildInfoPath = getTsBuildInfoEmitOutputFilePath(options); + return { jsFilePath, sourceMapFilePath, declarationFilePath, declarationMapPath, buildInfoPath }; +} +/*@internal*/ +export function getOutputPathsFor(sourceFile: SourceFile | Bundle, host: EmitHost, forceDtsPaths: boolean): EmitFileNames { + const options = host.getCompilerOptions(); + if (sourceFile.kind === SyntaxKind.Bundle) { + return getOutputPathsForBundle(options, forceDtsPaths); + } + else { + const ownOutputFilePath = getOwnEmitOutputFilePath(sourceFile.fileName, host, getOutputExtension(sourceFile, options)); + const isJsonFile = isJsonSourceFile(sourceFile); + // If json file emits to the same location skip writing it, if emitDeclarationOnly skip writing it + const isJsonEmittedToSameLocation = isJsonFile && + comparePaths(sourceFile.fileName, ownOutputFilePath, host.getCurrentDirectory(), !host.useCaseSensitiveFileNames()) === Comparison.EqualTo; + const jsFilePath = options.emitDeclarationOnly || isJsonEmittedToSameLocation ? undefined : ownOutputFilePath; + const sourceMapFilePath = !jsFilePath || isJsonSourceFile(sourceFile) ? undefined : getSourceMapFilePath(jsFilePath, options); + const declarationFilePath = (forceDtsPaths || (getEmitDeclarations(options) && !isJsonFile)) ? getDeclarationEmitOutputFilePath(sourceFile.fileName, host) : undefined; const declarationMapPath = declarationFilePath && getAreDeclarationMapsEnabled(options) ? declarationFilePath + ".map" : undefined; - const buildInfoPath = getTsBuildInfoEmitOutputFilePath(options); - return { jsFilePath, sourceMapFilePath, declarationFilePath, declarationMapPath, buildInfoPath }; + return { jsFilePath, sourceMapFilePath, declarationFilePath, declarationMapPath, buildInfoPath: undefined }; } - - /*@internal*/ - export function getOutputPathsFor(sourceFile: SourceFile | Bundle, host: EmitHost, forceDtsPaths: boolean): EmitFileNames { - const options = host.getCompilerOptions(); - if (sourceFile.kind === SyntaxKind.Bundle) { - return getOutputPathsForBundle(options, forceDtsPaths); - } - else { - const ownOutputFilePath = getOwnEmitOutputFilePath(sourceFile.fileName, host, getOutputExtension(sourceFile, options)); - const isJsonFile = isJsonSourceFile(sourceFile); - // If json file emits to the same location skip writing it, if emitDeclarationOnly skip writing it - const isJsonEmittedToSameLocation = isJsonFile && - comparePaths(sourceFile.fileName, ownOutputFilePath, host.getCurrentDirectory(), !host.useCaseSensitiveFileNames()) === Comparison.EqualTo; - const jsFilePath = options.emitDeclarationOnly || isJsonEmittedToSameLocation ? undefined : ownOutputFilePath; - const sourceMapFilePath = !jsFilePath || isJsonSourceFile(sourceFile) ? undefined : getSourceMapFilePath(jsFilePath, options); - const declarationFilePath = (forceDtsPaths || (getEmitDeclarations(options) && !isJsonFile)) ? getDeclarationEmitOutputFilePath(sourceFile.fileName, host) : undefined; - const declarationMapPath = declarationFilePath && getAreDeclarationMapsEnabled(options) ? declarationFilePath + ".map" : undefined; - return { jsFilePath, sourceMapFilePath, declarationFilePath, declarationMapPath, buildInfoPath: undefined }; - } - } - - function getSourceMapFilePath(jsFilePath: string, options: CompilerOptions) { - return (options.sourceMap && !options.inlineSourceMap) ? jsFilePath + ".map" : undefined; - } - - // JavaScript files are always LanguageVariant.JSX, as JSX syntax is allowed in .js files also. - // So for JavaScript files, '.jsx' is only emitted if the input was '.jsx', and JsxEmit.Preserve. - // For TypeScript, the only time to emit with a '.jsx' extension, is on JSX input, and JsxEmit.Preserve - /* @internal */ - export function getOutputExtension(sourceFile: SourceFile, options: CompilerOptions): Extension { - if (isJsonSourceFile(sourceFile)) { - return Extension.Json; - } - - if (options.jsx === JsxEmit.Preserve) { - if (isSourceFileJS(sourceFile)) { - if (fileExtensionIs(sourceFile.fileName, Extension.Jsx)) { - return Extension.Jsx; - } - } - else if (sourceFile.languageVariant === LanguageVariant.JSX) { - // TypeScript source file preserving JSX syntax +} +function getSourceMapFilePath(jsFilePath: string, options: CompilerOptions) { + return (options.sourceMap && !options.inlineSourceMap) ? jsFilePath + ".map" : undefined; +} +// JavaScript files are always LanguageVariant.JSX, as JSX syntax is allowed in .js files also. +// So for JavaScript files, '.jsx' is only emitted if the input was '.jsx', and JsxEmit.Preserve. +// For TypeScript, the only time to emit with a '.jsx' extension, is on JSX input, and JsxEmit.Preserve +/* @internal */ +export function getOutputExtension(sourceFile: SourceFile, options: CompilerOptions): Extension { + if (isJsonSourceFile(sourceFile)) { + return Extension.Json; + } + if (options.jsx === JsxEmit.Preserve) { + if (isSourceFileJS(sourceFile)) { + if (fileExtensionIs(sourceFile.fileName, Extension.Jsx)) { return Extension.Jsx; } } - return Extension.Js; - } - - function rootDirOfOptions(configFile: ParsedCommandLine) { - return configFile.options.rootDir || getDirectoryPath(Debug.checkDefined(configFile.options.configFilePath)); - } - - function getOutputPathWithoutChangingExt(inputFileName: string, configFile: ParsedCommandLine, ignoreCase: boolean, outputDir: string | undefined) { - return outputDir ? - resolvePath( - outputDir, - getRelativePathFromDirectory(rootDirOfOptions(configFile), inputFileName, ignoreCase) - ) : - inputFileName; - } - - /* @internal */ - export function getOutputDeclarationFileName(inputFileName: string, configFile: ParsedCommandLine, ignoreCase: boolean) { - Debug.assert(!fileExtensionIs(inputFileName, Extension.Dts) && !fileExtensionIs(inputFileName, Extension.Json)); - return changeExtension( - getOutputPathWithoutChangingExt(inputFileName, configFile, ignoreCase, configFile.options.declarationDir || configFile.options.outDir), - Extension.Dts - ); - } - - function getOutputJSFileName(inputFileName: string, configFile: ParsedCommandLine, ignoreCase: boolean) { - if (configFile.options.emitDeclarationOnly) return undefined; - const isJsonFile = fileExtensionIs(inputFileName, Extension.Json); - const outputFileName = changeExtension( - getOutputPathWithoutChangingExt(inputFileName, configFile, ignoreCase, configFile.options.outDir), - isJsonFile ? - Extension.Json : - fileExtensionIs(inputFileName, Extension.Tsx) && configFile.options.jsx === JsxEmit.Preserve ? - Extension.Jsx : - Extension.Js - ); - return !isJsonFile || comparePaths(inputFileName, outputFileName, Debug.checkDefined(configFile.options.configFilePath), ignoreCase) !== Comparison.EqualTo ? - outputFileName : - undefined; - } - - function createAddOutput() { - let outputs: string[] | undefined; - return { addOutput, getOutputs }; - function addOutput(path: string | undefined) { - if (path) { - (outputs || (outputs = [])).push(path); - } + else if (sourceFile.languageVariant === LanguageVariant.JSX) { + // TypeScript source file preserving JSX syntax + return Extension.Jsx; } - function getOutputs(): readonly string[] { - return outputs || emptyArray; + } + return Extension.Js; +} +function rootDirOfOptions(configFile: ParsedCommandLine) { + return configFile.options.rootDir || getDirectoryPath(Debug.checkDefined(configFile.options.configFilePath)); +} +function getOutputPathWithoutChangingExt(inputFileName: string, configFile: ParsedCommandLine, ignoreCase: boolean, outputDir: string | undefined) { + return outputDir ? + resolvePath(outputDir, getRelativePathFromDirectory(rootDirOfOptions(configFile), inputFileName, ignoreCase)) : + inputFileName; +} +/* @internal */ +export function getOutputDeclarationFileName(inputFileName: string, configFile: ParsedCommandLine, ignoreCase: boolean) { + Debug.assert(!fileExtensionIs(inputFileName, Extension.Dts) && !fileExtensionIs(inputFileName, Extension.Json)); + return changeExtension(getOutputPathWithoutChangingExt(inputFileName, configFile, ignoreCase, configFile.options.declarationDir || configFile.options.outDir), Extension.Dts); +} +function getOutputJSFileName(inputFileName: string, configFile: ParsedCommandLine, ignoreCase: boolean) { + if (configFile.options.emitDeclarationOnly) + return undefined; + const isJsonFile = fileExtensionIs(inputFileName, Extension.Json); + const outputFileName = changeExtension(getOutputPathWithoutChangingExt(inputFileName, configFile, ignoreCase, configFile.options.outDir), isJsonFile ? + Extension.Json : + fileExtensionIs(inputFileName, Extension.Tsx) && configFile.options.jsx === JsxEmit.Preserve ? + Extension.Jsx : + Extension.Js); + return !isJsonFile || comparePaths(inputFileName, outputFileName, Debug.checkDefined(configFile.options.configFilePath), ignoreCase) !== Comparison.EqualTo ? + outputFileName : + undefined; +} +function createAddOutput() { + let outputs: string[] | undefined; + return { addOutput, getOutputs }; + function addOutput(path: string | undefined) { + if (path) { + (outputs || (outputs = [])).push(path); } } - - function getSingleOutputFileNames(configFile: ParsedCommandLine, addOutput: ReturnType["addOutput"]) { - const { jsFilePath, sourceMapFilePath, declarationFilePath, declarationMapPath, buildInfoPath } = getOutputPathsForBundle(configFile.options, /*forceDtsPaths*/ false); - addOutput(jsFilePath); - addOutput(sourceMapFilePath); - addOutput(declarationFilePath); - addOutput(declarationMapPath); - addOutput(buildInfoPath); + function getOutputs(): readonly string[] { + return outputs || emptyArray; } - - function getOwnOutputFileNames(configFile: ParsedCommandLine, inputFileName: string, ignoreCase: boolean, addOutput: ReturnType["addOutput"]) { - if (fileExtensionIs(inputFileName, Extension.Dts)) return; - const js = getOutputJSFileName(inputFileName, configFile, ignoreCase); - addOutput(js); - if (fileExtensionIs(inputFileName, Extension.Json)) return; - if (js && configFile.options.sourceMap) { - addOutput(`${js}.map`); - } - if (getEmitDeclarations(configFile.options)) { - const dts = getOutputDeclarationFileName(inputFileName, configFile, ignoreCase); - addOutput(dts); - if (configFile.options.declarationMap) { - addOutput(`${dts}.map`); - } - } +} +function getSingleOutputFileNames(configFile: ParsedCommandLine, addOutput: ReturnType["addOutput"]) { + const { jsFilePath, sourceMapFilePath, declarationFilePath, declarationMapPath, buildInfoPath } = getOutputPathsForBundle(configFile.options, /*forceDtsPaths*/ false); + addOutput(jsFilePath); + addOutput(sourceMapFilePath); + addOutput(declarationFilePath); + addOutput(declarationMapPath); + addOutput(buildInfoPath); +} +function getOwnOutputFileNames(configFile: ParsedCommandLine, inputFileName: string, ignoreCase: boolean, addOutput: ReturnType["addOutput"]) { + if (fileExtensionIs(inputFileName, Extension.Dts)) + return; + const js = getOutputJSFileName(inputFileName, configFile, ignoreCase); + addOutput(js); + if (fileExtensionIs(inputFileName, Extension.Json)) + return; + if (js && configFile.options.sourceMap) { + addOutput(`${js}.map`); } - - /*@internal*/ - export function getAllProjectOutputs(configFile: ParsedCommandLine, ignoreCase: boolean): readonly string[] { - const { addOutput, getOutputs } = createAddOutput(); - if (configFile.options.outFile || configFile.options.out) { - getSingleOutputFileNames(configFile, addOutput); - } - else { - for (const inputFileName of configFile.fileNames) { - getOwnOutputFileNames(configFile, inputFileName, ignoreCase, addOutput); - } - addOutput(getTsBuildInfoEmitOutputFilePath(configFile.options)); + if (getEmitDeclarations(configFile.options)) { + const dts = getOutputDeclarationFileName(inputFileName, configFile, ignoreCase); + addOutput(dts); + if (configFile.options.declarationMap) { + addOutput(`${dts}.map`); } - return getOutputs(); } - - export function getOutputFileNames(commandLine: ParsedCommandLine, inputFileName: string, ignoreCase: boolean): readonly string[] { - inputFileName = normalizePath(inputFileName); - Debug.assert(contains(commandLine.fileNames, inputFileName), `Expected fileName to be present in command line`); - const { addOutput, getOutputs } = createAddOutput(); - if (commandLine.options.outFile || commandLine.options.out) { - getSingleOutputFileNames(commandLine, addOutput); - } - else { - getOwnOutputFileNames(commandLine, inputFileName, ignoreCase, addOutput); - } - return getOutputs(); +} +/*@internal*/ +export function getAllProjectOutputs(configFile: ParsedCommandLine, ignoreCase: boolean): readonly string[] { + const { addOutput, getOutputs } = createAddOutput(); + if (configFile.options.outFile || configFile.options.out) { + getSingleOutputFileNames(configFile, addOutput); } - - /*@internal*/ - export function getFirstProjectOutput(configFile: ParsedCommandLine, ignoreCase: boolean): string { - if (configFile.options.outFile || configFile.options.out) { - const { jsFilePath } = getOutputPathsForBundle(configFile.options, /*forceDtsPaths*/ false); - return Debug.checkDefined(jsFilePath, `project ${configFile.options.configFilePath} expected to have at least one output`); - } - + else { for (const inputFileName of configFile.fileNames) { - if (fileExtensionIs(inputFileName, Extension.Dts)) continue; - const jsFilePath = getOutputJSFileName(inputFileName, configFile, ignoreCase); - if (jsFilePath) return jsFilePath; - if (fileExtensionIs(inputFileName, Extension.Json)) continue; - if (getEmitDeclarations(configFile.options)) { - return getOutputDeclarationFileName(inputFileName, configFile, ignoreCase); - } + getOwnOutputFileNames(configFile, inputFileName, ignoreCase, addOutput); } - const buildInfoPath = getTsBuildInfoEmitOutputFilePath(configFile.options); - if (buildInfoPath) return buildInfoPath; - return Debug.fail(`project ${configFile.options.configFilePath} expected to have at least one output`); - } - - /*@internal*/ - // targetSourceFile is when users only want one file in entire project to be emitted. This is used in compileOnSave feature - export function emitFiles(resolver: EmitResolver, host: EmitHost, targetSourceFile: SourceFile | undefined, { scriptTransformers, declarationTransformers }: EmitTransformers, emitOnlyDtsFiles?: boolean, onlyBuildInfo?: boolean, forceDtsEmit?: boolean): EmitResult { - const compilerOptions = host.getCompilerOptions(); - const sourceMapDataList: SourceMapEmitResult[] | undefined = (compilerOptions.sourceMap || compilerOptions.inlineSourceMap || getAreDeclarationMapsEnabled(compilerOptions)) ? [] : undefined; - const emittedFilesList: string[] | undefined = compilerOptions.listEmittedFiles ? [] : undefined; - const emitterDiagnostics = createDiagnosticCollection(); - const newLine = getNewLineCharacter(compilerOptions, () => host.getNewLine()); - const writer = createTextWriter(newLine); - const { enter, exit } = performance.createTimer("printTime", "beforePrint", "afterPrint"); - let bundleBuildInfo: BundleBuildInfo | undefined; - let emitSkipped = false; - let exportedModulesFromDeclarationEmit: ExportedModulesFromDeclarationEmit | undefined; - - // Emit each output file - enter(); - forEachEmittedFile( - host, - emitSourceFileOrBundle, - getSourceFilesToEmit(host, targetSourceFile, forceDtsEmit), - forceDtsEmit, - onlyBuildInfo, - !targetSourceFile - ); - exit(); - - - return { - emitSkipped, - diagnostics: emitterDiagnostics.getDiagnostics(), - emittedFiles: emittedFilesList, - sourceMaps: sourceMapDataList, - exportedModulesFromDeclarationEmit - }; - - function emitSourceFileOrBundle({ jsFilePath, sourceMapFilePath, declarationFilePath, declarationMapPath, buildInfoPath }: EmitFileNames, sourceFileOrBundle: SourceFile | Bundle | undefined) { - let buildInfoDirectory: string | undefined; - if (buildInfoPath && sourceFileOrBundle && isBundle(sourceFileOrBundle)) { - buildInfoDirectory = getDirectoryPath(getNormalizedAbsolutePath(buildInfoPath, host.getCurrentDirectory())); - bundleBuildInfo = { - commonSourceDirectory: relativeToBuildInfo(host.getCommonSourceDirectory()), - sourceFiles: sourceFileOrBundle.sourceFiles.map(file => relativeToBuildInfo(getNormalizedAbsolutePath(file.fileName, host.getCurrentDirectory()))) - }; - } - emitJsFileOrBundle(sourceFileOrBundle, jsFilePath, sourceMapFilePath, relativeToBuildInfo); - emitDeclarationFileOrBundle(sourceFileOrBundle, declarationFilePath, declarationMapPath, relativeToBuildInfo); - emitBuildInfo(bundleBuildInfo, buildInfoPath); - - if (!emitSkipped && emittedFilesList) { - if (!emitOnlyDtsFiles) { - if (jsFilePath) { - emittedFilesList.push(jsFilePath); - } - if (sourceMapFilePath) { - emittedFilesList.push(sourceMapFilePath); - } - if (buildInfoPath) { - emittedFilesList.push(buildInfoPath); - } - } - if (declarationFilePath) { - emittedFilesList.push(declarationFilePath); - } - if (declarationMapPath) { - emittedFilesList.push(declarationMapPath); - } - } - - function relativeToBuildInfo(path: string) { - return ensurePathIsNonModuleName(getRelativePathFromDirectory(buildInfoDirectory!, path, host.getCanonicalFileName)); - } + addOutput(getTsBuildInfoEmitOutputFilePath(configFile.options)); + } + return getOutputs(); +} +export function getOutputFileNames(commandLine: ParsedCommandLine, inputFileName: string, ignoreCase: boolean): readonly string[] { + inputFileName = normalizePath(inputFileName); + Debug.assert(contains(commandLine.fileNames, inputFileName), `Expected fileName to be present in command line`); + const { addOutput, getOutputs } = createAddOutput(); + if (commandLine.options.outFile || commandLine.options.out) { + getSingleOutputFileNames(commandLine, addOutput); + } + else { + getOwnOutputFileNames(commandLine, inputFileName, ignoreCase, addOutput); + } + return getOutputs(); +} +/*@internal*/ +export function getFirstProjectOutput(configFile: ParsedCommandLine, ignoreCase: boolean): string { + if (configFile.options.outFile || configFile.options.out) { + const { jsFilePath } = getOutputPathsForBundle(configFile.options, /*forceDtsPaths*/ false); + return Debug.checkDefined(jsFilePath, `project ${configFile.options.configFilePath} expected to have at least one output`); + } + for (const inputFileName of configFile.fileNames) { + if (fileExtensionIs(inputFileName, Extension.Dts)) + continue; + const jsFilePath = getOutputJSFileName(inputFileName, configFile, ignoreCase); + if (jsFilePath) + return jsFilePath; + if (fileExtensionIs(inputFileName, Extension.Json)) + continue; + if (getEmitDeclarations(configFile.options)) { + return getOutputDeclarationFileName(inputFileName, configFile, ignoreCase); } - - function emitBuildInfo(bundle: BundleBuildInfo | undefined, buildInfoPath: string | undefined) { - // Write build information if applicable - if (!buildInfoPath || targetSourceFile || emitSkipped) return; - const program = host.getProgramBuildInfo(); - if (host.isEmitBlocked(buildInfoPath) || compilerOptions.noEmit) { - emitSkipped = true; - return; - } - const version = ts.version; // Extracted into a const so the form is stable between namespace and module - writeFile(host, emitterDiagnostics, buildInfoPath, getBuildInfoText({ bundle, program, version }), /*writeByteOrderMark*/ false); - } - - function emitJsFileOrBundle( - sourceFileOrBundle: SourceFile | Bundle | undefined, - jsFilePath: string | undefined, - sourceMapFilePath: string | undefined, - relativeToBuildInfo: (path: string) => string) { - if (!sourceFileOrBundle || emitOnlyDtsFiles || !jsFilePath) { - return; - } - - // Make sure not to write js file and source map file if any of them cannot be written - if ((jsFilePath && host.isEmitBlocked(jsFilePath)) || compilerOptions.noEmit) { - emitSkipped = true; - return; - } - // Transform the source files - const transform = transformNodes(resolver, host, compilerOptions, [sourceFileOrBundle], scriptTransformers, /*allowDtsFiles*/ false); - - const printerOptions: PrinterOptions = { - removeComments: compilerOptions.removeComments, - newLine: compilerOptions.newLine, - noEmitHelpers: compilerOptions.noEmitHelpers, - module: compilerOptions.module, - target: compilerOptions.target, - sourceMap: compilerOptions.sourceMap, - inlineSourceMap: compilerOptions.inlineSourceMap, - inlineSources: compilerOptions.inlineSources, - extendedDiagnostics: compilerOptions.extendedDiagnostics, - writeBundleFileInfo: !!bundleBuildInfo, - relativeToBuildInfo - }; - - // Create a printer to print the nodes - const printer = createPrinter(printerOptions, { - // resolver hooks - hasGlobalName: resolver.hasGlobalName, - - // transform hooks - onEmitNode: transform.emitNodeWithNotification, - isEmitNotificationEnabled: transform.isEmitNotificationEnabled, - substituteNode: transform.substituteNode, - }); - - Debug.assert(transform.transformed.length === 1, "Should only see one output from the transform"); - printSourceFileOrBundle(jsFilePath, sourceMapFilePath, transform.transformed[0], printer, compilerOptions); - - // Clean up emit nodes on parse tree - transform.dispose(); - if (bundleBuildInfo) bundleBuildInfo.js = printer.bundleFileInfo; - } - - function emitDeclarationFileOrBundle( - sourceFileOrBundle: SourceFile | Bundle | undefined, - declarationFilePath: string | undefined, - declarationMapPath: string | undefined, - relativeToBuildInfo: (path: string) => string) { - if (!sourceFileOrBundle) return; - if (!declarationFilePath) { - if (emitOnlyDtsFiles || compilerOptions.emitDeclarationOnly) emitSkipped = true; - return; - } - const sourceFiles = isSourceFile(sourceFileOrBundle) ? [sourceFileOrBundle] : sourceFileOrBundle.sourceFiles; - const filesForEmit = forceDtsEmit ? sourceFiles : filter(sourceFiles, isSourceFileNotJson); - // Setup and perform the transformation to retrieve declarations from the input files - const inputListOrBundle = (compilerOptions.outFile || compilerOptions.out) ? [createBundle(filesForEmit, !isSourceFile(sourceFileOrBundle) ? sourceFileOrBundle.prepends : undefined)] : filesForEmit; - if (emitOnlyDtsFiles && !getEmitDeclarations(compilerOptions)) { - // Checker wont collect the linked aliases since thats only done when declaration is enabled. - // Do that here when emitting only dts files - filesForEmit.forEach(collectLinkedAliases); - } - const declarationTransform = transformNodes(resolver, host, compilerOptions, inputListOrBundle, declarationTransformers, /*allowDtsFiles*/ false); - if (length(declarationTransform.diagnostics)) { - for (const diagnostic of declarationTransform.diagnostics!) { - emitterDiagnostics.add(diagnostic); - } - } - - const printerOptions: PrinterOptions = { - removeComments: compilerOptions.removeComments, - newLine: compilerOptions.newLine, - noEmitHelpers: true, - module: compilerOptions.module, - target: compilerOptions.target, - sourceMap: compilerOptions.sourceMap, - inlineSourceMap: compilerOptions.inlineSourceMap, - extendedDiagnostics: compilerOptions.extendedDiagnostics, - onlyPrintJsDocStyle: true, - writeBundleFileInfo: !!bundleBuildInfo, - recordInternalSection: !!bundleBuildInfo, - relativeToBuildInfo + } + const buildInfoPath = getTsBuildInfoEmitOutputFilePath(configFile.options); + if (buildInfoPath) + return buildInfoPath; + return Debug.fail(`project ${configFile.options.configFilePath} expected to have at least one output`); +} +/*@internal*/ +// targetSourceFile is when users only want one file in entire project to be emitted. This is used in compileOnSave feature +export function emitFiles(resolver: EmitResolver, host: EmitHost, targetSourceFile: SourceFile | undefined, { scriptTransformers, declarationTransformers }: EmitTransformers, emitOnlyDtsFiles?: boolean, onlyBuildInfo?: boolean, forceDtsEmit?: boolean): EmitResult { + const compilerOptions = host.getCompilerOptions(); + const sourceMapDataList: SourceMapEmitResult[] | undefined = (compilerOptions.sourceMap || compilerOptions.inlineSourceMap || getAreDeclarationMapsEnabled(compilerOptions)) ? [] : undefined; + const emittedFilesList: string[] | undefined = compilerOptions.listEmittedFiles ? [] : undefined; + const emitterDiagnostics = createDiagnosticCollection(); + const newLine = getNewLineCharacter(compilerOptions, () => host.getNewLine()); + const writer = createTextWriter(newLine); + const { enter, exit } = createTimer("printTime", "beforePrint", "afterPrint"); + let bundleBuildInfo: BundleBuildInfo | undefined; + let emitSkipped = false; + let exportedModulesFromDeclarationEmit: ExportedModulesFromDeclarationEmit | undefined; + // Emit each output file + enter(); + forEachEmittedFile(host, emitSourceFileOrBundle, getSourceFilesToEmit(host, targetSourceFile, forceDtsEmit), forceDtsEmit, onlyBuildInfo, !targetSourceFile); + exit(); + return { + emitSkipped, + diagnostics: emitterDiagnostics.getDiagnostics(), + emittedFiles: emittedFilesList, + sourceMaps: sourceMapDataList, + exportedModulesFromDeclarationEmit + }; + function emitSourceFileOrBundle({ jsFilePath, sourceMapFilePath, declarationFilePath, declarationMapPath, buildInfoPath }: EmitFileNames, sourceFileOrBundle: SourceFile | Bundle | undefined) { + let buildInfoDirectory: string | undefined; + if (buildInfoPath && sourceFileOrBundle && isBundle(sourceFileOrBundle)) { + buildInfoDirectory = getDirectoryPath(getNormalizedAbsolutePath(buildInfoPath, host.getCurrentDirectory())); + bundleBuildInfo = { + commonSourceDirectory: relativeToBuildInfo(host.getCommonSourceDirectory()), + sourceFiles: sourceFileOrBundle.sourceFiles.map(file => relativeToBuildInfo(getNormalizedAbsolutePath(file.fileName, host.getCurrentDirectory()))) }; - - const declarationPrinter = createPrinter(printerOptions, { - // resolver hooks - hasGlobalName: resolver.hasGlobalName, - - // transform hooks - onEmitNode: declarationTransform.emitNodeWithNotification, - isEmitNotificationEnabled: declarationTransform.isEmitNotificationEnabled, - substituteNode: declarationTransform.substituteNode, - }); - const declBlocked = (!!declarationTransform.diagnostics && !!declarationTransform.diagnostics.length) || !!host.isEmitBlocked(declarationFilePath) || !!compilerOptions.noEmit; - emitSkipped = emitSkipped || declBlocked; - if (!declBlocked || forceDtsEmit) { - Debug.assert(declarationTransform.transformed.length === 1, "Should only see one output from the decl transform"); - printSourceFileOrBundle( - declarationFilePath, - declarationMapPath, - declarationTransform.transformed[0], - declarationPrinter, - { - sourceMap: compilerOptions.declarationMap, - sourceRoot: compilerOptions.sourceRoot, - mapRoot: compilerOptions.mapRoot, - extendedDiagnostics: compilerOptions.extendedDiagnostics, - // Explicitly do not passthru either `inline` option - } - ); - if (forceDtsEmit && declarationTransform.transformed[0].kind === SyntaxKind.SourceFile) { - const sourceFile = declarationTransform.transformed[0]; - exportedModulesFromDeclarationEmit = sourceFile.exportedModulesFromDeclarationEmit; - } - } - declarationTransform.dispose(); - if (bundleBuildInfo) bundleBuildInfo.dts = declarationPrinter.bundleFileInfo; - } - - function collectLinkedAliases(node: Node) { - if (isExportAssignment(node)) { - if (node.expression.kind === SyntaxKind.Identifier) { - resolver.collectLinkedAliases(node.expression as Identifier, /*setVisibility*/ true); - } - return; - } - else if (isExportSpecifier(node)) { - resolver.collectLinkedAliases(node.propertyName || node.name, /*setVisibility*/ true); - return; - } - forEachChild(node, collectLinkedAliases); - } - - function printSourceFileOrBundle(jsFilePath: string, sourceMapFilePath: string | undefined, sourceFileOrBundle: SourceFile | Bundle, printer: Printer, mapOptions: SourceMapOptions) { - const bundle = sourceFileOrBundle.kind === SyntaxKind.Bundle ? sourceFileOrBundle : undefined; - const sourceFile = sourceFileOrBundle.kind === SyntaxKind.SourceFile ? sourceFileOrBundle : undefined; - const sourceFiles = bundle ? bundle.sourceFiles : [sourceFile!]; - - let sourceMapGenerator: SourceMapGenerator | undefined; - if (shouldEmitSourceMaps(mapOptions, sourceFileOrBundle)) { - sourceMapGenerator = createSourceMapGenerator( - host, - getBaseFileName(normalizeSlashes(jsFilePath)), - getSourceRoot(mapOptions), - getSourceMapDirectory(mapOptions, jsFilePath, sourceFile), - mapOptions); - } - - if (bundle) { - printer.writeBundle(bundle, writer, sourceMapGenerator); - } - else { - printer.writeFile(sourceFile!, writer, sourceMapGenerator); - } - - if (sourceMapGenerator) { - if (sourceMapDataList) { - sourceMapDataList.push({ - inputSourceFileNames: sourceMapGenerator.getSources(), - sourceMap: sourceMapGenerator.toJSON() - }); - } - - const sourceMappingURL = getSourceMappingURL( - mapOptions, - sourceMapGenerator, - jsFilePath, - sourceMapFilePath, - sourceFile); - - if (sourceMappingURL) { - if (!writer.isAtStartOfLine()) writer.rawWrite(newLine); - writer.writeComment(`//# ${"sourceMappingURL"}=${sourceMappingURL}`); // Tools can sometimes see this line as a source mapping url comment + } + emitJsFileOrBundle(sourceFileOrBundle, jsFilePath, sourceMapFilePath, relativeToBuildInfo); + emitDeclarationFileOrBundle(sourceFileOrBundle, declarationFilePath, declarationMapPath, relativeToBuildInfo); + emitBuildInfo(bundleBuildInfo, buildInfoPath); + if (!emitSkipped && emittedFilesList) { + if (!emitOnlyDtsFiles) { + if (jsFilePath) { + emittedFilesList.push(jsFilePath); } - - // Write the source map if (sourceMapFilePath) { - const sourceMap = sourceMapGenerator.toString(); - writeFile(host, emitterDiagnostics, sourceMapFilePath, sourceMap, /*writeByteOrderMark*/ false, sourceFiles); - } - } - else { - writer.writeLine(); - } - - // Write the output file - writeFile(host, emitterDiagnostics, jsFilePath, writer.getText(), !!compilerOptions.emitBOM, sourceFiles); - - // Reset state - writer.clear(); - } - - interface SourceMapOptions { - sourceMap?: boolean; - inlineSourceMap?: boolean; - inlineSources?: boolean; - sourceRoot?: string; - mapRoot?: string; - extendedDiagnostics?: boolean; - } - - function shouldEmitSourceMaps(mapOptions: SourceMapOptions, sourceFileOrBundle: SourceFile | Bundle) { - return (mapOptions.sourceMap || mapOptions.inlineSourceMap) - && (sourceFileOrBundle.kind !== SyntaxKind.SourceFile || !fileExtensionIs(sourceFileOrBundle.fileName, Extension.Json)); - } - - function getSourceRoot(mapOptions: SourceMapOptions) { - // Normalize source root and make sure it has trailing "/" so that it can be used to combine paths with the - // relative paths of the sources list in the sourcemap - const sourceRoot = normalizeSlashes(mapOptions.sourceRoot || ""); - return sourceRoot ? ensureTrailingDirectorySeparator(sourceRoot) : sourceRoot; - } - - function getSourceMapDirectory(mapOptions: SourceMapOptions, filePath: string, sourceFile: SourceFile | undefined) { - if (mapOptions.sourceRoot) return host.getCommonSourceDirectory(); - if (mapOptions.mapRoot) { - let sourceMapDir = normalizeSlashes(mapOptions.mapRoot); - if (sourceFile) { - // For modules or multiple emit files the mapRoot will have directory structure like the sources - // So if src\a.ts and src\lib\b.ts are compiled together user would be moving the maps into mapRoot\a.js.map and mapRoot\lib\b.js.map - sourceMapDir = getDirectoryPath(getSourceFilePathInNewDir(sourceFile.fileName, host, sourceMapDir)); + emittedFilesList.push(sourceMapFilePath); } - if (getRootLength(sourceMapDir) === 0) { - // The relative paths are relative to the common directory - sourceMapDir = combinePaths(host.getCommonSourceDirectory(), sourceMapDir); + if (buildInfoPath) { + emittedFilesList.push(buildInfoPath); } - return sourceMapDir; } - return getDirectoryPath(normalizePath(filePath)); - } - - function getSourceMappingURL(mapOptions: SourceMapOptions, sourceMapGenerator: SourceMapGenerator, filePath: string, sourceMapFilePath: string | undefined, sourceFile: SourceFile | undefined) { - if (mapOptions.inlineSourceMap) { - // Encode the sourceMap into the sourceMap url - const sourceMapText = sourceMapGenerator.toString(); - const base64SourceMapText = base64encode(sys, sourceMapText); - return `data:application/json;base64,${base64SourceMapText}`; + if (declarationFilePath) { + emittedFilesList.push(declarationFilePath); } - - const sourceMapFile = getBaseFileName(normalizeSlashes(Debug.checkDefined(sourceMapFilePath))); - if (mapOptions.mapRoot) { - let sourceMapDir = normalizeSlashes(mapOptions.mapRoot); - if (sourceFile) { - // For modules or multiple emit files the mapRoot will have directory structure like the sources - // So if src\a.ts and src\lib\b.ts are compiled together user would be moving the maps into mapRoot\a.js.map and mapRoot\lib\b.js.map - sourceMapDir = getDirectoryPath(getSourceFilePathInNewDir(sourceFile.fileName, host, sourceMapDir)); - } - if (getRootLength(sourceMapDir) === 0) { - // The relative paths are relative to the common directory - sourceMapDir = combinePaths(host.getCommonSourceDirectory(), sourceMapDir); - return getRelativePathToDirectoryOrUrl( - getDirectoryPath(normalizePath(filePath)), // get the relative sourceMapDir path based on jsFilePath - combinePaths(sourceMapDir, sourceMapFile), // this is where user expects to see sourceMap - host.getCurrentDirectory(), - host.getCanonicalFileName, - /*isAbsolutePathAnUrl*/ true); - } - else { - return combinePaths(sourceMapDir, sourceMapFile); - } + if (declarationMapPath) { + emittedFilesList.push(declarationMapPath); } - return sourceMapFile; - } - } - - /*@internal*/ - export function getBuildInfoText(buildInfo: BuildInfo) { - return JSON.stringify(buildInfo, undefined, 2); - } - - /*@internal*/ - export function getBuildInfo(buildInfoText: string) { - return JSON.parse(buildInfoText) as BuildInfo; - } - - /*@internal*/ - export const notImplementedResolver: EmitResolver = { - hasGlobalName: notImplemented, - getReferencedExportContainer: notImplemented, - getReferencedImportDeclaration: notImplemented, - getReferencedDeclarationWithCollidingName: notImplemented, - isDeclarationWithCollidingName: notImplemented, - isValueAliasDeclaration: notImplemented, - isReferencedAliasDeclaration: notImplemented, - isTopLevelValueImportEqualsWithEntityName: notImplemented, - getNodeCheckFlags: notImplemented, - isDeclarationVisible: notImplemented, - isLateBound: (_node): _node is LateBoundDeclaration => false, - collectLinkedAliases: notImplemented, - isImplementationOfOverload: notImplemented, - isRequiredInitializedParameter: notImplemented, - isOptionalUninitializedParameterProperty: notImplemented, - isExpandoFunctionDeclaration: notImplemented, - getPropertiesOfContainerFunction: notImplemented, - createTypeOfDeclaration: notImplemented, - createReturnTypeOfSignatureDeclaration: notImplemented, - createTypeOfExpression: notImplemented, - createLiteralConstValue: notImplemented, - isSymbolAccessible: notImplemented, - isEntityNameVisible: notImplemented, - // Returns the constant value this property access resolves to: notImplemented, or 'undefined' for a non-constant - getConstantValue: notImplemented, - getReferencedValueDeclaration: notImplemented, - getTypeReferenceSerializationKind: notImplemented, - isOptionalParameter: notImplemented, - moduleExportsSomeValue: notImplemented, - isArgumentsLocalBinding: notImplemented, - getExternalModuleFileFromDeclaration: notImplemented, - getTypeReferenceDirectivesForEntityName: notImplemented, - getTypeReferenceDirectivesForSymbol: notImplemented, - isLiteralConstDeclaration: notImplemented, - getJsxFactoryEntity: notImplemented, - getAllAccessorDeclarations: notImplemented, - getSymbolOfExternalModuleSpecifier: notImplemented, - isBindingCapturedByNode: notImplemented, - getDeclarationStatementsForSourceFile: notImplemented, - }; - - /*@internal*/ - /** File that isnt present resulting in error or output files */ - export type EmitUsingBuildInfoResult = string | readonly OutputFile[]; - - /*@internal*/ - export interface EmitUsingBuildInfoHost extends ModuleResolutionHost { - getCurrentDirectory(): string; - getCanonicalFileName(fileName: string): string; - useCaseSensitiveFileNames(): boolean; - getNewLine(): string; - } - - function createSourceFilesFromBundleBuildInfo(bundle: BundleBuildInfo, buildInfoDirectory: string, host: EmitUsingBuildInfoHost): readonly SourceFile[] { - const sourceFiles = bundle.sourceFiles.map(fileName => { - const sourceFile = createNode(SyntaxKind.SourceFile, 0, 0) as SourceFile; - sourceFile.fileName = getRelativePathFromDirectory( - host.getCurrentDirectory(), - getNormalizedAbsolutePath(fileName, buildInfoDirectory), - !host.useCaseSensitiveFileNames() - ); - sourceFile.text = ""; - sourceFile.statements = createNodeArray(); - return sourceFile; - }); - const jsBundle = Debug.checkDefined(bundle.js); - forEach(jsBundle.sources && jsBundle.sources.prologues, prologueInfo => { - const sourceFile = sourceFiles[prologueInfo.file]; - sourceFile.text = prologueInfo.text; - sourceFile.end = prologueInfo.text.length; - sourceFile.statements = createNodeArray(prologueInfo.directives.map(directive => { - const statement = createNode(SyntaxKind.ExpressionStatement, directive.pos, directive.end) as PrologueDirective; - statement.expression = createNode(SyntaxKind.StringLiteral, directive.expression.pos, directive.expression.end) as StringLiteral; - statement.expression.text = directive.expression.text; - return statement; - })); - }); - return sourceFiles; - } - - /*@internal*/ - export function emitUsingBuildInfo( - config: ParsedCommandLine, - host: EmitUsingBuildInfoHost, - getCommandLine: (ref: ProjectReference) => ParsedCommandLine | undefined, - customTransformers?: CustomTransformers - ): EmitUsingBuildInfoResult { - const { buildInfoPath, jsFilePath, sourceMapFilePath, declarationFilePath, declarationMapPath } = getOutputPathsForBundle(config.options, /*forceDtsPaths*/ false); - const buildInfoText = host.readFile(Debug.checkDefined(buildInfoPath)); - if (!buildInfoText) return buildInfoPath!; - const jsFileText = host.readFile(Debug.checkDefined(jsFilePath)); - if (!jsFileText) return jsFilePath!; - const sourceMapText = sourceMapFilePath && host.readFile(sourceMapFilePath); - // error if no source map or for now if inline sourcemap - if ((sourceMapFilePath && !sourceMapText) || config.options.inlineSourceMap) return sourceMapFilePath || "inline sourcemap decoding"; - // read declaration text - const declarationText = declarationFilePath && host.readFile(declarationFilePath); - if (declarationFilePath && !declarationText) return declarationFilePath; - const declarationMapText = declarationMapPath && host.readFile(declarationMapPath); - // error if no source map or for now if inline sourcemap - if ((declarationMapPath && !declarationMapText) || config.options.inlineSourceMap) return declarationMapPath || "inline sourcemap decoding"; - - const buildInfo = getBuildInfo(buildInfoText); - if (!buildInfo.bundle || !buildInfo.bundle.js || (declarationText && !buildInfo.bundle.dts)) return buildInfoPath!; - const buildInfoDirectory = getDirectoryPath(getNormalizedAbsolutePath(buildInfoPath!, host.getCurrentDirectory())); - const ownPrependInput = createInputFiles( - jsFileText, - declarationText!, - sourceMapFilePath, - sourceMapText, - declarationMapPath, - declarationMapText, - jsFilePath, - declarationFilePath, - buildInfoPath, - buildInfo, - /*onlyOwnText*/ true - ); - const outputFiles: OutputFile[] = []; - const prependNodes = createPrependNodes(config.projectReferences, getCommandLine, f => host.readFile(f)); - const sourceFilesForJsEmit = createSourceFilesFromBundleBuildInfo(buildInfo.bundle, buildInfoDirectory, host); - const emitHost: EmitHost = { - getPrependNodes: memoize(() => [...prependNodes, ownPrependInput]), - getCanonicalFileName: host.getCanonicalFileName, - getCommonSourceDirectory: () => getNormalizedAbsolutePath(buildInfo.bundle!.commonSourceDirectory, buildInfoDirectory), - getCompilerOptions: () => config.options, - getCurrentDirectory: () => host.getCurrentDirectory(), - getNewLine: () => host.getNewLine(), - getSourceFile: returnUndefined, - getSourceFileByPath: returnUndefined, - getSourceFiles: () => sourceFilesForJsEmit, - getLibFileFromReference: notImplemented, - isSourceFileFromExternalLibrary: returnFalse, - getResolvedProjectReferenceToRedirect: returnUndefined, - isSourceOfProjectReferenceRedirect: returnFalse, - writeFile: (name, text, writeByteOrderMark) => { - switch (name) { - case jsFilePath: - if (jsFileText === text) return; - break; - case sourceMapFilePath: - if (sourceMapText === text) return; - break; - case buildInfoPath: - const newBuildInfo = getBuildInfo(text); - newBuildInfo.program = buildInfo.program; - // Update sourceFileInfo - const { js, dts, sourceFiles } = buildInfo.bundle!; - newBuildInfo.bundle!.js!.sources = js!.sources; - if (dts) { - newBuildInfo.bundle!.dts!.sources = dts.sources; - } - newBuildInfo.bundle!.sourceFiles = sourceFiles; - outputFiles.push({ name, text: getBuildInfoText(newBuildInfo), writeByteOrderMark }); - return; - case declarationFilePath: - if (declarationText === text) return; - break; - case declarationMapPath: - if (declarationMapText === text) return; - break; - default: - Debug.fail(`Unexpected path: ${name}`); - } - outputFiles.push({ name, text, writeByteOrderMark }); - }, - isEmitBlocked: returnFalse, - readFile: f => host.readFile(f), - fileExists: f => host.fileExists(f), - directoryExists: host.directoryExists && (f => host.directoryExists!(f)), - useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames(), - getProgramBuildInfo: returnUndefined, - getSourceFileFromReference: returnUndefined, - redirectTargetsMap: createMultiMap() + } + function relativeToBuildInfo(path: string) { + return ensurePathIsNonModuleName(getRelativePathFromDirectory((buildInfoDirectory!), path, host.getCanonicalFileName)); + } + } + function emitBuildInfo(bundle: BundleBuildInfo | undefined, buildInfoPath: string | undefined) { + // Write build information if applicable + if (!buildInfoPath || targetSourceFile || emitSkipped) + return; + const program = host.getProgramBuildInfo(); + if (host.isEmitBlocked(buildInfoPath) || compilerOptions.noEmit) { + emitSkipped = true; + return; + } + const version = ts.version; // Extracted into a const so the form is stable between namespace and module + writeFile(host, emitterDiagnostics, buildInfoPath, getBuildInfoText({ bundle, program, version }), /*writeByteOrderMark*/ false); + } + function emitJsFileOrBundle(sourceFileOrBundle: SourceFile | Bundle | undefined, jsFilePath: string | undefined, sourceMapFilePath: string | undefined, relativeToBuildInfo: (path: string) => string) { + if (!sourceFileOrBundle || emitOnlyDtsFiles || !jsFilePath) { + return; + } + // Make sure not to write js file and source map file if any of them cannot be written + if ((jsFilePath && host.isEmitBlocked(jsFilePath)) || compilerOptions.noEmit) { + emitSkipped = true; + return; + } + // Transform the source files + const transform = transformNodes(resolver, host, compilerOptions, [sourceFileOrBundle], scriptTransformers, /*allowDtsFiles*/ false); + const printerOptions: PrinterOptions = { + removeComments: compilerOptions.removeComments, + newLine: compilerOptions.newLine, + noEmitHelpers: compilerOptions.noEmitHelpers, + module: compilerOptions.module, + target: compilerOptions.target, + sourceMap: compilerOptions.sourceMap, + inlineSourceMap: compilerOptions.inlineSourceMap, + inlineSources: compilerOptions.inlineSources, + extendedDiagnostics: compilerOptions.extendedDiagnostics, + writeBundleFileInfo: !!bundleBuildInfo, + relativeToBuildInfo }; - emitFiles( - notImplementedResolver, - emitHost, - /*targetSourceFile*/ undefined, - getTransformers(config.options, customTransformers) - ); - return outputFiles; - } - - const enum PipelinePhase { - Notification, - Substitution, - Comments, - SourceMaps, - Emit - } - - export function createPrinter(printerOptions: PrinterOptions = {}, handlers: PrintHandlers = {}): Printer { - const { - hasGlobalName, - onEmitNode = noEmitNotification, - isEmitNotificationEnabled, - substituteNode = noEmitSubstitution, - onBeforeEmitNodeArray, - onAfterEmitNodeArray, - onBeforeEmitToken, - onAfterEmitToken - } = handlers; - - const extendedDiagnostics = !!printerOptions.extendedDiagnostics; - const newLine = getNewLineCharacter(printerOptions); - const moduleKind = getEmitModuleKind(printerOptions); - const bundledHelpers = createMap(); - - let currentSourceFile: SourceFile | undefined; - let nodeIdToGeneratedName: string[]; // Map of generated names for specific nodes. - let autoGeneratedIdToGeneratedName: string[]; // Map of generated names for temp and loop variables. - let generatedNames: Map; // Set of names generated by the NameGenerator. - let tempFlagsStack: TempFlags[]; // Stack of enclosing name generation scopes. - let tempFlags: TempFlags; // TempFlags for the current name generation scope. - let reservedNamesStack: Map[]; // Stack of TempFlags reserved in enclosing name generation scopes. - let reservedNames: Map; // TempFlags to reserve in nested name generation scopes. - - let writer: EmitTextWriter; - let ownWriter: EmitTextWriter; // Reusable `EmitTextWriter` for basic printing. - let write = writeBase; - let isOwnFileEmit: boolean; - const bundleFileInfo = printerOptions.writeBundleFileInfo ? { sections: [] } as BundleFileInfo : undefined; - const relativeToBuildInfo = bundleFileInfo ? Debug.checkDefined(printerOptions.relativeToBuildInfo) : undefined; - const recordInternalSection = printerOptions.recordInternalSection; - let sourceFileTextPos = 0; - let sourceFileTextKind: BundleFileTextLikeKind = BundleFileSectionKind.Text; - - // Source Maps - let sourceMapsDisabled = true; - let sourceMapGenerator: SourceMapGenerator | undefined; - let sourceMapSource: SourceMapSource; - let sourceMapSourceIndex = -1; - - // Comments - let containerPos = -1; - let containerEnd = -1; - let declarationListContainerEnd = -1; - let currentLineMap: readonly number[] | undefined; - let detachedCommentsInfo: { nodePos: number, detachedCommentEndPos: number}[] | undefined; - let hasWrittenComment = false; - let commentsDisabled = !!printerOptions.removeComments; - let lastNode: Node | undefined; - let lastSubstitution: Node | undefined; - const { enter: enterComment, exit: exitComment } = performance.createTimerIf(extendedDiagnostics, "commentTime", "beforeComment", "afterComment"); - - reset(); - return { - // public API - printNode, - printList, - printFile, - printBundle, - - // internal API - writeNode, - writeList, - writeFile, - writeBundle, - bundleFileInfo + // Create a printer to print the nodes + const printer = createPrinter(printerOptions, { + // resolver hooks + hasGlobalName: resolver.hasGlobalName, + // transform hooks + onEmitNode: transform.emitNodeWithNotification, + isEmitNotificationEnabled: transform.isEmitNotificationEnabled, + substituteNode: transform.substituteNode, + }); + Debug.assert(transform.transformed.length === 1, "Should only see one output from the transform"); + printSourceFileOrBundle(jsFilePath, sourceMapFilePath, transform.transformed[0], printer, compilerOptions); + // Clean up emit nodes on parse tree + transform.dispose(); + if (bundleBuildInfo) + bundleBuildInfo.js = printer.bundleFileInfo; + } + function emitDeclarationFileOrBundle(sourceFileOrBundle: SourceFile | Bundle | undefined, declarationFilePath: string | undefined, declarationMapPath: string | undefined, relativeToBuildInfo: (path: string) => string) { + if (!sourceFileOrBundle) + return; + if (!declarationFilePath) { + if (emitOnlyDtsFiles || compilerOptions.emitDeclarationOnly) + emitSkipped = true; + return; + } + const sourceFiles = isSourceFile(sourceFileOrBundle) ? [sourceFileOrBundle] : sourceFileOrBundle.sourceFiles; + const filesForEmit = forceDtsEmit ? sourceFiles : filter(sourceFiles, isSourceFileNotJson); + // Setup and perform the transformation to retrieve declarations from the input files + const inputListOrBundle = (compilerOptions.outFile || compilerOptions.out) ? [createBundle(filesForEmit, !isSourceFile(sourceFileOrBundle) ? sourceFileOrBundle.prepends : undefined)] : filesForEmit; + if (emitOnlyDtsFiles && !getEmitDeclarations(compilerOptions)) { + // Checker wont collect the linked aliases since thats only done when declaration is enabled. + // Do that here when emitting only dts files + filesForEmit.forEach(collectLinkedAliases); + } + const declarationTransform = transformNodes(resolver, host, compilerOptions, inputListOrBundle, declarationTransformers, /*allowDtsFiles*/ false); + if (length(declarationTransform.diagnostics)) { + for (const diagnostic of declarationTransform.diagnostics!) { + emitterDiagnostics.add(diagnostic); + } + } + const printerOptions: PrinterOptions = { + removeComments: compilerOptions.removeComments, + newLine: compilerOptions.newLine, + noEmitHelpers: true, + module: compilerOptions.module, + target: compilerOptions.target, + sourceMap: compilerOptions.sourceMap, + inlineSourceMap: compilerOptions.inlineSourceMap, + extendedDiagnostics: compilerOptions.extendedDiagnostics, + onlyPrintJsDocStyle: true, + writeBundleFileInfo: !!bundleBuildInfo, + recordInternalSection: !!bundleBuildInfo, + relativeToBuildInfo }; - - function printNode(hint: EmitHint, node: Node, sourceFile: SourceFile): string { - switch (hint) { - case EmitHint.SourceFile: - Debug.assert(isSourceFile(node), "Expected a SourceFile node."); - break; - case EmitHint.IdentifierName: - Debug.assert(isIdentifier(node), "Expected an Identifier node."); - break; - case EmitHint.Expression: - Debug.assert(isExpression(node), "Expected an Expression node."); - break; - } - switch (node.kind) { - case SyntaxKind.SourceFile: return printFile(node); - case SyntaxKind.Bundle: return printBundle(node); - case SyntaxKind.UnparsedSource: return printUnparsedSource(node); - } - writeNode(hint, node, sourceFile, beginPrint()); - return endPrint(); - } - - function printList(format: ListFormat, nodes: NodeArray, sourceFile: SourceFile) { - writeList(format, nodes, sourceFile, beginPrint()); - return endPrint(); - } - - function printBundle(bundle: Bundle): string { - writeBundle(bundle, beginPrint(), /*sourceMapEmitter*/ undefined); - return endPrint(); - } - - function printFile(sourceFile: SourceFile): string { - writeFile(sourceFile, beginPrint(), /*sourceMapEmitter*/ undefined); - return endPrint(); - } - - function printUnparsedSource(unparsed: UnparsedSource): string { - writeUnparsedSource(unparsed, beginPrint()); - return endPrint(); - } - - /** - * If `sourceFile` is `undefined`, `node` must be a synthesized `TypeNode`. - */ - function writeNode(hint: EmitHint, node: TypeNode, sourceFile: undefined, output: EmitTextWriter): void; - function writeNode(hint: EmitHint, node: Node, sourceFile: SourceFile, output: EmitTextWriter): void; - function writeNode(hint: EmitHint, node: Node, sourceFile: SourceFile | undefined, output: EmitTextWriter) { - const previousWriter = writer; - setWriter(output, /*_sourceMapGenerator*/ undefined); - print(hint, node, sourceFile); - reset(); - writer = previousWriter; - } - - function writeList(format: ListFormat, nodes: NodeArray, sourceFile: SourceFile | undefined, output: EmitTextWriter) { - const previousWriter = writer; - setWriter(output, /*_sourceMapGenerator*/ undefined); - if (sourceFile) { - setSourceFile(sourceFile); - } - emitList(syntheticParent, nodes, format); - reset(); - writer = previousWriter; - } - - function getTextPosWithWriteLine() { - return writer.getTextPosWithWriteLine ? writer.getTextPosWithWriteLine() : writer.getTextPos(); - } - - function updateOrPushBundleFileTextLike(pos: number, end: number, kind: BundleFileTextLikeKind) { - const last = lastOrUndefined(bundleFileInfo!.sections); - if (last && last.kind === kind) { - last.end = end; - } - else { - bundleFileInfo!.sections.push({ pos, end, kind }); + const declarationPrinter = createPrinter(printerOptions, { + // resolver hooks + hasGlobalName: resolver.hasGlobalName, + // transform hooks + onEmitNode: declarationTransform.emitNodeWithNotification, + isEmitNotificationEnabled: declarationTransform.isEmitNotificationEnabled, + substituteNode: declarationTransform.substituteNode, + }); + const declBlocked = (!!declarationTransform.diagnostics && !!declarationTransform.diagnostics.length) || !!host.isEmitBlocked(declarationFilePath) || !!compilerOptions.noEmit; + emitSkipped = emitSkipped || declBlocked; + if (!declBlocked || forceDtsEmit) { + Debug.assert(declarationTransform.transformed.length === 1, "Should only see one output from the decl transform"); + printSourceFileOrBundle(declarationFilePath, declarationMapPath, declarationTransform.transformed[0], declarationPrinter, { + sourceMap: compilerOptions.declarationMap, + sourceRoot: compilerOptions.sourceRoot, + mapRoot: compilerOptions.mapRoot, + extendedDiagnostics: compilerOptions.extendedDiagnostics, + }); + if (forceDtsEmit && declarationTransform.transformed[0].kind === SyntaxKind.SourceFile) { + const sourceFile = declarationTransform.transformed[0]; + exportedModulesFromDeclarationEmit = sourceFile.exportedModulesFromDeclarationEmit; } } - - function recordBundleFileInternalSectionStart(node: Node) { - if (recordInternalSection && - bundleFileInfo && - currentSourceFile && - (isDeclaration(node) || isVariableStatement(node)) && - isInternalDeclaration(node, currentSourceFile) && - sourceFileTextKind !== BundleFileSectionKind.Internal) { - const prevSourceFileTextKind = sourceFileTextKind; - recordBundleFileTextLikeSection(writer.getTextPos()); - sourceFileTextPos = getTextPosWithWriteLine(); - sourceFileTextKind = BundleFileSectionKind.Internal; - return prevSourceFileTextKind; + declarationTransform.dispose(); + if (bundleBuildInfo) + bundleBuildInfo.dts = declarationPrinter.bundleFileInfo; + } + function collectLinkedAliases(node: Node) { + if (isExportAssignment(node)) { + if (node.expression.kind === SyntaxKind.Identifier) { + resolver.collectLinkedAliases((node.expression as Identifier), /*setVisibility*/ true); } - return undefined; + return; } - - function recordBundleFileInternalSectionEnd(prevSourceFileTextKind: ReturnType) { - if (prevSourceFileTextKind) { - recordBundleFileTextLikeSection(writer.getTextPos()); - sourceFileTextPos = getTextPosWithWriteLine(); - sourceFileTextKind = prevSourceFileTextKind; - } + else if (isExportSpecifier(node)) { + resolver.collectLinkedAliases(node.propertyName || node.name, /*setVisibility*/ true); + return; } - - function recordBundleFileTextLikeSection(end: number) { - if (sourceFileTextPos < end) { - updateOrPushBundleFileTextLike(sourceFileTextPos, end, sourceFileTextKind); - return true; - } - return false; + forEachChild(node, collectLinkedAliases); + } + function printSourceFileOrBundle(jsFilePath: string, sourceMapFilePath: string | undefined, sourceFileOrBundle: SourceFile | Bundle, printer: Printer, mapOptions: SourceMapOptions) { + const bundle = sourceFileOrBundle.kind === SyntaxKind.Bundle ? sourceFileOrBundle : undefined; + const sourceFile = sourceFileOrBundle.kind === SyntaxKind.SourceFile ? sourceFileOrBundle : undefined; + const sourceFiles = bundle ? bundle.sourceFiles : [sourceFile!]; + let sourceMapGenerator: SourceMapGenerator | undefined; + if (shouldEmitSourceMaps(mapOptions, sourceFileOrBundle)) { + sourceMapGenerator = createSourceMapGenerator(host, getBaseFileName(normalizeSlashes(jsFilePath)), getSourceRoot(mapOptions), getSourceMapDirectory(mapOptions, jsFilePath, sourceFile), mapOptions); } - - function writeBundle(bundle: Bundle, output: EmitTextWriter, sourceMapGenerator: SourceMapGenerator | undefined) { - isOwnFileEmit = false; - const previousWriter = writer; - setWriter(output, sourceMapGenerator); - emitShebangIfNeeded(bundle); - emitPrologueDirectivesIfNeeded(bundle); - emitHelpers(bundle); - emitSyntheticTripleSlashReferencesIfNeeded(bundle); - - for (const prepend of bundle.prepends) { - writeLine(); - const pos = writer.getTextPos(); - const savedSections = bundleFileInfo && bundleFileInfo.sections; - if (savedSections) bundleFileInfo!.sections = []; - print(EmitHint.Unspecified, prepend, /*sourceFile*/ undefined); - if (bundleFileInfo) { - const newSections = bundleFileInfo.sections; - bundleFileInfo.sections = savedSections!; - if (prepend.oldFileOfCurrentEmit) bundleFileInfo.sections.push(...newSections); - else { - newSections.forEach(section => Debug.assert(isBundleFileTextLike(section))); - bundleFileInfo.sections.push({ - pos, - end: writer.getTextPos(), - kind: BundleFileSectionKind.Prepend, - data: relativeToBuildInfo!((prepend as UnparsedSource).fileName), - texts: newSections as BundleFileTextLike[] - }); - } - } + if (bundle) { + printer.writeBundle(bundle, writer, sourceMapGenerator); + } + else { + printer.writeFile(sourceFile!, writer, sourceMapGenerator); + } + if (sourceMapGenerator) { + if (sourceMapDataList) { + sourceMapDataList.push({ + inputSourceFileNames: sourceMapGenerator.getSources(), + sourceMap: sourceMapGenerator.toJSON() + }); } - - sourceFileTextPos = getTextPosWithWriteLine(); - for (const sourceFile of bundle.sourceFiles) { - print(EmitHint.SourceFile, sourceFile, sourceFile); + const sourceMappingURL = getSourceMappingURL(mapOptions, sourceMapGenerator, jsFilePath, sourceMapFilePath, sourceFile); + if (sourceMappingURL) { + if (!writer.isAtStartOfLine()) + writer.rawWrite(newLine); + writer.writeComment(`//# ${"sourceMappingURL"}=${sourceMappingURL}`); // Tools can sometimes see this line as a source mapping url comment } - if (bundleFileInfo && bundle.sourceFiles.length) { - const end = writer.getTextPos(); - if (recordBundleFileTextLikeSection(end)) { - // Store prologues - const prologues = getPrologueDirectivesFromBundledSourceFiles(bundle); - if (prologues) { - if (!bundleFileInfo.sources) bundleFileInfo.sources = {}; - bundleFileInfo.sources.prologues = prologues; - } - - // Store helpes - const helpers = getHelpersFromBundledSourceFiles(bundle); - if (helpers) { - if (!bundleFileInfo.sources) bundleFileInfo.sources = {}; - bundleFileInfo.sources.helpers = helpers; - } - } + // Write the source map + if (sourceMapFilePath) { + const sourceMap = sourceMapGenerator.toString(); + writeFile(host, emitterDiagnostics, sourceMapFilePath, sourceMap, /*writeByteOrderMark*/ false, sourceFiles); } - - reset(); - writer = previousWriter; - } - - function writeUnparsedSource(unparsed: UnparsedSource, output: EmitTextWriter) { - const previousWriter = writer; - setWriter(output, /*_sourceMapGenerator*/ undefined); - print(EmitHint.Unspecified, unparsed, /*sourceFile*/ undefined); - reset(); - writer = previousWriter; - } - - function writeFile(sourceFile: SourceFile, output: EmitTextWriter, sourceMapGenerator: SourceMapGenerator | undefined) { - isOwnFileEmit = true; - const previousWriter = writer; - setWriter(output, sourceMapGenerator); - emitShebangIfNeeded(sourceFile); - emitPrologueDirectivesIfNeeded(sourceFile); - print(EmitHint.SourceFile, sourceFile, sourceFile); - reset(); - writer = previousWriter; } - - function beginPrint() { - return ownWriter || (ownWriter = createTextWriter(newLine)); - } - - function endPrint() { - const text = ownWriter.getText(); - ownWriter.clear(); - return text; + else { + writer.writeLine(); } - - function print(hint: EmitHint, node: Node, sourceFile: SourceFile | undefined) { + // Write the output file + writeFile(host, emitterDiagnostics, jsFilePath, writer.getText(), !!compilerOptions.emitBOM, sourceFiles); + // Reset state + writer.clear(); + } + interface SourceMapOptions { + sourceMap?: boolean; + inlineSourceMap?: boolean; + inlineSources?: boolean; + sourceRoot?: string; + mapRoot?: string; + extendedDiagnostics?: boolean; + } + function shouldEmitSourceMaps(mapOptions: SourceMapOptions, sourceFileOrBundle: SourceFile | Bundle) { + return (mapOptions.sourceMap || mapOptions.inlineSourceMap) + && (sourceFileOrBundle.kind !== SyntaxKind.SourceFile || !fileExtensionIs(sourceFileOrBundle.fileName, Extension.Json)); + } + function getSourceRoot(mapOptions: SourceMapOptions) { + // Normalize source root and make sure it has trailing "/" so that it can be used to combine paths with the + // relative paths of the sources list in the sourcemap + const sourceRoot = normalizeSlashes(mapOptions.sourceRoot || ""); + return sourceRoot ? ensureTrailingDirectorySeparator(sourceRoot) : sourceRoot; + } + function getSourceMapDirectory(mapOptions: SourceMapOptions, filePath: string, sourceFile: SourceFile | undefined) { + if (mapOptions.sourceRoot) + return host.getCommonSourceDirectory(); + if (mapOptions.mapRoot) { + let sourceMapDir = normalizeSlashes(mapOptions.mapRoot); if (sourceFile) { - setSourceFile(sourceFile); + // For modules or multiple emit files the mapRoot will have directory structure like the sources + // So if src\a.ts and src\lib\b.ts are compiled together user would be moving the maps into mapRoot\a.js.map and mapRoot\lib\b.js.map + sourceMapDir = getDirectoryPath(getSourceFilePathInNewDir(sourceFile.fileName, host, sourceMapDir)); } - - pipelineEmit(hint, node); - } - - function setSourceFile(sourceFile: SourceFile | undefined) { - currentSourceFile = sourceFile; - currentLineMap = undefined; - detachedCommentsInfo = undefined; - if (sourceFile) { - setSourceMapSource(sourceFile); + if (getRootLength(sourceMapDir) === 0) { + // The relative paths are relative to the common directory + sourceMapDir = combinePaths(host.getCommonSourceDirectory(), sourceMapDir); } + return sourceMapDir; } - - function setWriter(_writer: EmitTextWriter | undefined, _sourceMapGenerator: SourceMapGenerator | undefined) { - if (_writer && printerOptions.omitTrailingSemicolon) { - _writer = getTrailingSemicolonDeferringWriter(_writer); + return getDirectoryPath(normalizePath(filePath)); + } + function getSourceMappingURL(mapOptions: SourceMapOptions, sourceMapGenerator: SourceMapGenerator, filePath: string, sourceMapFilePath: string | undefined, sourceFile: SourceFile | undefined) { + if (mapOptions.inlineSourceMap) { + // Encode the sourceMap into the sourceMap url + const sourceMapText = sourceMapGenerator.toString(); + const base64SourceMapText = base64encode(sys, sourceMapText); + return `data:application/json;base64,${base64SourceMapText}`; + } + const sourceMapFile = getBaseFileName(normalizeSlashes(Debug.checkDefined(sourceMapFilePath))); + if (mapOptions.mapRoot) { + let sourceMapDir = normalizeSlashes(mapOptions.mapRoot); + if (sourceFile) { + // For modules or multiple emit files the mapRoot will have directory structure like the sources + // So if src\a.ts and src\lib\b.ts are compiled together user would be moving the maps into mapRoot\a.js.map and mapRoot\lib\b.js.map + sourceMapDir = getDirectoryPath(getSourceFilePathInNewDir(sourceFile.fileName, host, sourceMapDir)); + } + if (getRootLength(sourceMapDir) === 0) { + // The relative paths are relative to the common directory + sourceMapDir = combinePaths(host.getCommonSourceDirectory(), sourceMapDir); + return getRelativePathToDirectoryOrUrl(getDirectoryPath(normalizePath(filePath)), // get the relative sourceMapDir path based on jsFilePath + combinePaths(sourceMapDir, sourceMapFile), // this is where user expects to see sourceMap + host.getCurrentDirectory(), host.getCanonicalFileName, + /*isAbsolutePathAnUrl*/ true); } - - writer = _writer!; // TODO: GH#18217 - sourceMapGenerator = _sourceMapGenerator; - sourceMapsDisabled = !writer || !sourceMapGenerator; - } - - function reset() { - nodeIdToGeneratedName = []; - autoGeneratedIdToGeneratedName = []; - generatedNames = createMap(); - tempFlagsStack = []; - tempFlags = TempFlags.Auto; - reservedNamesStack = []; - currentSourceFile = undefined!; - currentLineMap = undefined!; - detachedCommentsInfo = undefined; - lastNode = undefined; - lastSubstitution = undefined; - setWriter(/*output*/ undefined, /*_sourceMapGenerator*/ undefined); - } - - function getCurrentLineMap() { - return currentLineMap || (currentLineMap = getLineStarts(currentSourceFile!)); - } - - function emit(node: Node): Node; - function emit(node: Node | undefined): Node | undefined; - function emit(node: Node | undefined) { - if (node === undefined) return; - - const prevSourceFileTextKind = recordBundleFileInternalSectionStart(node); - const substitute = pipelineEmit(EmitHint.Unspecified, node); - recordBundleFileInternalSectionEnd(prevSourceFileTextKind); - return substitute; - } - - function emitIdentifierName(node: Identifier): Node; - function emitIdentifierName(node: Identifier | undefined): Node | undefined; - function emitIdentifierName(node: Identifier | undefined): Node | undefined { - if (node === undefined) return; - return pipelineEmit(EmitHint.IdentifierName, node); - } - - function emitExpression(node: Expression): Node; - function emitExpression(node: Expression | undefined): Node | undefined; - function emitExpression(node: Expression | undefined): Node | undefined { - if (node === undefined) return; - return pipelineEmit(EmitHint.Expression, node); - } - - function emitJsxAttributeValue(node: StringLiteral | JsxExpression): Node { - return pipelineEmit(isStringLiteral(node) ? EmitHint.JsxAttributeValue : EmitHint.Unspecified, node); - } - - function pipelineEmit(emitHint: EmitHint, node: Node) { - const savedLastNode = lastNode; - const savedLastSubstitution = lastSubstitution; - lastNode = node; - lastSubstitution = undefined; - - const pipelinePhase = getPipelinePhase(PipelinePhase.Notification, emitHint, node); - pipelinePhase(emitHint, node); - - Debug.assert(lastNode === node); - - const substitute = lastSubstitution; - lastNode = savedLastNode; - lastSubstitution = savedLastSubstitution; - - return substitute || node; - } - - function getPipelinePhase(phase: PipelinePhase, emitHint: EmitHint, node: Node) { - switch (phase) { - case PipelinePhase.Notification: - if (onEmitNode !== noEmitNotification && (!isEmitNotificationEnabled || isEmitNotificationEnabled(node))) { - return pipelineEmitWithNotification; - } - // falls through - - case PipelinePhase.Substitution: - if (substituteNode !== noEmitSubstitution && (lastSubstitution = substituteNode(emitHint, node)) !== node) { - return pipelineEmitWithSubstitution; - } - // falls through - - case PipelinePhase.Comments: - if (!commentsDisabled && node.kind !== SyntaxKind.SourceFile) { - return pipelineEmitWithComments; - } - // falls through - - case PipelinePhase.SourceMaps: - if (!sourceMapsDisabled && node.kind !== SyntaxKind.SourceFile && !isInJsonFile(node)) { - return pipelineEmitWithSourceMap; - } - // falls through - - case PipelinePhase.Emit: - return pipelineEmitWithHint; - - default: - return Debug.assertNever(phase); + else { + return combinePaths(sourceMapDir, sourceMapFile); } } - - function getNextPipelinePhase(currentPhase: PipelinePhase, emitHint: EmitHint, node: Node) { - return getPipelinePhase(currentPhase + 1, emitHint, node); - } - - function pipelineEmitWithNotification(hint: EmitHint, node: Node) { - Debug.assert(lastNode === node); - const pipelinePhase = getNextPipelinePhase(PipelinePhase.Notification, hint, node); - onEmitNode(hint, node, pipelinePhase); - Debug.assert(lastNode === node); - } - - function pipelineEmitWithHint(hint: EmitHint, node: Node): void { - Debug.assert(lastNode === node || lastSubstitution === node); - if (hint === EmitHint.SourceFile) return emitSourceFile(cast(node, isSourceFile)); - if (hint === EmitHint.IdentifierName) return emitIdentifier(cast(node, isIdentifier)); - if (hint === EmitHint.JsxAttributeValue) return emitLiteral(cast(node, isStringLiteral), /*jsxAttributeEscape*/ true); - if (hint === EmitHint.MappedTypeParameter) return emitMappedTypeParameter(cast(node, isTypeParameterDeclaration)); - if (hint === EmitHint.EmbeddedStatement) { - Debug.assertNode(node, isEmptyStatement); - return emitEmptyStatement(/*isEmbeddedStatement*/ true); - } - if (hint === EmitHint.Unspecified) { - if (isKeyword(node.kind)) return writeTokenNode(node, writeKeyword); - - switch (node.kind) { - // Pseudo-literals - case SyntaxKind.TemplateHead: - case SyntaxKind.TemplateMiddle: - case SyntaxKind.TemplateTail: - return emitLiteral(node, /*jsxAttributeEscape*/ false); - - case SyntaxKind.UnparsedSource: - case SyntaxKind.UnparsedPrepend: - return emitUnparsedSourceOrPrepend(node); - - case SyntaxKind.UnparsedPrologue: - return writeUnparsedNode(node); - - case SyntaxKind.UnparsedText: - case SyntaxKind.UnparsedInternalText: - return emitUnparsedTextLike(node); - - case SyntaxKind.UnparsedSyntheticReference: - return emitUnparsedSyntheticReference(node); - - - // Identifiers - case SyntaxKind.Identifier: - return emitIdentifier(node); - - // PrivateIdentifiers - case SyntaxKind.PrivateIdentifier: - return emitPrivateIdentifier(node as PrivateIdentifier); - - // Parse tree nodes - // Names - case SyntaxKind.QualifiedName: - return emitQualifiedName(node); - case SyntaxKind.ComputedPropertyName: - return emitComputedPropertyName(node); - - // Signature elements - case SyntaxKind.TypeParameter: - return emitTypeParameter(node); - case SyntaxKind.Parameter: - return emitParameter(node); - case SyntaxKind.Decorator: - return emitDecorator(node); - - // Type members - case SyntaxKind.PropertySignature: - return emitPropertySignature(node); - case SyntaxKind.PropertyDeclaration: - return emitPropertyDeclaration(node); - case SyntaxKind.MethodSignature: - return emitMethodSignature(node); - case SyntaxKind.MethodDeclaration: - return emitMethodDeclaration(node); - case SyntaxKind.Constructor: - return emitConstructor(node); - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - return emitAccessorDeclaration(node); - case SyntaxKind.CallSignature: - return emitCallSignature(node); - case SyntaxKind.ConstructSignature: - return emitConstructSignature(node); - case SyntaxKind.IndexSignature: - return emitIndexSignature(node); - - // Types - case SyntaxKind.TypePredicate: - return emitTypePredicate(node); - case SyntaxKind.TypeReference: - return emitTypeReference(node); - case SyntaxKind.FunctionType: - return emitFunctionType(node); - case SyntaxKind.JSDocFunctionType: - return emitJSDocFunctionType(node as JSDocFunctionType); - case SyntaxKind.ConstructorType: - return emitConstructorType(node); - case SyntaxKind.TypeQuery: - return emitTypeQuery(node); - case SyntaxKind.TypeLiteral: - return emitTypeLiteral(node); - case SyntaxKind.ArrayType: - return emitArrayType(node); - case SyntaxKind.TupleType: - return emitTupleType(node); - case SyntaxKind.OptionalType: - return emitOptionalType(node); - case SyntaxKind.UnionType: - return emitUnionType(node); - case SyntaxKind.IntersectionType: - return emitIntersectionType(node); - case SyntaxKind.ConditionalType: - return emitConditionalType(node); - case SyntaxKind.InferType: - return emitInferType(node); - case SyntaxKind.ParenthesizedType: - return emitParenthesizedType(node); - case SyntaxKind.ExpressionWithTypeArguments: - return emitExpressionWithTypeArguments(node); - case SyntaxKind.ThisType: - return emitThisType(); - case SyntaxKind.TypeOperator: - return emitTypeOperator(node); - case SyntaxKind.IndexedAccessType: - return emitIndexedAccessType(node); - case SyntaxKind.MappedType: - return emitMappedType(node); - case SyntaxKind.LiteralType: - return emitLiteralType(node); - case SyntaxKind.ImportType: - return emitImportTypeNode(node); - case SyntaxKind.JSDocAllType: - writePunctuation("*"); - return; - case SyntaxKind.JSDocUnknownType: - writePunctuation("?"); + return sourceMapFile; + } +} +/*@internal*/ +export function getBuildInfoText(buildInfo: BuildInfo) { + return JSON.stringify(buildInfo, undefined, 2); +} +/*@internal*/ +export function getBuildInfo(buildInfoText: string) { + return JSON.parse(buildInfoText) as BuildInfo; +} +/*@internal*/ +export const notImplementedResolver: EmitResolver = { + hasGlobalName: notImplemented, + getReferencedExportContainer: notImplemented, + getReferencedImportDeclaration: notImplemented, + getReferencedDeclarationWithCollidingName: notImplemented, + isDeclarationWithCollidingName: notImplemented, + isValueAliasDeclaration: notImplemented, + isReferencedAliasDeclaration: notImplemented, + isTopLevelValueImportEqualsWithEntityName: notImplemented, + getNodeCheckFlags: notImplemented, + isDeclarationVisible: notImplemented, + isLateBound: (_node): _node is LateBoundDeclaration => false, + collectLinkedAliases: notImplemented, + isImplementationOfOverload: notImplemented, + isRequiredInitializedParameter: notImplemented, + isOptionalUninitializedParameterProperty: notImplemented, + isExpandoFunctionDeclaration: notImplemented, + getPropertiesOfContainerFunction: notImplemented, + createTypeOfDeclaration: notImplemented, + createReturnTypeOfSignatureDeclaration: notImplemented, + createTypeOfExpression: notImplemented, + createLiteralConstValue: notImplemented, + isSymbolAccessible: notImplemented, + isEntityNameVisible: notImplemented, + // Returns the constant value this property access resolves to: notImplemented, or 'undefined' for a non-constant + getConstantValue: notImplemented, + getReferencedValueDeclaration: notImplemented, + getTypeReferenceSerializationKind: notImplemented, + isOptionalParameter: notImplemented, + moduleExportsSomeValue: notImplemented, + isArgumentsLocalBinding: notImplemented, + getExternalModuleFileFromDeclaration: notImplemented, + getTypeReferenceDirectivesForEntityName: notImplemented, + getTypeReferenceDirectivesForSymbol: notImplemented, + isLiteralConstDeclaration: notImplemented, + getJsxFactoryEntity: notImplemented, + getAllAccessorDeclarations: notImplemented, + getSymbolOfExternalModuleSpecifier: notImplemented, + isBindingCapturedByNode: notImplemented, + getDeclarationStatementsForSourceFile: notImplemented, +}; +/*@internal*/ +/** File that isnt present resulting in error or output files */ +export type EmitUsingBuildInfoResult = string | readonly OutputFile[]; +/*@internal*/ +export interface EmitUsingBuildInfoHost extends ModuleResolutionHost { + getCurrentDirectory(): string; + getCanonicalFileName(fileName: string): string; + useCaseSensitiveFileNames(): boolean; + getNewLine(): string; +} +function createSourceFilesFromBundleBuildInfo(bundle: BundleBuildInfo, buildInfoDirectory: string, host: EmitUsingBuildInfoHost): readonly SourceFile[] { + const sourceFiles = bundle.sourceFiles.map(fileName => { + const sourceFile = (createNode(SyntaxKind.SourceFile, 0, 0) as SourceFile); + sourceFile.fileName = getRelativePathFromDirectory(host.getCurrentDirectory(), getNormalizedAbsolutePath(fileName, buildInfoDirectory), !host.useCaseSensitiveFileNames()); + sourceFile.text = ""; + sourceFile.statements = createNodeArray(); + return sourceFile; + }); + const jsBundle = Debug.checkDefined(bundle.js); + forEach(jsBundle.sources && jsBundle.sources.prologues, prologueInfo => { + const sourceFile = sourceFiles[prologueInfo.file]; + sourceFile.text = prologueInfo.text; + sourceFile.end = prologueInfo.text.length; + sourceFile.statements = createNodeArray(prologueInfo.directives.map(directive => { + const statement = (createNode(SyntaxKind.ExpressionStatement, directive.pos, directive.end) as PrologueDirective); + statement.expression = (createNode(SyntaxKind.StringLiteral, directive.expression.pos, directive.expression.end) as StringLiteral); + statement.expression.text = directive.expression.text; + return statement; + })); + }); + return sourceFiles; +} +/*@internal*/ +export function emitUsingBuildInfo(config: ParsedCommandLine, host: EmitUsingBuildInfoHost, getCommandLine: (ref: ProjectReference) => ParsedCommandLine | undefined, customTransformers?: CustomTransformers): EmitUsingBuildInfoResult { + const { buildInfoPath, jsFilePath, sourceMapFilePath, declarationFilePath, declarationMapPath } = getOutputPathsForBundle(config.options, /*forceDtsPaths*/ false); + const buildInfoText = host.readFile(Debug.checkDefined(buildInfoPath)); + if (!buildInfoText) + return buildInfoPath!; + const jsFileText = host.readFile(Debug.checkDefined(jsFilePath)); + if (!jsFileText) + return jsFilePath!; + const sourceMapText = sourceMapFilePath && host.readFile(sourceMapFilePath); + // error if no source map or for now if inline sourcemap + if ((sourceMapFilePath && !sourceMapText) || config.options.inlineSourceMap) + return sourceMapFilePath || "inline sourcemap decoding"; + // read declaration text + const declarationText = declarationFilePath && host.readFile(declarationFilePath); + if (declarationFilePath && !declarationText) + return declarationFilePath; + const declarationMapText = declarationMapPath && host.readFile(declarationMapPath); + // error if no source map or for now if inline sourcemap + if ((declarationMapPath && !declarationMapText) || config.options.inlineSourceMap) + return declarationMapPath || "inline sourcemap decoding"; + const buildInfo = getBuildInfo(buildInfoText); + if (!buildInfo.bundle || !buildInfo.bundle.js || (declarationText && !buildInfo.bundle.dts)) + return buildInfoPath!; + const buildInfoDirectory = getDirectoryPath(getNormalizedAbsolutePath((buildInfoPath!), host.getCurrentDirectory())); + const ownPrependInput = createInputFiles(jsFileText, (declarationText!), sourceMapFilePath, sourceMapText, declarationMapPath, declarationMapText, jsFilePath, declarationFilePath, buildInfoPath, buildInfo, + /*onlyOwnText*/ true); + const outputFiles: OutputFile[] = []; + const prependNodes = createPrependNodes(config.projectReferences, getCommandLine, f => host.readFile(f)); + const sourceFilesForJsEmit = createSourceFilesFromBundleBuildInfo(buildInfo.bundle, buildInfoDirectory, host); + const emitHost: EmitHost = { + getPrependNodes: memoize(() => [...prependNodes, ownPrependInput]), + getCanonicalFileName: host.getCanonicalFileName, + getCommonSourceDirectory: () => getNormalizedAbsolutePath(buildInfo.bundle!.commonSourceDirectory, buildInfoDirectory), + getCompilerOptions: () => config.options, + getCurrentDirectory: () => host.getCurrentDirectory(), + getNewLine: () => host.getNewLine(), + getSourceFile: returnUndefined, + getSourceFileByPath: returnUndefined, + getSourceFiles: () => sourceFilesForJsEmit, + getLibFileFromReference: notImplemented, + isSourceFileFromExternalLibrary: returnFalse, + getResolvedProjectReferenceToRedirect: returnUndefined, + isSourceOfProjectReferenceRedirect: returnFalse, + writeFile: (name, text, writeByteOrderMark) => { + switch (name) { + case jsFilePath: + if (jsFileText === text) return; - case SyntaxKind.JSDocNullableType: - return emitJSDocNullableType(node as JSDocNullableType); - case SyntaxKind.JSDocNonNullableType: - return emitJSDocNonNullableType(node as JSDocNonNullableType); - case SyntaxKind.JSDocOptionalType: - return emitJSDocOptionalType(node as JSDocOptionalType); - case SyntaxKind.RestType: - case SyntaxKind.JSDocVariadicType: - return emitRestOrJSDocVariadicType(node as RestTypeNode | JSDocVariadicType); - - // Binding patterns - case SyntaxKind.ObjectBindingPattern: - return emitObjectBindingPattern(node); - case SyntaxKind.ArrayBindingPattern: - return emitArrayBindingPattern(node); - case SyntaxKind.BindingElement: - return emitBindingElement(node); - - // Misc - case SyntaxKind.TemplateSpan: - return emitTemplateSpan(node); - case SyntaxKind.SemicolonClassElement: - return emitSemicolonClassElement(); - - // Statements - case SyntaxKind.Block: - return emitBlock(node); - case SyntaxKind.VariableStatement: - return emitVariableStatement(node); - case SyntaxKind.EmptyStatement: - return emitEmptyStatement(/*isEmbeddedStatement*/ false); - case SyntaxKind.ExpressionStatement: - return emitExpressionStatement(node); - case SyntaxKind.IfStatement: - return emitIfStatement(node); - case SyntaxKind.DoStatement: - return emitDoStatement(node); - case SyntaxKind.WhileStatement: - return emitWhileStatement(node); - case SyntaxKind.ForStatement: - return emitForStatement(node); - case SyntaxKind.ForInStatement: - return emitForInStatement(node); - case SyntaxKind.ForOfStatement: - return emitForOfStatement(node); - case SyntaxKind.ContinueStatement: - return emitContinueStatement(node); - case SyntaxKind.BreakStatement: - return emitBreakStatement(node); - case SyntaxKind.ReturnStatement: - return emitReturnStatement(node); - case SyntaxKind.WithStatement: - return emitWithStatement(node); - case SyntaxKind.SwitchStatement: - return emitSwitchStatement(node); - case SyntaxKind.LabeledStatement: - return emitLabeledStatement(node); - case SyntaxKind.ThrowStatement: - return emitThrowStatement(node); - case SyntaxKind.TryStatement: - return emitTryStatement(node); - case SyntaxKind.DebuggerStatement: - return emitDebuggerStatement(node); - - // Declarations - case SyntaxKind.VariableDeclaration: - return emitVariableDeclaration(node); - case SyntaxKind.VariableDeclarationList: - return emitVariableDeclarationList(node); - case SyntaxKind.FunctionDeclaration: - return emitFunctionDeclaration(node); - case SyntaxKind.ClassDeclaration: - return emitClassDeclaration(node); - case SyntaxKind.InterfaceDeclaration: - return emitInterfaceDeclaration(node); - case SyntaxKind.TypeAliasDeclaration: - return emitTypeAliasDeclaration(node); - case SyntaxKind.EnumDeclaration: - return emitEnumDeclaration(node); - case SyntaxKind.ModuleDeclaration: - return emitModuleDeclaration(node); - case SyntaxKind.ModuleBlock: - return emitModuleBlock(node); - case SyntaxKind.CaseBlock: - return emitCaseBlock(node); - case SyntaxKind.NamespaceExportDeclaration: - return emitNamespaceExportDeclaration(node); - case SyntaxKind.ImportEqualsDeclaration: - return emitImportEqualsDeclaration(node); - case SyntaxKind.ImportDeclaration: - return emitImportDeclaration(node); - case SyntaxKind.ImportClause: - return emitImportClause(node); - case SyntaxKind.NamespaceImport: - return emitNamespaceImport(node); - case SyntaxKind.NamespaceExport: - return emitNamespaceExport(node); - case SyntaxKind.NamedImports: - return emitNamedImports(node); - case SyntaxKind.ImportSpecifier: - return emitImportSpecifier(node); - case SyntaxKind.ExportAssignment: - return emitExportAssignment(node); - case SyntaxKind.ExportDeclaration: - return emitExportDeclaration(node); - case SyntaxKind.NamedExports: - return emitNamedExports(node); - case SyntaxKind.ExportSpecifier: - return emitExportSpecifier(node); - case SyntaxKind.MissingDeclaration: + break; + case sourceMapFilePath: + if (sourceMapText === text) return; - - // Module references - case SyntaxKind.ExternalModuleReference: - return emitExternalModuleReference(node); - - // JSX (non-expression) - case SyntaxKind.JsxText: - return emitJsxText(node); - case SyntaxKind.JsxOpeningElement: - case SyntaxKind.JsxOpeningFragment: - return emitJsxOpeningElementOrFragment(node); - case SyntaxKind.JsxClosingElement: - case SyntaxKind.JsxClosingFragment: - return emitJsxClosingElementOrFragment(node); - case SyntaxKind.JsxAttribute: - return emitJsxAttribute(node); - case SyntaxKind.JsxAttributes: - return emitJsxAttributes(node); - case SyntaxKind.JsxSpreadAttribute: - return emitJsxSpreadAttribute(node); - case SyntaxKind.JsxExpression: - return emitJsxExpression(node); - - // Clauses - case SyntaxKind.CaseClause: - return emitCaseClause(node); - case SyntaxKind.DefaultClause: - return emitDefaultClause(node); - case SyntaxKind.HeritageClause: - return emitHeritageClause(node); - case SyntaxKind.CatchClause: - return emitCatchClause(node); - - // Property assignments - case SyntaxKind.PropertyAssignment: - return emitPropertyAssignment(node); - case SyntaxKind.ShorthandPropertyAssignment: - return emitShorthandPropertyAssignment(node); - case SyntaxKind.SpreadAssignment: - return emitSpreadAssignment(node as SpreadAssignment); - - // Enum - case SyntaxKind.EnumMember: - return emitEnumMember(node); - - // JSDoc nodes (only used in codefixes currently) - case SyntaxKind.JSDocParameterTag: - case SyntaxKind.JSDocPropertyTag: - return emitJSDocPropertyLikeTag(node as JSDocPropertyLikeTag); - case SyntaxKind.JSDocReturnTag: - case SyntaxKind.JSDocTypeTag: - case SyntaxKind.JSDocThisTag: - case SyntaxKind.JSDocEnumTag: - return emitJSDocSimpleTypedTag(node as JSDocTypeTag); - case SyntaxKind.JSDocImplementsTag: - case SyntaxKind.JSDocAugmentsTag: - return emitJSDocHeritageTag(node as JSDocImplementsTag | JSDocAugmentsTag); - case SyntaxKind.JSDocTemplateTag: - return emitJSDocTemplateTag(node as JSDocTemplateTag); - case SyntaxKind.JSDocTypedefTag: - return emitJSDocTypedefTag(node as JSDocTypedefTag); - case SyntaxKind.JSDocCallbackTag: - return emitJSDocCallbackTag(node as JSDocCallbackTag); - case SyntaxKind.JSDocSignature: - return emitJSDocSignature(node as JSDocSignature); - case SyntaxKind.JSDocTypeLiteral: - return emitJSDocTypeLiteral(node as JSDocTypeLiteral); - case SyntaxKind.JSDocClassTag: - case SyntaxKind.JSDocTag: - return emitJSDocSimpleTag(node as JSDocTag); - - case SyntaxKind.JSDocComment: - return emitJSDoc(node as JSDoc); - - // Transformation nodes (ignored) - } - - if (isExpression(node)) { - hint = EmitHint.Expression; - if (substituteNode !== noEmitSubstitution) { - lastSubstitution = node = substituteNode(hint, node); + break; + case buildInfoPath: + const newBuildInfo = getBuildInfo(text); + newBuildInfo.program = buildInfo.program; + // Update sourceFileInfo + const { js, dts, sourceFiles } = buildInfo.bundle!; + newBuildInfo.bundle!.js!.sources = js!.sources; + if (dts) { + newBuildInfo.bundle!.dts!.sources = dts.sources; } - } - else if (isToken(node)) { - return writeTokenNode(node, writePunctuation); - } - } - if (hint === EmitHint.Expression) { - switch (node.kind) { - // Literals - case SyntaxKind.NumericLiteral: - case SyntaxKind.BigIntLiteral: - return emitNumericOrBigIntLiteral(node); - - case SyntaxKind.StringLiteral: - case SyntaxKind.RegularExpressionLiteral: - case SyntaxKind.NoSubstitutionTemplateLiteral: - return emitLiteral(node, /*jsxAttributeEscape*/ false); - - // Identifiers - case SyntaxKind.Identifier: - return emitIdentifier(node); - - // Reserved words - case SyntaxKind.FalseKeyword: - case SyntaxKind.NullKeyword: - case SyntaxKind.SuperKeyword: - case SyntaxKind.TrueKeyword: - case SyntaxKind.ThisKeyword: - case SyntaxKind.ImportKeyword: - writeTokenNode(node, writeKeyword); + newBuildInfo.bundle!.sourceFiles = sourceFiles; + outputFiles.push({ name, text: getBuildInfoText(newBuildInfo), writeByteOrderMark }); + return; + case declarationFilePath: + if (declarationText === text) return; - - // Expressions - case SyntaxKind.ArrayLiteralExpression: - return emitArrayLiteralExpression(node); - case SyntaxKind.ObjectLiteralExpression: - return emitObjectLiteralExpression(node); - case SyntaxKind.PropertyAccessExpression: - return emitPropertyAccessExpression(node); - case SyntaxKind.ElementAccessExpression: - return emitElementAccessExpression(node); - case SyntaxKind.CallExpression: - return emitCallExpression(node); - case SyntaxKind.NewExpression: - return emitNewExpression(node); - case SyntaxKind.TaggedTemplateExpression: - return emitTaggedTemplateExpression(node); - case SyntaxKind.TypeAssertionExpression: - return emitTypeAssertionExpression(node); - case SyntaxKind.ParenthesizedExpression: - return emitParenthesizedExpression(node); - case SyntaxKind.FunctionExpression: - return emitFunctionExpression(node); - case SyntaxKind.ArrowFunction: - return emitArrowFunction(node); - case SyntaxKind.DeleteExpression: - return emitDeleteExpression(node); - case SyntaxKind.TypeOfExpression: - return emitTypeOfExpression(node); - case SyntaxKind.VoidExpression: - return emitVoidExpression(node); - case SyntaxKind.AwaitExpression: - return emitAwaitExpression(node); - case SyntaxKind.PrefixUnaryExpression: - return emitPrefixUnaryExpression(node); - case SyntaxKind.PostfixUnaryExpression: - return emitPostfixUnaryExpression(node); - case SyntaxKind.BinaryExpression: - return emitBinaryExpression(node); - case SyntaxKind.ConditionalExpression: - return emitConditionalExpression(node); - case SyntaxKind.TemplateExpression: - return emitTemplateExpression(node); - case SyntaxKind.YieldExpression: - return emitYieldExpression(node); - case SyntaxKind.SpreadElement: - return emitSpreadExpression(node); - case SyntaxKind.ClassExpression: - return emitClassExpression(node); - case SyntaxKind.OmittedExpression: + break; + case declarationMapPath: + if (declarationMapText === text) return; - case SyntaxKind.AsExpression: - return emitAsExpression(node); - case SyntaxKind.NonNullExpression: - return emitNonNullExpression(node); - case SyntaxKind.MetaProperty: - return emitMetaProperty(node); - - // JSX - case SyntaxKind.JsxElement: - return emitJsxElement(node); - case SyntaxKind.JsxSelfClosingElement: - return emitJsxSelfClosingElement(node); - case SyntaxKind.JsxFragment: - return emitJsxFragment(node); - - // Transformation nodes - case SyntaxKind.PartiallyEmittedExpression: - return emitPartiallyEmittedExpression(node); - - case SyntaxKind.CommaListExpression: - return emitCommaList(node); + break; + default: + Debug.fail(`Unexpected path: ${name}`); + } + outputFiles.push({ name, text, writeByteOrderMark }); + }, + isEmitBlocked: returnFalse, + readFile: f => host.readFile(f), + fileExists: f => host.fileExists(f), + directoryExists: host.directoryExists && (f => host.directoryExists!(f)), + useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames(), + getProgramBuildInfo: returnUndefined, + getSourceFileFromReference: returnUndefined, + redirectTargetsMap: createMultiMap() + }; + emitFiles(notImplementedResolver, emitHost, + /*targetSourceFile*/ undefined, getTransformers(config.options, customTransformers)); + return outputFiles; +} +const enum PipelinePhase { + Notification, + Substitution, + Comments, + SourceMaps, + Emit +} +export function createPrinter(printerOptions: PrinterOptions = {}, handlers: PrintHandlers = {}): Printer { + const { hasGlobalName, onEmitNode = noEmitNotification, isEmitNotificationEnabled, substituteNode = noEmitSubstitution, onBeforeEmitNodeArray, onAfterEmitNodeArray, onBeforeEmitToken, onAfterEmitToken } = handlers; + const extendedDiagnostics = !!printerOptions.extendedDiagnostics; + const newLine = getNewLineCharacter(printerOptions); + const moduleKind = getEmitModuleKind(printerOptions); + const bundledHelpers = createMap(); + let currentSourceFile: SourceFile | undefined; + let nodeIdToGeneratedName: string[]; // Map of generated names for specific nodes. + let autoGeneratedIdToGeneratedName: string[]; // Map of generated names for temp and loop variables. + let generatedNames: ts.Map; // Set of names generated by the NameGenerator. + let tempFlagsStack: TempFlags[]; // Stack of enclosing name generation scopes. + let tempFlags: TempFlags; // TempFlags for the current name generation scope. + let reservedNamesStack: ts.Map[]; // Stack of TempFlags reserved in enclosing name generation scopes. + let reservedNames: ts.Map; // TempFlags to reserve in nested name generation scopes. + let writer: EmitTextWriter; + let ownWriter: EmitTextWriter; // Reusable `EmitTextWriter` for basic printing. + let write = writeBase; + let isOwnFileEmit: boolean; + const bundleFileInfo = printerOptions.writeBundleFileInfo ? { sections: [] } as BundleFileInfo : undefined; + const relativeToBuildInfo = bundleFileInfo ? Debug.checkDefined(printerOptions.relativeToBuildInfo) : undefined; + const recordInternalSection = printerOptions.recordInternalSection; + let sourceFileTextPos = 0; + let sourceFileTextKind: BundleFileTextLikeKind = BundleFileSectionKind.Text; + // Source Maps + let sourceMapsDisabled = true; + let sourceMapGenerator: SourceMapGenerator | undefined; + let sourceMapSource: SourceMapSource; + let sourceMapSourceIndex = -1; + // Comments + let containerPos = -1; + let containerEnd = -1; + let declarationListContainerEnd = -1; + let currentLineMap: readonly number[] | undefined; + let detachedCommentsInfo: { + nodePos: number; + detachedCommentEndPos: number; + }[] | undefined; + let hasWrittenComment = false; + let commentsDisabled = !!printerOptions.removeComments; + let lastNode: Node | undefined; + let lastSubstitution: Node | undefined; + const { enter: enterComment, exit: exitComment } = createTimerIf(extendedDiagnostics, "commentTime", "beforeComment", "afterComment"); + reset(); + return { + // public API + printNode, + printList, + printFile, + printBundle, + // internal API + writeNode, + writeList, + writeFile, + writeBundle, + bundleFileInfo + }; + function printNode(hint: EmitHint, node: Node, sourceFile: SourceFile): string { + switch (hint) { + case EmitHint.SourceFile: + Debug.assert(isSourceFile(node), "Expected a SourceFile node."); + break; + case EmitHint.IdentifierName: + Debug.assert(isIdentifier(node), "Expected an Identifier node."); + break; + case EmitHint.Expression: + Debug.assert(isExpression(node), "Expected an Expression node."); + break; + } + switch (node.kind) { + case SyntaxKind.SourceFile: return printFile((node)); + case SyntaxKind.Bundle: return printBundle((node)); + case SyntaxKind.UnparsedSource: return printUnparsedSource((node)); + } + writeNode(hint, node, sourceFile, beginPrint()); + return endPrint(); + } + function printList(format: ListFormat, nodes: NodeArray, sourceFile: SourceFile) { + writeList(format, nodes, sourceFile, beginPrint()); + return endPrint(); + } + function printBundle(bundle: Bundle): string { + writeBundle(bundle, beginPrint(), /*sourceMapEmitter*/ undefined); + return endPrint(); + } + function printFile(sourceFile: SourceFile): string { + writeFile(sourceFile, beginPrint(), /*sourceMapEmitter*/ undefined); + return endPrint(); + } + function printUnparsedSource(unparsed: UnparsedSource): string { + writeUnparsedSource(unparsed, beginPrint()); + return endPrint(); + } + /** + * If `sourceFile` is `undefined`, `node` must be a synthesized `TypeNode`. + */ + function writeNode(hint: EmitHint, node: TypeNode, sourceFile: undefined, output: EmitTextWriter): void; + function writeNode(hint: EmitHint, node: Node, sourceFile: SourceFile, output: EmitTextWriter): void; + function writeNode(hint: EmitHint, node: Node, sourceFile: SourceFile | undefined, output: EmitTextWriter) { + const previousWriter = writer; + setWriter(output, /*_sourceMapGenerator*/ undefined); + print(hint, node, sourceFile); + reset(); + writer = previousWriter; + } + function writeList(format: ListFormat, nodes: NodeArray, sourceFile: SourceFile | undefined, output: EmitTextWriter) { + const previousWriter = writer; + setWriter(output, /*_sourceMapGenerator*/ undefined); + if (sourceFile) { + setSourceFile(sourceFile); + } + emitList(syntheticParent, nodes, format); + reset(); + writer = previousWriter; + } + function getTextPosWithWriteLine() { + return writer.getTextPosWithWriteLine ? writer.getTextPosWithWriteLine() : writer.getTextPos(); + } + function updateOrPushBundleFileTextLike(pos: number, end: number, kind: BundleFileTextLikeKind) { + const last = lastOrUndefined(bundleFileInfo!.sections); + if (last && last.kind === kind) { + last.end = end; + } + else { + bundleFileInfo!.sections.push({ pos, end, kind }); + } + } + function recordBundleFileInternalSectionStart(node: Node) { + if (recordInternalSection && + bundleFileInfo && + currentSourceFile && + (isDeclaration(node) || isVariableStatement(node)) && + isInternalDeclaration(node, currentSourceFile) && + sourceFileTextKind !== BundleFileSectionKind.Internal) { + const prevSourceFileTextKind = sourceFileTextKind; + recordBundleFileTextLikeSection(writer.getTextPos()); + sourceFileTextPos = getTextPosWithWriteLine(); + sourceFileTextKind = BundleFileSectionKind.Internal; + return prevSourceFileTextKind; + } + return undefined; + } + function recordBundleFileInternalSectionEnd(prevSourceFileTextKind: ReturnType) { + if (prevSourceFileTextKind) { + recordBundleFileTextLikeSection(writer.getTextPos()); + sourceFileTextPos = getTextPosWithWriteLine(); + sourceFileTextKind = prevSourceFileTextKind; + } + } + function recordBundleFileTextLikeSection(end: number) { + if (sourceFileTextPos < end) { + updateOrPushBundleFileTextLike(sourceFileTextPos, end, sourceFileTextKind); + return true; + } + return false; + } + function writeBundle(bundle: Bundle, output: EmitTextWriter, sourceMapGenerator: SourceMapGenerator | undefined) { + isOwnFileEmit = false; + const previousWriter = writer; + setWriter(output, sourceMapGenerator); + emitShebangIfNeeded(bundle); + emitPrologueDirectivesIfNeeded(bundle); + emitHelpers(bundle); + emitSyntheticTripleSlashReferencesIfNeeded(bundle); + for (const prepend of bundle.prepends) { + writeLine(); + const pos = writer.getTextPos(); + const savedSections = bundleFileInfo && bundleFileInfo.sections; + if (savedSections) + bundleFileInfo!.sections = []; + print(EmitHint.Unspecified, prepend, /*sourceFile*/ undefined); + if (bundleFileInfo) { + const newSections = bundleFileInfo.sections; + bundleFileInfo.sections = savedSections!; + if (prepend.oldFileOfCurrentEmit) + bundleFileInfo.sections.push(...newSections); + else { + newSections.forEach(section => Debug.assert(isBundleFileTextLike(section))); + bundleFileInfo.sections.push({ + pos, + end: writer.getTextPos(), + kind: BundleFileSectionKind.Prepend, + data: relativeToBuildInfo!((prepend as UnparsedSource).fileName), + texts: (newSections as BundleFileTextLike[]) + }); } } } - - function emitMappedTypeParameter(node: TypeParameterDeclaration): void { - emit(node.name); - writeSpace(); - writeKeyword("in"); - writeSpace(); - emit(node.constraint); + sourceFileTextPos = getTextPosWithWriteLine(); + for (const sourceFile of bundle.sourceFiles) { + print(EmitHint.SourceFile, sourceFile, sourceFile); } - - function pipelineEmitWithSubstitution(hint: EmitHint, node: Node) { - Debug.assert(lastNode === node || lastSubstitution === node); - const pipelinePhase = getNextPipelinePhase(PipelinePhase.Substitution, hint, node); - pipelinePhase(hint, lastSubstitution!); - Debug.assert(lastNode === node || lastSubstitution === node); - } - - function getHelpersFromBundledSourceFiles(bundle: Bundle): string[] | undefined { - let result: string[] | undefined; - if (moduleKind === ModuleKind.None || printerOptions.noEmitHelpers) { - return undefined; - } - const bundledHelpers = createMap(); - for (const sourceFile of bundle.sourceFiles) { - const shouldSkip = getExternalHelpersModuleName(sourceFile) !== undefined; - const helpers = getSortedEmitHelpers(sourceFile); - if (!helpers) continue; - for (const helper of helpers) { - if (!helper.scoped && !shouldSkip && !bundledHelpers.get(helper.name)) { - bundledHelpers.set(helper.name, true); - (result || (result = [])).push(helper.name); - } + if (bundleFileInfo && bundle.sourceFiles.length) { + const end = writer.getTextPos(); + if (recordBundleFileTextLikeSection(end)) { + // Store prologues + const prologues = getPrologueDirectivesFromBundledSourceFiles(bundle); + if (prologues) { + if (!bundleFileInfo.sources) + bundleFileInfo.sources = {}; + bundleFileInfo.sources.prologues = prologues; } - } - - return result; - } - - function emitHelpers(node: Node) { - let helpersEmitted = false; - const bundle = node.kind === SyntaxKind.Bundle ? node : undefined; - if (bundle && moduleKind === ModuleKind.None) { - return; - } - const numPrepends = bundle ? bundle.prepends.length : 0; - const numNodes = bundle ? bundle.sourceFiles.length + numPrepends : 1; - for (let i = 0; i < numNodes; i++) { - const currentNode = bundle ? i < numPrepends ? bundle.prepends[i] : bundle.sourceFiles[i - numPrepends] : node; - const sourceFile = isSourceFile(currentNode) ? currentNode : isUnparsedSource(currentNode) ? undefined : currentSourceFile!; - const shouldSkip = printerOptions.noEmitHelpers || (!!sourceFile && hasRecordedExternalHelpers(sourceFile)); - const shouldBundle = (isSourceFile(currentNode) || isUnparsedSource(currentNode)) && !isOwnFileEmit; - const helpers = isUnparsedSource(currentNode) ? currentNode.helpers : getSortedEmitHelpers(currentNode); + // Store helpes + const helpers = getHelpersFromBundledSourceFiles(bundle); if (helpers) { - for (const helper of helpers) { - if (!helper.scoped) { - // Skip the helper if it can be skipped and the noEmitHelpers compiler - // option is set, or if it can be imported and the importHelpers compiler - // option is set. - if (shouldSkip) continue; - - // Skip the helper if it can be bundled but hasn't already been emitted and we - // are emitting a bundled module. - if (shouldBundle) { - if (bundledHelpers.get(helper.name)) { - continue; - } - - bundledHelpers.set(helper.name, true); - } - } - else if (bundle) { - // Skip the helper if it is scoped and we are emitting bundled helpers - continue; - } - const pos = getTextPosWithWriteLine(); - if (typeof helper.text === "string") { - writeLines(helper.text); - } - else { - writeLines(helper.text(makeFileLevelOptimisticUniqueName)); - } - if (bundleFileInfo) bundleFileInfo.sections.push({ pos, end: writer.getTextPos(), kind: BundleFileSectionKind.EmitHelpers, data: helper.name }); - helpersEmitted = true; - } + if (!bundleFileInfo.sources) + bundleFileInfo.sources = {}; + bundleFileInfo.sources.helpers = helpers; } } - - return helpersEmitted; } - - function getSortedEmitHelpers(node: Node) { - const helpers = getEmitHelpers(node); - return helpers && stableSort(helpers, compareEmitHelpers); + reset(); + writer = previousWriter; + } + function writeUnparsedSource(unparsed: UnparsedSource, output: EmitTextWriter) { + const previousWriter = writer; + setWriter(output, /*_sourceMapGenerator*/ undefined); + print(EmitHint.Unspecified, unparsed, /*sourceFile*/ undefined); + reset(); + writer = previousWriter; + } + function writeFile(sourceFile: SourceFile, output: EmitTextWriter, sourceMapGenerator: SourceMapGenerator | undefined) { + isOwnFileEmit = true; + const previousWriter = writer; + setWriter(output, sourceMapGenerator); + emitShebangIfNeeded(sourceFile); + emitPrologueDirectivesIfNeeded(sourceFile); + print(EmitHint.SourceFile, sourceFile, sourceFile); + reset(); + writer = previousWriter; + } + function beginPrint() { + return ownWriter || (ownWriter = createTextWriter(newLine)); + } + function endPrint() { + const text = ownWriter.getText(); + ownWriter.clear(); + return text; + } + function print(hint: EmitHint, node: Node, sourceFile: SourceFile | undefined) { + if (sourceFile) { + setSourceFile(sourceFile); } - - // - // Literals/Pseudo-literals - // - - // SyntaxKind.NumericLiteral - // SyntaxKind.BigIntLiteral - function emitNumericOrBigIntLiteral(node: NumericLiteral | BigIntLiteral) { - emitLiteral(node, /*jsxAttributeEscape*/ false); - } - - // SyntaxKind.StringLiteral - // SyntaxKind.RegularExpressionLiteral - // SyntaxKind.NoSubstitutionTemplateLiteral - // SyntaxKind.TemplateHead - // SyntaxKind.TemplateMiddle - // SyntaxKind.TemplateTail - function emitLiteral(node: LiteralLikeNode, jsxAttributeEscape: boolean) { - const text = getLiteralTextOfNode(node, printerOptions.neverAsciiEscape, jsxAttributeEscape); - if ((printerOptions.sourceMap || printerOptions.inlineSourceMap) - && (node.kind === SyntaxKind.StringLiteral || isTemplateLiteralKind(node.kind))) { - writeLiteral(text); - } - else { - // Quick info expects all literals to be called with writeStringLiteral, as there's no specific type for numberLiterals - writeStringLiteral(text); - } + pipelineEmit(hint, node); + } + function setSourceFile(sourceFile: SourceFile | undefined) { + currentSourceFile = sourceFile; + currentLineMap = undefined; + detachedCommentsInfo = undefined; + if (sourceFile) { + setSourceMapSource(sourceFile); } - - // SyntaxKind.UnparsedSource - // SyntaxKind.UnparsedPrepend - function emitUnparsedSourceOrPrepend(unparsed: UnparsedSource | UnparsedPrepend) { - for (const text of unparsed.texts) { - writeLine(); - emit(text); - } + } + function setWriter(_writer: EmitTextWriter | undefined, _sourceMapGenerator: SourceMapGenerator | undefined) { + if (_writer && printerOptions.omitTrailingSemicolon) { + _writer = getTrailingSemicolonDeferringWriter(_writer); } - - // SyntaxKind.UnparsedPrologue - // SyntaxKind.UnparsedText - // SyntaxKind.UnparsedInternal - // SyntaxKind.UnparsedSyntheticReference - function writeUnparsedNode(unparsed: UnparsedNode) { - writer.rawWrite(unparsed.parent.text.substring(unparsed.pos, unparsed.end)); - } - - // SyntaxKind.UnparsedText - // SyntaxKind.UnparsedInternal - function emitUnparsedTextLike(unparsed: UnparsedTextLike) { - const pos = getTextPosWithWriteLine(); - writeUnparsedNode(unparsed); - if (bundleFileInfo) { - updateOrPushBundleFileTextLike( - pos, - writer.getTextPos(), - unparsed.kind === SyntaxKind.UnparsedText ? - BundleFileSectionKind.Text : - BundleFileSectionKind.Internal - ); - } + writer = _writer!; // TODO: GH#18217 + sourceMapGenerator = _sourceMapGenerator; + sourceMapsDisabled = !writer || !sourceMapGenerator; + } + function reset() { + nodeIdToGeneratedName = []; + autoGeneratedIdToGeneratedName = []; + generatedNames = createMap(); + tempFlagsStack = []; + tempFlags = TempFlags.Auto; + reservedNamesStack = []; + currentSourceFile = undefined!; + currentLineMap = undefined!; + detachedCommentsInfo = undefined; + lastNode = undefined; + lastSubstitution = undefined; + setWriter(/*output*/ undefined, /*_sourceMapGenerator*/ undefined); + } + function getCurrentLineMap() { + return currentLineMap || (currentLineMap = getLineStarts((currentSourceFile!))); + } + function emit(node: Node): Node; + function emit(node: Node | undefined): Node | undefined; + function emit(node: Node | undefined) { + if (node === undefined) + return; + const prevSourceFileTextKind = recordBundleFileInternalSectionStart(node); + const substitute = pipelineEmit(EmitHint.Unspecified, node); + recordBundleFileInternalSectionEnd(prevSourceFileTextKind); + return substitute; + } + function emitIdentifierName(node: Identifier): Node; + function emitIdentifierName(node: Identifier | undefined): Node | undefined; + function emitIdentifierName(node: Identifier | undefined): Node | undefined { + if (node === undefined) + return; + return pipelineEmit(EmitHint.IdentifierName, node); + } + function emitExpression(node: Expression): Node; + function emitExpression(node: Expression | undefined): Node | undefined; + function emitExpression(node: Expression | undefined): Node | undefined { + if (node === undefined) + return; + return pipelineEmit(EmitHint.Expression, node); + } + function emitJsxAttributeValue(node: StringLiteral | JsxExpression): Node { + return pipelineEmit(isStringLiteral(node) ? EmitHint.JsxAttributeValue : EmitHint.Unspecified, node); + } + function pipelineEmit(emitHint: EmitHint, node: Node) { + const savedLastNode = lastNode; + const savedLastSubstitution = lastSubstitution; + lastNode = node; + lastSubstitution = undefined; + const pipelinePhase = getPipelinePhase(PipelinePhase.Notification, emitHint, node); + pipelinePhase(emitHint, node); + Debug.assert(lastNode === node); + const substitute = lastSubstitution; + lastNode = savedLastNode; + lastSubstitution = savedLastSubstitution; + return substitute || node; + } + function getPipelinePhase(phase: PipelinePhase, emitHint: EmitHint, node: Node) { + switch (phase) { + case PipelinePhase.Notification: + if (onEmitNode !== noEmitNotification && (!isEmitNotificationEnabled || isEmitNotificationEnabled(node))) { + return pipelineEmitWithNotification; + } + // falls through + case PipelinePhase.Substitution: + if (substituteNode !== noEmitSubstitution && (lastSubstitution = substituteNode(emitHint, node)) !== node) { + return pipelineEmitWithSubstitution; + } + // falls through + case PipelinePhase.Comments: + if (!commentsDisabled && node.kind !== SyntaxKind.SourceFile) { + return pipelineEmitWithComments; + } + // falls through + case PipelinePhase.SourceMaps: + if (!sourceMapsDisabled && node.kind !== SyntaxKind.SourceFile && !isInJsonFile(node)) { + return pipelineEmitWithSourceMap; + } + // falls through + case PipelinePhase.Emit: + return pipelineEmitWithHint; + default: + return Debug.assertNever(phase); } - - // SyntaxKind.UnparsedSyntheticReference - function emitUnparsedSyntheticReference(unparsed: UnparsedSyntheticReference) { - const pos = getTextPosWithWriteLine(); - writeUnparsedNode(unparsed); - if (bundleFileInfo) { - const section = clone(unparsed.section); - section.pos = pos; - section.end = writer.getTextPos(); - bundleFileInfo.sections.push(section); + } + function getNextPipelinePhase(currentPhase: PipelinePhase, emitHint: EmitHint, node: Node) { + return getPipelinePhase(currentPhase + 1, emitHint, node); + } + function pipelineEmitWithNotification(hint: EmitHint, node: Node) { + Debug.assert(lastNode === node); + const pipelinePhase = getNextPipelinePhase(PipelinePhase.Notification, hint, node); + onEmitNode(hint, node, pipelinePhase); + Debug.assert(lastNode === node); + } + function pipelineEmitWithHint(hint: EmitHint, node: Node): void { + Debug.assert(lastNode === node || lastSubstitution === node); + if (hint === EmitHint.SourceFile) + return emitSourceFile(cast(node, isSourceFile)); + if (hint === EmitHint.IdentifierName) + return emitIdentifier(cast(node, isIdentifier)); + if (hint === EmitHint.JsxAttributeValue) + return emitLiteral(cast(node, isStringLiteral), /*jsxAttributeEscape*/ true); + if (hint === EmitHint.MappedTypeParameter) + return emitMappedTypeParameter(cast(node, isTypeParameterDeclaration)); + if (hint === EmitHint.EmbeddedStatement) { + Debug.assertNode(node, isEmptyStatement); + return emitEmptyStatement(/*isEmbeddedStatement*/ true); + } + if (hint === EmitHint.Unspecified) { + if (isKeyword(node.kind)) + return writeTokenNode(node, writeKeyword); + switch (node.kind) { + // Pseudo-literals + case SyntaxKind.TemplateHead: + case SyntaxKind.TemplateMiddle: + case SyntaxKind.TemplateTail: + return emitLiteral((node), /*jsxAttributeEscape*/ false); + case SyntaxKind.UnparsedSource: + case SyntaxKind.UnparsedPrepend: + return emitUnparsedSourceOrPrepend((node)); + case SyntaxKind.UnparsedPrologue: + return writeUnparsedNode((node)); + case SyntaxKind.UnparsedText: + case SyntaxKind.UnparsedInternalText: + return emitUnparsedTextLike((node)); + case SyntaxKind.UnparsedSyntheticReference: + return emitUnparsedSyntheticReference((node)); + // Identifiers + case SyntaxKind.Identifier: + return emitIdentifier((node)); + // PrivateIdentifiers + case SyntaxKind.PrivateIdentifier: + return emitPrivateIdentifier((node as PrivateIdentifier)); + // Parse tree nodes + // Names + case SyntaxKind.QualifiedName: + return emitQualifiedName((node)); + case SyntaxKind.ComputedPropertyName: + return emitComputedPropertyName((node)); + // Signature elements + case SyntaxKind.TypeParameter: + return emitTypeParameter((node)); + case SyntaxKind.Parameter: + return emitParameter((node)); + case SyntaxKind.Decorator: + return emitDecorator((node)); + // Type members + case SyntaxKind.PropertySignature: + return emitPropertySignature((node)); + case SyntaxKind.PropertyDeclaration: + return emitPropertyDeclaration((node)); + case SyntaxKind.MethodSignature: + return emitMethodSignature((node)); + case SyntaxKind.MethodDeclaration: + return emitMethodDeclaration((node)); + case SyntaxKind.Constructor: + return emitConstructor((node)); + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + return emitAccessorDeclaration((node)); + case SyntaxKind.CallSignature: + return emitCallSignature((node)); + case SyntaxKind.ConstructSignature: + return emitConstructSignature((node)); + case SyntaxKind.IndexSignature: + return emitIndexSignature((node)); + // Types + case SyntaxKind.TypePredicate: + return emitTypePredicate((node)); + case SyntaxKind.TypeReference: + return emitTypeReference((node)); + case SyntaxKind.FunctionType: + return emitFunctionType((node)); + case SyntaxKind.JSDocFunctionType: + return emitJSDocFunctionType((node as JSDocFunctionType)); + case SyntaxKind.ConstructorType: + return emitConstructorType((node)); + case SyntaxKind.TypeQuery: + return emitTypeQuery((node)); + case SyntaxKind.TypeLiteral: + return emitTypeLiteral((node)); + case SyntaxKind.ArrayType: + return emitArrayType((node)); + case SyntaxKind.TupleType: + return emitTupleType((node)); + case SyntaxKind.OptionalType: + return emitOptionalType((node)); + case SyntaxKind.UnionType: + return emitUnionType((node)); + case SyntaxKind.IntersectionType: + return emitIntersectionType((node)); + case SyntaxKind.ConditionalType: + return emitConditionalType((node)); + case SyntaxKind.InferType: + return emitInferType((node)); + case SyntaxKind.ParenthesizedType: + return emitParenthesizedType((node)); + case SyntaxKind.ExpressionWithTypeArguments: + return emitExpressionWithTypeArguments((node)); + case SyntaxKind.ThisType: + return emitThisType(); + case SyntaxKind.TypeOperator: + return emitTypeOperator((node)); + case SyntaxKind.IndexedAccessType: + return emitIndexedAccessType((node)); + case SyntaxKind.MappedType: + return emitMappedType((node)); + case SyntaxKind.LiteralType: + return emitLiteralType((node)); + case SyntaxKind.ImportType: + return emitImportTypeNode((node)); + case SyntaxKind.JSDocAllType: + writePunctuation("*"); + return; + case SyntaxKind.JSDocUnknownType: + writePunctuation("?"); + return; + case SyntaxKind.JSDocNullableType: + return emitJSDocNullableType((node as JSDocNullableType)); + case SyntaxKind.JSDocNonNullableType: + return emitJSDocNonNullableType((node as JSDocNonNullableType)); + case SyntaxKind.JSDocOptionalType: + return emitJSDocOptionalType((node as JSDocOptionalType)); + case SyntaxKind.RestType: + case SyntaxKind.JSDocVariadicType: + return emitRestOrJSDocVariadicType((node as RestTypeNode | JSDocVariadicType)); + // Binding patterns + case SyntaxKind.ObjectBindingPattern: + return emitObjectBindingPattern((node)); + case SyntaxKind.ArrayBindingPattern: + return emitArrayBindingPattern((node)); + case SyntaxKind.BindingElement: + return emitBindingElement((node)); + // Misc + case SyntaxKind.TemplateSpan: + return emitTemplateSpan((node)); + case SyntaxKind.SemicolonClassElement: + return emitSemicolonClassElement(); + // Statements + case SyntaxKind.Block: + return emitBlock((node)); + case SyntaxKind.VariableStatement: + return emitVariableStatement((node)); + case SyntaxKind.EmptyStatement: + return emitEmptyStatement(/*isEmbeddedStatement*/ false); + case SyntaxKind.ExpressionStatement: + return emitExpressionStatement((node)); + case SyntaxKind.IfStatement: + return emitIfStatement((node)); + case SyntaxKind.DoStatement: + return emitDoStatement((node)); + case SyntaxKind.WhileStatement: + return emitWhileStatement((node)); + case SyntaxKind.ForStatement: + return emitForStatement((node)); + case SyntaxKind.ForInStatement: + return emitForInStatement((node)); + case SyntaxKind.ForOfStatement: + return emitForOfStatement((node)); + case SyntaxKind.ContinueStatement: + return emitContinueStatement((node)); + case SyntaxKind.BreakStatement: + return emitBreakStatement((node)); + case SyntaxKind.ReturnStatement: + return emitReturnStatement((node)); + case SyntaxKind.WithStatement: + return emitWithStatement((node)); + case SyntaxKind.SwitchStatement: + return emitSwitchStatement((node)); + case SyntaxKind.LabeledStatement: + return emitLabeledStatement((node)); + case SyntaxKind.ThrowStatement: + return emitThrowStatement((node)); + case SyntaxKind.TryStatement: + return emitTryStatement((node)); + case SyntaxKind.DebuggerStatement: + return emitDebuggerStatement((node)); + // Declarations + case SyntaxKind.VariableDeclaration: + return emitVariableDeclaration((node)); + case SyntaxKind.VariableDeclarationList: + return emitVariableDeclarationList((node)); + case SyntaxKind.FunctionDeclaration: + return emitFunctionDeclaration((node)); + case SyntaxKind.ClassDeclaration: + return emitClassDeclaration((node)); + case SyntaxKind.InterfaceDeclaration: + return emitInterfaceDeclaration((node)); + case SyntaxKind.TypeAliasDeclaration: + return emitTypeAliasDeclaration((node)); + case SyntaxKind.EnumDeclaration: + return emitEnumDeclaration((node)); + case SyntaxKind.ModuleDeclaration: + return emitModuleDeclaration((node)); + case SyntaxKind.ModuleBlock: + return emitModuleBlock((node)); + case SyntaxKind.CaseBlock: + return emitCaseBlock((node)); + case SyntaxKind.NamespaceExportDeclaration: + return emitNamespaceExportDeclaration((node)); + case SyntaxKind.ImportEqualsDeclaration: + return emitImportEqualsDeclaration((node)); + case SyntaxKind.ImportDeclaration: + return emitImportDeclaration((node)); + case SyntaxKind.ImportClause: + return emitImportClause((node)); + case SyntaxKind.NamespaceImport: + return emitNamespaceImport((node)); + case SyntaxKind.NamespaceExport: + return emitNamespaceExport((node)); + case SyntaxKind.NamedImports: + return emitNamedImports((node)); + case SyntaxKind.ImportSpecifier: + return emitImportSpecifier((node)); + case SyntaxKind.ExportAssignment: + return emitExportAssignment((node)); + case SyntaxKind.ExportDeclaration: + return emitExportDeclaration((node)); + case SyntaxKind.NamedExports: + return emitNamedExports((node)); + case SyntaxKind.ExportSpecifier: + return emitExportSpecifier((node)); + case SyntaxKind.MissingDeclaration: + return; + // Module references + case SyntaxKind.ExternalModuleReference: + return emitExternalModuleReference((node)); + // JSX (non-expression) + case SyntaxKind.JsxText: + return emitJsxText((node)); + case SyntaxKind.JsxOpeningElement: + case SyntaxKind.JsxOpeningFragment: + return emitJsxOpeningElementOrFragment((node)); + case SyntaxKind.JsxClosingElement: + case SyntaxKind.JsxClosingFragment: + return emitJsxClosingElementOrFragment((node)); + case SyntaxKind.JsxAttribute: + return emitJsxAttribute((node)); + case SyntaxKind.JsxAttributes: + return emitJsxAttributes((node)); + case SyntaxKind.JsxSpreadAttribute: + return emitJsxSpreadAttribute((node)); + case SyntaxKind.JsxExpression: + return emitJsxExpression((node)); + // Clauses + case SyntaxKind.CaseClause: + return emitCaseClause((node)); + case SyntaxKind.DefaultClause: + return emitDefaultClause((node)); + case SyntaxKind.HeritageClause: + return emitHeritageClause((node)); + case SyntaxKind.CatchClause: + return emitCatchClause((node)); + // Property assignments + case SyntaxKind.PropertyAssignment: + return emitPropertyAssignment((node)); + case SyntaxKind.ShorthandPropertyAssignment: + return emitShorthandPropertyAssignment((node)); + case SyntaxKind.SpreadAssignment: + return emitSpreadAssignment((node as SpreadAssignment)); + // Enum + case SyntaxKind.EnumMember: + return emitEnumMember((node)); + // JSDoc nodes (only used in codefixes currently) + case SyntaxKind.JSDocParameterTag: + case SyntaxKind.JSDocPropertyTag: + return emitJSDocPropertyLikeTag((node as JSDocPropertyLikeTag)); + case SyntaxKind.JSDocReturnTag: + case SyntaxKind.JSDocTypeTag: + case SyntaxKind.JSDocThisTag: + case SyntaxKind.JSDocEnumTag: + return emitJSDocSimpleTypedTag((node as JSDocTypeTag)); + case SyntaxKind.JSDocImplementsTag: + case SyntaxKind.JSDocAugmentsTag: + return emitJSDocHeritageTag((node as JSDocImplementsTag | JSDocAugmentsTag)); + case SyntaxKind.JSDocTemplateTag: + return emitJSDocTemplateTag((node as JSDocTemplateTag)); + case SyntaxKind.JSDocTypedefTag: + return emitJSDocTypedefTag((node as JSDocTypedefTag)); + case SyntaxKind.JSDocCallbackTag: + return emitJSDocCallbackTag((node as JSDocCallbackTag)); + case SyntaxKind.JSDocSignature: + return emitJSDocSignature((node as JSDocSignature)); + case SyntaxKind.JSDocTypeLiteral: + return emitJSDocTypeLiteral((node as JSDocTypeLiteral)); + case SyntaxKind.JSDocClassTag: + case SyntaxKind.JSDocTag: + return emitJSDocSimpleTag((node as JSDocTag)); + case SyntaxKind.JSDocComment: + return emitJSDoc((node as JSDoc)); + // Transformation nodes (ignored) + } + if (isExpression(node)) { + hint = EmitHint.Expression; + if (substituteNode !== noEmitSubstitution) { + lastSubstitution = node = substituteNode(hint, node); + } } - } - - // - // Identifiers - // - - function emitIdentifier(node: Identifier) { - const writeText = node.symbol ? writeSymbol : write; - writeText(getTextOfNode(node, /*includeTrivia*/ false), node.symbol); - emitList(node, node.typeArguments, ListFormat.TypeParameters); // Call emitList directly since it could be an array of TypeParameterDeclarations _or_ type arguments - } - - // - // Names - // - - function emitPrivateIdentifier(node: PrivateIdentifier) { - const writeText = node.symbol ? writeSymbol : write; - writeText(getTextOfNode(node, /*includeTrivia*/ false), node.symbol); - } - - - function emitQualifiedName(node: QualifiedName) { - emitEntityName(node.left); - writePunctuation("."); - emit(node.right); - } - - function emitEntityName(node: EntityName) { - if (node.kind === SyntaxKind.Identifier) { - emitExpression(node); + else if (isToken(node)) { + return writeTokenNode(node, writePunctuation); } - else { - emit(node); + } + if (hint === EmitHint.Expression) { + switch (node.kind) { + // Literals + case SyntaxKind.NumericLiteral: + case SyntaxKind.BigIntLiteral: + return emitNumericOrBigIntLiteral((node)); + case SyntaxKind.StringLiteral: + case SyntaxKind.RegularExpressionLiteral: + case SyntaxKind.NoSubstitutionTemplateLiteral: + return emitLiteral((node), /*jsxAttributeEscape*/ false); + // Identifiers + case SyntaxKind.Identifier: + return emitIdentifier((node)); + // Reserved words + case SyntaxKind.FalseKeyword: + case SyntaxKind.NullKeyword: + case SyntaxKind.SuperKeyword: + case SyntaxKind.TrueKeyword: + case SyntaxKind.ThisKeyword: + case SyntaxKind.ImportKeyword: + writeTokenNode(node, writeKeyword); + return; + // Expressions + case SyntaxKind.ArrayLiteralExpression: + return emitArrayLiteralExpression((node)); + case SyntaxKind.ObjectLiteralExpression: + return emitObjectLiteralExpression((node)); + case SyntaxKind.PropertyAccessExpression: + return emitPropertyAccessExpression((node)); + case SyntaxKind.ElementAccessExpression: + return emitElementAccessExpression((node)); + case SyntaxKind.CallExpression: + return emitCallExpression((node)); + case SyntaxKind.NewExpression: + return emitNewExpression((node)); + case SyntaxKind.TaggedTemplateExpression: + return emitTaggedTemplateExpression((node)); + case SyntaxKind.TypeAssertionExpression: + return emitTypeAssertionExpression((node)); + case SyntaxKind.ParenthesizedExpression: + return emitParenthesizedExpression((node)); + case SyntaxKind.FunctionExpression: + return emitFunctionExpression((node)); + case SyntaxKind.ArrowFunction: + return emitArrowFunction((node)); + case SyntaxKind.DeleteExpression: + return emitDeleteExpression((node)); + case SyntaxKind.TypeOfExpression: + return emitTypeOfExpression((node)); + case SyntaxKind.VoidExpression: + return emitVoidExpression((node)); + case SyntaxKind.AwaitExpression: + return emitAwaitExpression((node)); + case SyntaxKind.PrefixUnaryExpression: + return emitPrefixUnaryExpression((node)); + case SyntaxKind.PostfixUnaryExpression: + return emitPostfixUnaryExpression((node)); + case SyntaxKind.BinaryExpression: + return emitBinaryExpression((node)); + case SyntaxKind.ConditionalExpression: + return emitConditionalExpression((node)); + case SyntaxKind.TemplateExpression: + return emitTemplateExpression((node)); + case SyntaxKind.YieldExpression: + return emitYieldExpression((node)); + case SyntaxKind.SpreadElement: + return emitSpreadExpression((node)); + case SyntaxKind.ClassExpression: + return emitClassExpression((node)); + case SyntaxKind.OmittedExpression: + return; + case SyntaxKind.AsExpression: + return emitAsExpression((node)); + case SyntaxKind.NonNullExpression: + return emitNonNullExpression((node)); + case SyntaxKind.MetaProperty: + return emitMetaProperty((node)); + // JSX + case SyntaxKind.JsxElement: + return emitJsxElement((node)); + case SyntaxKind.JsxSelfClosingElement: + return emitJsxSelfClosingElement((node)); + case SyntaxKind.JsxFragment: + return emitJsxFragment((node)); + // Transformation nodes + case SyntaxKind.PartiallyEmittedExpression: + return emitPartiallyEmittedExpression((node)); + case SyntaxKind.CommaListExpression: + return emitCommaList((node)); } } - - function emitComputedPropertyName(node: ComputedPropertyName) { - writePunctuation("["); - emitExpression(node.expression); - writePunctuation("]"); + } + function emitMappedTypeParameter(node: TypeParameterDeclaration): void { + emit(node.name); + writeSpace(); + writeKeyword("in"); + writeSpace(); + emit(node.constraint); + } + function pipelineEmitWithSubstitution(hint: EmitHint, node: Node) { + Debug.assert(lastNode === node || lastSubstitution === node); + const pipelinePhase = getNextPipelinePhase(PipelinePhase.Substitution, hint, node); + pipelinePhase(hint, lastSubstitution!); + Debug.assert(lastNode === node || lastSubstitution === node); + } + function getHelpersFromBundledSourceFiles(bundle: Bundle): string[] | undefined { + let result: string[] | undefined; + if (moduleKind === ModuleKind.None || printerOptions.noEmitHelpers) { + return undefined; } - - // - // Signature elements - // - - function emitTypeParameter(node: TypeParameterDeclaration) { - emit(node.name); - if (node.constraint) { - writeSpace(); - writeKeyword("extends"); - writeSpace(); - emit(node.constraint); - } - if (node.default) { - writeSpace(); - writeOperator("="); - writeSpace(); - emit(node.default); + const bundledHelpers = createMap(); + for (const sourceFile of bundle.sourceFiles) { + const shouldSkip = getExternalHelpersModuleName(sourceFile) !== undefined; + const helpers = getSortedEmitHelpers(sourceFile); + if (!helpers) + continue; + for (const helper of helpers) { + if (!helper.scoped && !shouldSkip && !bundledHelpers.get(helper.name)) { + bundledHelpers.set(helper.name, true); + (result || (result = [])).push(helper.name); + } } } - - function emitParameter(node: ParameterDeclaration) { - emitDecorators(node, node.decorators); - emitModifiers(node, node.modifiers); - emit(node.dotDotDotToken); - emitNodeWithWriter(node.name, writeParameter); - emit(node.questionToken); - if (node.parent && node.parent.kind === SyntaxKind.JSDocFunctionType && !node.name) { - emit(node.type); - } - else { - emitTypeAnnotation(node.type); + return result; + } + function emitHelpers(node: Node) { + let helpersEmitted = false; + const bundle = node.kind === SyntaxKind.Bundle ? node : undefined; + if (bundle && moduleKind === ModuleKind.None) { + return; + } + const numPrepends = bundle ? bundle.prepends.length : 0; + const numNodes = bundle ? bundle.sourceFiles.length + numPrepends : 1; + for (let i = 0; i < numNodes; i++) { + const currentNode = bundle ? i < numPrepends ? bundle.prepends[i] : bundle.sourceFiles[i - numPrepends] : node; + const sourceFile = isSourceFile(currentNode) ? currentNode : isUnparsedSource(currentNode) ? undefined : currentSourceFile!; + const shouldSkip = printerOptions.noEmitHelpers || (!!sourceFile && hasRecordedExternalHelpers(sourceFile)); + const shouldBundle = (isSourceFile(currentNode) || isUnparsedSource(currentNode)) && !isOwnFileEmit; + const helpers = isUnparsedSource(currentNode) ? currentNode.helpers : getSortedEmitHelpers(currentNode); + if (helpers) { + for (const helper of helpers) { + if (!helper.scoped) { + // Skip the helper if it can be skipped and the noEmitHelpers compiler + // option is set, or if it can be imported and the importHelpers compiler + // option is set. + if (shouldSkip) + continue; + // Skip the helper if it can be bundled but hasn't already been emitted and we + // are emitting a bundled module. + if (shouldBundle) { + if (bundledHelpers.get(helper.name)) { + continue; + } + bundledHelpers.set(helper.name, true); + } + } + else if (bundle) { + // Skip the helper if it is scoped and we are emitting bundled helpers + continue; + } + const pos = getTextPosWithWriteLine(); + if (typeof helper.text === "string") { + writeLines(helper.text); + } + else { + writeLines(helper.text(makeFileLevelOptimisticUniqueName)); + } + if (bundleFileInfo) + bundleFileInfo.sections.push({ pos, end: writer.getTextPos(), kind: BundleFileSectionKind.EmitHelpers, data: helper.name }); + helpersEmitted = true; + } } - // The comment position has to fallback to any present node within the parameterdeclaration because as it turns out, the parser can make parameter declarations with _just_ an initializer. - emitInitializer(node.initializer, node.type ? node.type.end : node.questionToken ? node.questionToken.end : node.name ? node.name.end : node.modifiers ? node.modifiers.end : node.decorators ? node.decorators.end : node.pos, node); } - - function emitDecorator(decorator: Decorator) { - writePunctuation("@"); - emitExpression(decorator.expression); - } - - // - // Type members - // - - function emitPropertySignature(node: PropertySignature) { - emitDecorators(node, node.decorators); - emitModifiers(node, node.modifiers); - emitNodeWithWriter(node.name, writeProperty); - emit(node.questionToken); - emitTypeAnnotation(node.type); - writeTrailingSemicolon(); + return helpersEmitted; + } + function getSortedEmitHelpers(node: Node) { + const helpers = getEmitHelpers(node); + return helpers && stableSort(helpers, compareEmitHelpers); + } + // + // Literals/Pseudo-literals + // + // SyntaxKind.NumericLiteral + // SyntaxKind.BigIntLiteral + function emitNumericOrBigIntLiteral(node: NumericLiteral | BigIntLiteral) { + emitLiteral(node, /*jsxAttributeEscape*/ false); + } + // SyntaxKind.StringLiteral + // SyntaxKind.RegularExpressionLiteral + // SyntaxKind.NoSubstitutionTemplateLiteral + // SyntaxKind.TemplateHead + // SyntaxKind.TemplateMiddle + // SyntaxKind.TemplateTail + function emitLiteral(node: LiteralLikeNode, jsxAttributeEscape: boolean) { + const text = getLiteralTextOfNode(node, printerOptions.neverAsciiEscape, jsxAttributeEscape); + if ((printerOptions.sourceMap || printerOptions.inlineSourceMap) + && (node.kind === SyntaxKind.StringLiteral || isTemplateLiteralKind(node.kind))) { + writeLiteral(text); } - - function emitPropertyDeclaration(node: PropertyDeclaration) { - emitDecorators(node, node.decorators); - emitModifiers(node, node.modifiers); - emit(node.name); - emit(node.questionToken); - emit(node.exclamationToken); - emitTypeAnnotation(node.type); - emitInitializer(node.initializer, node.type ? node.type.end : node.questionToken ? node.questionToken.end : node.name.end, node); - writeTrailingSemicolon(); + else { + // Quick info expects all literals to be called with writeStringLiteral, as there's no specific type for numberLiterals + writeStringLiteral(text); } - - function emitMethodSignature(node: MethodSignature) { - pushNameGenerationScope(node); - emitDecorators(node, node.decorators); - emitModifiers(node, node.modifiers); - emit(node.name); - emit(node.questionToken); - emitTypeParameters(node, node.typeParameters); - emitParameters(node, node.parameters); - emitTypeAnnotation(node.type); - writeTrailingSemicolon(); - popNameGenerationScope(node); - } - - function emitMethodDeclaration(node: MethodDeclaration) { - emitDecorators(node, node.decorators); - emitModifiers(node, node.modifiers); - emit(node.asteriskToken); - emit(node.name); - emit(node.questionToken); - emitSignatureAndBody(node, emitSignatureHead); - } - - function emitConstructor(node: ConstructorDeclaration) { - emitModifiers(node, node.modifiers); - writeKeyword("constructor"); - emitSignatureAndBody(node, emitSignatureHead); - } - - function emitAccessorDeclaration(node: AccessorDeclaration) { - emitDecorators(node, node.decorators); - emitModifiers(node, node.modifiers); - writeKeyword(node.kind === SyntaxKind.GetAccessor ? "get" : "set"); - writeSpace(); - emit(node.name); - emitSignatureAndBody(node, emitSignatureHead); - } - - function emitCallSignature(node: CallSignatureDeclaration) { - pushNameGenerationScope(node); - emitDecorators(node, node.decorators); - emitModifiers(node, node.modifiers); - emitTypeParameters(node, node.typeParameters); - emitParameters(node, node.parameters); - emitTypeAnnotation(node.type); - writeTrailingSemicolon(); - popNameGenerationScope(node); - } - - function emitConstructSignature(node: ConstructSignatureDeclaration) { - pushNameGenerationScope(node); - emitDecorators(node, node.decorators); - emitModifiers(node, node.modifiers); - writeKeyword("new"); - writeSpace(); - emitTypeParameters(node, node.typeParameters); - emitParameters(node, node.parameters); - emitTypeAnnotation(node.type); - writeTrailingSemicolon(); - popNameGenerationScope(node); + } + // SyntaxKind.UnparsedSource + // SyntaxKind.UnparsedPrepend + function emitUnparsedSourceOrPrepend(unparsed: UnparsedSource | UnparsedPrepend) { + for (const text of unparsed.texts) { + writeLine(); + emit(text); } - - function emitIndexSignature(node: IndexSignatureDeclaration) { - emitDecorators(node, node.decorators); - emitModifiers(node, node.modifiers); - emitParametersForIndexSignature(node, node.parameters); - emitTypeAnnotation(node.type); - writeTrailingSemicolon(); + } + // SyntaxKind.UnparsedPrologue + // SyntaxKind.UnparsedText + // SyntaxKind.UnparsedInternal + // SyntaxKind.UnparsedSyntheticReference + function writeUnparsedNode(unparsed: UnparsedNode) { + writer.rawWrite(unparsed.parent.text.substring(unparsed.pos, unparsed.end)); + } + // SyntaxKind.UnparsedText + // SyntaxKind.UnparsedInternal + function emitUnparsedTextLike(unparsed: UnparsedTextLike) { + const pos = getTextPosWithWriteLine(); + writeUnparsedNode(unparsed); + if (bundleFileInfo) { + updateOrPushBundleFileTextLike(pos, writer.getTextPos(), unparsed.kind === SyntaxKind.UnparsedText ? + BundleFileSectionKind.Text : + BundleFileSectionKind.Internal); } - - function emitSemicolonClassElement() { - writeTrailingSemicolon(); + } + // SyntaxKind.UnparsedSyntheticReference + function emitUnparsedSyntheticReference(unparsed: UnparsedSyntheticReference) { + const pos = getTextPosWithWriteLine(); + writeUnparsedNode(unparsed); + if (bundleFileInfo) { + const section = clone(unparsed.section); + section.pos = pos; + section.end = writer.getTextPos(); + bundleFileInfo.sections.push(section); } - - // - // Types - // - - function emitTypePredicate(node: TypePredicateNode) { - if (node.assertsModifier) { - emit(node.assertsModifier); - writeSpace(); - } - emit(node.parameterName); - if (node.type) { - writeSpace(); - writeKeyword("is"); - writeSpace(); - emit(node.type); - } + } + // + // Identifiers + // + function emitIdentifier(node: Identifier) { + const writeText = node.symbol ? writeSymbol : write; + writeText(getTextOfNode(node, /*includeTrivia*/ false), node.symbol); + emitList(node, node.typeArguments, ListFormat.TypeParameters); // Call emitList directly since it could be an array of TypeParameterDeclarations _or_ type arguments + } + // + // Names + // + function emitPrivateIdentifier(node: PrivateIdentifier) { + const writeText = node.symbol ? writeSymbol : write; + writeText(getTextOfNode(node, /*includeTrivia*/ false), node.symbol); + } + function emitQualifiedName(node: QualifiedName) { + emitEntityName(node.left); + writePunctuation("."); + emit(node.right); + } + function emitEntityName(node: EntityName) { + if (node.kind === SyntaxKind.Identifier) { + emitExpression(node); } - - function emitTypeReference(node: TypeReferenceNode) { - emit(node.typeName); - emitTypeArguments(node, node.typeArguments); + else { + emit(node); } - - function emitFunctionType(node: FunctionTypeNode) { - pushNameGenerationScope(node); - emitTypeParameters(node, node.typeParameters); - emitParametersForArrow(node, node.parameters); + } + function emitComputedPropertyName(node: ComputedPropertyName) { + writePunctuation("["); + emitExpression(node.expression); + writePunctuation("]"); + } + // + // Signature elements + // + function emitTypeParameter(node: TypeParameterDeclaration) { + emit(node.name); + if (node.constraint) { writeSpace(); - writePunctuation("=>"); + writeKeyword("extends"); writeSpace(); - emit(node.type); - popNameGenerationScope(node); - } - - function emitJSDocFunctionType(node: JSDocFunctionType) { - writeKeyword("function"); - emitParameters(node, node.parameters); - writePunctuation(":"); - emit(node.type); + emit(node.constraint); } - - - function emitJSDocNullableType(node: JSDocNullableType) { - writePunctuation("?"); - emit(node.type); + if (node.default) { + writeSpace(); + writeOperator("="); + writeSpace(); + emit(node.default); } - - function emitJSDocNonNullableType(node: JSDocNonNullableType) { - writePunctuation("!"); + } + function emitParameter(node: ParameterDeclaration) { + emitDecorators(node, node.decorators); + emitModifiers(node, node.modifiers); + emit(node.dotDotDotToken); + emitNodeWithWriter(node.name, writeParameter); + emit(node.questionToken); + if (node.parent && node.parent.kind === SyntaxKind.JSDocFunctionType && !node.name) { emit(node.type); } - - function emitJSDocOptionalType(node: JSDocOptionalType) { - emit(node.type); - writePunctuation("="); + else { + emitTypeAnnotation(node.type); } - - function emitConstructorType(node: ConstructorTypeNode) { - pushNameGenerationScope(node); - writeKeyword("new"); + // The comment position has to fallback to any present node within the parameterdeclaration because as it turns out, the parser can make parameter declarations with _just_ an initializer. + emitInitializer(node.initializer, node.type ? node.type.end : node.questionToken ? node.questionToken.end : node.name ? node.name.end : node.modifiers ? node.modifiers.end : node.decorators ? node.decorators.end : node.pos, node); + } + function emitDecorator(decorator: Decorator) { + writePunctuation("@"); + emitExpression(decorator.expression); + } + // + // Type members + // + function emitPropertySignature(node: PropertySignature) { + emitDecorators(node, node.decorators); + emitModifiers(node, node.modifiers); + emitNodeWithWriter(node.name, writeProperty); + emit(node.questionToken); + emitTypeAnnotation(node.type); + writeTrailingSemicolon(); + } + function emitPropertyDeclaration(node: PropertyDeclaration) { + emitDecorators(node, node.decorators); + emitModifiers(node, node.modifiers); + emit(node.name); + emit(node.questionToken); + emit(node.exclamationToken); + emitTypeAnnotation(node.type); + emitInitializer(node.initializer, node.type ? node.type.end : node.questionToken ? node.questionToken.end : node.name.end, node); + writeTrailingSemicolon(); + } + function emitMethodSignature(node: MethodSignature) { + pushNameGenerationScope(node); + emitDecorators(node, node.decorators); + emitModifiers(node, node.modifiers); + emit(node.name); + emit(node.questionToken); + emitTypeParameters(node, node.typeParameters); + emitParameters(node, node.parameters); + emitTypeAnnotation(node.type); + writeTrailingSemicolon(); + popNameGenerationScope(node); + } + function emitMethodDeclaration(node: MethodDeclaration) { + emitDecorators(node, node.decorators); + emitModifiers(node, node.modifiers); + emit(node.asteriskToken); + emit(node.name); + emit(node.questionToken); + emitSignatureAndBody(node, emitSignatureHead); + } + function emitConstructor(node: ConstructorDeclaration) { + emitModifiers(node, node.modifiers); + writeKeyword("constructor"); + emitSignatureAndBody(node, emitSignatureHead); + } + function emitAccessorDeclaration(node: AccessorDeclaration) { + emitDecorators(node, node.decorators); + emitModifiers(node, node.modifiers); + writeKeyword(node.kind === SyntaxKind.GetAccessor ? "get" : "set"); + writeSpace(); + emit(node.name); + emitSignatureAndBody(node, emitSignatureHead); + } + function emitCallSignature(node: CallSignatureDeclaration) { + pushNameGenerationScope(node); + emitDecorators(node, node.decorators); + emitModifiers(node, node.modifiers); + emitTypeParameters(node, node.typeParameters); + emitParameters(node, node.parameters); + emitTypeAnnotation(node.type); + writeTrailingSemicolon(); + popNameGenerationScope(node); + } + function emitConstructSignature(node: ConstructSignatureDeclaration) { + pushNameGenerationScope(node); + emitDecorators(node, node.decorators); + emitModifiers(node, node.modifiers); + writeKeyword("new"); + writeSpace(); + emitTypeParameters(node, node.typeParameters); + emitParameters(node, node.parameters); + emitTypeAnnotation(node.type); + writeTrailingSemicolon(); + popNameGenerationScope(node); + } + function emitIndexSignature(node: IndexSignatureDeclaration) { + emitDecorators(node, node.decorators); + emitModifiers(node, node.modifiers); + emitParametersForIndexSignature(node, node.parameters); + emitTypeAnnotation(node.type); + writeTrailingSemicolon(); + } + function emitSemicolonClassElement() { + writeTrailingSemicolon(); + } + // + // Types + // + function emitTypePredicate(node: TypePredicateNode) { + if (node.assertsModifier) { + emit(node.assertsModifier); writeSpace(); - emitTypeParameters(node, node.typeParameters); - emitParameters(node, node.parameters); + } + emit(node.parameterName); + if (node.type) { writeSpace(); - writePunctuation("=>"); + writeKeyword("is"); writeSpace(); emit(node.type); - popNameGenerationScope(node); } - - function emitTypeQuery(node: TypeQueryNode) { - writeKeyword("typeof"); + } + function emitTypeReference(node: TypeReferenceNode) { + emit(node.typeName); + emitTypeArguments(node, node.typeArguments); + } + function emitFunctionType(node: FunctionTypeNode) { + pushNameGenerationScope(node); + emitTypeParameters(node, node.typeParameters); + emitParametersForArrow(node, node.parameters); + writeSpace(); + writePunctuation("=>"); + writeSpace(); + emit(node.type); + popNameGenerationScope(node); + } + function emitJSDocFunctionType(node: JSDocFunctionType) { + writeKeyword("function"); + emitParameters(node, node.parameters); + writePunctuation(":"); + emit(node.type); + } + function emitJSDocNullableType(node: JSDocNullableType) { + writePunctuation("?"); + emit(node.type); + } + function emitJSDocNonNullableType(node: JSDocNonNullableType) { + writePunctuation("!"); + emit(node.type); + } + function emitJSDocOptionalType(node: JSDocOptionalType) { + emit(node.type); + writePunctuation("="); + } + function emitConstructorType(node: ConstructorTypeNode) { + pushNameGenerationScope(node); + writeKeyword("new"); + writeSpace(); + emitTypeParameters(node, node.typeParameters); + emitParameters(node, node.parameters); + writeSpace(); + writePunctuation("=>"); + writeSpace(); + emit(node.type); + popNameGenerationScope(node); + } + function emitTypeQuery(node: TypeQueryNode) { + writeKeyword("typeof"); + writeSpace(); + emit(node.exprName); + } + function emitTypeLiteral(node: TypeLiteralNode) { + writePunctuation("{"); + const flags = getEmitFlags(node) & EmitFlags.SingleLine ? ListFormat.SingleLineTypeLiteralMembers : ListFormat.MultiLineTypeLiteralMembers; + emitList(node, node.members, flags | ListFormat.NoSpaceIfEmpty); + writePunctuation("}"); + } + function emitArrayType(node: ArrayTypeNode) { + emit(node.elementType); + writePunctuation("["); + writePunctuation("]"); + } + function emitRestOrJSDocVariadicType(node: RestTypeNode | JSDocVariadicType) { + writePunctuation("..."); + emit(node.type); + } + function emitTupleType(node: TupleTypeNode) { + writePunctuation("["); + emitList(node, node.elementTypes, ListFormat.TupleTypeElements); + writePunctuation("]"); + } + function emitOptionalType(node: OptionalTypeNode) { + emit(node.type); + writePunctuation("?"); + } + function emitUnionType(node: UnionTypeNode) { + emitList(node, node.types, ListFormat.UnionTypeConstituents); + } + function emitIntersectionType(node: IntersectionTypeNode) { + emitList(node, node.types, ListFormat.IntersectionTypeConstituents); + } + function emitConditionalType(node: ConditionalTypeNode) { + emit(node.checkType); + writeSpace(); + writeKeyword("extends"); + writeSpace(); + emit(node.extendsType); + writeSpace(); + writePunctuation("?"); + writeSpace(); + emit(node.trueType); + writeSpace(); + writePunctuation(":"); + writeSpace(); + emit(node.falseType); + } + function emitInferType(node: InferTypeNode) { + writeKeyword("infer"); + writeSpace(); + emit(node.typeParameter); + } + function emitParenthesizedType(node: ParenthesizedTypeNode) { + writePunctuation("("); + emit(node.type); + writePunctuation(")"); + } + function emitThisType() { + writeKeyword("this"); + } + function emitTypeOperator(node: TypeOperatorNode) { + writeTokenText(node.operator, writeKeyword); + writeSpace(); + emit(node.type); + } + function emitIndexedAccessType(node: IndexedAccessTypeNode) { + emit(node.objectType); + writePunctuation("["); + emit(node.indexType); + writePunctuation("]"); + } + function emitMappedType(node: MappedTypeNode) { + const emitFlags = getEmitFlags(node); + writePunctuation("{"); + if (emitFlags & EmitFlags.SingleLine) { writeSpace(); - emit(node.exprName); - } - - function emitTypeLiteral(node: TypeLiteralNode) { - writePunctuation("{"); - const flags = getEmitFlags(node) & EmitFlags.SingleLine ? ListFormat.SingleLineTypeLiteralMembers : ListFormat.MultiLineTypeLiteralMembers; - emitList(node, node.members, flags | ListFormat.NoSpaceIfEmpty); - writePunctuation("}"); - } - - function emitArrayType(node: ArrayTypeNode) { - emit(node.elementType); - writePunctuation("["); - writePunctuation("]"); - } - - function emitRestOrJSDocVariadicType(node: RestTypeNode | JSDocVariadicType) { - writePunctuation("..."); - emit(node.type); } - - function emitTupleType(node: TupleTypeNode) { - writePunctuation("["); - emitList(node, node.elementTypes, ListFormat.TupleTypeElements); - writePunctuation("]"); - } - - function emitOptionalType(node: OptionalTypeNode) { - emit(node.type); - writePunctuation("?"); - } - - function emitUnionType(node: UnionTypeNode) { - emitList(node, node.types, ListFormat.UnionTypeConstituents); - } - - function emitIntersectionType(node: IntersectionTypeNode) { - emitList(node, node.types, ListFormat.IntersectionTypeConstituents); + else { + writeLine(); + increaseIndent(); } - - function emitConditionalType(node: ConditionalTypeNode) { - emit(node.checkType); - writeSpace(); - writeKeyword("extends"); - writeSpace(); - emit(node.extendsType); - writeSpace(); - writePunctuation("?"); - writeSpace(); - emit(node.trueType); - writeSpace(); - writePunctuation(":"); + if (node.readonlyToken) { + emit(node.readonlyToken); + if (node.readonlyToken.kind !== SyntaxKind.ReadonlyKeyword) { + writeKeyword("readonly"); + } writeSpace(); - emit(node.falseType); } - - function emitInferType(node: InferTypeNode) { - writeKeyword("infer"); - writeSpace(); - emit(node.typeParameter); + writePunctuation("["); + pipelineEmit(EmitHint.MappedTypeParameter, node.typeParameter); + writePunctuation("]"); + if (node.questionToken) { + emit(node.questionToken); + if (node.questionToken.kind !== SyntaxKind.QuestionToken) { + writePunctuation("?"); + } } - - function emitParenthesizedType(node: ParenthesizedTypeNode) { - writePunctuation("("); - emit(node.type); - writePunctuation(")"); + writePunctuation(":"); + writeSpace(); + emit(node.type); + writeTrailingSemicolon(); + if (emitFlags & EmitFlags.SingleLine) { + writeSpace(); } - - function emitThisType() { - writeKeyword("this"); + else { + writeLine(); + decreaseIndent(); } - - function emitTypeOperator(node: TypeOperatorNode) { - writeTokenText(node.operator, writeKeyword); + writePunctuation("}"); + } + function emitLiteralType(node: LiteralTypeNode) { + emitExpression(node.literal); + } + function emitImportTypeNode(node: ImportTypeNode) { + if (node.isTypeOf) { + writeKeyword("typeof"); writeSpace(); - emit(node.type); } - - function emitIndexedAccessType(node: IndexedAccessTypeNode) { - emit(node.objectType); - writePunctuation("["); - emit(node.indexType); - writePunctuation("]"); + writeKeyword("import"); + writePunctuation("("); + emit(node.argument); + writePunctuation(")"); + if (node.qualifier) { + writePunctuation("."); + emit(node.qualifier); } - - function emitMappedType(node: MappedTypeNode) { - const emitFlags = getEmitFlags(node); - writePunctuation("{"); - if (emitFlags & EmitFlags.SingleLine) { - writeSpace(); - } - else { - writeLine(); - increaseIndent(); - } - if (node.readonlyToken) { - emit(node.readonlyToken); - if (node.readonlyToken.kind !== SyntaxKind.ReadonlyKeyword) { - writeKeyword("readonly"); - } - writeSpace(); - } - writePunctuation("["); - - pipelineEmit(EmitHint.MappedTypeParameter, node.typeParameter); - - writePunctuation("]"); - if (node.questionToken) { - emit(node.questionToken); - if (node.questionToken.kind !== SyntaxKind.QuestionToken) { - writePunctuation("?"); - } - } + emitTypeArguments(node, node.typeArguments); + } + // + // Binding patterns + // + function emitObjectBindingPattern(node: ObjectBindingPattern) { + writePunctuation("{"); + emitList(node, node.elements, ListFormat.ObjectBindingPatternElements); + writePunctuation("}"); + } + function emitArrayBindingPattern(node: ArrayBindingPattern) { + writePunctuation("["); + emitList(node, node.elements, ListFormat.ArrayBindingPatternElements); + writePunctuation("]"); + } + function emitBindingElement(node: BindingElement) { + emit(node.dotDotDotToken); + if (node.propertyName) { + emit(node.propertyName); writePunctuation(":"); writeSpace(); - emit(node.type); - writeTrailingSemicolon(); - if (emitFlags & EmitFlags.SingleLine) { - writeSpace(); - } - else { - writeLine(); - decreaseIndent(); - } - writePunctuation("}"); } - - function emitLiteralType(node: LiteralTypeNode) { - emitExpression(node.literal); + emit(node.name); + emitInitializer(node.initializer, node.name.end, node); + } + // + // Expressions + // + function emitArrayLiteralExpression(node: ArrayLiteralExpression) { + const elements = node.elements; + const preferNewLine = node.multiLine ? ListFormat.PreferNewLine : ListFormat.None; + emitExpressionList(node, elements, ListFormat.ArrayLiteralExpressionElements | preferNewLine); + } + function emitObjectLiteralExpression(node: ObjectLiteralExpression) { + forEach(node.properties, generateMemberNames); + const indentedFlag = getEmitFlags(node) & EmitFlags.Indented; + if (indentedFlag) { + increaseIndent(); } - - function emitImportTypeNode(node: ImportTypeNode) { - if (node.isTypeOf) { - writeKeyword("typeof"); - writeSpace(); - } - writeKeyword("import"); - writePunctuation("("); - emit(node.argument); - writePunctuation(")"); - if (node.qualifier) { - writePunctuation("."); - emit(node.qualifier); - } - emitTypeArguments(node, node.typeArguments); + const preferNewLine = node.multiLine ? ListFormat.PreferNewLine : ListFormat.None; + const allowTrailingComma = currentSourceFile!.languageVersion >= ScriptTarget.ES5 && !isJsonSourceFile((currentSourceFile!)) ? ListFormat.AllowTrailingComma : ListFormat.None; + emitList(node, node.properties, ListFormat.ObjectLiteralExpressionProperties | allowTrailingComma | preferNewLine); + if (indentedFlag) { + decreaseIndent(); } - - // - // Binding patterns - // - - function emitObjectBindingPattern(node: ObjectBindingPattern) { - writePunctuation("{"); - emitList(node, node.elements, ListFormat.ObjectBindingPatternElements); - writePunctuation("}"); + } + function emitPropertyAccessExpression(node: PropertyAccessExpression) { + const expression = cast(emitExpression(node.expression), isExpression); + const token = getDotOrQuestionDotToken(node); + const indentBeforeDot = needsIndentation(node, node.expression, token); + const indentAfterDot = needsIndentation(node, token, node.name); + increaseIndentIf(indentBeforeDot, /*writeSpaceIfNotIndenting*/ false); + const shouldEmitDotDot = token.kind !== SyntaxKind.QuestionDotToken && + mayNeedDotDotForPropertyAccess(expression) && + !writer.hasTrailingComment() && + !writer.hasTrailingWhitespace(); + if (shouldEmitDotDot) { + writePunctuation("."); } - - function emitArrayBindingPattern(node: ArrayBindingPattern) { - writePunctuation("["); - emitList(node, node.elements, ListFormat.ArrayBindingPatternElements); - writePunctuation("]"); + emitTokenWithComment(token.kind, node.expression.end, writePunctuation, node); + increaseIndentIf(indentAfterDot, /*writeSpaceIfNotIndenting*/ false); + emit(node.name); + decreaseIndentIf(indentBeforeDot, indentAfterDot); + } + // 1..toString is a valid property access, emit a dot after the literal + // Also emit a dot if expression is a integer const enum value - it will appear in generated code as numeric literal + function mayNeedDotDotForPropertyAccess(expression: Expression) { + expression = skipPartiallyEmittedExpressions(expression); + if (isNumericLiteral(expression)) { + // check if numeric literal is a decimal literal that was originally written with a dot + const text = getLiteralTextOfNode((expression), /*neverAsciiEscape*/ true, /*jsxAttributeEscape*/ false); + // If he number will be printed verbatim and it doesn't already contain a dot, add one + // if the expression doesn't have any comments that will be emitted. + return !expression.numericLiteralFlags && !stringContains(text, (tokenToString(SyntaxKind.DotToken)!)); + } + else if (isAccessExpression(expression)) { + // check if constant enum value is integer + const constantValue = getConstantValue(expression); + // isFinite handles cases when constantValue is undefined + return typeof constantValue === "number" && isFinite(constantValue) + && Math.floor(constantValue) === constantValue; } - - function emitBindingElement(node: BindingElement) { - emit(node.dotDotDotToken); - if (node.propertyName) { - emit(node.propertyName); - writePunctuation(":"); - writeSpace(); - } - emit(node.name); - emitInitializer(node.initializer, node.name.end, node); + } + function emitElementAccessExpression(node: ElementAccessExpression) { + emitExpression(node.expression); + emit(node.questionDotToken); + emitTokenWithComment(SyntaxKind.OpenBracketToken, node.expression.end, writePunctuation, node); + emitExpression(node.argumentExpression); + emitTokenWithComment(SyntaxKind.CloseBracketToken, node.argumentExpression.end, writePunctuation, node); + } + function emitCallExpression(node: CallExpression) { + emitExpression(node.expression); + emit(node.questionDotToken); + emitTypeArguments(node, node.typeArguments); + emitExpressionList(node, node.arguments, ListFormat.CallExpressionArguments); + } + function emitNewExpression(node: NewExpression) { + emitTokenWithComment(SyntaxKind.NewKeyword, node.pos, writeKeyword, node); + writeSpace(); + emitExpression(node.expression); + emitTypeArguments(node, node.typeArguments); + emitExpressionList(node, node.arguments, ListFormat.NewExpressionArguments); + } + function emitTaggedTemplateExpression(node: TaggedTemplateExpression) { + emitExpression(node.tag); + emitTypeArguments(node, node.typeArguments); + writeSpace(); + emitExpression(node.template); + } + function emitTypeAssertionExpression(node: TypeAssertion) { + writePunctuation("<"); + emit(node.type); + writePunctuation(">"); + emitExpression(node.expression); + } + function emitParenthesizedExpression(node: ParenthesizedExpression) { + const openParenPos = emitTokenWithComment(SyntaxKind.OpenParenToken, node.pos, writePunctuation, node); + emitExpression(node.expression); + emitTokenWithComment(SyntaxKind.CloseParenToken, node.expression ? node.expression.end : openParenPos, writePunctuation, node); + } + function emitFunctionExpression(node: FunctionExpression) { + generateNameIfNeeded(node.name); + emitFunctionDeclarationOrExpression(node); + } + function emitArrowFunction(node: ArrowFunction) { + emitDecorators(node, node.decorators); + emitModifiers(node, node.modifiers); + emitSignatureAndBody(node, emitArrowFunctionHead); + } + function emitArrowFunctionHead(node: ArrowFunction) { + emitTypeParameters(node, node.typeParameters); + emitParametersForArrow(node, node.parameters); + emitTypeAnnotation(node.type); + writeSpace(); + emit(node.equalsGreaterThanToken); + } + function emitDeleteExpression(node: DeleteExpression) { + emitTokenWithComment(SyntaxKind.DeleteKeyword, node.pos, writeKeyword, node); + writeSpace(); + emitExpression(node.expression); + } + function emitTypeOfExpression(node: TypeOfExpression) { + emitTokenWithComment(SyntaxKind.TypeOfKeyword, node.pos, writeKeyword, node); + writeSpace(); + emitExpression(node.expression); + } + function emitVoidExpression(node: VoidExpression) { + emitTokenWithComment(SyntaxKind.VoidKeyword, node.pos, writeKeyword, node); + writeSpace(); + emitExpression(node.expression); + } + function emitAwaitExpression(node: AwaitExpression) { + emitTokenWithComment(SyntaxKind.AwaitKeyword, node.pos, writeKeyword, node); + writeSpace(); + emitExpression(node.expression); + } + function emitPrefixUnaryExpression(node: PrefixUnaryExpression) { + writeTokenText(node.operator, writeOperator); + if (shouldEmitWhitespaceBeforeOperand(node)) { + writeSpace(); } - + emitExpression(node.operand); + } + function shouldEmitWhitespaceBeforeOperand(node: PrefixUnaryExpression) { + // In some cases, we need to emit a space between the operator and the operand. One obvious case + // is when the operator is an identifier, like delete or typeof. We also need to do this for plus + // and minus expressions in certain cases. Specifically, consider the following two cases (parens + // are just for clarity of exposition, and not part of the source code): // - // Expressions + // (+(+1)) + // (+(++1)) // - - function emitArrayLiteralExpression(node: ArrayLiteralExpression) { - const elements = node.elements; - const preferNewLine = node.multiLine ? ListFormat.PreferNewLine : ListFormat.None; - emitExpressionList(node, elements, ListFormat.ArrayLiteralExpressionElements | preferNewLine); - } - - function emitObjectLiteralExpression(node: ObjectLiteralExpression) { - forEach(node.properties, generateMemberNames); - - const indentedFlag = getEmitFlags(node) & EmitFlags.Indented; - if (indentedFlag) { - increaseIndent(); - } - - const preferNewLine = node.multiLine ? ListFormat.PreferNewLine : ListFormat.None; - const allowTrailingComma = currentSourceFile!.languageVersion >= ScriptTarget.ES5 && !isJsonSourceFile(currentSourceFile!) ? ListFormat.AllowTrailingComma : ListFormat.None; - emitList(node, node.properties, ListFormat.ObjectLiteralExpressionProperties | allowTrailingComma | preferNewLine); - - if (indentedFlag) { - decreaseIndent(); + // We need to emit a space in both cases. In the first case, the absence of a space will make + // the resulting expression a prefix increment operation. And in the second, it will make the resulting + // expression a prefix increment whose operand is a plus expression - (++(+x)) + // The same is true of minus of course. + const operand = node.operand; + return operand.kind === SyntaxKind.PrefixUnaryExpression + && ((node.operator === SyntaxKind.PlusToken && ((operand).operator === SyntaxKind.PlusToken || (operand).operator === SyntaxKind.PlusPlusToken)) + || (node.operator === SyntaxKind.MinusToken && ((operand).operator === SyntaxKind.MinusToken || (operand).operator === SyntaxKind.MinusMinusToken))); + } + function emitPostfixUnaryExpression(node: PostfixUnaryExpression) { + emitExpression(node.operand); + writeTokenText(node.operator, writeOperator); + } + const enum EmitBinaryExpressionState { + EmitLeft, + EmitRight, + FinishEmit + } + /** + * emitBinaryExpression includes an embedded work stack to attempt to handle as many nested binary expressions + * as possible without creating any additional stack frames. This can only be done when the emit pipeline does + * not require notification/substitution/comment/sourcemap decorations. + */ + function emitBinaryExpression(node: BinaryExpression) { + const nodeStack = [node]; + const stateStack = [EmitBinaryExpressionState.EmitLeft]; + let stackIndex = 0; + while (stackIndex >= 0) { + node = nodeStack[stackIndex]; + switch (stateStack[stackIndex]) { + case EmitBinaryExpressionState.EmitLeft: { + maybePipelineEmitExpression(node.left); + break; + } + case EmitBinaryExpressionState.EmitRight: { + const isCommaOperator = node.operatorToken.kind !== SyntaxKind.CommaToken; + const indentBeforeOperator = needsIndentation(node, node.left, node.operatorToken); + const indentAfterOperator = needsIndentation(node, node.operatorToken, node.right); + increaseIndentIf(indentBeforeOperator, isCommaOperator); + emitLeadingCommentsOfPosition(node.operatorToken.pos); + writeTokenNode(node.operatorToken, node.operatorToken.kind === SyntaxKind.InKeyword ? writeKeyword : writeOperator); + emitTrailingCommentsOfPosition(node.operatorToken.end, /*prefixSpace*/ true); // Binary operators should have a space before the comment starts + increaseIndentIf(indentAfterOperator, /*writeSpaceIfNotIndenting*/ true); + maybePipelineEmitExpression(node.right); + break; + } + case EmitBinaryExpressionState.FinishEmit: { + const indentBeforeOperator = needsIndentation(node, node.left, node.operatorToken); + const indentAfterOperator = needsIndentation(node, node.operatorToken, node.right); + decreaseIndentIf(indentBeforeOperator, indentAfterOperator); + stackIndex--; + break; + } + default: return Debug.fail(`Invalid state ${stateStack[stackIndex]} for emitBinaryExpressionWorker`); } } - - function emitPropertyAccessExpression(node: PropertyAccessExpression) { - const expression = cast(emitExpression(node.expression), isExpression); - const token = getDotOrQuestionDotToken(node); - const indentBeforeDot = needsIndentation(node, node.expression, token); - const indentAfterDot = needsIndentation(node, token, node.name); - - increaseIndentIf(indentBeforeDot, /*writeSpaceIfNotIndenting*/ false); - - const shouldEmitDotDot = - token.kind !== SyntaxKind.QuestionDotToken && - mayNeedDotDotForPropertyAccess(expression) && - !writer.hasTrailingComment() && - !writer.hasTrailingWhitespace(); - - if (shouldEmitDotDot) { - writePunctuation("."); - } - - emitTokenWithComment(token.kind, node.expression.end, writePunctuation, node); - increaseIndentIf(indentAfterDot, /*writeSpaceIfNotIndenting*/ false); - emit(node.name); - decreaseIndentIf(indentBeforeDot, indentAfterDot); - } - - // 1..toString is a valid property access, emit a dot after the literal - // Also emit a dot if expression is a integer const enum value - it will appear in generated code as numeric literal - function mayNeedDotDotForPropertyAccess(expression: Expression) { - expression = skipPartiallyEmittedExpressions(expression); - if (isNumericLiteral(expression)) { - // check if numeric literal is a decimal literal that was originally written with a dot - const text = getLiteralTextOfNode(expression, /*neverAsciiEscape*/ true, /*jsxAttributeEscape*/ false); - // If he number will be printed verbatim and it doesn't already contain a dot, add one - // if the expression doesn't have any comments that will be emitted. - return !expression.numericLiteralFlags && !stringContains(text, tokenToString(SyntaxKind.DotToken)!); + function maybePipelineEmitExpression(next: Expression) { + // Advance the state of this unit of work, + stateStack[stackIndex]++; + // Then actually do the work of emitting the node `next` returned by the prior state + // The following section should be identical to `pipelineEmit` save it assumes EmitHint.Expression and offloads + // binary expression handling, where possible, to the contained work queue + // #region trampolinePipelineEmit + const savedLastNode = lastNode; + const savedLastSubstitution = lastSubstitution; + lastNode = next; + lastSubstitution = undefined; + const pipelinePhase = getPipelinePhase(PipelinePhase.Notification, EmitHint.Expression, next); + if (pipelinePhase === pipelineEmitWithHint && isBinaryExpression(next)) { + // If the target pipeline phase is emit directly, and the next node's also a binary expression, + // skip all the intermediate indirection and push the expression directly onto the work stack + stackIndex++; + stateStack[stackIndex] = EmitBinaryExpressionState.EmitLeft; + nodeStack[stackIndex] = next; } - else if (isAccessExpression(expression)) { - // check if constant enum value is integer - const constantValue = getConstantValue(expression); - // isFinite handles cases when constantValue is undefined - return typeof constantValue === "number" && isFinite(constantValue) - && Math.floor(constantValue) === constantValue; + else { + pipelinePhase(EmitHint.Expression, next); } + Debug.assert(lastNode === next); + lastNode = savedLastNode; + lastSubstitution = savedLastSubstitution; + // #endregion trampolinePipelineEmit } - - function emitElementAccessExpression(node: ElementAccessExpression) { - emitExpression(node.expression); - emit(node.questionDotToken); - emitTokenWithComment(SyntaxKind.OpenBracketToken, node.expression.end, writePunctuation, node); - emitExpression(node.argumentExpression); - emitTokenWithComment(SyntaxKind.CloseBracketToken, node.argumentExpression.end, writePunctuation, node); - } - - function emitCallExpression(node: CallExpression) { - emitExpression(node.expression); - emit(node.questionDotToken); - emitTypeArguments(node, node.typeArguments); - emitExpressionList(node, node.arguments, ListFormat.CallExpressionArguments); - } - - function emitNewExpression(node: NewExpression) { - emitTokenWithComment(SyntaxKind.NewKeyword, node.pos, writeKeyword, node); + } + function emitConditionalExpression(node: ConditionalExpression) { + const indentBeforeQuestion = needsIndentation(node, node.condition, node.questionToken); + const indentAfterQuestion = needsIndentation(node, node.questionToken, node.whenTrue); + const indentBeforeColon = needsIndentation(node, node.whenTrue, node.colonToken); + const indentAfterColon = needsIndentation(node, node.colonToken, node.whenFalse); + emitExpression(node.condition); + increaseIndentIf(indentBeforeQuestion, /*writeSpaceIfNotIndenting*/ true); + emit(node.questionToken); + increaseIndentIf(indentAfterQuestion, /*writeSpaceIfNotIndenting*/ true); + emitExpression(node.whenTrue); + decreaseIndentIf(indentBeforeQuestion, indentAfterQuestion); + increaseIndentIf(indentBeforeColon, /*writeSpaceIfNotIndenting*/ true); + emit(node.colonToken); + increaseIndentIf(indentAfterColon, /*writeSpaceIfNotIndenting*/ true); + emitExpression(node.whenFalse); + decreaseIndentIf(indentBeforeColon, indentAfterColon); + } + function emitTemplateExpression(node: TemplateExpression) { + emit(node.head); + emitList(node, node.templateSpans, ListFormat.TemplateExpressionSpans); + } + function emitYieldExpression(node: YieldExpression) { + emitTokenWithComment(SyntaxKind.YieldKeyword, node.pos, writeKeyword, node); + emit(node.asteriskToken); + emitExpressionWithLeadingSpace(node.expression); + } + function emitSpreadExpression(node: SpreadElement) { + emitTokenWithComment(SyntaxKind.DotDotDotToken, node.pos, writePunctuation, node); + emitExpression(node.expression); + } + function emitClassExpression(node: ClassExpression) { + generateNameIfNeeded(node.name); + emitClassDeclarationOrExpression(node); + } + function emitExpressionWithTypeArguments(node: ExpressionWithTypeArguments) { + emitExpression(node.expression); + emitTypeArguments(node, node.typeArguments); + } + function emitAsExpression(node: AsExpression) { + emitExpression(node.expression); + if (node.type) { writeSpace(); - emitExpression(node.expression); - emitTypeArguments(node, node.typeArguments); - emitExpressionList(node, node.arguments, ListFormat.NewExpressionArguments); - } - - function emitTaggedTemplateExpression(node: TaggedTemplateExpression) { - emitExpression(node.tag); - emitTypeArguments(node, node.typeArguments); + writeKeyword("as"); writeSpace(); - emitExpression(node.template); - } - - function emitTypeAssertionExpression(node: TypeAssertion) { - writePunctuation("<"); emit(node.type); - writePunctuation(">"); - emitExpression(node.expression); - } - - function emitParenthesizedExpression(node: ParenthesizedExpression) { - const openParenPos = emitTokenWithComment(SyntaxKind.OpenParenToken, node.pos, writePunctuation, node); - emitExpression(node.expression); - emitTokenWithComment(SyntaxKind.CloseParenToken, node.expression ? node.expression.end : openParenPos, writePunctuation, node); - } - - function emitFunctionExpression(node: FunctionExpression) { - generateNameIfNeeded(node.name); - emitFunctionDeclarationOrExpression(node); - } - - function emitArrowFunction(node: ArrowFunction) { - emitDecorators(node, node.decorators); - emitModifiers(node, node.modifiers); - emitSignatureAndBody(node, emitArrowFunctionHead); - } - - function emitArrowFunctionHead(node: ArrowFunction) { - emitTypeParameters(node, node.typeParameters); - emitParametersForArrow(node, node.parameters); - emitTypeAnnotation(node.type); - writeSpace(); - emit(node.equalsGreaterThanToken); - } - - function emitDeleteExpression(node: DeleteExpression) { - emitTokenWithComment(SyntaxKind.DeleteKeyword, node.pos, writeKeyword, node); - writeSpace(); - emitExpression(node.expression); } - - function emitTypeOfExpression(node: TypeOfExpression) { - emitTokenWithComment(SyntaxKind.TypeOfKeyword, node.pos, writeKeyword, node); - writeSpace(); - emitExpression(node.expression); + } + function emitNonNullExpression(node: NonNullExpression) { + emitExpression(node.expression); + writeOperator("!"); + } + function emitMetaProperty(node: MetaProperty) { + writeToken(node.keywordToken, node.pos, writePunctuation); + writePunctuation("."); + emit(node.name); + } + // + // Misc + // + function emitTemplateSpan(node: TemplateSpan) { + emitExpression(node.expression); + emit(node.literal); + } + // + // Statements + // + function emitBlock(node: Block) { + emitBlockStatements(node, /*forceSingleLine*/ !node.multiLine && isEmptyBlock(node)); + } + function emitBlockStatements(node: BlockLike, forceSingleLine: boolean) { + emitTokenWithComment(SyntaxKind.OpenBraceToken, node.pos, writePunctuation, /*contextNode*/ node); + const format = forceSingleLine || getEmitFlags(node) & EmitFlags.SingleLine ? ListFormat.SingleLineBlockStatements : ListFormat.MultiLineBlockStatements; + emitList(node, node.statements, format); + emitTokenWithComment(SyntaxKind.CloseBraceToken, node.statements.end, writePunctuation, /*contextNode*/ node, /*indentLeading*/ !!(format & ListFormat.MultiLine)); + } + function emitVariableStatement(node: VariableStatement) { + emitModifiers(node, node.modifiers); + emit(node.declarationList); + writeTrailingSemicolon(); + } + function emitEmptyStatement(isEmbeddedStatement: boolean) { + // While most trailing semicolons are possibly insignificant, an embedded "empty" + // statement is significant and cannot be elided by a trailing-semicolon-omitting writer. + if (isEmbeddedStatement) { + writePunctuation(";"); } - - function emitVoidExpression(node: VoidExpression) { - emitTokenWithComment(SyntaxKind.VoidKeyword, node.pos, writeKeyword, node); - writeSpace(); - emitExpression(node.expression); + else { + writeTrailingSemicolon(); } - - function emitAwaitExpression(node: AwaitExpression) { - emitTokenWithComment(SyntaxKind.AwaitKeyword, node.pos, writeKeyword, node); - writeSpace(); - emitExpression(node.expression); + } + function emitExpressionStatement(node: ExpressionStatement) { + emitExpression(node.expression); + // Emit semicolon in non json files + // or if json file that created synthesized expression(eg.define expression statement when --out and amd code generation) + if (!isJsonSourceFile((currentSourceFile!)) || nodeIsSynthesized(node.expression)) { + writeTrailingSemicolon(); } - - function emitPrefixUnaryExpression(node: PrefixUnaryExpression) { - writeTokenText(node.operator, writeOperator); - if (shouldEmitWhitespaceBeforeOperand(node)) { + } + function emitIfStatement(node: IfStatement) { + const openParenPos = emitTokenWithComment(SyntaxKind.IfKeyword, node.pos, writeKeyword, node); + writeSpace(); + emitTokenWithComment(SyntaxKind.OpenParenToken, openParenPos, writePunctuation, node); + emitExpression(node.expression); + emitTokenWithComment(SyntaxKind.CloseParenToken, node.expression.end, writePunctuation, node); + emitEmbeddedStatement(node, node.thenStatement); + if (node.elseStatement) { + writeLineOrSpace(node); + emitTokenWithComment(SyntaxKind.ElseKeyword, node.thenStatement.end, writeKeyword, node); + if (node.elseStatement.kind === SyntaxKind.IfStatement) { writeSpace(); + emit(node.elseStatement); } - emitExpression(node.operand); - } - - function shouldEmitWhitespaceBeforeOperand(node: PrefixUnaryExpression) { - // In some cases, we need to emit a space between the operator and the operand. One obvious case - // is when the operator is an identifier, like delete or typeof. We also need to do this for plus - // and minus expressions in certain cases. Specifically, consider the following two cases (parens - // are just for clarity of exposition, and not part of the source code): - // - // (+(+1)) - // (+(++1)) - // - // We need to emit a space in both cases. In the first case, the absence of a space will make - // the resulting expression a prefix increment operation. And in the second, it will make the resulting - // expression a prefix increment whose operand is a plus expression - (++(+x)) - // The same is true of minus of course. - const operand = node.operand; - return operand.kind === SyntaxKind.PrefixUnaryExpression - && ((node.operator === SyntaxKind.PlusToken && ((operand).operator === SyntaxKind.PlusToken || (operand).operator === SyntaxKind.PlusPlusToken)) - || (node.operator === SyntaxKind.MinusToken && ((operand).operator === SyntaxKind.MinusToken || (operand).operator === SyntaxKind.MinusMinusToken))); - } - - function emitPostfixUnaryExpression(node: PostfixUnaryExpression) { - emitExpression(node.operand); - writeTokenText(node.operator, writeOperator); - } - - const enum EmitBinaryExpressionState { - EmitLeft, - EmitRight, - FinishEmit - } - - /** - * emitBinaryExpression includes an embedded work stack to attempt to handle as many nested binary expressions - * as possible without creating any additional stack frames. This can only be done when the emit pipeline does - * not require notification/substitution/comment/sourcemap decorations. - */ - function emitBinaryExpression(node: BinaryExpression) { - const nodeStack = [node]; - const stateStack = [EmitBinaryExpressionState.EmitLeft]; - let stackIndex = 0; - while (stackIndex >= 0) { - node = nodeStack[stackIndex]; - switch (stateStack[stackIndex]) { - case EmitBinaryExpressionState.EmitLeft: { - maybePipelineEmitExpression(node.left); - break; - } - case EmitBinaryExpressionState.EmitRight: { - const isCommaOperator = node.operatorToken.kind !== SyntaxKind.CommaToken; - const indentBeforeOperator = needsIndentation(node, node.left, node.operatorToken); - const indentAfterOperator = needsIndentation(node, node.operatorToken, node.right); - increaseIndentIf(indentBeforeOperator, isCommaOperator); - emitLeadingCommentsOfPosition(node.operatorToken.pos); - writeTokenNode(node.operatorToken, node.operatorToken.kind === SyntaxKind.InKeyword ? writeKeyword : writeOperator); - emitTrailingCommentsOfPosition(node.operatorToken.end, /*prefixSpace*/ true); // Binary operators should have a space before the comment starts - increaseIndentIf(indentAfterOperator, /*writeSpaceIfNotIndenting*/ true); - maybePipelineEmitExpression(node.right); - break; - } - case EmitBinaryExpressionState.FinishEmit: { - const indentBeforeOperator = needsIndentation(node, node.left, node.operatorToken); - const indentAfterOperator = needsIndentation(node, node.operatorToken, node.right); - decreaseIndentIf(indentBeforeOperator, indentAfterOperator); - stackIndex--; - break; - } - default: return Debug.fail(`Invalid state ${stateStack[stackIndex]} for emitBinaryExpressionWorker`); - } - } - - function maybePipelineEmitExpression(next: Expression) { - // Advance the state of this unit of work, - stateStack[stackIndex]++; - - // Then actually do the work of emitting the node `next` returned by the prior state - - // The following section should be identical to `pipelineEmit` save it assumes EmitHint.Expression and offloads - // binary expression handling, where possible, to the contained work queue - - // #region trampolinePipelineEmit - const savedLastNode = lastNode; - const savedLastSubstitution = lastSubstitution; - lastNode = next; - lastSubstitution = undefined; - - const pipelinePhase = getPipelinePhase(PipelinePhase.Notification, EmitHint.Expression, next); - if (pipelinePhase === pipelineEmitWithHint && isBinaryExpression(next)) { - // If the target pipeline phase is emit directly, and the next node's also a binary expression, - // skip all the intermediate indirection and push the expression directly onto the work stack - stackIndex++; - stateStack[stackIndex] = EmitBinaryExpressionState.EmitLeft; - nodeStack[stackIndex] = next; - } - else { - pipelinePhase(EmitHint.Expression, next); - } - - Debug.assert(lastNode === next); - - lastNode = savedLastNode; - lastSubstitution = savedLastSubstitution; - // #endregion trampolinePipelineEmit - } - } - - function emitConditionalExpression(node: ConditionalExpression) { - const indentBeforeQuestion = needsIndentation(node, node.condition, node.questionToken); - const indentAfterQuestion = needsIndentation(node, node.questionToken, node.whenTrue); - const indentBeforeColon = needsIndentation(node, node.whenTrue, node.colonToken); - const indentAfterColon = needsIndentation(node, node.colonToken, node.whenFalse); - - emitExpression(node.condition); - increaseIndentIf(indentBeforeQuestion, /*writeSpaceIfNotIndenting*/ true); - emit(node.questionToken); - increaseIndentIf(indentAfterQuestion, /*writeSpaceIfNotIndenting*/ true); - emitExpression(node.whenTrue); - decreaseIndentIf(indentBeforeQuestion, indentAfterQuestion); - - increaseIndentIf(indentBeforeColon, /*writeSpaceIfNotIndenting*/ true); - emit(node.colonToken); - increaseIndentIf(indentAfterColon, /*writeSpaceIfNotIndenting*/ true); - emitExpression(node.whenFalse); - decreaseIndentIf(indentBeforeColon, indentAfterColon); - } - - function emitTemplateExpression(node: TemplateExpression) { - emit(node.head); - emitList(node, node.templateSpans, ListFormat.TemplateExpressionSpans); - } - - function emitYieldExpression(node: YieldExpression) { - emitTokenWithComment(SyntaxKind.YieldKeyword, node.pos, writeKeyword, node); - emit(node.asteriskToken); - emitExpressionWithLeadingSpace(node.expression); - } - - function emitSpreadExpression(node: SpreadElement) { - emitTokenWithComment(SyntaxKind.DotDotDotToken, node.pos, writePunctuation, node); - emitExpression(node.expression); - } - - function emitClassExpression(node: ClassExpression) { - generateNameIfNeeded(node.name); - emitClassDeclarationOrExpression(node); - } - - function emitExpressionWithTypeArguments(node: ExpressionWithTypeArguments) { - emitExpression(node.expression); - emitTypeArguments(node, node.typeArguments); - } - - function emitAsExpression(node: AsExpression) { - emitExpression(node.expression); - if (node.type) { - writeSpace(); - writeKeyword("as"); - writeSpace(); - emit(node.type); + else { + emitEmbeddedStatement(node, node.elseStatement); } } - - function emitNonNullExpression(node: NonNullExpression) { - emitExpression(node.expression); - writeOperator("!"); - } - - function emitMetaProperty(node: MetaProperty) { - writeToken(node.keywordToken, node.pos, writePunctuation); - writePunctuation("."); - emit(node.name); - } - - // - // Misc - // - - function emitTemplateSpan(node: TemplateSpan) { - emitExpression(node.expression); - emit(node.literal); + } + function emitWhileClause(node: WhileStatement | DoStatement, startPos: number) { + const openParenPos = emitTokenWithComment(SyntaxKind.WhileKeyword, startPos, writeKeyword, node); + writeSpace(); + emitTokenWithComment(SyntaxKind.OpenParenToken, openParenPos, writePunctuation, node); + emitExpression(node.expression); + emitTokenWithComment(SyntaxKind.CloseParenToken, node.expression.end, writePunctuation, node); + } + function emitDoStatement(node: DoStatement) { + emitTokenWithComment(SyntaxKind.DoKeyword, node.pos, writeKeyword, node); + emitEmbeddedStatement(node, node.statement); + if (isBlock(node.statement)) { + writeSpace(); } - - // - // Statements - // - - function emitBlock(node: Block) { - emitBlockStatements(node, /*forceSingleLine*/ !node.multiLine && isEmptyBlock(node)); - } - - function emitBlockStatements(node: BlockLike, forceSingleLine: boolean) { - emitTokenWithComment(SyntaxKind.OpenBraceToken, node.pos, writePunctuation, /*contextNode*/ node); - const format = forceSingleLine || getEmitFlags(node) & EmitFlags.SingleLine ? ListFormat.SingleLineBlockStatements : ListFormat.MultiLineBlockStatements; - emitList(node, node.statements, format); - emitTokenWithComment(SyntaxKind.CloseBraceToken, node.statements.end, writePunctuation, /*contextNode*/ node, /*indentLeading*/ !!(format & ListFormat.MultiLine)); - } - - function emitVariableStatement(node: VariableStatement) { - emitModifiers(node, node.modifiers); - emit(node.declarationList); - writeTrailingSemicolon(); + else { + writeLineOrSpace(node); } - - function emitEmptyStatement(isEmbeddedStatement: boolean) { - // While most trailing semicolons are possibly insignificant, an embedded "empty" - // statement is significant and cannot be elided by a trailing-semicolon-omitting writer. - if (isEmbeddedStatement) { - writePunctuation(";"); + emitWhileClause(node, node.statement.end); + writeTrailingSemicolon(); + } + function emitWhileStatement(node: WhileStatement) { + emitWhileClause(node, node.pos); + emitEmbeddedStatement(node, node.statement); + } + function emitForStatement(node: ForStatement) { + const openParenPos = emitTokenWithComment(SyntaxKind.ForKeyword, node.pos, writeKeyword, node); + writeSpace(); + let pos = emitTokenWithComment(SyntaxKind.OpenParenToken, openParenPos, writePunctuation, /*contextNode*/ node); + emitForBinding(node.initializer); + pos = emitTokenWithComment(SyntaxKind.SemicolonToken, node.initializer ? node.initializer.end : pos, writePunctuation, node); + emitExpressionWithLeadingSpace(node.condition); + pos = emitTokenWithComment(SyntaxKind.SemicolonToken, node.condition ? node.condition.end : pos, writePunctuation, node); + emitExpressionWithLeadingSpace(node.incrementor); + emitTokenWithComment(SyntaxKind.CloseParenToken, node.incrementor ? node.incrementor.end : pos, writePunctuation, node); + emitEmbeddedStatement(node, node.statement); + } + function emitForInStatement(node: ForInStatement) { + const openParenPos = emitTokenWithComment(SyntaxKind.ForKeyword, node.pos, writeKeyword, node); + writeSpace(); + emitTokenWithComment(SyntaxKind.OpenParenToken, openParenPos, writePunctuation, node); + emitForBinding(node.initializer); + writeSpace(); + emitTokenWithComment(SyntaxKind.InKeyword, node.initializer.end, writeKeyword, node); + writeSpace(); + emitExpression(node.expression); + emitTokenWithComment(SyntaxKind.CloseParenToken, node.expression.end, writePunctuation, node); + emitEmbeddedStatement(node, node.statement); + } + function emitForOfStatement(node: ForOfStatement) { + const openParenPos = emitTokenWithComment(SyntaxKind.ForKeyword, node.pos, writeKeyword, node); + writeSpace(); + emitWithTrailingSpace(node.awaitModifier); + emitTokenWithComment(SyntaxKind.OpenParenToken, openParenPos, writePunctuation, node); + emitForBinding(node.initializer); + writeSpace(); + emitTokenWithComment(SyntaxKind.OfKeyword, node.initializer.end, writeKeyword, node); + writeSpace(); + emitExpression(node.expression); + emitTokenWithComment(SyntaxKind.CloseParenToken, node.expression.end, writePunctuation, node); + emitEmbeddedStatement(node, node.statement); + } + function emitForBinding(node: VariableDeclarationList | Expression | undefined) { + if (node !== undefined) { + if (node.kind === SyntaxKind.VariableDeclarationList) { + emit(node); } else { - writeTrailingSemicolon(); - } - } - - - function emitExpressionStatement(node: ExpressionStatement) { - emitExpression(node.expression); - // Emit semicolon in non json files - // or if json file that created synthesized expression(eg.define expression statement when --out and amd code generation) - if (!isJsonSourceFile(currentSourceFile!) || nodeIsSynthesized(node.expression)) { - writeTrailingSemicolon(); - } - } - - function emitIfStatement(node: IfStatement) { - const openParenPos = emitTokenWithComment(SyntaxKind.IfKeyword, node.pos, writeKeyword, node); - writeSpace(); - emitTokenWithComment(SyntaxKind.OpenParenToken, openParenPos, writePunctuation, node); - emitExpression(node.expression); - emitTokenWithComment(SyntaxKind.CloseParenToken, node.expression.end, writePunctuation, node); - emitEmbeddedStatement(node, node.thenStatement); - if (node.elseStatement) { - writeLineOrSpace(node); - emitTokenWithComment(SyntaxKind.ElseKeyword, node.thenStatement.end, writeKeyword, node); - if (node.elseStatement.kind === SyntaxKind.IfStatement) { - writeSpace(); - emit(node.elseStatement); - } - else { - emitEmbeddedStatement(node, node.elseStatement); - } + emitExpression(node); } } - - function emitWhileClause(node: WhileStatement | DoStatement, startPos: number) { - const openParenPos = emitTokenWithComment(SyntaxKind.WhileKeyword, startPos, writeKeyword, node); - writeSpace(); - emitTokenWithComment(SyntaxKind.OpenParenToken, openParenPos, writePunctuation, node); - emitExpression(node.expression); - emitTokenWithComment(SyntaxKind.CloseParenToken, node.expression.end, writePunctuation, node); - } - - function emitDoStatement(node: DoStatement) { - emitTokenWithComment(SyntaxKind.DoKeyword, node.pos, writeKeyword, node); - emitEmbeddedStatement(node, node.statement); - if (isBlock(node.statement)) { - writeSpace(); + } + function emitContinueStatement(node: ContinueStatement) { + emitTokenWithComment(SyntaxKind.ContinueKeyword, node.pos, writeKeyword, node); + emitWithLeadingSpace(node.label); + writeTrailingSemicolon(); + } + function emitBreakStatement(node: BreakStatement) { + emitTokenWithComment(SyntaxKind.BreakKeyword, node.pos, writeKeyword, node); + emitWithLeadingSpace(node.label); + writeTrailingSemicolon(); + } + function emitTokenWithComment(token: SyntaxKind, pos: number, writer: (s: string) => void, contextNode: Node, indentLeading?: boolean) { + const node = getParseTreeNode(contextNode); + const isSimilarNode = node && node.kind === contextNode.kind; + const startPos = pos; + if (isSimilarNode && currentSourceFile) { + pos = skipTrivia(currentSourceFile.text, pos); + } + if (emitLeadingCommentsOfPosition && isSimilarNode && contextNode.pos !== startPos) { + const needsIndent = indentLeading && currentSourceFile && !positionsAreOnSameLine(startPos, pos, currentSourceFile); + if (needsIndent) { + increaseIndent(); } - else { - writeLineOrSpace(node); + emitLeadingCommentsOfPosition(startPos); + if (needsIndent) { + decreaseIndent(); } - - emitWhileClause(node, node.statement.end); - writeTrailingSemicolon(); } - - function emitWhileStatement(node: WhileStatement) { - emitWhileClause(node, node.pos); - emitEmbeddedStatement(node, node.statement); - } - - function emitForStatement(node: ForStatement) { - const openParenPos = emitTokenWithComment(SyntaxKind.ForKeyword, node.pos, writeKeyword, node); - writeSpace(); - let pos = emitTokenWithComment(SyntaxKind.OpenParenToken, openParenPos, writePunctuation, /*contextNode*/ node); - emitForBinding(node.initializer); - pos = emitTokenWithComment(SyntaxKind.SemicolonToken, node.initializer ? node.initializer.end : pos, writePunctuation, node); - emitExpressionWithLeadingSpace(node.condition); - pos = emitTokenWithComment(SyntaxKind.SemicolonToken, node.condition ? node.condition.end : pos, writePunctuation, node); - emitExpressionWithLeadingSpace(node.incrementor); - emitTokenWithComment(SyntaxKind.CloseParenToken, node.incrementor ? node.incrementor.end : pos, writePunctuation, node); - emitEmbeddedStatement(node, node.statement); - } - - function emitForInStatement(node: ForInStatement) { - const openParenPos = emitTokenWithComment(SyntaxKind.ForKeyword, node.pos, writeKeyword, node); - writeSpace(); - emitTokenWithComment(SyntaxKind.OpenParenToken, openParenPos, writePunctuation, node); - emitForBinding(node.initializer); - writeSpace(); - emitTokenWithComment(SyntaxKind.InKeyword, node.initializer.end, writeKeyword, node); - writeSpace(); - emitExpression(node.expression); - emitTokenWithComment(SyntaxKind.CloseParenToken, node.expression.end, writePunctuation, node); - emitEmbeddedStatement(node, node.statement); + pos = writeTokenText(token, writer, pos); + if (emitTrailingCommentsOfPosition && isSimilarNode && contextNode.end !== pos) { + emitTrailingCommentsOfPosition(pos, /*prefixSpace*/ true); } - - function emitForOfStatement(node: ForOfStatement) { - const openParenPos = emitTokenWithComment(SyntaxKind.ForKeyword, node.pos, writeKeyword, node); - writeSpace(); - emitWithTrailingSpace(node.awaitModifier); - emitTokenWithComment(SyntaxKind.OpenParenToken, openParenPos, writePunctuation, node); - emitForBinding(node.initializer); - writeSpace(); - emitTokenWithComment(SyntaxKind.OfKeyword, node.initializer.end, writeKeyword, node); + return pos; + } + function emitReturnStatement(node: ReturnStatement) { + emitTokenWithComment(SyntaxKind.ReturnKeyword, node.pos, writeKeyword, /*contextNode*/ node); + emitExpressionWithLeadingSpace(node.expression); + writeTrailingSemicolon(); + } + function emitWithStatement(node: WithStatement) { + const openParenPos = emitTokenWithComment(SyntaxKind.WithKeyword, node.pos, writeKeyword, node); + writeSpace(); + emitTokenWithComment(SyntaxKind.OpenParenToken, openParenPos, writePunctuation, node); + emitExpression(node.expression); + emitTokenWithComment(SyntaxKind.CloseParenToken, node.expression.end, writePunctuation, node); + emitEmbeddedStatement(node, node.statement); + } + function emitSwitchStatement(node: SwitchStatement) { + const openParenPos = emitTokenWithComment(SyntaxKind.SwitchKeyword, node.pos, writeKeyword, node); + writeSpace(); + emitTokenWithComment(SyntaxKind.OpenParenToken, openParenPos, writePunctuation, node); + emitExpression(node.expression); + emitTokenWithComment(SyntaxKind.CloseParenToken, node.expression.end, writePunctuation, node); + writeSpace(); + emit(node.caseBlock); + } + function emitLabeledStatement(node: LabeledStatement) { + emit(node.label); + emitTokenWithComment(SyntaxKind.ColonToken, node.label.end, writePunctuation, node); + writeSpace(); + emit(node.statement); + } + function emitThrowStatement(node: ThrowStatement) { + emitTokenWithComment(SyntaxKind.ThrowKeyword, node.pos, writeKeyword, node); + emitExpressionWithLeadingSpace(node.expression); + writeTrailingSemicolon(); + } + function emitTryStatement(node: TryStatement) { + emitTokenWithComment(SyntaxKind.TryKeyword, node.pos, writeKeyword, node); + writeSpace(); + emit(node.tryBlock); + if (node.catchClause) { + writeLineOrSpace(node); + emit(node.catchClause); + } + if (node.finallyBlock) { + writeLineOrSpace(node); + emitTokenWithComment(SyntaxKind.FinallyKeyword, (node.catchClause || node.tryBlock).end, writeKeyword, node); writeSpace(); - emitExpression(node.expression); - emitTokenWithComment(SyntaxKind.CloseParenToken, node.expression.end, writePunctuation, node); - emitEmbeddedStatement(node, node.statement); + emit(node.finallyBlock); } - - function emitForBinding(node: VariableDeclarationList | Expression | undefined) { - if (node !== undefined) { - if (node.kind === SyntaxKind.VariableDeclarationList) { - emit(node); + } + function emitDebuggerStatement(node: DebuggerStatement) { + writeToken(SyntaxKind.DebuggerKeyword, node.pos, writeKeyword); + writeTrailingSemicolon(); + } + // + // Declarations + // + function emitVariableDeclaration(node: VariableDeclaration) { + emit(node.name); + emit(node.exclamationToken); + emitTypeAnnotation(node.type); + emitInitializer(node.initializer, node.type ? node.type.end : node.name.end, node); + } + function emitVariableDeclarationList(node: VariableDeclarationList) { + writeKeyword(isLet(node) ? "let" : isVarConst(node) ? "const" : "var"); + writeSpace(); + emitList(node, node.declarations, ListFormat.VariableDeclarationList); + } + function emitFunctionDeclaration(node: FunctionDeclaration) { + emitFunctionDeclarationOrExpression(node); + } + function emitFunctionDeclarationOrExpression(node: FunctionDeclaration | FunctionExpression) { + emitDecorators(node, node.decorators); + emitModifiers(node, node.modifiers); + writeKeyword("function"); + emit(node.asteriskToken); + writeSpace(); + emitIdentifierName(node.name); + emitSignatureAndBody(node, emitSignatureHead); + } + function emitBlockCallback(_hint: EmitHint, body: Node): void { + emitBlockFunctionBody((body)); + } + function emitSignatureAndBody(node: FunctionLikeDeclaration, emitSignatureHead: (node: SignatureDeclaration) => void) { + const body = node.body; + if (body) { + if (isBlock(body)) { + const indentedFlag = getEmitFlags(node) & EmitFlags.Indented; + if (indentedFlag) { + increaseIndent(); } - else { - emitExpression(node); + pushNameGenerationScope(node); + forEach(node.parameters, generateNames); + generateNames(node.body); + emitSignatureHead(node); + if (onEmitNode) { + onEmitNode(EmitHint.Unspecified, body, emitBlockCallback); } - } - } - - function emitContinueStatement(node: ContinueStatement) { - emitTokenWithComment(SyntaxKind.ContinueKeyword, node.pos, writeKeyword, node); - emitWithLeadingSpace(node.label); - writeTrailingSemicolon(); - } - - function emitBreakStatement(node: BreakStatement) { - emitTokenWithComment(SyntaxKind.BreakKeyword, node.pos, writeKeyword, node); - emitWithLeadingSpace(node.label); - writeTrailingSemicolon(); - } - - function emitTokenWithComment(token: SyntaxKind, pos: number, writer: (s: string) => void, contextNode: Node, indentLeading?: boolean) { - const node = getParseTreeNode(contextNode); - const isSimilarNode = node && node.kind === contextNode.kind; - const startPos = pos; - if (isSimilarNode && currentSourceFile) { - pos = skipTrivia(currentSourceFile.text, pos); - } - if (emitLeadingCommentsOfPosition && isSimilarNode && contextNode.pos !== startPos) { - const needsIndent = indentLeading && currentSourceFile && !positionsAreOnSameLine(startPos, pos, currentSourceFile); - if (needsIndent) { - increaseIndent(); + else { + emitBlockFunctionBody(body); } - emitLeadingCommentsOfPosition(startPos); - if (needsIndent) { + popNameGenerationScope(node); + if (indentedFlag) { decreaseIndent(); } } - pos = writeTokenText(token, writer, pos); - if (emitTrailingCommentsOfPosition && isSimilarNode && contextNode.end !== pos) { - emitTrailingCommentsOfPosition(pos, /*prefixSpace*/ true); + else { + emitSignatureHead(node); + writeSpace(); + emitExpression(body); } - return pos; } - - function emitReturnStatement(node: ReturnStatement) { - emitTokenWithComment(SyntaxKind.ReturnKeyword, node.pos, writeKeyword, /*contextNode*/ node); - emitExpressionWithLeadingSpace(node.expression); + else { + emitSignatureHead(node); writeTrailingSemicolon(); } - - function emitWithStatement(node: WithStatement) { - const openParenPos = emitTokenWithComment(SyntaxKind.WithKeyword, node.pos, writeKeyword, node); - writeSpace(); - emitTokenWithComment(SyntaxKind.OpenParenToken, openParenPos, writePunctuation, node); - emitExpression(node.expression); - emitTokenWithComment(SyntaxKind.CloseParenToken, node.expression.end, writePunctuation, node); - emitEmbeddedStatement(node, node.statement); + } + function emitSignatureHead(node: FunctionDeclaration | FunctionExpression | MethodDeclaration | AccessorDeclaration | ConstructorDeclaration) { + emitTypeParameters(node, node.typeParameters); + emitParameters(node, node.parameters); + emitTypeAnnotation(node.type); + } + function shouldEmitBlockFunctionBodyOnSingleLine(body: Block) { + // We must emit a function body as a single-line body in the following case: + // * The body has NodeEmitFlags.SingleLine specified. + // We must emit a function body as a multi-line body in the following cases: + // * The body is explicitly marked as multi-line. + // * A non-synthesized body's start and end position are on different lines. + // * Any statement in the body starts on a new line. + if (getEmitFlags(body) & EmitFlags.SingleLine) { + return true; } - - function emitSwitchStatement(node: SwitchStatement) { - const openParenPos = emitTokenWithComment(SyntaxKind.SwitchKeyword, node.pos, writeKeyword, node); - writeSpace(); - emitTokenWithComment(SyntaxKind.OpenParenToken, openParenPos, writePunctuation, node); - emitExpression(node.expression); - emitTokenWithComment(SyntaxKind.CloseParenToken, node.expression.end, writePunctuation, node); - writeSpace(); - emit(node.caseBlock); + if (body.multiLine) { + return false; } - - function emitLabeledStatement(node: LabeledStatement) { - emit(node.label); - emitTokenWithComment(SyntaxKind.ColonToken, node.label.end, writePunctuation, node); - writeSpace(); - emit(node.statement); + if (!nodeIsSynthesized(body) && !rangeIsOnSingleLine(body, (currentSourceFile!))) { + return false; } - - function emitThrowStatement(node: ThrowStatement) { - emitTokenWithComment(SyntaxKind.ThrowKeyword, node.pos, writeKeyword, node); - emitExpressionWithLeadingSpace(node.expression); - writeTrailingSemicolon(); + if (shouldWriteLeadingLineTerminator(body, body.statements, ListFormat.PreserveLines) + || shouldWriteClosingLineTerminator(body, body.statements, ListFormat.PreserveLines)) { + return false; } - - function emitTryStatement(node: TryStatement) { - emitTokenWithComment(SyntaxKind.TryKeyword, node.pos, writeKeyword, node); - writeSpace(); - emit(node.tryBlock); - if (node.catchClause) { - writeLineOrSpace(node); - emit(node.catchClause); - } - if (node.finallyBlock) { - writeLineOrSpace(node); - emitTokenWithComment(SyntaxKind.FinallyKeyword, (node.catchClause || node.tryBlock).end, writeKeyword, node); - writeSpace(); - emit(node.finallyBlock); + let previousStatement: Statement | undefined; + for (const statement of body.statements) { + if (shouldWriteSeparatingLineTerminator(previousStatement, statement, ListFormat.PreserveLines)) { + return false; } + previousStatement = statement; } - - function emitDebuggerStatement(node: DebuggerStatement) { - writeToken(SyntaxKind.DebuggerKeyword, node.pos, writeKeyword); - writeTrailingSemicolon(); + return true; + } + function emitBlockFunctionBody(body: Block) { + writeSpace(); + writePunctuation("{"); + increaseIndent(); + const emitBlockFunctionBody = shouldEmitBlockFunctionBodyOnSingleLine(body) + ? emitBlockFunctionBodyOnSingleLine + : emitBlockFunctionBodyWorker; + if (emitBodyWithDetachedComments) { + emitBodyWithDetachedComments(body, body.statements, emitBlockFunctionBody); } - - // - // Declarations - // - - function emitVariableDeclaration(node: VariableDeclaration) { - emit(node.name); - emit(node.exclamationToken); - emitTypeAnnotation(node.type); - emitInitializer(node.initializer, node.type ? node.type.end : node.name.end, node); + else { + emitBlockFunctionBody(body); } - - function emitVariableDeclarationList(node: VariableDeclarationList) { - writeKeyword(isLet(node) ? "let" : isVarConst(node) ? "const" : "var"); - writeSpace(); - emitList(node, node.declarations, ListFormat.VariableDeclarationList); - } - - function emitFunctionDeclaration(node: FunctionDeclaration) { - emitFunctionDeclarationOrExpression(node); - } - - function emitFunctionDeclarationOrExpression(node: FunctionDeclaration | FunctionExpression) { - emitDecorators(node, node.decorators); - emitModifiers(node, node.modifiers); - writeKeyword("function"); - emit(node.asteriskToken); + decreaseIndent(); + writeToken(SyntaxKind.CloseBraceToken, body.statements.end, writePunctuation, body); + } + function emitBlockFunctionBodyOnSingleLine(body: Block) { + emitBlockFunctionBodyWorker(body, /*emitBlockFunctionBodyOnSingleLine*/ true); + } + function emitBlockFunctionBodyWorker(body: Block, emitBlockFunctionBodyOnSingleLine?: boolean) { + // Emit all the prologue directives (like "use strict"). + const statementOffset = emitPrologueDirectives(body.statements); + const pos = writer.getTextPos(); + emitHelpers(body); + if (statementOffset === 0 && pos === writer.getTextPos() && emitBlockFunctionBodyOnSingleLine) { + decreaseIndent(); + emitList(body, body.statements, ListFormat.SingleLineFunctionBodyStatements); + increaseIndent(); + } + else { + emitList(body, body.statements, ListFormat.MultiLineFunctionBodyStatements, statementOffset); + } + } + function emitClassDeclaration(node: ClassDeclaration) { + emitClassDeclarationOrExpression(node); + } + function emitClassDeclarationOrExpression(node: ClassDeclaration | ClassExpression) { + forEach(node.members, generateMemberNames); + emitDecorators(node, node.decorators); + emitModifiers(node, node.modifiers); + writeKeyword("class"); + if (node.name) { writeSpace(); emitIdentifierName(node.name); - emitSignatureAndBody(node, emitSignatureHead); - } - - function emitBlockCallback(_hint: EmitHint, body: Node): void { - emitBlockFunctionBody(body); - } - - function emitSignatureAndBody(node: FunctionLikeDeclaration, emitSignatureHead: (node: SignatureDeclaration) => void) { - const body = node.body; - if (body) { - if (isBlock(body)) { - const indentedFlag = getEmitFlags(node) & EmitFlags.Indented; - if (indentedFlag) { - increaseIndent(); - } - - pushNameGenerationScope(node); - forEach(node.parameters, generateNames); - generateNames(node.body); - - emitSignatureHead(node); - if (onEmitNode) { - onEmitNode(EmitHint.Unspecified, body, emitBlockCallback); - } - else { - emitBlockFunctionBody(body); - } - popNameGenerationScope(node); - - if (indentedFlag) { - decreaseIndent(); - } - } - else { - emitSignatureHead(node); - writeSpace(); - emitExpression(body); - } - } - else { - emitSignatureHead(node); - writeTrailingSemicolon(); - } - } - - function emitSignatureHead(node: FunctionDeclaration | FunctionExpression | MethodDeclaration | AccessorDeclaration | ConstructorDeclaration) { - emitTypeParameters(node, node.typeParameters); - emitParameters(node, node.parameters); - emitTypeAnnotation(node.type); + const indentedFlag = getEmitFlags(node) & EmitFlags.Indented; + if (indentedFlag) { + increaseIndent(); } - - function shouldEmitBlockFunctionBodyOnSingleLine(body: Block) { - // We must emit a function body as a single-line body in the following case: - // * The body has NodeEmitFlags.SingleLine specified. - - // We must emit a function body as a multi-line body in the following cases: - // * The body is explicitly marked as multi-line. - // * A non-synthesized body's start and end position are on different lines. - // * Any statement in the body starts on a new line. - - if (getEmitFlags(body) & EmitFlags.SingleLine) { - return true; - } - - if (body.multiLine) { - return false; - } - - if (!nodeIsSynthesized(body) && !rangeIsOnSingleLine(body, currentSourceFile!)) { - return false; - } - - if (shouldWriteLeadingLineTerminator(body, body.statements, ListFormat.PreserveLines) - || shouldWriteClosingLineTerminator(body, body.statements, ListFormat.PreserveLines)) { - return false; - } - - let previousStatement: Statement | undefined; - for (const statement of body.statements) { - if (shouldWriteSeparatingLineTerminator(previousStatement, statement, ListFormat.PreserveLines)) { - return false; - } - - previousStatement = statement; - } - - return true; + emitTypeParameters(node, node.typeParameters); + emitList(node, node.heritageClauses, ListFormat.ClassHeritageClauses); + writeSpace(); + writePunctuation("{"); + emitList(node, node.members, ListFormat.ClassMembers); + writePunctuation("}"); + if (indentedFlag) { + decreaseIndent(); } - - function emitBlockFunctionBody(body: Block) { + } + function emitInterfaceDeclaration(node: InterfaceDeclaration) { + emitDecorators(node, node.decorators); + emitModifiers(node, node.modifiers); + writeKeyword("interface"); + writeSpace(); + emit(node.name); + emitTypeParameters(node, node.typeParameters); + emitList(node, node.heritageClauses, ListFormat.HeritageClauses); + writeSpace(); + writePunctuation("{"); + emitList(node, node.members, ListFormat.InterfaceMembers); + writePunctuation("}"); + } + function emitTypeAliasDeclaration(node: TypeAliasDeclaration) { + emitDecorators(node, node.decorators); + emitModifiers(node, node.modifiers); + writeKeyword("type"); + writeSpace(); + emit(node.name); + emitTypeParameters(node, node.typeParameters); + writeSpace(); + writePunctuation("="); + writeSpace(); + emit(node.type); + writeTrailingSemicolon(); + } + function emitEnumDeclaration(node: EnumDeclaration) { + emitModifiers(node, node.modifiers); + writeKeyword("enum"); + writeSpace(); + emit(node.name); + writeSpace(); + writePunctuation("{"); + emitList(node, node.members, ListFormat.EnumMembers); + writePunctuation("}"); + } + function emitModuleDeclaration(node: ModuleDeclaration) { + emitModifiers(node, node.modifiers); + if (~node.flags & NodeFlags.GlobalAugmentation) { + writeKeyword(node.flags & NodeFlags.Namespace ? "namespace" : "module"); writeSpace(); - writePunctuation("{"); - increaseIndent(); - - const emitBlockFunctionBody = shouldEmitBlockFunctionBodyOnSingleLine(body) - ? emitBlockFunctionBodyOnSingleLine - : emitBlockFunctionBodyWorker; - - if (emitBodyWithDetachedComments) { - emitBodyWithDetachedComments(body, body.statements, emitBlockFunctionBody); - } - else { - emitBlockFunctionBody(body); - } - - decreaseIndent(); - writeToken(SyntaxKind.CloseBraceToken, body.statements.end, writePunctuation, body); } - - function emitBlockFunctionBodyOnSingleLine(body: Block) { - emitBlockFunctionBodyWorker(body, /*emitBlockFunctionBodyOnSingleLine*/ true); + emit(node.name); + let body = node.body; + if (!body) + return writeTrailingSemicolon(); + while (body.kind === SyntaxKind.ModuleDeclaration) { + writePunctuation("."); + emit((body).name); + body = ((body).body!); } - - function emitBlockFunctionBodyWorker(body: Block, emitBlockFunctionBodyOnSingleLine?: boolean) { - // Emit all the prologue directives (like "use strict"). - const statementOffset = emitPrologueDirectives(body.statements); - const pos = writer.getTextPos(); - emitHelpers(body); - if (statementOffset === 0 && pos === writer.getTextPos() && emitBlockFunctionBodyOnSingleLine) { - decreaseIndent(); - emitList(body, body.statements, ListFormat.SingleLineFunctionBodyStatements); - increaseIndent(); - } - else { - emitList(body, body.statements, ListFormat.MultiLineFunctionBodyStatements, statementOffset); - } + writeSpace(); + emit(body); + } + function emitModuleBlock(node: ModuleBlock) { + pushNameGenerationScope(node); + forEach(node.statements, generateNames); + emitBlockStatements(node, /*forceSingleLine*/ isEmptyBlock(node)); + popNameGenerationScope(node); + } + function emitCaseBlock(node: CaseBlock) { + emitTokenWithComment(SyntaxKind.OpenBraceToken, node.pos, writePunctuation, node); + emitList(node, node.clauses, ListFormat.CaseBlockClauses); + emitTokenWithComment(SyntaxKind.CloseBraceToken, node.clauses.end, writePunctuation, node, /*indentLeading*/ true); + } + function emitImportEqualsDeclaration(node: ImportEqualsDeclaration) { + emitModifiers(node, node.modifiers); + emitTokenWithComment(SyntaxKind.ImportKeyword, node.modifiers ? node.modifiers.end : node.pos, writeKeyword, node); + writeSpace(); + emit(node.name); + writeSpace(); + emitTokenWithComment(SyntaxKind.EqualsToken, node.name.end, writePunctuation, node); + writeSpace(); + emitModuleReference(node.moduleReference); + writeTrailingSemicolon(); + } + function emitModuleReference(node: ModuleReference) { + if (node.kind === SyntaxKind.Identifier) { + emitExpression(node); } - - function emitClassDeclaration(node: ClassDeclaration) { - emitClassDeclarationOrExpression(node); - } - - function emitClassDeclarationOrExpression(node: ClassDeclaration | ClassExpression) { - forEach(node.members, generateMemberNames); - - emitDecorators(node, node.decorators); - emitModifiers(node, node.modifiers); - writeKeyword("class"); - if (node.name) { - writeSpace(); - emitIdentifierName(node.name); - } - - const indentedFlag = getEmitFlags(node) & EmitFlags.Indented; - if (indentedFlag) { - increaseIndent(); - } - - emitTypeParameters(node, node.typeParameters); - emitList(node, node.heritageClauses, ListFormat.ClassHeritageClauses); - - writeSpace(); - writePunctuation("{"); - emitList(node, node.members, ListFormat.ClassMembers); - writePunctuation("}"); - - if (indentedFlag) { - decreaseIndent(); - } + else { + emit(node); } - - function emitInterfaceDeclaration(node: InterfaceDeclaration) { - emitDecorators(node, node.decorators); - emitModifiers(node, node.modifiers); - writeKeyword("interface"); + } + function emitImportDeclaration(node: ImportDeclaration) { + emitModifiers(node, node.modifiers); + emitTokenWithComment(SyntaxKind.ImportKeyword, node.modifiers ? node.modifiers.end : node.pos, writeKeyword, node); + writeSpace(); + if (node.importClause) { + emit(node.importClause); writeSpace(); - emit(node.name); - emitTypeParameters(node, node.typeParameters); - emitList(node, node.heritageClauses, ListFormat.HeritageClauses); + emitTokenWithComment(SyntaxKind.FromKeyword, node.importClause.end, writeKeyword, node); writeSpace(); - writePunctuation("{"); - emitList(node, node.members, ListFormat.InterfaceMembers); - writePunctuation("}"); } - - function emitTypeAliasDeclaration(node: TypeAliasDeclaration) { - emitDecorators(node, node.decorators); - emitModifiers(node, node.modifiers); - writeKeyword("type"); - writeSpace(); - emit(node.name); - emitTypeParameters(node, node.typeParameters); - writeSpace(); - writePunctuation("="); + emitExpression(node.moduleSpecifier); + writeTrailingSemicolon(); + } + function emitImportClause(node: ImportClause) { + if (node.isTypeOnly) { + emitTokenWithComment(SyntaxKind.TypeKeyword, node.pos, writeKeyword, node); writeSpace(); - emit(node.type); - writeTrailingSemicolon(); } - - function emitEnumDeclaration(node: EnumDeclaration) { - emitModifiers(node, node.modifiers); - writeKeyword("enum"); + emit(node.name); + if (node.name && node.namedBindings) { + emitTokenWithComment(SyntaxKind.CommaToken, node.name.end, writePunctuation, node); writeSpace(); - emit(node.name); - - writeSpace(); - writePunctuation("{"); - emitList(node, node.members, ListFormat.EnumMembers); - writePunctuation("}"); } - - function emitModuleDeclaration(node: ModuleDeclaration) { - emitModifiers(node, node.modifiers); - if (~node.flags & NodeFlags.GlobalAugmentation) { - writeKeyword(node.flags & NodeFlags.Namespace ? "namespace" : "module"); - writeSpace(); - } - emit(node.name); - - let body = node.body; - if (!body) return writeTrailingSemicolon(); - while (body.kind === SyntaxKind.ModuleDeclaration) { - writePunctuation("."); - emit((body).name); - body = (body).body!; - } - - writeSpace(); - emit(body); - } - - function emitModuleBlock(node: ModuleBlock) { - pushNameGenerationScope(node); - forEach(node.statements, generateNames); - emitBlockStatements(node, /*forceSingleLine*/ isEmptyBlock(node)); - popNameGenerationScope(node); - } - - function emitCaseBlock(node: CaseBlock) { - emitTokenWithComment(SyntaxKind.OpenBraceToken, node.pos, writePunctuation, node); - emitList(node, node.clauses, ListFormat.CaseBlockClauses); - emitTokenWithComment(SyntaxKind.CloseBraceToken, node.clauses.end, writePunctuation, node, /*indentLeading*/ true); - } - - function emitImportEqualsDeclaration(node: ImportEqualsDeclaration) { - emitModifiers(node, node.modifiers); - emitTokenWithComment(SyntaxKind.ImportKeyword, node.modifiers ? node.modifiers.end : node.pos, writeKeyword, node); - writeSpace(); - emit(node.name); - writeSpace(); - emitTokenWithComment(SyntaxKind.EqualsToken, node.name.end, writePunctuation, node); - writeSpace(); - emitModuleReference(node.moduleReference); - writeTrailingSemicolon(); + emit(node.namedBindings); + } + function emitNamespaceImport(node: NamespaceImport) { + const asPos = emitTokenWithComment(SyntaxKind.AsteriskToken, node.pos, writePunctuation, node); + writeSpace(); + emitTokenWithComment(SyntaxKind.AsKeyword, asPos, writeKeyword, node); + writeSpace(); + emit(node.name); + } + function emitNamedImports(node: NamedImports) { + emitNamedImportsOrExports(node); + } + function emitImportSpecifier(node: ImportSpecifier) { + emitImportOrExportSpecifier(node); + } + function emitExportAssignment(node: ExportAssignment) { + const nextPos = emitTokenWithComment(SyntaxKind.ExportKeyword, node.pos, writeKeyword, node); + writeSpace(); + if (node.isExportEquals) { + emitTokenWithComment(SyntaxKind.EqualsToken, nextPos, writeOperator, node); } - - function emitModuleReference(node: ModuleReference) { - if (node.kind === SyntaxKind.Identifier) { - emitExpression(node); - } - else { - emit(node); - } + else { + emitTokenWithComment(SyntaxKind.DefaultKeyword, nextPos, writeKeyword, node); } - - function emitImportDeclaration(node: ImportDeclaration) { - emitModifiers(node, node.modifiers); - emitTokenWithComment(SyntaxKind.ImportKeyword, node.modifiers ? node.modifiers.end : node.pos, writeKeyword, node); + writeSpace(); + emitExpression(node.expression); + writeTrailingSemicolon(); + } + function emitExportDeclaration(node: ExportDeclaration) { + let nextPos = emitTokenWithComment(SyntaxKind.ExportKeyword, node.pos, writeKeyword, node); + writeSpace(); + if (node.isTypeOnly) { + nextPos = emitTokenWithComment(SyntaxKind.TypeKeyword, nextPos, writeKeyword, node); writeSpace(); - if (node.importClause) { - emit(node.importClause); - writeSpace(); - emitTokenWithComment(SyntaxKind.FromKeyword, node.importClause.end, writeKeyword, node); - writeSpace(); - } - emitExpression(node.moduleSpecifier); - writeTrailingSemicolon(); } - - function emitImportClause(node: ImportClause) { - if (node.isTypeOnly) { - emitTokenWithComment(SyntaxKind.TypeKeyword, node.pos, writeKeyword, node); - writeSpace(); - } - emit(node.name); - if (node.name && node.namedBindings) { - emitTokenWithComment(SyntaxKind.CommaToken, node.name.end, writePunctuation, node); - writeSpace(); - } - emit(node.namedBindings); + if (node.exportClause) { + emit(node.exportClause); + } + else { + nextPos = emitTokenWithComment(SyntaxKind.AsteriskToken, nextPos, writePunctuation, node); } - - function emitNamespaceImport(node: NamespaceImport) { - const asPos = emitTokenWithComment(SyntaxKind.AsteriskToken, node.pos, writePunctuation, node); + if (node.moduleSpecifier) { writeSpace(); - emitTokenWithComment(SyntaxKind.AsKeyword, asPos, writeKeyword, node); + const fromPos = node.exportClause ? node.exportClause.end : nextPos; + emitTokenWithComment(SyntaxKind.FromKeyword, fromPos, writeKeyword, node); writeSpace(); - emit(node.name); - } - - function emitNamedImports(node: NamedImports) { - emitNamedImportsOrExports(node); - } - - function emitImportSpecifier(node: ImportSpecifier) { - emitImportOrExportSpecifier(node); + emitExpression(node.moduleSpecifier); } - - function emitExportAssignment(node: ExportAssignment) { - const nextPos = emitTokenWithComment(SyntaxKind.ExportKeyword, node.pos, writeKeyword, node); + writeTrailingSemicolon(); + } + function emitNamespaceExportDeclaration(node: NamespaceExportDeclaration) { + let nextPos = emitTokenWithComment(SyntaxKind.ExportKeyword, node.pos, writeKeyword, node); + writeSpace(); + nextPos = emitTokenWithComment(SyntaxKind.AsKeyword, nextPos, writeKeyword, node); + writeSpace(); + nextPos = emitTokenWithComment(SyntaxKind.NamespaceKeyword, nextPos, writeKeyword, node); + writeSpace(); + emit(node.name); + writeTrailingSemicolon(); + } + function emitNamespaceExport(node: NamespaceExport) { + const asPos = emitTokenWithComment(SyntaxKind.AsteriskToken, node.pos, writePunctuation, node); + writeSpace(); + emitTokenWithComment(SyntaxKind.AsKeyword, asPos, writeKeyword, node); + writeSpace(); + emit(node.name); + } + function emitNamedExports(node: NamedExports) { + emitNamedImportsOrExports(node); + } + function emitExportSpecifier(node: ExportSpecifier) { + emitImportOrExportSpecifier(node); + } + function emitNamedImportsOrExports(node: NamedImportsOrExports) { + writePunctuation("{"); + emitList(node, node.elements, ListFormat.NamedImportsOrExportsElements); + writePunctuation("}"); + } + function emitImportOrExportSpecifier(node: ImportOrExportSpecifier) { + if (node.propertyName) { + emit(node.propertyName); writeSpace(); - if (node.isExportEquals) { - emitTokenWithComment(SyntaxKind.EqualsToken, nextPos, writeOperator, node); - } - else { - emitTokenWithComment(SyntaxKind.DefaultKeyword, nextPos, writeKeyword, node); - } + emitTokenWithComment(SyntaxKind.AsKeyword, node.propertyName.end, writeKeyword, node); writeSpace(); - emitExpression(node.expression); - writeTrailingSemicolon(); } - - function emitExportDeclaration(node: ExportDeclaration) { - let nextPos = emitTokenWithComment(SyntaxKind.ExportKeyword, node.pos, writeKeyword, node); - writeSpace(); - if (node.isTypeOnly) { - nextPos = emitTokenWithComment(SyntaxKind.TypeKeyword, nextPos, writeKeyword, node); - writeSpace(); - } - if (node.exportClause) { - emit(node.exportClause); - } - else { - nextPos = emitTokenWithComment(SyntaxKind.AsteriskToken, nextPos, writePunctuation, node); - } - if (node.moduleSpecifier) { - writeSpace(); - const fromPos = node.exportClause ? node.exportClause.end : nextPos; - emitTokenWithComment(SyntaxKind.FromKeyword, fromPos, writeKeyword, node); + emit(node.name); + } + // + // Module references + // + function emitExternalModuleReference(node: ExternalModuleReference) { + writeKeyword("require"); + writePunctuation("("); + emitExpression(node.expression); + writePunctuation(")"); + } + // + // JSX + // + function emitJsxElement(node: JsxElement) { + emit(node.openingElement); + emitList(node, node.children, ListFormat.JsxElementOrFragmentChildren); + emit(node.closingElement); + } + function emitJsxSelfClosingElement(node: JsxSelfClosingElement) { + writePunctuation("<"); + emitJsxTagName(node.tagName); + emitTypeArguments(node, node.typeArguments); + writeSpace(); + emit(node.attributes); + writePunctuation("/>"); + } + function emitJsxFragment(node: JsxFragment) { + emit(node.openingFragment); + emitList(node, node.children, ListFormat.JsxElementOrFragmentChildren); + emit(node.closingFragment); + } + function emitJsxOpeningElementOrFragment(node: JsxOpeningElement | JsxOpeningFragment) { + writePunctuation("<"); + if (isJsxOpeningElement(node)) { + emitJsxTagName(node.tagName); + emitTypeArguments(node, node.typeArguments); + if (node.attributes.properties && node.attributes.properties.length > 0) { writeSpace(); - emitExpression(node.moduleSpecifier); } - writeTrailingSemicolon(); - } - - function emitNamespaceExportDeclaration(node: NamespaceExportDeclaration) { - let nextPos = emitTokenWithComment(SyntaxKind.ExportKeyword, node.pos, writeKeyword, node); - writeSpace(); - nextPos = emitTokenWithComment(SyntaxKind.AsKeyword, nextPos, writeKeyword, node); - writeSpace(); - nextPos = emitTokenWithComment(SyntaxKind.NamespaceKeyword, nextPos, writeKeyword, node); - writeSpace(); - emit(node.name); - writeTrailingSemicolon(); - } - - function emitNamespaceExport(node: NamespaceExport) { - const asPos = emitTokenWithComment(SyntaxKind.AsteriskToken, node.pos, writePunctuation, node); - writeSpace(); - emitTokenWithComment(SyntaxKind.AsKeyword, asPos, writeKeyword, node); - writeSpace(); - emit(node.name); - } - - function emitNamedExports(node: NamedExports) { - emitNamedImportsOrExports(node); + emit(node.attributes); } - - function emitExportSpecifier(node: ExportSpecifier) { - emitImportOrExportSpecifier(node); + writePunctuation(">"); + } + function emitJsxText(node: JsxText) { + writer.writeLiteral(node.text); + } + function emitJsxClosingElementOrFragment(node: JsxClosingElement | JsxClosingFragment) { + writePunctuation(""); + } + function emitJsxAttributes(node: JsxAttributes) { + emitList(node, node.properties, ListFormat.JsxElementAttributes); + } + function emitJsxAttribute(node: JsxAttribute) { + emit(node.name); + emitNodeWithPrefix("=", writePunctuation, node.initializer, emitJsxAttributeValue); + } + function emitJsxSpreadAttribute(node: JsxSpreadAttribute) { + writePunctuation("{..."); + emitExpression(node.expression); + writePunctuation("}"); + } + function emitJsxExpression(node: JsxExpression) { + if (node.expression) { writePunctuation("{"); - emitList(node, node.elements, ListFormat.NamedImportsOrExportsElements); + emit(node.dotDotDotToken); + emitExpression(node.expression); writePunctuation("}"); } - - function emitImportOrExportSpecifier(node: ImportOrExportSpecifier) { - if (node.propertyName) { - emit(node.propertyName); - writeSpace(); - emitTokenWithComment(SyntaxKind.AsKeyword, node.propertyName.end, writeKeyword, node); - writeSpace(); - } - - emit(node.name); + } + function emitJsxTagName(node: JsxTagNameExpression) { + if (node.kind === SyntaxKind.Identifier) { + emitExpression(node); } - - // - // Module references - // - - function emitExternalModuleReference(node: ExternalModuleReference) { - writeKeyword("require"); - writePunctuation("("); - emitExpression(node.expression); - writePunctuation(")"); + else { + emit(node); } - - // - // JSX - // - - function emitJsxElement(node: JsxElement) { - emit(node.openingElement); - emitList(node, node.children, ListFormat.JsxElementOrFragmentChildren); - emit(node.closingElement); - } - - function emitJsxSelfClosingElement(node: JsxSelfClosingElement) { - writePunctuation("<"); - emitJsxTagName(node.tagName); - emitTypeArguments(node, node.typeArguments); + } + // + // Clauses + // + function emitCaseClause(node: CaseClause) { + emitTokenWithComment(SyntaxKind.CaseKeyword, node.pos, writeKeyword, node); + writeSpace(); + emitExpression(node.expression); + emitCaseOrDefaultClauseRest(node, node.statements, node.expression.end); + } + function emitDefaultClause(node: DefaultClause) { + const pos = emitTokenWithComment(SyntaxKind.DefaultKeyword, node.pos, writeKeyword, node); + emitCaseOrDefaultClauseRest(node, node.statements, pos); + } + function emitCaseOrDefaultClauseRest(parentNode: Node, statements: NodeArray, colonPos: number) { + const emitAsSingleStatement = statements.length === 1 && + ( + // treat synthesized nodes as located on the same line for emit purposes + nodeIsSynthesized(parentNode) || + nodeIsSynthesized(statements[0]) || + rangeStartPositionsAreOnSameLine(parentNode, statements[0], (currentSourceFile!))); + let format = ListFormat.CaseOrDefaultClauseStatements; + if (emitAsSingleStatement) { + writeToken(SyntaxKind.ColonToken, colonPos, writePunctuation, parentNode); writeSpace(); - emit(node.attributes); - writePunctuation("/>"); - } - - function emitJsxFragment(node: JsxFragment) { - emit(node.openingFragment); - emitList(node, node.children, ListFormat.JsxElementOrFragmentChildren); - emit(node.closingFragment); - } - - function emitJsxOpeningElementOrFragment(node: JsxOpeningElement | JsxOpeningFragment) { - writePunctuation("<"); - - if (isJsxOpeningElement(node)) { - emitJsxTagName(node.tagName); - emitTypeArguments(node, node.typeArguments); - if (node.attributes.properties && node.attributes.properties.length > 0) { - writeSpace(); - } - emit(node.attributes); - } - - writePunctuation(">"); - } - - function emitJsxText(node: JsxText) { - writer.writeLiteral(node.text); - } - - function emitJsxClosingElementOrFragment(node: JsxClosingElement | JsxClosingFragment) { - writePunctuation(""); + format &= ~(ListFormat.MultiLine | ListFormat.Indented); + } + else { + emitTokenWithComment(SyntaxKind.ColonToken, colonPos, writePunctuation, parentNode); } - - function emitJsxAttributes(node: JsxAttributes) { - emitList(node, node.properties, ListFormat.JsxElementAttributes); + emitList(parentNode, statements, format); + } + function emitHeritageClause(node: HeritageClause) { + writeSpace(); + writeTokenText(node.token, writeKeyword); + writeSpace(); + emitList(node, node.types, ListFormat.HeritageClauseTypes); + } + function emitCatchClause(node: CatchClause) { + const openParenPos = emitTokenWithComment(SyntaxKind.CatchKeyword, node.pos, writeKeyword, node); + writeSpace(); + if (node.variableDeclaration) { + emitTokenWithComment(SyntaxKind.OpenParenToken, openParenPos, writePunctuation, node); + emit(node.variableDeclaration); + emitTokenWithComment(SyntaxKind.CloseParenToken, node.variableDeclaration.end, writePunctuation, node); + writeSpace(); } - - function emitJsxAttribute(node: JsxAttribute) { - emit(node.name); - emitNodeWithPrefix("=", writePunctuation, node.initializer, emitJsxAttributeValue); + emit(node.block); + } + // + // Property assignments + // + function emitPropertyAssignment(node: PropertyAssignment) { + emit(node.name); + writePunctuation(":"); + writeSpace(); + // This is to ensure that we emit comment in the following case: + // For example: + // obj = { + // id: /*comment1*/ ()=>void + // } + // "comment1" is not considered to be leading comment for node.initializer + // but rather a trailing comment on the previous node. + const initializer = node.initializer; + if (emitTrailingCommentsOfPosition && (getEmitFlags(initializer) & EmitFlags.NoLeadingComments) === 0) { + const commentRange = getCommentRange(initializer); + emitTrailingCommentsOfPosition(commentRange.pos); + } + emitExpression(initializer); + } + function emitShorthandPropertyAssignment(node: ShorthandPropertyAssignment) { + emit(node.name); + if (node.objectAssignmentInitializer) { + writeSpace(); + writePunctuation("="); + writeSpace(); + emitExpression(node.objectAssignmentInitializer); } - - function emitJsxSpreadAttribute(node: JsxSpreadAttribute) { - writePunctuation("{..."); + } + function emitSpreadAssignment(node: SpreadAssignment) { + if (node.expression) { + emitTokenWithComment(SyntaxKind.DotDotDotToken, node.pos, writePunctuation, node); emitExpression(node.expression); - writePunctuation("}"); } - - function emitJsxExpression(node: JsxExpression) { - if (node.expression) { - writePunctuation("{"); - emit(node.dotDotDotToken); - emitExpression(node.expression); - writePunctuation("}"); + } + // + // Enum + // + function emitEnumMember(node: EnumMember) { + emit(node.name); + emitInitializer(node.initializer, node.name.end, node); + } + // + // JSDoc + // + function emitJSDoc(node: JSDoc) { + write("/**"); + if (node.comment) { + const lines = node.comment.split(/\r\n?|\n/g); + for (const line of lines) { + writeLine(); + writeSpace(); + writePunctuation("*"); + writeSpace(); + write(line); } } - - function emitJsxTagName(node: JsxTagNameExpression) { - if (node.kind === SyntaxKind.Identifier) { - emitExpression(node); + if (node.tags) { + if (node.tags.length === 1 && node.tags[0].kind === SyntaxKind.JSDocTypeTag && !node.comment) { + writeSpace(); + emit(node.tags[0]); } else { - emit(node); + emitList(node, node.tags, ListFormat.JSDocComment); } } - - // - // Clauses - // - - function emitCaseClause(node: CaseClause) { - emitTokenWithComment(SyntaxKind.CaseKeyword, node.pos, writeKeyword, node); - writeSpace(); - emitExpression(node.expression); - - emitCaseOrDefaultClauseRest(node, node.statements, node.expression.end); - } - - function emitDefaultClause(node: DefaultClause) { - const pos = emitTokenWithComment(SyntaxKind.DefaultKeyword, node.pos, writeKeyword, node); - emitCaseOrDefaultClauseRest(node, node.statements, pos); - } - - function emitCaseOrDefaultClauseRest(parentNode: Node, statements: NodeArray, colonPos: number) { - const emitAsSingleStatement = - statements.length === 1 && - ( - // treat synthesized nodes as located on the same line for emit purposes - nodeIsSynthesized(parentNode) || - nodeIsSynthesized(statements[0]) || - rangeStartPositionsAreOnSameLine(parentNode, statements[0], currentSourceFile!) - ); - - let format = ListFormat.CaseOrDefaultClauseStatements; - if (emitAsSingleStatement) { - writeToken(SyntaxKind.ColonToken, colonPos, writePunctuation, parentNode); - writeSpace(); - format &= ~(ListFormat.MultiLine | ListFormat.Indented); + writeSpace(); + write("*/"); + } + function emitJSDocSimpleTypedTag(tag: JSDocTypeTag | JSDocThisTag | JSDocEnumTag | JSDocReturnTag) { + emitJSDocTagName(tag.tagName); + emitJSDocTypeExpression(tag.typeExpression); + emitJSDocComment(tag.comment); + } + function emitJSDocHeritageTag(tag: JSDocImplementsTag | JSDocAugmentsTag) { + emitJSDocTagName(tag.tagName); + writeSpace(); + writePunctuation("{"); + emit(tag.class); + writePunctuation("}"); + emitJSDocComment(tag.comment); + } + function emitJSDocTemplateTag(tag: JSDocTemplateTag) { + emitJSDocTagName(tag.tagName); + emitJSDocTypeExpression(tag.constraint); + writeSpace(); + emitList(tag, tag.typeParameters, ListFormat.CommaListElements); + emitJSDocComment(tag.comment); + } + function emitJSDocTypedefTag(tag: JSDocTypedefTag) { + emitJSDocTagName(tag.tagName); + if (tag.typeExpression) { + if (tag.typeExpression.kind === SyntaxKind.JSDocTypeExpression) { + emitJSDocTypeExpression(tag.typeExpression); } else { - emitTokenWithComment(SyntaxKind.ColonToken, colonPos, writePunctuation, parentNode); + writeSpace(); + writePunctuation("{"); + write("Object"); + if (tag.typeExpression.isArrayType) { + writePunctuation("["); + writePunctuation("]"); + } + writePunctuation("}"); } - emitList(parentNode, statements, format); } - - function emitHeritageClause(node: HeritageClause) { - writeSpace(); - writeTokenText(node.token, writeKeyword); + if (tag.fullName) { writeSpace(); - emitList(node, node.types, ListFormat.HeritageClauseTypes); + emit(tag.fullName); } - - function emitCatchClause(node: CatchClause) { - const openParenPos = emitTokenWithComment(SyntaxKind.CatchKeyword, node.pos, writeKeyword, node); - writeSpace(); - if (node.variableDeclaration) { - emitTokenWithComment(SyntaxKind.OpenParenToken, openParenPos, writePunctuation, node); - emit(node.variableDeclaration); - emitTokenWithComment(SyntaxKind.CloseParenToken, node.variableDeclaration.end, writePunctuation, node); - writeSpace(); - } - emit(node.block); + emitJSDocComment(tag.comment); + if (tag.typeExpression && tag.typeExpression.kind === SyntaxKind.JSDocTypeLiteral) { + emitJSDocTypeLiteral(tag.typeExpression); } - - // - // Property assignments - // - - function emitPropertyAssignment(node: PropertyAssignment) { - emit(node.name); - writePunctuation(":"); + } + function emitJSDocCallbackTag(tag: JSDocCallbackTag) { + emitJSDocTagName(tag.tagName); + if (tag.name) { writeSpace(); - // This is to ensure that we emit comment in the following case: - // For example: - // obj = { - // id: /*comment1*/ ()=>void - // } - // "comment1" is not considered to be leading comment for node.initializer - // but rather a trailing comment on the previous node. - const initializer = node.initializer; - if (emitTrailingCommentsOfPosition && (getEmitFlags(initializer) & EmitFlags.NoLeadingComments) === 0) { - const commentRange = getCommentRange(initializer); - emitTrailingCommentsOfPosition(commentRange.pos); - } - emitExpression(initializer); - } - - function emitShorthandPropertyAssignment(node: ShorthandPropertyAssignment) { - emit(node.name); - if (node.objectAssignmentInitializer) { - writeSpace(); - writePunctuation("="); - writeSpace(); - emitExpression(node.objectAssignmentInitializer); - } + emit(tag.name); } - - function emitSpreadAssignment(node: SpreadAssignment) { - if (node.expression) { - emitTokenWithComment(SyntaxKind.DotDotDotToken, node.pos, writePunctuation, node); - emitExpression(node.expression); - } + emitJSDocComment(tag.comment); + emitJSDocSignature(tag.typeExpression); + } + function emitJSDocSimpleTag(tag: JSDocTag) { + emitJSDocTagName(tag.tagName); + emitJSDocComment(tag.comment); + } + function emitJSDocTypeLiteral(lit: JSDocTypeLiteral) { + emitList(lit, createNodeArray(lit.jsDocPropertyTags), ListFormat.JSDocComment); + } + function emitJSDocSignature(sig: JSDocSignature) { + if (sig.typeParameters) { + emitList(sig, createNodeArray(sig.typeParameters), ListFormat.JSDocComment); } - - // - // Enum - // - - function emitEnumMember(node: EnumMember) { - emit(node.name); - emitInitializer(node.initializer, node.name.end, node); + if (sig.parameters) { + emitList(sig, createNodeArray(sig.parameters), ListFormat.JSDocComment); } - - // - // JSDoc - // - function emitJSDoc(node: JSDoc) { - write("/**"); - if (node.comment) { - const lines = node.comment.split(/\r\n?|\n/g); - for (const line of lines) { - writeLine(); - writeSpace(); - writePunctuation("*"); - writeSpace(); - write(line); - } - } - if (node.tags) { - if (node.tags.length === 1 && node.tags[0].kind === SyntaxKind.JSDocTypeTag && !node.comment) { - writeSpace(); - emit(node.tags[0]); - } - else { - emitList(node, node.tags, ListFormat.JSDocComment); - } - } + if (sig.type) { + writeLine(); + writeSpace(); + writePunctuation("*"); writeSpace(); - write("*/"); + emit(sig.type); + } + } + function emitJSDocPropertyLikeTag(param: JSDocPropertyLikeTag) { + emitJSDocTagName(param.tagName); + emitJSDocTypeExpression(param.typeExpression); + writeSpace(); + if (param.isBracketed) { + writePunctuation("["); + } + emit(param.name); + if (param.isBracketed) { + writePunctuation("]"); } - - function emitJSDocSimpleTypedTag(tag: JSDocTypeTag | JSDocThisTag | JSDocEnumTag | JSDocReturnTag) { - emitJSDocTagName(tag.tagName); - emitJSDocTypeExpression(tag.typeExpression); - emitJSDocComment(tag.comment); + emitJSDocComment(param.comment); + } + function emitJSDocTagName(tagName: Identifier) { + writePunctuation("@"); + emit(tagName); + } + function emitJSDocComment(comment: string | undefined) { + if (comment) { + writeSpace(); + write(comment); } - - function emitJSDocHeritageTag(tag: JSDocImplementsTag | JSDocAugmentsTag) { - emitJSDocTagName(tag.tagName); + } + function emitJSDocTypeExpression(typeExpression: JSDocTypeExpression | undefined) { + if (typeExpression) { writeSpace(); writePunctuation("{"); - emit(tag.class); + emit(typeExpression.type); writePunctuation("}"); - emitJSDocComment(tag.comment); } - - function emitJSDocTemplateTag(tag: JSDocTemplateTag) { - emitJSDocTagName(tag.tagName); - emitJSDocTypeExpression(tag.constraint); - writeSpace(); - emitList(tag, tag.typeParameters, ListFormat.CommaListElements); - emitJSDocComment(tag.comment); - } - - function emitJSDocTypedefTag(tag: JSDocTypedefTag) { - emitJSDocTagName(tag.tagName); - if (tag.typeExpression) { - if (tag.typeExpression.kind === SyntaxKind.JSDocTypeExpression) { - emitJSDocTypeExpression(tag.typeExpression); - } - else { - writeSpace(); - writePunctuation("{"); - write("Object"); - if (tag.typeExpression.isArrayType) { - writePunctuation("["); - writePunctuation("]"); - } - writePunctuation("}"); - } - } - if (tag.fullName) { - writeSpace(); - emit(tag.fullName); - } - emitJSDocComment(tag.comment); - if (tag.typeExpression && tag.typeExpression.kind === SyntaxKind.JSDocTypeLiteral) { - emitJSDocTypeLiteral(tag.typeExpression); + } + // + // Top-level nodes + // + function emitSourceFile(node: SourceFile) { + writeLine(); + const statements = node.statements; + if (emitBodyWithDetachedComments) { + // Emit detached comment if there are no prologue directives or if the first node is synthesized. + // The synthesized node will have no leading comment so some comments may be missed. + const shouldEmitDetachedComment = statements.length === 0 || + !isPrologueDirective(statements[0]) || + nodeIsSynthesized(statements[0]); + if (shouldEmitDetachedComment) { + emitBodyWithDetachedComments(node, statements, emitSourceFileWorker); + return; } } - - function emitJSDocCallbackTag(tag: JSDocCallbackTag) { - emitJSDocTagName(tag.tagName); - if (tag.name) { - writeSpace(); - emit(tag.name); - } - emitJSDocComment(tag.comment); - emitJSDocSignature(tag.typeExpression); - } - - function emitJSDocSimpleTag(tag: JSDocTag) { - emitJSDocTagName(tag.tagName); - emitJSDocComment(tag.comment); - } - - function emitJSDocTypeLiteral(lit: JSDocTypeLiteral) { - emitList(lit, createNodeArray(lit.jsDocPropertyTags), ListFormat.JSDocComment); - } - - function emitJSDocSignature(sig: JSDocSignature) { - if (sig.typeParameters) { - emitList(sig, createNodeArray(sig.typeParameters), ListFormat.JSDocComment); - } - if (sig.parameters) { - emitList(sig, createNodeArray(sig.parameters), ListFormat.JSDocComment); - } - if (sig.type) { - writeLine(); - writeSpace(); - writePunctuation("*"); - writeSpace(); - emit(sig.type); + emitSourceFileWorker(node); + } + function emitSyntheticTripleSlashReferencesIfNeeded(node: Bundle) { + emitTripleSlashDirectives(!!node.hasNoDefaultLib, node.syntheticFileReferences || [], node.syntheticTypeReferences || [], node.syntheticLibReferences || []); + for (const prepend of node.prepends) { + if (isUnparsedSource(prepend) && prepend.syntheticReferences) { + for (const ref of prepend.syntheticReferences) { + emit(ref); + writeLine(); + } } } - - function emitJSDocPropertyLikeTag(param: JSDocPropertyLikeTag) { - emitJSDocTagName(param.tagName); - emitJSDocTypeExpression(param.typeExpression); - writeSpace(); - if (param.isBracketed) { - writePunctuation("["); - } - emit(param.name); - if (param.isBracketed) { - writePunctuation("]"); - } - emitJSDocComment(param.comment); + } + function emitTripleSlashDirectivesIfNeeded(node: SourceFile) { + if (node.isDeclarationFile) + emitTripleSlashDirectives(node.hasNoDefaultLib, node.referencedFiles, node.typeReferenceDirectives, node.libReferenceDirectives); + } + function emitTripleSlashDirectives(hasNoDefaultLib: boolean, files: readonly FileReference[], types: readonly FileReference[], libs: readonly FileReference[]) { + if (hasNoDefaultLib) { + const pos = writer.getTextPos(); + writeComment(`/// `); + if (bundleFileInfo) + bundleFileInfo.sections.push({ pos, end: writer.getTextPos(), kind: BundleFileSectionKind.NoDefaultLib }); + writeLine(); } - - function emitJSDocTagName(tagName: Identifier) { - writePunctuation("@"); - emit(tagName); + if (currentSourceFile && currentSourceFile.moduleName) { + writeComment(`/// `); + writeLine(); } - - function emitJSDocComment(comment: string | undefined) { - if (comment) { - writeSpace(); - write(comment); + if (currentSourceFile && currentSourceFile.amdDependencies) { + for (const dep of currentSourceFile.amdDependencies) { + if (dep.name) { + writeComment(`/// `); + } + else { + writeComment(`/// `); + } + writeLine(); } } - - function emitJSDocTypeExpression(typeExpression: JSDocTypeExpression | undefined) { - if (typeExpression) { - writeSpace(); - writePunctuation("{"); - emit(typeExpression.type); - writePunctuation("}"); - } + for (const directive of files) { + const pos = writer.getTextPos(); + writeComment(`/// `); + if (bundleFileInfo) + bundleFileInfo.sections.push({ pos, end: writer.getTextPos(), kind: BundleFileSectionKind.Reference, data: directive.fileName }); + writeLine(); } - - // - // Top-level nodes - // - - function emitSourceFile(node: SourceFile) { + for (const directive of types) { + const pos = writer.getTextPos(); + writeComment(`/// `); + if (bundleFileInfo) + bundleFileInfo.sections.push({ pos, end: writer.getTextPos(), kind: BundleFileSectionKind.Type, data: directive.fileName }); writeLine(); - const statements = node.statements; - if (emitBodyWithDetachedComments) { - // Emit detached comment if there are no prologue directives or if the first node is synthesized. - // The synthesized node will have no leading comment so some comments may be missed. - const shouldEmitDetachedComment = statements.length === 0 || - !isPrologueDirective(statements[0]) || - nodeIsSynthesized(statements[0]); - if (shouldEmitDetachedComment) { - emitBodyWithDetachedComments(node, statements, emitSourceFileWorker); - return; - } - } - emitSourceFileWorker(node); - } - - function emitSyntheticTripleSlashReferencesIfNeeded(node: Bundle) { - emitTripleSlashDirectives(!!node.hasNoDefaultLib, node.syntheticFileReferences || [], node.syntheticTypeReferences || [], node.syntheticLibReferences || []); - for (const prepend of node.prepends) { - if (isUnparsedSource(prepend) && prepend.syntheticReferences) { - for (const ref of prepend.syntheticReferences) { - emit(ref); - writeLine(); - } - } - } } - - function emitTripleSlashDirectivesIfNeeded(node: SourceFile) { - if (node.isDeclarationFile) emitTripleSlashDirectives(node.hasNoDefaultLib, node.referencedFiles, node.typeReferenceDirectives, node.libReferenceDirectives); + for (const directive of libs) { + const pos = writer.getTextPos(); + writeComment(`/// `); + if (bundleFileInfo) + bundleFileInfo.sections.push({ pos, end: writer.getTextPos(), kind: BundleFileSectionKind.Lib, data: directive.fileName }); + writeLine(); } - - function emitTripleSlashDirectives(hasNoDefaultLib: boolean, files: readonly FileReference[], types: readonly FileReference[], libs: readonly FileReference[]) { - if (hasNoDefaultLib) { - const pos = writer.getTextPos(); - writeComment(`/// `); - if (bundleFileInfo) bundleFileInfo.sections.push({ pos, end: writer.getTextPos(), kind: BundleFileSectionKind.NoDefaultLib }); - writeLine(); - } - if (currentSourceFile && currentSourceFile.moduleName) { - writeComment(`/// `); - writeLine(); - } - if (currentSourceFile && currentSourceFile.amdDependencies) { - for (const dep of currentSourceFile.amdDependencies) { - if (dep.name) { - writeComment(`/// `); - } - else { - writeComment(`/// `); + } + function emitSourceFileWorker(node: SourceFile) { + const statements = node.statements; + pushNameGenerationScope(node); + forEach(node.statements, generateNames); + emitHelpers(node); + const index = findIndex(statements, statement => !isPrologueDirective(statement)); + emitTripleSlashDirectivesIfNeeded(node); + emitList(node, statements, ListFormat.MultiLine, index === -1 ? statements.length : index); + popNameGenerationScope(node); + } + // Transformation nodes + function emitPartiallyEmittedExpression(node: PartiallyEmittedExpression) { + emitExpression(node.expression); + } + function emitCommaList(node: CommaListExpression) { + emitExpressionList(node, node.elements, ListFormat.CommaListElements); + } + /** + * Emits any prologue directives at the start of a Statement list, returning the + * number of prologue directives written to the output. + */ + function emitPrologueDirectives(statements: readonly Node[], sourceFile?: SourceFile, seenPrologueDirectives?: ts.Map, recordBundleFileSection?: true): number { + let needsToSetSourceFile = !!sourceFile; + for (let i = 0; i < statements.length; i++) { + const statement = statements[i]; + if (isPrologueDirective(statement)) { + const shouldEmitPrologueDirective = seenPrologueDirectives ? !seenPrologueDirectives.has(statement.expression.text) : true; + if (shouldEmitPrologueDirective) { + if (needsToSetSourceFile) { + needsToSetSourceFile = false; + setSourceFile(sourceFile); } writeLine(); + const pos = writer.getTextPos(); + emit(statement); + if (recordBundleFileSection && bundleFileInfo) + bundleFileInfo.sections.push({ pos, end: writer.getTextPos(), kind: BundleFileSectionKind.Prologue, data: statement.expression.text }); + if (seenPrologueDirectives) { + seenPrologueDirectives.set(statement.expression.text, true); + } } } - for (const directive of files) { - const pos = writer.getTextPos(); - writeComment(`/// `); - if (bundleFileInfo) bundleFileInfo.sections.push({ pos, end: writer.getTextPos(), kind: BundleFileSectionKind.Reference, data: directive.fileName }); - writeLine(); + else { + // return index of the first non prologue directive + return i; } - for (const directive of types) { - const pos = writer.getTextPos(); - writeComment(`/// `); - if (bundleFileInfo) bundleFileInfo.sections.push({ pos, end: writer.getTextPos(), kind: BundleFileSectionKind.Type, data: directive.fileName }); + } + return statements.length; + } + function emitUnparsedPrologues(prologues: readonly UnparsedPrologue[], seenPrologueDirectives: ts.Map) { + for (const prologue of prologues) { + if (!seenPrologueDirectives.has(prologue.data)) { writeLine(); - } - for (const directive of libs) { const pos = writer.getTextPos(); - writeComment(`/// `); - if (bundleFileInfo) bundleFileInfo.sections.push({ pos, end: writer.getTextPos(), kind: BundleFileSectionKind.Lib, data: directive.fileName }); - writeLine(); + emit(prologue); + if (bundleFileInfo) + bundleFileInfo.sections.push({ pos, end: writer.getTextPos(), kind: BundleFileSectionKind.Prologue, data: prologue.data }); + if (seenPrologueDirectives) { + seenPrologueDirectives.set(prologue.data, true); + } } } - - function emitSourceFileWorker(node: SourceFile) { - const statements = node.statements; - pushNameGenerationScope(node); - forEach(node.statements, generateNames); - emitHelpers(node); - const index = findIndex(statements, statement => !isPrologueDirective(statement)); - emitTripleSlashDirectivesIfNeeded(node); - emitList(node, statements, ListFormat.MultiLine, index === -1 ? statements.length : index); - popNameGenerationScope(node); - } - - // Transformation nodes - - function emitPartiallyEmittedExpression(node: PartiallyEmittedExpression) { - emitExpression(node.expression); + } + function emitPrologueDirectivesIfNeeded(sourceFileOrBundle: Bundle | SourceFile) { + if (isSourceFile(sourceFileOrBundle)) { + emitPrologueDirectives(sourceFileOrBundle.statements, sourceFileOrBundle); } - - function emitCommaList(node: CommaListExpression) { - emitExpressionList(node, node.elements, ListFormat.CommaListElements); - } - - /** - * Emits any prologue directives at the start of a Statement list, returning the - * number of prologue directives written to the output. - */ - function emitPrologueDirectives(statements: readonly Node[], sourceFile?: SourceFile, seenPrologueDirectives?: Map, recordBundleFileSection?: true): number { - let needsToSetSourceFile = !!sourceFile; - for (let i = 0; i < statements.length; i++) { - const statement = statements[i]; - if (isPrologueDirective(statement)) { - const shouldEmitPrologueDirective = seenPrologueDirectives ? !seenPrologueDirectives.has(statement.expression.text) : true; - if (shouldEmitPrologueDirective) { - if (needsToSetSourceFile) { - needsToSetSourceFile = false; - setSourceFile(sourceFile); - } - writeLine(); - const pos = writer.getTextPos(); - emit(statement); - if (recordBundleFileSection && bundleFileInfo) bundleFileInfo.sections.push({ pos, end: writer.getTextPos(), kind: BundleFileSectionKind.Prologue, data: statement.expression.text }); - if (seenPrologueDirectives) { - seenPrologueDirectives.set(statement.expression.text, true); - } - } - } - else { - // return index of the first non prologue directive - return i; - } + else { + const seenPrologueDirectives = createMap(); + for (const prepend of sourceFileOrBundle.prepends) { + emitUnparsedPrologues((prepend as UnparsedSource).prologues, seenPrologueDirectives); + } + for (const sourceFile of sourceFileOrBundle.sourceFiles) { + emitPrologueDirectives(sourceFile.statements, sourceFile, seenPrologueDirectives, /*recordBundleFileSection*/ true); } - - return statements.length; + setSourceFile(undefined); } - - function emitUnparsedPrologues(prologues: readonly UnparsedPrologue[], seenPrologueDirectives: Map) { - for (const prologue of prologues) { - if (!seenPrologueDirectives.has(prologue.data)) { - writeLine(); - const pos = writer.getTextPos(); - emit(prologue); - if (bundleFileInfo) bundleFileInfo.sections.push({ pos, end: writer.getTextPos(), kind: BundleFileSectionKind.Prologue, data: prologue.data }); - if (seenPrologueDirectives) { - seenPrologueDirectives.set(prologue.data, true); + } + function getPrologueDirectivesFromBundledSourceFiles(bundle: Bundle): SourceFilePrologueInfo[] | undefined { + const seenPrologueDirectives = createMap(); + let prologues: SourceFilePrologueInfo[] | undefined; + for (let index = 0; index < bundle.sourceFiles.length; index++) { + const sourceFile = bundle.sourceFiles[index]; + let directives: SourceFilePrologueDirective[] | undefined; + let end = 0; + for (const statement of sourceFile.statements) { + if (!isPrologueDirective(statement)) + break; + if (seenPrologueDirectives.has(statement.expression.text)) + continue; + seenPrologueDirectives.set(statement.expression.text, true); + (directives || (directives = [])).push({ + pos: statement.pos, + end: statement.end, + expression: { + pos: statement.expression.pos, + end: statement.expression.end, + text: statement.expression.text } - } + }); + end = end < statement.end ? statement.end : end; } + if (directives) + (prologues || (prologues = [])).push({ file: index, text: sourceFile.text.substring(0, end), directives }); } - - function emitPrologueDirectivesIfNeeded(sourceFileOrBundle: Bundle | SourceFile) { - if (isSourceFile(sourceFileOrBundle)) { - emitPrologueDirectives(sourceFileOrBundle.statements, sourceFileOrBundle); - } - else { - const seenPrologueDirectives = createMap(); - for (const prepend of sourceFileOrBundle.prepends) { - emitUnparsedPrologues((prepend as UnparsedSource).prologues, seenPrologueDirectives); - } - for (const sourceFile of sourceFileOrBundle.sourceFiles) { - emitPrologueDirectives(sourceFile.statements, sourceFile, seenPrologueDirectives, /*recordBundleFileSection*/ true); - } - setSourceFile(undefined); + return prologues; + } + function emitShebangIfNeeded(sourceFileOrBundle: Bundle | SourceFile | UnparsedSource) { + if (isSourceFile(sourceFileOrBundle) || isUnparsedSource(sourceFileOrBundle)) { + const shebang = getShebang(sourceFileOrBundle.text); + if (shebang) { + writeComment(shebang); + writeLine(); + return true; } } - - function getPrologueDirectivesFromBundledSourceFiles(bundle: Bundle): SourceFilePrologueInfo[] | undefined { - const seenPrologueDirectives = createMap(); - let prologues: SourceFilePrologueInfo[] | undefined; - for (let index = 0; index < bundle.sourceFiles.length; index++) { - const sourceFile = bundle.sourceFiles[index]; - let directives: SourceFilePrologueDirective[] | undefined; - let end = 0; - for (const statement of sourceFile.statements) { - if (!isPrologueDirective(statement)) break; - if (seenPrologueDirectives.has(statement.expression.text)) continue; - seenPrologueDirectives.set(statement.expression.text, true); - (directives || (directives = [])).push({ - pos: statement.pos, - end: statement.end, - expression: { - pos: statement.expression.pos, - end: statement.expression.end, - text: statement.expression.text - } - }); - end = end < statement.end ? statement.end : end; - } - if (directives) (prologues || (prologues = [])).push({ file: index, text: sourceFile.text.substring(0, end), directives }); - } - return prologues; - } - - function emitShebangIfNeeded(sourceFileOrBundle: Bundle | SourceFile | UnparsedSource) { - if (isSourceFile(sourceFileOrBundle) || isUnparsedSource(sourceFileOrBundle)) { - const shebang = getShebang(sourceFileOrBundle.text); - if (shebang) { - writeComment(shebang); - writeLine(); + else { + for (const prepend of sourceFileOrBundle.prepends) { + Debug.assertNode(prepend, isUnparsedSource); + if (emitShebangIfNeeded(prepend)) { return true; } } - else { - for (const prepend of sourceFileOrBundle.prepends) { - Debug.assertNode(prepend, isUnparsedSource); - if (emitShebangIfNeeded(prepend)) { - return true; - } - } - for (const sourceFile of sourceFileOrBundle.sourceFiles) { - // Emit only the first encountered shebang - if (emitShebangIfNeeded(sourceFile)) { - return true; - } + for (const sourceFile of sourceFileOrBundle.sourceFiles) { + // Emit only the first encountered shebang + if (emitShebangIfNeeded(sourceFile)) { + return true; } } } - - // - // Helpers - // - - function emitNodeWithWriter(node: Node | undefined, writer: typeof write) { - if (!node) return; - const savedWrite = write; - write = writer; - emit(node); - write = savedWrite; + } + // + // Helpers + // + function emitNodeWithWriter(node: Node | undefined, writer: typeof write) { + if (!node) + return; + const savedWrite = write; + write = writer; + emit(node); + write = savedWrite; + } + function emitModifiers(node: Node, modifiers: NodeArray | undefined) { + if (modifiers && modifiers.length) { + emitList(node, modifiers, ListFormat.Modifiers); + writeSpace(); } - - function emitModifiers(node: Node, modifiers: NodeArray | undefined) { - if (modifiers && modifiers.length) { - emitList(node, modifiers, ListFormat.Modifiers); - writeSpace(); - } + } + function emitTypeAnnotation(node: TypeNode | undefined) { + if (node) { + writePunctuation(":"); + writeSpace(); + emit(node); } - - function emitTypeAnnotation(node: TypeNode | undefined) { - if (node) { - writePunctuation(":"); - writeSpace(); - emit(node); - } + } + function emitInitializer(node: Expression | undefined, equalCommentStartPos: number, container: Node) { + if (node) { + writeSpace(); + emitTokenWithComment(SyntaxKind.EqualsToken, equalCommentStartPos, writeOperator, container); + writeSpace(); + emitExpression(node); } - - function emitInitializer(node: Expression | undefined, equalCommentStartPos: number, container: Node) { - if (node) { - writeSpace(); - emitTokenWithComment(SyntaxKind.EqualsToken, equalCommentStartPos, writeOperator, container); - writeSpace(); - emitExpression(node); - } + } + function emitNodeWithPrefix(prefix: string, prefixWriter: (s: string) => void, node: T | undefined, emit: (node: T) => void) { + if (node) { + prefixWriter(prefix); + emit(node); } - - function emitNodeWithPrefix(prefix: string, prefixWriter: (s: string) => void, node: T | undefined, emit: (node: T) => void) { - if (node) { - prefixWriter(prefix); - emit(node); - } + } + function emitWithLeadingSpace(node: Node | undefined) { + if (node) { + writeSpace(); + emit(node); } - - function emitWithLeadingSpace(node: Node | undefined) { - if (node) { - writeSpace(); - emit(node); - } + } + function emitExpressionWithLeadingSpace(node: Expression | undefined) { + if (node) { + writeSpace(); + emitExpression(node); } - - function emitExpressionWithLeadingSpace(node: Expression | undefined) { - if (node) { - writeSpace(); - emitExpression(node); - } + } + function emitWithTrailingSpace(node: Node | undefined) { + if (node) { + emit(node); + writeSpace(); } - - function emitWithTrailingSpace(node: Node | undefined) { - if (node) { - emit(node); - writeSpace(); - } + } + function emitEmbeddedStatement(parent: Node, node: Statement) { + if (isBlock(node) || getEmitFlags(parent) & EmitFlags.SingleLine) { + writeSpace(); + emit(node); } - - function emitEmbeddedStatement(parent: Node, node: Statement) { - if (isBlock(node) || getEmitFlags(parent) & EmitFlags.SingleLine) { - writeSpace(); - emit(node); + else { + writeLine(); + increaseIndent(); + if (isEmptyStatement(node)) { + pipelineEmit(EmitHint.EmbeddedStatement, node); } else { - writeLine(); - increaseIndent(); - if (isEmptyStatement(node)) { - pipelineEmit(EmitHint.EmbeddedStatement, node); - } - else { - emit(node); - } - decreaseIndent(); + emit(node); } + decreaseIndent(); + } + } + function emitDecorators(parentNode: Node, decorators: NodeArray | undefined) { + emitList(parentNode, decorators, ListFormat.Decorators); + } + function emitTypeArguments(parentNode: Node, typeArguments: NodeArray | undefined) { + emitList(parentNode, typeArguments, ListFormat.TypeArguments); + } + function emitTypeParameters(parentNode: SignatureDeclaration | InterfaceDeclaration | TypeAliasDeclaration | ClassDeclaration | ClassExpression, typeParameters: NodeArray | undefined) { + if (isFunctionLike(parentNode) && parentNode.typeArguments) { // Quick info uses type arguments in place of type parameters on instantiated signatures + return emitTypeArguments(parentNode, parentNode.typeArguments); + } + emitList(parentNode, typeParameters, ListFormat.TypeParameters); + } + function emitParameters(parentNode: Node, parameters: NodeArray) { + emitList(parentNode, parameters, ListFormat.Parameters); + } + function canEmitSimpleArrowHead(parentNode: FunctionTypeNode | ArrowFunction, parameters: NodeArray) { + const parameter = singleOrUndefined(parameters); + return parameter + && parameter.pos === parentNode.pos // may not have parsed tokens between parent and parameter + && isArrowFunction(parentNode) // only arrow functions may have simple arrow head + && !parentNode.type // arrow function may not have return type annotation + && !some(parentNode.decorators) // parent may not have decorators + && !some(parentNode.modifiers) // parent may not have modifiers + && !some(parentNode.typeParameters) // parent may not have type parameters + && !some(parameter.decorators) // parameter may not have decorators + && !some(parameter.modifiers) // parameter may not have modifiers + && !parameter.dotDotDotToken // parameter may not be rest + && !parameter.questionToken // parameter may not be optional + && !parameter.type // parameter may not have a type annotation + && !parameter.initializer // parameter may not have an initializer + && isIdentifier(parameter.name); // parameter name must be identifier + } + function emitParametersForArrow(parentNode: FunctionTypeNode | ArrowFunction, parameters: NodeArray) { + if (canEmitSimpleArrowHead(parentNode, parameters)) { + emitList(parentNode, parameters, ListFormat.Parameters & ~ListFormat.Parenthesis); + } + else { + emitParameters(parentNode, parameters); } - - function emitDecorators(parentNode: Node, decorators: NodeArray | undefined) { - emitList(parentNode, decorators, ListFormat.Decorators); + } + function emitParametersForIndexSignature(parentNode: Node, parameters: NodeArray) { + emitList(parentNode, parameters, ListFormat.IndexSignatureParameters); + } + function emitList(parentNode: TextRange, children: NodeArray | undefined, format: ListFormat, start?: number, count?: number) { + emitNodeList(emit, parentNode, children, format, start, count); + } + function emitExpressionList(parentNode: TextRange, children: NodeArray | undefined, format: ListFormat, start?: number, count?: number) { + emitNodeList((emitExpression as (node: Node) => void), parentNode, children, format, start, count); // TODO: GH#18217 + } + function writeDelimiter(format: ListFormat) { + switch (format & ListFormat.DelimitersMask) { + case ListFormat.None: + break; + case ListFormat.CommaDelimited: + writePunctuation(","); + break; + case ListFormat.BarDelimited: + writeSpace(); + writePunctuation("|"); + break; + case ListFormat.AsteriskDelimited: + writeSpace(); + writePunctuation("*"); + writeSpace(); + break; + case ListFormat.AmpersandDelimited: + writeSpace(); + writePunctuation("&"); + break; } - - function emitTypeArguments(parentNode: Node, typeArguments: NodeArray | undefined) { - emitList(parentNode, typeArguments, ListFormat.TypeArguments); + } + function emitNodeList(emit: (node: Node) => void, parentNode: TextRange, children: NodeArray | undefined, format: ListFormat, start = 0, count = children ? children.length - start : 0) { + const isUndefined = children === undefined; + if (isUndefined && format & ListFormat.OptionalIfUndefined) { + return; } - - function emitTypeParameters(parentNode: SignatureDeclaration | InterfaceDeclaration | TypeAliasDeclaration | ClassDeclaration | ClassExpression, typeParameters: NodeArray | undefined) { - if (isFunctionLike(parentNode) && parentNode.typeArguments) { // Quick info uses type arguments in place of type parameters on instantiated signatures - return emitTypeArguments(parentNode, parentNode.typeArguments); - } - emitList(parentNode, typeParameters, ListFormat.TypeParameters); - } - - function emitParameters(parentNode: Node, parameters: NodeArray) { - emitList(parentNode, parameters, ListFormat.Parameters); - } - - function canEmitSimpleArrowHead(parentNode: FunctionTypeNode | ArrowFunction, parameters: NodeArray) { - const parameter = singleOrUndefined(parameters); - return parameter - && parameter.pos === parentNode.pos // may not have parsed tokens between parent and parameter - && isArrowFunction(parentNode) // only arrow functions may have simple arrow head - && !parentNode.type // arrow function may not have return type annotation - && !some(parentNode.decorators) // parent may not have decorators - && !some(parentNode.modifiers) // parent may not have modifiers - && !some(parentNode.typeParameters) // parent may not have type parameters - && !some(parameter.decorators) // parameter may not have decorators - && !some(parameter.modifiers) // parameter may not have modifiers - && !parameter.dotDotDotToken // parameter may not be rest - && !parameter.questionToken // parameter may not be optional - && !parameter.type // parameter may not have a type annotation - && !parameter.initializer // parameter may not have an initializer - && isIdentifier(parameter.name); // parameter name must be identifier - } - - function emitParametersForArrow(parentNode: FunctionTypeNode | ArrowFunction, parameters: NodeArray) { - if (canEmitSimpleArrowHead(parentNode, parameters)) { - emitList(parentNode, parameters, ListFormat.Parameters & ~ListFormat.Parenthesis); + const isEmpty = children === undefined || start >= children.length || count === 0; + if (isEmpty && format & ListFormat.OptionalIfEmpty) { + if (onBeforeEmitNodeArray) { + onBeforeEmitNodeArray(children); } - else { - emitParameters(parentNode, parameters); + if (onAfterEmitNodeArray) { + onAfterEmitNodeArray(children); } + return; } - - function emitParametersForIndexSignature(parentNode: Node, parameters: NodeArray) { - emitList(parentNode, parameters, ListFormat.IndexSignatureParameters); - } - - function emitList(parentNode: TextRange, children: NodeArray | undefined, format: ListFormat, start?: number, count?: number) { - emitNodeList(emit, parentNode, children, format, start, count); - } - - function emitExpressionList(parentNode: TextRange, children: NodeArray | undefined, format: ListFormat, start?: number, count?: number) { - emitNodeList(emitExpression as (node: Node) => void, parentNode, children, format, start, count); // TODO: GH#18217 - } - - function writeDelimiter(format: ListFormat) { - switch (format & ListFormat.DelimitersMask) { - case ListFormat.None: - break; - case ListFormat.CommaDelimited: - writePunctuation(","); - break; - case ListFormat.BarDelimited: - writeSpace(); - writePunctuation("|"); - break; - case ListFormat.AsteriskDelimited: - writeSpace(); - writePunctuation("*"); - writeSpace(); - break; - case ListFormat.AmpersandDelimited: - writeSpace(); - writePunctuation("&"); - break; + if (format & ListFormat.BracketsMask) { + writePunctuation(getOpeningBracket(format)); + if (isEmpty && !isUndefined) { + // TODO: GH#18217 + emitTrailingCommentsOfPosition(children!.pos, /*prefixSpace*/ true); // Emit comments within empty bracketed lists } } - - function emitNodeList(emit: (node: Node) => void, parentNode: TextRange, children: NodeArray | undefined, format: ListFormat, start = 0, count = children ? children.length - start : 0) { - const isUndefined = children === undefined; - if (isUndefined && format & ListFormat.OptionalIfUndefined) { - return; + if (onBeforeEmitNodeArray) { + onBeforeEmitNodeArray(children); + } + if (isEmpty) { + // Write a line terminator if the parent node was multi-line + if (format & ListFormat.MultiLine) { + writeLine(); } - - const isEmpty = children === undefined || start >= children.length || count === 0; - if (isEmpty && format & ListFormat.OptionalIfEmpty) { - if (onBeforeEmitNodeArray) { - onBeforeEmitNodeArray(children); - } - if (onAfterEmitNodeArray) { - onAfterEmitNodeArray(children); - } - return; + else if (format & ListFormat.SpaceBetweenBraces && !(format & ListFormat.NoSpaceIfEmpty)) { + writeSpace(); } - - if (format & ListFormat.BracketsMask) { - writePunctuation(getOpeningBracket(format)); - if (isEmpty && !isUndefined) { - // TODO: GH#18217 - emitTrailingCommentsOfPosition(children!.pos, /*prefixSpace*/ true); // Emit comments within empty bracketed lists - } + } + else { + // Write the opening line terminator or leading whitespace. + const mayEmitInterveningComments = (format & ListFormat.NoInterveningComments) === 0; + let shouldEmitInterveningComments = mayEmitInterveningComments; + if (shouldWriteLeadingLineTerminator(parentNode, children!, format)) { // TODO: GH#18217 + writeLine(); + shouldEmitInterveningComments = false; } - - if (onBeforeEmitNodeArray) { - onBeforeEmitNodeArray(children); + else if (format & ListFormat.SpaceBetweenBraces) { + writeSpace(); } - - if (isEmpty) { - // Write a line terminator if the parent node was multi-line - if (format & ListFormat.MultiLine) { - writeLine(); - } - else if (format & ListFormat.SpaceBetweenBraces && !(format & ListFormat.NoSpaceIfEmpty)) { - writeSpace(); - } + // Increase the indent, if requested. + if (format & ListFormat.Indented) { + increaseIndent(); } - else { - // Write the opening line terminator or leading whitespace. - const mayEmitInterveningComments = (format & ListFormat.NoInterveningComments) === 0; - let shouldEmitInterveningComments = mayEmitInterveningComments; - if (shouldWriteLeadingLineTerminator(parentNode, children!, format)) { // TODO: GH#18217 + // Emit each child. + let previousSibling: Node | undefined; + let previousSourceFileTextKind: ReturnType; + let shouldDecreaseIndentAfterEmit = false; + for (let i = 0; i < count; i++) { + const child = children![start + i]; + // Write the delimiter if this is not the first node. + if (format & ListFormat.AsteriskDelimited) { + // always write JSDoc in the format "\n *" writeLine(); - shouldEmitInterveningComments = false; - } - else if (format & ListFormat.SpaceBetweenBraces) { - writeSpace(); - } - - // Increase the indent, if requested. - if (format & ListFormat.Indented) { - increaseIndent(); + writeDelimiter(format); } - - // Emit each child. - let previousSibling: Node | undefined; - let previousSourceFileTextKind: ReturnType; - let shouldDecreaseIndentAfterEmit = false; - for (let i = 0; i < count; i++) { - const child = children![start + i]; - - // Write the delimiter if this is not the first node. - if (format & ListFormat.AsteriskDelimited) { - // always write JSDoc in the format "\n *" - writeLine(); - writeDelimiter(format); - } - else if (previousSibling) { - // i.e - // function commentedParameters( - // /* Parameter a */ - // a - // /* End of parameter a */ -> this comment isn't considered to be trailing comment of parameter "a" due to newline - // , - if (format & ListFormat.DelimitersMask && previousSibling.end !== parentNode.end) { - emitLeadingCommentsOfPosition(previousSibling.end); - } - writeDelimiter(format); - recordBundleFileInternalSectionEnd(previousSourceFileTextKind); - - // Write either a line terminator or whitespace to separate the elements. - if (shouldWriteSeparatingLineTerminator(previousSibling, child, format)) { - // If a synthesized node in a single-line list starts on a new - // line, we should increase the indent. - if ((format & (ListFormat.LinesMask | ListFormat.Indented)) === ListFormat.SingleLine) { - increaseIndent(); - shouldDecreaseIndentAfterEmit = true; - } - - writeLine(); - shouldEmitInterveningComments = false; - } - else if (previousSibling && format & ListFormat.SpaceBetweenSiblings) { - writeSpace(); - } + else if (previousSibling) { + // i.e + // function commentedParameters( + // /* Parameter a */ + // a + // /* End of parameter a */ -> this comment isn't considered to be trailing comment of parameter "a" due to newline + // , + if (format & ListFormat.DelimitersMask && previousSibling.end !== parentNode.end) { + emitLeadingCommentsOfPosition(previousSibling.end); } - - // Emit this child. - previousSourceFileTextKind = recordBundleFileInternalSectionStart(child); - if (shouldEmitInterveningComments) { - if (emitTrailingCommentsOfPosition) { - const commentRange = getCommentRange(child); - emitTrailingCommentsOfPosition(commentRange.pos); + writeDelimiter(format); + recordBundleFileInternalSectionEnd(previousSourceFileTextKind); + // Write either a line terminator or whitespace to separate the elements. + if (shouldWriteSeparatingLineTerminator(previousSibling, child, format)) { + // If a synthesized node in a single-line list starts on a new + // line, we should increase the indent. + if ((format & (ListFormat.LinesMask | ListFormat.Indented)) === ListFormat.SingleLine) { + increaseIndent(); + shouldDecreaseIndentAfterEmit = true; } + writeLine(); + shouldEmitInterveningComments = false; } - else { - shouldEmitInterveningComments = mayEmitInterveningComments; - } - - emit(child); - - if (shouldDecreaseIndentAfterEmit) { - decreaseIndent(); - shouldDecreaseIndentAfterEmit = false; + else if (previousSibling && format & ListFormat.SpaceBetweenSiblings) { + writeSpace(); } - - previousSibling = child; } - - // Write a trailing comma, if requested. - const hasTrailingComma = (format & ListFormat.AllowTrailingComma) && children!.hasTrailingComma; - if (format & ListFormat.CommaDelimited && hasTrailingComma) { - writePunctuation(","); + // Emit this child. + previousSourceFileTextKind = recordBundleFileInternalSectionStart(child); + if (shouldEmitInterveningComments) { + if (emitTrailingCommentsOfPosition) { + const commentRange = getCommentRange(child); + emitTrailingCommentsOfPosition(commentRange.pos); + } } - - - // Emit any trailing comment of the last element in the list - // i.e - // var array = [... - // 2 - // /* end of element 2 */ - // ]; - if (previousSibling && format & ListFormat.DelimitersMask && previousSibling.end !== parentNode.end && !(getEmitFlags(previousSibling) & EmitFlags.NoTrailingComments)) { - emitLeadingCommentsOfPosition(previousSibling.end); + else { + shouldEmitInterveningComments = mayEmitInterveningComments; } - - // Decrease the indent, if requested. - if (format & ListFormat.Indented) { + emit(child); + if (shouldDecreaseIndentAfterEmit) { decreaseIndent(); + shouldDecreaseIndentAfterEmit = false; } - - recordBundleFileInternalSectionEnd(previousSourceFileTextKind); - - // Write the closing line terminator or closing whitespace. - if (shouldWriteClosingLineTerminator(parentNode, children!, format)) { - writeLine(); - } - else if (format & ListFormat.SpaceBetweenBraces) { - writeSpace(); - } + previousSibling = child; + } + // Write a trailing comma, if requested. + const hasTrailingComma = (format & ListFormat.AllowTrailingComma) && children!.hasTrailingComma; + if (format & ListFormat.CommaDelimited && hasTrailingComma) { + writePunctuation(","); + } + // Emit any trailing comment of the last element in the list + // i.e + // var array = [... + // 2 + // /* end of element 2 */ + // ]; + if (previousSibling && format & ListFormat.DelimitersMask && previousSibling.end !== parentNode.end && !(getEmitFlags(previousSibling) & EmitFlags.NoTrailingComments)) { + emitLeadingCommentsOfPosition(previousSibling.end); + } + // Decrease the indent, if requested. + if (format & ListFormat.Indented) { + decreaseIndent(); } - - if (onAfterEmitNodeArray) { - onAfterEmitNodeArray(children); + recordBundleFileInternalSectionEnd(previousSourceFileTextKind); + // Write the closing line terminator or closing whitespace. + if (shouldWriteClosingLineTerminator(parentNode, children!, format)) { + writeLine(); } - - if (format & ListFormat.BracketsMask) { - if (isEmpty && !isUndefined) { - // TODO: GH#18217 - emitLeadingCommentsOfPosition(children!.end); // Emit leading comments within empty lists - } - writePunctuation(getClosingBracket(format)); + else if (format & ListFormat.SpaceBetweenBraces) { + writeSpace(); } } - - // Writers - - function writeLiteral(s: string) { - writer.writeLiteral(s); + if (onAfterEmitNodeArray) { + onAfterEmitNodeArray(children); } - - function writeStringLiteral(s: string) { - writer.writeStringLiteral(s); - } - - function writeBase(s: string) { - writer.write(s); - } - - function writeSymbol(s: string, sym: Symbol) { - writer.writeSymbol(s, sym); - } - - function writePunctuation(s: string) { - writer.writePunctuation(s); - } - - function writeTrailingSemicolon() { - writer.writeTrailingSemicolon(";"); + if (format & ListFormat.BracketsMask) { + if (isEmpty && !isUndefined) { + // TODO: GH#18217 + emitLeadingCommentsOfPosition(children!.end); // Emit leading comments within empty lists + } + writePunctuation(getClosingBracket(format)); } - - function writeKeyword(s: string) { - writer.writeKeyword(s); + } + // Writers + function writeLiteral(s: string) { + writer.writeLiteral(s); + } + function writeStringLiteral(s: string) { + writer.writeStringLiteral(s); + } + function writeBase(s: string) { + writer.write(s); + } + function writeSymbol(s: string, sym: Symbol) { + writer.writeSymbol(s, sym); + } + function writePunctuation(s: string) { + writer.writePunctuation(s); + } + function writeTrailingSemicolon() { + writer.writeTrailingSemicolon(";"); + } + function writeKeyword(s: string) { + writer.writeKeyword(s); + } + function writeOperator(s: string) { + writer.writeOperator(s); + } + function writeParameter(s: string) { + writer.writeParameter(s); + } + function writeComment(s: string) { + writer.writeComment(s); + } + function writeSpace() { + writer.writeSpace(" "); + } + function writeProperty(s: string) { + writer.writeProperty(s); + } + function writeLine() { + writer.writeLine(); + } + function increaseIndent() { + writer.increaseIndent(); + } + function decreaseIndent() { + writer.decreaseIndent(); + } + function writeToken(token: SyntaxKind, pos: number, writer: (s: string) => void, contextNode?: Node) { + return !sourceMapsDisabled + ? emitTokenWithSourceMap(contextNode, token, writer, pos, writeTokenText) + : writeTokenText(token, writer, pos); + } + function writeTokenNode(node: Node, writer: (s: string) => void) { + if (onBeforeEmitToken) { + onBeforeEmitToken(node); } - - function writeOperator(s: string) { - writer.writeOperator(s); + writer((tokenToString(node.kind)!)); + if (onAfterEmitToken) { + onAfterEmitToken(node); } - - function writeParameter(s: string) { - writer.writeParameter(s); + } + function writeTokenText(token: SyntaxKind, writer: (s: string) => void): void; + function writeTokenText(token: SyntaxKind, writer: (s: string) => void, pos: number): number; + function writeTokenText(token: SyntaxKind, writer: (s: string) => void, pos?: number): number { + const tokenString = (tokenToString(token)!); + writer(tokenString); + return pos! < 0 ? pos! : pos! + tokenString.length; + } + function writeLineOrSpace(node: Node) { + if (getEmitFlags(node) & EmitFlags.SingleLine) { + writeSpace(); } - - function writeComment(s: string) { - writer.writeComment(s); + else { + writeLine(); } - - function writeSpace() { - writer.writeSpace(" "); + } + function writeLines(text: string): void { + const lines = text.split(/\r\n?|\n/g); + const indentation = guessIndentation(lines); + for (const lineText of lines) { + const line = indentation ? lineText.slice(indentation) : lineText; + if (line.length) { + writeLine(); + write(line); + } } - - function writeProperty(s: string) { - writer.writeProperty(s); + } + function increaseIndentIf(value: boolean, writeSpaceIfNotIndenting: boolean) { + if (value) { + increaseIndent(); + writeLine(); } - - function writeLine() { - writer.writeLine(); + else if (writeSpaceIfNotIndenting) { + writeSpace(); } - - function increaseIndent() { - writer.increaseIndent(); + } + // Helper function to decrease the indent if we previously indented. Allows multiple + // previous indent values to be considered at a time. This also allows caller to just + // call this once, passing in all their appropriate indent values, instead of needing + // to call this helper function multiple times. + function decreaseIndentIf(value1: boolean, value2: boolean) { + if (value1) { + decreaseIndent(); } - - function decreaseIndent() { - writer.decreaseIndent(); + if (value2) { + decreaseIndent(); } - - function writeToken(token: SyntaxKind, pos: number, writer: (s: string) => void, contextNode?: Node) { - return !sourceMapsDisabled - ? emitTokenWithSourceMap(contextNode, token, writer, pos, writeTokenText) - : writeTokenText(token, writer, pos); + } + function shouldWriteLeadingLineTerminator(parentNode: TextRange, children: NodeArray, format: ListFormat) { + if (format & ListFormat.MultiLine) { + return true; } - - function writeTokenNode(node: Node, writer: (s: string) => void) { - if (onBeforeEmitToken) { - onBeforeEmitToken(node); + if (format & ListFormat.PreserveLines) { + if (format & ListFormat.PreferNewLine) { + return true; } - writer(tokenToString(node.kind)!); - if (onAfterEmitToken) { - onAfterEmitToken(node); + const firstChild = children[0]; + if (firstChild === undefined) { + return !rangeIsOnSingleLine(parentNode, (currentSourceFile!)); } - } - - function writeTokenText(token: SyntaxKind, writer: (s: string) => void): void; - function writeTokenText(token: SyntaxKind, writer: (s: string) => void, pos: number): number; - function writeTokenText(token: SyntaxKind, writer: (s: string) => void, pos?: number): number { - const tokenString = tokenToString(token)!; - writer(tokenString); - return pos! < 0 ? pos! : pos! + tokenString.length; - } - - function writeLineOrSpace(node: Node) { - if (getEmitFlags(node) & EmitFlags.SingleLine) { - writeSpace(); + else if (positionIsSynthesized(parentNode.pos) || nodeIsSynthesized(firstChild)) { + return synthesizedNodeStartsOnNewLine(firstChild, format); } else { - writeLine(); - } - } - - function writeLines(text: string): void { - const lines = text.split(/\r\n?|\n/g); - const indentation = guessIndentation(lines); - for (const lineText of lines) { - const line = indentation ? lineText.slice(indentation) : lineText; - if (line.length) { - writeLine(); - write(line); - } + return !rangeStartPositionsAreOnSameLine(parentNode, firstChild, (currentSourceFile!)); } } - - function increaseIndentIf(value: boolean, writeSpaceIfNotIndenting: boolean) { - if (value) { - increaseIndent(); - writeLine(); - } - else if (writeSpaceIfNotIndenting) { - writeSpace(); - } + else { + return false; } - - // Helper function to decrease the indent if we previously indented. Allows multiple - // previous indent values to be considered at a time. This also allows caller to just - // call this once, passing in all their appropriate indent values, instead of needing - // to call this helper function multiple times. - function decreaseIndentIf(value1: boolean, value2: boolean) { - if (value1) { - decreaseIndent(); - } - if (value2) { - decreaseIndent(); - } + } + function shouldWriteSeparatingLineTerminator(previousNode: Node | undefined, nextNode: Node, format: ListFormat) { + if (format & ListFormat.MultiLine) { + return true; } - - function shouldWriteLeadingLineTerminator(parentNode: TextRange, children: NodeArray, format: ListFormat) { - if (format & ListFormat.MultiLine) { - return true; + else if (format & ListFormat.PreserveLines) { + if (previousNode === undefined || nextNode === undefined) { + return false; } - - if (format & ListFormat.PreserveLines) { - if (format & ListFormat.PreferNewLine) { - return true; - } - - const firstChild = children[0]; - if (firstChild === undefined) { - return !rangeIsOnSingleLine(parentNode, currentSourceFile!); - } - else if (positionIsSynthesized(parentNode.pos) || nodeIsSynthesized(firstChild)) { - return synthesizedNodeStartsOnNewLine(firstChild, format); - } - else { - return !rangeStartPositionsAreOnSameLine(parentNode, firstChild, currentSourceFile!); - } + else if (nodeIsSynthesized(previousNode) || nodeIsSynthesized(nextNode)) { + return synthesizedNodeStartsOnNewLine(previousNode, format) || synthesizedNodeStartsOnNewLine(nextNode, format); } else { - return false; + return !rangeEndIsOnSameLineAsRangeStart(previousNode, nextNode, (currentSourceFile!)); } } - - function shouldWriteSeparatingLineTerminator(previousNode: Node | undefined, nextNode: Node, format: ListFormat) { - if (format & ListFormat.MultiLine) { + else { + return getStartsOnNewLine(nextNode); + } + } + function shouldWriteClosingLineTerminator(parentNode: TextRange, children: NodeArray, format: ListFormat) { + if (format & ListFormat.MultiLine) { + return (format & ListFormat.NoTrailingNewLine) === 0; + } + else if (format & ListFormat.PreserveLines) { + if (format & ListFormat.PreferNewLine) { return true; } - else if (format & ListFormat.PreserveLines) { - if (previousNode === undefined || nextNode === undefined) { - return false; - } - else if (nodeIsSynthesized(previousNode) || nodeIsSynthesized(nextNode)) { - return synthesizedNodeStartsOnNewLine(previousNode, format) || synthesizedNodeStartsOnNewLine(nextNode, format); - } - else { - return !rangeEndIsOnSameLineAsRangeStart(previousNode, nextNode, currentSourceFile!); - } - } - else { - return getStartsOnNewLine(nextNode); - } - } - - function shouldWriteClosingLineTerminator(parentNode: TextRange, children: NodeArray, format: ListFormat) { - if (format & ListFormat.MultiLine) { - return (format & ListFormat.NoTrailingNewLine) === 0; + const lastChild = lastOrUndefined(children); + if (lastChild === undefined) { + return !rangeIsOnSingleLine(parentNode, (currentSourceFile!)); } - else if (format & ListFormat.PreserveLines) { - if (format & ListFormat.PreferNewLine) { - return true; - } - - const lastChild = lastOrUndefined(children); - if (lastChild === undefined) { - return !rangeIsOnSingleLine(parentNode, currentSourceFile!); - } - else if (positionIsSynthesized(parentNode.pos) || nodeIsSynthesized(lastChild)) { - return synthesizedNodeStartsOnNewLine(lastChild, format); - } - else { - return !rangeEndPositionsAreOnSameLine(parentNode, lastChild, currentSourceFile!); - } + else if (positionIsSynthesized(parentNode.pos) || nodeIsSynthesized(lastChild)) { + return synthesizedNodeStartsOnNewLine(lastChild, format); } else { - return false; + return !rangeEndPositionsAreOnSameLine(parentNode, lastChild, (currentSourceFile!)); } } - - function synthesizedNodeStartsOnNewLine(node: Node, format: ListFormat) { - if (nodeIsSynthesized(node)) { - const startsOnNewLine = getStartsOnNewLine(node); - if (startsOnNewLine === undefined) { - return (format & ListFormat.PreferNewLine) !== 0; - } - - return startsOnNewLine; - } - - return (format & ListFormat.PreferNewLine) !== 0; + else { + return false; } - - function needsIndentation(parent: Node, node1: Node, node2: Node): boolean { - if (getEmitFlags(parent) & EmitFlags.NoIndentation) { - return false; - } - - parent = skipSynthesizedParentheses(parent); - node1 = skipSynthesizedParentheses(node1); - node2 = skipSynthesizedParentheses(node2); - - // Always use a newline for synthesized code if the synthesizer desires it. - if (getStartsOnNewLine(node2)) { - return true; - } - - return !nodeIsSynthesized(parent) - && !nodeIsSynthesized(node1) - && !nodeIsSynthesized(node2) - && !rangeEndIsOnSameLineAsRangeStart(node1, node2, currentSourceFile!); - } - - function isEmptyBlock(block: BlockLike) { - return block.statements.length === 0 - && rangeEndIsOnSameLineAsRangeStart(block, block, currentSourceFile!); - } - - function skipSynthesizedParentheses(node: Node) { - while (node.kind === SyntaxKind.ParenthesizedExpression && nodeIsSynthesized(node)) { - node = (node).expression; + } + function synthesizedNodeStartsOnNewLine(node: Node, format: ListFormat) { + if (nodeIsSynthesized(node)) { + const startsOnNewLine = getStartsOnNewLine(node); + if (startsOnNewLine === undefined) { + return (format & ListFormat.PreferNewLine) !== 0; } - - return node; + return startsOnNewLine; } - - function getTextOfNode(node: Node, includeTrivia?: boolean): string { - if (isGeneratedIdentifier(node)) { - return generateName(node); - } - else if ((isIdentifier(node) || isPrivateIdentifier(node)) && (nodeIsSynthesized(node) || !node.parent || !currentSourceFile || (node.parent && currentSourceFile && getSourceFileOfNode(node) !== getOriginalNode(currentSourceFile)))) { - return idText(node); - } - else if (node.kind === SyntaxKind.StringLiteral && (node).textSourceNode) { - return getTextOfNode((node).textSourceNode!, includeTrivia); - } - else if (isLiteralExpression(node) && (nodeIsSynthesized(node) || !node.parent)) { - return node.text; - } - - return getSourceTextOfNodeFromSourceFile(currentSourceFile!, node, includeTrivia); - } - - function getLiteralTextOfNode(node: LiteralLikeNode, neverAsciiEscape: boolean | undefined, jsxAttributeEscape: boolean): string { - if (node.kind === SyntaxKind.StringLiteral && (node).textSourceNode) { - const textSourceNode = (node).textSourceNode!; - if (isIdentifier(textSourceNode)) { - return jsxAttributeEscape ? `"${escapeJsxAttributeString(getTextOfNode(textSourceNode))}"` : - neverAsciiEscape || (getEmitFlags(node) & EmitFlags.NoAsciiEscaping) ? `"${escapeString(getTextOfNode(textSourceNode))}"` : - `"${escapeNonAsciiString(getTextOfNode(textSourceNode))}"`; - } - else { - return getLiteralTextOfNode(textSourceNode, neverAsciiEscape, jsxAttributeEscape); - } - } - - return getLiteralText(node, currentSourceFile!, neverAsciiEscape, jsxAttributeEscape); - } - - /** - * Push a new name generation scope. - */ - function pushNameGenerationScope(node: Node | undefined) { - if (node && getEmitFlags(node) & EmitFlags.ReuseTempVariableScope) { - return; - } - tempFlagsStack.push(tempFlags); - tempFlags = 0; - reservedNamesStack.push(reservedNames); - } - - /** - * Pop the current name generation scope. - */ - function popNameGenerationScope(node: Node | undefined) { - if (node && getEmitFlags(node) & EmitFlags.ReuseTempVariableScope) { - return; - } - tempFlags = tempFlagsStack.pop()!; - reservedNames = reservedNamesStack.pop()!; + return (format & ListFormat.PreferNewLine) !== 0; + } + function needsIndentation(parent: Node, node1: Node, node2: Node): boolean { + if (getEmitFlags(parent) & EmitFlags.NoIndentation) { + return false; } - - function reserveNameInNestedScopes(name: string) { - if (!reservedNames || reservedNames === lastOrUndefined(reservedNamesStack)) { - reservedNames = createMap(); - } - reservedNames.set(name, true); + parent = skipSynthesizedParentheses(parent); + node1 = skipSynthesizedParentheses(node1); + node2 = skipSynthesizedParentheses(node2); + // Always use a newline for synthesized code if the synthesizer desires it. + if (getStartsOnNewLine(node2)) { + return true; } - - function generateNames(node: Node | undefined) { - if (!node) return; - switch (node.kind) { - case SyntaxKind.Block: - forEach((node).statements, generateNames); - break; - case SyntaxKind.LabeledStatement: - case SyntaxKind.WithStatement: - case SyntaxKind.DoStatement: - case SyntaxKind.WhileStatement: - generateNames((node).statement); - break; - case SyntaxKind.IfStatement: - generateNames((node).thenStatement); - generateNames((node).elseStatement); - break; - case SyntaxKind.ForStatement: - case SyntaxKind.ForOfStatement: - case SyntaxKind.ForInStatement: - generateNames((node).initializer); - generateNames((node).statement); - break; - case SyntaxKind.SwitchStatement: - generateNames((node).caseBlock); - break; - case SyntaxKind.CaseBlock: - forEach((node).clauses, generateNames); - break; - case SyntaxKind.CaseClause: - case SyntaxKind.DefaultClause: - forEach((node).statements, generateNames); - break; - case SyntaxKind.TryStatement: - generateNames((node).tryBlock); - generateNames((node).catchClause); - generateNames((node).finallyBlock); - break; - case SyntaxKind.CatchClause: - generateNames((node).variableDeclaration); - generateNames((node).block); - break; - case SyntaxKind.VariableStatement: - generateNames((node).declarationList); - break; - case SyntaxKind.VariableDeclarationList: - forEach((node).declarations, generateNames); - break; - case SyntaxKind.VariableDeclaration: - case SyntaxKind.Parameter: - case SyntaxKind.BindingElement: - case SyntaxKind.ClassDeclaration: - generateNameIfNeeded((node).name); - break; - case SyntaxKind.FunctionDeclaration: - generateNameIfNeeded((node).name); - if (getEmitFlags(node) & EmitFlags.ReuseTempVariableScope) { - forEach((node).parameters, generateNames); - generateNames((node).body); - } - break; - case SyntaxKind.ObjectBindingPattern: - case SyntaxKind.ArrayBindingPattern: - forEach((node).elements, generateNames); - break; - case SyntaxKind.ImportDeclaration: - generateNames((node).importClause); - break; - case SyntaxKind.ImportClause: - generateNameIfNeeded((node).name); - generateNames((node).namedBindings); - break; - case SyntaxKind.NamespaceImport: - generateNameIfNeeded((node).name); - break; - case SyntaxKind.NamespaceExport: - generateNameIfNeeded((node).name); - break; - case SyntaxKind.NamedImports: - forEach((node).elements, generateNames); - break; - case SyntaxKind.ImportSpecifier: - generateNameIfNeeded((node).propertyName || (node).name); - break; - } + return !nodeIsSynthesized(parent) + && !nodeIsSynthesized(node1) + && !nodeIsSynthesized(node2) + && !rangeEndIsOnSameLineAsRangeStart(node1, node2, (currentSourceFile!)); + } + function isEmptyBlock(block: BlockLike) { + return block.statements.length === 0 + && rangeEndIsOnSameLineAsRangeStart(block, block, (currentSourceFile!)); + } + function skipSynthesizedParentheses(node: Node) { + while (node.kind === SyntaxKind.ParenthesizedExpression && nodeIsSynthesized(node)) { + node = (node).expression; } - - function generateMemberNames(node: Node | undefined) { - if (!node) return; - switch (node.kind) { - case SyntaxKind.PropertyAssignment: - case SyntaxKind.ShorthandPropertyAssignment: - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - generateNameIfNeeded((node).name); - break; + return node; + } + function getTextOfNode(node: Node, includeTrivia?: boolean): string { + if (isGeneratedIdentifier(node)) { + return generateName(node); + } + else if ((isIdentifier(node) || isPrivateIdentifier(node)) && (nodeIsSynthesized(node) || !node.parent || !currentSourceFile || (node.parent && currentSourceFile && getSourceFileOfNode(node) !== getOriginalNode(currentSourceFile)))) { + return idText(node); + } + else if (node.kind === SyntaxKind.StringLiteral && (node).textSourceNode) { + return getTextOfNode(((node).textSourceNode!), includeTrivia); + } + else if (isLiteralExpression(node) && (nodeIsSynthesized(node) || !node.parent)) { + return node.text; + } + return getSourceTextOfNodeFromSourceFile((currentSourceFile!), node, includeTrivia); + } + function getLiteralTextOfNode(node: LiteralLikeNode, neverAsciiEscape: boolean | undefined, jsxAttributeEscape: boolean): string { + if (node.kind === SyntaxKind.StringLiteral && (node).textSourceNode) { + const textSourceNode = ((node).textSourceNode!); + if (isIdentifier(textSourceNode)) { + return jsxAttributeEscape ? `"${escapeJsxAttributeString(getTextOfNode(textSourceNode))}"` : + neverAsciiEscape || (getEmitFlags(node) & EmitFlags.NoAsciiEscaping) ? `"${escapeString(getTextOfNode(textSourceNode))}"` : + `"${escapeNonAsciiString(getTextOfNode(textSourceNode))}"`; + } + else { + return getLiteralTextOfNode(textSourceNode, neverAsciiEscape, jsxAttributeEscape); } } - - function generateNameIfNeeded(name: DeclarationName | undefined) { - if (name) { - if (isGeneratedIdentifier(name)) { - generateName(name); - } - else if (isBindingPattern(name)) { - generateNames(name); + return getLiteralText(node, (currentSourceFile!), neverAsciiEscape, jsxAttributeEscape); + } + /** + * Push a new name generation scope. + */ + function pushNameGenerationScope(node: Node | undefined) { + if (node && getEmitFlags(node) & EmitFlags.ReuseTempVariableScope) { + return; + } + tempFlagsStack.push(tempFlags); + tempFlags = 0; + reservedNamesStack.push(reservedNames); + } + /** + * Pop the current name generation scope. + */ + function popNameGenerationScope(node: Node | undefined) { + if (node && getEmitFlags(node) & EmitFlags.ReuseTempVariableScope) { + return; + } + tempFlags = tempFlagsStack.pop()!; + reservedNames = reservedNamesStack.pop()!; + } + function reserveNameInNestedScopes(name: string) { + if (!reservedNames || reservedNames === lastOrUndefined(reservedNamesStack)) { + reservedNames = createMap(); + } + reservedNames.set(name, true); + } + function generateNames(node: Node | undefined) { + if (!node) + return; + switch (node.kind) { + case SyntaxKind.Block: + forEach((node).statements, generateNames); + break; + case SyntaxKind.LabeledStatement: + case SyntaxKind.WithStatement: + case SyntaxKind.DoStatement: + case SyntaxKind.WhileStatement: + generateNames((node).statement); + break; + case SyntaxKind.IfStatement: + generateNames((node).thenStatement); + generateNames((node).elseStatement); + break; + case SyntaxKind.ForStatement: + case SyntaxKind.ForOfStatement: + case SyntaxKind.ForInStatement: + generateNames((node).initializer); + generateNames((node).statement); + break; + case SyntaxKind.SwitchStatement: + generateNames((node).caseBlock); + break; + case SyntaxKind.CaseBlock: + forEach((node).clauses, generateNames); + break; + case SyntaxKind.CaseClause: + case SyntaxKind.DefaultClause: + forEach((node).statements, generateNames); + break; + case SyntaxKind.TryStatement: + generateNames((node).tryBlock); + generateNames((node).catchClause); + generateNames((node).finallyBlock); + break; + case SyntaxKind.CatchClause: + generateNames((node).variableDeclaration); + generateNames((node).block); + break; + case SyntaxKind.VariableStatement: + generateNames((node).declarationList); + break; + case SyntaxKind.VariableDeclarationList: + forEach((node).declarations, generateNames); + break; + case SyntaxKind.VariableDeclaration: + case SyntaxKind.Parameter: + case SyntaxKind.BindingElement: + case SyntaxKind.ClassDeclaration: + generateNameIfNeeded((node).name); + break; + case SyntaxKind.FunctionDeclaration: + generateNameIfNeeded((node).name); + if (getEmitFlags(node) & EmitFlags.ReuseTempVariableScope) { + forEach((node).parameters, generateNames); + generateNames((node).body); } - } + break; + case SyntaxKind.ObjectBindingPattern: + case SyntaxKind.ArrayBindingPattern: + forEach((node).elements, generateNames); + break; + case SyntaxKind.ImportDeclaration: + generateNames((node).importClause); + break; + case SyntaxKind.ImportClause: + generateNameIfNeeded((node).name); + generateNames((node).namedBindings); + break; + case SyntaxKind.NamespaceImport: + generateNameIfNeeded((node).name); + break; + case SyntaxKind.NamespaceExport: + generateNameIfNeeded((node).name); + break; + case SyntaxKind.NamedImports: + forEach((node).elements, generateNames); + break; + case SyntaxKind.ImportSpecifier: + generateNameIfNeeded((node).propertyName || (node).name); + break; + } + } + function generateMemberNames(node: Node | undefined) { + if (!node) + return; + switch (node.kind) { + case SyntaxKind.PropertyAssignment: + case SyntaxKind.ShorthandPropertyAssignment: + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + generateNameIfNeeded((node).name); + break; } - - /** - * Generate the text for a generated identifier. - */ - function generateName(name: GeneratedIdentifier) { - if ((name.autoGenerateFlags & GeneratedIdentifierFlags.KindMask) === GeneratedIdentifierFlags.Node) { - // Node names generate unique names based on their original node - // and are cached based on that node's id. - return generateNameCached(getNodeForGeneratedName(name), name.autoGenerateFlags); + } + function generateNameIfNeeded(name: DeclarationName | undefined) { + if (name) { + if (isGeneratedIdentifier(name)) { + generateName(name); } - else { - // Auto, Loop, and Unique names are cached based on their unique - // autoGenerateId. - const autoGenerateId = name.autoGenerateId!; - return autoGeneratedIdToGeneratedName[autoGenerateId] || (autoGeneratedIdToGeneratedName[autoGenerateId] = makeName(name)); + else if (isBindingPattern(name)) { + generateNames(name); } } - - function generateNameCached(node: Node, flags?: GeneratedIdentifierFlags) { - const nodeId = getNodeId(node); - return nodeIdToGeneratedName[nodeId] || (nodeIdToGeneratedName[nodeId] = generateNameForNode(node, flags)); - } - - /** - * Returns a value indicating whether a name is unique globally, within the current file, - * or within the NameGenerator. - */ - function isUniqueName(name: string): boolean { - return isFileLevelUniqueName(name) - && !generatedNames.has(name) - && !(reservedNames && reservedNames.has(name)); - } - - /** - * Returns a value indicating whether a name is unique globally or within the current file. - */ - function isFileLevelUniqueName(name: string) { - return currentSourceFile ? ts.isFileLevelUniqueName(currentSourceFile, name, hasGlobalName) : true; - } - - /** - * Returns a value indicating whether a name is unique within a container. - */ - function isUniqueLocalName(name: string, container: Node): boolean { - for (let node = container; isNodeDescendantOf(node, container); node = node.nextContainer!) { - if (node.locals) { - const local = node.locals.get(escapeLeadingUnderscores(name)); - // We conservatively include alias symbols to cover cases where they're emitted as locals - if (local && local.flags & (SymbolFlags.Value | SymbolFlags.ExportValue | SymbolFlags.Alias)) { - return false; - } + } + /** + * Generate the text for a generated identifier. + */ + function generateName(name: GeneratedIdentifier) { + if ((name.autoGenerateFlags & GeneratedIdentifierFlags.KindMask) === GeneratedIdentifierFlags.Node) { + // Node names generate unique names based on their original node + // and are cached based on that node's id. + return generateNameCached(getNodeForGeneratedName(name), name.autoGenerateFlags); + } + else { + // Auto, Loop, and Unique names are cached based on their unique + // autoGenerateId. + const autoGenerateId = name.autoGenerateId!; + return autoGeneratedIdToGeneratedName[autoGenerateId] || (autoGeneratedIdToGeneratedName[autoGenerateId] = makeName(name)); + } + } + function generateNameCached(node: Node, flags?: GeneratedIdentifierFlags) { + const nodeId = getNodeId(node); + return nodeIdToGeneratedName[nodeId] || (nodeIdToGeneratedName[nodeId] = generateNameForNode(node, flags)); + } + /** + * Returns a value indicating whether a name is unique globally, within the current file, + * or within the NameGenerator. + */ + function isUniqueName(name: string): boolean { + return isFileLevelUniqueName(name) + && !generatedNames.has(name) + && !(reservedNames && reservedNames.has(name)); + } + /** + * Returns a value indicating whether a name is unique globally or within the current file. + */ + function isFileLevelUniqueName(name: string) { + return currentSourceFile ? ts.isFileLevelUniqueName(currentSourceFile, name, hasGlobalName) : true; + } + /** + * Returns a value indicating whether a name is unique within a container. + */ + function isUniqueLocalName(name: string, container: Node): boolean { + for (let node = container; isNodeDescendantOf(node, container); node = node.nextContainer!) { + if (node.locals) { + const local = node.locals.get(escapeLeadingUnderscores(name)); + // We conservatively include alias symbols to cover cases where they're emitted as locals + if (local && local.flags & (SymbolFlags.Value | SymbolFlags.ExportValue | SymbolFlags.Alias)) { + return false; } } - return true; } - - /** - * Return the next available name in the pattern _a ... _z, _0, _1, ... - * TempFlags._i or TempFlags._n may be used to express a preference for that dedicated name. - * Note that names generated by makeTempVariableName and makeUniqueName will never conflict. - */ - function makeTempVariableName(flags: TempFlags, reservedInNestedScopes?: boolean): string { - if (flags && !(tempFlags & flags)) { - const name = flags === TempFlags._i ? "_i" : "_n"; + return true; + } + /** + * Return the next available name in the pattern _a ... _z, _0, _1, ... + * TempFlags._i or TempFlags._n may be used to express a preference for that dedicated name. + * Note that names generated by makeTempVariableName and makeUniqueName will never conflict. + */ + function makeTempVariableName(flags: TempFlags, reservedInNestedScopes?: boolean): string { + if (flags && !(tempFlags & flags)) { + const name = flags === TempFlags._i ? "_i" : "_n"; + if (isUniqueName(name)) { + tempFlags |= flags; + if (reservedInNestedScopes) { + reserveNameInNestedScopes(name); + } + return name; + } + } + while (true) { + const count = tempFlags & TempFlags.CountMask; + tempFlags++; + // Skip over 'i' and 'n' + if (count !== 8 && count !== 13) { + const name = count < 26 + ? "_" + String.fromCharCode(CharacterCodes.a + count) + : "_" + (count - 26); if (isUniqueName(name)) { - tempFlags |= flags; if (reservedInNestedScopes) { reserveNameInNestedScopes(name); } return name; } } - while (true) { - const count = tempFlags & TempFlags.CountMask; - tempFlags++; - // Skip over 'i' and 'n' - if (count !== 8 && count !== 13) { - const name = count < 26 - ? "_" + String.fromCharCode(CharacterCodes.a + count) - : "_" + (count - 26); - if (isUniqueName(name)) { - if (reservedInNestedScopes) { - reserveNameInNestedScopes(name); - } - return name; - } - } - } } - - /** - * Generate a name that is unique within the current file and doesn't conflict with any names - * in global scope. The name is formed by adding an '_n' suffix to the specified base name, - * where n is a positive integer. Note that names generated by makeTempVariableName and - * makeUniqueName are guaranteed to never conflict. - * If `optimistic` is set, the first instance will use 'baseName' verbatim instead of 'baseName_1' - */ - function makeUniqueName(baseName: string, checkFn: (name: string) => boolean = isUniqueName, optimistic?: boolean, scoped?: boolean): string { - if (optimistic) { - if (checkFn(baseName)) { - if (scoped) { - reserveNameInNestedScopes(baseName); - } - else { - generatedNames.set(baseName, true); - } - return baseName; + } + /** + * Generate a name that is unique within the current file and doesn't conflict with any names + * in global scope. The name is formed by adding an '_n' suffix to the specified base name, + * where n is a positive integer. Note that names generated by makeTempVariableName and + * makeUniqueName are guaranteed to never conflict. + * If `optimistic` is set, the first instance will use 'baseName' verbatim instead of 'baseName_1' + */ + function makeUniqueName(baseName: string, checkFn: (name: string) => boolean = isUniqueName, optimistic?: boolean, scoped?: boolean): string { + if (optimistic) { + if (checkFn(baseName)) { + if (scoped) { + reserveNameInNestedScopes(baseName); } - } - // Find the first unique 'name_n', where n is a positive number - if (baseName.charCodeAt(baseName.length - 1) !== CharacterCodes._) { - baseName += "_"; - } - let i = 1; - while (true) { - const generatedName = baseName + i; - if (checkFn(generatedName)) { - if (scoped) { - reserveNameInNestedScopes(generatedName); - } - else { - generatedNames.set(generatedName, true); - } - return generatedName; + else { + generatedNames.set(baseName, true); } - i++; - } - } - - function makeFileLevelOptimisticUniqueName(name: string) { - return makeUniqueName(name, isFileLevelUniqueName, /*optimistic*/ true); - } - - /** - * Generates a unique name for a ModuleDeclaration or EnumDeclaration. - */ - function generateNameForModuleOrEnum(node: ModuleDeclaration | EnumDeclaration) { - const name = getTextOfNode(node.name); - // Use module/enum name itself if it is unique, otherwise make a unique variation - return isUniqueLocalName(name, node) ? name : makeUniqueName(name); - } - - /** - * Generates a unique name for an ImportDeclaration or ExportDeclaration. - */ - function generateNameForImportOrExportDeclaration(node: ImportDeclaration | ExportDeclaration) { - const expr = getExternalModuleName(node)!; // TODO: GH#18217 - const baseName = isStringLiteral(expr) ? - makeIdentifierFromModuleName(expr.text) : "module"; - return makeUniqueName(baseName); - } - - /** - * Generates a unique name for a default export. - */ - function generateNameForExportDefault() { - return makeUniqueName("default"); - } - - /** - * Generates a unique name for a class expression. - */ - function generateNameForClassExpression() { - return makeUniqueName("class"); - } - - function generateNameForMethodOrAccessor(node: MethodDeclaration | AccessorDeclaration) { - if (isIdentifier(node.name)) { - return generateNameCached(node.name); + return baseName; } - return makeTempVariableName(TempFlags.Auto); } - - /** - * Generates a unique name from a node. - */ - function generateNameForNode(node: Node, flags?: GeneratedIdentifierFlags): string { - switch (node.kind) { - case SyntaxKind.Identifier: - return makeUniqueName( - getTextOfNode(node), - isUniqueName, - !!(flags! & GeneratedIdentifierFlags.Optimistic), - !!(flags! & GeneratedIdentifierFlags.ReservedInNestedScopes) - ); - case SyntaxKind.ModuleDeclaration: - case SyntaxKind.EnumDeclaration: - return generateNameForModuleOrEnum(node); - case SyntaxKind.ImportDeclaration: - case SyntaxKind.ExportDeclaration: - return generateNameForImportOrExportDeclaration(node); - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ExportAssignment: - return generateNameForExportDefault(); - case SyntaxKind.ClassExpression: - return generateNameForClassExpression(); - case SyntaxKind.MethodDeclaration: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - return generateNameForMethodOrAccessor(node); - case SyntaxKind.ComputedPropertyName: - return makeTempVariableName(TempFlags.Auto, /*reserveInNestedScopes*/ true); - default: - return makeTempVariableName(TempFlags.Auto); - } + // Find the first unique 'name_n', where n is a positive number + if (baseName.charCodeAt(baseName.length - 1) !== CharacterCodes._) { + baseName += "_"; } - - /** - * Generates a unique identifier for a node. - */ - function makeName(name: GeneratedIdentifier) { - switch (name.autoGenerateFlags & GeneratedIdentifierFlags.KindMask) { - case GeneratedIdentifierFlags.Auto: - return makeTempVariableName(TempFlags.Auto, !!(name.autoGenerateFlags & GeneratedIdentifierFlags.ReservedInNestedScopes)); - case GeneratedIdentifierFlags.Loop: - return makeTempVariableName(TempFlags._i, !!(name.autoGenerateFlags & GeneratedIdentifierFlags.ReservedInNestedScopes)); - case GeneratedIdentifierFlags.Unique: - return makeUniqueName( - idText(name), - (name.autoGenerateFlags & GeneratedIdentifierFlags.FileLevel) ? isFileLevelUniqueName : isUniqueName, - !!(name.autoGenerateFlags & GeneratedIdentifierFlags.Optimistic), - !!(name.autoGenerateFlags & GeneratedIdentifierFlags.ReservedInNestedScopes) - ); - } - - return Debug.fail("Unsupported GeneratedIdentifierKind."); - } - - /** - * Gets the node from which a name should be generated. - */ - function getNodeForGeneratedName(name: GeneratedIdentifier) { - const autoGenerateId = name.autoGenerateId; - let node = name as Node; - let original = node.original; - while (original) { - node = original; - - // if "node" is a different generated name (having a different - // "autoGenerateId"), use it and stop traversing. - if (isIdentifier(node) - && !!(node.autoGenerateFlags! & GeneratedIdentifierFlags.Node) - && node.autoGenerateId !== autoGenerateId) { - break; - } - - original = node.original; - } - - // otherwise, return the original node for the source; - return node; - } - - // Comments - - function pipelineEmitWithComments(hint: EmitHint, node: Node) { - Debug.assert(lastNode === node || lastSubstitution === node); - enterComment(); - hasWrittenComment = false; - const emitFlags = getEmitFlags(node); - const { pos, end } = getCommentRange(node); - const isEmittedNode = node.kind !== SyntaxKind.NotEmittedStatement; - - // We have to explicitly check that the node is JsxText because if the compilerOptions.jsx is "preserve" we will not do any transformation. - // It is expensive to walk entire tree just to set one kind of node to have no comments. - const skipLeadingComments = pos < 0 || (emitFlags & EmitFlags.NoLeadingComments) !== 0 || node.kind === SyntaxKind.JsxText; - const skipTrailingComments = end < 0 || (emitFlags & EmitFlags.NoTrailingComments) !== 0 || node.kind === SyntaxKind.JsxText; - - // Save current container state on the stack. - const savedContainerPos = containerPos; - const savedContainerEnd = containerEnd; - const savedDeclarationListContainerEnd = declarationListContainerEnd; - if ((pos > 0 || end > 0) && pos !== end) { - // Emit leading comments if the position is not synthesized and the node - // has not opted out from emitting leading comments. - if (!skipLeadingComments) { - emitLeadingComments(pos, isEmittedNode); - } - - if (!skipLeadingComments || (pos >= 0 && (emitFlags & EmitFlags.NoLeadingComments) !== 0)) { - // Advance the container position if comments get emitted or if they've been disabled explicitly using NoLeadingComments. - containerPos = pos; - } - - if (!skipTrailingComments || (end >= 0 && (emitFlags & EmitFlags.NoTrailingComments) !== 0)) { - // As above. - containerEnd = end; - - // To avoid invalid comment emit in a down-level binding pattern, we - // keep track of the last declaration list container's end - if (node.kind === SyntaxKind.VariableDeclarationList) { - declarationListContainerEnd = end; - } + let i = 1; + while (true) { + const generatedName = baseName + i; + if (checkFn(generatedName)) { + if (scoped) { + reserveNameInNestedScopes(generatedName); } - } - forEach(getSyntheticLeadingComments(node), emitLeadingSynthesizedComment); - exitComment(); - - const pipelinePhase = getNextPipelinePhase(PipelinePhase.Comments, hint, node); - if (emitFlags & EmitFlags.NoNestedComments) { - commentsDisabled = true; - pipelinePhase(hint, node); - commentsDisabled = false; - } - else { - pipelinePhase(hint, node); - } - - enterComment(); - forEach(getSyntheticTrailingComments(node), emitTrailingSynthesizedComment); - if ((pos > 0 || end > 0) && pos !== end) { - // Restore previous container state. - containerPos = savedContainerPos; - containerEnd = savedContainerEnd; - declarationListContainerEnd = savedDeclarationListContainerEnd; - - // Emit trailing comments if the position is not synthesized and the node - // has not opted out from emitting leading comments and is an emitted node. - if (!skipTrailingComments && isEmittedNode) { - emitTrailingComments(end); + else { + generatedNames.set(generatedName, true); } + return generatedName; } - exitComment(); - Debug.assert(lastNode === node || lastSubstitution === node); + i++; } - - function emitLeadingSynthesizedComment(comment: SynthesizedComment) { - if (comment.kind === SyntaxKind.SingleLineCommentTrivia) { - writer.writeLine(); - } - writeSynthesizedComment(comment); - if (comment.hasTrailingNewLine || comment.kind === SyntaxKind.SingleLineCommentTrivia) { - writer.writeLine(); - } - else { - writer.writeSpace(" "); - } + } + function makeFileLevelOptimisticUniqueName(name: string) { + return makeUniqueName(name, isFileLevelUniqueName, /*optimistic*/ true); + } + /** + * Generates a unique name for a ModuleDeclaration or EnumDeclaration. + */ + function generateNameForModuleOrEnum(node: ModuleDeclaration | EnumDeclaration) { + const name = getTextOfNode(node.name); + // Use module/enum name itself if it is unique, otherwise make a unique variation + return isUniqueLocalName(name, node) ? name : makeUniqueName(name); + } + /** + * Generates a unique name for an ImportDeclaration or ExportDeclaration. + */ + function generateNameForImportOrExportDeclaration(node: ImportDeclaration | ExportDeclaration) { + const expr = (getExternalModuleName(node)!); // TODO: GH#18217 + const baseName = isStringLiteral(expr) ? + makeIdentifierFromModuleName(expr.text) : "module"; + return makeUniqueName(baseName); + } + /** + * Generates a unique name for a default export. + */ + function generateNameForExportDefault() { + return makeUniqueName("default"); + } + /** + * Generates a unique name for a class expression. + */ + function generateNameForClassExpression() { + return makeUniqueName("class"); + } + function generateNameForMethodOrAccessor(node: MethodDeclaration | AccessorDeclaration) { + if (isIdentifier(node.name)) { + return generateNameCached(node.name); } - - function emitTrailingSynthesizedComment(comment: SynthesizedComment) { - if (!writer.isAtStartOfLine()) { - writer.writeSpace(" "); - } - writeSynthesizedComment(comment); - if (comment.hasTrailingNewLine) { - writer.writeLine(); - } + return makeTempVariableName(TempFlags.Auto); + } + /** + * Generates a unique name from a node. + */ + function generateNameForNode(node: Node, flags?: GeneratedIdentifierFlags): string { + switch (node.kind) { + case SyntaxKind.Identifier: + return makeUniqueName(getTextOfNode(node), isUniqueName, !!((flags!) & GeneratedIdentifierFlags.Optimistic), !!((flags!) & GeneratedIdentifierFlags.ReservedInNestedScopes)); + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.EnumDeclaration: + return generateNameForModuleOrEnum((node)); + case SyntaxKind.ImportDeclaration: + case SyntaxKind.ExportDeclaration: + return generateNameForImportOrExportDeclaration((node)); + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ExportAssignment: + return generateNameForExportDefault(); + case SyntaxKind.ClassExpression: + return generateNameForClassExpression(); + case SyntaxKind.MethodDeclaration: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + return generateNameForMethodOrAccessor((node)); + case SyntaxKind.ComputedPropertyName: + return makeTempVariableName(TempFlags.Auto, /*reserveInNestedScopes*/ true); + default: + return makeTempVariableName(TempFlags.Auto); } - - function writeSynthesizedComment(comment: SynthesizedComment) { - const text = formatSynthesizedComment(comment); - const lineMap = comment.kind === SyntaxKind.MultiLineCommentTrivia ? computeLineStarts(text) : undefined; - writeCommentRange(text, lineMap!, writer, 0, text.length, newLine); - } - - function formatSynthesizedComment(comment: SynthesizedComment) { - return comment.kind === SyntaxKind.MultiLineCommentTrivia - ? `/*${comment.text}*/` - : `//${comment.text}`; - } - - function emitBodyWithDetachedComments(node: Node, detachedRange: TextRange, emitCallback: (node: Node) => void) { - enterComment(); - const { pos, end } = detachedRange; - const emitFlags = getEmitFlags(node); - const skipLeadingComments = pos < 0 || (emitFlags & EmitFlags.NoLeadingComments) !== 0; - const skipTrailingComments = commentsDisabled || end < 0 || (emitFlags & EmitFlags.NoTrailingComments) !== 0; + } + /** + * Generates a unique identifier for a node. + */ + function makeName(name: GeneratedIdentifier) { + switch (name.autoGenerateFlags & GeneratedIdentifierFlags.KindMask) { + case GeneratedIdentifierFlags.Auto: + return makeTempVariableName(TempFlags.Auto, !!(name.autoGenerateFlags & GeneratedIdentifierFlags.ReservedInNestedScopes)); + case GeneratedIdentifierFlags.Loop: + return makeTempVariableName(TempFlags._i, !!(name.autoGenerateFlags & GeneratedIdentifierFlags.ReservedInNestedScopes)); + case GeneratedIdentifierFlags.Unique: + return makeUniqueName(idText(name), (name.autoGenerateFlags & GeneratedIdentifierFlags.FileLevel) ? isFileLevelUniqueName : isUniqueName, !!(name.autoGenerateFlags & GeneratedIdentifierFlags.Optimistic), !!(name.autoGenerateFlags & GeneratedIdentifierFlags.ReservedInNestedScopes)); + } + return Debug.fail("Unsupported GeneratedIdentifierKind."); + } + /** + * Gets the node from which a name should be generated. + */ + function getNodeForGeneratedName(name: GeneratedIdentifier) { + const autoGenerateId = name.autoGenerateId; + let node = (name as Node); + let original = node.original; + while (original) { + node = original; + // if "node" is a different generated name (having a different + // "autoGenerateId"), use it and stop traversing. + if (isIdentifier(node) + && !!((node.autoGenerateFlags!) & GeneratedIdentifierFlags.Node) + && node.autoGenerateId !== autoGenerateId) { + break; + } + original = node.original; + } + // otherwise, return the original node for the source; + return node; + } + // Comments + function pipelineEmitWithComments(hint: EmitHint, node: Node) { + Debug.assert(lastNode === node || lastSubstitution === node); + enterComment(); + hasWrittenComment = false; + const emitFlags = getEmitFlags(node); + const { pos, end } = getCommentRange(node); + const isEmittedNode = node.kind !== SyntaxKind.NotEmittedStatement; + // We have to explicitly check that the node is JsxText because if the compilerOptions.jsx is "preserve" we will not do any transformation. + // It is expensive to walk entire tree just to set one kind of node to have no comments. + const skipLeadingComments = pos < 0 || (emitFlags & EmitFlags.NoLeadingComments) !== 0 || node.kind === SyntaxKind.JsxText; + const skipTrailingComments = end < 0 || (emitFlags & EmitFlags.NoTrailingComments) !== 0 || node.kind === SyntaxKind.JsxText; + // Save current container state on the stack. + const savedContainerPos = containerPos; + const savedContainerEnd = containerEnd; + const savedDeclarationListContainerEnd = declarationListContainerEnd; + if ((pos > 0 || end > 0) && pos !== end) { + // Emit leading comments if the position is not synthesized and the node + // has not opted out from emitting leading comments. if (!skipLeadingComments) { - emitDetachedCommentsAndUpdateCommentsInfo(detachedRange); - } - - exitComment(); - if (emitFlags & EmitFlags.NoNestedComments && !commentsDisabled) { - commentsDisabled = true; - emitCallback(node); - commentsDisabled = false; + emitLeadingComments(pos, isEmittedNode); } - else { - emitCallback(node); + if (!skipLeadingComments || (pos >= 0 && (emitFlags & EmitFlags.NoLeadingComments) !== 0)) { + // Advance the container position if comments get emitted or if they've been disabled explicitly using NoLeadingComments. + containerPos = pos; } - - enterComment(); - if (!skipTrailingComments) { - emitLeadingComments(detachedRange.end, /*isEmittedNode*/ true); - if (hasWrittenComment && !writer.isAtStartOfLine()) { - writer.writeLine(); + if (!skipTrailingComments || (end >= 0 && (emitFlags & EmitFlags.NoTrailingComments) !== 0)) { + // As above. + containerEnd = end; + // To avoid invalid comment emit in a down-level binding pattern, we + // keep track of the last declaration list container's end + if (node.kind === SyntaxKind.VariableDeclarationList) { + declarationListContainerEnd = end; } } - exitComment(); - - } - - function emitLeadingComments(pos: number, isEmittedNode: boolean) { - hasWrittenComment = false; - - if (isEmittedNode) { - forEachLeadingCommentToEmit(pos, emitLeadingComment); - } - else if (pos === 0) { - // If the node will not be emitted in JS, remove all the comments(normal, pinned and ///) associated with the node, - // unless it is a triple slash comment at the top of the file. - // For Example: - // /// - // declare var x; - // /// - // interface F {} - // The first /// will NOT be removed while the second one will be removed even though both node will not be emitted - forEachLeadingCommentToEmit(pos, emitTripleSlashLeadingComment); - } } - - function emitTripleSlashLeadingComment(commentPos: number, commentEnd: number, kind: SyntaxKind, hasTrailingNewLine: boolean, rangePos: number) { - if (isTripleSlashComment(commentPos, commentEnd)) { - emitLeadingComment(commentPos, commentEnd, kind, hasTrailingNewLine, rangePos); - } + forEach(getSyntheticLeadingComments(node), emitLeadingSynthesizedComment); + exitComment(); + const pipelinePhase = getNextPipelinePhase(PipelinePhase.Comments, hint, node); + if (emitFlags & EmitFlags.NoNestedComments) { + commentsDisabled = true; + pipelinePhase(hint, node); + commentsDisabled = false; } - - function shouldWriteComment(text: string, pos: number) { - if (printerOptions.onlyPrintJsDocStyle) { - return (isJSDocLikeText(text, pos) || isPinnedComment(text, pos)); - } - return true; + else { + pipelinePhase(hint, node); + } + enterComment(); + forEach(getSyntheticTrailingComments(node), emitTrailingSynthesizedComment); + if ((pos > 0 || end > 0) && pos !== end) { + // Restore previous container state. + containerPos = savedContainerPos; + containerEnd = savedContainerEnd; + declarationListContainerEnd = savedDeclarationListContainerEnd; + // Emit trailing comments if the position is not synthesized and the node + // has not opted out from emitting leading comments and is an emitted node. + if (!skipTrailingComments && isEmittedNode) { + emitTrailingComments(end); + } + } + exitComment(); + Debug.assert(lastNode === node || lastSubstitution === node); + } + function emitLeadingSynthesizedComment(comment: SynthesizedComment) { + if (comment.kind === SyntaxKind.SingleLineCommentTrivia) { + writer.writeLine(); } - - function emitLeadingComment(commentPos: number, commentEnd: number, kind: SyntaxKind, hasTrailingNewLine: boolean, rangePos: number) { - if (!shouldWriteComment(currentSourceFile!.text, commentPos)) return; - if (!hasWrittenComment) { - emitNewLineBeforeLeadingCommentOfPosition(getCurrentLineMap(), writer, rangePos, commentPos); - hasWrittenComment = true; - } - - // Leading comments are emitted at /*leading comment1 */space/*leading comment*/space - emitPos(commentPos); - writeCommentRange(currentSourceFile!.text, getCurrentLineMap(), writer, commentPos, commentEnd, newLine); - emitPos(commentEnd); - - if (hasTrailingNewLine) { - writer.writeLine(); - } - else if (kind === SyntaxKind.MultiLineCommentTrivia) { - writer.writeSpace(" "); - } + writeSynthesizedComment(comment); + if (comment.hasTrailingNewLine || comment.kind === SyntaxKind.SingleLineCommentTrivia) { + writer.writeLine(); } - - function emitLeadingCommentsOfPosition(pos: number) { - if (commentsDisabled || pos === -1) { - return; - } - - emitLeadingComments(pos, /*isEmittedNode*/ true); - } - - function emitTrailingComments(pos: number) { - forEachTrailingCommentToEmit(pos, emitTrailingComment); - } - - function emitTrailingComment(commentPos: number, commentEnd: number, _kind: SyntaxKind, hasTrailingNewLine: boolean) { - if (!shouldWriteComment(currentSourceFile!.text, commentPos)) return; - // trailing comments are emitted at space/*trailing comment1 */space/*trailing comment2*/ - if (!writer.isAtStartOfLine()) { - writer.writeSpace(" "); - } - - emitPos(commentPos); - writeCommentRange(currentSourceFile!.text, getCurrentLineMap(), writer, commentPos, commentEnd, newLine); - emitPos(commentEnd); - - if (hasTrailingNewLine) { + else { + writer.writeSpace(" "); + } + } + function emitTrailingSynthesizedComment(comment: SynthesizedComment) { + if (!writer.isAtStartOfLine()) { + writer.writeSpace(" "); + } + writeSynthesizedComment(comment); + if (comment.hasTrailingNewLine) { + writer.writeLine(); + } + } + function writeSynthesizedComment(comment: SynthesizedComment) { + const text = formatSynthesizedComment(comment); + const lineMap = comment.kind === SyntaxKind.MultiLineCommentTrivia ? computeLineStarts(text) : undefined; + writeCommentRange(text, (lineMap!), writer, 0, text.length, newLine); + } + function formatSynthesizedComment(comment: SynthesizedComment) { + return comment.kind === SyntaxKind.MultiLineCommentTrivia + ? `/*${comment.text}*/` + : `//${comment.text}`; + } + function emitBodyWithDetachedComments(node: Node, detachedRange: TextRange, emitCallback: (node: Node) => void) { + enterComment(); + const { pos, end } = detachedRange; + const emitFlags = getEmitFlags(node); + const skipLeadingComments = pos < 0 || (emitFlags & EmitFlags.NoLeadingComments) !== 0; + const skipTrailingComments = commentsDisabled || end < 0 || (emitFlags & EmitFlags.NoTrailingComments) !== 0; + if (!skipLeadingComments) { + emitDetachedCommentsAndUpdateCommentsInfo(detachedRange); + } + exitComment(); + if (emitFlags & EmitFlags.NoNestedComments && !commentsDisabled) { + commentsDisabled = true; + emitCallback(node); + commentsDisabled = false; + } + else { + emitCallback(node); + } + enterComment(); + if (!skipTrailingComments) { + emitLeadingComments(detachedRange.end, /*isEmittedNode*/ true); + if (hasWrittenComment && !writer.isAtStartOfLine()) { writer.writeLine(); } } - - function emitTrailingCommentsOfPosition(pos: number, prefixSpace?: boolean) { - if (commentsDisabled) { - return; - } - enterComment(); - forEachTrailingCommentToEmit(pos, prefixSpace ? emitTrailingComment : emitTrailingCommentOfPosition); - exitComment(); - } - - function emitTrailingCommentOfPosition(commentPos: number, commentEnd: number, _kind: SyntaxKind, hasTrailingNewLine: boolean) { - // trailing comments of a position are emitted at /*trailing comment1 */space/*trailing comment*/space - - emitPos(commentPos); - writeCommentRange(currentSourceFile!.text, getCurrentLineMap(), writer, commentPos, commentEnd, newLine); - emitPos(commentEnd); - - if (hasTrailingNewLine) { - writer.writeLine(); + exitComment(); + } + function emitLeadingComments(pos: number, isEmittedNode: boolean) { + hasWrittenComment = false; + if (isEmittedNode) { + forEachLeadingCommentToEmit(pos, emitLeadingComment); + } + else if (pos === 0) { + // If the node will not be emitted in JS, remove all the comments(normal, pinned and ///) associated with the node, + // unless it is a triple slash comment at the top of the file. + // For Example: + // /// + // declare var x; + // /// + // interface F {} + // The first /// will NOT be removed while the second one will be removed even though both node will not be emitted + forEachLeadingCommentToEmit(pos, emitTripleSlashLeadingComment); + } + } + function emitTripleSlashLeadingComment(commentPos: number, commentEnd: number, kind: SyntaxKind, hasTrailingNewLine: boolean, rangePos: number) { + if (isTripleSlashComment(commentPos, commentEnd)) { + emitLeadingComment(commentPos, commentEnd, kind, hasTrailingNewLine, rangePos); + } + } + function shouldWriteComment(text: string, pos: number) { + if (printerOptions.onlyPrintJsDocStyle) { + return (isJSDocLikeText(text, pos) || isPinnedComment(text, pos)); + } + return true; + } + function emitLeadingComment(commentPos: number, commentEnd: number, kind: SyntaxKind, hasTrailingNewLine: boolean, rangePos: number) { + if (!shouldWriteComment(currentSourceFile!.text, commentPos)) + return; + if (!hasWrittenComment) { + emitNewLineBeforeLeadingCommentOfPosition(getCurrentLineMap(), writer, rangePos, commentPos); + hasWrittenComment = true; + } + // Leading comments are emitted at /*leading comment1 */space/*leading comment*/space + emitPos(commentPos); + writeCommentRange(currentSourceFile!.text, getCurrentLineMap(), writer, commentPos, commentEnd, newLine); + emitPos(commentEnd); + if (hasTrailingNewLine) { + writer.writeLine(); + } + else if (kind === SyntaxKind.MultiLineCommentTrivia) { + writer.writeSpace(" "); + } + } + function emitLeadingCommentsOfPosition(pos: number) { + if (commentsDisabled || pos === -1) { + return; + } + emitLeadingComments(pos, /*isEmittedNode*/ true); + } + function emitTrailingComments(pos: number) { + forEachTrailingCommentToEmit(pos, emitTrailingComment); + } + function emitTrailingComment(commentPos: number, commentEnd: number, _kind: SyntaxKind, hasTrailingNewLine: boolean) { + if (!shouldWriteComment(currentSourceFile!.text, commentPos)) + return; + // trailing comments are emitted at space/*trailing comment1 */space/*trailing comment2*/ + if (!writer.isAtStartOfLine()) { + writer.writeSpace(" "); + } + emitPos(commentPos); + writeCommentRange(currentSourceFile!.text, getCurrentLineMap(), writer, commentPos, commentEnd, newLine); + emitPos(commentEnd); + if (hasTrailingNewLine) { + writer.writeLine(); + } + } + function emitTrailingCommentsOfPosition(pos: number, prefixSpace?: boolean) { + if (commentsDisabled) { + return; + } + enterComment(); + forEachTrailingCommentToEmit(pos, prefixSpace ? emitTrailingComment : emitTrailingCommentOfPosition); + exitComment(); + } + function emitTrailingCommentOfPosition(commentPos: number, commentEnd: number, _kind: SyntaxKind, hasTrailingNewLine: boolean) { + // trailing comments of a position are emitted at /*trailing comment1 */space/*trailing comment*/space + emitPos(commentPos); + writeCommentRange(currentSourceFile!.text, getCurrentLineMap(), writer, commentPos, commentEnd, newLine); + emitPos(commentEnd); + if (hasTrailingNewLine) { + writer.writeLine(); + } + else { + writer.writeSpace(" "); + } + } + function forEachLeadingCommentToEmit(pos: number, cb: (commentPos: number, commentEnd: number, kind: SyntaxKind, hasTrailingNewLine: boolean, rangePos: number) => void) { + // Emit the leading comments only if the container's pos doesn't match because the container should take care of emitting these comments + if (currentSourceFile && (containerPos === -1 || pos !== containerPos)) { + if (hasDetachedComments(pos)) { + forEachLeadingCommentWithoutDetachedComments(cb); } else { - writer.writeSpace(" "); + forEachLeadingCommentRange(currentSourceFile.text, pos, cb, /*state*/ pos); } } - - function forEachLeadingCommentToEmit(pos: number, cb: (commentPos: number, commentEnd: number, kind: SyntaxKind, hasTrailingNewLine: boolean, rangePos: number) => void) { - // Emit the leading comments only if the container's pos doesn't match because the container should take care of emitting these comments - if (currentSourceFile && (containerPos === -1 || pos !== containerPos)) { - if (hasDetachedComments(pos)) { - forEachLeadingCommentWithoutDetachedComments(cb); - } - else { - forEachLeadingCommentRange(currentSourceFile.text, pos, cb, /*state*/ pos); - } - } + } + function forEachTrailingCommentToEmit(end: number, cb: (commentPos: number, commentEnd: number, kind: SyntaxKind, hasTrailingNewLine: boolean) => void) { + // Emit the trailing comments only if the container's end doesn't match because the container should take care of emitting these comments + if (currentSourceFile && (containerEnd === -1 || (end !== containerEnd && end !== declarationListContainerEnd))) { + forEachTrailingCommentRange(currentSourceFile.text, end, cb); } - - function forEachTrailingCommentToEmit(end: number, cb: (commentPos: number, commentEnd: number, kind: SyntaxKind, hasTrailingNewLine: boolean) => void) { - // Emit the trailing comments only if the container's end doesn't match because the container should take care of emitting these comments - if (currentSourceFile && (containerEnd === -1 || (end !== containerEnd && end !== declarationListContainerEnd))) { - forEachTrailingCommentRange(currentSourceFile.text, end, cb); - } + } + function hasDetachedComments(pos: number) { + return detachedCommentsInfo !== undefined && last(detachedCommentsInfo).nodePos === pos; + } + function forEachLeadingCommentWithoutDetachedComments(cb: (commentPos: number, commentEnd: number, kind: SyntaxKind, hasTrailingNewLine: boolean, rangePos: number) => void) { + // get the leading comments from detachedPos + const pos = last((detachedCommentsInfo!)).detachedCommentEndPos; + if (detachedCommentsInfo!.length - 1) { + detachedCommentsInfo!.pop(); } - - function hasDetachedComments(pos: number) { - return detachedCommentsInfo !== undefined && last(detachedCommentsInfo).nodePos === pos; + else { + detachedCommentsInfo = undefined; } - - function forEachLeadingCommentWithoutDetachedComments(cb: (commentPos: number, commentEnd: number, kind: SyntaxKind, hasTrailingNewLine: boolean, rangePos: number) => void) { - // get the leading comments from detachedPos - const pos = last(detachedCommentsInfo!).detachedCommentEndPos; - if (detachedCommentsInfo!.length - 1) { - detachedCommentsInfo!.pop(); + forEachLeadingCommentRange(currentSourceFile!.text, pos, cb, /*state*/ pos); + } + function emitDetachedCommentsAndUpdateCommentsInfo(range: TextRange) { + const currentDetachedCommentInfo = emitDetachedComments(currentSourceFile!.text, getCurrentLineMap(), writer, emitComment, range, newLine, commentsDisabled); + if (currentDetachedCommentInfo) { + if (detachedCommentsInfo) { + detachedCommentsInfo.push(currentDetachedCommentInfo); } else { - detachedCommentsInfo = undefined; - } - - forEachLeadingCommentRange(currentSourceFile!.text, pos, cb, /*state*/ pos); - } - - function emitDetachedCommentsAndUpdateCommentsInfo(range: TextRange) { - const currentDetachedCommentInfo = emitDetachedComments(currentSourceFile!.text, getCurrentLineMap(), writer, emitComment, range, newLine, commentsDisabled); - if (currentDetachedCommentInfo) { - if (detachedCommentsInfo) { - detachedCommentsInfo.push(currentDetachedCommentInfo); - } - else { - detachedCommentsInfo = [currentDetachedCommentInfo]; - } + detachedCommentsInfo = [currentDetachedCommentInfo]; } } - - function emitComment(text: string, lineMap: number[], writer: EmitTextWriter, commentPos: number, commentEnd: number, newLine: string) { - if (!shouldWriteComment(currentSourceFile!.text, commentPos)) return; - emitPos(commentPos); - writeCommentRange(text, lineMap, writer, commentPos, commentEnd, newLine); - emitPos(commentEnd); - } - - /** - * Determine if the given comment is a triple-slash - * - * @return true if the comment is a triple-slash comment else false - */ - function isTripleSlashComment(commentPos: number, commentEnd: number) { - return isRecognizedTripleSlashComment(currentSourceFile!.text, commentPos, commentEnd); - } - - // Source Maps - - function getParsedSourceMap(node: UnparsedSource) { - if (node.parsedSourceMap === undefined && node.sourceMapText !== undefined) { - node.parsedSourceMap = tryParseRawSourceMap(node.sourceMapText) || false; + } + function emitComment(text: string, lineMap: number[], writer: EmitTextWriter, commentPos: number, commentEnd: number, newLine: string) { + if (!shouldWriteComment(currentSourceFile!.text, commentPos)) + return; + emitPos(commentPos); + writeCommentRange(text, lineMap, writer, commentPos, commentEnd, newLine); + emitPos(commentEnd); + } + /** + * Determine if the given comment is a triple-slash + * + * @return true if the comment is a triple-slash comment else false + */ + function isTripleSlashComment(commentPos: number, commentEnd: number) { + return isRecognizedTripleSlashComment(currentSourceFile!.text, commentPos, commentEnd); + } + // Source Maps + function getParsedSourceMap(node: UnparsedSource) { + if (node.parsedSourceMap === undefined && node.sourceMapText !== undefined) { + node.parsedSourceMap = tryParseRawSourceMap(node.sourceMapText) || false; + } + return node.parsedSourceMap || undefined; + } + function pipelineEmitWithSourceMap(hint: EmitHint, node: Node) { + Debug.assert(lastNode === node || lastSubstitution === node); + const pipelinePhase = getNextPipelinePhase(PipelinePhase.SourceMaps, hint, node); + if (isUnparsedSource(node) || isUnparsedPrepend(node)) { + pipelinePhase(hint, node); + } + else if (isUnparsedNode(node)) { + const parsed = getParsedSourceMap(node.parent); + if (parsed && sourceMapGenerator) { + sourceMapGenerator.appendSourceMap(writer.getLine(), writer.getColumn(), parsed, node.parent.sourceMapPath!, node.parent.getLineAndCharacterOfPosition(node.pos), node.parent.getLineAndCharacterOfPosition(node.end)); } - return node.parsedSourceMap || undefined; + pipelinePhase(hint, node); } - - function pipelineEmitWithSourceMap(hint: EmitHint, node: Node) { - Debug.assert(lastNode === node || lastSubstitution === node); - const pipelinePhase = getNextPipelinePhase(PipelinePhase.SourceMaps, hint, node); - if (isUnparsedSource(node) || isUnparsedPrepend(node)) { - pipelinePhase(hint, node); + else { + const { pos, end, source = sourceMapSource } = getSourceMapRange(node); + const emitFlags = getEmitFlags(node); + if (node.kind !== SyntaxKind.NotEmittedStatement + && (emitFlags & EmitFlags.NoLeadingSourceMap) === 0 + && pos >= 0) { + emitSourcePos(source, skipSourceTrivia(source, pos)); } - else if (isUnparsedNode(node)) { - const parsed = getParsedSourceMap(node.parent); - if (parsed && sourceMapGenerator) { - sourceMapGenerator.appendSourceMap( - writer.getLine(), - writer.getColumn(), - parsed, - node.parent.sourceMapPath!, - node.parent.getLineAndCharacterOfPosition(node.pos), - node.parent.getLineAndCharacterOfPosition(node.end) - ); - } + if (emitFlags & EmitFlags.NoNestedSourceMaps) { + sourceMapsDisabled = true; pipelinePhase(hint, node); + sourceMapsDisabled = false; } else { - const { pos, end, source = sourceMapSource } = getSourceMapRange(node); - const emitFlags = getEmitFlags(node); - if (node.kind !== SyntaxKind.NotEmittedStatement - && (emitFlags & EmitFlags.NoLeadingSourceMap) === 0 - && pos >= 0) { - emitSourcePos(source, skipSourceTrivia(source, pos)); - } - - if (emitFlags & EmitFlags.NoNestedSourceMaps) { - sourceMapsDisabled = true; - pipelinePhase(hint, node); - sourceMapsDisabled = false; - } - else { - pipelinePhase(hint, node); - } - - if (node.kind !== SyntaxKind.NotEmittedStatement - && (emitFlags & EmitFlags.NoTrailingSourceMap) === 0 - && end >= 0) { - emitSourcePos(source, end); - } - } - Debug.assert(lastNode === node || lastSubstitution === node); - } - - /** - * Skips trivia such as comments and white-space that can be optionally overridden by the source-map source - */ - function skipSourceTrivia(source: SourceMapSource, pos: number): number { - return source.skipTrivia ? source.skipTrivia(pos) : skipTrivia(source.text, pos); - } - - /** - * Emits a mapping. - * - * If the position is synthetic (undefined or a negative value), no mapping will be - * created. - * - * @param pos The position. - */ - function emitPos(pos: number) { - if (sourceMapsDisabled || positionIsSynthesized(pos) || isJsonSourceMapSource(sourceMapSource)) { - return; - } - - const { line: sourceLine, character: sourceCharacter } = getLineAndCharacterOfPosition(sourceMapSource, pos); - sourceMapGenerator!.addMapping( - writer.getLine(), - writer.getColumn(), - sourceMapSourceIndex, - sourceLine, - sourceCharacter, - /*nameIndex*/ undefined); - } - - function emitSourcePos(source: SourceMapSource, pos: number) { - if (source !== sourceMapSource) { - const savedSourceMapSource = sourceMapSource; - setSourceMapSource(source); - emitPos(pos); - setSourceMapSource(savedSourceMapSource); + pipelinePhase(hint, node); } - else { - emitPos(pos); + if (node.kind !== SyntaxKind.NotEmittedStatement + && (emitFlags & EmitFlags.NoTrailingSourceMap) === 0 + && end >= 0) { + emitSourcePos(source, end); } } - - /** - * Emits a token of a node with possible leading and trailing source maps. - * - * @param node The node containing the token. - * @param token The token to emit. - * @param tokenStartPos The start pos of the token. - * @param emitCallback The callback used to emit the token. - */ - function emitTokenWithSourceMap(node: Node | undefined, token: SyntaxKind, writer: (s: string) => void, tokenPos: number, emitCallback: (token: SyntaxKind, writer: (s: string) => void, tokenStartPos: number) => number) { - if (sourceMapsDisabled || node && isInJsonFile(node)) { - return emitCallback(token, writer, tokenPos); - } - - const emitNode = node && node.emitNode; - const emitFlags = emitNode && emitNode.flags || EmitFlags.None; - const range = emitNode && emitNode.tokenSourceMapRanges && emitNode.tokenSourceMapRanges[token]; - const source = range && range.source || sourceMapSource; - - tokenPos = skipSourceTrivia(source, range ? range.pos : tokenPos); - if ((emitFlags & EmitFlags.NoTokenLeadingSourceMaps) === 0 && tokenPos >= 0) { - emitSourcePos(source, tokenPos); - } - - tokenPos = emitCallback(token, writer, tokenPos); - - if (range) tokenPos = range.end; - if ((emitFlags & EmitFlags.NoTokenTrailingSourceMaps) === 0 && tokenPos >= 0) { - emitSourcePos(source, tokenPos); - } - - return tokenPos; + Debug.assert(lastNode === node || lastSubstitution === node); + } + /** + * Skips trivia such as comments and white-space that can be optionally overridden by the source-map source + */ + function skipSourceTrivia(source: SourceMapSource, pos: number): number { + return source.skipTrivia ? source.skipTrivia(pos) : skipTrivia(source.text, pos); + } + /** + * Emits a mapping. + * + * If the position is synthetic (undefined or a negative value), no mapping will be + * created. + * + * @param pos The position. + */ + function emitPos(pos: number) { + if (sourceMapsDisabled || positionIsSynthesized(pos) || isJsonSourceMapSource(sourceMapSource)) { + return; } - - function setSourceMapSource(source: SourceMapSource) { - if (sourceMapsDisabled) { - return; - } - - sourceMapSource = source; - - if (isJsonSourceMapSource(source)) { - return; - } - - sourceMapSourceIndex = sourceMapGenerator!.addSource(source.fileName); - if (printerOptions.inlineSources) { - sourceMapGenerator!.setSourceContent(sourceMapSourceIndex, source.text); - } + const { line: sourceLine, character: sourceCharacter } = getLineAndCharacterOfPosition(sourceMapSource, pos); + sourceMapGenerator!.addMapping(writer.getLine(), writer.getColumn(), sourceMapSourceIndex, sourceLine, sourceCharacter, + /*nameIndex*/ undefined); + } + function emitSourcePos(source: SourceMapSource, pos: number) { + if (source !== sourceMapSource) { + const savedSourceMapSource = sourceMapSource; + setSourceMapSource(source); + emitPos(pos); + setSourceMapSource(savedSourceMapSource); } - - function isJsonSourceMapSource(sourceFile: SourceMapSource) { - return fileExtensionIs(sourceFile.fileName, Extension.Json); + else { + emitPos(pos); } } - - function createBracketsMap() { - const brackets: string[][] = []; - brackets[ListFormat.Braces] = ["{", "}"]; - brackets[ListFormat.Parenthesis] = ["(", ")"]; - brackets[ListFormat.AngleBrackets] = ["<", ">"]; - brackets[ListFormat.SquareBrackets] = ["[", "]"]; - return brackets; - } - - function getOpeningBracket(format: ListFormat) { - return brackets[format & ListFormat.BracketsMask][0]; + /** + * Emits a token of a node with possible leading and trailing source maps. + * + * @param node The node containing the token. + * @param token The token to emit. + * @param tokenStartPos The start pos of the token. + * @param emitCallback The callback used to emit the token. + */ + function emitTokenWithSourceMap(node: Node | undefined, token: SyntaxKind, writer: (s: string) => void, tokenPos: number, emitCallback: (token: SyntaxKind, writer: (s: string) => void, tokenStartPos: number) => number) { + if (sourceMapsDisabled || node && isInJsonFile(node)) { + return emitCallback(token, writer, tokenPos); + } + const emitNode = node && node.emitNode; + const emitFlags = emitNode && emitNode.flags || EmitFlags.None; + const range = emitNode && emitNode.tokenSourceMapRanges && emitNode.tokenSourceMapRanges[token]; + const source = range && range.source || sourceMapSource; + tokenPos = skipSourceTrivia(source, range ? range.pos : tokenPos); + if ((emitFlags & EmitFlags.NoTokenLeadingSourceMaps) === 0 && tokenPos >= 0) { + emitSourcePos(source, tokenPos); + } + tokenPos = emitCallback(token, writer, tokenPos); + if (range) + tokenPos = range.end; + if ((emitFlags & EmitFlags.NoTokenTrailingSourceMaps) === 0 && tokenPos >= 0) { + emitSourcePos(source, tokenPos); + } + return tokenPos; } - - function getClosingBracket(format: ListFormat) { - return brackets[format & ListFormat.BracketsMask][1]; + function setSourceMapSource(source: SourceMapSource) { + if (sourceMapsDisabled) { + return; + } + sourceMapSource = source; + if (isJsonSourceMapSource(source)) { + return; + } + sourceMapSourceIndex = sourceMapGenerator!.addSource(source.fileName); + if (printerOptions.inlineSources) { + sourceMapGenerator!.setSourceContent(sourceMapSourceIndex, source.text); + } } - - // Flags enum to track count of temp variables and a few dedicated names - const enum TempFlags { - Auto = 0x00000000, // No preferred name - CountMask = 0x0FFFFFFF, // Temp variable counter - _i = 0x10000000, // Use/preference flag for '_i' + function isJsonSourceMapSource(sourceFile: SourceMapSource) { + return fileExtensionIs(sourceFile.fileName, Extension.Json); } } +function createBracketsMap() { + const brackets: string[][] = []; + brackets[ListFormat.Braces] = ["{", "}"]; + brackets[ListFormat.Parenthesis] = ["(", ")"]; + brackets[ListFormat.AngleBrackets] = ["<", ">"]; + brackets[ListFormat.SquareBrackets] = ["[", "]"]; + return brackets; +} +function getOpeningBracket(format: ListFormat) { + return brackets[format & ListFormat.BracketsMask][0]; +} +function getClosingBracket(format: ListFormat) { + return brackets[format & ListFormat.BracketsMask][1]; +} +// Flags enum to track count of temp variables and a few dedicated names +const enum TempFlags { + Auto = 0x00000000, + CountMask = 0x0FFFFFFF, + _i = 0x10000000 +} diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index 43c44599413b1..76b65aec211a0 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -1,217 +1,167 @@ +import { TransformationContext, noop, returnUndefined, notImplemented, Expression, createStrictEquality, createVoidZero, createTypeOf, createLiteral, PropertyName, TextRange, MemberExpression, isComputedPropertyName, setTextRange, createElementAccess, isIdentifier, isPrivateIdentifier, createPropertyAccess, getOrCreateEmitNode, EmitFlags, createCall, createIdentifier, JsxOpeningLikeElement, JsxOpeningFragment, NodeFlags, getParseTreeNode, EntityName, isQualifiedName, idText, LeftHandSideExpression, createNull, setEmitFlags, UnscopedEmitHelper, ForInitializer, Statement, isVariableDeclarationList, first, updateVariableDeclaration, createVariableStatement, updateVariableDeclarationList, createAssignment, createStatement, isBlock, updateBlock, createNodeArray, createBlock, LabeledStatement, updateLabel, SyntaxKind, skipParentheses, ArrayLiteralExpression, ObjectLiteralExpression, Identifier, ScriptTarget, isSuperProperty, createThis, PrimaryExpression, getEmitFlags, PropertyAccessExpression, createTempVariable, ElementAccessExpression, createCommaList, reduceLeft, createComma, getMutableClone, PrivateIdentifier, ObjectLiteralElementLike, Debug, NodeArray, Declaration, AccessorDeclaration, getAllAccessorDeclarations, createFunctionExpression, setOriginalNode, createPropertyAssignment, createFalse, createTrue, createObjectLiteral, aggregateTransformFlags, PropertyAssignment, ShorthandPropertyAssignment, getSynthesizedClone, MethodDeclaration, getNameOfDeclaration, isGeneratedIdentifier, getGeneratedNameForNode, hasModifier, ModifierFlags, nodeIsSynthesized, ConciseBody, Block, createReturn, FunctionDeclaration, getStartsOnNewLine, setStartsOnNewLine, ExpressionStatement, isStringLiteral, Node, VisitResult, isPrologueDirective, append, visitNode, isStatement, firstOrUndefined, skipPartiallyEmittedExpressions, createParen, getOperatorPrecedence, getOperatorAssociativity, getExpressionPrecedence, compareValues, Comparison, Associativity, isBinaryExpression, isLiteralKind, getExpressionAssociativity, BinaryExpression, NewExpression, isLeftHandSideExpression, isUnaryExpression, isCallExpression, TypeNode, createParenthesizedType, sameMap, some, isFunctionOrConstructorTypeNode, PostfixUnaryExpression, ConditionalExpression, TaggedTemplateExpression, CallExpression, AsExpression, NonNullExpression, PartiallyEmittedExpression, Token, CommaListExpression, ParenthesizedExpression, TypeAssertion, isAssertionExpression, AssertionExpression, updateParen, updateTypeAssertion, updateAsExpression, updateNonNullExpression, updatePartiallyEmittedExpression, getSourceMapRange, getCommentRange, getSyntheticLeadingComments, getSyntheticTrailingComments, SourceFile, getOriginalNode, isSourceFile, CompilerOptions, isEffectiveExternalModule, NamedImportBindings, getEmitModuleKind, ModuleKind, getEmitHelpers, pushIfUnique, compareStringsCaseSensitive, createNamedImports, map, isFileLevelUniqueName, createImportSpecifier, createNamespaceImport, createImportDeclaration, createImportClause, externalHelpersModuleNameText, addEmitFlags, createUniqueName, ImportDeclaration, ExportDeclaration, ImportEqualsDeclaration, getNamespaceDeclarationNode, isDefaultImport, getSourceTextOfNodeFromSourceFile, EmitHost, EmitResolver, getExternalModuleName, StringLiteral, LiteralExpression, getExternalModuleNameFromPath, BindingOrAssignmentElement, isDeclarationBindingElement, isPropertyAssignment, isAssignmentExpression, isShorthandPropertyAssignment, isSpreadElement, BindingOrAssignmentElementTarget, isObjectLiteralElementLike, BindingOrAssignmentElementRestIndicator, isSpreadAssignment, isPropertyName, NumericLiteral, BindingOrAssignmentPattern, isBindingElement, createSpread, isExpression, createSpreadAssignment, createShorthandPropertyAssignment, AssignmentPattern, ObjectBindingOrAssignmentPattern, isObjectBindingPattern, isObjectLiteralExpression, ArrayBindingOrAssignmentPattern, isArrayBindingPattern, createArrayLiteral, isArrayLiteralExpression, isBindingPattern } from "./ts"; /* @internal */ -namespace ts { - export const nullTransformationContext: TransformationContext = { - enableEmitNotification: noop, - enableSubstitution: noop, - endLexicalEnvironment: returnUndefined, - getCompilerOptions: notImplemented, - getEmitHost: notImplemented, - getEmitResolver: notImplemented, - hoistFunctionDeclaration: noop, - hoistVariableDeclaration: noop, - isEmitNotificationEnabled: notImplemented, - isSubstitutionEnabled: notImplemented, - onEmitNode: noop, - onSubstituteNode: notImplemented, - readEmitHelpers: notImplemented, - requestEmitHelper: noop, - resumeLexicalEnvironment: noop, - startLexicalEnvironment: noop, - suspendLexicalEnvironment: noop, - addDiagnostic: noop, - }; - - // Compound nodes - - export type TypeOfTag = "undefined" | "number" | "boolean" | "string" | "symbol" | "object" | "function"; - - export function createTypeCheck(value: Expression, tag: TypeOfTag) { - return tag === "undefined" - ? createStrictEquality(value, createVoidZero()) - : createStrictEquality(createTypeOf(value), createLiteral(tag)); - } - - export function createMemberAccessForPropertyName(target: Expression, memberName: PropertyName, location?: TextRange): MemberExpression { - if (isComputedPropertyName(memberName)) { - return setTextRange(createElementAccess(target, memberName.expression), location); - } - else { - const expression = setTextRange( - (isIdentifier(memberName) || isPrivateIdentifier(memberName)) - ? createPropertyAccess(target, memberName) - : createElementAccess(target, memberName), - memberName - ); - getOrCreateEmitNode(expression).flags |= EmitFlags.NoNestedSourceMaps; - return expression; - } +export const nullTransformationContext: TransformationContext = { + enableEmitNotification: noop, + enableSubstitution: noop, + endLexicalEnvironment: returnUndefined, + getCompilerOptions: notImplemented, + getEmitHost: notImplemented, + getEmitResolver: notImplemented, + hoistFunctionDeclaration: noop, + hoistVariableDeclaration: noop, + isEmitNotificationEnabled: notImplemented, + isSubstitutionEnabled: notImplemented, + onEmitNode: noop, + onSubstituteNode: notImplemented, + readEmitHelpers: notImplemented, + requestEmitHelper: noop, + resumeLexicalEnvironment: noop, + startLexicalEnvironment: noop, + suspendLexicalEnvironment: noop, + addDiagnostic: noop, +}; +// Compound nodes +/* @internal */ +export type TypeOfTag = "undefined" | "number" | "boolean" | "string" | "symbol" | "object" | "function"; +/* @internal */ +export function createTypeCheck(value: Expression, tag: TypeOfTag) { + return tag === "undefined" + ? createStrictEquality(value, createVoidZero()) + : createStrictEquality(createTypeOf(value), createLiteral(tag)); +} +/* @internal */ +export function createMemberAccessForPropertyName(target: Expression, memberName: PropertyName, location?: TextRange): MemberExpression { + if (isComputedPropertyName(memberName)) { + return setTextRange(createElementAccess(target, memberName.expression), location); + } + else { + const expression = setTextRange((isIdentifier(memberName) || isPrivateIdentifier(memberName)) + ? createPropertyAccess(target, memberName) + : createElementAccess(target, memberName), memberName); + getOrCreateEmitNode(expression).flags |= EmitFlags.NoNestedSourceMaps; + return expression; } - - export function createFunctionCall(func: Expression, thisArg: Expression, argumentsList: readonly Expression[], location?: TextRange) { - return setTextRange( - createCall( - createPropertyAccess(func, "call"), - /*typeArguments*/ undefined, - [ - thisArg, - ...argumentsList - ]), - location - ); - } - - export function createFunctionApply(func: Expression, thisArg: Expression, argumentsExpression: Expression, location?: TextRange) { - return setTextRange( - createCall( - createPropertyAccess(func, "apply"), - /*typeArguments*/ undefined, - [ - thisArg, - argumentsExpression - ] - ), - location - ); - } - - export function createArraySlice(array: Expression, start?: number | Expression) { - const argumentsList: Expression[] = []; - if (start !== undefined) { - argumentsList.push(typeof start === "number" ? createLiteral(start) : start); - } - - return createCall(createPropertyAccess(array, "slice"), /*typeArguments*/ undefined, argumentsList); - } - - export function createArrayConcat(array: Expression, values: readonly Expression[]) { - return createCall( - createPropertyAccess(array, "concat"), - /*typeArguments*/ undefined, - values - ); - } - - export function createMathPow(left: Expression, right: Expression, location?: TextRange) { - return setTextRange( - createCall( - createPropertyAccess(createIdentifier("Math"), "pow"), - /*typeArguments*/ undefined, - [left, right] - ), - location - ); - } - - function createReactNamespace(reactNamespace: string, parent: JsxOpeningLikeElement | JsxOpeningFragment) { - // To ensure the emit resolver can properly resolve the namespace, we need to - // treat this identifier as if it were a source tree node by clearing the `Synthesized` - // flag and setting a parent node. - const react = createIdentifier(reactNamespace || "React"); - react.flags &= ~NodeFlags.Synthesized; - // Set the parent that is in parse tree - // this makes sure that parent chain is intact for checker to traverse complete scope tree - react.parent = getParseTreeNode(parent); - return react; - } - - function createJsxFactoryExpressionFromEntityName(jsxFactory: EntityName, parent: JsxOpeningLikeElement | JsxOpeningFragment): Expression { - if (isQualifiedName(jsxFactory)) { - const left = createJsxFactoryExpressionFromEntityName(jsxFactory.left, parent); - const right = createIdentifier(idText(jsxFactory.right)); - right.escapedText = jsxFactory.right.escapedText; - return createPropertyAccess(left, right); +} +/* @internal */ +export function createFunctionCall(func: Expression, thisArg: Expression, argumentsList: readonly Expression[], location?: TextRange) { + return setTextRange(createCall(createPropertyAccess(func, "call"), + /*typeArguments*/ undefined, [ + thisArg, + ...argumentsList + ]), location); +} +/* @internal */ +export function createFunctionApply(func: Expression, thisArg: Expression, argumentsExpression: Expression, location?: TextRange) { + return setTextRange(createCall(createPropertyAccess(func, "apply"), + /*typeArguments*/ undefined, [ + thisArg, + argumentsExpression + ]), location); +} +/* @internal */ +export function createArraySlice(array: Expression, start?: number | Expression) { + const argumentsList: Expression[] = []; + if (start !== undefined) { + argumentsList.push(typeof start === "number" ? createLiteral(start) : start); + } + return createCall(createPropertyAccess(array, "slice"), /*typeArguments*/ undefined, argumentsList); +} +/* @internal */ +export function createArrayConcat(array: Expression, values: readonly Expression[]) { + return createCall(createPropertyAccess(array, "concat"), + /*typeArguments*/ undefined, values); +} +/* @internal */ +export function createMathPow(left: Expression, right: Expression, location?: TextRange) { + return setTextRange(createCall(createPropertyAccess(createIdentifier("Math"), "pow"), + /*typeArguments*/ undefined, [left, right]), location); +} +/* @internal */ +function createReactNamespace(reactNamespace: string, parent: JsxOpeningLikeElement | JsxOpeningFragment) { + // To ensure the emit resolver can properly resolve the namespace, we need to + // treat this identifier as if it were a source tree node by clearing the `Synthesized` + // flag and setting a parent node. + const react = createIdentifier(reactNamespace || "React"); + react.flags &= ~NodeFlags.Synthesized; + // Set the parent that is in parse tree + // this makes sure that parent chain is intact for checker to traverse complete scope tree + react.parent = getParseTreeNode(parent); + return react; +} +/* @internal */ +function createJsxFactoryExpressionFromEntityName(jsxFactory: EntityName, parent: JsxOpeningLikeElement | JsxOpeningFragment): Expression { + if (isQualifiedName(jsxFactory)) { + const left = createJsxFactoryExpressionFromEntityName(jsxFactory.left, parent); + const right = createIdentifier(idText(jsxFactory.right)); + right.escapedText = jsxFactory.right.escapedText; + return createPropertyAccess(left, right); + } + else { + return createReactNamespace(idText(jsxFactory), parent); + } +} +/* @internal */ +function createJsxFactoryExpression(jsxFactoryEntity: EntityName | undefined, reactNamespace: string, parent: JsxOpeningLikeElement | JsxOpeningFragment): Expression { + return jsxFactoryEntity ? + createJsxFactoryExpressionFromEntityName(jsxFactoryEntity, parent) : + createPropertyAccess(createReactNamespace(reactNamespace, parent), "createElement"); +} +/* @internal */ +export function createExpressionForJsxElement(jsxFactoryEntity: EntityName | undefined, reactNamespace: string, tagName: Expression, props: Expression, children: readonly Expression[], parentElement: JsxOpeningLikeElement, location: TextRange): LeftHandSideExpression { + const argumentsList = [tagName]; + if (props) { + argumentsList.push(props); + } + if (children && children.length > 0) { + if (!props) { + argumentsList.push(createNull()); + } + if (children.length > 1) { + for (const child of children) { + startOnNewLine(child); + argumentsList.push(child); + } } else { - return createReactNamespace(idText(jsxFactory), parent); + argumentsList.push(children[0]); } } - - function createJsxFactoryExpression(jsxFactoryEntity: EntityName | undefined, reactNamespace: string, parent: JsxOpeningLikeElement | JsxOpeningFragment): Expression { - return jsxFactoryEntity ? - createJsxFactoryExpressionFromEntityName(jsxFactoryEntity, parent) : - createPropertyAccess( - createReactNamespace(reactNamespace, parent), - "createElement" - ); - } - - export function createExpressionForJsxElement(jsxFactoryEntity: EntityName | undefined, reactNamespace: string, tagName: Expression, props: Expression, children: readonly Expression[], parentElement: JsxOpeningLikeElement, location: TextRange): LeftHandSideExpression { - const argumentsList = [tagName]; - if (props) { - argumentsList.push(props); - } - - if (children && children.length > 0) { - if (!props) { - argumentsList.push(createNull()); - } - - if (children.length > 1) { - for (const child of children) { - startOnNewLine(child); - argumentsList.push(child); - } - } - else { - argumentsList.push(children[0]); + return setTextRange(createCall(createJsxFactoryExpression(jsxFactoryEntity, reactNamespace, parentElement), + /*typeArguments*/ undefined, argumentsList), location); +} +/* @internal */ +export function createExpressionForJsxFragment(jsxFactoryEntity: EntityName | undefined, reactNamespace: string, children: readonly Expression[], parentElement: JsxOpeningFragment, location: TextRange): LeftHandSideExpression { + const tagName = createPropertyAccess(createReactNamespace(reactNamespace, parentElement), "Fragment"); + const argumentsList = [(tagName)]; + argumentsList.push(createNull()); + if (children && children.length > 0) { + if (children.length > 1) { + for (const child of children) { + startOnNewLine(child); + argumentsList.push(child); } } - - return setTextRange( - createCall( - createJsxFactoryExpression(jsxFactoryEntity, reactNamespace, parentElement), - /*typeArguments*/ undefined, - argumentsList - ), - location - ); - } - - export function createExpressionForJsxFragment(jsxFactoryEntity: EntityName | undefined, reactNamespace: string, children: readonly Expression[], parentElement: JsxOpeningFragment, location: TextRange): LeftHandSideExpression { - const tagName = createPropertyAccess( - createReactNamespace(reactNamespace, parentElement), - "Fragment" - ); - - const argumentsList = [tagName]; - argumentsList.push(createNull()); - - if (children && children.length > 0) { - if (children.length > 1) { - for (const child of children) { - startOnNewLine(child); - argumentsList.push(child); - } - } - else { - argumentsList.push(children[0]); - } + else { + argumentsList.push(children[0]); } - - return setTextRange( - createCall( - createJsxFactoryExpression(jsxFactoryEntity, reactNamespace, parentElement), - /*typeArguments*/ undefined, - argumentsList - ), - location - ); - } - - // Helpers - - /** - * Gets an identifier for the name of an *unscoped* emit helper. - */ - export function getUnscopedHelperName(name: string) { - return setEmitFlags(createIdentifier(name), EmitFlags.HelperName | EmitFlags.AdviseOnEmitNode); - } - - export const valuesHelper: UnscopedEmitHelper = { - name: "typescript:values", - importName: "__values", - scoped: false, - text: ` + } + return setTextRange(createCall(createJsxFactoryExpression(jsxFactoryEntity, reactNamespace, parentElement), + /*typeArguments*/ undefined, argumentsList), location); +} +// Helpers +/** + * Gets an identifier for the name of an *unscoped* emit helper. + */ +/* @internal */ +export function getUnscopedHelperName(name: string) { + return setEmitFlags(createIdentifier(name), EmitFlags.HelperName | EmitFlags.AdviseOnEmitNode); +} +/* @internal */ +export const valuesHelper: UnscopedEmitHelper = { + name: "typescript:values", + importName: "__values", + scoped: false, + text: ` var __values = (this && this.__values) || function(o) { var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; if (m) return m.call(o); @@ -223,25 +173,19 @@ namespace ts { }; throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); };` - }; - - export function createValuesHelper(context: TransformationContext, expression: Expression, location?: TextRange) { - context.requestEmitHelper(valuesHelper); - return setTextRange( - createCall( - getUnscopedHelperName("__values"), - /*typeArguments*/ undefined, - [expression] - ), - location - ); - } - - export const readHelper: UnscopedEmitHelper = { - name: "typescript:read", - importName: "__read", - scoped: false, - text: ` +}; +/* @internal */ +export function createValuesHelper(context: TransformationContext, expression: Expression, location?: TextRange) { + context.requestEmitHelper(valuesHelper); + return setTextRange(createCall(getUnscopedHelperName("__values"), + /*typeArguments*/ undefined, [expression]), location); +} +/* @internal */ +export const readHelper: UnscopedEmitHelper = { + name: "typescript:read", + importName: "__read", + scoped: false, + text: ` var __read = (this && this.__read) || function (o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; @@ -258,51 +202,39 @@ namespace ts { } return ar; };` - }; - - export function createReadHelper(context: TransformationContext, iteratorRecord: Expression, count: number | undefined, location?: TextRange) { - context.requestEmitHelper(readHelper); - return setTextRange( - createCall( - getUnscopedHelperName("__read"), - /*typeArguments*/ undefined, - count !== undefined - ? [iteratorRecord, createLiteral(count)] - : [iteratorRecord] - ), - location - ); - } - - export const spreadHelper: UnscopedEmitHelper = { - name: "typescript:spread", - importName: "__spread", - scoped: false, - dependencies: [readHelper], - text: ` +}; +/* @internal */ +export function createReadHelper(context: TransformationContext, iteratorRecord: Expression, count: number | undefined, location?: TextRange) { + context.requestEmitHelper(readHelper); + return setTextRange(createCall(getUnscopedHelperName("__read"), + /*typeArguments*/ undefined, count !== undefined + ? [iteratorRecord, createLiteral(count)] + : [iteratorRecord]), location); +} +/* @internal */ +export const spreadHelper: UnscopedEmitHelper = { + name: "typescript:spread", + importName: "__spread", + scoped: false, + dependencies: [readHelper], + text: ` var __spread = (this && this.__spread) || function () { for (var ar = [], i = 0; i < arguments.length; i++) ar = ar.concat(__read(arguments[i])); return ar; };` - }; - - export function createSpreadHelper(context: TransformationContext, argumentList: readonly Expression[], location?: TextRange) { - context.requestEmitHelper(spreadHelper); - return setTextRange( - createCall( - getUnscopedHelperName("__spread"), - /*typeArguments*/ undefined, - argumentList - ), - location - ); - } - - export const spreadArraysHelper: UnscopedEmitHelper = { - name: "typescript:spreadArrays", - importName: "__spreadArrays", - scoped: false, - text: ` +}; +/* @internal */ +export function createSpreadHelper(context: TransformationContext, argumentList: readonly Expression[], location?: TextRange) { + context.requestEmitHelper(spreadHelper); + return setTextRange(createCall(getUnscopedHelperName("__spread"), + /*typeArguments*/ undefined, argumentList), location); +} +/* @internal */ +export const spreadArraysHelper: UnscopedEmitHelper = { + name: "typescript:spreadArrays", + importName: "__spreadArrays", + scoped: false, + text: ` var __spreadArrays = (this && this.__spreadArrays) || function () { for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length; for (var r = Array(s), k = 0, i = 0; i < il; i++) @@ -310,1605 +242,1431 @@ namespace ts { r[k] = a[j]; return r; };` - }; - - export function createSpreadArraysHelper(context: TransformationContext, argumentList: readonly Expression[], location?: TextRange) { - context.requestEmitHelper(spreadArraysHelper); - return setTextRange( - createCall( - getUnscopedHelperName("__spreadArrays"), - /*typeArguments*/ undefined, - argumentList - ), - location - ); - } - - // Utilities - - export function createForOfBindingStatement(node: ForInitializer, boundValue: Expression): Statement { - if (isVariableDeclarationList(node)) { - const firstDeclaration = first(node.declarations); - const updatedDeclaration = updateVariableDeclaration( - firstDeclaration, - firstDeclaration.name, - /*typeNode*/ undefined, - boundValue - ); - return setTextRange( - createVariableStatement( - /*modifiers*/ undefined, - updateVariableDeclarationList(node, [updatedDeclaration]) - ), - /*location*/ node - ); - } - else { - const updatedExpression = setTextRange(createAssignment(node, boundValue), /*location*/ node); - return setTextRange(createStatement(updatedExpression), /*location*/ node); - } +}; +/* @internal */ +export function createSpreadArraysHelper(context: TransformationContext, argumentList: readonly Expression[], location?: TextRange) { + context.requestEmitHelper(spreadArraysHelper); + return setTextRange(createCall(getUnscopedHelperName("__spreadArrays"), + /*typeArguments*/ undefined, argumentList), location); +} +// Utilities +/* @internal */ +export function createForOfBindingStatement(node: ForInitializer, boundValue: Expression): Statement { + if (isVariableDeclarationList(node)) { + const firstDeclaration = first(node.declarations); + const updatedDeclaration = updateVariableDeclaration(firstDeclaration, firstDeclaration.name, + /*typeNode*/ undefined, boundValue); + return setTextRange(createVariableStatement( + /*modifiers*/ undefined, updateVariableDeclarationList(node, [updatedDeclaration])), + /*location*/ node); + } + else { + const updatedExpression = setTextRange(createAssignment(node, boundValue), /*location*/ node); + return setTextRange(createStatement(updatedExpression), /*location*/ node); } - - export function insertLeadingStatement(dest: Statement, source: Statement) { - if (isBlock(dest)) { - return updateBlock(dest, setTextRange(createNodeArray([source, ...dest.statements]), dest.statements)); - } - else { - return createBlock(createNodeArray([dest, source]), /*multiLine*/ true); - } +} +/* @internal */ +export function insertLeadingStatement(dest: Statement, source: Statement) { + if (isBlock(dest)) { + return updateBlock(dest, setTextRange(createNodeArray([source, ...dest.statements]), dest.statements)); } - - export function restoreEnclosingLabel(node: Statement, outermostLabeledStatement: LabeledStatement | undefined, afterRestoreLabelCallback?: (node: LabeledStatement) => void): Statement { - if (!outermostLabeledStatement) { - return node; - } - const updated = updateLabel( - outermostLabeledStatement, - outermostLabeledStatement.label, - outermostLabeledStatement.statement.kind === SyntaxKind.LabeledStatement - ? restoreEnclosingLabel(node, outermostLabeledStatement.statement) - : node - ); - if (afterRestoreLabelCallback) { - afterRestoreLabelCallback(outermostLabeledStatement); - } - return updated; - } - - export interface CallBinding { - target: LeftHandSideExpression; - thisArg: Expression; - } - - function shouldBeCapturedInTempVariable(node: Expression, cacheIdentifiers: boolean): boolean { - const target = skipParentheses(node); - switch (target.kind) { - case SyntaxKind.Identifier: - return cacheIdentifiers; - case SyntaxKind.ThisKeyword: - case SyntaxKind.NumericLiteral: - case SyntaxKind.BigIntLiteral: - case SyntaxKind.StringLiteral: + else { + return createBlock(createNodeArray([dest, source]), /*multiLine*/ true); + } +} +/* @internal */ +export function restoreEnclosingLabel(node: Statement, outermostLabeledStatement: LabeledStatement | undefined, afterRestoreLabelCallback?: (node: LabeledStatement) => void): Statement { + if (!outermostLabeledStatement) { + return node; + } + const updated = updateLabel(outermostLabeledStatement, outermostLabeledStatement.label, outermostLabeledStatement.statement.kind === SyntaxKind.LabeledStatement + ? restoreEnclosingLabel(node, (outermostLabeledStatement.statement)) + : node); + if (afterRestoreLabelCallback) { + afterRestoreLabelCallback(outermostLabeledStatement); + } + return updated; +} +/* @internal */ +export interface CallBinding { + target: LeftHandSideExpression; + thisArg: Expression; +} +/* @internal */ +function shouldBeCapturedInTempVariable(node: Expression, cacheIdentifiers: boolean): boolean { + const target = skipParentheses(node); + switch (target.kind) { + case SyntaxKind.Identifier: + return cacheIdentifiers; + case SyntaxKind.ThisKeyword: + case SyntaxKind.NumericLiteral: + case SyntaxKind.BigIntLiteral: + case SyntaxKind.StringLiteral: + return false; + case SyntaxKind.ArrayLiteralExpression: + const elements = (target).elements; + if (elements.length === 0) { return false; - case SyntaxKind.ArrayLiteralExpression: - const elements = (target).elements; - if (elements.length === 0) { - return false; - } - return true; - case SyntaxKind.ObjectLiteralExpression: - return (target).properties.length > 0; - default: - return true; - } + } + return true; + case SyntaxKind.ObjectLiteralExpression: + return (target).properties.length > 0; + default: + return true; } - - export function createCallBinding(expression: Expression, recordTempVariable: (temp: Identifier) => void, languageVersion?: ScriptTarget, cacheIdentifiers = false): CallBinding { - const callee = skipOuterExpressions(expression, OuterExpressionKinds.All); - let thisArg: Expression; - let target: LeftHandSideExpression; - if (isSuperProperty(callee)) { - thisArg = createThis(); - target = callee; - } - else if (callee.kind === SyntaxKind.SuperKeyword) { - thisArg = createThis(); - target = languageVersion! < ScriptTarget.ES2015 - ? setTextRange(createIdentifier("_super"), callee) - : callee; - } - else if (getEmitFlags(callee) & EmitFlags.HelperName) { - thisArg = createVoidZero(); - target = parenthesizeForAccess(callee); - } - else { - switch (callee.kind) { - case SyntaxKind.PropertyAccessExpression: { - if (shouldBeCapturedInTempVariable((callee).expression, cacheIdentifiers)) { - // for `a.b()` target is `(_a = a).b` and thisArg is `_a` - thisArg = createTempVariable(recordTempVariable); - target = createPropertyAccess( - setTextRange( - createAssignment( - thisArg, - (callee).expression - ), - (callee).expression - ), - (callee).name - ); - setTextRange(target, callee); - } - else { - thisArg = (callee).expression; - target = callee; - } - break; +} +/* @internal */ +export function createCallBinding(expression: Expression, recordTempVariable: (temp: Identifier) => void, languageVersion?: ScriptTarget, cacheIdentifiers = false): CallBinding { + const callee = skipOuterExpressions(expression, OuterExpressionKinds.All); + let thisArg: Expression; + let target: LeftHandSideExpression; + if (isSuperProperty(callee)) { + thisArg = createThis(); + target = callee; + } + else if (callee.kind === SyntaxKind.SuperKeyword) { + thisArg = createThis(); + target = (languageVersion!) < ScriptTarget.ES2015 + ? setTextRange(createIdentifier("_super"), callee) + : callee; + } + else if (getEmitFlags(callee) & EmitFlags.HelperName) { + thisArg = createVoidZero(); + target = parenthesizeForAccess(callee); + } + else { + switch (callee.kind) { + case SyntaxKind.PropertyAccessExpression: { + if (shouldBeCapturedInTempVariable((callee).expression, cacheIdentifiers)) { + // for `a.b()` target is `(_a = a).b` and thisArg is `_a` + thisArg = createTempVariable(recordTempVariable); + target = createPropertyAccess(setTextRange(createAssignment(thisArg, (callee).expression), (callee).expression), (callee).name); + setTextRange(target, callee); } - - case SyntaxKind.ElementAccessExpression: { - if (shouldBeCapturedInTempVariable((callee).expression, cacheIdentifiers)) { - // for `a[b]()` target is `(_a = a)[b]` and thisArg is `_a` - thisArg = createTempVariable(recordTempVariable); - target = createElementAccess( - setTextRange( - createAssignment( - thisArg, - (callee).expression - ), - (callee).expression - ), - (callee).argumentExpression - ); - setTextRange(target, callee); - } - else { - thisArg = (callee).expression; - target = callee; - } - - break; + else { + thisArg = (callee).expression; + target = (callee); } - - default: { - // for `a()` target is `a` and thisArg is `void 0` - thisArg = createVoidZero(); - target = parenthesizeForAccess(expression); - break; + break; + } + case SyntaxKind.ElementAccessExpression: { + if (shouldBeCapturedInTempVariable((callee).expression, cacheIdentifiers)) { + // for `a[b]()` target is `(_a = a)[b]` and thisArg is `_a` + thisArg = createTempVariable(recordTempVariable); + target = createElementAccess(setTextRange(createAssignment(thisArg, (callee).expression), (callee).expression), (callee).argumentExpression); + setTextRange(target, callee); + } + else { + thisArg = (callee).expression; + target = (callee); } + break; + } + default: { + // for `a()` target is `a` and thisArg is `void 0` + thisArg = createVoidZero(); + target = parenthesizeForAccess(expression); + break; } - } - - return { target, thisArg }; - } - - export function inlineExpressions(expressions: readonly Expression[]) { - // Avoid deeply nested comma expressions as traversing them during emit can result in "Maximum call - // stack size exceeded" errors. - return expressions.length > 10 - ? createCommaList(expressions) - : reduceLeft(expressions, createComma)!; - } - - export function createExpressionFromEntityName(node: EntityName | Expression): Expression { - if (isQualifiedName(node)) { - const left = createExpressionFromEntityName(node.left); - const right = getMutableClone(node.right); - return setTextRange(createPropertyAccess(left, right), node); - } - else { - return getMutableClone(node); } } - - export function createExpressionForPropertyName(memberName: Exclude): Expression { - if (isIdentifier(memberName)) { - return createLiteral(memberName); - } - else if (isComputedPropertyName(memberName)) { - return getMutableClone(memberName.expression); - } - else { - return getMutableClone(memberName); - } + return { target, thisArg }; +} +/* @internal */ +export function inlineExpressions(expressions: readonly Expression[]) { + // Avoid deeply nested comma expressions as traversing them during emit can result in "Maximum call + // stack size exceeded" errors. + return expressions.length > 10 + ? createCommaList(expressions) + : reduceLeft(expressions, createComma)!; +} +/* @internal */ +export function createExpressionFromEntityName(node: EntityName | Expression): Expression { + if (isQualifiedName(node)) { + const left = createExpressionFromEntityName(node.left); + const right = getMutableClone(node.right); + return setTextRange(createPropertyAccess(left, right), node); } - - export function createExpressionForObjectLiteralElementLike(node: ObjectLiteralExpression, property: ObjectLiteralElementLike, receiver: Expression): Expression | undefined { - if (property.name && isPrivateIdentifier(property.name)) { - Debug.failBadSyntaxKind(property.name, "Private identifiers are not allowed in object literals."); - } - switch (property.kind) { - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - return createExpressionForAccessorDeclaration(node.properties, property as typeof property & { name: Exclude; }, receiver, !!node.multiLine); - case SyntaxKind.PropertyAssignment: - return createExpressionForPropertyAssignment(property, receiver); - case SyntaxKind.ShorthandPropertyAssignment: - return createExpressionForShorthandPropertyAssignment(property, receiver); - case SyntaxKind.MethodDeclaration: - return createExpressionForMethodDeclaration(property, receiver); - } + else { + return getMutableClone(node); } - - function createExpressionForAccessorDeclaration(properties: NodeArray, property: AccessorDeclaration & { name: Exclude; }, receiver: Expression, multiLine: boolean) { - const { firstAccessor, getAccessor, setAccessor } = getAllAccessorDeclarations(properties, property); - if (property === firstAccessor) { - const properties: ObjectLiteralElementLike[] = []; - if (getAccessor) { - const getterFunction = createFunctionExpression( - getAccessor.modifiers, - /*asteriskToken*/ undefined, - /*name*/ undefined, - /*typeParameters*/ undefined, - getAccessor.parameters, - /*type*/ undefined, - getAccessor.body! // TODO: GH#18217 - ); - setTextRange(getterFunction, getAccessor); - setOriginalNode(getterFunction, getAccessor); - const getter = createPropertyAssignment("get", getterFunction); - properties.push(getter); - } - - if (setAccessor) { - const setterFunction = createFunctionExpression( - setAccessor.modifiers, - /*asteriskToken*/ undefined, - /*name*/ undefined, - /*typeParameters*/ undefined, - setAccessor.parameters, - /*type*/ undefined, - setAccessor.body! // TODO: GH#18217 - ); - setTextRange(setterFunction, setAccessor); - setOriginalNode(setterFunction, setAccessor); - const setter = createPropertyAssignment("set", setterFunction); - properties.push(setter); - } - - properties.push(createPropertyAssignment("enumerable", getAccessor || setAccessor ? createFalse() : createTrue())); - properties.push(createPropertyAssignment("configurable", createTrue())); - - const expression = setTextRange( - createCall( - createPropertyAccess(createIdentifier("Object"), "defineProperty"), - /*typeArguments*/ undefined, - [ - receiver, - createExpressionForPropertyName(property.name), - createObjectLiteral(properties, multiLine) - ] - ), - /*location*/ firstAccessor +} +/* @internal */ +export function createExpressionForPropertyName(memberName: Exclude): Expression { + if (isIdentifier(memberName)) { + return createLiteral(memberName); + } + else if (isComputedPropertyName(memberName)) { + return getMutableClone(memberName.expression); + } + else { + return getMutableClone(memberName); + } +} +/* @internal */ +export function createExpressionForObjectLiteralElementLike(node: ObjectLiteralExpression, property: ObjectLiteralElementLike, receiver: Expression): Expression | undefined { + if (property.name && isPrivateIdentifier(property.name)) { + Debug.failBadSyntaxKind(property.name, "Private identifiers are not allowed in object literals."); + } + switch (property.kind) { + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + return createExpressionForAccessorDeclaration(node.properties, (property as typeof property & { + name: Exclude; + }), receiver, !!node.multiLine); + case SyntaxKind.PropertyAssignment: + return createExpressionForPropertyAssignment(property, receiver); + case SyntaxKind.ShorthandPropertyAssignment: + return createExpressionForShorthandPropertyAssignment(property, receiver); + case SyntaxKind.MethodDeclaration: + return createExpressionForMethodDeclaration(property, receiver); + } +} +/* @internal */ +function createExpressionForAccessorDeclaration(properties: NodeArray, property: AccessorDeclaration & { + name: Exclude; +}, receiver: Expression, multiLine: boolean) { + const { firstAccessor, getAccessor, setAccessor } = getAllAccessorDeclarations(properties, property); + if (property === firstAccessor) { + const properties: ObjectLiteralElementLike[] = []; + if (getAccessor) { + const getterFunction = createFunctionExpression(getAccessor.modifiers, + /*asteriskToken*/ undefined, + /*name*/ undefined, + /*typeParameters*/ undefined, getAccessor.parameters, + /*type*/ undefined, (getAccessor.body!) // TODO: GH#18217 ); - - return aggregateTransformFlags(expression); - } - - return undefined; + setTextRange(getterFunction, getAccessor); + setOriginalNode(getterFunction, getAccessor); + const getter = createPropertyAssignment("get", getterFunction); + properties.push(getter); + } + if (setAccessor) { + const setterFunction = createFunctionExpression(setAccessor.modifiers, + /*asteriskToken*/ undefined, + /*name*/ undefined, + /*typeParameters*/ undefined, setAccessor.parameters, + /*type*/ undefined, (setAccessor.body!) // TODO: GH#18217 + ); + setTextRange(setterFunction, setAccessor); + setOriginalNode(setterFunction, setAccessor); + const setter = createPropertyAssignment("set", setterFunction); + properties.push(setter); + } + properties.push(createPropertyAssignment("enumerable", getAccessor || setAccessor ? createFalse() : createTrue())); + properties.push(createPropertyAssignment("configurable", createTrue())); + const expression = setTextRange(createCall(createPropertyAccess(createIdentifier("Object"), "defineProperty"), + /*typeArguments*/ undefined, [ + receiver, + createExpressionForPropertyName(property.name), + createObjectLiteral(properties, multiLine) + ]), + /*location*/ firstAccessor); + return aggregateTransformFlags(expression); + } + return undefined; +} +/* @internal */ +function createExpressionForPropertyAssignment(property: PropertyAssignment, receiver: Expression) { + return aggregateTransformFlags(setOriginalNode(setTextRange(createAssignment(createMemberAccessForPropertyName(receiver, property.name, /*location*/ property.name), property.initializer), property), property)); +} +/* @internal */ +function createExpressionForShorthandPropertyAssignment(property: ShorthandPropertyAssignment, receiver: Expression) { + return aggregateTransformFlags(setOriginalNode(setTextRange(createAssignment(createMemberAccessForPropertyName(receiver, property.name, /*location*/ property.name), getSynthesizedClone(property.name)), + /*location*/ property), + /*original*/ property)); +} +/* @internal */ +function createExpressionForMethodDeclaration(method: MethodDeclaration, receiver: Expression) { + return aggregateTransformFlags(setOriginalNode(setTextRange(createAssignment(createMemberAccessForPropertyName(receiver, method.name, /*location*/ method.name), setOriginalNode(setTextRange(createFunctionExpression(method.modifiers, method.asteriskToken, + /*name*/ undefined, + /*typeParameters*/ undefined, method.parameters, + /*type*/ undefined, (method.body!) // TODO: GH#18217 + ), + /*location*/ method), + /*original*/ method)), + /*location*/ method), + /*original*/ method)); +} +/** + * Gets the internal name of a declaration. This is primarily used for declarations that can be + * referred to by name in the body of an ES5 class function body. An internal name will *never* + * be prefixed with an module or namespace export modifier like "exports." when emitted as an + * expression. An internal name will also *never* be renamed due to a collision with a block + * scoped variable. + * + * @param node The declaration. + * @param allowComments A value indicating whether comments may be emitted for the name. + * @param allowSourceMaps A value indicating whether source maps may be emitted for the name. + */ +/* @internal */ +export function getInternalName(node: Declaration, allowComments?: boolean, allowSourceMaps?: boolean) { + return getName(node, allowComments, allowSourceMaps, EmitFlags.LocalName | EmitFlags.InternalName); +} +/** + * Gets whether an identifier should only be referred to by its internal name. + */ +/* @internal */ +export function isInternalName(node: Identifier) { + return (getEmitFlags(node) & EmitFlags.InternalName) !== 0; +} +/** + * Gets the local name of a declaration. This is primarily used for declarations that can be + * referred to by name in the declaration's immediate scope (classes, enums, namespaces). A + * local name will *never* be prefixed with an module or namespace export modifier like + * "exports." when emitted as an expression. + * + * @param node The declaration. + * @param allowComments A value indicating whether comments may be emitted for the name. + * @param allowSourceMaps A value indicating whether source maps may be emitted for the name. + */ +/* @internal */ +export function getLocalName(node: Declaration, allowComments?: boolean, allowSourceMaps?: boolean) { + return getName(node, allowComments, allowSourceMaps, EmitFlags.LocalName); +} +/** + * Gets whether an identifier should only be referred to by its local name. + */ +/* @internal */ +export function isLocalName(node: Identifier) { + return (getEmitFlags(node) & EmitFlags.LocalName) !== 0; +} +/** + * Gets the export name of a declaration. This is primarily used for declarations that can be + * referred to by name in the declaration's immediate scope (classes, enums, namespaces). An + * export name will *always* be prefixed with an module or namespace export modifier like + * `"exports."` when emitted as an expression if the name points to an exported symbol. + * + * @param node The declaration. + * @param allowComments A value indicating whether comments may be emitted for the name. + * @param allowSourceMaps A value indicating whether source maps may be emitted for the name. + */ +/* @internal */ +export function getExportName(node: Declaration, allowComments?: boolean, allowSourceMaps?: boolean): Identifier { + return getName(node, allowComments, allowSourceMaps, EmitFlags.ExportName); +} +/** + * Gets whether an identifier should only be referred to by its export representation if the + * name points to an exported symbol. + */ +/* @internal */ +export function isExportName(node: Identifier) { + return (getEmitFlags(node) & EmitFlags.ExportName) !== 0; +} +/** + * Gets the name of a declaration for use in declarations. + * + * @param node The declaration. + * @param allowComments A value indicating whether comments may be emitted for the name. + * @param allowSourceMaps A value indicating whether source maps may be emitted for the name. + */ +/* @internal */ +export function getDeclarationName(node: Declaration, allowComments?: boolean, allowSourceMaps?: boolean) { + return getName(node, allowComments, allowSourceMaps); +} +/* @internal */ +function getName(node: Declaration, allowComments?: boolean, allowSourceMaps?: boolean, emitFlags: EmitFlags = 0) { + const nodeName = getNameOfDeclaration(node); + if (nodeName && isIdentifier(nodeName) && !isGeneratedIdentifier(nodeName)) { + const name = getMutableClone(nodeName); + emitFlags |= getEmitFlags(nodeName); + if (!allowSourceMaps) + emitFlags |= EmitFlags.NoSourceMap; + if (!allowComments) + emitFlags |= EmitFlags.NoComments; + if (emitFlags) + setEmitFlags(name, emitFlags); + return name; + } + return getGeneratedNameForNode(node); +} +/** + * Gets the exported name of a declaration for use in expressions. + * + * An exported name will *always* be prefixed with an module or namespace export modifier like + * "exports." if the name points to an exported symbol. + * + * @param ns The namespace identifier. + * @param node The declaration. + * @param allowComments A value indicating whether comments may be emitted for the name. + * @param allowSourceMaps A value indicating whether source maps may be emitted for the name. + */ +/* @internal */ +export function getExternalModuleOrNamespaceExportName(ns: Identifier | undefined, node: Declaration, allowComments?: boolean, allowSourceMaps?: boolean): Identifier | PropertyAccessExpression { + if (ns && hasModifier(node, ModifierFlags.Export)) { + return getNamespaceMemberName(ns, getName(node), allowComments, allowSourceMaps); } - - function createExpressionForPropertyAssignment(property: PropertyAssignment, receiver: Expression) { - return aggregateTransformFlags( - setOriginalNode( - setTextRange( - createAssignment( - createMemberAccessForPropertyName(receiver, property.name, /*location*/ property.name), - property.initializer - ), - property - ), - property - ) - ); - } - - function createExpressionForShorthandPropertyAssignment(property: ShorthandPropertyAssignment, receiver: Expression) { - return aggregateTransformFlags( - setOriginalNode( - setTextRange( - createAssignment( - createMemberAccessForPropertyName(receiver, property.name, /*location*/ property.name), - getSynthesizedClone(property.name) - ), - /*location*/ property - ), - /*original*/ property - ) - ); - } - - function createExpressionForMethodDeclaration(method: MethodDeclaration, receiver: Expression) { - return aggregateTransformFlags( - setOriginalNode( - setTextRange( - createAssignment( - createMemberAccessForPropertyName(receiver, method.name, /*location*/ method.name), - setOriginalNode( - setTextRange( - createFunctionExpression( - method.modifiers, - method.asteriskToken, - /*name*/ undefined, - /*typeParameters*/ undefined, - method.parameters, - /*type*/ undefined, - method.body! // TODO: GH#18217 - ), - /*location*/ method - ), - /*original*/ method - ) - ), - /*location*/ method - ), - /*original*/ method - ) - ); - } - - /** - * Gets the internal name of a declaration. This is primarily used for declarations that can be - * referred to by name in the body of an ES5 class function body. An internal name will *never* - * be prefixed with an module or namespace export modifier like "exports." when emitted as an - * expression. An internal name will also *never* be renamed due to a collision with a block - * scoped variable. - * - * @param node The declaration. - * @param allowComments A value indicating whether comments may be emitted for the name. - * @param allowSourceMaps A value indicating whether source maps may be emitted for the name. - */ - export function getInternalName(node: Declaration, allowComments?: boolean, allowSourceMaps?: boolean) { - return getName(node, allowComments, allowSourceMaps, EmitFlags.LocalName | EmitFlags.InternalName); - } - - /** - * Gets whether an identifier should only be referred to by its internal name. - */ - export function isInternalName(node: Identifier) { - return (getEmitFlags(node) & EmitFlags.InternalName) !== 0; - } - - /** - * Gets the local name of a declaration. This is primarily used for declarations that can be - * referred to by name in the declaration's immediate scope (classes, enums, namespaces). A - * local name will *never* be prefixed with an module or namespace export modifier like - * "exports." when emitted as an expression. - * - * @param node The declaration. - * @param allowComments A value indicating whether comments may be emitted for the name. - * @param allowSourceMaps A value indicating whether source maps may be emitted for the name. - */ - export function getLocalName(node: Declaration, allowComments?: boolean, allowSourceMaps?: boolean) { - return getName(node, allowComments, allowSourceMaps, EmitFlags.LocalName); - } - - /** - * Gets whether an identifier should only be referred to by its local name. - */ - export function isLocalName(node: Identifier) { - return (getEmitFlags(node) & EmitFlags.LocalName) !== 0; - } - - /** - * Gets the export name of a declaration. This is primarily used for declarations that can be - * referred to by name in the declaration's immediate scope (classes, enums, namespaces). An - * export name will *always* be prefixed with an module or namespace export modifier like - * `"exports."` when emitted as an expression if the name points to an exported symbol. - * - * @param node The declaration. - * @param allowComments A value indicating whether comments may be emitted for the name. - * @param allowSourceMaps A value indicating whether source maps may be emitted for the name. - */ - export function getExportName(node: Declaration, allowComments?: boolean, allowSourceMaps?: boolean): Identifier { - return getName(node, allowComments, allowSourceMaps, EmitFlags.ExportName); - } - - /** - * Gets whether an identifier should only be referred to by its export representation if the - * name points to an exported symbol. - */ - export function isExportName(node: Identifier) { - return (getEmitFlags(node) & EmitFlags.ExportName) !== 0; - } - - /** - * Gets the name of a declaration for use in declarations. - * - * @param node The declaration. - * @param allowComments A value indicating whether comments may be emitted for the name. - * @param allowSourceMaps A value indicating whether source maps may be emitted for the name. - */ - export function getDeclarationName(node: Declaration, allowComments?: boolean, allowSourceMaps?: boolean) { - return getName(node, allowComments, allowSourceMaps); - } - - function getName(node: Declaration, allowComments?: boolean, allowSourceMaps?: boolean, emitFlags: EmitFlags = 0) { - const nodeName = getNameOfDeclaration(node); - if (nodeName && isIdentifier(nodeName) && !isGeneratedIdentifier(nodeName)) { - const name = getMutableClone(nodeName); - emitFlags |= getEmitFlags(nodeName); - if (!allowSourceMaps) emitFlags |= EmitFlags.NoSourceMap; - if (!allowComments) emitFlags |= EmitFlags.NoComments; - if (emitFlags) setEmitFlags(name, emitFlags); - return name; + return getExportName(node, allowComments, allowSourceMaps); +} +/** + * Gets a namespace-qualified name for use in expressions. + * + * @param ns The namespace identifier. + * @param name The name. + * @param allowComments A value indicating whether comments may be emitted for the name. + * @param allowSourceMaps A value indicating whether source maps may be emitted for the name. + */ +/* @internal */ +export function getNamespaceMemberName(ns: Identifier, name: Identifier, allowComments?: boolean, allowSourceMaps?: boolean): PropertyAccessExpression { + const qualifiedName = createPropertyAccess(ns, nodeIsSynthesized(name) ? name : getSynthesizedClone(name)); + setTextRange(qualifiedName, name); + let emitFlags: EmitFlags = 0; + if (!allowSourceMaps) + emitFlags |= EmitFlags.NoSourceMap; + if (!allowComments) + emitFlags |= EmitFlags.NoComments; + if (emitFlags) + setEmitFlags(qualifiedName, emitFlags); + return qualifiedName; +} +/* @internal */ +export function convertToFunctionBody(node: ConciseBody, multiLine?: boolean): Block { + return isBlock(node) ? node : setTextRange(createBlock([setTextRange(createReturn(node), node)], multiLine), node); +} +/* @internal */ +export function convertFunctionDeclarationToExpression(node: FunctionDeclaration) { + if (!node.body) + return Debug.fail(); + const updated = createFunctionExpression(node.modifiers, node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, node.body); + setOriginalNode(updated, node); + setTextRange(updated, node); + if (getStartsOnNewLine(node)) { + setStartsOnNewLine(updated, /*newLine*/ true); + } + aggregateTransformFlags(updated); + return updated; +} +/* @internal */ +function isUseStrictPrologue(node: ExpressionStatement): boolean { + return isStringLiteral(node.expression) && node.expression.text === "use strict"; +} +/** + * Add any necessary prologue-directives into target statement-array. + * The function needs to be called during each transformation step. + * This function needs to be called whenever we transform the statement + * list of a source file, namespace, or function-like body. + * + * @param target: result statements array + * @param source: origin statements array + * @param ensureUseStrict: boolean determining whether the function need to add prologue-directives + * @param visitor: Optional callback used to visit any custom prologue directives. + */ +/* @internal */ +export function addPrologue(target: Statement[], source: readonly Statement[], ensureUseStrict?: boolean, visitor?: (node: Node) => VisitResult): number { + const offset = addStandardPrologue(target, source, ensureUseStrict); + return addCustomPrologue(target, source, offset, visitor); +} +/** + * Add just the standard (string-expression) prologue-directives into target statement-array. + * The function needs to be called during each transformation step. + * This function needs to be called whenever we transform the statement + * list of a source file, namespace, or function-like body. + */ +/* @internal */ +export function addStandardPrologue(target: Statement[], source: readonly Statement[], ensureUseStrict?: boolean): number { + Debug.assert(target.length === 0, "Prologue directives should be at the first statement in the target statements array"); + let foundUseStrict = false; + let statementOffset = 0; + const numStatements = source.length; + while (statementOffset < numStatements) { + const statement = source[statementOffset]; + if (isPrologueDirective(statement)) { + if (isUseStrictPrologue(statement)) { + foundUseStrict = true; + } + target.push(statement); } - return getGeneratedNameForNode(node); + else { + break; + } + statementOffset++; + } + if (ensureUseStrict && !foundUseStrict) { + target.push(startOnNewLine(createStatement(createLiteral("use strict")))); } - - /** - * Gets the exported name of a declaration for use in expressions. - * - * An exported name will *always* be prefixed with an module or namespace export modifier like - * "exports." if the name points to an exported symbol. - * - * @param ns The namespace identifier. - * @param node The declaration. - * @param allowComments A value indicating whether comments may be emitted for the name. - * @param allowSourceMaps A value indicating whether source maps may be emitted for the name. - */ - export function getExternalModuleOrNamespaceExportName(ns: Identifier | undefined, node: Declaration, allowComments?: boolean, allowSourceMaps?: boolean): Identifier | PropertyAccessExpression { - if (ns && hasModifier(node, ModifierFlags.Export)) { - return getNamespaceMemberName(ns, getName(node), allowComments, allowSourceMaps); + return statementOffset; +} +/** + * Add just the custom prologue-directives into target statement-array. + * The function needs to be called during each transformation step. + * This function needs to be called whenever we transform the statement + * list of a source file, namespace, or function-like body. + */ +/* @internal */ +export function addCustomPrologue(target: Statement[], source: readonly Statement[], statementOffset: number, visitor?: (node: Node) => VisitResult): number; +/* @internal */ +export function addCustomPrologue(target: Statement[], source: readonly Statement[], statementOffset: number | undefined, visitor?: (node: Node) => VisitResult): number | undefined; +/* @internal */ +export function addCustomPrologue(target: Statement[], source: readonly Statement[], statementOffset: number | undefined, visitor?: (node: Node) => VisitResult): number | undefined { + const numStatements = source.length; + while (statementOffset !== undefined && statementOffset < numStatements) { + const statement = source[statementOffset]; + if (getEmitFlags(statement) & EmitFlags.CustomPrologue) { + append(target, visitor ? visitNode(statement, visitor, isStatement) : statement); } - return getExportName(node, allowComments, allowSourceMaps); - } - - /** - * Gets a namespace-qualified name for use in expressions. - * - * @param ns The namespace identifier. - * @param name The name. - * @param allowComments A value indicating whether comments may be emitted for the name. - * @param allowSourceMaps A value indicating whether source maps may be emitted for the name. - */ - export function getNamespaceMemberName(ns: Identifier, name: Identifier, allowComments?: boolean, allowSourceMaps?: boolean): PropertyAccessExpression { - const qualifiedName = createPropertyAccess(ns, nodeIsSynthesized(name) ? name : getSynthesizedClone(name)); - setTextRange(qualifiedName, name); - let emitFlags: EmitFlags = 0; - if (!allowSourceMaps) emitFlags |= EmitFlags.NoSourceMap; - if (!allowComments) emitFlags |= EmitFlags.NoComments; - if (emitFlags) setEmitFlags(qualifiedName, emitFlags); - return qualifiedName; - } - - export function convertToFunctionBody(node: ConciseBody, multiLine?: boolean): Block { - return isBlock(node) ? node : setTextRange(createBlock([setTextRange(createReturn(node), node)], multiLine), node); - } - - export function convertFunctionDeclarationToExpression(node: FunctionDeclaration) { - if (!node.body) return Debug.fail(); - const updated = createFunctionExpression( - node.modifiers, - node.asteriskToken, - node.name, - node.typeParameters, - node.parameters, - node.type, - node.body - ); - setOriginalNode(updated, node); - setTextRange(updated, node); - if (getStartsOnNewLine(node)) { - setStartsOnNewLine(updated, /*newLine*/ true); + else { + break; } - aggregateTransformFlags(updated); - return updated; - } - - function isUseStrictPrologue(node: ExpressionStatement): boolean { - return isStringLiteral(node.expression) && node.expression.text === "use strict"; - } - - /** - * Add any necessary prologue-directives into target statement-array. - * The function needs to be called during each transformation step. - * This function needs to be called whenever we transform the statement - * list of a source file, namespace, or function-like body. - * - * @param target: result statements array - * @param source: origin statements array - * @param ensureUseStrict: boolean determining whether the function need to add prologue-directives - * @param visitor: Optional callback used to visit any custom prologue directives. - */ - export function addPrologue(target: Statement[], source: readonly Statement[], ensureUseStrict?: boolean, visitor?: (node: Node) => VisitResult): number { - const offset = addStandardPrologue(target, source, ensureUseStrict); - return addCustomPrologue(target, source, offset, visitor); - } - - /** - * Add just the standard (string-expression) prologue-directives into target statement-array. - * The function needs to be called during each transformation step. - * This function needs to be called whenever we transform the statement - * list of a source file, namespace, or function-like body. - */ - export function addStandardPrologue(target: Statement[], source: readonly Statement[], ensureUseStrict?: boolean): number { - Debug.assert(target.length === 0, "Prologue directives should be at the first statement in the target statements array"); - let foundUseStrict = false; - let statementOffset = 0; - const numStatements = source.length; - while (statementOffset < numStatements) { - const statement = source[statementOffset]; - if (isPrologueDirective(statement)) { - if (isUseStrictPrologue(statement)) { - foundUseStrict = true; - } - target.push(statement); - } - else { - break; + statementOffset++; + } + return statementOffset; +} +/* @internal */ +export function findUseStrictPrologue(statements: readonly Statement[]): Statement | undefined { + for (const statement of statements) { + if (isPrologueDirective(statement)) { + if (isUseStrictPrologue(statement)) { + return statement; } - statementOffset++; } - if (ensureUseStrict && !foundUseStrict) { - target.push(startOnNewLine(createStatement(createLiteral("use strict")))); + else { + break; } - return statementOffset; - } - - /** - * Add just the custom prologue-directives into target statement-array. - * The function needs to be called during each transformation step. - * This function needs to be called whenever we transform the statement - * list of a source file, namespace, or function-like body. - */ - export function addCustomPrologue(target: Statement[], source: readonly Statement[], statementOffset: number, visitor?: (node: Node) => VisitResult): number; - export function addCustomPrologue(target: Statement[], source: readonly Statement[], statementOffset: number | undefined, visitor?: (node: Node) => VisitResult): number | undefined; - export function addCustomPrologue(target: Statement[], source: readonly Statement[], statementOffset: number | undefined, visitor?: (node: Node) => VisitResult): number | undefined { - const numStatements = source.length; - while (statementOffset !== undefined && statementOffset < numStatements) { - const statement = source[statementOffset]; - if (getEmitFlags(statement) & EmitFlags.CustomPrologue) { - append(target, visitor ? visitNode(statement, visitor, isStatement) : statement); - } - else { - break; + } + return undefined; +} +/* @internal */ +export function startsWithUseStrict(statements: readonly Statement[]) { + const firstStatement = firstOrUndefined(statements); + return firstStatement !== undefined + && isPrologueDirective(firstStatement) + && isUseStrictPrologue(firstStatement); +} +/** + * Ensures "use strict" directive is added + * + * @param statements An array of statements + */ +/* @internal */ +export function ensureUseStrict(statements: NodeArray): NodeArray { + const foundUseStrict = findUseStrictPrologue(statements); + if (!foundUseStrict) { + return setTextRange(createNodeArray([ + startOnNewLine(createStatement(createLiteral("use strict"))), + ...statements + ]), statements); + } + return statements; +} +/** + * Wraps the operand to a BinaryExpression in parentheses if they are needed to preserve the intended + * order of operations. + * + * @param binaryOperator The operator for the BinaryExpression. + * @param operand The operand for the BinaryExpression. + * @param isLeftSideOfBinary A value indicating whether the operand is the left side of the + * BinaryExpression. + */ +/* @internal */ +export function parenthesizeBinaryOperand(binaryOperator: SyntaxKind, operand: Expression, isLeftSideOfBinary: boolean, leftOperand?: Expression) { + const skipped = skipPartiallyEmittedExpressions(operand); + // If the resulting expression is already parenthesized, we do not need to do any further processing. + if (skipped.kind === SyntaxKind.ParenthesizedExpression) { + return operand; + } + return binaryOperandNeedsParentheses(binaryOperator, operand, isLeftSideOfBinary, leftOperand) + ? createParen(operand) + : operand; +} +/** + * Determines whether the operand to a BinaryExpression needs to be parenthesized. + * + * @param binaryOperator The operator for the BinaryExpression. + * @param operand The operand for the BinaryExpression. + * @param isLeftSideOfBinary A value indicating whether the operand is the left side of the + * BinaryExpression. + */ +/* @internal */ +function binaryOperandNeedsParentheses(binaryOperator: SyntaxKind, operand: Expression, isLeftSideOfBinary: boolean, leftOperand: Expression | undefined) { + // If the operand has lower precedence, then it needs to be parenthesized to preserve the + // intent of the expression. For example, if the operand is `a + b` and the operator is + // `*`, then we need to parenthesize the operand to preserve the intended order of + // operations: `(a + b) * x`. + // + // If the operand has higher precedence, then it does not need to be parenthesized. For + // example, if the operand is `a * b` and the operator is `+`, then we do not need to + // parenthesize to preserve the intended order of operations: `a * b + x`. + // + // If the operand has the same precedence, then we need to check the associativity of + // the operator based on whether this is the left or right operand of the expression. + // + // For example, if `a / d` is on the right of operator `*`, we need to parenthesize + // to preserve the intended order of operations: `x * (a / d)` + // + // If `a ** d` is on the left of operator `**`, we need to parenthesize to preserve + // the intended order of operations: `(a ** b) ** c` + const binaryOperatorPrecedence = getOperatorPrecedence(SyntaxKind.BinaryExpression, binaryOperator); + const binaryOperatorAssociativity = getOperatorAssociativity(SyntaxKind.BinaryExpression, binaryOperator); + const emittedOperand = skipPartiallyEmittedExpressions(operand); + if (!isLeftSideOfBinary && operand.kind === SyntaxKind.ArrowFunction && binaryOperatorPrecedence > 3) { + // We need to parenthesize arrow functions on the right side to avoid it being + // parsed as parenthesized expression: `a && (() => {})` + return true; + } + const operandPrecedence = getExpressionPrecedence(emittedOperand); + switch (compareValues(operandPrecedence, binaryOperatorPrecedence)) { + case Comparison.LessThan: + // If the operand is the right side of a right-associative binary operation + // and is a yield expression, then we do not need parentheses. + if (!isLeftSideOfBinary + && binaryOperatorAssociativity === Associativity.Right + && operand.kind === SyntaxKind.YieldExpression) { + return false; } - statementOffset++; - } - return statementOffset; - } - - export function findUseStrictPrologue(statements: readonly Statement[]): Statement | undefined { - for (const statement of statements) { - if (isPrologueDirective(statement)) { - if (isUseStrictPrologue(statement)) { - return statement; - } + return true; + case Comparison.GreaterThan: + return false; + case Comparison.EqualTo: + if (isLeftSideOfBinary) { + // No need to parenthesize the left operand when the binary operator is + // left associative: + // (a*b)/x -> a*b/x + // (a**b)/x -> a**b/x + // + // Parentheses are needed for the left operand when the binary operator is + // right associative: + // (a/b)**x -> (a/b)**x + // (a**b)**x -> (a**b)**x + return binaryOperatorAssociativity === Associativity.Right; } else { - break; - } - } - return undefined; - } - - export function startsWithUseStrict(statements: readonly Statement[]) { - const firstStatement = firstOrUndefined(statements); - return firstStatement !== undefined - && isPrologueDirective(firstStatement) - && isUseStrictPrologue(firstStatement); - } - - /** - * Ensures "use strict" directive is added - * - * @param statements An array of statements - */ - export function ensureUseStrict(statements: NodeArray): NodeArray { - const foundUseStrict = findUseStrictPrologue(statements); - - if (!foundUseStrict) { - return setTextRange( - createNodeArray([ - startOnNewLine(createStatement(createLiteral("use strict"))), - ...statements - ]), - statements - ); - } - - return statements; - } - - /** - * Wraps the operand to a BinaryExpression in parentheses if they are needed to preserve the intended - * order of operations. - * - * @param binaryOperator The operator for the BinaryExpression. - * @param operand The operand for the BinaryExpression. - * @param isLeftSideOfBinary A value indicating whether the operand is the left side of the - * BinaryExpression. - */ - export function parenthesizeBinaryOperand(binaryOperator: SyntaxKind, operand: Expression, isLeftSideOfBinary: boolean, leftOperand?: Expression) { - const skipped = skipPartiallyEmittedExpressions(operand); - - // If the resulting expression is already parenthesized, we do not need to do any further processing. - if (skipped.kind === SyntaxKind.ParenthesizedExpression) { - return operand; - } - - return binaryOperandNeedsParentheses(binaryOperator, operand, isLeftSideOfBinary, leftOperand) - ? createParen(operand) - : operand; - } - - /** - * Determines whether the operand to a BinaryExpression needs to be parenthesized. - * - * @param binaryOperator The operator for the BinaryExpression. - * @param operand The operand for the BinaryExpression. - * @param isLeftSideOfBinary A value indicating whether the operand is the left side of the - * BinaryExpression. - */ - function binaryOperandNeedsParentheses(binaryOperator: SyntaxKind, operand: Expression, isLeftSideOfBinary: boolean, leftOperand: Expression | undefined) { - // If the operand has lower precedence, then it needs to be parenthesized to preserve the - // intent of the expression. For example, if the operand is `a + b` and the operator is - // `*`, then we need to parenthesize the operand to preserve the intended order of - // operations: `(a + b) * x`. - // - // If the operand has higher precedence, then it does not need to be parenthesized. For - // example, if the operand is `a * b` and the operator is `+`, then we do not need to - // parenthesize to preserve the intended order of operations: `a * b + x`. - // - // If the operand has the same precedence, then we need to check the associativity of - // the operator based on whether this is the left or right operand of the expression. - // - // For example, if `a / d` is on the right of operator `*`, we need to parenthesize - // to preserve the intended order of operations: `x * (a / d)` - // - // If `a ** d` is on the left of operator `**`, we need to parenthesize to preserve - // the intended order of operations: `(a ** b) ** c` - const binaryOperatorPrecedence = getOperatorPrecedence(SyntaxKind.BinaryExpression, binaryOperator); - const binaryOperatorAssociativity = getOperatorAssociativity(SyntaxKind.BinaryExpression, binaryOperator); - const emittedOperand = skipPartiallyEmittedExpressions(operand); - if (!isLeftSideOfBinary && operand.kind === SyntaxKind.ArrowFunction && binaryOperatorPrecedence > 3) { - // We need to parenthesize arrow functions on the right side to avoid it being - // parsed as parenthesized expression: `a && (() => {})` - return true; - } - const operandPrecedence = getExpressionPrecedence(emittedOperand); - switch (compareValues(operandPrecedence, binaryOperatorPrecedence)) { - case Comparison.LessThan: - // If the operand is the right side of a right-associative binary operation - // and is a yield expression, then we do not need parentheses. - if (!isLeftSideOfBinary - && binaryOperatorAssociativity === Associativity.Right - && operand.kind === SyntaxKind.YieldExpression) { - return false; - } - - return true; - - case Comparison.GreaterThan: - return false; - - case Comparison.EqualTo: - if (isLeftSideOfBinary) { - // No need to parenthesize the left operand when the binary operator is - // left associative: - // (a*b)/x -> a*b/x - // (a**b)/x -> a**b/x - // - // Parentheses are needed for the left operand when the binary operator is - // right associative: - // (a/b)**x -> (a/b)**x - // (a**b)**x -> (a**b)**x - return binaryOperatorAssociativity === Associativity.Right; - } - else { - if (isBinaryExpression(emittedOperand) - && emittedOperand.operatorToken.kind === binaryOperator) { - // No need to parenthesize the right operand when the binary operator and - // operand are the same and one of the following: - // x*(a*b) => x*a*b - // x|(a|b) => x|a|b - // x&(a&b) => x&a&b - // x^(a^b) => x^a^b - if (operatorHasAssociativeProperty(binaryOperator)) { + if (isBinaryExpression(emittedOperand) + && emittedOperand.operatorToken.kind === binaryOperator) { + // No need to parenthesize the right operand when the binary operator and + // operand are the same and one of the following: + // x*(a*b) => x*a*b + // x|(a|b) => x|a|b + // x&(a&b) => x&a&b + // x^(a^b) => x^a^b + if (operatorHasAssociativeProperty(binaryOperator)) { + return false; + } + // No need to parenthesize the right operand when the binary operator + // is plus (+) if both the left and right operands consist solely of either + // literals of the same kind or binary plus (+) expressions for literals of + // the same kind (recursively). + // "a"+(1+2) => "a"+(1+2) + // "a"+("b"+"c") => "a"+"b"+"c" + if (binaryOperator === SyntaxKind.PlusToken) { + const leftKind = leftOperand ? getLiteralKindOfBinaryPlusOperand(leftOperand) : SyntaxKind.Unknown; + if (isLiteralKind(leftKind) && leftKind === getLiteralKindOfBinaryPlusOperand(emittedOperand)) { return false; } - - // No need to parenthesize the right operand when the binary operator - // is plus (+) if both the left and right operands consist solely of either - // literals of the same kind or binary plus (+) expressions for literals of - // the same kind (recursively). - // "a"+(1+2) => "a"+(1+2) - // "a"+("b"+"c") => "a"+"b"+"c" - if (binaryOperator === SyntaxKind.PlusToken) { - const leftKind = leftOperand ? getLiteralKindOfBinaryPlusOperand(leftOperand) : SyntaxKind.Unknown; - if (isLiteralKind(leftKind) && leftKind === getLiteralKindOfBinaryPlusOperand(emittedOperand)) { - return false; - } - } } - - // No need to parenthesize the right operand when the operand is right - // associative: - // x/(a**b) -> x/a**b - // x**(a**b) -> x**a**b - // - // Parentheses are needed for the right operand when the operand is left - // associative: - // x/(a*b) -> x/(a*b) - // x**(a/b) -> x**(a/b) - const operandAssociativity = getExpressionAssociativity(emittedOperand); - return operandAssociativity === Associativity.Left; } - } - } - - /** - * Determines whether a binary operator is mathematically associative. - * - * @param binaryOperator The binary operator. - */ - function operatorHasAssociativeProperty(binaryOperator: SyntaxKind) { - // The following operators are associative in JavaScript: - // (a*b)*c -> a*(b*c) -> a*b*c - // (a|b)|c -> a|(b|c) -> a|b|c - // (a&b)&c -> a&(b&c) -> a&b&c - // (a^b)^c -> a^(b^c) -> a^b^c - // - // While addition is associative in mathematics, JavaScript's `+` is not - // guaranteed to be associative as it is overloaded with string concatenation. - return binaryOperator === SyntaxKind.AsteriskToken - || binaryOperator === SyntaxKind.BarToken - || binaryOperator === SyntaxKind.AmpersandToken - || binaryOperator === SyntaxKind.CaretToken; - } - - interface BinaryPlusExpression extends BinaryExpression { - cachedLiteralKind: SyntaxKind; - } - - /** - * This function determines whether an expression consists of a homogeneous set of - * literal expressions or binary plus expressions that all share the same literal kind. - * It is used to determine whether the right-hand operand of a binary plus expression can be - * emitted without parentheses. - */ - function getLiteralKindOfBinaryPlusOperand(node: Expression): SyntaxKind { - node = skipPartiallyEmittedExpressions(node); - - if (isLiteralKind(node.kind)) { - return node.kind; - } - - if (node.kind === SyntaxKind.BinaryExpression && (node).operatorToken.kind === SyntaxKind.PlusToken) { - if ((node).cachedLiteralKind !== undefined) { - return (node).cachedLiteralKind; - } - - const leftKind = getLiteralKindOfBinaryPlusOperand((node).left); - const literalKind = isLiteralKind(leftKind) && leftKind === getLiteralKindOfBinaryPlusOperand((node).right) ? leftKind : - SyntaxKind.Unknown; - - (node).cachedLiteralKind = literalKind; - return literalKind; - } - - return SyntaxKind.Unknown; - } - - export function parenthesizeForConditionalHead(condition: Expression) { - const conditionalPrecedence = getOperatorPrecedence(SyntaxKind.ConditionalExpression, SyntaxKind.QuestionToken); - const emittedCondition = skipPartiallyEmittedExpressions(condition); - const conditionPrecedence = getExpressionPrecedence(emittedCondition); - if (compareValues(conditionPrecedence, conditionalPrecedence) !== Comparison.GreaterThan) { - return createParen(condition); - } - return condition; - } - - export function parenthesizeSubexpressionOfConditionalExpression(e: Expression): Expression { - // per ES grammar both 'whenTrue' and 'whenFalse' parts of conditional expression are assignment expressions - // so in case when comma expression is introduced as a part of previous transformations - // if should be wrapped in parens since comma operator has the lowest precedence - const emittedExpression = skipPartiallyEmittedExpressions(e); - return isCommaSequence(emittedExpression) - ? createParen(e) - : e; - } - - /** - * [Per the spec](https://tc39.github.io/ecma262/#prod-ExportDeclaration), `export default` accepts _AssigmentExpression_ but - * has a lookahead restriction for `function`, `async function`, and `class`. - * - * Basically, that means we need to parenthesize in the following cases: - * - * - BinaryExpression of CommaToken - * - CommaList (synthetic list of multiple comma expressions) - * - FunctionExpression - * - ClassExpression - */ - export function parenthesizeDefaultExpression(e: Expression) { - const check = skipPartiallyEmittedExpressions(e); - let needsParens = isCommaSequence(check); - if (!needsParens) { - switch (getLeftmostExpression(check, /*stopAtCallExpression*/ false).kind) { - case SyntaxKind.ClassExpression: - case SyntaxKind.FunctionExpression: - needsParens = true; + // No need to parenthesize the right operand when the operand is right + // associative: + // x/(a**b) -> x/a**b + // x**(a**b) -> x**a**b + // + // Parentheses are needed for the right operand when the operand is left + // associative: + // x/(a*b) -> x/(a*b) + // x**(a/b) -> x**(a/b) + const operandAssociativity = getExpressionAssociativity(emittedOperand); + return operandAssociativity === Associativity.Left; } - } - return needsParens ? createParen(e) : e; - } - - /** - * Wraps an expression in parentheses if it is needed in order to use the expression - * as the expression of a NewExpression node. - * - * @param expression The Expression node. - */ - export function parenthesizeForNew(expression: Expression): LeftHandSideExpression { - const leftmostExpr = getLeftmostExpression(expression, /*stopAtCallExpressions*/ true); - switch (leftmostExpr.kind) { - case SyntaxKind.CallExpression: - return createParen(expression); - - case SyntaxKind.NewExpression: - return !(leftmostExpr as NewExpression).arguments - ? createParen(expression) - : expression; - } - - return parenthesizeForAccess(expression); - } - - /** - * Wraps an expression in parentheses if it is needed in order to use the expression for - * property or element access. - * - * @param expr The expression node. - */ - export function parenthesizeForAccess(expression: Expression): LeftHandSideExpression { - // isLeftHandSideExpression is almost the correct criterion for when it is not necessary - // to parenthesize the expression before a dot. The known exception is: - // - // NewExpression: - // new C.x -> not the same as (new C).x - // - const emittedExpression = skipPartiallyEmittedExpressions(expression); - if (isLeftHandSideExpression(emittedExpression) - && (emittedExpression.kind !== SyntaxKind.NewExpression || (emittedExpression).arguments)) { - return expression; - } - - return setTextRange(createParen(expression), expression); } - - export function parenthesizePostfixOperand(operand: Expression) { - return isLeftHandSideExpression(operand) - ? operand - : setTextRange(createParen(operand), operand); - } - - export function parenthesizePrefixOperand(operand: Expression) { - return isUnaryExpression(operand) - ? operand - : setTextRange(createParen(operand), operand); - } - - export function parenthesizeListElements(elements: NodeArray) { - let result: Expression[] | undefined; - for (let i = 0; i < elements.length; i++) { - const element = parenthesizeExpressionForList(elements[i]); - if (result !== undefined || element !== elements[i]) { - if (result === undefined) { - result = elements.slice(0, i); - } - - result.push(element); - } - } - - if (result !== undefined) { - return setTextRange(createNodeArray(result, elements.hasTrailingComma), elements); - } - - return elements; - } - - export function parenthesizeExpressionForList(expression: Expression) { - const emittedExpression = skipPartiallyEmittedExpressions(expression); - const expressionPrecedence = getExpressionPrecedence(emittedExpression); - const commaPrecedence = getOperatorPrecedence(SyntaxKind.BinaryExpression, SyntaxKind.CommaToken); - return expressionPrecedence > commaPrecedence - ? expression - : setTextRange(createParen(expression), expression); - } - - export function parenthesizeExpressionForExpressionStatement(expression: Expression) { - const emittedExpression = skipPartiallyEmittedExpressions(expression); - if (isCallExpression(emittedExpression)) { - const callee = emittedExpression.expression; - const kind = skipPartiallyEmittedExpressions(callee).kind; - if (kind === SyntaxKind.FunctionExpression || kind === SyntaxKind.ArrowFunction) { - const mutableCall = getMutableClone(emittedExpression); - mutableCall.expression = setTextRange(createParen(callee), callee); - return recreateOuterExpressions(expression, mutableCall, OuterExpressionKinds.PartiallyEmittedExpressions); - } - } - - const leftmostExpressionKind = getLeftmostExpression(emittedExpression, /*stopAtCallExpressions*/ false).kind; - if (leftmostExpressionKind === SyntaxKind.ObjectLiteralExpression || leftmostExpressionKind === SyntaxKind.FunctionExpression) { - return setTextRange(createParen(expression), expression); +} +/** + * Determines whether a binary operator is mathematically associative. + * + * @param binaryOperator The binary operator. + */ +/* @internal */ +function operatorHasAssociativeProperty(binaryOperator: SyntaxKind) { + // The following operators are associative in JavaScript: + // (a*b)*c -> a*(b*c) -> a*b*c + // (a|b)|c -> a|(b|c) -> a|b|c + // (a&b)&c -> a&(b&c) -> a&b&c + // (a^b)^c -> a^(b^c) -> a^b^c + // + // While addition is associative in mathematics, JavaScript's `+` is not + // guaranteed to be associative as it is overloaded with string concatenation. + return binaryOperator === SyntaxKind.AsteriskToken + || binaryOperator === SyntaxKind.BarToken + || binaryOperator === SyntaxKind.AmpersandToken + || binaryOperator === SyntaxKind.CaretToken; +} +/* @internal */ +interface BinaryPlusExpression extends BinaryExpression { + cachedLiteralKind: SyntaxKind; +} +/** + * This function determines whether an expression consists of a homogeneous set of + * literal expressions or binary plus expressions that all share the same literal kind. + * It is used to determine whether the right-hand operand of a binary plus expression can be + * emitted without parentheses. + */ +/* @internal */ +function getLiteralKindOfBinaryPlusOperand(node: Expression): SyntaxKind { + node = skipPartiallyEmittedExpressions(node); + if (isLiteralKind(node.kind)) { + return node.kind; + } + if (node.kind === SyntaxKind.BinaryExpression && (node).operatorToken.kind === SyntaxKind.PlusToken) { + if ((node).cachedLiteralKind !== undefined) { + return (node).cachedLiteralKind; + } + const leftKind = getLiteralKindOfBinaryPlusOperand((node).left); + const literalKind = isLiteralKind(leftKind) && leftKind === getLiteralKindOfBinaryPlusOperand((node).right) ? leftKind : + SyntaxKind.Unknown; + (node).cachedLiteralKind = literalKind; + return literalKind; + } + return SyntaxKind.Unknown; +} +/* @internal */ +export function parenthesizeForConditionalHead(condition: Expression) { + const conditionalPrecedence = getOperatorPrecedence(SyntaxKind.ConditionalExpression, SyntaxKind.QuestionToken); + const emittedCondition = skipPartiallyEmittedExpressions(condition); + const conditionPrecedence = getExpressionPrecedence(emittedCondition); + if (compareValues(conditionPrecedence, conditionalPrecedence) !== Comparison.GreaterThan) { + return createParen(condition); + } + return condition; +} +/* @internal */ +export function parenthesizeSubexpressionOfConditionalExpression(e: Expression): Expression { + // per ES grammar both 'whenTrue' and 'whenFalse' parts of conditional expression are assignment expressions + // so in case when comma expression is introduced as a part of previous transformations + // if should be wrapped in parens since comma operator has the lowest precedence + const emittedExpression = skipPartiallyEmittedExpressions(e); + return isCommaSequence(emittedExpression) + ? createParen(e) + : e; +} +/** + * [Per the spec](https://tc39.github.io/ecma262/#prod-ExportDeclaration), `export default` accepts _AssigmentExpression_ but + * has a lookahead restriction for `function`, `async function`, and `class`. + * + * Basically, that means we need to parenthesize in the following cases: + * + * - BinaryExpression of CommaToken + * - CommaList (synthetic list of multiple comma expressions) + * - FunctionExpression + * - ClassExpression + */ +/* @internal */ +export function parenthesizeDefaultExpression(e: Expression) { + const check = skipPartiallyEmittedExpressions(e); + let needsParens = isCommaSequence(check); + if (!needsParens) { + switch (getLeftmostExpression(check, /*stopAtCallExpression*/ false).kind) { + case SyntaxKind.ClassExpression: + case SyntaxKind.FunctionExpression: + needsParens = true; } - - return expression; } - - export function parenthesizeConditionalTypeMember(member: TypeNode) { - return member.kind === SyntaxKind.ConditionalType ? createParenthesizedType(member) : member; - } - - export function parenthesizeElementTypeMember(member: TypeNode) { - switch (member.kind) { - case SyntaxKind.UnionType: - case SyntaxKind.IntersectionType: - case SyntaxKind.FunctionType: - case SyntaxKind.ConstructorType: - return createParenthesizedType(member); - } - return parenthesizeConditionalTypeMember(member); - } - - export function parenthesizeArrayTypeMember(member: TypeNode) { - switch (member.kind) { - case SyntaxKind.TypeQuery: - case SyntaxKind.TypeOperator: - case SyntaxKind.InferType: - return createParenthesizedType(member); - } - return parenthesizeElementTypeMember(member); - } - - export function parenthesizeElementTypeMembers(members: readonly TypeNode[]) { - return createNodeArray(sameMap(members, parenthesizeElementTypeMember)); - } - - export function parenthesizeTypeParameters(typeParameters: readonly TypeNode[] | undefined) { - if (some(typeParameters)) { - const params: TypeNode[] = []; - for (let i = 0; i < typeParameters.length; ++i) { - const entry = typeParameters[i]; - params.push(i === 0 && isFunctionOrConstructorTypeNode(entry) && entry.typeParameters ? - createParenthesizedType(entry) : - entry); + return needsParens ? createParen(e) : e; +} +/** + * Wraps an expression in parentheses if it is needed in order to use the expression + * as the expression of a NewExpression node. + * + * @param expression The Expression node. + */ +/* @internal */ +export function parenthesizeForNew(expression: Expression): LeftHandSideExpression { + const leftmostExpr = getLeftmostExpression(expression, /*stopAtCallExpressions*/ true); + switch (leftmostExpr.kind) { + case SyntaxKind.CallExpression: + return createParen(expression); + case SyntaxKind.NewExpression: + return !(leftmostExpr as NewExpression).arguments + ? createParen(expression) + : expression; + } + return parenthesizeForAccess(expression); +} +/** + * Wraps an expression in parentheses if it is needed in order to use the expression for + * property or element access. + * + * @param expr The expression node. + */ +/* @internal */ +export function parenthesizeForAccess(expression: Expression): LeftHandSideExpression { + // isLeftHandSideExpression is almost the correct criterion for when it is not necessary + // to parenthesize the expression before a dot. The known exception is: + // + // NewExpression: + // new C.x -> not the same as (new C).x + // + const emittedExpression = skipPartiallyEmittedExpressions(expression); + if (isLeftHandSideExpression(emittedExpression) + && (emittedExpression.kind !== SyntaxKind.NewExpression || (emittedExpression).arguments)) { + return expression; + } + return setTextRange(createParen(expression), expression); +} +/* @internal */ +export function parenthesizePostfixOperand(operand: Expression) { + return isLeftHandSideExpression(operand) + ? operand + : setTextRange(createParen(operand), operand); +} +/* @internal */ +export function parenthesizePrefixOperand(operand: Expression) { + return isUnaryExpression(operand) + ? operand + : setTextRange(createParen(operand), operand); +} +/* @internal */ +export function parenthesizeListElements(elements: NodeArray) { + let result: Expression[] | undefined; + for (let i = 0; i < elements.length; i++) { + const element = parenthesizeExpressionForList(elements[i]); + if (result !== undefined || element !== elements[i]) { + if (result === undefined) { + result = elements.slice(0, i); } - - return createNodeArray(params); + result.push(element); } } - - export function getLeftmostExpression(node: Expression, stopAtCallExpressions: boolean) { - while (true) { - switch (node.kind) { - case SyntaxKind.PostfixUnaryExpression: - node = (node).operand; - continue; - - case SyntaxKind.BinaryExpression: - node = (node).left; - continue; - - case SyntaxKind.ConditionalExpression: - node = (node).condition; - continue; - - case SyntaxKind.TaggedTemplateExpression: - node = (node).tag; - continue; - - case SyntaxKind.CallExpression: - if (stopAtCallExpressions) { - return node; - } - // falls through - case SyntaxKind.AsExpression: - case SyntaxKind.ElementAccessExpression: - case SyntaxKind.PropertyAccessExpression: - case SyntaxKind.NonNullExpression: - case SyntaxKind.PartiallyEmittedExpression: - node = (node).expression; - continue; - } - - return node; - } - + if (result !== undefined) { + return setTextRange(createNodeArray(result, elements.hasTrailingComma), elements); + } + return elements; +} +/* @internal */ +export function parenthesizeExpressionForList(expression: Expression) { + const emittedExpression = skipPartiallyEmittedExpressions(expression); + const expressionPrecedence = getExpressionPrecedence(emittedExpression); + const commaPrecedence = getOperatorPrecedence(SyntaxKind.BinaryExpression, SyntaxKind.CommaToken); + return expressionPrecedence > commaPrecedence + ? expression + : setTextRange(createParen(expression), expression); +} +/* @internal */ +export function parenthesizeExpressionForExpressionStatement(expression: Expression) { + const emittedExpression = skipPartiallyEmittedExpressions(expression); + if (isCallExpression(emittedExpression)) { + const callee = emittedExpression.expression; + const kind = skipPartiallyEmittedExpressions(callee).kind; + if (kind === SyntaxKind.FunctionExpression || kind === SyntaxKind.ArrowFunction) { + const mutableCall = getMutableClone(emittedExpression); + mutableCall.expression = setTextRange(createParen(callee), callee); + return recreateOuterExpressions(expression, mutableCall, OuterExpressionKinds.PartiallyEmittedExpressions); + } + } + const leftmostExpressionKind = getLeftmostExpression(emittedExpression, /*stopAtCallExpressions*/ false).kind; + if (leftmostExpressionKind === SyntaxKind.ObjectLiteralExpression || leftmostExpressionKind === SyntaxKind.FunctionExpression) { + return setTextRange(createParen(expression), expression); } - - export function parenthesizeConciseBody(body: ConciseBody): ConciseBody { - if (!isBlock(body) && (isCommaSequence(body) || getLeftmostExpression(body, /*stopAtCallExpressions*/ false).kind === SyntaxKind.ObjectLiteralExpression)) { - return setTextRange(createParen(body), body); + return expression; +} +/* @internal */ +export function parenthesizeConditionalTypeMember(member: TypeNode) { + return member.kind === SyntaxKind.ConditionalType ? createParenthesizedType(member) : member; +} +/* @internal */ +export function parenthesizeElementTypeMember(member: TypeNode) { + switch (member.kind) { + case SyntaxKind.UnionType: + case SyntaxKind.IntersectionType: + case SyntaxKind.FunctionType: + case SyntaxKind.ConstructorType: + return createParenthesizedType(member); + } + return parenthesizeConditionalTypeMember(member); +} +/* @internal */ +export function parenthesizeArrayTypeMember(member: TypeNode) { + switch (member.kind) { + case SyntaxKind.TypeQuery: + case SyntaxKind.TypeOperator: + case SyntaxKind.InferType: + return createParenthesizedType(member); + } + return parenthesizeElementTypeMember(member); +} +/* @internal */ +export function parenthesizeElementTypeMembers(members: readonly TypeNode[]) { + return createNodeArray(sameMap(members, parenthesizeElementTypeMember)); +} +/* @internal */ +export function parenthesizeTypeParameters(typeParameters: readonly TypeNode[] | undefined) { + if (some(typeParameters)) { + const params: TypeNode[] = []; + for (let i = 0; i < typeParameters.length; ++i) { + const entry = typeParameters[i]; + params.push(i === 0 && isFunctionOrConstructorTypeNode(entry) && entry.typeParameters ? + createParenthesizedType(entry) : + entry); } - - return body; - } - - export function isCommaSequence(node: Expression): node is BinaryExpression & {operatorToken: Token} | CommaListExpression { - return node.kind === SyntaxKind.BinaryExpression && (node).operatorToken.kind === SyntaxKind.CommaToken || - node.kind === SyntaxKind.CommaListExpression; - } - - export const enum OuterExpressionKinds { - Parentheses = 1 << 0, - Assertions = 1 << 1, - PartiallyEmittedExpressions = 1 << 2, - - All = Parentheses | Assertions | PartiallyEmittedExpressions - } - - export type OuterExpression = ParenthesizedExpression | TypeAssertion | AsExpression | NonNullExpression | PartiallyEmittedExpression; - - export function isOuterExpression(node: Node, kinds = OuterExpressionKinds.All): node is OuterExpression { + return createNodeArray(params); + } +} +/* @internal */ +export function getLeftmostExpression(node: Expression, stopAtCallExpressions: boolean) { + while (true) { switch (node.kind) { - case SyntaxKind.ParenthesizedExpression: - return (kinds & OuterExpressionKinds.Parentheses) !== 0; - case SyntaxKind.TypeAssertionExpression: + case SyntaxKind.PostfixUnaryExpression: + node = (node).operand; + continue; + case SyntaxKind.BinaryExpression: + node = (node).left; + continue; + case SyntaxKind.ConditionalExpression: + node = (node).condition; + continue; + case SyntaxKind.TaggedTemplateExpression: + node = (node).tag; + continue; + case SyntaxKind.CallExpression: + if (stopAtCallExpressions) { + return node; + } + // falls through case SyntaxKind.AsExpression: + case SyntaxKind.ElementAccessExpression: + case SyntaxKind.PropertyAccessExpression: case SyntaxKind.NonNullExpression: - return (kinds & OuterExpressionKinds.Assertions) !== 0; case SyntaxKind.PartiallyEmittedExpression: - return (kinds & OuterExpressionKinds.PartiallyEmittedExpressions) !== 0; - } - return false; - } - - export function skipOuterExpressions(node: Expression, kinds?: OuterExpressionKinds): Expression; - export function skipOuterExpressions(node: Node, kinds?: OuterExpressionKinds): Node; - export function skipOuterExpressions(node: Node, kinds = OuterExpressionKinds.All) { - let previousNode: Node; - do { - previousNode = node; - if (kinds & OuterExpressionKinds.Parentheses) { - node = skipParentheses(node); - } - - if (kinds & OuterExpressionKinds.Assertions) { - node = skipAssertions(node); - } - - if (kinds & OuterExpressionKinds.PartiallyEmittedExpressions) { - node = skipPartiallyEmittedExpressions(node); - } + node = (node).expression; + continue; } - while (previousNode !== node); - return node; } - - export function skipAssertions(node: Expression): Expression; - export function skipAssertions(node: Node): Node; - export function skipAssertions(node: Node): Node { - while (isAssertionExpression(node) || node.kind === SyntaxKind.NonNullExpression) { - node = (node).expression; - } - - return node; +} +/* @internal */ +export function parenthesizeConciseBody(body: ConciseBody): ConciseBody { + if (!isBlock(body) && (isCommaSequence(body) || getLeftmostExpression(body, /*stopAtCallExpressions*/ false).kind === SyntaxKind.ObjectLiteralExpression)) { + return setTextRange(createParen(body), body); } - - function updateOuterExpression(outerExpression: OuterExpression, expression: Expression) { - switch (outerExpression.kind) { - case SyntaxKind.ParenthesizedExpression: return updateParen(outerExpression, expression); - case SyntaxKind.TypeAssertionExpression: return updateTypeAssertion(outerExpression, outerExpression.type, expression); - case SyntaxKind.AsExpression: return updateAsExpression(outerExpression, expression, outerExpression.type); - case SyntaxKind.NonNullExpression: return updateNonNullExpression(outerExpression, expression); - case SyntaxKind.PartiallyEmittedExpression: return updatePartiallyEmittedExpression(outerExpression, expression); + return body; +} +/* @internal */ +export function isCommaSequence(node: Expression): node is (BinaryExpression & { + operatorToken: Token; +}) | CommaListExpression { + return node.kind === SyntaxKind.BinaryExpression && (node).operatorToken.kind === SyntaxKind.CommaToken || + node.kind === SyntaxKind.CommaListExpression; +} +/* @internal */ +export const enum OuterExpressionKinds { + Parentheses = 1 << 0, + Assertions = 1 << 1, + PartiallyEmittedExpressions = 1 << 2, + All = Parentheses | Assertions | PartiallyEmittedExpressions +} +/* @internal */ +export type OuterExpression = ParenthesizedExpression | TypeAssertion | AsExpression | NonNullExpression | PartiallyEmittedExpression; +/* @internal */ +export function isOuterExpression(node: Node, kinds = OuterExpressionKinds.All): node is OuterExpression { + switch (node.kind) { + case SyntaxKind.ParenthesizedExpression: + return (kinds & OuterExpressionKinds.Parentheses) !== 0; + case SyntaxKind.TypeAssertionExpression: + case SyntaxKind.AsExpression: + case SyntaxKind.NonNullExpression: + return (kinds & OuterExpressionKinds.Assertions) !== 0; + case SyntaxKind.PartiallyEmittedExpression: + return (kinds & OuterExpressionKinds.PartiallyEmittedExpressions) !== 0; + } + return false; +} +/* @internal */ +export function skipOuterExpressions(node: Expression, kinds?: OuterExpressionKinds): Expression; +/* @internal */ +export function skipOuterExpressions(node: Node, kinds?: OuterExpressionKinds): Node; +/* @internal */ +export function skipOuterExpressions(node: Node, kinds = OuterExpressionKinds.All) { + let previousNode: Node; + do { + previousNode = node; + if (kinds & OuterExpressionKinds.Parentheses) { + node = skipParentheses(node); } - } - - /** - * Determines whether a node is a parenthesized expression that can be ignored when recreating outer expressions. - * - * A parenthesized expression can be ignored when all of the following are true: - * - * - It's `pos` and `end` are not -1 - * - It does not have a custom source map range - * - It does not have a custom comment range - * - It does not have synthetic leading or trailing comments - * - * If an outermost parenthesized expression is ignored, but the containing expression requires a parentheses around - * the expression to maintain precedence, a new parenthesized expression should be created automatically when - * the containing expression is created/updated. - */ - function isIgnorableParen(node: Expression) { - return node.kind === SyntaxKind.ParenthesizedExpression - && nodeIsSynthesized(node) - && nodeIsSynthesized(getSourceMapRange(node)) - && nodeIsSynthesized(getCommentRange(node)) - && !some(getSyntheticLeadingComments(node)) - && !some(getSyntheticTrailingComments(node)); - } - - export function recreateOuterExpressions(outerExpression: Expression | undefined, innerExpression: Expression, kinds = OuterExpressionKinds.All): Expression { - if (outerExpression && isOuterExpression(outerExpression, kinds) && !isIgnorableParen(outerExpression)) { - return updateOuterExpression( - outerExpression, - recreateOuterExpressions(outerExpression.expression, innerExpression) - ); + if (kinds & OuterExpressionKinds.Assertions) { + node = skipAssertions(node); } - return innerExpression; - } - - export function startOnNewLine(node: T): T { - return setStartsOnNewLine(node, /*newLine*/ true); - } - - export function getExternalHelpersModuleName(node: SourceFile) { - const parseNode = getOriginalNode(node, isSourceFile); - const emitNode = parseNode && parseNode.emitNode; - return emitNode && emitNode.externalHelpersModuleName; - } - - export function hasRecordedExternalHelpers(sourceFile: SourceFile) { - const parseNode = getOriginalNode(sourceFile, isSourceFile); - const emitNode = parseNode && parseNode.emitNode; - return !!emitNode && (!!emitNode.externalHelpersModuleName || !!emitNode.externalHelpers); - } - - export function createExternalHelpersImportDeclarationIfNeeded(sourceFile: SourceFile, compilerOptions: CompilerOptions, hasExportStarsToExportValues?: boolean, hasImportStar?: boolean, hasImportDefault?: boolean) { - if (compilerOptions.importHelpers && isEffectiveExternalModule(sourceFile, compilerOptions)) { - let namedBindings: NamedImportBindings | undefined; - const moduleKind = getEmitModuleKind(compilerOptions); - if (moduleKind >= ModuleKind.ES2015 && moduleKind <= ModuleKind.ESNext) { - // use named imports - const helpers = getEmitHelpers(sourceFile); - if (helpers) { - const helperNames: string[] = []; - for (const helper of helpers) { - if (!helper.scoped) { - const importName = (helper as UnscopedEmitHelper).importName; - if (importName) { - pushIfUnique(helperNames, importName); - } + if (kinds & OuterExpressionKinds.PartiallyEmittedExpressions) { + node = skipPartiallyEmittedExpressions(node); + } + } while (previousNode !== node); + return node; +} +/* @internal */ +export function skipAssertions(node: Expression): Expression; +/* @internal */ +export function skipAssertions(node: Node): Node; +/* @internal */ +export function skipAssertions(node: Node): Node { + while (isAssertionExpression(node) || node.kind === SyntaxKind.NonNullExpression) { + node = (node).expression; + } + return node; +} +/* @internal */ +function updateOuterExpression(outerExpression: OuterExpression, expression: Expression) { + switch (outerExpression.kind) { + case SyntaxKind.ParenthesizedExpression: return updateParen(outerExpression, expression); + case SyntaxKind.TypeAssertionExpression: return updateTypeAssertion(outerExpression, outerExpression.type, expression); + case SyntaxKind.AsExpression: return updateAsExpression(outerExpression, expression, outerExpression.type); + case SyntaxKind.NonNullExpression: return updateNonNullExpression(outerExpression, expression); + case SyntaxKind.PartiallyEmittedExpression: return updatePartiallyEmittedExpression(outerExpression, expression); + } +} +/** + * Determines whether a node is a parenthesized expression that can be ignored when recreating outer expressions. + * + * A parenthesized expression can be ignored when all of the following are true: + * + * - It's `pos` and `end` are not -1 + * - It does not have a custom source map range + * - It does not have a custom comment range + * - It does not have synthetic leading or trailing comments + * + * If an outermost parenthesized expression is ignored, but the containing expression requires a parentheses around + * the expression to maintain precedence, a new parenthesized expression should be created automatically when + * the containing expression is created/updated. + */ +/* @internal */ +function isIgnorableParen(node: Expression) { + return node.kind === SyntaxKind.ParenthesizedExpression + && nodeIsSynthesized(node) + && nodeIsSynthesized(getSourceMapRange(node)) + && nodeIsSynthesized(getCommentRange(node)) + && !some(getSyntheticLeadingComments(node)) + && !some(getSyntheticTrailingComments(node)); +} +/* @internal */ +export function recreateOuterExpressions(outerExpression: Expression | undefined, innerExpression: Expression, kinds = OuterExpressionKinds.All): Expression { + if (outerExpression && isOuterExpression(outerExpression, kinds) && !isIgnorableParen(outerExpression)) { + return updateOuterExpression(outerExpression, recreateOuterExpressions(outerExpression.expression, innerExpression)); + } + return innerExpression; +} +/* @internal */ +export function startOnNewLine(node: T): T { + return setStartsOnNewLine(node, /*newLine*/ true); +} +/* @internal */ +export function getExternalHelpersModuleName(node: SourceFile) { + const parseNode = getOriginalNode(node, isSourceFile); + const emitNode = parseNode && parseNode.emitNode; + return emitNode && emitNode.externalHelpersModuleName; +} +/* @internal */ +export function hasRecordedExternalHelpers(sourceFile: SourceFile) { + const parseNode = getOriginalNode(sourceFile, isSourceFile); + const emitNode = parseNode && parseNode.emitNode; + return !!emitNode && (!!emitNode.externalHelpersModuleName || !!emitNode.externalHelpers); +} +/* @internal */ +export function createExternalHelpersImportDeclarationIfNeeded(sourceFile: SourceFile, compilerOptions: CompilerOptions, hasExportStarsToExportValues?: boolean, hasImportStar?: boolean, hasImportDefault?: boolean) { + if (compilerOptions.importHelpers && isEffectiveExternalModule(sourceFile, compilerOptions)) { + let namedBindings: NamedImportBindings | undefined; + const moduleKind = getEmitModuleKind(compilerOptions); + if (moduleKind >= ModuleKind.ES2015 && moduleKind <= ModuleKind.ESNext) { + // use named imports + const helpers = getEmitHelpers(sourceFile); + if (helpers) { + const helperNames: string[] = []; + for (const helper of helpers) { + if (!helper.scoped) { + const importName = (helper as UnscopedEmitHelper).importName; + if (importName) { + pushIfUnique(helperNames, importName); } } - if (some(helperNames)) { - helperNames.sort(compareStringsCaseSensitive); - // Alias the imports if the names are used somewhere in the file. - // NOTE: We don't need to care about global import collisions as this is a module. - namedBindings = createNamedImports( - map(helperNames, name => isFileLevelUniqueName(sourceFile, name) - ? createImportSpecifier(/*propertyName*/ undefined, createIdentifier(name)) - : createImportSpecifier(createIdentifier(name), getUnscopedHelperName(name)) - ) - ); - const parseNode = getOriginalNode(sourceFile, isSourceFile); - const emitNode = getOrCreateEmitNode(parseNode); - emitNode.externalHelpers = true; - } } - } - else { - // use a namespace import - const externalHelpersModuleName = getOrCreateExternalHelpersModuleNameIfNeeded(sourceFile, compilerOptions, hasExportStarsToExportValues, hasImportStar || hasImportDefault); - if (externalHelpersModuleName) { - namedBindings = createNamespaceImport(externalHelpersModuleName); + if (some(helperNames)) { + helperNames.sort(compareStringsCaseSensitive); + // Alias the imports if the names are used somewhere in the file. + // NOTE: We don't need to care about global import collisions as this is a module. + namedBindings = createNamedImports(map(helperNames, name => isFileLevelUniqueName(sourceFile, name) + ? createImportSpecifier(/*propertyName*/ undefined, createIdentifier(name)) + : createImportSpecifier(createIdentifier(name), getUnscopedHelperName(name)))); + const parseNode = getOriginalNode(sourceFile, isSourceFile); + const emitNode = getOrCreateEmitNode(parseNode); + emitNode.externalHelpers = true; } } - if (namedBindings) { - const externalHelpersImportDeclaration = createImportDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - createImportClause(/*name*/ undefined, namedBindings), - createLiteral(externalHelpersModuleNameText) - ); - addEmitFlags(externalHelpersImportDeclaration, EmitFlags.NeverApplyImportHelper); - return externalHelpersImportDeclaration; - } } - } - - export function getOrCreateExternalHelpersModuleNameIfNeeded(node: SourceFile, compilerOptions: CompilerOptions, hasExportStarsToExportValues?: boolean, hasImportStarOrImportDefault?: boolean) { - if (compilerOptions.importHelpers && isEffectiveExternalModule(node, compilerOptions)) { - const externalHelpersModuleName = getExternalHelpersModuleName(node); + else { + // use a namespace import + const externalHelpersModuleName = getOrCreateExternalHelpersModuleNameIfNeeded(sourceFile, compilerOptions, hasExportStarsToExportValues, hasImportStar || hasImportDefault); if (externalHelpersModuleName) { - return externalHelpersModuleName; + namedBindings = createNamespaceImport(externalHelpersModuleName); } - - const moduleKind = getEmitModuleKind(compilerOptions); - let create = (hasExportStarsToExportValues || (compilerOptions.esModuleInterop && hasImportStarOrImportDefault)) - && moduleKind !== ModuleKind.System - && moduleKind < ModuleKind.ES2015; - if (!create) { - const helpers = getEmitHelpers(node); - if (helpers) { - for (const helper of helpers) { - if (!helper.scoped) { - create = true; - break; - } + } + if (namedBindings) { + const externalHelpersImportDeclaration = createImportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, createImportClause(/*name*/ undefined, namedBindings), createLiteral(externalHelpersModuleNameText)); + addEmitFlags(externalHelpersImportDeclaration, EmitFlags.NeverApplyImportHelper); + return externalHelpersImportDeclaration; + } + } +} +/* @internal */ +export function getOrCreateExternalHelpersModuleNameIfNeeded(node: SourceFile, compilerOptions: CompilerOptions, hasExportStarsToExportValues?: boolean, hasImportStarOrImportDefault?: boolean) { + if (compilerOptions.importHelpers && isEffectiveExternalModule(node, compilerOptions)) { + const externalHelpersModuleName = getExternalHelpersModuleName(node); + if (externalHelpersModuleName) { + return externalHelpersModuleName; + } + const moduleKind = getEmitModuleKind(compilerOptions); + let create = (hasExportStarsToExportValues || (compilerOptions.esModuleInterop && hasImportStarOrImportDefault)) + && moduleKind !== ModuleKind.System + && moduleKind < ModuleKind.ES2015; + if (!create) { + const helpers = getEmitHelpers(node); + if (helpers) { + for (const helper of helpers) { + if (!helper.scoped) { + create = true; + break; } } } - - if (create) { - const parseNode = getOriginalNode(node, isSourceFile); - const emitNode = getOrCreateEmitNode(parseNode); - return emitNode.externalHelpersModuleName || (emitNode.externalHelpersModuleName = createUniqueName(externalHelpersModuleNameText)); - } - } - } - - /** - * Get the name of that target module from an import or export declaration - */ - export function getLocalNameForExternalImport(node: ImportDeclaration | ExportDeclaration | ImportEqualsDeclaration, sourceFile: SourceFile): Identifier | undefined { - const namespaceDeclaration = getNamespaceDeclarationNode(node); - if (namespaceDeclaration && !isDefaultImport(node)) { - const name = namespaceDeclaration.name; - return isGeneratedIdentifier(name) ? name : createIdentifier(getSourceTextOfNodeFromSourceFile(sourceFile, name) || idText(name)); - } - if (node.kind === SyntaxKind.ImportDeclaration && node.importClause) { - return getGeneratedNameForNode(node); } - if (node.kind === SyntaxKind.ExportDeclaration && node.moduleSpecifier) { - return getGeneratedNameForNode(node); + if (create) { + const parseNode = getOriginalNode(node, isSourceFile); + const emitNode = getOrCreateEmitNode(parseNode); + return emitNode.externalHelpersModuleName || (emitNode.externalHelpersModuleName = createUniqueName(externalHelpersModuleNameText)); } - return undefined; } - - /** - * Get the name of a target module from an import/export declaration as should be written in the emitted output. - * The emitted output name can be different from the input if: - * 1. The module has a /// - * 2. --out or --outFile is used, making the name relative to the rootDir - * 3- The containing SourceFile has an entry in renamedDependencies for the import as requested by some module loaders (e.g. System). - * Otherwise, a new StringLiteral node representing the module name will be returned. - */ - export function getExternalModuleNameLiteral(importNode: ImportDeclaration | ExportDeclaration | ImportEqualsDeclaration, sourceFile: SourceFile, host: EmitHost, resolver: EmitResolver, compilerOptions: CompilerOptions) { - const moduleName = getExternalModuleName(importNode)!; // TODO: GH#18217 - if (moduleName.kind === SyntaxKind.StringLiteral) { - return tryGetModuleNameFromDeclaration(importNode, host, resolver, compilerOptions) - || tryRenameExternalModule(moduleName, sourceFile) - || getSynthesizedClone(moduleName); - } - - return undefined; +} +/** + * Get the name of that target module from an import or export declaration + */ +/* @internal */ +export function getLocalNameForExternalImport(node: ImportDeclaration | ExportDeclaration | ImportEqualsDeclaration, sourceFile: SourceFile): Identifier | undefined { + const namespaceDeclaration = getNamespaceDeclarationNode(node); + if (namespaceDeclaration && !isDefaultImport(node)) { + const name = namespaceDeclaration.name; + return isGeneratedIdentifier(name) ? name : createIdentifier(getSourceTextOfNodeFromSourceFile(sourceFile, name) || idText(name)); } - - /** - * Some bundlers (SystemJS builder) sometimes want to rename dependencies. - * Here we check if alternative name was provided for a given moduleName and return it if possible. - */ - function tryRenameExternalModule(moduleName: LiteralExpression, sourceFile: SourceFile) { - const rename = sourceFile.renamedDependencies && sourceFile.renamedDependencies.get(moduleName.text); - return rename && createLiteral(rename); - } - - /** - * Get the name of a module as should be written in the emitted output. - * The emitted output name can be different from the input if: - * 1. The module has a /// - * 2. --out or --outFile is used, making the name relative to the rootDir - * Otherwise, a new StringLiteral node representing the module name will be returned. - */ - export function tryGetModuleNameFromFile(file: SourceFile | undefined, host: EmitHost, options: CompilerOptions): StringLiteral | undefined { - if (!file) { - return undefined; - } - if (file.moduleName) { - return createLiteral(file.moduleName); - } - if (!file.isDeclarationFile && (options.out || options.outFile)) { - return createLiteral(getExternalModuleNameFromPath(host, file.fileName)); - } - return undefined; + if (node.kind === SyntaxKind.ImportDeclaration && node.importClause) { + return getGeneratedNameForNode(node); } - - function tryGetModuleNameFromDeclaration(declaration: ImportEqualsDeclaration | ImportDeclaration | ExportDeclaration, host: EmitHost, resolver: EmitResolver, compilerOptions: CompilerOptions) { - return tryGetModuleNameFromFile(resolver.getExternalModuleFileFromDeclaration(declaration), host, compilerOptions); - } - - /** - * Gets the initializer of an BindingOrAssignmentElement. - */ - export function getInitializerOfBindingOrAssignmentElement(bindingElement: BindingOrAssignmentElement): Expression | undefined { - if (isDeclarationBindingElement(bindingElement)) { - // `1` in `let { a = 1 } = ...` - // `1` in `let { a: b = 1 } = ...` - // `1` in `let { a: {b} = 1 } = ...` - // `1` in `let { a: [b] = 1 } = ...` - // `1` in `let [a = 1] = ...` - // `1` in `let [{a} = 1] = ...` - // `1` in `let [[a] = 1] = ...` - return bindingElement.initializer; - } - - if (isPropertyAssignment(bindingElement)) { - // `1` in `({ a: b = 1 } = ...)` - // `1` in `({ a: {b} = 1 } = ...)` - // `1` in `({ a: [b] = 1 } = ...)` - const initializer = bindingElement.initializer; - return isAssignmentExpression(initializer, /*excludeCompoundAssignment*/ true) - ? initializer.right - : undefined; - } - - if (isShorthandPropertyAssignment(bindingElement)) { - // `1` in `({ a = 1 } = ...)` - return bindingElement.objectAssignmentInitializer; - } - - if (isAssignmentExpression(bindingElement, /*excludeCompoundAssignment*/ true)) { - // `1` in `[a = 1] = ...` - // `1` in `[{a} = 1] = ...` - // `1` in `[[a] = 1] = ...` - return bindingElement.right; - } - - if (isSpreadElement(bindingElement)) { - // Recovery consistent with existing emit. - return getInitializerOfBindingOrAssignmentElement(bindingElement.expression); - } + if (node.kind === SyntaxKind.ExportDeclaration && node.moduleSpecifier) { + return getGeneratedNameForNode(node); } - - /** - * Gets the name of an BindingOrAssignmentElement. - */ - export function getTargetOfBindingOrAssignmentElement(bindingElement: BindingOrAssignmentElement): BindingOrAssignmentElementTarget | undefined { - if (isDeclarationBindingElement(bindingElement)) { - // `a` in `let { a } = ...` - // `a` in `let { a = 1 } = ...` - // `b` in `let { a: b } = ...` - // `b` in `let { a: b = 1 } = ...` - // `a` in `let { ...a } = ...` - // `{b}` in `let { a: {b} } = ...` - // `{b}` in `let { a: {b} = 1 } = ...` - // `[b]` in `let { a: [b] } = ...` - // `[b]` in `let { a: [b] = 1 } = ...` - // `a` in `let [a] = ...` - // `a` in `let [a = 1] = ...` - // `a` in `let [...a] = ...` - // `{a}` in `let [{a}] = ...` - // `{a}` in `let [{a} = 1] = ...` - // `[a]` in `let [[a]] = ...` - // `[a]` in `let [[a] = 1] = ...` - return bindingElement.name; - } - - if (isObjectLiteralElementLike(bindingElement)) { - switch (bindingElement.kind) { - case SyntaxKind.PropertyAssignment: - // `b` in `({ a: b } = ...)` - // `b` in `({ a: b = 1 } = ...)` - // `{b}` in `({ a: {b} } = ...)` - // `{b}` in `({ a: {b} = 1 } = ...)` - // `[b]` in `({ a: [b] } = ...)` - // `[b]` in `({ a: [b] = 1 } = ...)` - // `b.c` in `({ a: b.c } = ...)` - // `b.c` in `({ a: b.c = 1 } = ...)` - // `b[0]` in `({ a: b[0] } = ...)` - // `b[0]` in `({ a: b[0] = 1 } = ...)` - return getTargetOfBindingOrAssignmentElement(bindingElement.initializer); - - case SyntaxKind.ShorthandPropertyAssignment: - // `a` in `({ a } = ...)` - // `a` in `({ a = 1 } = ...)` - return bindingElement.name; - - case SyntaxKind.SpreadAssignment: - // `a` in `({ ...a } = ...)` - return getTargetOfBindingOrAssignmentElement(bindingElement.expression); - } - - // no target - return undefined; - } - - if (isAssignmentExpression(bindingElement, /*excludeCompoundAssignment*/ true)) { - // `a` in `[a = 1] = ...` - // `{a}` in `[{a} = 1] = ...` - // `[a]` in `[[a] = 1] = ...` - // `a.b` in `[a.b = 1] = ...` - // `a[0]` in `[a[0] = 1] = ...` - return getTargetOfBindingOrAssignmentElement(bindingElement.left); - } - - if (isSpreadElement(bindingElement)) { - // `a` in `[...a] = ...` - return getTargetOfBindingOrAssignmentElement(bindingElement.expression); - } - - // `a` in `[a] = ...` - // `{a}` in `[{a}] = ...` - // `[a]` in `[[a]] = ...` - // `a.b` in `[a.b] = ...` - // `a[0]` in `[a[0]] = ...` - return bindingElement; - } - - /** - * Determines whether an BindingOrAssignmentElement is a rest element. - */ - export function getRestIndicatorOfBindingOrAssignmentElement(bindingElement: BindingOrAssignmentElement): BindingOrAssignmentElementRestIndicator | undefined { - switch (bindingElement.kind) { - case SyntaxKind.Parameter: - case SyntaxKind.BindingElement: - // `...` in `let [...a] = ...` - return bindingElement.dotDotDotToken; - - case SyntaxKind.SpreadElement: - case SyntaxKind.SpreadAssignment: - // `...` in `[...a] = ...` - return bindingElement; - } - + return undefined; +} +/** + * Get the name of a target module from an import/export declaration as should be written in the emitted output. + * The emitted output name can be different from the input if: + * 1. The module has a /// + * 2. --out or --outFile is used, making the name relative to the rootDir + * 3- The containing SourceFile has an entry in renamedDependencies for the import as requested by some module loaders (e.g. System). + * Otherwise, a new StringLiteral node representing the module name will be returned. + */ +/* @internal */ +export function getExternalModuleNameLiteral(importNode: ImportDeclaration | ExportDeclaration | ImportEqualsDeclaration, sourceFile: SourceFile, host: EmitHost, resolver: EmitResolver, compilerOptions: CompilerOptions) { + const moduleName = (getExternalModuleName(importNode)!); // TODO: GH#18217 + if (moduleName.kind === SyntaxKind.StringLiteral) { + return tryGetModuleNameFromDeclaration(importNode, host, resolver, compilerOptions) + || tryRenameExternalModule((moduleName), sourceFile) + || getSynthesizedClone((moduleName)); + } + return undefined; +} +/** + * Some bundlers (SystemJS builder) sometimes want to rename dependencies. + * Here we check if alternative name was provided for a given moduleName and return it if possible. + */ +/* @internal */ +function tryRenameExternalModule(moduleName: LiteralExpression, sourceFile: SourceFile) { + const rename = sourceFile.renamedDependencies && sourceFile.renamedDependencies.get(moduleName.text); + return rename && createLiteral(rename); +} +/** + * Get the name of a module as should be written in the emitted output. + * The emitted output name can be different from the input if: + * 1. The module has a /// + * 2. --out or --outFile is used, making the name relative to the rootDir + * Otherwise, a new StringLiteral node representing the module name will be returned. + */ +/* @internal */ +export function tryGetModuleNameFromFile(file: SourceFile | undefined, host: EmitHost, options: CompilerOptions): StringLiteral | undefined { + if (!file) { return undefined; } - - /** - * Gets the property name of a BindingOrAssignmentElement - */ - export function getPropertyNameOfBindingOrAssignmentElement(bindingElement: BindingOrAssignmentElement): Exclude | undefined { - const propertyName = tryGetPropertyNameOfBindingOrAssignmentElement(bindingElement); - Debug.assert(!!propertyName || isSpreadAssignment(bindingElement), "Invalid property name for binding element."); - return propertyName; - } - - export function tryGetPropertyNameOfBindingOrAssignmentElement(bindingElement: BindingOrAssignmentElement): Exclude | undefined { + if (file.moduleName) { + return createLiteral(file.moduleName); + } + if (!file.isDeclarationFile && (options.out || options.outFile)) { + return createLiteral(getExternalModuleNameFromPath(host, file.fileName)); + } + return undefined; +} +/* @internal */ +function tryGetModuleNameFromDeclaration(declaration: ImportEqualsDeclaration | ImportDeclaration | ExportDeclaration, host: EmitHost, resolver: EmitResolver, compilerOptions: CompilerOptions) { + return tryGetModuleNameFromFile(resolver.getExternalModuleFileFromDeclaration(declaration), host, compilerOptions); +} +/** + * Gets the initializer of an BindingOrAssignmentElement. + */ +/* @internal */ +export function getInitializerOfBindingOrAssignmentElement(bindingElement: BindingOrAssignmentElement): Expression | undefined { + if (isDeclarationBindingElement(bindingElement)) { + // `1` in `let { a = 1 } = ...` + // `1` in `let { a: b = 1 } = ...` + // `1` in `let { a: {b} = 1 } = ...` + // `1` in `let { a: [b] = 1 } = ...` + // `1` in `let [a = 1] = ...` + // `1` in `let [{a} = 1] = ...` + // `1` in `let [[a] = 1] = ...` + return bindingElement.initializer; + } + if (isPropertyAssignment(bindingElement)) { + // `1` in `({ a: b = 1 } = ...)` + // `1` in `({ a: {b} = 1 } = ...)` + // `1` in `({ a: [b] = 1 } = ...)` + const initializer = bindingElement.initializer; + return isAssignmentExpression(initializer, /*excludeCompoundAssignment*/ true) + ? initializer.right + : undefined; + } + if (isShorthandPropertyAssignment(bindingElement)) { + // `1` in `({ a = 1 } = ...)` + return bindingElement.objectAssignmentInitializer; + } + if (isAssignmentExpression(bindingElement, /*excludeCompoundAssignment*/ true)) { + // `1` in `[a = 1] = ...` + // `1` in `[{a} = 1] = ...` + // `1` in `[[a] = 1] = ...` + return bindingElement.right; + } + if (isSpreadElement(bindingElement)) { + // Recovery consistent with existing emit. + return getInitializerOfBindingOrAssignmentElement((bindingElement.expression)); + } +} +/** + * Gets the name of an BindingOrAssignmentElement. + */ +/* @internal */ +export function getTargetOfBindingOrAssignmentElement(bindingElement: BindingOrAssignmentElement): BindingOrAssignmentElementTarget | undefined { + if (isDeclarationBindingElement(bindingElement)) { + // `a` in `let { a } = ...` + // `a` in `let { a = 1 } = ...` + // `b` in `let { a: b } = ...` + // `b` in `let { a: b = 1 } = ...` + // `a` in `let { ...a } = ...` + // `{b}` in `let { a: {b} } = ...` + // `{b}` in `let { a: {b} = 1 } = ...` + // `[b]` in `let { a: [b] } = ...` + // `[b]` in `let { a: [b] = 1 } = ...` + // `a` in `let [a] = ...` + // `a` in `let [a = 1] = ...` + // `a` in `let [...a] = ...` + // `{a}` in `let [{a}] = ...` + // `{a}` in `let [{a} = 1] = ...` + // `[a]` in `let [[a]] = ...` + // `[a]` in `let [[a] = 1] = ...` + return bindingElement.name; + } + if (isObjectLiteralElementLike(bindingElement)) { switch (bindingElement.kind) { - case SyntaxKind.BindingElement: - // `a` in `let { a: b } = ...` - // `[a]` in `let { [a]: b } = ...` - // `"a"` in `let { "a": b } = ...` - // `1` in `let { 1: b } = ...` - if (bindingElement.propertyName) { - const propertyName = bindingElement.propertyName; - if (isPrivateIdentifier(propertyName)) { - return Debug.failBadSyntaxKind(propertyName); - } - return isComputedPropertyName(propertyName) && isStringOrNumericLiteral(propertyName.expression) - ? propertyName.expression - : propertyName; - } - - break; - case SyntaxKind.PropertyAssignment: - // `a` in `({ a: b } = ...)` - // `[a]` in `({ [a]: b } = ...)` - // `"a"` in `({ "a": b } = ...)` - // `1` in `({ 1: b } = ...)` - if (bindingElement.name) { - const propertyName = bindingElement.name; - if (isPrivateIdentifier(propertyName)) { - return Debug.failBadSyntaxKind(propertyName); - } - return isComputedPropertyName(propertyName) && isStringOrNumericLiteral(propertyName.expression) - ? propertyName.expression - : propertyName; - } - - break; - + // `b` in `({ a: b } = ...)` + // `b` in `({ a: b = 1 } = ...)` + // `{b}` in `({ a: {b} } = ...)` + // `{b}` in `({ a: {b} = 1 } = ...)` + // `[b]` in `({ a: [b] } = ...)` + // `[b]` in `({ a: [b] = 1 } = ...)` + // `b.c` in `({ a: b.c } = ...)` + // `b.c` in `({ a: b.c = 1 } = ...)` + // `b[0]` in `({ a: b[0] } = ...)` + // `b[0]` in `({ a: b[0] = 1 } = ...)` + return getTargetOfBindingOrAssignmentElement((bindingElement.initializer)); + case SyntaxKind.ShorthandPropertyAssignment: + // `a` in `({ a } = ...)` + // `a` in `({ a = 1 } = ...)` + return bindingElement.name; case SyntaxKind.SpreadAssignment: // `a` in `({ ...a } = ...)` - if (bindingElement.name && isPrivateIdentifier(bindingElement.name)) { - return Debug.failBadSyntaxKind(bindingElement.name); - } - return bindingElement.name; - } - - const target = getTargetOfBindingOrAssignmentElement(bindingElement); - if (target && isPropertyName(target)) { - return isComputedPropertyName(target) && isStringOrNumericLiteral(target.expression) - ? target.expression - : target; - } - } - - function isStringOrNumericLiteral(node: Node): node is StringLiteral | NumericLiteral { - const kind = node.kind; - return kind === SyntaxKind.StringLiteral - || kind === SyntaxKind.NumericLiteral; - } - - /** - * Gets the elements of a BindingOrAssignmentPattern - */ - export function getElementsOfBindingOrAssignmentPattern(name: BindingOrAssignmentPattern): readonly BindingOrAssignmentElement[] { - switch (name.kind) { - case SyntaxKind.ObjectBindingPattern: - case SyntaxKind.ArrayBindingPattern: - case SyntaxKind.ArrayLiteralExpression: - // `a` in `{a}` - // `a` in `[a]` - return name.elements; - - case SyntaxKind.ObjectLiteralExpression: - // `a` in `{a}` - return name.properties; + return getTargetOfBindingOrAssignmentElement((bindingElement.expression)); } + // no target + return undefined; } - - export function convertToArrayAssignmentElement(element: BindingOrAssignmentElement) { - if (isBindingElement(element)) { - if (element.dotDotDotToken) { - Debug.assertNode(element.name, isIdentifier); - return setOriginalNode(setTextRange(createSpread(element.name), element), element); + if (isAssignmentExpression(bindingElement, /*excludeCompoundAssignment*/ true)) { + // `a` in `[a = 1] = ...` + // `{a}` in `[{a} = 1] = ...` + // `[a]` in `[[a] = 1] = ...` + // `a.b` in `[a.b = 1] = ...` + // `a[0]` in `[a[0] = 1] = ...` + return getTargetOfBindingOrAssignmentElement((bindingElement.left)); + } + if (isSpreadElement(bindingElement)) { + // `a` in `[...a] = ...` + return getTargetOfBindingOrAssignmentElement((bindingElement.expression)); + } + // `a` in `[a] = ...` + // `{a}` in `[{a}] = ...` + // `[a]` in `[[a]] = ...` + // `a.b` in `[a.b] = ...` + // `a[0]` in `[a[0]] = ...` + return bindingElement; +} +/** + * Determines whether an BindingOrAssignmentElement is a rest element. + */ +/* @internal */ +export function getRestIndicatorOfBindingOrAssignmentElement(bindingElement: BindingOrAssignmentElement): BindingOrAssignmentElementRestIndicator | undefined { + switch (bindingElement.kind) { + case SyntaxKind.Parameter: + case SyntaxKind.BindingElement: + // `...` in `let [...a] = ...` + return bindingElement.dotDotDotToken; + case SyntaxKind.SpreadElement: + case SyntaxKind.SpreadAssignment: + // `...` in `[...a] = ...` + return bindingElement; + } + return undefined; +} +/** + * Gets the property name of a BindingOrAssignmentElement + */ +/* @internal */ +export function getPropertyNameOfBindingOrAssignmentElement(bindingElement: BindingOrAssignmentElement): Exclude | undefined { + const propertyName = tryGetPropertyNameOfBindingOrAssignmentElement(bindingElement); + Debug.assert(!!propertyName || isSpreadAssignment(bindingElement), "Invalid property name for binding element."); + return propertyName; +} +/* @internal */ +export function tryGetPropertyNameOfBindingOrAssignmentElement(bindingElement: BindingOrAssignmentElement): Exclude | undefined { + switch (bindingElement.kind) { + case SyntaxKind.BindingElement: + // `a` in `let { a: b } = ...` + // `[a]` in `let { [a]: b } = ...` + // `"a"` in `let { "a": b } = ...` + // `1` in `let { 1: b } = ...` + if (bindingElement.propertyName) { + const propertyName = bindingElement.propertyName; + if (isPrivateIdentifier(propertyName)) { + return Debug.failBadSyntaxKind(propertyName); + } + return isComputedPropertyName(propertyName) && isStringOrNumericLiteral(propertyName.expression) + ? propertyName.expression + : propertyName; } - const expression = convertToAssignmentElementTarget(element.name); - return element.initializer - ? setOriginalNode( - setTextRange( - createAssignment(expression, element.initializer), - element - ), - element - ) - : expression; - } - Debug.assertNode(element, isExpression); - return element; - } - - export function convertToObjectAssignmentElement(element: BindingOrAssignmentElement) { - if (isBindingElement(element)) { - if (element.dotDotDotToken) { - Debug.assertNode(element.name, isIdentifier); - return setOriginalNode(setTextRange(createSpreadAssignment(element.name), element), element); + break; + case SyntaxKind.PropertyAssignment: + // `a` in `({ a: b } = ...)` + // `[a]` in `({ [a]: b } = ...)` + // `"a"` in `({ "a": b } = ...)` + // `1` in `({ 1: b } = ...)` + if (bindingElement.name) { + const propertyName = bindingElement.name; + if (isPrivateIdentifier(propertyName)) { + return Debug.failBadSyntaxKind(propertyName); + } + return isComputedPropertyName(propertyName) && isStringOrNumericLiteral(propertyName.expression) + ? propertyName.expression + : propertyName; } - if (element.propertyName) { - const expression = convertToAssignmentElementTarget(element.name); - return setOriginalNode(setTextRange(createPropertyAssignment(element.propertyName, element.initializer ? createAssignment(expression, element.initializer) : expression), element), element); + break; + case SyntaxKind.SpreadAssignment: + // `a` in `({ ...a } = ...)` + if (bindingElement.name && isPrivateIdentifier(bindingElement.name)) { + return Debug.failBadSyntaxKind(bindingElement.name); } + return bindingElement.name; + } + const target = getTargetOfBindingOrAssignmentElement(bindingElement); + if (target && isPropertyName(target)) { + return isComputedPropertyName(target) && isStringOrNumericLiteral(target.expression) + ? target.expression + : target; + } +} +/* @internal */ +function isStringOrNumericLiteral(node: Node): node is StringLiteral | NumericLiteral { + const kind = node.kind; + return kind === SyntaxKind.StringLiteral + || kind === SyntaxKind.NumericLiteral; +} +/** + * Gets the elements of a BindingOrAssignmentPattern + */ +/* @internal */ +export function getElementsOfBindingOrAssignmentPattern(name: BindingOrAssignmentPattern): readonly BindingOrAssignmentElement[] { + switch (name.kind) { + case SyntaxKind.ObjectBindingPattern: + case SyntaxKind.ArrayBindingPattern: + case SyntaxKind.ArrayLiteralExpression: + // `a` in `{a}` + // `a` in `[a]` + return name.elements; + case SyntaxKind.ObjectLiteralExpression: + // `a` in `{a}` + return name.properties; + } +} +/* @internal */ +export function convertToArrayAssignmentElement(element: BindingOrAssignmentElement) { + if (isBindingElement(element)) { + if (element.dotDotDotToken) { Debug.assertNode(element.name, isIdentifier); - return setOriginalNode(setTextRange(createShorthandPropertyAssignment(element.name, element.initializer), element), element); + return setOriginalNode(setTextRange(createSpread(element.name), element), element); } - Debug.assertNode(element, isObjectLiteralElementLike); - return element; + const expression = convertToAssignmentElementTarget(element.name); + return element.initializer + ? setOriginalNode(setTextRange(createAssignment(expression, element.initializer), element), element) + : expression; } - - export function convertToAssignmentPattern(node: BindingOrAssignmentPattern): AssignmentPattern { - switch (node.kind) { - case SyntaxKind.ArrayBindingPattern: - case SyntaxKind.ArrayLiteralExpression: - return convertToArrayAssignmentPattern(node); - - case SyntaxKind.ObjectBindingPattern: - case SyntaxKind.ObjectLiteralExpression: - return convertToObjectAssignmentPattern(node); + Debug.assertNode(element, isExpression); + return element; +} +/* @internal */ +export function convertToObjectAssignmentElement(element: BindingOrAssignmentElement) { + if (isBindingElement(element)) { + if (element.dotDotDotToken) { + Debug.assertNode(element.name, isIdentifier); + return setOriginalNode(setTextRange(createSpreadAssignment(element.name), element), element); } - } - - export function convertToObjectAssignmentPattern(node: ObjectBindingOrAssignmentPattern) { - if (isObjectBindingPattern(node)) { - return setOriginalNode( - setTextRange( - createObjectLiteral(map(node.elements, convertToObjectAssignmentElement)), - node - ), - node - ); + if (element.propertyName) { + const expression = convertToAssignmentElementTarget(element.name); + return setOriginalNode(setTextRange(createPropertyAssignment(element.propertyName, element.initializer ? createAssignment(expression, element.initializer) : expression), element), element); } - Debug.assertNode(node, isObjectLiteralExpression); - return node; + Debug.assertNode(element.name, isIdentifier); + return setOriginalNode(setTextRange(createShorthandPropertyAssignment(element.name, element.initializer), element), element); } - - export function convertToArrayAssignmentPattern(node: ArrayBindingOrAssignmentPattern) { - if (isArrayBindingPattern(node)) { - return setOriginalNode( - setTextRange( - createArrayLiteral(map(node.elements, convertToArrayAssignmentElement)), - node - ), - node - ); - } - Debug.assertNode(node, isArrayLiteralExpression); - return node; + Debug.assertNode(element, isObjectLiteralElementLike); + return element; +} +/* @internal */ +export function convertToAssignmentPattern(node: BindingOrAssignmentPattern): AssignmentPattern { + switch (node.kind) { + case SyntaxKind.ArrayBindingPattern: + case SyntaxKind.ArrayLiteralExpression: + return convertToArrayAssignmentPattern(node); + case SyntaxKind.ObjectBindingPattern: + case SyntaxKind.ObjectLiteralExpression: + return convertToObjectAssignmentPattern(node); } - - export function convertToAssignmentElementTarget(node: BindingOrAssignmentElementTarget): Expression { - if (isBindingPattern(node)) { - return convertToAssignmentPattern(node); - } - - Debug.assertNode(node, isExpression); - return node; +} +/* @internal */ +export function convertToObjectAssignmentPattern(node: ObjectBindingOrAssignmentPattern) { + if (isObjectBindingPattern(node)) { + return setOriginalNode(setTextRange(createObjectLiteral(map(node.elements, convertToObjectAssignmentElement)), node), node); + } + Debug.assertNode(node, isObjectLiteralExpression); + return node; +} +/* @internal */ +export function convertToArrayAssignmentPattern(node: ArrayBindingOrAssignmentPattern) { + if (isArrayBindingPattern(node)) { + return setOriginalNode(setTextRange(createArrayLiteral(map(node.elements, convertToArrayAssignmentElement)), node), node); + } + Debug.assertNode(node, isArrayLiteralExpression); + return node; +} +/* @internal */ +export function convertToAssignmentElementTarget(node: BindingOrAssignmentElementTarget): Expression { + if (isBindingPattern(node)) { + return convertToAssignmentPattern(node); } + Debug.assertNode(node, isExpression); + return node; } diff --git a/src/compiler/factoryPublic.ts b/src/compiler/factoryPublic.ts index 04ec83a622231..41440598dfd7b 100644 --- a/src/compiler/factoryPublic.ts +++ b/src/compiler/factoryPublic.ts @@ -1,3702 +1,3030 @@ -namespace ts { - function createSynthesizedNode(kind: SyntaxKind): Node { - const node = createNode(kind, -1, -1); - node.flags |= NodeFlags.Synthesized; - return node; - } - - /* @internal */ - export function updateNode(updated: T, original: T): T { - if (updated !== original) { - setOriginalNode(updated, original); - setTextRange(updated, original); - aggregateTransformFlags(updated); - } - return updated; - } - - /* @internal */ export function createNodeArray(elements?: T[], hasTrailingComma?: boolean): MutableNodeArray; - export function createNodeArray(elements?: readonly T[], hasTrailingComma?: boolean): NodeArray; - /** - * Make `elements` into a `NodeArray`. If `elements` is `undefined`, returns an empty `NodeArray`. - */ - export function createNodeArray(elements?: readonly T[], hasTrailingComma?: boolean): NodeArray { - if (!elements || elements === emptyArray) { - elements = []; - } - else if (isNodeArray(elements)) { - return elements; - } - - const array = >elements; - array.pos = -1; - array.end = -1; - array.hasTrailingComma = hasTrailingComma; - return array; - } - - /** - * Creates a shallow, memberwise clone of a node with no source map location. - */ - /* @internal */ - export function getSynthesizedClone(node: T): T { - // We don't use "clone" from core.ts here, as we need to preserve the prototype chain of - // the original node. We also need to exclude specific properties and only include own- - // properties (to skip members already defined on the shared prototype). - - if (node === undefined) { - return node; - } - - const clone = createSynthesizedNode(node.kind); - clone.flags |= node.flags; - setOriginalNode(clone, node); - - for (const key in node) { - if (clone.hasOwnProperty(key) || !node.hasOwnProperty(key)) { - continue; - } - - (clone)[key] = (node)[key]; - } - - return clone; - } - - // Literals - - /* @internal */ export function createLiteral(value: string | StringLiteral | NoSubstitutionTemplateLiteral | NumericLiteral | Identifier, isSingleQuote: boolean): StringLiteral; // eslint-disable-line @typescript-eslint/unified-signatures - /* @internal */ export function createLiteral(value: string | number, isSingleQuote: boolean): StringLiteral | NumericLiteral; - /** If a node is passed, creates a string literal whose source text is read from a source node during emit. */ - export function createLiteral(value: string | StringLiteral | NoSubstitutionTemplateLiteral | NumericLiteral | Identifier): StringLiteral; - export function createLiteral(value: number | PseudoBigInt): NumericLiteral; - export function createLiteral(value: boolean): BooleanLiteral; - export function createLiteral(value: string | number | PseudoBigInt | boolean): PrimaryExpression; - export function createLiteral(value: string | number | PseudoBigInt | boolean | StringLiteral | NoSubstitutionTemplateLiteral | NumericLiteral | Identifier, isSingleQuote?: boolean): PrimaryExpression { - if (typeof value === "number") { - return createNumericLiteral(value + ""); - } - // eslint-disable-next-line no-in-operator - if (typeof value === "object" && "base10Value" in value) { // PseudoBigInt - return createBigIntLiteral(pseudoBigIntToString(value) + "n"); - } - if (typeof value === "boolean") { - return value ? createTrue() : createFalse(); - } - if (isString(value)) { - const res = createStringLiteral(value); - if (isSingleQuote) res.singleQuote = true; - return res; - } - return createLiteralFromNode(value); - } - - export function createNumericLiteral(value: string, numericLiteralFlags: TokenFlags = TokenFlags.None): NumericLiteral { - const node = createSynthesizedNode(SyntaxKind.NumericLiteral); - node.text = value; - node.numericLiteralFlags = numericLiteralFlags; - return node; - } - - export function createBigIntLiteral(value: string): BigIntLiteral { - const node = createSynthesizedNode(SyntaxKind.BigIntLiteral); - node.text = value; - return node; - } - - export function createStringLiteral(text: string): StringLiteral { - const node = createSynthesizedNode(SyntaxKind.StringLiteral); - node.text = text; - return node; - } - - export function createRegularExpressionLiteral(text: string): RegularExpressionLiteral { - const node = createSynthesizedNode(SyntaxKind.RegularExpressionLiteral); - node.text = text; - return node; - } - - function createLiteralFromNode(sourceNode: Exclude): StringLiteral { - const node = createStringLiteral(getTextOfIdentifierOrLiteral(sourceNode)); - node.textSourceNode = sourceNode; - return node; - } - - - // Identifiers - - export function createIdentifier(text: string): Identifier; - /* @internal */ - export function createIdentifier(text: string, typeArguments: readonly (TypeNode | TypeParameterDeclaration)[] | undefined): Identifier; // eslint-disable-line @typescript-eslint/unified-signatures - export function createIdentifier(text: string, typeArguments?: readonly (TypeNode | TypeParameterDeclaration)[]): Identifier { - const node = createSynthesizedNode(SyntaxKind.Identifier); - node.escapedText = escapeLeadingUnderscores(text); - node.originalKeywordKind = text ? stringToToken(text) : SyntaxKind.Unknown; - node.autoGenerateFlags = GeneratedIdentifierFlags.None; - node.autoGenerateId = 0; - if (typeArguments) { - node.typeArguments = createNodeArray(typeArguments as readonly TypeNode[]); - } - return node; - } - - export function updateIdentifier(node: Identifier): Identifier; - /* @internal */ - export function updateIdentifier(node: Identifier, typeArguments: NodeArray | undefined): Identifier; // eslint-disable-line @typescript-eslint/unified-signatures - export function updateIdentifier(node: Identifier, typeArguments?: NodeArray | undefined): Identifier { - return node.typeArguments !== typeArguments - ? updateNode(createIdentifier(idText(node), typeArguments), node) - : node; - } - - let nextAutoGenerateId = 0; - - /** Create a unique temporary variable. */ - export function createTempVariable(recordTempVariable: ((node: Identifier) => void) | undefined): Identifier; - /* @internal */ export function createTempVariable(recordTempVariable: ((node: Identifier) => void) | undefined, reservedInNestedScopes: boolean): GeneratedIdentifier; - export function createTempVariable(recordTempVariable: ((node: Identifier) => void) | undefined, reservedInNestedScopes?: boolean): GeneratedIdentifier { - const name = createIdentifier("") as GeneratedIdentifier; - name.autoGenerateFlags = GeneratedIdentifierFlags.Auto; - name.autoGenerateId = nextAutoGenerateId; - nextAutoGenerateId++; - if (recordTempVariable) { - recordTempVariable(name); - } - if (reservedInNestedScopes) { - name.autoGenerateFlags |= GeneratedIdentifierFlags.ReservedInNestedScopes; - } - return name; - } - - /** Create a unique temporary variable for use in a loop. */ - export function createLoopVariable(): Identifier { - const name = createIdentifier(""); - name.autoGenerateFlags = GeneratedIdentifierFlags.Loop; - name.autoGenerateId = nextAutoGenerateId; - nextAutoGenerateId++; - return name; - } - - /** Create a unique name based on the supplied text. */ - export function createUniqueName(text: string): Identifier { - const name = createIdentifier(text); - name.autoGenerateFlags = GeneratedIdentifierFlags.Unique; - name.autoGenerateId = nextAutoGenerateId; - nextAutoGenerateId++; - return name; - } - - /* @internal */ export function createOptimisticUniqueName(text: string): GeneratedIdentifier; - /** Create a unique name based on the supplied text. */ - export function createOptimisticUniqueName(text: string): Identifier; - export function createOptimisticUniqueName(text: string): GeneratedIdentifier { - const name = createIdentifier(text) as GeneratedIdentifier; - name.autoGenerateFlags = GeneratedIdentifierFlags.Unique | GeneratedIdentifierFlags.Optimistic; - name.autoGenerateId = nextAutoGenerateId; - nextAutoGenerateId++; - return name; - } - - /** Create a unique name based on the supplied text. This does not consider names injected by the transformer. */ - export function createFileLevelUniqueName(text: string): Identifier { - const name = createOptimisticUniqueName(text); - name.autoGenerateFlags |= GeneratedIdentifierFlags.FileLevel; - return name; - } - - /** Create a unique name generated for a node. */ - export function getGeneratedNameForNode(node: Node | undefined): Identifier; - /* @internal */ export function getGeneratedNameForNode(node: Node | undefined, flags: GeneratedIdentifierFlags): Identifier; // eslint-disable-line @typescript-eslint/unified-signatures - export function getGeneratedNameForNode(node: Node | undefined, flags?: GeneratedIdentifierFlags): Identifier { - const name = createIdentifier(node && isIdentifier(node) ? idText(node) : ""); - name.autoGenerateFlags = GeneratedIdentifierFlags.Node | flags!; - name.autoGenerateId = nextAutoGenerateId; - name.original = node; - nextAutoGenerateId++; - return name; - } - - // Private Identifiers - export function createPrivateIdentifier(text: string): PrivateIdentifier { - if (text[0] !== "#") { - Debug.fail("First character of private identifier must be #: " + text); +import { SyntaxKind, Node, createNode, NodeFlags, aggregateTransformFlags, MutableNodeArray, NodeArray, emptyArray, isNodeArray, StringLiteral, NoSubstitutionTemplateLiteral, NumericLiteral, Identifier, PseudoBigInt, BooleanLiteral, PrimaryExpression, pseudoBigIntToString, isString, TokenFlags, BigIntLiteral, RegularExpressionLiteral, PropertyNameLiteral, PrivateIdentifier, getTextOfIdentifierOrLiteral, TypeNode, TypeParameterDeclaration, escapeLeadingUnderscores, stringToToken, GeneratedIdentifierFlags, idText, GeneratedIdentifier, isIdentifier, Debug, Token, SuperExpression, ThisExpression, NullLiteral, Modifier, ModifierFlags, EntityName, QualifiedName, Expression, isCommaSequence, ComputedPropertyName, Decorator, DotDotDotToken, BindingName, QuestionToken, ParameterDeclaration, parenthesizeExpressionForList, parenthesizeForAccess, PropertyName, PropertySignature, ExclamationToken, PropertyDeclaration, MethodSignature, AsteriskToken, Block, MethodDeclaration, Push, PropertyAssignment, PropertyDescriptorAttributes, ConstructorDeclaration, GetAccessorDeclaration, SetAccessorDeclaration, CallSignatureDeclaration, ConstructSignatureDeclaration, IndexSignatureDeclaration, SignatureDeclaration, KeywordTypeNode, ThisTypeNode, AssertsToken, TypePredicateNode, TypeReferenceNode, parenthesizeTypeParameters, FunctionTypeNode, ConstructorTypeNode, TypeQueryNode, TypeElement, TypeLiteralNode, ArrayTypeNode, parenthesizeArrayTypeMember, TupleTypeNode, OptionalTypeNode, RestTypeNode, UnionTypeNode, IntersectionTypeNode, parenthesizeElementTypeMembers, UnionOrIntersectionTypeNode, ConditionalTypeNode, parenthesizeConditionalTypeMember, InferTypeNode, ImportTypeNode, ParenthesizedTypeNode, TypeOperatorNode, parenthesizeElementTypeMember, IndexedAccessTypeNode, ReadonlyToken, PlusToken, MinusToken, MappedTypeNode, LiteralTypeNode, BindingElement, ObjectBindingPattern, ArrayBindingElement, ArrayBindingPattern, ArrayLiteralExpression, parenthesizeListElements, ObjectLiteralElementLike, ObjectLiteralExpression, PropertyAccessExpression, EmitFlags, isOptionalChain, getEmitFlags, QuestionDotToken, PropertyAccessChain, ElementAccessExpression, ElementAccessChain, CallExpression, CallChain, NewExpression, parenthesizeForNew, TemplateLiteral, TaggedTemplateExpression, TypeAssertion, parenthesizePrefixOperand, ParenthesizedExpression, FunctionExpression, EqualsGreaterThanToken, ConciseBody, ArrowFunction, parenthesizeConciseBody, DeleteExpression, TypeOfExpression, VoidExpression, AwaitExpression, PrefixUnaryOperator, PrefixUnaryExpression, PostfixUnaryOperator, PostfixUnaryExpression, parenthesizePostfixOperand, BinaryOperator, BinaryOperatorToken, BinaryExpression, parenthesizeBinaryOperand, ConditionalExpression, ColonToken, parenthesizeForConditionalHead, parenthesizeSubexpressionOfConditionalExpression, TemplateHead, TemplateSpan, TemplateExpression, Scanner, TemplateLiteralToken, createScanner, ScriptTarget, LanguageVariant, TemplateLiteralLikeNode, TemplateMiddle, TemplateTail, YieldExpression, SpreadElement, HeritageClause, ClassElement, ClassExpression, OmittedExpression, ExpressionWithTypeArguments, AsExpression, NonNullExpression, MetaProperty, SemicolonClassElement, Statement, VariableDeclarationList, VariableDeclaration, VariableStatement, isArray, EmptyStatement, ExpressionStatement, parenthesizeExpressionForExpressionStatement, IfStatement, DoStatement, WhileStatement, ForInitializer, ForStatement, ForInStatement, AwaitKeywordToken, ForOfStatement, ContinueStatement, BreakStatement, ReturnStatement, WithStatement, CaseBlock, SwitchStatement, LabeledStatement, ThrowStatement, CatchClause, TryStatement, DebuggerStatement, FunctionDeclaration, ClassDeclaration, InterfaceDeclaration, TypeAliasDeclaration, EnumMember, EnumDeclaration, ModuleName, ModuleBody, ModuleDeclaration, ModuleBlock, CaseOrDefaultClause, NamespaceExportDeclaration, ModuleReference, ImportEqualsDeclaration, ImportClause, ImportDeclaration, NamedImportBindings, NamespaceImport, NamespaceExport, ImportSpecifier, NamedImports, ExportAssignment, parenthesizeDefaultExpression, NamedExportBindings, ExportDeclaration, ExportSpecifier, NamedExports, ExternalModuleReference, JSDocTypeExpression, JSDocTypeTag, JSDocReturnTag, JSDocThisTag, JSDocParameterTag, JSDocTag, JSDoc, JsxOpeningElement, JsxChild, JsxClosingElement, JsxElement, JsxTagNameExpression, JsxAttributes, JsxSelfClosingElement, JsxOpeningFragment, JsxClosingFragment, JsxFragment, JsxText, JsxExpression, JsxAttribute, JsxAttributeLike, JsxSpreadAttribute, CaseClause, DefaultClause, ShorthandPropertyAssignment, SpreadAssignment, SourceFile, NotEmittedStatement, EndOfDeclarationMarker, EmitNode, MergeDeclarationMarker, PartiallyEmittedExpression, nodeIsSynthesized, isParseTreeNode, CommaListExpression, isBinaryExpression, sameFlatMap, SyntheticReferenceExpression, UnparsedSource, InputFiles, Bundle, UnscopedEmitHelper, arrayToMap, valuesHelper, readHelper, spreadHelper, spreadArraysHelper, restHelper, decorateHelper, metadataHelper, paramHelper, awaiterHelper, assignHelper, awaitHelper, asyncGeneratorHelper, asyncDelegator, asyncValues, extendsHelper, templateObjectHelper, generatorHelper, importStarHelper, importDefaultHelper, classPrivateFieldGetHelper, classPrivateFieldSetHelper, createBindingHelper, setModuleDefaultHelper, getLineAndCharacterOfPosition, BundleFileInfo, UnparsedPrologue, FileReference, UnparsedSourceText, BundleFileSectionKind, UnparsedPrepend, UnparsedTextLike, UnparsedSyntheticReference, map, BundleFileSection, UnparsedNode, BundleFileHasNoDefaultLib, BundleFileReference, BuildInfo, createMap, getBuildInfo, DestructuringAssignment, isNotEmittedStatement, getSourceFileOfNode, getParseTreeNode, TextRange, SourceMapRange, objectAllocator, SynthesizedComment, append, EmitHelper, some, appendIfUnique, orderedRemoveItem, Comparison, compareValues, addRange } from "./ts"; +import * as ts from "./ts"; +function createSynthesizedNode(kind: SyntaxKind): Node { + const node = createNode(kind, -1, -1); + node.flags |= NodeFlags.Synthesized; + return node; +} +/* @internal */ +export function updateNode(updated: T, original: T): T { + if (updated !== original) { + setOriginalNode(updated, original); + setTextRange(updated, original); + aggregateTransformFlags(updated); + } + return updated; +} +/* @internal */ export function createNodeArray(elements?: T[], hasTrailingComma?: boolean): MutableNodeArray; +export function createNodeArray(elements?: readonly T[], hasTrailingComma?: boolean): NodeArray; +/** + * Make `elements` into a `NodeArray`. If `elements` is `undefined`, returns an empty `NodeArray`. + */ +export function createNodeArray(elements?: readonly T[], hasTrailingComma?: boolean): NodeArray { + if (!elements || elements === emptyArray) { + elements = []; + } + else if (isNodeArray(elements)) { + return elements; + } + const array = (>elements); + array.pos = -1; + array.end = -1; + array.hasTrailingComma = hasTrailingComma; + return array; +} +/** + * Creates a shallow, memberwise clone of a node with no source map location. + */ +/* @internal */ +export function getSynthesizedClone(node: T): T { + // We don't use "clone" from core.ts here, as we need to preserve the prototype chain of + // the original node. We also need to exclude specific properties and only include own- + // properties (to skip members already defined on the shared prototype). + if (node === undefined) { + return node; + } + const clone = createSynthesizedNode(node.kind); + clone.flags |= node.flags; + setOriginalNode(clone, node); + for (const key in node) { + if (clone.hasOwnProperty(key) || !node.hasOwnProperty(key)) { + continue; } - const node = createSynthesizedNode(SyntaxKind.PrivateIdentifier) as PrivateIdentifier; - node.escapedText = escapeLeadingUnderscores(text); - return node; - } - - // Punctuation - - export function createToken(token: TKind) { - return >createSynthesizedNode(token); - } - - // Reserved words - - export function createSuper() { - return createSynthesizedNode(SyntaxKind.SuperKeyword); - } - - export function createThis() { - return >createSynthesizedNode(SyntaxKind.ThisKeyword); - } - - export function createNull() { - return >createSynthesizedNode(SyntaxKind.NullKeyword); - } - - export function createTrue() { - return >createSynthesizedNode(SyntaxKind.TrueKeyword); - } - - export function createFalse() { - return >createSynthesizedNode(SyntaxKind.FalseKeyword); + (clone)[key] = (node)[key]; } - - // Modifiers - - export function createModifier(kind: T): Token { - return createToken(kind); - } - - export function createModifiersFromModifierFlags(flags: ModifierFlags) { - const result: Modifier[] = []; - if (flags & ModifierFlags.Export) { result.push(createModifier(SyntaxKind.ExportKeyword)); } - if (flags & ModifierFlags.Ambient) { result.push(createModifier(SyntaxKind.DeclareKeyword)); } - if (flags & ModifierFlags.Default) { result.push(createModifier(SyntaxKind.DefaultKeyword)); } - if (flags & ModifierFlags.Const) { result.push(createModifier(SyntaxKind.ConstKeyword)); } - if (flags & ModifierFlags.Public) { result.push(createModifier(SyntaxKind.PublicKeyword)); } - if (flags & ModifierFlags.Private) { result.push(createModifier(SyntaxKind.PrivateKeyword)); } - if (flags & ModifierFlags.Protected) { result.push(createModifier(SyntaxKind.ProtectedKeyword)); } - if (flags & ModifierFlags.Abstract) { result.push(createModifier(SyntaxKind.AbstractKeyword)); } - if (flags & ModifierFlags.Static) { result.push(createModifier(SyntaxKind.StaticKeyword)); } - if (flags & ModifierFlags.Readonly) { result.push(createModifier(SyntaxKind.ReadonlyKeyword)); } - if (flags & ModifierFlags.Async) { result.push(createModifier(SyntaxKind.AsyncKeyword)); } - return result; - } - - // Names - - export function createQualifiedName(left: EntityName, right: string | Identifier) { - const node = createSynthesizedNode(SyntaxKind.QualifiedName); - node.left = left; - node.right = asName(right); - return node; - } - - export function updateQualifiedName(node: QualifiedName, left: EntityName, right: Identifier) { - return node.left !== left - || node.right !== right - ? updateNode(createQualifiedName(left, right), node) - : node; - } - - function parenthesizeForComputedName(expression: Expression): Expression { - return isCommaSequence(expression) - ? createParen(expression) - : expression; - } - - export function createComputedPropertyName(expression: Expression) { - const node = createSynthesizedNode(SyntaxKind.ComputedPropertyName); - node.expression = parenthesizeForComputedName(expression); - return node; - } - - export function updateComputedPropertyName(node: ComputedPropertyName, expression: Expression) { - return node.expression !== expression - ? updateNode(createComputedPropertyName(expression), node) - : node; - } - - // Signature elements - - export function createTypeParameterDeclaration(name: string | Identifier, constraint?: TypeNode, defaultType?: TypeNode) { - const node = createSynthesizedNode(SyntaxKind.TypeParameter) as TypeParameterDeclaration; - node.name = asName(name); - node.constraint = constraint; - node.default = defaultType; - return node; - } - - export function updateTypeParameterDeclaration(node: TypeParameterDeclaration, name: Identifier, constraint: TypeNode | undefined, defaultType: TypeNode | undefined) { - return node.name !== name - || node.constraint !== constraint - || node.default !== defaultType - ? updateNode(createTypeParameterDeclaration(name, constraint, defaultType), node) - : node; - } - - export function createParameter( - decorators: readonly Decorator[] | undefined, - modifiers: readonly Modifier[] | undefined, - dotDotDotToken: DotDotDotToken | undefined, - name: string | BindingName, - questionToken?: QuestionToken, - type?: TypeNode, - initializer?: Expression) { - const node = createSynthesizedNode(SyntaxKind.Parameter); - node.decorators = asNodeArray(decorators); - node.modifiers = asNodeArray(modifiers); - node.dotDotDotToken = dotDotDotToken; - node.name = asName(name); - node.questionToken = questionToken; - node.type = type; - node.initializer = initializer ? parenthesizeExpressionForList(initializer) : undefined; - return node; - } - - export function updateParameter( - node: ParameterDeclaration, - decorators: readonly Decorator[] | undefined, - modifiers: readonly Modifier[] | undefined, - dotDotDotToken: DotDotDotToken | undefined, - name: string | BindingName, - questionToken: QuestionToken | undefined, - type: TypeNode | undefined, - initializer: Expression | undefined) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.dotDotDotToken !== dotDotDotToken - || node.name !== name - || node.questionToken !== questionToken - || node.type !== type - || node.initializer !== initializer - ? updateNode(createParameter(decorators, modifiers, dotDotDotToken, name, questionToken, type, initializer), node) - : node; + return clone; +} +// Literals +/* @internal */ export function createLiteral(value: string | StringLiteral | NoSubstitutionTemplateLiteral | NumericLiteral | Identifier, isSingleQuote: boolean): StringLiteral; // eslint-disable-line @typescript-eslint/unified-signatures +/* @internal */ export function createLiteral(value: string | number, isSingleQuote: boolean): StringLiteral | NumericLiteral; +/** If a node is passed, creates a string literal whose source text is read from a source node during emit. */ +export function createLiteral(value: string | StringLiteral | NoSubstitutionTemplateLiteral | NumericLiteral | Identifier): StringLiteral; +export function createLiteral(value: number | PseudoBigInt): NumericLiteral; +export function createLiteral(value: boolean): BooleanLiteral; +export function createLiteral(value: string | number | PseudoBigInt | boolean): PrimaryExpression; +export function createLiteral(value: string | number | PseudoBigInt | boolean | StringLiteral | NoSubstitutionTemplateLiteral | NumericLiteral | Identifier, isSingleQuote?: boolean): PrimaryExpression { + if (typeof value === "number") { + return createNumericLiteral(value + ""); + } + // eslint-disable-next-line no-in-operator + if (typeof value === "object" && "base10Value" in value) { // PseudoBigInt + return createBigIntLiteral(pseudoBigIntToString(value) + "n"); + } + if (typeof value === "boolean") { + return value ? createTrue() : createFalse(); + } + if (isString(value)) { + const res = createStringLiteral(value); + if (isSingleQuote) + res.singleQuote = true; + return res; + } + return createLiteralFromNode(value); +} +export function createNumericLiteral(value: string, numericLiteralFlags: TokenFlags = TokenFlags.None): NumericLiteral { + const node = (createSynthesizedNode(SyntaxKind.NumericLiteral)); + node.text = value; + node.numericLiteralFlags = numericLiteralFlags; + return node; +} +export function createBigIntLiteral(value: string): BigIntLiteral { + const node = (createSynthesizedNode(SyntaxKind.BigIntLiteral)); + node.text = value; + return node; +} +export function createStringLiteral(text: string): StringLiteral { + const node = (createSynthesizedNode(SyntaxKind.StringLiteral)); + node.text = text; + return node; +} +export function createRegularExpressionLiteral(text: string): RegularExpressionLiteral { + const node = (createSynthesizedNode(SyntaxKind.RegularExpressionLiteral)); + node.text = text; + return node; +} +function createLiteralFromNode(sourceNode: Exclude): StringLiteral { + const node = createStringLiteral(getTextOfIdentifierOrLiteral(sourceNode)); + node.textSourceNode = sourceNode; + return node; +} +// Identifiers +export function createIdentifier(text: string): Identifier; +/* @internal */ +export function createIdentifier(text: string, typeArguments: readonly (TypeNode | TypeParameterDeclaration)[] | undefined): Identifier; // eslint-disable-line @typescript-eslint/unified-signatures +export function createIdentifier(text: string, typeArguments?: readonly (TypeNode | TypeParameterDeclaration)[]): Identifier { + const node = (createSynthesizedNode(SyntaxKind.Identifier)); + node.escapedText = escapeLeadingUnderscores(text); + node.originalKeywordKind = text ? stringToToken(text) : SyntaxKind.Unknown; + node.autoGenerateFlags = GeneratedIdentifierFlags.None; + node.autoGenerateId = 0; + if (typeArguments) { + node.typeArguments = createNodeArray((typeArguments as readonly TypeNode[])); + } + return node; +} +export function updateIdentifier(node: Identifier): Identifier; +/* @internal */ +export function updateIdentifier(node: Identifier, typeArguments: NodeArray | undefined): Identifier; // eslint-disable-line @typescript-eslint/unified-signatures +export function updateIdentifier(node: Identifier, typeArguments?: NodeArray | undefined): Identifier { + return node.typeArguments !== typeArguments + ? updateNode(createIdentifier(idText(node), typeArguments), node) + : node; +} +let nextAutoGenerateId = 0; +/** Create a unique temporary variable. */ +export function createTempVariable(recordTempVariable: ((node: Identifier) => void) | undefined): Identifier; +/* @internal */ export function createTempVariable(recordTempVariable: ((node: Identifier) => void) | undefined, reservedInNestedScopes: boolean): GeneratedIdentifier; +export function createTempVariable(recordTempVariable: ((node: Identifier) => void) | undefined, reservedInNestedScopes?: boolean): GeneratedIdentifier { + const name = (createIdentifier("") as GeneratedIdentifier); + name.autoGenerateFlags = GeneratedIdentifierFlags.Auto; + name.autoGenerateId = nextAutoGenerateId; + nextAutoGenerateId++; + if (recordTempVariable) { + recordTempVariable(name); + } + if (reservedInNestedScopes) { + name.autoGenerateFlags |= GeneratedIdentifierFlags.ReservedInNestedScopes; + } + return name; +} +/** Create a unique temporary variable for use in a loop. */ +export function createLoopVariable(): Identifier { + const name = createIdentifier(""); + name.autoGenerateFlags = GeneratedIdentifierFlags.Loop; + name.autoGenerateId = nextAutoGenerateId; + nextAutoGenerateId++; + return name; +} +/** Create a unique name based on the supplied text. */ +export function createUniqueName(text: string): Identifier { + const name = createIdentifier(text); + name.autoGenerateFlags = GeneratedIdentifierFlags.Unique; + name.autoGenerateId = nextAutoGenerateId; + nextAutoGenerateId++; + return name; +} +/* @internal */ export function createOptimisticUniqueName(text: string): GeneratedIdentifier; +/** Create a unique name based on the supplied text. */ +export function createOptimisticUniqueName(text: string): Identifier; +export function createOptimisticUniqueName(text: string): GeneratedIdentifier { + const name = (createIdentifier(text) as GeneratedIdentifier); + name.autoGenerateFlags = GeneratedIdentifierFlags.Unique | GeneratedIdentifierFlags.Optimistic; + name.autoGenerateId = nextAutoGenerateId; + nextAutoGenerateId++; + return name; +} +/** Create a unique name based on the supplied text. This does not consider names injected by the transformer. */ +export function createFileLevelUniqueName(text: string): Identifier { + const name = createOptimisticUniqueName(text); + name.autoGenerateFlags |= GeneratedIdentifierFlags.FileLevel; + return name; +} +/** Create a unique name generated for a node. */ +export function getGeneratedNameForNode(node: Node | undefined): Identifier; +/* @internal */ export function getGeneratedNameForNode(node: Node | undefined, flags: GeneratedIdentifierFlags): Identifier; // eslint-disable-line @typescript-eslint/unified-signatures +export function getGeneratedNameForNode(node: Node | undefined, flags?: GeneratedIdentifierFlags): Identifier { + const name = createIdentifier(node && isIdentifier(node) ? idText(node) : ""); + name.autoGenerateFlags = GeneratedIdentifierFlags.Node | (flags!); + name.autoGenerateId = nextAutoGenerateId; + name.original = node; + nextAutoGenerateId++; + return name; +} +// Private Identifiers +export function createPrivateIdentifier(text: string): PrivateIdentifier { + if (text[0] !== "#") { + Debug.fail("First character of private identifier must be #: " + text); + } + const node = (createSynthesizedNode(SyntaxKind.PrivateIdentifier) as PrivateIdentifier); + node.escapedText = escapeLeadingUnderscores(text); + return node; +} +// Punctuation +export function createToken(token: TKind) { + return >createSynthesizedNode(token); +} +// Reserved words +export function createSuper() { + return createSynthesizedNode(SyntaxKind.SuperKeyword); +} +export function createThis() { + return >createSynthesizedNode(SyntaxKind.ThisKeyword); +} +export function createNull() { + return >createSynthesizedNode(SyntaxKind.NullKeyword); +} +export function createTrue() { + return >createSynthesizedNode(SyntaxKind.TrueKeyword); +} +export function createFalse() { + return >createSynthesizedNode(SyntaxKind.FalseKeyword); +} +// Modifiers +export function createModifier(kind: T): Token { + return createToken(kind); +} +export function createModifiersFromModifierFlags(flags: ModifierFlags) { + const result: Modifier[] = []; + if (flags & ModifierFlags.Export) { + result.push(createModifier(SyntaxKind.ExportKeyword)); } - - export function createDecorator(expression: Expression) { - const node = createSynthesizedNode(SyntaxKind.Decorator); - node.expression = parenthesizeForAccess(expression); - return node; + if (flags & ModifierFlags.Ambient) { + result.push(createModifier(SyntaxKind.DeclareKeyword)); } - - export function updateDecorator(node: Decorator, expression: Expression) { - return node.expression !== expression - ? updateNode(createDecorator(expression), node) - : node; + if (flags & ModifierFlags.Default) { + result.push(createModifier(SyntaxKind.DefaultKeyword)); } - - - // Type Elements - - export function createPropertySignature( - modifiers: readonly Modifier[] | undefined, - name: PropertyName | string, - questionToken: QuestionToken | undefined, - type: TypeNode | undefined, - initializer: Expression | undefined): PropertySignature { - const node = createSynthesizedNode(SyntaxKind.PropertySignature) as PropertySignature; - node.modifiers = asNodeArray(modifiers); - node.name = asName(name); - node.questionToken = questionToken; - node.type = type; - node.initializer = initializer; - return node; + if (flags & ModifierFlags.Const) { + result.push(createModifier(SyntaxKind.ConstKeyword)); } - - export function updatePropertySignature( - node: PropertySignature, - modifiers: readonly Modifier[] | undefined, - name: PropertyName, - questionToken: QuestionToken | undefined, - type: TypeNode | undefined, - initializer: Expression | undefined) { - return node.modifiers !== modifiers - || node.name !== name - || node.questionToken !== questionToken - || node.type !== type - || node.initializer !== initializer - ? updateNode(createPropertySignature(modifiers, name, questionToken, type, initializer), node) - : node; + if (flags & ModifierFlags.Public) { + result.push(createModifier(SyntaxKind.PublicKeyword)); } - - export function createProperty( - decorators: readonly Decorator[] | undefined, - modifiers: readonly Modifier[] | undefined, - name: string | PropertyName, - questionOrExclamationToken: QuestionToken | ExclamationToken | undefined, - type: TypeNode | undefined, - initializer: Expression | undefined) { - const node = createSynthesizedNode(SyntaxKind.PropertyDeclaration); - node.decorators = asNodeArray(decorators); - node.modifiers = asNodeArray(modifiers); - node.name = asName(name); - node.questionToken = questionOrExclamationToken !== undefined && questionOrExclamationToken.kind === SyntaxKind.QuestionToken ? questionOrExclamationToken : undefined; - node.exclamationToken = questionOrExclamationToken !== undefined && questionOrExclamationToken.kind === SyntaxKind.ExclamationToken ? questionOrExclamationToken : undefined; - node.type = type; - node.initializer = initializer; - return node; + if (flags & ModifierFlags.Private) { + result.push(createModifier(SyntaxKind.PrivateKeyword)); } - - export function updateProperty( - node: PropertyDeclaration, - decorators: readonly Decorator[] | undefined, - modifiers: readonly Modifier[] | undefined, - name: string | PropertyName, - questionOrExclamationToken: QuestionToken | ExclamationToken | undefined, - type: TypeNode | undefined, - initializer: Expression | undefined) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.name !== name - || node.questionToken !== (questionOrExclamationToken !== undefined && questionOrExclamationToken.kind === SyntaxKind.QuestionToken ? questionOrExclamationToken : undefined) - || node.exclamationToken !== (questionOrExclamationToken !== undefined && questionOrExclamationToken.kind === SyntaxKind.ExclamationToken ? questionOrExclamationToken : undefined) - || node.type !== type - || node.initializer !== initializer - ? updateNode(createProperty(decorators, modifiers, name, questionOrExclamationToken, type, initializer), node) - : node; + if (flags & ModifierFlags.Protected) { + result.push(createModifier(SyntaxKind.ProtectedKeyword)); } - - export function createMethodSignature( - typeParameters: readonly TypeParameterDeclaration[] | undefined, - parameters: readonly ParameterDeclaration[], - type: TypeNode | undefined, - name: string | PropertyName, - questionToken: QuestionToken | undefined) { - const node = createSignatureDeclaration(SyntaxKind.MethodSignature, typeParameters, parameters, type) as MethodSignature; - node.name = asName(name); - node.questionToken = questionToken; - return node; + if (flags & ModifierFlags.Abstract) { + result.push(createModifier(SyntaxKind.AbstractKeyword)); } - - export function updateMethodSignature(node: MethodSignature, typeParameters: NodeArray | undefined, parameters: NodeArray, type: TypeNode | undefined, name: PropertyName, questionToken: QuestionToken | undefined) { - return node.typeParameters !== typeParameters - || node.parameters !== parameters - || node.type !== type - || node.name !== name - || node.questionToken !== questionToken - ? updateNode(createMethodSignature(typeParameters, parameters, type, name, questionToken), node) - : node; + if (flags & ModifierFlags.Static) { + result.push(createModifier(SyntaxKind.StaticKeyword)); } - - export function createMethod( - decorators: readonly Decorator[] | undefined, - modifiers: readonly Modifier[] | undefined, - asteriskToken: AsteriskToken | undefined, - name: string | PropertyName, - questionToken: QuestionToken | undefined, - typeParameters: readonly TypeParameterDeclaration[] | undefined, - parameters: readonly ParameterDeclaration[], - type: TypeNode | undefined, - body: Block | undefined) { - const node = createSynthesizedNode(SyntaxKind.MethodDeclaration); - node.decorators = asNodeArray(decorators); - node.modifiers = asNodeArray(modifiers); - node.asteriskToken = asteriskToken; - node.name = asName(name); - node.questionToken = questionToken; - node.typeParameters = asNodeArray(typeParameters); - node.parameters = createNodeArray(parameters); - node.type = type; - node.body = body; - return node; + if (flags & ModifierFlags.Readonly) { + result.push(createModifier(SyntaxKind.ReadonlyKeyword)); } - - function createMethodCall(object: Expression, methodName: string | Identifier, argumentsList: readonly Expression[]) { - return createCall( - createPropertyAccess(object, asName(methodName)), - /*typeArguments*/ undefined, - argumentsList - ); + if (flags & ModifierFlags.Async) { + result.push(createModifier(SyntaxKind.AsyncKeyword)); } - - function createGlobalMethodCall(globalObjectName: string, methodName: string, argumentsList: readonly Expression[]) { - return createMethodCall(createIdentifier(globalObjectName), methodName, argumentsList); + return result; +} +// Names +export function createQualifiedName(left: EntityName, right: string | Identifier) { + const node = (createSynthesizedNode(SyntaxKind.QualifiedName)); + node.left = left; + node.right = asName(right); + return node; +} +export function updateQualifiedName(node: QualifiedName, left: EntityName, right: Identifier) { + return node.left !== left + || node.right !== right + ? updateNode(createQualifiedName(left, right), node) + : node; +} +function parenthesizeForComputedName(expression: Expression): Expression { + return isCommaSequence(expression) + ? createParen(expression) + : expression; +} +export function createComputedPropertyName(expression: Expression) { + const node = (createSynthesizedNode(SyntaxKind.ComputedPropertyName)); + node.expression = parenthesizeForComputedName(expression); + return node; +} +export function updateComputedPropertyName(node: ComputedPropertyName, expression: Expression) { + return node.expression !== expression + ? updateNode(createComputedPropertyName(expression), node) + : node; +} +// Signature elements +export function createTypeParameterDeclaration(name: string | Identifier, constraint?: TypeNode, defaultType?: TypeNode) { + const node = (createSynthesizedNode(SyntaxKind.TypeParameter) as TypeParameterDeclaration); + node.name = asName(name); + node.constraint = constraint; + node.default = defaultType; + return node; +} +export function updateTypeParameterDeclaration(node: TypeParameterDeclaration, name: Identifier, constraint: TypeNode | undefined, defaultType: TypeNode | undefined) { + return node.name !== name + || node.constraint !== constraint + || node.default !== defaultType + ? updateNode(createTypeParameterDeclaration(name, constraint, defaultType), node) + : node; +} +export function createParameter(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, dotDotDotToken: DotDotDotToken | undefined, name: string | BindingName, questionToken?: QuestionToken, type?: TypeNode, initializer?: Expression) { + const node = (createSynthesizedNode(SyntaxKind.Parameter)); + node.decorators = asNodeArray(decorators); + node.modifiers = asNodeArray(modifiers); + node.dotDotDotToken = dotDotDotToken; + node.name = asName(name); + node.questionToken = questionToken; + node.type = type; + node.initializer = initializer ? parenthesizeExpressionForList(initializer) : undefined; + return node; +} +export function updateParameter(node: ParameterDeclaration, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, dotDotDotToken: DotDotDotToken | undefined, name: string | BindingName, questionToken: QuestionToken | undefined, type: TypeNode | undefined, initializer: Expression | undefined) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.dotDotDotToken !== dotDotDotToken + || node.name !== name + || node.questionToken !== questionToken + || node.type !== type + || node.initializer !== initializer + ? updateNode(createParameter(decorators, modifiers, dotDotDotToken, name, questionToken, type, initializer), node) + : node; +} +export function createDecorator(expression: Expression) { + const node = (createSynthesizedNode(SyntaxKind.Decorator)); + node.expression = parenthesizeForAccess(expression); + return node; +} +export function updateDecorator(node: Decorator, expression: Expression) { + return node.expression !== expression + ? updateNode(createDecorator(expression), node) + : node; +} +// Type Elements +export function createPropertySignature(modifiers: readonly Modifier[] | undefined, name: PropertyName | string, questionToken: QuestionToken | undefined, type: TypeNode | undefined, initializer: Expression | undefined): PropertySignature { + const node = (createSynthesizedNode(SyntaxKind.PropertySignature) as PropertySignature); + node.modifiers = asNodeArray(modifiers); + node.name = asName(name); + node.questionToken = questionToken; + node.type = type; + node.initializer = initializer; + return node; +} +export function updatePropertySignature(node: PropertySignature, modifiers: readonly Modifier[] | undefined, name: PropertyName, questionToken: QuestionToken | undefined, type: TypeNode | undefined, initializer: Expression | undefined) { + return node.modifiers !== modifiers + || node.name !== name + || node.questionToken !== questionToken + || node.type !== type + || node.initializer !== initializer + ? updateNode(createPropertySignature(modifiers, name, questionToken, type, initializer), node) + : node; +} +export function createProperty(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, name: string | PropertyName, questionOrExclamationToken: QuestionToken | ExclamationToken | undefined, type: TypeNode | undefined, initializer: Expression | undefined) { + const node = (createSynthesizedNode(SyntaxKind.PropertyDeclaration)); + node.decorators = asNodeArray(decorators); + node.modifiers = asNodeArray(modifiers); + node.name = asName(name); + node.questionToken = questionOrExclamationToken !== undefined && questionOrExclamationToken.kind === SyntaxKind.QuestionToken ? questionOrExclamationToken : undefined; + node.exclamationToken = questionOrExclamationToken !== undefined && questionOrExclamationToken.kind === SyntaxKind.ExclamationToken ? questionOrExclamationToken : undefined; + node.type = type; + node.initializer = initializer; + return node; +} +export function updateProperty(node: PropertyDeclaration, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, name: string | PropertyName, questionOrExclamationToken: QuestionToken | ExclamationToken | undefined, type: TypeNode | undefined, initializer: Expression | undefined) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.name !== name + || node.questionToken !== (questionOrExclamationToken !== undefined && questionOrExclamationToken.kind === SyntaxKind.QuestionToken ? questionOrExclamationToken : undefined) + || node.exclamationToken !== (questionOrExclamationToken !== undefined && questionOrExclamationToken.kind === SyntaxKind.ExclamationToken ? questionOrExclamationToken : undefined) + || node.type !== type + || node.initializer !== initializer + ? updateNode(createProperty(decorators, modifiers, name, questionOrExclamationToken, type, initializer), node) + : node; +} +export function createMethodSignature(typeParameters: readonly TypeParameterDeclaration[] | undefined, parameters: readonly ParameterDeclaration[], type: TypeNode | undefined, name: string | PropertyName, questionToken: QuestionToken | undefined) { + const node = (createSignatureDeclaration(SyntaxKind.MethodSignature, typeParameters, parameters, type) as MethodSignature); + node.name = asName(name); + node.questionToken = questionToken; + return node; +} +export function updateMethodSignature(node: MethodSignature, typeParameters: NodeArray | undefined, parameters: NodeArray, type: TypeNode | undefined, name: PropertyName, questionToken: QuestionToken | undefined) { + return node.typeParameters !== typeParameters + || node.parameters !== parameters + || node.type !== type + || node.name !== name + || node.questionToken !== questionToken + ? updateNode(createMethodSignature(typeParameters, parameters, type, name, questionToken), node) + : node; +} +export function createMethod(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, asteriskToken: AsteriskToken | undefined, name: string | PropertyName, questionToken: QuestionToken | undefined, typeParameters: readonly TypeParameterDeclaration[] | undefined, parameters: readonly ParameterDeclaration[], type: TypeNode | undefined, body: Block | undefined) { + const node = (createSynthesizedNode(SyntaxKind.MethodDeclaration)); + node.decorators = asNodeArray(decorators); + node.modifiers = asNodeArray(modifiers); + node.asteriskToken = asteriskToken; + node.name = asName(name); + node.questionToken = questionToken; + node.typeParameters = asNodeArray(typeParameters); + node.parameters = createNodeArray(parameters); + node.type = type; + node.body = body; + return node; +} +function createMethodCall(object: Expression, methodName: string | Identifier, argumentsList: readonly Expression[]) { + return createCall(createPropertyAccess(object, asName(methodName)), + /*typeArguments*/ undefined, argumentsList); +} +function createGlobalMethodCall(globalObjectName: string, methodName: string, argumentsList: readonly Expression[]) { + return createMethodCall(createIdentifier(globalObjectName), methodName, argumentsList); +} +/* @internal */ +export function createObjectDefinePropertyCall(target: Expression, propertyName: string | Expression, attributes: Expression) { + return createGlobalMethodCall("Object", "defineProperty", [target, asExpression(propertyName), attributes]); +} +function tryAddPropertyAssignment(properties: Push, propertyName: string, expression: Expression | undefined) { + if (expression) { + properties.push(createPropertyAssignment(propertyName, expression)); + return true; } - - /* @internal */ - export function createObjectDefinePropertyCall(target: Expression, propertyName: string | Expression, attributes: Expression) { - return createGlobalMethodCall("Object", "defineProperty", [target, asExpression(propertyName), attributes]); + return false; +} +/* @internal */ +export function createPropertyDescriptor(attributes: PropertyDescriptorAttributes, singleLine?: boolean) { + const properties: PropertyAssignment[] = []; + tryAddPropertyAssignment(properties, "enumerable", asExpression(attributes.enumerable)); + tryAddPropertyAssignment(properties, "configurable", asExpression(attributes.configurable)); + let isData = tryAddPropertyAssignment(properties, "writable", asExpression(attributes.writable)); + isData = tryAddPropertyAssignment(properties, "value", attributes.value) || isData; + let isAccessor = tryAddPropertyAssignment(properties, "get", attributes.get); + isAccessor = tryAddPropertyAssignment(properties, "set", attributes.set) || isAccessor; + Debug.assert(!(isData && isAccessor), "A PropertyDescriptor may not be both an accessor descriptor and a data descriptor."); + return createObjectLiteral(properties, !singleLine); +} +export function updateMethod(node: MethodDeclaration, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, asteriskToken: AsteriskToken | undefined, name: PropertyName, questionToken: QuestionToken | undefined, typeParameters: readonly TypeParameterDeclaration[] | undefined, parameters: readonly ParameterDeclaration[], type: TypeNode | undefined, body: Block | undefined) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.asteriskToken !== asteriskToken + || node.name !== name + || node.questionToken !== questionToken + || node.typeParameters !== typeParameters + || node.parameters !== parameters + || node.type !== type + || node.body !== body + ? updateNode(createMethod(decorators, modifiers, asteriskToken, name, questionToken, typeParameters, parameters, type, body), node) + : node; +} +export function createConstructor(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, parameters: readonly ParameterDeclaration[], body: Block | undefined) { + const node = (createSynthesizedNode(SyntaxKind.Constructor)); + node.decorators = asNodeArray(decorators); + node.modifiers = asNodeArray(modifiers); + node.typeParameters = undefined; + node.parameters = createNodeArray(parameters); + node.type = undefined; + node.body = body; + return node; +} +export function updateConstructor(node: ConstructorDeclaration, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, parameters: readonly ParameterDeclaration[], body: Block | undefined) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.parameters !== parameters + || node.body !== body + ? updateNode(createConstructor(decorators, modifiers, parameters, body), node) + : node; +} +export function createGetAccessor(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, name: string | PropertyName, parameters: readonly ParameterDeclaration[], type: TypeNode | undefined, body: Block | undefined) { + const node = (createSynthesizedNode(SyntaxKind.GetAccessor)); + node.decorators = asNodeArray(decorators); + node.modifiers = asNodeArray(modifiers); + node.name = asName(name); + node.typeParameters = undefined; + node.parameters = createNodeArray(parameters); + node.type = type; + node.body = body; + return node; +} +export function updateGetAccessor(node: GetAccessorDeclaration, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, name: PropertyName, parameters: readonly ParameterDeclaration[], type: TypeNode | undefined, body: Block | undefined) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.name !== name + || node.parameters !== parameters + || node.type !== type + || node.body !== body + ? updateNode(createGetAccessor(decorators, modifiers, name, parameters, type, body), node) + : node; +} +export function createSetAccessor(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, name: string | PropertyName, parameters: readonly ParameterDeclaration[], body: Block | undefined) { + const node = (createSynthesizedNode(SyntaxKind.SetAccessor)); + node.decorators = asNodeArray(decorators); + node.modifiers = asNodeArray(modifiers); + node.name = asName(name); + node.typeParameters = undefined; + node.parameters = createNodeArray(parameters); + node.body = body; + return node; +} +export function updateSetAccessor(node: SetAccessorDeclaration, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, name: PropertyName, parameters: readonly ParameterDeclaration[], body: Block | undefined) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.name !== name + || node.parameters !== parameters + || node.body !== body + ? updateNode(createSetAccessor(decorators, modifiers, name, parameters, body), node) + : node; +} +export function createCallSignature(typeParameters: readonly TypeParameterDeclaration[] | undefined, parameters: readonly ParameterDeclaration[], type: TypeNode | undefined) { + return createSignatureDeclaration(SyntaxKind.CallSignature, typeParameters, parameters, type) as CallSignatureDeclaration; +} +export function updateCallSignature(node: CallSignatureDeclaration, typeParameters: NodeArray | undefined, parameters: NodeArray, type: TypeNode | undefined) { + return updateSignatureDeclaration(node, typeParameters, parameters, type); +} +export function createConstructSignature(typeParameters: readonly TypeParameterDeclaration[] | undefined, parameters: readonly ParameterDeclaration[], type: TypeNode | undefined) { + return createSignatureDeclaration(SyntaxKind.ConstructSignature, typeParameters, parameters, type) as ConstructSignatureDeclaration; +} +export function updateConstructSignature(node: ConstructSignatureDeclaration, typeParameters: NodeArray | undefined, parameters: NodeArray, type: TypeNode | undefined) { + return updateSignatureDeclaration(node, typeParameters, parameters, type); +} +export function createIndexSignature(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, parameters: readonly ParameterDeclaration[], type: TypeNode): IndexSignatureDeclaration { + const node = (createSynthesizedNode(SyntaxKind.IndexSignature) as IndexSignatureDeclaration); + node.decorators = asNodeArray(decorators); + node.modifiers = asNodeArray(modifiers); + node.parameters = createNodeArray(parameters); + node.type = type; + return node; +} +export function updateIndexSignature(node: IndexSignatureDeclaration, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, parameters: readonly ParameterDeclaration[], type: TypeNode) { + return node.parameters !== parameters + || node.type !== type + || node.decorators !== decorators + || node.modifiers !== modifiers + ? updateNode(createIndexSignature(decorators, modifiers, parameters, type), node) + : node; +} +/* @internal */ +export function createSignatureDeclaration(kind: SyntaxKind, typeParameters: readonly TypeParameterDeclaration[] | undefined, parameters: readonly ParameterDeclaration[], type: TypeNode | undefined, typeArguments?: readonly TypeNode[] | undefined) { + const node = (createSynthesizedNode(kind) as SignatureDeclaration); + node.typeParameters = asNodeArray(typeParameters); + node.parameters = asNodeArray(parameters); + node.type = type; + node.typeArguments = asNodeArray(typeArguments); + return node; +} +function updateSignatureDeclaration(node: T, typeParameters: NodeArray | undefined, parameters: NodeArray, type: TypeNode | undefined): T { + return node.typeParameters !== typeParameters + || node.parameters !== parameters + || node.type !== type + ? updateNode(createSignatureDeclaration(node.kind, typeParameters, parameters, type), node) + : node; +} +// Types +export function createKeywordTypeNode(kind: KeywordTypeNode["kind"]) { + return createSynthesizedNode(kind); +} +export function createTypePredicateNode(parameterName: Identifier | ThisTypeNode | string, type: TypeNode) { + return createTypePredicateNodeWithModifier(/*assertsModifier*/ undefined, parameterName, type); +} +export function createTypePredicateNodeWithModifier(assertsModifier: AssertsToken | undefined, parameterName: Identifier | ThisTypeNode | string, type: TypeNode | undefined) { + const node = (createSynthesizedNode(SyntaxKind.TypePredicate) as TypePredicateNode); + node.assertsModifier = assertsModifier; + node.parameterName = asName(parameterName); + node.type = type; + return node; +} +export function updateTypePredicateNode(node: TypePredicateNode, parameterName: Identifier | ThisTypeNode, type: TypeNode) { + return updateTypePredicateNodeWithModifier(node, node.assertsModifier, parameterName, type); +} +export function updateTypePredicateNodeWithModifier(node: TypePredicateNode, assertsModifier: AssertsToken | undefined, parameterName: Identifier | ThisTypeNode, type: TypeNode | undefined) { + return node.assertsModifier !== assertsModifier + || node.parameterName !== parameterName + || node.type !== type + ? updateNode(createTypePredicateNodeWithModifier(assertsModifier, parameterName, type), node) + : node; +} +export function createTypeReferenceNode(typeName: string | EntityName, typeArguments: readonly TypeNode[] | undefined) { + const node = (createSynthesizedNode(SyntaxKind.TypeReference) as TypeReferenceNode); + node.typeName = asName(typeName); + node.typeArguments = typeArguments && parenthesizeTypeParameters(typeArguments); + return node; +} +export function updateTypeReferenceNode(node: TypeReferenceNode, typeName: EntityName, typeArguments: NodeArray | undefined) { + return node.typeName !== typeName + || node.typeArguments !== typeArguments + ? updateNode(createTypeReferenceNode(typeName, typeArguments), node) + : node; +} +export function createFunctionTypeNode(typeParameters: readonly TypeParameterDeclaration[] | undefined, parameters: readonly ParameterDeclaration[], type: TypeNode | undefined) { + return createSignatureDeclaration(SyntaxKind.FunctionType, typeParameters, parameters, type) as FunctionTypeNode; +} +export function updateFunctionTypeNode(node: FunctionTypeNode, typeParameters: NodeArray | undefined, parameters: NodeArray, type: TypeNode | undefined) { + return updateSignatureDeclaration(node, typeParameters, parameters, type); +} +export function createConstructorTypeNode(typeParameters: readonly TypeParameterDeclaration[] | undefined, parameters: readonly ParameterDeclaration[], type: TypeNode | undefined) { + return createSignatureDeclaration(SyntaxKind.ConstructorType, typeParameters, parameters, type) as ConstructorTypeNode; +} +export function updateConstructorTypeNode(node: ConstructorTypeNode, typeParameters: NodeArray | undefined, parameters: NodeArray, type: TypeNode | undefined) { + return updateSignatureDeclaration(node, typeParameters, parameters, type); +} +export function createTypeQueryNode(exprName: EntityName) { + const node = (createSynthesizedNode(SyntaxKind.TypeQuery) as TypeQueryNode); + node.exprName = exprName; + return node; +} +export function updateTypeQueryNode(node: TypeQueryNode, exprName: EntityName) { + return node.exprName !== exprName + ? updateNode(createTypeQueryNode(exprName), node) + : node; +} +export function createTypeLiteralNode(members: readonly TypeElement[] | undefined) { + const node = (createSynthesizedNode(SyntaxKind.TypeLiteral) as TypeLiteralNode); + node.members = createNodeArray(members); + return node; +} +export function updateTypeLiteralNode(node: TypeLiteralNode, members: NodeArray) { + return node.members !== members + ? updateNode(createTypeLiteralNode(members), node) + : node; +} +export function createArrayTypeNode(elementType: TypeNode) { + const node = (createSynthesizedNode(SyntaxKind.ArrayType) as ArrayTypeNode); + node.elementType = parenthesizeArrayTypeMember(elementType); + return node; +} +export function updateArrayTypeNode(node: ArrayTypeNode, elementType: TypeNode): ArrayTypeNode { + return node.elementType !== elementType + ? updateNode(createArrayTypeNode(elementType), node) + : node; +} +export function createTupleTypeNode(elementTypes: readonly TypeNode[]) { + const node = (createSynthesizedNode(SyntaxKind.TupleType) as TupleTypeNode); + node.elementTypes = createNodeArray(elementTypes); + return node; +} +export function updateTupleTypeNode(node: TupleTypeNode, elementTypes: readonly TypeNode[]) { + return node.elementTypes !== elementTypes + ? updateNode(createTupleTypeNode(elementTypes), node) + : node; +} +export function createOptionalTypeNode(type: TypeNode) { + const node = (createSynthesizedNode(SyntaxKind.OptionalType) as OptionalTypeNode); + node.type = parenthesizeArrayTypeMember(type); + return node; +} +export function updateOptionalTypeNode(node: OptionalTypeNode, type: TypeNode): OptionalTypeNode { + return node.type !== type + ? updateNode(createOptionalTypeNode(type), node) + : node; +} +export function createRestTypeNode(type: TypeNode) { + const node = (createSynthesizedNode(SyntaxKind.RestType) as RestTypeNode); + node.type = type; + return node; +} +export function updateRestTypeNode(node: RestTypeNode, type: TypeNode): RestTypeNode { + return node.type !== type + ? updateNode(createRestTypeNode(type), node) + : node; +} +export function createUnionTypeNode(types: readonly TypeNode[]): UnionTypeNode { + return createUnionOrIntersectionTypeNode(SyntaxKind.UnionType, types); +} +export function updateUnionTypeNode(node: UnionTypeNode, types: NodeArray) { + return updateUnionOrIntersectionTypeNode(node, types); +} +export function createIntersectionTypeNode(types: readonly TypeNode[]): IntersectionTypeNode { + return createUnionOrIntersectionTypeNode(SyntaxKind.IntersectionType, types); +} +export function updateIntersectionTypeNode(node: IntersectionTypeNode, types: NodeArray) { + return updateUnionOrIntersectionTypeNode(node, types); +} +export function createUnionOrIntersectionTypeNode(kind: SyntaxKind.UnionType | SyntaxKind.IntersectionType, types: readonly TypeNode[]) { + const node = (createSynthesizedNode(kind) as UnionTypeNode | IntersectionTypeNode); + node.types = parenthesizeElementTypeMembers(types); + return node; +} +function updateUnionOrIntersectionTypeNode(node: T, types: NodeArray): T { + return node.types !== types + ? updateNode(createUnionOrIntersectionTypeNode(node.kind, types), node) + : node; +} +export function createConditionalTypeNode(checkType: TypeNode, extendsType: TypeNode, trueType: TypeNode, falseType: TypeNode) { + const node = (createSynthesizedNode(SyntaxKind.ConditionalType) as ConditionalTypeNode); + node.checkType = parenthesizeConditionalTypeMember(checkType); + node.extendsType = parenthesizeConditionalTypeMember(extendsType); + node.trueType = trueType; + node.falseType = falseType; + return node; +} +export function updateConditionalTypeNode(node: ConditionalTypeNode, checkType: TypeNode, extendsType: TypeNode, trueType: TypeNode, falseType: TypeNode) { + return node.checkType !== checkType + || node.extendsType !== extendsType + || node.trueType !== trueType + || node.falseType !== falseType + ? updateNode(createConditionalTypeNode(checkType, extendsType, trueType, falseType), node) + : node; +} +export function createInferTypeNode(typeParameter: TypeParameterDeclaration) { + const node = (createSynthesizedNode(SyntaxKind.InferType)); + node.typeParameter = typeParameter; + return node; +} +export function updateInferTypeNode(node: InferTypeNode, typeParameter: TypeParameterDeclaration) { + return node.typeParameter !== typeParameter + ? updateNode(createInferTypeNode(typeParameter), node) + : node; +} +export function createImportTypeNode(argument: TypeNode, qualifier?: EntityName, typeArguments?: readonly TypeNode[], isTypeOf?: boolean) { + const node = (createSynthesizedNode(SyntaxKind.ImportType)); + node.argument = argument; + node.qualifier = qualifier; + node.typeArguments = parenthesizeTypeParameters(typeArguments); + node.isTypeOf = isTypeOf; + return node; +} +export function updateImportTypeNode(node: ImportTypeNode, argument: TypeNode, qualifier?: EntityName, typeArguments?: readonly TypeNode[], isTypeOf?: boolean) { + return node.argument !== argument + || node.qualifier !== qualifier + || node.typeArguments !== typeArguments + || node.isTypeOf !== isTypeOf + ? updateNode(createImportTypeNode(argument, qualifier, typeArguments, isTypeOf), node) + : node; +} +export function createParenthesizedType(type: TypeNode) { + const node = (createSynthesizedNode(SyntaxKind.ParenthesizedType)); + node.type = type; + return node; +} +export function updateParenthesizedType(node: ParenthesizedTypeNode, type: TypeNode) { + return node.type !== type + ? updateNode(createParenthesizedType(type), node) + : node; +} +export function createThisTypeNode() { + return createSynthesizedNode(SyntaxKind.ThisType); +} +export function createTypeOperatorNode(type: TypeNode): TypeOperatorNode; +export function createTypeOperatorNode(operator: SyntaxKind.KeyOfKeyword | SyntaxKind.UniqueKeyword | SyntaxKind.ReadonlyKeyword, type: TypeNode): TypeOperatorNode; +export function createTypeOperatorNode(operatorOrType: SyntaxKind.KeyOfKeyword | SyntaxKind.UniqueKeyword | SyntaxKind.ReadonlyKeyword | TypeNode, type?: TypeNode) { + const node = (createSynthesizedNode(SyntaxKind.TypeOperator) as TypeOperatorNode); + node.operator = typeof operatorOrType === "number" ? operatorOrType : SyntaxKind.KeyOfKeyword; + node.type = parenthesizeElementTypeMember(typeof operatorOrType === "number" ? type! : operatorOrType); + return node; +} +export function updateTypeOperatorNode(node: TypeOperatorNode, type: TypeNode) { + return node.type !== type ? updateNode(createTypeOperatorNode(node.operator, type), node) : node; +} +export function createIndexedAccessTypeNode(objectType: TypeNode, indexType: TypeNode) { + const node = (createSynthesizedNode(SyntaxKind.IndexedAccessType) as IndexedAccessTypeNode); + node.objectType = parenthesizeElementTypeMember(objectType); + node.indexType = indexType; + return node; +} +export function updateIndexedAccessTypeNode(node: IndexedAccessTypeNode, objectType: TypeNode, indexType: TypeNode) { + return node.objectType !== objectType + || node.indexType !== indexType + ? updateNode(createIndexedAccessTypeNode(objectType, indexType), node) + : node; +} +export function createMappedTypeNode(readonlyToken: ReadonlyToken | PlusToken | MinusToken | undefined, typeParameter: TypeParameterDeclaration, questionToken: QuestionToken | PlusToken | MinusToken | undefined, type: TypeNode | undefined): MappedTypeNode { + const node = (createSynthesizedNode(SyntaxKind.MappedType) as MappedTypeNode); + node.readonlyToken = readonlyToken; + node.typeParameter = typeParameter; + node.questionToken = questionToken; + node.type = type; + return node; +} +export function updateMappedTypeNode(node: MappedTypeNode, readonlyToken: ReadonlyToken | PlusToken | MinusToken | undefined, typeParameter: TypeParameterDeclaration, questionToken: QuestionToken | PlusToken | MinusToken | undefined, type: TypeNode | undefined): MappedTypeNode { + return node.readonlyToken !== readonlyToken + || node.typeParameter !== typeParameter + || node.questionToken !== questionToken + || node.type !== type + ? updateNode(createMappedTypeNode(readonlyToken, typeParameter, questionToken, type), node) + : node; +} +export function createLiteralTypeNode(literal: LiteralTypeNode["literal"]) { + const node = (createSynthesizedNode(SyntaxKind.LiteralType) as LiteralTypeNode); + node.literal = literal; + return node; +} +export function updateLiteralTypeNode(node: LiteralTypeNode, literal: LiteralTypeNode["literal"]) { + return node.literal !== literal + ? updateNode(createLiteralTypeNode(literal), node) + : node; +} +// Binding Patterns +export function createObjectBindingPattern(elements: readonly BindingElement[]) { + const node = (createSynthesizedNode(SyntaxKind.ObjectBindingPattern)); + node.elements = createNodeArray(elements); + return node; +} +export function updateObjectBindingPattern(node: ObjectBindingPattern, elements: readonly BindingElement[]) { + return node.elements !== elements + ? updateNode(createObjectBindingPattern(elements), node) + : node; +} +export function createArrayBindingPattern(elements: readonly ArrayBindingElement[]) { + const node = (createSynthesizedNode(SyntaxKind.ArrayBindingPattern)); + node.elements = createNodeArray(elements); + return node; +} +export function updateArrayBindingPattern(node: ArrayBindingPattern, elements: readonly ArrayBindingElement[]) { + return node.elements !== elements + ? updateNode(createArrayBindingPattern(elements), node) + : node; +} +export function createBindingElement(dotDotDotToken: DotDotDotToken | undefined, propertyName: string | PropertyName | undefined, name: string | BindingName, initializer?: Expression) { + const node = (createSynthesizedNode(SyntaxKind.BindingElement)); + node.dotDotDotToken = dotDotDotToken; + node.propertyName = asName(propertyName); + node.name = asName(name); + node.initializer = initializer; + return node; +} +export function updateBindingElement(node: BindingElement, dotDotDotToken: DotDotDotToken | undefined, propertyName: PropertyName | undefined, name: BindingName, initializer: Expression | undefined) { + return node.propertyName !== propertyName + || node.dotDotDotToken !== dotDotDotToken + || node.name !== name + || node.initializer !== initializer + ? updateNode(createBindingElement(dotDotDotToken, propertyName, name, initializer), node) + : node; +} +// Expression +export function createArrayLiteral(elements?: readonly Expression[], multiLine?: boolean) { + const node = (createSynthesizedNode(SyntaxKind.ArrayLiteralExpression)); + node.elements = parenthesizeListElements(createNodeArray(elements)); + if (multiLine) + node.multiLine = true; + return node; +} +export function updateArrayLiteral(node: ArrayLiteralExpression, elements: readonly Expression[]) { + return node.elements !== elements + ? updateNode(createArrayLiteral(elements, node.multiLine), node) + : node; +} +export function createObjectLiteral(properties?: readonly ObjectLiteralElementLike[], multiLine?: boolean) { + const node = (createSynthesizedNode(SyntaxKind.ObjectLiteralExpression)); + node.properties = createNodeArray(properties); + if (multiLine) + node.multiLine = true; + return node; +} +export function updateObjectLiteral(node: ObjectLiteralExpression, properties: readonly ObjectLiteralElementLike[]) { + return node.properties !== properties + ? updateNode(createObjectLiteral(properties, node.multiLine), node) + : node; +} +export function createPropertyAccess(expression: Expression, name: string | Identifier | PrivateIdentifier) { + const node = (createSynthesizedNode(SyntaxKind.PropertyAccessExpression)); + node.expression = parenthesizeForAccess(expression); + node.name = asName(name); + setEmitFlags(node, EmitFlags.NoIndentation); + return node; +} +export function updatePropertyAccess(node: PropertyAccessExpression, expression: Expression, name: Identifier | PrivateIdentifier) { + if (isOptionalChain(node) && isIdentifier(node.name) && isIdentifier(name)) { + // Not sure why this cast was necessary: the previous line should already establish that node.name is an identifier + const theNode = (node as (typeof node & { + name: Identifier; + })); + return updatePropertyAccessChain(theNode, expression, node.questionDotToken, name); + } + // Because we are updating existed propertyAccess we want to inherit its emitFlags + // instead of using the default from createPropertyAccess + return node.expression !== expression + || node.name !== name + ? updateNode(setEmitFlags(createPropertyAccess(expression, name), getEmitFlags(node)), node) + : node; +} +export function createPropertyAccessChain(expression: Expression, questionDotToken: QuestionDotToken | undefined, name: string | Identifier) { + const node = (createSynthesizedNode(SyntaxKind.PropertyAccessExpression)); + node.flags |= NodeFlags.OptionalChain; + node.expression = parenthesizeForAccess(expression); + node.questionDotToken = questionDotToken; + node.name = asName(name); + setEmitFlags(node, EmitFlags.NoIndentation); + return node; +} +export function updatePropertyAccessChain(node: PropertyAccessChain, expression: Expression, questionDotToken: QuestionDotToken | undefined, name: Identifier) { + Debug.assert(!!(node.flags & NodeFlags.OptionalChain), "Cannot update a PropertyAccessExpression using updatePropertyAccessChain. Use updatePropertyAccess instead."); + // Because we are updating an existing PropertyAccessChain we want to inherit its emitFlags + // instead of using the default from createPropertyAccess + return node.expression !== expression + || node.questionDotToken !== questionDotToken + || node.name !== name + ? updateNode(setEmitFlags(createPropertyAccessChain(expression, questionDotToken, name), getEmitFlags(node)), node) + : node; +} +export function createElementAccess(expression: Expression, index: number | Expression) { + const node = (createSynthesizedNode(SyntaxKind.ElementAccessExpression)); + node.expression = parenthesizeForAccess(expression); + node.argumentExpression = asExpression(index); + return node; +} +export function updateElementAccess(node: ElementAccessExpression, expression: Expression, argumentExpression: Expression) { + if (isOptionalChain(node)) { + return updateElementAccessChain(node, expression, node.questionDotToken, argumentExpression); + } + return node.expression !== expression + || node.argumentExpression !== argumentExpression + ? updateNode(createElementAccess(expression, argumentExpression), node) + : node; +} +export function createElementAccessChain(expression: Expression, questionDotToken: QuestionDotToken | undefined, index: number | Expression) { + const node = (createSynthesizedNode(SyntaxKind.ElementAccessExpression)); + node.flags |= NodeFlags.OptionalChain; + node.expression = parenthesizeForAccess(expression); + node.questionDotToken = questionDotToken; + node.argumentExpression = asExpression(index); + return node; +} +export function updateElementAccessChain(node: ElementAccessChain, expression: Expression, questionDotToken: QuestionDotToken | undefined, argumentExpression: Expression) { + Debug.assert(!!(node.flags & NodeFlags.OptionalChain), "Cannot update an ElementAccessExpression using updateElementAccessChain. Use updateElementAccess instead."); + return node.expression !== expression + || node.questionDotToken !== questionDotToken + || node.argumentExpression !== argumentExpression + ? updateNode(createElementAccessChain(expression, questionDotToken, argumentExpression), node) + : node; +} +export function createCall(expression: Expression, typeArguments: readonly TypeNode[] | undefined, argumentsArray: readonly Expression[] | undefined) { + const node = (createSynthesizedNode(SyntaxKind.CallExpression)); + node.expression = parenthesizeForAccess(expression); + node.typeArguments = asNodeArray(typeArguments); + node.arguments = parenthesizeListElements(createNodeArray(argumentsArray)); + return node; +} +export function updateCall(node: CallExpression, expression: Expression, typeArguments: readonly TypeNode[] | undefined, argumentsArray: readonly Expression[]) { + if (isOptionalChain(node)) { + return updateCallChain(node, expression, node.questionDotToken, typeArguments, argumentsArray); + } + return node.expression !== expression + || node.typeArguments !== typeArguments + || node.arguments !== argumentsArray + ? updateNode(createCall(expression, typeArguments, argumentsArray), node) + : node; +} +export function createCallChain(expression: Expression, questionDotToken: QuestionDotToken | undefined, typeArguments: readonly TypeNode[] | undefined, argumentsArray: readonly Expression[] | undefined) { + const node = (createSynthesizedNode(SyntaxKind.CallExpression)); + node.flags |= NodeFlags.OptionalChain; + node.expression = parenthesizeForAccess(expression); + node.questionDotToken = questionDotToken; + node.typeArguments = asNodeArray(typeArguments); + node.arguments = parenthesizeListElements(createNodeArray(argumentsArray)); + return node; +} +export function updateCallChain(node: CallChain, expression: Expression, questionDotToken: QuestionDotToken | undefined, typeArguments: readonly TypeNode[] | undefined, argumentsArray: readonly Expression[]) { + Debug.assert(!!(node.flags & NodeFlags.OptionalChain), "Cannot update a CallExpression using updateCallChain. Use updateCall instead."); + return node.expression !== expression + || node.questionDotToken !== questionDotToken + || node.typeArguments !== typeArguments + || node.arguments !== argumentsArray + ? updateNode(createCallChain(expression, questionDotToken, typeArguments, argumentsArray), node) + : node; +} +export function createNew(expression: Expression, typeArguments: readonly TypeNode[] | undefined, argumentsArray: readonly Expression[] | undefined) { + const node = (createSynthesizedNode(SyntaxKind.NewExpression)); + node.expression = parenthesizeForNew(expression); + node.typeArguments = asNodeArray(typeArguments); + node.arguments = argumentsArray ? parenthesizeListElements(createNodeArray(argumentsArray)) : undefined; + return node; +} +export function updateNew(node: NewExpression, expression: Expression, typeArguments: readonly TypeNode[] | undefined, argumentsArray: readonly Expression[] | undefined) { + return node.expression !== expression + || node.typeArguments !== typeArguments + || node.arguments !== argumentsArray + ? updateNode(createNew(expression, typeArguments, argumentsArray), node) + : node; +} +/** @deprecated */ export function createTaggedTemplate(tag: Expression, template: TemplateLiteral): TaggedTemplateExpression; +export function createTaggedTemplate(tag: Expression, typeArguments: readonly TypeNode[] | undefined, template: TemplateLiteral): TaggedTemplateExpression; +/** @internal */ +export function createTaggedTemplate(tag: Expression, typeArgumentsOrTemplate: readonly TypeNode[] | TemplateLiteral | undefined, template?: TemplateLiteral): TaggedTemplateExpression; +export function createTaggedTemplate(tag: Expression, typeArgumentsOrTemplate: readonly TypeNode[] | TemplateLiteral | undefined, template?: TemplateLiteral) { + const node = (createSynthesizedNode(SyntaxKind.TaggedTemplateExpression)); + node.tag = parenthesizeForAccess(tag); + if (template) { + node.typeArguments = asNodeArray((typeArgumentsOrTemplate as readonly TypeNode[])); + node.template = template; + } + else { + node.typeArguments = undefined; + node.template = (typeArgumentsOrTemplate as TemplateLiteral); + } + return node; +} +/** @deprecated */ export function updateTaggedTemplate(node: TaggedTemplateExpression, tag: Expression, template: TemplateLiteral): TaggedTemplateExpression; +export function updateTaggedTemplate(node: TaggedTemplateExpression, tag: Expression, typeArguments: readonly TypeNode[] | undefined, template: TemplateLiteral): TaggedTemplateExpression; +export function updateTaggedTemplate(node: TaggedTemplateExpression, tag: Expression, typeArgumentsOrTemplate: readonly TypeNode[] | TemplateLiteral | undefined, template?: TemplateLiteral) { + return node.tag !== tag + || (template + ? node.typeArguments !== typeArgumentsOrTemplate || node.template !== template + : node.typeArguments !== undefined || node.template !== typeArgumentsOrTemplate) + ? updateNode(createTaggedTemplate(tag, typeArgumentsOrTemplate, template), node) + : node; +} +export function createTypeAssertion(type: TypeNode, expression: Expression) { + const node = (createSynthesizedNode(SyntaxKind.TypeAssertionExpression)); + node.type = type; + node.expression = parenthesizePrefixOperand(expression); + return node; +} +export function updateTypeAssertion(node: TypeAssertion, type: TypeNode, expression: Expression) { + return node.type !== type + || node.expression !== expression + ? updateNode(createTypeAssertion(type, expression), node) + : node; +} +export function createParen(expression: Expression) { + const node = (createSynthesizedNode(SyntaxKind.ParenthesizedExpression)); + node.expression = expression; + return node; +} +export function updateParen(node: ParenthesizedExpression, expression: Expression) { + return node.expression !== expression + ? updateNode(createParen(expression), node) + : node; +} +export function createFunctionExpression(modifiers: readonly Modifier[] | undefined, asteriskToken: AsteriskToken | undefined, name: string | Identifier | undefined, typeParameters: readonly TypeParameterDeclaration[] | undefined, parameters: readonly ParameterDeclaration[] | undefined, type: TypeNode | undefined, body: Block) { + const node = (createSynthesizedNode(SyntaxKind.FunctionExpression)); + node.modifiers = asNodeArray(modifiers); + node.asteriskToken = asteriskToken; + node.name = asName(name); + node.typeParameters = asNodeArray(typeParameters); + node.parameters = createNodeArray(parameters); + node.type = type; + node.body = body; + return node; +} +export function updateFunctionExpression(node: FunctionExpression, modifiers: readonly Modifier[] | undefined, asteriskToken: AsteriskToken | undefined, name: Identifier | undefined, typeParameters: readonly TypeParameterDeclaration[] | undefined, parameters: readonly ParameterDeclaration[], type: TypeNode | undefined, body: Block) { + return node.name !== name + || node.modifiers !== modifiers + || node.asteriskToken !== asteriskToken + || node.typeParameters !== typeParameters + || node.parameters !== parameters + || node.type !== type + || node.body !== body + ? updateNode(createFunctionExpression(modifiers, asteriskToken, name, typeParameters, parameters, type, body), node) + : node; +} +export function createArrowFunction(modifiers: readonly Modifier[] | undefined, typeParameters: readonly TypeParameterDeclaration[] | undefined, parameters: readonly ParameterDeclaration[], type: TypeNode | undefined, equalsGreaterThanToken: EqualsGreaterThanToken | undefined, body: ConciseBody) { + const node = (createSynthesizedNode(SyntaxKind.ArrowFunction)); + node.modifiers = asNodeArray(modifiers); + node.typeParameters = asNodeArray(typeParameters); + node.parameters = createNodeArray(parameters); + node.type = type; + node.equalsGreaterThanToken = equalsGreaterThanToken || createToken(SyntaxKind.EqualsGreaterThanToken); + node.body = parenthesizeConciseBody(body); + return node; +} +export function updateArrowFunction(node: ArrowFunction, modifiers: readonly Modifier[] | undefined, typeParameters: readonly TypeParameterDeclaration[] | undefined, parameters: readonly ParameterDeclaration[], type: TypeNode | undefined, equalsGreaterThanToken: Token, body: ConciseBody): ArrowFunction { + return node.modifiers !== modifiers + || node.typeParameters !== typeParameters + || node.parameters !== parameters + || node.type !== type + || node.equalsGreaterThanToken !== equalsGreaterThanToken + || node.body !== body + ? updateNode(createArrowFunction(modifiers, typeParameters, parameters, type, equalsGreaterThanToken, body), node) + : node; +} +export function createDelete(expression: Expression) { + const node = (createSynthesizedNode(SyntaxKind.DeleteExpression)); + node.expression = parenthesizePrefixOperand(expression); + return node; +} +export function updateDelete(node: DeleteExpression, expression: Expression) { + return node.expression !== expression + ? updateNode(createDelete(expression), node) + : node; +} +export function createTypeOf(expression: Expression) { + const node = (createSynthesizedNode(SyntaxKind.TypeOfExpression)); + node.expression = parenthesizePrefixOperand(expression); + return node; +} +export function updateTypeOf(node: TypeOfExpression, expression: Expression) { + return node.expression !== expression + ? updateNode(createTypeOf(expression), node) + : node; +} +export function createVoid(expression: Expression) { + const node = (createSynthesizedNode(SyntaxKind.VoidExpression)); + node.expression = parenthesizePrefixOperand(expression); + return node; +} +export function updateVoid(node: VoidExpression, expression: Expression) { + return node.expression !== expression + ? updateNode(createVoid(expression), node) + : node; +} +export function createAwait(expression: Expression) { + const node = (createSynthesizedNode(SyntaxKind.AwaitExpression)); + node.expression = parenthesizePrefixOperand(expression); + return node; +} +export function updateAwait(node: AwaitExpression, expression: Expression) { + return node.expression !== expression + ? updateNode(createAwait(expression), node) + : node; +} +export function createPrefix(operator: PrefixUnaryOperator, operand: Expression) { + const node = (createSynthesizedNode(SyntaxKind.PrefixUnaryExpression)); + node.operator = operator; + node.operand = parenthesizePrefixOperand(operand); + return node; +} +export function updatePrefix(node: PrefixUnaryExpression, operand: Expression) { + return node.operand !== operand + ? updateNode(createPrefix(node.operator, operand), node) + : node; +} +export function createPostfix(operand: Expression, operator: PostfixUnaryOperator) { + const node = (createSynthesizedNode(SyntaxKind.PostfixUnaryExpression)); + node.operand = parenthesizePostfixOperand(operand); + node.operator = operator; + return node; +} +export function updatePostfix(node: PostfixUnaryExpression, operand: Expression) { + return node.operand !== operand + ? updateNode(createPostfix(operand, node.operator), node) + : node; +} +export function createBinary(left: Expression, operator: BinaryOperator | BinaryOperatorToken, right: Expression) { + const node = (createSynthesizedNode(SyntaxKind.BinaryExpression)); + const operatorToken = asToken(operator); + const operatorKind = operatorToken.kind; + node.left = parenthesizeBinaryOperand(operatorKind, left, /*isLeftSideOfBinary*/ true, /*leftOperand*/ undefined); + node.operatorToken = operatorToken; + node.right = parenthesizeBinaryOperand(operatorKind, right, /*isLeftSideOfBinary*/ false, node.left); + return node; +} +export function updateBinary(node: BinaryExpression, left: Expression, right: Expression, operator?: BinaryOperator | BinaryOperatorToken) { + return node.left !== left + || node.right !== right + ? updateNode(createBinary(left, operator || node.operatorToken, right), node) + : node; +} +/** @deprecated */ export function createConditional(condition: Expression, whenTrue: Expression, whenFalse: Expression): ConditionalExpression; +export function createConditional(condition: Expression, questionToken: QuestionToken, whenTrue: Expression, colonToken: ColonToken, whenFalse: Expression): ConditionalExpression; +export function createConditional(condition: Expression, questionTokenOrWhenTrue: QuestionToken | Expression, whenTrueOrWhenFalse: Expression, colonToken?: ColonToken, whenFalse?: Expression) { + const node = (createSynthesizedNode(SyntaxKind.ConditionalExpression)); + node.condition = parenthesizeForConditionalHead(condition); + node.questionToken = whenFalse ? questionTokenOrWhenTrue : createToken(SyntaxKind.QuestionToken); + node.whenTrue = parenthesizeSubexpressionOfConditionalExpression(whenFalse ? whenTrueOrWhenFalse : questionTokenOrWhenTrue); + node.colonToken = whenFalse ? colonToken! : createToken(SyntaxKind.ColonToken); + node.whenFalse = parenthesizeSubexpressionOfConditionalExpression(whenFalse ? whenFalse : whenTrueOrWhenFalse); + return node; +} +export function updateConditional(node: ConditionalExpression, condition: Expression, questionToken: Token, whenTrue: Expression, colonToken: Token, whenFalse: Expression): ConditionalExpression { + return node.condition !== condition + || node.questionToken !== questionToken + || node.whenTrue !== whenTrue + || node.colonToken !== colonToken + || node.whenFalse !== whenFalse + ? updateNode(createConditional(condition, questionToken, whenTrue, colonToken, whenFalse), node) + : node; +} +export function createTemplateExpression(head: TemplateHead, templateSpans: readonly TemplateSpan[]) { + const node = (createSynthesizedNode(SyntaxKind.TemplateExpression)); + node.head = head; + node.templateSpans = createNodeArray(templateSpans); + return node; +} +export function updateTemplateExpression(node: TemplateExpression, head: TemplateHead, templateSpans: readonly TemplateSpan[]) { + return node.head !== head + || node.templateSpans !== templateSpans + ? updateNode(createTemplateExpression(head, templateSpans), node) + : node; +} +let rawTextScanner: Scanner | undefined; +const invalidValueSentinel: object = {}; +function getCookedText(kind: TemplateLiteralToken["kind"], rawText: string) { + if (!rawTextScanner) { + rawTextScanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ false, LanguageVariant.Standard); + } + switch (kind) { + case SyntaxKind.NoSubstitutionTemplateLiteral: + rawTextScanner.setText("`" + rawText + "`"); + break; + case SyntaxKind.TemplateHead: + rawTextScanner.setText("`" + rawText + "${"); + break; + case SyntaxKind.TemplateMiddle: + rawTextScanner.setText("}" + rawText + "${"); + break; + case SyntaxKind.TemplateTail: + rawTextScanner.setText("}" + rawText + "`"); + break; + } + let token = rawTextScanner.scan(); + if (token === SyntaxKind.CloseBracketToken) { + token = rawTextScanner.reScanTemplateToken(/* isTaggedTemplate */ false); + } + if (rawTextScanner.isUnterminated()) { + rawTextScanner.setText(undefined); + return invalidValueSentinel; + } + let tokenValue: string | undefined; + switch (token) { + case SyntaxKind.NoSubstitutionTemplateLiteral: + case SyntaxKind.TemplateHead: + case SyntaxKind.TemplateMiddle: + case SyntaxKind.TemplateTail: + tokenValue = rawTextScanner.getTokenValue(); + break; + } + if (rawTextScanner.scan() !== SyntaxKind.EndOfFileToken) { + rawTextScanner.setText(undefined); + return invalidValueSentinel; } - - function tryAddPropertyAssignment(properties: Push, propertyName: string, expression: Expression | undefined) { - if (expression) { - properties.push(createPropertyAssignment(propertyName, expression)); - return true; + rawTextScanner.setText(undefined); + return tokenValue; +} +function createTemplateLiteralLikeNode(kind: TemplateLiteralToken["kind"], text: string, rawText: string | undefined) { + const node = (createSynthesizedNode(kind)); + node.text = text; + if (rawText === undefined || text === rawText) { + node.rawText = rawText; + } + else { + const cooked = getCookedText(kind, rawText); + if (typeof cooked === "object") { + return Debug.fail("Invalid raw text"); } - return false; - } - - /* @internal */ - export function createPropertyDescriptor(attributes: PropertyDescriptorAttributes, singleLine?: boolean) { - const properties: PropertyAssignment[] = []; - tryAddPropertyAssignment(properties, "enumerable", asExpression(attributes.enumerable)); - tryAddPropertyAssignment(properties, "configurable", asExpression(attributes.configurable)); - - let isData = tryAddPropertyAssignment(properties, "writable", asExpression(attributes.writable)); - isData = tryAddPropertyAssignment(properties, "value", attributes.value) || isData; - - let isAccessor = tryAddPropertyAssignment(properties, "get", attributes.get); - isAccessor = tryAddPropertyAssignment(properties, "set", attributes.set) || isAccessor; - - Debug.assert(!(isData && isAccessor), "A PropertyDescriptor may not be both an accessor descriptor and a data descriptor."); - return createObjectLiteral(properties, !singleLine); - } - - export function updateMethod( - node: MethodDeclaration, - decorators: readonly Decorator[] | undefined, - modifiers: readonly Modifier[] | undefined, - asteriskToken: AsteriskToken | undefined, - name: PropertyName, - questionToken: QuestionToken | undefined, - typeParameters: readonly TypeParameterDeclaration[] | undefined, - parameters: readonly ParameterDeclaration[], - type: TypeNode | undefined, - body: Block | undefined) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.asteriskToken !== asteriskToken - || node.name !== name - || node.questionToken !== questionToken - || node.typeParameters !== typeParameters - || node.parameters !== parameters - || node.type !== type - || node.body !== body - ? updateNode(createMethod(decorators, modifiers, asteriskToken, name, questionToken, typeParameters, parameters, type, body), node) - : node; - } - - export function createConstructor(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, parameters: readonly ParameterDeclaration[], body: Block | undefined) { - const node = createSynthesizedNode(SyntaxKind.Constructor); - node.decorators = asNodeArray(decorators); - node.modifiers = asNodeArray(modifiers); - node.typeParameters = undefined; - node.parameters = createNodeArray(parameters); - node.type = undefined; - node.body = body; - return node; - } - - export function updateConstructor( - node: ConstructorDeclaration, - decorators: readonly Decorator[] | undefined, - modifiers: readonly Modifier[] | undefined, - parameters: readonly ParameterDeclaration[], - body: Block | undefined) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.parameters !== parameters - || node.body !== body - ? updateNode(createConstructor(decorators, modifiers, parameters, body), node) - : node; - } - - export function createGetAccessor( - decorators: readonly Decorator[] | undefined, - modifiers: readonly Modifier[] | undefined, - name: string | PropertyName, - parameters: readonly ParameterDeclaration[], - type: TypeNode | undefined, - body: Block | undefined) { - const node = createSynthesizedNode(SyntaxKind.GetAccessor); - node.decorators = asNodeArray(decorators); - node.modifiers = asNodeArray(modifiers); - node.name = asName(name); - node.typeParameters = undefined; - node.parameters = createNodeArray(parameters); - node.type = type; - node.body = body; - return node; - } - - export function updateGetAccessor( - node: GetAccessorDeclaration, - decorators: readonly Decorator[] | undefined, - modifiers: readonly Modifier[] | undefined, - name: PropertyName, - parameters: readonly ParameterDeclaration[], - type: TypeNode | undefined, - body: Block | undefined) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.name !== name - || node.parameters !== parameters - || node.type !== type - || node.body !== body - ? updateNode(createGetAccessor(decorators, modifiers, name, parameters, type, body), node) - : node; - } - - export function createSetAccessor( - decorators: readonly Decorator[] | undefined, - modifiers: readonly Modifier[] | undefined, - name: string | PropertyName, - parameters: readonly ParameterDeclaration[], - body: Block | undefined) { - const node = createSynthesizedNode(SyntaxKind.SetAccessor); - node.decorators = asNodeArray(decorators); - node.modifiers = asNodeArray(modifiers); - node.name = asName(name); - node.typeParameters = undefined; - node.parameters = createNodeArray(parameters); - node.body = body; - return node; - } - - export function updateSetAccessor( - node: SetAccessorDeclaration, - decorators: readonly Decorator[] | undefined, - modifiers: readonly Modifier[] | undefined, - name: PropertyName, - parameters: readonly ParameterDeclaration[], - body: Block | undefined) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.name !== name - || node.parameters !== parameters - || node.body !== body - ? updateNode(createSetAccessor(decorators, modifiers, name, parameters, body), node) - : node; - } - - export function createCallSignature(typeParameters: readonly TypeParameterDeclaration[] | undefined, parameters: readonly ParameterDeclaration[], type: TypeNode | undefined) { - return createSignatureDeclaration(SyntaxKind.CallSignature, typeParameters, parameters, type) as CallSignatureDeclaration; - } - - export function updateCallSignature(node: CallSignatureDeclaration, typeParameters: NodeArray | undefined, parameters: NodeArray, type: TypeNode | undefined) { - return updateSignatureDeclaration(node, typeParameters, parameters, type); - } - - export function createConstructSignature(typeParameters: readonly TypeParameterDeclaration[] | undefined, parameters: readonly ParameterDeclaration[], type: TypeNode | undefined) { - return createSignatureDeclaration(SyntaxKind.ConstructSignature, typeParameters, parameters, type) as ConstructSignatureDeclaration; - } - - export function updateConstructSignature(node: ConstructSignatureDeclaration, typeParameters: NodeArray | undefined, parameters: NodeArray, type: TypeNode | undefined) { - return updateSignatureDeclaration(node, typeParameters, parameters, type); - } - - export function createIndexSignature( - decorators: readonly Decorator[] | undefined, - modifiers: readonly Modifier[] | undefined, - parameters: readonly ParameterDeclaration[], - type: TypeNode): IndexSignatureDeclaration { - const node = createSynthesizedNode(SyntaxKind.IndexSignature) as IndexSignatureDeclaration; - node.decorators = asNodeArray(decorators); - node.modifiers = asNodeArray(modifiers); - node.parameters = createNodeArray(parameters); - node.type = type; - return node; - } - - export function updateIndexSignature( - node: IndexSignatureDeclaration, - decorators: readonly Decorator[] | undefined, - modifiers: readonly Modifier[] | undefined, - parameters: readonly ParameterDeclaration[], - type: TypeNode) { - return node.parameters !== parameters - || node.type !== type - || node.decorators !== decorators - || node.modifiers !== modifiers - ? updateNode(createIndexSignature(decorators, modifiers, parameters, type), node) - : node; - } - - /* @internal */ - export function createSignatureDeclaration(kind: SyntaxKind, typeParameters: readonly TypeParameterDeclaration[] | undefined, parameters: readonly ParameterDeclaration[], type: TypeNode | undefined, typeArguments?: readonly TypeNode[] | undefined) { - const node = createSynthesizedNode(kind) as SignatureDeclaration; - node.typeParameters = asNodeArray(typeParameters); - node.parameters = asNodeArray(parameters); - node.type = type; - node.typeArguments = asNodeArray(typeArguments); - return node; - } - - function updateSignatureDeclaration(node: T, typeParameters: NodeArray | undefined, parameters: NodeArray, type: TypeNode | undefined): T { - return node.typeParameters !== typeParameters - || node.parameters !== parameters - || node.type !== type - ? updateNode(createSignatureDeclaration(node.kind, typeParameters, parameters, type), node) - : node; - } - - // Types - - export function createKeywordTypeNode(kind: KeywordTypeNode["kind"]) { - return createSynthesizedNode(kind); - } - - export function createTypePredicateNode(parameterName: Identifier | ThisTypeNode | string, type: TypeNode) { - return createTypePredicateNodeWithModifier(/*assertsModifier*/ undefined, parameterName, type); - } - - export function createTypePredicateNodeWithModifier(assertsModifier: AssertsToken | undefined, parameterName: Identifier | ThisTypeNode | string, type: TypeNode | undefined) { - const node = createSynthesizedNode(SyntaxKind.TypePredicate) as TypePredicateNode; - node.assertsModifier = assertsModifier; - node.parameterName = asName(parameterName); - node.type = type; - return node; - } - - export function updateTypePredicateNode(node: TypePredicateNode, parameterName: Identifier | ThisTypeNode, type: TypeNode) { - return updateTypePredicateNodeWithModifier(node, node.assertsModifier, parameterName, type); + Debug.assert(text === cooked, "Expected argument 'text' to be the normalized (i.e. 'cooked') version of argument 'rawText'."); + node.rawText = rawText; } - - export function updateTypePredicateNodeWithModifier(node: TypePredicateNode, assertsModifier: AssertsToken | undefined, parameterName: Identifier | ThisTypeNode, type: TypeNode | undefined) { - return node.assertsModifier !== assertsModifier - || node.parameterName !== parameterName - || node.type !== type - ? updateNode(createTypePredicateNodeWithModifier(assertsModifier, parameterName, type), node) - : node; - } - - export function createTypeReferenceNode(typeName: string | EntityName, typeArguments: readonly TypeNode[] | undefined) { - const node = createSynthesizedNode(SyntaxKind.TypeReference) as TypeReferenceNode; - node.typeName = asName(typeName); - node.typeArguments = typeArguments && parenthesizeTypeParameters(typeArguments); - return node; - } - - export function updateTypeReferenceNode(node: TypeReferenceNode, typeName: EntityName, typeArguments: NodeArray | undefined) { - return node.typeName !== typeName - || node.typeArguments !== typeArguments - ? updateNode(createTypeReferenceNode(typeName, typeArguments), node) - : node; - } - - export function createFunctionTypeNode(typeParameters: readonly TypeParameterDeclaration[] | undefined, parameters: readonly ParameterDeclaration[], type: TypeNode | undefined) { - return createSignatureDeclaration(SyntaxKind.FunctionType, typeParameters, parameters, type) as FunctionTypeNode; - } - - export function updateFunctionTypeNode(node: FunctionTypeNode, typeParameters: NodeArray | undefined, parameters: NodeArray, type: TypeNode | undefined) { - return updateSignatureDeclaration(node, typeParameters, parameters, type); - } - - export function createConstructorTypeNode(typeParameters: readonly TypeParameterDeclaration[] | undefined, parameters: readonly ParameterDeclaration[], type: TypeNode | undefined) { - return createSignatureDeclaration(SyntaxKind.ConstructorType, typeParameters, parameters, type) as ConstructorTypeNode; - } - - export function updateConstructorTypeNode(node: ConstructorTypeNode, typeParameters: NodeArray | undefined, parameters: NodeArray, type: TypeNode | undefined) { - return updateSignatureDeclaration(node, typeParameters, parameters, type); - } - - export function createTypeQueryNode(exprName: EntityName) { - const node = createSynthesizedNode(SyntaxKind.TypeQuery) as TypeQueryNode; - node.exprName = exprName; - return node; - } - - export function updateTypeQueryNode(node: TypeQueryNode, exprName: EntityName) { - return node.exprName !== exprName - ? updateNode(createTypeQueryNode(exprName), node) - : node; - } - - export function createTypeLiteralNode(members: readonly TypeElement[] | undefined) { - const node = createSynthesizedNode(SyntaxKind.TypeLiteral) as TypeLiteralNode; - node.members = createNodeArray(members); - return node; - } - - export function updateTypeLiteralNode(node: TypeLiteralNode, members: NodeArray) { - return node.members !== members - ? updateNode(createTypeLiteralNode(members), node) - : node; - } - - export function createArrayTypeNode(elementType: TypeNode) { - const node = createSynthesizedNode(SyntaxKind.ArrayType) as ArrayTypeNode; - node.elementType = parenthesizeArrayTypeMember(elementType); - return node; - } - - export function updateArrayTypeNode(node: ArrayTypeNode, elementType: TypeNode): ArrayTypeNode { - return node.elementType !== elementType - ? updateNode(createArrayTypeNode(elementType), node) - : node; - } - - export function createTupleTypeNode(elementTypes: readonly TypeNode[]) { - const node = createSynthesizedNode(SyntaxKind.TupleType) as TupleTypeNode; - node.elementTypes = createNodeArray(elementTypes); - return node; - } - - export function updateTupleTypeNode(node: TupleTypeNode, elementTypes: readonly TypeNode[]) { - return node.elementTypes !== elementTypes - ? updateNode(createTupleTypeNode(elementTypes), node) - : node; - } - - export function createOptionalTypeNode(type: TypeNode) { - const node = createSynthesizedNode(SyntaxKind.OptionalType) as OptionalTypeNode; - node.type = parenthesizeArrayTypeMember(type); - return node; - } - - export function updateOptionalTypeNode(node: OptionalTypeNode, type: TypeNode): OptionalTypeNode { - return node.type !== type - ? updateNode(createOptionalTypeNode(type), node) - : node; - } - - export function createRestTypeNode(type: TypeNode) { - const node = createSynthesizedNode(SyntaxKind.RestType) as RestTypeNode; - node.type = type; - return node; - } - - export function updateRestTypeNode(node: RestTypeNode, type: TypeNode): RestTypeNode { - return node.type !== type - ? updateNode(createRestTypeNode(type), node) - : node; - } - - export function createUnionTypeNode(types: readonly TypeNode[]): UnionTypeNode { - return createUnionOrIntersectionTypeNode(SyntaxKind.UnionType, types); - } - - export function updateUnionTypeNode(node: UnionTypeNode, types: NodeArray) { - return updateUnionOrIntersectionTypeNode(node, types); - } - - export function createIntersectionTypeNode(types: readonly TypeNode[]): IntersectionTypeNode { - return createUnionOrIntersectionTypeNode(SyntaxKind.IntersectionType, types); - } - - export function updateIntersectionTypeNode(node: IntersectionTypeNode, types: NodeArray) { - return updateUnionOrIntersectionTypeNode(node, types); - } - - export function createUnionOrIntersectionTypeNode(kind: SyntaxKind.UnionType | SyntaxKind.IntersectionType, types: readonly TypeNode[]) { - const node = createSynthesizedNode(kind) as UnionTypeNode | IntersectionTypeNode; - node.types = parenthesizeElementTypeMembers(types); - return node; - } - - function updateUnionOrIntersectionTypeNode(node: T, types: NodeArray): T { - return node.types !== types - ? updateNode(createUnionOrIntersectionTypeNode(node.kind, types), node) - : node; - } - - export function createConditionalTypeNode(checkType: TypeNode, extendsType: TypeNode, trueType: TypeNode, falseType: TypeNode) { - const node = createSynthesizedNode(SyntaxKind.ConditionalType) as ConditionalTypeNode; - node.checkType = parenthesizeConditionalTypeMember(checkType); - node.extendsType = parenthesizeConditionalTypeMember(extendsType); - node.trueType = trueType; - node.falseType = falseType; - return node; - } - - export function updateConditionalTypeNode(node: ConditionalTypeNode, checkType: TypeNode, extendsType: TypeNode, trueType: TypeNode, falseType: TypeNode) { - return node.checkType !== checkType - || node.extendsType !== extendsType - || node.trueType !== trueType - || node.falseType !== falseType - ? updateNode(createConditionalTypeNode(checkType, extendsType, trueType, falseType), node) - : node; - } - - export function createInferTypeNode(typeParameter: TypeParameterDeclaration) { - const node = createSynthesizedNode(SyntaxKind.InferType); - node.typeParameter = typeParameter; - return node; - } - - export function updateInferTypeNode(node: InferTypeNode, typeParameter: TypeParameterDeclaration) { - return node.typeParameter !== typeParameter - ? updateNode(createInferTypeNode(typeParameter), node) - : node; - } - - export function createImportTypeNode(argument: TypeNode, qualifier?: EntityName, typeArguments?: readonly TypeNode[], isTypeOf?: boolean) { - const node = createSynthesizedNode(SyntaxKind.ImportType); - node.argument = argument; - node.qualifier = qualifier; - node.typeArguments = parenthesizeTypeParameters(typeArguments); - node.isTypeOf = isTypeOf; - return node; - } - - export function updateImportTypeNode(node: ImportTypeNode, argument: TypeNode, qualifier?: EntityName, typeArguments?: readonly TypeNode[], isTypeOf?: boolean) { - return node.argument !== argument - || node.qualifier !== qualifier - || node.typeArguments !== typeArguments - || node.isTypeOf !== isTypeOf - ? updateNode(createImportTypeNode(argument, qualifier, typeArguments, isTypeOf), node) - : node; - } - - export function createParenthesizedType(type: TypeNode) { - const node = createSynthesizedNode(SyntaxKind.ParenthesizedType); - node.type = type; - return node; - } - - export function updateParenthesizedType(node: ParenthesizedTypeNode, type: TypeNode) { - return node.type !== type - ? updateNode(createParenthesizedType(type), node) - : node; - } - - export function createThisTypeNode() { - return createSynthesizedNode(SyntaxKind.ThisType); - } - - export function createTypeOperatorNode(type: TypeNode): TypeOperatorNode; - export function createTypeOperatorNode(operator: SyntaxKind.KeyOfKeyword | SyntaxKind.UniqueKeyword | SyntaxKind.ReadonlyKeyword, type: TypeNode): TypeOperatorNode; - export function createTypeOperatorNode(operatorOrType: SyntaxKind.KeyOfKeyword | SyntaxKind.UniqueKeyword | SyntaxKind.ReadonlyKeyword | TypeNode, type?: TypeNode) { - const node = createSynthesizedNode(SyntaxKind.TypeOperator) as TypeOperatorNode; - node.operator = typeof operatorOrType === "number" ? operatorOrType : SyntaxKind.KeyOfKeyword; - node.type = parenthesizeElementTypeMember(typeof operatorOrType === "number" ? type! : operatorOrType); - return node; - } - - export function updateTypeOperatorNode(node: TypeOperatorNode, type: TypeNode) { - return node.type !== type ? updateNode(createTypeOperatorNode(node.operator, type), node) : node; - } - - export function createIndexedAccessTypeNode(objectType: TypeNode, indexType: TypeNode) { - const node = createSynthesizedNode(SyntaxKind.IndexedAccessType) as IndexedAccessTypeNode; - node.objectType = parenthesizeElementTypeMember(objectType); - node.indexType = indexType; - return node; - } - - export function updateIndexedAccessTypeNode(node: IndexedAccessTypeNode, objectType: TypeNode, indexType: TypeNode) { - return node.objectType !== objectType - || node.indexType !== indexType - ? updateNode(createIndexedAccessTypeNode(objectType, indexType), node) - : node; - } - - export function createMappedTypeNode(readonlyToken: ReadonlyToken | PlusToken | MinusToken | undefined, typeParameter: TypeParameterDeclaration, questionToken: QuestionToken | PlusToken | MinusToken | undefined, type: TypeNode | undefined): MappedTypeNode { - const node = createSynthesizedNode(SyntaxKind.MappedType) as MappedTypeNode; - node.readonlyToken = readonlyToken; - node.typeParameter = typeParameter; - node.questionToken = questionToken; - node.type = type; - return node; - } - - export function updateMappedTypeNode(node: MappedTypeNode, readonlyToken: ReadonlyToken | PlusToken | MinusToken | undefined, typeParameter: TypeParameterDeclaration, questionToken: QuestionToken | PlusToken | MinusToken | undefined, type: TypeNode | undefined): MappedTypeNode { - return node.readonlyToken !== readonlyToken - || node.typeParameter !== typeParameter - || node.questionToken !== questionToken - || node.type !== type - ? updateNode(createMappedTypeNode(readonlyToken, typeParameter, questionToken, type), node) - : node; - } - - export function createLiteralTypeNode(literal: LiteralTypeNode["literal"]) { - const node = createSynthesizedNode(SyntaxKind.LiteralType) as LiteralTypeNode; - node.literal = literal; - return node; - } - - export function updateLiteralTypeNode(node: LiteralTypeNode, literal: LiteralTypeNode["literal"]) { - return node.literal !== literal - ? updateNode(createLiteralTypeNode(literal), node) - : node; - } - - // Binding Patterns - - export function createObjectBindingPattern(elements: readonly BindingElement[]) { - const node = createSynthesizedNode(SyntaxKind.ObjectBindingPattern); - node.elements = createNodeArray(elements); - return node; - } - - export function updateObjectBindingPattern(node: ObjectBindingPattern, elements: readonly BindingElement[]) { - return node.elements !== elements - ? updateNode(createObjectBindingPattern(elements), node) - : node; - } - - export function createArrayBindingPattern(elements: readonly ArrayBindingElement[]) { - const node = createSynthesizedNode(SyntaxKind.ArrayBindingPattern); - node.elements = createNodeArray(elements); - return node; - } - - export function updateArrayBindingPattern(node: ArrayBindingPattern, elements: readonly ArrayBindingElement[]) { - return node.elements !== elements - ? updateNode(createArrayBindingPattern(elements), node) - : node; - } - - export function createBindingElement(dotDotDotToken: DotDotDotToken | undefined, propertyName: string | PropertyName | undefined, name: string | BindingName, initializer?: Expression) { - const node = createSynthesizedNode(SyntaxKind.BindingElement); - node.dotDotDotToken = dotDotDotToken; - node.propertyName = asName(propertyName); - node.name = asName(name); - node.initializer = initializer; - return node; - } - - export function updateBindingElement(node: BindingElement, dotDotDotToken: DotDotDotToken | undefined, propertyName: PropertyName | undefined, name: BindingName, initializer: Expression | undefined) { - return node.propertyName !== propertyName - || node.dotDotDotToken !== dotDotDotToken - || node.name !== name - || node.initializer !== initializer - ? updateNode(createBindingElement(dotDotDotToken, propertyName, name, initializer), node) - : node; - } - - // Expression - - export function createArrayLiteral(elements?: readonly Expression[], multiLine?: boolean) { - const node = createSynthesizedNode(SyntaxKind.ArrayLiteralExpression); - node.elements = parenthesizeListElements(createNodeArray(elements)); - if (multiLine) node.multiLine = true; - return node; - } - - export function updateArrayLiteral(node: ArrayLiteralExpression, elements: readonly Expression[]) { - return node.elements !== elements - ? updateNode(createArrayLiteral(elements, node.multiLine), node) - : node; - } - - export function createObjectLiteral(properties?: readonly ObjectLiteralElementLike[], multiLine?: boolean) { - const node = createSynthesizedNode(SyntaxKind.ObjectLiteralExpression); - node.properties = createNodeArray(properties); - if (multiLine) node.multiLine = true; - return node; - } - - export function updateObjectLiteral(node: ObjectLiteralExpression, properties: readonly ObjectLiteralElementLike[]) { - return node.properties !== properties - ? updateNode(createObjectLiteral(properties, node.multiLine), node) - : node; - } - - export function createPropertyAccess(expression: Expression, name: string | Identifier | PrivateIdentifier) { - const node = createSynthesizedNode(SyntaxKind.PropertyAccessExpression); - node.expression = parenthesizeForAccess(expression); - node.name = asName(name); - setEmitFlags(node, EmitFlags.NoIndentation); - return node; - } - - export function updatePropertyAccess(node: PropertyAccessExpression, expression: Expression, name: Identifier | PrivateIdentifier) { - if (isOptionalChain(node) && isIdentifier(node.name) && isIdentifier(name)) { - // Not sure why this cast was necessary: the previous line should already establish that node.name is an identifier - const theNode = node as (typeof node & { name: Identifier }); - return updatePropertyAccessChain(theNode, expression, node.questionDotToken, name); - } - // Because we are updating existed propertyAccess we want to inherit its emitFlags - // instead of using the default from createPropertyAccess - return node.expression !== expression - || node.name !== name - ? updateNode(setEmitFlags(createPropertyAccess(expression, name), getEmitFlags(node)), node) - : node; - } - - export function createPropertyAccessChain(expression: Expression, questionDotToken: QuestionDotToken | undefined, name: string | Identifier) { - const node = createSynthesizedNode(SyntaxKind.PropertyAccessExpression); - node.flags |= NodeFlags.OptionalChain; - node.expression = parenthesizeForAccess(expression); - node.questionDotToken = questionDotToken; - node.name = asName(name); - setEmitFlags(node, EmitFlags.NoIndentation); - return node; - } - - export function updatePropertyAccessChain(node: PropertyAccessChain, expression: Expression, questionDotToken: QuestionDotToken | undefined, name: Identifier) { - Debug.assert(!!(node.flags & NodeFlags.OptionalChain), "Cannot update a PropertyAccessExpression using updatePropertyAccessChain. Use updatePropertyAccess instead."); - // Because we are updating an existing PropertyAccessChain we want to inherit its emitFlags - // instead of using the default from createPropertyAccess - return node.expression !== expression - || node.questionDotToken !== questionDotToken - || node.name !== name - ? updateNode(setEmitFlags(createPropertyAccessChain(expression, questionDotToken, name), getEmitFlags(node)), node) - : node; - } - - export function createElementAccess(expression: Expression, index: number | Expression) { - const node = createSynthesizedNode(SyntaxKind.ElementAccessExpression); - node.expression = parenthesizeForAccess(expression); - node.argumentExpression = asExpression(index); - return node; - } - - export function updateElementAccess(node: ElementAccessExpression, expression: Expression, argumentExpression: Expression) { - if (isOptionalChain(node)) { - return updateElementAccessChain(node, expression, node.questionDotToken, argumentExpression); - } - return node.expression !== expression - || node.argumentExpression !== argumentExpression - ? updateNode(createElementAccess(expression, argumentExpression), node) - : node; - } - - export function createElementAccessChain(expression: Expression, questionDotToken: QuestionDotToken | undefined, index: number | Expression) { - const node = createSynthesizedNode(SyntaxKind.ElementAccessExpression); - node.flags |= NodeFlags.OptionalChain; - node.expression = parenthesizeForAccess(expression); - node.questionDotToken = questionDotToken; - node.argumentExpression = asExpression(index); - return node; - } - - export function updateElementAccessChain(node: ElementAccessChain, expression: Expression, questionDotToken: QuestionDotToken | undefined, argumentExpression: Expression) { - Debug.assert(!!(node.flags & NodeFlags.OptionalChain), "Cannot update an ElementAccessExpression using updateElementAccessChain. Use updateElementAccess instead."); - return node.expression !== expression - || node.questionDotToken !== questionDotToken - || node.argumentExpression !== argumentExpression - ? updateNode(createElementAccessChain(expression, questionDotToken, argumentExpression), node) - : node; - } - - export function createCall(expression: Expression, typeArguments: readonly TypeNode[] | undefined, argumentsArray: readonly Expression[] | undefined) { - const node = createSynthesizedNode(SyntaxKind.CallExpression); - node.expression = parenthesizeForAccess(expression); - node.typeArguments = asNodeArray(typeArguments); - node.arguments = parenthesizeListElements(createNodeArray(argumentsArray)); - return node; - } - - export function updateCall(node: CallExpression, expression: Expression, typeArguments: readonly TypeNode[] | undefined, argumentsArray: readonly Expression[]) { - if (isOptionalChain(node)) { - return updateCallChain(node, expression, node.questionDotToken, typeArguments, argumentsArray); - } - return node.expression !== expression - || node.typeArguments !== typeArguments - || node.arguments !== argumentsArray - ? updateNode(createCall(expression, typeArguments, argumentsArray), node) - : node; - } - - export function createCallChain(expression: Expression, questionDotToken: QuestionDotToken | undefined, typeArguments: readonly TypeNode[] | undefined, argumentsArray: readonly Expression[] | undefined) { - const node = createSynthesizedNode(SyntaxKind.CallExpression); - node.flags |= NodeFlags.OptionalChain; - node.expression = parenthesizeForAccess(expression); - node.questionDotToken = questionDotToken; - node.typeArguments = asNodeArray(typeArguments); - node.arguments = parenthesizeListElements(createNodeArray(argumentsArray)); - return node; - } - - export function updateCallChain(node: CallChain, expression: Expression, questionDotToken: QuestionDotToken | undefined, typeArguments: readonly TypeNode[] | undefined, argumentsArray: readonly Expression[]) { - Debug.assert(!!(node.flags & NodeFlags.OptionalChain), "Cannot update a CallExpression using updateCallChain. Use updateCall instead."); - return node.expression !== expression - || node.questionDotToken !== questionDotToken - || node.typeArguments !== typeArguments - || node.arguments !== argumentsArray - ? updateNode(createCallChain(expression, questionDotToken, typeArguments, argumentsArray), node) - : node; - } - - export function createNew(expression: Expression, typeArguments: readonly TypeNode[] | undefined, argumentsArray: readonly Expression[] | undefined) { - const node = createSynthesizedNode(SyntaxKind.NewExpression); - node.expression = parenthesizeForNew(expression); - node.typeArguments = asNodeArray(typeArguments); - node.arguments = argumentsArray ? parenthesizeListElements(createNodeArray(argumentsArray)) : undefined; - return node; - } - - export function updateNew(node: NewExpression, expression: Expression, typeArguments: readonly TypeNode[] | undefined, argumentsArray: readonly Expression[] | undefined) { - return node.expression !== expression - || node.typeArguments !== typeArguments - || node.arguments !== argumentsArray - ? updateNode(createNew(expression, typeArguments, argumentsArray), node) - : node; - } - - /** @deprecated */ export function createTaggedTemplate(tag: Expression, template: TemplateLiteral): TaggedTemplateExpression; - export function createTaggedTemplate(tag: Expression, typeArguments: readonly TypeNode[] | undefined, template: TemplateLiteral): TaggedTemplateExpression; - /** @internal */ - export function createTaggedTemplate(tag: Expression, typeArgumentsOrTemplate: readonly TypeNode[] | TemplateLiteral | undefined, template?: TemplateLiteral): TaggedTemplateExpression; - export function createTaggedTemplate(tag: Expression, typeArgumentsOrTemplate: readonly TypeNode[] | TemplateLiteral | undefined, template?: TemplateLiteral) { - const node = createSynthesizedNode(SyntaxKind.TaggedTemplateExpression); - node.tag = parenthesizeForAccess(tag); - if (template) { - node.typeArguments = asNodeArray(typeArgumentsOrTemplate as readonly TypeNode[]); - node.template = template; - } - else { - node.typeArguments = undefined; - node.template = typeArgumentsOrTemplate as TemplateLiteral; - } - return node; - } - - /** @deprecated */ export function updateTaggedTemplate(node: TaggedTemplateExpression, tag: Expression, template: TemplateLiteral): TaggedTemplateExpression; - export function updateTaggedTemplate(node: TaggedTemplateExpression, tag: Expression, typeArguments: readonly TypeNode[] | undefined, template: TemplateLiteral): TaggedTemplateExpression; - export function updateTaggedTemplate(node: TaggedTemplateExpression, tag: Expression, typeArgumentsOrTemplate: readonly TypeNode[] | TemplateLiteral | undefined, template?: TemplateLiteral) { - return node.tag !== tag - || (template - ? node.typeArguments !== typeArgumentsOrTemplate || node.template !== template - : node.typeArguments !== undefined || node.template !== typeArgumentsOrTemplate) - ? updateNode(createTaggedTemplate(tag, typeArgumentsOrTemplate, template), node) - : node; - } - - export function createTypeAssertion(type: TypeNode, expression: Expression) { - const node = createSynthesizedNode(SyntaxKind.TypeAssertionExpression); - node.type = type; - node.expression = parenthesizePrefixOperand(expression); - return node; - } - - export function updateTypeAssertion(node: TypeAssertion, type: TypeNode, expression: Expression) { - return node.type !== type - || node.expression !== expression - ? updateNode(createTypeAssertion(type, expression), node) - : node; - } - - export function createParen(expression: Expression) { - const node = createSynthesizedNode(SyntaxKind.ParenthesizedExpression); - node.expression = expression; - return node; - } - - export function updateParen(node: ParenthesizedExpression, expression: Expression) { - return node.expression !== expression - ? updateNode(createParen(expression), node) - : node; - } - - export function createFunctionExpression( - modifiers: readonly Modifier[] | undefined, - asteriskToken: AsteriskToken | undefined, - name: string | Identifier | undefined, - typeParameters: readonly TypeParameterDeclaration[] | undefined, - parameters: readonly ParameterDeclaration[] | undefined, - type: TypeNode | undefined, - body: Block) { - const node = createSynthesizedNode(SyntaxKind.FunctionExpression); - node.modifiers = asNodeArray(modifiers); - node.asteriskToken = asteriskToken; - node.name = asName(name); - node.typeParameters = asNodeArray(typeParameters); - node.parameters = createNodeArray(parameters); - node.type = type; - node.body = body; - return node; - } - - export function updateFunctionExpression( - node: FunctionExpression, - modifiers: readonly Modifier[] | undefined, - asteriskToken: AsteriskToken | undefined, - name: Identifier | undefined, - typeParameters: readonly TypeParameterDeclaration[] | undefined, - parameters: readonly ParameterDeclaration[], - type: TypeNode | undefined, - body: Block) { - return node.name !== name - || node.modifiers !== modifiers - || node.asteriskToken !== asteriskToken - || node.typeParameters !== typeParameters - || node.parameters !== parameters - || node.type !== type - || node.body !== body - ? updateNode(createFunctionExpression(modifiers, asteriskToken, name, typeParameters, parameters, type, body), node) - : node; - } - - export function createArrowFunction( - modifiers: readonly Modifier[] | undefined, - typeParameters: readonly TypeParameterDeclaration[] | undefined, - parameters: readonly ParameterDeclaration[], - type: TypeNode | undefined, - equalsGreaterThanToken: EqualsGreaterThanToken | undefined, - body: ConciseBody) { - const node = createSynthesizedNode(SyntaxKind.ArrowFunction); - node.modifiers = asNodeArray(modifiers); - node.typeParameters = asNodeArray(typeParameters); - node.parameters = createNodeArray(parameters); - node.type = type; - node.equalsGreaterThanToken = equalsGreaterThanToken || createToken(SyntaxKind.EqualsGreaterThanToken); - node.body = parenthesizeConciseBody(body); - return node; - } - export function updateArrowFunction( - node: ArrowFunction, - modifiers: readonly Modifier[] | undefined, - typeParameters: readonly TypeParameterDeclaration[] | undefined, - parameters: readonly ParameterDeclaration[], - type: TypeNode | undefined, - equalsGreaterThanToken: Token, - body: ConciseBody - ): ArrowFunction { - return node.modifiers !== modifiers - || node.typeParameters !== typeParameters - || node.parameters !== parameters - || node.type !== type - || node.equalsGreaterThanToken !== equalsGreaterThanToken - || node.body !== body - ? updateNode(createArrowFunction(modifiers, typeParameters, parameters, type, equalsGreaterThanToken, body), node) - : node; - } - - export function createDelete(expression: Expression) { - const node = createSynthesizedNode(SyntaxKind.DeleteExpression); - node.expression = parenthesizePrefixOperand(expression); - return node; - } - - export function updateDelete(node: DeleteExpression, expression: Expression) { - return node.expression !== expression - ? updateNode(createDelete(expression), node) - : node; - } - - export function createTypeOf(expression: Expression) { - const node = createSynthesizedNode(SyntaxKind.TypeOfExpression); - node.expression = parenthesizePrefixOperand(expression); - return node; - } - - export function updateTypeOf(node: TypeOfExpression, expression: Expression) { - return node.expression !== expression - ? updateNode(createTypeOf(expression), node) - : node; - } - - export function createVoid(expression: Expression) { - const node = createSynthesizedNode(SyntaxKind.VoidExpression); - node.expression = parenthesizePrefixOperand(expression); - return node; - } - - export function updateVoid(node: VoidExpression, expression: Expression) { - return node.expression !== expression - ? updateNode(createVoid(expression), node) - : node; - } - - export function createAwait(expression: Expression) { - const node = createSynthesizedNode(SyntaxKind.AwaitExpression); - node.expression = parenthesizePrefixOperand(expression); - return node; - } - - export function updateAwait(node: AwaitExpression, expression: Expression) { - return node.expression !== expression - ? updateNode(createAwait(expression), node) - : node; - } - - export function createPrefix(operator: PrefixUnaryOperator, operand: Expression) { - const node = createSynthesizedNode(SyntaxKind.PrefixUnaryExpression); - node.operator = operator; - node.operand = parenthesizePrefixOperand(operand); - return node; - } - - export function updatePrefix(node: PrefixUnaryExpression, operand: Expression) { - return node.operand !== operand - ? updateNode(createPrefix(node.operator, operand), node) - : node; - } - - export function createPostfix(operand: Expression, operator: PostfixUnaryOperator) { - const node = createSynthesizedNode(SyntaxKind.PostfixUnaryExpression); - node.operand = parenthesizePostfixOperand(operand); - node.operator = operator; - return node; - } - - export function updatePostfix(node: PostfixUnaryExpression, operand: Expression) { - return node.operand !== operand - ? updateNode(createPostfix(operand, node.operator), node) - : node; - } - - export function createBinary(left: Expression, operator: BinaryOperator | BinaryOperatorToken, right: Expression) { - const node = createSynthesizedNode(SyntaxKind.BinaryExpression); - const operatorToken = asToken(operator); - const operatorKind = operatorToken.kind; - node.left = parenthesizeBinaryOperand(operatorKind, left, /*isLeftSideOfBinary*/ true, /*leftOperand*/ undefined); - node.operatorToken = operatorToken; - node.right = parenthesizeBinaryOperand(operatorKind, right, /*isLeftSideOfBinary*/ false, node.left); - return node; - } - - export function updateBinary(node: BinaryExpression, left: Expression, right: Expression, operator?: BinaryOperator | BinaryOperatorToken) { - return node.left !== left - || node.right !== right - ? updateNode(createBinary(left, operator || node.operatorToken, right), node) - : node; - } - - /** @deprecated */ export function createConditional(condition: Expression, whenTrue: Expression, whenFalse: Expression): ConditionalExpression; - export function createConditional(condition: Expression, questionToken: QuestionToken, whenTrue: Expression, colonToken: ColonToken, whenFalse: Expression): ConditionalExpression; - export function createConditional(condition: Expression, questionTokenOrWhenTrue: QuestionToken | Expression, whenTrueOrWhenFalse: Expression, colonToken?: ColonToken, whenFalse?: Expression) { - const node = createSynthesizedNode(SyntaxKind.ConditionalExpression); - node.condition = parenthesizeForConditionalHead(condition); - node.questionToken = whenFalse ? questionTokenOrWhenTrue : createToken(SyntaxKind.QuestionToken); - node.whenTrue = parenthesizeSubexpressionOfConditionalExpression(whenFalse ? whenTrueOrWhenFalse : questionTokenOrWhenTrue); - node.colonToken = whenFalse ? colonToken! : createToken(SyntaxKind.ColonToken); - node.whenFalse = parenthesizeSubexpressionOfConditionalExpression(whenFalse ? whenFalse : whenTrueOrWhenFalse); - return node; - } - export function updateConditional( - node: ConditionalExpression, - condition: Expression, - questionToken: Token, - whenTrue: Expression, - colonToken: Token, - whenFalse: Expression - ): ConditionalExpression { - return node.condition !== condition - || node.questionToken !== questionToken - || node.whenTrue !== whenTrue - || node.colonToken !== colonToken - || node.whenFalse !== whenFalse - ? updateNode(createConditional(condition, questionToken, whenTrue, colonToken, whenFalse), node) - : node; - } - - export function createTemplateExpression(head: TemplateHead, templateSpans: readonly TemplateSpan[]) { - const node = createSynthesizedNode(SyntaxKind.TemplateExpression); - node.head = head; - node.templateSpans = createNodeArray(templateSpans); - return node; - } - - export function updateTemplateExpression(node: TemplateExpression, head: TemplateHead, templateSpans: readonly TemplateSpan[]) { - return node.head !== head - || node.templateSpans !== templateSpans - ? updateNode(createTemplateExpression(head, templateSpans), node) - : node; - } - - let rawTextScanner: Scanner | undefined; - const invalidValueSentinel: object = {}; - - function getCookedText(kind: TemplateLiteralToken["kind"], rawText: string) { - if (!rawTextScanner) { - rawTextScanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ false, LanguageVariant.Standard); - } - switch (kind) { - case SyntaxKind.NoSubstitutionTemplateLiteral: - rawTextScanner.setText("`" + rawText + "`"); - break; - case SyntaxKind.TemplateHead: - rawTextScanner.setText("`" + rawText + "${"); - break; - case SyntaxKind.TemplateMiddle: - rawTextScanner.setText("}" + rawText + "${"); - break; - case SyntaxKind.TemplateTail: - rawTextScanner.setText("}" + rawText + "`"); - break; - } - - let token = rawTextScanner.scan(); - if (token === SyntaxKind.CloseBracketToken) { - token = rawTextScanner.reScanTemplateToken(/* isTaggedTemplate */ false); - } - - if (rawTextScanner.isUnterminated()) { - rawTextScanner.setText(undefined); - return invalidValueSentinel; - } - - let tokenValue: string | undefined; - switch (token) { - case SyntaxKind.NoSubstitutionTemplateLiteral: - case SyntaxKind.TemplateHead: - case SyntaxKind.TemplateMiddle: - case SyntaxKind.TemplateTail: - tokenValue = rawTextScanner.getTokenValue(); - break; - } - - if (rawTextScanner.scan() !== SyntaxKind.EndOfFileToken) { - rawTextScanner.setText(undefined); - return invalidValueSentinel; - } - - rawTextScanner.setText(undefined); - return tokenValue; - } - - function createTemplateLiteralLikeNode(kind: TemplateLiteralToken["kind"], text: string, rawText: string | undefined) { - const node = createSynthesizedNode(kind); - node.text = text; - if (rawText === undefined || text === rawText) { - node.rawText = rawText; - } - else { - const cooked = getCookedText(kind, rawText); - if (typeof cooked === "object") { - return Debug.fail("Invalid raw text"); - } - - Debug.assert(text === cooked, "Expected argument 'text' to be the normalized (i.e. 'cooked') version of argument 'rawText'."); - node.rawText = rawText; - } - return node; - } - - export function createTemplateHead(text: string, rawText?: string) { - const node = createTemplateLiteralLikeNode(SyntaxKind.TemplateHead, text, rawText); - node.text = text; - return node; - } - - export function createTemplateMiddle(text: string, rawText?: string) { - const node = createTemplateLiteralLikeNode(SyntaxKind.TemplateMiddle, text, rawText); - node.text = text; - return node; - } - - export function createTemplateTail(text: string, rawText?: string) { - const node = createTemplateLiteralLikeNode(SyntaxKind.TemplateTail, text, rawText); - node.text = text; - return node; - } - - export function createNoSubstitutionTemplateLiteral(text: string, rawText?: string) { - const node = createTemplateLiteralLikeNode(SyntaxKind.NoSubstitutionTemplateLiteral, text, rawText); - return node; - } - - export function createYield(expression?: Expression): YieldExpression; - export function createYield(asteriskToken: AsteriskToken | undefined, expression: Expression): YieldExpression; - export function createYield(asteriskTokenOrExpression?: AsteriskToken | undefined | Expression, expression?: Expression) { - const node = createSynthesizedNode(SyntaxKind.YieldExpression); - node.asteriskToken = asteriskTokenOrExpression && asteriskTokenOrExpression.kind === SyntaxKind.AsteriskToken ? asteriskTokenOrExpression : undefined; - node.expression = asteriskTokenOrExpression && asteriskTokenOrExpression.kind !== SyntaxKind.AsteriskToken ? asteriskTokenOrExpression : expression; - return node; - } - - export function updateYield(node: YieldExpression, asteriskToken: AsteriskToken | undefined, expression: Expression) { - return node.expression !== expression - || node.asteriskToken !== asteriskToken - ? updateNode(createYield(asteriskToken, expression), node) - : node; - } - - export function createSpread(expression: Expression) { - const node = createSynthesizedNode(SyntaxKind.SpreadElement); - node.expression = parenthesizeExpressionForList(expression); - return node; - } - - export function updateSpread(node: SpreadElement, expression: Expression) { - return node.expression !== expression - ? updateNode(createSpread(expression), node) - : node; - } - - export function createClassExpression( - modifiers: readonly Modifier[] | undefined, - name: string | Identifier | undefined, - typeParameters: readonly TypeParameterDeclaration[] | undefined, - heritageClauses: readonly HeritageClause[] | undefined, - members: readonly ClassElement[]) { - const node = createSynthesizedNode(SyntaxKind.ClassExpression); - node.decorators = undefined; - node.modifiers = asNodeArray(modifiers); - node.name = asName(name); - node.typeParameters = asNodeArray(typeParameters); - node.heritageClauses = asNodeArray(heritageClauses); - node.members = createNodeArray(members); - return node; - } - - export function updateClassExpression( - node: ClassExpression, - modifiers: readonly Modifier[] | undefined, - name: Identifier | undefined, - typeParameters: readonly TypeParameterDeclaration[] | undefined, - heritageClauses: readonly HeritageClause[] | undefined, - members: readonly ClassElement[]) { - return node.modifiers !== modifiers - || node.name !== name - || node.typeParameters !== typeParameters - || node.heritageClauses !== heritageClauses - || node.members !== members - ? updateNode(createClassExpression(modifiers, name, typeParameters, heritageClauses, members), node) - : node; - } - - export function createOmittedExpression() { - return createSynthesizedNode(SyntaxKind.OmittedExpression); - } - - export function createExpressionWithTypeArguments(typeArguments: readonly TypeNode[] | undefined, expression: Expression) { - const node = createSynthesizedNode(SyntaxKind.ExpressionWithTypeArguments); - node.expression = parenthesizeForAccess(expression); - node.typeArguments = asNodeArray(typeArguments); - return node; - } - - export function updateExpressionWithTypeArguments(node: ExpressionWithTypeArguments, typeArguments: readonly TypeNode[] | undefined, expression: Expression) { - return node.typeArguments !== typeArguments - || node.expression !== expression - ? updateNode(createExpressionWithTypeArguments(typeArguments, expression), node) - : node; - } - - export function createAsExpression(expression: Expression, type: TypeNode) { - const node = createSynthesizedNode(SyntaxKind.AsExpression); - node.expression = expression; - node.type = type; - return node; - } - - export function updateAsExpression(node: AsExpression, expression: Expression, type: TypeNode) { - return node.expression !== expression - || node.type !== type - ? updateNode(createAsExpression(expression, type), node) - : node; - } - - export function createNonNullExpression(expression: Expression) { - const node = createSynthesizedNode(SyntaxKind.NonNullExpression); - node.expression = parenthesizeForAccess(expression); - return node; - } - - export function updateNonNullExpression(node: NonNullExpression, expression: Expression) { - return node.expression !== expression - ? updateNode(createNonNullExpression(expression), node) - : node; - } - - export function createMetaProperty(keywordToken: MetaProperty["keywordToken"], name: Identifier) { - const node = createSynthesizedNode(SyntaxKind.MetaProperty); - node.keywordToken = keywordToken; - node.name = name; - return node; - } - - export function updateMetaProperty(node: MetaProperty, name: Identifier) { - return node.name !== name - ? updateNode(createMetaProperty(node.keywordToken, name), node) - : node; - } - - // Misc - - export function createTemplateSpan(expression: Expression, literal: TemplateMiddle | TemplateTail) { - const node = createSynthesizedNode(SyntaxKind.TemplateSpan); - node.expression = expression; - node.literal = literal; - return node; - } - - export function updateTemplateSpan(node: TemplateSpan, expression: Expression, literal: TemplateMiddle | TemplateTail) { - return node.expression !== expression - || node.literal !== literal - ? updateNode(createTemplateSpan(expression, literal), node) - : node; - } - - export function createSemicolonClassElement() { - return createSynthesizedNode(SyntaxKind.SemicolonClassElement); - } - - // Element - - export function createBlock(statements: readonly Statement[], multiLine?: boolean): Block { - const block = createSynthesizedNode(SyntaxKind.Block); - block.statements = createNodeArray(statements); - if (multiLine) block.multiLine = multiLine; - return block; - } - - export function updateBlock(node: Block, statements: readonly Statement[]) { - return node.statements !== statements - ? updateNode(createBlock(statements, node.multiLine), node) - : node; - } - - export function createVariableStatement(modifiers: readonly Modifier[] | undefined, declarationList: VariableDeclarationList | readonly VariableDeclaration[]) { - const node = createSynthesizedNode(SyntaxKind.VariableStatement); - node.decorators = undefined; - node.modifiers = asNodeArray(modifiers); - node.declarationList = isArray(declarationList) ? createVariableDeclarationList(declarationList) : declarationList; - return node; - } - - export function updateVariableStatement(node: VariableStatement, modifiers: readonly Modifier[] | undefined, declarationList: VariableDeclarationList) { - return node.modifiers !== modifiers - || node.declarationList !== declarationList - ? updateNode(createVariableStatement(modifiers, declarationList), node) - : node; - } - - export function createEmptyStatement() { - return createSynthesizedNode(SyntaxKind.EmptyStatement); - } - - export function createExpressionStatement(expression: Expression): ExpressionStatement { - const node = createSynthesizedNode(SyntaxKind.ExpressionStatement); - node.expression = parenthesizeExpressionForExpressionStatement(expression); - return node; - } - - export function updateExpressionStatement(node: ExpressionStatement, expression: Expression) { - return node.expression !== expression - ? updateNode(createExpressionStatement(expression), node) - : node; - } - - /** @deprecated Use `createExpressionStatement` instead. */ - export const createStatement = createExpressionStatement; - /** @deprecated Use `updateExpressionStatement` instead. */ - export const updateStatement = updateExpressionStatement; - - export function createIf(expression: Expression, thenStatement: Statement, elseStatement?: Statement) { - const node = createSynthesizedNode(SyntaxKind.IfStatement); - node.expression = expression; - node.thenStatement = asEmbeddedStatement(thenStatement); - node.elseStatement = asEmbeddedStatement(elseStatement); - return node; - } - - export function updateIf(node: IfStatement, expression: Expression, thenStatement: Statement, elseStatement: Statement | undefined) { - return node.expression !== expression - || node.thenStatement !== thenStatement - || node.elseStatement !== elseStatement - ? updateNode(createIf(expression, thenStatement, elseStatement), node) - : node; - } - - export function createDo(statement: Statement, expression: Expression) { - const node = createSynthesizedNode(SyntaxKind.DoStatement); - node.statement = asEmbeddedStatement(statement); - node.expression = expression; - return node; - } - - export function updateDo(node: DoStatement, statement: Statement, expression: Expression) { - return node.statement !== statement - || node.expression !== expression - ? updateNode(createDo(statement, expression), node) - : node; - } - - export function createWhile(expression: Expression, statement: Statement) { - const node = createSynthesizedNode(SyntaxKind.WhileStatement); - node.expression = expression; - node.statement = asEmbeddedStatement(statement); - return node; - } - - export function updateWhile(node: WhileStatement, expression: Expression, statement: Statement) { - return node.expression !== expression - || node.statement !== statement - ? updateNode(createWhile(expression, statement), node) - : node; - } - - export function createFor(initializer: ForInitializer | undefined, condition: Expression | undefined, incrementor: Expression | undefined, statement: Statement) { - const node = createSynthesizedNode(SyntaxKind.ForStatement); - node.initializer = initializer; - node.condition = condition; - node.incrementor = incrementor; - node.statement = asEmbeddedStatement(statement); - return node; - } - - export function updateFor(node: ForStatement, initializer: ForInitializer | undefined, condition: Expression | undefined, incrementor: Expression | undefined, statement: Statement) { - return node.initializer !== initializer - || node.condition !== condition - || node.incrementor !== incrementor - || node.statement !== statement - ? updateNode(createFor(initializer, condition, incrementor, statement), node) - : node; - } - - export function createForIn(initializer: ForInitializer, expression: Expression, statement: Statement) { - const node = createSynthesizedNode(SyntaxKind.ForInStatement); - node.initializer = initializer; - node.expression = expression; - node.statement = asEmbeddedStatement(statement); - return node; - } - - export function updateForIn(node: ForInStatement, initializer: ForInitializer, expression: Expression, statement: Statement) { - return node.initializer !== initializer - || node.expression !== expression - || node.statement !== statement - ? updateNode(createForIn(initializer, expression, statement), node) - : node; - } - - export function createForOf(awaitModifier: AwaitKeywordToken | undefined, initializer: ForInitializer, expression: Expression, statement: Statement) { - const node = createSynthesizedNode(SyntaxKind.ForOfStatement); - node.awaitModifier = awaitModifier; - node.initializer = initializer; - node.expression = isCommaSequence(expression) ? createParen(expression) : expression; - node.statement = asEmbeddedStatement(statement); - return node; - } - - export function updateForOf(node: ForOfStatement, awaitModifier: AwaitKeywordToken | undefined, initializer: ForInitializer, expression: Expression, statement: Statement) { - return node.awaitModifier !== awaitModifier - || node.initializer !== initializer - || node.expression !== expression - || node.statement !== statement - ? updateNode(createForOf(awaitModifier, initializer, expression, statement), node) - : node; - } - - export function createContinue(label?: string | Identifier): ContinueStatement { - const node = createSynthesizedNode(SyntaxKind.ContinueStatement); - node.label = asName(label); - return node; - } - - export function updateContinue(node: ContinueStatement, label: Identifier | undefined) { - return node.label !== label - ? updateNode(createContinue(label), node) - : node; - } - - export function createBreak(label?: string | Identifier): BreakStatement { - const node = createSynthesizedNode(SyntaxKind.BreakStatement); - node.label = asName(label); - return node; - } - - export function updateBreak(node: BreakStatement, label: Identifier | undefined) { - return node.label !== label - ? updateNode(createBreak(label), node) - : node; - } - - export function createReturn(expression?: Expression): ReturnStatement { - const node = createSynthesizedNode(SyntaxKind.ReturnStatement); - node.expression = expression; - return node; - } - - export function updateReturn(node: ReturnStatement, expression: Expression | undefined) { - return node.expression !== expression - ? updateNode(createReturn(expression), node) - : node; - } - - export function createWith(expression: Expression, statement: Statement) { - const node = createSynthesizedNode(SyntaxKind.WithStatement); - node.expression = expression; - node.statement = asEmbeddedStatement(statement); - return node; - } - - export function updateWith(node: WithStatement, expression: Expression, statement: Statement) { - return node.expression !== expression - || node.statement !== statement - ? updateNode(createWith(expression, statement), node) - : node; - } - - export function createSwitch(expression: Expression, caseBlock: CaseBlock): SwitchStatement { - const node = createSynthesizedNode(SyntaxKind.SwitchStatement); - node.expression = parenthesizeExpressionForList(expression); - node.caseBlock = caseBlock; - return node; - } - - export function updateSwitch(node: SwitchStatement, expression: Expression, caseBlock: CaseBlock) { - return node.expression !== expression - || node.caseBlock !== caseBlock - ? updateNode(createSwitch(expression, caseBlock), node) - : node; - } - - export function createLabel(label: string | Identifier, statement: Statement) { - const node = createSynthesizedNode(SyntaxKind.LabeledStatement); - node.label = asName(label); - node.statement = asEmbeddedStatement(statement); - return node; - } - - export function updateLabel(node: LabeledStatement, label: Identifier, statement: Statement) { - return node.label !== label - || node.statement !== statement - ? updateNode(createLabel(label, statement), node) - : node; - } - - export function createThrow(expression: Expression) { - const node = createSynthesizedNode(SyntaxKind.ThrowStatement); - node.expression = expression; - return node; - } - - export function updateThrow(node: ThrowStatement, expression: Expression) { - return node.expression !== expression - ? updateNode(createThrow(expression), node) - : node; - } - - export function createTry(tryBlock: Block, catchClause: CatchClause | undefined, finallyBlock: Block | undefined) { - const node = createSynthesizedNode(SyntaxKind.TryStatement); - node.tryBlock = tryBlock; - node.catchClause = catchClause; - node.finallyBlock = finallyBlock; - return node; - } - - export function updateTry(node: TryStatement, tryBlock: Block, catchClause: CatchClause | undefined, finallyBlock: Block | undefined) { - return node.tryBlock !== tryBlock - || node.catchClause !== catchClause - || node.finallyBlock !== finallyBlock - ? updateNode(createTry(tryBlock, catchClause, finallyBlock), node) - : node; - } - - export function createDebuggerStatement() { - return createSynthesizedNode(SyntaxKind.DebuggerStatement); - } - - export function createVariableDeclaration(name: string | BindingName, type?: TypeNode, initializer?: Expression) { - /* Internally, one should probably use createTypeScriptVariableDeclaration instead and handle definite assignment assertions */ - const node = createSynthesizedNode(SyntaxKind.VariableDeclaration); - node.name = asName(name); - node.type = type; - node.initializer = initializer !== undefined ? parenthesizeExpressionForList(initializer) : undefined; - return node; - } - - export function updateVariableDeclaration(node: VariableDeclaration, name: BindingName, type: TypeNode | undefined, initializer: Expression | undefined) { - /* Internally, one should probably use updateTypeScriptVariableDeclaration instead and handle definite assignment assertions */ - return node.name !== name - || node.type !== type - || node.initializer !== initializer - ? updateNode(createVariableDeclaration(name, type, initializer), node) - : node; - } - - /* @internal */ - export function createTypeScriptVariableDeclaration(name: string | BindingName, exclaimationToken?: Token, type?: TypeNode, initializer?: Expression) { - const node = createSynthesizedNode(SyntaxKind.VariableDeclaration); - node.name = asName(name); - node.type = type; - node.initializer = initializer !== undefined ? parenthesizeExpressionForList(initializer) : undefined; - node.exclamationToken = exclaimationToken; - return node; - } - - /* @internal */ - export function updateTypeScriptVariableDeclaration(node: VariableDeclaration, name: BindingName, exclaimationToken: Token | undefined, type: TypeNode | undefined, initializer: Expression | undefined) { - return node.name !== name - || node.type !== type - || node.initializer !== initializer - || node.exclamationToken !== exclaimationToken - ? updateNode(createTypeScriptVariableDeclaration(name, exclaimationToken, type, initializer), node) - : node; - } - - export function createVariableDeclarationList(declarations: readonly VariableDeclaration[], flags = NodeFlags.None) { - const node = createSynthesizedNode(SyntaxKind.VariableDeclarationList); - node.flags |= flags & NodeFlags.BlockScoped; - node.declarations = createNodeArray(declarations); - return node; - } - - export function updateVariableDeclarationList(node: VariableDeclarationList, declarations: readonly VariableDeclaration[]) { - return node.declarations !== declarations - ? updateNode(createVariableDeclarationList(declarations, node.flags), node) - : node; - } - - export function createFunctionDeclaration( - decorators: readonly Decorator[] | undefined, - modifiers: readonly Modifier[] | undefined, - asteriskToken: AsteriskToken | undefined, - name: string | Identifier | undefined, - typeParameters: readonly TypeParameterDeclaration[] | undefined, - parameters: readonly ParameterDeclaration[], - type: TypeNode | undefined, - body: Block | undefined) { - const node = createSynthesizedNode(SyntaxKind.FunctionDeclaration); - node.decorators = asNodeArray(decorators); - node.modifiers = asNodeArray(modifiers); - node.asteriskToken = asteriskToken; - node.name = asName(name); - node.typeParameters = asNodeArray(typeParameters); - node.parameters = createNodeArray(parameters); - node.type = type; - node.body = body; - return node; - } - - export function updateFunctionDeclaration( - node: FunctionDeclaration, - decorators: readonly Decorator[] | undefined, - modifiers: readonly Modifier[] | undefined, - asteriskToken: AsteriskToken | undefined, - name: Identifier | undefined, - typeParameters: readonly TypeParameterDeclaration[] | undefined, - parameters: readonly ParameterDeclaration[], - type: TypeNode | undefined, - body: Block | undefined) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.asteriskToken !== asteriskToken - || node.name !== name - || node.typeParameters !== typeParameters - || node.parameters !== parameters - || node.type !== type - || node.body !== body - ? updateNode(createFunctionDeclaration(decorators, modifiers, asteriskToken, name, typeParameters, parameters, type, body), node) - : node; - } - - export function createClassDeclaration( - decorators: readonly Decorator[] | undefined, - modifiers: readonly Modifier[] | undefined, - name: string | Identifier | undefined, - typeParameters: readonly TypeParameterDeclaration[] | undefined, - heritageClauses: readonly HeritageClause[] | undefined, - members: readonly ClassElement[]) { - const node = createSynthesizedNode(SyntaxKind.ClassDeclaration); - node.decorators = asNodeArray(decorators); - node.modifiers = asNodeArray(modifiers); - node.name = asName(name); - node.typeParameters = asNodeArray(typeParameters); - node.heritageClauses = asNodeArray(heritageClauses); - node.members = createNodeArray(members); - return node; - } - - export function updateClassDeclaration( - node: ClassDeclaration, - decorators: readonly Decorator[] | undefined, - modifiers: readonly Modifier[] | undefined, - name: Identifier | undefined, - typeParameters: readonly TypeParameterDeclaration[] | undefined, - heritageClauses: readonly HeritageClause[] | undefined, - members: readonly ClassElement[]) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.name !== name - || node.typeParameters !== typeParameters - || node.heritageClauses !== heritageClauses - || node.members !== members - ? updateNode(createClassDeclaration(decorators, modifiers, name, typeParameters, heritageClauses, members), node) - : node; - } - - export function createInterfaceDeclaration( - decorators: readonly Decorator[] | undefined, - modifiers: readonly Modifier[] | undefined, - name: string | Identifier, - typeParameters: readonly TypeParameterDeclaration[] | undefined, - heritageClauses: readonly HeritageClause[] | undefined, - members: readonly TypeElement[]) { - const node = createSynthesizedNode(SyntaxKind.InterfaceDeclaration); - node.decorators = asNodeArray(decorators); - node.modifiers = asNodeArray(modifiers); - node.name = asName(name); - node.typeParameters = asNodeArray(typeParameters); - node.heritageClauses = asNodeArray(heritageClauses); - node.members = createNodeArray(members); - return node; - } - - export function updateInterfaceDeclaration( - node: InterfaceDeclaration, - decorators: readonly Decorator[] | undefined, - modifiers: readonly Modifier[] | undefined, - name: Identifier, - typeParameters: readonly TypeParameterDeclaration[] | undefined, - heritageClauses: readonly HeritageClause[] | undefined, - members: readonly TypeElement[]) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.name !== name - || node.typeParameters !== typeParameters - || node.heritageClauses !== heritageClauses - || node.members !== members - ? updateNode(createInterfaceDeclaration(decorators, modifiers, name, typeParameters, heritageClauses, members), node) - : node; - } - - export function createTypeAliasDeclaration( - decorators: readonly Decorator[] | undefined, - modifiers: readonly Modifier[] | undefined, - name: string | Identifier, - typeParameters: readonly TypeParameterDeclaration[] | undefined, - type: TypeNode) { - const node = createSynthesizedNode(SyntaxKind.TypeAliasDeclaration); - node.decorators = asNodeArray(decorators); - node.modifiers = asNodeArray(modifiers); - node.name = asName(name); - node.typeParameters = asNodeArray(typeParameters); - node.type = type; - return node; - } - - export function updateTypeAliasDeclaration( - node: TypeAliasDeclaration, - decorators: readonly Decorator[] | undefined, - modifiers: readonly Modifier[] | undefined, - name: Identifier, - typeParameters: readonly TypeParameterDeclaration[] | undefined, - type: TypeNode) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.name !== name - || node.typeParameters !== typeParameters - || node.type !== type - ? updateNode(createTypeAliasDeclaration(decorators, modifiers, name, typeParameters, type), node) - : node; - } - - export function createEnumDeclaration( - decorators: readonly Decorator[] | undefined, - modifiers: readonly Modifier[] | undefined, - name: string | Identifier, - members: readonly EnumMember[]) { - const node = createSynthesizedNode(SyntaxKind.EnumDeclaration); - node.decorators = asNodeArray(decorators); - node.modifiers = asNodeArray(modifiers); - node.name = asName(name); - node.members = createNodeArray(members); - return node; - } - - export function updateEnumDeclaration( - node: EnumDeclaration, - decorators: readonly Decorator[] | undefined, - modifiers: readonly Modifier[] | undefined, - name: Identifier, - members: readonly EnumMember[]) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.name !== name - || node.members !== members - ? updateNode(createEnumDeclaration(decorators, modifiers, name, members), node) - : node; - } - - export function createModuleDeclaration(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, name: ModuleName, body: ModuleBody | undefined, flags = NodeFlags.None) { - const node = createSynthesizedNode(SyntaxKind.ModuleDeclaration); - node.flags |= flags & (NodeFlags.Namespace | NodeFlags.NestedNamespace | NodeFlags.GlobalAugmentation); - node.decorators = asNodeArray(decorators); - node.modifiers = asNodeArray(modifiers); - node.name = name; - node.body = body; - return node; - } - - export function updateModuleDeclaration(node: ModuleDeclaration, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, name: ModuleName, body: ModuleBody | undefined) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.name !== name - || node.body !== body - ? updateNode(createModuleDeclaration(decorators, modifiers, name, body, node.flags), node) - : node; - } - - export function createModuleBlock(statements: readonly Statement[]) { - const node = createSynthesizedNode(SyntaxKind.ModuleBlock); - node.statements = createNodeArray(statements); - return node; - } - - export function updateModuleBlock(node: ModuleBlock, statements: readonly Statement[]) { - return node.statements !== statements - ? updateNode(createModuleBlock(statements), node) - : node; - } - - export function createCaseBlock(clauses: readonly CaseOrDefaultClause[]): CaseBlock { - const node = createSynthesizedNode(SyntaxKind.CaseBlock); - node.clauses = createNodeArray(clauses); - return node; - } - - export function updateCaseBlock(node: CaseBlock, clauses: readonly CaseOrDefaultClause[]) { - return node.clauses !== clauses - ? updateNode(createCaseBlock(clauses), node) - : node; - } - - export function createNamespaceExportDeclaration(name: string | Identifier) { - const node = createSynthesizedNode(SyntaxKind.NamespaceExportDeclaration); - node.name = asName(name); - return node; - } - - export function updateNamespaceExportDeclaration(node: NamespaceExportDeclaration, name: Identifier) { - return node.name !== name - ? updateNode(createNamespaceExportDeclaration(name), node) - : node; - } - - export function createImportEqualsDeclaration(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, name: string | Identifier, moduleReference: ModuleReference) { - const node = createSynthesizedNode(SyntaxKind.ImportEqualsDeclaration); - node.decorators = asNodeArray(decorators); - node.modifiers = asNodeArray(modifiers); - node.name = asName(name); - node.moduleReference = moduleReference; - return node; - } - - export function updateImportEqualsDeclaration(node: ImportEqualsDeclaration, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, name: Identifier, moduleReference: ModuleReference) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.name !== name - || node.moduleReference !== moduleReference - ? updateNode(createImportEqualsDeclaration(decorators, modifiers, name, moduleReference), node) - : node; - } - - export function createImportDeclaration( - decorators: readonly Decorator[] | undefined, - modifiers: readonly Modifier[] | undefined, - importClause: ImportClause | undefined, - moduleSpecifier: Expression): ImportDeclaration { - const node = createSynthesizedNode(SyntaxKind.ImportDeclaration); - node.decorators = asNodeArray(decorators); - node.modifiers = asNodeArray(modifiers); - node.importClause = importClause; - node.moduleSpecifier = moduleSpecifier; - return node; - } - - export function updateImportDeclaration( - node: ImportDeclaration, - decorators: readonly Decorator[] | undefined, - modifiers: readonly Modifier[] | undefined, - importClause: ImportClause | undefined, - moduleSpecifier: Expression) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.importClause !== importClause - || node.moduleSpecifier !== moduleSpecifier - ? updateNode(createImportDeclaration(decorators, modifiers, importClause, moduleSpecifier), node) - : node; - } - - export function createImportClause(name: Identifier | undefined, namedBindings: NamedImportBindings | undefined, isTypeOnly = false): ImportClause { - const node = createSynthesizedNode(SyntaxKind.ImportClause); - node.name = name; - node.namedBindings = namedBindings; - node.isTypeOnly = isTypeOnly; - return node; - } - - export function updateImportClause(node: ImportClause, name: Identifier | undefined, namedBindings: NamedImportBindings | undefined, isTypeOnly: boolean) { - return node.name !== name - || node.namedBindings !== namedBindings - || node.isTypeOnly !== isTypeOnly - ? updateNode(createImportClause(name, namedBindings, isTypeOnly), node) - : node; - } - - export function createNamespaceImport(name: Identifier): NamespaceImport { - const node = createSynthesizedNode(SyntaxKind.NamespaceImport); - node.name = name; - return node; - } - - export function createNamespaceExport(name: Identifier): NamespaceExport { - const node = createSynthesizedNode(SyntaxKind.NamespaceExport); - node.name = name; - return node; - } - - export function updateNamespaceImport(node: NamespaceImport, name: Identifier) { - return node.name !== name - ? updateNode(createNamespaceImport(name), node) - : node; - } - - export function updateNamespaceExport(node: NamespaceExport, name: Identifier) { - return node.name !== name - ? updateNode(createNamespaceExport(name), node) - : node; - } - - export function createNamedImports(elements: readonly ImportSpecifier[]): NamedImports { - const node = createSynthesizedNode(SyntaxKind.NamedImports); - node.elements = createNodeArray(elements); - return node; - } - - export function updateNamedImports(node: NamedImports, elements: readonly ImportSpecifier[]) { - return node.elements !== elements - ? updateNode(createNamedImports(elements), node) - : node; - } - - export function createImportSpecifier(propertyName: Identifier | undefined, name: Identifier) { - const node = createSynthesizedNode(SyntaxKind.ImportSpecifier); - node.propertyName = propertyName; - node.name = name; - return node; - } - - export function updateImportSpecifier(node: ImportSpecifier, propertyName: Identifier | undefined, name: Identifier) { - return node.propertyName !== propertyName - || node.name !== name - ? updateNode(createImportSpecifier(propertyName, name), node) - : node; - } - - export function createExportAssignment(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, isExportEquals: boolean | undefined, expression: Expression) { - const node = createSynthesizedNode(SyntaxKind.ExportAssignment); - node.decorators = asNodeArray(decorators); - node.modifiers = asNodeArray(modifiers); - node.isExportEquals = isExportEquals; - node.expression = isExportEquals ? parenthesizeBinaryOperand(SyntaxKind.EqualsToken, expression, /*isLeftSideOfBinary*/ false, /*leftOperand*/ undefined) : parenthesizeDefaultExpression(expression); - return node; - } - - export function updateExportAssignment(node: ExportAssignment, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, expression: Expression) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.expression !== expression - ? updateNode(createExportAssignment(decorators, modifiers, node.isExportEquals, expression), node) - : node; - } - - export function createExportDeclaration(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, exportClause: NamedExportBindings | undefined, moduleSpecifier?: Expression, isTypeOnly = false) { - const node = createSynthesizedNode(SyntaxKind.ExportDeclaration); - node.decorators = asNodeArray(decorators); - node.modifiers = asNodeArray(modifiers); - node.isTypeOnly = isTypeOnly; - node.exportClause = exportClause; - node.moduleSpecifier = moduleSpecifier; - return node; - } - - export function updateExportDeclaration( - node: ExportDeclaration, - decorators: readonly Decorator[] | undefined, - modifiers: readonly Modifier[] | undefined, - exportClause: NamedExportBindings | undefined, - moduleSpecifier: Expression | undefined, - isTypeOnly: boolean) { - return node.decorators !== decorators - || node.modifiers !== modifiers - || node.isTypeOnly !== isTypeOnly - || node.exportClause !== exportClause - || node.moduleSpecifier !== moduleSpecifier - ? updateNode(createExportDeclaration(decorators, modifiers, exportClause, moduleSpecifier, isTypeOnly), node) - : node; - } - - /* @internal */ - export function createEmptyExports() { - return createExportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, createNamedExports([]), /*moduleSpecifier*/ undefined); - } - - export function createNamedExports(elements: readonly ExportSpecifier[]) { - const node = createSynthesizedNode(SyntaxKind.NamedExports); - node.elements = createNodeArray(elements); - return node; - } - - export function updateNamedExports(node: NamedExports, elements: readonly ExportSpecifier[]) { - return node.elements !== elements - ? updateNode(createNamedExports(elements), node) - : node; - } - - export function createExportSpecifier(propertyName: string | Identifier | undefined, name: string | Identifier) { - const node = createSynthesizedNode(SyntaxKind.ExportSpecifier); - node.propertyName = asName(propertyName); - node.name = asName(name); - return node; - } - - export function updateExportSpecifier(node: ExportSpecifier, propertyName: Identifier | undefined, name: Identifier) { - return node.propertyName !== propertyName - || node.name !== name - ? updateNode(createExportSpecifier(propertyName, name), node) - : node; - } - - // Module references - - export function createExternalModuleReference(expression: Expression) { - const node = createSynthesizedNode(SyntaxKind.ExternalModuleReference); - node.expression = expression; - return node; - } - - export function updateExternalModuleReference(node: ExternalModuleReference, expression: Expression) { - return node.expression !== expression - ? updateNode(createExternalModuleReference(expression), node) - : node; - } - - // JSDoc - - /* @internal */ - export function createJSDocTypeExpression(type: TypeNode): JSDocTypeExpression { - const node = createSynthesizedNode(SyntaxKind.JSDocTypeExpression) as JSDocTypeExpression; - node.type = type; - return node; - } - - /* @internal */ - export function createJSDocTypeTag(typeExpression: JSDocTypeExpression, comment?: string): JSDocTypeTag { - const tag = createJSDocTag(SyntaxKind.JSDocTypeTag, "type"); - tag.typeExpression = typeExpression; - tag.comment = comment; - return tag; - } - - /* @internal */ - export function createJSDocReturnTag(typeExpression?: JSDocTypeExpression, comment?: string): JSDocReturnTag { - const tag = createJSDocTag(SyntaxKind.JSDocReturnTag, "returns"); - tag.typeExpression = typeExpression; - tag.comment = comment; - return tag; - } - - /** @internal */ - export function createJSDocThisTag(typeExpression?: JSDocTypeExpression): JSDocThisTag { - const tag = createJSDocTag(SyntaxKind.JSDocThisTag, "this"); - tag.typeExpression = typeExpression; - return tag; - } - - /* @internal */ - export function createJSDocParamTag(name: EntityName, isBracketed: boolean, typeExpression?: JSDocTypeExpression, comment?: string): JSDocParameterTag { - const tag = createJSDocTag(SyntaxKind.JSDocParameterTag, "param"); - tag.typeExpression = typeExpression; - tag.name = name; - tag.isBracketed = isBracketed; - tag.comment = comment; - return tag; - } - - /* @internal */ - export function createJSDocComment(comment?: string | undefined, tags?: NodeArray | undefined) { - const node = createSynthesizedNode(SyntaxKind.JSDocComment) as JSDoc; - node.comment = comment; - node.tags = tags; - return node; - } - - /* @internal */ - function createJSDocTag(kind: T["kind"], tagName: string): T { - const node = createSynthesizedNode(kind) as T; - node.tagName = createIdentifier(tagName); - return node; - } - - // JSX - - export function createJsxElement(openingElement: JsxOpeningElement, children: readonly JsxChild[], closingElement: JsxClosingElement) { - const node = createSynthesizedNode(SyntaxKind.JsxElement); - node.openingElement = openingElement; - node.children = createNodeArray(children); - node.closingElement = closingElement; - return node; - } - - export function updateJsxElement(node: JsxElement, openingElement: JsxOpeningElement, children: readonly JsxChild[], closingElement: JsxClosingElement) { - return node.openingElement !== openingElement - || node.children !== children - || node.closingElement !== closingElement - ? updateNode(createJsxElement(openingElement, children, closingElement), node) - : node; - } - - export function createJsxSelfClosingElement(tagName: JsxTagNameExpression, typeArguments: readonly TypeNode[] | undefined, attributes: JsxAttributes) { - const node = createSynthesizedNode(SyntaxKind.JsxSelfClosingElement); - node.tagName = tagName; - node.typeArguments = asNodeArray(typeArguments); - node.attributes = attributes; - return node; - } - - export function updateJsxSelfClosingElement(node: JsxSelfClosingElement, tagName: JsxTagNameExpression, typeArguments: readonly TypeNode[] | undefined, attributes: JsxAttributes) { - return node.tagName !== tagName - || node.typeArguments !== typeArguments - || node.attributes !== attributes - ? updateNode(createJsxSelfClosingElement(tagName, typeArguments, attributes), node) - : node; - } - - export function createJsxOpeningElement(tagName: JsxTagNameExpression, typeArguments: readonly TypeNode[] | undefined, attributes: JsxAttributes) { - const node = createSynthesizedNode(SyntaxKind.JsxOpeningElement); - node.tagName = tagName; - node.typeArguments = asNodeArray(typeArguments); - node.attributes = attributes; - return node; - } - - export function updateJsxOpeningElement(node: JsxOpeningElement, tagName: JsxTagNameExpression, typeArguments: readonly TypeNode[] | undefined, attributes: JsxAttributes) { - return node.tagName !== tagName - || node.typeArguments !== typeArguments - || node.attributes !== attributes - ? updateNode(createJsxOpeningElement(tagName, typeArguments, attributes), node) - : node; - } - - export function createJsxClosingElement(tagName: JsxTagNameExpression) { - const node = createSynthesizedNode(SyntaxKind.JsxClosingElement); - node.tagName = tagName; - return node; - } - - export function updateJsxClosingElement(node: JsxClosingElement, tagName: JsxTagNameExpression) { - return node.tagName !== tagName - ? updateNode(createJsxClosingElement(tagName), node) - : node; - } - - export function createJsxFragment(openingFragment: JsxOpeningFragment, children: readonly JsxChild[], closingFragment: JsxClosingFragment) { - const node = createSynthesizedNode(SyntaxKind.JsxFragment); - node.openingFragment = openingFragment; - node.children = createNodeArray(children); - node.closingFragment = closingFragment; - return node; - } - - export function createJsxText(text: string, containsOnlyTriviaWhiteSpaces?: boolean) { - const node = createSynthesizedNode(SyntaxKind.JsxText); - node.text = text; - node.containsOnlyTriviaWhiteSpaces = !!containsOnlyTriviaWhiteSpaces; - return node; - } - - export function updateJsxText(node: JsxText, text: string, containsOnlyTriviaWhiteSpaces?: boolean) { - return node.text !== text - || node.containsOnlyTriviaWhiteSpaces !== containsOnlyTriviaWhiteSpaces - ? updateNode(createJsxText(text, containsOnlyTriviaWhiteSpaces), node) - : node; - } - - export function createJsxOpeningFragment() { - return createSynthesizedNode(SyntaxKind.JsxOpeningFragment); - } - - export function createJsxJsxClosingFragment() { - return createSynthesizedNode(SyntaxKind.JsxClosingFragment); - } - - export function updateJsxFragment(node: JsxFragment, openingFragment: JsxOpeningFragment, children: readonly JsxChild[], closingFragment: JsxClosingFragment) { - return node.openingFragment !== openingFragment - || node.children !== children - || node.closingFragment !== closingFragment - ? updateNode(createJsxFragment(openingFragment, children, closingFragment), node) - : node; - } - - export function createJsxAttribute(name: Identifier, initializer: StringLiteral | JsxExpression) { - const node = createSynthesizedNode(SyntaxKind.JsxAttribute); - node.name = name; - node.initializer = initializer; - return node; - } - - export function updateJsxAttribute(node: JsxAttribute, name: Identifier, initializer: StringLiteral | JsxExpression) { - return node.name !== name - || node.initializer !== initializer - ? updateNode(createJsxAttribute(name, initializer), node) - : node; - } - - export function createJsxAttributes(properties: readonly JsxAttributeLike[]) { - const node = createSynthesizedNode(SyntaxKind.JsxAttributes); - node.properties = createNodeArray(properties); - return node; - } - - export function updateJsxAttributes(node: JsxAttributes, properties: readonly JsxAttributeLike[]) { - return node.properties !== properties - ? updateNode(createJsxAttributes(properties), node) - : node; - } - - export function createJsxSpreadAttribute(expression: Expression) { - const node = createSynthesizedNode(SyntaxKind.JsxSpreadAttribute); - node.expression = expression; - return node; - } - - export function updateJsxSpreadAttribute(node: JsxSpreadAttribute, expression: Expression) { - return node.expression !== expression - ? updateNode(createJsxSpreadAttribute(expression), node) - : node; - } - - export function createJsxExpression(dotDotDotToken: DotDotDotToken | undefined, expression: Expression | undefined) { - const node = createSynthesizedNode(SyntaxKind.JsxExpression); - node.dotDotDotToken = dotDotDotToken; - node.expression = expression; - return node; - } - - export function updateJsxExpression(node: JsxExpression, expression: Expression | undefined) { - return node.expression !== expression - ? updateNode(createJsxExpression(node.dotDotDotToken, expression), node) - : node; - } - - // Clauses - - export function createCaseClause(expression: Expression, statements: readonly Statement[]) { - const node = createSynthesizedNode(SyntaxKind.CaseClause); - node.expression = parenthesizeExpressionForList(expression); - node.statements = createNodeArray(statements); - return node; - } - - export function updateCaseClause(node: CaseClause, expression: Expression, statements: readonly Statement[]) { - return node.expression !== expression - || node.statements !== statements - ? updateNode(createCaseClause(expression, statements), node) - : node; - } - - export function createDefaultClause(statements: readonly Statement[]) { - const node = createSynthesizedNode(SyntaxKind.DefaultClause); - node.statements = createNodeArray(statements); - return node; - } - - export function updateDefaultClause(node: DefaultClause, statements: readonly Statement[]) { - return node.statements !== statements - ? updateNode(createDefaultClause(statements), node) - : node; - } - - export function createHeritageClause(token: HeritageClause["token"], types: readonly ExpressionWithTypeArguments[]) { - const node = createSynthesizedNode(SyntaxKind.HeritageClause); - node.token = token; - node.types = createNodeArray(types); - return node; - } - - export function updateHeritageClause(node: HeritageClause, types: readonly ExpressionWithTypeArguments[]) { - return node.types !== types - ? updateNode(createHeritageClause(node.token, types), node) - : node; - } - - export function createCatchClause(variableDeclaration: string | VariableDeclaration | undefined, block: Block) { - const node = createSynthesizedNode(SyntaxKind.CatchClause); - node.variableDeclaration = isString(variableDeclaration) ? createVariableDeclaration(variableDeclaration) : variableDeclaration; - node.block = block; - return node; - } - - export function updateCatchClause(node: CatchClause, variableDeclaration: VariableDeclaration | undefined, block: Block) { - return node.variableDeclaration !== variableDeclaration - || node.block !== block - ? updateNode(createCatchClause(variableDeclaration, block), node) - : node; - } - - // Property assignments - - export function createPropertyAssignment(name: string | PropertyName, initializer: Expression) { - const node = createSynthesizedNode(SyntaxKind.PropertyAssignment); - node.name = asName(name); - node.questionToken = undefined; - node.initializer = parenthesizeExpressionForList(initializer); - return node; - } - - export function updatePropertyAssignment(node: PropertyAssignment, name: PropertyName, initializer: Expression) { - return node.name !== name - || node.initializer !== initializer - ? updateNode(createPropertyAssignment(name, initializer), node) - : node; - } - - export function createShorthandPropertyAssignment(name: string | Identifier, objectAssignmentInitializer?: Expression) { - const node = createSynthesizedNode(SyntaxKind.ShorthandPropertyAssignment); - node.name = asName(name); - node.objectAssignmentInitializer = objectAssignmentInitializer !== undefined ? parenthesizeExpressionForList(objectAssignmentInitializer) : undefined; - return node; - } - - export function updateShorthandPropertyAssignment(node: ShorthandPropertyAssignment, name: Identifier, objectAssignmentInitializer: Expression | undefined) { - return node.name !== name - || node.objectAssignmentInitializer !== objectAssignmentInitializer - ? updateNode(createShorthandPropertyAssignment(name, objectAssignmentInitializer), node) - : node; - } - - export function createSpreadAssignment(expression: Expression) { - const node = createSynthesizedNode(SyntaxKind.SpreadAssignment); - node.expression = parenthesizeExpressionForList(expression); - return node; - } - - export function updateSpreadAssignment(node: SpreadAssignment, expression: Expression) { - return node.expression !== expression - ? updateNode(createSpreadAssignment(expression), node) - : node; - } - - // Enum - - export function createEnumMember(name: string | PropertyName, initializer?: Expression) { - const node = createSynthesizedNode(SyntaxKind.EnumMember); - node.name = asName(name); - node.initializer = initializer && parenthesizeExpressionForList(initializer); - return node; - } - - export function updateEnumMember(node: EnumMember, name: PropertyName, initializer: Expression | undefined) { - return node.name !== name - || node.initializer !== initializer - ? updateNode(createEnumMember(name, initializer), node) - : node; - } - - // Top-level nodes - - export function updateSourceFileNode(node: SourceFile, statements: readonly Statement[], isDeclarationFile?: boolean, referencedFiles?: SourceFile["referencedFiles"], typeReferences?: SourceFile["typeReferenceDirectives"], hasNoDefaultLib?: boolean, libReferences?: SourceFile["libReferenceDirectives"]) { - if ( - node.statements !== statements || - (isDeclarationFile !== undefined && node.isDeclarationFile !== isDeclarationFile) || - (referencedFiles !== undefined && node.referencedFiles !== referencedFiles) || - (typeReferences !== undefined && node.typeReferenceDirectives !== typeReferences) || - (libReferences !== undefined && node.libReferenceDirectives !== libReferences) || - (hasNoDefaultLib !== undefined && node.hasNoDefaultLib !== hasNoDefaultLib) - ) { - const updated = createSynthesizedNode(SyntaxKind.SourceFile); - updated.flags |= node.flags; - updated.statements = createNodeArray(statements); - updated.endOfFileToken = node.endOfFileToken; - updated.fileName = node.fileName; - updated.path = node.path; - updated.text = node.text; - updated.isDeclarationFile = isDeclarationFile === undefined ? node.isDeclarationFile : isDeclarationFile; - updated.referencedFiles = referencedFiles === undefined ? node.referencedFiles : referencedFiles; - updated.typeReferenceDirectives = typeReferences === undefined ? node.typeReferenceDirectives : typeReferences; - updated.hasNoDefaultLib = hasNoDefaultLib === undefined ? node.hasNoDefaultLib : hasNoDefaultLib; - updated.libReferenceDirectives = libReferences === undefined ? node.libReferenceDirectives : libReferences; - if (node.amdDependencies !== undefined) updated.amdDependencies = node.amdDependencies; - if (node.moduleName !== undefined) updated.moduleName = node.moduleName; - if (node.languageVariant !== undefined) updated.languageVariant = node.languageVariant; - if (node.renamedDependencies !== undefined) updated.renamedDependencies = node.renamedDependencies; - if (node.languageVersion !== undefined) updated.languageVersion = node.languageVersion; - if (node.scriptKind !== undefined) updated.scriptKind = node.scriptKind; - if (node.externalModuleIndicator !== undefined) updated.externalModuleIndicator = node.externalModuleIndicator; - if (node.commonJsModuleIndicator !== undefined) updated.commonJsModuleIndicator = node.commonJsModuleIndicator; - if (node.identifiers !== undefined) updated.identifiers = node.identifiers; - if (node.nodeCount !== undefined) updated.nodeCount = node.nodeCount; - if (node.identifierCount !== undefined) updated.identifierCount = node.identifierCount; - if (node.symbolCount !== undefined) updated.symbolCount = node.symbolCount; - if (node.parseDiagnostics !== undefined) updated.parseDiagnostics = node.parseDiagnostics; - if (node.bindDiagnostics !== undefined) updated.bindDiagnostics = node.bindDiagnostics; - if (node.bindSuggestionDiagnostics !== undefined) updated.bindSuggestionDiagnostics = node.bindSuggestionDiagnostics; - if (node.lineMap !== undefined) updated.lineMap = node.lineMap; - if (node.classifiableNames !== undefined) updated.classifiableNames = node.classifiableNames; - if (node.resolvedModules !== undefined) updated.resolvedModules = node.resolvedModules; - if (node.resolvedTypeReferenceDirectiveNames !== undefined) updated.resolvedTypeReferenceDirectiveNames = node.resolvedTypeReferenceDirectiveNames; - if (node.imports !== undefined) updated.imports = node.imports; - if (node.moduleAugmentations !== undefined) updated.moduleAugmentations = node.moduleAugmentations; - if (node.pragmas !== undefined) updated.pragmas = node.pragmas; - if (node.localJsxFactory !== undefined) updated.localJsxFactory = node.localJsxFactory; - if (node.localJsxNamespace !== undefined) updated.localJsxNamespace = node.localJsxNamespace; - return updateNode(updated, node); - } - - return node; - } - - /** - * Creates a shallow, memberwise clone of a node for mutation. - */ - export function getMutableClone(node: T): T { - const clone = getSynthesizedClone(node); - clone.pos = node.pos; - clone.end = node.end; - clone.parent = node.parent; - return clone; - } - - // Transformation nodes - - /** - * Creates a synthetic statement to act as a placeholder for a not-emitted statement in - * order to preserve comments. - * - * @param original The original statement. - */ - export function createNotEmittedStatement(original: Node) { - const node = createSynthesizedNode(SyntaxKind.NotEmittedStatement); - node.original = original; - setTextRange(node, original); - return node; - } - - /** - * Creates a synthetic element to act as a placeholder for the end of an emitted declaration in - * order to properly emit exports. - */ - /* @internal */ - export function createEndOfDeclarationMarker(original: Node) { - const node = createSynthesizedNode(SyntaxKind.EndOfDeclarationMarker); - node.emitNode = {} as EmitNode; - node.original = original; - return node; - } - - /** - * Creates a synthetic element to act as a placeholder for the beginning of a merged declaration in - * order to properly emit exports. - */ - /* @internal */ - export function createMergeDeclarationMarker(original: Node) { - const node = createSynthesizedNode(SyntaxKind.MergeDeclarationMarker); - node.emitNode = {} as EmitNode; - node.original = original; - return node; - } - - /** - * Creates a synthetic expression to act as a placeholder for a not-emitted expression in - * order to preserve comments or sourcemap positions. - * - * @param expression The inner expression to emit. - * @param original The original outer expression. - * @param location The location for the expression. Defaults to the positions from "original" if provided. - */ - export function createPartiallyEmittedExpression(expression: Expression, original?: Node) { - const node = createSynthesizedNode(SyntaxKind.PartiallyEmittedExpression); - node.expression = expression; - node.original = original; - setTextRange(node, original); - return node; - } - - export function updatePartiallyEmittedExpression(node: PartiallyEmittedExpression, expression: Expression) { - if (node.expression !== expression) { - return updateNode(createPartiallyEmittedExpression(expression, node.original), node); - } - return node; - } - - function flattenCommaElements(node: Expression): Expression | readonly Expression[] { - if (nodeIsSynthesized(node) && !isParseTreeNode(node) && !node.original && !node.emitNode && !node.id) { - if (node.kind === SyntaxKind.CommaListExpression) { - return (node).elements; - } - if (isBinaryExpression(node) && node.operatorToken.kind === SyntaxKind.CommaToken) { - return [node.left, node.right]; - } - } - return node; - } - - export function createCommaList(elements: readonly Expression[]) { - const node = createSynthesizedNode(SyntaxKind.CommaListExpression); - node.elements = createNodeArray(sameFlatMap(elements, flattenCommaElements)); - return node; - } - - export function updateCommaList(node: CommaListExpression, elements: readonly Expression[]) { - return node.elements !== elements - ? updateNode(createCommaList(elements), node) - : node; - } - - /* @internal */ - export function createSyntheticReferenceExpression(expression: Expression, thisArg: Expression) { - const node = createSynthesizedNode(SyntaxKind.SyntheticReferenceExpression); - node.expression = expression; - node.thisArg = thisArg; - return node; - } - - /* @internal */ - export function updateSyntheticReferenceExpression(node: SyntheticReferenceExpression, expression: Expression, thisArg: Expression) { - return node.expression !== expression - || node.thisArg !== thisArg - ? updateNode(createSyntheticReferenceExpression(expression, thisArg), node) - : node; - } - - export function createBundle(sourceFiles: readonly SourceFile[], prepends: readonly (UnparsedSource | InputFiles)[] = emptyArray) { - const node = createNode(SyntaxKind.Bundle); - node.prepends = prepends; - node.sourceFiles = sourceFiles; - return node; - } - - let allUnscopedEmitHelpers: ReadonlyMap | undefined; - function getAllUnscopedEmitHelpers() { - return allUnscopedEmitHelpers || (allUnscopedEmitHelpers = arrayToMap([ - valuesHelper, - readHelper, - spreadHelper, - spreadArraysHelper, - restHelper, - decorateHelper, - metadataHelper, - paramHelper, - awaiterHelper, - assignHelper, - awaitHelper, - asyncGeneratorHelper, - asyncDelegator, - asyncValues, - extendsHelper, - templateObjectHelper, - generatorHelper, - importStarHelper, - importDefaultHelper, - classPrivateFieldGetHelper, - classPrivateFieldSetHelper, - createBindingHelper, - setModuleDefaultHelper - ], helper => helper.name)); - } - - function createUnparsedSource() { - const node = createNode(SyntaxKind.UnparsedSource); - node.prologues = emptyArray; - node.referencedFiles = emptyArray; - node.libReferenceDirectives = emptyArray; - node.getLineAndCharacterOfPosition = pos => getLineAndCharacterOfPosition(node, pos); - return node; + return node; +} +export function createTemplateHead(text: string, rawText?: string) { + const node = (createTemplateLiteralLikeNode(SyntaxKind.TemplateHead, text, rawText)); + node.text = text; + return node; +} +export function createTemplateMiddle(text: string, rawText?: string) { + const node = (createTemplateLiteralLikeNode(SyntaxKind.TemplateMiddle, text, rawText)); + node.text = text; + return node; +} +export function createTemplateTail(text: string, rawText?: string) { + const node = (createTemplateLiteralLikeNode(SyntaxKind.TemplateTail, text, rawText)); + node.text = text; + return node; +} +export function createNoSubstitutionTemplateLiteral(text: string, rawText?: string) { + const node = (createTemplateLiteralLikeNode(SyntaxKind.NoSubstitutionTemplateLiteral, text, rawText)); + return node; +} +export function createYield(expression?: Expression): YieldExpression; +export function createYield(asteriskToken: AsteriskToken | undefined, expression: Expression): YieldExpression; +export function createYield(asteriskTokenOrExpression?: AsteriskToken | undefined | Expression, expression?: Expression) { + const node = (createSynthesizedNode(SyntaxKind.YieldExpression)); + node.asteriskToken = asteriskTokenOrExpression && asteriskTokenOrExpression.kind === SyntaxKind.AsteriskToken ? asteriskTokenOrExpression : undefined; + node.expression = asteriskTokenOrExpression && asteriskTokenOrExpression.kind !== SyntaxKind.AsteriskToken ? asteriskTokenOrExpression : expression; + return node; +} +export function updateYield(node: YieldExpression, asteriskToken: AsteriskToken | undefined, expression: Expression) { + return node.expression !== expression + || node.asteriskToken !== asteriskToken + ? updateNode(createYield(asteriskToken, expression), node) + : node; +} +export function createSpread(expression: Expression) { + const node = (createSynthesizedNode(SyntaxKind.SpreadElement)); + node.expression = parenthesizeExpressionForList(expression); + return node; +} +export function updateSpread(node: SpreadElement, expression: Expression) { + return node.expression !== expression + ? updateNode(createSpread(expression), node) + : node; +} +export function createClassExpression(modifiers: readonly Modifier[] | undefined, name: string | Identifier | undefined, typeParameters: readonly TypeParameterDeclaration[] | undefined, heritageClauses: readonly HeritageClause[] | undefined, members: readonly ClassElement[]) { + const node = (createSynthesizedNode(SyntaxKind.ClassExpression)); + node.decorators = undefined; + node.modifiers = asNodeArray(modifiers); + node.name = asName(name); + node.typeParameters = asNodeArray(typeParameters); + node.heritageClauses = asNodeArray(heritageClauses); + node.members = createNodeArray(members); + return node; +} +export function updateClassExpression(node: ClassExpression, modifiers: readonly Modifier[] | undefined, name: Identifier | undefined, typeParameters: readonly TypeParameterDeclaration[] | undefined, heritageClauses: readonly HeritageClause[] | undefined, members: readonly ClassElement[]) { + return node.modifiers !== modifiers + || node.name !== name + || node.typeParameters !== typeParameters + || node.heritageClauses !== heritageClauses + || node.members !== members + ? updateNode(createClassExpression(modifiers, name, typeParameters, heritageClauses, members), node) + : node; +} +export function createOmittedExpression() { + return createSynthesizedNode(SyntaxKind.OmittedExpression); +} +export function createExpressionWithTypeArguments(typeArguments: readonly TypeNode[] | undefined, expression: Expression) { + const node = (createSynthesizedNode(SyntaxKind.ExpressionWithTypeArguments)); + node.expression = parenthesizeForAccess(expression); + node.typeArguments = asNodeArray(typeArguments); + return node; +} +export function updateExpressionWithTypeArguments(node: ExpressionWithTypeArguments, typeArguments: readonly TypeNode[] | undefined, expression: Expression) { + return node.typeArguments !== typeArguments + || node.expression !== expression + ? updateNode(createExpressionWithTypeArguments(typeArguments, expression), node) + : node; +} +export function createAsExpression(expression: Expression, type: TypeNode) { + const node = (createSynthesizedNode(SyntaxKind.AsExpression)); + node.expression = expression; + node.type = type; + return node; +} +export function updateAsExpression(node: AsExpression, expression: Expression, type: TypeNode) { + return node.expression !== expression + || node.type !== type + ? updateNode(createAsExpression(expression, type), node) + : node; +} +export function createNonNullExpression(expression: Expression) { + const node = (createSynthesizedNode(SyntaxKind.NonNullExpression)); + node.expression = parenthesizeForAccess(expression); + return node; +} +export function updateNonNullExpression(node: NonNullExpression, expression: Expression) { + return node.expression !== expression + ? updateNode(createNonNullExpression(expression), node) + : node; +} +export function createMetaProperty(keywordToken: MetaProperty["keywordToken"], name: Identifier) { + const node = (createSynthesizedNode(SyntaxKind.MetaProperty)); + node.keywordToken = keywordToken; + node.name = name; + return node; +} +export function updateMetaProperty(node: MetaProperty, name: Identifier) { + return node.name !== name + ? updateNode(createMetaProperty(node.keywordToken, name), node) + : node; +} +// Misc +export function createTemplateSpan(expression: Expression, literal: TemplateMiddle | TemplateTail) { + const node = (createSynthesizedNode(SyntaxKind.TemplateSpan)); + node.expression = expression; + node.literal = literal; + return node; +} +export function updateTemplateSpan(node: TemplateSpan, expression: Expression, literal: TemplateMiddle | TemplateTail) { + return node.expression !== expression + || node.literal !== literal + ? updateNode(createTemplateSpan(expression, literal), node) + : node; +} +export function createSemicolonClassElement() { + return createSynthesizedNode(SyntaxKind.SemicolonClassElement); +} +// Element +export function createBlock(statements: readonly Statement[], multiLine?: boolean): Block { + const block = (createSynthesizedNode(SyntaxKind.Block)); + block.statements = createNodeArray(statements); + if (multiLine) + block.multiLine = multiLine; + return block; +} +export function updateBlock(node: Block, statements: readonly Statement[]) { + return node.statements !== statements + ? updateNode(createBlock(statements, node.multiLine), node) + : node; +} +export function createVariableStatement(modifiers: readonly Modifier[] | undefined, declarationList: VariableDeclarationList | readonly VariableDeclaration[]) { + const node = (createSynthesizedNode(SyntaxKind.VariableStatement)); + node.decorators = undefined; + node.modifiers = asNodeArray(modifiers); + node.declarationList = isArray(declarationList) ? createVariableDeclarationList(declarationList) : declarationList; + return node; +} +export function updateVariableStatement(node: VariableStatement, modifiers: readonly Modifier[] | undefined, declarationList: VariableDeclarationList) { + return node.modifiers !== modifiers + || node.declarationList !== declarationList + ? updateNode(createVariableStatement(modifiers, declarationList), node) + : node; +} +export function createEmptyStatement() { + return createSynthesizedNode(SyntaxKind.EmptyStatement); +} +export function createExpressionStatement(expression: Expression): ExpressionStatement { + const node = (createSynthesizedNode(SyntaxKind.ExpressionStatement)); + node.expression = parenthesizeExpressionForExpressionStatement(expression); + return node; +} +export function updateExpressionStatement(node: ExpressionStatement, expression: Expression) { + return node.expression !== expression + ? updateNode(createExpressionStatement(expression), node) + : node; +} +/** @deprecated Use `createExpressionStatement` instead. */ +export const createStatement = createExpressionStatement; +/** @deprecated Use `updateExpressionStatement` instead. */ +export const updateStatement = updateExpressionStatement; +export function createIf(expression: Expression, thenStatement: Statement, elseStatement?: Statement) { + const node = (createSynthesizedNode(SyntaxKind.IfStatement)); + node.expression = expression; + node.thenStatement = asEmbeddedStatement(thenStatement); + node.elseStatement = asEmbeddedStatement(elseStatement); + return node; +} +export function updateIf(node: IfStatement, expression: Expression, thenStatement: Statement, elseStatement: Statement | undefined) { + return node.expression !== expression + || node.thenStatement !== thenStatement + || node.elseStatement !== elseStatement + ? updateNode(createIf(expression, thenStatement, elseStatement), node) + : node; +} +export function createDo(statement: Statement, expression: Expression) { + const node = (createSynthesizedNode(SyntaxKind.DoStatement)); + node.statement = asEmbeddedStatement(statement); + node.expression = expression; + return node; +} +export function updateDo(node: DoStatement, statement: Statement, expression: Expression) { + return node.statement !== statement + || node.expression !== expression + ? updateNode(createDo(statement, expression), node) + : node; +} +export function createWhile(expression: Expression, statement: Statement) { + const node = (createSynthesizedNode(SyntaxKind.WhileStatement)); + node.expression = expression; + node.statement = asEmbeddedStatement(statement); + return node; +} +export function updateWhile(node: WhileStatement, expression: Expression, statement: Statement) { + return node.expression !== expression + || node.statement !== statement + ? updateNode(createWhile(expression, statement), node) + : node; +} +export function createFor(initializer: ForInitializer | undefined, condition: Expression | undefined, incrementor: Expression | undefined, statement: Statement) { + const node = (createSynthesizedNode(SyntaxKind.ForStatement)); + node.initializer = initializer; + node.condition = condition; + node.incrementor = incrementor; + node.statement = asEmbeddedStatement(statement); + return node; +} +export function updateFor(node: ForStatement, initializer: ForInitializer | undefined, condition: Expression | undefined, incrementor: Expression | undefined, statement: Statement) { + return node.initializer !== initializer + || node.condition !== condition + || node.incrementor !== incrementor + || node.statement !== statement + ? updateNode(createFor(initializer, condition, incrementor, statement), node) + : node; +} +export function createForIn(initializer: ForInitializer, expression: Expression, statement: Statement) { + const node = (createSynthesizedNode(SyntaxKind.ForInStatement)); + node.initializer = initializer; + node.expression = expression; + node.statement = asEmbeddedStatement(statement); + return node; +} +export function updateForIn(node: ForInStatement, initializer: ForInitializer, expression: Expression, statement: Statement) { + return node.initializer !== initializer + || node.expression !== expression + || node.statement !== statement + ? updateNode(createForIn(initializer, expression, statement), node) + : node; +} +export function createForOf(awaitModifier: AwaitKeywordToken | undefined, initializer: ForInitializer, expression: Expression, statement: Statement) { + const node = (createSynthesizedNode(SyntaxKind.ForOfStatement)); + node.awaitModifier = awaitModifier; + node.initializer = initializer; + node.expression = isCommaSequence(expression) ? createParen(expression) : expression; + node.statement = asEmbeddedStatement(statement); + return node; +} +export function updateForOf(node: ForOfStatement, awaitModifier: AwaitKeywordToken | undefined, initializer: ForInitializer, expression: Expression, statement: Statement) { + return node.awaitModifier !== awaitModifier + || node.initializer !== initializer + || node.expression !== expression + || node.statement !== statement + ? updateNode(createForOf(awaitModifier, initializer, expression, statement), node) + : node; +} +export function createContinue(label?: string | Identifier): ContinueStatement { + const node = (createSynthesizedNode(SyntaxKind.ContinueStatement)); + node.label = asName(label); + return node; +} +export function updateContinue(node: ContinueStatement, label: Identifier | undefined) { + return node.label !== label + ? updateNode(createContinue(label), node) + : node; +} +export function createBreak(label?: string | Identifier): BreakStatement { + const node = (createSynthesizedNode(SyntaxKind.BreakStatement)); + node.label = asName(label); + return node; +} +export function updateBreak(node: BreakStatement, label: Identifier | undefined) { + return node.label !== label + ? updateNode(createBreak(label), node) + : node; +} +export function createReturn(expression?: Expression): ReturnStatement { + const node = (createSynthesizedNode(SyntaxKind.ReturnStatement)); + node.expression = expression; + return node; +} +export function updateReturn(node: ReturnStatement, expression: Expression | undefined) { + return node.expression !== expression + ? updateNode(createReturn(expression), node) + : node; +} +export function createWith(expression: Expression, statement: Statement) { + const node = (createSynthesizedNode(SyntaxKind.WithStatement)); + node.expression = expression; + node.statement = asEmbeddedStatement(statement); + return node; +} +export function updateWith(node: WithStatement, expression: Expression, statement: Statement) { + return node.expression !== expression + || node.statement !== statement + ? updateNode(createWith(expression, statement), node) + : node; +} +export function createSwitch(expression: Expression, caseBlock: CaseBlock): SwitchStatement { + const node = (createSynthesizedNode(SyntaxKind.SwitchStatement)); + node.expression = parenthesizeExpressionForList(expression); + node.caseBlock = caseBlock; + return node; +} +export function updateSwitch(node: SwitchStatement, expression: Expression, caseBlock: CaseBlock) { + return node.expression !== expression + || node.caseBlock !== caseBlock + ? updateNode(createSwitch(expression, caseBlock), node) + : node; +} +export function createLabel(label: string | Identifier, statement: Statement) { + const node = (createSynthesizedNode(SyntaxKind.LabeledStatement)); + node.label = asName(label); + node.statement = asEmbeddedStatement(statement); + return node; +} +export function updateLabel(node: LabeledStatement, label: Identifier, statement: Statement) { + return node.label !== label + || node.statement !== statement + ? updateNode(createLabel(label, statement), node) + : node; +} +export function createThrow(expression: Expression) { + const node = (createSynthesizedNode(SyntaxKind.ThrowStatement)); + node.expression = expression; + return node; +} +export function updateThrow(node: ThrowStatement, expression: Expression) { + return node.expression !== expression + ? updateNode(createThrow(expression), node) + : node; +} +export function createTry(tryBlock: Block, catchClause: CatchClause | undefined, finallyBlock: Block | undefined) { + const node = (createSynthesizedNode(SyntaxKind.TryStatement)); + node.tryBlock = tryBlock; + node.catchClause = catchClause; + node.finallyBlock = finallyBlock; + return node; +} +export function updateTry(node: TryStatement, tryBlock: Block, catchClause: CatchClause | undefined, finallyBlock: Block | undefined) { + return node.tryBlock !== tryBlock + || node.catchClause !== catchClause + || node.finallyBlock !== finallyBlock + ? updateNode(createTry(tryBlock, catchClause, finallyBlock), node) + : node; +} +export function createDebuggerStatement() { + return createSynthesizedNode(SyntaxKind.DebuggerStatement); +} +export function createVariableDeclaration(name: string | BindingName, type?: TypeNode, initializer?: Expression) { + /* Internally, one should probably use createTypeScriptVariableDeclaration instead and handle definite assignment assertions */ + const node = (createSynthesizedNode(SyntaxKind.VariableDeclaration)); + node.name = asName(name); + node.type = type; + node.initializer = initializer !== undefined ? parenthesizeExpressionForList(initializer) : undefined; + return node; +} +export function updateVariableDeclaration(node: VariableDeclaration, name: BindingName, type: TypeNode | undefined, initializer: Expression | undefined) { + /* Internally, one should probably use updateTypeScriptVariableDeclaration instead and handle definite assignment assertions */ + return node.name !== name + || node.type !== type + || node.initializer !== initializer + ? updateNode(createVariableDeclaration(name, type, initializer), node) + : node; +} +/* @internal */ +export function createTypeScriptVariableDeclaration(name: string | BindingName, exclaimationToken?: Token, type?: TypeNode, initializer?: Expression) { + const node = (createSynthesizedNode(SyntaxKind.VariableDeclaration)); + node.name = asName(name); + node.type = type; + node.initializer = initializer !== undefined ? parenthesizeExpressionForList(initializer) : undefined; + node.exclamationToken = exclaimationToken; + return node; +} +/* @internal */ +export function updateTypeScriptVariableDeclaration(node: VariableDeclaration, name: BindingName, exclaimationToken: Token | undefined, type: TypeNode | undefined, initializer: Expression | undefined) { + return node.name !== name + || node.type !== type + || node.initializer !== initializer + || node.exclamationToken !== exclaimationToken + ? updateNode(createTypeScriptVariableDeclaration(name, exclaimationToken, type, initializer), node) + : node; +} +export function createVariableDeclarationList(declarations: readonly VariableDeclaration[], flags = NodeFlags.None) { + const node = (createSynthesizedNode(SyntaxKind.VariableDeclarationList)); + node.flags |= flags & NodeFlags.BlockScoped; + node.declarations = createNodeArray(declarations); + return node; +} +export function updateVariableDeclarationList(node: VariableDeclarationList, declarations: readonly VariableDeclaration[]) { + return node.declarations !== declarations + ? updateNode(createVariableDeclarationList(declarations, node.flags), node) + : node; +} +export function createFunctionDeclaration(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, asteriskToken: AsteriskToken | undefined, name: string | Identifier | undefined, typeParameters: readonly TypeParameterDeclaration[] | undefined, parameters: readonly ParameterDeclaration[], type: TypeNode | undefined, body: Block | undefined) { + const node = (createSynthesizedNode(SyntaxKind.FunctionDeclaration)); + node.decorators = asNodeArray(decorators); + node.modifiers = asNodeArray(modifiers); + node.asteriskToken = asteriskToken; + node.name = asName(name); + node.typeParameters = asNodeArray(typeParameters); + node.parameters = createNodeArray(parameters); + node.type = type; + node.body = body; + return node; +} +export function updateFunctionDeclaration(node: FunctionDeclaration, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, asteriskToken: AsteriskToken | undefined, name: Identifier | undefined, typeParameters: readonly TypeParameterDeclaration[] | undefined, parameters: readonly ParameterDeclaration[], type: TypeNode | undefined, body: Block | undefined) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.asteriskToken !== asteriskToken + || node.name !== name + || node.typeParameters !== typeParameters + || node.parameters !== parameters + || node.type !== type + || node.body !== body + ? updateNode(createFunctionDeclaration(decorators, modifiers, asteriskToken, name, typeParameters, parameters, type, body), node) + : node; +} +export function createClassDeclaration(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, name: string | Identifier | undefined, typeParameters: readonly TypeParameterDeclaration[] | undefined, heritageClauses: readonly HeritageClause[] | undefined, members: readonly ClassElement[]) { + const node = (createSynthesizedNode(SyntaxKind.ClassDeclaration)); + node.decorators = asNodeArray(decorators); + node.modifiers = asNodeArray(modifiers); + node.name = asName(name); + node.typeParameters = asNodeArray(typeParameters); + node.heritageClauses = asNodeArray(heritageClauses); + node.members = createNodeArray(members); + return node; +} +export function updateClassDeclaration(node: ClassDeclaration, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, name: Identifier | undefined, typeParameters: readonly TypeParameterDeclaration[] | undefined, heritageClauses: readonly HeritageClause[] | undefined, members: readonly ClassElement[]) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.name !== name + || node.typeParameters !== typeParameters + || node.heritageClauses !== heritageClauses + || node.members !== members + ? updateNode(createClassDeclaration(decorators, modifiers, name, typeParameters, heritageClauses, members), node) + : node; +} +export function createInterfaceDeclaration(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, name: string | Identifier, typeParameters: readonly TypeParameterDeclaration[] | undefined, heritageClauses: readonly HeritageClause[] | undefined, members: readonly TypeElement[]) { + const node = (createSynthesizedNode(SyntaxKind.InterfaceDeclaration)); + node.decorators = asNodeArray(decorators); + node.modifiers = asNodeArray(modifiers); + node.name = asName(name); + node.typeParameters = asNodeArray(typeParameters); + node.heritageClauses = asNodeArray(heritageClauses); + node.members = createNodeArray(members); + return node; +} +export function updateInterfaceDeclaration(node: InterfaceDeclaration, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, name: Identifier, typeParameters: readonly TypeParameterDeclaration[] | undefined, heritageClauses: readonly HeritageClause[] | undefined, members: readonly TypeElement[]) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.name !== name + || node.typeParameters !== typeParameters + || node.heritageClauses !== heritageClauses + || node.members !== members + ? updateNode(createInterfaceDeclaration(decorators, modifiers, name, typeParameters, heritageClauses, members), node) + : node; +} +export function createTypeAliasDeclaration(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, name: string | Identifier, typeParameters: readonly TypeParameterDeclaration[] | undefined, type: TypeNode) { + const node = (createSynthesizedNode(SyntaxKind.TypeAliasDeclaration)); + node.decorators = asNodeArray(decorators); + node.modifiers = asNodeArray(modifiers); + node.name = asName(name); + node.typeParameters = asNodeArray(typeParameters); + node.type = type; + return node; +} +export function updateTypeAliasDeclaration(node: TypeAliasDeclaration, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, name: Identifier, typeParameters: readonly TypeParameterDeclaration[] | undefined, type: TypeNode) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.name !== name + || node.typeParameters !== typeParameters + || node.type !== type + ? updateNode(createTypeAliasDeclaration(decorators, modifiers, name, typeParameters, type), node) + : node; +} +export function createEnumDeclaration(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, name: string | Identifier, members: readonly EnumMember[]) { + const node = (createSynthesizedNode(SyntaxKind.EnumDeclaration)); + node.decorators = asNodeArray(decorators); + node.modifiers = asNodeArray(modifiers); + node.name = asName(name); + node.members = createNodeArray(members); + return node; +} +export function updateEnumDeclaration(node: EnumDeclaration, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, name: Identifier, members: readonly EnumMember[]) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.name !== name + || node.members !== members + ? updateNode(createEnumDeclaration(decorators, modifiers, name, members), node) + : node; +} +export function createModuleDeclaration(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, name: ModuleName, body: ModuleBody | undefined, flags = NodeFlags.None) { + const node = (createSynthesizedNode(SyntaxKind.ModuleDeclaration)); + node.flags |= flags & (NodeFlags.Namespace | NodeFlags.NestedNamespace | NodeFlags.GlobalAugmentation); + node.decorators = asNodeArray(decorators); + node.modifiers = asNodeArray(modifiers); + node.name = name; + node.body = body; + return node; +} +export function updateModuleDeclaration(node: ModuleDeclaration, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, name: ModuleName, body: ModuleBody | undefined) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.name !== name + || node.body !== body + ? updateNode(createModuleDeclaration(decorators, modifiers, name, body, node.flags), node) + : node; +} +export function createModuleBlock(statements: readonly Statement[]) { + const node = (createSynthesizedNode(SyntaxKind.ModuleBlock)); + node.statements = createNodeArray(statements); + return node; +} +export function updateModuleBlock(node: ModuleBlock, statements: readonly Statement[]) { + return node.statements !== statements + ? updateNode(createModuleBlock(statements), node) + : node; +} +export function createCaseBlock(clauses: readonly CaseOrDefaultClause[]): CaseBlock { + const node = (createSynthesizedNode(SyntaxKind.CaseBlock)); + node.clauses = createNodeArray(clauses); + return node; +} +export function updateCaseBlock(node: CaseBlock, clauses: readonly CaseOrDefaultClause[]) { + return node.clauses !== clauses + ? updateNode(createCaseBlock(clauses), node) + : node; +} +export function createNamespaceExportDeclaration(name: string | Identifier) { + const node = (createSynthesizedNode(SyntaxKind.NamespaceExportDeclaration)); + node.name = asName(name); + return node; +} +export function updateNamespaceExportDeclaration(node: NamespaceExportDeclaration, name: Identifier) { + return node.name !== name + ? updateNode(createNamespaceExportDeclaration(name), node) + : node; +} +export function createImportEqualsDeclaration(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, name: string | Identifier, moduleReference: ModuleReference) { + const node = (createSynthesizedNode(SyntaxKind.ImportEqualsDeclaration)); + node.decorators = asNodeArray(decorators); + node.modifiers = asNodeArray(modifiers); + node.name = asName(name); + node.moduleReference = moduleReference; + return node; +} +export function updateImportEqualsDeclaration(node: ImportEqualsDeclaration, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, name: Identifier, moduleReference: ModuleReference) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.name !== name + || node.moduleReference !== moduleReference + ? updateNode(createImportEqualsDeclaration(decorators, modifiers, name, moduleReference), node) + : node; +} +export function createImportDeclaration(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, importClause: ImportClause | undefined, moduleSpecifier: Expression): ImportDeclaration { + const node = (createSynthesizedNode(SyntaxKind.ImportDeclaration)); + node.decorators = asNodeArray(decorators); + node.modifiers = asNodeArray(modifiers); + node.importClause = importClause; + node.moduleSpecifier = moduleSpecifier; + return node; +} +export function updateImportDeclaration(node: ImportDeclaration, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, importClause: ImportClause | undefined, moduleSpecifier: Expression) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.importClause !== importClause + || node.moduleSpecifier !== moduleSpecifier + ? updateNode(createImportDeclaration(decorators, modifiers, importClause, moduleSpecifier), node) + : node; +} +export function createImportClause(name: Identifier | undefined, namedBindings: NamedImportBindings | undefined, isTypeOnly = false): ImportClause { + const node = (createSynthesizedNode(SyntaxKind.ImportClause)); + node.name = name; + node.namedBindings = namedBindings; + node.isTypeOnly = isTypeOnly; + return node; +} +export function updateImportClause(node: ImportClause, name: Identifier | undefined, namedBindings: NamedImportBindings | undefined, isTypeOnly: boolean) { + return node.name !== name + || node.namedBindings !== namedBindings + || node.isTypeOnly !== isTypeOnly + ? updateNode(createImportClause(name, namedBindings, isTypeOnly), node) + : node; +} +export function createNamespaceImport(name: Identifier): NamespaceImport { + const node = (createSynthesizedNode(SyntaxKind.NamespaceImport)); + node.name = name; + return node; +} +export function createNamespaceExport(name: Identifier): NamespaceExport { + const node = (createSynthesizedNode(SyntaxKind.NamespaceExport)); + node.name = name; + return node; +} +export function updateNamespaceImport(node: NamespaceImport, name: Identifier) { + return node.name !== name + ? updateNode(createNamespaceImport(name), node) + : node; +} +export function updateNamespaceExport(node: NamespaceExport, name: Identifier) { + return node.name !== name + ? updateNode(createNamespaceExport(name), node) + : node; +} +export function createNamedImports(elements: readonly ImportSpecifier[]): NamedImports { + const node = (createSynthesizedNode(SyntaxKind.NamedImports)); + node.elements = createNodeArray(elements); + return node; +} +export function updateNamedImports(node: NamedImports, elements: readonly ImportSpecifier[]) { + return node.elements !== elements + ? updateNode(createNamedImports(elements), node) + : node; +} +export function createImportSpecifier(propertyName: Identifier | undefined, name: Identifier) { + const node = (createSynthesizedNode(SyntaxKind.ImportSpecifier)); + node.propertyName = propertyName; + node.name = name; + return node; +} +export function updateImportSpecifier(node: ImportSpecifier, propertyName: Identifier | undefined, name: Identifier) { + return node.propertyName !== propertyName + || node.name !== name + ? updateNode(createImportSpecifier(propertyName, name), node) + : node; +} +export function createExportAssignment(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, isExportEquals: boolean | undefined, expression: Expression) { + const node = (createSynthesizedNode(SyntaxKind.ExportAssignment)); + node.decorators = asNodeArray(decorators); + node.modifiers = asNodeArray(modifiers); + node.isExportEquals = isExportEquals; + node.expression = isExportEquals ? parenthesizeBinaryOperand(SyntaxKind.EqualsToken, expression, /*isLeftSideOfBinary*/ false, /*leftOperand*/ undefined) : parenthesizeDefaultExpression(expression); + return node; +} +export function updateExportAssignment(node: ExportAssignment, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, expression: Expression) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.expression !== expression + ? updateNode(createExportAssignment(decorators, modifiers, node.isExportEquals, expression), node) + : node; +} +export function createExportDeclaration(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, exportClause: NamedExportBindings | undefined, moduleSpecifier?: Expression, isTypeOnly = false) { + const node = (createSynthesizedNode(SyntaxKind.ExportDeclaration)); + node.decorators = asNodeArray(decorators); + node.modifiers = asNodeArray(modifiers); + node.isTypeOnly = isTypeOnly; + node.exportClause = exportClause; + node.moduleSpecifier = moduleSpecifier; + return node; +} +export function updateExportDeclaration(node: ExportDeclaration, decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, exportClause: NamedExportBindings | undefined, moduleSpecifier: Expression | undefined, isTypeOnly: boolean) { + return node.decorators !== decorators + || node.modifiers !== modifiers + || node.isTypeOnly !== isTypeOnly + || node.exportClause !== exportClause + || node.moduleSpecifier !== moduleSpecifier + ? updateNode(createExportDeclaration(decorators, modifiers, exportClause, moduleSpecifier, isTypeOnly), node) + : node; +} +/* @internal */ +export function createEmptyExports() { + return createExportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, createNamedExports([]), /*moduleSpecifier*/ undefined); +} +export function createNamedExports(elements: readonly ExportSpecifier[]) { + const node = (createSynthesizedNode(SyntaxKind.NamedExports)); + node.elements = createNodeArray(elements); + return node; +} +export function updateNamedExports(node: NamedExports, elements: readonly ExportSpecifier[]) { + return node.elements !== elements + ? updateNode(createNamedExports(elements), node) + : node; +} +export function createExportSpecifier(propertyName: string | Identifier | undefined, name: string | Identifier) { + const node = (createSynthesizedNode(SyntaxKind.ExportSpecifier)); + node.propertyName = asName(propertyName); + node.name = asName(name); + return node; +} +export function updateExportSpecifier(node: ExportSpecifier, propertyName: Identifier | undefined, name: Identifier) { + return node.propertyName !== propertyName + || node.name !== name + ? updateNode(createExportSpecifier(propertyName, name), node) + : node; +} +// Module references +export function createExternalModuleReference(expression: Expression) { + const node = (createSynthesizedNode(SyntaxKind.ExternalModuleReference)); + node.expression = expression; + return node; +} +export function updateExternalModuleReference(node: ExternalModuleReference, expression: Expression) { + return node.expression !== expression + ? updateNode(createExternalModuleReference(expression), node) + : node; +} +// JSDoc +/* @internal */ +export function createJSDocTypeExpression(type: TypeNode): JSDocTypeExpression { + const node = (createSynthesizedNode(SyntaxKind.JSDocTypeExpression) as JSDocTypeExpression); + node.type = type; + return node; +} +/* @internal */ +export function createJSDocTypeTag(typeExpression: JSDocTypeExpression, comment?: string): JSDocTypeTag { + const tag = createJSDocTag(SyntaxKind.JSDocTypeTag, "type"); + tag.typeExpression = typeExpression; + tag.comment = comment; + return tag; +} +/* @internal */ +export function createJSDocReturnTag(typeExpression?: JSDocTypeExpression, comment?: string): JSDocReturnTag { + const tag = createJSDocTag(SyntaxKind.JSDocReturnTag, "returns"); + tag.typeExpression = typeExpression; + tag.comment = comment; + return tag; +} +/** @internal */ +export function createJSDocThisTag(typeExpression?: JSDocTypeExpression): JSDocThisTag { + const tag = createJSDocTag(SyntaxKind.JSDocThisTag, "this"); + tag.typeExpression = typeExpression; + return tag; +} +/* @internal */ +export function createJSDocParamTag(name: EntityName, isBracketed: boolean, typeExpression?: JSDocTypeExpression, comment?: string): JSDocParameterTag { + const tag = createJSDocTag(SyntaxKind.JSDocParameterTag, "param"); + tag.typeExpression = typeExpression; + tag.name = name; + tag.isBracketed = isBracketed; + tag.comment = comment; + return tag; +} +/* @internal */ +export function createJSDocComment(comment?: string | undefined, tags?: NodeArray | undefined) { + const node = (createSynthesizedNode(SyntaxKind.JSDocComment) as JSDoc); + node.comment = comment; + node.tags = tags; + return node; +} +/* @internal */ +function createJSDocTag(kind: T["kind"], tagName: string): T { + const node = createSynthesizedNode(kind) as T; + node.tagName = createIdentifier(tagName); + return node; +} +// JSX +export function createJsxElement(openingElement: JsxOpeningElement, children: readonly JsxChild[], closingElement: JsxClosingElement) { + const node = (createSynthesizedNode(SyntaxKind.JsxElement)); + node.openingElement = openingElement; + node.children = createNodeArray(children); + node.closingElement = closingElement; + return node; +} +export function updateJsxElement(node: JsxElement, openingElement: JsxOpeningElement, children: readonly JsxChild[], closingElement: JsxClosingElement) { + return node.openingElement !== openingElement + || node.children !== children + || node.closingElement !== closingElement + ? updateNode(createJsxElement(openingElement, children, closingElement), node) + : node; +} +export function createJsxSelfClosingElement(tagName: JsxTagNameExpression, typeArguments: readonly TypeNode[] | undefined, attributes: JsxAttributes) { + const node = (createSynthesizedNode(SyntaxKind.JsxSelfClosingElement)); + node.tagName = tagName; + node.typeArguments = asNodeArray(typeArguments); + node.attributes = attributes; + return node; +} +export function updateJsxSelfClosingElement(node: JsxSelfClosingElement, tagName: JsxTagNameExpression, typeArguments: readonly TypeNode[] | undefined, attributes: JsxAttributes) { + return node.tagName !== tagName + || node.typeArguments !== typeArguments + || node.attributes !== attributes + ? updateNode(createJsxSelfClosingElement(tagName, typeArguments, attributes), node) + : node; +} +export function createJsxOpeningElement(tagName: JsxTagNameExpression, typeArguments: readonly TypeNode[] | undefined, attributes: JsxAttributes) { + const node = (createSynthesizedNode(SyntaxKind.JsxOpeningElement)); + node.tagName = tagName; + node.typeArguments = asNodeArray(typeArguments); + node.attributes = attributes; + return node; +} +export function updateJsxOpeningElement(node: JsxOpeningElement, tagName: JsxTagNameExpression, typeArguments: readonly TypeNode[] | undefined, attributes: JsxAttributes) { + return node.tagName !== tagName + || node.typeArguments !== typeArguments + || node.attributes !== attributes + ? updateNode(createJsxOpeningElement(tagName, typeArguments, attributes), node) + : node; +} +export function createJsxClosingElement(tagName: JsxTagNameExpression) { + const node = (createSynthesizedNode(SyntaxKind.JsxClosingElement)); + node.tagName = tagName; + return node; +} +export function updateJsxClosingElement(node: JsxClosingElement, tagName: JsxTagNameExpression) { + return node.tagName !== tagName + ? updateNode(createJsxClosingElement(tagName), node) + : node; +} +export function createJsxFragment(openingFragment: JsxOpeningFragment, children: readonly JsxChild[], closingFragment: JsxClosingFragment) { + const node = (createSynthesizedNode(SyntaxKind.JsxFragment)); + node.openingFragment = openingFragment; + node.children = createNodeArray(children); + node.closingFragment = closingFragment; + return node; +} +export function createJsxText(text: string, containsOnlyTriviaWhiteSpaces?: boolean) { + const node = (createSynthesizedNode(SyntaxKind.JsxText)); + node.text = text; + node.containsOnlyTriviaWhiteSpaces = !!containsOnlyTriviaWhiteSpaces; + return node; +} +export function updateJsxText(node: JsxText, text: string, containsOnlyTriviaWhiteSpaces?: boolean) { + return node.text !== text + || node.containsOnlyTriviaWhiteSpaces !== containsOnlyTriviaWhiteSpaces + ? updateNode(createJsxText(text, containsOnlyTriviaWhiteSpaces), node) + : node; +} +export function createJsxOpeningFragment() { + return createSynthesizedNode(SyntaxKind.JsxOpeningFragment); +} +export function createJsxJsxClosingFragment() { + return createSynthesizedNode(SyntaxKind.JsxClosingFragment); +} +export function updateJsxFragment(node: JsxFragment, openingFragment: JsxOpeningFragment, children: readonly JsxChild[], closingFragment: JsxClosingFragment) { + return node.openingFragment !== openingFragment + || node.children !== children + || node.closingFragment !== closingFragment + ? updateNode(createJsxFragment(openingFragment, children, closingFragment), node) + : node; +} +export function createJsxAttribute(name: Identifier, initializer: StringLiteral | JsxExpression) { + const node = (createSynthesizedNode(SyntaxKind.JsxAttribute)); + node.name = name; + node.initializer = initializer; + return node; +} +export function updateJsxAttribute(node: JsxAttribute, name: Identifier, initializer: StringLiteral | JsxExpression) { + return node.name !== name + || node.initializer !== initializer + ? updateNode(createJsxAttribute(name, initializer), node) + : node; +} +export function createJsxAttributes(properties: readonly JsxAttributeLike[]) { + const node = (createSynthesizedNode(SyntaxKind.JsxAttributes)); + node.properties = createNodeArray(properties); + return node; +} +export function updateJsxAttributes(node: JsxAttributes, properties: readonly JsxAttributeLike[]) { + return node.properties !== properties + ? updateNode(createJsxAttributes(properties), node) + : node; +} +export function createJsxSpreadAttribute(expression: Expression) { + const node = (createSynthesizedNode(SyntaxKind.JsxSpreadAttribute)); + node.expression = expression; + return node; +} +export function updateJsxSpreadAttribute(node: JsxSpreadAttribute, expression: Expression) { + return node.expression !== expression + ? updateNode(createJsxSpreadAttribute(expression), node) + : node; +} +export function createJsxExpression(dotDotDotToken: DotDotDotToken | undefined, expression: Expression | undefined) { + const node = (createSynthesizedNode(SyntaxKind.JsxExpression)); + node.dotDotDotToken = dotDotDotToken; + node.expression = expression; + return node; +} +export function updateJsxExpression(node: JsxExpression, expression: Expression | undefined) { + return node.expression !== expression + ? updateNode(createJsxExpression(node.dotDotDotToken, expression), node) + : node; +} +// Clauses +export function createCaseClause(expression: Expression, statements: readonly Statement[]) { + const node = (createSynthesizedNode(SyntaxKind.CaseClause)); + node.expression = parenthesizeExpressionForList(expression); + node.statements = createNodeArray(statements); + return node; +} +export function updateCaseClause(node: CaseClause, expression: Expression, statements: readonly Statement[]) { + return node.expression !== expression + || node.statements !== statements + ? updateNode(createCaseClause(expression, statements), node) + : node; +} +export function createDefaultClause(statements: readonly Statement[]) { + const node = (createSynthesizedNode(SyntaxKind.DefaultClause)); + node.statements = createNodeArray(statements); + return node; +} +export function updateDefaultClause(node: DefaultClause, statements: readonly Statement[]) { + return node.statements !== statements + ? updateNode(createDefaultClause(statements), node) + : node; +} +export function createHeritageClause(token: HeritageClause["token"], types: readonly ExpressionWithTypeArguments[]) { + const node = (createSynthesizedNode(SyntaxKind.HeritageClause)); + node.token = token; + node.types = createNodeArray(types); + return node; +} +export function updateHeritageClause(node: HeritageClause, types: readonly ExpressionWithTypeArguments[]) { + return node.types !== types + ? updateNode(createHeritageClause(node.token, types), node) + : node; +} +export function createCatchClause(variableDeclaration: string | VariableDeclaration | undefined, block: Block) { + const node = (createSynthesizedNode(SyntaxKind.CatchClause)); + node.variableDeclaration = isString(variableDeclaration) ? createVariableDeclaration(variableDeclaration) : variableDeclaration; + node.block = block; + return node; +} +export function updateCatchClause(node: CatchClause, variableDeclaration: VariableDeclaration | undefined, block: Block) { + return node.variableDeclaration !== variableDeclaration + || node.block !== block + ? updateNode(createCatchClause(variableDeclaration, block), node) + : node; +} +// Property assignments +export function createPropertyAssignment(name: string | PropertyName, initializer: Expression) { + const node = (createSynthesizedNode(SyntaxKind.PropertyAssignment)); + node.name = asName(name); + node.questionToken = undefined; + node.initializer = parenthesizeExpressionForList(initializer); + return node; +} +export function updatePropertyAssignment(node: PropertyAssignment, name: PropertyName, initializer: Expression) { + return node.name !== name + || node.initializer !== initializer + ? updateNode(createPropertyAssignment(name, initializer), node) + : node; +} +export function createShorthandPropertyAssignment(name: string | Identifier, objectAssignmentInitializer?: Expression) { + const node = (createSynthesizedNode(SyntaxKind.ShorthandPropertyAssignment)); + node.name = asName(name); + node.objectAssignmentInitializer = objectAssignmentInitializer !== undefined ? parenthesizeExpressionForList(objectAssignmentInitializer) : undefined; + return node; +} +export function updateShorthandPropertyAssignment(node: ShorthandPropertyAssignment, name: Identifier, objectAssignmentInitializer: Expression | undefined) { + return node.name !== name + || node.objectAssignmentInitializer !== objectAssignmentInitializer + ? updateNode(createShorthandPropertyAssignment(name, objectAssignmentInitializer), node) + : node; +} +export function createSpreadAssignment(expression: Expression) { + const node = (createSynthesizedNode(SyntaxKind.SpreadAssignment)); + node.expression = parenthesizeExpressionForList(expression); + return node; +} +export function updateSpreadAssignment(node: SpreadAssignment, expression: Expression) { + return node.expression !== expression + ? updateNode(createSpreadAssignment(expression), node) + : node; +} +// Enum +export function createEnumMember(name: string | PropertyName, initializer?: Expression) { + const node = (createSynthesizedNode(SyntaxKind.EnumMember)); + node.name = asName(name); + node.initializer = initializer && parenthesizeExpressionForList(initializer); + return node; +} +export function updateEnumMember(node: EnumMember, name: PropertyName, initializer: Expression | undefined) { + return node.name !== name + || node.initializer !== initializer + ? updateNode(createEnumMember(name, initializer), node) + : node; +} +// Top-level nodes +export function updateSourceFileNode(node: SourceFile, statements: readonly Statement[], isDeclarationFile?: boolean, referencedFiles?: SourceFile["referencedFiles"], typeReferences?: SourceFile["typeReferenceDirectives"], hasNoDefaultLib?: boolean, libReferences?: SourceFile["libReferenceDirectives"]) { + if (node.statements !== statements || + (isDeclarationFile !== undefined && node.isDeclarationFile !== isDeclarationFile) || + (referencedFiles !== undefined && node.referencedFiles !== referencedFiles) || + (typeReferences !== undefined && node.typeReferenceDirectives !== typeReferences) || + (libReferences !== undefined && node.libReferenceDirectives !== libReferences) || + (hasNoDefaultLib !== undefined && node.hasNoDefaultLib !== hasNoDefaultLib)) { + const updated = (createSynthesizedNode(SyntaxKind.SourceFile)); + updated.flags |= node.flags; + updated.statements = createNodeArray(statements); + updated.endOfFileToken = node.endOfFileToken; + updated.fileName = node.fileName; + updated.path = node.path; + updated.text = node.text; + updated.isDeclarationFile = isDeclarationFile === undefined ? node.isDeclarationFile : isDeclarationFile; + updated.referencedFiles = referencedFiles === undefined ? node.referencedFiles : referencedFiles; + updated.typeReferenceDirectives = typeReferences === undefined ? node.typeReferenceDirectives : typeReferences; + updated.hasNoDefaultLib = hasNoDefaultLib === undefined ? node.hasNoDefaultLib : hasNoDefaultLib; + updated.libReferenceDirectives = libReferences === undefined ? node.libReferenceDirectives : libReferences; + if (node.amdDependencies !== undefined) + updated.amdDependencies = node.amdDependencies; + if (node.moduleName !== undefined) + updated.moduleName = node.moduleName; + if (node.languageVariant !== undefined) + updated.languageVariant = node.languageVariant; + if (node.renamedDependencies !== undefined) + updated.renamedDependencies = node.renamedDependencies; + if (node.languageVersion !== undefined) + updated.languageVersion = node.languageVersion; + if (node.scriptKind !== undefined) + updated.scriptKind = node.scriptKind; + if (node.externalModuleIndicator !== undefined) + updated.externalModuleIndicator = node.externalModuleIndicator; + if (node.commonJsModuleIndicator !== undefined) + updated.commonJsModuleIndicator = node.commonJsModuleIndicator; + if (node.identifiers !== undefined) + updated.identifiers = node.identifiers; + if (node.nodeCount !== undefined) + updated.nodeCount = node.nodeCount; + if (node.identifierCount !== undefined) + updated.identifierCount = node.identifierCount; + if (node.symbolCount !== undefined) + updated.symbolCount = node.symbolCount; + if (node.parseDiagnostics !== undefined) + updated.parseDiagnostics = node.parseDiagnostics; + if (node.bindDiagnostics !== undefined) + updated.bindDiagnostics = node.bindDiagnostics; + if (node.bindSuggestionDiagnostics !== undefined) + updated.bindSuggestionDiagnostics = node.bindSuggestionDiagnostics; + if (node.lineMap !== undefined) + updated.lineMap = node.lineMap; + if (node.classifiableNames !== undefined) + updated.classifiableNames = node.classifiableNames; + if (node.resolvedModules !== undefined) + updated.resolvedModules = node.resolvedModules; + if (node.resolvedTypeReferenceDirectiveNames !== undefined) + updated.resolvedTypeReferenceDirectiveNames = node.resolvedTypeReferenceDirectiveNames; + if (node.imports !== undefined) + updated.imports = node.imports; + if (node.moduleAugmentations !== undefined) + updated.moduleAugmentations = node.moduleAugmentations; + if (node.pragmas !== undefined) + updated.pragmas = node.pragmas; + if (node.localJsxFactory !== undefined) + updated.localJsxFactory = node.localJsxFactory; + if (node.localJsxNamespace !== undefined) + updated.localJsxNamespace = node.localJsxNamespace; + return updateNode(updated, node); + } + return node; +} +/** + * Creates a shallow, memberwise clone of a node for mutation. + */ +export function getMutableClone(node: T): T { + const clone = getSynthesizedClone(node); + clone.pos = node.pos; + clone.end = node.end; + clone.parent = node.parent; + return clone; +} +// Transformation nodes +/** + * Creates a synthetic statement to act as a placeholder for a not-emitted statement in + * order to preserve comments. + * + * @param original The original statement. + */ +export function createNotEmittedStatement(original: Node) { + const node = (createSynthesizedNode(SyntaxKind.NotEmittedStatement)); + node.original = original; + setTextRange(node, original); + return node; +} +/** + * Creates a synthetic element to act as a placeholder for the end of an emitted declaration in + * order to properly emit exports. + */ +/* @internal */ +export function createEndOfDeclarationMarker(original: Node) { + const node = (createSynthesizedNode(SyntaxKind.EndOfDeclarationMarker)); + node.emitNode = ({} as EmitNode); + node.original = original; + return node; +} +/** + * Creates a synthetic element to act as a placeholder for the beginning of a merged declaration in + * order to properly emit exports. + */ +/* @internal */ +export function createMergeDeclarationMarker(original: Node) { + const node = (createSynthesizedNode(SyntaxKind.MergeDeclarationMarker)); + node.emitNode = ({} as EmitNode); + node.original = original; + return node; +} +/** + * Creates a synthetic expression to act as a placeholder for a not-emitted expression in + * order to preserve comments or sourcemap positions. + * + * @param expression The inner expression to emit. + * @param original The original outer expression. + * @param location The location for the expression. Defaults to the positions from "original" if provided. + */ +export function createPartiallyEmittedExpression(expression: Expression, original?: Node) { + const node = (createSynthesizedNode(SyntaxKind.PartiallyEmittedExpression)); + node.expression = expression; + node.original = original; + setTextRange(node, original); + return node; +} +export function updatePartiallyEmittedExpression(node: PartiallyEmittedExpression, expression: Expression) { + if (node.expression !== expression) { + return updateNode(createPartiallyEmittedExpression(expression, node.original), node); } - - export function createUnparsedSourceFile(text: string): UnparsedSource; - export function createUnparsedSourceFile(inputFile: InputFiles, type: "js" | "dts", stripInternal?: boolean): UnparsedSource; - export function createUnparsedSourceFile(text: string, mapPath: string | undefined, map: string | undefined): UnparsedSource; - export function createUnparsedSourceFile(textOrInputFiles: string | InputFiles, mapPathOrType?: string, mapTextOrStripInternal?: string | boolean): UnparsedSource { - const node = createUnparsedSource(); - let stripInternal: boolean | undefined; - let bundleFileInfo: BundleFileInfo | undefined; - if (!isString(textOrInputFiles)) { - Debug.assert(mapPathOrType === "js" || mapPathOrType === "dts"); - node.fileName = (mapPathOrType === "js" ? textOrInputFiles.javascriptPath : textOrInputFiles.declarationPath) || ""; - node.sourceMapPath = mapPathOrType === "js" ? textOrInputFiles.javascriptMapPath : textOrInputFiles.declarationMapPath; - Object.defineProperties(node, { - text: { get() { return mapPathOrType === "js" ? textOrInputFiles.javascriptText : textOrInputFiles.declarationText; } }, - sourceMapText: { get() { return mapPathOrType === "js" ? textOrInputFiles.javascriptMapText : textOrInputFiles.declarationMapText; } }, - }); - - - if (textOrInputFiles.buildInfo && textOrInputFiles.buildInfo.bundle) { - node.oldFileOfCurrentEmit = textOrInputFiles.oldFileOfCurrentEmit; - Debug.assert(mapTextOrStripInternal === undefined || typeof mapTextOrStripInternal === "boolean"); - stripInternal = mapTextOrStripInternal; - bundleFileInfo = mapPathOrType === "js" ? textOrInputFiles.buildInfo.bundle.js : textOrInputFiles.buildInfo.bundle.dts; - if (node.oldFileOfCurrentEmit) { - parseOldFileOfCurrentEmit(node, Debug.checkDefined(bundleFileInfo)); - return node; - } - } + return node; +} +function flattenCommaElements(node: Expression): Expression | readonly Expression[] { + if (nodeIsSynthesized(node) && !isParseTreeNode(node) && !node.original && !node.emitNode && !node.id) { + if (node.kind === SyntaxKind.CommaListExpression) { + return (node).elements; } - else { - node.fileName = ""; - node.text = textOrInputFiles; - node.sourceMapPath = mapPathOrType; - node.sourceMapText = mapTextOrStripInternal as string; + if (isBinaryExpression(node) && node.operatorToken.kind === SyntaxKind.CommaToken) { + return [node.left, node.right]; } - Debug.assert(!node.oldFileOfCurrentEmit); - parseUnparsedSourceFile(node, bundleFileInfo, stripInternal); - return node; } - - function parseUnparsedSourceFile(node: UnparsedSource, bundleFileInfo: BundleFileInfo | undefined, stripInternal: boolean | undefined) { - let prologues: UnparsedPrologue[] | undefined; - let helpers: UnscopedEmitHelper[] | undefined; - let referencedFiles: FileReference[] | undefined; - let typeReferenceDirectives: string[] | undefined; - let libReferenceDirectives: FileReference[] | undefined; - let texts: UnparsedSourceText[] | undefined; - - for (const section of bundleFileInfo ? bundleFileInfo.sections : emptyArray) { - switch (section.kind) { - case BundleFileSectionKind.Prologue: - (prologues || (prologues = [])).push(createUnparsedNode(section, node) as UnparsedPrologue); - break; - case BundleFileSectionKind.EmitHelpers: - (helpers || (helpers = [])).push(getAllUnscopedEmitHelpers().get(section.data)!); - break; - case BundleFileSectionKind.NoDefaultLib: - node.hasNoDefaultLib = true; - break; - case BundleFileSectionKind.Reference: - (referencedFiles || (referencedFiles = [])).push({ pos: -1, end: -1, fileName: section.data }); - break; - case BundleFileSectionKind.Type: - (typeReferenceDirectives || (typeReferenceDirectives = [])).push(section.data); - break; - case BundleFileSectionKind.Lib: - (libReferenceDirectives || (libReferenceDirectives = [])).push({ pos: -1, end: -1, fileName: section.data }); - break; - case BundleFileSectionKind.Prepend: - const prependNode = createUnparsedNode(section, node) as UnparsedPrepend; - let prependTexts: UnparsedTextLike[] | undefined; - for (const text of section.texts) { - if (!stripInternal || text.kind !== BundleFileSectionKind.Internal) { - (prependTexts || (prependTexts = [])).push(createUnparsedNode(text, node) as UnparsedTextLike); - } - } - prependNode.texts = prependTexts || emptyArray; - (texts || (texts = [])).push(prependNode); - break; - case BundleFileSectionKind.Internal: - if (stripInternal) { - if (!texts) texts = []; - break; - } - // falls through - - case BundleFileSectionKind.Text: - (texts || (texts = [])).push(createUnparsedNode(section, node) as UnparsedTextLike); - break; - default: - Debug.assertNever(section); + return node; +} +export function createCommaList(elements: readonly Expression[]) { + const node = (createSynthesizedNode(SyntaxKind.CommaListExpression)); + node.elements = createNodeArray(sameFlatMap(elements, flattenCommaElements)); + return node; +} +export function updateCommaList(node: CommaListExpression, elements: readonly Expression[]) { + return node.elements !== elements + ? updateNode(createCommaList(elements), node) + : node; +} +/* @internal */ +export function createSyntheticReferenceExpression(expression: Expression, thisArg: Expression) { + const node = (createSynthesizedNode(SyntaxKind.SyntheticReferenceExpression)); + node.expression = expression; + node.thisArg = thisArg; + return node; +} +/* @internal */ +export function updateSyntheticReferenceExpression(node: SyntheticReferenceExpression, expression: Expression, thisArg: Expression) { + return node.expression !== expression + || node.thisArg !== thisArg + ? updateNode(createSyntheticReferenceExpression(expression, thisArg), node) + : node; +} +export function createBundle(sourceFiles: readonly SourceFile[], prepends: readonly (UnparsedSource | InputFiles)[] = emptyArray) { + const node = (createNode(SyntaxKind.Bundle)); + node.prepends = prepends; + node.sourceFiles = sourceFiles; + return node; +} +let allUnscopedEmitHelpers: ts.ReadonlyMap | undefined; +function getAllUnscopedEmitHelpers() { + return allUnscopedEmitHelpers || (allUnscopedEmitHelpers = arrayToMap([ + valuesHelper, + readHelper, + spreadHelper, + spreadArraysHelper, + restHelper, + decorateHelper, + metadataHelper, + paramHelper, + awaiterHelper, + assignHelper, + awaitHelper, + asyncGeneratorHelper, + asyncDelegator, + asyncValues, + extendsHelper, + templateObjectHelper, + generatorHelper, + importStarHelper, + importDefaultHelper, + classPrivateFieldGetHelper, + classPrivateFieldSetHelper, + createBindingHelper, + setModuleDefaultHelper + ], helper => helper.name)); +} +function createUnparsedSource() { + const node = (createNode(SyntaxKind.UnparsedSource)); + node.prologues = emptyArray; + node.referencedFiles = emptyArray; + node.libReferenceDirectives = emptyArray; + node.getLineAndCharacterOfPosition = pos => getLineAndCharacterOfPosition(node, pos); + return node; +} +export function createUnparsedSourceFile(text: string): UnparsedSource; +export function createUnparsedSourceFile(inputFile: InputFiles, type: "js" | "dts", stripInternal?: boolean): UnparsedSource; +export function createUnparsedSourceFile(text: string, mapPath: string | undefined, map: string | undefined): UnparsedSource; +export function createUnparsedSourceFile(textOrInputFiles: string | InputFiles, mapPathOrType?: string, mapTextOrStripInternal?: string | boolean): UnparsedSource { + const node = createUnparsedSource(); + let stripInternal: boolean | undefined; + let bundleFileInfo: BundleFileInfo | undefined; + if (!isString(textOrInputFiles)) { + Debug.assert(mapPathOrType === "js" || mapPathOrType === "dts"); + node.fileName = (mapPathOrType === "js" ? textOrInputFiles.javascriptPath : textOrInputFiles.declarationPath) || ""; + node.sourceMapPath = mapPathOrType === "js" ? textOrInputFiles.javascriptMapPath : textOrInputFiles.declarationMapPath; + Object.defineProperties(node, { + text: { get() { return mapPathOrType === "js" ? textOrInputFiles.javascriptText : textOrInputFiles.declarationText; } }, + sourceMapText: { get() { return mapPathOrType === "js" ? textOrInputFiles.javascriptMapText : textOrInputFiles.declarationMapText; } }, + }); + if (textOrInputFiles.buildInfo && textOrInputFiles.buildInfo.bundle) { + node.oldFileOfCurrentEmit = textOrInputFiles.oldFileOfCurrentEmit; + Debug.assert(mapTextOrStripInternal === undefined || typeof mapTextOrStripInternal === "boolean"); + stripInternal = mapTextOrStripInternal; + bundleFileInfo = mapPathOrType === "js" ? textOrInputFiles.buildInfo.bundle.js : textOrInputFiles.buildInfo.bundle.dts; + if (node.oldFileOfCurrentEmit) { + parseOldFileOfCurrentEmit(node, Debug.checkDefined(bundleFileInfo)); + return node; } } - - node.prologues = prologues || emptyArray; - node.helpers = helpers; - node.referencedFiles = referencedFiles || emptyArray; - node.typeReferenceDirectives = typeReferenceDirectives; - node.libReferenceDirectives = libReferenceDirectives || emptyArray; - node.texts = texts || [createUnparsedNode({ kind: BundleFileSectionKind.Text, pos: 0, end: node.text.length }, node)]; } - - function parseOldFileOfCurrentEmit(node: UnparsedSource, bundleFileInfo: BundleFileInfo) { - Debug.assert(!!node.oldFileOfCurrentEmit); - let texts: UnparsedTextLike[] | undefined; - let syntheticReferences: UnparsedSyntheticReference[] | undefined; - for (const section of bundleFileInfo.sections) { - switch (section.kind) { - case BundleFileSectionKind.Internal: - case BundleFileSectionKind.Text: - (texts || (texts = [])).push(createUnparsedNode(section, node) as UnparsedTextLike); - break; - - case BundleFileSectionKind.NoDefaultLib: - case BundleFileSectionKind.Reference: - case BundleFileSectionKind.Type: - case BundleFileSectionKind.Lib: - (syntheticReferences || (syntheticReferences = [])).push(createUnparsedSyntheticReference(section, node)); - break; - - // Ignore - case BundleFileSectionKind.Prologue: - case BundleFileSectionKind.EmitHelpers: - case BundleFileSectionKind.Prepend: - break; - - default: - Debug.assertNever(section); - } - } - node.texts = texts || emptyArray; - node.helpers = map(bundleFileInfo.sources && bundleFileInfo.sources.helpers, name => getAllUnscopedEmitHelpers().get(name)!); - node.syntheticReferences = syntheticReferences; - return node; + else { + node.fileName = ""; + node.text = textOrInputFiles; + node.sourceMapPath = mapPathOrType; + node.sourceMapText = mapTextOrStripInternal as string; } - - function mapBundleFileSectionKindToSyntaxKind(kind: BundleFileSectionKind): SyntaxKind { - switch (kind) { - case BundleFileSectionKind.Prologue: return SyntaxKind.UnparsedPrologue; - case BundleFileSectionKind.Prepend: return SyntaxKind.UnparsedPrepend; - case BundleFileSectionKind.Internal: return SyntaxKind.UnparsedInternalText; - case BundleFileSectionKind.Text: return SyntaxKind.UnparsedText; - + Debug.assert(!node.oldFileOfCurrentEmit); + parseUnparsedSourceFile(node, bundleFileInfo, stripInternal); + return node; +} +function parseUnparsedSourceFile(node: UnparsedSource, bundleFileInfo: BundleFileInfo | undefined, stripInternal: boolean | undefined) { + let prologues: UnparsedPrologue[] | undefined; + let helpers: UnscopedEmitHelper[] | undefined; + let referencedFiles: FileReference[] | undefined; + let typeReferenceDirectives: string[] | undefined; + let libReferenceDirectives: FileReference[] | undefined; + let texts: UnparsedSourceText[] | undefined; + for (const section of bundleFileInfo ? bundleFileInfo.sections : emptyArray) { + switch (section.kind) { + case BundleFileSectionKind.Prologue: + (prologues || (prologues = [])).push((createUnparsedNode(section, node) as UnparsedPrologue)); + break; case BundleFileSectionKind.EmitHelpers: + (helpers || (helpers = [])).push(getAllUnscopedEmitHelpers().get(section.data)!); + break; case BundleFileSectionKind.NoDefaultLib: + node.hasNoDefaultLib = true; + break; case BundleFileSectionKind.Reference: + (referencedFiles || (referencedFiles = [])).push({ pos: -1, end: -1, fileName: section.data }); + break; case BundleFileSectionKind.Type: + (typeReferenceDirectives || (typeReferenceDirectives = [])).push(section.data); + break; case BundleFileSectionKind.Lib: - return Debug.fail(`BundleFileSectionKind: ${kind} not yet mapped to SyntaxKind`); - - default: - return Debug.assertNever(kind); - } - } - - function createUnparsedNode(section: BundleFileSection, parent: UnparsedSource): UnparsedNode { - const node = createNode(mapBundleFileSectionKindToSyntaxKind(section.kind), section.pos, section.end) as UnparsedNode; - node.parent = parent; - node.data = section.data; - return node; - } - - function createUnparsedSyntheticReference(section: BundleFileHasNoDefaultLib | BundleFileReference, parent: UnparsedSource) { - const node = createNode(SyntaxKind.UnparsedSyntheticReference, section.pos, section.end) as UnparsedSyntheticReference; - node.parent = parent; - node.data = section.data; - node.section = section; - return node; - } - - export function createInputFiles( - javascriptText: string, - declarationText: string - ): InputFiles; - export function createInputFiles( - readFileText: (path: string) => string | undefined, - javascriptPath: string, - javascriptMapPath: string | undefined, - declarationPath: string, - declarationMapPath: string | undefined, - buildInfoPath: string | undefined - ): InputFiles; - export function createInputFiles( - javascriptText: string, - declarationText: string, - javascriptMapPath: string | undefined, - javascriptMapText: string | undefined, - declarationMapPath: string | undefined, - declarationMapText: string | undefined - ): InputFiles; - /*@internal*/ - export function createInputFiles( - javascriptText: string, - declarationText: string, - javascriptMapPath: string | undefined, - javascriptMapText: string | undefined, - declarationMapPath: string | undefined, - declarationMapText: string | undefined, - javascriptPath: string | undefined, - declarationPath: string | undefined, - buildInfoPath?: string | undefined, - buildInfo?: BuildInfo, - oldFileOfCurrentEmit?: boolean - ): InputFiles; - export function createInputFiles( - javascriptTextOrReadFileText: string | ((path: string) => string | undefined), - declarationTextOrJavascriptPath: string, - javascriptMapPath?: string, - javascriptMapTextOrDeclarationPath?: string, - declarationMapPath?: string, - declarationMapTextOrBuildInfoPath?: string, - javascriptPath?: string | undefined, - declarationPath?: string | undefined, - buildInfoPath?: string | undefined, - buildInfo?: BuildInfo, - oldFileOfCurrentEmit?: boolean - ): InputFiles { - const node = createNode(SyntaxKind.InputFiles); - if (!isString(javascriptTextOrReadFileText)) { - const cache = createMap(); - const textGetter = (path: string | undefined) => { - if (path === undefined) return undefined; - let value = cache.get(path); - if (value === undefined) { - value = javascriptTextOrReadFileText(path); - cache.set(path, value !== undefined ? value : false); + (libReferenceDirectives || (libReferenceDirectives = [])).push({ pos: -1, end: -1, fileName: section.data }); + break; + case BundleFileSectionKind.Prepend: + const prependNode = (createUnparsedNode(section, node) as UnparsedPrepend); + let prependTexts: UnparsedTextLike[] | undefined; + for (const text of section.texts) { + if (!stripInternal || text.kind !== BundleFileSectionKind.Internal) { + (prependTexts || (prependTexts = [])).push((createUnparsedNode(text, node) as UnparsedTextLike)); + } } - return value !== false ? value as string : undefined; - }; - const definedTextGetter = (path: string) => { - const result = textGetter(path); - return result !== undefined ? result : `/* Input file ${path} was missing */\r\n`; - }; - let buildInfo: BuildInfo | false; - const getAndCacheBuildInfo = (getText: () => string | undefined) => { - if (buildInfo === undefined) { - const result = getText(); - buildInfo = result !== undefined ? getBuildInfo(result) : false; + prependNode.texts = prependTexts || emptyArray; + (texts || (texts = [])).push(prependNode); + break; + case BundleFileSectionKind.Internal: + if (stripInternal) { + if (!texts) + texts = []; + break; } - return buildInfo || undefined; - }; - node.javascriptPath = declarationTextOrJavascriptPath; - node.javascriptMapPath = javascriptMapPath; - node.declarationPath = Debug.checkDefined(javascriptMapTextOrDeclarationPath); - node.declarationMapPath = declarationMapPath; - node.buildInfoPath = declarationMapTextOrBuildInfoPath; - Object.defineProperties(node, { - javascriptText: { get() { return definedTextGetter(declarationTextOrJavascriptPath); } }, - javascriptMapText: { get() { return textGetter(javascriptMapPath); } }, // TODO:: if there is inline sourceMap in jsFile, use that - declarationText: { get() { return definedTextGetter(Debug.checkDefined(javascriptMapTextOrDeclarationPath)); } }, - declarationMapText: { get() { return textGetter(declarationMapPath); } }, // TODO:: if there is inline sourceMap in dtsFile, use that - buildInfo: { get() { return getAndCacheBuildInfo(() => textGetter(declarationMapTextOrBuildInfoPath)); } } - }); - } - else { - node.javascriptText = javascriptTextOrReadFileText; - node.javascriptMapPath = javascriptMapPath; - node.javascriptMapText = javascriptMapTextOrDeclarationPath; - node.declarationText = declarationTextOrJavascriptPath; - node.declarationMapPath = declarationMapPath; - node.declarationMapText = declarationMapTextOrBuildInfoPath; - node.javascriptPath = javascriptPath; - node.declarationPath = declarationPath; - node.buildInfoPath = buildInfoPath; - node.buildInfo = buildInfo; - node.oldFileOfCurrentEmit = oldFileOfCurrentEmit; + // falls through + case BundleFileSectionKind.Text: + (texts || (texts = [])).push((createUnparsedNode(section, node) as UnparsedTextLike)); + break; + default: + Debug.assertNever(section); } - return node; } - - export function updateBundle(node: Bundle, sourceFiles: readonly SourceFile[], prepends: readonly (UnparsedSource | InputFiles)[] = emptyArray) { - if (node.sourceFiles !== sourceFiles || node.prepends !== prepends) { - return createBundle(sourceFiles, prepends); + node.prologues = prologues || emptyArray; + node.helpers = helpers; + node.referencedFiles = referencedFiles || emptyArray; + node.typeReferenceDirectives = typeReferenceDirectives; + node.libReferenceDirectives = libReferenceDirectives || emptyArray; + node.texts = texts || [(createUnparsedNode({ kind: BundleFileSectionKind.Text, pos: 0, end: node.text.length }, node))]; +} +function parseOldFileOfCurrentEmit(node: UnparsedSource, bundleFileInfo: BundleFileInfo) { + Debug.assert(!!node.oldFileOfCurrentEmit); + let texts: UnparsedTextLike[] | undefined; + let syntheticReferences: UnparsedSyntheticReference[] | undefined; + for (const section of bundleFileInfo.sections) { + switch (section.kind) { + case BundleFileSectionKind.Internal: + case BundleFileSectionKind.Text: + (texts || (texts = [])).push((createUnparsedNode(section, node) as UnparsedTextLike)); + break; + case BundleFileSectionKind.NoDefaultLib: + case BundleFileSectionKind.Reference: + case BundleFileSectionKind.Type: + case BundleFileSectionKind.Lib: + (syntheticReferences || (syntheticReferences = [])).push(createUnparsedSyntheticReference(section, node)); + break; + // Ignore + case BundleFileSectionKind.Prologue: + case BundleFileSectionKind.EmitHelpers: + case BundleFileSectionKind.Prepend: + break; + default: + Debug.assertNever(section); } - return node; - } - - // Compound nodes - - export function createImmediatelyInvokedFunctionExpression(statements: readonly Statement[]): CallExpression; - export function createImmediatelyInvokedFunctionExpression(statements: readonly Statement[], param: ParameterDeclaration, paramValue: Expression): CallExpression; - export function createImmediatelyInvokedFunctionExpression(statements: readonly Statement[], param?: ParameterDeclaration, paramValue?: Expression) { - return createCall( - createFunctionExpression( - /*modifiers*/ undefined, - /*asteriskToken*/ undefined, - /*name*/ undefined, - /*typeParameters*/ undefined, - /*parameters*/ param ? [param] : [], - /*type*/ undefined, - createBlock(statements, /*multiLine*/ true) - ), - /*typeArguments*/ undefined, - /*argumentsArray*/ paramValue ? [paramValue] : [] - ); - } - - export function createImmediatelyInvokedArrowFunction(statements: readonly Statement[]): CallExpression; - export function createImmediatelyInvokedArrowFunction(statements: readonly Statement[], param: ParameterDeclaration, paramValue: Expression): CallExpression; - export function createImmediatelyInvokedArrowFunction(statements: readonly Statement[], param?: ParameterDeclaration, paramValue?: Expression) { - return createCall( - createArrowFunction( - /*modifiers*/ undefined, - /*typeParameters*/ undefined, - /*parameters*/ param ? [param] : [], - /*type*/ undefined, - /*equalsGreaterThanToken*/ undefined, - createBlock(statements, /*multiLine*/ true) - ), - /*typeArguments*/ undefined, - /*argumentsArray*/ paramValue ? [paramValue] : [] - ); - } - - - export function createComma(left: Expression, right: Expression) { - return createBinary(left, SyntaxKind.CommaToken, right); - } - - export function createLessThan(left: Expression, right: Expression) { - return createBinary(left, SyntaxKind.LessThanToken, right); - } - - export function createAssignment(left: ObjectLiteralExpression | ArrayLiteralExpression, right: Expression): DestructuringAssignment; - export function createAssignment(left: Expression, right: Expression): BinaryExpression; - export function createAssignment(left: Expression, right: Expression) { - return createBinary(left, SyntaxKind.EqualsToken, right); - } - - export function createStrictEquality(left: Expression, right: Expression) { - return createBinary(left, SyntaxKind.EqualsEqualsEqualsToken, right); } - - export function createStrictInequality(left: Expression, right: Expression) { - return createBinary(left, SyntaxKind.ExclamationEqualsEqualsToken, right); - } - - export function createAdd(left: Expression, right: Expression) { - return createBinary(left, SyntaxKind.PlusToken, right); - } - - export function createSubtract(left: Expression, right: Expression) { - return createBinary(left, SyntaxKind.MinusToken, right); - } - - export function createPostfixIncrement(operand: Expression) { - return createPostfix(operand, SyntaxKind.PlusPlusToken); - } - - export function createLogicalAnd(left: Expression, right: Expression) { - return createBinary(left, SyntaxKind.AmpersandAmpersandToken, right); - } - - export function createLogicalOr(left: Expression, right: Expression) { - return createBinary(left, SyntaxKind.BarBarToken, right); - } - - export function createNullishCoalesce(left: Expression, right: Expression) { - return createBinary(left, SyntaxKind.QuestionQuestionToken, right); - } - - export function createLogicalNot(operand: Expression) { - return createPrefix(SyntaxKind.ExclamationToken, operand); - } - - export function createVoidZero() { - return createVoid(createLiteral(0)); - } - - export function createExportDefault(expression: Expression) { - return createExportAssignment(/*decorators*/ undefined, /*modifiers*/ undefined, /*isExportEquals*/ false, expression); - } - - export function createExternalModuleExport(exportName: Identifier) { - return createExportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, createNamedExports([createExportSpecifier(/*propertyName*/ undefined, exportName)])); + node.texts = texts || emptyArray; + node.helpers = map(bundleFileInfo.sources && bundleFileInfo.sources.helpers, name => getAllUnscopedEmitHelpers().get(name)!); + node.syntheticReferences = syntheticReferences; + return node; +} +function mapBundleFileSectionKindToSyntaxKind(kind: BundleFileSectionKind): SyntaxKind { + switch (kind) { + case BundleFileSectionKind.Prologue: return SyntaxKind.UnparsedPrologue; + case BundleFileSectionKind.Prepend: return SyntaxKind.UnparsedPrepend; + case BundleFileSectionKind.Internal: return SyntaxKind.UnparsedInternalText; + case BundleFileSectionKind.Text: return SyntaxKind.UnparsedText; + case BundleFileSectionKind.EmitHelpers: + case BundleFileSectionKind.NoDefaultLib: + case BundleFileSectionKind.Reference: + case BundleFileSectionKind.Type: + case BundleFileSectionKind.Lib: + return Debug.fail(`BundleFileSectionKind: ${kind} not yet mapped to SyntaxKind`); + default: + return Debug.assertNever(kind); } - - // Utilities - - function asName(name: string | T): T | Identifier { - return isString(name) ? createIdentifier(name) : name; +} +function createUnparsedNode(section: BundleFileSection, parent: UnparsedSource): UnparsedNode { + const node = (createNode(mapBundleFileSectionKindToSyntaxKind(section.kind), section.pos, section.end) as UnparsedNode); + node.parent = parent; + node.data = section.data; + return node; +} +function createUnparsedSyntheticReference(section: BundleFileHasNoDefaultLib | BundleFileReference, parent: UnparsedSource) { + const node = (createNode(SyntaxKind.UnparsedSyntheticReference, section.pos, section.end) as UnparsedSyntheticReference); + node.parent = parent; + node.data = section.data; + node.section = section; + return node; +} +export function createInputFiles(javascriptText: string, declarationText: string): InputFiles; +export function createInputFiles(readFileText: (path: string) => string | undefined, javascriptPath: string, javascriptMapPath: string | undefined, declarationPath: string, declarationMapPath: string | undefined, buildInfoPath: string | undefined): InputFiles; +export function createInputFiles(javascriptText: string, declarationText: string, javascriptMapPath: string | undefined, javascriptMapText: string | undefined, declarationMapPath: string | undefined, declarationMapText: string | undefined): InputFiles; +/*@internal*/ +export function createInputFiles(javascriptText: string, declarationText: string, javascriptMapPath: string | undefined, javascriptMapText: string | undefined, declarationMapPath: string | undefined, declarationMapText: string | undefined, javascriptPath: string | undefined, declarationPath: string | undefined, buildInfoPath?: string | undefined, buildInfo?: BuildInfo, oldFileOfCurrentEmit?: boolean): InputFiles; +export function createInputFiles(javascriptTextOrReadFileText: string | ((path: string) => string | undefined), declarationTextOrJavascriptPath: string, javascriptMapPath?: string, javascriptMapTextOrDeclarationPath?: string, declarationMapPath?: string, declarationMapTextOrBuildInfoPath?: string, javascriptPath?: string | undefined, declarationPath?: string | undefined, buildInfoPath?: string | undefined, buildInfo?: BuildInfo, oldFileOfCurrentEmit?: boolean): InputFiles { + const node = (createNode(SyntaxKind.InputFiles)); + if (!isString(javascriptTextOrReadFileText)) { + const cache = createMap(); + const textGetter = (path: string | undefined) => { + if (path === undefined) + return undefined; + let value = cache.get(path); + if (value === undefined) { + value = javascriptTextOrReadFileText(path); + cache.set(path, value !== undefined ? value : false); + } + return value !== false ? value as string : undefined; + }; + const definedTextGetter = (path: string) => { + const result = textGetter(path); + return result !== undefined ? result : `/* Input file ${path} was missing */\r\n`; + }; + let buildInfo: BuildInfo | false; + const getAndCacheBuildInfo = (getText: () => string | undefined) => { + if (buildInfo === undefined) { + const result = getText(); + buildInfo = result !== undefined ? getBuildInfo(result) : false; + } + return buildInfo || undefined; + }; + node.javascriptPath = declarationTextOrJavascriptPath; + node.javascriptMapPath = javascriptMapPath; + node.declarationPath = Debug.checkDefined(javascriptMapTextOrDeclarationPath); + node.declarationMapPath = declarationMapPath; + node.buildInfoPath = declarationMapTextOrBuildInfoPath; + Object.defineProperties(node, { + javascriptText: { get() { return definedTextGetter(declarationTextOrJavascriptPath); } }, + javascriptMapText: { get() { return textGetter(javascriptMapPath); } }, + declarationText: { get() { return definedTextGetter(Debug.checkDefined(javascriptMapTextOrDeclarationPath)); } }, + declarationMapText: { get() { return textGetter(declarationMapPath); } }, + buildInfo: { get() { return getAndCacheBuildInfo(() => textGetter(declarationMapTextOrBuildInfoPath)); } } + }); + } + else { + node.javascriptText = javascriptTextOrReadFileText; + node.javascriptMapPath = javascriptMapPath; + node.javascriptMapText = javascriptMapTextOrDeclarationPath; + node.declarationText = declarationTextOrJavascriptPath; + node.declarationMapPath = declarationMapPath; + node.declarationMapText = declarationMapTextOrBuildInfoPath; + node.javascriptPath = javascriptPath; + node.declarationPath = declarationPath; + node.buildInfoPath = buildInfoPath; + node.buildInfo = buildInfo; + node.oldFileOfCurrentEmit = oldFileOfCurrentEmit; + } + return node; +} +export function updateBundle(node: Bundle, sourceFiles: readonly SourceFile[], prepends: readonly (UnparsedSource | InputFiles)[] = emptyArray) { + if (node.sourceFiles !== sourceFiles || node.prepends !== prepends) { + return createBundle(sourceFiles, prepends); } - - function asExpression(value: string | number | boolean | T): T | StringLiteral | NumericLiteral | BooleanLiteral { - return typeof value === "string" ? createStringLiteral(value) : - typeof value === "number" ? createNumericLiteral(""+value) : + return node; +} +// Compound nodes +export function createImmediatelyInvokedFunctionExpression(statements: readonly Statement[]): CallExpression; +export function createImmediatelyInvokedFunctionExpression(statements: readonly Statement[], param: ParameterDeclaration, paramValue: Expression): CallExpression; +export function createImmediatelyInvokedFunctionExpression(statements: readonly Statement[], param?: ParameterDeclaration, paramValue?: Expression) { + return createCall(createFunctionExpression( + /*modifiers*/ undefined, + /*asteriskToken*/ undefined, + /*name*/ undefined, + /*typeParameters*/ undefined, + /*parameters*/ param ? [param] : [], + /*type*/ undefined, createBlock(statements, /*multiLine*/ true)), + /*typeArguments*/ undefined, + /*argumentsArray*/ paramValue ? [paramValue] : []); +} +export function createImmediatelyInvokedArrowFunction(statements: readonly Statement[]): CallExpression; +export function createImmediatelyInvokedArrowFunction(statements: readonly Statement[], param: ParameterDeclaration, paramValue: Expression): CallExpression; +export function createImmediatelyInvokedArrowFunction(statements: readonly Statement[], param?: ParameterDeclaration, paramValue?: Expression) { + return createCall(createArrowFunction( + /*modifiers*/ undefined, + /*typeParameters*/ undefined, + /*parameters*/ param ? [param] : [], + /*type*/ undefined, + /*equalsGreaterThanToken*/ undefined, createBlock(statements, /*multiLine*/ true)), + /*typeArguments*/ undefined, + /*argumentsArray*/ paramValue ? [paramValue] : []); +} +export function createComma(left: Expression, right: Expression) { + return createBinary(left, SyntaxKind.CommaToken, right); +} +export function createLessThan(left: Expression, right: Expression) { + return createBinary(left, SyntaxKind.LessThanToken, right); +} +export function createAssignment(left: ObjectLiteralExpression | ArrayLiteralExpression, right: Expression): DestructuringAssignment; +export function createAssignment(left: Expression, right: Expression): BinaryExpression; +export function createAssignment(left: Expression, right: Expression) { + return createBinary(left, SyntaxKind.EqualsToken, right); +} +export function createStrictEquality(left: Expression, right: Expression) { + return createBinary(left, SyntaxKind.EqualsEqualsEqualsToken, right); +} +export function createStrictInequality(left: Expression, right: Expression) { + return createBinary(left, SyntaxKind.ExclamationEqualsEqualsToken, right); +} +export function createAdd(left: Expression, right: Expression) { + return createBinary(left, SyntaxKind.PlusToken, right); +} +export function createSubtract(left: Expression, right: Expression) { + return createBinary(left, SyntaxKind.MinusToken, right); +} +export function createPostfixIncrement(operand: Expression) { + return createPostfix(operand, SyntaxKind.PlusPlusToken); +} +export function createLogicalAnd(left: Expression, right: Expression) { + return createBinary(left, SyntaxKind.AmpersandAmpersandToken, right); +} +export function createLogicalOr(left: Expression, right: Expression) { + return createBinary(left, SyntaxKind.BarBarToken, right); +} +export function createNullishCoalesce(left: Expression, right: Expression) { + return createBinary(left, SyntaxKind.QuestionQuestionToken, right); +} +export function createLogicalNot(operand: Expression) { + return createPrefix(SyntaxKind.ExclamationToken, operand); +} +export function createVoidZero() { + return createVoid(createLiteral(0)); +} +export function createExportDefault(expression: Expression) { + return createExportAssignment(/*decorators*/ undefined, /*modifiers*/ undefined, /*isExportEquals*/ false, expression); +} +export function createExternalModuleExport(exportName: Identifier) { + return createExportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, createNamedExports([createExportSpecifier(/*propertyName*/ undefined, exportName)])); +} +// Utilities +function asName(name: string | T): T | Identifier { + return isString(name) ? createIdentifier(name) : name; +} +function asExpression(value: string | number | boolean | T): T | StringLiteral | NumericLiteral | BooleanLiteral { + return typeof value === "string" ? createStringLiteral(value) : + typeof value === "number" ? createNumericLiteral("" + value) : typeof value === "boolean" ? value ? createTrue() : createFalse() : - value; - } - - function asNodeArray(array: readonly T[]): NodeArray; - function asNodeArray(array: readonly T[] | undefined): NodeArray | undefined; - function asNodeArray(array: readonly T[] | undefined): NodeArray | undefined { - return array ? createNodeArray(array) : undefined; - } - - function asToken(value: TKind | Token): Token { - return typeof value === "number" ? createToken(value) : value; - } - - function asEmbeddedStatement(statement: T): T | EmptyStatement; - function asEmbeddedStatement(statement: T | undefined): T | EmptyStatement | undefined; - function asEmbeddedStatement(statement: T | undefined): T | EmptyStatement | undefined { - return statement && isNotEmittedStatement(statement) ? setTextRange(setOriginalNode(createEmptyStatement(), statement), statement) : statement; - } - - /** - * Clears any EmitNode entries from parse-tree nodes. - * @param sourceFile A source file. - */ - export function disposeEmitNodes(sourceFile: SourceFile) { - // During transformation we may need to annotate a parse tree node with transient - // transformation properties. As parse tree nodes live longer than transformation - // nodes, we need to make sure we reclaim any memory allocated for custom ranges - // from these nodes to ensure we do not hold onto entire subtrees just for position - // information. We also need to reset these nodes to a pre-transformation state - // for incremental parsing scenarios so that we do not impact later emit. - sourceFile = getSourceFileOfNode(getParseTreeNode(sourceFile)); - const emitNode = sourceFile && sourceFile.emitNode; - const annotatedNodes = emitNode && emitNode.annotatedNodes; - if (annotatedNodes) { - for (const node of annotatedNodes) { - node.emitNode = undefined; - } + value; +} +function asNodeArray(array: readonly T[]): NodeArray; +function asNodeArray(array: readonly T[] | undefined): NodeArray | undefined; +function asNodeArray(array: readonly T[] | undefined): NodeArray | undefined { + return array ? createNodeArray(array) : undefined; +} +function asToken(value: TKind | Token): Token { + return typeof value === "number" ? createToken(value) : value; +} +function asEmbeddedStatement(statement: T): T | EmptyStatement; +function asEmbeddedStatement(statement: T | undefined): T | EmptyStatement | undefined; +function asEmbeddedStatement(statement: T | undefined): T | EmptyStatement | undefined { + return statement && isNotEmittedStatement(statement) ? setTextRange(setOriginalNode(createEmptyStatement(), statement), statement) : statement; +} +/** + * Clears any EmitNode entries from parse-tree nodes. + * @param sourceFile A source file. + */ +export function disposeEmitNodes(sourceFile: SourceFile) { + // During transformation we may need to annotate a parse tree node with transient + // transformation properties. As parse tree nodes live longer than transformation + // nodes, we need to make sure we reclaim any memory allocated for custom ranges + // from these nodes to ensure we do not hold onto entire subtrees just for position + // information. We also need to reset these nodes to a pre-transformation state + // for incremental parsing scenarios so that we do not impact later emit. + sourceFile = getSourceFileOfNode(getParseTreeNode(sourceFile)); + const emitNode = sourceFile && sourceFile.emitNode; + const annotatedNodes = emitNode && emitNode.annotatedNodes; + if (annotatedNodes) { + for (const node of annotatedNodes) { + node.emitNode = undefined; } } - - /** - * Associates a node with the current transformation, initializing - * various transient transformation properties. - */ - /* @internal */ - export function getOrCreateEmitNode(node: Node): EmitNode { - if (!node.emitNode) { - if (isParseTreeNode(node)) { - // To avoid holding onto transformation artifacts, we keep track of any - // parse tree node we are annotating. This allows us to clean them up after - // all transformations have completed. - if (node.kind === SyntaxKind.SourceFile) { - return node.emitNode = { annotatedNodes: [node] } as EmitNode; - } - - const sourceFile = getSourceFileOfNode(getParseTreeNode(getSourceFileOfNode(node))); - getOrCreateEmitNode(sourceFile).annotatedNodes!.push(node); +} +/** + * Associates a node with the current transformation, initializing + * various transient transformation properties. + */ +/* @internal */ +export function getOrCreateEmitNode(node: Node): EmitNode { + if (!node.emitNode) { + if (isParseTreeNode(node)) { + // To avoid holding onto transformation artifacts, we keep track of any + // parse tree node we are annotating. This allows us to clean them up after + // all transformations have completed. + if (node.kind === SyntaxKind.SourceFile) { + return node.emitNode = ({ annotatedNodes: [node] } as EmitNode); } - - node.emitNode = {} as EmitNode; - } - - return node.emitNode; - } - - /** - * Sets `EmitFlags.NoComments` on a node and removes any leading and trailing synthetic comments. - * @internal - */ - export function removeAllComments(node: T): T { - const emitNode = getOrCreateEmitNode(node); - emitNode.flags |= EmitFlags.NoComments; - emitNode.leadingComments = undefined; - emitNode.trailingComments = undefined; - return node; - } - - export function setTextRange(range: T, location: TextRange | undefined): T { - if (location) { - range.pos = location.pos; - range.end = location.end; + const sourceFile = getSourceFileOfNode(getParseTreeNode(getSourceFileOfNode(node))); + getOrCreateEmitNode(sourceFile).annotatedNodes!.push(node); } - return range; - } - - /** - * Sets flags that control emit behavior of a node. - */ - export function setEmitFlags(node: T, emitFlags: EmitFlags) { - getOrCreateEmitNode(node).flags = emitFlags; - return node; - } - - /** - * Sets flags that control emit behavior of a node. - */ - /* @internal */ - export function addEmitFlags(node: T, emitFlags: EmitFlags) { - const emitNode = getOrCreateEmitNode(node); - emitNode.flags = emitNode.flags | emitFlags; - return node; - } - - /** - * Gets a custom text range to use when emitting source maps. - */ - export function getSourceMapRange(node: Node): SourceMapRange { - const emitNode = node.emitNode; - return (emitNode && emitNode.sourceMapRange) || node; - } - - /** - * Sets a custom text range to use when emitting source maps. - */ - export function setSourceMapRange(node: T, range: SourceMapRange | undefined) { - getOrCreateEmitNode(node).sourceMapRange = range; - return node; - } - - let SourceMapSource: new (fileName: string, text: string, skipTrivia?: (pos: number) => number) => SourceMapSource; - - /** - * Create an external source map source file reference - */ - export function createSourceMapSource(fileName: string, text: string, skipTrivia?: (pos: number) => number): SourceMapSource { - return new (SourceMapSource || (SourceMapSource = objectAllocator.getSourceMapSourceConstructor()))(fileName, text, skipTrivia); - } - - /** - * Gets the TextRange to use for source maps for a token of a node. - */ - export function getTokenSourceMapRange(node: Node, token: SyntaxKind): SourceMapRange | undefined { - const emitNode = node.emitNode; - const tokenSourceMapRanges = emitNode && emitNode.tokenSourceMapRanges; - return tokenSourceMapRanges && tokenSourceMapRanges[token]; - } - - /** - * Sets the TextRange to use for source maps for a token of a node. - */ - export function setTokenSourceMapRange(node: T, token: SyntaxKind, range: SourceMapRange | undefined) { - const emitNode = getOrCreateEmitNode(node); - const tokenSourceMapRanges = emitNode.tokenSourceMapRanges || (emitNode.tokenSourceMapRanges = []); - tokenSourceMapRanges[token] = range; - return node; - } - - /** - * Gets a custom text range to use when emitting comments. - */ - /*@internal*/ - export function getStartsOnNewLine(node: Node) { - const emitNode = node.emitNode; - return emitNode && emitNode.startsOnNewLine; - } - - /** - * Sets a custom text range to use when emitting comments. - */ - /*@internal*/ - export function setStartsOnNewLine(node: T, newLine: boolean) { - getOrCreateEmitNode(node).startsOnNewLine = newLine; - return node; - } - - /** - * Gets a custom text range to use when emitting comments. - */ - export function getCommentRange(node: Node) { - const emitNode = node.emitNode; - return (emitNode && emitNode.commentRange) || node; - } - - /** - * Sets a custom text range to use when emitting comments. - */ - export function setCommentRange(node: T, range: TextRange) { - getOrCreateEmitNode(node).commentRange = range; - return node; - } - - export function getSyntheticLeadingComments(node: Node): SynthesizedComment[] | undefined { - const emitNode = node.emitNode; - return emitNode && emitNode.leadingComments; - } - - export function setSyntheticLeadingComments(node: T, comments: SynthesizedComment[] | undefined) { - getOrCreateEmitNode(node).leadingComments = comments; - return node; - } - - export function addSyntheticLeadingComment(node: T, kind: SyntaxKind.SingleLineCommentTrivia | SyntaxKind.MultiLineCommentTrivia, text: string, hasTrailingNewLine?: boolean) { - return setSyntheticLeadingComments(node, append(getSyntheticLeadingComments(node), { kind, pos: -1, end: -1, hasTrailingNewLine, text })); - } - - export function getSyntheticTrailingComments(node: Node): SynthesizedComment[] | undefined { - const emitNode = node.emitNode; - return emitNode && emitNode.trailingComments; - } - - export function setSyntheticTrailingComments(node: T, comments: SynthesizedComment[] | undefined) { - getOrCreateEmitNode(node).trailingComments = comments; - return node; - } - - export function addSyntheticTrailingComment(node: T, kind: SyntaxKind.SingleLineCommentTrivia | SyntaxKind.MultiLineCommentTrivia, text: string, hasTrailingNewLine?: boolean) { - return setSyntheticTrailingComments(node, append(getSyntheticTrailingComments(node), { kind, pos: -1, end: -1, hasTrailingNewLine, text })); - } - - export function moveSyntheticComments(node: T, original: Node): T { - setSyntheticLeadingComments(node, getSyntheticLeadingComments(original)); - setSyntheticTrailingComments(node, getSyntheticTrailingComments(original)); - const emit = getOrCreateEmitNode(original); - emit.leadingComments = undefined; - emit.trailingComments = undefined; - return node; - } - - /** - * Gets the constant value to emit for an expression. - */ - export function getConstantValue(node: PropertyAccessExpression | ElementAccessExpression): string | number | undefined { - const emitNode = node.emitNode; - return emitNode && emitNode.constantValue; + node.emitNode = ({} as EmitNode); } - - /** - * Sets the constant value to emit for an expression. - */ - export function setConstantValue(node: PropertyAccessExpression | ElementAccessExpression, value: string | number): PropertyAccessExpression | ElementAccessExpression { - const emitNode = getOrCreateEmitNode(node); - emitNode.constantValue = value; - return node; + return node.emitNode; +} +/** + * Sets `EmitFlags.NoComments` on a node and removes any leading and trailing synthetic comments. + * @internal + */ +export function removeAllComments(node: T): T { + const emitNode = getOrCreateEmitNode(node); + emitNode.flags |= EmitFlags.NoComments; + emitNode.leadingComments = undefined; + emitNode.trailingComments = undefined; + return node; +} +export function setTextRange(range: T, location: TextRange | undefined): T { + if (location) { + range.pos = location.pos; + range.end = location.end; } - - /** - * Adds an EmitHelper to a node. - */ - export function addEmitHelper(node: T, helper: EmitHelper): T { + return range; +} +/** + * Sets flags that control emit behavior of a node. + */ +export function setEmitFlags(node: T, emitFlags: EmitFlags) { + getOrCreateEmitNode(node).flags = emitFlags; + return node; +} +/** + * Sets flags that control emit behavior of a node. + */ +/* @internal */ +export function addEmitFlags(node: T, emitFlags: EmitFlags) { + const emitNode = getOrCreateEmitNode(node); + emitNode.flags = emitNode.flags | emitFlags; + return node; +} +/** + * Gets a custom text range to use when emitting source maps. + */ +export function getSourceMapRange(node: Node): SourceMapRange { + const emitNode = node.emitNode; + return (emitNode && emitNode.sourceMapRange) || node; +} +/** + * Sets a custom text range to use when emitting source maps. + */ +export function setSourceMapRange(node: T, range: SourceMapRange | undefined) { + getOrCreateEmitNode(node).sourceMapRange = range; + return node; +} +let SourceMapSource: new (fileName: string, text: string, skipTrivia?: (pos: number) => number) => ts.SourceMapSource; +/** + * Create an external source map source file reference + */ +export function createSourceMapSource(fileName: string, text: string, skipTrivia?: (pos: number) => number): ts.SourceMapSource { + return new (SourceMapSource || (SourceMapSource = objectAllocator.getSourceMapSourceConstructor()))(fileName, text, skipTrivia); +} +/** + * Gets the TextRange to use for source maps for a token of a node. + */ +export function getTokenSourceMapRange(node: Node, token: SyntaxKind): SourceMapRange | undefined { + const emitNode = node.emitNode; + const tokenSourceMapRanges = emitNode && emitNode.tokenSourceMapRanges; + return tokenSourceMapRanges && tokenSourceMapRanges[token]; +} +/** + * Sets the TextRange to use for source maps for a token of a node. + */ +export function setTokenSourceMapRange(node: T, token: SyntaxKind, range: SourceMapRange | undefined) { + const emitNode = getOrCreateEmitNode(node); + const tokenSourceMapRanges = emitNode.tokenSourceMapRanges || (emitNode.tokenSourceMapRanges = []); + tokenSourceMapRanges[token] = range; + return node; +} +/** + * Gets a custom text range to use when emitting comments. + */ +/*@internal*/ +export function getStartsOnNewLine(node: Node) { + const emitNode = node.emitNode; + return emitNode && emitNode.startsOnNewLine; +} +/** + * Sets a custom text range to use when emitting comments. + */ +/*@internal*/ +export function setStartsOnNewLine(node: T, newLine: boolean) { + getOrCreateEmitNode(node).startsOnNewLine = newLine; + return node; +} +/** + * Gets a custom text range to use when emitting comments. + */ +export function getCommentRange(node: Node) { + const emitNode = node.emitNode; + return (emitNode && emitNode.commentRange) || node; +} +/** + * Sets a custom text range to use when emitting comments. + */ +export function setCommentRange(node: T, range: TextRange) { + getOrCreateEmitNode(node).commentRange = range; + return node; +} +export function getSyntheticLeadingComments(node: Node): SynthesizedComment[] | undefined { + const emitNode = node.emitNode; + return emitNode && emitNode.leadingComments; +} +export function setSyntheticLeadingComments(node: T, comments: SynthesizedComment[] | undefined) { + getOrCreateEmitNode(node).leadingComments = comments; + return node; +} +export function addSyntheticLeadingComment(node: T, kind: SyntaxKind.SingleLineCommentTrivia | SyntaxKind.MultiLineCommentTrivia, text: string, hasTrailingNewLine?: boolean) { + return setSyntheticLeadingComments(node, append(getSyntheticLeadingComments(node), { kind, pos: -1, end: -1, hasTrailingNewLine, text })); +} +export function getSyntheticTrailingComments(node: Node): SynthesizedComment[] | undefined { + const emitNode = node.emitNode; + return emitNode && emitNode.trailingComments; +} +export function setSyntheticTrailingComments(node: T, comments: SynthesizedComment[] | undefined) { + getOrCreateEmitNode(node).trailingComments = comments; + return node; +} +export function addSyntheticTrailingComment(node: T, kind: SyntaxKind.SingleLineCommentTrivia | SyntaxKind.MultiLineCommentTrivia, text: string, hasTrailingNewLine?: boolean) { + return setSyntheticTrailingComments(node, append(getSyntheticTrailingComments(node), { kind, pos: -1, end: -1, hasTrailingNewLine, text })); +} +export function moveSyntheticComments(node: T, original: Node): T { + setSyntheticLeadingComments(node, getSyntheticLeadingComments(original)); + setSyntheticTrailingComments(node, getSyntheticTrailingComments(original)); + const emit = getOrCreateEmitNode(original); + emit.leadingComments = undefined; + emit.trailingComments = undefined; + return node; +} +/** + * Gets the constant value to emit for an expression. + */ +export function getConstantValue(node: PropertyAccessExpression | ElementAccessExpression): string | number | undefined { + const emitNode = node.emitNode; + return emitNode && emitNode.constantValue; +} +/** + * Sets the constant value to emit for an expression. + */ +export function setConstantValue(node: PropertyAccessExpression | ElementAccessExpression, value: string | number): PropertyAccessExpression | ElementAccessExpression { + const emitNode = getOrCreateEmitNode(node); + emitNode.constantValue = value; + return node; +} +/** + * Adds an EmitHelper to a node. + */ +export function addEmitHelper(node: T, helper: EmitHelper): T { + const emitNode = getOrCreateEmitNode(node); + emitNode.helpers = append(emitNode.helpers, helper); + return node; +} +/** + * Add EmitHelpers to a node. + */ +export function addEmitHelpers(node: T, helpers: EmitHelper[] | undefined): T { + if (some(helpers)) { const emitNode = getOrCreateEmitNode(node); - emitNode.helpers = append(emitNode.helpers, helper); - return node; - } - - /** - * Add EmitHelpers to a node. - */ - export function addEmitHelpers(node: T, helpers: EmitHelper[] | undefined): T { - if (some(helpers)) { - const emitNode = getOrCreateEmitNode(node); - for (const helper of helpers) { - emitNode.helpers = appendIfUnique(emitNode.helpers, helper); - } + for (const helper of helpers) { + emitNode.helpers = appendIfUnique(emitNode.helpers, helper); } - return node; } - - /** - * Removes an EmitHelper from a node. - */ - export function removeEmitHelper(node: Node, helper: EmitHelper): boolean { - const emitNode = node.emitNode; - if (emitNode) { - const helpers = emitNode.helpers; - if (helpers) { - return orderedRemoveItem(helpers, helper); - } + return node; +} +/** + * Removes an EmitHelper from a node. + */ +export function removeEmitHelper(node: Node, helper: EmitHelper): boolean { + const emitNode = node.emitNode; + if (emitNode) { + const helpers = emitNode.helpers; + if (helpers) { + return orderedRemoveItem(helpers, helper); } - return false; } - - /** - * Gets the EmitHelpers of a node. - */ - export function getEmitHelpers(node: Node): EmitHelper[] | undefined { - const emitNode = node.emitNode; - return emitNode && emitNode.helpers; - } - - /** - * Moves matching emit helpers from a source node to a target node. - */ - export function moveEmitHelpers(source: Node, target: Node, predicate: (helper: EmitHelper) => boolean) { - const sourceEmitNode = source.emitNode; - const sourceEmitHelpers = sourceEmitNode && sourceEmitNode.helpers; - if (!some(sourceEmitHelpers)) return; - - const targetEmitNode = getOrCreateEmitNode(target); - let helpersRemoved = 0; - for (let i = 0; i < sourceEmitHelpers.length; i++) { - const helper = sourceEmitHelpers[i]; - if (predicate(helper)) { - helpersRemoved++; - targetEmitNode.helpers = appendIfUnique(targetEmitNode.helpers, helper); - } - else if (helpersRemoved > 0) { - sourceEmitHelpers[i - helpersRemoved] = helper; - } - } - - if (helpersRemoved > 0) { - sourceEmitHelpers.length -= helpersRemoved; + return false; +} +/** + * Gets the EmitHelpers of a node. + */ +export function getEmitHelpers(node: Node): EmitHelper[] | undefined { + const emitNode = node.emitNode; + return emitNode && emitNode.helpers; +} +/** + * Moves matching emit helpers from a source node to a target node. + */ +export function moveEmitHelpers(source: Node, target: Node, predicate: (helper: EmitHelper) => boolean) { + const sourceEmitNode = source.emitNode; + const sourceEmitHelpers = sourceEmitNode && sourceEmitNode.helpers; + if (!some(sourceEmitHelpers)) + return; + const targetEmitNode = getOrCreateEmitNode(target); + let helpersRemoved = 0; + for (let i = 0; i < sourceEmitHelpers.length; i++) { + const helper = sourceEmitHelpers[i]; + if (predicate(helper)) { + helpersRemoved++; + targetEmitNode.helpers = appendIfUnique(targetEmitNode.helpers, helper); } - } - - /* @internal */ - export function compareEmitHelpers(x: EmitHelper, y: EmitHelper) { - if (x === y) return Comparison.EqualTo; - if (x.priority === y.priority) return Comparison.EqualTo; - if (x.priority === undefined) return Comparison.GreaterThan; - if (y.priority === undefined) return Comparison.LessThan; - return compareValues(x.priority, y.priority); - } - - export function setOriginalNode(node: T, original: Node | undefined): T { - node.original = original; - if (original) { - const emitNode = original.emitNode; - if (emitNode) node.emitNode = mergeEmitNode(emitNode, node.emitNode); + else if (helpersRemoved > 0) { + sourceEmitHelpers[i - helpersRemoved] = helper; } - return node; } - - function mergeEmitNode(sourceEmitNode: EmitNode, destEmitNode: EmitNode | undefined) { - const { - flags, - leadingComments, - trailingComments, - commentRange, - sourceMapRange, - tokenSourceMapRanges, - constantValue, - helpers, - startsOnNewLine, - } = sourceEmitNode; - if (!destEmitNode) destEmitNode = {} as EmitNode; - // We are using `.slice()` here in case `destEmitNode.leadingComments` is pushed to later. - if (leadingComments) destEmitNode.leadingComments = addRange(leadingComments.slice(), destEmitNode.leadingComments); - if (trailingComments) destEmitNode.trailingComments = addRange(trailingComments.slice(), destEmitNode.trailingComments); - if (flags) destEmitNode.flags = flags; - if (commentRange) destEmitNode.commentRange = commentRange; - if (sourceMapRange) destEmitNode.sourceMapRange = sourceMapRange; - if (tokenSourceMapRanges) destEmitNode.tokenSourceMapRanges = mergeTokenSourceMapRanges(tokenSourceMapRanges, destEmitNode.tokenSourceMapRanges!); - if (constantValue !== undefined) destEmitNode.constantValue = constantValue; - if (helpers) destEmitNode.helpers = addRange(destEmitNode.helpers, helpers); - if (startsOnNewLine !== undefined) destEmitNode.startsOnNewLine = startsOnNewLine; - return destEmitNode; + if (helpersRemoved > 0) { + sourceEmitHelpers.length -= helpersRemoved; } - - function mergeTokenSourceMapRanges(sourceRanges: (TextRange | undefined)[], destRanges: (TextRange | undefined)[]) { - if (!destRanges) destRanges = []; - for (const key in sourceRanges) { - destRanges[key] = sourceRanges[key]; - } - return destRanges; +} +/* @internal */ +export function compareEmitHelpers(x: EmitHelper, y: EmitHelper) { + if (x === y) + return Comparison.EqualTo; + if (x.priority === y.priority) + return Comparison.EqualTo; + if (x.priority === undefined) + return Comparison.GreaterThan; + if (y.priority === undefined) + return Comparison.LessThan; + return compareValues(x.priority, y.priority); +} +export function setOriginalNode(node: T, original: Node | undefined): T { + node.original = original; + if (original) { + const emitNode = original.emitNode; + if (emitNode) + node.emitNode = mergeEmitNode(emitNode, node.emitNode); + } + return node; +} +function mergeEmitNode(sourceEmitNode: EmitNode, destEmitNode: EmitNode | undefined) { + const { flags, leadingComments, trailingComments, commentRange, sourceMapRange, tokenSourceMapRanges, constantValue, helpers, startsOnNewLine, } = sourceEmitNode; + if (!destEmitNode) + destEmitNode = ({} as EmitNode); + // We are using `.slice()` here in case `destEmitNode.leadingComments` is pushed to later. + if (leadingComments) + destEmitNode.leadingComments = addRange(leadingComments.slice(), destEmitNode.leadingComments); + if (trailingComments) + destEmitNode.trailingComments = addRange(trailingComments.slice(), destEmitNode.trailingComments); + if (flags) + destEmitNode.flags = flags; + if (commentRange) + destEmitNode.commentRange = commentRange; + if (sourceMapRange) + destEmitNode.sourceMapRange = sourceMapRange; + if (tokenSourceMapRanges) + destEmitNode.tokenSourceMapRanges = mergeTokenSourceMapRanges(tokenSourceMapRanges, destEmitNode.tokenSourceMapRanges!); + if (constantValue !== undefined) + destEmitNode.constantValue = constantValue; + if (helpers) + destEmitNode.helpers = addRange(destEmitNode.helpers, helpers); + if (startsOnNewLine !== undefined) + destEmitNode.startsOnNewLine = startsOnNewLine; + return destEmitNode; +} +function mergeTokenSourceMapRanges(sourceRanges: (TextRange | undefined)[], destRanges: (TextRange | undefined)[]) { + if (!destRanges) + destRanges = []; + for (const key in sourceRanges) { + destRanges[key] = sourceRanges[key]; } + return destRanges; } diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index 28550ab8cd103..15717c3c621d9 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -1,1519 +1,1350 @@ -namespace ts { - /* @internal */ - export function trace(host: ModuleResolutionHost, message: DiagnosticMessage, ...args: any[]): void; - export function trace(host: ModuleResolutionHost): void { - host.trace!(formatMessage.apply(undefined, arguments)); - } - - /* @internal */ - export function isTraceEnabled(compilerOptions: CompilerOptions, host: ModuleResolutionHost): boolean { - return !!compilerOptions.traceResolution && host.trace !== undefined; - } - - function withPackageId(packageInfo: PackageJsonInfo | undefined, r: PathAndExtension | undefined): Resolved | undefined { - let packageId: PackageId | undefined; - if (r && packageInfo) { - const packageJsonContent = packageInfo.packageJsonContent as PackageJson; - if (typeof packageJsonContent.name === "string" && typeof packageJsonContent.version === "string") { - packageId = { - name: packageJsonContent.name, - subModuleName: r.path.slice(packageInfo.packageDirectory.length + directorySeparator.length), - version: packageJsonContent.version - }; - } - } - return r && { path: r.path, extension: r.ext, packageId }; +import { ModuleResolutionHost, DiagnosticMessage, formatMessage, CompilerOptions, PackageId, directorySeparator, Debug, Extension, extensionIsTS, ResolvedModuleWithFailedLookupLocations, Push, MapLike, MatchingKeys, hasProperty, Diagnostics, normalizePath, combinePaths, VersionRange, versionMajorMinor, Version, version, GetEffectiveTypeRootsHost, getDirectoryPath, forEachAncestorDirectory, ResolvedProjectReference, ResolvedTypeReferenceDirectiveWithFailedLookupLocations, ResolvedTypeReferenceDirective, packageIdToString, firstDefined, directoryProbablyExists, isExternalModuleNameRelative, normalizePathAndParts, readJson, getBaseFileName, CharacterCodes, createMap, optionsHaveModuleResolutionChanges, GetCanonicalFileName, toPath, Path, getRootLength, getEmitModuleKind, ModuleKind, ModuleResolutionKind, perfLogger, pathIsRelative, endsWith, startsWith, forEach, contains, hasTrailingDirectorySeparator, stringContains, tryRemoveExtension, hasJSFileExtension, removeFileExtension, containsPath, getRelativePathFromDirectory, tryGetExtensionFromPath, normalizeSlashes, matchPatternOrExact, getOwnKeys, isString, matchedText, patternText, removePrefix } from "./ts"; +import * as ts from "./ts"; +/* @internal */ +export function trace(host: ModuleResolutionHost, message: DiagnosticMessage, ...args: any[]): void; +export function trace(host: ModuleResolutionHost): void { + host.trace!(formatMessage.apply(undefined, arguments)); +} +/* @internal */ +export function isTraceEnabled(compilerOptions: CompilerOptions, host: ModuleResolutionHost): boolean { + return !!compilerOptions.traceResolution && host.trace !== undefined; +} +function withPackageId(packageInfo: PackageJsonInfo | undefined, r: PathAndExtension | undefined): Resolved | undefined { + let packageId: PackageId | undefined; + if (r && packageInfo) { + const packageJsonContent = packageInfo.packageJsonContent as PackageJson; + if (typeof packageJsonContent.name === "string" && typeof packageJsonContent.version === "string") { + packageId = { + name: packageJsonContent.name, + subModuleName: r.path.slice(packageInfo.packageDirectory.length + directorySeparator.length), + version: packageJsonContent.version + }; + } + } + return r && { path: r.path, extension: r.ext, packageId }; +} +function noPackageId(r: PathAndExtension | undefined): Resolved | undefined { + return withPackageId(/*packageInfo*/ undefined, r); +} +function removeIgnoredPackageId(r: Resolved | undefined): PathAndExtension | undefined { + if (r) { + Debug.assert(r.packageId === undefined); + return { path: r.path, ext: r.extension }; } - - function noPackageId(r: PathAndExtension | undefined): Resolved | undefined { - return withPackageId(/*packageInfo*/ undefined, r); +} +/** Result of trying to resolve a module. */ +interface Resolved { + path: string; + extension: Extension; + packageId: PackageId | undefined; + /** + * When the resolved is not created from cache, the value is + * - string if original Path if it is symbolic link to the resolved path + * - undefined if path is not a symbolic link + * When the resolved is created using value from cache of ResolvedModuleWithFailedLookupLocations, the value is: + * - string if original Path if it is symbolic link to the resolved path + * - true if path is not a symbolic link - this indicates that the originalPath calculation is already done and needs to be skipped + */ + originalPath?: string | true; +} +/** Result of trying to resolve a module at a file. Needs to have 'packageId' added later. */ +interface PathAndExtension { + path: string; + // (Use a different name than `extension` to make sure Resolved isn't assignable to PathAndExtension.) + ext: Extension; +} +/** + * Kinds of file that we are currently looking for. + * Typically there is one pass with Extensions.TypeScript, then a second pass with Extensions.JavaScript. + */ +enum Extensions { + TypeScript, + JavaScript, + Json, + TSConfig, + DtsOnly /** Only '.d.ts' */ +} +interface PathAndPackageId { + readonly fileName: string; + readonly packageId: PackageId | undefined; +} +/** Used with `Extensions.DtsOnly` to extract the path from TypeScript results. */ +function resolvedTypeScriptOnly(resolved: Resolved | undefined): PathAndPackageId | undefined { + if (!resolved) { + return undefined; } - - function removeIgnoredPackageId(r: Resolved | undefined): PathAndExtension | undefined { - if (r) { - Debug.assert(r.packageId === undefined); - return { path: r.path, ext: r.extension }; + Debug.assert(extensionIsTS(resolved.extension)); + return { fileName: resolved.path, packageId: resolved.packageId }; +} +function createResolvedModuleWithFailedLookupLocations(resolved: Resolved | undefined, isExternalLibraryImport: boolean, failedLookupLocations: string[]): ResolvedModuleWithFailedLookupLocations { + return { + resolvedModule: resolved && { resolvedFileName: resolved.path, originalPath: resolved.originalPath === true ? undefined : resolved.originalPath, extension: resolved.extension, isExternalLibraryImport, packageId: resolved.packageId }, + failedLookupLocations + }; +} +interface ModuleResolutionState { + host: ModuleResolutionHost; + compilerOptions: CompilerOptions; + traceEnabled: boolean; + failedLookupLocations: Push; +} +/** Just the fields that we use for module resolution. */ +interface PackageJsonPathFields { + typings?: string; + types?: string; + typesVersions?: MapLike>; + main?: string; + tsconfig?: string; +} +interface PackageJson extends PackageJsonPathFields { + name?: string; + version?: string; +} +function readPackageJsonField>(jsonContent: PackageJson, fieldName: K, typeOfTag: "string", state: ModuleResolutionState): PackageJson[K] | undefined; +function readPackageJsonField>(jsonContent: PackageJson, fieldName: K, typeOfTag: "object", state: ModuleResolutionState): PackageJson[K] | undefined; +function readPackageJsonField(jsonContent: PackageJson, fieldName: K, typeOfTag: "string" | "object", state: ModuleResolutionState): PackageJson[K] | undefined { + if (!hasProperty(jsonContent, fieldName)) { + if (state.traceEnabled) { + trace(state.host, Diagnostics.package_json_does_not_have_a_0_field, fieldName); } + return; } - - /** Result of trying to resolve a module. */ - interface Resolved { - path: string; - extension: Extension; - packageId: PackageId | undefined; - /** - * When the resolved is not created from cache, the value is - * - string if original Path if it is symbolic link to the resolved path - * - undefined if path is not a symbolic link - * When the resolved is created using value from cache of ResolvedModuleWithFailedLookupLocations, the value is: - * - string if original Path if it is symbolic link to the resolved path - * - true if path is not a symbolic link - this indicates that the originalPath calculation is already done and needs to be skipped - */ - originalPath?: string | true; + const value = jsonContent[fieldName]; + if (typeof value !== typeOfTag || value === null) { // eslint-disable-line no-null/no-null + if (state.traceEnabled) { + // eslint-disable-next-line no-null/no-null + trace(state.host, Diagnostics.Expected_type_of_0_field_in_package_json_to_be_1_got_2, fieldName, typeOfTag, value === null ? "null" : typeof value); + } + return; } - - /** Result of trying to resolve a module at a file. Needs to have 'packageId' added later. */ - interface PathAndExtension { - path: string; - // (Use a different name than `extension` to make sure Resolved isn't assignable to PathAndExtension.) - ext: Extension; + return value; +} +function readPackageJsonPathField(jsonContent: PackageJson, fieldName: K, baseDirectory: string, state: ModuleResolutionState): PackageJson[K] | undefined { + const fileName = readPackageJsonField(jsonContent, fieldName, "string", state); + if (fileName === undefined) { + return; } - - /** - * Kinds of file that we are currently looking for. - * Typically there is one pass with Extensions.TypeScript, then a second pass with Extensions.JavaScript. - */ - enum Extensions { - TypeScript, /** '.ts', '.tsx', or '.d.ts' */ - JavaScript, /** '.js' or '.jsx' */ - Json, /** '.json' */ - TSConfig, /** '.json' with `tsconfig` used instead of `index` */ - DtsOnly /** Only '.d.ts' */ - } - - interface PathAndPackageId { - readonly fileName: string; - readonly packageId: PackageId | undefined; - } - /** Used with `Extensions.DtsOnly` to extract the path from TypeScript results. */ - function resolvedTypeScriptOnly(resolved: Resolved | undefined): PathAndPackageId | undefined { - if (!resolved) { - return undefined; - } - Debug.assert(extensionIsTS(resolved.extension)); - return { fileName: resolved.path, packageId: resolved.packageId }; - } - - function createResolvedModuleWithFailedLookupLocations(resolved: Resolved | undefined, isExternalLibraryImport: boolean, failedLookupLocations: string[]): ResolvedModuleWithFailedLookupLocations { - return { - resolvedModule: resolved && { resolvedFileName: resolved.path, originalPath: resolved.originalPath === true ? undefined : resolved.originalPath, extension: resolved.extension, isExternalLibraryImport, packageId: resolved.packageId }, - failedLookupLocations - }; - } - - interface ModuleResolutionState { - host: ModuleResolutionHost; - compilerOptions: CompilerOptions; - traceEnabled: boolean; - failedLookupLocations: Push; - } - - /** Just the fields that we use for module resolution. */ - interface PackageJsonPathFields { - typings?: string; - types?: string; - typesVersions?: MapLike>; - main?: string; - tsconfig?: string; - } - - interface PackageJson extends PackageJsonPathFields { - name?: string; - version?: string; - } - - function readPackageJsonField>(jsonContent: PackageJson, fieldName: K, typeOfTag: "string", state: ModuleResolutionState): PackageJson[K] | undefined; - function readPackageJsonField>(jsonContent: PackageJson, fieldName: K, typeOfTag: "object", state: ModuleResolutionState): PackageJson[K] | undefined; - function readPackageJsonField(jsonContent: PackageJson, fieldName: K, typeOfTag: "string" | "object", state: ModuleResolutionState): PackageJson[K] | undefined { - if (!hasProperty(jsonContent, fieldName)) { - if (state.traceEnabled) { - trace(state.host, Diagnostics.package_json_does_not_have_a_0_field, fieldName); - } - return; + if (!fileName) { + if (state.traceEnabled) { + trace(state.host, Diagnostics.package_json_had_a_falsy_0_field, fieldName); } - const value = jsonContent[fieldName]; - if (typeof value !== typeOfTag || value === null) { // eslint-disable-line no-null/no-null - if (state.traceEnabled) { - // eslint-disable-next-line no-null/no-null - trace(state.host, Diagnostics.Expected_type_of_0_field_in_package_json_to_be_1_got_2, fieldName, typeOfTag, value === null ? "null" : typeof value); + return; + } + const path = normalizePath(combinePaths(baseDirectory, fileName)); + if (state.traceEnabled) { + trace(state.host, Diagnostics.package_json_has_0_field_1_that_references_2, fieldName, fileName, path); + } + return path; +} +function readPackageJsonTypesFields(jsonContent: PackageJson, baseDirectory: string, state: ModuleResolutionState) { + return readPackageJsonPathField(jsonContent, "typings", baseDirectory, state) + || readPackageJsonPathField(jsonContent, "types", baseDirectory, state); +} +function readPackageJsonTSConfigField(jsonContent: PackageJson, baseDirectory: string, state: ModuleResolutionState) { + return readPackageJsonPathField(jsonContent, "tsconfig", baseDirectory, state); +} +function readPackageJsonMainField(jsonContent: PackageJson, baseDirectory: string, state: ModuleResolutionState) { + return readPackageJsonPathField(jsonContent, "main", baseDirectory, state); +} +function readPackageJsonTypesVersionsField(jsonContent: PackageJson, state: ModuleResolutionState) { + const typesVersions = readPackageJsonField(jsonContent, "typesVersions", "object", state); + if (typesVersions === undefined) + return; + if (state.traceEnabled) { + trace(state.host, Diagnostics.package_json_has_a_typesVersions_field_with_version_specific_path_mappings); + } + return typesVersions; +} +interface VersionPaths { + version: string; + paths: MapLike; +} +function readPackageJsonTypesVersionPaths(jsonContent: PackageJson, state: ModuleResolutionState): VersionPaths | undefined { + const typesVersions = readPackageJsonTypesVersionsField(jsonContent, state); + if (typesVersions === undefined) + return; + if (state.traceEnabled) { + for (const key in typesVersions) { + if (hasProperty(typesVersions, key) && !VersionRange.tryParse(key)) { + trace(state.host, Diagnostics.package_json_has_a_typesVersions_entry_0_that_is_not_a_valid_semver_range, key); } - return; } - return value; } - - function readPackageJsonPathField(jsonContent: PackageJson, fieldName: K, baseDirectory: string, state: ModuleResolutionState): PackageJson[K] | undefined { - const fileName = readPackageJsonField(jsonContent, fieldName, "string", state); - if (fileName === undefined) { - return; - } - if (!fileName) { - if (state.traceEnabled) { - trace(state.host, Diagnostics.package_json_had_a_falsy_0_field, fieldName); - } - return; + const result = getPackageJsonTypesVersionsPaths(typesVersions); + if (!result) { + if (state.traceEnabled) { + trace(state.host, Diagnostics.package_json_does_not_have_a_typesVersions_entry_that_matches_version_0, versionMajorMinor); } - const path = normalizePath(combinePaths(baseDirectory, fileName)); + return; + } + const { version: bestVersionKey, paths: bestVersionPaths } = result; + if (typeof bestVersionPaths !== "object") { if (state.traceEnabled) { - trace(state.host, Diagnostics.package_json_has_0_field_1_that_references_2, fieldName, fileName, path); + trace(state.host, Diagnostics.Expected_type_of_0_field_in_package_json_to_be_1_got_2, `typesVersions['${bestVersionKey}']`, "object", typeof bestVersionPaths); } - return path; + return; } - - function readPackageJsonTypesFields(jsonContent: PackageJson, baseDirectory: string, state: ModuleResolutionState) { - return readPackageJsonPathField(jsonContent, "typings", baseDirectory, state) - || readPackageJsonPathField(jsonContent, "types", baseDirectory, state); + return result; +} +let typeScriptVersion: Version | undefined; +/* @internal */ +export function getPackageJsonTypesVersionsPaths(typesVersions: MapLike>) { + if (!typeScriptVersion) + typeScriptVersion = new Version(version); + for (const key in typesVersions) { + if (!hasProperty(typesVersions, key)) + continue; + const keyRange = VersionRange.tryParse(key); + if (keyRange === undefined) { + continue; + } + // return the first entry whose range matches the current compiler version. + if (keyRange.test(typeScriptVersion)) { + return { version: key, paths: typesVersions[key] }; + } } - - function readPackageJsonTSConfigField(jsonContent: PackageJson, baseDirectory: string, state: ModuleResolutionState) { - return readPackageJsonPathField(jsonContent, "tsconfig", baseDirectory, state); +} +export function getEffectiveTypeRoots(options: CompilerOptions, host: GetEffectiveTypeRootsHost): string[] | undefined { + if (options.typeRoots) { + return options.typeRoots; } - - function readPackageJsonMainField(jsonContent: PackageJson, baseDirectory: string, state: ModuleResolutionState) { - return readPackageJsonPathField(jsonContent, "main", baseDirectory, state); + let currentDirectory: string | undefined; + if (options.configFilePath) { + currentDirectory = getDirectoryPath(options.configFilePath); } - - function readPackageJsonTypesVersionsField(jsonContent: PackageJson, state: ModuleResolutionState) { - const typesVersions = readPackageJsonField(jsonContent, "typesVersions", "object", state); - if (typesVersions === undefined) return; - - if (state.traceEnabled) { - trace(state.host, Diagnostics.package_json_has_a_typesVersions_field_with_version_specific_path_mappings); - } - - return typesVersions; + else if (host.getCurrentDirectory) { + currentDirectory = host.getCurrentDirectory(); } - - interface VersionPaths { - version: string; - paths: MapLike; + if (currentDirectory !== undefined) { + return getDefaultTypeRoots(currentDirectory, host); } - - function readPackageJsonTypesVersionPaths(jsonContent: PackageJson, state: ModuleResolutionState): VersionPaths | undefined { - const typesVersions = readPackageJsonTypesVersionsField(jsonContent, state); - if (typesVersions === undefined) return; - - if (state.traceEnabled) { - for (const key in typesVersions) { - if (hasProperty(typesVersions, key) && !VersionRange.tryParse(key)) { - trace(state.host, Diagnostics.package_json_has_a_typesVersions_entry_0_that_is_not_a_valid_semver_range, key); - } - } +} +/** + * Returns the path to every node_modules/@types directory from some ancestor directory. + * Returns undefined if there are none. + */ +function getDefaultTypeRoots(currentDirectory: string, host: { + directoryExists?: (directoryName: string) => boolean; +}): string[] | undefined { + if (!host.directoryExists) { + return [combinePaths(currentDirectory, nodeModulesAtTypes)]; + // And if it doesn't exist, tough. + } + let typeRoots: string[] | undefined; + forEachAncestorDirectory(normalizePath(currentDirectory), directory => { + const atTypes = combinePaths(directory, nodeModulesAtTypes); + if (host.directoryExists!(atTypes)) { + (typeRoots || (typeRoots = [])).push(atTypes); } - - const result = getPackageJsonTypesVersionsPaths(typesVersions); - if (!result) { - if (state.traceEnabled) { - trace(state.host, Diagnostics.package_json_does_not_have_a_typesVersions_entry_that_matches_version_0, versionMajorMinor); + return undefined; + }); + return typeRoots; +} +const nodeModulesAtTypes = combinePaths("node_modules", "@types"); +/** + * @param {string | undefined} containingFile - file that contains type reference directive, can be undefined if containing file is unknown. + * This is possible in case if resolution is performed for directives specified via 'types' parameter. In this case initial path for secondary lookups + * is assumed to be the same as root directory of the project. + */ +export function resolveTypeReferenceDirective(typeReferenceDirectiveName: string, containingFile: string | undefined, options: CompilerOptions, host: ModuleResolutionHost, redirectedReference?: ResolvedProjectReference): ResolvedTypeReferenceDirectiveWithFailedLookupLocations { + const traceEnabled = isTraceEnabled(options, host); + if (redirectedReference) { + options = redirectedReference.commandLine.options; + } + const failedLookupLocations: string[] = []; + const moduleResolutionState: ModuleResolutionState = { compilerOptions: options, host, traceEnabled, failedLookupLocations }; + const typeRoots = getEffectiveTypeRoots(options, host); + if (traceEnabled) { + if (containingFile === undefined) { + if (typeRoots === undefined) { + trace(host, Diagnostics.Resolving_type_reference_directive_0_containing_file_not_set_root_directory_not_set, typeReferenceDirectiveName); } - return; - } - - const { version: bestVersionKey, paths: bestVersionPaths } = result; - if (typeof bestVersionPaths !== "object") { - if (state.traceEnabled) { - trace(state.host, Diagnostics.Expected_type_of_0_field_in_package_json_to_be_1_got_2, `typesVersions['${bestVersionKey}']`, "object", typeof bestVersionPaths); + else { + trace(host, Diagnostics.Resolving_type_reference_directive_0_containing_file_not_set_root_directory_1, typeReferenceDirectiveName, typeRoots); } - return; } - - return result; - } - - let typeScriptVersion: Version | undefined; - - /* @internal */ - export function getPackageJsonTypesVersionsPaths(typesVersions: MapLike>) { - if (!typeScriptVersion) typeScriptVersion = new Version(version); - - for (const key in typesVersions) { - if (!hasProperty(typesVersions, key)) continue; - - const keyRange = VersionRange.tryParse(key); - if (keyRange === undefined) { - continue; + else { + if (typeRoots === undefined) { + trace(host, Diagnostics.Resolving_type_reference_directive_0_containing_file_1_root_directory_not_set, typeReferenceDirectiveName, containingFile); } - - // return the first entry whose range matches the current compiler version. - if (keyRange.test(typeScriptVersion)) { - return { version: key, paths: typesVersions[key] }; + else { + trace(host, Diagnostics.Resolving_type_reference_directive_0_containing_file_1_root_directory_2, typeReferenceDirectiveName, containingFile, typeRoots); } } - } - - export function getEffectiveTypeRoots(options: CompilerOptions, host: GetEffectiveTypeRootsHost): string[] | undefined { - if (options.typeRoots) { - return options.typeRoots; - } - - let currentDirectory: string | undefined; - if (options.configFilePath) { - currentDirectory = getDirectoryPath(options.configFilePath); - } - else if (host.getCurrentDirectory) { - currentDirectory = host.getCurrentDirectory(); - } - - if (currentDirectory !== undefined) { - return getDefaultTypeRoots(currentDirectory, host); + if (redirectedReference) { + trace(host, Diagnostics.Using_compiler_options_of_project_reference_redirect_0, redirectedReference.sourceFile.fileName); } } - - /** - * Returns the path to every node_modules/@types directory from some ancestor directory. - * Returns undefined if there are none. - */ - function getDefaultTypeRoots(currentDirectory: string, host: { directoryExists?: (directoryName: string) => boolean }): string[] | undefined { - if (!host.directoryExists) { - return [combinePaths(currentDirectory, nodeModulesAtTypes)]; - // And if it doesn't exist, tough. - } - - let typeRoots: string[] | undefined; - forEachAncestorDirectory(normalizePath(currentDirectory), directory => { - const atTypes = combinePaths(directory, nodeModulesAtTypes); - if (host.directoryExists!(atTypes)) { - (typeRoots || (typeRoots = [])).push(atTypes); - } - return undefined; - }); - return typeRoots; + let resolved = primaryLookup(); + let primary = true; + if (!resolved) { + resolved = secondaryLookup(); + primary = false; } - const nodeModulesAtTypes = combinePaths("node_modules", "@types"); - - /** - * @param {string | undefined} containingFile - file that contains type reference directive, can be undefined if containing file is unknown. - * This is possible in case if resolution is performed for directives specified via 'types' parameter. In this case initial path for secondary lookups - * is assumed to be the same as root directory of the project. - */ - export function resolveTypeReferenceDirective(typeReferenceDirectiveName: string, containingFile: string | undefined, options: CompilerOptions, host: ModuleResolutionHost, redirectedReference?: ResolvedProjectReference): ResolvedTypeReferenceDirectiveWithFailedLookupLocations { - const traceEnabled = isTraceEnabled(options, host); - if (redirectedReference) { - options = redirectedReference.commandLine.options; - } - const failedLookupLocations: string[] = []; - const moduleResolutionState: ModuleResolutionState = { compilerOptions: options, host, traceEnabled, failedLookupLocations }; - - const typeRoots = getEffectiveTypeRoots(options, host); + let resolvedTypeReferenceDirective: ResolvedTypeReferenceDirective | undefined; + if (resolved) { + const { fileName, packageId } = resolved; + const resolvedFileName = options.preserveSymlinks ? fileName : realPath(fileName, host, traceEnabled); if (traceEnabled) { - if (containingFile === undefined) { - if (typeRoots === undefined) { - trace(host, Diagnostics.Resolving_type_reference_directive_0_containing_file_not_set_root_directory_not_set, typeReferenceDirectiveName); - } - else { - trace(host, Diagnostics.Resolving_type_reference_directive_0_containing_file_not_set_root_directory_1, typeReferenceDirectiveName, typeRoots); - } + if (packageId) { + trace(host, Diagnostics.Type_reference_directive_0_was_successfully_resolved_to_1_with_Package_ID_2_primary_Colon_3, typeReferenceDirectiveName, resolvedFileName, packageIdToString(packageId), primary); } else { - if (typeRoots === undefined) { - trace(host, Diagnostics.Resolving_type_reference_directive_0_containing_file_1_root_directory_not_set, typeReferenceDirectiveName, containingFile); - } - else { - trace(host, Diagnostics.Resolving_type_reference_directive_0_containing_file_1_root_directory_2, typeReferenceDirectiveName, containingFile, typeRoots); - } + trace(host, Diagnostics.Type_reference_directive_0_was_successfully_resolved_to_1_primary_Colon_2, typeReferenceDirectiveName, resolvedFileName, primary); } - if (redirectedReference) { - trace(host, Diagnostics.Using_compiler_options_of_project_reference_redirect_0, redirectedReference.sourceFile.fileName); - } - } - - let resolved = primaryLookup(); - let primary = true; - if (!resolved) { - resolved = secondaryLookup(); - primary = false; } - - let resolvedTypeReferenceDirective: ResolvedTypeReferenceDirective | undefined; - if (resolved) { - const { fileName, packageId } = resolved; - const resolvedFileName = options.preserveSymlinks ? fileName : realPath(fileName, host, traceEnabled); + resolvedTypeReferenceDirective = { primary, resolvedFileName, packageId, isExternalLibraryImport: pathContainsNodeModules(fileName) }; + } + return { resolvedTypeReferenceDirective, failedLookupLocations }; + function primaryLookup(): PathAndPackageId | undefined { + // Check primary library paths + if (typeRoots && typeRoots.length) { if (traceEnabled) { - if (packageId) { - trace(host, Diagnostics.Type_reference_directive_0_was_successfully_resolved_to_1_with_Package_ID_2_primary_Colon_3, typeReferenceDirectiveName, resolvedFileName, packageIdToString(packageId), primary); - } - else { - trace(host, Diagnostics.Type_reference_directive_0_was_successfully_resolved_to_1_primary_Colon_2, typeReferenceDirectiveName, resolvedFileName, primary); + trace(host, Diagnostics.Resolving_with_primary_search_path_0, typeRoots.join(", ")); + } + return firstDefined(typeRoots, typeRoot => { + const candidate = combinePaths(typeRoot, typeReferenceDirectiveName); + const candidateDirectory = getDirectoryPath(candidate); + const directoryExists = directoryProbablyExists(candidateDirectory, host); + if (!directoryExists && traceEnabled) { + trace(host, Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, candidateDirectory); } + return resolvedTypeScriptOnly(loadNodeModuleFromDirectory(Extensions.DtsOnly, candidate, !directoryExists, moduleResolutionState)); + }); + } + else { + if (traceEnabled) { + trace(host, Diagnostics.Root_directory_cannot_be_determined_skipping_primary_search_paths); } - resolvedTypeReferenceDirective = { primary, resolvedFileName, packageId, isExternalLibraryImport: pathContainsNodeModules(fileName) }; - } - - return { resolvedTypeReferenceDirective, failedLookupLocations }; - - function primaryLookup(): PathAndPackageId | undefined { - // Check primary library paths - if (typeRoots && typeRoots.length) { - if (traceEnabled) { - trace(host, Diagnostics.Resolving_with_primary_search_path_0, typeRoots.join(", ")); - } - return firstDefined(typeRoots, typeRoot => { - const candidate = combinePaths(typeRoot, typeReferenceDirectiveName); - const candidateDirectory = getDirectoryPath(candidate); - const directoryExists = directoryProbablyExists(candidateDirectory, host); - if (!directoryExists && traceEnabled) { - trace(host, Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, candidateDirectory); - } - return resolvedTypeScriptOnly( - loadNodeModuleFromDirectory(Extensions.DtsOnly, candidate, - !directoryExists, moduleResolutionState)); - }); + } + } + function secondaryLookup(): PathAndPackageId | undefined { + const initialLocationForSecondaryLookup = containingFile && getDirectoryPath(containingFile); + if (initialLocationForSecondaryLookup !== undefined) { + // check secondary locations + if (traceEnabled) { + trace(host, Diagnostics.Looking_up_in_node_modules_folder_initial_location_0, initialLocationForSecondaryLookup); + } + let result: Resolved | undefined; + if (!isExternalModuleNameRelative(typeReferenceDirectiveName)) { + const searchResult = loadModuleFromNearestNodeModulesDirectory(Extensions.DtsOnly, typeReferenceDirectiveName, initialLocationForSecondaryLookup, moduleResolutionState, /*cache*/ undefined, /*redirectedReference*/ undefined); + result = searchResult && searchResult.value; } else { - if (traceEnabled) { - trace(host, Diagnostics.Root_directory_cannot_be_determined_skipping_primary_search_paths); - } + const { path: candidate } = normalizePathAndParts(combinePaths(initialLocationForSecondaryLookup, typeReferenceDirectiveName)); + result = nodeLoadModuleByRelativeName(Extensions.DtsOnly, candidate, /*onlyRecordFailures*/ false, moduleResolutionState, /*considerPackageJson*/ true); } - } - - function secondaryLookup(): PathAndPackageId | undefined { - const initialLocationForSecondaryLookup = containingFile && getDirectoryPath(containingFile); - - if (initialLocationForSecondaryLookup !== undefined) { - // check secondary locations - if (traceEnabled) { - trace(host, Diagnostics.Looking_up_in_node_modules_folder_initial_location_0, initialLocationForSecondaryLookup); - } - let result: Resolved | undefined; - if (!isExternalModuleNameRelative(typeReferenceDirectiveName)) { - const searchResult = loadModuleFromNearestNodeModulesDirectory(Extensions.DtsOnly, typeReferenceDirectiveName, initialLocationForSecondaryLookup, moduleResolutionState, /*cache*/ undefined, /*redirectedReference*/ undefined); - result = searchResult && searchResult.value; - } - else { - const { path: candidate } = normalizePathAndParts(combinePaths(initialLocationForSecondaryLookup, typeReferenceDirectiveName)); - result = nodeLoadModuleByRelativeName(Extensions.DtsOnly, candidate, /*onlyRecordFailures*/ false, moduleResolutionState, /*considerPackageJson*/ true); - } - const resolvedFile = resolvedTypeScriptOnly(result); - if (!resolvedFile && traceEnabled) { - trace(host, Diagnostics.Type_reference_directive_0_was_not_resolved, typeReferenceDirectiveName); - } - return resolvedFile; + const resolvedFile = resolvedTypeScriptOnly(result); + if (!resolvedFile && traceEnabled) { + trace(host, Diagnostics.Type_reference_directive_0_was_not_resolved, typeReferenceDirectiveName); } - else { - if (traceEnabled) { - trace(host, Diagnostics.Containing_file_is_not_specified_and_root_directory_cannot_be_determined_skipping_lookup_in_node_modules_folder); - } + return resolvedFile; + } + else { + if (traceEnabled) { + trace(host, Diagnostics.Containing_file_is_not_specified_and_root_directory_cannot_be_determined_skipping_lookup_in_node_modules_folder); } } } - - /** - * Given a set of options, returns the set of type directive names - * that should be included for this program automatically. - * This list could either come from the config file, - * or from enumerating the types root + initial secondary types lookup location. - * More type directives might appear in the program later as a result of loading actual source files; - * this list is only the set of defaults that are implicitly included. - */ - export function getAutomaticTypeDirectiveNames(options: CompilerOptions, host: ModuleResolutionHost): string[] { - // Use explicit type list from tsconfig.json - if (options.types) { - return options.types; - } - - // Walk the primary type lookup locations - const result: string[] = []; - if (host.directoryExists && host.getDirectories) { - const typeRoots = getEffectiveTypeRoots(options, host); - if (typeRoots) { - for (const root of typeRoots) { - if (host.directoryExists(root)) { - for (const typeDirectivePath of host.getDirectories(root)) { - const normalized = normalizePath(typeDirectivePath); - const packageJsonPath = combinePaths(root, normalized, "package.json"); - // `types-publisher` sometimes creates packages with `"typings": null` for packages that don't provide their own types. - // See `createNotNeededPackageJSON` in the types-publisher` repo. - // eslint-disable-next-line no-null/no-null - const isNotNeededPackage = host.fileExists(packageJsonPath) && (readJson(packageJsonPath, host) as PackageJson).typings === null; - if (!isNotNeededPackage) { - const baseFileName = getBaseFileName(normalized); - - // At this stage, skip results with leading dot. - if (baseFileName.charCodeAt(0) !== CharacterCodes.dot) { - // Return just the type directive names - result.push(baseFileName); - } +} +/** + * Given a set of options, returns the set of type directive names + * that should be included for this program automatically. + * This list could either come from the config file, + * or from enumerating the types root + initial secondary types lookup location. + * More type directives might appear in the program later as a result of loading actual source files; + * this list is only the set of defaults that are implicitly included. + */ +export function getAutomaticTypeDirectiveNames(options: CompilerOptions, host: ModuleResolutionHost): string[] { + // Use explicit type list from tsconfig.json + if (options.types) { + return options.types; + } + // Walk the primary type lookup locations + const result: string[] = []; + if (host.directoryExists && host.getDirectories) { + const typeRoots = getEffectiveTypeRoots(options, host); + if (typeRoots) { + for (const root of typeRoots) { + if (host.directoryExists(root)) { + for (const typeDirectivePath of host.getDirectories(root)) { + const normalized = normalizePath(typeDirectivePath); + const packageJsonPath = combinePaths(root, normalized, "package.json"); + // `types-publisher` sometimes creates packages with `"typings": null` for packages that don't provide their own types. + // See `createNotNeededPackageJSON` in the types-publisher` repo. + // eslint-disable-next-line no-null/no-null + const isNotNeededPackage = host.fileExists(packageJsonPath) && (readJson(packageJsonPath, host) as PackageJson).typings === null; + if (!isNotNeededPackage) { + const baseFileName = getBaseFileName(normalized); + // At this stage, skip results with leading dot. + if (baseFileName.charCodeAt(0) !== CharacterCodes.dot) { + // Return just the type directive names + result.push(baseFileName); } } } } } } - return result; } - - /** - * Cached module resolutions per containing directory. - * This assumes that any module id will have the same resolution for sibling files located in the same folder. - */ - export interface ModuleResolutionCache extends NonRelativeModuleNameResolutionCache { - getOrCreateCacheForDirectory(directoryName: string, redirectedReference?: ResolvedProjectReference): Map; - /*@internal*/ directoryToModuleNameMap: CacheWithRedirects>; + return result; +} +/** + * Cached module resolutions per containing directory. + * This assumes that any module id will have the same resolution for sibling files located in the same folder. + */ +export interface ModuleResolutionCache extends NonRelativeModuleNameResolutionCache { + getOrCreateCacheForDirectory(directoryName: string, redirectedReference?: ResolvedProjectReference): ts.Map; + /*@internal*/ directoryToModuleNameMap: CacheWithRedirects>; +} +/** + * Stored map from non-relative module name to a table: directory -> result of module lookup in this directory + * We support only non-relative module names because resolution of relative module names is usually more deterministic and thus less expensive. + */ +export interface NonRelativeModuleNameResolutionCache { + getOrCreateCacheForModuleName(nonRelativeModuleName: string, redirectedReference?: ResolvedProjectReference): PerModuleNameCache; + /*@internal*/ moduleNameToDirectoryMap: CacheWithRedirects; +} +export interface PerModuleNameCache { + get(directory: string): ResolvedModuleWithFailedLookupLocations | undefined; + set(directory: string, result: ResolvedModuleWithFailedLookupLocations): void; +} +export function createModuleResolutionCache(currentDirectory: string, getCanonicalFileName: (s: string) => string, options?: CompilerOptions): ModuleResolutionCache { + return createModuleResolutionCacheWithMaps(createCacheWithRedirects(options), createCacheWithRedirects(options), currentDirectory, getCanonicalFileName); +} +/*@internal*/ +export interface CacheWithRedirects { + ownMap: ts.Map; + redirectsMap: ts.Map>; + getOrCreateMapOfCacheRedirects(redirectedReference: ResolvedProjectReference | undefined): ts.Map; + clear(): void; + setOwnOptions(newOptions: CompilerOptions): void; + setOwnMap(newOwnMap: ts.Map): void; +} +/*@internal*/ +export function createCacheWithRedirects(options?: CompilerOptions): CacheWithRedirects { + let ownMap: ts.Map = createMap(); + const redirectsMap: ts.Map> = createMap(); + return { + ownMap, + redirectsMap, + getOrCreateMapOfCacheRedirects, + clear, + setOwnOptions, + setOwnMap + }; + function setOwnOptions(newOptions: CompilerOptions) { + options = newOptions; + } + function setOwnMap(newOwnMap: ts.Map) { + ownMap = newOwnMap; + } + function getOrCreateMapOfCacheRedirects(redirectedReference: ResolvedProjectReference | undefined) { + if (!redirectedReference) { + return ownMap; + } + const path = redirectedReference.sourceFile.path; + let redirects = redirectsMap.get(path); + if (!redirects) { + // Reuse map if redirected reference map uses same resolution + redirects = !options || optionsHaveModuleResolutionChanges(options, redirectedReference.commandLine.options) ? createMap() : ownMap; + redirectsMap.set(path, redirects); + } + return redirects; + } + function clear() { + ownMap.clear(); + redirectsMap.clear(); } - - /** - * Stored map from non-relative module name to a table: directory -> result of module lookup in this directory - * We support only non-relative module names because resolution of relative module names is usually more deterministic and thus less expensive. - */ - export interface NonRelativeModuleNameResolutionCache { - getOrCreateCacheForModuleName(nonRelativeModuleName: string, redirectedReference?: ResolvedProjectReference): PerModuleNameCache; - /*@internal*/ moduleNameToDirectoryMap: CacheWithRedirects; - } - - export interface PerModuleNameCache { - get(directory: string): ResolvedModuleWithFailedLookupLocations | undefined; - set(directory: string, result: ResolvedModuleWithFailedLookupLocations): void; - } - - export function createModuleResolutionCache(currentDirectory: string, getCanonicalFileName: (s: string) => string, options?: CompilerOptions): ModuleResolutionCache { - return createModuleResolutionCacheWithMaps( - createCacheWithRedirects(options), - createCacheWithRedirects(options), - currentDirectory, - getCanonicalFileName - ); - } - - - /*@internal*/ - export interface CacheWithRedirects { - ownMap: Map; - redirectsMap: Map>; - getOrCreateMapOfCacheRedirects(redirectedReference: ResolvedProjectReference | undefined): Map; - clear(): void; - setOwnOptions(newOptions: CompilerOptions): void; - setOwnMap(newOwnMap: Map): void; - } - - /*@internal*/ - export function createCacheWithRedirects(options?: CompilerOptions): CacheWithRedirects { - let ownMap: Map = createMap(); - const redirectsMap: Map> = createMap(); - return { - ownMap, - redirectsMap, - getOrCreateMapOfCacheRedirects, - clear, - setOwnOptions, - setOwnMap - }; - - function setOwnOptions(newOptions: CompilerOptions) { - options = newOptions; - } - - function setOwnMap(newOwnMap: Map) { - ownMap = newOwnMap; - } - - function getOrCreateMapOfCacheRedirects(redirectedReference: ResolvedProjectReference | undefined) { - if (!redirectedReference) { - return ownMap; - } - const path = redirectedReference.sourceFile.path; - let redirects = redirectsMap.get(path); - if (!redirects) { - // Reuse map if redirected reference map uses same resolution - redirects = !options || optionsHaveModuleResolutionChanges(options, redirectedReference.commandLine.options) ? createMap() : ownMap; - redirectsMap.set(path, redirects); +} +/*@internal*/ +export function createModuleResolutionCacheWithMaps(directoryToModuleNameMap: CacheWithRedirects>, moduleNameToDirectoryMap: CacheWithRedirects, currentDirectory: string, getCanonicalFileName: GetCanonicalFileName): ModuleResolutionCache { + return { getOrCreateCacheForDirectory, getOrCreateCacheForModuleName, directoryToModuleNameMap, moduleNameToDirectoryMap }; + function getOrCreateCacheForDirectory(directoryName: string, redirectedReference?: ResolvedProjectReference) { + const path = toPath(directoryName, currentDirectory, getCanonicalFileName); + return getOrCreateCache>(directoryToModuleNameMap, redirectedReference, path, createMap); + } + function getOrCreateCacheForModuleName(nonRelativeModuleName: string, redirectedReference?: ResolvedProjectReference): PerModuleNameCache { + Debug.assert(!isExternalModuleNameRelative(nonRelativeModuleName)); + return getOrCreateCache(moduleNameToDirectoryMap, redirectedReference, nonRelativeModuleName, createPerModuleNameCache); + } + function getOrCreateCache(cacheWithRedirects: CacheWithRedirects, redirectedReference: ResolvedProjectReference | undefined, key: string, create: () => T): T { + const cache = cacheWithRedirects.getOrCreateMapOfCacheRedirects(redirectedReference); + let result = cache.get(key); + if (!result) { + result = create(); + cache.set(key, result); + } + return result; + } + function createPerModuleNameCache(): PerModuleNameCache { + const directoryPathMap = createMap(); + return { get, set }; + function get(directory: string): ResolvedModuleWithFailedLookupLocations | undefined { + return directoryPathMap.get(toPath(directory, currentDirectory, getCanonicalFileName)); + } + /** + * At first this function add entry directory -> module resolution result to the table. + * Then it computes the set of parent folders for 'directory' that should have the same module resolution result + * and for every parent folder in set it adds entry: parent -> module resolution. . + * Lets say we first directory name: /a/b/c/d/e and resolution result is: /a/b/bar.ts. + * Set of parent folders that should have the same result will be: + * [ + * /a/b/c/d, /a/b/c, /a/b + * ] + * this means that request for module resolution from file in any of these folder will be immediately found in cache. + */ + function set(directory: string, result: ResolvedModuleWithFailedLookupLocations): void { + const path = toPath(directory, currentDirectory, getCanonicalFileName); + // if entry is already in cache do nothing + if (directoryPathMap.has(path)) { + return; + } + directoryPathMap.set(path, result); + const resolvedFileName = result.resolvedModule && + (result.resolvedModule.originalPath || result.resolvedModule.resolvedFileName); + // find common prefix between directory and resolved file name + // this common prefix should be the shortest path that has the same resolution + // directory: /a/b/c/d/e + // resolvedFileName: /a/b/foo.d.ts + // commonPrefix: /a/b + // for failed lookups cache the result for every directory up to root + const commonPrefix = resolvedFileName && getCommonPrefix(path, resolvedFileName); + let current = path; + while (current !== commonPrefix) { + const parent = getDirectoryPath(current); + if (parent === current || directoryPathMap.has(parent)) { + break; + } + directoryPathMap.set(parent, result); + current = parent; } - return redirects; - } - - function clear() { - ownMap.clear(); - redirectsMap.clear(); - } - } - - /*@internal*/ - export function createModuleResolutionCacheWithMaps( - directoryToModuleNameMap: CacheWithRedirects>, - moduleNameToDirectoryMap: CacheWithRedirects, - currentDirectory: string, - getCanonicalFileName: GetCanonicalFileName): ModuleResolutionCache { - - return { getOrCreateCacheForDirectory, getOrCreateCacheForModuleName, directoryToModuleNameMap, moduleNameToDirectoryMap }; - - function getOrCreateCacheForDirectory(directoryName: string, redirectedReference?: ResolvedProjectReference) { - const path = toPath(directoryName, currentDirectory, getCanonicalFileName); - return getOrCreateCache>(directoryToModuleNameMap, redirectedReference, path, createMap); - } - - function getOrCreateCacheForModuleName(nonRelativeModuleName: string, redirectedReference?: ResolvedProjectReference): PerModuleNameCache { - Debug.assert(!isExternalModuleNameRelative(nonRelativeModuleName)); - return getOrCreateCache(moduleNameToDirectoryMap, redirectedReference, nonRelativeModuleName, createPerModuleNameCache); - } - - function getOrCreateCache(cacheWithRedirects: CacheWithRedirects, redirectedReference: ResolvedProjectReference | undefined, key: string, create: () => T): T { - const cache = cacheWithRedirects.getOrCreateMapOfCacheRedirects(redirectedReference); - let result = cache.get(key); - if (!result) { - result = create(); - cache.set(key, result); + } + function getCommonPrefix(directory: Path, resolution: string) { + const resolutionDirectory = toPath(getDirectoryPath(resolution), currentDirectory, getCanonicalFileName); + // find first position where directory and resolution differs + let i = 0; + const limit = Math.min(directory.length, resolutionDirectory.length); + while (i < limit && directory.charCodeAt(i) === resolutionDirectory.charCodeAt(i)) { + i++; } - return result; - } - - function createPerModuleNameCache(): PerModuleNameCache { - const directoryPathMap = createMap(); - - return { get, set }; - - function get(directory: string): ResolvedModuleWithFailedLookupLocations | undefined { - return directoryPathMap.get(toPath(directory, currentDirectory, getCanonicalFileName)); + if (i === directory.length && (resolutionDirectory.length === i || resolutionDirectory[i] === directorySeparator)) { + return directory; } - - /** - * At first this function add entry directory -> module resolution result to the table. - * Then it computes the set of parent folders for 'directory' that should have the same module resolution result - * and for every parent folder in set it adds entry: parent -> module resolution. . - * Lets say we first directory name: /a/b/c/d/e and resolution result is: /a/b/bar.ts. - * Set of parent folders that should have the same result will be: - * [ - * /a/b/c/d, /a/b/c, /a/b - * ] - * this means that request for module resolution from file in any of these folder will be immediately found in cache. - */ - function set(directory: string, result: ResolvedModuleWithFailedLookupLocations): void { - const path = toPath(directory, currentDirectory, getCanonicalFileName); - // if entry is already in cache do nothing - if (directoryPathMap.has(path)) { - return; - } - directoryPathMap.set(path, result); - - const resolvedFileName = result.resolvedModule && - (result.resolvedModule.originalPath || result.resolvedModule.resolvedFileName); - // find common prefix between directory and resolved file name - // this common prefix should be the shortest path that has the same resolution - // directory: /a/b/c/d/e - // resolvedFileName: /a/b/foo.d.ts - // commonPrefix: /a/b - // for failed lookups cache the result for every directory up to root - const commonPrefix = resolvedFileName && getCommonPrefix(path, resolvedFileName); - let current = path; - while (current !== commonPrefix) { - const parent = getDirectoryPath(current); - if (parent === current || directoryPathMap.has(parent)) { - break; - } - directoryPathMap.set(parent, result); - current = parent; - } + const rootLength = getRootLength(directory); + if (i < rootLength) { + return undefined; } - - function getCommonPrefix(directory: Path, resolution: string) { - const resolutionDirectory = toPath(getDirectoryPath(resolution), currentDirectory, getCanonicalFileName); - - // find first position where directory and resolution differs - let i = 0; - const limit = Math.min(directory.length, resolutionDirectory.length); - while (i < limit && directory.charCodeAt(i) === resolutionDirectory.charCodeAt(i)) { - i++; - } - if (i === directory.length && (resolutionDirectory.length === i || resolutionDirectory[i] === directorySeparator)) { - return directory; - } - const rootLength = getRootLength(directory); - if (i < rootLength) { - return undefined; - } - const sep = directory.lastIndexOf(directorySeparator, i - 1); - if (sep === -1) { - return undefined; - } - return directory.substr(0, Math.max(sep, rootLength)); + const sep = directory.lastIndexOf(directorySeparator, i - 1); + if (sep === -1) { + return undefined; } + return directory.substr(0, Math.max(sep, rootLength)); } } - - export function resolveModuleNameFromCache(moduleName: string, containingFile: string, cache: ModuleResolutionCache): ResolvedModuleWithFailedLookupLocations | undefined { - const containingDirectory = getDirectoryPath(containingFile); - const perFolderCache = cache && cache.getOrCreateCacheForDirectory(containingDirectory); - return perFolderCache && perFolderCache.get(moduleName); +} +export function resolveModuleNameFromCache(moduleName: string, containingFile: string, cache: ModuleResolutionCache): ResolvedModuleWithFailedLookupLocations | undefined { + const containingDirectory = getDirectoryPath(containingFile); + const perFolderCache = cache && cache.getOrCreateCacheForDirectory(containingDirectory); + return perFolderCache && perFolderCache.get(moduleName); +} +export function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference): ResolvedModuleWithFailedLookupLocations { + const traceEnabled = isTraceEnabled(compilerOptions, host); + if (redirectedReference) { + compilerOptions = redirectedReference.commandLine.options; } - - export function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference): ResolvedModuleWithFailedLookupLocations { - const traceEnabled = isTraceEnabled(compilerOptions, host); + if (traceEnabled) { + trace(host, Diagnostics.Resolving_module_0_from_1, moduleName, containingFile); if (redirectedReference) { - compilerOptions = redirectedReference.commandLine.options; + trace(host, Diagnostics.Using_compiler_options_of_project_reference_redirect_0, redirectedReference.sourceFile.fileName); } + } + const containingDirectory = getDirectoryPath(containingFile); + const perFolderCache = cache && cache.getOrCreateCacheForDirectory(containingDirectory, redirectedReference); + let result = perFolderCache && perFolderCache.get(moduleName); + if (result) { if (traceEnabled) { - trace(host, Diagnostics.Resolving_module_0_from_1, moduleName, containingFile); - if (redirectedReference) { - trace(host, Diagnostics.Using_compiler_options_of_project_reference_redirect_0, redirectedReference.sourceFile.fileName); - } + trace(host, Diagnostics.Resolution_for_module_0_was_found_in_cache_from_location_1, moduleName, containingDirectory); } - const containingDirectory = getDirectoryPath(containingFile); - const perFolderCache = cache && cache.getOrCreateCacheForDirectory(containingDirectory, redirectedReference); - let result = perFolderCache && perFolderCache.get(moduleName); - - if (result) { + } + else { + let moduleResolution = compilerOptions.moduleResolution; + if (moduleResolution === undefined) { + moduleResolution = getEmitModuleKind(compilerOptions) === ModuleKind.CommonJS ? ModuleResolutionKind.NodeJs : ModuleResolutionKind.Classic; if (traceEnabled) { - trace(host, Diagnostics.Resolution_for_module_0_was_found_in_cache_from_location_1, moduleName, containingDirectory); + trace(host, Diagnostics.Module_resolution_kind_is_not_specified_using_0, ModuleResolutionKind[moduleResolution]); } } else { - let moduleResolution = compilerOptions.moduleResolution; - if (moduleResolution === undefined) { - moduleResolution = getEmitModuleKind(compilerOptions) === ModuleKind.CommonJS ? ModuleResolutionKind.NodeJs : ModuleResolutionKind.Classic; - if (traceEnabled) { - trace(host, Diagnostics.Module_resolution_kind_is_not_specified_using_0, ModuleResolutionKind[moduleResolution]); - } - } - else { - if (traceEnabled) { - trace(host, Diagnostics.Explicitly_specified_module_resolution_kind_Colon_0, ModuleResolutionKind[moduleResolution]); - } - } - - perfLogger.logStartResolveModule(moduleName /* , containingFile, ModuleResolutionKind[moduleResolution]*/); - switch (moduleResolution) { - case ModuleResolutionKind.NodeJs: - result = nodeModuleNameResolver(moduleName, containingFile, compilerOptions, host, cache, redirectedReference); - break; - case ModuleResolutionKind.Classic: - result = classicNameResolver(moduleName, containingFile, compilerOptions, host, cache, redirectedReference); - break; - default: - return Debug.fail(`Unexpected moduleResolution: ${moduleResolution}`); - } - if (result && result.resolvedModule) perfLogger.logInfoEvent(`Module "${moduleName}" resolved to "${result.resolvedModule.resolvedFileName}"`); - perfLogger.logStopResolveModule((result && result.resolvedModule) ? "" + result.resolvedModule.resolvedFileName : "null"); - - if (perFolderCache) { - perFolderCache.set(moduleName, result); - if (!isExternalModuleNameRelative(moduleName)) { - // put result in per-module name cache - cache!.getOrCreateCacheForModuleName(moduleName, redirectedReference).set(containingDirectory, result); - } + if (traceEnabled) { + trace(host, Diagnostics.Explicitly_specified_module_resolution_kind_Colon_0, ModuleResolutionKind[moduleResolution]); + } + } + perfLogger.logStartResolveModule(moduleName /* , containingFile, ModuleResolutionKind[moduleResolution]*/); + switch (moduleResolution) { + case ModuleResolutionKind.NodeJs: + result = nodeModuleNameResolver(moduleName, containingFile, compilerOptions, host, cache, redirectedReference); + break; + case ModuleResolutionKind.Classic: + result = classicNameResolver(moduleName, containingFile, compilerOptions, host, cache, redirectedReference); + break; + default: + return Debug.fail(`Unexpected moduleResolution: ${moduleResolution}`); + } + if (result && result.resolvedModule) + perfLogger.logInfoEvent(`Module "${moduleName}" resolved to "${result.resolvedModule.resolvedFileName}"`); + perfLogger.logStopResolveModule((result && result.resolvedModule) ? "" + result.resolvedModule.resolvedFileName : "null"); + if (perFolderCache) { + perFolderCache.set(moduleName, result); + if (!isExternalModuleNameRelative(moduleName)) { + // put result in per-module name cache + cache!.getOrCreateCacheForModuleName(moduleName, redirectedReference).set(containingDirectory, result); } } - - if (traceEnabled) { - if (result.resolvedModule) { - if (result.resolvedModule.packageId) { - trace(host, Diagnostics.Module_name_0_was_successfully_resolved_to_1_with_Package_ID_2, moduleName, result.resolvedModule.resolvedFileName, packageIdToString(result.resolvedModule.packageId)); - } - else { - trace(host, Diagnostics.Module_name_0_was_successfully_resolved_to_1, moduleName, result.resolvedModule.resolvedFileName); - } + } + if (traceEnabled) { + if (result.resolvedModule) { + if (result.resolvedModule.packageId) { + trace(host, Diagnostics.Module_name_0_was_successfully_resolved_to_1_with_Package_ID_2, moduleName, result.resolvedModule.resolvedFileName, packageIdToString(result.resolvedModule.packageId)); } else { - trace(host, Diagnostics.Module_name_0_was_not_resolved, moduleName); + trace(host, Diagnostics.Module_name_0_was_successfully_resolved_to_1, moduleName, result.resolvedModule.resolvedFileName); } } - - return result; - } - - /* - * Every module resolution kind can has its specific understanding how to load module from a specific path on disk - * I.e. for path '/a/b/c': - * - Node loader will first to try to check if '/a/b/c' points to a file with some supported extension and if this fails - * it will try to load module from directory: directory '/a/b/c' should exist and it should have either 'package.json' with - * 'typings' entry or file 'index' with some supported extension - * - Classic loader will only try to interpret '/a/b/c' as file. - */ - type ResolutionKindSpecificLoader = (extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState) => Resolved | undefined; - - /** - * Any module resolution kind can be augmented with optional settings: 'baseUrl', 'paths' and 'rootDirs' - they are used to - * mitigate differences between design time structure of the project and its runtime counterpart so the same import name - * can be resolved successfully by TypeScript compiler and runtime module loader. - * If these settings are set then loading procedure will try to use them to resolve module name and it can of failure it will - * fallback to standard resolution routine. - * - * - baseUrl - this setting controls how non-relative module names are resolved. If this setting is specified then non-relative - * names will be resolved relative to baseUrl: i.e. if baseUrl is '/a/b' then candidate location to resolve module name 'c/d' will - * be '/a/b/c/d' - * - paths - this setting can only be used when baseUrl is specified. allows to tune how non-relative module names - * will be resolved based on the content of the module name. - * Structure of 'paths' compiler options - * 'paths': { - * pattern-1: [...substitutions], - * pattern-2: [...substitutions], - * ... - * pattern-n: [...substitutions] - * } - * Pattern here is a string that can contain zero or one '*' character. During module resolution module name will be matched against - * all patterns in the list. Matching for patterns that don't contain '*' means that module name must be equal to pattern respecting the case. - * If pattern contains '*' then to match pattern "*" module name must start with the and end with . - * denotes part of the module name between and . - * If module name can be matches with multiple patterns then pattern with the longest prefix will be picked. - * After selecting pattern we'll use list of substitutions to get candidate locations of the module and the try to load module - * from the candidate location. - * Substitution is a string that can contain zero or one '*'. To get candidate location from substitution we'll pick every - * substitution in the list and replace '*' with string. If candidate location is not rooted it - * will be converted to absolute using baseUrl. - * For example: - * baseUrl: /a/b/c - * "paths": { - * // match all module names - * "*": [ - * "*", // use matched name as is, - * // will be looked as /a/b/c/ - * - * "folder1/*" // substitution will convert matched name to 'folder1/', - * // since it is not rooted then final candidate location will be /a/b/c/folder1/ - * ], - * // match module names that start with 'components/' - * "components/*": [ "/root/components/*" ] // substitution will convert /components/folder1/ to '/root/components/folder1/', - * // it is rooted so it will be final candidate location - * } - * - * 'rootDirs' allows the project to be spreaded across multiple locations and resolve modules with relative names as if - * they were in the same location. For example lets say there are two files - * '/local/src/content/file1.ts' - * '/shared/components/contracts/src/content/protocols/file2.ts' - * After bundling content of '/shared/components/contracts/src' will be merged with '/local/src' so - * if file1 has the following import 'import {x} from "./protocols/file2"' it will be resolved successfully in runtime. - * 'rootDirs' provides the way to tell compiler that in order to get the whole project it should behave as if content of all - * root dirs were merged together. - * I.e. for the example above 'rootDirs' will have two entries: [ '/local/src', '/shared/components/contracts/src' ]. - * Compiler will first convert './protocols/file2' into absolute path relative to the location of containing file: - * '/local/src/content/protocols/file2' and try to load it - failure. - * Then it will search 'rootDirs' looking for a longest matching prefix of this absolute path and if such prefix is found - absolute path will - * be converted to a path relative to found rootDir entry './content/protocols/file2' (*). As a last step compiler will check all remaining - * entries in 'rootDirs', use them to build absolute path out of (*) and try to resolve module from this location. - */ - function tryLoadModuleUsingOptionalResolutionSettings(extensions: Extensions, moduleName: string, containingDirectory: string, loader: ResolutionKindSpecificLoader, - state: ModuleResolutionState): Resolved | undefined { - - const resolved = tryLoadModuleUsingPathsIfEligible(extensions, moduleName, loader, state); - if (resolved) return resolved.value; - - if (!isExternalModuleNameRelative(moduleName)) { - return tryLoadModuleUsingBaseUrl(extensions, moduleName, loader, state); - } else { - return tryLoadModuleUsingRootDirs(extensions, moduleName, containingDirectory, loader, state); + trace(host, Diagnostics.Module_name_0_was_not_resolved, moduleName); } } - - function tryLoadModuleUsingPathsIfEligible(extensions: Extensions, moduleName: string, loader: ResolutionKindSpecificLoader, state: ModuleResolutionState) { - const { baseUrl, paths } = state.compilerOptions; - if (baseUrl && paths && !pathIsRelative(moduleName)) { - if (state.traceEnabled) { - trace(state.host, Diagnostics.baseUrl_option_is_set_to_0_using_this_value_to_resolve_non_relative_module_name_1, baseUrl, moduleName); - trace(state.host, Diagnostics.paths_option_is_specified_looking_for_a_pattern_to_match_module_name_0, moduleName); - } - return tryLoadModuleUsingPaths(extensions, moduleName, baseUrl, paths, loader, /*onlyRecordFailures*/ false, state); + return result; +} +/* + * Every module resolution kind can has its specific understanding how to load module from a specific path on disk + * I.e. for path '/a/b/c': + * - Node loader will first to try to check if '/a/b/c' points to a file with some supported extension and if this fails + * it will try to load module from directory: directory '/a/b/c' should exist and it should have either 'package.json' with + * 'typings' entry or file 'index' with some supported extension + * - Classic loader will only try to interpret '/a/b/c' as file. + */ +type ResolutionKindSpecificLoader = (extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState) => Resolved | undefined; +/** + * Any module resolution kind can be augmented with optional settings: 'baseUrl', 'paths' and 'rootDirs' - they are used to + * mitigate differences between design time structure of the project and its runtime counterpart so the same import name + * can be resolved successfully by TypeScript compiler and runtime module loader. + * If these settings are set then loading procedure will try to use them to resolve module name and it can of failure it will + * fallback to standard resolution routine. + * + * - baseUrl - this setting controls how non-relative module names are resolved. If this setting is specified then non-relative + * names will be resolved relative to baseUrl: i.e. if baseUrl is '/a/b' then candidate location to resolve module name 'c/d' will + * be '/a/b/c/d' + * - paths - this setting can only be used when baseUrl is specified. allows to tune how non-relative module names + * will be resolved based on the content of the module name. + * Structure of 'paths' compiler options + * 'paths': { + * pattern-1: [...substitutions], + * pattern-2: [...substitutions], + * ... + * pattern-n: [...substitutions] + * } + * Pattern here is a string that can contain zero or one '*' character. During module resolution module name will be matched against + * all patterns in the list. Matching for patterns that don't contain '*' means that module name must be equal to pattern respecting the case. + * If pattern contains '*' then to match pattern "*" module name must start with the and end with . + * denotes part of the module name between and . + * If module name can be matches with multiple patterns then pattern with the longest prefix will be picked. + * After selecting pattern we'll use list of substitutions to get candidate locations of the module and the try to load module + * from the candidate location. + * Substitution is a string that can contain zero or one '*'. To get candidate location from substitution we'll pick every + * substitution in the list and replace '*' with string. If candidate location is not rooted it + * will be converted to absolute using baseUrl. + * For example: + * baseUrl: /a/b/c + * "paths": { + * // match all module names + * "*": [ + * "*", // use matched name as is, + * // will be looked as /a/b/c/ + * + * "folder1/*" // substitution will convert matched name to 'folder1/', + * // since it is not rooted then final candidate location will be /a/b/c/folder1/ + * ], + * // match module names that start with 'components/' + * "components/*": [ "/root/components/*" ] // substitution will convert /components/folder1/ to '/root/components/folder1/', + * // it is rooted so it will be final candidate location + * } + * + * 'rootDirs' allows the project to be spreaded across multiple locations and resolve modules with relative names as if + * they were in the same location. For example lets say there are two files + * '/local/src/content/file1.ts' + * '/shared/components/contracts/src/content/protocols/file2.ts' + * After bundling content of '/shared/components/contracts/src' will be merged with '/local/src' so + * if file1 has the following import 'import {x} from "./protocols/file2"' it will be resolved successfully in runtime. + * 'rootDirs' provides the way to tell compiler that in order to get the whole project it should behave as if content of all + * root dirs were merged together. + * I.e. for the example above 'rootDirs' will have two entries: [ '/local/src', '/shared/components/contracts/src' ]. + * Compiler will first convert './protocols/file2' into absolute path relative to the location of containing file: + * '/local/src/content/protocols/file2' and try to load it - failure. + * Then it will search 'rootDirs' looking for a longest matching prefix of this absolute path and if such prefix is found - absolute path will + * be converted to a path relative to found rootDir entry './content/protocols/file2' (*). As a last step compiler will check all remaining + * entries in 'rootDirs', use them to build absolute path out of (*) and try to resolve module from this location. + */ +function tryLoadModuleUsingOptionalResolutionSettings(extensions: Extensions, moduleName: string, containingDirectory: string, loader: ResolutionKindSpecificLoader, state: ModuleResolutionState): Resolved | undefined { + const resolved = tryLoadModuleUsingPathsIfEligible(extensions, moduleName, loader, state); + if (resolved) + return resolved.value; + if (!isExternalModuleNameRelative(moduleName)) { + return tryLoadModuleUsingBaseUrl(extensions, moduleName, loader, state); + } + else { + return tryLoadModuleUsingRootDirs(extensions, moduleName, containingDirectory, loader, state); + } +} +function tryLoadModuleUsingPathsIfEligible(extensions: Extensions, moduleName: string, loader: ResolutionKindSpecificLoader, state: ModuleResolutionState) { + const { baseUrl, paths } = state.compilerOptions; + if (baseUrl && paths && !pathIsRelative(moduleName)) { + if (state.traceEnabled) { + trace(state.host, Diagnostics.baseUrl_option_is_set_to_0_using_this_value_to_resolve_non_relative_module_name_1, baseUrl, moduleName); + trace(state.host, Diagnostics.paths_option_is_specified_looking_for_a_pattern_to_match_module_name_0, moduleName); + } + return tryLoadModuleUsingPaths(extensions, moduleName, baseUrl, paths, loader, /*onlyRecordFailures*/ false, state); + } +} +function tryLoadModuleUsingRootDirs(extensions: Extensions, moduleName: string, containingDirectory: string, loader: ResolutionKindSpecificLoader, state: ModuleResolutionState): Resolved | undefined { + if (!state.compilerOptions.rootDirs) { + return undefined; + } + if (state.traceEnabled) { + trace(state.host, Diagnostics.rootDirs_option_is_set_using_it_to_resolve_relative_module_name_0, moduleName); + } + const candidate = normalizePath(combinePaths(containingDirectory, moduleName)); + let matchedRootDir: string | undefined; + let matchedNormalizedPrefix: string | undefined; + for (const rootDir of state.compilerOptions.rootDirs) { + // rootDirs are expected to be absolute + // in case of tsconfig.json this will happen automatically - compiler will expand relative names + // using location of tsconfig.json as base location + let normalizedRoot = normalizePath(rootDir); + if (!endsWith(normalizedRoot, directorySeparator)) { + normalizedRoot += directorySeparator; + } + const isLongestMatchingPrefix = startsWith(candidate, normalizedRoot) && + (matchedNormalizedPrefix === undefined || matchedNormalizedPrefix.length < normalizedRoot.length); + if (state.traceEnabled) { + trace(state.host, Diagnostics.Checking_if_0_is_the_longest_matching_prefix_for_1_2, normalizedRoot, candidate, isLongestMatchingPrefix); + } + if (isLongestMatchingPrefix) { + matchedNormalizedPrefix = normalizedRoot; + matchedRootDir = rootDir; } } - - function tryLoadModuleUsingRootDirs(extensions: Extensions, moduleName: string, containingDirectory: string, loader: ResolutionKindSpecificLoader, - state: ModuleResolutionState): Resolved | undefined { - - if (!state.compilerOptions.rootDirs) { - return undefined; + if (matchedNormalizedPrefix) { + if (state.traceEnabled) { + trace(state.host, Diagnostics.Longest_matching_prefix_for_0_is_1, candidate, matchedNormalizedPrefix); } - + const suffix = candidate.substr(matchedNormalizedPrefix.length); + // first - try to load from a initial location if (state.traceEnabled) { - trace(state.host, Diagnostics.rootDirs_option_is_set_using_it_to_resolve_relative_module_name_0, moduleName); + trace(state.host, Diagnostics.Loading_0_from_the_root_dir_1_candidate_location_2, suffix, matchedNormalizedPrefix, candidate); } - - const candidate = normalizePath(combinePaths(containingDirectory, moduleName)); - - let matchedRootDir: string | undefined; - let matchedNormalizedPrefix: string | undefined; - for (const rootDir of state.compilerOptions.rootDirs) { - // rootDirs are expected to be absolute - // in case of tsconfig.json this will happen automatically - compiler will expand relative names - // using location of tsconfig.json as base location - let normalizedRoot = normalizePath(rootDir); - if (!endsWith(normalizedRoot, directorySeparator)) { - normalizedRoot += directorySeparator; - } - const isLongestMatchingPrefix = - startsWith(candidate, normalizedRoot) && - (matchedNormalizedPrefix === undefined || matchedNormalizedPrefix.length < normalizedRoot.length); - - if (state.traceEnabled) { - trace(state.host, Diagnostics.Checking_if_0_is_the_longest_matching_prefix_for_1_2, normalizedRoot, candidate, isLongestMatchingPrefix); - } - - if (isLongestMatchingPrefix) { - matchedNormalizedPrefix = normalizedRoot; - matchedRootDir = rootDir; - } + const resolvedFileName = loader(extensions, candidate, !directoryProbablyExists(containingDirectory, state.host), state); + if (resolvedFileName) { + return resolvedFileName; } - if (matchedNormalizedPrefix) { - if (state.traceEnabled) { - trace(state.host, Diagnostics.Longest_matching_prefix_for_0_is_1, candidate, matchedNormalizedPrefix); + if (state.traceEnabled) { + trace(state.host, Diagnostics.Trying_other_entries_in_rootDirs); + } + // then try to resolve using remaining entries in rootDirs + for (const rootDir of state.compilerOptions.rootDirs) { + if (rootDir === matchedRootDir) { + // skip the initially matched entry + continue; } - const suffix = candidate.substr(matchedNormalizedPrefix.length); - - // first - try to load from a initial location + const candidate = combinePaths(normalizePath(rootDir), suffix); if (state.traceEnabled) { - trace(state.host, Diagnostics.Loading_0_from_the_root_dir_1_candidate_location_2, suffix, matchedNormalizedPrefix, candidate); + trace(state.host, Diagnostics.Loading_0_from_the_root_dir_1_candidate_location_2, suffix, rootDir, candidate); } - const resolvedFileName = loader(extensions, candidate, !directoryProbablyExists(containingDirectory, state.host), state); + const baseDirectory = getDirectoryPath(candidate); + const resolvedFileName = loader(extensions, candidate, !directoryProbablyExists(baseDirectory, state.host), state); if (resolvedFileName) { return resolvedFileName; } - - if (state.traceEnabled) { - trace(state.host, Diagnostics.Trying_other_entries_in_rootDirs); - } - // then try to resolve using remaining entries in rootDirs - for (const rootDir of state.compilerOptions.rootDirs) { - if (rootDir === matchedRootDir) { - // skip the initially matched entry - continue; - } - const candidate = combinePaths(normalizePath(rootDir), suffix); - if (state.traceEnabled) { - trace(state.host, Diagnostics.Loading_0_from_the_root_dir_1_candidate_location_2, suffix, rootDir, candidate); - } - const baseDirectory = getDirectoryPath(candidate); - const resolvedFileName = loader(extensions, candidate, !directoryProbablyExists(baseDirectory, state.host), state); - if (resolvedFileName) { - return resolvedFileName; - } - } - if (state.traceEnabled) { - trace(state.host, Diagnostics.Module_resolution_using_rootDirs_has_failed); - } } - return undefined; - } - - function tryLoadModuleUsingBaseUrl(extensions: Extensions, moduleName: string, loader: ResolutionKindSpecificLoader, state: ModuleResolutionState): Resolved | undefined { - const { baseUrl } = state.compilerOptions; - if (!baseUrl) { - return undefined; - } - if (state.traceEnabled) { - trace(state.host, Diagnostics.baseUrl_option_is_set_to_0_using_this_value_to_resolve_non_relative_module_name_1, baseUrl, moduleName); - } - const candidate = normalizePath(combinePaths(baseUrl, moduleName)); if (state.traceEnabled) { - trace(state.host, Diagnostics.Resolving_module_name_0_relative_to_base_url_1_2, moduleName, baseUrl, candidate); + trace(state.host, Diagnostics.Module_resolution_using_rootDirs_has_failed); } - return loader(extensions, candidate, !directoryProbablyExists(getDirectoryPath(candidate), state.host), state); } - - /** - * Expose resolution logic to allow us to use Node module resolution logic from arbitrary locations. - * No way to do this with `require()`: https://github.com/nodejs/node/issues/5963 - * Throws an error if the module can't be resolved. - */ - /* @internal */ - export function resolveJSModule(moduleName: string, initialDir: string, host: ModuleResolutionHost): string { - const { resolvedModule, failedLookupLocations } = tryResolveJSModuleWorker(moduleName, initialDir, host); - if (!resolvedModule) { - throw new Error(`Could not resolve JS module '${moduleName}' starting at '${initialDir}'. Looked in: ${failedLookupLocations.join(", ")}`); - } - return resolvedModule.resolvedFileName; - } - - /* @internal */ - export function tryResolveJSModule(moduleName: string, initialDir: string, host: ModuleResolutionHost): string | undefined { - const { resolvedModule } = tryResolveJSModuleWorker(moduleName, initialDir, host); - return resolvedModule && resolvedModule.resolvedFileName; - } - - const jsOnlyExtensions = [Extensions.JavaScript]; - const tsExtensions = [Extensions.TypeScript, Extensions.JavaScript]; - const tsPlusJsonExtensions = [...tsExtensions, Extensions.Json]; - const tsconfigExtensions = [Extensions.TSConfig]; - function tryResolveJSModuleWorker(moduleName: string, initialDir: string, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations { - return nodeModuleNameResolverWorker(moduleName, initialDir, { moduleResolution: ModuleResolutionKind.NodeJs, allowJs: true }, host, /*cache*/ undefined, jsOnlyExtensions, /*redirectedReferences*/ undefined); - } - - export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference): ResolvedModuleWithFailedLookupLocations; - /* @internal */ export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference, lookupConfig?: boolean): ResolvedModuleWithFailedLookupLocations; // eslint-disable-line @typescript-eslint/unified-signatures - export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference, lookupConfig?: boolean): ResolvedModuleWithFailedLookupLocations { - return nodeModuleNameResolverWorker(moduleName, getDirectoryPath(containingFile), compilerOptions, host, cache, lookupConfig ? tsconfigExtensions : (compilerOptions.resolveJsonModule ? tsPlusJsonExtensions : tsExtensions), redirectedReference); - } - - function nodeModuleNameResolverWorker(moduleName: string, containingDirectory: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache: ModuleResolutionCache | undefined, extensions: Extensions[], redirectedReference: ResolvedProjectReference | undefined): ResolvedModuleWithFailedLookupLocations { - const traceEnabled = isTraceEnabled(compilerOptions, host); - - const failedLookupLocations: string[] = []; - const state: ModuleResolutionState = { compilerOptions, host, traceEnabled, failedLookupLocations }; - - const result = forEach(extensions, ext => tryResolve(ext)); - if (result && result.value) { - const { resolved, isExternalLibraryImport } = result.value; - return createResolvedModuleWithFailedLookupLocations(resolved, isExternalLibraryImport, failedLookupLocations); - } - return { resolvedModule: undefined, failedLookupLocations }; - - function tryResolve(extensions: Extensions): SearchResult<{ resolved: Resolved, isExternalLibraryImport: boolean }> { - const loader: ResolutionKindSpecificLoader = (extensions, candidate, onlyRecordFailures, state) => nodeLoadModuleByRelativeName(extensions, candidate, onlyRecordFailures, state, /*considerPackageJson*/ true); - const resolved = tryLoadModuleUsingOptionalResolutionSettings(extensions, moduleName, containingDirectory, loader, state); - if (resolved) { - return toSearchResult({ resolved, isExternalLibraryImport: pathContainsNodeModules(resolved.path) }); - } - - if (!isExternalModuleNameRelative(moduleName)) { - if (traceEnabled) { - trace(host, Diagnostics.Loading_module_0_from_node_modules_folder_target_file_type_1, moduleName, Extensions[extensions]); - } - const resolved = loadModuleFromNearestNodeModulesDirectory(extensions, moduleName, containingDirectory, state, cache, redirectedReference); - if (!resolved) return undefined; - - let resolvedValue = resolved.value; - if (!compilerOptions.preserveSymlinks && resolvedValue && !resolvedValue.originalPath) { - const path = realPath(resolvedValue.path, host, traceEnabled); - const originalPath = path === resolvedValue.path ? undefined : resolvedValue.path; - resolvedValue = { ...resolvedValue, path, originalPath }; - } - // For node_modules lookups, get the real path so that multiple accesses to an `npm link`-ed module do not create duplicate files. - return { value: resolvedValue && { resolved: resolvedValue, isExternalLibraryImport: true } }; - } - else { - const { path: candidate, parts } = normalizePathAndParts(combinePaths(containingDirectory, moduleName)); - const resolved = nodeLoadModuleByRelativeName(extensions, candidate, /*onlyRecordFailures*/ false, state, /*considerPackageJson*/ true); - // Treat explicit "node_modules" import as an external library import. - return resolved && toSearchResult({ resolved, isExternalLibraryImport: contains(parts, "node_modules") }); - } - } + return undefined; +} +function tryLoadModuleUsingBaseUrl(extensions: Extensions, moduleName: string, loader: ResolutionKindSpecificLoader, state: ModuleResolutionState): Resolved | undefined { + const { baseUrl } = state.compilerOptions; + if (!baseUrl) { + return undefined; } - - function realPath(path: string, host: ModuleResolutionHost, traceEnabled: boolean): string { - if (!host.realpath) { - return path; - } - - const real = normalizePath(host.realpath(path)); - if (traceEnabled) { - trace(host, Diagnostics.Resolving_real_path_for_0_result_1, path, real); - } - Debug.assert(host.fileExists(real), `${path} linked to nonexistent file ${real}`); - return real; + if (state.traceEnabled) { + trace(state.host, Diagnostics.baseUrl_option_is_set_to_0_using_this_value_to_resolve_non_relative_module_name_1, baseUrl, moduleName); } - - function nodeLoadModuleByRelativeName(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState, considerPackageJson: boolean): Resolved | undefined { - if (state.traceEnabled) { - trace(state.host, Diagnostics.Loading_module_as_file_Slash_folder_candidate_module_location_0_target_file_type_1, candidate, Extensions[extensions]); - } - if (!hasTrailingDirectorySeparator(candidate)) { - if (!onlyRecordFailures) { - const parentOfCandidate = getDirectoryPath(candidate); - if (!directoryProbablyExists(parentOfCandidate, state.host)) { - if (state.traceEnabled) { - trace(state.host, Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, parentOfCandidate); - } - onlyRecordFailures = true; - } + const candidate = normalizePath(combinePaths(baseUrl, moduleName)); + if (state.traceEnabled) { + trace(state.host, Diagnostics.Resolving_module_name_0_relative_to_base_url_1_2, moduleName, baseUrl, candidate); + } + return loader(extensions, candidate, !directoryProbablyExists(getDirectoryPath(candidate), state.host), state); +} +/** + * Expose resolution logic to allow us to use Node module resolution logic from arbitrary locations. + * No way to do this with `require()`: https://github.com/nodejs/node/issues/5963 + * Throws an error if the module can't be resolved. + */ +/* @internal */ +export function resolveJSModule(moduleName: string, initialDir: string, host: ModuleResolutionHost): string { + const { resolvedModule, failedLookupLocations } = tryResolveJSModuleWorker(moduleName, initialDir, host); + if (!resolvedModule) { + throw new Error(`Could not resolve JS module '${moduleName}' starting at '${initialDir}'. Looked in: ${failedLookupLocations.join(", ")}`); + } + return resolvedModule.resolvedFileName; +} +/* @internal */ +export function tryResolveJSModule(moduleName: string, initialDir: string, host: ModuleResolutionHost): string | undefined { + const { resolvedModule } = tryResolveJSModuleWorker(moduleName, initialDir, host); + return resolvedModule && resolvedModule.resolvedFileName; +} +const jsOnlyExtensions = [Extensions.JavaScript]; +const tsExtensions = [Extensions.TypeScript, Extensions.JavaScript]; +const tsPlusJsonExtensions = [...tsExtensions, Extensions.Json]; +const tsconfigExtensions = [Extensions.TSConfig]; +function tryResolveJSModuleWorker(moduleName: string, initialDir: string, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations { + return nodeModuleNameResolverWorker(moduleName, initialDir, { moduleResolution: ModuleResolutionKind.NodeJs, allowJs: true }, host, /*cache*/ undefined, jsOnlyExtensions, /*redirectedReferences*/ undefined); +} +export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference): ResolvedModuleWithFailedLookupLocations; +/* @internal */ export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference, lookupConfig?: boolean): ResolvedModuleWithFailedLookupLocations; // eslint-disable-line @typescript-eslint/unified-signatures +export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference, lookupConfig?: boolean): ResolvedModuleWithFailedLookupLocations { + return nodeModuleNameResolverWorker(moduleName, getDirectoryPath(containingFile), compilerOptions, host, cache, lookupConfig ? tsconfigExtensions : (compilerOptions.resolveJsonModule ? tsPlusJsonExtensions : tsExtensions), redirectedReference); +} +function nodeModuleNameResolverWorker(moduleName: string, containingDirectory: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache: ModuleResolutionCache | undefined, extensions: Extensions[], redirectedReference: ResolvedProjectReference | undefined): ResolvedModuleWithFailedLookupLocations { + const traceEnabled = isTraceEnabled(compilerOptions, host); + const failedLookupLocations: string[] = []; + const state: ModuleResolutionState = { compilerOptions, host, traceEnabled, failedLookupLocations }; + const result = forEach(extensions, ext => tryResolve(ext)); + if (result && result.value) { + const { resolved, isExternalLibraryImport } = result.value; + return createResolvedModuleWithFailedLookupLocations(resolved, isExternalLibraryImport, failedLookupLocations); + } + return { resolvedModule: undefined, failedLookupLocations }; + function tryResolve(extensions: Extensions): SearchResult<{ + resolved: Resolved; + isExternalLibraryImport: boolean; + }> { + const loader: ResolutionKindSpecificLoader = (extensions, candidate, onlyRecordFailures, state) => nodeLoadModuleByRelativeName(extensions, candidate, onlyRecordFailures, state, /*considerPackageJson*/ true); + const resolved = tryLoadModuleUsingOptionalResolutionSettings(extensions, moduleName, containingDirectory, loader, state); + if (resolved) { + return toSearchResult({ resolved, isExternalLibraryImport: pathContainsNodeModules(resolved.path) }); + } + if (!isExternalModuleNameRelative(moduleName)) { + if (traceEnabled) { + trace(host, Diagnostics.Loading_module_0_from_node_modules_folder_target_file_type_1, moduleName, Extensions[extensions]); } - const resolvedFromFile = loadModuleFromFile(extensions, candidate, onlyRecordFailures, state); - if (resolvedFromFile) { - const packageDirectory = considerPackageJson ? parseNodeModuleFromPath(resolvedFromFile) : undefined; - const packageInfo = packageDirectory ? getPackageJsonInfo(packageDirectory, /*onlyRecordFailures*/ false, state) : undefined; - return withPackageId(packageInfo, resolvedFromFile); + const resolved = loadModuleFromNearestNodeModulesDirectory(extensions, moduleName, containingDirectory, state, cache, redirectedReference); + if (!resolved) + return undefined; + let resolvedValue = resolved.value; + if (!compilerOptions.preserveSymlinks && resolvedValue && !resolvedValue.originalPath) { + const path = realPath(resolvedValue.path, host, traceEnabled); + const originalPath = path === resolvedValue.path ? undefined : resolvedValue.path; + resolvedValue = { ...resolvedValue, path, originalPath }; } + // For node_modules lookups, get the real path so that multiple accesses to an `npm link`-ed module do not create duplicate files. + return { value: resolvedValue && { resolved: resolvedValue, isExternalLibraryImport: true } }; + } + else { + const { path: candidate, parts } = normalizePathAndParts(combinePaths(containingDirectory, moduleName)); + const resolved = nodeLoadModuleByRelativeName(extensions, candidate, /*onlyRecordFailures*/ false, state, /*considerPackageJson*/ true); + // Treat explicit "node_modules" import as an external library import. + return resolved && toSearchResult({ resolved, isExternalLibraryImport: contains(parts, "node_modules") }); } + } +} +function realPath(path: string, host: ModuleResolutionHost, traceEnabled: boolean): string { + if (!host.realpath) { + return path; + } + const real = normalizePath(host.realpath(path)); + if (traceEnabled) { + trace(host, Diagnostics.Resolving_real_path_for_0_result_1, path, real); + } + Debug.assert(host.fileExists(real), `${path} linked to nonexistent file ${real}`); + return real; +} +function nodeLoadModuleByRelativeName(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState, considerPackageJson: boolean): Resolved | undefined { + if (state.traceEnabled) { + trace(state.host, Diagnostics.Loading_module_as_file_Slash_folder_candidate_module_location_0_target_file_type_1, candidate, Extensions[extensions]); + } + if (!hasTrailingDirectorySeparator(candidate)) { if (!onlyRecordFailures) { - const candidateExists = directoryProbablyExists(candidate, state.host); - if (!candidateExists) { + const parentOfCandidate = getDirectoryPath(candidate); + if (!directoryProbablyExists(parentOfCandidate, state.host)) { if (state.traceEnabled) { - trace(state.host, Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, candidate); + trace(state.host, Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, parentOfCandidate); } onlyRecordFailures = true; } } - return loadNodeModuleFromDirectory(extensions, candidate, onlyRecordFailures, state, considerPackageJson); - } - - /*@internal*/ - export const nodeModulesPathPart = "/node_modules/"; - /*@internal*/ - export function pathContainsNodeModules(path: string): boolean { - return stringContains(path, nodeModulesPathPart); + const resolvedFromFile = loadModuleFromFile(extensions, candidate, onlyRecordFailures, state); + if (resolvedFromFile) { + const packageDirectory = considerPackageJson ? parseNodeModuleFromPath(resolvedFromFile) : undefined; + const packageInfo = packageDirectory ? getPackageJsonInfo(packageDirectory, /*onlyRecordFailures*/ false, state) : undefined; + return withPackageId(packageInfo, resolvedFromFile); + } } - - /** - * This will be called on the successfully resolved path from `loadModuleFromFile`. - * (Not neeeded for `loadModuleFromNodeModules` as that looks up the `package.json` as part of resolution.) - * - * packageDirectory is the directory of the package itself. - * For `blah/node_modules/foo/index.d.ts` this is packageDirectory: "foo" - * For `/node_modules/foo/bar.d.ts` this is packageDirectory: "foo" - * For `/node_modules/@types/foo/bar/index.d.ts` this is packageDirectory: "@types/foo" - * For `/node_modules/foo/bar/index.d.ts` this is packageDirectory: "foo" - */ - function parseNodeModuleFromPath(resolved: PathAndExtension): string | undefined { - const path = normalizePath(resolved.path); - const idx = path.lastIndexOf(nodeModulesPathPart); - if (idx === -1) { - return undefined; - } - - const indexAfterNodeModules = idx + nodeModulesPathPart.length; - let indexAfterPackageName = moveToNextDirectorySeparatorIfAvailable(path, indexAfterNodeModules); - if (path.charCodeAt(indexAfterNodeModules) === CharacterCodes.at) { - indexAfterPackageName = moveToNextDirectorySeparatorIfAvailable(path, indexAfterPackageName); - } - return path.slice(0, indexAfterPackageName); - } - - function moveToNextDirectorySeparatorIfAvailable(path: string, prevSeparatorIndex: number): number { - const nextSeparatorIndex = path.indexOf(directorySeparator, prevSeparatorIndex + 1); - return nextSeparatorIndex === -1 ? prevSeparatorIndex : nextSeparatorIndex; - } - - function loadModuleFromFileNoPackageId(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState): Resolved | undefined { - return noPackageId(loadModuleFromFile(extensions, candidate, onlyRecordFailures, state)); - } - - /** - * @param {boolean} onlyRecordFailures - if true then function won't try to actually load files but instead record all attempts as failures. This flag is necessary - * in cases when we know upfront that all load attempts will fail (because containing folder does not exists) however we still need to record all failed lookup locations. - */ - function loadModuleFromFile(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined { - if (extensions === Extensions.Json || extensions === Extensions.TSConfig) { - const extensionLess = tryRemoveExtension(candidate, Extension.Json); - return (extensionLess === undefined && extensions === Extensions.Json) ? undefined : tryAddingExtensions(extensionLess || candidate, extensions, onlyRecordFailures, state); - } - - // First, try adding an extension. An import of "foo" could be matched by a file "foo.ts", or "foo.js" by "foo.js.ts" - const resolvedByAddingExtension = tryAddingExtensions(candidate, extensions, onlyRecordFailures, state); - if (resolvedByAddingExtension) { - return resolvedByAddingExtension; - } - - // If that didn't work, try stripping a ".js" or ".jsx" extension and replacing it with a TypeScript one; - // e.g. "./foo.js" can be matched by "./foo.ts" or "./foo.d.ts" - if (hasJSFileExtension(candidate)) { - const extensionless = removeFileExtension(candidate); + if (!onlyRecordFailures) { + const candidateExists = directoryProbablyExists(candidate, state.host); + if (!candidateExists) { if (state.traceEnabled) { - const extension = candidate.substring(extensionless.length); - trace(state.host, Diagnostics.File_name_0_has_a_1_extension_stripping_it, candidate, extension); + trace(state.host, Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, candidate); } - return tryAddingExtensions(extensionless, extensions, onlyRecordFailures, state); + onlyRecordFailures = true; } } - - /** Try to return an existing file that adds one of the `extensions` to `candidate`. */ - function tryAddingExtensions(candidate: string, extensions: Extensions, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined { - if (!onlyRecordFailures) { - // check if containing folder exists - if it doesn't then just record failures for all supported extensions without disk probing - const directory = getDirectoryPath(candidate); - if (directory) { - onlyRecordFailures = !directoryProbablyExists(directory, state.host); - } - } - - switch (extensions) { - case Extensions.DtsOnly: - return tryExtension(Extension.Dts); - case Extensions.TypeScript: - return tryExtension(Extension.Ts) || tryExtension(Extension.Tsx) || tryExtension(Extension.Dts); - case Extensions.JavaScript: - return tryExtension(Extension.Js) || tryExtension(Extension.Jsx); - case Extensions.TSConfig: - case Extensions.Json: - return tryExtension(Extension.Json); - } - - function tryExtension(ext: Extension): PathAndExtension | undefined { - const path = tryFile(candidate + ext, onlyRecordFailures, state); - return path === undefined ? undefined : { path, ext }; - } + return loadNodeModuleFromDirectory(extensions, candidate, onlyRecordFailures, state, considerPackageJson); +} +/*@internal*/ +export const nodeModulesPathPart = "/node_modules/"; +/*@internal*/ +export function pathContainsNodeModules(path: string): boolean { + return stringContains(path, nodeModulesPathPart); +} +/** + * This will be called on the successfully resolved path from `loadModuleFromFile`. + * (Not neeeded for `loadModuleFromNodeModules` as that looks up the `package.json` as part of resolution.) + * + * packageDirectory is the directory of the package itself. + * For `blah/node_modules/foo/index.d.ts` this is packageDirectory: "foo" + * For `/node_modules/foo/bar.d.ts` this is packageDirectory: "foo" + * For `/node_modules/@types/foo/bar/index.d.ts` this is packageDirectory: "@types/foo" + * For `/node_modules/foo/bar/index.d.ts` this is packageDirectory: "foo" + */ +function parseNodeModuleFromPath(resolved: PathAndExtension): string | undefined { + const path = normalizePath(resolved.path); + const idx = path.lastIndexOf(nodeModulesPathPart); + if (idx === -1) { + return undefined; } - - /** Return the file if it exists. */ - function tryFile(fileName: string, onlyRecordFailures: boolean, state: ModuleResolutionState): string | undefined { - if (!onlyRecordFailures) { - if (state.host.fileExists(fileName)) { - if (state.traceEnabled) { - trace(state.host, Diagnostics.File_0_exist_use_it_as_a_name_resolution_result, fileName); - } - return fileName; - } - else { - if (state.traceEnabled) { - trace(state.host, Diagnostics.File_0_does_not_exist, fileName); - } - } + const indexAfterNodeModules = idx + nodeModulesPathPart.length; + let indexAfterPackageName = moveToNextDirectorySeparatorIfAvailable(path, indexAfterNodeModules); + if (path.charCodeAt(indexAfterNodeModules) === CharacterCodes.at) { + indexAfterPackageName = moveToNextDirectorySeparatorIfAvailable(path, indexAfterPackageName); + } + return path.slice(0, indexAfterPackageName); +} +function moveToNextDirectorySeparatorIfAvailable(path: string, prevSeparatorIndex: number): number { + const nextSeparatorIndex = path.indexOf(directorySeparator, prevSeparatorIndex + 1); + return nextSeparatorIndex === -1 ? prevSeparatorIndex : nextSeparatorIndex; +} +function loadModuleFromFileNoPackageId(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState): Resolved | undefined { + return noPackageId(loadModuleFromFile(extensions, candidate, onlyRecordFailures, state)); +} +/** + * @param {boolean} onlyRecordFailures - if true then function won't try to actually load files but instead record all attempts as failures. This flag is necessary + * in cases when we know upfront that all load attempts will fail (because containing folder does not exists) however we still need to record all failed lookup locations. + */ +function loadModuleFromFile(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined { + if (extensions === Extensions.Json || extensions === Extensions.TSConfig) { + const extensionLess = tryRemoveExtension(candidate, Extension.Json); + return (extensionLess === undefined && extensions === Extensions.Json) ? undefined : tryAddingExtensions(extensionLess || candidate, extensions, onlyRecordFailures, state); + } + // First, try adding an extension. An import of "foo" could be matched by a file "foo.ts", or "foo.js" by "foo.js.ts" + const resolvedByAddingExtension = tryAddingExtensions(candidate, extensions, onlyRecordFailures, state); + if (resolvedByAddingExtension) { + return resolvedByAddingExtension; + } + // If that didn't work, try stripping a ".js" or ".jsx" extension and replacing it with a TypeScript one; + // e.g. "./foo.js" can be matched by "./foo.ts" or "./foo.d.ts" + if (hasJSFileExtension(candidate)) { + const extensionless = removeFileExtension(candidate); + if (state.traceEnabled) { + const extension = candidate.substring(extensionless.length); + trace(state.host, Diagnostics.File_name_0_has_a_1_extension_stripping_it, candidate, extension); } - state.failedLookupLocations.push(fileName); - return undefined; + return tryAddingExtensions(extensionless, extensions, onlyRecordFailures, state); } - - function loadNodeModuleFromDirectory(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState, considerPackageJson = true) { - const packageInfo = considerPackageJson ? getPackageJsonInfo(candidate, onlyRecordFailures, state) : undefined; - const packageJsonContent = packageInfo && packageInfo.packageJsonContent; - const versionPaths = packageInfo && packageInfo.versionPaths; - return withPackageId(packageInfo, loadNodeModuleFromDirectoryWorker(extensions, candidate, onlyRecordFailures, state, packageJsonContent, versionPaths)); - } - - interface PackageJsonInfo { - packageDirectory: string; - packageJsonContent: PackageJsonPathFields; - versionPaths: VersionPaths | undefined; - } - - function getPackageJsonInfo(packageDirectory: string, onlyRecordFailures: boolean, state: ModuleResolutionState): PackageJsonInfo | undefined { - const { host, traceEnabled } = state; - const directoryExists = !onlyRecordFailures && directoryProbablyExists(packageDirectory, host); - const packageJsonPath = combinePaths(packageDirectory, "package.json"); - if (directoryExists && host.fileExists(packageJsonPath)) { - const packageJsonContent = readJson(packageJsonPath, host) as PackageJson; - if (traceEnabled) { - trace(host, Diagnostics.Found_package_json_at_0, packageJsonPath); +} +/** Try to return an existing file that adds one of the `extensions` to `candidate`. */ +function tryAddingExtensions(candidate: string, extensions: Extensions, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined { + if (!onlyRecordFailures) { + // check if containing folder exists - if it doesn't then just record failures for all supported extensions without disk probing + const directory = getDirectoryPath(candidate); + if (directory) { + onlyRecordFailures = !directoryProbablyExists(directory, state.host); + } + } + switch (extensions) { + case Extensions.DtsOnly: + return tryExtension(Extension.Dts); + case Extensions.TypeScript: + return tryExtension(Extension.Ts) || tryExtension(Extension.Tsx) || tryExtension(Extension.Dts); + case Extensions.JavaScript: + return tryExtension(Extension.Js) || tryExtension(Extension.Jsx); + case Extensions.TSConfig: + case Extensions.Json: + return tryExtension(Extension.Json); + } + function tryExtension(ext: Extension): PathAndExtension | undefined { + const path = tryFile(candidate + ext, onlyRecordFailures, state); + return path === undefined ? undefined : { path, ext }; + } +} +/** Return the file if it exists. */ +function tryFile(fileName: string, onlyRecordFailures: boolean, state: ModuleResolutionState): string | undefined { + if (!onlyRecordFailures) { + if (state.host.fileExists(fileName)) { + if (state.traceEnabled) { + trace(state.host, Diagnostics.File_0_exist_use_it_as_a_name_resolution_result, fileName); } - const versionPaths = readPackageJsonTypesVersionPaths(packageJsonContent, state); - return { packageDirectory, packageJsonContent, versionPaths }; + return fileName; } else { - if (directoryExists && traceEnabled) { - trace(host, Diagnostics.File_0_does_not_exist, packageJsonPath); - } - - // record package json as one of failed lookup locations - in the future if this file will appear it will invalidate resolution results - state.failedLookupLocations.push(packageJsonPath); - } - } - - function loadNodeModuleFromDirectoryWorker(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState, jsonContent: PackageJsonPathFields | undefined, versionPaths: VersionPaths | undefined): PathAndExtension | undefined { - let packageFile: string | undefined; - if (jsonContent) { - switch (extensions) { - case Extensions.JavaScript: - case Extensions.Json: - packageFile = readPackageJsonMainField(jsonContent, candidate, state); - break; - case Extensions.TypeScript: - // When resolving typescript modules, try resolving using main field as well - packageFile = readPackageJsonTypesFields(jsonContent, candidate, state) || readPackageJsonMainField(jsonContent, candidate, state); - break; - case Extensions.DtsOnly: - packageFile = readPackageJsonTypesFields(jsonContent, candidate, state); - break; - case Extensions.TSConfig: - packageFile = readPackageJsonTSConfigField(jsonContent, candidate, state); - break; - default: - return Debug.assertNever(extensions); - } - } - - const loader: ResolutionKindSpecificLoader = (extensions, candidate, onlyRecordFailures, state) => { - const fromFile = tryFile(candidate, onlyRecordFailures, state); - if (fromFile) { - const resolved = resolvedIfExtensionMatches(extensions, fromFile); - if (resolved) { - return noPackageId(resolved); - } - if (state.traceEnabled) { - trace(state.host, Diagnostics.File_0_has_an_unsupported_extension_so_skipping_it, fromFile); - } - } - - // Even if extensions is DtsOnly, we can still look up a .ts file as a result of package.json "types" - const nextExtensions = extensions === Extensions.DtsOnly ? Extensions.TypeScript : extensions; - // Don't do package.json lookup recursively, because Node.js' package lookup doesn't. - return nodeLoadModuleByRelativeName(nextExtensions, candidate, onlyRecordFailures, state, /*considerPackageJson*/ false); - }; - - const onlyRecordFailuresForPackageFile = packageFile ? !directoryProbablyExists(getDirectoryPath(packageFile), state.host) : undefined; - const onlyRecordFailuresForIndex = onlyRecordFailures || !directoryProbablyExists(candidate, state.host); - const indexPath = combinePaths(candidate, extensions === Extensions.TSConfig ? "tsconfig" : "index"); - - if (versionPaths && (!packageFile || containsPath(candidate, packageFile))) { - const moduleName = getRelativePathFromDirectory(candidate, packageFile || indexPath, /*ignoreCase*/ false); if (state.traceEnabled) { - trace(state.host, Diagnostics.package_json_has_a_typesVersions_entry_0_that_matches_compiler_version_1_looking_for_a_pattern_to_match_module_name_2, versionPaths.version, version, moduleName); - } - const result = tryLoadModuleUsingPaths(extensions, moduleName, candidate, versionPaths.paths, loader, onlyRecordFailuresForPackageFile || onlyRecordFailuresForIndex, state); - if (result) { - return removeIgnoredPackageId(result.value); + trace(state.host, Diagnostics.File_0_does_not_exist, fileName); } } - - // It won't have a `packageId` set, because we disabled `considerPackageJson`. - const packageFileResult = packageFile && removeIgnoredPackageId(loader(extensions, packageFile, onlyRecordFailuresForPackageFile!, state)); - if (packageFileResult) return packageFileResult; - - return loadModuleFromFile(extensions, indexPath, onlyRecordFailuresForIndex, state); - } - - /** Resolve from an arbitrarily specified file. Return `undefined` if it has an unsupported extension. */ - function resolvedIfExtensionMatches(extensions: Extensions, path: string): PathAndExtension | undefined { - const ext = tryGetExtensionFromPath(path); - return ext !== undefined && extensionIsOk(extensions, ext) ? { path, ext } : undefined; - } - - /** True if `extension` is one of the supported `extensions`. */ - function extensionIsOk(extensions: Extensions, extension: Extension): boolean { + } + state.failedLookupLocations.push(fileName); + return undefined; +} +function loadNodeModuleFromDirectory(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState, considerPackageJson = true) { + const packageInfo = considerPackageJson ? getPackageJsonInfo(candidate, onlyRecordFailures, state) : undefined; + const packageJsonContent = packageInfo && packageInfo.packageJsonContent; + const versionPaths = packageInfo && packageInfo.versionPaths; + return withPackageId(packageInfo, loadNodeModuleFromDirectoryWorker(extensions, candidate, onlyRecordFailures, state, packageJsonContent, versionPaths)); +} +interface PackageJsonInfo { + packageDirectory: string; + packageJsonContent: PackageJsonPathFields; + versionPaths: VersionPaths | undefined; +} +function getPackageJsonInfo(packageDirectory: string, onlyRecordFailures: boolean, state: ModuleResolutionState): PackageJsonInfo | undefined { + const { host, traceEnabled } = state; + const directoryExists = !onlyRecordFailures && directoryProbablyExists(packageDirectory, host); + const packageJsonPath = combinePaths(packageDirectory, "package.json"); + if (directoryExists && host.fileExists(packageJsonPath)) { + const packageJsonContent = (readJson(packageJsonPath, host) as PackageJson); + if (traceEnabled) { + trace(host, Diagnostics.Found_package_json_at_0, packageJsonPath); + } + const versionPaths = readPackageJsonTypesVersionPaths(packageJsonContent, state); + return { packageDirectory, packageJsonContent, versionPaths }; + } + else { + if (directoryExists && traceEnabled) { + trace(host, Diagnostics.File_0_does_not_exist, packageJsonPath); + } + // record package json as one of failed lookup locations - in the future if this file will appear it will invalidate resolution results + state.failedLookupLocations.push(packageJsonPath); + } +} +function loadNodeModuleFromDirectoryWorker(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState, jsonContent: PackageJsonPathFields | undefined, versionPaths: VersionPaths | undefined): PathAndExtension | undefined { + let packageFile: string | undefined; + if (jsonContent) { switch (extensions) { case Extensions.JavaScript: - return extension === Extension.Js || extension === Extension.Jsx; - case Extensions.TSConfig: case Extensions.Json: - return extension === Extension.Json; + packageFile = readPackageJsonMainField(jsonContent, candidate, state); + break; case Extensions.TypeScript: - return extension === Extension.Ts || extension === Extension.Tsx || extension === Extension.Dts; + // When resolving typescript modules, try resolving using main field as well + packageFile = readPackageJsonTypesFields(jsonContent, candidate, state) || readPackageJsonMainField(jsonContent, candidate, state); + break; case Extensions.DtsOnly: - return extension === Extension.Dts; + packageFile = readPackageJsonTypesFields(jsonContent, candidate, state); + break; + case Extensions.TSConfig: + packageFile = readPackageJsonTSConfigField(jsonContent, candidate, state); + break; + default: + return Debug.assertNever(extensions); } } - - /* @internal */ - export function parsePackageName(moduleName: string): { packageName: string, rest: string } { - let idx = moduleName.indexOf(directorySeparator); - if (moduleName[0] === "@") { - idx = moduleName.indexOf(directorySeparator, idx + 1); + const loader: ResolutionKindSpecificLoader = (extensions, candidate, onlyRecordFailures, state) => { + const fromFile = tryFile(candidate, onlyRecordFailures, state); + if (fromFile) { + const resolved = resolvedIfExtensionMatches(extensions, fromFile); + if (resolved) { + return noPackageId(resolved); + } + if (state.traceEnabled) { + trace(state.host, Diagnostics.File_0_has_an_unsupported_extension_so_skipping_it, fromFile); + } + } + // Even if extensions is DtsOnly, we can still look up a .ts file as a result of package.json "types" + const nextExtensions = extensions === Extensions.DtsOnly ? Extensions.TypeScript : extensions; + // Don't do package.json lookup recursively, because Node.js' package lookup doesn't. + return nodeLoadModuleByRelativeName(nextExtensions, candidate, onlyRecordFailures, state, /*considerPackageJson*/ false); + }; + const onlyRecordFailuresForPackageFile = packageFile ? !directoryProbablyExists(getDirectoryPath(packageFile), state.host) : undefined; + const onlyRecordFailuresForIndex = onlyRecordFailures || !directoryProbablyExists(candidate, state.host); + const indexPath = combinePaths(candidate, extensions === Extensions.TSConfig ? "tsconfig" : "index"); + if (versionPaths && (!packageFile || containsPath(candidate, packageFile))) { + const moduleName = getRelativePathFromDirectory(candidate, packageFile || indexPath, /*ignoreCase*/ false); + if (state.traceEnabled) { + trace(state.host, Diagnostics.package_json_has_a_typesVersions_entry_0_that_matches_compiler_version_1_looking_for_a_pattern_to_match_module_name_2, versionPaths.version, version, moduleName); + } + const result = tryLoadModuleUsingPaths(extensions, moduleName, candidate, versionPaths.paths, loader, onlyRecordFailuresForPackageFile || onlyRecordFailuresForIndex, state); + if (result) { + return removeIgnoredPackageId(result.value); } - return idx === -1 ? { packageName: moduleName, rest: "" } : { packageName: moduleName.slice(0, idx), rest: moduleName.slice(idx + 1) }; - } - - function loadModuleFromNearestNodeModulesDirectory(extensions: Extensions, moduleName: string, directory: string, state: ModuleResolutionState, cache: NonRelativeModuleNameResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined): SearchResult { - return loadModuleFromNearestNodeModulesDirectoryWorker(extensions, moduleName, directory, state, /*typesScopeOnly*/ false, cache, redirectedReference); } - - function loadModuleFromNearestNodeModulesDirectoryTypesScope(moduleName: string, directory: string, state: ModuleResolutionState): SearchResult { - // Extensions parameter here doesn't actually matter, because typesOnly ensures we're just doing @types lookup, which is always DtsOnly. - return loadModuleFromNearestNodeModulesDirectoryWorker(Extensions.DtsOnly, moduleName, directory, state, /*typesScopeOnly*/ true, /*cache*/ undefined, /*redirectedReference*/ undefined); + // It won't have a `packageId` set, because we disabled `considerPackageJson`. + const packageFileResult = packageFile && removeIgnoredPackageId(loader(extensions, packageFile, onlyRecordFailuresForPackageFile!, state)); + if (packageFileResult) + return packageFileResult; + return loadModuleFromFile(extensions, indexPath, onlyRecordFailuresForIndex, state); +} +/** Resolve from an arbitrarily specified file. Return `undefined` if it has an unsupported extension. */ +function resolvedIfExtensionMatches(extensions: Extensions, path: string): PathAndExtension | undefined { + const ext = tryGetExtensionFromPath(path); + return ext !== undefined && extensionIsOk(extensions, ext) ? { path, ext } : undefined; +} +/** True if `extension` is one of the supported `extensions`. */ +function extensionIsOk(extensions: Extensions, extension: Extension): boolean { + switch (extensions) { + case Extensions.JavaScript: + return extension === Extension.Js || extension === Extension.Jsx; + case Extensions.TSConfig: + case Extensions.Json: + return extension === Extension.Json; + case Extensions.TypeScript: + return extension === Extension.Ts || extension === Extension.Tsx || extension === Extension.Dts; + case Extensions.DtsOnly: + return extension === Extension.Dts; } - - function loadModuleFromNearestNodeModulesDirectoryWorker(extensions: Extensions, moduleName: string, directory: string, state: ModuleResolutionState, typesScopeOnly: boolean, cache: NonRelativeModuleNameResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined): SearchResult { - const perModuleNameCache = cache && cache.getOrCreateCacheForModuleName(moduleName, redirectedReference); - return forEachAncestorDirectory(normalizeSlashes(directory), ancestorDirectory => { - if (getBaseFileName(ancestorDirectory) !== "node_modules") { - const resolutionFromCache = tryFindNonRelativeModuleNameInCache(perModuleNameCache, moduleName, ancestorDirectory, state); - if (resolutionFromCache) { - return resolutionFromCache; - } - return toSearchResult(loadModuleFromImmediateNodeModulesDirectory(extensions, moduleName, ancestorDirectory, state, typesScopeOnly)); +} +/* @internal */ +export function parsePackageName(moduleName: string): { + packageName: string; + rest: string; +} { + let idx = moduleName.indexOf(directorySeparator); + if (moduleName[0] === "@") { + idx = moduleName.indexOf(directorySeparator, idx + 1); + } + return idx === -1 ? { packageName: moduleName, rest: "" } : { packageName: moduleName.slice(0, idx), rest: moduleName.slice(idx + 1) }; +} +function loadModuleFromNearestNodeModulesDirectory(extensions: Extensions, moduleName: string, directory: string, state: ModuleResolutionState, cache: NonRelativeModuleNameResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined): SearchResult { + return loadModuleFromNearestNodeModulesDirectoryWorker(extensions, moduleName, directory, state, /*typesScopeOnly*/ false, cache, redirectedReference); +} +function loadModuleFromNearestNodeModulesDirectoryTypesScope(moduleName: string, directory: string, state: ModuleResolutionState): SearchResult { + // Extensions parameter here doesn't actually matter, because typesOnly ensures we're just doing @types lookup, which is always DtsOnly. + return loadModuleFromNearestNodeModulesDirectoryWorker(Extensions.DtsOnly, moduleName, directory, state, /*typesScopeOnly*/ true, /*cache*/ undefined, /*redirectedReference*/ undefined); +} +function loadModuleFromNearestNodeModulesDirectoryWorker(extensions: Extensions, moduleName: string, directory: string, state: ModuleResolutionState, typesScopeOnly: boolean, cache: NonRelativeModuleNameResolutionCache | undefined, redirectedReference: ResolvedProjectReference | undefined): SearchResult { + const perModuleNameCache = cache && cache.getOrCreateCacheForModuleName(moduleName, redirectedReference); + return forEachAncestorDirectory(normalizeSlashes(directory), ancestorDirectory => { + if (getBaseFileName(ancestorDirectory) !== "node_modules") { + const resolutionFromCache = tryFindNonRelativeModuleNameInCache(perModuleNameCache, moduleName, ancestorDirectory, state); + if (resolutionFromCache) { + return resolutionFromCache; } - }); + return toSearchResult(loadModuleFromImmediateNodeModulesDirectory(extensions, moduleName, ancestorDirectory, state, typesScopeOnly)); + } + }); +} +function loadModuleFromImmediateNodeModulesDirectory(extensions: Extensions, moduleName: string, directory: string, state: ModuleResolutionState, typesScopeOnly: boolean): Resolved | undefined { + const nodeModulesFolder = combinePaths(directory, "node_modules"); + const nodeModulesFolderExists = directoryProbablyExists(nodeModulesFolder, state.host); + if (!nodeModulesFolderExists && state.traceEnabled) { + trace(state.host, Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, nodeModulesFolder); + } + const packageResult = typesScopeOnly ? undefined : loadModuleFromSpecificNodeModulesDirectory(extensions, moduleName, nodeModulesFolder, nodeModulesFolderExists, state); + if (packageResult) { + return packageResult; + } + if (extensions === Extensions.TypeScript || extensions === Extensions.DtsOnly) { + const nodeModulesAtTypes = combinePaths(nodeModulesFolder, "@types"); + let nodeModulesAtTypesExists = nodeModulesFolderExists; + if (nodeModulesFolderExists && !directoryProbablyExists(nodeModulesAtTypes, state.host)) { + if (state.traceEnabled) { + trace(state.host, Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, nodeModulesAtTypes); + } + nodeModulesAtTypesExists = false; + } + return loadModuleFromSpecificNodeModulesDirectory(Extensions.DtsOnly, mangleScopedPackageNameWithTrace(moduleName, state), nodeModulesAtTypes, nodeModulesAtTypesExists, state); } - - function loadModuleFromImmediateNodeModulesDirectory(extensions: Extensions, moduleName: string, directory: string, state: ModuleResolutionState, typesScopeOnly: boolean): Resolved | undefined { - const nodeModulesFolder = combinePaths(directory, "node_modules"); - const nodeModulesFolderExists = directoryProbablyExists(nodeModulesFolder, state.host); - if (!nodeModulesFolderExists && state.traceEnabled) { - trace(state.host, Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, nodeModulesFolder); - } - - const packageResult = typesScopeOnly ? undefined : loadModuleFromSpecificNodeModulesDirectory(extensions, moduleName, nodeModulesFolder, nodeModulesFolderExists, state); - if (packageResult) { - return packageResult; - } - if (extensions === Extensions.TypeScript || extensions === Extensions.DtsOnly) { - const nodeModulesAtTypes = combinePaths(nodeModulesFolder, "@types"); - let nodeModulesAtTypesExists = nodeModulesFolderExists; - if (nodeModulesFolderExists && !directoryProbablyExists(nodeModulesAtTypes, state.host)) { - if (state.traceEnabled) { - trace(state.host, Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, nodeModulesAtTypes); - } - nodeModulesAtTypesExists = false; +} +function loadModuleFromSpecificNodeModulesDirectory(extensions: Extensions, moduleName: string, nodeModulesDirectory: string, nodeModulesDirectoryExists: boolean, state: ModuleResolutionState): Resolved | undefined { + const candidate = normalizePath(combinePaths(nodeModulesDirectory, moduleName)); + // First look for a nested package.json, as in `node_modules/foo/bar/package.json`. + let packageInfo = getPackageJsonInfo(candidate, !nodeModulesDirectoryExists, state); + if (packageInfo) { + const fromFile = loadModuleFromFile(extensions, candidate, !nodeModulesDirectoryExists, state); + if (fromFile) { + return noPackageId(fromFile); + } + const fromDirectory = loadNodeModuleFromDirectoryWorker(extensions, candidate, !nodeModulesDirectoryExists, state, packageInfo.packageJsonContent, packageInfo.versionPaths); + return withPackageId(packageInfo, fromDirectory); + } + const loader: ResolutionKindSpecificLoader = (extensions, candidate, onlyRecordFailures, state) => { + const pathAndExtension = loadModuleFromFile(extensions, candidate, onlyRecordFailures, state) || + loadNodeModuleFromDirectoryWorker(extensions, candidate, onlyRecordFailures, state, packageInfo && packageInfo.packageJsonContent, packageInfo && packageInfo.versionPaths); + return withPackageId(packageInfo, pathAndExtension); + }; + const { packageName, rest } = parsePackageName(moduleName); + if (rest !== "") { // If "rest" is empty, we just did this search above. + const packageDirectory = combinePaths(nodeModulesDirectory, packageName); + // Don't use a "types" or "main" from here because we're not loading the root, but a subdirectory -- just here for the packageId and path mappings. + packageInfo = getPackageJsonInfo(packageDirectory, !nodeModulesDirectoryExists, state); + if (packageInfo && packageInfo.versionPaths) { + if (state.traceEnabled) { + trace(state.host, Diagnostics.package_json_has_a_typesVersions_entry_0_that_matches_compiler_version_1_looking_for_a_pattern_to_match_module_name_2, packageInfo.versionPaths.version, version, rest); } - return loadModuleFromSpecificNodeModulesDirectory(Extensions.DtsOnly, mangleScopedPackageNameWithTrace(moduleName, state), nodeModulesAtTypes, nodeModulesAtTypesExists, state); - } - } - - function loadModuleFromSpecificNodeModulesDirectory(extensions: Extensions, moduleName: string, nodeModulesDirectory: string, nodeModulesDirectoryExists: boolean, state: ModuleResolutionState): Resolved | undefined { - const candidate = normalizePath(combinePaths(nodeModulesDirectory, moduleName)); - - // First look for a nested package.json, as in `node_modules/foo/bar/package.json`. - let packageInfo = getPackageJsonInfo(candidate, !nodeModulesDirectoryExists, state); - if (packageInfo) { - const fromFile = loadModuleFromFile(extensions, candidate, !nodeModulesDirectoryExists, state); - if (fromFile) { - return noPackageId(fromFile); - } - - const fromDirectory = loadNodeModuleFromDirectoryWorker( - extensions, - candidate, - !nodeModulesDirectoryExists, - state, - packageInfo.packageJsonContent, - packageInfo.versionPaths - ); - return withPackageId(packageInfo, fromDirectory); - } - - const loader: ResolutionKindSpecificLoader = (extensions, candidate, onlyRecordFailures, state) => { - const pathAndExtension = - loadModuleFromFile(extensions, candidate, onlyRecordFailures, state) || - loadNodeModuleFromDirectoryWorker( - extensions, - candidate, - onlyRecordFailures, - state, - packageInfo && packageInfo.packageJsonContent, - packageInfo && packageInfo.versionPaths - ); - return withPackageId(packageInfo, pathAndExtension); - }; - - const { packageName, rest } = parsePackageName(moduleName); - if (rest !== "") { // If "rest" is empty, we just did this search above. - const packageDirectory = combinePaths(nodeModulesDirectory, packageName); - - // Don't use a "types" or "main" from here because we're not loading the root, but a subdirectory -- just here for the packageId and path mappings. - packageInfo = getPackageJsonInfo(packageDirectory, !nodeModulesDirectoryExists, state); - if (packageInfo && packageInfo.versionPaths) { - if (state.traceEnabled) { - trace(state.host, Diagnostics.package_json_has_a_typesVersions_entry_0_that_matches_compiler_version_1_looking_for_a_pattern_to_match_module_name_2, packageInfo.versionPaths.version, version, rest); - } - const packageDirectoryExists = nodeModulesDirectoryExists && directoryProbablyExists(packageDirectory, state.host); - const fromPaths = tryLoadModuleUsingPaths(extensions, rest, packageDirectory, packageInfo.versionPaths.paths, loader, !packageDirectoryExists, state); - if (fromPaths) { - return fromPaths.value; - } + const packageDirectoryExists = nodeModulesDirectoryExists && directoryProbablyExists(packageDirectory, state.host); + const fromPaths = tryLoadModuleUsingPaths(extensions, rest, packageDirectory, packageInfo.versionPaths.paths, loader, !packageDirectoryExists, state); + if (fromPaths) { + return fromPaths.value; } } - - return loader(extensions, candidate, !nodeModulesDirectoryExists, state); } - - function tryLoadModuleUsingPaths(extensions: Extensions, moduleName: string, baseDirectory: string, paths: MapLike, loader: ResolutionKindSpecificLoader, onlyRecordFailures: boolean, state: ModuleResolutionState): SearchResult { - const matchedPattern = matchPatternOrExact(getOwnKeys(paths), moduleName); - if (matchedPattern) { - const matchedStar = isString(matchedPattern) ? undefined : matchedText(matchedPattern, moduleName); - const matchedPatternText = isString(matchedPattern) ? matchedPattern : patternText(matchedPattern); + return loader(extensions, candidate, !nodeModulesDirectoryExists, state); +} +function tryLoadModuleUsingPaths(extensions: Extensions, moduleName: string, baseDirectory: string, paths: MapLike, loader: ResolutionKindSpecificLoader, onlyRecordFailures: boolean, state: ModuleResolutionState): SearchResult { + const matchedPattern = matchPatternOrExact(getOwnKeys(paths), moduleName); + if (matchedPattern) { + const matchedStar = isString(matchedPattern) ? undefined : matchedText(matchedPattern, moduleName); + const matchedPatternText = isString(matchedPattern) ? matchedPattern : patternText(matchedPattern); + if (state.traceEnabled) { + trace(state.host, Diagnostics.Module_name_0_matched_pattern_1, moduleName, matchedPatternText); + } + const resolved = forEach(paths[matchedPatternText], subst => { + const path = matchedStar ? subst.replace("*", matchedStar) : subst; + const candidate = normalizePath(combinePaths(baseDirectory, path)); if (state.traceEnabled) { - trace(state.host, Diagnostics.Module_name_0_matched_pattern_1, moduleName, matchedPatternText); - } - const resolved = forEach(paths[matchedPatternText], subst => { - const path = matchedStar ? subst.replace("*", matchedStar) : subst; - const candidate = normalizePath(combinePaths(baseDirectory, path)); - if (state.traceEnabled) { - trace(state.host, Diagnostics.Trying_substitution_0_candidate_module_location_Colon_1, subst, path); - } - // A path mapping may have an extension, in contrast to an import, which should omit it. - const extension = tryGetExtensionFromPath(candidate); - if (extension !== undefined) { - const path = tryFile(candidate, onlyRecordFailures, state); - if (path !== undefined) { - return noPackageId({ path, ext: extension }); - } + trace(state.host, Diagnostics.Trying_substitution_0_candidate_module_location_Colon_1, subst, path); + } + // A path mapping may have an extension, in contrast to an import, which should omit it. + const extension = tryGetExtensionFromPath(candidate); + if (extension !== undefined) { + const path = tryFile(candidate, onlyRecordFailures, state); + if (path !== undefined) { + return noPackageId({ path, ext: extension }); } - return loader(extensions, candidate, onlyRecordFailures || !directoryProbablyExists(getDirectoryPath(candidate), state.host), state); - }); - return { value: resolved }; - } - } - - /** Double underscores are used in DefinitelyTyped to delimit scoped packages. */ - const mangledScopedPackageSeparator = "__"; - - /** For a scoped package, we must look in `@types/foo__bar` instead of `@types/@foo/bar`. */ - function mangleScopedPackageNameWithTrace(packageName: string, state: ModuleResolutionState): string { - const mangled = mangleScopedPackageName(packageName); - if (state.traceEnabled && mangled !== packageName) { - trace(state.host, Diagnostics.Scoped_package_detected_looking_in_0, mangled); - } - return mangled; - } - - /* @internal */ - export function getTypesPackageName(packageName: string): string { - return `@types/${mangleScopedPackageName(packageName)}`; - } - - /* @internal */ - export function mangleScopedPackageName(packageName: string): string { - if (startsWith(packageName, "@")) { - const replaceSlash = packageName.replace(directorySeparator, mangledScopedPackageSeparator); - if (replaceSlash !== packageName) { - return replaceSlash.slice(1); // Take off the "@" } - } - return packageName; + return loader(extensions, candidate, onlyRecordFailures || !directoryProbablyExists(getDirectoryPath(candidate), state.host), state); + }); + return { value: resolved }; } - - /* @internal */ - export function getPackageNameFromTypesPackageName(mangledName: string): string { - const withoutAtTypePrefix = removePrefix(mangledName, "@types/"); - if (withoutAtTypePrefix !== mangledName) { - return unmangleScopedPackageName(withoutAtTypePrefix); +} +/** Double underscores are used in DefinitelyTyped to delimit scoped packages. */ +const mangledScopedPackageSeparator = "__"; +/** For a scoped package, we must look in `@types/foo__bar` instead of `@types/@foo/bar`. */ +function mangleScopedPackageNameWithTrace(packageName: string, state: ModuleResolutionState): string { + const mangled = mangleScopedPackageName(packageName); + if (state.traceEnabled && mangled !== packageName) { + trace(state.host, Diagnostics.Scoped_package_detected_looking_in_0, mangled); + } + return mangled; +} +/* @internal */ +export function getTypesPackageName(packageName: string): string { + return `@types/${mangleScopedPackageName(packageName)}`; +} +/* @internal */ +export function mangleScopedPackageName(packageName: string): string { + if (startsWith(packageName, "@")) { + const replaceSlash = packageName.replace(directorySeparator, mangledScopedPackageSeparator); + if (replaceSlash !== packageName) { + return replaceSlash.slice(1); // Take off the "@" } - return mangledName; } - - /* @internal */ - export function unmangleScopedPackageName(typesPackageName: string): string { - return stringContains(typesPackageName, mangledScopedPackageSeparator) ? - "@" + typesPackageName.replace(mangledScopedPackageSeparator, directorySeparator) : - typesPackageName; + return packageName; +} +/* @internal */ +export function getPackageNameFromTypesPackageName(mangledName: string): string { + const withoutAtTypePrefix = removePrefix(mangledName, "@types/"); + if (withoutAtTypePrefix !== mangledName) { + return unmangleScopedPackageName(withoutAtTypePrefix); } - - function tryFindNonRelativeModuleNameInCache(cache: PerModuleNameCache | undefined, moduleName: string, containingDirectory: string, state: ModuleResolutionState): SearchResult { - const result = cache && cache.get(containingDirectory); - if (result) { - if (state.traceEnabled) { - trace(state.host, Diagnostics.Resolution_for_module_0_was_found_in_cache_from_location_1, moduleName, containingDirectory); - } - state.failedLookupLocations.push(...result.failedLookupLocations); - return { value: result.resolvedModule && { path: result.resolvedModule.resolvedFileName, originalPath: result.resolvedModule.originalPath || true, extension: result.resolvedModule.extension, packageId: result.resolvedModule.packageId } }; - } - } - - export function classicNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: NonRelativeModuleNameResolutionCache, redirectedReference?: ResolvedProjectReference): ResolvedModuleWithFailedLookupLocations { - const traceEnabled = isTraceEnabled(compilerOptions, host); - const failedLookupLocations: string[] = []; - const state: ModuleResolutionState = { compilerOptions, host, traceEnabled, failedLookupLocations }; - const containingDirectory = getDirectoryPath(containingFile); - - const resolved = tryResolve(Extensions.TypeScript) || tryResolve(Extensions.JavaScript); - // No originalPath because classic resolution doesn't resolve realPath - return createResolvedModuleWithFailedLookupLocations(resolved && resolved.value, /*isExternalLibraryImport*/ false, failedLookupLocations); - - function tryResolve(extensions: Extensions): SearchResult { - const resolvedUsingSettings = tryLoadModuleUsingOptionalResolutionSettings(extensions, moduleName, containingDirectory, loadModuleFromFileNoPackageId, state); - if (resolvedUsingSettings) { - return { value: resolvedUsingSettings }; - } - - if (!isExternalModuleNameRelative(moduleName)) { - const perModuleNameCache = cache && cache.getOrCreateCacheForModuleName(moduleName, redirectedReference); - // Climb up parent directories looking for a module. - const resolved = forEachAncestorDirectory(containingDirectory, directory => { - const resolutionFromCache = tryFindNonRelativeModuleNameInCache(perModuleNameCache, moduleName, directory, state); - if (resolutionFromCache) { - return resolutionFromCache; - } - const searchName = normalizePath(combinePaths(directory, moduleName)); - return toSearchResult(loadModuleFromFileNoPackageId(extensions, searchName, /*onlyRecordFailures*/ false, state)); - }); - if (resolved) { - return resolved; - } - if (extensions === Extensions.TypeScript) { - // If we didn't find the file normally, look it up in @types. - return loadModuleFromNearestNodeModulesDirectoryTypesScope(moduleName, containingDirectory, state); + return mangledName; +} +/* @internal */ +export function unmangleScopedPackageName(typesPackageName: string): string { + return stringContains(typesPackageName, mangledScopedPackageSeparator) ? + "@" + typesPackageName.replace(mangledScopedPackageSeparator, directorySeparator) : + typesPackageName; +} +function tryFindNonRelativeModuleNameInCache(cache: PerModuleNameCache | undefined, moduleName: string, containingDirectory: string, state: ModuleResolutionState): SearchResult { + const result = cache && cache.get(containingDirectory); + if (result) { + if (state.traceEnabled) { + trace(state.host, Diagnostics.Resolution_for_module_0_was_found_in_cache_from_location_1, moduleName, containingDirectory); + } + state.failedLookupLocations.push(...result.failedLookupLocations); + return { value: result.resolvedModule && { path: result.resolvedModule.resolvedFileName, originalPath: result.resolvedModule.originalPath || true, extension: result.resolvedModule.extension, packageId: result.resolvedModule.packageId } }; + } +} +export function classicNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: NonRelativeModuleNameResolutionCache, redirectedReference?: ResolvedProjectReference): ResolvedModuleWithFailedLookupLocations { + const traceEnabled = isTraceEnabled(compilerOptions, host); + const failedLookupLocations: string[] = []; + const state: ModuleResolutionState = { compilerOptions, host, traceEnabled, failedLookupLocations }; + const containingDirectory = getDirectoryPath(containingFile); + const resolved = tryResolve(Extensions.TypeScript) || tryResolve(Extensions.JavaScript); + // No originalPath because classic resolution doesn't resolve realPath + return createResolvedModuleWithFailedLookupLocations(resolved && resolved.value, /*isExternalLibraryImport*/ false, failedLookupLocations); + function tryResolve(extensions: Extensions): SearchResult { + const resolvedUsingSettings = tryLoadModuleUsingOptionalResolutionSettings(extensions, moduleName, containingDirectory, loadModuleFromFileNoPackageId, state); + if (resolvedUsingSettings) { + return { value: resolvedUsingSettings }; + } + if (!isExternalModuleNameRelative(moduleName)) { + const perModuleNameCache = cache && cache.getOrCreateCacheForModuleName(moduleName, redirectedReference); + // Climb up parent directories looking for a module. + const resolved = forEachAncestorDirectory(containingDirectory, directory => { + const resolutionFromCache = tryFindNonRelativeModuleNameInCache(perModuleNameCache, moduleName, directory, state); + if (resolutionFromCache) { + return resolutionFromCache; } + const searchName = normalizePath(combinePaths(directory, moduleName)); + return toSearchResult(loadModuleFromFileNoPackageId(extensions, searchName, /*onlyRecordFailures*/ false, state)); + }); + if (resolved) { + return resolved; } - else { - const candidate = normalizePath(combinePaths(containingDirectory, moduleName)); - return toSearchResult(loadModuleFromFileNoPackageId(extensions, candidate, /*onlyRecordFailures*/ false, state)); + if (extensions === Extensions.TypeScript) { + // If we didn't find the file normally, look it up in @types. + return loadModuleFromNearestNodeModulesDirectoryTypesScope(moduleName, containingDirectory, state); } } - } - - /** - * A host may load a module from a global cache of typings. - * This is the minumum code needed to expose that functionality; the rest is in the host. - */ - /* @internal */ - export function loadModuleFromGlobalCache(moduleName: string, projectName: string | undefined, compilerOptions: CompilerOptions, host: ModuleResolutionHost, globalCache: string): ResolvedModuleWithFailedLookupLocations { - const traceEnabled = isTraceEnabled(compilerOptions, host); - if (traceEnabled) { - trace(host, Diagnostics.Auto_discovery_for_typings_is_enabled_in_project_0_Running_extra_resolution_pass_for_module_1_using_cache_location_2, projectName, moduleName, globalCache); + else { + const candidate = normalizePath(combinePaths(containingDirectory, moduleName)); + return toSearchResult(loadModuleFromFileNoPackageId(extensions, candidate, /*onlyRecordFailures*/ false, state)); } - const failedLookupLocations: string[] = []; - const state: ModuleResolutionState = { compilerOptions, host, traceEnabled, failedLookupLocations }; - const resolved = loadModuleFromImmediateNodeModulesDirectory(Extensions.DtsOnly, moduleName, globalCache, state, /*typesScopeOnly*/ false); - return createResolvedModuleWithFailedLookupLocations(resolved, /*isExternalLibraryImport*/ true, failedLookupLocations); - } - - /** - * Represents result of search. Normally when searching among several alternatives we treat value `undefined` as indicator - * that search fails and we should try another option. - * However this does not allow us to represent final result that should be used instead of further searching (i.e. a final result that was found in cache). - * SearchResult is used to deal with this issue, its values represents following outcomes: - * - undefined - not found, continue searching - * - { value: undefined } - not found - stop searching - * - { value: } - found - stop searching - */ - type SearchResult = { value: T | undefined } | undefined; - - /** - * Wraps value to SearchResult. - * @returns undefined if value is undefined or { value } otherwise - */ - function toSearchResult(value: T | undefined): SearchResult { - return value !== undefined ? { value } : undefined; } } +/** + * A host may load a module from a global cache of typings. + * This is the minumum code needed to expose that functionality; the rest is in the host. + */ +/* @internal */ +export function loadModuleFromGlobalCache(moduleName: string, projectName: string | undefined, compilerOptions: CompilerOptions, host: ModuleResolutionHost, globalCache: string): ResolvedModuleWithFailedLookupLocations { + const traceEnabled = isTraceEnabled(compilerOptions, host); + if (traceEnabled) { + trace(host, Diagnostics.Auto_discovery_for_typings_is_enabled_in_project_0_Running_extra_resolution_pass_for_module_1_using_cache_location_2, projectName, moduleName, globalCache); + } + const failedLookupLocations: string[] = []; + const state: ModuleResolutionState = { compilerOptions, host, traceEnabled, failedLookupLocations }; + const resolved = loadModuleFromImmediateNodeModulesDirectory(Extensions.DtsOnly, moduleName, globalCache, state, /*typesScopeOnly*/ false); + return createResolvedModuleWithFailedLookupLocations(resolved, /*isExternalLibraryImport*/ true, failedLookupLocations); +} +/** + * Represents result of search. Normally when searching among several alternatives we treat value `undefined` as indicator + * that search fails and we should try another option. + * However this does not allow us to represent final result that should be used instead of further searching (i.e. a final result that was found in cache). + * SearchResult is used to deal with this issue, its values represents following outcomes: + * - undefined - not found, continue searching + * - { value: undefined } - not found - stop searching + * - { value: } - found - stop searching + */ +type SearchResult = { + value: T | undefined; +} | undefined; +/** + * Wraps value to SearchResult. + * @returns undefined if value is undefined or { value } otherwise + */ +function toSearchResult(value: T | undefined): SearchResult { + return value !== undefined ? { value } : undefined; +} diff --git a/src/compiler/moduleSpecifiers.ts b/src/compiler/moduleSpecifiers.ts index 57d167c3da111..26e5323931682 100644 --- a/src/compiler/moduleSpecifiers.ts +++ b/src/compiler/moduleSpecifiers.ts @@ -1,491 +1,433 @@ +import { UserPreferences, CompilerOptions, SourceFile, getEmitModuleResolutionKind, ModuleResolutionKind, isExternalModuleNameRelative, hasJSFileExtension, endsWith, Path, ModuleSpecifierResolutionHost, RedirectTargetsMap, firstDefined, Symbol, getSourceFileOfNode, getNonAugmentationDeclaration, mapDefined, GetCanonicalFileName, createGetCanonicalFileName, getDirectoryPath, ensurePathIsNonModuleName, getRelativePathFromDirectory, removeFileExtension, Debug, startsWith, CharacterCodes, pathIsRelative, compareValues, getNormalizedAbsolutePath, discoverProbableSymlinks, compareStringsCaseSensitive, compareStringsCaseInsensitive, startsWithDirectory, find, Comparison, resolvePath, arrayToMap, identity, toPath, ensureTrailingDirectorySeparator, arrayFrom, isNonGlobalAmbientModule, isExternalModuleAugmentation, getTextOfIdentifierOrLiteral, ModuleDeclaration, StringLiteral, MapLike, normalizePath, removeTrailingDirectorySeparator, combinePaths, getPackageJsonTypesVersionsPaths, getPackageNameFromTypesPackageName, isString, getSupportedExtensions, ScriptKind, nodeModulesPathPart, fileExtensionIs, Extension, removeSuffix, extensionFromPath, JsxEmit, getRelativePathToDirectoryOrUrl, isRootedDiskPath } from "./ts"; // Used by importFixes, getEditsForFileRename, and declaration emit to synthesize import module specifiers. /* @internal */ -namespace ts.moduleSpecifiers { - const enum RelativePreference { Relative, NonRelative, Auto } - // See UserPreferences#importPathEnding - const enum Ending { Minimal, Index, JsExtension } - - // Processed preferences - interface Preferences { - readonly relativePreference: RelativePreference; - readonly ending: Ending; - } - - function getPreferences({ importModuleSpecifierPreference, importModuleSpecifierEnding }: UserPreferences, compilerOptions: CompilerOptions, importingSourceFile: SourceFile): Preferences { - return { - relativePreference: importModuleSpecifierPreference === "relative" ? RelativePreference.Relative : importModuleSpecifierPreference === "non-relative" ? RelativePreference.NonRelative : RelativePreference.Auto, - ending: getEnding(), - }; - function getEnding(): Ending { - switch (importModuleSpecifierEnding) { - case "minimal": return Ending.Minimal; - case "index": return Ending.Index; - case "js": return Ending.JsExtension; - default: return usesJsExtensionOnImports(importingSourceFile) ? Ending.JsExtension - : getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeJs ? Ending.Index : Ending.Minimal; - } +const enum RelativePreference { + Relative, + NonRelative, + Auto +} +// See UserPreferences#importPathEnding +/* @internal */ +const enum Ending { + Minimal, + Index, + JsExtension +} +// Processed preferences +/* @internal */ +interface Preferences { + readonly relativePreference: RelativePreference; + readonly ending: Ending; +} +/* @internal */ +function getPreferences({ importModuleSpecifierPreference, importModuleSpecifierEnding }: UserPreferences, compilerOptions: CompilerOptions, importingSourceFile: SourceFile): Preferences { + return { + relativePreference: importModuleSpecifierPreference === "relative" ? RelativePreference.Relative : importModuleSpecifierPreference === "non-relative" ? RelativePreference.NonRelative : RelativePreference.Auto, + ending: getEnding(), + }; + function getEnding(): Ending { + switch (importModuleSpecifierEnding) { + case "minimal": return Ending.Minimal; + case "index": return Ending.Index; + case "js": return Ending.JsExtension; + default: return usesJsExtensionOnImports(importingSourceFile) ? Ending.JsExtension + : getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeJs ? Ending.Index : Ending.Minimal; } } - - function getPreferencesForUpdate(compilerOptions: CompilerOptions, oldImportSpecifier: string): Preferences { - return { - relativePreference: isExternalModuleNameRelative(oldImportSpecifier) ? RelativePreference.Relative : RelativePreference.NonRelative, - ending: hasJSFileExtension(oldImportSpecifier) ? - Ending.JsExtension : - getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeJs || endsWith(oldImportSpecifier, "index") ? Ending.Index : Ending.Minimal, - }; - } - - export function updateModuleSpecifier( - compilerOptions: CompilerOptions, - importingSourceFileName: Path, - toFileName: string, - host: ModuleSpecifierResolutionHost, - files: readonly SourceFile[], - redirectTargetsMap: RedirectTargetsMap, - oldImportSpecifier: string, - ): string | undefined { - const res = getModuleSpecifierWorker(compilerOptions, importingSourceFileName, toFileName, host, files, redirectTargetsMap, getPreferencesForUpdate(compilerOptions, oldImportSpecifier)); - if (res === oldImportSpecifier) return undefined; - return res; - } - - // Note: importingSourceFile is just for usesJsExtensionOnImports - export function getModuleSpecifier( - compilerOptions: CompilerOptions, - importingSourceFile: SourceFile, - importingSourceFileName: Path, - toFileName: string, - host: ModuleSpecifierResolutionHost, - files: readonly SourceFile[], - preferences: UserPreferences = {}, - redirectTargetsMap: RedirectTargetsMap, - ): string { - return getModuleSpecifierWorker(compilerOptions, importingSourceFileName, toFileName, host, files, redirectTargetsMap, getPreferences(preferences, compilerOptions, importingSourceFile)); - } - - export function getNodeModulesPackageName( - compilerOptions: CompilerOptions, - importingSourceFileName: Path, - nodeModulesFileName: string, - host: ModuleSpecifierResolutionHost, - files: readonly SourceFile[], - redirectTargetsMap: RedirectTargetsMap, - ): string | undefined { - const info = getInfo(importingSourceFileName, host); - const modulePaths = getAllModulePaths(files, importingSourceFileName, nodeModulesFileName, info.getCanonicalFileName, host, redirectTargetsMap); - return firstDefined(modulePaths, - moduleFileName => tryGetModuleNameAsNodeModule(moduleFileName, info, host, compilerOptions, /*packageNameOnly*/ true)); - } - - function getModuleSpecifierWorker( - compilerOptions: CompilerOptions, - importingSourceFileName: Path, - toFileName: string, - host: ModuleSpecifierResolutionHost, - files: readonly SourceFile[], - redirectTargetsMap: RedirectTargetsMap, - preferences: Preferences - ): string { - const info = getInfo(importingSourceFileName, host); - const modulePaths = getAllModulePaths(files, importingSourceFileName, toFileName, info.getCanonicalFileName, host, redirectTargetsMap); - return firstDefined(modulePaths, moduleFileName => tryGetModuleNameAsNodeModule(moduleFileName, info, host, compilerOptions)) || - getLocalModuleSpecifier(toFileName, info, compilerOptions, preferences); +} +/* @internal */ +function getPreferencesForUpdate(compilerOptions: CompilerOptions, oldImportSpecifier: string): Preferences { + return { + relativePreference: isExternalModuleNameRelative(oldImportSpecifier) ? RelativePreference.Relative : RelativePreference.NonRelative, + ending: hasJSFileExtension(oldImportSpecifier) ? + Ending.JsExtension : + getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeJs || endsWith(oldImportSpecifier, "index") ? Ending.Index : Ending.Minimal, + }; +} +/* @internal */ +export function updateModuleSpecifier(compilerOptions: CompilerOptions, importingSourceFileName: Path, toFileName: string, host: ModuleSpecifierResolutionHost, files: readonly SourceFile[], redirectTargetsMap: RedirectTargetsMap, oldImportSpecifier: string): string | undefined { + const res = getModuleSpecifierWorker(compilerOptions, importingSourceFileName, toFileName, host, files, redirectTargetsMap, getPreferencesForUpdate(compilerOptions, oldImportSpecifier)); + if (res === oldImportSpecifier) + return undefined; + return res; +} +// Note: importingSourceFile is just for usesJsExtensionOnImports +/* @internal */ +export function getModuleSpecifier(compilerOptions: CompilerOptions, importingSourceFile: SourceFile, importingSourceFileName: Path, toFileName: string, host: ModuleSpecifierResolutionHost, files: readonly SourceFile[], preferences: UserPreferences = {}, redirectTargetsMap: RedirectTargetsMap): string { + return getModuleSpecifierWorker(compilerOptions, importingSourceFileName, toFileName, host, files, redirectTargetsMap, getPreferences(preferences, compilerOptions, importingSourceFile)); +} +/* @internal */ +export function getNodeModulesPackageName(compilerOptions: CompilerOptions, importingSourceFileName: Path, nodeModulesFileName: string, host: ModuleSpecifierResolutionHost, files: readonly SourceFile[], redirectTargetsMap: RedirectTargetsMap): string | undefined { + const info = getInfo(importingSourceFileName, host); + const modulePaths = getAllModulePaths(files, importingSourceFileName, nodeModulesFileName, info.getCanonicalFileName, host, redirectTargetsMap); + return firstDefined(modulePaths, moduleFileName => tryGetModuleNameAsNodeModule(moduleFileName, info, host, compilerOptions, /*packageNameOnly*/ true)); +} +/* @internal */ +function getModuleSpecifierWorker(compilerOptions: CompilerOptions, importingSourceFileName: Path, toFileName: string, host: ModuleSpecifierResolutionHost, files: readonly SourceFile[], redirectTargetsMap: RedirectTargetsMap, preferences: Preferences): string { + const info = getInfo(importingSourceFileName, host); + const modulePaths = getAllModulePaths(files, importingSourceFileName, toFileName, info.getCanonicalFileName, host, redirectTargetsMap); + return firstDefined(modulePaths, moduleFileName => tryGetModuleNameAsNodeModule(moduleFileName, info, host, compilerOptions)) || + getLocalModuleSpecifier(toFileName, info, compilerOptions, preferences); +} +/** Returns an import for each symlink and for the realpath. */ +/* @internal */ +export function getModuleSpecifiers(moduleSymbol: Symbol, compilerOptions: CompilerOptions, importingSourceFile: SourceFile, host: ModuleSpecifierResolutionHost, files: readonly SourceFile[], userPreferences: UserPreferences, redirectTargetsMap: RedirectTargetsMap): readonly string[] { + const ambient = tryGetModuleNameFromAmbientModule(moduleSymbol); + if (ambient) + return [ambient]; + const info = getInfo(importingSourceFile.path, host); + const moduleSourceFile = getSourceFileOfNode(moduleSymbol.valueDeclaration || getNonAugmentationDeclaration(moduleSymbol)); + const modulePaths = getAllModulePaths(files, importingSourceFile.path, moduleSourceFile.originalFileName, info.getCanonicalFileName, host, redirectTargetsMap); + const preferences = getPreferences(userPreferences, compilerOptions, importingSourceFile); + const global = mapDefined(modulePaths, moduleFileName => tryGetModuleNameAsNodeModule(moduleFileName, info, host, compilerOptions)); + return global.length ? global : modulePaths.map(moduleFileName => getLocalModuleSpecifier(moduleFileName, info, compilerOptions, preferences)); +} +/* @internal */ +interface Info { + readonly getCanonicalFileName: GetCanonicalFileName; + readonly sourceDirectory: Path; +} +// importingSourceFileName is separate because getEditsForFileRename may need to specify an updated path +/* @internal */ +function getInfo(importingSourceFileName: Path, host: ModuleSpecifierResolutionHost): Info { + const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames ? host.useCaseSensitiveFileNames() : true); + const sourceDirectory = getDirectoryPath(importingSourceFileName); + return { getCanonicalFileName, sourceDirectory }; +} +/* @internal */ +function getLocalModuleSpecifier(moduleFileName: string, { getCanonicalFileName, sourceDirectory }: Info, compilerOptions: CompilerOptions, { ending, relativePreference }: Preferences): string { + const { baseUrl, paths, rootDirs } = compilerOptions; + const relativePath = rootDirs && tryGetModuleNameFromRootDirs(rootDirs, moduleFileName, sourceDirectory, getCanonicalFileName, ending, compilerOptions) || + removeExtensionAndIndexPostFix(ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, moduleFileName, getCanonicalFileName)), ending, compilerOptions); + if (!baseUrl || relativePreference === RelativePreference.Relative) { + return relativePath; } - - /** Returns an import for each symlink and for the realpath. */ - export function getModuleSpecifiers( - moduleSymbol: Symbol, - compilerOptions: CompilerOptions, - importingSourceFile: SourceFile, - host: ModuleSpecifierResolutionHost, - files: readonly SourceFile[], - userPreferences: UserPreferences, - redirectTargetsMap: RedirectTargetsMap, - ): readonly string[] { - const ambient = tryGetModuleNameFromAmbientModule(moduleSymbol); - if (ambient) return [ambient]; - - const info = getInfo(importingSourceFile.path, host); - const moduleSourceFile = getSourceFileOfNode(moduleSymbol.valueDeclaration || getNonAugmentationDeclaration(moduleSymbol)); - const modulePaths = getAllModulePaths(files, importingSourceFile.path, moduleSourceFile.originalFileName, info.getCanonicalFileName, host, redirectTargetsMap); - - const preferences = getPreferences(userPreferences, compilerOptions, importingSourceFile); - const global = mapDefined(modulePaths, moduleFileName => tryGetModuleNameAsNodeModule(moduleFileName, info, host, compilerOptions)); - return global.length ? global : modulePaths.map(moduleFileName => getLocalModuleSpecifier(moduleFileName, info, compilerOptions, preferences)); + const relativeToBaseUrl = getRelativePathIfInDirectory(moduleFileName, baseUrl, getCanonicalFileName); + if (!relativeToBaseUrl) { + return relativePath; } - - interface Info { - readonly getCanonicalFileName: GetCanonicalFileName; - readonly sourceDirectory: Path; + const importRelativeToBaseUrl = removeExtensionAndIndexPostFix(relativeToBaseUrl, ending, compilerOptions); + const fromPaths = paths && tryGetModuleNameFromPaths(removeFileExtension(relativeToBaseUrl), importRelativeToBaseUrl, paths); + const nonRelative = fromPaths === undefined ? importRelativeToBaseUrl : fromPaths; + if (relativePreference === RelativePreference.NonRelative) { + return nonRelative; } - // importingSourceFileName is separate because getEditsForFileRename may need to specify an updated path - function getInfo(importingSourceFileName: Path, host: ModuleSpecifierResolutionHost): Info { - const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames ? host.useCaseSensitiveFileNames() : true); - const sourceDirectory = getDirectoryPath(importingSourceFileName); - return { getCanonicalFileName, sourceDirectory }; + if (relativePreference !== RelativePreference.Auto) + Debug.assertNever(relativePreference); + // Prefer a relative import over a baseUrl import if it has fewer components. + return isPathRelativeToParent(nonRelative) || countPathComponents(relativePath) < countPathComponents(nonRelative) ? relativePath : nonRelative; +} +/* @internal */ +export function countPathComponents(path: string): number { + let count = 0; + for (let i = startsWith(path, "./") ? 2 : 0; i < path.length; i++) { + if (path.charCodeAt(i) === CharacterCodes.slash) + count++; } - - function getLocalModuleSpecifier(moduleFileName: string, { getCanonicalFileName, sourceDirectory }: Info, compilerOptions: CompilerOptions, { ending, relativePreference }: Preferences): string { - const { baseUrl, paths, rootDirs } = compilerOptions; - - const relativePath = rootDirs && tryGetModuleNameFromRootDirs(rootDirs, moduleFileName, sourceDirectory, getCanonicalFileName, ending, compilerOptions) || - removeExtensionAndIndexPostFix(ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, moduleFileName, getCanonicalFileName)), ending, compilerOptions); - if (!baseUrl || relativePreference === RelativePreference.Relative) { - return relativePath; - } - - const relativeToBaseUrl = getRelativePathIfInDirectory(moduleFileName, baseUrl, getCanonicalFileName); - if (!relativeToBaseUrl) { - return relativePath; - } - - const importRelativeToBaseUrl = removeExtensionAndIndexPostFix(relativeToBaseUrl, ending, compilerOptions); - const fromPaths = paths && tryGetModuleNameFromPaths(removeFileExtension(relativeToBaseUrl), importRelativeToBaseUrl, paths); - const nonRelative = fromPaths === undefined ? importRelativeToBaseUrl : fromPaths; - - if (relativePreference === RelativePreference.NonRelative) { - return nonRelative; + return count; +} +/* @internal */ +function usesJsExtensionOnImports({ imports }: SourceFile): boolean { + return firstDefined(imports, ({ text }) => pathIsRelative(text) ? hasJSFileExtension(text) : undefined) || false; +} +/* @internal */ +function numberOfDirectorySeparators(str: string) { + const match = str.match(/\//g); + return match ? match.length : 0; +} +/* @internal */ +function comparePathsByNumberOfDirectorySeparators(a: string, b: string) { + return compareValues(numberOfDirectorySeparators(a), numberOfDirectorySeparators(b)); +} +/** + * Looks for existing imports that use symlinks to this module. + * Symlinks will be returned first so they are preferred over the real path. + */ +/* @internal */ +function getAllModulePaths(files: readonly SourceFile[], importingFileName: string, importedFileName: string, getCanonicalFileName: GetCanonicalFileName, host: ModuleSpecifierResolutionHost, redirectTargetsMap: RedirectTargetsMap): readonly string[] { + const redirects = redirectTargetsMap.get(importedFileName); + const importedFileNames = redirects ? [...redirects, importedFileName] : [importedFileName]; + const cwd = host.getCurrentDirectory ? host.getCurrentDirectory() : ""; + const targets = importedFileNames.map(f => getNormalizedAbsolutePath(f, cwd)); + const links = host.getProbableSymlinks + ? host.getProbableSymlinks(files) + : discoverProbableSymlinks(files, getCanonicalFileName, cwd); + const result: string[] = []; + const compareStrings = (!host.useCaseSensitiveFileNames || host.useCaseSensitiveFileNames()) ? compareStringsCaseSensitive : compareStringsCaseInsensitive; + links.forEach((resolved, path) => { + if (startsWithDirectory(importingFileName, resolved, getCanonicalFileName)) { + return; // Don't want to a package to globally import from itself } - - if (relativePreference !== RelativePreference.Auto) Debug.assertNever(relativePreference); - - // Prefer a relative import over a baseUrl import if it has fewer components. - return isPathRelativeToParent(nonRelative) || countPathComponents(relativePath) < countPathComponents(nonRelative) ? relativePath : nonRelative; - } - - export function countPathComponents(path: string): number { - let count = 0; - for (let i = startsWith(path, "./") ? 2 : 0; i < path.length; i++) { - if (path.charCodeAt(i) === CharacterCodes.slash) count++; + const target = find(targets, t => compareStrings(t.slice(0, resolved.length + 1), resolved + "/") === Comparison.EqualTo); + if (target === undefined) + return; + const relative = getRelativePathFromDirectory(resolved, target, getCanonicalFileName); + const option = resolvePath(path, relative); + if (!host.fileExists || host.fileExists(option)) { + result.push(option); } - return count; - } - - function usesJsExtensionOnImports({ imports }: SourceFile): boolean { - return firstDefined(imports, ({ text }) => pathIsRelative(text) ? hasJSFileExtension(text) : undefined) || false; - } - - function numberOfDirectorySeparators(str: string) { - const match = str.match(/\//g); - return match ? match.length : 0; - } - - function comparePathsByNumberOfDirectorySeparators(a: string, b: string) { - return compareValues( - numberOfDirectorySeparators(a), - numberOfDirectorySeparators(b) - ); - } - - /** - * Looks for existing imports that use symlinks to this module. - * Symlinks will be returned first so they are preferred over the real path. - */ - function getAllModulePaths(files: readonly SourceFile[], importingFileName: string, importedFileName: string, getCanonicalFileName: GetCanonicalFileName, host: ModuleSpecifierResolutionHost, redirectTargetsMap: RedirectTargetsMap): readonly string[] { - const redirects = redirectTargetsMap.get(importedFileName); - const importedFileNames = redirects ? [...redirects, importedFileName] : [importedFileName]; - const cwd = host.getCurrentDirectory ? host.getCurrentDirectory() : ""; - const targets = importedFileNames.map(f => getNormalizedAbsolutePath(f, cwd)); - const links = host.getProbableSymlinks - ? host.getProbableSymlinks(files) - : discoverProbableSymlinks(files, getCanonicalFileName, cwd); - - const result: string[] = []; - const compareStrings = (!host.useCaseSensitiveFileNames || host.useCaseSensitiveFileNames()) ? compareStringsCaseSensitive : compareStringsCaseInsensitive; - links.forEach((resolved, path) => { - if (startsWithDirectory(importingFileName, resolved, getCanonicalFileName)) { - return; // Don't want to a package to globally import from itself - } - - const target = find(targets, t => compareStrings(t.slice(0, resolved.length + 1), resolved + "/") === Comparison.EqualTo); - if (target === undefined) return; - - const relative = getRelativePathFromDirectory(resolved, target, getCanonicalFileName); - const option = resolvePath(path, relative); - if (!host.fileExists || host.fileExists(option)) { - result.push(option); + }); + result.push(...targets); + if (result.length < 2) + return result; + // Sort by paths closest to importing file Name directory + const allFileNames = arrayToMap(result, identity, getCanonicalFileName); + const sortedPaths: string[] = []; + for (let directory = getDirectoryPath(toPath(importingFileName, cwd, getCanonicalFileName)); allFileNames.size !== 0;) { + const directoryStart = ensureTrailingDirectorySeparator(directory); + let pathsInDirectory: string[] | undefined; + allFileNames.forEach((canonicalFileName, fileName) => { + if (startsWith(canonicalFileName, directoryStart)) { + (pathsInDirectory || (pathsInDirectory = [])).push(fileName); + allFileNames.delete(fileName); } }); - result.push(...targets); - if (result.length < 2) return result; - - // Sort by paths closest to importing file Name directory - const allFileNames = arrayToMap(result, identity, getCanonicalFileName); - const sortedPaths: string[] = []; - for ( - let directory = getDirectoryPath(toPath(importingFileName, cwd, getCanonicalFileName)); - allFileNames.size !== 0; - ) { - const directoryStart = ensureTrailingDirectorySeparator(directory); - let pathsInDirectory: string[] | undefined; - allFileNames.forEach((canonicalFileName, fileName) => { - if (startsWith(canonicalFileName, directoryStart)) { - (pathsInDirectory || (pathsInDirectory = [])).push(fileName); - allFileNames.delete(fileName); - } - }); - if (pathsInDirectory) { - if (pathsInDirectory.length > 1) { - pathsInDirectory.sort(comparePathsByNumberOfDirectorySeparators); - } - sortedPaths.push(...pathsInDirectory); + if (pathsInDirectory) { + if (pathsInDirectory.length > 1) { + pathsInDirectory.sort(comparePathsByNumberOfDirectorySeparators); } - const newDirectory = getDirectoryPath(directory); - if (newDirectory === directory) break; - directory = newDirectory; - } - if (allFileNames.size) { - const remainingPaths = arrayFrom(allFileNames.values()); - if (remainingPaths.length > 1) remainingPaths.sort(comparePathsByNumberOfDirectorySeparators); - sortedPaths.push(...remainingPaths); + sortedPaths.push(...pathsInDirectory); } - return sortedPaths; + const newDirectory = getDirectoryPath(directory); + if (newDirectory === directory) + break; + directory = newDirectory; } - - function tryGetModuleNameFromAmbientModule(moduleSymbol: Symbol): string | undefined { - const decl = find(moduleSymbol.declarations, - d => isNonGlobalAmbientModule(d) && (!isExternalModuleAugmentation(d) || !isExternalModuleNameRelative(getTextOfIdentifierOrLiteral(d.name))) - ) as (ModuleDeclaration & { name: StringLiteral }) | undefined; - if (decl) { - return decl.name.text; - } + if (allFileNames.size) { + const remainingPaths = arrayFrom(allFileNames.values()); + if (remainingPaths.length > 1) + remainingPaths.sort(comparePathsByNumberOfDirectorySeparators); + sortedPaths.push(...remainingPaths); } - - function tryGetModuleNameFromPaths(relativeToBaseUrlWithIndex: string, relativeToBaseUrl: string, paths: MapLike): string | undefined { - for (const key in paths) { - for (const patternText of paths[key]) { - const pattern = removeFileExtension(normalizePath(patternText)); - const indexOfStar = pattern.indexOf("*"); - if (indexOfStar !== -1) { - const prefix = pattern.substr(0, indexOfStar); - const suffix = pattern.substr(indexOfStar + 1); - if (relativeToBaseUrl.length >= prefix.length + suffix.length && - startsWith(relativeToBaseUrl, prefix) && - endsWith(relativeToBaseUrl, suffix) || - !suffix && relativeToBaseUrl === removeTrailingDirectorySeparator(prefix)) { - const matchedStar = relativeToBaseUrl.substr(prefix.length, relativeToBaseUrl.length - suffix.length); - return key.replace("*", matchedStar); - } - } - else if (pattern === relativeToBaseUrl || pattern === relativeToBaseUrlWithIndex) { - return key; - } - } - } - } - - function tryGetModuleNameFromRootDirs(rootDirs: readonly string[], moduleFileName: string, sourceDirectory: string, getCanonicalFileName: (file: string) => string, ending: Ending, compilerOptions: CompilerOptions): string | undefined { - const normalizedTargetPath = getPathRelativeToRootDirs(moduleFileName, rootDirs, getCanonicalFileName); - if (normalizedTargetPath === undefined) { - return undefined; - } - - const normalizedSourcePath = getPathRelativeToRootDirs(sourceDirectory, rootDirs, getCanonicalFileName); - const relativePath = normalizedSourcePath !== undefined ? ensurePathIsNonModuleName(getRelativePathFromDirectory(normalizedSourcePath, normalizedTargetPath, getCanonicalFileName)) : normalizedTargetPath; - return getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs - ? removeExtensionAndIndexPostFix(relativePath, ending, compilerOptions) - : removeFileExtension(relativePath); + return sortedPaths; +} +/* @internal */ +function tryGetModuleNameFromAmbientModule(moduleSymbol: Symbol): string | undefined { + const decl = (find(moduleSymbol.declarations, d => isNonGlobalAmbientModule(d) && (!isExternalModuleAugmentation(d) || !isExternalModuleNameRelative(getTextOfIdentifierOrLiteral(d.name)))) as (ModuleDeclaration & { + name: StringLiteral; + }) | undefined); + if (decl) { + return decl.name.text; } - - function tryGetModuleNameAsNodeModule(moduleFileName: string, { getCanonicalFileName, sourceDirectory }: Info, host: ModuleSpecifierResolutionHost, options: CompilerOptions, packageNameOnly?: boolean): string | undefined { - if (!host.fileExists || !host.readFile) { - return undefined; - } - const parts: NodeModulePathParts = getNodeModulePathParts(moduleFileName)!; - if (!parts) { - return undefined; - } - - let packageJsonContent: any | undefined; - const packageRootPath = moduleFileName.substring(0, parts.packageRootIndex); - if (!packageNameOnly) { - const packageJsonPath = combinePaths(packageRootPath, "package.json"); - packageJsonContent = host.fileExists(packageJsonPath) - ? JSON.parse(host.readFile(packageJsonPath)!) - : undefined; - const versionPaths = packageJsonContent && packageJsonContent.typesVersions - ? getPackageJsonTypesVersionsPaths(packageJsonContent.typesVersions) - : undefined; - if (versionPaths) { - const subModuleName = moduleFileName.slice(parts.packageRootIndex + 1); - const fromPaths = tryGetModuleNameFromPaths( - removeFileExtension(subModuleName), - removeExtensionAndIndexPostFix(subModuleName, Ending.Minimal, options), - versionPaths.paths - ); - if (fromPaths !== undefined) { - moduleFileName = combinePaths(moduleFileName.slice(0, parts.packageRootIndex), fromPaths); - } - } - } - - // Simplify the full file path to something that can be resolved by Node. - - // If the module could be imported by a directory name, use that directory's name - const moduleSpecifier = packageNameOnly ? moduleFileName : getDirectoryOrExtensionlessFileName(moduleFileName); - const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation && host.getGlobalTypingsCacheLocation(); - // Get a path that's relative to node_modules or the importing file's path - // if node_modules folder is in this folder or any of its parent folders, no need to keep it. - const pathToTopLevelNodeModules = getCanonicalFileName(moduleSpecifier.substring(0, parts.topLevelNodeModulesIndex)); - if (!(startsWith(sourceDirectory, pathToTopLevelNodeModules) || globalTypingsCacheLocation && startsWith(getCanonicalFileName(globalTypingsCacheLocation), pathToTopLevelNodeModules))) { - return undefined; - } - - // If the module was found in @types, get the actual Node package name - const nodeModulesDirectoryName = moduleSpecifier.substring(parts.topLevelPackageNameIndex + 1); - const packageName = getPackageNameFromTypesPackageName(nodeModulesDirectoryName); - // For classic resolution, only allow importing from node_modules/@types, not other node_modules - return getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeJs && packageName === nodeModulesDirectoryName ? undefined : packageName; - - function getDirectoryOrExtensionlessFileName(path: string): string { - // If the file is the main module, it can be imported by the package name - if (packageJsonContent) { - const mainFileRelative = packageJsonContent.typings || packageJsonContent.types || packageJsonContent.main; - if (isString(mainFileRelative)) { - const mainExportFile = toPath(mainFileRelative, packageRootPath, getCanonicalFileName); - if (removeFileExtension(mainExportFile) === removeFileExtension(getCanonicalFileName(path))) { - return packageRootPath; - } +} +/* @internal */ +function tryGetModuleNameFromPaths(relativeToBaseUrlWithIndex: string, relativeToBaseUrl: string, paths: MapLike): string | undefined { + for (const key in paths) { + for (const patternText of paths[key]) { + const pattern = removeFileExtension(normalizePath(patternText)); + const indexOfStar = pattern.indexOf("*"); + if (indexOfStar !== -1) { + const prefix = pattern.substr(0, indexOfStar); + const suffix = pattern.substr(indexOfStar + 1); + if (relativeToBaseUrl.length >= prefix.length + suffix.length && + startsWith(relativeToBaseUrl, prefix) && + endsWith(relativeToBaseUrl, suffix) || + !suffix && relativeToBaseUrl === removeTrailingDirectorySeparator(prefix)) { + const matchedStar = relativeToBaseUrl.substr(prefix.length, relativeToBaseUrl.length - suffix.length); + return key.replace("*", matchedStar); } } - - // We still have a file name - remove the extension - const fullModulePathWithoutExtension = removeFileExtension(path); - - // If the file is /index, it can be imported by its directory name - // IFF there is not _also_ a file by the same name - if (getCanonicalFileName(fullModulePathWithoutExtension.substring(parts.fileNameIndex)) === "/index" && !tryGetAnyFileFromPath(host, fullModulePathWithoutExtension.substring(0, parts.fileNameIndex))) { - return fullModulePathWithoutExtension.substring(0, parts.fileNameIndex); + else if (pattern === relativeToBaseUrl || pattern === relativeToBaseUrlWithIndex) { + return key; } - - return fullModulePathWithoutExtension; } } - - function tryGetAnyFileFromPath(host: ModuleSpecifierResolutionHost, path: string) { - if (!host.fileExists) return; - // We check all js, `node` and `json` extensions in addition to TS, since node module resolution would also choose those over the directory - const extensions = getSupportedExtensions({ allowJs: true }, [{ extension: "node", isMixedContent: false }, { extension: "json", isMixedContent: false, scriptKind: ScriptKind.JSON }]); - for (const e of extensions) { - const fullPath = path + e; - if (host.fileExists(fullPath)) { - return fullPath; +} +/* @internal */ +function tryGetModuleNameFromRootDirs(rootDirs: readonly string[], moduleFileName: string, sourceDirectory: string, getCanonicalFileName: (file: string) => string, ending: Ending, compilerOptions: CompilerOptions): string | undefined { + const normalizedTargetPath = getPathRelativeToRootDirs(moduleFileName, rootDirs, getCanonicalFileName); + if (normalizedTargetPath === undefined) { + return undefined; + } + const normalizedSourcePath = getPathRelativeToRootDirs(sourceDirectory, rootDirs, getCanonicalFileName); + const relativePath = normalizedSourcePath !== undefined ? ensurePathIsNonModuleName(getRelativePathFromDirectory(normalizedSourcePath, normalizedTargetPath, getCanonicalFileName)) : normalizedTargetPath; + return getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs + ? removeExtensionAndIndexPostFix(relativePath, ending, compilerOptions) + : removeFileExtension(relativePath); +} +/* @internal */ +function tryGetModuleNameAsNodeModule(moduleFileName: string, { getCanonicalFileName, sourceDirectory }: Info, host: ModuleSpecifierResolutionHost, options: CompilerOptions, packageNameOnly?: boolean): string | undefined { + if (!host.fileExists || !host.readFile) { + return undefined; + } + const parts: NodeModulePathParts = getNodeModulePathParts(moduleFileName)!; + if (!parts) { + return undefined; + } + let packageJsonContent: any | undefined; + const packageRootPath = moduleFileName.substring(0, parts.packageRootIndex); + if (!packageNameOnly) { + const packageJsonPath = combinePaths(packageRootPath, "package.json"); + packageJsonContent = host.fileExists(packageJsonPath) + ? JSON.parse(host.readFile(packageJsonPath)!) + : undefined; + const versionPaths = packageJsonContent && packageJsonContent.typesVersions + ? getPackageJsonTypesVersionsPaths(packageJsonContent.typesVersions) + : undefined; + if (versionPaths) { + const subModuleName = moduleFileName.slice(parts.packageRootIndex + 1); + const fromPaths = tryGetModuleNameFromPaths(removeFileExtension(subModuleName), removeExtensionAndIndexPostFix(subModuleName, Ending.Minimal, options), versionPaths.paths); + if (fromPaths !== undefined) { + moduleFileName = combinePaths(moduleFileName.slice(0, parts.packageRootIndex), fromPaths); } } } - - interface NodeModulePathParts { - readonly topLevelNodeModulesIndex: number; - readonly topLevelPackageNameIndex: number; - readonly packageRootIndex: number; - readonly fileNameIndex: number; + // Simplify the full file path to something that can be resolved by Node. + // If the module could be imported by a directory name, use that directory's name + const moduleSpecifier = packageNameOnly ? moduleFileName : getDirectoryOrExtensionlessFileName(moduleFileName); + const globalTypingsCacheLocation = host.getGlobalTypingsCacheLocation && host.getGlobalTypingsCacheLocation(); + // Get a path that's relative to node_modules or the importing file's path + // if node_modules folder is in this folder or any of its parent folders, no need to keep it. + const pathToTopLevelNodeModules = getCanonicalFileName(moduleSpecifier.substring(0, parts.topLevelNodeModulesIndex)); + if (!(startsWith(sourceDirectory, pathToTopLevelNodeModules) || globalTypingsCacheLocation && startsWith(getCanonicalFileName(globalTypingsCacheLocation), pathToTopLevelNodeModules))) { + return undefined; } - function getNodeModulePathParts(fullPath: string): NodeModulePathParts | undefined { - // If fullPath can't be valid module file within node_modules, returns undefined. - // Example of expected pattern: /base/path/node_modules/[@scope/otherpackage/@otherscope/node_modules/]package/[subdirectory/]file.js - // Returns indices: ^ ^ ^ ^ - - let topLevelNodeModulesIndex = 0; - let topLevelPackageNameIndex = 0; - let packageRootIndex = 0; - let fileNameIndex = 0; - - const enum States { - BeforeNodeModules, - NodeModules, - Scope, - PackageContent - } - - let partStart = 0; - let partEnd = 0; - let state = States.BeforeNodeModules; - - while (partEnd >= 0) { - partStart = partEnd; - partEnd = fullPath.indexOf("/", partStart + 1); - switch (state) { - case States.BeforeNodeModules: - if (fullPath.indexOf(nodeModulesPathPart, partStart) === partStart) { - topLevelNodeModulesIndex = partStart; - topLevelPackageNameIndex = partEnd; - state = States.NodeModules; - } - break; - case States.NodeModules: - case States.Scope: - if (state === States.NodeModules && fullPath.charAt(partStart + 1) === "@") { - state = States.Scope; - } - else { - packageRootIndex = partEnd; - state = States.PackageContent; - } - break; - case States.PackageContent: - if (fullPath.indexOf(nodeModulesPathPart, partStart) === partStart) { - state = States.NodeModules; - } - else { - state = States.PackageContent; - } - break; + // If the module was found in @types, get the actual Node package name + const nodeModulesDirectoryName = moduleSpecifier.substring(parts.topLevelPackageNameIndex + 1); + const packageName = getPackageNameFromTypesPackageName(nodeModulesDirectoryName); + // For classic resolution, only allow importing from node_modules/@types, not other node_modules + return getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeJs && packageName === nodeModulesDirectoryName ? undefined : packageName; + function getDirectoryOrExtensionlessFileName(path: string): string { + // If the file is the main module, it can be imported by the package name + if (packageJsonContent) { + const mainFileRelative = packageJsonContent.typings || packageJsonContent.types || packageJsonContent.main; + if (isString(mainFileRelative)) { + const mainExportFile = toPath(mainFileRelative, packageRootPath, getCanonicalFileName); + if (removeFileExtension(mainExportFile) === removeFileExtension(getCanonicalFileName(path))) { + return packageRootPath; + } } } - - fileNameIndex = partStart; - - return state > States.NodeModules ? { topLevelNodeModulesIndex, topLevelPackageNameIndex, packageRootIndex, fileNameIndex } : undefined; - } - - function getPathRelativeToRootDirs(path: string, rootDirs: readonly string[], getCanonicalFileName: GetCanonicalFileName): string | undefined { - return firstDefined(rootDirs, rootDir => { - const relativePath = getRelativePathIfInDirectory(path, rootDir, getCanonicalFileName)!; // TODO: GH#18217 - return isPathRelativeToParent(relativePath) ? undefined : relativePath; - }); + // We still have a file name - remove the extension + const fullModulePathWithoutExtension = removeFileExtension(path); + // If the file is /index, it can be imported by its directory name + // IFF there is not _also_ a file by the same name + if (getCanonicalFileName(fullModulePathWithoutExtension.substring(parts.fileNameIndex)) === "/index" && !tryGetAnyFileFromPath(host, fullModulePathWithoutExtension.substring(0, parts.fileNameIndex))) { + return fullModulePathWithoutExtension.substring(0, parts.fileNameIndex); + } + return fullModulePathWithoutExtension; } - - function removeExtensionAndIndexPostFix(fileName: string, ending: Ending, options: CompilerOptions): string { - if (fileExtensionIs(fileName, Extension.Json)) return fileName; - const noExtension = removeFileExtension(fileName); - switch (ending) { - case Ending.Minimal: - return removeSuffix(noExtension, "/index"); - case Ending.Index: - return noExtension; - case Ending.JsExtension: - return noExtension + getJSExtensionForFile(fileName, options); - default: - return Debug.assertNever(ending); +} +/* @internal */ +function tryGetAnyFileFromPath(host: ModuleSpecifierResolutionHost, path: string) { + if (!host.fileExists) + return; + // We check all js, `node` and `json` extensions in addition to TS, since node module resolution would also choose those over the directory + const extensions = getSupportedExtensions({ allowJs: true }, [{ extension: "node", isMixedContent: false }, { extension: "json", isMixedContent: false, scriptKind: ScriptKind.JSON }]); + for (const e of extensions) { + const fullPath = path + e; + if (host.fileExists(fullPath)) { + return fullPath; } } - - function getJSExtensionForFile(fileName: string, options: CompilerOptions): Extension { - const ext = extensionFromPath(fileName); - switch (ext) { - case Extension.Ts: - case Extension.Dts: - return Extension.Js; - case Extension.Tsx: - return options.jsx === JsxEmit.Preserve ? Extension.Jsx : Extension.Js; - case Extension.Js: - case Extension.Jsx: - case Extension.Json: - return ext; - case Extension.TsBuildInfo: - return Debug.fail(`Extension ${Extension.TsBuildInfo} is unsupported:: FileName:: ${fileName}`); - default: - return Debug.assertNever(ext); +} +/* @internal */ +interface NodeModulePathParts { + readonly topLevelNodeModulesIndex: number; + readonly topLevelPackageNameIndex: number; + readonly packageRootIndex: number; + readonly fileNameIndex: number; +} +/* @internal */ +function getNodeModulePathParts(fullPath: string): NodeModulePathParts | undefined { + // If fullPath can't be valid module file within node_modules, returns undefined. + // Example of expected pattern: /base/path/node_modules/[@scope/otherpackage/@otherscope/node_modules/]package/[subdirectory/]file.js + // Returns indices: ^ ^ ^ ^ + let topLevelNodeModulesIndex = 0; + let topLevelPackageNameIndex = 0; + let packageRootIndex = 0; + let fileNameIndex = 0; + const enum States { + BeforeNodeModules, + NodeModules, + Scope, + PackageContent + } + let partStart = 0; + let partEnd = 0; + let state = States.BeforeNodeModules; + while (partEnd >= 0) { + partStart = partEnd; + partEnd = fullPath.indexOf("/", partStart + 1); + switch (state) { + case States.BeforeNodeModules: + if (fullPath.indexOf(nodeModulesPathPart, partStart) === partStart) { + topLevelNodeModulesIndex = partStart; + topLevelPackageNameIndex = partEnd; + state = States.NodeModules; + } + break; + case States.NodeModules: + case States.Scope: + if (state === States.NodeModules && fullPath.charAt(partStart + 1) === "@") { + state = States.Scope; + } + else { + packageRootIndex = partEnd; + state = States.PackageContent; + } + break; + case States.PackageContent: + if (fullPath.indexOf(nodeModulesPathPart, partStart) === partStart) { + state = States.NodeModules; + } + else { + state = States.PackageContent; + } + break; } } - - function getRelativePathIfInDirectory(path: string, directoryPath: string, getCanonicalFileName: GetCanonicalFileName): string | undefined { - const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); - return isRootedDiskPath(relativePath) ? undefined : relativePath; + fileNameIndex = partStart; + return state > States.NodeModules ? { topLevelNodeModulesIndex, topLevelPackageNameIndex, packageRootIndex, fileNameIndex } : undefined; +} +/* @internal */ +function getPathRelativeToRootDirs(path: string, rootDirs: readonly string[], getCanonicalFileName: GetCanonicalFileName): string | undefined { + return firstDefined(rootDirs, rootDir => { + const relativePath = getRelativePathIfInDirectory(path, rootDir, getCanonicalFileName)!; // TODO: GH#18217 + return isPathRelativeToParent(relativePath) ? undefined : relativePath; + }); +} +/* @internal */ +function removeExtensionAndIndexPostFix(fileName: string, ending: Ending, options: CompilerOptions): string { + if (fileExtensionIs(fileName, Extension.Json)) + return fileName; + const noExtension = removeFileExtension(fileName); + switch (ending) { + case Ending.Minimal: + return removeSuffix(noExtension, "/index"); + case Ending.Index: + return noExtension; + case Ending.JsExtension: + return noExtension + getJSExtensionForFile(fileName, options); + default: + return Debug.assertNever(ending); } - - function isPathRelativeToParent(path: string): boolean { - return startsWith(path, ".."); +} +/* @internal */ +function getJSExtensionForFile(fileName: string, options: CompilerOptions): Extension { + const ext = extensionFromPath(fileName); + switch (ext) { + case Extension.Ts: + case Extension.Dts: + return Extension.Js; + case Extension.Tsx: + return options.jsx === JsxEmit.Preserve ? Extension.Jsx : Extension.Js; + case Extension.Js: + case Extension.Jsx: + case Extension.Json: + return ext; + case Extension.TsBuildInfo: + return Debug.fail(`Extension ${Extension.TsBuildInfo} is unsupported:: FileName:: ${fileName}`); + default: + return Debug.assertNever(ext); } } +/* @internal */ +function getRelativePathIfInDirectory(path: string, directoryPath: string, getCanonicalFileName: GetCanonicalFileName): string | undefined { + const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); + return isRootedDiskPath(relativePath) ? undefined : relativePath; +} +/* @internal */ +function isPathRelativeToParent(path: string): boolean { + return startsWith(path, ".."); +} diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 802756971d2e7..9e017a692035d 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -1,4726 +1,4124 @@ -namespace ts { - const enum SignatureFlags { - None = 0, - Yield = 1 << 0, - Await = 1 << 1, - Type = 1 << 2, - IgnoreMissingOpenBrace = 1 << 4, - JSDoc = 1 << 5, - } - - let NodeConstructor: new (kind: SyntaxKind, pos?: number, end?: number) => Node; - let TokenConstructor: new (kind: SyntaxKind, pos?: number, end?: number) => Node; - let IdentifierConstructor: new (kind: SyntaxKind, pos?: number, end?: number) => Node; - let PrivateIdentifierConstructor: new (kind: SyntaxKind, pos?: number, end?: number) => Node; - let SourceFileConstructor: new (kind: SyntaxKind, pos?: number, end?: number) => Node; - - export function createNode(kind: SyntaxKind, pos?: number, end?: number): Node { - if (kind === SyntaxKind.SourceFile) { - return new (SourceFileConstructor || (SourceFileConstructor = objectAllocator.getSourceFileConstructor()))(kind, pos, end); - } - else if (kind === SyntaxKind.Identifier) { - return new (IdentifierConstructor || (IdentifierConstructor = objectAllocator.getIdentifierConstructor()))(kind, pos, end); - } - else if (kind === SyntaxKind.PrivateIdentifier) { - return new (PrivateIdentifierConstructor || (PrivateIdentifierConstructor = objectAllocator.getPrivateIdentifierConstructor()))(kind, pos, end); - } - else if (!isNodeKind(kind)) { - return new (TokenConstructor || (TokenConstructor = objectAllocator.getTokenConstructor()))(kind, pos, end); - } - else { - return new (NodeConstructor || (NodeConstructor = objectAllocator.getNodeConstructor()))(kind, pos, end); - } +import { SyntaxKind, Node, objectAllocator, isNodeKind, NodeArray, CharacterCodes, QualifiedName, TypeParameterDeclaration, ShorthandPropertyAssignment, SpreadAssignment, ParameterDeclaration, PropertyDeclaration, PropertySignature, PropertyAssignment, VariableDeclaration, BindingElement, SignatureDeclaration, FunctionLikeDeclaration, ArrowFunction, TypeReferenceNode, TypePredicateNode, TypeQueryNode, TypeLiteralNode, ArrayTypeNode, TupleTypeNode, UnionOrIntersectionTypeNode, ConditionalTypeNode, InferTypeNode, ImportTypeNode, ParenthesizedTypeNode, TypeOperatorNode, IndexedAccessTypeNode, MappedTypeNode, LiteralTypeNode, BindingPattern, ArrayLiteralExpression, ObjectLiteralExpression, PropertyAccessExpression, ElementAccessExpression, CallExpression, TaggedTemplateExpression, TypeAssertion, ParenthesizedExpression, DeleteExpression, TypeOfExpression, VoidExpression, PrefixUnaryExpression, YieldExpression, AwaitExpression, PostfixUnaryExpression, BinaryExpression, AsExpression, NonNullExpression, MetaProperty, ConditionalExpression, SpreadElement, Block, SourceFile, VariableStatement, VariableDeclarationList, ExpressionStatement, IfStatement, DoStatement, WhileStatement, ForStatement, ForInStatement, ForOfStatement, BreakOrContinueStatement, ReturnStatement, WithStatement, SwitchStatement, CaseBlock, CaseClause, DefaultClause, LabeledStatement, ThrowStatement, TryStatement, CatchClause, Decorator, ClassLikeDeclaration, InterfaceDeclaration, ClassDeclaration, TypeAliasDeclaration, EnumDeclaration, EnumMember, ModuleDeclaration, ImportEqualsDeclaration, ImportDeclaration, ImportClause, NamespaceExportDeclaration, NamespaceImport, NamespaceExport, NamedImportsOrExports, ExportDeclaration, ImportOrExportSpecifier, ExportAssignment, TemplateExpression, TemplateSpan, ComputedPropertyName, HeritageClause, ExpressionWithTypeArguments, ExternalModuleReference, CommaListExpression, JsxElement, JsxFragment, JsxOpeningLikeElement, JsxAttributes, JsxAttribute, JsxSpreadAttribute, JsxExpression, JsxClosingElement, OptionalTypeNode, RestTypeNode, JSDocTypeExpression, JSDocTypeReferencingNode, JSDocFunctionType, JSDoc, JSDocTag, JSDocPropertyLikeTag, JSDocImplementsTag, JSDocAugmentsTag, JSDocTemplateTag, JSDocTypedefTag, JSDocCallbackTag, JSDocReturnTag, JSDocTypeTag, JSDocThisTag, JSDocEnumTag, forEach, JSDocSignature, JSDocTypeLiteral, PartiallyEmittedExpression, isArray, ScriptTarget, ScriptKind, perfLogger, EntityName, JsonSourceFile, TextChangeRange, NodeFlags, createScanner, DiagnosticWithLocation, ensureScriptKind, convertToObjectWorker, emptyArray, emptyMap, EndOfFileToken, JsonObjectExpressionStatement, BooleanLiteral, NullLiteral, JsonMinusNumericLiteral, StringLiteral, NumericLiteral, Diagnostics, LanguageVariant, createMap, Debug, DiagnosticMessage, createFileDiagnostic, HasJSDoc, mapDefined, getJSDocCommentRanges, hasJSDocNodes, normalizePath, lastOrUndefined, TextRange, isKeyword, JSDocSyntaxKind, tokenToString, Token, TokenFlags, MutableNodeArray, Identifier, __String, isLiteralKind, isTemplateLiteralKind, LiteralLikeNode, escapeLeadingUnderscores, tokenIsIdentifierOrKeyword, PropertyName, PrivateIdentifier, isModifierKind, tokenIsIdentifierOrKeywordOrGreaterThan, nodeIsMissing, containsParseError, JSDocContainer, MethodDeclaration, last, TemplateMiddle, TemplateTail, LiteralExpression, TemplateHead, TemplateLiteralLikeNode, NoSubstitutionTemplateLiteral, TypeNode, FunctionOrConstructorTypeNode, ThisTypeNode, JSDocAllType, JSDocOptionalType, JSDocNonNullableType, JSDocUnknownType, JSDocNullableType, JSDocNamepathType, JSDocVariadicType, getFullWidth, CallSignatureDeclaration, ConstructSignatureDeclaration, IndexSignatureDeclaration, MethodSignature, TypeElement, ReadonlyToken, PlusToken, MinusToken, QuestionToken, Expression, BinaryOperatorToken, isLeftHandSideExpression, isAssignmentOperator, Modifier, isJSDocFunctionType, nodeIsPresent, getBinaryOperatorPrecedence, PrefixUnaryOperator, UnaryExpression, skipTrivia, UpdateExpression, PostfixUnaryOperator, LeftHandSideExpression, MemberExpression, PrimaryExpression, JsxSelfClosingElement, getTextOfNodeFromSourceText, JsxText, JsxOpeningElement, JsxOpeningFragment, JsxTokenSyntaxKind, JsxChild, isJsxOpeningFragment, JsxTagNameExpression, ThisExpression, JsxTagNamePropertyAccess, JsxClosingFragment, QuestionDotToken, isPrivateIdentifier, isStringOrNumericLiteralLike, ObjectLiteralElementLike, AccessorDeclaration, FunctionExpression, NewExpression, addRelatedInfo, Statement, IterationStatement, CaseOrDefaultClause, FunctionDeclaration, some, ArrayBindingElement, OmittedExpression, ObjectBindingPattern, ArrayBindingPattern, ConstructorDeclaration, AsteriskToken, isClassMemberModifier, ClassElement, SemicolonClassElement, ClassExpression, ModuleBlock, NamespaceDeclaration, NamedImports, NamedExports, ImportSpecifier, ExportSpecifier, isMetaProperty, Diagnostic, isTypeReferenceNode, JSDocParameterTag, JSDocPropertyTag, append, isJSDocReturnTag, isJSDocTypeTag, JSDocAuthorTag, PropertyAccessEntityNameExpression, JSDocNamespaceDeclaration, JSDocNamespaceBody, AssertionLevel, textChangeRangeIsUnchanged, textSpanEnd, textChangeRangeNewSpan, createTextSpanFromBounds, createTextChangeRange, getLastChild, fileExtensionIs, Extension, PragmaMap, CheckJsDirective, FileReference, AmdDependency, PragmaPseudoMapEntry, getLeadingCommentRanges, toArray, PragmaPseudoMap, map, CommentRange, commentPragmas, PragmaDefinition, PragmaKindFlags } from "./ts"; +import { mark, measure } from "./ts.performance"; +import * as ts from "./ts"; +const enum SignatureFlags { + None = 0, + Yield = 1 << 0, + Await = 1 << 1, + Type = 1 << 2, + IgnoreMissingOpenBrace = 1 << 4, + JSDoc = 1 << 5 +} +let NodeConstructor: new (kind: SyntaxKind, pos?: number, end?: number) => Node; +let TokenConstructor: new (kind: SyntaxKind, pos?: number, end?: number) => Node; +let IdentifierConstructor: new (kind: SyntaxKind, pos?: number, end?: number) => Node; +let PrivateIdentifierConstructor: new (kind: SyntaxKind, pos?: number, end?: number) => Node; +let SourceFileConstructor: new (kind: SyntaxKind, pos?: number, end?: number) => Node; +export function createNode(kind: SyntaxKind, pos?: number, end?: number): Node { + if (kind === SyntaxKind.SourceFile) { + return new (SourceFileConstructor || (SourceFileConstructor = objectAllocator.getSourceFileConstructor()))(kind, pos, end); } - - function visitNode(cbNode: (node: Node) => T, node: Node | undefined): T | undefined { - return node && cbNode(node); + else if (kind === SyntaxKind.Identifier) { + return new (IdentifierConstructor || (IdentifierConstructor = objectAllocator.getIdentifierConstructor()))(kind, pos, end); } - - function visitNodes(cbNode: (node: Node) => T, cbNodes: ((node: NodeArray) => T | undefined) | undefined, nodes: NodeArray | undefined): T | undefined { - if (nodes) { - if (cbNodes) { - return cbNodes(nodes); - } - for (const node of nodes) { - const result = cbNode(node); - if (result) { - return result; - } + else if (kind === SyntaxKind.PrivateIdentifier) { + return new (PrivateIdentifierConstructor || (PrivateIdentifierConstructor = objectAllocator.getPrivateIdentifierConstructor()))(kind, pos, end); + } + else if (!isNodeKind(kind)) { + return new (TokenConstructor || (TokenConstructor = objectAllocator.getTokenConstructor()))(kind, pos, end); + } + else { + return new (NodeConstructor || (NodeConstructor = objectAllocator.getNodeConstructor()))(kind, pos, end); + } +} +function visitNode(cbNode: (node: Node) => T, node: Node | undefined): T | undefined { + return node && cbNode(node); +} +function visitNodes(cbNode: (node: Node) => T, cbNodes: ((node: NodeArray) => T | undefined) | undefined, nodes: NodeArray | undefined): T | undefined { + if (nodes) { + if (cbNodes) { + return cbNodes(nodes); + } + for (const node of nodes) { + const result = cbNode(node); + if (result) { + return result; } } } - - /*@internal*/ - export function isJSDocLikeText(text: string, start: number) { - return text.charCodeAt(start + 1) === CharacterCodes.asterisk && - text.charCodeAt(start + 2) === CharacterCodes.asterisk && - text.charCodeAt(start + 3) !== CharacterCodes.slash; +} +/*@internal*/ +export function isJSDocLikeText(text: string, start: number) { + return text.charCodeAt(start + 1) === CharacterCodes.asterisk && + text.charCodeAt(start + 2) === CharacterCodes.asterisk && + text.charCodeAt(start + 3) !== CharacterCodes.slash; +} +/** + * Invokes a callback for each child of the given node. The 'cbNode' callback is invoked for all child nodes + * stored in properties. If a 'cbNodes' callback is specified, it is invoked for embedded arrays; otherwise, + * embedded arrays are flattened and the 'cbNode' callback is invoked for each element. If a callback returns + * a truthy value, iteration stops and that value is returned. Otherwise, undefined is returned. + * + * @param node a given node to visit its children + * @param cbNode a callback to be invoked for all child nodes + * @param cbNodes a callback to be invoked for embedded array + * + * @remarks `forEachChild` must visit the children of a node in the order + * that they appear in the source code. The language service depends on this property to locate nodes by position. + */ +export function forEachChild(node: Node, cbNode: (node: Node) => T | undefined, cbNodes?: (nodes: NodeArray) => T | undefined): T | undefined { + if (!node || node.kind <= SyntaxKind.LastToken) { + return; } - - /** - * Invokes a callback for each child of the given node. The 'cbNode' callback is invoked for all child nodes - * stored in properties. If a 'cbNodes' callback is specified, it is invoked for embedded arrays; otherwise, - * embedded arrays are flattened and the 'cbNode' callback is invoked for each element. If a callback returns - * a truthy value, iteration stops and that value is returned. Otherwise, undefined is returned. - * - * @param node a given node to visit its children - * @param cbNode a callback to be invoked for all child nodes - * @param cbNodes a callback to be invoked for embedded array - * - * @remarks `forEachChild` must visit the children of a node in the order - * that they appear in the source code. The language service depends on this property to locate nodes by position. - */ - export function forEachChild(node: Node, cbNode: (node: Node) => T | undefined, cbNodes?: (nodes: NodeArray) => T | undefined): T | undefined { - if (!node || node.kind <= SyntaxKind.LastToken) { - return; + switch (node.kind) { + case SyntaxKind.QualifiedName: + return visitNode(cbNode, (node).left) || + visitNode(cbNode, (node).right); + case SyntaxKind.TypeParameter: + return visitNode(cbNode, (node).name) || + visitNode(cbNode, (node).constraint) || + visitNode(cbNode, (node).default) || + visitNode(cbNode, (node).expression); + case SyntaxKind.ShorthandPropertyAssignment: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node).name) || + visitNode(cbNode, (node).questionToken) || + visitNode(cbNode, (node).exclamationToken) || + visitNode(cbNode, (node).equalsToken) || + visitNode(cbNode, (node).objectAssignmentInitializer); + case SyntaxKind.SpreadAssignment: + return visitNode(cbNode, (node).expression); + case SyntaxKind.Parameter: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node).dotDotDotToken) || + visitNode(cbNode, (node).name) || + visitNode(cbNode, (node).questionToken) || + visitNode(cbNode, (node).type) || + visitNode(cbNode, (node).initializer); + case SyntaxKind.PropertyDeclaration: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node).name) || + visitNode(cbNode, (node).questionToken) || + visitNode(cbNode, (node).exclamationToken) || + visitNode(cbNode, (node).type) || + visitNode(cbNode, (node).initializer); + case SyntaxKind.PropertySignature: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node).name) || + visitNode(cbNode, (node).questionToken) || + visitNode(cbNode, (node).type) || + visitNode(cbNode, (node).initializer); + case SyntaxKind.PropertyAssignment: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node).name) || + visitNode(cbNode, (node).questionToken) || + visitNode(cbNode, (node).initializer); + case SyntaxKind.VariableDeclaration: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node).name) || + visitNode(cbNode, (node).exclamationToken) || + visitNode(cbNode, (node).type) || + visitNode(cbNode, (node).initializer); + case SyntaxKind.BindingElement: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node).dotDotDotToken) || + visitNode(cbNode, (node).propertyName) || + visitNode(cbNode, (node).name) || + visitNode(cbNode, (node).initializer); + case SyntaxKind.FunctionType: + case SyntaxKind.ConstructorType: + case SyntaxKind.CallSignature: + case SyntaxKind.ConstructSignature: + case SyntaxKind.IndexSignature: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNodes(cbNode, cbNodes, (node).typeParameters) || + visitNodes(cbNode, cbNodes, (node).parameters) || + visitNode(cbNode, (node).type); + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: + case SyntaxKind.Constructor: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + case SyntaxKind.FunctionExpression: + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.ArrowFunction: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node).asteriskToken) || + visitNode(cbNode, (node).name) || + visitNode(cbNode, (node).questionToken) || + visitNode(cbNode, (node).exclamationToken) || + visitNodes(cbNode, cbNodes, (node).typeParameters) || + visitNodes(cbNode, cbNodes, (node).parameters) || + visitNode(cbNode, (node).type) || + visitNode(cbNode, (node).equalsGreaterThanToken) || + visitNode(cbNode, (node).body); + case SyntaxKind.TypeReference: + return visitNode(cbNode, (node).typeName) || + visitNodes(cbNode, cbNodes, (node).typeArguments); + case SyntaxKind.TypePredicate: + return visitNode(cbNode, (node).assertsModifier) || + visitNode(cbNode, (node).parameterName) || + visitNode(cbNode, (node).type); + case SyntaxKind.TypeQuery: + return visitNode(cbNode, (node).exprName); + case SyntaxKind.TypeLiteral: + return visitNodes(cbNode, cbNodes, (node).members); + case SyntaxKind.ArrayType: + return visitNode(cbNode, (node).elementType); + case SyntaxKind.TupleType: + return visitNodes(cbNode, cbNodes, (node).elementTypes); + case SyntaxKind.UnionType: + case SyntaxKind.IntersectionType: + return visitNodes(cbNode, cbNodes, (node).types); + case SyntaxKind.ConditionalType: + return visitNode(cbNode, (node).checkType) || + visitNode(cbNode, (node).extendsType) || + visitNode(cbNode, (node).trueType) || + visitNode(cbNode, (node).falseType); + case SyntaxKind.InferType: + return visitNode(cbNode, (node).typeParameter); + case SyntaxKind.ImportType: + return visitNode(cbNode, (node).argument) || + visitNode(cbNode, (node).qualifier) || + visitNodes(cbNode, cbNodes, (node).typeArguments); + case SyntaxKind.ParenthesizedType: + case SyntaxKind.TypeOperator: + return visitNode(cbNode, (node).type); + case SyntaxKind.IndexedAccessType: + return visitNode(cbNode, (node).objectType) || + visitNode(cbNode, (node).indexType); + case SyntaxKind.MappedType: + return visitNode(cbNode, (node).readonlyToken) || + visitNode(cbNode, (node).typeParameter) || + visitNode(cbNode, (node).questionToken) || + visitNode(cbNode, (node).type); + case SyntaxKind.LiteralType: + return visitNode(cbNode, (node).literal); + case SyntaxKind.ObjectBindingPattern: + case SyntaxKind.ArrayBindingPattern: + return visitNodes(cbNode, cbNodes, (node).elements); + case SyntaxKind.ArrayLiteralExpression: + return visitNodes(cbNode, cbNodes, (node).elements); + case SyntaxKind.ObjectLiteralExpression: + return visitNodes(cbNode, cbNodes, (node).properties); + case SyntaxKind.PropertyAccessExpression: + return visitNode(cbNode, (node).expression) || + visitNode(cbNode, (node).questionDotToken) || + visitNode(cbNode, (node).name); + case SyntaxKind.ElementAccessExpression: + return visitNode(cbNode, (node).expression) || + visitNode(cbNode, (node).questionDotToken) || + visitNode(cbNode, (node).argumentExpression); + case SyntaxKind.CallExpression: + case SyntaxKind.NewExpression: + return visitNode(cbNode, (node).expression) || + visitNode(cbNode, (node).questionDotToken) || + visitNodes(cbNode, cbNodes, (node).typeArguments) || + visitNodes(cbNode, cbNodes, (node).arguments); + case SyntaxKind.TaggedTemplateExpression: + return visitNode(cbNode, (node).tag) || + visitNode(cbNode, (node).questionDotToken) || + visitNodes(cbNode, cbNodes, (node).typeArguments) || + visitNode(cbNode, (node).template); + case SyntaxKind.TypeAssertionExpression: + return visitNode(cbNode, (node).type) || + visitNode(cbNode, (node).expression); + case SyntaxKind.ParenthesizedExpression: + return visitNode(cbNode, (node).expression); + case SyntaxKind.DeleteExpression: + return visitNode(cbNode, (node).expression); + case SyntaxKind.TypeOfExpression: + return visitNode(cbNode, (node).expression); + case SyntaxKind.VoidExpression: + return visitNode(cbNode, (node).expression); + case SyntaxKind.PrefixUnaryExpression: + return visitNode(cbNode, (node).operand); + case SyntaxKind.YieldExpression: + return visitNode(cbNode, (node).asteriskToken) || + visitNode(cbNode, (node).expression); + case SyntaxKind.AwaitExpression: + return visitNode(cbNode, (node).expression); + case SyntaxKind.PostfixUnaryExpression: + return visitNode(cbNode, (node).operand); + case SyntaxKind.BinaryExpression: + return visitNode(cbNode, (node).left) || + visitNode(cbNode, (node).operatorToken) || + visitNode(cbNode, (node).right); + case SyntaxKind.AsExpression: + return visitNode(cbNode, (node).expression) || + visitNode(cbNode, (node).type); + case SyntaxKind.NonNullExpression: + return visitNode(cbNode, (node).expression); + case SyntaxKind.MetaProperty: + return visitNode(cbNode, (node).name); + case SyntaxKind.ConditionalExpression: + return visitNode(cbNode, (node).condition) || + visitNode(cbNode, (node).questionToken) || + visitNode(cbNode, (node).whenTrue) || + visitNode(cbNode, (node).colonToken) || + visitNode(cbNode, (node).whenFalse); + case SyntaxKind.SpreadElement: + return visitNode(cbNode, (node).expression); + case SyntaxKind.Block: + case SyntaxKind.ModuleBlock: + return visitNodes(cbNode, cbNodes, (node).statements); + case SyntaxKind.SourceFile: + return visitNodes(cbNode, cbNodes, (node).statements) || + visitNode(cbNode, (node).endOfFileToken); + case SyntaxKind.VariableStatement: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node).declarationList); + case SyntaxKind.VariableDeclarationList: + return visitNodes(cbNode, cbNodes, (node).declarations); + case SyntaxKind.ExpressionStatement: + return visitNode(cbNode, (node).expression); + case SyntaxKind.IfStatement: + return visitNode(cbNode, (node).expression) || + visitNode(cbNode, (node).thenStatement) || + visitNode(cbNode, (node).elseStatement); + case SyntaxKind.DoStatement: + return visitNode(cbNode, (node).statement) || + visitNode(cbNode, (node).expression); + case SyntaxKind.WhileStatement: + return visitNode(cbNode, (node).expression) || + visitNode(cbNode, (node).statement); + case SyntaxKind.ForStatement: + return visitNode(cbNode, (node).initializer) || + visitNode(cbNode, (node).condition) || + visitNode(cbNode, (node).incrementor) || + visitNode(cbNode, (node).statement); + case SyntaxKind.ForInStatement: + return visitNode(cbNode, (node).initializer) || + visitNode(cbNode, (node).expression) || + visitNode(cbNode, (node).statement); + case SyntaxKind.ForOfStatement: + return visitNode(cbNode, (node).awaitModifier) || + visitNode(cbNode, (node).initializer) || + visitNode(cbNode, (node).expression) || + visitNode(cbNode, (node).statement); + case SyntaxKind.ContinueStatement: + case SyntaxKind.BreakStatement: + return visitNode(cbNode, (node).label); + case SyntaxKind.ReturnStatement: + return visitNode(cbNode, (node).expression); + case SyntaxKind.WithStatement: + return visitNode(cbNode, (node).expression) || + visitNode(cbNode, (node).statement); + case SyntaxKind.SwitchStatement: + return visitNode(cbNode, (node).expression) || + visitNode(cbNode, (node).caseBlock); + case SyntaxKind.CaseBlock: + return visitNodes(cbNode, cbNodes, (node).clauses); + case SyntaxKind.CaseClause: + return visitNode(cbNode, (node).expression) || + visitNodes(cbNode, cbNodes, (node).statements); + case SyntaxKind.DefaultClause: + return visitNodes(cbNode, cbNodes, (node).statements); + case SyntaxKind.LabeledStatement: + return visitNode(cbNode, (node).label) || + visitNode(cbNode, (node).statement); + case SyntaxKind.ThrowStatement: + return visitNode(cbNode, (node).expression); + case SyntaxKind.TryStatement: + return visitNode(cbNode, (node).tryBlock) || + visitNode(cbNode, (node).catchClause) || + visitNode(cbNode, (node).finallyBlock); + case SyntaxKind.CatchClause: + return visitNode(cbNode, (node).variableDeclaration) || + visitNode(cbNode, (node).block); + case SyntaxKind.Decorator: + return visitNode(cbNode, (node).expression); + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ClassExpression: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node).name) || + visitNodes(cbNode, cbNodes, (node).typeParameters) || + visitNodes(cbNode, cbNodes, (node).heritageClauses) || + visitNodes(cbNode, cbNodes, (node).members); + case SyntaxKind.InterfaceDeclaration: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node).name) || + visitNodes(cbNode, cbNodes, (node).typeParameters) || + visitNodes(cbNode, cbNodes, (node).heritageClauses) || + visitNodes(cbNode, cbNodes, (node).members); + case SyntaxKind.TypeAliasDeclaration: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node).name) || + visitNodes(cbNode, cbNodes, (node).typeParameters) || + visitNode(cbNode, (node).type); + case SyntaxKind.EnumDeclaration: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node).name) || + visitNodes(cbNode, cbNodes, (node).members); + case SyntaxKind.EnumMember: + return visitNode(cbNode, (node).name) || + visitNode(cbNode, (node).initializer); + case SyntaxKind.ModuleDeclaration: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node).name) || + visitNode(cbNode, (node).body); + case SyntaxKind.ImportEqualsDeclaration: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node).name) || + visitNode(cbNode, (node).moduleReference); + case SyntaxKind.ImportDeclaration: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node).importClause) || + visitNode(cbNode, (node).moduleSpecifier); + case SyntaxKind.ImportClause: + return visitNode(cbNode, (node).name) || + visitNode(cbNode, (node).namedBindings); + case SyntaxKind.NamespaceExportDeclaration: + return visitNode(cbNode, (node).name); + case SyntaxKind.NamespaceImport: + return visitNode(cbNode, (node).name); + case SyntaxKind.NamespaceExport: + return visitNode(cbNode, (node).name); + case SyntaxKind.NamedImports: + case SyntaxKind.NamedExports: + return visitNodes(cbNode, cbNodes, (node).elements); + case SyntaxKind.ExportDeclaration: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node).exportClause) || + visitNode(cbNode, (node).moduleSpecifier); + case SyntaxKind.ImportSpecifier: + case SyntaxKind.ExportSpecifier: + return visitNode(cbNode, (node).propertyName) || + visitNode(cbNode, (node).name); + case SyntaxKind.ExportAssignment: + return visitNodes(cbNode, cbNodes, node.decorators) || + visitNodes(cbNode, cbNodes, node.modifiers) || + visitNode(cbNode, (node).expression); + case SyntaxKind.TemplateExpression: + return visitNode(cbNode, (node).head) || visitNodes(cbNode, cbNodes, (node).templateSpans); + case SyntaxKind.TemplateSpan: + return visitNode(cbNode, (node).expression) || visitNode(cbNode, (node).literal); + case SyntaxKind.ComputedPropertyName: + return visitNode(cbNode, (node).expression); + case SyntaxKind.HeritageClause: + return visitNodes(cbNode, cbNodes, (node).types); + case SyntaxKind.ExpressionWithTypeArguments: + return visitNode(cbNode, (node).expression) || + visitNodes(cbNode, cbNodes, (node).typeArguments); + case SyntaxKind.ExternalModuleReference: + return visitNode(cbNode, (node).expression); + case SyntaxKind.MissingDeclaration: + return visitNodes(cbNode, cbNodes, node.decorators); + case SyntaxKind.CommaListExpression: + return visitNodes(cbNode, cbNodes, (node).elements); + case SyntaxKind.JsxElement: + return visitNode(cbNode, (node).openingElement) || + visitNodes(cbNode, cbNodes, (node).children) || + visitNode(cbNode, (node).closingElement); + case SyntaxKind.JsxFragment: + return visitNode(cbNode, (node).openingFragment) || + visitNodes(cbNode, cbNodes, (node).children) || + visitNode(cbNode, (node).closingFragment); + case SyntaxKind.JsxSelfClosingElement: + case SyntaxKind.JsxOpeningElement: + return visitNode(cbNode, (node).tagName) || + visitNodes(cbNode, cbNodes, (node).typeArguments) || + visitNode(cbNode, (node).attributes); + case SyntaxKind.JsxAttributes: + return visitNodes(cbNode, cbNodes, (node).properties); + case SyntaxKind.JsxAttribute: + return visitNode(cbNode, (node).name) || + visitNode(cbNode, (node).initializer); + case SyntaxKind.JsxSpreadAttribute: + return visitNode(cbNode, (node).expression); + case SyntaxKind.JsxExpression: + return visitNode(cbNode, (node as JsxExpression).dotDotDotToken) || + visitNode(cbNode, (node as JsxExpression).expression); + case SyntaxKind.JsxClosingElement: + return visitNode(cbNode, (node).tagName); + case SyntaxKind.OptionalType: + case SyntaxKind.RestType: + case SyntaxKind.JSDocTypeExpression: + case SyntaxKind.JSDocNonNullableType: + case SyntaxKind.JSDocNullableType: + case SyntaxKind.JSDocOptionalType: + case SyntaxKind.JSDocVariadicType: + return visitNode(cbNode, (node).type); + case SyntaxKind.JSDocFunctionType: + return visitNodes(cbNode, cbNodes, (node).parameters) || + visitNode(cbNode, (node).type); + case SyntaxKind.JSDocComment: + return visitNodes(cbNode, cbNodes, (node).tags); + case SyntaxKind.JSDocParameterTag: + case SyntaxKind.JSDocPropertyTag: + return visitNode(cbNode, (node as JSDocTag).tagName) || + ((node as JSDocPropertyLikeTag).isNameFirst + ? visitNode(cbNode, (node).name) || + visitNode(cbNode, (node).typeExpression) + : visitNode(cbNode, (node).typeExpression) || + visitNode(cbNode, (node).name)); + case SyntaxKind.JSDocAuthorTag: + return visitNode(cbNode, (node as JSDocTag).tagName); + case SyntaxKind.JSDocImplementsTag: + return visitNode(cbNode, (node as JSDocTag).tagName) || + visitNode(cbNode, (node).class); + case SyntaxKind.JSDocAugmentsTag: + return visitNode(cbNode, (node as JSDocTag).tagName) || + visitNode(cbNode, (node).class); + case SyntaxKind.JSDocTemplateTag: + return visitNode(cbNode, (node as JSDocTag).tagName) || + visitNode(cbNode, (node).constraint) || + visitNodes(cbNode, cbNodes, (node).typeParameters); + case SyntaxKind.JSDocTypedefTag: + return visitNode(cbNode, (node as JSDocTag).tagName) || + ((node as JSDocTypedefTag).typeExpression && + (node as JSDocTypedefTag).typeExpression!.kind === SyntaxKind.JSDocTypeExpression + ? visitNode(cbNode, (node).typeExpression) || + visitNode(cbNode, (node).fullName) + : visitNode(cbNode, (node).fullName) || + visitNode(cbNode, (node).typeExpression)); + case SyntaxKind.JSDocCallbackTag: + return visitNode(cbNode, (node as JSDocTag).tagName) || + visitNode(cbNode, (node as JSDocCallbackTag).fullName) || + visitNode(cbNode, (node as JSDocCallbackTag).typeExpression); + case SyntaxKind.JSDocReturnTag: + case SyntaxKind.JSDocTypeTag: + case SyntaxKind.JSDocThisTag: + case SyntaxKind.JSDocEnumTag: + return visitNode(cbNode, (node as JSDocTag).tagName) || + visitNode(cbNode, (node as JSDocReturnTag | JSDocTypeTag | JSDocThisTag | JSDocEnumTag).typeExpression); + case SyntaxKind.JSDocSignature: + return forEach((node).typeParameters, cbNode) || + forEach((node).parameters, cbNode) || + visitNode(cbNode, (node).type); + case SyntaxKind.JSDocTypeLiteral: + return forEach((node as JSDocTypeLiteral).jsDocPropertyTags, cbNode); + case SyntaxKind.JSDocTag: + case SyntaxKind.JSDocClassTag: + case SyntaxKind.JSDocPublicTag: + case SyntaxKind.JSDocPrivateTag: + case SyntaxKind.JSDocProtectedTag: + case SyntaxKind.JSDocReadonlyTag: + return visitNode(cbNode, (node as JSDocTag).tagName); + case SyntaxKind.PartiallyEmittedExpression: + return visitNode(cbNode, (node).expression); + } +} +/** @internal */ +/** + * Invokes a callback for each child of the given node. The 'cbNode' callback is invoked for all child nodes + * stored in properties. If a 'cbNodes' callback is specified, it is invoked for embedded arrays; additionally, + * unlike `forEachChild`, embedded arrays are flattened and the 'cbNode' callback is invoked for each element. + * If a callback returns a truthy value, iteration stops and that value is returned. Otherwise, undefined is returned. + * + * @param node a given node to visit its children + * @param cbNode a callback to be invoked for all child nodes + * @param cbNodes a callback to be invoked for embedded array + * + * @remarks Unlike `forEachChild`, `forEachChildRecursively` handles recursively invoking the traversal on each child node found, + * and while doing so, handles traversing the structure without relying on the callstack to encode the tree structure. + */ +export function forEachChildRecursively(rootNode: Node, cbNode: (node: Node, parent: Node) => T | "skip" | undefined, cbNodes?: (nodes: NodeArray, parent: Node) => T | "skip" | undefined): T | undefined { + const stack: Node[] = [rootNode]; + while (stack.length) { + const parent = stack.pop()!; + const res = visitAllPossibleChildren(parent, gatherPossibleChildren(parent)); + if (res) { + return res; } - switch (node.kind) { - case SyntaxKind.QualifiedName: - return visitNode(cbNode, (node).left) || - visitNode(cbNode, (node).right); - case SyntaxKind.TypeParameter: - return visitNode(cbNode, (node).name) || - visitNode(cbNode, (node).constraint) || - visitNode(cbNode, (node).default) || - visitNode(cbNode, (node).expression); - case SyntaxKind.ShorthandPropertyAssignment: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node).name) || - visitNode(cbNode, (node).questionToken) || - visitNode(cbNode, (node).exclamationToken) || - visitNode(cbNode, (node).equalsToken) || - visitNode(cbNode, (node).objectAssignmentInitializer); - case SyntaxKind.SpreadAssignment: - return visitNode(cbNode, (node).expression); - case SyntaxKind.Parameter: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node).dotDotDotToken) || - visitNode(cbNode, (node).name) || - visitNode(cbNode, (node).questionToken) || - visitNode(cbNode, (node).type) || - visitNode(cbNode, (node).initializer); - case SyntaxKind.PropertyDeclaration: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node).name) || - visitNode(cbNode, (node).questionToken) || - visitNode(cbNode, (node).exclamationToken) || - visitNode(cbNode, (node).type) || - visitNode(cbNode, (node).initializer); - case SyntaxKind.PropertySignature: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node).name) || - visitNode(cbNode, (node).questionToken) || - visitNode(cbNode, (node).type) || - visitNode(cbNode, (node).initializer); - case SyntaxKind.PropertyAssignment: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node).name) || - visitNode(cbNode, (node).questionToken) || - visitNode(cbNode, (node).initializer); - case SyntaxKind.VariableDeclaration: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node).name) || - visitNode(cbNode, (node).exclamationToken) || - visitNode(cbNode, (node).type) || - visitNode(cbNode, (node).initializer); - case SyntaxKind.BindingElement: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node).dotDotDotToken) || - visitNode(cbNode, (node).propertyName) || - visitNode(cbNode, (node).name) || - visitNode(cbNode, (node).initializer); - case SyntaxKind.FunctionType: - case SyntaxKind.ConstructorType: - case SyntaxKind.CallSignature: - case SyntaxKind.ConstructSignature: - case SyntaxKind.IndexSignature: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNodes(cbNode, cbNodes, (node).typeParameters) || - visitNodes(cbNode, cbNodes, (node).parameters) || - visitNode(cbNode, (node).type); - case SyntaxKind.MethodDeclaration: - case SyntaxKind.MethodSignature: - case SyntaxKind.Constructor: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - case SyntaxKind.FunctionExpression: - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.ArrowFunction: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node).asteriskToken) || - visitNode(cbNode, (node).name) || - visitNode(cbNode, (node).questionToken) || - visitNode(cbNode, (node).exclamationToken) || - visitNodes(cbNode, cbNodes, (node).typeParameters) || - visitNodes(cbNode, cbNodes, (node).parameters) || - visitNode(cbNode, (node).type) || - visitNode(cbNode, (node).equalsGreaterThanToken) || - visitNode(cbNode, (node).body); - case SyntaxKind.TypeReference: - return visitNode(cbNode, (node).typeName) || - visitNodes(cbNode, cbNodes, (node).typeArguments); - case SyntaxKind.TypePredicate: - return visitNode(cbNode, (node).assertsModifier) || - visitNode(cbNode, (node).parameterName) || - visitNode(cbNode, (node).type); - case SyntaxKind.TypeQuery: - return visitNode(cbNode, (node).exprName); - case SyntaxKind.TypeLiteral: - return visitNodes(cbNode, cbNodes, (node).members); - case SyntaxKind.ArrayType: - return visitNode(cbNode, (node).elementType); - case SyntaxKind.TupleType: - return visitNodes(cbNode, cbNodes, (node).elementTypes); - case SyntaxKind.UnionType: - case SyntaxKind.IntersectionType: - return visitNodes(cbNode, cbNodes, (node).types); - case SyntaxKind.ConditionalType: - return visitNode(cbNode, (node).checkType) || - visitNode(cbNode, (node).extendsType) || - visitNode(cbNode, (node).trueType) || - visitNode(cbNode, (node).falseType); - case SyntaxKind.InferType: - return visitNode(cbNode, (node).typeParameter); - case SyntaxKind.ImportType: - return visitNode(cbNode, (node).argument) || - visitNode(cbNode, (node).qualifier) || - visitNodes(cbNode, cbNodes, (node).typeArguments); - case SyntaxKind.ParenthesizedType: - case SyntaxKind.TypeOperator: - return visitNode(cbNode, (node).type); - case SyntaxKind.IndexedAccessType: - return visitNode(cbNode, (node).objectType) || - visitNode(cbNode, (node).indexType); - case SyntaxKind.MappedType: - return visitNode(cbNode, (node).readonlyToken) || - visitNode(cbNode, (node).typeParameter) || - visitNode(cbNode, (node).questionToken) || - visitNode(cbNode, (node).type); - case SyntaxKind.LiteralType: - return visitNode(cbNode, (node).literal); - case SyntaxKind.ObjectBindingPattern: - case SyntaxKind.ArrayBindingPattern: - return visitNodes(cbNode, cbNodes, (node).elements); - case SyntaxKind.ArrayLiteralExpression: - return visitNodes(cbNode, cbNodes, (node).elements); - case SyntaxKind.ObjectLiteralExpression: - return visitNodes(cbNode, cbNodes, (node).properties); - case SyntaxKind.PropertyAccessExpression: - return visitNode(cbNode, (node).expression) || - visitNode(cbNode, (node).questionDotToken) || - visitNode(cbNode, (node).name); - case SyntaxKind.ElementAccessExpression: - return visitNode(cbNode, (node).expression) || - visitNode(cbNode, (node).questionDotToken) || - visitNode(cbNode, (node).argumentExpression); - case SyntaxKind.CallExpression: - case SyntaxKind.NewExpression: - return visitNode(cbNode, (node).expression) || - visitNode(cbNode, (node).questionDotToken) || - visitNodes(cbNode, cbNodes, (node).typeArguments) || - visitNodes(cbNode, cbNodes, (node).arguments); - case SyntaxKind.TaggedTemplateExpression: - return visitNode(cbNode, (node).tag) || - visitNode(cbNode, (node).questionDotToken) || - visitNodes(cbNode, cbNodes, (node).typeArguments) || - visitNode(cbNode, (node).template); - case SyntaxKind.TypeAssertionExpression: - return visitNode(cbNode, (node).type) || - visitNode(cbNode, (node).expression); - case SyntaxKind.ParenthesizedExpression: - return visitNode(cbNode, (node).expression); - case SyntaxKind.DeleteExpression: - return visitNode(cbNode, (node).expression); - case SyntaxKind.TypeOfExpression: - return visitNode(cbNode, (node).expression); - case SyntaxKind.VoidExpression: - return visitNode(cbNode, (node).expression); - case SyntaxKind.PrefixUnaryExpression: - return visitNode(cbNode, (node).operand); - case SyntaxKind.YieldExpression: - return visitNode(cbNode, (node).asteriskToken) || - visitNode(cbNode, (node).expression); - case SyntaxKind.AwaitExpression: - return visitNode(cbNode, (node).expression); - case SyntaxKind.PostfixUnaryExpression: - return visitNode(cbNode, (node).operand); - case SyntaxKind.BinaryExpression: - return visitNode(cbNode, (node).left) || - visitNode(cbNode, (node).operatorToken) || - visitNode(cbNode, (node).right); - case SyntaxKind.AsExpression: - return visitNode(cbNode, (node).expression) || - visitNode(cbNode, (node).type); - case SyntaxKind.NonNullExpression: - return visitNode(cbNode, (node).expression); - case SyntaxKind.MetaProperty: - return visitNode(cbNode, (node).name); - case SyntaxKind.ConditionalExpression: - return visitNode(cbNode, (node).condition) || - visitNode(cbNode, (node).questionToken) || - visitNode(cbNode, (node).whenTrue) || - visitNode(cbNode, (node).colonToken) || - visitNode(cbNode, (node).whenFalse); - case SyntaxKind.SpreadElement: - return visitNode(cbNode, (node).expression); - case SyntaxKind.Block: - case SyntaxKind.ModuleBlock: - return visitNodes(cbNode, cbNodes, (node).statements); - case SyntaxKind.SourceFile: - return visitNodes(cbNode, cbNodes, (node).statements) || - visitNode(cbNode, (node).endOfFileToken); - case SyntaxKind.VariableStatement: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node).declarationList); - case SyntaxKind.VariableDeclarationList: - return visitNodes(cbNode, cbNodes, (node).declarations); - case SyntaxKind.ExpressionStatement: - return visitNode(cbNode, (node).expression); - case SyntaxKind.IfStatement: - return visitNode(cbNode, (node).expression) || - visitNode(cbNode, (node).thenStatement) || - visitNode(cbNode, (node).elseStatement); - case SyntaxKind.DoStatement: - return visitNode(cbNode, (node).statement) || - visitNode(cbNode, (node).expression); - case SyntaxKind.WhileStatement: - return visitNode(cbNode, (node).expression) || - visitNode(cbNode, (node).statement); - case SyntaxKind.ForStatement: - return visitNode(cbNode, (node).initializer) || - visitNode(cbNode, (node).condition) || - visitNode(cbNode, (node).incrementor) || - visitNode(cbNode, (node).statement); - case SyntaxKind.ForInStatement: - return visitNode(cbNode, (node).initializer) || - visitNode(cbNode, (node).expression) || - visitNode(cbNode, (node).statement); - case SyntaxKind.ForOfStatement: - return visitNode(cbNode, (node).awaitModifier) || - visitNode(cbNode, (node).initializer) || - visitNode(cbNode, (node).expression) || - visitNode(cbNode, (node).statement); - case SyntaxKind.ContinueStatement: - case SyntaxKind.BreakStatement: - return visitNode(cbNode, (node).label); - case SyntaxKind.ReturnStatement: - return visitNode(cbNode, (node).expression); - case SyntaxKind.WithStatement: - return visitNode(cbNode, (node).expression) || - visitNode(cbNode, (node).statement); - case SyntaxKind.SwitchStatement: - return visitNode(cbNode, (node).expression) || - visitNode(cbNode, (node).caseBlock); - case SyntaxKind.CaseBlock: - return visitNodes(cbNode, cbNodes, (node).clauses); - case SyntaxKind.CaseClause: - return visitNode(cbNode, (node).expression) || - visitNodes(cbNode, cbNodes, (node).statements); - case SyntaxKind.DefaultClause: - return visitNodes(cbNode, cbNodes, (node).statements); - case SyntaxKind.LabeledStatement: - return visitNode(cbNode, (node).label) || - visitNode(cbNode, (node).statement); - case SyntaxKind.ThrowStatement: - return visitNode(cbNode, (node).expression); - case SyntaxKind.TryStatement: - return visitNode(cbNode, (node).tryBlock) || - visitNode(cbNode, (node).catchClause) || - visitNode(cbNode, (node).finallyBlock); - case SyntaxKind.CatchClause: - return visitNode(cbNode, (node).variableDeclaration) || - visitNode(cbNode, (node).block); - case SyntaxKind.Decorator: - return visitNode(cbNode, (node).expression); - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ClassExpression: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node).name) || - visitNodes(cbNode, cbNodes, (node).typeParameters) || - visitNodes(cbNode, cbNodes, (node).heritageClauses) || - visitNodes(cbNode, cbNodes, (node).members); - case SyntaxKind.InterfaceDeclaration: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node).name) || - visitNodes(cbNode, cbNodes, (node).typeParameters) || - visitNodes(cbNode, cbNodes, (node).heritageClauses) || - visitNodes(cbNode, cbNodes, (node).members); - case SyntaxKind.TypeAliasDeclaration: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node).name) || - visitNodes(cbNode, cbNodes, (node).typeParameters) || - visitNode(cbNode, (node).type); - case SyntaxKind.EnumDeclaration: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node).name) || - visitNodes(cbNode, cbNodes, (node).members); - case SyntaxKind.EnumMember: - return visitNode(cbNode, (node).name) || - visitNode(cbNode, (node).initializer); - case SyntaxKind.ModuleDeclaration: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node).name) || - visitNode(cbNode, (node).body); - case SyntaxKind.ImportEqualsDeclaration: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node).name) || - visitNode(cbNode, (node).moduleReference); - case SyntaxKind.ImportDeclaration: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node).importClause) || - visitNode(cbNode, (node).moduleSpecifier); - case SyntaxKind.ImportClause: - return visitNode(cbNode, (node).name) || - visitNode(cbNode, (node).namedBindings); - case SyntaxKind.NamespaceExportDeclaration: - return visitNode(cbNode, (node).name); - - case SyntaxKind.NamespaceImport: - return visitNode(cbNode, (node).name); - case SyntaxKind.NamespaceExport: - return visitNode(cbNode, (node).name); - case SyntaxKind.NamedImports: - case SyntaxKind.NamedExports: - return visitNodes(cbNode, cbNodes, (node).elements); - case SyntaxKind.ExportDeclaration: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node).exportClause) || - visitNode(cbNode, (node).moduleSpecifier); - case SyntaxKind.ImportSpecifier: - case SyntaxKind.ExportSpecifier: - return visitNode(cbNode, (node).propertyName) || - visitNode(cbNode, (node).name); - case SyntaxKind.ExportAssignment: - return visitNodes(cbNode, cbNodes, node.decorators) || - visitNodes(cbNode, cbNodes, node.modifiers) || - visitNode(cbNode, (node).expression); - case SyntaxKind.TemplateExpression: - return visitNode(cbNode, (node).head) || visitNodes(cbNode, cbNodes, (node).templateSpans); - case SyntaxKind.TemplateSpan: - return visitNode(cbNode, (node).expression) || visitNode(cbNode, (node).literal); - case SyntaxKind.ComputedPropertyName: - return visitNode(cbNode, (node).expression); - case SyntaxKind.HeritageClause: - return visitNodes(cbNode, cbNodes, (node).types); - case SyntaxKind.ExpressionWithTypeArguments: - return visitNode(cbNode, (node).expression) || - visitNodes(cbNode, cbNodes, (node).typeArguments); - case SyntaxKind.ExternalModuleReference: - return visitNode(cbNode, (node).expression); - case SyntaxKind.MissingDeclaration: - return visitNodes(cbNode, cbNodes, node.decorators); - case SyntaxKind.CommaListExpression: - return visitNodes(cbNode, cbNodes, (node).elements); - - case SyntaxKind.JsxElement: - return visitNode(cbNode, (node).openingElement) || - visitNodes(cbNode, cbNodes, (node).children) || - visitNode(cbNode, (node).closingElement); - case SyntaxKind.JsxFragment: - return visitNode(cbNode, (node).openingFragment) || - visitNodes(cbNode, cbNodes, (node).children) || - visitNode(cbNode, (node).closingFragment); - case SyntaxKind.JsxSelfClosingElement: - case SyntaxKind.JsxOpeningElement: - return visitNode(cbNode, (node).tagName) || - visitNodes(cbNode, cbNodes, (node).typeArguments) || - visitNode(cbNode, (node).attributes); - case SyntaxKind.JsxAttributes: - return visitNodes(cbNode, cbNodes, (node).properties); - case SyntaxKind.JsxAttribute: - return visitNode(cbNode, (node).name) || - visitNode(cbNode, (node).initializer); - case SyntaxKind.JsxSpreadAttribute: - return visitNode(cbNode, (node).expression); - case SyntaxKind.JsxExpression: - return visitNode(cbNode, (node as JsxExpression).dotDotDotToken) || - visitNode(cbNode, (node as JsxExpression).expression); - case SyntaxKind.JsxClosingElement: - return visitNode(cbNode, (node).tagName); - - case SyntaxKind.OptionalType: - case SyntaxKind.RestType: - case SyntaxKind.JSDocTypeExpression: - case SyntaxKind.JSDocNonNullableType: - case SyntaxKind.JSDocNullableType: - case SyntaxKind.JSDocOptionalType: - case SyntaxKind.JSDocVariadicType: - return visitNode(cbNode, (node).type); - case SyntaxKind.JSDocFunctionType: - return visitNodes(cbNode, cbNodes, (node).parameters) || - visitNode(cbNode, (node).type); - case SyntaxKind.JSDocComment: - return visitNodes(cbNode, cbNodes, (node).tags); - case SyntaxKind.JSDocParameterTag: - case SyntaxKind.JSDocPropertyTag: - return visitNode(cbNode, (node as JSDocTag).tagName) || - ((node as JSDocPropertyLikeTag).isNameFirst - ? visitNode(cbNode, (node).name) || - visitNode(cbNode, (node).typeExpression) - : visitNode(cbNode, (node).typeExpression) || - visitNode(cbNode, (node).name)); - case SyntaxKind.JSDocAuthorTag: - return visitNode(cbNode, (node as JSDocTag).tagName); - case SyntaxKind.JSDocImplementsTag: - return visitNode(cbNode, (node as JSDocTag).tagName) || - visitNode(cbNode, (node).class); - case SyntaxKind.JSDocAugmentsTag: - return visitNode(cbNode, (node as JSDocTag).tagName) || - visitNode(cbNode, (node).class); - case SyntaxKind.JSDocTemplateTag: - return visitNode(cbNode, (node as JSDocTag).tagName) || - visitNode(cbNode, (node).constraint) || - visitNodes(cbNode, cbNodes, (node).typeParameters); - case SyntaxKind.JSDocTypedefTag: - return visitNode(cbNode, (node as JSDocTag).tagName) || - ((node as JSDocTypedefTag).typeExpression && - (node as JSDocTypedefTag).typeExpression!.kind === SyntaxKind.JSDocTypeExpression - ? visitNode(cbNode, (node).typeExpression) || - visitNode(cbNode, (node).fullName) - : visitNode(cbNode, (node).fullName) || - visitNode(cbNode, (node).typeExpression)); - case SyntaxKind.JSDocCallbackTag: - return visitNode(cbNode, (node as JSDocTag).tagName) || - visitNode(cbNode, (node as JSDocCallbackTag).fullName) || - visitNode(cbNode, (node as JSDocCallbackTag).typeExpression); - case SyntaxKind.JSDocReturnTag: - case SyntaxKind.JSDocTypeTag: - case SyntaxKind.JSDocThisTag: - case SyntaxKind.JSDocEnumTag: - return visitNode(cbNode, (node as JSDocTag).tagName) || - visitNode(cbNode, (node as JSDocReturnTag | JSDocTypeTag | JSDocThisTag | JSDocEnumTag).typeExpression); - case SyntaxKind.JSDocSignature: - return forEach((node).typeParameters, cbNode) || - forEach((node).parameters, cbNode) || - visitNode(cbNode, (node).type); - case SyntaxKind.JSDocTypeLiteral: - return forEach((node as JSDocTypeLiteral).jsDocPropertyTags, cbNode); - case SyntaxKind.JSDocTag: - case SyntaxKind.JSDocClassTag: - case SyntaxKind.JSDocPublicTag: - case SyntaxKind.JSDocPrivateTag: - case SyntaxKind.JSDocProtectedTag: - case SyntaxKind.JSDocReadonlyTag: - return visitNode(cbNode, (node as JSDocTag).tagName); - case SyntaxKind.PartiallyEmittedExpression: - return visitNode(cbNode, (node).expression); - } - } - - /** @internal */ - /** - * Invokes a callback for each child of the given node. The 'cbNode' callback is invoked for all child nodes - * stored in properties. If a 'cbNodes' callback is specified, it is invoked for embedded arrays; additionally, - * unlike `forEachChild`, embedded arrays are flattened and the 'cbNode' callback is invoked for each element. - * If a callback returns a truthy value, iteration stops and that value is returned. Otherwise, undefined is returned. - * - * @param node a given node to visit its children - * @param cbNode a callback to be invoked for all child nodes - * @param cbNodes a callback to be invoked for embedded array - * - * @remarks Unlike `forEachChild`, `forEachChildRecursively` handles recursively invoking the traversal on each child node found, - * and while doing so, handles traversing the structure without relying on the callstack to encode the tree structure. - */ - export function forEachChildRecursively(rootNode: Node, cbNode: (node: Node, parent: Node) => T | "skip" | undefined, cbNodes?: (nodes: NodeArray, parent: Node) => T | "skip" | undefined): T | undefined { - - const stack: Node[] = [rootNode]; - while (stack.length) { - const parent = stack.pop()!; - const res = visitAllPossibleChildren(parent, gatherPossibleChildren(parent)); - if (res) { - return res; - } + } + return; + function gatherPossibleChildren(node: Node) { + const children: (Node | NodeArray)[] = []; + forEachChild(node, addWorkItem, addWorkItem); // By using a stack above and `unshift` here, we emulate a depth-first preorder traversal + return children; + function addWorkItem(n: Node | NodeArray) { + children.unshift(n); } - - return; - - function gatherPossibleChildren(node: Node) { - const children: (Node | NodeArray)[] = []; - forEachChild(node, addWorkItem, addWorkItem); // By using a stack above and `unshift` here, we emulate a depth-first preorder traversal - return children; - - function addWorkItem(n: Node | NodeArray) { - children.unshift(n); - } - } - - function visitAllPossibleChildren(parent: Node, children: readonly (Node | NodeArray)[]) { - for (const child of children) { - if (isArray(child)) { - if (cbNodes) { - const res = cbNodes(child, parent); - if (res) { - if (res === "skip") continue; - return res; - } - } - - for (let i = child.length - 1; i >= 0; i--) { - const realChild = child[i]; - const res = cbNode(realChild, parent); - if (res) { - if (res === "skip") continue; - return res; - } - stack.push(realChild); + } + function visitAllPossibleChildren(parent: Node, children: readonly (Node | NodeArray)[]) { + for (const child of children) { + if (isArray(child)) { + if (cbNodes) { + const res = cbNodes(child, parent); + if (res) { + if (res === "skip") + continue; + return res; } } - else { - stack.push(child); - const res = cbNode(child, parent); + for (let i = child.length - 1; i >= 0; i--) { + const realChild = child[i]; + const res = cbNode(realChild, parent); if (res) { - if (res === "skip") continue; + if (res === "skip") + continue; return res; } + stack.push(realChild); + } + } + else { + stack.push(child); + const res = cbNode(child, parent); + if (res) { + if (res === "skip") + continue; + return res; } } } } - - export function createSourceFile(fileName: string, sourceText: string, languageVersion: ScriptTarget, setParentNodes = false, scriptKind?: ScriptKind): SourceFile { - performance.mark("beforeParse"); - let result: SourceFile; - - perfLogger.logStartParseSourceFile(fileName); - if (languageVersion === ScriptTarget.JSON) { - result = Parser.parseSourceFile(fileName, sourceText, languageVersion, /*syntaxCursor*/ undefined, setParentNodes, ScriptKind.JSON); - } - else { - result = Parser.parseSourceFile(fileName, sourceText, languageVersion, /*syntaxCursor*/ undefined, setParentNodes, scriptKind); - } - perfLogger.logStopParseSourceFile(); - - performance.mark("afterParse"); - performance.measure("Parse", "beforeParse", "afterParse"); - return result; +} +export function createSourceFile(fileName: string, sourceText: string, languageVersion: ScriptTarget, setParentNodes = false, scriptKind?: ScriptKind): SourceFile { + mark("beforeParse"); + let result: SourceFile; + perfLogger.logStartParseSourceFile(fileName); + if (languageVersion === ScriptTarget.JSON) { + result = Parser.parseSourceFile(fileName, sourceText, languageVersion, /*syntaxCursor*/ undefined, setParentNodes, ScriptKind.JSON); } - - export function parseIsolatedEntityName(text: string, languageVersion: ScriptTarget): EntityName | undefined { - return Parser.parseIsolatedEntityName(text, languageVersion); + else { + result = Parser.parseSourceFile(fileName, sourceText, languageVersion, /*syntaxCursor*/ undefined, setParentNodes, scriptKind); } - - /** - * Parse json text into SyntaxTree and return node and parse errors if any - * @param fileName - * @param sourceText - */ - export function parseJsonText(fileName: string, sourceText: string): JsonSourceFile { - return Parser.parseJsonText(fileName, sourceText); - } - - // See also `isExternalOrCommonJsModule` in utilities.ts - export function isExternalModule(file: SourceFile): boolean { - return file.externalModuleIndicator !== undefined; - } - - // Produces a new SourceFile for the 'newText' provided. The 'textChangeRange' parameter - // indicates what changed between the 'text' that this SourceFile has and the 'newText'. - // The SourceFile will be created with the compiler attempting to reuse as many nodes from - // this file as possible. - // - // Note: this function mutates nodes from this SourceFile. That means any existing nodes - // from this SourceFile that are being held onto may change as a result (including - // becoming detached from any SourceFile). It is recommended that this SourceFile not - // be used once 'update' is called on it. - export function updateSourceFile(sourceFile: SourceFile, newText: string, textChangeRange: TextChangeRange, aggressiveChecks = false): SourceFile { - const newSourceFile = IncrementalParser.updateSourceFile(sourceFile, newText, textChangeRange, aggressiveChecks); - // Because new source file node is created, it may not have the flag PossiblyContainDynamicImport. This is the case if there is no new edit to add dynamic import. - // We will manually port the flag to the new source file. - newSourceFile.flags |= (sourceFile.flags & NodeFlags.PermanentlySetIncrementalFlags); - return newSourceFile; - } - - /* @internal */ - export function parseIsolatedJSDocComment(content: string, start?: number, length?: number) { - const result = Parser.JSDocParser.parseIsolatedJSDocComment(content, start, length); - if (result && result.jsDoc) { - // because the jsDocComment was parsed out of the source file, it might - // not be covered by the fixupParentReferences. - Parser.fixupParentReferences(result.jsDoc); - } - - return result; + perfLogger.logStopParseSourceFile(); + mark("afterParse"); + measure("Parse", "beforeParse", "afterParse"); + return result; +} +export function parseIsolatedEntityName(text: string, languageVersion: ScriptTarget): EntityName | undefined { + return Parser.parseIsolatedEntityName(text, languageVersion); +} +/** + * Parse json text into SyntaxTree and return node and parse errors if any + * @param fileName + * @param sourceText + */ +export function parseJsonText(fileName: string, sourceText: string): JsonSourceFile { + return Parser.parseJsonText(fileName, sourceText); +} +// See also `isExternalOrCommonJsModule` in utilities.ts +export function isExternalModule(file: SourceFile): boolean { + return file.externalModuleIndicator !== undefined; +} +// Produces a new SourceFile for the 'newText' provided. The 'textChangeRange' parameter +// indicates what changed between the 'text' that this SourceFile has and the 'newText'. +// The SourceFile will be created with the compiler attempting to reuse as many nodes from +// this file as possible. +// +// Note: this function mutates nodes from this SourceFile. That means any existing nodes +// from this SourceFile that are being held onto may change as a result (including +// becoming detached from any SourceFile). It is recommended that this SourceFile not +// be used once 'update' is called on it. +export function updateSourceFile(sourceFile: SourceFile, newText: string, textChangeRange: TextChangeRange, aggressiveChecks = false): SourceFile { + const newSourceFile = IncrementalParser.updateSourceFile(sourceFile, newText, textChangeRange, aggressiveChecks); + // Because new source file node is created, it may not have the flag PossiblyContainDynamicImport. This is the case if there is no new edit to add dynamic import. + // We will manually port the flag to the new source file. + newSourceFile.flags |= (sourceFile.flags & NodeFlags.PermanentlySetIncrementalFlags); + return newSourceFile; +} +/* @internal */ +export function parseIsolatedJSDocComment(content: string, start?: number, length?: number) { + const result = Parser.JSDocParser.parseIsolatedJSDocComment(content, start, length); + if (result && result.jsDoc) { + // because the jsDocComment was parsed out of the source file, it might + // not be covered by the fixupParentReferences. + Parser.fixupParentReferences(result.jsDoc); } - - /* @internal */ - // Exposed only for testing. - export function parseJSDocTypeExpressionForTests(content: string, start?: number, length?: number) { - return Parser.JSDocParser.parseJSDocTypeExpressionForTests(content, start, length); - } - - // Implement the parser as a singleton module. We do this for perf reasons because creating - // parser instances can actually be expensive enough to impact us on projects with many source - // files. - namespace Parser { - // Share a single scanner across all calls to parse a source file. This helps speed things - // up by avoiding the cost of creating/compiling scanners over and over again. - const scanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ true); - const disallowInAndDecoratorContext = NodeFlags.DisallowInContext | NodeFlags.DecoratorContext; - - // capture constructors in 'initializeState' to avoid null checks - let NodeConstructor: new (kind: SyntaxKind, pos: number, end: number) => Node; - let TokenConstructor: new (kind: SyntaxKind, pos: number, end: number) => Node; - let IdentifierConstructor: new (kind: SyntaxKind, pos: number, end: number) => Node; - let PrivateIdentifierConstructor: new (kind: SyntaxKind, pos: number, end: number) => Node; - let SourceFileConstructor: new (kind: SyntaxKind, pos: number, end: number) => Node; - - let sourceFile: SourceFile; - let parseDiagnostics: DiagnosticWithLocation[]; - let syntaxCursor: IncrementalParser.SyntaxCursor | undefined; - - let currentToken: SyntaxKind; - let sourceText: string; - let nodeCount: number; - let identifiers: Map; - let privateIdentifiers: Map; - let identifierCount: number; - - let parsingContext: ParsingContext; - - let notParenthesizedArrow: Map | undefined; - - // Flags that dictate what parsing context we're in. For example: - // Whether or not we are in strict parsing mode. All that changes in strict parsing mode is - // that some tokens that would be considered identifiers may be considered keywords. - // - // When adding more parser context flags, consider which is the more common case that the - // flag will be in. This should be the 'false' state for that flag. The reason for this is - // that we don't store data in our nodes unless the value is in the *non-default* state. So, - // for example, more often than code 'allows-in' (or doesn't 'disallow-in'). We opt for - // 'disallow-in' set to 'false'. Otherwise, if we had 'allowsIn' set to 'true', then almost - // all nodes would need extra state on them to store this info. - // - // Note: 'allowIn' and 'allowYield' track 1:1 with the [in] and [yield] concepts in the ES6 - // grammar specification. - // - // An important thing about these context concepts. By default they are effectively inherited - // while parsing through every grammar production. i.e. if you don't change them, then when - // you parse a sub-production, it will have the same context values as the parent production. - // This is great most of the time. After all, consider all the 'expression' grammar productions - // and how nearly all of them pass along the 'in' and 'yield' context values: - // - // EqualityExpression[In, Yield] : - // RelationalExpression[?In, ?Yield] - // EqualityExpression[?In, ?Yield] == RelationalExpression[?In, ?Yield] - // EqualityExpression[?In, ?Yield] != RelationalExpression[?In, ?Yield] - // EqualityExpression[?In, ?Yield] === RelationalExpression[?In, ?Yield] - // EqualityExpression[?In, ?Yield] !== RelationalExpression[?In, ?Yield] - // - // Where you have to be careful is then understanding what the points are in the grammar - // where the values are *not* passed along. For example: - // - // SingleNameBinding[Yield,GeneratorParameter] - // [+GeneratorParameter]BindingIdentifier[Yield] Initializer[In]opt - // [~GeneratorParameter]BindingIdentifier[?Yield]Initializer[In, ?Yield]opt - // - // Here this is saying that if the GeneratorParameter context flag is set, that we should - // explicitly set the 'yield' context flag to false before calling into the BindingIdentifier - // and we should explicitly unset the 'yield' context flag before calling into the Initializer. - // production. Conversely, if the GeneratorParameter context flag is not set, then we - // should leave the 'yield' context flag alone. - // - // Getting this all correct is tricky and requires careful reading of the grammar to - // understand when these values should be changed versus when they should be inherited. - // - // Note: it should not be necessary to save/restore these flags during speculative/lookahead - // parsing. These context flags are naturally stored and restored through normal recursive - // descent parsing and unwinding. - let contextFlags: NodeFlags; - - // Whether or not we've had a parse error since creating the last AST node. If we have - // encountered an error, it will be stored on the next AST node we create. Parse errors - // can be broken down into three categories: - // - // 1) An error that occurred during scanning. For example, an unterminated literal, or a - // character that was completely not understood. - // - // 2) A token was expected, but was not present. This type of error is commonly produced - // by the 'parseExpected' function. - // - // 3) A token was present that no parsing function was able to consume. This type of error - // only occurs in the 'abortParsingListOrMoveToNextToken' function when the parser - // decides to skip the token. - // - // In all of these cases, we want to mark the next node as having had an error before it. - // With this mark, we can know in incremental settings if this node can be reused, or if - // we have to reparse it. If we don't keep this information around, we may just reuse the - // node. in that event we would then not produce the same errors as we did before, causing - // significant confusion problems. - // - // Note: it is necessary that this value be saved/restored during speculative/lookahead - // parsing. During lookahead parsing, we will often create a node. That node will have - // this value attached, and then this value will be set back to 'false'. If we decide to - // rewind, we must get back to the same value we had prior to the lookahead. - // - // Note: any errors at the end of the file that do not precede a regular node, should get - // attached to the EOF token. - let parseErrorBeforeNextFinishedNode = false; - - export function parseSourceFile(fileName: string, sourceText: string, languageVersion: ScriptTarget, syntaxCursor: IncrementalParser.SyntaxCursor | undefined, setParentNodes = false, scriptKind?: ScriptKind): SourceFile { - scriptKind = ensureScriptKind(fileName, scriptKind); - if (scriptKind === ScriptKind.JSON) { - const result = parseJsonText(fileName, sourceText, languageVersion, syntaxCursor, setParentNodes); - convertToObjectWorker(result, result.parseDiagnostics, /*returnValue*/ false, /*knownRootOptions*/ undefined, /*jsonConversionNotifier*/ undefined); - result.referencedFiles = emptyArray; - result.typeReferenceDirectives = emptyArray; - result.libReferenceDirectives = emptyArray; - result.amdDependencies = emptyArray; - result.hasNoDefaultLib = false; - result.pragmas = emptyMap; - return result; - } - - initializeState(sourceText, languageVersion, syntaxCursor, scriptKind); - - const result = parseSourceFileWorker(fileName, languageVersion, setParentNodes, scriptKind); - - clearState(); - + return result; +} +/* @internal */ +// Exposed only for testing. +export function parseJSDocTypeExpressionForTests(content: string, start?: number, length?: number) { + return Parser.JSDocParser.parseJSDocTypeExpressionForTests(content, start, length); +} +// Implement the parser as a singleton module. We do this for perf reasons because creating +// parser instances can actually be expensive enough to impact us on projects with many source +// files. +namespace Parser { + // Share a single scanner across all calls to parse a source file. This helps speed things + // up by avoiding the cost of creating/compiling scanners over and over again. + const scanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ true); + const disallowInAndDecoratorContext = NodeFlags.DisallowInContext | NodeFlags.DecoratorContext; + // capture constructors in 'initializeState' to avoid null checks + let NodeConstructor: new (kind: SyntaxKind, pos: number, end: number) => Node; + let TokenConstructor: new (kind: SyntaxKind, pos: number, end: number) => Node; + let IdentifierConstructor: new (kind: SyntaxKind, pos: number, end: number) => Node; + let PrivateIdentifierConstructor: new (kind: SyntaxKind, pos: number, end: number) => Node; + let SourceFileConstructor: new (kind: SyntaxKind, pos: number, end: number) => Node; + let sourceFile: SourceFile; + let parseDiagnostics: DiagnosticWithLocation[]; + let syntaxCursor: IncrementalParser.SyntaxCursor | undefined; + let currentToken: SyntaxKind; + let sourceText: string; + let nodeCount: number; + let identifiers: ts.Map; + let privateIdentifiers: ts.Map; + let identifierCount: number; + let parsingContext: ParsingContext; + let notParenthesizedArrow: ts.Map | undefined; + // Flags that dictate what parsing context we're in. For example: + // Whether or not we are in strict parsing mode. All that changes in strict parsing mode is + // that some tokens that would be considered identifiers may be considered keywords. + // + // When adding more parser context flags, consider which is the more common case that the + // flag will be in. This should be the 'false' state for that flag. The reason for this is + // that we don't store data in our nodes unless the value is in the *non-default* state. So, + // for example, more often than code 'allows-in' (or doesn't 'disallow-in'). We opt for + // 'disallow-in' set to 'false'. Otherwise, if we had 'allowsIn' set to 'true', then almost + // all nodes would need extra state on them to store this info. + // + // Note: 'allowIn' and 'allowYield' track 1:1 with the [in] and [yield] concepts in the ES6 + // grammar specification. + // + // An important thing about these context concepts. By default they are effectively inherited + // while parsing through every grammar production. i.e. if you don't change them, then when + // you parse a sub-production, it will have the same context values as the parent production. + // This is great most of the time. After all, consider all the 'expression' grammar productions + // and how nearly all of them pass along the 'in' and 'yield' context values: + // + // EqualityExpression[In, Yield] : + // RelationalExpression[?In, ?Yield] + // EqualityExpression[?In, ?Yield] == RelationalExpression[?In, ?Yield] + // EqualityExpression[?In, ?Yield] != RelationalExpression[?In, ?Yield] + // EqualityExpression[?In, ?Yield] === RelationalExpression[?In, ?Yield] + // EqualityExpression[?In, ?Yield] !== RelationalExpression[?In, ?Yield] + // + // Where you have to be careful is then understanding what the points are in the grammar + // where the values are *not* passed along. For example: + // + // SingleNameBinding[Yield,GeneratorParameter] + // [+GeneratorParameter]BindingIdentifier[Yield] Initializer[In]opt + // [~GeneratorParameter]BindingIdentifier[?Yield]Initializer[In, ?Yield]opt + // + // Here this is saying that if the GeneratorParameter context flag is set, that we should + // explicitly set the 'yield' context flag to false before calling into the BindingIdentifier + // and we should explicitly unset the 'yield' context flag before calling into the Initializer. + // production. Conversely, if the GeneratorParameter context flag is not set, then we + // should leave the 'yield' context flag alone. + // + // Getting this all correct is tricky and requires careful reading of the grammar to + // understand when these values should be changed versus when they should be inherited. + // + // Note: it should not be necessary to save/restore these flags during speculative/lookahead + // parsing. These context flags are naturally stored and restored through normal recursive + // descent parsing and unwinding. + let contextFlags: NodeFlags; + // Whether or not we've had a parse error since creating the last AST node. If we have + // encountered an error, it will be stored on the next AST node we create. Parse errors + // can be broken down into three categories: + // + // 1) An error that occurred during scanning. For example, an unterminated literal, or a + // character that was completely not understood. + // + // 2) A token was expected, but was not present. This type of error is commonly produced + // by the 'parseExpected' function. + // + // 3) A token was present that no parsing function was able to consume. This type of error + // only occurs in the 'abortParsingListOrMoveToNextToken' function when the parser + // decides to skip the token. + // + // In all of these cases, we want to mark the next node as having had an error before it. + // With this mark, we can know in incremental settings if this node can be reused, or if + // we have to reparse it. If we don't keep this information around, we may just reuse the + // node. in that event we would then not produce the same errors as we did before, causing + // significant confusion problems. + // + // Note: it is necessary that this value be saved/restored during speculative/lookahead + // parsing. During lookahead parsing, we will often create a node. That node will have + // this value attached, and then this value will be set back to 'false'. If we decide to + // rewind, we must get back to the same value we had prior to the lookahead. + // + // Note: any errors at the end of the file that do not precede a regular node, should get + // attached to the EOF token. + let parseErrorBeforeNextFinishedNode = false; + export function parseSourceFile(fileName: string, sourceText: string, languageVersion: ScriptTarget, syntaxCursor: IncrementalParser.SyntaxCursor | undefined, setParentNodes = false, scriptKind?: ScriptKind): SourceFile { + scriptKind = ensureScriptKind(fileName, scriptKind); + if (scriptKind === ScriptKind.JSON) { + const result = parseJsonText(fileName, sourceText, languageVersion, syntaxCursor, setParentNodes); + convertToObjectWorker(result, result.parseDiagnostics, /*returnValue*/ false, /*knownRootOptions*/ undefined, /*jsonConversionNotifier*/ undefined); + result.referencedFiles = emptyArray; + result.typeReferenceDirectives = emptyArray; + result.libReferenceDirectives = emptyArray; + result.amdDependencies = emptyArray; + result.hasNoDefaultLib = false; + result.pragmas = emptyMap; return result; } - - export function parseIsolatedEntityName(content: string, languageVersion: ScriptTarget): EntityName | undefined { - // Choice of `isDeclarationFile` should be arbitrary - initializeState(content, languageVersion, /*syntaxCursor*/ undefined, ScriptKind.JS); - // Prime the scanner. - nextToken(); - const entityName = parseEntityName(/*allowReservedWords*/ true); - const isInvalid = token() === SyntaxKind.EndOfFileToken && !parseDiagnostics.length; - clearState(); - return isInvalid ? entityName : undefined; - } - - export function parseJsonText(fileName: string, sourceText: string, languageVersion: ScriptTarget = ScriptTarget.ES2015, syntaxCursor?: IncrementalParser.SyntaxCursor, setParentNodes?: boolean): JsonSourceFile { - initializeState(sourceText, languageVersion, syntaxCursor, ScriptKind.JSON); - // Set source file so that errors will be reported with this file name - sourceFile = createSourceFile(fileName, ScriptTarget.ES2015, ScriptKind.JSON, /*isDeclaration*/ false); - sourceFile.flags = contextFlags; - - // Prime the scanner. - nextToken(); - const pos = getNodePos(); - if (token() === SyntaxKind.EndOfFileToken) { - sourceFile.statements = createNodeArray([], pos, pos); - sourceFile.endOfFileToken = parseTokenNode(); - } - else { - const statement = createNode(SyntaxKind.ExpressionStatement) as JsonObjectExpressionStatement; - switch (token()) { - case SyntaxKind.OpenBracketToken: - statement.expression = parseArrayLiteralExpression(); - break; - case SyntaxKind.TrueKeyword: - case SyntaxKind.FalseKeyword: - case SyntaxKind.NullKeyword: - statement.expression = parseTokenNode(); - break; - case SyntaxKind.MinusToken: - if (lookAhead(() => nextToken() === SyntaxKind.NumericLiteral && nextToken() !== SyntaxKind.ColonToken)) { - statement.expression = parsePrefixUnaryExpression() as JsonMinusNumericLiteral; - } - else { - statement.expression = parseObjectLiteralExpression(); - } - break; - case SyntaxKind.NumericLiteral: - case SyntaxKind.StringLiteral: - if (lookAhead(() => nextToken() !== SyntaxKind.ColonToken)) { - statement.expression = parseLiteralNode() as StringLiteral | NumericLiteral; - break; - } - // falls through - default: - statement.expression = parseObjectLiteralExpression(); - break; - } - finishNode(statement); - sourceFile.statements = createNodeArray([statement], pos); - sourceFile.endOfFileToken = parseExpectedToken(SyntaxKind.EndOfFileToken, Diagnostics.Unexpected_token); - } - - if (setParentNodes) { - fixupParentReferences(sourceFile); - } - - sourceFile.nodeCount = nodeCount; - sourceFile.identifierCount = identifierCount; - sourceFile.identifiers = identifiers; - sourceFile.parseDiagnostics = parseDiagnostics; - - const result = sourceFile as JsonSourceFile; - clearState(); - return result; + initializeState(sourceText, languageVersion, syntaxCursor, scriptKind); + const result = parseSourceFileWorker(fileName, languageVersion, setParentNodes, scriptKind); + clearState(); + return result; + } + export function parseIsolatedEntityName(content: string, languageVersion: ScriptTarget): EntityName | undefined { + // Choice of `isDeclarationFile` should be arbitrary + initializeState(content, languageVersion, /*syntaxCursor*/ undefined, ScriptKind.JS); + // Prime the scanner. + nextToken(); + const entityName = parseEntityName(/*allowReservedWords*/ true); + const isInvalid = token() === SyntaxKind.EndOfFileToken && !parseDiagnostics.length; + clearState(); + return isInvalid ? entityName : undefined; + } + export function parseJsonText(fileName: string, sourceText: string, languageVersion: ScriptTarget = ScriptTarget.ES2015, syntaxCursor?: IncrementalParser.SyntaxCursor, setParentNodes?: boolean): JsonSourceFile { + initializeState(sourceText, languageVersion, syntaxCursor, ScriptKind.JSON); + // Set source file so that errors will be reported with this file name + sourceFile = createSourceFile(fileName, ScriptTarget.ES2015, ScriptKind.JSON, /*isDeclaration*/ false); + sourceFile.flags = contextFlags; + // Prime the scanner. + nextToken(); + const pos = getNodePos(); + if (token() === SyntaxKind.EndOfFileToken) { + sourceFile.statements = createNodeArray([], pos, pos); + sourceFile.endOfFileToken = parseTokenNode(); } - - function getLanguageVariant(scriptKind: ScriptKind) { - // .tsx and .jsx files are treated as jsx language variant. - return scriptKind === ScriptKind.TSX || scriptKind === ScriptKind.JSX || scriptKind === ScriptKind.JS || scriptKind === ScriptKind.JSON ? LanguageVariant.JSX : LanguageVariant.Standard; - } - - function initializeState(_sourceText: string, languageVersion: ScriptTarget, _syntaxCursor: IncrementalParser.SyntaxCursor | undefined, scriptKind: ScriptKind) { - NodeConstructor = objectAllocator.getNodeConstructor(); - TokenConstructor = objectAllocator.getTokenConstructor(); - IdentifierConstructor = objectAllocator.getIdentifierConstructor(); - PrivateIdentifierConstructor = objectAllocator.getPrivateIdentifierConstructor(); - SourceFileConstructor = objectAllocator.getSourceFileConstructor(); - - sourceText = _sourceText; - syntaxCursor = _syntaxCursor; - - parseDiagnostics = []; - parsingContext = 0; - identifiers = createMap(); - privateIdentifiers = createMap(); - identifierCount = 0; - nodeCount = 0; - - switch (scriptKind) { - case ScriptKind.JS: - case ScriptKind.JSX: - contextFlags = NodeFlags.JavaScriptFile; + else { + const statement = (createNode(SyntaxKind.ExpressionStatement) as JsonObjectExpressionStatement); + switch (token()) { + case SyntaxKind.OpenBracketToken: + statement.expression = parseArrayLiteralExpression(); + break; + case SyntaxKind.TrueKeyword: + case SyntaxKind.FalseKeyword: + case SyntaxKind.NullKeyword: + statement.expression = parseTokenNode(); break; - case ScriptKind.JSON: - contextFlags = NodeFlags.JavaScriptFile | NodeFlags.JsonFile; + case SyntaxKind.MinusToken: + if (lookAhead(() => nextToken() === SyntaxKind.NumericLiteral && nextToken() !== SyntaxKind.ColonToken)) { + statement.expression = (parsePrefixUnaryExpression() as JsonMinusNumericLiteral); + } + else { + statement.expression = parseObjectLiteralExpression(); + } break; + case SyntaxKind.NumericLiteral: + case SyntaxKind.StringLiteral: + if (lookAhead(() => nextToken() !== SyntaxKind.ColonToken)) { + statement.expression = (parseLiteralNode() as StringLiteral | NumericLiteral); + break; + } + // falls through default: - contextFlags = NodeFlags.None; + statement.expression = parseObjectLiteralExpression(); break; } - parseErrorBeforeNextFinishedNode = false; - - // Initialize and prime the scanner before parsing the source elements. - scanner.setText(sourceText); - scanner.setOnError(scanError); - scanner.setScriptTarget(languageVersion); - scanner.setLanguageVariant(getLanguageVariant(scriptKind)); - } - - function clearState() { - // Clear out the text the scanner is pointing at, so it doesn't keep anything alive unnecessarily. - scanner.setText(""); - scanner.setOnError(undefined); - - // Clear any data. We don't want to accidentally hold onto it for too long. - parseDiagnostics = undefined!; - sourceFile = undefined!; - identifiers = undefined!; - syntaxCursor = undefined; - sourceText = undefined!; - notParenthesizedArrow = undefined!; - } - - function parseSourceFileWorker(fileName: string, languageVersion: ScriptTarget, setParentNodes: boolean, scriptKind: ScriptKind): SourceFile { - const isDeclarationFile = isDeclarationFileName(fileName); - if (isDeclarationFile) { - contextFlags |= NodeFlags.Ambient; - } - - sourceFile = createSourceFile(fileName, languageVersion, scriptKind, isDeclarationFile); - sourceFile.flags = contextFlags; - - // Prime the scanner. - nextToken(); - // A member of ReadonlyArray isn't assignable to a member of T[] (and prevents a direct cast) - but this is where we set up those members so they can be readonly in the future - processCommentPragmas(sourceFile as {} as PragmaContext, sourceText); - processPragmasIntoFields(sourceFile as {} as PragmaContext, reportPragmaDiagnostic); - - sourceFile.statements = parseList(ParsingContext.SourceElements, parseStatement); - Debug.assert(token() === SyntaxKind.EndOfFileToken); - sourceFile.endOfFileToken = addJSDocComment(parseTokenNode()); - - setExternalModuleIndicator(sourceFile); - - sourceFile.nodeCount = nodeCount; - sourceFile.identifierCount = identifierCount; - sourceFile.identifiers = identifiers; - sourceFile.parseDiagnostics = parseDiagnostics; - - if (setParentNodes) { - fixupParentReferences(sourceFile); - } - - return sourceFile; - - function reportPragmaDiagnostic(pos: number, end: number, diagnostic: DiagnosticMessage) { - parseDiagnostics.push(createFileDiagnostic(sourceFile, pos, end, diagnostic)); - } + finishNode(statement); + sourceFile.statements = createNodeArray([statement], pos); + sourceFile.endOfFileToken = parseExpectedToken(SyntaxKind.EndOfFileToken, Diagnostics.Unexpected_token); } - - function addJSDocComment(node: T): T { - Debug.assert(!node.jsDoc); // Should only be called once per node - const jsDoc = mapDefined(getJSDocCommentRanges(node, sourceFile.text), comment => JSDocParser.parseJSDocComment(node, comment.pos, comment.end - comment.pos)); - if (jsDoc.length) node.jsDoc = jsDoc; - return node; + if (setParentNodes) { + fixupParentReferences(sourceFile); } - - export function fixupParentReferences(rootNode: Node) { - // normally parent references are set during binding. However, for clients that only need - // a syntax tree, and no semantic features, then the binding process is an unnecessary - // overhead. This functions allows us to set all the parents, without all the expense of - // binding. - forEachChildRecursively(rootNode, bindParentToChild); - - function bindParentToChild(child: Node, parent: Node) { - child.parent = parent; - if (hasJSDocNodes(child)) { - for (const doc of child.jsDoc!) { - bindParentToChild(doc, child); - forEachChildRecursively(doc, bindParentToChild); - } + sourceFile.nodeCount = nodeCount; + sourceFile.identifierCount = identifierCount; + sourceFile.identifiers = identifiers; + sourceFile.parseDiagnostics = parseDiagnostics; + const result = (sourceFile as JsonSourceFile); + clearState(); + return result; + } + function getLanguageVariant(scriptKind: ScriptKind) { + // .tsx and .jsx files are treated as jsx language variant. + return scriptKind === ScriptKind.TSX || scriptKind === ScriptKind.JSX || scriptKind === ScriptKind.JS || scriptKind === ScriptKind.JSON ? LanguageVariant.JSX : LanguageVariant.Standard; + } + function initializeState(_sourceText: string, languageVersion: ScriptTarget, _syntaxCursor: IncrementalParser.SyntaxCursor | undefined, scriptKind: ScriptKind) { + NodeConstructor = objectAllocator.getNodeConstructor(); + TokenConstructor = objectAllocator.getTokenConstructor(); + IdentifierConstructor = objectAllocator.getIdentifierConstructor(); + PrivateIdentifierConstructor = objectAllocator.getPrivateIdentifierConstructor(); + SourceFileConstructor = objectAllocator.getSourceFileConstructor(); + sourceText = _sourceText; + syntaxCursor = _syntaxCursor; + parseDiagnostics = []; + parsingContext = 0; + identifiers = createMap(); + privateIdentifiers = createMap(); + identifierCount = 0; + nodeCount = 0; + switch (scriptKind) { + case ScriptKind.JS: + case ScriptKind.JSX: + contextFlags = NodeFlags.JavaScriptFile; + break; + case ScriptKind.JSON: + contextFlags = NodeFlags.JavaScriptFile | NodeFlags.JsonFile; + break; + default: + contextFlags = NodeFlags.None; + break; + } + parseErrorBeforeNextFinishedNode = false; + // Initialize and prime the scanner before parsing the source elements. + scanner.setText(sourceText); + scanner.setOnError(scanError); + scanner.setScriptTarget(languageVersion); + scanner.setLanguageVariant(getLanguageVariant(scriptKind)); + } + function clearState() { + // Clear out the text the scanner is pointing at, so it doesn't keep anything alive unnecessarily. + scanner.setText(""); + scanner.setOnError(undefined); + // Clear any data. We don't want to accidentally hold onto it for too long. + parseDiagnostics = undefined!; + sourceFile = undefined!; + identifiers = undefined!; + syntaxCursor = undefined; + sourceText = undefined!; + notParenthesizedArrow = undefined!; + } + function parseSourceFileWorker(fileName: string, languageVersion: ScriptTarget, setParentNodes: boolean, scriptKind: ScriptKind): SourceFile { + const isDeclarationFile = isDeclarationFileName(fileName); + if (isDeclarationFile) { + contextFlags |= NodeFlags.Ambient; + } + sourceFile = createSourceFile(fileName, languageVersion, scriptKind, isDeclarationFile); + sourceFile.flags = contextFlags; + // Prime the scanner. + nextToken(); + // A member of ReadonlyArray isn't assignable to a member of T[] (and prevents a direct cast) - but this is where we set up those members so they can be readonly in the future + processCommentPragmas(sourceFile as {} as PragmaContext, sourceText); + processPragmasIntoFields(sourceFile as {} as PragmaContext, reportPragmaDiagnostic); + sourceFile.statements = parseList(ParsingContext.SourceElements, parseStatement); + Debug.assert(token() === SyntaxKind.EndOfFileToken); + sourceFile.endOfFileToken = addJSDocComment(parseTokenNode()); + setExternalModuleIndicator(sourceFile); + sourceFile.nodeCount = nodeCount; + sourceFile.identifierCount = identifierCount; + sourceFile.identifiers = identifiers; + sourceFile.parseDiagnostics = parseDiagnostics; + if (setParentNodes) { + fixupParentReferences(sourceFile); + } + return sourceFile; + function reportPragmaDiagnostic(pos: number, end: number, diagnostic: DiagnosticMessage) { + parseDiagnostics.push(createFileDiagnostic(sourceFile, pos, end, diagnostic)); + } + } + function addJSDocComment(node: T): T { + Debug.assert(!node.jsDoc); // Should only be called once per node + const jsDoc = mapDefined(getJSDocCommentRanges(node, sourceFile.text), comment => JSDocParser.parseJSDocComment(node, comment.pos, comment.end - comment.pos)); + if (jsDoc.length) + node.jsDoc = jsDoc; + return node; + } + export function fixupParentReferences(rootNode: Node) { + // normally parent references are set during binding. However, for clients that only need + // a syntax tree, and no semantic features, then the binding process is an unnecessary + // overhead. This functions allows us to set all the parents, without all the expense of + // binding. + forEachChildRecursively(rootNode, bindParentToChild); + function bindParentToChild(child: Node, parent: Node) { + child.parent = parent; + if (hasJSDocNodes(child)) { + for (const doc of child.jsDoc!) { + bindParentToChild(doc, child); + forEachChildRecursively(doc, bindParentToChild); } } } - - function createSourceFile(fileName: string, languageVersion: ScriptTarget, scriptKind: ScriptKind, isDeclarationFile: boolean): SourceFile { - // code from createNode is inlined here so createNode won't have to deal with special case of creating source files - // this is quite rare comparing to other nodes and createNode should be as fast as possible - const sourceFile = new SourceFileConstructor(SyntaxKind.SourceFile, /*pos*/ 0, /* end */ sourceText.length); - nodeCount++; - - sourceFile.text = sourceText; - sourceFile.bindDiagnostics = []; - sourceFile.bindSuggestionDiagnostics = undefined; - sourceFile.languageVersion = languageVersion; - sourceFile.fileName = normalizePath(fileName); - sourceFile.languageVariant = getLanguageVariant(scriptKind); - sourceFile.isDeclarationFile = isDeclarationFile; - sourceFile.scriptKind = scriptKind; - - return sourceFile; + } + function createSourceFile(fileName: string, languageVersion: ScriptTarget, scriptKind: ScriptKind, isDeclarationFile: boolean): SourceFile { + // code from createNode is inlined here so createNode won't have to deal with special case of creating source files + // this is quite rare comparing to other nodes and createNode should be as fast as possible + const sourceFile = (new SourceFileConstructor(SyntaxKind.SourceFile, /*pos*/ 0, /* end */ sourceText.length)); + nodeCount++; + sourceFile.text = sourceText; + sourceFile.bindDiagnostics = []; + sourceFile.bindSuggestionDiagnostics = undefined; + sourceFile.languageVersion = languageVersion; + sourceFile.fileName = normalizePath(fileName); + sourceFile.languageVariant = getLanguageVariant(scriptKind); + sourceFile.isDeclarationFile = isDeclarationFile; + sourceFile.scriptKind = scriptKind; + return sourceFile; + } + function setContextFlag(val: boolean, flag: NodeFlags) { + if (val) { + contextFlags |= flag; } - - function setContextFlag(val: boolean, flag: NodeFlags) { - if (val) { - contextFlags |= flag; - } - else { - contextFlags &= ~flag; - } - } - - function setDisallowInContext(val: boolean) { - setContextFlag(val, NodeFlags.DisallowInContext); - } - - function setYieldContext(val: boolean) { - setContextFlag(val, NodeFlags.YieldContext); - } - - function setDecoratorContext(val: boolean) { - setContextFlag(val, NodeFlags.DecoratorContext); - } - - function setAwaitContext(val: boolean) { - setContextFlag(val, NodeFlags.AwaitContext); - } - - function doOutsideOfContext(context: NodeFlags, func: () => T): T { - // contextFlagsToClear will contain only the context flags that are - // currently set that we need to temporarily clear - // We don't just blindly reset to the previous flags to ensure - // that we do not mutate cached flags for the incremental - // parser (ThisNodeHasError, ThisNodeOrAnySubNodesHasError, and - // HasAggregatedChildData). - const contextFlagsToClear = context & contextFlags; - if (contextFlagsToClear) { - // clear the requested context flags - setContextFlag(/*val*/ false, contextFlagsToClear); - const result = func(); - // restore the context flags we just cleared - setContextFlag(/*val*/ true, contextFlagsToClear); - return result; - } - - // no need to do anything special as we are not in any of the requested contexts - return func(); - } - - function doInsideOfContext(context: NodeFlags, func: () => T): T { - // contextFlagsToSet will contain only the context flags that - // are not currently set that we need to temporarily enable. - // We don't just blindly reset to the previous flags to ensure - // that we do not mutate cached flags for the incremental - // parser (ThisNodeHasError, ThisNodeOrAnySubNodesHasError, and - // HasAggregatedChildData). - const contextFlagsToSet = context & ~contextFlags; - if (contextFlagsToSet) { - // set the requested context flags - setContextFlag(/*val*/ true, contextFlagsToSet); - const result = func(); - // reset the context flags we just set - setContextFlag(/*val*/ false, contextFlagsToSet); - return result; - } - - // no need to do anything special as we are already in all of the requested contexts - return func(); - } - - function allowInAnd(func: () => T): T { - return doOutsideOfContext(NodeFlags.DisallowInContext, func); - } - - function disallowInAnd(func: () => T): T { - return doInsideOfContext(NodeFlags.DisallowInContext, func); - } - - function doInYieldContext(func: () => T): T { - return doInsideOfContext(NodeFlags.YieldContext, func); - } - - function doInDecoratorContext(func: () => T): T { - return doInsideOfContext(NodeFlags.DecoratorContext, func); - } - - function doInAwaitContext(func: () => T): T { - return doInsideOfContext(NodeFlags.AwaitContext, func); - } - - function doOutsideOfAwaitContext(func: () => T): T { - return doOutsideOfContext(NodeFlags.AwaitContext, func); - } - - function doInYieldAndAwaitContext(func: () => T): T { - return doInsideOfContext(NodeFlags.YieldContext | NodeFlags.AwaitContext, func); - } - - function doOutsideOfYieldAndAwaitContext(func: () => T): T { - return doOutsideOfContext(NodeFlags.YieldContext | NodeFlags.AwaitContext, func); - } - - function inContext(flags: NodeFlags) { - return (contextFlags & flags) !== 0; - } - - function inYieldContext() { - return inContext(NodeFlags.YieldContext); - } - - function inDisallowInContext() { - return inContext(NodeFlags.DisallowInContext); - } - - function inDecoratorContext() { - return inContext(NodeFlags.DecoratorContext); - } - - function inAwaitContext() { - return inContext(NodeFlags.AwaitContext); - } - - function parseErrorAtCurrentToken(message: DiagnosticMessage, arg0?: any): void { - parseErrorAt(scanner.getTokenPos(), scanner.getTextPos(), message, arg0); - } - - function parseErrorAtPosition(start: number, length: number, message: DiagnosticMessage, arg0?: any): void { - // Don't report another error if it would just be at the same position as the last error. - const lastError = lastOrUndefined(parseDiagnostics); - if (!lastError || start !== lastError.start) { - parseDiagnostics.push(createFileDiagnostic(sourceFile, start, length, message, arg0)); - } - - // Mark that we've encountered an error. We'll set an appropriate bit on the next - // node we finish so that it can't be reused incrementally. - parseErrorBeforeNextFinishedNode = true; - } - - function parseErrorAt(start: number, end: number, message: DiagnosticMessage, arg0?: any): void { - parseErrorAtPosition(start, end - start, message, arg0); - } - - function parseErrorAtRange(range: TextRange, message: DiagnosticMessage, arg0?: any): void { - parseErrorAt(range.pos, range.end, message, arg0); - } - - function scanError(message: DiagnosticMessage, length: number): void { - parseErrorAtPosition(scanner.getTextPos(), length, message); - } - - function getNodePos(): number { - return scanner.getStartPos(); - } - - // Use this function to access the current token instead of reading the currentToken - // variable. Since function results aren't narrowed in control flow analysis, this ensures - // that the type checker doesn't make wrong assumptions about the type of the current - // token (e.g. a call to nextToken() changes the current token but the checker doesn't - // reason about this side effect). Mainstream VMs inline simple functions like this, so - // there is no performance penalty. - function token(): SyntaxKind { - return currentToken; - } - - function nextTokenWithoutCheck() { - return currentToken = scanner.scan(); - } - - function nextToken(): SyntaxKind { - // if the keyword had an escape - if (isKeyword(currentToken) && (scanner.hasUnicodeEscape() || scanner.hasExtendedUnicodeEscape())) { - // issue a parse error for the escape - parseErrorAt(scanner.getTokenPos(), scanner.getTextPos(), Diagnostics.Keywords_cannot_contain_escape_characters); - } - return nextTokenWithoutCheck(); - } - - function nextTokenJSDoc(): JSDocSyntaxKind { - return currentToken = scanner.scanJsDocToken(); - } - - function reScanGreaterToken(): SyntaxKind { - return currentToken = scanner.reScanGreaterToken(); - } - - function reScanSlashToken(): SyntaxKind { - return currentToken = scanner.reScanSlashToken(); - } - - function reScanTemplateToken(isTaggedTemplate: boolean): SyntaxKind { - return currentToken = scanner.reScanTemplateToken(isTaggedTemplate); - } - - function reScanTemplateHeadOrNoSubstitutionTemplate(): SyntaxKind { - return currentToken = scanner.reScanTemplateHeadOrNoSubstitutionTemplate(); - } - - function reScanLessThanToken(): SyntaxKind { - return currentToken = scanner.reScanLessThanToken(); - } - - function scanJsxIdentifier(): SyntaxKind { - return currentToken = scanner.scanJsxIdentifier(); - } - - function scanJsxText(): SyntaxKind { - return currentToken = scanner.scanJsxToken(); - } - - function scanJsxAttributeValue(): SyntaxKind { - return currentToken = scanner.scanJsxAttributeValue(); - } - - function speculationHelper(callback: () => T, isLookAhead: boolean): T { - // Keep track of the state we'll need to rollback to if lookahead fails (or if the - // caller asked us to always reset our state). - const saveToken = currentToken; - const saveParseDiagnosticsLength = parseDiagnostics.length; - const saveParseErrorBeforeNextFinishedNode = parseErrorBeforeNextFinishedNode; - - // Note: it is not actually necessary to save/restore the context flags here. That's - // because the saving/restoring of these flags happens naturally through the recursive - // descent nature of our parser. However, we still store this here just so we can - // assert that invariant holds. - const saveContextFlags = contextFlags; - - // If we're only looking ahead, then tell the scanner to only lookahead as well. - // Otherwise, if we're actually speculatively parsing, then tell the scanner to do the - // same. - const result = isLookAhead - ? scanner.lookAhead(callback) - : scanner.tryScan(callback); - - Debug.assert(saveContextFlags === contextFlags); - - // If our callback returned something 'falsy' or we're just looking ahead, - // then unconditionally restore us to where we were. - if (!result || isLookAhead) { - currentToken = saveToken; - parseDiagnostics.length = saveParseDiagnosticsLength; - parseErrorBeforeNextFinishedNode = saveParseErrorBeforeNextFinishedNode; - } - + else { + contextFlags &= ~flag; + } + } + function setDisallowInContext(val: boolean) { + setContextFlag(val, NodeFlags.DisallowInContext); + } + function setYieldContext(val: boolean) { + setContextFlag(val, NodeFlags.YieldContext); + } + function setDecoratorContext(val: boolean) { + setContextFlag(val, NodeFlags.DecoratorContext); + } + function setAwaitContext(val: boolean) { + setContextFlag(val, NodeFlags.AwaitContext); + } + function doOutsideOfContext(context: NodeFlags, func: () => T): T { + // contextFlagsToClear will contain only the context flags that are + // currently set that we need to temporarily clear + // We don't just blindly reset to the previous flags to ensure + // that we do not mutate cached flags for the incremental + // parser (ThisNodeHasError, ThisNodeOrAnySubNodesHasError, and + // HasAggregatedChildData). + const contextFlagsToClear = context & contextFlags; + if (contextFlagsToClear) { + // clear the requested context flags + setContextFlag(/*val*/ false, contextFlagsToClear); + const result = func(); + // restore the context flags we just cleared + setContextFlag(/*val*/ true, contextFlagsToClear); return result; } - - /** Invokes the provided callback then unconditionally restores the parser to the state it - * was in immediately prior to invoking the callback. The result of invoking the callback - * is returned from this function. - */ - function lookAhead(callback: () => T): T { - return speculationHelper(callback, /*isLookAhead*/ true); - } - - /** Invokes the provided callback. If the callback returns something falsy, then it restores - * the parser to the state it was in immediately prior to invoking the callback. If the - * callback returns something truthy, then the parser state is not rolled back. The result - * of invoking the callback is returned from this function. - */ - function tryParse(callback: () => T): T { - return speculationHelper(callback, /*isLookAhead*/ false); + // no need to do anything special as we are not in any of the requested contexts + return func(); + } + function doInsideOfContext(context: NodeFlags, func: () => T): T { + // contextFlagsToSet will contain only the context flags that + // are not currently set that we need to temporarily enable. + // We don't just blindly reset to the previous flags to ensure + // that we do not mutate cached flags for the incremental + // parser (ThisNodeHasError, ThisNodeOrAnySubNodesHasError, and + // HasAggregatedChildData). + const contextFlagsToSet = context & ~contextFlags; + if (contextFlagsToSet) { + // set the requested context flags + setContextFlag(/*val*/ true, contextFlagsToSet); + const result = func(); + // reset the context flags we just set + setContextFlag(/*val*/ false, contextFlagsToSet); + return result; } - - // Ignore strict mode flag because we will report an error in type checker instead. - function isIdentifier(): boolean { - if (token() === SyntaxKind.Identifier) { - return true; - } - - // If we have a 'yield' keyword, and we're in the [yield] context, then 'yield' is - // considered a keyword and is not an identifier. - if (token() === SyntaxKind.YieldKeyword && inYieldContext()) { - return false; - } - - // If we have a 'await' keyword, and we're in the [Await] context, then 'await' is - // considered a keyword and is not an identifier. - if (token() === SyntaxKind.AwaitKeyword && inAwaitContext()) { - return false; - } - - return token() > SyntaxKind.LastReservedWord; + // no need to do anything special as we are already in all of the requested contexts + return func(); + } + function allowInAnd(func: () => T): T { + return doOutsideOfContext(NodeFlags.DisallowInContext, func); + } + function disallowInAnd(func: () => T): T { + return doInsideOfContext(NodeFlags.DisallowInContext, func); + } + function doInYieldContext(func: () => T): T { + return doInsideOfContext(NodeFlags.YieldContext, func); + } + function doInDecoratorContext(func: () => T): T { + return doInsideOfContext(NodeFlags.DecoratorContext, func); + } + function doInAwaitContext(func: () => T): T { + return doInsideOfContext(NodeFlags.AwaitContext, func); + } + function doOutsideOfAwaitContext(func: () => T): T { + return doOutsideOfContext(NodeFlags.AwaitContext, func); + } + function doInYieldAndAwaitContext(func: () => T): T { + return doInsideOfContext(NodeFlags.YieldContext | NodeFlags.AwaitContext, func); + } + function doOutsideOfYieldAndAwaitContext(func: () => T): T { + return doOutsideOfContext(NodeFlags.YieldContext | NodeFlags.AwaitContext, func); + } + function inContext(flags: NodeFlags) { + return (contextFlags & flags) !== 0; + } + function inYieldContext() { + return inContext(NodeFlags.YieldContext); + } + function inDisallowInContext() { + return inContext(NodeFlags.DisallowInContext); + } + function inDecoratorContext() { + return inContext(NodeFlags.DecoratorContext); + } + function inAwaitContext() { + return inContext(NodeFlags.AwaitContext); + } + function parseErrorAtCurrentToken(message: DiagnosticMessage, arg0?: any): void { + parseErrorAt(scanner.getTokenPos(), scanner.getTextPos(), message, arg0); + } + function parseErrorAtPosition(start: number, length: number, message: DiagnosticMessage, arg0?: any): void { + // Don't report another error if it would just be at the same position as the last error. + const lastError = lastOrUndefined(parseDiagnostics); + if (!lastError || start !== lastError.start) { + parseDiagnostics.push(createFileDiagnostic(sourceFile, start, length, message, arg0)); + } + // Mark that we've encountered an error. We'll set an appropriate bit on the next + // node we finish so that it can't be reused incrementally. + parseErrorBeforeNextFinishedNode = true; + } + function parseErrorAt(start: number, end: number, message: DiagnosticMessage, arg0?: any): void { + parseErrorAtPosition(start, end - start, message, arg0); + } + function parseErrorAtRange(range: TextRange, message: DiagnosticMessage, arg0?: any): void { + parseErrorAt(range.pos, range.end, message, arg0); + } + function scanError(message: DiagnosticMessage, length: number): void { + parseErrorAtPosition(scanner.getTextPos(), length, message); + } + function getNodePos(): number { + return scanner.getStartPos(); + } + // Use this function to access the current token instead of reading the currentToken + // variable. Since function results aren't narrowed in control flow analysis, this ensures + // that the type checker doesn't make wrong assumptions about the type of the current + // token (e.g. a call to nextToken() changes the current token but the checker doesn't + // reason about this side effect). Mainstream VMs inline simple functions like this, so + // there is no performance penalty. + function token(): SyntaxKind { + return currentToken; + } + function nextTokenWithoutCheck() { + return currentToken = scanner.scan(); + } + function nextToken(): SyntaxKind { + // if the keyword had an escape + if (isKeyword(currentToken) && (scanner.hasUnicodeEscape() || scanner.hasExtendedUnicodeEscape())) { + // issue a parse error for the escape + parseErrorAt(scanner.getTokenPos(), scanner.getTextPos(), Diagnostics.Keywords_cannot_contain_escape_characters); } - - function parseExpected(kind: SyntaxKind, diagnosticMessage?: DiagnosticMessage, shouldAdvance = true): boolean { - if (token() === kind) { - if (shouldAdvance) { - nextToken(); - } - return true; - } - - // Report specific message if provided with one. Otherwise, report generic fallback message. - if (diagnosticMessage) { - parseErrorAtCurrentToken(diagnosticMessage); - } - else { - parseErrorAtCurrentToken(Diagnostics._0_expected, tokenToString(kind)); - } - return false; + return nextTokenWithoutCheck(); + } + function nextTokenJSDoc(): JSDocSyntaxKind { + return currentToken = scanner.scanJsDocToken(); + } + function reScanGreaterToken(): SyntaxKind { + return currentToken = scanner.reScanGreaterToken(); + } + function reScanSlashToken(): SyntaxKind { + return currentToken = scanner.reScanSlashToken(); + } + function reScanTemplateToken(isTaggedTemplate: boolean): SyntaxKind { + return currentToken = scanner.reScanTemplateToken(isTaggedTemplate); + } + function reScanTemplateHeadOrNoSubstitutionTemplate(): SyntaxKind { + return currentToken = scanner.reScanTemplateHeadOrNoSubstitutionTemplate(); + } + function reScanLessThanToken(): SyntaxKind { + return currentToken = scanner.reScanLessThanToken(); + } + function scanJsxIdentifier(): SyntaxKind { + return currentToken = scanner.scanJsxIdentifier(); + } + function scanJsxText(): SyntaxKind { + return currentToken = scanner.scanJsxToken(); + } + function scanJsxAttributeValue(): SyntaxKind { + return currentToken = scanner.scanJsxAttributeValue(); + } + function speculationHelper(callback: () => T, isLookAhead: boolean): T { + // Keep track of the state we'll need to rollback to if lookahead fails (or if the + // caller asked us to always reset our state). + const saveToken = currentToken; + const saveParseDiagnosticsLength = parseDiagnostics.length; + const saveParseErrorBeforeNextFinishedNode = parseErrorBeforeNextFinishedNode; + // Note: it is not actually necessary to save/restore the context flags here. That's + // because the saving/restoring of these flags happens naturally through the recursive + // descent nature of our parser. However, we still store this here just so we can + // assert that invariant holds. + const saveContextFlags = contextFlags; + // If we're only looking ahead, then tell the scanner to only lookahead as well. + // Otherwise, if we're actually speculatively parsing, then tell the scanner to do the + // same. + const result = isLookAhead + ? scanner.lookAhead(callback) + : scanner.tryScan(callback); + Debug.assert(saveContextFlags === contextFlags); + // If our callback returned something 'falsy' or we're just looking ahead, + // then unconditionally restore us to where we were. + if (!result || isLookAhead) { + currentToken = saveToken; + parseDiagnostics.length = saveParseDiagnosticsLength; + parseErrorBeforeNextFinishedNode = saveParseErrorBeforeNextFinishedNode; } - - function parseExpectedJSDoc(kind: JSDocSyntaxKind) { - if (token() === kind) { - nextTokenJSDoc(); - return true; - } - parseErrorAtCurrentToken(Diagnostics._0_expected, tokenToString(kind)); + return result; + } + /** Invokes the provided callback then unconditionally restores the parser to the state it + * was in immediately prior to invoking the callback. The result of invoking the callback + * is returned from this function. + */ + function lookAhead(callback: () => T): T { + return speculationHelper(callback, /*isLookAhead*/ true); + } + /** Invokes the provided callback. If the callback returns something falsy, then it restores + * the parser to the state it was in immediately prior to invoking the callback. If the + * callback returns something truthy, then the parser state is not rolled back. The result + * of invoking the callback is returned from this function. + */ + function tryParse(callback: () => T): T { + return speculationHelper(callback, /*isLookAhead*/ false); + } + // Ignore strict mode flag because we will report an error in type checker instead. + function isIdentifier(): boolean { + if (token() === SyntaxKind.Identifier) { + return true; + } + // If we have a 'yield' keyword, and we're in the [yield] context, then 'yield' is + // considered a keyword and is not an identifier. + if (token() === SyntaxKind.YieldKeyword && inYieldContext()) { return false; } - - function parseOptional(t: SyntaxKind): boolean { - if (token() === t) { - nextToken(); - return true; - } + // If we have a 'await' keyword, and we're in the [Await] context, then 'await' is + // considered a keyword and is not an identifier. + if (token() === SyntaxKind.AwaitKeyword && inAwaitContext()) { return false; } - - function parseOptionalToken(t: TKind): Token; - function parseOptionalToken(t: SyntaxKind): Node | undefined { - if (token() === t) { - return parseTokenNode(); + return token() > SyntaxKind.LastReservedWord; + } + function parseExpected(kind: SyntaxKind, diagnosticMessage?: DiagnosticMessage, shouldAdvance = true): boolean { + if (token() === kind) { + if (shouldAdvance) { + nextToken(); } - return undefined; + return true; } - - function parseOptionalTokenJSDoc(t: TKind): Token; - function parseOptionalTokenJSDoc(t: JSDocSyntaxKind): Node | undefined { - if (token() === t) { - return parseTokenNodeJSDoc(); - } - return undefined; + // Report specific message if provided with one. Otherwise, report generic fallback message. + if (diagnosticMessage) { + parseErrorAtCurrentToken(diagnosticMessage); } - - function parseExpectedToken(t: TKind, diagnosticMessage?: DiagnosticMessage, arg0?: any): Token; - function parseExpectedToken(t: SyntaxKind, diagnosticMessage?: DiagnosticMessage, arg0?: any): Node { - return parseOptionalToken(t) || - createMissingNode(t, /*reportAtCurrentPosition*/ false, diagnosticMessage || Diagnostics._0_expected, arg0 || tokenToString(t)); - } - - function parseExpectedTokenJSDoc(t: TKind): Token; - function parseExpectedTokenJSDoc(t: JSDocSyntaxKind): Node { - return parseOptionalTokenJSDoc(t) || - createMissingNode(t, /*reportAtCurrentPosition*/ false, Diagnostics._0_expected, tokenToString(t)); - } - - function parseTokenNode(): T { - const node = createNode(token()); - nextToken(); - return finishNode(node); + else { + parseErrorAtCurrentToken(Diagnostics._0_expected, tokenToString(kind)); } - - function parseTokenNodeJSDoc(): T { - const node = createNode(token()); + return false; + } + function parseExpectedJSDoc(kind: JSDocSyntaxKind) { + if (token() === kind) { nextTokenJSDoc(); - return finishNode(node); + return true; } - - function canParseSemicolon() { - // If there's a real semicolon, then we can always parse it out. - if (token() === SyntaxKind.SemicolonToken) { - return true; - } - - // We can parse out an optional semicolon in ASI cases in the following cases. - return token() === SyntaxKind.CloseBraceToken || token() === SyntaxKind.EndOfFileToken || scanner.hasPrecedingLineBreak(); + parseErrorAtCurrentToken(Diagnostics._0_expected, tokenToString(kind)); + return false; + } + function parseOptional(t: SyntaxKind): boolean { + if (token() === t) { + nextToken(); + return true; } - - function parseSemicolon(): boolean { - if (canParseSemicolon()) { - if (token() === SyntaxKind.SemicolonToken) { - // consume the semicolon if it was explicitly provided. - nextToken(); - } - - return true; - } - else { - return parseExpected(SyntaxKind.SemicolonToken); + return false; + } + function parseOptionalToken(t: TKind): Token; + function parseOptionalToken(t: SyntaxKind): Node | undefined { + if (token() === t) { + return parseTokenNode(); + } + return undefined; + } + function parseOptionalTokenJSDoc(t: TKind): Token; + function parseOptionalTokenJSDoc(t: JSDocSyntaxKind): Node | undefined { + if (token() === t) { + return parseTokenNodeJSDoc(); + } + return undefined; + } + function parseExpectedToken(t: TKind, diagnosticMessage?: DiagnosticMessage, arg0?: any): Token; + function parseExpectedToken(t: SyntaxKind, diagnosticMessage?: DiagnosticMessage, arg0?: any): Node { + return parseOptionalToken(t) || + createMissingNode(t, /*reportAtCurrentPosition*/ false, diagnosticMessage || Diagnostics._0_expected, arg0 || tokenToString(t)); + } + function parseExpectedTokenJSDoc(t: TKind): Token; + function parseExpectedTokenJSDoc(t: JSDocSyntaxKind): Node { + return parseOptionalTokenJSDoc(t) || + createMissingNode(t, /*reportAtCurrentPosition*/ false, Diagnostics._0_expected, tokenToString(t)); + } + function parseTokenNode(): T { + const node = createNode(token()); + nextToken(); + return finishNode(node); + } + function parseTokenNodeJSDoc(): T { + const node = createNode(token()); + nextTokenJSDoc(); + return finishNode(node); + } + function canParseSemicolon() { + // If there's a real semicolon, then we can always parse it out. + if (token() === SyntaxKind.SemicolonToken) { + return true; + } + // We can parse out an optional semicolon in ASI cases in the following cases. + return token() === SyntaxKind.CloseBraceToken || token() === SyntaxKind.EndOfFileToken || scanner.hasPrecedingLineBreak(); + } + function parseSemicolon(): boolean { + if (canParseSemicolon()) { + if (token() === SyntaxKind.SemicolonToken) { + // consume the semicolon if it was explicitly provided. + nextToken(); } + return true; + } + else { + return parseExpected(SyntaxKind.SemicolonToken); } - - function createNode(kind: SyntaxKind, pos?: number): Node { - nodeCount++; - const p = pos! >= 0 ? pos! : scanner.getStartPos(); - return isNodeKind(kind) || kind === SyntaxKind.Unknown ? new NodeConstructor(kind, p, p) : - kind === SyntaxKind.Identifier ? new IdentifierConstructor(kind, p, p) : + } + function createNode(kind: SyntaxKind, pos?: number): Node { + nodeCount++; + const p = pos! >= 0 ? pos! : scanner.getStartPos(); + return isNodeKind(kind) || kind === SyntaxKind.Unknown ? new NodeConstructor(kind, p, p) : + kind === SyntaxKind.Identifier ? new IdentifierConstructor(kind, p, p) : kind === SyntaxKind.PrivateIdentifier ? new PrivateIdentifierConstructor(kind, p, p) : - new TokenConstructor(kind, p, p); + new TokenConstructor(kind, p, p); + } + function createNodeWithJSDoc(kind: SyntaxKind, pos?: number): Node { + const node = createNode(kind, pos); + if (scanner.getTokenFlags() & TokenFlags.PrecedingJSDocComment) { + addJSDocComment((node)); } - - function createNodeWithJSDoc(kind: SyntaxKind, pos?: number): Node { - const node = createNode(kind, pos); - if (scanner.getTokenFlags() & TokenFlags.PrecedingJSDocComment) { - addJSDocComment(node); - } - return node; + return node; + } + function createNodeArray(elements: T[], pos: number, end?: number): NodeArray { + // Since the element list of a node array is typically created by starting with an empty array and + // repeatedly calling push(), the list may not have the optimal memory layout. We invoke slice() for + // small arrays (1 to 4 elements) to give the VM a chance to allocate an optimal representation. + const length = elements.length; + const array = (>(length >= 1 && length <= 4 ? elements.slice() : elements)); + array.pos = pos; + array.end = end === undefined ? scanner.getStartPos() : end; + return array; + } + function finishNode(node: T, end?: number): T { + node.end = end === undefined ? scanner.getStartPos() : end; + if (contextFlags) { + node.flags |= contextFlags; + } + // Keep track on the node if we encountered an error while parsing it. If we did, then + // we cannot reuse the node incrementally. Once we've marked this node, clear out the + // flag so that we don't mark any subsequent nodes. + if (parseErrorBeforeNextFinishedNode) { + parseErrorBeforeNextFinishedNode = false; + node.flags |= NodeFlags.ThisNodeHasError; } - - function createNodeArray(elements: T[], pos: number, end?: number): NodeArray { - // Since the element list of a node array is typically created by starting with an empty array and - // repeatedly calling push(), the list may not have the optimal memory layout. We invoke slice() for - // small arrays (1 to 4 elements) to give the VM a chance to allocate an optimal representation. - const length = elements.length; - const array = >(length >= 1 && length <= 4 ? elements.slice() : elements); - array.pos = pos; - array.end = end === undefined ? scanner.getStartPos() : end; - return array; - } - - function finishNode(node: T, end?: number): T { - node.end = end === undefined ? scanner.getStartPos() : end; - - if (contextFlags) { - node.flags |= contextFlags; - } - - // Keep track on the node if we encountered an error while parsing it. If we did, then - // we cannot reuse the node incrementally. Once we've marked this node, clear out the - // flag so that we don't mark any subsequent nodes. - if (parseErrorBeforeNextFinishedNode) { - parseErrorBeforeNextFinishedNode = false; - node.flags |= NodeFlags.ThisNodeHasError; - } - - return node; + return node; + } + function createMissingNode(kind: T["kind"], reportAtCurrentPosition: false, diagnosticMessage?: DiagnosticMessage, arg0?: any): T; + function createMissingNode(kind: T["kind"], reportAtCurrentPosition: boolean, diagnosticMessage: DiagnosticMessage, arg0?: any): T; + function createMissingNode(kind: T["kind"], reportAtCurrentPosition: boolean, diagnosticMessage: DiagnosticMessage, arg0?: any): T { + if (reportAtCurrentPosition) { + parseErrorAtPosition(scanner.getStartPos(), 0, diagnosticMessage, arg0); } - - function createMissingNode(kind: T["kind"], reportAtCurrentPosition: false, diagnosticMessage?: DiagnosticMessage, arg0?: any): T; - function createMissingNode(kind: T["kind"], reportAtCurrentPosition: boolean, diagnosticMessage: DiagnosticMessage, arg0?: any): T; - function createMissingNode(kind: T["kind"], reportAtCurrentPosition: boolean, diagnosticMessage: DiagnosticMessage, arg0?: any): T { - if (reportAtCurrentPosition) { - parseErrorAtPosition(scanner.getStartPos(), 0, diagnosticMessage, arg0); - } - else if (diagnosticMessage) { - parseErrorAtCurrentToken(diagnosticMessage, arg0); - } - - const result = createNode(kind); - - if (kind === SyntaxKind.Identifier) { - (result as Identifier).escapedText = "" as __String; - } - else if (isLiteralKind(kind) || isTemplateLiteralKind(kind)) { - (result as LiteralLikeNode).text = ""; - } - - return finishNode(result) as T; - } - - function internIdentifier(text: string): string { - let identifier = identifiers.get(text); - if (identifier === undefined) { - identifiers.set(text, identifier = text); - } - return identifier; - } - - // An identifier that starts with two underscores has an extra underscore character prepended to it to avoid issues - // with magic property names like '__proto__'. The 'identifiers' object is used to share a single string instance for - // each identifier in order to reduce memory consumption. - function createIdentifier(isIdentifier: boolean, diagnosticMessage?: DiagnosticMessage, privateIdentifierDiagnosticMessage?: DiagnosticMessage): Identifier { - identifierCount++; - if (isIdentifier) { - const node = createNode(SyntaxKind.Identifier); - - // Store original token kind if it is not just an Identifier so we can report appropriate error later in type checker - if (token() !== SyntaxKind.Identifier) { - node.originalKeywordKind = token(); - } - node.escapedText = escapeLeadingUnderscores(internIdentifier(scanner.getTokenValue())); - nextTokenWithoutCheck(); - return finishNode(node); - } - - if (token() === SyntaxKind.PrivateIdentifier) { - parseErrorAtCurrentToken(privateIdentifierDiagnosticMessage || Diagnostics.Private_identifiers_are_not_allowed_outside_class_bodies); - return createIdentifier(/*isIdentifier*/ true); - } - - // Only for end of file because the error gets reported incorrectly on embedded script tags. - const reportAtCurrentPosition = token() === SyntaxKind.EndOfFileToken; - - const isReservedWord = scanner.isReservedWord(); - const msgArg = scanner.getTokenText(); - - const defaultMessage = isReservedWord ? - Diagnostics.Identifier_expected_0_is_a_reserved_word_that_cannot_be_used_here : - Diagnostics.Identifier_expected; - - return createMissingNode(SyntaxKind.Identifier, reportAtCurrentPosition, diagnosticMessage || defaultMessage, msgArg); - } - - function parseIdentifier(diagnosticMessage?: DiagnosticMessage, privateIdentifierDiagnosticMessage?: DiagnosticMessage): Identifier { - return createIdentifier(isIdentifier(), diagnosticMessage, privateIdentifierDiagnosticMessage); - } - - function parseIdentifierName(diagnosticMessage?: DiagnosticMessage): Identifier { - return createIdentifier(tokenIsIdentifierOrKeyword(token()), diagnosticMessage); - } - - function isLiteralPropertyName(): boolean { - return tokenIsIdentifierOrKeyword(token()) || - token() === SyntaxKind.StringLiteral || - token() === SyntaxKind.NumericLiteral; - } - - function parsePropertyNameWorker(allowComputedPropertyNames: boolean): PropertyName { - if (token() === SyntaxKind.StringLiteral || token() === SyntaxKind.NumericLiteral) { - const node = parseLiteralNode(); - node.text = internIdentifier(node.text); - return node; - } - if (allowComputedPropertyNames && token() === SyntaxKind.OpenBracketToken) { - return parseComputedPropertyName(); - } - if (token() === SyntaxKind.PrivateIdentifier) { - return parsePrivateIdentifier(); - } - return parseIdentifierName(); - } - - function parsePropertyName(): PropertyName { - return parsePropertyNameWorker(/*allowComputedPropertyNames*/ true); - } - - function parseComputedPropertyName(): ComputedPropertyName { - // PropertyName [Yield]: - // LiteralPropertyName - // ComputedPropertyName[?Yield] - const node = createNode(SyntaxKind.ComputedPropertyName); - parseExpected(SyntaxKind.OpenBracketToken); - - // We parse any expression (including a comma expression). But the grammar - // says that only an assignment expression is allowed, so the grammar checker - // will error if it sees a comma expression. - node.expression = allowInAnd(parseExpression); - - parseExpected(SyntaxKind.CloseBracketToken); - return finishNode(node); + else if (diagnosticMessage) { + parseErrorAtCurrentToken(diagnosticMessage, arg0); } - - function internPrivateIdentifier(text: string): string { - let privateIdentifier = privateIdentifiers.get(text); - if (privateIdentifier === undefined) { - privateIdentifiers.set(text, privateIdentifier = text); - } - return privateIdentifier; + const result = createNode(kind); + if (kind === SyntaxKind.Identifier) { + (result as Identifier).escapedText = ("" as __String); } - - function parsePrivateIdentifier(): PrivateIdentifier { - const node = createNode(SyntaxKind.PrivateIdentifier) as PrivateIdentifier; - node.escapedText = escapeLeadingUnderscores(internPrivateIdentifier(scanner.getTokenText())); - nextToken(); + else if (isLiteralKind(kind) || isTemplateLiteralKind(kind)) { + (result as LiteralLikeNode).text = ""; + } + return finishNode(result) as T; + } + function internIdentifier(text: string): string { + let identifier = identifiers.get(text); + if (identifier === undefined) { + identifiers.set(text, identifier = text); + } + return identifier; + } + // An identifier that starts with two underscores has an extra underscore character prepended to it to avoid issues + // with magic property names like '__proto__'. The 'identifiers' object is used to share a single string instance for + // each identifier in order to reduce memory consumption. + function createIdentifier(isIdentifier: boolean, diagnosticMessage?: DiagnosticMessage, privateIdentifierDiagnosticMessage?: DiagnosticMessage): Identifier { + identifierCount++; + if (isIdentifier) { + const node = (createNode(SyntaxKind.Identifier)); + // Store original token kind if it is not just an Identifier so we can report appropriate error later in type checker + if (token() !== SyntaxKind.Identifier) { + node.originalKeywordKind = token(); + } + node.escapedText = escapeLeadingUnderscores(internIdentifier(scanner.getTokenValue())); + nextTokenWithoutCheck(); return finishNode(node); } - - function parseContextualModifier(t: SyntaxKind): boolean { - return token() === t && tryParse(nextTokenCanFollowModifier); + if (token() === SyntaxKind.PrivateIdentifier) { + parseErrorAtCurrentToken(privateIdentifierDiagnosticMessage || Diagnostics.Private_identifiers_are_not_allowed_outside_class_bodies); + return createIdentifier(/*isIdentifier*/ true); + } + // Only for end of file because the error gets reported incorrectly on embedded script tags. + const reportAtCurrentPosition = token() === SyntaxKind.EndOfFileToken; + const isReservedWord = scanner.isReservedWord(); + const msgArg = scanner.getTokenText(); + const defaultMessage = isReservedWord ? + Diagnostics.Identifier_expected_0_is_a_reserved_word_that_cannot_be_used_here : + Diagnostics.Identifier_expected; + return createMissingNode(SyntaxKind.Identifier, reportAtCurrentPosition, diagnosticMessage || defaultMessage, msgArg); + } + function parseIdentifier(diagnosticMessage?: DiagnosticMessage, privateIdentifierDiagnosticMessage?: DiagnosticMessage): Identifier { + return createIdentifier(isIdentifier(), diagnosticMessage, privateIdentifierDiagnosticMessage); + } + function parseIdentifierName(diagnosticMessage?: DiagnosticMessage): Identifier { + return createIdentifier(tokenIsIdentifierOrKeyword(token()), diagnosticMessage); + } + function isLiteralPropertyName(): boolean { + return tokenIsIdentifierOrKeyword(token()) || + token() === SyntaxKind.StringLiteral || + token() === SyntaxKind.NumericLiteral; + } + function parsePropertyNameWorker(allowComputedPropertyNames: boolean): PropertyName { + if (token() === SyntaxKind.StringLiteral || token() === SyntaxKind.NumericLiteral) { + const node = (parseLiteralNode()); + node.text = internIdentifier(node.text); + return node; } - - function nextTokenIsOnSameLineAndCanFollowModifier() { - nextToken(); - if (scanner.hasPrecedingLineBreak()) { - return false; - } - return canFollowModifier(); + if (allowComputedPropertyNames && token() === SyntaxKind.OpenBracketToken) { + return parseComputedPropertyName(); } - - function nextTokenCanFollowModifier() { - switch (token()) { - case SyntaxKind.ConstKeyword: - // 'const' is only a modifier if followed by 'enum'. - return nextToken() === SyntaxKind.EnumKeyword; - case SyntaxKind.ExportKeyword: - nextToken(); - if (token() === SyntaxKind.DefaultKeyword) { - return lookAhead(nextTokenCanFollowDefaultKeyword); - } - if (token() === SyntaxKind.TypeKeyword) { - return lookAhead(nextTokenCanFollowExportModifier); - } - return canFollowExportModifier(); - case SyntaxKind.DefaultKeyword: - return nextTokenCanFollowDefaultKeyword(); - case SyntaxKind.StaticKeyword: - case SyntaxKind.GetKeyword: - case SyntaxKind.SetKeyword: - nextToken(); - return canFollowModifier(); - default: - return nextTokenIsOnSameLineAndCanFollowModifier(); - } + if (token() === SyntaxKind.PrivateIdentifier) { + return parsePrivateIdentifier(); } - - function canFollowExportModifier(): boolean { - return token() !== SyntaxKind.AsteriskToken - && token() !== SyntaxKind.AsKeyword - && token() !== SyntaxKind.OpenBraceToken - && canFollowModifier(); + return parseIdentifierName(); + } + function parsePropertyName(): PropertyName { + return parsePropertyNameWorker(/*allowComputedPropertyNames*/ true); + } + function parseComputedPropertyName(): ComputedPropertyName { + // PropertyName [Yield]: + // LiteralPropertyName + // ComputedPropertyName[?Yield] + const node = (createNode(SyntaxKind.ComputedPropertyName)); + parseExpected(SyntaxKind.OpenBracketToken); + // We parse any expression (including a comma expression). But the grammar + // says that only an assignment expression is allowed, so the grammar checker + // will error if it sees a comma expression. + node.expression = allowInAnd(parseExpression); + parseExpected(SyntaxKind.CloseBracketToken); + return finishNode(node); + } + function internPrivateIdentifier(text: string): string { + let privateIdentifier = privateIdentifiers.get(text); + if (privateIdentifier === undefined) { + privateIdentifiers.set(text, privateIdentifier = text); } - - function nextTokenCanFollowExportModifier(): boolean { - nextToken(); - return canFollowExportModifier(); - } - - function parseAnyContextualModifier(): boolean { - return isModifierKind(token()) && tryParse(nextTokenCanFollowModifier); - } - - function canFollowModifier(): boolean { - return token() === SyntaxKind.OpenBracketToken - || token() === SyntaxKind.OpenBraceToken - || token() === SyntaxKind.AsteriskToken - || token() === SyntaxKind.DotDotDotToken - || isLiteralPropertyName(); - } - - function nextTokenCanFollowDefaultKeyword(): boolean { - nextToken(); - return token() === SyntaxKind.ClassKeyword || token() === SyntaxKind.FunctionKeyword || - token() === SyntaxKind.InterfaceKeyword || - (token() === SyntaxKind.AbstractKeyword && lookAhead(nextTokenIsClassKeywordOnSameLine)) || - (token() === SyntaxKind.AsyncKeyword && lookAhead(nextTokenIsFunctionKeywordOnSameLine)); - } - - // True if positioned at the start of a list element - function isListElement(parsingContext: ParsingContext, inErrorRecovery: boolean): boolean { - const node = currentNode(parsingContext); - if (node) { - return true; - } - - switch (parsingContext) { - case ParsingContext.SourceElements: - case ParsingContext.BlockStatements: - case ParsingContext.SwitchClauseStatements: - // If we're in error recovery, then we don't want to treat ';' as an empty statement. - // The problem is that ';' can show up in far too many contexts, and if we see one - // and assume it's a statement, then we may bail out inappropriately from whatever - // we're parsing. For example, if we have a semicolon in the middle of a class, then - // we really don't want to assume the class is over and we're on a statement in the - // outer module. We just want to consume and move on. - return !(token() === SyntaxKind.SemicolonToken && inErrorRecovery) && isStartOfStatement(); - case ParsingContext.SwitchClauses: - return token() === SyntaxKind.CaseKeyword || token() === SyntaxKind.DefaultKeyword; - case ParsingContext.TypeMembers: - return lookAhead(isTypeMemberStart); - case ParsingContext.ClassMembers: - // We allow semicolons as class elements (as specified by ES6) as long as we're - // not in error recovery. If we're in error recovery, we don't want an errant - // semicolon to be treated as a class member (since they're almost always used - // for statements. - return lookAhead(isClassMemberStart) || (token() === SyntaxKind.SemicolonToken && !inErrorRecovery); - case ParsingContext.EnumMembers: - // Include open bracket computed properties. This technically also lets in indexers, - // which would be a candidate for improved error reporting. - return token() === SyntaxKind.OpenBracketToken || isLiteralPropertyName(); - case ParsingContext.ObjectLiteralMembers: - switch (token()) { - case SyntaxKind.OpenBracketToken: - case SyntaxKind.AsteriskToken: - case SyntaxKind.DotDotDotToken: - case SyntaxKind.DotToken: // Not an object literal member, but don't want to close the object (see `tests/cases/fourslash/completionsDotInObjectLiteral.ts`) - return true; - default: - return isLiteralPropertyName(); - } - case ParsingContext.RestProperties: - return isLiteralPropertyName(); - case ParsingContext.ObjectBindingElements: - return token() === SyntaxKind.OpenBracketToken || token() === SyntaxKind.DotDotDotToken || isLiteralPropertyName(); - case ParsingContext.HeritageClauseElement: - // If we see `{ ... }` then only consume it as an expression if it is followed by `,` or `{` - // That way we won't consume the body of a class in its heritage clause. - if (token() === SyntaxKind.OpenBraceToken) { - return lookAhead(isValidHeritageClauseObjectLiteral); - } - - if (!inErrorRecovery) { - return isStartOfLeftHandSideExpression() && !isHeritageClauseExtendsOrImplementsKeyword(); - } - else { - // If we're in error recovery we tighten up what we're willing to match. - // That way we don't treat something like "this" as a valid heritage clause - // element during recovery. - return isIdentifier() && !isHeritageClauseExtendsOrImplementsKeyword(); - } - case ParsingContext.VariableDeclarations: - return isIdentifierOrPrivateIdentifierOrPattern(); - case ParsingContext.ArrayBindingElements: - return token() === SyntaxKind.CommaToken || token() === SyntaxKind.DotDotDotToken || isIdentifierOrPrivateIdentifierOrPattern(); - case ParsingContext.TypeParameters: - return isIdentifier(); - case ParsingContext.ArrayLiteralMembers: - switch (token()) { - case SyntaxKind.CommaToken: - case SyntaxKind.DotToken: // Not an array literal member, but don't want to close the array (see `tests/cases/fourslash/completionsDotInArrayLiteralInObjectLiteral.ts`) - return true; - } - // falls through - case ParsingContext.ArgumentExpressions: - return token() === SyntaxKind.DotDotDotToken || isStartOfExpression(); - case ParsingContext.Parameters: - return isStartOfParameter(/*isJSDocParameter*/ false); - case ParsingContext.JSDocParameters: - return isStartOfParameter(/*isJSDocParameter*/ true); - case ParsingContext.TypeArguments: - case ParsingContext.TupleElementTypes: - return token() === SyntaxKind.CommaToken || isStartOfType(); - case ParsingContext.HeritageClauses: - return isHeritageClause(); - case ParsingContext.ImportOrExportSpecifiers: - return tokenIsIdentifierOrKeyword(token()); - case ParsingContext.JsxAttributes: - return tokenIsIdentifierOrKeyword(token()) || token() === SyntaxKind.OpenBraceToken; - case ParsingContext.JsxChildren: - return true; - } - - return Debug.fail("Non-exhaustive case in 'isListElement'."); + return privateIdentifier; + } + function parsePrivateIdentifier(): PrivateIdentifier { + const node = (createNode(SyntaxKind.PrivateIdentifier) as PrivateIdentifier); + node.escapedText = escapeLeadingUnderscores(internPrivateIdentifier(scanner.getTokenText())); + nextToken(); + return finishNode(node); + } + function parseContextualModifier(t: SyntaxKind): boolean { + return token() === t && tryParse(nextTokenCanFollowModifier); + } + function nextTokenIsOnSameLineAndCanFollowModifier() { + nextToken(); + if (scanner.hasPrecedingLineBreak()) { + return false; } - - function isValidHeritageClauseObjectLiteral() { - Debug.assert(token() === SyntaxKind.OpenBraceToken); - if (nextToken() === SyntaxKind.CloseBraceToken) { - // if we see "extends {}" then only treat the {} as what we're extending (and not - // the class body) if we have: - // - // extends {} { - // extends {}, - // extends {} extends - // extends {} implements - - const next = nextToken(); - return next === SyntaxKind.CommaToken || next === SyntaxKind.OpenBraceToken || next === SyntaxKind.ExtendsKeyword || next === SyntaxKind.ImplementsKeyword; - } - + return canFollowModifier(); + } + function nextTokenCanFollowModifier() { + switch (token()) { + case SyntaxKind.ConstKeyword: + // 'const' is only a modifier if followed by 'enum'. + return nextToken() === SyntaxKind.EnumKeyword; + case SyntaxKind.ExportKeyword: + nextToken(); + if (token() === SyntaxKind.DefaultKeyword) { + return lookAhead(nextTokenCanFollowDefaultKeyword); + } + if (token() === SyntaxKind.TypeKeyword) { + return lookAhead(nextTokenCanFollowExportModifier); + } + return canFollowExportModifier(); + case SyntaxKind.DefaultKeyword: + return nextTokenCanFollowDefaultKeyword(); + case SyntaxKind.StaticKeyword: + case SyntaxKind.GetKeyword: + case SyntaxKind.SetKeyword: + nextToken(); + return canFollowModifier(); + default: + return nextTokenIsOnSameLineAndCanFollowModifier(); + } + } + function canFollowExportModifier(): boolean { + return token() !== SyntaxKind.AsteriskToken + && token() !== SyntaxKind.AsKeyword + && token() !== SyntaxKind.OpenBraceToken + && canFollowModifier(); + } + function nextTokenCanFollowExportModifier(): boolean { + nextToken(); + return canFollowExportModifier(); + } + function parseAnyContextualModifier(): boolean { + return isModifierKind(token()) && tryParse(nextTokenCanFollowModifier); + } + function canFollowModifier(): boolean { + return token() === SyntaxKind.OpenBracketToken + || token() === SyntaxKind.OpenBraceToken + || token() === SyntaxKind.AsteriskToken + || token() === SyntaxKind.DotDotDotToken + || isLiteralPropertyName(); + } + function nextTokenCanFollowDefaultKeyword(): boolean { + nextToken(); + return token() === SyntaxKind.ClassKeyword || token() === SyntaxKind.FunctionKeyword || + token() === SyntaxKind.InterfaceKeyword || + (token() === SyntaxKind.AbstractKeyword && lookAhead(nextTokenIsClassKeywordOnSameLine)) || + (token() === SyntaxKind.AsyncKeyword && lookAhead(nextTokenIsFunctionKeywordOnSameLine)); + } + // True if positioned at the start of a list element + function isListElement(parsingContext: ParsingContext, inErrorRecovery: boolean): boolean { + const node = currentNode(parsingContext); + if (node) { return true; } - - function nextTokenIsIdentifier() { - nextToken(); - return isIdentifier(); + switch (parsingContext) { + case ParsingContext.SourceElements: + case ParsingContext.BlockStatements: + case ParsingContext.SwitchClauseStatements: + // If we're in error recovery, then we don't want to treat ';' as an empty statement. + // The problem is that ';' can show up in far too many contexts, and if we see one + // and assume it's a statement, then we may bail out inappropriately from whatever + // we're parsing. For example, if we have a semicolon in the middle of a class, then + // we really don't want to assume the class is over and we're on a statement in the + // outer module. We just want to consume and move on. + return !(token() === SyntaxKind.SemicolonToken && inErrorRecovery) && isStartOfStatement(); + case ParsingContext.SwitchClauses: + return token() === SyntaxKind.CaseKeyword || token() === SyntaxKind.DefaultKeyword; + case ParsingContext.TypeMembers: + return lookAhead(isTypeMemberStart); + case ParsingContext.ClassMembers: + // We allow semicolons as class elements (as specified by ES6) as long as we're + // not in error recovery. If we're in error recovery, we don't want an errant + // semicolon to be treated as a class member (since they're almost always used + // for statements. + return lookAhead(isClassMemberStart) || (token() === SyntaxKind.SemicolonToken && !inErrorRecovery); + case ParsingContext.EnumMembers: + // Include open bracket computed properties. This technically also lets in indexers, + // which would be a candidate for improved error reporting. + return token() === SyntaxKind.OpenBracketToken || isLiteralPropertyName(); + case ParsingContext.ObjectLiteralMembers: + switch (token()) { + case SyntaxKind.OpenBracketToken: + case SyntaxKind.AsteriskToken: + case SyntaxKind.DotDotDotToken: + case SyntaxKind.DotToken: // Not an object literal member, but don't want to close the object (see `tests/cases/fourslash/completionsDotInObjectLiteral.ts`) + return true; + default: + return isLiteralPropertyName(); + } + case ParsingContext.RestProperties: + return isLiteralPropertyName(); + case ParsingContext.ObjectBindingElements: + return token() === SyntaxKind.OpenBracketToken || token() === SyntaxKind.DotDotDotToken || isLiteralPropertyName(); + case ParsingContext.HeritageClauseElement: + // If we see `{ ... }` then only consume it as an expression if it is followed by `,` or `{` + // That way we won't consume the body of a class in its heritage clause. + if (token() === SyntaxKind.OpenBraceToken) { + return lookAhead(isValidHeritageClauseObjectLiteral); + } + if (!inErrorRecovery) { + return isStartOfLeftHandSideExpression() && !isHeritageClauseExtendsOrImplementsKeyword(); + } + else { + // If we're in error recovery we tighten up what we're willing to match. + // That way we don't treat something like "this" as a valid heritage clause + // element during recovery. + return isIdentifier() && !isHeritageClauseExtendsOrImplementsKeyword(); + } + case ParsingContext.VariableDeclarations: + return isIdentifierOrPrivateIdentifierOrPattern(); + case ParsingContext.ArrayBindingElements: + return token() === SyntaxKind.CommaToken || token() === SyntaxKind.DotDotDotToken || isIdentifierOrPrivateIdentifierOrPattern(); + case ParsingContext.TypeParameters: + return isIdentifier(); + case ParsingContext.ArrayLiteralMembers: + switch (token()) { + case SyntaxKind.CommaToken: + case SyntaxKind.DotToken: // Not an array literal member, but don't want to close the array (see `tests/cases/fourslash/completionsDotInArrayLiteralInObjectLiteral.ts`) + return true; + } + // falls through + case ParsingContext.ArgumentExpressions: + return token() === SyntaxKind.DotDotDotToken || isStartOfExpression(); + case ParsingContext.Parameters: + return isStartOfParameter(/*isJSDocParameter*/ false); + case ParsingContext.JSDocParameters: + return isStartOfParameter(/*isJSDocParameter*/ true); + case ParsingContext.TypeArguments: + case ParsingContext.TupleElementTypes: + return token() === SyntaxKind.CommaToken || isStartOfType(); + case ParsingContext.HeritageClauses: + return isHeritageClause(); + case ParsingContext.ImportOrExportSpecifiers: + return tokenIsIdentifierOrKeyword(token()); + case ParsingContext.JsxAttributes: + return tokenIsIdentifierOrKeyword(token()) || token() === SyntaxKind.OpenBraceToken; + case ParsingContext.JsxChildren: + return true; } - - function nextTokenIsIdentifierOrKeyword() { - nextToken(); - return tokenIsIdentifierOrKeyword(token()); + return Debug.fail("Non-exhaustive case in 'isListElement'."); + } + function isValidHeritageClauseObjectLiteral() { + Debug.assert(token() === SyntaxKind.OpenBraceToken); + if (nextToken() === SyntaxKind.CloseBraceToken) { + // if we see "extends {}" then only treat the {} as what we're extending (and not + // the class body) if we have: + // + // extends {} { + // extends {}, + // extends {} extends + // extends {} implements + const next = nextToken(); + return next === SyntaxKind.CommaToken || next === SyntaxKind.OpenBraceToken || next === SyntaxKind.ExtendsKeyword || next === SyntaxKind.ImplementsKeyword; + } + return true; + } + function nextTokenIsIdentifier() { + nextToken(); + return isIdentifier(); + } + function nextTokenIsIdentifierOrKeyword() { + nextToken(); + return tokenIsIdentifierOrKeyword(token()); + } + function nextTokenIsIdentifierOrKeywordOrGreaterThan() { + nextToken(); + return tokenIsIdentifierOrKeywordOrGreaterThan(token()); + } + function isHeritageClauseExtendsOrImplementsKeyword(): boolean { + if (token() === SyntaxKind.ImplementsKeyword || + token() === SyntaxKind.ExtendsKeyword) { + return lookAhead(nextTokenIsStartOfExpression); } - - function nextTokenIsIdentifierOrKeywordOrGreaterThan() { - nextToken(); - return tokenIsIdentifierOrKeywordOrGreaterThan(token()); + return false; + } + function nextTokenIsStartOfExpression() { + nextToken(); + return isStartOfExpression(); + } + function nextTokenIsStartOfType() { + nextToken(); + return isStartOfType(); + } + // True if positioned at a list terminator + function isListTerminator(kind: ParsingContext): boolean { + if (token() === SyntaxKind.EndOfFileToken) { + // Being at the end of the file ends all lists. + return true; } - - function isHeritageClauseExtendsOrImplementsKeyword(): boolean { - if (token() === SyntaxKind.ImplementsKeyword || - token() === SyntaxKind.ExtendsKeyword) { - - return lookAhead(nextTokenIsStartOfExpression); - } - - return false; + switch (kind) { + case ParsingContext.BlockStatements: + case ParsingContext.SwitchClauses: + case ParsingContext.TypeMembers: + case ParsingContext.ClassMembers: + case ParsingContext.EnumMembers: + case ParsingContext.ObjectLiteralMembers: + case ParsingContext.ObjectBindingElements: + case ParsingContext.ImportOrExportSpecifiers: + return token() === SyntaxKind.CloseBraceToken; + case ParsingContext.SwitchClauseStatements: + return token() === SyntaxKind.CloseBraceToken || token() === SyntaxKind.CaseKeyword || token() === SyntaxKind.DefaultKeyword; + case ParsingContext.HeritageClauseElement: + return token() === SyntaxKind.OpenBraceToken || token() === SyntaxKind.ExtendsKeyword || token() === SyntaxKind.ImplementsKeyword; + case ParsingContext.VariableDeclarations: + return isVariableDeclaratorListTerminator(); + case ParsingContext.TypeParameters: + // Tokens other than '>' are here for better error recovery + return token() === SyntaxKind.GreaterThanToken || token() === SyntaxKind.OpenParenToken || token() === SyntaxKind.OpenBraceToken || token() === SyntaxKind.ExtendsKeyword || token() === SyntaxKind.ImplementsKeyword; + case ParsingContext.ArgumentExpressions: + // Tokens other than ')' are here for better error recovery + return token() === SyntaxKind.CloseParenToken || token() === SyntaxKind.SemicolonToken; + case ParsingContext.ArrayLiteralMembers: + case ParsingContext.TupleElementTypes: + case ParsingContext.ArrayBindingElements: + return token() === SyntaxKind.CloseBracketToken; + case ParsingContext.JSDocParameters: + case ParsingContext.Parameters: + case ParsingContext.RestProperties: + // Tokens other than ')' and ']' (the latter for index signatures) are here for better error recovery + return token() === SyntaxKind.CloseParenToken || token() === SyntaxKind.CloseBracketToken /*|| token === SyntaxKind.OpenBraceToken*/; + case ParsingContext.TypeArguments: + // All other tokens should cause the type-argument to terminate except comma token + return token() !== SyntaxKind.CommaToken; + case ParsingContext.HeritageClauses: + return token() === SyntaxKind.OpenBraceToken || token() === SyntaxKind.CloseBraceToken; + case ParsingContext.JsxAttributes: + return token() === SyntaxKind.GreaterThanToken || token() === SyntaxKind.SlashToken; + case ParsingContext.JsxChildren: + return token() === SyntaxKind.LessThanToken && lookAhead(nextTokenIsSlash); + default: + return false; } - - function nextTokenIsStartOfExpression() { - nextToken(); - return isStartOfExpression(); + } + function isVariableDeclaratorListTerminator(): boolean { + // If we can consume a semicolon (either explicitly, or with ASI), then consider us done + // with parsing the list of variable declarators. + if (canParseSemicolon()) { + return true; } - - function nextTokenIsStartOfType() { - nextToken(); - return isStartOfType(); + // in the case where we're parsing the variable declarator of a 'for-in' statement, we + // are done if we see an 'in' keyword in front of us. Same with for-of + if (isInOrOfKeyword(token())) { + return true; } - - // True if positioned at a list terminator - function isListTerminator(kind: ParsingContext): boolean { - if (token() === SyntaxKind.EndOfFileToken) { - // Being at the end of the file ends all lists. - return true; - } - - switch (kind) { - case ParsingContext.BlockStatements: - case ParsingContext.SwitchClauses: - case ParsingContext.TypeMembers: - case ParsingContext.ClassMembers: - case ParsingContext.EnumMembers: - case ParsingContext.ObjectLiteralMembers: - case ParsingContext.ObjectBindingElements: - case ParsingContext.ImportOrExportSpecifiers: - return token() === SyntaxKind.CloseBraceToken; - case ParsingContext.SwitchClauseStatements: - return token() === SyntaxKind.CloseBraceToken || token() === SyntaxKind.CaseKeyword || token() === SyntaxKind.DefaultKeyword; - case ParsingContext.HeritageClauseElement: - return token() === SyntaxKind.OpenBraceToken || token() === SyntaxKind.ExtendsKeyword || token() === SyntaxKind.ImplementsKeyword; - case ParsingContext.VariableDeclarations: - return isVariableDeclaratorListTerminator(); - case ParsingContext.TypeParameters: - // Tokens other than '>' are here for better error recovery - return token() === SyntaxKind.GreaterThanToken || token() === SyntaxKind.OpenParenToken || token() === SyntaxKind.OpenBraceToken || token() === SyntaxKind.ExtendsKeyword || token() === SyntaxKind.ImplementsKeyword; - case ParsingContext.ArgumentExpressions: - // Tokens other than ')' are here for better error recovery - return token() === SyntaxKind.CloseParenToken || token() === SyntaxKind.SemicolonToken; - case ParsingContext.ArrayLiteralMembers: - case ParsingContext.TupleElementTypes: - case ParsingContext.ArrayBindingElements: - return token() === SyntaxKind.CloseBracketToken; - case ParsingContext.JSDocParameters: - case ParsingContext.Parameters: - case ParsingContext.RestProperties: - // Tokens other than ')' and ']' (the latter for index signatures) are here for better error recovery - return token() === SyntaxKind.CloseParenToken || token() === SyntaxKind.CloseBracketToken /*|| token === SyntaxKind.OpenBraceToken*/; - case ParsingContext.TypeArguments: - // All other tokens should cause the type-argument to terminate except comma token - return token() !== SyntaxKind.CommaToken; - case ParsingContext.HeritageClauses: - return token() === SyntaxKind.OpenBraceToken || token() === SyntaxKind.CloseBraceToken; - case ParsingContext.JsxAttributes: - return token() === SyntaxKind.GreaterThanToken || token() === SyntaxKind.SlashToken; - case ParsingContext.JsxChildren: - return token() === SyntaxKind.LessThanToken && lookAhead(nextTokenIsSlash); - default: - return false; - } + // ERROR RECOVERY TWEAK: + // For better error recovery, if we see an '=>' then we just stop immediately. We've got an + // arrow function here and it's going to be very unlikely that we'll resynchronize and get + // another variable declaration. + if (token() === SyntaxKind.EqualsGreaterThanToken) { + return true; } - - function isVariableDeclaratorListTerminator(): boolean { - // If we can consume a semicolon (either explicitly, or with ASI), then consider us done - // with parsing the list of variable declarators. - if (canParseSemicolon()) { - return true; + // Keep trying to parse out variable declarators. + return false; + } + // True if positioned at element or terminator of the current list or any enclosing list + function isInSomeParsingContext(): boolean { + for (let kind = 0; kind < ParsingContext.Count; kind++) { + if (parsingContext & (1 << kind)) { + if (isListElement(kind, /*inErrorRecovery*/ true) || isListTerminator(kind)) { + return true; + } } - - // in the case where we're parsing the variable declarator of a 'for-in' statement, we - // are done if we see an 'in' keyword in front of us. Same with for-of - if (isInOrOfKeyword(token())) { - return true; + } + return false; + } + // Parses a list of elements + function parseList(kind: ParsingContext, parseElement: () => T): NodeArray { + const saveParsingContext = parsingContext; + parsingContext |= 1 << kind; + const list = []; + const listPos = getNodePos(); + while (!isListTerminator(kind)) { + if (isListElement(kind, /*inErrorRecovery*/ false)) { + const element = parseListElement(kind, parseElement); + list.push(element); + continue; } - - // ERROR RECOVERY TWEAK: - // For better error recovery, if we see an '=>' then we just stop immediately. We've got an - // arrow function here and it's going to be very unlikely that we'll resynchronize and get - // another variable declaration. - if (token() === SyntaxKind.EqualsGreaterThanToken) { - return true; + if (abortParsingListOrMoveToNextToken(kind)) { + break; } - - // Keep trying to parse out variable declarators. - return false; } - - // True if positioned at element or terminator of the current list or any enclosing list - function isInSomeParsingContext(): boolean { - for (let kind = 0; kind < ParsingContext.Count; kind++) { - if (parsingContext & (1 << kind)) { - if (isListElement(kind, /*inErrorRecovery*/ true) || isListTerminator(kind)) { - return true; - } - } - } - - return false; + parsingContext = saveParsingContext; + return createNodeArray(list, listPos); + } + function parseListElement(parsingContext: ParsingContext, parseElement: () => T): T { + const node = currentNode(parsingContext); + if (node) { + return consumeNode(node); } - - // Parses a list of elements - function parseList(kind: ParsingContext, parseElement: () => T): NodeArray { - const saveParsingContext = parsingContext; - parsingContext |= 1 << kind; - const list = []; - const listPos = getNodePos(); - - while (!isListTerminator(kind)) { - if (isListElement(kind, /*inErrorRecovery*/ false)) { - const element = parseListElement(kind, parseElement); - list.push(element); - - continue; - } - - if (abortParsingListOrMoveToNextToken(kind)) { - break; - } - } - - parsingContext = saveParsingContext; - return createNodeArray(list, listPos); + return parseElement(); + } + function currentNode(parsingContext: ParsingContext): Node | undefined { + // If we don't have a cursor or the parsing context isn't reusable, there's nothing to reuse. + // + // If there is an outstanding parse error that we've encountered, but not attached to + // some node, then we cannot get a node from the old source tree. This is because we + // want to mark the next node we encounter as being unusable. + // + // Note: This may be too conservative. Perhaps we could reuse the node and set the bit + // on it (or its leftmost child) as having the error. For now though, being conservative + // is nice and likely won't ever affect perf. + if (!syntaxCursor || !isReusableParsingContext(parsingContext) || parseErrorBeforeNextFinishedNode) { + return undefined; } - - function parseListElement(parsingContext: ParsingContext, parseElement: () => T): T { - const node = currentNode(parsingContext); - if (node) { - return consumeNode(node); - } - - return parseElement(); + const node = syntaxCursor.currentNode(scanner.getStartPos()); + // Can't reuse a missing node. + // Can't reuse a node that intersected the change range. + // Can't reuse a node that contains a parse error. This is necessary so that we + // produce the same set of errors again. + if (nodeIsMissing(node) || node.intersectsChange || containsParseError(node)) { + return undefined; } - - function currentNode(parsingContext: ParsingContext): Node | undefined { - // If we don't have a cursor or the parsing context isn't reusable, there's nothing to reuse. - // - // If there is an outstanding parse error that we've encountered, but not attached to - // some node, then we cannot get a node from the old source tree. This is because we - // want to mark the next node we encounter as being unusable. - // - // Note: This may be too conservative. Perhaps we could reuse the node and set the bit - // on it (or its leftmost child) as having the error. For now though, being conservative - // is nice and likely won't ever affect perf. - if (!syntaxCursor || !isReusableParsingContext(parsingContext) || parseErrorBeforeNextFinishedNode) { - return undefined; - } - - const node = syntaxCursor.currentNode(scanner.getStartPos()); - - // Can't reuse a missing node. - // Can't reuse a node that intersected the change range. - // Can't reuse a node that contains a parse error. This is necessary so that we - // produce the same set of errors again. - if (nodeIsMissing(node) || node.intersectsChange || containsParseError(node)) { - return undefined; - } - - // We can only reuse a node if it was parsed under the same strict mode that we're - // currently in. i.e. if we originally parsed a node in non-strict mode, but then - // the user added 'using strict' at the top of the file, then we can't use that node - // again as the presence of strict mode may cause us to parse the tokens in the file - // differently. - // - // Note: we *can* reuse tokens when the strict mode changes. That's because tokens - // are unaffected by strict mode. It's just the parser will decide what to do with it - // differently depending on what mode it is in. - // - // This also applies to all our other context flags as well. - const nodeContextFlags = node.flags & NodeFlags.ContextFlags; - if (nodeContextFlags !== contextFlags) { - return undefined; - } - - // Ok, we have a node that looks like it could be reused. Now verify that it is valid - // in the current list parsing context that we're currently at. - if (!canReuseNode(node, parsingContext)) { - return undefined; - } - - if ((node as JSDocContainer).jsDocCache) { - // jsDocCache may include tags from parent nodes, which might have been modified. - (node as JSDocContainer).jsDocCache = undefined; - } - - return node; + // We can only reuse a node if it was parsed under the same strict mode that we're + // currently in. i.e. if we originally parsed a node in non-strict mode, but then + // the user added 'using strict' at the top of the file, then we can't use that node + // again as the presence of strict mode may cause us to parse the tokens in the file + // differently. + // + // Note: we *can* reuse tokens when the strict mode changes. That's because tokens + // are unaffected by strict mode. It's just the parser will decide what to do with it + // differently depending on what mode it is in. + // + // This also applies to all our other context flags as well. + const nodeContextFlags = node.flags & NodeFlags.ContextFlags; + if (nodeContextFlags !== contextFlags) { + return undefined; } - - function consumeNode(node: Node) { - // Move the scanner so it is after the node we just consumed. - scanner.setTextPos(node.end); - nextToken(); - return node; + // Ok, we have a node that looks like it could be reused. Now verify that it is valid + // in the current list parsing context that we're currently at. + if (!canReuseNode(node, parsingContext)) { + return undefined; + } + if ((node as JSDocContainer).jsDocCache) { + // jsDocCache may include tags from parent nodes, which might have been modified. + (node as JSDocContainer).jsDocCache = undefined; + } + return node; + } + function consumeNode(node: Node) { + // Move the scanner so it is after the node we just consumed. + scanner.setTextPos(node.end); + nextToken(); + return node; + } + function isReusableParsingContext(parsingContext: ParsingContext): boolean { + switch (parsingContext) { + case ParsingContext.ClassMembers: + case ParsingContext.SwitchClauses: + case ParsingContext.SourceElements: + case ParsingContext.BlockStatements: + case ParsingContext.SwitchClauseStatements: + case ParsingContext.EnumMembers: + case ParsingContext.TypeMembers: + case ParsingContext.VariableDeclarations: + case ParsingContext.JSDocParameters: + case ParsingContext.Parameters: + return true; } - - function isReusableParsingContext(parsingContext: ParsingContext): boolean { - switch (parsingContext) { - case ParsingContext.ClassMembers: - case ParsingContext.SwitchClauses: - case ParsingContext.SourceElements: - case ParsingContext.BlockStatements: - case ParsingContext.SwitchClauseStatements: - case ParsingContext.EnumMembers: - case ParsingContext.TypeMembers: - case ParsingContext.VariableDeclarations: - case ParsingContext.JSDocParameters: - case ParsingContext.Parameters: + return false; + } + function canReuseNode(node: Node, parsingContext: ParsingContext): boolean { + switch (parsingContext) { + case ParsingContext.ClassMembers: + return isReusableClassMember(node); + case ParsingContext.SwitchClauses: + return isReusableSwitchClause(node); + case ParsingContext.SourceElements: + case ParsingContext.BlockStatements: + case ParsingContext.SwitchClauseStatements: + return isReusableStatement(node); + case ParsingContext.EnumMembers: + return isReusableEnumMember(node); + case ParsingContext.TypeMembers: + return isReusableTypeMember(node); + case ParsingContext.VariableDeclarations: + return isReusableVariableDeclaration(node); + case ParsingContext.JSDocParameters: + case ParsingContext.Parameters: + return isReusableParameter(node); + // Any other lists we do not care about reusing nodes in. But feel free to add if + // you can do so safely. Danger areas involve nodes that may involve speculative + // parsing. If speculative parsing is involved with the node, then the range the + // parser reached while looking ahead might be in the edited range (see the example + // in canReuseVariableDeclaratorNode for a good case of this). + // case ParsingContext.HeritageClauses: + // This would probably be safe to reuse. There is no speculative parsing with + // heritage clauses. + // case ParsingContext.TypeParameters: + // This would probably be safe to reuse. There is no speculative parsing with + // type parameters. Note that that's because type *parameters* only occur in + // unambiguous *type* contexts. While type *arguments* occur in very ambiguous + // *expression* contexts. + // case ParsingContext.TupleElementTypes: + // This would probably be safe to reuse. There is no speculative parsing with + // tuple types. + // Technically, type argument list types are probably safe to reuse. While + // speculative parsing is involved with them (since type argument lists are only + // produced from speculative parsing a < as a type argument list), we only have + // the types because speculative parsing succeeded. Thus, the lookahead never + // went past the end of the list and rewound. + // case ParsingContext.TypeArguments: + // Note: these are almost certainly not safe to ever reuse. Expressions commonly + // need a large amount of lookahead, and we should not reuse them as they may + // have actually intersected the edit. + // case ParsingContext.ArgumentExpressions: + // This is not safe to reuse for the same reason as the 'AssignmentExpression' + // cases. i.e. a property assignment may end with an expression, and thus might + // have lookahead far beyond it's old node. + // case ParsingContext.ObjectLiteralMembers: + // This is probably not safe to reuse. There can be speculative parsing with + // type names in a heritage clause. There can be generic names in the type + // name list, and there can be left hand side expressions (which can have type + // arguments.) + // case ParsingContext.HeritageClauseElement: + // Perhaps safe to reuse, but it's unlikely we'd see more than a dozen attributes + // on any given element. Same for children. + // case ParsingContext.JsxAttributes: + // case ParsingContext.JsxChildren: + } + return false; + } + function isReusableClassMember(node: Node) { + if (node) { + switch (node.kind) { + case SyntaxKind.Constructor: + case SyntaxKind.IndexSignature: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.SemicolonClassElement: return true; + case SyntaxKind.MethodDeclaration: + // Method declarations are not necessarily reusable. An object-literal + // may have a method calls "constructor(...)" and we must reparse that + // into an actual .ConstructorDeclaration. + const methodDeclaration = (node); + const nameIsConstructor = methodDeclaration.name.kind === SyntaxKind.Identifier && + methodDeclaration.name.originalKeywordKind === SyntaxKind.ConstructorKeyword; + return !nameIsConstructor; } - return false; } - - function canReuseNode(node: Node, parsingContext: ParsingContext): boolean { - switch (parsingContext) { - case ParsingContext.ClassMembers: - return isReusableClassMember(node); - - case ParsingContext.SwitchClauses: - return isReusableSwitchClause(node); - - case ParsingContext.SourceElements: - case ParsingContext.BlockStatements: - case ParsingContext.SwitchClauseStatements: - return isReusableStatement(node); - - case ParsingContext.EnumMembers: - return isReusableEnumMember(node); - - case ParsingContext.TypeMembers: - return isReusableTypeMember(node); - - case ParsingContext.VariableDeclarations: - return isReusableVariableDeclaration(node); - - case ParsingContext.JSDocParameters: - case ParsingContext.Parameters: - return isReusableParameter(node); - - // Any other lists we do not care about reusing nodes in. But feel free to add if - // you can do so safely. Danger areas involve nodes that may involve speculative - // parsing. If speculative parsing is involved with the node, then the range the - // parser reached while looking ahead might be in the edited range (see the example - // in canReuseVariableDeclaratorNode for a good case of this). - - // case ParsingContext.HeritageClauses: - // This would probably be safe to reuse. There is no speculative parsing with - // heritage clauses. - - // case ParsingContext.TypeParameters: - // This would probably be safe to reuse. There is no speculative parsing with - // type parameters. Note that that's because type *parameters* only occur in - // unambiguous *type* contexts. While type *arguments* occur in very ambiguous - // *expression* contexts. - - // case ParsingContext.TupleElementTypes: - // This would probably be safe to reuse. There is no speculative parsing with - // tuple types. - - // Technically, type argument list types are probably safe to reuse. While - // speculative parsing is involved with them (since type argument lists are only - // produced from speculative parsing a < as a type argument list), we only have - // the types because speculative parsing succeeded. Thus, the lookahead never - // went past the end of the list and rewound. - // case ParsingContext.TypeArguments: - - // Note: these are almost certainly not safe to ever reuse. Expressions commonly - // need a large amount of lookahead, and we should not reuse them as they may - // have actually intersected the edit. - // case ParsingContext.ArgumentExpressions: - - // This is not safe to reuse for the same reason as the 'AssignmentExpression' - // cases. i.e. a property assignment may end with an expression, and thus might - // have lookahead far beyond it's old node. - // case ParsingContext.ObjectLiteralMembers: - - // This is probably not safe to reuse. There can be speculative parsing with - // type names in a heritage clause. There can be generic names in the type - // name list, and there can be left hand side expressions (which can have type - // arguments.) - // case ParsingContext.HeritageClauseElement: - - // Perhaps safe to reuse, but it's unlikely we'd see more than a dozen attributes - // on any given element. Same for children. - // case ParsingContext.JsxAttributes: - // case ParsingContext.JsxChildren: - - } - - return false; + return false; + } + function isReusableSwitchClause(node: Node) { + if (node) { + switch (node.kind) { + case SyntaxKind.CaseClause: + case SyntaxKind.DefaultClause: + return true; + } } - - function isReusableClassMember(node: Node) { - if (node) { - switch (node.kind) { - case SyntaxKind.Constructor: - case SyntaxKind.IndexSignature: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.SemicolonClassElement: - return true; - case SyntaxKind.MethodDeclaration: - // Method declarations are not necessarily reusable. An object-literal - // may have a method calls "constructor(...)" and we must reparse that - // into an actual .ConstructorDeclaration. - const methodDeclaration = node; - const nameIsConstructor = methodDeclaration.name.kind === SyntaxKind.Identifier && - methodDeclaration.name.originalKeywordKind === SyntaxKind.ConstructorKeyword; - - return !nameIsConstructor; - } + return false; + } + function isReusableStatement(node: Node) { + if (node) { + switch (node.kind) { + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.VariableStatement: + case SyntaxKind.Block: + case SyntaxKind.IfStatement: + case SyntaxKind.ExpressionStatement: + case SyntaxKind.ThrowStatement: + case SyntaxKind.ReturnStatement: + case SyntaxKind.SwitchStatement: + case SyntaxKind.BreakStatement: + case SyntaxKind.ContinueStatement: + case SyntaxKind.ForInStatement: + case SyntaxKind.ForOfStatement: + case SyntaxKind.ForStatement: + case SyntaxKind.WhileStatement: + case SyntaxKind.WithStatement: + case SyntaxKind.EmptyStatement: + case SyntaxKind.TryStatement: + case SyntaxKind.LabeledStatement: + case SyntaxKind.DoStatement: + case SyntaxKind.DebuggerStatement: + case SyntaxKind.ImportDeclaration: + case SyntaxKind.ImportEqualsDeclaration: + case SyntaxKind.ExportDeclaration: + case SyntaxKind.ExportAssignment: + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.TypeAliasDeclaration: + return true; } - - return false; } - - function isReusableSwitchClause(node: Node) { - if (node) { - switch (node.kind) { - case SyntaxKind.CaseClause: - case SyntaxKind.DefaultClause: - return true; - } + return false; + } + function isReusableEnumMember(node: Node) { + return node.kind === SyntaxKind.EnumMember; + } + function isReusableTypeMember(node: Node) { + if (node) { + switch (node.kind) { + case SyntaxKind.ConstructSignature: + case SyntaxKind.MethodSignature: + case SyntaxKind.IndexSignature: + case SyntaxKind.PropertySignature: + case SyntaxKind.CallSignature: + return true; } - + } + return false; + } + function isReusableVariableDeclaration(node: Node) { + if (node.kind !== SyntaxKind.VariableDeclaration) { return false; } - - function isReusableStatement(node: Node) { - if (node) { - switch (node.kind) { - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.VariableStatement: - case SyntaxKind.Block: - case SyntaxKind.IfStatement: - case SyntaxKind.ExpressionStatement: - case SyntaxKind.ThrowStatement: - case SyntaxKind.ReturnStatement: - case SyntaxKind.SwitchStatement: - case SyntaxKind.BreakStatement: - case SyntaxKind.ContinueStatement: - case SyntaxKind.ForInStatement: - case SyntaxKind.ForOfStatement: - case SyntaxKind.ForStatement: - case SyntaxKind.WhileStatement: - case SyntaxKind.WithStatement: - case SyntaxKind.EmptyStatement: - case SyntaxKind.TryStatement: - case SyntaxKind.LabeledStatement: - case SyntaxKind.DoStatement: - case SyntaxKind.DebuggerStatement: - case SyntaxKind.ImportDeclaration: - case SyntaxKind.ImportEqualsDeclaration: - case SyntaxKind.ExportDeclaration: - case SyntaxKind.ExportAssignment: - case SyntaxKind.ModuleDeclaration: - case SyntaxKind.ClassDeclaration: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.TypeAliasDeclaration: - return true; - } - } - + // Very subtle incremental parsing bug. Consider the following code: + // + // let v = new List < A, B + // + // This is actually legal code. It's a list of variable declarators "v = new List() + // + // then we have a problem. "v = new Listnode); + return variableDeclarator.initializer === undefined; + } + function isReusableParameter(node: Node) { + if (node.kind !== SyntaxKind.Parameter) { return false; } - - function isReusableEnumMember(node: Node) { - return node.kind === SyntaxKind.EnumMember; + // See the comment in isReusableVariableDeclaration for why we do this. + const parameter = (node); + return parameter.initializer === undefined; + } + // Returns true if we should abort parsing. + function abortParsingListOrMoveToNextToken(kind: ParsingContext) { + parseErrorAtCurrentToken(parsingContextErrors(kind)); + if (isInSomeParsingContext()) { + return true; + } + nextToken(); + return false; + } + function parsingContextErrors(context: ParsingContext): DiagnosticMessage { + switch (context) { + case ParsingContext.SourceElements: return Diagnostics.Declaration_or_statement_expected; + case ParsingContext.BlockStatements: return Diagnostics.Declaration_or_statement_expected; + case ParsingContext.SwitchClauses: return Diagnostics.case_or_default_expected; + case ParsingContext.SwitchClauseStatements: return Diagnostics.Statement_expected; + case ParsingContext.RestProperties: // fallthrough + case ParsingContext.TypeMembers: return Diagnostics.Property_or_signature_expected; + case ParsingContext.ClassMembers: return Diagnostics.Unexpected_token_A_constructor_method_accessor_or_property_was_expected; + case ParsingContext.EnumMembers: return Diagnostics.Enum_member_expected; + case ParsingContext.HeritageClauseElement: return Diagnostics.Expression_expected; + case ParsingContext.VariableDeclarations: return Diagnostics.Variable_declaration_expected; + case ParsingContext.ObjectBindingElements: return Diagnostics.Property_destructuring_pattern_expected; + case ParsingContext.ArrayBindingElements: return Diagnostics.Array_element_destructuring_pattern_expected; + case ParsingContext.ArgumentExpressions: return Diagnostics.Argument_expression_expected; + case ParsingContext.ObjectLiteralMembers: return Diagnostics.Property_assignment_expected; + case ParsingContext.ArrayLiteralMembers: return Diagnostics.Expression_or_comma_expected; + case ParsingContext.JSDocParameters: return Diagnostics.Parameter_declaration_expected; + case ParsingContext.Parameters: return Diagnostics.Parameter_declaration_expected; + case ParsingContext.TypeParameters: return Diagnostics.Type_parameter_declaration_expected; + case ParsingContext.TypeArguments: return Diagnostics.Type_argument_expected; + case ParsingContext.TupleElementTypes: return Diagnostics.Type_expected; + case ParsingContext.HeritageClauses: return Diagnostics.Unexpected_token_expected; + case ParsingContext.ImportOrExportSpecifiers: return Diagnostics.Identifier_expected; + case ParsingContext.JsxAttributes: return Diagnostics.Identifier_expected; + case ParsingContext.JsxChildren: return Diagnostics.Identifier_expected; + default: return undefined!; // TODO: GH#18217 `default: Debug.assertNever(context);` } - - function isReusableTypeMember(node: Node) { - if (node) { - switch (node.kind) { - case SyntaxKind.ConstructSignature: - case SyntaxKind.MethodSignature: - case SyntaxKind.IndexSignature: - case SyntaxKind.PropertySignature: - case SyntaxKind.CallSignature: - return true; + } + // Parses a comma-delimited list of elements + function parseDelimitedList(kind: ParsingContext, parseElement: () => T, considerSemicolonAsDelimiter?: boolean): NodeArray { + const saveParsingContext = parsingContext; + parsingContext |= 1 << kind; + const list = []; + const listPos = getNodePos(); + let commaStart = -1; // Meaning the previous token was not a comma + while (true) { + if (isListElement(kind, /*inErrorRecovery*/ false)) { + const startPos = scanner.getStartPos(); + list.push(parseListElement(kind, parseElement)); + commaStart = scanner.getTokenPos(); + if (parseOptional(SyntaxKind.CommaToken)) { + // No need to check for a zero length node since we know we parsed a comma + continue; } + commaStart = -1; // Back to the state where the last token was not a comma + if (isListTerminator(kind)) { + break; + } + // We didn't get a comma, and the list wasn't terminated, explicitly parse + // out a comma so we give a good error message. + parseExpected(SyntaxKind.CommaToken, getExpectedCommaDiagnostic(kind)); + // If the token was a semicolon, and the caller allows that, then skip it and + // continue. This ensures we get back on track and don't result in tons of + // parse errors. For example, this can happen when people do things like use + // a semicolon to delimit object literal members. Note: we'll have already + // reported an error when we called parseExpected above. + if (considerSemicolonAsDelimiter && token() === SyntaxKind.SemicolonToken && !scanner.hasPrecedingLineBreak()) { + nextToken(); + } + if (startPos === scanner.getStartPos()) { + // What we're parsing isn't actually remotely recognizable as a element and we've consumed no tokens whatsoever + // Consume a token to advance the parser in some way and avoid an infinite loop + // This can happen when we're speculatively parsing parenthesized expressions which we think may be arrow functions, + // or when a modifier keyword which is disallowed as a parameter name (ie, `static` in strict mode) is supplied + nextToken(); + } + continue; } - - return false; - } - - function isReusableVariableDeclaration(node: Node) { - if (node.kind !== SyntaxKind.VariableDeclaration) { - return false; + if (isListTerminator(kind)) { + break; } - - // Very subtle incremental parsing bug. Consider the following code: - // - // let v = new List < A, B - // - // This is actually legal code. It's a list of variable declarators "v = new List() - // - // then we have a problem. "v = new Listnode; - return variableDeclarator.initializer === undefined; - } - - function isReusableParameter(node: Node) { - if (node.kind !== SyntaxKind.Parameter) { - return false; + if (abortParsingListOrMoveToNextToken(kind)) { + break; } - - // See the comment in isReusableVariableDeclaration for why we do this. - const parameter = node; - return parameter.initializer === undefined; } - - // Returns true if we should abort parsing. - function abortParsingListOrMoveToNextToken(kind: ParsingContext) { - parseErrorAtCurrentToken(parsingContextErrors(kind)); - if (isInSomeParsingContext()) { - return true; - } - - nextToken(); - return false; + parsingContext = saveParsingContext; + const result = createNodeArray(list, listPos); + // Recording the trailing comma is deliberately done after the previous + // loop, and not just if we see a list terminator. This is because the list + // may have ended incorrectly, but it is still important to know if there + // was a trailing comma. + // Check if the last token was a comma. + if (commaStart >= 0) { + // Always preserve a trailing comma by marking it on the NodeArray + result.hasTrailingComma = true; } - - function parsingContextErrors(context: ParsingContext): DiagnosticMessage { - switch (context) { - case ParsingContext.SourceElements: return Diagnostics.Declaration_or_statement_expected; - case ParsingContext.BlockStatements: return Diagnostics.Declaration_or_statement_expected; - case ParsingContext.SwitchClauses: return Diagnostics.case_or_default_expected; - case ParsingContext.SwitchClauseStatements: return Diagnostics.Statement_expected; - case ParsingContext.RestProperties: // fallthrough - case ParsingContext.TypeMembers: return Diagnostics.Property_or_signature_expected; - case ParsingContext.ClassMembers: return Diagnostics.Unexpected_token_A_constructor_method_accessor_or_property_was_expected; - case ParsingContext.EnumMembers: return Diagnostics.Enum_member_expected; - case ParsingContext.HeritageClauseElement: return Diagnostics.Expression_expected; - case ParsingContext.VariableDeclarations: return Diagnostics.Variable_declaration_expected; - case ParsingContext.ObjectBindingElements: return Diagnostics.Property_destructuring_pattern_expected; - case ParsingContext.ArrayBindingElements: return Diagnostics.Array_element_destructuring_pattern_expected; - case ParsingContext.ArgumentExpressions: return Diagnostics.Argument_expression_expected; - case ParsingContext.ObjectLiteralMembers: return Diagnostics.Property_assignment_expected; - case ParsingContext.ArrayLiteralMembers: return Diagnostics.Expression_or_comma_expected; - case ParsingContext.JSDocParameters: return Diagnostics.Parameter_declaration_expected; - case ParsingContext.Parameters: return Diagnostics.Parameter_declaration_expected; - case ParsingContext.TypeParameters: return Diagnostics.Type_parameter_declaration_expected; - case ParsingContext.TypeArguments: return Diagnostics.Type_argument_expected; - case ParsingContext.TupleElementTypes: return Diagnostics.Type_expected; - case ParsingContext.HeritageClauses: return Diagnostics.Unexpected_token_expected; - case ParsingContext.ImportOrExportSpecifiers: return Diagnostics.Identifier_expected; - case ParsingContext.JsxAttributes: return Diagnostics.Identifier_expected; - case ParsingContext.JsxChildren: return Diagnostics.Identifier_expected; - default: return undefined!; // TODO: GH#18217 `default: Debug.assertNever(context);` - } - } - - // Parses a comma-delimited list of elements - function parseDelimitedList(kind: ParsingContext, parseElement: () => T, considerSemicolonAsDelimiter?: boolean): NodeArray { - const saveParsingContext = parsingContext; - parsingContext |= 1 << kind; - const list = []; - const listPos = getNodePos(); - - let commaStart = -1; // Meaning the previous token was not a comma - while (true) { - if (isListElement(kind, /*inErrorRecovery*/ false)) { - const startPos = scanner.getStartPos(); - list.push(parseListElement(kind, parseElement)); - commaStart = scanner.getTokenPos(); - - if (parseOptional(SyntaxKind.CommaToken)) { - // No need to check for a zero length node since we know we parsed a comma - continue; - } - - commaStart = -1; // Back to the state where the last token was not a comma - if (isListTerminator(kind)) { - break; - } - - // We didn't get a comma, and the list wasn't terminated, explicitly parse - // out a comma so we give a good error message. - parseExpected(SyntaxKind.CommaToken, getExpectedCommaDiagnostic(kind)); - - // If the token was a semicolon, and the caller allows that, then skip it and - // continue. This ensures we get back on track and don't result in tons of - // parse errors. For example, this can happen when people do things like use - // a semicolon to delimit object literal members. Note: we'll have already - // reported an error when we called parseExpected above. - if (considerSemicolonAsDelimiter && token() === SyntaxKind.SemicolonToken && !scanner.hasPrecedingLineBreak()) { - nextToken(); - } - if (startPos === scanner.getStartPos()) { - // What we're parsing isn't actually remotely recognizable as a element and we've consumed no tokens whatsoever - // Consume a token to advance the parser in some way and avoid an infinite loop - // This can happen when we're speculatively parsing parenthesized expressions which we think may be arrow functions, - // or when a modifier keyword which is disallowed as a parameter name (ie, `static` in strict mode) is supplied - nextToken(); - } - continue; - } - - if (isListTerminator(kind)) { - break; - } - - if (abortParsingListOrMoveToNextToken(kind)) { - break; - } - } - - parsingContext = saveParsingContext; - const result = createNodeArray(list, listPos); - // Recording the trailing comma is deliberately done after the previous - // loop, and not just if we see a list terminator. This is because the list - // may have ended incorrectly, but it is still important to know if there - // was a trailing comma. - // Check if the last token was a comma. - if (commaStart >= 0) { - // Always preserve a trailing comma by marking it on the NodeArray - result.hasTrailingComma = true; - } + return result; + } + function getExpectedCommaDiagnostic(kind: ParsingContext) { + return kind === ParsingContext.EnumMembers ? Diagnostics.An_enum_member_name_must_be_followed_by_a_or : undefined; + } + interface MissingList extends NodeArray { + isMissingList: true; + } + function createMissingList(): MissingList { + const list = createNodeArray([], getNodePos()) as MissingList; + list.isMissingList = true; + return list; + } + function isMissingList(arr: NodeArray): boolean { + return !!(arr as MissingList).isMissingList; + } + function parseBracketedList(kind: ParsingContext, parseElement: () => T, open: SyntaxKind, close: SyntaxKind): NodeArray { + if (parseExpected(open)) { + const result = parseDelimitedList(kind, parseElement); + parseExpected(close); return result; } - - function getExpectedCommaDiagnostic(kind: ParsingContext) { - return kind === ParsingContext.EnumMembers ? Diagnostics.An_enum_member_name_must_be_followed_by_a_or : undefined; - } - - interface MissingList extends NodeArray { - isMissingList: true; - } - - function createMissingList(): MissingList { - const list = createNodeArray([], getNodePos()) as MissingList; - list.isMissingList = true; - return list; - } - - function isMissingList(arr: NodeArray): boolean { - return !!(arr as MissingList).isMissingList; - } - - function parseBracketedList(kind: ParsingContext, parseElement: () => T, open: SyntaxKind, close: SyntaxKind): NodeArray { - if (parseExpected(open)) { - const result = parseDelimitedList(kind, parseElement); - parseExpected(close); - return result; - } - - return createMissingList(); - } - - function parseEntityName(allowReservedWords: boolean, diagnosticMessage?: DiagnosticMessage): EntityName { - let entity: EntityName = allowReservedWords ? parseIdentifierName(diagnosticMessage) : parseIdentifier(diagnosticMessage); - let dotPos = scanner.getStartPos(); - while (parseOptional(SyntaxKind.DotToken)) { - if (token() === SyntaxKind.LessThanToken) { - // the entity is part of a JSDoc-style generic, so record the trailing dot for later error reporting - entity.jsdocDotPos = dotPos; - break; - } - dotPos = scanner.getStartPos(); - entity = createQualifiedName(entity, parseRightSideOfDot(allowReservedWords, /* allowPrivateIdentifiers */ false) as Identifier); + return createMissingList(); + } + function parseEntityName(allowReservedWords: boolean, diagnosticMessage?: DiagnosticMessage): EntityName { + let entity: EntityName = allowReservedWords ? parseIdentifierName(diagnosticMessage) : parseIdentifier(diagnosticMessage); + let dotPos = scanner.getStartPos(); + while (parseOptional(SyntaxKind.DotToken)) { + if (token() === SyntaxKind.LessThanToken) { + // the entity is part of a JSDoc-style generic, so record the trailing dot for later error reporting + entity.jsdocDotPos = dotPos; + break; } - return entity; + dotPos = scanner.getStartPos(); + entity = createQualifiedName(entity, (parseRightSideOfDot(allowReservedWords, /* allowPrivateIdentifiers */ false) as Identifier)); } - - function createQualifiedName(entity: EntityName, name: Identifier): QualifiedName { - const node = createNode(SyntaxKind.QualifiedName, entity.pos) as QualifiedName; - node.left = entity; - node.right = name; - return finishNode(node); + return entity; + } + function createQualifiedName(entity: EntityName, name: Identifier): QualifiedName { + const node = (createNode(SyntaxKind.QualifiedName, entity.pos) as QualifiedName); + node.left = entity; + node.right = name; + return finishNode(node); + } + function parseRightSideOfDot(allowIdentifierNames: boolean, allowPrivateIdentifiers: boolean): Identifier | PrivateIdentifier { + // Technically a keyword is valid here as all identifiers and keywords are identifier names. + // However, often we'll encounter this in error situations when the identifier or keyword + // is actually starting another valid construct. + // + // So, we check for the following specific case: + // + // name. + // identifierOrKeyword identifierNameOrKeyword + // + // Note: the newlines are important here. For example, if that above code + // were rewritten into: + // + // name.identifierOrKeyword + // identifierNameOrKeyword + // + // Then we would consider it valid. That's because ASI would take effect and + // the code would be implicitly: "name.identifierOrKeyword; identifierNameOrKeyword". + // In the first case though, ASI will not take effect because there is not a + // line terminator after the identifier or keyword. + if (scanner.hasPrecedingLineBreak() && tokenIsIdentifierOrKeyword(token())) { + const matchesPattern = lookAhead(nextTokenIsIdentifierOrKeywordOnSameLine); + if (matchesPattern) { + // Report that we need an identifier. However, report it right after the dot, + // and not on the next token. This is because the next token might actually + // be an identifier and the error would be quite confusing. + return createMissingNode(SyntaxKind.Identifier, /*reportAtCurrentPosition*/ true, Diagnostics.Identifier_expected); + } + } + if (token() === SyntaxKind.PrivateIdentifier) { + const node = parsePrivateIdentifier(); + return allowPrivateIdentifiers ? node : createMissingNode(SyntaxKind.Identifier, /*reportAtCurrentPosition*/ true, Diagnostics.Identifier_expected); + } + return allowIdentifierNames ? parseIdentifierName() : parseIdentifier(); + } + function parseTemplateExpression(isTaggedTemplate: boolean): TemplateExpression { + const template = (createNode(SyntaxKind.TemplateExpression)); + template.head = parseTemplateHead(isTaggedTemplate); + Debug.assert(template.head.kind === SyntaxKind.TemplateHead, "Template head has wrong token kind"); + const list = []; + const listPos = getNodePos(); + do { + list.push(parseTemplateSpan(isTaggedTemplate)); + } while (last(list).literal.kind === SyntaxKind.TemplateMiddle); + template.templateSpans = createNodeArray(list, listPos); + return finishNode(template); + } + function parseTemplateSpan(isTaggedTemplate: boolean): TemplateSpan { + const span = (createNode(SyntaxKind.TemplateSpan)); + span.expression = allowInAnd(parseExpression); + let literal: TemplateMiddle | TemplateTail; + if (token() === SyntaxKind.CloseBraceToken) { + reScanTemplateToken(isTaggedTemplate); + literal = parseTemplateMiddleOrTemplateTail(); } - - function parseRightSideOfDot(allowIdentifierNames: boolean, allowPrivateIdentifiers: boolean): Identifier | PrivateIdentifier { - // Technically a keyword is valid here as all identifiers and keywords are identifier names. - // However, often we'll encounter this in error situations when the identifier or keyword - // is actually starting another valid construct. - // - // So, we check for the following specific case: - // - // name. - // identifierOrKeyword identifierNameOrKeyword - // - // Note: the newlines are important here. For example, if that above code - // were rewritten into: - // - // name.identifierOrKeyword - // identifierNameOrKeyword - // - // Then we would consider it valid. That's because ASI would take effect and - // the code would be implicitly: "name.identifierOrKeyword; identifierNameOrKeyword". - // In the first case though, ASI will not take effect because there is not a - // line terminator after the identifier or keyword. - if (scanner.hasPrecedingLineBreak() && tokenIsIdentifierOrKeyword(token())) { - const matchesPattern = lookAhead(nextTokenIsIdentifierOrKeywordOnSameLine); - - if (matchesPattern) { - // Report that we need an identifier. However, report it right after the dot, - // and not on the next token. This is because the next token might actually - // be an identifier and the error would be quite confusing. - return createMissingNode(SyntaxKind.Identifier, /*reportAtCurrentPosition*/ true, Diagnostics.Identifier_expected); - } - } - - if (token() === SyntaxKind.PrivateIdentifier) { - const node = parsePrivateIdentifier(); - return allowPrivateIdentifiers ? node : createMissingNode(SyntaxKind.Identifier, /*reportAtCurrentPosition*/ true, Diagnostics.Identifier_expected); - } - - return allowIdentifierNames ? parseIdentifierName() : parseIdentifier(); - } - - function parseTemplateExpression(isTaggedTemplate: boolean): TemplateExpression { - const template = createNode(SyntaxKind.TemplateExpression); - - template.head = parseTemplateHead(isTaggedTemplate); - Debug.assert(template.head.kind === SyntaxKind.TemplateHead, "Template head has wrong token kind"); - - const list = []; - const listPos = getNodePos(); - - do { - list.push(parseTemplateSpan(isTaggedTemplate)); - } - while (last(list).literal.kind === SyntaxKind.TemplateMiddle); - - template.templateSpans = createNodeArray(list, listPos); - - return finishNode(template); - } - - function parseTemplateSpan(isTaggedTemplate: boolean): TemplateSpan { - const span = createNode(SyntaxKind.TemplateSpan); - span.expression = allowInAnd(parseExpression); - - let literal: TemplateMiddle | TemplateTail; - if (token() === SyntaxKind.CloseBraceToken) { - reScanTemplateToken(isTaggedTemplate); - literal = parseTemplateMiddleOrTemplateTail(); - } - else { - literal = parseExpectedToken(SyntaxKind.TemplateTail, Diagnostics._0_expected, tokenToString(SyntaxKind.CloseBraceToken)); - } - - span.literal = literal; - return finishNode(span); - } - - function parseLiteralNode(): LiteralExpression { - return parseLiteralLikeNode(token()); - } - - function parseTemplateHead(isTaggedTemplate: boolean): TemplateHead { - if (isTaggedTemplate) { - reScanTemplateHeadOrNoSubstitutionTemplate(); - } - const fragment = parseLiteralLikeNode(token()); - Debug.assert(fragment.kind === SyntaxKind.TemplateHead, "Template head has wrong token kind"); - return fragment; - } - - function parseTemplateMiddleOrTemplateTail(): TemplateMiddle | TemplateTail { - const fragment = parseLiteralLikeNode(token()); - Debug.assert(fragment.kind === SyntaxKind.TemplateMiddle || fragment.kind === SyntaxKind.TemplateTail, "Template fragment has wrong token kind"); - return fragment; - } - - function parseLiteralLikeNode(kind: SyntaxKind): LiteralLikeNode { - const node = createNode(kind); - node.text = scanner.getTokenValue(); - switch (kind) { - case SyntaxKind.NoSubstitutionTemplateLiteral: - case SyntaxKind.TemplateHead: - case SyntaxKind.TemplateMiddle: - case SyntaxKind.TemplateTail: - const isLast = kind === SyntaxKind.NoSubstitutionTemplateLiteral || kind === SyntaxKind.TemplateTail; - const tokenText = scanner.getTokenText(); - (node).rawText = tokenText.substring(1, tokenText.length - (scanner.isUnterminated() ? 0 : isLast ? 1 : 2)); - break; - } - - if (scanner.hasExtendedUnicodeEscape()) { - node.hasExtendedUnicodeEscape = true; - } - - if (scanner.isUnterminated()) { - node.isUnterminated = true; - } - - // Octal literals are not allowed in strict mode or ES5 - // Note that theoretically the following condition would hold true literals like 009, - // which is not octal.But because of how the scanner separates the tokens, we would - // never get a token like this. Instead, we would get 00 and 9 as two separate tokens. - // We also do not need to check for negatives because any prefix operator would be part of a - // parent unary expression. - if (node.kind === SyntaxKind.NumericLiteral) { - (node).numericLiteralFlags = scanner.getTokenFlags() & TokenFlags.NumericLiteralFlags; - } - - if (isTemplateLiteralKind(node.kind)) { - (node).templateFlags = scanner.getTokenFlags() & TokenFlags.ContainsInvalidEscape; - } - - nextToken(); - finishNode(node); - - return node; + else { + literal = (parseExpectedToken(SyntaxKind.TemplateTail, Diagnostics._0_expected, tokenToString(SyntaxKind.CloseBraceToken))); } - - // TYPES - - function parseTypeReference(): TypeReferenceNode { - const node = createNode(SyntaxKind.TypeReference); - node.typeName = parseEntityName(/*allowReservedWords*/ true, Diagnostics.Type_expected); - if (!scanner.hasPrecedingLineBreak() && reScanLessThanToken() === SyntaxKind.LessThanToken) { - node.typeArguments = parseBracketedList(ParsingContext.TypeArguments, parseType, SyntaxKind.LessThanToken, SyntaxKind.GreaterThanToken); - } - return finishNode(node); + span.literal = literal; + return finishNode(span); + } + function parseLiteralNode(): LiteralExpression { + return parseLiteralLikeNode(token()); + } + function parseTemplateHead(isTaggedTemplate: boolean): TemplateHead { + if (isTaggedTemplate) { + reScanTemplateHeadOrNoSubstitutionTemplate(); } - - // If true, we should abort parsing an error function. - function typeHasArrowFunctionBlockingParseError(node: TypeNode): boolean { - switch (node.kind) { - case SyntaxKind.TypeReference: - return nodeIsMissing((node as TypeReferenceNode).typeName); - case SyntaxKind.FunctionType: - case SyntaxKind.ConstructorType: { - const { parameters, type } = node as FunctionOrConstructorTypeNode; - return isMissingList(parameters) || typeHasArrowFunctionBlockingParseError(type); - } - case SyntaxKind.ParenthesizedType: - return typeHasArrowFunctionBlockingParseError((node as ParenthesizedTypeNode).type); - default: - return false; + const fragment = parseLiteralLikeNode(token()); + Debug.assert(fragment.kind === SyntaxKind.TemplateHead, "Template head has wrong token kind"); + return fragment; + } + function parseTemplateMiddleOrTemplateTail(): TemplateMiddle | TemplateTail { + const fragment = parseLiteralLikeNode(token()); + Debug.assert(fragment.kind === SyntaxKind.TemplateMiddle || fragment.kind === SyntaxKind.TemplateTail, "Template fragment has wrong token kind"); + return fragment; + } + function parseLiteralLikeNode(kind: SyntaxKind): LiteralLikeNode { + const node = (createNode(kind)); + node.text = scanner.getTokenValue(); + switch (kind) { + case SyntaxKind.NoSubstitutionTemplateLiteral: + case SyntaxKind.TemplateHead: + case SyntaxKind.TemplateMiddle: + case SyntaxKind.TemplateTail: + const isLast = kind === SyntaxKind.NoSubstitutionTemplateLiteral || kind === SyntaxKind.TemplateTail; + const tokenText = scanner.getTokenText(); + (node).rawText = tokenText.substring(1, tokenText.length - (scanner.isUnterminated() ? 0 : isLast ? 1 : 2)); + break; + } + if (scanner.hasExtendedUnicodeEscape()) { + node.hasExtendedUnicodeEscape = true; + } + if (scanner.isUnterminated()) { + node.isUnterminated = true; + } + // Octal literals are not allowed in strict mode or ES5 + // Note that theoretically the following condition would hold true literals like 009, + // which is not octal.But because of how the scanner separates the tokens, we would + // never get a token like this. Instead, we would get 00 and 9 as two separate tokens. + // We also do not need to check for negatives because any prefix operator would be part of a + // parent unary expression. + if (node.kind === SyntaxKind.NumericLiteral) { + (node).numericLiteralFlags = scanner.getTokenFlags() & TokenFlags.NumericLiteralFlags; + } + if (isTemplateLiteralKind(node.kind)) { + (node).templateFlags = scanner.getTokenFlags() & TokenFlags.ContainsInvalidEscape; + } + nextToken(); + finishNode(node); + return node; + } + // TYPES + function parseTypeReference(): TypeReferenceNode { + const node = (createNode(SyntaxKind.TypeReference)); + node.typeName = parseEntityName(/*allowReservedWords*/ true, Diagnostics.Type_expected); + if (!scanner.hasPrecedingLineBreak() && reScanLessThanToken() === SyntaxKind.LessThanToken) { + node.typeArguments = parseBracketedList(ParsingContext.TypeArguments, parseType, SyntaxKind.LessThanToken, SyntaxKind.GreaterThanToken); + } + return finishNode(node); + } + // If true, we should abort parsing an error function. + function typeHasArrowFunctionBlockingParseError(node: TypeNode): boolean { + switch (node.kind) { + case SyntaxKind.TypeReference: + return nodeIsMissing((node as TypeReferenceNode).typeName); + case SyntaxKind.FunctionType: + case SyntaxKind.ConstructorType: { + const { parameters, type } = (node as FunctionOrConstructorTypeNode); + return isMissingList(parameters) || typeHasArrowFunctionBlockingParseError(type); } + case SyntaxKind.ParenthesizedType: + return typeHasArrowFunctionBlockingParseError((node as ParenthesizedTypeNode).type); + default: + return false; } - - function parseThisTypePredicate(lhs: ThisTypeNode): TypePredicateNode { - nextToken(); - const node = createNode(SyntaxKind.TypePredicate, lhs.pos) as TypePredicateNode; - node.parameterName = lhs; - node.type = parseType(); - return finishNode(node); + } + function parseThisTypePredicate(lhs: ThisTypeNode): TypePredicateNode { + nextToken(); + const node = (createNode(SyntaxKind.TypePredicate, lhs.pos) as TypePredicateNode); + node.parameterName = lhs; + node.type = parseType(); + return finishNode(node); + } + function parseThisTypeNode(): ThisTypeNode { + const node = (createNode(SyntaxKind.ThisType) as ThisTypeNode); + nextToken(); + return finishNode(node); + } + function parseJSDocAllType(postFixEquals: boolean): JSDocAllType | JSDocOptionalType { + const result = (createNode(SyntaxKind.JSDocAllType) as JSDocAllType); + if (postFixEquals) { + return createPostfixType(SyntaxKind.JSDocOptionalType, result) as JSDocOptionalType; } - - function parseThisTypeNode(): ThisTypeNode { - const node = createNode(SyntaxKind.ThisType) as ThisTypeNode; + else { nextToken(); - return finishNode(node); } - - function parseJSDocAllType(postFixEquals: boolean): JSDocAllType | JSDocOptionalType { - const result = createNode(SyntaxKind.JSDocAllType) as JSDocAllType; - if (postFixEquals) { - return createPostfixType(SyntaxKind.JSDocOptionalType, result) as JSDocOptionalType; - } - else { - nextToken(); - } + return finishNode(result); + } + function parseJSDocNonNullableType(): TypeNode { + const result = (createNode(SyntaxKind.JSDocNonNullableType) as JSDocNonNullableType); + nextToken(); + result.type = parseNonArrayType(); + return finishNode(result); + } + function parseJSDocUnknownOrNullableType(): JSDocUnknownType | JSDocNullableType { + const pos = scanner.getStartPos(); + // skip the ? + nextToken(); + // Need to lookahead to decide if this is a nullable or unknown type. + // Here are cases where we'll pick the unknown type: + // + // Foo(?, + // { a: ? } + // Foo(?) + // Foo + // Foo(?= + // (?| + if (token() === SyntaxKind.CommaToken || + token() === SyntaxKind.CloseBraceToken || + token() === SyntaxKind.CloseParenToken || + token() === SyntaxKind.GreaterThanToken || + token() === SyntaxKind.EqualsToken || + token() === SyntaxKind.BarToken) { + const result = (createNode(SyntaxKind.JSDocUnknownType, pos)); return finishNode(result); } - - function parseJSDocNonNullableType(): TypeNode { - const result = createNode(SyntaxKind.JSDocNonNullableType) as JSDocNonNullableType; - nextToken(); - result.type = parseNonArrayType(); + else { + const result = (createNode(SyntaxKind.JSDocNullableType, pos)); + result.type = parseType(); return finishNode(result); } - - function parseJSDocUnknownOrNullableType(): JSDocUnknownType | JSDocNullableType { - const pos = scanner.getStartPos(); - // skip the ? + } + function parseJSDocFunctionType(): JSDocFunctionType | TypeReferenceNode { + if (lookAhead(nextTokenIsOpenParen)) { + const result = (createNodeWithJSDoc(SyntaxKind.JSDocFunctionType)); nextToken(); - - // Need to lookahead to decide if this is a nullable or unknown type. - - // Here are cases where we'll pick the unknown type: - // - // Foo(?, - // { a: ? } - // Foo(?) - // Foo - // Foo(?= - // (?| - if (token() === SyntaxKind.CommaToken || - token() === SyntaxKind.CloseBraceToken || - token() === SyntaxKind.CloseParenToken || - token() === SyntaxKind.GreaterThanToken || - token() === SyntaxKind.EqualsToken || - token() === SyntaxKind.BarToken) { - - const result = createNode(SyntaxKind.JSDocUnknownType, pos); - return finishNode(result); - } - else { - const result = createNode(SyntaxKind.JSDocNullableType, pos); - result.type = parseType(); - return finishNode(result); - } + fillSignature(SyntaxKind.ColonToken, SignatureFlags.Type | SignatureFlags.JSDoc, result); + return finishNode(result); } - - function parseJSDocFunctionType(): JSDocFunctionType | TypeReferenceNode { - if (lookAhead(nextTokenIsOpenParen)) { - const result = createNodeWithJSDoc(SyntaxKind.JSDocFunctionType); - nextToken(); - fillSignature(SyntaxKind.ColonToken, SignatureFlags.Type | SignatureFlags.JSDoc, result); - return finishNode(result); - } - const node = createNode(SyntaxKind.TypeReference); - node.typeName = parseIdentifierName(); - return finishNode(node); + const node = (createNode(SyntaxKind.TypeReference)); + node.typeName = parseIdentifierName(); + return finishNode(node); + } + function parseJSDocParameter(): ParameterDeclaration { + const parameter = (createNode(SyntaxKind.Parameter) as ParameterDeclaration); + if (token() === SyntaxKind.ThisKeyword || token() === SyntaxKind.NewKeyword) { + parameter.name = parseIdentifierName(); + parseExpected(SyntaxKind.ColonToken); } - - function parseJSDocParameter(): ParameterDeclaration { - const parameter = createNode(SyntaxKind.Parameter) as ParameterDeclaration; - if (token() === SyntaxKind.ThisKeyword || token() === SyntaxKind.NewKeyword) { - parameter.name = parseIdentifierName(); - parseExpected(SyntaxKind.ColonToken); - } - parameter.type = parseJSDocType(); - return finishNode(parameter); - } - - function parseJSDocType(): TypeNode { - scanner.setInJSDocType(true); - const moduleSpecifier = parseOptionalToken(SyntaxKind.ModuleKeyword); - if (moduleSpecifier) { - const moduleTag = createNode(SyntaxKind.JSDocNamepathType, moduleSpecifier.pos) as JSDocNamepathType; - terminate: while (true) { - switch (token()) { - case SyntaxKind.CloseBraceToken: - case SyntaxKind.EndOfFileToken: - case SyntaxKind.CommaToken: - case SyntaxKind.WhitespaceTrivia: - break terminate; - default: - nextTokenJSDoc(); - } + parameter.type = parseJSDocType(); + return finishNode(parameter); + } + function parseJSDocType(): TypeNode { + scanner.setInJSDocType(true); + const moduleSpecifier = parseOptionalToken(SyntaxKind.ModuleKeyword); + if (moduleSpecifier) { + const moduleTag = (createNode(SyntaxKind.JSDocNamepathType, moduleSpecifier.pos) as JSDocNamepathType); + terminate: while (true) { + switch (token()) { + case SyntaxKind.CloseBraceToken: + case SyntaxKind.EndOfFileToken: + case SyntaxKind.CommaToken: + case SyntaxKind.WhitespaceTrivia: + break terminate; + default: + nextTokenJSDoc(); } - - scanner.setInJSDocType(false); - return finishNode(moduleTag); } - - const dotdotdot = parseOptionalToken(SyntaxKind.DotDotDotToken); - let type = parseTypeOrTypePredicate(); scanner.setInJSDocType(false); - if (dotdotdot) { - const variadic = createNode(SyntaxKind.JSDocVariadicType, dotdotdot.pos) as JSDocVariadicType; - variadic.type = type; - type = finishNode(variadic); - } - if (token() === SyntaxKind.EqualsToken) { - return createPostfixType(SyntaxKind.JSDocOptionalType, type); - } - return type; + return finishNode(moduleTag); } - - function parseTypeQuery(): TypeQueryNode { - const node = createNode(SyntaxKind.TypeQuery); - parseExpected(SyntaxKind.TypeOfKeyword); - node.exprName = parseEntityName(/*allowReservedWords*/ true); - return finishNode(node); + const dotdotdot = parseOptionalToken(SyntaxKind.DotDotDotToken); + let type = parseTypeOrTypePredicate(); + scanner.setInJSDocType(false); + if (dotdotdot) { + const variadic = (createNode(SyntaxKind.JSDocVariadicType, dotdotdot.pos) as JSDocVariadicType); + variadic.type = type; + type = finishNode(variadic); } - - function parseTypeParameter(): TypeParameterDeclaration { - const node = createNode(SyntaxKind.TypeParameter); - node.name = parseIdentifier(); - if (parseOptional(SyntaxKind.ExtendsKeyword)) { - // It's not uncommon for people to write improper constraints to a generic. If the - // user writes a constraint that is an expression and not an actual type, then parse - // it out as an expression (so we can recover well), but report that a type is needed - // instead. - if (isStartOfType() || !isStartOfExpression()) { - node.constraint = parseType(); - } - else { - // It was not a type, and it looked like an expression. Parse out an expression - // here so we recover well. Note: it is important that we call parseUnaryExpression - // and not parseExpression here. If the user has: - // - // - // - // We do *not* want to consume the `>` as we're consuming the expression for "". - node.expression = parseUnaryExpressionOrHigher(); - } + if (token() === SyntaxKind.EqualsToken) { + return createPostfixType(SyntaxKind.JSDocOptionalType, type); + } + return type; + } + function parseTypeQuery(): TypeQueryNode { + const node = (createNode(SyntaxKind.TypeQuery)); + parseExpected(SyntaxKind.TypeOfKeyword); + node.exprName = parseEntityName(/*allowReservedWords*/ true); + return finishNode(node); + } + function parseTypeParameter(): TypeParameterDeclaration { + const node = (createNode(SyntaxKind.TypeParameter)); + node.name = parseIdentifier(); + if (parseOptional(SyntaxKind.ExtendsKeyword)) { + // It's not uncommon for people to write improper constraints to a generic. If the + // user writes a constraint that is an expression and not an actual type, then parse + // it out as an expression (so we can recover well), but report that a type is needed + // instead. + if (isStartOfType() || !isStartOfExpression()) { + node.constraint = parseType(); } - - if (parseOptional(SyntaxKind.EqualsToken)) { - node.default = parseType(); + else { + // It was not a type, and it looked like an expression. Parse out an expression + // here so we recover well. Note: it is important that we call parseUnaryExpression + // and not parseExpression here. If the user has: + // + // + // + // We do *not* want to consume the `>` as we're consuming the expression for "". + node.expression = parseUnaryExpressionOrHigher(); } - - return finishNode(node); } - - function parseTypeParameters(): NodeArray | undefined { - if (token() === SyntaxKind.LessThanToken) { - return parseBracketedList(ParsingContext.TypeParameters, parseTypeParameter, SyntaxKind.LessThanToken, SyntaxKind.GreaterThanToken); - } + if (parseOptional(SyntaxKind.EqualsToken)) { + node.default = parseType(); } - - function parseParameterType(): TypeNode | undefined { - if (parseOptional(SyntaxKind.ColonToken)) { - return parseType(); - } - - return undefined; + return finishNode(node); + } + function parseTypeParameters(): NodeArray | undefined { + if (token() === SyntaxKind.LessThanToken) { + return parseBracketedList(ParsingContext.TypeParameters, parseTypeParameter, SyntaxKind.LessThanToken, SyntaxKind.GreaterThanToken); } - - function isStartOfParameter(isJSDocParameter: boolean): boolean { - return token() === SyntaxKind.DotDotDotToken || - isIdentifierOrPrivateIdentifierOrPattern() || - isModifierKind(token()) || - token() === SyntaxKind.AtToken || - isStartOfType(/*inStartOfParameter*/ !isJSDocParameter); - } - - function parseParameter(): ParameterDeclaration { - const node = createNodeWithJSDoc(SyntaxKind.Parameter); - if (token() === SyntaxKind.ThisKeyword) { - node.name = createIdentifier(/*isIdentifier*/ true); - node.type = parseParameterType(); - return finishNode(node); - } - - node.decorators = parseDecorators(); - node.modifiers = parseModifiers(); - node.dotDotDotToken = parseOptionalToken(SyntaxKind.DotDotDotToken); - - // FormalParameter [Yield,Await]: - // BindingElement[?Yield,?Await] - node.name = parseIdentifierOrPattern(Diagnostics.Private_identifiers_cannot_be_used_as_parameters); - if (getFullWidth(node.name) === 0 && !node.modifiers && isModifierKind(token())) { - // in cases like - // 'use strict' - // function foo(static) - // isParameter('static') === true, because of isModifier('static') - // however 'static' is not a legal identifier in a strict mode. - // so result of this function will be ParameterDeclaration (flags = 0, name = missing, type = undefined, initializer = undefined) - // and current token will not change => parsing of the enclosing parameter list will last till the end of time (or OOM) - // to avoid this we'll advance cursor to the next token. - nextToken(); - } - - node.questionToken = parseOptionalToken(SyntaxKind.QuestionToken); + } + function parseParameterType(): TypeNode | undefined { + if (parseOptional(SyntaxKind.ColonToken)) { + return parseType(); + } + return undefined; + } + function isStartOfParameter(isJSDocParameter: boolean): boolean { + return token() === SyntaxKind.DotDotDotToken || + isIdentifierOrPrivateIdentifierOrPattern() || + isModifierKind(token()) || + token() === SyntaxKind.AtToken || + isStartOfType(/*inStartOfParameter*/ !isJSDocParameter); + } + function parseParameter(): ParameterDeclaration { + const node = (createNodeWithJSDoc(SyntaxKind.Parameter)); + if (token() === SyntaxKind.ThisKeyword) { + node.name = createIdentifier(/*isIdentifier*/ true); node.type = parseParameterType(); - node.initializer = parseInitializer(); - return finishNode(node); } - - /** - * Note: If returnToken is EqualsGreaterThanToken, `signature.type` will always be defined. - * @returns If return type parsing succeeds - */ - function fillSignature( - returnToken: SyntaxKind.ColonToken | SyntaxKind.EqualsGreaterThanToken, - flags: SignatureFlags, - signature: SignatureDeclaration): boolean { - if (!(flags & SignatureFlags.JSDoc)) { - signature.typeParameters = parseTypeParameters(); - } - const parametersParsedSuccessfully = parseParameterList(signature, flags); - if (shouldParseReturnType(returnToken, !!(flags & SignatureFlags.Type))) { - signature.type = parseTypeOrTypePredicate(); - if (typeHasArrowFunctionBlockingParseError(signature.type)) return false; - } - return parametersParsedSuccessfully; - } - - function shouldParseReturnType(returnToken: SyntaxKind.ColonToken | SyntaxKind.EqualsGreaterThanToken, isType: boolean): boolean { - if (returnToken === SyntaxKind.EqualsGreaterThanToken) { - parseExpected(returnToken); - return true; - } - else if (parseOptional(SyntaxKind.ColonToken)) { - return true; - } - else if (isType && token() === SyntaxKind.EqualsGreaterThanToken) { - // This is easy to get backward, especially in type contexts, so parse the type anyway - parseErrorAtCurrentToken(Diagnostics._0_expected, tokenToString(SyntaxKind.ColonToken)); - nextToken(); - return true; - } - return false; + node.decorators = parseDecorators(); + node.modifiers = parseModifiers(); + node.dotDotDotToken = parseOptionalToken(SyntaxKind.DotDotDotToken); + // FormalParameter [Yield,Await]: + // BindingElement[?Yield,?Await] + node.name = parseIdentifierOrPattern(Diagnostics.Private_identifiers_cannot_be_used_as_parameters); + if (getFullWidth(node.name) === 0 && !node.modifiers && isModifierKind(token())) { + // in cases like + // 'use strict' + // function foo(static) + // isParameter('static') === true, because of isModifier('static') + // however 'static' is not a legal identifier in a strict mode. + // so result of this function will be ParameterDeclaration (flags = 0, name = missing, type = undefined, initializer = undefined) + // and current token will not change => parsing of the enclosing parameter list will last till the end of time (or OOM) + // to avoid this we'll advance cursor to the next token. + nextToken(); } - - // Returns true on success. - function parseParameterList(signature: SignatureDeclaration, flags: SignatureFlags): boolean { - // FormalParameters [Yield,Await]: (modified) - // [empty] - // FormalParameterList[?Yield,Await] - // - // FormalParameter[Yield,Await]: (modified) - // BindingElement[?Yield,Await] - // - // BindingElement [Yield,Await]: (modified) - // SingleNameBinding[?Yield,?Await] - // BindingPattern[?Yield,?Await]Initializer [In, ?Yield,?Await] opt - // - // SingleNameBinding [Yield,Await]: - // BindingIdentifier[?Yield,?Await]Initializer [In, ?Yield,?Await] opt - if (!parseExpected(SyntaxKind.OpenParenToken)) { - signature.parameters = createMissingList(); + node.questionToken = parseOptionalToken(SyntaxKind.QuestionToken); + node.type = parseParameterType(); + node.initializer = parseInitializer(); + return finishNode(node); + } + /** + * Note: If returnToken is EqualsGreaterThanToken, `signature.type` will always be defined. + * @returns If return type parsing succeeds + */ + function fillSignature(returnToken: SyntaxKind.ColonToken | SyntaxKind.EqualsGreaterThanToken, flags: SignatureFlags, signature: SignatureDeclaration): boolean { + if (!(flags & SignatureFlags.JSDoc)) { + signature.typeParameters = parseTypeParameters(); + } + const parametersParsedSuccessfully = parseParameterList(signature, flags); + if (shouldParseReturnType(returnToken, !!(flags & SignatureFlags.Type))) { + signature.type = parseTypeOrTypePredicate(); + if (typeHasArrowFunctionBlockingParseError(signature.type)) return false; - } - - const savedYieldContext = inYieldContext(); - const savedAwaitContext = inAwaitContext(); - - setYieldContext(!!(flags & SignatureFlags.Yield)); - setAwaitContext(!!(flags & SignatureFlags.Await)); - - signature.parameters = flags & SignatureFlags.JSDoc ? - parseDelimitedList(ParsingContext.JSDocParameters, parseJSDocParameter) : - parseDelimitedList(ParsingContext.Parameters, parseParameter); - - setYieldContext(savedYieldContext); - setAwaitContext(savedAwaitContext); - - return parseExpected(SyntaxKind.CloseParenToken); - } - - function parseTypeMemberSemicolon() { - // We allow type members to be separated by commas or (possibly ASI) semicolons. - // First check if it was a comma. If so, we're done with the member. - if (parseOptional(SyntaxKind.CommaToken)) { - return; - } - - // Didn't have a comma. We must have a (possible ASI) semicolon. - parseSemicolon(); } - - function parseSignatureMember(kind: SyntaxKind.CallSignature | SyntaxKind.ConstructSignature): CallSignatureDeclaration | ConstructSignatureDeclaration { - const node = createNodeWithJSDoc(kind); - if (kind === SyntaxKind.ConstructSignature) { - parseExpected(SyntaxKind.NewKeyword); - } - fillSignature(SyntaxKind.ColonToken, SignatureFlags.Type, node); - parseTypeMemberSemicolon(); - return finishNode(node); + return parametersParsedSuccessfully; + } + function shouldParseReturnType(returnToken: SyntaxKind.ColonToken | SyntaxKind.EqualsGreaterThanToken, isType: boolean): boolean { + if (returnToken === SyntaxKind.EqualsGreaterThanToken) { + parseExpected(returnToken); + return true; } - - function isIndexSignature(): boolean { - return token() === SyntaxKind.OpenBracketToken && lookAhead(isUnambiguouslyIndexSignature); + else if (parseOptional(SyntaxKind.ColonToken)) { + return true; } - - function isUnambiguouslyIndexSignature() { - // The only allowed sequence is: - // - // [id: - // - // However, for error recovery, we also check the following cases: - // - // [... - // [id, - // [id?, - // [id?: - // [id?] - // [public id - // [private id - // [protected id - // [] - // - nextToken(); - if (token() === SyntaxKind.DotDotDotToken || token() === SyntaxKind.CloseBracketToken) { - return true; - } - - if (isModifierKind(token())) { - nextToken(); - if (isIdentifier()) { - return true; - } - } - else if (!isIdentifier()) { - return false; - } - else { - // Skip the identifier - nextToken(); - } - - // A colon signifies a well formed indexer - // A comma should be a badly formed indexer because comma expressions are not allowed - // in computed properties. - if (token() === SyntaxKind.ColonToken || token() === SyntaxKind.CommaToken) { - return true; - } - - // Question mark could be an indexer with an optional property, - // or it could be a conditional expression in a computed property. - if (token() !== SyntaxKind.QuestionToken) { - return false; - } - - // If any of the following tokens are after the question mark, it cannot - // be a conditional expression, so treat it as an indexer. + else if (isType && token() === SyntaxKind.EqualsGreaterThanToken) { + // This is easy to get backward, especially in type contexts, so parse the type anyway + parseErrorAtCurrentToken(Diagnostics._0_expected, tokenToString(SyntaxKind.ColonToken)); nextToken(); - return token() === SyntaxKind.ColonToken || token() === SyntaxKind.CommaToken || token() === SyntaxKind.CloseBracketToken; + return true; } - - function parseIndexSignatureDeclaration(node: IndexSignatureDeclaration): IndexSignatureDeclaration { - node.kind = SyntaxKind.IndexSignature; - node.parameters = parseBracketedList(ParsingContext.Parameters, parseParameter, SyntaxKind.OpenBracketToken, SyntaxKind.CloseBracketToken); - node.type = parseTypeAnnotation(); - parseTypeMemberSemicolon(); - return finishNode(node); + return false; + } + // Returns true on success. + function parseParameterList(signature: SignatureDeclaration, flags: SignatureFlags): boolean { + // FormalParameters [Yield,Await]: (modified) + // [empty] + // FormalParameterList[?Yield,Await] + // + // FormalParameter[Yield,Await]: (modified) + // BindingElement[?Yield,Await] + // + // BindingElement [Yield,Await]: (modified) + // SingleNameBinding[?Yield,?Await] + // BindingPattern[?Yield,?Await]Initializer [In, ?Yield,?Await] opt + // + // SingleNameBinding [Yield,Await]: + // BindingIdentifier[?Yield,?Await]Initializer [In, ?Yield,?Await] opt + if (!parseExpected(SyntaxKind.OpenParenToken)) { + signature.parameters = createMissingList(); + return false; } - - function parsePropertyOrMethodSignature(node: PropertySignature | MethodSignature): PropertySignature | MethodSignature { - node.name = parsePropertyName(); - node.questionToken = parseOptionalToken(SyntaxKind.QuestionToken); - if (token() === SyntaxKind.OpenParenToken || token() === SyntaxKind.LessThanToken) { - node.kind = SyntaxKind.MethodSignature; - // Method signatures don't exist in expression contexts. So they have neither - // [Yield] nor [Await] - fillSignature(SyntaxKind.ColonToken, SignatureFlags.Type, node); - } - else { - node.kind = SyntaxKind.PropertySignature; - node.type = parseTypeAnnotation(); - if (token() === SyntaxKind.EqualsToken) { - // Although type literal properties cannot not have initializers, we attempt - // to parse an initializer so we can report in the checker that an interface - // property or type literal property cannot have an initializer. - (node).initializer = parseInitializer(); - } - } - parseTypeMemberSemicolon(); - return finishNode(node); + const savedYieldContext = inYieldContext(); + const savedAwaitContext = inAwaitContext(); + setYieldContext(!!(flags & SignatureFlags.Yield)); + setAwaitContext(!!(flags & SignatureFlags.Await)); + signature.parameters = flags & SignatureFlags.JSDoc ? + parseDelimitedList(ParsingContext.JSDocParameters, parseJSDocParameter) : + parseDelimitedList(ParsingContext.Parameters, parseParameter); + setYieldContext(savedYieldContext); + setAwaitContext(savedAwaitContext); + return parseExpected(SyntaxKind.CloseParenToken); + } + function parseTypeMemberSemicolon() { + // We allow type members to be separated by commas or (possibly ASI) semicolons. + // First check if it was a comma. If so, we're done with the member. + if (parseOptional(SyntaxKind.CommaToken)) { + return; } - - function isTypeMemberStart(): boolean { - // Return true if we have the start of a signature member - if (token() === SyntaxKind.OpenParenToken || token() === SyntaxKind.LessThanToken) { - return true; - } - let idToken = false; - // Eat up all modifiers, but hold on to the last one in case it is actually an identifier - while (isModifierKind(token())) { - idToken = true; - nextToken(); - } - // Index signatures and computed property names are type members - if (token() === SyntaxKind.OpenBracketToken) { + // Didn't have a comma. We must have a (possible ASI) semicolon. + parseSemicolon(); + } + function parseSignatureMember(kind: SyntaxKind.CallSignature | SyntaxKind.ConstructSignature): CallSignatureDeclaration | ConstructSignatureDeclaration { + const node = (createNodeWithJSDoc(kind)); + if (kind === SyntaxKind.ConstructSignature) { + parseExpected(SyntaxKind.NewKeyword); + } + fillSignature(SyntaxKind.ColonToken, SignatureFlags.Type, node); + parseTypeMemberSemicolon(); + return finishNode(node); + } + function isIndexSignature(): boolean { + return token() === SyntaxKind.OpenBracketToken && lookAhead(isUnambiguouslyIndexSignature); + } + function isUnambiguouslyIndexSignature() { + // The only allowed sequence is: + // + // [id: + // + // However, for error recovery, we also check the following cases: + // + // [... + // [id, + // [id?, + // [id?: + // [id?] + // [public id + // [private id + // [protected id + // [] + // + nextToken(); + if (token() === SyntaxKind.DotDotDotToken || token() === SyntaxKind.CloseBracketToken) { + return true; + } + if (isModifierKind(token())) { + nextToken(); + if (isIdentifier()) { return true; } - // Try to get the first property-like token following all modifiers - if (isLiteralPropertyName()) { - idToken = true; - nextToken(); - } - // If we were able to get any potential identifier, check that it is - // the start of a member declaration - if (idToken) { - return token() === SyntaxKind.OpenParenToken || - token() === SyntaxKind.LessThanToken || - token() === SyntaxKind.QuestionToken || - token() === SyntaxKind.ColonToken || - token() === SyntaxKind.CommaToken || - canParseSemicolon(); - } - return false; } - - function parseTypeMember(): TypeElement { - if (token() === SyntaxKind.OpenParenToken || token() === SyntaxKind.LessThanToken) { - return parseSignatureMember(SyntaxKind.CallSignature); - } - if (token() === SyntaxKind.NewKeyword && lookAhead(nextTokenIsOpenParenOrLessThan)) { - return parseSignatureMember(SyntaxKind.ConstructSignature); - } - const node = createNodeWithJSDoc(SyntaxKind.Unknown); - node.modifiers = parseModifiers(); - if (isIndexSignature()) { - return parseIndexSignatureDeclaration(node); - } - return parsePropertyOrMethodSignature(node); + else if (!isIdentifier()) { + return false; } - - function nextTokenIsOpenParenOrLessThan() { + else { + // Skip the identifier nextToken(); - return token() === SyntaxKind.OpenParenToken || token() === SyntaxKind.LessThanToken; } - - function nextTokenIsDot() { - return nextToken() === SyntaxKind.DotToken; + // A colon signifies a well formed indexer + // A comma should be a badly formed indexer because comma expressions are not allowed + // in computed properties. + if (token() === SyntaxKind.ColonToken || token() === SyntaxKind.CommaToken) { + return true; } - - function nextTokenIsOpenParenOrLessThanOrDot() { - switch (nextToken()) { - case SyntaxKind.OpenParenToken: - case SyntaxKind.LessThanToken: - case SyntaxKind.DotToken: - return true; - } + // Question mark could be an indexer with an optional property, + // or it could be a conditional expression in a computed property. + if (token() !== SyntaxKind.QuestionToken) { return false; } - - function parseTypeLiteral(): TypeLiteralNode { - const node = createNode(SyntaxKind.TypeLiteral); - node.members = parseObjectTypeMembers(); - return finishNode(node); + // If any of the following tokens are after the question mark, it cannot + // be a conditional expression, so treat it as an indexer. + nextToken(); + return token() === SyntaxKind.ColonToken || token() === SyntaxKind.CommaToken || token() === SyntaxKind.CloseBracketToken; + } + function parseIndexSignatureDeclaration(node: IndexSignatureDeclaration): IndexSignatureDeclaration { + node.kind = SyntaxKind.IndexSignature; + node.parameters = parseBracketedList(ParsingContext.Parameters, parseParameter, SyntaxKind.OpenBracketToken, SyntaxKind.CloseBracketToken); + node.type = parseTypeAnnotation(); + parseTypeMemberSemicolon(); + return finishNode(node); + } + function parsePropertyOrMethodSignature(node: PropertySignature | MethodSignature): PropertySignature | MethodSignature { + node.name = parsePropertyName(); + node.questionToken = parseOptionalToken(SyntaxKind.QuestionToken); + if (token() === SyntaxKind.OpenParenToken || token() === SyntaxKind.LessThanToken) { + node.kind = SyntaxKind.MethodSignature; + // Method signatures don't exist in expression contexts. So they have neither + // [Yield] nor [Await] + fillSignature(SyntaxKind.ColonToken, SignatureFlags.Type, (node)); } - - function parseObjectTypeMembers(): NodeArray { - let members: NodeArray; - if (parseExpected(SyntaxKind.OpenBraceToken)) { - members = parseList(ParsingContext.TypeMembers, parseTypeMember); - parseExpected(SyntaxKind.CloseBraceToken); - } - else { - members = createMissingList(); + else { + node.kind = SyntaxKind.PropertySignature; + node.type = parseTypeAnnotation(); + if (token() === SyntaxKind.EqualsToken) { + // Although type literal properties cannot not have initializers, we attempt + // to parse an initializer so we can report in the checker that an interface + // property or type literal property cannot have an initializer. + (node).initializer = parseInitializer(); } - - return members; } - - function isStartOfMappedType() { + parseTypeMemberSemicolon(); + return finishNode(node); + } + function isTypeMemberStart(): boolean { + // Return true if we have the start of a signature member + if (token() === SyntaxKind.OpenParenToken || token() === SyntaxKind.LessThanToken) { + return true; + } + let idToken = false; + // Eat up all modifiers, but hold on to the last one in case it is actually an identifier + while (isModifierKind(token())) { + idToken = true; nextToken(); - if (token() === SyntaxKind.PlusToken || token() === SyntaxKind.MinusToken) { - return nextToken() === SyntaxKind.ReadonlyKeyword; - } - if (token() === SyntaxKind.ReadonlyKeyword) { - nextToken(); - } - return token() === SyntaxKind.OpenBracketToken && nextTokenIsIdentifier() && nextToken() === SyntaxKind.InKeyword; } - - function parseMappedTypeParameter() { - const node = createNode(SyntaxKind.TypeParameter); - node.name = parseIdentifier(); - parseExpected(SyntaxKind.InKeyword); - node.constraint = parseType(); - return finishNode(node); + // Index signatures and computed property names are type members + if (token() === SyntaxKind.OpenBracketToken) { + return true; } - - function parseMappedType() { - const node = createNode(SyntaxKind.MappedType); - parseExpected(SyntaxKind.OpenBraceToken); - if (token() === SyntaxKind.ReadonlyKeyword || token() === SyntaxKind.PlusToken || token() === SyntaxKind.MinusToken) { - node.readonlyToken = parseTokenNode(); - if (node.readonlyToken.kind !== SyntaxKind.ReadonlyKeyword) { - parseExpectedToken(SyntaxKind.ReadonlyKeyword); - } - } - parseExpected(SyntaxKind.OpenBracketToken); - node.typeParameter = parseMappedTypeParameter(); - parseExpected(SyntaxKind.CloseBracketToken); - if (token() === SyntaxKind.QuestionToken || token() === SyntaxKind.PlusToken || token() === SyntaxKind.MinusToken) { - node.questionToken = parseTokenNode(); - if (node.questionToken.kind !== SyntaxKind.QuestionToken) { - parseExpectedToken(SyntaxKind.QuestionToken); - } - } - node.type = parseTypeAnnotation(); - parseSemicolon(); - parseExpected(SyntaxKind.CloseBraceToken); - return finishNode(node); + // Try to get the first property-like token following all modifiers + if (isLiteralPropertyName()) { + idToken = true; + nextToken(); } - - function parseTupleElementType() { - const pos = getNodePos(); - if (parseOptional(SyntaxKind.DotDotDotToken)) { - const node = createNode(SyntaxKind.RestType, pos); - node.type = parseType(); - return finishNode(node); - } - const type = parseType(); - if (!(contextFlags & NodeFlags.JSDoc) && type.kind === SyntaxKind.JSDocNullableType && type.pos === (type).type.pos) { - type.kind = SyntaxKind.OptionalType; - } - return type; + // If we were able to get any potential identifier, check that it is + // the start of a member declaration + if (idToken) { + return token() === SyntaxKind.OpenParenToken || + token() === SyntaxKind.LessThanToken || + token() === SyntaxKind.QuestionToken || + token() === SyntaxKind.ColonToken || + token() === SyntaxKind.CommaToken || + canParseSemicolon(); + } + return false; + } + function parseTypeMember(): TypeElement { + if (token() === SyntaxKind.OpenParenToken || token() === SyntaxKind.LessThanToken) { + return parseSignatureMember(SyntaxKind.CallSignature); } - - function parseTupleType(): TupleTypeNode { - const node = createNode(SyntaxKind.TupleType); - node.elementTypes = parseBracketedList(ParsingContext.TupleElementTypes, parseTupleElementType, SyntaxKind.OpenBracketToken, SyntaxKind.CloseBracketToken); - return finishNode(node); + if (token() === SyntaxKind.NewKeyword && lookAhead(nextTokenIsOpenParenOrLessThan)) { + return parseSignatureMember(SyntaxKind.ConstructSignature); } - - function parseParenthesizedType(): TypeNode { - const node = createNode(SyntaxKind.ParenthesizedType); - parseExpected(SyntaxKind.OpenParenToken); - node.type = parseType(); - parseExpected(SyntaxKind.CloseParenToken); - return finishNode(node); + const node = (createNodeWithJSDoc(SyntaxKind.Unknown)); + node.modifiers = parseModifiers(); + if (isIndexSignature()) { + return parseIndexSignatureDeclaration((node)); } - - function parseFunctionOrConstructorType(): TypeNode { - const pos = getNodePos(); - const kind = parseOptional(SyntaxKind.NewKeyword) ? SyntaxKind.ConstructorType : SyntaxKind.FunctionType; - const node = createNodeWithJSDoc(kind, pos); - fillSignature(SyntaxKind.EqualsGreaterThanToken, SignatureFlags.Type, node); - return finishNode(node); + return parsePropertyOrMethodSignature((node)); + } + function nextTokenIsOpenParenOrLessThan() { + nextToken(); + return token() === SyntaxKind.OpenParenToken || token() === SyntaxKind.LessThanToken; + } + function nextTokenIsDot() { + return nextToken() === SyntaxKind.DotToken; + } + function nextTokenIsOpenParenOrLessThanOrDot() { + switch (nextToken()) { + case SyntaxKind.OpenParenToken: + case SyntaxKind.LessThanToken: + case SyntaxKind.DotToken: + return true; } - - function parseKeywordAndNoDot(): TypeNode | undefined { - const node = parseTokenNode(); - return token() === SyntaxKind.DotToken ? undefined : node; - } - - function parseLiteralTypeNode(negative?: boolean): LiteralTypeNode { - const node = createNode(SyntaxKind.LiteralType) as LiteralTypeNode; - let unaryMinusExpression!: PrefixUnaryExpression; - if (negative) { - unaryMinusExpression = createNode(SyntaxKind.PrefixUnaryExpression) as PrefixUnaryExpression; - unaryMinusExpression.operator = SyntaxKind.MinusToken; - nextToken(); - } - let expression: BooleanLiteral | LiteralExpression | PrefixUnaryExpression = token() === SyntaxKind.TrueKeyword || token() === SyntaxKind.FalseKeyword - ? parseTokenNode() - : parseLiteralLikeNode(token()) as LiteralExpression; - if (negative) { - unaryMinusExpression.operand = expression; - finishNode(unaryMinusExpression); - expression = unaryMinusExpression; - } - node.literal = expression; - return finishNode(node); + return false; + } + function parseTypeLiteral(): TypeLiteralNode { + const node = (createNode(SyntaxKind.TypeLiteral)); + node.members = parseObjectTypeMembers(); + return finishNode(node); + } + function parseObjectTypeMembers(): NodeArray { + let members: NodeArray; + if (parseExpected(SyntaxKind.OpenBraceToken)) { + members = parseList(ParsingContext.TypeMembers, parseTypeMember); + parseExpected(SyntaxKind.CloseBraceToken); + } + else { + members = createMissingList(); } - - function isStartOfTypeOfImportType() { + return members; + } + function isStartOfMappedType() { + nextToken(); + if (token() === SyntaxKind.PlusToken || token() === SyntaxKind.MinusToken) { + return nextToken() === SyntaxKind.ReadonlyKeyword; + } + if (token() === SyntaxKind.ReadonlyKeyword) { nextToken(); - return token() === SyntaxKind.ImportKeyword; - } - - function parseImportType(): ImportTypeNode { - sourceFile.flags |= NodeFlags.PossiblyContainsDynamicImport; - const node = createNode(SyntaxKind.ImportType) as ImportTypeNode; - if (parseOptional(SyntaxKind.TypeOfKeyword)) { - node.isTypeOf = true; - } - parseExpected(SyntaxKind.ImportKeyword); - parseExpected(SyntaxKind.OpenParenToken); - node.argument = parseType(); - parseExpected(SyntaxKind.CloseParenToken); - if (parseOptional(SyntaxKind.DotToken)) { - node.qualifier = parseEntityName(/*allowReservedWords*/ true, Diagnostics.Type_expected); - } - if (!scanner.hasPrecedingLineBreak() && reScanLessThanToken() === SyntaxKind.LessThanToken) { - node.typeArguments = parseBracketedList(ParsingContext.TypeArguments, parseType, SyntaxKind.LessThanToken, SyntaxKind.GreaterThanToken); - } + } + return token() === SyntaxKind.OpenBracketToken && nextTokenIsIdentifier() && nextToken() === SyntaxKind.InKeyword; + } + function parseMappedTypeParameter() { + const node = (createNode(SyntaxKind.TypeParameter)); + node.name = parseIdentifier(); + parseExpected(SyntaxKind.InKeyword); + node.constraint = parseType(); + return finishNode(node); + } + function parseMappedType() { + const node = (createNode(SyntaxKind.MappedType)); + parseExpected(SyntaxKind.OpenBraceToken); + if (token() === SyntaxKind.ReadonlyKeyword || token() === SyntaxKind.PlusToken || token() === SyntaxKind.MinusToken) { + node.readonlyToken = parseTokenNode(); + if (node.readonlyToken.kind !== SyntaxKind.ReadonlyKeyword) { + parseExpectedToken(SyntaxKind.ReadonlyKeyword); + } + } + parseExpected(SyntaxKind.OpenBracketToken); + node.typeParameter = parseMappedTypeParameter(); + parseExpected(SyntaxKind.CloseBracketToken); + if (token() === SyntaxKind.QuestionToken || token() === SyntaxKind.PlusToken || token() === SyntaxKind.MinusToken) { + node.questionToken = parseTokenNode(); + if (node.questionToken.kind !== SyntaxKind.QuestionToken) { + parseExpectedToken(SyntaxKind.QuestionToken); + } + } + node.type = parseTypeAnnotation(); + parseSemicolon(); + parseExpected(SyntaxKind.CloseBraceToken); + return finishNode(node); + } + function parseTupleElementType() { + const pos = getNodePos(); + if (parseOptional(SyntaxKind.DotDotDotToken)) { + const node = (createNode(SyntaxKind.RestType, pos)); + node.type = parseType(); return finishNode(node); } - - function nextTokenIsNumericOrBigIntLiteral() { + const type = parseType(); + if (!(contextFlags & NodeFlags.JSDoc) && type.kind === SyntaxKind.JSDocNullableType && type.pos === (type).type.pos) { + type.kind = SyntaxKind.OptionalType; + } + return type; + } + function parseTupleType(): TupleTypeNode { + const node = (createNode(SyntaxKind.TupleType)); + node.elementTypes = parseBracketedList(ParsingContext.TupleElementTypes, parseTupleElementType, SyntaxKind.OpenBracketToken, SyntaxKind.CloseBracketToken); + return finishNode(node); + } + function parseParenthesizedType(): TypeNode { + const node = (createNode(SyntaxKind.ParenthesizedType)); + parseExpected(SyntaxKind.OpenParenToken); + node.type = parseType(); + parseExpected(SyntaxKind.CloseParenToken); + return finishNode(node); + } + function parseFunctionOrConstructorType(): TypeNode { + const pos = getNodePos(); + const kind = parseOptional(SyntaxKind.NewKeyword) ? SyntaxKind.ConstructorType : SyntaxKind.FunctionType; + const node = (createNodeWithJSDoc(kind, pos)); + fillSignature(SyntaxKind.EqualsGreaterThanToken, SignatureFlags.Type, node); + return finishNode(node); + } + function parseKeywordAndNoDot(): TypeNode | undefined { + const node = parseTokenNode(); + return token() === SyntaxKind.DotToken ? undefined : node; + } + function parseLiteralTypeNode(negative?: boolean): LiteralTypeNode { + const node = (createNode(SyntaxKind.LiteralType) as LiteralTypeNode); + let unaryMinusExpression!: PrefixUnaryExpression; + if (negative) { + unaryMinusExpression = (createNode(SyntaxKind.PrefixUnaryExpression) as PrefixUnaryExpression); + unaryMinusExpression.operator = SyntaxKind.MinusToken; nextToken(); - return token() === SyntaxKind.NumericLiteral || token() === SyntaxKind.BigIntLiteral; } - - function parseNonArrayType(): TypeNode { + let expression: BooleanLiteral | LiteralExpression | PrefixUnaryExpression = token() === SyntaxKind.TrueKeyword || token() === SyntaxKind.FalseKeyword + ? parseTokenNode() + : parseLiteralLikeNode(token()) as LiteralExpression; + if (negative) { + unaryMinusExpression.operand = expression; + finishNode(unaryMinusExpression); + expression = unaryMinusExpression; + } + node.literal = expression; + return finishNode(node); + } + function isStartOfTypeOfImportType() { + nextToken(); + return token() === SyntaxKind.ImportKeyword; + } + function parseImportType(): ImportTypeNode { + sourceFile.flags |= NodeFlags.PossiblyContainsDynamicImport; + const node = (createNode(SyntaxKind.ImportType) as ImportTypeNode); + if (parseOptional(SyntaxKind.TypeOfKeyword)) { + node.isTypeOf = true; + } + parseExpected(SyntaxKind.ImportKeyword); + parseExpected(SyntaxKind.OpenParenToken); + node.argument = parseType(); + parseExpected(SyntaxKind.CloseParenToken); + if (parseOptional(SyntaxKind.DotToken)) { + node.qualifier = parseEntityName(/*allowReservedWords*/ true, Diagnostics.Type_expected); + } + if (!scanner.hasPrecedingLineBreak() && reScanLessThanToken() === SyntaxKind.LessThanToken) { + node.typeArguments = parseBracketedList(ParsingContext.TypeArguments, parseType, SyntaxKind.LessThanToken, SyntaxKind.GreaterThanToken); + } + return finishNode(node); + } + function nextTokenIsNumericOrBigIntLiteral() { + nextToken(); + return token() === SyntaxKind.NumericLiteral || token() === SyntaxKind.BigIntLiteral; + } + function parseNonArrayType(): TypeNode { + switch (token()) { + case SyntaxKind.AnyKeyword: + case SyntaxKind.UnknownKeyword: + case SyntaxKind.StringKeyword: + case SyntaxKind.NumberKeyword: + case SyntaxKind.BigIntKeyword: + case SyntaxKind.SymbolKeyword: + case SyntaxKind.BooleanKeyword: + case SyntaxKind.UndefinedKeyword: + case SyntaxKind.NeverKeyword: + case SyntaxKind.ObjectKeyword: + // If these are followed by a dot, then parse these out as a dotted type reference instead. + return tryParse(parseKeywordAndNoDot) || parseTypeReference(); + case SyntaxKind.AsteriskToken: + return parseJSDocAllType(/*postfixEquals*/ false); + case SyntaxKind.AsteriskEqualsToken: + return parseJSDocAllType(/*postfixEquals*/ true); + case SyntaxKind.QuestionQuestionToken: + // If there is '??', consider that is prefix '?' in JSDoc type. + scanner.reScanQuestionToken(); + // falls through + case SyntaxKind.QuestionToken: + return parseJSDocUnknownOrNullableType(); + case SyntaxKind.FunctionKeyword: + return parseJSDocFunctionType(); + case SyntaxKind.ExclamationToken: + return parseJSDocNonNullableType(); + case SyntaxKind.NoSubstitutionTemplateLiteral: + case SyntaxKind.StringLiteral: + case SyntaxKind.NumericLiteral: + case SyntaxKind.BigIntLiteral: + case SyntaxKind.TrueKeyword: + case SyntaxKind.FalseKeyword: + return parseLiteralTypeNode(); + case SyntaxKind.MinusToken: + return lookAhead(nextTokenIsNumericOrBigIntLiteral) ? parseLiteralTypeNode(/*negative*/ true) : parseTypeReference(); + case SyntaxKind.VoidKeyword: + case SyntaxKind.NullKeyword: + return parseTokenNode(); + case SyntaxKind.ThisKeyword: { + const thisKeyword = parseThisTypeNode(); + if (token() === SyntaxKind.IsKeyword && !scanner.hasPrecedingLineBreak()) { + return parseThisTypePredicate(thisKeyword); + } + else { + return thisKeyword; + } + } + case SyntaxKind.TypeOfKeyword: + return lookAhead(isStartOfTypeOfImportType) ? parseImportType() : parseTypeQuery(); + case SyntaxKind.OpenBraceToken: + return lookAhead(isStartOfMappedType) ? parseMappedType() : parseTypeLiteral(); + case SyntaxKind.OpenBracketToken: + return parseTupleType(); + case SyntaxKind.OpenParenToken: + return parseParenthesizedType(); + case SyntaxKind.ImportKeyword: + return parseImportType(); + case SyntaxKind.AssertsKeyword: + return lookAhead(nextTokenIsIdentifierOrKeywordOnSameLine) ? parseAssertsTypePredicate() : parseTypeReference(); + default: + return parseTypeReference(); + } + } + function isStartOfType(inStartOfParameter?: boolean): boolean { + switch (token()) { + case SyntaxKind.AnyKeyword: + case SyntaxKind.UnknownKeyword: + case SyntaxKind.StringKeyword: + case SyntaxKind.NumberKeyword: + case SyntaxKind.BigIntKeyword: + case SyntaxKind.BooleanKeyword: + case SyntaxKind.ReadonlyKeyword: + case SyntaxKind.SymbolKeyword: + case SyntaxKind.UniqueKeyword: + case SyntaxKind.VoidKeyword: + case SyntaxKind.UndefinedKeyword: + case SyntaxKind.NullKeyword: + case SyntaxKind.ThisKeyword: + case SyntaxKind.TypeOfKeyword: + case SyntaxKind.NeverKeyword: + case SyntaxKind.OpenBraceToken: + case SyntaxKind.OpenBracketToken: + case SyntaxKind.LessThanToken: + case SyntaxKind.BarToken: + case SyntaxKind.AmpersandToken: + case SyntaxKind.NewKeyword: + case SyntaxKind.StringLiteral: + case SyntaxKind.NumericLiteral: + case SyntaxKind.BigIntLiteral: + case SyntaxKind.TrueKeyword: + case SyntaxKind.FalseKeyword: + case SyntaxKind.ObjectKeyword: + case SyntaxKind.AsteriskToken: + case SyntaxKind.QuestionToken: + case SyntaxKind.ExclamationToken: + case SyntaxKind.DotDotDotToken: + case SyntaxKind.InferKeyword: + case SyntaxKind.ImportKeyword: + case SyntaxKind.AssertsKeyword: + return true; + case SyntaxKind.FunctionKeyword: + return !inStartOfParameter; + case SyntaxKind.MinusToken: + return !inStartOfParameter && lookAhead(nextTokenIsNumericOrBigIntLiteral); + case SyntaxKind.OpenParenToken: + // Only consider '(' the start of a type if followed by ')', '...', an identifier, a modifier, + // or something that starts a type. We don't want to consider things like '(1)' a type. + return !inStartOfParameter && lookAhead(isStartOfParenthesizedOrFunctionType); + default: + return isIdentifier(); + } + } + function isStartOfParenthesizedOrFunctionType() { + nextToken(); + return token() === SyntaxKind.CloseParenToken || isStartOfParameter(/*isJSDocParameter*/ false) || isStartOfType(); + } + function parsePostfixTypeOrHigher(): TypeNode { + let type = parseNonArrayType(); + while (!scanner.hasPrecedingLineBreak()) { switch (token()) { - case SyntaxKind.AnyKeyword: - case SyntaxKind.UnknownKeyword: - case SyntaxKind.StringKeyword: - case SyntaxKind.NumberKeyword: - case SyntaxKind.BigIntKeyword: - case SyntaxKind.SymbolKeyword: - case SyntaxKind.BooleanKeyword: - case SyntaxKind.UndefinedKeyword: - case SyntaxKind.NeverKeyword: - case SyntaxKind.ObjectKeyword: - // If these are followed by a dot, then parse these out as a dotted type reference instead. - return tryParse(parseKeywordAndNoDot) || parseTypeReference(); - case SyntaxKind.AsteriskToken: - return parseJSDocAllType(/*postfixEquals*/ false); - case SyntaxKind.AsteriskEqualsToken: - return parseJSDocAllType(/*postfixEquals*/ true); - case SyntaxKind.QuestionQuestionToken: - // If there is '??', consider that is prefix '?' in JSDoc type. - scanner.reScanQuestionToken(); - // falls through - case SyntaxKind.QuestionToken: - return parseJSDocUnknownOrNullableType(); - case SyntaxKind.FunctionKeyword: - return parseJSDocFunctionType(); case SyntaxKind.ExclamationToken: - return parseJSDocNonNullableType(); - case SyntaxKind.NoSubstitutionTemplateLiteral: - case SyntaxKind.StringLiteral: - case SyntaxKind.NumericLiteral: - case SyntaxKind.BigIntLiteral: - case SyntaxKind.TrueKeyword: - case SyntaxKind.FalseKeyword: - return parseLiteralTypeNode(); - case SyntaxKind.MinusToken: - return lookAhead(nextTokenIsNumericOrBigIntLiteral) ? parseLiteralTypeNode(/*negative*/ true) : parseTypeReference(); - case SyntaxKind.VoidKeyword: - case SyntaxKind.NullKeyword: - return parseTokenNode(); - case SyntaxKind.ThisKeyword: { - const thisKeyword = parseThisTypeNode(); - if (token() === SyntaxKind.IsKeyword && !scanner.hasPrecedingLineBreak()) { - return parseThisTypePredicate(thisKeyword); + type = createPostfixType(SyntaxKind.JSDocNonNullableType, type); + break; + case SyntaxKind.QuestionToken: + // If not in JSDoc and next token is start of a type we have a conditional type + if (!(contextFlags & NodeFlags.JSDoc) && lookAhead(nextTokenIsStartOfType)) { + return type; + } + type = createPostfixType(SyntaxKind.JSDocNullableType, type); + break; + case SyntaxKind.OpenBracketToken: + parseExpected(SyntaxKind.OpenBracketToken); + if (isStartOfType()) { + const node = (createNode(SyntaxKind.IndexedAccessType, type.pos) as IndexedAccessTypeNode); + node.objectType = type; + node.indexType = parseType(); + parseExpected(SyntaxKind.CloseBracketToken); + type = finishNode(node); } else { - return thisKeyword; + const node = (createNode(SyntaxKind.ArrayType, type.pos) as ArrayTypeNode); + node.elementType = type; + parseExpected(SyntaxKind.CloseBracketToken); + type = finishNode(node); } - } - case SyntaxKind.TypeOfKeyword: - return lookAhead(isStartOfTypeOfImportType) ? parseImportType() : parseTypeQuery(); - case SyntaxKind.OpenBraceToken: - return lookAhead(isStartOfMappedType) ? parseMappedType() : parseTypeLiteral(); - case SyntaxKind.OpenBracketToken: - return parseTupleType(); - case SyntaxKind.OpenParenToken: - return parseParenthesizedType(); - case SyntaxKind.ImportKeyword: - return parseImportType(); - case SyntaxKind.AssertsKeyword: - return lookAhead(nextTokenIsIdentifierOrKeywordOnSameLine) ? parseAssertsTypePredicate() : parseTypeReference(); + break; default: - return parseTypeReference(); + return type; } } - - function isStartOfType(inStartOfParameter?: boolean): boolean { - switch (token()) { - case SyntaxKind.AnyKeyword: - case SyntaxKind.UnknownKeyword: - case SyntaxKind.StringKeyword: - case SyntaxKind.NumberKeyword: - case SyntaxKind.BigIntKeyword: - case SyntaxKind.BooleanKeyword: - case SyntaxKind.ReadonlyKeyword: - case SyntaxKind.SymbolKeyword: - case SyntaxKind.UniqueKeyword: - case SyntaxKind.VoidKeyword: - case SyntaxKind.UndefinedKeyword: - case SyntaxKind.NullKeyword: - case SyntaxKind.ThisKeyword: - case SyntaxKind.TypeOfKeyword: - case SyntaxKind.NeverKeyword: - case SyntaxKind.OpenBraceToken: - case SyntaxKind.OpenBracketToken: - case SyntaxKind.LessThanToken: - case SyntaxKind.BarToken: - case SyntaxKind.AmpersandToken: - case SyntaxKind.NewKeyword: - case SyntaxKind.StringLiteral: - case SyntaxKind.NumericLiteral: - case SyntaxKind.BigIntLiteral: - case SyntaxKind.TrueKeyword: - case SyntaxKind.FalseKeyword: - case SyntaxKind.ObjectKeyword: - case SyntaxKind.AsteriskToken: - case SyntaxKind.QuestionToken: - case SyntaxKind.ExclamationToken: - case SyntaxKind.DotDotDotToken: - case SyntaxKind.InferKeyword: - case SyntaxKind.ImportKeyword: - case SyntaxKind.AssertsKeyword: - return true; - case SyntaxKind.FunctionKeyword: - return !inStartOfParameter; - case SyntaxKind.MinusToken: - return !inStartOfParameter && lookAhead(nextTokenIsNumericOrBigIntLiteral); - case SyntaxKind.OpenParenToken: - // Only consider '(' the start of a type if followed by ')', '...', an identifier, a modifier, - // or something that starts a type. We don't want to consider things like '(1)' a type. - return !inStartOfParameter && lookAhead(isStartOfParenthesizedOrFunctionType); - default: - return isIdentifier(); - } + return type; + } + function createPostfixType(kind: SyntaxKind, type: TypeNode) { + nextToken(); + const postfix = (createNode(kind, type.pos) as OptionalTypeNode | JSDocOptionalType | JSDocNonNullableType | JSDocNullableType); + postfix.type = type; + return finishNode(postfix); + } + function parseTypeOperator(operator: SyntaxKind.KeyOfKeyword | SyntaxKind.UniqueKeyword | SyntaxKind.ReadonlyKeyword) { + const node = (createNode(SyntaxKind.TypeOperator)); + parseExpected(operator); + node.operator = operator; + node.type = parseTypeOperatorOrHigher(); + return finishNode(node); + } + function parseInferType(): InferTypeNode { + const node = (createNode(SyntaxKind.InferType)); + parseExpected(SyntaxKind.InferKeyword); + const typeParameter = (createNode(SyntaxKind.TypeParameter)); + typeParameter.name = parseIdentifier(); + node.typeParameter = finishNode(typeParameter); + return finishNode(node); + } + function parseTypeOperatorOrHigher(): TypeNode { + const operator = token(); + switch (operator) { + case SyntaxKind.KeyOfKeyword: + case SyntaxKind.UniqueKeyword: + case SyntaxKind.ReadonlyKeyword: + return parseTypeOperator(operator); + case SyntaxKind.InferKeyword: + return parseInferType(); + } + return parsePostfixTypeOrHigher(); + } + function parseUnionOrIntersectionType(kind: SyntaxKind.UnionType | SyntaxKind.IntersectionType, parseConstituentType: () => TypeNode, operator: SyntaxKind.BarToken | SyntaxKind.AmpersandToken): TypeNode { + const start = scanner.getStartPos(); + const hasLeadingOperator = parseOptional(operator); + let type = parseConstituentType(); + if (token() === operator || hasLeadingOperator) { + const types = [type]; + while (parseOptional(operator)) { + types.push(parseConstituentType()); + } + const node = (createNode(kind, start)); + node.types = createNodeArray(types, start); + type = finishNode(node); + } + return type; + } + function parseIntersectionTypeOrHigher(): TypeNode { + return parseUnionOrIntersectionType(SyntaxKind.IntersectionType, parseTypeOperatorOrHigher, SyntaxKind.AmpersandToken); + } + function parseUnionTypeOrHigher(): TypeNode { + return parseUnionOrIntersectionType(SyntaxKind.UnionType, parseIntersectionTypeOrHigher, SyntaxKind.BarToken); + } + function isStartOfFunctionType(): boolean { + if (token() === SyntaxKind.LessThanToken) { + return true; } - - function isStartOfParenthesizedOrFunctionType() { + return token() === SyntaxKind.OpenParenToken && lookAhead(isUnambiguouslyStartOfFunctionType); + } + function skipParameterStart(): boolean { + if (isModifierKind(token())) { + // Skip modifiers + parseModifiers(); + } + if (isIdentifier() || token() === SyntaxKind.ThisKeyword) { nextToken(); - return token() === SyntaxKind.CloseParenToken || isStartOfParameter(/*isJSDocParameter*/ false) || isStartOfType(); + return true; } - - function parsePostfixTypeOrHigher(): TypeNode { - let type = parseNonArrayType(); - while (!scanner.hasPrecedingLineBreak()) { - switch (token()) { - case SyntaxKind.ExclamationToken: - type = createPostfixType(SyntaxKind.JSDocNonNullableType, type); - break; - case SyntaxKind.QuestionToken: - // If not in JSDoc and next token is start of a type we have a conditional type - if (!(contextFlags & NodeFlags.JSDoc) && lookAhead(nextTokenIsStartOfType)) { - return type; - } - type = createPostfixType(SyntaxKind.JSDocNullableType, type); - break; - case SyntaxKind.OpenBracketToken: - parseExpected(SyntaxKind.OpenBracketToken); - if (isStartOfType()) { - const node = createNode(SyntaxKind.IndexedAccessType, type.pos) as IndexedAccessTypeNode; - node.objectType = type; - node.indexType = parseType(); - parseExpected(SyntaxKind.CloseBracketToken); - type = finishNode(node); - } - else { - const node = createNode(SyntaxKind.ArrayType, type.pos) as ArrayTypeNode; - node.elementType = type; - parseExpected(SyntaxKind.CloseBracketToken); - type = finishNode(node); - } - break; - default: - return type; + if (token() === SyntaxKind.OpenBracketToken || token() === SyntaxKind.OpenBraceToken) { + // Return true if we can parse an array or object binding pattern with no errors + const previousErrorCount = parseDiagnostics.length; + parseIdentifierOrPattern(); + return previousErrorCount === parseDiagnostics.length; + } + return false; + } + function isUnambiguouslyStartOfFunctionType() { + nextToken(); + if (token() === SyntaxKind.CloseParenToken || token() === SyntaxKind.DotDotDotToken) { + // ( ) + // ( ... + return true; + } + if (skipParameterStart()) { + // We successfully skipped modifiers (if any) and an identifier or binding pattern, + // now see if we have something that indicates a parameter declaration + if (token() === SyntaxKind.ColonToken || token() === SyntaxKind.CommaToken || + token() === SyntaxKind.QuestionToken || token() === SyntaxKind.EqualsToken) { + // ( xxx : + // ( xxx , + // ( xxx ? + // ( xxx = + return true; + } + if (token() === SyntaxKind.CloseParenToken) { + nextToken(); + if (token() === SyntaxKind.EqualsGreaterThanToken) { + // ( xxx ) => + return true; } } - return type; - } - - function createPostfixType(kind: SyntaxKind, type: TypeNode) { - nextToken(); - const postfix = createNode(kind, type.pos) as OptionalTypeNode | JSDocOptionalType | JSDocNonNullableType | JSDocNullableType; - postfix.type = type; - return finishNode(postfix); - } - - function parseTypeOperator(operator: SyntaxKind.KeyOfKeyword | SyntaxKind.UniqueKeyword | SyntaxKind.ReadonlyKeyword) { - const node = createNode(SyntaxKind.TypeOperator); - parseExpected(operator); - node.operator = operator; - node.type = parseTypeOperatorOrHigher(); - return finishNode(node); } - - function parseInferType(): InferTypeNode { - const node = createNode(SyntaxKind.InferType); - parseExpected(SyntaxKind.InferKeyword); - const typeParameter = createNode(SyntaxKind.TypeParameter); - typeParameter.name = parseIdentifier(); - node.typeParameter = finishNode(typeParameter); + return false; + } + function parseTypeOrTypePredicate(): TypeNode { + const typePredicateVariable = isIdentifier() && tryParse(parseTypePredicatePrefix); + const type = parseType(); + if (typePredicateVariable) { + const node = (createNode(SyntaxKind.TypePredicate, typePredicateVariable.pos)); + node.assertsModifier = undefined; + node.parameterName = typePredicateVariable; + node.type = type; return finishNode(node); } - - function parseTypeOperatorOrHigher(): TypeNode { - const operator = token(); - switch (operator) { - case SyntaxKind.KeyOfKeyword: - case SyntaxKind.UniqueKeyword: - case SyntaxKind.ReadonlyKeyword: - return parseTypeOperator(operator); - case SyntaxKind.InferKeyword: - return parseInferType(); - } - return parsePostfixTypeOrHigher(); - } - - function parseUnionOrIntersectionType(kind: SyntaxKind.UnionType | SyntaxKind.IntersectionType, parseConstituentType: () => TypeNode, operator: SyntaxKind.BarToken | SyntaxKind.AmpersandToken): TypeNode { - const start = scanner.getStartPos(); - const hasLeadingOperator = parseOptional(operator); - let type = parseConstituentType(); - if (token() === operator || hasLeadingOperator) { - const types = [type]; - while (parseOptional(operator)) { - types.push(parseConstituentType()); - } - const node = createNode(kind, start); - node.types = createNodeArray(types, start); - type = finishNode(node); - } + else { return type; } - - function parseIntersectionTypeOrHigher(): TypeNode { - return parseUnionOrIntersectionType(SyntaxKind.IntersectionType, parseTypeOperatorOrHigher, SyntaxKind.AmpersandToken); + } + function parseTypePredicatePrefix() { + const id = parseIdentifier(); + if (token() === SyntaxKind.IsKeyword && !scanner.hasPrecedingLineBreak()) { + nextToken(); + return id; } - - function parseUnionTypeOrHigher(): TypeNode { - return parseUnionOrIntersectionType(SyntaxKind.UnionType, parseIntersectionTypeOrHigher, SyntaxKind.BarToken); + } + function parseAssertsTypePredicate(): TypeNode { + const node = (createNode(SyntaxKind.TypePredicate)); + node.assertsModifier = parseExpectedToken(SyntaxKind.AssertsKeyword); + node.parameterName = token() === SyntaxKind.ThisKeyword ? parseThisTypeNode() : parseIdentifier(); + node.type = parseOptional(SyntaxKind.IsKeyword) ? parseType() : undefined; + return finishNode(node); + } + function parseType(): TypeNode { + // The rules about 'yield' only apply to actual code/expression contexts. They don't + // apply to 'type' contexts. So we disable these parameters here before moving on. + return doOutsideOfContext(NodeFlags.TypeExcludesFlags, parseTypeWorker); + } + function parseTypeWorker(noConditionalTypes?: boolean): TypeNode { + if (isStartOfFunctionType() || token() === SyntaxKind.NewKeyword) { + return parseFunctionOrConstructorType(); + } + const type = parseUnionTypeOrHigher(); + if (!noConditionalTypes && !scanner.hasPrecedingLineBreak() && parseOptional(SyntaxKind.ExtendsKeyword)) { + const node = (createNode(SyntaxKind.ConditionalType, type.pos)); + node.checkType = type; + // The type following 'extends' is not permitted to be another conditional type + node.extendsType = parseTypeWorker(/*noConditionalTypes*/ true); + parseExpected(SyntaxKind.QuestionToken); + node.trueType = parseTypeWorker(); + parseExpected(SyntaxKind.ColonToken); + node.falseType = parseTypeWorker(); + return finishNode(node); } - - function isStartOfFunctionType(): boolean { - if (token() === SyntaxKind.LessThanToken) { + return type; + } + function parseTypeAnnotation(): TypeNode | undefined { + return parseOptional(SyntaxKind.ColonToken) ? parseType() : undefined; + } + // EXPRESSIONS + function isStartOfLeftHandSideExpression(): boolean { + switch (token()) { + case SyntaxKind.ThisKeyword: + case SyntaxKind.SuperKeyword: + case SyntaxKind.NullKeyword: + case SyntaxKind.TrueKeyword: + case SyntaxKind.FalseKeyword: + case SyntaxKind.NumericLiteral: + case SyntaxKind.BigIntLiteral: + case SyntaxKind.StringLiteral: + case SyntaxKind.NoSubstitutionTemplateLiteral: + case SyntaxKind.TemplateHead: + case SyntaxKind.OpenParenToken: + case SyntaxKind.OpenBracketToken: + case SyntaxKind.OpenBraceToken: + case SyntaxKind.FunctionKeyword: + case SyntaxKind.ClassKeyword: + case SyntaxKind.NewKeyword: + case SyntaxKind.SlashToken: + case SyntaxKind.SlashEqualsToken: + case SyntaxKind.Identifier: return true; - } - return token() === SyntaxKind.OpenParenToken && lookAhead(isUnambiguouslyStartOfFunctionType); + case SyntaxKind.ImportKeyword: + return lookAhead(nextTokenIsOpenParenOrLessThanOrDot); + default: + return isIdentifier(); } - - function skipParameterStart(): boolean { - if (isModifierKind(token())) { - // Skip modifiers - parseModifiers(); - } - if (isIdentifier() || token() === SyntaxKind.ThisKeyword) { - nextToken(); - return true; - } - if (token() === SyntaxKind.OpenBracketToken || token() === SyntaxKind.OpenBraceToken) { - // Return true if we can parse an array or object binding pattern with no errors - const previousErrorCount = parseDiagnostics.length; - parseIdentifierOrPattern(); - return previousErrorCount === parseDiagnostics.length; - } - return false; + } + function isStartOfExpression(): boolean { + if (isStartOfLeftHandSideExpression()) { + return true; } - - function isUnambiguouslyStartOfFunctionType() { - nextToken(); - if (token() === SyntaxKind.CloseParenToken || token() === SyntaxKind.DotDotDotToken) { - // ( ) - // ( ... + switch (token()) { + case SyntaxKind.PlusToken: + case SyntaxKind.MinusToken: + case SyntaxKind.TildeToken: + case SyntaxKind.ExclamationToken: + case SyntaxKind.DeleteKeyword: + case SyntaxKind.TypeOfKeyword: + case SyntaxKind.VoidKeyword: + case SyntaxKind.PlusPlusToken: + case SyntaxKind.MinusMinusToken: + case SyntaxKind.LessThanToken: + case SyntaxKind.AwaitKeyword: + case SyntaxKind.YieldKeyword: + case SyntaxKind.PrivateIdentifier: + // Yield/await always starts an expression. Either it is an identifier (in which case + // it is definitely an expression). Or it's a keyword (either because we're in + // a generator or async function, or in strict mode (or both)) and it started a yield or await expression. return true; - } - if (skipParameterStart()) { - // We successfully skipped modifiers (if any) and an identifier or binding pattern, - // now see if we have something that indicates a parameter declaration - if (token() === SyntaxKind.ColonToken || token() === SyntaxKind.CommaToken || - token() === SyntaxKind.QuestionToken || token() === SyntaxKind.EqualsToken) { - // ( xxx : - // ( xxx , - // ( xxx ? - // ( xxx = + default: + // Error tolerance. If we see the start of some binary operator, we consider + // that the start of an expression. That way we'll parse out a missing identifier, + // give a good message about an identifier being missing, and then consume the + // rest of the binary expression. + if (isBinaryOperator()) { return true; } - if (token() === SyntaxKind.CloseParenToken) { - nextToken(); - if (token() === SyntaxKind.EqualsGreaterThanToken) { - // ( xxx ) => - return true; - } - } - } - return false; + return isIdentifier(); } - - function parseTypeOrTypePredicate(): TypeNode { - const typePredicateVariable = isIdentifier() && tryParse(parseTypePredicatePrefix); - const type = parseType(); - if (typePredicateVariable) { - const node = createNode(SyntaxKind.TypePredicate, typePredicateVariable.pos); - node.assertsModifier = undefined; - node.parameterName = typePredicateVariable; - node.type = type; - return finishNode(node); - } - else { - return type; - } + } + function isStartOfExpressionStatement(): boolean { + // As per the grammar, none of '{' or 'function' or 'class' can start an expression statement. + return token() !== SyntaxKind.OpenBraceToken && + token() !== SyntaxKind.FunctionKeyword && + token() !== SyntaxKind.ClassKeyword && + token() !== SyntaxKind.AtToken && + isStartOfExpression(); + } + function parseExpression(): Expression { + // Expression[in]: + // AssignmentExpression[in] + // Expression[in] , AssignmentExpression[in] + // clear the decorator context when parsing Expression, as it should be unambiguous when parsing a decorator + const saveDecoratorContext = inDecoratorContext(); + if (saveDecoratorContext) { + setDecoratorContext(/*val*/ false); + } + let expr = parseAssignmentExpressionOrHigher(); + let operatorToken: BinaryOperatorToken; + while ((operatorToken = parseOptionalToken(SyntaxKind.CommaToken))) { + expr = makeBinaryExpression(expr, operatorToken, parseAssignmentExpressionOrHigher()); + } + if (saveDecoratorContext) { + setDecoratorContext(/*val*/ true); + } + return expr; + } + function parseInitializer(): Expression | undefined { + return parseOptional(SyntaxKind.EqualsToken) ? parseAssignmentExpressionOrHigher() : undefined; + } + function parseAssignmentExpressionOrHigher(): Expression { + // AssignmentExpression[in,yield]: + // 1) ConditionalExpression[?in,?yield] + // 2) LeftHandSideExpression = AssignmentExpression[?in,?yield] + // 3) LeftHandSideExpression AssignmentOperator AssignmentExpression[?in,?yield] + // 4) ArrowFunctionExpression[?in,?yield] + // 5) AsyncArrowFunctionExpression[in,yield,await] + // 6) [+Yield] YieldExpression[?In] + // + // Note: for ease of implementation we treat productions '2' and '3' as the same thing. + // (i.e. they're both BinaryExpressions with an assignment operator in it). + // First, do the simple check if we have a YieldExpression (production '6'). + if (isYieldExpression()) { + return parseYieldExpression(); + } + // Then, check if we have an arrow function (production '4' and '5') that starts with a parenthesized + // parameter list or is an async arrow function. + // AsyncArrowFunctionExpression: + // 1) async[no LineTerminator here]AsyncArrowBindingIdentifier[?Yield][no LineTerminator here]=>AsyncConciseBody[?In] + // 2) CoverCallExpressionAndAsyncArrowHead[?Yield, ?Await][no LineTerminator here]=>AsyncConciseBody[?In] + // Production (1) of AsyncArrowFunctionExpression is parsed in "tryParseAsyncSimpleArrowFunctionExpression". + // And production (2) is parsed in "tryParseParenthesizedArrowFunctionExpression". + // + // If we do successfully parse arrow-function, we must *not* recurse for productions 1, 2 or 3. An ArrowFunction is + // not a LeftHandSideExpression, nor does it start a ConditionalExpression. So we are done + // with AssignmentExpression if we see one. + const arrowExpression = tryParseParenthesizedArrowFunctionExpression() || tryParseAsyncSimpleArrowFunctionExpression(); + if (arrowExpression) { + return arrowExpression; + } + // Now try to see if we're in production '1', '2' or '3'. A conditional expression can + // start with a LogicalOrExpression, while the assignment productions can only start with + // LeftHandSideExpressions. + // + // So, first, we try to just parse out a BinaryExpression. If we get something that is a + // LeftHandSide or higher, then we can try to parse out the assignment expression part. + // Otherwise, we try to parse out the conditional expression bit. We want to allow any + // binary expression here, so we pass in the 'lowest' precedence here so that it matches + // and consumes anything. + const expr = parseBinaryExpressionOrHigher(/*precedence*/ 0); + // To avoid a look-ahead, we did not handle the case of an arrow function with a single un-parenthesized + // parameter ('x => ...') above. We handle it here by checking if the parsed expression was a single + // identifier and the current token is an arrow. + if (expr.kind === SyntaxKind.Identifier && token() === SyntaxKind.EqualsGreaterThanToken) { + return parseSimpleArrowFunctionExpression((expr)); + } + // Now see if we might be in cases '2' or '3'. + // If the expression was a LHS expression, and we have an assignment operator, then + // we're in '2' or '3'. Consume the assignment and return. + // + // Note: we call reScanGreaterToken so that we get an appropriately merged token + // for cases like `> > =` becoming `>>=` + if (isLeftHandSideExpression(expr) && isAssignmentOperator(reScanGreaterToken())) { + return makeBinaryExpression(expr, parseTokenNode(), parseAssignmentExpressionOrHigher()); } - - function parseTypePredicatePrefix() { - const id = parseIdentifier(); - if (token() === SyntaxKind.IsKeyword && !scanner.hasPrecedingLineBreak()) { - nextToken(); - return id; + // It wasn't an assignment or a lambda. This is a conditional expression: + return parseConditionalExpressionRest(expr); + } + function isYieldExpression(): boolean { + if (token() === SyntaxKind.YieldKeyword) { + // If we have a 'yield' keyword, and this is a context where yield expressions are + // allowed, then definitely parse out a yield expression. + if (inYieldContext()) { + return true; } + // We're in a context where 'yield expr' is not allowed. However, if we can + // definitely tell that the user was trying to parse a 'yield expr' and not + // just a normal expr that start with a 'yield' identifier, then parse out + // a 'yield expr'. We can then report an error later that they are only + // allowed in generator expressions. + // + // for example, if we see 'yield(foo)', then we'll have to treat that as an + // invocation expression of something called 'yield'. However, if we have + // 'yield foo' then that is not legal as a normal expression, so we can + // definitely recognize this as a yield expression. + // + // for now we just check if the next token is an identifier. More heuristics + // can be added here later as necessary. We just need to make sure that we + // don't accidentally consume something legal. + return lookAhead(nextTokenIsIdentifierOrKeywordOrLiteralOnSameLine); } - - function parseAssertsTypePredicate(): TypeNode { - const node = createNode(SyntaxKind.TypePredicate); - node.assertsModifier = parseExpectedToken(SyntaxKind.AssertsKeyword); - node.parameterName = token() === SyntaxKind.ThisKeyword ? parseThisTypeNode() : parseIdentifier(); - node.type = parseOptional(SyntaxKind.IsKeyword) ? parseType() : undefined; + return false; + } + function nextTokenIsIdentifierOnSameLine() { + nextToken(); + return !scanner.hasPrecedingLineBreak() && isIdentifier(); + } + function parseYieldExpression(): YieldExpression { + const node = (createNode(SyntaxKind.YieldExpression)); + // YieldExpression[In] : + // yield + // yield [no LineTerminator here] [Lexical goal InputElementRegExp]AssignmentExpression[?In, Yield] + // yield [no LineTerminator here] * [Lexical goal InputElementRegExp]AssignmentExpression[?In, Yield] + nextToken(); + if (!scanner.hasPrecedingLineBreak() && + (token() === SyntaxKind.AsteriskToken || isStartOfExpression())) { + node.asteriskToken = parseOptionalToken(SyntaxKind.AsteriskToken); + node.expression = parseAssignmentExpressionOrHigher(); return finishNode(node); } - - function parseType(): TypeNode { - // The rules about 'yield' only apply to actual code/expression contexts. They don't - // apply to 'type' contexts. So we disable these parameters here before moving on. - return doOutsideOfContext(NodeFlags.TypeExcludesFlags, parseTypeWorker); - } - - function parseTypeWorker(noConditionalTypes?: boolean): TypeNode { - if (isStartOfFunctionType() || token() === SyntaxKind.NewKeyword) { - return parseFunctionOrConstructorType(); - } - const type = parseUnionTypeOrHigher(); - if (!noConditionalTypes && !scanner.hasPrecedingLineBreak() && parseOptional(SyntaxKind.ExtendsKeyword)) { - const node = createNode(SyntaxKind.ConditionalType, type.pos); - node.checkType = type; - // The type following 'extends' is not permitted to be another conditional type - node.extendsType = parseTypeWorker(/*noConditionalTypes*/ true); - parseExpected(SyntaxKind.QuestionToken); - node.trueType = parseTypeWorker(); - parseExpected(SyntaxKind.ColonToken); - node.falseType = parseTypeWorker(); - return finishNode(node); - } - return type; + else { + // if the next token is not on the same line as yield. or we don't have an '*' or + // the start of an expression, then this is just a simple "yield" expression. + return finishNode(node); } - - function parseTypeAnnotation(): TypeNode | undefined { - return parseOptional(SyntaxKind.ColonToken) ? parseType() : undefined; + } + function parseSimpleArrowFunctionExpression(identifier: Identifier, asyncModifier?: NodeArray | undefined): ArrowFunction { + Debug.assert(token() === SyntaxKind.EqualsGreaterThanToken, "parseSimpleArrowFunctionExpression should only have been called if we had a =>"); + let node: ArrowFunction; + if (asyncModifier) { + node = (createNode(SyntaxKind.ArrowFunction, asyncModifier.pos)); + node.modifiers = asyncModifier; } - - // EXPRESSIONS - function isStartOfLeftHandSideExpression(): boolean { - switch (token()) { - case SyntaxKind.ThisKeyword: - case SyntaxKind.SuperKeyword: - case SyntaxKind.NullKeyword: - case SyntaxKind.TrueKeyword: - case SyntaxKind.FalseKeyword: - case SyntaxKind.NumericLiteral: - case SyntaxKind.BigIntLiteral: - case SyntaxKind.StringLiteral: - case SyntaxKind.NoSubstitutionTemplateLiteral: - case SyntaxKind.TemplateHead: - case SyntaxKind.OpenParenToken: - case SyntaxKind.OpenBracketToken: - case SyntaxKind.OpenBraceToken: - case SyntaxKind.FunctionKeyword: - case SyntaxKind.ClassKeyword: - case SyntaxKind.NewKeyword: - case SyntaxKind.SlashToken: - case SyntaxKind.SlashEqualsToken: - case SyntaxKind.Identifier: - return true; - case SyntaxKind.ImportKeyword: - return lookAhead(nextTokenIsOpenParenOrLessThanOrDot); - default: - return isIdentifier(); - } + else { + node = (createNode(SyntaxKind.ArrowFunction, identifier.pos)); + } + const parameter = (createNode(SyntaxKind.Parameter, identifier.pos)); + parameter.name = identifier; + finishNode(parameter); + node.parameters = createNodeArray([parameter], parameter.pos, parameter.end); + node.equalsGreaterThanToken = parseExpectedToken(SyntaxKind.EqualsGreaterThanToken); + node.body = parseArrowFunctionExpressionBody(/*isAsync*/ !!asyncModifier); + return addJSDocComment(finishNode(node)); + } + function tryParseParenthesizedArrowFunctionExpression(): Expression | undefined { + const triState = isParenthesizedArrowFunctionExpression(); + if (triState === Tristate.False) { + // It's definitely not a parenthesized arrow function expression. + return undefined; } - - function isStartOfExpression(): boolean { - if (isStartOfLeftHandSideExpression()) { - return true; - } - - switch (token()) { - case SyntaxKind.PlusToken: - case SyntaxKind.MinusToken: - case SyntaxKind.TildeToken: - case SyntaxKind.ExclamationToken: - case SyntaxKind.DeleteKeyword: - case SyntaxKind.TypeOfKeyword: - case SyntaxKind.VoidKeyword: - case SyntaxKind.PlusPlusToken: - case SyntaxKind.MinusMinusToken: - case SyntaxKind.LessThanToken: - case SyntaxKind.AwaitKeyword: - case SyntaxKind.YieldKeyword: - case SyntaxKind.PrivateIdentifier: - // Yield/await always starts an expression. Either it is an identifier (in which case - // it is definitely an expression). Or it's a keyword (either because we're in - // a generator or async function, or in strict mode (or both)) and it started a yield or await expression. - return true; - default: - // Error tolerance. If we see the start of some binary operator, we consider - // that the start of an expression. That way we'll parse out a missing identifier, - // give a good message about an identifier being missing, and then consume the - // rest of the binary expression. - if (isBinaryOperator()) { - return true; - } - - return isIdentifier(); - } - } - - function isStartOfExpressionStatement(): boolean { - // As per the grammar, none of '{' or 'function' or 'class' can start an expression statement. - return token() !== SyntaxKind.OpenBraceToken && - token() !== SyntaxKind.FunctionKeyword && - token() !== SyntaxKind.ClassKeyword && - token() !== SyntaxKind.AtToken && - isStartOfExpression(); - } - - function parseExpression(): Expression { - // Expression[in]: - // AssignmentExpression[in] - // Expression[in] , AssignmentExpression[in] - - // clear the decorator context when parsing Expression, as it should be unambiguous when parsing a decorator - const saveDecoratorContext = inDecoratorContext(); - if (saveDecoratorContext) { - setDecoratorContext(/*val*/ false); - } - - let expr = parseAssignmentExpressionOrHigher(); - let operatorToken: BinaryOperatorToken; - while ((operatorToken = parseOptionalToken(SyntaxKind.CommaToken))) { - expr = makeBinaryExpression(expr, operatorToken, parseAssignmentExpressionOrHigher()); - } - - if (saveDecoratorContext) { - setDecoratorContext(/*val*/ true); - } - return expr; - } - - function parseInitializer(): Expression | undefined { - return parseOptional(SyntaxKind.EqualsToken) ? parseAssignmentExpressionOrHigher() : undefined; - } - - function parseAssignmentExpressionOrHigher(): Expression { - // AssignmentExpression[in,yield]: - // 1) ConditionalExpression[?in,?yield] - // 2) LeftHandSideExpression = AssignmentExpression[?in,?yield] - // 3) LeftHandSideExpression AssignmentOperator AssignmentExpression[?in,?yield] - // 4) ArrowFunctionExpression[?in,?yield] - // 5) AsyncArrowFunctionExpression[in,yield,await] - // 6) [+Yield] YieldExpression[?In] - // - // Note: for ease of implementation we treat productions '2' and '3' as the same thing. - // (i.e. they're both BinaryExpressions with an assignment operator in it). - - // First, do the simple check if we have a YieldExpression (production '6'). - if (isYieldExpression()) { - return parseYieldExpression(); - } - - // Then, check if we have an arrow function (production '4' and '5') that starts with a parenthesized - // parameter list or is an async arrow function. - // AsyncArrowFunctionExpression: - // 1) async[no LineTerminator here]AsyncArrowBindingIdentifier[?Yield][no LineTerminator here]=>AsyncConciseBody[?In] - // 2) CoverCallExpressionAndAsyncArrowHead[?Yield, ?Await][no LineTerminator here]=>AsyncConciseBody[?In] - // Production (1) of AsyncArrowFunctionExpression is parsed in "tryParseAsyncSimpleArrowFunctionExpression". - // And production (2) is parsed in "tryParseParenthesizedArrowFunctionExpression". - // - // If we do successfully parse arrow-function, we must *not* recurse for productions 1, 2 or 3. An ArrowFunction is - // not a LeftHandSideExpression, nor does it start a ConditionalExpression. So we are done - // with AssignmentExpression if we see one. - const arrowExpression = tryParseParenthesizedArrowFunctionExpression() || tryParseAsyncSimpleArrowFunctionExpression(); - if (arrowExpression) { - return arrowExpression; - } - - // Now try to see if we're in production '1', '2' or '3'. A conditional expression can - // start with a LogicalOrExpression, while the assignment productions can only start with - // LeftHandSideExpressions. - // - // So, first, we try to just parse out a BinaryExpression. If we get something that is a - // LeftHandSide or higher, then we can try to parse out the assignment expression part. - // Otherwise, we try to parse out the conditional expression bit. We want to allow any - // binary expression here, so we pass in the 'lowest' precedence here so that it matches - // and consumes anything. - const expr = parseBinaryExpressionOrHigher(/*precedence*/ 0); - - // To avoid a look-ahead, we did not handle the case of an arrow function with a single un-parenthesized - // parameter ('x => ...') above. We handle it here by checking if the parsed expression was a single - // identifier and the current token is an arrow. - if (expr.kind === SyntaxKind.Identifier && token() === SyntaxKind.EqualsGreaterThanToken) { - return parseSimpleArrowFunctionExpression(expr); - } - - // Now see if we might be in cases '2' or '3'. - // If the expression was a LHS expression, and we have an assignment operator, then - // we're in '2' or '3'. Consume the assignment and return. - // - // Note: we call reScanGreaterToken so that we get an appropriately merged token - // for cases like `> > =` becoming `>>=` - if (isLeftHandSideExpression(expr) && isAssignmentOperator(reScanGreaterToken())) { - return makeBinaryExpression(expr, parseTokenNode(), parseAssignmentExpressionOrHigher()); - } - - // It wasn't an assignment or a lambda. This is a conditional expression: - return parseConditionalExpressionRest(expr); - } - - function isYieldExpression(): boolean { - if (token() === SyntaxKind.YieldKeyword) { - // If we have a 'yield' keyword, and this is a context where yield expressions are - // allowed, then definitely parse out a yield expression. - if (inYieldContext()) { - return true; - } - - // We're in a context where 'yield expr' is not allowed. However, if we can - // definitely tell that the user was trying to parse a 'yield expr' and not - // just a normal expr that start with a 'yield' identifier, then parse out - // a 'yield expr'. We can then report an error later that they are only - // allowed in generator expressions. - // - // for example, if we see 'yield(foo)', then we'll have to treat that as an - // invocation expression of something called 'yield'. However, if we have - // 'yield foo' then that is not legal as a normal expression, so we can - // definitely recognize this as a yield expression. - // - // for now we just check if the next token is an identifier. More heuristics - // can be added here later as necessary. We just need to make sure that we - // don't accidentally consume something legal. - return lookAhead(nextTokenIsIdentifierOrKeywordOrLiteralOnSameLine); - } - - return false; + // If we definitely have an arrow function, then we can just parse one, not requiring a + // following => or { token. Otherwise, we *might* have an arrow function. Try to parse + // it out, but don't allow any ambiguity, and return 'undefined' if this could be an + // expression instead. + const arrowFunction = triState === Tristate.True + ? parseParenthesizedArrowFunctionExpressionHead(/*allowAmbiguity*/ true) + : tryParse(parsePossibleParenthesizedArrowFunctionExpressionHead); + if (!arrowFunction) { + // Didn't appear to actually be a parenthesized arrow function. Just bail out. + return undefined; } - - function nextTokenIsIdentifierOnSameLine() { - nextToken(); - return !scanner.hasPrecedingLineBreak() && isIdentifier(); - } - - function parseYieldExpression(): YieldExpression { - const node = createNode(SyntaxKind.YieldExpression); - - // YieldExpression[In] : - // yield - // yield [no LineTerminator here] [Lexical goal InputElementRegExp]AssignmentExpression[?In, Yield] - // yield [no LineTerminator here] * [Lexical goal InputElementRegExp]AssignmentExpression[?In, Yield] - nextToken(); - - if (!scanner.hasPrecedingLineBreak() && - (token() === SyntaxKind.AsteriskToken || isStartOfExpression())) { - node.asteriskToken = parseOptionalToken(SyntaxKind.AsteriskToken); - node.expression = parseAssignmentExpressionOrHigher(); - return finishNode(node); - } - else { - // if the next token is not on the same line as yield. or we don't have an '*' or - // the start of an expression, then this is just a simple "yield" expression. - return finishNode(node); - } + const isAsync = hasModifierOfKind(arrowFunction, SyntaxKind.AsyncKeyword); + // If we have an arrow, then try to parse the body. Even if not, try to parse if we + // have an opening brace, just in case we're in an error state. + const lastToken = token(); + arrowFunction.equalsGreaterThanToken = parseExpectedToken(SyntaxKind.EqualsGreaterThanToken); + arrowFunction.body = (lastToken === SyntaxKind.EqualsGreaterThanToken || lastToken === SyntaxKind.OpenBraceToken) + ? parseArrowFunctionExpressionBody(isAsync) + : parseIdentifier(); + return finishNode(arrowFunction); + } + // True -> We definitely expect a parenthesized arrow function here. + // False -> There *cannot* be a parenthesized arrow function here. + // Unknown -> There *might* be a parenthesized arrow function here. + // Speculatively look ahead to be sure, and rollback if not. + function isParenthesizedArrowFunctionExpression(): Tristate { + if (token() === SyntaxKind.OpenParenToken || token() === SyntaxKind.LessThanToken || token() === SyntaxKind.AsyncKeyword) { + return lookAhead(isParenthesizedArrowFunctionExpressionWorker); + } + if (token() === SyntaxKind.EqualsGreaterThanToken) { + // ERROR RECOVERY TWEAK: + // If we see a standalone => try to parse it as an arrow function expression as that's + // likely what the user intended to write. + return Tristate.True; } - - function parseSimpleArrowFunctionExpression(identifier: Identifier, asyncModifier?: NodeArray | undefined): ArrowFunction { - Debug.assert(token() === SyntaxKind.EqualsGreaterThanToken, "parseSimpleArrowFunctionExpression should only have been called if we had a =>"); - - let node: ArrowFunction; - if (asyncModifier) { - node = createNode(SyntaxKind.ArrowFunction, asyncModifier.pos); - node.modifiers = asyncModifier; - } - else { - node = createNode(SyntaxKind.ArrowFunction, identifier.pos); - } - - const parameter = createNode(SyntaxKind.Parameter, identifier.pos); - parameter.name = identifier; - finishNode(parameter); - - node.parameters = createNodeArray([parameter], parameter.pos, parameter.end); - - node.equalsGreaterThanToken = parseExpectedToken(SyntaxKind.EqualsGreaterThanToken); - node.body = parseArrowFunctionExpressionBody(/*isAsync*/ !!asyncModifier); - - return addJSDocComment(finishNode(node)); - } - - function tryParseParenthesizedArrowFunctionExpression(): Expression | undefined { - const triState = isParenthesizedArrowFunctionExpression(); - if (triState === Tristate.False) { - // It's definitely not a parenthesized arrow function expression. - return undefined; - } - - // If we definitely have an arrow function, then we can just parse one, not requiring a - // following => or { token. Otherwise, we *might* have an arrow function. Try to parse - // it out, but don't allow any ambiguity, and return 'undefined' if this could be an - // expression instead. - const arrowFunction = triState === Tristate.True - ? parseParenthesizedArrowFunctionExpressionHead(/*allowAmbiguity*/ true) - : tryParse(parsePossibleParenthesizedArrowFunctionExpressionHead); - - if (!arrowFunction) { - // Didn't appear to actually be a parenthesized arrow function. Just bail out. - return undefined; + // Definitely not a parenthesized arrow function. + return Tristate.False; + } + function isParenthesizedArrowFunctionExpressionWorker() { + if (token() === SyntaxKind.AsyncKeyword) { + nextToken(); + if (scanner.hasPrecedingLineBreak()) { + return Tristate.False; } - - const isAsync = hasModifierOfKind(arrowFunction, SyntaxKind.AsyncKeyword); - - // If we have an arrow, then try to parse the body. Even if not, try to parse if we - // have an opening brace, just in case we're in an error state. - const lastToken = token(); - arrowFunction.equalsGreaterThanToken = parseExpectedToken(SyntaxKind.EqualsGreaterThanToken); - arrowFunction.body = (lastToken === SyntaxKind.EqualsGreaterThanToken || lastToken === SyntaxKind.OpenBraceToken) - ? parseArrowFunctionExpressionBody(isAsync) - : parseIdentifier(); - - return finishNode(arrowFunction); - } - - // True -> We definitely expect a parenthesized arrow function here. - // False -> There *cannot* be a parenthesized arrow function here. - // Unknown -> There *might* be a parenthesized arrow function here. - // Speculatively look ahead to be sure, and rollback if not. - function isParenthesizedArrowFunctionExpression(): Tristate { - if (token() === SyntaxKind.OpenParenToken || token() === SyntaxKind.LessThanToken || token() === SyntaxKind.AsyncKeyword) { - return lookAhead(isParenthesizedArrowFunctionExpressionWorker); - } - - if (token() === SyntaxKind.EqualsGreaterThanToken) { - // ERROR RECOVERY TWEAK: - // If we see a standalone => try to parse it as an arrow function expression as that's - // likely what the user intended to write. - return Tristate.True; + if (token() !== SyntaxKind.OpenParenToken && token() !== SyntaxKind.LessThanToken) { + return Tristate.False; } - // Definitely not a parenthesized arrow function. - return Tristate.False; } - - function isParenthesizedArrowFunctionExpressionWorker() { - if (token() === SyntaxKind.AsyncKeyword) { - nextToken(); - if (scanner.hasPrecedingLineBreak()) { - return Tristate.False; - } - if (token() !== SyntaxKind.OpenParenToken && token() !== SyntaxKind.LessThanToken) { - return Tristate.False; - } - } - - const first = token(); - const second = nextToken(); - - if (first === SyntaxKind.OpenParenToken) { - if (second === SyntaxKind.CloseParenToken) { - // Simple cases: "() =>", "(): ", and "() {". - // This is an arrow function with no parameters. - // The last one is not actually an arrow function, - // but this is probably what the user intended. - const third = nextToken(); - switch (third) { - case SyntaxKind.EqualsGreaterThanToken: - case SyntaxKind.ColonToken: - case SyntaxKind.OpenBraceToken: - return Tristate.True; - default: - return Tristate.False; - } - } - - // If encounter "([" or "({", this could be the start of a binding pattern. - // Examples: - // ([ x ]) => { } - // ({ x }) => { } - // ([ x ]) - // ({ x }) - if (second === SyntaxKind.OpenBracketToken || second === SyntaxKind.OpenBraceToken) { - return Tristate.Unknown; - } - - // Simple case: "(..." - // This is an arrow function with a rest parameter. - if (second === SyntaxKind.DotDotDotToken) { - return Tristate.True; - } - - // Check for "(xxx yyy", where xxx is a modifier and yyy is an identifier. This - // isn't actually allowed, but we want to treat it as a lambda so we can provide - // a good error message. - if (isModifierKind(second) && second !== SyntaxKind.AsyncKeyword && lookAhead(nextTokenIsIdentifier)) { - return Tristate.True; - } - - // If we had "(" followed by something that's not an identifier, - // then this definitely doesn't look like a lambda. "this" is not - // valid, but we want to parse it and then give a semantic error. - if (!isIdentifier() && second !== SyntaxKind.ThisKeyword) { - return Tristate.False; - } - - switch (nextToken()) { + const first = token(); + const second = nextToken(); + if (first === SyntaxKind.OpenParenToken) { + if (second === SyntaxKind.CloseParenToken) { + // Simple cases: "() =>", "(): ", and "() {". + // This is an arrow function with no parameters. + // The last one is not actually an arrow function, + // but this is probably what the user intended. + const third = nextToken(); + switch (third) { + case SyntaxKind.EqualsGreaterThanToken: case SyntaxKind.ColonToken: - // If we have something like "(a:", then we must have a - // type-annotated parameter in an arrow function expression. + case SyntaxKind.OpenBraceToken: return Tristate.True; - case SyntaxKind.QuestionToken: - nextToken(); - // If we have "(a?:" or "(a?," or "(a?=" or "(a?)" then it is definitely a lambda. - if (token() === SyntaxKind.ColonToken || token() === SyntaxKind.CommaToken || token() === SyntaxKind.EqualsToken || token() === SyntaxKind.CloseParenToken) { - return Tristate.True; - } - // Otherwise it is definitely not a lambda. + default: return Tristate.False; - case SyntaxKind.CommaToken: - case SyntaxKind.EqualsToken: - case SyntaxKind.CloseParenToken: - // If we have "(a," or "(a=" or "(a)" this *could* be an arrow function - return Tristate.Unknown; } - // It is definitely not an arrow function - return Tristate.False; } - else { - Debug.assert(first === SyntaxKind.LessThanToken); - - // If we have "<" not followed by an identifier, - // then this definitely is not an arrow function. - if (!isIdentifier()) { - return Tristate.False; - } - - // JSX overrides - if (sourceFile.languageVariant === LanguageVariant.JSX) { - const isArrowFunctionInJsx = lookAhead(() => { - const third = nextToken(); - if (third === SyntaxKind.ExtendsKeyword) { - const fourth = nextToken(); - switch (fourth) { - case SyntaxKind.EqualsToken: - case SyntaxKind.GreaterThanToken: - return false; - default: - return true; - } - } - else if (third === SyntaxKind.CommaToken) { - return true; - } - return false; - }); - - if (isArrowFunctionInJsx) { - return Tristate.True; - } - - return Tristate.False; - } - - // This *could* be a parenthesized arrow function. + // If encounter "([" or "({", this could be the start of a binding pattern. + // Examples: + // ([ x ]) => { } + // ({ x }) => { } + // ([ x ]) + // ({ x }) + if (second === SyntaxKind.OpenBracketToken || second === SyntaxKind.OpenBraceToken) { return Tristate.Unknown; } - } - - function parsePossibleParenthesizedArrowFunctionExpressionHead(): ArrowFunction | undefined { - const tokenPos = scanner.getTokenPos(); - if (notParenthesizedArrow && notParenthesizedArrow.has(tokenPos.toString())) { - return undefined; + // Simple case: "(..." + // This is an arrow function with a rest parameter. + if (second === SyntaxKind.DotDotDotToken) { + return Tristate.True; } - - const result = parseParenthesizedArrowFunctionExpressionHead(/*allowAmbiguity*/ false); - if (!result) { - (notParenthesizedArrow || (notParenthesizedArrow = createMap())).set(tokenPos.toString(), true); + // Check for "(xxx yyy", where xxx is a modifier and yyy is an identifier. This + // isn't actually allowed, but we want to treat it as a lambda so we can provide + // a good error message. + if (isModifierKind(second) && second !== SyntaxKind.AsyncKeyword && lookAhead(nextTokenIsIdentifier)) { + return Tristate.True; } - - return result; - } - - function tryParseAsyncSimpleArrowFunctionExpression(): ArrowFunction | undefined { - // We do a check here so that we won't be doing unnecessarily call to "lookAhead" - if (token() === SyntaxKind.AsyncKeyword) { - if (lookAhead(isUnParenthesizedAsyncArrowFunctionWorker) === Tristate.True) { - const asyncModifier = parseModifiersForArrowFunction(); - const expr = parseBinaryExpressionOrHigher(/*precedence*/ 0); - return parseSimpleArrowFunctionExpression(expr, asyncModifier); - } + // If we had "(" followed by something that's not an identifier, + // then this definitely doesn't look like a lambda. "this" is not + // valid, but we want to parse it and then give a semantic error. + if (!isIdentifier() && second !== SyntaxKind.ThisKeyword) { + return Tristate.False; } - return undefined; - } - - function isUnParenthesizedAsyncArrowFunctionWorker(): Tristate { - // AsyncArrowFunctionExpression: - // 1) async[no LineTerminator here]AsyncArrowBindingIdentifier[?Yield][no LineTerminator here]=>AsyncConciseBody[?In] - // 2) CoverCallExpressionAndAsyncArrowHead[?Yield, ?Await][no LineTerminator here]=>AsyncConciseBody[?In] - if (token() === SyntaxKind.AsyncKeyword) { - nextToken(); - // If the "async" is followed by "=>" token then it is not a beginning of an async arrow-function - // but instead a simple arrow-function which will be parsed inside "parseAssignmentExpressionOrHigher" - if (scanner.hasPrecedingLineBreak() || token() === SyntaxKind.EqualsGreaterThanToken) { - return Tristate.False; - } - // Check for un-parenthesized AsyncArrowFunction - const expr = parseBinaryExpressionOrHigher(/*precedence*/ 0); - if (!scanner.hasPrecedingLineBreak() && expr.kind === SyntaxKind.Identifier && token() === SyntaxKind.EqualsGreaterThanToken) { + switch (nextToken()) { + case SyntaxKind.ColonToken: + // If we have something like "(a:", then we must have a + // type-annotated parameter in an arrow function expression. return Tristate.True; - } + case SyntaxKind.QuestionToken: + nextToken(); + // If we have "(a?:" or "(a?," or "(a?=" or "(a?)" then it is definitely a lambda. + if (token() === SyntaxKind.ColonToken || token() === SyntaxKind.CommaToken || token() === SyntaxKind.EqualsToken || token() === SyntaxKind.CloseParenToken) { + return Tristate.True; + } + // Otherwise it is definitely not a lambda. + return Tristate.False; + case SyntaxKind.CommaToken: + case SyntaxKind.EqualsToken: + case SyntaxKind.CloseParenToken: + // If we have "(a," or "(a=" or "(a)" this *could* be an arrow function + return Tristate.Unknown; } - + // It is definitely not an arrow function return Tristate.False; } - - function parseParenthesizedArrowFunctionExpressionHead(allowAmbiguity: boolean): ArrowFunction | undefined { - const node = createNodeWithJSDoc(SyntaxKind.ArrowFunction); - node.modifiers = parseModifiersForArrowFunction(); - const isAsync = hasModifierOfKind(node, SyntaxKind.AsyncKeyword) ? SignatureFlags.Await : SignatureFlags.None; - // Arrow functions are never generators. - // - // If we're speculatively parsing a signature for a parenthesized arrow function, then - // we have to have a complete parameter list. Otherwise we might see something like - // a => (b => c) - // And think that "(b =>" was actually a parenthesized arrow function with a missing - // close paren. - if (!fillSignature(SyntaxKind.ColonToken, isAsync, node) && !allowAmbiguity) { - return undefined; - } - - // Parsing a signature isn't enough. - // Parenthesized arrow signatures often look like other valid expressions. - // For instance: - // - "(x = 10)" is an assignment expression parsed as a signature with a default parameter value. - // - "(x,y)" is a comma expression parsed as a signature with two parameters. - // - "a ? (b): c" will have "(b):" parsed as a signature with a return type annotation. - // - "a ? (b): function() {}" will too, since function() is a valid JSDoc function type. - // - // So we need just a bit of lookahead to ensure that it can only be a signature. - const hasJSDocFunctionType = node.type && isJSDocFunctionType(node.type); - if (!allowAmbiguity && token() !== SyntaxKind.EqualsGreaterThanToken && (hasJSDocFunctionType || token() !== SyntaxKind.OpenBraceToken)) { - // Returning undefined here will cause our caller to rewind to where we started from. - return undefined; + else { + Debug.assert(first === SyntaxKind.LessThanToken); + // If we have "<" not followed by an identifier, + // then this definitely is not an arrow function. + if (!isIdentifier()) { + return Tristate.False; } - - return node; - } - - function parseArrowFunctionExpressionBody(isAsync: boolean): Block | Expression { - if (token() === SyntaxKind.OpenBraceToken) { - return parseFunctionBlock(isAsync ? SignatureFlags.Await : SignatureFlags.None); - } - - if (token() !== SyntaxKind.SemicolonToken && - token() !== SyntaxKind.FunctionKeyword && - token() !== SyntaxKind.ClassKeyword && - isStartOfStatement() && - !isStartOfExpressionStatement()) { - // Check if we got a plain statement (i.e. no expression-statements, no function/class expressions/declarations) - // - // Here we try to recover from a potential error situation in the case where the - // user meant to supply a block. For example, if the user wrote: - // - // a => - // let v = 0; - // } - // - // they may be missing an open brace. Check to see if that's the case so we can - // try to recover better. If we don't do this, then the next close curly we see may end - // up preemptively closing the containing construct. - // - // Note: even when 'IgnoreMissingOpenBrace' is passed, parseBody will still error. - return parseFunctionBlock(SignatureFlags.IgnoreMissingOpenBrace | (isAsync ? SignatureFlags.Await : SignatureFlags.None)); - } - - return isAsync - ? doInAwaitContext(parseAssignmentExpressionOrHigher) - : doOutsideOfAwaitContext(parseAssignmentExpressionOrHigher); - } - - function parseConditionalExpressionRest(leftOperand: Expression): Expression { - // Note: we are passed in an expression which was produced from parseBinaryExpressionOrHigher. - const questionToken = parseOptionalToken(SyntaxKind.QuestionToken); - if (!questionToken) { - return leftOperand; - } - - // Note: we explicitly 'allowIn' in the whenTrue part of the condition expression, and - // we do not that for the 'whenFalse' part. - const node = createNode(SyntaxKind.ConditionalExpression, leftOperand.pos); - node.condition = leftOperand; - node.questionToken = questionToken; - node.whenTrue = doOutsideOfContext(disallowInAndDecoratorContext, parseAssignmentExpressionOrHigher); - node.colonToken = parseExpectedToken(SyntaxKind.ColonToken); - node.whenFalse = nodeIsPresent(node.colonToken) - ? parseAssignmentExpressionOrHigher() - : createMissingNode(SyntaxKind.Identifier, /*reportAtCurrentPosition*/ false, Diagnostics._0_expected, tokenToString(SyntaxKind.ColonToken)); - return finishNode(node); - } - - function parseBinaryExpressionOrHigher(precedence: number): Expression { - const leftOperand = parseUnaryExpressionOrHigher(); - return parseBinaryExpressionRest(precedence, leftOperand); - } - - function isInOrOfKeyword(t: SyntaxKind) { - return t === SyntaxKind.InKeyword || t === SyntaxKind.OfKeyword; - } - - function parseBinaryExpressionRest(precedence: number, leftOperand: Expression): Expression { - while (true) { - // We either have a binary operator here, or we're finished. We call - // reScanGreaterToken so that we merge token sequences like > and = into >= - - reScanGreaterToken(); - const newPrecedence = getBinaryOperatorPrecedence(token()); - - // Check the precedence to see if we should "take" this operator - // - For left associative operator (all operator but **), consume the operator, - // recursively call the function below, and parse binaryExpression as a rightOperand - // of the caller if the new precedence of the operator is greater then or equal to the current precedence. - // For example: - // a - b - c; - // ^token; leftOperand = b. Return b to the caller as a rightOperand - // a * b - c - // ^token; leftOperand = b. Return b to the caller as a rightOperand - // a - b * c; - // ^token; leftOperand = b. Return b * c to the caller as a rightOperand - // - For right associative operator (**), consume the operator, recursively call the function - // and parse binaryExpression as a rightOperand of the caller if the new precedence of - // the operator is strictly grater than the current precedence - // For example: - // a ** b ** c; - // ^^token; leftOperand = b. Return b ** c to the caller as a rightOperand - // a - b ** c; - // ^^token; leftOperand = b. Return b ** c to the caller as a rightOperand - // a ** b - c - // ^token; leftOperand = b. Return b to the caller as a rightOperand - const consumeCurrentOperator = token() === SyntaxKind.AsteriskAsteriskToken ? - newPrecedence >= precedence : - newPrecedence > precedence; - - if (!consumeCurrentOperator) { - break; - } - - if (token() === SyntaxKind.InKeyword && inDisallowInContext()) { - break; - } - - if (token() === SyntaxKind.AsKeyword) { - // Make sure we *do* perform ASI for constructs like this: - // var x = foo - // as (Bar) - // This should be parsed as an initialized variable, followed - // by a function call to 'as' with the argument 'Bar' - if (scanner.hasPrecedingLineBreak()) { - break; - } - else { - nextToken(); - leftOperand = makeAsExpression(leftOperand, parseType()); + // JSX overrides + if (sourceFile.languageVariant === LanguageVariant.JSX) { + const isArrowFunctionInJsx = lookAhead(() => { + const third = nextToken(); + if (third === SyntaxKind.ExtendsKeyword) { + const fourth = nextToken(); + switch (fourth) { + case SyntaxKind.EqualsToken: + case SyntaxKind.GreaterThanToken: + return false; + default: + return true; + } } - } - else { - leftOperand = makeBinaryExpression(leftOperand, parseTokenNode(), parseBinaryExpressionOrHigher(newPrecedence)); - } - } - - return leftOperand; - } - - function isBinaryOperator() { - if (inDisallowInContext() && token() === SyntaxKind.InKeyword) { - return false; - } - - return getBinaryOperatorPrecedence(token()) > 0; - } - - function makeBinaryExpression(left: Expression, operatorToken: BinaryOperatorToken, right: Expression): BinaryExpression { - const node = createNode(SyntaxKind.BinaryExpression, left.pos); - node.left = left; - node.operatorToken = operatorToken; - node.right = right; - return finishNode(node); - } - - function makeAsExpression(left: Expression, right: TypeNode): AsExpression { - const node = createNode(SyntaxKind.AsExpression, left.pos); - node.expression = left; - node.type = right; - return finishNode(node); - } - - function parsePrefixUnaryExpression() { - const node = createNode(SyntaxKind.PrefixUnaryExpression); - node.operator = token(); - nextToken(); - node.operand = parseSimpleUnaryExpression(); - - return finishNode(node); - } - - function parseDeleteExpression() { - const node = createNode(SyntaxKind.DeleteExpression); - nextToken(); - node.expression = parseSimpleUnaryExpression(); - return finishNode(node); - } - - function parseTypeOfExpression() { - const node = createNode(SyntaxKind.TypeOfExpression); - nextToken(); - node.expression = parseSimpleUnaryExpression(); - return finishNode(node); - } - - function parseVoidExpression() { - const node = createNode(SyntaxKind.VoidExpression); - nextToken(); - node.expression = parseSimpleUnaryExpression(); - return finishNode(node); - } - - function isAwaitExpression(): boolean { - if (token() === SyntaxKind.AwaitKeyword) { - if (inAwaitContext()) { - return true; - } - - // here we are using similar heuristics as 'isYieldExpression' - return lookAhead(nextTokenIsIdentifierOrKeywordOrLiteralOnSameLine); - } - - return false; - } - - function parseAwaitExpression() { - const node = createNode(SyntaxKind.AwaitExpression); - nextToken(); - node.expression = parseSimpleUnaryExpression(); - return finishNode(node); - } - - /** - * Parse ES7 exponential expression and await expression - * - * ES7 ExponentiationExpression: - * 1) UnaryExpression[?Yield] - * 2) UpdateExpression[?Yield] ** ExponentiationExpression[?Yield] - * - */ - function parseUnaryExpressionOrHigher(): UnaryExpression | BinaryExpression { - /** - * ES7 UpdateExpression: - * 1) LeftHandSideExpression[?Yield] - * 2) LeftHandSideExpression[?Yield][no LineTerminator here]++ - * 3) LeftHandSideExpression[?Yield][no LineTerminator here]-- - * 4) ++UnaryExpression[?Yield] - * 5) --UnaryExpression[?Yield] - */ - if (isUpdateExpression()) { - const updateExpression = parseUpdateExpression(); - return token() === SyntaxKind.AsteriskAsteriskToken ? - parseBinaryExpressionRest(getBinaryOperatorPrecedence(token()), updateExpression) : - updateExpression; - } - - /** - * ES7 UnaryExpression: - * 1) UpdateExpression[?yield] - * 2) delete UpdateExpression[?yield] - * 3) void UpdateExpression[?yield] - * 4) typeof UpdateExpression[?yield] - * 5) + UpdateExpression[?yield] - * 6) - UpdateExpression[?yield] - * 7) ~ UpdateExpression[?yield] - * 8) ! UpdateExpression[?yield] - */ - const unaryOperator = token(); - const simpleUnaryExpression = parseSimpleUnaryExpression(); - if (token() === SyntaxKind.AsteriskAsteriskToken) { - const pos = skipTrivia(sourceText, simpleUnaryExpression.pos); - const { end } = simpleUnaryExpression; - if (simpleUnaryExpression.kind === SyntaxKind.TypeAssertionExpression) { - parseErrorAt(pos, end, Diagnostics.A_type_assertion_expression_is_not_allowed_in_the_left_hand_side_of_an_exponentiation_expression_Consider_enclosing_the_expression_in_parentheses); - } - else { - parseErrorAt(pos, end, Diagnostics.An_unary_expression_with_the_0_operator_is_not_allowed_in_the_left_hand_side_of_an_exponentiation_expression_Consider_enclosing_the_expression_in_parentheses, tokenToString(unaryOperator)); - } - } - return simpleUnaryExpression; - } - - /** - * Parse ES7 simple-unary expression or higher: - * - * ES7 UnaryExpression: - * 1) UpdateExpression[?yield] - * 2) delete UnaryExpression[?yield] - * 3) void UnaryExpression[?yield] - * 4) typeof UnaryExpression[?yield] - * 5) + UnaryExpression[?yield] - * 6) - UnaryExpression[?yield] - * 7) ~ UnaryExpression[?yield] - * 8) ! UnaryExpression[?yield] - * 9) [+Await] await UnaryExpression[?yield] - */ - function parseSimpleUnaryExpression(): UnaryExpression { - switch (token()) { - case SyntaxKind.PlusToken: - case SyntaxKind.MinusToken: - case SyntaxKind.TildeToken: - case SyntaxKind.ExclamationToken: - return parsePrefixUnaryExpression(); - case SyntaxKind.DeleteKeyword: - return parseDeleteExpression(); - case SyntaxKind.TypeOfKeyword: - return parseTypeOfExpression(); - case SyntaxKind.VoidKeyword: - return parseVoidExpression(); - case SyntaxKind.LessThanToken: - // This is modified UnaryExpression grammar in TypeScript - // UnaryExpression (modified): - // < type > UnaryExpression - return parseTypeAssertion(); - case SyntaxKind.AwaitKeyword: - if (isAwaitExpression()) { - return parseAwaitExpression(); + else if (third === SyntaxKind.CommaToken) { + return true; } - // falls through - default: - return parseUpdateExpression(); + return false; + }); + if (isArrowFunctionInJsx) { + return Tristate.True; + } + return Tristate.False; } + // This *could* be a parenthesized arrow function. + return Tristate.Unknown; } - - /** - * Check if the current token can possibly be an ES7 increment expression. - * - * ES7 UpdateExpression: - * LeftHandSideExpression[?Yield] - * LeftHandSideExpression[?Yield][no LineTerminator here]++ - * LeftHandSideExpression[?Yield][no LineTerminator here]-- - * ++LeftHandSideExpression[?Yield] - * --LeftHandSideExpression[?Yield] - */ - function isUpdateExpression(): boolean { - // This function is called inside parseUnaryExpression to decide - // whether to call parseSimpleUnaryExpression or call parseUpdateExpression directly - switch (token()) { - case SyntaxKind.PlusToken: - case SyntaxKind.MinusToken: - case SyntaxKind.TildeToken: - case SyntaxKind.ExclamationToken: - case SyntaxKind.DeleteKeyword: - case SyntaxKind.TypeOfKeyword: - case SyntaxKind.VoidKeyword: - case SyntaxKind.AwaitKeyword: - return false; - case SyntaxKind.LessThanToken: - // If we are not in JSX context, we are parsing TypeAssertion which is an UnaryExpression - if (sourceFile.languageVariant !== LanguageVariant.JSX) { - return false; - } - // We are in JSX context and the token is part of JSXElement. - // falls through - default: - return true; + } + function parsePossibleParenthesizedArrowFunctionExpressionHead(): ArrowFunction | undefined { + const tokenPos = scanner.getTokenPos(); + if (notParenthesizedArrow && notParenthesizedArrow.has(tokenPos.toString())) { + return undefined; + } + const result = parseParenthesizedArrowFunctionExpressionHead(/*allowAmbiguity*/ false); + if (!result) { + (notParenthesizedArrow || (notParenthesizedArrow = createMap())).set(tokenPos.toString(), true); + } + return result; + } + function tryParseAsyncSimpleArrowFunctionExpression(): ArrowFunction | undefined { + // We do a check here so that we won't be doing unnecessarily call to "lookAhead" + if (token() === SyntaxKind.AsyncKeyword) { + if (lookAhead(isUnParenthesizedAsyncArrowFunctionWorker) === Tristate.True) { + const asyncModifier = parseModifiersForArrowFunction(); + const expr = parseBinaryExpressionOrHigher(/*precedence*/ 0); + return parseSimpleArrowFunctionExpression((expr), asyncModifier); } } - - /** - * Parse ES7 UpdateExpression. UpdateExpression is used instead of ES6's PostFixExpression. - * - * ES7 UpdateExpression[yield]: - * 1) LeftHandSideExpression[?yield] - * 2) LeftHandSideExpression[?yield] [[no LineTerminator here]]++ - * 3) LeftHandSideExpression[?yield] [[no LineTerminator here]]-- - * 4) ++LeftHandSideExpression[?yield] - * 5) --LeftHandSideExpression[?yield] - * In TypeScript (2), (3) are parsed as PostfixUnaryExpression. (4), (5) are parsed as PrefixUnaryExpression - */ - function parseUpdateExpression(): UpdateExpression { - if (token() === SyntaxKind.PlusPlusToken || token() === SyntaxKind.MinusMinusToken) { - const node = createNode(SyntaxKind.PrefixUnaryExpression); - node.operator = token(); - nextToken(); - node.operand = parseLeftHandSideExpressionOrHigher(); - return finishNode(node); + return undefined; + } + function isUnParenthesizedAsyncArrowFunctionWorker(): Tristate { + // AsyncArrowFunctionExpression: + // 1) async[no LineTerminator here]AsyncArrowBindingIdentifier[?Yield][no LineTerminator here]=>AsyncConciseBody[?In] + // 2) CoverCallExpressionAndAsyncArrowHead[?Yield, ?Await][no LineTerminator here]=>AsyncConciseBody[?In] + if (token() === SyntaxKind.AsyncKeyword) { + nextToken(); + // If the "async" is followed by "=>" token then it is not a beginning of an async arrow-function + // but instead a simple arrow-function which will be parsed inside "parseAssignmentExpressionOrHigher" + if (scanner.hasPrecedingLineBreak() || token() === SyntaxKind.EqualsGreaterThanToken) { + return Tristate.False; } - else if (sourceFile.languageVariant === LanguageVariant.JSX && token() === SyntaxKind.LessThanToken && lookAhead(nextTokenIsIdentifierOrKeywordOrGreaterThan)) { - // JSXElement is part of primaryExpression - return parseJsxElementOrSelfClosingElementOrFragment(/*inExpressionContext*/ true); - } - - const expression = parseLeftHandSideExpressionOrHigher(); - - Debug.assert(isLeftHandSideExpression(expression)); - if ((token() === SyntaxKind.PlusPlusToken || token() === SyntaxKind.MinusMinusToken) && !scanner.hasPrecedingLineBreak()) { - const node = createNode(SyntaxKind.PostfixUnaryExpression, expression.pos); - node.operand = expression; - node.operator = token(); - nextToken(); - return finishNode(node); + // Check for un-parenthesized AsyncArrowFunction + const expr = parseBinaryExpressionOrHigher(/*precedence*/ 0); + if (!scanner.hasPrecedingLineBreak() && expr.kind === SyntaxKind.Identifier && token() === SyntaxKind.EqualsGreaterThanToken) { + return Tristate.True; } - - return expression; } - - function parseLeftHandSideExpressionOrHigher(): LeftHandSideExpression { - // Original Ecma: - // LeftHandSideExpression: See 11.2 - // NewExpression - // CallExpression - // - // Our simplification: - // - // LeftHandSideExpression: See 11.2 - // MemberExpression - // CallExpression + return Tristate.False; + } + function parseParenthesizedArrowFunctionExpressionHead(allowAmbiguity: boolean): ArrowFunction | undefined { + const node = (createNodeWithJSDoc(SyntaxKind.ArrowFunction)); + node.modifiers = parseModifiersForArrowFunction(); + const isAsync = hasModifierOfKind(node, SyntaxKind.AsyncKeyword) ? SignatureFlags.Await : SignatureFlags.None; + // Arrow functions are never generators. + // + // If we're speculatively parsing a signature for a parenthesized arrow function, then + // we have to have a complete parameter list. Otherwise we might see something like + // a => (b => c) + // And think that "(b =>" was actually a parenthesized arrow function with a missing + // close paren. + if (!fillSignature(SyntaxKind.ColonToken, isAsync, node) && !allowAmbiguity) { + return undefined; + } + // Parsing a signature isn't enough. + // Parenthesized arrow signatures often look like other valid expressions. + // For instance: + // - "(x = 10)" is an assignment expression parsed as a signature with a default parameter value. + // - "(x,y)" is a comma expression parsed as a signature with two parameters. + // - "a ? (b): c" will have "(b):" parsed as a signature with a return type annotation. + // - "a ? (b): function() {}" will too, since function() is a valid JSDoc function type. + // + // So we need just a bit of lookahead to ensure that it can only be a signature. + const hasJSDocFunctionType = node.type && isJSDocFunctionType(node.type); + if (!allowAmbiguity && token() !== SyntaxKind.EqualsGreaterThanToken && (hasJSDocFunctionType || token() !== SyntaxKind.OpenBraceToken)) { + // Returning undefined here will cause our caller to rewind to where we started from. + return undefined; + } + return node; + } + function parseArrowFunctionExpressionBody(isAsync: boolean): Block | Expression { + if (token() === SyntaxKind.OpenBraceToken) { + return parseFunctionBlock(isAsync ? SignatureFlags.Await : SignatureFlags.None); + } + if (token() !== SyntaxKind.SemicolonToken && + token() !== SyntaxKind.FunctionKeyword && + token() !== SyntaxKind.ClassKeyword && + isStartOfStatement() && + !isStartOfExpressionStatement()) { + // Check if we got a plain statement (i.e. no expression-statements, no function/class expressions/declarations) // - // See comment in parseMemberExpressionOrHigher on how we replaced NewExpression with - // MemberExpression to make our lives easier. + // Here we try to recover from a potential error situation in the case where the + // user meant to supply a block. For example, if the user wrote: // - // to best understand the below code, it's important to see how CallExpression expands - // out into its own productions: + // a => + // let v = 0; + // } // - // CallExpression: - // MemberExpression Arguments - // CallExpression Arguments - // CallExpression[Expression] - // CallExpression.IdentifierName - // import (AssignmentExpression) - // super Arguments - // super.IdentifierName + // they may be missing an open brace. Check to see if that's the case so we can + // try to recover better. If we don't do this, then the next close curly we see may end + // up preemptively closing the containing construct. // - // Because of the recursion in these calls, we need to bottom out first. There are three - // bottom out states we can run into: 1) We see 'super' which must start either of - // the last two CallExpression productions. 2) We see 'import' which must start import call. - // 3)we have a MemberExpression which either completes the LeftHandSideExpression, - // or starts the beginning of the first four CallExpression productions. - let expression: MemberExpression; - if (token() === SyntaxKind.ImportKeyword) { - if (lookAhead(nextTokenIsOpenParenOrLessThan)) { - // We don't want to eagerly consume all import keyword as import call expression so we look ahead to find "(" - // For example: - // var foo3 = require("subfolder - // import * as foo1 from "module-from-node - // We want this import to be a statement rather than import call expression - sourceFile.flags |= NodeFlags.PossiblyContainsDynamicImport; - expression = parseTokenNode(); - } - else if (lookAhead(nextTokenIsDot)) { - // This is an 'import.*' metaproperty (i.e. 'import.meta') - const fullStart = scanner.getStartPos(); - nextToken(); // advance past the 'import' - nextToken(); // advance past the dot - const node = createNode(SyntaxKind.MetaProperty, fullStart) as MetaProperty; - node.keywordToken = SyntaxKind.ImportKeyword; - node.name = parseIdentifierName(); - expression = finishNode(node); - - sourceFile.flags |= NodeFlags.PossiblyContainsImportMeta; + // Note: even when 'IgnoreMissingOpenBrace' is passed, parseBody will still error. + return parseFunctionBlock(SignatureFlags.IgnoreMissingOpenBrace | (isAsync ? SignatureFlags.Await : SignatureFlags.None)); + } + return isAsync + ? doInAwaitContext(parseAssignmentExpressionOrHigher) + : doOutsideOfAwaitContext(parseAssignmentExpressionOrHigher); + } + function parseConditionalExpressionRest(leftOperand: Expression): Expression { + // Note: we are passed in an expression which was produced from parseBinaryExpressionOrHigher. + const questionToken = parseOptionalToken(SyntaxKind.QuestionToken); + if (!questionToken) { + return leftOperand; + } + // Note: we explicitly 'allowIn' in the whenTrue part of the condition expression, and + // we do not that for the 'whenFalse' part. + const node = (createNode(SyntaxKind.ConditionalExpression, leftOperand.pos)); + node.condition = leftOperand; + node.questionToken = questionToken; + node.whenTrue = doOutsideOfContext(disallowInAndDecoratorContext, parseAssignmentExpressionOrHigher); + node.colonToken = parseExpectedToken(SyntaxKind.ColonToken); + node.whenFalse = nodeIsPresent(node.colonToken) + ? parseAssignmentExpressionOrHigher() + : createMissingNode(SyntaxKind.Identifier, /*reportAtCurrentPosition*/ false, Diagnostics._0_expected, tokenToString(SyntaxKind.ColonToken)); + return finishNode(node); + } + function parseBinaryExpressionOrHigher(precedence: number): Expression { + const leftOperand = parseUnaryExpressionOrHigher(); + return parseBinaryExpressionRest(precedence, leftOperand); + } + function isInOrOfKeyword(t: SyntaxKind) { + return t === SyntaxKind.InKeyword || t === SyntaxKind.OfKeyword; + } + function parseBinaryExpressionRest(precedence: number, leftOperand: Expression): Expression { + while (true) { + // We either have a binary operator here, or we're finished. We call + // reScanGreaterToken so that we merge token sequences like > and = into >= + reScanGreaterToken(); + const newPrecedence = getBinaryOperatorPrecedence(token()); + // Check the precedence to see if we should "take" this operator + // - For left associative operator (all operator but **), consume the operator, + // recursively call the function below, and parse binaryExpression as a rightOperand + // of the caller if the new precedence of the operator is greater then or equal to the current precedence. + // For example: + // a - b - c; + // ^token; leftOperand = b. Return b to the caller as a rightOperand + // a * b - c + // ^token; leftOperand = b. Return b to the caller as a rightOperand + // a - b * c; + // ^token; leftOperand = b. Return b * c to the caller as a rightOperand + // - For right associative operator (**), consume the operator, recursively call the function + // and parse binaryExpression as a rightOperand of the caller if the new precedence of + // the operator is strictly grater than the current precedence + // For example: + // a ** b ** c; + // ^^token; leftOperand = b. Return b ** c to the caller as a rightOperand + // a - b ** c; + // ^^token; leftOperand = b. Return b ** c to the caller as a rightOperand + // a ** b - c + // ^token; leftOperand = b. Return b to the caller as a rightOperand + const consumeCurrentOperator = token() === SyntaxKind.AsteriskAsteriskToken ? + newPrecedence >= precedence : + newPrecedence > precedence; + if (!consumeCurrentOperator) { + break; + } + if (token() === SyntaxKind.InKeyword && inDisallowInContext()) { + break; + } + if (token() === SyntaxKind.AsKeyword) { + // Make sure we *do* perform ASI for constructs like this: + // var x = foo + // as (Bar) + // This should be parsed as an initialized variable, followed + // by a function call to 'as' with the argument 'Bar' + if (scanner.hasPrecedingLineBreak()) { + break; } else { - expression = parseMemberExpressionOrHigher(); + nextToken(); + leftOperand = makeAsExpression(leftOperand, parseType()); } } else { - expression = token() === SyntaxKind.SuperKeyword ? parseSuperExpression() : parseMemberExpressionOrHigher(); - } - - // Now, we *may* be complete. However, we might have consumed the start of a - // CallExpression or OptionalExpression. As such, we need to consume the rest - // of it here to be complete. - return parseCallExpressionRest(expression); - } - - function parseMemberExpressionOrHigher(): MemberExpression { - // Note: to make our lives simpler, we decompose the NewExpression productions and - // place ObjectCreationExpression and FunctionExpression into PrimaryExpression. - // like so: - // - // PrimaryExpression : See 11.1 - // this - // Identifier - // Literal - // ArrayLiteral - // ObjectLiteral - // (Expression) - // FunctionExpression - // new MemberExpression Arguments? - // - // MemberExpression : See 11.2 - // PrimaryExpression - // MemberExpression[Expression] - // MemberExpression.IdentifierName - // - // CallExpression : See 11.2 - // MemberExpression - // CallExpression Arguments - // CallExpression[Expression] - // CallExpression.IdentifierName - // - // Technically this is ambiguous. i.e. CallExpression defines: - // - // CallExpression: - // CallExpression Arguments - // - // If you see: "new Foo()" - // - // Then that could be treated as a single ObjectCreationExpression, or it could be - // treated as the invocation of "new Foo". We disambiguate that in code (to match - // the original grammar) by making sure that if we see an ObjectCreationExpression - // we always consume arguments if they are there. So we treat "new Foo()" as an - // object creation only, and not at all as an invocation. Another way to think - // about this is that for every "new" that we see, we will consume an argument list if - // it is there as part of the *associated* object creation node. Any additional - // argument lists we see, will become invocation expressions. - // - // Because there are no other places in the grammar now that refer to FunctionExpression - // or ObjectCreationExpression, it is safe to push down into the PrimaryExpression - // production. - // - // Because CallExpression and MemberExpression are left recursive, we need to bottom out - // of the recursion immediately. So we parse out a primary expression to start with. - const expression = parsePrimaryExpression(); - return parseMemberExpressionRest(expression, /*allowOptionalChain*/ true); - } - - function parseSuperExpression(): MemberExpression { - const expression = parseTokenNode(); - if (token() === SyntaxKind.LessThanToken) { - const startPos = getNodePos(); - const typeArguments = tryParse(parseTypeArgumentsInExpression); - if (typeArguments !== undefined) { - parseErrorAt(startPos, getNodePos(), Diagnostics.super_may_not_use_type_arguments); - } + leftOperand = makeBinaryExpression(leftOperand, parseTokenNode(), parseBinaryExpressionOrHigher(newPrecedence)); } - - if (token() === SyntaxKind.OpenParenToken || token() === SyntaxKind.DotToken || token() === SyntaxKind.OpenBracketToken) { - return expression; - } - - // If we have seen "super" it must be followed by '(' or '.'. - // If it wasn't then just try to parse out a '.' and report an error. - const node = createNode(SyntaxKind.PropertyAccessExpression, expression.pos); - node.expression = expression; - parseExpectedToken(SyntaxKind.DotToken, Diagnostics.super_must_be_followed_by_an_argument_list_or_member_access); - // private names will never work with `super` (`super.#foo`), but that's a semantic error, not syntactic - node.name = parseRightSideOfDot(/*allowIdentifierNames*/ true, /*allowPrivateIdentifiers*/ true); - return finishNode(node); } - - function parseJsxElementOrSelfClosingElementOrFragment(inExpressionContext: boolean): JsxElement | JsxSelfClosingElement | JsxFragment { - const opening = parseJsxOpeningOrSelfClosingElementOrOpeningFragment(inExpressionContext); - let result: JsxElement | JsxSelfClosingElement | JsxFragment; - if (opening.kind === SyntaxKind.JsxOpeningElement) { - const node = createNode(SyntaxKind.JsxElement, opening.pos); - node.openingElement = opening; - - node.children = parseJsxChildren(node.openingElement); - node.closingElement = parseJsxClosingElement(inExpressionContext); - - if (!tagNamesAreEquivalent(node.openingElement.tagName, node.closingElement.tagName)) { - parseErrorAtRange(node.closingElement, Diagnostics.Expected_corresponding_JSX_closing_tag_for_0, getTextOfNodeFromSourceText(sourceText, node.openingElement.tagName)); - } - - result = finishNode(node); + return leftOperand; + } + function isBinaryOperator() { + if (inDisallowInContext() && token() === SyntaxKind.InKeyword) { + return false; + } + return getBinaryOperatorPrecedence(token()) > 0; + } + function makeBinaryExpression(left: Expression, operatorToken: BinaryOperatorToken, right: Expression): BinaryExpression { + const node = (createNode(SyntaxKind.BinaryExpression, left.pos)); + node.left = left; + node.operatorToken = operatorToken; + node.right = right; + return finishNode(node); + } + function makeAsExpression(left: Expression, right: TypeNode): AsExpression { + const node = (createNode(SyntaxKind.AsExpression, left.pos)); + node.expression = left; + node.type = right; + return finishNode(node); + } + function parsePrefixUnaryExpression() { + const node = (createNode(SyntaxKind.PrefixUnaryExpression)); + node.operator = (token()); + nextToken(); + node.operand = parseSimpleUnaryExpression(); + return finishNode(node); + } + function parseDeleteExpression() { + const node = (createNode(SyntaxKind.DeleteExpression)); + nextToken(); + node.expression = parseSimpleUnaryExpression(); + return finishNode(node); + } + function parseTypeOfExpression() { + const node = (createNode(SyntaxKind.TypeOfExpression)); + nextToken(); + node.expression = parseSimpleUnaryExpression(); + return finishNode(node); + } + function parseVoidExpression() { + const node = (createNode(SyntaxKind.VoidExpression)); + nextToken(); + node.expression = parseSimpleUnaryExpression(); + return finishNode(node); + } + function isAwaitExpression(): boolean { + if (token() === SyntaxKind.AwaitKeyword) { + if (inAwaitContext()) { + return true; } - else if (opening.kind === SyntaxKind.JsxOpeningFragment) { - const node = createNode(SyntaxKind.JsxFragment, opening.pos); - node.openingFragment = opening; - node.children = parseJsxChildren(node.openingFragment); - node.closingFragment = parseJsxClosingFragment(inExpressionContext); - - result = finishNode(node); + // here we are using similar heuristics as 'isYieldExpression' + return lookAhead(nextTokenIsIdentifierOrKeywordOrLiteralOnSameLine); + } + return false; + } + function parseAwaitExpression() { + const node = (createNode(SyntaxKind.AwaitExpression)); + nextToken(); + node.expression = parseSimpleUnaryExpression(); + return finishNode(node); + } + /** + * Parse ES7 exponential expression and await expression + * + * ES7 ExponentiationExpression: + * 1) UnaryExpression[?Yield] + * 2) UpdateExpression[?Yield] ** ExponentiationExpression[?Yield] + * + */ + function parseUnaryExpressionOrHigher(): UnaryExpression | BinaryExpression { + /** + * ES7 UpdateExpression: + * 1) LeftHandSideExpression[?Yield] + * 2) LeftHandSideExpression[?Yield][no LineTerminator here]++ + * 3) LeftHandSideExpression[?Yield][no LineTerminator here]-- + * 4) ++UnaryExpression[?Yield] + * 5) --UnaryExpression[?Yield] + */ + if (isUpdateExpression()) { + const updateExpression = parseUpdateExpression(); + return token() === SyntaxKind.AsteriskAsteriskToken ? + parseBinaryExpressionRest(getBinaryOperatorPrecedence(token()), updateExpression) : + updateExpression; + } + /** + * ES7 UnaryExpression: + * 1) UpdateExpression[?yield] + * 2) delete UpdateExpression[?yield] + * 3) void UpdateExpression[?yield] + * 4) typeof UpdateExpression[?yield] + * 5) + UpdateExpression[?yield] + * 6) - UpdateExpression[?yield] + * 7) ~ UpdateExpression[?yield] + * 8) ! UpdateExpression[?yield] + */ + const unaryOperator = token(); + const simpleUnaryExpression = parseSimpleUnaryExpression(); + if (token() === SyntaxKind.AsteriskAsteriskToken) { + const pos = skipTrivia(sourceText, simpleUnaryExpression.pos); + const { end } = simpleUnaryExpression; + if (simpleUnaryExpression.kind === SyntaxKind.TypeAssertionExpression) { + parseErrorAt(pos, end, Diagnostics.A_type_assertion_expression_is_not_allowed_in_the_left_hand_side_of_an_exponentiation_expression_Consider_enclosing_the_expression_in_parentheses); } else { - Debug.assert(opening.kind === SyntaxKind.JsxSelfClosingElement); - // Nothing else to do for self-closing elements - result = opening; - } - - // If the user writes the invalid code '
' in an expression context (i.e. not wrapped in - // an enclosing tag), we'll naively try to parse ^ this as a 'less than' operator and the remainder of the tag - // as garbage, which will cause the formatter to badly mangle the JSX. Perform a speculative parse of a JSX - // element if we see a < token so that we can wrap it in a synthetic binary expression so the formatter - // does less damage and we can report a better error. - // Since JSX elements are invalid < operands anyway, this lookahead parse will only occur in error scenarios - // of one sort or another. - if (inExpressionContext && token() === SyntaxKind.LessThanToken) { - const invalidElement = tryParse(() => parseJsxElementOrSelfClosingElementOrFragment(/*inExpressionContext*/ true)); - if (invalidElement) { - parseErrorAtCurrentToken(Diagnostics.JSX_expressions_must_have_one_parent_element); - const badNode = createNode(SyntaxKind.BinaryExpression, result.pos); - badNode.end = invalidElement.end; - badNode.left = result; - badNode.right = invalidElement; - badNode.operatorToken = createMissingNode(SyntaxKind.CommaToken, /*reportAtCurrentPosition*/ false); - badNode.operatorToken.pos = badNode.operatorToken.end = badNode.right.pos; - return badNode; - } + parseErrorAt(pos, end, Diagnostics.An_unary_expression_with_the_0_operator_is_not_allowed_in_the_left_hand_side_of_an_exponentiation_expression_Consider_enclosing_the_expression_in_parentheses, tokenToString(unaryOperator)); } - - return result; } - - function parseJsxText(): JsxText { - const node = createNode(SyntaxKind.JsxText); - node.text = scanner.getTokenValue(); - node.containsOnlyTriviaWhiteSpaces = currentToken === SyntaxKind.JsxTextAllWhiteSpaces; - currentToken = scanner.scanJsxToken(); + return simpleUnaryExpression; + } + /** + * Parse ES7 simple-unary expression or higher: + * + * ES7 UnaryExpression: + * 1) UpdateExpression[?yield] + * 2) delete UnaryExpression[?yield] + * 3) void UnaryExpression[?yield] + * 4) typeof UnaryExpression[?yield] + * 5) + UnaryExpression[?yield] + * 6) - UnaryExpression[?yield] + * 7) ~ UnaryExpression[?yield] + * 8) ! UnaryExpression[?yield] + * 9) [+Await] await UnaryExpression[?yield] + */ + function parseSimpleUnaryExpression(): UnaryExpression { + switch (token()) { + case SyntaxKind.PlusToken: + case SyntaxKind.MinusToken: + case SyntaxKind.TildeToken: + case SyntaxKind.ExclamationToken: + return parsePrefixUnaryExpression(); + case SyntaxKind.DeleteKeyword: + return parseDeleteExpression(); + case SyntaxKind.TypeOfKeyword: + return parseTypeOfExpression(); + case SyntaxKind.VoidKeyword: + return parseVoidExpression(); + case SyntaxKind.LessThanToken: + // This is modified UnaryExpression grammar in TypeScript + // UnaryExpression (modified): + // < type > UnaryExpression + return parseTypeAssertion(); + case SyntaxKind.AwaitKeyword: + if (isAwaitExpression()) { + return parseAwaitExpression(); + } + // falls through + default: + return parseUpdateExpression(); + } + } + /** + * Check if the current token can possibly be an ES7 increment expression. + * + * ES7 UpdateExpression: + * LeftHandSideExpression[?Yield] + * LeftHandSideExpression[?Yield][no LineTerminator here]++ + * LeftHandSideExpression[?Yield][no LineTerminator here]-- + * ++LeftHandSideExpression[?Yield] + * --LeftHandSideExpression[?Yield] + */ + function isUpdateExpression(): boolean { + // This function is called inside parseUnaryExpression to decide + // whether to call parseSimpleUnaryExpression or call parseUpdateExpression directly + switch (token()) { + case SyntaxKind.PlusToken: + case SyntaxKind.MinusToken: + case SyntaxKind.TildeToken: + case SyntaxKind.ExclamationToken: + case SyntaxKind.DeleteKeyword: + case SyntaxKind.TypeOfKeyword: + case SyntaxKind.VoidKeyword: + case SyntaxKind.AwaitKeyword: + return false; + case SyntaxKind.LessThanToken: + // If we are not in JSX context, we are parsing TypeAssertion which is an UnaryExpression + if (sourceFile.languageVariant !== LanguageVariant.JSX) { + return false; + } + // We are in JSX context and the token is part of JSXElement. + // falls through + default: + return true; + } + } + /** + * Parse ES7 UpdateExpression. UpdateExpression is used instead of ES6's PostFixExpression. + * + * ES7 UpdateExpression[yield]: + * 1) LeftHandSideExpression[?yield] + * 2) LeftHandSideExpression[?yield] [[no LineTerminator here]]++ + * 3) LeftHandSideExpression[?yield] [[no LineTerminator here]]-- + * 4) ++LeftHandSideExpression[?yield] + * 5) --LeftHandSideExpression[?yield] + * In TypeScript (2), (3) are parsed as PostfixUnaryExpression. (4), (5) are parsed as PrefixUnaryExpression + */ + function parseUpdateExpression(): UpdateExpression { + if (token() === SyntaxKind.PlusPlusToken || token() === SyntaxKind.MinusMinusToken) { + const node = (createNode(SyntaxKind.PrefixUnaryExpression)); + node.operator = (token()); + nextToken(); + node.operand = parseLeftHandSideExpressionOrHigher(); return finishNode(node); } - - function parseJsxChild(openingTag: JsxOpeningElement | JsxOpeningFragment, token: JsxTokenSyntaxKind): JsxChild | undefined { - switch (token) { - case SyntaxKind.EndOfFileToken: - // If we hit EOF, issue the error at the tag that lacks the closing element - // rather than at the end of the file (which is useless) - if (isJsxOpeningFragment(openingTag)) { - parseErrorAtRange(openingTag, Diagnostics.JSX_fragment_has_no_corresponding_closing_tag); - } - else { - parseErrorAtRange(openingTag.tagName, Diagnostics.JSX_element_0_has_no_corresponding_closing_tag, getTextOfNodeFromSourceText(sourceText, openingTag.tagName)); - } - return undefined; - case SyntaxKind.LessThanSlashToken: - case SyntaxKind.ConflictMarkerTrivia: - return undefined; - case SyntaxKind.JsxText: - case SyntaxKind.JsxTextAllWhiteSpaces: - return parseJsxText(); - case SyntaxKind.OpenBraceToken: - return parseJsxExpression(/*inExpressionContext*/ false); - case SyntaxKind.LessThanToken: - return parseJsxElementOrSelfClosingElementOrFragment(/*inExpressionContext*/ false); - default: - return Debug.assertNever(token); - } + else if (sourceFile.languageVariant === LanguageVariant.JSX && token() === SyntaxKind.LessThanToken && lookAhead(nextTokenIsIdentifierOrKeywordOrGreaterThan)) { + // JSXElement is part of primaryExpression + return parseJsxElementOrSelfClosingElementOrFragment(/*inExpressionContext*/ true); } - - function parseJsxChildren(openingTag: JsxOpeningElement | JsxOpeningFragment): NodeArray { - const list = []; - const listPos = getNodePos(); - const saveParsingContext = parsingContext; - parsingContext |= 1 << ParsingContext.JsxChildren; - - while (true) { - const child = parseJsxChild(openingTag, currentToken = scanner.reScanJsxToken()); - if (!child) break; - list.push(child); - } - - parsingContext = saveParsingContext; - return createNodeArray(list, listPos); - } - - function parseJsxAttributes(): JsxAttributes { - const jsxAttributes = createNode(SyntaxKind.JsxAttributes); - jsxAttributes.properties = parseList(ParsingContext.JsxAttributes, parseJsxAttribute); - return finishNode(jsxAttributes); - } - - function parseJsxOpeningOrSelfClosingElementOrOpeningFragment(inExpressionContext: boolean): JsxOpeningElement | JsxSelfClosingElement | JsxOpeningFragment { - const fullStart = scanner.getStartPos(); - - parseExpected(SyntaxKind.LessThanToken); - - if (token() === SyntaxKind.GreaterThanToken) { - // See below for explanation of scanJsxText - const node: JsxOpeningFragment = createNode(SyntaxKind.JsxOpeningFragment, fullStart); - scanJsxText(); - return finishNode(node); - } - - const tagName = parseJsxElementName(); - const typeArguments = tryParseTypeArguments(); - const attributes = parseJsxAttributes(); - - let node: JsxOpeningLikeElement; - - if (token() === SyntaxKind.GreaterThanToken) { - // Closing tag, so scan the immediately-following text with the JSX scanning instead - // of regular scanning to avoid treating illegal characters (e.g. '#') as immediate - // scanning errors - node = createNode(SyntaxKind.JsxOpeningElement, fullStart); - scanJsxText(); + const expression = parseLeftHandSideExpressionOrHigher(); + Debug.assert(isLeftHandSideExpression(expression)); + if ((token() === SyntaxKind.PlusPlusToken || token() === SyntaxKind.MinusMinusToken) && !scanner.hasPrecedingLineBreak()) { + const node = (createNode(SyntaxKind.PostfixUnaryExpression, expression.pos)); + node.operand = expression; + node.operator = (token()); + nextToken(); + return finishNode(node); + } + return expression; + } + function parseLeftHandSideExpressionOrHigher(): LeftHandSideExpression { + // Original Ecma: + // LeftHandSideExpression: See 11.2 + // NewExpression + // CallExpression + // + // Our simplification: + // + // LeftHandSideExpression: See 11.2 + // MemberExpression + // CallExpression + // + // See comment in parseMemberExpressionOrHigher on how we replaced NewExpression with + // MemberExpression to make our lives easier. + // + // to best understand the below code, it's important to see how CallExpression expands + // out into its own productions: + // + // CallExpression: + // MemberExpression Arguments + // CallExpression Arguments + // CallExpression[Expression] + // CallExpression.IdentifierName + // import (AssignmentExpression) + // super Arguments + // super.IdentifierName + // + // Because of the recursion in these calls, we need to bottom out first. There are three + // bottom out states we can run into: 1) We see 'super' which must start either of + // the last two CallExpression productions. 2) We see 'import' which must start import call. + // 3)we have a MemberExpression which either completes the LeftHandSideExpression, + // or starts the beginning of the first four CallExpression productions. + let expression: MemberExpression; + if (token() === SyntaxKind.ImportKeyword) { + if (lookAhead(nextTokenIsOpenParenOrLessThan)) { + // We don't want to eagerly consume all import keyword as import call expression so we look ahead to find "(" + // For example: + // var foo3 = require("subfolder + // import * as foo1 from "module-from-node + // We want this import to be a statement rather than import call expression + sourceFile.flags |= NodeFlags.PossiblyContainsDynamicImport; + expression = parseTokenNode(); + } + else if (lookAhead(nextTokenIsDot)) { + // This is an 'import.*' metaproperty (i.e. 'import.meta') + const fullStart = scanner.getStartPos(); + nextToken(); // advance past the 'import' + nextToken(); // advance past the dot + const node = (createNode(SyntaxKind.MetaProperty, fullStart) as MetaProperty); + node.keywordToken = SyntaxKind.ImportKeyword; + node.name = parseIdentifierName(); + expression = finishNode(node); + sourceFile.flags |= NodeFlags.PossiblyContainsImportMeta; } else { - parseExpected(SyntaxKind.SlashToken); - if (inExpressionContext) { - parseExpected(SyntaxKind.GreaterThanToken); - } - else { - parseExpected(SyntaxKind.GreaterThanToken, /*diagnostic*/ undefined, /*shouldAdvance*/ false); - scanJsxText(); - } - node = createNode(SyntaxKind.JsxSelfClosingElement, fullStart); + expression = parseMemberExpressionOrHigher(); } - - node.tagName = tagName; - node.typeArguments = typeArguments; - node.attributes = attributes; - - return finishNode(node); } - - function parseJsxElementName(): JsxTagNameExpression { - scanJsxIdentifier(); - // JsxElement can have name in the form of - // propertyAccessExpression - // primaryExpression in the form of an identifier and "this" keyword - // We can't just simply use parseLeftHandSideExpressionOrHigher because then we will start consider class,function etc as a keyword - // We only want to consider "this" as a primaryExpression - let expression: JsxTagNameExpression = token() === SyntaxKind.ThisKeyword ? - parseTokenNode() : parseIdentifierName(); - while (parseOptional(SyntaxKind.DotToken)) { - const propertyAccess: JsxTagNamePropertyAccess = createNode(SyntaxKind.PropertyAccessExpression, expression.pos); - propertyAccess.expression = expression; - propertyAccess.name = parseRightSideOfDot(/*allowIdentifierNames*/ true, /*allowPrivateIdentifiers*/ false); - expression = finishNode(propertyAccess); + else { + expression = token() === SyntaxKind.SuperKeyword ? parseSuperExpression() : parseMemberExpressionOrHigher(); + } + // Now, we *may* be complete. However, we might have consumed the start of a + // CallExpression or OptionalExpression. As such, we need to consume the rest + // of it here to be complete. + return parseCallExpressionRest(expression); + } + function parseMemberExpressionOrHigher(): MemberExpression { + // Note: to make our lives simpler, we decompose the NewExpression productions and + // place ObjectCreationExpression and FunctionExpression into PrimaryExpression. + // like so: + // + // PrimaryExpression : See 11.1 + // this + // Identifier + // Literal + // ArrayLiteral + // ObjectLiteral + // (Expression) + // FunctionExpression + // new MemberExpression Arguments? + // + // MemberExpression : See 11.2 + // PrimaryExpression + // MemberExpression[Expression] + // MemberExpression.IdentifierName + // + // CallExpression : See 11.2 + // MemberExpression + // CallExpression Arguments + // CallExpression[Expression] + // CallExpression.IdentifierName + // + // Technically this is ambiguous. i.e. CallExpression defines: + // + // CallExpression: + // CallExpression Arguments + // + // If you see: "new Foo()" + // + // Then that could be treated as a single ObjectCreationExpression, or it could be + // treated as the invocation of "new Foo". We disambiguate that in code (to match + // the original grammar) by making sure that if we see an ObjectCreationExpression + // we always consume arguments if they are there. So we treat "new Foo()" as an + // object creation only, and not at all as an invocation. Another way to think + // about this is that for every "new" that we see, we will consume an argument list if + // it is there as part of the *associated* object creation node. Any additional + // argument lists we see, will become invocation expressions. + // + // Because there are no other places in the grammar now that refer to FunctionExpression + // or ObjectCreationExpression, it is safe to push down into the PrimaryExpression + // production. + // + // Because CallExpression and MemberExpression are left recursive, we need to bottom out + // of the recursion immediately. So we parse out a primary expression to start with. + const expression = parsePrimaryExpression(); + return parseMemberExpressionRest(expression, /*allowOptionalChain*/ true); + } + function parseSuperExpression(): MemberExpression { + const expression = parseTokenNode(); + if (token() === SyntaxKind.LessThanToken) { + const startPos = getNodePos(); + const typeArguments = tryParse(parseTypeArgumentsInExpression); + if (typeArguments !== undefined) { + parseErrorAt(startPos, getNodePos(), Diagnostics.super_may_not_use_type_arguments); } + } + if (token() === SyntaxKind.OpenParenToken || token() === SyntaxKind.DotToken || token() === SyntaxKind.OpenBracketToken) { return expression; } - - function parseJsxExpression(inExpressionContext: boolean): JsxExpression | undefined { - const node = createNode(SyntaxKind.JsxExpression); - - if (!parseExpected(SyntaxKind.OpenBraceToken)) { - return undefined; - } - - if (token() !== SyntaxKind.CloseBraceToken) { - node.dotDotDotToken = parseOptionalToken(SyntaxKind.DotDotDotToken); - // Only an AssignmentExpression is valid here per the JSX spec, - // but we can unambiguously parse a comma sequence and provide - // a better error message in grammar checking. - node.expression = parseExpression(); - } - if (inExpressionContext) { - parseExpected(SyntaxKind.CloseBraceToken); - } - else { - if (parseExpected(SyntaxKind.CloseBraceToken, /*message*/ undefined, /*shouldAdvance*/ false)) { - scanJsxText(); - } - } - - return finishNode(node); + // If we have seen "super" it must be followed by '(' or '.'. + // If it wasn't then just try to parse out a '.' and report an error. + const node = (createNode(SyntaxKind.PropertyAccessExpression, expression.pos)); + node.expression = expression; + parseExpectedToken(SyntaxKind.DotToken, Diagnostics.super_must_be_followed_by_an_argument_list_or_member_access); + // private names will never work with `super` (`super.#foo`), but that's a semantic error, not syntactic + node.name = parseRightSideOfDot(/*allowIdentifierNames*/ true, /*allowPrivateIdentifiers*/ true); + return finishNode(node); + } + function parseJsxElementOrSelfClosingElementOrFragment(inExpressionContext: boolean): JsxElement | JsxSelfClosingElement | JsxFragment { + const opening = parseJsxOpeningOrSelfClosingElementOrOpeningFragment(inExpressionContext); + let result: JsxElement | JsxSelfClosingElement | JsxFragment; + if (opening.kind === SyntaxKind.JsxOpeningElement) { + const node = (createNode(SyntaxKind.JsxElement, opening.pos)); + node.openingElement = opening; + node.children = parseJsxChildren(node.openingElement); + node.closingElement = parseJsxClosingElement(inExpressionContext); + if (!tagNamesAreEquivalent(node.openingElement.tagName, node.closingElement.tagName)) { + parseErrorAtRange(node.closingElement, Diagnostics.Expected_corresponding_JSX_closing_tag_for_0, getTextOfNodeFromSourceText(sourceText, node.openingElement.tagName)); + } + result = finishNode(node); + } + else if (opening.kind === SyntaxKind.JsxOpeningFragment) { + const node = (createNode(SyntaxKind.JsxFragment, opening.pos)); + node.openingFragment = opening; + node.children = parseJsxChildren(node.openingFragment); + node.closingFragment = parseJsxClosingFragment(inExpressionContext); + result = finishNode(node); } - - function parseJsxAttribute(): JsxAttribute | JsxSpreadAttribute { - if (token() === SyntaxKind.OpenBraceToken) { - return parseJsxSpreadAttribute(); + else { + Debug.assert(opening.kind === SyntaxKind.JsxSelfClosingElement); + // Nothing else to do for self-closing elements + result = opening; + } + // If the user writes the invalid code '
' in an expression context (i.e. not wrapped in + // an enclosing tag), we'll naively try to parse ^ this as a 'less than' operator and the remainder of the tag + // as garbage, which will cause the formatter to badly mangle the JSX. Perform a speculative parse of a JSX + // element if we see a < token so that we can wrap it in a synthetic binary expression so the formatter + // does less damage and we can report a better error. + // Since JSX elements are invalid < operands anyway, this lookahead parse will only occur in error scenarios + // of one sort or another. + if (inExpressionContext && token() === SyntaxKind.LessThanToken) { + const invalidElement = tryParse(() => parseJsxElementOrSelfClosingElementOrFragment(/*inExpressionContext*/ true)); + if (invalidElement) { + parseErrorAtCurrentToken(Diagnostics.JSX_expressions_must_have_one_parent_element); + const badNode = (createNode(SyntaxKind.BinaryExpression, result.pos)); + badNode.end = invalidElement.end; + badNode.left = result; + badNode.right = invalidElement; + badNode.operatorToken = createMissingNode(SyntaxKind.CommaToken, /*reportAtCurrentPosition*/ false); + badNode.operatorToken.pos = badNode.operatorToken.end = badNode.right.pos; + return badNode; } - - scanJsxIdentifier(); - const node = createNode(SyntaxKind.JsxAttribute); - node.name = parseIdentifierName(); - if (token() === SyntaxKind.EqualsToken) { - switch (scanJsxAttributeValue()) { - case SyntaxKind.StringLiteral: - node.initializer = parseLiteralNode(); - break; - default: - node.initializer = parseJsxExpression(/*inExpressionContext*/ true); - break; + } + return result; + } + function parseJsxText(): JsxText { + const node = (createNode(SyntaxKind.JsxText)); + node.text = scanner.getTokenValue(); + node.containsOnlyTriviaWhiteSpaces = currentToken === SyntaxKind.JsxTextAllWhiteSpaces; + currentToken = scanner.scanJsxToken(); + return finishNode(node); + } + function parseJsxChild(openingTag: JsxOpeningElement | JsxOpeningFragment, token: JsxTokenSyntaxKind): JsxChild | undefined { + switch (token) { + case SyntaxKind.EndOfFileToken: + // If we hit EOF, issue the error at the tag that lacks the closing element + // rather than at the end of the file (which is useless) + if (isJsxOpeningFragment(openingTag)) { + parseErrorAtRange(openingTag, Diagnostics.JSX_fragment_has_no_corresponding_closing_tag); } - } - return finishNode(node); + else { + parseErrorAtRange(openingTag.tagName, Diagnostics.JSX_element_0_has_no_corresponding_closing_tag, getTextOfNodeFromSourceText(sourceText, openingTag.tagName)); + } + return undefined; + case SyntaxKind.LessThanSlashToken: + case SyntaxKind.ConflictMarkerTrivia: + return undefined; + case SyntaxKind.JsxText: + case SyntaxKind.JsxTextAllWhiteSpaces: + return parseJsxText(); + case SyntaxKind.OpenBraceToken: + return parseJsxExpression(/*inExpressionContext*/ false); + case SyntaxKind.LessThanToken: + return parseJsxElementOrSelfClosingElementOrFragment(/*inExpressionContext*/ false); + default: + return Debug.assertNever(token); } - - function parseJsxSpreadAttribute(): JsxSpreadAttribute { - const node = createNode(SyntaxKind.JsxSpreadAttribute); - parseExpected(SyntaxKind.OpenBraceToken); - parseExpected(SyntaxKind.DotDotDotToken); - node.expression = parseExpression(); - parseExpected(SyntaxKind.CloseBraceToken); - return finishNode(node); + } + function parseJsxChildren(openingTag: JsxOpeningElement | JsxOpeningFragment): NodeArray { + const list = []; + const listPos = getNodePos(); + const saveParsingContext = parsingContext; + parsingContext |= 1 << ParsingContext.JsxChildren; + while (true) { + const child = parseJsxChild(openingTag, currentToken = scanner.reScanJsxToken()); + if (!child) + break; + list.push(child); } - - function parseJsxClosingElement(inExpressionContext: boolean): JsxClosingElement { - const node = createNode(SyntaxKind.JsxClosingElement); - parseExpected(SyntaxKind.LessThanSlashToken); - node.tagName = parseJsxElementName(); - if (inExpressionContext) { - parseExpected(SyntaxKind.GreaterThanToken); - } - else { - parseExpected(SyntaxKind.GreaterThanToken, /*diagnostic*/ undefined, /*shouldAdvance*/ false); - scanJsxText(); - } + parsingContext = saveParsingContext; + return createNodeArray(list, listPos); + } + function parseJsxAttributes(): JsxAttributes { + const jsxAttributes = (createNode(SyntaxKind.JsxAttributes)); + jsxAttributes.properties = parseList(ParsingContext.JsxAttributes, parseJsxAttribute); + return finishNode(jsxAttributes); + } + function parseJsxOpeningOrSelfClosingElementOrOpeningFragment(inExpressionContext: boolean): JsxOpeningElement | JsxSelfClosingElement | JsxOpeningFragment { + const fullStart = scanner.getStartPos(); + parseExpected(SyntaxKind.LessThanToken); + if (token() === SyntaxKind.GreaterThanToken) { + // See below for explanation of scanJsxText + const node: JsxOpeningFragment = (createNode(SyntaxKind.JsxOpeningFragment, fullStart)); + scanJsxText(); return finishNode(node); } - - function parseJsxClosingFragment(inExpressionContext: boolean): JsxClosingFragment { - const node = createNode(SyntaxKind.JsxClosingFragment); - parseExpected(SyntaxKind.LessThanSlashToken); - if (tokenIsIdentifierOrKeyword(token())) { - parseErrorAtRange(parseJsxElementName(), Diagnostics.Expected_corresponding_closing_tag_for_JSX_fragment); - } + const tagName = parseJsxElementName(); + const typeArguments = tryParseTypeArguments(); + const attributes = parseJsxAttributes(); + let node: JsxOpeningLikeElement; + if (token() === SyntaxKind.GreaterThanToken) { + // Closing tag, so scan the immediately-following text with the JSX scanning instead + // of regular scanning to avoid treating illegal characters (e.g. '#') as immediate + // scanning errors + node = (createNode(SyntaxKind.JsxOpeningElement, fullStart)); + scanJsxText(); + } + else { + parseExpected(SyntaxKind.SlashToken); if (inExpressionContext) { parseExpected(SyntaxKind.GreaterThanToken); } @@ -4728,3773 +4126,3473 @@ namespace ts { parseExpected(SyntaxKind.GreaterThanToken, /*diagnostic*/ undefined, /*shouldAdvance*/ false); scanJsxText(); } - return finishNode(node); - } - - function parseTypeAssertion(): TypeAssertion { - const node = createNode(SyntaxKind.TypeAssertionExpression); - parseExpected(SyntaxKind.LessThanToken); - node.type = parseType(); - parseExpected(SyntaxKind.GreaterThanToken); - node.expression = parseSimpleUnaryExpression(); - return finishNode(node); - } - - function nextTokenIsIdentifierOrKeywordOrOpenBracketOrTemplate() { - nextToken(); - return tokenIsIdentifierOrKeyword(token()) - || token() === SyntaxKind.OpenBracketToken - || isTemplateStartOfTaggedTemplate(); - } - - function isStartOfOptionalPropertyOrElementAccessChain() { - return token() === SyntaxKind.QuestionDotToken - && lookAhead(nextTokenIsIdentifierOrKeywordOrOpenBracketOrTemplate); - } - - function parsePropertyAccessExpressionRest(expression: LeftHandSideExpression, questionDotToken: QuestionDotToken | undefined) { - const propertyAccess = createNode(SyntaxKind.PropertyAccessExpression, expression.pos); - propertyAccess.expression = expression; - propertyAccess.questionDotToken = questionDotToken; - propertyAccess.name = parseRightSideOfDot(/*allowIdentifierNames*/ true, /*allowPrivateIdentifiers*/ true); - if (questionDotToken || expression.flags & NodeFlags.OptionalChain) { - propertyAccess.flags |= NodeFlags.OptionalChain; - if (isPrivateIdentifier(propertyAccess.name)) { - parseErrorAtRange(propertyAccess.name, Diagnostics.An_optional_chain_cannot_contain_private_identifiers); - } - } - return finishNode(propertyAccess); + node = (createNode(SyntaxKind.JsxSelfClosingElement, fullStart)); } - - function parseElementAccessExpressionRest(expression: LeftHandSideExpression, questionDotToken: QuestionDotToken | undefined) { - const indexedAccess = createNode(SyntaxKind.ElementAccessExpression, expression.pos); - indexedAccess.expression = expression; - indexedAccess.questionDotToken = questionDotToken; - - if (token() === SyntaxKind.CloseBracketToken) { - indexedAccess.argumentExpression = createMissingNode(SyntaxKind.Identifier, /*reportAtCurrentPosition*/ true, Diagnostics.An_element_access_expression_should_take_an_argument); - } - else { - const argument = allowInAnd(parseExpression); - if (isStringOrNumericLiteralLike(argument)) { - argument.text = internIdentifier(argument.text); - } - indexedAccess.argumentExpression = argument; - } - - parseExpected(SyntaxKind.CloseBracketToken); - if (questionDotToken || expression.flags & NodeFlags.OptionalChain) { - indexedAccess.flags |= NodeFlags.OptionalChain; - } - return finishNode(indexedAccess); - } - - function parseMemberExpressionRest(expression: LeftHandSideExpression, allowOptionalChain: boolean): MemberExpression { - while (true) { - let questionDotToken: QuestionDotToken | undefined; - let isPropertyAccess = false; - if (allowOptionalChain && isStartOfOptionalPropertyOrElementAccessChain()) { - questionDotToken = parseExpectedToken(SyntaxKind.QuestionDotToken); - isPropertyAccess = tokenIsIdentifierOrKeyword(token()); - } - else { - isPropertyAccess = parseOptional(SyntaxKind.DotToken); - } - - if (isPropertyAccess) { - expression = parsePropertyAccessExpressionRest(expression, questionDotToken); - continue; - } - - if (!questionDotToken && token() === SyntaxKind.ExclamationToken && !scanner.hasPrecedingLineBreak()) { - nextToken(); - const nonNullExpression = createNode(SyntaxKind.NonNullExpression, expression.pos); - nonNullExpression.expression = expression; - expression = finishNode(nonNullExpression); - continue; - } - - // when in the [Decorator] context, we do not parse ElementAccess as it could be part of a ComputedPropertyName - if ((questionDotToken || !inDecoratorContext()) && parseOptional(SyntaxKind.OpenBracketToken)) { - expression = parseElementAccessExpressionRest(expression, questionDotToken); - continue; - } - - if (isTemplateStartOfTaggedTemplate()) { - expression = parseTaggedTemplateRest(expression, questionDotToken, /*typeArguments*/ undefined); - continue; - } - - return expression; - } - } - - function isTemplateStartOfTaggedTemplate() { - return token() === SyntaxKind.NoSubstitutionTemplateLiteral || token() === SyntaxKind.TemplateHead; - } - - function parseTaggedTemplateRest(tag: LeftHandSideExpression, questionDotToken: QuestionDotToken | undefined, typeArguments: NodeArray | undefined) { - const tagExpression = createNode(SyntaxKind.TaggedTemplateExpression, tag.pos); - tagExpression.tag = tag; - tagExpression.questionDotToken = questionDotToken; - tagExpression.typeArguments = typeArguments; - tagExpression.template = token() === SyntaxKind.NoSubstitutionTemplateLiteral - ? (reScanTemplateHeadOrNoSubstitutionTemplate(), parseLiteralNode()) - : parseTemplateExpression(/*isTaggedTemplate*/ true); - if (questionDotToken || tag.flags & NodeFlags.OptionalChain) { - tagExpression.flags |= NodeFlags.OptionalChain; - } - return finishNode(tagExpression); - } - - function parseCallExpressionRest(expression: LeftHandSideExpression): LeftHandSideExpression { - while (true) { - expression = parseMemberExpressionRest(expression, /*allowOptionalChain*/ true); - const questionDotToken = parseOptionalToken(SyntaxKind.QuestionDotToken); - - // handle 'foo<()' - if (token() === SyntaxKind.LessThanToken || token() === SyntaxKind.LessThanLessThanToken) { - // See if this is the start of a generic invocation. If so, consume it and - // keep checking for postfix expressions. Otherwise, it's just a '<' that's - // part of an arithmetic expression. Break out so we consume it higher in the - // stack. - const typeArguments = tryParse(parseTypeArgumentsInExpression); - if (typeArguments) { - if (isTemplateStartOfTaggedTemplate()) { - expression = parseTaggedTemplateRest(expression, questionDotToken, typeArguments); - continue; - } - - const callExpr = createNode(SyntaxKind.CallExpression, expression.pos); - callExpr.expression = expression; - callExpr.questionDotToken = questionDotToken; - callExpr.typeArguments = typeArguments; - callExpr.arguments = parseArgumentList(); - if (questionDotToken || expression.flags & NodeFlags.OptionalChain) { - callExpr.flags |= NodeFlags.OptionalChain; - } - expression = finishNode(callExpr); - continue; - } - } - else if (token() === SyntaxKind.OpenParenToken) { - const callExpr = createNode(SyntaxKind.CallExpression, expression.pos); - callExpr.expression = expression; - callExpr.questionDotToken = questionDotToken; - callExpr.arguments = parseArgumentList(); - if (questionDotToken || expression.flags & NodeFlags.OptionalChain) { - callExpr.flags |= NodeFlags.OptionalChain; - } - expression = finishNode(callExpr); - continue; - } - if (questionDotToken) { - // We failed to parse anything, so report a missing identifier here. - const propertyAccess = createNode(SyntaxKind.PropertyAccessExpression, expression.pos) as PropertyAccessExpression; - propertyAccess.expression = expression; - propertyAccess.questionDotToken = questionDotToken; - propertyAccess.name = createMissingNode(SyntaxKind.Identifier, /*reportAtCurrentPosition*/ false, Diagnostics.Identifier_expected); - propertyAccess.flags |= NodeFlags.OptionalChain; - expression = finishNode(propertyAccess); - } - break; - } - return expression; + node.tagName = tagName; + node.typeArguments = typeArguments; + node.attributes = attributes; + return finishNode(node); + } + function parseJsxElementName(): JsxTagNameExpression { + scanJsxIdentifier(); + // JsxElement can have name in the form of + // propertyAccessExpression + // primaryExpression in the form of an identifier and "this" keyword + // We can't just simply use parseLeftHandSideExpressionOrHigher because then we will start consider class,function etc as a keyword + // We only want to consider "this" as a primaryExpression + let expression: JsxTagNameExpression = token() === SyntaxKind.ThisKeyword ? + parseTokenNode() : parseIdentifierName(); + while (parseOptional(SyntaxKind.DotToken)) { + const propertyAccess: JsxTagNamePropertyAccess = (createNode(SyntaxKind.PropertyAccessExpression, expression.pos)); + propertyAccess.expression = expression; + propertyAccess.name = parseRightSideOfDot(/*allowIdentifierNames*/ true, /*allowPrivateIdentifiers*/ false); + expression = finishNode(propertyAccess); } - - function parseArgumentList() { - parseExpected(SyntaxKind.OpenParenToken); - const result = parseDelimitedList(ParsingContext.ArgumentExpressions, parseArgumentExpression); - parseExpected(SyntaxKind.CloseParenToken); - return result; + return expression; + } + function parseJsxExpression(inExpressionContext: boolean): JsxExpression | undefined { + const node = (createNode(SyntaxKind.JsxExpression)); + if (!parseExpected(SyntaxKind.OpenBraceToken)) { + return undefined; } - - function parseTypeArgumentsInExpression() { - if (reScanLessThanToken() !== SyntaxKind.LessThanToken) { - return undefined; - } - nextToken(); - - const typeArguments = parseDelimitedList(ParsingContext.TypeArguments, parseType); - if (!parseExpected(SyntaxKind.GreaterThanToken)) { - // If it doesn't have the closing `>` then it's definitely not an type argument list. - return undefined; - } - - // If we have a '<', then only parse this as a argument list if the type arguments - // are complete and we have an open paren. if we don't, rewind and return nothing. - return typeArguments && canFollowTypeArgumentsInExpression() - ? typeArguments - : undefined; + if (token() !== SyntaxKind.CloseBraceToken) { + node.dotDotDotToken = parseOptionalToken(SyntaxKind.DotDotDotToken); + // Only an AssignmentExpression is valid here per the JSX spec, + // but we can unambiguously parse a comma sequence and provide + // a better error message in grammar checking. + node.expression = parseExpression(); } - - function canFollowTypeArgumentsInExpression(): boolean { - switch (token()) { - case SyntaxKind.OpenParenToken: // foo( - case SyntaxKind.NoSubstitutionTemplateLiteral: // foo `...` - case SyntaxKind.TemplateHead: // foo `...${100}...` - // these are the only tokens can legally follow a type argument - // list. So we definitely want to treat them as type arg lists. - // falls through - case SyntaxKind.DotToken: // foo. - case SyntaxKind.CloseParenToken: // foo) - case SyntaxKind.CloseBracketToken: // foo] - case SyntaxKind.ColonToken: // foo: - case SyntaxKind.SemicolonToken: // foo; - case SyntaxKind.QuestionToken: // foo? - case SyntaxKind.EqualsEqualsToken: // foo == - case SyntaxKind.EqualsEqualsEqualsToken: // foo === - case SyntaxKind.ExclamationEqualsToken: // foo != - case SyntaxKind.ExclamationEqualsEqualsToken: // foo !== - case SyntaxKind.AmpersandAmpersandToken: // foo && - case SyntaxKind.BarBarToken: // foo || - case SyntaxKind.QuestionQuestionToken: // foo ?? - case SyntaxKind.CaretToken: // foo ^ - case SyntaxKind.AmpersandToken: // foo & - case SyntaxKind.BarToken: // foo | - case SyntaxKind.CloseBraceToken: // foo } - case SyntaxKind.EndOfFileToken: // foo - // these cases can't legally follow a type arg list. However, they're not legal - // expressions either. The user is probably in the middle of a generic type. So - // treat it as such. - return true; - - case SyntaxKind.CommaToken: // foo, - case SyntaxKind.OpenBraceToken: // foo { - // We don't want to treat these as type arguments. Otherwise we'll parse this - // as an invocation expression. Instead, we want to parse out the expression - // in isolation from the type arguments. - // falls through - default: - // Anything else treat as an expression. - return false; + if (inExpressionContext) { + parseExpected(SyntaxKind.CloseBraceToken); + } + else { + if (parseExpected(SyntaxKind.CloseBraceToken, /*message*/ undefined, /*shouldAdvance*/ false)) { + scanJsxText(); } } - - function parsePrimaryExpression(): PrimaryExpression { - switch (token()) { - case SyntaxKind.NumericLiteral: - case SyntaxKind.BigIntLiteral: + return finishNode(node); + } + function parseJsxAttribute(): JsxAttribute | JsxSpreadAttribute { + if (token() === SyntaxKind.OpenBraceToken) { + return parseJsxSpreadAttribute(); + } + scanJsxIdentifier(); + const node = (createNode(SyntaxKind.JsxAttribute)); + node.name = parseIdentifierName(); + if (token() === SyntaxKind.EqualsToken) { + switch (scanJsxAttributeValue()) { case SyntaxKind.StringLiteral: - case SyntaxKind.NoSubstitutionTemplateLiteral: - return parseLiteralNode(); - case SyntaxKind.ThisKeyword: - case SyntaxKind.SuperKeyword: - case SyntaxKind.NullKeyword: - case SyntaxKind.TrueKeyword: - case SyntaxKind.FalseKeyword: - return parseTokenNode(); - case SyntaxKind.OpenParenToken: - return parseParenthesizedExpression(); - case SyntaxKind.OpenBracketToken: - return parseArrayLiteralExpression(); - case SyntaxKind.OpenBraceToken: - return parseObjectLiteralExpression(); - case SyntaxKind.AsyncKeyword: - // Async arrow functions are parsed earlier in parseAssignmentExpressionOrHigher. - // If we encounter `async [no LineTerminator here] function` then this is an async - // function; otherwise, its an identifier. - if (!lookAhead(nextTokenIsFunctionKeywordOnSameLine)) { - break; - } - - return parseFunctionExpression(); - case SyntaxKind.ClassKeyword: - return parseClassExpression(); - case SyntaxKind.FunctionKeyword: - return parseFunctionExpression(); - case SyntaxKind.NewKeyword: - return parseNewExpressionOrNewDotTarget(); - case SyntaxKind.SlashToken: - case SyntaxKind.SlashEqualsToken: - if (reScanSlashToken() === SyntaxKind.RegularExpressionLiteral) { - return parseLiteralNode(); - } + node.initializer = (parseLiteralNode()); + break; + default: + node.initializer = parseJsxExpression(/*inExpressionContext*/ true); break; - case SyntaxKind.TemplateHead: - return parseTemplateExpression(/* isTaggedTemplate */ false); } - - return parseIdentifier(Diagnostics.Expression_expected); } - - function parseParenthesizedExpression(): ParenthesizedExpression { - const node = createNodeWithJSDoc(SyntaxKind.ParenthesizedExpression); - parseExpected(SyntaxKind.OpenParenToken); - node.expression = allowInAnd(parseExpression); - parseExpected(SyntaxKind.CloseParenToken); - return finishNode(node); + return finishNode(node); + } + function parseJsxSpreadAttribute(): JsxSpreadAttribute { + const node = (createNode(SyntaxKind.JsxSpreadAttribute)); + parseExpected(SyntaxKind.OpenBraceToken); + parseExpected(SyntaxKind.DotDotDotToken); + node.expression = parseExpression(); + parseExpected(SyntaxKind.CloseBraceToken); + return finishNode(node); + } + function parseJsxClosingElement(inExpressionContext: boolean): JsxClosingElement { + const node = (createNode(SyntaxKind.JsxClosingElement)); + parseExpected(SyntaxKind.LessThanSlashToken); + node.tagName = parseJsxElementName(); + if (inExpressionContext) { + parseExpected(SyntaxKind.GreaterThanToken); } - - function parseSpreadElement(): Expression { - const node = createNode(SyntaxKind.SpreadElement); - parseExpected(SyntaxKind.DotDotDotToken); - node.expression = parseAssignmentExpressionOrHigher(); - return finishNode(node); + else { + parseExpected(SyntaxKind.GreaterThanToken, /*diagnostic*/ undefined, /*shouldAdvance*/ false); + scanJsxText(); } - - function parseArgumentOrArrayLiteralElement(): Expression { - return token() === SyntaxKind.DotDotDotToken ? parseSpreadElement() : - token() === SyntaxKind.CommaToken ? createNode(SyntaxKind.OmittedExpression) : - parseAssignmentExpressionOrHigher(); + return finishNode(node); + } + function parseJsxClosingFragment(inExpressionContext: boolean): JsxClosingFragment { + const node = (createNode(SyntaxKind.JsxClosingFragment)); + parseExpected(SyntaxKind.LessThanSlashToken); + if (tokenIsIdentifierOrKeyword(token())) { + parseErrorAtRange(parseJsxElementName(), Diagnostics.Expected_corresponding_closing_tag_for_JSX_fragment); } - - function parseArgumentExpression(): Expression { - return doOutsideOfContext(disallowInAndDecoratorContext, parseArgumentOrArrayLiteralElement); + if (inExpressionContext) { + parseExpected(SyntaxKind.GreaterThanToken); } - - function parseArrayLiteralExpression(): ArrayLiteralExpression { - const node = createNode(SyntaxKind.ArrayLiteralExpression); - parseExpected(SyntaxKind.OpenBracketToken); - if (scanner.hasPrecedingLineBreak()) { - node.multiLine = true; - } - node.elements = parseDelimitedList(ParsingContext.ArrayLiteralMembers, parseArgumentOrArrayLiteralElement); - parseExpected(SyntaxKind.CloseBracketToken); - return finishNode(node); + else { + parseExpected(SyntaxKind.GreaterThanToken, /*diagnostic*/ undefined, /*shouldAdvance*/ false); + scanJsxText(); } - - function parseObjectLiteralElement(): ObjectLiteralElementLike { - const node = createNodeWithJSDoc(SyntaxKind.Unknown); - - if (parseOptionalToken(SyntaxKind.DotDotDotToken)) { - node.kind = SyntaxKind.SpreadAssignment; - (node).expression = parseAssignmentExpressionOrHigher(); - return finishNode(node); + return finishNode(node); + } + function parseTypeAssertion(): TypeAssertion { + const node = (createNode(SyntaxKind.TypeAssertionExpression)); + parseExpected(SyntaxKind.LessThanToken); + node.type = parseType(); + parseExpected(SyntaxKind.GreaterThanToken); + node.expression = parseSimpleUnaryExpression(); + return finishNode(node); + } + function nextTokenIsIdentifierOrKeywordOrOpenBracketOrTemplate() { + nextToken(); + return tokenIsIdentifierOrKeyword(token()) + || token() === SyntaxKind.OpenBracketToken + || isTemplateStartOfTaggedTemplate(); + } + function isStartOfOptionalPropertyOrElementAccessChain() { + return token() === SyntaxKind.QuestionDotToken + && lookAhead(nextTokenIsIdentifierOrKeywordOrOpenBracketOrTemplate); + } + function parsePropertyAccessExpressionRest(expression: LeftHandSideExpression, questionDotToken: QuestionDotToken | undefined) { + const propertyAccess = (createNode(SyntaxKind.PropertyAccessExpression, expression.pos)); + propertyAccess.expression = expression; + propertyAccess.questionDotToken = questionDotToken; + propertyAccess.name = parseRightSideOfDot(/*allowIdentifierNames*/ true, /*allowPrivateIdentifiers*/ true); + if (questionDotToken || expression.flags & NodeFlags.OptionalChain) { + propertyAccess.flags |= NodeFlags.OptionalChain; + if (isPrivateIdentifier(propertyAccess.name)) { + parseErrorAtRange(propertyAccess.name, Diagnostics.An_optional_chain_cannot_contain_private_identifiers); + } + } + return finishNode(propertyAccess); + } + function parseElementAccessExpressionRest(expression: LeftHandSideExpression, questionDotToken: QuestionDotToken | undefined) { + const indexedAccess = (createNode(SyntaxKind.ElementAccessExpression, expression.pos)); + indexedAccess.expression = expression; + indexedAccess.questionDotToken = questionDotToken; + if (token() === SyntaxKind.CloseBracketToken) { + indexedAccess.argumentExpression = createMissingNode(SyntaxKind.Identifier, /*reportAtCurrentPosition*/ true, Diagnostics.An_element_access_expression_should_take_an_argument); + } + else { + const argument = allowInAnd(parseExpression); + if (isStringOrNumericLiteralLike(argument)) { + argument.text = internIdentifier(argument.text); } - - node.decorators = parseDecorators(); - node.modifiers = parseModifiers(); - - if (parseContextualModifier(SyntaxKind.GetKeyword)) { - return parseAccessorDeclaration(node, SyntaxKind.GetAccessor); - } - if (parseContextualModifier(SyntaxKind.SetKeyword)) { - return parseAccessorDeclaration(node, SyntaxKind.SetAccessor); - } - - const asteriskToken = parseOptionalToken(SyntaxKind.AsteriskToken); - const tokenIsIdentifier = isIdentifier(); - node.name = parsePropertyName(); - // Disallowing of optional property assignments and definite assignment assertion happens in the grammar checker. - (node).questionToken = parseOptionalToken(SyntaxKind.QuestionToken); - (node).exclamationToken = parseOptionalToken(SyntaxKind.ExclamationToken); - - if (asteriskToken || token() === SyntaxKind.OpenParenToken || token() === SyntaxKind.LessThanToken) { - return parseMethodDeclaration(node, asteriskToken); - } - - // check if it is short-hand property assignment or normal property assignment - // NOTE: if token is EqualsToken it is interpreted as CoverInitializedName production - // CoverInitializedName[Yield] : - // IdentifierReference[?Yield] Initializer[In, ?Yield] - // this is necessary because ObjectLiteral productions are also used to cover grammar for ObjectAssignmentPattern - const isShorthandPropertyAssignment = tokenIsIdentifier && (token() !== SyntaxKind.ColonToken); - if (isShorthandPropertyAssignment) { - node.kind = SyntaxKind.ShorthandPropertyAssignment; - const equalsToken = parseOptionalToken(SyntaxKind.EqualsToken); - if (equalsToken) { - (node).equalsToken = equalsToken; - (node).objectAssignmentInitializer = allowInAnd(parseAssignmentExpressionOrHigher); - } + indexedAccess.argumentExpression = argument; + } + parseExpected(SyntaxKind.CloseBracketToken); + if (questionDotToken || expression.flags & NodeFlags.OptionalChain) { + indexedAccess.flags |= NodeFlags.OptionalChain; + } + return finishNode(indexedAccess); + } + function parseMemberExpressionRest(expression: LeftHandSideExpression, allowOptionalChain: boolean): MemberExpression { + while (true) { + let questionDotToken: QuestionDotToken | undefined; + let isPropertyAccess = false; + if (allowOptionalChain && isStartOfOptionalPropertyOrElementAccessChain()) { + questionDotToken = parseExpectedToken(SyntaxKind.QuestionDotToken); + isPropertyAccess = tokenIsIdentifierOrKeyword(token()); } else { - node.kind = SyntaxKind.PropertyAssignment; - parseExpected(SyntaxKind.ColonToken); - (node).initializer = allowInAnd(parseAssignmentExpressionOrHigher); + isPropertyAccess = parseOptional(SyntaxKind.DotToken); } - return finishNode(node); - } - - function parseObjectLiteralExpression(): ObjectLiteralExpression { - const node = createNode(SyntaxKind.ObjectLiteralExpression); - parseExpected(SyntaxKind.OpenBraceToken); - if (scanner.hasPrecedingLineBreak()) { - node.multiLine = true; + if (isPropertyAccess) { + expression = parsePropertyAccessExpressionRest(expression, questionDotToken); + continue; } - - node.properties = parseDelimitedList(ParsingContext.ObjectLiteralMembers, parseObjectLiteralElement, /*considerSemicolonAsDelimiter*/ true); - parseExpected(SyntaxKind.CloseBraceToken); - return finishNode(node); - } - - function parseFunctionExpression(): FunctionExpression { - // GeneratorExpression: - // function* BindingIdentifier [Yield][opt](FormalParameters[Yield]){ GeneratorBody } - // - // FunctionExpression: - // function BindingIdentifier[opt](FormalParameters){ FunctionBody } - const saveDecoratorContext = inDecoratorContext(); - if (saveDecoratorContext) { - setDecoratorContext(/*val*/ false); - } - - const node = createNodeWithJSDoc(SyntaxKind.FunctionExpression); - node.modifiers = parseModifiers(); - parseExpected(SyntaxKind.FunctionKeyword); - node.asteriskToken = parseOptionalToken(SyntaxKind.AsteriskToken); - - const isGenerator = node.asteriskToken ? SignatureFlags.Yield : SignatureFlags.None; - const isAsync = hasModifierOfKind(node, SyntaxKind.AsyncKeyword) ? SignatureFlags.Await : SignatureFlags.None; - node.name = - isGenerator && isAsync ? doInYieldAndAwaitContext(parseOptionalIdentifier) : - isGenerator ? doInYieldContext(parseOptionalIdentifier) : - isAsync ? doInAwaitContext(parseOptionalIdentifier) : - parseOptionalIdentifier(); - - fillSignature(SyntaxKind.ColonToken, isGenerator | isAsync, node); - node.body = parseFunctionBlock(isGenerator | isAsync); - - if (saveDecoratorContext) { - setDecoratorContext(/*val*/ true); - } - - return finishNode(node); - } - - function parseOptionalIdentifier(): Identifier | undefined { - return isIdentifier() ? parseIdentifier() : undefined; - } - - function parseNewExpressionOrNewDotTarget(): NewExpression | MetaProperty { - const fullStart = scanner.getStartPos(); - parseExpected(SyntaxKind.NewKeyword); - if (parseOptional(SyntaxKind.DotToken)) { - const node = createNode(SyntaxKind.MetaProperty, fullStart); - node.keywordToken = SyntaxKind.NewKeyword; - node.name = parseIdentifierName(); - return finishNode(node); + if (!questionDotToken && token() === SyntaxKind.ExclamationToken && !scanner.hasPrecedingLineBreak()) { + nextToken(); + const nonNullExpression = (createNode(SyntaxKind.NonNullExpression, expression.pos)); + nonNullExpression.expression = expression; + expression = finishNode(nonNullExpression); + continue; } - - let expression: MemberExpression = parsePrimaryExpression(); - let typeArguments; - while (true) { - expression = parseMemberExpressionRest(expression, /*allowOptionalChain*/ false); - typeArguments = tryParse(parseTypeArgumentsInExpression); - if (isTemplateStartOfTaggedTemplate()) { - Debug.assert(!!typeArguments, - "Expected a type argument list; all plain tagged template starts should be consumed in 'parseMemberExpressionRest'"); - expression = parseTaggedTemplateRest(expression, /*optionalChain*/ undefined, typeArguments); - typeArguments = undefined; - } - break; + // when in the [Decorator] context, we do not parse ElementAccess as it could be part of a ComputedPropertyName + if ((questionDotToken || !inDecoratorContext()) && parseOptional(SyntaxKind.OpenBracketToken)) { + expression = parseElementAccessExpressionRest(expression, questionDotToken); + continue; } - - const node = createNode(SyntaxKind.NewExpression, fullStart); - node.expression = expression; - node.typeArguments = typeArguments; - if (node.typeArguments || token() === SyntaxKind.OpenParenToken) { - node.arguments = parseArgumentList(); + if (isTemplateStartOfTaggedTemplate()) { + expression = parseTaggedTemplateRest(expression, questionDotToken, /*typeArguments*/ undefined); + continue; } - return finishNode(node); + return expression; } - - // STATEMENTS - function parseBlock(ignoreMissingOpenBrace: boolean, diagnosticMessage?: DiagnosticMessage): Block { - const node = createNode(SyntaxKind.Block); - const openBracePosition = scanner.getTokenPos(); - if (parseExpected(SyntaxKind.OpenBraceToken, diagnosticMessage) || ignoreMissingOpenBrace) { - if (scanner.hasPrecedingLineBreak()) { - node.multiLine = true; - } - - node.statements = parseList(ParsingContext.BlockStatements, parseStatement); - if (!parseExpected(SyntaxKind.CloseBraceToken)) { - const lastError = lastOrUndefined(parseDiagnostics); - if (lastError && lastError.code === Diagnostics._0_expected.code) { - addRelatedInfo( - lastError, - createFileDiagnostic(sourceFile, openBracePosition, 1, Diagnostics.The_parser_expected_to_find_a_to_match_the_token_here) - ); + } + function isTemplateStartOfTaggedTemplate() { + return token() === SyntaxKind.NoSubstitutionTemplateLiteral || token() === SyntaxKind.TemplateHead; + } + function parseTaggedTemplateRest(tag: LeftHandSideExpression, questionDotToken: QuestionDotToken | undefined, typeArguments: NodeArray | undefined) { + const tagExpression = (createNode(SyntaxKind.TaggedTemplateExpression, tag.pos)); + tagExpression.tag = tag; + tagExpression.questionDotToken = questionDotToken; + tagExpression.typeArguments = typeArguments; + tagExpression.template = token() === SyntaxKind.NoSubstitutionTemplateLiteral + ? (reScanTemplateHeadOrNoSubstitutionTemplate(), (parseLiteralNode())) + : parseTemplateExpression(/*isTaggedTemplate*/ true); + if (questionDotToken || tag.flags & NodeFlags.OptionalChain) { + tagExpression.flags |= NodeFlags.OptionalChain; + } + return finishNode(tagExpression); + } + function parseCallExpressionRest(expression: LeftHandSideExpression): LeftHandSideExpression { + while (true) { + expression = parseMemberExpressionRest(expression, /*allowOptionalChain*/ true); + const questionDotToken = parseOptionalToken(SyntaxKind.QuestionDotToken); + // handle 'foo<()' + if (token() === SyntaxKind.LessThanToken || token() === SyntaxKind.LessThanLessThanToken) { + // See if this is the start of a generic invocation. If so, consume it and + // keep checking for postfix expressions. Otherwise, it's just a '<' that's + // part of an arithmetic expression. Break out so we consume it higher in the + // stack. + const typeArguments = tryParse(parseTypeArgumentsInExpression); + if (typeArguments) { + if (isTemplateStartOfTaggedTemplate()) { + expression = parseTaggedTemplateRest(expression, questionDotToken, typeArguments); + continue; + } + const callExpr = (createNode(SyntaxKind.CallExpression, expression.pos)); + callExpr.expression = expression; + callExpr.questionDotToken = questionDotToken; + callExpr.typeArguments = typeArguments; + callExpr.arguments = parseArgumentList(); + if (questionDotToken || expression.flags & NodeFlags.OptionalChain) { + callExpr.flags |= NodeFlags.OptionalChain; } + expression = finishNode(callExpr); + continue; } } - else { - node.statements = createMissingList(); + else if (token() === SyntaxKind.OpenParenToken) { + const callExpr = (createNode(SyntaxKind.CallExpression, expression.pos)); + callExpr.expression = expression; + callExpr.questionDotToken = questionDotToken; + callExpr.arguments = parseArgumentList(); + if (questionDotToken || expression.flags & NodeFlags.OptionalChain) { + callExpr.flags |= NodeFlags.OptionalChain; + } + expression = finishNode(callExpr); + continue; } - return finishNode(node); - } - - function parseFunctionBlock(flags: SignatureFlags, diagnosticMessage?: DiagnosticMessage): Block { - const savedYieldContext = inYieldContext(); - setYieldContext(!!(flags & SignatureFlags.Yield)); - - const savedAwaitContext = inAwaitContext(); - setAwaitContext(!!(flags & SignatureFlags.Await)); - - // We may be in a [Decorator] context when parsing a function expression or - // arrow function. The body of the function is not in [Decorator] context. - const saveDecoratorContext = inDecoratorContext(); - if (saveDecoratorContext) { - setDecoratorContext(/*val*/ false); - } - - const block = parseBlock(!!(flags & SignatureFlags.IgnoreMissingOpenBrace), diagnosticMessage); - - if (saveDecoratorContext) { - setDecoratorContext(/*val*/ true); - } - - setYieldContext(savedYieldContext); - setAwaitContext(savedAwaitContext); - - return block; - } - - function parseEmptyStatement(): Statement { - const node = createNode(SyntaxKind.EmptyStatement); - parseExpected(SyntaxKind.SemicolonToken); - return finishNode(node); + if (questionDotToken) { + // We failed to parse anything, so report a missing identifier here. + const propertyAccess = (createNode(SyntaxKind.PropertyAccessExpression, expression.pos) as PropertyAccessExpression); + propertyAccess.expression = expression; + propertyAccess.questionDotToken = questionDotToken; + propertyAccess.name = createMissingNode(SyntaxKind.Identifier, /*reportAtCurrentPosition*/ false, Diagnostics.Identifier_expected); + propertyAccess.flags |= NodeFlags.OptionalChain; + expression = finishNode(propertyAccess); + } + break; } - - function parseIfStatement(): IfStatement { - const node = createNode(SyntaxKind.IfStatement); - parseExpected(SyntaxKind.IfKeyword); - parseExpected(SyntaxKind.OpenParenToken); - node.expression = allowInAnd(parseExpression); - parseExpected(SyntaxKind.CloseParenToken); - node.thenStatement = parseStatement(); - node.elseStatement = parseOptional(SyntaxKind.ElseKeyword) ? parseStatement() : undefined; - return finishNode(node); + return expression; + } + function parseArgumentList() { + parseExpected(SyntaxKind.OpenParenToken); + const result = parseDelimitedList(ParsingContext.ArgumentExpressions, parseArgumentExpression); + parseExpected(SyntaxKind.CloseParenToken); + return result; + } + function parseTypeArgumentsInExpression() { + if (reScanLessThanToken() !== SyntaxKind.LessThanToken) { + return undefined; } - - function parseDoStatement(): DoStatement { - const node = createNode(SyntaxKind.DoStatement); - parseExpected(SyntaxKind.DoKeyword); - node.statement = parseStatement(); - parseExpected(SyntaxKind.WhileKeyword); - parseExpected(SyntaxKind.OpenParenToken); - node.expression = allowInAnd(parseExpression); - parseExpected(SyntaxKind.CloseParenToken); - - // From: https://mail.mozilla.org/pipermail/es-discuss/2011-August/016188.html - // 157 min --- All allen at wirfs-brock.com CONF --- "do{;}while(false)false" prohibited in - // spec but allowed in consensus reality. Approved -- this is the de-facto standard whereby - // do;while(0)x will have a semicolon inserted before x. - parseOptional(SyntaxKind.SemicolonToken); - return finishNode(node); + nextToken(); + const typeArguments = parseDelimitedList(ParsingContext.TypeArguments, parseType); + if (!parseExpected(SyntaxKind.GreaterThanToken)) { + // If it doesn't have the closing `>` then it's definitely not an type argument list. + return undefined; } - - function parseWhileStatement(): WhileStatement { - const node = createNode(SyntaxKind.WhileStatement); - parseExpected(SyntaxKind.WhileKeyword); - parseExpected(SyntaxKind.OpenParenToken); - node.expression = allowInAnd(parseExpression); - parseExpected(SyntaxKind.CloseParenToken); - node.statement = parseStatement(); - return finishNode(node); + // If we have a '<', then only parse this as a argument list if the type arguments + // are complete and we have an open paren. if we don't, rewind and return nothing. + return typeArguments && canFollowTypeArgumentsInExpression() + ? typeArguments + : undefined; + } + function canFollowTypeArgumentsInExpression(): boolean { + switch (token()) { + case SyntaxKind.OpenParenToken: // foo( + case SyntaxKind.NoSubstitutionTemplateLiteral: // foo `...` + case SyntaxKind.TemplateHead: // foo `...${100}...` + // these are the only tokens can legally follow a type argument + // list. So we definitely want to treat them as type arg lists. + // falls through + case SyntaxKind.DotToken: // foo. + case SyntaxKind.CloseParenToken: // foo) + case SyntaxKind.CloseBracketToken: // foo] + case SyntaxKind.ColonToken: // foo: + case SyntaxKind.SemicolonToken: // foo; + case SyntaxKind.QuestionToken: // foo? + case SyntaxKind.EqualsEqualsToken: // foo == + case SyntaxKind.EqualsEqualsEqualsToken: // foo === + case SyntaxKind.ExclamationEqualsToken: // foo != + case SyntaxKind.ExclamationEqualsEqualsToken: // foo !== + case SyntaxKind.AmpersandAmpersandToken: // foo && + case SyntaxKind.BarBarToken: // foo || + case SyntaxKind.QuestionQuestionToken: // foo ?? + case SyntaxKind.CaretToken: // foo ^ + case SyntaxKind.AmpersandToken: // foo & + case SyntaxKind.BarToken: // foo | + case SyntaxKind.CloseBraceToken: // foo } + case SyntaxKind.EndOfFileToken: // foo + // these cases can't legally follow a type arg list. However, they're not legal + // expressions either. The user is probably in the middle of a generic type. So + // treat it as such. + return true; + case SyntaxKind.CommaToken: // foo, + case SyntaxKind.OpenBraceToken: // foo { + // We don't want to treat these as type arguments. Otherwise we'll parse this + // as an invocation expression. Instead, we want to parse out the expression + // in isolation from the type arguments. + // falls through + default: + // Anything else treat as an expression. + return false; } - - function parseForOrForInOrForOfStatement(): Statement { - const pos = getNodePos(); - parseExpected(SyntaxKind.ForKeyword); - const awaitToken = parseOptionalToken(SyntaxKind.AwaitKeyword); - parseExpected(SyntaxKind.OpenParenToken); - - let initializer!: VariableDeclarationList | Expression; - if (token() !== SyntaxKind.SemicolonToken) { - if (token() === SyntaxKind.VarKeyword || token() === SyntaxKind.LetKeyword || token() === SyntaxKind.ConstKeyword) { - initializer = parseVariableDeclarationList(/*inForStatementInitializer*/ true); - } - else { - initializer = disallowInAnd(parseExpression); - } - } - let forOrForInOrForOfStatement: IterationStatement; - if (awaitToken ? parseExpected(SyntaxKind.OfKeyword) : parseOptional(SyntaxKind.OfKeyword)) { - const forOfStatement = createNode(SyntaxKind.ForOfStatement, pos); - forOfStatement.awaitModifier = awaitToken; - forOfStatement.initializer = initializer; - forOfStatement.expression = allowInAnd(parseAssignmentExpressionOrHigher); - parseExpected(SyntaxKind.CloseParenToken); - forOrForInOrForOfStatement = forOfStatement; - } - else if (parseOptional(SyntaxKind.InKeyword)) { - const forInStatement = createNode(SyntaxKind.ForInStatement, pos); - forInStatement.initializer = initializer; - forInStatement.expression = allowInAnd(parseExpression); - parseExpected(SyntaxKind.CloseParenToken); - forOrForInOrForOfStatement = forInStatement; - } - else { - const forStatement = createNode(SyntaxKind.ForStatement, pos); - forStatement.initializer = initializer; - parseExpected(SyntaxKind.SemicolonToken); - if (token() !== SyntaxKind.SemicolonToken && token() !== SyntaxKind.CloseParenToken) { - forStatement.condition = allowInAnd(parseExpression); + } + function parsePrimaryExpression(): PrimaryExpression { + switch (token()) { + case SyntaxKind.NumericLiteral: + case SyntaxKind.BigIntLiteral: + case SyntaxKind.StringLiteral: + case SyntaxKind.NoSubstitutionTemplateLiteral: + return parseLiteralNode(); + case SyntaxKind.ThisKeyword: + case SyntaxKind.SuperKeyword: + case SyntaxKind.NullKeyword: + case SyntaxKind.TrueKeyword: + case SyntaxKind.FalseKeyword: + return parseTokenNode(); + case SyntaxKind.OpenParenToken: + return parseParenthesizedExpression(); + case SyntaxKind.OpenBracketToken: + return parseArrayLiteralExpression(); + case SyntaxKind.OpenBraceToken: + return parseObjectLiteralExpression(); + case SyntaxKind.AsyncKeyword: + // Async arrow functions are parsed earlier in parseAssignmentExpressionOrHigher. + // If we encounter `async [no LineTerminator here] function` then this is an async + // function; otherwise, its an identifier. + if (!lookAhead(nextTokenIsFunctionKeywordOnSameLine)) { + break; } - parseExpected(SyntaxKind.SemicolonToken); - if (token() !== SyntaxKind.CloseParenToken) { - forStatement.incrementor = allowInAnd(parseExpression); + return parseFunctionExpression(); + case SyntaxKind.ClassKeyword: + return parseClassExpression(); + case SyntaxKind.FunctionKeyword: + return parseFunctionExpression(); + case SyntaxKind.NewKeyword: + return parseNewExpressionOrNewDotTarget(); + case SyntaxKind.SlashToken: + case SyntaxKind.SlashEqualsToken: + if (reScanSlashToken() === SyntaxKind.RegularExpressionLiteral) { + return parseLiteralNode(); } - parseExpected(SyntaxKind.CloseParenToken); - forOrForInOrForOfStatement = forStatement; - } - - forOrForInOrForOfStatement.statement = parseStatement(); - - return finishNode(forOrForInOrForOfStatement); - } - - function parseBreakOrContinueStatement(kind: SyntaxKind): BreakOrContinueStatement { - const node = createNode(kind); - - parseExpected(kind === SyntaxKind.BreakStatement ? SyntaxKind.BreakKeyword : SyntaxKind.ContinueKeyword); - if (!canParseSemicolon()) { - node.label = parseIdentifier(); - } - - parseSemicolon(); - return finishNode(node); - } - - function parseReturnStatement(): ReturnStatement { - const node = createNode(SyntaxKind.ReturnStatement); - - parseExpected(SyntaxKind.ReturnKeyword); - if (!canParseSemicolon()) { - node.expression = allowInAnd(parseExpression); - } - - parseSemicolon(); - return finishNode(node); + break; + case SyntaxKind.TemplateHead: + return parseTemplateExpression(/* isTaggedTemplate */ false); } - - function parseWithStatement(): WithStatement { - const node = createNode(SyntaxKind.WithStatement); - parseExpected(SyntaxKind.WithKeyword); - parseExpected(SyntaxKind.OpenParenToken); - node.expression = allowInAnd(parseExpression); - parseExpected(SyntaxKind.CloseParenToken); - node.statement = doInsideOfContext(NodeFlags.InWithStatement, parseStatement); + return parseIdentifier(Diagnostics.Expression_expected); + } + function parseParenthesizedExpression(): ParenthesizedExpression { + const node = (createNodeWithJSDoc(SyntaxKind.ParenthesizedExpression)); + parseExpected(SyntaxKind.OpenParenToken); + node.expression = allowInAnd(parseExpression); + parseExpected(SyntaxKind.CloseParenToken); + return finishNode(node); + } + function parseSpreadElement(): Expression { + const node = (createNode(SyntaxKind.SpreadElement)); + parseExpected(SyntaxKind.DotDotDotToken); + node.expression = parseAssignmentExpressionOrHigher(); + return finishNode(node); + } + function parseArgumentOrArrayLiteralElement(): Expression { + return token() === SyntaxKind.DotDotDotToken ? parseSpreadElement() : + token() === SyntaxKind.CommaToken ? createNode(SyntaxKind.OmittedExpression) : + parseAssignmentExpressionOrHigher(); + } + function parseArgumentExpression(): Expression { + return doOutsideOfContext(disallowInAndDecoratorContext, parseArgumentOrArrayLiteralElement); + } + function parseArrayLiteralExpression(): ArrayLiteralExpression { + const node = (createNode(SyntaxKind.ArrayLiteralExpression)); + parseExpected(SyntaxKind.OpenBracketToken); + if (scanner.hasPrecedingLineBreak()) { + node.multiLine = true; + } + node.elements = parseDelimitedList(ParsingContext.ArrayLiteralMembers, parseArgumentOrArrayLiteralElement); + parseExpected(SyntaxKind.CloseBracketToken); + return finishNode(node); + } + function parseObjectLiteralElement(): ObjectLiteralElementLike { + const node = (createNodeWithJSDoc(SyntaxKind.Unknown)); + if (parseOptionalToken(SyntaxKind.DotDotDotToken)) { + node.kind = SyntaxKind.SpreadAssignment; + (node).expression = parseAssignmentExpressionOrHigher(); return finishNode(node); } - - function parseCaseClause(): CaseClause { - const node = createNode(SyntaxKind.CaseClause); - parseExpected(SyntaxKind.CaseKeyword); - node.expression = allowInAnd(parseExpression); - parseExpected(SyntaxKind.ColonToken); - node.statements = parseList(ParsingContext.SwitchClauseStatements, parseStatement); - return finishNode(node); + node.decorators = parseDecorators(); + node.modifiers = parseModifiers(); + if (parseContextualModifier(SyntaxKind.GetKeyword)) { + return parseAccessorDeclaration((node), SyntaxKind.GetAccessor); + } + if (parseContextualModifier(SyntaxKind.SetKeyword)) { + return parseAccessorDeclaration((node), SyntaxKind.SetAccessor); + } + const asteriskToken = parseOptionalToken(SyntaxKind.AsteriskToken); + const tokenIsIdentifier = isIdentifier(); + node.name = parsePropertyName(); + // Disallowing of optional property assignments and definite assignment assertion happens in the grammar checker. + (node).questionToken = parseOptionalToken(SyntaxKind.QuestionToken); + (node).exclamationToken = parseOptionalToken(SyntaxKind.ExclamationToken); + if (asteriskToken || token() === SyntaxKind.OpenParenToken || token() === SyntaxKind.LessThanToken) { + return parseMethodDeclaration((node), asteriskToken); + } + // check if it is short-hand property assignment or normal property assignment + // NOTE: if token is EqualsToken it is interpreted as CoverInitializedName production + // CoverInitializedName[Yield] : + // IdentifierReference[?Yield] Initializer[In, ?Yield] + // this is necessary because ObjectLiteral productions are also used to cover grammar for ObjectAssignmentPattern + const isShorthandPropertyAssignment = tokenIsIdentifier && (token() !== SyntaxKind.ColonToken); + if (isShorthandPropertyAssignment) { + node.kind = SyntaxKind.ShorthandPropertyAssignment; + const equalsToken = parseOptionalToken(SyntaxKind.EqualsToken); + if (equalsToken) { + (node).equalsToken = equalsToken; + (node).objectAssignmentInitializer = allowInAnd(parseAssignmentExpressionOrHigher); + } } - - function parseDefaultClause(): DefaultClause { - const node = createNode(SyntaxKind.DefaultClause); - parseExpected(SyntaxKind.DefaultKeyword); + else { + node.kind = SyntaxKind.PropertyAssignment; parseExpected(SyntaxKind.ColonToken); - node.statements = parseList(ParsingContext.SwitchClauseStatements, parseStatement); - return finishNode(node); - } - - function parseCaseOrDefaultClause(): CaseOrDefaultClause { - return token() === SyntaxKind.CaseKeyword ? parseCaseClause() : parseDefaultClause(); - } - - function parseSwitchStatement(): SwitchStatement { - const node = createNode(SyntaxKind.SwitchStatement); - parseExpected(SyntaxKind.SwitchKeyword); - parseExpected(SyntaxKind.OpenParenToken); - node.expression = allowInAnd(parseExpression); - parseExpected(SyntaxKind.CloseParenToken); - const caseBlock = createNode(SyntaxKind.CaseBlock); - parseExpected(SyntaxKind.OpenBraceToken); - caseBlock.clauses = parseList(ParsingContext.SwitchClauses, parseCaseOrDefaultClause); - parseExpected(SyntaxKind.CloseBraceToken); - node.caseBlock = finishNode(caseBlock); - return finishNode(node); - } - - function parseThrowStatement(): ThrowStatement { - // ThrowStatement[Yield] : - // throw [no LineTerminator here]Expression[In, ?Yield]; - - // Because of automatic semicolon insertion, we need to report error if this - // throw could be terminated with a semicolon. Note: we can't call 'parseExpression' - // directly as that might consume an expression on the following line. - // We just return 'undefined' in that case. The actual error will be reported in the - // grammar walker. - const node = createNode(SyntaxKind.ThrowStatement); - parseExpected(SyntaxKind.ThrowKeyword); - node.expression = scanner.hasPrecedingLineBreak() ? undefined : allowInAnd(parseExpression); - parseSemicolon(); - return finishNode(node); + (node).initializer = allowInAnd(parseAssignmentExpressionOrHigher); } - - // TODO: Review for error recovery - function parseTryStatement(): TryStatement { - const node = createNode(SyntaxKind.TryStatement); - - parseExpected(SyntaxKind.TryKeyword); - node.tryBlock = parseBlock(/*ignoreMissingOpenBrace*/ false); - node.catchClause = token() === SyntaxKind.CatchKeyword ? parseCatchClause() : undefined; - - // If we don't have a catch clause, then we must have a finally clause. Try to parse - // one out no matter what. - if (!node.catchClause || token() === SyntaxKind.FinallyKeyword) { - parseExpected(SyntaxKind.FinallyKeyword); - node.finallyBlock = parseBlock(/*ignoreMissingOpenBrace*/ false); - } - + return finishNode(node); + } + function parseObjectLiteralExpression(): ObjectLiteralExpression { + const node = (createNode(SyntaxKind.ObjectLiteralExpression)); + parseExpected(SyntaxKind.OpenBraceToken); + if (scanner.hasPrecedingLineBreak()) { + node.multiLine = true; + } + node.properties = parseDelimitedList(ParsingContext.ObjectLiteralMembers, parseObjectLiteralElement, /*considerSemicolonAsDelimiter*/ true); + parseExpected(SyntaxKind.CloseBraceToken); + return finishNode(node); + } + function parseFunctionExpression(): FunctionExpression { + // GeneratorExpression: + // function* BindingIdentifier [Yield][opt](FormalParameters[Yield]){ GeneratorBody } + // + // FunctionExpression: + // function BindingIdentifier[opt](FormalParameters){ FunctionBody } + const saveDecoratorContext = inDecoratorContext(); + if (saveDecoratorContext) { + setDecoratorContext(/*val*/ false); + } + const node = (createNodeWithJSDoc(SyntaxKind.FunctionExpression)); + node.modifiers = parseModifiers(); + parseExpected(SyntaxKind.FunctionKeyword); + node.asteriskToken = parseOptionalToken(SyntaxKind.AsteriskToken); + const isGenerator = node.asteriskToken ? SignatureFlags.Yield : SignatureFlags.None; + const isAsync = hasModifierOfKind(node, SyntaxKind.AsyncKeyword) ? SignatureFlags.Await : SignatureFlags.None; + node.name = + isGenerator && isAsync ? doInYieldAndAwaitContext(parseOptionalIdentifier) : + isGenerator ? doInYieldContext(parseOptionalIdentifier) : + isAsync ? doInAwaitContext(parseOptionalIdentifier) : + parseOptionalIdentifier(); + fillSignature(SyntaxKind.ColonToken, isGenerator | isAsync, node); + node.body = parseFunctionBlock(isGenerator | isAsync); + if (saveDecoratorContext) { + setDecoratorContext(/*val*/ true); + } + return finishNode(node); + } + function parseOptionalIdentifier(): Identifier | undefined { + return isIdentifier() ? parseIdentifier() : undefined; + } + function parseNewExpressionOrNewDotTarget(): NewExpression | MetaProperty { + const fullStart = scanner.getStartPos(); + parseExpected(SyntaxKind.NewKeyword); + if (parseOptional(SyntaxKind.DotToken)) { + const node = (createNode(SyntaxKind.MetaProperty, fullStart)); + node.keywordToken = SyntaxKind.NewKeyword; + node.name = parseIdentifierName(); return finishNode(node); } - - function parseCatchClause(): CatchClause { - const result = createNode(SyntaxKind.CatchClause); - parseExpected(SyntaxKind.CatchKeyword); - - if (parseOptional(SyntaxKind.OpenParenToken)) { - result.variableDeclaration = parseVariableDeclaration(); - parseExpected(SyntaxKind.CloseParenToken); + let expression: MemberExpression = parsePrimaryExpression(); + let typeArguments; + while (true) { + expression = parseMemberExpressionRest(expression, /*allowOptionalChain*/ false); + typeArguments = tryParse(parseTypeArgumentsInExpression); + if (isTemplateStartOfTaggedTemplate()) { + Debug.assert(!!typeArguments, "Expected a type argument list; all plain tagged template starts should be consumed in 'parseMemberExpressionRest'"); + expression = parseTaggedTemplateRest(expression, /*optionalChain*/ undefined, typeArguments); + typeArguments = undefined; + } + break; + } + const node = (createNode(SyntaxKind.NewExpression, fullStart)); + node.expression = expression; + node.typeArguments = typeArguments; + if (node.typeArguments || token() === SyntaxKind.OpenParenToken) { + node.arguments = parseArgumentList(); + } + return finishNode(node); + } + // STATEMENTS + function parseBlock(ignoreMissingOpenBrace: boolean, diagnosticMessage?: DiagnosticMessage): Block { + const node = (createNode(SyntaxKind.Block)); + const openBracePosition = scanner.getTokenPos(); + if (parseExpected(SyntaxKind.OpenBraceToken, diagnosticMessage) || ignoreMissingOpenBrace) { + if (scanner.hasPrecedingLineBreak()) { + node.multiLine = true; } - else { - // Keep shape of node to avoid degrading performance. - result.variableDeclaration = undefined; + node.statements = parseList(ParsingContext.BlockStatements, parseStatement); + if (!parseExpected(SyntaxKind.CloseBraceToken)) { + const lastError = lastOrUndefined(parseDiagnostics); + if (lastError && lastError.code === Diagnostics._0_expected.code) { + addRelatedInfo(lastError, createFileDiagnostic(sourceFile, openBracePosition, 1, Diagnostics.The_parser_expected_to_find_a_to_match_the_token_here)); + } } - - result.block = parseBlock(/*ignoreMissingOpenBrace*/ false); - return finishNode(result); } - - function parseDebuggerStatement(): Statement { - const node = createNode(SyntaxKind.DebuggerStatement); - parseExpected(SyntaxKind.DebuggerKeyword); - parseSemicolon(); - return finishNode(node); + else { + node.statements = createMissingList(); } - - function parseExpressionOrLabeledStatement(): ExpressionStatement | LabeledStatement { - // Avoiding having to do the lookahead for a labeled statement by just trying to parse - // out an expression, seeing if it is identifier and then seeing if it is followed by - // a colon. - const node = createNodeWithJSDoc(SyntaxKind.Unknown); - const expression = allowInAnd(parseExpression); - if (expression.kind === SyntaxKind.Identifier && parseOptional(SyntaxKind.ColonToken)) { - node.kind = SyntaxKind.LabeledStatement; - (node).label = expression; - (node).statement = parseStatement(); + return finishNode(node); + } + function parseFunctionBlock(flags: SignatureFlags, diagnosticMessage?: DiagnosticMessage): Block { + const savedYieldContext = inYieldContext(); + setYieldContext(!!(flags & SignatureFlags.Yield)); + const savedAwaitContext = inAwaitContext(); + setAwaitContext(!!(flags & SignatureFlags.Await)); + // We may be in a [Decorator] context when parsing a function expression or + // arrow function. The body of the function is not in [Decorator] context. + const saveDecoratorContext = inDecoratorContext(); + if (saveDecoratorContext) { + setDecoratorContext(/*val*/ false); + } + const block = parseBlock(!!(flags & SignatureFlags.IgnoreMissingOpenBrace), diagnosticMessage); + if (saveDecoratorContext) { + setDecoratorContext(/*val*/ true); + } + setYieldContext(savedYieldContext); + setAwaitContext(savedAwaitContext); + return block; + } + function parseEmptyStatement(): Statement { + const node = (createNode(SyntaxKind.EmptyStatement)); + parseExpected(SyntaxKind.SemicolonToken); + return finishNode(node); + } + function parseIfStatement(): IfStatement { + const node = (createNode(SyntaxKind.IfStatement)); + parseExpected(SyntaxKind.IfKeyword); + parseExpected(SyntaxKind.OpenParenToken); + node.expression = allowInAnd(parseExpression); + parseExpected(SyntaxKind.CloseParenToken); + node.thenStatement = parseStatement(); + node.elseStatement = parseOptional(SyntaxKind.ElseKeyword) ? parseStatement() : undefined; + return finishNode(node); + } + function parseDoStatement(): DoStatement { + const node = (createNode(SyntaxKind.DoStatement)); + parseExpected(SyntaxKind.DoKeyword); + node.statement = parseStatement(); + parseExpected(SyntaxKind.WhileKeyword); + parseExpected(SyntaxKind.OpenParenToken); + node.expression = allowInAnd(parseExpression); + parseExpected(SyntaxKind.CloseParenToken); + // From: https://mail.mozilla.org/pipermail/es-discuss/2011-August/016188.html + // 157 min --- All allen at wirfs-brock.com CONF --- "do{;}while(false)false" prohibited in + // spec but allowed in consensus reality. Approved -- this is the de-facto standard whereby + // do;while(0)x will have a semicolon inserted before x. + parseOptional(SyntaxKind.SemicolonToken); + return finishNode(node); + } + function parseWhileStatement(): WhileStatement { + const node = (createNode(SyntaxKind.WhileStatement)); + parseExpected(SyntaxKind.WhileKeyword); + parseExpected(SyntaxKind.OpenParenToken); + node.expression = allowInAnd(parseExpression); + parseExpected(SyntaxKind.CloseParenToken); + node.statement = parseStatement(); + return finishNode(node); + } + function parseForOrForInOrForOfStatement(): Statement { + const pos = getNodePos(); + parseExpected(SyntaxKind.ForKeyword); + const awaitToken = parseOptionalToken(SyntaxKind.AwaitKeyword); + parseExpected(SyntaxKind.OpenParenToken); + let initializer!: VariableDeclarationList | Expression; + if (token() !== SyntaxKind.SemicolonToken) { + if (token() === SyntaxKind.VarKeyword || token() === SyntaxKind.LetKeyword || token() === SyntaxKind.ConstKeyword) { + initializer = parseVariableDeclarationList(/*inForStatementInitializer*/ true); } else { - node.kind = SyntaxKind.ExpressionStatement; - (node).expression = expression; - parseSemicolon(); + initializer = disallowInAnd(parseExpression); } - return finishNode(node); - } - - function nextTokenIsIdentifierOrKeywordOnSameLine() { - nextToken(); - return tokenIsIdentifierOrKeyword(token()) && !scanner.hasPrecedingLineBreak(); - } - - function nextTokenIsClassKeywordOnSameLine() { - nextToken(); - return token() === SyntaxKind.ClassKeyword && !scanner.hasPrecedingLineBreak(); - } - - function nextTokenIsFunctionKeywordOnSameLine() { - nextToken(); - return token() === SyntaxKind.FunctionKeyword && !scanner.hasPrecedingLineBreak(); - } - - function nextTokenIsIdentifierOrKeywordOrLiteralOnSameLine() { - nextToken(); - return (tokenIsIdentifierOrKeyword(token()) || token() === SyntaxKind.NumericLiteral || token() === SyntaxKind.BigIntLiteral || token() === SyntaxKind.StringLiteral) && !scanner.hasPrecedingLineBreak(); } - - function isDeclaration(): boolean { - while (true) { - switch (token()) { - case SyntaxKind.VarKeyword: - case SyntaxKind.LetKeyword: - case SyntaxKind.ConstKeyword: - case SyntaxKind.FunctionKeyword: - case SyntaxKind.ClassKeyword: - case SyntaxKind.EnumKeyword: - return true; - - // 'declare', 'module', 'namespace', 'interface'* and 'type' are all legal JavaScript identifiers; - // however, an identifier cannot be followed by another identifier on the same line. This is what we - // count on to parse out the respective declarations. For instance, we exploit this to say that - // - // namespace n - // - // can be none other than the beginning of a namespace declaration, but need to respect that JavaScript sees - // - // namespace - // n - // - // as the identifier 'namespace' on one line followed by the identifier 'n' on another. - // We need to look one token ahead to see if it permissible to try parsing a declaration. - // - // *Note*: 'interface' is actually a strict mode reserved word. So while - // - // "use strict" - // interface - // I {} - // - // could be legal, it would add complexity for very little gain. - case SyntaxKind.InterfaceKeyword: - case SyntaxKind.TypeKeyword: - return nextTokenIsIdentifierOnSameLine(); - case SyntaxKind.ModuleKeyword: - case SyntaxKind.NamespaceKeyword: - return nextTokenIsIdentifierOrStringLiteralOnSameLine(); - case SyntaxKind.AbstractKeyword: - case SyntaxKind.AsyncKeyword: - case SyntaxKind.DeclareKeyword: - case SyntaxKind.PrivateKeyword: - case SyntaxKind.ProtectedKeyword: - case SyntaxKind.PublicKeyword: - case SyntaxKind.ReadonlyKeyword: - nextToken(); - // ASI takes effect for this modifier. - if (scanner.hasPrecedingLineBreak()) { - return false; - } - continue; - - case SyntaxKind.GlobalKeyword: - nextToken(); - return token() === SyntaxKind.OpenBraceToken || token() === SyntaxKind.Identifier || token() === SyntaxKind.ExportKeyword; - - case SyntaxKind.ImportKeyword: - nextToken(); - return token() === SyntaxKind.StringLiteral || token() === SyntaxKind.AsteriskToken || - token() === SyntaxKind.OpenBraceToken || tokenIsIdentifierOrKeyword(token()); - case SyntaxKind.ExportKeyword: - let currentToken = nextToken(); - if (currentToken === SyntaxKind.TypeKeyword) { - currentToken = lookAhead(nextToken); - } - if (currentToken === SyntaxKind.EqualsToken || currentToken === SyntaxKind.AsteriskToken || - currentToken === SyntaxKind.OpenBraceToken || currentToken === SyntaxKind.DefaultKeyword || - currentToken === SyntaxKind.AsKeyword) { - return true; - } - continue; - - case SyntaxKind.StaticKeyword: - nextToken(); - continue; - default: - return false; - } - } + let forOrForInOrForOfStatement: IterationStatement; + if (awaitToken ? parseExpected(SyntaxKind.OfKeyword) : parseOptional(SyntaxKind.OfKeyword)) { + const forOfStatement = (createNode(SyntaxKind.ForOfStatement, pos)); + forOfStatement.awaitModifier = awaitToken; + forOfStatement.initializer = initializer; + forOfStatement.expression = allowInAnd(parseAssignmentExpressionOrHigher); + parseExpected(SyntaxKind.CloseParenToken); + forOrForInOrForOfStatement = forOfStatement; } - - function isStartOfDeclaration(): boolean { - return lookAhead(isDeclaration); + else if (parseOptional(SyntaxKind.InKeyword)) { + const forInStatement = (createNode(SyntaxKind.ForInStatement, pos)); + forInStatement.initializer = initializer; + forInStatement.expression = allowInAnd(parseExpression); + parseExpected(SyntaxKind.CloseParenToken); + forOrForInOrForOfStatement = forInStatement; } - - function isStartOfStatement(): boolean { - switch (token()) { - case SyntaxKind.AtToken: - case SyntaxKind.SemicolonToken: - case SyntaxKind.OpenBraceToken: - case SyntaxKind.VarKeyword: - case SyntaxKind.LetKeyword: - case SyntaxKind.FunctionKeyword: - case SyntaxKind.ClassKeyword: - case SyntaxKind.EnumKeyword: - case SyntaxKind.IfKeyword: - case SyntaxKind.DoKeyword: - case SyntaxKind.WhileKeyword: - case SyntaxKind.ForKeyword: - case SyntaxKind.ContinueKeyword: - case SyntaxKind.BreakKeyword: - case SyntaxKind.ReturnKeyword: - case SyntaxKind.WithKeyword: - case SyntaxKind.SwitchKeyword: - case SyntaxKind.ThrowKeyword: - case SyntaxKind.TryKeyword: - case SyntaxKind.DebuggerKeyword: - // 'catch' and 'finally' do not actually indicate that the code is part of a statement, - // however, we say they are here so that we may gracefully parse them and error later. - // falls through - case SyntaxKind.CatchKeyword: - case SyntaxKind.FinallyKeyword: - return true; - - case SyntaxKind.ImportKeyword: - return isStartOfDeclaration() || lookAhead(nextTokenIsOpenParenOrLessThanOrDot); - - case SyntaxKind.ConstKeyword: - case SyntaxKind.ExportKeyword: - return isStartOfDeclaration(); - - case SyntaxKind.AsyncKeyword: - case SyntaxKind.DeclareKeyword: - case SyntaxKind.InterfaceKeyword: - case SyntaxKind.ModuleKeyword: - case SyntaxKind.NamespaceKeyword: - case SyntaxKind.TypeKeyword: - case SyntaxKind.GlobalKeyword: - // When these don't start a declaration, they're an identifier in an expression statement - return true; - - case SyntaxKind.PublicKeyword: - case SyntaxKind.PrivateKeyword: - case SyntaxKind.ProtectedKeyword: - case SyntaxKind.StaticKeyword: - case SyntaxKind.ReadonlyKeyword: - // When these don't start a declaration, they may be the start of a class member if an identifier - // immediately follows. Otherwise they're an identifier in an expression statement. - return isStartOfDeclaration() || !lookAhead(nextTokenIsIdentifierOrKeywordOnSameLine); - - default: - return isStartOfExpression(); + else { + const forStatement = (createNode(SyntaxKind.ForStatement, pos)); + forStatement.initializer = initializer; + parseExpected(SyntaxKind.SemicolonToken); + if (token() !== SyntaxKind.SemicolonToken && token() !== SyntaxKind.CloseParenToken) { + forStatement.condition = allowInAnd(parseExpression); + } + parseExpected(SyntaxKind.SemicolonToken); + if (token() !== SyntaxKind.CloseParenToken) { + forStatement.incrementor = allowInAnd(parseExpression); } + parseExpected(SyntaxKind.CloseParenToken); + forOrForInOrForOfStatement = forStatement; } - - function nextTokenIsIdentifierOrStartOfDestructuring() { - nextToken(); - return isIdentifier() || token() === SyntaxKind.OpenBraceToken || token() === SyntaxKind.OpenBracketToken; + forOrForInOrForOfStatement.statement = parseStatement(); + return finishNode(forOrForInOrForOfStatement); + } + function parseBreakOrContinueStatement(kind: SyntaxKind): BreakOrContinueStatement { + const node = (createNode(kind)); + parseExpected(kind === SyntaxKind.BreakStatement ? SyntaxKind.BreakKeyword : SyntaxKind.ContinueKeyword); + if (!canParseSemicolon()) { + node.label = parseIdentifier(); + } + parseSemicolon(); + return finishNode(node); + } + function parseReturnStatement(): ReturnStatement { + const node = (createNode(SyntaxKind.ReturnStatement)); + parseExpected(SyntaxKind.ReturnKeyword); + if (!canParseSemicolon()) { + node.expression = allowInAnd(parseExpression); + } + parseSemicolon(); + return finishNode(node); + } + function parseWithStatement(): WithStatement { + const node = (createNode(SyntaxKind.WithStatement)); + parseExpected(SyntaxKind.WithKeyword); + parseExpected(SyntaxKind.OpenParenToken); + node.expression = allowInAnd(parseExpression); + parseExpected(SyntaxKind.CloseParenToken); + node.statement = doInsideOfContext(NodeFlags.InWithStatement, parseStatement); + return finishNode(node); + } + function parseCaseClause(): CaseClause { + const node = (createNode(SyntaxKind.CaseClause)); + parseExpected(SyntaxKind.CaseKeyword); + node.expression = allowInAnd(parseExpression); + parseExpected(SyntaxKind.ColonToken); + node.statements = parseList(ParsingContext.SwitchClauseStatements, parseStatement); + return finishNode(node); + } + function parseDefaultClause(): DefaultClause { + const node = (createNode(SyntaxKind.DefaultClause)); + parseExpected(SyntaxKind.DefaultKeyword); + parseExpected(SyntaxKind.ColonToken); + node.statements = parseList(ParsingContext.SwitchClauseStatements, parseStatement); + return finishNode(node); + } + function parseCaseOrDefaultClause(): CaseOrDefaultClause { + return token() === SyntaxKind.CaseKeyword ? parseCaseClause() : parseDefaultClause(); + } + function parseSwitchStatement(): SwitchStatement { + const node = (createNode(SyntaxKind.SwitchStatement)); + parseExpected(SyntaxKind.SwitchKeyword); + parseExpected(SyntaxKind.OpenParenToken); + node.expression = allowInAnd(parseExpression); + parseExpected(SyntaxKind.CloseParenToken); + const caseBlock = (createNode(SyntaxKind.CaseBlock)); + parseExpected(SyntaxKind.OpenBraceToken); + caseBlock.clauses = parseList(ParsingContext.SwitchClauses, parseCaseOrDefaultClause); + parseExpected(SyntaxKind.CloseBraceToken); + node.caseBlock = finishNode(caseBlock); + return finishNode(node); + } + function parseThrowStatement(): ThrowStatement { + // ThrowStatement[Yield] : + // throw [no LineTerminator here]Expression[In, ?Yield]; + // Because of automatic semicolon insertion, we need to report error if this + // throw could be terminated with a semicolon. Note: we can't call 'parseExpression' + // directly as that might consume an expression on the following line. + // We just return 'undefined' in that case. The actual error will be reported in the + // grammar walker. + const node = (createNode(SyntaxKind.ThrowStatement)); + parseExpected(SyntaxKind.ThrowKeyword); + node.expression = scanner.hasPrecedingLineBreak() ? undefined : allowInAnd(parseExpression); + parseSemicolon(); + return finishNode(node); + } + // TODO: Review for error recovery + function parseTryStatement(): TryStatement { + const node = (createNode(SyntaxKind.TryStatement)); + parseExpected(SyntaxKind.TryKeyword); + node.tryBlock = parseBlock(/*ignoreMissingOpenBrace*/ false); + node.catchClause = token() === SyntaxKind.CatchKeyword ? parseCatchClause() : undefined; + // If we don't have a catch clause, then we must have a finally clause. Try to parse + // one out no matter what. + if (!node.catchClause || token() === SyntaxKind.FinallyKeyword) { + parseExpected(SyntaxKind.FinallyKeyword); + node.finallyBlock = parseBlock(/*ignoreMissingOpenBrace*/ false); + } + return finishNode(node); + } + function parseCatchClause(): CatchClause { + const result = (createNode(SyntaxKind.CatchClause)); + parseExpected(SyntaxKind.CatchKeyword); + if (parseOptional(SyntaxKind.OpenParenToken)) { + result.variableDeclaration = parseVariableDeclaration(); + parseExpected(SyntaxKind.CloseParenToken); + } + else { + // Keep shape of node to avoid degrading performance. + result.variableDeclaration = undefined; } - - function isLetDeclaration() { - // In ES6 'let' always starts a lexical declaration if followed by an identifier or { - // or [. - return lookAhead(nextTokenIsIdentifierOrStartOfDestructuring); + result.block = parseBlock(/*ignoreMissingOpenBrace*/ false); + return finishNode(result); + } + function parseDebuggerStatement(): Statement { + const node = (createNode(SyntaxKind.DebuggerStatement)); + parseExpected(SyntaxKind.DebuggerKeyword); + parseSemicolon(); + return finishNode(node); + } + function parseExpressionOrLabeledStatement(): ExpressionStatement | LabeledStatement { + // Avoiding having to do the lookahead for a labeled statement by just trying to parse + // out an expression, seeing if it is identifier and then seeing if it is followed by + // a colon. + const node = (createNodeWithJSDoc(SyntaxKind.Unknown)); + const expression = allowInAnd(parseExpression); + if (expression.kind === SyntaxKind.Identifier && parseOptional(SyntaxKind.ColonToken)) { + node.kind = SyntaxKind.LabeledStatement; + (node).label = (expression); + (node).statement = parseStatement(); + } + else { + node.kind = SyntaxKind.ExpressionStatement; + (node).expression = expression; + parseSemicolon(); } - - function parseStatement(): Statement { + return finishNode(node); + } + function nextTokenIsIdentifierOrKeywordOnSameLine() { + nextToken(); + return tokenIsIdentifierOrKeyword(token()) && !scanner.hasPrecedingLineBreak(); + } + function nextTokenIsClassKeywordOnSameLine() { + nextToken(); + return token() === SyntaxKind.ClassKeyword && !scanner.hasPrecedingLineBreak(); + } + function nextTokenIsFunctionKeywordOnSameLine() { + nextToken(); + return token() === SyntaxKind.FunctionKeyword && !scanner.hasPrecedingLineBreak(); + } + function nextTokenIsIdentifierOrKeywordOrLiteralOnSameLine() { + nextToken(); + return (tokenIsIdentifierOrKeyword(token()) || token() === SyntaxKind.NumericLiteral || token() === SyntaxKind.BigIntLiteral || token() === SyntaxKind.StringLiteral) && !scanner.hasPrecedingLineBreak(); + } + function isDeclaration(): boolean { + while (true) { switch (token()) { - case SyntaxKind.SemicolonToken: - return parseEmptyStatement(); - case SyntaxKind.OpenBraceToken: - return parseBlock(/*ignoreMissingOpenBrace*/ false); case SyntaxKind.VarKeyword: - return parseVariableStatement(createNodeWithJSDoc(SyntaxKind.VariableDeclaration)); case SyntaxKind.LetKeyword: - if (isLetDeclaration()) { - return parseVariableStatement(createNodeWithJSDoc(SyntaxKind.VariableDeclaration)); - } - break; + case SyntaxKind.ConstKeyword: case SyntaxKind.FunctionKeyword: - return parseFunctionDeclaration(createNodeWithJSDoc(SyntaxKind.FunctionDeclaration)); case SyntaxKind.ClassKeyword: - return parseClassDeclaration(createNodeWithJSDoc(SyntaxKind.ClassDeclaration)); - case SyntaxKind.IfKeyword: - return parseIfStatement(); - case SyntaxKind.DoKeyword: - return parseDoStatement(); - case SyntaxKind.WhileKeyword: - return parseWhileStatement(); - case SyntaxKind.ForKeyword: - return parseForOrForInOrForOfStatement(); - case SyntaxKind.ContinueKeyword: - return parseBreakOrContinueStatement(SyntaxKind.ContinueStatement); - case SyntaxKind.BreakKeyword: - return parseBreakOrContinueStatement(SyntaxKind.BreakStatement); - case SyntaxKind.ReturnKeyword: - return parseReturnStatement(); - case SyntaxKind.WithKeyword: - return parseWithStatement(); - case SyntaxKind.SwitchKeyword: - return parseSwitchStatement(); - case SyntaxKind.ThrowKeyword: - return parseThrowStatement(); - case SyntaxKind.TryKeyword: - // Include 'catch' and 'finally' for error recovery. - // falls through - case SyntaxKind.CatchKeyword: - case SyntaxKind.FinallyKeyword: - return parseTryStatement(); - case SyntaxKind.DebuggerKeyword: - return parseDebuggerStatement(); - case SyntaxKind.AtToken: - return parseDeclaration(); - case SyntaxKind.AsyncKeyword: + case SyntaxKind.EnumKeyword: + return true; + // 'declare', 'module', 'namespace', 'interface'* and 'type' are all legal JavaScript identifiers; + // however, an identifier cannot be followed by another identifier on the same line. This is what we + // count on to parse out the respective declarations. For instance, we exploit this to say that + // + // namespace n + // + // can be none other than the beginning of a namespace declaration, but need to respect that JavaScript sees + // + // namespace + // n + // + // as the identifier 'namespace' on one line followed by the identifier 'n' on another. + // We need to look one token ahead to see if it permissible to try parsing a declaration. + // + // *Note*: 'interface' is actually a strict mode reserved word. So while + // + // "use strict" + // interface + // I {} + // + // could be legal, it would add complexity for very little gain. case SyntaxKind.InterfaceKeyword: case SyntaxKind.TypeKeyword: + return nextTokenIsIdentifierOnSameLine(); case SyntaxKind.ModuleKeyword: case SyntaxKind.NamespaceKeyword: + return nextTokenIsIdentifierOrStringLiteralOnSameLine(); + case SyntaxKind.AbstractKeyword: + case SyntaxKind.AsyncKeyword: case SyntaxKind.DeclareKeyword: - case SyntaxKind.ConstKeyword: - case SyntaxKind.EnumKeyword: - case SyntaxKind.ExportKeyword: - case SyntaxKind.ImportKeyword: case SyntaxKind.PrivateKeyword: case SyntaxKind.ProtectedKeyword: case SyntaxKind.PublicKeyword: - case SyntaxKind.AbstractKeyword: - case SyntaxKind.StaticKeyword: case SyntaxKind.ReadonlyKeyword: - case SyntaxKind.GlobalKeyword: - if (isStartOfDeclaration()) { - return parseDeclaration(); + nextToken(); + // ASI takes effect for this modifier. + if (scanner.hasPrecedingLineBreak()) { + return false; } - break; - } - return parseExpressionOrLabeledStatement(); - } - - function isDeclareModifier(modifier: Modifier) { - return modifier.kind === SyntaxKind.DeclareKeyword; - } - - function parseDeclaration(): Statement { - const modifiers = lookAhead(() => (parseDecorators(), parseModifiers())); - // `parseListElement` attempted to get the reused node at this position, - // but the ambient context flag was not yet set, so the node appeared - // not reusable in that context. - const isAmbient = some(modifiers, isDeclareModifier); - if (isAmbient) { - const node = tryReuseAmbientDeclaration(); - if (node) { - return node; - } - } - - const node = createNodeWithJSDoc(SyntaxKind.Unknown); - node.decorators = parseDecorators(); - node.modifiers = parseModifiers(); - if (isAmbient) { - for (const m of node.modifiers!) { - m.flags |= NodeFlags.Ambient; - } - return doInsideOfContext(NodeFlags.Ambient, () => parseDeclarationWorker(node)); - } - else { - return parseDeclarationWorker(node); - } - } - - function tryReuseAmbientDeclaration(): Statement | undefined { - return doInsideOfContext(NodeFlags.Ambient, () => { - const node = currentNode(parsingContext); - if (node) { - return consumeNode(node) as Statement; - } - }); - } - - function parseDeclarationWorker(node: Statement): Statement { - switch (token()) { - case SyntaxKind.VarKeyword: - case SyntaxKind.LetKeyword: - case SyntaxKind.ConstKeyword: - return parseVariableStatement(node); - case SyntaxKind.FunctionKeyword: - return parseFunctionDeclaration(node); - case SyntaxKind.ClassKeyword: - return parseClassDeclaration(node); - case SyntaxKind.InterfaceKeyword: - return parseInterfaceDeclaration(node); - case SyntaxKind.TypeKeyword: - return parseTypeAliasDeclaration(node); - case SyntaxKind.EnumKeyword: - return parseEnumDeclaration(node); + continue; case SyntaxKind.GlobalKeyword: - case SyntaxKind.ModuleKeyword: - case SyntaxKind.NamespaceKeyword: - return parseModuleDeclaration(node); + nextToken(); + return token() === SyntaxKind.OpenBraceToken || token() === SyntaxKind.Identifier || token() === SyntaxKind.ExportKeyword; case SyntaxKind.ImportKeyword: - return parseImportDeclarationOrImportEqualsDeclaration(node); - case SyntaxKind.ExportKeyword: nextToken(); - switch (token()) { - case SyntaxKind.DefaultKeyword: - case SyntaxKind.EqualsToken: - return parseExportAssignment(node); - case SyntaxKind.AsKeyword: - return parseNamespaceExportDeclaration(node); - default: - return parseExportDeclaration(node); + return token() === SyntaxKind.StringLiteral || token() === SyntaxKind.AsteriskToken || + token() === SyntaxKind.OpenBraceToken || tokenIsIdentifierOrKeyword(token()); + case SyntaxKind.ExportKeyword: + let currentToken = nextToken(); + if (currentToken === SyntaxKind.TypeKeyword) { + currentToken = lookAhead(nextToken); } - default: - if (node.decorators || node.modifiers) { - // We reached this point because we encountered decorators and/or modifiers and assumed a declaration - // would follow. For recovery and error reporting purposes, return an incomplete declaration. - const missing = createMissingNode(SyntaxKind.MissingDeclaration, /*reportAtCurrentPosition*/ true, Diagnostics.Declaration_expected); - missing.pos = node.pos; - missing.decorators = node.decorators; - missing.modifiers = node.modifiers; - return finishNode(missing); + if (currentToken === SyntaxKind.EqualsToken || currentToken === SyntaxKind.AsteriskToken || + currentToken === SyntaxKind.OpenBraceToken || currentToken === SyntaxKind.DefaultKeyword || + currentToken === SyntaxKind.AsKeyword) { + return true; } - return undefined!; // TODO: GH#18217 + continue; + case SyntaxKind.StaticKeyword: + nextToken(); + continue; + default: + return false; } } - - function nextTokenIsIdentifierOrStringLiteralOnSameLine() { - nextToken(); - return !scanner.hasPrecedingLineBreak() && (isIdentifier() || token() === SyntaxKind.StringLiteral); + } + function isStartOfDeclaration(): boolean { + return lookAhead(isDeclaration); + } + function isStartOfStatement(): boolean { + switch (token()) { + case SyntaxKind.AtToken: + case SyntaxKind.SemicolonToken: + case SyntaxKind.OpenBraceToken: + case SyntaxKind.VarKeyword: + case SyntaxKind.LetKeyword: + case SyntaxKind.FunctionKeyword: + case SyntaxKind.ClassKeyword: + case SyntaxKind.EnumKeyword: + case SyntaxKind.IfKeyword: + case SyntaxKind.DoKeyword: + case SyntaxKind.WhileKeyword: + case SyntaxKind.ForKeyword: + case SyntaxKind.ContinueKeyword: + case SyntaxKind.BreakKeyword: + case SyntaxKind.ReturnKeyword: + case SyntaxKind.WithKeyword: + case SyntaxKind.SwitchKeyword: + case SyntaxKind.ThrowKeyword: + case SyntaxKind.TryKeyword: + case SyntaxKind.DebuggerKeyword: + // 'catch' and 'finally' do not actually indicate that the code is part of a statement, + // however, we say they are here so that we may gracefully parse them and error later. + // falls through + case SyntaxKind.CatchKeyword: + case SyntaxKind.FinallyKeyword: + return true; + case SyntaxKind.ImportKeyword: + return isStartOfDeclaration() || lookAhead(nextTokenIsOpenParenOrLessThanOrDot); + case SyntaxKind.ConstKeyword: + case SyntaxKind.ExportKeyword: + return isStartOfDeclaration(); + case SyntaxKind.AsyncKeyword: + case SyntaxKind.DeclareKeyword: + case SyntaxKind.InterfaceKeyword: + case SyntaxKind.ModuleKeyword: + case SyntaxKind.NamespaceKeyword: + case SyntaxKind.TypeKeyword: + case SyntaxKind.GlobalKeyword: + // When these don't start a declaration, they're an identifier in an expression statement + return true; + case SyntaxKind.PublicKeyword: + case SyntaxKind.PrivateKeyword: + case SyntaxKind.ProtectedKeyword: + case SyntaxKind.StaticKeyword: + case SyntaxKind.ReadonlyKeyword: + // When these don't start a declaration, they may be the start of a class member if an identifier + // immediately follows. Otherwise they're an identifier in an expression statement. + return isStartOfDeclaration() || !lookAhead(nextTokenIsIdentifierOrKeywordOnSameLine); + default: + return isStartOfExpression(); } - - function parseFunctionBlockOrSemicolon(flags: SignatureFlags, diagnosticMessage?: DiagnosticMessage): Block | undefined { - if (token() !== SyntaxKind.OpenBraceToken && canParseSemicolon()) { - parseSemicolon(); - return; - } - - return parseFunctionBlock(flags, diagnosticMessage); + } + function nextTokenIsIdentifierOrStartOfDestructuring() { + nextToken(); + return isIdentifier() || token() === SyntaxKind.OpenBraceToken || token() === SyntaxKind.OpenBracketToken; + } + function isLetDeclaration() { + // In ES6 'let' always starts a lexical declaration if followed by an identifier or { + // or [. + return lookAhead(nextTokenIsIdentifierOrStartOfDestructuring); + } + function parseStatement(): Statement { + switch (token()) { + case SyntaxKind.SemicolonToken: + return parseEmptyStatement(); + case SyntaxKind.OpenBraceToken: + return parseBlock(/*ignoreMissingOpenBrace*/ false); + case SyntaxKind.VarKeyword: + return parseVariableStatement((createNodeWithJSDoc(SyntaxKind.VariableDeclaration))); + case SyntaxKind.LetKeyword: + if (isLetDeclaration()) { + return parseVariableStatement((createNodeWithJSDoc(SyntaxKind.VariableDeclaration))); + } + break; + case SyntaxKind.FunctionKeyword: + return parseFunctionDeclaration((createNodeWithJSDoc(SyntaxKind.FunctionDeclaration))); + case SyntaxKind.ClassKeyword: + return parseClassDeclaration((createNodeWithJSDoc(SyntaxKind.ClassDeclaration))); + case SyntaxKind.IfKeyword: + return parseIfStatement(); + case SyntaxKind.DoKeyword: + return parseDoStatement(); + case SyntaxKind.WhileKeyword: + return parseWhileStatement(); + case SyntaxKind.ForKeyword: + return parseForOrForInOrForOfStatement(); + case SyntaxKind.ContinueKeyword: + return parseBreakOrContinueStatement(SyntaxKind.ContinueStatement); + case SyntaxKind.BreakKeyword: + return parseBreakOrContinueStatement(SyntaxKind.BreakStatement); + case SyntaxKind.ReturnKeyword: + return parseReturnStatement(); + case SyntaxKind.WithKeyword: + return parseWithStatement(); + case SyntaxKind.SwitchKeyword: + return parseSwitchStatement(); + case SyntaxKind.ThrowKeyword: + return parseThrowStatement(); + case SyntaxKind.TryKeyword: + // Include 'catch' and 'finally' for error recovery. + // falls through + case SyntaxKind.CatchKeyword: + case SyntaxKind.FinallyKeyword: + return parseTryStatement(); + case SyntaxKind.DebuggerKeyword: + return parseDebuggerStatement(); + case SyntaxKind.AtToken: + return parseDeclaration(); + case SyntaxKind.AsyncKeyword: + case SyntaxKind.InterfaceKeyword: + case SyntaxKind.TypeKeyword: + case SyntaxKind.ModuleKeyword: + case SyntaxKind.NamespaceKeyword: + case SyntaxKind.DeclareKeyword: + case SyntaxKind.ConstKeyword: + case SyntaxKind.EnumKeyword: + case SyntaxKind.ExportKeyword: + case SyntaxKind.ImportKeyword: + case SyntaxKind.PrivateKeyword: + case SyntaxKind.ProtectedKeyword: + case SyntaxKind.PublicKeyword: + case SyntaxKind.AbstractKeyword: + case SyntaxKind.StaticKeyword: + case SyntaxKind.ReadonlyKeyword: + case SyntaxKind.GlobalKeyword: + if (isStartOfDeclaration()) { + return parseDeclaration(); + } + break; } - - // DECLARATIONS - - function parseArrayBindingElement(): ArrayBindingElement { - if (token() === SyntaxKind.CommaToken) { - return createNode(SyntaxKind.OmittedExpression); + return parseExpressionOrLabeledStatement(); + } + function isDeclareModifier(modifier: Modifier) { + return modifier.kind === SyntaxKind.DeclareKeyword; + } + function parseDeclaration(): Statement { + const modifiers = lookAhead(() => (parseDecorators(), parseModifiers())); + // `parseListElement` attempted to get the reused node at this position, + // but the ambient context flag was not yet set, so the node appeared + // not reusable in that context. + const isAmbient = some(modifiers, isDeclareModifier); + if (isAmbient) { + const node = tryReuseAmbientDeclaration(); + if (node) { + return node; } - const node = createNode(SyntaxKind.BindingElement); - node.dotDotDotToken = parseOptionalToken(SyntaxKind.DotDotDotToken); - node.name = parseIdentifierOrPattern(); - node.initializer = parseInitializer(); - return finishNode(node); } - - function parseObjectBindingElement(): BindingElement { - const node = createNode(SyntaxKind.BindingElement); - node.dotDotDotToken = parseOptionalToken(SyntaxKind.DotDotDotToken); - const tokenIsIdentifier = isIdentifier(); - const propertyName = parsePropertyName(); - if (tokenIsIdentifier && token() !== SyntaxKind.ColonToken) { - node.name = propertyName; - } - else { - parseExpected(SyntaxKind.ColonToken); - node.propertyName = propertyName; - node.name = parseIdentifierOrPattern(); + const node = (createNodeWithJSDoc(SyntaxKind.Unknown)); + node.decorators = parseDecorators(); + node.modifiers = parseModifiers(); + if (isAmbient) { + for (const m of node.modifiers!) { + m.flags |= NodeFlags.Ambient; } - node.initializer = parseInitializer(); - return finishNode(node); + return doInsideOfContext(NodeFlags.Ambient, () => parseDeclarationWorker(node)); } - - function parseObjectBindingPattern(): ObjectBindingPattern { - const node = createNode(SyntaxKind.ObjectBindingPattern); - parseExpected(SyntaxKind.OpenBraceToken); - node.elements = parseDelimitedList(ParsingContext.ObjectBindingElements, parseObjectBindingElement); - parseExpected(SyntaxKind.CloseBraceToken); - return finishNode(node); + else { + return parseDeclarationWorker(node); } - - function parseArrayBindingPattern(): ArrayBindingPattern { - const node = createNode(SyntaxKind.ArrayBindingPattern); - parseExpected(SyntaxKind.OpenBracketToken); - node.elements = parseDelimitedList(ParsingContext.ArrayBindingElements, parseArrayBindingElement); - parseExpected(SyntaxKind.CloseBracketToken); - return finishNode(node); + } + function tryReuseAmbientDeclaration(): Statement | undefined { + return doInsideOfContext(NodeFlags.Ambient, () => { + const node = currentNode(parsingContext); + if (node) { + return consumeNode(node) as Statement; + } + }); + } + function parseDeclarationWorker(node: Statement): Statement { + switch (token()) { + case SyntaxKind.VarKeyword: + case SyntaxKind.LetKeyword: + case SyntaxKind.ConstKeyword: + return parseVariableStatement((node)); + case SyntaxKind.FunctionKeyword: + return parseFunctionDeclaration((node)); + case SyntaxKind.ClassKeyword: + return parseClassDeclaration((node)); + case SyntaxKind.InterfaceKeyword: + return parseInterfaceDeclaration((node)); + case SyntaxKind.TypeKeyword: + return parseTypeAliasDeclaration((node)); + case SyntaxKind.EnumKeyword: + return parseEnumDeclaration((node)); + case SyntaxKind.GlobalKeyword: + case SyntaxKind.ModuleKeyword: + case SyntaxKind.NamespaceKeyword: + return parseModuleDeclaration((node)); + case SyntaxKind.ImportKeyword: + return parseImportDeclarationOrImportEqualsDeclaration((node)); + case SyntaxKind.ExportKeyword: + nextToken(); + switch (token()) { + case SyntaxKind.DefaultKeyword: + case SyntaxKind.EqualsToken: + return parseExportAssignment((node)); + case SyntaxKind.AsKeyword: + return parseNamespaceExportDeclaration((node)); + default: + return parseExportDeclaration((node)); + } + default: + if (node.decorators || node.modifiers) { + // We reached this point because we encountered decorators and/or modifiers and assumed a declaration + // would follow. For recovery and error reporting purposes, return an incomplete declaration. + const missing = createMissingNode(SyntaxKind.MissingDeclaration, /*reportAtCurrentPosition*/ true, Diagnostics.Declaration_expected); + missing.pos = node.pos; + missing.decorators = node.decorators; + missing.modifiers = node.modifiers; + return finishNode(missing); + } + return undefined!; // TODO: GH#18217 } - - function isIdentifierOrPrivateIdentifierOrPattern() { - return token() === SyntaxKind.OpenBraceToken - || token() === SyntaxKind.OpenBracketToken - || token() === SyntaxKind.PrivateIdentifier - || isIdentifier(); + } + function nextTokenIsIdentifierOrStringLiteralOnSameLine() { + nextToken(); + return !scanner.hasPrecedingLineBreak() && (isIdentifier() || token() === SyntaxKind.StringLiteral); + } + function parseFunctionBlockOrSemicolon(flags: SignatureFlags, diagnosticMessage?: DiagnosticMessage): Block | undefined { + if (token() !== SyntaxKind.OpenBraceToken && canParseSemicolon()) { + parseSemicolon(); + return; } - - function parseIdentifierOrPattern(privateIdentifierDiagnosticMessage?: DiagnosticMessage): Identifier | BindingPattern { - if (token() === SyntaxKind.OpenBracketToken) { - return parseArrayBindingPattern(); - } - if (token() === SyntaxKind.OpenBraceToken) { - return parseObjectBindingPattern(); - } - return parseIdentifier(/*diagnosticMessage*/ undefined, privateIdentifierDiagnosticMessage); + return parseFunctionBlock(flags, diagnosticMessage); + } + // DECLARATIONS + function parseArrayBindingElement(): ArrayBindingElement { + if (token() === SyntaxKind.CommaToken) { + return createNode(SyntaxKind.OmittedExpression); + } + const node = (createNode(SyntaxKind.BindingElement)); + node.dotDotDotToken = parseOptionalToken(SyntaxKind.DotDotDotToken); + node.name = parseIdentifierOrPattern(); + node.initializer = parseInitializer(); + return finishNode(node); + } + function parseObjectBindingElement(): BindingElement { + const node = (createNode(SyntaxKind.BindingElement)); + node.dotDotDotToken = parseOptionalToken(SyntaxKind.DotDotDotToken); + const tokenIsIdentifier = isIdentifier(); + const propertyName = parsePropertyName(); + if (tokenIsIdentifier && token() !== SyntaxKind.ColonToken) { + node.name = (propertyName); } - - function parseVariableDeclarationAllowExclamation() { - return parseVariableDeclaration(/*allowExclamation*/ true); + else { + parseExpected(SyntaxKind.ColonToken); + node.propertyName = propertyName; + node.name = parseIdentifierOrPattern(); } - - function parseVariableDeclaration(allowExclamation?: boolean): VariableDeclaration { - const node = createNode(SyntaxKind.VariableDeclaration); - node.name = parseIdentifierOrPattern(Diagnostics.Private_identifiers_are_not_allowed_in_variable_declarations); - if (allowExclamation && node.name.kind === SyntaxKind.Identifier && - token() === SyntaxKind.ExclamationToken && !scanner.hasPrecedingLineBreak()) { - node.exclamationToken = parseTokenNode>(); - } - node.type = parseTypeAnnotation(); - if (!isInOrOfKeyword(token())) { - node.initializer = parseInitializer(); - } - return finishNode(node); + node.initializer = parseInitializer(); + return finishNode(node); + } + function parseObjectBindingPattern(): ObjectBindingPattern { + const node = (createNode(SyntaxKind.ObjectBindingPattern)); + parseExpected(SyntaxKind.OpenBraceToken); + node.elements = parseDelimitedList(ParsingContext.ObjectBindingElements, parseObjectBindingElement); + parseExpected(SyntaxKind.CloseBraceToken); + return finishNode(node); + } + function parseArrayBindingPattern(): ArrayBindingPattern { + const node = (createNode(SyntaxKind.ArrayBindingPattern)); + parseExpected(SyntaxKind.OpenBracketToken); + node.elements = parseDelimitedList(ParsingContext.ArrayBindingElements, parseArrayBindingElement); + parseExpected(SyntaxKind.CloseBracketToken); + return finishNode(node); + } + function isIdentifierOrPrivateIdentifierOrPattern() { + return token() === SyntaxKind.OpenBraceToken + || token() === SyntaxKind.OpenBracketToken + || token() === SyntaxKind.PrivateIdentifier + || isIdentifier(); + } + function parseIdentifierOrPattern(privateIdentifierDiagnosticMessage?: DiagnosticMessage): Identifier | BindingPattern { + if (token() === SyntaxKind.OpenBracketToken) { + return parseArrayBindingPattern(); } - - function parseVariableDeclarationList(inForStatementInitializer: boolean): VariableDeclarationList { - const node = createNode(SyntaxKind.VariableDeclarationList); - - switch (token()) { - case SyntaxKind.VarKeyword: - break; - case SyntaxKind.LetKeyword: - node.flags |= NodeFlags.Let; - break; - case SyntaxKind.ConstKeyword: - node.flags |= NodeFlags.Const; - break; - default: - Debug.fail(); - } - - nextToken(); - - // The user may have written the following: - // - // for (let of X) { } - // - // In this case, we want to parse an empty declaration list, and then parse 'of' - // as a keyword. The reason this is not automatic is that 'of' is a valid identifier. - // So we need to look ahead to determine if 'of' should be treated as a keyword in - // this context. - // The checker will then give an error that there is an empty declaration list. - if (token() === SyntaxKind.OfKeyword && lookAhead(canFollowContextualOfKeyword)) { - node.declarations = createMissingList(); - } - else { - const savedDisallowIn = inDisallowInContext(); - setDisallowInContext(inForStatementInitializer); - - node.declarations = parseDelimitedList(ParsingContext.VariableDeclarations, - inForStatementInitializer ? parseVariableDeclaration : parseVariableDeclarationAllowExclamation); - - setDisallowInContext(savedDisallowIn); - } - - return finishNode(node); + if (token() === SyntaxKind.OpenBraceToken) { + return parseObjectBindingPattern(); } - - function canFollowContextualOfKeyword(): boolean { - return nextTokenIsIdentifier() && nextToken() === SyntaxKind.CloseParenToken; + return parseIdentifier(/*diagnosticMessage*/ undefined, privateIdentifierDiagnosticMessage); + } + function parseVariableDeclarationAllowExclamation() { + return parseVariableDeclaration(/*allowExclamation*/ true); + } + function parseVariableDeclaration(allowExclamation?: boolean): VariableDeclaration { + const node = (createNode(SyntaxKind.VariableDeclaration)); + node.name = parseIdentifierOrPattern(Diagnostics.Private_identifiers_are_not_allowed_in_variable_declarations); + if (allowExclamation && node.name.kind === SyntaxKind.Identifier && + token() === SyntaxKind.ExclamationToken && !scanner.hasPrecedingLineBreak()) { + node.exclamationToken = parseTokenNode>(); + } + node.type = parseTypeAnnotation(); + if (!isInOrOfKeyword(token())) { + node.initializer = parseInitializer(); } - - function parseVariableStatement(node: VariableStatement): VariableStatement { - node.kind = SyntaxKind.VariableStatement; - node.declarationList = parseVariableDeclarationList(/*inForStatementInitializer*/ false); - parseSemicolon(); - return finishNode(node); + return finishNode(node); + } + function parseVariableDeclarationList(inForStatementInitializer: boolean): VariableDeclarationList { + const node = (createNode(SyntaxKind.VariableDeclarationList)); + switch (token()) { + case SyntaxKind.VarKeyword: + break; + case SyntaxKind.LetKeyword: + node.flags |= NodeFlags.Let; + break; + case SyntaxKind.ConstKeyword: + node.flags |= NodeFlags.Const; + break; + default: + Debug.fail(); } - - function parseFunctionDeclaration(node: FunctionDeclaration): FunctionDeclaration { - node.kind = SyntaxKind.FunctionDeclaration; - parseExpected(SyntaxKind.FunctionKeyword); - node.asteriskToken = parseOptionalToken(SyntaxKind.AsteriskToken); - node.name = hasModifierOfKind(node, SyntaxKind.DefaultKeyword) ? parseOptionalIdentifier() : parseIdentifier(); - const isGenerator = node.asteriskToken ? SignatureFlags.Yield : SignatureFlags.None; - const isAsync = hasModifierOfKind(node, SyntaxKind.AsyncKeyword) ? SignatureFlags.Await : SignatureFlags.None; - fillSignature(SyntaxKind.ColonToken, isGenerator | isAsync, node); - node.body = parseFunctionBlockOrSemicolon(isGenerator | isAsync, Diagnostics.or_expected); - return finishNode(node); + nextToken(); + // The user may have written the following: + // + // for (let of X) { } + // + // In this case, we want to parse an empty declaration list, and then parse 'of' + // as a keyword. The reason this is not automatic is that 'of' is a valid identifier. + // So we need to look ahead to determine if 'of' should be treated as a keyword in + // this context. + // The checker will then give an error that there is an empty declaration list. + if (token() === SyntaxKind.OfKeyword && lookAhead(canFollowContextualOfKeyword)) { + node.declarations = createMissingList(); } - - function parseConstructorName() { - if (token() === SyntaxKind.ConstructorKeyword) { - return parseExpected(SyntaxKind.ConstructorKeyword); - } - if (token() === SyntaxKind.StringLiteral && lookAhead(nextToken) === SyntaxKind.OpenParenToken) { - return tryParse(() => { - const literalNode = parseLiteralNode(); - return literalNode.text === "constructor" ? literalNode : undefined; - }); - } + else { + const savedDisallowIn = inDisallowInContext(); + setDisallowInContext(inForStatementInitializer); + node.declarations = parseDelimitedList(ParsingContext.VariableDeclarations, inForStatementInitializer ? parseVariableDeclaration : parseVariableDeclarationAllowExclamation); + setDisallowInContext(savedDisallowIn); + } + return finishNode(node); + } + function canFollowContextualOfKeyword(): boolean { + return nextTokenIsIdentifier() && nextToken() === SyntaxKind.CloseParenToken; + } + function parseVariableStatement(node: VariableStatement): VariableStatement { + node.kind = SyntaxKind.VariableStatement; + node.declarationList = parseVariableDeclarationList(/*inForStatementInitializer*/ false); + parseSemicolon(); + return finishNode(node); + } + function parseFunctionDeclaration(node: FunctionDeclaration): FunctionDeclaration { + node.kind = SyntaxKind.FunctionDeclaration; + parseExpected(SyntaxKind.FunctionKeyword); + node.asteriskToken = parseOptionalToken(SyntaxKind.AsteriskToken); + node.name = hasModifierOfKind(node, SyntaxKind.DefaultKeyword) ? parseOptionalIdentifier() : parseIdentifier(); + const isGenerator = node.asteriskToken ? SignatureFlags.Yield : SignatureFlags.None; + const isAsync = hasModifierOfKind(node, SyntaxKind.AsyncKeyword) ? SignatureFlags.Await : SignatureFlags.None; + fillSignature(SyntaxKind.ColonToken, isGenerator | isAsync, node); + node.body = parseFunctionBlockOrSemicolon(isGenerator | isAsync, Diagnostics.or_expected); + return finishNode(node); + } + function parseConstructorName() { + if (token() === SyntaxKind.ConstructorKeyword) { + return parseExpected(SyntaxKind.ConstructorKeyword); } - - function tryParseConstructorDeclaration(node: ConstructorDeclaration): ConstructorDeclaration | undefined { + if (token() === SyntaxKind.StringLiteral && lookAhead(nextToken) === SyntaxKind.OpenParenToken) { return tryParse(() => { - if (parseConstructorName()) { - node.kind = SyntaxKind.Constructor; - fillSignature(SyntaxKind.ColonToken, SignatureFlags.None, node); - node.body = parseFunctionBlockOrSemicolon(SignatureFlags.None, Diagnostics.or_expected); - return finishNode(node); - } + const literalNode = parseLiteralNode(); + return literalNode.text === "constructor" ? literalNode : undefined; }); } - - function parseMethodDeclaration(node: MethodDeclaration, asteriskToken: AsteriskToken, diagnosticMessage?: DiagnosticMessage): MethodDeclaration { - node.kind = SyntaxKind.MethodDeclaration; - node.asteriskToken = asteriskToken; - const isGenerator = asteriskToken ? SignatureFlags.Yield : SignatureFlags.None; - const isAsync = hasModifierOfKind(node, SyntaxKind.AsyncKeyword) ? SignatureFlags.Await : SignatureFlags.None; - fillSignature(SyntaxKind.ColonToken, isGenerator | isAsync, node); - node.body = parseFunctionBlockOrSemicolon(isGenerator | isAsync, diagnosticMessage); - return finishNode(node); - } - - function parsePropertyDeclaration(node: PropertyDeclaration): PropertyDeclaration { - node.kind = SyntaxKind.PropertyDeclaration; - if (!node.questionToken && token() === SyntaxKind.ExclamationToken && !scanner.hasPrecedingLineBreak()) { - node.exclamationToken = parseTokenNode>(); + } + function tryParseConstructorDeclaration(node: ConstructorDeclaration): ConstructorDeclaration | undefined { + return tryParse(() => { + if (parseConstructorName()) { + node.kind = SyntaxKind.Constructor; + fillSignature(SyntaxKind.ColonToken, SignatureFlags.None, node); + node.body = parseFunctionBlockOrSemicolon(SignatureFlags.None, Diagnostics.or_expected); + return finishNode(node); } - node.type = parseTypeAnnotation(); - node.initializer = doOutsideOfContext(NodeFlags.YieldContext | NodeFlags.AwaitContext | NodeFlags.DisallowInContext, parseInitializer); - - parseSemicolon(); - return finishNode(node); - } - - function parsePropertyOrMethodDeclaration(node: PropertyDeclaration | MethodDeclaration): PropertyDeclaration | MethodDeclaration { - const asteriskToken = parseOptionalToken(SyntaxKind.AsteriskToken); - node.name = parsePropertyName(); - // Note: this is not legal as per the grammar. But we allow it in the parser and - // report an error in the grammar checker. - node.questionToken = parseOptionalToken(SyntaxKind.QuestionToken); - if (asteriskToken || token() === SyntaxKind.OpenParenToken || token() === SyntaxKind.LessThanToken) { - return parseMethodDeclaration(node, asteriskToken, Diagnostics.or_expected); - } - return parsePropertyDeclaration(node); - } - - function parseAccessorDeclaration(node: AccessorDeclaration, kind: AccessorDeclaration["kind"]): AccessorDeclaration { - node.kind = kind; - node.name = parsePropertyName(); - fillSignature(SyntaxKind.ColonToken, SignatureFlags.None, node); - node.body = parseFunctionBlockOrSemicolon(SignatureFlags.None); - return finishNode(node); + }); + } + function parseMethodDeclaration(node: MethodDeclaration, asteriskToken: AsteriskToken, diagnosticMessage?: DiagnosticMessage): MethodDeclaration { + node.kind = SyntaxKind.MethodDeclaration; + node.asteriskToken = asteriskToken; + const isGenerator = asteriskToken ? SignatureFlags.Yield : SignatureFlags.None; + const isAsync = hasModifierOfKind(node, SyntaxKind.AsyncKeyword) ? SignatureFlags.Await : SignatureFlags.None; + fillSignature(SyntaxKind.ColonToken, isGenerator | isAsync, node); + node.body = parseFunctionBlockOrSemicolon(isGenerator | isAsync, diagnosticMessage); + return finishNode(node); + } + function parsePropertyDeclaration(node: PropertyDeclaration): PropertyDeclaration { + node.kind = SyntaxKind.PropertyDeclaration; + if (!node.questionToken && token() === SyntaxKind.ExclamationToken && !scanner.hasPrecedingLineBreak()) { + node.exclamationToken = parseTokenNode>(); + } + node.type = parseTypeAnnotation(); + node.initializer = doOutsideOfContext(NodeFlags.YieldContext | NodeFlags.AwaitContext | NodeFlags.DisallowInContext, parseInitializer); + parseSemicolon(); + return finishNode(node); + } + function parsePropertyOrMethodDeclaration(node: PropertyDeclaration | MethodDeclaration): PropertyDeclaration | MethodDeclaration { + const asteriskToken = parseOptionalToken(SyntaxKind.AsteriskToken); + node.name = parsePropertyName(); + // Note: this is not legal as per the grammar. But we allow it in the parser and + // report an error in the grammar checker. + node.questionToken = parseOptionalToken(SyntaxKind.QuestionToken); + if (asteriskToken || token() === SyntaxKind.OpenParenToken || token() === SyntaxKind.LessThanToken) { + return parseMethodDeclaration((node), asteriskToken, Diagnostics.or_expected); + } + return parsePropertyDeclaration((node)); + } + function parseAccessorDeclaration(node: AccessorDeclaration, kind: AccessorDeclaration["kind"]): AccessorDeclaration { + node.kind = kind; + node.name = parsePropertyName(); + fillSignature(SyntaxKind.ColonToken, SignatureFlags.None, node); + node.body = parseFunctionBlockOrSemicolon(SignatureFlags.None); + return finishNode(node); + } + function isClassMemberStart(): boolean { + let idToken: SyntaxKind | undefined; + if (token() === SyntaxKind.AtToken) { + return true; } - - function isClassMemberStart(): boolean { - let idToken: SyntaxKind | undefined; - - if (token() === SyntaxKind.AtToken) { - return true; - } - - // Eat up all modifiers, but hold on to the last one in case it is actually an identifier. - while (isModifierKind(token())) { - idToken = token(); - // If the idToken is a class modifier (protected, private, public, and static), it is - // certain that we are starting to parse class member. This allows better error recovery - // Example: - // public foo() ... // true - // public @dec blah ... // true; we will then report an error later - // export public ... // true; we will then report an error later - if (isClassMemberModifier(idToken)) { - return true; - } - - nextToken(); - } - - if (token() === SyntaxKind.AsteriskToken) { + // Eat up all modifiers, but hold on to the last one in case it is actually an identifier. + while (isModifierKind(token())) { + idToken = token(); + // If the idToken is a class modifier (protected, private, public, and static), it is + // certain that we are starting to parse class member. This allows better error recovery + // Example: + // public foo() ... // true + // public @dec blah ... // true; we will then report an error later + // export public ... // true; we will then report an error later + if (isClassMemberModifier(idToken)) { return true; } - - // Try to get the first property-like token following all modifiers. - // This can either be an identifier or the 'get' or 'set' keywords. - if (isLiteralPropertyName()) { - idToken = token(); - nextToken(); - } - - // Index signatures and computed properties are class members; we can parse. - if (token() === SyntaxKind.OpenBracketToken) { + nextToken(); + } + if (token() === SyntaxKind.AsteriskToken) { + return true; + } + // Try to get the first property-like token following all modifiers. + // This can either be an identifier or the 'get' or 'set' keywords. + if (isLiteralPropertyName()) { + idToken = token(); + nextToken(); + } + // Index signatures and computed properties are class members; we can parse. + if (token() === SyntaxKind.OpenBracketToken) { + return true; + } + // If we were able to get any potential identifier... + if (idToken !== undefined) { + // If we have a non-keyword identifier, or if we have an accessor, then it's safe to parse. + if (!isKeyword(idToken) || idToken === SyntaxKind.SetKeyword || idToken === SyntaxKind.GetKeyword) { return true; } - - // If we were able to get any potential identifier... - if (idToken !== undefined) { - // If we have a non-keyword identifier, or if we have an accessor, then it's safe to parse. - if (!isKeyword(idToken) || idToken === SyntaxKind.SetKeyword || idToken === SyntaxKind.GetKeyword) { - return true; - } - - // If it *is* a keyword, but not an accessor, check a little farther along - // to see if it should actually be parsed as a class member. - switch (token()) { - case SyntaxKind.OpenParenToken: // Method declaration - case SyntaxKind.LessThanToken: // Generic Method declaration - case SyntaxKind.ExclamationToken: // Non-null assertion on property name - case SyntaxKind.ColonToken: // Type Annotation for declaration - case SyntaxKind.EqualsToken: // Initializer for declaration - case SyntaxKind.QuestionToken: // Not valid, but permitted so that it gets caught later on. - return true; - default: - // Covers - // - Semicolons (declaration termination) - // - Closing braces (end-of-class, must be declaration) - // - End-of-files (not valid, but permitted so that it gets caught later on) - // - Line-breaks (enabling *automatic semicolon insertion*) - return canParseSemicolon(); - } - } - - return false; - } - - function parseDecorators(): NodeArray | undefined { - let list: Decorator[] | undefined; - const listPos = getNodePos(); - while (true) { - const decoratorStart = getNodePos(); - if (!parseOptional(SyntaxKind.AtToken)) { - break; - } - const decorator = createNode(SyntaxKind.Decorator, decoratorStart); - decorator.expression = doInDecoratorContext(parseLeftHandSideExpressionOrHigher); - finishNode(decorator); - (list || (list = [])).push(decorator); - } - return list && createNodeArray(list, listPos); - } - - /* - * There are situations in which a modifier like 'const' will appear unexpectedly, such as on a class member. - * In those situations, if we are entirely sure that 'const' is not valid on its own (such as when ASI takes effect - * and turns it into a standalone declaration), then it is better to parse it and report an error later. - * - * In such situations, 'permitInvalidConstAsModifier' should be set to true. - */ - function parseModifiers(permitInvalidConstAsModifier?: boolean): NodeArray | undefined { - let list: Modifier[] | undefined; - const listPos = getNodePos(); - while (true) { - const modifierStart = scanner.getStartPos(); - const modifierKind = token(); - - if (token() === SyntaxKind.ConstKeyword && permitInvalidConstAsModifier) { - // We need to ensure that any subsequent modifiers appear on the same line - // so that when 'const' is a standalone declaration, we don't issue an error. - if (!tryParse(nextTokenIsOnSameLineAndCanFollowModifier)) { - break; - } - } - else { - if (!parseAnyContextualModifier()) { - break; - } - } - - const modifier = finishNode(createNode(modifierKind, modifierStart)); - (list || (list = [])).push(modifier); - } - return list && createNodeArray(list, listPos); - } - - function parseModifiersForArrowFunction(): NodeArray | undefined { - let modifiers: NodeArray | undefined; - if (token() === SyntaxKind.AsyncKeyword) { - const modifierStart = scanner.getStartPos(); - const modifierKind = token(); - nextToken(); - const modifier = finishNode(createNode(modifierKind, modifierStart)); - modifiers = createNodeArray([modifier], modifierStart); + // If it *is* a keyword, but not an accessor, check a little farther along + // to see if it should actually be parsed as a class member. + switch (token()) { + case SyntaxKind.OpenParenToken: // Method declaration + case SyntaxKind.LessThanToken: // Generic Method declaration + case SyntaxKind.ExclamationToken: // Non-null assertion on property name + case SyntaxKind.ColonToken: // Type Annotation for declaration + case SyntaxKind.EqualsToken: // Initializer for declaration + case SyntaxKind.QuestionToken: // Not valid, but permitted so that it gets caught later on. + return true; + default: + // Covers + // - Semicolons (declaration termination) + // - Closing braces (end-of-class, must be declaration) + // - End-of-files (not valid, but permitted so that it gets caught later on) + // - Line-breaks (enabling *automatic semicolon insertion*) + return canParseSemicolon(); } - return modifiers; } - - function parseClassElement(): ClassElement { - if (token() === SyntaxKind.SemicolonToken) { - const result = createNode(SyntaxKind.SemicolonClassElement); - nextToken(); - return finishNode(result); + return false; + } + function parseDecorators(): NodeArray | undefined { + let list: Decorator[] | undefined; + const listPos = getNodePos(); + while (true) { + const decoratorStart = getNodePos(); + if (!parseOptional(SyntaxKind.AtToken)) { + break; } - - const node = createNodeWithJSDoc(SyntaxKind.Unknown); - node.decorators = parseDecorators(); - node.modifiers = parseModifiers(/*permitInvalidConstAsModifier*/ true); - - if (parseContextualModifier(SyntaxKind.GetKeyword)) { - return parseAccessorDeclaration(node, SyntaxKind.GetAccessor); - } - - if (parseContextualModifier(SyntaxKind.SetKeyword)) { - return parseAccessorDeclaration(node, SyntaxKind.SetAccessor); - } - - if (token() === SyntaxKind.ConstructorKeyword || token() === SyntaxKind.StringLiteral) { - const constructorDeclaration = tryParseConstructorDeclaration(node); - if (constructorDeclaration) { - return constructorDeclaration; + const decorator = (createNode(SyntaxKind.Decorator, decoratorStart)); + decorator.expression = doInDecoratorContext(parseLeftHandSideExpressionOrHigher); + finishNode(decorator); + (list || (list = [])).push(decorator); + } + return list && createNodeArray(list, listPos); + } + /* + * There are situations in which a modifier like 'const' will appear unexpectedly, such as on a class member. + * In those situations, if we are entirely sure that 'const' is not valid on its own (such as when ASI takes effect + * and turns it into a standalone declaration), then it is better to parse it and report an error later. + * + * In such situations, 'permitInvalidConstAsModifier' should be set to true. + */ + function parseModifiers(permitInvalidConstAsModifier?: boolean): NodeArray | undefined { + let list: Modifier[] | undefined; + const listPos = getNodePos(); + while (true) { + const modifierStart = scanner.getStartPos(); + const modifierKind = token(); + if (token() === SyntaxKind.ConstKeyword && permitInvalidConstAsModifier) { + // We need to ensure that any subsequent modifiers appear on the same line + // so that when 'const' is a standalone declaration, we don't issue an error. + if (!tryParse(nextTokenIsOnSameLineAndCanFollowModifier)) { + break; } } - - if (isIndexSignature()) { - return parseIndexSignatureDeclaration(node); - } - - // It is very important that we check this *after* checking indexers because - // the [ token can start an index signature or a computed property name - if (tokenIsIdentifierOrKeyword(token()) || - token() === SyntaxKind.StringLiteral || - token() === SyntaxKind.NumericLiteral || - token() === SyntaxKind.AsteriskToken || - token() === SyntaxKind.OpenBracketToken) { - const isAmbient = node.modifiers && some(node.modifiers, isDeclareModifier); - if (isAmbient) { - for (const m of node.modifiers!) { - m.flags |= NodeFlags.Ambient; - } - return doInsideOfContext(NodeFlags.Ambient, () => parsePropertyOrMethodDeclaration(node as PropertyDeclaration | MethodDeclaration)); - } - else { - return parsePropertyOrMethodDeclaration(node as PropertyDeclaration | MethodDeclaration); + else { + if (!parseAnyContextualModifier()) { + break; } } - - if (node.decorators || node.modifiers) { - // treat this as a property declaration with a missing name. - node.name = createMissingNode(SyntaxKind.Identifier, /*reportAtCurrentPosition*/ true, Diagnostics.Declaration_expected); - return parsePropertyDeclaration(node); - } - - // 'isClassMemberStart' should have hinted not to attempt parsing. - return Debug.fail("Should not have attempted to parse class member declaration."); - } - - function parseClassExpression(): ClassExpression { - return parseClassDeclarationOrExpression(createNodeWithJSDoc(SyntaxKind.Unknown), SyntaxKind.ClassExpression); - } - - function parseClassDeclaration(node: ClassLikeDeclaration): ClassDeclaration { - return parseClassDeclarationOrExpression(node, SyntaxKind.ClassDeclaration); - } - - function parseClassDeclarationOrExpression(node: ClassLikeDeclaration, kind: ClassLikeDeclaration["kind"]): ClassLikeDeclaration { - node.kind = kind; - parseExpected(SyntaxKind.ClassKeyword); - node.name = parseNameOfClassDeclarationOrExpression(); - node.typeParameters = parseTypeParameters(); - node.heritageClauses = parseHeritageClauses(); - - if (parseExpected(SyntaxKind.OpenBraceToken)) { - // ClassTail[Yield,Await] : (Modified) See 14.5 - // ClassHeritage[?Yield,?Await]opt { ClassBody[?Yield,?Await]opt } - node.members = parseClassMembers(); - parseExpected(SyntaxKind.CloseBraceToken); + const modifier = finishNode((createNode(modifierKind, modifierStart))); + (list || (list = [])).push(modifier); + } + return list && createNodeArray(list, listPos); + } + function parseModifiersForArrowFunction(): NodeArray | undefined { + let modifiers: NodeArray | undefined; + if (token() === SyntaxKind.AsyncKeyword) { + const modifierStart = scanner.getStartPos(); + const modifierKind = token(); + nextToken(); + const modifier = finishNode((createNode(modifierKind, modifierStart))); + modifiers = createNodeArray([modifier], modifierStart); + } + return modifiers; + } + function parseClassElement(): ClassElement { + if (token() === SyntaxKind.SemicolonToken) { + const result = (createNode(SyntaxKind.SemicolonClassElement)); + nextToken(); + return finishNode(result); + } + const node = (createNodeWithJSDoc(SyntaxKind.Unknown)); + node.decorators = parseDecorators(); + node.modifiers = parseModifiers(/*permitInvalidConstAsModifier*/ true); + if (parseContextualModifier(SyntaxKind.GetKeyword)) { + return parseAccessorDeclaration((node), SyntaxKind.GetAccessor); + } + if (parseContextualModifier(SyntaxKind.SetKeyword)) { + return parseAccessorDeclaration((node), SyntaxKind.SetAccessor); + } + if (token() === SyntaxKind.ConstructorKeyword || token() === SyntaxKind.StringLiteral) { + const constructorDeclaration = tryParseConstructorDeclaration((node)); + if (constructorDeclaration) { + return constructorDeclaration; + } + } + if (isIndexSignature()) { + return parseIndexSignatureDeclaration((node)); + } + // It is very important that we check this *after* checking indexers because + // the [ token can start an index signature or a computed property name + if (tokenIsIdentifierOrKeyword(token()) || + token() === SyntaxKind.StringLiteral || + token() === SyntaxKind.NumericLiteral || + token() === SyntaxKind.AsteriskToken || + token() === SyntaxKind.OpenBracketToken) { + const isAmbient = node.modifiers && some(node.modifiers, isDeclareModifier); + if (isAmbient) { + for (const m of node.modifiers!) { + m.flags |= NodeFlags.Ambient; + } + return doInsideOfContext(NodeFlags.Ambient, () => parsePropertyOrMethodDeclaration((node as PropertyDeclaration | MethodDeclaration))); } else { - node.members = createMissingList(); + return parsePropertyOrMethodDeclaration((node as PropertyDeclaration | MethodDeclaration)); } - - return finishNode(node); } - - function parseNameOfClassDeclarationOrExpression(): Identifier | undefined { - // implements is a future reserved word so - // 'class implements' might mean either - // - class expression with omitted name, 'implements' starts heritage clause - // - class with name 'implements' - // 'isImplementsClause' helps to disambiguate between these two cases - return isIdentifier() && !isImplementsClause() - ? parseIdentifier() - : undefined; - } - - function isImplementsClause() { - return token() === SyntaxKind.ImplementsKeyword && lookAhead(nextTokenIsIdentifierOrKeyword); - } - - function parseHeritageClauses(): NodeArray | undefined { + if (node.decorators || node.modifiers) { + // treat this as a property declaration with a missing name. + node.name = createMissingNode(SyntaxKind.Identifier, /*reportAtCurrentPosition*/ true, Diagnostics.Declaration_expected); + return parsePropertyDeclaration((node)); + } + // 'isClassMemberStart' should have hinted not to attempt parsing. + return Debug.fail("Should not have attempted to parse class member declaration."); + } + function parseClassExpression(): ClassExpression { + return parseClassDeclarationOrExpression((createNodeWithJSDoc(SyntaxKind.Unknown)), SyntaxKind.ClassExpression); + } + function parseClassDeclaration(node: ClassLikeDeclaration): ClassDeclaration { + return parseClassDeclarationOrExpression(node, SyntaxKind.ClassDeclaration); + } + function parseClassDeclarationOrExpression(node: ClassLikeDeclaration, kind: ClassLikeDeclaration["kind"]): ClassLikeDeclaration { + node.kind = kind; + parseExpected(SyntaxKind.ClassKeyword); + node.name = parseNameOfClassDeclarationOrExpression(); + node.typeParameters = parseTypeParameters(); + node.heritageClauses = parseHeritageClauses(); + if (parseExpected(SyntaxKind.OpenBraceToken)) { // ClassTail[Yield,Await] : (Modified) See 14.5 // ClassHeritage[?Yield,?Await]opt { ClassBody[?Yield,?Await]opt } - - if (isHeritageClause()) { - return parseList(ParsingContext.HeritageClauses, parseHeritageClause); - } - - return undefined; + node.members = parseClassMembers(); + parseExpected(SyntaxKind.CloseBraceToken); } - - function parseHeritageClause(): HeritageClause { - const tok = token(); - Debug.assert(tok === SyntaxKind.ExtendsKeyword || tok === SyntaxKind.ImplementsKeyword); // isListElement() should ensure this. - const node = createNode(SyntaxKind.HeritageClause); - node.token = tok; - nextToken(); - node.types = parseDelimitedList(ParsingContext.HeritageClauseElement, parseExpressionWithTypeArguments); - return finishNode(node); + else { + node.members = createMissingList(); } - - function parseExpressionWithTypeArguments(): ExpressionWithTypeArguments { - const node = createNode(SyntaxKind.ExpressionWithTypeArguments); - node.expression = parseLeftHandSideExpressionOrHigher(); - node.typeArguments = tryParseTypeArguments(); - return finishNode(node); + return finishNode(node); + } + function parseNameOfClassDeclarationOrExpression(): Identifier | undefined { + // implements is a future reserved word so + // 'class implements' might mean either + // - class expression with omitted name, 'implements' starts heritage clause + // - class with name 'implements' + // 'isImplementsClause' helps to disambiguate between these two cases + return isIdentifier() && !isImplementsClause() + ? parseIdentifier() + : undefined; + } + function isImplementsClause() { + return token() === SyntaxKind.ImplementsKeyword && lookAhead(nextTokenIsIdentifierOrKeyword); + } + function parseHeritageClauses(): NodeArray | undefined { + // ClassTail[Yield,Await] : (Modified) See 14.5 + // ClassHeritage[?Yield,?Await]opt { ClassBody[?Yield,?Await]opt } + if (isHeritageClause()) { + return parseList(ParsingContext.HeritageClauses, parseHeritageClause); } - - function tryParseTypeArguments(): NodeArray | undefined { - return token() === SyntaxKind.LessThanToken ? - parseBracketedList(ParsingContext.TypeArguments, parseType, SyntaxKind.LessThanToken, SyntaxKind.GreaterThanToken) : undefined; + return undefined; + } + function parseHeritageClause(): HeritageClause { + const tok = token(); + Debug.assert(tok === SyntaxKind.ExtendsKeyword || tok === SyntaxKind.ImplementsKeyword); // isListElement() should ensure this. + const node = (createNode(SyntaxKind.HeritageClause)); + node.token = tok; + nextToken(); + node.types = parseDelimitedList(ParsingContext.HeritageClauseElement, parseExpressionWithTypeArguments); + return finishNode(node); + } + function parseExpressionWithTypeArguments(): ExpressionWithTypeArguments { + const node = (createNode(SyntaxKind.ExpressionWithTypeArguments)); + node.expression = parseLeftHandSideExpressionOrHigher(); + node.typeArguments = tryParseTypeArguments(); + return finishNode(node); + } + function tryParseTypeArguments(): NodeArray | undefined { + return token() === SyntaxKind.LessThanToken ? + parseBracketedList(ParsingContext.TypeArguments, parseType, SyntaxKind.LessThanToken, SyntaxKind.GreaterThanToken) : undefined; + } + function isHeritageClause(): boolean { + return token() === SyntaxKind.ExtendsKeyword || token() === SyntaxKind.ImplementsKeyword; + } + function parseClassMembers(): NodeArray { + return parseList(ParsingContext.ClassMembers, parseClassElement); + } + function parseInterfaceDeclaration(node: InterfaceDeclaration): InterfaceDeclaration { + node.kind = SyntaxKind.InterfaceDeclaration; + parseExpected(SyntaxKind.InterfaceKeyword); + node.name = parseIdentifier(); + node.typeParameters = parseTypeParameters(); + node.heritageClauses = parseHeritageClauses(); + node.members = parseObjectTypeMembers(); + return finishNode(node); + } + function parseTypeAliasDeclaration(node: TypeAliasDeclaration): TypeAliasDeclaration { + node.kind = SyntaxKind.TypeAliasDeclaration; + parseExpected(SyntaxKind.TypeKeyword); + node.name = parseIdentifier(); + node.typeParameters = parseTypeParameters(); + parseExpected(SyntaxKind.EqualsToken); + node.type = parseType(); + parseSemicolon(); + return finishNode(node); + } + // In an ambient declaration, the grammar only allows integer literals as initializers. + // In a non-ambient declaration, the grammar allows uninitialized members only in a + // ConstantEnumMemberSection, which starts at the beginning of an enum declaration + // or any time an integer literal initializer is encountered. + function parseEnumMember(): EnumMember { + const node = (createNodeWithJSDoc(SyntaxKind.EnumMember)); + node.name = parsePropertyName(); + node.initializer = allowInAnd(parseInitializer); + return finishNode(node); + } + function parseEnumDeclaration(node: EnumDeclaration): EnumDeclaration { + node.kind = SyntaxKind.EnumDeclaration; + parseExpected(SyntaxKind.EnumKeyword); + node.name = parseIdentifier(); + if (parseExpected(SyntaxKind.OpenBraceToken)) { + node.members = doOutsideOfYieldAndAwaitContext(() => parseDelimitedList(ParsingContext.EnumMembers, parseEnumMember)); + parseExpected(SyntaxKind.CloseBraceToken); } - - function isHeritageClause(): boolean { - return token() === SyntaxKind.ExtendsKeyword || token() === SyntaxKind.ImplementsKeyword; + else { + node.members = createMissingList(); } - - function parseClassMembers(): NodeArray { - return parseList(ParsingContext.ClassMembers, parseClassElement); + return finishNode(node); + } + function parseModuleBlock(): ModuleBlock { + const node = (createNode(SyntaxKind.ModuleBlock)); + if (parseExpected(SyntaxKind.OpenBraceToken)) { + node.statements = parseList(ParsingContext.BlockStatements, parseStatement); + parseExpected(SyntaxKind.CloseBraceToken); } - - function parseInterfaceDeclaration(node: InterfaceDeclaration): InterfaceDeclaration { - node.kind = SyntaxKind.InterfaceDeclaration; - parseExpected(SyntaxKind.InterfaceKeyword); - node.name = parseIdentifier(); - node.typeParameters = parseTypeParameters(); - node.heritageClauses = parseHeritageClauses(); - node.members = parseObjectTypeMembers(); - return finishNode(node); + else { + node.statements = createMissingList(); } - - function parseTypeAliasDeclaration(node: TypeAliasDeclaration): TypeAliasDeclaration { - node.kind = SyntaxKind.TypeAliasDeclaration; - parseExpected(SyntaxKind.TypeKeyword); + return finishNode(node); + } + function parseModuleOrNamespaceDeclaration(node: ModuleDeclaration, flags: NodeFlags): ModuleDeclaration { + node.kind = SyntaxKind.ModuleDeclaration; + // If we are parsing a dotted namespace name, we want to + // propagate the 'Namespace' flag across the names if set. + const namespaceFlag = flags & NodeFlags.Namespace; + node.flags |= flags; + node.name = parseIdentifier(); + node.body = parseOptional(SyntaxKind.DotToken) + ? parseModuleOrNamespaceDeclaration((createNode(SyntaxKind.Unknown)), NodeFlags.NestedNamespace | namespaceFlag) + : parseModuleBlock(); + return finishNode(node); + } + function parseAmbientExternalModuleDeclaration(node: ModuleDeclaration): ModuleDeclaration { + node.kind = SyntaxKind.ModuleDeclaration; + if (token() === SyntaxKind.GlobalKeyword) { + // parse 'global' as name of global scope augmentation node.name = parseIdentifier(); - node.typeParameters = parseTypeParameters(); - parseExpected(SyntaxKind.EqualsToken); - node.type = parseType(); - parseSemicolon(); - return finishNode(node); + node.flags |= NodeFlags.GlobalAugmentation; } - - // In an ambient declaration, the grammar only allows integer literals as initializers. - // In a non-ambient declaration, the grammar allows uninitialized members only in a - // ConstantEnumMemberSection, which starts at the beginning of an enum declaration - // or any time an integer literal initializer is encountered. - function parseEnumMember(): EnumMember { - const node = createNodeWithJSDoc(SyntaxKind.EnumMember); - node.name = parsePropertyName(); - node.initializer = allowInAnd(parseInitializer); - return finishNode(node); + else { + node.name = (parseLiteralNode()); + node.name.text = internIdentifier(node.name.text); } - - function parseEnumDeclaration(node: EnumDeclaration): EnumDeclaration { - node.kind = SyntaxKind.EnumDeclaration; - parseExpected(SyntaxKind.EnumKeyword); - node.name = parseIdentifier(); - if (parseExpected(SyntaxKind.OpenBraceToken)) { - node.members = doOutsideOfYieldAndAwaitContext(() => parseDelimitedList(ParsingContext.EnumMembers, parseEnumMember)); - parseExpected(SyntaxKind.CloseBraceToken); - } - else { - node.members = createMissingList(); - } - return finishNode(node); + if (token() === SyntaxKind.OpenBraceToken) { + node.body = parseModuleBlock(); } - - function parseModuleBlock(): ModuleBlock { - const node = createNode(SyntaxKind.ModuleBlock); - if (parseExpected(SyntaxKind.OpenBraceToken)) { - node.statements = parseList(ParsingContext.BlockStatements, parseStatement); - parseExpected(SyntaxKind.CloseBraceToken); - } - else { - node.statements = createMissingList(); - } - return finishNode(node); + else { + parseSemicolon(); } - - function parseModuleOrNamespaceDeclaration(node: ModuleDeclaration, flags: NodeFlags): ModuleDeclaration { - node.kind = SyntaxKind.ModuleDeclaration; - // If we are parsing a dotted namespace name, we want to - // propagate the 'Namespace' flag across the names if set. - const namespaceFlag = flags & NodeFlags.Namespace; - node.flags |= flags; - node.name = parseIdentifier(); - node.body = parseOptional(SyntaxKind.DotToken) - ? parseModuleOrNamespaceDeclaration(createNode(SyntaxKind.Unknown), NodeFlags.NestedNamespace | namespaceFlag) - : parseModuleBlock(); - return finishNode(node); + return finishNode(node); + } + function parseModuleDeclaration(node: ModuleDeclaration): ModuleDeclaration { + let flags: NodeFlags = 0; + if (token() === SyntaxKind.GlobalKeyword) { + // global augmentation + return parseAmbientExternalModuleDeclaration(node); } - - function parseAmbientExternalModuleDeclaration(node: ModuleDeclaration): ModuleDeclaration { - node.kind = SyntaxKind.ModuleDeclaration; - if (token() === SyntaxKind.GlobalKeyword) { - // parse 'global' as name of global scope augmentation - node.name = parseIdentifier(); - node.flags |= NodeFlags.GlobalAugmentation; - } - else { - node.name = parseLiteralNode(); - node.name.text = internIdentifier(node.name.text); - } - if (token() === SyntaxKind.OpenBraceToken) { - node.body = parseModuleBlock(); - } - else { - parseSemicolon(); - } - return finishNode(node); + else if (parseOptional(SyntaxKind.NamespaceKeyword)) { + flags |= NodeFlags.Namespace; } - - function parseModuleDeclaration(node: ModuleDeclaration): ModuleDeclaration { - let flags: NodeFlags = 0; - if (token() === SyntaxKind.GlobalKeyword) { - // global augmentation + else { + parseExpected(SyntaxKind.ModuleKeyword); + if (token() === SyntaxKind.StringLiteral) { return parseAmbientExternalModuleDeclaration(node); } - else if (parseOptional(SyntaxKind.NamespaceKeyword)) { - flags |= NodeFlags.Namespace; - } - else { - parseExpected(SyntaxKind.ModuleKeyword); - if (token() === SyntaxKind.StringLiteral) { - return parseAmbientExternalModuleDeclaration(node); - } - } - return parseModuleOrNamespaceDeclaration(node, flags); } - - function isExternalModuleReference() { - return token() === SyntaxKind.RequireKeyword && - lookAhead(nextTokenIsOpenParen); - } - - function nextTokenIsOpenParen() { - return nextToken() === SyntaxKind.OpenParenToken; + return parseModuleOrNamespaceDeclaration(node, flags); + } + function isExternalModuleReference() { + return token() === SyntaxKind.RequireKeyword && + lookAhead(nextTokenIsOpenParen); + } + function nextTokenIsOpenParen() { + return nextToken() === SyntaxKind.OpenParenToken; + } + function nextTokenIsSlash() { + return nextToken() === SyntaxKind.SlashToken; + } + function parseNamespaceExportDeclaration(node: NamespaceExportDeclaration): NamespaceExportDeclaration { + node.kind = SyntaxKind.NamespaceExportDeclaration; + parseExpected(SyntaxKind.AsKeyword); + parseExpected(SyntaxKind.NamespaceKeyword); + node.name = parseIdentifier(); + parseSemicolon(); + return finishNode(node); + } + function parseImportDeclarationOrImportEqualsDeclaration(node: ImportEqualsDeclaration | ImportDeclaration): ImportEqualsDeclaration | ImportDeclaration { + parseExpected(SyntaxKind.ImportKeyword); + const afterImportPos = scanner.getStartPos(); + let identifier: Identifier | undefined; + if (isIdentifier()) { + identifier = parseIdentifier(); + } + let isTypeOnly = false; + if (token() !== SyntaxKind.FromKeyword && + identifier?.escapedText === "type" && + (isIdentifier() || tokenAfterImportDefinitelyProducesImportDeclaration())) { + isTypeOnly = true; + identifier = isIdentifier() ? parseIdentifier() : undefined; + } + if (identifier && !tokenAfterImportedIdentifierDefinitelyProducesImportDeclaration()) { + return parseImportEqualsDeclaration((node), identifier, isTypeOnly); + } + // Import statement + node.kind = SyntaxKind.ImportDeclaration; + // ImportDeclaration: + // import ImportClause from ModuleSpecifier ; + // import ModuleSpecifier; + if (identifier || // import id + token() === SyntaxKind.AsteriskToken || // import * + token() === SyntaxKind.OpenBraceToken // import { + ) { + (node).importClause = parseImportClause(identifier, afterImportPos, isTypeOnly); + parseExpected(SyntaxKind.FromKeyword); + } + (node).moduleSpecifier = parseModuleSpecifier(); + parseSemicolon(); + return finishNode(node); + } + function tokenAfterImportDefinitelyProducesImportDeclaration() { + return token() === SyntaxKind.AsteriskToken || token() === SyntaxKind.OpenBraceToken; + } + function tokenAfterImportedIdentifierDefinitelyProducesImportDeclaration() { + // In `import id ___`, the current token decides whether to produce + // an ImportDeclaration or ImportEqualsDeclaration. + return token() === SyntaxKind.CommaToken || token() === SyntaxKind.FromKeyword; + } + function parseImportEqualsDeclaration(node: ImportEqualsDeclaration, identifier: Identifier, isTypeOnly: boolean): ImportEqualsDeclaration { + node.kind = SyntaxKind.ImportEqualsDeclaration; + node.name = identifier; + parseExpected(SyntaxKind.EqualsToken); + node.moduleReference = parseModuleReference(); + parseSemicolon(); + const finished = finishNode(node); + if (isTypeOnly) { + parseErrorAtRange(finished, Diagnostics.Only_ECMAScript_imports_may_use_import_type); + } + return finished; + } + function parseImportClause(identifier: Identifier | undefined, fullStart: number, isTypeOnly: boolean) { + // ImportClause: + // ImportedDefaultBinding + // NameSpaceImport + // NamedImports + // ImportedDefaultBinding, NameSpaceImport + // ImportedDefaultBinding, NamedImports + const importClause = (createNode(SyntaxKind.ImportClause, fullStart)); + importClause.isTypeOnly = isTypeOnly; + if (identifier) { + // ImportedDefaultBinding: + // ImportedBinding + importClause.name = identifier; + } + // If there was no default import or if there is comma token after default import + // parse namespace or named imports + if (!importClause.name || + parseOptional(SyntaxKind.CommaToken)) { + importClause.namedBindings = token() === SyntaxKind.AsteriskToken ? parseNamespaceImport() : parseNamedImportsOrExports(SyntaxKind.NamedImports); + } + return finishNode(importClause); + } + function parseModuleReference() { + return isExternalModuleReference() + ? parseExternalModuleReference() + : parseEntityName(/*allowReservedWords*/ false); + } + function parseExternalModuleReference() { + const node = (createNode(SyntaxKind.ExternalModuleReference)); + parseExpected(SyntaxKind.RequireKeyword); + parseExpected(SyntaxKind.OpenParenToken); + node.expression = parseModuleSpecifier(); + parseExpected(SyntaxKind.CloseParenToken); + return finishNode(node); + } + function parseModuleSpecifier(): Expression { + if (token() === SyntaxKind.StringLiteral) { + const result = parseLiteralNode(); + result.text = internIdentifier(result.text); + return result; } - - function nextTokenIsSlash() { - return nextToken() === SyntaxKind.SlashToken; + else { + // We allow arbitrary expressions here, even though the grammar only allows string + // literals. We check to ensure that it is only a string literal later in the grammar + // check pass. + return parseExpression(); } - - function parseNamespaceExportDeclaration(node: NamespaceExportDeclaration): NamespaceExportDeclaration { - node.kind = SyntaxKind.NamespaceExportDeclaration; + } + function parseNamespaceImport(): NamespaceImport { + // NameSpaceImport: + // * as ImportedBinding + const namespaceImport = (createNode(SyntaxKind.NamespaceImport)); + parseExpected(SyntaxKind.AsteriskToken); + parseExpected(SyntaxKind.AsKeyword); + namespaceImport.name = parseIdentifier(); + return finishNode(namespaceImport); + } + function parseNamedImportsOrExports(kind: SyntaxKind.NamedImports): NamedImports; + function parseNamedImportsOrExports(kind: SyntaxKind.NamedExports): NamedExports; + function parseNamedImportsOrExports(kind: SyntaxKind): NamedImportsOrExports { + const node = (createNode(kind)); + // NamedImports: + // { } + // { ImportsList } + // { ImportsList, } + // ImportsList: + // ImportSpecifier + // ImportsList, ImportSpecifier + node.elements = ( | NodeArray>parseBracketedList(ParsingContext.ImportOrExportSpecifiers, kind === SyntaxKind.NamedImports ? parseImportSpecifier : parseExportSpecifier, SyntaxKind.OpenBraceToken, SyntaxKind.CloseBraceToken)); + return finishNode(node); + } + function parseExportSpecifier() { + return parseImportOrExportSpecifier(SyntaxKind.ExportSpecifier); + } + function parseImportSpecifier() { + return parseImportOrExportSpecifier(SyntaxKind.ImportSpecifier); + } + function parseImportOrExportSpecifier(kind: SyntaxKind): ImportOrExportSpecifier { + const node = (createNode(kind)); + // ImportSpecifier: + // BindingIdentifier + // IdentifierName as BindingIdentifier + // ExportSpecifier: + // IdentifierName + // IdentifierName as IdentifierName + let checkIdentifierIsKeyword = isKeyword(token()) && !isIdentifier(); + let checkIdentifierStart = scanner.getTokenPos(); + let checkIdentifierEnd = scanner.getTextPos(); + const identifierName = parseIdentifierName(); + if (token() === SyntaxKind.AsKeyword) { + node.propertyName = identifierName; parseExpected(SyntaxKind.AsKeyword); - parseExpected(SyntaxKind.NamespaceKeyword); - node.name = parseIdentifier(); - parseSemicolon(); - return finishNode(node); - } - - function parseImportDeclarationOrImportEqualsDeclaration(node: ImportEqualsDeclaration | ImportDeclaration): ImportEqualsDeclaration | ImportDeclaration { - parseExpected(SyntaxKind.ImportKeyword); - const afterImportPos = scanner.getStartPos(); - - let identifier: Identifier | undefined; - if (isIdentifier()) { - identifier = parseIdentifier(); - } - - let isTypeOnly = false; - if (token() !== SyntaxKind.FromKeyword && - identifier?.escapedText === "type" && - (isIdentifier() || tokenAfterImportDefinitelyProducesImportDeclaration()) - ) { - isTypeOnly = true; - identifier = isIdentifier() ? parseIdentifier() : undefined; - } - - if (identifier && !tokenAfterImportedIdentifierDefinitelyProducesImportDeclaration()) { - return parseImportEqualsDeclaration(node, identifier, isTypeOnly); - } - - // Import statement - node.kind = SyntaxKind.ImportDeclaration; - // ImportDeclaration: - // import ImportClause from ModuleSpecifier ; - // import ModuleSpecifier; - if (identifier || // import id - token() === SyntaxKind.AsteriskToken || // import * - token() === SyntaxKind.OpenBraceToken // import { - ) { - (node).importClause = parseImportClause(identifier, afterImportPos, isTypeOnly); - parseExpected(SyntaxKind.FromKeyword); - } - - (node).moduleSpecifier = parseModuleSpecifier(); - parseSemicolon(); - return finishNode(node); + checkIdentifierIsKeyword = isKeyword(token()) && !isIdentifier(); + checkIdentifierStart = scanner.getTokenPos(); + checkIdentifierEnd = scanner.getTextPos(); + node.name = parseIdentifierName(); } - - function tokenAfterImportDefinitelyProducesImportDeclaration() { - return token() === SyntaxKind.AsteriskToken || token() === SyntaxKind.OpenBraceToken; - } - - function tokenAfterImportedIdentifierDefinitelyProducesImportDeclaration() { - // In `import id ___`, the current token decides whether to produce - // an ImportDeclaration or ImportEqualsDeclaration. - return token() === SyntaxKind.CommaToken || token() === SyntaxKind.FromKeyword; - } - - function parseImportEqualsDeclaration(node: ImportEqualsDeclaration, identifier: Identifier, isTypeOnly: boolean): ImportEqualsDeclaration { - node.kind = SyntaxKind.ImportEqualsDeclaration; - node.name = identifier; - parseExpected(SyntaxKind.EqualsToken); - node.moduleReference = parseModuleReference(); - parseSemicolon(); - const finished = finishNode(node); - if (isTypeOnly) { - parseErrorAtRange(finished, Diagnostics.Only_ECMAScript_imports_may_use_import_type); - } - return finished; - } - - function parseImportClause(identifier: Identifier | undefined, fullStart: number, isTypeOnly: boolean) { - // ImportClause: - // ImportedDefaultBinding - // NameSpaceImport - // NamedImports - // ImportedDefaultBinding, NameSpaceImport - // ImportedDefaultBinding, NamedImports - - const importClause = createNode(SyntaxKind.ImportClause, fullStart); - importClause.isTypeOnly = isTypeOnly; - - if (identifier) { - // ImportedDefaultBinding: - // ImportedBinding - importClause.name = identifier; - } - - // If there was no default import or if there is comma token after default import - // parse namespace or named imports - if (!importClause.name || - parseOptional(SyntaxKind.CommaToken)) { - importClause.namedBindings = token() === SyntaxKind.AsteriskToken ? parseNamespaceImport() : parseNamedImportsOrExports(SyntaxKind.NamedImports); - } - - return finishNode(importClause); - } - - function parseModuleReference() { - return isExternalModuleReference() - ? parseExternalModuleReference() - : parseEntityName(/*allowReservedWords*/ false); - } - - function parseExternalModuleReference() { - const node = createNode(SyntaxKind.ExternalModuleReference); - parseExpected(SyntaxKind.RequireKeyword); - parseExpected(SyntaxKind.OpenParenToken); - node.expression = parseModuleSpecifier(); - parseExpected(SyntaxKind.CloseParenToken); - return finishNode(node); + else { + node.name = identifierName; } - - function parseModuleSpecifier(): Expression { - if (token() === SyntaxKind.StringLiteral) { - const result = parseLiteralNode(); - result.text = internIdentifier(result.text); - return result; - } - else { - // We allow arbitrary expressions here, even though the grammar only allows string - // literals. We check to ensure that it is only a string literal later in the grammar - // check pass. - return parseExpression(); - } - } - - function parseNamespaceImport(): NamespaceImport { - // NameSpaceImport: - // * as ImportedBinding - const namespaceImport = createNode(SyntaxKind.NamespaceImport); - parseExpected(SyntaxKind.AsteriskToken); - parseExpected(SyntaxKind.AsKeyword); - namespaceImport.name = parseIdentifier(); - return finishNode(namespaceImport); - } - - function parseNamedImportsOrExports(kind: SyntaxKind.NamedImports): NamedImports; - function parseNamedImportsOrExports(kind: SyntaxKind.NamedExports): NamedExports; - function parseNamedImportsOrExports(kind: SyntaxKind): NamedImportsOrExports { - const node = createNode(kind); - - // NamedImports: - // { } - // { ImportsList } - // { ImportsList, } - - // ImportsList: - // ImportSpecifier - // ImportsList, ImportSpecifier - node.elements = | NodeArray>parseBracketedList(ParsingContext.ImportOrExportSpecifiers, - kind === SyntaxKind.NamedImports ? parseImportSpecifier : parseExportSpecifier, - SyntaxKind.OpenBraceToken, SyntaxKind.CloseBraceToken); - return finishNode(node); + if (kind === SyntaxKind.ImportSpecifier && checkIdentifierIsKeyword) { + parseErrorAt(checkIdentifierStart, checkIdentifierEnd, Diagnostics.Identifier_expected); } - - function parseExportSpecifier() { - return parseImportOrExportSpecifier(SyntaxKind.ExportSpecifier); - } - - function parseImportSpecifier() { - return parseImportOrExportSpecifier(SyntaxKind.ImportSpecifier); - } - - function parseImportOrExportSpecifier(kind: SyntaxKind): ImportOrExportSpecifier { - const node = createNode(kind); - // ImportSpecifier: - // BindingIdentifier - // IdentifierName as BindingIdentifier - // ExportSpecifier: - // IdentifierName - // IdentifierName as IdentifierName - let checkIdentifierIsKeyword = isKeyword(token()) && !isIdentifier(); - let checkIdentifierStart = scanner.getTokenPos(); - let checkIdentifierEnd = scanner.getTextPos(); - const identifierName = parseIdentifierName(); - if (token() === SyntaxKind.AsKeyword) { - node.propertyName = identifierName; - parseExpected(SyntaxKind.AsKeyword); - checkIdentifierIsKeyword = isKeyword(token()) && !isIdentifier(); - checkIdentifierStart = scanner.getTokenPos(); - checkIdentifierEnd = scanner.getTextPos(); - node.name = parseIdentifierName(); - } - else { - node.name = identifierName; - } - if (kind === SyntaxKind.ImportSpecifier && checkIdentifierIsKeyword) { - parseErrorAt(checkIdentifierStart, checkIdentifierEnd, Diagnostics.Identifier_expected); + return finishNode(node); + } + function parseNamespaceExport(pos: number): NamespaceExport { + const node = (createNode(SyntaxKind.NamespaceExport, pos)); + node.name = parseIdentifier(); + return finishNode(node); + } + function parseExportDeclaration(node: ExportDeclaration): ExportDeclaration { + node.kind = SyntaxKind.ExportDeclaration; + node.isTypeOnly = parseOptional(SyntaxKind.TypeKeyword); + const namespaceExportPos = scanner.getStartPos(); + if (parseOptional(SyntaxKind.AsteriskToken)) { + if (parseOptional(SyntaxKind.AsKeyword)) { + node.exportClause = parseNamespaceExport(namespaceExportPos); } - return finishNode(node); + parseExpected(SyntaxKind.FromKeyword); + node.moduleSpecifier = parseModuleSpecifier(); } - - function parseNamespaceExport(pos: number): NamespaceExport { - const node = createNode(SyntaxKind.NamespaceExport, pos); - node.name = parseIdentifier(); - return finishNode(node); - } - - function parseExportDeclaration(node: ExportDeclaration): ExportDeclaration { - node.kind = SyntaxKind.ExportDeclaration; - node.isTypeOnly = parseOptional(SyntaxKind.TypeKeyword); - const namespaceExportPos = scanner.getStartPos(); - if (parseOptional(SyntaxKind.AsteriskToken)) { - if (parseOptional(SyntaxKind.AsKeyword)) { - node.exportClause = parseNamespaceExport(namespaceExportPos); - } + else { + node.exportClause = parseNamedImportsOrExports(SyntaxKind.NamedExports); + // It is not uncommon to accidentally omit the 'from' keyword. Additionally, in editing scenarios, + // the 'from' keyword can be parsed as a named export when the export clause is unterminated (i.e. `export { from "moduleName";`) + // If we don't have a 'from' keyword, see if we have a string literal such that ASI won't take effect. + if (token() === SyntaxKind.FromKeyword || (token() === SyntaxKind.StringLiteral && !scanner.hasPrecedingLineBreak())) { parseExpected(SyntaxKind.FromKeyword); node.moduleSpecifier = parseModuleSpecifier(); } - else { - node.exportClause = parseNamedImportsOrExports(SyntaxKind.NamedExports); - // It is not uncommon to accidentally omit the 'from' keyword. Additionally, in editing scenarios, - // the 'from' keyword can be parsed as a named export when the export clause is unterminated (i.e. `export { from "moduleName";`) - // If we don't have a 'from' keyword, see if we have a string literal such that ASI won't take effect. - if (token() === SyntaxKind.FromKeyword || (token() === SyntaxKind.StringLiteral && !scanner.hasPrecedingLineBreak())) { - parseExpected(SyntaxKind.FromKeyword); - node.moduleSpecifier = parseModuleSpecifier(); - } - } - parseSemicolon(); - return finishNode(node); } - - function parseExportAssignment(node: ExportAssignment): ExportAssignment { - node.kind = SyntaxKind.ExportAssignment; - if (parseOptional(SyntaxKind.EqualsToken)) { - node.isExportEquals = true; - } - else { - parseExpected(SyntaxKind.DefaultKeyword); + parseSemicolon(); + return finishNode(node); + } + function parseExportAssignment(node: ExportAssignment): ExportAssignment { + node.kind = SyntaxKind.ExportAssignment; + if (parseOptional(SyntaxKind.EqualsToken)) { + node.isExportEquals = true; + } + else { + parseExpected(SyntaxKind.DefaultKeyword); + } + node.expression = parseAssignmentExpressionOrHigher(); + parseSemicolon(); + return finishNode(node); + } + function setExternalModuleIndicator(sourceFile: SourceFile) { + // Try to use the first top-level import/export when available, then + // fall back to looking for an 'import.meta' somewhere in the tree if necessary. + sourceFile.externalModuleIndicator = + forEach(sourceFile.statements, isAnExternalModuleIndicatorNode) || + getImportMetaIfNecessary(sourceFile); + } + function isAnExternalModuleIndicatorNode(node: Node) { + return hasModifierOfKind(node, SyntaxKind.ExportKeyword) + || node.kind === SyntaxKind.ImportEqualsDeclaration && (node).moduleReference.kind === SyntaxKind.ExternalModuleReference + || node.kind === SyntaxKind.ImportDeclaration + || node.kind === SyntaxKind.ExportAssignment + || node.kind === SyntaxKind.ExportDeclaration ? node : undefined; + } + function getImportMetaIfNecessary(sourceFile: SourceFile) { + return sourceFile.flags & NodeFlags.PossiblyContainsImportMeta ? + walkTreeForExternalModuleIndicators(sourceFile) : + undefined; + } + function walkTreeForExternalModuleIndicators(node: Node): Node | undefined { + return isImportMeta(node) ? node : forEachChild(node, walkTreeForExternalModuleIndicators); + } + /** Do not use hasModifier inside the parser; it relies on parent pointers. Use this instead. */ + function hasModifierOfKind(node: Node, kind: SyntaxKind) { + return some(node.modifiers, m => m.kind === kind); + } + function isImportMeta(node: Node): boolean { + return isMetaProperty(node) && node.keywordToken === SyntaxKind.ImportKeyword && node.name.escapedText === "meta"; + } + const enum ParsingContext { + SourceElements, + BlockStatements, + SwitchClauses, + SwitchClauseStatements, + TypeMembers, + ClassMembers, + EnumMembers, + HeritageClauseElement, + VariableDeclarations, + ObjectBindingElements, + ArrayBindingElements, + ArgumentExpressions, + ObjectLiteralMembers, + JsxAttributes, + JsxChildren, + ArrayLiteralMembers, + Parameters, + JSDocParameters, + RestProperties, + TypeParameters, + TypeArguments, + TupleElementTypes, + HeritageClauses, + ImportOrExportSpecifiers, + Count // Number of parsing contexts + } + const enum Tristate { + False, + True, + Unknown + } + export namespace JSDocParser { + export function parseJSDocTypeExpressionForTests(content: string, start: number | undefined, length: number | undefined): { + jsDocTypeExpression: JSDocTypeExpression; + diagnostics: Diagnostic[]; + } | undefined { + initializeState(content, ScriptTarget.Latest, /*_syntaxCursor:*/ undefined, ScriptKind.JS); + sourceFile = createSourceFile("file.js", ScriptTarget.Latest, ScriptKind.JS, /*isDeclarationFile*/ false); + scanner.setText(content, start, length); + currentToken = scanner.scan(); + const jsDocTypeExpression = parseJSDocTypeExpression(); + const diagnostics = parseDiagnostics; + clearState(); + return jsDocTypeExpression ? { jsDocTypeExpression, diagnostics } : undefined; + } + // Parses out a JSDoc type expression. + export function parseJSDocTypeExpression(mayOmitBraces?: boolean): JSDocTypeExpression { + const result = (createNode(SyntaxKind.JSDocTypeExpression)); + const hasBrace = (mayOmitBraces ? parseOptional : parseExpected)(SyntaxKind.OpenBraceToken); + result.type = doInsideOfContext(NodeFlags.JSDoc, parseJSDocType); + if (!mayOmitBraces || hasBrace) { + parseExpectedJSDoc(SyntaxKind.CloseBraceToken); } - node.expression = parseAssignmentExpressionOrHigher(); - parseSemicolon(); - return finishNode(node); + fixupParentReferences(result); + return finishNode(result); } - - function setExternalModuleIndicator(sourceFile: SourceFile) { - // Try to use the first top-level import/export when available, then - // fall back to looking for an 'import.meta' somewhere in the tree if necessary. - sourceFile.externalModuleIndicator = - forEach(sourceFile.statements, isAnExternalModuleIndicatorNode) || - getImportMetaIfNecessary(sourceFile); - } - - function isAnExternalModuleIndicatorNode(node: Node) { - return hasModifierOfKind(node, SyntaxKind.ExportKeyword) - || node.kind === SyntaxKind.ImportEqualsDeclaration && (node).moduleReference.kind === SyntaxKind.ExternalModuleReference - || node.kind === SyntaxKind.ImportDeclaration - || node.kind === SyntaxKind.ExportAssignment - || node.kind === SyntaxKind.ExportDeclaration ? node : undefined; - } - - function getImportMetaIfNecessary(sourceFile: SourceFile) { - return sourceFile.flags & NodeFlags.PossiblyContainsImportMeta ? - walkTreeForExternalModuleIndicators(sourceFile) : - undefined; - } - - function walkTreeForExternalModuleIndicators(node: Node): Node | undefined { - return isImportMeta(node) ? node : forEachChild(node, walkTreeForExternalModuleIndicators); - } - - /** Do not use hasModifier inside the parser; it relies on parent pointers. Use this instead. */ - function hasModifierOfKind(node: Node, kind: SyntaxKind) { - return some(node.modifiers, m => m.kind === kind); - } - - function isImportMeta(node: Node): boolean { - return isMetaProperty(node) && node.keywordToken === SyntaxKind.ImportKeyword && node.name.escapedText === "meta"; - } - - const enum ParsingContext { - SourceElements, // Elements in source file - BlockStatements, // Statements in block - SwitchClauses, // Clauses in switch statement - SwitchClauseStatements, // Statements in switch clause - TypeMembers, // Members in interface or type literal - ClassMembers, // Members in class declaration - EnumMembers, // Members in enum declaration - HeritageClauseElement, // Elements in a heritage clause - VariableDeclarations, // Variable declarations in variable statement - ObjectBindingElements, // Binding elements in object binding list - ArrayBindingElements, // Binding elements in array binding list - ArgumentExpressions, // Expressions in argument list - ObjectLiteralMembers, // Members in object literal - JsxAttributes, // Attributes in jsx element - JsxChildren, // Things between opening and closing JSX tags - ArrayLiteralMembers, // Members in array literal - Parameters, // Parameters in parameter list - JSDocParameters, // JSDoc parameters in parameter list of JSDoc function type - RestProperties, // Property names in a rest type list - TypeParameters, // Type parameters in type parameter list - TypeArguments, // Type arguments in type argument list - TupleElementTypes, // Element types in tuple element type list - HeritageClauses, // Heritage clauses for a class or interface declaration. - ImportOrExportSpecifiers, // Named import clause's import specifier list - Count // Number of parsing contexts - } - - const enum Tristate { - False, - True, - Unknown - } - - export namespace JSDocParser { - export function parseJSDocTypeExpressionForTests(content: string, start: number | undefined, length: number | undefined): { jsDocTypeExpression: JSDocTypeExpression, diagnostics: Diagnostic[] } | undefined { - initializeState(content, ScriptTarget.Latest, /*_syntaxCursor:*/ undefined, ScriptKind.JS); - sourceFile = createSourceFile("file.js", ScriptTarget.Latest, ScriptKind.JS, /*isDeclarationFile*/ false); - scanner.setText(content, start, length); - currentToken = scanner.scan(); - const jsDocTypeExpression = parseJSDocTypeExpression(); - const diagnostics = parseDiagnostics; - clearState(); - - return jsDocTypeExpression ? { jsDocTypeExpression, diagnostics } : undefined; - } - - // Parses out a JSDoc type expression. - export function parseJSDocTypeExpression(mayOmitBraces?: boolean): JSDocTypeExpression { - const result = createNode(SyntaxKind.JSDocTypeExpression); - - const hasBrace = (mayOmitBraces ? parseOptional : parseExpected)(SyntaxKind.OpenBraceToken); - result.type = doInsideOfContext(NodeFlags.JSDoc, parseJSDocType); - if (!mayOmitBraces || hasBrace) { - parseExpectedJSDoc(SyntaxKind.CloseBraceToken); - } - - fixupParentReferences(result); - return finishNode(result); + export function parseIsolatedJSDocComment(content: string, start: number | undefined, length: number | undefined): { + jsDoc: JSDoc; + diagnostics: Diagnostic[]; + } | undefined { + initializeState(content, ScriptTarget.Latest, /*_syntaxCursor:*/ undefined, ScriptKind.JS); + sourceFile = ({ languageVariant: LanguageVariant.Standard, text: content }); + const jsDoc = doInsideOfContext(NodeFlags.JSDoc, () => parseJSDocCommentWorker(start, length)); + const diagnostics = parseDiagnostics; + clearState(); + return jsDoc ? { jsDoc, diagnostics } : undefined; + } + export function parseJSDocComment(parent: HasJSDoc, start: number, length: number): JSDoc | undefined { + const saveToken = currentToken; + const saveParseDiagnosticsLength = parseDiagnostics.length; + const saveParseErrorBeforeNextFinishedNode = parseErrorBeforeNextFinishedNode; + const comment = doInsideOfContext(NodeFlags.JSDoc, () => parseJSDocCommentWorker(start, length)); + if (comment) { + comment.parent = parent; + } + if (contextFlags & NodeFlags.JavaScriptFile) { + if (!sourceFile.jsDocDiagnostics) { + sourceFile.jsDocDiagnostics = []; + } + sourceFile.jsDocDiagnostics.push(...parseDiagnostics); + } + currentToken = saveToken; + parseDiagnostics.length = saveParseDiagnosticsLength; + parseErrorBeforeNextFinishedNode = saveParseErrorBeforeNextFinishedNode; + return comment; + } + const enum JSDocState { + BeginningOfLine, + SawAsterisk, + SavingComments, + SavingBackticks + } + const enum PropertyLikeParse { + Property = 1 << 0, + Parameter = 1 << 1, + CallbackParameter = 1 << 2 + } + function parseJSDocCommentWorker(start = 0, length: number | undefined): JSDoc | undefined { + const content = sourceText; + const end = length === undefined ? content.length : start + length; + length = end - start; + Debug.assert(start >= 0); + Debug.assert(start <= end); + Debug.assert(end <= content.length); + // Check for /** (JSDoc opening part) + if (!isJSDocLikeText(content, start)) { + return undefined; } - - export function parseIsolatedJSDocComment(content: string, start: number | undefined, length: number | undefined): { jsDoc: JSDoc, diagnostics: Diagnostic[] } | undefined { - initializeState(content, ScriptTarget.Latest, /*_syntaxCursor:*/ undefined, ScriptKind.JS); - sourceFile = { languageVariant: LanguageVariant.Standard, text: content }; - const jsDoc = doInsideOfContext(NodeFlags.JSDoc, () => parseJSDocCommentWorker(start, length)); - const diagnostics = parseDiagnostics; - clearState(); - - return jsDoc ? { jsDoc, diagnostics } : undefined; - } - - export function parseJSDocComment(parent: HasJSDoc, start: number, length: number): JSDoc | undefined { - const saveToken = currentToken; - const saveParseDiagnosticsLength = parseDiagnostics.length; - const saveParseErrorBeforeNextFinishedNode = parseErrorBeforeNextFinishedNode; - - const comment = doInsideOfContext(NodeFlags.JSDoc, () => parseJSDocCommentWorker(start, length)); - if (comment) { - comment.parent = parent; - } - - if (contextFlags & NodeFlags.JavaScriptFile) { - if (!sourceFile.jsDocDiagnostics) { - sourceFile.jsDocDiagnostics = []; + let tags: JSDocTag[]; + let tagsPos: number; + let tagsEnd: number; + const comments: string[] = []; + // + 3 for leading /**, - 5 in total for /** */ + return scanner.scanRange(start + 3, length - 5, () => { + // Initially we can parse out a tag. We also have seen a starting asterisk. + // This is so that /** * @type */ doesn't parse. + let state = JSDocState.SawAsterisk; + let margin: number | undefined; + // + 4 for leading '/** ' + let indent = start - Math.max(content.lastIndexOf("\n", start), 0) + 4; + function pushComment(text: string) { + if (!margin) { + margin = indent; } - sourceFile.jsDocDiagnostics.push(...parseDiagnostics); + comments.push(text); + indent += text.length; } - currentToken = saveToken; - parseDiagnostics.length = saveParseDiagnosticsLength; - parseErrorBeforeNextFinishedNode = saveParseErrorBeforeNextFinishedNode; - - return comment; - } - - const enum JSDocState { - BeginningOfLine, - SawAsterisk, - SavingComments, - SavingBackticks, // NOTE: Only used when parsing tag comments - } - - const enum PropertyLikeParse { - Property = 1 << 0, - Parameter = 1 << 1, - CallbackParameter = 1 << 2, - } - - function parseJSDocCommentWorker(start = 0, length: number | undefined): JSDoc | undefined { - const content = sourceText; - const end = length === undefined ? content.length : start + length; - length = end - start; - - Debug.assert(start >= 0); - Debug.assert(start <= end); - Debug.assert(end <= content.length); - - // Check for /** (JSDoc opening part) - if (!isJSDocLikeText(content, start)) { - return undefined; + nextTokenJSDoc(); + while (parseOptionalJsdoc(SyntaxKind.WhitespaceTrivia)) + ; + if (parseOptionalJsdoc(SyntaxKind.NewLineTrivia)) { + state = JSDocState.BeginningOfLine; + indent = 0; } - - let tags: JSDocTag[]; - let tagsPos: number; - let tagsEnd: number; - const comments: string[] = []; - - // + 3 for leading /**, - 5 in total for /** */ - return scanner.scanRange(start + 3, length - 5, () => { - // Initially we can parse out a tag. We also have seen a starting asterisk. - // This is so that /** * @type */ doesn't parse. - let state = JSDocState.SawAsterisk; - let margin: number | undefined; - // + 4 for leading '/** ' - let indent = start - Math.max(content.lastIndexOf("\n", start), 0) + 4; - function pushComment(text: string) { - if (!margin) { - margin = indent; - } - comments.push(text); - indent += text.length; - } - - nextTokenJSDoc(); - while (parseOptionalJsdoc(SyntaxKind.WhitespaceTrivia)); - if (parseOptionalJsdoc(SyntaxKind.NewLineTrivia)) { - state = JSDocState.BeginningOfLine; - indent = 0; - } - loop: while (true) { - switch (token()) { - case SyntaxKind.AtToken: - if (state === JSDocState.BeginningOfLine || state === JSDocState.SawAsterisk) { - removeTrailingWhitespace(comments); - addTag(parseTag(indent)); - // NOTE: According to usejsdoc.org, a tag goes to end of line, except the last tag. - // Real-world comments may break this rule, so "BeginningOfLine" will not be a real line beginning - // for malformed examples like `/** @param {string} x @returns {number} the length */` - state = JSDocState.BeginningOfLine; - margin = undefined; - } - else { - pushComment(scanner.getTokenText()); - } - break; - case SyntaxKind.NewLineTrivia: - comments.push(scanner.getTokenText()); + loop: while (true) { + switch (token()) { + case SyntaxKind.AtToken: + if (state === JSDocState.BeginningOfLine || state === JSDocState.SawAsterisk) { + removeTrailingWhitespace(comments); + addTag(parseTag(indent)); + // NOTE: According to usejsdoc.org, a tag goes to end of line, except the last tag. + // Real-world comments may break this rule, so "BeginningOfLine" will not be a real line beginning + // for malformed examples like `/** @param {string} x @returns {number} the length */` state = JSDocState.BeginningOfLine; - indent = 0; - break; - case SyntaxKind.AsteriskToken: - const asterisk = scanner.getTokenText(); - if (state === JSDocState.SawAsterisk || state === JSDocState.SavingComments) { - // If we've already seen an asterisk, then we can no longer parse a tag on this line - state = JSDocState.SavingComments; - pushComment(asterisk); - } - else { - // Ignore the first asterisk on a line - state = JSDocState.SawAsterisk; - indent += asterisk.length; - } - break; - case SyntaxKind.WhitespaceTrivia: - // only collect whitespace if we're already saving comments or have just crossed the comment indent margin - const whitespace = scanner.getTokenText(); - if (state === JSDocState.SavingComments) { - comments.push(whitespace); - } - else if (margin !== undefined && indent + whitespace.length > margin) { - comments.push(whitespace.slice(margin - indent - 1)); - } - indent += whitespace.length; - break; - case SyntaxKind.EndOfFileToken: - break loop; - default: - // Anything else is doc comment text. We just save it. Because it - // wasn't a tag, we can no longer parse a tag on this line until we hit the next - // line break. - state = JSDocState.SavingComments; + margin = undefined; + } + else { pushComment(scanner.getTokenText()); - break; - } - nextTokenJSDoc(); - } - removeLeadingNewlines(comments); - removeTrailingWhitespace(comments); - return createJSDocComment(); - }); - - function removeLeadingNewlines(comments: string[]) { - while (comments.length && (comments[0] === "\n" || comments[0] === "\r")) { - comments.shift(); - } - } - - function removeTrailingWhitespace(comments: string[]) { - while (comments.length && comments[comments.length - 1].trim() === "") { - comments.pop(); - } - } - - function createJSDocComment(): JSDoc { - const result = createNode(SyntaxKind.JSDocComment, start); - result.tags = tags && createNodeArray(tags, tagsPos, tagsEnd); - result.comment = comments.length ? comments.join("") : undefined; - return finishNode(result, end); - } - - function isNextNonwhitespaceTokenEndOfFile(): boolean { - // We must use infinite lookahead, as there could be any number of newlines :( - while (true) { - nextTokenJSDoc(); - if (token() === SyntaxKind.EndOfFileToken) { - return true; - } - if (!(token() === SyntaxKind.WhitespaceTrivia || token() === SyntaxKind.NewLineTrivia)) { - return false; - } - } - } - - function skipWhitespace(): void { - if (token() === SyntaxKind.WhitespaceTrivia || token() === SyntaxKind.NewLineTrivia) { - if (lookAhead(isNextNonwhitespaceTokenEndOfFile)) { - return; // Don't skip whitespace prior to EoF (or end of comment) - that shouldn't be included in any node's range - } - } - while (token() === SyntaxKind.WhitespaceTrivia || token() === SyntaxKind.NewLineTrivia) { - nextTokenJSDoc(); - } - } - - function skipWhitespaceOrAsterisk(): string { - if (token() === SyntaxKind.WhitespaceTrivia || token() === SyntaxKind.NewLineTrivia) { - if (lookAhead(isNextNonwhitespaceTokenEndOfFile)) { - return ""; // Don't skip whitespace prior to EoF (or end of comment) - that shouldn't be included in any node's range - } - } - - let precedingLineBreak = scanner.hasPrecedingLineBreak(); - let seenLineBreak = false; - let indentText = ""; - while ((precedingLineBreak && token() === SyntaxKind.AsteriskToken) || token() === SyntaxKind.WhitespaceTrivia || token() === SyntaxKind.NewLineTrivia) { - indentText += scanner.getTokenText(); - if (token() === SyntaxKind.NewLineTrivia) { - precedingLineBreak = true; - seenLineBreak = true; - indentText = ""; - } - else if (token() === SyntaxKind.AsteriskToken) { - precedingLineBreak = false; - } - nextTokenJSDoc(); - } - return seenLineBreak ? indentText : ""; - } - - function parseTag(margin: number) { - Debug.assert(token() === SyntaxKind.AtToken); - const start = scanner.getTokenPos(); - nextTokenJSDoc(); - - const tagName = parseJSDocIdentifierName(/*message*/ undefined); - const indentText = skipWhitespaceOrAsterisk(); - - let tag: JSDocTag | undefined; - switch (tagName.escapedText) { - case "author": - tag = parseAuthorTag(start, tagName, margin); - break; - case "implements": - tag = parseImplementsTag(start, tagName); - break; - case "augments": - case "extends": - tag = parseAugmentsTag(start, tagName); - break; - case "class": - case "constructor": - tag = parseSimpleTag(start, SyntaxKind.JSDocClassTag, tagName); - break; - case "public": - tag = parseSimpleTag(start, SyntaxKind.JSDocPublicTag, tagName); - break; - case "private": - tag = parseSimpleTag(start, SyntaxKind.JSDocPrivateTag, tagName); - break; - case "protected": - tag = parseSimpleTag(start, SyntaxKind.JSDocProtectedTag, tagName); - break; - case "readonly": - tag = parseSimpleTag(start, SyntaxKind.JSDocReadonlyTag, tagName); - break; - case "this": - tag = parseThisTag(start, tagName); - break; - case "enum": - tag = parseEnumTag(start, tagName); - break; - case "arg": - case "argument": - case "param": - return parseParameterOrPropertyTag(start, tagName, PropertyLikeParse.Parameter, margin); - case "return": - case "returns": - tag = parseReturnTag(start, tagName); - break; - case "template": - tag = parseTemplateTag(start, tagName); + } break; - case "type": - tag = parseTypeTag(start, tagName); + case SyntaxKind.NewLineTrivia: + comments.push(scanner.getTokenText()); + state = JSDocState.BeginningOfLine; + indent = 0; break; - case "typedef": - tag = parseTypedefTag(start, tagName, margin); + case SyntaxKind.AsteriskToken: + const asterisk = scanner.getTokenText(); + if (state === JSDocState.SawAsterisk || state === JSDocState.SavingComments) { + // If we've already seen an asterisk, then we can no longer parse a tag on this line + state = JSDocState.SavingComments; + pushComment(asterisk); + } + else { + // Ignore the first asterisk on a line + state = JSDocState.SawAsterisk; + indent += asterisk.length; + } break; - case "callback": - tag = parseCallbackTag(start, tagName, margin); + case SyntaxKind.WhitespaceTrivia: + // only collect whitespace if we're already saving comments or have just crossed the comment indent margin + const whitespace = scanner.getTokenText(); + if (state === JSDocState.SavingComments) { + comments.push(whitespace); + } + else if (margin !== undefined && indent + whitespace.length > margin) { + comments.push(whitespace.slice(margin - indent - 1)); + } + indent += whitespace.length; break; + case SyntaxKind.EndOfFileToken: + break loop; default: - tag = parseUnknownTag(start, tagName); + // Anything else is doc comment text. We just save it. Because it + // wasn't a tag, we can no longer parse a tag on this line until we hit the next + // line break. + state = JSDocState.SavingComments; + pushComment(scanner.getTokenText()); break; } - - if (!tag.comment) { - // some tags, like typedef and callback, have already parsed their comments earlier - if (!indentText) { - margin += tag.end - tag.pos; - } - tag.comment = parseTagComments(margin, indentText.slice(margin)); - } - return tag; + nextTokenJSDoc(); } - - function parseTagComments(indent: number, initialMargin?: string): string | undefined { - const comments: string[] = []; - let state = JSDocState.BeginningOfLine; - let margin: number | undefined; - function pushComment(text: string) { - if (!margin) { - margin = indent; - } - comments.push(text); - indent += text.length; - } - if (initialMargin !== undefined) { - // jump straight to saving comments if there is some initial indentation - if (initialMargin !== "") { - pushComment(initialMargin); - } - state = JSDocState.SawAsterisk; - } - let tok = token() as JSDocSyntaxKind; - loop: while (true) { - switch (tok) { - case SyntaxKind.NewLineTrivia: - if (state >= JSDocState.SawAsterisk) { - state = JSDocState.BeginningOfLine; - // don't use pushComment here because we want to keep the margin unchanged - comments.push(scanner.getTokenText()); - } - indent = 0; - break; - case SyntaxKind.AtToken: - if (state === JSDocState.SavingBackticks) { - comments.push(scanner.getTokenText()); - break; - } - scanner.setTextPos(scanner.getTextPos() - 1); - // falls through - case SyntaxKind.EndOfFileToken: - // Done - break loop; - case SyntaxKind.WhitespaceTrivia: - if (state === JSDocState.SavingComments || state === JSDocState.SavingBackticks) { - pushComment(scanner.getTokenText()); - } - else { - const whitespace = scanner.getTokenText(); - // if the whitespace crosses the margin, take only the whitespace that passes the margin - if (margin !== undefined && indent + whitespace.length > margin) { - comments.push(whitespace.slice(margin - indent)); - } - indent += whitespace.length; - } - break; - case SyntaxKind.OpenBraceToken: - state = JSDocState.SavingComments; - if (lookAhead(() => nextTokenJSDoc() === SyntaxKind.AtToken && tokenIsIdentifierOrKeyword(nextTokenJSDoc()) && scanner.getTokenText() === "link")) { - pushComment(scanner.getTokenText()); - nextTokenJSDoc(); - pushComment(scanner.getTokenText()); - nextTokenJSDoc(); - } - pushComment(scanner.getTokenText()); - break; - case SyntaxKind.BacktickToken: - if (state === JSDocState.SavingBackticks) { - state = JSDocState.SavingComments; - } - else { - state = JSDocState.SavingBackticks; - } - pushComment(scanner.getTokenText()); - break; - case SyntaxKind.AsteriskToken: - if (state === JSDocState.BeginningOfLine) { - // leading asterisks start recording on the *next* (non-whitespace) token - state = JSDocState.SawAsterisk; - indent += 1; - break; - } - // record the * as a comment - // falls through - default: - if (state !== JSDocState.SavingBackticks) { - state = JSDocState.SavingComments; // leading identifiers start recording as well - } - pushComment(scanner.getTokenText()); - break; - } - tok = nextTokenJSDoc(); - } - - removeLeadingNewlines(comments); - removeTrailingWhitespace(comments); - return comments.length === 0 ? undefined : comments.join(""); + removeLeadingNewlines(comments); + removeTrailingWhitespace(comments); + return createJSDocComment(); + }); + function removeLeadingNewlines(comments: string[]) { + while (comments.length && (comments[0] === "\n" || comments[0] === "\r")) { + comments.shift(); } - - function parseUnknownTag(start: number, tagName: Identifier) { - const result = createNode(SyntaxKind.JSDocTag, start); - result.tagName = tagName; - return finishNode(result); + } + function removeTrailingWhitespace(comments: string[]) { + while (comments.length && comments[comments.length - 1].trim() === "") { + comments.pop(); } - - function addTag(tag: JSDocTag | undefined): void { - if (!tag) { - return; + } + function createJSDocComment(): JSDoc { + const result = (createNode(SyntaxKind.JSDocComment, start)); + result.tags = tags && createNodeArray(tags, tagsPos, tagsEnd); + result.comment = comments.length ? comments.join("") : undefined; + return finishNode(result, end); + } + function isNextNonwhitespaceTokenEndOfFile(): boolean { + // We must use infinite lookahead, as there could be any number of newlines :( + while (true) { + nextTokenJSDoc(); + if (token() === SyntaxKind.EndOfFileToken) { + return true; } - if (!tags) { - tags = [tag]; - tagsPos = tag.pos; + if (!(token() === SyntaxKind.WhitespaceTrivia || token() === SyntaxKind.NewLineTrivia)) { + return false; } - else { - tags.push(tag); + } + } + function skipWhitespace(): void { + if (token() === SyntaxKind.WhitespaceTrivia || token() === SyntaxKind.NewLineTrivia) { + if (lookAhead(isNextNonwhitespaceTokenEndOfFile)) { + return; // Don't skip whitespace prior to EoF (or end of comment) - that shouldn't be included in any node's range } - tagsEnd = tag.end; } - - function tryParseTypeExpression(): JSDocTypeExpression | undefined { - skipWhitespaceOrAsterisk(); - return token() === SyntaxKind.OpenBraceToken ? parseJSDocTypeExpression() : undefined; + while (token() === SyntaxKind.WhitespaceTrivia || token() === SyntaxKind.NewLineTrivia) { + nextTokenJSDoc(); } - - function parseBracketNameInPropertyAndParamTag(): { name: EntityName, isBracketed: boolean } { - // Looking for something like '[foo]', 'foo', '[foo.bar]' or 'foo.bar' - const isBracketed = parseOptionalJsdoc(SyntaxKind.OpenBracketToken); - if (isBracketed) { - skipWhitespace(); + } + function skipWhitespaceOrAsterisk(): string { + if (token() === SyntaxKind.WhitespaceTrivia || token() === SyntaxKind.NewLineTrivia) { + if (lookAhead(isNextNonwhitespaceTokenEndOfFile)) { + return ""; // Don't skip whitespace prior to EoF (or end of comment) - that shouldn't be included in any node's range } - // a markdown-quoted name: `arg` is not legal jsdoc, but occurs in the wild - const isBackquoted = parseOptionalJsdoc(SyntaxKind.BacktickToken); - const name = parseJSDocEntityName(); - if (isBackquoted) { - parseExpectedTokenJSDoc(SyntaxKind.BacktickToken); + } + let precedingLineBreak = scanner.hasPrecedingLineBreak(); + let seenLineBreak = false; + let indentText = ""; + while ((precedingLineBreak && token() === SyntaxKind.AsteriskToken) || token() === SyntaxKind.WhitespaceTrivia || token() === SyntaxKind.NewLineTrivia) { + indentText += scanner.getTokenText(); + if (token() === SyntaxKind.NewLineTrivia) { + precedingLineBreak = true; + seenLineBreak = true; + indentText = ""; } - if (isBracketed) { - skipWhitespace(); - // May have an optional default, e.g. '[foo = 42]' - if (parseOptionalToken(SyntaxKind.EqualsToken)) { - parseExpression(); - } - - parseExpected(SyntaxKind.CloseBracketToken); + else if (token() === SyntaxKind.AsteriskToken) { + precedingLineBreak = false; } - - return { name, isBracketed }; + nextTokenJSDoc(); } - - function isObjectOrObjectArrayTypeReference(node: TypeNode): boolean { - switch (node.kind) { - case SyntaxKind.ObjectKeyword: - return true; - case SyntaxKind.ArrayType: - return isObjectOrObjectArrayTypeReference((node as ArrayTypeNode).elementType); - default: - return isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && node.typeName.escapedText === "Object" && !node.typeArguments; + return seenLineBreak ? indentText : ""; + } + function parseTag(margin: number) { + Debug.assert(token() === SyntaxKind.AtToken); + const start = scanner.getTokenPos(); + nextTokenJSDoc(); + const tagName = parseJSDocIdentifierName(/*message*/ undefined); + const indentText = skipWhitespaceOrAsterisk(); + let tag: JSDocTag | undefined; + switch (tagName.escapedText) { + case "author": + tag = parseAuthorTag(start, tagName, margin); + break; + case "implements": + tag = parseImplementsTag(start, tagName); + break; + case "augments": + case "extends": + tag = parseAugmentsTag(start, tagName); + break; + case "class": + case "constructor": + tag = parseSimpleTag(start, SyntaxKind.JSDocClassTag, tagName); + break; + case "public": + tag = parseSimpleTag(start, SyntaxKind.JSDocPublicTag, tagName); + break; + case "private": + tag = parseSimpleTag(start, SyntaxKind.JSDocPrivateTag, tagName); + break; + case "protected": + tag = parseSimpleTag(start, SyntaxKind.JSDocProtectedTag, tagName); + break; + case "readonly": + tag = parseSimpleTag(start, SyntaxKind.JSDocReadonlyTag, tagName); + break; + case "this": + tag = parseThisTag(start, tagName); + break; + case "enum": + tag = parseEnumTag(start, tagName); + break; + case "arg": + case "argument": + case "param": + return parseParameterOrPropertyTag(start, tagName, PropertyLikeParse.Parameter, margin); + case "return": + case "returns": + tag = parseReturnTag(start, tagName); + break; + case "template": + tag = parseTemplateTag(start, tagName); + break; + case "type": + tag = parseTypeTag(start, tagName); + break; + case "typedef": + tag = parseTypedefTag(start, tagName, margin); + break; + case "callback": + tag = parseCallbackTag(start, tagName, margin); + break; + default: + tag = parseUnknownTag(start, tagName); + break; + } + if (!tag.comment) { + // some tags, like typedef and callback, have already parsed their comments earlier + if (!indentText) { + margin += tag.end - tag.pos; } + tag.comment = parseTagComments(margin, indentText.slice(margin)); } - - function parseParameterOrPropertyTag(start: number, tagName: Identifier, target: PropertyLikeParse, indent: number): JSDocParameterTag | JSDocPropertyTag { - let typeExpression = tryParseTypeExpression(); - let isNameFirst = !typeExpression; - skipWhitespaceOrAsterisk(); - - const { name, isBracketed } = parseBracketNameInPropertyAndParamTag(); - skipWhitespace(); - - if (isNameFirst) { - typeExpression = tryParseTypeExpression(); + return tag; + } + function parseTagComments(indent: number, initialMargin?: string): string | undefined { + const comments: string[] = []; + let state = JSDocState.BeginningOfLine; + let margin: number | undefined; + function pushComment(text: string) { + if (!margin) { + margin = indent; } - - const result = target === PropertyLikeParse.Property ? - createNode(SyntaxKind.JSDocPropertyTag, start) : - createNode(SyntaxKind.JSDocParameterTag, start); - const comment = parseTagComments(indent + scanner.getStartPos() - start); - const nestedTypeLiteral = target !== PropertyLikeParse.CallbackParameter && parseNestedTypeLiteral(typeExpression, name, target, indent); - if (nestedTypeLiteral) { - typeExpression = nestedTypeLiteral; - isNameFirst = true; + comments.push(text); + indent += text.length; + } + if (initialMargin !== undefined) { + // jump straight to saving comments if there is some initial indentation + if (initialMargin !== "") { + pushComment(initialMargin); } - result.tagName = tagName; - result.typeExpression = typeExpression; - result.name = name; - result.isNameFirst = isNameFirst; - result.isBracketed = isBracketed; - result.comment = comment; - return finishNode(result); + state = JSDocState.SawAsterisk; } - - function parseNestedTypeLiteral(typeExpression: JSDocTypeExpression | undefined, name: EntityName, target: PropertyLikeParse, indent: number) { - if (typeExpression && isObjectOrObjectArrayTypeReference(typeExpression.type)) { - const typeLiteralExpression = createNode(SyntaxKind.JSDocTypeExpression, scanner.getTokenPos()); - let child: JSDocPropertyLikeTag | JSDocTypeTag | false; - let jsdocTypeLiteral: JSDocTypeLiteral; - const start = scanner.getStartPos(); - let children: JSDocPropertyLikeTag[] | undefined; - while (child = tryParse(() => parseChildParameterOrPropertyTag(target, indent, name))) { - if (child.kind === SyntaxKind.JSDocParameterTag || child.kind === SyntaxKind.JSDocPropertyTag) { - children = append(children, child); + let tok = (token() as JSDocSyntaxKind); + loop: while (true) { + switch (tok) { + case SyntaxKind.NewLineTrivia: + if (state >= JSDocState.SawAsterisk) { + state = JSDocState.BeginningOfLine; + // don't use pushComment here because we want to keep the margin unchanged + comments.push(scanner.getTokenText()); } - } - if (children) { - jsdocTypeLiteral = createNode(SyntaxKind.JSDocTypeLiteral, start); - jsdocTypeLiteral.jsDocPropertyTags = children; - if (typeExpression.type.kind === SyntaxKind.ArrayType) { - jsdocTypeLiteral.isArrayType = true; + indent = 0; + break; + case SyntaxKind.AtToken: + if (state === JSDocState.SavingBackticks) { + comments.push(scanner.getTokenText()); + break; } - typeLiteralExpression.type = finishNode(jsdocTypeLiteral); - return finishNode(typeLiteralExpression); - } + scanner.setTextPos(scanner.getTextPos() - 1); + // falls through + case SyntaxKind.EndOfFileToken: + // Done + break loop; + case SyntaxKind.WhitespaceTrivia: + if (state === JSDocState.SavingComments || state === JSDocState.SavingBackticks) { + pushComment(scanner.getTokenText()); + } + else { + const whitespace = scanner.getTokenText(); + // if the whitespace crosses the margin, take only the whitespace that passes the margin + if (margin !== undefined && indent + whitespace.length > margin) { + comments.push(whitespace.slice(margin - indent)); + } + indent += whitespace.length; + } + break; + case SyntaxKind.OpenBraceToken: + state = JSDocState.SavingComments; + if (lookAhead(() => nextTokenJSDoc() === SyntaxKind.AtToken && tokenIsIdentifierOrKeyword(nextTokenJSDoc()) && scanner.getTokenText() === "link")) { + pushComment(scanner.getTokenText()); + nextTokenJSDoc(); + pushComment(scanner.getTokenText()); + nextTokenJSDoc(); + } + pushComment(scanner.getTokenText()); + break; + case SyntaxKind.BacktickToken: + if (state === JSDocState.SavingBackticks) { + state = JSDocState.SavingComments; + } + else { + state = JSDocState.SavingBackticks; + } + pushComment(scanner.getTokenText()); + break; + case SyntaxKind.AsteriskToken: + if (state === JSDocState.BeginningOfLine) { + // leading asterisks start recording on the *next* (non-whitespace) token + state = JSDocState.SawAsterisk; + indent += 1; + break; + } + // record the * as a comment + // falls through + default: + if (state !== JSDocState.SavingBackticks) { + state = JSDocState.SavingComments; // leading identifiers start recording as well + } + pushComment(scanner.getTokenText()); + break; } + tok = nextTokenJSDoc(); } - - function parseReturnTag(start: number, tagName: Identifier): JSDocReturnTag { - if (some(tags, isJSDocReturnTag)) { - parseErrorAt(tagName.pos, scanner.getTokenPos(), Diagnostics._0_tag_already_specified, tagName.escapedText); - } - - const result = createNode(SyntaxKind.JSDocReturnTag, start); - result.tagName = tagName; - result.typeExpression = tryParseTypeExpression(); - return finishNode(result); + removeLeadingNewlines(comments); + removeTrailingWhitespace(comments); + return comments.length === 0 ? undefined : comments.join(""); + } + function parseUnknownTag(start: number, tagName: Identifier) { + const result = (createNode(SyntaxKind.JSDocTag, start)); + result.tagName = tagName; + return finishNode(result); + } + function addTag(tag: JSDocTag | undefined): void { + if (!tag) { + return; } - - function parseTypeTag(start: number, tagName: Identifier): JSDocTypeTag { - if (some(tags, isJSDocTypeTag)) { - parseErrorAt(tagName.pos, scanner.getTokenPos(), Diagnostics._0_tag_already_specified, tagName.escapedText); - } - - const result = createNode(SyntaxKind.JSDocTypeTag, start); - result.tagName = tagName; - result.typeExpression = parseJSDocTypeExpression(/*mayOmitBraces*/ true); - return finishNode(result); + if (!tags) { + tags = [tag]; + tagsPos = tag.pos; } - - function parseAuthorTag(start: number, tagName: Identifier, indent: number): JSDocAuthorTag { - const result = createNode(SyntaxKind.JSDocAuthorTag, start); - result.tagName = tagName; - - const authorInfoWithEmail = tryParse(() => tryParseAuthorNameAndEmail()); - if (!authorInfoWithEmail) { - return finishNode(result); - } - - result.comment = authorInfoWithEmail; - - if (lookAhead(() => nextToken() !== SyntaxKind.NewLineTrivia)) { - const comment = parseTagComments(indent); - if (comment) { - result.comment += comment; - } + else { + tags.push(tag); + } + tagsEnd = tag.end; + } + function tryParseTypeExpression(): JSDocTypeExpression | undefined { + skipWhitespaceOrAsterisk(); + return token() === SyntaxKind.OpenBraceToken ? parseJSDocTypeExpression() : undefined; + } + function parseBracketNameInPropertyAndParamTag(): { + name: EntityName; + isBracketed: boolean; + } { + // Looking for something like '[foo]', 'foo', '[foo.bar]' or 'foo.bar' + const isBracketed = parseOptionalJsdoc(SyntaxKind.OpenBracketToken); + if (isBracketed) { + skipWhitespace(); + } + // a markdown-quoted name: `arg` is not legal jsdoc, but occurs in the wild + const isBackquoted = parseOptionalJsdoc(SyntaxKind.BacktickToken); + const name = parseJSDocEntityName(); + if (isBackquoted) { + parseExpectedTokenJSDoc(SyntaxKind.BacktickToken); + } + if (isBracketed) { + skipWhitespace(); + // May have an optional default, e.g. '[foo = 42]' + if (parseOptionalToken(SyntaxKind.EqualsToken)) { + parseExpression(); } - - return finishNode(result); + parseExpected(SyntaxKind.CloseBracketToken); } - - function tryParseAuthorNameAndEmail(): string | undefined { - const comments: string[] = []; - let seenLessThan = false; - let seenGreaterThan = false; - let token = scanner.getToken(); - - loop: while (true) { - switch (token) { - case SyntaxKind.Identifier: - case SyntaxKind.WhitespaceTrivia: - case SyntaxKind.DotToken: - case SyntaxKind.AtToken: - comments.push(scanner.getTokenText()); - break; - case SyntaxKind.LessThanToken: - if (seenLessThan || seenGreaterThan) { - return; - } - seenLessThan = true; - comments.push(scanner.getTokenText()); - break; - case SyntaxKind.GreaterThanToken: - if (!seenLessThan || seenGreaterThan) { - return; - } - seenGreaterThan = true; - comments.push(scanner.getTokenText()); - scanner.setTextPos(scanner.getTokenPos() + 1); - break loop; - case SyntaxKind.NewLineTrivia: - case SyntaxKind.EndOfFileToken: - break loop; + return { name, isBracketed }; + } + function isObjectOrObjectArrayTypeReference(node: TypeNode): boolean { + switch (node.kind) { + case SyntaxKind.ObjectKeyword: + return true; + case SyntaxKind.ArrayType: + return isObjectOrObjectArrayTypeReference((node as ArrayTypeNode).elementType); + default: + return isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && node.typeName.escapedText === "Object" && !node.typeArguments; + } + } + function parseParameterOrPropertyTag(start: number, tagName: Identifier, target: PropertyLikeParse, indent: number): JSDocParameterTag | JSDocPropertyTag { + let typeExpression = tryParseTypeExpression(); + let isNameFirst = !typeExpression; + skipWhitespaceOrAsterisk(); + const { name, isBracketed } = parseBracketNameInPropertyAndParamTag(); + skipWhitespace(); + if (isNameFirst) { + typeExpression = tryParseTypeExpression(); + } + const result = target === PropertyLikeParse.Property ? + createNode(SyntaxKind.JSDocPropertyTag, start) : + createNode(SyntaxKind.JSDocParameterTag, start); + const comment = parseTagComments(indent + scanner.getStartPos() - start); + const nestedTypeLiteral = target !== PropertyLikeParse.CallbackParameter && parseNestedTypeLiteral(typeExpression, name, target, indent); + if (nestedTypeLiteral) { + typeExpression = nestedTypeLiteral; + isNameFirst = true; + } + result.tagName = tagName; + result.typeExpression = typeExpression; + result.name = name; + result.isNameFirst = isNameFirst; + result.isBracketed = isBracketed; + result.comment = comment; + return finishNode(result); + } + function parseNestedTypeLiteral(typeExpression: JSDocTypeExpression | undefined, name: EntityName, target: PropertyLikeParse, indent: number) { + if (typeExpression && isObjectOrObjectArrayTypeReference(typeExpression.type)) { + const typeLiteralExpression = (createNode(SyntaxKind.JSDocTypeExpression, scanner.getTokenPos())); + let child: JSDocPropertyLikeTag | JSDocTypeTag | false; + let jsdocTypeLiteral: JSDocTypeLiteral; + const start = scanner.getStartPos(); + let children: JSDocPropertyLikeTag[] | undefined; + while (child = tryParse(() => parseChildParameterOrPropertyTag(target, indent, name))) { + if (child.kind === SyntaxKind.JSDocParameterTag || child.kind === SyntaxKind.JSDocPropertyTag) { + children = append(children, child); } - - token = nextTokenJSDoc(); } - - if (seenLessThan && seenGreaterThan) { - return comments.length === 0 ? undefined : comments.join(""); + if (children) { + jsdocTypeLiteral = (createNode(SyntaxKind.JSDocTypeLiteral, start)); + jsdocTypeLiteral.jsDocPropertyTags = children; + if (typeExpression.type.kind === SyntaxKind.ArrayType) { + jsdocTypeLiteral.isArrayType = true; + } + typeLiteralExpression.type = finishNode(jsdocTypeLiteral); + return finishNode(typeLiteralExpression); } } - - function parseImplementsTag(start: number, tagName: Identifier): JSDocImplementsTag { - const result = createNode(SyntaxKind.JSDocImplementsTag, start); - result.tagName = tagName; - result.class = parseExpressionWithTypeArgumentsForAugments(); - return finishNode(result); + } + function parseReturnTag(start: number, tagName: Identifier): JSDocReturnTag { + if (some(tags, isJSDocReturnTag)) { + parseErrorAt(tagName.pos, scanner.getTokenPos(), Diagnostics._0_tag_already_specified, tagName.escapedText); } - - function parseAugmentsTag(start: number, tagName: Identifier): JSDocAugmentsTag { - const result = createNode(SyntaxKind.JSDocAugmentsTag, start); - result.tagName = tagName; - result.class = parseExpressionWithTypeArgumentsForAugments(); + const result = (createNode(SyntaxKind.JSDocReturnTag, start)); + result.tagName = tagName; + result.typeExpression = tryParseTypeExpression(); + return finishNode(result); + } + function parseTypeTag(start: number, tagName: Identifier): JSDocTypeTag { + if (some(tags, isJSDocTypeTag)) { + parseErrorAt(tagName.pos, scanner.getTokenPos(), Diagnostics._0_tag_already_specified, tagName.escapedText); + } + const result = (createNode(SyntaxKind.JSDocTypeTag, start)); + result.tagName = tagName; + result.typeExpression = parseJSDocTypeExpression(/*mayOmitBraces*/ true); + return finishNode(result); + } + function parseAuthorTag(start: number, tagName: Identifier, indent: number): JSDocAuthorTag { + const result = (createNode(SyntaxKind.JSDocAuthorTag, start)); + result.tagName = tagName; + const authorInfoWithEmail = tryParse(() => tryParseAuthorNameAndEmail()); + if (!authorInfoWithEmail) { return finishNode(result); } - - function parseExpressionWithTypeArgumentsForAugments(): ExpressionWithTypeArguments & { expression: Identifier | PropertyAccessEntityNameExpression } { - const usedBrace = parseOptional(SyntaxKind.OpenBraceToken); - const node = createNode(SyntaxKind.ExpressionWithTypeArguments) as ExpressionWithTypeArguments & { expression: Identifier | PropertyAccessEntityNameExpression }; - node.expression = parsePropertyAccessEntityNameExpression(); - node.typeArguments = tryParseTypeArguments(); - const res = finishNode(node); - if (usedBrace) { - parseExpected(SyntaxKind.CloseBraceToken); + result.comment = authorInfoWithEmail; + if (lookAhead(() => nextToken() !== SyntaxKind.NewLineTrivia)) { + const comment = parseTagComments(indent); + if (comment) { + result.comment += comment; } - return res; } - - function parsePropertyAccessEntityNameExpression() { - let node: Identifier | PropertyAccessEntityNameExpression = parseJSDocIdentifierName(); - while (parseOptional(SyntaxKind.DotToken)) { - const prop: PropertyAccessEntityNameExpression = createNode(SyntaxKind.PropertyAccessExpression, node.pos) as PropertyAccessEntityNameExpression; - prop.expression = node; - prop.name = parseJSDocIdentifierName(); - node = finishNode(prop); + return finishNode(result); + } + function tryParseAuthorNameAndEmail(): string | undefined { + const comments: string[] = []; + let seenLessThan = false; + let seenGreaterThan = false; + let token = scanner.getToken(); + loop: while (true) { + switch (token) { + case SyntaxKind.Identifier: + case SyntaxKind.WhitespaceTrivia: + case SyntaxKind.DotToken: + case SyntaxKind.AtToken: + comments.push(scanner.getTokenText()); + break; + case SyntaxKind.LessThanToken: + if (seenLessThan || seenGreaterThan) { + return; + } + seenLessThan = true; + comments.push(scanner.getTokenText()); + break; + case SyntaxKind.GreaterThanToken: + if (!seenLessThan || seenGreaterThan) { + return; + } + seenGreaterThan = true; + comments.push(scanner.getTokenText()); + scanner.setTextPos(scanner.getTokenPos() + 1); + break loop; + case SyntaxKind.NewLineTrivia: + case SyntaxKind.EndOfFileToken: + break loop; } - return node; + token = nextTokenJSDoc(); } - - function parseSimpleTag(start: number, kind: SyntaxKind, tagName: Identifier): JSDocTag { - const tag = createNode(kind, start); - tag.tagName = tagName; - return finishNode(tag); + if (seenLessThan && seenGreaterThan) { + return comments.length === 0 ? undefined : comments.join(""); } - - function parseThisTag(start: number, tagName: Identifier): JSDocThisTag { - const tag = createNode(SyntaxKind.JSDocThisTag, start); - tag.tagName = tagName; - tag.typeExpression = parseJSDocTypeExpression(/*mayOmitBraces*/ true); - skipWhitespace(); - return finishNode(tag); + } + function parseImplementsTag(start: number, tagName: Identifier): JSDocImplementsTag { + const result = (createNode(SyntaxKind.JSDocImplementsTag, start)); + result.tagName = tagName; + result.class = parseExpressionWithTypeArgumentsForAugments(); + return finishNode(result); + } + function parseAugmentsTag(start: number, tagName: Identifier): JSDocAugmentsTag { + const result = (createNode(SyntaxKind.JSDocAugmentsTag, start)); + result.tagName = tagName; + result.class = parseExpressionWithTypeArgumentsForAugments(); + return finishNode(result); + } + function parseExpressionWithTypeArgumentsForAugments(): ExpressionWithTypeArguments & { + expression: Identifier | PropertyAccessEntityNameExpression; + } { + const usedBrace = parseOptional(SyntaxKind.OpenBraceToken); + const node = (createNode(SyntaxKind.ExpressionWithTypeArguments) as ExpressionWithTypeArguments & { + expression: Identifier | PropertyAccessEntityNameExpression; + }); + node.expression = parsePropertyAccessEntityNameExpression(); + node.typeArguments = tryParseTypeArguments(); + const res = finishNode(node); + if (usedBrace) { + parseExpected(SyntaxKind.CloseBraceToken); } - - function parseEnumTag(start: number, tagName: Identifier): JSDocEnumTag { - const tag = createNode(SyntaxKind.JSDocEnumTag, start); - tag.tagName = tagName; - tag.typeExpression = parseJSDocTypeExpression(/*mayOmitBraces*/ true); - skipWhitespace(); - return finishNode(tag); + return res; + } + function parsePropertyAccessEntityNameExpression() { + let node: Identifier | PropertyAccessEntityNameExpression = parseJSDocIdentifierName(); + while (parseOptional(SyntaxKind.DotToken)) { + const prop: PropertyAccessEntityNameExpression = (createNode(SyntaxKind.PropertyAccessExpression, node.pos) as PropertyAccessEntityNameExpression); + prop.expression = node; + prop.name = parseJSDocIdentifierName(); + node = finishNode(prop); } - - function parseTypedefTag(start: number, tagName: Identifier, indent: number): JSDocTypedefTag { - const typeExpression = tryParseTypeExpression(); - skipWhitespaceOrAsterisk(); - - const typedefTag = createNode(SyntaxKind.JSDocTypedefTag, start); - typedefTag.tagName = tagName; - typedefTag.fullName = parseJSDocTypeNameWithNamespace(); - typedefTag.name = getJSDocTypeAliasName(typedefTag.fullName); - skipWhitespace(); - typedefTag.comment = parseTagComments(indent); - - typedefTag.typeExpression = typeExpression; - let end: number | undefined; - if (!typeExpression || isObjectOrObjectArrayTypeReference(typeExpression.type)) { - let child: JSDocTypeTag | JSDocPropertyTag | false; - let jsdocTypeLiteral: JSDocTypeLiteral | undefined; - let childTypeTag: JSDocTypeTag | undefined; - while (child = tryParse(() => parseChildPropertyTag(indent))) { - if (!jsdocTypeLiteral) { - jsdocTypeLiteral = createNode(SyntaxKind.JSDocTypeLiteral, start); - } - if (child.kind === SyntaxKind.JSDocTypeTag) { - if (childTypeTag) { - break; - } - else { - childTypeTag = child; - } + return node; + } + function parseSimpleTag(start: number, kind: SyntaxKind, tagName: Identifier): JSDocTag { + const tag = (createNode(kind, start)); + tag.tagName = tagName; + return finishNode(tag); + } + function parseThisTag(start: number, tagName: Identifier): JSDocThisTag { + const tag = (createNode(SyntaxKind.JSDocThisTag, start)); + tag.tagName = tagName; + tag.typeExpression = parseJSDocTypeExpression(/*mayOmitBraces*/ true); + skipWhitespace(); + return finishNode(tag); + } + function parseEnumTag(start: number, tagName: Identifier): JSDocEnumTag { + const tag = (createNode(SyntaxKind.JSDocEnumTag, start)); + tag.tagName = tagName; + tag.typeExpression = parseJSDocTypeExpression(/*mayOmitBraces*/ true); + skipWhitespace(); + return finishNode(tag); + } + function parseTypedefTag(start: number, tagName: Identifier, indent: number): JSDocTypedefTag { + const typeExpression = tryParseTypeExpression(); + skipWhitespaceOrAsterisk(); + const typedefTag = (createNode(SyntaxKind.JSDocTypedefTag, start)); + typedefTag.tagName = tagName; + typedefTag.fullName = parseJSDocTypeNameWithNamespace(); + typedefTag.name = getJSDocTypeAliasName(typedefTag.fullName); + skipWhitespace(); + typedefTag.comment = parseTagComments(indent); + typedefTag.typeExpression = typeExpression; + let end: number | undefined; + if (!typeExpression || isObjectOrObjectArrayTypeReference(typeExpression.type)) { + let child: JSDocTypeTag | JSDocPropertyTag | false; + let jsdocTypeLiteral: JSDocTypeLiteral | undefined; + let childTypeTag: JSDocTypeTag | undefined; + while (child = tryParse(() => parseChildPropertyTag(indent))) { + if (!jsdocTypeLiteral) { + jsdocTypeLiteral = (createNode(SyntaxKind.JSDocTypeLiteral, start)); + } + if (child.kind === SyntaxKind.JSDocTypeTag) { + if (childTypeTag) { + break; } else { - jsdocTypeLiteral.jsDocPropertyTags = append(jsdocTypeLiteral.jsDocPropertyTags as MutableNodeArray, child); + childTypeTag = child; } } - if (jsdocTypeLiteral) { - if (typeExpression && typeExpression.type.kind === SyntaxKind.ArrayType) { - jsdocTypeLiteral.isArrayType = true; - } - typedefTag.typeExpression = childTypeTag && childTypeTag.typeExpression && !isObjectOrObjectArrayTypeReference(childTypeTag.typeExpression.type) ? - childTypeTag.typeExpression : - finishNode(jsdocTypeLiteral); - end = typedefTag.typeExpression.end; + else { + jsdocTypeLiteral.jsDocPropertyTags = append((jsdocTypeLiteral.jsDocPropertyTags as MutableNodeArray), child); } } - - // Only include the characters between the name end and the next token if a comment was actually parsed out - otherwise it's just whitespace - return finishNode(typedefTag, end || typedefTag.comment !== undefined ? scanner.getStartPos() : (typedefTag.fullName || typedefTag.typeExpression || typedefTag.tagName).end); - } - - function parseJSDocTypeNameWithNamespace(nested?: boolean) { - const pos = scanner.getTokenPos(); - if (!tokenIsIdentifierOrKeyword(token())) { - return undefined; - } - const typeNameOrNamespaceName = parseJSDocIdentifierName(); - if (parseOptional(SyntaxKind.DotToken)) { - const jsDocNamespaceNode = createNode(SyntaxKind.ModuleDeclaration, pos); - if (nested) { - jsDocNamespaceNode.flags |= NodeFlags.NestedNamespace; + if (jsdocTypeLiteral) { + if (typeExpression && typeExpression.type.kind === SyntaxKind.ArrayType) { + jsdocTypeLiteral.isArrayType = true; } - jsDocNamespaceNode.name = typeNameOrNamespaceName; - jsDocNamespaceNode.body = parseJSDocTypeNameWithNamespace(/*nested*/ true); - return finishNode(jsDocNamespaceNode); + typedefTag.typeExpression = childTypeTag && childTypeTag.typeExpression && !isObjectOrObjectArrayTypeReference(childTypeTag.typeExpression.type) ? + childTypeTag.typeExpression : + finishNode(jsdocTypeLiteral); + end = typedefTag.typeExpression.end; } - - if (nested) { - typeNameOrNamespaceName.isInJSDocNamespace = true; - } - return typeNameOrNamespaceName; } - - function parseCallbackTag(start: number, tagName: Identifier, indent: number): JSDocCallbackTag { - const callbackTag = createNode(SyntaxKind.JSDocCallbackTag, start) as JSDocCallbackTag; - callbackTag.tagName = tagName; - callbackTag.fullName = parseJSDocTypeNameWithNamespace(); - callbackTag.name = getJSDocTypeAliasName(callbackTag.fullName); - skipWhitespace(); - callbackTag.comment = parseTagComments(indent); - - let child: JSDocParameterTag | false; - const jsdocSignature = createNode(SyntaxKind.JSDocSignature, start) as JSDocSignature; - jsdocSignature.parameters = []; - while (child = tryParse(() => parseChildParameterOrPropertyTag(PropertyLikeParse.CallbackParameter, indent) as JSDocParameterTag)) { - jsdocSignature.parameters = append(jsdocSignature.parameters as MutableNodeArray, child); + // Only include the characters between the name end and the next token if a comment was actually parsed out - otherwise it's just whitespace + return finishNode(typedefTag, end || typedefTag.comment !== undefined ? scanner.getStartPos() : (typedefTag.fullName || typedefTag.typeExpression || typedefTag.tagName).end); + } + function parseJSDocTypeNameWithNamespace(nested?: boolean) { + const pos = scanner.getTokenPos(); + if (!tokenIsIdentifierOrKeyword(token())) { + return undefined; + } + const typeNameOrNamespaceName = parseJSDocIdentifierName(); + if (parseOptional(SyntaxKind.DotToken)) { + const jsDocNamespaceNode = (createNode(SyntaxKind.ModuleDeclaration, pos)); + if (nested) { + jsDocNamespaceNode.flags |= NodeFlags.NestedNamespace; } - const returnTag = tryParse(() => { - if (parseOptionalJsdoc(SyntaxKind.AtToken)) { - const tag = parseTag(indent); - if (tag && tag.kind === SyntaxKind.JSDocReturnTag) { - return tag as JSDocReturnTag; - } + jsDocNamespaceNode.name = typeNameOrNamespaceName; + jsDocNamespaceNode.body = parseJSDocTypeNameWithNamespace(/*nested*/ true); + return finishNode(jsDocNamespaceNode); + } + if (nested) { + typeNameOrNamespaceName.isInJSDocNamespace = true; + } + return typeNameOrNamespaceName; + } + function parseCallbackTag(start: number, tagName: Identifier, indent: number): JSDocCallbackTag { + const callbackTag = (createNode(SyntaxKind.JSDocCallbackTag, start) as JSDocCallbackTag); + callbackTag.tagName = tagName; + callbackTag.fullName = parseJSDocTypeNameWithNamespace(); + callbackTag.name = getJSDocTypeAliasName(callbackTag.fullName); + skipWhitespace(); + callbackTag.comment = parseTagComments(indent); + let child: JSDocParameterTag | false; + const jsdocSignature = (createNode(SyntaxKind.JSDocSignature, start) as JSDocSignature); + jsdocSignature.parameters = []; + while (child = tryParse(() => parseChildParameterOrPropertyTag(PropertyLikeParse.CallbackParameter, indent) as JSDocParameterTag)) { + jsdocSignature.parameters = append((jsdocSignature.parameters as MutableNodeArray), child); + } + const returnTag = tryParse(() => { + if (parseOptionalJsdoc(SyntaxKind.AtToken)) { + const tag = parseTag(indent); + if (tag && tag.kind === SyntaxKind.JSDocReturnTag) { + return tag as JSDocReturnTag; } - }); - if (returnTag) { - jsdocSignature.type = returnTag; } - callbackTag.typeExpression = finishNode(jsdocSignature); - return finishNode(callbackTag); + }); + if (returnTag) { + jsdocSignature.type = returnTag; } - - function getJSDocTypeAliasName(fullName: JSDocNamespaceBody | undefined) { - if (fullName) { - let rightNode = fullName; - while (true) { - if (ts.isIdentifier(rightNode) || !rightNode.body) { - return ts.isIdentifier(rightNode) ? rightNode : rightNode.name; - } - rightNode = rightNode.body; + callbackTag.typeExpression = finishNode(jsdocSignature); + return finishNode(callbackTag); + } + function getJSDocTypeAliasName(fullName: JSDocNamespaceBody | undefined) { + if (fullName) { + let rightNode = fullName; + while (true) { + if (ts.isIdentifier(rightNode) || !rightNode.body) { + return ts.isIdentifier(rightNode) ? rightNode : rightNode.name; } + rightNode = rightNode.body; } } - - function escapedTextsEqual(a: EntityName, b: EntityName): boolean { - while (!ts.isIdentifier(a) || !ts.isIdentifier(b)) { - if (!ts.isIdentifier(a) && !ts.isIdentifier(b) && a.right.escapedText === b.right.escapedText) { - a = a.left; - b = b.left; - } - else { - return false; - } + } + function escapedTextsEqual(a: EntityName, b: EntityName): boolean { + while (!ts.isIdentifier(a) || !ts.isIdentifier(b)) { + if (!ts.isIdentifier(a) && !ts.isIdentifier(b) && a.right.escapedText === b.right.escapedText) { + a = a.left; + b = b.left; + } + else { + return false; } - return a.escapedText === b.escapedText; - } - - function parseChildPropertyTag(indent: number) { - return parseChildParameterOrPropertyTag(PropertyLikeParse.Property, indent) as JSDocTypeTag | JSDocPropertyTag | false; } - - function parseChildParameterOrPropertyTag(target: PropertyLikeParse, indent: number, name?: EntityName): JSDocTypeTag | JSDocPropertyTag | JSDocParameterTag | false { - let canParseTag = true; - let seenAsterisk = false; - while (true) { - switch (nextTokenJSDoc()) { - case SyntaxKind.AtToken: - if (canParseTag) { - const child = tryParseChildTag(target, indent); - if (child && (child.kind === SyntaxKind.JSDocParameterTag || child.kind === SyntaxKind.JSDocPropertyTag) && - target !== PropertyLikeParse.CallbackParameter && - name && (ts.isIdentifier(child.name) || !escapedTextsEqual(name, child.name.left))) { - return false; - } - return child; - } - seenAsterisk = false; - break; - case SyntaxKind.NewLineTrivia: - canParseTag = true; - seenAsterisk = false; - break; - case SyntaxKind.AsteriskToken: - if (seenAsterisk) { - canParseTag = false; + return a.escapedText === b.escapedText; + } + function parseChildPropertyTag(indent: number) { + return parseChildParameterOrPropertyTag(PropertyLikeParse.Property, indent) as JSDocTypeTag | JSDocPropertyTag | false; + } + function parseChildParameterOrPropertyTag(target: PropertyLikeParse, indent: number, name?: EntityName): JSDocTypeTag | JSDocPropertyTag | JSDocParameterTag | false { + let canParseTag = true; + let seenAsterisk = false; + while (true) { + switch (nextTokenJSDoc()) { + case SyntaxKind.AtToken: + if (canParseTag) { + const child = tryParseChildTag(target, indent); + if (child && (child.kind === SyntaxKind.JSDocParameterTag || child.kind === SyntaxKind.JSDocPropertyTag) && + target !== PropertyLikeParse.CallbackParameter && + name && (ts.isIdentifier(child.name) || !escapedTextsEqual(name, child.name.left))) { + return false; } - seenAsterisk = true; - break; - case SyntaxKind.Identifier: + return child; + } + seenAsterisk = false; + break; + case SyntaxKind.NewLineTrivia: + canParseTag = true; + seenAsterisk = false; + break; + case SyntaxKind.AsteriskToken: + if (seenAsterisk) { canParseTag = false; - break; - case SyntaxKind.EndOfFileToken: - return false; - } - } - } - - function tryParseChildTag(target: PropertyLikeParse, indent: number): JSDocTypeTag | JSDocPropertyTag | JSDocParameterTag | false { - Debug.assert(token() === SyntaxKind.AtToken); - const start = scanner.getStartPos(); - nextTokenJSDoc(); - - const tagName = parseJSDocIdentifierName(); - skipWhitespace(); - let t: PropertyLikeParse; - switch (tagName.escapedText) { - case "type": - return target === PropertyLikeParse.Property && parseTypeTag(start, tagName); - case "prop": - case "property": - t = PropertyLikeParse.Property; + } + seenAsterisk = true; break; - case "arg": - case "argument": - case "param": - t = PropertyLikeParse.Parameter | PropertyLikeParse.CallbackParameter; + case SyntaxKind.Identifier: + canParseTag = false; break; - default: + case SyntaxKind.EndOfFileToken: return false; } - if (!(target & t)) { + } + } + function tryParseChildTag(target: PropertyLikeParse, indent: number): JSDocTypeTag | JSDocPropertyTag | JSDocParameterTag | false { + Debug.assert(token() === SyntaxKind.AtToken); + const start = scanner.getStartPos(); + nextTokenJSDoc(); + const tagName = parseJSDocIdentifierName(); + skipWhitespace(); + let t: PropertyLikeParse; + switch (tagName.escapedText) { + case "type": + return target === PropertyLikeParse.Property && parseTypeTag(start, tagName); + case "prop": + case "property": + t = PropertyLikeParse.Property; + break; + case "arg": + case "argument": + case "param": + t = PropertyLikeParse.Parameter | PropertyLikeParse.CallbackParameter; + break; + default: return false; - } - return parseParameterOrPropertyTag(start, tagName, target, indent); } - - function parseTemplateTag(start: number, tagName: Identifier): JSDocTemplateTag { - // the template tag looks like '@template {Constraint} T,U,V' - let constraint: JSDocTypeExpression | undefined; - if (token() === SyntaxKind.OpenBraceToken) { - constraint = parseJSDocTypeExpression(); - } - - const typeParameters = []; - const typeParametersPos = getNodePos(); - do { - skipWhitespace(); - const typeParameter = createNode(SyntaxKind.TypeParameter); - typeParameter.name = parseJSDocIdentifierName(Diagnostics.Unexpected_token_A_type_parameter_name_was_expected_without_curly_braces); - finishNode(typeParameter); - skipWhitespaceOrAsterisk(); - typeParameters.push(typeParameter); - } while (parseOptionalJsdoc(SyntaxKind.CommaToken)); - - const result = createNode(SyntaxKind.JSDocTemplateTag, start); - result.tagName = tagName; - result.constraint = constraint; - result.typeParameters = createNodeArray(typeParameters, typeParametersPos); - finishNode(result); - return result; + if (!(target & t)) { + return false; + } + return parseParameterOrPropertyTag(start, tagName, target, indent); + } + function parseTemplateTag(start: number, tagName: Identifier): JSDocTemplateTag { + // the template tag looks like '@template {Constraint} T,U,V' + let constraint: JSDocTypeExpression | undefined; + if (token() === SyntaxKind.OpenBraceToken) { + constraint = parseJSDocTypeExpression(); + } + const typeParameters = []; + const typeParametersPos = getNodePos(); + do { + skipWhitespace(); + const typeParameter = (createNode(SyntaxKind.TypeParameter)); + typeParameter.name = parseJSDocIdentifierName(Diagnostics.Unexpected_token_A_type_parameter_name_was_expected_without_curly_braces); + finishNode(typeParameter); + skipWhitespaceOrAsterisk(); + typeParameters.push(typeParameter); + } while (parseOptionalJsdoc(SyntaxKind.CommaToken)); + const result = (createNode(SyntaxKind.JSDocTemplateTag, start)); + result.tagName = tagName; + result.constraint = constraint; + result.typeParameters = createNodeArray(typeParameters, typeParametersPos); + finishNode(result); + return result; + } + function parseOptionalJsdoc(t: JSDocSyntaxKind): boolean { + if (token() === t) { + nextTokenJSDoc(); + return true; } - - function parseOptionalJsdoc(t: JSDocSyntaxKind): boolean { - if (token() === t) { - nextTokenJSDoc(); - return true; - } - return false; + return false; + } + function parseJSDocEntityName(): EntityName { + let entity: EntityName = parseJSDocIdentifierName(); + if (parseOptional(SyntaxKind.OpenBracketToken)) { + parseExpected(SyntaxKind.CloseBracketToken); + // Note that y[] is accepted as an entity name, but the postfix brackets are not saved for checking. + // Technically usejsdoc.org requires them for specifying a property of a type equivalent to Array<{ x: ...}> + // but it's not worth it to enforce that restriction. } - - function parseJSDocEntityName(): EntityName { - let entity: EntityName = parseJSDocIdentifierName(); + while (parseOptional(SyntaxKind.DotToken)) { + const name = parseJSDocIdentifierName(); if (parseOptional(SyntaxKind.OpenBracketToken)) { parseExpected(SyntaxKind.CloseBracketToken); - // Note that y[] is accepted as an entity name, but the postfix brackets are not saved for checking. - // Technically usejsdoc.org requires them for specifying a property of a type equivalent to Array<{ x: ...}> - // but it's not worth it to enforce that restriction. - } - while (parseOptional(SyntaxKind.DotToken)) { - const name = parseJSDocIdentifierName(); - if (parseOptional(SyntaxKind.OpenBracketToken)) { - parseExpected(SyntaxKind.CloseBracketToken); - } - entity = createQualifiedName(entity, name); } - return entity; + entity = createQualifiedName(entity, name); } - - function parseJSDocIdentifierName(message?: DiagnosticMessage): Identifier { - if (!tokenIsIdentifierOrKeyword(token())) { - return createMissingNode(SyntaxKind.Identifier, /*reportAtCurrentPosition*/ !message, message || Diagnostics.Identifier_expected); - } - - identifierCount++; - const pos = scanner.getTokenPos(); - const end = scanner.getTextPos(); - const result = createNode(SyntaxKind.Identifier, pos); - if (token() !== SyntaxKind.Identifier) { - result.originalKeywordKind = token(); - } - result.escapedText = escapeLeadingUnderscores(internIdentifier(scanner.getTokenValue())); - finishNode(result, end); - - nextTokenJSDoc(); - return result; + return entity; + } + function parseJSDocIdentifierName(message?: DiagnosticMessage): Identifier { + if (!tokenIsIdentifierOrKeyword(token())) { + return createMissingNode(SyntaxKind.Identifier, /*reportAtCurrentPosition*/ !message, message || Diagnostics.Identifier_expected); + } + identifierCount++; + const pos = scanner.getTokenPos(); + const end = scanner.getTextPos(); + const result = (createNode(SyntaxKind.Identifier, pos)); + if (token() !== SyntaxKind.Identifier) { + result.originalKeywordKind = token(); } + result.escapedText = escapeLeadingUnderscores(internIdentifier(scanner.getTokenValue())); + finishNode(result, end); + nextTokenJSDoc(); + return result; } } } - - namespace IncrementalParser { - export function updateSourceFile(sourceFile: SourceFile, newText: string, textChangeRange: TextChangeRange, aggressiveChecks: boolean): SourceFile { - aggressiveChecks = aggressiveChecks || Debug.shouldAssert(AssertionLevel.Aggressive); - - checkChangeRange(sourceFile, newText, textChangeRange, aggressiveChecks); - if (textChangeRangeIsUnchanged(textChangeRange)) { - // if the text didn't change, then we can just return our current source file as-is. - return sourceFile; - } - - if (sourceFile.statements.length === 0) { - // If we don't have any statements in the current source file, then there's no real - // way to incrementally parse. So just do a full parse instead. - return Parser.parseSourceFile(sourceFile.fileName, newText, sourceFile.languageVersion, /*syntaxCursor*/ undefined, /*setParentNodes*/ true, sourceFile.scriptKind); - } - - // Make sure we're not trying to incrementally update a source file more than once. Once - // we do an update the original source file is considered unusable from that point onwards. - // - // This is because we do incremental parsing in-place. i.e. we take nodes from the old - // tree and give them new positions and parents. From that point on, trusting the old - // tree at all is not possible as far too much of it may violate invariants. - const incrementalSourceFile = sourceFile; - Debug.assert(!incrementalSourceFile.hasBeenIncrementallyParsed); - incrementalSourceFile.hasBeenIncrementallyParsed = true; - - const oldText = sourceFile.text; - const syntaxCursor = createSyntaxCursor(sourceFile); - - // Make the actual change larger so that we know to reparse anything whose lookahead - // might have intersected the change. - const changeRange = extendToAffectedRange(sourceFile, textChangeRange); - checkChangeRange(sourceFile, newText, changeRange, aggressiveChecks); - - // Ensure that extending the affected range only moved the start of the change range - // earlier in the file. - Debug.assert(changeRange.span.start <= textChangeRange.span.start); - Debug.assert(textSpanEnd(changeRange.span) === textSpanEnd(textChangeRange.span)); - Debug.assert(textSpanEnd(textChangeRangeNewSpan(changeRange)) === textSpanEnd(textChangeRangeNewSpan(textChangeRange))); - - // The is the amount the nodes after the edit range need to be adjusted. It can be - // positive (if the edit added characters), negative (if the edit deleted characters) - // or zero (if this was a pure overwrite with nothing added/removed). - const delta = textChangeRangeNewSpan(changeRange).length - changeRange.span.length; - - // If we added or removed characters during the edit, then we need to go and adjust all - // the nodes after the edit. Those nodes may move forward (if we inserted chars) or they - // may move backward (if we deleted chars). - // - // Doing this helps us out in two ways. First, it means that any nodes/tokens we want - // to reuse are already at the appropriate position in the new text. That way when we - // reuse them, we don't have to figure out if they need to be adjusted. Second, it makes - // it very easy to determine if we can reuse a node. If the node's position is at where - // we are in the text, then we can reuse it. Otherwise we can't. If the node's position - // is ahead of us, then we'll need to rescan tokens. If the node's position is behind - // us, then we'll need to skip it or crumble it as appropriate - // - // We will also adjust the positions of nodes that intersect the change range as well. - // By doing this, we ensure that all the positions in the old tree are consistent, not - // just the positions of nodes entirely before/after the change range. By being - // consistent, we can then easily map from positions to nodes in the old tree easily. - // - // Also, mark any syntax elements that intersect the changed span. We know, up front, - // that we cannot reuse these elements. - updateTokenPositionsAndMarkElements(incrementalSourceFile, - changeRange.span.start, textSpanEnd(changeRange.span), textSpanEnd(textChangeRangeNewSpan(changeRange)), delta, oldText, newText, aggressiveChecks); - - // Now that we've set up our internal incremental state just proceed and parse the - // source file in the normal fashion. When possible the parser will retrieve and - // reuse nodes from the old tree. - // - // Note: passing in 'true' for setNodeParents is very important. When incrementally - // parsing, we will be reusing nodes from the old tree, and placing it into new - // parents. If we don't set the parents now, we'll end up with an observably - // inconsistent tree. Setting the parents on the new tree should be very fast. We - // will immediately bail out of walking any subtrees when we can see that their parents - // are already correct. - const result = Parser.parseSourceFile(sourceFile.fileName, newText, sourceFile.languageVersion, syntaxCursor, /*setParentNodes*/ true, sourceFile.scriptKind); - - return result; +} +namespace IncrementalParser { + export function updateSourceFile(sourceFile: SourceFile, newText: string, textChangeRange: TextChangeRange, aggressiveChecks: boolean): SourceFile { + aggressiveChecks = aggressiveChecks || Debug.shouldAssert(AssertionLevel.Aggressive); + checkChangeRange(sourceFile, newText, textChangeRange, aggressiveChecks); + if (textChangeRangeIsUnchanged(textChangeRange)) { + // if the text didn't change, then we can just return our current source file as-is. + return sourceFile; + } + if (sourceFile.statements.length === 0) { + // If we don't have any statements in the current source file, then there's no real + // way to incrementally parse. So just do a full parse instead. + return Parser.parseSourceFile(sourceFile.fileName, newText, sourceFile.languageVersion, /*syntaxCursor*/ undefined, /*setParentNodes*/ true, sourceFile.scriptKind); + } + // Make sure we're not trying to incrementally update a source file more than once. Once + // we do an update the original source file is considered unusable from that point onwards. + // + // This is because we do incremental parsing in-place. i.e. we take nodes from the old + // tree and give them new positions and parents. From that point on, trusting the old + // tree at all is not possible as far too much of it may violate invariants. + const incrementalSourceFile = (sourceFile); + Debug.assert(!incrementalSourceFile.hasBeenIncrementallyParsed); + incrementalSourceFile.hasBeenIncrementallyParsed = true; + const oldText = sourceFile.text; + const syntaxCursor = createSyntaxCursor(sourceFile); + // Make the actual change larger so that we know to reparse anything whose lookahead + // might have intersected the change. + const changeRange = extendToAffectedRange(sourceFile, textChangeRange); + checkChangeRange(sourceFile, newText, changeRange, aggressiveChecks); + // Ensure that extending the affected range only moved the start of the change range + // earlier in the file. + Debug.assert(changeRange.span.start <= textChangeRange.span.start); + Debug.assert(textSpanEnd(changeRange.span) === textSpanEnd(textChangeRange.span)); + Debug.assert(textSpanEnd(textChangeRangeNewSpan(changeRange)) === textSpanEnd(textChangeRangeNewSpan(textChangeRange))); + // The is the amount the nodes after the edit range need to be adjusted. It can be + // positive (if the edit added characters), negative (if the edit deleted characters) + // or zero (if this was a pure overwrite with nothing added/removed). + const delta = textChangeRangeNewSpan(changeRange).length - changeRange.span.length; + // If we added or removed characters during the edit, then we need to go and adjust all + // the nodes after the edit. Those nodes may move forward (if we inserted chars) or they + // may move backward (if we deleted chars). + // + // Doing this helps us out in two ways. First, it means that any nodes/tokens we want + // to reuse are already at the appropriate position in the new text. That way when we + // reuse them, we don't have to figure out if they need to be adjusted. Second, it makes + // it very easy to determine if we can reuse a node. If the node's position is at where + // we are in the text, then we can reuse it. Otherwise we can't. If the node's position + // is ahead of us, then we'll need to rescan tokens. If the node's position is behind + // us, then we'll need to skip it or crumble it as appropriate + // + // We will also adjust the positions of nodes that intersect the change range as well. + // By doing this, we ensure that all the positions in the old tree are consistent, not + // just the positions of nodes entirely before/after the change range. By being + // consistent, we can then easily map from positions to nodes in the old tree easily. + // + // Also, mark any syntax elements that intersect the changed span. We know, up front, + // that we cannot reuse these elements. + updateTokenPositionsAndMarkElements(incrementalSourceFile, changeRange.span.start, textSpanEnd(changeRange.span), textSpanEnd(textChangeRangeNewSpan(changeRange)), delta, oldText, newText, aggressiveChecks); + // Now that we've set up our internal incremental state just proceed and parse the + // source file in the normal fashion. When possible the parser will retrieve and + // reuse nodes from the old tree. + // + // Note: passing in 'true' for setNodeParents is very important. When incrementally + // parsing, we will be reusing nodes from the old tree, and placing it into new + // parents. If we don't set the parents now, we'll end up with an observably + // inconsistent tree. Setting the parents on the new tree should be very fast. We + // will immediately bail out of walking any subtrees when we can see that their parents + // are already correct. + const result = Parser.parseSourceFile(sourceFile.fileName, newText, sourceFile.languageVersion, syntaxCursor, /*setParentNodes*/ true, sourceFile.scriptKind); + return result; + } + function moveElementEntirelyPastChangeRange(element: IncrementalElement, isArray: boolean, delta: number, oldText: string, newText: string, aggressiveChecks: boolean) { + if (isArray) { + visitArray(element); + } + else { + visitNode(element); } - - function moveElementEntirelyPastChangeRange(element: IncrementalElement, isArray: boolean, delta: number, oldText: string, newText: string, aggressiveChecks: boolean) { - if (isArray) { - visitArray(element); + return; + function visitNode(node: IncrementalNode) { + let text = ""; + if (aggressiveChecks && shouldCheckNode(node)) { + text = oldText.substring(node.pos, node.end); } - else { - visitNode(element); + // Ditch any existing LS children we may have created. This way we can avoid + // moving them forward. + if (node._children) { + node._children = undefined; } - return; - - function visitNode(node: IncrementalNode) { - let text = ""; - if (aggressiveChecks && shouldCheckNode(node)) { - text = oldText.substring(node.pos, node.end); - } - - // Ditch any existing LS children we may have created. This way we can avoid - // moving them forward. - if (node._children) { - node._children = undefined; + node.pos += delta; + node.end += delta; + if (aggressiveChecks && shouldCheckNode(node)) { + Debug.assert(text === newText.substring(node.pos, node.end)); + } + forEachChild(node, visitNode, visitArray); + if (hasJSDocNodes(node)) { + for (const jsDocComment of node.jsDoc!) { + visitNode((jsDocComment)); } - - node.pos += delta; - node.end += delta; - - if (aggressiveChecks && shouldCheckNode(node)) { - Debug.assert(text === newText.substring(node.pos, node.end)); + } + checkNodePositions(node, aggressiveChecks); + } + function visitArray(array: IncrementalNodeArray) { + array._children = undefined; + array.pos += delta; + array.end += delta; + for (const node of array) { + visitNode(node); + } + } + } + function shouldCheckNode(node: Node) { + switch (node.kind) { + case SyntaxKind.StringLiteral: + case SyntaxKind.NumericLiteral: + case SyntaxKind.Identifier: + return true; + } + return false; + } + function adjustIntersectingElement(element: IncrementalElement, changeStart: number, changeRangeOldEnd: number, changeRangeNewEnd: number, delta: number) { + Debug.assert(element.end >= changeStart, "Adjusting an element that was entirely before the change range"); + Debug.assert(element.pos <= changeRangeOldEnd, "Adjusting an element that was entirely after the change range"); + Debug.assert(element.pos <= element.end); + // We have an element that intersects the change range in some way. It may have its + // start, or its end (or both) in the changed range. We want to adjust any part + // that intersects such that the final tree is in a consistent state. i.e. all + // children have spans within the span of their parent, and all siblings are ordered + // properly. + // We may need to update both the 'pos' and the 'end' of the element. + // If the 'pos' is before the start of the change, then we don't need to touch it. + // If it isn't, then the 'pos' must be inside the change. How we update it will + // depend if delta is positive or negative. If delta is positive then we have + // something like: + // + // -------------------AAA----------------- + // -------------------BBBCCCCCCC----------------- + // + // In this case, we consider any node that started in the change range to still be + // starting at the same position. + // + // however, if the delta is negative, then we instead have something like this: + // + // -------------------XXXYYYYYYY----------------- + // -------------------ZZZ----------------- + // + // In this case, any element that started in the 'X' range will keep its position. + // However any element that started after that will have their pos adjusted to be + // at the end of the new range. i.e. any node that started in the 'Y' range will + // be adjusted to have their start at the end of the 'Z' range. + // + // The element will keep its position if possible. Or Move backward to the new-end + // if it's in the 'Y' range. + element.pos = Math.min(element.pos, changeRangeNewEnd); + // If the 'end' is after the change range, then we always adjust it by the delta + // amount. However, if the end is in the change range, then how we adjust it + // will depend on if delta is positive or negative. If delta is positive then we + // have something like: + // + // -------------------AAA----------------- + // -------------------BBBCCCCCCC----------------- + // + // In this case, we consider any node that ended inside the change range to keep its + // end position. + // + // however, if the delta is negative, then we instead have something like this: + // + // -------------------XXXYYYYYYY----------------- + // -------------------ZZZ----------------- + // + // In this case, any element that ended in the 'X' range will keep its position. + // However any element that ended after that will have their pos adjusted to be + // at the end of the new range. i.e. any node that ended in the 'Y' range will + // be adjusted to have their end at the end of the 'Z' range. + if (element.end >= changeRangeOldEnd) { + // Element ends after the change range. Always adjust the end pos. + element.end += delta; + } + else { + // Element ends in the change range. The element will keep its position if + // possible. Or Move backward to the new-end if it's in the 'Y' range. + element.end = Math.min(element.end, changeRangeNewEnd); + } + Debug.assert(element.pos <= element.end); + if (element.parent) { + Debug.assert(element.pos >= element.parent.pos); + Debug.assert(element.end <= element.parent.end); + } + } + function checkNodePositions(node: Node, aggressiveChecks: boolean) { + if (aggressiveChecks) { + let pos = node.pos; + const visitNode = (child: Node) => { + Debug.assert(child.pos >= pos); + pos = child.end; + }; + if (hasJSDocNodes(node)) { + for (const jsDocComment of node.jsDoc!) { + visitNode(jsDocComment); } - - forEachChild(node, visitNode, visitArray); - if (hasJSDocNodes(node)) { - for (const jsDocComment of node.jsDoc!) { - visitNode(jsDocComment); + } + forEachChild(node, visitNode); + Debug.assert(pos <= node.end); + } + } + function updateTokenPositionsAndMarkElements(sourceFile: IncrementalNode, changeStart: number, changeRangeOldEnd: number, changeRangeNewEnd: number, delta: number, oldText: string, newText: string, aggressiveChecks: boolean): void { + visitNode(sourceFile); + return; + function visitNode(child: IncrementalNode) { + Debug.assert(child.pos <= child.end); + if (child.pos > changeRangeOldEnd) { + // Node is entirely past the change range. We need to move both its pos and + // end, forward or backward appropriately. + moveElementEntirelyPastChangeRange(child, /*isArray*/ false, delta, oldText, newText, aggressiveChecks); + return; + } + // Check if the element intersects the change range. If it does, then it is not + // reusable. Also, we'll need to recurse to see what constituent portions we may + // be able to use. + const fullEnd = child.end; + if (fullEnd >= changeStart) { + child.intersectsChange = true; + child._children = undefined; + // Adjust the pos or end (or both) of the intersecting element accordingly. + adjustIntersectingElement(child, changeStart, changeRangeOldEnd, changeRangeNewEnd, delta); + forEachChild(child, visitNode, visitArray); + if (hasJSDocNodes(child)) { + for (const jsDocComment of child.jsDoc!) { + visitNode((jsDocComment)); } } - checkNodePositions(node, aggressiveChecks); + checkNodePositions(child, aggressiveChecks); + return; + } + // Otherwise, the node is entirely before the change range. No need to do anything with it. + Debug.assert(fullEnd < changeStart); + } + function visitArray(array: IncrementalNodeArray) { + Debug.assert(array.pos <= array.end); + if (array.pos > changeRangeOldEnd) { + // Array is entirely after the change range. We need to move it, and move any of + // its children. + moveElementEntirelyPastChangeRange(array, /*isArray*/ true, delta, oldText, newText, aggressiveChecks); + return; } - - function visitArray(array: IncrementalNodeArray) { + // Check if the element intersects the change range. If it does, then it is not + // reusable. Also, we'll need to recurse to see what constituent portions we may + // be able to use. + const fullEnd = array.end; + if (fullEnd >= changeStart) { + array.intersectsChange = true; array._children = undefined; - array.pos += delta; - array.end += delta; - + // Adjust the pos or end (or both) of the intersecting array accordingly. + adjustIntersectingElement(array, changeStart, changeRangeOldEnd, changeRangeNewEnd, delta); for (const node of array) { visitNode(node); } + return; } + // Otherwise, the array is entirely before the change range. No need to do anything with it. + Debug.assert(fullEnd < changeStart); } - - function shouldCheckNode(node: Node) { - switch (node.kind) { - case SyntaxKind.StringLiteral: - case SyntaxKind.NumericLiteral: - case SyntaxKind.Identifier: - return true; - } - - return false; - } - - function adjustIntersectingElement(element: IncrementalElement, changeStart: number, changeRangeOldEnd: number, changeRangeNewEnd: number, delta: number) { - Debug.assert(element.end >= changeStart, "Adjusting an element that was entirely before the change range"); - Debug.assert(element.pos <= changeRangeOldEnd, "Adjusting an element that was entirely after the change range"); - Debug.assert(element.pos <= element.end); - - // We have an element that intersects the change range in some way. It may have its - // start, or its end (or both) in the changed range. We want to adjust any part - // that intersects such that the final tree is in a consistent state. i.e. all - // children have spans within the span of their parent, and all siblings are ordered - // properly. - - // We may need to update both the 'pos' and the 'end' of the element. - - // If the 'pos' is before the start of the change, then we don't need to touch it. - // If it isn't, then the 'pos' must be inside the change. How we update it will - // depend if delta is positive or negative. If delta is positive then we have - // something like: - // - // -------------------AAA----------------- - // -------------------BBBCCCCCCC----------------- - // - // In this case, we consider any node that started in the change range to still be - // starting at the same position. - // - // however, if the delta is negative, then we instead have something like this: - // - // -------------------XXXYYYYYYY----------------- - // -------------------ZZZ----------------- - // - // In this case, any element that started in the 'X' range will keep its position. - // However any element that started after that will have their pos adjusted to be - // at the end of the new range. i.e. any node that started in the 'Y' range will - // be adjusted to have their start at the end of the 'Z' range. - // - // The element will keep its position if possible. Or Move backward to the new-end - // if it's in the 'Y' range. - element.pos = Math.min(element.pos, changeRangeNewEnd); - - // If the 'end' is after the change range, then we always adjust it by the delta - // amount. However, if the end is in the change range, then how we adjust it - // will depend on if delta is positive or negative. If delta is positive then we - // have something like: - // - // -------------------AAA----------------- - // -------------------BBBCCCCCCC----------------- - // - // In this case, we consider any node that ended inside the change range to keep its - // end position. - // - // however, if the delta is negative, then we instead have something like this: - // - // -------------------XXXYYYYYYY----------------- - // -------------------ZZZ----------------- - // - // In this case, any element that ended in the 'X' range will keep its position. - // However any element that ended after that will have their pos adjusted to be - // at the end of the new range. i.e. any node that ended in the 'Y' range will - // be adjusted to have their end at the end of the 'Z' range. - if (element.end >= changeRangeOldEnd) { - // Element ends after the change range. Always adjust the end pos. - element.end += delta; - } - else { - // Element ends in the change range. The element will keep its position if - // possible. Or Move backward to the new-end if it's in the 'Y' range. - element.end = Math.min(element.end, changeRangeNewEnd); - } - - Debug.assert(element.pos <= element.end); - if (element.parent) { - Debug.assert(element.pos >= element.parent.pos); - Debug.assert(element.end <= element.parent.end); - } - } - - function checkNodePositions(node: Node, aggressiveChecks: boolean) { - if (aggressiveChecks) { - let pos = node.pos; - const visitNode = (child: Node) => { - Debug.assert(child.pos >= pos); - pos = child.end; - }; - if (hasJSDocNodes(node)) { - for (const jsDocComment of node.jsDoc!) { - visitNode(jsDocComment); - } - } - forEachChild(node, visitNode); - Debug.assert(pos <= node.end); - } - } - - function updateTokenPositionsAndMarkElements( - sourceFile: IncrementalNode, - changeStart: number, - changeRangeOldEnd: number, - changeRangeNewEnd: number, - delta: number, - oldText: string, - newText: string, - aggressiveChecks: boolean): void { - - visitNode(sourceFile); - return; - - function visitNode(child: IncrementalNode) { - Debug.assert(child.pos <= child.end); - if (child.pos > changeRangeOldEnd) { - // Node is entirely past the change range. We need to move both its pos and - // end, forward or backward appropriately. - moveElementEntirelyPastChangeRange(child, /*isArray*/ false, delta, oldText, newText, aggressiveChecks); - return; - } - - // Check if the element intersects the change range. If it does, then it is not - // reusable. Also, we'll need to recurse to see what constituent portions we may - // be able to use. - const fullEnd = child.end; - if (fullEnd >= changeStart) { - child.intersectsChange = true; - child._children = undefined; - - // Adjust the pos or end (or both) of the intersecting element accordingly. - adjustIntersectingElement(child, changeStart, changeRangeOldEnd, changeRangeNewEnd, delta); - forEachChild(child, visitNode, visitArray); - if (hasJSDocNodes(child)) { - for (const jsDocComment of child.jsDoc!) { - visitNode(jsDocComment); - } - } - checkNodePositions(child, aggressiveChecks); - return; - } - - // Otherwise, the node is entirely before the change range. No need to do anything with it. - Debug.assert(fullEnd < changeStart); - } - - function visitArray(array: IncrementalNodeArray) { - Debug.assert(array.pos <= array.end); - if (array.pos > changeRangeOldEnd) { - // Array is entirely after the change range. We need to move it, and move any of - // its children. - moveElementEntirelyPastChangeRange(array, /*isArray*/ true, delta, oldText, newText, aggressiveChecks); - return; + } + function extendToAffectedRange(sourceFile: SourceFile, changeRange: TextChangeRange): TextChangeRange { + // Consider the following code: + // void foo() { /; } + // + // If the text changes with an insertion of / just before the semicolon then we end up with: + // void foo() { //; } + // + // If we were to just use the changeRange a is, then we would not rescan the { token + // (as it does not intersect the actual original change range). Because an edit may + // change the token touching it, we actually need to look back *at least* one token so + // that the prior token sees that change. + const maxLookahead = 1; + let start = changeRange.span.start; + // the first iteration aligns us with the change start. subsequent iteration move us to + // the left by maxLookahead tokens. We only need to do this as long as we're not at the + // start of the tree. + for (let i = 0; start > 0 && i <= maxLookahead; i++) { + const nearestNode = findNearestNodeStartingBeforeOrAtPosition(sourceFile, start); + Debug.assert(nearestNode.pos <= start); + const position = nearestNode.pos; + start = Math.max(0, position - 1); + } + const finalSpan = createTextSpanFromBounds(start, textSpanEnd(changeRange.span)); + const finalLength = changeRange.newLength + (changeRange.span.start - start); + return createTextChangeRange(finalSpan, finalLength); + } + function findNearestNodeStartingBeforeOrAtPosition(sourceFile: SourceFile, position: number): Node { + let bestResult: Node = sourceFile; + let lastNodeEntirelyBeforePosition: Node | undefined; + forEachChild(sourceFile, visit); + if (lastNodeEntirelyBeforePosition) { + const lastChildOfLastEntireNodeBeforePosition = getLastDescendant(lastNodeEntirelyBeforePosition); + if (lastChildOfLastEntireNodeBeforePosition.pos > bestResult.pos) { + bestResult = lastChildOfLastEntireNodeBeforePosition; + } + } + return bestResult; + function getLastDescendant(node: Node): Node { + while (true) { + const lastChild = getLastChild(node); + if (lastChild) { + node = lastChild; } - - // Check if the element intersects the change range. If it does, then it is not - // reusable. Also, we'll need to recurse to see what constituent portions we may - // be able to use. - const fullEnd = array.end; - if (fullEnd >= changeStart) { - array.intersectsChange = true; - array._children = undefined; - - // Adjust the pos or end (or both) of the intersecting array accordingly. - adjustIntersectingElement(array, changeStart, changeRangeOldEnd, changeRangeNewEnd, delta); - for (const node of array) { - visitNode(node); - } - return; + else { + return node; } - - // Otherwise, the array is entirely before the change range. No need to do anything with it. - Debug.assert(fullEnd < changeStart); } } - - function extendToAffectedRange(sourceFile: SourceFile, changeRange: TextChangeRange): TextChangeRange { - // Consider the following code: - // void foo() { /; } - // - // If the text changes with an insertion of / just before the semicolon then we end up with: - // void foo() { //; } - // - // If we were to just use the changeRange a is, then we would not rescan the { token - // (as it does not intersect the actual original change range). Because an edit may - // change the token touching it, we actually need to look back *at least* one token so - // that the prior token sees that change. - const maxLookahead = 1; - - let start = changeRange.span.start; - - // the first iteration aligns us with the change start. subsequent iteration move us to - // the left by maxLookahead tokens. We only need to do this as long as we're not at the - // start of the tree. - for (let i = 0; start > 0 && i <= maxLookahead; i++) { - const nearestNode = findNearestNodeStartingBeforeOrAtPosition(sourceFile, start); - Debug.assert(nearestNode.pos <= start); - const position = nearestNode.pos; - - start = Math.max(0, position - 1); - } - - const finalSpan = createTextSpanFromBounds(start, textSpanEnd(changeRange.span)); - const finalLength = changeRange.newLength + (changeRange.span.start - start); - - return createTextChangeRange(finalSpan, finalLength); - } - - function findNearestNodeStartingBeforeOrAtPosition(sourceFile: SourceFile, position: number): Node { - let bestResult: Node = sourceFile; - let lastNodeEntirelyBeforePosition: Node | undefined; - - forEachChild(sourceFile, visit); - - if (lastNodeEntirelyBeforePosition) { - const lastChildOfLastEntireNodeBeforePosition = getLastDescendant(lastNodeEntirelyBeforePosition); - if (lastChildOfLastEntireNodeBeforePosition.pos > bestResult.pos) { - bestResult = lastChildOfLastEntireNodeBeforePosition; - } - } - - return bestResult; - - function getLastDescendant(node: Node): Node { - while (true) { - const lastChild = getLastChild(node); - if (lastChild) { - node = lastChild; - } - else { - return node; - } - } + function visit(child: Node) { + if (nodeIsMissing(child)) { + // Missing nodes are effectively invisible to us. We never even consider them + // When trying to find the nearest node before us. + return; } - - function visit(child: Node) { - if (nodeIsMissing(child)) { - // Missing nodes are effectively invisible to us. We never even consider them - // When trying to find the nearest node before us. - return; - } - - // If the child intersects this position, then this node is currently the nearest - // node that starts before the position. - if (child.pos <= position) { - if (child.pos >= bestResult.pos) { - // This node starts before the position, and is closer to the position than - // the previous best node we found. It is now the new best node. - bestResult = child; - } - - // Now, the node may overlap the position, or it may end entirely before the - // position. If it overlaps with the position, then either it, or one of its - // children must be the nearest node before the position. So we can just - // recurse into this child to see if we can find something better. - if (position < child.end) { - // The nearest node is either this child, or one of the children inside - // of it. We've already marked this child as the best so far. Recurse - // in case one of the children is better. - forEachChild(child, visit); - - // Once we look at the children of this node, then there's no need to - // continue any further. - return true; - } - else { - Debug.assert(child.end <= position); - // The child ends entirely before this position. Say you have the following - // (where $ is the position) - // - // ? $ : <...> <...> - // - // We would want to find the nearest preceding node in "complex expr 2". - // To support that, we keep track of this node, and once we're done searching - // for a best node, we recurse down this node to see if we can find a good - // result in it. - // - // This approach allows us to quickly skip over nodes that are entirely - // before the position, while still allowing us to find any nodes in the - // last one that might be what we want. - lastNodeEntirelyBeforePosition = child; - } + // If the child intersects this position, then this node is currently the nearest + // node that starts before the position. + if (child.pos <= position) { + if (child.pos >= bestResult.pos) { + // This node starts before the position, and is closer to the position than + // the previous best node we found. It is now the new best node. + bestResult = child; + } + // Now, the node may overlap the position, or it may end entirely before the + // position. If it overlaps with the position, then either it, or one of its + // children must be the nearest node before the position. So we can just + // recurse into this child to see if we can find something better. + if (position < child.end) { + // The nearest node is either this child, or one of the children inside + // of it. We've already marked this child as the best so far. Recurse + // in case one of the children is better. + forEachChild(child, visit); + // Once we look at the children of this node, then there's no need to + // continue any further. + return true; } else { - Debug.assert(child.pos > position); - // We're now at a node that is entirely past the position we're searching for. - // This node (and all following nodes) could never contribute to the result, - // so just skip them by returning 'true' here. - return true; + Debug.assert(child.end <= position); + // The child ends entirely before this position. Say you have the following + // (where $ is the position) + // + // ? $ : <...> <...> + // + // We would want to find the nearest preceding node in "complex expr 2". + // To support that, we keep track of this node, and once we're done searching + // for a best node, we recurse down this node to see if we can find a good + // result in it. + // + // This approach allows us to quickly skip over nodes that are entirely + // before the position, while still allowing us to find any nodes in the + // last one that might be what we want. + lastNodeEntirelyBeforePosition = child; } } + else { + Debug.assert(child.pos > position); + // We're now at a node that is entirely past the position we're searching for. + // This node (and all following nodes) could never contribute to the result, + // so just skip them by returning 'true' here. + return true; + } } - - function checkChangeRange(sourceFile: SourceFile, newText: string, textChangeRange: TextChangeRange, aggressiveChecks: boolean) { - const oldText = sourceFile.text; - if (textChangeRange) { - Debug.assert((oldText.length - textChangeRange.span.length + textChangeRange.newLength) === newText.length); - - if (aggressiveChecks || Debug.shouldAssert(AssertionLevel.VeryAggressive)) { - const oldTextPrefix = oldText.substr(0, textChangeRange.span.start); - const newTextPrefix = newText.substr(0, textChangeRange.span.start); - Debug.assert(oldTextPrefix === newTextPrefix); - - const oldTextSuffix = oldText.substring(textSpanEnd(textChangeRange.span), oldText.length); - const newTextSuffix = newText.substring(textSpanEnd(textChangeRangeNewSpan(textChangeRange)), newText.length); - Debug.assert(oldTextSuffix === newTextSuffix); - } + } + function checkChangeRange(sourceFile: SourceFile, newText: string, textChangeRange: TextChangeRange, aggressiveChecks: boolean) { + const oldText = sourceFile.text; + if (textChangeRange) { + Debug.assert((oldText.length - textChangeRange.span.length + textChangeRange.newLength) === newText.length); + if (aggressiveChecks || Debug.shouldAssert(AssertionLevel.VeryAggressive)) { + const oldTextPrefix = oldText.substr(0, textChangeRange.span.start); + const newTextPrefix = newText.substr(0, textChangeRange.span.start); + Debug.assert(oldTextPrefix === newTextPrefix); + const oldTextSuffix = oldText.substring(textSpanEnd(textChangeRange.span), oldText.length); + const newTextSuffix = newText.substring(textSpanEnd(textChangeRangeNewSpan(textChangeRange)), newText.length); + Debug.assert(oldTextSuffix === newTextSuffix); } } - - interface IncrementalElement extends TextRange { - parent: Node; - intersectsChange: boolean; - length?: number; - _children: Node[] | undefined; - } - - export interface IncrementalNode extends Node, IncrementalElement { - hasBeenIncrementallyParsed: boolean; - } - - interface IncrementalNodeArray extends NodeArray, IncrementalElement { - length: number; - } - - // Allows finding nodes in the source file at a certain position in an efficient manner. - // The implementation takes advantage of the calling pattern it knows the parser will - // make in order to optimize finding nodes as quickly as possible. - export interface SyntaxCursor { - currentNode(position: number): IncrementalNode; - } - - function createSyntaxCursor(sourceFile: SourceFile): SyntaxCursor { - let currentArray: NodeArray = sourceFile.statements; - let currentArrayIndex = 0; - - Debug.assert(currentArrayIndex < currentArray.length); - let current = currentArray[currentArrayIndex]; - let lastQueriedPosition = InvalidPosition.Value; - - return { - currentNode(position: number) { - // Only compute the current node if the position is different than the last time - // we were asked. The parser commonly asks for the node at the same position - // twice. Once to know if can read an appropriate list element at a certain point, - // and then to actually read and consume the node. - if (position !== lastQueriedPosition) { - // Much of the time the parser will need the very next node in the array that - // we just returned a node from.So just simply check for that case and move - // forward in the array instead of searching for the node again. - if (current && current.end === position && currentArrayIndex < (currentArray.length - 1)) { - currentArrayIndex++; - current = currentArray[currentArrayIndex]; - } - - // If we don't have a node, or the node we have isn't in the right position, - // then try to find a viable node at the position requested. - if (!current || current.pos !== position) { - findHighestListElementThatStartsAtPosition(position); - } + } + interface IncrementalElement extends TextRange { + parent: Node; + intersectsChange: boolean; + length?: number; + _children: Node[] | undefined; + } + export interface IncrementalNode extends Node, IncrementalElement { + hasBeenIncrementallyParsed: boolean; + } + interface IncrementalNodeArray extends NodeArray, IncrementalElement { + length: number; + } + // Allows finding nodes in the source file at a certain position in an efficient manner. + // The implementation takes advantage of the calling pattern it knows the parser will + // make in order to optimize finding nodes as quickly as possible. + export interface SyntaxCursor { + currentNode(position: number): IncrementalNode; + } + function createSyntaxCursor(sourceFile: SourceFile): SyntaxCursor { + let currentArray: NodeArray = sourceFile.statements; + let currentArrayIndex = 0; + Debug.assert(currentArrayIndex < currentArray.length); + let current = currentArray[currentArrayIndex]; + let lastQueriedPosition = InvalidPosition.Value; + return { + currentNode(position: number) { + // Only compute the current node if the position is different than the last time + // we were asked. The parser commonly asks for the node at the same position + // twice. Once to know if can read an appropriate list element at a certain point, + // and then to actually read and consume the node. + if (position !== lastQueriedPosition) { + // Much of the time the parser will need the very next node in the array that + // we just returned a node from.So just simply check for that case and move + // forward in the array instead of searching for the node again. + if (current && current.end === position && currentArrayIndex < (currentArray.length - 1)) { + currentArrayIndex++; + current = currentArray[currentArrayIndex]; } - - // Cache this query so that we don't do any extra work if the parser calls back - // into us. Note: this is very common as the parser will make pairs of calls like - // 'isListElement -> parseListElement'. If we were unable to find a node when - // called with 'isListElement', we don't want to redo the work when parseListElement - // is called immediately after. - lastQueriedPosition = position; - - // Either we don'd have a node, or we have a node at the position being asked for. - Debug.assert(!current || current.pos === position); - return current; - } - }; - - // Finds the highest element in the tree we can find that starts at the provided position. - // The element must be a direct child of some node list in the tree. This way after we - // return it, we can easily return its next sibling in the list. - function findHighestListElementThatStartsAtPosition(position: number) { - // Clear out any cached state about the last node we found. - currentArray = undefined!; - currentArrayIndex = InvalidPosition.Value; - current = undefined!; - - // Recurse into the source file to find the highest node at this position. - forEachChild(sourceFile, visitNode, visitArray); - return; - - function visitNode(node: Node) { - if (position >= node.pos && position < node.end) { - // Position was within this node. Keep searching deeper to find the node. - forEachChild(node, visitNode, visitArray); - - // don't proceed any further in the search. - return true; + // If we don't have a node, or the node we have isn't in the right position, + // then try to find a viable node at the position requested. + if (!current || current.pos !== position) { + findHighestListElementThatStartsAtPosition(position); } - - // position wasn't in this node, have to keep searching. - return false; } - - function visitArray(array: NodeArray) { - if (position >= array.pos && position < array.end) { - // position was in this array. Search through this array to see if we find a - // viable element. - for (let i = 0; i < array.length; i++) { - const child = array[i]; - if (child) { - if (child.pos === position) { - // Found the right node. We're done. - currentArray = array; - currentArrayIndex = i; - current = child; + // Cache this query so that we don't do any extra work if the parser calls back + // into us. Note: this is very common as the parser will make pairs of calls like + // 'isListElement -> parseListElement'. If we were unable to find a node when + // called with 'isListElement', we don't want to redo the work when parseListElement + // is called immediately after. + lastQueriedPosition = position; + // Either we don'd have a node, or we have a node at the position being asked for. + Debug.assert(!current || current.pos === position); + return current; + } + }; + // Finds the highest element in the tree we can find that starts at the provided position. + // The element must be a direct child of some node list in the tree. This way after we + // return it, we can easily return its next sibling in the list. + function findHighestListElementThatStartsAtPosition(position: number) { + // Clear out any cached state about the last node we found. + currentArray = undefined!; + currentArrayIndex = InvalidPosition.Value; + current = undefined!; + // Recurse into the source file to find the highest node at this position. + forEachChild(sourceFile, visitNode, visitArray); + return; + function visitNode(node: Node) { + if (position >= node.pos && position < node.end) { + // Position was within this node. Keep searching deeper to find the node. + forEachChild(node, visitNode, visitArray); + // don't proceed any further in the search. + return true; + } + // position wasn't in this node, have to keep searching. + return false; + } + function visitArray(array: NodeArray) { + if (position >= array.pos && position < array.end) { + // position was in this array. Search through this array to see if we find a + // viable element. + for (let i = 0; i < array.length; i++) { + const child = array[i]; + if (child) { + if (child.pos === position) { + // Found the right node. We're done. + currentArray = array; + currentArrayIndex = i; + current = child; + return true; + } + else { + if (child.pos < position && position < child.end) { + // Position in somewhere within this child. Search in it and + // stop searching in this array. + forEachChild(child, visitNode, visitArray); return true; } - else { - if (child.pos < position && position < child.end) { - // Position in somewhere within this child. Search in it and - // stop searching in this array. - forEachChild(child, visitNode, visitArray); - return true; - } - } } } } - - // position wasn't in this array, have to keep searching. - return false; } + // position wasn't in this array, have to keep searching. + return false; } } - - const enum InvalidPosition { - Value = -1 - } - } - - /** @internal */ - export function isDeclarationFileName(fileName: string): boolean { - return fileExtensionIs(fileName, Extension.Dts); - } - - /*@internal*/ - export interface PragmaContext { - languageVersion: ScriptTarget; - pragmas?: PragmaMap; - checkJsDirective?: CheckJsDirective; - referencedFiles: FileReference[]; - typeReferenceDirectives: FileReference[]; - libReferenceDirectives: FileReference[]; - amdDependencies: AmdDependency[]; - hasNoDefaultLib?: boolean; - moduleName?: string; - } - - /*@internal*/ - export function processCommentPragmas(context: PragmaContext, sourceText: string): void { - const pragmas: PragmaPseudoMapEntry[] = []; - - for (const range of getLeadingCommentRanges(sourceText, 0) || emptyArray) { - const comment = sourceText.substring(range.pos, range.end); - extractPragmas(pragmas, range, comment); - } - - context.pragmas = createMap() as PragmaMap; - for (const pragma of pragmas) { - if (context.pragmas.has(pragma.name)) { - const currentValue = context.pragmas.get(pragma.name); - if (currentValue instanceof Array) { - currentValue.push(pragma.args); - } - else { - context.pragmas.set(pragma.name, [currentValue, pragma.args]); - } - continue; + } + const enum InvalidPosition { + Value = -1 + } +} +/** @internal */ +export function isDeclarationFileName(fileName: string): boolean { + return fileExtensionIs(fileName, Extension.Dts); +} +/*@internal*/ +export interface PragmaContext { + languageVersion: ScriptTarget; + pragmas?: PragmaMap; + checkJsDirective?: CheckJsDirective; + referencedFiles: FileReference[]; + typeReferenceDirectives: FileReference[]; + libReferenceDirectives: FileReference[]; + amdDependencies: AmdDependency[]; + hasNoDefaultLib?: boolean; + moduleName?: string; +} +/*@internal*/ +export function processCommentPragmas(context: PragmaContext, sourceText: string): void { + const pragmas: PragmaPseudoMapEntry[] = []; + for (const range of getLeadingCommentRanges(sourceText, 0) || emptyArray) { + const comment = sourceText.substring(range.pos, range.end); + extractPragmas(pragmas, range, comment); + } + context.pragmas = (createMap() as PragmaMap); + for (const pragma of pragmas) { + if (context.pragmas.has(pragma.name)) { + const currentValue = context.pragmas.get(pragma.name); + if (currentValue instanceof Array) { + currentValue.push(pragma.args); } - context.pragmas.set(pragma.name, pragma.args); - } - } - - /*@internal*/ - type PragmaDiagnosticReporter = (pos: number, length: number, message: DiagnosticMessage) => void; - - /*@internal*/ - export function processPragmasIntoFields(context: PragmaContext, reportDiagnostic: PragmaDiagnosticReporter): void { - context.checkJsDirective = undefined; - context.referencedFiles = []; - context.typeReferenceDirectives = []; - context.libReferenceDirectives = []; - context.amdDependencies = []; - context.hasNoDefaultLib = false; - context.pragmas!.forEach((entryOrList, key) => { // TODO: GH#18217 - // TODO: The below should be strongly type-guarded and not need casts/explicit annotations, since entryOrList is related to - // key and key is constrained to a union; but it's not (see GH#21483 for at least partial fix) :( - switch (key) { - case "reference": { - const referencedFiles = context.referencedFiles; - const typeReferenceDirectives = context.typeReferenceDirectives; - const libReferenceDirectives = context.libReferenceDirectives; - forEach(toArray(entryOrList) as PragmaPseudoMap["reference"][], arg => { - const { types, lib, path } = arg.arguments; - if (arg.arguments["no-default-lib"]) { - context.hasNoDefaultLib = true; - } - else if (types) { - typeReferenceDirectives.push({ pos: types.pos, end: types.end, fileName: types.value }); - } - else if (lib) { - libReferenceDirectives.push({ pos: lib.pos, end: lib.end, fileName: lib.value }); - } - else if (path) { - referencedFiles.push({ pos: path.pos, end: path.end, fileName: path.value }); - } - else { - reportDiagnostic(arg.range.pos, arg.range.end - arg.range.pos, Diagnostics.Invalid_reference_directive_syntax); - } - }); - break; - } - case "amd-dependency": { - context.amdDependencies = map( - toArray(entryOrList) as PragmaPseudoMap["amd-dependency"][], - x => ({ name: x.arguments.name, path: x.arguments.path })); - break; - } - case "amd-module": { - if (entryOrList instanceof Array) { - for (const entry of entryOrList) { - if (context.moduleName) { - // TODO: It's probably fine to issue this diagnostic on all instances of the pragma - reportDiagnostic(entry.range.pos, entry.range.end - entry.range.pos, Diagnostics.An_AMD_module_cannot_have_multiple_name_assignments); - } - context.moduleName = (entry as PragmaPseudoMap["amd-module"]).arguments.name; - } + else { + context.pragmas.set(pragma.name, [currentValue, pragma.args]); + } + continue; + } + context.pragmas.set(pragma.name, pragma.args); + } +} +/*@internal*/ +type PragmaDiagnosticReporter = (pos: number, length: number, message: DiagnosticMessage) => void; +/*@internal*/ +export function processPragmasIntoFields(context: PragmaContext, reportDiagnostic: PragmaDiagnosticReporter): void { + context.checkJsDirective = undefined; + context.referencedFiles = []; + context.typeReferenceDirectives = []; + context.libReferenceDirectives = []; + context.amdDependencies = []; + context.hasNoDefaultLib = false; + context.pragmas!.forEach((entryOrList, key) => { + // TODO: The below should be strongly type-guarded and not need casts/explicit annotations, since entryOrList is related to + // key and key is constrained to a union; but it's not (see GH#21483 for at least partial fix) :( + switch (key) { + case "reference": { + const referencedFiles = context.referencedFiles; + const typeReferenceDirectives = context.typeReferenceDirectives; + const libReferenceDirectives = context.libReferenceDirectives; + forEach((toArray(entryOrList) as PragmaPseudoMap["reference"][]), arg => { + const { types, lib, path } = arg.arguments; + if (arg.arguments["no-default-lib"]) { + context.hasNoDefaultLib = true; + } + else if (types) { + typeReferenceDirectives.push({ pos: types.pos, end: types.end, fileName: types.value }); + } + else if (lib) { + libReferenceDirectives.push({ pos: lib.pos, end: lib.end, fileName: lib.value }); + } + else if (path) { + referencedFiles.push({ pos: path.pos, end: path.end, fileName: path.value }); } else { - context.moduleName = (entryOrList as PragmaPseudoMap["amd-module"]).arguments.name; + reportDiagnostic(arg.range.pos, arg.range.end - arg.range.pos, Diagnostics.Invalid_reference_directive_syntax); } - break; - } - case "ts-nocheck": - case "ts-check": { - // _last_ of either nocheck or check in a file is the "winner" - forEach(toArray(entryOrList), entry => { - if (!context.checkJsDirective || entry.range.pos > context.checkJsDirective.pos) { - context.checkJsDirective = { - enabled: key === "ts-check", - end: entry.range.end, - pos: entry.range.pos - }; - } - }); - break; - } - case "jsx": return; // Accessed directly - default: Debug.fail("Unhandled pragma kind"); // Can this be made into an assertNever in the future? + }); + break; } - }); - } - - const namedArgRegExCache = createMap(); - function getNamedArgRegEx(name: string): RegExp { - if (namedArgRegExCache.has(name)) { - return namedArgRegExCache.get(name)!; - } - const result = new RegExp(`(\\s${name}\\s*=\\s*)('|")(.+?)\\2`, "im"); - namedArgRegExCache.set(name, result); - return result; - } - - const tripleSlashXMLCommentStartRegEx = /^\/\/\/\s*<(\S+)\s.*?\/>/im; - const singleLinePragmaRegEx = /^\/\/\/?\s*@(\S+)\s*(.*)\s*$/im; - function extractPragmas(pragmas: PragmaPseudoMapEntry[], range: CommentRange, text: string) { - const tripleSlash = range.kind === SyntaxKind.SingleLineCommentTrivia && tripleSlashXMLCommentStartRegEx.exec(text); - if (tripleSlash) { - const name = tripleSlash[1].toLowerCase() as keyof PragmaPseudoMap; // Technically unsafe cast, but we do it so the below check to make it safe typechecks - const pragma = commentPragmas[name] as PragmaDefinition; - if (!pragma || !(pragma.kind! & PragmaKindFlags.TripleSlashXML)) { - return; + case "amd-dependency": { + context.amdDependencies = map((toArray(entryOrList) as PragmaPseudoMap["amd-dependency"][]), x => ({ name: x.arguments.name, path: x.arguments.path })); + break; } - if (pragma.args) { - const argument: {[index: string]: string | {value: string, pos: number, end: number}} = {}; - for (const arg of pragma.args) { - const matcher = getNamedArgRegEx(arg.name); - const matchResult = matcher.exec(text); - if (!matchResult && !arg.optional) { - return; // Missing required argument, don't parse - } - else if (matchResult) { - if (arg.captureSpan) { - const startPos = range.pos + matchResult.index + matchResult[1].length + matchResult[2].length; - argument[arg.name] = { - value: matchResult[3], - pos: startPos, - end: startPos + matchResult[3].length - }; - } - else { - argument[arg.name] = matchResult[3]; + case "amd-module": { + if (entryOrList instanceof Array) { + for (const entry of entryOrList) { + if (context.moduleName) { + // TODO: It's probably fine to issue this diagnostic on all instances of the pragma + reportDiagnostic(entry.range.pos, entry.range.end - entry.range.pos, Diagnostics.An_AMD_module_cannot_have_multiple_name_assignments); } + context.moduleName = (entry as PragmaPseudoMap["amd-module"]).arguments.name; } } - pragmas.push({ name, args: { arguments: argument, range } } as PragmaPseudoMapEntry); + else { + context.moduleName = (entryOrList as PragmaPseudoMap["amd-module"]).arguments.name; + } + break; } - else { - pragmas.push({ name, args: { arguments: {}, range } } as PragmaPseudoMapEntry); + case "ts-nocheck": + case "ts-check": { + // _last_ of either nocheck or check in a file is the "winner" + forEach(toArray(entryOrList), entry => { + if (!context.checkJsDirective || entry.range.pos > context.checkJsDirective.pos) { + context.checkJsDirective = { + enabled: key === "ts-check", + end: entry.range.end, + pos: entry.range.pos + }; + } + }); + break; } - return; + case "jsx": return; // Accessed directly + default: Debug.fail("Unhandled pragma kind"); // Can this be made into an assertNever in the future? } - - const singleLine = range.kind === SyntaxKind.SingleLineCommentTrivia && singleLinePragmaRegEx.exec(text); - if (singleLine) { - return addPragmaForMatch(pragmas, range, PragmaKindFlags.SingleLine, singleLine); + }); +} +const namedArgRegExCache = createMap(); +function getNamedArgRegEx(name: string): RegExp { + if (namedArgRegExCache.has(name)) { + return namedArgRegExCache.get(name)!; + } + const result = new RegExp(`(\\s${name}\\s*=\\s*)('|")(.+?)\\2`, "im"); + namedArgRegExCache.set(name, result); + return result; +} +const tripleSlashXMLCommentStartRegEx = /^\/\/\/\s*<(\S+)\s.*?\/>/im; +const singleLinePragmaRegEx = /^\/\/\/?\s*@(\S+)\s*(.*)\s*$/im; +function extractPragmas(pragmas: PragmaPseudoMapEntry[], range: CommentRange, text: string) { + const tripleSlash = range.kind === SyntaxKind.SingleLineCommentTrivia && tripleSlashXMLCommentStartRegEx.exec(text); + if (tripleSlash) { + const name = (tripleSlash[1].toLowerCase() as keyof PragmaPseudoMap); // Technically unsafe cast, but we do it so the below check to make it safe typechecks + const pragma = (commentPragmas[name] as PragmaDefinition); + if (!pragma || !((pragma.kind!) & PragmaKindFlags.TripleSlashXML)) { + return; } - - if (range.kind === SyntaxKind.MultiLineCommentTrivia) { - const multiLinePragmaRegEx = /\s*@(\S+)\s*(.*)\s*$/gim; // Defined inline since it uses the "g" flag, which keeps a persistent index (for iterating) - let multiLineMatch: RegExpExecArray | null; - while (multiLineMatch = multiLinePragmaRegEx.exec(text)) { - addPragmaForMatch(pragmas, range, PragmaKindFlags.MultiLine, multiLineMatch); + if (pragma.args) { + const argument: { + [index: string]: string | { + value: string; + pos: number; + end: number; + }; + } = {}; + for (const arg of pragma.args) { + const matcher = getNamedArgRegEx(arg.name); + const matchResult = matcher.exec(text); + if (!matchResult && !arg.optional) { + return; // Missing required argument, don't parse + } + else if (matchResult) { + if (arg.captureSpan) { + const startPos = range.pos + matchResult.index + matchResult[1].length + matchResult[2].length; + argument[arg.name] = { + value: matchResult[3], + pos: startPos, + end: startPos + matchResult[3].length + }; + } + else { + argument[arg.name] = matchResult[3]; + } + } } + pragmas.push(({ name, args: { arguments: argument, range } } as PragmaPseudoMapEntry)); } - } - - function addPragmaForMatch(pragmas: PragmaPseudoMapEntry[], range: CommentRange, kind: PragmaKindFlags, match: RegExpExecArray) { - if (!match) return; - const name = match[1].toLowerCase() as keyof PragmaPseudoMap; // Technically unsafe cast, but we do it so they below check to make it safe typechecks - const pragma = commentPragmas[name] as PragmaDefinition; - if (!pragma || !(pragma.kind! & kind)) { - return; + else { + pragmas.push(({ name, args: { arguments: {}, range } } as PragmaPseudoMapEntry)); } - const args = match[2]; // Split on spaces and match up positionally with definition - const argument = getNamedPragmaArguments(pragma, args); - if (argument === "fail") return; // Missing required argument, fail to parse it - pragmas.push({ name, args: { arguments: argument, range } } as PragmaPseudoMapEntry); return; } - - function getNamedPragmaArguments(pragma: PragmaDefinition, text: string | undefined): {[index: string]: string} | "fail" { - if (!text) return {}; - if (!pragma.args) return {}; - const args = text.split(/\s+/); - const argMap: {[index: string]: string} = {}; - for (let i = 0; i < pragma.args.length; i++) { - const argument = pragma.args[i]; - if (!args[i] && !argument.optional) { - return "fail"; - } - if (argument.captureSpan) { - return Debug.fail("Capture spans not yet implemented for non-xml pragmas"); - } - argMap[argument.name] = args[i]; - } - return argMap; + const singleLine = range.kind === SyntaxKind.SingleLineCommentTrivia && singleLinePragmaRegEx.exec(text); + if (singleLine) { + return addPragmaForMatch(pragmas, range, PragmaKindFlags.SingleLine, singleLine); } - - /** @internal */ - export function tagNamesAreEquivalent(lhs: JsxTagNameExpression, rhs: JsxTagNameExpression): boolean { - if (lhs.kind !== rhs.kind) { - return false; - } - - if (lhs.kind === SyntaxKind.Identifier) { - return lhs.escapedText === (rhs).escapedText; + if (range.kind === SyntaxKind.MultiLineCommentTrivia) { + const multiLinePragmaRegEx = /\s*@(\S+)\s*(.*)\s*$/gim; // Defined inline since it uses the "g" flag, which keeps a persistent index (for iterating) + let multiLineMatch: RegExpExecArray | null; + while (multiLineMatch = multiLinePragmaRegEx.exec(text)) { + addPragmaForMatch(pragmas, range, PragmaKindFlags.MultiLine, multiLineMatch); } - - if (lhs.kind === SyntaxKind.ThisKeyword) { - return true; - } - - // If we are at this statement then we must have PropertyAccessExpression and because tag name in Jsx element can only - // take forms of JsxTagNameExpression which includes an identifier, "this" expression, or another propertyAccessExpression - // it is safe to case the expression property as such. See parseJsxElementName for how we parse tag name in Jsx element - return (lhs).name.escapedText === (rhs).name.escapedText && - tagNamesAreEquivalent((lhs).expression as JsxTagNameExpression, (rhs).expression as JsxTagNameExpression); } } +function addPragmaForMatch(pragmas: PragmaPseudoMapEntry[], range: CommentRange, kind: PragmaKindFlags, match: RegExpExecArray) { + if (!match) + return; + const name = (match[1].toLowerCase() as keyof PragmaPseudoMap); // Technically unsafe cast, but we do it so they below check to make it safe typechecks + const pragma = (commentPragmas[name] as PragmaDefinition); + if (!pragma || !(pragma.kind! & kind)) { + return; + } + const args = match[2]; // Split on spaces and match up positionally with definition + const argument = getNamedPragmaArguments(pragma, args); + if (argument === "fail") + return; // Missing required argument, fail to parse it + pragmas.push(({ name, args: { arguments: argument, range } } as PragmaPseudoMapEntry)); + return; +} +function getNamedPragmaArguments(pragma: PragmaDefinition, text: string | undefined): { + [index: string]: string; +} | "fail" { + if (!text) + return {}; + if (!pragma.args) + return {}; + const args = text.split(/\s+/); + const argMap: { + [index: string]: string; + } = {}; + for (let i = 0; i < pragma.args.length; i++) { + const argument = pragma.args[i]; + if (!args[i] && !argument.optional) { + return "fail"; + } + if (argument.captureSpan) { + return Debug.fail("Capture spans not yet implemented for non-xml pragmas"); + } + argMap[argument.name] = args[i]; + } + return argMap; +} +/** @internal */ +export function tagNamesAreEquivalent(lhs: JsxTagNameExpression, rhs: JsxTagNameExpression): boolean { + if (lhs.kind !== rhs.kind) { + return false; + } + if (lhs.kind === SyntaxKind.Identifier) { + return lhs.escapedText === (rhs).escapedText; + } + if (lhs.kind === SyntaxKind.ThisKeyword) { + return true; + } + // If we are at this statement then we must have PropertyAccessExpression and because tag name in Jsx element can only + // take forms of JsxTagNameExpression which includes an identifier, "this" expression, or another propertyAccessExpression + // it is safe to case the expression property as such. See parseJsxElementName for how we parse tag name in Jsx element + return (lhs).name.escapedText === (rhs).name.escapedText && + tagNamesAreEquivalent(((lhs).expression as JsxTagNameExpression), ((rhs).expression as JsxTagNameExpression)); +} diff --git a/src/compiler/path.ts b/src/compiler/path.ts index d7d8d964d542b..f4f0da2d94602 100644 --- a/src/compiler/path.ts +++ b/src/compiler/path.ts @@ -1,855 +1,867 @@ +import { CharacterCodes, stringContains, endsWith, Path, startsWith, equateStringsCaseInsensitive, equateStringsCaseSensitive, lastOrUndefined, some, Comparison, compareStringsCaseInsensitive, compareValues, compareStringsCaseSensitive, getStringComparer, GetCanonicalFileName, Debug, identity } from "./ts"; /* @internal */ -namespace ts { - /** - * Internally, we represent paths as strings with '/' as the directory separator. - * When we make system calls (eg: LanguageServiceHost.getDirectory()), - * we expect the host to correctly handle paths in our specified format. - */ - export const directorySeparator = "/"; - const altDirectorySeparator = "\\"; - const urlSchemeSeparator = "://"; - const backslashRegExp = /\\/g; - - //// Path Tests - - /** - * Determines whether a charCode corresponds to `/` or `\`. - */ - export function isAnyDirectorySeparator(charCode: number): boolean { - return charCode === CharacterCodes.slash || charCode === CharacterCodes.backslash; - } - - /** - * Determines whether a path starts with a URL scheme (e.g. starts with `http://`, `ftp://`, `file://`, etc.). - */ - export function isUrl(path: string) { - return getEncodedRootLength(path) < 0; - } - - /** - * Determines whether a path is an absolute disk path (e.g. starts with `/`, or a dos path - * like `c:`, `c:\` or `c:/`). - */ - export function isRootedDiskPath(path: string) { - return getEncodedRootLength(path) > 0; - } - - /** - * Determines whether a path consists only of a path root. - */ - export function isDiskPathRoot(path: string) { - const rootLength = getEncodedRootLength(path); - return rootLength > 0 && rootLength === path.length; - } - - /** - * Determines whether a path starts with an absolute path component (i.e. `/`, `c:/`, `file://`, etc.). - * - * ```ts - * // POSIX - * pathIsAbsolute("/path/to/file.ext") === true - * // DOS - * pathIsAbsolute("c:/path/to/file.ext") === true - * // URL - * pathIsAbsolute("file:///path/to/file.ext") === true - * // Non-absolute - * pathIsAbsolute("path/to/file.ext") === false - * pathIsAbsolute("./path/to/file.ext") === false - * ``` - */ - export function pathIsAbsolute(path: string): boolean { - return getEncodedRootLength(path) !== 0; - } - - /** - * Determines whether a path starts with a relative path component (i.e. `.` or `..`). - */ - export function pathIsRelative(path: string): boolean { - return /^\.\.?($|[\\/])/.test(path); - } - - export function hasExtension(fileName: string): boolean { - return stringContains(getBaseFileName(fileName), "."); - } - - export function fileExtensionIs(path: string, extension: string): boolean { - return path.length > extension.length && endsWith(path, extension); - } - - export function fileExtensionIsOneOf(path: string, extensions: readonly string[]): boolean { - for (const extension of extensions) { - if (fileExtensionIs(path, extension)) { - return true; - } - } - - return false; - } - - /** - * Determines whether a path has a trailing separator (`/` or `\\`). - */ - export function hasTrailingDirectorySeparator(path: string) { - return path.length > 0 && isAnyDirectorySeparator(path.charCodeAt(path.length - 1)); - } - - //// Path Parsing - - function isVolumeCharacter(charCode: number) { - return (charCode >= CharacterCodes.a && charCode <= CharacterCodes.z) || - (charCode >= CharacterCodes.A && charCode <= CharacterCodes.Z); - } - - function getFileUrlVolumeSeparatorEnd(url: string, start: number) { - const ch0 = url.charCodeAt(start); - if (ch0 === CharacterCodes.colon) return start + 1; - if (ch0 === CharacterCodes.percent && url.charCodeAt(start + 1) === CharacterCodes._3) { - const ch2 = url.charCodeAt(start + 2); - if (ch2 === CharacterCodes.a || ch2 === CharacterCodes.A) return start + 3; +/** + * Internally, we represent paths as strings with '/' as the directory separator. + * When we make system calls (eg: LanguageServiceHost.getDirectory()), + * we expect the host to correctly handle paths in our specified format. + */ +export const directorySeparator = "/"; +/* @internal */ +const altDirectorySeparator = "\\"; +/* @internal */ +const urlSchemeSeparator = "://"; +/* @internal */ +const backslashRegExp = /\\/g; +//// Path Tests +/** + * Determines whether a charCode corresponds to `/` or `\`. + */ +/* @internal */ +export function isAnyDirectorySeparator(charCode: number): boolean { + return charCode === CharacterCodes.slash || charCode === CharacterCodes.backslash; +} +/** + * Determines whether a path starts with a URL scheme (e.g. starts with `http://`, `ftp://`, `file://`, etc.). + */ +/* @internal */ +export function isUrl(path: string) { + return getEncodedRootLength(path) < 0; +} +/** + * Determines whether a path is an absolute disk path (e.g. starts with `/`, or a dos path + * like `c:`, `c:\` or `c:/`). + */ +/* @internal */ +export function isRootedDiskPath(path: string) { + return getEncodedRootLength(path) > 0; +} +/** + * Determines whether a path consists only of a path root. + */ +/* @internal */ +export function isDiskPathRoot(path: string) { + const rootLength = getEncodedRootLength(path); + return rootLength > 0 && rootLength === path.length; +} +/** + * Determines whether a path starts with an absolute path component (i.e. `/`, `c:/`, `file://`, etc.). + * + * ```ts + * // POSIX + * pathIsAbsolute("/path/to/file.ext") === true + * // DOS + * pathIsAbsolute("c:/path/to/file.ext") === true + * // URL + * pathIsAbsolute("file:///path/to/file.ext") === true + * // Non-absolute + * pathIsAbsolute("path/to/file.ext") === false + * pathIsAbsolute("./path/to/file.ext") === false + * ``` + */ +/* @internal */ +export function pathIsAbsolute(path: string): boolean { + return getEncodedRootLength(path) !== 0; +} +/** + * Determines whether a path starts with a relative path component (i.e. `.` or `..`). + */ +/* @internal */ +export function pathIsRelative(path: string): boolean { + return /^\.\.?($|[\\/])/.test(path); +} +/* @internal */ +export function hasExtension(fileName: string): boolean { + return stringContains(getBaseFileName(fileName), "."); +} +/* @internal */ +export function fileExtensionIs(path: string, extension: string): boolean { + return path.length > extension.length && endsWith(path, extension); +} +/* @internal */ +export function fileExtensionIsOneOf(path: string, extensions: readonly string[]): boolean { + for (const extension of extensions) { + if (fileExtensionIs(path, extension)) { + return true; } - return -1; } - - /** - * Returns length of the root part of a path or URL (i.e. length of "/", "x:/", "//server/share/, file:///user/files"). - * If the root is part of a URL, the twos-complement of the root length is returned. - */ - function getEncodedRootLength(path: string): number { - if (!path) return 0; - const ch0 = path.charCodeAt(0); - - // POSIX or UNC - if (ch0 === CharacterCodes.slash || ch0 === CharacterCodes.backslash) { - if (path.charCodeAt(1) !== ch0) return 1; // POSIX: "/" (or non-normalized "\") - - const p1 = path.indexOf(ch0 === CharacterCodes.slash ? directorySeparator : altDirectorySeparator, 2); - if (p1 < 0) return path.length; // UNC: "//server" or "\\server" - - return p1 + 1; // UNC: "//server/" or "\\server\" - } - - // DOS - if (isVolumeCharacter(ch0) && path.charCodeAt(1) === CharacterCodes.colon) { - const ch2 = path.charCodeAt(2); - if (ch2 === CharacterCodes.slash || ch2 === CharacterCodes.backslash) return 3; // DOS: "c:/" or "c:\" - if (path.length === 2) return 2; // DOS: "c:" (but not "c:d") - } - - // URL - const schemeEnd = path.indexOf(urlSchemeSeparator); - if (schemeEnd !== -1) { - const authorityStart = schemeEnd + urlSchemeSeparator.length; - const authorityEnd = path.indexOf(directorySeparator, authorityStart); - if (authorityEnd !== -1) { // URL: "file:///", "file://server/", "file://server/path" - // For local "file" URLs, include the leading DOS volume (if present). - // Per https://www.ietf.org/rfc/rfc1738.txt, a host of "" or "localhost" is a - // special case interpreted as "the machine from which the URL is being interpreted". - const scheme = path.slice(0, schemeEnd); - const authority = path.slice(authorityStart, authorityEnd); - if (scheme === "file" && (authority === "" || authority === "localhost") && - isVolumeCharacter(path.charCodeAt(authorityEnd + 1))) { - const volumeSeparatorEnd = getFileUrlVolumeSeparatorEnd(path, authorityEnd + 2); - if (volumeSeparatorEnd !== -1) { - if (path.charCodeAt(volumeSeparatorEnd) === CharacterCodes.slash) { - // URL: "file:///c:/", "file://localhost/c:/", "file:///c%3a/", "file://localhost/c%3a/" - return ~(volumeSeparatorEnd + 1); - } - if (volumeSeparatorEnd === path.length) { - // URL: "file:///c:", "file://localhost/c:", "file:///c$3a", "file://localhost/c%3a" - // but not "file:///c:d" or "file:///c%3ad" - return ~volumeSeparatorEnd; - } + return false; +} +/** + * Determines whether a path has a trailing separator (`/` or `\\`). + */ +/* @internal */ +export function hasTrailingDirectorySeparator(path: string) { + return path.length > 0 && isAnyDirectorySeparator(path.charCodeAt(path.length - 1)); +} +//// Path Parsing +/* @internal */ +function isVolumeCharacter(charCode: number) { + return (charCode >= CharacterCodes.a && charCode <= CharacterCodes.z) || + (charCode >= CharacterCodes.A && charCode <= CharacterCodes.Z); +} +/* @internal */ +function getFileUrlVolumeSeparatorEnd(url: string, start: number) { + const ch0 = url.charCodeAt(start); + if (ch0 === CharacterCodes.colon) + return start + 1; + if (ch0 === CharacterCodes.percent && url.charCodeAt(start + 1) === CharacterCodes._3) { + const ch2 = url.charCodeAt(start + 2); + if (ch2 === CharacterCodes.a || ch2 === CharacterCodes.A) + return start + 3; + } + return -1; +} +/** + * Returns length of the root part of a path or URL (i.e. length of "/", "x:/", "//server/share/, file:///user/files"). + * If the root is part of a URL, the twos-complement of the root length is returned. + */ +/* @internal */ +function getEncodedRootLength(path: string): number { + if (!path) + return 0; + const ch0 = path.charCodeAt(0); + // POSIX or UNC + if (ch0 === CharacterCodes.slash || ch0 === CharacterCodes.backslash) { + if (path.charCodeAt(1) !== ch0) + return 1; // POSIX: "/" (or non-normalized "\") + const p1 = path.indexOf(ch0 === CharacterCodes.slash ? directorySeparator : altDirectorySeparator, 2); + if (p1 < 0) + return path.length; // UNC: "//server" or "\\server" + return p1 + 1; // UNC: "//server/" or "\\server\" + } + // DOS + if (isVolumeCharacter(ch0) && path.charCodeAt(1) === CharacterCodes.colon) { + const ch2 = path.charCodeAt(2); + if (ch2 === CharacterCodes.slash || ch2 === CharacterCodes.backslash) + return 3; // DOS: "c:/" or "c:\" + if (path.length === 2) + return 2; // DOS: "c:" (but not "c:d") + } + // URL + const schemeEnd = path.indexOf(urlSchemeSeparator); + if (schemeEnd !== -1) { + const authorityStart = schemeEnd + urlSchemeSeparator.length; + const authorityEnd = path.indexOf(directorySeparator, authorityStart); + if (authorityEnd !== -1) { // URL: "file:///", "file://server/", "file://server/path" + // For local "file" URLs, include the leading DOS volume (if present). + // Per https://www.ietf.org/rfc/rfc1738.txt, a host of "" or "localhost" is a + // special case interpreted as "the machine from which the URL is being interpreted". + const scheme = path.slice(0, schemeEnd); + const authority = path.slice(authorityStart, authorityEnd); + if (scheme === "file" && (authority === "" || authority === "localhost") && + isVolumeCharacter(path.charCodeAt(authorityEnd + 1))) { + const volumeSeparatorEnd = getFileUrlVolumeSeparatorEnd(path, authorityEnd + 2); + if (volumeSeparatorEnd !== -1) { + if (path.charCodeAt(volumeSeparatorEnd) === CharacterCodes.slash) { + // URL: "file:///c:/", "file://localhost/c:/", "file:///c%3a/", "file://localhost/c%3a/" + return ~(volumeSeparatorEnd + 1); + } + if (volumeSeparatorEnd === path.length) { + // URL: "file:///c:", "file://localhost/c:", "file:///c$3a", "file://localhost/c%3a" + // but not "file:///c:d" or "file:///c%3ad" + return ~volumeSeparatorEnd; } } - return ~(authorityEnd + 1); // URL: "file://server/", "http://server/" - } - return ~path.length; // URL: "file://server", "http://server" - } - - // relative - return 0; - } - - /** - * Returns length of the root part of a path or URL (i.e. length of "/", "x:/", "//server/share/, file:///user/files"). - * - * For example: - * ```ts - * getRootLength("a") === 0 // "" - * getRootLength("/") === 1 // "/" - * getRootLength("c:") === 2 // "c:" - * getRootLength("c:d") === 0 // "" - * getRootLength("c:/") === 3 // "c:/" - * getRootLength("c:\\") === 3 // "c:\\" - * getRootLength("//server") === 7 // "//server" - * getRootLength("//server/share") === 8 // "//server/" - * getRootLength("\\\\server") === 7 // "\\\\server" - * getRootLength("\\\\server\\share") === 8 // "\\\\server\\" - * getRootLength("file:///path") === 8 // "file:///" - * getRootLength("file:///c:") === 10 // "file:///c:" - * getRootLength("file:///c:d") === 8 // "file:///" - * getRootLength("file:///c:/path") === 11 // "file:///c:/" - * getRootLength("file://server") === 13 // "file://server" - * getRootLength("file://server/path") === 14 // "file://server/" - * getRootLength("http://server") === 13 // "http://server" - * getRootLength("http://server/path") === 14 // "http://server/" - * ``` - */ - export function getRootLength(path: string) { - const rootLength = getEncodedRootLength(path); - return rootLength < 0 ? ~rootLength : rootLength; - } - - /** - * Returns the path except for its basename. Semantics align with NodeJS's `path.dirname` - * except that we support URLs as well. - * - * ```ts - * // POSIX - * getDirectoryPath("/path/to/file.ext") === "/path/to" - * getDirectoryPath("/path/to/") === "/path" - * getDirectoryPath("/") === "/" - * // DOS - * getDirectoryPath("c:/path/to/file.ext") === "c:/path/to" - * getDirectoryPath("c:/path/to/") === "c:/path" - * getDirectoryPath("c:/") === "c:/" - * getDirectoryPath("c:") === "c:" - * // URL - * getDirectoryPath("http://typescriptlang.org/path/to/file.ext") === "http://typescriptlang.org/path/to" - * getDirectoryPath("http://typescriptlang.org/path/to") === "http://typescriptlang.org/path" - * getDirectoryPath("http://typescriptlang.org/") === "http://typescriptlang.org/" - * getDirectoryPath("http://typescriptlang.org") === "http://typescriptlang.org" - * ``` - */ - export function getDirectoryPath(path: Path): Path; - /** - * Returns the path except for its basename. Semantics align with NodeJS's `path.dirname` - * except that we support URLs as well. - * - * ```ts - * // POSIX - * getDirectoryPath("/path/to/file.ext") === "/path/to" - * getDirectoryPath("/path/to/") === "/path" - * getDirectoryPath("/") === "/" - * // DOS - * getDirectoryPath("c:/path/to/file.ext") === "c:/path/to" - * getDirectoryPath("c:/path/to/") === "c:/path" - * getDirectoryPath("c:/") === "c:/" - * getDirectoryPath("c:") === "c:" - * // URL - * getDirectoryPath("http://typescriptlang.org/path/to/file.ext") === "http://typescriptlang.org/path/to" - * getDirectoryPath("http://typescriptlang.org/path/to") === "http://typescriptlang.org/path" - * getDirectoryPath("http://typescriptlang.org/") === "http://typescriptlang.org/" - * getDirectoryPath("http://typescriptlang.org") === "http://typescriptlang.org" - * getDirectoryPath("file://server/path/to/file.ext") === "file://server/path/to" - * getDirectoryPath("file://server/path/to") === "file://server/path" - * getDirectoryPath("file://server/") === "file://server/" - * getDirectoryPath("file://server") === "file://server" - * getDirectoryPath("file:///path/to/file.ext") === "file:///path/to" - * getDirectoryPath("file:///path/to") === "file:///path" - * getDirectoryPath("file:///") === "file:///" - * getDirectoryPath("file://") === "file://" - * ``` - */ - export function getDirectoryPath(path: string): string; - export function getDirectoryPath(path: string): string { - path = normalizeSlashes(path); - - // If the path provided is itself the root, then return it. - const rootLength = getRootLength(path); - if (rootLength === path.length) return path; - - // return the leading portion of the path up to the last (non-terminal) directory separator - // but not including any trailing directory separator. - path = removeTrailingDirectorySeparator(path); - return path.slice(0, Math.max(rootLength, path.lastIndexOf(directorySeparator))); - } - - /** - * Returns the path except for its containing directory name. - * Semantics align with NodeJS's `path.basename` except that we support URL's as well. - * - * ```ts - * // POSIX - * getBaseFileName("/path/to/file.ext") === "file.ext" - * getBaseFileName("/path/to/") === "to" - * getBaseFileName("/") === "" - * // DOS - * getBaseFileName("c:/path/to/file.ext") === "file.ext" - * getBaseFileName("c:/path/to/") === "to" - * getBaseFileName("c:/") === "" - * getBaseFileName("c:") === "" - * // URL - * getBaseFileName("http://typescriptlang.org/path/to/file.ext") === "file.ext" - * getBaseFileName("http://typescriptlang.org/path/to/") === "to" - * getBaseFileName("http://typescriptlang.org/") === "" - * getBaseFileName("http://typescriptlang.org") === "" - * getBaseFileName("file://server/path/to/file.ext") === "file.ext" - * getBaseFileName("file://server/path/to/") === "to" - * getBaseFileName("file://server/") === "" - * getBaseFileName("file://server") === "" - * getBaseFileName("file:///path/to/file.ext") === "file.ext" - * getBaseFileName("file:///path/to/") === "to" - * getBaseFileName("file:///") === "" - * getBaseFileName("file://") === "" - * ``` - */ - export function getBaseFileName(path: string): string; - /** - * Gets the portion of a path following the last (non-terminal) separator (`/`). - * Semantics align with NodeJS's `path.basename` except that we support URL's as well. - * If the base name has any one of the provided extensions, it is removed. - * - * ```ts - * getBaseFileName("/path/to/file.ext", ".ext", true) === "file" - * getBaseFileName("/path/to/file.js", ".ext", true) === "file.js" - * getBaseFileName("/path/to/file.js", [".ext", ".js"], true) === "file" - * getBaseFileName("/path/to/file.ext", ".EXT", false) === "file.ext" - * ``` - */ - export function getBaseFileName(path: string, extensions: string | readonly string[], ignoreCase: boolean): string; - export function getBaseFileName(path: string, extensions?: string | readonly string[], ignoreCase?: boolean) { - path = normalizeSlashes(path); - - // if the path provided is itself the root, then it has not file name. - const rootLength = getRootLength(path); - if (rootLength === path.length) return ""; - - // return the trailing portion of the path starting after the last (non-terminal) directory - // separator but not including any trailing directory separator. - path = removeTrailingDirectorySeparator(path); - const name = path.slice(Math.max(getRootLength(path), path.lastIndexOf(directorySeparator) + 1)); - const extension = extensions !== undefined && ignoreCase !== undefined ? getAnyExtensionFromPath(name, extensions, ignoreCase) : undefined; - return extension ? name.slice(0, name.length - extension.length) : name; - } - - function tryGetExtensionFromPath(path: string, extension: string, stringEqualityComparer: (a: string, b: string) => boolean) { - if (!startsWith(extension, ".")) extension = "." + extension; - if (path.length >= extension.length && path.charCodeAt(path.length - extension.length) === CharacterCodes.dot) { - const pathExtension = path.slice(path.length - extension.length); - if (stringEqualityComparer(pathExtension, extension)) { - return pathExtension; } + return ~(authorityEnd + 1); // URL: "file://server/", "http://server/" } - } - - function getAnyExtensionFromPathWorker(path: string, extensions: string | readonly string[], stringEqualityComparer: (a: string, b: string) => boolean) { - if (typeof extensions === "string") { - return tryGetExtensionFromPath(path, extensions, stringEqualityComparer) || ""; - } - for (const extension of extensions) { - const result = tryGetExtensionFromPath(path, extension, stringEqualityComparer); - if (result) return result; - } + return ~path.length; // URL: "file://server", "http://server" + } + // relative + return 0; +} +/** + * Returns length of the root part of a path or URL (i.e. length of "/", "x:/", "//server/share/, file:///user/files"). + * + * For example: + * ```ts + * getRootLength("a") === 0 // "" + * getRootLength("/") === 1 // "/" + * getRootLength("c:") === 2 // "c:" + * getRootLength("c:d") === 0 // "" + * getRootLength("c:/") === 3 // "c:/" + * getRootLength("c:\\") === 3 // "c:\\" + * getRootLength("//server") === 7 // "//server" + * getRootLength("//server/share") === 8 // "//server/" + * getRootLength("\\\\server") === 7 // "\\\\server" + * getRootLength("\\\\server\\share") === 8 // "\\\\server\\" + * getRootLength("file:///path") === 8 // "file:///" + * getRootLength("file:///c:") === 10 // "file:///c:" + * getRootLength("file:///c:d") === 8 // "file:///" + * getRootLength("file:///c:/path") === 11 // "file:///c:/" + * getRootLength("file://server") === 13 // "file://server" + * getRootLength("file://server/path") === 14 // "file://server/" + * getRootLength("http://server") === 13 // "http://server" + * getRootLength("http://server/path") === 14 // "http://server/" + * ``` + */ +/* @internal */ +export function getRootLength(path: string) { + const rootLength = getEncodedRootLength(path); + return rootLength < 0 ? ~rootLength : rootLength; +} +/** + * Returns the path except for its basename. Semantics align with NodeJS's `path.dirname` + * except that we support URLs as well. + * + * ```ts + * // POSIX + * getDirectoryPath("/path/to/file.ext") === "/path/to" + * getDirectoryPath("/path/to/") === "/path" + * getDirectoryPath("/") === "/" + * // DOS + * getDirectoryPath("c:/path/to/file.ext") === "c:/path/to" + * getDirectoryPath("c:/path/to/") === "c:/path" + * getDirectoryPath("c:/") === "c:/" + * getDirectoryPath("c:") === "c:" + * // URL + * getDirectoryPath("http://typescriptlang.org/path/to/file.ext") === "http://typescriptlang.org/path/to" + * getDirectoryPath("http://typescriptlang.org/path/to") === "http://typescriptlang.org/path" + * getDirectoryPath("http://typescriptlang.org/") === "http://typescriptlang.org/" + * getDirectoryPath("http://typescriptlang.org") === "http://typescriptlang.org" + * ``` + */ +/* @internal */ +export function getDirectoryPath(path: Path): Path; +/** + * Returns the path except for its basename. Semantics align with NodeJS's `path.dirname` + * except that we support URLs as well. + * + * ```ts + * // POSIX + * getDirectoryPath("/path/to/file.ext") === "/path/to" + * getDirectoryPath("/path/to/") === "/path" + * getDirectoryPath("/") === "/" + * // DOS + * getDirectoryPath("c:/path/to/file.ext") === "c:/path/to" + * getDirectoryPath("c:/path/to/") === "c:/path" + * getDirectoryPath("c:/") === "c:/" + * getDirectoryPath("c:") === "c:" + * // URL + * getDirectoryPath("http://typescriptlang.org/path/to/file.ext") === "http://typescriptlang.org/path/to" + * getDirectoryPath("http://typescriptlang.org/path/to") === "http://typescriptlang.org/path" + * getDirectoryPath("http://typescriptlang.org/") === "http://typescriptlang.org/" + * getDirectoryPath("http://typescriptlang.org") === "http://typescriptlang.org" + * getDirectoryPath("file://server/path/to/file.ext") === "file://server/path/to" + * getDirectoryPath("file://server/path/to") === "file://server/path" + * getDirectoryPath("file://server/") === "file://server/" + * getDirectoryPath("file://server") === "file://server" + * getDirectoryPath("file:///path/to/file.ext") === "file:///path/to" + * getDirectoryPath("file:///path/to") === "file:///path" + * getDirectoryPath("file:///") === "file:///" + * getDirectoryPath("file://") === "file://" + * ``` + */ +/* @internal */ +export function getDirectoryPath(path: string): string; +/* @internal */ +export function getDirectoryPath(path: string): string { + path = normalizeSlashes(path); + // If the path provided is itself the root, then return it. + const rootLength = getRootLength(path); + if (rootLength === path.length) + return path; + // return the leading portion of the path up to the last (non-terminal) directory separator + // but not including any trailing directory separator. + path = removeTrailingDirectorySeparator(path); + return path.slice(0, Math.max(rootLength, path.lastIndexOf(directorySeparator))); +} +/** + * Returns the path except for its containing directory name. + * Semantics align with NodeJS's `path.basename` except that we support URL's as well. + * + * ```ts + * // POSIX + * getBaseFileName("/path/to/file.ext") === "file.ext" + * getBaseFileName("/path/to/") === "to" + * getBaseFileName("/") === "" + * // DOS + * getBaseFileName("c:/path/to/file.ext") === "file.ext" + * getBaseFileName("c:/path/to/") === "to" + * getBaseFileName("c:/") === "" + * getBaseFileName("c:") === "" + * // URL + * getBaseFileName("http://typescriptlang.org/path/to/file.ext") === "file.ext" + * getBaseFileName("http://typescriptlang.org/path/to/") === "to" + * getBaseFileName("http://typescriptlang.org/") === "" + * getBaseFileName("http://typescriptlang.org") === "" + * getBaseFileName("file://server/path/to/file.ext") === "file.ext" + * getBaseFileName("file://server/path/to/") === "to" + * getBaseFileName("file://server/") === "" + * getBaseFileName("file://server") === "" + * getBaseFileName("file:///path/to/file.ext") === "file.ext" + * getBaseFileName("file:///path/to/") === "to" + * getBaseFileName("file:///") === "" + * getBaseFileName("file://") === "" + * ``` + */ +/* @internal */ +export function getBaseFileName(path: string): string; +/** + * Gets the portion of a path following the last (non-terminal) separator (`/`). + * Semantics align with NodeJS's `path.basename` except that we support URL's as well. + * If the base name has any one of the provided extensions, it is removed. + * + * ```ts + * getBaseFileName("/path/to/file.ext", ".ext", true) === "file" + * getBaseFileName("/path/to/file.js", ".ext", true) === "file.js" + * getBaseFileName("/path/to/file.js", [".ext", ".js"], true) === "file" + * getBaseFileName("/path/to/file.ext", ".EXT", false) === "file.ext" + * ``` + */ +/* @internal */ +export function getBaseFileName(path: string, extensions: string | readonly string[], ignoreCase: boolean): string; +/* @internal */ +export function getBaseFileName(path: string, extensions?: string | readonly string[], ignoreCase?: boolean) { + path = normalizeSlashes(path); + // if the path provided is itself the root, then it has not file name. + const rootLength = getRootLength(path); + if (rootLength === path.length) return ""; - } - - /** - * Gets the file extension for a path. - * - * ```ts - * getAnyExtensionFromPath("/path/to/file.ext") === ".ext" - * getAnyExtensionFromPath("/path/to/file.ext/") === ".ext" - * getAnyExtensionFromPath("/path/to/file") === "" - * getAnyExtensionFromPath("/path/to.ext/file") === "" - * ``` - */ - export function getAnyExtensionFromPath(path: string): string; - /** - * Gets the file extension for a path, provided it is one of the provided extensions. - * - * ```ts - * getAnyExtensionFromPath("/path/to/file.ext", ".ext", true) === ".ext" - * getAnyExtensionFromPath("/path/to/file.js", ".ext", true) === "" - * getAnyExtensionFromPath("/path/to/file.js", [".ext", ".js"], true) === ".js" - * getAnyExtensionFromPath("/path/to/file.ext", ".EXT", false) === "" - */ - export function getAnyExtensionFromPath(path: string, extensions: string | readonly string[], ignoreCase: boolean): string; - export function getAnyExtensionFromPath(path: string, extensions?: string | readonly string[], ignoreCase?: boolean): string { - // Retrieves any string from the final "." onwards from a base file name. - // Unlike extensionFromPath, which throws an exception on unrecognized extensions. - if (extensions) { - return getAnyExtensionFromPathWorker(removeTrailingDirectorySeparator(path), extensions, ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive); - } - const baseFileName = getBaseFileName(path); - const extensionIndex = baseFileName.lastIndexOf("."); - if (extensionIndex >= 0) { - return baseFileName.substring(extensionIndex); + // return the trailing portion of the path starting after the last (non-terminal) directory + // separator but not including any trailing directory separator. + path = removeTrailingDirectorySeparator(path); + const name = path.slice(Math.max(getRootLength(path), path.lastIndexOf(directorySeparator) + 1)); + const extension = extensions !== undefined && ignoreCase !== undefined ? getAnyExtensionFromPath(name, extensions, ignoreCase) : undefined; + return extension ? name.slice(0, name.length - extension.length) : name; +} +/* @internal */ +function tryGetExtensionFromPath(path: string, extension: string, stringEqualityComparer: (a: string, b: string) => boolean) { + if (!startsWith(extension, ".")) + extension = "." + extension; + if (path.length >= extension.length && path.charCodeAt(path.length - extension.length) === CharacterCodes.dot) { + const pathExtension = path.slice(path.length - extension.length); + if (stringEqualityComparer(pathExtension, extension)) { + return pathExtension; } - return ""; - } - - function pathComponents(path: string, rootLength: number) { - const root = path.substring(0, rootLength); - const rest = path.substring(rootLength).split(directorySeparator); - if (rest.length && !lastOrUndefined(rest)) rest.pop(); - return [root, ...rest]; - } - - /** - * Parse a path into an array containing a root component (at index 0) and zero or more path - * components (at indices > 0). The result is not normalized. - * If the path is relative, the root component is `""`. - * If the path is absolute, the root component includes the first path separator (`/`). - * - * ```ts - * // POSIX - * getPathComponents("/path/to/file.ext") === ["/", "path", "to", "file.ext"] - * getPathComponents("/path/to/") === ["/", "path", "to"] - * getPathComponents("/") === ["/"] - * // DOS - * getPathComponents("c:/path/to/file.ext") === ["c:/", "path", "to", "file.ext"] - * getPathComponents("c:/path/to/") === ["c:/", "path", "to"] - * getPathComponents("c:/") === ["c:/"] - * getPathComponents("c:") === ["c:"] - * // URL - * getPathComponents("http://typescriptlang.org/path/to/file.ext") === ["http://typescriptlang.org/", "path", "to", "file.ext"] - * getPathComponents("http://typescriptlang.org/path/to/") === ["http://typescriptlang.org/", "path", "to"] - * getPathComponents("http://typescriptlang.org/") === ["http://typescriptlang.org/"] - * getPathComponents("http://typescriptlang.org") === ["http://typescriptlang.org"] - * getPathComponents("file://server/path/to/file.ext") === ["file://server/", "path", "to", "file.ext"] - * getPathComponents("file://server/path/to/") === ["file://server/", "path", "to"] - * getPathComponents("file://server/") === ["file://server/"] - * getPathComponents("file://server") === ["file://server"] - * getPathComponents("file:///path/to/file.ext") === ["file:///", "path", "to", "file.ext"] - * getPathComponents("file:///path/to/") === ["file:///", "path", "to"] - * getPathComponents("file:///") === ["file:///"] - * getPathComponents("file://") === ["file://"] - */ - export function getPathComponents(path: string, currentDirectory = "") { - path = combinePaths(currentDirectory, path); - return pathComponents(path, getRootLength(path)); } - - //// Path Formatting - - /** - * Formats a parsed path consisting of a root component (at index 0) and zero or more path - * segments (at indices > 0). - * - * ```ts - * getPathFromPathComponents(["/", "path", "to", "file.ext"]) === "/path/to/file.ext" - * ``` - */ - export function getPathFromPathComponents(pathComponents: readonly string[]) { - if (pathComponents.length === 0) return ""; - - const root = pathComponents[0] && ensureTrailingDirectorySeparator(pathComponents[0]); - return root + pathComponents.slice(1).join(directorySeparator); +} +/* @internal */ +function getAnyExtensionFromPathWorker(path: string, extensions: string | readonly string[], stringEqualityComparer: (a: string, b: string) => boolean) { + if (typeof extensions === "string") { + return tryGetExtensionFromPath(path, extensions, stringEqualityComparer) || ""; } - - //// Path Normalization - - /** - * Normalize path separators, converting `\` into `/`. - */ - export function normalizeSlashes(path: string): string { - return path.replace(backslashRegExp, directorySeparator); + for (const extension of extensions) { + const result = tryGetExtensionFromPath(path, extension, stringEqualityComparer); + if (result) + return result; } - - /** - * Reduce an array of path components to a more simplified path by navigating any - * `"."` or `".."` entries in the path. - */ - export function reducePathComponents(components: readonly string[]) { - if (!some(components)) return []; - const reduced = [components[0]]; - for (let i = 1; i < components.length; i++) { - const component = components[i]; - if (!component) continue; - if (component === ".") continue; - if (component === "..") { - if (reduced.length > 1) { - if (reduced[reduced.length - 1] !== "..") { - reduced.pop(); - continue; - } + return ""; +} +/** + * Gets the file extension for a path. + * + * ```ts + * getAnyExtensionFromPath("/path/to/file.ext") === ".ext" + * getAnyExtensionFromPath("/path/to/file.ext/") === ".ext" + * getAnyExtensionFromPath("/path/to/file") === "" + * getAnyExtensionFromPath("/path/to.ext/file") === "" + * ``` + */ +/* @internal */ +export function getAnyExtensionFromPath(path: string): string; +/** + * Gets the file extension for a path, provided it is one of the provided extensions. + * + * ```ts + * getAnyExtensionFromPath("/path/to/file.ext", ".ext", true) === ".ext" + * getAnyExtensionFromPath("/path/to/file.js", ".ext", true) === "" + * getAnyExtensionFromPath("/path/to/file.js", [".ext", ".js"], true) === ".js" + * getAnyExtensionFromPath("/path/to/file.ext", ".EXT", false) === "" + */ +/* @internal */ +export function getAnyExtensionFromPath(path: string, extensions: string | readonly string[], ignoreCase: boolean): string; +/* @internal */ +export function getAnyExtensionFromPath(path: string, extensions?: string | readonly string[], ignoreCase?: boolean): string { + // Retrieves any string from the final "." onwards from a base file name. + // Unlike extensionFromPath, which throws an exception on unrecognized extensions. + if (extensions) { + return getAnyExtensionFromPathWorker(removeTrailingDirectorySeparator(path), extensions, ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive); + } + const baseFileName = getBaseFileName(path); + const extensionIndex = baseFileName.lastIndexOf("."); + if (extensionIndex >= 0) { + return baseFileName.substring(extensionIndex); + } + return ""; +} +/* @internal */ +function pathComponents(path: string, rootLength: number) { + const root = path.substring(0, rootLength); + const rest = path.substring(rootLength).split(directorySeparator); + if (rest.length && !lastOrUndefined(rest)) + rest.pop(); + return [root, ...rest]; +} +/** + * Parse a path into an array containing a root component (at index 0) and zero or more path + * components (at indices > 0). The result is not normalized. + * If the path is relative, the root component is `""`. + * If the path is absolute, the root component includes the first path separator (`/`). + * + * ```ts + * // POSIX + * getPathComponents("/path/to/file.ext") === ["/", "path", "to", "file.ext"] + * getPathComponents("/path/to/") === ["/", "path", "to"] + * getPathComponents("/") === ["/"] + * // DOS + * getPathComponents("c:/path/to/file.ext") === ["c:/", "path", "to", "file.ext"] + * getPathComponents("c:/path/to/") === ["c:/", "path", "to"] + * getPathComponents("c:/") === ["c:/"] + * getPathComponents("c:") === ["c:"] + * // URL + * getPathComponents("http://typescriptlang.org/path/to/file.ext") === ["http://typescriptlang.org/", "path", "to", "file.ext"] + * getPathComponents("http://typescriptlang.org/path/to/") === ["http://typescriptlang.org/", "path", "to"] + * getPathComponents("http://typescriptlang.org/") === ["http://typescriptlang.org/"] + * getPathComponents("http://typescriptlang.org") === ["http://typescriptlang.org"] + * getPathComponents("file://server/path/to/file.ext") === ["file://server/", "path", "to", "file.ext"] + * getPathComponents("file://server/path/to/") === ["file://server/", "path", "to"] + * getPathComponents("file://server/") === ["file://server/"] + * getPathComponents("file://server") === ["file://server"] + * getPathComponents("file:///path/to/file.ext") === ["file:///", "path", "to", "file.ext"] + * getPathComponents("file:///path/to/") === ["file:///", "path", "to"] + * getPathComponents("file:///") === ["file:///"] + * getPathComponents("file://") === ["file://"] + */ +/* @internal */ +export function getPathComponents(path: string, currentDirectory = "") { + path = combinePaths(currentDirectory, path); + return pathComponents(path, getRootLength(path)); +} +//// Path Formatting +/** + * Formats a parsed path consisting of a root component (at index 0) and zero or more path + * segments (at indices > 0). + * + * ```ts + * getPathFromPathComponents(["/", "path", "to", "file.ext"]) === "/path/to/file.ext" + * ``` + */ +/* @internal */ +export function getPathFromPathComponents(pathComponents: readonly string[]) { + if (pathComponents.length === 0) + return ""; + const root = pathComponents[0] && ensureTrailingDirectorySeparator(pathComponents[0]); + return root + pathComponents.slice(1).join(directorySeparator); +} +//// Path Normalization +/** + * Normalize path separators, converting `\` into `/`. + */ +/* @internal */ +export function normalizeSlashes(path: string): string { + return path.replace(backslashRegExp, directorySeparator); +} +/** + * Reduce an array of path components to a more simplified path by navigating any + * `"."` or `".."` entries in the path. + */ +/* @internal */ +export function reducePathComponents(components: readonly string[]) { + if (!some(components)) + return []; + const reduced = [components[0]]; + for (let i = 1; i < components.length; i++) { + const component = components[i]; + if (!component) + continue; + if (component === ".") + continue; + if (component === "..") { + if (reduced.length > 1) { + if (reduced[reduced.length - 1] !== "..") { + reduced.pop(); + continue; } - else if (reduced[0]) continue; - } - reduced.push(component); - } - return reduced; - } - - /** - * Combines paths. If a path is absolute, it replaces any previous path. Relative paths are not simplified. - * - * ```ts - * // Non-rooted - * combinePaths("path", "to", "file.ext") === "path/to/file.ext" - * combinePaths("path", "dir", "..", "to", "file.ext") === "path/dir/../to/file.ext" - * // POSIX - * combinePaths("/path", "to", "file.ext") === "/path/to/file.ext" - * combinePaths("/path", "/to", "file.ext") === "/to/file.ext" - * // DOS - * combinePaths("c:/path", "to", "file.ext") === "c:/path/to/file.ext" - * combinePaths("c:/path", "c:/to", "file.ext") === "c:/to/file.ext" - * // URL - * combinePaths("file:///path", "to", "file.ext") === "file:///path/to/file.ext" - * combinePaths("file:///path", "file:///to", "file.ext") === "file:///to/file.ext" - * ``` - */ - export function combinePaths(path: string, ...paths: (string | undefined)[]): string { - if (path) path = normalizeSlashes(path); - for (let relativePath of paths) { - if (!relativePath) continue; - relativePath = normalizeSlashes(relativePath); - if (!path || getRootLength(relativePath) !== 0) { - path = relativePath; - } - else { - path = ensureTrailingDirectorySeparator(path) + relativePath; } + else if (reduced[0]) + continue; } - return path; - } - - /** - * Combines and resolves paths. If a path is absolute, it replaces any previous path. Any - * `.` and `..` path components are resolved. Trailing directory separators are preserved. - * - * ```ts - * resolvePath("/path", "to", "file.ext") === "path/to/file.ext" - * resolvePath("/path", "to", "file.ext/") === "path/to/file.ext/" - * resolvePath("/path", "dir", "..", "to", "file.ext") === "path/to/file.ext" - * ``` - */ - export function resolvePath(path: string, ...paths: (string | undefined)[]): string { - return normalizePath(some(paths) ? combinePaths(path, ...paths) : normalizeSlashes(path)); - } - - /** - * Parse a path into an array containing a root component (at index 0) and zero or more path - * components (at indices > 0). The result is normalized. - * If the path is relative, the root component is `""`. - * If the path is absolute, the root component includes the first path separator (`/`). - * - * ```ts - * getNormalizedPathComponents("to/dir/../file.ext", "/path/") === ["/", "path", "to", "file.ext"] - */ - export function getNormalizedPathComponents(path: string, currentDirectory: string | undefined) { - return reducePathComponents(getPathComponents(path, currentDirectory)); - } - - export function getNormalizedAbsolutePath(fileName: string, currentDirectory: string | undefined) { - return getPathFromPathComponents(getNormalizedPathComponents(fileName, currentDirectory)); - } - - export function normalizePath(path: string): string { - path = normalizeSlashes(path); - const normalized = getPathFromPathComponents(reducePathComponents(getPathComponents(path))); - return normalized && hasTrailingDirectorySeparator(path) ? ensureTrailingDirectorySeparator(normalized) : normalized; - } - - function getPathWithoutRoot(pathComponents: readonly string[]) { - if (pathComponents.length === 0) return ""; - return pathComponents.slice(1).join(directorySeparator); - } - - export function getNormalizedAbsolutePathWithoutRoot(fileName: string, currentDirectory: string | undefined) { - return getPathWithoutRoot(getNormalizedPathComponents(fileName, currentDirectory)); - } - - export function toPath(fileName: string, basePath: string | undefined, getCanonicalFileName: (path: string) => string): Path { - const nonCanonicalizedPath = isRootedDiskPath(fileName) - ? normalizePath(fileName) - : getNormalizedAbsolutePath(fileName, basePath); - return getCanonicalFileName(nonCanonicalizedPath); - } - - export function normalizePathAndParts(path: string): { path: string, parts: string[] } { + reduced.push(component); + } + return reduced; +} +/** + * Combines paths. If a path is absolute, it replaces any previous path. Relative paths are not simplified. + * + * ```ts + * // Non-rooted + * combinePaths("path", "to", "file.ext") === "path/to/file.ext" + * combinePaths("path", "dir", "..", "to", "file.ext") === "path/dir/../to/file.ext" + * // POSIX + * combinePaths("/path", "to", "file.ext") === "/path/to/file.ext" + * combinePaths("/path", "/to", "file.ext") === "/to/file.ext" + * // DOS + * combinePaths("c:/path", "to", "file.ext") === "c:/path/to/file.ext" + * combinePaths("c:/path", "c:/to", "file.ext") === "c:/to/file.ext" + * // URL + * combinePaths("file:///path", "to", "file.ext") === "file:///path/to/file.ext" + * combinePaths("file:///path", "file:///to", "file.ext") === "file:///to/file.ext" + * ``` + */ +/* @internal */ +export function combinePaths(path: string, ...paths: (string | undefined)[]): string { + if (path) path = normalizeSlashes(path); - const [root, ...parts] = reducePathComponents(getPathComponents(path)); - if (parts.length) { - const joinedParts = root + parts.join(directorySeparator); - return { path: hasTrailingDirectorySeparator(path) ? ensureTrailingDirectorySeparator(joinedParts) : joinedParts, parts }; + for (let relativePath of paths) { + if (!relativePath) + continue; + relativePath = normalizeSlashes(relativePath); + if (!path || getRootLength(relativePath) !== 0) { + path = relativePath; } else { - return { path: root, parts }; - } - } - - //// Path Mutation - - /** - * Removes a trailing directory separator from a path, if it does not already have one. - * - * ```ts - * removeTrailingDirectorySeparator("/path/to/file.ext") === "/path/to/file.ext" - * removeTrailingDirectorySeparator("/path/to/file.ext/") === "/path/to/file.ext" - * ``` - */ - export function removeTrailingDirectorySeparator(path: Path): Path; - export function removeTrailingDirectorySeparator(path: string): string; - export function removeTrailingDirectorySeparator(path: string) { - if (hasTrailingDirectorySeparator(path)) { - return path.substr(0, path.length - 1); - } - - return path; - } - - /** - * Adds a trailing directory separator to a path, if it does not already have one. - * - * ```ts - * ensureTrailingDirectorySeparator("/path/to/file.ext") === "/path/to/file.ext/" - * ensureTrailingDirectorySeparator("/path/to/file.ext/") === "/path/to/file.ext/" - * ``` - */ - export function ensureTrailingDirectorySeparator(path: Path): Path; - export function ensureTrailingDirectorySeparator(path: string): string; - export function ensureTrailingDirectorySeparator(path: string) { - if (!hasTrailingDirectorySeparator(path)) { - return path + directorySeparator; + path = ensureTrailingDirectorySeparator(path) + relativePath; } - - return path; - } - - /** - * Ensures a path is either absolute (prefixed with `/` or `c:`) or dot-relative (prefixed - * with `./` or `../`) so as not to be confused with an unprefixed module name. - * - * ```ts - * ensurePathIsNonModuleName("/path/to/file.ext") === "/path/to/file.ext" - * ensurePathIsNonModuleName("./path/to/file.ext") === "./path/to/file.ext" - * ensurePathIsNonModuleName("../path/to/file.ext") === "../path/to/file.ext" - * ensurePathIsNonModuleName("path/to/file.ext") === "./path/to/file.ext" - * ``` - */ - export function ensurePathIsNonModuleName(path: string): string { - return !pathIsAbsolute(path) && !pathIsRelative(path) ? "./" + path : path; - } - - /** - * Changes the extension of a path to the provided extension. - * - * ```ts - * changeAnyExtension("/path/to/file.ext", ".js") === "/path/to/file.js" - * ``` - */ - export function changeAnyExtension(path: string, ext: string): string; - /** - * Changes the extension of a path to the provided extension if it has one of the provided extensions. - * - * ```ts - * changeAnyExtension("/path/to/file.ext", ".js", ".ext") === "/path/to/file.js" - * changeAnyExtension("/path/to/file.ext", ".js", ".ts") === "/path/to/file.ext" - * changeAnyExtension("/path/to/file.ext", ".js", [".ext", ".ts"]) === "/path/to/file.js" - * ``` - */ - export function changeAnyExtension(path: string, ext: string, extensions: string | readonly string[], ignoreCase: boolean): string; - export function changeAnyExtension(path: string, ext: string, extensions?: string | readonly string[], ignoreCase?: boolean) { - const pathext = extensions !== undefined && ignoreCase !== undefined ? getAnyExtensionFromPath(path, extensions, ignoreCase) : getAnyExtensionFromPath(path); - return pathext ? path.slice(0, path.length - pathext.length) + (startsWith(ext, ".") ? ext : "." + ext) : path; } - - //// Path Comparisons - - // check path for these segments: '', '.'. '..' - const relativePathSegmentRegExp = /(^|\/)\.{0,2}($|\/)/; - - function comparePathsWorker(a: string, b: string, componentComparer: (a: string, b: string) => Comparison) { - if (a === b) return Comparison.EqualTo; - if (a === undefined) return Comparison.LessThan; - if (b === undefined) return Comparison.GreaterThan; - - // NOTE: Performance optimization - shortcut if the root segments differ as there would be no - // need to perform path reduction. - const aRoot = a.substring(0, getRootLength(a)); - const bRoot = b.substring(0, getRootLength(b)); - const result = compareStringsCaseInsensitive(aRoot, bRoot); + return path; +} +/** + * Combines and resolves paths. If a path is absolute, it replaces any previous path. Any + * `.` and `..` path components are resolved. Trailing directory separators are preserved. + * + * ```ts + * resolvePath("/path", "to", "file.ext") === "path/to/file.ext" + * resolvePath("/path", "to", "file.ext/") === "path/to/file.ext/" + * resolvePath("/path", "dir", "..", "to", "file.ext") === "path/to/file.ext" + * ``` + */ +/* @internal */ +export function resolvePath(path: string, ...paths: (string | undefined)[]): string { + return normalizePath(some(paths) ? combinePaths(path, ...paths) : normalizeSlashes(path)); +} +/** + * Parse a path into an array containing a root component (at index 0) and zero or more path + * components (at indices > 0). The result is normalized. + * If the path is relative, the root component is `""`. + * If the path is absolute, the root component includes the first path separator (`/`). + * + * ```ts + * getNormalizedPathComponents("to/dir/../file.ext", "/path/") === ["/", "path", "to", "file.ext"] + */ +/* @internal */ +export function getNormalizedPathComponents(path: string, currentDirectory: string | undefined) { + return reducePathComponents(getPathComponents(path, currentDirectory)); +} +/* @internal */ +export function getNormalizedAbsolutePath(fileName: string, currentDirectory: string | undefined) { + return getPathFromPathComponents(getNormalizedPathComponents(fileName, currentDirectory)); +} +/* @internal */ +export function normalizePath(path: string): string { + path = normalizeSlashes(path); + const normalized = getPathFromPathComponents(reducePathComponents(getPathComponents(path))); + return normalized && hasTrailingDirectorySeparator(path) ? ensureTrailingDirectorySeparator(normalized) : normalized; +} +/* @internal */ +function getPathWithoutRoot(pathComponents: readonly string[]) { + if (pathComponents.length === 0) + return ""; + return pathComponents.slice(1).join(directorySeparator); +} +/* @internal */ +export function getNormalizedAbsolutePathWithoutRoot(fileName: string, currentDirectory: string | undefined) { + return getPathWithoutRoot(getNormalizedPathComponents(fileName, currentDirectory)); +} +/* @internal */ +export function toPath(fileName: string, basePath: string | undefined, getCanonicalFileName: (path: string) => string): Path { + const nonCanonicalizedPath = isRootedDiskPath(fileName) + ? normalizePath(fileName) + : getNormalizedAbsolutePath(fileName, basePath); + return getCanonicalFileName(nonCanonicalizedPath); +} +/* @internal */ +export function normalizePathAndParts(path: string): { + path: string; + parts: string[]; +} { + path = normalizeSlashes(path); + const [root, ...parts] = reducePathComponents(getPathComponents(path)); + if (parts.length) { + const joinedParts = root + parts.join(directorySeparator); + return { path: hasTrailingDirectorySeparator(path) ? ensureTrailingDirectorySeparator(joinedParts) : joinedParts, parts }; + } + else { + return { path: root, parts }; + } +} +//// Path Mutation +/** + * Removes a trailing directory separator from a path, if it does not already have one. + * + * ```ts + * removeTrailingDirectorySeparator("/path/to/file.ext") === "/path/to/file.ext" + * removeTrailingDirectorySeparator("/path/to/file.ext/") === "/path/to/file.ext" + * ``` + */ +/* @internal */ +export function removeTrailingDirectorySeparator(path: Path): Path; +/* @internal */ +export function removeTrailingDirectorySeparator(path: string): string; +/* @internal */ +export function removeTrailingDirectorySeparator(path: string) { + if (hasTrailingDirectorySeparator(path)) { + return path.substr(0, path.length - 1); + } + return path; +} +/** + * Adds a trailing directory separator to a path, if it does not already have one. + * + * ```ts + * ensureTrailingDirectorySeparator("/path/to/file.ext") === "/path/to/file.ext/" + * ensureTrailingDirectorySeparator("/path/to/file.ext/") === "/path/to/file.ext/" + * ``` + */ +/* @internal */ +export function ensureTrailingDirectorySeparator(path: Path): Path; +/* @internal */ +export function ensureTrailingDirectorySeparator(path: string): string; +/* @internal */ +export function ensureTrailingDirectorySeparator(path: string) { + if (!hasTrailingDirectorySeparator(path)) { + return path + directorySeparator; + } + return path; +} +/** + * Ensures a path is either absolute (prefixed with `/` or `c:`) or dot-relative (prefixed + * with `./` or `../`) so as not to be confused with an unprefixed module name. + * + * ```ts + * ensurePathIsNonModuleName("/path/to/file.ext") === "/path/to/file.ext" + * ensurePathIsNonModuleName("./path/to/file.ext") === "./path/to/file.ext" + * ensurePathIsNonModuleName("../path/to/file.ext") === "../path/to/file.ext" + * ensurePathIsNonModuleName("path/to/file.ext") === "./path/to/file.ext" + * ``` + */ +/* @internal */ +export function ensurePathIsNonModuleName(path: string): string { + return !pathIsAbsolute(path) && !pathIsRelative(path) ? "./" + path : path; +} +/** + * Changes the extension of a path to the provided extension. + * + * ```ts + * changeAnyExtension("/path/to/file.ext", ".js") === "/path/to/file.js" + * ``` + */ +/* @internal */ +export function changeAnyExtension(path: string, ext: string): string; +/** + * Changes the extension of a path to the provided extension if it has one of the provided extensions. + * + * ```ts + * changeAnyExtension("/path/to/file.ext", ".js", ".ext") === "/path/to/file.js" + * changeAnyExtension("/path/to/file.ext", ".js", ".ts") === "/path/to/file.ext" + * changeAnyExtension("/path/to/file.ext", ".js", [".ext", ".ts"]) === "/path/to/file.js" + * ``` + */ +/* @internal */ +export function changeAnyExtension(path: string, ext: string, extensions: string | readonly string[], ignoreCase: boolean): string; +/* @internal */ +export function changeAnyExtension(path: string, ext: string, extensions?: string | readonly string[], ignoreCase?: boolean) { + const pathext = extensions !== undefined && ignoreCase !== undefined ? getAnyExtensionFromPath(path, extensions, ignoreCase) : getAnyExtensionFromPath(path); + return pathext ? path.slice(0, path.length - pathext.length) + (startsWith(ext, ".") ? ext : "." + ext) : path; +} +//// Path Comparisons +// check path for these segments: '', '.'. '..' +/* @internal */ +const relativePathSegmentRegExp = /(^|\/)\.{0,2}($|\/)/; +/* @internal */ +function comparePathsWorker(a: string, b: string, componentComparer: (a: string, b: string) => Comparison) { + if (a === b) + return Comparison.EqualTo; + if (a === undefined) + return Comparison.LessThan; + if (b === undefined) + return Comparison.GreaterThan; + // NOTE: Performance optimization - shortcut if the root segments differ as there would be no + // need to perform path reduction. + const aRoot = a.substring(0, getRootLength(a)); + const bRoot = b.substring(0, getRootLength(b)); + const result = compareStringsCaseInsensitive(aRoot, bRoot); + if (result !== Comparison.EqualTo) { + return result; + } + // NOTE: Performance optimization - shortcut if there are no relative path segments in + // the non-root portion of the path + const aRest = a.substring(aRoot.length); + const bRest = b.substring(bRoot.length); + if (!relativePathSegmentRegExp.test(aRest) && !relativePathSegmentRegExp.test(bRest)) { + return componentComparer(aRest, bRest); + } + // The path contains a relative path segment. Normalize the paths and perform a slower component + // by component comparison. + const aComponents = reducePathComponents(getPathComponents(a)); + const bComponents = reducePathComponents(getPathComponents(b)); + const sharedLength = Math.min(aComponents.length, bComponents.length); + for (let i = 1; i < sharedLength; i++) { + const result = componentComparer(aComponents[i], bComponents[i]); if (result !== Comparison.EqualTo) { return result; } - - // NOTE: Performance optimization - shortcut if there are no relative path segments in - // the non-root portion of the path - const aRest = a.substring(aRoot.length); - const bRest = b.substring(bRoot.length); - if (!relativePathSegmentRegExp.test(aRest) && !relativePathSegmentRegExp.test(bRest)) { - return componentComparer(aRest, bRest); - } - - // The path contains a relative path segment. Normalize the paths and perform a slower component - // by component comparison. - const aComponents = reducePathComponents(getPathComponents(a)); - const bComponents = reducePathComponents(getPathComponents(b)); - const sharedLength = Math.min(aComponents.length, bComponents.length); - for (let i = 1; i < sharedLength; i++) { - const result = componentComparer(aComponents[i], bComponents[i]); - if (result !== Comparison.EqualTo) { - return result; - } - } - return compareValues(aComponents.length, bComponents.length); - } - - /** - * Performs a case-sensitive comparison of two paths. Path roots are always compared case-insensitively. - */ - export function comparePathsCaseSensitive(a: string, b: string) { - return comparePathsWorker(a, b, compareStringsCaseSensitive); } - - /** - * Performs a case-insensitive comparison of two paths. - */ - export function comparePathsCaseInsensitive(a: string, b: string) { - return comparePathsWorker(a, b, compareStringsCaseInsensitive); + return compareValues(aComponents.length, bComponents.length); +} +/** + * Performs a case-sensitive comparison of two paths. Path roots are always compared case-insensitively. + */ +/* @internal */ +export function comparePathsCaseSensitive(a: string, b: string) { + return comparePathsWorker(a, b, compareStringsCaseSensitive); +} +/** + * Performs a case-insensitive comparison of two paths. + */ +/* @internal */ +export function comparePathsCaseInsensitive(a: string, b: string) { + return comparePathsWorker(a, b, compareStringsCaseInsensitive); +} +/** + * Compare two paths using the provided case sensitivity. + */ +/* @internal */ +export function comparePaths(a: string, b: string, ignoreCase?: boolean): Comparison; +/* @internal */ +export function comparePaths(a: string, b: string, currentDirectory: string, ignoreCase?: boolean): Comparison; +/* @internal */ +export function comparePaths(a: string, b: string, currentDirectory?: string | boolean, ignoreCase?: boolean) { + if (typeof currentDirectory === "string") { + a = combinePaths(currentDirectory, a); + b = combinePaths(currentDirectory, b); + } + else if (typeof currentDirectory === "boolean") { + ignoreCase = currentDirectory; + } + return comparePathsWorker(a, b, getStringComparer(ignoreCase)); +} +/** + * Determines whether a `parent` path contains a `child` path using the provide case sensitivity. + */ +/* @internal */ +export function containsPath(parent: string, child: string, ignoreCase?: boolean): boolean; +/* @internal */ +export function containsPath(parent: string, child: string, currentDirectory: string, ignoreCase?: boolean): boolean; +/* @internal */ +export function containsPath(parent: string, child: string, currentDirectory?: string | boolean, ignoreCase?: boolean) { + if (typeof currentDirectory === "string") { + parent = combinePaths(currentDirectory, parent); + child = combinePaths(currentDirectory, child); } - - /** - * Compare two paths using the provided case sensitivity. - */ - export function comparePaths(a: string, b: string, ignoreCase?: boolean): Comparison; - export function comparePaths(a: string, b: string, currentDirectory: string, ignoreCase?: boolean): Comparison; - export function comparePaths(a: string, b: string, currentDirectory?: string | boolean, ignoreCase?: boolean) { - if (typeof currentDirectory === "string") { - a = combinePaths(currentDirectory, a); - b = combinePaths(currentDirectory, b); - } - else if (typeof currentDirectory === "boolean") { - ignoreCase = currentDirectory; - } - return comparePathsWorker(a, b, getStringComparer(ignoreCase)); + else if (typeof currentDirectory === "boolean") { + ignoreCase = currentDirectory; } - - /** - * Determines whether a `parent` path contains a `child` path using the provide case sensitivity. - */ - export function containsPath(parent: string, child: string, ignoreCase?: boolean): boolean; - export function containsPath(parent: string, child: string, currentDirectory: string, ignoreCase?: boolean): boolean; - export function containsPath(parent: string, child: string, currentDirectory?: string | boolean, ignoreCase?: boolean) { - if (typeof currentDirectory === "string") { - parent = combinePaths(currentDirectory, parent); - child = combinePaths(currentDirectory, child); - } - else if (typeof currentDirectory === "boolean") { - ignoreCase = currentDirectory; - } - if (parent === undefined || child === undefined) return false; - if (parent === child) return true; - const parentComponents = reducePathComponents(getPathComponents(parent)); - const childComponents = reducePathComponents(getPathComponents(child)); - if (childComponents.length < parentComponents.length) { - return false; - } - - const componentEqualityComparer = ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive; - for (let i = 0; i < parentComponents.length; i++) { - const equalityComparer = i === 0 ? equateStringsCaseInsensitive : componentEqualityComparer; - if (!equalityComparer(parentComponents[i], childComponents[i])) { - return false; - } - } - + if (parent === undefined || child === undefined) + return false; + if (parent === child) return true; + const parentComponents = reducePathComponents(getPathComponents(parent)); + const childComponents = reducePathComponents(getPathComponents(child)); + if (childComponents.length < parentComponents.length) { + return false; } - - /** - * Determines whether `fileName` starts with the specified `directoryName` using the provided path canonicalization callback. - * Comparison is case-sensitive between the canonical paths. - * - * @deprecated Use `containsPath` if possible. - */ - export function startsWithDirectory(fileName: string, directoryName: string, getCanonicalFileName: GetCanonicalFileName): boolean { - const canonicalFileName = getCanonicalFileName(fileName); - const canonicalDirectoryName = getCanonicalFileName(directoryName); - return startsWith(canonicalFileName, canonicalDirectoryName + "/") || startsWith(canonicalFileName, canonicalDirectoryName + "\\"); - } - - //// Relative Paths - - export function getPathComponentsRelativeTo(from: string, to: string, stringEqualityComparer: (a: string, b: string) => boolean, getCanonicalFileName: GetCanonicalFileName) { - const fromComponents = reducePathComponents(getPathComponents(from)); - const toComponents = reducePathComponents(getPathComponents(to)); - - let start: number; - for (start = 0; start < fromComponents.length && start < toComponents.length; start++) { - const fromComponent = getCanonicalFileName(fromComponents[start]); - const toComponent = getCanonicalFileName(toComponents[start]); - const comparer = start === 0 ? equateStringsCaseInsensitive : stringEqualityComparer; - if (!comparer(fromComponent, toComponent)) break; - } - - if (start === 0) { - return toComponents; - } - - const components = toComponents.slice(start); - const relative: string[] = []; - for (; start < fromComponents.length; start++) { - relative.push(".."); + const componentEqualityComparer = ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive; + for (let i = 0; i < parentComponents.length; i++) { + const equalityComparer = i === 0 ? equateStringsCaseInsensitive : componentEqualityComparer; + if (!equalityComparer(parentComponents[i], childComponents[i])) { + return false; } - return ["", ...relative, ...components]; - } - - /** - * Gets a relative path that can be used to traverse between `from` and `to`. - */ - export function getRelativePathFromDirectory(from: string, to: string, ignoreCase: boolean): string; - /** - * Gets a relative path that can be used to traverse between `from` and `to`. - */ - export function getRelativePathFromDirectory(fromDirectory: string, to: string, getCanonicalFileName: GetCanonicalFileName): string; // eslint-disable-line @typescript-eslint/unified-signatures - export function getRelativePathFromDirectory(fromDirectory: string, to: string, getCanonicalFileNameOrIgnoreCase: GetCanonicalFileName | boolean) { - Debug.assert((getRootLength(fromDirectory) > 0) === (getRootLength(to) > 0), "Paths must either both be absolute or both be relative"); - const getCanonicalFileName = typeof getCanonicalFileNameOrIgnoreCase === "function" ? getCanonicalFileNameOrIgnoreCase : identity; - const ignoreCase = typeof getCanonicalFileNameOrIgnoreCase === "boolean" ? getCanonicalFileNameOrIgnoreCase : false; - const pathComponents = getPathComponentsRelativeTo(fromDirectory, to, ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive, getCanonicalFileName); - return getPathFromPathComponents(pathComponents); - } - - export function convertToRelativePath(absoluteOrRelativePath: string, basePath: string, getCanonicalFileName: (path: string) => string): string { - return !isRootedDiskPath(absoluteOrRelativePath) - ? absoluteOrRelativePath - : getRelativePathToDirectoryOrUrl(basePath, absoluteOrRelativePath, basePath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); - } - - export function getRelativePathFromFile(from: string, to: string, getCanonicalFileName: GetCanonicalFileName) { - return ensurePathIsNonModuleName(getRelativePathFromDirectory(getDirectoryPath(from), to, getCanonicalFileName)); } - - export function getRelativePathToDirectoryOrUrl(directoryPathOrUrl: string, relativeOrAbsolutePath: string, currentDirectory: string, getCanonicalFileName: GetCanonicalFileName, isAbsolutePathAnUrl: boolean) { - const pathComponents = getPathComponentsRelativeTo( - resolvePath(currentDirectory, directoryPathOrUrl), - resolvePath(currentDirectory, relativeOrAbsolutePath), - equateStringsCaseSensitive, - getCanonicalFileName - ); - - const firstComponent = pathComponents[0]; - if (isAbsolutePathAnUrl && isRootedDiskPath(firstComponent)) { - const prefix = firstComponent.charAt(0) === directorySeparator ? "file://" : "file:///"; - pathComponents[0] = prefix + firstComponent; + return true; +} +/** + * Determines whether `fileName` starts with the specified `directoryName` using the provided path canonicalization callback. + * Comparison is case-sensitive between the canonical paths. + * + * @deprecated Use `containsPath` if possible. + */ +/* @internal */ +export function startsWithDirectory(fileName: string, directoryName: string, getCanonicalFileName: GetCanonicalFileName): boolean { + const canonicalFileName = getCanonicalFileName(fileName); + const canonicalDirectoryName = getCanonicalFileName(directoryName); + return startsWith(canonicalFileName, canonicalDirectoryName + "/") || startsWith(canonicalFileName, canonicalDirectoryName + "\\"); +} +//// Relative Paths +/* @internal */ +export function getPathComponentsRelativeTo(from: string, to: string, stringEqualityComparer: (a: string, b: string) => boolean, getCanonicalFileName: GetCanonicalFileName) { + const fromComponents = reducePathComponents(getPathComponents(from)); + const toComponents = reducePathComponents(getPathComponents(to)); + let start: number; + for (start = 0; start < fromComponents.length && start < toComponents.length; start++) { + const fromComponent = getCanonicalFileName(fromComponents[start]); + const toComponent = getCanonicalFileName(toComponents[start]); + const comparer = start === 0 ? equateStringsCaseInsensitive : stringEqualityComparer; + if (!comparer(fromComponent, toComponent)) + break; + } + if (start === 0) { + return toComponents; + } + const components = toComponents.slice(start); + const relative: string[] = []; + for (; start < fromComponents.length; start++) { + relative.push(".."); + } + return ["", ...relative, ...components]; +} +/** + * Gets a relative path that can be used to traverse between `from` and `to`. + */ +/* @internal */ +export function getRelativePathFromDirectory(from: string, to: string, ignoreCase: boolean): string; +/** + * Gets a relative path that can be used to traverse between `from` and `to`. + */ +/* @internal */ +export function getRelativePathFromDirectory(fromDirectory: string, to: string, getCanonicalFileName: GetCanonicalFileName): string; // eslint-disable-line @typescript-eslint/unified-signatures +/* @internal */ +export function getRelativePathFromDirectory(fromDirectory: string, to: string, getCanonicalFileNameOrIgnoreCase: GetCanonicalFileName | boolean) { + Debug.assert((getRootLength(fromDirectory) > 0) === (getRootLength(to) > 0), "Paths must either both be absolute or both be relative"); + const getCanonicalFileName = typeof getCanonicalFileNameOrIgnoreCase === "function" ? getCanonicalFileNameOrIgnoreCase : identity; + const ignoreCase = typeof getCanonicalFileNameOrIgnoreCase === "boolean" ? getCanonicalFileNameOrIgnoreCase : false; + const pathComponents = getPathComponentsRelativeTo(fromDirectory, to, ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive, getCanonicalFileName); + return getPathFromPathComponents(pathComponents); +} +/* @internal */ +export function convertToRelativePath(absoluteOrRelativePath: string, basePath: string, getCanonicalFileName: (path: string) => string): string { + return !isRootedDiskPath(absoluteOrRelativePath) + ? absoluteOrRelativePath + : getRelativePathToDirectoryOrUrl(basePath, absoluteOrRelativePath, basePath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); +} +/* @internal */ +export function getRelativePathFromFile(from: string, to: string, getCanonicalFileName: GetCanonicalFileName) { + return ensurePathIsNonModuleName(getRelativePathFromDirectory(getDirectoryPath(from), to, getCanonicalFileName)); +} +/* @internal */ +export function getRelativePathToDirectoryOrUrl(directoryPathOrUrl: string, relativeOrAbsolutePath: string, currentDirectory: string, getCanonicalFileName: GetCanonicalFileName, isAbsolutePathAnUrl: boolean) { + const pathComponents = getPathComponentsRelativeTo(resolvePath(currentDirectory, directoryPathOrUrl), resolvePath(currentDirectory, relativeOrAbsolutePath), equateStringsCaseSensitive, getCanonicalFileName); + const firstComponent = pathComponents[0]; + if (isAbsolutePathAnUrl && isRootedDiskPath(firstComponent)) { + const prefix = firstComponent.charAt(0) === directorySeparator ? "file://" : "file:///"; + pathComponents[0] = prefix + firstComponent; + } + return getPathFromPathComponents(pathComponents); +} +//// Path Traversal +/** + * Calls `callback` on `directory` and every ancestor directory it has, returning the first defined result. + */ +/* @internal */ +export function forEachAncestorDirectory(directory: Path, callback: (directory: Path) => T | undefined): T | undefined; +/* @internal */ +export function forEachAncestorDirectory(directory: string, callback: (directory: string) => T | undefined): T | undefined; +/* @internal */ +export function forEachAncestorDirectory(directory: Path, callback: (directory: Path) => T | undefined): T | undefined { + while (true) { + const result = callback(directory); + if (result !== undefined) { + return result; } - - return getPathFromPathComponents(pathComponents); - } - - //// Path Traversal - - /** - * Calls `callback` on `directory` and every ancestor directory it has, returning the first defined result. - */ - export function forEachAncestorDirectory(directory: Path, callback: (directory: Path) => T | undefined): T | undefined; - export function forEachAncestorDirectory(directory: string, callback: (directory: string) => T | undefined): T | undefined; - export function forEachAncestorDirectory(directory: Path, callback: (directory: Path) => T | undefined): T | undefined { - while (true) { - const result = callback(directory); - if (result !== undefined) { - return result; - } - - const parentPath = getDirectoryPath(directory); - if (parentPath === directory) { - return undefined; - } - - directory = parentPath; + const parentPath = getDirectoryPath(directory); + if (parentPath === directory) { + return undefined; } + directory = parentPath; } -} \ No newline at end of file +} diff --git a/src/compiler/perfLogger.ts b/src/compiler/perfLogger.ts index 8ed8feac90c4c..1315c4cdce5fc 100644 --- a/src/compiler/perfLogger.ts +++ b/src/compiler/perfLogger.ts @@ -1,41 +1,42 @@ +import { noop } from "./ts"; /* @internal */ -namespace ts { - type PerfLogger = typeof import("@microsoft/typescript-etw"); - const nullLogger: PerfLogger = { - logEvent: noop, - logErrEvent: noop, - logPerfEvent: noop, - logInfoEvent: noop, - logStartCommand: noop, - logStopCommand: noop, - logStartUpdateProgram: noop, - logStopUpdateProgram: noop, - logStartUpdateGraph: noop, - logStopUpdateGraph: noop, - logStartResolveModule: noop, - logStopResolveModule: noop, - logStartParseSourceFile: noop, - logStopParseSourceFile: noop, - logStartReadFile: noop, - logStopReadFile: noop, - logStartBindFile: noop, - logStopBindFile: noop, - logStartScheduledOperation: noop, - logStopScheduledOperation: noop, - }; - - // Load optional module to enable Event Tracing for Windows - // See https://github.com/microsoft/typescript-etw for more information - let etwModule; - try { - // require() will throw an exception if the module is not installed - // It may also return undefined if not installed properly - etwModule = require("@microsoft/typescript-etw"); - } - catch (e) { - etwModule = undefined; - } - - /** Performance logger that will generate ETW events if possible - check for `logEvent` member, as `etwModule` will be `{}` when browserified */ - export const perfLogger: PerfLogger = etwModule && etwModule.logEvent ? etwModule : nullLogger; +type PerfLogger = typeof import("@microsoft/typescript-etw"); +/* @internal */ +const nullLogger: PerfLogger = { + logEvent: noop, + logErrEvent: noop, + logPerfEvent: noop, + logInfoEvent: noop, + logStartCommand: noop, + logStopCommand: noop, + logStartUpdateProgram: noop, + logStopUpdateProgram: noop, + logStartUpdateGraph: noop, + logStopUpdateGraph: noop, + logStartResolveModule: noop, + logStopResolveModule: noop, + logStartParseSourceFile: noop, + logStopParseSourceFile: noop, + logStartReadFile: noop, + logStopReadFile: noop, + logStartBindFile: noop, + logStopBindFile: noop, + logStartScheduledOperation: noop, + logStopScheduledOperation: noop, +}; +// Load optional module to enable Event Tracing for Windows +// See https://github.com/microsoft/typescript-etw for more information +/* @internal */ +let etwModule; +/* @internal */ +try { + // require() will throw an exception if the module is not installed + // It may also return undefined if not installed properly + etwModule = require("@microsoft/typescript-etw"); } +catch (e) { + etwModule = undefined; +} +/** Performance logger that will generate ETW events if possible - check for `logEvent` member, as `etwModule` will be `{}` when browserified */ +/* @internal */ +export const perfLogger: PerfLogger = etwModule && etwModule.logEvent ? etwModule : nullLogger; diff --git a/src/compiler/performance.ts b/src/compiler/performance.ts index 4a7f5c99278b2..daf52919d4be5 100644 --- a/src/compiler/performance.ts +++ b/src/compiler/performance.ts @@ -1,122 +1,127 @@ +import { Debug, noop, timestamp, createMap } from "./ts"; +import * as ts from "./ts"; /*@internal*/ /** Performance measurements for the compiler. */ -namespace ts.performance { - declare const onProfilerEvent: { (markName: string): void; profiler: boolean; }; - - // NOTE: cannot use ts.noop as core.ts loads after this - const profilerEvent: (markName: string) => void = typeof onProfilerEvent === "function" && onProfilerEvent.profiler === true ? onProfilerEvent : () => { /*empty*/ }; - - let enabled = false; - let profilerStart = 0; - let counts: Map; - let marks: Map; - let measures: Map; - - export interface Timer { - enter(): void; - exit(): void; - } - - export function createTimerIf(condition: boolean, measureName: string, startMarkName: string, endMarkName: string) { - return condition ? createTimer(measureName, startMarkName, endMarkName) : nullTimer; - } - - export function createTimer(measureName: string, startMarkName: string, endMarkName: string): Timer { - let enterCount = 0; - return { - enter, - exit - }; - - function enter() { - if (++enterCount === 1) { - mark(startMarkName); - } - } - - function exit() { - if (--enterCount === 0) { - mark(endMarkName); - measure(measureName, startMarkName, endMarkName); - } - else if (enterCount < 0) { - Debug.fail("enter/exit count does not match."); - } +declare const onProfilerEvent: { + (markName: string): void; + profiler: boolean; +}; +// NOTE: cannot use ts.noop as core.ts loads after this +/* @internal */ +const profilerEvent: (markName: string) => void = typeof onProfilerEvent === "function" && onProfilerEvent.profiler === true ? onProfilerEvent : () => { }; +/* @internal */ +let enabled = false; +/* @internal */ +let profilerStart = 0; +/* @internal */ +let counts: ts.Map; +/* @internal */ +let marks: ts.Map; +/* @internal */ +let measures: ts.Map; +/* @internal */ +export interface Timer { + enter(): void; + exit(): void; +} +/* @internal */ +export function createTimerIf(condition: boolean, measureName: string, startMarkName: string, endMarkName: string) { + return condition ? createTimer(measureName, startMarkName, endMarkName) : nullTimer; +} +/* @internal */ +export function createTimer(measureName: string, startMarkName: string, endMarkName: string): Timer { + let enterCount = 0; + return { + enter, + exit + }; + function enter() { + if (++enterCount === 1) { + mark(startMarkName); } } - - export const nullTimer: Timer = { enter: noop, exit: noop }; - - /** - * Marks a performance event. - * - * @param markName The name of the mark. - */ - export function mark(markName: string) { - if (enabled) { - marks.set(markName, timestamp()); - counts.set(markName, (counts.get(markName) || 0) + 1); - profilerEvent(markName); + function exit() { + if (--enterCount === 0) { + mark(endMarkName); + measure(measureName, startMarkName, endMarkName); } - } - - /** - * Adds a performance measurement with the specified name. - * - * @param measureName The name of the performance measurement. - * @param startMarkName The name of the starting mark. If not supplied, the point at which the - * profiler was enabled is used. - * @param endMarkName The name of the ending mark. If not supplied, the current timestamp is - * used. - */ - export function measure(measureName: string, startMarkName?: string, endMarkName?: string) { - if (enabled) { - const end = endMarkName && marks.get(endMarkName) || timestamp(); - const start = startMarkName && marks.get(startMarkName) || profilerStart; - measures.set(measureName, (measures.get(measureName) || 0) + (end - start)); + else if (enterCount < 0) { + Debug.fail("enter/exit count does not match."); } } - - /** - * Gets the number of times a marker was encountered. - * - * @param markName The name of the mark. - */ - export function getCount(markName: string) { - return counts && counts.get(markName) || 0; - } - - /** - * Gets the total duration of all measurements with the supplied name. - * - * @param measureName The name of the measure whose durations should be accumulated. - */ - export function getDuration(measureName: string) { - return measures && measures.get(measureName) || 0; - } - - /** - * Iterate over each measure, performing some action - * - * @param cb The action to perform for each measure - */ - export function forEachMeasure(cb: (measureName: string, duration: number) => void) { - measures.forEach((measure, key) => { - cb(key, measure); - }); - } - - /** Enables (and resets) performance measurements for the compiler. */ - export function enable() { - counts = createMap(); - marks = createMap(); - measures = createMap(); - enabled = true; - profilerStart = timestamp(); +} +/* @internal */ +export const nullTimer: Timer = { enter: noop, exit: noop }; +/** + * Marks a performance event. + * + * @param markName The name of the mark. + */ +/* @internal */ +export function mark(markName: string) { + if (enabled) { + marks.set(markName, timestamp()); + counts.set(markName, (counts.get(markName) || 0) + 1); + profilerEvent(markName); } - - /** Disables performance measurements for the compiler. */ - export function disable() { - enabled = false; +} +/** + * Adds a performance measurement with the specified name. + * + * @param measureName The name of the performance measurement. + * @param startMarkName The name of the starting mark. If not supplied, the point at which the + * profiler was enabled is used. + * @param endMarkName The name of the ending mark. If not supplied, the current timestamp is + * used. + */ +/* @internal */ +export function measure(measureName: string, startMarkName?: string, endMarkName?: string) { + if (enabled) { + const end = endMarkName && marks.get(endMarkName) || timestamp(); + const start = startMarkName && marks.get(startMarkName) || profilerStart; + measures.set(measureName, (measures.get(measureName) || 0) + (end - start)); } } +/** + * Gets the number of times a marker was encountered. + * + * @param markName The name of the mark. + */ +/* @internal */ +export function getCount(markName: string) { + return counts && counts.get(markName) || 0; +} +/** + * Gets the total duration of all measurements with the supplied name. + * + * @param measureName The name of the measure whose durations should be accumulated. + */ +/* @internal */ +export function getDuration(measureName: string) { + return measures && measures.get(measureName) || 0; +} +/** + * Iterate over each measure, performing some action + * + * @param cb The action to perform for each measure + */ +/* @internal */ +export function forEachMeasure(cb: (measureName: string, duration: number) => void) { + measures.forEach((measure, key) => { + cb(key, measure); + }); +} +/** Enables (and resets) performance measurements for the compiler. */ +/* @internal */ +export function enable() { + counts = createMap(); + marks = createMap(); + measures = createMap(); + enabled = true; + profilerStart = timestamp(); +} +/** Disables performance measurements for the compiler. */ +/* @internal */ +export function disable() { + enabled = false; +} diff --git a/src/compiler/performanceTimestamp.ts b/src/compiler/performanceTimestamp.ts index 044e7a65c0992..912c2cc4d900d 100644 --- a/src/compiler/performanceTimestamp.ts +++ b/src/compiler/performanceTimestamp.ts @@ -1,6 +1,7 @@ /*@internal*/ -namespace ts { - declare const performance: { now?(): number } | undefined; - /** Gets a timestamp with (at least) ms resolution */ - export const timestamp = typeof performance !== "undefined" && performance.now ? () => performance.now!() : Date.now ? Date.now : () => +(new Date()); -} \ No newline at end of file +declare const performance: { + now?(): number; +} | undefined; +/** Gets a timestamp with (at least) ms resolution */ +/* @internal */ +export const timestamp = typeof performance !== "undefined" && performance.now ? () => performance.now!() : Date.now ? Date.now : () => +(new Date()); diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 6e9e167119d47..3c649522afdfe 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -1,1667 +1,1513 @@ -namespace ts { - const ignoreDiagnosticCommentRegEx = /(^\s*$)|(^\s*\/\/\/?\s*(@ts-ignore)?)/; - - export function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean, configName = "tsconfig.json"): string | undefined { - return forEachAncestorDirectory(searchPath, ancestor => { - const fileName = combinePaths(ancestor, configName); - return fileExists(fileName) ? fileName : undefined; - }); - } - - export function resolveTripleslashReference(moduleName: string, containingFile: string): string { - const basePath = getDirectoryPath(containingFile); - const referencedFileName = isRootedDiskPath(moduleName) ? moduleName : combinePaths(basePath, moduleName); - return normalizePath(referencedFileName); - } - - /* @internal */ - export function computeCommonSourceDirectoryOfFilenames(fileNames: string[], currentDirectory: string, getCanonicalFileName: GetCanonicalFileName): string { - let commonPathComponents: string[] | undefined; - const failed = forEach(fileNames, sourceFile => { - // Each file contributes into common source file path - const sourcePathComponents = getNormalizedPathComponents(sourceFile, currentDirectory); - sourcePathComponents.pop(); // The base file name is not part of the common directory path - - if (!commonPathComponents) { - // first file - commonPathComponents = sourcePathComponents; - return; - } - - const n = Math.min(commonPathComponents.length, sourcePathComponents.length); - for (let i = 0; i < n; i++) { - if (getCanonicalFileName(commonPathComponents[i]) !== getCanonicalFileName(sourcePathComponents[i])) { - if (i === 0) { - // Failed to find any common path component - return true; - } - - // New common path found that is 0 -> i-1 - commonPathComponents.length = i; - break; +import { forEachAncestorDirectory, combinePaths, getDirectoryPath, isRootedDiskPath, normalizePath, GetCanonicalFileName, forEach, getNormalizedPathComponents, getPathFromPathComponents, CompilerOptions, CompilerHost, sys, createMap, createGetCanonicalFileName, ScriptTarget, SourceFile, createSourceFile, writeFileEnsuringDirectories, isWatchSet, missingFileModifiedTime, getNewLineCharacter, getDefaultLibFileName, memoize, maybeBind, WriteFileCallback, Path, fileExtensionIs, Extension, isBuildInfoFile, isDeclarationFileName, Program, CancellationToken, Diagnostic, BuilderProgram, addRange, getEmitDeclarations, sortAndDeduplicateDiagnostics, emptyArray, diagnosticCategoryName, getLineAndCharacterOfPosition, convertToRelativePath, DiagnosticCategory, Debug, padLeft, getPositionOfLineAndCharacter, DiagnosticMessageChain, isString, ResolvedProjectReference, TextRange, RefFileKind, HasInvalidatedResolution, ProjectReference, arrayIsEqualTo, compareDataObjects, projectReferenceIsEqualTo, contains, ParsedCommandLine, sourceFileAffectingCompilerOptions, isJsonEqual, getCompilerOptionValue, CreateProgramOptions, isArray, TypeChecker, UnderscoreEscapedMap, MultiMap, DiagnosticWithLocation, ResolvedTypeReferenceDirective, createDiagnosticCollection, getSupportedExtensions, getSuppoertedExtensionsWithJsonIfResolveJsonModule, ObjectLiteralExpression, ModuleResolutionCache, ResolvedModuleFull, returnFalse, clone, extensionFromPath, createModuleResolutionCache, resolveModuleName, resolveTypeReferenceDirective, createMultiMap, SourceOfProjectReferenceRedirect, StructureIsReused, getEmitModuleKind, ModuleKind, changeExtension, getOutputDeclarationFileName, getAutomaticTypeDirectiveNames, arrayFrom, mapDefinedIterator, stableSort, compareValues, containsPath, getBaseFileName, removeSuffix, removePrefix, libs, ResolvedModuleWithFailedLookupLocations, resolveModuleNameFromCache, filter, sourceFileMayBeEmitted, getNormalizedAbsolutePath, normalizeSlashes, directorySeparator, createUnderscoreEscapedMap, copyEntries, isTraceEnabled, trace, Diagnostics, getResolvedModule, changesAffectModuleResolution, NodeFlags, hasChangesInResolutions, moduleResolutionIsEqualTo, zipToMap, map, toFileNameLowerCase, typeDirectiveIsEqualTo, EmitHost, EmitResult, emitFiles, notImplementedResolver, noTransformers, equateStringsCaseSensitive, equateStringsCaseInsensitive, some, createTypeChecker, CustomTransformers, getTransformers, flatMap, skipTypeChecking, append, isSourceFileJS, concatenate, OperationCanceledException, isCheckJsEnabledForFile, ScriptKind, getLineStarts, computeLineAndCharacterOfPosition, forEachChildRecursively, Node, SyntaxKind, ParameterDeclaration, PropertyDeclaration, MethodDeclaration, FunctionLikeDeclaration, VariableDeclaration, ImportClause, ExportDeclaration, ExportAssignment, HeritageClause, tokenToString, AsExpression, NodeArray, DeclarationWithTypeParameterChildren, Modifier, NodeWithTypeArguments, DiagnosticMessage, createFileDiagnostic, createDiagnosticForNodeInSourceFile, noop, SortedReadonlyArray, FileReference, StringLiteralLike, Identifier, isExternalModule, StringLiteral, createLiteral, externalHelpersModuleNameText, createImportDeclaration, addEmitFlags, EmitFlags, Statement, isAnyImportOrReExport, getExternalModuleName, isStringLiteral, isExternalModuleNameRelative, isModuleDeclaration, isAmbientModule, hasModifier, ModifierFlags, getTextOfIdentifierOrLiteral, ModuleBlock, ModuleDeclaration, isRequireCall, isImportCall, isStringLiteralLike, isLiteralImportTypeNode, hasJSDocNodes, forEachChild, libMap, UnparsedSource, hasExtension, hasJSFileExtension, PackageId, find, stringContains, nodeModulesPathPart, getNormalizedAbsolutePathWithoutRoot, packageIdToString, setResolvedTypeReferenceDirective, getSpellingSuggestion, identity, createCompilerDiagnostic, setResolvedModule, resolutionExtensionIsTSOrJson, isInJSFile, skipTrivia, mapDefined, arrayToSet, JsonSourceFile, parseJsonSourceFileConfigFileContent, getStrictOptionValue, isIncrementalCompilation, hasProperty, hasZeroOrOneAsteriskCharacter, getErrorSpanForNode, getEmitModuleResolutionKind, ModuleResolutionKind, hasJsonModuleEmitEnabled, getRootLength, parseIsolatedEntityName, isIdentifierText, forEachEmittedFile, chainDiagnosticMessages, createCompilerDiagnosticFromMessageChain, elementAt, getTsBuildInfoEmitOutputFilePath, isObjectLiteralExpression, getPropertyAssignment, isArrayLiteralExpression, PropertyAssignment, firstDefined, getTsConfigPropArray, getTsConfigObjectLiteralExpression, removeFileExtension, fileExtensionIsOneOf, supportedJSExtensions, comparePaths, Comparison, discoverProbableSymlinks, ProgramToEmitFilesAndReportErrors, DiagnosticReporter, DirectoryStructureHost, ParseConfigFileHost, returnUndefined, InputFiles, getOutputPathsForBundle, createInputFiles, ResolvedConfigFileName, resolveConfigFileProjectName } from "./ts"; +import { mark, measure } from "./ts.performance"; +import * as ts from "./ts"; +const ignoreDiagnosticCommentRegEx = /(^\s*$)|(^\s*\/\/\/?\s*(@ts-ignore)?)/; +export function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean, configName = "tsconfig.json"): string | undefined { + return forEachAncestorDirectory(searchPath, ancestor => { + const fileName = combinePaths(ancestor, configName); + return fileExists(fileName) ? fileName : undefined; + }); +} +export function resolveTripleslashReference(moduleName: string, containingFile: string): string { + const basePath = getDirectoryPath(containingFile); + const referencedFileName = isRootedDiskPath(moduleName) ? moduleName : combinePaths(basePath, moduleName); + return normalizePath(referencedFileName); +} +/* @internal */ +export function computeCommonSourceDirectoryOfFilenames(fileNames: string[], currentDirectory: string, getCanonicalFileName: GetCanonicalFileName): string { + let commonPathComponents: string[] | undefined; + const failed = forEach(fileNames, sourceFile => { + // Each file contributes into common source file path + const sourcePathComponents = getNormalizedPathComponents(sourceFile, currentDirectory); + sourcePathComponents.pop(); // The base file name is not part of the common directory path + if (!commonPathComponents) { + // first file + commonPathComponents = sourcePathComponents; + return; + } + const n = Math.min(commonPathComponents.length, sourcePathComponents.length); + for (let i = 0; i < n; i++) { + if (getCanonicalFileName(commonPathComponents[i]) !== getCanonicalFileName(sourcePathComponents[i])) { + if (i === 0) { + // Failed to find any common path component + return true; } + // New common path found that is 0 -> i-1 + commonPathComponents.length = i; + break; } - - // If the sourcePathComponents was shorter than the commonPathComponents, truncate to the sourcePathComponents - if (sourcePathComponents.length < commonPathComponents.length) { - commonPathComponents.length = sourcePathComponents.length; - } - }); - - // A common path can not be found when paths span multiple drives on windows, for example - if (failed) { - return ""; - } - - if (!commonPathComponents) { // Can happen when all input files are .d.ts files - return currentDirectory; - } - - return getPathFromPathComponents(commonPathComponents); - } - - interface OutputFingerprint { - hash: string; - byteOrderMark: boolean; - mtime: Date; - } - - export function createCompilerHost(options: CompilerOptions, setParentNodes?: boolean): CompilerHost { - return createCompilerHostWorker(options, setParentNodes); - } - - /*@internal*/ - // TODO(shkamat): update this after reworking ts build API - export function createCompilerHostWorker(options: CompilerOptions, setParentNodes?: boolean, system = sys): CompilerHost { - const existingDirectories = createMap(); - const getCanonicalFileName = createGetCanonicalFileName(system.useCaseSensitiveFileNames); - function getSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void): SourceFile | undefined { - let text: string | undefined; - try { - performance.mark("beforeIORead"); - text = compilerHost.readFile(fileName); - performance.mark("afterIORead"); - performance.measure("I/O Read", "beforeIORead", "afterIORead"); - } - catch (e) { - if (onError) { - onError(e.message); - } - text = ""; - } - return text !== undefined ? createSourceFile(fileName, text, languageVersion, setParentNodes) : undefined; - } - - function directoryExists(directoryPath: string): boolean { - if (existingDirectories.has(directoryPath)) { - return true; - } - if ((compilerHost.directoryExists || system.directoryExists)(directoryPath)) { - existingDirectories.set(directoryPath, true); - return true; + } + // If the sourcePathComponents was shorter than the commonPathComponents, truncate to the sourcePathComponents + if (sourcePathComponents.length < commonPathComponents.length) { + commonPathComponents.length = sourcePathComponents.length; + } + }); + // A common path can not be found when paths span multiple drives on windows, for example + if (failed) { + return ""; + } + if (!commonPathComponents) { // Can happen when all input files are .d.ts files + return currentDirectory; + } + return getPathFromPathComponents(commonPathComponents); +} +interface OutputFingerprint { + hash: string; + byteOrderMark: boolean; + mtime: Date; +} +export function createCompilerHost(options: CompilerOptions, setParentNodes?: boolean): CompilerHost { + return createCompilerHostWorker(options, setParentNodes); +} +/*@internal*/ +// TODO(shkamat): update this after reworking ts build API +export function createCompilerHostWorker(options: CompilerOptions, setParentNodes?: boolean, system = sys): CompilerHost { + const existingDirectories = createMap(); + const getCanonicalFileName = createGetCanonicalFileName(system.useCaseSensitiveFileNames); + function getSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void): SourceFile | undefined { + let text: string | undefined; + try { + mark("beforeIORead"); + text = compilerHost.readFile(fileName); + mark("afterIORead"); + measure("I/O Read", "beforeIORead", "afterIORead"); + } + catch (e) { + if (onError) { + onError(e.message); + } + text = ""; + } + return text !== undefined ? createSourceFile(fileName, text, languageVersion, setParentNodes) : undefined; + } + function directoryExists(directoryPath: string): boolean { + if (existingDirectories.has(directoryPath)) { + return true; + } + if ((compilerHost.directoryExists || system.directoryExists)(directoryPath)) { + existingDirectories.set(directoryPath, true); + return true; + } + return false; + } + function writeFile(fileName: string, data: string, writeByteOrderMark: boolean, onError?: (message: string) => void) { + try { + mark("beforeIOWrite"); + // NOTE: If patchWriteFileEnsuringDirectory has been called, + // the system.writeFile will do its own directory creation and + // the ensureDirectoriesExist call will always be redundant. + writeFileEnsuringDirectories(fileName, data, writeByteOrderMark, (path, data, writeByteOrderMark) => writeFileWorker(path, data, writeByteOrderMark), path => (compilerHost.createDirectory || system.createDirectory)(path), path => directoryExists(path)); + mark("afterIOWrite"); + measure("I/O Write", "beforeIOWrite", "afterIOWrite"); + } + catch (e) { + if (onError) { + onError(e.message); } - return false; } - - function writeFile(fileName: string, data: string, writeByteOrderMark: boolean, onError?: (message: string) => void) { - try { - performance.mark("beforeIOWrite"); - - // NOTE: If patchWriteFileEnsuringDirectory has been called, - // the system.writeFile will do its own directory creation and - // the ensureDirectoriesExist call will always be redundant. - writeFileEnsuringDirectories( - fileName, - data, - writeByteOrderMark, - (path, data, writeByteOrderMark) => writeFileWorker(path, data, writeByteOrderMark), - path => (compilerHost.createDirectory || system.createDirectory)(path), - path => directoryExists(path)); - - performance.mark("afterIOWrite"); - performance.measure("I/O Write", "beforeIOWrite", "afterIOWrite"); - } - catch (e) { - if (onError) { - onError(e.message); - } - } - } - - let outputFingerprints: Map; - function writeFileWorker(fileName: string, data: string, writeByteOrderMark: boolean) { - if (!isWatchSet(options) || !system.createHash || !system.getModifiedTime) { - system.writeFile(fileName, data, writeByteOrderMark); + } + let outputFingerprints: ts.Map; + function writeFileWorker(fileName: string, data: string, writeByteOrderMark: boolean) { + if (!isWatchSet(options) || !system.createHash || !system.getModifiedTime) { + system.writeFile(fileName, data, writeByteOrderMark); + return; + } + if (!outputFingerprints) { + outputFingerprints = createMap(); + } + const hash = system.createHash(data); + const mtimeBefore = system.getModifiedTime(fileName); + if (mtimeBefore) { + const fingerprint = outputFingerprints.get(fileName); + // If output has not been changed, and the file has no external modification + if (fingerprint && + fingerprint.byteOrderMark === writeByteOrderMark && + fingerprint.hash === hash && + fingerprint.mtime.getTime() === mtimeBefore.getTime()) { return; } - - if (!outputFingerprints) { - outputFingerprints = createMap(); - } - - const hash = system.createHash(data); - const mtimeBefore = system.getModifiedTime(fileName); - - if (mtimeBefore) { - const fingerprint = outputFingerprints.get(fileName); - // If output has not been changed, and the file has no external modification - if (fingerprint && - fingerprint.byteOrderMark === writeByteOrderMark && - fingerprint.hash === hash && - fingerprint.mtime.getTime() === mtimeBefore.getTime()) { - return; - } - } - - system.writeFile(fileName, data, writeByteOrderMark); - - const mtimeAfter = system.getModifiedTime(fileName) || missingFileModifiedTime; - - outputFingerprints.set(fileName, { - hash, - byteOrderMark: writeByteOrderMark, - mtime: mtimeAfter - }); } - - function getDefaultLibLocation(): string { - return getDirectoryPath(normalizePath(system.getExecutingFilePath())); - } - - const newLine = getNewLineCharacter(options, () => system.newLine); - const realpath = system.realpath && ((path: string) => system.realpath!(path)); - const compilerHost: CompilerHost = { - getSourceFile, - getDefaultLibLocation, - getDefaultLibFileName: options => combinePaths(getDefaultLibLocation(), getDefaultLibFileName(options)), - writeFile, - getCurrentDirectory: memoize(() => system.getCurrentDirectory()), - useCaseSensitiveFileNames: () => system.useCaseSensitiveFileNames, - getCanonicalFileName, - getNewLine: () => newLine, - fileExists: fileName => system.fileExists(fileName), - readFile: fileName => system.readFile(fileName), - trace: (s: string) => system.write(s + newLine), - directoryExists: directoryName => system.directoryExists(directoryName), - getEnvironmentVariable: name => system.getEnvironmentVariable ? system.getEnvironmentVariable(name) : "", - getDirectories: (path: string) => system.getDirectories(path), - realpath, - readDirectory: (path, extensions, include, exclude, depth) => system.readDirectory(path, extensions, include, exclude, depth), - createDirectory: d => system.createDirectory(d), - createHash: maybeBind(system, system.createHash) - }; - return compilerHost; - } - - /*@internal*/ - interface CompilerHostLikeForCache { - fileExists(fileName: string): boolean; - readFile(fileName: string, encoding?: string): string | undefined; - directoryExists?(directory: string): boolean; - createDirectory?(directory: string): void; - writeFile?: WriteFileCallback; - } - - /*@internal*/ - export function changeCompilerHostLikeToUseCache( - host: CompilerHostLikeForCache, - toPath: (fileName: string) => Path, - getSourceFile?: CompilerHost["getSourceFile"] - ) { - const originalReadFile = host.readFile; - const originalFileExists = host.fileExists; - const originalDirectoryExists = host.directoryExists; - const originalCreateDirectory = host.createDirectory; - const originalWriteFile = host.writeFile; - const readFileCache = createMap(); - const fileExistsCache = createMap(); - const directoryExistsCache = createMap(); - const sourceFileCache = createMap(); - - const readFileWithCache = (fileName: string): string | undefined => { - const key = toPath(fileName); - const value = readFileCache.get(key); - if (value !== undefined) return value !== false ? value : undefined; - return setReadFileCache(key, fileName); - }; - const setReadFileCache = (key: Path, fileName: string) => { - const newValue = originalReadFile.call(host, fileName); - readFileCache.set(key, newValue !== undefined ? newValue : false); - return newValue; - }; - host.readFile = fileName => { + system.writeFile(fileName, data, writeByteOrderMark); + const mtimeAfter = system.getModifiedTime(fileName) || missingFileModifiedTime; + outputFingerprints.set(fileName, { + hash, + byteOrderMark: writeByteOrderMark, + mtime: mtimeAfter + }); + } + function getDefaultLibLocation(): string { + return getDirectoryPath(normalizePath(system.getExecutingFilePath())); + } + const newLine = getNewLineCharacter(options, () => system.newLine); + const realpath = system.realpath && ((path: string) => system.realpath!(path)); + const compilerHost: CompilerHost = { + getSourceFile, + getDefaultLibLocation, + getDefaultLibFileName: options => combinePaths(getDefaultLibLocation(), getDefaultLibFileName(options)), + writeFile, + getCurrentDirectory: memoize(() => system.getCurrentDirectory()), + useCaseSensitiveFileNames: () => system.useCaseSensitiveFileNames, + getCanonicalFileName, + getNewLine: () => newLine, + fileExists: fileName => system.fileExists(fileName), + readFile: fileName => system.readFile(fileName), + trace: (s: string) => system.write(s + newLine), + directoryExists: directoryName => system.directoryExists(directoryName), + getEnvironmentVariable: name => system.getEnvironmentVariable ? system.getEnvironmentVariable(name) : "", + getDirectories: (path: string) => system.getDirectories(path), + realpath, + readDirectory: (path, extensions, include, exclude, depth) => system.readDirectory(path, extensions, include, exclude, depth), + createDirectory: d => system.createDirectory(d), + createHash: maybeBind(system, system.createHash) + }; + return compilerHost; +} +/*@internal*/ +interface CompilerHostLikeForCache { + fileExists(fileName: string): boolean; + readFile(fileName: string, encoding?: string): string | undefined; + directoryExists?(directory: string): boolean; + createDirectory?(directory: string): void; + writeFile?: WriteFileCallback; +} +/*@internal*/ +export function changeCompilerHostLikeToUseCache(host: CompilerHostLikeForCache, toPath: (fileName: string) => Path, getSourceFile?: CompilerHost["getSourceFile"]) { + const originalReadFile = host.readFile; + const originalFileExists = host.fileExists; + const originalDirectoryExists = host.directoryExists; + const originalCreateDirectory = host.createDirectory; + const originalWriteFile = host.writeFile; + const readFileCache = createMap(); + const fileExistsCache = createMap(); + const directoryExistsCache = createMap(); + const sourceFileCache = createMap(); + const readFileWithCache = (fileName: string): string | undefined => { + const key = toPath(fileName); + const value = readFileCache.get(key); + if (value !== undefined) + return value !== false ? value : undefined; + return setReadFileCache(key, fileName); + }; + const setReadFileCache = (key: Path, fileName: string) => { + const newValue = originalReadFile.call(host, fileName); + readFileCache.set(key, newValue !== undefined ? newValue : false); + return newValue; + }; + host.readFile = fileName => { + const key = toPath(fileName); + const value = readFileCache.get(key); + if (value !== undefined) + return value !== false ? value : undefined; // could be .d.ts from output + // Cache json or buildInfo + if (!fileExtensionIs(fileName, Extension.Json) && !isBuildInfoFile(fileName)) { + return originalReadFile.call(host, fileName); + } + return setReadFileCache(key, fileName); + }; + const getSourceFileWithCache: CompilerHost["getSourceFile"] | undefined = getSourceFile ? (fileName, languageVersion, onError, shouldCreateNewSourceFile) => { + const key = toPath(fileName); + const value = sourceFileCache.get(key); + if (value) + return value; + const sourceFile = getSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile); + if (sourceFile && (isDeclarationFileName(fileName) || fileExtensionIs(fileName, Extension.Json))) { + sourceFileCache.set(key, sourceFile); + } + return sourceFile; + } : undefined; + // fileExists for any kind of extension + host.fileExists = fileName => { + const key = toPath(fileName); + const value = fileExistsCache.get(key); + if (value !== undefined) + return value; + const newValue = originalFileExists.call(host, fileName); + fileExistsCache.set(key, !!newValue); + return newValue; + }; + if (originalWriteFile) { + host.writeFile = (fileName, data, writeByteOrderMark, onError, sourceFiles) => { const key = toPath(fileName); + fileExistsCache.delete(key); const value = readFileCache.get(key); - if (value !== undefined) return value !== false ? value : undefined; // could be .d.ts from output - // Cache json or buildInfo - if (!fileExtensionIs(fileName, Extension.Json) && !isBuildInfoFile(fileName)) { - return originalReadFile.call(host, fileName); + if (value !== undefined && value !== data) { + readFileCache.delete(key); + sourceFileCache.delete(key); } - - return setReadFileCache(key, fileName); - }; - - const getSourceFileWithCache: CompilerHost["getSourceFile"] | undefined = getSourceFile ? (fileName, languageVersion, onError, shouldCreateNewSourceFile) => { - const key = toPath(fileName); - const value = sourceFileCache.get(key); - if (value) return value; - - const sourceFile = getSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile); - if (sourceFile && (isDeclarationFileName(fileName) || fileExtensionIs(fileName, Extension.Json))) { - sourceFileCache.set(key, sourceFile); + else if (getSourceFileWithCache) { + const sourceFile = sourceFileCache.get(key); + if (sourceFile && sourceFile.text !== data) { + sourceFileCache.delete(key); + } } - return sourceFile; - } : undefined; - - // fileExists for any kind of extension - host.fileExists = fileName => { - const key = toPath(fileName); - const value = fileExistsCache.get(key); - if (value !== undefined) return value; - const newValue = originalFileExists.call(host, fileName); - fileExistsCache.set(key, !!newValue); + originalWriteFile.call(host, fileName, data, writeByteOrderMark, onError, sourceFiles); + }; + } + // directoryExists + if (originalDirectoryExists && originalCreateDirectory) { + host.directoryExists = directory => { + const key = toPath(directory); + const value = directoryExistsCache.get(key); + if (value !== undefined) + return value; + const newValue = originalDirectoryExists.call(host, directory); + directoryExistsCache.set(key, !!newValue); return newValue; }; - if (originalWriteFile) { - host.writeFile = (fileName, data, writeByteOrderMark, onError, sourceFiles) => { - const key = toPath(fileName); - fileExistsCache.delete(key); - - const value = readFileCache.get(key); - if (value !== undefined && value !== data) { - readFileCache.delete(key); - sourceFileCache.delete(key); - } - else if (getSourceFileWithCache) { - const sourceFile = sourceFileCache.get(key); - if (sourceFile && sourceFile.text !== data) { - sourceFileCache.delete(key); - } - } - originalWriteFile.call(host, fileName, data, writeByteOrderMark, onError, sourceFiles); - }; + host.createDirectory = directory => { + const key = toPath(directory); + directoryExistsCache.delete(key); + originalCreateDirectory.call(host, directory); + }; + } + return { + originalReadFile, + originalFileExists, + originalDirectoryExists, + originalCreateDirectory, + originalWriteFile, + getSourceFileWithCache, + readFileWithCache + }; +} +export function getPreEmitDiagnostics(program: Program, sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[]; +/*@internal*/ export function getPreEmitDiagnostics(program: BuilderProgram, sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[]; // eslint-disable-line @typescript-eslint/unified-signatures +export function getPreEmitDiagnostics(program: Program | BuilderProgram, sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[] { + let diagnostics: Diagnostic[] | undefined; + diagnostics = addRange(diagnostics, program.getConfigFileParsingDiagnostics()); + diagnostics = addRange(diagnostics, program.getOptionsDiagnostics(cancellationToken)); + diagnostics = addRange(diagnostics, program.getSyntacticDiagnostics(sourceFile, cancellationToken)); + diagnostics = addRange(diagnostics, program.getGlobalDiagnostics(cancellationToken)); + diagnostics = addRange(diagnostics, program.getSemanticDiagnostics(sourceFile, cancellationToken)); + if (getEmitDeclarations(program.getCompilerOptions())) { + diagnostics = addRange(diagnostics, program.getDeclarationDiagnostics(sourceFile, cancellationToken)); + } + return sortAndDeduplicateDiagnostics(diagnostics || emptyArray); +} +export interface FormatDiagnosticsHost { + getCurrentDirectory(): string; + getCanonicalFileName(fileName: string): string; + getNewLine(): string; +} +export function formatDiagnostics(diagnostics: readonly Diagnostic[], host: FormatDiagnosticsHost): string { + let output = ""; + for (const diagnostic of diagnostics) { + output += formatDiagnostic(diagnostic, host); + } + return output; +} +export function formatDiagnostic(diagnostic: Diagnostic, host: FormatDiagnosticsHost): string { + const errorMessage = `${diagnosticCategoryName(diagnostic)} TS${diagnostic.code}: ${flattenDiagnosticMessageText(diagnostic.messageText, host.getNewLine())}${host.getNewLine()}`; + if (diagnostic.file) { + const { line, character } = getLineAndCharacterOfPosition(diagnostic.file, (diagnostic.start!)); // TODO: GH#18217 + const fileName = diagnostic.file.fileName; + const relativeFileName = convertToRelativePath(fileName, host.getCurrentDirectory(), fileName => host.getCanonicalFileName(fileName)); + return `${relativeFileName}(${line + 1},${character + 1}): ` + errorMessage; + } + return errorMessage; +} +/** @internal */ +export enum ForegroundColorEscapeSequences { + Grey = "\u001b[90m", + Red = "\u001b[91m", + Yellow = "\u001b[93m", + Blue = "\u001b[94m", + Cyan = "\u001b[96m" +} +const gutterStyleSequence = "\u001b[7m"; +const gutterSeparator = " "; +const resetEscapeSequence = "\u001b[0m"; +const ellipsis = "..."; +const halfIndent = " "; +const indent = " "; +function getCategoryFormat(category: DiagnosticCategory): ForegroundColorEscapeSequences { + switch (category) { + case DiagnosticCategory.Error: return ForegroundColorEscapeSequences.Red; + case DiagnosticCategory.Warning: return ForegroundColorEscapeSequences.Yellow; + case DiagnosticCategory.Suggestion: return Debug.fail("Should never get an Info diagnostic on the command line."); + case DiagnosticCategory.Message: return ForegroundColorEscapeSequences.Blue; + } +} +/** @internal */ +export function formatColorAndReset(text: string, formatStyle: string) { + return formatStyle + text + resetEscapeSequence; +} +function formatCodeSpan(file: SourceFile, start: number, length: number, indent: string, squiggleColor: ForegroundColorEscapeSequences, host: FormatDiagnosticsHost) { + const { line: firstLine, character: firstLineChar } = getLineAndCharacterOfPosition(file, start); + const { line: lastLine, character: lastLineChar } = getLineAndCharacterOfPosition(file, start + length); + const lastLineInFile = getLineAndCharacterOfPosition(file, file.text.length).line; + const hasMoreThanFiveLines = (lastLine - firstLine) >= 4; + let gutterWidth = (lastLine + 1 + "").length; + if (hasMoreThanFiveLines) { + gutterWidth = Math.max(ellipsis.length, gutterWidth); + } + let context = ""; + for (let i = firstLine; i <= lastLine; i++) { + context += host.getNewLine(); + // If the error spans over 5 lines, we'll only show the first 2 and last 2 lines, + // so we'll skip ahead to the second-to-last line. + if (hasMoreThanFiveLines && firstLine + 1 < i && i < lastLine - 1) { + context += indent + formatColorAndReset(padLeft(ellipsis, gutterWidth), gutterStyleSequence) + gutterSeparator + host.getNewLine(); + i = lastLine - 1; + } + const lineStart = getPositionOfLineAndCharacter(file, i, 0); + const lineEnd = i < lastLineInFile ? getPositionOfLineAndCharacter(file, i + 1, 0) : file.text.length; + let lineContent = file.text.slice(lineStart, lineEnd); + lineContent = lineContent.replace(/\s+$/g, ""); // trim from end + lineContent = lineContent.replace("\t", " "); // convert tabs to single spaces + // Output the gutter and the actual contents of the line. + context += indent + formatColorAndReset(padLeft(i + 1 + "", gutterWidth), gutterStyleSequence) + gutterSeparator; + context += lineContent + host.getNewLine(); + // Output the gutter and the error span for the line using tildes. + context += indent + formatColorAndReset(padLeft("", gutterWidth), gutterStyleSequence) + gutterSeparator; + context += squiggleColor; + if (i === firstLine) { + // If we're on the last line, then limit it to the last character of the last line. + // Otherwise, we'll just squiggle the rest of the line, giving 'slice' no end position. + const lastCharForLine = i === lastLine ? lastLineChar : undefined; + context += lineContent.slice(0, firstLineChar).replace(/\S/g, " "); + context += lineContent.slice(firstLineChar, lastCharForLine).replace(/./g, "~"); + } + else if (i === lastLine) { + context += lineContent.slice(0, lastLineChar).replace(/./g, "~"); } - - // directoryExists - if (originalDirectoryExists && originalCreateDirectory) { - host.directoryExists = directory => { - const key = toPath(directory); - const value = directoryExistsCache.get(key); - if (value !== undefined) return value; - const newValue = originalDirectoryExists.call(host, directory); - directoryExistsCache.set(key, !!newValue); - return newValue; - }; - host.createDirectory = directory => { - const key = toPath(directory); - directoryExistsCache.delete(key); - originalCreateDirectory.call(host, directory); - }; + else { + // Squiggle the entire line. + context += lineContent.replace(/./g, "~"); } - - return { - originalReadFile, - originalFileExists, - originalDirectoryExists, - originalCreateDirectory, - originalWriteFile, - getSourceFileWithCache, - readFileWithCache - }; + context += resetEscapeSequence; } - - export function getPreEmitDiagnostics(program: Program, sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[]; - /*@internal*/ export function getPreEmitDiagnostics(program: BuilderProgram, sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[]; // eslint-disable-line @typescript-eslint/unified-signatures - export function getPreEmitDiagnostics(program: Program | BuilderProgram, sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[] { - let diagnostics: Diagnostic[] | undefined; - diagnostics = addRange(diagnostics, program.getConfigFileParsingDiagnostics()); - diagnostics = addRange(diagnostics, program.getOptionsDiagnostics(cancellationToken)); - diagnostics = addRange(diagnostics, program.getSyntacticDiagnostics(sourceFile, cancellationToken)); - diagnostics = addRange(diagnostics, program.getGlobalDiagnostics(cancellationToken)); - diagnostics = addRange(diagnostics, program.getSemanticDiagnostics(sourceFile, cancellationToken)); - - if (getEmitDeclarations(program.getCompilerOptions())) { - diagnostics = addRange(diagnostics, program.getDeclarationDiagnostics(sourceFile, cancellationToken)); - } - - return sortAndDeduplicateDiagnostics(diagnostics || emptyArray); - } - - export interface FormatDiagnosticsHost { - getCurrentDirectory(): string; - getCanonicalFileName(fileName: string): string; - getNewLine(): string; - } - - export function formatDiagnostics(diagnostics: readonly Diagnostic[], host: FormatDiagnosticsHost): string { - let output = ""; - - for (const diagnostic of diagnostics) { - output += formatDiagnostic(diagnostic, host); - } - return output; - } - - export function formatDiagnostic(diagnostic: Diagnostic, host: FormatDiagnosticsHost): string { - const errorMessage = `${diagnosticCategoryName(diagnostic)} TS${diagnostic.code}: ${flattenDiagnosticMessageText(diagnostic.messageText, host.getNewLine())}${host.getNewLine()}`; - + return context; +} +/* @internal */ +export function formatLocation(file: SourceFile, start: number, host: FormatDiagnosticsHost, color = formatColorAndReset) { + const { line: firstLine, character: firstLineChar } = getLineAndCharacterOfPosition(file, start); // TODO: GH#18217 + const relativeFileName = host ? convertToRelativePath(file.fileName, host.getCurrentDirectory(), fileName => host.getCanonicalFileName(fileName)) : file.fileName; + let output = ""; + output += color(relativeFileName, ForegroundColorEscapeSequences.Cyan); + output += ":"; + output += color(`${firstLine + 1}`, ForegroundColorEscapeSequences.Yellow); + output += ":"; + output += color(`${firstLineChar + 1}`, ForegroundColorEscapeSequences.Yellow); + return output; +} +export function formatDiagnosticsWithColorAndContext(diagnostics: readonly Diagnostic[], host: FormatDiagnosticsHost): string { + let output = ""; + for (const diagnostic of diagnostics) { if (diagnostic.file) { - const { line, character } = getLineAndCharacterOfPosition(diagnostic.file, diagnostic.start!); // TODO: GH#18217 - const fileName = diagnostic.file.fileName; - const relativeFileName = convertToRelativePath(fileName, host.getCurrentDirectory(), fileName => host.getCanonicalFileName(fileName)); - return `${relativeFileName}(${line + 1},${character + 1}): ` + errorMessage; - } - - return errorMessage; - } - - /** @internal */ - export enum ForegroundColorEscapeSequences { - Grey = "\u001b[90m", - Red = "\u001b[91m", - Yellow = "\u001b[93m", - Blue = "\u001b[94m", - Cyan = "\u001b[96m" - } - const gutterStyleSequence = "\u001b[7m"; - const gutterSeparator = " "; - const resetEscapeSequence = "\u001b[0m"; - const ellipsis = "..."; - const halfIndent = " "; - const indent = " "; - function getCategoryFormat(category: DiagnosticCategory): ForegroundColorEscapeSequences { - switch (category) { - case DiagnosticCategory.Error: return ForegroundColorEscapeSequences.Red; - case DiagnosticCategory.Warning: return ForegroundColorEscapeSequences.Yellow; - case DiagnosticCategory.Suggestion: return Debug.fail("Should never get an Info diagnostic on the command line."); - case DiagnosticCategory.Message: return ForegroundColorEscapeSequences.Blue; - } - } - - /** @internal */ - export function formatColorAndReset(text: string, formatStyle: string) { - return formatStyle + text + resetEscapeSequence; - } - - function formatCodeSpan(file: SourceFile, start: number, length: number, indent: string, squiggleColor: ForegroundColorEscapeSequences, host: FormatDiagnosticsHost) { - const { line: firstLine, character: firstLineChar } = getLineAndCharacterOfPosition(file, start); - const { line: lastLine, character: lastLineChar } = getLineAndCharacterOfPosition(file, start + length); - const lastLineInFile = getLineAndCharacterOfPosition(file, file.text.length).line; - - const hasMoreThanFiveLines = (lastLine - firstLine) >= 4; - let gutterWidth = (lastLine + 1 + "").length; - if (hasMoreThanFiveLines) { - gutterWidth = Math.max(ellipsis.length, gutterWidth); - } - - let context = ""; - for (let i = firstLine; i <= lastLine; i++) { - context += host.getNewLine(); - // If the error spans over 5 lines, we'll only show the first 2 and last 2 lines, - // so we'll skip ahead to the second-to-last line. - if (hasMoreThanFiveLines && firstLine + 1 < i && i < lastLine - 1) { - context += indent + formatColorAndReset(padLeft(ellipsis, gutterWidth), gutterStyleSequence) + gutterSeparator + host.getNewLine(); - i = lastLine - 1; - } - - const lineStart = getPositionOfLineAndCharacter(file, i, 0); - const lineEnd = i < lastLineInFile ? getPositionOfLineAndCharacter(file, i + 1, 0) : file.text.length; - let lineContent = file.text.slice(lineStart, lineEnd); - lineContent = lineContent.replace(/\s+$/g, ""); // trim from end - lineContent = lineContent.replace("\t", " "); // convert tabs to single spaces - - // Output the gutter and the actual contents of the line. - context += indent + formatColorAndReset(padLeft(i + 1 + "", gutterWidth), gutterStyleSequence) + gutterSeparator; - context += lineContent + host.getNewLine(); - - // Output the gutter and the error span for the line using tildes. - context += indent + formatColorAndReset(padLeft("", gutterWidth), gutterStyleSequence) + gutterSeparator; - context += squiggleColor; - if (i === firstLine) { - // If we're on the last line, then limit it to the last character of the last line. - // Otherwise, we'll just squiggle the rest of the line, giving 'slice' no end position. - const lastCharForLine = i === lastLine ? lastLineChar : undefined; - - context += lineContent.slice(0, firstLineChar).replace(/\S/g, " "); - context += lineContent.slice(firstLineChar, lastCharForLine).replace(/./g, "~"); - } - else if (i === lastLine) { - context += lineContent.slice(0, lastLineChar).replace(/./g, "~"); - } - else { - // Squiggle the entire line. - context += lineContent.replace(/./g, "~"); - } - context += resetEscapeSequence; - } - return context; - } - - /* @internal */ - export function formatLocation(file: SourceFile, start: number, host: FormatDiagnosticsHost, color = formatColorAndReset) { - const { line: firstLine, character: firstLineChar } = getLineAndCharacterOfPosition(file, start); // TODO: GH#18217 - const relativeFileName = host ? convertToRelativePath(file.fileName, host.getCurrentDirectory(), fileName => host.getCanonicalFileName(fileName)) : file.fileName; - - let output = ""; - output += color(relativeFileName, ForegroundColorEscapeSequences.Cyan); - output += ":"; - output += color(`${firstLine + 1}`, ForegroundColorEscapeSequences.Yellow); - output += ":"; - output += color(`${firstLineChar + 1}`, ForegroundColorEscapeSequences.Yellow); - return output; - } - - export function formatDiagnosticsWithColorAndContext(diagnostics: readonly Diagnostic[], host: FormatDiagnosticsHost): string { - let output = ""; - for (const diagnostic of diagnostics) { - if (diagnostic.file) { - const { file, start } = diagnostic; - output += formatLocation(file, start!, host); // TODO: GH#18217 - output += " - "; - } - - output += formatColorAndReset(diagnosticCategoryName(diagnostic), getCategoryFormat(diagnostic.category)); - output += formatColorAndReset(` TS${diagnostic.code}: `, ForegroundColorEscapeSequences.Grey); - output += flattenDiagnosticMessageText(diagnostic.messageText, host.getNewLine()); - - if (diagnostic.file) { + const { file, start } = diagnostic; + output += formatLocation(file, start!, host); // TODO: GH#18217 + output += " - "; + } + output += formatColorAndReset(diagnosticCategoryName(diagnostic), getCategoryFormat(diagnostic.category)); + output += formatColorAndReset(` TS${diagnostic.code}: `, ForegroundColorEscapeSequences.Grey); + output += flattenDiagnosticMessageText(diagnostic.messageText, host.getNewLine()); + if (diagnostic.file) { + output += host.getNewLine(); + output += formatCodeSpan(diagnostic.file, diagnostic.start!, diagnostic.length!, "", getCategoryFormat(diagnostic.category), host); // TODO: GH#18217 + if (diagnostic.relatedInformation) { output += host.getNewLine(); - output += formatCodeSpan(diagnostic.file, diagnostic.start!, diagnostic.length!, "", getCategoryFormat(diagnostic.category), host); // TODO: GH#18217 - if (diagnostic.relatedInformation) { - output += host.getNewLine(); - for (const { file, start, length, messageText } of diagnostic.relatedInformation) { - if (file) { - output += host.getNewLine(); - output += halfIndent + formatLocation(file, start!, host); // TODO: GH#18217 - output += formatCodeSpan(file, start!, length!, indent, ForegroundColorEscapeSequences.Cyan, host); // TODO: GH#18217 - } + for (const { file, start, length, messageText } of diagnostic.relatedInformation) { + if (file) { output += host.getNewLine(); - output += indent + flattenDiagnosticMessageText(messageText, host.getNewLine()); + output += halfIndent + formatLocation(file, start!, host); // TODO: GH#18217 + output += formatCodeSpan(file, start!, length!, indent, ForegroundColorEscapeSequences.Cyan, host); // TODO: GH#18217 } + output += host.getNewLine(); + output += indent + flattenDiagnosticMessageText(messageText, host.getNewLine()); } } - - output += host.getNewLine(); } - return output; + output += host.getNewLine(); } - - export function flattenDiagnosticMessageText(diag: string | DiagnosticMessageChain | undefined, newLine: string, indent = 0): string { - if (isString(diag)) { - return diag; - } - else if (diag === undefined) { - return ""; - } - let result = ""; - if (indent) { - result += newLine; - - for (let i = 0; i < indent; i++) { - result += " "; - } + return output; +} +export function flattenDiagnosticMessageText(diag: string | DiagnosticMessageChain | undefined, newLine: string, indent = 0): string { + if (isString(diag)) { + return diag; + } + else if (diag === undefined) { + return ""; + } + let result = ""; + if (indent) { + result += newLine; + for (let i = 0; i < indent; i++) { + result += " "; } - result += diag.messageText; - indent++; - if (diag.next) { - for (const kid of diag.next) { - result += flattenDiagnosticMessageText(kid, newLine, indent); - } + } + result += diag.messageText; + indent++; + if (diag.next) { + for (const kid of diag.next) { + result += flattenDiagnosticMessageText(kid, newLine, indent); } - return result; } - - /* @internal */ - export function loadWithLocalCache(names: string[], containingFile: string, redirectedReference: ResolvedProjectReference | undefined, loader: (name: string, containingFile: string, redirectedReference: ResolvedProjectReference | undefined) => T): T[] { - if (names.length === 0) { - return []; + return result; +} +/* @internal */ +export function loadWithLocalCache(names: string[], containingFile: string, redirectedReference: ResolvedProjectReference | undefined, loader: (name: string, containingFile: string, redirectedReference: ResolvedProjectReference | undefined) => T): T[] { + if (names.length === 0) { + return []; + } + const resolutions: T[] = []; + const cache = createMap(); + for (const name of names) { + let result: T; + if (cache.has(name)) { + result = cache.get(name)!; } - const resolutions: T[] = []; - const cache = createMap(); - for (const name of names) { - let result: T; - if (cache.has(name)) { - result = cache.get(name)!; - } - else { - cache.set(name, result = loader(name, containingFile, redirectedReference)); - } - resolutions.push(result); + else { + cache.set(name, result = loader(name, containingFile, redirectedReference)); } - return resolutions; + resolutions.push(result); } - - /* @internal */ - export const inferredTypesContainingFile = "__inferred type names__.ts"; - - interface DiagnosticCache { - perFile?: Map; - allDiagnostics?: readonly T[]; + return resolutions; +} +/* @internal */ +export const inferredTypesContainingFile = "__inferred type names__.ts"; +interface DiagnosticCache { + perFile?: ts.Map; + allDiagnostics?: readonly T[]; +} +interface RefFile extends TextRange { + kind: RefFileKind; + index: number; + file: SourceFile; +} +/** + * Determines if program structure is upto date or needs to be recreated + */ +/* @internal */ +export function isProgramUptoDate(program: Program | undefined, rootFileNames: string[], newOptions: CompilerOptions, getSourceVersion: (path: Path, fileName: string) => string | undefined, fileExists: (fileName: string) => boolean, hasInvalidatedResolution: HasInvalidatedResolution, hasChangedAutomaticTypeDirectiveNames: boolean, projectReferences: readonly ProjectReference[] | undefined): boolean { + // If we haven't created a program yet or have changed automatic type directives, then it is not up-to-date + if (!program || hasChangedAutomaticTypeDirectiveNames) { + return false; } - - interface RefFile extends TextRange { - kind: RefFileKind; - index: number; - file: SourceFile; + // If root file names don't match + if (!arrayIsEqualTo(program.getRootFileNames(), rootFileNames)) { + return false; } - - /** - * Determines if program structure is upto date or needs to be recreated - */ - /* @internal */ - export function isProgramUptoDate( - program: Program | undefined, - rootFileNames: string[], - newOptions: CompilerOptions, - getSourceVersion: (path: Path, fileName: string) => string | undefined, - fileExists: (fileName: string) => boolean, - hasInvalidatedResolution: HasInvalidatedResolution, - hasChangedAutomaticTypeDirectiveNames: boolean, - projectReferences: readonly ProjectReference[] | undefined - ): boolean { - // If we haven't created a program yet or have changed automatic type directives, then it is not up-to-date - if (!program || hasChangedAutomaticTypeDirectiveNames) { - return false; - } - - // If root file names don't match - if (!arrayIsEqualTo(program.getRootFileNames(), rootFileNames)) { - return false; - } - - let seenResolvedRefs: ResolvedProjectReference[] | undefined; - - // If project references don't match - if (!arrayIsEqualTo(program.getProjectReferences(), projectReferences, projectReferenceUptoDate)) { - return false; - } - - // If any file is not up-to-date, then the whole program is not up-to-date - if (program.getSourceFiles().some(sourceFileNotUptoDate)) { - return false; - } - - // If any of the missing file paths are now created - if (program.getMissingFilePaths().some(fileExists)) { - return false; - } - - const currentOptions = program.getCompilerOptions(); - // If the compilation settings do no match, then the program is not up-to-date - if (!compareDataObjects(currentOptions, newOptions)) { + let seenResolvedRefs: ResolvedProjectReference[] | undefined; + // If project references don't match + if (!arrayIsEqualTo(program.getProjectReferences(), projectReferences, projectReferenceUptoDate)) { + return false; + } + // If any file is not up-to-date, then the whole program is not up-to-date + if (program.getSourceFiles().some(sourceFileNotUptoDate)) { + return false; + } + // If any of the missing file paths are now created + if (program.getMissingFilePaths().some(fileExists)) { + return false; + } + const currentOptions = program.getCompilerOptions(); + // If the compilation settings do no match, then the program is not up-to-date + if (!compareDataObjects(currentOptions, newOptions)) { + return false; + } + // If everything matches but the text of config file is changed, + // error locations can change for program options, so update the program + if (currentOptions.configFile && newOptions.configFile) { + return currentOptions.configFile.text === newOptions.configFile.text; + } + return true; + function sourceFileNotUptoDate(sourceFile: SourceFile) { + return !sourceFileVersionUptoDate(sourceFile) || + hasInvalidatedResolution(sourceFile.path); + } + function sourceFileVersionUptoDate(sourceFile: SourceFile) { + return sourceFile.version === getSourceVersion(sourceFile.resolvedPath, sourceFile.fileName); + } + function projectReferenceUptoDate(oldRef: ProjectReference, newRef: ProjectReference, index: number) { + if (!projectReferenceIsEqualTo(oldRef, newRef)) { return false; } - - // If everything matches but the text of config file is changed, - // error locations can change for program options, so update the program - if (currentOptions.configFile && newOptions.configFile) { - return currentOptions.configFile.text === newOptions.configFile.text; - } - - return true; - - function sourceFileNotUptoDate(sourceFile: SourceFile) { - return !sourceFileVersionUptoDate(sourceFile) || - hasInvalidatedResolution(sourceFile.path); - } - - function sourceFileVersionUptoDate(sourceFile: SourceFile) { - return sourceFile.version === getSourceVersion(sourceFile.resolvedPath, sourceFile.fileName); - } - - function projectReferenceUptoDate(oldRef: ProjectReference, newRef: ProjectReference, index: number) { - if (!projectReferenceIsEqualTo(oldRef, newRef)) { - return false; + return resolvedProjectReferenceUptoDate(program!.getResolvedProjectReferences()![index], oldRef); + } + function resolvedProjectReferenceUptoDate(oldResolvedRef: ResolvedProjectReference | undefined, oldRef: ProjectReference): boolean { + if (oldResolvedRef) { + if (contains(seenResolvedRefs, oldResolvedRef)) { + // Assume true + return true; } - return resolvedProjectReferenceUptoDate(program!.getResolvedProjectReferences()![index], oldRef); - } - - function resolvedProjectReferenceUptoDate(oldResolvedRef: ResolvedProjectReference | undefined, oldRef: ProjectReference): boolean { - if (oldResolvedRef) { - if (contains(seenResolvedRefs, oldResolvedRef)) { - // Assume true - return true; - } - - // If sourceFile for the oldResolvedRef existed, check the version for uptodate - if (!sourceFileVersionUptoDate(oldResolvedRef.sourceFile)) { - return false; - } - - // Add to seen before checking the referenced paths of this config file - (seenResolvedRefs || (seenResolvedRefs = [])).push(oldResolvedRef); - - // If child project references are upto date, this project reference is uptodate - return !forEach(oldResolvedRef.references, (childResolvedRef, index) => - !resolvedProjectReferenceUptoDate(childResolvedRef, oldResolvedRef.commandLine.projectReferences![index])); + // If sourceFile for the oldResolvedRef existed, check the version for uptodate + if (!sourceFileVersionUptoDate(oldResolvedRef.sourceFile)) { + return false; } - - // In old program, not able to resolve project reference path, - // so if config file doesnt exist, it is uptodate. - return !fileExists(resolveProjectReferencePath(oldRef)); + // Add to seen before checking the referenced paths of this config file + (seenResolvedRefs || (seenResolvedRefs = [])).push(oldResolvedRef); + // If child project references are upto date, this project reference is uptodate + return !forEach(oldResolvedRef.references, (childResolvedRef, index) => !resolvedProjectReferenceUptoDate(childResolvedRef, oldResolvedRef.commandLine.projectReferences![index])); } + // In old program, not able to resolve project reference path, + // so if config file doesnt exist, it is uptodate. + return !fileExists(resolveProjectReferencePath(oldRef)); } - - export function getConfigFileParsingDiagnostics(configFileParseResult: ParsedCommandLine): readonly Diagnostic[] { - return configFileParseResult.options.configFile ? - [...configFileParseResult.options.configFile.parseDiagnostics, ...configFileParseResult.errors] : - configFileParseResult.errors; +} +export function getConfigFileParsingDiagnostics(configFileParseResult: ParsedCommandLine): readonly Diagnostic[] { + return configFileParseResult.options.configFile ? + [...configFileParseResult.options.configFile.parseDiagnostics, ...configFileParseResult.errors] : + configFileParseResult.errors; +} +/** + * Determine if source file needs to be re-created even if its text hasn't changed + */ +function shouldProgramCreateNewSourceFiles(program: Program | undefined, newOptions: CompilerOptions): boolean { + if (!program) + return false; + // If any compiler options change, we can't reuse old source file even if version match + // The change in options like these could result in change in syntax tree or `sourceFile.bindDiagnostics`. + const oldOptions = program.getCompilerOptions(); + return !!sourceFileAffectingCompilerOptions.some(option => !isJsonEqual(getCompilerOptionValue(oldOptions, option), getCompilerOptionValue(newOptions, option))); +} +function createCreateProgramOptions(rootNames: readonly string[], options: CompilerOptions, host?: CompilerHost, oldProgram?: Program, configFileParsingDiagnostics?: readonly Diagnostic[]): CreateProgramOptions { + return { + rootNames, + options, + host, + oldProgram, + configFileParsingDiagnostics + }; +} +/** + * Create a new 'Program' instance. A Program is an immutable collection of 'SourceFile's and a 'CompilerOptions' + * that represent a compilation unit. + * + * Creating a program proceeds from a set of root files, expanding the set of inputs by following imports and + * triple-slash-reference-path directives transitively. '@types' and triple-slash-reference-types are also pulled in. + * + * @param createProgramOptions - The options for creating a program. + * @returns A 'Program' object. + */ +export function createProgram(createProgramOptions: CreateProgramOptions): Program; +/** + * Create a new 'Program' instance. A Program is an immutable collection of 'SourceFile's and a 'CompilerOptions' + * that represent a compilation unit. + * + * Creating a program proceeds from a set of root files, expanding the set of inputs by following imports and + * triple-slash-reference-path directives transitively. '@types' and triple-slash-reference-types are also pulled in. + * + * @param rootNames - A set of root files. + * @param options - The compiler options which should be used. + * @param host - The host interacts with the underlying file system. + * @param oldProgram - Reuses an old program structure. + * @param configFileParsingDiagnostics - error during config file parsing + * @returns A 'Program' object. + */ +export function createProgram(rootNames: readonly string[], options: CompilerOptions, host?: CompilerHost, oldProgram?: Program, configFileParsingDiagnostics?: readonly Diagnostic[]): Program; +export function createProgram(rootNamesOrOptions: readonly string[] | CreateProgramOptions, _options?: CompilerOptions, _host?: CompilerHost, _oldProgram?: Program, _configFileParsingDiagnostics?: readonly Diagnostic[]): Program { + const createProgramOptions = isArray(rootNamesOrOptions) ? createCreateProgramOptions(rootNamesOrOptions, _options!, _host, _oldProgram, _configFileParsingDiagnostics) : rootNamesOrOptions; // TODO: GH#18217 + const { rootNames, options, configFileParsingDiagnostics, projectReferences } = createProgramOptions; + let { oldProgram } = createProgramOptions; + let processingDefaultLibFiles: SourceFile[] | undefined; + let processingOtherFiles: SourceFile[] | undefined; + let files: SourceFile[]; + let symlinks: ts.ReadonlyMap | undefined; + let commonSourceDirectory: string; + let diagnosticsProducingTypeChecker: TypeChecker; + let noDiagnosticsTypeChecker: TypeChecker; + let classifiableNames: UnderscoreEscapedMap; + const ambientModuleNameToUnmodifiedFileName = createMap(); + // Todo:: Use this to report why file was included in --extendedDiagnostics + let refFileMap: MultiMap | undefined; + const cachedBindAndCheckDiagnosticsForFile: DiagnosticCache = {}; + const cachedDeclarationDiagnosticsForFile: DiagnosticCache = {}; + let resolvedTypeReferenceDirectives = createMap(); + let fileProcessingDiagnostics = createDiagnosticCollection(); + // The below settings are to track if a .js file should be add to the program if loaded via searching under node_modules. + // This works as imported modules are discovered recursively in a depth first manner, specifically: + // - For each root file, findSourceFile is called. + // - This calls processImportedModules for each module imported in the source file. + // - This calls resolveModuleNames, and then calls findSourceFile for each resolved module. + // As all these operations happen - and are nested - within the createProgram call, they close over the below variables. + // The current resolution depth is tracked by incrementing/decrementing as the depth first search progresses. + const maxNodeModuleJsDepth = typeof options.maxNodeModuleJsDepth === "number" ? options.maxNodeModuleJsDepth : 0; + let currentNodeModulesDepth = 0; + // If a module has some of its imports skipped due to being at the depth limit under node_modules, then track + // this, as it may be imported at a shallower depth later, and then it will need its skipped imports processed. + const modulesWithElidedImports = createMap(); + // Track source files that are source files found by searching under node_modules, as these shouldn't be compiled. + const sourceFilesFoundSearchingNodeModules = createMap(); + mark("beforeProgram"); + const host = createProgramOptions.host || createCompilerHost(options); + const configParsingHost = parseConfigHostFromCompilerHostLike(host); + let skipDefaultLib = options.noLib; + const getDefaultLibraryFileName = memoize(() => host.getDefaultLibFileName(options)); + const defaultLibraryPath = host.getDefaultLibLocation ? host.getDefaultLibLocation() : getDirectoryPath(getDefaultLibraryFileName()); + const programDiagnostics = createDiagnosticCollection(); + const currentDirectory = host.getCurrentDirectory(); + const supportedExtensions = getSupportedExtensions(options); + const supportedExtensionsWithJsonIfResolveJsonModule = getSuppoertedExtensionsWithJsonIfResolveJsonModule(options, supportedExtensions); + // Map storing if there is emit blocking diagnostics for given input + const hasEmitBlockingDiagnostics = createMap(); + let _compilerOptionsObjectLiteralSyntax: ObjectLiteralExpression | null | undefined; + let moduleResolutionCache: ModuleResolutionCache | undefined; + let actualResolveModuleNamesWorker: (moduleNames: string[], containingFile: string, reusedNames?: string[], redirectedReference?: ResolvedProjectReference) => ResolvedModuleFull[]; + const hasInvalidatedResolution = host.hasInvalidatedResolution || returnFalse; + if (host.resolveModuleNames) { + actualResolveModuleNamesWorker = (moduleNames, containingFile, reusedNames, redirectedReference) => host.resolveModuleNames!(Debug.checkEachDefined(moduleNames), containingFile, reusedNames, redirectedReference, options).map(resolved => { + // An older host may have omitted extension, in which case we should infer it from the file extension of resolvedFileName. + if (!resolved || (resolved as ResolvedModuleFull).extension !== undefined) { + return resolved as ResolvedModuleFull; + } + const withExtension = (clone(resolved) as ResolvedModuleFull); + withExtension.extension = extensionFromPath(resolved.resolvedFileName); + return withExtension; + }); } - - /** - * Determine if source file needs to be re-created even if its text hasn't changed - */ - function shouldProgramCreateNewSourceFiles(program: Program | undefined, newOptions: CompilerOptions): boolean { - if (!program) return false; - // If any compiler options change, we can't reuse old source file even if version match - // The change in options like these could result in change in syntax tree or `sourceFile.bindDiagnostics`. - const oldOptions = program.getCompilerOptions(); - return !!sourceFileAffectingCompilerOptions.some(option => - !isJsonEqual(getCompilerOptionValue(oldOptions, option), getCompilerOptionValue(newOptions, option))); - } - - function createCreateProgramOptions(rootNames: readonly string[], options: CompilerOptions, host?: CompilerHost, oldProgram?: Program, configFileParsingDiagnostics?: readonly Diagnostic[]): CreateProgramOptions { - return { - rootNames, - options, - host, - oldProgram, - configFileParsingDiagnostics - }; + else { + moduleResolutionCache = createModuleResolutionCache(currentDirectory, x => host.getCanonicalFileName(x), options); + const loader = (moduleName: string, containingFile: string, redirectedReference: ResolvedProjectReference | undefined) => resolveModuleName(moduleName, containingFile, options, host, moduleResolutionCache, redirectedReference).resolvedModule!; // TODO: GH#18217 + actualResolveModuleNamesWorker = (moduleNames, containingFile, _reusedNames, redirectedReference) => loadWithLocalCache(Debug.checkEachDefined(moduleNames), containingFile, redirectedReference, loader); } - - /** - * Create a new 'Program' instance. A Program is an immutable collection of 'SourceFile's and a 'CompilerOptions' - * that represent a compilation unit. - * - * Creating a program proceeds from a set of root files, expanding the set of inputs by following imports and - * triple-slash-reference-path directives transitively. '@types' and triple-slash-reference-types are also pulled in. - * - * @param createProgramOptions - The options for creating a program. - * @returns A 'Program' object. - */ - export function createProgram(createProgramOptions: CreateProgramOptions): Program; + let actualResolveTypeReferenceDirectiveNamesWorker: (typeDirectiveNames: string[], containingFile: string, redirectedReference?: ResolvedProjectReference) => (ResolvedTypeReferenceDirective | undefined)[]; + if (host.resolveTypeReferenceDirectives) { + actualResolveTypeReferenceDirectiveNamesWorker = (typeDirectiveNames, containingFile, redirectedReference) => host.resolveTypeReferenceDirectives!(Debug.checkEachDefined(typeDirectiveNames), containingFile, redirectedReference, options); + } + else { + const loader = (typesRef: string, containingFile: string, redirectedReference: ResolvedProjectReference | undefined) => resolveTypeReferenceDirective(typesRef, containingFile, options, host, redirectedReference).resolvedTypeReferenceDirective!; // TODO: GH#18217 + actualResolveTypeReferenceDirectiveNamesWorker = (typeReferenceDirectiveNames, containingFile, redirectedReference) => loadWithLocalCache(Debug.checkEachDefined(typeReferenceDirectiveNames), containingFile, redirectedReference, loader); + } + // Map from a stringified PackageId to the source file with that id. + // Only one source file may have a given packageId. Others become redirects (see createRedirectSourceFile). + // `packageIdToSourceFile` is only used while building the program, while `sourceFileToPackageName` and `isSourceFileTargetOfRedirect` are kept around. + const packageIdToSourceFile = createMap(); + // Maps from a SourceFile's `.path` to the name of the package it was imported with. + let sourceFileToPackageName = createMap(); + // Key is a file name. Value is the (non-empty, or undefined) list of files that redirect to it. + let redirectTargetsMap = createMultiMap(); /** - * Create a new 'Program' instance. A Program is an immutable collection of 'SourceFile's and a 'CompilerOptions' - * that represent a compilation unit. - * - * Creating a program proceeds from a set of root files, expanding the set of inputs by following imports and - * triple-slash-reference-path directives transitively. '@types' and triple-slash-reference-types are also pulled in. - * - * @param rootNames - A set of root files. - * @param options - The compiler options which should be used. - * @param host - The host interacts with the underlying file system. - * @param oldProgram - Reuses an old program structure. - * @param configFileParsingDiagnostics - error during config file parsing - * @returns A 'Program' object. + * map with + * - SourceFile if present + * - false if sourceFile missing for source of project reference redirect + * - undefined otherwise */ - export function createProgram(rootNames: readonly string[], options: CompilerOptions, host?: CompilerHost, oldProgram?: Program, configFileParsingDiagnostics?: readonly Diagnostic[]): Program; - export function createProgram(rootNamesOrOptions: readonly string[] | CreateProgramOptions, _options?: CompilerOptions, _host?: CompilerHost, _oldProgram?: Program, _configFileParsingDiagnostics?: readonly Diagnostic[]): Program { - const createProgramOptions = isArray(rootNamesOrOptions) ? createCreateProgramOptions(rootNamesOrOptions, _options!, _host, _oldProgram, _configFileParsingDiagnostics) : rootNamesOrOptions; // TODO: GH#18217 - const { rootNames, options, configFileParsingDiagnostics, projectReferences } = createProgramOptions; - let { oldProgram } = createProgramOptions; - - let processingDefaultLibFiles: SourceFile[] | undefined; - let processingOtherFiles: SourceFile[] | undefined; - let files: SourceFile[]; - let symlinks: ReadonlyMap | undefined; - let commonSourceDirectory: string; - let diagnosticsProducingTypeChecker: TypeChecker; - let noDiagnosticsTypeChecker: TypeChecker; - let classifiableNames: UnderscoreEscapedMap; - const ambientModuleNameToUnmodifiedFileName = createMap(); - // Todo:: Use this to report why file was included in --extendedDiagnostics - let refFileMap: MultiMap | undefined; - - const cachedBindAndCheckDiagnosticsForFile: DiagnosticCache = {}; - const cachedDeclarationDiagnosticsForFile: DiagnosticCache = {}; - - let resolvedTypeReferenceDirectives = createMap(); - let fileProcessingDiagnostics = createDiagnosticCollection(); - - // The below settings are to track if a .js file should be add to the program if loaded via searching under node_modules. - // This works as imported modules are discovered recursively in a depth first manner, specifically: - // - For each root file, findSourceFile is called. - // - This calls processImportedModules for each module imported in the source file. - // - This calls resolveModuleNames, and then calls findSourceFile for each resolved module. - // As all these operations happen - and are nested - within the createProgram call, they close over the below variables. - // The current resolution depth is tracked by incrementing/decrementing as the depth first search progresses. - const maxNodeModuleJsDepth = typeof options.maxNodeModuleJsDepth === "number" ? options.maxNodeModuleJsDepth : 0; - let currentNodeModulesDepth = 0; - - // If a module has some of its imports skipped due to being at the depth limit under node_modules, then track - // this, as it may be imported at a shallower depth later, and then it will need its skipped imports processed. - const modulesWithElidedImports = createMap(); - - // Track source files that are source files found by searching under node_modules, as these shouldn't be compiled. - const sourceFilesFoundSearchingNodeModules = createMap(); - - performance.mark("beforeProgram"); - - const host = createProgramOptions.host || createCompilerHost(options); - const configParsingHost = parseConfigHostFromCompilerHostLike(host); - - let skipDefaultLib = options.noLib; - const getDefaultLibraryFileName = memoize(() => host.getDefaultLibFileName(options)); - const defaultLibraryPath = host.getDefaultLibLocation ? host.getDefaultLibLocation() : getDirectoryPath(getDefaultLibraryFileName()); - const programDiagnostics = createDiagnosticCollection(); - const currentDirectory = host.getCurrentDirectory(); - const supportedExtensions = getSupportedExtensions(options); - const supportedExtensionsWithJsonIfResolveJsonModule = getSuppoertedExtensionsWithJsonIfResolveJsonModule(options, supportedExtensions); - - // Map storing if there is emit blocking diagnostics for given input - const hasEmitBlockingDiagnostics = createMap(); - let _compilerOptionsObjectLiteralSyntax: ObjectLiteralExpression | null | undefined; - - let moduleResolutionCache: ModuleResolutionCache | undefined; - let actualResolveModuleNamesWorker: (moduleNames: string[], containingFile: string, reusedNames?: string[], redirectedReference?: ResolvedProjectReference) => ResolvedModuleFull[]; - const hasInvalidatedResolution = host.hasInvalidatedResolution || returnFalse; - if (host.resolveModuleNames) { - actualResolveModuleNamesWorker = (moduleNames, containingFile, reusedNames, redirectedReference) => host.resolveModuleNames!(Debug.checkEachDefined(moduleNames), containingFile, reusedNames, redirectedReference, options).map(resolved => { - // An older host may have omitted extension, in which case we should infer it from the file extension of resolvedFileName. - if (!resolved || (resolved as ResolvedModuleFull).extension !== undefined) { - return resolved as ResolvedModuleFull; - } - const withExtension = clone(resolved) as ResolvedModuleFull; - withExtension.extension = extensionFromPath(resolved.resolvedFileName); - return withExtension; - }); - } - else { - moduleResolutionCache = createModuleResolutionCache(currentDirectory, x => host.getCanonicalFileName(x), options); - const loader = (moduleName: string, containingFile: string, redirectedReference: ResolvedProjectReference | undefined) => resolveModuleName(moduleName, containingFile, options, host, moduleResolutionCache, redirectedReference).resolvedModule!; // TODO: GH#18217 - actualResolveModuleNamesWorker = (moduleNames, containingFile, _reusedNames, redirectedReference) => loadWithLocalCache(Debug.checkEachDefined(moduleNames), containingFile, redirectedReference, loader); - } - - let actualResolveTypeReferenceDirectiveNamesWorker: (typeDirectiveNames: string[], containingFile: string, redirectedReference?: ResolvedProjectReference) => (ResolvedTypeReferenceDirective | undefined)[]; - if (host.resolveTypeReferenceDirectives) { - actualResolveTypeReferenceDirectiveNamesWorker = (typeDirectiveNames, containingFile, redirectedReference) => host.resolveTypeReferenceDirectives!(Debug.checkEachDefined(typeDirectiveNames), containingFile, redirectedReference, options); - } - else { - const loader = (typesRef: string, containingFile: string, redirectedReference: ResolvedProjectReference | undefined) => resolveTypeReferenceDirective(typesRef, containingFile, options, host, redirectedReference).resolvedTypeReferenceDirective!; // TODO: GH#18217 - actualResolveTypeReferenceDirectiveNamesWorker = (typeReferenceDirectiveNames, containingFile, redirectedReference) => loadWithLocalCache(Debug.checkEachDefined(typeReferenceDirectiveNames), containingFile, redirectedReference, loader); - } - - // Map from a stringified PackageId to the source file with that id. - // Only one source file may have a given packageId. Others become redirects (see createRedirectSourceFile). - // `packageIdToSourceFile` is only used while building the program, while `sourceFileToPackageName` and `isSourceFileTargetOfRedirect` are kept around. - const packageIdToSourceFile = createMap(); - // Maps from a SourceFile's `.path` to the name of the package it was imported with. - let sourceFileToPackageName = createMap(); - // Key is a file name. Value is the (non-empty, or undefined) list of files that redirect to it. - let redirectTargetsMap = createMultiMap(); - - /** - * map with - * - SourceFile if present - * - false if sourceFile missing for source of project reference redirect - * - undefined otherwise - */ - const filesByName = createMap(); - let missingFilePaths: readonly Path[] | undefined; - // stores 'filename -> file association' ignoring case - // used to track cases when two file names differ only in casing - const filesByNameIgnoreCase = host.useCaseSensitiveFileNames() ? createMap() : undefined; - - // A parallel array to projectReferences storing the results of reading in the referenced tsconfig files - let resolvedProjectReferences: readonly (ResolvedProjectReference | undefined)[] | undefined; - let projectReferenceRedirects: Map | undefined; - let mapFromFileToProjectReferenceRedirects: Map | undefined; - let mapFromToProjectReferenceRedirectSource: Map | undefined; - const useSourceOfProjectReferenceRedirect = !!host.useSourceOfProjectReferenceRedirect && host.useSourceOfProjectReferenceRedirect(); - - const shouldCreateNewSourceFile = shouldProgramCreateNewSourceFiles(oldProgram, options); - // We set `structuralIsReused` to `undefined` because `tryReuseStructureFromOldProgram` calls `tryReuseStructureFromOldProgram` which checks - // `structuralIsReused`, which would be a TDZ violation if it was not set in advance to `undefined`. - let structuralIsReused: StructureIsReused | undefined; - structuralIsReused = tryReuseStructureFromOldProgram(); // eslint-disable-line prefer-const - if (structuralIsReused !== StructureIsReused.Completely) { - processingDefaultLibFiles = []; - processingOtherFiles = []; - - if (projectReferences) { - if (!resolvedProjectReferences) { - resolvedProjectReferences = projectReferences.map(parseProjectReferenceConfigFile); - } - if (host.setResolvedProjectReferenceCallbacks) { - host.setResolvedProjectReferenceCallbacks({ - getSourceOfProjectReferenceRedirect, - forEachResolvedProjectReference - }); - } - if (rootNames.length) { - for (const parsedRef of resolvedProjectReferences) { - if (!parsedRef) continue; - const out = parsedRef.commandLine.options.outFile || parsedRef.commandLine.options.out; - if (useSourceOfProjectReferenceRedirect) { - if (out || getEmitModuleKind(parsedRef.commandLine.options) === ModuleKind.None) { - for (const fileName of parsedRef.commandLine.fileNames) { - processSourceFile(fileName, /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, /*packageId*/ undefined); - } + const filesByName = createMap(); + let missingFilePaths: readonly Path[] | undefined; + // stores 'filename -> file association' ignoring case + // used to track cases when two file names differ only in casing + const filesByNameIgnoreCase = host.useCaseSensitiveFileNames() ? createMap() : undefined; + // A parallel array to projectReferences storing the results of reading in the referenced tsconfig files + let resolvedProjectReferences: readonly (ResolvedProjectReference | undefined)[] | undefined; + let projectReferenceRedirects: ts.Map | undefined; + let mapFromFileToProjectReferenceRedirects: ts.Map | undefined; + let mapFromToProjectReferenceRedirectSource: ts.Map | undefined; + const useSourceOfProjectReferenceRedirect = !!host.useSourceOfProjectReferenceRedirect && host.useSourceOfProjectReferenceRedirect(); + const shouldCreateNewSourceFile = shouldProgramCreateNewSourceFiles(oldProgram, options); + // We set `structuralIsReused` to `undefined` because `tryReuseStructureFromOldProgram` calls `tryReuseStructureFromOldProgram` which checks + // `structuralIsReused`, which would be a TDZ violation if it was not set in advance to `undefined`. + let structuralIsReused: StructureIsReused | undefined; + structuralIsReused = tryReuseStructureFromOldProgram(); // eslint-disable-line prefer-const + if (structuralIsReused !== StructureIsReused.Completely) { + processingDefaultLibFiles = []; + processingOtherFiles = []; + if (projectReferences) { + if (!resolvedProjectReferences) { + resolvedProjectReferences = projectReferences.map(parseProjectReferenceConfigFile); + } + if (host.setResolvedProjectReferenceCallbacks) { + host.setResolvedProjectReferenceCallbacks({ + getSourceOfProjectReferenceRedirect, + forEachResolvedProjectReference + }); + } + if (rootNames.length) { + for (const parsedRef of resolvedProjectReferences) { + if (!parsedRef) + continue; + const out = parsedRef.commandLine.options.outFile || parsedRef.commandLine.options.out; + if (useSourceOfProjectReferenceRedirect) { + if (out || getEmitModuleKind(parsedRef.commandLine.options) === ModuleKind.None) { + for (const fileName of parsedRef.commandLine.fileNames) { + processSourceFile(fileName, /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, /*packageId*/ undefined); } } - else { - if (out) { - processSourceFile(changeExtension(out, ".d.ts"), /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, /*packageId*/ undefined); - } - else if (getEmitModuleKind(parsedRef.commandLine.options) === ModuleKind.None) { - for (const fileName of parsedRef.commandLine.fileNames) { - if (!fileExtensionIs(fileName, Extension.Dts) && !fileExtensionIs(fileName, Extension.Json)) { - processSourceFile(getOutputDeclarationFileName(fileName, parsedRef.commandLine, !host.useCaseSensitiveFileNames()), /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, /*packageId*/ undefined); - } + } + else { + if (out) { + processSourceFile(changeExtension(out, ".d.ts"), /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, /*packageId*/ undefined); + } + else if (getEmitModuleKind(parsedRef.commandLine.options) === ModuleKind.None) { + for (const fileName of parsedRef.commandLine.fileNames) { + if (!fileExtensionIs(fileName, Extension.Dts) && !fileExtensionIs(fileName, Extension.Json)) { + processSourceFile(getOutputDeclarationFileName(fileName, parsedRef.commandLine, !host.useCaseSensitiveFileNames()), /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, /*packageId*/ undefined); } } } } } } - - forEach(rootNames, name => processRootFile(name, /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false)); - - // load type declarations specified via 'types' argument or implicitly from types/ and node_modules/@types folders - const typeReferences: string[] = rootNames.length ? getAutomaticTypeDirectiveNames(options, host) : emptyArray; - - if (typeReferences.length) { - // This containingFilename needs to match with the one used in managed-side - const containingDirectory = options.configFilePath ? getDirectoryPath(options.configFilePath) : host.getCurrentDirectory(); - const containingFilename = combinePaths(containingDirectory, inferredTypesContainingFile); - const resolutions = resolveTypeReferenceDirectiveNamesWorker(typeReferences, containingFilename); - for (let i = 0; i < typeReferences.length; i++) { - processTypeReferenceDirective(typeReferences[i], resolutions[i]); - } - } - - // Do not process the default library if: - // - The '--noLib' flag is used. - // - A 'no-default-lib' reference comment is encountered in - // processing the root files. - if (rootNames.length && !skipDefaultLib) { - // If '--lib' is not specified, include default library file according to '--target' - // otherwise, using options specified in '--lib' instead of '--target' default library file - const defaultLibraryFileName = getDefaultLibraryFileName(); - if (!options.lib && defaultLibraryFileName) { - processRootFile(defaultLibraryFileName, /*isDefaultLib*/ true, /*ignoreNoDefaultLib*/ false); - } - else { - forEach(options.lib, libFileName => { - processRootFile(combinePaths(defaultLibraryPath, libFileName), /*isDefaultLib*/ true, /*ignoreNoDefaultLib*/ false); - }); - } - } - - missingFilePaths = arrayFrom(mapDefinedIterator(filesByName.entries(), ([path, file]) => file === undefined ? path as Path : undefined)); - files = stableSort(processingDefaultLibFiles, compareDefaultLibFiles).concat(processingOtherFiles); - processingDefaultLibFiles = undefined; - processingOtherFiles = undefined; - } - - Debug.assert(!!missingFilePaths); - - // Release any files we have acquired in the old program but are - // not part of the new program. - if (oldProgram && host.onReleaseOldSourceFile) { - const oldSourceFiles = oldProgram.getSourceFiles(); - for (const oldSourceFile of oldSourceFiles) { - const newFile = getSourceFileByPath(oldSourceFile.resolvedPath); - if (shouldCreateNewSourceFile || !newFile || - // old file wasnt redirect but new file is - (oldSourceFile.resolvedPath === oldSourceFile.path && newFile.resolvedPath !== oldSourceFile.path)) { - host.onReleaseOldSourceFile(oldSourceFile, oldProgram.getCompilerOptions(), !!getSourceFileByPath(oldSourceFile.path)); - } - } - oldProgram.forEachResolvedProjectReference((resolvedProjectReference, resolvedProjectReferencePath) => { - if (resolvedProjectReference && !getResolvedProjectReferenceByPath(resolvedProjectReferencePath)) { - host.onReleaseOldSourceFile!(resolvedProjectReference.sourceFile, oldProgram!.getCompilerOptions(), /*hasSourceFileByPath*/ false); - } - }); } - - // unconditionally set oldProgram to undefined to prevent it from being captured in closure - oldProgram = undefined; - - const program: Program = { - getRootFileNames: () => rootNames, - getSourceFile, - getSourceFileByPath, - getSourceFiles: () => files, - getMissingFilePaths: () => missingFilePaths!, // TODO: GH#18217 - getRefFileMap: () => refFileMap, - getFilesByNameMap: () => filesByName, - getCompilerOptions: () => options, - getSyntacticDiagnostics, - getOptionsDiagnostics, - getGlobalDiagnostics, - getSemanticDiagnostics, - getSuggestionDiagnostics, - getDeclarationDiagnostics, - getBindAndCheckDiagnostics, - getProgramDiagnostics, - getTypeChecker, - getClassifiableNames, - getDiagnosticsProducingTypeChecker, - getCommonSourceDirectory, - emit, - getCurrentDirectory: () => currentDirectory, - getNodeCount: () => getDiagnosticsProducingTypeChecker().getNodeCount(), - getIdentifierCount: () => getDiagnosticsProducingTypeChecker().getIdentifierCount(), - getSymbolCount: () => getDiagnosticsProducingTypeChecker().getSymbolCount(), - getTypeCount: () => getDiagnosticsProducingTypeChecker().getTypeCount(), - getInstantiationCount: () => getDiagnosticsProducingTypeChecker().getInstantiationCount(), - getRelationCacheSizes: () => getDiagnosticsProducingTypeChecker().getRelationCacheSizes(), - getFileProcessingDiagnostics: () => fileProcessingDiagnostics, - getResolvedTypeReferenceDirectives: () => resolvedTypeReferenceDirectives, - isSourceFileFromExternalLibrary, - isSourceFileDefaultLibrary, - dropDiagnosticsProducingTypeChecker, - getSourceFileFromReference, - getLibFileFromReference, - sourceFileToPackageName, - redirectTargetsMap, - isEmittedFile, - getConfigFileParsingDiagnostics, - getResolvedModuleWithFailedLookupLocationsFromCache, - getProjectReferences, - getResolvedProjectReferences, - getProjectReferenceRedirect, - getResolvedProjectReferenceToRedirect, - getResolvedProjectReferenceByPath, - forEachResolvedProjectReference, - isSourceOfProjectReferenceRedirect, - emitBuildInfo, - getProbableSymlinks - }; - - verifyCompilerOptions(); - performance.mark("afterProgram"); - performance.measure("Program", "beforeProgram", "afterProgram"); - - return program; - - function resolveModuleNamesWorker(moduleNames: string[], containingFile: string, reusedNames?: string[], redirectedReference?: ResolvedProjectReference) { - performance.mark("beforeResolveModule"); - const result = actualResolveModuleNamesWorker(moduleNames, containingFile, reusedNames, redirectedReference); - performance.mark("afterResolveModule"); - performance.measure("ResolveModule", "beforeResolveModule", "afterResolveModule"); - return result; + forEach(rootNames, name => processRootFile(name, /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false)); + // load type declarations specified via 'types' argument or implicitly from types/ and node_modules/@types folders + const typeReferences: string[] = rootNames.length ? getAutomaticTypeDirectiveNames(options, host) : emptyArray; + if (typeReferences.length) { + // This containingFilename needs to match with the one used in managed-side + const containingDirectory = options.configFilePath ? getDirectoryPath(options.configFilePath) : host.getCurrentDirectory(); + const containingFilename = combinePaths(containingDirectory, inferredTypesContainingFile); + const resolutions = resolveTypeReferenceDirectiveNamesWorker(typeReferences, containingFilename); + for (let i = 0; i < typeReferences.length; i++) { + processTypeReferenceDirective(typeReferences[i], resolutions[i]); + } } - - function resolveTypeReferenceDirectiveNamesWorker(typeDirectiveNames: string[], containingFile: string, redirectedReference?: ResolvedProjectReference) { - performance.mark("beforeResolveTypeReference"); - const result = actualResolveTypeReferenceDirectiveNamesWorker(typeDirectiveNames, containingFile, redirectedReference); - performance.mark("afterResolveTypeReference"); - performance.measure("ResolveTypeReference", "beforeResolveTypeReference", "afterResolveTypeReference"); - return result; + // Do not process the default library if: + // - The '--noLib' flag is used. + // - A 'no-default-lib' reference comment is encountered in + // processing the root files. + if (rootNames.length && !skipDefaultLib) { + // If '--lib' is not specified, include default library file according to '--target' + // otherwise, using options specified in '--lib' instead of '--target' default library file + const defaultLibraryFileName = getDefaultLibraryFileName(); + if (!options.lib && defaultLibraryFileName) { + processRootFile(defaultLibraryFileName, /*isDefaultLib*/ true, /*ignoreNoDefaultLib*/ false); + } + else { + forEach(options.lib, libFileName => { + processRootFile(combinePaths(defaultLibraryPath, libFileName), /*isDefaultLib*/ true, /*ignoreNoDefaultLib*/ false); + }); + } } - - function compareDefaultLibFiles(a: SourceFile, b: SourceFile) { - return compareValues(getDefaultLibFilePriority(a), getDefaultLibFilePriority(b)); - } - - function getDefaultLibFilePriority(a: SourceFile) { - if (containsPath(defaultLibraryPath, a.fileName, /*ignoreCase*/ false)) { - const basename = getBaseFileName(a.fileName); - if (basename === "lib.d.ts" || basename === "lib.es6.d.ts") return 0; - const name = removeSuffix(removePrefix(basename, "lib."), ".d.ts"); - const index = libs.indexOf(name); - if (index !== -1) return index + 1; - } - return libs.length + 2; - } - - function getResolvedModuleWithFailedLookupLocationsFromCache(moduleName: string, containingFile: string): ResolvedModuleWithFailedLookupLocations | undefined { - return moduleResolutionCache && resolveModuleNameFromCache(moduleName, containingFile, moduleResolutionCache); - } - - function toPath(fileName: string): Path { - return ts.toPath(fileName, currentDirectory, getCanonicalFileName); - } - - function getCommonSourceDirectory() { - if (commonSourceDirectory === undefined) { - const emittedFiles = filter(files, file => sourceFileMayBeEmitted(file, program)); - if (options.rootDir && checkSourceFilesBelongToPath(emittedFiles, options.rootDir)) { - // If a rootDir is specified use it as the commonSourceDirectory - commonSourceDirectory = getNormalizedAbsolutePath(options.rootDir, currentDirectory); - } - else if (options.composite && options.configFilePath) { - // Project compilations never infer their root from the input source paths - commonSourceDirectory = getDirectoryPath(normalizeSlashes(options.configFilePath)); - checkSourceFilesBelongToPath(emittedFiles, commonSourceDirectory); - } - else { - commonSourceDirectory = computeCommonSourceDirectory(emittedFiles); - } - - if (commonSourceDirectory && commonSourceDirectory[commonSourceDirectory.length - 1] !== directorySeparator) { - // Make sure directory path ends with directory separator so this string can directly - // used to replace with "" to get the relative path of the source file and the relative path doesn't - // start with / making it rooted path - commonSourceDirectory += directorySeparator; - } - } - return commonSourceDirectory; - } - - function getClassifiableNames() { - if (!classifiableNames) { - // Initialize a checker so that all our files are bound. - getTypeChecker(); - classifiableNames = createUnderscoreEscapedMap(); - - for (const sourceFile of files) { - copyEntries(sourceFile.classifiableNames!, classifiableNames); - } - } - - return classifiableNames; - } - - function resolveModuleNamesReusingOldState(moduleNames: string[], containingFile: string, file: SourceFile) { - if (structuralIsReused === StructureIsReused.Not && !file.ambientModuleNames.length) { - // If the old program state does not permit reusing resolutions and `file` does not contain locally defined ambient modules, - // the best we can do is fallback to the default logic. - return resolveModuleNamesWorker(moduleNames, containingFile, /*reusedNames*/ undefined, getResolvedProjectReferenceToRedirect(file.originalFileName)); - } - - const oldSourceFile = oldProgram && oldProgram.getSourceFile(containingFile); - if (oldSourceFile !== file && file.resolvedModules) { - // `file` was created for the new program. - // - // We only set `file.resolvedModules` via work from the current function, - // so it is defined iff we already called the current function on `file`. - // That call happened no later than the creation of the `file` object, - // which per above occurred during the current program creation. - // Since we assume the filesystem does not change during program creation, - // it is safe to reuse resolutions from the earlier call. - const result: ResolvedModuleFull[] = []; - for (const moduleName of moduleNames) { - const resolvedModule = file.resolvedModules.get(moduleName)!; - result.push(resolvedModule); - } - return result; + missingFilePaths = arrayFrom(mapDefinedIterator(filesByName.entries(), ([path, file]) => file === undefined ? path as Path : undefined)); + files = stableSort(processingDefaultLibFiles, compareDefaultLibFiles).concat(processingOtherFiles); + processingDefaultLibFiles = undefined; + processingOtherFiles = undefined; + } + Debug.assert(!!missingFilePaths); + // Release any files we have acquired in the old program but are + // not part of the new program. + if (oldProgram && host.onReleaseOldSourceFile) { + const oldSourceFiles = oldProgram.getSourceFiles(); + for (const oldSourceFile of oldSourceFiles) { + const newFile = getSourceFileByPath(oldSourceFile.resolvedPath); + if (shouldCreateNewSourceFile || !newFile || + // old file wasnt redirect but new file is + (oldSourceFile.resolvedPath === oldSourceFile.path && newFile.resolvedPath !== oldSourceFile.path)) { + host.onReleaseOldSourceFile(oldSourceFile, oldProgram.getCompilerOptions(), !!getSourceFileByPath(oldSourceFile.path)); + } + } + oldProgram.forEachResolvedProjectReference((resolvedProjectReference, resolvedProjectReferencePath) => { + if (resolvedProjectReference && !getResolvedProjectReferenceByPath(resolvedProjectReferencePath)) { + host.onReleaseOldSourceFile!(resolvedProjectReference.sourceFile, oldProgram!.getCompilerOptions(), /*hasSourceFileByPath*/ false); } - // At this point, we know at least one of the following hold: - // - file has local declarations for ambient modules - // - old program state is available - // With this information, we can infer some module resolutions without performing resolution. - - /** An ordered list of module names for which we cannot recover the resolution. */ - let unknownModuleNames: string[] | undefined; - /** - * The indexing of elements in this list matches that of `moduleNames`. - * - * Before combining results, result[i] is in one of the following states: - * * undefined: needs to be recomputed, - * * predictedToResolveToAmbientModuleMarker: known to be an ambient module. - * Needs to be reset to undefined before returning, - * * ResolvedModuleFull instance: can be reused. - */ - let result: ResolvedModuleFull[] | undefined; - let reusedNames: string[] | undefined; - /** A transient placeholder used to mark predicted resolution in the result list. */ - const predictedToResolveToAmbientModuleMarker: ResolvedModuleFull = {}; - - for (let i = 0; i < moduleNames.length; i++) { - const moduleName = moduleNames[i]; - // If the source file is unchanged and doesnt have invalidated resolution, reuse the module resolutions - if (file === oldSourceFile && !hasInvalidatedResolution(oldSourceFile.path)) { - const oldResolvedModule = oldSourceFile && oldSourceFile.resolvedModules!.get(moduleName); - if (oldResolvedModule) { - if (isTraceEnabled(options, host)) { - trace(host, Diagnostics.Reusing_resolution_of_module_0_to_file_1_from_old_program, moduleName, containingFile); - } - (result || (result = new Array(moduleNames.length)))[i] = oldResolvedModule; - (reusedNames || (reusedNames = [])).push(moduleName); - continue; - } - } - // We know moduleName resolves to an ambient module provided that moduleName: - // - is in the list of ambient modules locally declared in the current source file. - // - resolved to an ambient module in the old program whose declaration is in an unmodified file - // (so the same module declaration will land in the new program) - let resolvesToAmbientModuleInNonModifiedFile = false; - if (contains(file.ambientModuleNames, moduleName)) { - resolvesToAmbientModuleInNonModifiedFile = true; + }); + } + // unconditionally set oldProgram to undefined to prevent it from being captured in closure + oldProgram = undefined; + const program: Program = { + getRootFileNames: () => rootNames, + getSourceFile, + getSourceFileByPath, + getSourceFiles: () => files, + getMissingFilePaths: () => missingFilePaths!, + getRefFileMap: () => refFileMap, + getFilesByNameMap: () => filesByName, + getCompilerOptions: () => options, + getSyntacticDiagnostics, + getOptionsDiagnostics, + getGlobalDiagnostics, + getSemanticDiagnostics, + getSuggestionDiagnostics, + getDeclarationDiagnostics, + getBindAndCheckDiagnostics, + getProgramDiagnostics, + getTypeChecker, + getClassifiableNames, + getDiagnosticsProducingTypeChecker, + getCommonSourceDirectory, + emit, + getCurrentDirectory: () => currentDirectory, + getNodeCount: () => getDiagnosticsProducingTypeChecker().getNodeCount(), + getIdentifierCount: () => getDiagnosticsProducingTypeChecker().getIdentifierCount(), + getSymbolCount: () => getDiagnosticsProducingTypeChecker().getSymbolCount(), + getTypeCount: () => getDiagnosticsProducingTypeChecker().getTypeCount(), + getInstantiationCount: () => getDiagnosticsProducingTypeChecker().getInstantiationCount(), + getRelationCacheSizes: () => getDiagnosticsProducingTypeChecker().getRelationCacheSizes(), + getFileProcessingDiagnostics: () => fileProcessingDiagnostics, + getResolvedTypeReferenceDirectives: () => resolvedTypeReferenceDirectives, + isSourceFileFromExternalLibrary, + isSourceFileDefaultLibrary, + dropDiagnosticsProducingTypeChecker, + getSourceFileFromReference, + getLibFileFromReference, + sourceFileToPackageName, + redirectTargetsMap, + isEmittedFile, + getConfigFileParsingDiagnostics, + getResolvedModuleWithFailedLookupLocationsFromCache, + getProjectReferences, + getResolvedProjectReferences, + getProjectReferenceRedirect, + getResolvedProjectReferenceToRedirect, + getResolvedProjectReferenceByPath, + forEachResolvedProjectReference, + isSourceOfProjectReferenceRedirect, + emitBuildInfo, + getProbableSymlinks + }; + verifyCompilerOptions(); + mark("afterProgram"); + measure("Program", "beforeProgram", "afterProgram"); + return program; + function resolveModuleNamesWorker(moduleNames: string[], containingFile: string, reusedNames?: string[], redirectedReference?: ResolvedProjectReference) { + mark("beforeResolveModule"); + const result = actualResolveModuleNamesWorker(moduleNames, containingFile, reusedNames, redirectedReference); + mark("afterResolveModule"); + measure("ResolveModule", "beforeResolveModule", "afterResolveModule"); + return result; + } + function resolveTypeReferenceDirectiveNamesWorker(typeDirectiveNames: string[], containingFile: string, redirectedReference?: ResolvedProjectReference) { + mark("beforeResolveTypeReference"); + const result = actualResolveTypeReferenceDirectiveNamesWorker(typeDirectiveNames, containingFile, redirectedReference); + mark("afterResolveTypeReference"); + measure("ResolveTypeReference", "beforeResolveTypeReference", "afterResolveTypeReference"); + return result; + } + function compareDefaultLibFiles(a: SourceFile, b: SourceFile) { + return compareValues(getDefaultLibFilePriority(a), getDefaultLibFilePriority(b)); + } + function getDefaultLibFilePriority(a: SourceFile) { + if (containsPath(defaultLibraryPath, a.fileName, /*ignoreCase*/ false)) { + const basename = getBaseFileName(a.fileName); + if (basename === "lib.d.ts" || basename === "lib.es6.d.ts") + return 0; + const name = removeSuffix(removePrefix(basename, "lib."), ".d.ts"); + const index = libs.indexOf(name); + if (index !== -1) + return index + 1; + } + return libs.length + 2; + } + function getResolvedModuleWithFailedLookupLocationsFromCache(moduleName: string, containingFile: string): ResolvedModuleWithFailedLookupLocations | undefined { + return moduleResolutionCache && resolveModuleNameFromCache(moduleName, containingFile, moduleResolutionCache); + } + function toPath(fileName: string): Path { + return ts.toPath(fileName, currentDirectory, getCanonicalFileName); + } + function getCommonSourceDirectory() { + if (commonSourceDirectory === undefined) { + const emittedFiles = filter(files, file => sourceFileMayBeEmitted(file, program)); + if (options.rootDir && checkSourceFilesBelongToPath(emittedFiles, options.rootDir)) { + // If a rootDir is specified use it as the commonSourceDirectory + commonSourceDirectory = getNormalizedAbsolutePath(options.rootDir, currentDirectory); + } + else if (options.composite && options.configFilePath) { + // Project compilations never infer their root from the input source paths + commonSourceDirectory = getDirectoryPath(normalizeSlashes(options.configFilePath)); + checkSourceFilesBelongToPath(emittedFiles, commonSourceDirectory); + } + else { + commonSourceDirectory = computeCommonSourceDirectory(emittedFiles); + } + if (commonSourceDirectory && commonSourceDirectory[commonSourceDirectory.length - 1] !== directorySeparator) { + // Make sure directory path ends with directory separator so this string can directly + // used to replace with "" to get the relative path of the source file and the relative path doesn't + // start with / making it rooted path + commonSourceDirectory += directorySeparator; + } + } + return commonSourceDirectory; + } + function getClassifiableNames() { + if (!classifiableNames) { + // Initialize a checker so that all our files are bound. + getTypeChecker(); + classifiableNames = createUnderscoreEscapedMap(); + for (const sourceFile of files) { + copyEntries((sourceFile.classifiableNames!), classifiableNames); + } + } + return classifiableNames; + } + function resolveModuleNamesReusingOldState(moduleNames: string[], containingFile: string, file: SourceFile) { + if (structuralIsReused === StructureIsReused.Not && !file.ambientModuleNames.length) { + // If the old program state does not permit reusing resolutions and `file` does not contain locally defined ambient modules, + // the best we can do is fallback to the default logic. + return resolveModuleNamesWorker(moduleNames, containingFile, /*reusedNames*/ undefined, getResolvedProjectReferenceToRedirect(file.originalFileName)); + } + const oldSourceFile = oldProgram && oldProgram.getSourceFile(containingFile); + if (oldSourceFile !== file && file.resolvedModules) { + // `file` was created for the new program. + // + // We only set `file.resolvedModules` via work from the current function, + // so it is defined iff we already called the current function on `file`. + // That call happened no later than the creation of the `file` object, + // which per above occurred during the current program creation. + // Since we assume the filesystem does not change during program creation, + // it is safe to reuse resolutions from the earlier call. + const result: ResolvedModuleFull[] = []; + for (const moduleName of moduleNames) { + const resolvedModule = file.resolvedModules.get(moduleName)!; + result.push(resolvedModule); + } + return result; + } + // At this point, we know at least one of the following hold: + // - file has local declarations for ambient modules + // - old program state is available + // With this information, we can infer some module resolutions without performing resolution. + /** An ordered list of module names for which we cannot recover the resolution. */ + let unknownModuleNames: string[] | undefined; + /** + * The indexing of elements in this list matches that of `moduleNames`. + * + * Before combining results, result[i] is in one of the following states: + * * undefined: needs to be recomputed, + * * predictedToResolveToAmbientModuleMarker: known to be an ambient module. + * Needs to be reset to undefined before returning, + * * ResolvedModuleFull instance: can be reused. + */ + let result: ResolvedModuleFull[] | undefined; + let reusedNames: string[] | undefined; + /** A transient placeholder used to mark predicted resolution in the result list. */ + const predictedToResolveToAmbientModuleMarker: ResolvedModuleFull = ({}); + for (let i = 0; i < moduleNames.length; i++) { + const moduleName = moduleNames[i]; + // If the source file is unchanged and doesnt have invalidated resolution, reuse the module resolutions + if (file === oldSourceFile && !hasInvalidatedResolution(oldSourceFile.path)) { + const oldResolvedModule = oldSourceFile && oldSourceFile.resolvedModules!.get(moduleName); + if (oldResolvedModule) { if (isTraceEnabled(options, host)) { - trace(host, Diagnostics.Module_0_was_resolved_as_locally_declared_ambient_module_in_file_1, moduleName, containingFile); - } - } - else { - resolvesToAmbientModuleInNonModifiedFile = moduleNameResolvesToAmbientModuleInNonModifiedFile(moduleName); - } - - if (resolvesToAmbientModuleInNonModifiedFile) { - (result || (result = new Array(moduleNames.length)))[i] = predictedToResolveToAmbientModuleMarker; - } - else { - // Resolution failed in the old program, or resolved to an ambient module for which we can't reuse the result. - (unknownModuleNames || (unknownModuleNames = [])).push(moduleName); - } - } - - const resolutions = unknownModuleNames && unknownModuleNames.length - ? resolveModuleNamesWorker(unknownModuleNames, containingFile, reusedNames, getResolvedProjectReferenceToRedirect(file.originalFileName)) - : emptyArray; - - // Combine results of resolutions and predicted results - if (!result) { - // There were no unresolved/ambient resolutions. - Debug.assert(resolutions.length === moduleNames.length); - return resolutions; - } - - let j = 0; - for (let i = 0; i < result.length; i++) { - if (result[i]) { - // `result[i]` is either a `ResolvedModuleFull` or a marker. - // If it is the former, we can leave it as is. - if (result[i] === predictedToResolveToAmbientModuleMarker) { - result[i] = undefined!; // TODO: GH#18217 + trace(host, Diagnostics.Reusing_resolution_of_module_0_to_file_1_from_old_program, moduleName, containingFile); } - } - else { - result[i] = resolutions[j]; - j++; + (result || (result = new Array(moduleNames.length)))[i] = oldResolvedModule; + (reusedNames || (reusedNames = [])).push(moduleName); + continue; } } - Debug.assert(j === resolutions.length); - - return result; - - // If we change our policy of rechecking failed lookups on each program create, - // we should adjust the value returned here. - function moduleNameResolvesToAmbientModuleInNonModifiedFile(moduleName: string): boolean { - const resolutionToFile = getResolvedModule(oldSourceFile, moduleName); - const resolvedFile = resolutionToFile && oldProgram!.getSourceFile(resolutionToFile.resolvedFileName); - if (resolutionToFile && resolvedFile) { - // In the old program, we resolved to an ambient module that was in the same - // place as we expected to find an actual module file. - // We actually need to return 'false' here even though this seems like a 'true' case - // because the normal module resolution algorithm will find this anyway. - return false; - } - - // at least one of declarations should come from non-modified source file - const unmodifiedFile = ambientModuleNameToUnmodifiedFileName.get(moduleName); - - if (!unmodifiedFile) { - return false; - } - + // We know moduleName resolves to an ambient module provided that moduleName: + // - is in the list of ambient modules locally declared in the current source file. + // - resolved to an ambient module in the old program whose declaration is in an unmodified file + // (so the same module declaration will land in the new program) + let resolvesToAmbientModuleInNonModifiedFile = false; + if (contains(file.ambientModuleNames, moduleName)) { + resolvesToAmbientModuleInNonModifiedFile = true; if (isTraceEnabled(options, host)) { - trace(host, Diagnostics.Module_0_was_resolved_as_ambient_module_declared_in_1_since_this_file_was_not_modified, moduleName, unmodifiedFile); + trace(host, Diagnostics.Module_0_was_resolved_as_locally_declared_ambient_module_in_file_1, moduleName, containingFile); } - return true; + } + else { + resolvesToAmbientModuleInNonModifiedFile = moduleNameResolvesToAmbientModuleInNonModifiedFile(moduleName); + } + if (resolvesToAmbientModuleInNonModifiedFile) { + (result || (result = new Array(moduleNames.length)))[i] = predictedToResolveToAmbientModuleMarker; + } + else { + // Resolution failed in the old program, or resolved to an ambient module for which we can't reuse the result. + (unknownModuleNames || (unknownModuleNames = [])).push(moduleName); } } - - function canReuseProjectReferences(): boolean { - return !forEachProjectReference( - oldProgram!.getProjectReferences(), - oldProgram!.getResolvedProjectReferences(), - (oldResolvedRef, index, parent) => { - const newRef = (parent ? parent.commandLine.projectReferences : projectReferences)![index]; - const newResolvedRef = parseProjectReferenceConfigFile(newRef); - if (oldResolvedRef) { - // Resolved project reference has gone missing or changed - return !newResolvedRef || newResolvedRef.sourceFile !== oldResolvedRef.sourceFile; - } - else { - // A previously-unresolved reference may be resolved now - return newResolvedRef !== undefined; - } - }, - (oldProjectReferences, parent) => { - // If array of references is changed, we cant resue old program - const newReferences = parent ? getResolvedProjectReferenceByPath(parent.sourceFile.path)!.commandLine.projectReferences : projectReferences; - return !arrayIsEqualTo(oldProjectReferences, newReferences, projectReferenceIsEqualTo); - } - ); - } - - function tryReuseStructureFromOldProgram(): StructureIsReused { - if (!oldProgram) { - return StructureIsReused.Not; - } - - // check properties that can affect structure of the program or module resolution strategy - // if any of these properties has changed - structure cannot be reused - const oldOptions = oldProgram.getCompilerOptions(); - if (changesAffectModuleResolution(oldOptions, options)) { - return oldProgram.structureIsReused = StructureIsReused.Not; + const resolutions = unknownModuleNames && unknownModuleNames.length + ? resolveModuleNamesWorker(unknownModuleNames, containingFile, reusedNames, getResolvedProjectReferenceToRedirect(file.originalFileName)) + : emptyArray; + // Combine results of resolutions and predicted results + if (!result) { + // There were no unresolved/ambient resolutions. + Debug.assert(resolutions.length === moduleNames.length); + return resolutions; + } + let j = 0; + for (let i = 0; i < result.length; i++) { + if (result[i]) { + // `result[i]` is either a `ResolvedModuleFull` or a marker. + // If it is the former, we can leave it as is. + if (result[i] === predictedToResolveToAmbientModuleMarker) { + result[i] = undefined!; // TODO: GH#18217 + } } - - Debug.assert(!(oldProgram.structureIsReused! & (StructureIsReused.Completely | StructureIsReused.SafeModules))); - - // there is an old program, check if we can reuse its structure - const oldRootNames = oldProgram.getRootFileNames(); - if (!arrayIsEqualTo(oldRootNames, rootNames)) { - return oldProgram.structureIsReused = StructureIsReused.Not; + else { + result[i] = resolutions[j]; + j++; } - - if (!arrayIsEqualTo(options.types, oldOptions.types)) { - return oldProgram.structureIsReused = StructureIsReused.Not; + } + Debug.assert(j === resolutions.length); + return result; + // If we change our policy of rechecking failed lookups on each program create, + // we should adjust the value returned here. + function moduleNameResolvesToAmbientModuleInNonModifiedFile(moduleName: string): boolean { + const resolutionToFile = getResolvedModule(oldSourceFile, moduleName); + const resolvedFile = resolutionToFile && oldProgram!.getSourceFile(resolutionToFile.resolvedFileName); + if (resolutionToFile && resolvedFile) { + // In the old program, we resolved to an ambient module that was in the same + // place as we expected to find an actual module file. + // We actually need to return 'false' here even though this seems like a 'true' case + // because the normal module resolution algorithm will find this anyway. + return false; } - - // Check if any referenced project tsconfig files are different - if (!canReuseProjectReferences()) { - return oldProgram.structureIsReused = StructureIsReused.Not; + // at least one of declarations should come from non-modified source file + const unmodifiedFile = ambientModuleNameToUnmodifiedFileName.get(moduleName); + if (!unmodifiedFile) { + return false; } - if (projectReferences) { - resolvedProjectReferences = projectReferences.map(parseProjectReferenceConfigFile); - if (host.setResolvedProjectReferenceCallbacks) { - host.setResolvedProjectReferenceCallbacks({ - getSourceOfProjectReferenceRedirect, - forEachResolvedProjectReference - }); - } - } - - // check if program source files has changed in the way that can affect structure of the program - const newSourceFiles: SourceFile[] = []; - const modifiedSourceFiles: { oldFile: SourceFile, newFile: SourceFile }[] = []; - oldProgram.structureIsReused = StructureIsReused.Completely; - - // If the missing file paths are now present, it can change the progam structure, - // and hence cant reuse the structure. - // This is same as how we dont reuse the structure if one of the file from old program is now missing - if (oldProgram.getMissingFilePaths().some(missingFilePath => host.fileExists(missingFilePath))) { + if (isTraceEnabled(options, host)) { + trace(host, Diagnostics.Module_0_was_resolved_as_ambient_module_declared_in_1_since_this_file_was_not_modified, moduleName, unmodifiedFile); + } + return true; + } + } + function canReuseProjectReferences(): boolean { + return !forEachProjectReference(oldProgram!.getProjectReferences(), oldProgram!.getResolvedProjectReferences(), (oldResolvedRef, index, parent) => { + const newRef = (parent ? parent.commandLine.projectReferences : projectReferences)![index]; + const newResolvedRef = parseProjectReferenceConfigFile(newRef); + if (oldResolvedRef) { + // Resolved project reference has gone missing or changed + return !newResolvedRef || newResolvedRef.sourceFile !== oldResolvedRef.sourceFile; + } + else { + // A previously-unresolved reference may be resolved now + return newResolvedRef !== undefined; + } + }, (oldProjectReferences, parent) => { + // If array of references is changed, we cant resue old program + const newReferences = parent ? getResolvedProjectReferenceByPath(parent.sourceFile.path)!.commandLine.projectReferences : projectReferences; + return !arrayIsEqualTo(oldProjectReferences, newReferences, projectReferenceIsEqualTo); + }); + } + function tryReuseStructureFromOldProgram(): StructureIsReused { + if (!oldProgram) { + return StructureIsReused.Not; + } + // check properties that can affect structure of the program or module resolution strategy + // if any of these properties has changed - structure cannot be reused + const oldOptions = oldProgram.getCompilerOptions(); + if (changesAffectModuleResolution(oldOptions, options)) { + return oldProgram.structureIsReused = StructureIsReused.Not; + } + Debug.assert(!((oldProgram.structureIsReused!) & (StructureIsReused.Completely | StructureIsReused.SafeModules))); + // there is an old program, check if we can reuse its structure + const oldRootNames = oldProgram.getRootFileNames(); + if (!arrayIsEqualTo(oldRootNames, rootNames)) { + return oldProgram.structureIsReused = StructureIsReused.Not; + } + if (!arrayIsEqualTo(options.types, oldOptions.types)) { + return oldProgram.structureIsReused = StructureIsReused.Not; + } + // Check if any referenced project tsconfig files are different + if (!canReuseProjectReferences()) { + return oldProgram.structureIsReused = StructureIsReused.Not; + } + if (projectReferences) { + resolvedProjectReferences = projectReferences.map(parseProjectReferenceConfigFile); + if (host.setResolvedProjectReferenceCallbacks) { + host.setResolvedProjectReferenceCallbacks({ + getSourceOfProjectReferenceRedirect, + forEachResolvedProjectReference + }); + } + } + // check if program source files has changed in the way that can affect structure of the program + const newSourceFiles: SourceFile[] = []; + const modifiedSourceFiles: { + oldFile: SourceFile; + newFile: SourceFile; + }[] = []; + oldProgram.structureIsReused = StructureIsReused.Completely; + // If the missing file paths are now present, it can change the progam structure, + // and hence cant reuse the structure. + // This is same as how we dont reuse the structure if one of the file from old program is now missing + if (oldProgram.getMissingFilePaths().some(missingFilePath => host.fileExists(missingFilePath))) { + return oldProgram.structureIsReused = StructureIsReused.Not; + } + const oldSourceFiles = oldProgram.getSourceFiles(); + const enum SeenPackageName { + Exists, + Modified + } + const seenPackageNames = createMap(); + for (const oldSourceFile of oldSourceFiles) { + let newSourceFile = host.getSourceFileByPath + ? host.getSourceFileByPath(oldSourceFile.fileName, oldSourceFile.resolvedPath, options.target!, /*onError*/ undefined, shouldCreateNewSourceFile) + : host.getSourceFile(oldSourceFile.fileName, options.target!, /*onError*/ undefined, shouldCreateNewSourceFile); // TODO: GH#18217 + if (!newSourceFile) { return oldProgram.structureIsReused = StructureIsReused.Not; } - - const oldSourceFiles = oldProgram.getSourceFiles(); - const enum SeenPackageName { Exists, Modified } - const seenPackageNames = createMap(); - - for (const oldSourceFile of oldSourceFiles) { - let newSourceFile = host.getSourceFileByPath - ? host.getSourceFileByPath(oldSourceFile.fileName, oldSourceFile.resolvedPath, options.target!, /*onError*/ undefined, shouldCreateNewSourceFile) - : host.getSourceFile(oldSourceFile.fileName, options.target!, /*onError*/ undefined, shouldCreateNewSourceFile); // TODO: GH#18217 - - if (!newSourceFile) { + Debug.assert(!newSourceFile.redirectInfo, "Host should not return a redirect source file from `getSourceFile`"); + let fileChanged: boolean; + if (oldSourceFile.redirectInfo) { + // We got `newSourceFile` by path, so it is actually for the unredirected file. + // This lets us know if the unredirected file has changed. If it has we should break the redirect. + if (newSourceFile !== oldSourceFile.redirectInfo.unredirected) { + // Underlying file has changed. Might not redirect anymore. Must rebuild program. return oldProgram.structureIsReused = StructureIsReused.Not; } - - Debug.assert(!newSourceFile.redirectInfo, "Host should not return a redirect source file from `getSourceFile`"); - - let fileChanged: boolean; - if (oldSourceFile.redirectInfo) { - // We got `newSourceFile` by path, so it is actually for the unredirected file. - // This lets us know if the unredirected file has changed. If it has we should break the redirect. - if (newSourceFile !== oldSourceFile.redirectInfo.unredirected) { - // Underlying file has changed. Might not redirect anymore. Must rebuild program. - return oldProgram.structureIsReused = StructureIsReused.Not; - } - fileChanged = false; - newSourceFile = oldSourceFile; // Use the redirect. + fileChanged = false; + newSourceFile = oldSourceFile; // Use the redirect. + } + else if (oldProgram.redirectTargetsMap.has(oldSourceFile.path)) { + // If a redirected-to source file changes, the redirect may be broken. + if (newSourceFile !== oldSourceFile) { + return oldProgram.structureIsReused = StructureIsReused.Not; } - else if (oldProgram.redirectTargetsMap.has(oldSourceFile.path)) { - // If a redirected-to source file changes, the redirect may be broken. - if (newSourceFile !== oldSourceFile) { - return oldProgram.structureIsReused = StructureIsReused.Not; - } - fileChanged = false; + fileChanged = false; + } + else { + fileChanged = newSourceFile !== oldSourceFile; + } + // Since the project references havent changed, its right to set originalFileName and resolvedPath here + newSourceFile.path = oldSourceFile.path; + newSourceFile.originalFileName = oldSourceFile.originalFileName; + newSourceFile.resolvedPath = oldSourceFile.resolvedPath; + newSourceFile.fileName = oldSourceFile.fileName; + const packageName = oldProgram.sourceFileToPackageName.get(oldSourceFile.path); + if (packageName !== undefined) { + // If there are 2 different source files for the same package name and at least one of them changes, + // they might become redirects. So we must rebuild the program. + const prevKind = seenPackageNames.get(packageName); + const newKind = fileChanged ? SeenPackageName.Modified : SeenPackageName.Exists; + if ((prevKind !== undefined && newKind === SeenPackageName.Modified) || prevKind === SeenPackageName.Modified) { + return oldProgram.structureIsReused = StructureIsReused.Not; } - else { - fileChanged = newSourceFile !== oldSourceFile; - } - - // Since the project references havent changed, its right to set originalFileName and resolvedPath here - newSourceFile.path = oldSourceFile.path; - newSourceFile.originalFileName = oldSourceFile.originalFileName; - newSourceFile.resolvedPath = oldSourceFile.resolvedPath; - newSourceFile.fileName = oldSourceFile.fileName; - - const packageName = oldProgram.sourceFileToPackageName.get(oldSourceFile.path); - if (packageName !== undefined) { - // If there are 2 different source files for the same package name and at least one of them changes, - // they might become redirects. So we must rebuild the program. - const prevKind = seenPackageNames.get(packageName); - const newKind = fileChanged ? SeenPackageName.Modified : SeenPackageName.Exists; - if ((prevKind !== undefined && newKind === SeenPackageName.Modified) || prevKind === SeenPackageName.Modified) { - return oldProgram.structureIsReused = StructureIsReused.Not; - } - seenPackageNames.set(packageName, newKind); - } - - if (fileChanged) { - // The `newSourceFile` object was created for the new program. - - if (!arrayIsEqualTo(oldSourceFile.libReferenceDirectives, newSourceFile.libReferenceDirectives, fileReferenceIsEqualTo)) { - // 'lib' references has changed. Matches behavior in changesAffectModuleResolution - return oldProgram.structureIsReused = StructureIsReused.Not; - } - - if (oldSourceFile.hasNoDefaultLib !== newSourceFile.hasNoDefaultLib) { - // value of no-default-lib has changed - // this will affect if default library is injected into the list of files - oldProgram.structureIsReused = StructureIsReused.SafeModules; - } - - // check tripleslash references - if (!arrayIsEqualTo(oldSourceFile.referencedFiles, newSourceFile.referencedFiles, fileReferenceIsEqualTo)) { - // tripleslash references has changed - oldProgram.structureIsReused = StructureIsReused.SafeModules; - } - - // check imports and module augmentations - collectExternalModuleReferences(newSourceFile); - if (!arrayIsEqualTo(oldSourceFile.imports, newSourceFile.imports, moduleNameIsEqualTo)) { - // imports has changed - oldProgram.structureIsReused = StructureIsReused.SafeModules; - } - if (!arrayIsEqualTo(oldSourceFile.moduleAugmentations, newSourceFile.moduleAugmentations, moduleNameIsEqualTo)) { - // moduleAugmentations has changed - oldProgram.structureIsReused = StructureIsReused.SafeModules; - } - if ((oldSourceFile.flags & NodeFlags.PermanentlySetIncrementalFlags) !== (newSourceFile.flags & NodeFlags.PermanentlySetIncrementalFlags)) { - // dynamicImport has changed - oldProgram.structureIsReused = StructureIsReused.SafeModules; - } - - if (!arrayIsEqualTo(oldSourceFile.typeReferenceDirectives, newSourceFile.typeReferenceDirectives, fileReferenceIsEqualTo)) { - // 'types' references has changed - oldProgram.structureIsReused = StructureIsReused.SafeModules; - } - - // tentatively approve the file - modifiedSourceFiles.push({ oldFile: oldSourceFile, newFile: newSourceFile }); + seenPackageNames.set(packageName, newKind); + } + if (fileChanged) { + // The `newSourceFile` object was created for the new program. + if (!arrayIsEqualTo(oldSourceFile.libReferenceDirectives, newSourceFile.libReferenceDirectives, fileReferenceIsEqualTo)) { + // 'lib' references has changed. Matches behavior in changesAffectModuleResolution + return oldProgram.structureIsReused = StructureIsReused.Not; } - else if (hasInvalidatedResolution(oldSourceFile.path)) { - // 'module/types' references could have changed + if (oldSourceFile.hasNoDefaultLib !== newSourceFile.hasNoDefaultLib) { + // value of no-default-lib has changed + // this will affect if default library is injected into the list of files + oldProgram.structureIsReused = StructureIsReused.SafeModules; + } + // check tripleslash references + if (!arrayIsEqualTo(oldSourceFile.referencedFiles, newSourceFile.referencedFiles, fileReferenceIsEqualTo)) { + // tripleslash references has changed + oldProgram.structureIsReused = StructureIsReused.SafeModules; + } + // check imports and module augmentations + collectExternalModuleReferences(newSourceFile); + if (!arrayIsEqualTo(oldSourceFile.imports, newSourceFile.imports, moduleNameIsEqualTo)) { + // imports has changed oldProgram.structureIsReused = StructureIsReused.SafeModules; - - // add file to the modified list so that we will resolve it later - modifiedSourceFiles.push({ oldFile: oldSourceFile, newFile: newSourceFile }); - } - - // if file has passed all checks it should be safe to reuse it - newSourceFiles.push(newSourceFile); - } - - if (oldProgram.structureIsReused !== StructureIsReused.Completely) { - return oldProgram.structureIsReused; - } - - const modifiedFiles = modifiedSourceFiles.map(f => f.oldFile); - for (const oldFile of oldSourceFiles) { - if (!contains(modifiedFiles, oldFile)) { - for (const moduleName of oldFile.ambientModuleNames) { - ambientModuleNameToUnmodifiedFileName.set(moduleName, oldFile.fileName); - } } + if (!arrayIsEqualTo(oldSourceFile.moduleAugmentations, newSourceFile.moduleAugmentations, moduleNameIsEqualTo)) { + // moduleAugmentations has changed + oldProgram.structureIsReused = StructureIsReused.SafeModules; + } + if ((oldSourceFile.flags & NodeFlags.PermanentlySetIncrementalFlags) !== (newSourceFile.flags & NodeFlags.PermanentlySetIncrementalFlags)) { + // dynamicImport has changed + oldProgram.structureIsReused = StructureIsReused.SafeModules; + } + if (!arrayIsEqualTo(oldSourceFile.typeReferenceDirectives, newSourceFile.typeReferenceDirectives, fileReferenceIsEqualTo)) { + // 'types' references has changed + oldProgram.structureIsReused = StructureIsReused.SafeModules; + } + // tentatively approve the file + modifiedSourceFiles.push({ oldFile: oldSourceFile, newFile: newSourceFile }); + } + else if (hasInvalidatedResolution(oldSourceFile.path)) { + // 'module/types' references could have changed + oldProgram.structureIsReused = StructureIsReused.SafeModules; + // add file to the modified list so that we will resolve it later + modifiedSourceFiles.push({ oldFile: oldSourceFile, newFile: newSourceFile }); + } + // if file has passed all checks it should be safe to reuse it + newSourceFiles.push(newSourceFile); + } + if (oldProgram.structureIsReused !== StructureIsReused.Completely) { + return oldProgram.structureIsReused; + } + const modifiedFiles = modifiedSourceFiles.map(f => f.oldFile); + for (const oldFile of oldSourceFiles) { + if (!contains(modifiedFiles, oldFile)) { + for (const moduleName of oldFile.ambientModuleNames) { + ambientModuleNameToUnmodifiedFileName.set(moduleName, oldFile.fileName); + } + } + } + // try to verify results of module resolution + for (const { oldFile: oldSourceFile, newFile: newSourceFile } of modifiedSourceFiles) { + const newSourceFilePath = getNormalizedAbsolutePath(newSourceFile.originalFileName, currentDirectory); + const moduleNames = getModuleNames(newSourceFile); + const resolutions = resolveModuleNamesReusingOldState(moduleNames, newSourceFilePath, newSourceFile); + // ensure that module resolution results are still correct + const resolutionsChanged = hasChangesInResolutions(moduleNames, resolutions, oldSourceFile.resolvedModules, moduleResolutionIsEqualTo); + if (resolutionsChanged) { + oldProgram.structureIsReused = StructureIsReused.SafeModules; + newSourceFile.resolvedModules = zipToMap(moduleNames, resolutions); } - // try to verify results of module resolution - for (const { oldFile: oldSourceFile, newFile: newSourceFile } of modifiedSourceFiles) { - const newSourceFilePath = getNormalizedAbsolutePath(newSourceFile.originalFileName, currentDirectory); - const moduleNames = getModuleNames(newSourceFile); - const resolutions = resolveModuleNamesReusingOldState(moduleNames, newSourceFilePath, newSourceFile); - // ensure that module resolution results are still correct - const resolutionsChanged = hasChangesInResolutions(moduleNames, resolutions, oldSourceFile.resolvedModules, moduleResolutionIsEqualTo); + else { + newSourceFile.resolvedModules = oldSourceFile.resolvedModules; + } + if (resolveTypeReferenceDirectiveNamesWorker) { + // We lower-case all type references because npm automatically lowercases all packages. See GH#9824. + const typesReferenceDirectives = map(newSourceFile.typeReferenceDirectives, ref => toFileNameLowerCase(ref.fileName)); + const resolutions = resolveTypeReferenceDirectiveNamesWorker(typesReferenceDirectives, newSourceFilePath, getResolvedProjectReferenceToRedirect(newSourceFile.originalFileName)); + // ensure that types resolutions are still correct + const resolutionsChanged = hasChangesInResolutions(typesReferenceDirectives, resolutions, oldSourceFile.resolvedTypeReferenceDirectiveNames, typeDirectiveIsEqualTo); if (resolutionsChanged) { oldProgram.structureIsReused = StructureIsReused.SafeModules; - newSourceFile.resolvedModules = zipToMap(moduleNames, resolutions); + newSourceFile.resolvedTypeReferenceDirectiveNames = zipToMap(typesReferenceDirectives, resolutions); } else { - newSourceFile.resolvedModules = oldSourceFile.resolvedModules; - } - if (resolveTypeReferenceDirectiveNamesWorker) { - // We lower-case all type references because npm automatically lowercases all packages. See GH#9824. - const typesReferenceDirectives = map(newSourceFile.typeReferenceDirectives, ref => toFileNameLowerCase(ref.fileName)); - const resolutions = resolveTypeReferenceDirectiveNamesWorker(typesReferenceDirectives, newSourceFilePath, getResolvedProjectReferenceToRedirect(newSourceFile.originalFileName)); - // ensure that types resolutions are still correct - const resolutionsChanged = hasChangesInResolutions(typesReferenceDirectives, resolutions, oldSourceFile.resolvedTypeReferenceDirectiveNames, typeDirectiveIsEqualTo); - if (resolutionsChanged) { - oldProgram.structureIsReused = StructureIsReused.SafeModules; - newSourceFile.resolvedTypeReferenceDirectiveNames = zipToMap(typesReferenceDirectives, resolutions); - } - else { - newSourceFile.resolvedTypeReferenceDirectiveNames = oldSourceFile.resolvedTypeReferenceDirectiveNames; - } + newSourceFile.resolvedTypeReferenceDirectiveNames = oldSourceFile.resolvedTypeReferenceDirectiveNames; } } - - if (oldProgram.structureIsReused !== StructureIsReused.Completely) { - return oldProgram.structureIsReused; - } - - if (host.hasChangedAutomaticTypeDirectiveNames) { - return oldProgram.structureIsReused = StructureIsReused.SafeModules; - } - - missingFilePaths = oldProgram.getMissingFilePaths(); - refFileMap = oldProgram.getRefFileMap(); - - // update fileName -> file mapping - Debug.assert(newSourceFiles.length === oldProgram.getSourceFiles().length); - for (const newSourceFile of newSourceFiles) { - filesByName.set(newSourceFile.path, newSourceFile); - } - const oldFilesByNameMap = oldProgram.getFilesByNameMap(); - oldFilesByNameMap.forEach((oldFile, path) => { - if (!oldFile) { - filesByName.set(path, oldFile); - return; - } - if (oldFile.path === path) { - // Set the file as found during node modules search if it was found that way in old progra, - if (oldProgram!.isSourceFileFromExternalLibrary(oldFile)) { - sourceFilesFoundSearchingNodeModules.set(oldFile.path, true); + } + if (oldProgram.structureIsReused !== StructureIsReused.Completely) { + return oldProgram.structureIsReused; + } + if (host.hasChangedAutomaticTypeDirectiveNames) { + return oldProgram.structureIsReused = StructureIsReused.SafeModules; + } + missingFilePaths = oldProgram.getMissingFilePaths(); + refFileMap = oldProgram.getRefFileMap(); + // update fileName -> file mapping + Debug.assert(newSourceFiles.length === oldProgram.getSourceFiles().length); + for (const newSourceFile of newSourceFiles) { + filesByName.set(newSourceFile.path, newSourceFile); + } + const oldFilesByNameMap = oldProgram.getFilesByNameMap(); + oldFilesByNameMap.forEach((oldFile, path) => { + if (!oldFile) { + filesByName.set(path, oldFile); + return; + } + if (oldFile.path === path) { + // Set the file as found during node modules search if it was found that way in old progra, + if (oldProgram!.isSourceFileFromExternalLibrary(oldFile)) { + sourceFilesFoundSearchingNodeModules.set(oldFile.path, true); + } + return; + } + filesByName.set(path, filesByName.get(oldFile.path)); + }); + files = newSourceFiles; + fileProcessingDiagnostics = oldProgram.getFileProcessingDiagnostics(); + for (const modifiedFile of modifiedSourceFiles) { + fileProcessingDiagnostics.reattachFileDiagnostics(modifiedFile.newFile); + } + resolvedTypeReferenceDirectives = oldProgram.getResolvedTypeReferenceDirectives(); + sourceFileToPackageName = oldProgram.sourceFileToPackageName; + redirectTargetsMap = oldProgram.redirectTargetsMap; + return oldProgram.structureIsReused = StructureIsReused.Completely; + } + function getEmitHost(writeFileCallback?: WriteFileCallback): EmitHost { + return { + getPrependNodes, + getCanonicalFileName, + getCommonSourceDirectory: program.getCommonSourceDirectory, + getCompilerOptions: program.getCompilerOptions, + getCurrentDirectory: () => currentDirectory, + getNewLine: () => host.getNewLine(), + getSourceFile: program.getSourceFile, + getSourceFileByPath: program.getSourceFileByPath, + getSourceFiles: program.getSourceFiles, + getLibFileFromReference: program.getLibFileFromReference, + isSourceFileFromExternalLibrary, + getResolvedProjectReferenceToRedirect, + isSourceOfProjectReferenceRedirect, + getProbableSymlinks, + writeFile: writeFileCallback || ((fileName, data, writeByteOrderMark, onError, sourceFiles) => host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles)), + isEmitBlocked, + readFile: f => host.readFile(f), + fileExists: f => { + // Use local caches + const path = toPath(f); + if (getSourceFileByPath(path)) + return true; + if (contains(missingFilePaths, path)) + return false; + // Before falling back to the host + return host.fileExists(f); + }, + ...(host.directoryExists ? { directoryExists: f => host.directoryExists!(f) } : {}), + useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames(), + getProgramBuildInfo: () => program.getProgramBuildInfo && program.getProgramBuildInfo(), + getSourceFileFromReference: (file, ref) => program.getSourceFileFromReference(file, ref), + redirectTargetsMap, + }; + } + function emitBuildInfo(writeFileCallback?: WriteFileCallback): EmitResult { + Debug.assert(!options.out && !options.outFile); + mark("beforeEmit"); + const emitResult = emitFiles(notImplementedResolver, getEmitHost(writeFileCallback), + /*targetSourceFile*/ undefined, noTransformers, + /*emitOnlyDtsFiles*/ false, + /*onlyBuildInfo*/ true); + mark("afterEmit"); + measure("Emit", "beforeEmit", "afterEmit"); + return emitResult; + } + function getResolvedProjectReferences() { + return resolvedProjectReferences; + } + function getProjectReferences() { + return projectReferences; + } + function getPrependNodes() { + return createPrependNodes(projectReferences, (_ref, index) => resolvedProjectReferences![index]!.commandLine, fileName => { + const path = toPath(fileName); + const sourceFile = getSourceFileByPath(path); + return sourceFile ? sourceFile.text : filesByName.has(path) ? undefined : host.readFile(path); + }); + } + function isSourceFileFromExternalLibrary(file: SourceFile): boolean { + return !!sourceFilesFoundSearchingNodeModules.get(file.path); + } + function isSourceFileDefaultLibrary(file: SourceFile): boolean { + if (file.hasNoDefaultLib) { + return true; + } + if (!options.noLib) { + return false; + } + // If '--lib' is not specified, include default library file according to '--target' + // otherwise, using options specified in '--lib' instead of '--target' default library file + const equalityComparer = host.useCaseSensitiveFileNames() ? equateStringsCaseSensitive : equateStringsCaseInsensitive; + if (!options.lib) { + return equalityComparer(file.fileName, getDefaultLibraryFileName()); + } + else { + return some(options.lib, libFileName => equalityComparer(file.fileName, combinePaths(defaultLibraryPath, libFileName))); + } + } + function getDiagnosticsProducingTypeChecker() { + return diagnosticsProducingTypeChecker || (diagnosticsProducingTypeChecker = createTypeChecker(program, /*produceDiagnostics:*/ true)); + } + function dropDiagnosticsProducingTypeChecker() { + diagnosticsProducingTypeChecker = undefined!; + } + function getTypeChecker() { + return noDiagnosticsTypeChecker || (noDiagnosticsTypeChecker = createTypeChecker(program, /*produceDiagnostics:*/ false)); + } + function emit(sourceFile?: SourceFile, writeFileCallback?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, transformers?: CustomTransformers, forceDtsEmit?: boolean): EmitResult { + return runWithCancellationToken(() => emitWorker(program, sourceFile, writeFileCallback, cancellationToken, emitOnlyDtsFiles, transformers, forceDtsEmit)); + } + function isEmitBlocked(emitFileName: string): boolean { + return hasEmitBlockingDiagnostics.has(toPath(emitFileName)); + } + function emitWorker(program: Program, sourceFile: SourceFile | undefined, writeFileCallback: WriteFileCallback | undefined, cancellationToken: CancellationToken | undefined, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers, forceDtsEmit?: boolean): EmitResult { + if (!forceDtsEmit) { + const result = handleNoEmitOptions(program, sourceFile, cancellationToken); + if (result) + return result; + } + // Create the emit resolver outside of the "emitTime" tracking code below. That way + // any cost associated with it (like type checking) are appropriate associated with + // the type-checking counter. + // + // If the -out option is specified, we should not pass the source file to getEmitResolver. + // This is because in the -out scenario all files need to be emitted, and therefore all + // files need to be type checked. And the way to specify that all files need to be type + // checked is to not pass the file to getEmitResolver. + const emitResolver = getDiagnosticsProducingTypeChecker().getEmitResolver((options.outFile || options.out) ? undefined : sourceFile, cancellationToken); + mark("beforeEmit"); + const emitResult = emitFiles(emitResolver, getEmitHost(writeFileCallback), sourceFile, getTransformers(options, customTransformers, emitOnlyDtsFiles), emitOnlyDtsFiles, + /*onlyBuildInfo*/ false, forceDtsEmit); + mark("afterEmit"); + measure("Emit", "beforeEmit", "afterEmit"); + return emitResult; + } + function getSourceFile(fileName: string): SourceFile | undefined { + return getSourceFileByPath(toPath(fileName)); + } + function getSourceFileByPath(path: Path): SourceFile | undefined { + return filesByName.get(path) || undefined; + } + function getDiagnosticsHelper(sourceFile: SourceFile | undefined, getDiagnostics: (sourceFile: SourceFile, cancellationToken: CancellationToken | undefined) => readonly T[], cancellationToken: CancellationToken | undefined): readonly T[] { + if (sourceFile) { + return getDiagnostics(sourceFile, cancellationToken); + } + return sortAndDeduplicateDiagnostics(flatMap(program.getSourceFiles(), sourceFile => { + if (cancellationToken) { + cancellationToken.throwIfCancellationRequested(); + } + return getDiagnostics(sourceFile, cancellationToken); + })); + } + function getSyntacticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly DiagnosticWithLocation[] { + return getDiagnosticsHelper(sourceFile, getSyntacticDiagnosticsForFile, cancellationToken); + } + function getSemanticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[] { + return getDiagnosticsHelper(sourceFile, getSemanticDiagnosticsForFile, cancellationToken); + } + function getBindAndCheckDiagnostics(sourceFile: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[] { + return getBindAndCheckDiagnosticsForFile(sourceFile, cancellationToken); + } + function getProgramDiagnostics(sourceFile: SourceFile): readonly Diagnostic[] { + if (skipTypeChecking(sourceFile, options, program)) { + return emptyArray; + } + const fileProcessingDiagnosticsInFile = fileProcessingDiagnostics.getDiagnostics(sourceFile.fileName); + const programDiagnosticsInFile = programDiagnostics.getDiagnostics(sourceFile.fileName); + let diagnostics: Diagnostic[] | undefined; + for (const diags of [fileProcessingDiagnosticsInFile, programDiagnosticsInFile]) { + if (diags) { + for (const diag of diags) { + if (shouldReportDiagnostic(diag)) { + diagnostics = append(diagnostics, diag); } - return; } - filesByName.set(path, filesByName.get(oldFile.path)); - }); - - files = newSourceFiles; - fileProcessingDiagnostics = oldProgram.getFileProcessingDiagnostics(); - - for (const modifiedFile of modifiedSourceFiles) { - fileProcessingDiagnostics.reattachFileDiagnostics(modifiedFile.newFile); - } - resolvedTypeReferenceDirectives = oldProgram.getResolvedTypeReferenceDirectives(); - - sourceFileToPackageName = oldProgram.sourceFileToPackageName; - redirectTargetsMap = oldProgram.redirectTargetsMap; - - return oldProgram.structureIsReused = StructureIsReused.Completely; - } - - function getEmitHost(writeFileCallback?: WriteFileCallback): EmitHost { - return { - getPrependNodes, - getCanonicalFileName, - getCommonSourceDirectory: program.getCommonSourceDirectory, - getCompilerOptions: program.getCompilerOptions, - getCurrentDirectory: () => currentDirectory, - getNewLine: () => host.getNewLine(), - getSourceFile: program.getSourceFile, - getSourceFileByPath: program.getSourceFileByPath, - getSourceFiles: program.getSourceFiles, - getLibFileFromReference: program.getLibFileFromReference, - isSourceFileFromExternalLibrary, - getResolvedProjectReferenceToRedirect, - isSourceOfProjectReferenceRedirect, - getProbableSymlinks, - writeFile: writeFileCallback || ( - (fileName, data, writeByteOrderMark, onError, sourceFiles) => host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles)), - isEmitBlocked, - readFile: f => host.readFile(f), - fileExists: f => { - // Use local caches - const path = toPath(f); - if (getSourceFileByPath(path)) return true; - if (contains(missingFilePaths, path)) return false; - // Before falling back to the host - return host.fileExists(f); - }, - ...(host.directoryExists ? { directoryExists: f => host.directoryExists!(f) } : {}), - useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames(), - getProgramBuildInfo: () => program.getProgramBuildInfo && program.getProgramBuildInfo(), - getSourceFileFromReference: (file, ref) => program.getSourceFileFromReference(file, ref), - redirectTargetsMap, - }; - } - - function emitBuildInfo(writeFileCallback?: WriteFileCallback): EmitResult { - Debug.assert(!options.out && !options.outFile); - performance.mark("beforeEmit"); - const emitResult = emitFiles( - notImplementedResolver, - getEmitHost(writeFileCallback), - /*targetSourceFile*/ undefined, - /*transformers*/ noTransformers, - /*emitOnlyDtsFiles*/ false, - /*onlyBuildInfo*/ true - ); - - performance.mark("afterEmit"); - performance.measure("Emit", "beforeEmit", "afterEmit"); - return emitResult; - } - - function getResolvedProjectReferences() { - return resolvedProjectReferences; - } - - function getProjectReferences() { - return projectReferences; - } - - function getPrependNodes() { - return createPrependNodes( - projectReferences, - (_ref, index) => resolvedProjectReferences![index]!.commandLine, - fileName => { - const path = toPath(fileName); - const sourceFile = getSourceFileByPath(path); - return sourceFile ? sourceFile.text : filesByName.has(path) ? undefined : host.readFile(path); - } - ); - } - - function isSourceFileFromExternalLibrary(file: SourceFile): boolean { - return !!sourceFilesFoundSearchingNodeModules.get(file.path); - } - - function isSourceFileDefaultLibrary(file: SourceFile): boolean { - if (file.hasNoDefaultLib) { - return true; - } - - if (!options.noLib) { - return false; - } - - // If '--lib' is not specified, include default library file according to '--target' - // otherwise, using options specified in '--lib' instead of '--target' default library file - const equalityComparer = host.useCaseSensitiveFileNames() ? equateStringsCaseSensitive : equateStringsCaseInsensitive; - if (!options.lib) { - return equalityComparer(file.fileName, getDefaultLibraryFileName()); - } - else { - return some(options.lib, libFileName => equalityComparer(file.fileName, combinePaths(defaultLibraryPath, libFileName))); } } - - function getDiagnosticsProducingTypeChecker() { - return diagnosticsProducingTypeChecker || (diagnosticsProducingTypeChecker = createTypeChecker(program, /*produceDiagnostics:*/ true)); - } - - function dropDiagnosticsProducingTypeChecker() { - diagnosticsProducingTypeChecker = undefined!; - } - - function getTypeChecker() { - return noDiagnosticsTypeChecker || (noDiagnosticsTypeChecker = createTypeChecker(program, /*produceDiagnostics:*/ false)); + return diagnostics || emptyArray; + } + function getDeclarationDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly DiagnosticWithLocation[] { + const options = program.getCompilerOptions(); + // collect diagnostics from the program only once if either no source file was specified or out/outFile is set (bundled emit) + if (!sourceFile || options.out || options.outFile) { + return getDeclarationDiagnosticsWorker(sourceFile, cancellationToken); } - - function emit(sourceFile?: SourceFile, writeFileCallback?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, transformers?: CustomTransformers, forceDtsEmit?: boolean): EmitResult { - return runWithCancellationToken(() => emitWorker(program, sourceFile, writeFileCallback, cancellationToken, emitOnlyDtsFiles, transformers, forceDtsEmit)); + else { + return getDiagnosticsHelper(sourceFile, getDeclarationDiagnosticsForFile, cancellationToken); } - - function isEmitBlocked(emitFileName: string): boolean { - return hasEmitBlockingDiagnostics.has(toPath(emitFileName)); + } + function getSyntacticDiagnosticsForFile(sourceFile: SourceFile): readonly DiagnosticWithLocation[] { + // For JavaScript files, we report semantic errors for using TypeScript-only + // constructs from within a JavaScript file as syntactic errors. + if (isSourceFileJS(sourceFile)) { + if (!sourceFile.additionalSyntacticDiagnostics) { + sourceFile.additionalSyntacticDiagnostics = getJSSyntacticDiagnosticsForFile(sourceFile); + } + return concatenate(sourceFile.additionalSyntacticDiagnostics, sourceFile.parseDiagnostics); } - - function emitWorker(program: Program, sourceFile: SourceFile | undefined, writeFileCallback: WriteFileCallback | undefined, cancellationToken: CancellationToken | undefined, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers, forceDtsEmit?: boolean): EmitResult { - if (!forceDtsEmit) { - const result = handleNoEmitOptions(program, sourceFile, cancellationToken); - if (result) return result; + return sourceFile.parseDiagnostics; + } + function runWithCancellationToken(func: () => T): T { + try { + return func(); + } + catch (e) { + if (e instanceof OperationCanceledException) { + // We were canceled while performing the operation. Because our type checker + // might be a bad state, we need to throw it away. + // + // Note: we are overly aggressive here. We do not actually *have* to throw away + // the "noDiagnosticsTypeChecker". However, for simplicity, i'd like to keep + // the lifetimes of these two TypeCheckers the same. Also, we generally only + // cancel when the user has made a change anyways. And, in that case, we (the + // program instance) will get thrown away anyways. So trying to keep one of + // these type checkers alive doesn't serve much purpose. + noDiagnosticsTypeChecker = undefined!; + diagnosticsProducingTypeChecker = undefined!; } - - // Create the emit resolver outside of the "emitTime" tracking code below. That way - // any cost associated with it (like type checking) are appropriate associated with - // the type-checking counter. - // - // If the -out option is specified, we should not pass the source file to getEmitResolver. - // This is because in the -out scenario all files need to be emitted, and therefore all - // files need to be type checked. And the way to specify that all files need to be type - // checked is to not pass the file to getEmitResolver. - const emitResolver = getDiagnosticsProducingTypeChecker().getEmitResolver((options.outFile || options.out) ? undefined : sourceFile, cancellationToken); - - performance.mark("beforeEmit"); - - const emitResult = emitFiles( - emitResolver, - getEmitHost(writeFileCallback), - sourceFile, - getTransformers(options, customTransformers, emitOnlyDtsFiles), - emitOnlyDtsFiles, - /*onlyBuildInfo*/ false, - forceDtsEmit - ); - - performance.mark("afterEmit"); - performance.measure("Emit", "beforeEmit", "afterEmit"); - return emitResult; - } - - function getSourceFile(fileName: string): SourceFile | undefined { - return getSourceFileByPath(toPath(fileName)); - } - - function getSourceFileByPath(path: Path): SourceFile | undefined { - return filesByName.get(path) || undefined; - } - - function getDiagnosticsHelper( - sourceFile: SourceFile | undefined, - getDiagnostics: (sourceFile: SourceFile, cancellationToken: CancellationToken | undefined) => readonly T[], - cancellationToken: CancellationToken | undefined): readonly T[] { - if (sourceFile) { - return getDiagnostics(sourceFile, cancellationToken); - } - return sortAndDeduplicateDiagnostics(flatMap(program.getSourceFiles(), sourceFile => { - if (cancellationToken) { - cancellationToken.throwIfCancellationRequested(); - } - return getDiagnostics(sourceFile, cancellationToken); - })); - } - - function getSyntacticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly DiagnosticWithLocation[] { - return getDiagnosticsHelper(sourceFile, getSyntacticDiagnosticsForFile, cancellationToken); - } - - function getSemanticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[] { - return getDiagnosticsHelper(sourceFile, getSemanticDiagnosticsForFile, cancellationToken); - } - - function getBindAndCheckDiagnostics(sourceFile: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[] { - return getBindAndCheckDiagnosticsForFile(sourceFile, cancellationToken); - } - - function getProgramDiagnostics(sourceFile: SourceFile): readonly Diagnostic[] { + throw e; + } + } + function getSemanticDiagnosticsForFile(sourceFile: SourceFile, cancellationToken: CancellationToken | undefined): readonly Diagnostic[] { + return concatenate(getBindAndCheckDiagnosticsForFile(sourceFile, cancellationToken), getProgramDiagnostics(sourceFile)); + } + function getBindAndCheckDiagnosticsForFile(sourceFile: SourceFile, cancellationToken: CancellationToken | undefined): readonly Diagnostic[] { + return getAndCacheDiagnostics(sourceFile, cancellationToken, cachedBindAndCheckDiagnosticsForFile, getBindAndCheckDiagnosticsForFileNoCache); + } + function getBindAndCheckDiagnosticsForFileNoCache(sourceFile: SourceFile, cancellationToken: CancellationToken | undefined): readonly Diagnostic[] { + return runWithCancellationToken(() => { if (skipTypeChecking(sourceFile, options, program)) { return emptyArray; } - - const fileProcessingDiagnosticsInFile = fileProcessingDiagnostics.getDiagnostics(sourceFile.fileName); - const programDiagnosticsInFile = programDiagnostics.getDiagnostics(sourceFile.fileName); - + const typeChecker = getDiagnosticsProducingTypeChecker(); + Debug.assert(!!sourceFile.bindDiagnostics); + const isCheckJs = isCheckJsEnabledForFile(sourceFile, options); + const isTsNoCheck = !!sourceFile.checkJsDirective && sourceFile.checkJsDirective.enabled === false; + // By default, only type-check .ts, .tsx, 'Deferred' and 'External' files (external files are added by plugins) + const includeBindAndCheckDiagnostics = !isTsNoCheck && (sourceFile.scriptKind === ScriptKind.TS || sourceFile.scriptKind === ScriptKind.TSX || + sourceFile.scriptKind === ScriptKind.External || isCheckJs || sourceFile.scriptKind === ScriptKind.Deferred); + const bindDiagnostics: readonly Diagnostic[] = includeBindAndCheckDiagnostics ? sourceFile.bindDiagnostics : emptyArray; + const checkDiagnostics = includeBindAndCheckDiagnostics ? typeChecker.getDiagnostics(sourceFile, cancellationToken) : emptyArray; let diagnostics: Diagnostic[] | undefined; - for (const diags of [fileProcessingDiagnosticsInFile, programDiagnosticsInFile]) { + for (const diags of [bindDiagnostics, checkDiagnostics, isCheckJs ? sourceFile.jsDocDiagnostics : undefined]) { if (diags) { for (const diag of diags) { if (shouldReportDiagnostic(diag)) { @@ -1671,1904 +1517,1531 @@ namespace ts { } } return diagnostics || emptyArray; - } - - function getDeclarationDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly DiagnosticWithLocation[] { - const options = program.getCompilerOptions(); - // collect diagnostics from the program only once if either no source file was specified or out/outFile is set (bundled emit) - if (!sourceFile || options.out || options.outFile) { - return getDeclarationDiagnosticsWorker(sourceFile, cancellationToken); - } - else { - return getDiagnosticsHelper(sourceFile, getDeclarationDiagnosticsForFile, cancellationToken); - } - } - - function getSyntacticDiagnosticsForFile(sourceFile: SourceFile): readonly DiagnosticWithLocation[] { - // For JavaScript files, we report semantic errors for using TypeScript-only - // constructs from within a JavaScript file as syntactic errors. - if (isSourceFileJS(sourceFile)) { - if (!sourceFile.additionalSyntacticDiagnostics) { - sourceFile.additionalSyntacticDiagnostics = getJSSyntacticDiagnosticsForFile(sourceFile); - } - return concatenate(sourceFile.additionalSyntacticDiagnostics, sourceFile.parseDiagnostics); - } - return sourceFile.parseDiagnostics; - } - - function runWithCancellationToken(func: () => T): T { - try { - return func(); - } - catch (e) { - if (e instanceof OperationCanceledException) { - // We were canceled while performing the operation. Because our type checker - // might be a bad state, we need to throw it away. - // - // Note: we are overly aggressive here. We do not actually *have* to throw away - // the "noDiagnosticsTypeChecker". However, for simplicity, i'd like to keep - // the lifetimes of these two TypeCheckers the same. Also, we generally only - // cancel when the user has made a change anyways. And, in that case, we (the - // program instance) will get thrown away anyways. So trying to keep one of - // these type checkers alive doesn't serve much purpose. - noDiagnosticsTypeChecker = undefined!; - diagnosticsProducingTypeChecker = undefined!; - } - - throw e; - } - } - - function getSemanticDiagnosticsForFile(sourceFile: SourceFile, cancellationToken: CancellationToken | undefined): readonly Diagnostic[] { - return concatenate( - getBindAndCheckDiagnosticsForFile(sourceFile, cancellationToken), - getProgramDiagnostics(sourceFile) - ); - } - - function getBindAndCheckDiagnosticsForFile(sourceFile: SourceFile, cancellationToken: CancellationToken | undefined): readonly Diagnostic[] { - return getAndCacheDiagnostics(sourceFile, cancellationToken, cachedBindAndCheckDiagnosticsForFile, getBindAndCheckDiagnosticsForFileNoCache); - } - - function getBindAndCheckDiagnosticsForFileNoCache(sourceFile: SourceFile, cancellationToken: CancellationToken | undefined): readonly Diagnostic[] { - return runWithCancellationToken(() => { - if (skipTypeChecking(sourceFile, options, program)) { - return emptyArray; - } - - const typeChecker = getDiagnosticsProducingTypeChecker(); - - Debug.assert(!!sourceFile.bindDiagnostics); - - const isCheckJs = isCheckJsEnabledForFile(sourceFile, options); - const isTsNoCheck = !!sourceFile.checkJsDirective && sourceFile.checkJsDirective.enabled === false; - // By default, only type-check .ts, .tsx, 'Deferred' and 'External' files (external files are added by plugins) - const includeBindAndCheckDiagnostics = !isTsNoCheck && (sourceFile.scriptKind === ScriptKind.TS || sourceFile.scriptKind === ScriptKind.TSX || - sourceFile.scriptKind === ScriptKind.External || isCheckJs || sourceFile.scriptKind === ScriptKind.Deferred); - const bindDiagnostics: readonly Diagnostic[] = includeBindAndCheckDiagnostics ? sourceFile.bindDiagnostics : emptyArray; - const checkDiagnostics = includeBindAndCheckDiagnostics ? typeChecker.getDiagnostics(sourceFile, cancellationToken) : emptyArray; - - let diagnostics: Diagnostic[] | undefined; - for (const diags of [bindDiagnostics, checkDiagnostics, isCheckJs ? sourceFile.jsDocDiagnostics : undefined]) { - if (diags) { - for (const diag of diags) { - if (shouldReportDiagnostic(diag)) { - diagnostics = append(diagnostics, diag); - } - } - } + }); + } + function getSuggestionDiagnostics(sourceFile: SourceFile, cancellationToken: CancellationToken): readonly DiagnosticWithLocation[] { + return runWithCancellationToken(() => { + return getDiagnosticsProducingTypeChecker().getSuggestionDiagnostics(sourceFile, cancellationToken); + }); + } + /** + * Skip errors if previous line start with '// @ts-ignore' comment, not counting non-empty non-comment lines + */ + function shouldReportDiagnostic(diagnostic: Diagnostic) { + const { file, start } = diagnostic; + if (file) { + const lineStarts = getLineStarts(file); + let { line } = computeLineAndCharacterOfPosition(lineStarts, (start!)); // TODO: GH#18217 + while (line > 0) { + const previousLineText = file.text.slice(lineStarts[line - 1], lineStarts[line]); + const result = ignoreDiagnosticCommentRegEx.exec(previousLineText); + if (!result) { + // non-empty line + return true; } - return diagnostics || emptyArray; - }); - } - - function getSuggestionDiagnostics(sourceFile: SourceFile, cancellationToken: CancellationToken): readonly DiagnosticWithLocation[] { - return runWithCancellationToken(() => { - return getDiagnosticsProducingTypeChecker().getSuggestionDiagnostics(sourceFile, cancellationToken); - }); - } - - /** - * Skip errors if previous line start with '// @ts-ignore' comment, not counting non-empty non-comment lines - */ - function shouldReportDiagnostic(diagnostic: Diagnostic) { - const { file, start } = diagnostic; - if (file) { - const lineStarts = getLineStarts(file); - let { line } = computeLineAndCharacterOfPosition(lineStarts, start!); // TODO: GH#18217 - while (line > 0) { - const previousLineText = file.text.slice(lineStarts[line - 1], lineStarts[line]); - const result = ignoreDiagnosticCommentRegEx.exec(previousLineText); - if (!result) { - // non-empty line - return true; - } - if (result[3]) { - // @ts-ignore - return false; - } - line--; + if (result[3]) { + // @ts-ignore + return false; } + line--; } - return true; } - - function getJSSyntacticDiagnosticsForFile(sourceFile: SourceFile): DiagnosticWithLocation[] { - return runWithCancellationToken(() => { - const diagnostics: DiagnosticWithLocation[] = []; - walk(sourceFile, sourceFile); - forEachChildRecursively(sourceFile, walk, walkArray); - - return diagnostics; - - function walk(node: Node, parent: Node) { - // Return directly from the case if the given node doesnt want to visit each child - // Otherwise break to visit each child - - switch (parent.kind) { - case SyntaxKind.Parameter: - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.MethodDeclaration: - if ((parent).questionToken === node) { - diagnostics.push(createDiagnosticForNode(node, Diagnostics.The_0_modifier_can_only_be_used_in_TypeScript_files, "?")); - return "skip"; - } - // falls through - case SyntaxKind.MethodSignature: - case SyntaxKind.Constructor: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - case SyntaxKind.FunctionExpression: - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.ArrowFunction: - case SyntaxKind.VariableDeclaration: - // type annotation - if ((parent).type === node) { - diagnostics.push(createDiagnosticForNode(node, Diagnostics.Type_annotations_can_only_be_used_in_TypeScript_files)); - return "skip"; - } - } - - switch (node.kind) { - case SyntaxKind.ImportClause: - if ((node as ImportClause).isTypeOnly) { - diagnostics.push(createDiagnosticForNode(node.parent, Diagnostics._0_declarations_can_only_be_used_in_TypeScript_files, "import type")); - return "skip"; - } - break; - case SyntaxKind.ExportDeclaration: - if ((node as ExportDeclaration).isTypeOnly) { - diagnostics.push(createDiagnosticForNode(node, Diagnostics._0_declarations_can_only_be_used_in_TypeScript_files, "export type")); - return "skip"; - } - break; - case SyntaxKind.ImportEqualsDeclaration: - diagnostics.push(createDiagnosticForNode(node, Diagnostics.import_can_only_be_used_in_TypeScript_files)); + return true; + } + function getJSSyntacticDiagnosticsForFile(sourceFile: SourceFile): DiagnosticWithLocation[] { + return runWithCancellationToken(() => { + const diagnostics: DiagnosticWithLocation[] = []; + walk(sourceFile, sourceFile); + forEachChildRecursively(sourceFile, walk, walkArray); + return diagnostics; + function walk(node: Node, parent: Node) { + // Return directly from the case if the given node doesnt want to visit each child + // Otherwise break to visit each child + switch (parent.kind) { + case SyntaxKind.Parameter: + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.MethodDeclaration: + if ((parent).questionToken === node) { + diagnostics.push(createDiagnosticForNode(node, Diagnostics.The_0_modifier_can_only_be_used_in_TypeScript_files, "?")); return "skip"; - case SyntaxKind.ExportAssignment: - if ((node).isExportEquals) { - diagnostics.push(createDiagnosticForNode(node, Diagnostics.export_can_only_be_used_in_TypeScript_files)); - return "skip"; - } - break; - case SyntaxKind.HeritageClause: - const heritageClause = node; - if (heritageClause.token === SyntaxKind.ImplementsKeyword) { - diagnostics.push(createDiagnosticForNode(node, Diagnostics.implements_clauses_can_only_be_used_in_TypeScript_files)); - return "skip"; - } - break; - case SyntaxKind.InterfaceDeclaration: - const interfaceKeyword = tokenToString(SyntaxKind.InterfaceKeyword); - Debug.assertIsDefined(interfaceKeyword); - diagnostics.push(createDiagnosticForNode(node, Diagnostics._0_declarations_can_only_be_used_in_TypeScript_files, interfaceKeyword)); + } + // falls through + case SyntaxKind.MethodSignature: + case SyntaxKind.Constructor: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + case SyntaxKind.FunctionExpression: + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.ArrowFunction: + case SyntaxKind.VariableDeclaration: + // type annotation + if ((parent).type === node) { + diagnostics.push(createDiagnosticForNode(node, Diagnostics.Type_annotations_can_only_be_used_in_TypeScript_files)); + return "skip"; + } + } + switch (node.kind) { + case SyntaxKind.ImportClause: + if ((node as ImportClause).isTypeOnly) { + diagnostics.push(createDiagnosticForNode(node.parent, Diagnostics._0_declarations_can_only_be_used_in_TypeScript_files, "import type")); return "skip"; - case SyntaxKind.ModuleDeclaration: - const moduleKeyword = node.flags & NodeFlags.Namespace ? tokenToString(SyntaxKind.NamespaceKeyword) : tokenToString(SyntaxKind.ModuleKeyword); - Debug.assertIsDefined(moduleKeyword); - diagnostics.push(createDiagnosticForNode(node, Diagnostics._0_declarations_can_only_be_used_in_TypeScript_files, moduleKeyword)); + } + break; + case SyntaxKind.ExportDeclaration: + if ((node as ExportDeclaration).isTypeOnly) { + diagnostics.push(createDiagnosticForNode(node, Diagnostics._0_declarations_can_only_be_used_in_TypeScript_files, "export type")); return "skip"; - case SyntaxKind.TypeAliasDeclaration: - diagnostics.push(createDiagnosticForNode(node, Diagnostics.Type_aliases_can_only_be_used_in_TypeScript_files)); + } + break; + case SyntaxKind.ImportEqualsDeclaration: + diagnostics.push(createDiagnosticForNode(node, Diagnostics.import_can_only_be_used_in_TypeScript_files)); + return "skip"; + case SyntaxKind.ExportAssignment: + if ((node).isExportEquals) { + diagnostics.push(createDiagnosticForNode(node, Diagnostics.export_can_only_be_used_in_TypeScript_files)); return "skip"; - case SyntaxKind.EnumDeclaration: - const enumKeyword = Debug.checkDefined(tokenToString(SyntaxKind.EnumKeyword)); - diagnostics.push(createDiagnosticForNode(node, Diagnostics._0_declarations_can_only_be_used_in_TypeScript_files, enumKeyword)); + } + break; + case SyntaxKind.HeritageClause: + const heritageClause = (node); + if (heritageClause.token === SyntaxKind.ImplementsKeyword) { + diagnostics.push(createDiagnosticForNode(node, Diagnostics.implements_clauses_can_only_be_used_in_TypeScript_files)); return "skip"; - case SyntaxKind.NonNullExpression: - diagnostics.push(createDiagnosticForNode(node, Diagnostics.Non_null_assertions_can_only_be_used_in_TypeScript_files)); + } + break; + case SyntaxKind.InterfaceDeclaration: + const interfaceKeyword = tokenToString(SyntaxKind.InterfaceKeyword); + Debug.assertIsDefined(interfaceKeyword); + diagnostics.push(createDiagnosticForNode(node, Diagnostics._0_declarations_can_only_be_used_in_TypeScript_files, interfaceKeyword)); + return "skip"; + case SyntaxKind.ModuleDeclaration: + const moduleKeyword = node.flags & NodeFlags.Namespace ? tokenToString(SyntaxKind.NamespaceKeyword) : tokenToString(SyntaxKind.ModuleKeyword); + Debug.assertIsDefined(moduleKeyword); + diagnostics.push(createDiagnosticForNode(node, Diagnostics._0_declarations_can_only_be_used_in_TypeScript_files, moduleKeyword)); + return "skip"; + case SyntaxKind.TypeAliasDeclaration: + diagnostics.push(createDiagnosticForNode(node, Diagnostics.Type_aliases_can_only_be_used_in_TypeScript_files)); + return "skip"; + case SyntaxKind.EnumDeclaration: + const enumKeyword = Debug.checkDefined(tokenToString(SyntaxKind.EnumKeyword)); + diagnostics.push(createDiagnosticForNode(node, Diagnostics._0_declarations_can_only_be_used_in_TypeScript_files, enumKeyword)); + return "skip"; + case SyntaxKind.NonNullExpression: + diagnostics.push(createDiagnosticForNode(node, Diagnostics.Non_null_assertions_can_only_be_used_in_TypeScript_files)); + return "skip"; + case SyntaxKind.AsExpression: + diagnostics.push(createDiagnosticForNode((node as AsExpression).type, Diagnostics.Type_assertion_expressions_can_only_be_used_in_TypeScript_files)); + return "skip"; + case SyntaxKind.TypeAssertionExpression: + Debug.fail(); // Won't parse these in a JS file anyway, as they are interpreted as JSX. + } + } + function walkArray(nodes: NodeArray, parent: Node) { + if (parent.decorators === nodes && !options.experimentalDecorators) { + diagnostics.push(createDiagnosticForNode(parent, Diagnostics.Experimental_support_for_decorators_is_a_feature_that_is_subject_to_change_in_a_future_release_Set_the_experimentalDecorators_option_in_your_tsconfig_or_jsconfig_to_remove_this_warning)); + } + switch (parent.kind) { + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ClassExpression: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.Constructor: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + case SyntaxKind.FunctionExpression: + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.ArrowFunction: + // Check type parameters + if (nodes === (parent).typeParameters) { + diagnostics.push(createDiagnosticForNodeArray(nodes, Diagnostics.Type_parameter_declarations_can_only_be_used_in_TypeScript_files)); return "skip"; - case SyntaxKind.AsExpression: - diagnostics.push(createDiagnosticForNode((node as AsExpression).type, Diagnostics.Type_assertion_expressions_can_only_be_used_in_TypeScript_files)); + } + // falls through + case SyntaxKind.VariableStatement: + // Check modifiers + if (nodes === parent.modifiers) { + checkModifiers(parent.modifiers, parent.kind === SyntaxKind.VariableStatement); return "skip"; - case SyntaxKind.TypeAssertionExpression: - Debug.fail(); // Won't parse these in a JS file anyway, as they are interpreted as JSX. - } - } - - function walkArray(nodes: NodeArray, parent: Node) { - if (parent.decorators === nodes && !options.experimentalDecorators) { - diagnostics.push(createDiagnosticForNode(parent, Diagnostics.Experimental_support_for_decorators_is_a_feature_that_is_subject_to_change_in_a_future_release_Set_the_experimentalDecorators_option_in_your_tsconfig_or_jsconfig_to_remove_this_warning)); - } - - switch (parent.kind) { - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ClassExpression: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.Constructor: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - case SyntaxKind.FunctionExpression: - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.ArrowFunction: - // Check type parameters - if (nodes === (parent).typeParameters) { - diagnostics.push(createDiagnosticForNodeArray(nodes, Diagnostics.Type_parameter_declarations_can_only_be_used_in_TypeScript_files)); - return "skip"; - } - // falls through - - case SyntaxKind.VariableStatement: - // Check modifiers - if (nodes === parent.modifiers) { - checkModifiers(parent.modifiers, parent.kind === SyntaxKind.VariableStatement); - return "skip"; - } - break; - case SyntaxKind.PropertyDeclaration: - // Check modifiers of property declaration - if (nodes === (parent).modifiers) { - for (const modifier of >nodes) { - if (modifier.kind !== SyntaxKind.StaticKeyword) { - diagnostics.push(createDiagnosticForNode(modifier, Diagnostics.The_0_modifier_can_only_be_used_in_TypeScript_files, tokenToString(modifier.kind))); - } + } + break; + case SyntaxKind.PropertyDeclaration: + // Check modifiers of property declaration + if (nodes === (parent).modifiers) { + for (const modifier of >nodes) { + if (modifier.kind !== SyntaxKind.StaticKeyword) { + diagnostics.push(createDiagnosticForNode(modifier, Diagnostics.The_0_modifier_can_only_be_used_in_TypeScript_files, tokenToString(modifier.kind))); } - return "skip"; - } - break; - case SyntaxKind.Parameter: - // Check modifiers of parameter declaration - if (nodes === (parent).modifiers) { - diagnostics.push(createDiagnosticForNodeArray(nodes, Diagnostics.Parameter_modifiers_can_only_be_used_in_TypeScript_files)); - return "skip"; } - break; - case SyntaxKind.CallExpression: - case SyntaxKind.NewExpression: - case SyntaxKind.ExpressionWithTypeArguments: - case SyntaxKind.JsxSelfClosingElement: - case SyntaxKind.JsxOpeningElement: - case SyntaxKind.TaggedTemplateExpression: - // Check type arguments - if (nodes === (parent).typeArguments) { - diagnostics.push(createDiagnosticForNodeArray(nodes, Diagnostics.Type_arguments_can_only_be_used_in_TypeScript_files)); - return "skip"; + return "skip"; + } + break; + case SyntaxKind.Parameter: + // Check modifiers of parameter declaration + if (nodes === (parent).modifiers) { + diagnostics.push(createDiagnosticForNodeArray(nodes, Diagnostics.Parameter_modifiers_can_only_be_used_in_TypeScript_files)); + return "skip"; + } + break; + case SyntaxKind.CallExpression: + case SyntaxKind.NewExpression: + case SyntaxKind.ExpressionWithTypeArguments: + case SyntaxKind.JsxSelfClosingElement: + case SyntaxKind.JsxOpeningElement: + case SyntaxKind.TaggedTemplateExpression: + // Check type arguments + if (nodes === (parent).typeArguments) { + diagnostics.push(createDiagnosticForNodeArray(nodes, Diagnostics.Type_arguments_can_only_be_used_in_TypeScript_files)); + return "skip"; + } + break; + } + } + function checkModifiers(modifiers: NodeArray, isConstValid: boolean) { + for (const modifier of modifiers) { + switch (modifier.kind) { + case SyntaxKind.ConstKeyword: + if (isConstValid) { + continue; } + // to report error, + // falls through + case SyntaxKind.PublicKeyword: + case SyntaxKind.PrivateKeyword: + case SyntaxKind.ProtectedKeyword: + case SyntaxKind.ReadonlyKeyword: + case SyntaxKind.DeclareKeyword: + case SyntaxKind.AbstractKeyword: + diagnostics.push(createDiagnosticForNode(modifier, Diagnostics.The_0_modifier_can_only_be_used_in_TypeScript_files, tokenToString(modifier.kind))); break; + // These are all legal modifiers. + case SyntaxKind.StaticKeyword: + case SyntaxKind.ExportKeyword: + case SyntaxKind.DefaultKeyword: } } - - function checkModifiers(modifiers: NodeArray, isConstValid: boolean) { - for (const modifier of modifiers) { - switch (modifier.kind) { - case SyntaxKind.ConstKeyword: - if (isConstValid) { - continue; - } - // to report error, - // falls through - case SyntaxKind.PublicKeyword: - case SyntaxKind.PrivateKeyword: - case SyntaxKind.ProtectedKeyword: - case SyntaxKind.ReadonlyKeyword: - case SyntaxKind.DeclareKeyword: - case SyntaxKind.AbstractKeyword: - diagnostics.push(createDiagnosticForNode(modifier, Diagnostics.The_0_modifier_can_only_be_used_in_TypeScript_files, tokenToString(modifier.kind))); - break; - - // These are all legal modifiers. - case SyntaxKind.StaticKeyword: - case SyntaxKind.ExportKeyword: - case SyntaxKind.DefaultKeyword: - } - } - } - - function createDiagnosticForNodeArray(nodes: NodeArray, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number): DiagnosticWithLocation { - const start = nodes.pos; - return createFileDiagnostic(sourceFile, start, nodes.end - start, message, arg0, arg1, arg2); - } - - // Since these are syntactic diagnostics, parent might not have been set - // this means the sourceFile cannot be infered from the node - function createDiagnosticForNode(node: Node, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number): DiagnosticWithLocation { - return createDiagnosticForNodeInSourceFile(sourceFile, node, message, arg0, arg1, arg2); - } - }); - } - - function getDeclarationDiagnosticsWorker(sourceFile: SourceFile | undefined, cancellationToken: CancellationToken | undefined): readonly DiagnosticWithLocation[] { - return getAndCacheDiagnostics(sourceFile, cancellationToken, cachedDeclarationDiagnosticsForFile, getDeclarationDiagnosticsForFileNoCache); - } - - function getDeclarationDiagnosticsForFileNoCache(sourceFile: SourceFile | undefined, cancellationToken: CancellationToken | undefined): readonly DiagnosticWithLocation[] { - return runWithCancellationToken(() => { - const resolver = getDiagnosticsProducingTypeChecker().getEmitResolver(sourceFile, cancellationToken); - // Don't actually write any files since we're just getting diagnostics. - return ts.getDeclarationDiagnostics(getEmitHost(noop), resolver, sourceFile) || emptyArray; - }); - } - - function getAndCacheDiagnostics( - sourceFile: T, - cancellationToken: CancellationToken | undefined, - cache: DiagnosticCache, - getDiagnostics: (sourceFile: T, cancellationToken: CancellationToken | undefined) => readonly U[], - ): readonly U[] { - - const cachedResult = sourceFile - ? cache.perFile && cache.perFile.get(sourceFile.path) - : cache.allDiagnostics; - - if (cachedResult) { - return cachedResult; - } - const result = getDiagnostics(sourceFile, cancellationToken); - if (sourceFile) { - if (!cache.perFile) { - cache.perFile = createMap(); - } - cache.perFile.set(sourceFile.path, result); } - else { - cache.allDiagnostics = result; + function createDiagnosticForNodeArray(nodes: NodeArray, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number): DiagnosticWithLocation { + const start = nodes.pos; + return createFileDiagnostic(sourceFile, start, nodes.end - start, message, arg0, arg1, arg2); } - return result; - } - - function getDeclarationDiagnosticsForFile(sourceFile: SourceFile, cancellationToken: CancellationToken): readonly DiagnosticWithLocation[] { - return sourceFile.isDeclarationFile ? [] : getDeclarationDiagnosticsWorker(sourceFile, cancellationToken); - } - - function getOptionsDiagnostics(): SortedReadonlyArray { - return sortAndDeduplicateDiagnostics(concatenate( - fileProcessingDiagnostics.getGlobalDiagnostics(), - concatenate( - programDiagnostics.getGlobalDiagnostics(), - getOptionsDiagnosticsOfConfigFile() - ) - )); - } - - function getOptionsDiagnosticsOfConfigFile() { - if (!options.configFile) { return emptyArray; } - let diagnostics = programDiagnostics.getDiagnostics(options.configFile.fileName); - forEachResolvedProjectReference(resolvedRef => { - if (resolvedRef) { - diagnostics = concatenate(diagnostics, programDiagnostics.getDiagnostics(resolvedRef.sourceFile.fileName)); - } - }); - return diagnostics; - } - - function getGlobalDiagnostics(): SortedReadonlyArray { - return rootNames.length ? sortAndDeduplicateDiagnostics(getDiagnosticsProducingTypeChecker().getGlobalDiagnostics().slice()) : emptyArray as any as SortedReadonlyArray; - } - - function getConfigFileParsingDiagnostics(): readonly Diagnostic[] { - return configFileParsingDiagnostics || emptyArray; + // Since these are syntactic diagnostics, parent might not have been set + // this means the sourceFile cannot be infered from the node + function createDiagnosticForNode(node: Node, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number): DiagnosticWithLocation { + return createDiagnosticForNodeInSourceFile(sourceFile, node, message, arg0, arg1, arg2); + } + }); + } + function getDeclarationDiagnosticsWorker(sourceFile: SourceFile | undefined, cancellationToken: CancellationToken | undefined): readonly DiagnosticWithLocation[] { + return getAndCacheDiagnostics(sourceFile, cancellationToken, cachedDeclarationDiagnosticsForFile, getDeclarationDiagnosticsForFileNoCache); + } + function getDeclarationDiagnosticsForFileNoCache(sourceFile: SourceFile | undefined, cancellationToken: CancellationToken | undefined): readonly DiagnosticWithLocation[] { + return runWithCancellationToken(() => { + const resolver = getDiagnosticsProducingTypeChecker().getEmitResolver(sourceFile, cancellationToken); + // Don't actually write any files since we're just getting diagnostics. + return ts.getDeclarationDiagnostics(getEmitHost(noop), resolver, sourceFile) || emptyArray; + }); + } + function getAndCacheDiagnostics(sourceFile: T, cancellationToken: CancellationToken | undefined, cache: DiagnosticCache, getDiagnostics: (sourceFile: T, cancellationToken: CancellationToken | undefined) => readonly U[]): readonly U[] { + const cachedResult = sourceFile + ? cache.perFile && cache.perFile.get(sourceFile.path) + : cache.allDiagnostics; + if (cachedResult) { + return cachedResult; } - - function processRootFile(fileName: string, isDefaultLib: boolean, ignoreNoDefaultLib: boolean) { - processSourceFile(normalizePath(fileName), isDefaultLib, ignoreNoDefaultLib, /*packageId*/ undefined); + const result = getDiagnostics(sourceFile, cancellationToken); + if (sourceFile) { + if (!cache.perFile) { + cache.perFile = createMap(); + } + cache.perFile.set(sourceFile.path, result); } - - function fileReferenceIsEqualTo(a: FileReference, b: FileReference): boolean { - return a.fileName === b.fileName; + else { + cache.allDiagnostics = result; } - - function moduleNameIsEqualTo(a: StringLiteralLike | Identifier, b: StringLiteralLike | Identifier): boolean { - return a.kind === SyntaxKind.Identifier - ? b.kind === SyntaxKind.Identifier && a.escapedText === b.escapedText - : b.kind === SyntaxKind.StringLiteral && a.text === b.text; + return result; + } + function getDeclarationDiagnosticsForFile(sourceFile: SourceFile, cancellationToken: CancellationToken): readonly DiagnosticWithLocation[] { + return sourceFile.isDeclarationFile ? [] : getDeclarationDiagnosticsWorker(sourceFile, cancellationToken); + } + function getOptionsDiagnostics(): SortedReadonlyArray { + return sortAndDeduplicateDiagnostics(concatenate(fileProcessingDiagnostics.getGlobalDiagnostics(), concatenate(programDiagnostics.getGlobalDiagnostics(), getOptionsDiagnosticsOfConfigFile()))); + } + function getOptionsDiagnosticsOfConfigFile() { + if (!options.configFile) { + return emptyArray; } - - function collectExternalModuleReferences(file: SourceFile): void { - if (file.imports) { - return; + let diagnostics = programDiagnostics.getDiagnostics(options.configFile.fileName); + forEachResolvedProjectReference(resolvedRef => { + if (resolvedRef) { + diagnostics = concatenate(diagnostics, programDiagnostics.getDiagnostics(resolvedRef.sourceFile.fileName)); } - - const isJavaScriptFile = isSourceFileJS(file); - const isExternalModuleFile = isExternalModule(file); - - // file.imports may not be undefined if there exists dynamic import - let imports: StringLiteralLike[] | undefined; - let moduleAugmentations: (StringLiteral | Identifier)[] | undefined; - let ambientModules: string[] | undefined; - - // If we are importing helpers, we need to add a synthetic reference to resolve the - // helpers library. - if (options.importHelpers - && (options.isolatedModules || isExternalModuleFile) - && !file.isDeclarationFile) { - // synthesize 'import "tslib"' declaration - const externalHelpersModuleReference = createLiteral(externalHelpersModuleNameText); - const importDecl = createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, /*importClause*/ undefined, externalHelpersModuleReference); - addEmitFlags(importDecl, EmitFlags.NeverApplyImportHelper); - externalHelpersModuleReference.parent = importDecl; - importDecl.parent = file; - imports = [externalHelpersModuleReference]; - } - - for (const node of file.statements) { - collectModuleReferences(node, /*inAmbientModule*/ false); - } - if ((file.flags & NodeFlags.PossiblyContainsDynamicImport) || isJavaScriptFile) { - collectDynamicImportOrRequireCalls(file); - } - - file.imports = imports || emptyArray; - file.moduleAugmentations = moduleAugmentations || emptyArray; - file.ambientModuleNames = ambientModules || emptyArray; - + }); + return diagnostics; + } + function getGlobalDiagnostics(): SortedReadonlyArray { + return rootNames.length ? sortAndDeduplicateDiagnostics(getDiagnosticsProducingTypeChecker().getGlobalDiagnostics().slice()) : emptyArray as any as SortedReadonlyArray; + } + function getConfigFileParsingDiagnostics(): readonly Diagnostic[] { + return configFileParsingDiagnostics || emptyArray; + } + function processRootFile(fileName: string, isDefaultLib: boolean, ignoreNoDefaultLib: boolean) { + processSourceFile(normalizePath(fileName), isDefaultLib, ignoreNoDefaultLib, /*packageId*/ undefined); + } + function fileReferenceIsEqualTo(a: FileReference, b: FileReference): boolean { + return a.fileName === b.fileName; + } + function moduleNameIsEqualTo(a: StringLiteralLike | Identifier, b: StringLiteralLike | Identifier): boolean { + return a.kind === SyntaxKind.Identifier + ? b.kind === SyntaxKind.Identifier && a.escapedText === b.escapedText + : b.kind === SyntaxKind.StringLiteral && a.text === b.text; + } + function collectExternalModuleReferences(file: SourceFile): void { + if (file.imports) { return; - - function collectModuleReferences(node: Statement, inAmbientModule: boolean): void { - if (isAnyImportOrReExport(node)) { - const moduleNameExpr = getExternalModuleName(node); - // TypeScript 1.0 spec (April 2014): 12.1.6 - // An ExternalImportDeclaration in an AmbientExternalModuleDeclaration may reference other external modules - // only through top - level external module names. Relative external module names are not permitted. - if (moduleNameExpr && isStringLiteral(moduleNameExpr) && moduleNameExpr.text && (!inAmbientModule || !isExternalModuleNameRelative(moduleNameExpr.text))) { - imports = append(imports, moduleNameExpr); - } - } - else if (isModuleDeclaration(node)) { - if (isAmbientModule(node) && (inAmbientModule || hasModifier(node, ModifierFlags.Ambient) || file.isDeclarationFile)) { - const nameText = getTextOfIdentifierOrLiteral(node.name); - // Ambient module declarations can be interpreted as augmentations for some existing external modules. - // This will happen in two cases: - // - if current file is external module then module augmentation is a ambient module declaration defined in the top level scope - // - if current file is not external module then module augmentation is an ambient module declaration with non-relative module name - // immediately nested in top level ambient module declaration . - if (isExternalModuleFile || (inAmbientModule && !isExternalModuleNameRelative(nameText))) { - (moduleAugmentations || (moduleAugmentations = [])).push(node.name); + } + const isJavaScriptFile = isSourceFileJS(file); + const isExternalModuleFile = isExternalModule(file); + // file.imports may not be undefined if there exists dynamic import + let imports: StringLiteralLike[] | undefined; + let moduleAugmentations: (StringLiteral | Identifier)[] | undefined; + let ambientModules: string[] | undefined; + // If we are importing helpers, we need to add a synthetic reference to resolve the + // helpers library. + if (options.importHelpers + && (options.isolatedModules || isExternalModuleFile) + && !file.isDeclarationFile) { + // synthesize 'import "tslib"' declaration + const externalHelpersModuleReference = createLiteral(externalHelpersModuleNameText); + const importDecl = createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, /*importClause*/ undefined, externalHelpersModuleReference); + addEmitFlags(importDecl, EmitFlags.NeverApplyImportHelper); + externalHelpersModuleReference.parent = importDecl; + importDecl.parent = file; + imports = [externalHelpersModuleReference]; + } + for (const node of file.statements) { + collectModuleReferences(node, /*inAmbientModule*/ false); + } + if ((file.flags & NodeFlags.PossiblyContainsDynamicImport) || isJavaScriptFile) { + collectDynamicImportOrRequireCalls(file); + } + file.imports = imports || emptyArray; + file.moduleAugmentations = moduleAugmentations || emptyArray; + file.ambientModuleNames = ambientModules || emptyArray; + return; + function collectModuleReferences(node: Statement, inAmbientModule: boolean): void { + if (isAnyImportOrReExport(node)) { + const moduleNameExpr = getExternalModuleName(node); + // TypeScript 1.0 spec (April 2014): 12.1.6 + // An ExternalImportDeclaration in an AmbientExternalModuleDeclaration may reference other external modules + // only through top - level external module names. Relative external module names are not permitted. + if (moduleNameExpr && isStringLiteral(moduleNameExpr) && moduleNameExpr.text && (!inAmbientModule || !isExternalModuleNameRelative(moduleNameExpr.text))) { + imports = append(imports, moduleNameExpr); + } + } + else if (isModuleDeclaration(node)) { + if (isAmbientModule(node) && (inAmbientModule || hasModifier(node, ModifierFlags.Ambient) || file.isDeclarationFile)) { + const nameText = getTextOfIdentifierOrLiteral(node.name); + // Ambient module declarations can be interpreted as augmentations for some existing external modules. + // This will happen in two cases: + // - if current file is external module then module augmentation is a ambient module declaration defined in the top level scope + // - if current file is not external module then module augmentation is an ambient module declaration with non-relative module name + // immediately nested in top level ambient module declaration . + if (isExternalModuleFile || (inAmbientModule && !isExternalModuleNameRelative(nameText))) { + (moduleAugmentations || (moduleAugmentations = [])).push(node.name); + } + else if (!inAmbientModule) { + if (file.isDeclarationFile) { + // for global .d.ts files record name of ambient module + (ambientModules || (ambientModules = [])).push(nameText); } - else if (!inAmbientModule) { - if (file.isDeclarationFile) { - // for global .d.ts files record name of ambient module - (ambientModules || (ambientModules = [])).push(nameText); - } - // An AmbientExternalModuleDeclaration declares an external module. - // This type of declaration is permitted only in the global module. - // The StringLiteral must specify a top - level external module name. - // Relative external module names are not permitted - - // NOTE: body of ambient module is always a module block, if it exists - const body = (node).body; - if (body) { - for (const statement of body.statements) { - collectModuleReferences(statement, /*inAmbientModule*/ true); - } + // An AmbientExternalModuleDeclaration declares an external module. + // This type of declaration is permitted only in the global module. + // The StringLiteral must specify a top - level external module name. + // Relative external module names are not permitted + // NOTE: body of ambient module is always a module block, if it exists + const body = ((node).body); + if (body) { + for (const statement of body.statements) { + collectModuleReferences(statement, /*inAmbientModule*/ true); } } } } } - - function collectDynamicImportOrRequireCalls(file: SourceFile) { - const r = /import|require/g; - while (r.exec(file.text) !== null) { // eslint-disable-line no-null/no-null - const node = getNodeAtPosition(file, r.lastIndex); - if (isRequireCall(node, /*checkArgumentIsStringLiteralLike*/ true)) { - imports = append(imports, node.arguments[0]); - } - // we have to check the argument list has length of 1. We will still have to process these even though we have parsing error. - else if (isImportCall(node) && node.arguments.length === 1 && isStringLiteralLike(node.arguments[0])) { - imports = append(imports, node.arguments[0] as StringLiteralLike); + } + function collectDynamicImportOrRequireCalls(file: SourceFile) { + const r = /import|require/g; + while (r.exec(file.text) !== null) { // eslint-disable-line no-null/no-null + const node = getNodeAtPosition(file, r.lastIndex); + if (isRequireCall(node, /*checkArgumentIsStringLiteralLike*/ true)) { + imports = append(imports, node.arguments[0]); + } + // we have to check the argument list has length of 1. We will still have to process these even though we have parsing error. + else if (isImportCall(node) && node.arguments.length === 1 && isStringLiteralLike(node.arguments[0])) { + imports = append(imports, (node.arguments[0] as StringLiteralLike)); + } + else if (isLiteralImportTypeNode(node)) { + imports = append(imports, node.argument.literal); + } + } + } + /** Returns a token if position is in [start-of-leading-trivia, end), includes JSDoc only in JS files */ + function getNodeAtPosition(sourceFile: SourceFile, position: number): Node { + let current: Node = sourceFile; + const getContainingChild = (child: Node) => { + if (child.pos <= position && (position < child.end || (position === child.end && (child.kind === SyntaxKind.EndOfFileToken)))) { + return child; + } + }; + while (true) { + const child = isJavaScriptFile && hasJSDocNodes(current) && forEach(current.jsDoc, getContainingChild) || forEachChild(current, getContainingChild); + if (!child) { + return current; + } + current = child; + } + } + } + function getLibFileFromReference(ref: FileReference) { + const libName = toFileNameLowerCase(ref.fileName); + const libFileName = libMap.get(libName); + if (libFileName) { + return getSourceFile(combinePaths(defaultLibraryPath, libFileName)); + } + } + /** This should have similar behavior to 'processSourceFile' without diagnostics or mutation. */ + function getSourceFileFromReference(referencingFile: SourceFile | UnparsedSource, ref: FileReference): SourceFile | undefined { + return getSourceFileFromReferenceWorker(resolveTripleslashReference(ref.fileName, referencingFile.fileName), fileName => filesByName.get(toPath(fileName)) || undefined); + } + function getSourceFileFromReferenceWorker(fileName: string, getSourceFile: (fileName: string) => SourceFile | undefined, fail?: (diagnostic: DiagnosticMessage, ...argument: string[]) => void, refFile?: SourceFile): SourceFile | undefined { + if (hasExtension(fileName)) { + const canonicalFileName = host.getCanonicalFileName(fileName); + if (!options.allowNonTsExtensions && !forEach(supportedExtensionsWithJsonIfResolveJsonModule, extension => fileExtensionIs(canonicalFileName, extension))) { + if (fail) { + if (hasJSFileExtension(canonicalFileName)) { + fail(Diagnostics.File_0_is_a_JavaScript_file_Did_you_mean_to_enable_the_allowJs_option, fileName); } - else if (isLiteralImportTypeNode(node)) { - imports = append(imports, node.argument.literal); + else { + fail(Diagnostics.File_0_has_an_unsupported_extension_The_only_supported_extensions_are_1, fileName, "'" + supportedExtensions.join("', '") + "'"); } } + return undefined; } - - /** Returns a token if position is in [start-of-leading-trivia, end), includes JSDoc only in JS files */ - function getNodeAtPosition(sourceFile: SourceFile, position: number): Node { - let current: Node = sourceFile; - const getContainingChild = (child: Node) => { - if (child.pos <= position && (position < child.end || (position === child.end && (child.kind === SyntaxKind.EndOfFileToken)))) { - return child; + const sourceFile = getSourceFile(fileName); + if (fail) { + if (!sourceFile) { + const redirect = getProjectReferenceRedirect(fileName); + if (redirect) { + fail(Diagnostics.Output_file_0_has_not_been_built_from_source_file_1, redirect, fileName); } - }; - while (true) { - const child = isJavaScriptFile && hasJSDocNodes(current) && forEach(current.jsDoc, getContainingChild) || forEachChild(current, getContainingChild); - if (!child) { - return current; + else { + fail(Diagnostics.File_0_not_found, fileName); } - current = child; + } + else if (refFile && canonicalFileName === host.getCanonicalFileName(refFile.fileName)) { + fail(Diagnostics.A_file_cannot_have_a_reference_to_itself); } } + return sourceFile; } - - function getLibFileFromReference(ref: FileReference) { - const libName = toFileNameLowerCase(ref.fileName); - const libFileName = libMap.get(libName); - if (libFileName) { - return getSourceFile(combinePaths(defaultLibraryPath, libFileName)); - } - } - - /** This should have similar behavior to 'processSourceFile' without diagnostics or mutation. */ - function getSourceFileFromReference(referencingFile: SourceFile | UnparsedSource, ref: FileReference): SourceFile | undefined { - return getSourceFileFromReferenceWorker(resolveTripleslashReference(ref.fileName, referencingFile.fileName), fileName => filesByName.get(toPath(fileName)) || undefined); - } - - function getSourceFileFromReferenceWorker( - fileName: string, - getSourceFile: (fileName: string) => SourceFile | undefined, - fail?: (diagnostic: DiagnosticMessage, ...argument: string[]) => void, - refFile?: SourceFile): SourceFile | undefined { - - if (hasExtension(fileName)) { - const canonicalFileName = host.getCanonicalFileName(fileName); - if (!options.allowNonTsExtensions && !forEach(supportedExtensionsWithJsonIfResolveJsonModule, extension => fileExtensionIs(canonicalFileName, extension))) { - if (fail) { - if (hasJSFileExtension(canonicalFileName)) { - fail(Diagnostics.File_0_is_a_JavaScript_file_Did_you_mean_to_enable_the_allowJs_option, fileName); - } - else { - fail(Diagnostics.File_0_has_an_unsupported_extension_The_only_supported_extensions_are_1, fileName, "'" + supportedExtensions.join("', '") + "'"); - } - } - return undefined; + else { + const sourceFileNoExtension = options.allowNonTsExtensions && getSourceFile(fileName); + if (sourceFileNoExtension) + return sourceFileNoExtension; + if (fail && options.allowNonTsExtensions) { + fail(Diagnostics.File_0_not_found, fileName); + return undefined; + } + const sourceFileWithAddedExtension = forEach(supportedExtensions, extension => getSourceFile(fileName + extension)); + if (fail && !sourceFileWithAddedExtension) + fail(Diagnostics.File_0_not_found, fileName + Extension.Ts); + return sourceFileWithAddedExtension; + } + } + /** This has side effects through `findSourceFile`. */ + function processSourceFile(fileName: string, isDefaultLib: boolean, ignoreNoDefaultLib: boolean, packageId: PackageId | undefined, refFile?: RefFile): void { + getSourceFileFromReferenceWorker(fileName, fileName => findSourceFile(fileName, toPath(fileName), isDefaultLib, ignoreNoDefaultLib, refFile, packageId), // TODO: GH#18217 + (diagnostic, ...args) => fileProcessingDiagnostics.add(createRefFileDiagnostic(refFile, diagnostic, ...args)), refFile && refFile.file); + } + function reportFileNamesDifferOnlyInCasingError(fileName: string, existingFile: SourceFile, refFile: RefFile | undefined): void { + const refs = !refFile ? refFileMap && refFileMap.get(existingFile.path) : undefined; + const refToReportErrorOn = refs && find(refs, ref => ref.referencedFileName === existingFile.fileName); + fileProcessingDiagnostics.add(refToReportErrorOn ? + createFileDiagnosticAtReference(refToReportErrorOn, Diagnostics.Already_included_file_name_0_differs_from_file_name_1_only_in_casing, existingFile.fileName, fileName) : + createRefFileDiagnostic(refFile, Diagnostics.File_name_0_differs_from_already_included_file_name_1_only_in_casing, fileName, existingFile.fileName)); + } + function createRedirectSourceFile(redirectTarget: SourceFile, unredirected: SourceFile, fileName: string, path: Path, resolvedPath: Path, originalFileName: string): SourceFile { + const redirect: SourceFile = Object.create(redirectTarget); + redirect.fileName = fileName; + redirect.path = path; + redirect.resolvedPath = resolvedPath; + redirect.originalFileName = originalFileName; + redirect.redirectInfo = { redirectTarget, unredirected }; + sourceFilesFoundSearchingNodeModules.set(path, currentNodeModulesDepth > 0); + Object.defineProperties(redirect, { + id: { + get(this: SourceFile) { return this.redirectInfo!.redirectTarget.id; }, + set(this: SourceFile, value: SourceFile["id"]) { this.redirectInfo!.redirectTarget.id = value; }, + }, + symbol: { + get(this: SourceFile) { return this.redirectInfo!.redirectTarget.symbol; }, + set(this: SourceFile, value: SourceFile["symbol"]) { this.redirectInfo!.redirectTarget.symbol = value; }, + }, + }); + return redirect; + } + // Get source file from normalized fileName + function findSourceFile(fileName: string, path: Path, isDefaultLib: boolean, ignoreNoDefaultLib: boolean, refFile: RefFile | undefined, packageId: PackageId | undefined): SourceFile | undefined { + if (useSourceOfProjectReferenceRedirect) { + let source = getSourceOfProjectReferenceRedirect(fileName); + // If preserveSymlinks is true, module resolution wont jump the symlink + // but the resolved real path may be the .d.ts from project reference + // Note:: Currently we try the real path only if the + // file is from node_modules to avoid having to run real path on all file paths + if (!source && + host.realpath && + options.preserveSymlinks && + isDeclarationFileName(fileName) && + stringContains(fileName, nodeModulesPathPart)) { + const realPath = host.realpath(fileName); + if (realPath !== fileName) + source = getSourceOfProjectReferenceRedirect(realPath); + } + if (source) { + const file = isString(source) ? + findSourceFile(source, toPath(source), isDefaultLib, ignoreNoDefaultLib, refFile, packageId) : + undefined; + if (file) + addFileToFilesByName(file, path, /*redirectedPath*/ undefined); + return file; + } + } + const originalFileName = fileName; + if (filesByName.has(path)) { + const file = filesByName.get(path); + addFileToRefFileMap(fileName, file || undefined, refFile); + // try to check if we've already seen this file but with a different casing in path + // NOTE: this only makes sense for case-insensitive file systems, and only on files which are not redirected + if (file && options.forceConsistentCasingInFileNames) { + const checkedName = file.fileName; + const isRedirect = toPath(checkedName) !== toPath(fileName); + if (isRedirect) { + fileName = getProjectReferenceRedirect(fileName) || fileName; + } + // Check if it differs only in drive letters its ok to ignore that error: + const checkedAbsolutePath = getNormalizedAbsolutePathWithoutRoot(checkedName, currentDirectory); + const inputAbsolutePath = getNormalizedAbsolutePathWithoutRoot(fileName, currentDirectory); + if (checkedAbsolutePath !== inputAbsolutePath) { + reportFileNamesDifferOnlyInCasingError(fileName, file, refFile); + } + } + // If the file was previously found via a node_modules search, but is now being processed as a root file, + // then everything it sucks in may also be marked incorrectly, and needs to be checked again. + if (file && sourceFilesFoundSearchingNodeModules.get(file.path) && currentNodeModulesDepth === 0) { + sourceFilesFoundSearchingNodeModules.set(file.path, false); + if (!options.noResolve) { + processReferencedFiles(file, isDefaultLib); + processTypeReferenceDirectives(file); } - - const sourceFile = getSourceFile(fileName); - if (fail) { - if (!sourceFile) { - const redirect = getProjectReferenceRedirect(fileName); - if (redirect) { - fail(Diagnostics.Output_file_0_has_not_been_built_from_source_file_1, redirect, fileName); - } - else { - fail(Diagnostics.File_0_not_found, fileName); - } - } - else if (refFile && canonicalFileName === host.getCanonicalFileName(refFile.fileName)) { - fail(Diagnostics.A_file_cannot_have_a_reference_to_itself); - } + if (!options.noLib) { + processLibReferenceDirectives(file); } - return sourceFile; + modulesWithElidedImports.set(file.path, false); + processImportedModules(file); } - else { - const sourceFileNoExtension = options.allowNonTsExtensions && getSourceFile(fileName); - if (sourceFileNoExtension) return sourceFileNoExtension; - - if (fail && options.allowNonTsExtensions) { - fail(Diagnostics.File_0_not_found, fileName); + // See if we need to reprocess the imports due to prior skipped imports + else if (file && modulesWithElidedImports.get(file.path)) { + if (currentNodeModulesDepth < maxNodeModuleJsDepth) { + modulesWithElidedImports.set(file.path, false); + processImportedModules(file); + } + } + return file || undefined; + } + let redirectedPath: Path | undefined; + if (refFile && !useSourceOfProjectReferenceRedirect) { + const redirectProject = getProjectReferenceRedirectProject(fileName); + if (redirectProject) { + if (redirectProject.commandLine.options.outFile || redirectProject.commandLine.options.out) { + // Shouldnt create many to 1 mapping file in --out scenario return undefined; } - - const sourceFileWithAddedExtension = forEach(supportedExtensions, extension => getSourceFile(fileName + extension)); - if (fail && !sourceFileWithAddedExtension) fail(Diagnostics.File_0_not_found, fileName + Extension.Ts); - return sourceFileWithAddedExtension; - } - } - - /** This has side effects through `findSourceFile`. */ - function processSourceFile(fileName: string, isDefaultLib: boolean, ignoreNoDefaultLib: boolean, packageId: PackageId | undefined, refFile?: RefFile): void { - getSourceFileFromReferenceWorker( - fileName, - fileName => findSourceFile(fileName, toPath(fileName), isDefaultLib, ignoreNoDefaultLib, refFile, packageId), // TODO: GH#18217 - (diagnostic, ...args) => fileProcessingDiagnostics.add( - createRefFileDiagnostic(refFile, diagnostic, ...args) - ), - refFile && refFile.file - ); - } - - function reportFileNamesDifferOnlyInCasingError(fileName: string, existingFile: SourceFile, refFile: RefFile | undefined): void { - const refs = !refFile ? refFileMap && refFileMap.get(existingFile.path) : undefined; - const refToReportErrorOn = refs && find(refs, ref => ref.referencedFileName === existingFile.fileName); - fileProcessingDiagnostics.add(refToReportErrorOn ? - createFileDiagnosticAtReference( - refToReportErrorOn, - Diagnostics.Already_included_file_name_0_differs_from_file_name_1_only_in_casing, - existingFile.fileName, - fileName, - ) : - createRefFileDiagnostic( - refFile, - Diagnostics.File_name_0_differs_from_already_included_file_name_1_only_in_casing, - fileName, - existingFile.fileName - ) - ); - } - - function createRedirectSourceFile(redirectTarget: SourceFile, unredirected: SourceFile, fileName: string, path: Path, resolvedPath: Path, originalFileName: string): SourceFile { - const redirect: SourceFile = Object.create(redirectTarget); - redirect.fileName = fileName; - redirect.path = path; - redirect.resolvedPath = resolvedPath; - redirect.originalFileName = originalFileName; - redirect.redirectInfo = { redirectTarget, unredirected }; + const redirect = getProjectReferenceOutputName(redirectProject, fileName); + fileName = redirect; + // Once we start redirecting to a file, we can potentially come back to it + // via a back-reference from another file in the .d.ts folder. If that happens we'll + // end up trying to add it to the program *again* because we were tracking it via its + // original (un-redirected) name. So we have to map both the original path and the redirected path + // to the source file we're about to find/create + redirectedPath = toPath(redirect); + } + } + // We haven't looked for this file, do so now and cache result + const file = host.getSourceFile(fileName, (options.target!), hostErrorMessage => fileProcessingDiagnostics.add(createRefFileDiagnostic(refFile, Diagnostics.Cannot_read_file_0_Colon_1, fileName, hostErrorMessage)), shouldCreateNewSourceFile); + if (packageId) { + const packageIdKey = packageIdToString(packageId); + const fileFromPackageId = packageIdToSourceFile.get(packageIdKey); + if (fileFromPackageId) { + // Some other SourceFile already exists with this package name and version. + // Instead of creating a duplicate, just redirect to the existing one. + const dupFile = createRedirectSourceFile(fileFromPackageId, file!, fileName, path, toPath(fileName), originalFileName); // TODO: GH#18217 + redirectTargetsMap.add(fileFromPackageId.path, fileName); + addFileToFilesByName(dupFile, path, redirectedPath); + sourceFileToPackageName.set(path, packageId.name); + processingOtherFiles!.push(dupFile); + return dupFile; + } + else if (file) { + // This is the first source file to have this packageId. + packageIdToSourceFile.set(packageIdKey, file); + sourceFileToPackageName.set(path, packageId.name); + } + } + addFileToFilesByName(file, path, redirectedPath); + if (file) { sourceFilesFoundSearchingNodeModules.set(path, currentNodeModulesDepth > 0); - Object.defineProperties(redirect, { - id: { - get(this: SourceFile) { return this.redirectInfo!.redirectTarget.id; }, - set(this: SourceFile, value: SourceFile["id"]) { this.redirectInfo!.redirectTarget.id = value; }, - }, - symbol: { - get(this: SourceFile) { return this.redirectInfo!.redirectTarget.symbol; }, - set(this: SourceFile, value: SourceFile["symbol"]) { this.redirectInfo!.redirectTarget.symbol = value; }, - }, + file.fileName = fileName; // Ensure that source file has same name as what we were looking for + file.path = path; + file.resolvedPath = toPath(fileName); + file.originalFileName = originalFileName; + addFileToRefFileMap(fileName, file, refFile); + if (host.useCaseSensitiveFileNames()) { + const pathLowerCase = toFileNameLowerCase(path); + // for case-sensitive file systems check if we've already seen some file with similar filename ignoring case + const existingFile = filesByNameIgnoreCase!.get(pathLowerCase); + if (existingFile) { + reportFileNamesDifferOnlyInCasingError(fileName, existingFile, refFile); + } + else { + filesByNameIgnoreCase!.set(pathLowerCase, file); + } + } + skipDefaultLib = skipDefaultLib || (file.hasNoDefaultLib && !ignoreNoDefaultLib); + if (!options.noResolve) { + processReferencedFiles(file, isDefaultLib); + processTypeReferenceDirectives(file); + } + if (!options.noLib) { + processLibReferenceDirectives(file); + } + // always process imported modules to record module name resolutions + processImportedModules(file); + if (isDefaultLib) { + processingDefaultLibFiles!.push(file); + } + else { + processingOtherFiles!.push(file); + } + } + return file; + } + function addFileToRefFileMap(referencedFileName: string, file: SourceFile | undefined, refFile: RefFile | undefined) { + if (refFile && file) { + (refFileMap || (refFileMap = createMultiMap())).add(file.path, { + referencedFileName, + kind: refFile.kind, + index: refFile.index, + file: refFile.file.path }); - return redirect; - } - - // Get source file from normalized fileName - function findSourceFile(fileName: string, path: Path, isDefaultLib: boolean, ignoreNoDefaultLib: boolean, refFile: RefFile | undefined, packageId: PackageId | undefined): SourceFile | undefined { - if (useSourceOfProjectReferenceRedirect) { - let source = getSourceOfProjectReferenceRedirect(fileName); - // If preserveSymlinks is true, module resolution wont jump the symlink - // but the resolved real path may be the .d.ts from project reference - // Note:: Currently we try the real path only if the - // file is from node_modules to avoid having to run real path on all file paths - if (!source && - host.realpath && - options.preserveSymlinks && - isDeclarationFileName(fileName) && - stringContains(fileName, nodeModulesPathPart)) { - const realPath = host.realpath(fileName); - if (realPath !== fileName) source = getSourceOfProjectReferenceRedirect(realPath); - } - if (source) { - const file = isString(source) ? - findSourceFile(source, toPath(source), isDefaultLib, ignoreNoDefaultLib, refFile, packageId) : - undefined; - if (file) addFileToFilesByName(file, path, /*redirectedPath*/ undefined); - return file; - } - } - const originalFileName = fileName; - if (filesByName.has(path)) { - const file = filesByName.get(path); - addFileToRefFileMap(fileName, file || undefined, refFile); - // try to check if we've already seen this file but with a different casing in path - // NOTE: this only makes sense for case-insensitive file systems, and only on files which are not redirected - if (file && options.forceConsistentCasingInFileNames) { - const checkedName = file.fileName; - const isRedirect = toPath(checkedName) !== toPath(fileName); - if (isRedirect) { - fileName = getProjectReferenceRedirect(fileName) || fileName; - } - // Check if it differs only in drive letters its ok to ignore that error: - const checkedAbsolutePath = getNormalizedAbsolutePathWithoutRoot(checkedName, currentDirectory); - const inputAbsolutePath = getNormalizedAbsolutePathWithoutRoot(fileName, currentDirectory); - if (checkedAbsolutePath !== inputAbsolutePath) { - reportFileNamesDifferOnlyInCasingError(fileName, file, refFile); - } + } + } + function addFileToFilesByName(file: SourceFile | undefined, path: Path, redirectedPath: Path | undefined) { + if (redirectedPath) { + filesByName.set(redirectedPath, file); + filesByName.set(path, file || false); + } + else { + filesByName.set(path, file); + } + } + function getProjectReferenceRedirect(fileName: string): string | undefined { + const referencedProject = getProjectReferenceRedirectProject(fileName); + return referencedProject && getProjectReferenceOutputName(referencedProject, fileName); + } + function getProjectReferenceRedirectProject(fileName: string) { + // Ignore dts or any json files + if (!resolvedProjectReferences || !resolvedProjectReferences.length || fileExtensionIs(fileName, Extension.Dts) || fileExtensionIs(fileName, Extension.Json)) { + return undefined; + } + // If this file is produced by a referenced project, we need to rewrite it to + // look in the output folder of the referenced project rather than the input + return getResolvedProjectReferenceToRedirect(fileName); + } + function getProjectReferenceOutputName(referencedProject: ResolvedProjectReference, fileName: string) { + const out = referencedProject.commandLine.options.outFile || referencedProject.commandLine.options.out; + return out ? + changeExtension(out, Extension.Dts) : + getOutputDeclarationFileName(fileName, referencedProject.commandLine, !host.useCaseSensitiveFileNames()); + } + /** + * Get the referenced project if the file is input file from that reference project + */ + function getResolvedProjectReferenceToRedirect(fileName: string) { + if (mapFromFileToProjectReferenceRedirects === undefined) { + mapFromFileToProjectReferenceRedirects = createMap(); + forEachResolvedProjectReference((referencedProject, referenceProjectPath) => { + // not input file from the referenced project, ignore + if (referencedProject && + toPath(options.configFilePath!) !== referenceProjectPath) { + referencedProject.commandLine.fileNames.forEach(f => mapFromFileToProjectReferenceRedirects!.set(toPath(f), referenceProjectPath)); } - - // If the file was previously found via a node_modules search, but is now being processed as a root file, - // then everything it sucks in may also be marked incorrectly, and needs to be checked again. - if (file && sourceFilesFoundSearchingNodeModules.get(file.path) && currentNodeModulesDepth === 0) { - sourceFilesFoundSearchingNodeModules.set(file.path, false); - if (!options.noResolve) { - processReferencedFiles(file, isDefaultLib); - processTypeReferenceDirectives(file); + }); + } + const referencedProjectPath = mapFromFileToProjectReferenceRedirects.get(toPath(fileName)); + return referencedProjectPath && getResolvedProjectReferenceByPath(referencedProjectPath); + } + function forEachResolvedProjectReference(cb: (resolvedProjectReference: ResolvedProjectReference | undefined, resolvedProjectReferencePath: Path) => T | undefined): T | undefined { + return forEachProjectReference(projectReferences, resolvedProjectReferences, (resolvedRef, index, parent) => { + const ref = (parent ? parent.commandLine.projectReferences : projectReferences)![index]; + const resolvedRefPath = toPath(resolveProjectReferencePath(ref)); + return cb(resolvedRef, resolvedRefPath); + }); + } + function getSourceOfProjectReferenceRedirect(file: string) { + if (!isDeclarationFileName(file)) + return undefined; + if (mapFromToProjectReferenceRedirectSource === undefined) { + mapFromToProjectReferenceRedirectSource = createMap(); + forEachResolvedProjectReference(resolvedRef => { + if (resolvedRef) { + const out = resolvedRef.commandLine.options.outFile || resolvedRef.commandLine.options.out; + if (out) { + // Dont know which source file it means so return true? + const outputDts = changeExtension(out, Extension.Dts); + mapFromToProjectReferenceRedirectSource!.set(toPath(outputDts), true); } - if (!options.noLib) { - processLibReferenceDirectives(file); + else { + forEach(resolvedRef.commandLine.fileNames, fileName => { + if (!fileExtensionIs(fileName, Extension.Dts) && !fileExtensionIs(fileName, Extension.Json)) { + const outputDts = getOutputDeclarationFileName(fileName, resolvedRef.commandLine, host.useCaseSensitiveFileNames()); + mapFromToProjectReferenceRedirectSource!.set(toPath(outputDts), fileName); + } + }); } - - modulesWithElidedImports.set(file.path, false); - processImportedModules(file); } - // See if we need to reprocess the imports due to prior skipped imports - else if (file && modulesWithElidedImports.get(file.path)) { - if (currentNodeModulesDepth < maxNodeModuleJsDepth) { - modulesWithElidedImports.set(file.path, false); - processImportedModules(file); - } + }); + } + return mapFromToProjectReferenceRedirectSource.get(toPath(file)); + } + function isSourceOfProjectReferenceRedirect(fileName: string) { + return useSourceOfProjectReferenceRedirect && !!getResolvedProjectReferenceToRedirect(fileName); + } + function forEachProjectReference(projectReferences: readonly ProjectReference[] | undefined, resolvedProjectReferences: readonly (ResolvedProjectReference | undefined)[] | undefined, cbResolvedRef: (resolvedRef: ResolvedProjectReference | undefined, index: number, parent: ResolvedProjectReference | undefined) => T | undefined, cbRef?: (projectReferences: readonly ProjectReference[] | undefined, parent: ResolvedProjectReference | undefined) => T | undefined): T | undefined { + let seenResolvedRefs: ResolvedProjectReference[] | undefined; + return worker(projectReferences, resolvedProjectReferences, /*parent*/ undefined, cbResolvedRef, cbRef); + function worker(projectReferences: readonly ProjectReference[] | undefined, resolvedProjectReferences: readonly (ResolvedProjectReference | undefined)[] | undefined, parent: ResolvedProjectReference | undefined, cbResolvedRef: (resolvedRef: ResolvedProjectReference | undefined, index: number, parent: ResolvedProjectReference | undefined) => T | undefined, cbRef?: (projectReferences: readonly ProjectReference[] | undefined, parent: ResolvedProjectReference | undefined) => T | undefined): T | undefined { + // Visit project references first + if (cbRef) { + const result = cbRef(projectReferences, parent); + if (result) { + return result; + } + } + return forEach(resolvedProjectReferences, (resolvedRef, index) => { + if (contains(seenResolvedRefs, resolvedRef)) { + // ignore recursives + return undefined; } - - return file || undefined; - } - - let redirectedPath: Path | undefined; - if (refFile && !useSourceOfProjectReferenceRedirect) { - const redirectProject = getProjectReferenceRedirectProject(fileName); - if (redirectProject) { - if (redirectProject.commandLine.options.outFile || redirectProject.commandLine.options.out) { - // Shouldnt create many to 1 mapping file in --out scenario - return undefined; - } - const redirect = getProjectReferenceOutputName(redirectProject, fileName); - fileName = redirect; - // Once we start redirecting to a file, we can potentially come back to it - // via a back-reference from another file in the .d.ts folder. If that happens we'll - // end up trying to add it to the program *again* because we were tracking it via its - // original (un-redirected) name. So we have to map both the original path and the redirected path - // to the source file we're about to find/create - redirectedPath = toPath(redirect); - } - } - - // We haven't looked for this file, do so now and cache result - const file = host.getSourceFile( - fileName, - options.target!, - hostErrorMessage => fileProcessingDiagnostics.add(createRefFileDiagnostic( - refFile, - Diagnostics.Cannot_read_file_0_Colon_1, - fileName, - hostErrorMessage - )), - shouldCreateNewSourceFile - ); - - if (packageId) { - const packageIdKey = packageIdToString(packageId); - const fileFromPackageId = packageIdToSourceFile.get(packageIdKey); - if (fileFromPackageId) { - // Some other SourceFile already exists with this package name and version. - // Instead of creating a duplicate, just redirect to the existing one. - const dupFile = createRedirectSourceFile(fileFromPackageId, file!, fileName, path, toPath(fileName), originalFileName); // TODO: GH#18217 - redirectTargetsMap.add(fileFromPackageId.path, fileName); - addFileToFilesByName(dupFile, path, redirectedPath); - sourceFileToPackageName.set(path, packageId.name); - processingOtherFiles!.push(dupFile); - return dupFile; - } - else if (file) { - // This is the first source file to have this packageId. - packageIdToSourceFile.set(packageIdKey, file); - sourceFileToPackageName.set(path, packageId.name); - } - } - addFileToFilesByName(file, path, redirectedPath); - - if (file) { - sourceFilesFoundSearchingNodeModules.set(path, currentNodeModulesDepth > 0); - file.fileName = fileName; // Ensure that source file has same name as what we were looking for - file.path = path; - file.resolvedPath = toPath(fileName); - file.originalFileName = originalFileName; - addFileToRefFileMap(fileName, file, refFile); - - if (host.useCaseSensitiveFileNames()) { - const pathLowerCase = toFileNameLowerCase(path); - // for case-sensitive file systems check if we've already seen some file with similar filename ignoring case - const existingFile = filesByNameIgnoreCase!.get(pathLowerCase); - if (existingFile) { - reportFileNamesDifferOnlyInCasingError(fileName, existingFile, refFile); - } - else { - filesByNameIgnoreCase!.set(pathLowerCase, file); + const result = cbResolvedRef(resolvedRef, index, parent); + if (result) { + return result; + } + if (!resolvedRef) + return undefined; + (seenResolvedRefs || (seenResolvedRefs = [])).push(resolvedRef); + return worker(resolvedRef.commandLine.projectReferences, resolvedRef.references, resolvedRef, cbResolvedRef, cbRef); + }); + } + } + function getResolvedProjectReferenceByPath(projectReferencePath: Path): ResolvedProjectReference | undefined { + if (!projectReferenceRedirects) { + return undefined; + } + return projectReferenceRedirects.get(projectReferencePath) || undefined; + } + function processReferencedFiles(file: SourceFile, isDefaultLib: boolean) { + forEach(file.referencedFiles, (ref, index) => { + const referencedFileName = resolveTripleslashReference(ref.fileName, file.originalFileName); + processSourceFile(referencedFileName, isDefaultLib, + /*ignoreNoDefaultLib*/ false, + /*packageId*/ undefined, { + kind: RefFileKind.ReferenceFile, + index, + file, + pos: ref.pos, + end: ref.end + }); + }); + } + function processTypeReferenceDirectives(file: SourceFile) { + // We lower-case all type references because npm automatically lowercases all packages. See GH#9824. + const typeDirectives = map(file.typeReferenceDirectives, ref => toFileNameLowerCase(ref.fileName)); + if (!typeDirectives) { + return; + } + const resolutions = resolveTypeReferenceDirectiveNamesWorker(typeDirectives, file.originalFileName, getResolvedProjectReferenceToRedirect(file.originalFileName)); + for (let i = 0; i < typeDirectives.length; i++) { + const ref = file.typeReferenceDirectives[i]; + const resolvedTypeReferenceDirective = resolutions[i]; + // store resolved type directive on the file + const fileName = toFileNameLowerCase(ref.fileName); + setResolvedTypeReferenceDirective(file, fileName, resolvedTypeReferenceDirective); + processTypeReferenceDirective(fileName, resolvedTypeReferenceDirective, { + kind: RefFileKind.TypeReferenceDirective, + index: i, + file, + pos: ref.pos, + end: ref.end + }); + } + } + function processTypeReferenceDirective(typeReferenceDirective: string, resolvedTypeReferenceDirective?: ResolvedTypeReferenceDirective, refFile?: RefFile): void { + // If we already found this library as a primary reference - nothing to do + const previousResolution = resolvedTypeReferenceDirectives.get(typeReferenceDirective); + if (previousResolution && previousResolution.primary) { + return; + } + let saveResolution = true; + if (resolvedTypeReferenceDirective) { + if (resolvedTypeReferenceDirective.isExternalLibraryImport) + currentNodeModulesDepth++; + if (resolvedTypeReferenceDirective.primary) { + // resolved from the primary path + processSourceFile(resolvedTypeReferenceDirective.resolvedFileName!, /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, resolvedTypeReferenceDirective.packageId, refFile); // TODO: GH#18217 + } + else { + // If we already resolved to this file, it must have been a secondary reference. Check file contents + // for sameness and possibly issue an error + if (previousResolution) { + // Don't bother reading the file again if it's the same file. + if (resolvedTypeReferenceDirective.resolvedFileName !== previousResolution.resolvedFileName) { + const otherFileText = host.readFile(resolvedTypeReferenceDirective.resolvedFileName!); + const existingFile = getSourceFile(previousResolution.resolvedFileName!)!; + if (otherFileText !== existingFile.text) { + // Try looking up ref for original file + const refs = !refFile ? refFileMap && refFileMap.get(existingFile.path) : undefined; + const refToReportErrorOn = refs && find(refs, ref => ref.referencedFileName === existingFile.fileName); + fileProcessingDiagnostics.add(refToReportErrorOn ? + createFileDiagnosticAtReference(refToReportErrorOn, Diagnostics.Conflicting_definitions_for_0_found_at_1_and_2_Consider_installing_a_specific_version_of_this_library_to_resolve_the_conflict, typeReferenceDirective, resolvedTypeReferenceDirective.resolvedFileName, previousResolution.resolvedFileName) : + createRefFileDiagnostic(refFile, Diagnostics.Conflicting_definitions_for_0_found_at_1_and_2_Consider_installing_a_specific_version_of_this_library_to_resolve_the_conflict, typeReferenceDirective, resolvedTypeReferenceDirective.resolvedFileName, previousResolution.resolvedFileName)); + } } - } - - skipDefaultLib = skipDefaultLib || (file.hasNoDefaultLib && !ignoreNoDefaultLib); - - if (!options.noResolve) { - processReferencedFiles(file, isDefaultLib); - processTypeReferenceDirectives(file); - } - if (!options.noLib) { - processLibReferenceDirectives(file); - } - - - // always process imported modules to record module name resolutions - processImportedModules(file); - - if (isDefaultLib) { - processingDefaultLibFiles!.push(file); + // don't overwrite previous resolution result + saveResolution = false; } else { - processingOtherFiles!.push(file); + // First resolution of this library + processSourceFile(resolvedTypeReferenceDirective.resolvedFileName!, /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, resolvedTypeReferenceDirective.packageId, refFile); } } - return file; + if (resolvedTypeReferenceDirective.isExternalLibraryImport) + currentNodeModulesDepth--; } - - function addFileToRefFileMap(referencedFileName: string, file: SourceFile | undefined, refFile: RefFile | undefined) { - if (refFile && file) { - (refFileMap || (refFileMap = createMultiMap())).add(file.path, { - referencedFileName, - kind: refFile.kind, - index: refFile.index, - file: refFile.file.path - }); - } + else { + fileProcessingDiagnostics.add(createRefFileDiagnostic(refFile, Diagnostics.Cannot_find_type_definition_file_for_0, typeReferenceDirective)); } - - function addFileToFilesByName(file: SourceFile | undefined, path: Path, redirectedPath: Path | undefined) { - if (redirectedPath) { - filesByName.set(redirectedPath, file); - filesByName.set(path, file || false); + if (saveResolution) { + resolvedTypeReferenceDirectives.set(typeReferenceDirective, resolvedTypeReferenceDirective); + } + } + function processLibReferenceDirectives(file: SourceFile) { + forEach(file.libReferenceDirectives, libReference => { + const libName = toFileNameLowerCase(libReference.fileName); + const libFileName = libMap.get(libName); + if (libFileName) { + // we ignore any 'no-default-lib' reference set on this file. + processRootFile(combinePaths(defaultLibraryPath, libFileName), /*isDefaultLib*/ true, /*ignoreNoDefaultLib*/ true); } else { - filesByName.set(path, file); + const unqualifiedLibName = removeSuffix(removePrefix(libName, "lib."), ".d.ts"); + const suggestion = getSpellingSuggestion(unqualifiedLibName, libs, identity); + const message = suggestion ? Diagnostics.Cannot_find_lib_definition_for_0_Did_you_mean_1 : Diagnostics.Cannot_find_lib_definition_for_0; + fileProcessingDiagnostics.add(createFileDiagnostic(file, libReference.pos, libReference.end - libReference.pos, message, libName, suggestion)); } + }); + } + function createRefFileDiagnostic(refFile: RefFile | undefined, message: DiagnosticMessage, ...args: any[]): Diagnostic { + if (!refFile) { + return createCompilerDiagnostic(message, ...args); } - - function getProjectReferenceRedirect(fileName: string): string | undefined { - const referencedProject = getProjectReferenceRedirectProject(fileName); - return referencedProject && getProjectReferenceOutputName(referencedProject, fileName); + else { + return createFileDiagnostic(refFile.file, refFile.pos, refFile.end - refFile.pos, message, ...args); } - - function getProjectReferenceRedirectProject(fileName: string) { - // Ignore dts or any json files - if (!resolvedProjectReferences || !resolvedProjectReferences.length || fileExtensionIs(fileName, Extension.Dts) || fileExtensionIs(fileName, Extension.Json)) { - return undefined; + } + function getCanonicalFileName(fileName: string): string { + return host.getCanonicalFileName(fileName); + } + function processImportedModules(file: SourceFile) { + collectExternalModuleReferences(file); + if (file.imports.length || file.moduleAugmentations.length) { + // Because global augmentation doesn't have string literal name, we can check for global augmentation as such. + const moduleNames = getModuleNames(file); + const resolutions = resolveModuleNamesReusingOldState(moduleNames, getNormalizedAbsolutePath(file.originalFileName, currentDirectory), file); + Debug.assert(resolutions.length === moduleNames.length); + for (let i = 0; i < moduleNames.length; i++) { + const resolution = resolutions[i]; + setResolvedModule(file, moduleNames[i], resolution); + if (!resolution) { + continue; + } + const isFromNodeModulesSearch = resolution.isExternalLibraryImport; + const isJsFile = !resolutionExtensionIsTSOrJson(resolution.extension); + const isJsFileFromNodeModules = isFromNodeModulesSearch && isJsFile; + const resolvedFileName = resolution.resolvedFileName; + if (isFromNodeModulesSearch) { + currentNodeModulesDepth++; + } + // add file to program only if: + // - resolution was successful + // - noResolve is falsy + // - module name comes from the list of imports + // - it's not a top level JavaScript module that exceeded the search max + const elideImport = isJsFileFromNodeModules && currentNodeModulesDepth > maxNodeModuleJsDepth; + // Don't add the file if it has a bad extension (e.g. 'tsx' if we don't have '--allowJs') + // This may still end up being an untyped module -- the file won't be included but imports will be allowed. + const shouldAddFile = resolvedFileName + && !getResolutionDiagnostic(options, resolution) + && !options.noResolve + && i < file.imports.length + && !elideImport + && !(isJsFile && !options.allowJs) + && (isInJSFile(file.imports[i]) || !(file.imports[i].flags & NodeFlags.JSDoc)); + if (elideImport) { + modulesWithElidedImports.set(file.path, true); + } + else if (shouldAddFile) { + const path = toPath(resolvedFileName); + const pos = skipTrivia(file.text, file.imports[i].pos); + findSourceFile(resolvedFileName, path, + /*isDefaultLib*/ false, + /*ignoreNoDefaultLib*/ false, { + kind: RefFileKind.Import, + index: i, + file, + pos, + end: file.imports[i].end + }, resolution.packageId); + } + if (isFromNodeModulesSearch) { + currentNodeModulesDepth--; + } } - - // If this file is produced by a referenced project, we need to rewrite it to - // look in the output folder of the referenced project rather than the input - return getResolvedProjectReferenceToRedirect(fileName); - } - - - function getProjectReferenceOutputName(referencedProject: ResolvedProjectReference, fileName: string) { - const out = referencedProject.commandLine.options.outFile || referencedProject.commandLine.options.out; - return out ? - changeExtension(out, Extension.Dts) : - getOutputDeclarationFileName(fileName, referencedProject.commandLine, !host.useCaseSensitiveFileNames()); } - - /** - * Get the referenced project if the file is input file from that reference project - */ - function getResolvedProjectReferenceToRedirect(fileName: string) { - if (mapFromFileToProjectReferenceRedirects === undefined) { - mapFromFileToProjectReferenceRedirects = createMap(); - forEachResolvedProjectReference((referencedProject, referenceProjectPath) => { - // not input file from the referenced project, ignore - if (referencedProject && - toPath(options.configFilePath!) !== referenceProjectPath) { - referencedProject.commandLine.fileNames.forEach(f => - mapFromFileToProjectReferenceRedirects!.set(toPath(f), referenceProjectPath)); - } - }); - } - - const referencedProjectPath = mapFromFileToProjectReferenceRedirects.get(toPath(fileName)); - return referencedProjectPath && getResolvedProjectReferenceByPath(referencedProjectPath); - } - - function forEachResolvedProjectReference( - cb: (resolvedProjectReference: ResolvedProjectReference | undefined, resolvedProjectReferencePath: Path) => T | undefined - ): T | undefined { - return forEachProjectReference(projectReferences, resolvedProjectReferences, (resolvedRef, index, parent) => { - const ref = (parent ? parent.commandLine.projectReferences : projectReferences)![index]; - const resolvedRefPath = toPath(resolveProjectReferencePath(ref)); - return cb(resolvedRef, resolvedRefPath); - }); + else { + // no imports - drop cached module resolutions + file.resolvedModules = undefined; } - - function getSourceOfProjectReferenceRedirect(file: string) { - if (!isDeclarationFileName(file)) return undefined; - if (mapFromToProjectReferenceRedirectSource === undefined) { - mapFromToProjectReferenceRedirectSource = createMap(); - forEachResolvedProjectReference(resolvedRef => { - if (resolvedRef) { - const out = resolvedRef.commandLine.options.outFile || resolvedRef.commandLine.options.out; - if (out) { - // Dont know which source file it means so return true? - const outputDts = changeExtension(out, Extension.Dts); - mapFromToProjectReferenceRedirectSource!.set(toPath(outputDts), true); - } - else { - forEach(resolvedRef.commandLine.fileNames, fileName => { - if (!fileExtensionIs(fileName, Extension.Dts) && !fileExtensionIs(fileName, Extension.Json)) { - const outputDts = getOutputDeclarationFileName(fileName, resolvedRef.commandLine, host.useCaseSensitiveFileNames()); - mapFromToProjectReferenceRedirectSource!.set(toPath(outputDts), fileName); - } - }); - } - } - }); - } - return mapFromToProjectReferenceRedirectSource.get(toPath(file)); - } - - function isSourceOfProjectReferenceRedirect(fileName: string) { - return useSourceOfProjectReferenceRedirect && !!getResolvedProjectReferenceToRedirect(fileName); - } - - function forEachProjectReference( - projectReferences: readonly ProjectReference[] | undefined, - resolvedProjectReferences: readonly (ResolvedProjectReference | undefined)[] | undefined, - cbResolvedRef: (resolvedRef: ResolvedProjectReference | undefined, index: number, parent: ResolvedProjectReference | undefined) => T | undefined, - cbRef?: (projectReferences: readonly ProjectReference[] | undefined, parent: ResolvedProjectReference | undefined) => T | undefined - ): T | undefined { - let seenResolvedRefs: ResolvedProjectReference[] | undefined; - - return worker(projectReferences, resolvedProjectReferences, /*parent*/ undefined, cbResolvedRef, cbRef); - - function worker( - projectReferences: readonly ProjectReference[] | undefined, - resolvedProjectReferences: readonly (ResolvedProjectReference | undefined)[] | undefined, - parent: ResolvedProjectReference | undefined, - cbResolvedRef: (resolvedRef: ResolvedProjectReference | undefined, index: number, parent: ResolvedProjectReference | undefined) => T | undefined, - cbRef?: (projectReferences: readonly ProjectReference[] | undefined, parent: ResolvedProjectReference | undefined) => T | undefined, - ): T | undefined { - - // Visit project references first - if (cbRef) { - const result = cbRef(projectReferences, parent); - if (result) { return result; } - } - - return forEach(resolvedProjectReferences, (resolvedRef, index) => { - if (contains(seenResolvedRefs, resolvedRef)) { - // ignore recursives - return undefined; - } - - const result = cbResolvedRef(resolvedRef, index, parent); - if (result) { - return result; - } - - if (!resolvedRef) return undefined; - - (seenResolvedRefs || (seenResolvedRefs = [])).push(resolvedRef); - return worker(resolvedRef.commandLine.projectReferences, resolvedRef.references, resolvedRef, cbResolvedRef, cbRef); - }); + } + function computeCommonSourceDirectory(sourceFiles: SourceFile[]): string { + const fileNames = mapDefined(sourceFiles, file => file.isDeclarationFile ? undefined : file.fileName); + return computeCommonSourceDirectoryOfFilenames(fileNames, currentDirectory, getCanonicalFileName); + } + function checkSourceFilesBelongToPath(sourceFiles: readonly SourceFile[], rootDirectory: string): boolean { + let allFilesBelongToPath = true; + const absoluteRootDirectoryPath = host.getCanonicalFileName(getNormalizedAbsolutePath(rootDirectory, currentDirectory)); + let rootPaths: ts.Map | undefined; + for (const sourceFile of sourceFiles) { + if (!sourceFile.isDeclarationFile) { + const absoluteSourceFilePath = host.getCanonicalFileName(getNormalizedAbsolutePath(sourceFile.fileName, currentDirectory)); + if (absoluteSourceFilePath.indexOf(absoluteRootDirectoryPath) !== 0) { + if (!rootPaths) + rootPaths = arrayToSet(rootNames, toPath); + addProgramDiagnosticAtRefPath(sourceFile, rootPaths, Diagnostics.File_0_is_not_under_rootDir_1_rootDir_is_expected_to_contain_all_source_files, sourceFile.fileName, rootDirectory); + allFilesBelongToPath = false; + } + } + } + return allFilesBelongToPath; + } + function parseProjectReferenceConfigFile(ref: ProjectReference): ResolvedProjectReference | undefined { + if (!projectReferenceRedirects) { + projectReferenceRedirects = createMap(); + } + // The actual filename (i.e. add "/tsconfig.json" if necessary) + const refPath = resolveProjectReferencePath(ref); + const sourceFilePath = toPath(refPath); + const fromCache = projectReferenceRedirects.get(sourceFilePath); + if (fromCache !== undefined) { + return fromCache || undefined; + } + let commandLine: ParsedCommandLine | undefined; + let sourceFile: JsonSourceFile | undefined; + if (host.getParsedCommandLine) { + commandLine = host.getParsedCommandLine(refPath); + if (!commandLine) { + addFileToFilesByName(/*sourceFile*/ undefined, sourceFilePath, /*redirectedPath*/ undefined); + projectReferenceRedirects.set(sourceFilePath, false); + return undefined; } + sourceFile = Debug.checkDefined(commandLine.options.configFile); + Debug.assert(!sourceFile.path || sourceFile.path === sourceFilePath); + addFileToFilesByName(sourceFile, sourceFilePath, /*redirectedPath*/ undefined); } - - function getResolvedProjectReferenceByPath(projectReferencePath: Path): ResolvedProjectReference | undefined { - if (!projectReferenceRedirects) { + else { + // An absolute path pointing to the containing directory of the config file + const basePath = getNormalizedAbsolutePath(getDirectoryPath(refPath), host.getCurrentDirectory()); + sourceFile = (host.getSourceFile(refPath, ScriptTarget.JSON) as JsonSourceFile | undefined); + addFileToFilesByName(sourceFile, sourceFilePath, /*redirectedPath*/ undefined); + if (sourceFile === undefined) { + projectReferenceRedirects.set(sourceFilePath, false); return undefined; } - - return projectReferenceRedirects.get(projectReferencePath) || undefined; - } - - function processReferencedFiles(file: SourceFile, isDefaultLib: boolean) { - forEach(file.referencedFiles, (ref, index) => { - const referencedFileName = resolveTripleslashReference(ref.fileName, file.originalFileName); - processSourceFile( - referencedFileName, - isDefaultLib, - /*ignoreNoDefaultLib*/ false, - /*packageId*/ undefined, - { - kind: RefFileKind.ReferenceFile, - index, - file, - pos: ref.pos, - end: ref.end - } - ); - }); + commandLine = parseJsonSourceFileConfigFileContent(sourceFile, configParsingHost, basePath, /*existingOptions*/ undefined, refPath); } - - function processTypeReferenceDirectives(file: SourceFile) { - // We lower-case all type references because npm automatically lowercases all packages. See GH#9824. - const typeDirectives = map(file.typeReferenceDirectives, ref => toFileNameLowerCase(ref.fileName)); - if (!typeDirectives) { - return; + sourceFile.fileName = refPath; + sourceFile.path = sourceFilePath; + sourceFile.resolvedPath = sourceFilePath; + sourceFile.originalFileName = refPath; + const resolvedRef: ResolvedProjectReference = { commandLine, sourceFile }; + projectReferenceRedirects.set(sourceFilePath, resolvedRef); + if (commandLine.projectReferences) { + resolvedRef.references = commandLine.projectReferences.map(parseProjectReferenceConfigFile); + } + return resolvedRef; + } + function verifyCompilerOptions() { + if (options.strictPropertyInitialization && !getStrictOptionValue(options, "strictNullChecks")) { + createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1, "strictPropertyInitialization", "strictNullChecks"); + } + if (options.isolatedModules) { + if (options.out) { + createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_with_option_1, "out", "isolatedModules"); } - - const resolutions = resolveTypeReferenceDirectiveNamesWorker(typeDirectives, file.originalFileName, getResolvedProjectReferenceToRedirect(file.originalFileName)); - - for (let i = 0; i < typeDirectives.length; i++) { - const ref = file.typeReferenceDirectives[i]; - const resolvedTypeReferenceDirective = resolutions[i]; - // store resolved type directive on the file - const fileName = toFileNameLowerCase(ref.fileName); - setResolvedTypeReferenceDirective(file, fileName, resolvedTypeReferenceDirective); - processTypeReferenceDirective( - fileName, - resolvedTypeReferenceDirective, - { - kind: RefFileKind.TypeReferenceDirective, - index: i, - file, - pos: ref.pos, - end: ref.end - } - ); - } - } - - function processTypeReferenceDirective( - typeReferenceDirective: string, - resolvedTypeReferenceDirective?: ResolvedTypeReferenceDirective, - refFile?: RefFile - ): void { - - // If we already found this library as a primary reference - nothing to do - const previousResolution = resolvedTypeReferenceDirectives.get(typeReferenceDirective); - if (previousResolution && previousResolution.primary) { - return; + if (options.outFile) { + createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_with_option_1, "outFile", "isolatedModules"); } - let saveResolution = true; - if (resolvedTypeReferenceDirective) { - if (resolvedTypeReferenceDirective.isExternalLibraryImport) currentNodeModulesDepth++; - - if (resolvedTypeReferenceDirective.primary) { - // resolved from the primary path - processSourceFile(resolvedTypeReferenceDirective.resolvedFileName!, /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, resolvedTypeReferenceDirective.packageId, refFile); // TODO: GH#18217 - } - else { - // If we already resolved to this file, it must have been a secondary reference. Check file contents - // for sameness and possibly issue an error - if (previousResolution) { - // Don't bother reading the file again if it's the same file. - if (resolvedTypeReferenceDirective.resolvedFileName !== previousResolution.resolvedFileName) { - const otherFileText = host.readFile(resolvedTypeReferenceDirective.resolvedFileName!); - const existingFile = getSourceFile(previousResolution.resolvedFileName!)!; - if (otherFileText !== existingFile.text) { - // Try looking up ref for original file - const refs = !refFile ? refFileMap && refFileMap.get(existingFile.path) : undefined; - const refToReportErrorOn = refs && find(refs, ref => ref.referencedFileName === existingFile.fileName); - fileProcessingDiagnostics.add( - refToReportErrorOn ? - createFileDiagnosticAtReference( - refToReportErrorOn, - Diagnostics.Conflicting_definitions_for_0_found_at_1_and_2_Consider_installing_a_specific_version_of_this_library_to_resolve_the_conflict, - typeReferenceDirective, - resolvedTypeReferenceDirective.resolvedFileName, - previousResolution.resolvedFileName - ) : - createRefFileDiagnostic( - refFile, - Diagnostics.Conflicting_definitions_for_0_found_at_1_and_2_Consider_installing_a_specific_version_of_this_library_to_resolve_the_conflict, - typeReferenceDirective, - resolvedTypeReferenceDirective.resolvedFileName, - previousResolution.resolvedFileName - ) - ); - } - } - // don't overwrite previous resolution result - saveResolution = false; - } - else { - // First resolution of this library - processSourceFile(resolvedTypeReferenceDirective.resolvedFileName!, /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, resolvedTypeReferenceDirective.packageId, refFile); - } - } - - if (resolvedTypeReferenceDirective.isExternalLibraryImport) currentNodeModulesDepth--; + } + if (options.inlineSourceMap) { + if (options.sourceMap) { + createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_with_option_1, "sourceMap", "inlineSourceMap"); } - else { - fileProcessingDiagnostics.add(createRefFileDiagnostic( - refFile, - Diagnostics.Cannot_find_type_definition_file_for_0, - typeReferenceDirective - )); - } - - if (saveResolution) { - resolvedTypeReferenceDirectives.set(typeReferenceDirective, resolvedTypeReferenceDirective); - } - } - - function processLibReferenceDirectives(file: SourceFile) { - forEach(file.libReferenceDirectives, libReference => { - const libName = toFileNameLowerCase(libReference.fileName); - const libFileName = libMap.get(libName); - if (libFileName) { - // we ignore any 'no-default-lib' reference set on this file. - processRootFile(combinePaths(defaultLibraryPath, libFileName), /*isDefaultLib*/ true, /*ignoreNoDefaultLib*/ true); - } - else { - const unqualifiedLibName = removeSuffix(removePrefix(libName, "lib."), ".d.ts"); - const suggestion = getSpellingSuggestion(unqualifiedLibName, libs, identity); - const message = suggestion ? Diagnostics.Cannot_find_lib_definition_for_0_Did_you_mean_1 : Diagnostics.Cannot_find_lib_definition_for_0; - fileProcessingDiagnostics.add(createFileDiagnostic( - file, - libReference.pos, - libReference.end - libReference.pos, - message, - libName, - suggestion - )); - } - }); + if (options.mapRoot) { + createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_with_option_1, "mapRoot", "inlineSourceMap"); + } + } + if (options.paths && options.baseUrl === undefined) { + createDiagnosticForOptionName(Diagnostics.Option_paths_cannot_be_used_without_specifying_baseUrl_option, "paths"); } - - function createRefFileDiagnostic(refFile: RefFile | undefined, message: DiagnosticMessage, ...args: any[]): Diagnostic { - if (!refFile) { - return createCompilerDiagnostic(message, ...args); + if (options.composite) { + if (options.declaration === false) { + createDiagnosticForOptionName(Diagnostics.Composite_projects_may_not_disable_declaration_emit, "declaration"); } - else { - return createFileDiagnostic(refFile.file, refFile.pos, refFile.end - refFile.pos, message, ...args); - } - } - - function getCanonicalFileName(fileName: string): string { - return host.getCanonicalFileName(fileName); - } - - function processImportedModules(file: SourceFile) { - collectExternalModuleReferences(file); - if (file.imports.length || file.moduleAugmentations.length) { - // Because global augmentation doesn't have string literal name, we can check for global augmentation as such. - const moduleNames = getModuleNames(file); - const resolutions = resolveModuleNamesReusingOldState(moduleNames, getNormalizedAbsolutePath(file.originalFileName, currentDirectory), file); - Debug.assert(resolutions.length === moduleNames.length); - for (let i = 0; i < moduleNames.length; i++) { - const resolution = resolutions[i]; - setResolvedModule(file, moduleNames[i], resolution); - - if (!resolution) { - continue; - } - - const isFromNodeModulesSearch = resolution.isExternalLibraryImport; - const isJsFile = !resolutionExtensionIsTSOrJson(resolution.extension); - const isJsFileFromNodeModules = isFromNodeModulesSearch && isJsFile; - const resolvedFileName = resolution.resolvedFileName; - - if (isFromNodeModulesSearch) { - currentNodeModulesDepth++; - } - - // add file to program only if: - // - resolution was successful - // - noResolve is falsy - // - module name comes from the list of imports - // - it's not a top level JavaScript module that exceeded the search max - const elideImport = isJsFileFromNodeModules && currentNodeModulesDepth > maxNodeModuleJsDepth; - // Don't add the file if it has a bad extension (e.g. 'tsx' if we don't have '--allowJs') - // This may still end up being an untyped module -- the file won't be included but imports will be allowed. - const shouldAddFile = resolvedFileName - && !getResolutionDiagnostic(options, resolution) - && !options.noResolve - && i < file.imports.length - && !elideImport - && !(isJsFile && !options.allowJs) - && (isInJSFile(file.imports[i]) || !(file.imports[i].flags & NodeFlags.JSDoc)); - - if (elideImport) { - modulesWithElidedImports.set(file.path, true); - } - else if (shouldAddFile) { - const path = toPath(resolvedFileName); - const pos = skipTrivia(file.text, file.imports[i].pos); - findSourceFile( - resolvedFileName, - path, - /*isDefaultLib*/ false, - /*ignoreNoDefaultLib*/ false, - { - kind: RefFileKind.Import, - index: i, - file, - pos, - end: file.imports[i].end - }, - resolution.packageId - ); - } - - if (isFromNodeModulesSearch) { - currentNodeModulesDepth--; - } - } + if (options.incremental === false) { + createDiagnosticForOptionName(Diagnostics.Composite_projects_may_not_disable_incremental_compilation, "declaration"); } - else { - // no imports - drop cached module resolutions - file.resolvedModules = undefined; - } - } - - function computeCommonSourceDirectory(sourceFiles: SourceFile[]): string { - const fileNames = mapDefined(sourceFiles, file => file.isDeclarationFile ? undefined : file.fileName); - return computeCommonSourceDirectoryOfFilenames(fileNames, currentDirectory, getCanonicalFileName); - } - - function checkSourceFilesBelongToPath(sourceFiles: readonly SourceFile[], rootDirectory: string): boolean { - let allFilesBelongToPath = true; - const absoluteRootDirectoryPath = host.getCanonicalFileName(getNormalizedAbsolutePath(rootDirectory, currentDirectory)); - let rootPaths: Map | undefined; - - for (const sourceFile of sourceFiles) { - if (!sourceFile.isDeclarationFile) { - const absoluteSourceFilePath = host.getCanonicalFileName(getNormalizedAbsolutePath(sourceFile.fileName, currentDirectory)); - if (absoluteSourceFilePath.indexOf(absoluteRootDirectoryPath) !== 0) { - if (!rootPaths) rootPaths = arrayToSet(rootNames, toPath); - addProgramDiagnosticAtRefPath( - sourceFile, - rootPaths, - Diagnostics.File_0_is_not_under_rootDir_1_rootDir_is_expected_to_contain_all_source_files, - sourceFile.fileName, - rootDirectory - ); - allFilesBelongToPath = false; - } - } + } + if (options.tsBuildInfoFile) { + if (!isIncrementalCompilation(options)) { + createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1_or_option_2, "tsBuildInfoFile", "incremental", "composite"); } - - return allFilesBelongToPath; - } - - function parseProjectReferenceConfigFile(ref: ProjectReference): ResolvedProjectReference | undefined { - if (!projectReferenceRedirects) { - projectReferenceRedirects = createMap(); - } - - // The actual filename (i.e. add "/tsconfig.json" if necessary) - const refPath = resolveProjectReferencePath(ref); - const sourceFilePath = toPath(refPath); - const fromCache = projectReferenceRedirects.get(sourceFilePath); - if (fromCache !== undefined) { - return fromCache || undefined; - } - - let commandLine: ParsedCommandLine | undefined; - let sourceFile: JsonSourceFile | undefined; - if (host.getParsedCommandLine) { - commandLine = host.getParsedCommandLine(refPath); - if (!commandLine) { - addFileToFilesByName(/*sourceFile*/ undefined, sourceFilePath, /*redirectedPath*/ undefined); - projectReferenceRedirects.set(sourceFilePath, false); - return undefined; + } + else if (options.incremental && !options.outFile && !options.out && !options.configFilePath) { + programDiagnostics.add(createCompilerDiagnostic(Diagnostics.Option_incremental_can_only_be_specified_using_tsconfig_emitting_to_single_file_or_when_option_tsBuildInfoFile_is_specified)); + } + if (!options.listFilesOnly && options.noEmit && isIncrementalCompilation(options)) { + createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_with_option_1, "noEmit", options.incremental ? "incremental" : "composite"); + } + verifyProjectReferences(); + // List of collected files is complete; validate exhautiveness if this is a project with a file list + if (options.composite) { + const rootPaths = arrayToSet(rootNames, toPath); + for (const file of files) { + // Ignore file that is not emitted + if (sourceFileMayBeEmitted(file, program) && !rootPaths.has(file.path)) { + addProgramDiagnosticAtRefPath(file, rootPaths, Diagnostics.File_0_is_not_listed_within_the_file_list_of_project_1_Projects_must_list_all_files_or_use_an_include_pattern, file.fileName, options.configFilePath || ""); } - sourceFile = Debug.checkDefined(commandLine.options.configFile); - Debug.assert(!sourceFile.path || sourceFile.path === sourceFilePath); - addFileToFilesByName(sourceFile, sourceFilePath, /*redirectedPath*/ undefined); } - else { - // An absolute path pointing to the containing directory of the config file - const basePath = getNormalizedAbsolutePath(getDirectoryPath(refPath), host.getCurrentDirectory()); - sourceFile = host.getSourceFile(refPath, ScriptTarget.JSON) as JsonSourceFile | undefined; - addFileToFilesByName(sourceFile, sourceFilePath, /*redirectedPath*/ undefined); - if (sourceFile === undefined) { - projectReferenceRedirects.set(sourceFilePath, false); - return undefined; + } + if (options.paths) { + for (const key in options.paths) { + if (!hasProperty(options.paths, key)) { + continue; } - commandLine = parseJsonSourceFileConfigFileContent(sourceFile, configParsingHost, basePath, /*existingOptions*/ undefined, refPath); - } - sourceFile.fileName = refPath; - sourceFile.path = sourceFilePath; - sourceFile.resolvedPath = sourceFilePath; - sourceFile.originalFileName = refPath; - - const resolvedRef: ResolvedProjectReference = { commandLine, sourceFile }; - projectReferenceRedirects.set(sourceFilePath, resolvedRef); - if (commandLine.projectReferences) { - resolvedRef.references = commandLine.projectReferences.map(parseProjectReferenceConfigFile); - } - return resolvedRef; - } - - function verifyCompilerOptions() { - if (options.strictPropertyInitialization && !getStrictOptionValue(options, "strictNullChecks")) { - createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1, "strictPropertyInitialization", "strictNullChecks"); - } - - if (options.isolatedModules) { - if (options.out) { - createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_with_option_1, "out", "isolatedModules"); - } - - if (options.outFile) { - createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_with_option_1, "outFile", "isolatedModules"); - } - } - - if (options.inlineSourceMap) { - if (options.sourceMap) { - createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_with_option_1, "sourceMap", "inlineSourceMap"); - } - if (options.mapRoot) { - createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_with_option_1, "mapRoot", "inlineSourceMap"); - } - } - - if (options.paths && options.baseUrl === undefined) { - createDiagnosticForOptionName(Diagnostics.Option_paths_cannot_be_used_without_specifying_baseUrl_option, "paths"); - } - - if (options.composite) { - if (options.declaration === false) { - createDiagnosticForOptionName(Diagnostics.Composite_projects_may_not_disable_declaration_emit, "declaration"); - } - if (options.incremental === false) { - createDiagnosticForOptionName(Diagnostics.Composite_projects_may_not_disable_incremental_compilation, "declaration"); - } - } - - if (options.tsBuildInfoFile) { - if (!isIncrementalCompilation(options)) { - createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1_or_option_2, "tsBuildInfoFile", "incremental", "composite"); - } - } - else if (options.incremental && !options.outFile && !options.out && !options.configFilePath) { - programDiagnostics.add(createCompilerDiagnostic(Diagnostics.Option_incremental_can_only_be_specified_using_tsconfig_emitting_to_single_file_or_when_option_tsBuildInfoFile_is_specified)); - } - - if (!options.listFilesOnly && options.noEmit && isIncrementalCompilation(options)) { - createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_with_option_1, "noEmit", options.incremental ? "incremental" : "composite"); - } - - verifyProjectReferences(); - - // List of collected files is complete; validate exhautiveness if this is a project with a file list - if (options.composite) { - const rootPaths = arrayToSet(rootNames, toPath); - for (const file of files) { - // Ignore file that is not emitted - if (sourceFileMayBeEmitted(file, program) && !rootPaths.has(file.path)) { - addProgramDiagnosticAtRefPath( - file, - rootPaths, - Diagnostics.File_0_is_not_listed_within_the_file_list_of_project_1_Projects_must_list_all_files_or_use_an_include_pattern, - file.fileName, - options.configFilePath || "" - ); - } + if (!hasZeroOrOneAsteriskCharacter(key)) { + createDiagnosticForOptionPaths(/*onKey*/ true, key, Diagnostics.Pattern_0_can_have_at_most_one_Asterisk_character, key); } - } - - if (options.paths) { - for (const key in options.paths) { - if (!hasProperty(options.paths, key)) { - continue; + if (isArray(options.paths[key])) { + const len = options.paths[key].length; + if (len === 0) { + createDiagnosticForOptionPaths(/*onKey*/ false, key, Diagnostics.Substitutions_for_pattern_0_shouldn_t_be_an_empty_array, key); } - if (!hasZeroOrOneAsteriskCharacter(key)) { - createDiagnosticForOptionPaths(/*onKey*/ true, key, Diagnostics.Pattern_0_can_have_at_most_one_Asterisk_character, key); - } - if (isArray(options.paths[key])) { - const len = options.paths[key].length; - if (len === 0) { - createDiagnosticForOptionPaths(/*onKey*/ false, key, Diagnostics.Substitutions_for_pattern_0_shouldn_t_be_an_empty_array, key); - } - for (let i = 0; i < len; i++) { - const subst = options.paths[key][i]; - const typeOfSubst = typeof subst; - if (typeOfSubst === "string") { - if (!hasZeroOrOneAsteriskCharacter(subst)) { - createDiagnosticForOptionPathKeyValue(key, i, Diagnostics.Substitution_0_in_pattern_1_can_have_at_most_one_Asterisk_character, subst, key); - } - } - else { - createDiagnosticForOptionPathKeyValue(key, i, Diagnostics.Substitution_0_for_pattern_1_has_incorrect_type_expected_string_got_2, subst, key, typeOfSubst); + for (let i = 0; i < len; i++) { + const subst = options.paths[key][i]; + const typeOfSubst = typeof subst; + if (typeOfSubst === "string") { + if (!hasZeroOrOneAsteriskCharacter(subst)) { + createDiagnosticForOptionPathKeyValue(key, i, Diagnostics.Substitution_0_in_pattern_1_can_have_at_most_one_Asterisk_character, subst, key); } } + else { + createDiagnosticForOptionPathKeyValue(key, i, Diagnostics.Substitution_0_for_pattern_1_has_incorrect_type_expected_string_got_2, subst, key, typeOfSubst); + } } - else { - createDiagnosticForOptionPaths(/*onKey*/ false, key, Diagnostics.Substitutions_for_pattern_0_should_be_an_array, key); - } - } - } - - if (!options.sourceMap && !options.inlineSourceMap) { - if (options.inlineSources) { - createDiagnosticForOptionName(Diagnostics.Option_0_can_only_be_used_when_either_option_inlineSourceMap_or_option_sourceMap_is_provided, "inlineSources"); } - if (options.sourceRoot) { - createDiagnosticForOptionName(Diagnostics.Option_0_can_only_be_used_when_either_option_inlineSourceMap_or_option_sourceMap_is_provided, "sourceRoot"); + else { + createDiagnosticForOptionPaths(/*onKey*/ false, key, Diagnostics.Substitutions_for_pattern_0_should_be_an_array, key); } } - - if (options.out && options.outFile) { - createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_with_option_1, "out", "outFile"); + } + if (!options.sourceMap && !options.inlineSourceMap) { + if (options.inlineSources) { + createDiagnosticForOptionName(Diagnostics.Option_0_can_only_be_used_when_either_option_inlineSourceMap_or_option_sourceMap_is_provided, "inlineSources"); } - - if (options.mapRoot && !(options.sourceMap || options.declarationMap)) { - // Error to specify --mapRoot without --sourcemap - createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1_or_option_2, "mapRoot", "sourceMap", "declarationMap"); + if (options.sourceRoot) { + createDiagnosticForOptionName(Diagnostics.Option_0_can_only_be_used_when_either_option_inlineSourceMap_or_option_sourceMap_is_provided, "sourceRoot"); } - - if (options.declarationDir) { - if (!getEmitDeclarations(options)) { - createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1_or_option_2, "declarationDir", "declaration", "composite"); - } - if (options.out || options.outFile) { - createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_with_option_1, "declarationDir", options.out ? "out" : "outFile"); - } + } + if (options.out && options.outFile) { + createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_with_option_1, "out", "outFile"); + } + if (options.mapRoot && !(options.sourceMap || options.declarationMap)) { + // Error to specify --mapRoot without --sourcemap + createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1_or_option_2, "mapRoot", "sourceMap", "declarationMap"); + } + if (options.declarationDir) { + if (!getEmitDeclarations(options)) { + createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1_or_option_2, "declarationDir", "declaration", "composite"); } - - if (options.declarationMap && !getEmitDeclarations(options)) { - createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1_or_option_2, "declarationMap", "declaration", "composite"); + if (options.out || options.outFile) { + createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_with_option_1, "declarationDir", options.out ? "out" : "outFile"); } - - if (options.lib && options.noLib) { - createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_with_option_1, "lib", "noLib"); + } + if (options.declarationMap && !getEmitDeclarations(options)) { + createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1_or_option_2, "declarationMap", "declaration", "composite"); + } + if (options.lib && options.noLib) { + createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_with_option_1, "lib", "noLib"); + } + if (options.noImplicitUseStrict && getStrictOptionValue(options, "alwaysStrict")) { + createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_with_option_1, "noImplicitUseStrict", "alwaysStrict"); + } + const languageVersion = options.target || ScriptTarget.ES3; + const outFile = options.outFile || options.out; + const firstNonAmbientExternalModuleSourceFile = find(files, f => isExternalModule(f) && !f.isDeclarationFile); + if (options.isolatedModules) { + if (options.module === ModuleKind.None && languageVersion < ScriptTarget.ES2015) { + createDiagnosticForOptionName(Diagnostics.Option_isolatedModules_can_only_be_used_when_either_option_module_is_provided_or_option_target_is_ES2015_or_higher, "isolatedModules", "target"); } - - if (options.noImplicitUseStrict && getStrictOptionValue(options, "alwaysStrict")) { - createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_with_option_1, "noImplicitUseStrict", "alwaysStrict"); + const firstNonExternalModuleSourceFile = find(files, f => !isExternalModule(f) && !isSourceFileJS(f) && !f.isDeclarationFile && f.scriptKind !== ScriptKind.JSON); + if (firstNonExternalModuleSourceFile) { + const span = getErrorSpanForNode(firstNonExternalModuleSourceFile, firstNonExternalModuleSourceFile); + programDiagnostics.add(createFileDiagnostic(firstNonExternalModuleSourceFile, span.start, span.length, Diagnostics.All_files_must_be_modules_when_the_isolatedModules_flag_is_provided)); } - - const languageVersion = options.target || ScriptTarget.ES3; - const outFile = options.outFile || options.out; - - const firstNonAmbientExternalModuleSourceFile = find(files, f => isExternalModule(f) && !f.isDeclarationFile); - if (options.isolatedModules) { - if (options.module === ModuleKind.None && languageVersion < ScriptTarget.ES2015) { - createDiagnosticForOptionName(Diagnostics.Option_isolatedModules_can_only_be_used_when_either_option_module_is_provided_or_option_target_is_ES2015_or_higher, "isolatedModules", "target"); - } - - const firstNonExternalModuleSourceFile = find(files, f => !isExternalModule(f) && !isSourceFileJS(f) && !f.isDeclarationFile && f.scriptKind !== ScriptKind.JSON); - if (firstNonExternalModuleSourceFile) { - const span = getErrorSpanForNode(firstNonExternalModuleSourceFile, firstNonExternalModuleSourceFile); - programDiagnostics.add(createFileDiagnostic(firstNonExternalModuleSourceFile, span.start, span.length, Diagnostics.All_files_must_be_modules_when_the_isolatedModules_flag_is_provided)); - } + } + else if (firstNonAmbientExternalModuleSourceFile && languageVersion < ScriptTarget.ES2015 && options.module === ModuleKind.None) { + // We cannot use createDiagnosticFromNode because nodes do not have parents yet + const span = getErrorSpanForNode(firstNonAmbientExternalModuleSourceFile, (firstNonAmbientExternalModuleSourceFile.externalModuleIndicator!)); + programDiagnostics.add(createFileDiagnostic(firstNonAmbientExternalModuleSourceFile, span.start, span.length, Diagnostics.Cannot_use_imports_exports_or_module_augmentations_when_module_is_none)); + } + // Cannot specify module gen that isn't amd or system with --out + if (outFile && !options.emitDeclarationOnly) { + if (options.module && !(options.module === ModuleKind.AMD || options.module === ModuleKind.System)) { + createDiagnosticForOptionName(Diagnostics.Only_amd_and_system_modules_are_supported_alongside_0, options.out ? "out" : "outFile", "module"); } - else if (firstNonAmbientExternalModuleSourceFile && languageVersion < ScriptTarget.ES2015 && options.module === ModuleKind.None) { - // We cannot use createDiagnosticFromNode because nodes do not have parents yet - const span = getErrorSpanForNode(firstNonAmbientExternalModuleSourceFile, firstNonAmbientExternalModuleSourceFile.externalModuleIndicator!); - programDiagnostics.add(createFileDiagnostic(firstNonAmbientExternalModuleSourceFile, span.start, span.length, Diagnostics.Cannot_use_imports_exports_or_module_augmentations_when_module_is_none)); + else if (options.module === undefined && firstNonAmbientExternalModuleSourceFile) { + const span = getErrorSpanForNode(firstNonAmbientExternalModuleSourceFile, (firstNonAmbientExternalModuleSourceFile.externalModuleIndicator!)); + programDiagnostics.add(createFileDiagnostic(firstNonAmbientExternalModuleSourceFile, span.start, span.length, Diagnostics.Cannot_compile_modules_using_option_0_unless_the_module_flag_is_amd_or_system, options.out ? "out" : "outFile")); } - - // Cannot specify module gen that isn't amd or system with --out - if (outFile && !options.emitDeclarationOnly) { - if (options.module && !(options.module === ModuleKind.AMD || options.module === ModuleKind.System)) { - createDiagnosticForOptionName(Diagnostics.Only_amd_and_system_modules_are_supported_alongside_0, options.out ? "out" : "outFile", "module"); - } - else if (options.module === undefined && firstNonAmbientExternalModuleSourceFile) { - const span = getErrorSpanForNode(firstNonAmbientExternalModuleSourceFile, firstNonAmbientExternalModuleSourceFile.externalModuleIndicator!); - programDiagnostics.add(createFileDiagnostic(firstNonAmbientExternalModuleSourceFile, span.start, span.length, Diagnostics.Cannot_compile_modules_using_option_0_unless_the_module_flag_is_amd_or_system, options.out ? "out" : "outFile")); - } + } + if (options.resolveJsonModule) { + if (getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeJs) { + createDiagnosticForOptionName(Diagnostics.Option_resolveJsonModule_cannot_be_specified_without_node_module_resolution_strategy, "resolveJsonModule"); } - - if (options.resolveJsonModule) { - if (getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeJs) { - createDiagnosticForOptionName(Diagnostics.Option_resolveJsonModule_cannot_be_specified_without_node_module_resolution_strategy, "resolveJsonModule"); - } - // Any emit other than common js, amd, es2015 or esnext is error - else if (!hasJsonModuleEmitEnabled(options)) { - createDiagnosticForOptionName(Diagnostics.Option_resolveJsonModule_can_only_be_specified_when_module_code_generation_is_commonjs_amd_es2015_or_esNext, "resolveJsonModule", "module"); - } + // Any emit other than common js, amd, es2015 or esnext is error + else if (!hasJsonModuleEmitEnabled(options)) { + createDiagnosticForOptionName(Diagnostics.Option_resolveJsonModule_can_only_be_specified_when_module_code_generation_is_commonjs_amd_es2015_or_esNext, "resolveJsonModule", "module"); } - - // there has to be common source directory if user specified --outdir || --sourceRoot - // if user specified --mapRoot, there needs to be common source directory if there would be multiple files being emitted - if (options.outDir || // there is --outDir specified - options.sourceRoot || // there is --sourceRoot specified - options.mapRoot) { // there is --mapRoot specified - - // Precalculate and cache the common source directory - const dir = getCommonSourceDirectory(); - - // If we failed to find a good common directory, but outDir is specified and at least one of our files is on a windows drive/URL/other resource, add a failure - if (options.outDir && dir === "" && files.some(file => getRootLength(file.fileName) > 1)) { - createDiagnosticForOptionName(Diagnostics.Cannot_find_the_common_subdirectory_path_for_the_input_files, "outDir"); - } + } + // there has to be common source directory if user specified --outdir || --sourceRoot + // if user specified --mapRoot, there needs to be common source directory if there would be multiple files being emitted + if (options.outDir || // there is --outDir specified + options.sourceRoot || // there is --sourceRoot specified + options.mapRoot) { // there is --mapRoot specified + // Precalculate and cache the common source directory + const dir = getCommonSourceDirectory(); + // If we failed to find a good common directory, but outDir is specified and at least one of our files is on a windows drive/URL/other resource, add a failure + if (options.outDir && dir === "" && files.some(file => getRootLength(file.fileName) > 1)) { + createDiagnosticForOptionName(Diagnostics.Cannot_find_the_common_subdirectory_path_for_the_input_files, "outDir"); } - - if (options.useDefineForClassFields && languageVersion === ScriptTarget.ES3) { - createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_when_option_target_is_ES3, "useDefineForClassFields"); - } - - if (options.checkJs && !options.allowJs) { - programDiagnostics.add(createCompilerDiagnostic(Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1, "checkJs", "allowJs")); - } - - if (options.emitDeclarationOnly) { - if (!getEmitDeclarations(options)) { - createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1_or_option_2, "emitDeclarationOnly", "declaration", "composite"); - } - - if (options.noEmit) { - createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_with_option_1, "emitDeclarationOnly", "noEmit"); - } - } - - if (options.emitDecoratorMetadata && - !options.experimentalDecorators) { - createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1, "emitDecoratorMetadata", "experimentalDecorators"); - } - - if (options.jsxFactory) { - if (options.reactNamespace) { - createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_with_option_1, "reactNamespace", "jsxFactory"); - } - if (!parseIsolatedEntityName(options.jsxFactory, languageVersion)) { - createOptionValueDiagnostic("jsxFactory", Diagnostics.Invalid_value_for_jsxFactory_0_is_not_a_valid_identifier_or_qualified_name, options.jsxFactory); - } - } - else if (options.reactNamespace && !isIdentifierText(options.reactNamespace, languageVersion)) { - createOptionValueDiagnostic("reactNamespace", Diagnostics.Invalid_value_for_reactNamespace_0_is_not_a_valid_identifier, options.reactNamespace); - } - - // If the emit is enabled make sure that every output file is unique and not overwriting any of the input files - if (!options.noEmit && !options.suppressOutputPathCheck) { - const emitHost = getEmitHost(); - const emitFilesSeen = createMap(); - forEachEmittedFile(emitHost, (emitFileNames) => { - if (!options.emitDeclarationOnly) { - verifyEmitFilePath(emitFileNames.jsFilePath, emitFilesSeen); - } - verifyEmitFilePath(emitFileNames.declarationFilePath, emitFilesSeen); - }); + } + if (options.useDefineForClassFields && languageVersion === ScriptTarget.ES3) { + createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_when_option_target_is_ES3, "useDefineForClassFields"); + } + if (options.checkJs && !options.allowJs) { + programDiagnostics.add(createCompilerDiagnostic(Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1, "checkJs", "allowJs")); + } + if (options.emitDeclarationOnly) { + if (!getEmitDeclarations(options)) { + createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1_or_option_2, "emitDeclarationOnly", "declaration", "composite"); } - - // Verify that all the emit files are unique and don't overwrite input files - function verifyEmitFilePath(emitFileName: string | undefined, emitFilesSeen: Map) { - if (emitFileName) { - const emitFilePath = toPath(emitFileName); - // Report error if the output overwrites input file - if (filesByName.has(emitFilePath)) { - let chain: DiagnosticMessageChain | undefined; - if (!options.configFilePath) { - // The program is from either an inferred project or an external project - chain = chainDiagnosticMessages(/*details*/ undefined, Diagnostics.Adding_a_tsconfig_json_file_will_help_organize_projects_that_contain_both_TypeScript_and_JavaScript_files_Learn_more_at_https_Colon_Slash_Slashaka_ms_Slashtsconfig); - } - chain = chainDiagnosticMessages(chain, Diagnostics.Cannot_write_file_0_because_it_would_overwrite_input_file, emitFileName); - blockEmittingOfFile(emitFileName, createCompilerDiagnosticFromMessageChain(chain)); - } - - const emitFileKey = !host.useCaseSensitiveFileNames() ? toFileNameLowerCase(emitFilePath) : emitFilePath; - // Report error if multiple files write into same file - if (emitFilesSeen.has(emitFileKey)) { - // Already seen the same emit file - report error - blockEmittingOfFile(emitFileName, createCompilerDiagnostic(Diagnostics.Cannot_write_file_0_because_it_would_be_overwritten_by_multiple_input_files, emitFileName)); - } - else { - emitFilesSeen.set(emitFileKey, true); - } - } + if (options.noEmit) { + createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_with_option_1, "emitDeclarationOnly", "noEmit"); } } - - function createFileDiagnosticAtReference(refPathToReportErrorOn: ts.RefFile, message: DiagnosticMessage, ...args: (string | number | undefined)[]) { - const refFile = Debug.checkDefined(getSourceFileByPath(refPathToReportErrorOn.file)); - const { kind, index } = refPathToReportErrorOn; - let pos: number, end: number; - switch (kind) { - case RefFileKind.Import: - pos = skipTrivia(refFile.text, refFile.imports[index].pos); - end = refFile.imports[index].end; - break; - case RefFileKind.ReferenceFile: - ({ pos, end } = refFile.referencedFiles[index]); - break; - case RefFileKind.TypeReferenceDirective: - ({ pos, end } = refFile.typeReferenceDirectives[index]); - break; - default: - return Debug.assertNever(kind); - } - return createFileDiagnostic(refFile, pos, end - pos, message, ...args); - } - - function addProgramDiagnosticAtRefPath(file: SourceFile, rootPaths: Map, message: DiagnosticMessage, ...args: (string | number | undefined)[]) { - const refPaths = refFileMap && refFileMap.get(file.path); - const refPathToReportErrorOn = forEach(refPaths, refPath => rootPaths.has(refPath.file) ? refPath : undefined) || - elementAt(refPaths, 0); - programDiagnostics.add( - refPathToReportErrorOn ? - createFileDiagnosticAtReference(refPathToReportErrorOn, message, ...args) : - createCompilerDiagnostic(message, ...args) - ); - } - - function verifyProjectReferences() { - const buildInfoPath = !options.noEmit && !options.suppressOutputPathCheck ? getTsBuildInfoEmitOutputFilePath(options) : undefined; - forEachProjectReference(projectReferences, resolvedProjectReferences, (resolvedRef, index, parent) => { - const ref = (parent ? parent.commandLine.projectReferences : projectReferences)![index]; - const parentFile = parent && parent.sourceFile as JsonSourceFile; - if (!resolvedRef) { - createDiagnosticForReference(parentFile, index, Diagnostics.File_0_not_found, ref.path); - return; - } - const options = resolvedRef.commandLine.options; - if (!options.composite) { - // ok to not have composite if the current program is container only - const inputs = parent ? parent.commandLine.fileNames : rootNames; - if (inputs.length) { - createDiagnosticForReference(parentFile, index, Diagnostics.Referenced_project_0_must_have_setting_composite_Colon_true, ref.path); - } + if (options.emitDecoratorMetadata && + !options.experimentalDecorators) { + createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_without_specifying_option_1, "emitDecoratorMetadata", "experimentalDecorators"); + } + if (options.jsxFactory) { + if (options.reactNamespace) { + createDiagnosticForOptionName(Diagnostics.Option_0_cannot_be_specified_with_option_1, "reactNamespace", "jsxFactory"); + } + if (!parseIsolatedEntityName(options.jsxFactory, languageVersion)) { + createOptionValueDiagnostic("jsxFactory", Diagnostics.Invalid_value_for_jsxFactory_0_is_not_a_valid_identifier_or_qualified_name, options.jsxFactory); + } + } + else if (options.reactNamespace && !isIdentifierText(options.reactNamespace, languageVersion)) { + createOptionValueDiagnostic("reactNamespace", Diagnostics.Invalid_value_for_reactNamespace_0_is_not_a_valid_identifier, options.reactNamespace); + } + // If the emit is enabled make sure that every output file is unique and not overwriting any of the input files + if (!options.noEmit && !options.suppressOutputPathCheck) { + const emitHost = getEmitHost(); + const emitFilesSeen = createMap(); + forEachEmittedFile(emitHost, (emitFileNames) => { + if (!options.emitDeclarationOnly) { + verifyEmitFilePath(emitFileNames.jsFilePath, emitFilesSeen); } - if (ref.prepend) { - const out = options.outFile || options.out; - if (out) { - if (!host.fileExists(out)) { - createDiagnosticForReference(parentFile, index, Diagnostics.Output_file_0_from_project_1_does_not_exist, out, ref.path); - } - } - else { - createDiagnosticForReference(parentFile, index, Diagnostics.Cannot_prepend_project_0_because_it_does_not_have_outFile_set, ref.path); - } + verifyEmitFilePath(emitFileNames.declarationFilePath, emitFilesSeen); + }); + } + // Verify that all the emit files are unique and don't overwrite input files + function verifyEmitFilePath(emitFileName: string | undefined, emitFilesSeen: ts.Map) { + if (emitFileName) { + const emitFilePath = toPath(emitFileName); + // Report error if the output overwrites input file + if (filesByName.has(emitFilePath)) { + let chain: DiagnosticMessageChain | undefined; + if (!options.configFilePath) { + // The program is from either an inferred project or an external project + chain = chainDiagnosticMessages(/*details*/ undefined, Diagnostics.Adding_a_tsconfig_json_file_will_help_organize_projects_that_contain_both_TypeScript_and_JavaScript_files_Learn_more_at_https_Colon_Slash_Slashaka_ms_Slashtsconfig); + } + chain = chainDiagnosticMessages(chain, Diagnostics.Cannot_write_file_0_because_it_would_overwrite_input_file, emitFileName); + blockEmittingOfFile(emitFileName, createCompilerDiagnosticFromMessageChain(chain)); + } + const emitFileKey = !host.useCaseSensitiveFileNames() ? toFileNameLowerCase(emitFilePath) : emitFilePath; + // Report error if multiple files write into same file + if (emitFilesSeen.has(emitFileKey)) { + // Already seen the same emit file - report error + blockEmittingOfFile(emitFileName, createCompilerDiagnostic(Diagnostics.Cannot_write_file_0_because_it_would_be_overwritten_by_multiple_input_files, emitFileName)); } - if (!parent && buildInfoPath && buildInfoPath === getTsBuildInfoEmitOutputFilePath(options)) { - createDiagnosticForReference(parentFile, index, Diagnostics.Cannot_write_file_0_because_it_will_overwrite_tsbuildinfo_file_generated_by_referenced_project_1, buildInfoPath, ref.path); - hasEmitBlockingDiagnostics.set(toPath(buildInfoPath), true); + else { + emitFilesSeen.set(emitFileKey, true); } - }); + } } - - function createDiagnosticForOptionPathKeyValue(key: string, valueIndex: number, message: DiagnosticMessage, arg0: string | number, arg1: string | number, arg2?: string | number) { - let needCompilerDiagnostic = true; - const pathsSyntax = getOptionPathsSyntax(); - for (const pathProp of pathsSyntax) { - if (isObjectLiteralExpression(pathProp.initializer)) { - for (const keyProps of getPropertyAssignment(pathProp.initializer, key)) { - const initializer = keyProps.initializer; - if (isArrayLiteralExpression(initializer) && initializer.elements.length > valueIndex) { - programDiagnostics.add(createDiagnosticForNodeInSourceFile(options.configFile!, initializer.elements[valueIndex], message, arg0, arg1, arg2)); - needCompilerDiagnostic = false; - } + } + function createFileDiagnosticAtReference(refPathToReportErrorOn: ts.RefFile, message: DiagnosticMessage, ...args: (string | number | undefined)[]) { + const refFile = Debug.checkDefined(getSourceFileByPath(refPathToReportErrorOn.file)); + const { kind, index } = refPathToReportErrorOn; + let pos: number, end: number; + switch (kind) { + case RefFileKind.Import: + pos = skipTrivia(refFile.text, refFile.imports[index].pos); + end = refFile.imports[index].end; + break; + case RefFileKind.ReferenceFile: + ({ pos, end } = refFile.referencedFiles[index]); + break; + case RefFileKind.TypeReferenceDirective: + ({ pos, end } = refFile.typeReferenceDirectives[index]); + break; + default: + return Debug.assertNever(kind); + } + return createFileDiagnostic(refFile, pos, end - pos, message, ...args); + } + function addProgramDiagnosticAtRefPath(file: SourceFile, rootPaths: ts.Map, message: DiagnosticMessage, ...args: (string | number | undefined)[]) { + const refPaths = refFileMap && refFileMap.get(file.path); + const refPathToReportErrorOn = forEach(refPaths, refPath => rootPaths.has(refPath.file) ? refPath : undefined) || + elementAt(refPaths, 0); + programDiagnostics.add(refPathToReportErrorOn ? + createFileDiagnosticAtReference(refPathToReportErrorOn, message, ...args) : + createCompilerDiagnostic(message, ...args)); + } + function verifyProjectReferences() { + const buildInfoPath = !options.noEmit && !options.suppressOutputPathCheck ? getTsBuildInfoEmitOutputFilePath(options) : undefined; + forEachProjectReference(projectReferences, resolvedProjectReferences, (resolvedRef, index, parent) => { + const ref = (parent ? parent.commandLine.projectReferences : projectReferences)![index]; + const parentFile = parent && (parent.sourceFile as JsonSourceFile); + if (!resolvedRef) { + createDiagnosticForReference(parentFile, index, Diagnostics.File_0_not_found, ref.path); + return; + } + const options = resolvedRef.commandLine.options; + if (!options.composite) { + // ok to not have composite if the current program is container only + const inputs = parent ? parent.commandLine.fileNames : rootNames; + if (inputs.length) { + createDiagnosticForReference(parentFile, index, Diagnostics.Referenced_project_0_must_have_setting_composite_Colon_true, ref.path); + } + } + if (ref.prepend) { + const out = options.outFile || options.out; + if (out) { + if (!host.fileExists(out)) { + createDiagnosticForReference(parentFile, index, Diagnostics.Output_file_0_from_project_1_does_not_exist, out, ref.path); } } + else { + createDiagnosticForReference(parentFile, index, Diagnostics.Cannot_prepend_project_0_because_it_does_not_have_outFile_set, ref.path); + } } - - if (needCompilerDiagnostic) { - programDiagnostics.add(createCompilerDiagnostic(message, arg0, arg1, arg2)); + if (!parent && buildInfoPath && buildInfoPath === getTsBuildInfoEmitOutputFilePath(options)) { + createDiagnosticForReference(parentFile, index, Diagnostics.Cannot_write_file_0_because_it_will_overwrite_tsbuildinfo_file_generated_by_referenced_project_1, buildInfoPath, ref.path); + hasEmitBlockingDiagnostics.set(toPath(buildInfoPath), true); } - } - - function createDiagnosticForOptionPaths(onKey: boolean, key: string, message: DiagnosticMessage, arg0: string | number) { - let needCompilerDiagnostic = true; - const pathsSyntax = getOptionPathsSyntax(); - for (const pathProp of pathsSyntax) { - if (isObjectLiteralExpression(pathProp.initializer) && - createOptionDiagnosticInObjectLiteralSyntax( - pathProp.initializer, onKey, key, /*key2*/ undefined, - message, arg0)) { - needCompilerDiagnostic = false; + }); + } + function createDiagnosticForOptionPathKeyValue(key: string, valueIndex: number, message: DiagnosticMessage, arg0: string | number, arg1: string | number, arg2?: string | number) { + let needCompilerDiagnostic = true; + const pathsSyntax = getOptionPathsSyntax(); + for (const pathProp of pathsSyntax) { + if (isObjectLiteralExpression(pathProp.initializer)) { + for (const keyProps of getPropertyAssignment(pathProp.initializer, key)) { + const initializer = keyProps.initializer; + if (isArrayLiteralExpression(initializer) && initializer.elements.length > valueIndex) { + programDiagnostics.add(createDiagnosticForNodeInSourceFile((options.configFile!), initializer.elements[valueIndex], message, arg0, arg1, arg2)); + needCompilerDiagnostic = false; + } } } - if (needCompilerDiagnostic) { - programDiagnostics.add(createCompilerDiagnostic(message, arg0)); - } } - - function getOptionsSyntaxByName(name: string): object | undefined { - const compilerOptionsObjectLiteralSyntax = getCompilerOptionsObjectLiteralSyntax(); - if (compilerOptionsObjectLiteralSyntax) { - return getPropertyAssignment(compilerOptionsObjectLiteralSyntax, name); + if (needCompilerDiagnostic) { + programDiagnostics.add(createCompilerDiagnostic(message, arg0, arg1, arg2)); + } + } + function createDiagnosticForOptionPaths(onKey: boolean, key: string, message: DiagnosticMessage, arg0: string | number) { + let needCompilerDiagnostic = true; + const pathsSyntax = getOptionPathsSyntax(); + for (const pathProp of pathsSyntax) { + if (isObjectLiteralExpression(pathProp.initializer) && + createOptionDiagnosticInObjectLiteralSyntax(pathProp.initializer, onKey, key, /*key2*/ undefined, message, arg0)) { + needCompilerDiagnostic = false; } - return undefined; } - - function getOptionPathsSyntax(): PropertyAssignment[] { - return getOptionsSyntaxByName("paths") as PropertyAssignment[] || emptyArray; + if (needCompilerDiagnostic) { + programDiagnostics.add(createCompilerDiagnostic(message, arg0)); } - - function createDiagnosticForOptionName(message: DiagnosticMessage, option1: string, option2?: string, option3?: string) { - createDiagnosticForOption(/*onKey*/ true, option1, option2, message, option1, option2, option3); + } + function getOptionsSyntaxByName(name: string): object | undefined { + const compilerOptionsObjectLiteralSyntax = getCompilerOptionsObjectLiteralSyntax(); + if (compilerOptionsObjectLiteralSyntax) { + return getPropertyAssignment(compilerOptionsObjectLiteralSyntax, name); } - - function createOptionValueDiagnostic(option1: string, message: DiagnosticMessage, arg0: string) { - createDiagnosticForOption(/*onKey*/ false, option1, /*option2*/ undefined, message, arg0); + return undefined; + } + function getOptionPathsSyntax(): PropertyAssignment[] { + return (getOptionsSyntaxByName("paths") as PropertyAssignment[]) || emptyArray; + } + function createDiagnosticForOptionName(message: DiagnosticMessage, option1: string, option2?: string, option3?: string) { + createDiagnosticForOption(/*onKey*/ true, option1, option2, message, option1, option2, option3); + } + function createOptionValueDiagnostic(option1: string, message: DiagnosticMessage, arg0: string) { + createDiagnosticForOption(/*onKey*/ false, option1, /*option2*/ undefined, message, arg0); + } + function createDiagnosticForReference(sourceFile: JsonSourceFile | undefined, index: number, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number) { + const referencesSyntax = firstDefined(getTsConfigPropArray(sourceFile || options.configFile, "references"), property => isArrayLiteralExpression(property.initializer) ? property.initializer : undefined); + if (referencesSyntax && referencesSyntax.elements.length > index) { + programDiagnostics.add(createDiagnosticForNodeInSourceFile(sourceFile || options.configFile!, referencesSyntax.elements[index], message, arg0, arg1)); } - - function createDiagnosticForReference(sourceFile: JsonSourceFile | undefined, index: number, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number) { - const referencesSyntax = firstDefined(getTsConfigPropArray(sourceFile || options.configFile, "references"), - property => isArrayLiteralExpression(property.initializer) ? property.initializer : undefined); - if (referencesSyntax && referencesSyntax.elements.length > index) { - programDiagnostics.add(createDiagnosticForNodeInSourceFile(sourceFile || options.configFile!, referencesSyntax.elements[index], message, arg0, arg1)); - } - else { - programDiagnostics.add(createCompilerDiagnostic(message, arg0, arg1)); - } - } - - function createDiagnosticForOption(onKey: boolean, option1: string, option2: string | undefined, message: DiagnosticMessage, arg0: string | number, arg1?: string | number, arg2?: string | number) { - const compilerOptionsObjectLiteralSyntax = getCompilerOptionsObjectLiteralSyntax(); - const needCompilerDiagnostic = !compilerOptionsObjectLiteralSyntax || - !createOptionDiagnosticInObjectLiteralSyntax(compilerOptionsObjectLiteralSyntax, onKey, option1, option2, message, arg0, arg1, arg2); - - if (needCompilerDiagnostic) { - programDiagnostics.add(createCompilerDiagnostic(message, arg0, arg1, arg2)); - } - } - - function getCompilerOptionsObjectLiteralSyntax() { - if (_compilerOptionsObjectLiteralSyntax === undefined) { - _compilerOptionsObjectLiteralSyntax = null; // eslint-disable-line no-null/no-null - const jsonObjectLiteral = getTsConfigObjectLiteralExpression(options.configFile); - if (jsonObjectLiteral) { - for (const prop of getPropertyAssignment(jsonObjectLiteral, "compilerOptions")) { - if (isObjectLiteralExpression(prop.initializer)) { - _compilerOptionsObjectLiteralSyntax = prop.initializer; - break; - } + else { + programDiagnostics.add(createCompilerDiagnostic(message, arg0, arg1)); + } + } + function createDiagnosticForOption(onKey: boolean, option1: string, option2: string | undefined, message: DiagnosticMessage, arg0: string | number, arg1?: string | number, arg2?: string | number) { + const compilerOptionsObjectLiteralSyntax = getCompilerOptionsObjectLiteralSyntax(); + const needCompilerDiagnostic = !compilerOptionsObjectLiteralSyntax || + !createOptionDiagnosticInObjectLiteralSyntax(compilerOptionsObjectLiteralSyntax, onKey, option1, option2, message, arg0, arg1, arg2); + if (needCompilerDiagnostic) { + programDiagnostics.add(createCompilerDiagnostic(message, arg0, arg1, arg2)); + } + } + function getCompilerOptionsObjectLiteralSyntax() { + if (_compilerOptionsObjectLiteralSyntax === undefined) { + _compilerOptionsObjectLiteralSyntax = null; // eslint-disable-line no-null/no-null + const jsonObjectLiteral = getTsConfigObjectLiteralExpression(options.configFile); + if (jsonObjectLiteral) { + for (const prop of getPropertyAssignment(jsonObjectLiteral, "compilerOptions")) { + if (isObjectLiteralExpression(prop.initializer)) { + _compilerOptionsObjectLiteralSyntax = prop.initializer; + break; } } } - return _compilerOptionsObjectLiteralSyntax; } - - function createOptionDiagnosticInObjectLiteralSyntax(objectLiteral: ObjectLiteralExpression, onKey: boolean, key1: string, key2: string | undefined, message: DiagnosticMessage, arg0: string | number, arg1?: string | number, arg2?: string | number): boolean { - const props = getPropertyAssignment(objectLiteral, key1, key2); - for (const prop of props) { - programDiagnostics.add(createDiagnosticForNodeInSourceFile(options.configFile!, onKey ? prop.name : prop.initializer, message, arg0, arg1, arg2)); - } - return !!props.length; + return _compilerOptionsObjectLiteralSyntax; + } + function createOptionDiagnosticInObjectLiteralSyntax(objectLiteral: ObjectLiteralExpression, onKey: boolean, key1: string, key2: string | undefined, message: DiagnosticMessage, arg0: string | number, arg1?: string | number, arg2?: string | number): boolean { + const props = getPropertyAssignment(objectLiteral, key1, key2); + for (const prop of props) { + programDiagnostics.add(createDiagnosticForNodeInSourceFile((options.configFile!), onKey ? prop.name : prop.initializer, message, arg0, arg1, arg2)); } - - function blockEmittingOfFile(emitFileName: string, diag: Diagnostic) { - hasEmitBlockingDiagnostics.set(toPath(emitFileName), true); - programDiagnostics.add(diag); + return !!props.length; + } + function blockEmittingOfFile(emitFileName: string, diag: Diagnostic) { + hasEmitBlockingDiagnostics.set(toPath(emitFileName), true); + programDiagnostics.add(diag); + } + function isEmittedFile(file: string): boolean { + if (options.noEmit) { + return false; } - - function isEmittedFile(file: string): boolean { - if (options.noEmit) { - return false; - } - - // If this is source file, its not emitted file - const filePath = toPath(file); - if (getSourceFileByPath(filePath)) { - return false; - } - - // If options have --outFile or --out just check that - const out = options.outFile || options.out; - if (out) { - return isSameFile(filePath, out) || isSameFile(filePath, removeFileExtension(out) + Extension.Dts); - } - - // If declarationDir is specified, return if its a file in that directory - if (options.declarationDir && containsPath(options.declarationDir, filePath, currentDirectory, !host.useCaseSensitiveFileNames())) { - return true; - } - - // If --outDir, check if file is in that directory - if (options.outDir) { - return containsPath(options.outDir, filePath, currentDirectory, !host.useCaseSensitiveFileNames()); - } - - if (fileExtensionIsOneOf(filePath, supportedJSExtensions) || fileExtensionIs(filePath, Extension.Dts)) { - // Otherwise just check if sourceFile with the name exists - const filePathWithoutExtension = removeFileExtension(filePath); - return !!getSourceFileByPath((filePathWithoutExtension + Extension.Ts) as Path) || - !!getSourceFileByPath((filePathWithoutExtension + Extension.Tsx) as Path); - } + // If this is source file, its not emitted file + const filePath = toPath(file); + if (getSourceFileByPath(filePath)) { return false; } - - function isSameFile(file1: string, file2: string) { - return comparePaths(file1, file2, currentDirectory, !host.useCaseSensitiveFileNames()) === Comparison.EqualTo; + // If options have --outFile or --out just check that + const out = options.outFile || options.out; + if (out) { + return isSameFile(filePath, out) || isSameFile(filePath, removeFileExtension(out) + Extension.Dts); } - - function getProbableSymlinks(): ReadonlyMap { - if (host.getSymlinks) { - return host.getSymlinks(); - } - return symlinks || (symlinks = discoverProbableSymlinks( - files, - getCanonicalFileName, - host.getCurrentDirectory())); + // If declarationDir is specified, return if its a file in that directory + if (options.declarationDir && containsPath(options.declarationDir, filePath, currentDirectory, !host.useCaseSensitiveFileNames())) { + return true; } - } - - /*@internal*/ - export function handleNoEmitOptions(program: ProgramToEmitFilesAndReportErrors, sourceFile: SourceFile | undefined, cancellationToken: CancellationToken | undefined): EmitResult | undefined { - const options = program.getCompilerOptions(); - if (options.noEmit) { - return { diagnostics: emptyArray, sourceMaps: undefined, emittedFiles: undefined, emitSkipped: true }; - } - - // If the noEmitOnError flag is set, then check if we have any errors so far. If so, - // immediately bail out. Note that we pass 'undefined' for 'sourceFile' so that we - // get any preEmit diagnostics, not just the ones - if (!options.noEmitOnError) return undefined; - let diagnostics: readonly Diagnostic[] = [ - ...program.getOptionsDiagnostics(cancellationToken), - ...program.getSyntacticDiagnostics(sourceFile, cancellationToken), - ...program.getGlobalDiagnostics(cancellationToken), - ...program.getSemanticDiagnostics(sourceFile, cancellationToken) - ]; - - if (diagnostics.length === 0 && getEmitDeclarations(program.getCompilerOptions())) { - diagnostics = program.getDeclarationDiagnostics(/*sourceFile*/ undefined, cancellationToken); - } - - return diagnostics.length > 0 ? - { diagnostics, sourceMaps: undefined, emittedFiles: undefined, emitSkipped: true } : - undefined; - } - - /*@internal*/ - interface CompilerHostLike { - useCaseSensitiveFileNames(): boolean; - getCurrentDirectory(): string; - fileExists(fileName: string): boolean; - readFile(fileName: string): string | undefined; - readDirectory?(rootDir: string, extensions: readonly string[], excludes: readonly string[] | undefined, includes: readonly string[], depth?: number): string[]; - trace?(s: string): void; - onUnRecoverableConfigFileDiagnostic?: DiagnosticReporter; - } - - /* @internal */ - export function parseConfigHostFromCompilerHostLike(host: CompilerHostLike, directoryStructureHost: DirectoryStructureHost = host): ParseConfigFileHost { - return { - fileExists: f => directoryStructureHost.fileExists(f), - readDirectory(root, extensions, excludes, includes, depth) { - Debug.assertIsDefined(directoryStructureHost.readDirectory, "'CompilerHost.readDirectory' must be implemented to correctly process 'projectReferences'"); - return directoryStructureHost.readDirectory(root, extensions, excludes, includes, depth); - }, - readFile: f => directoryStructureHost.readFile(f), - useCaseSensitiveFileNames: host.useCaseSensitiveFileNames(), - getCurrentDirectory: () => host.getCurrentDirectory(), - onUnRecoverableConfigFileDiagnostic: host.onUnRecoverableConfigFileDiagnostic || returnUndefined, - trace: host.trace ? (s) => host.trace!(s) : undefined - }; - } - - // For backward compatibility - /** @deprecated */ export interface ResolveProjectReferencePathHost { - fileExists(fileName: string): boolean; - } - - /* @internal */ - export function createPrependNodes(projectReferences: readonly ProjectReference[] | undefined, getCommandLine: (ref: ProjectReference, index: number) => ParsedCommandLine | undefined, readFile: (path: string) => string | undefined) { - if (!projectReferences) return emptyArray; - let nodes: InputFiles[] | undefined; - for (let i = 0; i < projectReferences.length; i++) { - const ref = projectReferences[i]; - const resolvedRefOpts = getCommandLine(ref, i); - if (ref.prepend && resolvedRefOpts && resolvedRefOpts.options) { - const out = resolvedRefOpts.options.outFile || resolvedRefOpts.options.out; - // Upstream project didn't have outFile set -- skip (error will have been issued earlier) - if (!out) continue; - - const { jsFilePath, sourceMapFilePath, declarationFilePath, declarationMapPath, buildInfoPath } = getOutputPathsForBundle(resolvedRefOpts.options, /*forceDtsPaths*/ true); - const node = createInputFiles(readFile, jsFilePath!, sourceMapFilePath, declarationFilePath!, declarationMapPath, buildInfoPath); - (nodes || (nodes = [])).push(node); - } - } - return nodes || emptyArray; - } - /** - * Returns the target config filename of a project reference. - * Note: The file might not exist. - */ - export function resolveProjectReferencePath(ref: ProjectReference): ResolvedConfigFileName; - /** @deprecated */ export function resolveProjectReferencePath(host: ResolveProjectReferencePathHost, ref: ProjectReference): ResolvedConfigFileName; - export function resolveProjectReferencePath(hostOrRef: ResolveProjectReferencePathHost | ProjectReference, ref?: ProjectReference): ResolvedConfigFileName { - const passedInRef = ref ? ref : hostOrRef as ProjectReference; - return resolveConfigFileProjectName(passedInRef.path); - } - - /* @internal */ - /** - * Returns a DiagnosticMessage if we won't include a resolved module due to its extension. - * The DiagnosticMessage's parameters are the imported module name, and the filename it resolved to. - * This returns a diagnostic even if the module will be an untyped module. - */ - export function getResolutionDiagnostic(options: CompilerOptions, { extension }: ResolvedModuleFull): DiagnosticMessage | undefined { - switch (extension) { - case Extension.Ts: - case Extension.Dts: - // These are always allowed. - return undefined; - case Extension.Tsx: - return needJsx(); - case Extension.Jsx: - return needJsx() || needAllowJs(); - case Extension.Js: - return needAllowJs(); - case Extension.Json: - return needResolveJsonModule(); + // If --outDir, check if file is in that directory + if (options.outDir) { + return containsPath(options.outDir, filePath, currentDirectory, !host.useCaseSensitiveFileNames()); } - - function needJsx() { - return options.jsx ? undefined : Diagnostics.Module_0_was_resolved_to_1_but_jsx_is_not_set; + if (fileExtensionIsOneOf(filePath, supportedJSExtensions) || fileExtensionIs(filePath, Extension.Dts)) { + // Otherwise just check if sourceFile with the name exists + const filePathWithoutExtension = removeFileExtension(filePath); + return !!getSourceFileByPath(((filePathWithoutExtension + Extension.Ts) as Path)) || + !!getSourceFileByPath(((filePathWithoutExtension + Extension.Tsx) as Path)); } - function needAllowJs() { - return options.allowJs || !getStrictOptionValue(options, "noImplicitAny") ? undefined : Diagnostics.Could_not_find_a_declaration_file_for_module_0_1_implicitly_has_an_any_type; + return false; + } + function isSameFile(file1: string, file2: string) { + return comparePaths(file1, file2, currentDirectory, !host.useCaseSensitiveFileNames()) === Comparison.EqualTo; + } + function getProbableSymlinks(): ts.ReadonlyMap { + if (host.getSymlinks) { + return host.getSymlinks(); } - function needResolveJsonModule() { - return options.resolveJsonModule ? undefined : Diagnostics.Module_0_was_resolved_to_1_but_resolveJsonModule_is_not_used; + return symlinks || (symlinks = discoverProbableSymlinks(files, getCanonicalFileName, host.getCurrentDirectory())); + } +} +/*@internal*/ +export function handleNoEmitOptions(program: ProgramToEmitFilesAndReportErrors, sourceFile: SourceFile | undefined, cancellationToken: CancellationToken | undefined): EmitResult | undefined { + const options = program.getCompilerOptions(); + if (options.noEmit) { + return { diagnostics: emptyArray, sourceMaps: undefined, emittedFiles: undefined, emitSkipped: true }; + } + // If the noEmitOnError flag is set, then check if we have any errors so far. If so, + // immediately bail out. Note that we pass 'undefined' for 'sourceFile' so that we + // get any preEmit diagnostics, not just the ones + if (!options.noEmitOnError) + return undefined; + let diagnostics: readonly Diagnostic[] = [ + ...program.getOptionsDiagnostics(cancellationToken), + ...program.getSyntacticDiagnostics(sourceFile, cancellationToken), + ...program.getGlobalDiagnostics(cancellationToken), + ...program.getSemanticDiagnostics(sourceFile, cancellationToken) + ]; + if (diagnostics.length === 0 && getEmitDeclarations(program.getCompilerOptions())) { + diagnostics = program.getDeclarationDiagnostics(/*sourceFile*/ undefined, cancellationToken); + } + return diagnostics.length > 0 ? + { diagnostics, sourceMaps: undefined, emittedFiles: undefined, emitSkipped: true } : + undefined; +} +/*@internal*/ +interface CompilerHostLike { + useCaseSensitiveFileNames(): boolean; + getCurrentDirectory(): string; + fileExists(fileName: string): boolean; + readFile(fileName: string): string | undefined; + readDirectory?(rootDir: string, extensions: readonly string[], excludes: readonly string[] | undefined, includes: readonly string[], depth?: number): string[]; + trace?(s: string): void; + onUnRecoverableConfigFileDiagnostic?: DiagnosticReporter; +} +/* @internal */ +export function parseConfigHostFromCompilerHostLike(host: CompilerHostLike, directoryStructureHost: DirectoryStructureHost = host): ParseConfigFileHost { + return { + fileExists: f => directoryStructureHost.fileExists(f), + readDirectory(root, extensions, excludes, includes, depth) { + Debug.assertIsDefined(directoryStructureHost.readDirectory, "'CompilerHost.readDirectory' must be implemented to correctly process 'projectReferences'"); + return directoryStructureHost.readDirectory(root, extensions, excludes, includes, depth); + }, + readFile: f => directoryStructureHost.readFile(f), + useCaseSensitiveFileNames: host.useCaseSensitiveFileNames(), + getCurrentDirectory: () => host.getCurrentDirectory(), + onUnRecoverableConfigFileDiagnostic: host.onUnRecoverableConfigFileDiagnostic || returnUndefined, + trace: host.trace ? (s) => host.trace!(s) : undefined + }; +} +// For backward compatibility +/** @deprecated */ export interface ResolveProjectReferencePathHost { + fileExists(fileName: string): boolean; +} +/* @internal */ +export function createPrependNodes(projectReferences: readonly ProjectReference[] | undefined, getCommandLine: (ref: ProjectReference, index: number) => ParsedCommandLine | undefined, readFile: (path: string) => string | undefined) { + if (!projectReferences) + return emptyArray; + let nodes: InputFiles[] | undefined; + for (let i = 0; i < projectReferences.length; i++) { + const ref = projectReferences[i]; + const resolvedRefOpts = getCommandLine(ref, i); + if (ref.prepend && resolvedRefOpts && resolvedRefOpts.options) { + const out = resolvedRefOpts.options.outFile || resolvedRefOpts.options.out; + // Upstream project didn't have outFile set -- skip (error will have been issued earlier) + if (!out) + continue; + const { jsFilePath, sourceMapFilePath, declarationFilePath, declarationMapPath, buildInfoPath } = getOutputPathsForBundle(resolvedRefOpts.options, /*forceDtsPaths*/ true); + const node = createInputFiles(readFile, (jsFilePath!), sourceMapFilePath, (declarationFilePath!), declarationMapPath, buildInfoPath); + (nodes || (nodes = [])).push(node); } } - - function getModuleNames({ imports, moduleAugmentations }: SourceFile): string[] { - const res = imports.map(i => i.text); - for (const aug of moduleAugmentations) { - if (aug.kind === SyntaxKind.StringLiteral) { - res.push(aug.text); - } - // Do nothing if it's an Identifier; we don't need to do module resolution for `declare global`. + return nodes || emptyArray; +} +/** + * Returns the target config filename of a project reference. + * Note: The file might not exist. + */ +export function resolveProjectReferencePath(ref: ProjectReference): ResolvedConfigFileName; +/** @deprecated */ export function resolveProjectReferencePath(host: ResolveProjectReferencePathHost, ref: ProjectReference): ResolvedConfigFileName; +export function resolveProjectReferencePath(hostOrRef: ResolveProjectReferencePathHost | ProjectReference, ref?: ProjectReference): ResolvedConfigFileName { + const passedInRef = ref ? ref : hostOrRef as ProjectReference; + return resolveConfigFileProjectName(passedInRef.path); +} +/* @internal */ +/** + * Returns a DiagnosticMessage if we won't include a resolved module due to its extension. + * The DiagnosticMessage's parameters are the imported module name, and the filename it resolved to. + * This returns a diagnostic even if the module will be an untyped module. + */ +export function getResolutionDiagnostic(options: CompilerOptions, { extension }: ResolvedModuleFull): DiagnosticMessage | undefined { + switch (extension) { + case Extension.Ts: + case Extension.Dts: + // These are always allowed. + return undefined; + case Extension.Tsx: + return needJsx(); + case Extension.Jsx: + return needJsx() || needAllowJs(); + case Extension.Js: + return needAllowJs(); + case Extension.Json: + return needResolveJsonModule(); + } + function needJsx() { + return options.jsx ? undefined : Diagnostics.Module_0_was_resolved_to_1_but_jsx_is_not_set; + } + function needAllowJs() { + return options.allowJs || !getStrictOptionValue(options, "noImplicitAny") ? undefined : Diagnostics.Could_not_find_a_declaration_file_for_module_0_1_implicitly_has_an_any_type; + } + function needResolveJsonModule() { + return options.resolveJsonModule ? undefined : Diagnostics.Module_0_was_resolved_to_1_but_resolveJsonModule_is_not_used; + } +} +function getModuleNames({ imports, moduleAugmentations }: SourceFile): string[] { + const res = imports.map(i => i.text); + for (const aug of moduleAugmentations) { + if (aug.kind === SyntaxKind.StringLiteral) { + res.push(aug.text); } - return res; + // Do nothing if it's an Identifier; we don't need to do module resolution for `declare global`. } + return res; } diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index 011baa788b1a2..a102395181357 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -1,863 +1,735 @@ +import { Path, ResolvedProjectReference, ResolvedModuleFull, ResolvedTypeReferenceDirective, HasInvalidatedResolution, ResolvedModuleWithFailedLookupLocations, ResolvedTypeReferenceDirectiveWithFailedLookupLocations, ModuleResolutionHost, GetCanonicalFileName, CompilerOptions, DirectoryWatcherCallback, WatchDirectoryFlags, FileWatcher, CachedDirectoryStructureHost, Program, endsWith, removeSuffix, some, ignoredPaths, stringContains, getRootLength, directorySeparator, CharacterCodes, createMultiMap, memoize, createMap, CacheWithRedirects, createCacheWithRedirects, PerModuleNameCache, createModuleResolutionCacheWithMaps, Extension, removeTrailingDirectorySeparator, getNormalizedAbsolutePath, startsWith, clearMap, closeFileWatcherOf, returnTrue, isExternalModuleNameRelative, extensionIsTS, loadModuleFromGlobalCache, Debug, addRange, getDirectoryPath, contains, resolveTypeReferenceDirective, resolutionExtensionIsTSOrJson, isRootedDiskPath, normalizePath, pathContainsNodeModules, fileExtensionIsOneOf, fileExtensionIs, inferredTypesContainingFile, isEmittedFileOfProgram, closeFileWatcher, getEffectiveTypeRoots, mutateMap, arrayToMap } from "./ts"; +import * as ts from "./ts"; /*@internal*/ -namespace ts { - /** This is the cache of module/typedirectives resolution that can be retained across program */ - export interface ResolutionCache { - startRecordingFilesWithChangedResolutions(): void; - finishRecordingFilesWithChangedResolutions(): Path[] | undefined; - - resolveModuleNames(moduleNames: string[], containingFile: string, reusedNames: string[] | undefined, redirectedReference?: ResolvedProjectReference): (ResolvedModuleFull | undefined)[]; - getResolvedModuleWithFailedLookupLocationsFromCache(moduleName: string, containingFile: string): CachedResolvedModuleWithFailedLookupLocations | undefined; - resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string, redirectedReference?: ResolvedProjectReference): (ResolvedTypeReferenceDirective | undefined)[]; - - invalidateResolutionOfFile(filePath: Path): void; - removeResolutionsOfFile(filePath: Path): void; - removeResolutionsFromProjectReferenceRedirects(filePath: Path): void; - setFilesWithInvalidatedNonRelativeUnresolvedImports(filesWithUnresolvedImports: Map): void; - createHasInvalidatedResolution(forceAllFilesAsInvalidated?: boolean): HasInvalidatedResolution; - - startCachingPerDirectoryResolution(): void; - finishCachingPerDirectoryResolution(): void; - - updateTypeRootsWatch(): void; - closeTypeRootsWatch(): void; - - clear(): void; - } - - interface ResolutionWithFailedLookupLocations { - readonly failedLookupLocations: readonly string[]; - isInvalidated?: boolean; - refCount?: number; - } - - interface ResolutionWithResolvedFileName { - resolvedFileName: string | undefined; - } - - interface CachedResolvedModuleWithFailedLookupLocations extends ResolvedModuleWithFailedLookupLocations, ResolutionWithFailedLookupLocations { - } - - interface CachedResolvedTypeReferenceDirectiveWithFailedLookupLocations extends ResolvedTypeReferenceDirectiveWithFailedLookupLocations, ResolutionWithFailedLookupLocations { - } - - export interface ResolutionCacheHost extends ModuleResolutionHost { - toPath(fileName: string): Path; - getCanonicalFileName: GetCanonicalFileName; - getCompilationSettings(): CompilerOptions; - watchDirectoryOfFailedLookupLocation(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher; - onInvalidatedResolution(): void; - watchTypeRootsDirectory(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher; - onChangedAutomaticTypeDirectiveNames(): void; - getCachedDirectoryStructureHost(): CachedDirectoryStructureHost | undefined; - projectName?: string; - getGlobalCache?(): string | undefined; - globalCacheResolutionModuleName?(externalModuleName: string): string; - writeLog(s: string): void; - maxNumberOfFilesToIterateForInvalidation?: number; - getCurrentProgram(): Program | undefined; - fileIsOpen(filePath: Path): boolean; - } - - interface DirectoryWatchesOfFailedLookup { - /** watcher for the directory of failed lookup */ - watcher: FileWatcher; - /** ref count keeping this directory watch alive */ - refCount: number; - /** is the directory watched being non recursive */ - nonRecursive?: boolean; - } - - interface DirectoryOfFailedLookupWatch { - dir: string; - dirPath: Path; - nonRecursive?: boolean; - } - - export function removeIgnoredPath(path: Path): Path | undefined { - // Consider whole staging folder as if node_modules changed. - if (endsWith(path, "/node_modules/.staging")) { - return removeSuffix(path, "/.staging") as Path; - } - - return some(ignoredPaths, searchPath => stringContains(path, searchPath)) ? - undefined : - path; - } - - /** - * Filter out paths like - * "/", "/user", "/user/username", "/user/username/folderAtRoot", - * "c:/", "c:/users", "c:/users/username", "c:/users/username/folderAtRoot", "c:/folderAtRoot" - * @param dirPath - */ - export function canWatchDirectory(dirPath: Path) { - const rootLength = getRootLength(dirPath); - if (dirPath.length === rootLength) { - // Ignore "/", "c:/" +/** This is the cache of module/typedirectives resolution that can be retained across program */ +export interface ResolutionCache { + startRecordingFilesWithChangedResolutions(): void; + finishRecordingFilesWithChangedResolutions(): Path[] | undefined; + resolveModuleNames(moduleNames: string[], containingFile: string, reusedNames: string[] | undefined, redirectedReference?: ResolvedProjectReference): (ResolvedModuleFull | undefined)[]; + getResolvedModuleWithFailedLookupLocationsFromCache(moduleName: string, containingFile: string): CachedResolvedModuleWithFailedLookupLocations | undefined; + resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string, redirectedReference?: ResolvedProjectReference): (ResolvedTypeReferenceDirective | undefined)[]; + invalidateResolutionOfFile(filePath: Path): void; + removeResolutionsOfFile(filePath: Path): void; + removeResolutionsFromProjectReferenceRedirects(filePath: Path): void; + setFilesWithInvalidatedNonRelativeUnresolvedImports(filesWithUnresolvedImports: ts.Map): void; + createHasInvalidatedResolution(forceAllFilesAsInvalidated?: boolean): HasInvalidatedResolution; + startCachingPerDirectoryResolution(): void; + finishCachingPerDirectoryResolution(): void; + updateTypeRootsWatch(): void; + closeTypeRootsWatch(): void; + clear(): void; +} +/* @internal */ +interface ResolutionWithFailedLookupLocations { + readonly failedLookupLocations: readonly string[]; + isInvalidated?: boolean; + refCount?: number; +} +/* @internal */ +interface ResolutionWithResolvedFileName { + resolvedFileName: string | undefined; +} +/* @internal */ +interface CachedResolvedModuleWithFailedLookupLocations extends ResolvedModuleWithFailedLookupLocations, ResolutionWithFailedLookupLocations { +} +/* @internal */ +interface CachedResolvedTypeReferenceDirectiveWithFailedLookupLocations extends ResolvedTypeReferenceDirectiveWithFailedLookupLocations, ResolutionWithFailedLookupLocations { +} +/* @internal */ +export interface ResolutionCacheHost extends ModuleResolutionHost { + toPath(fileName: string): Path; + getCanonicalFileName: GetCanonicalFileName; + getCompilationSettings(): CompilerOptions; + watchDirectoryOfFailedLookupLocation(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher; + onInvalidatedResolution(): void; + watchTypeRootsDirectory(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher; + onChangedAutomaticTypeDirectiveNames(): void; + getCachedDirectoryStructureHost(): CachedDirectoryStructureHost | undefined; + projectName?: string; + getGlobalCache?(): string | undefined; + globalCacheResolutionModuleName?(externalModuleName: string): string; + writeLog(s: string): void; + maxNumberOfFilesToIterateForInvalidation?: number; + getCurrentProgram(): Program | undefined; + fileIsOpen(filePath: Path): boolean; +} +/* @internal */ +interface DirectoryWatchesOfFailedLookup { + /** watcher for the directory of failed lookup */ + watcher: FileWatcher; + /** ref count keeping this directory watch alive */ + refCount: number; + /** is the directory watched being non recursive */ + nonRecursive?: boolean; +} +/* @internal */ +interface DirectoryOfFailedLookupWatch { + dir: string; + dirPath: Path; + nonRecursive?: boolean; +} +/* @internal */ +export function removeIgnoredPath(path: Path): Path | undefined { + // Consider whole staging folder as if node_modules changed. + if (endsWith(path, "/node_modules/.staging")) { + return removeSuffix(path, "/.staging") as Path; + } + return some(ignoredPaths, searchPath => stringContains(path, searchPath)) ? + undefined : + path; +} +/** + * Filter out paths like + * "/", "/user", "/user/username", "/user/username/folderAtRoot", + * "c:/", "c:/users", "c:/users/username", "c:/users/username/folderAtRoot", "c:/folderAtRoot" + * @param dirPath + */ +/* @internal */ +export function canWatchDirectory(dirPath: Path) { + const rootLength = getRootLength(dirPath); + if (dirPath.length === rootLength) { + // Ignore "/", "c:/" + return false; + } + let nextDirectorySeparator = dirPath.indexOf(directorySeparator, rootLength); + if (nextDirectorySeparator === -1) { + // ignore "/user", "c:/users" or "c:/folderAtRoot" + return false; + } + let pathPartForUserCheck = dirPath.substring(rootLength, nextDirectorySeparator + 1); + const isNonDirectorySeparatorRoot = rootLength > 1 || dirPath.charCodeAt(0) !== CharacterCodes.slash; + if (isNonDirectorySeparatorRoot && + dirPath.search(/[a-zA-Z]:/) !== 0 && // Non dos style paths + pathPartForUserCheck.search(/[a-zA-z]\$\//) === 0) { // Dos style nextPart + nextDirectorySeparator = dirPath.indexOf(directorySeparator, nextDirectorySeparator + 1); + if (nextDirectorySeparator === -1) { + // ignore "//vda1cs4850/c$/folderAtRoot" return false; } - - let nextDirectorySeparator = dirPath.indexOf(directorySeparator, rootLength); - if (nextDirectorySeparator === -1) { - // ignore "/user", "c:/users" or "c:/folderAtRoot" + pathPartForUserCheck = dirPath.substring(rootLength + pathPartForUserCheck.length, nextDirectorySeparator + 1); + } + if (isNonDirectorySeparatorRoot && + pathPartForUserCheck.search(/users\//i) !== 0) { + // Paths like c:/folderAtRoot/subFolder are allowed + return true; + } + for (let searchIndex = nextDirectorySeparator + 1, searchLevels = 2; searchLevels > 0; searchLevels--) { + searchIndex = dirPath.indexOf(directorySeparator, searchIndex) + 1; + if (searchIndex === 0) { + // Folder isnt at expected minimum levels return false; } - - let pathPartForUserCheck = dirPath.substring(rootLength, nextDirectorySeparator + 1); - const isNonDirectorySeparatorRoot = rootLength > 1 || dirPath.charCodeAt(0) !== CharacterCodes.slash; - if (isNonDirectorySeparatorRoot && - dirPath.search(/[a-zA-Z]:/) !== 0 && // Non dos style paths - pathPartForUserCheck.search(/[a-zA-z]\$\//) === 0) { // Dos style nextPart - nextDirectorySeparator = dirPath.indexOf(directorySeparator, nextDirectorySeparator + 1); - if (nextDirectorySeparator === -1) { - // ignore "//vda1cs4850/c$/folderAtRoot" - return false; - } - - pathPartForUserCheck = dirPath.substring(rootLength + pathPartForUserCheck.length, nextDirectorySeparator + 1); - } - - if (isNonDirectorySeparatorRoot && - pathPartForUserCheck.search(/users\//i) !== 0) { - // Paths like c:/folderAtRoot/subFolder are allowed - return true; - } - - for (let searchIndex = nextDirectorySeparator + 1, searchLevels = 2; searchLevels > 0; searchLevels--) { - searchIndex = dirPath.indexOf(directorySeparator, searchIndex) + 1; - if (searchIndex === 0) { - // Folder isnt at expected minimum levels - return false; - } + } + return true; +} +/* @internal */ +export const maxNumberOfFilesToIterateForInvalidation = 256; +/* @internal */ +type GetResolutionWithResolvedFileName = (resolution: T) => R | undefined; +/* @internal */ +export function createResolutionCache(resolutionHost: ResolutionCacheHost, rootDirForResolution: string | undefined, logChangesWhenResolvingModule: boolean): ResolutionCache { + let filesWithChangedSetOfUnresolvedImports: Path[] | undefined; + let filesWithInvalidatedResolutions: ts.Map | undefined; + let filesWithInvalidatedNonRelativeUnresolvedImports: ts.ReadonlyMap | undefined; + let allFilesHaveInvalidatedResolution = false; + const nonRelativeExternalModuleResolutions = createMultiMap(); + const getCurrentDirectory = memoize(() => resolutionHost.getCurrentDirectory!()); // TODO: GH#18217 + const cachedDirectoryStructureHost = resolutionHost.getCachedDirectoryStructureHost(); + // The resolvedModuleNames and resolvedTypeReferenceDirectives are the cache of resolutions per file. + // The key in the map is source file's path. + // The values are Map of resolutions with key being name lookedup. + const resolvedModuleNames = createMap>(); + const perDirectoryResolvedModuleNames: CacheWithRedirects> = createCacheWithRedirects(); + const nonRelativeModuleNameCache: CacheWithRedirects = createCacheWithRedirects(); + const moduleResolutionCache = createModuleResolutionCacheWithMaps(perDirectoryResolvedModuleNames, nonRelativeModuleNameCache, getCurrentDirectory(), resolutionHost.getCanonicalFileName); + const resolvedTypeReferenceDirectives = createMap>(); + const perDirectoryResolvedTypeReferenceDirectives: CacheWithRedirects> = createCacheWithRedirects(); + /** + * These are the extensions that failed lookup files will have by default, + * any other extension of failed lookup will be store that path in custom failed lookup path + * This helps in not having to comb through all resolutions when files are added/removed + * Note that .d.ts file also has .d.ts extension hence will be part of default extensions + */ + const failedLookupDefaultExtensions = [Extension.Ts, Extension.Tsx, Extension.Js, Extension.Jsx, Extension.Json]; + const customFailedLookupPaths = createMap(); + const directoryWatchesOfFailedLookups = createMap(); + const rootDir = rootDirForResolution && removeTrailingDirectorySeparator(getNormalizedAbsolutePath(rootDirForResolution, getCurrentDirectory())); + const rootPath = ((rootDir && resolutionHost.toPath(rootDir)) as Path); // TODO: GH#18217 + const rootSplitLength = rootPath !== undefined ? rootPath.split(directorySeparator).length : 0; + // TypeRoot watches for the types that get added as part of getAutomaticTypeDirectiveNames + const typeRootsWatches = createMap(); + return { + startRecordingFilesWithChangedResolutions, + finishRecordingFilesWithChangedResolutions, + // perDirectoryResolvedModuleNames and perDirectoryResolvedTypeReferenceDirectives could be non empty if there was exception during program update + // (between startCachingPerDirectoryResolution and finishCachingPerDirectoryResolution) + startCachingPerDirectoryResolution: clearPerDirectoryResolutions, + finishCachingPerDirectoryResolution, + resolveModuleNames, + getResolvedModuleWithFailedLookupLocationsFromCache, + resolveTypeReferenceDirectives, + removeResolutionsFromProjectReferenceRedirects, + removeResolutionsOfFile, + invalidateResolutionOfFile, + setFilesWithInvalidatedNonRelativeUnresolvedImports, + createHasInvalidatedResolution, + updateTypeRootsWatch, + closeTypeRootsWatch, + clear + }; + function getResolvedModule(resolution: CachedResolvedModuleWithFailedLookupLocations) { + return resolution.resolvedModule; + } + function getResolvedTypeReferenceDirective(resolution: CachedResolvedTypeReferenceDirectiveWithFailedLookupLocations) { + return resolution.resolvedTypeReferenceDirective; + } + function isInDirectoryPath(dir: Path | undefined, file: Path) { + if (dir === undefined || file.length <= dir.length) { + return false; } - return true; + return startsWith(file, dir) && file[dir.length] === directorySeparator; } - - export const maxNumberOfFilesToIterateForInvalidation = 256; - - type GetResolutionWithResolvedFileName = - (resolution: T) => R | undefined; - - export function createResolutionCache(resolutionHost: ResolutionCacheHost, rootDirForResolution: string | undefined, logChangesWhenResolvingModule: boolean): ResolutionCache { - let filesWithChangedSetOfUnresolvedImports: Path[] | undefined; - let filesWithInvalidatedResolutions: Map | undefined; - let filesWithInvalidatedNonRelativeUnresolvedImports: ReadonlyMap | undefined; - let allFilesHaveInvalidatedResolution = false; - const nonRelativeExternalModuleResolutions = createMultiMap(); - - const getCurrentDirectory = memoize(() => resolutionHost.getCurrentDirectory!()); // TODO: GH#18217 - const cachedDirectoryStructureHost = resolutionHost.getCachedDirectoryStructureHost(); - - // The resolvedModuleNames and resolvedTypeReferenceDirectives are the cache of resolutions per file. - // The key in the map is source file's path. - // The values are Map of resolutions with key being name lookedup. - const resolvedModuleNames = createMap>(); - const perDirectoryResolvedModuleNames: CacheWithRedirects> = createCacheWithRedirects(); - const nonRelativeModuleNameCache: CacheWithRedirects = createCacheWithRedirects(); - const moduleResolutionCache = createModuleResolutionCacheWithMaps( - perDirectoryResolvedModuleNames, - nonRelativeModuleNameCache, - getCurrentDirectory(), - resolutionHost.getCanonicalFileName - ); - - const resolvedTypeReferenceDirectives = createMap>(); - const perDirectoryResolvedTypeReferenceDirectives: CacheWithRedirects> = createCacheWithRedirects(); - - /** - * These are the extensions that failed lookup files will have by default, - * any other extension of failed lookup will be store that path in custom failed lookup path - * This helps in not having to comb through all resolutions when files are added/removed - * Note that .d.ts file also has .d.ts extension hence will be part of default extensions - */ - const failedLookupDefaultExtensions = [Extension.Ts, Extension.Tsx, Extension.Js, Extension.Jsx, Extension.Json]; - const customFailedLookupPaths = createMap(); - - const directoryWatchesOfFailedLookups = createMap(); - const rootDir = rootDirForResolution && removeTrailingDirectorySeparator(getNormalizedAbsolutePath(rootDirForResolution, getCurrentDirectory())); - const rootPath = (rootDir && resolutionHost.toPath(rootDir)) as Path; // TODO: GH#18217 - const rootSplitLength = rootPath !== undefined ? rootPath.split(directorySeparator).length : 0; - - // TypeRoot watches for the types that get added as part of getAutomaticTypeDirectiveNames - const typeRootsWatches = createMap(); - - return { - startRecordingFilesWithChangedResolutions, - finishRecordingFilesWithChangedResolutions, - // perDirectoryResolvedModuleNames and perDirectoryResolvedTypeReferenceDirectives could be non empty if there was exception during program update - // (between startCachingPerDirectoryResolution and finishCachingPerDirectoryResolution) - startCachingPerDirectoryResolution: clearPerDirectoryResolutions, - finishCachingPerDirectoryResolution, - resolveModuleNames, - getResolvedModuleWithFailedLookupLocationsFromCache, - resolveTypeReferenceDirectives, - removeResolutionsFromProjectReferenceRedirects, - removeResolutionsOfFile, - invalidateResolutionOfFile, - setFilesWithInvalidatedNonRelativeUnresolvedImports, - createHasInvalidatedResolution, - updateTypeRootsWatch, - closeTypeRootsWatch, - clear - }; - - function getResolvedModule(resolution: CachedResolvedModuleWithFailedLookupLocations) { - return resolution.resolvedModule; - } - - function getResolvedTypeReferenceDirective(resolution: CachedResolvedTypeReferenceDirectiveWithFailedLookupLocations) { - return resolution.resolvedTypeReferenceDirective; - } - - function isInDirectoryPath(dir: Path | undefined, file: Path) { - if (dir === undefined || file.length <= dir.length) { - return false; - } - return startsWith(file, dir) && file[dir.length] === directorySeparator; + function clear() { + clearMap(directoryWatchesOfFailedLookups, closeFileWatcherOf); + customFailedLookupPaths.clear(); + nonRelativeExternalModuleResolutions.clear(); + closeTypeRootsWatch(); + resolvedModuleNames.clear(); + resolvedTypeReferenceDirectives.clear(); + allFilesHaveInvalidatedResolution = false; + // perDirectoryResolvedModuleNames and perDirectoryResolvedTypeReferenceDirectives could be non empty if there was exception during program update + // (between startCachingPerDirectoryResolution and finishCachingPerDirectoryResolution) + clearPerDirectoryResolutions(); + } + function startRecordingFilesWithChangedResolutions() { + filesWithChangedSetOfUnresolvedImports = []; + } + function finishRecordingFilesWithChangedResolutions() { + const collected = filesWithChangedSetOfUnresolvedImports; + filesWithChangedSetOfUnresolvedImports = undefined; + return collected; + } + function isFileWithInvalidatedNonRelativeUnresolvedImports(path: Path): boolean { + if (!filesWithInvalidatedNonRelativeUnresolvedImports) { + return false; } - - function clear() { - clearMap(directoryWatchesOfFailedLookups, closeFileWatcherOf); - customFailedLookupPaths.clear(); - nonRelativeExternalModuleResolutions.clear(); - closeTypeRootsWatch(); - resolvedModuleNames.clear(); - resolvedTypeReferenceDirectives.clear(); - allFilesHaveInvalidatedResolution = false; - // perDirectoryResolvedModuleNames and perDirectoryResolvedTypeReferenceDirectives could be non empty if there was exception during program update - // (between startCachingPerDirectoryResolution and finishCachingPerDirectoryResolution) - clearPerDirectoryResolutions(); - } - - function startRecordingFilesWithChangedResolutions() { - filesWithChangedSetOfUnresolvedImports = []; - } - - function finishRecordingFilesWithChangedResolutions() { - const collected = filesWithChangedSetOfUnresolvedImports; - filesWithChangedSetOfUnresolvedImports = undefined; - return collected; - } - - function isFileWithInvalidatedNonRelativeUnresolvedImports(path: Path): boolean { - if (!filesWithInvalidatedNonRelativeUnresolvedImports) { - return false; - } - - // Invalidated if file has unresolved imports - const value = filesWithInvalidatedNonRelativeUnresolvedImports.get(path); - return !!value && !!value.length; - } - - function createHasInvalidatedResolution(forceAllFilesAsInvalidated?: boolean): HasInvalidatedResolution { - if (allFilesHaveInvalidatedResolution || forceAllFilesAsInvalidated) { - // Any file asked would have invalidated resolution - filesWithInvalidatedResolutions = undefined; - return returnTrue; - } - const collected = filesWithInvalidatedResolutions; + // Invalidated if file has unresolved imports + const value = filesWithInvalidatedNonRelativeUnresolvedImports.get(path); + return !!value && !!value.length; + } + function createHasInvalidatedResolution(forceAllFilesAsInvalidated?: boolean): HasInvalidatedResolution { + if (allFilesHaveInvalidatedResolution || forceAllFilesAsInvalidated) { + // Any file asked would have invalidated resolution filesWithInvalidatedResolutions = undefined; - return path => (!!collected && collected.has(path)) || - isFileWithInvalidatedNonRelativeUnresolvedImports(path); - } - - function clearPerDirectoryResolutions() { - perDirectoryResolvedModuleNames.clear(); - nonRelativeModuleNameCache.clear(); - perDirectoryResolvedTypeReferenceDirectives.clear(); - nonRelativeExternalModuleResolutions.forEach(watchFailedLookupLocationOfNonRelativeModuleResolutions); - nonRelativeExternalModuleResolutions.clear(); - } - - function finishCachingPerDirectoryResolution() { - allFilesHaveInvalidatedResolution = false; - filesWithInvalidatedNonRelativeUnresolvedImports = undefined; - clearPerDirectoryResolutions(); - directoryWatchesOfFailedLookups.forEach((watcher, path) => { - if (watcher.refCount === 0) { - directoryWatchesOfFailedLookups.delete(path); - watcher.watcher.close(); - } - }); + return returnTrue; } - - function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, redirectedReference?: ResolvedProjectReference): CachedResolvedModuleWithFailedLookupLocations { - const primaryResult = ts.resolveModuleName(moduleName, containingFile, compilerOptions, host, moduleResolutionCache, redirectedReference); - // return result immediately only if global cache support is not enabled or if it is .ts, .tsx or .d.ts - if (!resolutionHost.getGlobalCache) { - return primaryResult; - } - - // otherwise try to load typings from @types - const globalCache = resolutionHost.getGlobalCache(); - if (globalCache !== undefined && !isExternalModuleNameRelative(moduleName) && !(primaryResult.resolvedModule && extensionIsTS(primaryResult.resolvedModule.extension))) { - // create different collection of failed lookup locations for second pass - // if it will fail and we've already found something during the first pass - we don't want to pollute its results - const { resolvedModule, failedLookupLocations } = loadModuleFromGlobalCache( - Debug.checkDefined(resolutionHost.globalCacheResolutionModuleName)(moduleName), - resolutionHost.projectName, - compilerOptions, - host, - globalCache); - if (resolvedModule) { - return { resolvedModule, failedLookupLocations: addRange(primaryResult.failedLookupLocations as string[], failedLookupLocations) }; - } - } - - // Default return the result from the first pass + const collected = filesWithInvalidatedResolutions; + filesWithInvalidatedResolutions = undefined; + return path => (!!collected && collected.has(path)) || + isFileWithInvalidatedNonRelativeUnresolvedImports(path); + } + function clearPerDirectoryResolutions() { + perDirectoryResolvedModuleNames.clear(); + nonRelativeModuleNameCache.clear(); + perDirectoryResolvedTypeReferenceDirectives.clear(); + nonRelativeExternalModuleResolutions.forEach(watchFailedLookupLocationOfNonRelativeModuleResolutions); + nonRelativeExternalModuleResolutions.clear(); + } + function finishCachingPerDirectoryResolution() { + allFilesHaveInvalidatedResolution = false; + filesWithInvalidatedNonRelativeUnresolvedImports = undefined; + clearPerDirectoryResolutions(); + directoryWatchesOfFailedLookups.forEach((watcher, path) => { + if (watcher.refCount === 0) { + directoryWatchesOfFailedLookups.delete(path); + watcher.watcher.close(); + } + }); + } + function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, redirectedReference?: ResolvedProjectReference): CachedResolvedModuleWithFailedLookupLocations { + const primaryResult = ts.resolveModuleName(moduleName, containingFile, compilerOptions, host, moduleResolutionCache, redirectedReference); + // return result immediately only if global cache support is not enabled or if it is .ts, .tsx or .d.ts + if (!resolutionHost.getGlobalCache) { return primaryResult; } - - function resolveNamesWithLocalCache( - names: readonly string[], - containingFile: string, - redirectedReference: ResolvedProjectReference | undefined, - cache: Map>, - perDirectoryCacheWithRedirects: CacheWithRedirects>, - loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost, redirectedReference?: ResolvedProjectReference) => T, - getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName, - shouldRetryResolution: (t: T) => boolean, - reusedNames: readonly string[] | undefined, - logChanges: boolean): (R | undefined)[] { - - const path = resolutionHost.toPath(containingFile); - const resolutionsInFile = cache.get(path) || cache.set(path, createMap()).get(path)!; - const dirPath = getDirectoryPath(path); - const perDirectoryCache = perDirectoryCacheWithRedirects.getOrCreateMapOfCacheRedirects(redirectedReference); - let perDirectoryResolution = perDirectoryCache.get(dirPath); - if (!perDirectoryResolution) { - perDirectoryResolution = createMap(); - perDirectoryCache.set(dirPath, perDirectoryResolution); + // otherwise try to load typings from @types + const globalCache = resolutionHost.getGlobalCache(); + if (globalCache !== undefined && !isExternalModuleNameRelative(moduleName) && !(primaryResult.resolvedModule && extensionIsTS(primaryResult.resolvedModule.extension))) { + // create different collection of failed lookup locations for second pass + // if it will fail and we've already found something during the first pass - we don't want to pollute its results + const { resolvedModule, failedLookupLocations } = loadModuleFromGlobalCache(Debug.checkDefined(resolutionHost.globalCacheResolutionModuleName)(moduleName), resolutionHost.projectName, compilerOptions, host, globalCache); + if (resolvedModule) { + return { resolvedModule, failedLookupLocations: addRange((primaryResult.failedLookupLocations as string[]), failedLookupLocations) }; } - const resolvedModules: (R | undefined)[] = []; - const compilerOptions = resolutionHost.getCompilationSettings(); - const hasInvalidatedNonRelativeUnresolvedImport = logChanges && isFileWithInvalidatedNonRelativeUnresolvedImports(path); - - // All the resolutions in this file are invalidated if this file wasnt resolved using same redirect - const program = resolutionHost.getCurrentProgram(); - const oldRedirect = program && program.getResolvedProjectReferenceToRedirect(containingFile); - const unmatchedRedirects = oldRedirect ? - !redirectedReference || redirectedReference.sourceFile.path !== oldRedirect.sourceFile.path : - !!redirectedReference; - - const seenNamesInFile = createMap(); - for (const name of names) { - let resolution = resolutionsInFile.get(name); - // Resolution is valid if it is present and not invalidated - if (!seenNamesInFile.has(name) && - allFilesHaveInvalidatedResolution || unmatchedRedirects || !resolution || resolution.isInvalidated || - // If the name is unresolved import that was invalidated, recalculate - (hasInvalidatedNonRelativeUnresolvedImport && !isExternalModuleNameRelative(name) && shouldRetryResolution(resolution))) { - const existingResolution = resolution; - const resolutionInDirectory = perDirectoryResolution.get(name); - if (resolutionInDirectory) { - resolution = resolutionInDirectory; - } - else { - resolution = loader(name, containingFile, compilerOptions, resolutionHost, redirectedReference); - perDirectoryResolution.set(name, resolution); - } - resolutionsInFile.set(name, resolution); - watchFailedLookupLocationsOfExternalModuleResolutions(name, resolution); - if (existingResolution) { - stopWatchFailedLookupLocationOfResolution(existingResolution); - } - - if (logChanges && filesWithChangedSetOfUnresolvedImports && !resolutionIsEqualTo(existingResolution, resolution)) { - filesWithChangedSetOfUnresolvedImports.push(path); - // reset log changes to avoid recording the same file multiple times - logChanges = false; - } - } - Debug.assert(resolution !== undefined && !resolution.isInvalidated); - seenNamesInFile.set(name, true); - resolvedModules.push(getResolutionWithResolvedFileName(resolution)); - } - - // Stop watching and remove the unused name - resolutionsInFile.forEach((resolution, name) => { - if (!seenNamesInFile.has(name) && !contains(reusedNames, name)) { - stopWatchFailedLookupLocationOfResolution(resolution); - resolutionsInFile.delete(name); - } - }); - - return resolvedModules; - - function resolutionIsEqualTo(oldResolution: T | undefined, newResolution: T | undefined): boolean { - if (oldResolution === newResolution) { - return true; + } + // Default return the result from the first pass + return primaryResult; + } + function resolveNamesWithLocalCache(names: readonly string[], containingFile: string, redirectedReference: ResolvedProjectReference | undefined, cache: ts.Map>, perDirectoryCacheWithRedirects: CacheWithRedirects>, loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost, redirectedReference?: ResolvedProjectReference) => T, getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName, shouldRetryResolution: (t: T) => boolean, reusedNames: readonly string[] | undefined, logChanges: boolean): (R | undefined)[] { + const path = resolutionHost.toPath(containingFile); + const resolutionsInFile = cache.get(path) || (cache.set(path, createMap()).get(path)!); + const dirPath = getDirectoryPath(path); + const perDirectoryCache = perDirectoryCacheWithRedirects.getOrCreateMapOfCacheRedirects(redirectedReference); + let perDirectoryResolution = perDirectoryCache.get(dirPath); + if (!perDirectoryResolution) { + perDirectoryResolution = createMap(); + perDirectoryCache.set(dirPath, perDirectoryResolution); + } + const resolvedModules: (R | undefined)[] = []; + const compilerOptions = resolutionHost.getCompilationSettings(); + const hasInvalidatedNonRelativeUnresolvedImport = logChanges && isFileWithInvalidatedNonRelativeUnresolvedImports(path); + // All the resolutions in this file are invalidated if this file wasnt resolved using same redirect + const program = resolutionHost.getCurrentProgram(); + const oldRedirect = program && program.getResolvedProjectReferenceToRedirect(containingFile); + const unmatchedRedirects = oldRedirect ? + !redirectedReference || redirectedReference.sourceFile.path !== oldRedirect.sourceFile.path : + !!redirectedReference; + const seenNamesInFile = createMap(); + for (const name of names) { + let resolution = resolutionsInFile.get(name); + // Resolution is valid if it is present and not invalidated + if (!seenNamesInFile.has(name) && + allFilesHaveInvalidatedResolution || unmatchedRedirects || !resolution || resolution.isInvalidated || + // If the name is unresolved import that was invalidated, recalculate + (hasInvalidatedNonRelativeUnresolvedImport && !isExternalModuleNameRelative(name) && shouldRetryResolution(resolution))) { + const existingResolution = resolution; + const resolutionInDirectory = perDirectoryResolution.get(name); + if (resolutionInDirectory) { + resolution = resolutionInDirectory; } - if (!oldResolution || !newResolution) { - return false; + else { + resolution = loader(name, containingFile, compilerOptions, resolutionHost, redirectedReference); + perDirectoryResolution.set(name, resolution); } - const oldResult = getResolutionWithResolvedFileName(oldResolution); - const newResult = getResolutionWithResolvedFileName(newResolution); - if (oldResult === newResult) { - return true; + resolutionsInFile.set(name, resolution); + watchFailedLookupLocationsOfExternalModuleResolutions(name, resolution); + if (existingResolution) { + stopWatchFailedLookupLocationOfResolution(existingResolution); } - if (!oldResult || !newResult) { - return false; + if (logChanges && filesWithChangedSetOfUnresolvedImports && !resolutionIsEqualTo(existingResolution, resolution)) { + filesWithChangedSetOfUnresolvedImports.push(path); + // reset log changes to avoid recording the same file multiple times + logChanges = false; } - return oldResult.resolvedFileName === newResult.resolvedFileName; } + Debug.assert(resolution !== undefined && !resolution.isInvalidated); + seenNamesInFile.set(name, true); + resolvedModules.push(getResolutionWithResolvedFileName(resolution)); } - - function resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string, redirectedReference?: ResolvedProjectReference): (ResolvedTypeReferenceDirective | undefined)[] { - return resolveNamesWithLocalCache( - typeDirectiveNames, containingFile, redirectedReference, - resolvedTypeReferenceDirectives, perDirectoryResolvedTypeReferenceDirectives, - resolveTypeReferenceDirective, getResolvedTypeReferenceDirective, - /*shouldRetryResolution*/ resolution => resolution.resolvedTypeReferenceDirective === undefined, - /*reusedNames*/ undefined, /*logChanges*/ false - ); - } - - function resolveModuleNames(moduleNames: string[], containingFile: string, reusedNames: string[] | undefined, redirectedReference?: ResolvedProjectReference): (ResolvedModuleFull | undefined)[] { - return resolveNamesWithLocalCache( - moduleNames, containingFile, redirectedReference, - resolvedModuleNames, perDirectoryResolvedModuleNames, - resolveModuleName, getResolvedModule, - /*shouldRetryResolution*/ resolution => !resolution.resolvedModule || !resolutionExtensionIsTSOrJson(resolution.resolvedModule.extension), - reusedNames, logChangesWhenResolvingModule - ); - } - - function getResolvedModuleWithFailedLookupLocationsFromCache(moduleName: string, containingFile: string): CachedResolvedModuleWithFailedLookupLocations | undefined { - const cache = resolvedModuleNames.get(resolutionHost.toPath(containingFile)); - return cache && cache.get(moduleName); - } - - function isNodeModulesDirectory(dirPath: Path) { - return endsWith(dirPath, "/node_modules"); - } - - function isNodeModulesAtTypesDirectory(dirPath: Path) { - return endsWith(dirPath, "/node_modules/@types"); - } - - function getDirectoryToWatchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path): DirectoryOfFailedLookupWatch | undefined { - if (isInDirectoryPath(rootPath, failedLookupLocationPath)) { - // Ensure failed look up is normalized path - failedLookupLocation = isRootedDiskPath(failedLookupLocation) ? normalizePath(failedLookupLocation) : getNormalizedAbsolutePath(failedLookupLocation, getCurrentDirectory()); - const failedLookupPathSplit = failedLookupLocationPath.split(directorySeparator); - const failedLookupSplit = failedLookupLocation.split(directorySeparator); - Debug.assert(failedLookupSplit.length === failedLookupPathSplit.length, `FailedLookup: ${failedLookupLocation} failedLookupLocationPath: ${failedLookupLocationPath}`); - if (failedLookupPathSplit.length > rootSplitLength + 1) { - // Instead of watching root, watch directory in root to avoid watching excluded directories not needed for module resolution - return { - dir: failedLookupSplit.slice(0, rootSplitLength + 1).join(directorySeparator), - dirPath: failedLookupPathSplit.slice(0, rootSplitLength + 1).join(directorySeparator) as Path - }; - } - else { - // Always watch root directory non recursively - return { - dir: rootDir!, - dirPath: rootPath, - nonRecursive: false - }; - } - } - - return getDirectoryToWatchFromFailedLookupLocationDirectory( - getDirectoryPath(getNormalizedAbsolutePath(failedLookupLocation, getCurrentDirectory())), - getDirectoryPath(failedLookupLocationPath) - ); - } - - function getDirectoryToWatchFromFailedLookupLocationDirectory(dir: string, dirPath: Path): DirectoryOfFailedLookupWatch | undefined { - // If directory path contains node module, get the most parent node_modules directory for watching - while (pathContainsNodeModules(dirPath)) { - dir = getDirectoryPath(dir); - dirPath = getDirectoryPath(dirPath); + // Stop watching and remove the unused name + resolutionsInFile.forEach((resolution, name) => { + if (!seenNamesInFile.has(name) && !contains(reusedNames, name)) { + stopWatchFailedLookupLocationOfResolution(resolution); + resolutionsInFile.delete(name); } - - // If the directory is node_modules use it to watch, always watch it recursively - if (isNodeModulesDirectory(dirPath)) { - return canWatchDirectory(getDirectoryPath(dirPath)) ? { dir, dirPath } : undefined; + }); + return resolvedModules; + function resolutionIsEqualTo(oldResolution: T | undefined, newResolution: T | undefined): boolean { + if (oldResolution === newResolution) { + return true; } - - let nonRecursive = true; - // Use some ancestor of the root directory - let subDirectoryPath: Path | undefined, subDirectory: string | undefined; - if (rootPath !== undefined) { - while (!isInDirectoryPath(dirPath, rootPath)) { - const parentPath = getDirectoryPath(dirPath); - if (parentPath === dirPath) { - break; - } - nonRecursive = false; - subDirectoryPath = dirPath; - subDirectory = dir; - dirPath = parentPath; - dir = getDirectoryPath(dir); - } - } - - return canWatchDirectory(dirPath) ? { dir: subDirectory || dir, dirPath: subDirectoryPath || dirPath, nonRecursive } : undefined; - } - - function isPathWithDefaultFailedLookupExtension(path: Path) { - return fileExtensionIsOneOf(path, failedLookupDefaultExtensions); - } - - function watchFailedLookupLocationsOfExternalModuleResolutions(name: string, resolution: ResolutionWithFailedLookupLocations) { - // No need to set the resolution refCount - if (resolution.failedLookupLocations && resolution.failedLookupLocations.length) { - if (resolution.refCount) { - resolution.refCount++; - } - else { - resolution.refCount = 1; - if (isExternalModuleNameRelative(name)) { - watchFailedLookupLocationOfResolution(resolution); - } - else { - nonRelativeExternalModuleResolutions.add(name, resolution); - } - } + if (!oldResolution || !newResolution) { + return false; } - } - - function watchFailedLookupLocationOfResolution(resolution: ResolutionWithFailedLookupLocations) { - Debug.assert(!!resolution.refCount); - - const { failedLookupLocations } = resolution; - let setAtRoot = false; - for (const failedLookupLocation of failedLookupLocations) { - const failedLookupLocationPath = resolutionHost.toPath(failedLookupLocation); - const toWatch = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath); - if (toWatch) { - const { dir, dirPath, nonRecursive } = toWatch; - // If the failed lookup location path is not one of the supported extensions, - // store it in the custom path - if (!isPathWithDefaultFailedLookupExtension(failedLookupLocationPath)) { - const refCount = customFailedLookupPaths.get(failedLookupLocationPath) || 0; - customFailedLookupPaths.set(failedLookupLocationPath, refCount + 1); - } - if (dirPath === rootPath) { - Debug.assert(!nonRecursive); - setAtRoot = true; - } - else { - setDirectoryWatcher(dir, dirPath, nonRecursive); - } - } + const oldResult = getResolutionWithResolvedFileName(oldResolution); + const newResult = getResolutionWithResolvedFileName(newResolution); + if (oldResult === newResult) { + return true; } - - if (setAtRoot) { - // This is always non recursive - setDirectoryWatcher(rootDir!, rootPath, /*nonRecursive*/ true); // TODO: GH#18217 + if (!oldResult || !newResult) { + return false; } + return oldResult.resolvedFileName === newResult.resolvedFileName; } - - function setRefCountToUndefined(resolution: ResolutionWithFailedLookupLocations) { - resolution.refCount = undefined; - } - - function watchFailedLookupLocationOfNonRelativeModuleResolutions(resolutions: ResolutionWithFailedLookupLocations[], name: string) { - const program = resolutionHost.getCurrentProgram(); - const updateResolution = program && program.getTypeChecker().tryFindAmbientModuleWithoutAugmentations(name) ? - setRefCountToUndefined : watchFailedLookupLocationOfResolution; - resolutions.forEach(updateResolution); - } - - function setDirectoryWatcher(dir: string, dirPath: Path, nonRecursive?: boolean) { - const dirWatcher = directoryWatchesOfFailedLookups.get(dirPath); - if (dirWatcher) { - Debug.assert(!!nonRecursive === !!dirWatcher.nonRecursive); - dirWatcher.refCount++; + } + function resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string, redirectedReference?: ResolvedProjectReference): (ResolvedTypeReferenceDirective | undefined)[] { + return resolveNamesWithLocalCache(typeDirectiveNames, containingFile, redirectedReference, resolvedTypeReferenceDirectives, perDirectoryResolvedTypeReferenceDirectives, resolveTypeReferenceDirective, getResolvedTypeReferenceDirective, + /*shouldRetryResolution*/ resolution => resolution.resolvedTypeReferenceDirective === undefined, + /*reusedNames*/ undefined, /*logChanges*/ false); + } + function resolveModuleNames(moduleNames: string[], containingFile: string, reusedNames: string[] | undefined, redirectedReference?: ResolvedProjectReference): (ResolvedModuleFull | undefined)[] { + return resolveNamesWithLocalCache(moduleNames, containingFile, redirectedReference, resolvedModuleNames, perDirectoryResolvedModuleNames, resolveModuleName, getResolvedModule, + /*shouldRetryResolution*/ resolution => !resolution.resolvedModule || !resolutionExtensionIsTSOrJson(resolution.resolvedModule.extension), reusedNames, logChangesWhenResolvingModule); + } + function getResolvedModuleWithFailedLookupLocationsFromCache(moduleName: string, containingFile: string): CachedResolvedModuleWithFailedLookupLocations | undefined { + const cache = resolvedModuleNames.get(resolutionHost.toPath(containingFile)); + return cache && cache.get(moduleName); + } + function isNodeModulesDirectory(dirPath: Path) { + return endsWith(dirPath, "/node_modules"); + } + function isNodeModulesAtTypesDirectory(dirPath: Path) { + return endsWith(dirPath, "/node_modules/@types"); + } + function getDirectoryToWatchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path): DirectoryOfFailedLookupWatch | undefined { + if (isInDirectoryPath(rootPath, failedLookupLocationPath)) { + // Ensure failed look up is normalized path + failedLookupLocation = isRootedDiskPath(failedLookupLocation) ? normalizePath(failedLookupLocation) : getNormalizedAbsolutePath(failedLookupLocation, getCurrentDirectory()); + const failedLookupPathSplit = failedLookupLocationPath.split(directorySeparator); + const failedLookupSplit = failedLookupLocation.split(directorySeparator); + Debug.assert(failedLookupSplit.length === failedLookupPathSplit.length, `FailedLookup: ${failedLookupLocation} failedLookupLocationPath: ${failedLookupLocationPath}`); + if (failedLookupPathSplit.length > rootSplitLength + 1) { + // Instead of watching root, watch directory in root to avoid watching excluded directories not needed for module resolution + return { + dir: failedLookupSplit.slice(0, rootSplitLength + 1).join(directorySeparator), + dirPath: (failedLookupPathSplit.slice(0, rootSplitLength + 1).join(directorySeparator) as Path) + }; } else { - directoryWatchesOfFailedLookups.set(dirPath, { watcher: createDirectoryWatcher(dir, dirPath, nonRecursive), refCount: 1, nonRecursive }); + // Always watch root directory non recursively + return { + dir: rootDir!, + dirPath: rootPath, + nonRecursive: false + }; } } - - function stopWatchFailedLookupLocationOfResolution(resolution: ResolutionWithFailedLookupLocations) { - if (!resolution.refCount) { - return; + return getDirectoryToWatchFromFailedLookupLocationDirectory(getDirectoryPath(getNormalizedAbsolutePath(failedLookupLocation, getCurrentDirectory())), getDirectoryPath(failedLookupLocationPath)); + } + function getDirectoryToWatchFromFailedLookupLocationDirectory(dir: string, dirPath: Path): DirectoryOfFailedLookupWatch | undefined { + // If directory path contains node module, get the most parent node_modules directory for watching + while (pathContainsNodeModules(dirPath)) { + dir = getDirectoryPath(dir); + dirPath = getDirectoryPath(dirPath); + } + // If the directory is node_modules use it to watch, always watch it recursively + if (isNodeModulesDirectory(dirPath)) { + return canWatchDirectory(getDirectoryPath(dirPath)) ? { dir, dirPath } : undefined; + } + let nonRecursive = true; + // Use some ancestor of the root directory + let subDirectoryPath: Path | undefined, subDirectory: string | undefined; + if (rootPath !== undefined) { + while (!isInDirectoryPath(dirPath, rootPath)) { + const parentPath = getDirectoryPath(dirPath); + if (parentPath === dirPath) { + break; + } + nonRecursive = false; + subDirectoryPath = dirPath; + subDirectory = dir; + dirPath = parentPath; + dir = getDirectoryPath(dir); } - - resolution.refCount--; + } + return canWatchDirectory(dirPath) ? { dir: subDirectory || dir, dirPath: subDirectoryPath || dirPath, nonRecursive } : undefined; + } + function isPathWithDefaultFailedLookupExtension(path: Path) { + return fileExtensionIsOneOf(path, failedLookupDefaultExtensions); + } + function watchFailedLookupLocationsOfExternalModuleResolutions(name: string, resolution: ResolutionWithFailedLookupLocations) { + // No need to set the resolution refCount + if (resolution.failedLookupLocations && resolution.failedLookupLocations.length) { if (resolution.refCount) { - return; + resolution.refCount++; } - - const { failedLookupLocations } = resolution; - let removeAtRoot = false; - for (const failedLookupLocation of failedLookupLocations) { - const failedLookupLocationPath = resolutionHost.toPath(failedLookupLocation); - const toWatch = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath); - if (toWatch) { - const { dirPath } = toWatch; - const refCount = customFailedLookupPaths.get(failedLookupLocationPath); - if (refCount) { - if (refCount === 1) { - customFailedLookupPaths.delete(failedLookupLocationPath); - } - else { - Debug.assert(refCount > 1); - customFailedLookupPaths.set(failedLookupLocationPath, refCount - 1); - } - } - - if (dirPath === rootPath) { - removeAtRoot = true; - } - else { - removeDirectoryWatcher(dirPath); - } + else { + resolution.refCount = 1; + if (isExternalModuleNameRelative(name)) { + watchFailedLookupLocationOfResolution(resolution); + } + else { + nonRelativeExternalModuleResolutions.add(name, resolution); } - } - if (removeAtRoot) { - removeDirectoryWatcher(rootPath); } } - - function removeDirectoryWatcher(dirPath: string) { - const dirWatcher = directoryWatchesOfFailedLookups.get(dirPath)!; - // Do not close the watcher yet since it might be needed by other failed lookup locations. - dirWatcher.refCount--; - } - - function createDirectoryWatcher(directory: string, dirPath: Path, nonRecursive: boolean | undefined) { - return resolutionHost.watchDirectoryOfFailedLookupLocation(directory, fileOrDirectory => { - const fileOrDirectoryPath = resolutionHost.toPath(fileOrDirectory); - if (cachedDirectoryStructureHost) { - // Since the file existence changed, update the sourceFiles cache - cachedDirectoryStructureHost.addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath); + } + function watchFailedLookupLocationOfResolution(resolution: ResolutionWithFailedLookupLocations) { + Debug.assert(!!resolution.refCount); + const { failedLookupLocations } = resolution; + let setAtRoot = false; + for (const failedLookupLocation of failedLookupLocations) { + const failedLookupLocationPath = resolutionHost.toPath(failedLookupLocation); + const toWatch = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath); + if (toWatch) { + const { dir, dirPath, nonRecursive } = toWatch; + // If the failed lookup location path is not one of the supported extensions, + // store it in the custom path + if (!isPathWithDefaultFailedLookupExtension(failedLookupLocationPath)) { + const refCount = customFailedLookupPaths.get(failedLookupLocationPath) || 0; + customFailedLookupPaths.set(failedLookupLocationPath, refCount + 1); + } + if (dirPath === rootPath) { + Debug.assert(!nonRecursive); + setAtRoot = true; } - - if (!allFilesHaveInvalidatedResolution && invalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath, dirPath === fileOrDirectoryPath)) { - resolutionHost.onInvalidatedResolution(); + else { + setDirectoryWatcher(dir, dirPath, nonRecursive); } - }, nonRecursive ? WatchDirectoryFlags.None : WatchDirectoryFlags.Recursive); - } - - function removeResolutionsOfFileFromCache(cache: Map>, filePath: Path) { - // Deleted file, stop watching failed lookups for all the resolutions in the file - const resolutions = cache.get(filePath); - if (resolutions) { - resolutions.forEach(stopWatchFailedLookupLocationOfResolution); - cache.delete(filePath); } } - - function removeResolutionsFromProjectReferenceRedirects(filePath: Path) { - if (!fileExtensionIs(filePath, Extension.Json)) { return; } - - const program = resolutionHost.getCurrentProgram(); - if (!program) { return; } - - // If this file is input file for the referenced project, get it - const resolvedProjectReference = program.getResolvedProjectReferenceByPath(filePath); - if (!resolvedProjectReference) { return; } - - // filePath is for the projectReference and the containing file is from this project reference, invalidate the resolution - resolvedProjectReference.commandLine.fileNames.forEach(f => removeResolutionsOfFile(resolutionHost.toPath(f))); - } - - function removeResolutionsOfFile(filePath: Path) { - removeResolutionsOfFileFromCache(resolvedModuleNames, filePath); - removeResolutionsOfFileFromCache(resolvedTypeReferenceDirectives, filePath); - } - - function invalidateResolutionCache( - cache: Map>, - isInvalidatedResolution: (resolution: T, getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName) => boolean, - getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName - ) { - const seen = createMap>(); - cache.forEach((resolutions, containingFilePath) => { - const dirPath = getDirectoryPath(containingFilePath); - let seenInDir = seen.get(dirPath); - if (!seenInDir) { - seenInDir = createMap(); - seen.set(dirPath, seenInDir); - } - resolutions.forEach((resolution, name) => { - if (seenInDir!.has(name)) { - return; + if (setAtRoot) { + // This is always non recursive + setDirectoryWatcher(rootDir!, rootPath, /*nonRecursive*/ true); // TODO: GH#18217 + } + } + function setRefCountToUndefined(resolution: ResolutionWithFailedLookupLocations) { + resolution.refCount = undefined; + } + function watchFailedLookupLocationOfNonRelativeModuleResolutions(resolutions: ResolutionWithFailedLookupLocations[], name: string) { + const program = resolutionHost.getCurrentProgram(); + const updateResolution = program && program.getTypeChecker().tryFindAmbientModuleWithoutAugmentations(name) ? + setRefCountToUndefined : watchFailedLookupLocationOfResolution; + resolutions.forEach(updateResolution); + } + function setDirectoryWatcher(dir: string, dirPath: Path, nonRecursive?: boolean) { + const dirWatcher = directoryWatchesOfFailedLookups.get(dirPath); + if (dirWatcher) { + Debug.assert(!!nonRecursive === !!dirWatcher.nonRecursive); + dirWatcher.refCount++; + } + else { + directoryWatchesOfFailedLookups.set(dirPath, { watcher: createDirectoryWatcher(dir, dirPath, nonRecursive), refCount: 1, nonRecursive }); + } + } + function stopWatchFailedLookupLocationOfResolution(resolution: ResolutionWithFailedLookupLocations) { + if (!resolution.refCount) { + return; + } + resolution.refCount--; + if (resolution.refCount) { + return; + } + const { failedLookupLocations } = resolution; + let removeAtRoot = false; + for (const failedLookupLocation of failedLookupLocations) { + const failedLookupLocationPath = resolutionHost.toPath(failedLookupLocation); + const toWatch = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath); + if (toWatch) { + const { dirPath } = toWatch; + const refCount = customFailedLookupPaths.get(failedLookupLocationPath); + if (refCount) { + if (refCount === 1) { + customFailedLookupPaths.delete(failedLookupLocationPath); } - seenInDir!.set(name, true); - if (!resolution.isInvalidated && isInvalidatedResolution(resolution, getResolutionWithResolvedFileName)) { - // Mark the file as needing re-evaluation of module resolution instead of using it blindly. - resolution.isInvalidated = true; - (filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap())).set(containingFilePath, true); - - // When its a file with inferred types resolution, invalidate type reference directive resolution - if (endsWith(containingFilePath, inferredTypesContainingFile)) { - resolutionHost.onChangedAutomaticTypeDirectiveNames(); - } + else { + Debug.assert(refCount > 1); + customFailedLookupPaths.set(failedLookupLocationPath, refCount - 1); } - }); - }); - } - - function hasReachedResolutionIterationLimit() { - const maxSize = resolutionHost.maxNumberOfFilesToIterateForInvalidation || maxNumberOfFilesToIterateForInvalidation; - return resolvedModuleNames.size > maxSize || resolvedTypeReferenceDirectives.size > maxSize; - } - - function invalidateResolutions( - isInvalidatedResolution: (resolution: ResolutionWithFailedLookupLocations, getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName) => boolean, - ) { - // If more than maxNumberOfFilesToIterateForInvalidation present, - // just invalidated all files and recalculate the resolutions for files instead - if (hasReachedResolutionIterationLimit()) { - allFilesHaveInvalidatedResolution = true; - return; - } - invalidateResolutionCache(resolvedModuleNames, isInvalidatedResolution, getResolvedModule); - invalidateResolutionCache(resolvedTypeReferenceDirectives, isInvalidatedResolution, getResolvedTypeReferenceDirective); - } - - function invalidateResolutionOfFile(filePath: Path) { - removeResolutionsOfFile(filePath); - invalidateResolutions( - // Resolution is invalidated if the resulting file name is same as the deleted file path - (resolution, getResolutionWithResolvedFileName) => { - const result = getResolutionWithResolvedFileName(resolution); - return !!result && resolutionHost.toPath(result.resolvedFileName!) === filePath; // TODO: GH#18217 } - ); - } - - function setFilesWithInvalidatedNonRelativeUnresolvedImports(filesMap: ReadonlyMap) { - Debug.assert(filesWithInvalidatedNonRelativeUnresolvedImports === filesMap || filesWithInvalidatedNonRelativeUnresolvedImports === undefined); - filesWithInvalidatedNonRelativeUnresolvedImports = filesMap; - } - - function invalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath: Path, isCreatingWatchedDirectory: boolean) { - let isChangedFailedLookupLocation: (location: string) => boolean; - if (isCreatingWatchedDirectory) { - // Watching directory is created - // Invalidate any resolution has failed lookup in this directory - isChangedFailedLookupLocation = location => isInDirectoryPath(fileOrDirectoryPath, resolutionHost.toPath(location)); - } - else { - // If something to do with folder/file starting with "." in node_modules folder, skip it - const updatedPath = removeIgnoredPath(fileOrDirectoryPath); - if (!updatedPath) return false; - fileOrDirectoryPath = updatedPath; - - // prevent saving an open file from over-eagerly triggering invalidation - if (resolutionHost.fileIsOpen(fileOrDirectoryPath)) { - return false; - } - - // Some file or directory in the watching directory is created - // Return early if it does not have any of the watching extension or not the custom failed lookup path - const dirOfFileOrDirectory = getDirectoryPath(fileOrDirectoryPath); - if (isNodeModulesAtTypesDirectory(fileOrDirectoryPath) || isNodeModulesDirectory(fileOrDirectoryPath) || - isNodeModulesAtTypesDirectory(dirOfFileOrDirectory) || isNodeModulesDirectory(dirOfFileOrDirectory)) { - // Invalidate any resolution from this directory - isChangedFailedLookupLocation = location => { - const locationPath = resolutionHost.toPath(location); - return locationPath === fileOrDirectoryPath || startsWith(resolutionHost.toPath(location), fileOrDirectoryPath); - }; + if (dirPath === rootPath) { + removeAtRoot = true; } else { - if (!isPathWithDefaultFailedLookupExtension(fileOrDirectoryPath) && !customFailedLookupPaths.has(fileOrDirectoryPath)) { - return false; - } - // Ignore emits from the program - if (isEmittedFileOfProgram(resolutionHost.getCurrentProgram(), fileOrDirectoryPath)) { - return false; - } - // Resolution need to be invalidated if failed lookup location is same as the file or directory getting created - isChangedFailedLookupLocation = location => resolutionHost.toPath(location) === fileOrDirectoryPath; + removeDirectoryWatcher(dirPath); } } - const hasChangedFailedLookupLocation = (resolution: ResolutionWithFailedLookupLocations) => some(resolution.failedLookupLocations, isChangedFailedLookupLocation); - const invalidatedFilesCount = filesWithInvalidatedResolutions && filesWithInvalidatedResolutions.size; - invalidateResolutions( - // Resolution is invalidated if the resulting file name is same as the deleted file path - hasChangedFailedLookupLocation - ); - return allFilesHaveInvalidatedResolution || filesWithInvalidatedResolutions && filesWithInvalidatedResolutions.size !== invalidatedFilesCount; - } - - function closeTypeRootsWatch() { - clearMap(typeRootsWatches, closeFileWatcher); - } - - function getDirectoryToWatchFailedLookupLocationFromTypeRoot(typeRoot: string, typeRootPath: Path): Path | undefined { - if (allFilesHaveInvalidatedResolution) { - return undefined; + } + if (removeAtRoot) { + removeDirectoryWatcher(rootPath); + } + } + function removeDirectoryWatcher(dirPath: string) { + const dirWatcher = directoryWatchesOfFailedLookups.get(dirPath)!; + // Do not close the watcher yet since it might be needed by other failed lookup locations. + dirWatcher.refCount--; + } + function createDirectoryWatcher(directory: string, dirPath: Path, nonRecursive: boolean | undefined) { + return resolutionHost.watchDirectoryOfFailedLookupLocation(directory, fileOrDirectory => { + const fileOrDirectoryPath = resolutionHost.toPath(fileOrDirectory); + if (cachedDirectoryStructureHost) { + // Since the file existence changed, update the sourceFiles cache + cachedDirectoryStructureHost.addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath); } - - if (isInDirectoryPath(rootPath, typeRootPath)) { - return rootPath; + if (!allFilesHaveInvalidatedResolution && invalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath, dirPath === fileOrDirectoryPath)) { + resolutionHost.onInvalidatedResolution(); } - const toWatch = getDirectoryToWatchFromFailedLookupLocationDirectory(typeRoot, typeRootPath); - return toWatch && directoryWatchesOfFailedLookups.has(toWatch.dirPath) ? toWatch.dirPath : undefined; - } - - function createTypeRootsWatch(typeRootPath: Path, typeRoot: string): FileWatcher { - // Create new watch and recursive info - return resolutionHost.watchTypeRootsDirectory(typeRoot, fileOrDirectory => { - const fileOrDirectoryPath = resolutionHost.toPath(fileOrDirectory); - if (cachedDirectoryStructureHost) { - // Since the file existence changed, update the sourceFiles cache - cachedDirectoryStructureHost.addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath); + }, nonRecursive ? WatchDirectoryFlags.None : WatchDirectoryFlags.Recursive); + } + function removeResolutionsOfFileFromCache(cache: ts.Map>, filePath: Path) { + // Deleted file, stop watching failed lookups for all the resolutions in the file + const resolutions = cache.get(filePath); + if (resolutions) { + resolutions.forEach(stopWatchFailedLookupLocationOfResolution); + cache.delete(filePath); + } + } + function removeResolutionsFromProjectReferenceRedirects(filePath: Path) { + if (!fileExtensionIs(filePath, Extension.Json)) { + return; + } + const program = resolutionHost.getCurrentProgram(); + if (!program) { + return; + } + // If this file is input file for the referenced project, get it + const resolvedProjectReference = program.getResolvedProjectReferenceByPath(filePath); + if (!resolvedProjectReference) { + return; + } + // filePath is for the projectReference and the containing file is from this project reference, invalidate the resolution + resolvedProjectReference.commandLine.fileNames.forEach(f => removeResolutionsOfFile(resolutionHost.toPath(f))); + } + function removeResolutionsOfFile(filePath: Path) { + removeResolutionsOfFileFromCache(resolvedModuleNames, filePath); + removeResolutionsOfFileFromCache(resolvedTypeReferenceDirectives, filePath); + } + function invalidateResolutionCache(cache: ts.Map>, isInvalidatedResolution: (resolution: T, getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName) => boolean, getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName) { + const seen = createMap>(); + cache.forEach((resolutions, containingFilePath) => { + const dirPath = getDirectoryPath(containingFilePath); + let seenInDir = seen.get(dirPath); + if (!seenInDir) { + seenInDir = createMap(); + seen.set(dirPath, seenInDir); + } + resolutions.forEach((resolution, name) => { + if (seenInDir!.has(name)) { + return; } - - // For now just recompile - // We could potentially store more data here about whether it was/would be really be used or not - // and with that determine to trigger compilation but for now this is enough - resolutionHost.onChangedAutomaticTypeDirectiveNames(); - - // Since directory watchers invoked are flaky, the failed lookup location events might not be triggered - // So handle to failed lookup locations here as well to ensure we are invalidating resolutions - const dirPath = getDirectoryToWatchFailedLookupLocationFromTypeRoot(typeRoot, typeRootPath); - if (dirPath && invalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath, dirPath === fileOrDirectoryPath)) { - resolutionHost.onInvalidatedResolution(); + seenInDir!.set(name, true); + if (!resolution.isInvalidated && isInvalidatedResolution(resolution, getResolutionWithResolvedFileName)) { + // Mark the file as needing re-evaluation of module resolution instead of using it blindly. + resolution.isInvalidated = true; + (filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap())).set(containingFilePath, true); + // When its a file with inferred types resolution, invalidate type reference directive resolution + if (endsWith(containingFilePath, inferredTypesContainingFile)) { + resolutionHost.onChangedAutomaticTypeDirectiveNames(); + } } - }, WatchDirectoryFlags.Recursive); - } - - /** - * Watches the types that would get added as part of getAutomaticTypeDirectiveNames - * To be called when compiler options change - */ - function updateTypeRootsWatch() { - const options = resolutionHost.getCompilationSettings(); - if (options.types) { - // No need to do any watch since resolution cache is going to handle the failed lookups - // for the types added by this - closeTypeRootsWatch(); - return; + }); + }); + } + function hasReachedResolutionIterationLimit() { + const maxSize = resolutionHost.maxNumberOfFilesToIterateForInvalidation || maxNumberOfFilesToIterateForInvalidation; + return resolvedModuleNames.size > maxSize || resolvedTypeReferenceDirectives.size > maxSize; + } + function invalidateResolutions(isInvalidatedResolution: (resolution: ResolutionWithFailedLookupLocations, getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName) => boolean) { + // If more than maxNumberOfFilesToIterateForInvalidation present, + // just invalidated all files and recalculate the resolutions for files instead + if (hasReachedResolutionIterationLimit()) { + allFilesHaveInvalidatedResolution = true; + return; + } + invalidateResolutionCache(resolvedModuleNames, isInvalidatedResolution, getResolvedModule); + invalidateResolutionCache(resolvedTypeReferenceDirectives, isInvalidatedResolution, getResolvedTypeReferenceDirective); + } + function invalidateResolutionOfFile(filePath: Path) { + removeResolutionsOfFile(filePath); + invalidateResolutions( + // Resolution is invalidated if the resulting file name is same as the deleted file path + (resolution, getResolutionWithResolvedFileName) => { + const result = getResolutionWithResolvedFileName(resolution); + return !!result && resolutionHost.toPath(result.resolvedFileName!) === filePath; // TODO: GH#18217 + }); + } + function setFilesWithInvalidatedNonRelativeUnresolvedImports(filesMap: ts.ReadonlyMap) { + Debug.assert(filesWithInvalidatedNonRelativeUnresolvedImports === filesMap || filesWithInvalidatedNonRelativeUnresolvedImports === undefined); + filesWithInvalidatedNonRelativeUnresolvedImports = filesMap; + } + function invalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath: Path, isCreatingWatchedDirectory: boolean) { + let isChangedFailedLookupLocation: (location: string) => boolean; + if (isCreatingWatchedDirectory) { + // Watching directory is created + // Invalidate any resolution has failed lookup in this directory + isChangedFailedLookupLocation = location => isInDirectoryPath(fileOrDirectoryPath, resolutionHost.toPath(location)); + } + else { + // If something to do with folder/file starting with "." in node_modules folder, skip it + const updatedPath = removeIgnoredPath(fileOrDirectoryPath); + if (!updatedPath) + return false; + fileOrDirectoryPath = updatedPath; + // prevent saving an open file from over-eagerly triggering invalidation + if (resolutionHost.fileIsOpen(fileOrDirectoryPath)) { + return false; } - - // we need to assume the directories exist to ensure that we can get all the type root directories that get included - // But filter directories that are at root level to say directory doesnt exist, so that we arent watching them - const typeRoots = getEffectiveTypeRoots(options, { directoryExists: directoryExistsForTypeRootWatch, getCurrentDirectory }); - if (typeRoots) { - mutateMap( - typeRootsWatches, - arrayToMap(typeRoots, tr => resolutionHost.toPath(tr)), - { - createNewValue: createTypeRootsWatch, - onDeleteValue: closeFileWatcher - } - ); + // Some file or directory in the watching directory is created + // Return early if it does not have any of the watching extension or not the custom failed lookup path + const dirOfFileOrDirectory = getDirectoryPath(fileOrDirectoryPath); + if (isNodeModulesAtTypesDirectory(fileOrDirectoryPath) || isNodeModulesDirectory(fileOrDirectoryPath) || + isNodeModulesAtTypesDirectory(dirOfFileOrDirectory) || isNodeModulesDirectory(dirOfFileOrDirectory)) { + // Invalidate any resolution from this directory + isChangedFailedLookupLocation = location => { + const locationPath = resolutionHost.toPath(location); + return locationPath === fileOrDirectoryPath || startsWith(resolutionHost.toPath(location), fileOrDirectoryPath); + }; } else { - closeTypeRootsWatch(); + if (!isPathWithDefaultFailedLookupExtension(fileOrDirectoryPath) && !customFailedLookupPaths.has(fileOrDirectoryPath)) { + return false; + } + // Ignore emits from the program + if (isEmittedFileOfProgram(resolutionHost.getCurrentProgram(), fileOrDirectoryPath)) { + return false; + } + // Resolution need to be invalidated if failed lookup location is same as the file or directory getting created + isChangedFailedLookupLocation = location => resolutionHost.toPath(location) === fileOrDirectoryPath; } } - - /** - * Use this function to return if directory exists to get type roots to watch - * If we return directory exists then only the paths will be added to type roots - * Hence return true for all directories except root directories which are filtered from watching - */ - function directoryExistsForTypeRootWatch(nodeTypesDirectory: string) { - const dir = getDirectoryPath(getDirectoryPath(nodeTypesDirectory)); - const dirPath = resolutionHost.toPath(dir); - return dirPath === rootPath || canWatchDirectory(dirPath); + const hasChangedFailedLookupLocation = (resolution: ResolutionWithFailedLookupLocations) => some(resolution.failedLookupLocations, isChangedFailedLookupLocation); + const invalidatedFilesCount = filesWithInvalidatedResolutions && filesWithInvalidatedResolutions.size; + invalidateResolutions( + // Resolution is invalidated if the resulting file name is same as the deleted file path + hasChangedFailedLookupLocation); + return allFilesHaveInvalidatedResolution || filesWithInvalidatedResolutions && filesWithInvalidatedResolutions.size !== invalidatedFilesCount; + } + function closeTypeRootsWatch() { + clearMap(typeRootsWatches, closeFileWatcher); + } + function getDirectoryToWatchFailedLookupLocationFromTypeRoot(typeRoot: string, typeRootPath: Path): Path | undefined { + if (allFilesHaveInvalidatedResolution) { + return undefined; + } + if (isInDirectoryPath(rootPath, typeRootPath)) { + return rootPath; } + const toWatch = getDirectoryToWatchFromFailedLookupLocationDirectory(typeRoot, typeRootPath); + return toWatch && directoryWatchesOfFailedLookups.has(toWatch.dirPath) ? toWatch.dirPath : undefined; + } + function createTypeRootsWatch(typeRootPath: Path, typeRoot: string): FileWatcher { + // Create new watch and recursive info + return resolutionHost.watchTypeRootsDirectory(typeRoot, fileOrDirectory => { + const fileOrDirectoryPath = resolutionHost.toPath(fileOrDirectory); + if (cachedDirectoryStructureHost) { + // Since the file existence changed, update the sourceFiles cache + cachedDirectoryStructureHost.addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath); + } + // For now just recompile + // We could potentially store more data here about whether it was/would be really be used or not + // and with that determine to trigger compilation but for now this is enough + resolutionHost.onChangedAutomaticTypeDirectiveNames(); + // Since directory watchers invoked are flaky, the failed lookup location events might not be triggered + // So handle to failed lookup locations here as well to ensure we are invalidating resolutions + const dirPath = getDirectoryToWatchFailedLookupLocationFromTypeRoot(typeRoot, typeRootPath); + if (dirPath && invalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath, dirPath === fileOrDirectoryPath)) { + resolutionHost.onInvalidatedResolution(); + } + }, WatchDirectoryFlags.Recursive); + } + /** + * Watches the types that would get added as part of getAutomaticTypeDirectiveNames + * To be called when compiler options change + */ + function updateTypeRootsWatch() { + const options = resolutionHost.getCompilationSettings(); + if (options.types) { + // No need to do any watch since resolution cache is going to handle the failed lookups + // for the types added by this + closeTypeRootsWatch(); + return; + } + // we need to assume the directories exist to ensure that we can get all the type root directories that get included + // But filter directories that are at root level to say directory doesnt exist, so that we arent watching them + const typeRoots = getEffectiveTypeRoots(options, { directoryExists: directoryExistsForTypeRootWatch, getCurrentDirectory }); + if (typeRoots) { + mutateMap(typeRootsWatches, arrayToMap(typeRoots, tr => resolutionHost.toPath(tr)), { + createNewValue: createTypeRootsWatch, + onDeleteValue: closeFileWatcher + }); + } + else { + closeTypeRootsWatch(); + } + } + /** + * Use this function to return if directory exists to get type roots to watch + * If we return directory exists then only the paths will be added to type roots + * Hence return true for all directories except root directories which are filtered from watching + */ + function directoryExistsForTypeRootWatch(nodeTypesDirectory: string) { + const dir = getDirectoryPath(getDirectoryPath(nodeTypesDirectory)); + const dirPath = resolutionHost.toPath(dir); + return dirPath === rootPath || canWatchDirectory(dirPath); } } diff --git a/src/compiler/scanner.ts b/src/compiler/scanner.ts index 7377e2a4c6637..64e6536a3ea86 100644 --- a/src/compiler/scanner.ts +++ b/src/compiler/scanner.ts @@ -1,557 +1,688 @@ -namespace ts { - export type ErrorCallback = (message: DiagnosticMessage, length: number) => void; - +import { DiagnosticMessage, SyntaxKind, TokenFlags, JsxTokenSyntaxKind, JSDocSyntaxKind, ScriptTarget, LanguageVariant, MapLike, KeywordSyntaxKind, createMapFromTemplate, CharacterCodes, SourceFileLike, Debug, arraysEqual, LineAndCharacter, binarySearch, identity, compareValues, positionIsSynthesized, Diagnostics, CommentKind, CommentRange, parsePseudoBigInt } from "./ts"; +import * as ts from "./ts"; +export type ErrorCallback = (message: DiagnosticMessage, length: number) => void; +/* @internal */ +export function tokenIsIdentifierOrKeyword(token: SyntaxKind): boolean { + return token >= SyntaxKind.Identifier; +} +/* @internal */ +export function tokenIsIdentifierOrKeywordOrGreaterThan(token: SyntaxKind): boolean { + return token === SyntaxKind.GreaterThanToken || tokenIsIdentifierOrKeyword(token); +} +export interface Scanner { + getStartPos(): number; + getToken(): SyntaxKind; + getTextPos(): number; + getTokenPos(): number; + getTokenText(): string; + getTokenValue(): string; + hasUnicodeEscape(): boolean; + hasExtendedUnicodeEscape(): boolean; + hasPrecedingLineBreak(): boolean; + isIdentifier(): boolean; + isReservedWord(): boolean; + isUnterminated(): boolean; /* @internal */ - export function tokenIsIdentifierOrKeyword(token: SyntaxKind): boolean { - return token >= SyntaxKind.Identifier; - } - + getTokenFlags(): TokenFlags; + reScanGreaterToken(): SyntaxKind; + reScanSlashToken(): SyntaxKind; + reScanTemplateToken(isTaggedTemplate: boolean): SyntaxKind; + reScanTemplateHeadOrNoSubstitutionTemplate(): SyntaxKind; + scanJsxIdentifier(): SyntaxKind; + scanJsxAttributeValue(): SyntaxKind; + reScanJsxAttributeValue(): SyntaxKind; + reScanJsxToken(): JsxTokenSyntaxKind; + reScanLessThanToken(): SyntaxKind; + reScanQuestionToken(): SyntaxKind; + scanJsxToken(): JsxTokenSyntaxKind; + scanJsDocToken(): JSDocSyntaxKind; + scan(): SyntaxKind; + getText(): string; + // Sets the text for the scanner to scan. An optional subrange starting point and length + // can be provided to have the scanner only scan a portion of the text. + setText(text: string | undefined, start?: number, length?: number): void; + setOnError(onError: ErrorCallback | undefined): void; + setScriptTarget(scriptTarget: ScriptTarget): void; + setLanguageVariant(variant: LanguageVariant): void; + setTextPos(textPos: number): void; /* @internal */ - export function tokenIsIdentifierOrKeywordOrGreaterThan(token: SyntaxKind): boolean { - return token === SyntaxKind.GreaterThanToken || tokenIsIdentifierOrKeyword(token); - } - - export interface Scanner { - getStartPos(): number; - getToken(): SyntaxKind; - getTextPos(): number; - getTokenPos(): number; - getTokenText(): string; - getTokenValue(): string; - hasUnicodeEscape(): boolean; - hasExtendedUnicodeEscape(): boolean; - hasPrecedingLineBreak(): boolean; - isIdentifier(): boolean; - isReservedWord(): boolean; - isUnterminated(): boolean; - /* @internal */ - getTokenFlags(): TokenFlags; - reScanGreaterToken(): SyntaxKind; - reScanSlashToken(): SyntaxKind; - reScanTemplateToken(isTaggedTemplate: boolean): SyntaxKind; - reScanTemplateHeadOrNoSubstitutionTemplate(): SyntaxKind; - scanJsxIdentifier(): SyntaxKind; - scanJsxAttributeValue(): SyntaxKind; - reScanJsxAttributeValue(): SyntaxKind; - reScanJsxToken(): JsxTokenSyntaxKind; - reScanLessThanToken(): SyntaxKind; - reScanQuestionToken(): SyntaxKind; - scanJsxToken(): JsxTokenSyntaxKind; - scanJsDocToken(): JSDocSyntaxKind; - scan(): SyntaxKind; - getText(): string; - // Sets the text for the scanner to scan. An optional subrange starting point and length - // can be provided to have the scanner only scan a portion of the text. - setText(text: string | undefined, start?: number, length?: number): void; - setOnError(onError: ErrorCallback | undefined): void; - setScriptTarget(scriptTarget: ScriptTarget): void; - setLanguageVariant(variant: LanguageVariant): void; - setTextPos(textPos: number): void; - /* @internal */ - setInJSDocType(inType: boolean): void; - // Invokes the provided callback then unconditionally restores the scanner to the state it - // was in immediately prior to invoking the callback. The result of invoking the callback - // is returned from this function. - lookAhead(callback: () => T): T; - - // Invokes the callback with the scanner set to scan the specified range. When the callback - // returns, the scanner is restored to the state it was in before scanRange was called. - scanRange(start: number, length: number, callback: () => T): T; - - // Invokes the provided callback. If the callback returns something falsy, then it restores - // the scanner to the state it was in immediately prior to invoking the callback. If the - // callback returns something truthy, then the scanner state is not rolled back. The result - // of invoking the callback is returned from this function. - tryScan(callback: () => T): T; + setInJSDocType(inType: boolean): void; + // Invokes the provided callback then unconditionally restores the scanner to the state it + // was in immediately prior to invoking the callback. The result of invoking the callback + // is returned from this function. + lookAhead(callback: () => T): T; + // Invokes the callback with the scanner set to scan the specified range. When the callback + // returns, the scanner is restored to the state it was in before scanRange was called. + scanRange(start: number, length: number, callback: () => T): T; + // Invokes the provided callback. If the callback returns something falsy, then it restores + // the scanner to the state it was in immediately prior to invoking the callback. If the + // callback returns something truthy, then the scanner state is not rolled back. The result + // of invoking the callback is returned from this function. + tryScan(callback: () => T): T; +} +const textToKeywordObj: MapLike = { + abstract: SyntaxKind.AbstractKeyword, + any: SyntaxKind.AnyKeyword, + as: SyntaxKind.AsKeyword, + asserts: SyntaxKind.AssertsKeyword, + bigint: SyntaxKind.BigIntKeyword, + boolean: SyntaxKind.BooleanKeyword, + break: SyntaxKind.BreakKeyword, + case: SyntaxKind.CaseKeyword, + catch: SyntaxKind.CatchKeyword, + class: SyntaxKind.ClassKeyword, + continue: SyntaxKind.ContinueKeyword, + const: SyntaxKind.ConstKeyword, + ["" + "constructor"]: SyntaxKind.ConstructorKeyword, + debugger: SyntaxKind.DebuggerKeyword, + declare: SyntaxKind.DeclareKeyword, + default: SyntaxKind.DefaultKeyword, + delete: SyntaxKind.DeleteKeyword, + do: SyntaxKind.DoKeyword, + else: SyntaxKind.ElseKeyword, + enum: SyntaxKind.EnumKeyword, + export: SyntaxKind.ExportKeyword, + extends: SyntaxKind.ExtendsKeyword, + false: SyntaxKind.FalseKeyword, + finally: SyntaxKind.FinallyKeyword, + for: SyntaxKind.ForKeyword, + from: SyntaxKind.FromKeyword, + function: SyntaxKind.FunctionKeyword, + get: SyntaxKind.GetKeyword, + if: SyntaxKind.IfKeyword, + implements: SyntaxKind.ImplementsKeyword, + import: SyntaxKind.ImportKeyword, + in: SyntaxKind.InKeyword, + infer: SyntaxKind.InferKeyword, + instanceof: SyntaxKind.InstanceOfKeyword, + interface: SyntaxKind.InterfaceKeyword, + is: SyntaxKind.IsKeyword, + keyof: SyntaxKind.KeyOfKeyword, + let: SyntaxKind.LetKeyword, + module: SyntaxKind.ModuleKeyword, + namespace: SyntaxKind.NamespaceKeyword, + never: SyntaxKind.NeverKeyword, + new: SyntaxKind.NewKeyword, + null: SyntaxKind.NullKeyword, + number: SyntaxKind.NumberKeyword, + object: SyntaxKind.ObjectKeyword, + package: SyntaxKind.PackageKeyword, + private: SyntaxKind.PrivateKeyword, + protected: SyntaxKind.ProtectedKeyword, + public: SyntaxKind.PublicKeyword, + readonly: SyntaxKind.ReadonlyKeyword, + require: SyntaxKind.RequireKeyword, + global: SyntaxKind.GlobalKeyword, + return: SyntaxKind.ReturnKeyword, + set: SyntaxKind.SetKeyword, + static: SyntaxKind.StaticKeyword, + string: SyntaxKind.StringKeyword, + super: SyntaxKind.SuperKeyword, + switch: SyntaxKind.SwitchKeyword, + symbol: SyntaxKind.SymbolKeyword, + this: SyntaxKind.ThisKeyword, + throw: SyntaxKind.ThrowKeyword, + true: SyntaxKind.TrueKeyword, + try: SyntaxKind.TryKeyword, + type: SyntaxKind.TypeKeyword, + typeof: SyntaxKind.TypeOfKeyword, + undefined: SyntaxKind.UndefinedKeyword, + unique: SyntaxKind.UniqueKeyword, + unknown: SyntaxKind.UnknownKeyword, + var: SyntaxKind.VarKeyword, + void: SyntaxKind.VoidKeyword, + while: SyntaxKind.WhileKeyword, + with: SyntaxKind.WithKeyword, + yield: SyntaxKind.YieldKeyword, + async: SyntaxKind.AsyncKeyword, + await: SyntaxKind.AwaitKeyword, + of: SyntaxKind.OfKeyword, +}; +const textToKeyword = createMapFromTemplate(textToKeywordObj); +const textToToken = createMapFromTemplate({ + ...textToKeywordObj, + "{": SyntaxKind.OpenBraceToken, + "}": SyntaxKind.CloseBraceToken, + "(": SyntaxKind.OpenParenToken, + ")": SyntaxKind.CloseParenToken, + "[": SyntaxKind.OpenBracketToken, + "]": SyntaxKind.CloseBracketToken, + ".": SyntaxKind.DotToken, + "...": SyntaxKind.DotDotDotToken, + ";": SyntaxKind.SemicolonToken, + ",": SyntaxKind.CommaToken, + "<": SyntaxKind.LessThanToken, + ">": SyntaxKind.GreaterThanToken, + "<=": SyntaxKind.LessThanEqualsToken, + ">=": SyntaxKind.GreaterThanEqualsToken, + "==": SyntaxKind.EqualsEqualsToken, + "!=": SyntaxKind.ExclamationEqualsToken, + "===": SyntaxKind.EqualsEqualsEqualsToken, + "!==": SyntaxKind.ExclamationEqualsEqualsToken, + "=>": SyntaxKind.EqualsGreaterThanToken, + "+": SyntaxKind.PlusToken, + "-": SyntaxKind.MinusToken, + "**": SyntaxKind.AsteriskAsteriskToken, + "*": SyntaxKind.AsteriskToken, + "/": SyntaxKind.SlashToken, + "%": SyntaxKind.PercentToken, + "++": SyntaxKind.PlusPlusToken, + "--": SyntaxKind.MinusMinusToken, + "<<": SyntaxKind.LessThanLessThanToken, + ">": SyntaxKind.GreaterThanGreaterThanToken, + ">>>": SyntaxKind.GreaterThanGreaterThanGreaterThanToken, + "&": SyntaxKind.AmpersandToken, + "|": SyntaxKind.BarToken, + "^": SyntaxKind.CaretToken, + "!": SyntaxKind.ExclamationToken, + "~": SyntaxKind.TildeToken, + "&&": SyntaxKind.AmpersandAmpersandToken, + "||": SyntaxKind.BarBarToken, + "?": SyntaxKind.QuestionToken, + "??": SyntaxKind.QuestionQuestionToken, + "?.": SyntaxKind.QuestionDotToken, + ":": SyntaxKind.ColonToken, + "=": SyntaxKind.EqualsToken, + "+=": SyntaxKind.PlusEqualsToken, + "-=": SyntaxKind.MinusEqualsToken, + "*=": SyntaxKind.AsteriskEqualsToken, + "**=": SyntaxKind.AsteriskAsteriskEqualsToken, + "/=": SyntaxKind.SlashEqualsToken, + "%=": SyntaxKind.PercentEqualsToken, + "<<=": SyntaxKind.LessThanLessThanEqualsToken, + ">>=": SyntaxKind.GreaterThanGreaterThanEqualsToken, + ">>>=": SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken, + "&=": SyntaxKind.AmpersandEqualsToken, + "|=": SyntaxKind.BarEqualsToken, + "^=": SyntaxKind.CaretEqualsToken, + "@": SyntaxKind.AtToken, + "`": SyntaxKind.BacktickToken +}); +/* + As per ECMAScript Language Specification 3th Edition, Section 7.6: Identifiers + IdentifierStart :: + Can contain Unicode 3.0.0 categories: + Uppercase letter (Lu), + Lowercase letter (Ll), + Titlecase letter (Lt), + Modifier letter (Lm), + Other letter (Lo), or + Letter number (Nl). + IdentifierPart :: = + Can contain IdentifierStart + Unicode 3.0.0 categories: + Non-spacing mark (Mn), + Combining spacing mark (Mc), + Decimal number (Nd), or + Connector punctuation (Pc). + + Codepoint ranges for ES3 Identifiers are extracted from the Unicode 3.0.0 specification at: + http://www.unicode.org/Public/3.0-Update/UnicodeData-3.0.0.txt +*/ +const unicodeES3IdentifierStart = [170, 170, 181, 181, 186, 186, 192, 214, 216, 246, 248, 543, 546, 563, 592, 685, 688, 696, 699, 705, 720, 721, 736, 740, 750, 750, 890, 890, 902, 902, 904, 906, 908, 908, 910, 929, 931, 974, 976, 983, 986, 1011, 1024, 1153, 1164, 1220, 1223, 1224, 1227, 1228, 1232, 1269, 1272, 1273, 1329, 1366, 1369, 1369, 1377, 1415, 1488, 1514, 1520, 1522, 1569, 1594, 1600, 1610, 1649, 1747, 1749, 1749, 1765, 1766, 1786, 1788, 1808, 1808, 1810, 1836, 1920, 1957, 2309, 2361, 2365, 2365, 2384, 2384, 2392, 2401, 2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482, 2486, 2489, 2524, 2525, 2527, 2529, 2544, 2545, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611, 2613, 2614, 2616, 2617, 2649, 2652, 2654, 2654, 2674, 2676, 2693, 2699, 2701, 2701, 2703, 2705, 2707, 2728, 2730, 2736, 2738, 2739, 2741, 2745, 2749, 2749, 2768, 2768, 2784, 2784, 2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867, 2870, 2873, 2877, 2877, 2908, 2909, 2911, 2913, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 2997, 2999, 3001, 3077, 3084, 3086, 3088, 3090, 3112, 3114, 3123, 3125, 3129, 3168, 3169, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3294, 3294, 3296, 3297, 3333, 3340, 3342, 3344, 3346, 3368, 3370, 3385, 3424, 3425, 3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526, 3585, 3632, 3634, 3635, 3648, 3654, 3713, 3714, 3716, 3716, 3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743, 3745, 3747, 3749, 3749, 3751, 3751, 3754, 3755, 3757, 3760, 3762, 3763, 3773, 3773, 3776, 3780, 3782, 3782, 3804, 3805, 3840, 3840, 3904, 3911, 3913, 3946, 3976, 3979, 4096, 4129, 4131, 4135, 4137, 4138, 4176, 4181, 4256, 4293, 4304, 4342, 4352, 4441, 4447, 4514, 4520, 4601, 4608, 4614, 4616, 4678, 4680, 4680, 4682, 4685, 4688, 4694, 4696, 4696, 4698, 4701, 4704, 4742, 4744, 4744, 4746, 4749, 4752, 4782, 4784, 4784, 4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4814, 4816, 4822, 4824, 4846, 4848, 4878, 4880, 4880, 4882, 4885, 4888, 4894, 4896, 4934, 4936, 4954, 5024, 5108, 5121, 5740, 5743, 5750, 5761, 5786, 5792, 5866, 6016, 6067, 6176, 6263, 6272, 6312, 7680, 7835, 7840, 7929, 7936, 7957, 7960, 7965, 7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061, 8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140, 8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188, 8319, 8319, 8450, 8450, 8455, 8455, 8458, 8467, 8469, 8469, 8473, 8477, 8484, 8484, 8486, 8486, 8488, 8488, 8490, 8493, 8495, 8497, 8499, 8505, 8544, 8579, 12293, 12295, 12321, 12329, 12337, 12341, 12344, 12346, 12353, 12436, 12445, 12446, 12449, 12538, 12540, 12542, 12549, 12588, 12593, 12686, 12704, 12727, 13312, 19893, 19968, 40869, 40960, 42124, 44032, 55203, 63744, 64045, 64256, 64262, 64275, 64279, 64285, 64285, 64287, 64296, 64298, 64310, 64312, 64316, 64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433, 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019, 65136, 65138, 65140, 65140, 65142, 65276, 65313, 65338, 65345, 65370, 65382, 65470, 65474, 65479, 65482, 65487, 65490, 65495, 65498, 65500,]; +const unicodeES3IdentifierPart = [170, 170, 181, 181, 186, 186, 192, 214, 216, 246, 248, 543, 546, 563, 592, 685, 688, 696, 699, 705, 720, 721, 736, 740, 750, 750, 768, 846, 864, 866, 890, 890, 902, 902, 904, 906, 908, 908, 910, 929, 931, 974, 976, 983, 986, 1011, 1024, 1153, 1155, 1158, 1164, 1220, 1223, 1224, 1227, 1228, 1232, 1269, 1272, 1273, 1329, 1366, 1369, 1369, 1377, 1415, 1425, 1441, 1443, 1465, 1467, 1469, 1471, 1471, 1473, 1474, 1476, 1476, 1488, 1514, 1520, 1522, 1569, 1594, 1600, 1621, 1632, 1641, 1648, 1747, 1749, 1756, 1759, 1768, 1770, 1773, 1776, 1788, 1808, 1836, 1840, 1866, 1920, 1968, 2305, 2307, 2309, 2361, 2364, 2381, 2384, 2388, 2392, 2403, 2406, 2415, 2433, 2435, 2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482, 2486, 2489, 2492, 2492, 2494, 2500, 2503, 2504, 2507, 2509, 2519, 2519, 2524, 2525, 2527, 2531, 2534, 2545, 2562, 2562, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611, 2613, 2614, 2616, 2617, 2620, 2620, 2622, 2626, 2631, 2632, 2635, 2637, 2649, 2652, 2654, 2654, 2662, 2676, 2689, 2691, 2693, 2699, 2701, 2701, 2703, 2705, 2707, 2728, 2730, 2736, 2738, 2739, 2741, 2745, 2748, 2757, 2759, 2761, 2763, 2765, 2768, 2768, 2784, 2784, 2790, 2799, 2817, 2819, 2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867, 2870, 2873, 2876, 2883, 2887, 2888, 2891, 2893, 2902, 2903, 2908, 2909, 2911, 2913, 2918, 2927, 2946, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 2997, 2999, 3001, 3006, 3010, 3014, 3016, 3018, 3021, 3031, 3031, 3047, 3055, 3073, 3075, 3077, 3084, 3086, 3088, 3090, 3112, 3114, 3123, 3125, 3129, 3134, 3140, 3142, 3144, 3146, 3149, 3157, 3158, 3168, 3169, 3174, 3183, 3202, 3203, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3262, 3268, 3270, 3272, 3274, 3277, 3285, 3286, 3294, 3294, 3296, 3297, 3302, 3311, 3330, 3331, 3333, 3340, 3342, 3344, 3346, 3368, 3370, 3385, 3390, 3395, 3398, 3400, 3402, 3405, 3415, 3415, 3424, 3425, 3430, 3439, 3458, 3459, 3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526, 3530, 3530, 3535, 3540, 3542, 3542, 3544, 3551, 3570, 3571, 3585, 3642, 3648, 3662, 3664, 3673, 3713, 3714, 3716, 3716, 3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743, 3745, 3747, 3749, 3749, 3751, 3751, 3754, 3755, 3757, 3769, 3771, 3773, 3776, 3780, 3782, 3782, 3784, 3789, 3792, 3801, 3804, 3805, 3840, 3840, 3864, 3865, 3872, 3881, 3893, 3893, 3895, 3895, 3897, 3897, 3902, 3911, 3913, 3946, 3953, 3972, 3974, 3979, 3984, 3991, 3993, 4028, 4038, 4038, 4096, 4129, 4131, 4135, 4137, 4138, 4140, 4146, 4150, 4153, 4160, 4169, 4176, 4185, 4256, 4293, 4304, 4342, 4352, 4441, 4447, 4514, 4520, 4601, 4608, 4614, 4616, 4678, 4680, 4680, 4682, 4685, 4688, 4694, 4696, 4696, 4698, 4701, 4704, 4742, 4744, 4744, 4746, 4749, 4752, 4782, 4784, 4784, 4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4814, 4816, 4822, 4824, 4846, 4848, 4878, 4880, 4880, 4882, 4885, 4888, 4894, 4896, 4934, 4936, 4954, 4969, 4977, 5024, 5108, 5121, 5740, 5743, 5750, 5761, 5786, 5792, 5866, 6016, 6099, 6112, 6121, 6160, 6169, 6176, 6263, 6272, 6313, 7680, 7835, 7840, 7929, 7936, 7957, 7960, 7965, 7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061, 8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140, 8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188, 8255, 8256, 8319, 8319, 8400, 8412, 8417, 8417, 8450, 8450, 8455, 8455, 8458, 8467, 8469, 8469, 8473, 8477, 8484, 8484, 8486, 8486, 8488, 8488, 8490, 8493, 8495, 8497, 8499, 8505, 8544, 8579, 12293, 12295, 12321, 12335, 12337, 12341, 12344, 12346, 12353, 12436, 12441, 12442, 12445, 12446, 12449, 12542, 12549, 12588, 12593, 12686, 12704, 12727, 13312, 19893, 19968, 40869, 40960, 42124, 44032, 55203, 63744, 64045, 64256, 64262, 64275, 64279, 64285, 64296, 64298, 64310, 64312, 64316, 64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433, 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019, 65056, 65059, 65075, 65076, 65101, 65103, 65136, 65138, 65140, 65140, 65142, 65276, 65296, 65305, 65313, 65338, 65343, 65343, 65345, 65370, 65381, 65470, 65474, 65479, 65482, 65487, 65490, 65495, 65498, 65500,]; +/* + As per ECMAScript Language Specification 5th Edition, Section 7.6: ISyntaxToken Names and Identifiers + IdentifierStart :: + Can contain Unicode 6.2 categories: + Uppercase letter (Lu), + Lowercase letter (Ll), + Titlecase letter (Lt), + Modifier letter (Lm), + Other letter (Lo), or + Letter number (Nl). + IdentifierPart :: + Can contain IdentifierStart + Unicode 6.2 categories: + Non-spacing mark (Mn), + Combining spacing mark (Mc), + Decimal number (Nd), + Connector punctuation (Pc), + , or + . + + Codepoint ranges for ES5 Identifiers are extracted from the Unicode 6.2 specification at: + http://www.unicode.org/Public/6.2.0/ucd/UnicodeData.txt +*/ +const unicodeES5IdentifierStart = [170, 170, 181, 181, 186, 186, 192, 214, 216, 246, 248, 705, 710, 721, 736, 740, 748, 748, 750, 750, 880, 884, 886, 887, 890, 893, 902, 902, 904, 906, 908, 908, 910, 929, 931, 1013, 1015, 1153, 1162, 1319, 1329, 1366, 1369, 1369, 1377, 1415, 1488, 1514, 1520, 1522, 1568, 1610, 1646, 1647, 1649, 1747, 1749, 1749, 1765, 1766, 1774, 1775, 1786, 1788, 1791, 1791, 1808, 1808, 1810, 1839, 1869, 1957, 1969, 1969, 1994, 2026, 2036, 2037, 2042, 2042, 2048, 2069, 2074, 2074, 2084, 2084, 2088, 2088, 2112, 2136, 2208, 2208, 2210, 2220, 2308, 2361, 2365, 2365, 2384, 2384, 2392, 2401, 2417, 2423, 2425, 2431, 2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482, 2486, 2489, 2493, 2493, 2510, 2510, 2524, 2525, 2527, 2529, 2544, 2545, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611, 2613, 2614, 2616, 2617, 2649, 2652, 2654, 2654, 2674, 2676, 2693, 2701, 2703, 2705, 2707, 2728, 2730, 2736, 2738, 2739, 2741, 2745, 2749, 2749, 2768, 2768, 2784, 2785, 2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867, 2869, 2873, 2877, 2877, 2908, 2909, 2911, 2913, 2929, 2929, 2947, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001, 3024, 3024, 3077, 3084, 3086, 3088, 3090, 3112, 3114, 3123, 3125, 3129, 3133, 3133, 3160, 3161, 3168, 3169, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3261, 3261, 3294, 3294, 3296, 3297, 3313, 3314, 3333, 3340, 3342, 3344, 3346, 3386, 3389, 3389, 3406, 3406, 3424, 3425, 3450, 3455, 3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526, 3585, 3632, 3634, 3635, 3648, 3654, 3713, 3714, 3716, 3716, 3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743, 3745, 3747, 3749, 3749, 3751, 3751, 3754, 3755, 3757, 3760, 3762, 3763, 3773, 3773, 3776, 3780, 3782, 3782, 3804, 3807, 3840, 3840, 3904, 3911, 3913, 3948, 3976, 3980, 4096, 4138, 4159, 4159, 4176, 4181, 4186, 4189, 4193, 4193, 4197, 4198, 4206, 4208, 4213, 4225, 4238, 4238, 4256, 4293, 4295, 4295, 4301, 4301, 4304, 4346, 4348, 4680, 4682, 4685, 4688, 4694, 4696, 4696, 4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784, 4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880, 4882, 4885, 4888, 4954, 4992, 5007, 5024, 5108, 5121, 5740, 5743, 5759, 5761, 5786, 5792, 5866, 5870, 5872, 5888, 5900, 5902, 5905, 5920, 5937, 5952, 5969, 5984, 5996, 5998, 6000, 6016, 6067, 6103, 6103, 6108, 6108, 6176, 6263, 6272, 6312, 6314, 6314, 6320, 6389, 6400, 6428, 6480, 6509, 6512, 6516, 6528, 6571, 6593, 6599, 6656, 6678, 6688, 6740, 6823, 6823, 6917, 6963, 6981, 6987, 7043, 7072, 7086, 7087, 7098, 7141, 7168, 7203, 7245, 7247, 7258, 7293, 7401, 7404, 7406, 7409, 7413, 7414, 7424, 7615, 7680, 7957, 7960, 7965, 7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061, 8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140, 8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188, 8305, 8305, 8319, 8319, 8336, 8348, 8450, 8450, 8455, 8455, 8458, 8467, 8469, 8469, 8473, 8477, 8484, 8484, 8486, 8486, 8488, 8488, 8490, 8493, 8495, 8505, 8508, 8511, 8517, 8521, 8526, 8526, 8544, 8584, 11264, 11310, 11312, 11358, 11360, 11492, 11499, 11502, 11506, 11507, 11520, 11557, 11559, 11559, 11565, 11565, 11568, 11623, 11631, 11631, 11648, 11670, 11680, 11686, 11688, 11694, 11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726, 11728, 11734, 11736, 11742, 11823, 11823, 12293, 12295, 12321, 12329, 12337, 12341, 12344, 12348, 12353, 12438, 12445, 12447, 12449, 12538, 12540, 12543, 12549, 12589, 12593, 12686, 12704, 12730, 12784, 12799, 13312, 19893, 19968, 40908, 40960, 42124, 42192, 42237, 42240, 42508, 42512, 42527, 42538, 42539, 42560, 42606, 42623, 42647, 42656, 42735, 42775, 42783, 42786, 42888, 42891, 42894, 42896, 42899, 42912, 42922, 43000, 43009, 43011, 43013, 43015, 43018, 43020, 43042, 43072, 43123, 43138, 43187, 43250, 43255, 43259, 43259, 43274, 43301, 43312, 43334, 43360, 43388, 43396, 43442, 43471, 43471, 43520, 43560, 43584, 43586, 43588, 43595, 43616, 43638, 43642, 43642, 43648, 43695, 43697, 43697, 43701, 43702, 43705, 43709, 43712, 43712, 43714, 43714, 43739, 43741, 43744, 43754, 43762, 43764, 43777, 43782, 43785, 43790, 43793, 43798, 43808, 43814, 43816, 43822, 43968, 44002, 44032, 55203, 55216, 55238, 55243, 55291, 63744, 64109, 64112, 64217, 64256, 64262, 64275, 64279, 64285, 64285, 64287, 64296, 64298, 64310, 64312, 64316, 64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433, 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019, 65136, 65140, 65142, 65276, 65313, 65338, 65345, 65370, 65382, 65470, 65474, 65479, 65482, 65487, 65490, 65495, 65498, 65500,]; +const unicodeES5IdentifierPart = [170, 170, 181, 181, 186, 186, 192, 214, 216, 246, 248, 705, 710, 721, 736, 740, 748, 748, 750, 750, 768, 884, 886, 887, 890, 893, 902, 902, 904, 906, 908, 908, 910, 929, 931, 1013, 1015, 1153, 1155, 1159, 1162, 1319, 1329, 1366, 1369, 1369, 1377, 1415, 1425, 1469, 1471, 1471, 1473, 1474, 1476, 1477, 1479, 1479, 1488, 1514, 1520, 1522, 1552, 1562, 1568, 1641, 1646, 1747, 1749, 1756, 1759, 1768, 1770, 1788, 1791, 1791, 1808, 1866, 1869, 1969, 1984, 2037, 2042, 2042, 2048, 2093, 2112, 2139, 2208, 2208, 2210, 2220, 2276, 2302, 2304, 2403, 2406, 2415, 2417, 2423, 2425, 2431, 2433, 2435, 2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482, 2486, 2489, 2492, 2500, 2503, 2504, 2507, 2510, 2519, 2519, 2524, 2525, 2527, 2531, 2534, 2545, 2561, 2563, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611, 2613, 2614, 2616, 2617, 2620, 2620, 2622, 2626, 2631, 2632, 2635, 2637, 2641, 2641, 2649, 2652, 2654, 2654, 2662, 2677, 2689, 2691, 2693, 2701, 2703, 2705, 2707, 2728, 2730, 2736, 2738, 2739, 2741, 2745, 2748, 2757, 2759, 2761, 2763, 2765, 2768, 2768, 2784, 2787, 2790, 2799, 2817, 2819, 2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867, 2869, 2873, 2876, 2884, 2887, 2888, 2891, 2893, 2902, 2903, 2908, 2909, 2911, 2915, 2918, 2927, 2929, 2929, 2946, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001, 3006, 3010, 3014, 3016, 3018, 3021, 3024, 3024, 3031, 3031, 3046, 3055, 3073, 3075, 3077, 3084, 3086, 3088, 3090, 3112, 3114, 3123, 3125, 3129, 3133, 3140, 3142, 3144, 3146, 3149, 3157, 3158, 3160, 3161, 3168, 3171, 3174, 3183, 3202, 3203, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3260, 3268, 3270, 3272, 3274, 3277, 3285, 3286, 3294, 3294, 3296, 3299, 3302, 3311, 3313, 3314, 3330, 3331, 3333, 3340, 3342, 3344, 3346, 3386, 3389, 3396, 3398, 3400, 3402, 3406, 3415, 3415, 3424, 3427, 3430, 3439, 3450, 3455, 3458, 3459, 3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526, 3530, 3530, 3535, 3540, 3542, 3542, 3544, 3551, 3570, 3571, 3585, 3642, 3648, 3662, 3664, 3673, 3713, 3714, 3716, 3716, 3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743, 3745, 3747, 3749, 3749, 3751, 3751, 3754, 3755, 3757, 3769, 3771, 3773, 3776, 3780, 3782, 3782, 3784, 3789, 3792, 3801, 3804, 3807, 3840, 3840, 3864, 3865, 3872, 3881, 3893, 3893, 3895, 3895, 3897, 3897, 3902, 3911, 3913, 3948, 3953, 3972, 3974, 3991, 3993, 4028, 4038, 4038, 4096, 4169, 4176, 4253, 4256, 4293, 4295, 4295, 4301, 4301, 4304, 4346, 4348, 4680, 4682, 4685, 4688, 4694, 4696, 4696, 4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784, 4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880, 4882, 4885, 4888, 4954, 4957, 4959, 4992, 5007, 5024, 5108, 5121, 5740, 5743, 5759, 5761, 5786, 5792, 5866, 5870, 5872, 5888, 5900, 5902, 5908, 5920, 5940, 5952, 5971, 5984, 5996, 5998, 6000, 6002, 6003, 6016, 6099, 6103, 6103, 6108, 6109, 6112, 6121, 6155, 6157, 6160, 6169, 6176, 6263, 6272, 6314, 6320, 6389, 6400, 6428, 6432, 6443, 6448, 6459, 6470, 6509, 6512, 6516, 6528, 6571, 6576, 6601, 6608, 6617, 6656, 6683, 6688, 6750, 6752, 6780, 6783, 6793, 6800, 6809, 6823, 6823, 6912, 6987, 6992, 7001, 7019, 7027, 7040, 7155, 7168, 7223, 7232, 7241, 7245, 7293, 7376, 7378, 7380, 7414, 7424, 7654, 7676, 7957, 7960, 7965, 7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061, 8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140, 8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188, 8204, 8205, 8255, 8256, 8276, 8276, 8305, 8305, 8319, 8319, 8336, 8348, 8400, 8412, 8417, 8417, 8421, 8432, 8450, 8450, 8455, 8455, 8458, 8467, 8469, 8469, 8473, 8477, 8484, 8484, 8486, 8486, 8488, 8488, 8490, 8493, 8495, 8505, 8508, 8511, 8517, 8521, 8526, 8526, 8544, 8584, 11264, 11310, 11312, 11358, 11360, 11492, 11499, 11507, 11520, 11557, 11559, 11559, 11565, 11565, 11568, 11623, 11631, 11631, 11647, 11670, 11680, 11686, 11688, 11694, 11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726, 11728, 11734, 11736, 11742, 11744, 11775, 11823, 11823, 12293, 12295, 12321, 12335, 12337, 12341, 12344, 12348, 12353, 12438, 12441, 12442, 12445, 12447, 12449, 12538, 12540, 12543, 12549, 12589, 12593, 12686, 12704, 12730, 12784, 12799, 13312, 19893, 19968, 40908, 40960, 42124, 42192, 42237, 42240, 42508, 42512, 42539, 42560, 42607, 42612, 42621, 42623, 42647, 42655, 42737, 42775, 42783, 42786, 42888, 42891, 42894, 42896, 42899, 42912, 42922, 43000, 43047, 43072, 43123, 43136, 43204, 43216, 43225, 43232, 43255, 43259, 43259, 43264, 43309, 43312, 43347, 43360, 43388, 43392, 43456, 43471, 43481, 43520, 43574, 43584, 43597, 43600, 43609, 43616, 43638, 43642, 43643, 43648, 43714, 43739, 43741, 43744, 43759, 43762, 43766, 43777, 43782, 43785, 43790, 43793, 43798, 43808, 43814, 43816, 43822, 43968, 44010, 44012, 44013, 44016, 44025, 44032, 55203, 55216, 55238, 55243, 55291, 63744, 64109, 64112, 64217, 64256, 64262, 64275, 64279, 64285, 64296, 64298, 64310, 64312, 64316, 64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433, 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019, 65024, 65039, 65056, 65062, 65075, 65076, 65101, 65103, 65136, 65140, 65142, 65276, 65296, 65305, 65313, 65338, 65343, 65343, 65345, 65370, 65382, 65470, 65474, 65479, 65482, 65487, 65490, 65495, 65498, 65500,]; +/** + * Generated by scripts/regenerate-unicode-identifier-parts.js on node v12.4.0 with unicode 12.1 + * based on http://www.unicode.org/reports/tr31/ and https://www.ecma-international.org/ecma-262/6.0/#sec-names-and-keywords + * unicodeESNextIdentifierStart corresponds to the ID_Start and Other_ID_Start property, and + * unicodeESNextIdentifierPart corresponds to ID_Continue, Other_ID_Continue, plus ID_Start and Other_ID_Start + */ +const unicodeESNextIdentifierStart = [65, 90, 97, 122, 170, 170, 181, 181, 186, 186, 192, 214, 216, 246, 248, 705, 710, 721, 736, 740, 748, 748, 750, 750, 880, 884, 886, 887, 890, 893, 895, 895, 902, 902, 904, 906, 908, 908, 910, 929, 931, 1013, 1015, 1153, 1162, 1327, 1329, 1366, 1369, 1369, 1376, 1416, 1488, 1514, 1519, 1522, 1568, 1610, 1646, 1647, 1649, 1747, 1749, 1749, 1765, 1766, 1774, 1775, 1786, 1788, 1791, 1791, 1808, 1808, 1810, 1839, 1869, 1957, 1969, 1969, 1994, 2026, 2036, 2037, 2042, 2042, 2048, 2069, 2074, 2074, 2084, 2084, 2088, 2088, 2112, 2136, 2144, 2154, 2208, 2228, 2230, 2237, 2308, 2361, 2365, 2365, 2384, 2384, 2392, 2401, 2417, 2432, 2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482, 2486, 2489, 2493, 2493, 2510, 2510, 2524, 2525, 2527, 2529, 2544, 2545, 2556, 2556, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611, 2613, 2614, 2616, 2617, 2649, 2652, 2654, 2654, 2674, 2676, 2693, 2701, 2703, 2705, 2707, 2728, 2730, 2736, 2738, 2739, 2741, 2745, 2749, 2749, 2768, 2768, 2784, 2785, 2809, 2809, 2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867, 2869, 2873, 2877, 2877, 2908, 2909, 2911, 2913, 2929, 2929, 2947, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001, 3024, 3024, 3077, 3084, 3086, 3088, 3090, 3112, 3114, 3129, 3133, 3133, 3160, 3162, 3168, 3169, 3200, 3200, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3261, 3261, 3294, 3294, 3296, 3297, 3313, 3314, 3333, 3340, 3342, 3344, 3346, 3386, 3389, 3389, 3406, 3406, 3412, 3414, 3423, 3425, 3450, 3455, 3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526, 3585, 3632, 3634, 3635, 3648, 3654, 3713, 3714, 3716, 3716, 3718, 3722, 3724, 3747, 3749, 3749, 3751, 3760, 3762, 3763, 3773, 3773, 3776, 3780, 3782, 3782, 3804, 3807, 3840, 3840, 3904, 3911, 3913, 3948, 3976, 3980, 4096, 4138, 4159, 4159, 4176, 4181, 4186, 4189, 4193, 4193, 4197, 4198, 4206, 4208, 4213, 4225, 4238, 4238, 4256, 4293, 4295, 4295, 4301, 4301, 4304, 4346, 4348, 4680, 4682, 4685, 4688, 4694, 4696, 4696, 4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784, 4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880, 4882, 4885, 4888, 4954, 4992, 5007, 5024, 5109, 5112, 5117, 5121, 5740, 5743, 5759, 5761, 5786, 5792, 5866, 5870, 5880, 5888, 5900, 5902, 5905, 5920, 5937, 5952, 5969, 5984, 5996, 5998, 6000, 6016, 6067, 6103, 6103, 6108, 6108, 6176, 6264, 6272, 6312, 6314, 6314, 6320, 6389, 6400, 6430, 6480, 6509, 6512, 6516, 6528, 6571, 6576, 6601, 6656, 6678, 6688, 6740, 6823, 6823, 6917, 6963, 6981, 6987, 7043, 7072, 7086, 7087, 7098, 7141, 7168, 7203, 7245, 7247, 7258, 7293, 7296, 7304, 7312, 7354, 7357, 7359, 7401, 7404, 7406, 7411, 7413, 7414, 7418, 7418, 7424, 7615, 7680, 7957, 7960, 7965, 7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061, 8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140, 8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188, 8305, 8305, 8319, 8319, 8336, 8348, 8450, 8450, 8455, 8455, 8458, 8467, 8469, 8469, 8472, 8477, 8484, 8484, 8486, 8486, 8488, 8488, 8490, 8505, 8508, 8511, 8517, 8521, 8526, 8526, 8544, 8584, 11264, 11310, 11312, 11358, 11360, 11492, 11499, 11502, 11506, 11507, 11520, 11557, 11559, 11559, 11565, 11565, 11568, 11623, 11631, 11631, 11648, 11670, 11680, 11686, 11688, 11694, 11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726, 11728, 11734, 11736, 11742, 12293, 12295, 12321, 12329, 12337, 12341, 12344, 12348, 12353, 12438, 12443, 12447, 12449, 12538, 12540, 12543, 12549, 12591, 12593, 12686, 12704, 12730, 12784, 12799, 13312, 19893, 19968, 40943, 40960, 42124, 42192, 42237, 42240, 42508, 42512, 42527, 42538, 42539, 42560, 42606, 42623, 42653, 42656, 42735, 42775, 42783, 42786, 42888, 42891, 42943, 42946, 42950, 42999, 43009, 43011, 43013, 43015, 43018, 43020, 43042, 43072, 43123, 43138, 43187, 43250, 43255, 43259, 43259, 43261, 43262, 43274, 43301, 43312, 43334, 43360, 43388, 43396, 43442, 43471, 43471, 43488, 43492, 43494, 43503, 43514, 43518, 43520, 43560, 43584, 43586, 43588, 43595, 43616, 43638, 43642, 43642, 43646, 43695, 43697, 43697, 43701, 43702, 43705, 43709, 43712, 43712, 43714, 43714, 43739, 43741, 43744, 43754, 43762, 43764, 43777, 43782, 43785, 43790, 43793, 43798, 43808, 43814, 43816, 43822, 43824, 43866, 43868, 43879, 43888, 44002, 44032, 55203, 55216, 55238, 55243, 55291, 63744, 64109, 64112, 64217, 64256, 64262, 64275, 64279, 64285, 64285, 64287, 64296, 64298, 64310, 64312, 64316, 64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433, 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019, 65136, 65140, 65142, 65276, 65313, 65338, 65345, 65370, 65382, 65470, 65474, 65479, 65482, 65487, 65490, 65495, 65498, 65500, 65536, 65547, 65549, 65574, 65576, 65594, 65596, 65597, 65599, 65613, 65616, 65629, 65664, 65786, 65856, 65908, 66176, 66204, 66208, 66256, 66304, 66335, 66349, 66378, 66384, 66421, 66432, 66461, 66464, 66499, 66504, 66511, 66513, 66517, 66560, 66717, 66736, 66771, 66776, 66811, 66816, 66855, 66864, 66915, 67072, 67382, 67392, 67413, 67424, 67431, 67584, 67589, 67592, 67592, 67594, 67637, 67639, 67640, 67644, 67644, 67647, 67669, 67680, 67702, 67712, 67742, 67808, 67826, 67828, 67829, 67840, 67861, 67872, 67897, 67968, 68023, 68030, 68031, 68096, 68096, 68112, 68115, 68117, 68119, 68121, 68149, 68192, 68220, 68224, 68252, 68288, 68295, 68297, 68324, 68352, 68405, 68416, 68437, 68448, 68466, 68480, 68497, 68608, 68680, 68736, 68786, 68800, 68850, 68864, 68899, 69376, 69404, 69415, 69415, 69424, 69445, 69600, 69622, 69635, 69687, 69763, 69807, 69840, 69864, 69891, 69926, 69956, 69956, 69968, 70002, 70006, 70006, 70019, 70066, 70081, 70084, 70106, 70106, 70108, 70108, 70144, 70161, 70163, 70187, 70272, 70278, 70280, 70280, 70282, 70285, 70287, 70301, 70303, 70312, 70320, 70366, 70405, 70412, 70415, 70416, 70419, 70440, 70442, 70448, 70450, 70451, 70453, 70457, 70461, 70461, 70480, 70480, 70493, 70497, 70656, 70708, 70727, 70730, 70751, 70751, 70784, 70831, 70852, 70853, 70855, 70855, 71040, 71086, 71128, 71131, 71168, 71215, 71236, 71236, 71296, 71338, 71352, 71352, 71424, 71450, 71680, 71723, 71840, 71903, 71935, 71935, 72096, 72103, 72106, 72144, 72161, 72161, 72163, 72163, 72192, 72192, 72203, 72242, 72250, 72250, 72272, 72272, 72284, 72329, 72349, 72349, 72384, 72440, 72704, 72712, 72714, 72750, 72768, 72768, 72818, 72847, 72960, 72966, 72968, 72969, 72971, 73008, 73030, 73030, 73056, 73061, 73063, 73064, 73066, 73097, 73112, 73112, 73440, 73458, 73728, 74649, 74752, 74862, 74880, 75075, 77824, 78894, 82944, 83526, 92160, 92728, 92736, 92766, 92880, 92909, 92928, 92975, 92992, 92995, 93027, 93047, 93053, 93071, 93760, 93823, 93952, 94026, 94032, 94032, 94099, 94111, 94176, 94177, 94179, 94179, 94208, 100343, 100352, 101106, 110592, 110878, 110928, 110930, 110948, 110951, 110960, 111355, 113664, 113770, 113776, 113788, 113792, 113800, 113808, 113817, 119808, 119892, 119894, 119964, 119966, 119967, 119970, 119970, 119973, 119974, 119977, 119980, 119982, 119993, 119995, 119995, 119997, 120003, 120005, 120069, 120071, 120074, 120077, 120084, 120086, 120092, 120094, 120121, 120123, 120126, 120128, 120132, 120134, 120134, 120138, 120144, 120146, 120485, 120488, 120512, 120514, 120538, 120540, 120570, 120572, 120596, 120598, 120628, 120630, 120654, 120656, 120686, 120688, 120712, 120714, 120744, 120746, 120770, 120772, 120779, 123136, 123180, 123191, 123197, 123214, 123214, 123584, 123627, 124928, 125124, 125184, 125251, 125259, 125259, 126464, 126467, 126469, 126495, 126497, 126498, 126500, 126500, 126503, 126503, 126505, 126514, 126516, 126519, 126521, 126521, 126523, 126523, 126530, 126530, 126535, 126535, 126537, 126537, 126539, 126539, 126541, 126543, 126545, 126546, 126548, 126548, 126551, 126551, 126553, 126553, 126555, 126555, 126557, 126557, 126559, 126559, 126561, 126562, 126564, 126564, 126567, 126570, 126572, 126578, 126580, 126583, 126585, 126588, 126590, 126590, 126592, 126601, 126603, 126619, 126625, 126627, 126629, 126633, 126635, 126651, 131072, 173782, 173824, 177972, 177984, 178205, 178208, 183969, 183984, 191456, 194560, 195101]; +const unicodeESNextIdentifierPart = [48, 57, 65, 90, 95, 95, 97, 122, 170, 170, 181, 181, 183, 183, 186, 186, 192, 214, 216, 246, 248, 705, 710, 721, 736, 740, 748, 748, 750, 750, 768, 884, 886, 887, 890, 893, 895, 895, 902, 906, 908, 908, 910, 929, 931, 1013, 1015, 1153, 1155, 1159, 1162, 1327, 1329, 1366, 1369, 1369, 1376, 1416, 1425, 1469, 1471, 1471, 1473, 1474, 1476, 1477, 1479, 1479, 1488, 1514, 1519, 1522, 1552, 1562, 1568, 1641, 1646, 1747, 1749, 1756, 1759, 1768, 1770, 1788, 1791, 1791, 1808, 1866, 1869, 1969, 1984, 2037, 2042, 2042, 2045, 2045, 2048, 2093, 2112, 2139, 2144, 2154, 2208, 2228, 2230, 2237, 2259, 2273, 2275, 2403, 2406, 2415, 2417, 2435, 2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482, 2486, 2489, 2492, 2500, 2503, 2504, 2507, 2510, 2519, 2519, 2524, 2525, 2527, 2531, 2534, 2545, 2556, 2556, 2558, 2558, 2561, 2563, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611, 2613, 2614, 2616, 2617, 2620, 2620, 2622, 2626, 2631, 2632, 2635, 2637, 2641, 2641, 2649, 2652, 2654, 2654, 2662, 2677, 2689, 2691, 2693, 2701, 2703, 2705, 2707, 2728, 2730, 2736, 2738, 2739, 2741, 2745, 2748, 2757, 2759, 2761, 2763, 2765, 2768, 2768, 2784, 2787, 2790, 2799, 2809, 2815, 2817, 2819, 2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867, 2869, 2873, 2876, 2884, 2887, 2888, 2891, 2893, 2902, 2903, 2908, 2909, 2911, 2915, 2918, 2927, 2929, 2929, 2946, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001, 3006, 3010, 3014, 3016, 3018, 3021, 3024, 3024, 3031, 3031, 3046, 3055, 3072, 3084, 3086, 3088, 3090, 3112, 3114, 3129, 3133, 3140, 3142, 3144, 3146, 3149, 3157, 3158, 3160, 3162, 3168, 3171, 3174, 3183, 3200, 3203, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3260, 3268, 3270, 3272, 3274, 3277, 3285, 3286, 3294, 3294, 3296, 3299, 3302, 3311, 3313, 3314, 3328, 3331, 3333, 3340, 3342, 3344, 3346, 3396, 3398, 3400, 3402, 3406, 3412, 3415, 3423, 3427, 3430, 3439, 3450, 3455, 3458, 3459, 3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526, 3530, 3530, 3535, 3540, 3542, 3542, 3544, 3551, 3558, 3567, 3570, 3571, 3585, 3642, 3648, 3662, 3664, 3673, 3713, 3714, 3716, 3716, 3718, 3722, 3724, 3747, 3749, 3749, 3751, 3773, 3776, 3780, 3782, 3782, 3784, 3789, 3792, 3801, 3804, 3807, 3840, 3840, 3864, 3865, 3872, 3881, 3893, 3893, 3895, 3895, 3897, 3897, 3902, 3911, 3913, 3948, 3953, 3972, 3974, 3991, 3993, 4028, 4038, 4038, 4096, 4169, 4176, 4253, 4256, 4293, 4295, 4295, 4301, 4301, 4304, 4346, 4348, 4680, 4682, 4685, 4688, 4694, 4696, 4696, 4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784, 4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880, 4882, 4885, 4888, 4954, 4957, 4959, 4969, 4977, 4992, 5007, 5024, 5109, 5112, 5117, 5121, 5740, 5743, 5759, 5761, 5786, 5792, 5866, 5870, 5880, 5888, 5900, 5902, 5908, 5920, 5940, 5952, 5971, 5984, 5996, 5998, 6000, 6002, 6003, 6016, 6099, 6103, 6103, 6108, 6109, 6112, 6121, 6155, 6157, 6160, 6169, 6176, 6264, 6272, 6314, 6320, 6389, 6400, 6430, 6432, 6443, 6448, 6459, 6470, 6509, 6512, 6516, 6528, 6571, 6576, 6601, 6608, 6618, 6656, 6683, 6688, 6750, 6752, 6780, 6783, 6793, 6800, 6809, 6823, 6823, 6832, 6845, 6912, 6987, 6992, 7001, 7019, 7027, 7040, 7155, 7168, 7223, 7232, 7241, 7245, 7293, 7296, 7304, 7312, 7354, 7357, 7359, 7376, 7378, 7380, 7418, 7424, 7673, 7675, 7957, 7960, 7965, 7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061, 8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140, 8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188, 8255, 8256, 8276, 8276, 8305, 8305, 8319, 8319, 8336, 8348, 8400, 8412, 8417, 8417, 8421, 8432, 8450, 8450, 8455, 8455, 8458, 8467, 8469, 8469, 8472, 8477, 8484, 8484, 8486, 8486, 8488, 8488, 8490, 8505, 8508, 8511, 8517, 8521, 8526, 8526, 8544, 8584, 11264, 11310, 11312, 11358, 11360, 11492, 11499, 11507, 11520, 11557, 11559, 11559, 11565, 11565, 11568, 11623, 11631, 11631, 11647, 11670, 11680, 11686, 11688, 11694, 11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726, 11728, 11734, 11736, 11742, 11744, 11775, 12293, 12295, 12321, 12335, 12337, 12341, 12344, 12348, 12353, 12438, 12441, 12447, 12449, 12538, 12540, 12543, 12549, 12591, 12593, 12686, 12704, 12730, 12784, 12799, 13312, 19893, 19968, 40943, 40960, 42124, 42192, 42237, 42240, 42508, 42512, 42539, 42560, 42607, 42612, 42621, 42623, 42737, 42775, 42783, 42786, 42888, 42891, 42943, 42946, 42950, 42999, 43047, 43072, 43123, 43136, 43205, 43216, 43225, 43232, 43255, 43259, 43259, 43261, 43309, 43312, 43347, 43360, 43388, 43392, 43456, 43471, 43481, 43488, 43518, 43520, 43574, 43584, 43597, 43600, 43609, 43616, 43638, 43642, 43714, 43739, 43741, 43744, 43759, 43762, 43766, 43777, 43782, 43785, 43790, 43793, 43798, 43808, 43814, 43816, 43822, 43824, 43866, 43868, 43879, 43888, 44010, 44012, 44013, 44016, 44025, 44032, 55203, 55216, 55238, 55243, 55291, 63744, 64109, 64112, 64217, 64256, 64262, 64275, 64279, 64285, 64296, 64298, 64310, 64312, 64316, 64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433, 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019, 65024, 65039, 65056, 65071, 65075, 65076, 65101, 65103, 65136, 65140, 65142, 65276, 65296, 65305, 65313, 65338, 65343, 65343, 65345, 65370, 65382, 65470, 65474, 65479, 65482, 65487, 65490, 65495, 65498, 65500, 65536, 65547, 65549, 65574, 65576, 65594, 65596, 65597, 65599, 65613, 65616, 65629, 65664, 65786, 65856, 65908, 66045, 66045, 66176, 66204, 66208, 66256, 66272, 66272, 66304, 66335, 66349, 66378, 66384, 66426, 66432, 66461, 66464, 66499, 66504, 66511, 66513, 66517, 66560, 66717, 66720, 66729, 66736, 66771, 66776, 66811, 66816, 66855, 66864, 66915, 67072, 67382, 67392, 67413, 67424, 67431, 67584, 67589, 67592, 67592, 67594, 67637, 67639, 67640, 67644, 67644, 67647, 67669, 67680, 67702, 67712, 67742, 67808, 67826, 67828, 67829, 67840, 67861, 67872, 67897, 67968, 68023, 68030, 68031, 68096, 68099, 68101, 68102, 68108, 68115, 68117, 68119, 68121, 68149, 68152, 68154, 68159, 68159, 68192, 68220, 68224, 68252, 68288, 68295, 68297, 68326, 68352, 68405, 68416, 68437, 68448, 68466, 68480, 68497, 68608, 68680, 68736, 68786, 68800, 68850, 68864, 68903, 68912, 68921, 69376, 69404, 69415, 69415, 69424, 69456, 69600, 69622, 69632, 69702, 69734, 69743, 69759, 69818, 69840, 69864, 69872, 69881, 69888, 69940, 69942, 69951, 69956, 69958, 69968, 70003, 70006, 70006, 70016, 70084, 70089, 70092, 70096, 70106, 70108, 70108, 70144, 70161, 70163, 70199, 70206, 70206, 70272, 70278, 70280, 70280, 70282, 70285, 70287, 70301, 70303, 70312, 70320, 70378, 70384, 70393, 70400, 70403, 70405, 70412, 70415, 70416, 70419, 70440, 70442, 70448, 70450, 70451, 70453, 70457, 70459, 70468, 70471, 70472, 70475, 70477, 70480, 70480, 70487, 70487, 70493, 70499, 70502, 70508, 70512, 70516, 70656, 70730, 70736, 70745, 70750, 70751, 70784, 70853, 70855, 70855, 70864, 70873, 71040, 71093, 71096, 71104, 71128, 71133, 71168, 71232, 71236, 71236, 71248, 71257, 71296, 71352, 71360, 71369, 71424, 71450, 71453, 71467, 71472, 71481, 71680, 71738, 71840, 71913, 71935, 71935, 72096, 72103, 72106, 72151, 72154, 72161, 72163, 72164, 72192, 72254, 72263, 72263, 72272, 72345, 72349, 72349, 72384, 72440, 72704, 72712, 72714, 72758, 72760, 72768, 72784, 72793, 72818, 72847, 72850, 72871, 72873, 72886, 72960, 72966, 72968, 72969, 72971, 73014, 73018, 73018, 73020, 73021, 73023, 73031, 73040, 73049, 73056, 73061, 73063, 73064, 73066, 73102, 73104, 73105, 73107, 73112, 73120, 73129, 73440, 73462, 73728, 74649, 74752, 74862, 74880, 75075, 77824, 78894, 82944, 83526, 92160, 92728, 92736, 92766, 92768, 92777, 92880, 92909, 92912, 92916, 92928, 92982, 92992, 92995, 93008, 93017, 93027, 93047, 93053, 93071, 93760, 93823, 93952, 94026, 94031, 94087, 94095, 94111, 94176, 94177, 94179, 94179, 94208, 100343, 100352, 101106, 110592, 110878, 110928, 110930, 110948, 110951, 110960, 111355, 113664, 113770, 113776, 113788, 113792, 113800, 113808, 113817, 113821, 113822, 119141, 119145, 119149, 119154, 119163, 119170, 119173, 119179, 119210, 119213, 119362, 119364, 119808, 119892, 119894, 119964, 119966, 119967, 119970, 119970, 119973, 119974, 119977, 119980, 119982, 119993, 119995, 119995, 119997, 120003, 120005, 120069, 120071, 120074, 120077, 120084, 120086, 120092, 120094, 120121, 120123, 120126, 120128, 120132, 120134, 120134, 120138, 120144, 120146, 120485, 120488, 120512, 120514, 120538, 120540, 120570, 120572, 120596, 120598, 120628, 120630, 120654, 120656, 120686, 120688, 120712, 120714, 120744, 120746, 120770, 120772, 120779, 120782, 120831, 121344, 121398, 121403, 121452, 121461, 121461, 121476, 121476, 121499, 121503, 121505, 121519, 122880, 122886, 122888, 122904, 122907, 122913, 122915, 122916, 122918, 122922, 123136, 123180, 123184, 123197, 123200, 123209, 123214, 123214, 123584, 123641, 124928, 125124, 125136, 125142, 125184, 125259, 125264, 125273, 126464, 126467, 126469, 126495, 126497, 126498, 126500, 126500, 126503, 126503, 126505, 126514, 126516, 126519, 126521, 126521, 126523, 126523, 126530, 126530, 126535, 126535, 126537, 126537, 126539, 126539, 126541, 126543, 126545, 126546, 126548, 126548, 126551, 126551, 126553, 126553, 126555, 126555, 126557, 126557, 126559, 126559, 126561, 126562, 126564, 126564, 126567, 126570, 126572, 126578, 126580, 126583, 126585, 126588, 126590, 126590, 126592, 126601, 126603, 126619, 126625, 126627, 126629, 126633, 126635, 126651, 131072, 173782, 173824, 177972, 177984, 178205, 178208, 183969, 183984, 191456, 194560, 195101, 917760, 917999]; +function lookupInUnicodeMap(code: number, map: readonly number[]): boolean { + // Bail out quickly if it couldn't possibly be in the map. + if (code < map[0]) { + return false; } - - const textToKeywordObj: MapLike = { - abstract: SyntaxKind.AbstractKeyword, - any: SyntaxKind.AnyKeyword, - as: SyntaxKind.AsKeyword, - asserts: SyntaxKind.AssertsKeyword, - bigint: SyntaxKind.BigIntKeyword, - boolean: SyntaxKind.BooleanKeyword, - break: SyntaxKind.BreakKeyword, - case: SyntaxKind.CaseKeyword, - catch: SyntaxKind.CatchKeyword, - class: SyntaxKind.ClassKeyword, - continue: SyntaxKind.ContinueKeyword, - const: SyntaxKind.ConstKeyword, - ["" + "constructor"]: SyntaxKind.ConstructorKeyword, - debugger: SyntaxKind.DebuggerKeyword, - declare: SyntaxKind.DeclareKeyword, - default: SyntaxKind.DefaultKeyword, - delete: SyntaxKind.DeleteKeyword, - do: SyntaxKind.DoKeyword, - else: SyntaxKind.ElseKeyword, - enum: SyntaxKind.EnumKeyword, - export: SyntaxKind.ExportKeyword, - extends: SyntaxKind.ExtendsKeyword, - false: SyntaxKind.FalseKeyword, - finally: SyntaxKind.FinallyKeyword, - for: SyntaxKind.ForKeyword, - from: SyntaxKind.FromKeyword, - function: SyntaxKind.FunctionKeyword, - get: SyntaxKind.GetKeyword, - if: SyntaxKind.IfKeyword, - implements: SyntaxKind.ImplementsKeyword, - import: SyntaxKind.ImportKeyword, - in: SyntaxKind.InKeyword, - infer: SyntaxKind.InferKeyword, - instanceof: SyntaxKind.InstanceOfKeyword, - interface: SyntaxKind.InterfaceKeyword, - is: SyntaxKind.IsKeyword, - keyof: SyntaxKind.KeyOfKeyword, - let: SyntaxKind.LetKeyword, - module: SyntaxKind.ModuleKeyword, - namespace: SyntaxKind.NamespaceKeyword, - never: SyntaxKind.NeverKeyword, - new: SyntaxKind.NewKeyword, - null: SyntaxKind.NullKeyword, - number: SyntaxKind.NumberKeyword, - object: SyntaxKind.ObjectKeyword, - package: SyntaxKind.PackageKeyword, - private: SyntaxKind.PrivateKeyword, - protected: SyntaxKind.ProtectedKeyword, - public: SyntaxKind.PublicKeyword, - readonly: SyntaxKind.ReadonlyKeyword, - require: SyntaxKind.RequireKeyword, - global: SyntaxKind.GlobalKeyword, - return: SyntaxKind.ReturnKeyword, - set: SyntaxKind.SetKeyword, - static: SyntaxKind.StaticKeyword, - string: SyntaxKind.StringKeyword, - super: SyntaxKind.SuperKeyword, - switch: SyntaxKind.SwitchKeyword, - symbol: SyntaxKind.SymbolKeyword, - this: SyntaxKind.ThisKeyword, - throw: SyntaxKind.ThrowKeyword, - true: SyntaxKind.TrueKeyword, - try: SyntaxKind.TryKeyword, - type: SyntaxKind.TypeKeyword, - typeof: SyntaxKind.TypeOfKeyword, - undefined: SyntaxKind.UndefinedKeyword, - unique: SyntaxKind.UniqueKeyword, - unknown: SyntaxKind.UnknownKeyword, - var: SyntaxKind.VarKeyword, - void: SyntaxKind.VoidKeyword, - while: SyntaxKind.WhileKeyword, - with: SyntaxKind.WithKeyword, - yield: SyntaxKind.YieldKeyword, - async: SyntaxKind.AsyncKeyword, - await: SyntaxKind.AwaitKeyword, - of: SyntaxKind.OfKeyword, - }; - - const textToKeyword = createMapFromTemplate(textToKeywordObj); - - const textToToken = createMapFromTemplate({ - ...textToKeywordObj, - "{": SyntaxKind.OpenBraceToken, - "}": SyntaxKind.CloseBraceToken, - "(": SyntaxKind.OpenParenToken, - ")": SyntaxKind.CloseParenToken, - "[": SyntaxKind.OpenBracketToken, - "]": SyntaxKind.CloseBracketToken, - ".": SyntaxKind.DotToken, - "...": SyntaxKind.DotDotDotToken, - ";": SyntaxKind.SemicolonToken, - ",": SyntaxKind.CommaToken, - "<": SyntaxKind.LessThanToken, - ">": SyntaxKind.GreaterThanToken, - "<=": SyntaxKind.LessThanEqualsToken, - ">=": SyntaxKind.GreaterThanEqualsToken, - "==": SyntaxKind.EqualsEqualsToken, - "!=": SyntaxKind.ExclamationEqualsToken, - "===": SyntaxKind.EqualsEqualsEqualsToken, - "!==": SyntaxKind.ExclamationEqualsEqualsToken, - "=>": SyntaxKind.EqualsGreaterThanToken, - "+": SyntaxKind.PlusToken, - "-": SyntaxKind.MinusToken, - "**": SyntaxKind.AsteriskAsteriskToken, - "*": SyntaxKind.AsteriskToken, - "/": SyntaxKind.SlashToken, - "%": SyntaxKind.PercentToken, - "++": SyntaxKind.PlusPlusToken, - "--": SyntaxKind.MinusMinusToken, - "<<": SyntaxKind.LessThanLessThanToken, - ">": SyntaxKind.GreaterThanGreaterThanToken, - ">>>": SyntaxKind.GreaterThanGreaterThanGreaterThanToken, - "&": SyntaxKind.AmpersandToken, - "|": SyntaxKind.BarToken, - "^": SyntaxKind.CaretToken, - "!": SyntaxKind.ExclamationToken, - "~": SyntaxKind.TildeToken, - "&&": SyntaxKind.AmpersandAmpersandToken, - "||": SyntaxKind.BarBarToken, - "?": SyntaxKind.QuestionToken, - "??": SyntaxKind.QuestionQuestionToken, - "?.": SyntaxKind.QuestionDotToken, - ":": SyntaxKind.ColonToken, - "=": SyntaxKind.EqualsToken, - "+=": SyntaxKind.PlusEqualsToken, - "-=": SyntaxKind.MinusEqualsToken, - "*=": SyntaxKind.AsteriskEqualsToken, - "**=": SyntaxKind.AsteriskAsteriskEqualsToken, - "/=": SyntaxKind.SlashEqualsToken, - "%=": SyntaxKind.PercentEqualsToken, - "<<=": SyntaxKind.LessThanLessThanEqualsToken, - ">>=": SyntaxKind.GreaterThanGreaterThanEqualsToken, - ">>>=": SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken, - "&=": SyntaxKind.AmpersandEqualsToken, - "|=": SyntaxKind.BarEqualsToken, - "^=": SyntaxKind.CaretEqualsToken, - "@": SyntaxKind.AtToken, - "`": SyntaxKind.BacktickToken - }); - - /* - As per ECMAScript Language Specification 3th Edition, Section 7.6: Identifiers - IdentifierStart :: - Can contain Unicode 3.0.0 categories: - Uppercase letter (Lu), - Lowercase letter (Ll), - Titlecase letter (Lt), - Modifier letter (Lm), - Other letter (Lo), or - Letter number (Nl). - IdentifierPart :: = - Can contain IdentifierStart + Unicode 3.0.0 categories: - Non-spacing mark (Mn), - Combining spacing mark (Mc), - Decimal number (Nd), or - Connector punctuation (Pc). - - Codepoint ranges for ES3 Identifiers are extracted from the Unicode 3.0.0 specification at: - http://www.unicode.org/Public/3.0-Update/UnicodeData-3.0.0.txt - */ - const unicodeES3IdentifierStart = [170, 170, 181, 181, 186, 186, 192, 214, 216, 246, 248, 543, 546, 563, 592, 685, 688, 696, 699, 705, 720, 721, 736, 740, 750, 750, 890, 890, 902, 902, 904, 906, 908, 908, 910, 929, 931, 974, 976, 983, 986, 1011, 1024, 1153, 1164, 1220, 1223, 1224, 1227, 1228, 1232, 1269, 1272, 1273, 1329, 1366, 1369, 1369, 1377, 1415, 1488, 1514, 1520, 1522, 1569, 1594, 1600, 1610, 1649, 1747, 1749, 1749, 1765, 1766, 1786, 1788, 1808, 1808, 1810, 1836, 1920, 1957, 2309, 2361, 2365, 2365, 2384, 2384, 2392, 2401, 2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482, 2486, 2489, 2524, 2525, 2527, 2529, 2544, 2545, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611, 2613, 2614, 2616, 2617, 2649, 2652, 2654, 2654, 2674, 2676, 2693, 2699, 2701, 2701, 2703, 2705, 2707, 2728, 2730, 2736, 2738, 2739, 2741, 2745, 2749, 2749, 2768, 2768, 2784, 2784, 2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867, 2870, 2873, 2877, 2877, 2908, 2909, 2911, 2913, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 2997, 2999, 3001, 3077, 3084, 3086, 3088, 3090, 3112, 3114, 3123, 3125, 3129, 3168, 3169, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3294, 3294, 3296, 3297, 3333, 3340, 3342, 3344, 3346, 3368, 3370, 3385, 3424, 3425, 3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526, 3585, 3632, 3634, 3635, 3648, 3654, 3713, 3714, 3716, 3716, 3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743, 3745, 3747, 3749, 3749, 3751, 3751, 3754, 3755, 3757, 3760, 3762, 3763, 3773, 3773, 3776, 3780, 3782, 3782, 3804, 3805, 3840, 3840, 3904, 3911, 3913, 3946, 3976, 3979, 4096, 4129, 4131, 4135, 4137, 4138, 4176, 4181, 4256, 4293, 4304, 4342, 4352, 4441, 4447, 4514, 4520, 4601, 4608, 4614, 4616, 4678, 4680, 4680, 4682, 4685, 4688, 4694, 4696, 4696, 4698, 4701, 4704, 4742, 4744, 4744, 4746, 4749, 4752, 4782, 4784, 4784, 4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4814, 4816, 4822, 4824, 4846, 4848, 4878, 4880, 4880, 4882, 4885, 4888, 4894, 4896, 4934, 4936, 4954, 5024, 5108, 5121, 5740, 5743, 5750, 5761, 5786, 5792, 5866, 6016, 6067, 6176, 6263, 6272, 6312, 7680, 7835, 7840, 7929, 7936, 7957, 7960, 7965, 7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061, 8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140, 8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188, 8319, 8319, 8450, 8450, 8455, 8455, 8458, 8467, 8469, 8469, 8473, 8477, 8484, 8484, 8486, 8486, 8488, 8488, 8490, 8493, 8495, 8497, 8499, 8505, 8544, 8579, 12293, 12295, 12321, 12329, 12337, 12341, 12344, 12346, 12353, 12436, 12445, 12446, 12449, 12538, 12540, 12542, 12549, 12588, 12593, 12686, 12704, 12727, 13312, 19893, 19968, 40869, 40960, 42124, 44032, 55203, 63744, 64045, 64256, 64262, 64275, 64279, 64285, 64285, 64287, 64296, 64298, 64310, 64312, 64316, 64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433, 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019, 65136, 65138, 65140, 65140, 65142, 65276, 65313, 65338, 65345, 65370, 65382, 65470, 65474, 65479, 65482, 65487, 65490, 65495, 65498, 65500, ]; - const unicodeES3IdentifierPart = [170, 170, 181, 181, 186, 186, 192, 214, 216, 246, 248, 543, 546, 563, 592, 685, 688, 696, 699, 705, 720, 721, 736, 740, 750, 750, 768, 846, 864, 866, 890, 890, 902, 902, 904, 906, 908, 908, 910, 929, 931, 974, 976, 983, 986, 1011, 1024, 1153, 1155, 1158, 1164, 1220, 1223, 1224, 1227, 1228, 1232, 1269, 1272, 1273, 1329, 1366, 1369, 1369, 1377, 1415, 1425, 1441, 1443, 1465, 1467, 1469, 1471, 1471, 1473, 1474, 1476, 1476, 1488, 1514, 1520, 1522, 1569, 1594, 1600, 1621, 1632, 1641, 1648, 1747, 1749, 1756, 1759, 1768, 1770, 1773, 1776, 1788, 1808, 1836, 1840, 1866, 1920, 1968, 2305, 2307, 2309, 2361, 2364, 2381, 2384, 2388, 2392, 2403, 2406, 2415, 2433, 2435, 2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482, 2486, 2489, 2492, 2492, 2494, 2500, 2503, 2504, 2507, 2509, 2519, 2519, 2524, 2525, 2527, 2531, 2534, 2545, 2562, 2562, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611, 2613, 2614, 2616, 2617, 2620, 2620, 2622, 2626, 2631, 2632, 2635, 2637, 2649, 2652, 2654, 2654, 2662, 2676, 2689, 2691, 2693, 2699, 2701, 2701, 2703, 2705, 2707, 2728, 2730, 2736, 2738, 2739, 2741, 2745, 2748, 2757, 2759, 2761, 2763, 2765, 2768, 2768, 2784, 2784, 2790, 2799, 2817, 2819, 2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867, 2870, 2873, 2876, 2883, 2887, 2888, 2891, 2893, 2902, 2903, 2908, 2909, 2911, 2913, 2918, 2927, 2946, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 2997, 2999, 3001, 3006, 3010, 3014, 3016, 3018, 3021, 3031, 3031, 3047, 3055, 3073, 3075, 3077, 3084, 3086, 3088, 3090, 3112, 3114, 3123, 3125, 3129, 3134, 3140, 3142, 3144, 3146, 3149, 3157, 3158, 3168, 3169, 3174, 3183, 3202, 3203, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3262, 3268, 3270, 3272, 3274, 3277, 3285, 3286, 3294, 3294, 3296, 3297, 3302, 3311, 3330, 3331, 3333, 3340, 3342, 3344, 3346, 3368, 3370, 3385, 3390, 3395, 3398, 3400, 3402, 3405, 3415, 3415, 3424, 3425, 3430, 3439, 3458, 3459, 3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526, 3530, 3530, 3535, 3540, 3542, 3542, 3544, 3551, 3570, 3571, 3585, 3642, 3648, 3662, 3664, 3673, 3713, 3714, 3716, 3716, 3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743, 3745, 3747, 3749, 3749, 3751, 3751, 3754, 3755, 3757, 3769, 3771, 3773, 3776, 3780, 3782, 3782, 3784, 3789, 3792, 3801, 3804, 3805, 3840, 3840, 3864, 3865, 3872, 3881, 3893, 3893, 3895, 3895, 3897, 3897, 3902, 3911, 3913, 3946, 3953, 3972, 3974, 3979, 3984, 3991, 3993, 4028, 4038, 4038, 4096, 4129, 4131, 4135, 4137, 4138, 4140, 4146, 4150, 4153, 4160, 4169, 4176, 4185, 4256, 4293, 4304, 4342, 4352, 4441, 4447, 4514, 4520, 4601, 4608, 4614, 4616, 4678, 4680, 4680, 4682, 4685, 4688, 4694, 4696, 4696, 4698, 4701, 4704, 4742, 4744, 4744, 4746, 4749, 4752, 4782, 4784, 4784, 4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4814, 4816, 4822, 4824, 4846, 4848, 4878, 4880, 4880, 4882, 4885, 4888, 4894, 4896, 4934, 4936, 4954, 4969, 4977, 5024, 5108, 5121, 5740, 5743, 5750, 5761, 5786, 5792, 5866, 6016, 6099, 6112, 6121, 6160, 6169, 6176, 6263, 6272, 6313, 7680, 7835, 7840, 7929, 7936, 7957, 7960, 7965, 7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061, 8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140, 8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188, 8255, 8256, 8319, 8319, 8400, 8412, 8417, 8417, 8450, 8450, 8455, 8455, 8458, 8467, 8469, 8469, 8473, 8477, 8484, 8484, 8486, 8486, 8488, 8488, 8490, 8493, 8495, 8497, 8499, 8505, 8544, 8579, 12293, 12295, 12321, 12335, 12337, 12341, 12344, 12346, 12353, 12436, 12441, 12442, 12445, 12446, 12449, 12542, 12549, 12588, 12593, 12686, 12704, 12727, 13312, 19893, 19968, 40869, 40960, 42124, 44032, 55203, 63744, 64045, 64256, 64262, 64275, 64279, 64285, 64296, 64298, 64310, 64312, 64316, 64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433, 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019, 65056, 65059, 65075, 65076, 65101, 65103, 65136, 65138, 65140, 65140, 65142, 65276, 65296, 65305, 65313, 65338, 65343, 65343, 65345, 65370, 65381, 65470, 65474, 65479, 65482, 65487, 65490, 65495, 65498, 65500, ]; - - /* - As per ECMAScript Language Specification 5th Edition, Section 7.6: ISyntaxToken Names and Identifiers - IdentifierStart :: - Can contain Unicode 6.2 categories: - Uppercase letter (Lu), - Lowercase letter (Ll), - Titlecase letter (Lt), - Modifier letter (Lm), - Other letter (Lo), or - Letter number (Nl). - IdentifierPart :: - Can contain IdentifierStart + Unicode 6.2 categories: - Non-spacing mark (Mn), - Combining spacing mark (Mc), - Decimal number (Nd), - Connector punctuation (Pc), - , or - . - - Codepoint ranges for ES5 Identifiers are extracted from the Unicode 6.2 specification at: - http://www.unicode.org/Public/6.2.0/ucd/UnicodeData.txt - */ - const unicodeES5IdentifierStart = [170, 170, 181, 181, 186, 186, 192, 214, 216, 246, 248, 705, 710, 721, 736, 740, 748, 748, 750, 750, 880, 884, 886, 887, 890, 893, 902, 902, 904, 906, 908, 908, 910, 929, 931, 1013, 1015, 1153, 1162, 1319, 1329, 1366, 1369, 1369, 1377, 1415, 1488, 1514, 1520, 1522, 1568, 1610, 1646, 1647, 1649, 1747, 1749, 1749, 1765, 1766, 1774, 1775, 1786, 1788, 1791, 1791, 1808, 1808, 1810, 1839, 1869, 1957, 1969, 1969, 1994, 2026, 2036, 2037, 2042, 2042, 2048, 2069, 2074, 2074, 2084, 2084, 2088, 2088, 2112, 2136, 2208, 2208, 2210, 2220, 2308, 2361, 2365, 2365, 2384, 2384, 2392, 2401, 2417, 2423, 2425, 2431, 2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482, 2486, 2489, 2493, 2493, 2510, 2510, 2524, 2525, 2527, 2529, 2544, 2545, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611, 2613, 2614, 2616, 2617, 2649, 2652, 2654, 2654, 2674, 2676, 2693, 2701, 2703, 2705, 2707, 2728, 2730, 2736, 2738, 2739, 2741, 2745, 2749, 2749, 2768, 2768, 2784, 2785, 2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867, 2869, 2873, 2877, 2877, 2908, 2909, 2911, 2913, 2929, 2929, 2947, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001, 3024, 3024, 3077, 3084, 3086, 3088, 3090, 3112, 3114, 3123, 3125, 3129, 3133, 3133, 3160, 3161, 3168, 3169, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3261, 3261, 3294, 3294, 3296, 3297, 3313, 3314, 3333, 3340, 3342, 3344, 3346, 3386, 3389, 3389, 3406, 3406, 3424, 3425, 3450, 3455, 3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526, 3585, 3632, 3634, 3635, 3648, 3654, 3713, 3714, 3716, 3716, 3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743, 3745, 3747, 3749, 3749, 3751, 3751, 3754, 3755, 3757, 3760, 3762, 3763, 3773, 3773, 3776, 3780, 3782, 3782, 3804, 3807, 3840, 3840, 3904, 3911, 3913, 3948, 3976, 3980, 4096, 4138, 4159, 4159, 4176, 4181, 4186, 4189, 4193, 4193, 4197, 4198, 4206, 4208, 4213, 4225, 4238, 4238, 4256, 4293, 4295, 4295, 4301, 4301, 4304, 4346, 4348, 4680, 4682, 4685, 4688, 4694, 4696, 4696, 4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784, 4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880, 4882, 4885, 4888, 4954, 4992, 5007, 5024, 5108, 5121, 5740, 5743, 5759, 5761, 5786, 5792, 5866, 5870, 5872, 5888, 5900, 5902, 5905, 5920, 5937, 5952, 5969, 5984, 5996, 5998, 6000, 6016, 6067, 6103, 6103, 6108, 6108, 6176, 6263, 6272, 6312, 6314, 6314, 6320, 6389, 6400, 6428, 6480, 6509, 6512, 6516, 6528, 6571, 6593, 6599, 6656, 6678, 6688, 6740, 6823, 6823, 6917, 6963, 6981, 6987, 7043, 7072, 7086, 7087, 7098, 7141, 7168, 7203, 7245, 7247, 7258, 7293, 7401, 7404, 7406, 7409, 7413, 7414, 7424, 7615, 7680, 7957, 7960, 7965, 7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061, 8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140, 8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188, 8305, 8305, 8319, 8319, 8336, 8348, 8450, 8450, 8455, 8455, 8458, 8467, 8469, 8469, 8473, 8477, 8484, 8484, 8486, 8486, 8488, 8488, 8490, 8493, 8495, 8505, 8508, 8511, 8517, 8521, 8526, 8526, 8544, 8584, 11264, 11310, 11312, 11358, 11360, 11492, 11499, 11502, 11506, 11507, 11520, 11557, 11559, 11559, 11565, 11565, 11568, 11623, 11631, 11631, 11648, 11670, 11680, 11686, 11688, 11694, 11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726, 11728, 11734, 11736, 11742, 11823, 11823, 12293, 12295, 12321, 12329, 12337, 12341, 12344, 12348, 12353, 12438, 12445, 12447, 12449, 12538, 12540, 12543, 12549, 12589, 12593, 12686, 12704, 12730, 12784, 12799, 13312, 19893, 19968, 40908, 40960, 42124, 42192, 42237, 42240, 42508, 42512, 42527, 42538, 42539, 42560, 42606, 42623, 42647, 42656, 42735, 42775, 42783, 42786, 42888, 42891, 42894, 42896, 42899, 42912, 42922, 43000, 43009, 43011, 43013, 43015, 43018, 43020, 43042, 43072, 43123, 43138, 43187, 43250, 43255, 43259, 43259, 43274, 43301, 43312, 43334, 43360, 43388, 43396, 43442, 43471, 43471, 43520, 43560, 43584, 43586, 43588, 43595, 43616, 43638, 43642, 43642, 43648, 43695, 43697, 43697, 43701, 43702, 43705, 43709, 43712, 43712, 43714, 43714, 43739, 43741, 43744, 43754, 43762, 43764, 43777, 43782, 43785, 43790, 43793, 43798, 43808, 43814, 43816, 43822, 43968, 44002, 44032, 55203, 55216, 55238, 55243, 55291, 63744, 64109, 64112, 64217, 64256, 64262, 64275, 64279, 64285, 64285, 64287, 64296, 64298, 64310, 64312, 64316, 64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433, 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019, 65136, 65140, 65142, 65276, 65313, 65338, 65345, 65370, 65382, 65470, 65474, 65479, 65482, 65487, 65490, 65495, 65498, 65500, ]; - const unicodeES5IdentifierPart = [170, 170, 181, 181, 186, 186, 192, 214, 216, 246, 248, 705, 710, 721, 736, 740, 748, 748, 750, 750, 768, 884, 886, 887, 890, 893, 902, 902, 904, 906, 908, 908, 910, 929, 931, 1013, 1015, 1153, 1155, 1159, 1162, 1319, 1329, 1366, 1369, 1369, 1377, 1415, 1425, 1469, 1471, 1471, 1473, 1474, 1476, 1477, 1479, 1479, 1488, 1514, 1520, 1522, 1552, 1562, 1568, 1641, 1646, 1747, 1749, 1756, 1759, 1768, 1770, 1788, 1791, 1791, 1808, 1866, 1869, 1969, 1984, 2037, 2042, 2042, 2048, 2093, 2112, 2139, 2208, 2208, 2210, 2220, 2276, 2302, 2304, 2403, 2406, 2415, 2417, 2423, 2425, 2431, 2433, 2435, 2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482, 2486, 2489, 2492, 2500, 2503, 2504, 2507, 2510, 2519, 2519, 2524, 2525, 2527, 2531, 2534, 2545, 2561, 2563, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611, 2613, 2614, 2616, 2617, 2620, 2620, 2622, 2626, 2631, 2632, 2635, 2637, 2641, 2641, 2649, 2652, 2654, 2654, 2662, 2677, 2689, 2691, 2693, 2701, 2703, 2705, 2707, 2728, 2730, 2736, 2738, 2739, 2741, 2745, 2748, 2757, 2759, 2761, 2763, 2765, 2768, 2768, 2784, 2787, 2790, 2799, 2817, 2819, 2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867, 2869, 2873, 2876, 2884, 2887, 2888, 2891, 2893, 2902, 2903, 2908, 2909, 2911, 2915, 2918, 2927, 2929, 2929, 2946, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001, 3006, 3010, 3014, 3016, 3018, 3021, 3024, 3024, 3031, 3031, 3046, 3055, 3073, 3075, 3077, 3084, 3086, 3088, 3090, 3112, 3114, 3123, 3125, 3129, 3133, 3140, 3142, 3144, 3146, 3149, 3157, 3158, 3160, 3161, 3168, 3171, 3174, 3183, 3202, 3203, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3260, 3268, 3270, 3272, 3274, 3277, 3285, 3286, 3294, 3294, 3296, 3299, 3302, 3311, 3313, 3314, 3330, 3331, 3333, 3340, 3342, 3344, 3346, 3386, 3389, 3396, 3398, 3400, 3402, 3406, 3415, 3415, 3424, 3427, 3430, 3439, 3450, 3455, 3458, 3459, 3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526, 3530, 3530, 3535, 3540, 3542, 3542, 3544, 3551, 3570, 3571, 3585, 3642, 3648, 3662, 3664, 3673, 3713, 3714, 3716, 3716, 3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743, 3745, 3747, 3749, 3749, 3751, 3751, 3754, 3755, 3757, 3769, 3771, 3773, 3776, 3780, 3782, 3782, 3784, 3789, 3792, 3801, 3804, 3807, 3840, 3840, 3864, 3865, 3872, 3881, 3893, 3893, 3895, 3895, 3897, 3897, 3902, 3911, 3913, 3948, 3953, 3972, 3974, 3991, 3993, 4028, 4038, 4038, 4096, 4169, 4176, 4253, 4256, 4293, 4295, 4295, 4301, 4301, 4304, 4346, 4348, 4680, 4682, 4685, 4688, 4694, 4696, 4696, 4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784, 4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880, 4882, 4885, 4888, 4954, 4957, 4959, 4992, 5007, 5024, 5108, 5121, 5740, 5743, 5759, 5761, 5786, 5792, 5866, 5870, 5872, 5888, 5900, 5902, 5908, 5920, 5940, 5952, 5971, 5984, 5996, 5998, 6000, 6002, 6003, 6016, 6099, 6103, 6103, 6108, 6109, 6112, 6121, 6155, 6157, 6160, 6169, 6176, 6263, 6272, 6314, 6320, 6389, 6400, 6428, 6432, 6443, 6448, 6459, 6470, 6509, 6512, 6516, 6528, 6571, 6576, 6601, 6608, 6617, 6656, 6683, 6688, 6750, 6752, 6780, 6783, 6793, 6800, 6809, 6823, 6823, 6912, 6987, 6992, 7001, 7019, 7027, 7040, 7155, 7168, 7223, 7232, 7241, 7245, 7293, 7376, 7378, 7380, 7414, 7424, 7654, 7676, 7957, 7960, 7965, 7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061, 8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140, 8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188, 8204, 8205, 8255, 8256, 8276, 8276, 8305, 8305, 8319, 8319, 8336, 8348, 8400, 8412, 8417, 8417, 8421, 8432, 8450, 8450, 8455, 8455, 8458, 8467, 8469, 8469, 8473, 8477, 8484, 8484, 8486, 8486, 8488, 8488, 8490, 8493, 8495, 8505, 8508, 8511, 8517, 8521, 8526, 8526, 8544, 8584, 11264, 11310, 11312, 11358, 11360, 11492, 11499, 11507, 11520, 11557, 11559, 11559, 11565, 11565, 11568, 11623, 11631, 11631, 11647, 11670, 11680, 11686, 11688, 11694, 11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726, 11728, 11734, 11736, 11742, 11744, 11775, 11823, 11823, 12293, 12295, 12321, 12335, 12337, 12341, 12344, 12348, 12353, 12438, 12441, 12442, 12445, 12447, 12449, 12538, 12540, 12543, 12549, 12589, 12593, 12686, 12704, 12730, 12784, 12799, 13312, 19893, 19968, 40908, 40960, 42124, 42192, 42237, 42240, 42508, 42512, 42539, 42560, 42607, 42612, 42621, 42623, 42647, 42655, 42737, 42775, 42783, 42786, 42888, 42891, 42894, 42896, 42899, 42912, 42922, 43000, 43047, 43072, 43123, 43136, 43204, 43216, 43225, 43232, 43255, 43259, 43259, 43264, 43309, 43312, 43347, 43360, 43388, 43392, 43456, 43471, 43481, 43520, 43574, 43584, 43597, 43600, 43609, 43616, 43638, 43642, 43643, 43648, 43714, 43739, 43741, 43744, 43759, 43762, 43766, 43777, 43782, 43785, 43790, 43793, 43798, 43808, 43814, 43816, 43822, 43968, 44010, 44012, 44013, 44016, 44025, 44032, 55203, 55216, 55238, 55243, 55291, 63744, 64109, 64112, 64217, 64256, 64262, 64275, 64279, 64285, 64296, 64298, 64310, 64312, 64316, 64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433, 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019, 65024, 65039, 65056, 65062, 65075, 65076, 65101, 65103, 65136, 65140, 65142, 65276, 65296, 65305, 65313, 65338, 65343, 65343, 65345, 65370, 65382, 65470, 65474, 65479, 65482, 65487, 65490, 65495, 65498, 65500, ]; - - /** - * Generated by scripts/regenerate-unicode-identifier-parts.js on node v12.4.0 with unicode 12.1 - * based on http://www.unicode.org/reports/tr31/ and https://www.ecma-international.org/ecma-262/6.0/#sec-names-and-keywords - * unicodeESNextIdentifierStart corresponds to the ID_Start and Other_ID_Start property, and - * unicodeESNextIdentifierPart corresponds to ID_Continue, Other_ID_Continue, plus ID_Start and Other_ID_Start - */ - const unicodeESNextIdentifierStart = [65, 90, 97, 122, 170, 170, 181, 181, 186, 186, 192, 214, 216, 246, 248, 705, 710, 721, 736, 740, 748, 748, 750, 750, 880, 884, 886, 887, 890, 893, 895, 895, 902, 902, 904, 906, 908, 908, 910, 929, 931, 1013, 1015, 1153, 1162, 1327, 1329, 1366, 1369, 1369, 1376, 1416, 1488, 1514, 1519, 1522, 1568, 1610, 1646, 1647, 1649, 1747, 1749, 1749, 1765, 1766, 1774, 1775, 1786, 1788, 1791, 1791, 1808, 1808, 1810, 1839, 1869, 1957, 1969, 1969, 1994, 2026, 2036, 2037, 2042, 2042, 2048, 2069, 2074, 2074, 2084, 2084, 2088, 2088, 2112, 2136, 2144, 2154, 2208, 2228, 2230, 2237, 2308, 2361, 2365, 2365, 2384, 2384, 2392, 2401, 2417, 2432, 2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482, 2486, 2489, 2493, 2493, 2510, 2510, 2524, 2525, 2527, 2529, 2544, 2545, 2556, 2556, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611, 2613, 2614, 2616, 2617, 2649, 2652, 2654, 2654, 2674, 2676, 2693, 2701, 2703, 2705, 2707, 2728, 2730, 2736, 2738, 2739, 2741, 2745, 2749, 2749, 2768, 2768, 2784, 2785, 2809, 2809, 2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867, 2869, 2873, 2877, 2877, 2908, 2909, 2911, 2913, 2929, 2929, 2947, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001, 3024, 3024, 3077, 3084, 3086, 3088, 3090, 3112, 3114, 3129, 3133, 3133, 3160, 3162, 3168, 3169, 3200, 3200, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3261, 3261, 3294, 3294, 3296, 3297, 3313, 3314, 3333, 3340, 3342, 3344, 3346, 3386, 3389, 3389, 3406, 3406, 3412, 3414, 3423, 3425, 3450, 3455, 3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526, 3585, 3632, 3634, 3635, 3648, 3654, 3713, 3714, 3716, 3716, 3718, 3722, 3724, 3747, 3749, 3749, 3751, 3760, 3762, 3763, 3773, 3773, 3776, 3780, 3782, 3782, 3804, 3807, 3840, 3840, 3904, 3911, 3913, 3948, 3976, 3980, 4096, 4138, 4159, 4159, 4176, 4181, 4186, 4189, 4193, 4193, 4197, 4198, 4206, 4208, 4213, 4225, 4238, 4238, 4256, 4293, 4295, 4295, 4301, 4301, 4304, 4346, 4348, 4680, 4682, 4685, 4688, 4694, 4696, 4696, 4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784, 4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880, 4882, 4885, 4888, 4954, 4992, 5007, 5024, 5109, 5112, 5117, 5121, 5740, 5743, 5759, 5761, 5786, 5792, 5866, 5870, 5880, 5888, 5900, 5902, 5905, 5920, 5937, 5952, 5969, 5984, 5996, 5998, 6000, 6016, 6067, 6103, 6103, 6108, 6108, 6176, 6264, 6272, 6312, 6314, 6314, 6320, 6389, 6400, 6430, 6480, 6509, 6512, 6516, 6528, 6571, 6576, 6601, 6656, 6678, 6688, 6740, 6823, 6823, 6917, 6963, 6981, 6987, 7043, 7072, 7086, 7087, 7098, 7141, 7168, 7203, 7245, 7247, 7258, 7293, 7296, 7304, 7312, 7354, 7357, 7359, 7401, 7404, 7406, 7411, 7413, 7414, 7418, 7418, 7424, 7615, 7680, 7957, 7960, 7965, 7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061, 8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140, 8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188, 8305, 8305, 8319, 8319, 8336, 8348, 8450, 8450, 8455, 8455, 8458, 8467, 8469, 8469, 8472, 8477, 8484, 8484, 8486, 8486, 8488, 8488, 8490, 8505, 8508, 8511, 8517, 8521, 8526, 8526, 8544, 8584, 11264, 11310, 11312, 11358, 11360, 11492, 11499, 11502, 11506, 11507, 11520, 11557, 11559, 11559, 11565, 11565, 11568, 11623, 11631, 11631, 11648, 11670, 11680, 11686, 11688, 11694, 11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726, 11728, 11734, 11736, 11742, 12293, 12295, 12321, 12329, 12337, 12341, 12344, 12348, 12353, 12438, 12443, 12447, 12449, 12538, 12540, 12543, 12549, 12591, 12593, 12686, 12704, 12730, 12784, 12799, 13312, 19893, 19968, 40943, 40960, 42124, 42192, 42237, 42240, 42508, 42512, 42527, 42538, 42539, 42560, 42606, 42623, 42653, 42656, 42735, 42775, 42783, 42786, 42888, 42891, 42943, 42946, 42950, 42999, 43009, 43011, 43013, 43015, 43018, 43020, 43042, 43072, 43123, 43138, 43187, 43250, 43255, 43259, 43259, 43261, 43262, 43274, 43301, 43312, 43334, 43360, 43388, 43396, 43442, 43471, 43471, 43488, 43492, 43494, 43503, 43514, 43518, 43520, 43560, 43584, 43586, 43588, 43595, 43616, 43638, 43642, 43642, 43646, 43695, 43697, 43697, 43701, 43702, 43705, 43709, 43712, 43712, 43714, 43714, 43739, 43741, 43744, 43754, 43762, 43764, 43777, 43782, 43785, 43790, 43793, 43798, 43808, 43814, 43816, 43822, 43824, 43866, 43868, 43879, 43888, 44002, 44032, 55203, 55216, 55238, 55243, 55291, 63744, 64109, 64112, 64217, 64256, 64262, 64275, 64279, 64285, 64285, 64287, 64296, 64298, 64310, 64312, 64316, 64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433, 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019, 65136, 65140, 65142, 65276, 65313, 65338, 65345, 65370, 65382, 65470, 65474, 65479, 65482, 65487, 65490, 65495, 65498, 65500, 65536, 65547, 65549, 65574, 65576, 65594, 65596, 65597, 65599, 65613, 65616, 65629, 65664, 65786, 65856, 65908, 66176, 66204, 66208, 66256, 66304, 66335, 66349, 66378, 66384, 66421, 66432, 66461, 66464, 66499, 66504, 66511, 66513, 66517, 66560, 66717, 66736, 66771, 66776, 66811, 66816, 66855, 66864, 66915, 67072, 67382, 67392, 67413, 67424, 67431, 67584, 67589, 67592, 67592, 67594, 67637, 67639, 67640, 67644, 67644, 67647, 67669, 67680, 67702, 67712, 67742, 67808, 67826, 67828, 67829, 67840, 67861, 67872, 67897, 67968, 68023, 68030, 68031, 68096, 68096, 68112, 68115, 68117, 68119, 68121, 68149, 68192, 68220, 68224, 68252, 68288, 68295, 68297, 68324, 68352, 68405, 68416, 68437, 68448, 68466, 68480, 68497, 68608, 68680, 68736, 68786, 68800, 68850, 68864, 68899, 69376, 69404, 69415, 69415, 69424, 69445, 69600, 69622, 69635, 69687, 69763, 69807, 69840, 69864, 69891, 69926, 69956, 69956, 69968, 70002, 70006, 70006, 70019, 70066, 70081, 70084, 70106, 70106, 70108, 70108, 70144, 70161, 70163, 70187, 70272, 70278, 70280, 70280, 70282, 70285, 70287, 70301, 70303, 70312, 70320, 70366, 70405, 70412, 70415, 70416, 70419, 70440, 70442, 70448, 70450, 70451, 70453, 70457, 70461, 70461, 70480, 70480, 70493, 70497, 70656, 70708, 70727, 70730, 70751, 70751, 70784, 70831, 70852, 70853, 70855, 70855, 71040, 71086, 71128, 71131, 71168, 71215, 71236, 71236, 71296, 71338, 71352, 71352, 71424, 71450, 71680, 71723, 71840, 71903, 71935, 71935, 72096, 72103, 72106, 72144, 72161, 72161, 72163, 72163, 72192, 72192, 72203, 72242, 72250, 72250, 72272, 72272, 72284, 72329, 72349, 72349, 72384, 72440, 72704, 72712, 72714, 72750, 72768, 72768, 72818, 72847, 72960, 72966, 72968, 72969, 72971, 73008, 73030, 73030, 73056, 73061, 73063, 73064, 73066, 73097, 73112, 73112, 73440, 73458, 73728, 74649, 74752, 74862, 74880, 75075, 77824, 78894, 82944, 83526, 92160, 92728, 92736, 92766, 92880, 92909, 92928, 92975, 92992, 92995, 93027, 93047, 93053, 93071, 93760, 93823, 93952, 94026, 94032, 94032, 94099, 94111, 94176, 94177, 94179, 94179, 94208, 100343, 100352, 101106, 110592, 110878, 110928, 110930, 110948, 110951, 110960, 111355, 113664, 113770, 113776, 113788, 113792, 113800, 113808, 113817, 119808, 119892, 119894, 119964, 119966, 119967, 119970, 119970, 119973, 119974, 119977, 119980, 119982, 119993, 119995, 119995, 119997, 120003, 120005, 120069, 120071, 120074, 120077, 120084, 120086, 120092, 120094, 120121, 120123, 120126, 120128, 120132, 120134, 120134, 120138, 120144, 120146, 120485, 120488, 120512, 120514, 120538, 120540, 120570, 120572, 120596, 120598, 120628, 120630, 120654, 120656, 120686, 120688, 120712, 120714, 120744, 120746, 120770, 120772, 120779, 123136, 123180, 123191, 123197, 123214, 123214, 123584, 123627, 124928, 125124, 125184, 125251, 125259, 125259, 126464, 126467, 126469, 126495, 126497, 126498, 126500, 126500, 126503, 126503, 126505, 126514, 126516, 126519, 126521, 126521, 126523, 126523, 126530, 126530, 126535, 126535, 126537, 126537, 126539, 126539, 126541, 126543, 126545, 126546, 126548, 126548, 126551, 126551, 126553, 126553, 126555, 126555, 126557, 126557, 126559, 126559, 126561, 126562, 126564, 126564, 126567, 126570, 126572, 126578, 126580, 126583, 126585, 126588, 126590, 126590, 126592, 126601, 126603, 126619, 126625, 126627, 126629, 126633, 126635, 126651, 131072, 173782, 173824, 177972, 177984, 178205, 178208, 183969, 183984, 191456, 194560, 195101]; - const unicodeESNextIdentifierPart = [48, 57, 65, 90, 95, 95, 97, 122, 170, 170, 181, 181, 183, 183, 186, 186, 192, 214, 216, 246, 248, 705, 710, 721, 736, 740, 748, 748, 750, 750, 768, 884, 886, 887, 890, 893, 895, 895, 902, 906, 908, 908, 910, 929, 931, 1013, 1015, 1153, 1155, 1159, 1162, 1327, 1329, 1366, 1369, 1369, 1376, 1416, 1425, 1469, 1471, 1471, 1473, 1474, 1476, 1477, 1479, 1479, 1488, 1514, 1519, 1522, 1552, 1562, 1568, 1641, 1646, 1747, 1749, 1756, 1759, 1768, 1770, 1788, 1791, 1791, 1808, 1866, 1869, 1969, 1984, 2037, 2042, 2042, 2045, 2045, 2048, 2093, 2112, 2139, 2144, 2154, 2208, 2228, 2230, 2237, 2259, 2273, 2275, 2403, 2406, 2415, 2417, 2435, 2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482, 2486, 2489, 2492, 2500, 2503, 2504, 2507, 2510, 2519, 2519, 2524, 2525, 2527, 2531, 2534, 2545, 2556, 2556, 2558, 2558, 2561, 2563, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611, 2613, 2614, 2616, 2617, 2620, 2620, 2622, 2626, 2631, 2632, 2635, 2637, 2641, 2641, 2649, 2652, 2654, 2654, 2662, 2677, 2689, 2691, 2693, 2701, 2703, 2705, 2707, 2728, 2730, 2736, 2738, 2739, 2741, 2745, 2748, 2757, 2759, 2761, 2763, 2765, 2768, 2768, 2784, 2787, 2790, 2799, 2809, 2815, 2817, 2819, 2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867, 2869, 2873, 2876, 2884, 2887, 2888, 2891, 2893, 2902, 2903, 2908, 2909, 2911, 2915, 2918, 2927, 2929, 2929, 2946, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001, 3006, 3010, 3014, 3016, 3018, 3021, 3024, 3024, 3031, 3031, 3046, 3055, 3072, 3084, 3086, 3088, 3090, 3112, 3114, 3129, 3133, 3140, 3142, 3144, 3146, 3149, 3157, 3158, 3160, 3162, 3168, 3171, 3174, 3183, 3200, 3203, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3260, 3268, 3270, 3272, 3274, 3277, 3285, 3286, 3294, 3294, 3296, 3299, 3302, 3311, 3313, 3314, 3328, 3331, 3333, 3340, 3342, 3344, 3346, 3396, 3398, 3400, 3402, 3406, 3412, 3415, 3423, 3427, 3430, 3439, 3450, 3455, 3458, 3459, 3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526, 3530, 3530, 3535, 3540, 3542, 3542, 3544, 3551, 3558, 3567, 3570, 3571, 3585, 3642, 3648, 3662, 3664, 3673, 3713, 3714, 3716, 3716, 3718, 3722, 3724, 3747, 3749, 3749, 3751, 3773, 3776, 3780, 3782, 3782, 3784, 3789, 3792, 3801, 3804, 3807, 3840, 3840, 3864, 3865, 3872, 3881, 3893, 3893, 3895, 3895, 3897, 3897, 3902, 3911, 3913, 3948, 3953, 3972, 3974, 3991, 3993, 4028, 4038, 4038, 4096, 4169, 4176, 4253, 4256, 4293, 4295, 4295, 4301, 4301, 4304, 4346, 4348, 4680, 4682, 4685, 4688, 4694, 4696, 4696, 4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784, 4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880, 4882, 4885, 4888, 4954, 4957, 4959, 4969, 4977, 4992, 5007, 5024, 5109, 5112, 5117, 5121, 5740, 5743, 5759, 5761, 5786, 5792, 5866, 5870, 5880, 5888, 5900, 5902, 5908, 5920, 5940, 5952, 5971, 5984, 5996, 5998, 6000, 6002, 6003, 6016, 6099, 6103, 6103, 6108, 6109, 6112, 6121, 6155, 6157, 6160, 6169, 6176, 6264, 6272, 6314, 6320, 6389, 6400, 6430, 6432, 6443, 6448, 6459, 6470, 6509, 6512, 6516, 6528, 6571, 6576, 6601, 6608, 6618, 6656, 6683, 6688, 6750, 6752, 6780, 6783, 6793, 6800, 6809, 6823, 6823, 6832, 6845, 6912, 6987, 6992, 7001, 7019, 7027, 7040, 7155, 7168, 7223, 7232, 7241, 7245, 7293, 7296, 7304, 7312, 7354, 7357, 7359, 7376, 7378, 7380, 7418, 7424, 7673, 7675, 7957, 7960, 7965, 7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061, 8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140, 8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188, 8255, 8256, 8276, 8276, 8305, 8305, 8319, 8319, 8336, 8348, 8400, 8412, 8417, 8417, 8421, 8432, 8450, 8450, 8455, 8455, 8458, 8467, 8469, 8469, 8472, 8477, 8484, 8484, 8486, 8486, 8488, 8488, 8490, 8505, 8508, 8511, 8517, 8521, 8526, 8526, 8544, 8584, 11264, 11310, 11312, 11358, 11360, 11492, 11499, 11507, 11520, 11557, 11559, 11559, 11565, 11565, 11568, 11623, 11631, 11631, 11647, 11670, 11680, 11686, 11688, 11694, 11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726, 11728, 11734, 11736, 11742, 11744, 11775, 12293, 12295, 12321, 12335, 12337, 12341, 12344, 12348, 12353, 12438, 12441, 12447, 12449, 12538, 12540, 12543, 12549, 12591, 12593, 12686, 12704, 12730, 12784, 12799, 13312, 19893, 19968, 40943, 40960, 42124, 42192, 42237, 42240, 42508, 42512, 42539, 42560, 42607, 42612, 42621, 42623, 42737, 42775, 42783, 42786, 42888, 42891, 42943, 42946, 42950, 42999, 43047, 43072, 43123, 43136, 43205, 43216, 43225, 43232, 43255, 43259, 43259, 43261, 43309, 43312, 43347, 43360, 43388, 43392, 43456, 43471, 43481, 43488, 43518, 43520, 43574, 43584, 43597, 43600, 43609, 43616, 43638, 43642, 43714, 43739, 43741, 43744, 43759, 43762, 43766, 43777, 43782, 43785, 43790, 43793, 43798, 43808, 43814, 43816, 43822, 43824, 43866, 43868, 43879, 43888, 44010, 44012, 44013, 44016, 44025, 44032, 55203, 55216, 55238, 55243, 55291, 63744, 64109, 64112, 64217, 64256, 64262, 64275, 64279, 64285, 64296, 64298, 64310, 64312, 64316, 64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433, 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019, 65024, 65039, 65056, 65071, 65075, 65076, 65101, 65103, 65136, 65140, 65142, 65276, 65296, 65305, 65313, 65338, 65343, 65343, 65345, 65370, 65382, 65470, 65474, 65479, 65482, 65487, 65490, 65495, 65498, 65500, 65536, 65547, 65549, 65574, 65576, 65594, 65596, 65597, 65599, 65613, 65616, 65629, 65664, 65786, 65856, 65908, 66045, 66045, 66176, 66204, 66208, 66256, 66272, 66272, 66304, 66335, 66349, 66378, 66384, 66426, 66432, 66461, 66464, 66499, 66504, 66511, 66513, 66517, 66560, 66717, 66720, 66729, 66736, 66771, 66776, 66811, 66816, 66855, 66864, 66915, 67072, 67382, 67392, 67413, 67424, 67431, 67584, 67589, 67592, 67592, 67594, 67637, 67639, 67640, 67644, 67644, 67647, 67669, 67680, 67702, 67712, 67742, 67808, 67826, 67828, 67829, 67840, 67861, 67872, 67897, 67968, 68023, 68030, 68031, 68096, 68099, 68101, 68102, 68108, 68115, 68117, 68119, 68121, 68149, 68152, 68154, 68159, 68159, 68192, 68220, 68224, 68252, 68288, 68295, 68297, 68326, 68352, 68405, 68416, 68437, 68448, 68466, 68480, 68497, 68608, 68680, 68736, 68786, 68800, 68850, 68864, 68903, 68912, 68921, 69376, 69404, 69415, 69415, 69424, 69456, 69600, 69622, 69632, 69702, 69734, 69743, 69759, 69818, 69840, 69864, 69872, 69881, 69888, 69940, 69942, 69951, 69956, 69958, 69968, 70003, 70006, 70006, 70016, 70084, 70089, 70092, 70096, 70106, 70108, 70108, 70144, 70161, 70163, 70199, 70206, 70206, 70272, 70278, 70280, 70280, 70282, 70285, 70287, 70301, 70303, 70312, 70320, 70378, 70384, 70393, 70400, 70403, 70405, 70412, 70415, 70416, 70419, 70440, 70442, 70448, 70450, 70451, 70453, 70457, 70459, 70468, 70471, 70472, 70475, 70477, 70480, 70480, 70487, 70487, 70493, 70499, 70502, 70508, 70512, 70516, 70656, 70730, 70736, 70745, 70750, 70751, 70784, 70853, 70855, 70855, 70864, 70873, 71040, 71093, 71096, 71104, 71128, 71133, 71168, 71232, 71236, 71236, 71248, 71257, 71296, 71352, 71360, 71369, 71424, 71450, 71453, 71467, 71472, 71481, 71680, 71738, 71840, 71913, 71935, 71935, 72096, 72103, 72106, 72151, 72154, 72161, 72163, 72164, 72192, 72254, 72263, 72263, 72272, 72345, 72349, 72349, 72384, 72440, 72704, 72712, 72714, 72758, 72760, 72768, 72784, 72793, 72818, 72847, 72850, 72871, 72873, 72886, 72960, 72966, 72968, 72969, 72971, 73014, 73018, 73018, 73020, 73021, 73023, 73031, 73040, 73049, 73056, 73061, 73063, 73064, 73066, 73102, 73104, 73105, 73107, 73112, 73120, 73129, 73440, 73462, 73728, 74649, 74752, 74862, 74880, 75075, 77824, 78894, 82944, 83526, 92160, 92728, 92736, 92766, 92768, 92777, 92880, 92909, 92912, 92916, 92928, 92982, 92992, 92995, 93008, 93017, 93027, 93047, 93053, 93071, 93760, 93823, 93952, 94026, 94031, 94087, 94095, 94111, 94176, 94177, 94179, 94179, 94208, 100343, 100352, 101106, 110592, 110878, 110928, 110930, 110948, 110951, 110960, 111355, 113664, 113770, 113776, 113788, 113792, 113800, 113808, 113817, 113821, 113822, 119141, 119145, 119149, 119154, 119163, 119170, 119173, 119179, 119210, 119213, 119362, 119364, 119808, 119892, 119894, 119964, 119966, 119967, 119970, 119970, 119973, 119974, 119977, 119980, 119982, 119993, 119995, 119995, 119997, 120003, 120005, 120069, 120071, 120074, 120077, 120084, 120086, 120092, 120094, 120121, 120123, 120126, 120128, 120132, 120134, 120134, 120138, 120144, 120146, 120485, 120488, 120512, 120514, 120538, 120540, 120570, 120572, 120596, 120598, 120628, 120630, 120654, 120656, 120686, 120688, 120712, 120714, 120744, 120746, 120770, 120772, 120779, 120782, 120831, 121344, 121398, 121403, 121452, 121461, 121461, 121476, 121476, 121499, 121503, 121505, 121519, 122880, 122886, 122888, 122904, 122907, 122913, 122915, 122916, 122918, 122922, 123136, 123180, 123184, 123197, 123200, 123209, 123214, 123214, 123584, 123641, 124928, 125124, 125136, 125142, 125184, 125259, 125264, 125273, 126464, 126467, 126469, 126495, 126497, 126498, 126500, 126500, 126503, 126503, 126505, 126514, 126516, 126519, 126521, 126521, 126523, 126523, 126530, 126530, 126535, 126535, 126537, 126537, 126539, 126539, 126541, 126543, 126545, 126546, 126548, 126548, 126551, 126551, 126553, 126553, 126555, 126555, 126557, 126557, 126559, 126559, 126561, 126562, 126564, 126564, 126567, 126570, 126572, 126578, 126580, 126583, 126585, 126588, 126590, 126590, 126592, 126601, 126603, 126619, 126625, 126627, 126629, 126633, 126635, 126651, 131072, 173782, 173824, 177972, 177984, 178205, 178208, 183969, 183984, 191456, 194560, 195101, 917760, 917999]; - - function lookupInUnicodeMap(code: number, map: readonly number[]): boolean { - // Bail out quickly if it couldn't possibly be in the map. - if (code < map[0]) { - return false; + // Perform binary search in one of the Unicode range maps + let lo = 0; + let hi: number = map.length; + let mid: number; + while (lo + 1 < hi) { + mid = lo + (hi - lo) / 2; + // mid has to be even to catch a range's beginning + mid -= mid % 2; + if (map[mid] <= code && code <= map[mid + 1]) { + return true; + } + if (code < map[mid]) { + hi = mid; } - - // Perform binary search in one of the Unicode range maps - let lo = 0; - let hi: number = map.length; - let mid: number; - - while (lo + 1 < hi) { - mid = lo + (hi - lo) / 2; - // mid has to be even to catch a range's beginning - mid -= mid % 2; - if (map[mid] <= code && code <= map[mid + 1]) { - return true; - } - - if (code < map[mid]) { - hi = mid; - } - else { - lo = mid + 2; - } + else { + lo = mid + 2; } - - return false; } - - /* @internal */ export function isUnicodeIdentifierStart(code: number, languageVersion: ScriptTarget | undefined) { - return languageVersion! >= ScriptTarget.ES2015 ? - lookupInUnicodeMap(code, unicodeESNextIdentifierStart) : - languageVersion! === ScriptTarget.ES5 ? lookupInUnicodeMap(code, unicodeES5IdentifierStart) : + return false; +} +/* @internal */ export function isUnicodeIdentifierStart(code: number, languageVersion: ScriptTarget | undefined) { + return (languageVersion!) >= ScriptTarget.ES2015 ? + lookupInUnicodeMap(code, unicodeESNextIdentifierStart) : + (languageVersion!) === ScriptTarget.ES5 ? lookupInUnicodeMap(code, unicodeES5IdentifierStart) : lookupInUnicodeMap(code, unicodeES3IdentifierStart); - } - - function isUnicodeIdentifierPart(code: number, languageVersion: ScriptTarget | undefined) { - return languageVersion! >= ScriptTarget.ES2015 ? - lookupInUnicodeMap(code, unicodeESNextIdentifierPart) : - languageVersion! === ScriptTarget.ES5 ? lookupInUnicodeMap(code, unicodeES5IdentifierPart) : +} +function isUnicodeIdentifierPart(code: number, languageVersion: ScriptTarget | undefined) { + return (languageVersion!) >= ScriptTarget.ES2015 ? + lookupInUnicodeMap(code, unicodeESNextIdentifierPart) : + (languageVersion!) === ScriptTarget.ES5 ? lookupInUnicodeMap(code, unicodeES5IdentifierPart) : lookupInUnicodeMap(code, unicodeES3IdentifierPart); - } - - function makeReverseMap(source: Map): string[] { - const result: string[] = []; - source.forEach((value, name) => { - result[value] = name; - }); - return result; - } - - const tokenStrings = makeReverseMap(textToToken); - export function tokenToString(t: SyntaxKind): string | undefined { - return tokenStrings[t]; - } - - /* @internal */ - export function stringToToken(s: string): SyntaxKind | undefined { - return textToToken.get(s); - } - - /* @internal */ - export function computeLineStarts(text: string): number[] { - const result: number[] = new Array(); - let pos = 0; - let lineStart = 0; - while (pos < text.length) { - const ch = text.charCodeAt(pos); - pos++; - switch (ch) { - case CharacterCodes.carriageReturn: - if (text.charCodeAt(pos) === CharacterCodes.lineFeed) { - pos++; - } - // falls through - case CharacterCodes.lineFeed: +} +function makeReverseMap(source: ts.Map): string[] { + const result: string[] = []; + source.forEach((value, name) => { + result[value] = name; + }); + return result; +} +const tokenStrings = makeReverseMap(textToToken); +export function tokenToString(t: SyntaxKind): string | undefined { + return tokenStrings[t]; +} +/* @internal */ +export function stringToToken(s: string): SyntaxKind | undefined { + return textToToken.get(s); +} +/* @internal */ +export function computeLineStarts(text: string): number[] { + const result: number[] = new Array(); + let pos = 0; + let lineStart = 0; + while (pos < text.length) { + const ch = text.charCodeAt(pos); + pos++; + switch (ch) { + case CharacterCodes.carriageReturn: + if (text.charCodeAt(pos) === CharacterCodes.lineFeed) { + pos++; + } + // falls through + case CharacterCodes.lineFeed: + result.push(lineStart); + lineStart = pos; + break; + default: + if (ch > CharacterCodes.maxAsciiCharacter && isLineBreak(ch)) { result.push(lineStart); lineStart = pos; - break; - default: - if (ch > CharacterCodes.maxAsciiCharacter && isLineBreak(ch)) { - result.push(lineStart); - lineStart = pos; - } - break; - } + } + break; } - result.push(lineStart); - return result; } - - export function getPositionOfLineAndCharacter(sourceFile: SourceFileLike, line: number, character: number): number; - /* @internal */ - export function getPositionOfLineAndCharacter(sourceFile: SourceFileLike, line: number, character: number, allowEdits?: true): number; // eslint-disable-line @typescript-eslint/unified-signatures - export function getPositionOfLineAndCharacter(sourceFile: SourceFileLike, line: number, character: number, allowEdits?: true): number { - return sourceFile.getPositionOfLineAndCharacter ? - sourceFile.getPositionOfLineAndCharacter(line, character, allowEdits) : - computePositionOfLineAndCharacter(getLineStarts(sourceFile), line, character, sourceFile.text, allowEdits); - } - - /* @internal */ - export function computePositionOfLineAndCharacter(lineStarts: readonly number[], line: number, character: number, debugText?: string, allowEdits?: true): number { - if (line < 0 || line >= lineStarts.length) { - if (allowEdits) { - // Clamp line to nearest allowable value - line = line < 0 ? 0 : line >= lineStarts.length ? lineStarts.length - 1 : line; - } - else { - Debug.fail(`Bad line number. Line: ${line}, lineStarts.length: ${lineStarts.length} , line map is correct? ${debugText !== undefined ? arraysEqual(lineStarts, computeLineStarts(debugText)) : "unknown"}`); - } - } - - const res = lineStarts[line] + character; + result.push(lineStart); + return result; +} +export function getPositionOfLineAndCharacter(sourceFile: SourceFileLike, line: number, character: number): number; +/* @internal */ +export function getPositionOfLineAndCharacter(sourceFile: SourceFileLike, line: number, character: number, allowEdits?: true): number; // eslint-disable-line @typescript-eslint/unified-signatures +export function getPositionOfLineAndCharacter(sourceFile: SourceFileLike, line: number, character: number, allowEdits?: true): number { + return sourceFile.getPositionOfLineAndCharacter ? + sourceFile.getPositionOfLineAndCharacter(line, character, allowEdits) : + computePositionOfLineAndCharacter(getLineStarts(sourceFile), line, character, sourceFile.text, allowEdits); +} +/* @internal */ +export function computePositionOfLineAndCharacter(lineStarts: readonly number[], line: number, character: number, debugText?: string, allowEdits?: true): number { + if (line < 0 || line >= lineStarts.length) { if (allowEdits) { - // Clamp to nearest allowable values to allow the underlying to be edited without crashing (accuracy is lost, instead) - // TODO: Somehow track edits between file as it was during the creation of sourcemap we have and the current file and - // apply them to the computed position to improve accuracy - return res > lineStarts[line + 1] ? lineStarts[line + 1] : typeof debugText === "string" && res > debugText.length ? debugText.length : res; + // Clamp line to nearest allowable value + line = line < 0 ? 0 : line >= lineStarts.length ? lineStarts.length - 1 : line; } - if (line < lineStarts.length - 1) { - Debug.assert(res < lineStarts[line + 1]); - } - else if (debugText !== undefined) { - Debug.assert(res <= debugText.length); // Allow single character overflow for trailing newline - } - return res; - } - - /* @internal */ - export function getLineStarts(sourceFile: SourceFileLike): readonly number[] { - return sourceFile.lineMap || (sourceFile.lineMap = computeLineStarts(sourceFile.text)); - } - - /* @internal */ - /** - * We assume the first line starts at position 0 and 'position' is non-negative. - */ - export function computeLineAndCharacterOfPosition(lineStarts: readonly number[], position: number): LineAndCharacter { - let lineNumber = binarySearch(lineStarts, position, identity, compareValues); - if (lineNumber < 0) { - // If the actual position was not found, - // the binary search returns the 2's-complement of the next line start - // e.g. if the line starts at [5, 10, 23, 80] and the position requested was 20 - // then the search will return -2. - // - // We want the index of the previous line start, so we subtract 1. - // Review 2's-complement if this is confusing. - lineNumber = ~lineNumber - 1; - Debug.assert(lineNumber !== -1, "position cannot precede the beginning of the file"); + else { + Debug.fail(`Bad line number. Line: ${line}, lineStarts.length: ${lineStarts.length} , line map is correct? ${debugText !== undefined ? arraysEqual(lineStarts, computeLineStarts(debugText)) : "unknown"}`); } - return { - line: lineNumber, - character: position - lineStarts[lineNumber] - }; } - - export function getLineAndCharacterOfPosition(sourceFile: SourceFileLike, position: number): LineAndCharacter { - return computeLineAndCharacterOfPosition(getLineStarts(sourceFile), position); - } - - export function isWhiteSpaceLike(ch: number): boolean { - return isWhiteSpaceSingleLine(ch) || isLineBreak(ch); - } - - /** Does not include line breaks. For that, see isWhiteSpaceLike. */ - export function isWhiteSpaceSingleLine(ch: number): boolean { - // Note: nextLine is in the Zs space, and should be considered to be a whitespace. - // It is explicitly not a line-break as it isn't in the exact set specified by EcmaScript. - return ch === CharacterCodes.space || - ch === CharacterCodes.tab || - ch === CharacterCodes.verticalTab || - ch === CharacterCodes.formFeed || - ch === CharacterCodes.nonBreakingSpace || - ch === CharacterCodes.nextLine || - ch === CharacterCodes.ogham || - ch >= CharacterCodes.enQuad && ch <= CharacterCodes.zeroWidthSpace || - ch === CharacterCodes.narrowNoBreakSpace || - ch === CharacterCodes.mathematicalSpace || - ch === CharacterCodes.ideographicSpace || - ch === CharacterCodes.byteOrderMark; + const res = lineStarts[line] + character; + if (allowEdits) { + // Clamp to nearest allowable values to allow the underlying to be edited without crashing (accuracy is lost, instead) + // TODO: Somehow track edits between file as it was during the creation of sourcemap we have and the current file and + // apply them to the computed position to improve accuracy + return res > lineStarts[line + 1] ? lineStarts[line + 1] : typeof debugText === "string" && res > debugText.length ? debugText.length : res; } - - export function isLineBreak(ch: number): boolean { - // ES5 7.3: - // The ECMAScript line terminator characters are listed in Table 3. - // Table 3: Line Terminator Characters - // Code Unit Value Name Formal Name - // \u000A Line Feed - // \u000D Carriage Return - // \u2028 Line separator - // \u2029 Paragraph separator - // Only the characters in Table 3 are treated as line terminators. Other new line or line - // breaking characters are treated as white space but not as line terminators. - - return ch === CharacterCodes.lineFeed || - ch === CharacterCodes.carriageReturn || - ch === CharacterCodes.lineSeparator || - ch === CharacterCodes.paragraphSeparator; + if (line < lineStarts.length - 1) { + Debug.assert(res < lineStarts[line + 1]); } - - function isDigit(ch: number): boolean { - return ch >= CharacterCodes._0 && ch <= CharacterCodes._9; + else if (debugText !== undefined) { + Debug.assert(res <= debugText.length); // Allow single character overflow for trailing newline } - - function isHexDigit(ch: number): boolean { - return isDigit(ch) || ch >= CharacterCodes.A && ch <= CharacterCodes.F || ch >= CharacterCodes.a && ch <= CharacterCodes.f; + return res; +} +/* @internal */ +export function getLineStarts(sourceFile: SourceFileLike): readonly number[] { + return sourceFile.lineMap || (sourceFile.lineMap = computeLineStarts(sourceFile.text)); +} +/* @internal */ +/** + * We assume the first line starts at position 0 and 'position' is non-negative. + */ +export function computeLineAndCharacterOfPosition(lineStarts: readonly number[], position: number): LineAndCharacter { + let lineNumber = binarySearch(lineStarts, position, identity, compareValues); + if (lineNumber < 0) { + // If the actual position was not found, + // the binary search returns the 2's-complement of the next line start + // e.g. if the line starts at [5, 10, 23, 80] and the position requested was 20 + // then the search will return -2. + // + // We want the index of the previous line start, so we subtract 1. + // Review 2's-complement if this is confusing. + lineNumber = ~lineNumber - 1; + Debug.assert(lineNumber !== -1, "position cannot precede the beginning of the file"); } - - function isCodePoint(code: number): boolean { - return code <= 0x10FFFF; + return { + line: lineNumber, + character: position - lineStarts[lineNumber] + }; +} +export function getLineAndCharacterOfPosition(sourceFile: SourceFileLike, position: number): LineAndCharacter { + return computeLineAndCharacterOfPosition(getLineStarts(sourceFile), position); +} +export function isWhiteSpaceLike(ch: number): boolean { + return isWhiteSpaceSingleLine(ch) || isLineBreak(ch); +} +/** Does not include line breaks. For that, see isWhiteSpaceLike. */ +export function isWhiteSpaceSingleLine(ch: number): boolean { + // Note: nextLine is in the Zs space, and should be considered to be a whitespace. + // It is explicitly not a line-break as it isn't in the exact set specified by EcmaScript. + return ch === CharacterCodes.space || + ch === CharacterCodes.tab || + ch === CharacterCodes.verticalTab || + ch === CharacterCodes.formFeed || + ch === CharacterCodes.nonBreakingSpace || + ch === CharacterCodes.nextLine || + ch === CharacterCodes.ogham || + ch >= CharacterCodes.enQuad && ch <= CharacterCodes.zeroWidthSpace || + ch === CharacterCodes.narrowNoBreakSpace || + ch === CharacterCodes.mathematicalSpace || + ch === CharacterCodes.ideographicSpace || + ch === CharacterCodes.byteOrderMark; +} +export function isLineBreak(ch: number): boolean { + // ES5 7.3: + // The ECMAScript line terminator characters are listed in Table 3. + // Table 3: Line Terminator Characters + // Code Unit Value Name Formal Name + // \u000A Line Feed + // \u000D Carriage Return + // \u2028 Line separator + // \u2029 Paragraph separator + // Only the characters in Table 3 are treated as line terminators. Other new line or line + // breaking characters are treated as white space but not as line terminators. + return ch === CharacterCodes.lineFeed || + ch === CharacterCodes.carriageReturn || + ch === CharacterCodes.lineSeparator || + ch === CharacterCodes.paragraphSeparator; +} +function isDigit(ch: number): boolean { + return ch >= CharacterCodes._0 && ch <= CharacterCodes._9; +} +function isHexDigit(ch: number): boolean { + return isDigit(ch) || ch >= CharacterCodes.A && ch <= CharacterCodes.F || ch >= CharacterCodes.a && ch <= CharacterCodes.f; +} +function isCodePoint(code: number): boolean { + return code <= 0x10FFFF; +} +/* @internal */ +export function isOctalDigit(ch: number): boolean { + return ch >= CharacterCodes._0 && ch <= CharacterCodes._7; +} +export function couldStartTrivia(text: string, pos: number): boolean { + // Keep in sync with skipTrivia + const ch = text.charCodeAt(pos); + switch (ch) { + case CharacterCodes.carriageReturn: + case CharacterCodes.lineFeed: + case CharacterCodes.tab: + case CharacterCodes.verticalTab: + case CharacterCodes.formFeed: + case CharacterCodes.space: + case CharacterCodes.slash: + // starts of normal trivia + // falls through + case CharacterCodes.lessThan: + case CharacterCodes.bar: + case CharacterCodes.equals: + case CharacterCodes.greaterThan: + // Starts of conflict marker trivia + return true; + case CharacterCodes.hash: + // Only if its the beginning can we have #! trivia + return pos === 0; + default: + return ch > CharacterCodes.maxAsciiCharacter; } - - /* @internal */ - export function isOctalDigit(ch: number): boolean { - return ch >= CharacterCodes._0 && ch <= CharacterCodes._7; +} +/* @internal */ +export function skipTrivia(text: string, pos: number, stopAfterLineBreak?: boolean, stopAtComments = false): number { + if (positionIsSynthesized(pos)) { + return pos; } - - export function couldStartTrivia(text: string, pos: number): boolean { - // Keep in sync with skipTrivia + // Keep in sync with couldStartTrivia + while (true) { const ch = text.charCodeAt(pos); switch (ch) { case CharacterCodes.carriageReturn: + if (text.charCodeAt(pos + 1) === CharacterCodes.lineFeed) { + pos++; + } + // falls through case CharacterCodes.lineFeed: + pos++; + if (stopAfterLineBreak) { + return pos; + } + continue; case CharacterCodes.tab: case CharacterCodes.verticalTab: case CharacterCodes.formFeed: case CharacterCodes.space: + pos++; + continue; case CharacterCodes.slash: - // starts of normal trivia - // falls through + if (stopAtComments) { + break; + } + if (text.charCodeAt(pos + 1) === CharacterCodes.slash) { + pos += 2; + while (pos < text.length) { + if (isLineBreak(text.charCodeAt(pos))) { + break; + } + pos++; + } + continue; + } + if (text.charCodeAt(pos + 1) === CharacterCodes.asterisk) { + pos += 2; + while (pos < text.length) { + if (text.charCodeAt(pos) === CharacterCodes.asterisk && text.charCodeAt(pos + 1) === CharacterCodes.slash) { + pos += 2; + break; + } + pos++; + } + continue; + } + break; case CharacterCodes.lessThan: case CharacterCodes.bar: case CharacterCodes.equals: case CharacterCodes.greaterThan: - // Starts of conflict marker trivia - return true; + if (isConflictMarkerTrivia(text, pos)) { + pos = scanConflictMarkerTrivia(text, pos); + continue; + } + break; case CharacterCodes.hash: - // Only if its the beginning can we have #! trivia - return pos === 0; + if (pos === 0 && isShebangTrivia(text, pos)) { + pos = scanShebangTrivia(text, pos); + continue; + } + break; default: - return ch > CharacterCodes.maxAsciiCharacter; + if (ch > CharacterCodes.maxAsciiCharacter && (isWhiteSpaceLike(ch))) { + pos++; + continue; + } + break; } + return pos; } - - /* @internal */ - export function skipTrivia(text: string, pos: number, stopAfterLineBreak?: boolean, stopAtComments = false): number { - if (positionIsSynthesized(pos)) { - return pos; +} +// All conflict markers consist of the same character repeated seven times. If it is +// a <<<<<<< or >>>>>>> marker then it is also followed by a space. +const mergeConflictMarkerLength = "<<<<<<<".length; +function isConflictMarkerTrivia(text: string, pos: number) { + Debug.assert(pos >= 0); + // Conflict markers must be at the start of a line. + if (pos === 0 || isLineBreak(text.charCodeAt(pos - 1))) { + const ch = text.charCodeAt(pos); + if ((pos + mergeConflictMarkerLength) < text.length) { + for (let i = 0; i < mergeConflictMarkerLength; i++) { + if (text.charCodeAt(pos + i) !== ch) { + return false; + } + } + return ch === CharacterCodes.equals || + text.charCodeAt(pos + mergeConflictMarkerLength) === CharacterCodes.space; } - - // Keep in sync with couldStartTrivia - while (true) { - const ch = text.charCodeAt(pos); - switch (ch) { - case CharacterCodes.carriageReturn: - if (text.charCodeAt(pos + 1) === CharacterCodes.lineFeed) { - pos++; - } - // falls through - case CharacterCodes.lineFeed: - pos++; - if (stopAfterLineBreak) { - return pos; - } - continue; - case CharacterCodes.tab: - case CharacterCodes.verticalTab: - case CharacterCodes.formFeed: - case CharacterCodes.space: + } + return false; +} +function scanConflictMarkerTrivia(text: string, pos: number, error?: (diag: DiagnosticMessage, pos?: number, len?: number) => void) { + if (error) { + error(Diagnostics.Merge_conflict_marker_encountered, pos, mergeConflictMarkerLength); + } + const ch = text.charCodeAt(pos); + const len = text.length; + if (ch === CharacterCodes.lessThan || ch === CharacterCodes.greaterThan) { + while (pos < len && !isLineBreak(text.charCodeAt(pos))) { + pos++; + } + } + else { + Debug.assert(ch === CharacterCodes.bar || ch === CharacterCodes.equals); + // Consume everything from the start of a ||||||| or ======= marker to the start + // of the next ======= or >>>>>>> marker. + while (pos < len) { + const currentChar = text.charCodeAt(pos); + if ((currentChar === CharacterCodes.equals || currentChar === CharacterCodes.greaterThan) && currentChar !== ch && isConflictMarkerTrivia(text, pos)) { + break; + } + pos++; + } + } + return pos; +} +const shebangTriviaRegex = /^#!.*/; +/*@internal*/ +export function isShebangTrivia(text: string, pos: number) { + // Shebangs check must only be done at the start of the file + Debug.assert(pos === 0); + return shebangTriviaRegex.test(text); +} +/*@internal*/ +export function scanShebangTrivia(text: string, pos: number) { + const shebang = shebangTriviaRegex.exec(text)![0]; + pos = pos + shebang.length; + return pos; +} +/** + * Invokes a callback for each comment range following the provided position. + * + * Single-line comment ranges include the leading double-slash characters but not the ending + * line break. Multi-line comment ranges include the leading slash-asterisk and trailing + * asterisk-slash characters. + * + * @param reduce If true, accumulates the result of calling the callback in a fashion similar + * to reduceLeft. If false, iteration stops when the callback returns a truthy value. + * @param text The source text to scan. + * @param pos The position at which to start scanning. + * @param trailing If false, whitespace is skipped until the first line break and comments + * between that location and the next token are returned. If true, comments occurring + * between the given position and the next line break are returned. + * @param cb The callback to execute as each comment range is encountered. + * @param state A state value to pass to each iteration of the callback. + * @param initial An initial value to pass when accumulating results (when "reduce" is true). + * @returns If "reduce" is true, the accumulated value. If "reduce" is false, the first truthy + * return value of the callback. + */ +function iterateCommentRanges(reduce: boolean, text: string, pos: number, trailing: boolean, cb: (pos: number, end: number, kind: CommentKind, hasTrailingNewLine: boolean, state: T, memo: U | undefined) => U, state: T, initial?: U): U | undefined { + let pendingPos!: number; + let pendingEnd!: number; + let pendingKind!: CommentKind; + let pendingHasTrailingNewLine!: boolean; + let hasPendingCommentRange = false; + let collecting = trailing; + let accumulator = initial; + if (pos === 0) { + collecting = true; + const shebang = getShebang(text); + if (shebang) { + pos = shebang.length; + } + } + scan: while (pos >= 0 && pos < text.length) { + const ch = text.charCodeAt(pos); + switch (ch) { + case CharacterCodes.carriageReturn: + if (text.charCodeAt(pos + 1) === CharacterCodes.lineFeed) { pos++; - continue; - case CharacterCodes.slash: - if (stopAtComments) { - break; - } - if (text.charCodeAt(pos + 1) === CharacterCodes.slash) { - pos += 2; + } + // falls through + case CharacterCodes.lineFeed: + pos++; + if (trailing) { + break scan; + } + collecting = true; + if (hasPendingCommentRange) { + pendingHasTrailingNewLine = true; + } + continue; + case CharacterCodes.tab: + case CharacterCodes.verticalTab: + case CharacterCodes.formFeed: + case CharacterCodes.space: + pos++; + continue; + case CharacterCodes.slash: + const nextChar = text.charCodeAt(pos + 1); + let hasTrailingNewLine = false; + if (nextChar === CharacterCodes.slash || nextChar === CharacterCodes.asterisk) { + const kind = nextChar === CharacterCodes.slash ? SyntaxKind.SingleLineCommentTrivia : SyntaxKind.MultiLineCommentTrivia; + const startPos = pos; + pos += 2; + if (nextChar === CharacterCodes.slash) { while (pos < text.length) { if (isLineBreak(text.charCodeAt(pos))) { + hasTrailingNewLine = true; break; } pos++; } - continue; } - if (text.charCodeAt(pos + 1) === CharacterCodes.asterisk) { - pos += 2; + else { while (pos < text.length) { if (text.charCodeAt(pos) === CharacterCodes.asterisk && text.charCodeAt(pos + 1) === CharacterCodes.slash) { pos += 2; @@ -559,1725 +690,1092 @@ namespace ts { } pos++; } - continue; - } - break; - - case CharacterCodes.lessThan: - case CharacterCodes.bar: - case CharacterCodes.equals: - case CharacterCodes.greaterThan: - if (isConflictMarkerTrivia(text, pos)) { - pos = scanConflictMarkerTrivia(text, pos); - continue; } - break; - - case CharacterCodes.hash: - if (pos === 0 && isShebangTrivia(text, pos)) { - pos = scanShebangTrivia(text, pos); - continue; + if (collecting) { + if (hasPendingCommentRange) { + accumulator = cb(pendingPos, pendingEnd, pendingKind, pendingHasTrailingNewLine, state, accumulator); + if (!reduce && accumulator) { + // If we are not reducing and we have a truthy result, return it. + return accumulator; + } + } + pendingPos = startPos; + pendingEnd = pos; + pendingKind = kind; + pendingHasTrailingNewLine = hasTrailingNewLine; + hasPendingCommentRange = true; } - break; - - default: - if (ch > CharacterCodes.maxAsciiCharacter && (isWhiteSpaceLike(ch))) { - pos++; - continue; - } - break; - } - return pos; - } - } - - // All conflict markers consist of the same character repeated seven times. If it is - // a <<<<<<< or >>>>>>> marker then it is also followed by a space. - const mergeConflictMarkerLength = "<<<<<<<".length; - - function isConflictMarkerTrivia(text: string, pos: number) { - Debug.assert(pos >= 0); - - // Conflict markers must be at the start of a line. - if (pos === 0 || isLineBreak(text.charCodeAt(pos - 1))) { - const ch = text.charCodeAt(pos); - - if ((pos + mergeConflictMarkerLength) < text.length) { - for (let i = 0; i < mergeConflictMarkerLength; i++) { - if (text.charCodeAt(pos + i) !== ch) { - return false; + continue; + } + break scan; + default: + if (ch > CharacterCodes.maxAsciiCharacter && (isWhiteSpaceLike(ch))) { + if (hasPendingCommentRange && isLineBreak(ch)) { + pendingHasTrailingNewLine = true; } + pos++; + continue; } - - return ch === CharacterCodes.equals || - text.charCodeAt(pos + mergeConflictMarkerLength) === CharacterCodes.space; - } + break scan; } - + } + if (hasPendingCommentRange) { + accumulator = cb(pendingPos, pendingEnd, pendingKind, pendingHasTrailingNewLine, state, accumulator); + } + return accumulator; +} +export function forEachLeadingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: CommentKind, hasTrailingNewLine: boolean) => U): U | undefined; +export function forEachLeadingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: CommentKind, hasTrailingNewLine: boolean, state: T) => U, state: T): U | undefined; +export function forEachLeadingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: CommentKind, hasTrailingNewLine: boolean, state: T) => U, state?: T): U | undefined { + return iterateCommentRanges(/*reduce*/ false, text, pos, /*trailing*/ false, cb, state); +} +export function forEachTrailingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: CommentKind, hasTrailingNewLine: boolean) => U): U | undefined; +export function forEachTrailingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: CommentKind, hasTrailingNewLine: boolean, state: T) => U, state: T): U | undefined; +export function forEachTrailingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: CommentKind, hasTrailingNewLine: boolean, state: T) => U, state?: T): U | undefined { + return iterateCommentRanges(/*reduce*/ false, text, pos, /*trailing*/ true, cb, state); +} +export function reduceEachLeadingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: CommentKind, hasTrailingNewLine: boolean, state: T, memo: U) => U, state: T, initial: U) { + return iterateCommentRanges(/*reduce*/ true, text, pos, /*trailing*/ false, cb, state, initial); +} +export function reduceEachTrailingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: CommentKind, hasTrailingNewLine: boolean, state: T, memo: U) => U, state: T, initial: U) { + return iterateCommentRanges(/*reduce*/ true, text, pos, /*trailing*/ true, cb, state, initial); +} +function appendCommentRange(pos: number, end: number, kind: CommentKind, hasTrailingNewLine: boolean, _state: any, comments: CommentRange[]) { + if (!comments) { + comments = []; + } + comments.push({ kind, pos, end, hasTrailingNewLine }); + return comments; +} +export function getLeadingCommentRanges(text: string, pos: number): CommentRange[] | undefined { + return reduceEachLeadingCommentRange(text, pos, appendCommentRange, /*state*/ undefined, /*initial*/ undefined); +} +export function getTrailingCommentRanges(text: string, pos: number): CommentRange[] | undefined { + return reduceEachTrailingCommentRange(text, pos, appendCommentRange, /*state*/ undefined, /*initial*/ undefined); +} +/** Optionally, get the shebang */ +export function getShebang(text: string): string | undefined { + const match = shebangTriviaRegex.exec(text); + if (match) { + return match[0]; + } +} +export function isIdentifierStart(ch: number, languageVersion: ScriptTarget | undefined): boolean { + return ch >= CharacterCodes.A && ch <= CharacterCodes.Z || ch >= CharacterCodes.a && ch <= CharacterCodes.z || + ch === CharacterCodes.$ || ch === CharacterCodes._ || + ch > CharacterCodes.maxAsciiCharacter && isUnicodeIdentifierStart(ch, languageVersion); +} +export function isIdentifierPart(ch: number, languageVersion: ScriptTarget | undefined): boolean { + return ch >= CharacterCodes.A && ch <= CharacterCodes.Z || ch >= CharacterCodes.a && ch <= CharacterCodes.z || + ch >= CharacterCodes._0 && ch <= CharacterCodes._9 || ch === CharacterCodes.$ || ch === CharacterCodes._ || + ch > CharacterCodes.maxAsciiCharacter && isUnicodeIdentifierPart(ch, languageVersion); +} +/* @internal */ +export function isIdentifierText(name: string, languageVersion: ScriptTarget | undefined): boolean { + let ch = codePointAt(name, 0); + if (!isIdentifierStart(ch, languageVersion)) { return false; } - - function scanConflictMarkerTrivia(text: string, pos: number, error?: (diag: DiagnosticMessage, pos?: number, len?: number) => void) { - if (error) { - error(Diagnostics.Merge_conflict_marker_encountered, pos, mergeConflictMarkerLength); + for (let i = charSize(ch); i < name.length; i += charSize(ch)) { + if (!isIdentifierPart(ch = codePointAt(name, i), languageVersion)) { + return false; } - - const ch = text.charCodeAt(pos); - const len = text.length; - - if (ch === CharacterCodes.lessThan || ch === CharacterCodes.greaterThan) { - while (pos < len && !isLineBreak(text.charCodeAt(pos))) { - pos++; - } + } + return true; +} +// Creates a scanner over a (possibly unspecified) range of a piece of text. +export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean, languageVariant = LanguageVariant.Standard, textInitial?: string, onError?: ErrorCallback, start?: number, length?: number): Scanner { + let text = textInitial!; + // Current position (end position of text of current token) + let pos: number; + // end of text + let end: number; + // Start position of whitespace before current token + let startPos: number; + // Start position of text of current token + let tokenPos: number; + let token: SyntaxKind; + let tokenValue!: string; + let tokenFlags: TokenFlags; + let inJSDocType = 0; + setText(text, start, length); + const scanner: Scanner = { + getStartPos: () => startPos, + getTextPos: () => pos, + getToken: () => token, + getTokenPos: () => tokenPos, + getTokenText: () => text.substring(tokenPos, pos), + getTokenValue: () => tokenValue, + hasUnicodeEscape: () => (tokenFlags & TokenFlags.UnicodeEscape) !== 0, + hasExtendedUnicodeEscape: () => (tokenFlags & TokenFlags.ExtendedUnicodeEscape) !== 0, + hasPrecedingLineBreak: () => (tokenFlags & TokenFlags.PrecedingLineBreak) !== 0, + isIdentifier: () => token === SyntaxKind.Identifier || token > SyntaxKind.LastReservedWord, + isReservedWord: () => token >= SyntaxKind.FirstReservedWord && token <= SyntaxKind.LastReservedWord, + isUnterminated: () => (tokenFlags & TokenFlags.Unterminated) !== 0, + getTokenFlags: () => tokenFlags, + reScanGreaterToken, + reScanSlashToken, + reScanTemplateToken, + reScanTemplateHeadOrNoSubstitutionTemplate, + scanJsxIdentifier, + scanJsxAttributeValue, + reScanJsxAttributeValue, + reScanJsxToken, + reScanLessThanToken, + reScanQuestionToken, + scanJsxToken, + scanJsDocToken, + scan, + getText, + setText, + setScriptTarget, + setLanguageVariant, + setOnError, + setTextPos, + setInJSDocType, + tryScan, + lookAhead, + scanRange, + }; + if (Debug.isDebugging) { + Object.defineProperty(scanner, "__debugShowCurrentPositionInText", { + get: () => { + const text = scanner.getText(); + return text.slice(0, scanner.getStartPos()) + "║" + text.slice(scanner.getStartPos()); + }, + }); + } + return scanner; + function error(message: DiagnosticMessage): void; + function error(message: DiagnosticMessage, errPos: number, length: number): void; + function error(message: DiagnosticMessage, errPos: number = pos, length?: number): void { + if (onError) { + const oldPos = pos; + pos = errPos; + onError(message, length || 0); + pos = oldPos; } - else { - Debug.assert(ch === CharacterCodes.bar || ch === CharacterCodes.equals); - // Consume everything from the start of a ||||||| or ======= marker to the start - // of the next ======= or >>>>>>> marker. - while (pos < len) { - const currentChar = text.charCodeAt(pos); - if ((currentChar === CharacterCodes.equals || currentChar === CharacterCodes.greaterThan) && currentChar !== ch && isConflictMarkerTrivia(text, pos)) { - break; + } + function scanNumberFragment(): string { + let start = pos; + let allowSeparator = false; + let isPreviousTokenSeparator = false; + let result = ""; + while (true) { + const ch = text.charCodeAt(pos); + if (ch === CharacterCodes._) { + tokenFlags |= TokenFlags.ContainsSeparator; + if (allowSeparator) { + allowSeparator = false; + isPreviousTokenSeparator = true; + result += text.substring(start, pos); } - + else if (isPreviousTokenSeparator) { + error(Diagnostics.Multiple_consecutive_numeric_separators_are_not_permitted, pos, 1); + } + else { + error(Diagnostics.Numeric_separators_are_not_allowed_here, pos, 1); + } + pos++; + start = pos; + continue; + } + if (isDigit(ch)) { + allowSeparator = true; + isPreviousTokenSeparator = false; pos++; + continue; } + break; } - - return pos; - } - - const shebangTriviaRegex = /^#!.*/; - - /*@internal*/ - export function isShebangTrivia(text: string, pos: number) { - // Shebangs check must only be done at the start of the file - Debug.assert(pos === 0); - return shebangTriviaRegex.test(text); - } - - /*@internal*/ - export function scanShebangTrivia(text: string, pos: number) { - const shebang = shebangTriviaRegex.exec(text)![0]; - pos = pos + shebang.length; - return pos; + if (text.charCodeAt(pos - 1) === CharacterCodes._) { + error(Diagnostics.Numeric_separators_are_not_allowed_here, pos - 1, 1); + } + return result + text.substring(start, pos); } - - /** - * Invokes a callback for each comment range following the provided position. - * - * Single-line comment ranges include the leading double-slash characters but not the ending - * line break. Multi-line comment ranges include the leading slash-asterisk and trailing - * asterisk-slash characters. - * - * @param reduce If true, accumulates the result of calling the callback in a fashion similar - * to reduceLeft. If false, iteration stops when the callback returns a truthy value. - * @param text The source text to scan. - * @param pos The position at which to start scanning. - * @param trailing If false, whitespace is skipped until the first line break and comments - * between that location and the next token are returned. If true, comments occurring - * between the given position and the next line break are returned. - * @param cb The callback to execute as each comment range is encountered. - * @param state A state value to pass to each iteration of the callback. - * @param initial An initial value to pass when accumulating results (when "reduce" is true). - * @returns If "reduce" is true, the accumulated value. If "reduce" is false, the first truthy - * return value of the callback. - */ - function iterateCommentRanges(reduce: boolean, text: string, pos: number, trailing: boolean, cb: (pos: number, end: number, kind: CommentKind, hasTrailingNewLine: boolean, state: T, memo: U | undefined) => U, state: T, initial?: U): U | undefined { - let pendingPos!: number; - let pendingEnd!: number; - let pendingKind!: CommentKind; - let pendingHasTrailingNewLine!: boolean; - let hasPendingCommentRange = false; - let collecting = trailing; - let accumulator = initial; - if (pos === 0) { - collecting = true; - const shebang = getShebang(text); - if (shebang) { - pos = shebang.length; + function scanNumber(): { + type: SyntaxKind; + value: string; + } { + const start = pos; + const mainFragment = scanNumberFragment(); + let decimalFragment: string | undefined; + let scientificFragment: string | undefined; + if (text.charCodeAt(pos) === CharacterCodes.dot) { + pos++; + decimalFragment = scanNumberFragment(); + } + let end = pos; + if (text.charCodeAt(pos) === CharacterCodes.E || text.charCodeAt(pos) === CharacterCodes.e) { + pos++; + tokenFlags |= TokenFlags.Scientific; + if (text.charCodeAt(pos) === CharacterCodes.plus || text.charCodeAt(pos) === CharacterCodes.minus) + pos++; + const preNumericPart = pos; + const finalFragment = scanNumberFragment(); + if (!finalFragment) { + error(Diagnostics.Digit_expected); + } + else { + scientificFragment = text.substring(end, preNumericPart) + finalFragment; + end = pos; } } - scan: while (pos >= 0 && pos < text.length) { - const ch = text.charCodeAt(pos); - switch (ch) { - case CharacterCodes.carriageReturn: - if (text.charCodeAt(pos + 1) === CharacterCodes.lineFeed) { - pos++; - } - // falls through - case CharacterCodes.lineFeed: - pos++; - if (trailing) { - break scan; - } - - collecting = true; - if (hasPendingCommentRange) { - pendingHasTrailingNewLine = true; - } - - continue; - case CharacterCodes.tab: - case CharacterCodes.verticalTab: - case CharacterCodes.formFeed: - case CharacterCodes.space: - pos++; - continue; - case CharacterCodes.slash: - const nextChar = text.charCodeAt(pos + 1); - let hasTrailingNewLine = false; - if (nextChar === CharacterCodes.slash || nextChar === CharacterCodes.asterisk) { - const kind = nextChar === CharacterCodes.slash ? SyntaxKind.SingleLineCommentTrivia : SyntaxKind.MultiLineCommentTrivia; - const startPos = pos; - pos += 2; - if (nextChar === CharacterCodes.slash) { - while (pos < text.length) { - if (isLineBreak(text.charCodeAt(pos))) { - hasTrailingNewLine = true; - break; - } - pos++; - } - } - else { - while (pos < text.length) { - if (text.charCodeAt(pos) === CharacterCodes.asterisk && text.charCodeAt(pos + 1) === CharacterCodes.slash) { - pos += 2; - break; - } - pos++; - } - } - - if (collecting) { - if (hasPendingCommentRange) { - accumulator = cb(pendingPos, pendingEnd, pendingKind, pendingHasTrailingNewLine, state, accumulator); - if (!reduce && accumulator) { - // If we are not reducing and we have a truthy result, return it. - return accumulator; - } - } - - pendingPos = startPos; - pendingEnd = pos; - pendingKind = kind; - pendingHasTrailingNewLine = hasTrailingNewLine; - hasPendingCommentRange = true; - } - - continue; - } - break scan; - default: - if (ch > CharacterCodes.maxAsciiCharacter && (isWhiteSpaceLike(ch))) { - if (hasPendingCommentRange && isLineBreak(ch)) { - pendingHasTrailingNewLine = true; - } - pos++; - continue; - } - break scan; + let result: string; + if (tokenFlags & TokenFlags.ContainsSeparator) { + result = mainFragment; + if (decimalFragment) { + result += "." + decimalFragment; + } + if (scientificFragment) { + result += scientificFragment; } } - - if (hasPendingCommentRange) { - accumulator = cb(pendingPos, pendingEnd, pendingKind, pendingHasTrailingNewLine, state, accumulator); + else { + result = text.substring(start, end); // No need to use all the fragments; no _ removal needed } - - return accumulator; - } - - export function forEachLeadingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: CommentKind, hasTrailingNewLine: boolean) => U): U | undefined; - export function forEachLeadingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: CommentKind, hasTrailingNewLine: boolean, state: T) => U, state: T): U | undefined; - export function forEachLeadingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: CommentKind, hasTrailingNewLine: boolean, state: T) => U, state?: T): U | undefined { - return iterateCommentRanges(/*reduce*/ false, text, pos, /*trailing*/ false, cb, state); - } - - export function forEachTrailingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: CommentKind, hasTrailingNewLine: boolean) => U): U | undefined; - export function forEachTrailingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: CommentKind, hasTrailingNewLine: boolean, state: T) => U, state: T): U | undefined; - export function forEachTrailingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: CommentKind, hasTrailingNewLine: boolean, state: T) => U, state?: T): U | undefined { - return iterateCommentRanges(/*reduce*/ false, text, pos, /*trailing*/ true, cb, state); - } - - export function reduceEachLeadingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: CommentKind, hasTrailingNewLine: boolean, state: T, memo: U) => U, state: T, initial: U) { - return iterateCommentRanges(/*reduce*/ true, text, pos, /*trailing*/ false, cb, state, initial); - } - - export function reduceEachTrailingCommentRange(text: string, pos: number, cb: (pos: number, end: number, kind: CommentKind, hasTrailingNewLine: boolean, state: T, memo: U) => U, state: T, initial: U) { - return iterateCommentRanges(/*reduce*/ true, text, pos, /*trailing*/ true, cb, state, initial); - } - - function appendCommentRange(pos: number, end: number, kind: CommentKind, hasTrailingNewLine: boolean, _state: any, comments: CommentRange[]) { - if (!comments) { - comments = []; + if (decimalFragment !== undefined || tokenFlags & TokenFlags.Scientific) { + checkForIdentifierStartAfterNumericLiteral(start, decimalFragment === undefined && !!(tokenFlags & TokenFlags.Scientific)); + return { + type: SyntaxKind.NumericLiteral, + value: "" + +result // if value is not an integer, it can be safely coerced to a number + }; } - - comments.push({ kind, pos, end, hasTrailingNewLine }); - return comments; - } - - export function getLeadingCommentRanges(text: string, pos: number): CommentRange[] | undefined { - return reduceEachLeadingCommentRange(text, pos, appendCommentRange, /*state*/ undefined, /*initial*/ undefined); - } - - export function getTrailingCommentRanges(text: string, pos: number): CommentRange[] | undefined { - return reduceEachTrailingCommentRange(text, pos, appendCommentRange, /*state*/ undefined, /*initial*/ undefined); - } - - /** Optionally, get the shebang */ - export function getShebang(text: string): string | undefined { - const match = shebangTriviaRegex.exec(text); - if (match) { - return match[0]; + else { + tokenValue = result; + const type = checkBigIntSuffix(); // if value is an integer, check whether it is a bigint + checkForIdentifierStartAfterNumericLiteral(start); + return { type, value: tokenValue }; } } - - export function isIdentifierStart(ch: number, languageVersion: ScriptTarget | undefined): boolean { - return ch >= CharacterCodes.A && ch <= CharacterCodes.Z || ch >= CharacterCodes.a && ch <= CharacterCodes.z || - ch === CharacterCodes.$ || ch === CharacterCodes._ || - ch > CharacterCodes.maxAsciiCharacter && isUnicodeIdentifierStart(ch, languageVersion); - } - - export function isIdentifierPart(ch: number, languageVersion: ScriptTarget | undefined): boolean { - return ch >= CharacterCodes.A && ch <= CharacterCodes.Z || ch >= CharacterCodes.a && ch <= CharacterCodes.z || - ch >= CharacterCodes._0 && ch <= CharacterCodes._9 || ch === CharacterCodes.$ || ch === CharacterCodes._ || - ch > CharacterCodes.maxAsciiCharacter && isUnicodeIdentifierPart(ch, languageVersion); - } - - /* @internal */ - export function isIdentifierText(name: string, languageVersion: ScriptTarget | undefined): boolean { - let ch = codePointAt(name, 0); - if (!isIdentifierStart(ch, languageVersion)) { - return false; + function checkForIdentifierStartAfterNumericLiteral(numericStart: number, isScientific?: boolean) { + if (!isIdentifierStart(codePointAt(text, pos), languageVersion)) { + return; } - - for (let i = charSize(ch); i < name.length; i += charSize(ch)) { - if (!isIdentifierPart(ch = codePointAt(name, i), languageVersion)) { - return false; + const identifierStart = pos; + const { length } = scanIdentifierParts(); + if (length === 1 && text[identifierStart] === "n") { + if (isScientific) { + error(Diagnostics.A_bigint_literal_cannot_use_exponential_notation, numericStart, identifierStart - numericStart + 1); + } + else { + error(Diagnostics.A_bigint_literal_must_be_an_integer, numericStart, identifierStart - numericStart + 1); } } - - return true; - } - - // Creates a scanner over a (possibly unspecified) range of a piece of text. - export function createScanner(languageVersion: ScriptTarget, - skipTrivia: boolean, - languageVariant = LanguageVariant.Standard, - textInitial?: string, - onError?: ErrorCallback, - start?: number, - length?: number): Scanner { - - let text = textInitial!; - - // Current position (end position of text of current token) - let pos: number; - - - // end of text - let end: number; - - // Start position of whitespace before current token - let startPos: number; - - // Start position of text of current token - let tokenPos: number; - - let token: SyntaxKind; - let tokenValue!: string; - let tokenFlags: TokenFlags; - - let inJSDocType = 0; - - setText(text, start, length); - - const scanner: Scanner = { - getStartPos: () => startPos, - getTextPos: () => pos, - getToken: () => token, - getTokenPos: () => tokenPos, - getTokenText: () => text.substring(tokenPos, pos), - getTokenValue: () => tokenValue, - hasUnicodeEscape: () => (tokenFlags & TokenFlags.UnicodeEscape) !== 0, - hasExtendedUnicodeEscape: () => (tokenFlags & TokenFlags.ExtendedUnicodeEscape) !== 0, - hasPrecedingLineBreak: () => (tokenFlags & TokenFlags.PrecedingLineBreak) !== 0, - isIdentifier: () => token === SyntaxKind.Identifier || token > SyntaxKind.LastReservedWord, - isReservedWord: () => token >= SyntaxKind.FirstReservedWord && token <= SyntaxKind.LastReservedWord, - isUnterminated: () => (tokenFlags & TokenFlags.Unterminated) !== 0, - getTokenFlags: () => tokenFlags, - reScanGreaterToken, - reScanSlashToken, - reScanTemplateToken, - reScanTemplateHeadOrNoSubstitutionTemplate, - scanJsxIdentifier, - scanJsxAttributeValue, - reScanJsxAttributeValue, - reScanJsxToken, - reScanLessThanToken, - reScanQuestionToken, - scanJsxToken, - scanJsDocToken, - scan, - getText, - setText, - setScriptTarget, - setLanguageVariant, - setOnError, - setTextPos, - setInJSDocType, - tryScan, - lookAhead, - scanRange, - }; - - if (Debug.isDebugging) { - Object.defineProperty(scanner, "__debugShowCurrentPositionInText", { - get: () => { - const text = scanner.getText(); - return text.slice(0, scanner.getStartPos()) + "║" + text.slice(scanner.getStartPos()); - }, - }); + else { + error(Diagnostics.An_identifier_or_keyword_cannot_immediately_follow_a_numeric_literal, identifierStart, length); + pos = identifierStart; } - - return scanner; - - function error(message: DiagnosticMessage): void; - function error(message: DiagnosticMessage, errPos: number, length: number): void; - function error(message: DiagnosticMessage, errPos: number = pos, length?: number): void { - if (onError) { - const oldPos = pos; - pos = errPos; - onError(message, length || 0); - pos = oldPos; - } + } + function scanOctalDigits(): number { + const start = pos; + while (isOctalDigit(text.charCodeAt(pos))) { + pos++; } - - function scanNumberFragment(): string { - let start = pos; - let allowSeparator = false; - let isPreviousTokenSeparator = false; - let result = ""; - while (true) { - const ch = text.charCodeAt(pos); - if (ch === CharacterCodes._) { - tokenFlags |= TokenFlags.ContainsSeparator; - if (allowSeparator) { - allowSeparator = false; - isPreviousTokenSeparator = true; - result += text.substring(start, pos); - } - else if (isPreviousTokenSeparator) { - error(Diagnostics.Multiple_consecutive_numeric_separators_are_not_permitted, pos, 1); - } - else { - error(Diagnostics.Numeric_separators_are_not_allowed_here, pos, 1); - } - pos++; - start = pos; - continue; + return +(text.substring(start, pos)); + } + /** + * Scans the given number of hexadecimal digits in the text, + * returning -1 if the given number is unavailable. + */ + function scanExactNumberOfHexDigits(count: number, canHaveSeparators: boolean): number { + const valueString = scanHexDigits(/*minCount*/ count, /*scanAsManyAsPossible*/ false, canHaveSeparators); + return valueString ? parseInt(valueString, 16) : -1; + } + /** + * Scans as many hexadecimal digits as are available in the text, + * returning "" if the given number of digits was unavailable. + */ + function scanMinimumNumberOfHexDigits(count: number, canHaveSeparators: boolean): string { + return scanHexDigits(/*minCount*/ count, /*scanAsManyAsPossible*/ true, canHaveSeparators); + } + function scanHexDigits(minCount: number, scanAsManyAsPossible: boolean, canHaveSeparators: boolean): string { + let valueChars: number[] = []; + let allowSeparator = false; + let isPreviousTokenSeparator = false; + while (valueChars.length < minCount || scanAsManyAsPossible) { + let ch = text.charCodeAt(pos); + if (canHaveSeparators && ch === CharacterCodes._) { + tokenFlags |= TokenFlags.ContainsSeparator; + if (allowSeparator) { + allowSeparator = false; + isPreviousTokenSeparator = true; } - if (isDigit(ch)) { - allowSeparator = true; - isPreviousTokenSeparator = false; - pos++; - continue; + else if (isPreviousTokenSeparator) { + error(Diagnostics.Multiple_consecutive_numeric_separators_are_not_permitted, pos, 1); } - break; + else { + error(Diagnostics.Numeric_separators_are_not_allowed_here, pos, 1); + } + pos++; + continue; + } + allowSeparator = canHaveSeparators; + if (ch >= CharacterCodes.A && ch <= CharacterCodes.F) { + ch += CharacterCodes.a - CharacterCodes.A; // standardize hex literals to lowercase } - if (text.charCodeAt(pos - 1) === CharacterCodes._) { - error(Diagnostics.Numeric_separators_are_not_allowed_here, pos - 1, 1); + else if (!((ch >= CharacterCodes._0 && ch <= CharacterCodes._9) || + (ch >= CharacterCodes.a && ch <= CharacterCodes.f))) { + break; } - return result + text.substring(start, pos); + valueChars.push(ch); + pos++; + isPreviousTokenSeparator = false; } - - function scanNumber(): {type: SyntaxKind, value: string} { - const start = pos; - const mainFragment = scanNumberFragment(); - let decimalFragment: string | undefined; - let scientificFragment: string | undefined; - if (text.charCodeAt(pos) === CharacterCodes.dot) { - pos++; - decimalFragment = scanNumberFragment(); + if (valueChars.length < minCount) { + valueChars = []; + } + if (text.charCodeAt(pos - 1) === CharacterCodes._) { + error(Diagnostics.Numeric_separators_are_not_allowed_here, pos - 1, 1); + } + return String.fromCharCode(...valueChars); + } + function scanString(jsxAttributeString = false): string { + const quote = text.charCodeAt(pos); + pos++; + let result = ""; + let start = pos; + while (true) { + if (pos >= end) { + result += text.substring(start, pos); + tokenFlags |= TokenFlags.Unterminated; + error(Diagnostics.Unterminated_string_literal); + break; } - let end = pos; - if (text.charCodeAt(pos) === CharacterCodes.E || text.charCodeAt(pos) === CharacterCodes.e) { + const ch = text.charCodeAt(pos); + if (ch === quote) { + result += text.substring(start, pos); pos++; - tokenFlags |= TokenFlags.Scientific; - if (text.charCodeAt(pos) === CharacterCodes.plus || text.charCodeAt(pos) === CharacterCodes.minus) pos++; - const preNumericPart = pos; - const finalFragment = scanNumberFragment(); - if (!finalFragment) { - error(Diagnostics.Digit_expected); - } - else { - scientificFragment = text.substring(end, preNumericPart) + finalFragment; - end = pos; - } - } - let result: string; - if (tokenFlags & TokenFlags.ContainsSeparator) { - result = mainFragment; - if (decimalFragment) { - result += "." + decimalFragment; - } - if (scientificFragment) { - result += scientificFragment; - } - } - else { - result = text.substring(start, end); // No need to use all the fragments; no _ removal needed + break; } - - if (decimalFragment !== undefined || tokenFlags & TokenFlags.Scientific) { - checkForIdentifierStartAfterNumericLiteral(start, decimalFragment === undefined && !!(tokenFlags & TokenFlags.Scientific)); - return { - type: SyntaxKind.NumericLiteral, - value: "" + +result // if value is not an integer, it can be safely coerced to a number - }; + if (ch === CharacterCodes.backslash && !jsxAttributeString) { + result += text.substring(start, pos); + result += scanEscapeSequence(); + start = pos; + continue; } - else { - tokenValue = result; - const type = checkBigIntSuffix(); // if value is an integer, check whether it is a bigint - checkForIdentifierStartAfterNumericLiteral(start); - return { type, value: tokenValue }; + if (isLineBreak(ch) && !jsxAttributeString) { + result += text.substring(start, pos); + tokenFlags |= TokenFlags.Unterminated; + error(Diagnostics.Unterminated_string_literal); + break; } + pos++; } - - function checkForIdentifierStartAfterNumericLiteral(numericStart: number, isScientific?: boolean) { - if (!isIdentifierStart(codePointAt(text, pos), languageVersion)) { - return; + return result; + } + /** + * Sets the current 'tokenValue' and returns a NoSubstitutionTemplateLiteral or + * a literal component of a TemplateExpression. + */ + function scanTemplateAndSetTokenValue(isTaggedTemplate: boolean): SyntaxKind { + const startedWithBacktick = text.charCodeAt(pos) === CharacterCodes.backtick; + pos++; + let start = pos; + let contents = ""; + let resultingToken: SyntaxKind; + while (true) { + if (pos >= end) { + contents += text.substring(start, pos); + tokenFlags |= TokenFlags.Unterminated; + error(Diagnostics.Unterminated_template_literal); + resultingToken = startedWithBacktick ? SyntaxKind.NoSubstitutionTemplateLiteral : SyntaxKind.TemplateTail; + break; } - - const identifierStart = pos; - const { length } = scanIdentifierParts(); - - if (length === 1 && text[identifierStart] === "n") { - if (isScientific) { - error(Diagnostics.A_bigint_literal_cannot_use_exponential_notation, numericStart, identifierStart - numericStart + 1); - } - else { - error(Diagnostics.A_bigint_literal_must_be_an_integer, numericStart, identifierStart - numericStart + 1); - } + const currChar = text.charCodeAt(pos); + // '`' + if (currChar === CharacterCodes.backtick) { + contents += text.substring(start, pos); + pos++; + resultingToken = startedWithBacktick ? SyntaxKind.NoSubstitutionTemplateLiteral : SyntaxKind.TemplateTail; + break; } - else { - error(Diagnostics.An_identifier_or_keyword_cannot_immediately_follow_a_numeric_literal, identifierStart, length); - pos = identifierStart; + // '${' + if (currChar === CharacterCodes.$ && pos + 1 < end && text.charCodeAt(pos + 1) === CharacterCodes.openBrace) { + contents += text.substring(start, pos); + pos += 2; + resultingToken = startedWithBacktick ? SyntaxKind.TemplateHead : SyntaxKind.TemplateMiddle; + break; } - } - - function scanOctalDigits(): number { - const start = pos; - while (isOctalDigit(text.charCodeAt(pos))) { + // Escape character + if (currChar === CharacterCodes.backslash) { + contents += text.substring(start, pos); + contents += scanEscapeSequence(isTaggedTemplate); + start = pos; + continue; + } + // Speculated ECMAScript 6 Spec 11.8.6.1: + // and LineTerminatorSequences are normalized to for Template Values + if (currChar === CharacterCodes.carriageReturn) { + contents += text.substring(start, pos); pos++; + if (pos < end && text.charCodeAt(pos) === CharacterCodes.lineFeed) { + pos++; + } + contents += "\n"; + start = pos; + continue; } - return +(text.substring(start, pos)); - } - - /** - * Scans the given number of hexadecimal digits in the text, - * returning -1 if the given number is unavailable. - */ - function scanExactNumberOfHexDigits(count: number, canHaveSeparators: boolean): number { - const valueString = scanHexDigits(/*minCount*/ count, /*scanAsManyAsPossible*/ false, canHaveSeparators); - return valueString ? parseInt(valueString, 16) : -1; + pos++; } - - /** - * Scans as many hexadecimal digits as are available in the text, - * returning "" if the given number of digits was unavailable. - */ - function scanMinimumNumberOfHexDigits(count: number, canHaveSeparators: boolean): string { - return scanHexDigits(/*minCount*/ count, /*scanAsManyAsPossible*/ true, canHaveSeparators); + Debug.assert(resultingToken !== undefined); + tokenValue = contents; + return resultingToken; + } + function scanEscapeSequence(isTaggedTemplate?: boolean): string { + const start = pos; + pos++; + if (pos >= end) { + error(Diagnostics.Unexpected_end_of_text); + return ""; } - - function scanHexDigits(minCount: number, scanAsManyAsPossible: boolean, canHaveSeparators: boolean): string { - let valueChars: number[] = []; - let allowSeparator = false; - let isPreviousTokenSeparator = false; - while (valueChars.length < minCount || scanAsManyAsPossible) { - let ch = text.charCodeAt(pos); - if (canHaveSeparators && ch === CharacterCodes._) { - tokenFlags |= TokenFlags.ContainsSeparator; - if (allowSeparator) { - allowSeparator = false; - isPreviousTokenSeparator = true; - } - else if (isPreviousTokenSeparator) { - error(Diagnostics.Multiple_consecutive_numeric_separators_are_not_permitted, pos, 1); - } - else { - error(Diagnostics.Numeric_separators_are_not_allowed_here, pos, 1); - } + const ch = text.charCodeAt(pos); + pos++; + switch (ch) { + case CharacterCodes._0: + // '\01' + if (isTaggedTemplate && pos < end && isDigit(text.charCodeAt(pos))) { pos++; - continue; - } - allowSeparator = canHaveSeparators; - if (ch >= CharacterCodes.A && ch <= CharacterCodes.F) { - ch += CharacterCodes.a - CharacterCodes.A; // standardize hex literals to lowercase + tokenFlags |= TokenFlags.ContainsInvalidEscape; + return text.substring(start, pos); } - else if (!((ch >= CharacterCodes._0 && ch <= CharacterCodes._9) || - (ch >= CharacterCodes.a && ch <= CharacterCodes.f) - )) { - break; + return "\0"; + case CharacterCodes.b: + return "\b"; + case CharacterCodes.t: + return "\t"; + case CharacterCodes.n: + return "\n"; + case CharacterCodes.v: + return "\v"; + case CharacterCodes.f: + return "\f"; + case CharacterCodes.r: + return "\r"; + case CharacterCodes.singleQuote: + return "\'"; + case CharacterCodes.doubleQuote: + return "\""; + case CharacterCodes.u: + if (isTaggedTemplate) { + // '\u' or '\u0' or '\u00' or '\u000' + for (let escapePos = pos; escapePos < pos + 4; escapePos++) { + if (escapePos < end && !isHexDigit(text.charCodeAt(escapePos)) && text.charCodeAt(escapePos) !== CharacterCodes.openBrace) { + pos = escapePos; + tokenFlags |= TokenFlags.ContainsInvalidEscape; + return text.substring(start, pos); + } + } } - valueChars.push(ch); - pos++; - isPreviousTokenSeparator = false; - } - if (valueChars.length < minCount) { - valueChars = []; - } - if (text.charCodeAt(pos - 1) === CharacterCodes._) { - error(Diagnostics.Numeric_separators_are_not_allowed_here, pos - 1, 1); - } - return String.fromCharCode(...valueChars); - } - - function scanString(jsxAttributeString = false): string { - const quote = text.charCodeAt(pos); - pos++; - let result = ""; - let start = pos; - while (true) { - if (pos >= end) { - result += text.substring(start, pos); - tokenFlags |= TokenFlags.Unterminated; - error(Diagnostics.Unterminated_string_literal); - break; - } - const ch = text.charCodeAt(pos); - if (ch === quote) { - result += text.substring(start, pos); - pos++; - break; - } - if (ch === CharacterCodes.backslash && !jsxAttributeString) { - result += text.substring(start, pos); - result += scanEscapeSequence(); - start = pos; - continue; - } - if (isLineBreak(ch) && !jsxAttributeString) { - result += text.substring(start, pos); - tokenFlags |= TokenFlags.Unterminated; - error(Diagnostics.Unterminated_string_literal); - break; - } - pos++; - } - return result; - } - - /** - * Sets the current 'tokenValue' and returns a NoSubstitutionTemplateLiteral or - * a literal component of a TemplateExpression. - */ - function scanTemplateAndSetTokenValue(isTaggedTemplate: boolean): SyntaxKind { - const startedWithBacktick = text.charCodeAt(pos) === CharacterCodes.backtick; - - pos++; - let start = pos; - let contents = ""; - let resultingToken: SyntaxKind; - - while (true) { - if (pos >= end) { - contents += text.substring(start, pos); - tokenFlags |= TokenFlags.Unterminated; - error(Diagnostics.Unterminated_template_literal); - resultingToken = startedWithBacktick ? SyntaxKind.NoSubstitutionTemplateLiteral : SyntaxKind.TemplateTail; - break; - } - - const currChar = text.charCodeAt(pos); - - // '`' - if (currChar === CharacterCodes.backtick) { - contents += text.substring(start, pos); - pos++; - resultingToken = startedWithBacktick ? SyntaxKind.NoSubstitutionTemplateLiteral : SyntaxKind.TemplateTail; - break; - } - - // '${' - if (currChar === CharacterCodes.$ && pos + 1 < end && text.charCodeAt(pos + 1) === CharacterCodes.openBrace) { - contents += text.substring(start, pos); - pos += 2; - resultingToken = startedWithBacktick ? SyntaxKind.TemplateHead : SyntaxKind.TemplateMiddle; - break; - } - - // Escape character - if (currChar === CharacterCodes.backslash) { - contents += text.substring(start, pos); - contents += scanEscapeSequence(isTaggedTemplate); - start = pos; - continue; - } - - // Speculated ECMAScript 6 Spec 11.8.6.1: - // and LineTerminatorSequences are normalized to for Template Values - if (currChar === CharacterCodes.carriageReturn) { - contents += text.substring(start, pos); + // '\u{DDDDDDDD}' + if (pos < end && text.charCodeAt(pos) === CharacterCodes.openBrace) { pos++; - - if (pos < end && text.charCodeAt(pos) === CharacterCodes.lineFeed) { - pos++; - } - - contents += "\n"; - start = pos; - continue; - } - - pos++; - } - - Debug.assert(resultingToken !== undefined); - - tokenValue = contents; - return resultingToken; - } - - function scanEscapeSequence(isTaggedTemplate?: boolean): string { - const start = pos; - pos++; - if (pos >= end) { - error(Diagnostics.Unexpected_end_of_text); - return ""; - } - const ch = text.charCodeAt(pos); - pos++; - switch (ch) { - case CharacterCodes._0: - // '\01' - if (isTaggedTemplate && pos < end && isDigit(text.charCodeAt(pos))) { - pos++; + // '\u{' + if (isTaggedTemplate && !isHexDigit(text.charCodeAt(pos))) { tokenFlags |= TokenFlags.ContainsInvalidEscape; return text.substring(start, pos); } - return "\0"; - case CharacterCodes.b: - return "\b"; - case CharacterCodes.t: - return "\t"; - case CharacterCodes.n: - return "\n"; - case CharacterCodes.v: - return "\v"; - case CharacterCodes.f: - return "\f"; - case CharacterCodes.r: - return "\r"; - case CharacterCodes.singleQuote: - return "\'"; - case CharacterCodes.doubleQuote: - return "\""; - case CharacterCodes.u: if (isTaggedTemplate) { - // '\u' or '\u0' or '\u00' or '\u000' - for (let escapePos = pos; escapePos < pos + 4; escapePos++) { - if (escapePos < end && !isHexDigit(text.charCodeAt(escapePos)) && text.charCodeAt(escapePos) !== CharacterCodes.openBrace) { - pos = escapePos; - tokenFlags |= TokenFlags.ContainsInvalidEscape; - return text.substring(start, pos); - } - } - } - // '\u{DDDDDDDD}' - if (pos < end && text.charCodeAt(pos) === CharacterCodes.openBrace) { - pos++; - - // '\u{' - if (isTaggedTemplate && !isHexDigit(text.charCodeAt(pos))) { + const savePos = pos; + const escapedValueString = scanMinimumNumberOfHexDigits(1, /*canHaveSeparators*/ false); + const escapedValue = escapedValueString ? parseInt(escapedValueString, 16) : -1; + // '\u{Not Code Point' or '\u{CodePoint' + if (!isCodePoint(escapedValue) || text.charCodeAt(pos) !== CharacterCodes.closeBrace) { tokenFlags |= TokenFlags.ContainsInvalidEscape; return text.substring(start, pos); } - - if (isTaggedTemplate) { - const savePos = pos; - const escapedValueString = scanMinimumNumberOfHexDigits(1, /*canHaveSeparators*/ false); - const escapedValue = escapedValueString ? parseInt(escapedValueString, 16) : -1; - - // '\u{Not Code Point' or '\u{CodePoint' - if (!isCodePoint(escapedValue) || text.charCodeAt(pos) !== CharacterCodes.closeBrace) { - tokenFlags |= TokenFlags.ContainsInvalidEscape; - return text.substring(start, pos); - } - else { - pos = savePos; - } + else { + pos = savePos; } - tokenFlags |= TokenFlags.ExtendedUnicodeEscape; - return scanExtendedUnicodeEscape(); } - - tokenFlags |= TokenFlags.UnicodeEscape; - // '\uDDDD' - return scanHexadecimalEscape(/*numDigits*/ 4); - - case CharacterCodes.x: - if (isTaggedTemplate) { - if (!isHexDigit(text.charCodeAt(pos))) { - tokenFlags |= TokenFlags.ContainsInvalidEscape; - return text.substring(start, pos); - } - else if (!isHexDigit(text.charCodeAt(pos + 1))) { - pos++; - tokenFlags |= TokenFlags.ContainsInvalidEscape; - return text.substring(start, pos); - } + tokenFlags |= TokenFlags.ExtendedUnicodeEscape; + return scanExtendedUnicodeEscape(); + } + tokenFlags |= TokenFlags.UnicodeEscape; + // '\uDDDD' + return scanHexadecimalEscape(/*numDigits*/ 4); + case CharacterCodes.x: + if (isTaggedTemplate) { + if (!isHexDigit(text.charCodeAt(pos))) { + tokenFlags |= TokenFlags.ContainsInvalidEscape; + return text.substring(start, pos); } - // '\xDD' - return scanHexadecimalEscape(/*numDigits*/ 2); - - // when encountering a LineContinuation (i.e. a backslash and a line terminator sequence), - // the line terminator is interpreted to be "the empty code unit sequence". - case CharacterCodes.carriageReturn: - if (pos < end && text.charCodeAt(pos) === CharacterCodes.lineFeed) { + else if (!isHexDigit(text.charCodeAt(pos + 1))) { pos++; + tokenFlags |= TokenFlags.ContainsInvalidEscape; + return text.substring(start, pos); } - // falls through - case CharacterCodes.lineFeed: - case CharacterCodes.lineSeparator: - case CharacterCodes.paragraphSeparator: - return ""; - default: - return String.fromCharCode(ch); - } - } - - function scanHexadecimalEscape(numDigits: number): string { - const escapedValue = scanExactNumberOfHexDigits(numDigits, /*canHaveSeparators*/ false); - - if (escapedValue >= 0) { - return String.fromCharCode(escapedValue); - } - else { - error(Diagnostics.Hexadecimal_digit_expected); + } + // '\xDD' + return scanHexadecimalEscape(/*numDigits*/ 2); + // when encountering a LineContinuation (i.e. a backslash and a line terminator sequence), + // the line terminator is interpreted to be "the empty code unit sequence". + case CharacterCodes.carriageReturn: + if (pos < end && text.charCodeAt(pos) === CharacterCodes.lineFeed) { + pos++; + } + // falls through + case CharacterCodes.lineFeed: + case CharacterCodes.lineSeparator: + case CharacterCodes.paragraphSeparator: return ""; - } + default: + return String.fromCharCode(ch); } - - function scanExtendedUnicodeEscape(): string { - const escapedValueString = scanMinimumNumberOfHexDigits(1, /*canHaveSeparators*/ false); - const escapedValue = escapedValueString ? parseInt(escapedValueString, 16) : -1; - let isInvalidExtendedEscape = false; - - // Validate the value of the digit - if (escapedValue < 0) { - error(Diagnostics.Hexadecimal_digit_expected); - isInvalidExtendedEscape = true; - } - else if (escapedValue > 0x10FFFF) { - error(Diagnostics.An_extended_Unicode_escape_value_must_be_between_0x0_and_0x10FFFF_inclusive); - isInvalidExtendedEscape = true; - } - - if (pos >= end) { - error(Diagnostics.Unexpected_end_of_text); - isInvalidExtendedEscape = true; - } - else if (text.charCodeAt(pos) === CharacterCodes.closeBrace) { - // Only swallow the following character up if it's a '}'. - pos++; - } - else { - error(Diagnostics.Unterminated_Unicode_escape_sequence); - isInvalidExtendedEscape = true; - } - - if (isInvalidExtendedEscape) { - return ""; - } - - return utf16EncodeAsString(escapedValue); + } + function scanHexadecimalEscape(numDigits: number): string { + const escapedValue = scanExactNumberOfHexDigits(numDigits, /*canHaveSeparators*/ false); + if (escapedValue >= 0) { + return String.fromCharCode(escapedValue); } - - // Current character is known to be a backslash. Check for Unicode escape of the form '\uXXXX' - // and return code point value if valid Unicode escape is found. Otherwise return -1. - function peekUnicodeEscape(): number { - if (pos + 5 < end && text.charCodeAt(pos + 1) === CharacterCodes.u) { - const start = pos; - pos += 2; - const value = scanExactNumberOfHexDigits(4, /*canHaveSeparators*/ false); - pos = start; - return value; - } - return -1; + else { + error(Diagnostics.Hexadecimal_digit_expected); + return ""; } - - - function peekExtendedUnicodeEscape(): number { - if (languageVersion >= ScriptTarget.ES2015 && codePointAt(text, pos + 1) === CharacterCodes.u && codePointAt(text, pos + 2) === CharacterCodes.openBrace) { - const start = pos; - pos += 3; - const escapedValueString = scanMinimumNumberOfHexDigits(1, /*canHaveSeparators*/ false); - const escapedValue = escapedValueString ? parseInt(escapedValueString, 16) : -1; - pos = start; - return escapedValue; - } - return -1; + } + function scanExtendedUnicodeEscape(): string { + const escapedValueString = scanMinimumNumberOfHexDigits(1, /*canHaveSeparators*/ false); + const escapedValue = escapedValueString ? parseInt(escapedValueString, 16) : -1; + let isInvalidExtendedEscape = false; + // Validate the value of the digit + if (escapedValue < 0) { + error(Diagnostics.Hexadecimal_digit_expected); + isInvalidExtendedEscape = true; + } + else if (escapedValue > 0x10FFFF) { + error(Diagnostics.An_extended_Unicode_escape_value_must_be_between_0x0_and_0x10FFFF_inclusive); + isInvalidExtendedEscape = true; + } + if (pos >= end) { + error(Diagnostics.Unexpected_end_of_text); + isInvalidExtendedEscape = true; + } + else if (text.charCodeAt(pos) === CharacterCodes.closeBrace) { + // Only swallow the following character up if it's a '}'. + pos++; } - - function scanIdentifierParts(): string { - let result = ""; - let start = pos; - while (pos < end) { - let ch = codePointAt(text, pos); - if (isIdentifierPart(ch, languageVersion)) { - pos += charSize(ch); - } - else if (ch === CharacterCodes.backslash) { - ch = peekExtendedUnicodeEscape(); - if (ch >= 0 && isIdentifierPart(ch, languageVersion)) { - pos += 3; - tokenFlags |= TokenFlags.ExtendedUnicodeEscape; - result += scanExtendedUnicodeEscape(); - start = pos; - continue; - } - ch = peekUnicodeEscape(); - if (!(ch >= 0 && isIdentifierPart(ch, languageVersion))) { - break; - } - tokenFlags |= TokenFlags.UnicodeEscape; - result += text.substring(start, pos); - result += utf16EncodeAsString(ch); - // Valid Unicode escape is always six characters - pos += 6; + else { + error(Diagnostics.Unterminated_Unicode_escape_sequence); + isInvalidExtendedEscape = true; + } + if (isInvalidExtendedEscape) { + return ""; + } + return utf16EncodeAsString(escapedValue); + } + // Current character is known to be a backslash. Check for Unicode escape of the form '\uXXXX' + // and return code point value if valid Unicode escape is found. Otherwise return -1. + function peekUnicodeEscape(): number { + if (pos + 5 < end && text.charCodeAt(pos + 1) === CharacterCodes.u) { + const start = pos; + pos += 2; + const value = scanExactNumberOfHexDigits(4, /*canHaveSeparators*/ false); + pos = start; + return value; + } + return -1; + } + function peekExtendedUnicodeEscape(): number { + if (languageVersion >= ScriptTarget.ES2015 && codePointAt(text, pos + 1) === CharacterCodes.u && codePointAt(text, pos + 2) === CharacterCodes.openBrace) { + const start = pos; + pos += 3; + const escapedValueString = scanMinimumNumberOfHexDigits(1, /*canHaveSeparators*/ false); + const escapedValue = escapedValueString ? parseInt(escapedValueString, 16) : -1; + pos = start; + return escapedValue; + } + return -1; + } + function scanIdentifierParts(): string { + let result = ""; + let start = pos; + while (pos < end) { + let ch = codePointAt(text, pos); + if (isIdentifierPart(ch, languageVersion)) { + pos += charSize(ch); + } + else if (ch === CharacterCodes.backslash) { + ch = peekExtendedUnicodeEscape(); + if (ch >= 0 && isIdentifierPart(ch, languageVersion)) { + pos += 3; + tokenFlags |= TokenFlags.ExtendedUnicodeEscape; + result += scanExtendedUnicodeEscape(); start = pos; + continue; } - else { + ch = peekUnicodeEscape(); + if (!(ch >= 0 && isIdentifierPart(ch, languageVersion))) { break; } + tokenFlags |= TokenFlags.UnicodeEscape; + result += text.substring(start, pos); + result += utf16EncodeAsString(ch); + // Valid Unicode escape is always six characters + pos += 6; + start = pos; + } + else { + break; } - result += text.substring(start, pos); - return result; } - - function getIdentifierToken(): SyntaxKind.Identifier | KeywordSyntaxKind { - // Reserved words are between 2 and 11 characters long and start with a lowercase letter - const len = tokenValue.length; - if (len >= 2 && len <= 11) { - const ch = tokenValue.charCodeAt(0); - if (ch >= CharacterCodes.a && ch <= CharacterCodes.z) { - const keyword = textToKeyword.get(tokenValue); - if (keyword !== undefined) { - return token = keyword; - } + result += text.substring(start, pos); + return result; + } + function getIdentifierToken(): SyntaxKind.Identifier | KeywordSyntaxKind { + // Reserved words are between 2 and 11 characters long and start with a lowercase letter + const len = tokenValue.length; + if (len >= 2 && len <= 11) { + const ch = tokenValue.charCodeAt(0); + if (ch >= CharacterCodes.a && ch <= CharacterCodes.z) { + const keyword = textToKeyword.get(tokenValue); + if (keyword !== undefined) { + return token = keyword; } } - return token = SyntaxKind.Identifier; } - - function scanBinaryOrOctalDigits(base: 2 | 8): string { - let value = ""; - // For counting number of digits; Valid binaryIntegerLiteral must have at least one binary digit following B or b. - // Similarly valid octalIntegerLiteral must have at least one octal digit following o or O. - let separatorAllowed = false; - let isPreviousTokenSeparator = false; - while (true) { - const ch = text.charCodeAt(pos); - // Numeric separators are allowed anywhere within a numeric literal, except not at the beginning, or following another separator - if (ch === CharacterCodes._) { - tokenFlags |= TokenFlags.ContainsSeparator; - if (separatorAllowed) { - separatorAllowed = false; - isPreviousTokenSeparator = true; - } - else if (isPreviousTokenSeparator) { - error(Diagnostics.Multiple_consecutive_numeric_separators_are_not_permitted, pos, 1); - } - else { - error(Diagnostics.Numeric_separators_are_not_allowed_here, pos, 1); - } - pos++; - continue; + return token = SyntaxKind.Identifier; + } + function scanBinaryOrOctalDigits(base: 2 | 8): string { + let value = ""; + // For counting number of digits; Valid binaryIntegerLiteral must have at least one binary digit following B or b. + // Similarly valid octalIntegerLiteral must have at least one octal digit following o or O. + let separatorAllowed = false; + let isPreviousTokenSeparator = false; + while (true) { + const ch = text.charCodeAt(pos); + // Numeric separators are allowed anywhere within a numeric literal, except not at the beginning, or following another separator + if (ch === CharacterCodes._) { + tokenFlags |= TokenFlags.ContainsSeparator; + if (separatorAllowed) { + separatorAllowed = false; + isPreviousTokenSeparator = true; } - separatorAllowed = true; - if (!isDigit(ch) || ch - CharacterCodes._0 >= base) { - break; + else if (isPreviousTokenSeparator) { + error(Diagnostics.Multiple_consecutive_numeric_separators_are_not_permitted, pos, 1); + } + else { + error(Diagnostics.Numeric_separators_are_not_allowed_here, pos, 1); } - value += text[pos]; pos++; - isPreviousTokenSeparator = false; + continue; } - if (text.charCodeAt(pos - 1) === CharacterCodes._) { - // Literal ends with underscore - not allowed - error(Diagnostics.Numeric_separators_are_not_allowed_here, pos - 1, 1); + separatorAllowed = true; + if (!isDigit(ch) || ch - CharacterCodes._0 >= base) { + break; } - return value; + value += text[pos]; + pos++; + isPreviousTokenSeparator = false; } - - function checkBigIntSuffix(): SyntaxKind { - if (text.charCodeAt(pos) === CharacterCodes.n) { - tokenValue += "n"; - // Use base 10 instead of base 2 or base 8 for shorter literals - if (tokenFlags & TokenFlags.BinaryOrOctalSpecifier) { - tokenValue = parsePseudoBigInt(tokenValue) + "n"; - } - pos++; - return SyntaxKind.BigIntLiteral; - } - else { // not a bigint, so can convert to number in simplified form - // Number() may not support 0b or 0o, so use parseInt() instead - const numericValue = tokenFlags & TokenFlags.BinarySpecifier - ? parseInt(tokenValue.slice(2), 2) // skip "0b" - : tokenFlags & TokenFlags.OctalSpecifier - ? parseInt(tokenValue.slice(2), 8) // skip "0o" - : +tokenValue; - tokenValue = "" + numericValue; - return SyntaxKind.NumericLiteral; + if (text.charCodeAt(pos - 1) === CharacterCodes._) { + // Literal ends with underscore - not allowed + error(Diagnostics.Numeric_separators_are_not_allowed_here, pos - 1, 1); + } + return value; + } + function checkBigIntSuffix(): SyntaxKind { + if (text.charCodeAt(pos) === CharacterCodes.n) { + tokenValue += "n"; + // Use base 10 instead of base 2 or base 8 for shorter literals + if (tokenFlags & TokenFlags.BinaryOrOctalSpecifier) { + tokenValue = parsePseudoBigInt(tokenValue) + "n"; } + pos++; + return SyntaxKind.BigIntLiteral; + } + else { // not a bigint, so can convert to number in simplified form + // Number() may not support 0b or 0o, so use parseInt() instead + const numericValue = tokenFlags & TokenFlags.BinarySpecifier + ? parseInt(tokenValue.slice(2), 2) // skip "0b" + : tokenFlags & TokenFlags.OctalSpecifier + ? parseInt(tokenValue.slice(2), 8) // skip "0o" + : +tokenValue; + tokenValue = "" + numericValue; + return SyntaxKind.NumericLiteral; } - - function scan(): SyntaxKind { - startPos = pos; - tokenFlags = TokenFlags.None; - let asteriskSeen = false; - while (true) { - tokenPos = pos; - if (pos >= end) { - return token = SyntaxKind.EndOfFileToken; + } + function scan(): SyntaxKind { + startPos = pos; + tokenFlags = TokenFlags.None; + let asteriskSeen = false; + while (true) { + tokenPos = pos; + if (pos >= end) { + return token = SyntaxKind.EndOfFileToken; + } + let ch = codePointAt(text, pos); + // Special handling for shebang + if (ch === CharacterCodes.hash && pos === 0 && isShebangTrivia(text, pos)) { + pos = scanShebangTrivia(text, pos); + if (skipTrivia) { + continue; } - let ch = codePointAt(text, pos); - - // Special handling for shebang - if (ch === CharacterCodes.hash && pos === 0 && isShebangTrivia(text, pos)) { - pos = scanShebangTrivia(text, pos); + else { + return token = SyntaxKind.ShebangTrivia; + } + } + switch (ch) { + case CharacterCodes.lineFeed: + case CharacterCodes.carriageReturn: + tokenFlags |= TokenFlags.PrecedingLineBreak; if (skipTrivia) { + pos++; continue; } else { - return token = SyntaxKind.ShebangTrivia; - } - } - - switch (ch) { - case CharacterCodes.lineFeed: - case CharacterCodes.carriageReturn: - tokenFlags |= TokenFlags.PrecedingLineBreak; - if (skipTrivia) { - pos++; - continue; + if (ch === CharacterCodes.carriageReturn && pos + 1 < end && text.charCodeAt(pos + 1) === CharacterCodes.lineFeed) { + // consume both CR and LF + pos += 2; } else { - if (ch === CharacterCodes.carriageReturn && pos + 1 < end && text.charCodeAt(pos + 1) === CharacterCodes.lineFeed) { - // consume both CR and LF - pos += 2; - } - else { - pos++; - } - return token = SyntaxKind.NewLineTrivia; - } - case CharacterCodes.tab: - case CharacterCodes.verticalTab: - case CharacterCodes.formFeed: - case CharacterCodes.space: - case CharacterCodes.nonBreakingSpace: - case CharacterCodes.ogham: - case CharacterCodes.enQuad: - case CharacterCodes.emQuad: - case CharacterCodes.enSpace: - case CharacterCodes.emSpace: - case CharacterCodes.threePerEmSpace: - case CharacterCodes.fourPerEmSpace: - case CharacterCodes.sixPerEmSpace: - case CharacterCodes.figureSpace: - case CharacterCodes.punctuationSpace: - case CharacterCodes.thinSpace: - case CharacterCodes.hairSpace: - case CharacterCodes.zeroWidthSpace: - case CharacterCodes.narrowNoBreakSpace: - case CharacterCodes.mathematicalSpace: - case CharacterCodes.ideographicSpace: - case CharacterCodes.byteOrderMark: - if (skipTrivia) { pos++; - continue; - } - else { - while (pos < end && isWhiteSpaceSingleLine(text.charCodeAt(pos))) { - pos++; - } - return token = SyntaxKind.WhitespaceTrivia; - } - case CharacterCodes.exclamation: - if (text.charCodeAt(pos + 1) === CharacterCodes.equals) { - if (text.charCodeAt(pos + 2) === CharacterCodes.equals) { - return pos += 3, token = SyntaxKind.ExclamationEqualsEqualsToken; - } - return pos += 2, token = SyntaxKind.ExclamationEqualsToken; } + return token = SyntaxKind.NewLineTrivia; + } + case CharacterCodes.tab: + case CharacterCodes.verticalTab: + case CharacterCodes.formFeed: + case CharacterCodes.space: + case CharacterCodes.nonBreakingSpace: + case CharacterCodes.ogham: + case CharacterCodes.enQuad: + case CharacterCodes.emQuad: + case CharacterCodes.enSpace: + case CharacterCodes.emSpace: + case CharacterCodes.threePerEmSpace: + case CharacterCodes.fourPerEmSpace: + case CharacterCodes.sixPerEmSpace: + case CharacterCodes.figureSpace: + case CharacterCodes.punctuationSpace: + case CharacterCodes.thinSpace: + case CharacterCodes.hairSpace: + case CharacterCodes.zeroWidthSpace: + case CharacterCodes.narrowNoBreakSpace: + case CharacterCodes.mathematicalSpace: + case CharacterCodes.ideographicSpace: + case CharacterCodes.byteOrderMark: + if (skipTrivia) { pos++; - return token = SyntaxKind.ExclamationToken; - case CharacterCodes.doubleQuote: - case CharacterCodes.singleQuote: - tokenValue = scanString(); - return token = SyntaxKind.StringLiteral; - case CharacterCodes.backtick: - return token = scanTemplateAndSetTokenValue(/* isTaggedTemplate */ false); - case CharacterCodes.percent: - if (text.charCodeAt(pos + 1) === CharacterCodes.equals) { - return pos += 2, token = SyntaxKind.PercentEqualsToken; + continue; + } + else { + while (pos < end && isWhiteSpaceSingleLine(text.charCodeAt(pos))) { + pos++; } - pos++; - return token = SyntaxKind.PercentToken; - case CharacterCodes.ampersand: - if (text.charCodeAt(pos + 1) === CharacterCodes.ampersand) { - return pos += 2, token = SyntaxKind.AmpersandAmpersandToken; - } - if (text.charCodeAt(pos + 1) === CharacterCodes.equals) { - return pos += 2, token = SyntaxKind.AmpersandEqualsToken; + return token = SyntaxKind.WhitespaceTrivia; + } + case CharacterCodes.exclamation: + if (text.charCodeAt(pos + 1) === CharacterCodes.equals) { + if (text.charCodeAt(pos + 2) === CharacterCodes.equals) { + return pos += 3, token = SyntaxKind.ExclamationEqualsEqualsToken; } - pos++; - return token = SyntaxKind.AmpersandToken; - case CharacterCodes.openParen: - pos++; - return token = SyntaxKind.OpenParenToken; - case CharacterCodes.closeParen: - pos++; - return token = SyntaxKind.CloseParenToken; - case CharacterCodes.asterisk: - if (text.charCodeAt(pos + 1) === CharacterCodes.equals) { - return pos += 2, token = SyntaxKind.AsteriskEqualsToken; + return pos += 2, token = SyntaxKind.ExclamationEqualsToken; + } + pos++; + return token = SyntaxKind.ExclamationToken; + case CharacterCodes.doubleQuote: + case CharacterCodes.singleQuote: + tokenValue = scanString(); + return token = SyntaxKind.StringLiteral; + case CharacterCodes.backtick: + return token = scanTemplateAndSetTokenValue(/* isTaggedTemplate */ false); + case CharacterCodes.percent: + if (text.charCodeAt(pos + 1) === CharacterCodes.equals) { + return pos += 2, token = SyntaxKind.PercentEqualsToken; + } + pos++; + return token = SyntaxKind.PercentToken; + case CharacterCodes.ampersand: + if (text.charCodeAt(pos + 1) === CharacterCodes.ampersand) { + return pos += 2, token = SyntaxKind.AmpersandAmpersandToken; + } + if (text.charCodeAt(pos + 1) === CharacterCodes.equals) { + return pos += 2, token = SyntaxKind.AmpersandEqualsToken; + } + pos++; + return token = SyntaxKind.AmpersandToken; + case CharacterCodes.openParen: + pos++; + return token = SyntaxKind.OpenParenToken; + case CharacterCodes.closeParen: + pos++; + return token = SyntaxKind.CloseParenToken; + case CharacterCodes.asterisk: + if (text.charCodeAt(pos + 1) === CharacterCodes.equals) { + return pos += 2, token = SyntaxKind.AsteriskEqualsToken; + } + if (text.charCodeAt(pos + 1) === CharacterCodes.asterisk) { + if (text.charCodeAt(pos + 2) === CharacterCodes.equals) { + return pos += 3, token = SyntaxKind.AsteriskAsteriskEqualsToken; } - if (text.charCodeAt(pos + 1) === CharacterCodes.asterisk) { - if (text.charCodeAt(pos + 2) === CharacterCodes.equals) { - return pos += 3, token = SyntaxKind.AsteriskAsteriskEqualsToken; + return pos += 2, token = SyntaxKind.AsteriskAsteriskToken; + } + pos++; + if (inJSDocType && !asteriskSeen && (tokenFlags & TokenFlags.PrecedingLineBreak)) { + // decoration at the start of a JSDoc comment line + asteriskSeen = true; + continue; + } + return token = SyntaxKind.AsteriskToken; + case CharacterCodes.plus: + if (text.charCodeAt(pos + 1) === CharacterCodes.plus) { + return pos += 2, token = SyntaxKind.PlusPlusToken; + } + if (text.charCodeAt(pos + 1) === CharacterCodes.equals) { + return pos += 2, token = SyntaxKind.PlusEqualsToken; + } + pos++; + return token = SyntaxKind.PlusToken; + case CharacterCodes.comma: + pos++; + return token = SyntaxKind.CommaToken; + case CharacterCodes.minus: + if (text.charCodeAt(pos + 1) === CharacterCodes.minus) { + return pos += 2, token = SyntaxKind.MinusMinusToken; + } + if (text.charCodeAt(pos + 1) === CharacterCodes.equals) { + return pos += 2, token = SyntaxKind.MinusEqualsToken; + } + pos++; + return token = SyntaxKind.MinusToken; + case CharacterCodes.dot: + if (isDigit(text.charCodeAt(pos + 1))) { + tokenValue = scanNumber().value; + return token = SyntaxKind.NumericLiteral; + } + if (text.charCodeAt(pos + 1) === CharacterCodes.dot && text.charCodeAt(pos + 2) === CharacterCodes.dot) { + return pos += 3, token = SyntaxKind.DotDotDotToken; + } + pos++; + return token = SyntaxKind.DotToken; + case CharacterCodes.slash: + // Single-line comment + if (text.charCodeAt(pos + 1) === CharacterCodes.slash) { + pos += 2; + while (pos < end) { + if (isLineBreak(text.charCodeAt(pos))) { + break; } - return pos += 2, token = SyntaxKind.AsteriskAsteriskToken; + pos++; } - pos++; - if (inJSDocType && !asteriskSeen && (tokenFlags & TokenFlags.PrecedingLineBreak)) { - // decoration at the start of a JSDoc comment line - asteriskSeen = true; + if (skipTrivia) { continue; } - return token = SyntaxKind.AsteriskToken; - case CharacterCodes.plus: - if (text.charCodeAt(pos + 1) === CharacterCodes.plus) { - return pos += 2, token = SyntaxKind.PlusPlusToken; - } - if (text.charCodeAt(pos + 1) === CharacterCodes.equals) { - return pos += 2, token = SyntaxKind.PlusEqualsToken; - } - pos++; - return token = SyntaxKind.PlusToken; - case CharacterCodes.comma: - pos++; - return token = SyntaxKind.CommaToken; - case CharacterCodes.minus: - if (text.charCodeAt(pos + 1) === CharacterCodes.minus) { - return pos += 2, token = SyntaxKind.MinusMinusToken; - } - if (text.charCodeAt(pos + 1) === CharacterCodes.equals) { - return pos += 2, token = SyntaxKind.MinusEqualsToken; - } - pos++; - return token = SyntaxKind.MinusToken; - case CharacterCodes.dot: - if (isDigit(text.charCodeAt(pos + 1))) { - tokenValue = scanNumber().value; - return token = SyntaxKind.NumericLiteral; - } - if (text.charCodeAt(pos + 1) === CharacterCodes.dot && text.charCodeAt(pos + 2) === CharacterCodes.dot) { - return pos += 3, token = SyntaxKind.DotDotDotToken; - } - pos++; - return token = SyntaxKind.DotToken; - case CharacterCodes.slash: - // Single-line comment - if (text.charCodeAt(pos + 1) === CharacterCodes.slash) { - pos += 2; - - while (pos < end) { - if (isLineBreak(text.charCodeAt(pos))) { - break; - } - pos++; - - } - - if (skipTrivia) { - continue; - } - else { - return token = SyntaxKind.SingleLineCommentTrivia; - } - } - // Multi-line comment - if (text.charCodeAt(pos + 1) === CharacterCodes.asterisk) { - pos += 2; - if (text.charCodeAt(pos) === CharacterCodes.asterisk && text.charCodeAt(pos + 1) !== CharacterCodes.slash) { - tokenFlags |= TokenFlags.PrecedingJSDocComment; - } - - let commentClosed = false; - while (pos < end) { - const ch = text.charCodeAt(pos); - - if (ch === CharacterCodes.asterisk && text.charCodeAt(pos + 1) === CharacterCodes.slash) { - pos += 2; - commentClosed = true; - break; - } - - if (isLineBreak(ch)) { - tokenFlags |= TokenFlags.PrecedingLineBreak; - } - pos++; - } - - if (!commentClosed) { - error(Diagnostics.Asterisk_Slash_expected); - } - - if (skipTrivia) { - continue; - } - else { - if (!commentClosed) { - tokenFlags |= TokenFlags.Unterminated; - } - return token = SyntaxKind.MultiLineCommentTrivia; - } - } - - if (text.charCodeAt(pos + 1) === CharacterCodes.equals) { - return pos += 2, token = SyntaxKind.SlashEqualsToken; - } - - pos++; - return token = SyntaxKind.SlashToken; - - case CharacterCodes._0: - if (pos + 2 < end && (text.charCodeAt(pos + 1) === CharacterCodes.X || text.charCodeAt(pos + 1) === CharacterCodes.x)) { - pos += 2; - tokenValue = scanMinimumNumberOfHexDigits(1, /*canHaveSeparators*/ true); - if (!tokenValue) { - error(Diagnostics.Hexadecimal_digit_expected); - tokenValue = "0"; - } - tokenValue = "0x" + tokenValue; - tokenFlags |= TokenFlags.HexSpecifier; - return token = checkBigIntSuffix(); - } - else if (pos + 2 < end && (text.charCodeAt(pos + 1) === CharacterCodes.B || text.charCodeAt(pos + 1) === CharacterCodes.b)) { - pos += 2; - tokenValue = scanBinaryOrOctalDigits(/* base */ 2); - if (!tokenValue) { - error(Diagnostics.Binary_digit_expected); - tokenValue = "0"; - } - tokenValue = "0b" + tokenValue; - tokenFlags |= TokenFlags.BinarySpecifier; - return token = checkBigIntSuffix(); - } - else if (pos + 2 < end && (text.charCodeAt(pos + 1) === CharacterCodes.O || text.charCodeAt(pos + 1) === CharacterCodes.o)) { - pos += 2; - tokenValue = scanBinaryOrOctalDigits(/* base */ 8); - if (!tokenValue) { - error(Diagnostics.Octal_digit_expected); - tokenValue = "0"; - } - tokenValue = "0o" + tokenValue; - tokenFlags |= TokenFlags.OctalSpecifier; - return token = checkBigIntSuffix(); - } - // Try to parse as an octal - if (pos + 1 < end && isOctalDigit(text.charCodeAt(pos + 1))) { - tokenValue = "" + scanOctalDigits(); - tokenFlags |= TokenFlags.Octal; - return token = SyntaxKind.NumericLiteral; - } - // This fall-through is a deviation from the EcmaScript grammar. The grammar says that a leading zero - // can only be followed by an octal digit, a dot, or the end of the number literal. However, we are being - // permissive and allowing decimal digits of the form 08* and 09* (which many browsers also do). - // falls through - case CharacterCodes._1: - case CharacterCodes._2: - case CharacterCodes._3: - case CharacterCodes._4: - case CharacterCodes._5: - case CharacterCodes._6: - case CharacterCodes._7: - case CharacterCodes._8: - case CharacterCodes._9: - ({ type: token, value: tokenValue } = scanNumber()); - return token; - case CharacterCodes.colon: - pos++; - return token = SyntaxKind.ColonToken; - case CharacterCodes.semicolon: - pos++; - return token = SyntaxKind.SemicolonToken; - case CharacterCodes.lessThan: - if (isConflictMarkerTrivia(text, pos)) { - pos = scanConflictMarkerTrivia(text, pos, error); - if (skipTrivia) { - continue; - } - else { - return token = SyntaxKind.ConflictMarkerTrivia; - } - } - - if (text.charCodeAt(pos + 1) === CharacterCodes.lessThan) { - if (text.charCodeAt(pos + 2) === CharacterCodes.equals) { - return pos += 3, token = SyntaxKind.LessThanLessThanEqualsToken; - } - return pos += 2, token = SyntaxKind.LessThanLessThanToken; - } - if (text.charCodeAt(pos + 1) === CharacterCodes.equals) { - return pos += 2, token = SyntaxKind.LessThanEqualsToken; - } - if (languageVariant === LanguageVariant.JSX && - text.charCodeAt(pos + 1) === CharacterCodes.slash && - text.charCodeAt(pos + 2) !== CharacterCodes.asterisk) { - return pos += 2, token = SyntaxKind.LessThanSlashToken; - } - pos++; - return token = SyntaxKind.LessThanToken; - case CharacterCodes.equals: - if (isConflictMarkerTrivia(text, pos)) { - pos = scanConflictMarkerTrivia(text, pos, error); - if (skipTrivia) { - continue; - } - else { - return token = SyntaxKind.ConflictMarkerTrivia; - } - } - - if (text.charCodeAt(pos + 1) === CharacterCodes.equals) { - if (text.charCodeAt(pos + 2) === CharacterCodes.equals) { - return pos += 3, token = SyntaxKind.EqualsEqualsEqualsToken; - } - return pos += 2, token = SyntaxKind.EqualsEqualsToken; + else { + return token = SyntaxKind.SingleLineCommentTrivia; } - if (text.charCodeAt(pos + 1) === CharacterCodes.greaterThan) { - return pos += 2, token = SyntaxKind.EqualsGreaterThanToken; + } + // Multi-line comment + if (text.charCodeAt(pos + 1) === CharacterCodes.asterisk) { + pos += 2; + if (text.charCodeAt(pos) === CharacterCodes.asterisk && text.charCodeAt(pos + 1) !== CharacterCodes.slash) { + tokenFlags |= TokenFlags.PrecedingJSDocComment; } - pos++; - return token = SyntaxKind.EqualsToken; - case CharacterCodes.greaterThan: - if (isConflictMarkerTrivia(text, pos)) { - pos = scanConflictMarkerTrivia(text, pos, error); - if (skipTrivia) { - continue; + let commentClosed = false; + while (pos < end) { + const ch = text.charCodeAt(pos); + if (ch === CharacterCodes.asterisk && text.charCodeAt(pos + 1) === CharacterCodes.slash) { + pos += 2; + commentClosed = true; + break; } - else { - return token = SyntaxKind.ConflictMarkerTrivia; + if (isLineBreak(ch)) { + tokenFlags |= TokenFlags.PrecedingLineBreak; } - } - - pos++; - return token = SyntaxKind.GreaterThanToken; - case CharacterCodes.question: - pos++; - if (text.charCodeAt(pos) === CharacterCodes.dot && !isDigit(text.charCodeAt(pos + 1))) { pos++; - return token = SyntaxKind.QuestionDotToken; - } - if (text.charCodeAt(pos) === CharacterCodes.question) { - pos++; - return token = SyntaxKind.QuestionQuestionToken; - } - return token = SyntaxKind.QuestionToken; - case CharacterCodes.openBracket: - pos++; - return token = SyntaxKind.OpenBracketToken; - case CharacterCodes.closeBracket: - pos++; - return token = SyntaxKind.CloseBracketToken; - case CharacterCodes.caret: - if (text.charCodeAt(pos + 1) === CharacterCodes.equals) { - return pos += 2, token = SyntaxKind.CaretEqualsToken; - } - pos++; - return token = SyntaxKind.CaretToken; - case CharacterCodes.openBrace: - pos++; - return token = SyntaxKind.OpenBraceToken; - case CharacterCodes.bar: - if (isConflictMarkerTrivia(text, pos)) { - pos = scanConflictMarkerTrivia(text, pos, error); - if (skipTrivia) { - continue; - } - else { - return token = SyntaxKind.ConflictMarkerTrivia; - } - } - - if (text.charCodeAt(pos + 1) === CharacterCodes.bar) { - return pos += 2, token = SyntaxKind.BarBarToken; - } - if (text.charCodeAt(pos + 1) === CharacterCodes.equals) { - return pos += 2, token = SyntaxKind.BarEqualsToken; - } - pos++; - return token = SyntaxKind.BarToken; - case CharacterCodes.closeBrace: - pos++; - return token = SyntaxKind.CloseBraceToken; - case CharacterCodes.tilde: - pos++; - return token = SyntaxKind.TildeToken; - case CharacterCodes.at: - pos++; - return token = SyntaxKind.AtToken; - case CharacterCodes.backslash: - const extendedCookedChar = peekExtendedUnicodeEscape(); - if (extendedCookedChar >= 0 && isIdentifierStart(extendedCookedChar, languageVersion)) { - pos += 3; - tokenFlags |= TokenFlags.ExtendedUnicodeEscape; - tokenValue = scanExtendedUnicodeEscape() + scanIdentifierParts(); - return token = getIdentifierToken(); } - - const cookedChar = peekUnicodeEscape(); - if (cookedChar >= 0 && isIdentifierStart(cookedChar, languageVersion)) { - pos += 6; - tokenFlags |= TokenFlags.UnicodeEscape; - tokenValue = String.fromCharCode(cookedChar) + scanIdentifierParts(); - return token = getIdentifierToken(); - } - - error(Diagnostics.Invalid_character); - pos++; - return token = SyntaxKind.Unknown; - case CharacterCodes.hash: - if (pos !== 0 && text[pos + 1] === "!") { - error(Diagnostics.can_only_be_used_at_the_start_of_a_file); - pos++; - return token = SyntaxKind.Unknown; + if (!commentClosed) { + error(Diagnostics.Asterisk_Slash_expected); } - pos++; - if (isIdentifierStart(ch = text.charCodeAt(pos), languageVersion)) { - pos++; - while (pos < end && isIdentifierPart(ch = text.charCodeAt(pos), languageVersion)) pos++; - tokenValue = text.substring(tokenPos, pos); - if (ch === CharacterCodes.backslash) { - tokenValue += scanIdentifierParts(); - } + if (skipTrivia) { + continue; } else { - tokenValue = "#"; - error(Diagnostics.Invalid_character); - } - return token = SyntaxKind.PrivateIdentifier; - default: - if (isIdentifierStart(ch, languageVersion)) { - pos += charSize(ch); - while (pos < end && isIdentifierPart(ch = codePointAt(text, pos), languageVersion)) pos += charSize(ch); - tokenValue = text.substring(tokenPos, pos); - if (ch === CharacterCodes.backslash) { - tokenValue += scanIdentifierParts(); + if (!commentClosed) { + tokenFlags |= TokenFlags.Unterminated; } - return token = getIdentifierToken(); + return token = SyntaxKind.MultiLineCommentTrivia; } - else if (isWhiteSpaceSingleLine(ch)) { - pos += charSize(ch); - continue; + } + if (text.charCodeAt(pos + 1) === CharacterCodes.equals) { + return pos += 2, token = SyntaxKind.SlashEqualsToken; + } + pos++; + return token = SyntaxKind.SlashToken; + case CharacterCodes._0: + if (pos + 2 < end && (text.charCodeAt(pos + 1) === CharacterCodes.X || text.charCodeAt(pos + 1) === CharacterCodes.x)) { + pos += 2; + tokenValue = scanMinimumNumberOfHexDigits(1, /*canHaveSeparators*/ true); + if (!tokenValue) { + error(Diagnostics.Hexadecimal_digit_expected); + tokenValue = "0"; } - else if (isLineBreak(ch)) { - tokenFlags |= TokenFlags.PrecedingLineBreak; - pos += charSize(ch); - continue; + tokenValue = "0x" + tokenValue; + tokenFlags |= TokenFlags.HexSpecifier; + return token = checkBigIntSuffix(); + } + else if (pos + 2 < end && (text.charCodeAt(pos + 1) === CharacterCodes.B || text.charCodeAt(pos + 1) === CharacterCodes.b)) { + pos += 2; + tokenValue = scanBinaryOrOctalDigits(/* base */ 2); + if (!tokenValue) { + error(Diagnostics.Binary_digit_expected); + tokenValue = "0"; } - error(Diagnostics.Invalid_character); - pos += charSize(ch); - return token = SyntaxKind.Unknown; - } - } - } - - function reScanGreaterToken(): SyntaxKind { - if (token === SyntaxKind.GreaterThanToken) { - if (text.charCodeAt(pos) === CharacterCodes.greaterThan) { - if (text.charCodeAt(pos + 1) === CharacterCodes.greaterThan) { - if (text.charCodeAt(pos + 2) === CharacterCodes.equals) { - return pos += 3, token = SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken; + tokenValue = "0b" + tokenValue; + tokenFlags |= TokenFlags.BinarySpecifier; + return token = checkBigIntSuffix(); + } + else if (pos + 2 < end && (text.charCodeAt(pos + 1) === CharacterCodes.O || text.charCodeAt(pos + 1) === CharacterCodes.o)) { + pos += 2; + tokenValue = scanBinaryOrOctalDigits(/* base */ 8); + if (!tokenValue) { + error(Diagnostics.Octal_digit_expected); + tokenValue = "0"; } - return pos += 2, token = SyntaxKind.GreaterThanGreaterThanGreaterThanToken; + tokenValue = "0o" + tokenValue; + tokenFlags |= TokenFlags.OctalSpecifier; + return token = checkBigIntSuffix(); } - if (text.charCodeAt(pos + 1) === CharacterCodes.equals) { - return pos += 2, token = SyntaxKind.GreaterThanGreaterThanEqualsToken; + // Try to parse as an octal + if (pos + 1 < end && isOctalDigit(text.charCodeAt(pos + 1))) { + tokenValue = "" + scanOctalDigits(); + tokenFlags |= TokenFlags.Octal; + return token = SyntaxKind.NumericLiteral; } + // This fall-through is a deviation from the EcmaScript grammar. The grammar says that a leading zero + // can only be followed by an octal digit, a dot, or the end of the number literal. However, we are being + // permissive and allowing decimal digits of the form 08* and 09* (which many browsers also do). + // falls through + case CharacterCodes._1: + case CharacterCodes._2: + case CharacterCodes._3: + case CharacterCodes._4: + case CharacterCodes._5: + case CharacterCodes._6: + case CharacterCodes._7: + case CharacterCodes._8: + case CharacterCodes._9: + ({ type: token, value: tokenValue } = scanNumber()); + return token; + case CharacterCodes.colon: pos++; - return token = SyntaxKind.GreaterThanGreaterThanToken; - } - if (text.charCodeAt(pos) === CharacterCodes.equals) { + return token = SyntaxKind.ColonToken; + case CharacterCodes.semicolon: pos++; - return token = SyntaxKind.GreaterThanEqualsToken; - } - } - return token; - } - - function reScanSlashToken(): SyntaxKind { - if (token === SyntaxKind.SlashToken || token === SyntaxKind.SlashEqualsToken) { - let p = tokenPos + 1; - let inEscape = false; - let inCharacterClass = false; - while (true) { - // If we reach the end of a file, or hit a newline, then this is an unterminated - // regex. Report error and return what we have so far. - if (p >= end) { - tokenFlags |= TokenFlags.Unterminated; - error(Diagnostics.Unterminated_regular_expression_literal); - break; + return token = SyntaxKind.SemicolonToken; + case CharacterCodes.lessThan: + if (isConflictMarkerTrivia(text, pos)) { + pos = scanConflictMarkerTrivia(text, pos, error); + if (skipTrivia) { + continue; + } + else { + return token = SyntaxKind.ConflictMarkerTrivia; + } } - - const ch = text.charCodeAt(p); - if (isLineBreak(ch)) { - tokenFlags |= TokenFlags.Unterminated; - error(Diagnostics.Unterminated_regular_expression_literal); - break; + if (text.charCodeAt(pos + 1) === CharacterCodes.lessThan) { + if (text.charCodeAt(pos + 2) === CharacterCodes.equals) { + return pos += 3, token = SyntaxKind.LessThanLessThanEqualsToken; + } + return pos += 2, token = SyntaxKind.LessThanLessThanToken; } - - if (inEscape) { - // Parsing an escape character; - // reset the flag and just advance to the next char. - inEscape = false; + if (text.charCodeAt(pos + 1) === CharacterCodes.equals) { + return pos += 2, token = SyntaxKind.LessThanEqualsToken; } - else if (ch === CharacterCodes.slash && !inCharacterClass) { - // A slash within a character class is permissible, - // but in general it signals the end of the regexp literal. - p++; - break; + if (languageVariant === LanguageVariant.JSX && + text.charCodeAt(pos + 1) === CharacterCodes.slash && + text.charCodeAt(pos + 2) !== CharacterCodes.asterisk) { + return pos += 2, token = SyntaxKind.LessThanSlashToken; } - else if (ch === CharacterCodes.openBracket) { - inCharacterClass = true; + pos++; + return token = SyntaxKind.LessThanToken; + case CharacterCodes.equals: + if (isConflictMarkerTrivia(text, pos)) { + pos = scanConflictMarkerTrivia(text, pos, error); + if (skipTrivia) { + continue; + } + else { + return token = SyntaxKind.ConflictMarkerTrivia; + } } - else if (ch === CharacterCodes.backslash) { - inEscape = true; + if (text.charCodeAt(pos + 1) === CharacterCodes.equals) { + if (text.charCodeAt(pos + 2) === CharacterCodes.equals) { + return pos += 3, token = SyntaxKind.EqualsEqualsEqualsToken; + } + return pos += 2, token = SyntaxKind.EqualsEqualsToken; } - else if (ch === CharacterCodes.closeBracket) { - inCharacterClass = false; + if (text.charCodeAt(pos + 1) === CharacterCodes.greaterThan) { + return pos += 2, token = SyntaxKind.EqualsGreaterThanToken; } - p++; - } - - while (p < end && isIdentifierPart(text.charCodeAt(p), languageVersion)) { - p++; - } - pos = p; - tokenValue = text.substring(tokenPos, pos); - token = SyntaxKind.RegularExpressionLiteral; - } - return token; - } - - /** - * Unconditionally back up and scan a template expression portion. - */ - function reScanTemplateToken(isTaggedTemplate: boolean): SyntaxKind { - Debug.assert(token === SyntaxKind.CloseBraceToken, "'reScanTemplateToken' should only be called on a '}'"); - pos = tokenPos; - return token = scanTemplateAndSetTokenValue(isTaggedTemplate); - } - - function reScanTemplateHeadOrNoSubstitutionTemplate(): SyntaxKind { - pos = tokenPos; - return token = scanTemplateAndSetTokenValue(/* isTaggedTemplate */ true); - } - - function reScanJsxToken(): JsxTokenSyntaxKind { - pos = tokenPos = startPos; - return token = scanJsxToken(); - } - - function reScanLessThanToken(): SyntaxKind { - if (token === SyntaxKind.LessThanLessThanToken) { - pos = tokenPos + 1; - return token = SyntaxKind.LessThanToken; - } - return token; - } - - function reScanQuestionToken(): SyntaxKind { - Debug.assert(token === SyntaxKind.QuestionQuestionToken, "'reScanQuestionToken' should only be called on a '??'"); - pos = tokenPos + 1; - return token = SyntaxKind.QuestionToken; - } - - function scanJsxToken(): JsxTokenSyntaxKind { - startPos = tokenPos = pos; - - if (pos >= end) { - return token = SyntaxKind.EndOfFileToken; - } - - let char = text.charCodeAt(pos); - if (char === CharacterCodes.lessThan) { - if (text.charCodeAt(pos + 1) === CharacterCodes.slash) { - pos += 2; - return token = SyntaxKind.LessThanSlashToken; - } - pos++; - return token = SyntaxKind.LessThanToken; - } - - if (char === CharacterCodes.openBrace) { - pos++; - return token = SyntaxKind.OpenBraceToken; - } - - // First non-whitespace character on this line. - let firstNonWhitespace = 0; - let lastNonWhitespace = -1; - - // These initial values are special because the first line is: - // firstNonWhitespace = 0 to indicate that we want leading whitespace, - - while (pos < end) { - - // We want to keep track of the last non-whitespace (but including - // newlines character for hitting the end of the JSX Text region) - if (!isWhiteSpaceSingleLine(char)) { - lastNonWhitespace = pos; - } - - char = text.charCodeAt(pos); - if (char === CharacterCodes.openBrace) { - break; - } - if (char === CharacterCodes.lessThan) { + pos++; + return token = SyntaxKind.EqualsToken; + case CharacterCodes.greaterThan: if (isConflictMarkerTrivia(text, pos)) { pos = scanConflictMarkerTrivia(text, pos, error); - return token = SyntaxKind.ConflictMarkerTrivia; - } - break; - } - if (char === CharacterCodes.greaterThan) { - error(Diagnostics.Unexpected_token_Did_you_mean_or_gt, pos, 1); - } - if (char === CharacterCodes.closeBrace) { - error(Diagnostics.Unexpected_token_Did_you_mean_or_rbrace, pos, 1); - } - - if (lastNonWhitespace > 0) lastNonWhitespace++; - - // FirstNonWhitespace is 0, then we only see whitespaces so far. If we see a linebreak, we want to ignore that whitespaces. - // i.e (- : whitespace) - //
---- - //
becomes
- // - //
----
becomes
----
- if (isLineBreak(char) && firstNonWhitespace === 0) { - firstNonWhitespace = -1; - } - else if (!isWhiteSpaceLike(char)) { - firstNonWhitespace = pos; - } - - pos++; - } - - const endPosition = lastNonWhitespace === -1 ? pos : lastNonWhitespace; - tokenValue = text.substring(startPos, endPosition); - - return firstNonWhitespace === -1 ? SyntaxKind.JsxTextAllWhiteSpaces : SyntaxKind.JsxText; - } - - // Scans a JSX identifier; these differ from normal identifiers in that - // they allow dashes - function scanJsxIdentifier(): SyntaxKind { - if (tokenIsIdentifierOrKeyword(token)) { - // An identifier or keyword has already been parsed - check for a `-` and then append it and everything after it to the token - // Do note that this means that `scanJsxIdentifier` effectively _mutates_ the visible token without advancing to a new token - // Any caller should be expecting this behavior and should only read the pos or token value after calling it. - while (pos < end) { - const ch = text.charCodeAt(pos); - if (ch === CharacterCodes.minus) { - tokenValue += "-"; - pos++; - continue; + if (skipTrivia) { + continue; + } + else { + return token = SyntaxKind.ConflictMarkerTrivia; + } } - const oldPos = pos; - tokenValue += scanIdentifierParts(); // reuse `scanIdentifierParts` so unicode escapes are handled - if (pos === oldPos) { - break; + pos++; + return token = SyntaxKind.GreaterThanToken; + case CharacterCodes.question: + pos++; + if (text.charCodeAt(pos) === CharacterCodes.dot && !isDigit(text.charCodeAt(pos + 1))) { + pos++; + return token = SyntaxKind.QuestionDotToken; } - } - } - return token; - } - - function scanJsxAttributeValue(): SyntaxKind { - startPos = pos; - - switch (text.charCodeAt(pos)) { - case CharacterCodes.doubleQuote: - case CharacterCodes.singleQuote: - tokenValue = scanString(/*jsxAttributeString*/ true); - return token = SyntaxKind.StringLiteral; - default: - // If this scans anything other than `{`, it's a parse error. - return scan(); - } - } - - function reScanJsxAttributeValue(): SyntaxKind { - pos = tokenPos = startPos; - return scanJsxAttributeValue(); - } - - function scanJsDocToken(): JSDocSyntaxKind { - startPos = tokenPos = pos; - tokenFlags = TokenFlags.None; - if (pos >= end) { - return token = SyntaxKind.EndOfFileToken; - } - - const ch = codePointAt(text, pos); - pos += charSize(ch); - switch (ch) { - case CharacterCodes.tab: - case CharacterCodes.verticalTab: - case CharacterCodes.formFeed: - case CharacterCodes.space: - while (pos < end && isWhiteSpaceSingleLine(text.charCodeAt(pos))) { + if (text.charCodeAt(pos) === CharacterCodes.question) { pos++; + return token = SyntaxKind.QuestionQuestionToken; } - return token = SyntaxKind.WhitespaceTrivia; - case CharacterCodes.at: - return token = SyntaxKind.AtToken; - case CharacterCodes.lineFeed: - case CharacterCodes.carriageReturn: - tokenFlags |= TokenFlags.PrecedingLineBreak; - return token = SyntaxKind.NewLineTrivia; - case CharacterCodes.asterisk: - return token = SyntaxKind.AsteriskToken; - case CharacterCodes.openBrace: - return token = SyntaxKind.OpenBraceToken; - case CharacterCodes.closeBrace: - return token = SyntaxKind.CloseBraceToken; + return token = SyntaxKind.QuestionToken; case CharacterCodes.openBracket: + pos++; return token = SyntaxKind.OpenBracketToken; case CharacterCodes.closeBracket: + pos++; return token = SyntaxKind.CloseBracketToken; - case CharacterCodes.lessThan: - return token = SyntaxKind.LessThanToken; - case CharacterCodes.greaterThan: - return token = SyntaxKind.GreaterThanToken; - case CharacterCodes.equals: - return token = SyntaxKind.EqualsToken; - case CharacterCodes.comma: - return token = SyntaxKind.CommaToken; - case CharacterCodes.dot: - return token = SyntaxKind.DotToken; - case CharacterCodes.backtick: - return token = SyntaxKind.BacktickToken; + case CharacterCodes.caret: + if (text.charCodeAt(pos + 1) === CharacterCodes.equals) { + return pos += 2, token = SyntaxKind.CaretEqualsToken; + } + pos++; + return token = SyntaxKind.CaretToken; + case CharacterCodes.openBrace: + pos++; + return token = SyntaxKind.OpenBraceToken; + case CharacterCodes.bar: + if (isConflictMarkerTrivia(text, pos)) { + pos = scanConflictMarkerTrivia(text, pos, error); + if (skipTrivia) { + continue; + } + else { + return token = SyntaxKind.ConflictMarkerTrivia; + } + } + if (text.charCodeAt(pos + 1) === CharacterCodes.bar) { + return pos += 2, token = SyntaxKind.BarBarToken; + } + if (text.charCodeAt(pos + 1) === CharacterCodes.equals) { + return pos += 2, token = SyntaxKind.BarEqualsToken; + } + pos++; + return token = SyntaxKind.BarToken; + case CharacterCodes.closeBrace: + pos++; + return token = SyntaxKind.CloseBraceToken; + case CharacterCodes.tilde: + pos++; + return token = SyntaxKind.TildeToken; + case CharacterCodes.at: + pos++; + return token = SyntaxKind.AtToken; case CharacterCodes.backslash: - pos--; const extendedCookedChar = peekExtendedUnicodeEscape(); if (extendedCookedChar >= 0 && isIdentifierStart(extendedCookedChar, languageVersion)) { pos += 3; @@ -2285,7 +1783,6 @@ namespace ts { tokenValue = scanExtendedUnicodeEscape() + scanIdentifierParts(); return token = getIdentifierToken(); } - const cookedChar = peekUnicodeEscape(); if (cookedChar >= 0 && isIdentifierStart(cookedChar, languageVersion)) { pos += 6; @@ -2293,161 +1790,453 @@ namespace ts { tokenValue = String.fromCharCode(cookedChar) + scanIdentifierParts(); return token = getIdentifierToken(); } + error(Diagnostics.Invalid_character); pos++; return token = SyntaxKind.Unknown; + case CharacterCodes.hash: + if (pos !== 0 && text[pos + 1] === "!") { + error(Diagnostics.can_only_be_used_at_the_start_of_a_file); + pos++; + return token = SyntaxKind.Unknown; + } + pos++; + if (isIdentifierStart(ch = text.charCodeAt(pos), languageVersion)) { + pos++; + while (pos < end && isIdentifierPart(ch = text.charCodeAt(pos), languageVersion)) + pos++; + tokenValue = text.substring(tokenPos, pos); + if (ch === CharacterCodes.backslash) { + tokenValue += scanIdentifierParts(); + } + } + else { + tokenValue = "#"; + error(Diagnostics.Invalid_character); + } + return token = SyntaxKind.PrivateIdentifier; + default: + if (isIdentifierStart(ch, languageVersion)) { + pos += charSize(ch); + while (pos < end && isIdentifierPart(ch = codePointAt(text, pos), languageVersion)) + pos += charSize(ch); + tokenValue = text.substring(tokenPos, pos); + if (ch === CharacterCodes.backslash) { + tokenValue += scanIdentifierParts(); + } + return token = getIdentifierToken(); + } + else if (isWhiteSpaceSingleLine(ch)) { + pos += charSize(ch); + continue; + } + else if (isLineBreak(ch)) { + tokenFlags |= TokenFlags.PrecedingLineBreak; + pos += charSize(ch); + continue; + } + error(Diagnostics.Invalid_character); + pos += charSize(ch); + return token = SyntaxKind.Unknown; } - - if (isIdentifierStart(ch, languageVersion)) { - let char = ch; - while (pos < end && isIdentifierPart(char = codePointAt(text, pos), languageVersion) || text.charCodeAt(pos) === CharacterCodes.minus) pos += charSize(char); - tokenValue = text.substring(tokenPos, pos); - if (char === CharacterCodes.backslash) { - tokenValue += scanIdentifierParts(); + } + } + function reScanGreaterToken(): SyntaxKind { + if (token === SyntaxKind.GreaterThanToken) { + if (text.charCodeAt(pos) === CharacterCodes.greaterThan) { + if (text.charCodeAt(pos + 1) === CharacterCodes.greaterThan) { + if (text.charCodeAt(pos + 2) === CharacterCodes.equals) { + return pos += 3, token = SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken; + } + return pos += 2, token = SyntaxKind.GreaterThanGreaterThanGreaterThanToken; + } + if (text.charCodeAt(pos + 1) === CharacterCodes.equals) { + return pos += 2, token = SyntaxKind.GreaterThanGreaterThanEqualsToken; } - return token = getIdentifierToken(); + pos++; + return token = SyntaxKind.GreaterThanGreaterThanToken; } - else { - return token = SyntaxKind.Unknown; + if (text.charCodeAt(pos) === CharacterCodes.equals) { + pos++; + return token = SyntaxKind.GreaterThanEqualsToken; } } - - function speculationHelper(callback: () => T, isLookahead: boolean): T { - const savePos = pos; - const saveStartPos = startPos; - const saveTokenPos = tokenPos; - const saveToken = token; - const saveTokenValue = tokenValue; - const saveTokenFlags = tokenFlags; - const result = callback(); - - // If our callback returned something 'falsy' or we're just looking ahead, - // then unconditionally restore us to where we were. - if (!result || isLookahead) { - pos = savePos; - startPos = saveStartPos; - tokenPos = saveTokenPos; - token = saveToken; - tokenValue = saveTokenValue; - tokenFlags = saveTokenFlags; + return token; + } + function reScanSlashToken(): SyntaxKind { + if (token === SyntaxKind.SlashToken || token === SyntaxKind.SlashEqualsToken) { + let p = tokenPos + 1; + let inEscape = false; + let inCharacterClass = false; + while (true) { + // If we reach the end of a file, or hit a newline, then this is an unterminated + // regex. Report error and return what we have so far. + if (p >= end) { + tokenFlags |= TokenFlags.Unterminated; + error(Diagnostics.Unterminated_regular_expression_literal); + break; + } + const ch = text.charCodeAt(p); + if (isLineBreak(ch)) { + tokenFlags |= TokenFlags.Unterminated; + error(Diagnostics.Unterminated_regular_expression_literal); + break; + } + if (inEscape) { + // Parsing an escape character; + // reset the flag and just advance to the next char. + inEscape = false; + } + else if (ch === CharacterCodes.slash && !inCharacterClass) { + // A slash within a character class is permissible, + // but in general it signals the end of the regexp literal. + p++; + break; + } + else if (ch === CharacterCodes.openBracket) { + inCharacterClass = true; + } + else if (ch === CharacterCodes.backslash) { + inEscape = true; + } + else if (ch === CharacterCodes.closeBracket) { + inCharacterClass = false; + } + p++; } - return result; - } - - function scanRange(start: number, length: number, callback: () => T): T { - const saveEnd = end; - const savePos = pos; - const saveStartPos = startPos; - const saveTokenPos = tokenPos; - const saveToken = token; - const saveTokenValue = tokenValue; - const saveTokenFlags = tokenFlags; - - setText(text, start, length); - const result = callback(); - - end = saveEnd; - pos = savePos; - startPos = saveStartPos; - tokenPos = saveTokenPos; - token = saveToken; - tokenValue = saveTokenValue; - tokenFlags = saveTokenFlags; - - return result; - } - - function lookAhead(callback: () => T): T { - return speculationHelper(callback, /*isLookahead*/ true); + while (p < end && isIdentifierPart(text.charCodeAt(p), languageVersion)) { + p++; + } + pos = p; + tokenValue = text.substring(tokenPos, pos); + token = SyntaxKind.RegularExpressionLiteral; } - - function tryScan(callback: () => T): T { - return speculationHelper(callback, /*isLookahead*/ false); + return token; + } + /** + * Unconditionally back up and scan a template expression portion. + */ + function reScanTemplateToken(isTaggedTemplate: boolean): SyntaxKind { + Debug.assert(token === SyntaxKind.CloseBraceToken, "'reScanTemplateToken' should only be called on a '}'"); + pos = tokenPos; + return token = scanTemplateAndSetTokenValue(isTaggedTemplate); + } + function reScanTemplateHeadOrNoSubstitutionTemplate(): SyntaxKind { + pos = tokenPos; + return token = scanTemplateAndSetTokenValue(/* isTaggedTemplate */ true); + } + function reScanJsxToken(): JsxTokenSyntaxKind { + pos = tokenPos = startPos; + return token = scanJsxToken(); + } + function reScanLessThanToken(): SyntaxKind { + if (token === SyntaxKind.LessThanLessThanToken) { + pos = tokenPos + 1; + return token = SyntaxKind.LessThanToken; } - - function getText(): string { - return text; + return token; + } + function reScanQuestionToken(): SyntaxKind { + Debug.assert(token === SyntaxKind.QuestionQuestionToken, "'reScanQuestionToken' should only be called on a '??'"); + pos = tokenPos + 1; + return token = SyntaxKind.QuestionToken; + } + function scanJsxToken(): JsxTokenSyntaxKind { + startPos = tokenPos = pos; + if (pos >= end) { + return token = SyntaxKind.EndOfFileToken; + } + let char = text.charCodeAt(pos); + if (char === CharacterCodes.lessThan) { + if (text.charCodeAt(pos + 1) === CharacterCodes.slash) { + pos += 2; + return token = SyntaxKind.LessThanSlashToken; + } + pos++; + return token = SyntaxKind.LessThanToken; } - - function setText(newText: string | undefined, start: number | undefined, length: number | undefined) { - text = newText || ""; - end = length === undefined ? text.length : start! + length; - setTextPos(start || 0); + if (char === CharacterCodes.openBrace) { + pos++; + return token = SyntaxKind.OpenBraceToken; + } + // First non-whitespace character on this line. + let firstNonWhitespace = 0; + let lastNonWhitespace = -1; + // These initial values are special because the first line is: + // firstNonWhitespace = 0 to indicate that we want leading whitespace, + while (pos < end) { + // We want to keep track of the last non-whitespace (but including + // newlines character for hitting the end of the JSX Text region) + if (!isWhiteSpaceSingleLine(char)) { + lastNonWhitespace = pos; + } + char = text.charCodeAt(pos); + if (char === CharacterCodes.openBrace) { + break; + } + if (char === CharacterCodes.lessThan) { + if (isConflictMarkerTrivia(text, pos)) { + pos = scanConflictMarkerTrivia(text, pos, error); + return token = SyntaxKind.ConflictMarkerTrivia; + } + break; + } + if (char === CharacterCodes.greaterThan) { + error(Diagnostics.Unexpected_token_Did_you_mean_or_gt, pos, 1); + } + if (char === CharacterCodes.closeBrace) { + error(Diagnostics.Unexpected_token_Did_you_mean_or_rbrace, pos, 1); + } + if (lastNonWhitespace > 0) + lastNonWhitespace++; + // FirstNonWhitespace is 0, then we only see whitespaces so far. If we see a linebreak, we want to ignore that whitespaces. + // i.e (- : whitespace) + //
---- + //
becomes
+ // + //
----
becomes
----
+ if (isLineBreak(char) && firstNonWhitespace === 0) { + firstNonWhitespace = -1; + } + else if (!isWhiteSpaceLike(char)) { + firstNonWhitespace = pos; + } + pos++; } - - function setOnError(errorCallback: ErrorCallback | undefined) { - onError = errorCallback; + const endPosition = lastNonWhitespace === -1 ? pos : lastNonWhitespace; + tokenValue = text.substring(startPos, endPosition); + return firstNonWhitespace === -1 ? SyntaxKind.JsxTextAllWhiteSpaces : SyntaxKind.JsxText; + } + // Scans a JSX identifier; these differ from normal identifiers in that + // they allow dashes + function scanJsxIdentifier(): SyntaxKind { + if (tokenIsIdentifierOrKeyword(token)) { + // An identifier or keyword has already been parsed - check for a `-` and then append it and everything after it to the token + // Do note that this means that `scanJsxIdentifier` effectively _mutates_ the visible token without advancing to a new token + // Any caller should be expecting this behavior and should only read the pos or token value after calling it. + while (pos < end) { + const ch = text.charCodeAt(pos); + if (ch === CharacterCodes.minus) { + tokenValue += "-"; + pos++; + continue; + } + const oldPos = pos; + tokenValue += scanIdentifierParts(); // reuse `scanIdentifierParts` so unicode escapes are handled + if (pos === oldPos) { + break; + } + } } - - function setScriptTarget(scriptTarget: ScriptTarget) { - languageVersion = scriptTarget; + return token; + } + function scanJsxAttributeValue(): SyntaxKind { + startPos = pos; + switch (text.charCodeAt(pos)) { + case CharacterCodes.doubleQuote: + case CharacterCodes.singleQuote: + tokenValue = scanString(/*jsxAttributeString*/ true); + return token = SyntaxKind.StringLiteral; + default: + // If this scans anything other than `{`, it's a parse error. + return scan(); } - - function setLanguageVariant(variant: LanguageVariant) { - languageVariant = variant; + } + function reScanJsxAttributeValue(): SyntaxKind { + pos = tokenPos = startPos; + return scanJsxAttributeValue(); + } + function scanJsDocToken(): JSDocSyntaxKind { + startPos = tokenPos = pos; + tokenFlags = TokenFlags.None; + if (pos >= end) { + return token = SyntaxKind.EndOfFileToken; + } + const ch = codePointAt(text, pos); + pos += charSize(ch); + switch (ch) { + case CharacterCodes.tab: + case CharacterCodes.verticalTab: + case CharacterCodes.formFeed: + case CharacterCodes.space: + while (pos < end && isWhiteSpaceSingleLine(text.charCodeAt(pos))) { + pos++; + } + return token = SyntaxKind.WhitespaceTrivia; + case CharacterCodes.at: + return token = SyntaxKind.AtToken; + case CharacterCodes.lineFeed: + case CharacterCodes.carriageReturn: + tokenFlags |= TokenFlags.PrecedingLineBreak; + return token = SyntaxKind.NewLineTrivia; + case CharacterCodes.asterisk: + return token = SyntaxKind.AsteriskToken; + case CharacterCodes.openBrace: + return token = SyntaxKind.OpenBraceToken; + case CharacterCodes.closeBrace: + return token = SyntaxKind.CloseBraceToken; + case CharacterCodes.openBracket: + return token = SyntaxKind.OpenBracketToken; + case CharacterCodes.closeBracket: + return token = SyntaxKind.CloseBracketToken; + case CharacterCodes.lessThan: + return token = SyntaxKind.LessThanToken; + case CharacterCodes.greaterThan: + return token = SyntaxKind.GreaterThanToken; + case CharacterCodes.equals: + return token = SyntaxKind.EqualsToken; + case CharacterCodes.comma: + return token = SyntaxKind.CommaToken; + case CharacterCodes.dot: + return token = SyntaxKind.DotToken; + case CharacterCodes.backtick: + return token = SyntaxKind.BacktickToken; + case CharacterCodes.backslash: + pos--; + const extendedCookedChar = peekExtendedUnicodeEscape(); + if (extendedCookedChar >= 0 && isIdentifierStart(extendedCookedChar, languageVersion)) { + pos += 3; + tokenFlags |= TokenFlags.ExtendedUnicodeEscape; + tokenValue = scanExtendedUnicodeEscape() + scanIdentifierParts(); + return token = getIdentifierToken(); + } + const cookedChar = peekUnicodeEscape(); + if (cookedChar >= 0 && isIdentifierStart(cookedChar, languageVersion)) { + pos += 6; + tokenFlags |= TokenFlags.UnicodeEscape; + tokenValue = String.fromCharCode(cookedChar) + scanIdentifierParts(); + return token = getIdentifierToken(); + } + pos++; + return token = SyntaxKind.Unknown; } - - function setTextPos(textPos: number) { - Debug.assert(textPos >= 0); - pos = textPos; - startPos = textPos; - tokenPos = textPos; - token = SyntaxKind.Unknown; - tokenValue = undefined!; - tokenFlags = TokenFlags.None; + if (isIdentifierStart(ch, languageVersion)) { + let char = ch; + while (pos < end && isIdentifierPart(char = codePointAt(text, pos), languageVersion) || text.charCodeAt(pos) === CharacterCodes.minus) + pos += charSize(char); + tokenValue = text.substring(tokenPos, pos); + if (char === CharacterCodes.backslash) { + tokenValue += scanIdentifierParts(); + } + return token = getIdentifierToken(); } - - function setInJSDocType(inType: boolean) { - inJSDocType += inType ? 1 : -1; + else { + return token = SyntaxKind.Unknown; } } - - /* @internal */ - const codePointAt: (s: string, i: number) => number = (String.prototype as any).codePointAt ? (s, i) => (s as any).codePointAt(i) : function codePointAt(str, i): number { - // from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/codePointAt - const size = str.length; - // Account for out-of-bounds indices: - if (i < 0 || i >= size) { - return undefined!; // String.codePointAt returns `undefined` for OOB indexes - } - // Get the first code unit - const first = str.charCodeAt(i); - // check if it’s the start of a surrogate pair - if (first >= 0xD800 && first <= 0xDBFF && size > i + 1) { // high surrogate and there is a next code unit - const second = str.charCodeAt(i + 1); - if (second >= 0xDC00 && second <= 0xDFFF) { // low surrogate - // https://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae - return (first - 0xD800) * 0x400 + second - 0xDC00 + 0x10000; - } - } - return first; - }; - - /* @internal */ - function charSize(ch: number) { - if (ch >= 0x10000) { - return 2; + function speculationHelper(callback: () => T, isLookahead: boolean): T { + const savePos = pos; + const saveStartPos = startPos; + const saveTokenPos = tokenPos; + const saveToken = token; + const saveTokenValue = tokenValue; + const saveTokenFlags = tokenFlags; + const result = callback(); + // If our callback returned something 'falsy' or we're just looking ahead, + // then unconditionally restore us to where we were. + if (!result || isLookahead) { + pos = savePos; + startPos = saveStartPos; + tokenPos = saveTokenPos; + token = saveToken; + tokenValue = saveTokenValue; + tokenFlags = saveTokenFlags; } - return 1; + return result; } - - // Derived from the 10.1.1 UTF16Encoding of the ES6 Spec. - function utf16EncodeAsStringFallback(codePoint: number) { - Debug.assert(0x0 <= codePoint && codePoint <= 0x10FFFF); - - if (codePoint <= 65535) { - return String.fromCharCode(codePoint); + function scanRange(start: number, length: number, callback: () => T): T { + const saveEnd = end; + const savePos = pos; + const saveStartPos = startPos; + const saveTokenPos = tokenPos; + const saveToken = token; + const saveTokenValue = tokenValue; + const saveTokenFlags = tokenFlags; + setText(text, start, length); + const result = callback(); + end = saveEnd; + pos = savePos; + startPos = saveStartPos; + tokenPos = saveTokenPos; + token = saveToken; + tokenValue = saveTokenValue; + tokenFlags = saveTokenFlags; + return result; + } + function lookAhead(callback: () => T): T { + return speculationHelper(callback, /*isLookahead*/ true); + } + function tryScan(callback: () => T): T { + return speculationHelper(callback, /*isLookahead*/ false); + } + function getText(): string { + return text; + } + function setText(newText: string | undefined, start: number | undefined, length: number | undefined) { + text = newText || ""; + end = length === undefined ? text.length : start! + length; + setTextPos(start || 0); + } + function setOnError(errorCallback: ErrorCallback | undefined) { + onError = errorCallback; + } + function setScriptTarget(scriptTarget: ScriptTarget) { + languageVersion = scriptTarget; + } + function setLanguageVariant(variant: LanguageVariant) { + languageVariant = variant; + } + function setTextPos(textPos: number) { + Debug.assert(textPos >= 0); + pos = textPos; + startPos = textPos; + tokenPos = textPos; + token = SyntaxKind.Unknown; + tokenValue = undefined!; + tokenFlags = TokenFlags.None; + } + function setInJSDocType(inType: boolean) { + inJSDocType += inType ? 1 : -1; + } +} +/* @internal */ +const codePointAt: (s: string, i: number) => number = (String.prototype as any).codePointAt ? (s, i) => (s as any).codePointAt(i) : function codePointAt(str, i): number { + // from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/codePointAt + const size = str.length; + // Account for out-of-bounds indices: + if (i < 0 || i >= size) { + return undefined!; // String.codePointAt returns `undefined` for OOB indexes + } + // Get the first code unit + const first = str.charCodeAt(i); + // check if it’s the start of a surrogate pair + if (first >= 0xD800 && first <= 0xDBFF && size > i + 1) { // high surrogate and there is a next code unit + const second = str.charCodeAt(i + 1); + if (second >= 0xDC00 && second <= 0xDFFF) { // low surrogate + // https://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae + return (first - 0xD800) * 0x400 + second - 0xDC00 + 0x10000; } - - const codeUnit1 = Math.floor((codePoint - 65536) / 1024) + 0xD800; - const codeUnit2 = ((codePoint - 65536) % 1024) + 0xDC00; - - return String.fromCharCode(codeUnit1, codeUnit2); } - - const utf16EncodeAsStringWorker: (codePoint: number) => string = (String as any).fromCodePoint ? codePoint => (String as any).fromCodePoint(codePoint) : utf16EncodeAsStringFallback; - - /* @internal */ - export function utf16EncodeAsString(codePoint: number) { - return utf16EncodeAsStringWorker(codePoint); + return first; +}; +/* @internal */ +function charSize(ch: number) { + if (ch >= 0x10000) { + return 2; + } + return 1; +} +// Derived from the 10.1.1 UTF16Encoding of the ES6 Spec. +function utf16EncodeAsStringFallback(codePoint: number) { + Debug.assert(0x0 <= codePoint && codePoint <= 0x10FFFF); + if (codePoint <= 65535) { + return String.fromCharCode(codePoint); } + const codeUnit1 = Math.floor((codePoint - 65536) / 1024) + 0xD800; + const codeUnit2 = ((codePoint - 65536) % 1024) + 0xDC00; + return String.fromCharCode(codeUnit1, codeUnit2); +} +const utf16EncodeAsStringWorker: (codePoint: number) => string = (String as any).fromCodePoint ? codePoint => (String as any).fromCodePoint(codePoint) : utf16EncodeAsStringFallback; +/* @internal */ +export function utf16EncodeAsString(codePoint: number) { + return utf16EncodeAsStringWorker(codePoint); } diff --git a/src/compiler/semver.ts b/src/compiler/semver.ts index 1c77d24d07258..54eda363cd922 100644 --- a/src/compiler/semver.ts +++ b/src/compiler/semver.ts @@ -1,391 +1,383 @@ +import { Debug, emptyArray, Comparison, compareValues, some, compareStringsCaseSensitive, map } from "./ts"; /* @internal */ -namespace ts { - // https://semver.org/#spec-item-2 - // > A normal version number MUST take the form X.Y.Z where X, Y, and Z are non-negative - // > integers, and MUST NOT contain leading zeroes. X is the major version, Y is the minor - // > version, and Z is the patch version. Each element MUST increase numerically. - // - // NOTE: We differ here in that we allow X and X.Y, with missing parts having the default - // value of `0`. - const versionRegExp = /^(0|[1-9]\d*)(?:\.(0|[1-9]\d*)(?:\.(0|[1-9]\d*)(?:\-([a-z0-9-.]+))?(?:\+([a-z0-9-.]+))?)?)?$/i; - - // https://semver.org/#spec-item-9 - // > A pre-release version MAY be denoted by appending a hyphen and a series of dot separated - // > identifiers immediately following the patch version. Identifiers MUST comprise only ASCII - // > alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty. Numeric identifiers - // > MUST NOT include leading zeroes. - const prereleaseRegExp = /^(?:0|[1-9]\d*|[a-z-][a-z0-9-]*)(?:\.(?:0|[1-9]\d*|[a-z-][a-z0-9-]*))*$/i; - - // https://semver.org/#spec-item-10 - // > Build metadata MAY be denoted by appending a plus sign and a series of dot separated - // > identifiers immediately following the patch or pre-release version. Identifiers MUST - // > comprise only ASCII alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty. - const buildRegExp = /^[a-z0-9-]+(?:\.[a-z0-9-]+)*$/i; - - // https://semver.org/#spec-item-9 - // > Numeric identifiers MUST NOT include leading zeroes. - const numericIdentifierRegExp = /^(0|[1-9]\d*)$/; - - /** - * Describes a precise semantic version number, https://semver.org - */ - export class Version { - static readonly zero = new Version(0, 0, 0); - - readonly major: number; - readonly minor: number; - readonly patch: number; - readonly prerelease: readonly string[]; - readonly build: readonly string[]; - - constructor(text: string); - constructor(major: number, minor?: number, patch?: number, prerelease?: string, build?: string); - constructor(major: number | string, minor = 0, patch = 0, prerelease = "", build = "") { - if (typeof major === "string") { - const result = Debug.checkDefined(tryParseComponents(major), "Invalid version"); - ({ major, minor, patch, prerelease, build } = result); - } - - Debug.assert(major >= 0, "Invalid argument: major"); - Debug.assert(minor >= 0, "Invalid argument: minor"); - Debug.assert(patch >= 0, "Invalid argument: patch"); - Debug.assert(!prerelease || prereleaseRegExp.test(prerelease), "Invalid argument: prerelease"); - Debug.assert(!build || buildRegExp.test(build), "Invalid argument: build"); - this.major = major; - this.minor = minor; - this.patch = patch; - this.prerelease = prerelease ? prerelease.split(".") : emptyArray; - this.build = build ? build.split(".") : emptyArray; - } - - static tryParse(text: string) { - const result = tryParseComponents(text); - if (!result) return undefined; - - const { major, minor, patch, prerelease, build } = result; - return new Version(major, minor, patch, prerelease, build); - } - - compareTo(other: Version | undefined) { - // https://semver.org/#spec-item-11 - // > Precedence is determined by the first difference when comparing each of these - // > identifiers from left to right as follows: Major, minor, and patch versions are - // > always compared numerically. - // - // https://semver.org/#spec-item-11 - // > Precedence for two pre-release versions with the same major, minor, and patch version - // > MUST be determined by comparing each dot separated identifier from left to right until - // > a difference is found [...] - // - // https://semver.org/#spec-item-11 - // > Build metadata does not figure into precedence - if (this === other) return Comparison.EqualTo; - if (other === undefined) return Comparison.GreaterThan; - return compareValues(this.major, other.major) - || compareValues(this.minor, other.minor) - || compareValues(this.patch, other.patch) - || comparePrerelaseIdentifiers(this.prerelease, other.prerelease); - } - - increment(field: "major" | "minor" | "patch") { - switch (field) { - case "major": return new Version(this.major + 1, 0, 0); - case "minor": return new Version(this.major, this.minor + 1, 0); - case "patch": return new Version(this.major, this.minor, this.patch + 1); - default: return Debug.assertNever(field); - } - } - - toString() { - let result = `${this.major}.${this.minor}.${this.patch}`; - if (some(this.prerelease)) result += `-${this.prerelease.join(".")}`; - if (some(this.build)) result += `+${this.build.join(".")}`; - return result; +// https://semver.org/#spec-item-2 +// > A normal version number MUST take the form X.Y.Z where X, Y, and Z are non-negative +// > integers, and MUST NOT contain leading zeroes. X is the major version, Y is the minor +// > version, and Z is the patch version. Each element MUST increase numerically. +// +// NOTE: We differ here in that we allow X and X.Y, with missing parts having the default +// value of `0`. +const versionRegExp = /^(0|[1-9]\d*)(?:\.(0|[1-9]\d*)(?:\.(0|[1-9]\d*)(?:\-([a-z0-9-.]+))?(?:\+([a-z0-9-.]+))?)?)?$/i; +// https://semver.org/#spec-item-9 +// > A pre-release version MAY be denoted by appending a hyphen and a series of dot separated +// > identifiers immediately following the patch version. Identifiers MUST comprise only ASCII +// > alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty. Numeric identifiers +// > MUST NOT include leading zeroes. +/* @internal */ +const prereleaseRegExp = /^(?:0|[1-9]\d*|[a-z-][a-z0-9-]*)(?:\.(?:0|[1-9]\d*|[a-z-][a-z0-9-]*))*$/i; +// https://semver.org/#spec-item-10 +// > Build metadata MAY be denoted by appending a plus sign and a series of dot separated +// > identifiers immediately following the patch or pre-release version. Identifiers MUST +// > comprise only ASCII alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty. +/* @internal */ +const buildRegExp = /^[a-z0-9-]+(?:\.[a-z0-9-]+)*$/i; +// https://semver.org/#spec-item-9 +// > Numeric identifiers MUST NOT include leading zeroes. +/* @internal */ +const numericIdentifierRegExp = /^(0|[1-9]\d*)$/; +/** + * Describes a precise semantic version number, https://semver.org + */ +/* @internal */ +export class Version { + static readonly zero = new Version(0, 0, 0); + readonly major: number; + readonly minor: number; + readonly patch: number; + readonly prerelease: readonly string[]; + readonly build: readonly string[]; + constructor(text: string); + constructor(major: number, minor?: number, patch?: number, prerelease?: string, build?: string); + constructor(major: number | string, minor = 0, patch = 0, prerelease = "", build = "") { + if (typeof major === "string") { + const result = Debug.checkDefined(tryParseComponents(major), "Invalid version"); + ({ major, minor, patch, prerelease, build } = result); } + Debug.assert(major >= 0, "Invalid argument: major"); + Debug.assert(minor >= 0, "Invalid argument: minor"); + Debug.assert(patch >= 0, "Invalid argument: patch"); + Debug.assert(!prerelease || prereleaseRegExp.test(prerelease), "Invalid argument: prerelease"); + Debug.assert(!build || buildRegExp.test(build), "Invalid argument: build"); + this.major = major; + this.minor = minor; + this.patch = patch; + this.prerelease = prerelease ? prerelease.split(".") : emptyArray; + this.build = build ? build.split(".") : emptyArray; } - - function tryParseComponents(text: string) { - const match = versionRegExp.exec(text); - if (!match) return undefined; - - const [, major, minor = "0", patch = "0", prerelease = "", build = ""] = match; - if (prerelease && !prereleaseRegExp.test(prerelease)) return undefined; - if (build && !buildRegExp.test(build)) return undefined; - return { - major: parseInt(major, 10), - minor: parseInt(minor, 10), - patch: parseInt(patch, 10), - prerelease, - build - }; + static tryParse(text: string) { + const result = tryParseComponents(text); + if (!result) + return undefined; + const { major, minor, patch, prerelease, build } = result; + return new Version(major, minor, patch, prerelease, build); } - - function comparePrerelaseIdentifiers(left: readonly string[], right: readonly string[]) { + compareTo(other: Version | undefined) { // https://semver.org/#spec-item-11 - // > When major, minor, and patch are equal, a pre-release version has lower precedence - // > than a normal version. - if (left === right) return Comparison.EqualTo; - if (left.length === 0) return right.length === 0 ? Comparison.EqualTo : Comparison.GreaterThan; - if (right.length === 0) return Comparison.LessThan; - + // > Precedence is determined by the first difference when comparing each of these + // > identifiers from left to right as follows: Major, minor, and patch versions are + // > always compared numerically. + // // https://semver.org/#spec-item-11 // > Precedence for two pre-release versions with the same major, minor, and patch version // > MUST be determined by comparing each dot separated identifier from left to right until // > a difference is found [...] - const length = Math.min(left.length, right.length); - for (let i = 0; i < length; i++) { - const leftIdentifier = left[i]; - const rightIdentifier = right[i]; - if (leftIdentifier === rightIdentifier) continue; - - const leftIsNumeric = numericIdentifierRegExp.test(leftIdentifier); - const rightIsNumeric = numericIdentifierRegExp.test(rightIdentifier); - if (leftIsNumeric || rightIsNumeric) { - // https://semver.org/#spec-item-11 - // > Numeric identifiers always have lower precedence than non-numeric identifiers. - if (leftIsNumeric !== rightIsNumeric) return leftIsNumeric ? Comparison.LessThan : Comparison.GreaterThan; - - // https://semver.org/#spec-item-11 - // > identifiers consisting of only digits are compared numerically - const result = compareValues(+leftIdentifier, +rightIdentifier); - if (result) return result; - } - else { - // https://semver.org/#spec-item-11 - // > identifiers with letters or hyphens are compared lexically in ASCII sort order. - const result = compareStringsCaseSensitive(leftIdentifier, rightIdentifier); - if (result) return result; - } - } - + // // https://semver.org/#spec-item-11 - // > A larger set of pre-release fields has a higher precedence than a smaller set, if all - // > of the preceding identifiers are equal. - return compareValues(left.length, right.length); + // > Build metadata does not figure into precedence + if (this === other) + return Comparison.EqualTo; + if (other === undefined) + return Comparison.GreaterThan; + return compareValues(this.major, other.major) + || compareValues(this.minor, other.minor) + || compareValues(this.patch, other.patch) + || comparePrerelaseIdentifiers(this.prerelease, other.prerelease); } - - /** - * Describes a semantic version range, per https://github.com/npm/node-semver#ranges - */ - export class VersionRange { - private _alternatives: readonly (readonly Comparator[])[]; - - constructor(spec: string) { - this._alternatives = spec ? Debug.checkDefined(parseRange(spec), "Invalid range spec.") : emptyArray; - } - - static tryParse(text: string) { - const sets = parseRange(text); - if (sets) { - const range = new VersionRange(""); - range._alternatives = sets; - return range; - } - return undefined; + increment(field: "major" | "minor" | "patch") { + switch (field) { + case "major": return new Version(this.major + 1, 0, 0); + case "minor": return new Version(this.major, this.minor + 1, 0); + case "patch": return new Version(this.major, this.minor, this.patch + 1); + default: return Debug.assertNever(field); } - - test(version: Version | string) { - if (typeof version === "string") version = new Version(version); - return testDisjunction(version, this._alternatives); + } + toString() { + let result = `${this.major}.${this.minor}.${this.patch}`; + if (some(this.prerelease)) + result += `-${this.prerelease.join(".")}`; + if (some(this.build)) + result += `+${this.build.join(".")}`; + return result; + } +} +/* @internal */ +function tryParseComponents(text: string) { + const match = versionRegExp.exec(text); + if (!match) + return undefined; + const [, major, minor = "0", patch = "0", prerelease = "", build = ""] = match; + if (prerelease && !prereleaseRegExp.test(prerelease)) + return undefined; + if (build && !buildRegExp.test(build)) + return undefined; + return { + major: parseInt(major, 10), + minor: parseInt(minor, 10), + patch: parseInt(patch, 10), + prerelease, + build + }; +} +/* @internal */ +function comparePrerelaseIdentifiers(left: readonly string[], right: readonly string[]) { + // https://semver.org/#spec-item-11 + // > When major, minor, and patch are equal, a pre-release version has lower precedence + // > than a normal version. + if (left === right) + return Comparison.EqualTo; + if (left.length === 0) + return right.length === 0 ? Comparison.EqualTo : Comparison.GreaterThan; + if (right.length === 0) + return Comparison.LessThan; + // https://semver.org/#spec-item-11 + // > Precedence for two pre-release versions with the same major, minor, and patch version + // > MUST be determined by comparing each dot separated identifier from left to right until + // > a difference is found [...] + const length = Math.min(left.length, right.length); + for (let i = 0; i < length; i++) { + const leftIdentifier = left[i]; + const rightIdentifier = right[i]; + if (leftIdentifier === rightIdentifier) + continue; + const leftIsNumeric = numericIdentifierRegExp.test(leftIdentifier); + const rightIsNumeric = numericIdentifierRegExp.test(rightIdentifier); + if (leftIsNumeric || rightIsNumeric) { + // https://semver.org/#spec-item-11 + // > Numeric identifiers always have lower precedence than non-numeric identifiers. + if (leftIsNumeric !== rightIsNumeric) + return leftIsNumeric ? Comparison.LessThan : Comparison.GreaterThan; + // https://semver.org/#spec-item-11 + // > identifiers consisting of only digits are compared numerically + const result = compareValues(+leftIdentifier, +rightIdentifier); + if (result) + return result; } - - toString() { - return formatDisjunction(this._alternatives); + else { + // https://semver.org/#spec-item-11 + // > identifiers with letters or hyphens are compared lexically in ASCII sort order. + const result = compareStringsCaseSensitive(leftIdentifier, rightIdentifier); + if (result) + return result; } } - - interface Comparator { - readonly operator: "<" | "<=" | ">" | ">=" | "="; - readonly operand: Version; + // https://semver.org/#spec-item-11 + // > A larger set of pre-release fields has a higher precedence than a smaller set, if all + // > of the preceding identifiers are equal. + return compareValues(left.length, right.length); +} +/** + * Describes a semantic version range, per https://github.com/npm/node-semver#ranges + */ +/* @internal */ +export class VersionRange { + private _alternatives: readonly (readonly Comparator[])[]; + constructor(spec: string) { + this._alternatives = spec ? Debug.checkDefined(parseRange(spec), "Invalid range spec.") : emptyArray; } - - // https://github.com/npm/node-semver#range-grammar - // - // range-set ::= range ( logical-or range ) * - // range ::= hyphen | simple ( ' ' simple ) * | '' - // logical-or ::= ( ' ' ) * '||' ( ' ' ) * - const logicalOrRegExp = /\s*\|\|\s*/g; - const whitespaceRegExp = /\s+/g; - - // https://github.com/npm/node-semver#range-grammar - // - // partial ::= xr ( '.' xr ( '.' xr qualifier ? )? )? - // xr ::= 'x' | 'X' | '*' | nr - // nr ::= '0' | ['1'-'9'] ( ['0'-'9'] ) * - // qualifier ::= ( '-' pre )? ( '+' build )? - // pre ::= parts - // build ::= parts - // parts ::= part ( '.' part ) * - // part ::= nr | [-0-9A-Za-z]+ - const partialRegExp = /^([xX*0]|[1-9]\d*)(?:\.([xX*0]|[1-9]\d*)(?:\.([xX*0]|[1-9]\d*)(?:-([a-z0-9-.]+))?(?:\+([a-z0-9-.]+))?)?)?$/i; - - // https://github.com/npm/node-semver#range-grammar - // - // hyphen ::= partial ' - ' partial - const hyphenRegExp = /^\s*([a-z0-9-+.*]+)\s+-\s+([a-z0-9-+.*]+)\s*$/i; - - // https://github.com/npm/node-semver#range-grammar - // - // simple ::= primitive | partial | tilde | caret - // primitive ::= ( '<' | '>' | '>=' | '<=' | '=' ) partial - // tilde ::= '~' partial - // caret ::= '^' partial - const rangeRegExp = /^\s*(~|\^|<|<=|>|>=|=)?\s*([a-z0-9-+.*]+)$/i; - - function parseRange(text: string) { - const alternatives: Comparator[][] = []; - for (const range of text.trim().split(logicalOrRegExp)) { - if (!range) continue; - const comparators: Comparator[] = []; - const match = hyphenRegExp.exec(range); - if (match) { - if (!parseHyphen(match[1], match[2], comparators)) return undefined; - } - else { - for (const simple of range.split(whitespaceRegExp)) { - const match = rangeRegExp.exec(simple); - if (!match || !parseComparator(match[1], match[2], comparators)) return undefined; - } - } - alternatives.push(comparators); + static tryParse(text: string) { + const sets = parseRange(text); + if (sets) { + const range = new VersionRange(""); + range._alternatives = sets; + return range; } - return alternatives; + return undefined; } - - function parsePartial(text: string) { - const match = partialRegExp.exec(text); - if (!match) return undefined; - - const [, major, minor = "*", patch = "*", prerelease, build] = match; - const version = new Version( - isWildcard(major) ? 0 : parseInt(major, 10), - isWildcard(major) || isWildcard(minor) ? 0 : parseInt(minor, 10), - isWildcard(major) || isWildcard(minor) || isWildcard(patch) ? 0 : parseInt(patch, 10), - prerelease, - build); - - return { version, major, minor, patch }; + test(version: Version | string) { + if (typeof version === "string") + version = new Version(version); + return testDisjunction(version, this._alternatives); } - - function parseHyphen(left: string, right: string, comparators: Comparator[]) { - const leftResult = parsePartial(left); - if (!leftResult) return false; - - const rightResult = parsePartial(right); - if (!rightResult) return false; - - if (!isWildcard(leftResult.major)) { - comparators.push(createComparator(">=", leftResult.version)); - } - - if (!isWildcard(rightResult.major)) { - comparators.push( - isWildcard(rightResult.minor) ? createComparator("<", rightResult.version.increment("major")) : - isWildcard(rightResult.patch) ? createComparator("<", rightResult.version.increment("minor")) : - createComparator("<=", rightResult.version)); - } - - return true; + toString() { + return formatDisjunction(this._alternatives); } - - function parseComparator(operator: string, text: string, comparators: Comparator[]) { - const result = parsePartial(text); - if (!result) return false; - - const { version, major, minor, patch } = result; - if (!isWildcard(major)) { - switch (operator) { - case "~": - comparators.push(createComparator(">=", version)); - comparators.push(createComparator("<", version.increment( - isWildcard(minor) ? "major" : - "minor"))); - break; - case "^": - comparators.push(createComparator(">=", version)); - comparators.push(createComparator("<", version.increment( - version.major > 0 || isWildcard(minor) ? "major" : - version.minor > 0 || isWildcard(patch) ? "minor" : - "patch"))); - break; - case "<": - case ">=": - comparators.push(createComparator(operator, version)); - break; - case "<=": - case ">": - comparators.push( - isWildcard(minor) ? createComparator(operator === "<=" ? "<" : ">=", version.increment("major")) : - isWildcard(patch) ? createComparator(operator === "<=" ? "<" : ">=", version.increment("minor")) : - createComparator(operator, version)); - break; - case "=": - case undefined: - if (isWildcard(minor) || isWildcard(patch)) { - comparators.push(createComparator(">=", version)); - comparators.push(createComparator("<", version.increment(isWildcard(minor) ? "major" : "minor"))); - } - else { - comparators.push(createComparator("=", version)); - } - break; - default: - // unrecognized - return false; - } +} +/* @internal */ +interface Comparator { + readonly operator: "<" | "<=" | ">" | ">=" | "="; + readonly operand: Version; +} +// https://github.com/npm/node-semver#range-grammar +// +// range-set ::= range ( logical-or range ) * +// range ::= hyphen | simple ( ' ' simple ) * | '' +// logical-or ::= ( ' ' ) * '||' ( ' ' ) * +/* @internal */ +const logicalOrRegExp = /\s*\|\|\s*/g; +/* @internal */ +const whitespaceRegExp = /\s+/g; +// https://github.com/npm/node-semver#range-grammar +// +// partial ::= xr ( '.' xr ( '.' xr qualifier ? )? )? +// xr ::= 'x' | 'X' | '*' | nr +// nr ::= '0' | ['1'-'9'] ( ['0'-'9'] ) * +// qualifier ::= ( '-' pre )? ( '+' build )? +// pre ::= parts +// build ::= parts +// parts ::= part ( '.' part ) * +// part ::= nr | [-0-9A-Za-z]+ +/* @internal */ +const partialRegExp = /^([xX*0]|[1-9]\d*)(?:\.([xX*0]|[1-9]\d*)(?:\.([xX*0]|[1-9]\d*)(?:-([a-z0-9-.]+))?(?:\+([a-z0-9-.]+))?)?)?$/i; +// https://github.com/npm/node-semver#range-grammar +// +// hyphen ::= partial ' - ' partial +/* @internal */ +const hyphenRegExp = /^\s*([a-z0-9-+.*]+)\s+-\s+([a-z0-9-+.*]+)\s*$/i; +// https://github.com/npm/node-semver#range-grammar +// +// simple ::= primitive | partial | tilde | caret +// primitive ::= ( '<' | '>' | '>=' | '<=' | '=' ) partial +// tilde ::= '~' partial +// caret ::= '^' partial +/* @internal */ +const rangeRegExp = /^\s*(~|\^|<|<=|>|>=|=)?\s*([a-z0-9-+.*]+)$/i; +/* @internal */ +function parseRange(text: string) { + const alternatives: Comparator[][] = []; + for (const range of text.trim().split(logicalOrRegExp)) { + if (!range) + continue; + const comparators: Comparator[] = []; + const match = hyphenRegExp.exec(range); + if (match) { + if (!parseHyphen(match[1], match[2], comparators)) + return undefined; } - else if (operator === "<" || operator === ">") { - comparators.push(createComparator("<", Version.zero)); + else { + for (const simple of range.split(whitespaceRegExp)) { + const match = rangeRegExp.exec(simple); + if (!match || !parseComparator(match[1], match[2], comparators)) + return undefined; + } } - - return true; - } - - function isWildcard(part: string) { - return part === "*" || part === "x" || part === "X"; - } - - function createComparator(operator: Comparator["operator"], operand: Version) { - return { operator, operand }; + alternatives.push(comparators); } - - function testDisjunction(version: Version, alternatives: readonly (readonly Comparator[])[]) { - // an empty disjunction is treated as "*" (all versions) - if (alternatives.length === 0) return true; - for (const alternative of alternatives) { - if (testAlternative(version, alternative)) return true; - } + return alternatives; +} +/* @internal */ +function parsePartial(text: string) { + const match = partialRegExp.exec(text); + if (!match) + return undefined; + const [, major, minor = "*", patch = "*", prerelease, build] = match; + const version = new Version(isWildcard(major) ? 0 : parseInt(major, 10), isWildcard(major) || isWildcard(minor) ? 0 : parseInt(minor, 10), isWildcard(major) || isWildcard(minor) || isWildcard(patch) ? 0 : parseInt(patch, 10), prerelease, build); + return { version, major, minor, patch }; +} +/* @internal */ +function parseHyphen(left: string, right: string, comparators: Comparator[]) { + const leftResult = parsePartial(left); + if (!leftResult) + return false; + const rightResult = parsePartial(right); + if (!rightResult) return false; + if (!isWildcard(leftResult.major)) { + comparators.push(createComparator(">=", leftResult.version)); } - - function testAlternative(version: Version, comparators: readonly Comparator[]) { - for (const comparator of comparators) { - if (!testComparator(version, comparator.operator, comparator.operand)) return false; - } - return true; + if (!isWildcard(rightResult.major)) { + comparators.push(isWildcard(rightResult.minor) ? createComparator("<", rightResult.version.increment("major")) : + isWildcard(rightResult.patch) ? createComparator("<", rightResult.version.increment("minor")) : + createComparator("<=", rightResult.version)); } - - function testComparator(version: Version, operator: Comparator["operator"], operand: Version) { - const cmp = version.compareTo(operand); + return true; +} +/* @internal */ +function parseComparator(operator: string, text: string, comparators: Comparator[]) { + const result = parsePartial(text); + if (!result) + return false; + const { version, major, minor, patch } = result; + if (!isWildcard(major)) { switch (operator) { - case "<": return cmp < 0; - case "<=": return cmp <= 0; - case ">": return cmp > 0; - case ">=": return cmp >= 0; - case "=": return cmp === 0; - default: return Debug.assertNever(operator); + case "~": + comparators.push(createComparator(">=", version)); + comparators.push(createComparator("<", version.increment(isWildcard(minor) ? "major" : + "minor"))); + break; + case "^": + comparators.push(createComparator(">=", version)); + comparators.push(createComparator("<", version.increment(version.major > 0 || isWildcard(minor) ? "major" : + version.minor > 0 || isWildcard(patch) ? "minor" : + "patch"))); + break; + case "<": + case ">=": + comparators.push(createComparator(operator, version)); + break; + case "<=": + case ">": + comparators.push(isWildcard(minor) ? createComparator(operator === "<=" ? "<" : ">=", version.increment("major")) : + isWildcard(patch) ? createComparator(operator === "<=" ? "<" : ">=", version.increment("minor")) : + createComparator(operator, version)); + break; + case "=": + case undefined: + if (isWildcard(minor) || isWildcard(patch)) { + comparators.push(createComparator(">=", version)); + comparators.push(createComparator("<", version.increment(isWildcard(minor) ? "major" : "minor"))); + } + else { + comparators.push(createComparator("=", version)); + } + break; + default: + // unrecognized + return false; } } - - function formatDisjunction(alternatives: readonly (readonly Comparator[])[]) { - return map(alternatives, formatAlternative).join(" || ") || "*"; + else if (operator === "<" || operator === ">") { + comparators.push(createComparator("<", Version.zero)); + } + return true; +} +/* @internal */ +function isWildcard(part: string) { + return part === "*" || part === "x" || part === "X"; +} +/* @internal */ +function createComparator(operator: Comparator["operator"], operand: Version) { + return { operator, operand }; +} +/* @internal */ +function testDisjunction(version: Version, alternatives: readonly (readonly Comparator[])[]) { + // an empty disjunction is treated as "*" (all versions) + if (alternatives.length === 0) + return true; + for (const alternative of alternatives) { + if (testAlternative(version, alternative)) + return true; } - - function formatAlternative(comparators: readonly Comparator[]) { - return map(comparators, formatComparator).join(" "); + return false; +} +/* @internal */ +function testAlternative(version: Version, comparators: readonly Comparator[]) { + for (const comparator of comparators) { + if (!testComparator(version, comparator.operator, comparator.operand)) + return false; } - - function formatComparator(comparator: Comparator) { - return `${comparator.operator}${comparator.operand}`; + return true; +} +/* @internal */ +function testComparator(version: Version, operator: Comparator["operator"], operand: Version) { + const cmp = version.compareTo(operand); + switch (operator) { + case "<": return cmp < 0; + case "<=": return cmp <= 0; + case ">": return cmp > 0; + case ">=": return cmp >= 0; + case "=": return cmp === 0; + default: return Debug.assertNever(operator); } -} \ No newline at end of file +} +/* @internal */ +function formatDisjunction(alternatives: readonly (readonly Comparator[])[]) { + return map(alternatives, formatAlternative).join(" || ") || "*"; +} +/* @internal */ +function formatAlternative(comparators: readonly Comparator[]) { + return map(comparators, formatComparator).join(" "); +} +/* @internal */ +function formatComparator(comparator: Comparator) { + return `${comparator.operator}${comparator.operand}`; +} diff --git a/src/compiler/sourcemap.ts b/src/compiler/sourcemap.ts index 5fae21601a388..d7d1382663aca 100644 --- a/src/compiler/sourcemap.ts +++ b/src/compiler/sourcemap.ts @@ -1,743 +1,695 @@ +import { EmitHost, SourceMapGenerator, createMap, getRelativePathToDirectoryOrUrl, Debug, RawSourceMap, LineAndCharacter, combinePaths, getDirectoryPath, isArray, every, isString, CharacterCodes, compareValues, DocumentPositionMapperHost, DocumentPositionMapper, getNormalizedAbsolutePath, createMapFromEntries, SortedReadonlyArray, getPositionOfLineAndCharacter, arrayFrom, emptyArray, sortAndDeduplicate, DocumentPosition, some, binarySearchKey, identity } from "./ts"; +import { createTimer, nullTimer } from "./ts.performance"; +import * as ts from "./ts"; /* @internal */ -namespace ts { - export interface SourceMapGeneratorOptions { - extendedDiagnostics?: boolean; - } - - export function createSourceMapGenerator(host: EmitHost, file: string, sourceRoot: string, sourcesDirectoryPath: string, generatorOptions: SourceMapGeneratorOptions): SourceMapGenerator { - const { enter, exit } = generatorOptions.extendedDiagnostics - ? performance.createTimer("Source Map", "beforeSourcemap", "afterSourcemap") - : performance.nullTimer; - - // Current source map file and its index in the sources list - const rawSources: string[] = []; - const sources: string[] = []; - const sourceToSourceIndexMap = createMap(); - let sourcesContent: (string | null)[] | undefined; - - const names: string[] = []; - let nameToNameIndexMap: Map | undefined; - let mappings = ""; - - // Last recorded and encoded mappings - let lastGeneratedLine = 0; - let lastGeneratedCharacter = 0; - let lastSourceIndex = 0; - let lastSourceLine = 0; - let lastSourceCharacter = 0; - let lastNameIndex = 0; - let hasLast = false; - - let pendingGeneratedLine = 0; - let pendingGeneratedCharacter = 0; - let pendingSourceIndex = 0; - let pendingSourceLine = 0; - let pendingSourceCharacter = 0; - let pendingNameIndex = 0; - let hasPending = false; - let hasPendingSource = false; - let hasPendingName = false; - - return { - getSources: () => rawSources, - addSource, - setSourceContent, - addName, - addMapping, - appendSourceMap, - toJSON, - toString: () => JSON.stringify(toJSON()) - }; - - function addSource(fileName: string) { - enter(); - const source = getRelativePathToDirectoryOrUrl(sourcesDirectoryPath, - fileName, - host.getCurrentDirectory(), - host.getCanonicalFileName, - /*isAbsolutePathAnUrl*/ true); - - let sourceIndex = sourceToSourceIndexMap.get(source); - if (sourceIndex === undefined) { - sourceIndex = sources.length; - sources.push(source); - rawSources.push(fileName); - sourceToSourceIndexMap.set(source, sourceIndex); - } - exit(); - return sourceIndex; +export interface SourceMapGeneratorOptions { + extendedDiagnostics?: boolean; +} +/* @internal */ +export function createSourceMapGenerator(host: EmitHost, file: string, sourceRoot: string, sourcesDirectoryPath: string, generatorOptions: SourceMapGeneratorOptions): SourceMapGenerator { + const { enter, exit } = generatorOptions.extendedDiagnostics + ? createTimer("Source Map", "beforeSourcemap", "afterSourcemap") + : nullTimer; + // Current source map file and its index in the sources list + const rawSources: string[] = []; + const sources: string[] = []; + const sourceToSourceIndexMap = createMap(); + let sourcesContent: (string | null)[] | undefined; + const names: string[] = []; + let nameToNameIndexMap: ts.Map | undefined; + let mappings = ""; + // Last recorded and encoded mappings + let lastGeneratedLine = 0; + let lastGeneratedCharacter = 0; + let lastSourceIndex = 0; + let lastSourceLine = 0; + let lastSourceCharacter = 0; + let lastNameIndex = 0; + let hasLast = false; + let pendingGeneratedLine = 0; + let pendingGeneratedCharacter = 0; + let pendingSourceIndex = 0; + let pendingSourceLine = 0; + let pendingSourceCharacter = 0; + let pendingNameIndex = 0; + let hasPending = false; + let hasPendingSource = false; + let hasPendingName = false; + return { + getSources: () => rawSources, + addSource, + setSourceContent, + addName, + addMapping, + appendSourceMap, + toJSON, + toString: () => JSON.stringify(toJSON()) + }; + function addSource(fileName: string) { + enter(); + const source = getRelativePathToDirectoryOrUrl(sourcesDirectoryPath, fileName, host.getCurrentDirectory(), host.getCanonicalFileName, + /*isAbsolutePathAnUrl*/ true); + let sourceIndex = sourceToSourceIndexMap.get(source); + if (sourceIndex === undefined) { + sourceIndex = sources.length; + sources.push(source); + rawSources.push(fileName); + sourceToSourceIndexMap.set(source, sourceIndex); } - - /* eslint-disable boolean-trivia, no-null/no-null */ - function setSourceContent(sourceIndex: number, content: string | null) { - enter(); - if (content !== null) { - if (!sourcesContent) sourcesContent = []; - while (sourcesContent.length < sourceIndex) { - sourcesContent.push(null); - } - sourcesContent[sourceIndex] = content; + exit(); + return sourceIndex; + } + /* eslint-disable boolean-trivia, no-null/no-null */ + function setSourceContent(sourceIndex: number, content: string | null) { + enter(); + if (content !== null) { + if (!sourcesContent) + sourcesContent = []; + while (sourcesContent.length < sourceIndex) { + sourcesContent.push(null); } - exit(); + sourcesContent[sourceIndex] = content; } - /* eslint-enable boolean-trivia, no-null/no-null */ - - function addName(name: string) { - enter(); - if (!nameToNameIndexMap) nameToNameIndexMap = createMap(); - let nameIndex = nameToNameIndexMap.get(name); - if (nameIndex === undefined) { - nameIndex = names.length; - names.push(name); - nameToNameIndexMap.set(name, nameIndex); - } - exit(); - return nameIndex; + exit(); + } + /* eslint-enable boolean-trivia, no-null/no-null */ + function addName(name: string) { + enter(); + if (!nameToNameIndexMap) + nameToNameIndexMap = createMap(); + let nameIndex = nameToNameIndexMap.get(name); + if (nameIndex === undefined) { + nameIndex = names.length; + names.push(name); + nameToNameIndexMap.set(name, nameIndex); } - - function isNewGeneratedPosition(generatedLine: number, generatedCharacter: number) { - return !hasPending - || pendingGeneratedLine !== generatedLine - || pendingGeneratedCharacter !== generatedCharacter; + exit(); + return nameIndex; + } + function isNewGeneratedPosition(generatedLine: number, generatedCharacter: number) { + return !hasPending + || pendingGeneratedLine !== generatedLine + || pendingGeneratedCharacter !== generatedCharacter; + } + function isBacktrackingSourcePosition(sourceIndex: number | undefined, sourceLine: number | undefined, sourceCharacter: number | undefined) { + return sourceIndex !== undefined + && sourceLine !== undefined + && sourceCharacter !== undefined + && pendingSourceIndex === sourceIndex + && (pendingSourceLine > sourceLine + || pendingSourceLine === sourceLine && pendingSourceCharacter > sourceCharacter); + } + function addMapping(generatedLine: number, generatedCharacter: number, sourceIndex?: number, sourceLine?: number, sourceCharacter?: number, nameIndex?: number) { + Debug.assert(generatedLine >= pendingGeneratedLine, "generatedLine cannot backtrack"); + Debug.assert(generatedCharacter >= 0, "generatedCharacter cannot be negative"); + Debug.assert(sourceIndex === undefined || sourceIndex >= 0, "sourceIndex cannot be negative"); + Debug.assert(sourceLine === undefined || sourceLine >= 0, "sourceLine cannot be negative"); + Debug.assert(sourceCharacter === undefined || sourceCharacter >= 0, "sourceCharacter cannot be negative"); + enter(); + // If this location wasn't recorded or the location in source is going backwards, record the mapping + if (isNewGeneratedPosition(generatedLine, generatedCharacter) || + isBacktrackingSourcePosition(sourceIndex, sourceLine, sourceCharacter)) { + commitPendingMapping(); + pendingGeneratedLine = generatedLine; + pendingGeneratedCharacter = generatedCharacter; + hasPendingSource = false; + hasPendingName = false; + hasPending = true; } - - function isBacktrackingSourcePosition(sourceIndex: number | undefined, sourceLine: number | undefined, sourceCharacter: number | undefined) { - return sourceIndex !== undefined - && sourceLine !== undefined - && sourceCharacter !== undefined - && pendingSourceIndex === sourceIndex - && (pendingSourceLine > sourceLine - || pendingSourceLine === sourceLine && pendingSourceCharacter > sourceCharacter); + if (sourceIndex !== undefined && sourceLine !== undefined && sourceCharacter !== undefined) { + pendingSourceIndex = sourceIndex; + pendingSourceLine = sourceLine; + pendingSourceCharacter = sourceCharacter; + hasPendingSource = true; + if (nameIndex !== undefined) { + pendingNameIndex = nameIndex; + hasPendingName = true; + } } - - function addMapping(generatedLine: number, generatedCharacter: number, sourceIndex?: number, sourceLine?: number, sourceCharacter?: number, nameIndex?: number) { - Debug.assert(generatedLine >= pendingGeneratedLine, "generatedLine cannot backtrack"); - Debug.assert(generatedCharacter >= 0, "generatedCharacter cannot be negative"); - Debug.assert(sourceIndex === undefined || sourceIndex >= 0, "sourceIndex cannot be negative"); - Debug.assert(sourceLine === undefined || sourceLine >= 0, "sourceLine cannot be negative"); - Debug.assert(sourceCharacter === undefined || sourceCharacter >= 0, "sourceCharacter cannot be negative"); - enter(); - // If this location wasn't recorded or the location in source is going backwards, record the mapping - if (isNewGeneratedPosition(generatedLine, generatedCharacter) || - isBacktrackingSourcePosition(sourceIndex, sourceLine, sourceCharacter)) { - commitPendingMapping(); - pendingGeneratedLine = generatedLine; - pendingGeneratedCharacter = generatedCharacter; - hasPendingSource = false; - hasPendingName = false; - hasPending = true; + exit(); + } + function appendSourceMap(generatedLine: number, generatedCharacter: number, map: RawSourceMap, sourceMapPath: string, start?: LineAndCharacter, end?: LineAndCharacter) { + Debug.assert(generatedLine >= pendingGeneratedLine, "generatedLine cannot backtrack"); + Debug.assert(generatedCharacter >= 0, "generatedCharacter cannot be negative"); + enter(); + // First, decode the old component sourcemap + const sourceIndexToNewSourceIndexMap: number[] = []; + let nameIndexToNewNameIndexMap: number[] | undefined; + const mappingIterator = decodeMappings(map.mappings); + for (let iterResult = mappingIterator.next(); !iterResult.done; iterResult = mappingIterator.next()) { + const raw = iterResult.value; + if (end && (raw.generatedLine > end.line || + (raw.generatedLine === end.line && raw.generatedCharacter > end.character))) { + break; } - - if (sourceIndex !== undefined && sourceLine !== undefined && sourceCharacter !== undefined) { - pendingSourceIndex = sourceIndex; - pendingSourceLine = sourceLine; - pendingSourceCharacter = sourceCharacter; - hasPendingSource = true; - if (nameIndex !== undefined) { - pendingNameIndex = nameIndex; - hasPendingName = true; - } + if (start && (raw.generatedLine < start.line || + (start.line === raw.generatedLine && raw.generatedCharacter < start.character))) { + continue; } - exit(); - } - - function appendSourceMap(generatedLine: number, generatedCharacter: number, map: RawSourceMap, sourceMapPath: string, start?: LineAndCharacter, end?: LineAndCharacter) { - Debug.assert(generatedLine >= pendingGeneratedLine, "generatedLine cannot backtrack"); - Debug.assert(generatedCharacter >= 0, "generatedCharacter cannot be negative"); - enter(); - // First, decode the old component sourcemap - const sourceIndexToNewSourceIndexMap: number[] = []; - let nameIndexToNewNameIndexMap: number[] | undefined; - const mappingIterator = decodeMappings(map.mappings); - for (let iterResult = mappingIterator.next(); !iterResult.done; iterResult = mappingIterator.next()) { - const raw = iterResult.value; - if (end && ( - raw.generatedLine > end.line || - (raw.generatedLine === end.line && raw.generatedCharacter > end.character))) { - break; - } - - if (start && ( - raw.generatedLine < start.line || - (start.line === raw.generatedLine && raw.generatedCharacter < start.character))) { - continue; - } - // Then reencode all the updated mappings into the overall map - let newSourceIndex: number | undefined; - let newSourceLine: number | undefined; - let newSourceCharacter: number | undefined; - let newNameIndex: number | undefined; - if (raw.sourceIndex !== undefined) { - newSourceIndex = sourceIndexToNewSourceIndexMap[raw.sourceIndex]; - if (newSourceIndex === undefined) { - // Apply offsets to each position and fixup source entries - const rawPath = map.sources[raw.sourceIndex]; - const relativePath = map.sourceRoot ? combinePaths(map.sourceRoot, rawPath) : rawPath; - const combinedPath = combinePaths(getDirectoryPath(sourceMapPath), relativePath); - sourceIndexToNewSourceIndexMap[raw.sourceIndex] = newSourceIndex = addSource(combinedPath); - if (map.sourcesContent && typeof map.sourcesContent[raw.sourceIndex] === "string") { - setSourceContent(newSourceIndex, map.sourcesContent[raw.sourceIndex]); - } + // Then reencode all the updated mappings into the overall map + let newSourceIndex: number | undefined; + let newSourceLine: number | undefined; + let newSourceCharacter: number | undefined; + let newNameIndex: number | undefined; + if (raw.sourceIndex !== undefined) { + newSourceIndex = sourceIndexToNewSourceIndexMap[raw.sourceIndex]; + if (newSourceIndex === undefined) { + // Apply offsets to each position and fixup source entries + const rawPath = map.sources[raw.sourceIndex]; + const relativePath = map.sourceRoot ? combinePaths(map.sourceRoot, rawPath) : rawPath; + const combinedPath = combinePaths(getDirectoryPath(sourceMapPath), relativePath); + sourceIndexToNewSourceIndexMap[raw.sourceIndex] = newSourceIndex = addSource(combinedPath); + if (map.sourcesContent && typeof map.sourcesContent[raw.sourceIndex] === "string") { + setSourceContent(newSourceIndex, map.sourcesContent[raw.sourceIndex]); } - - newSourceLine = raw.sourceLine; - newSourceCharacter = raw.sourceCharacter; - if (map.names && raw.nameIndex !== undefined) { - if (!nameIndexToNewNameIndexMap) nameIndexToNewNameIndexMap = []; - newNameIndex = nameIndexToNewNameIndexMap[raw.nameIndex]; - if (newNameIndex === undefined) { - nameIndexToNewNameIndexMap[raw.nameIndex] = newNameIndex = addName(map.names[raw.nameIndex]); - } + } + newSourceLine = raw.sourceLine; + newSourceCharacter = raw.sourceCharacter; + if (map.names && raw.nameIndex !== undefined) { + if (!nameIndexToNewNameIndexMap) + nameIndexToNewNameIndexMap = []; + newNameIndex = nameIndexToNewNameIndexMap[raw.nameIndex]; + if (newNameIndex === undefined) { + nameIndexToNewNameIndexMap[raw.nameIndex] = newNameIndex = addName(map.names[raw.nameIndex]); } } - - const rawGeneratedLine = raw.generatedLine - (start ? start.line : 0); - const newGeneratedLine = rawGeneratedLine + generatedLine; - const rawGeneratedCharacter = start && start.line === raw.generatedLine ? raw.generatedCharacter - start.character : raw.generatedCharacter; - const newGeneratedCharacter = rawGeneratedLine === 0 ? rawGeneratedCharacter + generatedCharacter : rawGeneratedCharacter; - addMapping(newGeneratedLine, newGeneratedCharacter, newSourceIndex, newSourceLine, newSourceCharacter, newNameIndex); } - exit(); + const rawGeneratedLine = raw.generatedLine - (start ? start.line : 0); + const newGeneratedLine = rawGeneratedLine + generatedLine; + const rawGeneratedCharacter = start && start.line === raw.generatedLine ? raw.generatedCharacter - start.character : raw.generatedCharacter; + const newGeneratedCharacter = rawGeneratedLine === 0 ? rawGeneratedCharacter + generatedCharacter : rawGeneratedCharacter; + addMapping(newGeneratedLine, newGeneratedCharacter, newSourceIndex, newSourceLine, newSourceCharacter, newNameIndex); } - - function shouldCommitMapping() { - return !hasLast - || lastGeneratedLine !== pendingGeneratedLine - || lastGeneratedCharacter !== pendingGeneratedCharacter - || lastSourceIndex !== pendingSourceIndex - || lastSourceLine !== pendingSourceLine - || lastSourceCharacter !== pendingSourceCharacter - || lastNameIndex !== pendingNameIndex; + exit(); + } + function shouldCommitMapping() { + return !hasLast + || lastGeneratedLine !== pendingGeneratedLine + || lastGeneratedCharacter !== pendingGeneratedCharacter + || lastSourceIndex !== pendingSourceIndex + || lastSourceLine !== pendingSourceLine + || lastSourceCharacter !== pendingSourceCharacter + || lastNameIndex !== pendingNameIndex; + } + function commitPendingMapping() { + if (!hasPending || !shouldCommitMapping()) { + return; } - - function commitPendingMapping() { - if (!hasPending || !shouldCommitMapping()) { - return; - } - - enter(); - - // Line/Comma delimiters - if (lastGeneratedLine < pendingGeneratedLine) { - // Emit line delimiters - do { - mappings += ";"; - lastGeneratedLine++; - lastGeneratedCharacter = 0; - } - while (lastGeneratedLine < pendingGeneratedLine); - } - else { - Debug.assertEqual(lastGeneratedLine, pendingGeneratedLine, "generatedLine cannot backtrack"); - // Emit comma to separate the entry - if (hasLast) { - mappings += ","; - } - } - - // 1. Relative generated character - mappings += base64VLQFormatEncode(pendingGeneratedCharacter - lastGeneratedCharacter); - lastGeneratedCharacter = pendingGeneratedCharacter; - - if (hasPendingSource) { - // 2. Relative sourceIndex - mappings += base64VLQFormatEncode(pendingSourceIndex - lastSourceIndex); - lastSourceIndex = pendingSourceIndex; - - // 3. Relative source line - mappings += base64VLQFormatEncode(pendingSourceLine - lastSourceLine); - lastSourceLine = pendingSourceLine; - - // 4. Relative source character - mappings += base64VLQFormatEncode(pendingSourceCharacter - lastSourceCharacter); - lastSourceCharacter = pendingSourceCharacter; - - if (hasPendingName) { - // 5. Relative nameIndex - mappings += base64VLQFormatEncode(pendingNameIndex - lastNameIndex); - lastNameIndex = pendingNameIndex; - } + enter(); + // Line/Comma delimiters + if (lastGeneratedLine < pendingGeneratedLine) { + // Emit line delimiters + do { + mappings += ";"; + lastGeneratedLine++; + lastGeneratedCharacter = 0; + } while (lastGeneratedLine < pendingGeneratedLine); + } + else { + Debug.assertEqual(lastGeneratedLine, pendingGeneratedLine, "generatedLine cannot backtrack"); + // Emit comma to separate the entry + if (hasLast) { + mappings += ","; } - - hasLast = true; - exit(); } - - function toJSON(): RawSourceMap { - commitPendingMapping(); - return { - version: 3, - file, - sourceRoot, - sources, - names, - mappings, - sourcesContent, - }; + // 1. Relative generated character + mappings += base64VLQFormatEncode(pendingGeneratedCharacter - lastGeneratedCharacter); + lastGeneratedCharacter = pendingGeneratedCharacter; + if (hasPendingSource) { + // 2. Relative sourceIndex + mappings += base64VLQFormatEncode(pendingSourceIndex - lastSourceIndex); + lastSourceIndex = pendingSourceIndex; + // 3. Relative source line + mappings += base64VLQFormatEncode(pendingSourceLine - lastSourceLine); + lastSourceLine = pendingSourceLine; + // 4. Relative source character + mappings += base64VLQFormatEncode(pendingSourceCharacter - lastSourceCharacter); + lastSourceCharacter = pendingSourceCharacter; + if (hasPendingName) { + // 5. Relative nameIndex + mappings += base64VLQFormatEncode(pendingNameIndex - lastNameIndex); + lastNameIndex = pendingNameIndex; + } } + hasLast = true; + exit(); } - - // Sometimes tools can see the following line as a source mapping url comment, so we mangle it a bit (the [M]) - const sourceMapCommentRegExp = /^\/\/[@#] source[M]appingURL=(.+)\s*$/; - const whitespaceOrMapCommentRegExp = /^\s*(\/\/[@#] .*)?$/; - - export interface LineInfo { - getLineCount(): number; - getLineText(line: number): string; - } - - export function getLineInfo(text: string, lineStarts: readonly number[]): LineInfo { + function toJSON(): RawSourceMap { + commitPendingMapping(); return { - getLineCount: () => lineStarts.length, - getLineText: line => text.substring(lineStarts[line], lineStarts[line + 1]) + version: 3, + file, + sourceRoot, + sources, + names, + mappings, + sourcesContent, }; } - - /** - * Tries to find the sourceMappingURL comment at the end of a file. - */ - export function tryGetSourceMappingURL(lineInfo: LineInfo) { - for (let index = lineInfo.getLineCount() - 1; index >= 0; index--) { - const line = lineInfo.getLineText(index); - const comment = sourceMapCommentRegExp.exec(line); - if (comment) { - return comment[1]; - } - // If we see a non-whitespace/map comment-like line, break, to avoid scanning up the entire file - else if (!line.match(whitespaceOrMapCommentRegExp)) { - break; - } - } - } - - /* eslint-disable no-null/no-null */ - function isStringOrNull(x: any) { - return typeof x === "string" || x === null; - } - - export function isRawSourceMap(x: any): x is RawSourceMap { - return x !== null - && typeof x === "object" - && x.version === 3 - && typeof x.file === "string" - && typeof x.mappings === "string" - && isArray(x.sources) && every(x.sources, isString) - && (x.sourceRoot === undefined || x.sourceRoot === null || typeof x.sourceRoot === "string") - && (x.sourcesContent === undefined || x.sourcesContent === null || isArray(x.sourcesContent) && every(x.sourcesContent, isStringOrNull)) - && (x.names === undefined || x.names === null || isArray(x.names) && every(x.names, isString)); - } - /* eslint-enable no-null/no-null */ - - export function tryParseRawSourceMap(text: string) { - try { - const parsed = JSON.parse(text); - if (isRawSourceMap(parsed)) { - return parsed; - } +} +// Sometimes tools can see the following line as a source mapping url comment, so we mangle it a bit (the [M]) +/* @internal */ +const sourceMapCommentRegExp = /^\/\/[@#] source[M]appingURL=(.+)\s*$/; +/* @internal */ +const whitespaceOrMapCommentRegExp = /^\s*(\/\/[@#] .*)?$/; +/* @internal */ +export interface LineInfo { + getLineCount(): number; + getLineText(line: number): string; +} +/* @internal */ +export function getLineInfo(text: string, lineStarts: readonly number[]): LineInfo { + return { + getLineCount: () => lineStarts.length, + getLineText: line => text.substring(lineStarts[line], lineStarts[line + 1]) + }; +} +/** + * Tries to find the sourceMappingURL comment at the end of a file. + */ +/* @internal */ +export function tryGetSourceMappingURL(lineInfo: LineInfo) { + for (let index = lineInfo.getLineCount() - 1; index >= 0; index--) { + const line = lineInfo.getLineText(index); + const comment = sourceMapCommentRegExp.exec(line); + if (comment) { + return comment[1]; } - catch { - // empty + // If we see a non-whitespace/map comment-like line, break, to avoid scanning up the entire file + else if (!line.match(whitespaceOrMapCommentRegExp)) { + break; } - - return undefined; - } - - export interface MappingsDecoder extends Iterator { - readonly pos: number; - readonly error: string | undefined; - readonly state: Required; } - - export interface Mapping { - generatedLine: number; - generatedCharacter: number; - sourceIndex?: number; - sourceLine?: number; - sourceCharacter?: number; - nameIndex?: number; +} +/* eslint-disable no-null/no-null */ +/* @internal */ +function isStringOrNull(x: any) { + return typeof x === "string" || x === null; +} +/* @internal */ +export function isRawSourceMap(x: any): x is RawSourceMap { + return x !== null + && typeof x === "object" + && x.version === 3 + && typeof x.file === "string" + && typeof x.mappings === "string" + && isArray(x.sources) && every(x.sources, isString) + && (x.sourceRoot === undefined || x.sourceRoot === null || typeof x.sourceRoot === "string") + && (x.sourcesContent === undefined || x.sourcesContent === null || isArray(x.sourcesContent) && every(x.sourcesContent, isStringOrNull)) + && (x.names === undefined || x.names === null || isArray(x.names) && every(x.names, isString)); +} +/* eslint-enable no-null/no-null */ +/* @internal */ +export function tryParseRawSourceMap(text: string) { + try { + const parsed = JSON.parse(text); + if (isRawSourceMap(parsed)) { + return parsed; + } } - - export interface SourceMapping extends Mapping { - sourceIndex: number; - sourceLine: number; - sourceCharacter: number; + catch { + // empty } - - export function decodeMappings(mappings: string): MappingsDecoder { - let done = false; - let pos = 0; - let generatedLine = 0; - let generatedCharacter = 0; - let sourceIndex = 0; - let sourceLine = 0; - let sourceCharacter = 0; - let nameIndex = 0; - let error: string | undefined; - - return { - get pos() { return pos; }, - get error() { return error; }, - get state() { return captureMapping(/*hasSource*/ true, /*hasName*/ true); }, - next() { - while (!done && pos < mappings.length) { - const ch = mappings.charCodeAt(pos); - if (ch === CharacterCodes.semicolon) { - // new line - generatedLine++; - generatedCharacter = 0; - pos++; - continue; - } - - if (ch === CharacterCodes.comma) { - // Next entry is on same line - no action needed - pos++; - continue; - } - - let hasSource = false; - let hasName = false; - - generatedCharacter += base64VLQFormatDecode(); - if (hasReportedError()) return stopIterating(); - if (generatedCharacter < 0) return setErrorAndStopIterating("Invalid generatedCharacter found"); - + return undefined; +} +/* @internal */ +export interface MappingsDecoder extends ts.Iterator { + readonly pos: number; + readonly error: string | undefined; + readonly state: Required; +} +/* @internal */ +export interface Mapping { + generatedLine: number; + generatedCharacter: number; + sourceIndex?: number; + sourceLine?: number; + sourceCharacter?: number; + nameIndex?: number; +} +/* @internal */ +export interface SourceMapping extends Mapping { + sourceIndex: number; + sourceLine: number; + sourceCharacter: number; +} +/* @internal */ +export function decodeMappings(mappings: string): MappingsDecoder { + let done = false; + let pos = 0; + let generatedLine = 0; + let generatedCharacter = 0; + let sourceIndex = 0; + let sourceLine = 0; + let sourceCharacter = 0; + let nameIndex = 0; + let error: string | undefined; + return { + get pos() { return pos; }, + get error() { return error; }, + get state() { return captureMapping(/*hasSource*/ true, /*hasName*/ true); }, + next() { + while (!done && pos < mappings.length) { + const ch = mappings.charCodeAt(pos); + if (ch === CharacterCodes.semicolon) { + // new line + generatedLine++; + generatedCharacter = 0; + pos++; + continue; + } + if (ch === CharacterCodes.comma) { + // Next entry is on same line - no action needed + pos++; + continue; + } + let hasSource = false; + let hasName = false; + generatedCharacter += base64VLQFormatDecode(); + if (hasReportedError()) + return stopIterating(); + if (generatedCharacter < 0) + return setErrorAndStopIterating("Invalid generatedCharacter found"); + if (!isSourceMappingSegmentEnd()) { + hasSource = true; + sourceIndex += base64VLQFormatDecode(); + if (hasReportedError()) + return stopIterating(); + if (sourceIndex < 0) + return setErrorAndStopIterating("Invalid sourceIndex found"); + if (isSourceMappingSegmentEnd()) + return setErrorAndStopIterating("Unsupported Format: No entries after sourceIndex"); + sourceLine += base64VLQFormatDecode(); + if (hasReportedError()) + return stopIterating(); + if (sourceLine < 0) + return setErrorAndStopIterating("Invalid sourceLine found"); + if (isSourceMappingSegmentEnd()) + return setErrorAndStopIterating("Unsupported Format: No entries after sourceLine"); + sourceCharacter += base64VLQFormatDecode(); + if (hasReportedError()) + return stopIterating(); + if (sourceCharacter < 0) + return setErrorAndStopIterating("Invalid sourceCharacter found"); if (!isSourceMappingSegmentEnd()) { - hasSource = true; - - sourceIndex += base64VLQFormatDecode(); - if (hasReportedError()) return stopIterating(); - if (sourceIndex < 0) return setErrorAndStopIterating("Invalid sourceIndex found"); - if (isSourceMappingSegmentEnd()) return setErrorAndStopIterating("Unsupported Format: No entries after sourceIndex"); - - sourceLine += base64VLQFormatDecode(); - if (hasReportedError()) return stopIterating(); - if (sourceLine < 0) return setErrorAndStopIterating("Invalid sourceLine found"); - if (isSourceMappingSegmentEnd()) return setErrorAndStopIterating("Unsupported Format: No entries after sourceLine"); - - sourceCharacter += base64VLQFormatDecode(); - if (hasReportedError()) return stopIterating(); - if (sourceCharacter < 0) return setErrorAndStopIterating("Invalid sourceCharacter found"); - - if (!isSourceMappingSegmentEnd()) { - hasName = true; - nameIndex += base64VLQFormatDecode(); - if (hasReportedError()) return stopIterating(); - if (nameIndex < 0) return setErrorAndStopIterating("Invalid nameIndex found"); - - if (!isSourceMappingSegmentEnd()) return setErrorAndStopIterating("Unsupported Error Format: Entries after nameIndex"); - } + hasName = true; + nameIndex += base64VLQFormatDecode(); + if (hasReportedError()) + return stopIterating(); + if (nameIndex < 0) + return setErrorAndStopIterating("Invalid nameIndex found"); + if (!isSourceMappingSegmentEnd()) + return setErrorAndStopIterating("Unsupported Error Format: Entries after nameIndex"); } - - return { value: captureMapping(hasSource, hasName), done }; } - - return stopIterating(); - } - }; - - function captureMapping(hasSource: true, hasName: true): Required; - function captureMapping(hasSource: boolean, hasName: boolean): Mapping; - function captureMapping(hasSource: boolean, hasName: boolean): Mapping { - return { - generatedLine, - generatedCharacter, - sourceIndex: hasSource ? sourceIndex : undefined, - sourceLine: hasSource ? sourceLine : undefined, - sourceCharacter: hasSource ? sourceCharacter : undefined, - nameIndex: hasName ? nameIndex : undefined - }; - } - - function stopIterating(): { value: never, done: true } { - done = true; - return { value: undefined!, done: true }; - } - - function setError(message: string) { - if (error === undefined) { - error = message; + return { value: captureMapping(hasSource, hasName), done }; } - } - - function setErrorAndStopIterating(message: string) { - setError(message); return stopIterating(); } - - function hasReportedError() { - return error !== undefined; + }; + function captureMapping(hasSource: true, hasName: true): Required; + function captureMapping(hasSource: boolean, hasName: boolean): Mapping; + function captureMapping(hasSource: boolean, hasName: boolean): Mapping { + return { + generatedLine, + generatedCharacter, + sourceIndex: hasSource ? sourceIndex : undefined, + sourceLine: hasSource ? sourceLine : undefined, + sourceCharacter: hasSource ? sourceCharacter : undefined, + nameIndex: hasName ? nameIndex : undefined + }; + } + function stopIterating(): { + value: never; + done: true; + } { + done = true; + return { value: undefined!, done: true }; + } + function setError(message: string) { + if (error === undefined) { + error = message; } - - function isSourceMappingSegmentEnd() { - return (pos === mappings.length || - mappings.charCodeAt(pos) === CharacterCodes.comma || - mappings.charCodeAt(pos) === CharacterCodes.semicolon); + } + function setErrorAndStopIterating(message: string) { + setError(message); + return stopIterating(); + } + function hasReportedError() { + return error !== undefined; + } + function isSourceMappingSegmentEnd() { + return (pos === mappings.length || + mappings.charCodeAt(pos) === CharacterCodes.comma || + mappings.charCodeAt(pos) === CharacterCodes.semicolon); + } + function base64VLQFormatDecode(): number { + let moreDigits = true; + let shiftCount = 0; + let value = 0; + for (; moreDigits; pos++) { + if (pos >= mappings.length) + return setError("Error in decoding base64VLQFormatDecode, past the mapping string"), -1; + // 6 digit number + const currentByte = base64FormatDecode(mappings.charCodeAt(pos)); + if (currentByte === -1) + return setError("Invalid character in VLQ"), -1; + // If msb is set, we still have more bits to continue + moreDigits = (currentByte & 32) !== 0; + // least significant 5 bits are the next msbs in the final value. + value = value | ((currentByte & 31) << shiftCount); + shiftCount += 5; } - - function base64VLQFormatDecode(): number { - let moreDigits = true; - let shiftCount = 0; - let value = 0; - - for (; moreDigits; pos++) { - if (pos >= mappings.length) return setError("Error in decoding base64VLQFormatDecode, past the mapping string"), -1; - - // 6 digit number - const currentByte = base64FormatDecode(mappings.charCodeAt(pos)); - if (currentByte === -1) return setError("Invalid character in VLQ"), -1; - - // If msb is set, we still have more bits to continue - moreDigits = (currentByte & 32) !== 0; - - // least significant 5 bits are the next msbs in the final value. - value = value | ((currentByte & 31) << shiftCount); - shiftCount += 5; - } - - // Least significant bit if 1 represents negative and rest of the msb is actual absolute value - if ((value & 1) === 0) { - // + number - value = value >> 1; - } - else { - // - number - value = value >> 1; - value = -value; - } - - return value; + // Least significant bit if 1 represents negative and rest of the msb is actual absolute value + if ((value & 1) === 0) { + // + number + value = value >> 1; + } + else { + // - number + value = value >> 1; + value = -value; } + return value; } - - export function sameMapping(left: T, right: T) { - return left === right - || left.generatedLine === right.generatedLine +} +/* @internal */ +export function sameMapping(left: T, right: T) { + return left === right + || left.generatedLine === right.generatedLine && left.generatedCharacter === right.generatedCharacter && left.sourceIndex === right.sourceIndex && left.sourceLine === right.sourceLine && left.sourceCharacter === right.sourceCharacter && left.nameIndex === right.nameIndex; - } - - export function isSourceMapping(mapping: Mapping): mapping is SourceMapping { - return mapping.sourceIndex !== undefined - && mapping.sourceLine !== undefined - && mapping.sourceCharacter !== undefined; - } - - function base64FormatEncode(value: number) { - return value >= 0 && value < 26 ? CharacterCodes.A + value : - value >= 26 && value < 52 ? CharacterCodes.a + value - 26 : +} +/* @internal */ +export function isSourceMapping(mapping: Mapping): mapping is SourceMapping { + return mapping.sourceIndex !== undefined + && mapping.sourceLine !== undefined + && mapping.sourceCharacter !== undefined; +} +/* @internal */ +function base64FormatEncode(value: number) { + return value >= 0 && value < 26 ? CharacterCodes.A + value : + value >= 26 && value < 52 ? CharacterCodes.a + value - 26 : value >= 52 && value < 62 ? CharacterCodes._0 + value - 52 : - value === 62 ? CharacterCodes.plus : - value === 63 ? CharacterCodes.slash : - Debug.fail(`${value}: not a base64 value`); - } - - function base64FormatDecode(ch: number) { - return ch >= CharacterCodes.A && ch <= CharacterCodes.Z ? ch - CharacterCodes.A : - ch >= CharacterCodes.a && ch <= CharacterCodes.z ? ch - CharacterCodes.a + 26 : + value === 62 ? CharacterCodes.plus : + value === 63 ? CharacterCodes.slash : + Debug.fail(`${value}: not a base64 value`); +} +/* @internal */ +function base64FormatDecode(ch: number) { + return ch >= CharacterCodes.A && ch <= CharacterCodes.Z ? ch - CharacterCodes.A : + ch >= CharacterCodes.a && ch <= CharacterCodes.z ? ch - CharacterCodes.a + 26 : ch >= CharacterCodes._0 && ch <= CharacterCodes._9 ? ch - CharacterCodes._0 + 52 : - ch === CharacterCodes.plus ? 62 : - ch === CharacterCodes.slash ? 63 : - -1; + ch === CharacterCodes.plus ? 62 : + ch === CharacterCodes.slash ? 63 : + -1; +} +/* @internal */ +function base64VLQFormatEncode(inValue: number) { + // Add a new least significant bit that has the sign of the value. + // if negative number the least significant bit that gets added to the number has value 1 + // else least significant bit value that gets added is 0 + // eg. -1 changes to binary : 01 [1] => 3 + // +1 changes to binary : 01 [0] => 2 + if (inValue < 0) { + inValue = ((-inValue) << 1) + 1; } - - function base64VLQFormatEncode(inValue: number) { - // Add a new least significant bit that has the sign of the value. - // if negative number the least significant bit that gets added to the number has value 1 - // else least significant bit value that gets added is 0 - // eg. -1 changes to binary : 01 [1] => 3 - // +1 changes to binary : 01 [0] => 2 - if (inValue < 0) { - inValue = ((-inValue) << 1) + 1; + else { + inValue = inValue << 1; + } + // Encode 5 bits at a time starting from least significant bits + let encodedStr = ""; + do { + let currentDigit = inValue & 31; // 11111 + inValue = inValue >> 5; + if (inValue > 0) { + // There are still more digits to decode, set the msb (6th bit) + currentDigit = currentDigit | 32; } - else { - inValue = inValue << 1; + encodedStr = encodedStr + String.fromCharCode(base64FormatEncode(currentDigit)); + } while (inValue > 0); + return encodedStr; +} +/* @internal */ +interface MappedPosition { + generatedPosition: number; + source: string | undefined; + sourceIndex: number | undefined; + sourcePosition: number | undefined; + nameIndex: number | undefined; +} +/* @internal */ +interface SourceMappedPosition extends MappedPosition { + source: string; + sourceIndex: number; + sourcePosition: number; +} +/* @internal */ +function isSourceMappedPosition(value: MappedPosition): value is SourceMappedPosition { + return value.sourceIndex !== undefined + && value.sourcePosition !== undefined; +} +/* @internal */ +function sameMappedPosition(left: MappedPosition, right: MappedPosition) { + return left.generatedPosition === right.generatedPosition + && left.sourceIndex === right.sourceIndex + && left.sourcePosition === right.sourcePosition; +} +/* @internal */ +function compareSourcePositions(left: SourceMappedPosition, right: SourceMappedPosition) { + // Compares sourcePosition without comparing sourceIndex + // since the mappings are grouped by sourceIndex + Debug.assert(left.sourceIndex === right.sourceIndex); + return compareValues(left.sourcePosition, right.sourcePosition); +} +/* @internal */ +function compareGeneratedPositions(left: MappedPosition, right: MappedPosition) { + return compareValues(left.generatedPosition, right.generatedPosition); +} +/* @internal */ +function getSourcePositionOfMapping(value: SourceMappedPosition) { + return value.sourcePosition; +} +/* @internal */ +function getGeneratedPositionOfMapping(value: MappedPosition) { + return value.generatedPosition; +} +/* @internal */ +export function createDocumentPositionMapper(host: DocumentPositionMapperHost, map: RawSourceMap, mapPath: string): DocumentPositionMapper { + const mapDirectory = getDirectoryPath(mapPath); + const sourceRoot = map.sourceRoot ? getNormalizedAbsolutePath(map.sourceRoot, mapDirectory) : mapDirectory; + const generatedAbsoluteFilePath = getNormalizedAbsolutePath(map.file, mapDirectory); + const generatedFile = host.getSourceFileLike(generatedAbsoluteFilePath); + const sourceFileAbsolutePaths = map.sources.map(source => getNormalizedAbsolutePath(source, sourceRoot)); + const sourceToSourceIndexMap = createMapFromEntries(sourceFileAbsolutePaths.map((source, i) => [host.getCanonicalFileName(source), i] as [string, number])); + let decodedMappings: readonly MappedPosition[] | undefined; + let generatedMappings: SortedReadonlyArray | undefined; + let sourceMappings: readonly SortedReadonlyArray[] | undefined; + return { + getSourcePosition, + getGeneratedPosition + }; + function processMapping(mapping: Mapping): MappedPosition { + const generatedPosition = generatedFile !== undefined + ? getPositionOfLineAndCharacter(generatedFile, mapping.generatedLine, mapping.generatedCharacter, /*allowEdits*/ true) + : -1; + let source: string | undefined; + let sourcePosition: number | undefined; + if (isSourceMapping(mapping)) { + const sourceFile = host.getSourceFileLike(sourceFileAbsolutePaths[mapping.sourceIndex]); + source = map.sources[mapping.sourceIndex]; + sourcePosition = sourceFile !== undefined + ? getPositionOfLineAndCharacter(sourceFile, mapping.sourceLine, mapping.sourceCharacter, /*allowEdits*/ true) + : -1; } - - // Encode 5 bits at a time starting from least significant bits - let encodedStr = ""; - do { - let currentDigit = inValue & 31; // 11111 - inValue = inValue >> 5; - if (inValue > 0) { - // There are still more digits to decode, set the msb (6th bit) - currentDigit = currentDigit | 32; - } - encodedStr = encodedStr + String.fromCharCode(base64FormatEncode(currentDigit)); - } while (inValue > 0); - - return encodedStr; - } - - interface MappedPosition { - generatedPosition: number; - source: string | undefined; - sourceIndex: number | undefined; - sourcePosition: number | undefined; - nameIndex: number | undefined; - } - - interface SourceMappedPosition extends MappedPosition { - source: string; - sourceIndex: number; - sourcePosition: number; - } - - function isSourceMappedPosition(value: MappedPosition): value is SourceMappedPosition { - return value.sourceIndex !== undefined - && value.sourcePosition !== undefined; - } - - function sameMappedPosition(left: MappedPosition, right: MappedPosition) { - return left.generatedPosition === right.generatedPosition - && left.sourceIndex === right.sourceIndex - && left.sourcePosition === right.sourcePosition; - } - - function compareSourcePositions(left: SourceMappedPosition, right: SourceMappedPosition) { - // Compares sourcePosition without comparing sourceIndex - // since the mappings are grouped by sourceIndex - Debug.assert(left.sourceIndex === right.sourceIndex); - return compareValues(left.sourcePosition, right.sourcePosition); - } - - function compareGeneratedPositions(left: MappedPosition, right: MappedPosition) { - return compareValues(left.generatedPosition, right.generatedPosition); - } - - function getSourcePositionOfMapping(value: SourceMappedPosition) { - return value.sourcePosition; - } - - function getGeneratedPositionOfMapping(value: MappedPosition) { - return value.generatedPosition; - } - - export function createDocumentPositionMapper(host: DocumentPositionMapperHost, map: RawSourceMap, mapPath: string): DocumentPositionMapper { - const mapDirectory = getDirectoryPath(mapPath); - const sourceRoot = map.sourceRoot ? getNormalizedAbsolutePath(map.sourceRoot, mapDirectory) : mapDirectory; - const generatedAbsoluteFilePath = getNormalizedAbsolutePath(map.file, mapDirectory); - const generatedFile = host.getSourceFileLike(generatedAbsoluteFilePath); - const sourceFileAbsolutePaths = map.sources.map(source => getNormalizedAbsolutePath(source, sourceRoot)); - const sourceToSourceIndexMap = createMapFromEntries(sourceFileAbsolutePaths.map((source, i) => [host.getCanonicalFileName(source), i] as [string, number])); - let decodedMappings: readonly MappedPosition[] | undefined; - let generatedMappings: SortedReadonlyArray | undefined; - let sourceMappings: readonly SortedReadonlyArray[] | undefined; - return { - getSourcePosition, - getGeneratedPosition + generatedPosition, + source, + sourceIndex: mapping.sourceIndex, + sourcePosition, + nameIndex: mapping.nameIndex }; - - function processMapping(mapping: Mapping): MappedPosition { - const generatedPosition = generatedFile !== undefined - ? getPositionOfLineAndCharacter(generatedFile, mapping.generatedLine, mapping.generatedCharacter, /*allowEdits*/ true) - : -1; - let source: string | undefined; - let sourcePosition: number | undefined; - if (isSourceMapping(mapping)) { - const sourceFile = host.getSourceFileLike(sourceFileAbsolutePaths[mapping.sourceIndex]); - source = map.sources[mapping.sourceIndex]; - sourcePosition = sourceFile !== undefined - ? getPositionOfLineAndCharacter(sourceFile, mapping.sourceLine, mapping.sourceCharacter, /*allowEdits*/ true) - : -1; - } - return { - generatedPosition, - source, - sourceIndex: mapping.sourceIndex, - sourcePosition, - nameIndex: mapping.nameIndex - }; - } - - function getDecodedMappings() { - if (decodedMappings === undefined) { - const decoder = decodeMappings(map.mappings); - const mappings = arrayFrom(decoder, processMapping); - if (decoder.error !== undefined) { - if (host.log) { - host.log(`Encountered error while decoding sourcemap: ${decoder.error}`); - } - decodedMappings = emptyArray; - } - else { - decodedMappings = mappings; + } + function getDecodedMappings() { + if (decodedMappings === undefined) { + const decoder = decodeMappings(map.mappings); + const mappings = arrayFrom(decoder, processMapping); + if (decoder.error !== undefined) { + if (host.log) { + host.log(`Encountered error while decoding sourcemap: ${decoder.error}`); } + decodedMappings = emptyArray; } - return decodedMappings; - } - - function getSourceMappings(sourceIndex: number) { - if (sourceMappings === undefined) { - const lists: SourceMappedPosition[][] = []; - for (const mapping of getDecodedMappings()) { - if (!isSourceMappedPosition(mapping)) continue; - let list = lists[mapping.sourceIndex]; - if (!list) lists[mapping.sourceIndex] = list = []; - list.push(mapping); - } - sourceMappings = lists.map(list => sortAndDeduplicate(list, compareSourcePositions, sameMappedPosition)); + else { + decodedMappings = mappings; } - return sourceMappings[sourceIndex]; } - - function getGeneratedMappings() { - if (generatedMappings === undefined) { - const list: MappedPosition[] = []; - for (const mapping of getDecodedMappings()) { - list.push(mapping); - } - generatedMappings = sortAndDeduplicate(list, compareGeneratedPositions, sameMappedPosition); + return decodedMappings; + } + function getSourceMappings(sourceIndex: number) { + if (sourceMappings === undefined) { + const lists: SourceMappedPosition[][] = []; + for (const mapping of getDecodedMappings()) { + if (!isSourceMappedPosition(mapping)) + continue; + let list = lists[mapping.sourceIndex]; + if (!list) + lists[mapping.sourceIndex] = list = []; + list.push(mapping); } - return generatedMappings; + sourceMappings = lists.map(list => sortAndDeduplicate(list, compareSourcePositions, sameMappedPosition)); } - - function getGeneratedPosition(loc: DocumentPosition): DocumentPosition { - const sourceIndex = sourceToSourceIndexMap.get(host.getCanonicalFileName(loc.fileName)); - if (sourceIndex === undefined) return loc; - - const sourceMappings = getSourceMappings(sourceIndex); - if (!some(sourceMappings)) return loc; - - let targetIndex = binarySearchKey(sourceMappings, loc.pos, getSourcePositionOfMapping, compareValues); - if (targetIndex < 0) { - // if no exact match, closest is 2's complement of result - targetIndex = ~targetIndex; - } - - const mapping = sourceMappings[targetIndex]; - if (mapping === undefined || mapping.sourceIndex !== sourceIndex) { - return loc; + return sourceMappings[sourceIndex]; + } + function getGeneratedMappings() { + if (generatedMappings === undefined) { + const list: MappedPosition[] = []; + for (const mapping of getDecodedMappings()) { + list.push(mapping); } - - return { fileName: generatedAbsoluteFilePath, pos: mapping.generatedPosition }; // Closest pos + generatedMappings = sortAndDeduplicate(list, compareGeneratedPositions, sameMappedPosition); } - - function getSourcePosition(loc: DocumentPosition): DocumentPosition { - const generatedMappings = getGeneratedMappings(); - if (!some(generatedMappings)) return loc; - - let targetIndex = binarySearchKey(generatedMappings, loc.pos, getGeneratedPositionOfMapping, compareValues); - if (targetIndex < 0) { - // if no exact match, closest is 2's complement of result - targetIndex = ~targetIndex; - } - - const mapping = generatedMappings[targetIndex]; - if (mapping === undefined || !isSourceMappedPosition(mapping)) { - return loc; - } - - return { fileName: sourceFileAbsolutePaths[mapping.sourceIndex], pos: mapping.sourcePosition }; // Closest pos + return generatedMappings; + } + function getGeneratedPosition(loc: DocumentPosition): DocumentPosition { + const sourceIndex = sourceToSourceIndexMap.get(host.getCanonicalFileName(loc.fileName)); + if (sourceIndex === undefined) + return loc; + const sourceMappings = getSourceMappings(sourceIndex); + if (!some(sourceMappings)) + return loc; + let targetIndex = binarySearchKey(sourceMappings, loc.pos, getSourcePositionOfMapping, compareValues); + if (targetIndex < 0) { + // if no exact match, closest is 2's complement of result + targetIndex = ~targetIndex; + } + const mapping = sourceMappings[targetIndex]; + if (mapping === undefined || mapping.sourceIndex !== sourceIndex) { + return loc; } + return { fileName: generatedAbsoluteFilePath, pos: mapping.generatedPosition }; // Closest pos + } + function getSourcePosition(loc: DocumentPosition): DocumentPosition { + const generatedMappings = getGeneratedMappings(); + if (!some(generatedMappings)) + return loc; + let targetIndex = binarySearchKey(generatedMappings, loc.pos, getGeneratedPositionOfMapping, compareValues); + if (targetIndex < 0) { + // if no exact match, closest is 2's complement of result + targetIndex = ~targetIndex; + } + const mapping = generatedMappings[targetIndex]; + if (mapping === undefined || !isSourceMappedPosition(mapping)) { + return loc; + } + return { fileName: sourceFileAbsolutePaths[mapping.sourceIndex], pos: mapping.sourcePosition }; // Closest pos } - - export const identitySourceMapConsumer: DocumentPositionMapper = { - getSourcePosition: identity, - getGeneratedPosition: identity - }; } +/* @internal */ +export const identitySourceMapConsumer: DocumentPositionMapper = { + getSourcePosition: identity, + getGeneratedPosition: identity +}; diff --git a/src/compiler/symbolWalker.ts b/src/compiler/symbolWalker.ts index b03ef31db6e11..ed9d63b6a2120 100644 --- a/src/compiler/symbolWalker.ts +++ b/src/compiler/symbolWalker.ts @@ -1,193 +1,163 @@ +import { Signature, Type, TypePredicate, ObjectType, ResolvedType, Symbol, Node, IndexKind, TypeParameter, EntityNameOrEntityNameExpression, Identifier, TypeReference, SymbolWalker, getOwnValues, clear, TypeFlags, ObjectFlags, MappedType, InterfaceType, UnionOrIntersectionType, IndexType, IndexedAccessType, forEach, getSymbolId, SyntaxKind, TypeQueryNode } from "./ts"; /** @internal */ -namespace ts { - export function createGetSymbolWalker( - getRestTypeOfSignature: (sig: Signature) => Type, - getTypePredicateOfSignature: (sig: Signature) => TypePredicate | undefined, - getReturnTypeOfSignature: (sig: Signature) => Type, - getBaseTypes: (type: Type) => Type[], - resolveStructuredTypeMembers: (type: ObjectType) => ResolvedType, - getTypeOfSymbol: (sym: Symbol) => Type, - getResolvedSymbol: (node: Node) => Symbol, - getIndexTypeOfStructuredType: (type: Type, kind: IndexKind) => Type | undefined, - getConstraintOfTypeParameter: (typeParameter: TypeParameter) => Type | undefined, - getFirstIdentifier: (node: EntityNameOrEntityNameExpression) => Identifier, - getTypeArguments: (type: TypeReference) => readonly Type[]) { - - return getSymbolWalker; - - function getSymbolWalker(accept: (symbol: Symbol) => boolean = () => true): SymbolWalker { - const visitedTypes: Type[] = []; // Sparse array from id to type - const visitedSymbols: Symbol[] = []; // Sparse array from id to symbol - - return { - walkType: type => { - try { - visitType(type); - return { visitedTypes: getOwnValues(visitedTypes), visitedSymbols: getOwnValues(visitedSymbols) }; - } - finally { - clear(visitedTypes); - clear(visitedSymbols); - } - }, - walkSymbol: symbol => { - try { - visitSymbol(symbol); - return { visitedTypes: getOwnValues(visitedTypes), visitedSymbols: getOwnValues(visitedSymbols) }; - } - finally { - clear(visitedTypes); - clear(visitedSymbols); - } - }, - }; - - function visitType(type: Type | undefined): void { - if (!type) { - return; - } - - if (visitedTypes[type.id]) { - return; - } - visitedTypes[type.id] = type; - - // Reuse visitSymbol to visit the type's symbol, - // but be sure to bail on recuring into the type if accept declines the symbol. - const shouldBail = visitSymbol(type.symbol); - if (shouldBail) return; - - // Visit the type's related types, if any - if (type.flags & TypeFlags.Object) { - const objectType = type as ObjectType; - const objectFlags = objectType.objectFlags; - if (objectFlags & ObjectFlags.Reference) { - visitTypeReference(type as TypeReference); - } - if (objectFlags & ObjectFlags.Mapped) { - visitMappedType(type as MappedType); - } - if (objectFlags & (ObjectFlags.Class | ObjectFlags.Interface)) { - visitInterfaceType(type as InterfaceType); - } - if (objectFlags & (ObjectFlags.Tuple | ObjectFlags.Anonymous)) { - visitObjectType(objectType); - } - } - if (type.flags & TypeFlags.TypeParameter) { - visitTypeParameter(type as TypeParameter); - } - if (type.flags & TypeFlags.UnionOrIntersection) { - visitUnionOrIntersectionType(type as UnionOrIntersectionType); - } - if (type.flags & TypeFlags.Index) { - visitIndexType(type as IndexType); - } - if (type.flags & TypeFlags.IndexedAccess) { - visitIndexedAccessType(type as IndexedAccessType); - } +export function createGetSymbolWalker(getRestTypeOfSignature: (sig: Signature) => Type, getTypePredicateOfSignature: (sig: Signature) => TypePredicate | undefined, getReturnTypeOfSignature: (sig: Signature) => Type, getBaseTypes: (type: Type) => Type[], resolveStructuredTypeMembers: (type: ObjectType) => ResolvedType, getTypeOfSymbol: (sym: Symbol) => Type, getResolvedSymbol: (node: Node) => Symbol, getIndexTypeOfStructuredType: (type: Type, kind: IndexKind) => Type | undefined, getConstraintOfTypeParameter: (typeParameter: TypeParameter) => Type | undefined, getFirstIdentifier: (node: EntityNameOrEntityNameExpression) => Identifier, getTypeArguments: (type: TypeReference) => readonly Type[]) { + return getSymbolWalker; + function getSymbolWalker(accept: (symbol: Symbol) => boolean = () => true): SymbolWalker { + const visitedTypes: Type[] = []; // Sparse array from id to type + const visitedSymbols: Symbol[] = []; // Sparse array from id to symbol + return { + walkType: type => { + try { + visitType(type); + return { visitedTypes: getOwnValues(visitedTypes), visitedSymbols: getOwnValues(visitedSymbols) }; + } + finally { + clear(visitedTypes); + clear(visitedSymbols); + } + }, + walkSymbol: symbol => { + try { + visitSymbol(symbol); + return { visitedTypes: getOwnValues(visitedTypes), visitedSymbols: getOwnValues(visitedSymbols) }; + } + finally { + clear(visitedTypes); + clear(visitedSymbols); + } + }, + }; + function visitType(type: Type | undefined): void { + if (!type) { + return; } - - function visitTypeReference(type: TypeReference): void { - visitType(type.target); - forEach(getTypeArguments(type), visitType); - } - - function visitTypeParameter(type: TypeParameter): void { - visitType(getConstraintOfTypeParameter(type)); - } - - function visitUnionOrIntersectionType(type: UnionOrIntersectionType): void { - forEach(type.types, visitType); - } - - function visitIndexType(type: IndexType): void { - visitType(type.type); - } - - function visitIndexedAccessType(type: IndexedAccessType): void { - visitType(type.objectType); - visitType(type.indexType); - visitType(type.constraint); - } - - function visitMappedType(type: MappedType): void { - visitType(type.typeParameter); - visitType(type.constraintType); - visitType(type.templateType); - visitType(type.modifiersType); - } - - function visitSignature(signature: Signature): void { - const typePredicate = getTypePredicateOfSignature(signature); - if (typePredicate) { - visitType(typePredicate.type); - } - forEach(signature.typeParameters, visitType); - - for (const parameter of signature.parameters) { - visitSymbol(parameter); - } - visitType(getRestTypeOfSignature(signature)); - visitType(getReturnTypeOfSignature(signature)); - } - - function visitInterfaceType(interfaceT: InterfaceType): void { - visitObjectType(interfaceT); - forEach(interfaceT.typeParameters, visitType); - forEach(getBaseTypes(interfaceT), visitType); - visitType(interfaceT.thisType); - } - - function visitObjectType(type: ObjectType): void { - const stringIndexType = getIndexTypeOfStructuredType(type, IndexKind.String); - visitType(stringIndexType); - const numberIndexType = getIndexTypeOfStructuredType(type, IndexKind.Number); - visitType(numberIndexType); - - // The two checks above *should* have already resolved the type (if needed), so this should be cached - const resolved = resolveStructuredTypeMembers(type); - for (const signature of resolved.callSignatures) { - visitSignature(signature); - } - for (const signature of resolved.constructSignatures) { - visitSignature(signature); - } - for (const p of resolved.properties) { - visitSymbol(p); - } + if (visitedTypes[type.id]) { + return; } - - function visitSymbol(symbol: Symbol | undefined): boolean { - if (!symbol) { - return false; - } - const symbolId = getSymbolId(symbol); - if (visitedSymbols[symbolId]) { - return false; - } - visitedSymbols[symbolId] = symbol; - if (!accept(symbol)) { - return true; - } - const t = getTypeOfSymbol(symbol); - visitType(t); // Should handle members on classes and such - if (symbol.exports) { - symbol.exports.forEach(visitSymbol); + visitedTypes[type.id] = type; + // Reuse visitSymbol to visit the type's symbol, + // but be sure to bail on recuring into the type if accept declines the symbol. + const shouldBail = visitSymbol(type.symbol); + if (shouldBail) + return; + // Visit the type's related types, if any + if (type.flags & TypeFlags.Object) { + const objectType = (type as ObjectType); + const objectFlags = objectType.objectFlags; + if (objectFlags & ObjectFlags.Reference) { + visitTypeReference((type as TypeReference)); + } + if (objectFlags & ObjectFlags.Mapped) { + visitMappedType((type as MappedType)); + } + if (objectFlags & (ObjectFlags.Class | ObjectFlags.Interface)) { + visitInterfaceType((type as InterfaceType)); + } + if (objectFlags & (ObjectFlags.Tuple | ObjectFlags.Anonymous)) { + visitObjectType(objectType); } - forEach(symbol.declarations, d => { - // Type queries are too far resolved when we just visit the symbol's type - // (their type resolved directly to the member deeply referenced) - // So to get the intervening symbols, we need to check if there's a type - // query node on any of the symbol's declarations and get symbols there - if ((d as any).type && (d as any).type.kind === SyntaxKind.TypeQuery) { - const query = (d as any).type as TypeQueryNode; - const entity = getResolvedSymbol(getFirstIdentifier(query.exprName)); - visitSymbol(entity); - } - }); + } + if (type.flags & TypeFlags.TypeParameter) { + visitTypeParameter((type as TypeParameter)); + } + if (type.flags & TypeFlags.UnionOrIntersection) { + visitUnionOrIntersectionType((type as UnionOrIntersectionType)); + } + if (type.flags & TypeFlags.Index) { + visitIndexType((type as IndexType)); + } + if (type.flags & TypeFlags.IndexedAccess) { + visitIndexedAccessType((type as IndexedAccessType)); + } + } + function visitTypeReference(type: TypeReference): void { + visitType(type.target); + forEach(getTypeArguments(type), visitType); + } + function visitTypeParameter(type: TypeParameter): void { + visitType(getConstraintOfTypeParameter(type)); + } + function visitUnionOrIntersectionType(type: UnionOrIntersectionType): void { + forEach(type.types, visitType); + } + function visitIndexType(type: IndexType): void { + visitType(type.type); + } + function visitIndexedAccessType(type: IndexedAccessType): void { + visitType(type.objectType); + visitType(type.indexType); + visitType(type.constraint); + } + function visitMappedType(type: MappedType): void { + visitType(type.typeParameter); + visitType(type.constraintType); + visitType(type.templateType); + visitType(type.modifiersType); + } + function visitSignature(signature: Signature): void { + const typePredicate = getTypePredicateOfSignature(signature); + if (typePredicate) { + visitType(typePredicate.type); + } + forEach(signature.typeParameters, visitType); + for (const parameter of signature.parameters) { + visitSymbol(parameter); + } + visitType(getRestTypeOfSignature(signature)); + visitType(getReturnTypeOfSignature(signature)); + } + function visitInterfaceType(interfaceT: InterfaceType): void { + visitObjectType(interfaceT); + forEach(interfaceT.typeParameters, visitType); + forEach(getBaseTypes(interfaceT), visitType); + visitType(interfaceT.thisType); + } + function visitObjectType(type: ObjectType): void { + const stringIndexType = getIndexTypeOfStructuredType(type, IndexKind.String); + visitType(stringIndexType); + const numberIndexType = getIndexTypeOfStructuredType(type, IndexKind.Number); + visitType(numberIndexType); + // The two checks above *should* have already resolved the type (if needed), so this should be cached + const resolved = resolveStructuredTypeMembers(type); + for (const signature of resolved.callSignatures) { + visitSignature(signature); + } + for (const signature of resolved.constructSignatures) { + visitSignature(signature); + } + for (const p of resolved.properties) { + visitSymbol(p); + } + } + function visitSymbol(symbol: Symbol | undefined): boolean { + if (!symbol) { return false; } + const symbolId = getSymbolId(symbol); + if (visitedSymbols[symbolId]) { + return false; + } + visitedSymbols[symbolId] = symbol; + if (!accept(symbol)) { + return true; + } + const t = getTypeOfSymbol(symbol); + visitType(t); // Should handle members on classes and such + if (symbol.exports) { + symbol.exports.forEach(visitSymbol); + } + forEach(symbol.declarations, d => { + // Type queries are too far resolved when we just visit the symbol's type + // (their type resolved directly to the member deeply referenced) + // So to get the intervening symbols, we need to check if there's a type + // query node on any of the symbol's declarations and get symbols there + if ((d as any).type && (d as any).type.kind === SyntaxKind.TypeQuery) { + const query = ((d as any).type as TypeQueryNode); + const entity = getResolvedSymbol(getFirstIdentifier(query.exprName)); + visitSymbol(entity); + } + }); + return false; } } -} \ No newline at end of file +} diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 6a1f30f557fdf..1d121e79793b5 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -1,1737 +1,1508 @@ +import { WatchOptions, unorderedRemoveItem, Debug, createMultiMap, createMap, createGetCanonicalFileName, getDirectoryPath, isString, getNormalizedAbsolutePath, forEach, closeFileWatcherOf, noop, getStringComparer, Path, emptyArray, closeFileWatcher, startsWith, directorySeparator, timestamp, enumerateInsertsAndDeletes, mapDefined, normalizePath, Comparison, some, stringContains, combinePaths, WatchFileKind, getFallbackOptions, PollingWatchKind, WatchDirectoryKind, writeFileEnsuringDirectories, RequireResult, resolveJSModule, normalizeSlashes, getRootLength, containsPath, getRelativePathToDirectoryOrUrl, perfLogger, FileSystemEntries, emptyFileSystemEntries, matchFiles, AssertionLevel } from "./ts"; +import * as ts from "./ts"; declare function setTimeout(handler: (...args: any[]) => void, timeout: number): any; declare function clearTimeout(handle: any): void; - -namespace ts { - /** - * djb2 hashing algorithm - * http://www.cse.yorku.ca/~oz/hash.html - */ - /* @internal */ - export function generateDjb2Hash(data: string): string { - let acc = 5381; - for (let i = 0; i < data.length; i++) { - acc = ((acc << 5) + acc) + data.charCodeAt(i); - } - return acc.toString(); +/** + * djb2 hashing algorithm + * http://www.cse.yorku.ca/~oz/hash.html + */ +/* @internal */ +export function generateDjb2Hash(data: string): string { + let acc = 5381; + for (let i = 0; i < data.length; i++) { + acc = ((acc << 5) + acc) + data.charCodeAt(i); } - - /** - * Set a high stack trace limit to provide more information in case of an error. - * Called for command-line and server use cases. - * Not called if TypeScript is used as a library. - */ - /* @internal */ - export function setStackTraceLimit() { - if ((Error as any).stackTraceLimit < 100) { // Also tests that we won't set the property if it doesn't exist. - (Error as any).stackTraceLimit = 100; + return acc.toString(); +} +/** + * Set a high stack trace limit to provide more information in case of an error. + * Called for command-line and server use cases. + * Not called if TypeScript is used as a library. + */ +/* @internal */ +export function setStackTraceLimit() { + if ((Error as any).stackTraceLimit < 100) { // Also tests that we won't set the property if it doesn't exist. + (Error as any).stackTraceLimit = 100; + } +} +export enum FileWatcherEventKind { + Created, + Changed, + Deleted +} +export type FileWatcherCallback = (fileName: string, eventKind: FileWatcherEventKind) => void; +export type DirectoryWatcherCallback = (fileName: string) => void; +/*@internal*/ +export interface WatchedFile { + readonly fileName: string; + readonly callback: FileWatcherCallback; + mtime: Date; +} +/* @internal */ +export enum PollingInterval { + High = 2000, + Medium = 500, + Low = 250 +} +/* @internal */ +export type HostWatchFile = (fileName: string, callback: FileWatcherCallback, pollingInterval: PollingInterval, options: WatchOptions | undefined) => FileWatcher; +/* @internal */ +export type HostWatchDirectory = (fileName: string, callback: DirectoryWatcherCallback, recursive: boolean, options: WatchOptions | undefined) => FileWatcher; +/* @internal */ +export const missingFileModifiedTime = new Date(0); // Any subsequent modification will occur after this time +interface Levels { + Low: number; + Medium: number; + High: number; +} +function createPollingIntervalBasedLevels(levels: Levels) { + return { + [PollingInterval.Low]: levels.Low, + [PollingInterval.Medium]: levels.Medium, + [PollingInterval.High]: levels.High + }; +} +const defaultChunkLevels: Levels = { Low: 32, Medium: 64, High: 256 }; +let pollingChunkSize = createPollingIntervalBasedLevels(defaultChunkLevels); +/* @internal */ +export let unchangedPollThresholds = createPollingIntervalBasedLevels(defaultChunkLevels); +/* @internal */ +export function setCustomPollingValues(system: System) { + if (!system.getEnvironmentVariable) { + return; + } + const pollingIntervalChanged = setCustomLevels("TSC_WATCH_POLLINGINTERVAL", PollingInterval); + pollingChunkSize = getCustomPollingBasedLevels("TSC_WATCH_POLLINGCHUNKSIZE", defaultChunkLevels) || pollingChunkSize; + unchangedPollThresholds = getCustomPollingBasedLevels("TSC_WATCH_UNCHANGEDPOLLTHRESHOLDS", defaultChunkLevels) || unchangedPollThresholds; + function getLevel(envVar: string, level: keyof Levels) { + return system.getEnvironmentVariable(`${envVar}_${level.toUpperCase()}`); + } + function getCustomLevels(baseVariable: string) { + let customLevels: Partial | undefined; + setCustomLevel("Low"); + setCustomLevel("Medium"); + setCustomLevel("High"); + return customLevels; + function setCustomLevel(level: keyof Levels) { + const customLevel = getLevel(baseVariable, level); + if (customLevel) { + (customLevels || (customLevels = {}))[level] = Number(customLevel); + } } } - - export enum FileWatcherEventKind { - Created, - Changed, - Deleted + function setCustomLevels(baseVariable: string, levels: Levels) { + const customLevels = getCustomLevels(baseVariable); + if (customLevels) { + setLevel("Low"); + setLevel("Medium"); + setLevel("High"); + return true; + } + return false; + function setLevel(level: keyof Levels) { + levels[level] = customLevels![level] || levels[level]; + } } - - export type FileWatcherCallback = (fileName: string, eventKind: FileWatcherEventKind) => void; - export type DirectoryWatcherCallback = (fileName: string) => void; - /*@internal*/ - export interface WatchedFile { - readonly fileName: string; - readonly callback: FileWatcherCallback; - mtime: Date; + function getCustomPollingBasedLevels(baseVariable: string, defaultLevels: Levels) { + const customLevels = getCustomLevels(baseVariable); + return (pollingIntervalChanged || customLevels) && + createPollingIntervalBasedLevels(customLevels ? { ...defaultLevels, ...customLevels } : defaultLevels); } - - /* @internal */ - export enum PollingInterval { - High = 2000, - Medium = 500, - Low = 250 +} +/* @internal */ +export function createDynamicPriorityPollingWatchFile(host: { + getModifiedTime: NonNullable; + setTimeout: NonNullable; +}): HostWatchFile { + interface WatchedFile extends ts.WatchedFile { + isClosed?: boolean; + unchangedPolls: number; } - - /* @internal */ - export type HostWatchFile = (fileName: string, callback: FileWatcherCallback, pollingInterval: PollingInterval, options: WatchOptions | undefined) => FileWatcher; - /* @internal */ - export type HostWatchDirectory = (fileName: string, callback: DirectoryWatcherCallback, recursive: boolean, options: WatchOptions | undefined) => FileWatcher; - - /* @internal */ - export const missingFileModifiedTime = new Date(0); // Any subsequent modification will occur after this time - - interface Levels { - Low: number; - Medium: number; - High: number; + interface PollingIntervalQueue extends Array { + pollingInterval: PollingInterval; + pollIndex: number; + pollScheduled: boolean; } - - function createPollingIntervalBasedLevels(levels: Levels) { + const watchedFiles: WatchedFile[] = []; + const changedFilesInLastPoll: WatchedFile[] = []; + const lowPollingIntervalQueue = createPollingIntervalQueue(PollingInterval.Low); + const mediumPollingIntervalQueue = createPollingIntervalQueue(PollingInterval.Medium); + const highPollingIntervalQueue = createPollingIntervalQueue(PollingInterval.High); + return watchFile; + function watchFile(fileName: string, callback: FileWatcherCallback, defaultPollingInterval: PollingInterval): FileWatcher { + const file: WatchedFile = { + fileName, + callback, + unchangedPolls: 0, + mtime: getModifiedTime(fileName) + }; + watchedFiles.push(file); + addToPollingIntervalQueue(file, defaultPollingInterval); return { - [PollingInterval.Low]: levels.Low, - [PollingInterval.Medium]: levels.Medium, - [PollingInterval.High]: levels.High + close: () => { + file.isClosed = true; + // Remove from watchedFiles + unorderedRemoveItem(watchedFiles, file); + // Do not update polling interval queue since that will happen as part of polling + } }; } - - const defaultChunkLevels: Levels = { Low: 32, Medium: 64, High: 256 }; - let pollingChunkSize = createPollingIntervalBasedLevels(defaultChunkLevels); - /* @internal */ - export let unchangedPollThresholds = createPollingIntervalBasedLevels(defaultChunkLevels); - - /* @internal */ - export function setCustomPollingValues(system: System) { - if (!system.getEnvironmentVariable) { - return; - } - const pollingIntervalChanged = setCustomLevels("TSC_WATCH_POLLINGINTERVAL", PollingInterval); - pollingChunkSize = getCustomPollingBasedLevels("TSC_WATCH_POLLINGCHUNKSIZE", defaultChunkLevels) || pollingChunkSize; - unchangedPollThresholds = getCustomPollingBasedLevels("TSC_WATCH_UNCHANGEDPOLLTHRESHOLDS", defaultChunkLevels) || unchangedPollThresholds; - - function getLevel(envVar: string, level: keyof Levels) { - return system.getEnvironmentVariable(`${envVar}_${level.toUpperCase()}`); - } - - function getCustomLevels(baseVariable: string) { - let customLevels: Partial | undefined; - setCustomLevel("Low"); - setCustomLevel("Medium"); - setCustomLevel("High"); - return customLevels; - - function setCustomLevel(level: keyof Levels) { - const customLevel = getLevel(baseVariable, level); - if (customLevel) { - (customLevels || (customLevels = {}))[level] = Number(customLevel); - } - } - } - - function setCustomLevels(baseVariable: string, levels: Levels) { - const customLevels = getCustomLevels(baseVariable); - if (customLevels) { - setLevel("Low"); - setLevel("Medium"); - setLevel("High"); - return true; - } - return false; - - function setLevel(level: keyof Levels) { - levels[level] = customLevels![level] || levels[level]; - } - } - - function getCustomPollingBasedLevels(baseVariable: string, defaultLevels: Levels) { - const customLevels = getCustomLevels(baseVariable); - return (pollingIntervalChanged || customLevels) && - createPollingIntervalBasedLevels(customLevels ? { ...defaultLevels, ...customLevels } : defaultLevels); - } + function createPollingIntervalQueue(pollingInterval: PollingInterval): PollingIntervalQueue { + const queue = [] as WatchedFile[] as PollingIntervalQueue; + queue.pollingInterval = pollingInterval; + queue.pollIndex = 0; + queue.pollScheduled = false; + return queue; } - - /* @internal */ - export function createDynamicPriorityPollingWatchFile(host: { - getModifiedTime: NonNullable; - setTimeout: NonNullable; - }): HostWatchFile { - interface WatchedFile extends ts.WatchedFile { - isClosed?: boolean; - unchangedPolls: number; + function pollPollingIntervalQueue(queue: PollingIntervalQueue) { + queue.pollIndex = pollQueue(queue, queue.pollingInterval, queue.pollIndex, pollingChunkSize[queue.pollingInterval]); + // Set the next polling index and timeout + if (queue.length) { + scheduleNextPoll(queue.pollingInterval); } - - interface PollingIntervalQueue extends Array { - pollingInterval: PollingInterval; - pollIndex: number; - pollScheduled: boolean; - } - - const watchedFiles: WatchedFile[] = []; - const changedFilesInLastPoll: WatchedFile[] = []; - const lowPollingIntervalQueue = createPollingIntervalQueue(PollingInterval.Low); - const mediumPollingIntervalQueue = createPollingIntervalQueue(PollingInterval.Medium); - const highPollingIntervalQueue = createPollingIntervalQueue(PollingInterval.High); - return watchFile; - - function watchFile(fileName: string, callback: FileWatcherCallback, defaultPollingInterval: PollingInterval): FileWatcher { - const file: WatchedFile = { - fileName, - callback, - unchangedPolls: 0, - mtime: getModifiedTime(fileName) - }; - watchedFiles.push(file); - - addToPollingIntervalQueue(file, defaultPollingInterval); - return { - close: () => { - file.isClosed = true; - // Remove from watchedFiles - unorderedRemoveItem(watchedFiles, file); - // Do not update polling interval queue since that will happen as part of polling - } - }; - } - - function createPollingIntervalQueue(pollingInterval: PollingInterval): PollingIntervalQueue { - const queue = [] as WatchedFile[] as PollingIntervalQueue; - queue.pollingInterval = pollingInterval; - queue.pollIndex = 0; + else { + Debug.assert(queue.pollIndex === 0); queue.pollScheduled = false; - return queue; } - - function pollPollingIntervalQueue(queue: PollingIntervalQueue) { - queue.pollIndex = pollQueue(queue, queue.pollingInterval, queue.pollIndex, pollingChunkSize[queue.pollingInterval]); - // Set the next polling index and timeout - if (queue.length) { - scheduleNextPoll(queue.pollingInterval); + } + function pollLowPollingIntervalQueue(queue: PollingIntervalQueue) { + // Always poll complete list of changedFilesInLastPoll + pollQueue(changedFilesInLastPoll, PollingInterval.Low, /*pollIndex*/ 0, changedFilesInLastPoll.length); + // Finally do the actual polling of the queue + pollPollingIntervalQueue(queue); + // Schedule poll if there are files in changedFilesInLastPoll but no files in the actual queue + // as pollPollingIntervalQueue wont schedule for next poll + if (!queue.pollScheduled && changedFilesInLastPoll.length) { + scheduleNextPoll(PollingInterval.Low); + } + } + function pollQueue(queue: (WatchedFile | undefined)[], pollingInterval: PollingInterval, pollIndex: number, chunkSize: number) { + // Max visit would be all elements of the queue + let needsVisit = queue.length; + let definedValueCopyToIndex = pollIndex; + for (let polled = 0; polled < chunkSize && needsVisit > 0; nextPollIndex(), needsVisit--) { + const watchedFile = queue[pollIndex]; + if (!watchedFile) { + continue; } - else { - Debug.assert(queue.pollIndex === 0); - queue.pollScheduled = false; + else if (watchedFile.isClosed) { + queue[pollIndex] = undefined; + continue; } - } - - function pollLowPollingIntervalQueue(queue: PollingIntervalQueue) { - // Always poll complete list of changedFilesInLastPoll - pollQueue(changedFilesInLastPoll, PollingInterval.Low, /*pollIndex*/ 0, changedFilesInLastPoll.length); - - // Finally do the actual polling of the queue - pollPollingIntervalQueue(queue); - // Schedule poll if there are files in changedFilesInLastPoll but no files in the actual queue - // as pollPollingIntervalQueue wont schedule for next poll - if (!queue.pollScheduled && changedFilesInLastPoll.length) { - scheduleNextPoll(PollingInterval.Low); + polled++; + const fileChanged = onWatchedFileStat(watchedFile, getModifiedTime(watchedFile.fileName)); + if (watchedFile.isClosed) { + // Closed watcher as part of callback + queue[pollIndex] = undefined; } - } - - function pollQueue(queue: (WatchedFile | undefined)[], pollingInterval: PollingInterval, pollIndex: number, chunkSize: number) { - // Max visit would be all elements of the queue - let needsVisit = queue.length; - let definedValueCopyToIndex = pollIndex; - for (let polled = 0; polled < chunkSize && needsVisit > 0; nextPollIndex(), needsVisit--) { - const watchedFile = queue[pollIndex]; - if (!watchedFile) { - continue; - } - else if (watchedFile.isClosed) { - queue[pollIndex] = undefined; - continue; - } - - polled++; - const fileChanged = onWatchedFileStat(watchedFile, getModifiedTime(watchedFile.fileName)); - if (watchedFile.isClosed) { - // Closed watcher as part of callback - queue[pollIndex] = undefined; - } - else if (fileChanged) { - watchedFile.unchangedPolls = 0; - // Changed files go to changedFilesInLastPoll queue - if (queue !== changedFilesInLastPoll) { - queue[pollIndex] = undefined; - addChangedFileToLowPollingIntervalQueue(watchedFile); - } - } - else if (watchedFile.unchangedPolls !== unchangedPollThresholds[pollingInterval]) { - watchedFile.unchangedPolls++; - } - else if (queue === changedFilesInLastPoll) { - // Restart unchangedPollCount for unchanged file and move to low polling interval queue - watchedFile.unchangedPolls = 1; - queue[pollIndex] = undefined; - addToPollingIntervalQueue(watchedFile, PollingInterval.Low); - } - else if (pollingInterval !== PollingInterval.High) { - watchedFile.unchangedPolls++; + else if (fileChanged) { + watchedFile.unchangedPolls = 0; + // Changed files go to changedFilesInLastPoll queue + if (queue !== changedFilesInLastPoll) { queue[pollIndex] = undefined; - addToPollingIntervalQueue(watchedFile, pollingInterval === PollingInterval.Low ? PollingInterval.Medium : PollingInterval.High); - } - - if (queue[pollIndex]) { - // Copy this file to the non hole location - if (definedValueCopyToIndex < pollIndex) { - queue[definedValueCopyToIndex] = watchedFile; - queue[pollIndex] = undefined; - } - definedValueCopyToIndex++; + addChangedFileToLowPollingIntervalQueue(watchedFile); } } - - // Return next poll index - return pollIndex; - - function nextPollIndex() { - pollIndex++; - if (pollIndex === queue.length) { - if (definedValueCopyToIndex < pollIndex) { - // There are holes from nextDefinedValueIndex to end of queue, change queue size - queue.length = definedValueCopyToIndex; - } - pollIndex = 0; - definedValueCopyToIndex = 0; - } + else if (watchedFile.unchangedPolls !== unchangedPollThresholds[pollingInterval]) { + watchedFile.unchangedPolls++; } - } - - function pollingIntervalQueue(pollingInterval: PollingInterval) { - switch (pollingInterval) { - case PollingInterval.Low: - return lowPollingIntervalQueue; - case PollingInterval.Medium: - return mediumPollingIntervalQueue; - case PollingInterval.High: - return highPollingIntervalQueue; + else if (queue === changedFilesInLastPoll) { + // Restart unchangedPollCount for unchanged file and move to low polling interval queue + watchedFile.unchangedPolls = 1; + queue[pollIndex] = undefined; + addToPollingIntervalQueue(watchedFile, PollingInterval.Low); } - } - - function addToPollingIntervalQueue(file: WatchedFile, pollingInterval: PollingInterval) { - pollingIntervalQueue(pollingInterval).push(file); - scheduleNextPollIfNotAlreadyScheduled(pollingInterval); - } - - function addChangedFileToLowPollingIntervalQueue(file: WatchedFile) { - changedFilesInLastPoll.push(file); - scheduleNextPollIfNotAlreadyScheduled(PollingInterval.Low); - } - - function scheduleNextPollIfNotAlreadyScheduled(pollingInterval: PollingInterval) { - if (!pollingIntervalQueue(pollingInterval).pollScheduled) { - scheduleNextPoll(pollingInterval); + else if (pollingInterval !== PollingInterval.High) { + watchedFile.unchangedPolls++; + queue[pollIndex] = undefined; + addToPollingIntervalQueue(watchedFile, pollingInterval === PollingInterval.Low ? PollingInterval.Medium : PollingInterval.High); + } + if (queue[pollIndex]) { + // Copy this file to the non hole location + if (definedValueCopyToIndex < pollIndex) { + queue[definedValueCopyToIndex] = watchedFile; + queue[pollIndex] = undefined; + } + definedValueCopyToIndex++; } } - - function scheduleNextPoll(pollingInterval: PollingInterval) { - pollingIntervalQueue(pollingInterval).pollScheduled = host.setTimeout(pollingInterval === PollingInterval.Low ? pollLowPollingIntervalQueue : pollPollingIntervalQueue, pollingInterval, pollingIntervalQueue(pollingInterval)); - } - - function getModifiedTime(fileName: string) { - return host.getModifiedTime(fileName) || missingFileModifiedTime; - } - } - - function createUseFsEventsOnParentDirectoryWatchFile(fsWatch: FsWatch, useCaseSensitiveFileNames: boolean): HostWatchFile { - // One file can have multiple watchers - const fileWatcherCallbacks = createMultiMap(); - const dirWatchers = createMap(); - const toCanonicalName = createGetCanonicalFileName(useCaseSensitiveFileNames); - return nonPollingWatchFile; - - function nonPollingWatchFile(fileName: string, callback: FileWatcherCallback, _pollingInterval: PollingInterval, fallbackOptions: WatchOptions | undefined): FileWatcher { - const filePath = toCanonicalName(fileName); - fileWatcherCallbacks.add(filePath, callback); - const dirPath = getDirectoryPath(filePath) || "."; - const watcher = dirWatchers.get(dirPath) || - createDirectoryWatcher(getDirectoryPath(fileName) || ".", dirPath, fallbackOptions); - watcher.referenceCount++; - return { - close: () => { - if (watcher.referenceCount === 1) { - watcher.close(); - dirWatchers.delete(dirPath); - } - else { - watcher.referenceCount--; - } - fileWatcherCallbacks.remove(filePath, callback); + // Return next poll index + return pollIndex; + function nextPollIndex() { + pollIndex++; + if (pollIndex === queue.length) { + if (definedValueCopyToIndex < pollIndex) { + // There are holes from nextDefinedValueIndex to end of queue, change queue size + queue.length = definedValueCopyToIndex; } - }; + pollIndex = 0; + definedValueCopyToIndex = 0; + } } - - function createDirectoryWatcher(dirName: string, dirPath: string, fallbackOptions: WatchOptions | undefined) { - const watcher = fsWatch( - dirName, - FileSystemEntryKind.Directory, - (_eventName: string, relativeFileName) => { - // When files are deleted from disk, the triggered "rename" event would have a relativefileName of "undefined" - if (!isString(relativeFileName)) { return; } - const fileName = getNormalizedAbsolutePath(relativeFileName, dirName); - // Some applications save a working file via rename operations - const callbacks = fileName && fileWatcherCallbacks.get(toCanonicalName(fileName)); - if (callbacks) { - for (const fileCallback of callbacks) { - fileCallback(fileName, FileWatcherEventKind.Changed); - } - } - }, - /*recursive*/ false, - PollingInterval.Medium, - fallbackOptions - ) as DirectoryWatcher; - watcher.referenceCount = 0; - dirWatchers.set(dirPath, watcher); - return watcher; + } + function pollingIntervalQueue(pollingInterval: PollingInterval) { + switch (pollingInterval) { + case PollingInterval.Low: + return lowPollingIntervalQueue; + case PollingInterval.Medium: + return mediumPollingIntervalQueue; + case PollingInterval.High: + return highPollingIntervalQueue; } } - - /* @internal */ - export function createSingleFileWatcherPerName( - watchFile: HostWatchFile, - useCaseSensitiveFileNames: boolean - ): HostWatchFile { - interface SingleFileWatcher { - watcher: FileWatcher; - refCount: number; + function addToPollingIntervalQueue(file: WatchedFile, pollingInterval: PollingInterval) { + pollingIntervalQueue(pollingInterval).push(file); + scheduleNextPollIfNotAlreadyScheduled(pollingInterval); + } + function addChangedFileToLowPollingIntervalQueue(file: WatchedFile) { + changedFilesInLastPoll.push(file); + scheduleNextPollIfNotAlreadyScheduled(PollingInterval.Low); + } + function scheduleNextPollIfNotAlreadyScheduled(pollingInterval: PollingInterval) { + if (!pollingIntervalQueue(pollingInterval).pollScheduled) { + scheduleNextPoll(pollingInterval); } - const cache = createMap(); - const callbacksCache = createMultiMap(); - const toCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); - - return (fileName, callback, pollingInterval, options) => { - const path = toCanonicalFileName(fileName); - const existing = cache.get(path); - if (existing) { - existing.refCount++; + } + function scheduleNextPoll(pollingInterval: PollingInterval) { + pollingIntervalQueue(pollingInterval).pollScheduled = host.setTimeout(pollingInterval === PollingInterval.Low ? pollLowPollingIntervalQueue : pollPollingIntervalQueue, pollingInterval, pollingIntervalQueue(pollingInterval)); + } + function getModifiedTime(fileName: string) { + return host.getModifiedTime(fileName) || missingFileModifiedTime; + } +} +function createUseFsEventsOnParentDirectoryWatchFile(fsWatch: FsWatch, useCaseSensitiveFileNames: boolean): HostWatchFile { + // One file can have multiple watchers + const fileWatcherCallbacks = createMultiMap(); + const dirWatchers = createMap(); + const toCanonicalName = createGetCanonicalFileName(useCaseSensitiveFileNames); + return nonPollingWatchFile; + function nonPollingWatchFile(fileName: string, callback: FileWatcherCallback, _pollingInterval: PollingInterval, fallbackOptions: WatchOptions | undefined): FileWatcher { + const filePath = toCanonicalName(fileName); + fileWatcherCallbacks.add(filePath, callback); + const dirPath = getDirectoryPath(filePath) || "."; + const watcher = dirWatchers.get(dirPath) || + createDirectoryWatcher(getDirectoryPath(fileName) || ".", dirPath, fallbackOptions); + watcher.referenceCount++; + return { + close: () => { + if (watcher.referenceCount === 1) { + watcher.close(); + dirWatchers.delete(dirPath); + } + else { + watcher.referenceCount--; + } + fileWatcherCallbacks.remove(filePath, callback); } - else { - cache.set(path, { - watcher: watchFile( - fileName, - (fileName, eventKind) => forEach( - callbacksCache.get(path), - cb => cb(fileName, eventKind) - ), - pollingInterval, - options - ), - refCount: 1 - }); + }; + } + function createDirectoryWatcher(dirName: string, dirPath: string, fallbackOptions: WatchOptions | undefined) { + const watcher = (fsWatch(dirName, FileSystemEntryKind.Directory, (_eventName: string, relativeFileName) => { + // When files are deleted from disk, the triggered "rename" event would have a relativefileName of "undefined" + if (!isString(relativeFileName)) { + return; } - callbacksCache.add(path, callback); - - return { - close: () => { - const watcher = Debug.checkDefined(cache.get(path)); - callbacksCache.remove(path, callback); - watcher.refCount--; - if (watcher.refCount) return; - cache.delete(path); - closeFileWatcherOf(watcher); + const fileName = getNormalizedAbsolutePath(relativeFileName, dirName); + // Some applications save a working file via rename operations + const callbacks = fileName && fileWatcherCallbacks.get(toCanonicalName(fileName)); + if (callbacks) { + for (const fileCallback of callbacks) { + fileCallback(fileName, FileWatcherEventKind.Changed); } - }; - }; + } + }, + /*recursive*/ false, PollingInterval.Medium, fallbackOptions) as DirectoryWatcher); + watcher.referenceCount = 0; + dirWatchers.set(dirPath, watcher); + return watcher; } - - /** - * Returns true if file status changed - */ - /*@internal*/ - export function onWatchedFileStat(watchedFile: WatchedFile, modifiedTime: Date): boolean { - const oldTime = watchedFile.mtime.getTime(); - const newTime = modifiedTime.getTime(); - if (oldTime !== newTime) { - watchedFile.mtime = modifiedTime; - watchedFile.callback(watchedFile.fileName, getFileWatcherEventKind(oldTime, newTime)); - return true; - } - - return false; +} +/* @internal */ +export function createSingleFileWatcherPerName(watchFile: HostWatchFile, useCaseSensitiveFileNames: boolean): HostWatchFile { + interface SingleFileWatcher { + watcher: FileWatcher; + refCount: number; } - - /*@internal*/ - export function getFileWatcherEventKind(oldTime: number, newTime: number) { - return oldTime === 0 - ? FileWatcherEventKind.Created - : newTime === 0 - ? FileWatcherEventKind.Deleted - : FileWatcherEventKind.Changed; + const cache = createMap(); + const callbacksCache = createMultiMap(); + const toCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); + return (fileName, callback, pollingInterval, options) => { + const path = toCanonicalFileName(fileName); + const existing = cache.get(path); + if (existing) { + existing.refCount++; + } + else { + cache.set(path, { + watcher: watchFile(fileName, (fileName, eventKind) => forEach(callbacksCache.get(path), cb => cb(fileName, eventKind)), pollingInterval, options), + refCount: 1 + }); + } + callbacksCache.add(path, callback); + return { + close: () => { + const watcher = Debug.checkDefined(cache.get(path)); + callbacksCache.remove(path, callback); + watcher.refCount--; + if (watcher.refCount) + return; + cache.delete(path); + closeFileWatcherOf(watcher); + } + }; + }; +} +/** + * Returns true if file status changed + */ +/*@internal*/ +export function onWatchedFileStat(watchedFile: WatchedFile, modifiedTime: Date): boolean { + const oldTime = watchedFile.mtime.getTime(); + const newTime = modifiedTime.getTime(); + if (oldTime !== newTime) { + watchedFile.mtime = modifiedTime; + watchedFile.callback(watchedFile.fileName, getFileWatcherEventKind(oldTime, newTime)); + return true; } - - /*@internal*/ - export const ignoredPaths = ["/node_modules/.", "/.git", "/.#"]; - - /*@internal*/ - export let sysLog: (s: string) => void = noop; // eslint-disable-line prefer-const - - /*@internal*/ - export function setSysLog(logger: typeof sysLog) { - sysLog = logger; + return false; +} +/*@internal*/ +export function getFileWatcherEventKind(oldTime: number, newTime: number) { + return oldTime === 0 + ? FileWatcherEventKind.Created + : newTime === 0 + ? FileWatcherEventKind.Deleted + : FileWatcherEventKind.Changed; +} +/*@internal*/ +export const ignoredPaths = ["/node_modules/.", "/.git", "/.#"]; +/*@internal*/ +export let sysLog: (s: string) => void = noop; // eslint-disable-line prefer-const +/*@internal*/ +export function setSysLog(logger: typeof sysLog) { + sysLog = logger; +} +/*@internal*/ +export interface RecursiveDirectoryWatcherHost { + watchDirectory: HostWatchDirectory; + useCaseSensitiveFileNames: boolean; + getAccessibleSortedChildDirectories(path: string): readonly string[]; + directoryExists(dir: string): boolean; + realpath(s: string): string; + setTimeout: NonNullable; + clearTimeout: NonNullable; +} +/** + * Watch the directory recursively using host provided method to watch child directories + * that means if this is recursive watcher, watch the children directories as well + * (eg on OS that dont support recursive watch using fs.watch use fs.watchFile) + */ +/*@internal*/ +export function createDirectoryWatcherSupportingRecursive(host: RecursiveDirectoryWatcherHost): HostWatchDirectory { + interface ChildDirectoryWatcher extends FileWatcher { + dirName: string; } - - /*@internal*/ - export interface RecursiveDirectoryWatcherHost { - watchDirectory: HostWatchDirectory; - useCaseSensitiveFileNames: boolean; - getAccessibleSortedChildDirectories(path: string): readonly string[]; - directoryExists(dir: string): boolean; - realpath(s: string): string; - setTimeout: NonNullable; - clearTimeout: NonNullable; + type ChildWatches = readonly ChildDirectoryWatcher[]; + interface HostDirectoryWatcher { + watcher: FileWatcher; + childWatches: ChildWatches; + refCount: number; } - + const cache = createMap(); + const callbackCache = createMultiMap<{ + dirName: string; + callback: DirectoryWatcherCallback; + }>(); + const cacheToUpdateChildWatches = createMap<{ + dirName: string; + options: WatchOptions | undefined; + }>(); + let timerToUpdateChildWatches: any; + const filePathComparer = getStringComparer(!host.useCaseSensitiveFileNames); + const toCanonicalFilePath = createGetCanonicalFileName(host.useCaseSensitiveFileNames); + return (dirName, callback, recursive, options) => recursive ? + createDirectoryWatcher(dirName, options, callback) : + host.watchDirectory(dirName, callback, recursive, options); /** - * Watch the directory recursively using host provided method to watch child directories - * that means if this is recursive watcher, watch the children directories as well - * (eg on OS that dont support recursive watch using fs.watch use fs.watchFile) + * Create the directory watcher for the dirPath. */ - /*@internal*/ - export function createDirectoryWatcherSupportingRecursive(host: RecursiveDirectoryWatcherHost): HostWatchDirectory { - interface ChildDirectoryWatcher extends FileWatcher { - dirName: string; - } - type ChildWatches = readonly ChildDirectoryWatcher[]; - interface HostDirectoryWatcher { - watcher: FileWatcher; - childWatches: ChildWatches; - refCount: number; + function createDirectoryWatcher(dirName: string, options: WatchOptions | undefined, callback?: DirectoryWatcherCallback): ChildDirectoryWatcher { + const dirPath = (toCanonicalFilePath(dirName) as Path); + let directoryWatcher = cache.get(dirPath); + if (directoryWatcher) { + directoryWatcher.refCount++; } - - const cache = createMap(); - const callbackCache = createMultiMap<{ dirName: string; callback: DirectoryWatcherCallback; }>(); - const cacheToUpdateChildWatches = createMap<{ dirName: string; options: WatchOptions | undefined; }>(); - let timerToUpdateChildWatches: any; - - const filePathComparer = getStringComparer(!host.useCaseSensitiveFileNames); - const toCanonicalFilePath = createGetCanonicalFileName(host.useCaseSensitiveFileNames); - - return (dirName, callback, recursive, options) => recursive ? - createDirectoryWatcher(dirName, options, callback) : - host.watchDirectory(dirName, callback, recursive, options); - - /** - * Create the directory watcher for the dirPath. - */ - function createDirectoryWatcher(dirName: string, options: WatchOptions | undefined, callback?: DirectoryWatcherCallback): ChildDirectoryWatcher { - const dirPath = toCanonicalFilePath(dirName) as Path; - let directoryWatcher = cache.get(dirPath); - if (directoryWatcher) { - directoryWatcher.refCount++; - } - else { - directoryWatcher = { - watcher: host.watchDirectory(dirName, fileName => { - if (isIgnoredPath(fileName)) return; - - if (options?.synchronousWatchDirectory) { - // Call the actual callback - invokeCallbacks(dirPath, fileName); - - // Iterate through existing children and update the watches if needed - updateChildWatches(dirName, dirPath, options); - } - else { - nonSyncUpdateChildWatches(dirName, dirPath, fileName, options); - } - }, /*recursive*/ false, options), - refCount: 1, - childWatches: emptyArray - }; - cache.set(dirPath, directoryWatcher); - updateChildWatches(dirName, dirPath, options); - } - - const callbackToAdd = callback && { dirName, callback }; - if (callbackToAdd) { - callbackCache.add(dirPath, callbackToAdd); - } - - return { - dirName, - close: () => { - const directoryWatcher = Debug.checkDefined(cache.get(dirPath)); - if (callbackToAdd) callbackCache.remove(dirPath, callbackToAdd); - directoryWatcher.refCount--; - - if (directoryWatcher.refCount) return; - - cache.delete(dirPath); - closeFileWatcherOf(directoryWatcher); - directoryWatcher.childWatches.forEach(closeFileWatcher); - } - }; - } - - function invokeCallbacks(dirPath: Path, fileNameOrInvokeMap: string | Map) { - let fileName: string | undefined; - let invokeMap: Map | undefined; - if (isString(fileNameOrInvokeMap)) { - fileName = fileNameOrInvokeMap; - } - else { - invokeMap = fileNameOrInvokeMap; - } - // Call the actual callback - callbackCache.forEach((callbacks, rootDirName) => { - if (invokeMap && invokeMap.has(rootDirName)) return; - if (rootDirName === dirPath || (startsWith(dirPath, rootDirName) && dirPath[rootDirName.length] === directorySeparator)) { - if (invokeMap) { - invokeMap.set(rootDirName, true); + else { + directoryWatcher = { + watcher: host.watchDirectory(dirName, fileName => { + if (isIgnoredPath(fileName)) + return; + if (options?.synchronousWatchDirectory) { + // Call the actual callback + invokeCallbacks(dirPath, fileName); + // Iterate through existing children and update the watches if needed + updateChildWatches(dirName, dirPath, options); } else { - callbacks.forEach(({ callback }) => callback(fileName!)); + nonSyncUpdateChildWatches(dirName, dirPath, fileName, options); } - } - }); + }, /*recursive*/ false, options), + refCount: 1, + childWatches: emptyArray + }; + cache.set(dirPath, directoryWatcher); + updateChildWatches(dirName, dirPath, options); } - - function nonSyncUpdateChildWatches(dirName: string, dirPath: Path, fileName: string, options: WatchOptions | undefined) { - // Iterate through existing children and update the watches if needed - const parentWatcher = cache.get(dirPath); - if (parentWatcher && host.directoryExists(dirName)) { - // Schedule the update and postpone invoke for callbacks - scheduleUpdateChildWatches(dirName, dirPath, options); - return; - } - - // Call the actual callbacks and remove child watches - invokeCallbacks(dirPath, fileName); - removeChildWatches(parentWatcher); + const callbackToAdd = callback && { dirName, callback }; + if (callbackToAdd) { + callbackCache.add(dirPath, callbackToAdd); } - - function scheduleUpdateChildWatches(dirName: string, dirPath: Path, options: WatchOptions | undefined) { - if (!cacheToUpdateChildWatches.has(dirPath)) { - cacheToUpdateChildWatches.set(dirPath, { dirName, options }); + return { + dirName, + close: () => { + const directoryWatcher = Debug.checkDefined(cache.get(dirPath)); + if (callbackToAdd) + callbackCache.remove(dirPath, callbackToAdd); + directoryWatcher.refCount--; + if (directoryWatcher.refCount) + return; + cache.delete(dirPath); + closeFileWatcherOf(directoryWatcher); + directoryWatcher.childWatches.forEach(closeFileWatcher); } - if (timerToUpdateChildWatches) { - host.clearTimeout(timerToUpdateChildWatches); - timerToUpdateChildWatches = undefined; + }; + } + function invokeCallbacks(dirPath: Path, fileNameOrInvokeMap: string | ts.Map) { + let fileName: string | undefined; + let invokeMap: ts.Map | undefined; + if (isString(fileNameOrInvokeMap)) { + fileName = fileNameOrInvokeMap; + } + else { + invokeMap = fileNameOrInvokeMap; + } + // Call the actual callback + callbackCache.forEach((callbacks, rootDirName) => { + if (invokeMap && invokeMap.has(rootDirName)) + return; + if (rootDirName === dirPath || (startsWith(dirPath, rootDirName) && dirPath[rootDirName.length] === directorySeparator)) { + if (invokeMap) { + invokeMap.set(rootDirName, true); + } + else { + callbacks.forEach(({ callback }) => callback(fileName!)); + } } - timerToUpdateChildWatches = host.setTimeout(onTimerToUpdateChildWatches, 1000); + }); + } + function nonSyncUpdateChildWatches(dirName: string, dirPath: Path, fileName: string, options: WatchOptions | undefined) { + // Iterate through existing children and update the watches if needed + const parentWatcher = cache.get(dirPath); + if (parentWatcher && host.directoryExists(dirName)) { + // Schedule the update and postpone invoke for callbacks + scheduleUpdateChildWatches(dirName, dirPath, options); + return; + } + // Call the actual callbacks and remove child watches + invokeCallbacks(dirPath, fileName); + removeChildWatches(parentWatcher); + } + function scheduleUpdateChildWatches(dirName: string, dirPath: Path, options: WatchOptions | undefined) { + if (!cacheToUpdateChildWatches.has(dirPath)) { + cacheToUpdateChildWatches.set(dirPath, { dirName, options }); } - - function onTimerToUpdateChildWatches() { + if (timerToUpdateChildWatches) { + host.clearTimeout(timerToUpdateChildWatches); timerToUpdateChildWatches = undefined; - sysLog(`sysLog:: onTimerToUpdateChildWatches:: ${cacheToUpdateChildWatches.size}`); - const start = timestamp(); - const invokeMap = createMap(); - - while (!timerToUpdateChildWatches && cacheToUpdateChildWatches.size) { - const { value: [dirPath, { dirName, options }], done } = cacheToUpdateChildWatches.entries().next(); - Debug.assert(!done); - cacheToUpdateChildWatches.delete(dirPath); - // Because the child refresh is fresh, we would need to invalidate whole root directory being watched - // to ensure that all the changes are reflected at this time - invokeCallbacks(dirPath as Path, invokeMap); - updateChildWatches(dirName, dirPath as Path, options); - } - - sysLog(`sysLog:: invokingWatchers:: ${timestamp() - start}ms:: ${cacheToUpdateChildWatches.size}`); - callbackCache.forEach((callbacks, rootDirName) => { - if (invokeMap.has(rootDirName)) { - callbacks.forEach(({ callback, dirName }) => callback(dirName)); - } - }); - - const elapsed = timestamp() - start; - sysLog(`sysLog:: Elapsed ${elapsed}ms:: onTimerToUpdateChildWatches:: ${cacheToUpdateChildWatches.size} ${timerToUpdateChildWatches}`); } - - function removeChildWatches(parentWatcher: HostDirectoryWatcher | undefined) { - if (!parentWatcher) return; - const existingChildWatches = parentWatcher.childWatches; - parentWatcher.childWatches = emptyArray; - for (const childWatcher of existingChildWatches) { - childWatcher.close(); - removeChildWatches(cache.get(toCanonicalFilePath(childWatcher.dirName))); - } + timerToUpdateChildWatches = host.setTimeout(onTimerToUpdateChildWatches, 1000); + } + function onTimerToUpdateChildWatches() { + timerToUpdateChildWatches = undefined; + sysLog(`sysLog:: onTimerToUpdateChildWatches:: ${cacheToUpdateChildWatches.size}`); + const start = timestamp(); + const invokeMap = createMap(); + while (!timerToUpdateChildWatches && cacheToUpdateChildWatches.size) { + const { value: [dirPath, { dirName, options }], done } = cacheToUpdateChildWatches.entries().next(); + Debug.assert(!done); + cacheToUpdateChildWatches.delete(dirPath); + // Because the child refresh is fresh, we would need to invalidate whole root directory being watched + // to ensure that all the changes are reflected at this time + invokeCallbacks((dirPath as Path), invokeMap); + updateChildWatches(dirName, (dirPath as Path), options); } - - function updateChildWatches(dirName: string, dirPath: Path, options: WatchOptions | undefined) { - // Iterate through existing children and update the watches if needed - const parentWatcher = cache.get(dirPath); - if (parentWatcher) { - parentWatcher.childWatches = watchChildDirectories(dirName, parentWatcher.childWatches, options); + sysLog(`sysLog:: invokingWatchers:: ${timestamp() - start}ms:: ${cacheToUpdateChildWatches.size}`); + callbackCache.forEach((callbacks, rootDirName) => { + if (invokeMap.has(rootDirName)) { + callbacks.forEach(({ callback, dirName }) => callback(dirName)); } + }); + const elapsed = timestamp() - start; + sysLog(`sysLog:: Elapsed ${elapsed}ms:: onTimerToUpdateChildWatches:: ${cacheToUpdateChildWatches.size} ${timerToUpdateChildWatches}`); + } + function removeChildWatches(parentWatcher: HostDirectoryWatcher | undefined) { + if (!parentWatcher) + return; + const existingChildWatches = parentWatcher.childWatches; + parentWatcher.childWatches = emptyArray; + for (const childWatcher of existingChildWatches) { + childWatcher.close(); + removeChildWatches(cache.get(toCanonicalFilePath(childWatcher.dirName))); } - + } + function updateChildWatches(dirName: string, dirPath: Path, options: WatchOptions | undefined) { + // Iterate through existing children and update the watches if needed + const parentWatcher = cache.get(dirPath); + if (parentWatcher) { + parentWatcher.childWatches = watchChildDirectories(dirName, parentWatcher.childWatches, options); + } + } + /** + * Watch the directories in the parentDir + */ + function watchChildDirectories(parentDir: string, existingChildWatches: ChildWatches, options: WatchOptions | undefined): ChildWatches { + let newChildWatches: ChildDirectoryWatcher[] | undefined; + enumerateInsertsAndDeletes(host.directoryExists(parentDir) ? mapDefined(host.getAccessibleSortedChildDirectories(parentDir), child => { + const childFullName = getNormalizedAbsolutePath(child, parentDir); + // Filter our the symbolic link directories since those arent included in recursive watch + // which is same behaviour when recursive: true is passed to fs.watch + return !isIgnoredPath(childFullName) && filePathComparer(childFullName, normalizePath(host.realpath(childFullName))) === Comparison.EqualTo ? childFullName : undefined; + }) : emptyArray, existingChildWatches, (child, childWatcher) => filePathComparer(child, childWatcher.dirName), createAndAddChildDirectoryWatcher, closeFileWatcher, addChildDirectoryWatcher); + return newChildWatches || emptyArray; /** - * Watch the directories in the parentDir + * Create new childDirectoryWatcher and add it to the new ChildDirectoryWatcher list */ - function watchChildDirectories(parentDir: string, existingChildWatches: ChildWatches, options: WatchOptions | undefined): ChildWatches { - let newChildWatches: ChildDirectoryWatcher[] | undefined; - enumerateInsertsAndDeletes( - host.directoryExists(parentDir) ? mapDefined(host.getAccessibleSortedChildDirectories(parentDir), child => { - const childFullName = getNormalizedAbsolutePath(child, parentDir); - // Filter our the symbolic link directories since those arent included in recursive watch - // which is same behaviour when recursive: true is passed to fs.watch - return !isIgnoredPath(childFullName) && filePathComparer(childFullName, normalizePath(host.realpath(childFullName))) === Comparison.EqualTo ? childFullName : undefined; - }) : emptyArray, - existingChildWatches, - (child, childWatcher) => filePathComparer(child, childWatcher.dirName), - createAndAddChildDirectoryWatcher, - closeFileWatcher, - addChildDirectoryWatcher - ); - - return newChildWatches || emptyArray; - - /** - * Create new childDirectoryWatcher and add it to the new ChildDirectoryWatcher list - */ - function createAndAddChildDirectoryWatcher(childName: string) { - const result = createDirectoryWatcher(childName, options); - addChildDirectoryWatcher(result); - } - - /** - * Add child directory watcher to the new ChildDirectoryWatcher list - */ - function addChildDirectoryWatcher(childWatcher: ChildDirectoryWatcher) { - (newChildWatches || (newChildWatches = [])).push(childWatcher); - } + function createAndAddChildDirectoryWatcher(childName: string) { + const result = createDirectoryWatcher(childName, options); + addChildDirectoryWatcher(result); } - - function isIgnoredPath(path: string) { - return some(ignoredPaths, searchPath => isInPath(path, searchPath)); - } - - function isInPath(path: string, searchPath: string) { - if (stringContains(path, searchPath)) return true; - if (host.useCaseSensitiveFileNames) return false; - return stringContains(toCanonicalFilePath(path), searchPath); + /** + * Add child directory watcher to the new ChildDirectoryWatcher list + */ + function addChildDirectoryWatcher(childWatcher: ChildDirectoryWatcher) { + (newChildWatches || (newChildWatches = [])).push(childWatcher); } } - - /*@internal*/ - export type FsWatchCallback = (eventName: "rename" | "change", relativeFileName: string | undefined) => void; - /*@internal*/ - export type FsWatch = (fileOrDirectory: string, entryKind: FileSystemEntryKind, callback: FsWatchCallback, recursive: boolean, fallbackPollingInterval: PollingInterval, fallbackOptions: WatchOptions | undefined) => FileWatcher; - - /*@internal*/ - export const enum FileSystemEntryKind { - File, - Directory, + function isIgnoredPath(path: string) { + return some(ignoredPaths, searchPath => isInPath(path, searchPath)); } - - /*@internal*/ - export function createFileWatcherCallback(callback: FsWatchCallback): FileWatcherCallback { - return (_fileName, eventKind) => callback(eventKind === FileWatcherEventKind.Changed ? "change" : "rename", ""); + function isInPath(path: string, searchPath: string) { + if (stringContains(path, searchPath)) + return true; + if (host.useCaseSensitiveFileNames) + return false; + return stringContains(toCanonicalFilePath(path), searchPath); } - - function createFsWatchCallbackForFileWatcherCallback( - fileName: string, - callback: FileWatcherCallback, - fileExists: System["fileExists"] - ): FsWatchCallback { - return eventName => { - if (eventName === "rename") { - callback(fileName, fileExists(fileName) ? FileWatcherEventKind.Created : FileWatcherEventKind.Deleted); - } - else { - // Change - callback(fileName, FileWatcherEventKind.Changed); - } - }; +} +/*@internal*/ +export type FsWatchCallback = (eventName: "rename" | "change", relativeFileName: string | undefined) => void; +/*@internal*/ +export type FsWatch = (fileOrDirectory: string, entryKind: FileSystemEntryKind, callback: FsWatchCallback, recursive: boolean, fallbackPollingInterval: PollingInterval, fallbackOptions: WatchOptions | undefined) => FileWatcher; +/*@internal*/ +export const enum FileSystemEntryKind { + File, + Directory +} +/*@internal*/ +export function createFileWatcherCallback(callback: FsWatchCallback): FileWatcherCallback { + return (_fileName, eventKind) => callback(eventKind === FileWatcherEventKind.Changed ? "change" : "rename", ""); +} +function createFsWatchCallbackForFileWatcherCallback(fileName: string, callback: FileWatcherCallback, fileExists: System["fileExists"]): FsWatchCallback { + return eventName => { + if (eventName === "rename") { + callback(fileName, fileExists(fileName) ? FileWatcherEventKind.Created : FileWatcherEventKind.Deleted); + } + else { + // Change + callback(fileName, FileWatcherEventKind.Changed); + } + }; +} +function createFsWatchCallbackForDirectoryWatcherCallback(directoryName: string, callback: DirectoryWatcherCallback): FsWatchCallback { + return (eventName, relativeFileName) => { + // In watchDirectory we only care about adding and removing files (when event name is + // "rename"); changes made within files are handled by corresponding fileWatchers (when + // event name is "change") + if (eventName === "rename") { + // When deleting a file, the passed baseFileName is null + callback(!relativeFileName ? directoryName : normalizePath(combinePaths(directoryName, relativeFileName))); + } + }; +} +/*@internal*/ +export interface CreateSystemWatchFunctions { + // Polling watch file + pollingWatchFile: HostWatchFile; + // For dynamic polling watch file + getModifiedTime: NonNullable; + setTimeout: NonNullable; + clearTimeout: NonNullable; + // For fs events : + fsWatch: FsWatch; + fileExists: System["fileExists"]; + useCaseSensitiveFileNames: boolean; + fsSupportsRecursiveFsWatch: boolean; + directoryExists: System["directoryExists"]; + getAccessibleSortedChildDirectories(path: string): readonly string[]; + realpath(s: string): string; + // For backward compatibility environment variables + tscWatchFile: string | undefined; + useNonPollingWatchers?: boolean; + tscWatchDirectory: string | undefined; +} +/*@internal*/ +export function createSystemWatchFunctions({ pollingWatchFile, getModifiedTime, setTimeout, clearTimeout, fsWatch, fileExists, useCaseSensitiveFileNames, fsSupportsRecursiveFsWatch, directoryExists, getAccessibleSortedChildDirectories, realpath, tscWatchFile, useNonPollingWatchers, tscWatchDirectory, }: CreateSystemWatchFunctions): { + watchFile: HostWatchFile; + watchDirectory: HostWatchDirectory; +} { + let dynamicPollingWatchFile: HostWatchFile | undefined; + let nonPollingWatchFile: HostWatchFile | undefined; + let hostRecursiveDirectoryWatcher: HostWatchDirectory | undefined; + return { + watchFile, + watchDirectory + }; + function watchFile(fileName: string, callback: FileWatcherCallback, pollingInterval: PollingInterval, options: WatchOptions | undefined): FileWatcher { + options = updateOptionsForWatchFile(options, useNonPollingWatchers); + const watchFileKind = Debug.checkDefined(options.watchFile); + switch (watchFileKind) { + case WatchFileKind.FixedPollingInterval: + return pollingWatchFile(fileName, callback, PollingInterval.Low, /*options*/ undefined); + case WatchFileKind.PriorityPollingInterval: + return pollingWatchFile(fileName, callback, pollingInterval, /*options*/ undefined); + case WatchFileKind.DynamicPriorityPolling: + return ensureDynamicPollingWatchFile()(fileName, callback, pollingInterval, /*options*/ undefined); + case WatchFileKind.UseFsEvents: + return fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallbackForFileWatcherCallback(fileName, callback, fileExists), + /*recursive*/ false, pollingInterval, getFallbackOptions(options)); + case WatchFileKind.UseFsEventsOnParentDirectory: + if (!nonPollingWatchFile) { + nonPollingWatchFile = createUseFsEventsOnParentDirectoryWatchFile(fsWatch, useCaseSensitiveFileNames); + } + return nonPollingWatchFile(fileName, callback, pollingInterval, getFallbackOptions(options)); + default: + Debug.assertNever(watchFileKind); + } } - - function createFsWatchCallbackForDirectoryWatcherCallback(directoryName: string, callback: DirectoryWatcherCallback): FsWatchCallback { - return (eventName, relativeFileName) => { - // In watchDirectory we only care about adding and removing files (when event name is - // "rename"); changes made within files are handled by corresponding fileWatchers (when - // event name is "change") - if (eventName === "rename") { - // When deleting a file, the passed baseFileName is null - callback(!relativeFileName ? directoryName : normalizePath(combinePaths(directoryName, relativeFileName))); - } - }; + function ensureDynamicPollingWatchFile() { + return dynamicPollingWatchFile || + (dynamicPollingWatchFile = createDynamicPriorityPollingWatchFile({ getModifiedTime, setTimeout })); } - - /*@internal*/ - export interface CreateSystemWatchFunctions { - // Polling watch file - pollingWatchFile: HostWatchFile; - // For dynamic polling watch file - getModifiedTime: NonNullable; - setTimeout: NonNullable; - clearTimeout: NonNullable; - // For fs events : - fsWatch: FsWatch; - fileExists: System["fileExists"]; - useCaseSensitiveFileNames: boolean; - fsSupportsRecursiveFsWatch: boolean; - directoryExists: System["directoryExists"]; - getAccessibleSortedChildDirectories(path: string): readonly string[]; - realpath(s: string): string; - // For backward compatibility environment variables - tscWatchFile: string | undefined; - useNonPollingWatchers?: boolean; - tscWatchDirectory: string | undefined; + function updateOptionsForWatchFile(options: WatchOptions | undefined, useNonPollingWatchers?: boolean): WatchOptions { + if (options && options.watchFile !== undefined) + return options; + switch (tscWatchFile) { + case "PriorityPollingInterval": + // Use polling interval based on priority when create watch using host.watchFile + return { watchFile: WatchFileKind.PriorityPollingInterval }; + case "DynamicPriorityPolling": + // Use polling interval but change the interval depending on file changes and their default polling interval + return { watchFile: WatchFileKind.DynamicPriorityPolling }; + case "UseFsEvents": + // Use notifications from FS to watch with falling back to fs.watchFile + return generateWatchFileOptions(WatchFileKind.UseFsEvents, PollingWatchKind.PriorityInterval, options); + case "UseFsEventsWithFallbackDynamicPolling": + // Use notifications from FS to watch with falling back to dynamic watch file + return generateWatchFileOptions(WatchFileKind.UseFsEvents, PollingWatchKind.DynamicPriority, options); + case "UseFsEventsOnParentDirectory": + useNonPollingWatchers = true; + // fall through + default: + return useNonPollingWatchers ? + // Use notifications from FS to watch with falling back to fs.watchFile + generateWatchFileOptions(WatchFileKind.UseFsEventsOnParentDirectory, PollingWatchKind.PriorityInterval, options) : + // Default to do not use fixed polling interval + { watchFile: WatchFileKind.FixedPollingInterval }; + } } - - /*@internal*/ - export function createSystemWatchFunctions({ - pollingWatchFile, - getModifiedTime, - setTimeout, - clearTimeout, - fsWatch, - fileExists, - useCaseSensitiveFileNames, - fsSupportsRecursiveFsWatch, - directoryExists, - getAccessibleSortedChildDirectories, - realpath, - tscWatchFile, - useNonPollingWatchers, - tscWatchDirectory, - }: CreateSystemWatchFunctions): { watchFile: HostWatchFile; watchDirectory: HostWatchDirectory; } { - let dynamicPollingWatchFile: HostWatchFile | undefined; - let nonPollingWatchFile: HostWatchFile | undefined; - let hostRecursiveDirectoryWatcher: HostWatchDirectory | undefined; + function generateWatchFileOptions(watchFile: WatchFileKind, fallbackPolling: PollingWatchKind, options: WatchOptions | undefined): WatchOptions { + const defaultFallbackPolling = options?.fallbackPolling; return { watchFile, - watchDirectory + fallbackPolling: defaultFallbackPolling === undefined ? + fallbackPolling : + defaultFallbackPolling }; - - function watchFile(fileName: string, callback: FileWatcherCallback, pollingInterval: PollingInterval, options: WatchOptions | undefined): FileWatcher { - options = updateOptionsForWatchFile(options, useNonPollingWatchers); - const watchFileKind = Debug.checkDefined(options.watchFile); - switch (watchFileKind) { - case WatchFileKind.FixedPollingInterval: - return pollingWatchFile(fileName, callback, PollingInterval.Low, /*options*/ undefined); - case WatchFileKind.PriorityPollingInterval: - return pollingWatchFile(fileName, callback, pollingInterval, /*options*/ undefined); - case WatchFileKind.DynamicPriorityPolling: - return ensureDynamicPollingWatchFile()(fileName, callback, pollingInterval, /*options*/ undefined); - case WatchFileKind.UseFsEvents: - return fsWatch( - fileName, - FileSystemEntryKind.File, - createFsWatchCallbackForFileWatcherCallback(fileName, callback, fileExists), - /*recursive*/ false, - pollingInterval, - getFallbackOptions(options) - ); - case WatchFileKind.UseFsEventsOnParentDirectory: - if (!nonPollingWatchFile) { - nonPollingWatchFile = createUseFsEventsOnParentDirectoryWatchFile(fsWatch, useCaseSensitiveFileNames); - } - return nonPollingWatchFile(fileName, callback, pollingInterval, getFallbackOptions(options)); - default: - Debug.assertNever(watchFileKind); - } - } - - function ensureDynamicPollingWatchFile() { - return dynamicPollingWatchFile || - (dynamicPollingWatchFile = createDynamicPriorityPollingWatchFile({ getModifiedTime, setTimeout })); - } - - function updateOptionsForWatchFile(options: WatchOptions | undefined, useNonPollingWatchers?: boolean): WatchOptions { - if (options && options.watchFile !== undefined) return options; - switch (tscWatchFile) { - case "PriorityPollingInterval": - // Use polling interval based on priority when create watch using host.watchFile - return { watchFile: WatchFileKind.PriorityPollingInterval }; - case "DynamicPriorityPolling": - // Use polling interval but change the interval depending on file changes and their default polling interval - return { watchFile: WatchFileKind.DynamicPriorityPolling }; - case "UseFsEvents": - // Use notifications from FS to watch with falling back to fs.watchFile - return generateWatchFileOptions(WatchFileKind.UseFsEvents, PollingWatchKind.PriorityInterval, options); - case "UseFsEventsWithFallbackDynamicPolling": - // Use notifications from FS to watch with falling back to dynamic watch file - return generateWatchFileOptions(WatchFileKind.UseFsEvents, PollingWatchKind.DynamicPriority, options); - case "UseFsEventsOnParentDirectory": - useNonPollingWatchers = true; - // fall through - default: - return useNonPollingWatchers ? - // Use notifications from FS to watch with falling back to fs.watchFile - generateWatchFileOptions(WatchFileKind.UseFsEventsOnParentDirectory, PollingWatchKind.PriorityInterval, options) : - // Default to do not use fixed polling interval - { watchFile: WatchFileKind.FixedPollingInterval }; - } - } - - function generateWatchFileOptions( - watchFile: WatchFileKind, - fallbackPolling: PollingWatchKind, - options: WatchOptions | undefined - ): WatchOptions { - const defaultFallbackPolling = options?.fallbackPolling; - return { - watchFile, - fallbackPolling: defaultFallbackPolling === undefined ? - fallbackPolling : - defaultFallbackPolling - }; + } + function watchDirectory(directoryName: string, callback: DirectoryWatcherCallback, recursive: boolean, options: WatchOptions | undefined): FileWatcher { + if (fsSupportsRecursiveFsWatch) { + return fsWatch(directoryName, FileSystemEntryKind.Directory, createFsWatchCallbackForDirectoryWatcherCallback(directoryName, callback), recursive, PollingInterval.Medium, getFallbackOptions(options)); } - - function watchDirectory(directoryName: string, callback: DirectoryWatcherCallback, recursive: boolean, options: WatchOptions | undefined): FileWatcher { - if (fsSupportsRecursiveFsWatch) { - return fsWatch( - directoryName, - FileSystemEntryKind.Directory, - createFsWatchCallbackForDirectoryWatcherCallback(directoryName, callback), - recursive, - PollingInterval.Medium, - getFallbackOptions(options) - ); - } - - if (!hostRecursiveDirectoryWatcher) { - hostRecursiveDirectoryWatcher = createDirectoryWatcherSupportingRecursive({ - useCaseSensitiveFileNames, - directoryExists, - getAccessibleSortedChildDirectories, - watchDirectory: nonRecursiveWatchDirectory, - realpath, - setTimeout, - clearTimeout - }); - } - return hostRecursiveDirectoryWatcher(directoryName, callback, recursive, options); + if (!hostRecursiveDirectoryWatcher) { + hostRecursiveDirectoryWatcher = createDirectoryWatcherSupportingRecursive({ + useCaseSensitiveFileNames, + directoryExists, + getAccessibleSortedChildDirectories, + watchDirectory: nonRecursiveWatchDirectory, + realpath, + setTimeout, + clearTimeout + }); } - - function nonRecursiveWatchDirectory(directoryName: string, callback: DirectoryWatcherCallback, recursive: boolean, options: WatchOptions | undefined): FileWatcher { - Debug.assert(!recursive); - options = updateOptionsForWatchDirectory(options); - const watchDirectoryKind = Debug.checkDefined(options.watchDirectory); - switch (watchDirectoryKind) { - case WatchDirectoryKind.FixedPollingInterval: - return pollingWatchFile( - directoryName, - () => callback(directoryName), - PollingInterval.Medium, - /*options*/ undefined - ); - case WatchDirectoryKind.DynamicPriorityPolling: - return ensureDynamicPollingWatchFile()( - directoryName, - () => callback(directoryName), - PollingInterval.Medium, - /*options*/ undefined - ); - case WatchDirectoryKind.UseFsEvents: - return fsWatch( - directoryName, - FileSystemEntryKind.Directory, - createFsWatchCallbackForDirectoryWatcherCallback(directoryName, callback), - recursive, - PollingInterval.Medium, - getFallbackOptions(options) - ); - default: - Debug.assertNever(watchDirectoryKind); - } + return hostRecursiveDirectoryWatcher(directoryName, callback, recursive, options); + } + function nonRecursiveWatchDirectory(directoryName: string, callback: DirectoryWatcherCallback, recursive: boolean, options: WatchOptions | undefined): FileWatcher { + Debug.assert(!recursive); + options = updateOptionsForWatchDirectory(options); + const watchDirectoryKind = Debug.checkDefined(options.watchDirectory); + switch (watchDirectoryKind) { + case WatchDirectoryKind.FixedPollingInterval: + return pollingWatchFile(directoryName, () => callback(directoryName), PollingInterval.Medium, + /*options*/ undefined); + case WatchDirectoryKind.DynamicPriorityPolling: + return ensureDynamicPollingWatchFile()(directoryName, () => callback(directoryName), PollingInterval.Medium, + /*options*/ undefined); + case WatchDirectoryKind.UseFsEvents: + return fsWatch(directoryName, FileSystemEntryKind.Directory, createFsWatchCallbackForDirectoryWatcherCallback(directoryName, callback), recursive, PollingInterval.Medium, getFallbackOptions(options)); + default: + Debug.assertNever(watchDirectoryKind); } - - function updateOptionsForWatchDirectory(options: WatchOptions | undefined): WatchOptions { - if (options && options.watchDirectory !== undefined) return options; - switch (tscWatchDirectory) { - case "RecursiveDirectoryUsingFsWatchFile": - // Use polling interval based on priority when create watch using host.watchFile - return { watchDirectory: WatchDirectoryKind.FixedPollingInterval }; - case "RecursiveDirectoryUsingDynamicPriorityPolling": - // Use polling interval but change the interval depending on file changes and their default polling interval - return { watchDirectory: WatchDirectoryKind.DynamicPriorityPolling }; - default: - const defaultFallbackPolling = options?.fallbackPolling; - return { - watchDirectory: WatchDirectoryKind.UseFsEvents, - fallbackPolling: defaultFallbackPolling !== undefined ? - defaultFallbackPolling : - undefined - }; - } + } + function updateOptionsForWatchDirectory(options: WatchOptions | undefined): WatchOptions { + if (options && options.watchDirectory !== undefined) + return options; + switch (tscWatchDirectory) { + case "RecursiveDirectoryUsingFsWatchFile": + // Use polling interval based on priority when create watch using host.watchFile + return { watchDirectory: WatchDirectoryKind.FixedPollingInterval }; + case "RecursiveDirectoryUsingDynamicPriorityPolling": + // Use polling interval but change the interval depending on file changes and their default polling interval + return { watchDirectory: WatchDirectoryKind.DynamicPriorityPolling }; + default: + const defaultFallbackPolling = options?.fallbackPolling; + return { + watchDirectory: WatchDirectoryKind.UseFsEvents, + fallbackPolling: defaultFallbackPolling !== undefined ? + defaultFallbackPolling : + undefined + }; } } - +} +/** + * patch writefile to create folder before writing the file + */ +/*@internal*/ +export function patchWriteFileEnsuringDirectory(sys: System) { + // patch writefile to create folder before writing the file + const originalWriteFile = sys.writeFile; + sys.writeFile = (path, data, writeBom) => writeFileEnsuringDirectories(path, data, !!writeBom, (path, data, writeByteOrderMark) => originalWriteFile.call(sys, path, data, writeByteOrderMark), path => sys.createDirectory(path), path => sys.directoryExists(path)); +} +/*@internal*/ +export type BufferEncoding = "ascii" | "utf8" | "utf-8" | "utf16le" | "ucs2" | "ucs-2" | "base64" | "latin1" | "binary" | "hex"; +/*@internal*/ +interface NodeBuffer extends Uint8Array { + constructor: any; + write(str: string, encoding?: BufferEncoding): number; + write(str: string, offset: number, encoding?: BufferEncoding): number; + write(str: string, offset: number, length: number, encoding?: BufferEncoding): number; + toString(encoding?: string, start?: number, end?: number): string; + toJSON(): { + type: "Buffer"; + data: number[]; + }; + equals(otherBuffer: Uint8Array): boolean; + compare(otherBuffer: Uint8Array, targetStart?: number, targetEnd?: number, sourceStart?: number, sourceEnd?: number): number; + copy(targetBuffer: Uint8Array, targetStart?: number, sourceStart?: number, sourceEnd?: number): number; + slice(begin?: number, end?: number): Buffer; + subarray(begin?: number, end?: number): Buffer; + writeUIntLE(value: number, offset: number, byteLength: number): number; + writeUIntBE(value: number, offset: number, byteLength: number): number; + writeIntLE(value: number, offset: number, byteLength: number): number; + writeIntBE(value: number, offset: number, byteLength: number): number; + readUIntLE(offset: number, byteLength: number): number; + readUIntBE(offset: number, byteLength: number): number; + readIntLE(offset: number, byteLength: number): number; + readIntBE(offset: number, byteLength: number): number; + readUInt8(offset: number): number; + readUInt16LE(offset: number): number; + readUInt16BE(offset: number): number; + readUInt32LE(offset: number): number; + readUInt32BE(offset: number): number; + readInt8(offset: number): number; + readInt16LE(offset: number): number; + readInt16BE(offset: number): number; + readInt32LE(offset: number): number; + readInt32BE(offset: number): number; + readFloatLE(offset: number): number; + readFloatBE(offset: number): number; + readDoubleLE(offset: number): number; + readDoubleBE(offset: number): number; + reverse(): this; + swap16(): Buffer; + swap32(): Buffer; + swap64(): Buffer; + writeUInt8(value: number, offset: number): number; + writeUInt16LE(value: number, offset: number): number; + writeUInt16BE(value: number, offset: number): number; + writeUInt32LE(value: number, offset: number): number; + writeUInt32BE(value: number, offset: number): number; + writeInt8(value: number, offset: number): number; + writeInt16LE(value: number, offset: number): number; + writeInt16BE(value: number, offset: number): number; + writeInt32LE(value: number, offset: number): number; + writeInt32BE(value: number, offset: number): number; + writeFloatLE(value: number, offset: number): number; + writeFloatBE(value: number, offset: number): number; + writeDoubleLE(value: number, offset: number): number; + writeDoubleBE(value: number, offset: number): number; + readBigUInt64BE?(offset?: number): bigint; + readBigUInt64LE?(offset?: number): bigint; + readBigInt64BE?(offset?: number): bigint; + readBigInt64LE?(offset?: number): bigint; + writeBigInt64BE?(value: bigint, offset?: number): number; + writeBigInt64LE?(value: bigint, offset?: number): number; + writeBigUInt64BE?(value: bigint, offset?: number): number; + writeBigUInt64LE?(value: bigint, offset?: number): number; + fill(value: string | Uint8Array | number, offset?: number, end?: number, encoding?: BufferEncoding): this; + indexOf(value: string | number | Uint8Array, byteOffset?: number, encoding?: BufferEncoding): number; + lastIndexOf(value: string | number | Uint8Array, byteOffset?: number, encoding?: BufferEncoding): number; + entries(): IterableIterator<[number, number]>; + includes(value: string | number | Buffer, byteOffset?: number, encoding?: BufferEncoding): boolean; + keys(): IterableIterator; + values(): IterableIterator; +} +/*@internal*/ +interface Buffer extends NodeBuffer { +} +// TODO: GH#18217 Methods on System are often used as if they are certainly defined +export interface System { + args: string[]; + newLine: string; + useCaseSensitiveFileNames: boolean; + write(s: string): void; + writeOutputIsTTY?(): boolean; + readFile(path: string, encoding?: string): string | undefined; + getFileSize?(path: string): number; + writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; /** - * patch writefile to create folder before writing the file + * @pollingInterval - this parameter is used in polling-based watchers and ignored in watchers that + * use native OS file watching */ - /*@internal*/ - export function patchWriteFileEnsuringDirectory(sys: System) { - // patch writefile to create folder before writing the file - const originalWriteFile = sys.writeFile; - sys.writeFile = (path, data, writeBom) => - writeFileEnsuringDirectories( - path, - data, - !!writeBom, - (path, data, writeByteOrderMark) => originalWriteFile.call(sys, path, data, writeByteOrderMark), - path => sys.createDirectory(path), - path => sys.directoryExists(path)); - } - - /*@internal*/ - export type BufferEncoding = "ascii" | "utf8" | "utf-8" | "utf16le" | "ucs2" | "ucs-2" | "base64" | "latin1" | "binary" | "hex"; - - /*@internal*/ - interface NodeBuffer extends Uint8Array { - constructor: any; - write(str: string, encoding?: BufferEncoding): number; - write(str: string, offset: number, encoding?: BufferEncoding): number; - write(str: string, offset: number, length: number, encoding?: BufferEncoding): number; - toString(encoding?: string, start?: number, end?: number): string; - toJSON(): { type: "Buffer"; data: number[] }; - equals(otherBuffer: Uint8Array): boolean; - compare( - otherBuffer: Uint8Array, - targetStart?: number, - targetEnd?: number, - sourceStart?: number, - sourceEnd?: number - ): number; - copy(targetBuffer: Uint8Array, targetStart?: number, sourceStart?: number, sourceEnd?: number): number; - slice(begin?: number, end?: number): Buffer; - subarray(begin?: number, end?: number): Buffer; - writeUIntLE(value: number, offset: number, byteLength: number): number; - writeUIntBE(value: number, offset: number, byteLength: number): number; - writeIntLE(value: number, offset: number, byteLength: number): number; - writeIntBE(value: number, offset: number, byteLength: number): number; - readUIntLE(offset: number, byteLength: number): number; - readUIntBE(offset: number, byteLength: number): number; - readIntLE(offset: number, byteLength: number): number; - readIntBE(offset: number, byteLength: number): number; - readUInt8(offset: number): number; - readUInt16LE(offset: number): number; - readUInt16BE(offset: number): number; - readUInt32LE(offset: number): number; - readUInt32BE(offset: number): number; - readInt8(offset: number): number; - readInt16LE(offset: number): number; - readInt16BE(offset: number): number; - readInt32LE(offset: number): number; - readInt32BE(offset: number): number; - readFloatLE(offset: number): number; - readFloatBE(offset: number): number; - readDoubleLE(offset: number): number; - readDoubleBE(offset: number): number; - reverse(): this; - swap16(): Buffer; - swap32(): Buffer; - swap64(): Buffer; - writeUInt8(value: number, offset: number): number; - writeUInt16LE(value: number, offset: number): number; - writeUInt16BE(value: number, offset: number): number; - writeUInt32LE(value: number, offset: number): number; - writeUInt32BE(value: number, offset: number): number; - writeInt8(value: number, offset: number): number; - writeInt16LE(value: number, offset: number): number; - writeInt16BE(value: number, offset: number): number; - writeInt32LE(value: number, offset: number): number; - writeInt32BE(value: number, offset: number): number; - writeFloatLE(value: number, offset: number): number; - writeFloatBE(value: number, offset: number): number; - writeDoubleLE(value: number, offset: number): number; - writeDoubleBE(value: number, offset: number): number; - readBigUInt64BE?(offset?: number): bigint; - readBigUInt64LE?(offset?: number): bigint; - readBigInt64BE?(offset?: number): bigint; - readBigInt64LE?(offset?: number): bigint; - writeBigInt64BE?(value: bigint, offset?: number): number; - writeBigInt64LE?(value: bigint, offset?: number): number; - writeBigUInt64BE?(value: bigint, offset?: number): number; - writeBigUInt64LE?(value: bigint, offset?: number): number; - fill(value: string | Uint8Array | number, offset?: number, end?: number, encoding?: BufferEncoding): this; - indexOf(value: string | number | Uint8Array, byteOffset?: number, encoding?: BufferEncoding): number; - lastIndexOf(value: string | number | Uint8Array, byteOffset?: number, encoding?: BufferEncoding): number; - entries(): IterableIterator<[number, number]>; - includes(value: string | number | Buffer, byteOffset?: number, encoding?: BufferEncoding): boolean; - keys(): IterableIterator; - values(): IterableIterator; - } - - /*@internal*/ - interface Buffer extends NodeBuffer { } - - // TODO: GH#18217 Methods on System are often used as if they are certainly defined - export interface System { - args: string[]; - newLine: string; - useCaseSensitiveFileNames: boolean; - write(s: string): void; - writeOutputIsTTY?(): boolean; - readFile(path: string, encoding?: string): string | undefined; - getFileSize?(path: string): number; - writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; - - /** - * @pollingInterval - this parameter is used in polling-based watchers and ignored in watchers that - * use native OS file watching - */ - watchFile?(path: string, callback: FileWatcherCallback, pollingInterval?: number, options?: WatchOptions): FileWatcher; - watchDirectory?(path: string, callback: DirectoryWatcherCallback, recursive?: boolean, options?: WatchOptions): FileWatcher; - resolvePath(path: string): string; - fileExists(path: string): boolean; - directoryExists(path: string): boolean; - createDirectory(path: string): void; - getExecutingFilePath(): string; - getCurrentDirectory(): string; - getDirectories(path: string): string[]; - readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[]; - getModifiedTime?(path: string): Date | undefined; - setModifiedTime?(path: string, time: Date): void; - deleteFile?(path: string): void; - /** - * A good implementation is node.js' `crypto.createHash`. (https://nodejs.org/api/crypto.html#crypto_crypto_createhash_algorithm) - */ - createHash?(data: string): string; - /** This must be cryptographically secure. Only implement this method using `crypto.createHash("sha256")`. */ - createSHA256Hash?(data: string): string; - getMemoryUsage?(): number; - exit(exitCode?: number): void; - /*@internal*/ enableCPUProfiler?(path: string, continuation: () => void): boolean; - /*@internal*/ disableCPUProfiler?(continuation: () => void): boolean; - realpath?(path: string): string; - /*@internal*/ getEnvironmentVariable(name: string): string; - /*@internal*/ tryEnableSourceMapsForHost?(): void; - /*@internal*/ debugMode?: boolean; - setTimeout?(callback: (...args: any[]) => void, ms: number, ...args: any[]): any; - clearTimeout?(timeoutId: any): void; - clearScreen?(): void; - /*@internal*/ setBlocking?(): void; - base64decode?(input: string): string; - base64encode?(input: string): string; - /*@internal*/ bufferFrom?(input: string, encoding?: string): Buffer; - // For testing - /*@internal*/ now?(): Date; - /*@internal*/ require?(baseDir: string, moduleName: string): RequireResult; + watchFile?(path: string, callback: FileWatcherCallback, pollingInterval?: number, options?: WatchOptions): FileWatcher; + watchDirectory?(path: string, callback: DirectoryWatcherCallback, recursive?: boolean, options?: WatchOptions): FileWatcher; + resolvePath(path: string): string; + fileExists(path: string): boolean; + directoryExists(path: string): boolean; + createDirectory(path: string): void; + getExecutingFilePath(): string; + getCurrentDirectory(): string; + getDirectories(path: string): string[]; + readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[]; + getModifiedTime?(path: string): Date | undefined; + setModifiedTime?(path: string, time: Date): void; + deleteFile?(path: string): void; + /** + * A good implementation is node.js' `crypto.createHash`. (https://nodejs.org/api/crypto.html#crypto_crypto_createhash_algorithm) + */ + createHash?(data: string): string; + /** This must be cryptographically secure. Only implement this method using `crypto.createHash("sha256")`. */ + createSHA256Hash?(data: string): string; + getMemoryUsage?(): number; + exit(exitCode?: number): void; + /*@internal*/ enableCPUProfiler?(path: string, continuation: () => void): boolean; + /*@internal*/ disableCPUProfiler?(continuation: () => void): boolean; + realpath?(path: string): string; + /*@internal*/ getEnvironmentVariable(name: string): string; + /*@internal*/ tryEnableSourceMapsForHost?(): void; + /*@internal*/ debugMode?: boolean; + setTimeout?(callback: (...args: any[]) => void, ms: number, ...args: any[]): any; + clearTimeout?(timeoutId: any): void; + clearScreen?(): void; + /*@internal*/ setBlocking?(): void; + base64decode?(input: string): string; + base64encode?(input: string): string; + /*@internal*/ bufferFrom?(input: string, encoding?: string): Buffer; + // For testing + /*@internal*/ now?(): Date; + /*@internal*/ require?(baseDir: string, moduleName: string): RequireResult; +} +export interface FileWatcher { + close(): void; +} +interface DirectoryWatcher extends FileWatcher { + referenceCount: number; +} +declare const require: any; +declare const process: any; +declare const global: any; +declare const __filename: string; +declare const __dirname: string; +export function getNodeMajorVersion(): number | undefined { + if (typeof process === "undefined") { + return undefined; } - - export interface FileWatcher { - close(): void; + const version: string = process.version; + if (!version) { + return undefined; } - - interface DirectoryWatcher extends FileWatcher { - referenceCount: number; + const dot = version.indexOf("."); + if (dot === -1) { + return undefined; } - - declare const require: any; - declare const process: any; - declare const global: any; - declare const __filename: string; - declare const __dirname: string; - - export function getNodeMajorVersion(): number | undefined { - if (typeof process === "undefined") { - return undefined; - } - const version: string = process.version; - if (!version) { - return undefined; + return parseInt(version.substring(1, dot)); +} +// TODO: GH#18217 this is used as if it's certainly defined in many places. +// eslint-disable-next-line prefer-const +export let sys: System = (() => { + // NodeJS detects "\uFEFF" at the start of the string and *replaces* it with the actual + // byte order mark from the specified encoding. Using any other byte order mark does + // not actually work. + const byteOrderMarkIndicator = "\uFEFF"; + function getNodeSystem(): System { + const nativePattern = /^native |^\([^)]+\)$|^(internal[\\/]|[a-zA-Z0-9_\s]+(\.js)?$)/; + const _fs: typeof import("fs") = require("fs"); + const _path: typeof import("path") = require("path"); + const _os = require("os"); + // crypto can be absent on reduced node installations + let _crypto: typeof import("crypto") | undefined; + try { + _crypto = require("crypto"); } - const dot = version.indexOf("."); - if (dot === -1) { - return undefined; + catch { + _crypto = undefined; } - return parseInt(version.substring(1, dot)); - } - - // TODO: GH#18217 this is used as if it's certainly defined in many places. - // eslint-disable-next-line prefer-const - export let sys: System = (() => { - // NodeJS detects "\uFEFF" at the start of the string and *replaces* it with the actual - // byte order mark from the specified encoding. Using any other byte order mark does - // not actually work. - const byteOrderMarkIndicator = "\uFEFF"; - - function getNodeSystem(): System { - const nativePattern = /^native |^\([^)]+\)$|^(internal[\\/]|[a-zA-Z0-9_\s]+(\.js)?$)/; - const _fs: typeof import("fs") = require("fs"); - const _path: typeof import("path") = require("path"); - const _os = require("os"); - // crypto can be absent on reduced node installations - let _crypto: typeof import("crypto") | undefined; - try { - _crypto = require("crypto"); - } - catch { - _crypto = undefined; - } - let activeSession: import("inspector").Session | "stopping" | undefined; - let profilePath = "./profile.cpuprofile"; - - const Buffer: { - new (input: string, encoding?: string): any; - from?(input: string, encoding?: string): any; - } = require("buffer").Buffer; - - const nodeVersion = getNodeMajorVersion(); - const isNode4OrLater = nodeVersion! >= 4; - const isLinuxOrMacOs = process.platform === "linux" || process.platform === "darwin"; - - const platform: string = _os.platform(); - const useCaseSensitiveFileNames = isFileSystemCaseSensitive(); - const fsSupportsRecursiveFsWatch = isNode4OrLater && (process.platform === "win32" || process.platform === "darwin"); - const { watchFile, watchDirectory } = createSystemWatchFunctions({ - pollingWatchFile: createSingleFileWatcherPerName(fsWatchFileWorker, useCaseSensitiveFileNames), - getModifiedTime, - setTimeout, - clearTimeout, - fsWatch, - useCaseSensitiveFileNames, - fileExists, - // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows - // (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643) - fsSupportsRecursiveFsWatch, - directoryExists, - getAccessibleSortedChildDirectories: path => getAccessibleFileSystemEntries(path).directories, - realpath, - tscWatchFile: process.env.TSC_WATCHFILE, - useNonPollingWatchers: process.env.TSC_NONPOLLING_WATCHER, - tscWatchDirectory: process.env.TSC_WATCHDIRECTORY, - }); - const nodeSystem: System = { - args: process.argv.slice(2), - newLine: _os.EOL, - useCaseSensitiveFileNames, - write(s: string): void { - process.stdout.write(s); - }, - writeOutputIsTTY() { - return process.stdout.isTTY; - }, - readFile, - writeFile, - watchFile, - watchDirectory, - resolvePath: path => _path.resolve(path), - fileExists, - directoryExists, - createDirectory(directoryName: string) { - if (!nodeSystem.directoryExists(directoryName)) { - // Wrapped in a try-catch to prevent crashing if we are in a race - // with another copy of ourselves to create the same directory - try { - _fs.mkdirSync(directoryName); - } - catch (e) { - if (e.code !== "EEXIST") { - // Failed for some other reason (access denied?); still throw - throw e; - } - } - } - }, - getExecutingFilePath() { - return __filename; - }, - getCurrentDirectory() { - return process.cwd(); - }, - getDirectories, - getEnvironmentVariable(name: string) { - return process.env[name] || ""; - }, - readDirectory, - getModifiedTime, - setModifiedTime, - deleteFile, - createHash: _crypto ? createSHA256Hash : generateDjb2Hash, - createSHA256Hash: _crypto ? createSHA256Hash : undefined, - getMemoryUsage() { - if (global.gc) { - global.gc(); - } - return process.memoryUsage().heapUsed; - }, - getFileSize(path) { - try { - const stat = _fs.statSync(path); - if (stat.isFile()) { - return stat.size; - } - } - catch { /*ignore*/ } - return 0; - }, - exit(exitCode?: number): void { - disableCPUProfiler(() => process.exit(exitCode)); - }, - enableCPUProfiler, - disableCPUProfiler, - realpath, - debugMode: some(process.execArgv, arg => /^--(inspect|debug)(-brk)?(=\d+)?$/i.test(arg)), - tryEnableSourceMapsForHost() { + let activeSession: import("inspector").Session | "stopping" | undefined; + let profilePath = "./profile.cpuprofile"; + const Buffer: { + new (input: string, encoding?: string): any; + from?(input: string, encoding?: string): any; + } = require("buffer").Buffer; + const nodeVersion = getNodeMajorVersion(); + const isNode4OrLater = nodeVersion! >= 4; + const isLinuxOrMacOs = process.platform === "linux" || process.platform === "darwin"; + const platform: string = _os.platform(); + const useCaseSensitiveFileNames = isFileSystemCaseSensitive(); + const fsSupportsRecursiveFsWatch = isNode4OrLater && (process.platform === "win32" || process.platform === "darwin"); + const { watchFile, watchDirectory } = createSystemWatchFunctions({ + pollingWatchFile: createSingleFileWatcherPerName(fsWatchFileWorker, useCaseSensitiveFileNames), + getModifiedTime, + setTimeout, + clearTimeout, + fsWatch, + useCaseSensitiveFileNames, + fileExists, + // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows + // (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643) + fsSupportsRecursiveFsWatch, + directoryExists, + getAccessibleSortedChildDirectories: path => getAccessibleFileSystemEntries(path).directories, + realpath, + tscWatchFile: process.env.TSC_WATCHFILE, + useNonPollingWatchers: process.env.TSC_NONPOLLING_WATCHER, + tscWatchDirectory: process.env.TSC_WATCHDIRECTORY, + }); + const nodeSystem: System = { + args: process.argv.slice(2), + newLine: _os.EOL, + useCaseSensitiveFileNames, + write(s: string): void { + process.stdout.write(s); + }, + writeOutputIsTTY() { + return process.stdout.isTTY; + }, + readFile, + writeFile, + watchFile, + watchDirectory, + resolvePath: path => _path.resolve(path), + fileExists, + directoryExists, + createDirectory(directoryName: string) { + if (!nodeSystem.directoryExists(directoryName)) { + // Wrapped in a try-catch to prevent crashing if we are in a race + // with another copy of ourselves to create the same directory try { - require("source-map-support").install(); - } - catch { - // Could not enable source maps. + _fs.mkdirSync(directoryName); } - }, - setTimeout, - clearTimeout, - clearScreen: () => { - process.stdout.write("\x1Bc"); - }, - setBlocking: () => { - if (process.stdout && process.stdout._handle && process.stdout._handle.setBlocking) { - process.stdout._handle.setBlocking(true); - } - }, - bufferFrom, - base64decode: input => bufferFrom(input, "base64").toString("utf8"), - base64encode: input => bufferFrom(input).toString("base64"), - require: (baseDir, moduleName) => { - try { - const modulePath = resolveJSModule(moduleName, baseDir, nodeSystem); - return { module: require(modulePath), modulePath, error: undefined }; + catch (e) { + if (e.code !== "EEXIST") { + // Failed for some other reason (access denied?); still throw + throw e; + } } - catch (error) { - return { module: undefined, modulePath: undefined, error }; + } + }, + getExecutingFilePath() { + return __filename; + }, + getCurrentDirectory() { + return process.cwd(); + }, + getDirectories, + getEnvironmentVariable(name: string) { + return process.env[name] || ""; + }, + readDirectory, + getModifiedTime, + setModifiedTime, + deleteFile, + createHash: _crypto ? createSHA256Hash : generateDjb2Hash, + createSHA256Hash: _crypto ? createSHA256Hash : undefined, + getMemoryUsage() { + if (global.gc) { + global.gc(); + } + return process.memoryUsage().heapUsed; + }, + getFileSize(path) { + try { + const stat = _fs.statSync(path); + if (stat.isFile()) { + return stat.size; } } - }; - return nodeSystem; - - /** - * Uses the builtin inspector APIs to capture a CPU profile - * See https://nodejs.org/api/inspector.html#inspector_example_usage for details - */ - function enableCPUProfiler(path: string, cb: () => void) { - if (activeSession) { - cb(); - return false; + catch { /*ignore*/ } + return 0; + }, + exit(exitCode?: number): void { + disableCPUProfiler(() => process.exit(exitCode)); + }, + enableCPUProfiler, + disableCPUProfiler, + realpath, + debugMode: some((process.execArgv), arg => /^--(inspect|debug)(-brk)?(=\d+)?$/i.test(arg)), + tryEnableSourceMapsForHost() { + try { + require("source-map-support").install(); } - const inspector: typeof import("inspector") = require("inspector"); - if (!inspector || !inspector.Session) { - cb(); - return false; + catch { + // Could not enable source maps. } - const session = new inspector.Session(); - session.connect(); - - session.post("Profiler.enable", () => { - session.post("Profiler.start", () => { - activeSession = session; - profilePath = path; - cb(); - }); - }); - return true; - } - - /** - * Strips non-TS paths from the profile, so users with private projects shouldn't - * need to worry about leaking paths by submitting a cpu profile to us - */ - function cleanupPaths(profile: import("inspector").Profiler.Profile) { - let externalFileCounter = 0; - const remappedPaths = createMap(); - const normalizedDir = normalizeSlashes(__dirname); - // Windows rooted dir names need an extra `/` prepended to be valid file:/// urls - const fileUrlRoot = `file://${getRootLength(normalizedDir) === 1 ? "" : "/"}${normalizedDir}`; - for (const node of profile.nodes) { - if (node.callFrame.url) { - const url = normalizeSlashes(node.callFrame.url); - if (containsPath(fileUrlRoot, url, useCaseSensitiveFileNames)) { - node.callFrame.url = getRelativePathToDirectoryOrUrl(fileUrlRoot, url, fileUrlRoot, createGetCanonicalFileName(useCaseSensitiveFileNames), /*isAbsolutePathAnUrl*/ true); - } - else if (!nativePattern.test(url)) { - node.callFrame.url = (remappedPaths.has(url) ? remappedPaths : remappedPaths.set(url, `external${externalFileCounter}.js`)).get(url)!; - externalFileCounter++; - } - } + }, + setTimeout, + clearTimeout, + clearScreen: () => { + process.stdout.write("\x1Bc"); + }, + setBlocking: () => { + if (process.stdout && process.stdout._handle && process.stdout._handle.setBlocking) { + process.stdout._handle.setBlocking(true); } - return profile; - } - - function disableCPUProfiler(cb: () => void) { - if (activeSession && activeSession !== "stopping") { - const s = activeSession; - activeSession.post("Profiler.stop", (err, { profile }) => { - if (!err) { - try { - if (_fs.statSync(profilePath).isDirectory()) { - profilePath = _path.join(profilePath, `${(new Date()).toISOString().replace(/:/g, "-")}+P${process.pid}.cpuprofile`); - } - } - catch { - // do nothing and ignore fallible fs operation - } - try { - _fs.mkdirSync(_path.dirname(profilePath), { recursive: true }); - } - catch { - // do nothing and ignore fallible fs operation - } - _fs.writeFileSync(profilePath, JSON.stringify(cleanupPaths(profile))); - } - activeSession = undefined; - s.disconnect(); - cb(); - }); - activeSession = "stopping"; - return true; + }, + bufferFrom, + base64decode: input => bufferFrom(input, "base64").toString("utf8"), + base64encode: input => bufferFrom(input).toString("base64"), + require: (baseDir, moduleName) => { + try { + const modulePath = resolveJSModule(moduleName, baseDir, nodeSystem); + return { module: require(modulePath), modulePath, error: undefined }; } - else { - cb(); - return false; + catch (error) { + return { module: undefined, modulePath: undefined, error }; } } - - function bufferFrom(input: string, encoding?: string): Buffer { - // See https://github.com/Microsoft/TypeScript/issues/25652 - return Buffer.from && (Buffer.from as Function) !== Int8Array.from - ? Buffer.from(input, encoding) - : new Buffer(input, encoding); + }; + return nodeSystem; + /** + * Uses the builtin inspector APIs to capture a CPU profile + * See https://nodejs.org/api/inspector.html#inspector_example_usage for details + */ + function enableCPUProfiler(path: string, cb: () => void) { + if (activeSession) { + cb(); + return false; } - - function isFileSystemCaseSensitive(): boolean { - // win32\win64 are case insensitive platforms - if (platform === "win32" || platform === "win64") { - return false; - } - // If this file exists under a different case, we must be case-insensitve. - return !fileExists(swapCase(__filename)); + const inspector: typeof import("inspector") = require("inspector"); + if (!inspector || !inspector.Session) { + cb(); + return false; } - - /** Convert all lowercase chars to uppercase, and vice-versa */ - function swapCase(s: string): string { - return s.replace(/\w/g, (ch) => { - const up = ch.toUpperCase(); - return ch === up ? ch.toLowerCase() : up; + const session = new inspector.Session(); + session.connect(); + session.post("Profiler.enable", () => { + session.post("Profiler.start", () => { + activeSession = session; + profilePath = path; + cb(); }); - } - - function fsWatchFileWorker(fileName: string, callback: FileWatcherCallback, pollingInterval: number): FileWatcher { - _fs.watchFile(fileName, { persistent: true, interval: pollingInterval }, fileChanged); - let eventKind: FileWatcherEventKind; - return { - close: () => _fs.unwatchFile(fileName, fileChanged) - }; - - function fileChanged(curr: any, prev: any) { - // previous event kind check is to ensure we recongnize the file as previously also missing when it is restored or renamed twice (that is it disappears and reappears) - // In such case, prevTime returned is same as prev time of event when file was deleted as per node documentation - const isPreviouslyDeleted = +prev.mtime === 0 || eventKind === FileWatcherEventKind.Deleted; - if (+curr.mtime === 0) { - if (isPreviouslyDeleted) { - // Already deleted file, no need to callback again - return; - } - eventKind = FileWatcherEventKind.Deleted; - } - else if (isPreviouslyDeleted) { - eventKind = FileWatcherEventKind.Created; - } - // If there is no change in modified time, ignore the event - else if (+curr.mtime === +prev.mtime) { - return; + }); + return true; + } + /** + * Strips non-TS paths from the profile, so users with private projects shouldn't + * need to worry about leaking paths by submitting a cpu profile to us + */ + function cleanupPaths(profile: import("inspector").Profiler.Profile) { + let externalFileCounter = 0; + const remappedPaths = createMap(); + const normalizedDir = normalizeSlashes(__dirname); + // Windows rooted dir names need an extra `/` prepended to be valid file:/// urls + const fileUrlRoot = `file://${getRootLength(normalizedDir) === 1 ? "" : "/"}${normalizedDir}`; + for (const node of profile.nodes) { + if (node.callFrame.url) { + const url = normalizeSlashes(node.callFrame.url); + if (containsPath(fileUrlRoot, url, useCaseSensitiveFileNames)) { + node.callFrame.url = getRelativePathToDirectoryOrUrl(fileUrlRoot, url, fileUrlRoot, createGetCanonicalFileName(useCaseSensitiveFileNames), /*isAbsolutePathAnUrl*/ true); } - else { - // File changed - eventKind = FileWatcherEventKind.Changed; + else if (!nativePattern.test(url)) { + node.callFrame.url = (remappedPaths.has(url) ? remappedPaths : remappedPaths.set(url, `external${externalFileCounter}.js`)).get(url)!; + externalFileCounter++; } - callback(fileName, eventKind); } } - - function fsWatch( - fileOrDirectory: string, - entryKind: FileSystemEntryKind, - callback: FsWatchCallback, - recursive: boolean, - fallbackPollingInterval: PollingInterval, - fallbackOptions: WatchOptions | undefined - ): FileWatcher { - let options: any; - let lastDirectoryPartWithDirectorySeparator: string | undefined; - let lastDirectoryPart: string | undefined; - if (isLinuxOrMacOs) { - lastDirectoryPartWithDirectorySeparator = fileOrDirectory.substr(fileOrDirectory.lastIndexOf(directorySeparator)); - lastDirectoryPart = lastDirectoryPartWithDirectorySeparator.slice(directorySeparator.length); - } - /** Watcher for the file system entry depending on whether it is missing or present */ - let watcher = !fileSystemEntryExists(fileOrDirectory, entryKind) ? - watchMissingFileSystemEntry() : - watchPresentFileSystemEntry(); - return { - close: () => { - // Close the watcher (either existing file system entry watcher or missing file system entry watcher) - watcher.close(); - watcher = undefined!; - } - }; - - /** - * Invoke the callback with rename and update the watcher if not closed - * @param createWatcher - */ - function invokeCallbackAndUpdateWatcher(createWatcher: () => FileWatcher) { - sysLog(`sysLog:: ${fileOrDirectory}:: Changing watcher to ${createWatcher === watchPresentFileSystemEntry ? "Present" : "Missing"}FileSystemEntryWatcher`); - // Call the callback for current directory - callback("rename", ""); - - // If watcher is not closed, update it - if (watcher) { - watcher.close(); - watcher = createWatcher(); - } - } - - /** - * Watch the file or directory that is currently present - * and when the watched file or directory is deleted, switch to missing file system entry watcher - */ - function watchPresentFileSystemEntry(): FileWatcher { - // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows - // (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643) - if (options === undefined) { - if (fsSupportsRecursiveFsWatch) { - options = { persistent: true, recursive: !!recursive }; + return profile; + } + function disableCPUProfiler(cb: () => void) { + if (activeSession && activeSession !== "stopping") { + const s = activeSession; + activeSession.post("Profiler.stop", (err, { profile }) => { + if (!err) { + try { + if (_fs.statSync(profilePath).isDirectory()) { + profilePath = _path.join(profilePath, `${(new Date()).toISOString().replace(/:/g, "-")}+P${process.pid}.cpuprofile`); + } } - else { - options = { persistent: true }; + catch { + // do nothing and ignore fallible fs operation } + try { + _fs.mkdirSync(_path.dirname(profilePath), { recursive: true }); + } + catch { + // do nothing and ignore fallible fs operation + } + _fs.writeFileSync(profilePath, JSON.stringify(cleanupPaths(profile))); } - try { - const presentWatcher = _fs.watch( - fileOrDirectory, - options, - isLinuxOrMacOs ? - callbackChangingToMissingFileSystemEntry : - callback - ); - // Watch the missing file or directory or error - presentWatcher.on("error", () => invokeCallbackAndUpdateWatcher(watchMissingFileSystemEntry)); - return presentWatcher; - } - catch (e) { - // Catch the exception and use polling instead - // Eg. on linux the number of watches are limited and one could easily exhaust watches and the exception ENOSPC is thrown when creating watcher at that point - // so instead of throwing error, use fs.watchFile - return watchPresentFileSystemEntryWithFsWatchFile(); + activeSession = undefined; + s.disconnect(); + cb(); + }); + activeSession = "stopping"; + return true; + } + else { + cb(); + return false; + } + } + function bufferFrom(input: string, encoding?: string): Buffer { + // See https://github.com/Microsoft/TypeScript/issues/25652 + return Buffer.from && (Buffer.from as Function) !== Int8Array.from + ? Buffer.from(input, encoding) + : new Buffer(input, encoding); + } + function isFileSystemCaseSensitive(): boolean { + // win32\win64 are case insensitive platforms + if (platform === "win32" || platform === "win64") { + return false; + } + // If this file exists under a different case, we must be case-insensitve. + return !fileExists(swapCase(__filename)); + } + /** Convert all lowercase chars to uppercase, and vice-versa */ + function swapCase(s: string): string { + return s.replace(/\w/g, (ch) => { + const up = ch.toUpperCase(); + return ch === up ? ch.toLowerCase() : up; + }); + } + function fsWatchFileWorker(fileName: string, callback: FileWatcherCallback, pollingInterval: number): FileWatcher { + _fs.watchFile(fileName, { persistent: true, interval: pollingInterval }, fileChanged); + let eventKind: FileWatcherEventKind; + return { + close: () => _fs.unwatchFile(fileName, fileChanged) + }; + function fileChanged(curr: any, prev: any) { + // previous event kind check is to ensure we recongnize the file as previously also missing when it is restored or renamed twice (that is it disappears and reappears) + // In such case, prevTime returned is same as prev time of event when file was deleted as per node documentation + const isPreviouslyDeleted = +prev.mtime === 0 || eventKind === FileWatcherEventKind.Deleted; + if (+curr.mtime === 0) { + if (isPreviouslyDeleted) { + // Already deleted file, no need to callback again + return; } + eventKind = FileWatcherEventKind.Deleted; } - - function callbackChangingToMissingFileSystemEntry(event: "rename" | "change", relativeName: string | undefined) { - // because relativeName is not guaranteed to be correct we need to check on each rename with few combinations - // Eg on ubuntu while watching app/node_modules the relativeName is "node_modules" which is neither relative nor full path - return event === "rename" && - (!relativeName || - relativeName === lastDirectoryPart || - relativeName.lastIndexOf(lastDirectoryPartWithDirectorySeparator!) === relativeName.length - lastDirectoryPartWithDirectorySeparator!.length) && - !fileSystemEntryExists(fileOrDirectory, entryKind) ? - invokeCallbackAndUpdateWatcher(watchMissingFileSystemEntry) : - callback(event, relativeName); + else if (isPreviouslyDeleted) { + eventKind = FileWatcherEventKind.Created; } - - /** - * Watch the file or directory using fs.watchFile since fs.watch threw exception - * Eg. on linux the number of watches are limited and one could easily exhaust watches and the exception ENOSPC is thrown when creating watcher at that point - */ - function watchPresentFileSystemEntryWithFsWatchFile(): FileWatcher { - sysLog(`sysLog:: ${fileOrDirectory}:: Changing to fsWatchFile`); - return watchFile( - fileOrDirectory, - createFileWatcherCallback(callback), - fallbackPollingInterval, - fallbackOptions - ); + // If there is no change in modified time, ignore the event + else if (+curr.mtime === +prev.mtime) { + return; } - - /** - * Watch the file or directory that is missing - * and switch to existing file or directory when the missing filesystem entry is created - */ - function watchMissingFileSystemEntry(): FileWatcher { - return watchFile( - fileOrDirectory, - (_fileName, eventKind) => { - if (eventKind === FileWatcherEventKind.Created && fileSystemEntryExists(fileOrDirectory, entryKind)) { - // Call the callback for current file or directory - // For now it could be callback for the inner directory creation, - // but just return current directory, better than current no-op - invokeCallbackAndUpdateWatcher(watchPresentFileSystemEntry); - } - }, - fallbackPollingInterval, - fallbackOptions - ); + else { + // File changed + eventKind = FileWatcherEventKind.Changed; } + callback(fileName, eventKind); } - - function readFileWorker(fileName: string, _encoding?: string): string | undefined { - let buffer: Buffer; - try { - buffer = _fs.readFileSync(fileName); + } + function fsWatch(fileOrDirectory: string, entryKind: FileSystemEntryKind, callback: FsWatchCallback, recursive: boolean, fallbackPollingInterval: PollingInterval, fallbackOptions: WatchOptions | undefined): FileWatcher { + let options: any; + let lastDirectoryPartWithDirectorySeparator: string | undefined; + let lastDirectoryPart: string | undefined; + if (isLinuxOrMacOs) { + lastDirectoryPartWithDirectorySeparator = fileOrDirectory.substr(fileOrDirectory.lastIndexOf(directorySeparator)); + lastDirectoryPart = lastDirectoryPartWithDirectorySeparator.slice(directorySeparator.length); + } + /** Watcher for the file system entry depending on whether it is missing or present */ + let watcher = !fileSystemEntryExists(fileOrDirectory, entryKind) ? + watchMissingFileSystemEntry() : + watchPresentFileSystemEntry(); + return { + close: () => { + // Close the watcher (either existing file system entry watcher or missing file system entry watcher) + watcher.close(); + watcher = undefined!; } - catch (e) { - return undefined; + }; + /** + * Invoke the callback with rename and update the watcher if not closed + * @param createWatcher + */ + function invokeCallbackAndUpdateWatcher(createWatcher: () => FileWatcher) { + sysLog(`sysLog:: ${fileOrDirectory}:: Changing watcher to ${createWatcher === watchPresentFileSystemEntry ? "Present" : "Missing"}FileSystemEntryWatcher`); + // Call the callback for current directory + callback("rename", ""); + // If watcher is not closed, update it + if (watcher) { + watcher.close(); + watcher = createWatcher(); } - let len = buffer.length; - if (len >= 2 && buffer[0] === 0xFE && buffer[1] === 0xFF) { - // Big endian UTF-16 byte order mark detected. Since big endian is not supported by node.js, - // flip all byte pairs and treat as little endian. - len &= ~1; // Round down to a multiple of 2 - for (let i = 0; i < len; i += 2) { - const temp = buffer[i]; - buffer[i] = buffer[i + 1]; - buffer[i + 1] = temp; + } + /** + * Watch the file or directory that is currently present + * and when the watched file or directory is deleted, switch to missing file system entry watcher + */ + function watchPresentFileSystemEntry(): FileWatcher { + // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows + // (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643) + if (options === undefined) { + if (fsSupportsRecursiveFsWatch) { + options = { persistent: true, recursive: !!recursive }; + } + else { + options = { persistent: true }; } - return buffer.toString("utf16le", 2); } - if (len >= 2 && buffer[0] === 0xFF && buffer[1] === 0xFE) { - // Little endian UTF-16 byte order mark detected - return buffer.toString("utf16le", 2); + try { + const presentWatcher = _fs.watch(fileOrDirectory, options, isLinuxOrMacOs ? + callbackChangingToMissingFileSystemEntry : + callback); + // Watch the missing file or directory or error + presentWatcher.on("error", () => invokeCallbackAndUpdateWatcher(watchMissingFileSystemEntry)); + return presentWatcher; } - if (len >= 3 && buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) { - // UTF-8 byte order mark detected - return buffer.toString("utf8", 3); + catch (e) { + // Catch the exception and use polling instead + // Eg. on linux the number of watches are limited and one could easily exhaust watches and the exception ENOSPC is thrown when creating watcher at that point + // so instead of throwing error, use fs.watchFile + return watchPresentFileSystemEntryWithFsWatchFile(); } - // Default is UTF-8 with no byte order mark - return buffer.toString("utf8"); } - - function readFile(fileName: string, _encoding?: string): string | undefined { - perfLogger.logStartReadFile(fileName); - const file = readFileWorker(fileName, _encoding); - perfLogger.logStopReadFile(); - return file; + function callbackChangingToMissingFileSystemEntry(event: "rename" | "change", relativeName: string | undefined) { + // because relativeName is not guaranteed to be correct we need to check on each rename with few combinations + // Eg on ubuntu while watching app/node_modules the relativeName is "node_modules" which is neither relative nor full path + return event === "rename" && + (!relativeName || + relativeName === lastDirectoryPart || + relativeName.lastIndexOf(lastDirectoryPartWithDirectorySeparator!) === relativeName.length - lastDirectoryPartWithDirectorySeparator!.length) && + !fileSystemEntryExists(fileOrDirectory, entryKind) ? + invokeCallbackAndUpdateWatcher(watchMissingFileSystemEntry) : + callback(event, relativeName); } - - function writeFile(fileName: string, data: string, writeByteOrderMark?: boolean): void { - perfLogger.logEvent("WriteFile: " + fileName); - // If a BOM is required, emit one - if (writeByteOrderMark) { - data = byteOrderMarkIndicator + data; - } - - let fd: number | undefined; - - try { - fd = _fs.openSync(fileName, "w"); - _fs.writeSync(fd, data, /*position*/ undefined, "utf8"); - } - finally { - if (fd !== undefined) { - _fs.closeSync(fd); + /** + * Watch the file or directory using fs.watchFile since fs.watch threw exception + * Eg. on linux the number of watches are limited and one could easily exhaust watches and the exception ENOSPC is thrown when creating watcher at that point + */ + function watchPresentFileSystemEntryWithFsWatchFile(): FileWatcher { + sysLog(`sysLog:: ${fileOrDirectory}:: Changing to fsWatchFile`); + return watchFile(fileOrDirectory, createFileWatcherCallback(callback), fallbackPollingInterval, fallbackOptions); + } + /** + * Watch the file or directory that is missing + * and switch to existing file or directory when the missing filesystem entry is created + */ + function watchMissingFileSystemEntry(): FileWatcher { + return watchFile(fileOrDirectory, (_fileName, eventKind) => { + if (eventKind === FileWatcherEventKind.Created && fileSystemEntryExists(fileOrDirectory, entryKind)) { + // Call the callback for current file or directory + // For now it could be callback for the inner directory creation, + // but just return current directory, better than current no-op + invokeCallbackAndUpdateWatcher(watchPresentFileSystemEntry); } + }, fallbackPollingInterval, fallbackOptions); + } + } + function readFileWorker(fileName: string, _encoding?: string): string | undefined { + let buffer: Buffer; + try { + buffer = _fs.readFileSync(fileName); + } + catch (e) { + return undefined; + } + let len = buffer.length; + if (len >= 2 && buffer[0] === 0xFE && buffer[1] === 0xFF) { + // Big endian UTF-16 byte order mark detected. Since big endian is not supported by node.js, + // flip all byte pairs and treat as little endian. + len &= ~1; // Round down to a multiple of 2 + for (let i = 0; i < len; i += 2) { + const temp = buffer[i]; + buffer[i] = buffer[i + 1]; + buffer[i + 1] = temp; } + return buffer.toString("utf16le", 2); } - - function getAccessibleFileSystemEntries(path: string): FileSystemEntries { - perfLogger.logEvent("ReadDir: " + (path || ".")); - try { - const entries = _fs.readdirSync(path || ".", { withFileTypes: true }); - const files: string[] = []; - const directories: string[] = []; - for (const dirent of entries) { - // withFileTypes is not supported before Node 10.10. - const entry = typeof dirent === "string" ? dirent : dirent.name; - - // This is necessary because on some file system node fails to exclude - // "." and "..". See https://github.com/nodejs/node/issues/4002 - if (entry === "." || entry === "..") { - continue; - } - - let stat: any; - if (typeof dirent === "string" || dirent.isSymbolicLink()) { - const name = combinePaths(path, entry); - - try { - stat = _fs.statSync(name); - } - catch (e) { - continue; - } - } - else { - stat = dirent; - } - - if (stat.isFile()) { - files.push(entry); + if (len >= 2 && buffer[0] === 0xFF && buffer[1] === 0xFE) { + // Little endian UTF-16 byte order mark detected + return buffer.toString("utf16le", 2); + } + if (len >= 3 && buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) { + // UTF-8 byte order mark detected + return buffer.toString("utf8", 3); + } + // Default is UTF-8 with no byte order mark + return buffer.toString("utf8"); + } + function readFile(fileName: string, _encoding?: string): string | undefined { + perfLogger.logStartReadFile(fileName); + const file = readFileWorker(fileName, _encoding); + perfLogger.logStopReadFile(); + return file; + } + function writeFile(fileName: string, data: string, writeByteOrderMark?: boolean): void { + perfLogger.logEvent("WriteFile: " + fileName); + // If a BOM is required, emit one + if (writeByteOrderMark) { + data = byteOrderMarkIndicator + data; + } + let fd: number | undefined; + try { + fd = _fs.openSync(fileName, "w"); + _fs.writeSync(fd, data, /*position*/ undefined, "utf8"); + } + finally { + if (fd !== undefined) { + _fs.closeSync(fd); + } + } + } + function getAccessibleFileSystemEntries(path: string): FileSystemEntries { + perfLogger.logEvent("ReadDir: " + (path || ".")); + try { + const entries = _fs.readdirSync(path || ".", { withFileTypes: true }); + const files: string[] = []; + const directories: string[] = []; + for (const dirent of entries) { + // withFileTypes is not supported before Node 10.10. + const entry = typeof dirent === "string" ? dirent : dirent.name; + // This is necessary because on some file system node fails to exclude + // "." and "..". See https://github.com/nodejs/node/issues/4002 + if (entry === "." || entry === "..") { + continue; + } + let stat: any; + if (typeof dirent === "string" || dirent.isSymbolicLink()) { + const name = combinePaths(path, entry); + try { + stat = _fs.statSync(name); } - else if (stat.isDirectory()) { - directories.push(entry); + catch (e) { + continue; } } - files.sort(); - directories.sort(); - return { files, directories }; - } - catch (e) { - return emptyFileSystemEntries; + else { + stat = dirent; + } + if (stat.isFile()) { + files.push(entry); + } + else if (stat.isDirectory()) { + directories.push(entry); + } } + files.sort(); + directories.sort(); + return { files, directories }; } - - function readDirectory(path: string, extensions?: readonly string[], excludes?: readonly string[], includes?: readonly string[], depth?: number): string[] { - return matchFiles(path, extensions, excludes, includes, useCaseSensitiveFileNames, process.cwd(), depth, getAccessibleFileSystemEntries, realpath); + catch (e) { + return emptyFileSystemEntries; } - - function fileSystemEntryExists(path: string, entryKind: FileSystemEntryKind): boolean { - try { - const stat = _fs.statSync(path); - switch (entryKind) { - case FileSystemEntryKind.File: return stat.isFile(); - case FileSystemEntryKind.Directory: return stat.isDirectory(); - default: return false; - } - } - catch (e) { - return false; + } + function readDirectory(path: string, extensions?: readonly string[], excludes?: readonly string[], includes?: readonly string[], depth?: number): string[] { + return matchFiles(path, extensions, excludes, includes, useCaseSensitiveFileNames, process.cwd(), depth, getAccessibleFileSystemEntries, realpath); + } + function fileSystemEntryExists(path: string, entryKind: FileSystemEntryKind): boolean { + try { + const stat = _fs.statSync(path); + switch (entryKind) { + case FileSystemEntryKind.File: return stat.isFile(); + case FileSystemEntryKind.Directory: return stat.isDirectory(); + default: return false; } } - - function fileExists(path: string): boolean { - return fileSystemEntryExists(path, FileSystemEntryKind.File); - } - - function directoryExists(path: string): boolean { - return fileSystemEntryExists(path, FileSystemEntryKind.Directory); + catch (e) { + return false; } - - function getDirectories(path: string): string[] { - return getAccessibleFileSystemEntries(path).directories.slice(); + } + function fileExists(path: string): boolean { + return fileSystemEntryExists(path, FileSystemEntryKind.File); + } + function directoryExists(path: string): boolean { + return fileSystemEntryExists(path, FileSystemEntryKind.Directory); + } + function getDirectories(path: string): string[] { + return getAccessibleFileSystemEntries(path).directories.slice(); + } + function realpath(path: string): string { + try { + return _fs.realpathSync(path); } - - function realpath(path: string): string { - try { - return _fs.realpathSync(path); - } - catch { - return path; - } + catch { + return path; } - - function getModifiedTime(path: string) { - try { - return _fs.statSync(path).mtime; - } - catch (e) { - return undefined; - } + } + function getModifiedTime(path: string) { + try { + return _fs.statSync(path).mtime; } - - function setModifiedTime(path: string, time: Date) { - try { - _fs.utimesSync(path, time, time); - } - catch (e) { - return; - } + catch (e) { + return undefined; } - - function deleteFile(path: string) { - try { - return _fs.unlinkSync(path); - } - catch (e) { - return; - } + } + function setModifiedTime(path: string, time: Date) { + try { + _fs.utimesSync(path, time, time); } - - function createSHA256Hash(data: string): string { - const hash = _crypto!.createHash("sha256"); - hash.update(data); - return hash.digest("hex"); + catch (e) { + return; } } - - let sys: System | undefined; - if (typeof process !== "undefined" && process.nextTick && !process.browser && typeof require !== "undefined") { - // process and process.nextTick checks if current environment is node-like - // process.browser check excludes webpack and browserify - sys = getNodeSystem(); + function deleteFile(path: string) { + try { + return _fs.unlinkSync(path); + } + catch (e) { + return; + } } - if (sys) { - // patch writefile to create folder before writing the file - patchWriteFileEnsuringDirectory(sys); + function createSHA256Hash(data: string): string { + const hash = _crypto!.createHash("sha256"); + hash.update(data); + return hash.digest("hex"); } - return sys!; - })(); - - if (sys && sys.getEnvironmentVariable) { - setCustomPollingValues(sys); - Debug.setAssertionLevel(/^development$/i.test(sys.getEnvironmentVariable("NODE_ENV")) - ? AssertionLevel.Normal - : AssertionLevel.None); } - if (sys && sys.debugMode) { - Debug.isDebugging = true; + let sys: System | undefined; + if (typeof process !== "undefined" && process.nextTick && !process.browser && typeof require !== "undefined") { + // process and process.nextTick checks if current environment is node-like + // process.browser check excludes webpack and browserify + sys = getNodeSystem(); } + if (sys) { + // patch writefile to create folder before writing the file + patchWriteFileEnsuringDirectory(sys); + } + return sys!; +})(); +if (sys && sys.getEnvironmentVariable) { + setCustomPollingValues(sys); + Debug.setAssertionLevel(/^development$/i.test(sys.getEnvironmentVariable("NODE_ENV")) + ? AssertionLevel.Normal + : AssertionLevel.None); +} +if (sys && sys.debugMode) { + Debug.isDebugging = true; } diff --git a/src/compiler/transformer.ts b/src/compiler/transformer.ts index a3784a62a8243..949bc5e1038fd 100644 --- a/src/compiler/transformer.ts +++ b/src/compiler/transformer.ts @@ -1,452 +1,404 @@ +import { ModuleKind, TransformerFactory, SourceFile, Bundle, transformECMAScriptModule, transformSystemModule, transformModule, EmitTransformers, emptyArray, CompilerOptions, CustomTransformers, getEmitScriptTarget, getEmitModuleKind, addRange, map, transformTypeScript, transformClassFields, JsxEmit, transformJsx, ScriptTarget, transformESNext, transformES2020, transformES2019, transformES2018, transformES2017, transformES2016, transformES2015, transformGenerators, transformES5, transformDeclarations, CustomTransformer, Transformer, isBundle, CustomTransformerFactory, chainBundle, identity, EmitHint, Node, EmitResolver, EmitHost, TransformationResult, SyntaxKind, VariableDeclaration, FunctionDeclaration, EmitHelper, TransformationContext, DiagnosticWithLocation, Debug, disposeEmitNodes, getSourceFileOfNode, getParseTreeNode, isSourceFile, getEmitFlags, EmitFlags, Identifier, setEmitFlags, createVariableDeclaration, Statement, createVariableStatement, createVariableDeclarationList, append } from "./ts"; +import { mark, measure } from "./ts.performance"; /* @internal */ -namespace ts { - function getModuleTransformer(moduleKind: ModuleKind): TransformerFactory { - switch (moduleKind) { - case ModuleKind.ESNext: - case ModuleKind.ES2020: - case ModuleKind.ES2015: - return transformECMAScriptModule; - case ModuleKind.System: - return transformSystemModule; - default: - return transformModule; - } +function getModuleTransformer(moduleKind: ModuleKind): TransformerFactory { + switch (moduleKind) { + case ModuleKind.ESNext: + case ModuleKind.ES2020: + case ModuleKind.ES2015: + return transformECMAScriptModule; + case ModuleKind.System: + return transformSystemModule; + default: + return transformModule; } - - const enum TransformationState { - Uninitialized, - Initialized, - Completed, - Disposed +} +/* @internal */ +const enum TransformationState { + Uninitialized, + Initialized, + Completed, + Disposed +} +/* @internal */ +const enum SyntaxKindFeatureFlags { + Substitution = 1 << 0, + EmitNotifications = 1 << 1 +} +/* @internal */ +export const noTransformers: EmitTransformers = { scriptTransformers: emptyArray, declarationTransformers: emptyArray }; +/* @internal */ +export function getTransformers(compilerOptions: CompilerOptions, customTransformers?: CustomTransformers, emitOnlyDtsFiles?: boolean): EmitTransformers { + return { + scriptTransformers: getScriptTransformers(compilerOptions, customTransformers, emitOnlyDtsFiles), + declarationTransformers: getDeclarationTransformers(customTransformers), + }; +} +/* @internal */ +function getScriptTransformers(compilerOptions: CompilerOptions, customTransformers?: CustomTransformers, emitOnlyDtsFiles?: boolean) { + if (emitOnlyDtsFiles) + return emptyArray; + const jsx = compilerOptions.jsx; + const languageVersion = getEmitScriptTarget(compilerOptions); + const moduleKind = getEmitModuleKind(compilerOptions); + const transformers: TransformerFactory[] = []; + addRange(transformers, customTransformers && map(customTransformers.before, wrapScriptTransformerFactory)); + transformers.push(transformTypeScript); + transformers.push(transformClassFields); + if (jsx === JsxEmit.React) { + transformers.push(transformJsx); } - - const enum SyntaxKindFeatureFlags { - Substitution = 1 << 0, - EmitNotifications = 1 << 1, + if (languageVersion < ScriptTarget.ESNext) { + transformers.push(transformESNext); } - - export const noTransformers: EmitTransformers = { scriptTransformers: emptyArray, declarationTransformers: emptyArray }; - - export function getTransformers(compilerOptions: CompilerOptions, customTransformers?: CustomTransformers, emitOnlyDtsFiles?: boolean): EmitTransformers { - return { - scriptTransformers: getScriptTransformers(compilerOptions, customTransformers, emitOnlyDtsFiles), - declarationTransformers: getDeclarationTransformers(customTransformers), - }; + if (languageVersion < ScriptTarget.ES2020) { + transformers.push(transformES2020); } - - function getScriptTransformers(compilerOptions: CompilerOptions, customTransformers?: CustomTransformers, emitOnlyDtsFiles?: boolean) { - if (emitOnlyDtsFiles) return emptyArray; - - const jsx = compilerOptions.jsx; - const languageVersion = getEmitScriptTarget(compilerOptions); - const moduleKind = getEmitModuleKind(compilerOptions); - const transformers: TransformerFactory[] = []; - - addRange(transformers, customTransformers && map(customTransformers.before, wrapScriptTransformerFactory)); - - transformers.push(transformTypeScript); - transformers.push(transformClassFields); - - if (jsx === JsxEmit.React) { - transformers.push(transformJsx); - } - - if (languageVersion < ScriptTarget.ESNext) { - transformers.push(transformESNext); - } - - if (languageVersion < ScriptTarget.ES2020) { - transformers.push(transformES2020); - } - - if (languageVersion < ScriptTarget.ES2019) { - transformers.push(transformES2019); - } - - if (languageVersion < ScriptTarget.ES2018) { - transformers.push(transformES2018); - } - - if (languageVersion < ScriptTarget.ES2017) { - transformers.push(transformES2017); - } - - if (languageVersion < ScriptTarget.ES2016) { - transformers.push(transformES2016); - } - - if (languageVersion < ScriptTarget.ES2015) { - transformers.push(transformES2015); - transformers.push(transformGenerators); - } - - transformers.push(getModuleTransformer(moduleKind)); - - // The ES5 transformer is last so that it can substitute expressions like `exports.default` - // for ES3. - if (languageVersion < ScriptTarget.ES5) { - transformers.push(transformES5); + if (languageVersion < ScriptTarget.ES2019) { + transformers.push(transformES2019); + } + if (languageVersion < ScriptTarget.ES2018) { + transformers.push(transformES2018); + } + if (languageVersion < ScriptTarget.ES2017) { + transformers.push(transformES2017); + } + if (languageVersion < ScriptTarget.ES2016) { + transformers.push(transformES2016); + } + if (languageVersion < ScriptTarget.ES2015) { + transformers.push(transformES2015); + transformers.push(transformGenerators); + } + transformers.push(getModuleTransformer(moduleKind)); + // The ES5 transformer is last so that it can substitute expressions like `exports.default` + // for ES3. + if (languageVersion < ScriptTarget.ES5) { + transformers.push(transformES5); + } + addRange(transformers, customTransformers && map(customTransformers.after, wrapScriptTransformerFactory)); + return transformers; +} +/* @internal */ +function getDeclarationTransformers(customTransformers?: CustomTransformers) { + const transformers: TransformerFactory[] = []; + transformers.push(transformDeclarations); + addRange(transformers, customTransformers && map(customTransformers.afterDeclarations, wrapDeclarationTransformerFactory)); + return transformers; +} +/** + * Wrap a custom script or declaration transformer object in a `Transformer` callback with fallback support for transforming bundles. + */ +/* @internal */ +function wrapCustomTransformer(transformer: CustomTransformer): Transformer { + return node => isBundle(node) ? transformer.transformBundle(node) : transformer.transformSourceFile(node); +} +/** + * Wrap a transformer factory that may return a custom script or declaration transformer object. + */ +/* @internal */ +function wrapCustomTransformerFactory(transformer: TransformerFactory | CustomTransformerFactory, handleDefault: (node: Transformer) => Transformer): TransformerFactory { + return context => { + const customTransformer = transformer(context); + return typeof customTransformer === "function" + ? handleDefault(customTransformer) + : wrapCustomTransformer(customTransformer); + }; +} +/* @internal */ +function wrapScriptTransformerFactory(transformer: TransformerFactory | CustomTransformerFactory): TransformerFactory { + return wrapCustomTransformerFactory(transformer, chainBundle); +} +/* @internal */ +function wrapDeclarationTransformerFactory(transformer: TransformerFactory | CustomTransformerFactory): TransformerFactory { + return wrapCustomTransformerFactory(transformer, identity); +} +/* @internal */ +export function noEmitSubstitution(_hint: EmitHint, node: Node) { + return node; +} +/* @internal */ +export function noEmitNotification(hint: EmitHint, node: Node, callback: (hint: EmitHint, node: Node) => void) { + callback(hint, node); +} +/** + * Transforms an array of SourceFiles by passing them through each transformer. + * + * @param resolver The emit resolver provided by the checker. + * @param host The emit host object used to interact with the file system. + * @param options Compiler options to surface in the `TransformationContext`. + * @param nodes An array of nodes to transform. + * @param transforms An array of `TransformerFactory` callbacks. + * @param allowDtsFiles A value indicating whether to allow the transformation of .d.ts files. + */ +/* @internal */ +export function transformNodes(resolver: EmitResolver | undefined, host: EmitHost | undefined, options: CompilerOptions, nodes: readonly T[], transformers: readonly TransformerFactory[], allowDtsFiles: boolean): TransformationResult { + const enabledSyntaxKindFeatures = new Array(SyntaxKind.Count); + let lexicalEnvironmentVariableDeclarations: VariableDeclaration[]; + let lexicalEnvironmentFunctionDeclarations: FunctionDeclaration[]; + let lexicalEnvironmentVariableDeclarationsStack: VariableDeclaration[][] = []; + let lexicalEnvironmentFunctionDeclarationsStack: FunctionDeclaration[][] = []; + let lexicalEnvironmentStackOffset = 0; + let lexicalEnvironmentSuspended = false; + let emitHelpers: EmitHelper[] | undefined; + let onSubstituteNode: TransformationContext["onSubstituteNode"] = noEmitSubstitution; + let onEmitNode: TransformationContext["onEmitNode"] = noEmitNotification; + let state = TransformationState.Uninitialized; + const diagnostics: DiagnosticWithLocation[] = []; + // The transformation context is provided to each transformer as part of transformer + // initialization. + const context: TransformationContext = { + getCompilerOptions: () => options, + getEmitResolver: () => resolver!, + getEmitHost: () => host!, + startLexicalEnvironment, + suspendLexicalEnvironment, + resumeLexicalEnvironment, + endLexicalEnvironment, + hoistVariableDeclaration, + hoistFunctionDeclaration, + requestEmitHelper, + readEmitHelpers, + enableSubstitution, + enableEmitNotification, + isSubstitutionEnabled, + isEmitNotificationEnabled, + get onSubstituteNode() { return onSubstituteNode; }, + set onSubstituteNode(value) { + Debug.assert(state < TransformationState.Initialized, "Cannot modify transformation hooks after initialization has completed."); + Debug.assert(value !== undefined, "Value must not be 'undefined'"); + onSubstituteNode = value; + }, + get onEmitNode() { return onEmitNode; }, + set onEmitNode(value) { + Debug.assert(state < TransformationState.Initialized, "Cannot modify transformation hooks after initialization has completed."); + Debug.assert(value !== undefined, "Value must not be 'undefined'"); + onEmitNode = value; + }, + addDiagnostic(diag) { + diagnostics.push(diag); } - - addRange(transformers, customTransformers && map(customTransformers.after, wrapScriptTransformerFactory)); - return transformers; + }; + // Ensure the parse tree is clean before applying transformations + for (const node of nodes) { + disposeEmitNodes(getSourceFileOfNode(getParseTreeNode(node))); } - - function getDeclarationTransformers(customTransformers?: CustomTransformers) { - const transformers: TransformerFactory[] = []; - transformers.push(transformDeclarations); - addRange(transformers, customTransformers && map(customTransformers.afterDeclarations, wrapDeclarationTransformerFactory)); - return transformers; + mark("beforeTransform"); + // Chain together and initialize each transformer. + const transformersWithContext = transformers.map(t => t(context)); + const transformation = (node: T): T => { + for (const transform of transformersWithContext) { + node = transform(node); + } + return node; + }; + // prevent modification of transformation hooks. + state = TransformationState.Initialized; + // Transform each node. + const transformed = map(nodes, allowDtsFiles ? transformation : transformRoot); + // prevent modification of the lexical environment. + state = TransformationState.Completed; + mark("afterTransform"); + measure("transformTime", "beforeTransform", "afterTransform"); + return { + transformed, + substituteNode, + emitNodeWithNotification, + isEmitNotificationEnabled, + dispose, + diagnostics + }; + function transformRoot(node: T) { + return node && (!isSourceFile(node) || !node.isDeclarationFile) ? transformation(node) : node; } - /** - * Wrap a custom script or declaration transformer object in a `Transformer` callback with fallback support for transforming bundles. + * Enables expression substitutions in the pretty printer for the provided SyntaxKind. */ - function wrapCustomTransformer(transformer: CustomTransformer): Transformer { - return node => isBundle(node) ? transformer.transformBundle(node) : transformer.transformSourceFile(node); + function enableSubstitution(kind: SyntaxKind) { + Debug.assert(state < TransformationState.Completed, "Cannot modify the transformation context after transformation has completed."); + enabledSyntaxKindFeatures[kind] |= SyntaxKindFeatureFlags.Substitution; } - /** - * Wrap a transformer factory that may return a custom script or declaration transformer object. + * Determines whether expression substitutions are enabled for the provided node. */ - function wrapCustomTransformerFactory(transformer: TransformerFactory | CustomTransformerFactory, handleDefault: (node: Transformer) => Transformer): TransformerFactory { - return context => { - const customTransformer = transformer(context); - return typeof customTransformer === "function" - ? handleDefault(customTransformer) - : wrapCustomTransformer(customTransformer); - }; - } - - function wrapScriptTransformerFactory(transformer: TransformerFactory | CustomTransformerFactory): TransformerFactory { - return wrapCustomTransformerFactory(transformer, chainBundle); + function isSubstitutionEnabled(node: Node) { + return (enabledSyntaxKindFeatures[node.kind] & SyntaxKindFeatureFlags.Substitution) !== 0 + && (getEmitFlags(node) & EmitFlags.NoSubstitution) === 0; } - - function wrapDeclarationTransformerFactory(transformer: TransformerFactory | CustomTransformerFactory): TransformerFactory { - return wrapCustomTransformerFactory(transformer, identity); + /** + * Emits a node with possible substitution. + * + * @param hint A hint as to the intended usage of the node. + * @param node The node to emit. + * @param emitCallback The callback used to emit the node or its substitute. + */ + function substituteNode(hint: EmitHint, node: Node) { + Debug.assert(state < TransformationState.Disposed, "Cannot substitute a node after the result is disposed."); + return node && isSubstitutionEnabled(node) && onSubstituteNode(hint, node) || node; } - - export function noEmitSubstitution(_hint: EmitHint, node: Node) { - return node; + /** + * Enables before/after emit notifications in the pretty printer for the provided SyntaxKind. + */ + function enableEmitNotification(kind: SyntaxKind) { + Debug.assert(state < TransformationState.Completed, "Cannot modify the transformation context after transformation has completed."); + enabledSyntaxKindFeatures[kind] |= SyntaxKindFeatureFlags.EmitNotifications; } - - export function noEmitNotification(hint: EmitHint, node: Node, callback: (hint: EmitHint, node: Node) => void) { - callback(hint, node); + /** + * Determines whether before/after emit notifications should be raised in the pretty + * printer when it emits a node. + */ + function isEmitNotificationEnabled(node: Node) { + return (enabledSyntaxKindFeatures[node.kind] & SyntaxKindFeatureFlags.EmitNotifications) !== 0 + || (getEmitFlags(node) & EmitFlags.AdviseOnEmitNode) !== 0; } - /** - * Transforms an array of SourceFiles by passing them through each transformer. + * Emits a node with possible emit notification. * - * @param resolver The emit resolver provided by the checker. - * @param host The emit host object used to interact with the file system. - * @param options Compiler options to surface in the `TransformationContext`. - * @param nodes An array of nodes to transform. - * @param transforms An array of `TransformerFactory` callbacks. - * @param allowDtsFiles A value indicating whether to allow the transformation of .d.ts files. + * @param hint A hint as to the intended usage of the node. + * @param node The node to emit. + * @param emitCallback The callback used to emit the node. */ - export function transformNodes(resolver: EmitResolver | undefined, host: EmitHost | undefined, options: CompilerOptions, nodes: readonly T[], transformers: readonly TransformerFactory[], allowDtsFiles: boolean): TransformationResult { - const enabledSyntaxKindFeatures = new Array(SyntaxKind.Count); - let lexicalEnvironmentVariableDeclarations: VariableDeclaration[]; - let lexicalEnvironmentFunctionDeclarations: FunctionDeclaration[]; - let lexicalEnvironmentVariableDeclarationsStack: VariableDeclaration[][] = []; - let lexicalEnvironmentFunctionDeclarationsStack: FunctionDeclaration[][] = []; - let lexicalEnvironmentStackOffset = 0; - let lexicalEnvironmentSuspended = false; - let emitHelpers: EmitHelper[] | undefined; - let onSubstituteNode: TransformationContext["onSubstituteNode"] = noEmitSubstitution; - let onEmitNode: TransformationContext["onEmitNode"] = noEmitNotification; - let state = TransformationState.Uninitialized; - const diagnostics: DiagnosticWithLocation[] = []; - - // The transformation context is provided to each transformer as part of transformer - // initialization. - const context: TransformationContext = { - getCompilerOptions: () => options, - getEmitResolver: () => resolver!, // TODO: GH#18217 - getEmitHost: () => host!, // TODO: GH#18217 - startLexicalEnvironment, - suspendLexicalEnvironment, - resumeLexicalEnvironment, - endLexicalEnvironment, - hoistVariableDeclaration, - hoistFunctionDeclaration, - requestEmitHelper, - readEmitHelpers, - enableSubstitution, - enableEmitNotification, - isSubstitutionEnabled, - isEmitNotificationEnabled, - get onSubstituteNode() { return onSubstituteNode; }, - set onSubstituteNode(value) { - Debug.assert(state < TransformationState.Initialized, "Cannot modify transformation hooks after initialization has completed."); - Debug.assert(value !== undefined, "Value must not be 'undefined'"); - onSubstituteNode = value; - }, - get onEmitNode() { return onEmitNode; }, - set onEmitNode(value) { - Debug.assert(state < TransformationState.Initialized, "Cannot modify transformation hooks after initialization has completed."); - Debug.assert(value !== undefined, "Value must not be 'undefined'"); - onEmitNode = value; - }, - addDiagnostic(diag) { - diagnostics.push(diag); + function emitNodeWithNotification(hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void) { + Debug.assert(state < TransformationState.Disposed, "Cannot invoke TransformationResult callbacks after the result is disposed."); + if (node) { + // TODO: Remove check and unconditionally use onEmitNode when API is breakingly changed + // (see https://github.com/microsoft/TypeScript/pull/36248/files/5062623f39120171b98870c71344b3242eb03d23#r369766739) + if (isEmitNotificationEnabled(node)) { + onEmitNode(hint, node, emitCallback); } - }; - - // Ensure the parse tree is clean before applying transformations - for (const node of nodes) { - disposeEmitNodes(getSourceFileOfNode(getParseTreeNode(node))); - } - - performance.mark("beforeTransform"); - - // Chain together and initialize each transformer. - const transformersWithContext = transformers.map(t => t(context)); - const transformation = (node: T): T => { - for (const transform of transformersWithContext) { - node = transform(node); + else { + emitCallback(hint, node); } - return node; - }; - - // prevent modification of transformation hooks. - state = TransformationState.Initialized; - - // Transform each node. - const transformed = map(nodes, allowDtsFiles ? transformation : transformRoot); - - // prevent modification of the lexical environment. - state = TransformationState.Completed; - - performance.mark("afterTransform"); - performance.measure("transformTime", "beforeTransform", "afterTransform"); - - return { - transformed, - substituteNode, - emitNodeWithNotification, - isEmitNotificationEnabled, - dispose, - diagnostics - }; - - function transformRoot(node: T) { - return node && (!isSourceFile(node) || !node.isDeclarationFile) ? transformation(node) : node; - } - - /** - * Enables expression substitutions in the pretty printer for the provided SyntaxKind. - */ - function enableSubstitution(kind: SyntaxKind) { - Debug.assert(state < TransformationState.Completed, "Cannot modify the transformation context after transformation has completed."); - enabledSyntaxKindFeatures[kind] |= SyntaxKindFeatureFlags.Substitution; } - - /** - * Determines whether expression substitutions are enabled for the provided node. - */ - function isSubstitutionEnabled(node: Node) { - return (enabledSyntaxKindFeatures[node.kind] & SyntaxKindFeatureFlags.Substitution) !== 0 - && (getEmitFlags(node) & EmitFlags.NoSubstitution) === 0; + } + /** + * Records a hoisted variable declaration for the provided name within a lexical environment. + */ + function hoistVariableDeclaration(name: Identifier): void { + Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization."); + Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed."); + const decl = setEmitFlags(createVariableDeclaration(name), EmitFlags.NoNestedSourceMaps); + if (!lexicalEnvironmentVariableDeclarations) { + lexicalEnvironmentVariableDeclarations = [decl]; } - - /** - * Emits a node with possible substitution. - * - * @param hint A hint as to the intended usage of the node. - * @param node The node to emit. - * @param emitCallback The callback used to emit the node or its substitute. - */ - function substituteNode(hint: EmitHint, node: Node) { - Debug.assert(state < TransformationState.Disposed, "Cannot substitute a node after the result is disposed."); - return node && isSubstitutionEnabled(node) && onSubstituteNode(hint, node) || node; + else { + lexicalEnvironmentVariableDeclarations.push(decl); } - - /** - * Enables before/after emit notifications in the pretty printer for the provided SyntaxKind. - */ - function enableEmitNotification(kind: SyntaxKind) { - Debug.assert(state < TransformationState.Completed, "Cannot modify the transformation context after transformation has completed."); - enabledSyntaxKindFeatures[kind] |= SyntaxKindFeatureFlags.EmitNotifications; + } + /** + * Records a hoisted function declaration within a lexical environment. + */ + function hoistFunctionDeclaration(func: FunctionDeclaration): void { + Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization."); + Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed."); + if (!lexicalEnvironmentFunctionDeclarations) { + lexicalEnvironmentFunctionDeclarations = [func]; } - - /** - * Determines whether before/after emit notifications should be raised in the pretty - * printer when it emits a node. - */ - function isEmitNotificationEnabled(node: Node) { - return (enabledSyntaxKindFeatures[node.kind] & SyntaxKindFeatureFlags.EmitNotifications) !== 0 - || (getEmitFlags(node) & EmitFlags.AdviseOnEmitNode) !== 0; + else { + lexicalEnvironmentFunctionDeclarations.push(func); } - - /** - * Emits a node with possible emit notification. - * - * @param hint A hint as to the intended usage of the node. - * @param node The node to emit. - * @param emitCallback The callback used to emit the node. - */ - function emitNodeWithNotification(hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void) { - Debug.assert(state < TransformationState.Disposed, "Cannot invoke TransformationResult callbacks after the result is disposed."); - if (node) { - // TODO: Remove check and unconditionally use onEmitNode when API is breakingly changed - // (see https://github.com/microsoft/TypeScript/pull/36248/files/5062623f39120171b98870c71344b3242eb03d23#r369766739) - if (isEmitNotificationEnabled(node)) { - onEmitNode(hint, node, emitCallback); + } + /** + * Starts a new lexical environment. Any existing hoisted variable or function declarations + * are pushed onto a stack, and the related storage variables are reset. + */ + function startLexicalEnvironment(): void { + Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization."); + Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed."); + Debug.assert(!lexicalEnvironmentSuspended, "Lexical environment is suspended."); + // Save the current lexical environment. Rather than resizing the array we adjust the + // stack size variable. This allows us to reuse existing array slots we've + // already allocated between transformations to avoid allocation and GC overhead during + // transformation. + lexicalEnvironmentVariableDeclarationsStack[lexicalEnvironmentStackOffset] = lexicalEnvironmentVariableDeclarations; + lexicalEnvironmentFunctionDeclarationsStack[lexicalEnvironmentStackOffset] = lexicalEnvironmentFunctionDeclarations; + lexicalEnvironmentStackOffset++; + lexicalEnvironmentVariableDeclarations = undefined!; + lexicalEnvironmentFunctionDeclarations = undefined!; + } + /** Suspends the current lexical environment, usually after visiting a parameter list. */ + function suspendLexicalEnvironment(): void { + Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization."); + Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed."); + Debug.assert(!lexicalEnvironmentSuspended, "Lexical environment is already suspended."); + lexicalEnvironmentSuspended = true; + } + /** Resumes a suspended lexical environment, usually before visiting a function body. */ + function resumeLexicalEnvironment(): void { + Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization."); + Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed."); + Debug.assert(lexicalEnvironmentSuspended, "Lexical environment is not suspended."); + lexicalEnvironmentSuspended = false; + } + /** + * Ends a lexical environment. The previous set of hoisted declarations are restored and + * any hoisted declarations added in this environment are returned. + */ + function endLexicalEnvironment(): Statement[] | undefined { + Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization."); + Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed."); + Debug.assert(!lexicalEnvironmentSuspended, "Lexical environment is suspended."); + let statements: Statement[] | undefined; + if (lexicalEnvironmentVariableDeclarations || lexicalEnvironmentFunctionDeclarations) { + if (lexicalEnvironmentFunctionDeclarations) { + statements = [...lexicalEnvironmentFunctionDeclarations]; + } + if (lexicalEnvironmentVariableDeclarations) { + const statement = createVariableStatement( + /*modifiers*/ undefined, createVariableDeclarationList(lexicalEnvironmentVariableDeclarations)); + setEmitFlags(statement, EmitFlags.CustomPrologue); + if (!statements) { + statements = [statement]; } else { - emitCallback(hint, node); + statements.push(statement); } } } - - /** - * Records a hoisted variable declaration for the provided name within a lexical environment. - */ - function hoistVariableDeclaration(name: Identifier): void { - Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization."); - Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed."); - const decl = setEmitFlags(createVariableDeclaration(name), EmitFlags.NoNestedSourceMaps); - if (!lexicalEnvironmentVariableDeclarations) { - lexicalEnvironmentVariableDeclarations = [decl]; - } - else { - lexicalEnvironmentVariableDeclarations.push(decl); - } + // Restore the previous lexical environment. + lexicalEnvironmentStackOffset--; + lexicalEnvironmentVariableDeclarations = lexicalEnvironmentVariableDeclarationsStack[lexicalEnvironmentStackOffset]; + lexicalEnvironmentFunctionDeclarations = lexicalEnvironmentFunctionDeclarationsStack[lexicalEnvironmentStackOffset]; + if (lexicalEnvironmentStackOffset === 0) { + lexicalEnvironmentVariableDeclarationsStack = []; + lexicalEnvironmentFunctionDeclarationsStack = []; } - - /** - * Records a hoisted function declaration within a lexical environment. - */ - function hoistFunctionDeclaration(func: FunctionDeclaration): void { - Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization."); - Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed."); - if (!lexicalEnvironmentFunctionDeclarations) { - lexicalEnvironmentFunctionDeclarations = [func]; - } - else { - lexicalEnvironmentFunctionDeclarations.push(func); + return statements; + } + function requestEmitHelper(helper: EmitHelper): void { + Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the transformation context during initialization."); + Debug.assert(state < TransformationState.Completed, "Cannot modify the transformation context after transformation has completed."); + Debug.assert(!helper.scoped, "Cannot request a scoped emit helper."); + if (helper.dependencies) { + for (const h of helper.dependencies) { + requestEmitHelper(h); } } - - /** - * Starts a new lexical environment. Any existing hoisted variable or function declarations - * are pushed onto a stack, and the related storage variables are reset. - */ - function startLexicalEnvironment(): void { - Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization."); - Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed."); - Debug.assert(!lexicalEnvironmentSuspended, "Lexical environment is suspended."); - - // Save the current lexical environment. Rather than resizing the array we adjust the - // stack size variable. This allows us to reuse existing array slots we've - // already allocated between transformations to avoid allocation and GC overhead during - // transformation. - lexicalEnvironmentVariableDeclarationsStack[lexicalEnvironmentStackOffset] = lexicalEnvironmentVariableDeclarations; - lexicalEnvironmentFunctionDeclarationsStack[lexicalEnvironmentStackOffset] = lexicalEnvironmentFunctionDeclarations; - lexicalEnvironmentStackOffset++; + emitHelpers = append(emitHelpers, helper); + } + function readEmitHelpers(): EmitHelper[] | undefined { + Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the transformation context during initialization."); + Debug.assert(state < TransformationState.Completed, "Cannot modify the transformation context after transformation has completed."); + const helpers = emitHelpers; + emitHelpers = undefined; + return helpers; + } + function dispose() { + if (state < TransformationState.Disposed) { + // Clean up emit nodes on parse tree + for (const node of nodes) { + disposeEmitNodes(getSourceFileOfNode(getParseTreeNode(node))); + } + // Release references to external entries for GC purposes. lexicalEnvironmentVariableDeclarations = undefined!; + lexicalEnvironmentVariableDeclarationsStack = undefined!; lexicalEnvironmentFunctionDeclarations = undefined!; - } - - /** Suspends the current lexical environment, usually after visiting a parameter list. */ - function suspendLexicalEnvironment(): void { - Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization."); - Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed."); - Debug.assert(!lexicalEnvironmentSuspended, "Lexical environment is already suspended."); - lexicalEnvironmentSuspended = true; - } - - /** Resumes a suspended lexical environment, usually before visiting a function body. */ - function resumeLexicalEnvironment(): void { - Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization."); - Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed."); - Debug.assert(lexicalEnvironmentSuspended, "Lexical environment is not suspended."); - lexicalEnvironmentSuspended = false; - } - - /** - * Ends a lexical environment. The previous set of hoisted declarations are restored and - * any hoisted declarations added in this environment are returned. - */ - function endLexicalEnvironment(): Statement[] | undefined { - Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization."); - Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed."); - Debug.assert(!lexicalEnvironmentSuspended, "Lexical environment is suspended."); - - let statements: Statement[] | undefined; - if (lexicalEnvironmentVariableDeclarations || lexicalEnvironmentFunctionDeclarations) { - if (lexicalEnvironmentFunctionDeclarations) { - statements = [...lexicalEnvironmentFunctionDeclarations]; - } - - if (lexicalEnvironmentVariableDeclarations) { - const statement = createVariableStatement( - /*modifiers*/ undefined, - createVariableDeclarationList(lexicalEnvironmentVariableDeclarations) - ); - - setEmitFlags(statement, EmitFlags.CustomPrologue); - - if (!statements) { - statements = [statement]; - } - else { - statements.push(statement); - } - } - } - - // Restore the previous lexical environment. - lexicalEnvironmentStackOffset--; - lexicalEnvironmentVariableDeclarations = lexicalEnvironmentVariableDeclarationsStack[lexicalEnvironmentStackOffset]; - lexicalEnvironmentFunctionDeclarations = lexicalEnvironmentFunctionDeclarationsStack[lexicalEnvironmentStackOffset]; - if (lexicalEnvironmentStackOffset === 0) { - lexicalEnvironmentVariableDeclarationsStack = []; - lexicalEnvironmentFunctionDeclarationsStack = []; - } - return statements; - } - - function requestEmitHelper(helper: EmitHelper): void { - Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the transformation context during initialization."); - Debug.assert(state < TransformationState.Completed, "Cannot modify the transformation context after transformation has completed."); - Debug.assert(!helper.scoped, "Cannot request a scoped emit helper."); - if (helper.dependencies) { - for (const h of helper.dependencies) { - requestEmitHelper(h); - } - } - emitHelpers = append(emitHelpers, helper); - } - - function readEmitHelpers(): EmitHelper[] | undefined { - Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the transformation context during initialization."); - Debug.assert(state < TransformationState.Completed, "Cannot modify the transformation context after transformation has completed."); - const helpers = emitHelpers; + lexicalEnvironmentFunctionDeclarationsStack = undefined!; + onSubstituteNode = undefined!; + onEmitNode = undefined!; emitHelpers = undefined; - return helpers; - } - - function dispose() { - if (state < TransformationState.Disposed) { - // Clean up emit nodes on parse tree - for (const node of nodes) { - disposeEmitNodes(getSourceFileOfNode(getParseTreeNode(node))); - } - - // Release references to external entries for GC purposes. - lexicalEnvironmentVariableDeclarations = undefined!; - lexicalEnvironmentVariableDeclarationsStack = undefined!; - lexicalEnvironmentFunctionDeclarations = undefined!; - lexicalEnvironmentFunctionDeclarationsStack = undefined!; - onSubstituteNode = undefined!; - onEmitNode = undefined!; - emitHelpers = undefined; - - // Prevent further use of the transformation result. - state = TransformationState.Disposed; - } + // Prevent further use of the transformation result. + state = TransformationState.Disposed; } } } diff --git a/src/compiler/transformers/classFields.ts b/src/compiler/transformers/classFields.ts index a4c8878fbbcbb..ad4f3d6233c37 100644 --- a/src/compiler/transformers/classFields.ts +++ b/src/compiler/transformers/classFields.ts @@ -1,1084 +1,824 @@ +import { Identifier, UnderscoreEscapedMap, TransformationContext, getEmitScriptTarget, ScriptTarget, Expression, Statement, chainBundle, SourceFile, visitEachChild, addEmitHelpers, Node, VisitResult, TransformFlags, SyntaxKind, ClassLikeDeclaration, PropertyDeclaration, VariableStatement, ComputedPropertyName, PropertyAccessExpression, PrefixUnaryExpression, PostfixUnaryExpression, CallExpression, BinaryExpression, PrivateIdentifier, ExpressionStatement, ForStatement, TaggedTemplateExpression, AssignmentPattern, setOriginalNode, createIdentifier, some, updateComputedPropertyName, inlineExpressions, Debug, isPrivateIdentifier, updateProperty, visitNodes, isModifier, isSimpleInlineableExpression, visitNode, isExpression, nodeIsSynthesized, getSynthesizedClone, isPrivateIdentifierPropertyAccessExpression, createPrefix, createBinary, createLiteral, createTempVariable, compact, createAssignment, isPostfixUnaryExpression, updateFor, isForInitializer, isStatement, updateExpressionStatement, createCallBinding, updateCall, createPropertyAccess, updateTaggedTemplate, createCall, isTemplateLiteral, isDestructuringAssignment, updateBinary, isAssignmentExpression, AssignmentOperator, isCompoundAssignment, getNonAssignmentOperatorForCompoundAssignment, isClassDeclaration, ClassElement, isPropertyDeclaration, ClassDeclaration, forEach, getEffectiveBaseTypeNode, skipOuterExpressions, updateClassDeclaration, isHeritageClause, createExpressionStatement, getProperties, getInternalName, ClassExpression, getOriginalNode, updateClassExpression, NodeCheckFlags, GeneratedIdentifierFlags, getOriginalNodeId, setEmitFlags, EmitFlags, getEmitFlags, startOnNewLine, addRange, map, isPrivateIdentifierPropertyDeclaration, isClassElement, setTextRange, createNodeArray, hasStaticModifier, isInitializedProperty, getFirstConstructorWithBody, isConstructorDeclaration, visitParameterList, createConstructor, ConstructorDeclaration, filter, visitFunctionBody, createSuper, createSpread, addPrologueDirectivesAndInitialSuperCall, findIndex, isParameterPropertyDeclaration, createThis, mergeLexicalEnvironment, createBlock, LeftHandSideExpression, setSourceMapRange, moveRangePastModifiers, setCommentRange, isComputedPropertyName, getGeneratedNameForNode, isIdentifier, createVoidZero, createMemberAccessForPropertyName, createStringLiteral, unescapeLeadingUnderscores, createPropertyDescriptor, createObjectDefinePropertyCall, EmitHint, PropertyName, skipPartiallyEmittedExpressions, isGeneratedIdentifier, getTextOfPropertyName, createOptimisticUniqueName, createUnderscoreEscapedMap, createNew, PrivateIdentifierPropertyAccessExpression, isThisProperty, isSuperProperty, isSimpleCopiableExpression, createParen, createObjectLiteral, createSetAccessor, createParameter, getTargetOfBindingOrAssignmentElement, isSpreadElement, updateSpread, ObjectLiteralElementLike, isPropertyAssignment, getInitializerOfBindingOrAssignmentElement, updatePropertyAssignment, isArrayLiteralExpression, updateArrayLiteral, updateObjectLiteral, isObjectLiteralElementLike, UnscopedEmitHelper, getUnscopedHelperName } from "../ts"; /*@internal*/ -namespace ts { - const enum ClassPropertySubstitutionFlags { - /** - * Enables substitutions for class expressions with static fields - * which have initializers that reference the class name. - */ - ClassAliases = 1 << 0, - } - - const enum PrivateIdentifierPlacement { - InstanceField - } - - type PrivateIdentifierInfo = PrivateIdentifierInstanceField; - - interface PrivateIdentifierInstanceField { - placement: PrivateIdentifierPlacement.InstanceField; - weakMapName: Identifier; - } - +const enum ClassPropertySubstitutionFlags { /** - * A mapping of private names to information needed for transformation. + * Enables substitutions for class expressions with static fields + * which have initializers that reference the class name. */ - type PrivateIdentifierEnvironment = UnderscoreEscapedMap; - + ClassAliases = 1 << 0 +} +/* @internal */ +const enum PrivateIdentifierPlacement { + InstanceField +} +/* @internal */ +type PrivateIdentifierInfo = PrivateIdentifierInstanceField; +/* @internal */ +interface PrivateIdentifierInstanceField { + placement: PrivateIdentifierPlacement.InstanceField; + weakMapName: Identifier; +} +/** + * A mapping of private names to information needed for transformation. + */ +/* @internal */ +type PrivateIdentifierEnvironment = UnderscoreEscapedMap; +/** + * Transforms ECMAScript Class Syntax. + * TypeScript parameter property syntax is transformed in the TypeScript transformer. + * For now, this transforms public field declarations using TypeScript class semantics, + * where declarations are elided and initializers are transformed as assignments in the constructor. + * When --useDefineForClassFields is on, this transforms to ECMAScript semantics, with Object.defineProperty. + */ +/* @internal */ +export function transformClassFields(context: TransformationContext) { + const { hoistVariableDeclaration, endLexicalEnvironment, resumeLexicalEnvironment } = context; + const resolver = context.getEmitResolver(); + const compilerOptions = context.getCompilerOptions(); + const languageVersion = getEmitScriptTarget(compilerOptions); + const shouldTransformPrivateFields = languageVersion < ScriptTarget.ESNext; + const previousOnSubstituteNode = context.onSubstituteNode; + context.onSubstituteNode = onSubstituteNode; + let enabledSubstitutions: ClassPropertySubstitutionFlags; + let classAliases: Identifier[]; /** - * Transforms ECMAScript Class Syntax. - * TypeScript parameter property syntax is transformed in the TypeScript transformer. - * For now, this transforms public field declarations using TypeScript class semantics, - * where declarations are elided and initializers are transformed as assignments in the constructor. - * When --useDefineForClassFields is on, this transforms to ECMAScript semantics, with Object.defineProperty. + * Tracks what computed name expressions originating from elided names must be inlined + * at the next execution site, in document order */ - export function transformClassFields(context: TransformationContext) { - const { - hoistVariableDeclaration, - endLexicalEnvironment, - resumeLexicalEnvironment - } = context; - const resolver = context.getEmitResolver(); - const compilerOptions = context.getCompilerOptions(); - const languageVersion = getEmitScriptTarget(compilerOptions); - - const shouldTransformPrivateFields = languageVersion < ScriptTarget.ESNext; - - const previousOnSubstituteNode = context.onSubstituteNode; - context.onSubstituteNode = onSubstituteNode; - - let enabledSubstitutions: ClassPropertySubstitutionFlags; - - let classAliases: Identifier[]; - - /** - * Tracks what computed name expressions originating from elided names must be inlined - * at the next execution site, in document order - */ - let pendingExpressions: Expression[] | undefined; - - /** - * Tracks what computed name expression statements and static property initializers must be - * emitted at the next execution site, in document order (for decorated classes). - */ - let pendingStatements: Statement[] | undefined; - - const privateIdentifierEnvironmentStack: (PrivateIdentifierEnvironment | undefined)[] = []; - let currentPrivateIdentifierEnvironment: PrivateIdentifierEnvironment | undefined; - - return chainBundle(transformSourceFile); - - function transformSourceFile(node: SourceFile) { - const options = context.getCompilerOptions(); - if (node.isDeclarationFile - || options.useDefineForClassFields && options.target === ScriptTarget.ESNext) { - return node; - } - const visited = visitEachChild(node, visitor, context); - addEmitHelpers(visited, context.readEmitHelpers()); - return visited; + let pendingExpressions: Expression[] | undefined; + /** + * Tracks what computed name expression statements and static property initializers must be + * emitted at the next execution site, in document order (for decorated classes). + */ + let pendingStatements: Statement[] | undefined; + const privateIdentifierEnvironmentStack: (PrivateIdentifierEnvironment | undefined)[] = []; + let currentPrivateIdentifierEnvironment: PrivateIdentifierEnvironment | undefined; + return chainBundle(transformSourceFile); + function transformSourceFile(node: SourceFile) { + const options = context.getCompilerOptions(); + if (node.isDeclarationFile + || options.useDefineForClassFields && options.target === ScriptTarget.ESNext) { + return node; } - - function visitor(node: Node): VisitResult { - if (!(node.transformFlags & TransformFlags.ContainsClassFields)) return node; - - switch (node.kind) { - case SyntaxKind.ClassExpression: - case SyntaxKind.ClassDeclaration: - return visitClassLike(node as ClassLikeDeclaration); - case SyntaxKind.PropertyDeclaration: - return visitPropertyDeclaration(node as PropertyDeclaration); - case SyntaxKind.VariableStatement: - return visitVariableStatement(node as VariableStatement); - case SyntaxKind.ComputedPropertyName: - return visitComputedPropertyName(node as ComputedPropertyName); - case SyntaxKind.PropertyAccessExpression: - return visitPropertyAccessExpression(node as PropertyAccessExpression); - case SyntaxKind.PrefixUnaryExpression: - return visitPrefixUnaryExpression(node as PrefixUnaryExpression); - case SyntaxKind.PostfixUnaryExpression: - return visitPostfixUnaryExpression(node as PostfixUnaryExpression, /*valueIsDiscarded*/ false); - case SyntaxKind.CallExpression: - return visitCallExpression(node as CallExpression); - case SyntaxKind.BinaryExpression: - return visitBinaryExpression(node as BinaryExpression); - case SyntaxKind.PrivateIdentifier: - return visitPrivateIdentifier(node as PrivateIdentifier); - case SyntaxKind.ExpressionStatement: - return visitExpressionStatement(node as ExpressionStatement); - case SyntaxKind.ForStatement: - return visitForStatement(node as ForStatement); - case SyntaxKind.TaggedTemplateExpression: - return visitTaggedTemplateExpression(node as TaggedTemplateExpression); - } - return visitEachChild(node, visitor, context); + const visited = visitEachChild(node, visitor, context); + addEmitHelpers(visited, context.readEmitHelpers()); + return visited; + } + function visitor(node: Node): VisitResult { + if (!(node.transformFlags & TransformFlags.ContainsClassFields)) + return node; + switch (node.kind) { + case SyntaxKind.ClassExpression: + case SyntaxKind.ClassDeclaration: + return visitClassLike((node as ClassLikeDeclaration)); + case SyntaxKind.PropertyDeclaration: + return visitPropertyDeclaration((node as PropertyDeclaration)); + case SyntaxKind.VariableStatement: + return visitVariableStatement((node as VariableStatement)); + case SyntaxKind.ComputedPropertyName: + return visitComputedPropertyName((node as ComputedPropertyName)); + case SyntaxKind.PropertyAccessExpression: + return visitPropertyAccessExpression((node as PropertyAccessExpression)); + case SyntaxKind.PrefixUnaryExpression: + return visitPrefixUnaryExpression((node as PrefixUnaryExpression)); + case SyntaxKind.PostfixUnaryExpression: + return visitPostfixUnaryExpression((node as PostfixUnaryExpression), /*valueIsDiscarded*/ false); + case SyntaxKind.CallExpression: + return visitCallExpression((node as CallExpression)); + case SyntaxKind.BinaryExpression: + return visitBinaryExpression((node as BinaryExpression)); + case SyntaxKind.PrivateIdentifier: + return visitPrivateIdentifier((node as PrivateIdentifier)); + case SyntaxKind.ExpressionStatement: + return visitExpressionStatement((node as ExpressionStatement)); + case SyntaxKind.ForStatement: + return visitForStatement((node as ForStatement)); + case SyntaxKind.TaggedTemplateExpression: + return visitTaggedTemplateExpression((node as TaggedTemplateExpression)); + } + return visitEachChild(node, visitor, context); + } + function visitorDestructuringTarget(node: Node): VisitResult { + switch (node.kind) { + case SyntaxKind.ObjectLiteralExpression: + case SyntaxKind.ArrayLiteralExpression: + return visitAssignmentPattern((node as AssignmentPattern)); + default: + return visitor(node); } - - function visitorDestructuringTarget(node: Node): VisitResult { - switch (node.kind) { - case SyntaxKind.ObjectLiteralExpression: - case SyntaxKind.ArrayLiteralExpression: - return visitAssignmentPattern(node as AssignmentPattern); - default: - return visitor(node); - } + } + /** + * If we visit a private name, this means it is an undeclared private name. + * Replace it with an empty identifier to indicate a problem with the code. + */ + function visitPrivateIdentifier(node: PrivateIdentifier) { + if (!shouldTransformPrivateFields) { + return node; } - - /** - * If we visit a private name, this means it is an undeclared private name. - * Replace it with an empty identifier to indicate a problem with the code. - */ - function visitPrivateIdentifier(node: PrivateIdentifier) { - if (!shouldTransformPrivateFields) { + return setOriginalNode(createIdentifier(""), node); + } + /** + * Visits the members of a class that has fields. + * + * @param node The node to visit. + */ + function classElementVisitor(node: Node): VisitResult { + switch (node.kind) { + case SyntaxKind.Constructor: + // Constructors for classes using class fields are transformed in + // `visitClassDeclaration` or `visitClassExpression`. + return undefined; + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + case SyntaxKind.MethodDeclaration: + // Visit the name of the member (if it's a computed property name). + return visitEachChild(node, classElementVisitor, context); + case SyntaxKind.PropertyDeclaration: + return visitPropertyDeclaration((node as PropertyDeclaration)); + case SyntaxKind.ComputedPropertyName: + return visitComputedPropertyName((node as ComputedPropertyName)); + case SyntaxKind.SemicolonClassElement: return node; - } - return setOriginalNode(createIdentifier(""), node); - } - - /** - * Visits the members of a class that has fields. - * - * @param node The node to visit. - */ - function classElementVisitor(node: Node): VisitResult { - switch (node.kind) { - case SyntaxKind.Constructor: - // Constructors for classes using class fields are transformed in - // `visitClassDeclaration` or `visitClassExpression`. - return undefined; - - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - case SyntaxKind.MethodDeclaration: - // Visit the name of the member (if it's a computed property name). - return visitEachChild(node, classElementVisitor, context); - - case SyntaxKind.PropertyDeclaration: - return visitPropertyDeclaration(node as PropertyDeclaration); - - case SyntaxKind.ComputedPropertyName: - return visitComputedPropertyName(node as ComputedPropertyName); - - case SyntaxKind.SemicolonClassElement: - return node; - - default: - return visitor(node); - } + default: + return visitor(node); } - - function visitVariableStatement(node: VariableStatement) { - const savedPendingStatements = pendingStatements; - pendingStatements = []; - - const visitedNode = visitEachChild(node, visitor, context); - const statement = some(pendingStatements) ? - [visitedNode, ...pendingStatements] : - visitedNode; - - pendingStatements = savedPendingStatements; - return statement; + } + function visitVariableStatement(node: VariableStatement) { + const savedPendingStatements = pendingStatements; + pendingStatements = []; + const visitedNode = visitEachChild(node, visitor, context); + const statement = some(pendingStatements) ? + [visitedNode, ...pendingStatements] : + visitedNode; + pendingStatements = savedPendingStatements; + return statement; + } + function visitComputedPropertyName(name: ComputedPropertyName) { + let node = visitEachChild(name, visitor, context); + if (some(pendingExpressions)) { + const expressions = pendingExpressions; + expressions.push(name.expression); + pendingExpressions = []; + node = updateComputedPropertyName(node, inlineExpressions(expressions)); + } + return node; + } + function visitPropertyDeclaration(node: PropertyDeclaration) { + Debug.assert(!some(node.decorators)); + if (!shouldTransformPrivateFields && isPrivateIdentifier(node.name)) { + // Initializer is elided as the field is initialized in transformConstructor. + return updateProperty(node, + /*decorators*/ undefined, visitNodes(node.modifiers, visitor, isModifier), node.name, + /*questionOrExclamationToken*/ undefined, + /*type*/ undefined, + /*initializer*/ undefined); + } + // Create a temporary variable to store a computed property name (if necessary). + // If it's not inlineable, then we emit an expression after the class which assigns + // the property name to the temporary variable. + const expr = getPropertyNameExpressionIfNeeded(node.name, !!node.initializer || !!context.getCompilerOptions().useDefineForClassFields); + if (expr && !isSimpleInlineableExpression(expr)) { + (pendingExpressions || (pendingExpressions = [])).push(expr); + } + return undefined; + } + function createPrivateIdentifierAccess(info: PrivateIdentifierInfo, receiver: Expression): Expression { + receiver = visitNode(receiver, visitor, isExpression); + switch (info.placement) { + case PrivateIdentifierPlacement.InstanceField: + return createClassPrivateFieldGetHelper(context, nodeIsSynthesized(receiver) ? receiver : getSynthesizedClone(receiver), info.weakMapName); + default: return Debug.fail("Unexpected private identifier placement"); } - - function visitComputedPropertyName(name: ComputedPropertyName) { - let node = visitEachChild(name, visitor, context); - if (some(pendingExpressions)) { - const expressions = pendingExpressions; - expressions.push(name.expression); - pendingExpressions = []; - node = updateComputedPropertyName( - node, - inlineExpressions(expressions) - ); + } + function visitPropertyAccessExpression(node: PropertyAccessExpression) { + if (shouldTransformPrivateFields && isPrivateIdentifier(node.name)) { + const privateIdentifierInfo = accessPrivateIdentifier(node.name); + if (privateIdentifierInfo) { + return setOriginalNode(createPrivateIdentifierAccess(privateIdentifierInfo, node.expression), node); } - return node; } - - function visitPropertyDeclaration(node: PropertyDeclaration) { - Debug.assert(!some(node.decorators)); - if (!shouldTransformPrivateFields && isPrivateIdentifier(node.name)) { - // Initializer is elided as the field is initialized in transformConstructor. - return updateProperty( - node, - /*decorators*/ undefined, - visitNodes(node.modifiers, visitor, isModifier), - node.name, - /*questionOrExclamationToken*/ undefined, - /*type*/ undefined, - /*initializer*/ undefined - ); - } - // Create a temporary variable to store a computed property name (if necessary). - // If it's not inlineable, then we emit an expression after the class which assigns - // the property name to the temporary variable. - const expr = getPropertyNameExpressionIfNeeded(node.name, !!node.initializer || !!context.getCompilerOptions().useDefineForClassFields); - if (expr && !isSimpleInlineableExpression(expr)) { - (pendingExpressions || (pendingExpressions = [])).push(expr); + return visitEachChild(node, visitor, context); + } + function visitPrefixUnaryExpression(node: PrefixUnaryExpression) { + if (shouldTransformPrivateFields && isPrivateIdentifierPropertyAccessExpression(node.operand)) { + const operator = node.operator === SyntaxKind.PlusPlusToken ? + SyntaxKind.PlusToken : node.operator === SyntaxKind.MinusMinusToken ? + SyntaxKind.MinusToken : undefined; + let info: PrivateIdentifierInfo | undefined; + if (operator && (info = accessPrivateIdentifier(node.operand.name))) { + const receiver = visitNode(node.operand.expression, visitor, isExpression); + const { readExpression, initializeExpression } = createCopiableReceiverExpr(receiver); + const existingValue = createPrefix(SyntaxKind.PlusToken, createPrivateIdentifierAccess(info, readExpression)); + return setOriginalNode(createPrivateIdentifierAssignment(info, initializeExpression || readExpression, createBinary(existingValue, operator, createLiteral(1)), SyntaxKind.EqualsToken), node); } - return undefined; } - - function createPrivateIdentifierAccess(info: PrivateIdentifierInfo, receiver: Expression): Expression { - receiver = visitNode(receiver, visitor, isExpression); - switch (info.placement) { - case PrivateIdentifierPlacement.InstanceField: - return createClassPrivateFieldGetHelper( - context, - nodeIsSynthesized(receiver) ? receiver : getSynthesizedClone(receiver), - info.weakMapName - ); - default: return Debug.fail("Unexpected private identifier placement"); + return visitEachChild(node, visitor, context); + } + function visitPostfixUnaryExpression(node: PostfixUnaryExpression, valueIsDiscarded: boolean) { + if (shouldTransformPrivateFields && isPrivateIdentifierPropertyAccessExpression(node.operand)) { + const operator = node.operator === SyntaxKind.PlusPlusToken ? + SyntaxKind.PlusToken : node.operator === SyntaxKind.MinusMinusToken ? + SyntaxKind.MinusToken : undefined; + let info: PrivateIdentifierInfo | undefined; + if (operator && (info = accessPrivateIdentifier(node.operand.name))) { + const receiver = visitNode(node.operand.expression, visitor, isExpression); + const { readExpression, initializeExpression } = createCopiableReceiverExpr(receiver); + const existingValue = createPrefix(SyntaxKind.PlusToken, createPrivateIdentifierAccess(info, readExpression)); + // Create a temporary variable to store the value returned by the expression. + const returnValue = valueIsDiscarded ? undefined : createTempVariable(hoistVariableDeclaration); + return setOriginalNode(inlineExpressions(compact([ + createPrivateIdentifierAssignment(info, initializeExpression || readExpression, createBinary(returnValue ? createAssignment(returnValue, existingValue) : existingValue, operator, createLiteral(1)), SyntaxKind.EqualsToken), + returnValue + ])), node); } } - - function visitPropertyAccessExpression(node: PropertyAccessExpression) { - if (shouldTransformPrivateFields && isPrivateIdentifier(node.name)) { - const privateIdentifierInfo = accessPrivateIdentifier(node.name); - if (privateIdentifierInfo) { - return setOriginalNode( - createPrivateIdentifierAccess(privateIdentifierInfo, node.expression), - node - ); - } - } - return visitEachChild(node, visitor, context); + return visitEachChild(node, visitor, context); + } + function visitForStatement(node: ForStatement) { + if (node.incrementor && isPostfixUnaryExpression(node.incrementor)) { + return updateFor(node, visitNode(node.initializer, visitor, isForInitializer), visitNode(node.condition, visitor, isExpression), visitPostfixUnaryExpression(node.incrementor, /*valueIsDiscarded*/ true), visitNode(node.statement, visitor, isStatement)); } - - function visitPrefixUnaryExpression(node: PrefixUnaryExpression) { - if (shouldTransformPrivateFields && isPrivateIdentifierPropertyAccessExpression(node.operand)) { - const operator = node.operator === SyntaxKind.PlusPlusToken ? - SyntaxKind.PlusToken : node.operator === SyntaxKind.MinusMinusToken ? - SyntaxKind.MinusToken : undefined; - let info: PrivateIdentifierInfo | undefined; - if (operator && (info = accessPrivateIdentifier(node.operand.name))) { - const receiver = visitNode(node.operand.expression, visitor, isExpression); - const { readExpression, initializeExpression } = createCopiableReceiverExpr(receiver); - - const existingValue = createPrefix(SyntaxKind.PlusToken, createPrivateIdentifierAccess(info, readExpression)); - - return setOriginalNode( - createPrivateIdentifierAssignment( - info, - initializeExpression || readExpression, - createBinary(existingValue, operator, createLiteral(1)), - SyntaxKind.EqualsToken - ), - node - ); - } - } - return visitEachChild(node, visitor, context); + return visitEachChild(node, visitor, context); + } + function visitExpressionStatement(node: ExpressionStatement) { + if (isPostfixUnaryExpression(node.expression)) { + return updateExpressionStatement(node, visitPostfixUnaryExpression(node.expression, /*valueIsDiscarded*/ true)); } - - function visitPostfixUnaryExpression(node: PostfixUnaryExpression, valueIsDiscarded: boolean) { - if (shouldTransformPrivateFields && isPrivateIdentifierPropertyAccessExpression(node.operand)) { - const operator = node.operator === SyntaxKind.PlusPlusToken ? - SyntaxKind.PlusToken : node.operator === SyntaxKind.MinusMinusToken ? - SyntaxKind.MinusToken : undefined; - let info: PrivateIdentifierInfo | undefined; - if (operator && (info = accessPrivateIdentifier(node.operand.name))) { - const receiver = visitNode(node.operand.expression, visitor, isExpression); - const { readExpression, initializeExpression } = createCopiableReceiverExpr(receiver); - - const existingValue = createPrefix(SyntaxKind.PlusToken, createPrivateIdentifierAccess(info, readExpression)); - - // Create a temporary variable to store the value returned by the expression. - const returnValue = valueIsDiscarded ? undefined : createTempVariable(hoistVariableDeclaration); - - return setOriginalNode( - inlineExpressions(compact([ - createPrivateIdentifierAssignment( - info, - initializeExpression || readExpression, - createBinary( - returnValue ? createAssignment(returnValue, existingValue) : existingValue, - operator, - createLiteral(1) - ), - SyntaxKind.EqualsToken - ), - returnValue - ])), - node - ); + return visitEachChild(node, visitor, context); + } + function createCopiableReceiverExpr(receiver: Expression): { + readExpression: Expression; + initializeExpression: Expression | undefined; + } { + const clone = nodeIsSynthesized(receiver) ? receiver : getSynthesizedClone(receiver); + if (isSimpleInlineableExpression(receiver)) { + return { readExpression: clone, initializeExpression: undefined }; + } + const readExpression = createTempVariable(hoistVariableDeclaration); + const initializeExpression = createAssignment(readExpression, clone); + return { readExpression, initializeExpression }; + } + function visitCallExpression(node: CallExpression) { + if (shouldTransformPrivateFields && isPrivateIdentifierPropertyAccessExpression(node.expression)) { + // Transform call expressions of private names to properly bind the `this` parameter. + const { thisArg, target } = createCallBinding(node.expression, hoistVariableDeclaration, languageVersion); + return updateCall(node, createPropertyAccess(visitNode(target, visitor), "call"), + /*typeArguments*/ undefined, [visitNode(thisArg, visitor, isExpression), ...visitNodes(node.arguments, visitor, isExpression)]); + } + return visitEachChild(node, visitor, context); + } + function visitTaggedTemplateExpression(node: TaggedTemplateExpression) { + if (shouldTransformPrivateFields && isPrivateIdentifierPropertyAccessExpression(node.tag)) { + // Bind the `this` correctly for tagged template literals when the tag is a private identifier property access. + const { thisArg, target } = createCallBinding(node.tag, hoistVariableDeclaration, languageVersion); + return updateTaggedTemplate(node, createCall(createPropertyAccess(visitNode(target, visitor), "bind"), + /*typeArguments*/ undefined, [visitNode(thisArg, visitor, isExpression)]), visitNode(node.template, visitor, isTemplateLiteral)); + } + return visitEachChild(node, visitor, context); + } + function visitBinaryExpression(node: BinaryExpression) { + if (shouldTransformPrivateFields) { + if (isDestructuringAssignment(node)) { + const savedPendingExpressions = pendingExpressions; + pendingExpressions = undefined!; + node = updateBinary(node, visitNode(node.left, visitorDestructuringTarget), visitNode(node.right, visitor), node.operatorToken); + const expr = some(pendingExpressions) ? + inlineExpressions(compact([...pendingExpressions!, node])) : + node; + pendingExpressions = savedPendingExpressions; + return expr; + } + if (isAssignmentExpression(node) && isPrivateIdentifierPropertyAccessExpression(node.left)) { + const info = accessPrivateIdentifier(node.left.name); + if (info) { + return setOriginalNode(createPrivateIdentifierAssignment(info, node.left.expression, node.right, node.operatorToken.kind), node); } } - return visitEachChild(node, visitor, context); } - - function visitForStatement(node: ForStatement) { - if (node.incrementor && isPostfixUnaryExpression(node.incrementor)) { - return updateFor( - node, - visitNode(node.initializer, visitor, isForInitializer), - visitNode(node.condition, visitor, isExpression), - visitPostfixUnaryExpression(node.incrementor, /*valueIsDiscarded*/ true), - visitNode(node.statement, visitor, isStatement) - ); + return visitEachChild(node, visitor, context); + } + function createPrivateIdentifierAssignment(info: PrivateIdentifierInfo, receiver: Expression, right: Expression, operator: AssignmentOperator) { + switch (info.placement) { + case PrivateIdentifierPlacement.InstanceField: { + return createPrivateIdentifierInstanceFieldAssignment(info, receiver, right, operator); } - return visitEachChild(node, visitor, context); + default: return Debug.fail("Unexpected private identifier placement"); } - - function visitExpressionStatement(node: ExpressionStatement) { - if (isPostfixUnaryExpression(node.expression)) { - return updateExpressionStatement(node, visitPostfixUnaryExpression(node.expression, /*valueIsDiscarded*/ true)); - } - return visitEachChild(node, visitor, context); + } + function createPrivateIdentifierInstanceFieldAssignment(info: PrivateIdentifierInstanceField, receiver: Expression, right: Expression, operator: AssignmentOperator) { + receiver = visitNode(receiver, visitor, isExpression); + right = visitNode(right, visitor, isExpression); + if (isCompoundAssignment(operator)) { + const { readExpression, initializeExpression } = createCopiableReceiverExpr(receiver); + return createClassPrivateFieldSetHelper(context, initializeExpression || readExpression, info.weakMapName, createBinary(createClassPrivateFieldGetHelper(context, readExpression, info.weakMapName), getNonAssignmentOperatorForCompoundAssignment(operator), right)); } - - function createCopiableReceiverExpr(receiver: Expression): { readExpression: Expression; initializeExpression: Expression | undefined } { - const clone = nodeIsSynthesized(receiver) ? receiver : getSynthesizedClone(receiver); - if (isSimpleInlineableExpression(receiver)) { - return { readExpression: clone, initializeExpression: undefined }; - } - const readExpression = createTempVariable(hoistVariableDeclaration); - const initializeExpression = createAssignment(readExpression, clone); - return { readExpression, initializeExpression }; + else { + return createClassPrivateFieldSetHelper(context, receiver, info.weakMapName, right); } - - function visitCallExpression(node: CallExpression) { - if (shouldTransformPrivateFields && isPrivateIdentifierPropertyAccessExpression(node.expression)) { - // Transform call expressions of private names to properly bind the `this` parameter. - const { thisArg, target } = createCallBinding(node.expression, hoistVariableDeclaration, languageVersion); - return updateCall( - node, - createPropertyAccess(visitNode(target, visitor), "call"), - /*typeArguments*/ undefined, - [visitNode(thisArg, visitor, isExpression), ...visitNodes(node.arguments, visitor, isExpression)] - ); - } + } + /** + * Set up the environment for a class. + */ + function visitClassLike(node: ClassLikeDeclaration) { + const savedPendingExpressions = pendingExpressions; + pendingExpressions = undefined; + if (shouldTransformPrivateFields) { + startPrivateIdentifierEnvironment(); + } + const result = isClassDeclaration(node) ? + visitClassDeclaration(node) : + visitClassExpression(node); + if (shouldTransformPrivateFields) { + endPrivateIdentifierEnvironment(); + } + pendingExpressions = savedPendingExpressions; + return result; + } + function doesClassElementNeedTransform(node: ClassElement) { + return isPropertyDeclaration(node) || (shouldTransformPrivateFields && node.name && isPrivateIdentifier(node.name)); + } + function visitClassDeclaration(node: ClassDeclaration) { + if (!forEach(node.members, doesClassElementNeedTransform)) { return visitEachChild(node, visitor, context); } - - function visitTaggedTemplateExpression(node: TaggedTemplateExpression) { - if (shouldTransformPrivateFields && isPrivateIdentifierPropertyAccessExpression(node.tag)) { - // Bind the `this` correctly for tagged template literals when the tag is a private identifier property access. - const { thisArg, target } = createCallBinding(node.tag, hoistVariableDeclaration, languageVersion); - return updateTaggedTemplate( - node, - createCall( - createPropertyAccess(visitNode(target, visitor), "bind"), - /*typeArguments*/ undefined, - [visitNode(thisArg, visitor, isExpression)] - ), - visitNode(node.template, visitor, isTemplateLiteral) - ); - } + const extendsClauseElement = getEffectiveBaseTypeNode(node); + const isDerivedClass = !!(extendsClauseElement && skipOuterExpressions(extendsClauseElement.expression).kind !== SyntaxKind.NullKeyword); + const statements: Statement[] = [ + updateClassDeclaration(node, + /*decorators*/ undefined, node.modifiers, node.name, + /*typeParameters*/ undefined, visitNodes(node.heritageClauses, visitor, isHeritageClause), transformClassMembers(node, isDerivedClass)) + ]; + // Write any pending expressions from elided or moved computed property names + if (some(pendingExpressions)) { + statements.push(createExpressionStatement(inlineExpressions(pendingExpressions))); + } + // Emit static property assignment. Because classDeclaration is lexically evaluated, + // it is safe to emit static property assignment after classDeclaration + // From ES6 specification: + // HasLexicalDeclaration (N) : Determines if the argument identifier has a binding in this environment record that was created using + // a lexical declaration such as a LexicalDeclaration or a ClassDeclaration. + const staticProperties = getProperties(node, /*requireInitializer*/ true, /*isStatic*/ true); + if (some(staticProperties)) { + addPropertyStatements(statements, staticProperties, getInternalName(node)); + } + return statements; + } + function visitClassExpression(node: ClassExpression): Expression { + if (!forEach(node.members, doesClassElementNeedTransform)) { return visitEachChild(node, visitor, context); } - - function visitBinaryExpression(node: BinaryExpression) { - if (shouldTransformPrivateFields) { - if (isDestructuringAssignment(node)) { - const savedPendingExpressions = pendingExpressions; - pendingExpressions = undefined!; - node = updateBinary( - node, - visitNode(node.left, visitorDestructuringTarget), - visitNode(node.right, visitor), - node.operatorToken - ); - const expr = some(pendingExpressions) ? - inlineExpressions(compact([...pendingExpressions!, node])) : - node; - pendingExpressions = savedPendingExpressions; - return expr; + // If this class expression is a transformation of a decorated class declaration, + // then we want to output the pendingExpressions as statements, not as inlined + // expressions with the class statement. + // + // In this case, we use pendingStatements to produce the same output as the + // class declaration transformation. The VariableStatement visitor will insert + // these statements after the class expression variable statement. + const isDecoratedClassDeclaration = isClassDeclaration(getOriginalNode(node)); + const staticProperties = getProperties(node, /*requireInitializer*/ true, /*isStatic*/ true); + const extendsClauseElement = getEffectiveBaseTypeNode(node); + const isDerivedClass = !!(extendsClauseElement && skipOuterExpressions(extendsClauseElement.expression).kind !== SyntaxKind.NullKeyword); + const classExpression = updateClassExpression(node, node.modifiers, node.name, + /*typeParameters*/ undefined, visitNodes(node.heritageClauses, visitor, isHeritageClause), transformClassMembers(node, isDerivedClass)); + if (some(staticProperties) || some(pendingExpressions)) { + if (isDecoratedClassDeclaration) { + Debug.assertIsDefined(pendingStatements, "Decorated classes transformed by TypeScript are expected to be within a variable declaration."); + // Write any pending expressions from elided or moved computed property names + if (pendingStatements && pendingExpressions && some(pendingExpressions)) { + pendingStatements.push(createExpressionStatement(inlineExpressions(pendingExpressions))); } - if (isAssignmentExpression(node) && isPrivateIdentifierPropertyAccessExpression(node.left)) { - const info = accessPrivateIdentifier(node.left.name); - if (info) { - return setOriginalNode( - createPrivateIdentifierAssignment(info, node.left.expression, node.right, node.operatorToken.kind), - node - ); - } + if (pendingStatements && some(staticProperties)) { + addPropertyStatements(pendingStatements, staticProperties, getInternalName(node)); } + return classExpression; } - return visitEachChild(node, visitor, context); - } - - function createPrivateIdentifierAssignment(info: PrivateIdentifierInfo, receiver: Expression, right: Expression, operator: AssignmentOperator) { - switch (info.placement) { - case PrivateIdentifierPlacement.InstanceField: { - return createPrivateIdentifierInstanceFieldAssignment(info, receiver, right, operator); + else { + const expressions: Expression[] = []; + const isClassWithConstructorReference = resolver.getNodeCheckFlags(node) & NodeCheckFlags.ClassWithConstructorReference; + const temp = createTempVariable(hoistVariableDeclaration, !!isClassWithConstructorReference); + if (isClassWithConstructorReference) { + // record an alias as the class name is not in scope for statics. + enableSubstitutionForClassAliases(); + const alias = getSynthesizedClone(temp); + alias.autoGenerateFlags &= ~GeneratedIdentifierFlags.ReservedInNestedScopes; + classAliases[getOriginalNodeId(node)] = alias; + } + // To preserve the behavior of the old emitter, we explicitly indent + // the body of a class with static initializers. + setEmitFlags(classExpression, EmitFlags.Indented | getEmitFlags(classExpression)); + expressions.push(startOnNewLine(createAssignment(temp, classExpression))); + // Add any pending expressions leftover from elided or relocated computed property names + addRange(expressions, map(pendingExpressions, startOnNewLine)); + addRange(expressions, generateInitializedPropertyExpressions(staticProperties, temp)); + expressions.push(startOnNewLine(temp)); + return inlineExpressions(expressions); + } + } + return classExpression; + } + function transformClassMembers(node: ClassDeclaration | ClassExpression, isDerivedClass: boolean) { + if (shouldTransformPrivateFields) { + // Declare private names. + for (const member of node.members) { + if (isPrivateIdentifierPropertyDeclaration(member)) { + addPrivateIdentifierToEnvironment(member.name); } - default: return Debug.fail("Unexpected private identifier placement"); } } - - function createPrivateIdentifierInstanceFieldAssignment(info: PrivateIdentifierInstanceField, receiver: Expression, right: Expression, operator: AssignmentOperator) { - receiver = visitNode(receiver, visitor, isExpression); - right = visitNode(right, visitor, isExpression); - if (isCompoundAssignment(operator)) { - const { readExpression, initializeExpression } = createCopiableReceiverExpr(receiver); - return createClassPrivateFieldSetHelper( - context, - initializeExpression || readExpression, - info.weakMapName, - createBinary( - createClassPrivateFieldGetHelper(context, readExpression, info.weakMapName), - getNonAssignmentOperatorForCompoundAssignment(operator), - right - ) - ); - } - else { - return createClassPrivateFieldSetHelper(context, receiver, info.weakMapName, right); - } + const members: ClassElement[] = []; + const constructor = transformConstructor(node, isDerivedClass); + if (constructor) { + members.push(constructor); } - - /** - * Set up the environment for a class. - */ - function visitClassLike(node: ClassLikeDeclaration) { - const savedPendingExpressions = pendingExpressions; - pendingExpressions = undefined; - if (shouldTransformPrivateFields) { - startPrivateIdentifierEnvironment(); - } - - const result = isClassDeclaration(node) ? - visitClassDeclaration(node) : - visitClassExpression(node); - - if (shouldTransformPrivateFields) { - endPrivateIdentifierEnvironment(); - } - pendingExpressions = savedPendingExpressions; - return result; + addRange(members, visitNodes(node.members, classElementVisitor, isClassElement)); + return setTextRange(createNodeArray(members), /*location*/ node.members); + } + function isPropertyDeclarationThatRequiresConstructorStatement(member: ClassElement): member is PropertyDeclaration { + if (!isPropertyDeclaration(member) || hasStaticModifier(member)) { + return false; } - - function doesClassElementNeedTransform(node: ClassElement) { - return isPropertyDeclaration(node) || (shouldTransformPrivateFields && node.name && isPrivateIdentifier(node.name)); + if (context.getCompilerOptions().useDefineForClassFields) { + // If we are using define semantics and targeting ESNext or higher, + // then we don't need to transform any class properties. + return languageVersion < ScriptTarget.ESNext; } - - function visitClassDeclaration(node: ClassDeclaration) { - if (!forEach(node.members, doesClassElementNeedTransform)) { - return visitEachChild(node, visitor, context); - } - - const extendsClauseElement = getEffectiveBaseTypeNode(node); - const isDerivedClass = !!(extendsClauseElement && skipOuterExpressions(extendsClauseElement.expression).kind !== SyntaxKind.NullKeyword); - - const statements: Statement[] = [ - updateClassDeclaration( - node, - /*decorators*/ undefined, - node.modifiers, - node.name, - /*typeParameters*/ undefined, - visitNodes(node.heritageClauses, visitor, isHeritageClause), - transformClassMembers(node, isDerivedClass) - ) - ]; - - // Write any pending expressions from elided or moved computed property names - if (some(pendingExpressions)) { - statements.push(createExpressionStatement(inlineExpressions(pendingExpressions))); - } - - // Emit static property assignment. Because classDeclaration is lexically evaluated, - // it is safe to emit static property assignment after classDeclaration - // From ES6 specification: - // HasLexicalDeclaration (N) : Determines if the argument identifier has a binding in this environment record that was created using - // a lexical declaration such as a LexicalDeclaration or a ClassDeclaration. - const staticProperties = getProperties(node, /*requireInitializer*/ true, /*isStatic*/ true); - if (some(staticProperties)) { - addPropertyStatements(statements, staticProperties, getInternalName(node)); - } - - return statements; + return isInitializedProperty(member) || shouldTransformPrivateFields && isPrivateIdentifierPropertyDeclaration(member); + } + function transformConstructor(node: ClassDeclaration | ClassExpression, isDerivedClass: boolean) { + const constructor = visitNode(getFirstConstructorWithBody(node), visitor, isConstructorDeclaration); + const properties = node.members.filter(isPropertyDeclarationThatRequiresConstructorStatement); + if (!some(properties)) { + return constructor; + } + const parameters = visitParameterList(constructor ? constructor.parameters : undefined, visitor, context); + const body = transformConstructorBody(node, constructor, isDerivedClass); + if (!body) { + return undefined; } - - function visitClassExpression(node: ClassExpression): Expression { - if (!forEach(node.members, doesClassElementNeedTransform)) { - return visitEachChild(node, visitor, context); - } - - // If this class expression is a transformation of a decorated class declaration, - // then we want to output the pendingExpressions as statements, not as inlined - // expressions with the class statement. + return startOnNewLine(setOriginalNode(setTextRange(createConstructor( + /*decorators*/ undefined, + /*modifiers*/ undefined, parameters, body), constructor || node), constructor)); + } + function transformConstructorBody(node: ClassDeclaration | ClassExpression, constructor: ConstructorDeclaration | undefined, isDerivedClass: boolean) { + const useDefineForClassFields = context.getCompilerOptions().useDefineForClassFields; + let properties = getProperties(node, /*requireInitializer*/ false, /*isStatic*/ false); + if (!useDefineForClassFields) { + properties = filter(properties, property => !!property.initializer || isPrivateIdentifier(property.name)); + } + // Only generate synthetic constructor when there are property initializers to move. + if (!constructor && !some(properties)) { + return visitFunctionBody(/*node*/ undefined, visitor, context); + } + resumeLexicalEnvironment(); + let indexOfFirstStatement = 0; + let statements: Statement[] = []; + if (!constructor && isDerivedClass) { + // Add a synthetic `super` call: // - // In this case, we use pendingStatements to produce the same output as the - // class declaration transformation. The VariableStatement visitor will insert - // these statements after the class expression variable statement. - const isDecoratedClassDeclaration = isClassDeclaration(getOriginalNode(node)); - - const staticProperties = getProperties(node, /*requireInitializer*/ true, /*isStatic*/ true); - const extendsClauseElement = getEffectiveBaseTypeNode(node); - const isDerivedClass = !!(extendsClauseElement && skipOuterExpressions(extendsClauseElement.expression).kind !== SyntaxKind.NullKeyword); - - const classExpression = updateClassExpression( - node, - node.modifiers, - node.name, - /*typeParameters*/ undefined, - visitNodes(node.heritageClauses, visitor, isHeritageClause), - transformClassMembers(node, isDerivedClass) - ); - - if (some(staticProperties) || some(pendingExpressions)) { - if (isDecoratedClassDeclaration) { - Debug.assertIsDefined(pendingStatements, "Decorated classes transformed by TypeScript are expected to be within a variable declaration."); - - // Write any pending expressions from elided or moved computed property names - if (pendingStatements && pendingExpressions && some(pendingExpressions)) { - pendingStatements.push(createExpressionStatement(inlineExpressions(pendingExpressions))); - } - - if (pendingStatements && some(staticProperties)) { - addPropertyStatements(pendingStatements, staticProperties, getInternalName(node)); - } - return classExpression; - } - else { - const expressions: Expression[] = []; - const isClassWithConstructorReference = resolver.getNodeCheckFlags(node) & NodeCheckFlags.ClassWithConstructorReference; - const temp = createTempVariable(hoistVariableDeclaration, !!isClassWithConstructorReference); - if (isClassWithConstructorReference) { - // record an alias as the class name is not in scope for statics. - enableSubstitutionForClassAliases(); - const alias = getSynthesizedClone(temp); - alias.autoGenerateFlags &= ~GeneratedIdentifierFlags.ReservedInNestedScopes; - classAliases[getOriginalNodeId(node)] = alias; - } - - // To preserve the behavior of the old emitter, we explicitly indent - // the body of a class with static initializers. - setEmitFlags(classExpression, EmitFlags.Indented | getEmitFlags(classExpression)); - expressions.push(startOnNewLine(createAssignment(temp, classExpression))); - // Add any pending expressions leftover from elided or relocated computed property names - addRange(expressions, map(pendingExpressions, startOnNewLine)); - addRange(expressions, generateInitializedPropertyExpressions(staticProperties, temp)); - expressions.push(startOnNewLine(temp)); - - return inlineExpressions(expressions); - } - } - - return classExpression; - } - - function transformClassMembers(node: ClassDeclaration | ClassExpression, isDerivedClass: boolean) { - if (shouldTransformPrivateFields) { - // Declare private names. - for (const member of node.members) { - if (isPrivateIdentifierPropertyDeclaration(member)) { - addPrivateIdentifierToEnvironment(member.name); - } + // super(...arguments); + // + statements.push(createExpressionStatement(createCall(createSuper(), + /*typeArguments*/ undefined, [createSpread(createIdentifier("arguments"))]))); + } + if (constructor) { + indexOfFirstStatement = addPrologueDirectivesAndInitialSuperCall(constructor, statements, visitor); + } + // Add the property initializers. Transforms this: + // + // public x = 1; + // + // Into this: + // + // constructor() { + // this.x = 1; + // } + // + if (constructor?.body) { + let afterParameterProperties = findIndex(constructor.body.statements, s => !isParameterPropertyDeclaration(getOriginalNode(s), constructor), indexOfFirstStatement); + if (afterParameterProperties === -1) { + afterParameterProperties = constructor.body.statements.length; + } + if (afterParameterProperties > indexOfFirstStatement) { + if (!useDefineForClassFields) { + addRange(statements, visitNodes(constructor.body.statements, visitor, isStatement, indexOfFirstStatement, afterParameterProperties - indexOfFirstStatement)); } + indexOfFirstStatement = afterParameterProperties; } - - const members: ClassElement[] = []; - const constructor = transformConstructor(node, isDerivedClass); - if (constructor) { - members.push(constructor); - } - addRange(members, visitNodes(node.members, classElementVisitor, isClassElement)); - return setTextRange(createNodeArray(members), /*location*/ node.members); } - - function isPropertyDeclarationThatRequiresConstructorStatement(member: ClassElement): member is PropertyDeclaration { - if (!isPropertyDeclaration(member) || hasStaticModifier(member)) { - return false; - } - if (context.getCompilerOptions().useDefineForClassFields) { - // If we are using define semantics and targeting ESNext or higher, - // then we don't need to transform any class properties. - return languageVersion < ScriptTarget.ESNext; - } - return isInitializedProperty(member) || shouldTransformPrivateFields && isPrivateIdentifierPropertyDeclaration(member); + addPropertyStatements(statements, properties, createThis()); + // Add existing statements, skipping the initial super call. + if (constructor) { + addRange(statements, visitNodes(constructor.body!.statements, visitor, isStatement, indexOfFirstStatement)); } - - function transformConstructor(node: ClassDeclaration | ClassExpression, isDerivedClass: boolean) { - const constructor = visitNode(getFirstConstructorWithBody(node), visitor, isConstructorDeclaration); - const properties = node.members.filter(isPropertyDeclarationThatRequiresConstructorStatement); - if (!some(properties)) { - return constructor; - } - const parameters = visitParameterList(constructor ? constructor.parameters : undefined, visitor, context); - const body = transformConstructorBody(node, constructor, isDerivedClass); - if (!body) { - return undefined; - } - return startOnNewLine( - setOriginalNode( - setTextRange( - createConstructor( - /*decorators*/ undefined, - /*modifiers*/ undefined, - parameters, - body - ), - constructor || node - ), - constructor - ) - ); + statements = mergeLexicalEnvironment(statements, endLexicalEnvironment()); + return setTextRange(createBlock(setTextRange(createNodeArray(statements), + /*location*/ constructor ? constructor.body!.statements : node.members), + /*multiLine*/ true), + /*location*/ constructor ? constructor.body : undefined); + } + /** + * Generates assignment statements for property initializers. + * + * @param properties An array of property declarations to transform. + * @param receiver The receiver on which each property should be assigned. + */ + function addPropertyStatements(statements: Statement[], properties: readonly PropertyDeclaration[], receiver: LeftHandSideExpression) { + for (const property of properties) { + const expression = transformProperty(property, receiver); + if (!expression) { + continue; + } + const statement = createExpressionStatement(expression); + setSourceMapRange(statement, moveRangePastModifiers(property)); + setCommentRange(statement, property); + setOriginalNode(statement, property); + statements.push(statement); } - - function transformConstructorBody(node: ClassDeclaration | ClassExpression, constructor: ConstructorDeclaration | undefined, isDerivedClass: boolean) { - const useDefineForClassFields = context.getCompilerOptions().useDefineForClassFields; - let properties = getProperties(node, /*requireInitializer*/ false, /*isStatic*/ false); - if (!useDefineForClassFields) { - properties = filter(properties, property => !!property.initializer || isPrivateIdentifier(property.name)); - } - - - // Only generate synthetic constructor when there are property initializers to move. - if (!constructor && !some(properties)) { - return visitFunctionBody(/*node*/ undefined, visitor, context); - } - - resumeLexicalEnvironment(); - - let indexOfFirstStatement = 0; - let statements: Statement[] = []; - - if (!constructor && isDerivedClass) { - // Add a synthetic `super` call: - // - // super(...arguments); - // - statements.push( - createExpressionStatement( - createCall( - createSuper(), - /*typeArguments*/ undefined, - [createSpread(createIdentifier("arguments"))] - ) - ) - ); - } - - if (constructor) { - indexOfFirstStatement = addPrologueDirectivesAndInitialSuperCall(constructor, statements, visitor); - } - // Add the property initializers. Transforms this: - // - // public x = 1; - // - // Into this: - // - // constructor() { - // this.x = 1; - // } - // - if (constructor?.body) { - let afterParameterProperties = findIndex(constructor.body.statements, s => !isParameterPropertyDeclaration(getOriginalNode(s), constructor), indexOfFirstStatement); - if (afterParameterProperties === -1) { - afterParameterProperties = constructor.body.statements.length; - } - if (afterParameterProperties > indexOfFirstStatement) { - if (!useDefineForClassFields) { - addRange(statements, visitNodes(constructor.body.statements, visitor, isStatement, indexOfFirstStatement, afterParameterProperties - indexOfFirstStatement)); + } + /** + * Generates assignment expressions for property initializers. + * + * @param properties An array of property declarations to transform. + * @param receiver The receiver on which each property should be assigned. + */ + function generateInitializedPropertyExpressions(properties: readonly PropertyDeclaration[], receiver: LeftHandSideExpression) { + const expressions: Expression[] = []; + for (const property of properties) { + const expression = transformProperty(property, receiver); + if (!expression) { + continue; + } + startOnNewLine(expression); + setSourceMapRange(expression, moveRangePastModifiers(property)); + setCommentRange(expression, property); + setOriginalNode(expression, property); + expressions.push(expression); + } + return expressions; + } + /** + * Transforms a property initializer into an assignment statement. + * + * @param property The property declaration. + * @param receiver The object receiving the property assignment. + */ + function transformProperty(property: PropertyDeclaration, receiver: LeftHandSideExpression) { + // We generate a name here in order to reuse the value cached by the relocated computed name expression (which uses the same generated name) + const emitAssignment = !context.getCompilerOptions().useDefineForClassFields; + const propertyName = isComputedPropertyName(property.name) && !isSimpleInlineableExpression(property.name.expression) + ? updateComputedPropertyName(property.name, getGeneratedNameForNode(property.name)) + : property.name; + if (shouldTransformPrivateFields && isPrivateIdentifier(propertyName)) { + const privateIdentifierInfo = accessPrivateIdentifier(propertyName); + if (privateIdentifierInfo) { + switch (privateIdentifierInfo.placement) { + case PrivateIdentifierPlacement.InstanceField: { + return createPrivateInstanceFieldInitializer(receiver, visitNode(property.initializer, visitor, isExpression), privateIdentifierInfo.weakMapName); } - indexOfFirstStatement = afterParameterProperties; } } - addPropertyStatements(statements, properties, createThis()); - - // Add existing statements, skipping the initial super call. - if (constructor) { - addRange(statements, visitNodes(constructor.body!.statements, visitor, isStatement, indexOfFirstStatement)); + else { + Debug.fail("Undeclared private name for property declaration."); } - - statements = mergeLexicalEnvironment(statements, endLexicalEnvironment()); - - return setTextRange( - createBlock( - setTextRange( - createNodeArray(statements), - /*location*/ constructor ? constructor.body!.statements : node.members - ), - /*multiLine*/ true - ), - /*location*/ constructor ? constructor.body : undefined - ); } - - /** - * Generates assignment statements for property initializers. - * - * @param properties An array of property declarations to transform. - * @param receiver The receiver on which each property should be assigned. - */ - function addPropertyStatements(statements: Statement[], properties: readonly PropertyDeclaration[], receiver: LeftHandSideExpression) { - for (const property of properties) { - const expression = transformProperty(property, receiver); - if (!expression) { - continue; - } - const statement = createExpressionStatement(expression); - setSourceMapRange(statement, moveRangePastModifiers(property)); - setCommentRange(statement, property); - setOriginalNode(statement, property); - statements.push(statement); - } + if (isPrivateIdentifier(propertyName) && !property.initializer) { + return undefined; } - - /** - * Generates assignment expressions for property initializers. - * - * @param properties An array of property declarations to transform. - * @param receiver The receiver on which each property should be assigned. - */ - function generateInitializedPropertyExpressions(properties: readonly PropertyDeclaration[], receiver: LeftHandSideExpression) { - const expressions: Expression[] = []; - for (const property of properties) { - const expression = transformProperty(property, receiver); - if (!expression) { - continue; - } - startOnNewLine(expression); - setSourceMapRange(expression, moveRangePastModifiers(property)); - setCommentRange(expression, property); - setOriginalNode(expression, property); - expressions.push(expression); - } - - return expressions; + if (isPrivateIdentifier(propertyName) && !property.initializer) { + return undefined; } - - /** - * Transforms a property initializer into an assignment statement. - * - * @param property The property declaration. - * @param receiver The object receiving the property assignment. - */ - function transformProperty(property: PropertyDeclaration, receiver: LeftHandSideExpression) { - // We generate a name here in order to reuse the value cached by the relocated computed name expression (which uses the same generated name) - const emitAssignment = !context.getCompilerOptions().useDefineForClassFields; - const propertyName = isComputedPropertyName(property.name) && !isSimpleInlineableExpression(property.name.expression) - ? updateComputedPropertyName(property.name, getGeneratedNameForNode(property.name)) - : property.name; - - if (shouldTransformPrivateFields && isPrivateIdentifier(propertyName)) { - const privateIdentifierInfo = accessPrivateIdentifier(propertyName); - if (privateIdentifierInfo) { - switch (privateIdentifierInfo.placement) { - case PrivateIdentifierPlacement.InstanceField: { - return createPrivateInstanceFieldInitializer( - receiver, - visitNode(property.initializer, visitor, isExpression), - privateIdentifierInfo.weakMapName - ); - } - } - } - else { - Debug.fail("Undeclared private name for property declaration."); - } - } - if (isPrivateIdentifier(propertyName) && !property.initializer) { - return undefined; - } - - if (isPrivateIdentifier(propertyName) && !property.initializer) { - return undefined; - } - - const propertyOriginalNode = getOriginalNode(property); - const initializer = property.initializer || emitAssignment ? visitNode(property.initializer, visitor, isExpression) - : isParameterPropertyDeclaration(propertyOriginalNode, propertyOriginalNode.parent) && isIdentifier(propertyName) ? propertyName + const propertyOriginalNode = getOriginalNode(property); + const initializer = property.initializer || emitAssignment ? visitNode(property.initializer, visitor, isExpression) + : isParameterPropertyDeclaration(propertyOriginalNode, propertyOriginalNode.parent) && isIdentifier(propertyName) ? propertyName : createVoidZero(); - - if (emitAssignment || isPrivateIdentifier(propertyName)) { - const memberAccess = createMemberAccessForPropertyName(receiver, propertyName, /*location*/ propertyName); - return createAssignment(memberAccess, initializer); - } - else { - const name = isComputedPropertyName(propertyName) ? propertyName.expression - : isIdentifier(propertyName) ? createStringLiteral(unescapeLeadingUnderscores(propertyName.escapedText)) - : propertyName; - const descriptor = createPropertyDescriptor({ value: initializer, configurable: true, writable: true, enumerable: true }); - return createObjectDefinePropertyCall(receiver, name, descriptor); - } + if (emitAssignment || isPrivateIdentifier(propertyName)) { + const memberAccess = createMemberAccessForPropertyName(receiver, propertyName, /*location*/ propertyName); + return createAssignment(memberAccess, initializer); } - - function enableSubstitutionForClassAliases() { - if ((enabledSubstitutions & ClassPropertySubstitutionFlags.ClassAliases) === 0) { - enabledSubstitutions |= ClassPropertySubstitutionFlags.ClassAliases; - - // We need to enable substitutions for identifiers. This allows us to - // substitute class names inside of a class declaration. - context.enableSubstitution(SyntaxKind.Identifier); - - // Keep track of class aliases. - classAliases = []; - } + else { + const name = isComputedPropertyName(propertyName) ? propertyName.expression + : isIdentifier(propertyName) ? createStringLiteral(unescapeLeadingUnderscores(propertyName.escapedText)) + : propertyName; + const descriptor = createPropertyDescriptor({ value: initializer, configurable: true, writable: true, enumerable: true }); + return createObjectDefinePropertyCall(receiver, name, descriptor); } - - /** - * Hooks node substitutions. - * - * @param hint The context for the emitter. - * @param node The node to substitute. - */ - function onSubstituteNode(hint: EmitHint, node: Node) { - node = previousOnSubstituteNode(hint, node); - if (hint === EmitHint.Expression) { - return substituteExpression(node as Expression); - } - return node; + } + function enableSubstitutionForClassAliases() { + if ((enabledSubstitutions & ClassPropertySubstitutionFlags.ClassAliases) === 0) { + enabledSubstitutions |= ClassPropertySubstitutionFlags.ClassAliases; + // We need to enable substitutions for identifiers. This allows us to + // substitute class names inside of a class declaration. + context.enableSubstitution(SyntaxKind.Identifier); + // Keep track of class aliases. + classAliases = []; } - - function substituteExpression(node: Expression) { - switch (node.kind) { - case SyntaxKind.Identifier: - return substituteExpressionIdentifier(node as Identifier); - } - return node; + } + /** + * Hooks node substitutions. + * + * @param hint The context for the emitter. + * @param node The node to substitute. + */ + function onSubstituteNode(hint: EmitHint, node: Node) { + node = previousOnSubstituteNode(hint, node); + if (hint === EmitHint.Expression) { + return substituteExpression((node as Expression)); } - - function substituteExpressionIdentifier(node: Identifier): Expression { - return trySubstituteClassAlias(node) || node; + return node; + } + function substituteExpression(node: Expression) { + switch (node.kind) { + case SyntaxKind.Identifier: + return substituteExpressionIdentifier((node as Identifier)); } - - function trySubstituteClassAlias(node: Identifier): Expression | undefined { - if (enabledSubstitutions & ClassPropertySubstitutionFlags.ClassAliases) { - if (resolver.getNodeCheckFlags(node) & NodeCheckFlags.ConstructorReferenceInClass) { - // Due to the emit for class decorators, any reference to the class from inside of the class body - // must instead be rewritten to point to a temporary variable to avoid issues with the double-bind - // behavior of class names in ES6. - // Also, when emitting statics for class expressions, we must substitute a class alias for - // constructor references in static property initializers. - const declaration = resolver.getReferencedValueDeclaration(node); - if (declaration) { - const classAlias = classAliases[declaration.id!]; // TODO: GH#18217 - if (classAlias) { - const clone = getSynthesizedClone(classAlias); - setSourceMapRange(clone, node); - setCommentRange(clone, node); - return clone; - } + return node; + } + function substituteExpressionIdentifier(node: Identifier): Expression { + return trySubstituteClassAlias(node) || node; + } + function trySubstituteClassAlias(node: Identifier): Expression | undefined { + if (enabledSubstitutions & ClassPropertySubstitutionFlags.ClassAliases) { + if (resolver.getNodeCheckFlags(node) & NodeCheckFlags.ConstructorReferenceInClass) { + // Due to the emit for class decorators, any reference to the class from inside of the class body + // must instead be rewritten to point to a temporary variable to avoid issues with the double-bind + // behavior of class names in ES6. + // Also, when emitting statics for class expressions, we must substitute a class alias for + // constructor references in static property initializers. + const declaration = resolver.getReferencedValueDeclaration(node); + if (declaration) { + const classAlias = classAliases[declaration.id!]; // TODO: GH#18217 + if (classAlias) { + const clone = getSynthesizedClone(classAlias); + setSourceMapRange(clone, node); + setCommentRange(clone, node); + return clone; } } } - - return undefined; } - - - /** - * If the name is a computed property, this function transforms it, then either returns an expression which caches the - * value of the result or the expression itself if the value is either unused or safe to inline into multiple locations - * @param shouldHoist Does the expression need to be reused? (ie, for an initializer or a decorator) - */ - function getPropertyNameExpressionIfNeeded(name: PropertyName, shouldHoist: boolean): Expression | undefined { - if (isComputedPropertyName(name)) { - const expression = visitNode(name.expression, visitor, isExpression); - const innerExpression = skipPartiallyEmittedExpressions(expression); - const inlinable = isSimpleInlineableExpression(innerExpression); - const alreadyTransformed = isAssignmentExpression(innerExpression) && isGeneratedIdentifier(innerExpression.left); - if (!alreadyTransformed && !inlinable && shouldHoist) { - const generatedName = getGeneratedNameForNode(name); - hoistVariableDeclaration(generatedName); - return createAssignment(generatedName, expression); - } - return (inlinable || isIdentifier(innerExpression)) ? undefined : expression; - } - } - - function startPrivateIdentifierEnvironment() { - privateIdentifierEnvironmentStack.push(currentPrivateIdentifierEnvironment); - currentPrivateIdentifierEnvironment = undefined; - } - - function endPrivateIdentifierEnvironment() { - currentPrivateIdentifierEnvironment = privateIdentifierEnvironmentStack.pop(); + return undefined; + } + /** + * If the name is a computed property, this function transforms it, then either returns an expression which caches the + * value of the result or the expression itself if the value is either unused or safe to inline into multiple locations + * @param shouldHoist Does the expression need to be reused? (ie, for an initializer or a decorator) + */ + function getPropertyNameExpressionIfNeeded(name: PropertyName, shouldHoist: boolean): Expression | undefined { + if (isComputedPropertyName(name)) { + const expression = visitNode(name.expression, visitor, isExpression); + const innerExpression = skipPartiallyEmittedExpressions(expression); + const inlinable = isSimpleInlineableExpression(innerExpression); + const alreadyTransformed = isAssignmentExpression(innerExpression) && isGeneratedIdentifier(innerExpression.left); + if (!alreadyTransformed && !inlinable && shouldHoist) { + const generatedName = getGeneratedNameForNode(name); + hoistVariableDeclaration(generatedName); + return createAssignment(generatedName, expression); + } + return (inlinable || isIdentifier(innerExpression)) ? undefined : expression; } - - function addPrivateIdentifierToEnvironment(name: PrivateIdentifier) { - const text = getTextOfPropertyName(name) as string; - const weakMapName = createOptimisticUniqueName("_" + text.substring(1)); - weakMapName.autoGenerateFlags |= GeneratedIdentifierFlags.ReservedInNestedScopes; - hoistVariableDeclaration(weakMapName); - (currentPrivateIdentifierEnvironment || (currentPrivateIdentifierEnvironment = createUnderscoreEscapedMap())) - .set(name.escapedText, { placement: PrivateIdentifierPlacement.InstanceField, weakMapName }); - (pendingExpressions || (pendingExpressions = [])).push( - createAssignment( - weakMapName, - createNew( - createIdentifier("WeakMap"), - /*typeArguments*/ undefined, - [] - ) - ) - ); + } + function startPrivateIdentifierEnvironment() { + privateIdentifierEnvironmentStack.push(currentPrivateIdentifierEnvironment); + currentPrivateIdentifierEnvironment = undefined; + } + function endPrivateIdentifierEnvironment() { + currentPrivateIdentifierEnvironment = privateIdentifierEnvironmentStack.pop(); + } + function addPrivateIdentifierToEnvironment(name: PrivateIdentifier) { + const text = (getTextOfPropertyName(name) as string); + const weakMapName = createOptimisticUniqueName("_" + text.substring(1)); + weakMapName.autoGenerateFlags |= GeneratedIdentifierFlags.ReservedInNestedScopes; + hoistVariableDeclaration(weakMapName); + (currentPrivateIdentifierEnvironment || (currentPrivateIdentifierEnvironment = createUnderscoreEscapedMap())) + .set(name.escapedText, { placement: PrivateIdentifierPlacement.InstanceField, weakMapName }); + (pendingExpressions || (pendingExpressions = [])).push(createAssignment(weakMapName, createNew(createIdentifier("WeakMap"), + /*typeArguments*/ undefined, []))); + } + function accessPrivateIdentifier(name: PrivateIdentifier) { + if (currentPrivateIdentifierEnvironment) { + const info = currentPrivateIdentifierEnvironment.get(name.escapedText); + if (info) { + return info; + } } - - function accessPrivateIdentifier(name: PrivateIdentifier) { - if (currentPrivateIdentifierEnvironment) { - const info = currentPrivateIdentifierEnvironment.get(name.escapedText); - if (info) { - return info; - } + for (let i = privateIdentifierEnvironmentStack.length - 1; i >= 0; --i) { + const env = privateIdentifierEnvironmentStack[i]; + if (!env) { + continue; } - for (let i = privateIdentifierEnvironmentStack.length - 1; i >= 0; --i) { - const env = privateIdentifierEnvironmentStack[i]; - if (!env) { - continue; - } - const info = env.get(name.escapedText); - if (info) { - return info; - } + const info = env.get(name.escapedText); + if (info) { + return info; } - return undefined; } - - - function wrapPrivateIdentifierForDestructuringTarget(node: PrivateIdentifierPropertyAccessExpression) { - const parameter = getGeneratedNameForNode(node); - const info = accessPrivateIdentifier(node.name); - if (!info) { - return visitEachChild(node, visitor, context); + return undefined; + } + function wrapPrivateIdentifierForDestructuringTarget(node: PrivateIdentifierPropertyAccessExpression) { + const parameter = getGeneratedNameForNode(node); + const info = accessPrivateIdentifier(node.name); + if (!info) { + return visitEachChild(node, visitor, context); + } + let receiver = node.expression; + // We cannot copy `this` or `super` into the function because they will be bound + // differently inside the function. + if (isThisProperty(node) || isSuperProperty(node) || !isSimpleCopiableExpression(node.expression)) { + receiver = createTempVariable(hoistVariableDeclaration); + ((receiver as Identifier).autoGenerateFlags!) |= GeneratedIdentifierFlags.ReservedInNestedScopes; + (pendingExpressions || (pendingExpressions = [])).push(createBinary(receiver, SyntaxKind.EqualsToken, node.expression)); + } + return createPropertyAccess( + // Explicit parens required because of v8 regression (https://bugs.chromium.org/p/v8/issues/detail?id=9560) + createParen(createObjectLiteral([ + createSetAccessor( + /*decorators*/ undefined, + /*modifiers*/ undefined, "value", [createParameter( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, parameter, + /*questionToken*/ undefined, + /*type*/ undefined, + /*initializer*/ undefined)], createBlock([createExpressionStatement(createPrivateIdentifierAssignment(info, receiver, parameter, SyntaxKind.EqualsToken))])) + ])), "value"); + } + function visitArrayAssignmentTarget(node: AssignmentPattern) { + const target = getTargetOfBindingOrAssignmentElement(node); + if (target && isPrivateIdentifierPropertyAccessExpression(target)) { + const wrapped = wrapPrivateIdentifierForDestructuringTarget(target); + if (isAssignmentExpression(node)) { + return updateBinary(node, wrapped, visitNode(node.right, visitor, isExpression), node.operatorToken); + } + else if (isSpreadElement(node)) { + return updateSpread(node, wrapped); } - let receiver = node.expression; - // We cannot copy `this` or `super` into the function because they will be bound - // differently inside the function. - if (isThisProperty(node) || isSuperProperty(node) || !isSimpleCopiableExpression(node.expression)) { - receiver = createTempVariable(hoistVariableDeclaration); - (receiver as Identifier).autoGenerateFlags! |= GeneratedIdentifierFlags.ReservedInNestedScopes; - (pendingExpressions || (pendingExpressions = [])).push(createBinary(receiver, SyntaxKind.EqualsToken, node.expression)); + else { + return wrapped; } - return createPropertyAccess( - // Explicit parens required because of v8 regression (https://bugs.chromium.org/p/v8/issues/detail?id=9560) - createParen( - createObjectLiteral([ - createSetAccessor( - /*decorators*/ undefined, - /*modifiers*/ undefined, - "value", - [createParameter( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, - parameter, - /*questionToken*/ undefined, - /*type*/ undefined, - /*initializer*/ undefined - )], - createBlock( - [createExpressionStatement( - createPrivateIdentifierAssignment( - info, - receiver, - parameter, - SyntaxKind.EqualsToken - ) - )] - ) - ) - ]) - ), - "value" - ); } - - function visitArrayAssignmentTarget(node: AssignmentPattern) { + return visitNode(node, visitorDestructuringTarget); + } + function visitObjectAssignmentTarget(node: ObjectLiteralElementLike) { + if (isPropertyAssignment(node)) { const target = getTargetOfBindingOrAssignmentElement(node); if (target && isPrivateIdentifierPropertyAccessExpression(target)) { + const initializer = getInitializerOfBindingOrAssignmentElement(node); const wrapped = wrapPrivateIdentifierForDestructuringTarget(target); - if (isAssignmentExpression(node)) { - return updateBinary( - node, - wrapped, - visitNode(node.right, visitor, isExpression), - node.operatorToken - ); - } - else if (isSpreadElement(node)) { - return updateSpread(node, wrapped); - } - else { - return wrapped; - } + return updatePropertyAssignment(node, visitNode(node.name, visitor), initializer ? createAssignment(wrapped, visitNode(initializer, visitor)) : wrapped); } - return visitNode(node, visitorDestructuringTarget); + return updatePropertyAssignment(node, visitNode(node.name, visitor), visitNode(node.initializer, visitorDestructuringTarget)); } - - function visitObjectAssignmentTarget(node: ObjectLiteralElementLike) { - if (isPropertyAssignment(node)) { - const target = getTargetOfBindingOrAssignmentElement(node); - if (target && isPrivateIdentifierPropertyAccessExpression(target)) { - const initializer = getInitializerOfBindingOrAssignmentElement(node); - const wrapped = wrapPrivateIdentifierForDestructuringTarget(target); - return updatePropertyAssignment( - node, - visitNode(node.name, visitor), - initializer ? createAssignment(wrapped, visitNode(initializer, visitor)) : wrapped, - ); - } - return updatePropertyAssignment( - node, - visitNode(node.name, visitor), - visitNode(node.initializer, visitorDestructuringTarget) - ); - } - return visitNode(node, visitor); + return visitNode(node, visitor); + } + function visitAssignmentPattern(node: AssignmentPattern) { + if (isArrayLiteralExpression(node)) { + // Transforms private names in destructuring assignment array bindings. + // + // Source: + // ([ this.#myProp ] = [ "hello" ]); + // + // Transformation: + // [ { set value(x) { this.#myProp = x; } }.value ] = [ "hello" ]; + return updateArrayLiteral(node, visitNodes(node.elements, visitArrayAssignmentTarget, isExpression)); } - - - function visitAssignmentPattern(node: AssignmentPattern) { - if (isArrayLiteralExpression(node)) { - // Transforms private names in destructuring assignment array bindings. - // - // Source: - // ([ this.#myProp ] = [ "hello" ]); - // - // Transformation: - // [ { set value(x) { this.#myProp = x; } }.value ] = [ "hello" ]; - return updateArrayLiteral( - node, - visitNodes(node.elements, visitArrayAssignmentTarget, isExpression) - ); - } - else { - // Transforms private names in destructuring assignment object bindings. - // - // Source: - // ({ stringProperty: this.#myProp } = { stringProperty: "hello" }); - // - // Transformation: - // ({ stringProperty: { set value(x) { this.#myProp = x; } }.value }) = { stringProperty: "hello" }; - return updateObjectLiteral( - node, - visitNodes(node.properties, visitObjectAssignmentTarget, isObjectLiteralElementLike) - ); - } + else { + // Transforms private names in destructuring assignment object bindings. + // + // Source: + // ({ stringProperty: this.#myProp } = { stringProperty: "hello" }); + // + // Transformation: + // ({ stringProperty: { set value(x) { this.#myProp = x; } }.value }) = { stringProperty: "hello" }; + return updateObjectLiteral(node, visitNodes(node.properties, visitObjectAssignmentTarget, isObjectLiteralElementLike)); } } - - function createPrivateInstanceFieldInitializer(receiver: LeftHandSideExpression, initializer: Expression | undefined, weakMapName: Identifier) { - return createCall( - createPropertyAccess(weakMapName, "set"), - /*typeArguments*/ undefined, - [receiver, initializer || createVoidZero()] - ); - } - - export const classPrivateFieldGetHelper: UnscopedEmitHelper = { - name: "typescript:classPrivateFieldGet", - scoped: false, - text: ` +} +/* @internal */ +function createPrivateInstanceFieldInitializer(receiver: LeftHandSideExpression, initializer: Expression | undefined, weakMapName: Identifier) { + return createCall(createPropertyAccess(weakMapName, "set"), + /*typeArguments*/ undefined, [receiver, initializer || createVoidZero()]); +} +/* @internal */ +export const classPrivateFieldGetHelper: UnscopedEmitHelper = { + name: "typescript:classPrivateFieldGet", + scoped: false, + text: ` var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, privateMap) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to get private field on non-instance"); } return privateMap.get(receiver); };` - }; - - function createClassPrivateFieldGetHelper(context: TransformationContext, receiver: Expression, privateField: Identifier) { - context.requestEmitHelper(classPrivateFieldGetHelper); - return createCall(getUnscopedHelperName("__classPrivateFieldGet"), /* typeArguments */ undefined, [receiver, privateField]); - } - - export const classPrivateFieldSetHelper: UnscopedEmitHelper = { - name: "typescript:classPrivateFieldSet", - scoped: false, - text: ` +}; +/* @internal */ +function createClassPrivateFieldGetHelper(context: TransformationContext, receiver: Expression, privateField: Identifier) { + context.requestEmitHelper(classPrivateFieldGetHelper); + return createCall(getUnscopedHelperName("__classPrivateFieldGet"), /* typeArguments */ undefined, [receiver, privateField]); +} +/* @internal */ +export const classPrivateFieldSetHelper: UnscopedEmitHelper = { + name: "typescript:classPrivateFieldSet", + scoped: false, + text: ` var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, privateMap, value) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to set private field on non-instance"); @@ -1086,10 +826,9 @@ namespace ts { privateMap.set(receiver, value); return value; };` - }; - - function createClassPrivateFieldSetHelper(context: TransformationContext, receiver: Expression, privateField: Identifier, value: Expression) { - context.requestEmitHelper(classPrivateFieldSetHelper); - return createCall(getUnscopedHelperName("__classPrivateFieldSet"), /* typeArguments */ undefined, [receiver, privateField, value]); - } +}; +/* @internal */ +function createClassPrivateFieldSetHelper(context: TransformationContext, receiver: Expression, privateField: Identifier, value: Expression) { + context.requestEmitHelper(classPrivateFieldSetHelper); + return createCall(getUnscopedHelperName("__classPrivateFieldSet"), /* typeArguments */ undefined, [receiver, privateField, value]); } diff --git a/src/compiler/transformers/declarations.ts b/src/compiler/transformers/declarations.ts index 00417535145b0..d72759f0df3c5 100644 --- a/src/compiler/transformers/declarations.ts +++ b/src/compiler/transformers/declarations.ts @@ -1,1629 +1,1313 @@ +import { EmitHost, EmitResolver, SourceFile, DiagnosticWithLocation, isJsonSourceFile, transformNodes, filter, isSourceFileNotJson, CommentRange, stringContains, Node, getParseTreeNode, SyntaxKind, FunctionLike, ParameterDeclaration, concatenate, getTrailingCommentRanges, skipTrivia, getLeadingCommentRanges, last, getLeadingCommentRangesOfNode, forEach, NodeBuilderFlags, TransformationContext, Debug, GetSymbolAccessibilityDiagnostic, LateVisibilityPaintedStatement, VisitResult, ExportAssignment, Symbol, SymbolTracker, DeclarationName, AnyImportSyntax, createMap, ModuleDeclaration, SymbolFlags, length, getSourceFileOfNode, getOriginalNodeId, SymbolAccessibilityResult, SymbolAccessibility, pushIfUnique, createDiagnosticForNode, getTextOfNode, Diagnostics, declarationNameToString, Bundle, createBundle, map, isExternalOrCommonJsModule, isSourceFileJS, createNodeArray, visitNodes, updateSourceFileNode, createModuleDeclaration, createModifier, createLiteral, getResolvedExternalModuleName, createModuleBlock, setTextRange, mapDefined, createUnparsedSourceFile, getDirectoryPath, normalizeSlashes, getOutputPathsFor, FileReference, NodeArray, Statement, isAnyImportSyntax, isExternalModule, createEmptyExports, arrayFrom, isImportEqualsDeclaration, isExternalModuleReference, isStringLiteralLike, isImportDeclaration, isStringLiteral, contains, toPath, pathIsRelative, getRelativePathToDirectoryOrUrl, startsWith, hasExtension, UnparsedSource, isUnparsedSource, toFileNameLowerCase, BindingName, updateArrayBindingPattern, updateObjectBindingPattern, ArrayBindingElement, updateBindingElement, ModifierFlags, TypeNode, createGetSymbolAccessibilityDiagnosticForNode, updateParameter, createToken, FunctionDeclaration, MethodDeclaration, GetAccessorDeclaration, SetAccessorDeclaration, BindingElement, ConstructSignatureDeclaration, VariableDeclaration, MethodSignature, CallSignatureDeclaration, PropertyDeclaration, PropertySignature, hasModifier, visitNode, createKeywordTypeNode, NamedDeclaration, OmittedExpression, isOmittedExpression, isBindingPattern, some, AccessorDeclaration, getThisParameter, isSetAccessorDeclaration, getSetAccessorValueParameter, createParameter, append, emptyArray, TypeParameterDeclaration, isSourceFile, isTypeAliasDeclaration, isModuleDeclaration, isClassDeclaration, isInterfaceDeclaration, isFunctionLike, isIndexSignatureDeclaration, isMappedTypeNode, EntityNameOrEntityNameExpression, hasJSDocNodes, setCommentRange, getCommentRange, ImportEqualsDeclaration, ImportDeclaration, ExportDeclaration, ImportTypeNode, StringLiteral, getExternalModuleNameFromDeclaration, getExternalModuleImportEqualsDeclarationExpression, updateImportEqualsDeclaration, updateExternalModuleReference, updateImportDeclaration, updateImportClause, updateNamedImports, isLateVisibilityPaintedStatement, isArray, needsScopeMarker, isExternalModuleIndicator, isDeclaration, hasDynamicName, Declaration, isSemicolonClassElement, canProduceDiagnostics, isMethodDeclaration, isMethodSignature, createProperty, DeclarationDiagnosticProducing, isTypeQueryNode, isEntityName, isEntityNameExpression, visitEachChild, updateExpressionWithTypeArguments, parenthesizeTypeParameters, updateTypeReferenceNode, updateConstructSignature, createSignatureDeclaration, isPrivateIdentifier, updateGetAccessor, updateSetAccessor, updateProperty, updatePropertySignature, updateMethodSignature, updateCallSignature, updateIndexSignature, updateTypeScriptVariableDeclaration, updateTypeParameterDeclaration, updateConditionalTypeNode, updateFunctionTypeNode, updateConstructorTypeNode, isLiteralImportTypeNode, updateImportTypeNode, updateLiteralTypeNode, isTypeNode, setOriginalNode, updateExportDeclaration, createOptimisticUniqueName, createVariableDeclaration, createVariableStatement, createVariableDeclarationList, NodeFlags, updateExportAssignment, getMutableClone, createModifiersFromModifierFlags, getModifierFlags, updateTypeAliasDeclaration, isTypeParameterDeclaration, updateInterfaceDeclaration, updateFunctionDeclaration, createIdentifier, NamespaceDeclaration, createSymbolTable, isPropertyAccessExpression, unescapeLeadingUnderscores, updateModuleDeclaration, createExportAssignment, isGlobalScopeAugmentation, updateModuleBlock, isExternalModuleAugmentation, ModuleBody, getFirstConstructorWithBody, compact, flatMap, BindingPattern, Identifier, createPrivateIdentifier, getEffectiveBaseTypeNode, updateHeritageClause, updateClassDeclaration, updateEnumDeclaration, updateEnumMember, VariableStatement, updateVariableStatement, updateVariableDeclarationList, flatten, createGetSymbolAccessibilityDiagnosticForNodeName, LateBoundDeclaration, isExportAssignment, isExportDeclaration, Modifier, AllAccessorDeclarations, HeritageClause, InterfaceDeclaration, ClassDeclaration, TypeAliasDeclaration, EnumDeclaration, ConstructorDeclaration, IndexSignatureDeclaration, ExpressionWithTypeArguments, TypeReferenceNode, ConditionalTypeNode, FunctionTypeNode, ConstructorTypeNode } from "../ts"; +import { getModuleSpecifier } from "../ts.moduleSpecifiers"; +import * as ts from "../ts"; /*@internal*/ -namespace ts { - export function getDeclarationDiagnostics(host: EmitHost, resolver: EmitResolver, file: SourceFile | undefined): DiagnosticWithLocation[] | undefined { - if (file && isJsonSourceFile(file)) { - return []; // No declaration diagnostics for json for now - } - const compilerOptions = host.getCompilerOptions(); - const result = transformNodes(resolver, host, compilerOptions, file ? [file] : filter(host.getSourceFiles(), isSourceFileNotJson), [transformDeclarations], /*allowDtsFiles*/ false); - return result.diagnostics; +export function getDeclarationDiagnostics(host: EmitHost, resolver: EmitResolver, file: SourceFile | undefined): DiagnosticWithLocation[] | undefined { + if (file && isJsonSourceFile(file)) { + return []; // No declaration diagnostics for json for now } - - function hasInternalAnnotation(range: CommentRange, currentSourceFile: SourceFile) { - const comment = currentSourceFile.text.substring(range.pos, range.end); - return stringContains(comment, "@internal"); + const compilerOptions = host.getCompilerOptions(); + const result = transformNodes(resolver, host, compilerOptions, file ? [file] : filter(host.getSourceFiles(), isSourceFileNotJson), [transformDeclarations], /*allowDtsFiles*/ false); + return result.diagnostics; +} +/* @internal */ +function hasInternalAnnotation(range: CommentRange, currentSourceFile: SourceFile) { + const comment = currentSourceFile.text.substring(range.pos, range.end); + return stringContains(comment, "@internal"); +} +/* @internal */ +export function isInternalDeclaration(node: Node, currentSourceFile: SourceFile) { + const parseTreeNode = getParseTreeNode(node); + if (parseTreeNode && parseTreeNode.kind === SyntaxKind.Parameter) { + const paramIdx = (parseTreeNode.parent as FunctionLike).parameters.indexOf((parseTreeNode as ParameterDeclaration)); + const previousSibling = paramIdx > 0 ? (parseTreeNode.parent as FunctionLike).parameters[paramIdx - 1] : undefined; + const text = currentSourceFile.text; + const commentRanges = previousSibling + ? concatenate( + // to handle + // ... parameters, /* @internal */ + // public param: string + getTrailingCommentRanges(text, skipTrivia(text, previousSibling.end + 1, /* stopAfterLineBreak */ false, /* stopAtComments */ true)), getLeadingCommentRanges(text, node.pos)) + : getTrailingCommentRanges(text, skipTrivia(text, node.pos, /* stopAfterLineBreak */ false, /* stopAtComments */ true)); + return commentRanges && commentRanges.length && hasInternalAnnotation(last(commentRanges), currentSourceFile); } - - export function isInternalDeclaration(node: Node, currentSourceFile: SourceFile) { - const parseTreeNode = getParseTreeNode(node); - if (parseTreeNode && parseTreeNode.kind === SyntaxKind.Parameter) { - const paramIdx = (parseTreeNode.parent as FunctionLike).parameters.indexOf(parseTreeNode as ParameterDeclaration); - const previousSibling = paramIdx > 0 ? (parseTreeNode.parent as FunctionLike).parameters[paramIdx - 1] : undefined; - const text = currentSourceFile.text; - const commentRanges = previousSibling - ? concatenate( - // to handle - // ... parameters, /* @internal */ - // public param: string - getTrailingCommentRanges(text, skipTrivia(text, previousSibling.end + 1, /* stopAfterLineBreak */ false, /* stopAtComments */ true)), - getLeadingCommentRanges(text, node.pos) - ) - : getTrailingCommentRanges(text, skipTrivia(text, node.pos, /* stopAfterLineBreak */ false, /* stopAtComments */ true)); - return commentRanges && commentRanges.length && hasInternalAnnotation(last(commentRanges), currentSourceFile); - } - const leadingCommentRanges = parseTreeNode && getLeadingCommentRangesOfNode(parseTreeNode, currentSourceFile); - return !!forEach(leadingCommentRanges, range => { - return hasInternalAnnotation(range, currentSourceFile); - }); + const leadingCommentRanges = parseTreeNode && getLeadingCommentRangesOfNode(parseTreeNode, currentSourceFile); + return !!forEach(leadingCommentRanges, range => { + return hasInternalAnnotation(range, currentSourceFile); + }); +} +/* @internal */ +const declarationEmitNodeBuilderFlags = NodeBuilderFlags.MultilineObjectLiterals | + NodeBuilderFlags.WriteClassExpressionAsTypeLiteral | + NodeBuilderFlags.UseTypeOfFunction | + NodeBuilderFlags.UseStructuralFallback | + NodeBuilderFlags.AllowEmptyTuple | + NodeBuilderFlags.GenerateNamesForShadowedTypeParams | + NodeBuilderFlags.NoTruncation; +/** + * Transforms a ts file into a .d.ts file + * This process requires type information, which is retrieved through the emit resolver. Because of this, + * in many places this transformer assumes it will be operating on parse tree nodes directly. + * This means that _no transforms should be allowed to occur before this one_. + */ +/* @internal */ +export function transformDeclarations(context: TransformationContext) { + const throwDiagnostic = () => Debug.fail("Diagnostic emitted without context"); + let getSymbolAccessibilityDiagnostic: GetSymbolAccessibilityDiagnostic = throwDiagnostic; + let needsDeclare = true; + let isBundledEmit = false; + let resultHasExternalModuleIndicator = false; + let needsScopeFixMarker = false; + let resultHasScopeMarker = false; + let enclosingDeclaration: Node; + let necessaryTypeReferences: ts.Map | undefined; + let lateMarkedStatements: LateVisibilityPaintedStatement[] | undefined; + let lateStatementReplacementMap: ts.Map>; + let suppressNewDiagnosticContexts: boolean; + let exportedModulesFromDeclarationEmit: Symbol[] | undefined; + const host = context.getEmitHost(); + const symbolTracker: SymbolTracker = { + trackSymbol, + reportInaccessibleThisError, + reportInaccessibleUniqueSymbolError, + reportPrivateInBaseOfClassExpression, + reportLikelyUnsafeImportRequiredError, + moduleResolverHost: host, + trackReferencedAmbientModule, + trackExternalModuleSymbolOfImportTypeNode + }; + let errorNameNode: DeclarationName | undefined; + let currentSourceFile: SourceFile; + let refs: ts.Map; + let libs: ts.Map; + let emittedImports: readonly AnyImportSyntax[] | undefined; // must be declared in container so it can be `undefined` while transformer's first pass + const resolver = context.getEmitResolver(); + const options = context.getCompilerOptions(); + const { noResolve, stripInternal } = options; + return transformRoot; + function recordTypeReferenceDirectivesIfNecessary(typeReferenceDirectives: readonly string[] | undefined): void { + if (!typeReferenceDirectives) { + return; + } + necessaryTypeReferences = necessaryTypeReferences || createMap(); + for (const ref of typeReferenceDirectives) { + necessaryTypeReferences.set(ref, true); + } } - - const declarationEmitNodeBuilderFlags = - NodeBuilderFlags.MultilineObjectLiterals | - NodeBuilderFlags.WriteClassExpressionAsTypeLiteral | - NodeBuilderFlags.UseTypeOfFunction | - NodeBuilderFlags.UseStructuralFallback | - NodeBuilderFlags.AllowEmptyTuple | - NodeBuilderFlags.GenerateNamesForShadowedTypeParams | - NodeBuilderFlags.NoTruncation; - - /** - * Transforms a ts file into a .d.ts file - * This process requires type information, which is retrieved through the emit resolver. Because of this, - * in many places this transformer assumes it will be operating on parse tree nodes directly. - * This means that _no transforms should be allowed to occur before this one_. - */ - export function transformDeclarations(context: TransformationContext) { - const throwDiagnostic = () => Debug.fail("Diagnostic emitted without context"); - let getSymbolAccessibilityDiagnostic: GetSymbolAccessibilityDiagnostic = throwDiagnostic; - let needsDeclare = true; - let isBundledEmit = false; - let resultHasExternalModuleIndicator = false; - let needsScopeFixMarker = false; - let resultHasScopeMarker = false; - let enclosingDeclaration: Node; - let necessaryTypeReferences: Map | undefined; - let lateMarkedStatements: LateVisibilityPaintedStatement[] | undefined; - let lateStatementReplacementMap: Map>; - let suppressNewDiagnosticContexts: boolean; - let exportedModulesFromDeclarationEmit: Symbol[] | undefined; - - const host = context.getEmitHost(); - const symbolTracker: SymbolTracker = { - trackSymbol, - reportInaccessibleThisError, - reportInaccessibleUniqueSymbolError, - reportPrivateInBaseOfClassExpression, - reportLikelyUnsafeImportRequiredError, - moduleResolverHost: host, - trackReferencedAmbientModule, - trackExternalModuleSymbolOfImportTypeNode - }; - let errorNameNode: DeclarationName | undefined; - - let currentSourceFile: SourceFile; - let refs: Map; - let libs: Map; - let emittedImports: readonly AnyImportSyntax[] | undefined; // must be declared in container so it can be `undefined` while transformer's first pass - const resolver = context.getEmitResolver(); - const options = context.getCompilerOptions(); - const { noResolve, stripInternal } = options; - return transformRoot; - - function recordTypeReferenceDirectivesIfNecessary(typeReferenceDirectives: readonly string[] | undefined): void { - if (!typeReferenceDirectives) { - return; - } - necessaryTypeReferences = necessaryTypeReferences || createMap(); - for (const ref of typeReferenceDirectives) { - necessaryTypeReferences.set(ref, true); - } + function trackReferencedAmbientModule(node: ModuleDeclaration, symbol: Symbol) { + // If it is visible via `// `, then we should just use that + const directives = resolver.getTypeReferenceDirectivesForSymbol(symbol, SymbolFlags.All); + if (length(directives)) { + return recordTypeReferenceDirectivesIfNecessary(directives); } - - function trackReferencedAmbientModule(node: ModuleDeclaration, symbol: Symbol) { - // If it is visible via `// `, then we should just use that - const directives = resolver.getTypeReferenceDirectivesForSymbol(symbol, SymbolFlags.All); - if (length(directives)) { - return recordTypeReferenceDirectivesIfNecessary(directives); - } - // Otherwise we should emit a path-based reference - const container = getSourceFileOfNode(node); - refs.set("" + getOriginalNodeId(container), container); - } - - function handleSymbolAccessibilityError(symbolAccessibilityResult: SymbolAccessibilityResult) { - if (symbolAccessibilityResult.accessibility === SymbolAccessibility.Accessible) { - // Add aliases back onto the possible imports list if they're not there so we can try them again with updated visibility info - if (symbolAccessibilityResult && symbolAccessibilityResult.aliasesToMakeVisible) { - if (!lateMarkedStatements) { - lateMarkedStatements = symbolAccessibilityResult.aliasesToMakeVisible; - } - else { - for (const ref of symbolAccessibilityResult.aliasesToMakeVisible) { - pushIfUnique(lateMarkedStatements, ref); - } - } + // Otherwise we should emit a path-based reference + const container = getSourceFileOfNode(node); + refs.set("" + getOriginalNodeId(container), container); + } + function handleSymbolAccessibilityError(symbolAccessibilityResult: SymbolAccessibilityResult) { + if (symbolAccessibilityResult.accessibility === SymbolAccessibility.Accessible) { + // Add aliases back onto the possible imports list if they're not there so we can try them again with updated visibility info + if (symbolAccessibilityResult && symbolAccessibilityResult.aliasesToMakeVisible) { + if (!lateMarkedStatements) { + lateMarkedStatements = symbolAccessibilityResult.aliasesToMakeVisible; } - - // TODO: Do all these accessibility checks inside/after the first pass in the checker when declarations are enabled, if possible - } - else { - // Report error - const errorInfo = getSymbolAccessibilityDiagnostic(symbolAccessibilityResult); - if (errorInfo) { - if (errorInfo.typeName) { - context.addDiagnostic(createDiagnosticForNode(symbolAccessibilityResult.errorNode || errorInfo.errorNode, - errorInfo.diagnosticMessage, - getTextOfNode(errorInfo.typeName), - symbolAccessibilityResult.errorSymbolName, - symbolAccessibilityResult.errorModuleName)); - } - else { - context.addDiagnostic(createDiagnosticForNode(symbolAccessibilityResult.errorNode || errorInfo.errorNode, - errorInfo.diagnosticMessage, - symbolAccessibilityResult.errorSymbolName, - symbolAccessibilityResult.errorModuleName)); + else { + for (const ref of symbolAccessibilityResult.aliasesToMakeVisible) { + pushIfUnique(lateMarkedStatements, ref); } } } + // TODO: Do all these accessibility checks inside/after the first pass in the checker when declarations are enabled, if possible } - - function trackExternalModuleSymbolOfImportTypeNode(symbol: Symbol) { - if (!isBundledEmit) { - (exportedModulesFromDeclarationEmit || (exportedModulesFromDeclarationEmit = [])).push(symbol); + else { + // Report error + const errorInfo = getSymbolAccessibilityDiagnostic(symbolAccessibilityResult); + if (errorInfo) { + if (errorInfo.typeName) { + context.addDiagnostic(createDiagnosticForNode(symbolAccessibilityResult.errorNode || errorInfo.errorNode, errorInfo.diagnosticMessage, getTextOfNode(errorInfo.typeName), symbolAccessibilityResult.errorSymbolName, symbolAccessibilityResult.errorModuleName)); + } + else { + context.addDiagnostic(createDiagnosticForNode(symbolAccessibilityResult.errorNode || errorInfo.errorNode, errorInfo.diagnosticMessage, symbolAccessibilityResult.errorSymbolName, symbolAccessibilityResult.errorModuleName)); + } } } - - function trackSymbol(symbol: Symbol, enclosingDeclaration?: Node, meaning?: SymbolFlags) { - if (symbol.flags & SymbolFlags.TypeParameter) return; - handleSymbolAccessibilityError(resolver.isSymbolAccessible(symbol, enclosingDeclaration, meaning, /*shouldComputeAliasesToMakeVisible*/ true)); - recordTypeReferenceDirectivesIfNecessary(resolver.getTypeReferenceDirectivesForSymbol(symbol, meaning)); - } - - function reportPrivateInBaseOfClassExpression(propertyName: string) { - if (errorNameNode) { - context.addDiagnostic( - createDiagnosticForNode(errorNameNode, Diagnostics.Property_0_of_exported_class_expression_may_not_be_private_or_protected, propertyName)); - } + } + function trackExternalModuleSymbolOfImportTypeNode(symbol: Symbol) { + if (!isBundledEmit) { + (exportedModulesFromDeclarationEmit || (exportedModulesFromDeclarationEmit = [])).push(symbol); } - - function reportInaccessibleUniqueSymbolError() { - if (errorNameNode) { - context.addDiagnostic(createDiagnosticForNode(errorNameNode, Diagnostics.The_inferred_type_of_0_references_an_inaccessible_1_type_A_type_annotation_is_necessary, - declarationNameToString(errorNameNode), - "unique symbol")); - } + } + function trackSymbol(symbol: Symbol, enclosingDeclaration?: Node, meaning?: SymbolFlags) { + if (symbol.flags & SymbolFlags.TypeParameter) + return; + handleSymbolAccessibilityError(resolver.isSymbolAccessible(symbol, enclosingDeclaration, meaning, /*shouldComputeAliasesToMakeVisible*/ true)); + recordTypeReferenceDirectivesIfNecessary(resolver.getTypeReferenceDirectivesForSymbol(symbol, meaning)); + } + function reportPrivateInBaseOfClassExpression(propertyName: string) { + if (errorNameNode) { + context.addDiagnostic(createDiagnosticForNode(errorNameNode, Diagnostics.Property_0_of_exported_class_expression_may_not_be_private_or_protected, propertyName)); } - - function reportInaccessibleThisError() { - if (errorNameNode) { - context.addDiagnostic(createDiagnosticForNode(errorNameNode, Diagnostics.The_inferred_type_of_0_references_an_inaccessible_1_type_A_type_annotation_is_necessary, - declarationNameToString(errorNameNode), - "this")); - } + } + function reportInaccessibleUniqueSymbolError() { + if (errorNameNode) { + context.addDiagnostic(createDiagnosticForNode(errorNameNode, Diagnostics.The_inferred_type_of_0_references_an_inaccessible_1_type_A_type_annotation_is_necessary, declarationNameToString(errorNameNode), "unique symbol")); } - - function reportLikelyUnsafeImportRequiredError(specifier: string) { - if (errorNameNode) { - context.addDiagnostic(createDiagnosticForNode(errorNameNode, Diagnostics.The_inferred_type_of_0_cannot_be_named_without_a_reference_to_1_This_is_likely_not_portable_A_type_annotation_is_necessary, - declarationNameToString(errorNameNode), - specifier)); - } + } + function reportInaccessibleThisError() { + if (errorNameNode) { + context.addDiagnostic(createDiagnosticForNode(errorNameNode, Diagnostics.The_inferred_type_of_0_references_an_inaccessible_1_type_A_type_annotation_is_necessary, declarationNameToString(errorNameNode), "this")); } - - function transformDeclarationsForJS(sourceFile: SourceFile, bundled?: boolean) { - const oldDiag = getSymbolAccessibilityDiagnostic; - getSymbolAccessibilityDiagnostic = (s) => ({ - diagnosticMessage: s.errorModuleName - ? Diagnostics.Declaration_emit_for_this_file_requires_using_private_name_0_from_module_1_An_explicit_type_annotation_may_unblock_declaration_emit - : Diagnostics.Declaration_emit_for_this_file_requires_using_private_name_0_An_explicit_type_annotation_may_unblock_declaration_emit, - errorNode: s.errorNode || sourceFile - }); - const result = resolver.getDeclarationStatementsForSourceFile(sourceFile, declarationEmitNodeBuilderFlags, symbolTracker, bundled); - getSymbolAccessibilityDiagnostic = oldDiag; - return result; - } - - function transformRoot(node: Bundle): Bundle; - function transformRoot(node: SourceFile): SourceFile; - function transformRoot(node: SourceFile | Bundle): SourceFile | Bundle; - function transformRoot(node: SourceFile | Bundle) { - if (node.kind === SyntaxKind.SourceFile && node.isDeclarationFile) { - return node; - } - - if (node.kind === SyntaxKind.Bundle) { - isBundledEmit = true; - refs = createMap(); - libs = createMap(); - let hasNoDefaultLib = false; - const bundle = createBundle(map(node.sourceFiles, - sourceFile => { - if (sourceFile.isDeclarationFile) return undefined!; // Omit declaration files from bundle results, too // TODO: GH#18217 - hasNoDefaultLib = hasNoDefaultLib || sourceFile.hasNoDefaultLib; - currentSourceFile = sourceFile; - enclosingDeclaration = sourceFile; - lateMarkedStatements = undefined; - suppressNewDiagnosticContexts = false; - lateStatementReplacementMap = createMap(); - getSymbolAccessibilityDiagnostic = throwDiagnostic; - needsScopeFixMarker = false; - resultHasScopeMarker = false; - collectReferences(sourceFile, refs); - collectLibs(sourceFile, libs); - if (isExternalOrCommonJsModule(sourceFile) || isJsonSourceFile(sourceFile)) { - resultHasExternalModuleIndicator = false; // unused in external module bundle emit (all external modules are within module blocks, therefore are known to be modules) - needsDeclare = false; - const statements = isSourceFileJS(sourceFile) ? createNodeArray(transformDeclarationsForJS(sourceFile, /*bundled*/ true)) : visitNodes(sourceFile.statements, visitDeclarationStatements); - const newFile = updateSourceFileNode(sourceFile, [createModuleDeclaration( - [], - [createModifier(SyntaxKind.DeclareKeyword)], - createLiteral(getResolvedExternalModuleName(context.getEmitHost(), sourceFile)), - createModuleBlock(setTextRange(createNodeArray(transformAndReplaceLatePaintedStatements(statements)), sourceFile.statements)) - )], /*isDeclarationFile*/ true, /*referencedFiles*/ [], /*typeReferences*/ [], /*hasNoDefaultLib*/ false, /*libReferences*/ []); - return newFile; - } - needsDeclare = true; - const updated = isSourceFileJS(sourceFile) ? createNodeArray(transformDeclarationsForJS(sourceFile)) : visitNodes(sourceFile.statements, visitDeclarationStatements); - return updateSourceFileNode(sourceFile, transformAndReplaceLatePaintedStatements(updated), /*isDeclarationFile*/ true, /*referencedFiles*/ [], /*typeReferences*/ [], /*hasNoDefaultLib*/ false, /*libReferences*/ []); - } - ), mapDefined(node.prepends, prepend => { - if (prepend.kind === SyntaxKind.InputFiles) { - const sourceFile = createUnparsedSourceFile(prepend, "dts", stripInternal); - hasNoDefaultLib = hasNoDefaultLib || !!sourceFile.hasNoDefaultLib; - collectReferences(sourceFile, refs); - recordTypeReferenceDirectivesIfNecessary(sourceFile.typeReferenceDirectives); - collectLibs(sourceFile, libs); - return sourceFile; - } - return prepend; - })); - bundle.syntheticFileReferences = []; - bundle.syntheticTypeReferences = getFileReferencesForUsedTypeReferences(); - bundle.syntheticLibReferences = getLibReferences(); - bundle.hasNoDefaultLib = hasNoDefaultLib; - const outputFilePath = getDirectoryPath(normalizeSlashes(getOutputPathsFor(node, host, /*forceDtsPaths*/ true).declarationFilePath!)); - const referenceVisitor = mapReferencesIntoArray(bundle.syntheticFileReferences as FileReference[], outputFilePath); - refs.forEach(referenceVisitor); - return bundle; - } - - // Single source file - needsDeclare = true; - needsScopeFixMarker = false; - resultHasScopeMarker = false; - enclosingDeclaration = node; - currentSourceFile = node; - getSymbolAccessibilityDiagnostic = throwDiagnostic; - isBundledEmit = false; - resultHasExternalModuleIndicator = false; - suppressNewDiagnosticContexts = false; - lateMarkedStatements = undefined; - lateStatementReplacementMap = createMap(); - necessaryTypeReferences = undefined; - refs = collectReferences(currentSourceFile, createMap()); - libs = collectLibs(currentSourceFile, createMap()); - const references: FileReference[] = []; - const outputFilePath = getDirectoryPath(normalizeSlashes(getOutputPathsFor(node, host, /*forceDtsPaths*/ true).declarationFilePath!)); - const referenceVisitor = mapReferencesIntoArray(references, outputFilePath); - let combinedStatements: NodeArray; - if (isSourceFileJS(currentSourceFile)) { - combinedStatements = createNodeArray(transformDeclarationsForJS(node)); - refs.forEach(referenceVisitor); - emittedImports = filter(combinedStatements, isAnyImportSyntax); - } - else { - const statements = visitNodes(node.statements, visitDeclarationStatements); - combinedStatements = setTextRange(createNodeArray(transformAndReplaceLatePaintedStatements(statements)), node.statements); - refs.forEach(referenceVisitor); - emittedImports = filter(combinedStatements, isAnyImportSyntax); - if (isExternalModule(node) && (!resultHasExternalModuleIndicator || (needsScopeFixMarker && !resultHasScopeMarker))) { - combinedStatements = setTextRange(createNodeArray([...combinedStatements, createEmptyExports()]), combinedStatements); + } + function reportLikelyUnsafeImportRequiredError(specifier: string) { + if (errorNameNode) { + context.addDiagnostic(createDiagnosticForNode(errorNameNode, Diagnostics.The_inferred_type_of_0_cannot_be_named_without_a_reference_to_1_This_is_likely_not_portable_A_type_annotation_is_necessary, declarationNameToString(errorNameNode), specifier)); + } + } + function transformDeclarationsForJS(sourceFile: SourceFile, bundled?: boolean) { + const oldDiag = getSymbolAccessibilityDiagnostic; + getSymbolAccessibilityDiagnostic = (s) => ({ + diagnosticMessage: s.errorModuleName + ? Diagnostics.Declaration_emit_for_this_file_requires_using_private_name_0_from_module_1_An_explicit_type_annotation_may_unblock_declaration_emit + : Diagnostics.Declaration_emit_for_this_file_requires_using_private_name_0_An_explicit_type_annotation_may_unblock_declaration_emit, + errorNode: s.errorNode || sourceFile + }); + const result = resolver.getDeclarationStatementsForSourceFile(sourceFile, declarationEmitNodeBuilderFlags, symbolTracker, bundled); + getSymbolAccessibilityDiagnostic = oldDiag; + return result; + } + function transformRoot(node: Bundle): Bundle; + function transformRoot(node: SourceFile): SourceFile; + function transformRoot(node: SourceFile | Bundle): SourceFile | Bundle; + function transformRoot(node: SourceFile | Bundle) { + if (node.kind === SyntaxKind.SourceFile && node.isDeclarationFile) { + return node; + } + if (node.kind === SyntaxKind.Bundle) { + isBundledEmit = true; + refs = createMap(); + libs = createMap(); + let hasNoDefaultLib = false; + const bundle = createBundle(map(node.sourceFiles, sourceFile => { + if (sourceFile.isDeclarationFile) + return undefined!; // Omit declaration files from bundle results, too // TODO: GH#18217 + hasNoDefaultLib = hasNoDefaultLib || sourceFile.hasNoDefaultLib; + currentSourceFile = sourceFile; + enclosingDeclaration = sourceFile; + lateMarkedStatements = undefined; + suppressNewDiagnosticContexts = false; + lateStatementReplacementMap = createMap(); + getSymbolAccessibilityDiagnostic = throwDiagnostic; + needsScopeFixMarker = false; + resultHasScopeMarker = false; + collectReferences(sourceFile, refs); + collectLibs(sourceFile, libs); + if (isExternalOrCommonJsModule(sourceFile) || isJsonSourceFile(sourceFile)) { + resultHasExternalModuleIndicator = false; // unused in external module bundle emit (all external modules are within module blocks, therefore are known to be modules) + needsDeclare = false; + const statements = isSourceFileJS(sourceFile) ? createNodeArray(transformDeclarationsForJS(sourceFile, /*bundled*/ true)) : visitNodes(sourceFile.statements, visitDeclarationStatements); + const newFile = updateSourceFileNode(sourceFile, [createModuleDeclaration([], [createModifier(SyntaxKind.DeclareKeyword)], createLiteral(getResolvedExternalModuleName(context.getEmitHost(), sourceFile)), createModuleBlock(setTextRange(createNodeArray(transformAndReplaceLatePaintedStatements(statements)), sourceFile.statements)))], /*isDeclarationFile*/ true, /*referencedFiles*/ [], /*typeReferences*/ [], /*hasNoDefaultLib*/ false, /*libReferences*/ []); + return newFile; } + needsDeclare = true; + const updated = isSourceFileJS(sourceFile) ? createNodeArray(transformDeclarationsForJS(sourceFile)) : visitNodes(sourceFile.statements, visitDeclarationStatements); + return updateSourceFileNode(sourceFile, transformAndReplaceLatePaintedStatements(updated), /*isDeclarationFile*/ true, /*referencedFiles*/ [], /*typeReferences*/ [], /*hasNoDefaultLib*/ false, /*libReferences*/ []); + }), mapDefined(node.prepends, prepend => { + if (prepend.kind === SyntaxKind.InputFiles) { + const sourceFile = createUnparsedSourceFile(prepend, "dts", stripInternal); + hasNoDefaultLib = hasNoDefaultLib || !!sourceFile.hasNoDefaultLib; + collectReferences(sourceFile, refs); + recordTypeReferenceDirectivesIfNecessary(sourceFile.typeReferenceDirectives); + collectLibs(sourceFile, libs); + return sourceFile; + } + return prepend; + })); + bundle.syntheticFileReferences = []; + bundle.syntheticTypeReferences = getFileReferencesForUsedTypeReferences(); + bundle.syntheticLibReferences = getLibReferences(); + bundle.hasNoDefaultLib = hasNoDefaultLib; + const outputFilePath = getDirectoryPath(normalizeSlashes((getOutputPathsFor(node, host, /*forceDtsPaths*/ true).declarationFilePath!))); + const referenceVisitor = mapReferencesIntoArray((bundle.syntheticFileReferences as FileReference[]), outputFilePath); + refs.forEach(referenceVisitor); + return bundle; + } + // Single source file + needsDeclare = true; + needsScopeFixMarker = false; + resultHasScopeMarker = false; + enclosingDeclaration = node; + currentSourceFile = node; + getSymbolAccessibilityDiagnostic = throwDiagnostic; + isBundledEmit = false; + resultHasExternalModuleIndicator = false; + suppressNewDiagnosticContexts = false; + lateMarkedStatements = undefined; + lateStatementReplacementMap = createMap(); + necessaryTypeReferences = undefined; + refs = collectReferences(currentSourceFile, createMap()); + libs = collectLibs(currentSourceFile, createMap()); + const references: FileReference[] = []; + const outputFilePath = getDirectoryPath(normalizeSlashes((getOutputPathsFor(node, host, /*forceDtsPaths*/ true).declarationFilePath!))); + const referenceVisitor = mapReferencesIntoArray(references, outputFilePath); + let combinedStatements: NodeArray; + if (isSourceFileJS(currentSourceFile)) { + combinedStatements = createNodeArray(transformDeclarationsForJS(node)); + refs.forEach(referenceVisitor); + emittedImports = filter(combinedStatements, isAnyImportSyntax); + } + else { + const statements = visitNodes(node.statements, visitDeclarationStatements); + combinedStatements = setTextRange(createNodeArray(transformAndReplaceLatePaintedStatements(statements)), node.statements); + refs.forEach(referenceVisitor); + emittedImports = filter(combinedStatements, isAnyImportSyntax); + if (isExternalModule(node) && (!resultHasExternalModuleIndicator || (needsScopeFixMarker && !resultHasScopeMarker))) { + combinedStatements = setTextRange(createNodeArray([...combinedStatements, createEmptyExports()]), combinedStatements); } - const updated = updateSourceFileNode(node, combinedStatements, /*isDeclarationFile*/ true, references, getFileReferencesForUsedTypeReferences(), node.hasNoDefaultLib, getLibReferences()); - updated.exportedModulesFromDeclarationEmit = exportedModulesFromDeclarationEmit; - return updated; - - function getLibReferences() { - return map(arrayFrom(libs.keys()), lib => ({ fileName: lib, pos: -1, end: -1 })); - } - - function getFileReferencesForUsedTypeReferences() { - return necessaryTypeReferences ? mapDefined(arrayFrom(necessaryTypeReferences.keys()), getFileReferenceForTypeName) : []; - } - - function getFileReferenceForTypeName(typeName: string): FileReference | undefined { - // Elide type references for which we have imports - if (emittedImports) { - for (const importStatement of emittedImports) { - if (isImportEqualsDeclaration(importStatement) && isExternalModuleReference(importStatement.moduleReference)) { - const expr = importStatement.moduleReference.expression; - if (isStringLiteralLike(expr) && expr.text === typeName) { - return undefined; - } - } - else if (isImportDeclaration(importStatement) && isStringLiteral(importStatement.moduleSpecifier) && importStatement.moduleSpecifier.text === typeName) { + } + const updated = updateSourceFileNode(node, combinedStatements, /*isDeclarationFile*/ true, references, getFileReferencesForUsedTypeReferences(), node.hasNoDefaultLib, getLibReferences()); + updated.exportedModulesFromDeclarationEmit = exportedModulesFromDeclarationEmit; + return updated; + function getLibReferences() { + return map(arrayFrom(libs.keys()), lib => ({ fileName: lib, pos: -1, end: -1 })); + } + function getFileReferencesForUsedTypeReferences() { + return necessaryTypeReferences ? mapDefined(arrayFrom(necessaryTypeReferences.keys()), getFileReferenceForTypeName) : []; + } + function getFileReferenceForTypeName(typeName: string): FileReference | undefined { + // Elide type references for which we have imports + if (emittedImports) { + for (const importStatement of emittedImports) { + if (isImportEqualsDeclaration(importStatement) && isExternalModuleReference(importStatement.moduleReference)) { + const expr = importStatement.moduleReference.expression; + if (isStringLiteralLike(expr) && expr.text === typeName) { return undefined; } } + else if (isImportDeclaration(importStatement) && isStringLiteral(importStatement.moduleSpecifier) && importStatement.moduleSpecifier.text === typeName) { + return undefined; + } } - return { fileName: typeName, pos: -1, end: -1 }; } - - function mapReferencesIntoArray(references: FileReference[], outputFilePath: string): (file: SourceFile) => void { - return file => { - let declFileName: string; - if (file.isDeclarationFile) { // Neither decl files or js should have their refs changed - declFileName = file.fileName; + return { fileName: typeName, pos: -1, end: -1 }; + } + function mapReferencesIntoArray(references: FileReference[], outputFilePath: string): (file: SourceFile) => void { + return file => { + let declFileName: string; + if (file.isDeclarationFile) { // Neither decl files or js should have their refs changed + declFileName = file.fileName; + } + else { + if (isBundledEmit && contains((node as Bundle).sourceFiles, file)) + return; // Omit references to files which are being merged + const paths = getOutputPathsFor(file, host, /*forceDtsPaths*/ true); + declFileName = paths.declarationFilePath || paths.jsFilePath || file.fileName; + } + if (declFileName) { + const specifier = getModuleSpecifier( + // We pathify the baseUrl since we pathify the other paths here, so we can still easily check if the other paths are within the baseUrl + // TODO: Should we _always_ be pathifying the baseUrl as we read it in? + { ...options, baseUrl: options.baseUrl && toPath(options.baseUrl, host.getCurrentDirectory(), host.getCanonicalFileName) }, currentSourceFile, toPath(outputFilePath, host.getCurrentDirectory(), host.getCanonicalFileName), toPath(declFileName, host.getCurrentDirectory(), host.getCanonicalFileName), host, host.getSourceFiles(), + /*preferences*/ undefined, host.redirectTargetsMap); + if (!pathIsRelative(specifier)) { + // If some compiler option/symlink/whatever allows access to the file containing the ambient module declaration + // via a non-relative name, emit a type reference directive to that non-relative name, rather than + // a relative path to the declaration file + recordTypeReferenceDirectivesIfNecessary([specifier]); + return; } - else { - if (isBundledEmit && contains((node as Bundle).sourceFiles, file)) return; // Omit references to files which are being merged - const paths = getOutputPathsFor(file, host, /*forceDtsPaths*/ true); - declFileName = paths.declarationFilePath || paths.jsFilePath || file.fileName; + let fileName = getRelativePathToDirectoryOrUrl(outputFilePath, declFileName, host.getCurrentDirectory(), host.getCanonicalFileName, + /*isAbsolutePathAnUrl*/ false); + if (startsWith(fileName, "./") && hasExtension(fileName)) { + fileName = fileName.substring(2); } - - if (declFileName) { - const specifier = moduleSpecifiers.getModuleSpecifier( - // We pathify the baseUrl since we pathify the other paths here, so we can still easily check if the other paths are within the baseUrl - // TODO: Should we _always_ be pathifying the baseUrl as we read it in? - { ...options, baseUrl: options.baseUrl && toPath(options.baseUrl, host.getCurrentDirectory(), host.getCanonicalFileName) }, - currentSourceFile, - toPath(outputFilePath, host.getCurrentDirectory(), host.getCanonicalFileName), - toPath(declFileName, host.getCurrentDirectory(), host.getCanonicalFileName), - host, - host.getSourceFiles(), - /*preferences*/ undefined, - host.redirectTargetsMap - ); - if (!pathIsRelative(specifier)) { - // If some compiler option/symlink/whatever allows access to the file containing the ambient module declaration - // via a non-relative name, emit a type reference directive to that non-relative name, rather than - // a relative path to the declaration file - recordTypeReferenceDirectivesIfNecessary([specifier]); - return; - } - - let fileName = getRelativePathToDirectoryOrUrl( - outputFilePath, - declFileName, - host.getCurrentDirectory(), - host.getCanonicalFileName, - /*isAbsolutePathAnUrl*/ false - ); - if (startsWith(fileName, "./") && hasExtension(fileName)) { - fileName = fileName.substring(2); - } - - // omit references to files from node_modules (npm may disambiguate module - // references when installing this package, making the path is unreliable). - if (startsWith(fileName, "node_modules/") || fileName.indexOf("/node_modules/") !== -1) { - return; - } - - references.push({ pos: -1, end: -1, fileName }); + // omit references to files from node_modules (npm may disambiguate module + // references when installing this package, making the path is unreliable). + if (startsWith(fileName, "node_modules/") || fileName.indexOf("/node_modules/") !== -1) { + return; } - }; - } - } - - function collectReferences(sourceFile: SourceFile | UnparsedSource, ret: Map) { - if (noResolve || (!isUnparsedSource(sourceFile) && isSourceFileJS(sourceFile))) return ret; - forEach(sourceFile.referencedFiles, f => { - const elem = host.getSourceFileFromReference(sourceFile, f); - if (elem) { - ret.set("" + getOriginalNodeId(elem), elem); + references.push({ pos: -1, end: -1, fileName }); } - }); - return ret; + }; } - - function collectLibs(sourceFile: SourceFile | UnparsedSource, ret: Map) { - forEach(sourceFile.libReferenceDirectives, ref => { - const lib = host.getLibFileFromReference(ref); - if (lib) { - ret.set(toFileNameLowerCase(ref.fileName), true); - } - }); + } + function collectReferences(sourceFile: SourceFile | UnparsedSource, ret: ts.Map) { + if (noResolve || (!isUnparsedSource(sourceFile) && isSourceFileJS(sourceFile))) return ret; + forEach(sourceFile.referencedFiles, f => { + const elem = host.getSourceFileFromReference(sourceFile, f); + if (elem) { + ret.set("" + getOriginalNodeId(elem), elem); + } + }); + return ret; + } + function collectLibs(sourceFile: SourceFile | UnparsedSource, ret: ts.Map) { + forEach(sourceFile.libReferenceDirectives, ref => { + const lib = host.getLibFileFromReference(ref); + if (lib) { + ret.set(toFileNameLowerCase(ref.fileName), true); + } + }); + return ret; + } + function filterBindingPatternInitializers(name: BindingName) { + if (name.kind === SyntaxKind.Identifier) { + return name; } - - function filterBindingPatternInitializers(name: BindingName) { - if (name.kind === SyntaxKind.Identifier) { - return name; + else { + if (name.kind === SyntaxKind.ArrayBindingPattern) { + return updateArrayBindingPattern(name, visitNodes(name.elements, visitBindingElement)); } else { - if (name.kind === SyntaxKind.ArrayBindingPattern) { - return updateArrayBindingPattern(name, visitNodes(name.elements, visitBindingElement)); - } - else { - return updateObjectBindingPattern(name, visitNodes(name.elements, visitBindingElement)); - } - } - - function visitBindingElement(elem: T): T; - function visitBindingElement(elem: ArrayBindingElement): ArrayBindingElement { - if (elem.kind === SyntaxKind.OmittedExpression) { - return elem; - } - return updateBindingElement(elem, elem.dotDotDotToken, elem.propertyName, filterBindingPatternInitializers(elem.name), shouldPrintWithInitializer(elem) ? elem.initializer : undefined); + return updateObjectBindingPattern(name, visitNodes(name.elements, visitBindingElement)); } } - - function ensureParameter(p: ParameterDeclaration, modifierMask?: ModifierFlags, type?: TypeNode): ParameterDeclaration { - let oldDiag: typeof getSymbolAccessibilityDiagnostic | undefined; - if (!suppressNewDiagnosticContexts) { - oldDiag = getSymbolAccessibilityDiagnostic; - getSymbolAccessibilityDiagnostic = createGetSymbolAccessibilityDiagnosticForNode(p); + function visitBindingElement(elem: T): T; + function visitBindingElement(elem: ArrayBindingElement): ArrayBindingElement { + if (elem.kind === SyntaxKind.OmittedExpression) { + return elem; } - const newParam = updateParameter( - p, - /*decorators*/ undefined, - maskModifiers(p, modifierMask), - p.dotDotDotToken, - filterBindingPatternInitializers(p.name), - resolver.isOptionalParameter(p) ? (p.questionToken || createToken(SyntaxKind.QuestionToken)) : undefined, - ensureType(p, type || p.type, /*ignorePrivate*/ true), // Ignore private param props, since this type is going straight back into a param - ensureNoInitializer(p) - ); + return updateBindingElement(elem, elem.dotDotDotToken, elem.propertyName, filterBindingPatternInitializers(elem.name), shouldPrintWithInitializer(elem) ? elem.initializer : undefined); + } + } + function ensureParameter(p: ParameterDeclaration, modifierMask?: ModifierFlags, type?: TypeNode): ParameterDeclaration { + let oldDiag: typeof getSymbolAccessibilityDiagnostic | undefined; + if (!suppressNewDiagnosticContexts) { + oldDiag = getSymbolAccessibilityDiagnostic; + getSymbolAccessibilityDiagnostic = createGetSymbolAccessibilityDiagnosticForNode(p); + } + const newParam = updateParameter(p, + /*decorators*/ undefined, maskModifiers(p, modifierMask), p.dotDotDotToken, filterBindingPatternInitializers(p.name), resolver.isOptionalParameter(p) ? (p.questionToken || createToken(SyntaxKind.QuestionToken)) : undefined, ensureType(p, type || p.type, /*ignorePrivate*/ true), // Ignore private param props, since this type is going straight back into a param + ensureNoInitializer(p)); + if (!suppressNewDiagnosticContexts) { + getSymbolAccessibilityDiagnostic = oldDiag!; + } + return newParam; + } + function shouldPrintWithInitializer(node: Node) { + return canHaveLiteralInitializer(node) && resolver.isLiteralConstDeclaration((getParseTreeNode(node) as CanHaveLiteralInitializer)); // TODO: Make safe + } + function ensureNoInitializer(node: CanHaveLiteralInitializer) { + if (shouldPrintWithInitializer(node)) { + return resolver.createLiteralConstValue((getParseTreeNode(node) as CanHaveLiteralInitializer), symbolTracker); // TODO: Make safe + } + return undefined; + } + type HasInferredType = FunctionDeclaration | MethodDeclaration | GetAccessorDeclaration | SetAccessorDeclaration | BindingElement | ConstructSignatureDeclaration | VariableDeclaration | MethodSignature | CallSignatureDeclaration | ParameterDeclaration | PropertyDeclaration | PropertySignature; + function ensureType(node: HasInferredType, type: TypeNode | undefined, ignorePrivate?: boolean): TypeNode | undefined { + if (!ignorePrivate && hasModifier(node, ModifierFlags.Private)) { + // Private nodes emit no types (except private parameter properties, whose parameter types are actually visible) + return; + } + if (shouldPrintWithInitializer(node)) { + // Literal const declarations will have an initializer ensured rather than a type + return; + } + const shouldUseResolverType = node.kind === SyntaxKind.Parameter && + (resolver.isRequiredInitializedParameter(node) || + resolver.isOptionalUninitializedParameterProperty(node)); + if (type && !shouldUseResolverType) { + return visitNode(type, visitDeclarationSubtree); + } + if (!getParseTreeNode(node)) { + return type ? visitNode(type, visitDeclarationSubtree) : createKeywordTypeNode(SyntaxKind.AnyKeyword); + } + if (node.kind === SyntaxKind.SetAccessor) { + // Set accessors with no associated type node (from it's param or get accessor return) are `any` since they are never contextually typed right now + // (The inferred type here will be void, but the old declaration emitter printed `any`, so this replicates that) + return createKeywordTypeNode(SyntaxKind.AnyKeyword); + } + errorNameNode = node.name; + let oldDiag: typeof getSymbolAccessibilityDiagnostic; + if (!suppressNewDiagnosticContexts) { + oldDiag = getSymbolAccessibilityDiagnostic; + getSymbolAccessibilityDiagnostic = createGetSymbolAccessibilityDiagnosticForNode(node); + } + if (node.kind === SyntaxKind.VariableDeclaration || node.kind === SyntaxKind.BindingElement) { + return cleanup(resolver.createTypeOfDeclaration(node, enclosingDeclaration, declarationEmitNodeBuilderFlags, symbolTracker)); + } + if (node.kind === SyntaxKind.Parameter + || node.kind === SyntaxKind.PropertyDeclaration + || node.kind === SyntaxKind.PropertySignature) { + if (!node.initializer) + return cleanup(resolver.createTypeOfDeclaration(node, enclosingDeclaration, declarationEmitNodeBuilderFlags, symbolTracker, shouldUseResolverType)); + return cleanup(resolver.createTypeOfDeclaration(node, enclosingDeclaration, declarationEmitNodeBuilderFlags, symbolTracker, shouldUseResolverType) || resolver.createTypeOfExpression(node.initializer, enclosingDeclaration, declarationEmitNodeBuilderFlags, symbolTracker)); + } + return cleanup(resolver.createReturnTypeOfSignatureDeclaration(node, enclosingDeclaration, declarationEmitNodeBuilderFlags, symbolTracker)); + function cleanup(returnValue: TypeNode | undefined) { + errorNameNode = undefined; if (!suppressNewDiagnosticContexts) { - getSymbolAccessibilityDiagnostic = oldDiag!; + getSymbolAccessibilityDiagnostic = oldDiag; } - return newParam; + return returnValue || createKeywordTypeNode(SyntaxKind.AnyKeyword); } - - function shouldPrintWithInitializer(node: Node) { - return canHaveLiteralInitializer(node) && resolver.isLiteralConstDeclaration(getParseTreeNode(node) as CanHaveLiteralInitializer); // TODO: Make safe + } + function isDeclarationAndNotVisible(node: NamedDeclaration) { + node = (getParseTreeNode(node) as NamedDeclaration); + switch (node.kind) { + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.EnumDeclaration: + return !resolver.isDeclarationVisible(node); + // The following should be doing their own visibility checks based on filtering their members + case SyntaxKind.VariableDeclaration: + return !getBindingNameVisible((node as VariableDeclaration)); + case SyntaxKind.ImportEqualsDeclaration: + case SyntaxKind.ImportDeclaration: + case SyntaxKind.ExportDeclaration: + case SyntaxKind.ExportAssignment: + return false; } - - function ensureNoInitializer(node: CanHaveLiteralInitializer) { - if (shouldPrintWithInitializer(node)) { - return resolver.createLiteralConstValue(getParseTreeNode(node) as CanHaveLiteralInitializer, symbolTracker); // TODO: Make safe - } - return undefined; - } - - type HasInferredType = - | FunctionDeclaration - | MethodDeclaration - | GetAccessorDeclaration - | SetAccessorDeclaration - | BindingElement - | ConstructSignatureDeclaration - | VariableDeclaration - | MethodSignature - | CallSignatureDeclaration - | ParameterDeclaration - | PropertyDeclaration - | PropertySignature; - - function ensureType(node: HasInferredType, type: TypeNode | undefined, ignorePrivate?: boolean): TypeNode | undefined { - if (!ignorePrivate && hasModifier(node, ModifierFlags.Private)) { - // Private nodes emit no types (except private parameter properties, whose parameter types are actually visible) - return; - } - if (shouldPrintWithInitializer(node)) { - // Literal const declarations will have an initializer ensured rather than a type - return; - } - const shouldUseResolverType = node.kind === SyntaxKind.Parameter && - (resolver.isRequiredInitializedParameter(node) || - resolver.isOptionalUninitializedParameterProperty(node)); - if (type && !shouldUseResolverType) { - return visitNode(type, visitDeclarationSubtree); - } - if (!getParseTreeNode(node)) { - return type ? visitNode(type, visitDeclarationSubtree) : createKeywordTypeNode(SyntaxKind.AnyKeyword); - } - if (node.kind === SyntaxKind.SetAccessor) { - // Set accessors with no associated type node (from it's param or get accessor return) are `any` since they are never contextually typed right now - // (The inferred type here will be void, but the old declaration emitter printed `any`, so this replicates that) - return createKeywordTypeNode(SyntaxKind.AnyKeyword); + return false; + } + function getBindingNameVisible(elem: BindingElement | VariableDeclaration | OmittedExpression): boolean { + if (isOmittedExpression(elem)) { + return false; + } + if (isBindingPattern(elem.name)) { + // If any child binding pattern element has been marked visible (usually by collect linked aliases), then this is visible + return some(elem.name.elements, getBindingNameVisible); + } + else { + return resolver.isDeclarationVisible(elem); + } + } + function updateParamsList(node: Node, params: NodeArray, modifierMask?: ModifierFlags) { + if (hasModifier(node, ModifierFlags.Private)) { + return undefined!; // TODO: GH#18217 + } + const newParams = map(params, p => ensureParameter(p, modifierMask)); + if (!newParams) { + return undefined!; // TODO: GH#18217 + } + return createNodeArray(newParams, params.hasTrailingComma); + } + function updateAccessorParamsList(input: AccessorDeclaration, isPrivate: boolean) { + let newParams: ParameterDeclaration[] | undefined; + if (!isPrivate) { + const thisParameter = getThisParameter(input); + if (thisParameter) { + newParams = [ensureParameter(thisParameter)]; } - errorNameNode = node.name; - let oldDiag: typeof getSymbolAccessibilityDiagnostic; - if (!suppressNewDiagnosticContexts) { - oldDiag = getSymbolAccessibilityDiagnostic; - getSymbolAccessibilityDiagnostic = createGetSymbolAccessibilityDiagnosticForNode(node); + } + if (isSetAccessorDeclaration(input)) { + let newValueParameter: ParameterDeclaration | undefined; + if (!isPrivate) { + const valueParameter = getSetAccessorValueParameter(input); + if (valueParameter) { + const accessorType = getTypeAnnotationFromAllAccessorDeclarations(input, resolver.getAllAccessorDeclarations(input)); + newValueParameter = ensureParameter(valueParameter, /*modifierMask*/ undefined, accessorType); + } } - if (node.kind === SyntaxKind.VariableDeclaration || node.kind === SyntaxKind.BindingElement) { - return cleanup(resolver.createTypeOfDeclaration(node, enclosingDeclaration, declarationEmitNodeBuilderFlags, symbolTracker)); + if (!newValueParameter) { + newValueParameter = createParameter( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, "value"); } - if (node.kind === SyntaxKind.Parameter - || node.kind === SyntaxKind.PropertyDeclaration - || node.kind === SyntaxKind.PropertySignature) { - if (!node.initializer) return cleanup(resolver.createTypeOfDeclaration(node, enclosingDeclaration, declarationEmitNodeBuilderFlags, symbolTracker, shouldUseResolverType)); - return cleanup(resolver.createTypeOfDeclaration(node, enclosingDeclaration, declarationEmitNodeBuilderFlags, symbolTracker, shouldUseResolverType) || resolver.createTypeOfExpression(node.initializer, enclosingDeclaration, declarationEmitNodeBuilderFlags, symbolTracker)); + newParams = append(newParams, newValueParameter); + } + return createNodeArray(newParams || emptyArray) as NodeArray; + } + function ensureTypeParams(node: Node, params: NodeArray | undefined) { + return hasModifier(node, ModifierFlags.Private) ? undefined : visitNodes(params, visitDeclarationSubtree); + } + function isEnclosingDeclaration(node: Node) { + return isSourceFile(node) + || isTypeAliasDeclaration(node) + || isModuleDeclaration(node) + || isClassDeclaration(node) + || isInterfaceDeclaration(node) + || isFunctionLike(node) + || isIndexSignatureDeclaration(node) + || isMappedTypeNode(node); + } + function checkEntityNameVisibility(entityName: EntityNameOrEntityNameExpression, enclosingDeclaration: Node) { + const visibilityResult = resolver.isEntityNameVisible(entityName, enclosingDeclaration); + handleSymbolAccessibilityError(visibilityResult); + recordTypeReferenceDirectivesIfNecessary(resolver.getTypeReferenceDirectivesForEntityName(entityName)); + } + function preserveJsDoc(updated: T, original: Node): T { + if (hasJSDocNodes(updated) && hasJSDocNodes(original)) { + updated.jsDoc = original.jsDoc; + } + return setCommentRange(updated, getCommentRange(original)); + } + function rewriteModuleSpecifier(parent: ImportEqualsDeclaration | ImportDeclaration | ExportDeclaration | ModuleDeclaration | ImportTypeNode, input: T | undefined): T | StringLiteral { + if (!input) + return undefined!; // TODO: GH#18217 + resultHasExternalModuleIndicator = resultHasExternalModuleIndicator || (parent.kind !== SyntaxKind.ModuleDeclaration && parent.kind !== SyntaxKind.ImportType); + if (isStringLiteralLike(input)) { + if (isBundledEmit) { + const newName = getExternalModuleNameFromDeclaration(context.getEmitHost(), resolver, parent); + if (newName) { + return createLiteral(newName); + } } - return cleanup(resolver.createReturnTypeOfSignatureDeclaration(node, enclosingDeclaration, declarationEmitNodeBuilderFlags, symbolTracker)); - - function cleanup(returnValue: TypeNode | undefined) { - errorNameNode = undefined; - if (!suppressNewDiagnosticContexts) { - getSymbolAccessibilityDiagnostic = oldDiag; + else { + const symbol = resolver.getSymbolOfExternalModuleSpecifier(input); + if (symbol) { + (exportedModulesFromDeclarationEmit || (exportedModulesFromDeclarationEmit = [])).push(symbol); } - return returnValue || createKeywordTypeNode(SyntaxKind.AnyKeyword); } } - - function isDeclarationAndNotVisible(node: NamedDeclaration) { - node = getParseTreeNode(node) as NamedDeclaration; - switch (node.kind) { - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.ModuleDeclaration: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.ClassDeclaration: - case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.EnumDeclaration: - return !resolver.isDeclarationVisible(node); - // The following should be doing their own visibility checks based on filtering their members - case SyntaxKind.VariableDeclaration: - return !getBindingNameVisible(node as VariableDeclaration); - case SyntaxKind.ImportEqualsDeclaration: - case SyntaxKind.ImportDeclaration: - case SyntaxKind.ExportDeclaration: - case SyntaxKind.ExportAssignment: - return false; - } - return false; + return input; + } + function transformImportEqualsDeclaration(decl: ImportEqualsDeclaration) { + if (!resolver.isDeclarationVisible(decl)) + return; + if (decl.moduleReference.kind === SyntaxKind.ExternalModuleReference) { + // Rewrite external module names if necessary + const specifier = getExternalModuleImportEqualsDeclarationExpression(decl); + return updateImportEqualsDeclaration(decl, + /*decorators*/ undefined, decl.modifiers, decl.name, updateExternalModuleReference(decl.moduleReference, rewriteModuleSpecifier(decl, specifier))); } - - function getBindingNameVisible(elem: BindingElement | VariableDeclaration | OmittedExpression): boolean { - if (isOmittedExpression(elem)) { - return false; - } - if (isBindingPattern(elem.name)) { - // If any child binding pattern element has been marked visible (usually by collect linked aliases), then this is visible - return some(elem.name.elements, getBindingNameVisible); - } - else { - return resolver.isDeclarationVisible(elem); + else { + const oldDiag = getSymbolAccessibilityDiagnostic; + getSymbolAccessibilityDiagnostic = createGetSymbolAccessibilityDiagnosticForNode(decl); + checkEntityNameVisibility(decl.moduleReference, enclosingDeclaration); + getSymbolAccessibilityDiagnostic = oldDiag; + return decl; + } + } + function transformImportDeclaration(decl: ImportDeclaration) { + if (!decl.importClause) { + // import "mod" - possibly needed for side effects? (global interface patches, module augmentations, etc) + return updateImportDeclaration(decl, + /*decorators*/ undefined, decl.modifiers, decl.importClause, rewriteModuleSpecifier(decl, decl.moduleSpecifier)); + } + // The `importClause` visibility corresponds to the default's visibility. + const visibleDefaultBinding = decl.importClause && decl.importClause.name && resolver.isDeclarationVisible(decl.importClause) ? decl.importClause.name : undefined; + if (!decl.importClause.namedBindings) { + // No named bindings (either namespace or list), meaning the import is just default or should be elided + return visibleDefaultBinding && updateImportDeclaration(decl, /*decorators*/ undefined, decl.modifiers, updateImportClause(decl.importClause, visibleDefaultBinding, + /*namedBindings*/ undefined, decl.importClause.isTypeOnly), rewriteModuleSpecifier(decl, decl.moduleSpecifier)); + } + if (decl.importClause.namedBindings.kind === SyntaxKind.NamespaceImport) { + // Namespace import (optionally with visible default) + const namedBindings = resolver.isDeclarationVisible(decl.importClause.namedBindings) ? decl.importClause.namedBindings : /*namedBindings*/ undefined; + return visibleDefaultBinding || namedBindings ? updateImportDeclaration(decl, /*decorators*/ undefined, decl.modifiers, updateImportClause(decl.importClause, visibleDefaultBinding, namedBindings, decl.importClause.isTypeOnly), rewriteModuleSpecifier(decl, decl.moduleSpecifier)) : undefined; + } + // Named imports (optionally with visible default) + const bindingList = mapDefined(decl.importClause.namedBindings.elements, b => resolver.isDeclarationVisible(b) ? b : undefined); + if ((bindingList && bindingList.length) || visibleDefaultBinding) { + return updateImportDeclaration(decl, + /*decorators*/ undefined, decl.modifiers, updateImportClause(decl.importClause, visibleDefaultBinding, bindingList && bindingList.length ? updateNamedImports(decl.importClause.namedBindings, bindingList) : undefined, decl.importClause.isTypeOnly), rewriteModuleSpecifier(decl, decl.moduleSpecifier)); + } + // Nothing visible + } + function transformAndReplaceLatePaintedStatements(statements: NodeArray): NodeArray { + // This is a `while` loop because `handleSymbolAccessibilityError` can see additional import aliases marked as visible during + // error handling which must now be included in the output and themselves checked for errors. + // For example: + // ``` + // module A { + // export module Q {} + // import B = Q; + // import C = B; + // export import D = C; + // } + // ``` + // In such a scenario, only Q and D are initially visible, but we don't consider imports as private names - instead we say they if they are referenced they must + // be recorded. So while checking D's visibility we mark C as visible, then we must check C which in turn marks B, completing the chain of + // dependent imports and allowing a valid declaration file output. Today, this dependent alias marking only happens for internal import aliases. + while (length(lateMarkedStatements)) { + const i = lateMarkedStatements!.shift()!; + if (!isLateVisibilityPaintedStatement(i)) { + return Debug.fail(`Late replaced statement was found which is not handled by the declaration transformer!: ${(ts as any).SyntaxKind ? (ts as any).SyntaxKind[(i as any).kind] : (i as any).kind}`); + } + const priorNeedsDeclare = needsDeclare; + needsDeclare = i.parent && isSourceFile(i.parent) && !(isExternalModule(i.parent) && isBundledEmit); + const result = transformTopLevelDeclaration(i); + needsDeclare = priorNeedsDeclare; + lateStatementReplacementMap.set("" + getOriginalNodeId(i), result); + } + // And lastly, we need to get the final form of all those indetermine import declarations from before and add them to the output list + // (and remove them from the set to examine for outter declarations) + return visitNodes(statements, visitLateVisibilityMarkedStatements); + function visitLateVisibilityMarkedStatements(statement: Statement) { + if (isLateVisibilityPaintedStatement(statement)) { + const key = "" + getOriginalNodeId(statement); + if (lateStatementReplacementMap.has(key)) { + const result = lateStatementReplacementMap.get(key); + lateStatementReplacementMap.delete(key); + if (result) { + if (isArray(result) ? some(result, needsScopeMarker) : needsScopeMarker(result)) { + // Top-level declarations in .d.ts files are always considered exported even without a modifier unless there's an export assignment or specifier + needsScopeFixMarker = true; + } + if (isSourceFile(statement.parent) && (isArray(result) ? some(result, isExternalModuleIndicator) : isExternalModuleIndicator(result))) { + resultHasExternalModuleIndicator = true; + } + } + return result; + } } + return statement; } - - function updateParamsList(node: Node, params: NodeArray, modifierMask?: ModifierFlags) { - if (hasModifier(node, ModifierFlags.Private)) { - return undefined!; // TODO: GH#18217 + } + function visitDeclarationSubtree(input: Node): VisitResult { + if (shouldStripInternal(input)) + return; + if (isDeclaration(input)) { + if (isDeclarationAndNotVisible(input)) + return; + if (hasDynamicName(input) && !resolver.isLateBound((getParseTreeNode(input) as Declaration))) { + return; } - const newParams = map(params, p => ensureParameter(p, modifierMask)); - if (!newParams) { - return undefined!; // TODO: GH#18217 + } + // Elide implementation signatures from overload sets + if (isFunctionLike(input) && resolver.isImplementationOfOverload(input)) + return; + // Elide semicolon class statements + if (isSemicolonClassElement(input)) + return; + let previousEnclosingDeclaration: typeof enclosingDeclaration; + if (isEnclosingDeclaration(input)) { + previousEnclosingDeclaration = enclosingDeclaration; + enclosingDeclaration = (input as Declaration); + } + const oldDiag = getSymbolAccessibilityDiagnostic; + // Setup diagnostic-related flags before first potential `cleanup` call, otherwise + // We'd see a TDZ violation at runtime + const canProduceDiagnostic = canProduceDiagnostics(input); + const oldWithinObjectLiteralType = suppressNewDiagnosticContexts; + let shouldEnterSuppressNewDiagnosticsContextContext = ((input.kind === SyntaxKind.TypeLiteral || input.kind === SyntaxKind.MappedType) && input.parent.kind !== SyntaxKind.TypeAliasDeclaration); + // Emit methods which are private as properties with no type information + if (isMethodDeclaration(input) || isMethodSignature(input)) { + if (hasModifier(input, ModifierFlags.Private)) { + if (input.symbol && input.symbol.declarations && input.symbol.declarations[0] !== input) + return; // Elide all but the first overload + return cleanup(createProperty(/*decorators*/ undefined, ensureModifiers(input), input.name, /*questionToken*/ undefined, /*type*/ undefined, /*initializer*/ undefined)); } - return createNodeArray(newParams, params.hasTrailingComma); } - - function updateAccessorParamsList(input: AccessorDeclaration, isPrivate: boolean) { - let newParams: ParameterDeclaration[] | undefined; - if (!isPrivate) { - const thisParameter = getThisParameter(input); - if (thisParameter) { - newParams = [ensureParameter(thisParameter)]; + if (canProduceDiagnostic && !suppressNewDiagnosticContexts) { + getSymbolAccessibilityDiagnostic = createGetSymbolAccessibilityDiagnosticForNode((input as DeclarationDiagnosticProducing)); + } + if (isTypeQueryNode(input)) { + checkEntityNameVisibility(input.exprName, enclosingDeclaration); + } + if (shouldEnterSuppressNewDiagnosticsContextContext) { + // We stop making new diagnostic contexts within object literal types. Unless it's an object type on the RHS of a type alias declaration. Then we do. + suppressNewDiagnosticContexts = true; + } + if (isProcessedComponent(input)) { + switch (input.kind) { + case SyntaxKind.ExpressionWithTypeArguments: { + if ((isEntityName(input.expression) || isEntityNameExpression(input.expression))) { + checkEntityNameVisibility(input.expression, enclosingDeclaration); + } + const node = visitEachChild(input, visitDeclarationSubtree, context); + return cleanup(updateExpressionWithTypeArguments(node, parenthesizeTypeParameters(node.typeArguments), node.expression)); } - } - if (isSetAccessorDeclaration(input)) { - let newValueParameter: ParameterDeclaration | undefined; - if (!isPrivate) { - const valueParameter = getSetAccessorValueParameter(input); - if (valueParameter) { - const accessorType = getTypeAnnotationFromAllAccessorDeclarations(input, resolver.getAllAccessorDeclarations(input)); - newValueParameter = ensureParameter(valueParameter, /*modifierMask*/ undefined, accessorType); + case SyntaxKind.TypeReference: { + checkEntityNameVisibility(input.typeName, enclosingDeclaration); + const node = visitEachChild(input, visitDeclarationSubtree, context); + return cleanup(updateTypeReferenceNode(node, node.typeName, parenthesizeTypeParameters(node.typeArguments))); + } + case SyntaxKind.ConstructSignature: + return cleanup(updateConstructSignature(input, ensureTypeParams(input, input.typeParameters), updateParamsList(input, input.parameters), ensureType(input, input.type))); + case SyntaxKind.Constructor: { + // A constructor declaration may not have a type annotation + const ctor = createSignatureDeclaration(SyntaxKind.Constructor, ensureTypeParams(input, input.typeParameters), updateParamsList(input, input.parameters, ModifierFlags.None), + /*type*/ undefined); + ctor.modifiers = createNodeArray(ensureModifiers(input)); + return cleanup(ctor); + } + case SyntaxKind.MethodDeclaration: { + if (isPrivateIdentifier(input.name)) { + return cleanup(/*returnValue*/ undefined); } + const sig = (createSignatureDeclaration(SyntaxKind.MethodSignature, ensureTypeParams(input, input.typeParameters), updateParamsList(input, input.parameters), ensureType(input, input.type)) as MethodSignature); + sig.name = input.name; + sig.modifiers = createNodeArray(ensureModifiers(input)); + sig.questionToken = input.questionToken; + return cleanup(sig); } - if (!newValueParameter) { - newValueParameter = createParameter( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, - "value" - ); + case SyntaxKind.GetAccessor: { + if (isPrivateIdentifier(input.name)) { + return cleanup(/*returnValue*/ undefined); + } + const accessorType = getTypeAnnotationFromAllAccessorDeclarations(input, resolver.getAllAccessorDeclarations(input)); + return cleanup(updateGetAccessor(input, + /*decorators*/ undefined, ensureModifiers(input), input.name, updateAccessorParamsList(input, hasModifier(input, ModifierFlags.Private)), ensureType(input, accessorType), + /*body*/ undefined)); } - newParams = append(newParams, newValueParameter); - } - return createNodeArray(newParams || emptyArray) as NodeArray; - } - - function ensureTypeParams(node: Node, params: NodeArray | undefined) { - return hasModifier(node, ModifierFlags.Private) ? undefined : visitNodes(params, visitDeclarationSubtree); - } - - function isEnclosingDeclaration(node: Node) { - return isSourceFile(node) - || isTypeAliasDeclaration(node) - || isModuleDeclaration(node) - || isClassDeclaration(node) - || isInterfaceDeclaration(node) - || isFunctionLike(node) - || isIndexSignatureDeclaration(node) - || isMappedTypeNode(node); - } - - function checkEntityNameVisibility(entityName: EntityNameOrEntityNameExpression, enclosingDeclaration: Node) { - const visibilityResult = resolver.isEntityNameVisible(entityName, enclosingDeclaration); - handleSymbolAccessibilityError(visibilityResult); - recordTypeReferenceDirectivesIfNecessary(resolver.getTypeReferenceDirectivesForEntityName(entityName)); - } - - function preserveJsDoc(updated: T, original: Node): T { - if (hasJSDocNodes(updated) && hasJSDocNodes(original)) { - updated.jsDoc = original.jsDoc; - } - return setCommentRange(updated, getCommentRange(original)); - } - - function rewriteModuleSpecifier(parent: ImportEqualsDeclaration | ImportDeclaration | ExportDeclaration | ModuleDeclaration | ImportTypeNode, input: T | undefined): T | StringLiteral { - if (!input) return undefined!; // TODO: GH#18217 - resultHasExternalModuleIndicator = resultHasExternalModuleIndicator || (parent.kind !== SyntaxKind.ModuleDeclaration && parent.kind !== SyntaxKind.ImportType); - if (isStringLiteralLike(input)) { - if (isBundledEmit) { - const newName = getExternalModuleNameFromDeclaration(context.getEmitHost(), resolver, parent); - if (newName) { - return createLiteral(newName); + case SyntaxKind.SetAccessor: { + if (isPrivateIdentifier(input.name)) { + return cleanup(/*returnValue*/ undefined); } + return cleanup(updateSetAccessor(input, + /*decorators*/ undefined, ensureModifiers(input), input.name, updateAccessorParamsList(input, hasModifier(input, ModifierFlags.Private)), + /*body*/ undefined)); } - else { - const symbol = resolver.getSymbolOfExternalModuleSpecifier(input); - if (symbol) { - (exportedModulesFromDeclarationEmit || (exportedModulesFromDeclarationEmit = [])).push(symbol); + case SyntaxKind.PropertyDeclaration: + if (isPrivateIdentifier(input.name)) { + return cleanup(/*returnValue*/ undefined); + } + return cleanup(updateProperty(input, + /*decorators*/ undefined, ensureModifiers(input), input.name, input.questionToken, ensureType(input, input.type), ensureNoInitializer(input))); + case SyntaxKind.PropertySignature: + if (isPrivateIdentifier(input.name)) { + return cleanup(/*returnValue*/ undefined); } + return cleanup(updatePropertySignature(input, ensureModifiers(input), input.name, input.questionToken, ensureType(input, input.type), ensureNoInitializer(input))); + case SyntaxKind.MethodSignature: { + if (isPrivateIdentifier(input.name)) { + return cleanup(/*returnValue*/ undefined); + } + return cleanup(updateMethodSignature(input, ensureTypeParams(input, input.typeParameters), updateParamsList(input, input.parameters), ensureType(input, input.type), input.name, input.questionToken)); } - } - return input; - } - - function transformImportEqualsDeclaration(decl: ImportEqualsDeclaration) { - if (!resolver.isDeclarationVisible(decl)) return; - if (decl.moduleReference.kind === SyntaxKind.ExternalModuleReference) { - // Rewrite external module names if necessary - const specifier = getExternalModuleImportEqualsDeclarationExpression(decl); - return updateImportEqualsDeclaration( - decl, - /*decorators*/ undefined, - decl.modifiers, - decl.name, - updateExternalModuleReference(decl.moduleReference, rewriteModuleSpecifier(decl, specifier)) - ); - } - else { - const oldDiag = getSymbolAccessibilityDiagnostic; - getSymbolAccessibilityDiagnostic = createGetSymbolAccessibilityDiagnosticForNode(decl); - checkEntityNameVisibility(decl.moduleReference, enclosingDeclaration); - getSymbolAccessibilityDiagnostic = oldDiag; - return decl; - } - } - - function transformImportDeclaration(decl: ImportDeclaration) { - if (!decl.importClause) { - // import "mod" - possibly needed for side effects? (global interface patches, module augmentations, etc) - return updateImportDeclaration( - decl, - /*decorators*/ undefined, - decl.modifiers, - decl.importClause, - rewriteModuleSpecifier(decl, decl.moduleSpecifier) - ); - } - // The `importClause` visibility corresponds to the default's visibility. - const visibleDefaultBinding = decl.importClause && decl.importClause.name && resolver.isDeclarationVisible(decl.importClause) ? decl.importClause.name : undefined; - if (!decl.importClause.namedBindings) { - // No named bindings (either namespace or list), meaning the import is just default or should be elided - return visibleDefaultBinding && updateImportDeclaration(decl, /*decorators*/ undefined, decl.modifiers, updateImportClause( - decl.importClause, - visibleDefaultBinding, - /*namedBindings*/ undefined, - decl.importClause.isTypeOnly, - ), rewriteModuleSpecifier(decl, decl.moduleSpecifier)); - } - if (decl.importClause.namedBindings.kind === SyntaxKind.NamespaceImport) { - // Namespace import (optionally with visible default) - const namedBindings = resolver.isDeclarationVisible(decl.importClause.namedBindings) ? decl.importClause.namedBindings : /*namedBindings*/ undefined; - return visibleDefaultBinding || namedBindings ? updateImportDeclaration(decl, /*decorators*/ undefined, decl.modifiers, updateImportClause( - decl.importClause, - visibleDefaultBinding, - namedBindings, - decl.importClause.isTypeOnly, - ), rewriteModuleSpecifier(decl, decl.moduleSpecifier)) : undefined; - } - // Named imports (optionally with visible default) - const bindingList = mapDefined(decl.importClause.namedBindings.elements, b => resolver.isDeclarationVisible(b) ? b : undefined); - if ((bindingList && bindingList.length) || visibleDefaultBinding) { - return updateImportDeclaration( - decl, - /*decorators*/ undefined, - decl.modifiers, - updateImportClause( - decl.importClause, - visibleDefaultBinding, - bindingList && bindingList.length ? updateNamedImports(decl.importClause.namedBindings, bindingList) : undefined, - decl.importClause.isTypeOnly, - ), - rewriteModuleSpecifier(decl, decl.moduleSpecifier) - ); - } - // Nothing visible - } - - function transformAndReplaceLatePaintedStatements(statements: NodeArray): NodeArray { - // This is a `while` loop because `handleSymbolAccessibilityError` can see additional import aliases marked as visible during - // error handling which must now be included in the output and themselves checked for errors. - // For example: - // ``` - // module A { - // export module Q {} - // import B = Q; - // import C = B; - // export import D = C; - // } - // ``` - // In such a scenario, only Q and D are initially visible, but we don't consider imports as private names - instead we say they if they are referenced they must - // be recorded. So while checking D's visibility we mark C as visible, then we must check C which in turn marks B, completing the chain of - // dependent imports and allowing a valid declaration file output. Today, this dependent alias marking only happens for internal import aliases. - while (length(lateMarkedStatements)) { - const i = lateMarkedStatements!.shift()!; - if (!isLateVisibilityPaintedStatement(i)) { - return Debug.fail(`Late replaced statement was found which is not handled by the declaration transformer!: ${(ts as any).SyntaxKind ? (ts as any).SyntaxKind[(i as any).kind] : (i as any).kind}`); + case SyntaxKind.CallSignature: { + return cleanup(updateCallSignature(input, ensureTypeParams(input, input.typeParameters), updateParamsList(input, input.parameters), ensureType(input, input.type))); } - const priorNeedsDeclare = needsDeclare; - needsDeclare = i.parent && isSourceFile(i.parent) && !(isExternalModule(i.parent) && isBundledEmit); - const result = transformTopLevelDeclaration(i); - needsDeclare = priorNeedsDeclare; - lateStatementReplacementMap.set("" + getOriginalNodeId(i), result); - } - - // And lastly, we need to get the final form of all those indetermine import declarations from before and add them to the output list - // (and remove them from the set to examine for outter declarations) - return visitNodes(statements, visitLateVisibilityMarkedStatements); - - function visitLateVisibilityMarkedStatements(statement: Statement) { - if (isLateVisibilityPaintedStatement(statement)) { - const key = "" + getOriginalNodeId(statement); - if (lateStatementReplacementMap.has(key)) { - const result = lateStatementReplacementMap.get(key); - lateStatementReplacementMap.delete(key); - if (result) { - if (isArray(result) ? some(result, needsScopeMarker) : needsScopeMarker(result)) { - // Top-level declarations in .d.ts files are always considered exported even without a modifier unless there's an export assignment or specifier - needsScopeFixMarker = true; - } - if (isSourceFile(statement.parent) && (isArray(result) ? some(result, isExternalModuleIndicator) : isExternalModuleIndicator(result))) { - resultHasExternalModuleIndicator = true; - } - } - return result; + case SyntaxKind.IndexSignature: { + return cleanup(updateIndexSignature(input, + /*decorators*/ undefined, ensureModifiers(input), updateParamsList(input, input.parameters), visitNode(input.type, visitDeclarationSubtree) || createKeywordTypeNode(SyntaxKind.AnyKeyword))); + } + case SyntaxKind.VariableDeclaration: { + if (isBindingPattern(input.name)) { + return recreateBindingPattern(input.name); + } + shouldEnterSuppressNewDiagnosticsContextContext = true; + suppressNewDiagnosticContexts = true; // Variable declaration types also suppress new diagnostic contexts, provided the contexts wouldn't be made for binding pattern types + return cleanup(updateTypeScriptVariableDeclaration(input, input.name, /*exclaimationToken*/ undefined, ensureType(input, input.type), ensureNoInitializer(input))); + } + case SyntaxKind.TypeParameter: { + if (isPrivateMethodTypeParameter(input) && (input.default || input.constraint)) { + return cleanup(updateTypeParameterDeclaration(input, input.name, /*constraint*/ undefined, /*defaultType*/ undefined)); } + return cleanup(visitEachChild(input, visitDeclarationSubtree, context)); } - return statement; + case SyntaxKind.ConditionalType: { + // We have to process conditional types in a special way because for visibility purposes we need to push a new enclosingDeclaration + // just for the `infer` types in the true branch. It's an implicit declaration scope that only applies to _part_ of the type. + const checkType = visitNode(input.checkType, visitDeclarationSubtree); + const extendsType = visitNode(input.extendsType, visitDeclarationSubtree); + const oldEnclosingDecl = enclosingDeclaration; + enclosingDeclaration = input.trueType; + const trueType = visitNode(input.trueType, visitDeclarationSubtree); + enclosingDeclaration = oldEnclosingDecl; + const falseType = visitNode(input.falseType, visitDeclarationSubtree); + return cleanup(updateConditionalTypeNode(input, checkType, extendsType, trueType, falseType)); + } + case SyntaxKind.FunctionType: { + return cleanup(updateFunctionTypeNode(input, visitNodes(input.typeParameters, visitDeclarationSubtree), updateParamsList(input, input.parameters), visitNode(input.type, visitDeclarationSubtree))); + } + case SyntaxKind.ConstructorType: { + return cleanup(updateConstructorTypeNode(input, visitNodes(input.typeParameters, visitDeclarationSubtree), updateParamsList(input, input.parameters), visitNode(input.type, visitDeclarationSubtree))); + } + case SyntaxKind.ImportType: { + if (!isLiteralImportTypeNode(input)) + return cleanup(input); + return cleanup(updateImportTypeNode(input, updateLiteralTypeNode(input.argument, rewriteModuleSpecifier(input, input.argument.literal)), input.qualifier, visitNodes(input.typeArguments, visitDeclarationSubtree, isTypeNode), input.isTypeOf)); + } + default: Debug.assertNever(input, `Attempted to process unhandled node kind: ${(ts as any).SyntaxKind[(input as any).kind]}`); } } - - function visitDeclarationSubtree(input: Node): VisitResult { - if (shouldStripInternal(input)) return; - if (isDeclaration(input)) { - if (isDeclarationAndNotVisible(input)) return; - if (hasDynamicName(input) && !resolver.isLateBound(getParseTreeNode(input) as Declaration)) { - return; - } + return cleanup(visitEachChild(input, visitDeclarationSubtree, context)); + function cleanup(returnValue: T | undefined): T | undefined { + if (returnValue && canProduceDiagnostic && hasDynamicName((input as Declaration))) { + checkName((input as DeclarationDiagnosticProducing)); } - - // Elide implementation signatures from overload sets - if (isFunctionLike(input) && resolver.isImplementationOfOverload(input)) return; - - // Elide semicolon class statements - if (isSemicolonClassElement(input)) return; - - let previousEnclosingDeclaration: typeof enclosingDeclaration; if (isEnclosingDeclaration(input)) { - previousEnclosingDeclaration = enclosingDeclaration; - enclosingDeclaration = input as Declaration; - } - const oldDiag = getSymbolAccessibilityDiagnostic; - - // Setup diagnostic-related flags before first potential `cleanup` call, otherwise - // We'd see a TDZ violation at runtime - const canProduceDiagnostic = canProduceDiagnostics(input); - const oldWithinObjectLiteralType = suppressNewDiagnosticContexts; - let shouldEnterSuppressNewDiagnosticsContextContext = ((input.kind === SyntaxKind.TypeLiteral || input.kind === SyntaxKind.MappedType) && input.parent.kind !== SyntaxKind.TypeAliasDeclaration); - - // Emit methods which are private as properties with no type information - if (isMethodDeclaration(input) || isMethodSignature(input)) { - if (hasModifier(input, ModifierFlags.Private)) { - if (input.symbol && input.symbol.declarations && input.symbol.declarations[0] !== input) return; // Elide all but the first overload - return cleanup(createProperty(/*decorators*/undefined, ensureModifiers(input), input.name, /*questionToken*/ undefined, /*type*/ undefined, /*initializer*/ undefined)); - } + enclosingDeclaration = previousEnclosingDeclaration; } - if (canProduceDiagnostic && !suppressNewDiagnosticContexts) { - getSymbolAccessibilityDiagnostic = createGetSymbolAccessibilityDiagnosticForNode(input as DeclarationDiagnosticProducing); - } - - if (isTypeQueryNode(input)) { - checkEntityNameVisibility(input.exprName, enclosingDeclaration); + getSymbolAccessibilityDiagnostic = oldDiag; } - if (shouldEnterSuppressNewDiagnosticsContextContext) { - // We stop making new diagnostic contexts within object literal types. Unless it's an object type on the RHS of a type alias declaration. Then we do. - suppressNewDiagnosticContexts = true; + suppressNewDiagnosticContexts = oldWithinObjectLiteralType; } - - if (isProcessedComponent(input)) { - switch (input.kind) { - case SyntaxKind.ExpressionWithTypeArguments: { - if ((isEntityName(input.expression) || isEntityNameExpression(input.expression))) { - checkEntityNameVisibility(input.expression, enclosingDeclaration); - } - const node = visitEachChild(input, visitDeclarationSubtree, context); - return cleanup(updateExpressionWithTypeArguments(node, parenthesizeTypeParameters(node.typeArguments), node.expression)); - } - case SyntaxKind.TypeReference: { - checkEntityNameVisibility(input.typeName, enclosingDeclaration); - const node = visitEachChild(input, visitDeclarationSubtree, context); - return cleanup(updateTypeReferenceNode(node, node.typeName, parenthesizeTypeParameters(node.typeArguments))); - } - case SyntaxKind.ConstructSignature: - return cleanup(updateConstructSignature( - input, - ensureTypeParams(input, input.typeParameters), - updateParamsList(input, input.parameters), - ensureType(input, input.type) - )); - case SyntaxKind.Constructor: { - // A constructor declaration may not have a type annotation - const ctor = createSignatureDeclaration( - SyntaxKind.Constructor, - ensureTypeParams(input, input.typeParameters), - updateParamsList(input, input.parameters, ModifierFlags.None), - /*type*/ undefined - ); - ctor.modifiers = createNodeArray(ensureModifiers(input)); - return cleanup(ctor); - } - case SyntaxKind.MethodDeclaration: { - if (isPrivateIdentifier(input.name)) { - return cleanup(/*returnValue*/ undefined); - } - const sig = createSignatureDeclaration( - SyntaxKind.MethodSignature, - ensureTypeParams(input, input.typeParameters), - updateParamsList(input, input.parameters), - ensureType(input, input.type) - ) as MethodSignature; - sig.name = input.name; - sig.modifiers = createNodeArray(ensureModifiers(input)); - sig.questionToken = input.questionToken; - return cleanup(sig); - } - case SyntaxKind.GetAccessor: { - if (isPrivateIdentifier(input.name)) { - return cleanup(/*returnValue*/ undefined); - } - const accessorType = getTypeAnnotationFromAllAccessorDeclarations(input, resolver.getAllAccessorDeclarations(input)); - return cleanup(updateGetAccessor( - input, - /*decorators*/ undefined, - ensureModifiers(input), - input.name, - updateAccessorParamsList(input, hasModifier(input, ModifierFlags.Private)), - ensureType(input, accessorType), - /*body*/ undefined)); - } - case SyntaxKind.SetAccessor: { - if (isPrivateIdentifier(input.name)) { - return cleanup(/*returnValue*/ undefined); - } - return cleanup(updateSetAccessor( - input, - /*decorators*/ undefined, - ensureModifiers(input), - input.name, - updateAccessorParamsList(input, hasModifier(input, ModifierFlags.Private)), - /*body*/ undefined)); - } - case SyntaxKind.PropertyDeclaration: - if (isPrivateIdentifier(input.name)) { - return cleanup(/*returnValue*/ undefined); - } - return cleanup(updateProperty( - input, - /*decorators*/ undefined, - ensureModifiers(input), - input.name, - input.questionToken, - ensureType(input, input.type), - ensureNoInitializer(input) - )); - case SyntaxKind.PropertySignature: - if (isPrivateIdentifier(input.name)) { - return cleanup(/*returnValue*/ undefined); - } - return cleanup(updatePropertySignature( - input, - ensureModifiers(input), - input.name, - input.questionToken, - ensureType(input, input.type), - ensureNoInitializer(input) - )); - case SyntaxKind.MethodSignature: { - if (isPrivateIdentifier(input.name)) { - return cleanup(/*returnValue*/ undefined); - } - return cleanup(updateMethodSignature( - input, - ensureTypeParams(input, input.typeParameters), - updateParamsList(input, input.parameters), - ensureType(input, input.type), - input.name, - input.questionToken - )); - } - case SyntaxKind.CallSignature: { - return cleanup(updateCallSignature( - input, - ensureTypeParams(input, input.typeParameters), - updateParamsList(input, input.parameters), - ensureType(input, input.type) - )); - } - case SyntaxKind.IndexSignature: { - return cleanup(updateIndexSignature( - input, - /*decorators*/ undefined, - ensureModifiers(input), - updateParamsList(input, input.parameters), - visitNode(input.type, visitDeclarationSubtree) || createKeywordTypeNode(SyntaxKind.AnyKeyword) - )); - } - case SyntaxKind.VariableDeclaration: { - if (isBindingPattern(input.name)) { - return recreateBindingPattern(input.name); - } - shouldEnterSuppressNewDiagnosticsContextContext = true; - suppressNewDiagnosticContexts = true; // Variable declaration types also suppress new diagnostic contexts, provided the contexts wouldn't be made for binding pattern types - return cleanup(updateTypeScriptVariableDeclaration(input, input.name, /*exclaimationToken*/ undefined, ensureType(input, input.type), ensureNoInitializer(input))); - } - case SyntaxKind.TypeParameter: { - if (isPrivateMethodTypeParameter(input) && (input.default || input.constraint)) { - return cleanup(updateTypeParameterDeclaration(input, input.name, /*constraint*/ undefined, /*defaultType*/ undefined)); - } - return cleanup(visitEachChild(input, visitDeclarationSubtree, context)); - } - case SyntaxKind.ConditionalType: { - // We have to process conditional types in a special way because for visibility purposes we need to push a new enclosingDeclaration - // just for the `infer` types in the true branch. It's an implicit declaration scope that only applies to _part_ of the type. - const checkType = visitNode(input.checkType, visitDeclarationSubtree); - const extendsType = visitNode(input.extendsType, visitDeclarationSubtree); - const oldEnclosingDecl = enclosingDeclaration; - enclosingDeclaration = input.trueType; - const trueType = visitNode(input.trueType, visitDeclarationSubtree); - enclosingDeclaration = oldEnclosingDecl; - const falseType = visitNode(input.falseType, visitDeclarationSubtree); - return cleanup(updateConditionalTypeNode(input, checkType, extendsType, trueType, falseType)); - } - case SyntaxKind.FunctionType: { - return cleanup(updateFunctionTypeNode(input, visitNodes(input.typeParameters, visitDeclarationSubtree), updateParamsList(input, input.parameters), visitNode(input.type, visitDeclarationSubtree))); - } - case SyntaxKind.ConstructorType: { - return cleanup(updateConstructorTypeNode(input, visitNodes(input.typeParameters, visitDeclarationSubtree), updateParamsList(input, input.parameters), visitNode(input.type, visitDeclarationSubtree))); - } - case SyntaxKind.ImportType: { - if (!isLiteralImportTypeNode(input)) return cleanup(input); - return cleanup(updateImportTypeNode( - input, - updateLiteralTypeNode(input.argument, rewriteModuleSpecifier(input, input.argument.literal)), - input.qualifier, - visitNodes(input.typeArguments, visitDeclarationSubtree, isTypeNode), - input.isTypeOf - )); - } - default: Debug.assertNever(input, `Attempted to process unhandled node kind: ${(ts as any).SyntaxKind[(input as any).kind]}`); - } + if (returnValue === input) { + return returnValue; } - - return cleanup(visitEachChild(input, visitDeclarationSubtree, context)); - - function cleanup(returnValue: T | undefined): T | undefined { - if (returnValue && canProduceDiagnostic && hasDynamicName(input as Declaration)) { - checkName(input as DeclarationDiagnosticProducing); - } - if (isEnclosingDeclaration(input)) { - enclosingDeclaration = previousEnclosingDeclaration; + return returnValue && setOriginalNode(preserveJsDoc(returnValue, input), input); + } + } + function isPrivateMethodTypeParameter(node: TypeParameterDeclaration) { + return node.parent.kind === SyntaxKind.MethodDeclaration && hasModifier(node.parent, ModifierFlags.Private); + } + function visitDeclarationStatements(input: Node): VisitResult { + if (!isPreservedDeclarationStatement(input)) { + // return undefined for unmatched kinds to omit them from the tree + return; + } + if (shouldStripInternal(input)) + return; + switch (input.kind) { + case SyntaxKind.ExportDeclaration: { + if (isSourceFile(input.parent)) { + resultHasExternalModuleIndicator = true; } - if (canProduceDiagnostic && !suppressNewDiagnosticContexts) { - getSymbolAccessibilityDiagnostic = oldDiag; + resultHasScopeMarker = true; + // Always visible if the parent node isn't dropped for being not visible + // Rewrite external module names if necessary + return updateExportDeclaration(input, + /*decorators*/ undefined, input.modifiers, input.exportClause, rewriteModuleSpecifier(input, input.moduleSpecifier), input.isTypeOnly); + } + case SyntaxKind.ExportAssignment: { + // Always visible if the parent node isn't dropped for being not visible + if (isSourceFile(input.parent)) { + resultHasExternalModuleIndicator = true; } - if (shouldEnterSuppressNewDiagnosticsContextContext) { - suppressNewDiagnosticContexts = oldWithinObjectLiteralType; + resultHasScopeMarker = true; + if (input.expression.kind === SyntaxKind.Identifier) { + return input; } - if (returnValue === input) { - return returnValue; + else { + const newId = createOptimisticUniqueName("_default"); + getSymbolAccessibilityDiagnostic = () => ({ + diagnosticMessage: Diagnostics.Default_export_of_the_module_has_or_is_using_private_name_0, + errorNode: input + }); + const varDecl = createVariableDeclaration(newId, resolver.createTypeOfExpression(input.expression, input, declarationEmitNodeBuilderFlags, symbolTracker), /*initializer*/ undefined); + const statement = createVariableStatement(needsDeclare ? [createModifier(SyntaxKind.DeclareKeyword)] : [], createVariableDeclarationList([varDecl], NodeFlags.Const)); + return [statement, updateExportAssignment(input, input.decorators, input.modifiers, newId)]; } - return returnValue && setOriginalNode(preserveJsDoc(returnValue, input), input); } } - - function isPrivateMethodTypeParameter(node: TypeParameterDeclaration) { - return node.parent.kind === SyntaxKind.MethodDeclaration && hasModifier(node.parent, ModifierFlags.Private); + const result = transformTopLevelDeclaration(input); + // Don't actually transform yet; just leave as original node - will be elided/swapped by late pass + lateStatementReplacementMap.set("" + getOriginalNodeId(input), result); + return input; + } + function stripExportModifiers(statement: Statement): Statement { + if (isImportEqualsDeclaration(statement) || hasModifier(statement, ModifierFlags.Default)) { + // `export import` statements should remain as-is, as imports are _not_ implicitly exported in an ambient namespace + // Likewise, `export default` classes and the like and just be `default`, so we preserve their `export` modifiers, too + return statement; } - - function visitDeclarationStatements(input: Node): VisitResult { - if (!isPreservedDeclarationStatement(input)) { - // return undefined for unmatched kinds to omit them from the tree - return; + const clone = getMutableClone(statement); + const modifiers = createModifiersFromModifierFlags(getModifierFlags(statement) & (ModifierFlags.All ^ ModifierFlags.Export)); + clone.modifiers = modifiers.length ? createNodeArray(modifiers) : undefined; + return clone; + } + function transformTopLevelDeclaration(input: LateVisibilityPaintedStatement) { + if (shouldStripInternal(input)) + return; + switch (input.kind) { + case SyntaxKind.ImportEqualsDeclaration: { + return transformImportEqualsDeclaration(input); } - if (shouldStripInternal(input)) return; - - switch (input.kind) { - case SyntaxKind.ExportDeclaration: { - if (isSourceFile(input.parent)) { - resultHasExternalModuleIndicator = true; + case SyntaxKind.ImportDeclaration: { + return transformImportDeclaration(input); + } + } + if (isDeclaration(input) && isDeclarationAndNotVisible(input)) + return; + // Elide implementation signatures from overload sets + if (isFunctionLike(input) && resolver.isImplementationOfOverload(input)) + return; + let previousEnclosingDeclaration: typeof enclosingDeclaration; + if (isEnclosingDeclaration(input)) { + previousEnclosingDeclaration = enclosingDeclaration; + enclosingDeclaration = (input as Declaration); + } + const canProdiceDiagnostic = canProduceDiagnostics(input); + const oldDiag = getSymbolAccessibilityDiagnostic; + if (canProdiceDiagnostic) { + getSymbolAccessibilityDiagnostic = createGetSymbolAccessibilityDiagnosticForNode((input as DeclarationDiagnosticProducing)); + } + const previousNeedsDeclare = needsDeclare; + switch (input.kind) { + case SyntaxKind.TypeAliasDeclaration: // Type aliases get `declare`d if need be (for legacy support), but that's all + return cleanup(updateTypeAliasDeclaration(input, + /*decorators*/ undefined, ensureModifiers(input), input.name, visitNodes(input.typeParameters, visitDeclarationSubtree, isTypeParameterDeclaration), visitNode(input.type, visitDeclarationSubtree, isTypeNode))); + case SyntaxKind.InterfaceDeclaration: { + return cleanup(updateInterfaceDeclaration(input, + /*decorators*/ undefined, ensureModifiers(input), input.name, ensureTypeParams(input, input.typeParameters), transformHeritageClauses(input.heritageClauses), visitNodes(input.members, visitDeclarationSubtree))); + } + case SyntaxKind.FunctionDeclaration: { + // Generators lose their generator-ness, excepting their return type + const clean = cleanup(updateFunctionDeclaration(input, + /*decorators*/ undefined, ensureModifiers(input), + /*asteriskToken*/ undefined, input.name, ensureTypeParams(input, input.typeParameters), updateParamsList(input, input.parameters), ensureType(input, input.type), + /*body*/ undefined)); + if (clean && resolver.isExpandoFunctionDeclaration(input)) { + const props = resolver.getPropertiesOfContainerFunction(input); + const fakespace = createModuleDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, clean.name || createIdentifier("_default"), createModuleBlock([]), NodeFlags.Namespace); + fakespace.flags ^= NodeFlags.Synthesized; // unset synthesized so it is usable as an enclosing declaration + fakespace.parent = (enclosingDeclaration as SourceFile | NamespaceDeclaration); + fakespace.locals = createSymbolTable(props); + fakespace.symbol = props[0].parent!; + const declarations = mapDefined(props, p => { + if (!isPropertyAccessExpression(p.valueDeclaration)) { + return undefined; // TODO GH#33569: Handle element access expressions that created late bound names (rather than silently omitting them) + } + getSymbolAccessibilityDiagnostic = createGetSymbolAccessibilityDiagnosticForNode(p.valueDeclaration); + const type = resolver.createTypeOfDeclaration(p.valueDeclaration, fakespace, declarationEmitNodeBuilderFlags, symbolTracker); + getSymbolAccessibilityDiagnostic = oldDiag; + const varDecl = createVariableDeclaration(unescapeLeadingUnderscores(p.escapedName), type, /*initializer*/ undefined); + return createVariableStatement(/*modifiers*/ undefined, createVariableDeclarationList([varDecl])); + }); + const namespaceDecl = createModuleDeclaration(/*decorators*/ undefined, ensureModifiers(input), (input.name!), createModuleBlock(declarations), NodeFlags.Namespace); + if (!hasModifier(clean, ModifierFlags.Default)) { + return [clean, namespaceDecl]; } - resultHasScopeMarker = true; - // Always visible if the parent node isn't dropped for being not visible - // Rewrite external module names if necessary - return updateExportDeclaration( - input, - /*decorators*/ undefined, - input.modifiers, - input.exportClause, - rewriteModuleSpecifier(input, input.moduleSpecifier), - input.isTypeOnly); - } - case SyntaxKind.ExportAssignment: { - // Always visible if the parent node isn't dropped for being not visible + const modifiers = createModifiersFromModifierFlags((getModifierFlags(clean) & ~ModifierFlags.ExportDefault) | ModifierFlags.Ambient); + const cleanDeclaration = updateFunctionDeclaration(clean, + /*decorators*/ undefined, modifiers, + /*asteriskToken*/ undefined, clean.name, clean.typeParameters, clean.parameters, clean.type, + /*body*/ undefined); + const namespaceDeclaration = updateModuleDeclaration(namespaceDecl, + /*decorators*/ undefined, modifiers, namespaceDecl.name, namespaceDecl.body); + const exportDefaultDeclaration = createExportAssignment( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*isExportEquals*/ false, namespaceDecl.name); if (isSourceFile(input.parent)) { resultHasExternalModuleIndicator = true; } resultHasScopeMarker = true; - if (input.expression.kind === SyntaxKind.Identifier) { - return input; - } - else { - const newId = createOptimisticUniqueName("_default"); - getSymbolAccessibilityDiagnostic = () => ({ - diagnosticMessage: Diagnostics.Default_export_of_the_module_has_or_is_using_private_name_0, - errorNode: input - }); - const varDecl = createVariableDeclaration(newId, resolver.createTypeOfExpression(input.expression, input, declarationEmitNodeBuilderFlags, symbolTracker), /*initializer*/ undefined); - const statement = createVariableStatement(needsDeclare ? [createModifier(SyntaxKind.DeclareKeyword)] : [], createVariableDeclarationList([varDecl], NodeFlags.Const)); - return [statement, updateExportAssignment(input, input.decorators, input.modifiers, newId)]; - } + return [cleanDeclaration, namespaceDeclaration, exportDefaultDeclaration]; } - } - - const result = transformTopLevelDeclaration(input); - // Don't actually transform yet; just leave as original node - will be elided/swapped by late pass - lateStatementReplacementMap.set("" + getOriginalNodeId(input), result); - return input; - } - - function stripExportModifiers(statement: Statement): Statement { - if (isImportEqualsDeclaration(statement) || hasModifier(statement, ModifierFlags.Default)) { - // `export import` statements should remain as-is, as imports are _not_ implicitly exported in an ambient namespace - // Likewise, `export default` classes and the like and just be `default`, so we preserve their `export` modifiers, too - return statement; - } - const clone = getMutableClone(statement); - const modifiers = createModifiersFromModifierFlags(getModifierFlags(statement) & (ModifierFlags.All ^ ModifierFlags.Export)); - clone.modifiers = modifiers.length ? createNodeArray(modifiers) : undefined; - return clone; - } - - function transformTopLevelDeclaration(input: LateVisibilityPaintedStatement) { - if (shouldStripInternal(input)) return; - switch (input.kind) { - case SyntaxKind.ImportEqualsDeclaration: { - return transformImportEqualsDeclaration(input); - } - case SyntaxKind.ImportDeclaration: { - return transformImportDeclaration(input); + else { + return clean; } } - if (isDeclaration(input) && isDeclarationAndNotVisible(input)) return; - - // Elide implementation signatures from overload sets - if (isFunctionLike(input) && resolver.isImplementationOfOverload(input)) return; - - let previousEnclosingDeclaration: typeof enclosingDeclaration; - if (isEnclosingDeclaration(input)) { - previousEnclosingDeclaration = enclosingDeclaration; - enclosingDeclaration = input as Declaration; - } - - const canProdiceDiagnostic = canProduceDiagnostics(input); - const oldDiag = getSymbolAccessibilityDiagnostic; - if (canProdiceDiagnostic) { - getSymbolAccessibilityDiagnostic = createGetSymbolAccessibilityDiagnosticForNode(input as DeclarationDiagnosticProducing); - } - - const previousNeedsDeclare = needsDeclare; - switch (input.kind) { - case SyntaxKind.TypeAliasDeclaration: // Type aliases get `declare`d if need be (for legacy support), but that's all - return cleanup(updateTypeAliasDeclaration( - input, - /*decorators*/ undefined, - ensureModifiers(input), - input.name, - visitNodes(input.typeParameters, visitDeclarationSubtree, isTypeParameterDeclaration), - visitNode(input.type, visitDeclarationSubtree, isTypeNode) - )); - case SyntaxKind.InterfaceDeclaration: { - return cleanup(updateInterfaceDeclaration( - input, - /*decorators*/ undefined, - ensureModifiers(input), - input.name, - ensureTypeParams(input, input.typeParameters), - transformHeritageClauses(input.heritageClauses), - visitNodes(input.members, visitDeclarationSubtree) - )); - } - case SyntaxKind.FunctionDeclaration: { - // Generators lose their generator-ness, excepting their return type - const clean = cleanup(updateFunctionDeclaration( - input, - /*decorators*/ undefined, - ensureModifiers(input), - /*asteriskToken*/ undefined, - input.name, - ensureTypeParams(input, input.typeParameters), - updateParamsList(input, input.parameters), - ensureType(input, input.type), - /*body*/ undefined - )); - if (clean && resolver.isExpandoFunctionDeclaration(input)) { - const props = resolver.getPropertiesOfContainerFunction(input); - const fakespace = createModuleDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, clean.name || createIdentifier("_default"), createModuleBlock([]), NodeFlags.Namespace); - fakespace.flags ^= NodeFlags.Synthesized; // unset synthesized so it is usable as an enclosing declaration - fakespace.parent = enclosingDeclaration as SourceFile | NamespaceDeclaration; - fakespace.locals = createSymbolTable(props); - fakespace.symbol = props[0].parent!; - const declarations = mapDefined(props, p => { - if (!isPropertyAccessExpression(p.valueDeclaration)) { - return undefined; // TODO GH#33569: Handle element access expressions that created late bound names (rather than silently omitting them) - } - getSymbolAccessibilityDiagnostic = createGetSymbolAccessibilityDiagnosticForNode(p.valueDeclaration); - const type = resolver.createTypeOfDeclaration(p.valueDeclaration, fakespace, declarationEmitNodeBuilderFlags, symbolTracker); - getSymbolAccessibilityDiagnostic = oldDiag; - const varDecl = createVariableDeclaration(unescapeLeadingUnderscores(p.escapedName), type, /*initializer*/ undefined); - return createVariableStatement(/*modifiers*/ undefined, createVariableDeclarationList([varDecl])); - }); - const namespaceDecl = createModuleDeclaration(/*decorators*/ undefined, ensureModifiers(input), input.name!, createModuleBlock(declarations), NodeFlags.Namespace); - - if (!hasModifier(clean, ModifierFlags.Default)) { - return [clean, namespaceDecl]; + case SyntaxKind.ModuleDeclaration: { + needsDeclare = false; + const inner = input.body; + if (inner && inner.kind === SyntaxKind.ModuleBlock) { + const oldNeedsScopeFix = needsScopeFixMarker; + const oldHasScopeFix = resultHasScopeMarker; + resultHasScopeMarker = false; + needsScopeFixMarker = false; + const statements = visitNodes(inner.statements, visitDeclarationStatements); + let lateStatements = transformAndReplaceLatePaintedStatements(statements); + if (input.flags & NodeFlags.Ambient) { + needsScopeFixMarker = false; // If it was `declare`'d everything is implicitly exported already, ignore late printed "privates" + } + // With the final list of statements, there are 3 possibilities: + // 1. There's an export assignment or export declaration in the namespace - do nothing + // 2. Everything is exported and there are no export assignments or export declarations - strip all export modifiers + // 3. Some things are exported, some are not, and there's no marker - add an empty marker + if (!isGlobalScopeAugmentation(input) && !hasScopeMarker(lateStatements) && !resultHasScopeMarker) { + if (needsScopeFixMarker) { + lateStatements = createNodeArray([...lateStatements, createEmptyExports()]); } - - const modifiers = createModifiersFromModifierFlags((getModifierFlags(clean) & ~ModifierFlags.ExportDefault) | ModifierFlags.Ambient); - const cleanDeclaration = updateFunctionDeclaration( - clean, - /*decorators*/ undefined, - modifiers, - /*asteriskToken*/ undefined, - clean.name, - clean.typeParameters, - clean.parameters, - clean.type, - /*body*/ undefined - ); - - const namespaceDeclaration = updateModuleDeclaration( - namespaceDecl, - /*decorators*/ undefined, - modifiers, - namespaceDecl.name, - namespaceDecl.body - ); - - const exportDefaultDeclaration = createExportAssignment( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*isExportEquals*/ false, - namespaceDecl.name - ); - - if (isSourceFile(input.parent)) { - resultHasExternalModuleIndicator = true; + else { + lateStatements = visitNodes(lateStatements, stripExportModifiers); } - resultHasScopeMarker = true; - - return [cleanDeclaration, namespaceDeclaration, exportDefaultDeclaration]; - } - else { - return clean; } + const body = updateModuleBlock(inner, lateStatements); + needsDeclare = previousNeedsDeclare; + needsScopeFixMarker = oldNeedsScopeFix; + resultHasScopeMarker = oldHasScopeFix; + const mods = ensureModifiers(input); + return cleanup(updateModuleDeclaration(input, + /*decorators*/ undefined, mods, isExternalModuleAugmentation(input) ? rewriteModuleSpecifier(input, input.name) : input.name, body)); } - case SyntaxKind.ModuleDeclaration: { + else { + needsDeclare = previousNeedsDeclare; + const mods = ensureModifiers(input); needsDeclare = false; - const inner = input.body; - if (inner && inner.kind === SyntaxKind.ModuleBlock) { - const oldNeedsScopeFix = needsScopeFixMarker; - const oldHasScopeFix = resultHasScopeMarker; - resultHasScopeMarker = false; - needsScopeFixMarker = false; - const statements = visitNodes(inner.statements, visitDeclarationStatements); - let lateStatements = transformAndReplaceLatePaintedStatements(statements); - if (input.flags & NodeFlags.Ambient) { - needsScopeFixMarker = false; // If it was `declare`'d everything is implicitly exported already, ignore late printed "privates" + visitNode(inner, visitDeclarationStatements); + // eagerly transform nested namespaces (the nesting doesn't need any elision or painting done) + const id = "" + getOriginalNodeId((inner!)); // TODO: GH#18217 + const body = lateStatementReplacementMap.get(id); + lateStatementReplacementMap.delete(id); + return cleanup(updateModuleDeclaration(input, + /*decorators*/ undefined, mods, input.name, (body as ModuleBody))); + } + } + case SyntaxKind.ClassDeclaration: { + const modifiers = createNodeArray(ensureModifiers(input)); + const typeParameters = ensureTypeParams(input, input.typeParameters); + const ctor = getFirstConstructorWithBody(input); + let parameterProperties: readonly PropertyDeclaration[] | undefined; + if (ctor) { + const oldDiag = getSymbolAccessibilityDiagnostic; + parameterProperties = compact(flatMap(ctor.parameters, (param) => { + if (!hasModifier(param, ModifierFlags.ParameterPropertyModifier) || shouldStripInternal(param)) + return; + getSymbolAccessibilityDiagnostic = createGetSymbolAccessibilityDiagnosticForNode(param); + if (param.name.kind === SyntaxKind.Identifier) { + return preserveJsDoc(createProperty( + /*decorators*/ undefined, ensureModifiers(param), param.name, param.questionToken, ensureType(param, param.type), ensureNoInitializer(param)), param); } - // With the final list of statements, there are 3 possibilities: - // 1. There's an export assignment or export declaration in the namespace - do nothing - // 2. Everything is exported and there are no export assignments or export declarations - strip all export modifiers - // 3. Some things are exported, some are not, and there's no marker - add an empty marker - if (!isGlobalScopeAugmentation(input) && !hasScopeMarker(lateStatements) && !resultHasScopeMarker) { - if (needsScopeFixMarker) { - lateStatements = createNodeArray([...lateStatements, createEmptyExports()]); - } - else { - lateStatements = visitNodes(lateStatements, stripExportModifiers); - } + else { + // Pattern - this is currently an error, but we emit declarations for it somewhat correctly + return walkBindingPattern(param.name); } - const body = updateModuleBlock(inner, lateStatements); - needsDeclare = previousNeedsDeclare; - needsScopeFixMarker = oldNeedsScopeFix; - resultHasScopeMarker = oldHasScopeFix; - const mods = ensureModifiers(input); - return cleanup(updateModuleDeclaration( - input, - /*decorators*/ undefined, - mods, - isExternalModuleAugmentation(input) ? rewriteModuleSpecifier(input, input.name) : input.name, - body - )); - } - else { - needsDeclare = previousNeedsDeclare; - const mods = ensureModifiers(input); - needsDeclare = false; - visitNode(inner, visitDeclarationStatements); - // eagerly transform nested namespaces (the nesting doesn't need any elision or painting done) - const id = "" + getOriginalNodeId(inner!); // TODO: GH#18217 - const body = lateStatementReplacementMap.get(id); - lateStatementReplacementMap.delete(id); - return cleanup(updateModuleDeclaration( - input, - /*decorators*/ undefined, - mods, - input.name, - body as ModuleBody - )); - } - } - case SyntaxKind.ClassDeclaration: { - const modifiers = createNodeArray(ensureModifiers(input)); - const typeParameters = ensureTypeParams(input, input.typeParameters); - const ctor = getFirstConstructorWithBody(input); - let parameterProperties: readonly PropertyDeclaration[] | undefined; - if (ctor) { - const oldDiag = getSymbolAccessibilityDiagnostic; - parameterProperties = compact(flatMap(ctor.parameters, (param) => { - if (!hasModifier(param, ModifierFlags.ParameterPropertyModifier) || shouldStripInternal(param)) return; - getSymbolAccessibilityDiagnostic = createGetSymbolAccessibilityDiagnosticForNode(param); - if (param.name.kind === SyntaxKind.Identifier) { - return preserveJsDoc(createProperty( - /*decorators*/ undefined, - ensureModifiers(param), - param.name, - param.questionToken, - ensureType(param, param.type), - ensureNoInitializer(param)), param); - } - else { - // Pattern - this is currently an error, but we emit declarations for it somewhat correctly - return walkBindingPattern(param.name); - } - - function walkBindingPattern(pattern: BindingPattern) { - let elems: PropertyDeclaration[] | undefined; - for (const elem of pattern.elements) { - if (isOmittedExpression(elem)) continue; - if (isBindingPattern(elem.name)) { - elems = concatenate(elems, walkBindingPattern(elem.name)); - } - elems = elems || []; - elems.push(createProperty( - /*decorators*/ undefined, - ensureModifiers(param), - elem.name as Identifier, - /*questionToken*/ undefined, - ensureType(elem, /*type*/ undefined), - /*initializer*/ undefined - )); + function walkBindingPattern(pattern: BindingPattern) { + let elems: PropertyDeclaration[] | undefined; + for (const elem of pattern.elements) { + if (isOmittedExpression(elem)) + continue; + if (isBindingPattern(elem.name)) { + elems = concatenate(elems, walkBindingPattern(elem.name)); } - return elems; - } - })); - getSymbolAccessibilityDiagnostic = oldDiag; - } - - const hasPrivateIdentifier = some(input.members, member => !!member.name && isPrivateIdentifier(member.name)); - const privateIdentifier = hasPrivateIdentifier ? [ - createProperty( - /*decorators*/ undefined, - /*modifiers*/ undefined, - createPrivateIdentifier("#private"), - /*questionToken*/ undefined, - /*type*/ undefined, - /*initializer*/ undefined - ) - ] : undefined; - const memberNodes = concatenate(concatenate(privateIdentifier, parameterProperties), visitNodes(input.members, visitDeclarationSubtree)); - const members = createNodeArray(memberNodes); - - const extendsClause = getEffectiveBaseTypeNode(input); - if (extendsClause && !isEntityNameExpression(extendsClause.expression) && extendsClause.expression.kind !== SyntaxKind.NullKeyword) { - // We must add a temporary declaration for the extends clause expression - - const oldId = input.name ? unescapeLeadingUnderscores(input.name.escapedText) : "default"; - const newId = createOptimisticUniqueName(`${oldId}_base`); - getSymbolAccessibilityDiagnostic = () => ({ - diagnosticMessage: Diagnostics.extends_clause_of_exported_class_0_has_or_is_using_private_name_1, - errorNode: extendsClause, - typeName: input.name - }); - const varDecl = createVariableDeclaration(newId, resolver.createTypeOfExpression(extendsClause.expression, input, declarationEmitNodeBuilderFlags, symbolTracker), /*initializer*/ undefined); - const statement = createVariableStatement(needsDeclare ? [createModifier(SyntaxKind.DeclareKeyword)] : [], createVariableDeclarationList([varDecl], NodeFlags.Const)); - const heritageClauses = createNodeArray(map(input.heritageClauses, clause => { - if (clause.token === SyntaxKind.ExtendsKeyword) { - const oldDiag = getSymbolAccessibilityDiagnostic; - getSymbolAccessibilityDiagnostic = createGetSymbolAccessibilityDiagnosticForNode(clause.types[0]); - const newClause = updateHeritageClause(clause, map(clause.types, t => updateExpressionWithTypeArguments(t, visitNodes(t.typeArguments, visitDeclarationSubtree), newId))); - getSymbolAccessibilityDiagnostic = oldDiag; - return newClause; + elems = elems || []; + elems.push(createProperty( + /*decorators*/ undefined, ensureModifiers(param), (elem.name as Identifier), + /*questionToken*/ undefined, ensureType(elem, /*type*/ undefined), + /*initializer*/ undefined)); } - return updateHeritageClause(clause, visitNodes(createNodeArray(filter(clause.types, t => isEntityNameExpression(t.expression) || t.expression.kind === SyntaxKind.NullKeyword)), visitDeclarationSubtree)); - })); - return [statement, cleanup(updateClassDeclaration( - input, - /*decorators*/ undefined, - modifiers, - input.name, - typeParameters, - heritageClauses, - members - ))!]; // TODO: GH#18217 - } - else { - const heritageClauses = transformHeritageClauses(input.heritageClauses); - return cleanup(updateClassDeclaration( - input, - /*decorators*/ undefined, - modifiers, - input.name, - typeParameters, - heritageClauses, - members - )); - } - } - case SyntaxKind.VariableStatement: { - return cleanup(transformVariableStatement(input)); - } - case SyntaxKind.EnumDeclaration: { - return cleanup(updateEnumDeclaration(input, /*decorators*/ undefined, createNodeArray(ensureModifiers(input)), input.name, createNodeArray(mapDefined(input.members, m => { - if (shouldStripInternal(m)) return; - // Rewrite enum values to their constants, if available - const constValue = resolver.getConstantValue(m); - return preserveJsDoc(updateEnumMember(m, m.name, constValue !== undefined ? createLiteral(constValue) : undefined), m); - })))); - } - } - // Anything left unhandled is an error, so this should be unreachable - return Debug.assertNever(input, `Unhandled top-level node in declaration emit: ${(ts as any).SyntaxKind[(input as any).kind]}`); - - function cleanup(node: T | undefined): T | undefined { - if (isEnclosingDeclaration(input)) { - enclosingDeclaration = previousEnclosingDeclaration; - } - if (canProdiceDiagnostic) { + return elems; + } + })); getSymbolAccessibilityDiagnostic = oldDiag; } - if (input.kind === SyntaxKind.ModuleDeclaration) { - needsDeclare = previousNeedsDeclare; - } - if (node as Node === input) { - return node; - } - return node && setOriginalNode(preserveJsDoc(node, input), input); - } - } - - function transformVariableStatement(input: VariableStatement) { - if (!forEach(input.declarationList.declarations, getBindingNameVisible)) return; - const nodes = visitNodes(input.declarationList.declarations, visitDeclarationSubtree); - if (!length(nodes)) return; - return updateVariableStatement(input, createNodeArray(ensureModifiers(input)), updateVariableDeclarationList(input.declarationList, nodes)); - } - - function recreateBindingPattern(d: BindingPattern): VariableDeclaration[] { - return flatten(mapDefined(d.elements, e => recreateBindingElement(e))); - } - - function recreateBindingElement(e: ArrayBindingElement) { - if (e.kind === SyntaxKind.OmittedExpression) { - return; - } - if (e.name) { - if (!getBindingNameVisible(e)) return; - if (isBindingPattern(e.name)) { - return recreateBindingPattern(e.name); + const hasPrivateIdentifier = some(input.members, member => !!member.name && isPrivateIdentifier(member.name)); + const privateIdentifier = hasPrivateIdentifier ? [ + createProperty( + /*decorators*/ undefined, + /*modifiers*/ undefined, createPrivateIdentifier("#private"), + /*questionToken*/ undefined, + /*type*/ undefined, + /*initializer*/ undefined) + ] : undefined; + const memberNodes = concatenate(concatenate(privateIdentifier, parameterProperties), visitNodes(input.members, visitDeclarationSubtree)); + const members = createNodeArray(memberNodes); + const extendsClause = getEffectiveBaseTypeNode(input); + if (extendsClause && !isEntityNameExpression(extendsClause.expression) && extendsClause.expression.kind !== SyntaxKind.NullKeyword) { + // We must add a temporary declaration for the extends clause expression + const oldId = input.name ? unescapeLeadingUnderscores(input.name.escapedText) : "default"; + const newId = createOptimisticUniqueName(`${oldId}_base`); + getSymbolAccessibilityDiagnostic = () => ({ + diagnosticMessage: Diagnostics.extends_clause_of_exported_class_0_has_or_is_using_private_name_1, + errorNode: extendsClause, + typeName: input.name + }); + const varDecl = createVariableDeclaration(newId, resolver.createTypeOfExpression(extendsClause.expression, input, declarationEmitNodeBuilderFlags, symbolTracker), /*initializer*/ undefined); + const statement = createVariableStatement(needsDeclare ? [createModifier(SyntaxKind.DeclareKeyword)] : [], createVariableDeclarationList([varDecl], NodeFlags.Const)); + const heritageClauses = createNodeArray(map(input.heritageClauses, clause => { + if (clause.token === SyntaxKind.ExtendsKeyword) { + const oldDiag = getSymbolAccessibilityDiagnostic; + getSymbolAccessibilityDiagnostic = createGetSymbolAccessibilityDiagnosticForNode(clause.types[0]); + const newClause = updateHeritageClause(clause, map(clause.types, t => updateExpressionWithTypeArguments(t, visitNodes(t.typeArguments, visitDeclarationSubtree), newId))); + getSymbolAccessibilityDiagnostic = oldDiag; + return newClause; + } + return updateHeritageClause(clause, visitNodes(createNodeArray(filter(clause.types, t => isEntityNameExpression(t.expression) || t.expression.kind === SyntaxKind.NullKeyword)), visitDeclarationSubtree)); + })); + return [statement, (cleanup(updateClassDeclaration(input, + /*decorators*/ undefined, modifiers, input.name, typeParameters, heritageClauses, members))!)]; // TODO: GH#18217 } else { - return createVariableDeclaration(e.name, ensureType(e, /*type*/ undefined), /*initializer*/ undefined); + const heritageClauses = transformHeritageClauses(input.heritageClauses); + return cleanup(updateClassDeclaration(input, + /*decorators*/ undefined, modifiers, input.name, typeParameters, heritageClauses, members)); } } - } - - function checkName(node: DeclarationDiagnosticProducing) { - let oldDiag: typeof getSymbolAccessibilityDiagnostic | undefined; - if (!suppressNewDiagnosticContexts) { - oldDiag = getSymbolAccessibilityDiagnostic; - getSymbolAccessibilityDiagnostic = createGetSymbolAccessibilityDiagnosticForNodeName(node); + case SyntaxKind.VariableStatement: { + return cleanup(transformVariableStatement(input)); } - errorNameNode = (node as NamedDeclaration).name; - Debug.assert(resolver.isLateBound(getParseTreeNode(node) as Declaration)); // Should only be called with dynamic names - const decl = node as NamedDeclaration as LateBoundDeclaration; - const entityName = decl.name.expression; - checkEntityNameVisibility(entityName, enclosingDeclaration); - if (!suppressNewDiagnosticContexts) { - getSymbolAccessibilityDiagnostic = oldDiag!; + case SyntaxKind.EnumDeclaration: { + return cleanup(updateEnumDeclaration(input, /*decorators*/ undefined, createNodeArray(ensureModifiers(input)), input.name, createNodeArray(mapDefined(input.members, m => { + if (shouldStripInternal(m)) + return; + // Rewrite enum values to their constants, if available + const constValue = resolver.getConstantValue(m); + return preserveJsDoc(updateEnumMember(m, m.name, constValue !== undefined ? createLiteral(constValue) : undefined), m); + })))); } - errorNameNode = undefined; - } - - function shouldStripInternal(node: Node) { - return !!stripInternal && !!node && isInternalDeclaration(node, currentSourceFile); - } - - function isScopeMarker(node: Node) { - return isExportAssignment(node) || isExportDeclaration(node); - } - - function hasScopeMarker(statements: readonly Statement[]) { - return some(statements, isScopeMarker); } - - function ensureModifiers(node: Node): readonly Modifier[] | undefined { - const currentFlags = getModifierFlags(node); - const newFlags = ensureModifierFlags(node); - if (currentFlags === newFlags) { - return node.modifiers; + // Anything left unhandled is an error, so this should be unreachable + return Debug.assertNever(input, `Unhandled top-level node in declaration emit: ${(ts as any).SyntaxKind[(input as any).kind]}`); + function cleanup(node: T | undefined): T | undefined { + if (isEnclosingDeclaration(input)) { + enclosingDeclaration = previousEnclosingDeclaration; } - return createModifiersFromModifierFlags(newFlags); - } - - function ensureModifierFlags(node: Node): ModifierFlags { - let mask = ModifierFlags.All ^ (ModifierFlags.Public | ModifierFlags.Async); // No async modifiers in declaration files - let additions = (needsDeclare && !isAlwaysType(node)) ? ModifierFlags.Ambient : ModifierFlags.None; - const parentIsFile = node.parent.kind === SyntaxKind.SourceFile; - if (!parentIsFile || (isBundledEmit && parentIsFile && isExternalModule(node.parent as SourceFile))) { - mask ^= ModifierFlags.Ambient; - additions = ModifierFlags.None; + if (canProdiceDiagnostic) { + getSymbolAccessibilityDiagnostic = oldDiag; } - return maskModifierFlags(node, mask, additions); - } - - function getTypeAnnotationFromAllAccessorDeclarations(node: AccessorDeclaration, accessors: AllAccessorDeclarations) { - let accessorType = getTypeAnnotationFromAccessor(node); - if (!accessorType && node !== accessors.firstAccessor) { - accessorType = getTypeAnnotationFromAccessor(accessors.firstAccessor); - // If we end up pulling the type from the second accessor, we also need to change the diagnostic context to get the expected error message - getSymbolAccessibilityDiagnostic = createGetSymbolAccessibilityDiagnosticForNode(accessors.firstAccessor); + if (input.kind === SyntaxKind.ModuleDeclaration) { + needsDeclare = previousNeedsDeclare; } - if (!accessorType && accessors.secondAccessor && node !== accessors.secondAccessor) { - accessorType = getTypeAnnotationFromAccessor(accessors.secondAccessor); - // If we end up pulling the type from the second accessor, we also need to change the diagnostic context to get the expected error message - getSymbolAccessibilityDiagnostic = createGetSymbolAccessibilityDiagnosticForNode(accessors.secondAccessor); + if ((node as Node) === input) { + return node; } - return accessorType; + return node && setOriginalNode(preserveJsDoc(node, input), input); } - - function transformHeritageClauses(nodes: NodeArray | undefined) { - return createNodeArray(filter(map(nodes, clause => updateHeritageClause(clause, visitNodes(createNodeArray(filter(clause.types, t => { - return isEntityNameExpression(t.expression) || (clause.token === SyntaxKind.ExtendsKeyword && t.expression.kind === SyntaxKind.NullKeyword); - })), visitDeclarationSubtree))), clause => clause.types && !!clause.types.length)); + } + function transformVariableStatement(input: VariableStatement) { + if (!forEach(input.declarationList.declarations, getBindingNameVisible)) + return; + const nodes = visitNodes(input.declarationList.declarations, visitDeclarationSubtree); + if (!length(nodes)) + return; + return updateVariableStatement(input, createNodeArray(ensureModifiers(input)), updateVariableDeclarationList(input.declarationList, nodes)); + } + function recreateBindingPattern(d: BindingPattern): VariableDeclaration[] { + return flatten(mapDefined(d.elements, e => recreateBindingElement(e))); + } + function recreateBindingElement(e: ArrayBindingElement) { + if (e.kind === SyntaxKind.OmittedExpression) { + return; + } + if (e.name) { + if (!getBindingNameVisible(e)) + return; + if (isBindingPattern(e.name)) { + return recreateBindingPattern(e.name); + } + else { + return createVariableDeclaration(e.name, ensureType(e, /*type*/ undefined), /*initializer*/ undefined); + } } } - - function isAlwaysType(node: Node) { - if (node.kind === SyntaxKind.InterfaceDeclaration) { - return true; + function checkName(node: DeclarationDiagnosticProducing) { + let oldDiag: typeof getSymbolAccessibilityDiagnostic | undefined; + if (!suppressNewDiagnosticContexts) { + oldDiag = getSymbolAccessibilityDiagnostic; + getSymbolAccessibilityDiagnostic = createGetSymbolAccessibilityDiagnosticForNodeName(node); } - return false; + errorNameNode = (node as NamedDeclaration).name; + Debug.assert(resolver.isLateBound((getParseTreeNode(node) as Declaration))); // Should only be called with dynamic names + const decl = (node as NamedDeclaration as LateBoundDeclaration); + const entityName = decl.name.expression; + checkEntityNameVisibility(entityName, enclosingDeclaration); + if (!suppressNewDiagnosticContexts) { + getSymbolAccessibilityDiagnostic = oldDiag!; + } + errorNameNode = undefined; } - - // Elide "public" modifier, as it is the default - function maskModifiers(node: Node, modifierMask?: ModifierFlags, modifierAdditions?: ModifierFlags): Modifier[] { - return createModifiersFromModifierFlags(maskModifierFlags(node, modifierMask, modifierAdditions)); + function shouldStripInternal(node: Node) { + return !!stripInternal && !!node && isInternalDeclaration(node, currentSourceFile); } - - function maskModifierFlags(node: Node, modifierMask: ModifierFlags = ModifierFlags.All ^ ModifierFlags.Public, modifierAdditions: ModifierFlags = ModifierFlags.None): ModifierFlags { - let flags = (getModifierFlags(node) & modifierMask) | modifierAdditions; - if (flags & ModifierFlags.Default && !(flags & ModifierFlags.Export)) { - // A non-exported default is a nonsequitor - we usually try to remove all export modifiers - // from statements in ambient declarations; but a default export must retain its export modifier to be syntactically valid - flags ^= ModifierFlags.Export; - } - if (flags & ModifierFlags.Default && flags & ModifierFlags.Ambient) { - flags ^= ModifierFlags.Ambient; // `declare` is never required alongside `default` (and would be an error if printed) - } - return flags; + function isScopeMarker(node: Node) { + return isExportAssignment(node) || isExportDeclaration(node); } - - function getTypeAnnotationFromAccessor(accessor: AccessorDeclaration): TypeNode | undefined { - if (accessor) { - return accessor.kind === SyntaxKind.GetAccessor - ? accessor.type // Getter - return type - : accessor.parameters.length > 0 - ? accessor.parameters[0].type // Setter parameter type - : undefined; - } + function hasScopeMarker(statements: readonly Statement[]) { + return some(statements, isScopeMarker); } - - type CanHaveLiteralInitializer = VariableDeclaration | PropertyDeclaration | PropertySignature | ParameterDeclaration; - function canHaveLiteralInitializer(node: Node): boolean { - switch (node.kind) { - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.PropertySignature: - return !hasModifier(node, ModifierFlags.Private); - case SyntaxKind.Parameter: - case SyntaxKind.VariableDeclaration: - return true; + function ensureModifiers(node: Node): readonly Modifier[] | undefined { + const currentFlags = getModifierFlags(node); + const newFlags = ensureModifierFlags(node); + if (currentFlags === newFlags) { + return node.modifiers; } - return false; + return createModifiersFromModifierFlags(newFlags); } - - type ProcessedDeclarationStatement = - | FunctionDeclaration - | ModuleDeclaration - | ImportEqualsDeclaration - | InterfaceDeclaration - | ClassDeclaration - | TypeAliasDeclaration - | EnumDeclaration - | VariableStatement - | ImportDeclaration - | ExportDeclaration - | ExportAssignment; - - function isPreservedDeclarationStatement(node: Node): node is ProcessedDeclarationStatement { - switch (node.kind) { - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.ModuleDeclaration: - case SyntaxKind.ImportEqualsDeclaration: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.ClassDeclaration: - case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.VariableStatement: - case SyntaxKind.ImportDeclaration: - case SyntaxKind.ExportDeclaration: - case SyntaxKind.ExportAssignment: - return true; + function ensureModifierFlags(node: Node): ModifierFlags { + let mask = ModifierFlags.All ^ (ModifierFlags.Public | ModifierFlags.Async); // No async modifiers in declaration files + let additions = (needsDeclare && !isAlwaysType(node)) ? ModifierFlags.Ambient : ModifierFlags.None; + const parentIsFile = node.parent.kind === SyntaxKind.SourceFile; + if (!parentIsFile || (isBundledEmit && parentIsFile && isExternalModule((node.parent as SourceFile)))) { + mask ^= ModifierFlags.Ambient; + additions = ModifierFlags.None; } - return false; + return maskModifierFlags(node, mask, additions); } - - type ProcessedComponent = - | ConstructSignatureDeclaration - | ConstructorDeclaration - | MethodDeclaration - | GetAccessorDeclaration - | SetAccessorDeclaration - | PropertyDeclaration - | PropertySignature - | MethodSignature - | CallSignatureDeclaration - | IndexSignatureDeclaration - | VariableDeclaration - | TypeParameterDeclaration - | ExpressionWithTypeArguments - | TypeReferenceNode - | ConditionalTypeNode - | FunctionTypeNode - | ConstructorTypeNode - | ImportTypeNode; - - function isProcessedComponent(node: Node): node is ProcessedComponent { - switch (node.kind) { - case SyntaxKind.ConstructSignature: - case SyntaxKind.Constructor: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.PropertySignature: - case SyntaxKind.MethodSignature: - case SyntaxKind.CallSignature: - case SyntaxKind.IndexSignature: - case SyntaxKind.VariableDeclaration: - case SyntaxKind.TypeParameter: - case SyntaxKind.ExpressionWithTypeArguments: - case SyntaxKind.TypeReference: - case SyntaxKind.ConditionalType: - case SyntaxKind.FunctionType: - case SyntaxKind.ConstructorType: - case SyntaxKind.ImportType: - return true; + function getTypeAnnotationFromAllAccessorDeclarations(node: AccessorDeclaration, accessors: AllAccessorDeclarations) { + let accessorType = getTypeAnnotationFromAccessor(node); + if (!accessorType && node !== accessors.firstAccessor) { + accessorType = getTypeAnnotationFromAccessor(accessors.firstAccessor); + // If we end up pulling the type from the second accessor, we also need to change the diagnostic context to get the expected error message + getSymbolAccessibilityDiagnostic = createGetSymbolAccessibilityDiagnosticForNode(accessors.firstAccessor); } - return false; + if (!accessorType && accessors.secondAccessor && node !== accessors.secondAccessor) { + accessorType = getTypeAnnotationFromAccessor(accessors.secondAccessor); + // If we end up pulling the type from the second accessor, we also need to change the diagnostic context to get the expected error message + getSymbolAccessibilityDiagnostic = createGetSymbolAccessibilityDiagnosticForNode(accessors.secondAccessor); + } + return accessorType; + } + function transformHeritageClauses(nodes: NodeArray | undefined) { + return createNodeArray(filter(map(nodes, clause => updateHeritageClause(clause, visitNodes(createNodeArray(filter(clause.types, t => { + return isEntityNameExpression(t.expression) || (clause.token === SyntaxKind.ExtendsKeyword && t.expression.kind === SyntaxKind.NullKeyword); + })), visitDeclarationSubtree))), clause => clause.types && !!clause.types.length)); + } +} +/* @internal */ +function isAlwaysType(node: Node) { + if (node.kind === SyntaxKind.InterfaceDeclaration) { + return true; + } + return false; +} +// Elide "public" modifier, as it is the default +/* @internal */ +function maskModifiers(node: Node, modifierMask?: ModifierFlags, modifierAdditions?: ModifierFlags): Modifier[] { + return createModifiersFromModifierFlags(maskModifierFlags(node, modifierMask, modifierAdditions)); +} +/* @internal */ +function maskModifierFlags(node: Node, modifierMask: ModifierFlags = ModifierFlags.All ^ ModifierFlags.Public, modifierAdditions: ModifierFlags = ModifierFlags.None): ModifierFlags { + let flags = (getModifierFlags(node) & modifierMask) | modifierAdditions; + if (flags & ModifierFlags.Default && !(flags & ModifierFlags.Export)) { + // A non-exported default is a nonsequitor - we usually try to remove all export modifiers + // from statements in ambient declarations; but a default export must retain its export modifier to be syntactically valid + flags ^= ModifierFlags.Export; + } + if (flags & ModifierFlags.Default && flags & ModifierFlags.Ambient) { + flags ^= ModifierFlags.Ambient; // `declare` is never required alongside `default` (and would be an error if printed) + } + return flags; +} +/* @internal */ +function getTypeAnnotationFromAccessor(accessor: AccessorDeclaration): TypeNode | undefined { + if (accessor) { + return accessor.kind === SyntaxKind.GetAccessor + ? accessor.type // Getter - return type + : accessor.parameters.length > 0 + ? accessor.parameters[0].type // Setter parameter type + : undefined; + } +} +/* @internal */ +type CanHaveLiteralInitializer = VariableDeclaration | PropertyDeclaration | PropertySignature | ParameterDeclaration; +/* @internal */ +function canHaveLiteralInitializer(node: Node): boolean { + switch (node.kind) { + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.PropertySignature: + return !hasModifier(node, ModifierFlags.Private); + case SyntaxKind.Parameter: + case SyntaxKind.VariableDeclaration: + return true; + } + return false; +} +/* @internal */ +type ProcessedDeclarationStatement = FunctionDeclaration | ModuleDeclaration | ImportEqualsDeclaration | InterfaceDeclaration | ClassDeclaration | TypeAliasDeclaration | EnumDeclaration | VariableStatement | ImportDeclaration | ExportDeclaration | ExportAssignment; +/* @internal */ +function isPreservedDeclarationStatement(node: Node): node is ProcessedDeclarationStatement { + switch (node.kind) { + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.ImportEqualsDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.VariableStatement: + case SyntaxKind.ImportDeclaration: + case SyntaxKind.ExportDeclaration: + case SyntaxKind.ExportAssignment: + return true; + } + return false; +} +/* @internal */ +type ProcessedComponent = ConstructSignatureDeclaration | ConstructorDeclaration | MethodDeclaration | GetAccessorDeclaration | SetAccessorDeclaration | PropertyDeclaration | PropertySignature | MethodSignature | CallSignatureDeclaration | IndexSignatureDeclaration | VariableDeclaration | TypeParameterDeclaration | ExpressionWithTypeArguments | TypeReferenceNode | ConditionalTypeNode | FunctionTypeNode | ConstructorTypeNode | ImportTypeNode; +/* @internal */ +function isProcessedComponent(node: Node): node is ProcessedComponent { + switch (node.kind) { + case SyntaxKind.ConstructSignature: + case SyntaxKind.Constructor: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.PropertySignature: + case SyntaxKind.MethodSignature: + case SyntaxKind.CallSignature: + case SyntaxKind.IndexSignature: + case SyntaxKind.VariableDeclaration: + case SyntaxKind.TypeParameter: + case SyntaxKind.ExpressionWithTypeArguments: + case SyntaxKind.TypeReference: + case SyntaxKind.ConditionalType: + case SyntaxKind.FunctionType: + case SyntaxKind.ConstructorType: + case SyntaxKind.ImportType: + return true; } + return false; } diff --git a/src/compiler/transformers/declarations/diagnostics.ts b/src/compiler/transformers/declarations/diagnostics.ts index ac44f7efd15e5..1b84d1e739fd7 100644 --- a/src/compiler/transformers/declarations/diagnostics.ts +++ b/src/compiler/transformers/declarations/diagnostics.ts @@ -1,76 +1,152 @@ +import { SymbolAccessibilityResult, Node, DiagnosticMessage, DeclarationName, QualifiedName, VariableDeclaration, PropertyDeclaration, PropertySignature, BindingElement, SetAccessorDeclaration, GetAccessorDeclaration, ConstructSignatureDeclaration, CallSignatureDeclaration, MethodDeclaration, MethodSignature, FunctionDeclaration, ParameterDeclaration, TypeParameterDeclaration, ExpressionWithTypeArguments, ImportEqualsDeclaration, TypeAliasDeclaration, ConstructorDeclaration, IndexSignatureDeclaration, PropertyAccessExpression, isVariableDeclaration, isPropertyDeclaration, isPropertySignature, isBindingElement, isSetAccessor, isGetAccessor, isConstructSignatureDeclaration, isCallSignatureDeclaration, isMethodDeclaration, isMethodSignature, isFunctionDeclaration, isParameter, isTypeParameterDeclaration, isExpressionWithTypeArguments, isImportEqualsDeclaration, isTypeAliasDeclaration, isConstructorDeclaration, isIndexSignatureDeclaration, isPropertyAccessExpression, NamedDeclaration, hasModifier, ModifierFlags, SymbolAccessibility, Diagnostics, SyntaxKind, isParameterPropertyDeclaration, Debug, isHeritageClause, getNameOfDeclaration, Declaration } from "../../ts"; +import * as ts from "../../ts"; /* @internal */ -namespace ts { - export type GetSymbolAccessibilityDiagnostic = (symbolAccessibilityResult: SymbolAccessibilityResult) => (SymbolAccessibilityDiagnostic | undefined); - - export interface SymbolAccessibilityDiagnostic { - errorNode: Node; - diagnosticMessage: DiagnosticMessage; - typeName?: DeclarationName | QualifiedName; +export type GetSymbolAccessibilityDiagnostic = (symbolAccessibilityResult: SymbolAccessibilityResult) => (SymbolAccessibilityDiagnostic | undefined); +/* @internal */ +export interface SymbolAccessibilityDiagnostic { + errorNode: Node; + diagnosticMessage: DiagnosticMessage; + typeName?: DeclarationName | QualifiedName; +} +/* @internal */ +export type DeclarationDiagnosticProducing = VariableDeclaration | PropertyDeclaration | PropertySignature | BindingElement | SetAccessorDeclaration | GetAccessorDeclaration | ConstructSignatureDeclaration | CallSignatureDeclaration | MethodDeclaration | MethodSignature | FunctionDeclaration | ParameterDeclaration | TypeParameterDeclaration | ExpressionWithTypeArguments | ImportEqualsDeclaration | TypeAliasDeclaration | ConstructorDeclaration | IndexSignatureDeclaration | PropertyAccessExpression; +/* @internal */ +export function canProduceDiagnostics(node: Node): node is DeclarationDiagnosticProducing { + return isVariableDeclaration(node) || + isPropertyDeclaration(node) || + isPropertySignature(node) || + isBindingElement(node) || + isSetAccessor(node) || + isGetAccessor(node) || + isConstructSignatureDeclaration(node) || + isCallSignatureDeclaration(node) || + isMethodDeclaration(node) || + isMethodSignature(node) || + isFunctionDeclaration(node) || + isParameter(node) || + isTypeParameterDeclaration(node) || + isExpressionWithTypeArguments(node) || + isImportEqualsDeclaration(node) || + isTypeAliasDeclaration(node) || + isConstructorDeclaration(node) || + isIndexSignatureDeclaration(node) || + isPropertyAccessExpression(node); +} +/* @internal */ +export function createGetSymbolAccessibilityDiagnosticForNodeName(node: DeclarationDiagnosticProducing) { + if (isSetAccessor(node) || isGetAccessor(node)) { + return getAccessorNameVisibilityError; + } + else if (isMethodSignature(node) || isMethodDeclaration(node)) { + return getMethodNameVisibilityError; + } + else { + return createGetSymbolAccessibilityDiagnosticForNode(node); } - - export type DeclarationDiagnosticProducing = - | VariableDeclaration - | PropertyDeclaration - | PropertySignature - | BindingElement - | SetAccessorDeclaration - | GetAccessorDeclaration - | ConstructSignatureDeclaration - | CallSignatureDeclaration - | MethodDeclaration - | MethodSignature - | FunctionDeclaration - | ParameterDeclaration - | TypeParameterDeclaration - | ExpressionWithTypeArguments - | ImportEqualsDeclaration - | TypeAliasDeclaration - | ConstructorDeclaration - | IndexSignatureDeclaration - | PropertyAccessExpression; - - export function canProduceDiagnostics(node: Node): node is DeclarationDiagnosticProducing { - return isVariableDeclaration(node) || - isPropertyDeclaration(node) || - isPropertySignature(node) || - isBindingElement(node) || - isSetAccessor(node) || - isGetAccessor(node) || - isConstructSignatureDeclaration(node) || - isCallSignatureDeclaration(node) || - isMethodDeclaration(node) || - isMethodSignature(node) || - isFunctionDeclaration(node) || - isParameter(node) || - isTypeParameterDeclaration(node) || - isExpressionWithTypeArguments(node) || - isImportEqualsDeclaration(node) || - isTypeAliasDeclaration(node) || - isConstructorDeclaration(node) || - isIndexSignatureDeclaration(node) || - isPropertyAccessExpression(node); + function getAccessorNameVisibilityError(symbolAccessibilityResult: SymbolAccessibilityResult) { + const diagnosticMessage = getAccessorNameVisibilityDiagnosticMessage(symbolAccessibilityResult); + return diagnosticMessage !== undefined ? { + diagnosticMessage, + errorNode: node, + typeName: (node as NamedDeclaration).name + } : undefined; } - - export function createGetSymbolAccessibilityDiagnosticForNodeName(node: DeclarationDiagnosticProducing) { - if (isSetAccessor(node) || isGetAccessor(node)) { - return getAccessorNameVisibilityError; + function getAccessorNameVisibilityDiagnosticMessage(symbolAccessibilityResult: SymbolAccessibilityResult) { + if (hasModifier(node, ModifierFlags.Static)) { + return symbolAccessibilityResult.errorModuleName ? + symbolAccessibilityResult.accessibility === SymbolAccessibility.CannotBeNamed ? + Diagnostics.Public_static_property_0_of_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : + Diagnostics.Public_static_property_0_of_exported_class_has_or_is_using_name_1_from_private_module_2 : + Diagnostics.Public_static_property_0_of_exported_class_has_or_is_using_private_name_1; } - else if (isMethodSignature(node) || isMethodDeclaration(node)) { - return getMethodNameVisibilityError; + else if (node.parent.kind === SyntaxKind.ClassDeclaration) { + return symbolAccessibilityResult.errorModuleName ? + symbolAccessibilityResult.accessibility === SymbolAccessibility.CannotBeNamed ? + Diagnostics.Public_property_0_of_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : + Diagnostics.Public_property_0_of_exported_class_has_or_is_using_name_1_from_private_module_2 : + Diagnostics.Public_property_0_of_exported_class_has_or_is_using_private_name_1; } else { - return createGetSymbolAccessibilityDiagnosticForNode(node); + return symbolAccessibilityResult.errorModuleName ? + Diagnostics.Property_0_of_exported_interface_has_or_is_using_name_1_from_private_module_2 : + Diagnostics.Property_0_of_exported_interface_has_or_is_using_private_name_1; + } + } + function getMethodNameVisibilityError(symbolAccessibilityResult: SymbolAccessibilityResult): SymbolAccessibilityDiagnostic | undefined { + const diagnosticMessage = getMethodNameVisibilityDiagnosticMessage(symbolAccessibilityResult); + return diagnosticMessage !== undefined ? { + diagnosticMessage, + errorNode: node, + typeName: (node as NamedDeclaration).name + } : undefined; + } + function getMethodNameVisibilityDiagnosticMessage(symbolAccessibilityResult: SymbolAccessibilityResult) { + if (hasModifier(node, ModifierFlags.Static)) { + return symbolAccessibilityResult.errorModuleName ? + symbolAccessibilityResult.accessibility === SymbolAccessibility.CannotBeNamed ? + Diagnostics.Public_static_method_0_of_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : + Diagnostics.Public_static_method_0_of_exported_class_has_or_is_using_name_1_from_private_module_2 : + Diagnostics.Public_static_method_0_of_exported_class_has_or_is_using_private_name_1; + } + else if (node.parent.kind === SyntaxKind.ClassDeclaration) { + return symbolAccessibilityResult.errorModuleName ? + symbolAccessibilityResult.accessibility === SymbolAccessibility.CannotBeNamed ? + Diagnostics.Public_method_0_of_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : + Diagnostics.Public_method_0_of_exported_class_has_or_is_using_name_1_from_private_module_2 : + Diagnostics.Public_method_0_of_exported_class_has_or_is_using_private_name_1; + } + else { + return symbolAccessibilityResult.errorModuleName ? + Diagnostics.Method_0_of_exported_interface_has_or_is_using_name_1_from_private_module_2 : + Diagnostics.Method_0_of_exported_interface_has_or_is_using_private_name_1; + } + } +} +/* @internal */ +export function createGetSymbolAccessibilityDiagnosticForNode(node: DeclarationDiagnosticProducing): (symbolAccessibilityResult: SymbolAccessibilityResult) => SymbolAccessibilityDiagnostic | undefined { + if (isVariableDeclaration(node) || isPropertyDeclaration(node) || isPropertySignature(node) || isPropertyAccessExpression(node) || isBindingElement(node) || isConstructorDeclaration(node)) { + return getVariableDeclarationTypeVisibilityError; + } + else if (isSetAccessor(node) || isGetAccessor(node)) { + return getAccessorDeclarationTypeVisibilityError; + } + else if (isConstructSignatureDeclaration(node) || isCallSignatureDeclaration(node) || isMethodDeclaration(node) || isMethodSignature(node) || isFunctionDeclaration(node) || isIndexSignatureDeclaration(node)) { + return getReturnTypeVisibilityError; + } + else if (isParameter(node)) { + if (isParameterPropertyDeclaration(node, node.parent) && hasModifier(node.parent, ModifierFlags.Private)) { + return getVariableDeclarationTypeVisibilityError; } - function getAccessorNameVisibilityError(symbolAccessibilityResult: SymbolAccessibilityResult) { - const diagnosticMessage = getAccessorNameVisibilityDiagnosticMessage(symbolAccessibilityResult); - return diagnosticMessage !== undefined ? { - diagnosticMessage, - errorNode: node, - typeName: (node as NamedDeclaration).name - } : undefined; + return getParameterDeclarationTypeVisibilityError; + } + else if (isTypeParameterDeclaration(node)) { + return getTypeParameterConstraintVisibilityError; + } + else if (isExpressionWithTypeArguments(node)) { + return getHeritageClauseVisibilityError; + } + else if (isImportEqualsDeclaration(node)) { + return getImportEntityNameVisibilityError; + } + else if (isTypeAliasDeclaration(node)) { + return getTypeAliasDeclarationVisibilityError; + } + else { + return Debug.assertNever(node, `Attempted to set a declaration diagnostic context for unhandled node kind: ${(ts as any).SyntaxKind[(node as any).kind]}`); + } + function getVariableDeclarationTypeVisibilityDiagnosticMessage(symbolAccessibilityResult: SymbolAccessibilityResult) { + if (node.kind === SyntaxKind.VariableDeclaration || node.kind === SyntaxKind.BindingElement) { + return symbolAccessibilityResult.errorModuleName ? + symbolAccessibilityResult.accessibility === SymbolAccessibility.CannotBeNamed ? + Diagnostics.Exported_variable_0_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : + Diagnostics.Exported_variable_0_has_or_is_using_name_1_from_private_module_2 : + Diagnostics.Exported_variable_0_has_or_is_using_private_name_1; } - - function getAccessorNameVisibilityDiagnosticMessage(symbolAccessibilityResult: SymbolAccessibilityResult) { + // This check is to ensure we don't report error on constructor parameter property as that error would be reported during parameter emit + // The only exception here is if the constructor was marked as private. we are not emitting the constructor parameters at all. + else if (node.kind === SyntaxKind.PropertyDeclaration || node.kind === SyntaxKind.PropertyAccessExpression || node.kind === SyntaxKind.PropertySignature || + (node.kind === SyntaxKind.Parameter && hasModifier(node.parent, ModifierFlags.Private))) { + // TODO(jfreeman): Deal with computed properties in error reporting. if (hasModifier(node, ModifierFlags.Static)) { return symbolAccessibilityResult.errorModuleName ? symbolAccessibilityResult.accessibility === SymbolAccessibility.CannotBeNamed ? @@ -78,7 +154,7 @@ namespace ts { Diagnostics.Public_static_property_0_of_exported_class_has_or_is_using_name_1_from_private_module_2 : Diagnostics.Public_static_property_0_of_exported_class_has_or_is_using_private_name_1; } - else if (node.parent.kind === SyntaxKind.ClassDeclaration) { + else if (node.parent.kind === SyntaxKind.ClassDeclaration || node.kind === SyntaxKind.Parameter) { return symbolAccessibilityResult.errorModuleName ? symbolAccessibilityResult.accessibility === SymbolAccessibility.CannotBeNamed ? Diagnostics.Public_property_0_of_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : @@ -86,399 +162,269 @@ namespace ts { Diagnostics.Public_property_0_of_exported_class_has_or_is_using_private_name_1; } else { + // Interfaces cannot have types that cannot be named return symbolAccessibilityResult.errorModuleName ? Diagnostics.Property_0_of_exported_interface_has_or_is_using_name_1_from_private_module_2 : Diagnostics.Property_0_of_exported_interface_has_or_is_using_private_name_1; } } - - function getMethodNameVisibilityError(symbolAccessibilityResult: SymbolAccessibilityResult): SymbolAccessibilityDiagnostic | undefined { - const diagnosticMessage = getMethodNameVisibilityDiagnosticMessage(symbolAccessibilityResult); - return diagnosticMessage !== undefined ? { - diagnosticMessage, - errorNode: node, - typeName: (node as NamedDeclaration).name - } : undefined; - } - - function getMethodNameVisibilityDiagnosticMessage(symbolAccessibilityResult: SymbolAccessibilityResult) { + } + function getVariableDeclarationTypeVisibilityError(symbolAccessibilityResult: SymbolAccessibilityResult): SymbolAccessibilityDiagnostic | undefined { + const diagnosticMessage = getVariableDeclarationTypeVisibilityDiagnosticMessage(symbolAccessibilityResult); + return diagnosticMessage !== undefined ? { + diagnosticMessage, + errorNode: node, + typeName: (node as NamedDeclaration).name + } : undefined; + } + function getAccessorDeclarationTypeVisibilityError(symbolAccessibilityResult: SymbolAccessibilityResult): SymbolAccessibilityDiagnostic { + let diagnosticMessage: DiagnosticMessage; + if (node.kind === SyntaxKind.SetAccessor) { + // Getters can infer the return type from the returned expression, but setters cannot, so the + // "_from_external_module_1_but_cannot_be_named" case cannot occur. if (hasModifier(node, ModifierFlags.Static)) { - return symbolAccessibilityResult.errorModuleName ? - symbolAccessibilityResult.accessibility === SymbolAccessibility.CannotBeNamed ? - Diagnostics.Public_static_method_0_of_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : - Diagnostics.Public_static_method_0_of_exported_class_has_or_is_using_name_1_from_private_module_2 : - Diagnostics.Public_static_method_0_of_exported_class_has_or_is_using_private_name_1; - } - else if (node.parent.kind === SyntaxKind.ClassDeclaration) { - return symbolAccessibilityResult.errorModuleName ? - symbolAccessibilityResult.accessibility === SymbolAccessibility.CannotBeNamed ? - Diagnostics.Public_method_0_of_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : - Diagnostics.Public_method_0_of_exported_class_has_or_is_using_name_1_from_private_module_2 : - Diagnostics.Public_method_0_of_exported_class_has_or_is_using_private_name_1; + diagnosticMessage = symbolAccessibilityResult.errorModuleName ? + Diagnostics.Parameter_type_of_public_static_setter_0_from_exported_class_has_or_is_using_name_1_from_private_module_2 : + Diagnostics.Parameter_type_of_public_static_setter_0_from_exported_class_has_or_is_using_private_name_1; } else { - return symbolAccessibilityResult.errorModuleName ? - Diagnostics.Method_0_of_exported_interface_has_or_is_using_name_1_from_private_module_2 : - Diagnostics.Method_0_of_exported_interface_has_or_is_using_private_name_1; - } - } - } - - export function createGetSymbolAccessibilityDiagnosticForNode(node: DeclarationDiagnosticProducing): (symbolAccessibilityResult: SymbolAccessibilityResult) => SymbolAccessibilityDiagnostic | undefined { - if (isVariableDeclaration(node) || isPropertyDeclaration(node) || isPropertySignature(node) || isPropertyAccessExpression(node) || isBindingElement(node) || isConstructorDeclaration(node)) { - return getVariableDeclarationTypeVisibilityError; - } - else if (isSetAccessor(node) || isGetAccessor(node)) { - return getAccessorDeclarationTypeVisibilityError; - } - else if (isConstructSignatureDeclaration(node) || isCallSignatureDeclaration(node) || isMethodDeclaration(node) || isMethodSignature(node) || isFunctionDeclaration(node) || isIndexSignatureDeclaration(node)) { - return getReturnTypeVisibilityError; - } - else if (isParameter(node)) { - if (isParameterPropertyDeclaration(node, node.parent) && hasModifier(node.parent, ModifierFlags.Private)) { - return getVariableDeclarationTypeVisibilityError; + diagnosticMessage = symbolAccessibilityResult.errorModuleName ? + Diagnostics.Parameter_type_of_public_setter_0_from_exported_class_has_or_is_using_name_1_from_private_module_2 : + Diagnostics.Parameter_type_of_public_setter_0_from_exported_class_has_or_is_using_private_name_1; } - return getParameterDeclarationTypeVisibilityError; - } - else if (isTypeParameterDeclaration(node)) { - return getTypeParameterConstraintVisibilityError; - } - else if (isExpressionWithTypeArguments(node)) { - return getHeritageClauseVisibilityError; - } - else if (isImportEqualsDeclaration(node)) { - return getImportEntityNameVisibilityError; - } - else if (isTypeAliasDeclaration(node)) { - return getTypeAliasDeclarationVisibilityError; } else { - return Debug.assertNever(node, `Attempted to set a declaration diagnostic context for unhandled node kind: ${(ts as any).SyntaxKind[(node as any).kind]}`); - } - - function getVariableDeclarationTypeVisibilityDiagnosticMessage(symbolAccessibilityResult: SymbolAccessibilityResult) { - if (node.kind === SyntaxKind.VariableDeclaration || node.kind === SyntaxKind.BindingElement) { - return symbolAccessibilityResult.errorModuleName ? + if (hasModifier(node, ModifierFlags.Static)) { + diagnosticMessage = symbolAccessibilityResult.errorModuleName ? symbolAccessibilityResult.accessibility === SymbolAccessibility.CannotBeNamed ? - Diagnostics.Exported_variable_0_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : - Diagnostics.Exported_variable_0_has_or_is_using_name_1_from_private_module_2 : - Diagnostics.Exported_variable_0_has_or_is_using_private_name_1; + Diagnostics.Return_type_of_public_static_getter_0_from_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : + Diagnostics.Return_type_of_public_static_getter_0_from_exported_class_has_or_is_using_name_1_from_private_module_2 : + Diagnostics.Return_type_of_public_static_getter_0_from_exported_class_has_or_is_using_private_name_1; } - // This check is to ensure we don't report error on constructor parameter property as that error would be reported during parameter emit - // The only exception here is if the constructor was marked as private. we are not emitting the constructor parameters at all. - else if (node.kind === SyntaxKind.PropertyDeclaration || node.kind === SyntaxKind.PropertyAccessExpression || node.kind === SyntaxKind.PropertySignature || - (node.kind === SyntaxKind.Parameter && hasModifier(node.parent, ModifierFlags.Private))) { - // TODO(jfreeman): Deal with computed properties in error reporting. - if (hasModifier(node, ModifierFlags.Static)) { - return symbolAccessibilityResult.errorModuleName ? - symbolAccessibilityResult.accessibility === SymbolAccessibility.CannotBeNamed ? - Diagnostics.Public_static_property_0_of_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : - Diagnostics.Public_static_property_0_of_exported_class_has_or_is_using_name_1_from_private_module_2 : - Diagnostics.Public_static_property_0_of_exported_class_has_or_is_using_private_name_1; - } - else if (node.parent.kind === SyntaxKind.ClassDeclaration || node.kind === SyntaxKind.Parameter) { - return symbolAccessibilityResult.errorModuleName ? - symbolAccessibilityResult.accessibility === SymbolAccessibility.CannotBeNamed ? - Diagnostics.Public_property_0_of_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : - Diagnostics.Public_property_0_of_exported_class_has_or_is_using_name_1_from_private_module_2 : - Diagnostics.Public_property_0_of_exported_class_has_or_is_using_private_name_1; - } - else { - // Interfaces cannot have types that cannot be named - return symbolAccessibilityResult.errorModuleName ? - Diagnostics.Property_0_of_exported_interface_has_or_is_using_name_1_from_private_module_2 : - Diagnostics.Property_0_of_exported_interface_has_or_is_using_private_name_1; - } + else { + diagnosticMessage = symbolAccessibilityResult.errorModuleName ? + symbolAccessibilityResult.accessibility === SymbolAccessibility.CannotBeNamed ? + Diagnostics.Return_type_of_public_getter_0_from_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : + Diagnostics.Return_type_of_public_getter_0_from_exported_class_has_or_is_using_name_1_from_private_module_2 : + Diagnostics.Return_type_of_public_getter_0_from_exported_class_has_or_is_using_private_name_1; } } - - function getVariableDeclarationTypeVisibilityError(symbolAccessibilityResult: SymbolAccessibilityResult): SymbolAccessibilityDiagnostic | undefined { - const diagnosticMessage = getVariableDeclarationTypeVisibilityDiagnosticMessage(symbolAccessibilityResult); - return diagnosticMessage !== undefined ? { - diagnosticMessage, - errorNode: node, - typeName: (node as NamedDeclaration).name - } : undefined; - } - - function getAccessorDeclarationTypeVisibilityError(symbolAccessibilityResult: SymbolAccessibilityResult): SymbolAccessibilityDiagnostic { - let diagnosticMessage: DiagnosticMessage; - if (node.kind === SyntaxKind.SetAccessor) { - // Getters can infer the return type from the returned expression, but setters cannot, so the - // "_from_external_module_1_but_cannot_be_named" case cannot occur. - if (hasModifier(node, ModifierFlags.Static)) { - diagnosticMessage = symbolAccessibilityResult.errorModuleName ? - Diagnostics.Parameter_type_of_public_static_setter_0_from_exported_class_has_or_is_using_name_1_from_private_module_2 : - Diagnostics.Parameter_type_of_public_static_setter_0_from_exported_class_has_or_is_using_private_name_1; - } - else { - diagnosticMessage = symbolAccessibilityResult.errorModuleName ? - Diagnostics.Parameter_type_of_public_setter_0_from_exported_class_has_or_is_using_name_1_from_private_module_2 : - Diagnostics.Parameter_type_of_public_setter_0_from_exported_class_has_or_is_using_private_name_1; - } - } - else { + return { + diagnosticMessage, + errorNode: ((node as NamedDeclaration).name!), + typeName: (node as NamedDeclaration).name + }; + } + function getReturnTypeVisibilityError(symbolAccessibilityResult: SymbolAccessibilityResult): SymbolAccessibilityDiagnostic { + let diagnosticMessage: DiagnosticMessage; + switch (node.kind) { + case SyntaxKind.ConstructSignature: + // Interfaces cannot have return types that cannot be named + diagnosticMessage = symbolAccessibilityResult.errorModuleName ? + Diagnostics.Return_type_of_constructor_signature_from_exported_interface_has_or_is_using_name_0_from_private_module_1 : + Diagnostics.Return_type_of_constructor_signature_from_exported_interface_has_or_is_using_private_name_0; + break; + case SyntaxKind.CallSignature: + // Interfaces cannot have return types that cannot be named + diagnosticMessage = symbolAccessibilityResult.errorModuleName ? + Diagnostics.Return_type_of_call_signature_from_exported_interface_has_or_is_using_name_0_from_private_module_1 : + Diagnostics.Return_type_of_call_signature_from_exported_interface_has_or_is_using_private_name_0; + break; + case SyntaxKind.IndexSignature: + // Interfaces cannot have return types that cannot be named + diagnosticMessage = symbolAccessibilityResult.errorModuleName ? + Diagnostics.Return_type_of_index_signature_from_exported_interface_has_or_is_using_name_0_from_private_module_1 : + Diagnostics.Return_type_of_index_signature_from_exported_interface_has_or_is_using_private_name_0; + break; + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: if (hasModifier(node, ModifierFlags.Static)) { diagnosticMessage = symbolAccessibilityResult.errorModuleName ? symbolAccessibilityResult.accessibility === SymbolAccessibility.CannotBeNamed ? - Diagnostics.Return_type_of_public_static_getter_0_from_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : - Diagnostics.Return_type_of_public_static_getter_0_from_exported_class_has_or_is_using_name_1_from_private_module_2 : - Diagnostics.Return_type_of_public_static_getter_0_from_exported_class_has_or_is_using_private_name_1; + Diagnostics.Return_type_of_public_static_method_from_exported_class_has_or_is_using_name_0_from_external_module_1_but_cannot_be_named : + Diagnostics.Return_type_of_public_static_method_from_exported_class_has_or_is_using_name_0_from_private_module_1 : + Diagnostics.Return_type_of_public_static_method_from_exported_class_has_or_is_using_private_name_0; } - else { + else if (node.parent.kind === SyntaxKind.ClassDeclaration) { diagnosticMessage = symbolAccessibilityResult.errorModuleName ? symbolAccessibilityResult.accessibility === SymbolAccessibility.CannotBeNamed ? - Diagnostics.Return_type_of_public_getter_0_from_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : - Diagnostics.Return_type_of_public_getter_0_from_exported_class_has_or_is_using_name_1_from_private_module_2 : - Diagnostics.Return_type_of_public_getter_0_from_exported_class_has_or_is_using_private_name_1; + Diagnostics.Return_type_of_public_method_from_exported_class_has_or_is_using_name_0_from_external_module_1_but_cannot_be_named : + Diagnostics.Return_type_of_public_method_from_exported_class_has_or_is_using_name_0_from_private_module_1 : + Diagnostics.Return_type_of_public_method_from_exported_class_has_or_is_using_private_name_0; } - } - return { - diagnosticMessage, - errorNode: (node as NamedDeclaration).name!, - typeName: (node as NamedDeclaration).name - }; - } - - function getReturnTypeVisibilityError(symbolAccessibilityResult: SymbolAccessibilityResult): SymbolAccessibilityDiagnostic { - let diagnosticMessage: DiagnosticMessage; - switch (node.kind) { - case SyntaxKind.ConstructSignature: - // Interfaces cannot have return types that cannot be named - diagnosticMessage = symbolAccessibilityResult.errorModuleName ? - Diagnostics.Return_type_of_constructor_signature_from_exported_interface_has_or_is_using_name_0_from_private_module_1 : - Diagnostics.Return_type_of_constructor_signature_from_exported_interface_has_or_is_using_private_name_0; - break; - - case SyntaxKind.CallSignature: - // Interfaces cannot have return types that cannot be named - diagnosticMessage = symbolAccessibilityResult.errorModuleName ? - Diagnostics.Return_type_of_call_signature_from_exported_interface_has_or_is_using_name_0_from_private_module_1 : - Diagnostics.Return_type_of_call_signature_from_exported_interface_has_or_is_using_private_name_0; - break; - - case SyntaxKind.IndexSignature: + else { // Interfaces cannot have return types that cannot be named diagnosticMessage = symbolAccessibilityResult.errorModuleName ? - Diagnostics.Return_type_of_index_signature_from_exported_interface_has_or_is_using_name_0_from_private_module_1 : - Diagnostics.Return_type_of_index_signature_from_exported_interface_has_or_is_using_private_name_0; - break; - - case SyntaxKind.MethodDeclaration: - case SyntaxKind.MethodSignature: - if (hasModifier(node, ModifierFlags.Static)) { - diagnosticMessage = symbolAccessibilityResult.errorModuleName ? - symbolAccessibilityResult.accessibility === SymbolAccessibility.CannotBeNamed ? - Diagnostics.Return_type_of_public_static_method_from_exported_class_has_or_is_using_name_0_from_external_module_1_but_cannot_be_named : - Diagnostics.Return_type_of_public_static_method_from_exported_class_has_or_is_using_name_0_from_private_module_1 : - Diagnostics.Return_type_of_public_static_method_from_exported_class_has_or_is_using_private_name_0; - } - else if (node.parent.kind === SyntaxKind.ClassDeclaration) { - diagnosticMessage = symbolAccessibilityResult.errorModuleName ? - symbolAccessibilityResult.accessibility === SymbolAccessibility.CannotBeNamed ? - Diagnostics.Return_type_of_public_method_from_exported_class_has_or_is_using_name_0_from_external_module_1_but_cannot_be_named : - Diagnostics.Return_type_of_public_method_from_exported_class_has_or_is_using_name_0_from_private_module_1 : - Diagnostics.Return_type_of_public_method_from_exported_class_has_or_is_using_private_name_0; - } - else { - // Interfaces cannot have return types that cannot be named - diagnosticMessage = symbolAccessibilityResult.errorModuleName ? - Diagnostics.Return_type_of_method_from_exported_interface_has_or_is_using_name_0_from_private_module_1 : - Diagnostics.Return_type_of_method_from_exported_interface_has_or_is_using_private_name_0; - } - break; - - case SyntaxKind.FunctionDeclaration: - diagnosticMessage = symbolAccessibilityResult.errorModuleName ? - symbolAccessibilityResult.accessibility === SymbolAccessibility.CannotBeNamed ? - Diagnostics.Return_type_of_exported_function_has_or_is_using_name_0_from_external_module_1_but_cannot_be_named : - Diagnostics.Return_type_of_exported_function_has_or_is_using_name_0_from_private_module_1 : - Diagnostics.Return_type_of_exported_function_has_or_is_using_private_name_0; - break; - - default: - return Debug.fail("This is unknown kind for signature: " + node.kind); - } - - return { - diagnosticMessage, - errorNode: (node as NamedDeclaration).name || node - }; - } - - function getParameterDeclarationTypeVisibilityError(symbolAccessibilityResult: SymbolAccessibilityResult): SymbolAccessibilityDiagnostic | undefined { - const diagnosticMessage: DiagnosticMessage = getParameterDeclarationTypeVisibilityDiagnosticMessage(symbolAccessibilityResult); - return diagnosticMessage !== undefined ? { - diagnosticMessage, - errorNode: node, - typeName: (node as NamedDeclaration).name - } : undefined; + Diagnostics.Return_type_of_method_from_exported_interface_has_or_is_using_name_0_from_private_module_1 : + Diagnostics.Return_type_of_method_from_exported_interface_has_or_is_using_private_name_0; + } + break; + case SyntaxKind.FunctionDeclaration: + diagnosticMessage = symbolAccessibilityResult.errorModuleName ? + symbolAccessibilityResult.accessibility === SymbolAccessibility.CannotBeNamed ? + Diagnostics.Return_type_of_exported_function_has_or_is_using_name_0_from_external_module_1_but_cannot_be_named : + Diagnostics.Return_type_of_exported_function_has_or_is_using_name_0_from_private_module_1 : + Diagnostics.Return_type_of_exported_function_has_or_is_using_private_name_0; + break; + default: + return Debug.fail("This is unknown kind for signature: " + node.kind); } - - function getParameterDeclarationTypeVisibilityDiagnosticMessage(symbolAccessibilityResult: SymbolAccessibilityResult): DiagnosticMessage { - switch (node.parent.kind) { - case SyntaxKind.Constructor: + return { + diagnosticMessage, + errorNode: (node as NamedDeclaration).name || node + }; + } + function getParameterDeclarationTypeVisibilityError(symbolAccessibilityResult: SymbolAccessibilityResult): SymbolAccessibilityDiagnostic | undefined { + const diagnosticMessage: DiagnosticMessage = getParameterDeclarationTypeVisibilityDiagnosticMessage(symbolAccessibilityResult); + return diagnosticMessage !== undefined ? { + diagnosticMessage, + errorNode: node, + typeName: (node as NamedDeclaration).name + } : undefined; + } + function getParameterDeclarationTypeVisibilityDiagnosticMessage(symbolAccessibilityResult: SymbolAccessibilityResult): DiagnosticMessage { + switch (node.parent.kind) { + case SyntaxKind.Constructor: + return symbolAccessibilityResult.errorModuleName ? + symbolAccessibilityResult.accessibility === SymbolAccessibility.CannotBeNamed ? + Diagnostics.Parameter_0_of_constructor_from_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : + Diagnostics.Parameter_0_of_constructor_from_exported_class_has_or_is_using_name_1_from_private_module_2 : + Diagnostics.Parameter_0_of_constructor_from_exported_class_has_or_is_using_private_name_1; + case SyntaxKind.ConstructSignature: + case SyntaxKind.ConstructorType: + // Interfaces cannot have parameter types that cannot be named + return symbolAccessibilityResult.errorModuleName ? + Diagnostics.Parameter_0_of_constructor_signature_from_exported_interface_has_or_is_using_name_1_from_private_module_2 : + Diagnostics.Parameter_0_of_constructor_signature_from_exported_interface_has_or_is_using_private_name_1; + case SyntaxKind.CallSignature: + // Interfaces cannot have parameter types that cannot be named + return symbolAccessibilityResult.errorModuleName ? + Diagnostics.Parameter_0_of_call_signature_from_exported_interface_has_or_is_using_name_1_from_private_module_2 : + Diagnostics.Parameter_0_of_call_signature_from_exported_interface_has_or_is_using_private_name_1; + case SyntaxKind.IndexSignature: + // Interfaces cannot have parameter types that cannot be named + return symbolAccessibilityResult.errorModuleName ? + Diagnostics.Parameter_0_of_index_signature_from_exported_interface_has_or_is_using_name_1_from_private_module_2 : + Diagnostics.Parameter_0_of_index_signature_from_exported_interface_has_or_is_using_private_name_1; + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: + if (hasModifier(node.parent, ModifierFlags.Static)) { return symbolAccessibilityResult.errorModuleName ? symbolAccessibilityResult.accessibility === SymbolAccessibility.CannotBeNamed ? - Diagnostics.Parameter_0_of_constructor_from_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : - Diagnostics.Parameter_0_of_constructor_from_exported_class_has_or_is_using_name_1_from_private_module_2 : - Diagnostics.Parameter_0_of_constructor_from_exported_class_has_or_is_using_private_name_1; - - case SyntaxKind.ConstructSignature: - case SyntaxKind.ConstructorType: - // Interfaces cannot have parameter types that cannot be named - return symbolAccessibilityResult.errorModuleName ? - Diagnostics.Parameter_0_of_constructor_signature_from_exported_interface_has_or_is_using_name_1_from_private_module_2 : - Diagnostics.Parameter_0_of_constructor_signature_from_exported_interface_has_or_is_using_private_name_1; - - case SyntaxKind.CallSignature: - // Interfaces cannot have parameter types that cannot be named - return symbolAccessibilityResult.errorModuleName ? - Diagnostics.Parameter_0_of_call_signature_from_exported_interface_has_or_is_using_name_1_from_private_module_2 : - Diagnostics.Parameter_0_of_call_signature_from_exported_interface_has_or_is_using_private_name_1; - - case SyntaxKind.IndexSignature: - // Interfaces cannot have parameter types that cannot be named - return symbolAccessibilityResult.errorModuleName ? - Diagnostics.Parameter_0_of_index_signature_from_exported_interface_has_or_is_using_name_1_from_private_module_2 : - Diagnostics.Parameter_0_of_index_signature_from_exported_interface_has_or_is_using_private_name_1; - - case SyntaxKind.MethodDeclaration: - case SyntaxKind.MethodSignature: - if (hasModifier(node.parent, ModifierFlags.Static)) { - return symbolAccessibilityResult.errorModuleName ? - symbolAccessibilityResult.accessibility === SymbolAccessibility.CannotBeNamed ? - Diagnostics.Parameter_0_of_public_static_method_from_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : - Diagnostics.Parameter_0_of_public_static_method_from_exported_class_has_or_is_using_name_1_from_private_module_2 : - Diagnostics.Parameter_0_of_public_static_method_from_exported_class_has_or_is_using_private_name_1; - } - else if (node.parent.parent.kind === SyntaxKind.ClassDeclaration) { - return symbolAccessibilityResult.errorModuleName ? - symbolAccessibilityResult.accessibility === SymbolAccessibility.CannotBeNamed ? - Diagnostics.Parameter_0_of_public_method_from_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : - Diagnostics.Parameter_0_of_public_method_from_exported_class_has_or_is_using_name_1_from_private_module_2 : - Diagnostics.Parameter_0_of_public_method_from_exported_class_has_or_is_using_private_name_1; - } - else { - // Interfaces cannot have parameter types that cannot be named - return symbolAccessibilityResult.errorModuleName ? - Diagnostics.Parameter_0_of_method_from_exported_interface_has_or_is_using_name_1_from_private_module_2 : - Diagnostics.Parameter_0_of_method_from_exported_interface_has_or_is_using_private_name_1; - } - - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.FunctionType: + Diagnostics.Parameter_0_of_public_static_method_from_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : + Diagnostics.Parameter_0_of_public_static_method_from_exported_class_has_or_is_using_name_1_from_private_module_2 : + Diagnostics.Parameter_0_of_public_static_method_from_exported_class_has_or_is_using_private_name_1; + } + else if (node.parent.parent.kind === SyntaxKind.ClassDeclaration) { return symbolAccessibilityResult.errorModuleName ? symbolAccessibilityResult.accessibility === SymbolAccessibility.CannotBeNamed ? - Diagnostics.Parameter_0_of_exported_function_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : - Diagnostics.Parameter_0_of_exported_function_has_or_is_using_name_1_from_private_module_2 : - Diagnostics.Parameter_0_of_exported_function_has_or_is_using_private_name_1; - case SyntaxKind.SetAccessor: - case SyntaxKind.GetAccessor: + Diagnostics.Parameter_0_of_public_method_from_exported_class_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : + Diagnostics.Parameter_0_of_public_method_from_exported_class_has_or_is_using_name_1_from_private_module_2 : + Diagnostics.Parameter_0_of_public_method_from_exported_class_has_or_is_using_private_name_1; + } + else { + // Interfaces cannot have parameter types that cannot be named return symbolAccessibilityResult.errorModuleName ? - symbolAccessibilityResult.accessibility === SymbolAccessibility.CannotBeNamed ? - Diagnostics.Parameter_0_of_accessor_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : - Diagnostics.Parameter_0_of_accessor_has_or_is_using_name_1_from_private_module_2 : - Diagnostics.Parameter_0_of_accessor_has_or_is_using_private_name_1; - - default: - return Debug.fail(`Unknown parent for parameter: ${(ts as any).SyntaxKind[node.parent.kind]}`); - } - } - - function getTypeParameterConstraintVisibilityError(): SymbolAccessibilityDiagnostic { - // Type parameter constraints are named by user so we should always be able to name it - let diagnosticMessage: DiagnosticMessage; - switch (node.parent.kind) { - case SyntaxKind.ClassDeclaration: - diagnosticMessage = Diagnostics.Type_parameter_0_of_exported_class_has_or_is_using_private_name_1; - break; - - case SyntaxKind.InterfaceDeclaration: - diagnosticMessage = Diagnostics.Type_parameter_0_of_exported_interface_has_or_is_using_private_name_1; - break; - - case SyntaxKind.MappedType: - diagnosticMessage = Diagnostics.Type_parameter_0_of_exported_mapped_object_type_is_using_private_name_1; - break; - - case SyntaxKind.ConstructorType: - case SyntaxKind.ConstructSignature: - diagnosticMessage = Diagnostics.Type_parameter_0_of_constructor_signature_from_exported_interface_has_or_is_using_private_name_1; - break; - - case SyntaxKind.CallSignature: - diagnosticMessage = Diagnostics.Type_parameter_0_of_call_signature_from_exported_interface_has_or_is_using_private_name_1; - break; - - case SyntaxKind.MethodDeclaration: - case SyntaxKind.MethodSignature: - if (hasModifier(node.parent, ModifierFlags.Static)) { - diagnosticMessage = Diagnostics.Type_parameter_0_of_public_static_method_from_exported_class_has_or_is_using_private_name_1; - } - else if (node.parent.parent.kind === SyntaxKind.ClassDeclaration) { - diagnosticMessage = Diagnostics.Type_parameter_0_of_public_method_from_exported_class_has_or_is_using_private_name_1; - } - else { - diagnosticMessage = Diagnostics.Type_parameter_0_of_method_from_exported_interface_has_or_is_using_private_name_1; - } - break; - - case SyntaxKind.FunctionType: - case SyntaxKind.FunctionDeclaration: - diagnosticMessage = Diagnostics.Type_parameter_0_of_exported_function_has_or_is_using_private_name_1; - break; - - case SyntaxKind.TypeAliasDeclaration: - diagnosticMessage = Diagnostics.Type_parameter_0_of_exported_type_alias_has_or_is_using_private_name_1; - break; - - default: - return Debug.fail("This is unknown parent for type parameter: " + node.parent.kind); - } - - return { - diagnosticMessage, - errorNode: node, - typeName: (node as NamedDeclaration).name - }; + Diagnostics.Parameter_0_of_method_from_exported_interface_has_or_is_using_name_1_from_private_module_2 : + Diagnostics.Parameter_0_of_method_from_exported_interface_has_or_is_using_private_name_1; + } + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.FunctionType: + return symbolAccessibilityResult.errorModuleName ? + symbolAccessibilityResult.accessibility === SymbolAccessibility.CannotBeNamed ? + Diagnostics.Parameter_0_of_exported_function_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : + Diagnostics.Parameter_0_of_exported_function_has_or_is_using_name_1_from_private_module_2 : + Diagnostics.Parameter_0_of_exported_function_has_or_is_using_private_name_1; + case SyntaxKind.SetAccessor: + case SyntaxKind.GetAccessor: + return symbolAccessibilityResult.errorModuleName ? + symbolAccessibilityResult.accessibility === SymbolAccessibility.CannotBeNamed ? + Diagnostics.Parameter_0_of_accessor_has_or_is_using_name_1_from_external_module_2_but_cannot_be_named : + Diagnostics.Parameter_0_of_accessor_has_or_is_using_name_1_from_private_module_2 : + Diagnostics.Parameter_0_of_accessor_has_or_is_using_private_name_1; + default: + return Debug.fail(`Unknown parent for parameter: ${(ts as any).SyntaxKind[node.parent.kind]}`); } - - function getHeritageClauseVisibilityError(): SymbolAccessibilityDiagnostic { - let diagnosticMessage: DiagnosticMessage; - // Heritage clause is written by user so it can always be named - if (node.parent.parent.kind === SyntaxKind.ClassDeclaration) { - // Class or Interface implemented/extended is inaccessible - diagnosticMessage = isHeritageClause(node.parent) && node.parent.token === SyntaxKind.ImplementsKeyword ? - Diagnostics.Implements_clause_of_exported_class_0_has_or_is_using_private_name_1 : - Diagnostics.extends_clause_of_exported_class_0_has_or_is_using_private_name_1; - } - else { - // interface is inaccessible - diagnosticMessage = Diagnostics.extends_clause_of_exported_interface_0_has_or_is_using_private_name_1; - } - - return { - diagnosticMessage, - errorNode: node, - typeName: getNameOfDeclaration(node.parent.parent as Declaration) - }; + } + function getTypeParameterConstraintVisibilityError(): SymbolAccessibilityDiagnostic { + // Type parameter constraints are named by user so we should always be able to name it + let diagnosticMessage: DiagnosticMessage; + switch (node.parent.kind) { + case SyntaxKind.ClassDeclaration: + diagnosticMessage = Diagnostics.Type_parameter_0_of_exported_class_has_or_is_using_private_name_1; + break; + case SyntaxKind.InterfaceDeclaration: + diagnosticMessage = Diagnostics.Type_parameter_0_of_exported_interface_has_or_is_using_private_name_1; + break; + case SyntaxKind.MappedType: + diagnosticMessage = Diagnostics.Type_parameter_0_of_exported_mapped_object_type_is_using_private_name_1; + break; + case SyntaxKind.ConstructorType: + case SyntaxKind.ConstructSignature: + diagnosticMessage = Diagnostics.Type_parameter_0_of_constructor_signature_from_exported_interface_has_or_is_using_private_name_1; + break; + case SyntaxKind.CallSignature: + diagnosticMessage = Diagnostics.Type_parameter_0_of_call_signature_from_exported_interface_has_or_is_using_private_name_1; + break; + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: + if (hasModifier(node.parent, ModifierFlags.Static)) { + diagnosticMessage = Diagnostics.Type_parameter_0_of_public_static_method_from_exported_class_has_or_is_using_private_name_1; + } + else if (node.parent.parent.kind === SyntaxKind.ClassDeclaration) { + diagnosticMessage = Diagnostics.Type_parameter_0_of_public_method_from_exported_class_has_or_is_using_private_name_1; + } + else { + diagnosticMessage = Diagnostics.Type_parameter_0_of_method_from_exported_interface_has_or_is_using_private_name_1; + } + break; + case SyntaxKind.FunctionType: + case SyntaxKind.FunctionDeclaration: + diagnosticMessage = Diagnostics.Type_parameter_0_of_exported_function_has_or_is_using_private_name_1; + break; + case SyntaxKind.TypeAliasDeclaration: + diagnosticMessage = Diagnostics.Type_parameter_0_of_exported_type_alias_has_or_is_using_private_name_1; + break; + default: + return Debug.fail("This is unknown parent for type parameter: " + node.parent.kind); } - - function getImportEntityNameVisibilityError(): SymbolAccessibilityDiagnostic { - return { - diagnosticMessage: Diagnostics.Import_declaration_0_is_using_private_name_1, - errorNode: node, - typeName: (node as NamedDeclaration).name - }; + return { + diagnosticMessage, + errorNode: node, + typeName: (node as NamedDeclaration).name + }; + } + function getHeritageClauseVisibilityError(): SymbolAccessibilityDiagnostic { + let diagnosticMessage: DiagnosticMessage; + // Heritage clause is written by user so it can always be named + if (node.parent.parent.kind === SyntaxKind.ClassDeclaration) { + // Class or Interface implemented/extended is inaccessible + diagnosticMessage = isHeritageClause(node.parent) && node.parent.token === SyntaxKind.ImplementsKeyword ? + Diagnostics.Implements_clause_of_exported_class_0_has_or_is_using_private_name_1 : + Diagnostics.extends_clause_of_exported_class_0_has_or_is_using_private_name_1; } - - function getTypeAliasDeclarationVisibilityError(): SymbolAccessibilityDiagnostic { - return { - diagnosticMessage: Diagnostics.Exported_type_alias_0_has_or_is_using_private_name_1, - errorNode: (node as TypeAliasDeclaration).type, - typeName: (node as TypeAliasDeclaration).name - }; + else { + // interface is inaccessible + diagnosticMessage = Diagnostics.extends_clause_of_exported_interface_0_has_or_is_using_private_name_1; } + return { + diagnosticMessage, + errorNode: node, + typeName: getNameOfDeclaration((node.parent.parent as Declaration)) + }; + } + function getImportEntityNameVisibilityError(): SymbolAccessibilityDiagnostic { + return { + diagnosticMessage: Diagnostics.Import_declaration_0_is_using_private_name_1, + errorNode: node, + typeName: (node as NamedDeclaration).name + }; + } + function getTypeAliasDeclarationVisibilityError(): SymbolAccessibilityDiagnostic { + return { + diagnosticMessage: Diagnostics.Exported_type_alias_0_has_or_is_using_private_name_1, + errorNode: (node as TypeAliasDeclaration).type, + typeName: (node as TypeAliasDeclaration).name + }; } } diff --git a/src/compiler/transformers/destructuring.ts b/src/compiler/transformers/destructuring.ts index f3c3593292609..dd402d5f79e48 100644 --- a/src/compiler/transformers/destructuring.ts +++ b/src/compiler/transformers/destructuring.ts @@ -1,537 +1,493 @@ +import { TransformationContext, Expression, BindingOrAssignmentElementTarget, TextRange, Node, BindingOrAssignmentElement, ArrayBindingOrAssignmentPattern, ObjectBindingOrAssignmentPattern, Identifier, VisitResult, VariableDeclaration, DestructuringAssignment, isDestructuringAssignment, isEmptyArrayLiteral, isEmptyObjectLiteral, visitNode, isExpression, isIdentifier, nodeIsSynthesized, some, aggregateTransformFlags, inlineExpressions, createOmittedExpression, setEmitFlags, EmitFlags, append, Debug, setTextRange, createAssignment, __String, getTargetOfBindingOrAssignmentElement, isBindingOrAssignmentPattern, BindingOrAssignmentPattern, getElementsOfBindingOrAssignmentPattern, tryGetPropertyNameOfBindingOrAssignmentElement, isComputedPropertyName, isLiteralExpression, forEach, ParameterDeclaration, BindingName, isVariableDeclaration, getInitializerOfBindingOrAssignmentElement, updateVariableDeclaration, createTempVariable, last, addRange, createVariableDeclaration, isBindingName, createVoidZero, isObjectBindingOrAssignmentPattern, isArrayBindingOrAssignmentPattern, isDeclarationBindingElement, getRestIndicatorOfBindingOrAssignmentElement, getPropertyNameOfBindingOrAssignmentElement, TransformFlags, ElementAccessExpression, createReadHelper, every, isOmittedExpression, createElementAccess, createArraySlice, createConditional, createTypeCheck, PropertyName, LeftHandSideExpression, isStringOrNumericLiteralLike, getSynthesizedClone, createIdentifier, idText, createPropertyAccess, isArrayBindingElement, createArrayBindingPattern, ArrayBindingElement, createArrayLiteral, map, convertToArrayAssignmentElement, isBindingElement, createObjectBindingPattern, BindingElement, createObjectLiteral, convertToObjectAssignmentElement, createBindingElement, UnscopedEmitHelper, createAdd, createLiteral, createCall, getUnscopedHelperName } from "../ts"; /*@internal*/ -namespace ts { - interface FlattenContext { - context: TransformationContext; - level: FlattenLevel; - downlevelIteration: boolean; - hoistTempVariables: boolean; - emitExpression: (value: Expression) => void; - emitBindingOrAssignment: (target: BindingOrAssignmentElementTarget, value: Expression, location: TextRange, original: Node | undefined) => void; - createArrayBindingOrAssignmentPattern: (elements: BindingOrAssignmentElement[]) => ArrayBindingOrAssignmentPattern; - createObjectBindingOrAssignmentPattern: (elements: BindingOrAssignmentElement[]) => ObjectBindingOrAssignmentPattern; - createArrayBindingOrAssignmentElement: (node: Identifier) => BindingOrAssignmentElement; - visitor?: (node: Node) => VisitResult; - } - - export const enum FlattenLevel { - All, - ObjectRest, - } - - /** - * Flattens a DestructuringAssignment or a VariableDeclaration to an expression. - * - * @param node The node to flatten. - * @param visitor An optional visitor used to visit initializers. - * @param context The transformation context. - * @param level Indicates the extent to which flattening should occur. - * @param needsValue An optional value indicating whether the value from the right-hand-side of - * the destructuring assignment is needed as part of a larger expression. - * @param createAssignmentCallback An optional callback used to create the assignment expression. - */ - export function flattenDestructuringAssignment( - node: VariableDeclaration | DestructuringAssignment, - visitor: ((node: Node) => VisitResult) | undefined, - context: TransformationContext, - level: FlattenLevel, - needsValue?: boolean, - createAssignmentCallback?: (name: Identifier, value: Expression, location?: TextRange) => Expression): Expression { - let location: TextRange = node; - let value: Expression | undefined; - if (isDestructuringAssignment(node)) { - value = node.right; - while (isEmptyArrayLiteral(node.left) || isEmptyObjectLiteral(node.left)) { - if (isDestructuringAssignment(value)) { - location = node = value; - value = node.right; - } - else { - return visitNode(value, visitor, isExpression); - } - } - } - - let expressions: Expression[] | undefined; - const flattenContext: FlattenContext = { - context, - level, - downlevelIteration: !!context.getCompilerOptions().downlevelIteration, - hoistTempVariables: true, - emitExpression, - emitBindingOrAssignment, - createArrayBindingOrAssignmentPattern: makeArrayAssignmentPattern, - createObjectBindingOrAssignmentPattern: makeObjectAssignmentPattern, - createArrayBindingOrAssignmentElement: makeAssignmentElement, - visitor - }; - - if (value) { - value = visitNode(value, visitor, isExpression); - - if (isIdentifier(value) && bindingOrAssignmentElementAssignsToName(node, value.escapedText) || - bindingOrAssignmentElementContainsNonLiteralComputedName(node)) { - // If the right-hand value of the assignment is also an assignment target then - // we need to cache the right-hand value. - value = ensureIdentifier(flattenContext, value, /*reuseIdentifierExpressions*/ false, location); - } - else if (needsValue) { - // If the right-hand value of the destructuring assignment needs to be preserved (as - // is the case when the destructuring assignment is part of a larger expression), - // then we need to cache the right-hand value. - // - // The source map location for the assignment should point to the entire binary - // expression. - value = ensureIdentifier(flattenContext, value, /*reuseIdentifierExpressions*/ true, location); - } - else if (nodeIsSynthesized(node)) { - // Generally, the source map location for a destructuring assignment is the root - // expression. - // - // However, if the root expression is synthesized (as in the case - // of the initializer when transforming a ForOfStatement), then the source map - // location should point to the right-hand value of the expression. - location = value; +interface FlattenContext { + context: TransformationContext; + level: FlattenLevel; + downlevelIteration: boolean; + hoistTempVariables: boolean; + emitExpression: (value: Expression) => void; + emitBindingOrAssignment: (target: BindingOrAssignmentElementTarget, value: Expression, location: TextRange, original: Node | undefined) => void; + createArrayBindingOrAssignmentPattern: (elements: BindingOrAssignmentElement[]) => ArrayBindingOrAssignmentPattern; + createObjectBindingOrAssignmentPattern: (elements: BindingOrAssignmentElement[]) => ObjectBindingOrAssignmentPattern; + createArrayBindingOrAssignmentElement: (node: Identifier) => BindingOrAssignmentElement; + visitor?: (node: Node) => VisitResult; +} +/* @internal */ +export const enum FlattenLevel { + All, + ObjectRest +} +/** + * Flattens a DestructuringAssignment or a VariableDeclaration to an expression. + * + * @param node The node to flatten. + * @param visitor An optional visitor used to visit initializers. + * @param context The transformation context. + * @param level Indicates the extent to which flattening should occur. + * @param needsValue An optional value indicating whether the value from the right-hand-side of + * the destructuring assignment is needed as part of a larger expression. + * @param createAssignmentCallback An optional callback used to create the assignment expression. + */ +/* @internal */ +export function flattenDestructuringAssignment(node: VariableDeclaration | DestructuringAssignment, visitor: ((node: Node) => VisitResult) | undefined, context: TransformationContext, level: FlattenLevel, needsValue?: boolean, createAssignmentCallback?: (name: Identifier, value: Expression, location?: TextRange) => Expression): Expression { + let location: TextRange = node; + let value: Expression | undefined; + if (isDestructuringAssignment(node)) { + value = node.right; + while (isEmptyArrayLiteral(node.left) || isEmptyObjectLiteral(node.left)) { + if (isDestructuringAssignment(value)) { + location = node = value; + value = node.right; } - } - - flattenBindingOrAssignmentElement(flattenContext, node, value, location, /*skipInitializer*/ isDestructuringAssignment(node)); - - if (value && needsValue) { - if (!some(expressions)) { - return value; + else { + return visitNode(value, visitor, isExpression); } - - expressions.push(value); - } - - return aggregateTransformFlags(inlineExpressions(expressions!)) || createOmittedExpression(); - - function emitExpression(expression: Expression) { - // NOTE: this completely disables source maps, but aligns with the behavior of - // `emitAssignment` in the old emitter. - setEmitFlags(expression, EmitFlags.NoNestedSourceMaps); - aggregateTransformFlags(expression); - expressions = append(expressions, expression); - } - - function emitBindingOrAssignment(target: BindingOrAssignmentElementTarget, value: Expression, location: TextRange, original: Node) { - Debug.assertNode(target, createAssignmentCallback ? isIdentifier : isExpression); - const expression = createAssignmentCallback - ? createAssignmentCallback(target, value, location) - : setTextRange( - createAssignment(visitNode(target, visitor, isExpression), value), - location - ); - expression.original = original; - emitExpression(expression); } } - - function bindingOrAssignmentElementAssignsToName(element: BindingOrAssignmentElement, escapedName: __String): boolean { - const target = getTargetOfBindingOrAssignmentElement(element)!; // TODO: GH#18217 - if (isBindingOrAssignmentPattern(target)) { - return bindingOrAssignmentPatternAssignsToName(target, escapedName); - } - else if (isIdentifier(target)) { - return target.escapedText === escapedName; + let expressions: Expression[] | undefined; + const flattenContext: FlattenContext = { + context, + level, + downlevelIteration: !!context.getCompilerOptions().downlevelIteration, + hoistTempVariables: true, + emitExpression, + emitBindingOrAssignment, + createArrayBindingOrAssignmentPattern: makeArrayAssignmentPattern, + createObjectBindingOrAssignmentPattern: makeObjectAssignmentPattern, + createArrayBindingOrAssignmentElement: makeAssignmentElement, + visitor + }; + if (value) { + value = visitNode(value, visitor, isExpression); + if (isIdentifier(value) && bindingOrAssignmentElementAssignsToName(node, value.escapedText) || + bindingOrAssignmentElementContainsNonLiteralComputedName(node)) { + // If the right-hand value of the assignment is also an assignment target then + // we need to cache the right-hand value. + value = ensureIdentifier(flattenContext, value, /*reuseIdentifierExpressions*/ false, location); + } + else if (needsValue) { + // If the right-hand value of the destructuring assignment needs to be preserved (as + // is the case when the destructuring assignment is part of a larger expression), + // then we need to cache the right-hand value. + // + // The source map location for the assignment should point to the entire binary + // expression. + value = ensureIdentifier(flattenContext, value, /*reuseIdentifierExpressions*/ true, location); + } + else if (nodeIsSynthesized(node)) { + // Generally, the source map location for a destructuring assignment is the root + // expression. + // + // However, if the root expression is synthesized (as in the case + // of the initializer when transforming a ForOfStatement), then the source map + // location should point to the right-hand value of the expression. + location = value; } - return false; - } - - function bindingOrAssignmentPatternAssignsToName(pattern: BindingOrAssignmentPattern, escapedName: __String): boolean { - const elements = getElementsOfBindingOrAssignmentPattern(pattern); - for (const element of elements) { - if (bindingOrAssignmentElementAssignsToName(element, escapedName)) { - return true; - } + } + flattenBindingOrAssignmentElement(flattenContext, node, value, location, /*skipInitializer*/ isDestructuringAssignment(node)); + if (value && needsValue) { + if (!some(expressions)) { + return value; } - return false; + expressions.push(value); + } + return aggregateTransformFlags(inlineExpressions((expressions!))) || createOmittedExpression(); + function emitExpression(expression: Expression) { + // NOTE: this completely disables source maps, but aligns with the behavior of + // `emitAssignment` in the old emitter. + setEmitFlags(expression, EmitFlags.NoNestedSourceMaps); + aggregateTransformFlags(expression); + expressions = append(expressions, expression); + } + function emitBindingOrAssignment(target: BindingOrAssignmentElementTarget, value: Expression, location: TextRange, original: Node) { + Debug.assertNode(target, createAssignmentCallback ? isIdentifier : isExpression); + const expression = createAssignmentCallback + ? createAssignmentCallback((target), value, location) + : setTextRange(createAssignment(visitNode((target), visitor, isExpression), value), location); + expression.original = original; + emitExpression(expression); + } +} +/* @internal */ +function bindingOrAssignmentElementAssignsToName(element: BindingOrAssignmentElement, escapedName: __String): boolean { + const target = (getTargetOfBindingOrAssignmentElement(element)!); // TODO: GH#18217 + if (isBindingOrAssignmentPattern(target)) { + return bindingOrAssignmentPatternAssignsToName(target, escapedName); } - - function bindingOrAssignmentElementContainsNonLiteralComputedName(element: BindingOrAssignmentElement): boolean { - const propertyName = tryGetPropertyNameOfBindingOrAssignmentElement(element); - if (propertyName && isComputedPropertyName(propertyName) && !isLiteralExpression(propertyName.expression)) { + else if (isIdentifier(target)) { + return target.escapedText === escapedName; + } + return false; +} +/* @internal */ +function bindingOrAssignmentPatternAssignsToName(pattern: BindingOrAssignmentPattern, escapedName: __String): boolean { + const elements = getElementsOfBindingOrAssignmentPattern(pattern); + for (const element of elements) { + if (bindingOrAssignmentElementAssignsToName(element, escapedName)) { return true; } - const target = getTargetOfBindingOrAssignmentElement(element); - return !!target && isBindingOrAssignmentPattern(target) && bindingOrAssignmentPatternContainsNonLiteralComputedName(target); - } - - function bindingOrAssignmentPatternContainsNonLiteralComputedName(pattern: BindingOrAssignmentPattern): boolean { - return !!forEach(getElementsOfBindingOrAssignmentPattern(pattern), bindingOrAssignmentElementContainsNonLiteralComputedName); - } - - /** - * Flattens a VariableDeclaration or ParameterDeclaration to one or more variable declarations. - * - * @param node The node to flatten. - * @param visitor An optional visitor used to visit initializers. - * @param context The transformation context. - * @param boundValue The value bound to the declaration. - * @param skipInitializer A value indicating whether to ignore the initializer of `node`. - * @param hoistTempVariables Indicates whether temporary variables should not be recorded in-line. - * @param level Indicates the extent to which flattening should occur. - */ - export function flattenDestructuringBinding( - node: VariableDeclaration | ParameterDeclaration, - visitor: (node: Node) => VisitResult, - context: TransformationContext, - level: FlattenLevel, - rval?: Expression, - hoistTempVariables = false, - skipInitializer?: boolean): VariableDeclaration[] { - let pendingExpressions: Expression[] | undefined; - const pendingDeclarations: { pendingExpressions?: Expression[], name: BindingName, value: Expression, location?: TextRange, original?: Node; }[] = []; - const declarations: VariableDeclaration[] = []; - const flattenContext: FlattenContext = { - context, - level, - downlevelIteration: !!context.getCompilerOptions().downlevelIteration, - hoistTempVariables, - emitExpression, - emitBindingOrAssignment, - createArrayBindingOrAssignmentPattern: makeArrayBindingPattern, - createObjectBindingOrAssignmentPattern: makeObjectBindingPattern, - createArrayBindingOrAssignmentElement: makeBindingElement, - visitor - }; - - if (isVariableDeclaration(node)) { - let initializer = getInitializerOfBindingOrAssignmentElement(node); - if (initializer && (isIdentifier(initializer) && bindingOrAssignmentElementAssignsToName(node, initializer.escapedText) || - bindingOrAssignmentElementContainsNonLiteralComputedName(node))) { - // If the right-hand value of the assignment is also an assignment target then - // we need to cache the right-hand value. - initializer = ensureIdentifier(flattenContext, initializer, /*reuseIdentifierExpressions*/ false, initializer); - node = updateVariableDeclaration(node, node.name, node.type, initializer); - } - } - - flattenBindingOrAssignmentElement(flattenContext, node, rval, node, skipInitializer); - if (pendingExpressions) { - const temp = createTempVariable(/*recordTempVariable*/ undefined); - if (hoistTempVariables) { - const value = inlineExpressions(pendingExpressions); - pendingExpressions = undefined; - emitBindingOrAssignment(temp, value, /*location*/ undefined, /*original*/ undefined); - } - else { - context.hoistVariableDeclaration(temp); - const pendingDeclaration = last(pendingDeclarations); - pendingDeclaration.pendingExpressions = append( - pendingDeclaration.pendingExpressions, - createAssignment(temp, pendingDeclaration.value) - ); - addRange(pendingDeclaration.pendingExpressions, pendingExpressions); - pendingDeclaration.value = temp; - } - } - for (const { pendingExpressions, name, value, location, original } of pendingDeclarations) { - const variable = createVariableDeclaration( - name, - /*type*/ undefined, - pendingExpressions ? inlineExpressions(append(pendingExpressions, value)) : value - ); - variable.original = original; - setTextRange(variable, location); - if (isIdentifier(name)) { - setEmitFlags(variable, EmitFlags.NoNestedSourceMaps); - } - aggregateTransformFlags(variable); - declarations.push(variable); - } - return declarations; - - function emitExpression(value: Expression) { - pendingExpressions = append(pendingExpressions, value); - } - - function emitBindingOrAssignment(target: BindingOrAssignmentElementTarget, value: Expression, location: TextRange | undefined, original: Node | undefined) { - Debug.assertNode(target, isBindingName); - if (pendingExpressions) { - value = inlineExpressions(append(pendingExpressions, value)); - pendingExpressions = undefined; - } - pendingDeclarations.push({ pendingExpressions, name: target, value, location, original }); - } } - - /** - * Flattens a BindingOrAssignmentElement into zero or more bindings or assignments. - * - * @param flattenContext Options used to control flattening. - * @param element The element to flatten. - * @param value The current RHS value to assign to the element. - * @param location The location to use for source maps and comments. - * @param skipInitializer An optional value indicating whether to include the initializer - * for the element. - */ - function flattenBindingOrAssignmentElement( - flattenContext: FlattenContext, - element: BindingOrAssignmentElement, - value: Expression | undefined, - location: TextRange, - skipInitializer?: boolean) { - if (!skipInitializer) { - const initializer = visitNode(getInitializerOfBindingOrAssignmentElement(element), flattenContext.visitor, isExpression); - if (initializer) { - // Combine value and initializer - value = value ? createDefaultValueCheck(flattenContext, value, initializer, location) : initializer; - } - else if (!value) { - // Use 'void 0' in absence of value and initializer - value = createVoidZero(); - } - } - const bindingTarget = getTargetOfBindingOrAssignmentElement(element)!; // TODO: GH#18217 - if (isObjectBindingOrAssignmentPattern(bindingTarget)) { - flattenObjectBindingOrAssignmentPattern(flattenContext, element, bindingTarget, value!, location); + return false; +} +/* @internal */ +function bindingOrAssignmentElementContainsNonLiteralComputedName(element: BindingOrAssignmentElement): boolean { + const propertyName = tryGetPropertyNameOfBindingOrAssignmentElement(element); + if (propertyName && isComputedPropertyName(propertyName) && !isLiteralExpression(propertyName.expression)) { + return true; + } + const target = getTargetOfBindingOrAssignmentElement(element); + return !!target && isBindingOrAssignmentPattern(target) && bindingOrAssignmentPatternContainsNonLiteralComputedName(target); +} +/* @internal */ +function bindingOrAssignmentPatternContainsNonLiteralComputedName(pattern: BindingOrAssignmentPattern): boolean { + return !!forEach(getElementsOfBindingOrAssignmentPattern(pattern), bindingOrAssignmentElementContainsNonLiteralComputedName); +} +/** + * Flattens a VariableDeclaration or ParameterDeclaration to one or more variable declarations. + * + * @param node The node to flatten. + * @param visitor An optional visitor used to visit initializers. + * @param context The transformation context. + * @param boundValue The value bound to the declaration. + * @param skipInitializer A value indicating whether to ignore the initializer of `node`. + * @param hoistTempVariables Indicates whether temporary variables should not be recorded in-line. + * @param level Indicates the extent to which flattening should occur. + */ +/* @internal */ +export function flattenDestructuringBinding(node: VariableDeclaration | ParameterDeclaration, visitor: (node: Node) => VisitResult, context: TransformationContext, level: FlattenLevel, rval?: Expression, hoistTempVariables = false, skipInitializer?: boolean): VariableDeclaration[] { + let pendingExpressions: Expression[] | undefined; + const pendingDeclarations: { + pendingExpressions?: Expression[]; + name: BindingName; + value: Expression; + location?: TextRange; + original?: Node; + }[] = []; + const declarations: VariableDeclaration[] = []; + const flattenContext: FlattenContext = { + context, + level, + downlevelIteration: !!context.getCompilerOptions().downlevelIteration, + hoistTempVariables, + emitExpression, + emitBindingOrAssignment, + createArrayBindingOrAssignmentPattern: makeArrayBindingPattern, + createObjectBindingOrAssignmentPattern: makeObjectBindingPattern, + createArrayBindingOrAssignmentElement: makeBindingElement, + visitor + }; + if (isVariableDeclaration(node)) { + let initializer = getInitializerOfBindingOrAssignmentElement(node); + if (initializer && (isIdentifier(initializer) && bindingOrAssignmentElementAssignsToName(node, initializer.escapedText) || + bindingOrAssignmentElementContainsNonLiteralComputedName(node))) { + // If the right-hand value of the assignment is also an assignment target then + // we need to cache the right-hand value. + initializer = ensureIdentifier(flattenContext, initializer, /*reuseIdentifierExpressions*/ false, initializer); + node = updateVariableDeclaration(node, node.name, node.type, initializer); } - else if (isArrayBindingOrAssignmentPattern(bindingTarget)) { - flattenArrayBindingOrAssignmentPattern(flattenContext, element, bindingTarget, value!, location); + } + flattenBindingOrAssignmentElement(flattenContext, node, rval, node, skipInitializer); + if (pendingExpressions) { + const temp = createTempVariable(/*recordTempVariable*/ undefined); + if (hoistTempVariables) { + const value = inlineExpressions(pendingExpressions); + pendingExpressions = undefined; + emitBindingOrAssignment(temp, value, /*location*/ undefined, /*original*/ undefined); } else { - flattenContext.emitBindingOrAssignment(bindingTarget, value!, location, /*original*/ element); // TODO: GH#18217 + context.hoistVariableDeclaration(temp); + const pendingDeclaration = last(pendingDeclarations); + pendingDeclaration.pendingExpressions = append(pendingDeclaration.pendingExpressions, createAssignment(temp, pendingDeclaration.value)); + addRange(pendingDeclaration.pendingExpressions, pendingExpressions); + pendingDeclaration.value = temp; } } - - /** - * Flattens an ObjectBindingOrAssignmentPattern into zero or more bindings or assignments. - * - * @param flattenContext Options used to control flattening. - * @param parent The parent element of the pattern. - * @param pattern The ObjectBindingOrAssignmentPattern to flatten. - * @param value The current RHS value to assign to the element. - * @param location The location to use for source maps and comments. - */ - function flattenObjectBindingOrAssignmentPattern(flattenContext: FlattenContext, parent: BindingOrAssignmentElement, pattern: ObjectBindingOrAssignmentPattern, value: Expression, location: TextRange) { - const elements = getElementsOfBindingOrAssignmentPattern(pattern); - const numElements = elements.length; - if (numElements !== 1) { - // For anything other than a single-element destructuring we need to generate a temporary - // to ensure value is evaluated exactly once. Additionally, if we have zero elements - // we need to emit *something* to ensure that in case a 'var' keyword was already emitted, - // so in that case, we'll intentionally create that temporary. - const reuseIdentifierExpressions = !isDeclarationBindingElement(parent) || numElements !== 0; - value = ensureIdentifier(flattenContext, value, reuseIdentifierExpressions, location); + for (const { pendingExpressions, name, value, location, original } of pendingDeclarations) { + const variable = createVariableDeclaration(name, + /*type*/ undefined, pendingExpressions ? inlineExpressions(append(pendingExpressions, value)) : value); + variable.original = original; + setTextRange(variable, location); + if (isIdentifier(name)) { + setEmitFlags(variable, EmitFlags.NoNestedSourceMaps); + } + aggregateTransformFlags(variable); + declarations.push(variable); + } + return declarations; + function emitExpression(value: Expression) { + pendingExpressions = append(pendingExpressions, value); + } + function emitBindingOrAssignment(target: BindingOrAssignmentElementTarget, value: Expression, location: TextRange | undefined, original: Node | undefined) { + Debug.assertNode(target, isBindingName); + if (pendingExpressions) { + value = inlineExpressions(append(pendingExpressions, value)); + pendingExpressions = undefined; } - let bindingElements: BindingOrAssignmentElement[] | undefined; - let computedTempVariables: Expression[] | undefined; - for (let i = 0; i < numElements; i++) { - const element = elements[i]; - if (!getRestIndicatorOfBindingOrAssignmentElement(element)) { - const propertyName = getPropertyNameOfBindingOrAssignmentElement(element)!; - if (flattenContext.level >= FlattenLevel.ObjectRest - && !(element.transformFlags & (TransformFlags.ContainsRestOrSpread | TransformFlags.ContainsObjectRestOrSpread)) - && !(getTargetOfBindingOrAssignmentElement(element)!.transformFlags & (TransformFlags.ContainsRestOrSpread | TransformFlags.ContainsObjectRestOrSpread)) - && !isComputedPropertyName(propertyName)) { - bindingElements = append(bindingElements, visitNode(element, flattenContext.visitor)); - } - else { - if (bindingElements) { - flattenContext.emitBindingOrAssignment(flattenContext.createObjectBindingOrAssignmentPattern(bindingElements), value, location, pattern); - bindingElements = undefined; - } - const rhsValue = createDestructuringPropertyAccess(flattenContext, value, propertyName); - if (isComputedPropertyName(propertyName)) { - computedTempVariables = append(computedTempVariables, (rhsValue as ElementAccessExpression).argumentExpression); - } - flattenBindingOrAssignmentElement(flattenContext, element, rhsValue, /*location*/ element); - } + pendingDeclarations.push({ pendingExpressions, name: target, value, location, original }); + } +} +/** + * Flattens a BindingOrAssignmentElement into zero or more bindings or assignments. + * + * @param flattenContext Options used to control flattening. + * @param element The element to flatten. + * @param value The current RHS value to assign to the element. + * @param location The location to use for source maps and comments. + * @param skipInitializer An optional value indicating whether to include the initializer + * for the element. + */ +/* @internal */ +function flattenBindingOrAssignmentElement(flattenContext: FlattenContext, element: BindingOrAssignmentElement, value: Expression | undefined, location: TextRange, skipInitializer?: boolean) { + if (!skipInitializer) { + const initializer = visitNode(getInitializerOfBindingOrAssignmentElement(element), flattenContext.visitor, isExpression); + if (initializer) { + // Combine value and initializer + value = value ? createDefaultValueCheck(flattenContext, value, initializer, location) : initializer; + } + else if (!value) { + // Use 'void 0' in absence of value and initializer + value = createVoidZero(); + } + } + const bindingTarget = (getTargetOfBindingOrAssignmentElement(element)!); // TODO: GH#18217 + if (isObjectBindingOrAssignmentPattern(bindingTarget)) { + flattenObjectBindingOrAssignmentPattern(flattenContext, element, bindingTarget, value!, location); + } + else if (isArrayBindingOrAssignmentPattern(bindingTarget)) { + flattenArrayBindingOrAssignmentPattern(flattenContext, element, bindingTarget, value!, location); + } + else { + flattenContext.emitBindingOrAssignment(bindingTarget, value!, location, /*original*/ element); // TODO: GH#18217 + } +} +/** + * Flattens an ObjectBindingOrAssignmentPattern into zero or more bindings or assignments. + * + * @param flattenContext Options used to control flattening. + * @param parent The parent element of the pattern. + * @param pattern The ObjectBindingOrAssignmentPattern to flatten. + * @param value The current RHS value to assign to the element. + * @param location The location to use for source maps and comments. + */ +/* @internal */ +function flattenObjectBindingOrAssignmentPattern(flattenContext: FlattenContext, parent: BindingOrAssignmentElement, pattern: ObjectBindingOrAssignmentPattern, value: Expression, location: TextRange) { + const elements = getElementsOfBindingOrAssignmentPattern(pattern); + const numElements = elements.length; + if (numElements !== 1) { + // For anything other than a single-element destructuring we need to generate a temporary + // to ensure value is evaluated exactly once. Additionally, if we have zero elements + // we need to emit *something* to ensure that in case a 'var' keyword was already emitted, + // so in that case, we'll intentionally create that temporary. + const reuseIdentifierExpressions = !isDeclarationBindingElement(parent) || numElements !== 0; + value = ensureIdentifier(flattenContext, value, reuseIdentifierExpressions, location); + } + let bindingElements: BindingOrAssignmentElement[] | undefined; + let computedTempVariables: Expression[] | undefined; + for (let i = 0; i < numElements; i++) { + const element = elements[i]; + if (!getRestIndicatorOfBindingOrAssignmentElement(element)) { + const propertyName = (getPropertyNameOfBindingOrAssignmentElement(element)!); + if (flattenContext.level >= FlattenLevel.ObjectRest + && !(element.transformFlags & (TransformFlags.ContainsRestOrSpread | TransformFlags.ContainsObjectRestOrSpread)) + && !(getTargetOfBindingOrAssignmentElement(element)!.transformFlags & (TransformFlags.ContainsRestOrSpread | TransformFlags.ContainsObjectRestOrSpread)) + && !isComputedPropertyName(propertyName)) { + bindingElements = append(bindingElements, visitNode(element, flattenContext.visitor)); } - else if (i === numElements - 1) { + else { if (bindingElements) { flattenContext.emitBindingOrAssignment(flattenContext.createObjectBindingOrAssignmentPattern(bindingElements), value, location, pattern); bindingElements = undefined; } - const rhsValue = createRestCall(flattenContext.context, value, elements, computedTempVariables!, pattern); // TODO: GH#18217 - flattenBindingOrAssignmentElement(flattenContext, element, rhsValue, element); + const rhsValue = createDestructuringPropertyAccess(flattenContext, value, propertyName); + if (isComputedPropertyName(propertyName)) { + computedTempVariables = append(computedTempVariables, (rhsValue as ElementAccessExpression).argumentExpression); + } + flattenBindingOrAssignmentElement(flattenContext, element, rhsValue, /*location*/ element); } } - if (bindingElements) { - flattenContext.emitBindingOrAssignment(flattenContext.createObjectBindingOrAssignmentPattern(bindingElements), value, location, pattern); + else if (i === numElements - 1) { + if (bindingElements) { + flattenContext.emitBindingOrAssignment(flattenContext.createObjectBindingOrAssignmentPattern(bindingElements), value, location, pattern); + bindingElements = undefined; + } + const rhsValue = createRestCall(flattenContext.context, value, elements, computedTempVariables!, pattern); // TODO: GH#18217 + flattenBindingOrAssignmentElement(flattenContext, element, rhsValue, element); } } - - /** - * Flattens an ArrayBindingOrAssignmentPattern into zero or more bindings or assignments. - * - * @param flattenContext Options used to control flattening. - * @param parent The parent element of the pattern. - * @param pattern The ArrayBindingOrAssignmentPattern to flatten. - * @param value The current RHS value to assign to the element. - * @param location The location to use for source maps and comments. - */ - function flattenArrayBindingOrAssignmentPattern(flattenContext: FlattenContext, parent: BindingOrAssignmentElement, pattern: ArrayBindingOrAssignmentPattern, value: Expression, location: TextRange) { - const elements = getElementsOfBindingOrAssignmentPattern(pattern); - const numElements = elements.length; - if (flattenContext.level < FlattenLevel.ObjectRest && flattenContext.downlevelIteration) { - // Read the elements of the iterable into an array - value = ensureIdentifier( - flattenContext, - createReadHelper( - flattenContext.context, - value, - numElements > 0 && getRestIndicatorOfBindingOrAssignmentElement(elements[numElements - 1]) - ? undefined - : numElements, - location - ), - /*reuseIdentifierExpressions*/ false, - location - ); - } - else if (numElements !== 1 && (flattenContext.level < FlattenLevel.ObjectRest || numElements === 0) - || every(elements, isOmittedExpression)) { - // For anything other than a single-element destructuring we need to generate a temporary - // to ensure value is evaluated exactly once. Additionally, if we have zero elements - // we need to emit *something* to ensure that in case a 'var' keyword was already emitted, - // so in that case, we'll intentionally create that temporary. - // Or all the elements of the binding pattern are omitted expression such as "var [,] = [1,2]", - // then we will create temporary variable. - const reuseIdentifierExpressions = !isDeclarationBindingElement(parent) || numElements !== 0; - value = ensureIdentifier(flattenContext, value, reuseIdentifierExpressions, location); - } - let bindingElements: BindingOrAssignmentElement[] | undefined; - let restContainingElements: [Identifier, BindingOrAssignmentElement][] | undefined; - for (let i = 0; i < numElements; i++) { - const element = elements[i]; - if (flattenContext.level >= FlattenLevel.ObjectRest) { - // If an array pattern contains an ObjectRest, we must cache the result so that we - // can perform the ObjectRest destructuring in a different declaration - if (element.transformFlags & TransformFlags.ContainsObjectRestOrSpread) { - const temp = createTempVariable(/*recordTempVariable*/ undefined); - if (flattenContext.hoistTempVariables) { - flattenContext.context.hoistVariableDeclaration(temp); - } - - restContainingElements = append(restContainingElements, <[Identifier, BindingOrAssignmentElement]>[temp, element]); - bindingElements = append(bindingElements, flattenContext.createArrayBindingOrAssignmentElement(temp)); - } - else { - bindingElements = append(bindingElements, element); + if (bindingElements) { + flattenContext.emitBindingOrAssignment(flattenContext.createObjectBindingOrAssignmentPattern(bindingElements), value, location, pattern); + } +} +/** + * Flattens an ArrayBindingOrAssignmentPattern into zero or more bindings or assignments. + * + * @param flattenContext Options used to control flattening. + * @param parent The parent element of the pattern. + * @param pattern The ArrayBindingOrAssignmentPattern to flatten. + * @param value The current RHS value to assign to the element. + * @param location The location to use for source maps and comments. + */ +/* @internal */ +function flattenArrayBindingOrAssignmentPattern(flattenContext: FlattenContext, parent: BindingOrAssignmentElement, pattern: ArrayBindingOrAssignmentPattern, value: Expression, location: TextRange) { + const elements = getElementsOfBindingOrAssignmentPattern(pattern); + const numElements = elements.length; + if (flattenContext.level < FlattenLevel.ObjectRest && flattenContext.downlevelIteration) { + // Read the elements of the iterable into an array + value = ensureIdentifier(flattenContext, createReadHelper(flattenContext.context, value, numElements > 0 && getRestIndicatorOfBindingOrAssignmentElement(elements[numElements - 1]) + ? undefined + : numElements, location), + /*reuseIdentifierExpressions*/ false, location); + } + else if (numElements !== 1 && (flattenContext.level < FlattenLevel.ObjectRest || numElements === 0) + || every(elements, isOmittedExpression)) { + // For anything other than a single-element destructuring we need to generate a temporary + // to ensure value is evaluated exactly once. Additionally, if we have zero elements + // we need to emit *something* to ensure that in case a 'var' keyword was already emitted, + // so in that case, we'll intentionally create that temporary. + // Or all the elements of the binding pattern are omitted expression such as "var [,] = [1,2]", + // then we will create temporary variable. + const reuseIdentifierExpressions = !isDeclarationBindingElement(parent) || numElements !== 0; + value = ensureIdentifier(flattenContext, value, reuseIdentifierExpressions, location); + } + let bindingElements: BindingOrAssignmentElement[] | undefined; + let restContainingElements: [Identifier, BindingOrAssignmentElement][] | undefined; + for (let i = 0; i < numElements; i++) { + const element = elements[i]; + if (flattenContext.level >= FlattenLevel.ObjectRest) { + // If an array pattern contains an ObjectRest, we must cache the result so that we + // can perform the ObjectRest destructuring in a different declaration + if (element.transformFlags & TransformFlags.ContainsObjectRestOrSpread) { + const temp = createTempVariable(/*recordTempVariable*/ undefined); + if (flattenContext.hoistTempVariables) { + flattenContext.context.hoistVariableDeclaration(temp); } + restContainingElements = append(restContainingElements, (<[Identifier, BindingOrAssignmentElement]>[temp, element])); + bindingElements = append(bindingElements, flattenContext.createArrayBindingOrAssignmentElement(temp)); } - else if (isOmittedExpression(element)) { - continue; - } - else if (!getRestIndicatorOfBindingOrAssignmentElement(element)) { - const rhsValue = createElementAccess(value, i); - flattenBindingOrAssignmentElement(flattenContext, element, rhsValue, /*location*/ element); - } - else if (i === numElements - 1) { - const rhsValue = createArraySlice(value, i); - flattenBindingOrAssignmentElement(flattenContext, element, rhsValue, /*location*/ element); + else { + bindingElements = append(bindingElements, element); } } - if (bindingElements) { - flattenContext.emitBindingOrAssignment(flattenContext.createArrayBindingOrAssignmentPattern(bindingElements), value, location, pattern); + else if (isOmittedExpression(element)) { + continue; } - if (restContainingElements) { - for (const [id, element] of restContainingElements) { - flattenBindingOrAssignmentElement(flattenContext, element, id, element); - } - } - } - - /** - * Creates an expression used to provide a default value if a value is `undefined` at runtime. - * - * @param flattenContext Options used to control flattening. - * @param value The RHS value to test. - * @param defaultValue The default value to use if `value` is `undefined` at runtime. - * @param location The location to use for source maps and comments. - */ - function createDefaultValueCheck(flattenContext: FlattenContext, value: Expression, defaultValue: Expression, location: TextRange): Expression { - value = ensureIdentifier(flattenContext, value, /*reuseIdentifierExpressions*/ true, location); - return createConditional(createTypeCheck(value, "undefined"), defaultValue, value); - } - - /** - * Creates either a PropertyAccessExpression or an ElementAccessExpression for the - * right-hand side of a transformed destructuring assignment. - * - * @link https://tc39.github.io/ecma262/#sec-runtime-semantics-keyeddestructuringassignmentevaluation - * - * @param flattenContext Options used to control flattening. - * @param value The RHS value that is the source of the property. - * @param propertyName The destructuring property name. - */ - function createDestructuringPropertyAccess(flattenContext: FlattenContext, value: Expression, propertyName: PropertyName): LeftHandSideExpression { - if (isComputedPropertyName(propertyName)) { - const argumentExpression = ensureIdentifier(flattenContext, visitNode(propertyName.expression, flattenContext.visitor), /*reuseIdentifierExpressions*/ false, /*location*/ propertyName); - return createElementAccess(value, argumentExpression); + else if (!getRestIndicatorOfBindingOrAssignmentElement(element)) { + const rhsValue = createElementAccess(value, i); + flattenBindingOrAssignmentElement(flattenContext, element, rhsValue, /*location*/ element); } - else if (isStringOrNumericLiteralLike(propertyName)) { - const argumentExpression = getSynthesizedClone(propertyName); - argumentExpression.text = argumentExpression.text; - return createElementAccess(value, argumentExpression); + else if (i === numElements - 1) { + const rhsValue = createArraySlice(value, i); + flattenBindingOrAssignmentElement(flattenContext, element, rhsValue, /*location*/ element); } - else { - const name = createIdentifier(idText(propertyName)); - return createPropertyAccess(value, name); + } + if (bindingElements) { + flattenContext.emitBindingOrAssignment(flattenContext.createArrayBindingOrAssignmentPattern(bindingElements), value, location, pattern); + } + if (restContainingElements) { + for (const [id, element] of restContainingElements) { + flattenBindingOrAssignmentElement(flattenContext, element, id, element); } } - - /** - * Ensures that there exists a declared identifier whose value holds the given expression. - * This function is useful to ensure that the expression's value can be read from in subsequent expressions. - * Unless 'reuseIdentifierExpressions' is false, 'value' will be returned if it is just an identifier. - * - * @param flattenContext Options used to control flattening. - * @param value the expression whose value needs to be bound. - * @param reuseIdentifierExpressions true if identifier expressions can simply be returned; - * false if it is necessary to always emit an identifier. - * @param location The location to use for source maps and comments. - */ - function ensureIdentifier(flattenContext: FlattenContext, value: Expression, reuseIdentifierExpressions: boolean, location: TextRange) { - if (isIdentifier(value) && reuseIdentifierExpressions) { - return value; +} +/** + * Creates an expression used to provide a default value if a value is `undefined` at runtime. + * + * @param flattenContext Options used to control flattening. + * @param value The RHS value to test. + * @param defaultValue The default value to use if `value` is `undefined` at runtime. + * @param location The location to use for source maps and comments. + */ +/* @internal */ +function createDefaultValueCheck(flattenContext: FlattenContext, value: Expression, defaultValue: Expression, location: TextRange): Expression { + value = ensureIdentifier(flattenContext, value, /*reuseIdentifierExpressions*/ true, location); + return createConditional(createTypeCheck(value, "undefined"), defaultValue, value); +} +/** + * Creates either a PropertyAccessExpression or an ElementAccessExpression for the + * right-hand side of a transformed destructuring assignment. + * + * @link https://tc39.github.io/ecma262/#sec-runtime-semantics-keyeddestructuringassignmentevaluation + * + * @param flattenContext Options used to control flattening. + * @param value The RHS value that is the source of the property. + * @param propertyName The destructuring property name. + */ +/* @internal */ +function createDestructuringPropertyAccess(flattenContext: FlattenContext, value: Expression, propertyName: PropertyName): LeftHandSideExpression { + if (isComputedPropertyName(propertyName)) { + const argumentExpression = ensureIdentifier(flattenContext, visitNode(propertyName.expression, flattenContext.visitor), /*reuseIdentifierExpressions*/ false, /*location*/ propertyName); + return createElementAccess(value, argumentExpression); + } + else if (isStringOrNumericLiteralLike(propertyName)) { + const argumentExpression = getSynthesizedClone(propertyName); + argumentExpression.text = argumentExpression.text; + return createElementAccess(value, argumentExpression); + } + else { + const name = createIdentifier(idText(propertyName)); + return createPropertyAccess(value, name); + } +} +/** + * Ensures that there exists a declared identifier whose value holds the given expression. + * This function is useful to ensure that the expression's value can be read from in subsequent expressions. + * Unless 'reuseIdentifierExpressions' is false, 'value' will be returned if it is just an identifier. + * + * @param flattenContext Options used to control flattening. + * @param value the expression whose value needs to be bound. + * @param reuseIdentifierExpressions true if identifier expressions can simply be returned; + * false if it is necessary to always emit an identifier. + * @param location The location to use for source maps and comments. + */ +/* @internal */ +function ensureIdentifier(flattenContext: FlattenContext, value: Expression, reuseIdentifierExpressions: boolean, location: TextRange) { + if (isIdentifier(value) && reuseIdentifierExpressions) { + return value; + } + else { + const temp = createTempVariable(/*recordTempVariable*/ undefined); + if (flattenContext.hoistTempVariables) { + flattenContext.context.hoistVariableDeclaration(temp); + flattenContext.emitExpression(setTextRange(createAssignment(temp, value), location)); } else { - const temp = createTempVariable(/*recordTempVariable*/ undefined); - if (flattenContext.hoistTempVariables) { - flattenContext.context.hoistVariableDeclaration(temp); - flattenContext.emitExpression(setTextRange(createAssignment(temp, value), location)); - } - else { - flattenContext.emitBindingOrAssignment(temp, value, location, /*original*/ undefined); - } - return temp; + flattenContext.emitBindingOrAssignment(temp, value, location, /*original*/ undefined); } + return temp; } - - function makeArrayBindingPattern(elements: BindingOrAssignmentElement[]) { - Debug.assertEachNode(elements, isArrayBindingElement); - return createArrayBindingPattern(elements); - } - - function makeArrayAssignmentPattern(elements: BindingOrAssignmentElement[]) { - return createArrayLiteral(map(elements, convertToArrayAssignmentElement)); - } - - function makeObjectBindingPattern(elements: BindingOrAssignmentElement[]) { - Debug.assertEachNode(elements, isBindingElement); - return createObjectBindingPattern(elements); - } - - function makeObjectAssignmentPattern(elements: BindingOrAssignmentElement[]) { - return createObjectLiteral(map(elements, convertToObjectAssignmentElement)); - } - - function makeBindingElement(name: Identifier) { - return createBindingElement(/*dotDotDotToken*/ undefined, /*propertyName*/ undefined, name); - } - - function makeAssignmentElement(name: Identifier) { - return name; - } - - export const restHelper: UnscopedEmitHelper = { - name: "typescript:rest", - importName: "__rest", - scoped: false, - text: ` +} +/* @internal */ +function makeArrayBindingPattern(elements: BindingOrAssignmentElement[]) { + Debug.assertEachNode(elements, isArrayBindingElement); + return createArrayBindingPattern((elements)); +} +/* @internal */ +function makeArrayAssignmentPattern(elements: BindingOrAssignmentElement[]) { + return createArrayLiteral(map(elements, convertToArrayAssignmentElement)); +} +/* @internal */ +function makeObjectBindingPattern(elements: BindingOrAssignmentElement[]) { + Debug.assertEachNode(elements, isBindingElement); + return createObjectBindingPattern((elements)); +} +/* @internal */ +function makeObjectAssignmentPattern(elements: BindingOrAssignmentElement[]) { + return createObjectLiteral(map(elements, convertToObjectAssignmentElement)); +} +/* @internal */ +function makeBindingElement(name: Identifier) { + return createBindingElement(/*dotDotDotToken*/ undefined, /*propertyName*/ undefined, name); +} +/* @internal */ +function makeAssignmentElement(name: Identifier) { + return name; +} +/* @internal */ +export const restHelper: UnscopedEmitHelper = { + name: "typescript:rest", + importName: "__rest", + scoped: false, + text: ` var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) @@ -543,44 +499,32 @@ namespace ts { } return t; };` - }; - - /** Given value: o, propName: p, pattern: { a, b, ...p } from the original statement - * `{ a, b, ...p } = o`, create `p = __rest(o, ["a", "b"]);` - */ - function createRestCall(context: TransformationContext, value: Expression, elements: readonly BindingOrAssignmentElement[], computedTempVariables: readonly Expression[], location: TextRange): Expression { - context.requestEmitHelper(restHelper); - const propertyNames: Expression[] = []; - let computedTempVariableOffset = 0; - for (let i = 0; i < elements.length - 1; i++) { - const propertyName = getPropertyNameOfBindingOrAssignmentElement(elements[i]); - if (propertyName) { - if (isComputedPropertyName(propertyName)) { - const temp = computedTempVariables[computedTempVariableOffset]; - computedTempVariableOffset++; - // typeof _tmp === "symbol" ? _tmp : _tmp + "" - propertyNames.push( - createConditional( - createTypeCheck(temp, "symbol"), - temp, - createAdd(temp, createLiteral("")) - ) - ); - } - else { - propertyNames.push(createLiteral(propertyName)); - } +}; +/** Given value: o, propName: p, pattern: { a, b, ...p } from the original statement + * `{ a, b, ...p } = o`, create `p = __rest(o, ["a", "b"]);` + */ +/* @internal */ +function createRestCall(context: TransformationContext, value: Expression, elements: readonly BindingOrAssignmentElement[], computedTempVariables: readonly Expression[], location: TextRange): Expression { + context.requestEmitHelper(restHelper); + const propertyNames: Expression[] = []; + let computedTempVariableOffset = 0; + for (let i = 0; i < elements.length - 1; i++) { + const propertyName = getPropertyNameOfBindingOrAssignmentElement(elements[i]); + if (propertyName) { + if (isComputedPropertyName(propertyName)) { + const temp = computedTempVariables[computedTempVariableOffset]; + computedTempVariableOffset++; + // typeof _tmp === "symbol" ? _tmp : _tmp + "" + propertyNames.push(createConditional(createTypeCheck(temp, "symbol"), temp, createAdd(temp, createLiteral("")))); + } + else { + propertyNames.push(createLiteral(propertyName)); } } - return createCall( - getUnscopedHelperName("__rest"), - /*typeArguments*/ undefined, - [ - value, - setTextRange( - createArrayLiteral(propertyNames), - location - ) - ]); } + return createCall(getUnscopedHelperName("__rest"), + /*typeArguments*/ undefined, [ + value, + setTextRange(createArrayLiteral(propertyNames), location) + ]); } diff --git a/src/compiler/transformers/es2015.ts b/src/compiler/transformers/es2015.ts index 6bffbe3b299b1..74ebec7cca601 100644 --- a/src/compiler/transformers/es2015.ts +++ b/src/compiler/transformers/es2015.ts @@ -1,4317 +1,3274 @@ +import { Identifier, ParameterDeclaration, IterationStatement, LabeledStatement, Statement, TransformationContext, SourceFile, VariableDeclaration, append, createVariableDeclaration, chainBundle, addEmitHelpers, Node, SyntaxKind, ReturnStatement, TransformFlags, isStatement, isIterationStatement, getEmitFlags, EmitFlags, VisitResult, ClassDeclaration, ClassExpression, FunctionDeclaration, ArrowFunction, FunctionExpression, VariableDeclarationList, SwitchStatement, CaseBlock, Block, BreakOrContinueStatement, DoStatement, WhileStatement, ForStatement, ForInStatement, ForOfStatement, ExpressionStatement, ObjectLiteralExpression, CatchClause, ShorthandPropertyAssignment, ComputedPropertyName, ArrayLiteralExpression, CallExpression, NewExpression, ParenthesizedExpression, BinaryExpression, LiteralExpression, StringLiteral, NumericLiteral, TaggedTemplateExpression, TemplateExpression, YieldExpression, SpreadElement, MetaProperty, MethodDeclaration, AccessorDeclaration, VariableStatement, visitEachChild, addStandardPrologue, addCustomPrologue, addRange, visitNodes, createVariableStatement, createVariableDeclarationList, mergeLexicalEnvironment, updateSourceFileNode, setTextRange, createNodeArray, concatenate, setOriginalNode, createReturn, createFileLevelUniqueName, createObjectLiteral, createPropertyAssignment, createIdentifier, visitNode, isExpression, createVoidZero, createUniqueName, isGeneratedIdentifier, idText, Expression, createLiteral, createBinary, getLocalName, startOnNewLine, hasModifier, ModifierFlags, createExportDefault, createExternalModuleExport, createEndOfDeclarationMarker, setEmitFlags, singleOrMany, getClassExtendsHeritageElement, createFunctionExpression, createParameter, createPartiallyEmittedExpression, skipTrivia, createParen, createCall, addSyntheticLeadingComment, ExpressionWithTypeArguments, createTokenRange, getInternalName, insertStatementsAfterStandardPrologue, createBlock, createExpressionStatement, getFirstConstructorWithBody, createFunctionDeclaration, ConstructorDeclaration, visitParameterList, FunctionBody, skipOuterExpressions, isExpressionStatement, isSuperCall, cast, isBinaryExpression, isCallExpression, setCommentRange, getCommentRange, IfStatement, lastOrUndefined, createThis, createLogicalOr, createLogicalAnd, createStrictInequality, createNull, createFunctionApply, isBindingPattern, getGeneratedNameForNode, FunctionLikeDeclaration, some, BindingPattern, insertStatementAfterCustomPrologue, flattenDestructuringBinding, FlattenLevel, createAssignment, createIf, createTypeCheck, getSynthesizedClone, getMutableClone, createTempVariable, createLoopVariable, createArrayLiteral, createFor, createLessThan, createPropertyAccess, createPostfixIncrement, createElementAccess, createSubtract, insertStatementsAfterCustomPrologue, setSourceMapRange, createConditional, Debug, SemicolonClassElement, getAllAccessorDeclarations, createEmptyStatement, LeftHandSideExpression, getSourceMapRange, isPropertyName, isPrivateIdentifier, isComputedPropertyName, isIdentifier, createStringLiteral, unescapeLeadingUnderscores, createObjectDefinePropertyCall, createPropertyDescriptor, createMemberAccessForPropertyName, AllAccessorDeclarations, createExpressionForPropertyName, ObjectLiteralElementLike, createFalse, createTrue, updateFunctionExpression, updateFunctionDeclaration, isModifier, TextRange, isClassLike, isBlock, moveRangeEnd, nodeIsSynthesized, rangeEndIsOnSameLineAsRangeStart, moveSyntheticComments, arrayIsEqualTo, setTokenSourceMapRange, updateExpressionStatement, updateParen, isDestructuringAssignment, flattenDestructuringAssignment, NodeFlags, inlineExpressions, flatMap, last, createRange, NodeCheckFlags, createMap, unwrapInnermostStatementOfLabel, restoreEnclosingLabel, liftToBlock, isVariableDeclarationList, firstOrUndefined, moveRangePos, aggregateTransformFlags, updateBlock, createValuesHelper, createLogicalNot, createTry, createCatchClause, createFunctionCall, createThrow, isObjectLiteralElementLike, isForStatement, isOmittedExpression, updateFor, isForInitializer, updateForOf, updateForIn, updateDo, updateWhile, getCombinedNodeFlags, createToken, map, createStatement, createPrefix, createBreak, createYield, createTypeOf, CaseClause, createSwitch, createCaseBlock, createContinue, createCaseClause, BindingElement, PropertyAssignment, updateCatchClause, updateGetAccessor, updateSetAccessor, isSpreadElement, isSuperProperty, updateCall, isArrowFunction, isVariableStatement, first, filter, tryCast, isAssignmentExpression, isFunctionExpression, isReturnStatement, elementAt, recreateOuterExpressions, createCallBinding, createNew, NodeArray, flatten, spanMap, __String, createSpreadHelper, createSpreadArraysHelper, isArrayLiteralExpression, every, TokenFlags, createNumericLiteral, processTaggedTemplateExpression, ProcessLevel, reduceLeft, createAdd, EmitHint, isFunctionLike, isInternalName, getParseTreeNode, NamedDeclaration, Declaration, PrimaryExpression, getNameOfDeclaration, ClassLikeDeclaration, getEnclosingBlockScopeContainer, isClassElement, ClassElement, singleOrUndefined, getUnscopedHelperName, UnscopedEmitHelper } from "../ts"; +import * as ts from "../ts"; /*@internal*/ -namespace ts { - const enum ES2015SubstitutionFlags { - /** Enables substitutions for captured `this` */ - CapturedThis = 1 << 0, - /** Enables substitutions for block-scoped bindings. */ - BlockScopedBindings = 1 << 1, - } - - /** - * If loop contains block scoped binding captured in some function then loop body is converted to a function. - * Lexical bindings declared in loop initializer will be passed into the loop body function as parameters, - * however if this binding is modified inside the body - this new value should be propagated back to the original binding. - * This is done by declaring new variable (out parameter holder) outside of the loop for every binding that is reassigned inside the body. - * On every iteration this variable is initialized with value of corresponding binding. - * At every point where control flow leaves the loop either explicitly (break/continue) or implicitly (at the end of loop body) - * we copy the value inside the loop to the out parameter holder. - * +const enum ES2015SubstitutionFlags { + /** Enables substitutions for captured `this` */ + CapturedThis = 1 << 0, + /** Enables substitutions for block-scoped bindings. */ + BlockScopedBindings = 1 << 1 +} +/** + * If loop contains block scoped binding captured in some function then loop body is converted to a function. + * Lexical bindings declared in loop initializer will be passed into the loop body function as parameters, + * however if this binding is modified inside the body - this new value should be propagated back to the original binding. + * This is done by declaring new variable (out parameter holder) outside of the loop for every binding that is reassigned inside the body. + * On every iteration this variable is initialized with value of corresponding binding. + * At every point where control flow leaves the loop either explicitly (break/continue) or implicitly (at the end of loop body) + * we copy the value inside the loop to the out parameter holder. + * + * for (let x;;) { + * let a = 1; + * let b = () => a; + * x++ + * if (...) break; + * ... + * } + * + * will be converted to + * + * var out_x; + * var loop = function(x) { + * var a = 1; + * var b = function() { return a; } + * x++; + * if (...) return out_x = x, "break"; + * ... + * out_x = x; + * } + * for (var x;;) { + * out_x = x; + * var state = loop(x); + * x = out_x; + * if (state === "break") break; + * } + * + * NOTE: values to out parameters are not copies if loop is abrupted with 'return' - in this case this will end the entire enclosing function + * so nobody can observe this new value. + */ +/* @internal */ +interface LoopOutParameter { + flags: LoopOutParameterFlags; + originalName: Identifier; + outParamName: Identifier; +} +/* @internal */ +const enum LoopOutParameterFlags { + Body = 1 << 0, + Initializer = 1 << 1 +} +/* @internal */ +const enum CopyDirection { + ToOriginal, + ToOutParameter +} +/* @internal */ +const enum Jump { + Break = 1 << 1, + Continue = 1 << 2, + Return = 1 << 3 +} +/* @internal */ +interface ConvertedLoopState { + /* + * set of labels that occurred inside the converted loop + * used to determine if labeled jump can be emitted as is or it should be dispatched to calling code + */ + labels?: ts.Map; + /* + * collection of labeled jumps that transfer control outside the converted loop. + * maps store association 'label -> labelMarker' where + * - label - value of label as it appear in code + * - label marker - return value that should be interpreted by calling code as 'jump to
" with type any + // And at `
` (with a closing `>`), the completion list will contain "div". + const tagName = location.parent.parent.openingElement.tagName; + const hasClosingAngleBracket = !!findChildOfKind(location.parent, SyntaxKind.GreaterThanToken, sourceFile); + const entry: CompletionEntry = { + name: tagName.getFullText(sourceFile) + (hasClosingAngleBracket ? "" : ">"), + kind: ScriptElementKind.classElement, + kindModifiers: undefined, + sortText: SortText.LocationPriority, }; + return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: false, entries: [entry] }; } - - export function getCompletionsAtPosition( - host: LanguageServiceHost, - program: Program, - log: Log, - sourceFile: SourceFile, - position: number, - preferences: UserPreferences, - triggerCharacter: CompletionsTriggerCharacter | undefined, - ): CompletionInfo | undefined { - const typeChecker = program.getTypeChecker(); - const compilerOptions = program.getCompilerOptions(); - - const contextToken = findPrecedingToken(position, sourceFile); - if (triggerCharacter && !isInString(sourceFile, position, contextToken) && !isValidTrigger(sourceFile, triggerCharacter, contextToken, position)) { + const entries: CompletionEntry[] = []; + if (isUncheckedFile(sourceFile, compilerOptions)) { + const uniqueNames = getCompletionEntriesFromSymbols(symbols, entries, location, sourceFile, typeChecker, compilerOptions.target!, log, completionKind, preferences, propertyAccessToConvert, isJsxInitializer, recommendedCompletion, symbolToOriginInfoMap, symbolToSortTextMap); + getJSCompletionEntries(sourceFile, location!.pos, uniqueNames, compilerOptions.target!, entries); // TODO: GH#18217 + } + else { + if (!isNewIdentifierLocation && (!symbols || symbols.length === 0) && keywordFilters === KeywordCompletionFilters.None) { return undefined; } - - const stringCompletions = StringCompletions.getStringLiteralCompletions(sourceFile, position, contextToken, typeChecker, compilerOptions, host, log, preferences); - if (stringCompletions) { - return stringCompletions; + getCompletionEntriesFromSymbols(symbols, entries, location, sourceFile, typeChecker, compilerOptions.target!, log, completionKind, preferences, propertyAccessToConvert, isJsxInitializer, recommendedCompletion, symbolToOriginInfoMap, symbolToSortTextMap); + } + if (keywordFilters !== KeywordCompletionFilters.None) { + const entryNames = arrayToSet(entries, e => e.name); + for (const keywordEntry of getKeywordCompletions(keywordFilters, !insideJsDocTagTypeExpression && isSourceFileJS(sourceFile))) { + if (!entryNames.has(keywordEntry.name)) { + entries.push(keywordEntry); + } } - - if (contextToken && isBreakOrContinueStatement(contextToken.parent) - && (contextToken.kind === SyntaxKind.BreakKeyword || contextToken.kind === SyntaxKind.ContinueKeyword || contextToken.kind === SyntaxKind.Identifier)) { - return getLabelCompletionAtPosition(contextToken.parent); + } + for (const literal of literals) { + entries.push(createCompletionEntryForLiteral(literal, preferences)); + } + return { isGlobalCompletion: isInSnippetScope, isMemberCompletion: isMemberCompletionKind(completionKind), isNewIdentifierLocation, entries }; +} +/* @internal */ +function isUncheckedFile(sourceFile: SourceFile, compilerOptions: CompilerOptions): boolean { + return isSourceFileJS(sourceFile) && !isCheckJsEnabledForFile(sourceFile, compilerOptions); +} +/* @internal */ +function isMemberCompletionKind(kind: CompletionKind): boolean { + switch (kind) { + case CompletionKind.ObjectPropertyDeclaration: + case CompletionKind.MemberLike: + case CompletionKind.PropertyAccess: + return true; + default: + return false; + } +} +/* @internal */ +function getJSCompletionEntries(sourceFile: SourceFile, position: number, uniqueNames: ts.Map, target: ScriptTarget, entries: Push): void { + getNameTable(sourceFile).forEach((pos, name) => { + // Skip identifiers produced only from the current location + if (pos === position) { + return; + } + const realName = unescapeLeadingUnderscores(name); + if (addToSeen(uniqueNames, realName) && isIdentifierText(realName, target)) { + entries.push({ + name: realName, + kind: ScriptElementKind.warning, + kindModifiers: "", + sortText: SortText.JavascriptIdentifiers, + isFromUncheckedFile: true + }); } - - const completionData = getCompletionData(program, log, sourceFile, isUncheckedFile(sourceFile, compilerOptions), position, preferences, /*detailsEntryId*/ undefined, host); - if (!completionData) { + }); +} +/* @internal */ +function completionNameForLiteral(literal: string | number | PseudoBigInt, preferences: UserPreferences): string { + return typeof literal === "object" ? pseudoBigIntToString(literal) + "n" : + isString(literal) ? quote(literal, preferences) : JSON.stringify(literal); +} +/* @internal */ +function createCompletionEntryForLiteral(literal: string | number | PseudoBigInt, preferences: UserPreferences): CompletionEntry { + return { name: completionNameForLiteral(literal, preferences), kind: ScriptElementKind.string, kindModifiers: ScriptElementKindModifier.none, sortText: SortText.LocationPriority }; +} +/* @internal */ +function createCompletionEntry(symbol: Symbol, sortText: SortText, location: Node | undefined, sourceFile: SourceFile, typeChecker: TypeChecker, name: string, needsConvertPropertyAccess: boolean, origin: SymbolOriginInfo | undefined, recommendedCompletion: Symbol | undefined, propertyAccessToConvert: PropertyAccessExpression | undefined, isJsxInitializer: IsJsxInitializer | undefined, preferences: UserPreferences): CompletionEntry | undefined { + let insertText: string | undefined; + let replacementSpan: TextSpan | undefined; + const insertQuestionDot = origin && originIsNullableMember(origin); + const useBraces = origin && originIsSymbolMember(origin) || needsConvertPropertyAccess; + if (origin && originIsThisType(origin)) { + insertText = needsConvertPropertyAccess + ? `this${insertQuestionDot ? "?." : ""}[${quotePropertyName(name, preferences)}]` + : `this${insertQuestionDot ? "?." : "."}${name}`; + } + // We should only have needsConvertPropertyAccess if there's a property access to convert. But see #21790. + // Somehow there was a global with a non-identifier name. Hopefully someone will complain about getting a "foo bar" global completion and provide a repro. + else if ((useBraces || insertQuestionDot) && propertyAccessToConvert) { + insertText = useBraces ? needsConvertPropertyAccess ? `[${quotePropertyName(name, preferences)}]` : `[${name}]` : name; + if (insertQuestionDot || propertyAccessToConvert.questionDotToken) { + insertText = `?.${insertText}`; + } + const dot = findChildOfKind(propertyAccessToConvert, SyntaxKind.DotToken, sourceFile) || + findChildOfKind(propertyAccessToConvert, SyntaxKind.QuestionDotToken, sourceFile); + if (!dot) { return undefined; } - - switch (completionData.kind) { - case CompletionDataKind.Data: - return completionInfoFromData(sourceFile, typeChecker, compilerOptions, log, completionData, preferences); - case CompletionDataKind.JsDocTagName: - // If the current position is a jsDoc tag name, only tag names should be provided for completion - return jsdocCompletionInfo(JsDoc.getJSDocTagNameCompletions()); - case CompletionDataKind.JsDocTag: - // If the current position is a jsDoc tag, only tags should be provided for completion - return jsdocCompletionInfo(JsDoc.getJSDocTagCompletions()); - case CompletionDataKind.JsDocParameterName: - return jsdocCompletionInfo(JsDoc.getJSDocParameterNameCompletions(completionData.tag)); - default: - return Debug.assertNever(completionData); + // If the text after the '.' starts with this name, write over it. Else, add new text. + const end = startsWith(name, propertyAccessToConvert.name.text) ? propertyAccessToConvert.name.end : dot.end; + replacementSpan = createTextSpanFromBounds(dot.getStart(sourceFile), end); + } + if (isJsxInitializer) { + if (insertText === undefined) + insertText = name; + insertText = `{${insertText}}`; + if (typeof isJsxInitializer !== "boolean") { + replacementSpan = createTextSpanFromNode(isJsxInitializer, sourceFile); } } - - function jsdocCompletionInfo(entries: CompletionEntry[]): CompletionInfo { - return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries }; + if (origin && originIsPromise(origin) && propertyAccessToConvert) { + if (insertText === undefined) + insertText = name; + const precedingToken = findPrecedingToken(propertyAccessToConvert.pos, sourceFile); + let awaitText = ""; + if (precedingToken && positionIsASICandidate(precedingToken.end, precedingToken.parent, sourceFile)) { + awaitText = ";"; + } + awaitText += `(await ${propertyAccessToConvert.expression.getText()})`; + insertText = needsConvertPropertyAccess ? `${awaitText}${insertText}` : `${awaitText}${insertQuestionDot ? "?." : "."}${insertText}`; + replacementSpan = createTextSpanFromBounds(propertyAccessToConvert.getStart(sourceFile), propertyAccessToConvert.end); } - - function completionInfoFromData(sourceFile: SourceFile, typeChecker: TypeChecker, compilerOptions: CompilerOptions, log: Log, completionData: CompletionData, preferences: UserPreferences): CompletionInfo | undefined { - const { - symbols, - completionKind, - isInSnippetScope, - isNewIdentifierLocation, - location, - propertyAccessToConvert, - keywordFilters, - literals, - symbolToOriginInfoMap, - recommendedCompletion, - isJsxInitializer, - insideJsDocTagTypeExpression, - symbolToSortTextMap, - } = completionData; - - if (location && location.parent && isJsxClosingElement(location.parent)) { - // In the TypeScript JSX element, if such element is not defined. When users query for completion at closing tag, - // instead of simply giving unknown value, the completion will return the tag-name of an associated opening-element. - // For example: - // var x =
" with type any - // And at `
` (with a closing `>`), the completion list will contain "div". - const tagName = location.parent.parent.openingElement.tagName; - const hasClosingAngleBracket = !!findChildOfKind(location.parent, SyntaxKind.GreaterThanToken, sourceFile); - const entry: CompletionEntry = { - name: tagName.getFullText(sourceFile) + (hasClosingAngleBracket ? "" : ">"), - kind: ScriptElementKind.classElement, - kindModifiers: undefined, - sortText: SortText.LocationPriority, - }; - return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: false, entries: [entry] }; - } - - const entries: CompletionEntry[] = []; - - if (isUncheckedFile(sourceFile, compilerOptions)) { - const uniqueNames = getCompletionEntriesFromSymbols( - symbols, - entries, - location, - sourceFile, - typeChecker, - compilerOptions.target!, - log, - completionKind, - preferences, - propertyAccessToConvert, - isJsxInitializer, - recommendedCompletion, - symbolToOriginInfoMap, - symbolToSortTextMap - ); - getJSCompletionEntries(sourceFile, location!.pos, uniqueNames, compilerOptions.target!, entries); // TODO: GH#18217 - } - else { - if (!isNewIdentifierLocation && (!symbols || symbols.length === 0) && keywordFilters === KeywordCompletionFilters.None) { - return undefined; - } - - getCompletionEntriesFromSymbols( - symbols, - entries, - location, - sourceFile, - typeChecker, - compilerOptions.target!, - log, - completionKind, - preferences, - propertyAccessToConvert, - isJsxInitializer, - recommendedCompletion, - symbolToOriginInfoMap, - symbolToSortTextMap - ); - } - - if (keywordFilters !== KeywordCompletionFilters.None) { - const entryNames = arrayToSet(entries, e => e.name); - for (const keywordEntry of getKeywordCompletions(keywordFilters, !insideJsDocTagTypeExpression && isSourceFileJS(sourceFile))) { - if (!entryNames.has(keywordEntry.name)) { - entries.push(keywordEntry); - } - } - } - - for (const literal of literals) { - entries.push(createCompletionEntryForLiteral(literal, preferences)); - } - - return { isGlobalCompletion: isInSnippetScope, isMemberCompletion: isMemberCompletionKind(completionKind), isNewIdentifierLocation, entries }; + if (insertText !== undefined && !preferences.includeCompletionsWithInsertText) { + return undefined; } - - function isUncheckedFile(sourceFile: SourceFile, compilerOptions: CompilerOptions): boolean { - return isSourceFileJS(sourceFile) && !isCheckJsEnabledForFile(sourceFile, compilerOptions); + // TODO(drosen): Right now we just permit *all* semantic meanings when calling + // 'getSymbolKind' which is permissible given that it is backwards compatible; but + // really we should consider passing the meaning for the node so that we don't report + // that a suggestion for a value is an interface. We COULD also just do what + // 'getSymbolModifiers' does, which is to use the first declaration. + // Use a 'sortText' of 0' so that all symbol completion entries come before any other + // entries (like JavaScript identifier entries). + return { + name, + kind: getSymbolKind(typeChecker, symbol, (location!)), + kindModifiers: getSymbolModifiers(symbol), + sortText, + source: getSourceFromOrigin(origin), + hasAction: origin && originIsExport(origin) || undefined, + isRecommended: isRecommendedCompletionMatch(symbol, recommendedCompletion, typeChecker) || undefined, + insertText, + replacementSpan, + }; +} +/* @internal */ +function quotePropertyName(name: string, preferences: UserPreferences): string { + if (/^\d+$/.test(name)) { + return name; } - - function isMemberCompletionKind(kind: CompletionKind): boolean { - switch (kind) { - case CompletionKind.ObjectPropertyDeclaration: - case CompletionKind.MemberLike: - case CompletionKind.PropertyAccess: - return true; - default: - return false; - } + return quote(name, preferences); +} +/* @internal */ +function isRecommendedCompletionMatch(localSymbol: Symbol, recommendedCompletion: Symbol | undefined, checker: TypeChecker): boolean { + return localSymbol === recommendedCompletion || + !!(localSymbol.flags & SymbolFlags.ExportValue) && checker.getExportSymbolOfSymbol(localSymbol) === recommendedCompletion; +} +/* @internal */ +function getSourceFromOrigin(origin: SymbolOriginInfo | undefined): string | undefined { + return origin && originIsExport(origin) ? stripQuotes(origin.moduleSymbol.name) : undefined; +} +/* @internal */ +export function getCompletionEntriesFromSymbols(symbols: readonly Symbol[], entries: Push, location: Node | undefined, sourceFile: SourceFile, typeChecker: TypeChecker, target: ScriptTarget, log: Log, kind: CompletionKind, preferences: UserPreferences, propertyAccessToConvert?: PropertyAccessExpression, isJsxInitializer?: IsJsxInitializer, recommendedCompletion?: Symbol, symbolToOriginInfoMap?: SymbolOriginInfoMap, symbolToSortTextMap?: SymbolSortTextMap): ts.Map { + const start = timestamp(); + // Tracks unique names. + // We don't set this for global variables or completions from external module exports, because we can have multiple of those. + // Based on the order we add things we will always see locals first, then globals, then module exports. + // So adding a completion for a local will prevent us from adding completions for external module exports sharing the same name. + const uniques = createMap(); + for (const symbol of symbols) { + const origin = symbolToOriginInfoMap ? symbolToOriginInfoMap[getSymbolId(symbol)] : undefined; + const info = getCompletionEntryDisplayNameForSymbol(symbol, target, origin, kind); + if (!info) { + continue; + } + const { name, needsConvertPropertyAccess } = info; + if (uniques.has(name)) { + continue; + } + const entry = createCompletionEntry(symbol, symbolToSortTextMap && symbolToSortTextMap[getSymbolId(symbol)] || SortText.LocationPriority, location, sourceFile, typeChecker, name, needsConvertPropertyAccess, origin, recommendedCompletion, propertyAccessToConvert, isJsxInitializer, preferences); + if (!entry) { + continue; + } + // Latter case tests whether this is a global variable. + if (!origin && !(symbol.parent === undefined && !some(symbol.declarations, d => d.getSourceFile() === location!.getSourceFile()))) { // TODO: GH#18217 + uniques.set(name, true); + } + entries.push(entry); } - - function getJSCompletionEntries( - sourceFile: SourceFile, - position: number, - uniqueNames: Map, - target: ScriptTarget, - entries: Push): void { - getNameTable(sourceFile).forEach((pos, name) => { - // Skip identifiers produced only from the current location - if (pos === position) { - return; - } - const realName = unescapeLeadingUnderscores(name); - if (addToSeen(uniqueNames, realName) && isIdentifierText(realName, target)) { + log("getCompletionsAtPosition: getCompletionEntriesFromSymbols: " + (timestamp() - start)); + return uniques; +} +/* @internal */ +function getLabelCompletionAtPosition(node: BreakOrContinueStatement): CompletionInfo | undefined { + const entries = getLabelStatementCompletions(node); + if (entries.length) { + return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries }; + } +} +/* @internal */ +function getLabelStatementCompletions(node: Node): CompletionEntry[] { + const entries: CompletionEntry[] = []; + const uniques = createMap(); + let current = node; + while (current) { + if (isFunctionLike(current)) { + break; + } + if (isLabeledStatement(current)) { + const name = current.label.text; + if (!uniques.has(name)) { + uniques.set(name, true); entries.push({ - name: realName, - kind: ScriptElementKind.warning, - kindModifiers: "", - sortText: SortText.JavascriptIdentifiers, - isFromUncheckedFile: true + name, + kindModifiers: ScriptElementKindModifier.none, + kind: ScriptElementKind.label, + sortText: SortText.LocationPriority }); } - }); + } + current = current.parent; } - - function completionNameForLiteral(literal: string | number | PseudoBigInt, preferences: UserPreferences): string { - return typeof literal === "object" ? pseudoBigIntToString(literal) + "n" : - isString(literal) ? quote(literal, preferences) : JSON.stringify(literal); - } - - function createCompletionEntryForLiteral(literal: string | number | PseudoBigInt, preferences: UserPreferences): CompletionEntry { - return { name: completionNameForLiteral(literal, preferences), kind: ScriptElementKind.string, kindModifiers: ScriptElementKindModifier.none, sortText: SortText.LocationPriority }; - } - - function createCompletionEntry( - symbol: Symbol, - sortText: SortText, - location: Node | undefined, - sourceFile: SourceFile, - typeChecker: TypeChecker, - name: string, - needsConvertPropertyAccess: boolean, - origin: SymbolOriginInfo | undefined, - recommendedCompletion: Symbol | undefined, - propertyAccessToConvert: PropertyAccessExpression | undefined, - isJsxInitializer: IsJsxInitializer | undefined, - preferences: UserPreferences, - ): CompletionEntry | undefined { - let insertText: string | undefined; - let replacementSpan: TextSpan | undefined; - - const insertQuestionDot = origin && originIsNullableMember(origin); - const useBraces = origin && originIsSymbolMember(origin) || needsConvertPropertyAccess; - if (origin && originIsThisType(origin)) { - insertText = needsConvertPropertyAccess - ? `this${insertQuestionDot ? "?." : ""}[${quotePropertyName(name, preferences)}]` - : `this${insertQuestionDot ? "?." : "."}${name}`; - } - // We should only have needsConvertPropertyAccess if there's a property access to convert. But see #21790. - // Somehow there was a global with a non-identifier name. Hopefully someone will complain about getting a "foo bar" global completion and provide a repro. - else if ((useBraces || insertQuestionDot) && propertyAccessToConvert) { - insertText = useBraces ? needsConvertPropertyAccess ? `[${quotePropertyName(name, preferences)}]` : `[${name}]` : name; - if (insertQuestionDot || propertyAccessToConvert.questionDotToken) { - insertText = `?.${insertText}`; - } - - const dot = findChildOfKind(propertyAccessToConvert, SyntaxKind.DotToken, sourceFile) || - findChildOfKind(propertyAccessToConvert, SyntaxKind.QuestionDotToken, sourceFile); - if (!dot) { - return undefined; - } - // If the text after the '.' starts with this name, write over it. Else, add new text. - const end = startsWith(name, propertyAccessToConvert.name.text) ? propertyAccessToConvert.name.end : dot.end; - replacementSpan = createTextSpanFromBounds(dot.getStart(sourceFile), end); - } - - if (isJsxInitializer) { - if (insertText === undefined) insertText = name; - insertText = `{${insertText}}`; - if (typeof isJsxInitializer !== "boolean") { - replacementSpan = createTextSpanFromNode(isJsxInitializer, sourceFile); + return entries; +} +/* @internal */ +interface SymbolCompletion { + type: "symbol"; + symbol: Symbol; + location: Node | undefined; + symbolToOriginInfoMap: SymbolOriginInfoMap; + previousToken: Node | undefined; + readonly isJsxInitializer: IsJsxInitializer; + readonly isTypeOnlyLocation: boolean; +} +/* @internal */ +function getSymbolCompletionFromEntryId(program: Program, log: Log, sourceFile: SourceFile, position: number, entryId: CompletionEntryIdentifier, host: LanguageServiceHost, preferences: UserPreferences): SymbolCompletion | { + type: "request"; + request: Request; +} | { + type: "literal"; + literal: string | number | PseudoBigInt; +} | { + type: "none"; +} { + const compilerOptions = program.getCompilerOptions(); + const completionData = getCompletionData(program, log, sourceFile, isUncheckedFile(sourceFile, compilerOptions), position, { includeCompletionsForModuleExports: true, includeCompletionsWithInsertText: true }, entryId, host); + if (!completionData) { + return { type: "none" }; + } + if (completionData.kind !== CompletionDataKind.Data) { + return { type: "request", request: completionData }; + } + const { symbols, literals, location, completionKind, symbolToOriginInfoMap, previousToken, isJsxInitializer, isTypeOnlyLocation } = completionData; + const literal = find(literals, l => completionNameForLiteral(l, preferences) === entryId.name); + if (literal !== undefined) + return { type: "literal", literal }; + // Find the symbol with the matching entry name. + // We don't need to perform character checks here because we're only comparing the + // name against 'entryName' (which is known to be good), not building a new + // completion entry. + return firstDefined(symbols, (symbol): SymbolCompletion | undefined => { + const origin = symbolToOriginInfoMap[getSymbolId(symbol)]; + const info = getCompletionEntryDisplayNameForSymbol(symbol, compilerOptions.target!, origin, completionKind); + return info && info.name === entryId.name && getSourceFromOrigin(origin) === entryId.source + ? { type: "symbol" as const, symbol, location, symbolToOriginInfoMap, previousToken, isJsxInitializer, isTypeOnlyLocation } + : undefined; + }) || { type: "none" }; +} +/* @internal */ +export interface CompletionEntryIdentifier { + name: string; + source?: string; +} +/* @internal */ +export function getCompletionEntryDetails(program: Program, log: Log, sourceFile: SourceFile, position: number, entryId: CompletionEntryIdentifier, host: LanguageServiceHost, formatContext: FormatContext, preferences: UserPreferences, cancellationToken: CancellationToken): CompletionEntryDetails | undefined { + const typeChecker = program.getTypeChecker(); + const compilerOptions = program.getCompilerOptions(); + const { name } = entryId; + const contextToken = findPrecedingToken(position, sourceFile); + if (isInString(sourceFile, position, contextToken)) { + return getStringLiteralCompletionDetails(name, sourceFile, position, contextToken, typeChecker, compilerOptions, host, cancellationToken); + } + // Compute all the completion symbols again. + const symbolCompletion = getSymbolCompletionFromEntryId(program, log, sourceFile, position, entryId, host, preferences); + switch (symbolCompletion.type) { + case "request": { + const { request } = symbolCompletion; + switch (request.kind) { + case CompletionDataKind.JsDocTagName: + return getJSDocTagNameCompletionDetails(name); + case CompletionDataKind.JsDocTag: + return getJSDocTagCompletionDetails(name); + case CompletionDataKind.JsDocParameterName: + return getJSDocParameterNameCompletionDetails(name); + default: + return Debug.assertNever(request); } } - if (origin && originIsPromise(origin) && propertyAccessToConvert) { - if (insertText === undefined) insertText = name; - const precedingToken = findPrecedingToken(propertyAccessToConvert.pos, sourceFile); - let awaitText = ""; - if (precedingToken && positionIsASICandidate(precedingToken.end, precedingToken.parent, sourceFile)) { - awaitText = ";"; - } - - awaitText += `(await ${propertyAccessToConvert.expression.getText()})`; - insertText = needsConvertPropertyAccess ? `${awaitText}${insertText}` : `${awaitText}${insertQuestionDot ? "?." : "."}${insertText}`; - replacementSpan = createTextSpanFromBounds(propertyAccessToConvert.getStart(sourceFile), propertyAccessToConvert.end); + case "symbol": { + const { symbol, location, symbolToOriginInfoMap, previousToken } = symbolCompletion; + const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(symbolToOriginInfoMap, symbol, program, typeChecker, host, compilerOptions, sourceFile, position, previousToken, formatContext, preferences); + return createCompletionDetailsForSymbol(symbol, typeChecker, sourceFile, location!, cancellationToken, codeActions, sourceDisplay); // TODO: GH#18217 } - - if (insertText !== undefined && !preferences.includeCompletionsWithInsertText) { - return undefined; + case "literal": { + const { literal } = symbolCompletion; + return createSimpleDetails(completionNameForLiteral(literal, preferences), ScriptElementKind.string, typeof literal === "string" ? SymbolDisplayPartKind.stringLiteral : SymbolDisplayPartKind.numericLiteral); } - - // TODO(drosen): Right now we just permit *all* semantic meanings when calling - // 'getSymbolKind' which is permissible given that it is backwards compatible; but - // really we should consider passing the meaning for the node so that we don't report - // that a suggestion for a value is an interface. We COULD also just do what - // 'getSymbolModifiers' does, which is to use the first declaration. - - // Use a 'sortText' of 0' so that all symbol completion entries come before any other - // entries (like JavaScript identifier entries). - return { - name, - kind: SymbolDisplay.getSymbolKind(typeChecker, symbol, location!), // TODO: GH#18217 - kindModifiers: SymbolDisplay.getSymbolModifiers(symbol), - sortText, - source: getSourceFromOrigin(origin), - hasAction: origin && originIsExport(origin) || undefined, - isRecommended: isRecommendedCompletionMatch(symbol, recommendedCompletion, typeChecker) || undefined, - insertText, - replacementSpan, - }; + case "none": + // Didn't find a symbol with this name. See if we can find a keyword instead. + return allKeywordsCompletions().some(c => c.name === name) ? createSimpleDetails(name, ScriptElementKind.keyword, SymbolDisplayPartKind.keyword) : undefined; + default: + Debug.assertNever(symbolCompletion); } - - function quotePropertyName(name: string, preferences: UserPreferences): string { - if (/^\d+$/.test(name)) { - return name; - } - - return quote(name, preferences); - } - - function isRecommendedCompletionMatch(localSymbol: Symbol, recommendedCompletion: Symbol | undefined, checker: TypeChecker): boolean { - return localSymbol === recommendedCompletion || - !!(localSymbol.flags & SymbolFlags.ExportValue) && checker.getExportSymbolOfSymbol(localSymbol) === recommendedCompletion; - } - - function getSourceFromOrigin(origin: SymbolOriginInfo | undefined): string | undefined { - return origin && originIsExport(origin) ? stripQuotes(origin.moduleSymbol.name) : undefined; - } - - export function getCompletionEntriesFromSymbols( - symbols: readonly Symbol[], - entries: Push, - location: Node | undefined, - sourceFile: SourceFile, - typeChecker: TypeChecker, - target: ScriptTarget, - log: Log, - kind: CompletionKind, - preferences: UserPreferences, - propertyAccessToConvert?: PropertyAccessExpression, - isJsxInitializer?: IsJsxInitializer, - recommendedCompletion?: Symbol, - symbolToOriginInfoMap?: SymbolOriginInfoMap, - symbolToSortTextMap?: SymbolSortTextMap, - ): Map { - const start = timestamp(); - // Tracks unique names. - // We don't set this for global variables or completions from external module exports, because we can have multiple of those. - // Based on the order we add things we will always see locals first, then globals, then module exports. - // So adding a completion for a local will prevent us from adding completions for external module exports sharing the same name. - const uniques = createMap(); - for (const symbol of symbols) { - const origin = symbolToOriginInfoMap ? symbolToOriginInfoMap[getSymbolId(symbol)] : undefined; - const info = getCompletionEntryDisplayNameForSymbol(symbol, target, origin, kind); - if (!info) { - continue; - } - const { name, needsConvertPropertyAccess } = info; - if (uniques.has(name)) { - continue; - } - - const entry = createCompletionEntry( - symbol, - symbolToSortTextMap && symbolToSortTextMap[getSymbolId(symbol)] || SortText.LocationPriority, - location, - sourceFile, - typeChecker, - name, - needsConvertPropertyAccess, - origin, - recommendedCompletion, - propertyAccessToConvert, - isJsxInitializer, - preferences - ); - if (!entry) { - continue; - } - - // Latter case tests whether this is a global variable. - if (!origin && !(symbol.parent === undefined && !some(symbol.declarations, d => d.getSourceFile() === location!.getSourceFile()))) { // TODO: GH#18217 - uniques.set(name, true); +} +/* @internal */ +function createSimpleDetails(name: string, kind: ScriptElementKind, kind2: SymbolDisplayPartKind): CompletionEntryDetails { + return createCompletionDetails(name, ScriptElementKindModifier.none, kind, [displayPart(name, kind2)]); +} +/* @internal */ +export function createCompletionDetailsForSymbol(symbol: Symbol, checker: TypeChecker, sourceFile: SourceFile, location: Node, cancellationToken: CancellationToken, codeActions?: CodeAction[], sourceDisplay?: SymbolDisplayPart[]): CompletionEntryDetails { + const { displayParts, documentation, symbolKind, tags } = checker.runWithCancellationToken(cancellationToken, checker => getSymbolDisplayPartsDocumentationAndSymbolKind(checker, symbol, sourceFile, location, location, SemanticMeaning.All)); + return createCompletionDetails(symbol.name, getSymbolModifiers(symbol), symbolKind, displayParts, documentation, tags, codeActions, sourceDisplay); +} +/* @internal */ +export function createCompletionDetails(name: string, kindModifiers: string, kind: ScriptElementKind, displayParts: SymbolDisplayPart[], documentation?: SymbolDisplayPart[], tags?: JSDocTagInfo[], codeActions?: CodeAction[], source?: SymbolDisplayPart[]): CompletionEntryDetails { + return { name, kindModifiers, kind, displayParts, documentation, tags, codeActions, source }; +} +/* @internal */ +interface CodeActionsAndSourceDisplay { + readonly codeActions: CodeAction[] | undefined; + readonly sourceDisplay: SymbolDisplayPart[] | undefined; +} +/* @internal */ +function getCompletionEntryCodeActionsAndSourceDisplay(symbolToOriginInfoMap: SymbolOriginInfoMap, symbol: Symbol, program: Program, checker: TypeChecker, host: LanguageServiceHost, compilerOptions: CompilerOptions, sourceFile: SourceFile, position: number, previousToken: Node | undefined, formatContext: FormatContext, preferences: UserPreferences): CodeActionsAndSourceDisplay { + const symbolOriginInfo = symbolToOriginInfoMap[getSymbolId(symbol)]; + if (!symbolOriginInfo || !originIsExport(symbolOriginInfo)) { + return { codeActions: undefined, sourceDisplay: undefined }; + } + const { moduleSymbol } = symbolOriginInfo; + const exportedSymbol = checker.getMergedSymbol(skipAlias(symbol.exportSymbol || symbol, checker)); + const { moduleSpecifier, codeAction } = getImportCompletionAction(exportedSymbol, moduleSymbol, sourceFile, getNameForExportedSymbol(symbol, (compilerOptions.target!)), host, program, formatContext, previousToken && isIdentifier(previousToken) ? previousToken.getStart(sourceFile) : position, preferences); + return { sourceDisplay: [textPart(moduleSpecifier)], codeActions: [codeAction] }; +} +/* @internal */ +export function getCompletionEntrySymbol(program: Program, log: Log, sourceFile: SourceFile, position: number, entryId: CompletionEntryIdentifier, host: LanguageServiceHost, preferences: UserPreferences): Symbol | undefined { + const completion = getSymbolCompletionFromEntryId(program, log, sourceFile, position, entryId, host, preferences); + return completion.type === "symbol" ? completion.symbol : undefined; +} +/* @internal */ +const enum CompletionDataKind { + Data, + JsDocTagName, + JsDocTag, + JsDocParameterName +} +/** true: after the `=` sign but no identifier has been typed yet. Else is the Identifier after the initializer. */ +/* @internal */ +type IsJsxInitializer = boolean | Identifier; +/* @internal */ +interface CompletionData { + readonly kind: CompletionDataKind.Data; + readonly symbols: readonly Symbol[]; + readonly completionKind: CompletionKind; + readonly isInSnippetScope: boolean; + /** Note that the presence of this alone doesn't mean that we need a conversion. Only do that if the completion is not an ordinary identifier. */ + readonly propertyAccessToConvert: PropertyAccessExpression | undefined; + readonly isNewIdentifierLocation: boolean; + readonly location: Node | undefined; + readonly keywordFilters: KeywordCompletionFilters; + readonly literals: readonly (string | number | PseudoBigInt)[]; + readonly symbolToOriginInfoMap: SymbolOriginInfoMap; + readonly recommendedCompletion: Symbol | undefined; + readonly previousToken: Node | undefined; + readonly isJsxInitializer: IsJsxInitializer; + readonly insideJsDocTagTypeExpression: boolean; + readonly symbolToSortTextMap: SymbolSortTextMap; + readonly isTypeOnlyLocation: boolean; +} +/* @internal */ +type Request = { + readonly kind: CompletionDataKind.JsDocTagName | CompletionDataKind.JsDocTag; +} | { + readonly kind: CompletionDataKind.JsDocParameterName; + tag: JSDocParameterTag; +}; +/* @internal */ +export const enum CompletionKind { + ObjectPropertyDeclaration, + Global, + PropertyAccess, + MemberLike, + String, + None +} +/* @internal */ +function getRecommendedCompletion(previousToken: Node, contextualType: Type, checker: TypeChecker): Symbol | undefined { + // For a union, return the first one with a recommended completion. + return firstDefined(contextualType && (contextualType.isUnion() ? contextualType.types : [contextualType]), type => { + const symbol = type && type.symbol; + // Don't include make a recommended completion for an abstract class + return symbol && (symbol.flags & (SymbolFlags.EnumMember | SymbolFlags.Enum | SymbolFlags.Class) && !isAbstractConstructorSymbol(symbol)) + ? getFirstSymbolInChain(symbol, previousToken, checker) + : undefined; + }); +} +/* @internal */ +function getContextualType(previousToken: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): Type | undefined { + const { parent } = previousToken; + switch (previousToken.kind) { + case SyntaxKind.Identifier: + return getContextualTypeFromParent((previousToken as Identifier), checker); + case SyntaxKind.EqualsToken: + switch (parent.kind) { + case SyntaxKind.VariableDeclaration: + return checker.getContextualType(((parent as VariableDeclaration).initializer!)); // TODO: GH#18217 + case SyntaxKind.BinaryExpression: + return checker.getTypeAtLocation((parent as BinaryExpression).left); + case SyntaxKind.JsxAttribute: + return checker.getContextualTypeForJsxAttribute((parent as JsxAttribute)); + default: + return undefined; } - - entries.push(entry); - } - - log("getCompletionsAtPosition: getCompletionEntriesFromSymbols: " + (timestamp() - start)); - return uniques; - } - - function getLabelCompletionAtPosition(node: BreakOrContinueStatement): CompletionInfo | undefined { - const entries = getLabelStatementCompletions(node); - if (entries.length) { - return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries }; - } - } - - function getLabelStatementCompletions(node: Node): CompletionEntry[] { - const entries: CompletionEntry[] = []; - const uniques = createMap(); - let current = node; - - while (current) { - if (isFunctionLike(current)) { - break; + case SyntaxKind.NewKeyword: + return checker.getContextualType((parent as Expression)); + case SyntaxKind.CaseKeyword: + return getSwitchedType(cast(parent, isCaseClause), checker); + case SyntaxKind.OpenBraceToken: + return isJsxExpression(parent) && parent.parent.kind !== SyntaxKind.JsxElement ? checker.getContextualTypeForJsxAttribute(parent.parent) : undefined; + default: + const argInfo = getArgumentInfoForCompletions(previousToken, position, sourceFile); + return argInfo ? + // At `,`, treat this as the next argument after the comma. + checker.getContextualTypeForArgumentAtIndex(argInfo.invocation, argInfo.argumentIndex + (previousToken.kind === SyntaxKind.CommaToken ? 1 : 0)) : + isEqualityOperatorKind(previousToken.kind) && isBinaryExpression(parent) && isEqualityOperatorKind(parent.operatorToken.kind) ? + // completion at `x ===/**/` should be for the right side + checker.getTypeAtLocation(parent.left) : + checker.getContextualType((previousToken as Expression)); + } +} +/* @internal */ +function getFirstSymbolInChain(symbol: Symbol, enclosingDeclaration: Node, checker: TypeChecker): Symbol | undefined { + const chain = checker.getAccessibleSymbolChain(symbol, enclosingDeclaration, /*meaning*/ SymbolFlags.All, /*useOnlyExternalAliasing*/ false); + if (chain) + return first(chain); + return symbol.parent && (isModuleSymbol(symbol.parent) ? symbol : getFirstSymbolInChain(symbol.parent, enclosingDeclaration, checker)); +} +/* @internal */ +function isModuleSymbol(symbol: Symbol): boolean { + return symbol.declarations.some(d => d.kind === SyntaxKind.SourceFile); +} +/* @internal */ +function getCompletionData(program: Program, log: (message: string) => void, sourceFile: SourceFile, isUncheckedFile: boolean, position: number, preferences: Pick, detailsEntryId: CompletionEntryIdentifier | undefined, host: LanguageServiceHost): CompletionData | Request | undefined { + const typeChecker = program.getTypeChecker(); + let start = timestamp(); + let currentToken = getTokenAtPosition(sourceFile, position); // TODO: GH#15853 + // We will check for jsdoc comments with insideComment and getJsDocTagAtPosition. (TODO: that seems rather inefficient to check the same thing so many times.) + log("getCompletionData: Get current token: " + (timestamp() - start)); + start = timestamp(); + const insideComment = isInComment(sourceFile, position, currentToken); + log("getCompletionData: Is inside comment: " + (timestamp() - start)); + let insideJsDocTagTypeExpression = false; + let isInSnippetScope = false; + if (insideComment) { + if (hasDocComment(sourceFile, position)) { + if (sourceFile.text.charCodeAt(position - 1) === CharacterCodes.at) { + // The current position is next to the '@' sign, when no tag name being provided yet. + // Provide a full list of tag names + return { kind: CompletionDataKind.JsDocTagName }; } - if (isLabeledStatement(current)) { - const name = current.label.text; - if (!uniques.has(name)) { - uniques.set(name, true); - entries.push({ - name, - kindModifiers: ScriptElementKindModifier.none, - kind: ScriptElementKind.label, - sortText: SortText.LocationPriority - }); + else { + // When completion is requested without "@", we will have check to make sure that + // there are no comments prefix the request position. We will only allow "*" and space. + // e.g + // /** |c| /* + // + // /** + // |c| + // */ + // + // /** + // * |c| + // */ + // + // /** + // * |c| + // */ + const lineStart = getLineStartPositionForPosition(position, sourceFile); + if (!(sourceFile.text.substring(lineStart, position).match(/[^\*|\s|(/\*\*)]/))) { + return { kind: CompletionDataKind.JsDocTag }; } } - current = current.parent; - } - return entries; - } - - interface SymbolCompletion { - type: "symbol"; - symbol: Symbol; - location: Node | undefined; - symbolToOriginInfoMap: SymbolOriginInfoMap; - previousToken: Node | undefined; - readonly isJsxInitializer: IsJsxInitializer; - readonly isTypeOnlyLocation: boolean; - } - function getSymbolCompletionFromEntryId( - program: Program, - log: Log, - sourceFile: SourceFile, - position: number, - entryId: CompletionEntryIdentifier, - host: LanguageServiceHost, - preferences: UserPreferences, - ): SymbolCompletion | { type: "request", request: Request } | { type: "literal", literal: string | number | PseudoBigInt } | { type: "none" } { - const compilerOptions = program.getCompilerOptions(); - const completionData = getCompletionData(program, log, sourceFile, isUncheckedFile(sourceFile, compilerOptions), position, { includeCompletionsForModuleExports: true, includeCompletionsWithInsertText: true }, entryId, host); - if (!completionData) { - return { type: "none" }; - } - if (completionData.kind !== CompletionDataKind.Data) { - return { type: "request", request: completionData }; - } - - const { symbols, literals, location, completionKind, symbolToOriginInfoMap, previousToken, isJsxInitializer, isTypeOnlyLocation } = completionData; - - const literal = find(literals, l => completionNameForLiteral(l, preferences) === entryId.name); - if (literal !== undefined) return { type: "literal", literal }; - - // Find the symbol with the matching entry name. - // We don't need to perform character checks here because we're only comparing the - // name against 'entryName' (which is known to be good), not building a new - // completion entry. - return firstDefined(symbols, (symbol): SymbolCompletion | undefined => { - const origin = symbolToOriginInfoMap[getSymbolId(symbol)]; - const info = getCompletionEntryDisplayNameForSymbol(symbol, compilerOptions.target!, origin, completionKind); - return info && info.name === entryId.name && getSourceFromOrigin(origin) === entryId.source - ? { type: "symbol" as const, symbol, location, symbolToOriginInfoMap, previousToken, isJsxInitializer, isTypeOnlyLocation } - : undefined; - }) || { type: "none" }; - } - - export interface CompletionEntryIdentifier { - name: string; - source?: string; - } - - export function getCompletionEntryDetails( - program: Program, - log: Log, - sourceFile: SourceFile, - position: number, - entryId: CompletionEntryIdentifier, - host: LanguageServiceHost, - formatContext: formatting.FormatContext, - preferences: UserPreferences, - cancellationToken: CancellationToken, - ): CompletionEntryDetails | undefined { - const typeChecker = program.getTypeChecker(); - const compilerOptions = program.getCompilerOptions(); - const { name } = entryId; - - const contextToken = findPrecedingToken(position, sourceFile); - if (isInString(sourceFile, position, contextToken)) { - return StringCompletions.getStringLiteralCompletionDetails(name, sourceFile, position, contextToken, typeChecker, compilerOptions, host, cancellationToken); - } - - // Compute all the completion symbols again. - const symbolCompletion = getSymbolCompletionFromEntryId(program, log, sourceFile, position, entryId, host, preferences); - switch (symbolCompletion.type) { - case "request": { - const { request } = symbolCompletion; - switch (request.kind) { - case CompletionDataKind.JsDocTagName: - return JsDoc.getJSDocTagNameCompletionDetails(name); - case CompletionDataKind.JsDocTag: - return JsDoc.getJSDocTagCompletionDetails(name); - case CompletionDataKind.JsDocParameterName: - return JsDoc.getJSDocParameterNameCompletionDetails(name); - default: - return Debug.assertNever(request); + } + // Completion should work inside certain JsDoc tags. For example: + // /** @type {number | string} */ + // Completion should work in the brackets + const tag = getJsDocTagAtPosition(currentToken, position); + if (tag) { + if (tag.tagName.pos <= position && position <= tag.tagName.end) { + return { kind: CompletionDataKind.JsDocTagName }; + } + if (isTagWithTypeExpression(tag) && tag.typeExpression && tag.typeExpression.kind === SyntaxKind.JSDocTypeExpression) { + currentToken = getTokenAtPosition(sourceFile, position); + if (!currentToken || + (!isDeclarationName(currentToken) && + (currentToken.parent.kind !== SyntaxKind.JSDocPropertyTag || + (currentToken.parent).name !== currentToken))) { + // Use as type location if inside tag's type expression + insideJsDocTagTypeExpression = isCurrentlyEditingNode(tag.typeExpression); } } - case "symbol": { - const { symbol, location, symbolToOriginInfoMap, previousToken } = symbolCompletion; - const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(symbolToOriginInfoMap, symbol, program, typeChecker, host, compilerOptions, sourceFile, position, previousToken, formatContext, preferences); - return createCompletionDetailsForSymbol(symbol, typeChecker, sourceFile, location!, cancellationToken, codeActions, sourceDisplay); // TODO: GH#18217 + if (!insideJsDocTagTypeExpression && isJSDocParameterTag(tag) && (nodeIsMissing(tag.name) || tag.name.pos <= position && position <= tag.name.end)) { + return { kind: CompletionDataKind.JsDocParameterName, tag }; } - case "literal": { - const { literal } = symbolCompletion; - return createSimpleDetails(completionNameForLiteral(literal, preferences), ScriptElementKind.string, typeof literal === "string" ? SymbolDisplayPartKind.stringLiteral : SymbolDisplayPartKind.numericLiteral); - } - case "none": - // Didn't find a symbol with this name. See if we can find a keyword instead. - return allKeywordsCompletions().some(c => c.name === name) ? createSimpleDetails(name, ScriptElementKind.keyword, SymbolDisplayPartKind.keyword) : undefined; - default: - Debug.assertNever(symbolCompletion); - } - } - - function createSimpleDetails(name: string, kind: ScriptElementKind, kind2: SymbolDisplayPartKind): CompletionEntryDetails { - return createCompletionDetails(name, ScriptElementKindModifier.none, kind, [displayPart(name, kind2)]); - } - - export function createCompletionDetailsForSymbol(symbol: Symbol, checker: TypeChecker, sourceFile: SourceFile, location: Node, cancellationToken: CancellationToken, codeActions?: CodeAction[], sourceDisplay?: SymbolDisplayPart[]): CompletionEntryDetails { - const { displayParts, documentation, symbolKind, tags } = - checker.runWithCancellationToken(cancellationToken, checker => - SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(checker, symbol, sourceFile, location, location, SemanticMeaning.All) - ); - return createCompletionDetails(symbol.name, SymbolDisplay.getSymbolModifiers(symbol), symbolKind, displayParts, documentation, tags, codeActions, sourceDisplay); - } - - export function createCompletionDetails(name: string, kindModifiers: string, kind: ScriptElementKind, displayParts: SymbolDisplayPart[], documentation?: SymbolDisplayPart[], tags?: JSDocTagInfo[], codeActions?: CodeAction[], source?: SymbolDisplayPart[]): CompletionEntryDetails { - return { name, kindModifiers, kind, displayParts, documentation, tags, codeActions, source }; - } - - interface CodeActionsAndSourceDisplay { - readonly codeActions: CodeAction[] | undefined; - readonly sourceDisplay: SymbolDisplayPart[] | undefined; - } - function getCompletionEntryCodeActionsAndSourceDisplay( - symbolToOriginInfoMap: SymbolOriginInfoMap, - symbol: Symbol, - program: Program, - checker: TypeChecker, - host: LanguageServiceHost, - compilerOptions: CompilerOptions, - sourceFile: SourceFile, - position: number, - previousToken: Node | undefined, - formatContext: formatting.FormatContext, - preferences: UserPreferences, - ): CodeActionsAndSourceDisplay { - const symbolOriginInfo = symbolToOriginInfoMap[getSymbolId(symbol)]; - if (!symbolOriginInfo || !originIsExport(symbolOriginInfo)) { - return { codeActions: undefined, sourceDisplay: undefined }; - } - - const { moduleSymbol } = symbolOriginInfo; - const exportedSymbol = checker.getMergedSymbol(skipAlias(symbol.exportSymbol || symbol, checker)); - const { moduleSpecifier, codeAction } = codefix.getImportCompletionAction( - exportedSymbol, - moduleSymbol, - sourceFile, - getNameForExportedSymbol(symbol, compilerOptions.target!), - host, - program, - formatContext, - previousToken && isIdentifier(previousToken) ? previousToken.getStart(sourceFile) : position, - preferences); - return { sourceDisplay: [textPart(moduleSpecifier)], codeActions: [codeAction] }; - } - - export function getCompletionEntrySymbol( - program: Program, - log: Log, - sourceFile: SourceFile, - position: number, - entryId: CompletionEntryIdentifier, - host: LanguageServiceHost, - preferences: UserPreferences, - ): Symbol | undefined { - const completion = getSymbolCompletionFromEntryId(program, log, sourceFile, position, entryId, host, preferences); - return completion.type === "symbol" ? completion.symbol : undefined; - } - - const enum CompletionDataKind { Data, JsDocTagName, JsDocTag, JsDocParameterName } - /** true: after the `=` sign but no identifier has been typed yet. Else is the Identifier after the initializer. */ - type IsJsxInitializer = boolean | Identifier; - interface CompletionData { - readonly kind: CompletionDataKind.Data; - readonly symbols: readonly Symbol[]; - readonly completionKind: CompletionKind; - readonly isInSnippetScope: boolean; - /** Note that the presence of this alone doesn't mean that we need a conversion. Only do that if the completion is not an ordinary identifier. */ - readonly propertyAccessToConvert: PropertyAccessExpression | undefined; - readonly isNewIdentifierLocation: boolean; - readonly location: Node | undefined; - readonly keywordFilters: KeywordCompletionFilters; - readonly literals: readonly (string | number | PseudoBigInt)[]; - readonly symbolToOriginInfoMap: SymbolOriginInfoMap; - readonly recommendedCompletion: Symbol | undefined; - readonly previousToken: Node | undefined; - readonly isJsxInitializer: IsJsxInitializer; - readonly insideJsDocTagTypeExpression: boolean; - readonly symbolToSortTextMap: SymbolSortTextMap; - readonly isTypeOnlyLocation: boolean; - } - type Request = { readonly kind: CompletionDataKind.JsDocTagName | CompletionDataKind.JsDocTag } | { readonly kind: CompletionDataKind.JsDocParameterName, tag: JSDocParameterTag }; - - export const enum CompletionKind { - ObjectPropertyDeclaration, - Global, - PropertyAccess, - MemberLike, - String, - None, - } - - function getRecommendedCompletion(previousToken: Node, contextualType: Type, checker: TypeChecker): Symbol | undefined { - // For a union, return the first one with a recommended completion. - return firstDefined(contextualType && (contextualType.isUnion() ? contextualType.types : [contextualType]), type => { - const symbol = type && type.symbol; - // Don't include make a recommended completion for an abstract class - return symbol && (symbol.flags & (SymbolFlags.EnumMember | SymbolFlags.Enum | SymbolFlags.Class) && !isAbstractConstructorSymbol(symbol)) - ? getFirstSymbolInChain(symbol, previousToken, checker) - : undefined; - }); + } + if (!insideJsDocTagTypeExpression) { + // Proceed if the current position is in jsDoc tag expression; otherwise it is a normal + // comment or the plain text part of a jsDoc comment, so no completion should be available + log("Returning an empty list because completion was inside a regular comment or plain text part of a JsDoc comment."); + return undefined; + } + } + start = timestamp(); + const previousToken = (findPrecedingToken(position, sourceFile, /*startNode*/ undefined)!); // TODO: GH#18217 + log("getCompletionData: Get previous token 1: " + (timestamp() - start)); + // The decision to provide completion depends on the contextToken, which is determined through the previousToken. + // Note: 'previousToken' (and thus 'contextToken') can be undefined if we are the beginning of the file + let contextToken = previousToken; + // Check if the caret is at the end of an identifier; this is a partial identifier that we want to complete: e.g. a.toS| + // Skip this partial identifier and adjust the contextToken to the token that precedes it. + if (contextToken && position <= contextToken.end && (isIdentifierOrPrivateIdentifier(contextToken) || isKeyword(contextToken.kind))) { + const start = timestamp(); + contextToken = (findPrecedingToken(contextToken.getFullStart(), sourceFile, /*startNode*/ undefined)!); // TODO: GH#18217 + log("getCompletionData: Get previous token 2: " + (timestamp() - start)); } - - function getContextualType(previousToken: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): Type | undefined { - const { parent } = previousToken; - switch (previousToken.kind) { - case SyntaxKind.Identifier: - return getContextualTypeFromParent(previousToken as Identifier, checker); - case SyntaxKind.EqualsToken: - switch (parent.kind) { - case SyntaxKind.VariableDeclaration: - return checker.getContextualType((parent as VariableDeclaration).initializer!); // TODO: GH#18217 - case SyntaxKind.BinaryExpression: - return checker.getTypeAtLocation((parent as BinaryExpression).left); - case SyntaxKind.JsxAttribute: - return checker.getContextualTypeForJsxAttribute(parent as JsxAttribute); - default: + // Find the node where completion is requested on. + // Also determine whether we are trying to complete with members of that node + // or attributes of a JSX tag. + let node = currentToken; + let propertyAccessToConvert: PropertyAccessExpression | undefined; + let isRightOfDot = false; + let isRightOfQuestionDot = false; + let isRightOfOpenTag = false; + let isStartingCloseTag = false; + let isJsxInitializer: IsJsxInitializer = false; + let location = getTouchingPropertyName(sourceFile, position); + if (contextToken) { + // Bail out if this is a known invalid completion location + if (isCompletionListBlocker(contextToken)) { + log("Returning an empty list because completion was requested in an invalid position."); + return undefined; + } + let parent = contextToken.parent; + if (contextToken.kind === SyntaxKind.DotToken || contextToken.kind === SyntaxKind.QuestionDotToken) { + isRightOfDot = contextToken.kind === SyntaxKind.DotToken; + isRightOfQuestionDot = contextToken.kind === SyntaxKind.QuestionDotToken; + switch (parent.kind) { + case SyntaxKind.PropertyAccessExpression: + propertyAccessToConvert = (parent as PropertyAccessExpression); + node = propertyAccessToConvert.expression; + if (node.end === contextToken.pos && + isCallExpression(node) && + node.getChildCount(sourceFile) && + last(node.getChildren(sourceFile)).kind !== SyntaxKind.CloseParenToken) { + // This is likely dot from incorrectly parsed call expression and user is starting to write spread + // eg: Math.min(./**/) return undefined; - } - case SyntaxKind.NewKeyword: - return checker.getContextualType(parent as Expression); - case SyntaxKind.CaseKeyword: - return getSwitchedType(cast(parent, isCaseClause), checker); - case SyntaxKind.OpenBraceToken: - return isJsxExpression(parent) && parent.parent.kind !== SyntaxKind.JsxElement ? checker.getContextualTypeForJsxAttribute(parent.parent) : undefined; - default: - const argInfo = SignatureHelp.getArgumentInfoForCompletions(previousToken, position, sourceFile); - return argInfo ? - // At `,`, treat this as the next argument after the comma. - checker.getContextualTypeForArgumentAtIndex(argInfo.invocation, argInfo.argumentIndex + (previousToken.kind === SyntaxKind.CommaToken ? 1 : 0)) : - isEqualityOperatorKind(previousToken.kind) && isBinaryExpression(parent) && isEqualityOperatorKind(parent.operatorToken.kind) ? - // completion at `x ===/**/` should be for the right side - checker.getTypeAtLocation(parent.left) : - checker.getContextualType(previousToken as Expression); - } - } - - function getFirstSymbolInChain(symbol: Symbol, enclosingDeclaration: Node, checker: TypeChecker): Symbol | undefined { - const chain = checker.getAccessibleSymbolChain(symbol, enclosingDeclaration, /*meaning*/ SymbolFlags.All, /*useOnlyExternalAliasing*/ false); - if (chain) return first(chain); - return symbol.parent && (isModuleSymbol(symbol.parent) ? symbol : getFirstSymbolInChain(symbol.parent, enclosingDeclaration, checker)); - } - - function isModuleSymbol(symbol: Symbol): boolean { - return symbol.declarations.some(d => d.kind === SyntaxKind.SourceFile); - } - - function getCompletionData( - program: Program, - log: (message: string) => void, - sourceFile: SourceFile, - isUncheckedFile: boolean, - position: number, - preferences: Pick, - detailsEntryId: CompletionEntryIdentifier | undefined, - host: LanguageServiceHost, - ): CompletionData | Request | undefined { - const typeChecker = program.getTypeChecker(); - - let start = timestamp(); - let currentToken = getTokenAtPosition(sourceFile, position); // TODO: GH#15853 - // We will check for jsdoc comments with insideComment and getJsDocTagAtPosition. (TODO: that seems rather inefficient to check the same thing so many times.) - - log("getCompletionData: Get current token: " + (timestamp() - start)); - - start = timestamp(); - const insideComment = isInComment(sourceFile, position, currentToken); - log("getCompletionData: Is inside comment: " + (timestamp() - start)); - - let insideJsDocTagTypeExpression = false; - let isInSnippetScope = false; - if (insideComment) { - if (hasDocComment(sourceFile, position)) { - if (sourceFile.text.charCodeAt(position - 1) === CharacterCodes.at) { - // The current position is next to the '@' sign, when no tag name being provided yet. - // Provide a full list of tag names - return { kind: CompletionDataKind.JsDocTagName }; - } - else { - // When completion is requested without "@", we will have check to make sure that - // there are no comments prefix the request position. We will only allow "*" and space. - // e.g - // /** |c| /* - // - // /** - // |c| - // */ - // - // /** - // * |c| - // */ - // - // /** - // * |c| - // */ - const lineStart = getLineStartPositionForPosition(position, sourceFile); - if (!(sourceFile.text.substring(lineStart, position).match(/[^\*|\s|(/\*\*)]/))) { - return { kind: CompletionDataKind.JsDocTag }; - } - } - } - - // Completion should work inside certain JsDoc tags. For example: - // /** @type {number | string} */ - // Completion should work in the brackets - const tag = getJsDocTagAtPosition(currentToken, position); - if (tag) { - if (tag.tagName.pos <= position && position <= tag.tagName.end) { - return { kind: CompletionDataKind.JsDocTagName }; - } - if (isTagWithTypeExpression(tag) && tag.typeExpression && tag.typeExpression.kind === SyntaxKind.JSDocTypeExpression) { - currentToken = getTokenAtPosition(sourceFile, position); - if (!currentToken || - (!isDeclarationName(currentToken) && - (currentToken.parent.kind !== SyntaxKind.JSDocPropertyTag || - (currentToken.parent).name !== currentToken))) { - // Use as type location if inside tag's type expression - insideJsDocTagTypeExpression = isCurrentlyEditingNode(tag.typeExpression); } - } - if (!insideJsDocTagTypeExpression && isJSDocParameterTag(tag) && (nodeIsMissing(tag.name) || tag.name.pos <= position && position <= tag.name.end)) { - return { kind: CompletionDataKind.JsDocParameterName, tag }; - } - } - - if (!insideJsDocTagTypeExpression) { - // Proceed if the current position is in jsDoc tag expression; otherwise it is a normal - // comment or the plain text part of a jsDoc comment, so no completion should be available - log("Returning an empty list because completion was inside a regular comment or plain text part of a JsDoc comment."); - return undefined; + break; + case SyntaxKind.QualifiedName: + node = (parent as QualifiedName).left; + break; + case SyntaxKind.ModuleDeclaration: + node = (parent as ModuleDeclaration).name; + break; + case SyntaxKind.ImportType: + case SyntaxKind.MetaProperty: + node = parent; + break; + default: + // There is nothing that precedes the dot, so this likely just a stray character + // or leading into a '...' token. Just bail out instead. + return undefined; } } - - start = timestamp(); - const previousToken = findPrecedingToken(position, sourceFile, /*startNode*/ undefined)!; // TODO: GH#18217 - log("getCompletionData: Get previous token 1: " + (timestamp() - start)); - - // The decision to provide completion depends on the contextToken, which is determined through the previousToken. - // Note: 'previousToken' (and thus 'contextToken') can be undefined if we are the beginning of the file - let contextToken = previousToken; - - // Check if the caret is at the end of an identifier; this is a partial identifier that we want to complete: e.g. a.toS| - // Skip this partial identifier and adjust the contextToken to the token that precedes it. - if (contextToken && position <= contextToken.end && (isIdentifierOrPrivateIdentifier(contextToken) || isKeyword(contextToken.kind))) { - const start = timestamp(); - contextToken = findPrecedingToken(contextToken.getFullStart(), sourceFile, /*startNode*/ undefined)!; // TODO: GH#18217 - log("getCompletionData: Get previous token 2: " + (timestamp() - start)); - } - - // Find the node where completion is requested on. - // Also determine whether we are trying to complete with members of that node - // or attributes of a JSX tag. - let node = currentToken; - let propertyAccessToConvert: PropertyAccessExpression | undefined; - let isRightOfDot = false; - let isRightOfQuestionDot = false; - let isRightOfOpenTag = false; - let isStartingCloseTag = false; - let isJsxInitializer: IsJsxInitializer = false; - - let location = getTouchingPropertyName(sourceFile, position); - if (contextToken) { - // Bail out if this is a known invalid completion location - if (isCompletionListBlocker(contextToken)) { - log("Returning an empty list because completion was requested in an invalid position."); - return undefined; - } - - let parent = contextToken.parent; - if (contextToken.kind === SyntaxKind.DotToken || contextToken.kind === SyntaxKind.QuestionDotToken) { - isRightOfDot = contextToken.kind === SyntaxKind.DotToken; - isRightOfQuestionDot = contextToken.kind === SyntaxKind.QuestionDotToken; - switch (parent.kind) { - case SyntaxKind.PropertyAccessExpression: - propertyAccessToConvert = parent as PropertyAccessExpression; - node = propertyAccessToConvert.expression; - if (node.end === contextToken.pos && - isCallExpression(node) && - node.getChildCount(sourceFile) && - last(node.getChildren(sourceFile)).kind !== SyntaxKind.CloseParenToken) { - // This is likely dot from incorrectly parsed call expression and user is starting to write spread - // eg: Math.min(./**/) - return undefined; + else if (sourceFile.languageVariant === LanguageVariant.JSX) { + // + // If the tagname is a property access expression, we will then walk up to the top most of property access expression. + // Then, try to get a JSX container and its associated attributes type. + if (parent && parent.kind === SyntaxKind.PropertyAccessExpression) { + contextToken = parent; + parent = parent.parent; + } + // Fix location + if (currentToken.parent === location) { + switch (currentToken.kind) { + case SyntaxKind.GreaterThanToken: + if (currentToken.parent.kind === SyntaxKind.JsxElement || currentToken.parent.kind === SyntaxKind.JsxOpeningElement) { + location = currentToken; } break; - case SyntaxKind.QualifiedName: - node = (parent as QualifiedName).left; - break; - case SyntaxKind.ModuleDeclaration: - node = (parent as ModuleDeclaration).name; - break; - case SyntaxKind.ImportType: - case SyntaxKind.MetaProperty: - node = parent; + case SyntaxKind.SlashToken: + if (currentToken.parent.kind === SyntaxKind.JsxSelfClosingElement) { + location = currentToken; + } break; - default: - // There is nothing that precedes the dot, so this likely just a stray character - // or leading into a '...' token. Just bail out instead. - return undefined; } } - else if (sourceFile.languageVariant === LanguageVariant.JSX) { - // - // If the tagname is a property access expression, we will then walk up to the top most of property access expression. - // Then, try to get a JSX container and its associated attributes type. - if (parent && parent.kind === SyntaxKind.PropertyAccessExpression) { - contextToken = parent; - parent = parent.parent; - } - - // Fix location - if (currentToken.parent === location) { - switch (currentToken.kind) { - case SyntaxKind.GreaterThanToken: - if (currentToken.parent.kind === SyntaxKind.JsxElement || currentToken.parent.kind === SyntaxKind.JsxOpeningElement) { - location = currentToken; - } - break; - - case SyntaxKind.SlashToken: - if (currentToken.parent.kind === SyntaxKind.JsxSelfClosingElement) { - location = currentToken; - } - break; + switch (parent.kind) { + case SyntaxKind.JsxClosingElement: + if (contextToken.kind === SyntaxKind.SlashToken) { + isStartingCloseTag = true; + location = contextToken; } - } - - switch (parent.kind) { - case SyntaxKind.JsxClosingElement: - if (contextToken.kind === SyntaxKind.SlashToken) { - isStartingCloseTag = true; - location = contextToken; - } + break; + case SyntaxKind.BinaryExpression: + if (!binaryExpressionMayBeOpenTag((parent as BinaryExpression))) { break; - - case SyntaxKind.BinaryExpression: - if (!binaryExpressionMayBeOpenTag(parent as BinaryExpression)) { + } + // falls through + case SyntaxKind.JsxSelfClosingElement: + case SyntaxKind.JsxElement: + case SyntaxKind.JsxOpeningElement: + if (contextToken.kind === SyntaxKind.LessThanToken) { + isRightOfOpenTag = true; + location = contextToken; + } + break; + case SyntaxKind.JsxAttribute: + switch (previousToken.kind) { + case SyntaxKind.EqualsToken: + isJsxInitializer = true; break; - } - // falls through - - case SyntaxKind.JsxSelfClosingElement: - case SyntaxKind.JsxElement: - case SyntaxKind.JsxOpeningElement: - if (contextToken.kind === SyntaxKind.LessThanToken) { - isRightOfOpenTag = true; - location = contextToken; - } - break; - - case SyntaxKind.JsxAttribute: - switch (previousToken.kind) { - case SyntaxKind.EqualsToken: - isJsxInitializer = true; - break; - case SyntaxKind.Identifier: - // For `
` we don't want to treat this as a jsx inializer, instead it's the attribute name. - if (parent !== previousToken.parent && - !(parent as JsxAttribute).initializer && - findChildOfKind(parent, SyntaxKind.EqualsToken, sourceFile)) { - isJsxInitializer = previousToken as Identifier; - } - } - break; - } - } - } - - const semanticStart = timestamp(); - let completionKind = CompletionKind.None; - let isNewIdentifierLocation = false; - let keywordFilters = KeywordCompletionFilters.None; - // This also gets mutated in nested-functions after the return - let symbols: Symbol[] = []; - const symbolToOriginInfoMap: SymbolOriginInfoMap = []; - const symbolToSortTextMap: SymbolSortTextMap = []; - const importSuggestionsCache = host.getImportSuggestionsCache && host.getImportSuggestionsCache(); - const isTypeOnly = isTypeOnlyCompletion(); - - if (isRightOfDot || isRightOfQuestionDot) { - getTypeScriptMemberSymbols(); - } - else if (isRightOfOpenTag) { - const tagSymbols = typeChecker.getJsxIntrinsicTagNamesAt(location); - Debug.assertEachIsDefined(tagSymbols, "getJsxIntrinsicTagNames() should all be defined"); - tryGetGlobalSymbols(); - symbols = tagSymbols.concat(symbols); - completionKind = CompletionKind.MemberLike; - keywordFilters = KeywordCompletionFilters.None; - } - else if (isStartingCloseTag) { - const tagName = (contextToken.parent.parent).openingElement.tagName; - const tagSymbol = typeChecker.getSymbolAtLocation(tagName); - if (tagSymbol) { - symbols = [tagSymbol]; + case SyntaxKind.Identifier: + // For `
` we don't want to treat this as a jsx inializer, instead it's the attribute name. + if (parent !== previousToken.parent && + !(parent as JsxAttribute).initializer && + findChildOfKind(parent, SyntaxKind.EqualsToken, sourceFile)) { + isJsxInitializer = (previousToken as Identifier); + } + } + break; } - completionKind = CompletionKind.MemberLike; - keywordFilters = KeywordCompletionFilters.None; } - else { - // For JavaScript or TypeScript, if we're not after a dot, then just try to get the - // global symbols in scope. These results should be valid for either language as - // the set of symbols that can be referenced from this location. - if (!tryGetGlobalSymbols()) { - return undefined; - } + } + const semanticStart = timestamp(); + let completionKind = CompletionKind.None; + let isNewIdentifierLocation = false; + let keywordFilters = KeywordCompletionFilters.None; + // This also gets mutated in nested-functions after the return + let symbols: Symbol[] = []; + const symbolToOriginInfoMap: SymbolOriginInfoMap = []; + const symbolToSortTextMap: SymbolSortTextMap = []; + const importSuggestionsCache = host.getImportSuggestionsCache && host.getImportSuggestionsCache(); + const isTypeOnly = isTypeOnlyCompletion(); + if (isRightOfDot || isRightOfQuestionDot) { + getTypeScriptMemberSymbols(); + } + else if (isRightOfOpenTag) { + const tagSymbols = typeChecker.getJsxIntrinsicTagNamesAt(location); + Debug.assertEachIsDefined(tagSymbols, "getJsxIntrinsicTagNames() should all be defined"); + tryGetGlobalSymbols(); + symbols = tagSymbols.concat(symbols); + completionKind = CompletionKind.MemberLike; + keywordFilters = KeywordCompletionFilters.None; + } + else if (isStartingCloseTag) { + const tagName = (contextToken.parent.parent).openingElement.tagName; + const tagSymbol = typeChecker.getSymbolAtLocation(tagName); + if (tagSymbol) { + symbols = [tagSymbol]; + } + completionKind = CompletionKind.MemberLike; + keywordFilters = KeywordCompletionFilters.None; + } + else { + // For JavaScript or TypeScript, if we're not after a dot, then just try to get the + // global symbols in scope. These results should be valid for either language as + // the set of symbols that can be referenced from this location. + if (!tryGetGlobalSymbols()) { + return undefined; } - - log("getCompletionData: Semantic work: " + (timestamp() - semanticStart)); - const contextualType = previousToken && getContextualType(previousToken, position, sourceFile, typeChecker); - const literals = mapDefined(contextualType && (contextualType.isUnion() ? contextualType.types : [contextualType]), t => t.isLiteral() ? t.value : undefined); - - const recommendedCompletion = previousToken && contextualType && getRecommendedCompletion(previousToken, contextualType, typeChecker); - return { - kind: CompletionDataKind.Data, - symbols, - completionKind, - isInSnippetScope, - propertyAccessToConvert, - isNewIdentifierLocation, - location, - keywordFilters, - literals, - symbolToOriginInfoMap, - recommendedCompletion, - previousToken, - isJsxInitializer, - insideJsDocTagTypeExpression, - symbolToSortTextMap, - isTypeOnlyLocation: isTypeOnly - }; - - type JSDocTagWithTypeExpression = JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag; - - function isTagWithTypeExpression(tag: JSDocTag): tag is JSDocTagWithTypeExpression { - switch (tag.kind) { - case SyntaxKind.JSDocParameterTag: - case SyntaxKind.JSDocPropertyTag: - case SyntaxKind.JSDocReturnTag: - case SyntaxKind.JSDocTypeTag: - case SyntaxKind.JSDocTypedefTag: - return true; - default: - return false; - } + } + log("getCompletionData: Semantic work: " + (timestamp() - semanticStart)); + const contextualType = previousToken && getContextualType(previousToken, position, sourceFile, typeChecker); + const literals = mapDefined(contextualType && (contextualType.isUnion() ? contextualType.types : [contextualType]), t => t.isLiteral() ? t.value : undefined); + const recommendedCompletion = previousToken && contextualType && getRecommendedCompletion(previousToken, contextualType, typeChecker); + return { + kind: CompletionDataKind.Data, + symbols, + completionKind, + isInSnippetScope, + propertyAccessToConvert, + isNewIdentifierLocation, + location, + keywordFilters, + literals, + symbolToOriginInfoMap, + recommendedCompletion, + previousToken, + isJsxInitializer, + insideJsDocTagTypeExpression, + symbolToSortTextMap, + isTypeOnlyLocation: isTypeOnly + }; + type JSDocTagWithTypeExpression = JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag; + function isTagWithTypeExpression(tag: JSDocTag): tag is JSDocTagWithTypeExpression { + switch (tag.kind) { + case SyntaxKind.JSDocParameterTag: + case SyntaxKind.JSDocPropertyTag: + case SyntaxKind.JSDocReturnTag: + case SyntaxKind.JSDocTypeTag: + case SyntaxKind.JSDocTypedefTag: + return true; + default: + return false; } - - function getTypeScriptMemberSymbols(): void { - // Right of dot member completion list - completionKind = CompletionKind.PropertyAccess; - - // Since this is qualified name check it's a type node location - const isImportType = isLiteralImportTypeNode(node); - const isTypeLocation = insideJsDocTagTypeExpression - || (isImportType && !(node as ImportTypeNode).isTypeOf) - || isPartOfTypeNode(node.parent) - || isPossiblyTypeArgumentPosition(contextToken, sourceFile, typeChecker); - const isRhsOfImportDeclaration = isInRightSideOfInternalImportEqualsDeclaration(node); - if (isEntityName(node) || isImportType) { - const isNamespaceName = isModuleDeclaration(node.parent); - if (isNamespaceName) isNewIdentifierLocation = true; - let symbol = typeChecker.getSymbolAtLocation(node); - if (symbol) { - symbol = skipAlias(symbol, typeChecker); - - if (symbol.flags & (SymbolFlags.Module | SymbolFlags.Enum)) { - // Extract module or enum members - const exportedSymbols = typeChecker.getExportsOfModule(symbol); - Debug.assertEachIsDefined(exportedSymbols, "getExportsOfModule() should all be defined"); - const isValidValueAccess = (symbol: Symbol) => typeChecker.isValidPropertyAccess(isImportType ? node : (node.parent), symbol.name); - const isValidTypeAccess = (symbol: Symbol) => symbolCanBeReferencedAtTypeLocation(symbol); - const isValidAccess: (symbol: Symbol) => boolean = - isNamespaceName - // At `namespace N.M/**/`, if this is the only declaration of `M`, don't include `M` as a completion. - ? symbol => !!(symbol.flags & SymbolFlags.Namespace) && !symbol.declarations.every(d => d.parent === node.parent) - : isRhsOfImportDeclaration ? - // Any kind is allowed when dotting off namespace in internal import equals declaration - symbol => isValidTypeAccess(symbol) || isValidValueAccess(symbol) : - isTypeLocation ? isValidTypeAccess : isValidValueAccess; - for (const exportedSymbol of exportedSymbols) { - if (isValidAccess(exportedSymbol)) { - symbols.push(exportedSymbol); - } + } + function getTypeScriptMemberSymbols(): void { + // Right of dot member completion list + completionKind = CompletionKind.PropertyAccess; + // Since this is qualified name check it's a type node location + const isImportType = isLiteralImportTypeNode(node); + const isTypeLocation = insideJsDocTagTypeExpression + || (isImportType && !(node as ImportTypeNode).isTypeOf) + || isPartOfTypeNode(node.parent) + || isPossiblyTypeArgumentPosition(contextToken, sourceFile, typeChecker); + const isRhsOfImportDeclaration = isInRightSideOfInternalImportEqualsDeclaration(node); + if (isEntityName(node) || isImportType) { + const isNamespaceName = isModuleDeclaration(node.parent); + if (isNamespaceName) + isNewIdentifierLocation = true; + let symbol = typeChecker.getSymbolAtLocation(node); + if (symbol) { + symbol = skipAlias(symbol, typeChecker); + if (symbol.flags & (SymbolFlags.Module | SymbolFlags.Enum)) { + // Extract module or enum members + const exportedSymbols = typeChecker.getExportsOfModule(symbol); + Debug.assertEachIsDefined(exportedSymbols, "getExportsOfModule() should all be defined"); + const isValidValueAccess = (symbol: Symbol) => typeChecker.isValidPropertyAccess(isImportType ? node : (node.parent), symbol.name); + const isValidTypeAccess = (symbol: Symbol) => symbolCanBeReferencedAtTypeLocation(symbol); + const isValidAccess: (symbol: Symbol) => boolean = isNamespaceName + // At `namespace N.M/**/`, if this is the only declaration of `M`, don't include `M` as a completion. + ? symbol => !!(symbol.flags & SymbolFlags.Namespace) && !symbol.declarations.every(d => d.parent === node.parent) + : isRhsOfImportDeclaration ? + // Any kind is allowed when dotting off namespace in internal import equals declaration + symbol => isValidTypeAccess(symbol) || isValidValueAccess(symbol) : + isTypeLocation ? isValidTypeAccess : isValidValueAccess; + for (const exportedSymbol of exportedSymbols) { + if (isValidAccess(exportedSymbol)) { + symbols.push(exportedSymbol); } - - // If the module is merged with a value, we must get the type of the class and add its propertes (for inherited static methods). - if (!isTypeLocation && - symbol.declarations && - symbol.declarations.some(d => d.kind !== SyntaxKind.SourceFile && d.kind !== SyntaxKind.ModuleDeclaration && d.kind !== SyntaxKind.EnumDeclaration)) { - let type = typeChecker.getTypeOfSymbolAtLocation(symbol, node).getNonOptionalType(); - let insertQuestionDot = false; - if (type.isNullableType()) { - const canCorrectToQuestionDot = - isRightOfDot && + } + // If the module is merged with a value, we must get the type of the class and add its propertes (for inherited static methods). + if (!isTypeLocation && + symbol.declarations && + symbol.declarations.some(d => d.kind !== SyntaxKind.SourceFile && d.kind !== SyntaxKind.ModuleDeclaration && d.kind !== SyntaxKind.EnumDeclaration)) { + let type = typeChecker.getTypeOfSymbolAtLocation(symbol, node).getNonOptionalType(); + let insertQuestionDot = false; + if (type.isNullableType()) { + const canCorrectToQuestionDot = isRightOfDot && !isRightOfQuestionDot && preferences.includeAutomaticOptionalChainCompletions !== false; - - if (canCorrectToQuestionDot || isRightOfQuestionDot) { - type = type.getNonNullableType(); - if (canCorrectToQuestionDot) { - insertQuestionDot = true; - } + if (canCorrectToQuestionDot || isRightOfQuestionDot) { + type = type.getNonNullableType(); + if (canCorrectToQuestionDot) { + insertQuestionDot = true; } } - addTypeProperties(type, !!(node.flags & NodeFlags.AwaitContext), insertQuestionDot); } - - return; + addTypeProperties(type, !!(node.flags & NodeFlags.AwaitContext), insertQuestionDot); } + return; } } - - if (isMetaProperty(node) && (node.keywordToken === SyntaxKind.NewKeyword || node.keywordToken === SyntaxKind.ImportKeyword) && contextToken === node.getChildAt(1)) { - const completion = (node.keywordToken === SyntaxKind.NewKeyword) ? "target" : "meta"; - symbols.push(typeChecker.createSymbol(SymbolFlags.Property, escapeLeadingUnderscores(completion))); - return; - } - - if (!isTypeLocation) { - let type = typeChecker.getTypeAtLocation(node).getNonOptionalType(); - let insertQuestionDot = false; - if (type.isNullableType()) { - const canCorrectToQuestionDot = - isRightOfDot && - !isRightOfQuestionDot && - preferences.includeAutomaticOptionalChainCompletions !== false; - - if (canCorrectToQuestionDot || isRightOfQuestionDot) { - type = type.getNonNullableType(); - if (canCorrectToQuestionDot) { - insertQuestionDot = true; - } + } + if (isMetaProperty(node) && (node.keywordToken === SyntaxKind.NewKeyword || node.keywordToken === SyntaxKind.ImportKeyword) && contextToken === node.getChildAt(1)) { + const completion = (node.keywordToken === SyntaxKind.NewKeyword) ? "target" : "meta"; + symbols.push(typeChecker.createSymbol(SymbolFlags.Property, escapeLeadingUnderscores(completion))); + return; + } + if (!isTypeLocation) { + let type = typeChecker.getTypeAtLocation(node).getNonOptionalType(); + let insertQuestionDot = false; + if (type.isNullableType()) { + const canCorrectToQuestionDot = isRightOfDot && + !isRightOfQuestionDot && + preferences.includeAutomaticOptionalChainCompletions !== false; + if (canCorrectToQuestionDot || isRightOfQuestionDot) { + type = type.getNonNullableType(); + if (canCorrectToQuestionDot) { + insertQuestionDot = true; } } - addTypeProperties(type, !!(node.flags & NodeFlags.AwaitContext), insertQuestionDot); } + addTypeProperties(type, !!(node.flags & NodeFlags.AwaitContext), insertQuestionDot); } - - function addTypeProperties(type: Type, insertAwait: boolean, insertQuestionDot: boolean): void { - isNewIdentifierLocation = !!type.getStringIndexType(); - if (isRightOfQuestionDot && some(type.getCallSignatures())) { - isNewIdentifierLocation = true; - } - - const propertyAccess = node.kind === SyntaxKind.ImportType ? node : node.parent; - if (isUncheckedFile) { - // In javascript files, for union types, we don't just get the members that - // the individual types have in common, we also include all the members that - // each individual type has. This is because we're going to add all identifiers - // anyways. So we might as well elevate the members that were at least part - // of the individual types to a higher status since we know what they are. - symbols.push(...getPropertiesForCompletion(type, typeChecker)); - } - else { - for (const symbol of type.getApparentProperties()) { - if (typeChecker.isValidPropertyAccessForCompletions(propertyAccess, type, symbol)) { - addPropertySymbol(symbol, /*insertAwait*/ false, insertQuestionDot); - } + } + function addTypeProperties(type: Type, insertAwait: boolean, insertQuestionDot: boolean): void { + isNewIdentifierLocation = !!type.getStringIndexType(); + if (isRightOfQuestionDot && some(type.getCallSignatures())) { + isNewIdentifierLocation = true; + } + const propertyAccess = node.kind === SyntaxKind.ImportType ? node : node.parent; + if (isUncheckedFile) { + // In javascript files, for union types, we don't just get the members that + // the individual types have in common, we also include all the members that + // each individual type has. This is because we're going to add all identifiers + // anyways. So we might as well elevate the members that were at least part + // of the individual types to a higher status since we know what they are. + symbols.push(...getPropertiesForCompletion(type, typeChecker)); + } + else { + for (const symbol of type.getApparentProperties()) { + if (typeChecker.isValidPropertyAccessForCompletions(propertyAccess, type, symbol)) { + addPropertySymbol(symbol, /*insertAwait*/ false, insertQuestionDot); } } - - if (insertAwait && preferences.includeCompletionsWithInsertText) { - const promiseType = typeChecker.getPromisedTypeOfPromise(type); - if (promiseType) { - for (const symbol of promiseType.getApparentProperties()) { - if (typeChecker.isValidPropertyAccessForCompletions(propertyAccess, promiseType, symbol)) { - addPropertySymbol(symbol, /* insertAwait */ true, insertQuestionDot); - } + } + if (insertAwait && preferences.includeCompletionsWithInsertText) { + const promiseType = typeChecker.getPromisedTypeOfPromise(type); + if (promiseType) { + for (const symbol of promiseType.getApparentProperties()) { + if (typeChecker.isValidPropertyAccessForCompletions(propertyAccess, promiseType, symbol)) { + addPropertySymbol(symbol, /* insertAwait */ true, insertQuestionDot); } } } } - - function addPropertySymbol(symbol: Symbol, insertAwait: boolean, insertQuestionDot: boolean) { - // For a computed property with an accessible name like `Symbol.iterator`, - // we'll add a completion for the *name* `Symbol` instead of for the property. - // If this is e.g. [Symbol.iterator], add a completion for `Symbol`. - const computedPropertyName = firstDefined(symbol.declarations, decl => tryCast(getNameOfDeclaration(decl), isComputedPropertyName)); - if (computedPropertyName) { - const leftMostName = getLeftMostName(computedPropertyName.expression); // The completion is for `Symbol`, not `iterator`. - const nameSymbol = leftMostName && typeChecker.getSymbolAtLocation(leftMostName); - // If this is nested like for `namespace N { export const sym = Symbol(); }`, we'll add the completion for `N`. - const firstAccessibleSymbol = nameSymbol && getFirstSymbolInChain(nameSymbol, contextToken, typeChecker); - if (firstAccessibleSymbol && !symbolToOriginInfoMap[getSymbolId(firstAccessibleSymbol)]) { - symbols.push(firstAccessibleSymbol); - const moduleSymbol = firstAccessibleSymbol.parent; - symbolToOriginInfoMap[getSymbolId(firstAccessibleSymbol)] = - !moduleSymbol || !isExternalModuleSymbol(moduleSymbol) - ? { kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberNoExport) } - : { kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberExport), moduleSymbol, isDefaultExport: false }; - } - else if (preferences.includeCompletionsWithInsertText) { - addSymbolOriginInfo(symbol); - symbols.push(symbol); - } - } - else { + } + function addPropertySymbol(symbol: Symbol, insertAwait: boolean, insertQuestionDot: boolean) { + // For a computed property with an accessible name like `Symbol.iterator`, + // we'll add a completion for the *name* `Symbol` instead of for the property. + // If this is e.g. [Symbol.iterator], add a completion for `Symbol`. + const computedPropertyName = firstDefined(symbol.declarations, decl => tryCast(getNameOfDeclaration(decl), isComputedPropertyName)); + if (computedPropertyName) { + const leftMostName = getLeftMostName(computedPropertyName.expression); // The completion is for `Symbol`, not `iterator`. + const nameSymbol = leftMostName && typeChecker.getSymbolAtLocation(leftMostName); + // If this is nested like for `namespace N { export const sym = Symbol(); }`, we'll add the completion for `N`. + const firstAccessibleSymbol = nameSymbol && getFirstSymbolInChain(nameSymbol, contextToken, typeChecker); + if (firstAccessibleSymbol && !symbolToOriginInfoMap[getSymbolId(firstAccessibleSymbol)]) { + symbols.push(firstAccessibleSymbol); + const moduleSymbol = firstAccessibleSymbol.parent; + symbolToOriginInfoMap[getSymbolId(firstAccessibleSymbol)] = + !moduleSymbol || !isExternalModuleSymbol(moduleSymbol) + ? { kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberNoExport) } + : { kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberExport), moduleSymbol, isDefaultExport: false }; + } + else if (preferences.includeCompletionsWithInsertText) { addSymbolOriginInfo(symbol); symbols.push(symbol); } - - function addSymbolOriginInfo(symbol: Symbol) { - if (preferences.includeCompletionsWithInsertText) { - if (insertAwait && !symbolToOriginInfoMap[getSymbolId(symbol)]) { - symbolToOriginInfoMap[getSymbolId(symbol)] = { kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.Promise) }; - } - else if (insertQuestionDot) { - symbolToOriginInfoMap[getSymbolId(symbol)] = { kind: SymbolOriginInfoKind.Nullable }; - } - } - } - - function getNullableSymbolOriginInfoKind(kind: SymbolOriginInfoKind) { - return insertQuestionDot ? kind | SymbolOriginInfoKind.Nullable : kind; - } } - - /** Given 'a.b.c', returns 'a'. */ - function getLeftMostName(e: Expression): Identifier | undefined { - return isIdentifier(e) ? e : isPropertyAccessExpression(e) ? getLeftMostName(e.expression) : undefined; - } - - function tryGetGlobalSymbols(): boolean { - const result: GlobalsSearch = tryGetObjectLikeCompletionSymbols() - || tryGetImportOrExportClauseCompletionSymbols() - || tryGetConstructorCompletion() - || tryGetClassLikeCompletionSymbols() - || tryGetJsxCompletionSymbols() - || (getGlobalCompletions(), GlobalsSearch.Success); - return result === GlobalsSearch.Success; - } - - function tryGetConstructorCompletion(): GlobalsSearch { - if (!tryGetConstructorLikeCompletionContainer(contextToken)) return GlobalsSearch.Continue; - // no members, only keywords - completionKind = CompletionKind.None; - // Declaring new property/method/accessor - isNewIdentifierLocation = true; - // Has keywords for constructor parameter - keywordFilters = KeywordCompletionFilters.ConstructorParameterKeywords; - return GlobalsSearch.Success; - } - - function tryGetJsxCompletionSymbols(): GlobalsSearch { - const jsxContainer = tryGetContainingJsxElement(contextToken); - // Cursor is inside a JSX self-closing element or opening element - const attrsType = jsxContainer && typeChecker.getContextualType(jsxContainer.attributes); - if (!attrsType) return GlobalsSearch.Continue; - const completionsType = jsxContainer && typeChecker.getContextualType(jsxContainer.attributes, ContextFlags.Completions); - symbols = filterJsxAttributes(getPropertiesForObjectExpression(attrsType, completionsType, jsxContainer!.attributes, typeChecker), jsxContainer!.attributes.properties); - setSortTextToOptionalMember(); - completionKind = CompletionKind.MemberLike; - isNewIdentifierLocation = false; - return GlobalsSearch.Success; + else { + addSymbolOriginInfo(symbol); + symbols.push(symbol); } - - function getGlobalCompletions(): void { - keywordFilters = tryGetFunctionLikeBodyCompletionContainer(contextToken) ? KeywordCompletionFilters.FunctionLikeBodyKeywords : KeywordCompletionFilters.All; - - // Get all entities in the current scope. - completionKind = CompletionKind.Global; - isNewIdentifierLocation = isNewIdentifierDefinitionLocation(contextToken); - - if (previousToken !== contextToken) { - Debug.assert(!!previousToken, "Expected 'contextToken' to be defined when different from 'previousToken'."); - } - // We need to find the node that will give us an appropriate scope to begin - // aggregating completion candidates. This is achieved in 'getScopeNode' - // by finding the first node that encompasses a position, accounting for whether a node - // is "complete" to decide whether a position belongs to the node. - // - // However, at the end of an identifier, we are interested in the scope of the identifier - // itself, but fall outside of the identifier. For instance: - // - // xyz => x$ - // - // the cursor is outside of both the 'x' and the arrow function 'xyz => x', - // so 'xyz' is not returned in our results. - // - // We define 'adjustedPosition' so that we may appropriately account for - // being at the end of an identifier. The intention is that if requesting completion - // at the end of an identifier, it should be effectively equivalent to requesting completion - // anywhere inside/at the beginning of the identifier. So in the previous case, the - // 'adjustedPosition' will work as if requesting completion in the following: - // - // xyz => $x - // - // If previousToken !== contextToken, then - // - 'contextToken' was adjusted to the token prior to 'previousToken' - // because we were at the end of an identifier. - // - 'previousToken' is defined. - const adjustedPosition = previousToken !== contextToken ? - previousToken.getStart() : - position; - - const scopeNode = getScopeNode(contextToken, adjustedPosition, sourceFile) || sourceFile; - isInSnippetScope = isSnippetScope(scopeNode); - - const symbolMeanings = (isTypeOnly ? SymbolFlags.None : SymbolFlags.Value) | SymbolFlags.Type | SymbolFlags.Namespace | SymbolFlags.Alias; - - symbols = typeChecker.getSymbolsInScope(scopeNode, symbolMeanings); - Debug.assertEachIsDefined(symbols, "getSymbolsInScope() should all be defined"); - for (const symbol of symbols) { - if (!typeChecker.isArgumentsSymbol(symbol) && - !some(symbol.declarations, d => d.getSourceFile() === sourceFile)) { - symbolToSortTextMap[getSymbolId(symbol)] = SortText.GlobalsOrKeywords; + function addSymbolOriginInfo(symbol: Symbol) { + if (preferences.includeCompletionsWithInsertText) { + if (insertAwait && !symbolToOriginInfoMap[getSymbolId(symbol)]) { + symbolToOriginInfoMap[getSymbolId(symbol)] = { kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.Promise) }; } - } - - // Need to insert 'this.' before properties of `this` type, so only do that if `includeInsertTextCompletions` - if (preferences.includeCompletionsWithInsertText && scopeNode.kind !== SyntaxKind.SourceFile) { - const thisType = typeChecker.tryGetThisTypeAt(scopeNode, /*includeGlobalThis*/ false); - if (thisType) { - for (const symbol of getPropertiesForCompletion(thisType, typeChecker)) { - symbolToOriginInfoMap[getSymbolId(symbol)] = { kind: SymbolOriginInfoKind.ThisType }; - symbols.push(symbol); - symbolToSortTextMap[getSymbolId(symbol)] = SortText.SuggestedClassMembers; - } - } - } - - if (shouldOfferImportCompletions()) { - const lowerCaseTokenText = previousToken && isIdentifier(previousToken) ? previousToken.text.toLowerCase() : ""; - const autoImportSuggestions = getSymbolsFromOtherSourceFileExports(program.getCompilerOptions().target!, host); - if (!detailsEntryId && importSuggestionsCache) { - importSuggestionsCache.set(sourceFile.fileName, autoImportSuggestions, host.getProjectVersion && host.getProjectVersion()); + else if (insertQuestionDot) { + symbolToOriginInfoMap[getSymbolId(symbol)] = { kind: SymbolOriginInfoKind.Nullable }; } - autoImportSuggestions.forEach(({ symbol, symbolName, skipFilter, origin }) => { - if (detailsEntryId) { - if (detailsEntryId.source && stripQuotes(origin.moduleSymbol.name) !== detailsEntryId.source) { - return; - } - } - else if (!skipFilter && !stringContainsCharactersInOrder(symbolName.toLowerCase(), lowerCaseTokenText)) { - return; - } - - const symbolId = getSymbolId(symbol); - symbols.push(symbol); - symbolToOriginInfoMap[symbolId] = origin; - symbolToSortTextMap[symbolId] = SortText.AutoImportSuggestions; - }); - } - filterGlobalCompletion(symbols); - } - - function shouldOfferImportCompletions(): boolean { - // If not already a module, must have modules enabled and not currently be in a commonjs module. (TODO: import completions for commonjs) - if (!preferences.includeCompletionsForModuleExports) return false; - // If already using ES6 modules, OK to continue using them. - if (sourceFile.externalModuleIndicator) return true; - // If already using commonjs, don't introduce ES6. - if (sourceFile.commonJsModuleIndicator) return false; - // If module transpilation is enabled or we're targeting es6 or above, or not emitting, OK. - if (compilerOptionsIndicateEs6Modules(program.getCompilerOptions())) return true; - // If some file is using ES6 modules, assume that it's OK to add more. - return programContainsEs6Modules(program); - } - - function isSnippetScope(scopeNode: Node): boolean { - switch (scopeNode.kind) { - case SyntaxKind.SourceFile: - case SyntaxKind.TemplateExpression: - case SyntaxKind.JsxExpression: - case SyntaxKind.Block: - return true; - default: - return isStatement(scopeNode); } } - - function filterGlobalCompletion(symbols: Symbol[]): void { - const isTypeOnly = isTypeOnlyCompletion(); - if (isTypeOnly) { - keywordFilters = isTypeAssertion() - ? KeywordCompletionFilters.TypeAssertionKeywords - : KeywordCompletionFilters.TypeKeywords; - } - - filterMutate(symbols, symbol => { - if (!isSourceFile(location)) { - // export = /**/ here we want to get all meanings, so any symbol is ok - if (isExportAssignment(location.parent)) { - return true; - } - - symbol = skipAlias(symbol, typeChecker); - - // import m = /**/ <-- It can only access namespace (if typing import = x. this would get member symbols and not namespace) - if (isInRightSideOfInternalImportEqualsDeclaration(location)) { - return !!(symbol.flags & SymbolFlags.Namespace); - } - - if (isTypeOnly) { - // It's a type, but you can reach it by namespace.type as well - return symbolCanBeReferencedAtTypeLocation(symbol); - } - } - - // expressions are value space (which includes the value namespaces) - return !!(getCombinedLocalAndExportSymbolFlags(symbol) & SymbolFlags.Value); - }); + function getNullableSymbolOriginInfoKind(kind: SymbolOriginInfoKind) { + return insertQuestionDot ? kind | SymbolOriginInfoKind.Nullable : kind; } - - function isTypeAssertion(): boolean { - return isAssertionExpression(contextToken.parent); + } + /** Given 'a.b.c', returns 'a'. */ + function getLeftMostName(e: Expression): Identifier | undefined { + return isIdentifier(e) ? e : isPropertyAccessExpression(e) ? getLeftMostName(e.expression) : undefined; + } + function tryGetGlobalSymbols(): boolean { + const result: GlobalsSearch = tryGetObjectLikeCompletionSymbols() + || tryGetImportOrExportClauseCompletionSymbols() + || tryGetConstructorCompletion() + || tryGetClassLikeCompletionSymbols() + || tryGetJsxCompletionSymbols() + || (getGlobalCompletions(), GlobalsSearch.Success); + return result === GlobalsSearch.Success; + } + function tryGetConstructorCompletion(): GlobalsSearch { + if (!tryGetConstructorLikeCompletionContainer(contextToken)) + return GlobalsSearch.Continue; + // no members, only keywords + completionKind = CompletionKind.None; + // Declaring new property/method/accessor + isNewIdentifierLocation = true; + // Has keywords for constructor parameter + keywordFilters = KeywordCompletionFilters.ConstructorParameterKeywords; + return GlobalsSearch.Success; + } + function tryGetJsxCompletionSymbols(): GlobalsSearch { + const jsxContainer = tryGetContainingJsxElement(contextToken); + // Cursor is inside a JSX self-closing element or opening element + const attrsType = jsxContainer && typeChecker.getContextualType(jsxContainer.attributes); + if (!attrsType) + return GlobalsSearch.Continue; + const completionsType = jsxContainer && typeChecker.getContextualType(jsxContainer.attributes, ContextFlags.Completions); + symbols = filterJsxAttributes(getPropertiesForObjectExpression(attrsType, completionsType, jsxContainer!.attributes, typeChecker), jsxContainer!.attributes.properties); + setSortTextToOptionalMember(); + completionKind = CompletionKind.MemberLike; + isNewIdentifierLocation = false; + return GlobalsSearch.Success; + } + function getGlobalCompletions(): void { + keywordFilters = tryGetFunctionLikeBodyCompletionContainer(contextToken) ? KeywordCompletionFilters.FunctionLikeBodyKeywords : KeywordCompletionFilters.All; + // Get all entities in the current scope. + completionKind = CompletionKind.Global; + isNewIdentifierLocation = isNewIdentifierDefinitionLocation(contextToken); + if (previousToken !== contextToken) { + Debug.assert(!!previousToken, "Expected 'contextToken' to be defined when different from 'previousToken'."); + } + // We need to find the node that will give us an appropriate scope to begin + // aggregating completion candidates. This is achieved in 'getScopeNode' + // by finding the first node that encompasses a position, accounting for whether a node + // is "complete" to decide whether a position belongs to the node. + // + // However, at the end of an identifier, we are interested in the scope of the identifier + // itself, but fall outside of the identifier. For instance: + // + // xyz => x$ + // + // the cursor is outside of both the 'x' and the arrow function 'xyz => x', + // so 'xyz' is not returned in our results. + // + // We define 'adjustedPosition' so that we may appropriately account for + // being at the end of an identifier. The intention is that if requesting completion + // at the end of an identifier, it should be effectively equivalent to requesting completion + // anywhere inside/at the beginning of the identifier. So in the previous case, the + // 'adjustedPosition' will work as if requesting completion in the following: + // + // xyz => $x + // + // If previousToken !== contextToken, then + // - 'contextToken' was adjusted to the token prior to 'previousToken' + // because we were at the end of an identifier. + // - 'previousToken' is defined. + const adjustedPosition = previousToken !== contextToken ? + previousToken.getStart() : + position; + const scopeNode = getScopeNode(contextToken, adjustedPosition, sourceFile) || sourceFile; + isInSnippetScope = isSnippetScope(scopeNode); + const symbolMeanings = (isTypeOnly ? SymbolFlags.None : SymbolFlags.Value) | SymbolFlags.Type | SymbolFlags.Namespace | SymbolFlags.Alias; + symbols = typeChecker.getSymbolsInScope(scopeNode, symbolMeanings); + Debug.assertEachIsDefined(symbols, "getSymbolsInScope() should all be defined"); + for (const symbol of symbols) { + if (!typeChecker.isArgumentsSymbol(symbol) && + !some(symbol.declarations, d => d.getSourceFile() === sourceFile)) { + symbolToSortTextMap[getSymbolId(symbol)] = SortText.GlobalsOrKeywords; + } } - - function isTypeOnlyCompletion(): boolean { - return insideJsDocTagTypeExpression - || !isContextTokenValueLocation(contextToken) && - (isPossiblyTypeArgumentPosition(contextToken, sourceFile, typeChecker) - || isPartOfTypeNode(location) - || isContextTokenTypeLocation(contextToken)); - } - - function isContextTokenValueLocation(contextToken: Node) { - return contextToken && - contextToken.kind === SyntaxKind.TypeOfKeyword && - (contextToken.parent.kind === SyntaxKind.TypeQuery || isTypeOfExpression(contextToken.parent)); - } - - function isContextTokenTypeLocation(contextToken: Node): boolean { - if (contextToken) { - const parentKind = contextToken.parent.kind; - switch (contextToken.kind) { - case SyntaxKind.ColonToken: - return parentKind === SyntaxKind.PropertyDeclaration || - parentKind === SyntaxKind.PropertySignature || - parentKind === SyntaxKind.Parameter || - parentKind === SyntaxKind.VariableDeclaration || - isFunctionLikeKind(parentKind); - - case SyntaxKind.EqualsToken: - return parentKind === SyntaxKind.TypeAliasDeclaration; - - case SyntaxKind.AsKeyword: - return parentKind === SyntaxKind.AsExpression; - - case SyntaxKind.LessThanToken: - return parentKind === SyntaxKind.TypeReference || - parentKind === SyntaxKind.TypeAssertionExpression; - - case SyntaxKind.ExtendsKeyword: - return parentKind === SyntaxKind.TypeParameter; + // Need to insert 'this.' before properties of `this` type, so only do that if `includeInsertTextCompletions` + if (preferences.includeCompletionsWithInsertText && scopeNode.kind !== SyntaxKind.SourceFile) { + const thisType = typeChecker.tryGetThisTypeAt(scopeNode, /*includeGlobalThis*/ false); + if (thisType) { + for (const symbol of getPropertiesForCompletion(thisType, typeChecker)) { + symbolToOriginInfoMap[getSymbolId(symbol)] = { kind: SymbolOriginInfoKind.ThisType }; + symbols.push(symbol); + symbolToSortTextMap[getSymbolId(symbol)] = SortText.SuggestedClassMembers; } } - return false; - } - - /** True if symbol is a type or a module containing at least one type. */ - function symbolCanBeReferencedAtTypeLocation(symbol: Symbol, seenModules = createMap()): boolean { - const sym = skipAlias(symbol.exportSymbol || symbol, typeChecker); - return !!(sym.flags & SymbolFlags.Type) || - !!(sym.flags & SymbolFlags.Module) && - addToSeen(seenModules, getSymbolId(sym)) && - typeChecker.getExportsOfModule(sym).some(e => symbolCanBeReferencedAtTypeLocation(e, seenModules)); } - - /** - * Gathers symbols that can be imported from other files, de-duplicating along the way. Symbols can be "duplicates" - * if re-exported from another module, e.g. `export { foo } from "./a"`. That syntax creates a fresh symbol, but - * it’s just an alias to the first, and both have the same name, so we generally want to filter those aliases out, - * if and only if the the first can be imported (it may be excluded due to package.json filtering in - * `codefix.forEachExternalModuleToImportFrom`). - * - * Example. Imagine a chain of node_modules re-exporting one original symbol: - * - * ```js - * node_modules/x/index.js node_modules/y/index.js node_modules/z/index.js - * +-----------------------+ +--------------------------+ +--------------------------+ - * | | | | | | - * | export const foo = 0; | <--- | export { foo } from 'x'; | <--- | export { foo } from 'y'; | - * | | | | | | - * +-----------------------+ +--------------------------+ +--------------------------+ - * ``` - * - * Also imagine three buckets, which we’ll reference soon: - * - * ```md - * | | | | | | - * | **Bucket A** | | **Bucket B** | | **Bucket C** | - * | Symbols to | | Aliases to symbols | | Symbols to return | - * | definitely | | in Buckets A or C | | if nothing better | - * | return | | (don’t return these) | | comes along | - * |__________________| |______________________| |___________________| - * ``` - * - * We _probably_ want to show `foo` from 'x', but not from 'y' or 'z'. However, if 'x' is not in a package.json, it - * will not appear in a `forEachExternalModuleToImportFrom` iteration. Furthermore, the order of iterations is not - * guaranteed, as it is host-dependent. Therefore, when presented with the symbol `foo` from module 'y' alone, we - * may not be sure whether or not it should go in the list. So, we’ll take the following steps: - * - * 1. Resolve alias `foo` from 'y' to the export declaration in 'x', get the symbol there, and see if that symbol is - * already in Bucket A (symbols we already know will be returned). If it is, put `foo` from 'y' in Bucket B - * (symbols that are aliases to symbols in Bucket A). If it’s not, put it in Bucket C. - * 2. Next, imagine we see `foo` from module 'z'. Again, we resolve the alias to the nearest export, which is in 'y'. - * At this point, if that nearest export from 'y' is in _any_ of the three buckets, we know the symbol in 'z' - * should never be returned in the final list, so put it in Bucket B. - * 3. Next, imagine we see `foo` from module 'x', the original. Syntactically, it doesn’t look like a re-export, so - * we can just check Bucket C to see if we put any aliases to the original in there. If they exist, throw them out. - * Put this symbol in Bucket A. - * 4. After we’ve iterated through every symbol of every module, any symbol left in Bucket C means that step 3 didn’t - * occur for that symbol---that is, the original symbol is not in Bucket A, so we should include the alias. Move - * everything from Bucket C to Bucket A. - */ - function getSymbolsFromOtherSourceFileExports(target: ScriptTarget, host: LanguageServiceHost): readonly AutoImportSuggestion[] { - const cached = importSuggestionsCache && importSuggestionsCache.get( - sourceFile.fileName, - typeChecker, - detailsEntryId && host.getProjectVersion ? host.getProjectVersion() : undefined); - - if (cached) { - log("getSymbolsFromOtherSourceFileExports: Using cached list"); - return cached; + if (shouldOfferImportCompletions()) { + const lowerCaseTokenText = previousToken && isIdentifier(previousToken) ? previousToken.text.toLowerCase() : ""; + const autoImportSuggestions = getSymbolsFromOtherSourceFileExports(program.getCompilerOptions().target!, host); + if (!detailsEntryId && importSuggestionsCache) { + importSuggestionsCache.set(sourceFile.fileName, autoImportSuggestions, host.getProjectVersion && host.getProjectVersion()); } - - const startTime = timestamp(); - log(`getSymbolsFromOtherSourceFileExports: Recomputing list${detailsEntryId ? " for details entry" : ""}`); - const seenResolvedModules = createMap(); - /** Bucket B */ - const aliasesToAlreadyIncludedSymbols = createMap(); - /** Bucket C */ - const aliasesToReturnIfOriginalsAreMissing = createMap<{ alias: Symbol, moduleSymbol: Symbol }>(); - /** Bucket A */ - const results: AutoImportSuggestion[] = []; - /** Ids present in `results` for faster lookup */ - const resultSymbolIds = createMap(); - - codefix.forEachExternalModuleToImportFrom(program, host, sourceFile, !detailsEntryId, moduleSymbol => { - // Perf -- ignore other modules if this is a request for details - if (detailsEntryId && detailsEntryId.source && stripQuotes(moduleSymbol.name) !== detailsEntryId.source) { - return; - } - - const resolvedModuleSymbol = typeChecker.resolveExternalModuleSymbol(moduleSymbol); - // resolvedModuleSymbol may be a namespace. A namespace may be `export =` by multiple module declarations, but only keep the first one. - if (!addToSeen(seenResolvedModules, getSymbolId(resolvedModuleSymbol))) { - return; - } - - // Don't add another completion for `export =` of a symbol that's already global. - // So in `declare namespace foo {} declare module "foo" { export = foo; }`, there will just be the global completion for `foo`. - if (resolvedModuleSymbol !== moduleSymbol && - every(resolvedModuleSymbol.declarations, d => !!d.getSourceFile().externalModuleIndicator && !findAncestor(d, isGlobalScopeAugmentation))) { - pushSymbol(resolvedModuleSymbol, moduleSymbol, /*skipFilter*/ true); - } - - for (const symbol of typeChecker.getExportsOfModule(moduleSymbol)) { - // If this is `export { _break as break };` (a keyword) -- skip this and prefer the keyword completion. - if (some(symbol.declarations, d => isExportSpecifier(d) && !!d.propertyName && isIdentifierANonContextualKeyword(d.name))) { - continue; - } - - const symbolId = getSymbolId(symbol).toString(); - // If `symbol.parent !== moduleSymbol`, this is an `export * from "foo"` re-export. Those don't create new symbols. - const isExportStarFromReExport = typeChecker.getMergedSymbol(symbol.parent!) !== resolvedModuleSymbol; - // If `!!d.parent.parent.moduleSpecifier`, this is `export { foo } from "foo"` re-export, which creates a new symbol (thus isn't caught by the first check). - if (isExportStarFromReExport || some(symbol.declarations, d => isExportSpecifier(d) && !d.propertyName && !!d.parent.parent.moduleSpecifier)) { - // Walk the export chain back one module (step 1 or 2 in diagrammed example). - // Or, in the case of `export * from "foo"`, `symbol` already points to the original export, so just use that. - const nearestExportSymbol = isExportStarFromReExport ? symbol : getNearestExportSymbol(symbol); - if (!nearestExportSymbol) continue; - const nearestExportSymbolId = getSymbolId(nearestExportSymbol).toString(); - const symbolHasBeenSeen = resultSymbolIds.has(nearestExportSymbolId) || aliasesToAlreadyIncludedSymbols.has(nearestExportSymbolId); - if (!symbolHasBeenSeen) { - aliasesToReturnIfOriginalsAreMissing.set(nearestExportSymbolId, { alias: symbol, moduleSymbol }); - aliasesToAlreadyIncludedSymbols.set(symbolId, true); - } - else { - // Perf - we know this symbol is an alias to one that’s already covered in `symbols`, so store it here - // in case another symbol re-exports this one; that way we can short-circuit as soon as we see this symbol id. - addToSeen(aliasesToAlreadyIncludedSymbols, symbolId); - } - } - else { - // This is not a re-export, so see if we have any aliases pending and remove them (step 3 in diagrammed example) - aliasesToReturnIfOriginalsAreMissing.delete(symbolId); - pushSymbol(symbol, moduleSymbol); + autoImportSuggestions.forEach(({ symbol, symbolName, skipFilter, origin }) => { + if (detailsEntryId) { + if (detailsEntryId.source && stripQuotes(origin.moduleSymbol.name) !== detailsEntryId.source) { + return; } } - }); - - // By this point, any potential duplicates that were actually duplicates have been - // removed, so the rest need to be added. (Step 4 in diagrammed example) - aliasesToReturnIfOriginalsAreMissing.forEach(({ alias, moduleSymbol }) => pushSymbol(alias, moduleSymbol)); - log(`getSymbolsFromOtherSourceFileExports: ${timestamp() - startTime}`); - return results; - - function pushSymbol(symbol: Symbol, moduleSymbol: Symbol, skipFilter = false) { - const isDefaultExport = symbol.escapedName === InternalSymbolName.Default; - if (isDefaultExport) { - symbol = getLocalSymbolForExportDefault(symbol) || symbol; - } - if (typeChecker.isUndefinedSymbol(symbol)) { + else if (!skipFilter && !stringContainsCharactersInOrder(symbolName.toLowerCase(), lowerCaseTokenText)) { return; } - addToSeen(resultSymbolIds, getSymbolId(symbol)); - const origin: SymbolOriginInfoExport = { kind: SymbolOriginInfoKind.Export, moduleSymbol, isDefaultExport }; - results.push({ - symbol, - symbolName: getNameForExportedSymbol(symbol, target), - origin, - skipFilter, - }); - } - } - - function getNearestExportSymbol(fromSymbol: Symbol) { - return findAlias(typeChecker, fromSymbol, alias => { - return some(alias.declarations, d => isExportSpecifier(d) || !!d.localSymbol); + const symbolId = getSymbolId(symbol); + symbols.push(symbol); + symbolToOriginInfoMap[symbolId] = origin; + symbolToSortTextMap[symbolId] = SortText.AutoImportSuggestions; }); } - - /** - * True if you could remove some characters in `a` to get `b`. - * E.g., true for "abcdef" and "bdf". - * But not true for "abcdef" and "dbf". - */ - function stringContainsCharactersInOrder(str: string, characters: string): boolean { - if (characters.length === 0) { - return true; - } - - let characterIndex = 0; - for (let strIndex = 0; strIndex < str.length; strIndex++) { - if (str.charCodeAt(strIndex) === characters.charCodeAt(characterIndex)) { - characterIndex++; - if (characterIndex === characters.length) { - return true; - } - } - } - - // Did not find all characters + filterGlobalCompletion(symbols); + } + function shouldOfferImportCompletions(): boolean { + // If not already a module, must have modules enabled and not currently be in a commonjs module. (TODO: import completions for commonjs) + if (!preferences.includeCompletionsForModuleExports) return false; - } - - /** - * Finds the first node that "embraces" the position, so that one may - * accurately aggregate locals from the closest containing scope. - */ - function getScopeNode(initialToken: Node | undefined, position: number, sourceFile: SourceFile) { - let scope: Node | undefined = initialToken; - while (scope && !positionBelongsToNode(scope, position, sourceFile)) { - scope = scope.parent; - } - return scope; - } - - function isCompletionListBlocker(contextToken: Node): boolean { - const start = timestamp(); - const result = isInStringOrRegularExpressionOrTemplateLiteral(contextToken) || - isSolelyIdentifierDefinitionLocation(contextToken) || - isDotOfNumericLiteral(contextToken) || - isInJsxText(contextToken); - log("getCompletionsAtPosition: isCompletionListBlocker: " + (timestamp() - start)); - return result; - } - - function isInJsxText(contextToken: Node): boolean { - if (contextToken.kind === SyntaxKind.JsxText) { - return true; - } - - if (contextToken.kind === SyntaxKind.GreaterThanToken && contextToken.parent) { - if (contextToken.parent.kind === SyntaxKind.JsxOpeningElement) { - // Two possibilities: - // 1.
/**/ - // - contextToken: GreaterThanToken (before cursor) - // - location: JSXElement - // - different parents (JSXOpeningElement, JSXElement) - // 2. /**/> - // - contextToken: GreaterThanToken (before cursor) - // - location: GreaterThanToken (after cursor) - // - same parent (JSXOpeningElement) - return location.parent.kind !== SyntaxKind.JsxOpeningElement; - } - - if (contextToken.parent.kind === SyntaxKind.JsxClosingElement || contextToken.parent.kind === SyntaxKind.JsxSelfClosingElement) { - return !!contextToken.parent.parent && contextToken.parent.parent.kind === SyntaxKind.JsxElement; - } - } + // If already using ES6 modules, OK to continue using them. + if (sourceFile.externalModuleIndicator) + return true; + // If already using commonjs, don't introduce ES6. + if (sourceFile.commonJsModuleIndicator) return false; + // If module transpilation is enabled or we're targeting es6 or above, or not emitting, OK. + if (compilerOptionsIndicateEs6Modules(program.getCompilerOptions())) + return true; + // If some file is using ES6 modules, assume that it's OK to add more. + return programContainsEs6Modules(program); + } + function isSnippetScope(scopeNode: Node): boolean { + switch (scopeNode.kind) { + case SyntaxKind.SourceFile: + case SyntaxKind.TemplateExpression: + case SyntaxKind.JsxExpression: + case SyntaxKind.Block: + return true; + default: + return isStatement(scopeNode); } - - function isNewIdentifierDefinitionLocation(previousToken: Node | undefined): boolean { - if (previousToken) { - const containingNodeKind = previousToken.parent.kind; - // Previous token may have been a keyword that was converted to an identifier. - switch (keywordForNode(previousToken)) { - case SyntaxKind.CommaToken: - return containingNodeKind === SyntaxKind.CallExpression // func( a, | - || containingNodeKind === SyntaxKind.Constructor // constructor( a, | /* public, protected, private keywords are allowed here, so show completion */ - || containingNodeKind === SyntaxKind.NewExpression // new C(a, | - || containingNodeKind === SyntaxKind.ArrayLiteralExpression // [a, | - || containingNodeKind === SyntaxKind.BinaryExpression // const x = (a, | - || containingNodeKind === SyntaxKind.FunctionType; // var x: (s: string, list| - - case SyntaxKind.OpenParenToken: - return containingNodeKind === SyntaxKind.CallExpression // func( | - || containingNodeKind === SyntaxKind.Constructor // constructor( | - || containingNodeKind === SyntaxKind.NewExpression // new C(a| - || containingNodeKind === SyntaxKind.ParenthesizedExpression // const x = (a| - || containingNodeKind === SyntaxKind.ParenthesizedType; // function F(pred: (a| /* this can become an arrow function, where 'a' is the argument */ - - case SyntaxKind.OpenBracketToken: - return containingNodeKind === SyntaxKind.ArrayLiteralExpression // [ | - || containingNodeKind === SyntaxKind.IndexSignature // [ | : string ] - || containingNodeKind === SyntaxKind.ComputedPropertyName; // [ | /* this can become an index signature */ - - case SyntaxKind.ModuleKeyword: // module | - case SyntaxKind.NamespaceKeyword: // namespace | - return true; - - case SyntaxKind.DotToken: - return containingNodeKind === SyntaxKind.ModuleDeclaration; // module A.| - - case SyntaxKind.OpenBraceToken: - return containingNodeKind === SyntaxKind.ClassDeclaration; // class A{ | - - case SyntaxKind.EqualsToken: - return containingNodeKind === SyntaxKind.VariableDeclaration // const x = a| - || containingNodeKind === SyntaxKind.BinaryExpression; // x = a| - - case SyntaxKind.TemplateHead: - return containingNodeKind === SyntaxKind.TemplateExpression; // `aa ${| - - case SyntaxKind.TemplateMiddle: - return containingNodeKind === SyntaxKind.TemplateSpan; // `aa ${10} dd ${| - - case SyntaxKind.PublicKeyword: - case SyntaxKind.PrivateKeyword: - case SyntaxKind.ProtectedKeyword: - return containingNodeKind === SyntaxKind.PropertyDeclaration; // class A{ public | + } + function filterGlobalCompletion(symbols: Symbol[]): void { + const isTypeOnly = isTypeOnlyCompletion(); + if (isTypeOnly) { + keywordFilters = isTypeAssertion() + ? KeywordCompletionFilters.TypeAssertionKeywords + : KeywordCompletionFilters.TypeKeywords; + } + filterMutate(symbols, symbol => { + if (!isSourceFile(location)) { + // export = /**/ here we want to get all meanings, so any symbol is ok + if (isExportAssignment(location.parent)) { + return true; } - } - - return false; - } - - function isInStringOrRegularExpressionOrTemplateLiteral(contextToken: Node): boolean { - // 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'). - return (isRegularExpressionLiteral(contextToken) || isStringTextContainingNode(contextToken)) && ( - rangeContainsPositionExclusive(createTextRangeFromSpan(createTextSpanFromNode(contextToken)), position) || - position === contextToken.end && (!!contextToken.isUnterminated || isRegularExpressionLiteral(contextToken))); - } - - /** - * Aggregates relevant symbols for completion in object literals and object binding patterns. - * Relevant symbols are stored in the captured 'symbols' variable. - * - * @returns true if 'symbols' was successfully populated; false otherwise. - */ - function tryGetObjectLikeCompletionSymbols(): GlobalsSearch | undefined { - const objectLikeContainer = tryGetObjectLikeCompletionContainer(contextToken); - if (!objectLikeContainer) return GlobalsSearch.Continue; - - // We're looking up possible property names from contextual/inferred/declared type. - completionKind = CompletionKind.ObjectPropertyDeclaration; - - let typeMembers: Symbol[] | undefined; - let existingMembers: readonly Declaration[] | undefined; - - if (objectLikeContainer.kind === SyntaxKind.ObjectLiteralExpression) { - const instantiatedType = typeChecker.getContextualType(objectLikeContainer); - const completionsType = instantiatedType && typeChecker.getContextualType(objectLikeContainer, ContextFlags.Completions); - if (!instantiatedType || !completionsType) return GlobalsSearch.Fail; - isNewIdentifierLocation = hasIndexSignature(instantiatedType || completionsType); - typeMembers = getPropertiesForObjectExpression(instantiatedType, completionsType, objectLikeContainer, typeChecker); - existingMembers = objectLikeContainer.properties; - } - else { - Debug.assert(objectLikeContainer.kind === SyntaxKind.ObjectBindingPattern); - // We are *only* completing on properties from the type being destructured. - isNewIdentifierLocation = false; - - const rootDeclaration = getRootDeclaration(objectLikeContainer.parent); - if (!isVariableLike(rootDeclaration)) return Debug.fail("Root declaration is not variable-like."); - - // We don't want to complete using the type acquired by the shape - // of the binding pattern; we are only interested in types acquired - // through type declaration or inference. - // Also proceed if rootDeclaration is a parameter and if its containing function expression/arrow function is contextually typed - - // type of parameter will flow in from the contextual type of the function - let canGetType = hasInitializer(rootDeclaration) || hasType(rootDeclaration) || rootDeclaration.parent.parent.kind === SyntaxKind.ForOfStatement; - if (!canGetType && rootDeclaration.kind === SyntaxKind.Parameter) { - if (isExpression(rootDeclaration.parent)) { - canGetType = !!typeChecker.getContextualType(rootDeclaration.parent); - } - else if (rootDeclaration.parent.kind === SyntaxKind.MethodDeclaration || rootDeclaration.parent.kind === SyntaxKind.SetAccessor) { - canGetType = isExpression(rootDeclaration.parent.parent) && !!typeChecker.getContextualType(rootDeclaration.parent.parent); - } + symbol = skipAlias(symbol, typeChecker); + // import m = /**/ <-- It can only access namespace (if typing import = x. this would get member symbols and not namespace) + if (isInRightSideOfInternalImportEqualsDeclaration(location)) { + return !!(symbol.flags & SymbolFlags.Namespace); } - if (canGetType) { - const typeForObject = typeChecker.getTypeAtLocation(objectLikeContainer); - if (!typeForObject) return GlobalsSearch.Fail; - // In a binding pattern, get only known properties (unless in the same scope). - // Everywhere else we will get all possible properties. - const containerClass = getContainingClass(objectLikeContainer); - typeMembers = typeChecker.getPropertiesOfType(typeForObject).filter(symbol => - // either public - !(getDeclarationModifierFlagsFromSymbol(symbol) & ModifierFlags.NonPublicAccessibilityModifier) - // or we're in it - || containerClass && contains(typeForObject.symbol.declarations, containerClass)); - existingMembers = objectLikeContainer.elements; + if (isTypeOnly) { + // It's a type, but you can reach it by namespace.type as well + return symbolCanBeReferencedAtTypeLocation(symbol); } } - - if (typeMembers && typeMembers.length > 0) { - // Add filtered items to the completion list - symbols = filterObjectMembersList(typeMembers, Debug.checkDefined(existingMembers)); + // expressions are value space (which includes the value namespaces) + return !!(getCombinedLocalAndExportSymbolFlags(symbol) & SymbolFlags.Value); + }); + } + function isTypeAssertion(): boolean { + return isAssertionExpression(contextToken.parent); + } + function isTypeOnlyCompletion(): boolean { + return insideJsDocTagTypeExpression + || !isContextTokenValueLocation(contextToken) && + (isPossiblyTypeArgumentPosition(contextToken, sourceFile, typeChecker) + || isPartOfTypeNode(location) + || isContextTokenTypeLocation(contextToken)); + } + function isContextTokenValueLocation(contextToken: Node) { + return contextToken && + contextToken.kind === SyntaxKind.TypeOfKeyword && + (contextToken.parent.kind === SyntaxKind.TypeQuery || isTypeOfExpression(contextToken.parent)); + } + function isContextTokenTypeLocation(contextToken: Node): boolean { + if (contextToken) { + const parentKind = contextToken.parent.kind; + switch (contextToken.kind) { + case SyntaxKind.ColonToken: + return parentKind === SyntaxKind.PropertyDeclaration || + parentKind === SyntaxKind.PropertySignature || + parentKind === SyntaxKind.Parameter || + parentKind === SyntaxKind.VariableDeclaration || + isFunctionLikeKind(parentKind); + case SyntaxKind.EqualsToken: + return parentKind === SyntaxKind.TypeAliasDeclaration; + case SyntaxKind.AsKeyword: + return parentKind === SyntaxKind.AsExpression; + case SyntaxKind.LessThanToken: + return parentKind === SyntaxKind.TypeReference || + parentKind === SyntaxKind.TypeAssertionExpression; + case SyntaxKind.ExtendsKeyword: + return parentKind === SyntaxKind.TypeParameter; } - setSortTextToOptionalMember(); - - return GlobalsSearch.Success; } - - /** - * Aggregates relevant symbols for completion in import clauses and export clauses - * whose declarations have a module specifier; for instance, symbols will be aggregated for - * - * import { | } from "moduleName"; - * export { a as foo, | } from "moduleName"; - * - * but not for - * - * export { | }; - * - * Relevant symbols are stored in the captured 'symbols' variable. - * - * @returns true if 'symbols' was successfully populated; false otherwise. - */ - function tryGetImportOrExportClauseCompletionSymbols(): GlobalsSearch { - // `import { |` or `import { a as 0, | }` - const namedImportsOrExports = contextToken && (contextToken.kind === SyntaxKind.OpenBraceToken || contextToken.kind === SyntaxKind.CommaToken) - ? tryCast(contextToken.parent, isNamedImportsOrExports) : undefined; - if (!namedImportsOrExports) return GlobalsSearch.Continue; - - // cursor is in an import clause - // try to show exported member for imported module - const { moduleSpecifier } = namedImportsOrExports.kind === SyntaxKind.NamedImports ? namedImportsOrExports.parent.parent : namedImportsOrExports.parent; - const moduleSpecifierSymbol = typeChecker.getSymbolAtLocation(moduleSpecifier!); // TODO: GH#18217 - if (!moduleSpecifierSymbol) return GlobalsSearch.Fail; - - completionKind = CompletionKind.MemberLike; - isNewIdentifierLocation = false; - const exports = typeChecker.getExportsAndPropertiesOfModule(moduleSpecifierSymbol); - const existing = arrayToSet(namedImportsOrExports.elements, n => isCurrentlyEditingNode(n) ? undefined : (n.propertyName || n.name).escapedText); - symbols = exports.filter(e => e.escapedName !== InternalSymbolName.Default && !existing.get(e.escapedName)); - return GlobalsSearch.Success; - } - - /** - * Aggregates relevant symbols for completion in class declaration - * Relevant symbols are stored in the captured 'symbols' variable. - */ - function tryGetClassLikeCompletionSymbols(): GlobalsSearch { - const decl = tryGetObjectTypeDeclarationCompletionContainer(sourceFile, contextToken, location, position); - if (!decl) return GlobalsSearch.Continue; - - // We're looking up possible property names from parent type. - completionKind = CompletionKind.MemberLike; - // Declaring new property/method/accessor - isNewIdentifierLocation = true; - keywordFilters = contextToken.kind === SyntaxKind.AsteriskToken ? KeywordCompletionFilters.None : - isClassLike(decl) ? KeywordCompletionFilters.ClassElementKeywords : KeywordCompletionFilters.InterfaceElementKeywords; - - // If you're in an interface you don't want to repeat things from super-interface. So just stop here. - if (!isClassLike(decl)) return GlobalsSearch.Success; - - const classElement = contextToken.kind === SyntaxKind.SemicolonToken ? contextToken.parent.parent : contextToken.parent; - let classElementModifierFlags = isClassElement(classElement) ? getModifierFlags(classElement) : ModifierFlags.None; - // If this is context token is not something we are editing now, consider if this would lead to be modifier - if (contextToken.kind === SyntaxKind.Identifier && !isCurrentlyEditingNode(contextToken)) { - switch (contextToken.getText()) { - case "private": - classElementModifierFlags = classElementModifierFlags | ModifierFlags.Private; - break; - case "static": - classElementModifierFlags = classElementModifierFlags | ModifierFlags.Static; - break; - } + return false; + } + /** True if symbol is a type or a module containing at least one type. */ + function symbolCanBeReferencedAtTypeLocation(symbol: Symbol, seenModules = createMap()): boolean { + const sym = skipAlias(symbol.exportSymbol || symbol, typeChecker); + return !!(sym.flags & SymbolFlags.Type) || + !!(sym.flags & SymbolFlags.Module) && + addToSeen(seenModules, getSymbolId(sym)) && + typeChecker.getExportsOfModule(sym).some(e => symbolCanBeReferencedAtTypeLocation(e, seenModules)); + } + /** + * Gathers symbols that can be imported from other files, de-duplicating along the way. Symbols can be "duplicates" + * if re-exported from another module, e.g. `export { foo } from "./a"`. That syntax creates a fresh symbol, but + * it’s just an alias to the first, and both have the same name, so we generally want to filter those aliases out, + * if and only if the the first can be imported (it may be excluded due to package.json filtering in + * `codefix.forEachExternalModuleToImportFrom`). + * + * Example. Imagine a chain of node_modules re-exporting one original symbol: + * + * ```js + * node_modules/x/index.js node_modules/y/index.js node_modules/z/index.js + * +-----------------------+ +--------------------------+ +--------------------------+ + * | | | | | | + * | export const foo = 0; | <--- | export { foo } from 'x'; | <--- | export { foo } from 'y'; | + * | | | | | | + * +-----------------------+ +--------------------------+ +--------------------------+ + * ``` + * + * Also imagine three buckets, which we’ll reference soon: + * + * ```md + * | | | | | | + * | **Bucket A** | | **Bucket B** | | **Bucket C** | + * | Symbols to | | Aliases to symbols | | Symbols to return | + * | definitely | | in Buckets A or C | | if nothing better | + * | return | | (don’t return these) | | comes along | + * |__________________| |______________________| |___________________| + * ``` + * + * We _probably_ want to show `foo` from 'x', but not from 'y' or 'z'. However, if 'x' is not in a package.json, it + * will not appear in a `forEachExternalModuleToImportFrom` iteration. Furthermore, the order of iterations is not + * guaranteed, as it is host-dependent. Therefore, when presented with the symbol `foo` from module 'y' alone, we + * may not be sure whether or not it should go in the list. So, we’ll take the following steps: + * + * 1. Resolve alias `foo` from 'y' to the export declaration in 'x', get the symbol there, and see if that symbol is + * already in Bucket A (symbols we already know will be returned). If it is, put `foo` from 'y' in Bucket B + * (symbols that are aliases to symbols in Bucket A). If it’s not, put it in Bucket C. + * 2. Next, imagine we see `foo` from module 'z'. Again, we resolve the alias to the nearest export, which is in 'y'. + * At this point, if that nearest export from 'y' is in _any_ of the three buckets, we know the symbol in 'z' + * should never be returned in the final list, so put it in Bucket B. + * 3. Next, imagine we see `foo` from module 'x', the original. Syntactically, it doesn’t look like a re-export, so + * we can just check Bucket C to see if we put any aliases to the original in there. If they exist, throw them out. + * Put this symbol in Bucket A. + * 4. After we’ve iterated through every symbol of every module, any symbol left in Bucket C means that step 3 didn’t + * occur for that symbol---that is, the original symbol is not in Bucket A, so we should include the alias. Move + * everything from Bucket C to Bucket A. + */ + function getSymbolsFromOtherSourceFileExports(target: ScriptTarget, host: LanguageServiceHost): readonly AutoImportSuggestion[] { + const cached = importSuggestionsCache && importSuggestionsCache.get(sourceFile.fileName, typeChecker, detailsEntryId && host.getProjectVersion ? host.getProjectVersion() : undefined); + if (cached) { + log("getSymbolsFromOtherSourceFileExports: Using cached list"); + return cached; + } + const startTime = timestamp(); + log(`getSymbolsFromOtherSourceFileExports: Recomputing list${detailsEntryId ? " for details entry" : ""}`); + const seenResolvedModules = createMap(); + /** Bucket B */ + const aliasesToAlreadyIncludedSymbols = createMap(); + /** Bucket C */ + const aliasesToReturnIfOriginalsAreMissing = createMap<{ + alias: Symbol; + moduleSymbol: Symbol; + }>(); + /** Bucket A */ + const results: AutoImportSuggestion[] = []; + /** Ids present in `results` for faster lookup */ + const resultSymbolIds = createMap(); + forEachExternalModuleToImportFrom(program, host, sourceFile, !detailsEntryId, moduleSymbol => { + // Perf -- ignore other modules if this is a request for details + if (detailsEntryId && detailsEntryId.source && stripQuotes(moduleSymbol.name) !== detailsEntryId.source) { + return; } - - // No member list for private methods - if (!(classElementModifierFlags & ModifierFlags.Private)) { - // List of property symbols of base type that are not private and already implemented - const baseSymbols = flatMap(getAllSuperTypeNodes(decl), baseTypeNode => { - const type = typeChecker.getTypeAtLocation(baseTypeNode); - return type && typeChecker.getPropertiesOfType(classElementModifierFlags & ModifierFlags.Static ? typeChecker.getTypeOfSymbolAtLocation(type.symbol, decl) : type); - }); - symbols = filterClassMembersList(baseSymbols, decl.members, classElementModifierFlags); + const resolvedModuleSymbol = typeChecker.resolveExternalModuleSymbol(moduleSymbol); + // resolvedModuleSymbol may be a namespace. A namespace may be `export =` by multiple module declarations, but only keep the first one. + if (!addToSeen(seenResolvedModules, getSymbolId(resolvedModuleSymbol))) { + return; } - - return GlobalsSearch.Success; - } - - /** - * Returns the immediate owning object literal or binding pattern of a context token, - * on the condition that one exists and that the context implies completion should be given. - */ - function tryGetObjectLikeCompletionContainer(contextToken: Node): ObjectLiteralExpression | ObjectBindingPattern | undefined { - if (contextToken) { - const { parent } = contextToken; - switch (contextToken.kind) { - case SyntaxKind.OpenBraceToken: // const x = { | - case SyntaxKind.CommaToken: // const x = { a: 0, | - if (isObjectLiteralExpression(parent) || isObjectBindingPattern(parent)) { - return parent; - } - break; - case SyntaxKind.AsteriskToken: - return isMethodDeclaration(parent) ? tryCast(parent.parent, isObjectLiteralExpression) : undefined; - case SyntaxKind.Identifier: - return (contextToken as Identifier).text === "async" && isShorthandPropertyAssignment(contextToken.parent) - ? contextToken.parent.parent : undefined; - } + // Don't add another completion for `export =` of a symbol that's already global. + // So in `declare namespace foo {} declare module "foo" { export = foo; }`, there will just be the global completion for `foo`. + if (resolvedModuleSymbol !== moduleSymbol && + every(resolvedModuleSymbol.declarations, d => !!d.getSourceFile().externalModuleIndicator && !findAncestor(d, isGlobalScopeAugmentation))) { + pushSymbol(resolvedModuleSymbol, moduleSymbol, /*skipFilter*/ true); } - - return undefined; - } - - function isConstructorParameterCompletion(node: Node): boolean { - return !!node.parent && isParameter(node.parent) && isConstructorDeclaration(node.parent.parent) - && (isParameterPropertyModifier(node.kind) || isDeclarationName(node)); - } - - /** - * Returns the immediate owning class declaration of a context token, - * on the condition that one exists and that the context implies completion should be given. - */ - function tryGetConstructorLikeCompletionContainer(contextToken: Node): ConstructorDeclaration | undefined { - if (contextToken) { - const parent = contextToken.parent; - switch (contextToken.kind) { - case SyntaxKind.OpenParenToken: - case SyntaxKind.CommaToken: - return isConstructorDeclaration(contextToken.parent) ? contextToken.parent : undefined; - - default: - if (isConstructorParameterCompletion(contextToken)) { - return parent.parent as ConstructorDeclaration; - } + for (const symbol of typeChecker.getExportsOfModule(moduleSymbol)) { + // If this is `export { _break as break };` (a keyword) -- skip this and prefer the keyword completion. + if (some(symbol.declarations, d => isExportSpecifier(d) && !!d.propertyName && isIdentifierANonContextualKeyword(d.name))) { + continue; } - } - return undefined; - } - - function tryGetFunctionLikeBodyCompletionContainer(contextToken: Node): FunctionLikeDeclaration | undefined { - if (contextToken) { - let prev: Node; - const container = findAncestor(contextToken.parent, (node: Node) => { - if (isClassLike(node)) { - return "quit"; + const symbolId = getSymbolId(symbol).toString(); + // If `symbol.parent !== moduleSymbol`, this is an `export * from "foo"` re-export. Those don't create new symbols. + const isExportStarFromReExport = typeChecker.getMergedSymbol(symbol.parent!) !== resolvedModuleSymbol; + // If `!!d.parent.parent.moduleSpecifier`, this is `export { foo } from "foo"` re-export, which creates a new symbol (thus isn't caught by the first check). + if (isExportStarFromReExport || some(symbol.declarations, d => isExportSpecifier(d) && !d.propertyName && !!d.parent.parent.moduleSpecifier)) { + // Walk the export chain back one module (step 1 or 2 in diagrammed example). + // Or, in the case of `export * from "foo"`, `symbol` already points to the original export, so just use that. + const nearestExportSymbol = isExportStarFromReExport ? symbol : getNearestExportSymbol(symbol); + if (!nearestExportSymbol) + continue; + const nearestExportSymbolId = getSymbolId(nearestExportSymbol).toString(); + const symbolHasBeenSeen = resultSymbolIds.has(nearestExportSymbolId) || aliasesToAlreadyIncludedSymbols.has(nearestExportSymbolId); + if (!symbolHasBeenSeen) { + aliasesToReturnIfOriginalsAreMissing.set(nearestExportSymbolId, { alias: symbol, moduleSymbol }); + aliasesToAlreadyIncludedSymbols.set(symbolId, true); } - if (isFunctionLikeDeclaration(node) && prev === node.body) { - return true; + else { + // Perf - we know this symbol is an alias to one that’s already covered in `symbols`, so store it here + // in case another symbol re-exports this one; that way we can short-circuit as soon as we see this symbol id. + addToSeen(aliasesToAlreadyIncludedSymbols, symbolId); } - prev = node; - return false; - }); - return container && container as FunctionLikeDeclaration; + } + else { + // This is not a re-export, so see if we have any aliases pending and remove them (step 3 in diagrammed example) + aliasesToReturnIfOriginalsAreMissing.delete(symbolId); + pushSymbol(symbol, moduleSymbol); + } + } + }); + // By this point, any potential duplicates that were actually duplicates have been + // removed, so the rest need to be added. (Step 4 in diagrammed example) + aliasesToReturnIfOriginalsAreMissing.forEach(({ alias, moduleSymbol }) => pushSymbol(alias, moduleSymbol)); + log(`getSymbolsFromOtherSourceFileExports: ${timestamp() - startTime}`); + return results; + function pushSymbol(symbol: Symbol, moduleSymbol: Symbol, skipFilter = false) { + const isDefaultExport = symbol.escapedName === InternalSymbolName.Default; + if (isDefaultExport) { + symbol = getLocalSymbolForExportDefault(symbol) || symbol; + } + if (typeChecker.isUndefinedSymbol(symbol)) { + return; } + addToSeen(resultSymbolIds, getSymbolId(symbol)); + const origin: SymbolOriginInfoExport = { kind: SymbolOriginInfoKind.Export, moduleSymbol, isDefaultExport }; + results.push({ + symbol, + symbolName: getNameForExportedSymbol(symbol, target), + origin, + skipFilter, + }); } - - function tryGetContainingJsxElement(contextToken: Node): JsxOpeningLikeElement | undefined { - if (contextToken) { - const parent = contextToken.parent; - switch (contextToken.kind) { - case SyntaxKind.GreaterThanToken: // End of a type argument list - case SyntaxKind.LessThanSlashToken: - case SyntaxKind.SlashToken: - case SyntaxKind.Identifier: - case SyntaxKind.PropertyAccessExpression: - case SyntaxKind.JsxAttributes: - case SyntaxKind.JsxAttribute: - case SyntaxKind.JsxSpreadAttribute: - if (parent && (parent.kind === SyntaxKind.JsxSelfClosingElement || parent.kind === SyntaxKind.JsxOpeningElement)) { - if (contextToken.kind === SyntaxKind.GreaterThanToken) { - const precedingToken = findPrecedingToken(contextToken.pos, sourceFile, /*startNode*/ undefined); - if (!(parent as JsxOpeningLikeElement).typeArguments || (precedingToken && precedingToken.kind === SyntaxKind.SlashToken)) break; - } - return parent; - } - else if (parent.kind === SyntaxKind.JsxAttribute) { - // Currently we parse JsxOpeningLikeElement as: - // JsxOpeningLikeElement - // attributes: JsxAttributes - // properties: NodeArray - return parent.parent.parent as JsxOpeningLikeElement; - } - break; - - // The context token is the closing } or " of an attribute, which means - // its parent is a JsxExpression, whose parent is a JsxAttribute, - // whose parent is a JsxOpeningLikeElement - case SyntaxKind.StringLiteral: - if (parent && ((parent.kind === SyntaxKind.JsxAttribute) || (parent.kind === SyntaxKind.JsxSpreadAttribute))) { - // Currently we parse JsxOpeningLikeElement as: - // JsxOpeningLikeElement - // attributes: JsxAttributes - // properties: NodeArray - return parent.parent.parent as JsxOpeningLikeElement; - } - - break; - - case SyntaxKind.CloseBraceToken: - if (parent && - parent.kind === SyntaxKind.JsxExpression && - parent.parent && parent.parent.kind === SyntaxKind.JsxAttribute) { - // Currently we parse JsxOpeningLikeElement as: - // JsxOpeningLikeElement - // attributes: JsxAttributes - // properties: NodeArray - // each JsxAttribute can have initializer as JsxExpression - return parent.parent.parent.parent as JsxOpeningLikeElement; - } - - if (parent && parent.kind === SyntaxKind.JsxSpreadAttribute) { - // Currently we parse JsxOpeningLikeElement as: - // JsxOpeningLikeElement - // attributes: JsxAttributes - // properties: NodeArray - return parent.parent.parent as JsxOpeningLikeElement; - } - - break; + } + function getNearestExportSymbol(fromSymbol: Symbol) { + return findAlias(typeChecker, fromSymbol, alias => { + return some(alias.declarations, d => isExportSpecifier(d) || !!d.localSymbol); + }); + } + /** + * True if you could remove some characters in `a` to get `b`. + * E.g., true for "abcdef" and "bdf". + * But not true for "abcdef" and "dbf". + */ + function stringContainsCharactersInOrder(str: string, characters: string): boolean { + if (characters.length === 0) { + return true; + } + let characterIndex = 0; + for (let strIndex = 0; strIndex < str.length; strIndex++) { + if (str.charCodeAt(strIndex) === characters.charCodeAt(characterIndex)) { + characterIndex++; + if (characterIndex === characters.length) { + return true; } } - return undefined; } - - /** - * @returns true if we are certain that the currently edited location must define a new location; false otherwise. - */ - function isSolelyIdentifierDefinitionLocation(contextToken: Node): boolean { - const parent = contextToken.parent; - const containingNodeKind = parent.kind; - switch (contextToken.kind) { + // Did not find all characters + return false; + } + /** + * Finds the first node that "embraces" the position, so that one may + * accurately aggregate locals from the closest containing scope. + */ + function getScopeNode(initialToken: Node | undefined, position: number, sourceFile: SourceFile) { + let scope: Node | undefined = initialToken; + while (scope && !positionBelongsToNode(scope, position, sourceFile)) { + scope = scope.parent; + } + return scope; + } + function isCompletionListBlocker(contextToken: Node): boolean { + const start = timestamp(); + const result = isInStringOrRegularExpressionOrTemplateLiteral(contextToken) || + isSolelyIdentifierDefinitionLocation(contextToken) || + isDotOfNumericLiteral(contextToken) || + isInJsxText(contextToken); + log("getCompletionsAtPosition: isCompletionListBlocker: " + (timestamp() - start)); + return result; + } + function isInJsxText(contextToken: Node): boolean { + if (contextToken.kind === SyntaxKind.JsxText) { + return true; + } + if (contextToken.kind === SyntaxKind.GreaterThanToken && contextToken.parent) { + if (contextToken.parent.kind === SyntaxKind.JsxOpeningElement) { + // Two possibilities: + // 1.
/**/ + // - contextToken: GreaterThanToken (before cursor) + // - location: JSXElement + // - different parents (JSXOpeningElement, JSXElement) + // 2. /**/> + // - contextToken: GreaterThanToken (before cursor) + // - location: GreaterThanToken (after cursor) + // - same parent (JSXOpeningElement) + return location.parent.kind !== SyntaxKind.JsxOpeningElement; + } + if (contextToken.parent.kind === SyntaxKind.JsxClosingElement || contextToken.parent.kind === SyntaxKind.JsxSelfClosingElement) { + return !!contextToken.parent.parent && contextToken.parent.parent.kind === SyntaxKind.JsxElement; + } + } + return false; + } + function isNewIdentifierDefinitionLocation(previousToken: Node | undefined): boolean { + if (previousToken) { + const containingNodeKind = previousToken.parent.kind; + // Previous token may have been a keyword that was converted to an identifier. + switch (keywordForNode(previousToken)) { case SyntaxKind.CommaToken: - return containingNodeKind === SyntaxKind.VariableDeclaration || - isVariableDeclarationListButNotTypeArgument(contextToken) || - containingNodeKind === SyntaxKind.VariableStatement || - containingNodeKind === SyntaxKind.EnumDeclaration || // enum a { foo, | - isFunctionLikeButNotConstructor(containingNodeKind) || - containingNodeKind === SyntaxKind.InterfaceDeclaration || // interface A= contextToken.pos); - - case SyntaxKind.DotToken: - return containingNodeKind === SyntaxKind.ArrayBindingPattern; // var [.| - - case SyntaxKind.ColonToken: - return containingNodeKind === SyntaxKind.BindingElement; // var {x :html| - - case SyntaxKind.OpenBracketToken: - return containingNodeKind === SyntaxKind.ArrayBindingPattern; // var [x| - + return containingNodeKind === SyntaxKind.CallExpression // func( a, | + || containingNodeKind === SyntaxKind.Constructor // constructor( a, | /* public, protected, private keywords are allowed here, so show completion */ + || containingNodeKind === SyntaxKind.NewExpression // new C(a, | + || containingNodeKind === SyntaxKind.ArrayLiteralExpression // [a, | + || containingNodeKind === SyntaxKind.BinaryExpression // const x = (a, | + || containingNodeKind === SyntaxKind.FunctionType; // var x: (s: string, list| case SyntaxKind.OpenParenToken: - return containingNodeKind === SyntaxKind.CatchClause || - isFunctionLikeButNotConstructor(containingNodeKind); - + return containingNodeKind === SyntaxKind.CallExpression // func( | + || containingNodeKind === SyntaxKind.Constructor // constructor( | + || containingNodeKind === SyntaxKind.NewExpression // new C(a| + || containingNodeKind === SyntaxKind.ParenthesizedExpression // const x = (a| + || containingNodeKind === SyntaxKind.ParenthesizedType; // function F(pred: (a| /* this can become an arrow function, where 'a' is the argument */ + case SyntaxKind.OpenBracketToken: + return containingNodeKind === SyntaxKind.ArrayLiteralExpression // [ | + || containingNodeKind === SyntaxKind.IndexSignature // [ | : string ] + || containingNodeKind === SyntaxKind.ComputedPropertyName; // [ | /* this can become an index signature */ + case SyntaxKind.ModuleKeyword: // module | + case SyntaxKind.NamespaceKeyword: // namespace | + return true; + case SyntaxKind.DotToken: + return containingNodeKind === SyntaxKind.ModuleDeclaration; // module A.| case SyntaxKind.OpenBraceToken: - return containingNodeKind === SyntaxKind.EnumDeclaration; // enum a { | - - case SyntaxKind.LessThanToken: - return containingNodeKind === SyntaxKind.ClassDeclaration || // class A< | - containingNodeKind === SyntaxKind.ClassExpression || // var C = class D< | - containingNodeKind === SyntaxKind.InterfaceDeclaration || // interface A< | - containingNodeKind === SyntaxKind.TypeAliasDeclaration || // type List< | - isFunctionLikeKind(containingNodeKind); - - case SyntaxKind.StaticKeyword: - return containingNodeKind === SyntaxKind.PropertyDeclaration && !isClassLike(parent.parent); - - case SyntaxKind.DotDotDotToken: - return containingNodeKind === SyntaxKind.Parameter || - (!!parent.parent && parent.parent.kind === SyntaxKind.ArrayBindingPattern); // var [...z| - + return containingNodeKind === SyntaxKind.ClassDeclaration; // class A{ | + case SyntaxKind.EqualsToken: + return containingNodeKind === SyntaxKind.VariableDeclaration // const x = a| + || containingNodeKind === SyntaxKind.BinaryExpression; // x = a| + case SyntaxKind.TemplateHead: + return containingNodeKind === SyntaxKind.TemplateExpression; // `aa ${| + case SyntaxKind.TemplateMiddle: + return containingNodeKind === SyntaxKind.TemplateSpan; // `aa ${10} dd ${| case SyntaxKind.PublicKeyword: case SyntaxKind.PrivateKeyword: case SyntaxKind.ProtectedKeyword: - return containingNodeKind === SyntaxKind.Parameter && !isConstructorDeclaration(parent.parent); - - case SyntaxKind.AsKeyword: - return containingNodeKind === SyntaxKind.ImportSpecifier || - containingNodeKind === SyntaxKind.ExportSpecifier || - containingNodeKind === SyntaxKind.NamespaceImport; - - case SyntaxKind.GetKeyword: - case SyntaxKind.SetKeyword: - return !isFromObjectTypeDeclaration(contextToken); - - case SyntaxKind.ClassKeyword: - case SyntaxKind.EnumKeyword: - case SyntaxKind.InterfaceKeyword: - case SyntaxKind.FunctionKeyword: - case SyntaxKind.VarKeyword: - case SyntaxKind.ImportKeyword: - case SyntaxKind.LetKeyword: - case SyntaxKind.ConstKeyword: - case SyntaxKind.YieldKeyword: - case SyntaxKind.TypeKeyword: // type htm| - return true; - - case SyntaxKind.AsteriskToken: - return isFunctionLike(contextToken.parent) && !isMethodDeclaration(contextToken.parent); - } - - // If the previous token is keyword correspoding to class member completion keyword - // there will be completion available here - if (isClassMemberCompletionKeyword(keywordForNode(contextToken)) && isFromObjectTypeDeclaration(contextToken)) { - return false; - } - - if (isConstructorParameterCompletion(contextToken)) { - // constructor parameter completion is available only if - // - its modifier of the constructor parameter or - // - its name of the parameter and not being edited - // eg. constructor(a |<- this shouldnt show completion - if (!isIdentifier(contextToken) || - isParameterPropertyModifier(keywordForNode(contextToken)) || - isCurrentlyEditingNode(contextToken)) { - return false; - } - } - - // Previous token may have been a keyword that was converted to an identifier. - switch (keywordForNode(contextToken)) { - case SyntaxKind.AbstractKeyword: - case SyntaxKind.ClassKeyword: - case SyntaxKind.ConstKeyword: - case SyntaxKind.DeclareKeyword: - case SyntaxKind.EnumKeyword: - case SyntaxKind.FunctionKeyword: - case SyntaxKind.InterfaceKeyword: - case SyntaxKind.LetKeyword: - case SyntaxKind.PrivateKeyword: - case SyntaxKind.ProtectedKeyword: - case SyntaxKind.PublicKeyword: - case SyntaxKind.StaticKeyword: - case SyntaxKind.VarKeyword: - case SyntaxKind.YieldKeyword: - return true; - case SyntaxKind.AsyncKeyword: - return isPropertyDeclaration(contextToken.parent); - } - - return isDeclarationName(contextToken) - && !isJsxAttribute(contextToken.parent) - // Don't block completions if we're in `class C /**/`, because we're *past* the end of the identifier and might want to complete `extends`. - // If `contextToken !== previousToken`, this is `class C ex/**/`. - && !(isClassLike(contextToken.parent) && (contextToken !== previousToken || position > previousToken.end)); - } - - function isFunctionLikeButNotConstructor(kind: SyntaxKind) { - return isFunctionLikeKind(kind) && kind !== SyntaxKind.Constructor; - } - - function isDotOfNumericLiteral(contextToken: Node): boolean { - if (contextToken.kind === SyntaxKind.NumericLiteral) { - const text = contextToken.getFullText(); - return text.charAt(text.length - 1) === "."; + return containingNodeKind === SyntaxKind.PropertyDeclaration; // class A{ public | } - - return false; } - - function isVariableDeclarationListButNotTypeArgument(node: Node): boolean { - return node.parent.kind === SyntaxKind.VariableDeclarationList - && !isPossiblyTypeArgumentPosition(node, sourceFile, typeChecker); - } - - /** - * Filters out completion suggestions for named imports or exports. - * - * @returns Symbols to be suggested in an object binding pattern or object literal expression, barring those whose declarations - * do not occur at the current position and have not otherwise been typed. - */ - function filterObjectMembersList(contextualMemberSymbols: Symbol[], existingMembers: readonly Declaration[]): Symbol[] { - if (existingMembers.length === 0) { - return contextualMemberSymbols; - } - - const membersDeclaredBySpreadAssignment = createMap(); - const existingMemberNames = createUnderscoreEscapedMap(); - for (const m of existingMembers) { - // Ignore omitted expressions for missing members - if (m.kind !== SyntaxKind.PropertyAssignment && - m.kind !== SyntaxKind.ShorthandPropertyAssignment && - m.kind !== SyntaxKind.BindingElement && - m.kind !== SyntaxKind.MethodDeclaration && - m.kind !== SyntaxKind.GetAccessor && - m.kind !== SyntaxKind.SetAccessor && - m.kind !== SyntaxKind.SpreadAssignment) { - continue; - } - - // If this is the current item we are editing right now, do not filter it out - if (isCurrentlyEditingNode(m)) { - continue; - } - - let existingName: __String | undefined; - - if (isSpreadAssignment(m)) { - setMembersDeclaredBySpreadAssignment(m, membersDeclaredBySpreadAssignment); - } - else if (isBindingElement(m) && m.propertyName) { - // include only identifiers in completion list - if (m.propertyName.kind === SyntaxKind.Identifier) { - existingName = m.propertyName.escapedText; - } + return false; + } + function isInStringOrRegularExpressionOrTemplateLiteral(contextToken: Node): boolean { + // 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'). + return (isRegularExpressionLiteral(contextToken) || isStringTextContainingNode(contextToken)) && (rangeContainsPositionExclusive(createTextRangeFromSpan(createTextSpanFromNode(contextToken)), position) || + position === contextToken.end && (!!contextToken.isUnterminated || isRegularExpressionLiteral(contextToken))); + } + /** + * Aggregates relevant symbols for completion in object literals and object binding patterns. + * Relevant symbols are stored in the captured 'symbols' variable. + * + * @returns true if 'symbols' was successfully populated; false otherwise. + */ + function tryGetObjectLikeCompletionSymbols(): GlobalsSearch | undefined { + const objectLikeContainer = tryGetObjectLikeCompletionContainer(contextToken); + if (!objectLikeContainer) + return GlobalsSearch.Continue; + // We're looking up possible property names from contextual/inferred/declared type. + completionKind = CompletionKind.ObjectPropertyDeclaration; + let typeMembers: Symbol[] | undefined; + let existingMembers: readonly Declaration[] | undefined; + if (objectLikeContainer.kind === SyntaxKind.ObjectLiteralExpression) { + const instantiatedType = typeChecker.getContextualType(objectLikeContainer); + const completionsType = instantiatedType && typeChecker.getContextualType(objectLikeContainer, ContextFlags.Completions); + if (!instantiatedType || !completionsType) + return GlobalsSearch.Fail; + isNewIdentifierLocation = hasIndexSignature(instantiatedType || completionsType); + typeMembers = getPropertiesForObjectExpression(instantiatedType, completionsType, objectLikeContainer, typeChecker); + existingMembers = objectLikeContainer.properties; + } + else { + Debug.assert(objectLikeContainer.kind === SyntaxKind.ObjectBindingPattern); + // We are *only* completing on properties from the type being destructured. + isNewIdentifierLocation = false; + const rootDeclaration = getRootDeclaration(objectLikeContainer.parent); + if (!isVariableLike(rootDeclaration)) + return Debug.fail("Root declaration is not variable-like."); + // We don't want to complete using the type acquired by the shape + // of the binding pattern; we are only interested in types acquired + // through type declaration or inference. + // Also proceed if rootDeclaration is a parameter and if its containing function expression/arrow function is contextually typed - + // type of parameter will flow in from the contextual type of the function + let canGetType = hasInitializer(rootDeclaration) || hasType(rootDeclaration) || rootDeclaration.parent.parent.kind === SyntaxKind.ForOfStatement; + if (!canGetType && rootDeclaration.kind === SyntaxKind.Parameter) { + if (isExpression(rootDeclaration.parent)) { + canGetType = !!typeChecker.getContextualType((rootDeclaration.parent)); } - else { - // TODO: Account for computed property name - // NOTE: if one only performs this step when m.name is an identifier, - // things like '__proto__' are not filtered out. - const name = getNameOfDeclaration(m); - existingName = name && isPropertyNameLiteral(name) ? getEscapedTextOfIdentifierOrLiteral(name) : undefined; + else if (rootDeclaration.parent.kind === SyntaxKind.MethodDeclaration || rootDeclaration.parent.kind === SyntaxKind.SetAccessor) { + canGetType = isExpression(rootDeclaration.parent.parent) && !!typeChecker.getContextualType((rootDeclaration.parent.parent)); } - - existingMemberNames.set(existingName!, true); // TODO: GH#18217 } - - const filteredSymbols = contextualMemberSymbols.filter(m => !existingMemberNames.get(m.escapedName)); - setSortTextToMemberDeclaredBySpreadAssignment(membersDeclaredBySpreadAssignment, filteredSymbols); - - return filteredSymbols; - } - - function setMembersDeclaredBySpreadAssignment(declaration: SpreadAssignment | JsxSpreadAttribute, membersDeclaredBySpreadAssignment: Map) { - const expression = declaration.expression; - const symbol = typeChecker.getSymbolAtLocation(expression); - const type = symbol && typeChecker.getTypeOfSymbolAtLocation(symbol, expression); - const properties = type && (type).properties; - if (properties) { - properties.forEach(property => { - membersDeclaredBySpreadAssignment.set(property.name, true); - }); - } - } - - // Set SortText to OptionalMember if it is an optinoal member - function setSortTextToOptionalMember() { - symbols.forEach(m => { - if (m.flags & SymbolFlags.Optional) { - symbolToSortTextMap[getSymbolId(m)] = symbolToSortTextMap[getSymbolId(m)] || SortText.OptionalMember; - } + if (canGetType) { + const typeForObject = typeChecker.getTypeAtLocation(objectLikeContainer); + if (!typeForObject) + return GlobalsSearch.Fail; + // In a binding pattern, get only known properties (unless in the same scope). + // Everywhere else we will get all possible properties. + const containerClass = getContainingClass(objectLikeContainer); + typeMembers = typeChecker.getPropertiesOfType(typeForObject).filter(symbol => + // either public + !(getDeclarationModifierFlagsFromSymbol(symbol) & ModifierFlags.NonPublicAccessibilityModifier) + // or we're in it + || containerClass && contains(typeForObject.symbol.declarations, containerClass)); + existingMembers = objectLikeContainer.elements; + } + } + if (typeMembers && typeMembers.length > 0) { + // Add filtered items to the completion list + symbols = filterObjectMembersList(typeMembers, Debug.checkDefined(existingMembers)); + } + setSortTextToOptionalMember(); + return GlobalsSearch.Success; + } + /** + * Aggregates relevant symbols for completion in import clauses and export clauses + * whose declarations have a module specifier; for instance, symbols will be aggregated for + * + * import { | } from "moduleName"; + * export { a as foo, | } from "moduleName"; + * + * but not for + * + * export { | }; + * + * Relevant symbols are stored in the captured 'symbols' variable. + * + * @returns true if 'symbols' was successfully populated; false otherwise. + */ + function tryGetImportOrExportClauseCompletionSymbols(): GlobalsSearch { + // `import { |` or `import { a as 0, | }` + const namedImportsOrExports = contextToken && (contextToken.kind === SyntaxKind.OpenBraceToken || contextToken.kind === SyntaxKind.CommaToken) + ? tryCast(contextToken.parent, isNamedImportsOrExports) : undefined; + if (!namedImportsOrExports) + return GlobalsSearch.Continue; + // cursor is in an import clause + // try to show exported member for imported module + const { moduleSpecifier } = namedImportsOrExports.kind === SyntaxKind.NamedImports ? namedImportsOrExports.parent.parent : namedImportsOrExports.parent; + const moduleSpecifierSymbol = typeChecker.getSymbolAtLocation(moduleSpecifier!); // TODO: GH#18217 + if (!moduleSpecifierSymbol) + return GlobalsSearch.Fail; + completionKind = CompletionKind.MemberLike; + isNewIdentifierLocation = false; + const exports = typeChecker.getExportsAndPropertiesOfModule(moduleSpecifierSymbol); + const existing = arrayToSet(namedImportsOrExports.elements, n => isCurrentlyEditingNode(n) ? undefined : (n.propertyName || n.name).escapedText); + symbols = exports.filter(e => e.escapedName !== InternalSymbolName.Default && !existing.get(e.escapedName)); + return GlobalsSearch.Success; + } + /** + * Aggregates relevant symbols for completion in class declaration + * Relevant symbols are stored in the captured 'symbols' variable. + */ + function tryGetClassLikeCompletionSymbols(): GlobalsSearch { + const decl = tryGetObjectTypeDeclarationCompletionContainer(sourceFile, contextToken, location, position); + if (!decl) + return GlobalsSearch.Continue; + // We're looking up possible property names from parent type. + completionKind = CompletionKind.MemberLike; + // Declaring new property/method/accessor + isNewIdentifierLocation = true; + keywordFilters = contextToken.kind === SyntaxKind.AsteriskToken ? KeywordCompletionFilters.None : + isClassLike(decl) ? KeywordCompletionFilters.ClassElementKeywords : KeywordCompletionFilters.InterfaceElementKeywords; + // If you're in an interface you don't want to repeat things from super-interface. So just stop here. + if (!isClassLike(decl)) + return GlobalsSearch.Success; + const classElement = contextToken.kind === SyntaxKind.SemicolonToken ? contextToken.parent.parent : contextToken.parent; + let classElementModifierFlags = isClassElement(classElement) ? getModifierFlags(classElement) : ModifierFlags.None; + // If this is context token is not something we are editing now, consider if this would lead to be modifier + if (contextToken.kind === SyntaxKind.Identifier && !isCurrentlyEditingNode(contextToken)) { + switch (contextToken.getText()) { + case "private": + classElementModifierFlags = classElementModifierFlags | ModifierFlags.Private; + break; + case "static": + classElementModifierFlags = classElementModifierFlags | ModifierFlags.Static; + break; + } + } + // No member list for private methods + if (!(classElementModifierFlags & ModifierFlags.Private)) { + // List of property symbols of base type that are not private and already implemented + const baseSymbols = flatMap(getAllSuperTypeNodes(decl), baseTypeNode => { + const type = typeChecker.getTypeAtLocation(baseTypeNode); + return type && typeChecker.getPropertiesOfType(classElementModifierFlags & ModifierFlags.Static ? typeChecker.getTypeOfSymbolAtLocation(type.symbol, decl) : type); }); + symbols = filterClassMembersList(baseSymbols, decl.members, classElementModifierFlags); } - - // Set SortText to MemberDeclaredBySpreadAssignment if it is fulfilled by spread assignment - function setSortTextToMemberDeclaredBySpreadAssignment(membersDeclaredBySpreadAssignment: Map, contextualMemberSymbols: Symbol[]): void { - if (membersDeclaredBySpreadAssignment.size === 0) { - return; - } - for (const contextualMemberSymbol of contextualMemberSymbols) { - if (membersDeclaredBySpreadAssignment.has(contextualMemberSymbol.name)) { - symbolToSortTextMap[getSymbolId(contextualMemberSymbol)] = SortText.MemberDeclaredBySpreadAssignment; - } + return GlobalsSearch.Success; + } + /** + * Returns the immediate owning object literal or binding pattern of a context token, + * on the condition that one exists and that the context implies completion should be given. + */ + function tryGetObjectLikeCompletionContainer(contextToken: Node): ObjectLiteralExpression | ObjectBindingPattern | undefined { + if (contextToken) { + const { parent } = contextToken; + switch (contextToken.kind) { + case SyntaxKind.OpenBraceToken: // const x = { | + case SyntaxKind.CommaToken: // const x = { a: 0, | + if (isObjectLiteralExpression(parent) || isObjectBindingPattern(parent)) { + return parent; + } + break; + case SyntaxKind.AsteriskToken: + return isMethodDeclaration(parent) ? tryCast(parent.parent, isObjectLiteralExpression) : undefined; + case SyntaxKind.Identifier: + return (contextToken as Identifier).text === "async" && isShorthandPropertyAssignment(contextToken.parent) + ? contextToken.parent.parent : undefined; } } - - /** - * Filters out completion suggestions for class elements. - * - * @returns Symbols to be suggested in an class element depending on existing memebers and symbol flags - */ - function filterClassMembersList(baseSymbols: readonly Symbol[], existingMembers: readonly ClassElement[], currentClassElementModifierFlags: ModifierFlags): Symbol[] { - const existingMemberNames = createUnderscoreEscapedMap(); - for (const m of existingMembers) { - // Ignore omitted expressions for missing members - if (m.kind !== SyntaxKind.PropertyDeclaration && - m.kind !== SyntaxKind.MethodDeclaration && - m.kind !== SyntaxKind.GetAccessor && - m.kind !== SyntaxKind.SetAccessor) { - continue; - } - - // If this is the current item we are editing right now, do not filter it out - if (isCurrentlyEditingNode(m)) { - continue; - } - - // Dont filter member even if the name matches if it is declared private in the list - if (hasModifier(m, ModifierFlags.Private)) { - continue; - } - - // do not filter it out if the static presence doesnt match - if (hasModifier(m, ModifierFlags.Static) !== !!(currentClassElementModifierFlags & ModifierFlags.Static)) { - continue; - } - - const existingName = getPropertyNameForPropertyNameNode(m.name!); - if (existingName) { - existingMemberNames.set(existingName, true); - } + return undefined; + } + function isConstructorParameterCompletion(node: Node): boolean { + return !!node.parent && isParameter(node.parent) && isConstructorDeclaration(node.parent.parent) + && (isParameterPropertyModifier(node.kind) || isDeclarationName(node)); + } + /** + * Returns the immediate owning class declaration of a context token, + * on the condition that one exists and that the context implies completion should be given. + */ + function tryGetConstructorLikeCompletionContainer(contextToken: Node): ConstructorDeclaration | undefined { + if (contextToken) { + const parent = contextToken.parent; + switch (contextToken.kind) { + case SyntaxKind.OpenParenToken: + case SyntaxKind.CommaToken: + return isConstructorDeclaration(contextToken.parent) ? contextToken.parent : undefined; + default: + if (isConstructorParameterCompletion(contextToken)) { + return parent.parent as ConstructorDeclaration; + } } - - return baseSymbols.filter(propertySymbol => - !existingMemberNames.has(propertySymbol.escapedName) && - !!propertySymbol.declarations && - !(getDeclarationModifierFlagsFromSymbol(propertySymbol) & ModifierFlags.Private)); - } - - /** - * Filters out completion suggestions from 'symbols' according to existing JSX attributes. - * - * @returns Symbols to be suggested in a JSX element, barring those whose attributes - * do not occur at the current position and have not otherwise been typed. - */ - function filterJsxAttributes(symbols: Symbol[], attributes: NodeArray): Symbol[] { - const seenNames = createUnderscoreEscapedMap(); - const membersDeclaredBySpreadAssignment = createMap(); - for (const attr of attributes) { - // If this is the current item we are editing right now, do not filter it out - if (isCurrentlyEditingNode(attr)) { - continue; - } - - if (attr.kind === SyntaxKind.JsxAttribute) { - seenNames.set(attr.name.escapedText, true); + } + return undefined; + } + function tryGetFunctionLikeBodyCompletionContainer(contextToken: Node): FunctionLikeDeclaration | undefined { + if (contextToken) { + let prev: Node; + const container = findAncestor(contextToken.parent, (node: Node) => { + if (isClassLike(node)) { + return "quit"; } - else if (isJsxSpreadAttribute(attr)) { - setMembersDeclaredBySpreadAssignment(attr, membersDeclaredBySpreadAssignment); + if (isFunctionLikeDeclaration(node) && prev === node.body) { + return true; } + prev = node; + return false; + }); + return container && (container as FunctionLikeDeclaration); + } + } + function tryGetContainingJsxElement(contextToken: Node): JsxOpeningLikeElement | undefined { + if (contextToken) { + const parent = contextToken.parent; + switch (contextToken.kind) { + case SyntaxKind.GreaterThanToken: // End of a type argument list + case SyntaxKind.LessThanSlashToken: + case SyntaxKind.SlashToken: + case SyntaxKind.Identifier: + case SyntaxKind.PropertyAccessExpression: + case SyntaxKind.JsxAttributes: + case SyntaxKind.JsxAttribute: + case SyntaxKind.JsxSpreadAttribute: + if (parent && (parent.kind === SyntaxKind.JsxSelfClosingElement || parent.kind === SyntaxKind.JsxOpeningElement)) { + if (contextToken.kind === SyntaxKind.GreaterThanToken) { + const precedingToken = findPrecedingToken(contextToken.pos, sourceFile, /*startNode*/ undefined); + if (!(parent as JsxOpeningLikeElement).typeArguments || (precedingToken && precedingToken.kind === SyntaxKind.SlashToken)) + break; + } + return parent; + } + else if (parent.kind === SyntaxKind.JsxAttribute) { + // Currently we parse JsxOpeningLikeElement as: + // JsxOpeningLikeElement + // attributes: JsxAttributes + // properties: NodeArray + return parent.parent.parent as JsxOpeningLikeElement; + } + break; + // The context token is the closing } or " of an attribute, which means + // its parent is a JsxExpression, whose parent is a JsxAttribute, + // whose parent is a JsxOpeningLikeElement + case SyntaxKind.StringLiteral: + if (parent && ((parent.kind === SyntaxKind.JsxAttribute) || (parent.kind === SyntaxKind.JsxSpreadAttribute))) { + // Currently we parse JsxOpeningLikeElement as: + // JsxOpeningLikeElement + // attributes: JsxAttributes + // properties: NodeArray + return parent.parent.parent as JsxOpeningLikeElement; + } + break; + case SyntaxKind.CloseBraceToken: + if (parent && + parent.kind === SyntaxKind.JsxExpression && + parent.parent && parent.parent.kind === SyntaxKind.JsxAttribute) { + // Currently we parse JsxOpeningLikeElement as: + // JsxOpeningLikeElement + // attributes: JsxAttributes + // properties: NodeArray + // each JsxAttribute can have initializer as JsxExpression + return parent.parent.parent.parent as JsxOpeningLikeElement; + } + if (parent && parent.kind === SyntaxKind.JsxSpreadAttribute) { + // Currently we parse JsxOpeningLikeElement as: + // JsxOpeningLikeElement + // attributes: JsxAttributes + // properties: NodeArray + return parent.parent.parent as JsxOpeningLikeElement; + } + break; } - const filteredSymbols = symbols.filter(a => !seenNames.get(a.escapedName)); - - setSortTextToMemberDeclaredBySpreadAssignment(membersDeclaredBySpreadAssignment, filteredSymbols); - - return filteredSymbols; - } - - function isCurrentlyEditingNode(node: Node): boolean { - return node.getStart(sourceFile) <= position && position <= node.getEnd(); - } - } - - interface CompletionEntryDisplayNameForSymbol { - readonly name: string; - readonly needsConvertPropertyAccess: boolean; - } - function getCompletionEntryDisplayNameForSymbol( - symbol: Symbol, - target: ScriptTarget, - origin: SymbolOriginInfo | undefined, - kind: CompletionKind, - ): CompletionEntryDisplayNameForSymbol | undefined { - const name = originIsExport(origin) ? getNameForExportedSymbol(symbol, target) : symbol.name; - if (name === undefined - // If the symbol is external module, don't show it in the completion list - // (i.e declare module "http" { const x; } | // <= request completion here, "http" should not be there) - || symbol.flags & SymbolFlags.Module && isSingleOrDoubleQuote(name.charCodeAt(0)) - // If the symbol is the internal name of an ES symbol, it is not a valid entry. Internal names for ES symbols start with "__@" - || isKnownSymbol(symbol)) { - return undefined; } - - const validNameResult: CompletionEntryDisplayNameForSymbol = { name, needsConvertPropertyAccess: false }; - if (isIdentifierText(name, target) || symbol.valueDeclaration && isPrivateIdentifierPropertyDeclaration(symbol.valueDeclaration)) { - return validNameResult; + return undefined; + } + /** + * @returns true if we are certain that the currently edited location must define a new location; false otherwise. + */ + function isSolelyIdentifierDefinitionLocation(contextToken: Node): boolean { + const parent = contextToken.parent; + const containingNodeKind = parent.kind; + switch (contextToken.kind) { + case SyntaxKind.CommaToken: + return containingNodeKind === SyntaxKind.VariableDeclaration || + isVariableDeclarationListButNotTypeArgument(contextToken) || + containingNodeKind === SyntaxKind.VariableStatement || + containingNodeKind === SyntaxKind.EnumDeclaration || // enum a { foo, | + isFunctionLikeButNotConstructor(containingNodeKind) || + containingNodeKind === SyntaxKind.InterfaceDeclaration || // interface A= contextToken.pos); + case SyntaxKind.DotToken: + return containingNodeKind === SyntaxKind.ArrayBindingPattern; // var [.| + case SyntaxKind.ColonToken: + return containingNodeKind === SyntaxKind.BindingElement; // var {x :html| + case SyntaxKind.OpenBracketToken: + return containingNodeKind === SyntaxKind.ArrayBindingPattern; // var [x| + case SyntaxKind.OpenParenToken: + return containingNodeKind === SyntaxKind.CatchClause || + isFunctionLikeButNotConstructor(containingNodeKind); + case SyntaxKind.OpenBraceToken: + return containingNodeKind === SyntaxKind.EnumDeclaration; // enum a { | + case SyntaxKind.LessThanToken: + return containingNodeKind === SyntaxKind.ClassDeclaration || // class A< | + containingNodeKind === SyntaxKind.ClassExpression || // var C = class D< | + containingNodeKind === SyntaxKind.InterfaceDeclaration || // interface A< | + containingNodeKind === SyntaxKind.TypeAliasDeclaration || // type List< | + isFunctionLikeKind(containingNodeKind); + case SyntaxKind.StaticKeyword: + return containingNodeKind === SyntaxKind.PropertyDeclaration && !isClassLike(parent.parent); + case SyntaxKind.DotDotDotToken: + return containingNodeKind === SyntaxKind.Parameter || + (!!parent.parent && parent.parent.kind === SyntaxKind.ArrayBindingPattern); // var [...z| + case SyntaxKind.PublicKeyword: + case SyntaxKind.PrivateKeyword: + case SyntaxKind.ProtectedKeyword: + return containingNodeKind === SyntaxKind.Parameter && !isConstructorDeclaration(parent.parent); + case SyntaxKind.AsKeyword: + return containingNodeKind === SyntaxKind.ImportSpecifier || + containingNodeKind === SyntaxKind.ExportSpecifier || + containingNodeKind === SyntaxKind.NamespaceImport; + case SyntaxKind.GetKeyword: + case SyntaxKind.SetKeyword: + return !isFromObjectTypeDeclaration(contextToken); + case SyntaxKind.ClassKeyword: + case SyntaxKind.EnumKeyword: + case SyntaxKind.InterfaceKeyword: + case SyntaxKind.FunctionKeyword: + case SyntaxKind.VarKeyword: + case SyntaxKind.ImportKeyword: + case SyntaxKind.LetKeyword: + case SyntaxKind.ConstKeyword: + case SyntaxKind.YieldKeyword: + case SyntaxKind.TypeKeyword: // type htm| + return true; + case SyntaxKind.AsteriskToken: + return isFunctionLike(contextToken.parent) && !isMethodDeclaration(contextToken.parent); } - switch (kind) { - case CompletionKind.MemberLike: - return undefined; - case CompletionKind.ObjectPropertyDeclaration: - // TODO: GH#18169 - return { name: JSON.stringify(name), needsConvertPropertyAccess: false }; - case CompletionKind.PropertyAccess: - case CompletionKind.Global: // For a 'this.' completion it will be in a global context, but may have a non-identifier name. - // Don't add a completion for a name starting with a space. See https://github.com/Microsoft/TypeScript/pull/20547 - return name.charCodeAt(0) === CharacterCodes.space ? undefined : { name, needsConvertPropertyAccess: true }; - case CompletionKind.None: - case CompletionKind.String: - return validNameResult; - default: - Debug.assertNever(kind); - } - } - - // A cache of completion entries for keywords, these do not change between sessions - const _keywordCompletions: CompletionEntry[][] = []; - const allKeywordsCompletions: () => readonly CompletionEntry[] = memoize(() => { - const res: CompletionEntry[] = []; - for (let i = SyntaxKind.FirstKeyword; i <= SyntaxKind.LastKeyword; i++) { - res.push({ - name: tokenToString(i)!, - kind: ScriptElementKind.keyword, - kindModifiers: ScriptElementKindModifier.none, - sortText: SortText.GlobalsOrKeywords - }); + // If the previous token is keyword correspoding to class member completion keyword + // there will be completion available here + if (isClassMemberCompletionKeyword(keywordForNode(contextToken)) && isFromObjectTypeDeclaration(contextToken)) { + return false; } - return res; - }); - - function getKeywordCompletions(keywordFilter: KeywordCompletionFilters, filterOutTsOnlyKeywords: boolean): readonly CompletionEntry[] { - if (!filterOutTsOnlyKeywords) return getTypescriptKeywordCompletions(keywordFilter); - - const index = keywordFilter + KeywordCompletionFilters.Last + 1; - return _keywordCompletions[index] || - (_keywordCompletions[index] = getTypescriptKeywordCompletions(keywordFilter) - .filter(entry => !isTypeScriptOnlyKeyword(stringToToken(entry.name)!)) - ); - } - - function getTypescriptKeywordCompletions(keywordFilter: KeywordCompletionFilters): readonly CompletionEntry[] { - return _keywordCompletions[keywordFilter] || (_keywordCompletions[keywordFilter] = allKeywordsCompletions().filter(entry => { - const kind = stringToToken(entry.name)!; - switch (keywordFilter) { - case KeywordCompletionFilters.None: - return false; - case KeywordCompletionFilters.All: - return isFunctionLikeBodyKeyword(kind) - || kind === SyntaxKind.DeclareKeyword - || kind === SyntaxKind.ModuleKeyword - || kind === SyntaxKind.TypeKeyword - || kind === SyntaxKind.NamespaceKeyword - || kind === SyntaxKind.AsKeyword - || isTypeKeyword(kind) && kind !== SyntaxKind.UndefinedKeyword; - case KeywordCompletionFilters.FunctionLikeBodyKeywords: - return isFunctionLikeBodyKeyword(kind); - case KeywordCompletionFilters.ClassElementKeywords: - return isClassMemberCompletionKeyword(kind); - case KeywordCompletionFilters.InterfaceElementKeywords: - return isInterfaceOrTypeLiteralCompletionKeyword(kind); - case KeywordCompletionFilters.ConstructorParameterKeywords: - return isParameterPropertyModifier(kind); - case KeywordCompletionFilters.TypeAssertionKeywords: - return isTypeKeyword(kind) || kind === SyntaxKind.ConstKeyword; - case KeywordCompletionFilters.TypeKeywords: - return isTypeKeyword(kind); - default: - return Debug.assertNever(keywordFilter); + if (isConstructorParameterCompletion(contextToken)) { + // constructor parameter completion is available only if + // - its modifier of the constructor parameter or + // - its name of the parameter and not being edited + // eg. constructor(a |<- this shouldnt show completion + if (!isIdentifier(contextToken) || + isParameterPropertyModifier(keywordForNode(contextToken)) || + isCurrentlyEditingNode(contextToken)) { + return false; } - })); - } - - function isTypeScriptOnlyKeyword(kind: SyntaxKind) { - switch (kind) { + } + // Previous token may have been a keyword that was converted to an identifier. + switch (keywordForNode(contextToken)) { case SyntaxKind.AbstractKeyword: - case SyntaxKind.AnyKeyword: - case SyntaxKind.BigIntKeyword: - case SyntaxKind.BooleanKeyword: + case SyntaxKind.ClassKeyword: + case SyntaxKind.ConstKeyword: case SyntaxKind.DeclareKeyword: case SyntaxKind.EnumKeyword: - case SyntaxKind.GlobalKeyword: - case SyntaxKind.ImplementsKeyword: - case SyntaxKind.InferKeyword: + case SyntaxKind.FunctionKeyword: case SyntaxKind.InterfaceKeyword: - case SyntaxKind.IsKeyword: - case SyntaxKind.KeyOfKeyword: - case SyntaxKind.ModuleKeyword: - case SyntaxKind.NamespaceKeyword: - case SyntaxKind.NeverKeyword: - case SyntaxKind.NumberKeyword: - case SyntaxKind.ObjectKeyword: + case SyntaxKind.LetKeyword: case SyntaxKind.PrivateKeyword: case SyntaxKind.ProtectedKeyword: case SyntaxKind.PublicKeyword: - case SyntaxKind.ReadonlyKeyword: - case SyntaxKind.StringKeyword: - case SyntaxKind.SymbolKeyword: - case SyntaxKind.TypeKeyword: - case SyntaxKind.UniqueKeyword: - case SyntaxKind.UnknownKeyword: + case SyntaxKind.StaticKeyword: + case SyntaxKind.VarKeyword: + case SyntaxKind.YieldKeyword: return true; - default: - return false; + case SyntaxKind.AsyncKeyword: + return isPropertyDeclaration(contextToken.parent); } + return isDeclarationName(contextToken) + && !isJsxAttribute(contextToken.parent) + // Don't block completions if we're in `class C /**/`, because we're *past* the end of the identifier and might want to complete `extends`. + // If `contextToken !== previousToken`, this is `class C ex/**/`. + && !(isClassLike(contextToken.parent) && (contextToken !== previousToken || position > previousToken.end)); } - - function isInterfaceOrTypeLiteralCompletionKeyword(kind: SyntaxKind): boolean { - return kind === SyntaxKind.ReadonlyKeyword; + function isFunctionLikeButNotConstructor(kind: SyntaxKind) { + return isFunctionLikeKind(kind) && kind !== SyntaxKind.Constructor; } - - function isClassMemberCompletionKeyword(kind: SyntaxKind) { - switch (kind) { - case SyntaxKind.AbstractKeyword: - case SyntaxKind.ConstructorKeyword: - case SyntaxKind.GetKeyword: - case SyntaxKind.SetKeyword: - case SyntaxKind.AsyncKeyword: - case SyntaxKind.DeclareKeyword: - return true; - default: - return isClassMemberModifier(kind); - } - } - - function isFunctionLikeBodyKeyword(kind: SyntaxKind) { - return kind === SyntaxKind.AsyncKeyword - || kind === SyntaxKind.AwaitKeyword - || !isContextualKeyword(kind) && !isClassMemberCompletionKeyword(kind); - } - - function keywordForNode(node: Node): SyntaxKind { - return isIdentifier(node) ? node.originalKeywordKind || SyntaxKind.Unknown : node.kind; - } - - /** Get the corresponding JSDocTag node if the position is in a jsDoc comment */ - function getJsDocTagAtPosition(node: Node, position: number): JSDocTag | undefined { - const jsdoc = findAncestor(node, isJSDoc); - return jsdoc && jsdoc.tags && (rangeContainsPosition(jsdoc, position) ? findLast(jsdoc.tags, tag => tag.pos < position) : undefined); - } - - function getPropertiesForObjectExpression(contextualType: Type, completionsType: Type | undefined, obj: ObjectLiteralExpression | JsxAttributes, checker: TypeChecker): Symbol[] { - const hasCompletionsType = completionsType && completionsType !== contextualType; - const type = hasCompletionsType && !(completionsType!.flags & TypeFlags.AnyOrUnknown) - ? checker.getUnionType([contextualType, completionsType!]) - : contextualType; - - const properties = type.isUnion() - ? checker.getAllPossiblePropertiesOfTypes(type.types.filter(memberType => - // If we're providing completions for an object literal, skip primitive, array-like, or callable types since those shouldn't be implemented by object literals. - !(memberType.flags & TypeFlags.Primitive || - checker.isArrayLikeType(memberType) || - typeHasCallOrConstructSignatures(memberType, checker) || - checker.isTypeInvalidDueToUnionDiscriminant(memberType, obj)))) - : type.getApparentProperties(); - - return hasCompletionsType ? properties.filter(hasDeclarationOtherThanSelf) : properties; - - // Filter out members whose only declaration is the object literal itself to avoid - // self-fulfilling completions like: - // - // function f(x: T) {} - // f({ abc/**/: "" }) // `abc` is a member of `T` but only because it declares itself - function hasDeclarationOtherThanSelf(member: Symbol) { - return some(member.declarations, decl => decl.parent !== obj); + function isDotOfNumericLiteral(contextToken: Node): boolean { + if (contextToken.kind === SyntaxKind.NumericLiteral) { + const text = contextToken.getFullText(); + return text.charAt(text.length - 1) === "."; } + return false; + } + function isVariableDeclarationListButNotTypeArgument(node: Node): boolean { + return node.parent.kind === SyntaxKind.VariableDeclarationList + && !isPossiblyTypeArgumentPosition(node, sourceFile, typeChecker); } - /** - * Gets all properties on a type, but if that type is a union of several types, - * excludes array-like types or callable/constructable types. + * Filters out completion suggestions for named imports or exports. + * + * @returns Symbols to be suggested in an object binding pattern or object literal expression, barring those whose declarations + * do not occur at the current position and have not otherwise been typed. */ - function getPropertiesForCompletion(type: Type, checker: TypeChecker): Symbol[] { - return type.isUnion() - ? Debug.checkEachDefined(checker.getAllPossiblePropertiesOfTypes(type.types), "getAllPossiblePropertiesOfTypes() should all be defined") - : Debug.checkEachDefined(type.getApparentProperties(), "getApparentProperties() should all be defined"); + function filterObjectMembersList(contextualMemberSymbols: Symbol[], existingMembers: readonly Declaration[]): Symbol[] { + if (existingMembers.length === 0) { + return contextualMemberSymbols; + } + const membersDeclaredBySpreadAssignment = createMap(); + const existingMemberNames = createUnderscoreEscapedMap(); + for (const m of existingMembers) { + // Ignore omitted expressions for missing members + if (m.kind !== SyntaxKind.PropertyAssignment && + m.kind !== SyntaxKind.ShorthandPropertyAssignment && + m.kind !== SyntaxKind.BindingElement && + m.kind !== SyntaxKind.MethodDeclaration && + m.kind !== SyntaxKind.GetAccessor && + m.kind !== SyntaxKind.SetAccessor && + m.kind !== SyntaxKind.SpreadAssignment) { + continue; + } + // If this is the current item we are editing right now, do not filter it out + if (isCurrentlyEditingNode(m)) { + continue; + } + let existingName: __String | undefined; + if (isSpreadAssignment(m)) { + setMembersDeclaredBySpreadAssignment(m, membersDeclaredBySpreadAssignment); + } + else if (isBindingElement(m) && m.propertyName) { + // include only identifiers in completion list + if (m.propertyName.kind === SyntaxKind.Identifier) { + existingName = m.propertyName.escapedText; + } + } + else { + // TODO: Account for computed property name + // NOTE: if one only performs this step when m.name is an identifier, + // things like '__proto__' are not filtered out. + const name = getNameOfDeclaration(m); + existingName = name && isPropertyNameLiteral(name) ? getEscapedTextOfIdentifierOrLiteral(name) : undefined; + } + existingMemberNames.set(existingName!, true); // TODO: GH#18217 + } + const filteredSymbols = contextualMemberSymbols.filter(m => !existingMemberNames.get(m.escapedName)); + setSortTextToMemberDeclaredBySpreadAssignment(membersDeclaredBySpreadAssignment, filteredSymbols); + return filteredSymbols; + } + function setMembersDeclaredBySpreadAssignment(declaration: SpreadAssignment | JsxSpreadAttribute, membersDeclaredBySpreadAssignment: ts.Map) { + const expression = declaration.expression; + const symbol = typeChecker.getSymbolAtLocation(expression); + const type = symbol && typeChecker.getTypeOfSymbolAtLocation(symbol, expression); + const properties = type && (type).properties; + if (properties) { + properties.forEach(property => { + membersDeclaredBySpreadAssignment.set(property.name, true); + }); + } + } + // Set SortText to OptionalMember if it is an optinoal member + function setSortTextToOptionalMember() { + symbols.forEach(m => { + if (m.flags & SymbolFlags.Optional) { + symbolToSortTextMap[getSymbolId(m)] = symbolToSortTextMap[getSymbolId(m)] || SortText.OptionalMember; + } + }); + } + // Set SortText to MemberDeclaredBySpreadAssignment if it is fulfilled by spread assignment + function setSortTextToMemberDeclaredBySpreadAssignment(membersDeclaredBySpreadAssignment: ts.Map, contextualMemberSymbols: Symbol[]): void { + if (membersDeclaredBySpreadAssignment.size === 0) { + return; + } + for (const contextualMemberSymbol of contextualMemberSymbols) { + if (membersDeclaredBySpreadAssignment.has(contextualMemberSymbol.name)) { + symbolToSortTextMap[getSymbolId(contextualMemberSymbol)] = SortText.MemberDeclaredBySpreadAssignment; + } + } } - /** - * Returns the immediate owning class declaration of a context token, - * on the condition that one exists and that the context implies completion should be given. + * Filters out completion suggestions for class elements. + * + * @returns Symbols to be suggested in an class element depending on existing memebers and symbol flags */ - function tryGetObjectTypeDeclarationCompletionContainer(sourceFile: SourceFile, contextToken: Node | undefined, location: Node, position: number): ObjectTypeDeclaration | undefined { - // class c { method() { } | method2() { } } - switch (location.kind) { - case SyntaxKind.SyntaxList: - return tryCast(location.parent, isObjectTypeDeclaration); - case SyntaxKind.EndOfFileToken: - const cls = tryCast(lastOrUndefined(cast(location.parent, isSourceFile).statements), isObjectTypeDeclaration); - if (cls && !findChildOfKind(cls, SyntaxKind.CloseBraceToken, sourceFile)) { - return cls; - } - break; - case SyntaxKind.Identifier: // class c extends React.Component { a: () => 1\n compon| } - if (isFromObjectTypeDeclaration(location)) { - return findAncestor(location, isObjectTypeDeclaration); - } + function filterClassMembersList(baseSymbols: readonly Symbol[], existingMembers: readonly ClassElement[], currentClassElementModifierFlags: ModifierFlags): Symbol[] { + const existingMemberNames = createUnderscoreEscapedMap(); + for (const m of existingMembers) { + // Ignore omitted expressions for missing members + if (m.kind !== SyntaxKind.PropertyDeclaration && + m.kind !== SyntaxKind.MethodDeclaration && + m.kind !== SyntaxKind.GetAccessor && + m.kind !== SyntaxKind.SetAccessor) { + continue; + } + // If this is the current item we are editing right now, do not filter it out + if (isCurrentlyEditingNode(m)) { + continue; + } + // Dont filter member even if the name matches if it is declared private in the list + if (hasModifier(m, ModifierFlags.Private)) { + continue; + } + // do not filter it out if the static presence doesnt match + if (hasModifier(m, ModifierFlags.Static) !== !!(currentClassElementModifierFlags & ModifierFlags.Static)) { + continue; + } + const existingName = getPropertyNameForPropertyNameNode((m.name!)); + if (existingName) { + existingMemberNames.set(existingName, true); + } } - - if (!contextToken) return undefined; - - switch (contextToken.kind) { - case SyntaxKind.EqualsToken: // class c { public prop = | /* global completions */ } - return undefined; - - case SyntaxKind.SemicolonToken: // class c {getValue(): number; | } - case SyntaxKind.CloseBraceToken: // class c { method() { } | } - // class c { method() { } b| } - return isFromObjectTypeDeclaration(location) && (location.parent as ClassElement | TypeElement).name === location - ? location.parent.parent as ObjectTypeDeclaration - : tryCast(location, isObjectTypeDeclaration); - case SyntaxKind.OpenBraceToken: // class c { | - case SyntaxKind.CommaToken: // class c {getValue(): number, | } - return tryCast(contextToken.parent, isObjectTypeDeclaration); - default: - if (!isFromObjectTypeDeclaration(contextToken)) { - // class c extends React.Component { a: () => 1\n| } - if (getLineAndCharacterOfPosition(sourceFile, contextToken.getEnd()).line !== getLineAndCharacterOfPosition(sourceFile, position).line && isObjectTypeDeclaration(location)) { - return location; - } - return undefined; - } - const isValidKeyword = isClassLike(contextToken.parent.parent) ? isClassMemberCompletionKeyword : isInterfaceOrTypeLiteralCompletionKeyword; - return (isValidKeyword(contextToken.kind) || contextToken.kind === SyntaxKind.AsteriskToken || isIdentifier(contextToken) && isValidKeyword(stringToToken(contextToken.text)!)) // TODO: GH#18217 - ? contextToken.parent.parent as ObjectTypeDeclaration : undefined; + return baseSymbols.filter(propertySymbol => !existingMemberNames.has(propertySymbol.escapedName) && + !!propertySymbol.declarations && + !(getDeclarationModifierFlagsFromSymbol(propertySymbol) & ModifierFlags.Private)); + } + /** + * Filters out completion suggestions from 'symbols' according to existing JSX attributes. + * + * @returns Symbols to be suggested in a JSX element, barring those whose attributes + * do not occur at the current position and have not otherwise been typed. + */ + function filterJsxAttributes(symbols: Symbol[], attributes: NodeArray): Symbol[] { + const seenNames = createUnderscoreEscapedMap(); + const membersDeclaredBySpreadAssignment = createMap(); + for (const attr of attributes) { + // If this is the current item we are editing right now, do not filter it out + if (isCurrentlyEditingNode(attr)) { + continue; + } + if (attr.kind === SyntaxKind.JsxAttribute) { + seenNames.set(attr.name.escapedText, true); + } + else if (isJsxSpreadAttribute(attr)) { + setMembersDeclaredBySpreadAssignment(attr, membersDeclaredBySpreadAssignment); + } } + const filteredSymbols = symbols.filter(a => !seenNames.get(a.escapedName)); + setSortTextToMemberDeclaredBySpreadAssignment(membersDeclaredBySpreadAssignment, filteredSymbols); + return filteredSymbols; } - - // TODO: GH#19856 Would like to return `node is Node & { parent: (ClassElement | TypeElement) & { parent: ObjectTypeDeclaration } }` but then compilation takes > 10 minutes - function isFromObjectTypeDeclaration(node: Node): boolean { - return node.parent && isClassOrTypeElement(node.parent) && isObjectTypeDeclaration(node.parent.parent); + function isCurrentlyEditingNode(node: Node): boolean { + return node.getStart(sourceFile) <= position && position <= node.getEnd(); } - - function isValidTrigger(sourceFile: SourceFile, triggerCharacter: CompletionsTriggerCharacter, contextToken: Node | undefined, position: number): boolean { - switch (triggerCharacter) { - case ".": - case "@": - return true; - case '"': - case "'": - case "`": - // Only automatically bring up completions if this is an opening quote. - return !!contextToken && isStringLiteralOrTemplate(contextToken) && position === contextToken.getStart(sourceFile) + 1; - case "#": - return !!contextToken && isPrivateIdentifier(contextToken) && !!getContainingClass(contextToken); - case "<": - // Opening JSX tag - return !!contextToken && contextToken.kind === SyntaxKind.LessThanToken && (!isBinaryExpression(contextToken.parent) || binaryExpressionMayBeOpenTag(contextToken.parent)); - case "/": - return !!contextToken && (isStringLiteralLike(contextToken) - ? !!tryGetImportFromModuleSpecifier(contextToken) - : contextToken.kind === SyntaxKind.SlashToken && isJsxClosingElement(contextToken.parent)); +} +/* @internal */ +interface CompletionEntryDisplayNameForSymbol { + readonly name: string; + readonly needsConvertPropertyAccess: boolean; +} +/* @internal */ +function getCompletionEntryDisplayNameForSymbol(symbol: Symbol, target: ScriptTarget, origin: SymbolOriginInfo | undefined, kind: CompletionKind): CompletionEntryDisplayNameForSymbol | undefined { + const name = originIsExport(origin) ? getNameForExportedSymbol(symbol, target) : symbol.name; + if (name === undefined + // If the symbol is external module, don't show it in the completion list + // (i.e declare module "http" { const x; } | // <= request completion here, "http" should not be there) + || symbol.flags & SymbolFlags.Module && isSingleOrDoubleQuote(name.charCodeAt(0)) + // If the symbol is the internal name of an ES symbol, it is not a valid entry. Internal names for ES symbols start with "__@" + || isKnownSymbol(symbol)) { + return undefined; + } + const validNameResult: CompletionEntryDisplayNameForSymbol = { name, needsConvertPropertyAccess: false }; + if (isIdentifierText(name, target) || symbol.valueDeclaration && isPrivateIdentifierPropertyDeclaration(symbol.valueDeclaration)) { + return validNameResult; + } + switch (kind) { + case CompletionKind.MemberLike: + return undefined; + case CompletionKind.ObjectPropertyDeclaration: + // TODO: GH#18169 + return { name: JSON.stringify(name), needsConvertPropertyAccess: false }; + case CompletionKind.PropertyAccess: + case CompletionKind.Global: // For a 'this.' completion it will be in a global context, but may have a non-identifier name. + // Don't add a completion for a name starting with a space. See https://github.com/Microsoft/TypeScript/pull/20547 + return name.charCodeAt(0) === CharacterCodes.space ? undefined : { name, needsConvertPropertyAccess: true }; + case CompletionKind.None: + case CompletionKind.String: + return validNameResult; + default: + Debug.assertNever(kind); + } +} +// A cache of completion entries for keywords, these do not change between sessions +/* @internal */ +const _keywordCompletions: CompletionEntry[][] = []; +/* @internal */ +const allKeywordsCompletions: () => readonly CompletionEntry[] = memoize(() => { + const res: CompletionEntry[] = []; + for (let i = SyntaxKind.FirstKeyword; i <= SyntaxKind.LastKeyword; i++) { + res.push({ + name: (tokenToString(i)!), + kind: ScriptElementKind.keyword, + kindModifiers: ScriptElementKindModifier.none, + sortText: SortText.GlobalsOrKeywords + }); + } + return res; +}); +/* @internal */ +function getKeywordCompletions(keywordFilter: KeywordCompletionFilters, filterOutTsOnlyKeywords: boolean): readonly CompletionEntry[] { + if (!filterOutTsOnlyKeywords) + return getTypescriptKeywordCompletions(keywordFilter); + const index = keywordFilter + KeywordCompletionFilters.Last + 1; + return _keywordCompletions[index] || + (_keywordCompletions[index] = getTypescriptKeywordCompletions(keywordFilter) + .filter(entry => !isTypeScriptOnlyKeyword((stringToToken(entry.name)!)))); +} +/* @internal */ +function getTypescriptKeywordCompletions(keywordFilter: KeywordCompletionFilters): readonly CompletionEntry[] { + return _keywordCompletions[keywordFilter] || (_keywordCompletions[keywordFilter] = allKeywordsCompletions().filter(entry => { + const kind = (stringToToken(entry.name)!); + switch (keywordFilter) { + case KeywordCompletionFilters.None: + return false; + case KeywordCompletionFilters.All: + return isFunctionLikeBodyKeyword(kind) + || kind === SyntaxKind.DeclareKeyword + || kind === SyntaxKind.ModuleKeyword + || kind === SyntaxKind.TypeKeyword + || kind === SyntaxKind.NamespaceKeyword + || kind === SyntaxKind.AsKeyword + || isTypeKeyword(kind) && kind !== SyntaxKind.UndefinedKeyword; + case KeywordCompletionFilters.FunctionLikeBodyKeywords: + return isFunctionLikeBodyKeyword(kind); + case KeywordCompletionFilters.ClassElementKeywords: + return isClassMemberCompletionKeyword(kind); + case KeywordCompletionFilters.InterfaceElementKeywords: + return isInterfaceOrTypeLiteralCompletionKeyword(kind); + case KeywordCompletionFilters.ConstructorParameterKeywords: + return isParameterPropertyModifier(kind); + case KeywordCompletionFilters.TypeAssertionKeywords: + return isTypeKeyword(kind) || kind === SyntaxKind.ConstKeyword; + case KeywordCompletionFilters.TypeKeywords: + return isTypeKeyword(kind); default: - return Debug.assertNever(triggerCharacter); + return Debug.assertNever(keywordFilter); } + })); +} +/* @internal */ +function isTypeScriptOnlyKeyword(kind: SyntaxKind) { + switch (kind) { + case SyntaxKind.AbstractKeyword: + case SyntaxKind.AnyKeyword: + case SyntaxKind.BigIntKeyword: + case SyntaxKind.BooleanKeyword: + case SyntaxKind.DeclareKeyword: + case SyntaxKind.EnumKeyword: + case SyntaxKind.GlobalKeyword: + case SyntaxKind.ImplementsKeyword: + case SyntaxKind.InferKeyword: + case SyntaxKind.InterfaceKeyword: + case SyntaxKind.IsKeyword: + case SyntaxKind.KeyOfKeyword: + case SyntaxKind.ModuleKeyword: + case SyntaxKind.NamespaceKeyword: + case SyntaxKind.NeverKeyword: + case SyntaxKind.NumberKeyword: + case SyntaxKind.ObjectKeyword: + case SyntaxKind.PrivateKeyword: + case SyntaxKind.ProtectedKeyword: + case SyntaxKind.PublicKeyword: + case SyntaxKind.ReadonlyKeyword: + case SyntaxKind.StringKeyword: + case SyntaxKind.SymbolKeyword: + case SyntaxKind.TypeKeyword: + case SyntaxKind.UniqueKeyword: + case SyntaxKind.UnknownKeyword: + return true; + default: + return false; } - - function binaryExpressionMayBeOpenTag({ left }: BinaryExpression): boolean { - return nodeIsMissing(left); +} +/* @internal */ +function isInterfaceOrTypeLiteralCompletionKeyword(kind: SyntaxKind): boolean { + return kind === SyntaxKind.ReadonlyKeyword; +} +/* @internal */ +function isClassMemberCompletionKeyword(kind: SyntaxKind) { + switch (kind) { + case SyntaxKind.AbstractKeyword: + case SyntaxKind.ConstructorKeyword: + case SyntaxKind.GetKeyword: + case SyntaxKind.SetKeyword: + case SyntaxKind.AsyncKeyword: + case SyntaxKind.DeclareKeyword: + return true; + default: + return isClassMemberModifier(kind); } - - function findAlias(typeChecker: TypeChecker, symbol: Symbol, predicate: (symbol: Symbol) => boolean): Symbol | undefined { - let currentAlias: Symbol | undefined = symbol; - while (currentAlias.flags & SymbolFlags.Alias && (currentAlias = typeChecker.getImmediateAliasedSymbol(currentAlias))) { - if (predicate(currentAlias)) { - return currentAlias; +} +/* @internal */ +function isFunctionLikeBodyKeyword(kind: SyntaxKind) { + return kind === SyntaxKind.AsyncKeyword + || kind === SyntaxKind.AwaitKeyword + || !isContextualKeyword(kind) && !isClassMemberCompletionKeyword(kind); +} +/* @internal */ +function keywordForNode(node: Node): SyntaxKind { + return isIdentifier(node) ? node.originalKeywordKind || SyntaxKind.Unknown : node.kind; +} +/** Get the corresponding JSDocTag node if the position is in a jsDoc comment */ +/* @internal */ +function getJsDocTagAtPosition(node: Node, position: number): JSDocTag | undefined { + const jsdoc = findAncestor(node, isJSDoc); + return jsdoc && jsdoc.tags && (rangeContainsPosition(jsdoc, position) ? findLast(jsdoc.tags, tag => tag.pos < position) : undefined); +} +/* @internal */ +function getPropertiesForObjectExpression(contextualType: Type, completionsType: Type | undefined, obj: ObjectLiteralExpression | JsxAttributes, checker: TypeChecker): Symbol[] { + const hasCompletionsType = completionsType && completionsType !== contextualType; + const type = hasCompletionsType && !(completionsType!.flags & TypeFlags.AnyOrUnknown) + ? checker.getUnionType([contextualType, completionsType!]) + : contextualType; + const properties = type.isUnion() + ? checker.getAllPossiblePropertiesOfTypes(type.types.filter(memberType => + // If we're providing completions for an object literal, skip primitive, array-like, or callable types since those shouldn't be implemented by object literals. + !(memberType.flags & TypeFlags.Primitive || + checker.isArrayLikeType(memberType) || + typeHasCallOrConstructSignatures(memberType, checker) || + checker.isTypeInvalidDueToUnionDiscriminant(memberType, obj)))) + : type.getApparentProperties(); + return hasCompletionsType ? properties.filter(hasDeclarationOtherThanSelf) : properties; + // Filter out members whose only declaration is the object literal itself to avoid + // self-fulfilling completions like: + // + // function f(x: T) {} + // f({ abc/**/: "" }) // `abc` is a member of `T` but only because it declares itself + function hasDeclarationOtherThanSelf(member: Symbol) { + return some(member.declarations, decl => decl.parent !== obj); + } +} +/** + * Gets all properties on a type, but if that type is a union of several types, + * excludes array-like types or callable/constructable types. + */ +/* @internal */ +function getPropertiesForCompletion(type: Type, checker: TypeChecker): Symbol[] { + return type.isUnion() + ? Debug.checkEachDefined(checker.getAllPossiblePropertiesOfTypes(type.types), "getAllPossiblePropertiesOfTypes() should all be defined") + : Debug.checkEachDefined(type.getApparentProperties(), "getApparentProperties() should all be defined"); +} +/** + * Returns the immediate owning class declaration of a context token, + * on the condition that one exists and that the context implies completion should be given. + */ +/* @internal */ +function tryGetObjectTypeDeclarationCompletionContainer(sourceFile: SourceFile, contextToken: Node | undefined, location: Node, position: number): ObjectTypeDeclaration | undefined { + // class c { method() { } | method2() { } } + switch (location.kind) { + case SyntaxKind.SyntaxList: + return tryCast(location.parent, isObjectTypeDeclaration); + case SyntaxKind.EndOfFileToken: + const cls = tryCast(lastOrUndefined(cast(location.parent, isSourceFile).statements), isObjectTypeDeclaration); + if (cls && !findChildOfKind(cls, SyntaxKind.CloseBraceToken, sourceFile)) { + return cls; + } + break; + case SyntaxKind.Identifier: // class c extends React.Component { a: () => 1\n compon| } + if (isFromObjectTypeDeclaration(location)) { + return findAncestor(location, isObjectTypeDeclaration); + } + } + if (!contextToken) + return undefined; + switch (contextToken.kind) { + case SyntaxKind.EqualsToken: // class c { public prop = | /* global completions */ } + return undefined; + case SyntaxKind.SemicolonToken: // class c {getValue(): number; | } + case SyntaxKind.CloseBraceToken: // class c { method() { } | } + // class c { method() { } b| } + return isFromObjectTypeDeclaration(location) && (location.parent as ClassElement | TypeElement).name === location + ? location.parent.parent as ObjectTypeDeclaration + : tryCast(location, isObjectTypeDeclaration); + case SyntaxKind.OpenBraceToken: // class c { | + case SyntaxKind.CommaToken: // class c {getValue(): number, | } + return tryCast(contextToken.parent, isObjectTypeDeclaration); + default: + if (!isFromObjectTypeDeclaration(contextToken)) { + // class c extends React.Component { a: () => 1\n| } + if (getLineAndCharacterOfPosition(sourceFile, contextToken.getEnd()).line !== getLineAndCharacterOfPosition(sourceFile, position).line && isObjectTypeDeclaration(location)) { + return location; + } + return undefined; } + const isValidKeyword = isClassLike(contextToken.parent.parent) ? isClassMemberCompletionKeyword : isInterfaceOrTypeLiteralCompletionKeyword; + return (isValidKeyword(contextToken.kind) || contextToken.kind === SyntaxKind.AsteriskToken || isIdentifier(contextToken) && isValidKeyword((stringToToken(contextToken.text)!))) // TODO: GH#18217 + ? contextToken.parent.parent as ObjectTypeDeclaration : undefined; + } +} +// TODO: GH#19856 Would like to return `node is Node & { parent: (ClassElement | TypeElement) & { parent: ObjectTypeDeclaration } }` but then compilation takes > 10 minutes +/* @internal */ +function isFromObjectTypeDeclaration(node: Node): boolean { + return node.parent && isClassOrTypeElement(node.parent) && isObjectTypeDeclaration(node.parent.parent); +} +/* @internal */ +function isValidTrigger(sourceFile: SourceFile, triggerCharacter: CompletionsTriggerCharacter, contextToken: Node | undefined, position: number): boolean { + switch (triggerCharacter) { + case ".": + case "@": + return true; + case '"': + case "'": + case "`": + // Only automatically bring up completions if this is an opening quote. + return !!contextToken && isStringLiteralOrTemplate(contextToken) && position === contextToken.getStart(sourceFile) + 1; + case "#": + return !!contextToken && isPrivateIdentifier(contextToken) && !!getContainingClass(contextToken); + case "<": + // Opening JSX tag + return !!contextToken && contextToken.kind === SyntaxKind.LessThanToken && (!isBinaryExpression(contextToken.parent) || binaryExpressionMayBeOpenTag(contextToken.parent)); + case "/": + return !!contextToken && (isStringLiteralLike(contextToken) + ? !!tryGetImportFromModuleSpecifier(contextToken) + : contextToken.kind === SyntaxKind.SlashToken && isJsxClosingElement(contextToken.parent)); + default: + return Debug.assertNever(triggerCharacter); + } +} +/* @internal */ +function binaryExpressionMayBeOpenTag({ left }: BinaryExpression): boolean { + return nodeIsMissing(left); +} +/* @internal */ +function findAlias(typeChecker: TypeChecker, symbol: Symbol, predicate: (symbol: Symbol) => boolean): Symbol | undefined { + let currentAlias: Symbol | undefined = symbol; + while (currentAlias.flags & SymbolFlags.Alias && (currentAlias = typeChecker.getImmediateAliasedSymbol(currentAlias))) { + if (predicate(currentAlias)) { + return currentAlias; } } } diff --git a/src/services/documentHighlights.ts b/src/services/documentHighlights.ts index 378739144110d..608e190c79134 100644 --- a/src/services/documentHighlights.ts +++ b/src/services/documentHighlights.ts @@ -1,511 +1,427 @@ -namespace ts { - export interface DocumentHighlights { - fileName: string; - highlightSpans: HighlightSpan[]; +import { HighlightSpan, Program, CancellationToken, SourceFile, getTouchingPropertyName, isJsxOpeningElement, isJsxClosingElement, Node, createTextSpanFromNode, HighlightSpanKind, arrayToSet, arrayToMultiMap, arrayFrom, Debug, find, SyntaxKind, isIfStatement, isReturnStatement, isThrowStatement, isTryStatement, isSwitchStatement, isDefaultClause, isCaseClause, isBreakOrContinueStatement, IterationStatement, isIterationStatement, isConstructorDeclaration, isAccessor, isAwaitExpression, isModifierKind, isDeclaration, isVariableStatement, mapDefined, contains, ThrowStatement, concatenate, isFunctionLike, isFunctionBlock, BreakOrContinueStatement, toArray, findAncestor, Modifier, modifierToFlag, findModifier, ModifierFlags, ModuleBlock, Block, CaseClause, DefaultClause, ConstructorDeclaration, MethodDeclaration, FunctionDeclaration, ObjectTypeDeclaration, isClassDeclaration, isClassLike, Push, forEach, SwitchStatement, TryStatement, findChildOfKind, forEachReturnStatement, ReturnStatement, FunctionLikeDeclaration, getContainingFunction, cast, isBlock, forEachChild, isYieldExpression, isInterfaceDeclaration, isModuleDeclaration, isTypeAliasDeclaration, isTypeNode, IfStatement, isWhiteSpaceSingleLine, createTextSpanFromBounds, __String, isLabeledStatement } from "./ts"; +import { getReferenceEntriesForNode, toHighlightSpan } from "./ts.FindAllReferences"; +export interface DocumentHighlights { + fileName: string; + highlightSpans: HighlightSpan[]; +} +/* @internal */ +export namespace DocumentHighlights { + export function getDocumentHighlights(program: Program, cancellationToken: CancellationToken, sourceFile: SourceFile, position: number, sourceFilesToSearch: readonly SourceFile[]): DocumentHighlights[] | undefined { + const node = getTouchingPropertyName(sourceFile, position); + if (node.parent && (isJsxOpeningElement(node.parent) && node.parent.tagName === node || isJsxClosingElement(node.parent))) { + // For a JSX element, just highlight the matching tag, not all references. + const { openingElement, closingElement } = node.parent.parent; + const highlightSpans = [openingElement, closingElement].map(({ tagName }) => getHighlightSpanForNode(tagName, sourceFile)); + return [{ fileName: sourceFile.fileName, highlightSpans }]; + } + return getSemanticDocumentHighlights(position, node, program, cancellationToken, sourceFilesToSearch) || getSyntacticDocumentHighlights(node, sourceFile); } - - /* @internal */ - export namespace DocumentHighlights { - export function getDocumentHighlights(program: Program, cancellationToken: CancellationToken, sourceFile: SourceFile, position: number, sourceFilesToSearch: readonly SourceFile[]): DocumentHighlights[] | undefined { - const node = getTouchingPropertyName(sourceFile, position); - - if (node.parent && (isJsxOpeningElement(node.parent) && node.parent.tagName === node || isJsxClosingElement(node.parent))) { - // For a JSX element, just highlight the matching tag, not all references. - const { openingElement, closingElement } = node.parent.parent; - const highlightSpans = [openingElement, closingElement].map(({ tagName }) => getHighlightSpanForNode(tagName, sourceFile)); - return [{ fileName: sourceFile.fileName, highlightSpans }]; + function getHighlightSpanForNode(node: Node, sourceFile: SourceFile): HighlightSpan { + return { + fileName: sourceFile.fileName, + textSpan: createTextSpanFromNode(node, sourceFile), + kind: HighlightSpanKind.none + }; + } + function getSemanticDocumentHighlights(position: number, node: Node, program: Program, cancellationToken: CancellationToken, sourceFilesToSearch: readonly SourceFile[]): DocumentHighlights[] | undefined { + const sourceFilesSet = arrayToSet(sourceFilesToSearch, f => f.fileName); + const referenceEntries = getReferenceEntriesForNode(position, node, program, sourceFilesToSearch, cancellationToken, /*options*/ undefined, sourceFilesSet); + if (!referenceEntries) + return undefined; + const map = arrayToMultiMap(referenceEntries.map(toHighlightSpan), e => e.fileName, e => e.span); + return arrayFrom(map.entries(), ([fileName, highlightSpans]) => { + if (!sourceFilesSet.has(fileName)) { + Debug.assert(program.redirectTargetsMap.has(fileName)); + const redirectTarget = program.getSourceFile(fileName); + const redirect = (find(sourceFilesToSearch, f => !!f.redirectInfo && f.redirectInfo.redirectTarget === redirectTarget)!); + fileName = redirect.fileName; + Debug.assert(sourceFilesSet.has(fileName)); } - - return getSemanticDocumentHighlights(position, node, program, cancellationToken, sourceFilesToSearch) || getSyntacticDocumentHighlights(node, sourceFile); + return { fileName, highlightSpans }; + }); + } + function getSyntacticDocumentHighlights(node: Node, sourceFile: SourceFile): DocumentHighlights[] | undefined { + const highlightSpans = getHighlightSpans(node, sourceFile); + return highlightSpans && [{ fileName: sourceFile.fileName, highlightSpans }]; + } + function getHighlightSpans(node: Node, sourceFile: SourceFile): HighlightSpan[] | undefined { + switch (node.kind) { + case SyntaxKind.IfKeyword: + case SyntaxKind.ElseKeyword: + return isIfStatement(node.parent) ? getIfElseOccurrences(node.parent, sourceFile) : undefined; + case SyntaxKind.ReturnKeyword: + return useParent(node.parent, isReturnStatement, getReturnOccurrences); + case SyntaxKind.ThrowKeyword: + return useParent(node.parent, isThrowStatement, getThrowOccurrences); + case SyntaxKind.TryKeyword: + case SyntaxKind.CatchKeyword: + case SyntaxKind.FinallyKeyword: + const tryStatement = node.kind === SyntaxKind.CatchKeyword ? node.parent.parent : node.parent; + return useParent(tryStatement, isTryStatement, getTryCatchFinallyOccurrences); + case SyntaxKind.SwitchKeyword: + return useParent(node.parent, isSwitchStatement, getSwitchCaseDefaultOccurrences); + case SyntaxKind.CaseKeyword: + case SyntaxKind.DefaultKeyword: { + if (isDefaultClause(node.parent) || isCaseClause(node.parent)) { + return useParent(node.parent.parent.parent, isSwitchStatement, getSwitchCaseDefaultOccurrences); + } + return undefined; + } + case SyntaxKind.BreakKeyword: + case SyntaxKind.ContinueKeyword: + return useParent(node.parent, isBreakOrContinueStatement, getBreakOrContinueStatementOccurrences); + case SyntaxKind.ForKeyword: + case SyntaxKind.WhileKeyword: + case SyntaxKind.DoKeyword: + return useParent(node.parent, (n): n is IterationStatement => isIterationStatement(n, /*lookInLabeledStatements*/ true), getLoopBreakContinueOccurrences); + case SyntaxKind.ConstructorKeyword: + return getFromAllDeclarations(isConstructorDeclaration, [SyntaxKind.ConstructorKeyword]); + case SyntaxKind.GetKeyword: + case SyntaxKind.SetKeyword: + return getFromAllDeclarations(isAccessor, [SyntaxKind.GetKeyword, SyntaxKind.SetKeyword]); + case SyntaxKind.AwaitKeyword: + return useParent(node.parent, isAwaitExpression, getAsyncAndAwaitOccurrences); + case SyntaxKind.AsyncKeyword: + return highlightSpans(getAsyncAndAwaitOccurrences(node)); + case SyntaxKind.YieldKeyword: + return highlightSpans(getYieldOccurrences(node)); + default: + return isModifierKind(node.kind) && (isDeclaration(node.parent) || isVariableStatement(node.parent)) + ? highlightSpans(getModifierOccurrences(node.kind, node.parent)) + : undefined; } - - function getHighlightSpanForNode(node: Node, sourceFile: SourceFile): HighlightSpan { - return { - fileName: sourceFile.fileName, - textSpan: createTextSpanFromNode(node, sourceFile), - kind: HighlightSpanKind.none - }; + function getFromAllDeclarations(nodeTest: (node: Node) => node is T, keywords: readonly SyntaxKind[]): HighlightSpan[] | undefined { + return useParent(node.parent, nodeTest, decl => mapDefined(decl.symbol.declarations, d => nodeTest(d) ? find(d.getChildren(sourceFile), c => contains(keywords, c.kind)) : undefined)); } - - function getSemanticDocumentHighlights(position: number, node: Node, program: Program, cancellationToken: CancellationToken, sourceFilesToSearch: readonly SourceFile[]): DocumentHighlights[] | undefined { - const sourceFilesSet = arrayToSet(sourceFilesToSearch, f => f.fileName); - const referenceEntries = FindAllReferences.getReferenceEntriesForNode(position, node, program, sourceFilesToSearch, cancellationToken, /*options*/ undefined, sourceFilesSet); - if (!referenceEntries) return undefined; - const map = arrayToMultiMap(referenceEntries.map(FindAllReferences.toHighlightSpan), e => e.fileName, e => e.span); - return arrayFrom(map.entries(), ([fileName, highlightSpans]) => { - if (!sourceFilesSet.has(fileName)) { - Debug.assert(program.redirectTargetsMap.has(fileName)); - const redirectTarget = program.getSourceFile(fileName); - const redirect = find(sourceFilesToSearch, f => !!f.redirectInfo && f.redirectInfo.redirectTarget === redirectTarget)!; - fileName = redirect.fileName; - Debug.assert(sourceFilesSet.has(fileName)); - } - return { fileName, highlightSpans }; - }); + function useParent(node: Node, nodeTest: (node: Node) => node is T, getNodes: (node: T, sourceFile: SourceFile) => readonly Node[] | undefined): HighlightSpan[] | undefined { + return nodeTest(node) ? highlightSpans(getNodes(node, sourceFile)) : undefined; } - - function getSyntacticDocumentHighlights(node: Node, sourceFile: SourceFile): DocumentHighlights[] | undefined { - const highlightSpans = getHighlightSpans(node, sourceFile); - return highlightSpans && [{ fileName: sourceFile.fileName, highlightSpans }]; + function highlightSpans(nodes: readonly Node[] | undefined): HighlightSpan[] | undefined { + return nodes && nodes.map(node => getHighlightSpanForNode(node, sourceFile)); } - - function getHighlightSpans(node: Node, sourceFile: SourceFile): HighlightSpan[] | undefined { - switch (node.kind) { - case SyntaxKind.IfKeyword: - case SyntaxKind.ElseKeyword: - return isIfStatement(node.parent) ? getIfElseOccurrences(node.parent, sourceFile) : undefined; - case SyntaxKind.ReturnKeyword: - return useParent(node.parent, isReturnStatement, getReturnOccurrences); - case SyntaxKind.ThrowKeyword: - return useParent(node.parent, isThrowStatement, getThrowOccurrences); - case SyntaxKind.TryKeyword: - case SyntaxKind.CatchKeyword: - case SyntaxKind.FinallyKeyword: - const tryStatement = node.kind === SyntaxKind.CatchKeyword ? node.parent.parent : node.parent; - return useParent(tryStatement, isTryStatement, getTryCatchFinallyOccurrences); - case SyntaxKind.SwitchKeyword: - return useParent(node.parent, isSwitchStatement, getSwitchCaseDefaultOccurrences); - case SyntaxKind.CaseKeyword: - case SyntaxKind.DefaultKeyword: { - if (isDefaultClause(node.parent) || isCaseClause(node.parent)) { - return useParent(node.parent.parent.parent, isSwitchStatement, getSwitchCaseDefaultOccurrences); - } - return undefined; - } - case SyntaxKind.BreakKeyword: - case SyntaxKind.ContinueKeyword: - return useParent(node.parent, isBreakOrContinueStatement, getBreakOrContinueStatementOccurrences); - case SyntaxKind.ForKeyword: - case SyntaxKind.WhileKeyword: - case SyntaxKind.DoKeyword: - return useParent(node.parent, (n): n is IterationStatement => isIterationStatement(n, /*lookInLabeledStatements*/ true), getLoopBreakContinueOccurrences); - case SyntaxKind.ConstructorKeyword: - return getFromAllDeclarations(isConstructorDeclaration, [SyntaxKind.ConstructorKeyword]); - case SyntaxKind.GetKeyword: - case SyntaxKind.SetKeyword: - return getFromAllDeclarations(isAccessor, [SyntaxKind.GetKeyword, SyntaxKind.SetKeyword]); - case SyntaxKind.AwaitKeyword: - return useParent(node.parent, isAwaitExpression, getAsyncAndAwaitOccurrences); - case SyntaxKind.AsyncKeyword: - return highlightSpans(getAsyncAndAwaitOccurrences(node)); - case SyntaxKind.YieldKeyword: - return highlightSpans(getYieldOccurrences(node)); - default: - return isModifierKind(node.kind) && (isDeclaration(node.parent) || isVariableStatement(node.parent)) - ? highlightSpans(getModifierOccurrences(node.kind, node.parent)) - : undefined; - } - - function getFromAllDeclarations(nodeTest: (node: Node) => node is T, keywords: readonly SyntaxKind[]): HighlightSpan[] | undefined { - return useParent(node.parent, nodeTest, decl => mapDefined(decl.symbol.declarations, d => - nodeTest(d) ? find(d.getChildren(sourceFile), c => contains(keywords, c.kind)) : undefined)); - } - - function useParent(node: Node, nodeTest: (node: Node) => node is T, getNodes: (node: T, sourceFile: SourceFile) => readonly Node[] | undefined): HighlightSpan[] | undefined { - return nodeTest(node) ? highlightSpans(getNodes(node, sourceFile)) : undefined; + } + /** + * Aggregates all throw-statements within this node *without* crossing + * into function boundaries and try-blocks with catch-clauses. + */ + function aggregateOwnedThrowStatements(node: Node): readonly ThrowStatement[] | undefined { + if (isThrowStatement(node)) { + return [node]; + } + else if (isTryStatement(node)) { + // Exceptions thrown within a try block lacking a catch clause are "owned" in the current context. + return concatenate(node.catchClause ? aggregateOwnedThrowStatements(node.catchClause) : node.tryBlock && aggregateOwnedThrowStatements(node.tryBlock), node.finallyBlock && aggregateOwnedThrowStatements(node.finallyBlock)); + } + // Do not cross function boundaries. + return isFunctionLike(node) ? undefined : flatMapChildren(node, aggregateOwnedThrowStatements); + } + /** + * For lack of a better name, this function takes a throw statement and returns the + * nearest ancestor that is a try-block (whose try statement has a catch clause), + * function-block, or source file. + */ + function getThrowStatementOwner(throwStatement: ThrowStatement): Node | undefined { + let child: Node = throwStatement; + while (child.parent) { + const parent = child.parent; + if (isFunctionBlock(parent) || parent.kind === SyntaxKind.SourceFile) { + return parent; } - - function highlightSpans(nodes: readonly Node[] | undefined): HighlightSpan[] | undefined { - return nodes && nodes.map(node => getHighlightSpanForNode(node, sourceFile)); + // A throw-statement is only owned by a try-statement if the try-statement has + // a catch clause, and if the throw-statement occurs within the try block. + if (isTryStatement(parent) && parent.tryBlock === child && parent.catchClause) { + return child; } + child = parent; } - - /** - * Aggregates all throw-statements within this node *without* crossing - * into function boundaries and try-blocks with catch-clauses. - */ - function aggregateOwnedThrowStatements(node: Node): readonly ThrowStatement[] | undefined { - if (isThrowStatement(node)) { - return [node]; + return undefined; + } + function aggregateAllBreakAndContinueStatements(node: Node): readonly BreakOrContinueStatement[] | undefined { + return isBreakOrContinueStatement(node) ? [node] : isFunctionLike(node) ? undefined : flatMapChildren(node, aggregateAllBreakAndContinueStatements); + } + function flatMapChildren(node: Node, cb: (child: Node) => readonly T[] | T | undefined): readonly T[] { + const result: T[] = []; + node.forEachChild(child => { + const value = cb(child); + if (value !== undefined) { + result.push(...toArray(value)); } - else if (isTryStatement(node)) { - // Exceptions thrown within a try block lacking a catch clause are "owned" in the current context. - return concatenate( - node.catchClause ? aggregateOwnedThrowStatements(node.catchClause) : node.tryBlock && aggregateOwnedThrowStatements(node.tryBlock), - node.finallyBlock && aggregateOwnedThrowStatements(node.finallyBlock)); + }); + return result; + } + function ownsBreakOrContinueStatement(owner: Node, statement: BreakOrContinueStatement): boolean { + const actualOwner = getBreakOrContinueOwner(statement); + return !!actualOwner && actualOwner === owner; + } + function getBreakOrContinueOwner(statement: BreakOrContinueStatement): Node | undefined { + return findAncestor(statement, node => { + switch (node.kind) { + case SyntaxKind.SwitchStatement: + if (statement.kind === SyntaxKind.ContinueStatement) { + return false; + } + // falls through + case SyntaxKind.ForStatement: + case SyntaxKind.ForInStatement: + case SyntaxKind.ForOfStatement: + case SyntaxKind.WhileStatement: + case SyntaxKind.DoStatement: + return !statement.label || isLabeledBy(node, statement.label.escapedText); + default: + // Don't cross function boundaries. + // TODO: GH#20090 + return isFunctionLike(node) && "quit"; } - // Do not cross function boundaries. - return isFunctionLike(node) ? undefined : flatMapChildren(node, aggregateOwnedThrowStatements); - } - - /** - * For lack of a better name, this function takes a throw statement and returns the - * nearest ancestor that is a try-block (whose try statement has a catch clause), - * function-block, or source file. - */ - function getThrowStatementOwner(throwStatement: ThrowStatement): Node | undefined { - let child: Node = throwStatement; - - while (child.parent) { - const parent = child.parent; - - if (isFunctionBlock(parent) || parent.kind === SyntaxKind.SourceFile) { - return parent; + }); + } + function getModifierOccurrences(modifier: Modifier["kind"], declaration: Node): Node[] { + return mapDefined(getNodesToSearchForModifier(declaration, modifierToFlag(modifier)), node => findModifier(node, modifier)); + } + function getNodesToSearchForModifier(declaration: Node, modifierFlag: ModifierFlags): readonly Node[] | undefined { + // Types of node whose children might have modifiers. + const container = (declaration.parent as ModuleBlock | SourceFile | Block | CaseClause | DefaultClause | ConstructorDeclaration | MethodDeclaration | FunctionDeclaration | ObjectTypeDeclaration); + switch (container.kind) { + case SyntaxKind.ModuleBlock: + case SyntaxKind.SourceFile: + case SyntaxKind.Block: + case SyntaxKind.CaseClause: + case SyntaxKind.DefaultClause: + // Container is either a class declaration or the declaration is a classDeclaration + if (modifierFlag & ModifierFlags.Abstract && isClassDeclaration(declaration)) { + return [...declaration.members, declaration]; } - - // A throw-statement is only owned by a try-statement if the try-statement has - // a catch clause, and if the throw-statement occurs within the try block. - if (isTryStatement(parent) && parent.tryBlock === child && parent.catchClause) { - return child; + else { + return container.statements; } - - child = parent; - } - - return undefined; - } - - function aggregateAllBreakAndContinueStatements(node: Node): readonly BreakOrContinueStatement[] | undefined { - return isBreakOrContinueStatement(node) ? [node] : isFunctionLike(node) ? undefined : flatMapChildren(node, aggregateAllBreakAndContinueStatements); - } - - function flatMapChildren(node: Node, cb: (child: Node) => readonly T[] | T | undefined): readonly T[] { - const result: T[] = []; - node.forEachChild(child => { - const value = cb(child); - if (value !== undefined) { - result.push(...toArray(value)); + case SyntaxKind.Constructor: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.FunctionDeclaration: + return [...container.parameters, ...(isClassLike(container.parent) ? container.parent.members : [])]; + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ClassExpression: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.TypeLiteral: + const nodes = container.members; + // If we're an accessibility modifier, we're in an instance member and should search + // the constructor's parameter list for instance members as well. + if (modifierFlag & (ModifierFlags.AccessibilityModifier | ModifierFlags.Readonly)) { + const constructor = find(container.members, isConstructorDeclaration); + if (constructor) { + return [...nodes, ...constructor.parameters]; + } } - }); - return result; - } - - function ownsBreakOrContinueStatement(owner: Node, statement: BreakOrContinueStatement): boolean { - const actualOwner = getBreakOrContinueOwner(statement); - return !!actualOwner && actualOwner === owner; - } - - function getBreakOrContinueOwner(statement: BreakOrContinueStatement): Node | undefined { - return findAncestor(statement, node => { - switch (node.kind) { - case SyntaxKind.SwitchStatement: - if (statement.kind === SyntaxKind.ContinueStatement) { - return false; - } - // falls through - - case SyntaxKind.ForStatement: - case SyntaxKind.ForInStatement: - case SyntaxKind.ForOfStatement: - case SyntaxKind.WhileStatement: - case SyntaxKind.DoStatement: - return !statement.label || isLabeledBy(node, statement.label.escapedText); - default: - // Don't cross function boundaries. - // TODO: GH#20090 - return isFunctionLike(node) && "quit"; + else if (modifierFlag & ModifierFlags.Abstract) { + return [...nodes, container]; } - }); + return nodes; + default: + Debug.assertNever(container, "Invalid container kind."); } - - function getModifierOccurrences(modifier: Modifier["kind"], declaration: Node): Node[] { - return mapDefined(getNodesToSearchForModifier(declaration, modifierToFlag(modifier)), node => findModifier(node, modifier)); + } + function pushKeywordIf(keywordList: Push, token: Node | undefined, ...expected: SyntaxKind[]): boolean { + if (token && contains(expected, token.kind)) { + keywordList.push(token); + return true; } - - function getNodesToSearchForModifier(declaration: Node, modifierFlag: ModifierFlags): readonly Node[] | undefined { - // Types of node whose children might have modifiers. - const container = declaration.parent as ModuleBlock | SourceFile | Block | CaseClause | DefaultClause | ConstructorDeclaration | MethodDeclaration | FunctionDeclaration | ObjectTypeDeclaration; - switch (container.kind) { - case SyntaxKind.ModuleBlock: - case SyntaxKind.SourceFile: - case SyntaxKind.Block: - case SyntaxKind.CaseClause: - case SyntaxKind.DefaultClause: - // Container is either a class declaration or the declaration is a classDeclaration - if (modifierFlag & ModifierFlags.Abstract && isClassDeclaration(declaration)) { - return [...declaration.members, declaration]; - } - else { - return container.statements; - } - case SyntaxKind.Constructor: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.FunctionDeclaration: - return [...container.parameters, ...(isClassLike(container.parent) ? container.parent.members : [])]; - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ClassExpression: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.TypeLiteral: - const nodes = container.members; - - // If we're an accessibility modifier, we're in an instance member and should search - // the constructor's parameter list for instance members as well. - if (modifierFlag & (ModifierFlags.AccessibilityModifier | ModifierFlags.Readonly)) { - const constructor = find(container.members, isConstructorDeclaration); - if (constructor) { - return [...nodes, ...constructor.parameters]; - } - } - else if (modifierFlag & ModifierFlags.Abstract) { - return [...nodes, container]; + return false; + } + function getLoopBreakContinueOccurrences(loopNode: IterationStatement): Node[] { + const keywords: Node[] = []; + if (pushKeywordIf(keywords, loopNode.getFirstToken(), SyntaxKind.ForKeyword, SyntaxKind.WhileKeyword, SyntaxKind.DoKeyword)) { + // If we succeeded and got a do-while loop, then start looking for a 'while' keyword. + if (loopNode.kind === SyntaxKind.DoStatement) { + const loopTokens = loopNode.getChildren(); + for (let i = loopTokens.length - 1; i >= 0; i--) { + if (pushKeywordIf(keywords, loopTokens[i], SyntaxKind.WhileKeyword)) { + break; } - return nodes; - default: - Debug.assertNever(container, "Invalid container kind."); + } } } - - function pushKeywordIf(keywordList: Push, token: Node | undefined, ...expected: SyntaxKind[]): boolean { - if (token && contains(expected, token.kind)) { - keywordList.push(token); - return true; + forEach(aggregateAllBreakAndContinueStatements(loopNode.statement), statement => { + if (ownsBreakOrContinueStatement(loopNode, statement)) { + pushKeywordIf(keywords, statement.getFirstToken(), SyntaxKind.BreakKeyword, SyntaxKind.ContinueKeyword); } - - return false; - } - - function getLoopBreakContinueOccurrences(loopNode: IterationStatement): Node[] { - const keywords: Node[] = []; - - if (pushKeywordIf(keywords, loopNode.getFirstToken(), SyntaxKind.ForKeyword, SyntaxKind.WhileKeyword, SyntaxKind.DoKeyword)) { - // If we succeeded and got a do-while loop, then start looking for a 'while' keyword. - if (loopNode.kind === SyntaxKind.DoStatement) { - const loopTokens = loopNode.getChildren(); - - for (let i = loopTokens.length - 1; i >= 0; i--) { - if (pushKeywordIf(keywords, loopTokens[i], SyntaxKind.WhileKeyword)) { - break; - } - } - } + }); + return keywords; + } + function getBreakOrContinueStatementOccurrences(breakOrContinueStatement: BreakOrContinueStatement): Node[] | undefined { + const owner = getBreakOrContinueOwner(breakOrContinueStatement); + if (owner) { + switch (owner.kind) { + case SyntaxKind.ForStatement: + case SyntaxKind.ForInStatement: + case SyntaxKind.ForOfStatement: + case SyntaxKind.DoStatement: + case SyntaxKind.WhileStatement: + return getLoopBreakContinueOccurrences((owner)); + case SyntaxKind.SwitchStatement: + return getSwitchCaseDefaultOccurrences((owner)); } - - forEach(aggregateAllBreakAndContinueStatements(loopNode.statement), statement => { - if (ownsBreakOrContinueStatement(loopNode, statement)) { - pushKeywordIf(keywords, statement.getFirstToken(), SyntaxKind.BreakKeyword, SyntaxKind.ContinueKeyword); + } + return undefined; + } + function getSwitchCaseDefaultOccurrences(switchStatement: SwitchStatement): Node[] { + const keywords: Node[] = []; + pushKeywordIf(keywords, switchStatement.getFirstToken(), SyntaxKind.SwitchKeyword); + // Go through each clause in the switch statement, collecting the 'case'/'default' keywords. + forEach(switchStatement.caseBlock.clauses, clause => { + pushKeywordIf(keywords, clause.getFirstToken(), SyntaxKind.CaseKeyword, SyntaxKind.DefaultKeyword); + forEach(aggregateAllBreakAndContinueStatements(clause), statement => { + if (ownsBreakOrContinueStatement(switchStatement, statement)) { + pushKeywordIf(keywords, statement.getFirstToken(), SyntaxKind.BreakKeyword); } }); - - return keywords; + }); + return keywords; + } + function getTryCatchFinallyOccurrences(tryStatement: TryStatement, sourceFile: SourceFile): Node[] { + const keywords: Node[] = []; + pushKeywordIf(keywords, tryStatement.getFirstToken(), SyntaxKind.TryKeyword); + if (tryStatement.catchClause) { + pushKeywordIf(keywords, tryStatement.catchClause.getFirstToken(), SyntaxKind.CatchKeyword); } - - function getBreakOrContinueStatementOccurrences(breakOrContinueStatement: BreakOrContinueStatement): Node[] | undefined { - const owner = getBreakOrContinueOwner(breakOrContinueStatement); - - if (owner) { - switch (owner.kind) { - case SyntaxKind.ForStatement: - case SyntaxKind.ForInStatement: - case SyntaxKind.ForOfStatement: - case SyntaxKind.DoStatement: - case SyntaxKind.WhileStatement: - return getLoopBreakContinueOccurrences(owner); - case SyntaxKind.SwitchStatement: - return getSwitchCaseDefaultOccurrences(owner); - - } - } - + if (tryStatement.finallyBlock) { + const finallyKeyword = (findChildOfKind(tryStatement, SyntaxKind.FinallyKeyword, sourceFile)!); + pushKeywordIf(keywords, finallyKeyword, SyntaxKind.FinallyKeyword); + } + return keywords; + } + function getThrowOccurrences(throwStatement: ThrowStatement, sourceFile: SourceFile): Node[] | undefined { + const owner = getThrowStatementOwner(throwStatement); + if (!owner) { return undefined; } - - function getSwitchCaseDefaultOccurrences(switchStatement: SwitchStatement): Node[] { - const keywords: Node[] = []; - - pushKeywordIf(keywords, switchStatement.getFirstToken(), SyntaxKind.SwitchKeyword); - - // Go through each clause in the switch statement, collecting the 'case'/'default' keywords. - forEach(switchStatement.caseBlock.clauses, clause => { - pushKeywordIf(keywords, clause.getFirstToken(), SyntaxKind.CaseKeyword, SyntaxKind.DefaultKeyword); - - forEach(aggregateAllBreakAndContinueStatements(clause), statement => { - if (ownsBreakOrContinueStatement(switchStatement, statement)) { - pushKeywordIf(keywords, statement.getFirstToken(), SyntaxKind.BreakKeyword); - } - }); + const keywords: Node[] = []; + forEach(aggregateOwnedThrowStatements(owner), throwStatement => { + keywords.push((findChildOfKind(throwStatement, SyntaxKind.ThrowKeyword, sourceFile)!)); + }); + // If the "owner" is a function, then we equate 'return' and 'throw' statements in their + // ability to "jump out" of the function, and include occurrences for both. + if (isFunctionBlock(owner)) { + forEachReturnStatement((owner), returnStatement => { + keywords.push((findChildOfKind(returnStatement, SyntaxKind.ReturnKeyword, sourceFile)!)); }); - - return keywords; } - - function getTryCatchFinallyOccurrences(tryStatement: TryStatement, sourceFile: SourceFile): Node[] { - const keywords: Node[] = []; - - pushKeywordIf(keywords, tryStatement.getFirstToken(), SyntaxKind.TryKeyword); - - if (tryStatement.catchClause) { - pushKeywordIf(keywords, tryStatement.catchClause.getFirstToken(), SyntaxKind.CatchKeyword); - } - - if (tryStatement.finallyBlock) { - const finallyKeyword = findChildOfKind(tryStatement, SyntaxKind.FinallyKeyword, sourceFile)!; - pushKeywordIf(keywords, finallyKeyword, SyntaxKind.FinallyKeyword); - } - - return keywords; + return keywords; + } + function getReturnOccurrences(returnStatement: ReturnStatement, sourceFile: SourceFile): Node[] | undefined { + const func = (getContainingFunction(returnStatement)); + if (!func) { + return undefined; } - - function getThrowOccurrences(throwStatement: ThrowStatement, sourceFile: SourceFile): Node[] | undefined { - const owner = getThrowStatementOwner(throwStatement); - - if (!owner) { - return undefined; - } - - const keywords: Node[] = []; - - forEach(aggregateOwnedThrowStatements(owner), throwStatement => { - keywords.push(findChildOfKind(throwStatement, SyntaxKind.ThrowKeyword, sourceFile)!); - }); - - // If the "owner" is a function, then we equate 'return' and 'throw' statements in their - // ability to "jump out" of the function, and include occurrences for both. - if (isFunctionBlock(owner)) { - forEachReturnStatement(owner, returnStatement => { - keywords.push(findChildOfKind(returnStatement, SyntaxKind.ReturnKeyword, sourceFile)!); - }); - } - - return keywords; + const keywords: Node[] = []; + forEachReturnStatement(cast(func.body, isBlock), returnStatement => { + keywords.push((findChildOfKind(returnStatement, SyntaxKind.ReturnKeyword, sourceFile)!)); + }); + // Include 'throw' statements that do not occur within a try block. + forEach(aggregateOwnedThrowStatements(func.body!), throwStatement => { + keywords.push((findChildOfKind(throwStatement, SyntaxKind.ThrowKeyword, sourceFile)!)); + }); + return keywords; + } + function getAsyncAndAwaitOccurrences(node: Node): Node[] | undefined { + const func = (getContainingFunction(node)); + if (!func) { + return undefined; } - - function getReturnOccurrences(returnStatement: ReturnStatement, sourceFile: SourceFile): Node[] | undefined { - const func = getContainingFunction(returnStatement); - if (!func) { - return undefined; - } - - const keywords: Node[] = []; - forEachReturnStatement(cast(func.body, isBlock), returnStatement => { - keywords.push(findChildOfKind(returnStatement, SyntaxKind.ReturnKeyword, sourceFile)!); + const keywords: Node[] = []; + if (func.modifiers) { + func.modifiers.forEach(modifier => { + pushKeywordIf(keywords, modifier, SyntaxKind.AsyncKeyword); }); - - // Include 'throw' statements that do not occur within a try block. - forEach(aggregateOwnedThrowStatements(func.body!), throwStatement => { - keywords.push(findChildOfKind(throwStatement, SyntaxKind.ThrowKeyword, sourceFile)!); - }); - - return keywords; } - - function getAsyncAndAwaitOccurrences(node: Node): Node[] | undefined { - const func = getContainingFunction(node); - if (!func) { - return undefined; - } - - const keywords: Node[] = []; - - if (func.modifiers) { - func.modifiers.forEach(modifier => { - pushKeywordIf(keywords, modifier, SyntaxKind.AsyncKeyword); - }); - } - - forEachChild(func, child => { - traverseWithoutCrossingFunction(child, node => { - if (isAwaitExpression(node)) { - pushKeywordIf(keywords, node.getFirstToken(), SyntaxKind.AwaitKeyword); - } - }); + forEachChild(func, child => { + traverseWithoutCrossingFunction(child, node => { + if (isAwaitExpression(node)) { + pushKeywordIf(keywords, node.getFirstToken(), SyntaxKind.AwaitKeyword); + } }); - - - return keywords; + }); + return keywords; + } + function getYieldOccurrences(node: Node): Node[] | undefined { + const func = (getContainingFunction(node) as FunctionDeclaration); + if (!func) { + return undefined; } - - function getYieldOccurrences(node: Node): Node[] | undefined { - const func = getContainingFunction(node) as FunctionDeclaration; - if (!func) { - return undefined; - } - - const keywords: Node[] = []; - - forEachChild(func, child => { - traverseWithoutCrossingFunction(child, node => { - if (isYieldExpression(node)) { - pushKeywordIf(keywords, node.getFirstToken(), SyntaxKind.YieldKeyword); - } - }); + const keywords: Node[] = []; + forEachChild(func, child => { + traverseWithoutCrossingFunction(child, node => { + if (isYieldExpression(node)) { + pushKeywordIf(keywords, node.getFirstToken(), SyntaxKind.YieldKeyword); + } }); - - return keywords; - } - - // Do not cross function/class/interface/module/type boundaries. - function traverseWithoutCrossingFunction(node: Node, cb: (node: Node) => void) { - cb(node); - if (!isFunctionLike(node) && !isClassLike(node) && !isInterfaceDeclaration(node) && !isModuleDeclaration(node) && !isTypeAliasDeclaration(node) && !isTypeNode(node)) { - forEachChild(node, child => traverseWithoutCrossingFunction(child, cb)); - } + }); + return keywords; + } + // Do not cross function/class/interface/module/type boundaries. + function traverseWithoutCrossingFunction(node: Node, cb: (node: Node) => void) { + cb(node); + if (!isFunctionLike(node) && !isClassLike(node) && !isInterfaceDeclaration(node) && !isModuleDeclaration(node) && !isTypeAliasDeclaration(node) && !isTypeNode(node)) { + forEachChild(node, child => traverseWithoutCrossingFunction(child, cb)); } - - function getIfElseOccurrences(ifStatement: IfStatement, sourceFile: SourceFile): HighlightSpan[] { - const keywords = getIfElseKeywords(ifStatement, sourceFile); - const result: HighlightSpan[] = []; - - // We'd like to highlight else/ifs together if they are only separated by whitespace - // (i.e. the keywords are separated by no comments, no newlines). - for (let i = 0; i < keywords.length; i++) { - if (keywords[i].kind === SyntaxKind.ElseKeyword && i < keywords.length - 1) { - const elseKeyword = keywords[i]; - const ifKeyword = keywords[i + 1]; // this *should* always be an 'if' keyword. - - let shouldCombineElseAndIf = true; - - // Avoid recalculating getStart() by iterating backwards. - for (let j = ifKeyword.getStart(sourceFile) - 1; j >= elseKeyword.end; j--) { - if (!isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(j))) { - shouldCombineElseAndIf = false; - break; - } - } - - if (shouldCombineElseAndIf) { - result.push({ - fileName: sourceFile.fileName, - textSpan: createTextSpanFromBounds(elseKeyword.getStart(), ifKeyword.end), - kind: HighlightSpanKind.reference - }); - i++; // skip the next keyword - continue; + } + function getIfElseOccurrences(ifStatement: IfStatement, sourceFile: SourceFile): HighlightSpan[] { + const keywords = getIfElseKeywords(ifStatement, sourceFile); + const result: HighlightSpan[] = []; + // We'd like to highlight else/ifs together if they are only separated by whitespace + // (i.e. the keywords are separated by no comments, no newlines). + for (let i = 0; i < keywords.length; i++) { + if (keywords[i].kind === SyntaxKind.ElseKeyword && i < keywords.length - 1) { + const elseKeyword = keywords[i]; + const ifKeyword = keywords[i + 1]; // this *should* always be an 'if' keyword. + let shouldCombineElseAndIf = true; + // Avoid recalculating getStart() by iterating backwards. + for (let j = ifKeyword.getStart(sourceFile) - 1; j >= elseKeyword.end; j--) { + if (!isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(j))) { + shouldCombineElseAndIf = false; + break; } } - - // Ordinary case: just highlight the keyword. - result.push(getHighlightSpanForNode(keywords[i], sourceFile)); + if (shouldCombineElseAndIf) { + result.push({ + fileName: sourceFile.fileName, + textSpan: createTextSpanFromBounds(elseKeyword.getStart(), ifKeyword.end), + kind: HighlightSpanKind.reference + }); + i++; // skip the next keyword + continue; + } } - - return result; + // Ordinary case: just highlight the keyword. + result.push(getHighlightSpanForNode(keywords[i], sourceFile)); } - - function getIfElseKeywords(ifStatement: IfStatement, sourceFile: SourceFile): Node[] { - const keywords: Node[] = []; - - // Traverse upwards through all parent if-statements linked by their else-branches. - while (isIfStatement(ifStatement.parent) && ifStatement.parent.elseStatement === ifStatement) { - ifStatement = ifStatement.parent; - } - - // Now traverse back down through the else branches, aggregating if/else keywords of if-statements. - while (true) { - const children = ifStatement.getChildren(sourceFile); - pushKeywordIf(keywords, children[0], SyntaxKind.IfKeyword); - - // Generally the 'else' keyword is second-to-last, so we traverse backwards. - for (let i = children.length - 1; i >= 0; i--) { - if (pushKeywordIf(keywords, children[i], SyntaxKind.ElseKeyword)) { - break; - } - } - - if (!ifStatement.elseStatement || !isIfStatement(ifStatement.elseStatement)) { + return result; + } + function getIfElseKeywords(ifStatement: IfStatement, sourceFile: SourceFile): Node[] { + const keywords: Node[] = []; + // Traverse upwards through all parent if-statements linked by their else-branches. + while (isIfStatement(ifStatement.parent) && ifStatement.parent.elseStatement === ifStatement) { + ifStatement = ifStatement.parent; + } + // Now traverse back down through the else branches, aggregating if/else keywords of if-statements. + while (true) { + const children = ifStatement.getChildren(sourceFile); + pushKeywordIf(keywords, children[0], SyntaxKind.IfKeyword); + // Generally the 'else' keyword is second-to-last, so we traverse backwards. + for (let i = children.length - 1; i >= 0; i--) { + if (pushKeywordIf(keywords, children[i], SyntaxKind.ElseKeyword)) { break; } - - ifStatement = ifStatement.elseStatement; } - - return keywords; - } - - /** - * Whether or not a 'node' is preceded by a label of the given string. - * Note: 'node' cannot be a SourceFile. - */ - function isLabeledBy(node: Node, labelName: __String): boolean { - return !!findAncestor(node.parent, owner => !isLabeledStatement(owner) ? "quit" : owner.label.escapedText === labelName); + if (!ifStatement.elseStatement || !isIfStatement(ifStatement.elseStatement)) { + break; + } + ifStatement = ifStatement.elseStatement; } + return keywords; + } + /** + * Whether or not a 'node' is preceded by a label of the given string. + * Note: 'node' cannot be a SourceFile. + */ + function isLabeledBy(node: Node, labelName: __String): boolean { + return !!findAncestor(node.parent, owner => !isLabeledStatement(owner) ? "quit" : owner.label.escapedText === labelName); } } diff --git a/src/services/documentRegistry.ts b/src/services/documentRegistry.ts index decb66b5340e4..e1d23e031fff3 100644 --- a/src/services/documentRegistry.ts +++ b/src/services/documentRegistry.ts @@ -1,266 +1,209 @@ -namespace ts { +import { CompilerOptions, IScriptSnapshot, ScriptKind, SourceFile, Path, createMap, createGetCanonicalFileName, arrayFrom, toPath, getOrUpdate, ScriptTarget, Debug, createLanguageServiceSourceFile, updateLanguageServiceSourceFile, sourceFileAffectingCompilerOptions, getCompilerOptionValue } from "./ts"; +import * as ts from "./ts"; +/** + * The document registry represents a store of SourceFile objects that can be shared between + * multiple LanguageService instances. A LanguageService instance holds on the SourceFile (AST) + * of files in the context. + * SourceFile objects account for most of the memory usage by the language service. Sharing + * the same DocumentRegistry instance between different instances of LanguageService allow + * for more efficient memory utilization since all projects will share at least the library + * file (lib.d.ts). + * + * A more advanced use of the document registry is to serialize sourceFile objects to disk + * and re-hydrate them when needed. + * + * To create a default DocumentRegistry, use createDocumentRegistry to create one, and pass it + * to all subsequent createLanguageService calls. + */ +export interface DocumentRegistry { /** - * The document registry represents a store of SourceFile objects that can be shared between - * multiple LanguageService instances. A LanguageService instance holds on the SourceFile (AST) - * of files in the context. - * SourceFile objects account for most of the memory usage by the language service. Sharing - * the same DocumentRegistry instance between different instances of LanguageService allow - * for more efficient memory utilization since all projects will share at least the library - * file (lib.d.ts). + * Request a stored SourceFile with a given fileName and compilationSettings. + * The first call to acquire will call createLanguageServiceSourceFile to generate + * the SourceFile if was not found in the registry. * - * A more advanced use of the document registry is to serialize sourceFile objects to disk - * and re-hydrate them when needed. + * @param fileName The name of the file requested + * @param compilationSettings Some compilation settings like target affects the + * shape of a the resulting SourceFile. This allows the DocumentRegistry to store + * multiple copies of the same file for different compilation settings. + * @param scriptSnapshot Text of the file. Only used if the file was not found + * in the registry and a new one was created. + * @param version Current version of the file. Only used if the file was not found + * in the registry and a new one was created. + */ + acquireDocument(fileName: string, compilationSettings: CompilerOptions, scriptSnapshot: IScriptSnapshot, version: string, scriptKind?: ScriptKind): SourceFile; + acquireDocumentWithKey(fileName: string, path: Path, compilationSettings: CompilerOptions, key: DocumentRegistryBucketKey, scriptSnapshot: IScriptSnapshot, version: string, scriptKind?: ScriptKind): SourceFile; + /** + * Request an updated version of an already existing SourceFile with a given fileName + * and compilationSettings. The update will in-turn call updateLanguageServiceSourceFile + * to get an updated SourceFile. * - * To create a default DocumentRegistry, use createDocumentRegistry to create one, and pass it - * to all subsequent createLanguageService calls. + * @param fileName The name of the file requested + * @param compilationSettings Some compilation settings like target affects the + * shape of a the resulting SourceFile. This allows the DocumentRegistry to store + * multiple copies of the same file for different compilation settings. + * @param scriptSnapshot Text of the file. + * @param version Current version of the file. */ - export interface DocumentRegistry { - /** - * Request a stored SourceFile with a given fileName and compilationSettings. - * The first call to acquire will call createLanguageServiceSourceFile to generate - * the SourceFile if was not found in the registry. - * - * @param fileName The name of the file requested - * @param compilationSettings Some compilation settings like target affects the - * shape of a the resulting SourceFile. This allows the DocumentRegistry to store - * multiple copies of the same file for different compilation settings. - * @param scriptSnapshot Text of the file. Only used if the file was not found - * in the registry and a new one was created. - * @param version Current version of the file. Only used if the file was not found - * in the registry and a new one was created. - */ - acquireDocument( - fileName: string, - compilationSettings: CompilerOptions, - scriptSnapshot: IScriptSnapshot, - version: string, - scriptKind?: ScriptKind): SourceFile; - - acquireDocumentWithKey( - fileName: string, - path: Path, - compilationSettings: CompilerOptions, - key: DocumentRegistryBucketKey, - scriptSnapshot: IScriptSnapshot, - version: string, - scriptKind?: ScriptKind): SourceFile; - - /** - * Request an updated version of an already existing SourceFile with a given fileName - * and compilationSettings. The update will in-turn call updateLanguageServiceSourceFile - * to get an updated SourceFile. - * - * @param fileName The name of the file requested - * @param compilationSettings Some compilation settings like target affects the - * shape of a the resulting SourceFile. This allows the DocumentRegistry to store - * multiple copies of the same file for different compilation settings. - * @param scriptSnapshot Text of the file. - * @param version Current version of the file. - */ - updateDocument( - fileName: string, - compilationSettings: CompilerOptions, - scriptSnapshot: IScriptSnapshot, - version: string, - scriptKind?: ScriptKind): SourceFile; - - updateDocumentWithKey( - fileName: string, - path: Path, - compilationSettings: CompilerOptions, - key: DocumentRegistryBucketKey, - scriptSnapshot: IScriptSnapshot, - version: string, - scriptKind?: ScriptKind): SourceFile; - - getKeyForCompilationSettings(settings: CompilerOptions): DocumentRegistryBucketKey; - /** - * Informs the DocumentRegistry that a file is not needed any longer. - * - * Note: It is not allowed to call release on a SourceFile that was not acquired from - * this registry originally. - * - * @param fileName The name of the file to be released - * @param compilationSettings The compilation settings used to acquire the file - */ - releaseDocument(fileName: string, compilationSettings: CompilerOptions): void; - - releaseDocumentWithKey(path: Path, key: DocumentRegistryBucketKey): void; - - /*@internal*/ - getLanguageServiceRefCounts(path: Path): [string, number | undefined][]; - - reportStats(): string; - } - + updateDocument(fileName: string, compilationSettings: CompilerOptions, scriptSnapshot: IScriptSnapshot, version: string, scriptKind?: ScriptKind): SourceFile; + updateDocumentWithKey(fileName: string, path: Path, compilationSettings: CompilerOptions, key: DocumentRegistryBucketKey, scriptSnapshot: IScriptSnapshot, version: string, scriptKind?: ScriptKind): SourceFile; + getKeyForCompilationSettings(settings: CompilerOptions): DocumentRegistryBucketKey; + /** + * Informs the DocumentRegistry that a file is not needed any longer. + * + * Note: It is not allowed to call release on a SourceFile that was not acquired from + * this registry originally. + * + * @param fileName The name of the file to be released + * @param compilationSettings The compilation settings used to acquire the file + */ + releaseDocument(fileName: string, compilationSettings: CompilerOptions): void; + releaseDocumentWithKey(path: Path, key: DocumentRegistryBucketKey): void; /*@internal*/ - export interface ExternalDocumentCache { - setDocument(key: DocumentRegistryBucketKey, path: Path, sourceFile: SourceFile): void; - getDocument(key: DocumentRegistryBucketKey, path: Path): SourceFile | undefined; + getLanguageServiceRefCounts(path: Path): [string, number | undefined][]; + reportStats(): string; +} +/*@internal*/ +export interface ExternalDocumentCache { + setDocument(key: DocumentRegistryBucketKey, path: Path, sourceFile: SourceFile): void; + getDocument(key: DocumentRegistryBucketKey, path: Path): SourceFile | undefined; +} +export type DocumentRegistryBucketKey = string & { + __bucketKey: any; +}; +interface DocumentRegistryEntry { + sourceFile: SourceFile; + // The number of language services that this source file is referenced in. When no more + // language services are referencing the file, then the file can be removed from the + // registry. + languageServiceRefCount: number; +} +export function createDocumentRegistry(useCaseSensitiveFileNames?: boolean, currentDirectory?: string): DocumentRegistry { + return createDocumentRegistryInternal(useCaseSensitiveFileNames, currentDirectory); +} +/*@internal*/ +export function createDocumentRegistryInternal(useCaseSensitiveFileNames?: boolean, currentDirectory = "", externalCache?: ExternalDocumentCache): DocumentRegistry { + // Maps from compiler setting target (ES3, ES5, etc.) to all the cached documents we have + // for those settings. + const buckets = createMap>(); + const getCanonicalFileName = createGetCanonicalFileName(!!useCaseSensitiveFileNames); + function reportStats() { + const bucketInfoArray = arrayFrom(buckets.keys()).filter(name => name && name.charAt(0) === "_").map(name => { + const entries = buckets.get(name)!; + const sourceFiles: { + name: string; + refCount: number; + }[] = []; + entries.forEach((entry, name) => { + sourceFiles.push({ + name, + refCount: entry.languageServiceRefCount + }); + }); + sourceFiles.sort((x, y) => y.refCount - x.refCount); + return { + bucket: name, + sourceFiles + }; + }); + return JSON.stringify(bucketInfoArray, undefined, 2); } - - export type DocumentRegistryBucketKey = string & { __bucketKey: any }; - - interface DocumentRegistryEntry { - sourceFile: SourceFile; - - // The number of language services that this source file is referenced in. When no more - // language services are referencing the file, then the file can be removed from the - // registry. - languageServiceRefCount: number; + function acquireDocument(fileName: string, compilationSettings: CompilerOptions, scriptSnapshot: IScriptSnapshot, version: string, scriptKind?: ScriptKind): SourceFile { + const path = toPath(fileName, currentDirectory, getCanonicalFileName); + const key = getKeyForCompilationSettings(compilationSettings); + return acquireDocumentWithKey(fileName, path, compilationSettings, key, scriptSnapshot, version, scriptKind); } - - export function createDocumentRegistry(useCaseSensitiveFileNames?: boolean, currentDirectory?: string): DocumentRegistry { - return createDocumentRegistryInternal(useCaseSensitiveFileNames, currentDirectory); + function acquireDocumentWithKey(fileName: string, path: Path, compilationSettings: CompilerOptions, key: DocumentRegistryBucketKey, scriptSnapshot: IScriptSnapshot, version: string, scriptKind?: ScriptKind): SourceFile { + return acquireOrUpdateDocument(fileName, path, compilationSettings, key, scriptSnapshot, version, /*acquiring*/ true, scriptKind); } - - /*@internal*/ - export function createDocumentRegistryInternal(useCaseSensitiveFileNames?: boolean, currentDirectory = "", externalCache?: ExternalDocumentCache): DocumentRegistry { - // Maps from compiler setting target (ES3, ES5, etc.) to all the cached documents we have - // for those settings. - const buckets = createMap>(); - const getCanonicalFileName = createGetCanonicalFileName(!!useCaseSensitiveFileNames); - - function reportStats() { - const bucketInfoArray = arrayFrom(buckets.keys()).filter(name => name && name.charAt(0) === "_").map(name => { - const entries = buckets.get(name)!; - const sourceFiles: { name: string; refCount: number; }[] = []; - entries.forEach((entry, name) => { - sourceFiles.push({ - name, - refCount: entry.languageServiceRefCount - }); - }); - sourceFiles.sort((x, y) => y.refCount - x.refCount); - return { - bucket: name, - sourceFiles - }; - }); - return JSON.stringify(bucketInfoArray, undefined, 2); - } - - function acquireDocument(fileName: string, compilationSettings: CompilerOptions, scriptSnapshot: IScriptSnapshot, version: string, scriptKind?: ScriptKind): SourceFile { - const path = toPath(fileName, currentDirectory, getCanonicalFileName); - const key = getKeyForCompilationSettings(compilationSettings); - return acquireDocumentWithKey(fileName, path, compilationSettings, key, scriptSnapshot, version, scriptKind); - } - - function acquireDocumentWithKey(fileName: string, path: Path, compilationSettings: CompilerOptions, key: DocumentRegistryBucketKey, scriptSnapshot: IScriptSnapshot, version: string, scriptKind?: ScriptKind): SourceFile { - return acquireOrUpdateDocument(fileName, path, compilationSettings, key, scriptSnapshot, version, /*acquiring*/ true, scriptKind); - } - - function updateDocument(fileName: string, compilationSettings: CompilerOptions, scriptSnapshot: IScriptSnapshot, version: string, scriptKind?: ScriptKind): SourceFile { - const path = toPath(fileName, currentDirectory, getCanonicalFileName); - const key = getKeyForCompilationSettings(compilationSettings); - return updateDocumentWithKey(fileName, path, compilationSettings, key, scriptSnapshot, version, scriptKind); - } - - function updateDocumentWithKey(fileName: string, path: Path, compilationSettings: CompilerOptions, key: DocumentRegistryBucketKey, scriptSnapshot: IScriptSnapshot, version: string, scriptKind?: ScriptKind): SourceFile { - return acquireOrUpdateDocument(fileName, path, compilationSettings, key, scriptSnapshot, version, /*acquiring*/ false, scriptKind); - } - - function acquireOrUpdateDocument( - fileName: string, - path: Path, - compilationSettings: CompilerOptions, - key: DocumentRegistryBucketKey, - scriptSnapshot: IScriptSnapshot, - version: string, - acquiring: boolean, - scriptKind?: ScriptKind): SourceFile { - - const bucket = getOrUpdate>(buckets, key, createMap); - let entry = bucket.get(path); - const scriptTarget = scriptKind === ScriptKind.JSON ? ScriptTarget.JSON : compilationSettings.target || ScriptTarget.ES5; - if (!entry && externalCache) { - const sourceFile = externalCache.getDocument(key, path); - if (sourceFile) { - Debug.assert(acquiring); - entry = { - sourceFile, - languageServiceRefCount: 0 - }; - bucket.set(path, entry); - } - } - - if (!entry) { - // Have never seen this file with these settings. Create a new source file for it. - const sourceFile = createLanguageServiceSourceFile(fileName, scriptSnapshot, scriptTarget, version, /*setNodeParents*/ false, scriptKind); - if (externalCache) { - externalCache.setDocument(key, path, sourceFile); - } + function updateDocument(fileName: string, compilationSettings: CompilerOptions, scriptSnapshot: IScriptSnapshot, version: string, scriptKind?: ScriptKind): SourceFile { + const path = toPath(fileName, currentDirectory, getCanonicalFileName); + const key = getKeyForCompilationSettings(compilationSettings); + return updateDocumentWithKey(fileName, path, compilationSettings, key, scriptSnapshot, version, scriptKind); + } + function updateDocumentWithKey(fileName: string, path: Path, compilationSettings: CompilerOptions, key: DocumentRegistryBucketKey, scriptSnapshot: IScriptSnapshot, version: string, scriptKind?: ScriptKind): SourceFile { + return acquireOrUpdateDocument(fileName, path, compilationSettings, key, scriptSnapshot, version, /*acquiring*/ false, scriptKind); + } + function acquireOrUpdateDocument(fileName: string, path: Path, compilationSettings: CompilerOptions, key: DocumentRegistryBucketKey, scriptSnapshot: IScriptSnapshot, version: string, acquiring: boolean, scriptKind?: ScriptKind): SourceFile { + const bucket = getOrUpdate>(buckets, key, createMap); + let entry = bucket.get(path); + const scriptTarget = scriptKind === ScriptKind.JSON ? ScriptTarget.JSON : compilationSettings.target || ScriptTarget.ES5; + if (!entry && externalCache) { + const sourceFile = externalCache.getDocument(key, path); + if (sourceFile) { + Debug.assert(acquiring); entry = { sourceFile, - languageServiceRefCount: 1, + languageServiceRefCount: 0 }; bucket.set(path, entry); } - else { - // We have an entry for this file. However, it may be for a different version of - // the script snapshot. If so, update it appropriately. Otherwise, we can just - // return it as is. - if (entry.sourceFile.version !== version) { - entry.sourceFile = updateLanguageServiceSourceFile(entry.sourceFile, scriptSnapshot, version, - scriptSnapshot.getChangeRange(entry.sourceFile.scriptSnapshot!)); // TODO: GH#18217 - if (externalCache) { - externalCache.setDocument(key, path, entry.sourceFile); - } - } - - // If we're acquiring, then this is the first time this LS is asking for this document. - // Increase our ref count so we know there's another LS using the document. If we're - // not acquiring, then that means the LS is 'updating' the file instead, and that means - // it has already acquired the document previously. As such, we do not need to increase - // the ref count. - if (acquiring) { - entry.languageServiceRefCount++; - } - } - Debug.assert(entry.languageServiceRefCount !== 0); - - return entry.sourceFile; } - - function releaseDocument(fileName: string, compilationSettings: CompilerOptions): void { - const path = toPath(fileName, currentDirectory, getCanonicalFileName); - const key = getKeyForCompilationSettings(compilationSettings); - return releaseDocumentWithKey(path, key); + if (!entry) { + // Have never seen this file with these settings. Create a new source file for it. + const sourceFile = createLanguageServiceSourceFile(fileName, scriptSnapshot, scriptTarget, version, /*setNodeParents*/ false, scriptKind); + if (externalCache) { + externalCache.setDocument(key, path, sourceFile); + } + entry = { + sourceFile, + languageServiceRefCount: 1, + }; + bucket.set(path, entry); } - - function releaseDocumentWithKey(path: Path, key: DocumentRegistryBucketKey): void { - const bucket = Debug.checkDefined(buckets.get(key)); - const entry = bucket.get(path)!; - entry.languageServiceRefCount--; - - Debug.assert(entry.languageServiceRefCount >= 0); - if (entry.languageServiceRefCount === 0) { - bucket.delete(path); + else { + // We have an entry for this file. However, it may be for a different version of + // the script snapshot. If so, update it appropriately. Otherwise, we can just + // return it as is. + if (entry.sourceFile.version !== version) { + entry.sourceFile = updateLanguageServiceSourceFile(entry.sourceFile, scriptSnapshot, version, scriptSnapshot.getChangeRange(entry.sourceFile.scriptSnapshot!)); // TODO: GH#18217 + if (externalCache) { + externalCache.setDocument(key, path, entry.sourceFile); + } + } + // If we're acquiring, then this is the first time this LS is asking for this document. + // Increase our ref count so we know there's another LS using the document. If we're + // not acquiring, then that means the LS is 'updating' the file instead, and that means + // it has already acquired the document previously. As such, we do not need to increase + // the ref count. + if (acquiring) { + entry.languageServiceRefCount++; } } - - function getLanguageServiceRefCounts(path: Path) { - return arrayFrom(buckets.entries(), ([key, bucket]): [string, number | undefined] => { - const entry = bucket.get(path); - return [key, entry && entry.languageServiceRefCount]; - }); + Debug.assert(entry.languageServiceRefCount !== 0); + return entry.sourceFile; + } + function releaseDocument(fileName: string, compilationSettings: CompilerOptions): void { + const path = toPath(fileName, currentDirectory, getCanonicalFileName); + const key = getKeyForCompilationSettings(compilationSettings); + return releaseDocumentWithKey(path, key); + } + function releaseDocumentWithKey(path: Path, key: DocumentRegistryBucketKey): void { + const bucket = Debug.checkDefined(buckets.get(key)); + const entry = bucket.get(path)!; + entry.languageServiceRefCount--; + Debug.assert(entry.languageServiceRefCount >= 0); + if (entry.languageServiceRefCount === 0) { + bucket.delete(path); } - - return { - acquireDocument, - acquireDocumentWithKey, - updateDocument, - updateDocumentWithKey, - releaseDocument, - releaseDocumentWithKey, - getLanguageServiceRefCounts, - reportStats, - getKeyForCompilationSettings - }; } - - function getKeyForCompilationSettings(settings: CompilerOptions): DocumentRegistryBucketKey { - return sourceFileAffectingCompilerOptions.map(option => getCompilerOptionValue(settings, option)).join("|") as DocumentRegistryBucketKey; + function getLanguageServiceRefCounts(path: Path) { + return arrayFrom(buckets.entries(), ([key, bucket]): [string, number | undefined] => { + const entry = bucket.get(path); + return [key, entry && entry.languageServiceRefCount]; + }); } + return { + acquireDocument, + acquireDocumentWithKey, + updateDocument, + updateDocumentWithKey, + releaseDocument, + releaseDocumentWithKey, + getLanguageServiceRefCounts, + reportStats, + getKeyForCompilationSettings + }; +} +function getKeyForCompilationSettings(settings: CompilerOptions): DocumentRegistryBucketKey { + return sourceFileAffectingCompilerOptions.map(option => getCompilerOptionValue(settings, option)).join("|") as DocumentRegistryBucketKey; } diff --git a/src/services/exportAsModule.ts b/src/services/exportAsModule.ts index 6760e9c38c2d0..eb3e66f8b57c9 100644 --- a/src/services/exportAsModule.ts +++ b/src/services/exportAsModule.ts @@ -1,7 +1,13 @@ -// Here we expose the TypeScript services as an external module -// so that it may be consumed easily like a node module. -// @ts-ignore -/* @internal */ declare const module: { exports: {} }; +import * as ts from "./ts"; +/* @internal */ +declare global { + // Here we expose the TypeScript services as an external module + // so that it may be consumed easily like a node module. + // @ts-ignore + /* @internal */ const module: { + exports: {}; + }; +} if (typeof module !== "undefined" && module.exports) { module.exports = ts; -} \ No newline at end of file +} diff --git a/src/services/findAllReferences.ts b/src/services/findAllReferences.ts index 4682bd29fe3a3..311500267d897 100644 --- a/src/services/findAllReferences.ts +++ b/src/services/findAllReferences.ts @@ -1,2091 +1,1912 @@ +import { Symbol, Identifier, Node, StringLiteral, TextSpan, NamedDeclaration, isDeclaration, isExportAssignment, isInJSFile, isBinaryExpression, isAccessExpression, getAssignmentDeclarationKind, AssignmentDeclarationKind, isJsxOpeningElement, isJsxClosingElement, isJsxSelfClosingElement, isLabeledStatement, isBreakOrContinueStatement, isStringLiteralLike, tryGetImportFromModuleSpecifier, findAncestor, isStatement, isJSDocTag, Statement, JSDocTag, isComputedPropertyName, isConstructorDeclaration, isImportOrExportSpecifier, isBindingElement, SyntaxKind, hasModifier, ModifierFlags, BinaryExpression, ForInOrOfStatement, isVariableDeclarationList, isVariableStatement, isForInOrOfStatement, isExpressionStatement, isArrayLiteralOrObjectLiteralDestructuringPattern, SourceFile, Program, CancellationToken, ReferencedSymbol, getTouchingPropertyName, mapDefined, ImplementationLocation, map, isSuperProperty, TypeChecker, arrayToSet, flatMap, ReferencedSymbolDefinitionInfo, first, getNameOfDeclaration, ScriptElementKind, displayPart, SymbolDisplayPartKind, tokenToString, getContainerNode, textPart, getTextOfNode, Debug, SymbolDisplayPart, firstOrUndefined, RenameLocation, ReferenceEntry, DocumentSpan, isIdentifier, isShorthandPropertyAssignment, isObjectBindingElementWithoutPropertyName, isImportSpecifier, isExportSpecifier, contains, emptyOptions, punctuationPart, getNodeKind, HighlightSpan, HighlightSpanKind, createTextSpanFromBounds, getDeclarationFromName, isWriteAccess, isLiteralComputedPropertyDeclarationName, Declaration, NodeFlags, PropertyAssignment, FunctionDeclaration, FunctionExpression, ConstructorDeclaration, MethodDeclaration, GetAccessorDeclaration, SetAccessorDeclaration, VariableDeclaration, PropertyDeclaration, isCatchClause, getAdjustedReferenceLocation, getAdjustedRenameLocation, isSourceFile, isStringLiteral, InternalSymbolName, SymbolFlags, isNamespaceExportDeclaration, find, skipAlias, findIndex, compareValues, isLiteralTypeNode, cast, isImportTypeNode, createTextSpanFromRange, ModuleDeclaration, isPropertyAccessExpression, findChildOfKind, emptyArray, isTypeOperatorNode, isTypeKeyword, isVoidExpression, isJumpStatementTarget, getTargetLabel, isLabelOfLabeledStatement, isThis, SemanticMeaning, isClassLike, firstDefined, isTypeLiteralNode, isUnionTypeNode, __String, isModuleDeclaration, createMap, nodeSeenTracker, Push, stripQuotes, symbolName, getLocalSymbolForExportDefault, escapeLeadingUnderscores, getSymbolId, getNodeId, addToSeen, getNameTable, isPrivateIdentifierPropertyDeclaration, getAncestor, isExternalModuleSymbol, isExternalOrCommonJsModule, isParameterPropertyDeclaration, SignatureDeclaration, CallExpression, climbPastPropertyAccess, isCallExpression, isIdentifierPart, ScriptTarget, PrivateIdentifier, StringLiteralLike, isLiteralNameOfPropertyDeclarationOrIndexAccess, isNameOfModuleDeclaration, isExpressionOfExternalModuleImportEqualsDeclaration, isBindableObjectDefinePropertyCall, NumericLiteral, getMeaningFromLocation, isInString, isInNonReferenceComment, createTextSpan, ExportSpecifier, isNewExpressionTarget, isMethodOrAccessor, isFunctionLike, ClassLikeDeclaration, isCallExpressionTarget, isDeclarationName, isQualifiedName, isTypeNode, isTypeElement, hasType, hasInitializer, FunctionLikeDeclaration, forEachReturnStatement, Block, isAssertionExpression, Expression, InterfaceDeclaration, isExpressionWithTypeArguments, tryCast, ParenthesizedExpression, getAllSuperTypeNodes, getSuperContainer, getModifierFlags, ParameterDeclaration, getThisContainer, isObjectLiteralMethod, isExternalModule, isParameter, getContainingObjectLiteralElement, getPropertySymbolsFromContextualType, getDeclarationOfKind, getPropertySymbolFromBindingElement, getPropertySymbolsFromBaseTypes, BindingElement, getCheckFlags, CheckFlags, getMeaningFromDeclaration, isInterfaceDeclaration, isTypeAliasDeclaration, isVariableLike, isFunctionLikeDeclaration, isModuleOrEnumDeclaration, forEachChild, tryGetClassExtendingExpressionWithTypeArguments, isRightSideOfPropertyAccess, PropertyAccessExpression } from "./ts"; +import { getSymbolDisplayPartsDocumentationAndSymbolKind } from "./ts.SymbolDisplay"; +import { getReferenceAtPosition } from "./ts.GoToDefinition"; +import { ModuleReference, findModuleReferences, ExportKind, ImportExport, ImportTracker, ExportInfo, ImportsResult, createImportTracker, getExportInfo, getImportOrExportSymbol } from "./ts.FindAllReferences"; +import * as ts from "./ts"; /* @internal */ -namespace ts.FindAllReferences { - export interface SymbolAndEntries { - readonly definition: Definition | undefined; - readonly references: readonly Entry[]; - } - - export const enum DefinitionKind { Symbol, Label, Keyword, This, String } - export type Definition = - | { readonly type: DefinitionKind.Symbol; readonly symbol: Symbol } - | { readonly type: DefinitionKind.Label; readonly node: Identifier } - | { readonly type: DefinitionKind.Keyword; readonly node: Node } - | { readonly type: DefinitionKind.This; readonly node: Node } - | { readonly type: DefinitionKind.String; readonly node: StringLiteral }; - - export const enum EntryKind { Span, Node, StringLiteral, SearchedLocalFoundProperty, SearchedPropertyFoundLocal } - export type NodeEntryKind = EntryKind.Node | EntryKind.StringLiteral | EntryKind.SearchedLocalFoundProperty | EntryKind.SearchedPropertyFoundLocal; - export type Entry = NodeEntry | SpanEntry; - export interface ContextWithStartAndEndNode { - start: Node; - end: Node; - } - export type ContextNode = Node | ContextWithStartAndEndNode; - export interface NodeEntry { - readonly kind: NodeEntryKind; - readonly node: Node; - readonly context?: ContextNode; - } - export interface SpanEntry { - readonly kind: EntryKind.Span; - readonly fileName: string; - readonly textSpan: TextSpan; - } - export function nodeEntry(node: Node, kind: NodeEntryKind = EntryKind.Node): NodeEntry { - return { - kind, - node: (node as NamedDeclaration).name || node, - context: getContextNodeForNodeEntry(node) - }; +export interface SymbolAndEntries { + readonly definition: Definition | undefined; + readonly references: readonly Entry[]; +} +/* @internal */ +export const enum DefinitionKind { + Symbol, + Label, + Keyword, + This, + String +} +/* @internal */ +export type Definition = { + readonly type: DefinitionKind.Symbol; + readonly symbol: Symbol; +} | { + readonly type: DefinitionKind.Label; + readonly node: Identifier; +} | { + readonly type: DefinitionKind.Keyword; + readonly node: Node; +} | { + readonly type: DefinitionKind.This; + readonly node: Node; +} | { + readonly type: DefinitionKind.String; + readonly node: StringLiteral; +}; +/* @internal */ +export const enum EntryKind { + Span, + Node, + StringLiteral, + SearchedLocalFoundProperty, + SearchedPropertyFoundLocal +} +/* @internal */ +export type NodeEntryKind = EntryKind.Node | EntryKind.StringLiteral | EntryKind.SearchedLocalFoundProperty | EntryKind.SearchedPropertyFoundLocal; +/* @internal */ +export type Entry = NodeEntry | SpanEntry; +/* @internal */ +export interface ContextWithStartAndEndNode { + start: Node; + end: Node; +} +/* @internal */ +export type ContextNode = Node | ContextWithStartAndEndNode; +/* @internal */ +export interface NodeEntry { + readonly kind: NodeEntryKind; + readonly node: Node; + readonly context?: ContextNode; +} +/* @internal */ +export interface SpanEntry { + readonly kind: EntryKind.Span; + readonly fileName: string; + readonly textSpan: TextSpan; +} +/* @internal */ +export function nodeEntry(node: Node, kind: NodeEntryKind = EntryKind.Node): NodeEntry { + return { + kind, + node: (node as NamedDeclaration).name || node, + context: getContextNodeForNodeEntry(node) + }; +} +/* @internal */ +export function isContextWithStartAndEndNode(node: ContextNode): node is ContextWithStartAndEndNode { + return node && (node as Node).kind === undefined; +} +/* @internal */ +function getContextNodeForNodeEntry(node: Node): ContextNode | undefined { + if (isDeclaration(node)) { + return getContextNode(node); } - - export function isContextWithStartAndEndNode(node: ContextNode): node is ContextWithStartAndEndNode { - return node && (node as Node).kind === undefined; - } - - function getContextNodeForNodeEntry(node: Node): ContextNode | undefined { - if (isDeclaration(node)) { - return getContextNode(node); - } - - if (!node.parent) return undefined; - - if (!isDeclaration(node.parent) && !isExportAssignment(node.parent)) { - // Special property assignment in javascript - if (isInJSFile(node)) { - const binaryExpression = isBinaryExpression(node.parent) ? - node.parent : - isAccessExpression(node.parent) && - isBinaryExpression(node.parent.parent) && - node.parent.parent.left === node.parent ? - node.parent.parent : - undefined; - if (binaryExpression && getAssignmentDeclarationKind(binaryExpression) !== AssignmentDeclarationKind.None) { - return getContextNode(binaryExpression); - } - } - - // Jsx Tags - if (isJsxOpeningElement(node.parent) || isJsxClosingElement(node.parent)) { - return node.parent.parent; - } - else if (isJsxSelfClosingElement(node.parent) || - isLabeledStatement(node.parent) || - isBreakOrContinueStatement(node.parent)) { - return node.parent; - } - else if (isStringLiteralLike(node)) { - const validImport = tryGetImportFromModuleSpecifier(node); - if (validImport) { - const declOrStatement = findAncestor(validImport, node => - isDeclaration(node) || - isStatement(node) || - isJSDocTag(node) - )! as NamedDeclaration | Statement | JSDocTag; - return isDeclaration(declOrStatement) ? - getContextNode(declOrStatement) : - declOrStatement; - } - } - - // Handle computed property name - const propertyName = findAncestor(node, isComputedPropertyName); - return propertyName ? - getContextNode(propertyName.parent) : - undefined; - } - - if (node.parent.name === node || // node is name of declaration, use parent - isConstructorDeclaration(node.parent) || - isExportAssignment(node.parent) || - // Property name of the import export specifier or binding pattern, use parent - ((isImportOrExportSpecifier(node.parent) || isBindingElement(node.parent)) - && node.parent.propertyName === node) || - // Is default export - (node.kind === SyntaxKind.DefaultKeyword && hasModifier(node.parent, ModifierFlags.ExportDefault))) { - return getContextNode(node.parent); - } - + if (!node.parent) return undefined; + if (!isDeclaration(node.parent) && !isExportAssignment(node.parent)) { + // Special property assignment in javascript + if (isInJSFile(node)) { + const binaryExpression = isBinaryExpression(node.parent) ? + node.parent : + isAccessExpression(node.parent) && + isBinaryExpression(node.parent.parent) && + node.parent.parent.left === node.parent ? + node.parent.parent : + undefined; + if (binaryExpression && getAssignmentDeclarationKind(binaryExpression) !== AssignmentDeclarationKind.None) { + return getContextNode(binaryExpression); + } + } + // Jsx Tags + if (isJsxOpeningElement(node.parent) || isJsxClosingElement(node.parent)) { + return node.parent.parent; + } + else if (isJsxSelfClosingElement(node.parent) || + isLabeledStatement(node.parent) || + isBreakOrContinueStatement(node.parent)) { + return node.parent; + } + else if (isStringLiteralLike(node)) { + const validImport = tryGetImportFromModuleSpecifier(node); + if (validImport) { + const declOrStatement = (findAncestor(validImport, node => isDeclaration(node) || + isStatement(node) || + isJSDocTag(node))! as NamedDeclaration | Statement | JSDocTag); + return isDeclaration(declOrStatement) ? + getContextNode(declOrStatement) : + declOrStatement; + } + } + // Handle computed property name + const propertyName = findAncestor(node, isComputedPropertyName); + return propertyName ? + getContextNode(propertyName.parent) : + undefined; } - - export function getContextNode(node: NamedDeclaration | BinaryExpression | ForInOrOfStatement | undefined): ContextNode | undefined { - if (!node) return undefined; - switch (node.kind) { - case SyntaxKind.VariableDeclaration: - return !isVariableDeclarationList(node.parent) || node.parent.declarations.length !== 1 ? - node : - isVariableStatement(node.parent.parent) ? - node.parent.parent : - isForInOrOfStatement(node.parent.parent) ? - getContextNode(node.parent.parent) : - node.parent; - - case SyntaxKind.BindingElement: - return getContextNode(node.parent.parent as NamedDeclaration); - - case SyntaxKind.ImportSpecifier: - return node.parent.parent.parent; - - case SyntaxKind.ExportSpecifier: - case SyntaxKind.NamespaceImport: - return node.parent.parent; - - case SyntaxKind.ImportClause: - return node.parent; - - case SyntaxKind.BinaryExpression: - return isExpressionStatement(node.parent) ? - node.parent : - node; - - case SyntaxKind.ForOfStatement: - case SyntaxKind.ForInStatement: - return { - start: (node as ForInOrOfStatement).initializer, - end: (node as ForInOrOfStatement).expression - }; - - case SyntaxKind.PropertyAssignment: - case SyntaxKind.ShorthandPropertyAssignment: - return isArrayLiteralOrObjectLiteralDestructuringPattern(node.parent) ? - getContextNode( - findAncestor(node.parent, node => - isBinaryExpression(node) || isForInOrOfStatement(node) - ) as BinaryExpression | ForInOrOfStatement - ) : - node; - - default: - return node; - } + if (node.parent.name === node || // node is name of declaration, use parent + isConstructorDeclaration(node.parent) || + isExportAssignment(node.parent) || + // Property name of the import export specifier or binding pattern, use parent + ((isImportOrExportSpecifier(node.parent) || isBindingElement(node.parent)) + && node.parent.propertyName === node) || + // Is default export + (node.kind === SyntaxKind.DefaultKeyword && hasModifier(node.parent, ModifierFlags.ExportDefault))) { + return getContextNode(node.parent); } - - export function toContextSpan(textSpan: TextSpan, sourceFile: SourceFile, context?: ContextNode): { contextSpan: TextSpan } | undefined { - if (!context) return undefined; - const contextSpan = isContextWithStartAndEndNode(context) ? - getTextSpan(context.start, sourceFile, context.end) : - getTextSpan(context, sourceFile); - return contextSpan.start !== textSpan.start || contextSpan.length !== textSpan.length ? - { contextSpan } : - undefined; + return undefined; +} +/* @internal */ +export function getContextNode(node: NamedDeclaration | BinaryExpression | ForInOrOfStatement | undefined): ContextNode | undefined { + if (!node) + return undefined; + switch (node.kind) { + case SyntaxKind.VariableDeclaration: + return !isVariableDeclarationList(node.parent) || node.parent.declarations.length !== 1 ? + node : + isVariableStatement(node.parent.parent) ? + node.parent.parent : + isForInOrOfStatement(node.parent.parent) ? + getContextNode(node.parent.parent) : + node.parent; + case SyntaxKind.BindingElement: + return getContextNode((node.parent.parent as NamedDeclaration)); + case SyntaxKind.ImportSpecifier: + return node.parent.parent.parent; + case SyntaxKind.ExportSpecifier: + case SyntaxKind.NamespaceImport: + return node.parent.parent; + case SyntaxKind.ImportClause: + return node.parent; + case SyntaxKind.BinaryExpression: + return isExpressionStatement(node.parent) ? + node.parent : + node; + case SyntaxKind.ForOfStatement: + case SyntaxKind.ForInStatement: + return { + start: (node as ForInOrOfStatement).initializer, + end: (node as ForInOrOfStatement).expression + }; + case SyntaxKind.PropertyAssignment: + case SyntaxKind.ShorthandPropertyAssignment: + return isArrayLiteralOrObjectLiteralDestructuringPattern(node.parent) ? + getContextNode((findAncestor(node.parent, node => isBinaryExpression(node) || isForInOrOfStatement(node)) as BinaryExpression | ForInOrOfStatement)) : + node; + default: + return node; } - - export const enum FindReferencesUse { - /** - * When searching for references to a symbol, the location will not be adjusted (this is the default behavior when not specified). - */ - Other, - /** - * When searching for references to a symbol, the location will be adjusted if the cursor was on a keyword. - */ - References, - /** - * When searching for references to a symbol, the location will be adjusted if the cursor was on a keyword. - * Unlike `References`, the location will only be adjusted keyword belonged to a declaration with a valid name. - * If set, we will find fewer references -- if it is referenced by several different names, we still only find references for the original name. - */ - Rename, - } - - export interface Options { - readonly findInStrings?: boolean; - readonly findInComments?: boolean; - readonly use?: FindReferencesUse; - /** True if we are searching for implementations. We will have a different method of adding references if so. */ - readonly implementations?: boolean; - /** - * True to opt in for enhanced renaming of shorthand properties and import/export specifiers. - * The options controls the behavior for the whole rename operation; it cannot be changed on a per-file basis. - * Default is false for backwards compatibility. - */ - readonly providePrefixAndSuffixTextForRename?: boolean; +} +/* @internal */ +export function toContextSpan(textSpan: TextSpan, sourceFile: SourceFile, context?: ContextNode): { + contextSpan: TextSpan; +} | undefined { + if (!context) + return undefined; + const contextSpan = isContextWithStartAndEndNode(context) ? + getTextSpan(context.start, sourceFile, context.end) : + getTextSpan(context, sourceFile); + return contextSpan.start !== textSpan.start || contextSpan.length !== textSpan.length ? + { contextSpan } : + undefined; +} +/* @internal */ +export const enum FindReferencesUse { + /** + * When searching for references to a symbol, the location will not be adjusted (this is the default behavior when not specified). + */ + Other, + /** + * When searching for references to a symbol, the location will be adjusted if the cursor was on a keyword. + */ + References, + /** + * When searching for references to a symbol, the location will be adjusted if the cursor was on a keyword. + * Unlike `References`, the location will only be adjusted keyword belonged to a declaration with a valid name. + * If set, we will find fewer references -- if it is referenced by several different names, we still only find references for the original name. + */ + Rename +} +/* @internal */ +export interface Options { + readonly findInStrings?: boolean; + readonly findInComments?: boolean; + readonly use?: FindReferencesUse; + /** True if we are searching for implementations. We will have a different method of adding references if so. */ + readonly implementations?: boolean; + /** + * True to opt in for enhanced renaming of shorthand properties and import/export specifiers. + * The options controls the behavior for the whole rename operation; it cannot be changed on a per-file basis. + * Default is false for backwards compatibility. + */ + readonly providePrefixAndSuffixTextForRename?: boolean; +} +/* @internal */ +export function findReferencedSymbols(program: Program, cancellationToken: CancellationToken, sourceFiles: readonly SourceFile[], sourceFile: SourceFile, position: number): ReferencedSymbol[] | undefined { + const node = getTouchingPropertyName(sourceFile, position); + const referencedSymbols = Core.getReferencedSymbolsForNode(position, node, program, sourceFiles, cancellationToken, { use: FindReferencesUse.References }); + const checker = program.getTypeChecker(); + return !referencedSymbols || !referencedSymbols.length ? undefined : mapDefined(referencedSymbols, ({ definition, references }) => + // Only include referenced symbols that have a valid definition. + definition && { + definition: checker.runWithCancellationToken(cancellationToken, checker => definitionToReferencedSymbolDefinitionInfo(definition, checker, node)), + references: references.map(toReferenceEntry) + }); +} +/* @internal */ +export function getImplementationsAtPosition(program: Program, cancellationToken: CancellationToken, sourceFiles: readonly SourceFile[], sourceFile: SourceFile, position: number): ImplementationLocation[] | undefined { + const node = getTouchingPropertyName(sourceFile, position); + const referenceEntries = getImplementationReferenceEntries(program, cancellationToken, sourceFiles, node, position); + const checker = program.getTypeChecker(); + return map(referenceEntries, entry => toImplementationLocation(entry, checker)); +} +/* @internal */ +function getImplementationReferenceEntries(program: Program, cancellationToken: CancellationToken, sourceFiles: readonly SourceFile[], node: Node, position: number): readonly Entry[] | undefined { + if (node.kind === SyntaxKind.SourceFile) { + return undefined; } - - export function findReferencedSymbols(program: Program, cancellationToken: CancellationToken, sourceFiles: readonly SourceFile[], sourceFile: SourceFile, position: number): ReferencedSymbol[] | undefined { - const node = getTouchingPropertyName(sourceFile, position); - const referencedSymbols = Core.getReferencedSymbolsForNode(position, node, program, sourceFiles, cancellationToken, { use: FindReferencesUse.References }); - const checker = program.getTypeChecker(); - return !referencedSymbols || !referencedSymbols.length ? undefined : mapDefined(referencedSymbols, ({ definition, references }) => - // Only include referenced symbols that have a valid definition. - definition && { - definition: checker.runWithCancellationToken(cancellationToken, checker => definitionToReferencedSymbolDefinitionInfo(definition, checker, node)), - references: references.map(toReferenceEntry) - }); + const checker = program.getTypeChecker(); + // If invoked directly on a shorthand property assignment, then return + // the declaration of the symbol being assigned (not the symbol being assigned to). + if (node.parent.kind === SyntaxKind.ShorthandPropertyAssignment) { + const result: NodeEntry[] = []; + Core.getReferenceEntriesForShorthandPropertyAssignment(node, checker, node => result.push(nodeEntry(node))); + return result; } - - export function getImplementationsAtPosition(program: Program, cancellationToken: CancellationToken, sourceFiles: readonly SourceFile[], sourceFile: SourceFile, position: number): ImplementationLocation[] | undefined { - const node = getTouchingPropertyName(sourceFile, position); - const referenceEntries = getImplementationReferenceEntries(program, cancellationToken, sourceFiles, node, position); - const checker = program.getTypeChecker(); - return map(referenceEntries, entry => toImplementationLocation(entry, checker)); + else if (node.kind === SyntaxKind.SuperKeyword || isSuperProperty(node.parent)) { + // References to and accesses on the super keyword only have one possible implementation, so no + // need to "Find all References" + const symbol = checker.getSymbolAtLocation(node)!; + return symbol.valueDeclaration && [nodeEntry(symbol.valueDeclaration)]; } - - function getImplementationReferenceEntries(program: Program, cancellationToken: CancellationToken, sourceFiles: readonly SourceFile[], node: Node, position: number): readonly Entry[] | undefined { - if (node.kind === SyntaxKind.SourceFile) { - return undefined; - } - - const checker = program.getTypeChecker(); - // If invoked directly on a shorthand property assignment, then return - // the declaration of the symbol being assigned (not the symbol being assigned to). - if (node.parent.kind === SyntaxKind.ShorthandPropertyAssignment) { - const result: NodeEntry[] = []; - Core.getReferenceEntriesForShorthandPropertyAssignment(node, checker, node => result.push(nodeEntry(node))); - return result; - } - else if (node.kind === SyntaxKind.SuperKeyword || isSuperProperty(node.parent)) { - // References to and accesses on the super keyword only have one possible implementation, so no - // need to "Find all References" - const symbol = checker.getSymbolAtLocation(node)!; - return symbol.valueDeclaration && [nodeEntry(symbol.valueDeclaration)]; - } - else { - // Perform "Find all References" and retrieve only those that are implementations - return getReferenceEntriesForNode(position, node, program, sourceFiles, cancellationToken, { implementations: true, use: FindReferencesUse.References }); - } - } - - export function findReferenceOrRenameEntries( - program: Program, cancellationToken: CancellationToken, sourceFiles: readonly SourceFile[], node: Node, position: number, options: Options | undefined, - convertEntry: ToReferenceOrRenameEntry, - ): T[] | undefined { - return map(flattenEntries(Core.getReferencedSymbolsForNode(position, node, program, sourceFiles, cancellationToken, options)), entry => convertEntry(entry, node, program.getTypeChecker())); - } - - export type ToReferenceOrRenameEntry = (entry: Entry, originalNode: Node, checker: TypeChecker) => T; - - export function getReferenceEntriesForNode( - position: number, - node: Node, - program: Program, - sourceFiles: readonly SourceFile[], - cancellationToken: CancellationToken, - options: Options = {}, - sourceFilesSet: ReadonlyMap = arrayToSet(sourceFiles, f => f.fileName), - ): readonly Entry[] | undefined { - return flattenEntries(Core.getReferencedSymbolsForNode(position, node, program, sourceFiles, cancellationToken, options, sourceFilesSet)); - } - - function flattenEntries(referenceSymbols: readonly SymbolAndEntries[] | undefined): readonly Entry[] | undefined { - return referenceSymbols && flatMap(referenceSymbols, r => r.references); - } - - function definitionToReferencedSymbolDefinitionInfo(def: Definition, checker: TypeChecker, originalNode: Node): ReferencedSymbolDefinitionInfo { - const info = (() => { - switch (def.type) { - case DefinitionKind.Symbol: { - const { symbol } = def; - const { displayParts, kind } = getDefinitionKindAndDisplayParts(symbol, checker, originalNode); - const name = displayParts.map(p => p.text).join(""); - const declaration = symbol.declarations ? first(symbol.declarations) : undefined; - return { - node: declaration ? - getNameOfDeclaration(declaration) || declaration : - originalNode, - name, - kind, - displayParts, - context: getContextNode(declaration) - }; - } - case DefinitionKind.Label: { - const { node } = def; - return { node, name: node.text, kind: ScriptElementKind.label, displayParts: [displayPart(node.text, SymbolDisplayPartKind.text)] }; - } - case DefinitionKind.Keyword: { - const { node } = def; - const name = tokenToString(node.kind)!; - return { node, name, kind: ScriptElementKind.keyword, displayParts: [{ text: name, kind: ScriptElementKind.keyword }] }; - } - case DefinitionKind.This: { - const { node } = def; - const symbol = checker.getSymbolAtLocation(node); - const displayParts = symbol && SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind( - checker, symbol, node.getSourceFile(), getContainerNode(node), node).displayParts || [textPart("this")]; - return { node, name: "this", kind: ScriptElementKind.variableElement, displayParts }; - } - case DefinitionKind.String: { - const { node } = def; - return { node, name: node.text, kind: ScriptElementKind.variableElement, displayParts: [displayPart(getTextOfNode(node), SymbolDisplayPartKind.stringLiteral)] }; - } - default: - return Debug.assertNever(def); + else { + // Perform "Find all References" and retrieve only those that are implementations + return getReferenceEntriesForNode(position, node, program, sourceFiles, cancellationToken, { implementations: true, use: FindReferencesUse.References }); + } +} +/* @internal */ +export function findReferenceOrRenameEntries(program: Program, cancellationToken: CancellationToken, sourceFiles: readonly SourceFile[], node: Node, position: number, options: Options | undefined, convertEntry: ToReferenceOrRenameEntry): T[] | undefined { + return map(flattenEntries(Core.getReferencedSymbolsForNode(position, node, program, sourceFiles, cancellationToken, options)), entry => convertEntry(entry, node, program.getTypeChecker())); +} +/* @internal */ +export type ToReferenceOrRenameEntry = (entry: Entry, originalNode: Node, checker: TypeChecker) => T; +/* @internal */ +export function getReferenceEntriesForNode(position: number, node: Node, program: Program, sourceFiles: readonly SourceFile[], cancellationToken: CancellationToken, options: Options = {}, sourceFilesSet: ts.ReadonlyMap = arrayToSet(sourceFiles, f => f.fileName)): readonly Entry[] | undefined { + return flattenEntries(Core.getReferencedSymbolsForNode(position, node, program, sourceFiles, cancellationToken, options, sourceFilesSet)); +} +/* @internal */ +function flattenEntries(referenceSymbols: readonly SymbolAndEntries[] | undefined): readonly Entry[] | undefined { + return referenceSymbols && flatMap(referenceSymbols, r => r.references); +} +/* @internal */ +function definitionToReferencedSymbolDefinitionInfo(def: Definition, checker: TypeChecker, originalNode: Node): ReferencedSymbolDefinitionInfo { + const info = (() => { + switch (def.type) { + case DefinitionKind.Symbol: { + const { symbol } = def; + const { displayParts, kind } = getDefinitionKindAndDisplayParts(symbol, checker, originalNode); + const name = displayParts.map(p => p.text).join(""); + const declaration = symbol.declarations ? first(symbol.declarations) : undefined; + return { + node: declaration ? + getNameOfDeclaration(declaration) || declaration : + originalNode, + name, + kind, + displayParts, + context: getContextNode(declaration) + }; + } + case DefinitionKind.Label: { + const { node } = def; + return { node, name: node.text, kind: ScriptElementKind.label, displayParts: [displayPart(node.text, SymbolDisplayPartKind.text)] }; + } + case DefinitionKind.Keyword: { + const { node } = def; + const name = (tokenToString(node.kind)!); + return { node, name, kind: ScriptElementKind.keyword, displayParts: [{ text: name, kind: ScriptElementKind.keyword }] }; + } + case DefinitionKind.This: { + const { node } = def; + const symbol = checker.getSymbolAtLocation(node); + const displayParts = symbol && getSymbolDisplayPartsDocumentationAndSymbolKind(checker, symbol, node.getSourceFile(), getContainerNode(node), node).displayParts || [textPart("this")]; + return { node, name: "this", kind: ScriptElementKind.variableElement, displayParts }; } - })(); - - const { node, name, kind, displayParts, context } = info; - const sourceFile = node.getSourceFile(); - const textSpan = getTextSpan(isComputedPropertyName(node) ? node.expression : node, sourceFile); + case DefinitionKind.String: { + const { node } = def; + return { node, name: node.text, kind: ScriptElementKind.variableElement, displayParts: [displayPart(getTextOfNode(node), SymbolDisplayPartKind.stringLiteral)] }; + } + default: + return Debug.assertNever(def); + } + })(); + const { node, name, kind, displayParts, context } = info; + const sourceFile = node.getSourceFile(); + const textSpan = getTextSpan(isComputedPropertyName(node) ? node.expression : node, sourceFile); + return { + containerKind: ScriptElementKind.unknown, + containerName: "", + fileName: sourceFile.fileName, + kind, + name, + textSpan, + displayParts, + ...toContextSpan(textSpan, sourceFile, context) + }; +} +/* @internal */ +function getDefinitionKindAndDisplayParts(symbol: Symbol, checker: TypeChecker, node: Node): { + displayParts: SymbolDisplayPart[]; + kind: ScriptElementKind; +} { + const meaning = Core.getIntersectingMeaningFromDeclarations(node, symbol); + const enclosingDeclaration = symbol.declarations && firstOrUndefined(symbol.declarations) || node; + const { displayParts, symbolKind } = getSymbolDisplayPartsDocumentationAndSymbolKind(checker, symbol, enclosingDeclaration.getSourceFile(), enclosingDeclaration, enclosingDeclaration, meaning); + return { displayParts, kind: symbolKind }; +} +/* @internal */ +export function toRenameLocation(entry: Entry, originalNode: Node, checker: TypeChecker, providePrefixAndSuffixText: boolean): RenameLocation { + return { ...entryToDocumentSpan(entry), ...(providePrefixAndSuffixText && getPrefixAndSuffixText(entry, originalNode, checker)) }; +} +/* @internal */ +export function toReferenceEntry(entry: Entry): ReferenceEntry { + const documentSpan = entryToDocumentSpan(entry); + if (entry.kind === EntryKind.Span) { + return { ...documentSpan, isWriteAccess: false, isDefinition: false }; + } + const { kind, node } = entry; + return { + ...documentSpan, + isWriteAccess: isWriteAccessForReference(node), + isDefinition: isDefinitionForReference(node), + isInString: kind === EntryKind.StringLiteral ? true : undefined, + }; +} +/* @internal */ +function entryToDocumentSpan(entry: Entry): DocumentSpan { + if (entry.kind === EntryKind.Span) { + return { textSpan: entry.textSpan, fileName: entry.fileName }; + } + else { + const sourceFile = entry.node.getSourceFile(); + const textSpan = getTextSpan(entry.node, sourceFile); return { - containerKind: ScriptElementKind.unknown, - containerName: "", - fileName: sourceFile.fileName, - kind, - name, textSpan, - displayParts, - ...toContextSpan(textSpan, sourceFile, context) + fileName: sourceFile.fileName, + ...toContextSpan(textSpan, sourceFile, entry.context) }; } - - function getDefinitionKindAndDisplayParts(symbol: Symbol, checker: TypeChecker, node: Node): { displayParts: SymbolDisplayPart[], kind: ScriptElementKind } { - const meaning = Core.getIntersectingMeaningFromDeclarations(node, symbol); - const enclosingDeclaration = symbol.declarations && firstOrUndefined(symbol.declarations) || node; - const { displayParts, symbolKind } = - SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(checker, symbol, enclosingDeclaration.getSourceFile(), enclosingDeclaration, enclosingDeclaration, meaning); - return { displayParts, kind: symbolKind }; - } - - export function toRenameLocation(entry: Entry, originalNode: Node, checker: TypeChecker, providePrefixAndSuffixText: boolean): RenameLocation { - return { ...entryToDocumentSpan(entry), ...(providePrefixAndSuffixText && getPrefixAndSuffixText(entry, originalNode, checker)) }; - } - - export function toReferenceEntry(entry: Entry): ReferenceEntry { - const documentSpan = entryToDocumentSpan(entry); - if (entry.kind === EntryKind.Span) { - return { ...documentSpan, isWriteAccess: false, isDefinition: false }; - } - const { kind, node } = entry; +} +/* @internal */ +interface PrefixAndSuffix { + readonly prefixText?: string; + readonly suffixText?: string; +} +/* @internal */ +function getPrefixAndSuffixText(entry: Entry, originalNode: Node, checker: TypeChecker): PrefixAndSuffix { + if (entry.kind !== EntryKind.Span && isIdentifier(originalNode)) { + const { node, kind } = entry; + const name = originalNode.text; + const isShorthandAssignment = isShorthandPropertyAssignment(node.parent); + if (isShorthandAssignment || isObjectBindingElementWithoutPropertyName(node.parent) && node.parent.name === node) { + const prefixColon: PrefixAndSuffix = { prefixText: name + ": " }; + const suffixColon: PrefixAndSuffix = { suffixText: ": " + name }; + return kind === EntryKind.SearchedLocalFoundProperty ? prefixColon + : kind === EntryKind.SearchedPropertyFoundLocal ? suffixColon + // In `const o = { x }; o.x`, symbolAtLocation at `x` in `{ x }` is the property symbol. + // For a binding element `const { x } = o;`, symbolAtLocation at `x` is the property symbol. + : isShorthandAssignment ? suffixColon : prefixColon; + } + else if (isImportSpecifier(entry.node.parent) && !entry.node.parent.propertyName) { + // If the original symbol was using this alias, just rename the alias. + const originalSymbol = isExportSpecifier(originalNode.parent) ? checker.getExportSpecifierLocalTargetSymbol(originalNode.parent) : checker.getSymbolAtLocation(originalNode); + return contains(originalSymbol!.declarations, entry.node.parent) ? { prefixText: name + " as " } : emptyOptions; + } + else if (isExportSpecifier(entry.node.parent) && !entry.node.parent.propertyName) { + // If the symbol for the node is same as declared node symbol use prefix text + return originalNode === entry.node || checker.getSymbolAtLocation(originalNode) === checker.getSymbolAtLocation(entry.node) ? + { prefixText: name + " as " } : + { suffixText: " as " + name }; + } + } + return emptyOptions; +} +/* @internal */ +function toImplementationLocation(entry: Entry, checker: TypeChecker): ImplementationLocation { + const documentSpan = entryToDocumentSpan(entry); + if (entry.kind !== EntryKind.Span) { + const { node } = entry; return { ...documentSpan, - isWriteAccess: isWriteAccessForReference(node), - isDefinition: isDefinitionForReference(node), - isInString: kind === EntryKind.StringLiteral ? true : undefined, + ...implementationKindDisplayParts(node, checker) }; } - - function entryToDocumentSpan(entry: Entry): DocumentSpan { - if (entry.kind === EntryKind.Span) { - return { textSpan: entry.textSpan, fileName: entry.fileName }; - } - else { - const sourceFile = entry.node.getSourceFile(); - const textSpan = getTextSpan(entry.node, sourceFile); - return { - textSpan, - fileName: sourceFile.fileName, - ...toContextSpan(textSpan, sourceFile, entry.context) - }; - } + else { + return { ...documentSpan, kind: ScriptElementKind.unknown, displayParts: [] }; } - - interface PrefixAndSuffix { readonly prefixText?: string; readonly suffixText?: string; } - function getPrefixAndSuffixText(entry: Entry, originalNode: Node, checker: TypeChecker): PrefixAndSuffix { - if (entry.kind !== EntryKind.Span && isIdentifier(originalNode)) { - const { node, kind } = entry; - const name = originalNode.text; - const isShorthandAssignment = isShorthandPropertyAssignment(node.parent); - if (isShorthandAssignment || isObjectBindingElementWithoutPropertyName(node.parent) && node.parent.name === node) { - const prefixColon: PrefixAndSuffix = { prefixText: name + ": " }; - const suffixColon: PrefixAndSuffix = { suffixText: ": " + name }; - return kind === EntryKind.SearchedLocalFoundProperty ? prefixColon - : kind === EntryKind.SearchedPropertyFoundLocal ? suffixColon - // In `const o = { x }; o.x`, symbolAtLocation at `x` in `{ x }` is the property symbol. - // For a binding element `const { x } = o;`, symbolAtLocation at `x` is the property symbol. - : isShorthandAssignment ? suffixColon : prefixColon; - } - else if (isImportSpecifier(entry.node.parent) && !entry.node.parent.propertyName) { - // If the original symbol was using this alias, just rename the alias. - const originalSymbol = isExportSpecifier(originalNode.parent) ? checker.getExportSpecifierLocalTargetSymbol(originalNode.parent) : checker.getSymbolAtLocation(originalNode); - return contains(originalSymbol!.declarations, entry.node.parent) ? { prefixText: name + " as " } : emptyOptions; - } - else if (isExportSpecifier(entry.node.parent) && !entry.node.parent.propertyName) { - // If the symbol for the node is same as declared node symbol use prefix text - return originalNode === entry.node || checker.getSymbolAtLocation(originalNode) === checker.getSymbolAtLocation(entry.node) ? - { prefixText: name + " as " } : - { suffixText: " as " + name }; +} +/* @internal */ +function implementationKindDisplayParts(node: Node, checker: TypeChecker): { + kind: ScriptElementKind; + displayParts: SymbolDisplayPart[]; +} { + const symbol = checker.getSymbolAtLocation(isDeclaration(node) && node.name ? node.name : node); + if (symbol) { + return getDefinitionKindAndDisplayParts(symbol, checker, node); + } + else if (node.kind === SyntaxKind.ObjectLiteralExpression) { + return { + kind: ScriptElementKind.interfaceElement, + displayParts: [punctuationPart(SyntaxKind.OpenParenToken), textPart("object literal"), punctuationPart(SyntaxKind.CloseParenToken)] + }; + } + else if (node.kind === SyntaxKind.ClassExpression) { + return { + kind: ScriptElementKind.localClassElement, + displayParts: [punctuationPart(SyntaxKind.OpenParenToken), textPart("anonymous local class"), punctuationPart(SyntaxKind.CloseParenToken)] + }; + } + else { + return { kind: getNodeKind(node), displayParts: [] }; + } +} +/* @internal */ +export function toHighlightSpan(entry: Entry): { + fileName: string; + span: HighlightSpan; +} { + const documentSpan = entryToDocumentSpan(entry); + if (entry.kind === EntryKind.Span) { + return { + fileName: documentSpan.fileName, + span: { + textSpan: documentSpan.textSpan, + kind: HighlightSpanKind.reference } - } - - return emptyOptions; + }; } - - function toImplementationLocation(entry: Entry, checker: TypeChecker): ImplementationLocation { - const documentSpan = entryToDocumentSpan(entry); - if (entry.kind !== EntryKind.Span) { - const { node } = entry; - return { - ...documentSpan, - ...implementationKindDisplayParts(node, checker) - }; - } - else { - return { ...documentSpan, kind: ScriptElementKind.unknown, displayParts: [] }; - } + const writeAccess = isWriteAccessForReference(entry.node); + const span: HighlightSpan = { + textSpan: documentSpan.textSpan, + kind: writeAccess ? HighlightSpanKind.writtenReference : HighlightSpanKind.reference, + isInString: entry.kind === EntryKind.StringLiteral ? true : undefined, + ...documentSpan.contextSpan && { contextSpan: documentSpan.contextSpan } + }; + return { fileName: documentSpan.fileName, span }; +} +/* @internal */ +function getTextSpan(node: Node, sourceFile: SourceFile, endNode?: Node): TextSpan { + let start = node.getStart(sourceFile); + let end = (endNode || node).getEnd(); + if (isStringLiteralLike(node)) { + Debug.assert(endNode === undefined); + start += 1; + end -= 1; + } + return createTextSpanFromBounds(start, end); +} +/* @internal */ +export function getTextSpanOfEntry(entry: Entry) { + return entry.kind === EntryKind.Span ? entry.textSpan : + getTextSpan(entry.node, entry.node.getSourceFile()); +} +/** A node is considered a writeAccess iff it is a name of a declaration or a target of an assignment */ +/* @internal */ +function isWriteAccessForReference(node: Node): boolean { + const decl = getDeclarationFromName(node); + return !!decl && declarationIsWriteAccess(decl) || node.kind === SyntaxKind.DefaultKeyword || isWriteAccess(node); +} +/* @internal */ +function isDefinitionForReference(node: Node): boolean { + return node.kind === SyntaxKind.DefaultKeyword + || !!getDeclarationFromName(node) + || isLiteralComputedPropertyDeclarationName(node) + || (node.kind === SyntaxKind.ConstructorKeyword && isConstructorDeclaration(node.parent)); +} +/** + * True if 'decl' provides a value, as in `function f() {}`; + * false if 'decl' is just a location for a future write, as in 'let x;' + */ +/* @internal */ +function declarationIsWriteAccess(decl: Declaration): boolean { + // Consider anything in an ambient declaration to be a write access since it may be coming from JS. + if (!!(decl.flags & NodeFlags.Ambient)) + return true; + switch (decl.kind) { + case SyntaxKind.BinaryExpression: + case SyntaxKind.BindingElement: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ClassExpression: + case SyntaxKind.DefaultKeyword: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.EnumMember: + case SyntaxKind.ExportSpecifier: + case SyntaxKind.ImportClause: // default import + case SyntaxKind.ImportEqualsDeclaration: + case SyntaxKind.ImportSpecifier: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.JSDocCallbackTag: + case SyntaxKind.JSDocTypedefTag: + case SyntaxKind.JsxAttribute: + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.NamespaceExportDeclaration: + case SyntaxKind.NamespaceImport: + case SyntaxKind.NamespaceExport: + case SyntaxKind.Parameter: + case SyntaxKind.ShorthandPropertyAssignment: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.TypeParameter: + return true; + case SyntaxKind.PropertyAssignment: + // In `({ x: y } = 0);`, `x` is not a write access. (Won't call this function for `y`.) + return !isArrayLiteralOrObjectLiteralDestructuringPattern((decl as PropertyAssignment).parent); + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.FunctionExpression: + case SyntaxKind.Constructor: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + return !!(decl as FunctionDeclaration | FunctionExpression | ConstructorDeclaration | MethodDeclaration | GetAccessorDeclaration | SetAccessorDeclaration).body; + case SyntaxKind.VariableDeclaration: + case SyntaxKind.PropertyDeclaration: + return !!(decl as VariableDeclaration | PropertyDeclaration).initializer || isCatchClause(decl.parent); + case SyntaxKind.MethodSignature: + case SyntaxKind.PropertySignature: + case SyntaxKind.JSDocPropertyTag: + case SyntaxKind.JSDocParameterTag: + return false; + default: + return Debug.failBadSyntaxKind(decl); } - - function implementationKindDisplayParts(node: Node, checker: TypeChecker): { kind: ScriptElementKind, displayParts: SymbolDisplayPart[] } { - const symbol = checker.getSymbolAtLocation(isDeclaration(node) && node.name ? node.name : node); - if (symbol) { - return getDefinitionKindAndDisplayParts(symbol, checker, node); +} +/** Encapsulates the core find-all-references algorithm. */ +/* @internal */ +export namespace Core { + /** Core find-all-references algorithm. Handles special cases before delegating to `getReferencedSymbolsForSymbol`. */ + export function getReferencedSymbolsForNode(position: number, node: Node, program: Program, sourceFiles: readonly SourceFile[], cancellationToken: CancellationToken, options: Options = {}, sourceFilesSet: ts.ReadonlyMap = arrayToSet(sourceFiles, f => f.fileName)): readonly SymbolAndEntries[] | undefined { + if (options.use === FindReferencesUse.References) { + node = getAdjustedReferenceLocation(node); } - else if (node.kind === SyntaxKind.ObjectLiteralExpression) { - return { - kind: ScriptElementKind.interfaceElement, - displayParts: [punctuationPart(SyntaxKind.OpenParenToken), textPart("object literal"), punctuationPart(SyntaxKind.CloseParenToken)] - }; + else if (options.use === FindReferencesUse.Rename) { + node = getAdjustedRenameLocation(node); } - else if (node.kind === SyntaxKind.ClassExpression) { - return { - kind: ScriptElementKind.localClassElement, - displayParts: [punctuationPart(SyntaxKind.OpenParenToken), textPart("anonymous local class"), punctuationPart(SyntaxKind.CloseParenToken)] - }; + if (isSourceFile(node)) { + const reference = getReferenceAtPosition(node, position, program); + const moduleSymbol = reference && program.getTypeChecker().getMergedSymbol(reference.file.symbol); + return moduleSymbol && getReferencedSymbolsForModule(program, moduleSymbol, /*excludeImportTypeOfExportEquals*/ false, sourceFiles, sourceFilesSet); } - else { - return { kind: getNodeKind(node), displayParts: [] }; + if (!options.implementations) { + const special = getReferencedSymbolsSpecial(node, sourceFiles, cancellationToken); + if (special) { + return special; + } } + const checker = program.getTypeChecker(); + const symbol = checker.getSymbolAtLocation(node); + // Could not find a symbol e.g. unknown identifier + if (!symbol) { + // String literal might be a property (and thus have a symbol), so do this here rather than in getReferencedSymbolsSpecial. + return !options.implementations && isStringLiteral(node) ? getReferencesForStringLiteral(node, sourceFiles, cancellationToken) : undefined; + } + if (symbol.escapedName === InternalSymbolName.ExportEquals) { + return getReferencedSymbolsForModule(program, symbol.parent!, /*excludeImportTypeOfExportEquals*/ false, sourceFiles, sourceFilesSet); + } + const moduleReferences = getReferencedSymbolsForModuleIfDeclaredBySourceFile(symbol, program, sourceFiles, cancellationToken, options, sourceFilesSet); + if (moduleReferences && !(symbol.flags & SymbolFlags.Transient)) { + return moduleReferences; + } + const aliasedSymbol = getMergedAliasedSymbolOfNamespaceExportDeclaration(node, symbol, checker); + const moduleReferencesOfExportTarget = aliasedSymbol && + getReferencedSymbolsForModuleIfDeclaredBySourceFile(aliasedSymbol, program, sourceFiles, cancellationToken, options, sourceFilesSet); + const references = getReferencedSymbolsForSymbol(symbol, node, sourceFiles, sourceFilesSet, checker, cancellationToken, options); + return mergeReferences(program, moduleReferences, references, moduleReferencesOfExportTarget); } - - export function toHighlightSpan(entry: Entry): { fileName: string, span: HighlightSpan } { - const documentSpan = entryToDocumentSpan(entry); - if (entry.kind === EntryKind.Span) { - return { - fileName: documentSpan.fileName, - span: { - textSpan: documentSpan.textSpan, - kind: HighlightSpanKind.reference - } - }; - } - - const writeAccess = isWriteAccessForReference(entry.node); - const span: HighlightSpan = { - textSpan: documentSpan.textSpan, - kind: writeAccess ? HighlightSpanKind.writtenReference : HighlightSpanKind.reference, - isInString: entry.kind === EntryKind.StringLiteral ? true : undefined, - ...documentSpan.contextSpan && { contextSpan: documentSpan.contextSpan } - }; - return { fileName: documentSpan.fileName, span }; - } - - function getTextSpan(node: Node, sourceFile: SourceFile, endNode?: Node): TextSpan { - let start = node.getStart(sourceFile); - let end = (endNode || node).getEnd(); - if (isStringLiteralLike(node)) { - Debug.assert(endNode === undefined); - start += 1; - end -= 1; - } - return createTextSpanFromBounds(start, end); - } - - export function getTextSpanOfEntry(entry: Entry) { - return entry.kind === EntryKind.Span ? entry.textSpan : - getTextSpan(entry.node, entry.node.getSourceFile()); - } - - /** A node is considered a writeAccess iff it is a name of a declaration or a target of an assignment */ - function isWriteAccessForReference(node: Node): boolean { - const decl = getDeclarationFromName(node); - return !!decl && declarationIsWriteAccess(decl) || node.kind === SyntaxKind.DefaultKeyword || isWriteAccess(node); - } - - function isDefinitionForReference(node: Node): boolean { - return node.kind === SyntaxKind.DefaultKeyword - || !!getDeclarationFromName(node) - || isLiteralComputedPropertyDeclarationName(node) - || (node.kind === SyntaxKind.ConstructorKeyword && isConstructorDeclaration(node.parent)); - } - - /** - * True if 'decl' provides a value, as in `function f() {}`; - * false if 'decl' is just a location for a future write, as in 'let x;' - */ - function declarationIsWriteAccess(decl: Declaration): boolean { - // Consider anything in an ambient declaration to be a write access since it may be coming from JS. - if (!!(decl.flags & NodeFlags.Ambient)) return true; - - switch (decl.kind) { - case SyntaxKind.BinaryExpression: - case SyntaxKind.BindingElement: - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ClassExpression: - case SyntaxKind.DefaultKeyword: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.EnumMember: - case SyntaxKind.ExportSpecifier: - case SyntaxKind.ImportClause: // default import - case SyntaxKind.ImportEqualsDeclaration: - case SyntaxKind.ImportSpecifier: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.JSDocCallbackTag: - case SyntaxKind.JSDocTypedefTag: - case SyntaxKind.JsxAttribute: - case SyntaxKind.ModuleDeclaration: - case SyntaxKind.NamespaceExportDeclaration: - case SyntaxKind.NamespaceImport: - case SyntaxKind.NamespaceExport: - case SyntaxKind.Parameter: - case SyntaxKind.ShorthandPropertyAssignment: - case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.TypeParameter: - return true; - - case SyntaxKind.PropertyAssignment: - // In `({ x: y } = 0);`, `x` is not a write access. (Won't call this function for `y`.) - return !isArrayLiteralOrObjectLiteralDestructuringPattern((decl as PropertyAssignment).parent); - - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.FunctionExpression: - case SyntaxKind.Constructor: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - return !!(decl as FunctionDeclaration | FunctionExpression | ConstructorDeclaration | MethodDeclaration | GetAccessorDeclaration | SetAccessorDeclaration).body; - - case SyntaxKind.VariableDeclaration: - case SyntaxKind.PropertyDeclaration: - return !!(decl as VariableDeclaration | PropertyDeclaration).initializer || isCatchClause(decl.parent); - - case SyntaxKind.MethodSignature: - case SyntaxKind.PropertySignature: - case SyntaxKind.JSDocPropertyTag: - case SyntaxKind.JSDocParameterTag: - return false; - - default: - return Debug.failBadSyntaxKind(decl); + function getMergedAliasedSymbolOfNamespaceExportDeclaration(node: Node, symbol: Symbol, checker: TypeChecker) { + if (node.parent && isNamespaceExportDeclaration(node.parent)) { + const aliasedSymbol = checker.getAliasedSymbol(symbol); + const targetSymbol = checker.getMergedSymbol(aliasedSymbol); + if (aliasedSymbol !== targetSymbol) { + return targetSymbol; + } } + return undefined; } - - /** Encapsulates the core find-all-references algorithm. */ - export namespace Core { - /** Core find-all-references algorithm. Handles special cases before delegating to `getReferencedSymbolsForSymbol`. */ - export function getReferencedSymbolsForNode(position: number, node: Node, program: Program, sourceFiles: readonly SourceFile[], cancellationToken: CancellationToken, options: Options = {}, sourceFilesSet: ReadonlyMap = arrayToSet(sourceFiles, f => f.fileName)): readonly SymbolAndEntries[] | undefined { - if (options.use === FindReferencesUse.References) { - node = getAdjustedReferenceLocation(node); - } - else if (options.use === FindReferencesUse.Rename) { - node = getAdjustedRenameLocation(node); - } - if (isSourceFile(node)) { - const reference = GoToDefinition.getReferenceAtPosition(node, position, program); - const moduleSymbol = reference && program.getTypeChecker().getMergedSymbol(reference.file.symbol); - return moduleSymbol && getReferencedSymbolsForModule(program, moduleSymbol, /*excludeImportTypeOfExportEquals*/ false, sourceFiles, sourceFilesSet); - } - - if (!options.implementations) { - const special = getReferencedSymbolsSpecial(node, sourceFiles, cancellationToken); - if (special) { - return special; - } - } - - const checker = program.getTypeChecker(); - const symbol = checker.getSymbolAtLocation(node); - - // Could not find a symbol e.g. unknown identifier - if (!symbol) { - // String literal might be a property (and thus have a symbol), so do this here rather than in getReferencedSymbolsSpecial. - return !options.implementations && isStringLiteral(node) ? getReferencesForStringLiteral(node, sourceFiles, cancellationToken) : undefined; - } - - if (symbol.escapedName === InternalSymbolName.ExportEquals) { - return getReferencedSymbolsForModule(program, symbol.parent!, /*excludeImportTypeOfExportEquals*/ false, sourceFiles, sourceFilesSet); - } - - const moduleReferences = getReferencedSymbolsForModuleIfDeclaredBySourceFile(symbol, program, sourceFiles, cancellationToken, options, sourceFilesSet); - if (moduleReferences && !(symbol.flags & SymbolFlags.Transient)) { - return moduleReferences; - } - - const aliasedSymbol = getMergedAliasedSymbolOfNamespaceExportDeclaration(node, symbol, checker); - const moduleReferencesOfExportTarget = aliasedSymbol && - getReferencedSymbolsForModuleIfDeclaredBySourceFile(aliasedSymbol, program, sourceFiles, cancellationToken, options, sourceFilesSet); - - const references = getReferencedSymbolsForSymbol(symbol, node, sourceFiles, sourceFilesSet, checker, cancellationToken, options); - return mergeReferences(program, moduleReferences, references, moduleReferencesOfExportTarget); - } - - function getMergedAliasedSymbolOfNamespaceExportDeclaration(node: Node, symbol: Symbol, checker: TypeChecker) { - if (node.parent && isNamespaceExportDeclaration(node.parent)) { - const aliasedSymbol = checker.getAliasedSymbol(symbol); - const targetSymbol = checker.getMergedSymbol(aliasedSymbol); - if (aliasedSymbol !== targetSymbol) { - return targetSymbol; - } - } + function getReferencedSymbolsForModuleIfDeclaredBySourceFile(symbol: Symbol, program: Program, sourceFiles: readonly SourceFile[], cancellationToken: CancellationToken, options: Options, sourceFilesSet: ts.ReadonlyMap) { + const moduleSourceFile = (symbol.flags & SymbolFlags.Module) && symbol.declarations && find(symbol.declarations, isSourceFile); + if (!moduleSourceFile) return undefined; - } - - function getReferencedSymbolsForModuleIfDeclaredBySourceFile(symbol: Symbol, program: Program, sourceFiles: readonly SourceFile[], cancellationToken: CancellationToken, options: Options, sourceFilesSet: ReadonlyMap) { - const moduleSourceFile = (symbol.flags & SymbolFlags.Module) && symbol.declarations && find(symbol.declarations, isSourceFile); - if (!moduleSourceFile) return undefined; - const exportEquals = symbol.exports!.get(InternalSymbolName.ExportEquals); - // If !!exportEquals, we're about to add references to `import("mod")` anyway, so don't double-count them. - const moduleReferences = getReferencedSymbolsForModule(program, symbol, !!exportEquals, sourceFiles, sourceFilesSet); - if (!exportEquals || !sourceFilesSet.has(moduleSourceFile.fileName)) return moduleReferences; - // Continue to get references to 'export ='. - const checker = program.getTypeChecker(); - symbol = skipAlias(exportEquals, checker); - return mergeReferences(program, moduleReferences, getReferencedSymbolsForSymbol(symbol, /*node*/ undefined, sourceFiles, sourceFilesSet, checker, cancellationToken, options)); - } - - /** - * Merges the references by sorting them (by file index in sourceFiles and their location in it) that point to same definition symbol - */ - function mergeReferences(program: Program, ...referencesToMerge: (SymbolAndEntries[] | undefined)[]): SymbolAndEntries[] | undefined { - let result: SymbolAndEntries[] | undefined; - for (const references of referencesToMerge) { - if (!references || !references.length) continue; - if (!result) { - result = references; + const exportEquals = symbol.exports!.get(InternalSymbolName.ExportEquals); + // If !!exportEquals, we're about to add references to `import("mod")` anyway, so don't double-count them. + const moduleReferences = getReferencedSymbolsForModule(program, symbol, !!exportEquals, sourceFiles, sourceFilesSet); + if (!exportEquals || !sourceFilesSet.has(moduleSourceFile.fileName)) + return moduleReferences; + // Continue to get references to 'export ='. + const checker = program.getTypeChecker(); + symbol = skipAlias(exportEquals, checker); + return mergeReferences(program, moduleReferences, getReferencedSymbolsForSymbol(symbol, /*node*/ undefined, sourceFiles, sourceFilesSet, checker, cancellationToken, options)); + } + /** + * Merges the references by sorting them (by file index in sourceFiles and their location in it) that point to same definition symbol + */ + function mergeReferences(program: Program, ...referencesToMerge: (SymbolAndEntries[] | undefined)[]): SymbolAndEntries[] | undefined { + let result: SymbolAndEntries[] | undefined; + for (const references of referencesToMerge) { + if (!references || !references.length) + continue; + if (!result) { + result = references; + continue; + } + for (const entry of references) { + if (!entry.definition || entry.definition.type !== DefinitionKind.Symbol) { + result.push(entry); continue; } - for (const entry of references) { - if (!entry.definition || entry.definition.type !== DefinitionKind.Symbol) { - result.push(entry); - continue; - } - const symbol = entry.definition.symbol; - const refIndex = findIndex(result, ref => !!ref.definition && - ref.definition.type === DefinitionKind.Symbol && - ref.definition.symbol === symbol); - if (refIndex === -1) { - result.push(entry); - continue; - } - - const reference = result[refIndex]; - result[refIndex] = { - definition: reference.definition, - references: reference.references.concat(entry.references).sort((entry1, entry2) => { - const entry1File = getSourceFileIndexOfEntry(program, entry1); - const entry2File = getSourceFileIndexOfEntry(program, entry2); - if (entry1File !== entry2File) { - return compareValues(entry1File, entry2File); - } - - const entry1Span = getTextSpanOfEntry(entry1); - const entry2Span = getTextSpanOfEntry(entry2); - return entry1Span.start !== entry2Span.start ? - compareValues(entry1Span.start, entry2Span.start) : - compareValues(entry1Span.length, entry2Span.length); - }) - }; + const symbol = entry.definition.symbol; + const refIndex = findIndex(result, ref => !!ref.definition && + ref.definition.type === DefinitionKind.Symbol && + ref.definition.symbol === symbol); + if (refIndex === -1) { + result.push(entry); + continue; } - } - return result; - } - - function getSourceFileIndexOfEntry(program: Program, entry: Entry) { - const sourceFile = entry.kind === EntryKind.Span ? - program.getSourceFile(entry.fileName)! : - entry.node.getSourceFile(); - return program.getSourceFiles().indexOf(sourceFile); - } - - function getReferencedSymbolsForModule(program: Program, symbol: Symbol, excludeImportTypeOfExportEquals: boolean, sourceFiles: readonly SourceFile[], sourceFilesSet: ReadonlyMap): SymbolAndEntries[] { - Debug.assert(!!symbol.valueDeclaration); - - const references = mapDefined(findModuleReferences(program, sourceFiles, symbol), reference => { - if (reference.kind === "import") { - const parent = reference.literal.parent; - if (isLiteralTypeNode(parent)) { - const importType = cast(parent.parent, isImportTypeNode); - if (excludeImportTypeOfExportEquals && !importType.qualifier) { - return undefined; + const reference = result[refIndex]; + result[refIndex] = { + definition: reference.definition, + references: reference.references.concat(entry.references).sort((entry1, entry2) => { + const entry1File = getSourceFileIndexOfEntry(program, entry1); + const entry2File = getSourceFileIndexOfEntry(program, entry2); + if (entry1File !== entry2File) { + return compareValues(entry1File, entry2File); } - } - // import("foo") with no qualifier will reference the `export =` of the module, which may be referenced anyway. - return nodeEntry(reference.literal); - } - else { - return { - kind: EntryKind.Span, - fileName: reference.referencingFile.fileName, - textSpan: createTextSpanFromRange(reference.ref), - }; - } - }); - - if (symbol.declarations) { - for (const decl of symbol.declarations) { - switch (decl.kind) { - case SyntaxKind.SourceFile: - // Don't include the source file itself. (This may not be ideal behavior, but awkward to include an entire file as a reference.) - break; - case SyntaxKind.ModuleDeclaration: - if (sourceFilesSet.has(decl.getSourceFile().fileName)) { - references.push(nodeEntry((decl as ModuleDeclaration).name)); - } - break; - default: - // This may be merged with something. - Debug.assert(!!(symbol.flags & SymbolFlags.Transient), "Expected a module symbol to be declared by a SourceFile or ModuleDeclaration."); - } - } - } - - const exported = symbol.exports!.get(InternalSymbolName.ExportEquals); - if (exported) { - for (const decl of exported.declarations) { - const sourceFile = decl.getSourceFile(); - if (sourceFilesSet.has(sourceFile.fileName)) { - // At `module.exports = ...`, reference node is `module` - const node = isBinaryExpression(decl) && isPropertyAccessExpression(decl.left) ? decl.left.expression : - isExportAssignment(decl) ? Debug.checkDefined(findChildOfKind(decl, SyntaxKind.ExportKeyword, sourceFile)) : - getNameOfDeclaration(decl) || decl; - references.push(nodeEntry(node)); - } - } - } - - return references.length ? [{ definition: { type: DefinitionKind.Symbol, symbol }, references }] : emptyArray; - } - - /** As in a `readonly prop: any` or `constructor(readonly prop: any)`, not a `readonly any[]`. */ - function isReadonlyTypeOperator(node: Node): boolean { - return node.kind === SyntaxKind.ReadonlyKeyword - && isTypeOperatorNode(node.parent) - && node.parent.operator === SyntaxKind.ReadonlyKeyword; - } - - /** getReferencedSymbols for special node kinds. */ - function getReferencedSymbolsSpecial(node: Node, sourceFiles: readonly SourceFile[], cancellationToken: CancellationToken): SymbolAndEntries[] | undefined { - if (isTypeKeyword(node.kind)) { - // A void expression (i.e., `void foo()`) is not special, but the `void` type is. - if (node.kind === SyntaxKind.VoidKeyword && isVoidExpression(node.parent)) { - return undefined; - } - - // A modifier readonly (like on a property declaration) is not special; - // a readonly type keyword (like `readonly string[]`) is. - if (node.kind === SyntaxKind.ReadonlyKeyword && !isReadonlyTypeOperator(node)) { - return undefined; - } - // Likewise, when we *are* looking for a special keyword, make sure we - // *don’t* include readonly member modifiers. - return getAllReferencesForKeyword( - sourceFiles, - node.kind, - cancellationToken, - node.kind === SyntaxKind.ReadonlyKeyword ? isReadonlyTypeOperator : undefined); - } - - // Labels - if (isJumpStatementTarget(node)) { - const labelDefinition = getTargetLabel(node.parent, node.text); - // if we have a label definition, look within its statement for references, if not, then - // the label is undefined and we have no results.. - return labelDefinition && getLabelReferencesInNode(labelDefinition.parent, labelDefinition); - } - else if (isLabelOfLabeledStatement(node)) { - // it is a label definition and not a target, search within the parent labeledStatement - return getLabelReferencesInNode(node.parent, node); - } - - if (isThis(node)) { - return getReferencesForThisKeyword(node, sourceFiles, cancellationToken); - } - - if (node.kind === SyntaxKind.SuperKeyword) { - return getReferencesForSuperKeyword(node); - } - - return undefined; - } - - /** Core find-all-references algorithm for a normal symbol. */ - function getReferencedSymbolsForSymbol(originalSymbol: Symbol, node: Node | undefined, sourceFiles: readonly SourceFile[], sourceFilesSet: ReadonlyMap, checker: TypeChecker, cancellationToken: CancellationToken, options: Options): SymbolAndEntries[] { - const symbol = node && skipPastExportOrImportSpecifierOrUnion(originalSymbol, node, checker, /*useLocalSymbolForExportSpecifier*/ !isForRenameWithPrefixAndSuffixText(options)) || originalSymbol; - - // Compute the meaning from the location and the symbol it references - const searchMeaning = node ? getIntersectingMeaningFromDeclarations(node, symbol) : SemanticMeaning.All; - const result: SymbolAndEntries[] = []; - const state = new State(sourceFiles, sourceFilesSet, node ? getSpecialSearchKind(node) : SpecialSearchKind.None, checker, cancellationToken, searchMeaning, options, result); - - const exportSpecifier = !isForRenameWithPrefixAndSuffixText(options) ? undefined : find(symbol.declarations, isExportSpecifier); - if (exportSpecifier) { - // When renaming at an export specifier, rename the export and not the thing being exported. - getReferencesAtExportSpecifier(exportSpecifier.name, symbol, exportSpecifier, state.createSearch(node, originalSymbol, /*comingFrom*/ undefined), state, /*addReferencesHere*/ true, /*alwaysGetReferences*/ true); - } - else if (node && node.kind === SyntaxKind.DefaultKeyword) { - addReference(node, symbol, state); - searchForImportsOfExport(node, symbol, { exportingModuleSymbol: Debug.checkDefined(symbol.parent, "Expected export symbol to have a parent"), exportKind: ExportKind.Default }, state); - } - else { - const search = state.createSearch(node, symbol, /*comingFrom*/ undefined, { allSearchSymbols: node ? populateSearchSymbolSet(symbol, node, checker, options.use === FindReferencesUse.Rename, !!options.providePrefixAndSuffixTextForRename, !!options.implementations) : [symbol] }); - getReferencesInContainerOrFiles(symbol, state, search); - } - - return result; - } - - function getReferencesInContainerOrFiles(symbol: Symbol, state: State, search: Search): void { - // Try to get the smallest valid scope that we can limit our search to; - // otherwise we'll need to search globally (i.e. include each file). - const scope = getSymbolScope(symbol); - if (scope) { - getReferencesInContainer(scope, scope.getSourceFile(), search, state, /*addReferencesHere*/ !(isSourceFile(scope) && !contains(state.sourceFiles, scope))); - } - else { - // Global search - for (const sourceFile of state.sourceFiles) { - state.cancellationToken.throwIfCancellationRequested(); - searchForName(sourceFile, search, state); - } + const entry1Span = getTextSpanOfEntry(entry1); + const entry2Span = getTextSpanOfEntry(entry2); + return entry1Span.start !== entry2Span.start ? + compareValues(entry1Span.start, entry2Span.start) : + compareValues(entry1Span.length, entry2Span.length); + }) + }; } } - - function getSpecialSearchKind(node: Node): SpecialSearchKind { - switch (node.kind) { - case SyntaxKind.ConstructorKeyword: - return SpecialSearchKind.Constructor; - case SyntaxKind.Identifier: - if (isClassLike(node.parent)) { - Debug.assert(node.parent.name === node); - return SpecialSearchKind.Class; + return result; + } + function getSourceFileIndexOfEntry(program: Program, entry: Entry) { + const sourceFile = entry.kind === EntryKind.Span ? + program.getSourceFile(entry.fileName)! : + entry.node.getSourceFile(); + return program.getSourceFiles().indexOf(sourceFile); + } + function getReferencedSymbolsForModule(program: Program, symbol: Symbol, excludeImportTypeOfExportEquals: boolean, sourceFiles: readonly SourceFile[], sourceFilesSet: ts.ReadonlyMap): SymbolAndEntries[] { + Debug.assert(!!symbol.valueDeclaration); + const references = mapDefined(findModuleReferences(program, sourceFiles, symbol), reference => { + if (reference.kind === "import") { + const parent = reference.literal.parent; + if (isLiteralTypeNode(parent)) { + const importType = cast(parent.parent, isImportTypeNode); + if (excludeImportTypeOfExportEquals && !importType.qualifier) { + return undefined; } - // falls through - default: - return SpecialSearchKind.None; - } - } - - /** Handle a few special cases relating to export/import specifiers. */ - function skipPastExportOrImportSpecifierOrUnion(symbol: Symbol, node: Node, checker: TypeChecker, useLocalSymbolForExportSpecifier: boolean): Symbol | undefined { - const { parent } = node; - if (isExportSpecifier(parent) && useLocalSymbolForExportSpecifier) { - return getLocalSymbolForExportSpecifier(node as Identifier, symbol, parent, checker); - } - // If the symbol is declared as part of a declaration like `{ type: "a" } | { type: "b" }`, use the property on the union type to get more references. - return firstDefined(symbol.declarations, decl => { - if (!decl.parent) { - // Ignore UMD module and global merge - if (symbol.flags & SymbolFlags.Transient) return undefined; - // Assertions for GH#21814. We should be handling SourceFile symbols in `getReferencedSymbolsForModule` instead of getting here. - Debug.fail(`Unexpected symbol at ${Debug.formatSyntaxKind(node.kind)}: ${Debug.formatSymbol(symbol)}`); - } - return isTypeLiteralNode(decl.parent) && isUnionTypeNode(decl.parent.parent) - ? checker.getPropertyOfType(checker.getTypeFromTypeNode(decl.parent.parent), symbol.name) - : undefined; - }); - } - - /** - * Symbol that is currently being searched for. - * This will be replaced if we find an alias for the symbol. - */ - interface Search { - /** If coming from an export, we will not recursively search for the imported symbol (since that's where we came from). */ - readonly comingFrom?: ImportExport; - - readonly symbol: Symbol; - readonly text: string; - readonly escapedText: __String; - /** Only set if `options.implementations` is true. These are the symbols checked to get the implementations of a property access. */ - readonly parents: readonly Symbol[] | undefined; - readonly allSearchSymbols: readonly Symbol[]; - - /** - * Whether a symbol is in the search set. - * Do not compare directly to `symbol` because there may be related symbols to search for. See `populateSearchSymbolSet`. - */ - includes(symbol: Symbol): boolean; - } - - const enum SpecialSearchKind { - None, - Constructor, - Class, - } - - function getNonModuleSymbolOfMergedModuleSymbol(symbol: Symbol) { - if (!(symbol.flags & (SymbolFlags.Module | SymbolFlags.Transient))) return undefined; - const decl = symbol.declarations && find(symbol.declarations, d => !isSourceFile(d) && !isModuleDeclaration(d)); - return decl && decl.symbol; - } - - /** - * Holds all state needed for the finding references. - * Unlike `Search`, there is only one `State`. - */ - class State { - /** Cache for `explicitlyinheritsFrom`. */ - readonly inheritsFromCache = createMap(); - - /** - * Type nodes can contain multiple references to the same type. For example: - * let x: Foo & (Foo & Bar) = ... - * Because we are returning the implementation locations and not the identifier locations, - * duplicate entries would be returned here as each of the type references is part of - * the same implementation. For that reason, check before we add a new entry. - */ - readonly markSeenContainingTypeReference = nodeSeenTracker(); - - /** - * It's possible that we will encounter the right side of `export { foo as bar } from "x";` more than once. - * For example: - * // b.ts - * export { foo as bar } from "./a"; - * import { bar } from "./b"; - * - * Normally at `foo as bar` we directly add `foo` and do not locally search for it (since it doesn't declare a local). - * But another reference to it may appear in the same source file. - * See `tests/cases/fourslash/transitiveExportImports3.ts`. - */ - readonly markSeenReExportRHS = nodeSeenTracker(); - - constructor( - readonly sourceFiles: readonly SourceFile[], - readonly sourceFilesSet: ReadonlyMap, - readonly specialSearchKind: SpecialSearchKind, - readonly checker: TypeChecker, - readonly cancellationToken: CancellationToken, - readonly searchMeaning: SemanticMeaning, - readonly options: Options, - private readonly result: Push) { - } - - includesSourceFile(sourceFile: SourceFile): boolean { - return this.sourceFilesSet.has(sourceFile.fileName); - } - - private importTracker: ImportTracker | undefined; - /** Gets every place to look for references of an exported symbols. See `ImportsResult` in `importTracker.ts` for more documentation. */ - getImportSearches(exportSymbol: Symbol, exportInfo: ExportInfo): ImportsResult { - if (!this.importTracker) this.importTracker = createImportTracker(this.sourceFiles, this.sourceFilesSet, this.checker, this.cancellationToken); - return this.importTracker(exportSymbol, exportInfo, this.options.use === FindReferencesUse.Rename); - } - - /** @param allSearchSymbols set of additional symbols for use by `includes`. */ - createSearch(location: Node | undefined, symbol: Symbol, comingFrom: ImportExport | undefined, searchOptions: { text?: string, allSearchSymbols?: Symbol[] } = {}): Search { - // Note: if this is an external module symbol, the name doesn't include quotes. - // Note: getLocalSymbolForExportDefault handles `export default class C {}`, but not `export default C` or `export { C as default }`. - // The other two forms seem to be handled downstream (e.g. in `skipPastExportOrImportSpecifier`), so special-casing the first form - // here appears to be intentional). - const { - text = stripQuotes(symbolName(getLocalSymbolForExportDefault(symbol) || getNonModuleSymbolOfMergedModuleSymbol(symbol) || symbol)), - allSearchSymbols = [symbol], - } = searchOptions; - const escapedText = escapeLeadingUnderscores(text); - const parents = this.options.implementations && location ? getParentSymbolsOfPropertyAccess(location, symbol, this.checker) : undefined; - return { symbol, comingFrom, text, escapedText, parents, allSearchSymbols, includes: sym => contains(allSearchSymbols, sym) }; - } - - private readonly symbolIdToReferences: Entry[][] = []; - /** - * Callback to add references for a particular searched symbol. - * This initializes a reference group, so only call this if you will add at least one reference. - */ - referenceAdder(searchSymbol: Symbol): (node: Node, kind?: NodeEntryKind) => void { - const symbolId = getSymbolId(searchSymbol); - let references = this.symbolIdToReferences[symbolId]; - if (!references) { - references = this.symbolIdToReferences[symbolId] = []; - this.result.push({ definition: { type: DefinitionKind.Symbol, symbol: searchSymbol }, references }); - } - return (node, kind) => references.push(nodeEntry(node, kind)); - } - - /** Add a reference with no associated definition. */ - addStringOrCommentReference(fileName: string, textSpan: TextSpan): void { - this.result.push({ - definition: undefined, - references: [{ kind: EntryKind.Span, fileName, textSpan }] - }); - } - - // Source file ID → symbol ID → Whether the symbol has been searched for in the source file. - private readonly sourceFileToSeenSymbols: Map[] = []; - /** Returns `true` the first time we search for a symbol in a file and `false` afterwards. */ - markSearchedSymbols(sourceFile: SourceFile, symbols: readonly Symbol[]): boolean { - const sourceId = getNodeId(sourceFile); - const seenSymbols = this.sourceFileToSeenSymbols[sourceId] || (this.sourceFileToSeenSymbols[sourceId] = createMap()); - - let anyNewSymbols = false; - for (const sym of symbols) { - anyNewSymbols = addToSeen(seenSymbols, getSymbolId(sym)) || anyNewSymbols; - } - return anyNewSymbols; - } - } - - /** Search for all imports of a given exported symbol using `State.getImportSearches`. */ - function searchForImportsOfExport(exportLocation: Node, exportSymbol: Symbol, exportInfo: ExportInfo, state: State): void { - const { importSearches, singleReferences, indirectUsers } = state.getImportSearches(exportSymbol, exportInfo); - - // For `import { foo as bar }` just add the reference to `foo`, and don't otherwise search in the file. - if (singleReferences.length) { - const addRef = state.referenceAdder(exportSymbol); - for (const singleRef of singleReferences) { - if (shouldAddSingleReference(singleRef, state)) addRef(singleRef); } + // import("foo") with no qualifier will reference the `export =` of the module, which may be referenced anyway. + return nodeEntry(reference.literal); } - - // For each import, find all references to that import in its source file. - for (const [importLocation, importSymbol] of importSearches) { - getReferencesInSourceFile(importLocation.getSourceFile(), state.createSearch(importLocation, importSymbol, ImportExport.Export), state); + else { + return { + kind: EntryKind.Span, + fileName: reference.referencingFile.fileName, + textSpan: createTextSpanFromRange(reference.ref), + }; } - - if (indirectUsers.length) { - let indirectSearch: Search | undefined; - switch (exportInfo.exportKind) { - case ExportKind.Named: - indirectSearch = state.createSearch(exportLocation, exportSymbol, ImportExport.Export); + }); + if (symbol.declarations) { + for (const decl of symbol.declarations) { + switch (decl.kind) { + case SyntaxKind.SourceFile: + // Don't include the source file itself. (This may not be ideal behavior, but awkward to include an entire file as a reference.) break; - case ExportKind.Default: - // Search for a property access to '.default'. This can't be renamed. - indirectSearch = state.options.use === FindReferencesUse.Rename ? undefined : state.createSearch(exportLocation, exportSymbol, ImportExport.Export, { text: "default" }); - break; - case ExportKind.ExportEquals: + case SyntaxKind.ModuleDeclaration: + if (sourceFilesSet.has(decl.getSourceFile().fileName)) { + references.push(nodeEntry((decl as ModuleDeclaration).name)); + } break; - } - if (indirectSearch) { - for (const indirectUser of indirectUsers) { - searchForName(indirectUser, indirectSearch, state); - } + default: + // This may be merged with something. + Debug.assert(!!(symbol.flags & SymbolFlags.Transient), "Expected a module symbol to be declared by a SourceFile or ModuleDeclaration."); } } } - - export function eachExportReference( - sourceFiles: readonly SourceFile[], - checker: TypeChecker, - cancellationToken: CancellationToken | undefined, - exportSymbol: Symbol, - exportingModuleSymbol: Symbol, - exportName: string, - isDefaultExport: boolean, - cb: (ref: Identifier) => void, - ): void { - const importTracker = createImportTracker(sourceFiles, arrayToSet(sourceFiles, f => f.fileName), checker, cancellationToken); - const { importSearches, indirectUsers } = importTracker(exportSymbol, { exportKind: isDefaultExport ? ExportKind.Default : ExportKind.Named, exportingModuleSymbol }, /*isForRename*/ false); - for (const [importLocation] of importSearches) { - cb(importLocation); - } - for (const indirectUser of indirectUsers) { - for (const node of getPossibleSymbolReferenceNodes(indirectUser, isDefaultExport ? "default" : exportName)) { - // Import specifiers should be handled by importSearches - if (isIdentifier(node) && !isImportOrExportSpecifier(node.parent) && checker.getSymbolAtLocation(node) === exportSymbol) { - cb(node); - } + const exported = symbol.exports!.get(InternalSymbolName.ExportEquals); + if (exported) { + for (const decl of exported.declarations) { + const sourceFile = decl.getSourceFile(); + if (sourceFilesSet.has(sourceFile.fileName)) { + // At `module.exports = ...`, reference node is `module` + const node = isBinaryExpression(decl) && isPropertyAccessExpression(decl.left) ? decl.left.expression : + isExportAssignment(decl) ? Debug.checkDefined(findChildOfKind(decl, SyntaxKind.ExportKeyword, sourceFile)) : + getNameOfDeclaration(decl) || decl; + references.push(nodeEntry(node)); } } } - - function shouldAddSingleReference(singleRef: Identifier | StringLiteral, state: State): boolean { - if (!hasMatchingMeaning(singleRef, state)) return false; - if (state.options.use !== FindReferencesUse.Rename) return true; - // Don't rename an import type `import("./module-name")` when renaming `name` in `export = name;` - if (!isIdentifier(singleRef)) return false; - // At `default` in `import { default as x }` or `export { default as x }`, do add a reference, but do not rename. - return !(isImportOrExportSpecifier(singleRef.parent) && singleRef.escapedText === InternalSymbolName.Default); - } - - // Go to the symbol we imported from and find references for it. - function searchForImportedSymbol(symbol: Symbol, state: State): void { - if (!symbol.declarations) return; - - for (const declaration of symbol.declarations) { - const exportingFile = declaration.getSourceFile(); - // Need to search in the file even if it's not in the search-file set, because it might export the symbol. - getReferencesInSourceFile(exportingFile, state.createSearch(declaration, symbol, ImportExport.Import), state, state.includesSourceFile(exportingFile)); - } - } - - /** Search for all occurences of an identifier in a source file (and filter out the ones that match). */ - function searchForName(sourceFile: SourceFile, search: Search, state: State): void { - if (getNameTable(sourceFile).get(search.escapedText) !== undefined) { - getReferencesInSourceFile(sourceFile, search, state); - } - } - - function getPropertySymbolOfDestructuringAssignment(location: Node, checker: TypeChecker): Symbol | undefined { - return isArrayLiteralOrObjectLiteralDestructuringPattern(location.parent.parent) - ? checker.getPropertySymbolOfDestructuringAssignment(location) - : undefined; - } - - /** - * Determines the smallest scope in which a symbol may have named references. - * Note that not every construct has been accounted for. This function can - * probably be improved. - * - * @returns undefined if the scope cannot be determined, implying that - * a reference to a symbol can occur anywhere. - */ - function getSymbolScope(symbol: Symbol): Node | undefined { - // If this is the symbol of a named function expression or named class expression, - // then named references are limited to its own scope. - const { declarations, flags, parent, valueDeclaration } = symbol; - if (valueDeclaration && (valueDeclaration.kind === SyntaxKind.FunctionExpression || valueDeclaration.kind === SyntaxKind.ClassExpression)) { - return valueDeclaration; - } - - if (!declarations) { - return undefined; - } - - // If this is private property or method, the scope is the containing class - if (flags & (SymbolFlags.Property | SymbolFlags.Method)) { - const privateDeclaration = find(declarations, d => hasModifier(d, ModifierFlags.Private) || isPrivateIdentifierPropertyDeclaration(d)); - if (privateDeclaration) { - return getAncestor(privateDeclaration, SyntaxKind.ClassDeclaration); - } - // Else this is a public property and could be accessed from anywhere. - return undefined; - } - - // If symbol is of object binding pattern element without property name we would want to - // look for property too and that could be anywhere - if (declarations.some(isObjectBindingElementWithoutPropertyName)) { + return references.length ? [{ definition: { type: DefinitionKind.Symbol, symbol }, references }] : emptyArray; + } + /** As in a `readonly prop: any` or `constructor(readonly prop: any)`, not a `readonly any[]`. */ + function isReadonlyTypeOperator(node: Node): boolean { + return node.kind === SyntaxKind.ReadonlyKeyword + && isTypeOperatorNode(node.parent) + && node.parent.operator === SyntaxKind.ReadonlyKeyword; + } + /** getReferencedSymbols for special node kinds. */ + function getReferencedSymbolsSpecial(node: Node, sourceFiles: readonly SourceFile[], cancellationToken: CancellationToken): SymbolAndEntries[] | undefined { + if (isTypeKeyword(node.kind)) { + // A void expression (i.e., `void foo()`) is not special, but the `void` type is. + if (node.kind === SyntaxKind.VoidKeyword && isVoidExpression(node.parent)) { return undefined; } - - /* - If the symbol has a parent, it's globally visible unless: - - It's a private property (handled above). - - It's a type parameter. - - The parent is an external module: then we should only search in the module (and recurse on the export later). - - But if the parent has `export as namespace`, the symbol is globally visible through that namespace. - */ - const exposedByParent = parent && !(symbol.flags & SymbolFlags.TypeParameter); - if (exposedByParent && !(isExternalModuleSymbol(parent!) && !parent!.globalExports)) { + // A modifier readonly (like on a property declaration) is not special; + // a readonly type keyword (like `readonly string[]`) is. + if (node.kind === SyntaxKind.ReadonlyKeyword && !isReadonlyTypeOperator(node)) { return undefined; } - - let scope: Node | undefined; - for (const declaration of declarations) { - const container = getContainerNode(declaration); - if (scope && scope !== container) { - // Different declarations have different containers, bail out - return undefined; - } - - if (!container || container.kind === SyntaxKind.SourceFile && !isExternalOrCommonJsModule(container)) { - // This is a global variable and not an external module, any declaration defined - // within this scope is visible outside the file - return undefined; - } - - // The search scope is the container node - scope = container; - } - - // If symbol.parent, this means we are in an export of an external module. (Otherwise we would have returned `undefined` above.) - // For an export of a module, we may be in a declaration file, and it may be accessed elsewhere. E.g.: - // declare module "a" { export type T = number; } - // declare module "b" { import { T } from "a"; export const x: T; } - // So we must search the whole source file. (Because we will mark the source file as seen, we we won't return to it when searching for imports.) - return exposedByParent ? scope!.getSourceFile() : scope; // TODO: GH#18217 - } - - /** Used as a quick check for whether a symbol is used at all in a file (besides its definition). */ - export function isSymbolReferencedInFile(definition: Identifier, checker: TypeChecker, sourceFile: SourceFile): boolean { - return eachSymbolReferenceInFile(definition, checker, sourceFile, () => true) || false; - } - - export function eachSymbolReferenceInFile(definition: Identifier, checker: TypeChecker, sourceFile: SourceFile, cb: (token: Identifier) => T): T | undefined { - const symbol = isParameterPropertyDeclaration(definition.parent, definition.parent.parent) - ? first(checker.getSymbolsOfParameterPropertyDeclaration(definition.parent, definition.text)) - : checker.getSymbolAtLocation(definition); - if (!symbol) return undefined; - for (const token of getPossibleSymbolReferenceNodes(sourceFile, symbol.name)) { - if (!isIdentifier(token) || token === definition || token.escapedText !== definition.escapedText) continue; - const referenceSymbol: Symbol = checker.getSymbolAtLocation(token)!; // See GH#19955 for why the type annotation is necessary - if (referenceSymbol === symbol - || checker.getShorthandAssignmentValueSymbol(token.parent) === symbol - || isExportSpecifier(token.parent) && getLocalSymbolForExportSpecifier(token, referenceSymbol, token.parent, checker) === symbol) { - const res = cb(token); - if (res) return res; - } - } + // Likewise, when we *are* looking for a special keyword, make sure we + // *don’t* include readonly member modifiers. + return getAllReferencesForKeyword(sourceFiles, node.kind, cancellationToken, node.kind === SyntaxKind.ReadonlyKeyword ? isReadonlyTypeOperator : undefined); } - - export function eachSignatureCall(signature: SignatureDeclaration, sourceFiles: readonly SourceFile[], checker: TypeChecker, cb: (call: CallExpression) => void): void { - if (!signature.name || !isIdentifier(signature.name)) return; - - const symbol = Debug.checkDefined(checker.getSymbolAtLocation(signature.name)); - - for (const sourceFile of sourceFiles) { - for (const name of getPossibleSymbolReferenceNodes(sourceFile, symbol.name)) { - if (!isIdentifier(name) || name === signature.name || name.escapedText !== signature.name.escapedText) continue; - const called = climbPastPropertyAccess(name); - const call = called.parent; - if (!isCallExpression(call) || call.expression !== called) continue; - const referenceSymbol = checker.getSymbolAtLocation(name); - if (referenceSymbol && checker.getRootSymbols(referenceSymbol).some(s => s === symbol)) { - cb(call); - } - } - } + // Labels + if (isJumpStatementTarget(node)) { + const labelDefinition = getTargetLabel(node.parent, node.text); + // if we have a label definition, look within its statement for references, if not, then + // the label is undefined and we have no results.. + return labelDefinition && getLabelReferencesInNode(labelDefinition.parent, labelDefinition); } - - function getPossibleSymbolReferenceNodes(sourceFile: SourceFile, symbolName: string, container: Node = sourceFile): readonly Node[] { - return getPossibleSymbolReferencePositions(sourceFile, symbolName, container).map(pos => getTouchingPropertyName(sourceFile, pos)); - } - - function getPossibleSymbolReferencePositions(sourceFile: SourceFile, symbolName: string, container: Node = sourceFile): readonly number[] { - const positions: number[] = []; - - /// TODO: Cache symbol existence for files to save text search - // Also, need to make this work for unicode escapes. - - // Be resilient in the face of a symbol with no name or zero length name - if (!symbolName || !symbolName.length) { - return positions; - } - - const text = sourceFile.text; - const sourceLength = text.length; - const symbolNameLength = symbolName.length; - - let position = text.indexOf(symbolName, container.pos); - while (position >= 0) { - // If we are past the end, stop looking - if (position > container.end) break; - - // We found a match. Make sure it's not part of a larger word (i.e. the char - // before and after it have to be a non-identifier char). - const endPosition = position + symbolNameLength; - - if ((position === 0 || !isIdentifierPart(text.charCodeAt(position - 1), ScriptTarget.Latest)) && - (endPosition === sourceLength || !isIdentifierPart(text.charCodeAt(endPosition), ScriptTarget.Latest))) { - // Found a real match. Keep searching. - positions.push(position); - } - position = text.indexOf(symbolName, position + symbolNameLength + 1); - } - - return positions; + else if (isLabelOfLabeledStatement(node)) { + // it is a label definition and not a target, search within the parent labeledStatement + return getLabelReferencesInNode(node.parent, node); } - - function getLabelReferencesInNode(container: Node, targetLabel: Identifier): SymbolAndEntries[] { - const sourceFile = container.getSourceFile(); - const labelName = targetLabel.text; - const references = mapDefined(getPossibleSymbolReferenceNodes(sourceFile, labelName, container), node => - // Only pick labels that are either the target label, or have a target that is the target label - node === targetLabel || (isJumpStatementTarget(node) && getTargetLabel(node, labelName) === targetLabel) ? nodeEntry(node) : undefined); - return [{ definition: { type: DefinitionKind.Label, node: targetLabel }, references }]; - } - - function isValidReferencePosition(node: Node, searchSymbolName: string): boolean { - // Compare the length so we filter out strict superstrings of the symbol we are looking for - switch (node.kind) { - case SyntaxKind.PrivateIdentifier: - case SyntaxKind.Identifier: - return (node as PrivateIdentifier | Identifier).text.length === searchSymbolName.length; - case SyntaxKind.NoSubstitutionTemplateLiteral: - case SyntaxKind.StringLiteral: { - const str = node as StringLiteralLike; - return (isLiteralNameOfPropertyDeclarationOrIndexAccess(str) || isNameOfModuleDeclaration(node) || isExpressionOfExternalModuleImportEqualsDeclaration(node) || (isCallExpression(node.parent) && isBindableObjectDefinePropertyCall(node.parent) && node.parent.arguments[1] === node)) && - str.text.length === searchSymbolName.length; - } - - case SyntaxKind.NumericLiteral: - return isLiteralNameOfPropertyDeclarationOrIndexAccess(node as NumericLiteral) && (node as NumericLiteral).text.length === searchSymbolName.length; - - case SyntaxKind.DefaultKeyword: - return "default".length === searchSymbolName.length; - - default: - return false; - } + if (isThis(node)) { + return getReferencesForThisKeyword(node, sourceFiles, cancellationToken); } - - function getAllReferencesForKeyword(sourceFiles: readonly SourceFile[], keywordKind: SyntaxKind, cancellationToken: CancellationToken, filter?: (node: Node) => boolean): SymbolAndEntries[] | undefined { - const references = flatMap(sourceFiles, sourceFile => { - cancellationToken.throwIfCancellationRequested(); - return mapDefined(getPossibleSymbolReferenceNodes(sourceFile, tokenToString(keywordKind)!, sourceFile), referenceLocation => { - if (referenceLocation.kind === keywordKind && (!filter || filter(referenceLocation))) { - return nodeEntry(referenceLocation); - } - }); - }); - return references.length ? [{ definition: { type: DefinitionKind.Keyword, node: references[0].node }, references }] : undefined; + if (node.kind === SyntaxKind.SuperKeyword) { + return getReferencesForSuperKeyword(node); } - - function getReferencesInSourceFile(sourceFile: SourceFile, search: Search, state: State, addReferencesHere = true): void { - state.cancellationToken.throwIfCancellationRequested(); - return getReferencesInContainer(sourceFile, sourceFile, search, state, addReferencesHere); + return undefined; + } + /** Core find-all-references algorithm for a normal symbol. */ + function getReferencedSymbolsForSymbol(originalSymbol: Symbol, node: Node | undefined, sourceFiles: readonly SourceFile[], sourceFilesSet: ts.ReadonlyMap, checker: TypeChecker, cancellationToken: CancellationToken, options: Options): SymbolAndEntries[] { + const symbol = node && skipPastExportOrImportSpecifierOrUnion(originalSymbol, node, checker, /*useLocalSymbolForExportSpecifier*/ !isForRenameWithPrefixAndSuffixText(options)) || originalSymbol; + // Compute the meaning from the location and the symbol it references + const searchMeaning = node ? getIntersectingMeaningFromDeclarations(node, symbol) : SemanticMeaning.All; + const result: SymbolAndEntries[] = []; + const state = new State(sourceFiles, sourceFilesSet, node ? getSpecialSearchKind(node) : SpecialSearchKind.None, checker, cancellationToken, searchMeaning, options, result); + const exportSpecifier = !isForRenameWithPrefixAndSuffixText(options) ? undefined : find(symbol.declarations, isExportSpecifier); + if (exportSpecifier) { + // When renaming at an export specifier, rename the export and not the thing being exported. + getReferencesAtExportSpecifier(exportSpecifier.name, symbol, exportSpecifier, state.createSearch(node, originalSymbol, /*comingFrom*/ undefined), state, /*addReferencesHere*/ true, /*alwaysGetReferences*/ true); + } + else if (node && node.kind === SyntaxKind.DefaultKeyword) { + addReference(node, symbol, state); + searchForImportsOfExport(node, symbol, { exportingModuleSymbol: Debug.checkDefined(symbol.parent, "Expected export symbol to have a parent"), exportKind: ExportKind.Default }, state); } - - /** - * Search within node "container" for references for a search value, where the search value is defined as a - * tuple of(searchSymbol, searchText, searchLocation, and searchMeaning). - * searchLocation: a node where the search value - */ - function getReferencesInContainer(container: Node, sourceFile: SourceFile, search: Search, state: State, addReferencesHere: boolean): void { - if (!state.markSearchedSymbols(sourceFile, search.allSearchSymbols)) { - return; - } - - for (const position of getPossibleSymbolReferencePositions(sourceFile, search.text, container)) { - getReferencesAtLocation(sourceFile, position, search, state, addReferencesHere); + else { + const search = state.createSearch(node, symbol, /*comingFrom*/ undefined, { allSearchSymbols: node ? populateSearchSymbolSet(symbol, node, checker, options.use === FindReferencesUse.Rename, !!options.providePrefixAndSuffixTextForRename, !!options.implementations) : [symbol] }); + getReferencesInContainerOrFiles(symbol, state, search); + } + return result; + } + function getReferencesInContainerOrFiles(symbol: Symbol, state: State, search: Search): void { + // Try to get the smallest valid scope that we can limit our search to; + // otherwise we'll need to search globally (i.e. include each file). + const scope = getSymbolScope(symbol); + if (scope) { + getReferencesInContainer(scope, scope.getSourceFile(), search, state, /*addReferencesHere*/ !(isSourceFile(scope) && !contains(state.sourceFiles, scope))); + } + else { + // Global search + for (const sourceFile of state.sourceFiles) { + state.cancellationToken.throwIfCancellationRequested(); + searchForName(sourceFile, search, state); } } - - function hasMatchingMeaning(referenceLocation: Node, state: State): boolean { - return !!(getMeaningFromLocation(referenceLocation) & state.searchMeaning); - } - - function getReferencesAtLocation(sourceFile: SourceFile, position: number, search: Search, state: State, addReferencesHere: boolean): void { - const referenceLocation = getTouchingPropertyName(sourceFile, position); - - if (!isValidReferencePosition(referenceLocation, search.text)) { - // This wasn't the start of a token. Check to see if it might be a - // match in a comment or string if that's what the caller is asking - // for. - if (!state.options.implementations && (state.options.findInStrings && isInString(sourceFile, position) || state.options.findInComments && isInNonReferenceComment(sourceFile, position))) { - // In the case where we're looking inside comments/strings, we don't have - // an actual definition. So just use 'undefined' here. Features like - // 'Rename' won't care (as they ignore the definitions), and features like - // 'FindReferences' will just filter out these results. - state.addStringOrCommentReference(sourceFile.fileName, createTextSpan(position, search.text.length)); + } + function getSpecialSearchKind(node: Node): SpecialSearchKind { + switch (node.kind) { + case SyntaxKind.ConstructorKeyword: + return SpecialSearchKind.Constructor; + case SyntaxKind.Identifier: + if (isClassLike(node.parent)) { + Debug.assert(node.parent.name === node); + return SpecialSearchKind.Class; } - - return; - } - - if (!hasMatchingMeaning(referenceLocation, state)) return; - - const referenceSymbol = state.checker.getSymbolAtLocation(referenceLocation); - if (!referenceSymbol) { - return; - } - - const parent = referenceLocation.parent; - if (isImportSpecifier(parent) && parent.propertyName === referenceLocation) { - // This is added through `singleReferences` in ImportsResult. If we happen to see it again, don't add it again. - return; - } - - if (isExportSpecifier(parent)) { - Debug.assert(referenceLocation.kind === SyntaxKind.Identifier); - getReferencesAtExportSpecifier(referenceLocation as Identifier, referenceSymbol, parent, search, state, addReferencesHere); - return; + // falls through + default: + return SpecialSearchKind.None; + } + } + /** Handle a few special cases relating to export/import specifiers. */ + function skipPastExportOrImportSpecifierOrUnion(symbol: Symbol, node: Node, checker: TypeChecker, useLocalSymbolForExportSpecifier: boolean): Symbol | undefined { + const { parent } = node; + if (isExportSpecifier(parent) && useLocalSymbolForExportSpecifier) { + return getLocalSymbolForExportSpecifier((node as Identifier), symbol, parent, checker); + } + // If the symbol is declared as part of a declaration like `{ type: "a" } | { type: "b" }`, use the property on the union type to get more references. + return firstDefined(symbol.declarations, decl => { + if (!decl.parent) { + // Ignore UMD module and global merge + if (symbol.flags & SymbolFlags.Transient) + return undefined; + // Assertions for GH#21814. We should be handling SourceFile symbols in `getReferencedSymbolsForModule` instead of getting here. + Debug.fail(`Unexpected symbol at ${Debug.formatSyntaxKind(node.kind)}: ${Debug.formatSymbol(symbol)}`); } - - const relatedSymbol = getRelatedSymbol(search, referenceSymbol, referenceLocation, state); - if (!relatedSymbol) { - getReferenceForShorthandProperty(referenceSymbol, search, state); - return; + return isTypeLiteralNode(decl.parent) && isUnionTypeNode(decl.parent.parent) + ? checker.getPropertyOfType(checker.getTypeFromTypeNode(decl.parent.parent), symbol.name) + : undefined; + }); + } + /** + * Symbol that is currently being searched for. + * This will be replaced if we find an alias for the symbol. + */ + interface Search { + /** If coming from an export, we will not recursively search for the imported symbol (since that's where we came from). */ + readonly comingFrom?: ImportExport; + readonly symbol: Symbol; + readonly text: string; + readonly escapedText: __String; + /** Only set if `options.implementations` is true. These are the symbols checked to get the implementations of a property access. */ + readonly parents: readonly Symbol[] | undefined; + readonly allSearchSymbols: readonly Symbol[]; + /** + * Whether a symbol is in the search set. + * Do not compare directly to `symbol` because there may be related symbols to search for. See `populateSearchSymbolSet`. + */ + includes(symbol: Symbol): boolean; + } + const enum SpecialSearchKind { + None, + Constructor, + Class + } + function getNonModuleSymbolOfMergedModuleSymbol(symbol: Symbol) { + if (!(symbol.flags & (SymbolFlags.Module | SymbolFlags.Transient))) + return undefined; + const decl = symbol.declarations && find(symbol.declarations, d => !isSourceFile(d) && !isModuleDeclaration(d)); + return decl && decl.symbol; + } + /** + * Holds all state needed for the finding references. + * Unlike `Search`, there is only one `State`. + */ + class State { + /** Cache for `explicitlyinheritsFrom`. */ + readonly inheritsFromCache = createMap(); + /** + * Type nodes can contain multiple references to the same type. For example: + * let x: Foo & (Foo & Bar) = ... + * Because we are returning the implementation locations and not the identifier locations, + * duplicate entries would be returned here as each of the type references is part of + * the same implementation. For that reason, check before we add a new entry. + */ + readonly markSeenContainingTypeReference = nodeSeenTracker(); + /** + * It's possible that we will encounter the right side of `export { foo as bar } from "x";` more than once. + * For example: + * // b.ts + * export { foo as bar } from "./a"; + * import { bar } from "./b"; + * + * Normally at `foo as bar` we directly add `foo` and do not locally search for it (since it doesn't declare a local). + * But another reference to it may appear in the same source file. + * See `tests/cases/fourslash/transitiveExportImports3.ts`. + */ + readonly markSeenReExportRHS = nodeSeenTracker(); + constructor(readonly sourceFiles: readonly SourceFile[], readonly sourceFilesSet: ts.ReadonlyMap, readonly specialSearchKind: SpecialSearchKind, readonly checker: TypeChecker, readonly cancellationToken: CancellationToken, readonly searchMeaning: SemanticMeaning, readonly options: Options, private readonly result: Push) { + } + includesSourceFile(sourceFile: SourceFile): boolean { + return this.sourceFilesSet.has(sourceFile.fileName); + } + private importTracker: ImportTracker | undefined; + /** Gets every place to look for references of an exported symbols. See `ImportsResult` in `importTracker.ts` for more documentation. */ + getImportSearches(exportSymbol: Symbol, exportInfo: ExportInfo): ImportsResult { + if (!this.importTracker) + this.importTracker = createImportTracker(this.sourceFiles, this.sourceFilesSet, this.checker, this.cancellationToken); + return this.importTracker(exportSymbol, exportInfo, this.options.use === FindReferencesUse.Rename); + } + /** @param allSearchSymbols set of additional symbols for use by `includes`. */ + createSearch(location: Node | undefined, symbol: Symbol, comingFrom: ImportExport | undefined, searchOptions: { + text?: string; + allSearchSymbols?: Symbol[]; + } = {}): Search { + // Note: if this is an external module symbol, the name doesn't include quotes. + // Note: getLocalSymbolForExportDefault handles `export default class C {}`, but not `export default C` or `export { C as default }`. + // The other two forms seem to be handled downstream (e.g. in `skipPastExportOrImportSpecifier`), so special-casing the first form + // here appears to be intentional). + const { text = stripQuotes(symbolName(getLocalSymbolForExportDefault(symbol) || getNonModuleSymbolOfMergedModuleSymbol(symbol) || symbol)), allSearchSymbols = [symbol], } = searchOptions; + const escapedText = escapeLeadingUnderscores(text); + const parents = this.options.implementations && location ? getParentSymbolsOfPropertyAccess(location, symbol, this.checker) : undefined; + return { symbol, comingFrom, text, escapedText, parents, allSearchSymbols, includes: sym => contains(allSearchSymbols, sym) }; + } + private readonly symbolIdToReferences: Entry[][] = []; + /** + * Callback to add references for a particular searched symbol. + * This initializes a reference group, so only call this if you will add at least one reference. + */ + referenceAdder(searchSymbol: Symbol): (node: Node, kind?: NodeEntryKind) => void { + const symbolId = getSymbolId(searchSymbol); + let references = this.symbolIdToReferences[symbolId]; + if (!references) { + references = this.symbolIdToReferences[symbolId] = []; + this.result.push({ definition: { type: DefinitionKind.Symbol, symbol: searchSymbol }, references }); + } + return (node, kind) => references.push(nodeEntry(node, kind)); + } + /** Add a reference with no associated definition. */ + addStringOrCommentReference(fileName: string, textSpan: TextSpan): void { + this.result.push({ + definition: undefined, + references: [{ kind: EntryKind.Span, fileName, textSpan }] + }); + } + // Source file ID → symbol ID → Whether the symbol has been searched for in the source file. + private readonly sourceFileToSeenSymbols: ts.Map[] = []; + /** Returns `true` the first time we search for a symbol in a file and `false` afterwards. */ + markSearchedSymbols(sourceFile: SourceFile, symbols: readonly Symbol[]): boolean { + const sourceId = getNodeId(sourceFile); + const seenSymbols = this.sourceFileToSeenSymbols[sourceId] || (this.sourceFileToSeenSymbols[sourceId] = createMap()); + let anyNewSymbols = false; + for (const sym of symbols) { + anyNewSymbols = addToSeen(seenSymbols, getSymbolId(sym)) || anyNewSymbols; } - - switch (state.specialSearchKind) { - case SpecialSearchKind.None: - if (addReferencesHere) addReference(referenceLocation, relatedSymbol, state); + return anyNewSymbols; + } + } + /** Search for all imports of a given exported symbol using `State.getImportSearches`. */ + function searchForImportsOfExport(exportLocation: Node, exportSymbol: Symbol, exportInfo: ExportInfo, state: State): void { + const { importSearches, singleReferences, indirectUsers } = state.getImportSearches(exportSymbol, exportInfo); + // For `import { foo as bar }` just add the reference to `foo`, and don't otherwise search in the file. + if (singleReferences.length) { + const addRef = state.referenceAdder(exportSymbol); + for (const singleRef of singleReferences) { + if (shouldAddSingleReference(singleRef, state)) + addRef(singleRef); + } + } + // For each import, find all references to that import in its source file. + for (const [importLocation, importSymbol] of importSearches) { + getReferencesInSourceFile(importLocation.getSourceFile(), state.createSearch(importLocation, importSymbol, ImportExport.Export), state); + } + if (indirectUsers.length) { + let indirectSearch: Search | undefined; + switch (exportInfo.exportKind) { + case ExportKind.Named: + indirectSearch = state.createSearch(exportLocation, exportSymbol, ImportExport.Export); break; - case SpecialSearchKind.Constructor: - addConstructorReferences(referenceLocation, sourceFile, search, state); + case ExportKind.Default: + // Search for a property access to '.default'. This can't be renamed. + indirectSearch = state.options.use === FindReferencesUse.Rename ? undefined : state.createSearch(exportLocation, exportSymbol, ImportExport.Export, { text: "default" }); break; - case SpecialSearchKind.Class: - addClassStaticThisReferences(referenceLocation, search, state); + case ExportKind.ExportEquals: break; - default: - Debug.assertNever(state.specialSearchKind); - } - - getImportOrExportReferences(referenceLocation, referenceSymbol, search, state); - } - - function getReferencesAtExportSpecifier( - referenceLocation: Identifier, - referenceSymbol: Symbol, - exportSpecifier: ExportSpecifier, - search: Search, - state: State, - addReferencesHere: boolean, - alwaysGetReferences?: boolean, - ): void { - Debug.assert(!alwaysGetReferences || !!state.options.providePrefixAndSuffixTextForRename, "If alwaysGetReferences is true, then prefix/suffix text must be enabled"); - - const { parent, propertyName, name } = exportSpecifier; - const exportDeclaration = parent.parent; - const localSymbol = getLocalSymbolForExportSpecifier(referenceLocation, referenceSymbol, exportSpecifier, state.checker); - if (!alwaysGetReferences && !search.includes(localSymbol)) { - return; - } - - if (!propertyName) { - // Don't rename at `export { default } from "m";`. (but do continue to search for imports of the re-export) - if (!(state.options.use === FindReferencesUse.Rename && (name.escapedText === InternalSymbolName.Default))) { - addRef(); - } } - else if (referenceLocation === propertyName) { - // For `export { foo as bar } from "baz"`, "`foo`" will be added from the singleReferences for import searches of the original export. - // For `export { foo as bar };`, where `foo` is a local, so add it now. - if (!exportDeclaration.moduleSpecifier) { - addRef(); - } - - if (addReferencesHere && state.options.use !== FindReferencesUse.Rename && state.markSeenReExportRHS(name)) { - addReference(name, Debug.checkDefined(exportSpecifier.symbol), state); + if (indirectSearch) { + for (const indirectUser of indirectUsers) { + searchForName(indirectUser, indirectSearch, state); } } - else { - if (state.markSeenReExportRHS(referenceLocation)) { - addRef(); + } + } + export function eachExportReference(sourceFiles: readonly SourceFile[], checker: TypeChecker, cancellationToken: CancellationToken | undefined, exportSymbol: Symbol, exportingModuleSymbol: Symbol, exportName: string, isDefaultExport: boolean, cb: (ref: Identifier) => void): void { + const importTracker = createImportTracker(sourceFiles, arrayToSet(sourceFiles, f => f.fileName), checker, cancellationToken); + const { importSearches, indirectUsers } = importTracker(exportSymbol, { exportKind: isDefaultExport ? ExportKind.Default : ExportKind.Named, exportingModuleSymbol }, /*isForRename*/ false); + for (const [importLocation] of importSearches) { + cb(importLocation); + } + for (const indirectUser of indirectUsers) { + for (const node of getPossibleSymbolReferenceNodes(indirectUser, isDefaultExport ? "default" : exportName)) { + // Import specifiers should be handled by importSearches + if (isIdentifier(node) && !isImportOrExportSpecifier(node.parent) && checker.getSymbolAtLocation(node) === exportSymbol) { + cb(node); } } - - // For `export { foo as bar }`, rename `foo`, but not `bar`. - if (!isForRenameWithPrefixAndSuffixText(state.options) || alwaysGetReferences) { - const isDefaultExport = referenceLocation.originalKeywordKind === SyntaxKind.DefaultKeyword - || exportSpecifier.name.originalKeywordKind === SyntaxKind.DefaultKeyword; - const exportKind = isDefaultExport ? ExportKind.Default : ExportKind.Named; - const exportSymbol = Debug.checkDefined(exportSpecifier.symbol); - const exportInfo = getExportInfo(exportSymbol, exportKind, state.checker); - if (exportInfo) { - searchForImportsOfExport(referenceLocation, exportSymbol, exportInfo, state); - } + } + } + function shouldAddSingleReference(singleRef: Identifier | StringLiteral, state: State): boolean { + if (!hasMatchingMeaning(singleRef, state)) + return false; + if (state.options.use !== FindReferencesUse.Rename) + return true; + // Don't rename an import type `import("./module-name")` when renaming `name` in `export = name;` + if (!isIdentifier(singleRef)) + return false; + // At `default` in `import { default as x }` or `export { default as x }`, do add a reference, but do not rename. + return !(isImportOrExportSpecifier(singleRef.parent) && singleRef.escapedText === InternalSymbolName.Default); + } + // Go to the symbol we imported from and find references for it. + function searchForImportedSymbol(symbol: Symbol, state: State): void { + if (!symbol.declarations) + return; + for (const declaration of symbol.declarations) { + const exportingFile = declaration.getSourceFile(); + // Need to search in the file even if it's not in the search-file set, because it might export the symbol. + getReferencesInSourceFile(exportingFile, state.createSearch(declaration, symbol, ImportExport.Import), state, state.includesSourceFile(exportingFile)); + } + } + /** Search for all occurences of an identifier in a source file (and filter out the ones that match). */ + function searchForName(sourceFile: SourceFile, search: Search, state: State): void { + if (getNameTable(sourceFile).get(search.escapedText) !== undefined) { + getReferencesInSourceFile(sourceFile, search, state); + } + } + function getPropertySymbolOfDestructuringAssignment(location: Node, checker: TypeChecker): Symbol | undefined { + return isArrayLiteralOrObjectLiteralDestructuringPattern(location.parent.parent) + ? checker.getPropertySymbolOfDestructuringAssignment((location)) + : undefined; + } + /** + * Determines the smallest scope in which a symbol may have named references. + * Note that not every construct has been accounted for. This function can + * probably be improved. + * + * @returns undefined if the scope cannot be determined, implying that + * a reference to a symbol can occur anywhere. + */ + function getSymbolScope(symbol: Symbol): Node | undefined { + // If this is the symbol of a named function expression or named class expression, + // then named references are limited to its own scope. + const { declarations, flags, parent, valueDeclaration } = symbol; + if (valueDeclaration && (valueDeclaration.kind === SyntaxKind.FunctionExpression || valueDeclaration.kind === SyntaxKind.ClassExpression)) { + return valueDeclaration; + } + if (!declarations) { + return undefined; + } + // If this is private property or method, the scope is the containing class + if (flags & (SymbolFlags.Property | SymbolFlags.Method)) { + const privateDeclaration = find(declarations, d => hasModifier(d, ModifierFlags.Private) || isPrivateIdentifierPropertyDeclaration(d)); + if (privateDeclaration) { + return getAncestor(privateDeclaration, SyntaxKind.ClassDeclaration); } - - // At `export { x } from "foo"`, also search for the imported symbol `"foo".x`. - if (search.comingFrom !== ImportExport.Export && exportDeclaration.moduleSpecifier && !propertyName && !isForRenameWithPrefixAndSuffixText(state.options)) { - const imported = state.checker.getExportSpecifierLocalTargetSymbol(exportSpecifier); - if (imported) searchForImportedSymbol(imported, state); + // Else this is a public property and could be accessed from anywhere. + return undefined; + } + // If symbol is of object binding pattern element without property name we would want to + // look for property too and that could be anywhere + if (declarations.some(isObjectBindingElementWithoutPropertyName)) { + return undefined; + } + /* + If the symbol has a parent, it's globally visible unless: + - It's a private property (handled above). + - It's a type parameter. + - The parent is an external module: then we should only search in the module (and recurse on the export later). + - But if the parent has `export as namespace`, the symbol is globally visible through that namespace. + */ + const exposedByParent = parent && !(symbol.flags & SymbolFlags.TypeParameter); + if (exposedByParent && !(isExternalModuleSymbol((parent!)) && !parent!.globalExports)) { + return undefined; + } + let scope: Node | undefined; + for (const declaration of declarations) { + const container = getContainerNode(declaration); + if (scope && scope !== container) { + // Different declarations have different containers, bail out + return undefined; } - - function addRef() { - if (addReferencesHere) addReference(referenceLocation, localSymbol, state); + if (!container || container.kind === SyntaxKind.SourceFile && !isExternalOrCommonJsModule((container))) { + // This is a global variable and not an external module, any declaration defined + // within this scope is visible outside the file + return undefined; } + // The search scope is the container node + scope = container; } - - function getLocalSymbolForExportSpecifier(referenceLocation: Identifier, referenceSymbol: Symbol, exportSpecifier: ExportSpecifier, checker: TypeChecker): Symbol { - return isExportSpecifierAlias(referenceLocation, exportSpecifier) && checker.getExportSpecifierLocalTargetSymbol(exportSpecifier) || referenceSymbol; - } - - function isExportSpecifierAlias(referenceLocation: Identifier, exportSpecifier: ExportSpecifier): boolean { - const { parent, propertyName, name } = exportSpecifier; - Debug.assert(propertyName === referenceLocation || name === referenceLocation); - if (propertyName) { - // Given `export { foo as bar } [from "someModule"]`: It's an alias at `foo`, but at `bar` it's a new symbol. - return propertyName === referenceLocation; - } - else { - // `export { foo } from "foo"` is a re-export. - // `export { foo };` is not a re-export, it creates an alias for the local variable `foo`. - return !parent.parent.moduleSpecifier; + // If symbol.parent, this means we are in an export of an external module. (Otherwise we would have returned `undefined` above.) + // For an export of a module, we may be in a declaration file, and it may be accessed elsewhere. E.g.: + // declare module "a" { export type T = number; } + // declare module "b" { import { T } from "a"; export const x: T; } + // So we must search the whole source file. (Because we will mark the source file as seen, we we won't return to it when searching for imports.) + return exposedByParent ? scope!.getSourceFile() : scope; // TODO: GH#18217 + } + /** Used as a quick check for whether a symbol is used at all in a file (besides its definition). */ + export function isSymbolReferencedInFile(definition: Identifier, checker: TypeChecker, sourceFile: SourceFile): boolean { + return eachSymbolReferenceInFile(definition, checker, sourceFile, () => true) || false; + } + export function eachSymbolReferenceInFile(definition: Identifier, checker: TypeChecker, sourceFile: SourceFile, cb: (token: Identifier) => T): T | undefined { + const symbol = isParameterPropertyDeclaration(definition.parent, definition.parent.parent) + ? first(checker.getSymbolsOfParameterPropertyDeclaration(definition.parent, definition.text)) + : checker.getSymbolAtLocation(definition); + if (!symbol) + return undefined; + for (const token of getPossibleSymbolReferenceNodes(sourceFile, symbol.name)) { + if (!isIdentifier(token) || token === definition || token.escapedText !== definition.escapedText) + continue; + const referenceSymbol: Symbol = (checker.getSymbolAtLocation(token)!); // See GH#19955 for why the type annotation is necessary + if (referenceSymbol === symbol + || checker.getShorthandAssignmentValueSymbol(token.parent) === symbol + || isExportSpecifier(token.parent) && getLocalSymbolForExportSpecifier(token, referenceSymbol, token.parent, checker) === symbol) { + const res = cb(token); + if (res) + return res; } } - - function getImportOrExportReferences(referenceLocation: Node, referenceSymbol: Symbol, search: Search, state: State): void { - const importOrExport = getImportOrExportSymbol(referenceLocation, referenceSymbol, state.checker, search.comingFrom === ImportExport.Export); - if (!importOrExport) return; - - const { symbol } = importOrExport; - - if (importOrExport.kind === ImportExport.Import) { - if (!(isForRenameWithPrefixAndSuffixText(state.options))) { - searchForImportedSymbol(symbol, state); + } + export function eachSignatureCall(signature: SignatureDeclaration, sourceFiles: readonly SourceFile[], checker: TypeChecker, cb: (call: CallExpression) => void): void { + if (!signature.name || !isIdentifier(signature.name)) + return; + const symbol = Debug.checkDefined(checker.getSymbolAtLocation(signature.name)); + for (const sourceFile of sourceFiles) { + for (const name of getPossibleSymbolReferenceNodes(sourceFile, symbol.name)) { + if (!isIdentifier(name) || name === signature.name || name.escapedText !== signature.name.escapedText) + continue; + const called = climbPastPropertyAccess(name); + const call = called.parent; + if (!isCallExpression(call) || call.expression !== called) + continue; + const referenceSymbol = checker.getSymbolAtLocation(name); + if (referenceSymbol && checker.getRootSymbols(referenceSymbol).some(s => s === symbol)) { + cb(call); } } - else { - searchForImportsOfExport(referenceLocation, symbol, importOrExport.exportInfo, state); - } } - - function getReferenceForShorthandProperty({ flags, valueDeclaration }: Symbol, search: Search, state: State): void { - const shorthandValueSymbol = state.checker.getShorthandAssignmentValueSymbol(valueDeclaration)!; - const name = valueDeclaration && getNameOfDeclaration(valueDeclaration); - /* - * Because in short-hand property assignment, an identifier which stored as name of the short-hand property assignment - * has two meanings: property name and property value. Therefore when we do findAllReference at the position where - * an identifier is declared, the language service should return the position of the variable declaration as well as - * the position in short-hand property assignment excluding property accessing. However, if we do findAllReference at the - * position of property accessing, the referenceEntry of such position will be handled in the first case. - */ - if (!(flags & SymbolFlags.Transient) && name && search.includes(shorthandValueSymbol)) { - addReference(name, shorthandValueSymbol, state); + } + function getPossibleSymbolReferenceNodes(sourceFile: SourceFile, symbolName: string, container: Node = sourceFile): readonly Node[] { + return getPossibleSymbolReferencePositions(sourceFile, symbolName, container).map(pos => getTouchingPropertyName(sourceFile, pos)); + } + function getPossibleSymbolReferencePositions(sourceFile: SourceFile, symbolName: string, container: Node = sourceFile): readonly number[] { + const positions: number[] = []; + /// TODO: Cache symbol existence for files to save text search + // Also, need to make this work for unicode escapes. + // Be resilient in the face of a symbol with no name or zero length name + if (!symbolName || !symbolName.length) { + return positions; + } + const text = sourceFile.text; + const sourceLength = text.length; + const symbolNameLength = symbolName.length; + let position = text.indexOf(symbolName, container.pos); + while (position >= 0) { + // If we are past the end, stop looking + if (position > container.end) + break; + // We found a match. Make sure it's not part of a larger word (i.e. the char + // before and after it have to be a non-identifier char). + const endPosition = position + symbolNameLength; + if ((position === 0 || !isIdentifierPart(text.charCodeAt(position - 1), ScriptTarget.Latest)) && + (endPosition === sourceLength || !isIdentifierPart(text.charCodeAt(endPosition), ScriptTarget.Latest))) { + // Found a real match. Keep searching. + positions.push(position); + } + position = text.indexOf(symbolName, position + symbolNameLength + 1); + } + return positions; + } + function getLabelReferencesInNode(container: Node, targetLabel: Identifier): SymbolAndEntries[] { + const sourceFile = container.getSourceFile(); + const labelName = targetLabel.text; + const references = mapDefined(getPossibleSymbolReferenceNodes(sourceFile, labelName, container), node => + // Only pick labels that are either the target label, or have a target that is the target label + node === targetLabel || (isJumpStatementTarget(node) && getTargetLabel(node, labelName) === targetLabel) ? nodeEntry(node) : undefined); + return [{ definition: { type: DefinitionKind.Label, node: targetLabel }, references }]; + } + function isValidReferencePosition(node: Node, searchSymbolName: string): boolean { + // Compare the length so we filter out strict superstrings of the symbol we are looking for + switch (node.kind) { + case SyntaxKind.PrivateIdentifier: + case SyntaxKind.Identifier: + return (node as PrivateIdentifier | Identifier).text.length === searchSymbolName.length; + case SyntaxKind.NoSubstitutionTemplateLiteral: + case SyntaxKind.StringLiteral: { + const str = (node as StringLiteralLike); + return (isLiteralNameOfPropertyDeclarationOrIndexAccess(str) || isNameOfModuleDeclaration(node) || isExpressionOfExternalModuleImportEqualsDeclaration(node) || (isCallExpression(node.parent) && isBindableObjectDefinePropertyCall(node.parent) && node.parent.arguments[1] === node)) && + str.text.length === searchSymbolName.length; + } + case SyntaxKind.NumericLiteral: + return isLiteralNameOfPropertyDeclarationOrIndexAccess((node as NumericLiteral)) && (node as NumericLiteral).text.length === searchSymbolName.length; + case SyntaxKind.DefaultKeyword: + return "default".length === searchSymbolName.length; + default: + return false; + } + } + function getAllReferencesForKeyword(sourceFiles: readonly SourceFile[], keywordKind: SyntaxKind, cancellationToken: CancellationToken, filter?: (node: Node) => boolean): SymbolAndEntries[] | undefined { + const references = flatMap(sourceFiles, sourceFile => { + cancellationToken.throwIfCancellationRequested(); + return mapDefined(getPossibleSymbolReferenceNodes(sourceFile, (tokenToString(keywordKind)!), sourceFile), referenceLocation => { + if (referenceLocation.kind === keywordKind && (!filter || filter(referenceLocation))) { + return nodeEntry(referenceLocation); + } + }); + }); + return references.length ? [{ definition: { type: DefinitionKind.Keyword, node: references[0].node }, references }] : undefined; + } + function getReferencesInSourceFile(sourceFile: SourceFile, search: Search, state: State, addReferencesHere = true): void { + state.cancellationToken.throwIfCancellationRequested(); + return getReferencesInContainer(sourceFile, sourceFile, search, state, addReferencesHere); + } + /** + * Search within node "container" for references for a search value, where the search value is defined as a + * tuple of(searchSymbol, searchText, searchLocation, and searchMeaning). + * searchLocation: a node where the search value + */ + function getReferencesInContainer(container: Node, sourceFile: SourceFile, search: Search, state: State, addReferencesHere: boolean): void { + if (!state.markSearchedSymbols(sourceFile, search.allSearchSymbols)) { + return; + } + for (const position of getPossibleSymbolReferencePositions(sourceFile, search.text, container)) { + getReferencesAtLocation(sourceFile, position, search, state, addReferencesHere); + } + } + function hasMatchingMeaning(referenceLocation: Node, state: State): boolean { + return !!(getMeaningFromLocation(referenceLocation) & state.searchMeaning); + } + function getReferencesAtLocation(sourceFile: SourceFile, position: number, search: Search, state: State, addReferencesHere: boolean): void { + const referenceLocation = getTouchingPropertyName(sourceFile, position); + if (!isValidReferencePosition(referenceLocation, search.text)) { + // This wasn't the start of a token. Check to see if it might be a + // match in a comment or string if that's what the caller is asking + // for. + if (!state.options.implementations && (state.options.findInStrings && isInString(sourceFile, position) || state.options.findInComments && isInNonReferenceComment(sourceFile, position))) { + // In the case where we're looking inside comments/strings, we don't have + // an actual definition. So just use 'undefined' here. Features like + // 'Rename' won't care (as they ignore the definitions), and features like + // 'FindReferences' will just filter out these results. + state.addStringOrCommentReference(sourceFile.fileName, createTextSpan(position, search.text.length)); + } + return; + } + if (!hasMatchingMeaning(referenceLocation, state)) + return; + const referenceSymbol = state.checker.getSymbolAtLocation(referenceLocation); + if (!referenceSymbol) { + return; + } + const parent = referenceLocation.parent; + if (isImportSpecifier(parent) && parent.propertyName === referenceLocation) { + // This is added through `singleReferences` in ImportsResult. If we happen to see it again, don't add it again. + return; + } + if (isExportSpecifier(parent)) { + Debug.assert(referenceLocation.kind === SyntaxKind.Identifier); + getReferencesAtExportSpecifier((referenceLocation as Identifier), referenceSymbol, parent, search, state, addReferencesHere); + return; + } + const relatedSymbol = getRelatedSymbol(search, referenceSymbol, referenceLocation, state); + if (!relatedSymbol) { + getReferenceForShorthandProperty(referenceSymbol, search, state); + return; + } + switch (state.specialSearchKind) { + case SpecialSearchKind.None: + if (addReferencesHere) + addReference(referenceLocation, relatedSymbol, state); + break; + case SpecialSearchKind.Constructor: + addConstructorReferences(referenceLocation, sourceFile, search, state); + break; + case SpecialSearchKind.Class: + addClassStaticThisReferences(referenceLocation, search, state); + break; + default: + Debug.assertNever(state.specialSearchKind); + } + getImportOrExportReferences(referenceLocation, referenceSymbol, search, state); + } + function getReferencesAtExportSpecifier(referenceLocation: Identifier, referenceSymbol: Symbol, exportSpecifier: ExportSpecifier, search: Search, state: State, addReferencesHere: boolean, alwaysGetReferences?: boolean): void { + Debug.assert(!alwaysGetReferences || !!state.options.providePrefixAndSuffixTextForRename, "If alwaysGetReferences is true, then prefix/suffix text must be enabled"); + const { parent, propertyName, name } = exportSpecifier; + const exportDeclaration = parent.parent; + const localSymbol = getLocalSymbolForExportSpecifier(referenceLocation, referenceSymbol, exportSpecifier, state.checker); + if (!alwaysGetReferences && !search.includes(localSymbol)) { + return; + } + if (!propertyName) { + // Don't rename at `export { default } from "m";`. (but do continue to search for imports of the re-export) + if (!(state.options.use === FindReferencesUse.Rename && (name.escapedText === InternalSymbolName.Default))) { + addRef(); } } - - function addReference(referenceLocation: Node, relatedSymbol: Symbol | RelatedSymbol, state: State): void { - const { kind, symbol } = "kind" in relatedSymbol ? relatedSymbol : { kind: undefined, symbol: relatedSymbol }; // eslint-disable-line no-in-operator - const addRef = state.referenceAdder(symbol); - if (state.options.implementations) { - addImplementationReferences(referenceLocation, addRef, state); + else if (referenceLocation === propertyName) { + // For `export { foo as bar } from "baz"`, "`foo`" will be added from the singleReferences for import searches of the original export. + // For `export { foo as bar };`, where `foo` is a local, so add it now. + if (!exportDeclaration.moduleSpecifier) { + addRef(); } - else { - addRef(referenceLocation, kind); + if (addReferencesHere && state.options.use !== FindReferencesUse.Rename && state.markSeenReExportRHS(name)) { + addReference(name, Debug.checkDefined(exportSpecifier.symbol), state); } } - - /** Adds references when a constructor is used with `new this()` in its own class and `super()` calls in subclasses. */ - function addConstructorReferences(referenceLocation: Node, sourceFile: SourceFile, search: Search, state: State): void { - if (isNewExpressionTarget(referenceLocation)) { - addReference(referenceLocation, search.symbol, state); + else { + if (state.markSeenReExportRHS(referenceLocation)) { + addRef(); } - - const pusher = () => state.referenceAdder(search.symbol); - - if (isClassLike(referenceLocation.parent)) { - Debug.assert(referenceLocation.kind === SyntaxKind.DefaultKeyword || referenceLocation.parent.name === referenceLocation); - // This is the class declaration containing the constructor. - findOwnConstructorReferences(search.symbol, sourceFile, pusher()); + } + // For `export { foo as bar }`, rename `foo`, but not `bar`. + if (!isForRenameWithPrefixAndSuffixText(state.options) || alwaysGetReferences) { + const isDefaultExport = referenceLocation.originalKeywordKind === SyntaxKind.DefaultKeyword + || exportSpecifier.name.originalKeywordKind === SyntaxKind.DefaultKeyword; + const exportKind = isDefaultExport ? ExportKind.Default : ExportKind.Named; + const exportSymbol = Debug.checkDefined(exportSpecifier.symbol); + const exportInfo = getExportInfo(exportSymbol, exportKind, state.checker); + if (exportInfo) { + searchForImportsOfExport(referenceLocation, exportSymbol, exportInfo, state); } - else { - // If this class appears in `extends C`, then the extending class' "super" calls are references. - const classExtending = tryGetClassByExtendingIdentifier(referenceLocation); - if (classExtending) { - findSuperConstructorAccesses(classExtending, pusher()); - findInheritedConstructorReferences(classExtending, state); - } + } + // At `export { x } from "foo"`, also search for the imported symbol `"foo".x`. + if (search.comingFrom !== ImportExport.Export && exportDeclaration.moduleSpecifier && !propertyName && !isForRenameWithPrefixAndSuffixText(state.options)) { + const imported = state.checker.getExportSpecifierLocalTargetSymbol(exportSpecifier); + if (imported) + searchForImportedSymbol(imported, state); + } + function addRef() { + if (addReferencesHere) + addReference(referenceLocation, localSymbol, state); + } + } + function getLocalSymbolForExportSpecifier(referenceLocation: Identifier, referenceSymbol: Symbol, exportSpecifier: ExportSpecifier, checker: TypeChecker): Symbol { + return isExportSpecifierAlias(referenceLocation, exportSpecifier) && checker.getExportSpecifierLocalTargetSymbol(exportSpecifier) || referenceSymbol; + } + function isExportSpecifierAlias(referenceLocation: Identifier, exportSpecifier: ExportSpecifier): boolean { + const { parent, propertyName, name } = exportSpecifier; + Debug.assert(propertyName === referenceLocation || name === referenceLocation); + if (propertyName) { + // Given `export { foo as bar } [from "someModule"]`: It's an alias at `foo`, but at `bar` it's a new symbol. + return propertyName === referenceLocation; + } + else { + // `export { foo } from "foo"` is a re-export. + // `export { foo };` is not a re-export, it creates an alias for the local variable `foo`. + return !parent.parent.moduleSpecifier; + } + } + function getImportOrExportReferences(referenceLocation: Node, referenceSymbol: Symbol, search: Search, state: State): void { + const importOrExport = getImportOrExportSymbol(referenceLocation, referenceSymbol, state.checker, search.comingFrom === ImportExport.Export); + if (!importOrExport) + return; + const { symbol } = importOrExport; + if (importOrExport.kind === ImportExport.Import) { + if (!(isForRenameWithPrefixAndSuffixText(state.options))) { + searchForImportedSymbol(symbol, state); } } - - function addClassStaticThisReferences(referenceLocation: Node, search: Search, state: State): void { + else { + searchForImportsOfExport(referenceLocation, symbol, importOrExport.exportInfo, state); + } + } + function getReferenceForShorthandProperty({ flags, valueDeclaration }: Symbol, search: Search, state: State): void { + const shorthandValueSymbol = state.checker.getShorthandAssignmentValueSymbol(valueDeclaration)!; + const name = valueDeclaration && getNameOfDeclaration(valueDeclaration); + /* + * Because in short-hand property assignment, an identifier which stored as name of the short-hand property assignment + * has two meanings: property name and property value. Therefore when we do findAllReference at the position where + * an identifier is declared, the language service should return the position of the variable declaration as well as + * the position in short-hand property assignment excluding property accessing. However, if we do findAllReference at the + * position of property accessing, the referenceEntry of such position will be handled in the first case. + */ + if (!(flags & SymbolFlags.Transient) && name && search.includes(shorthandValueSymbol)) { + addReference(name, shorthandValueSymbol, state); + } + } + function addReference(referenceLocation: Node, relatedSymbol: Symbol | RelatedSymbol, state: State): void { + const { kind, symbol } = "kind" in relatedSymbol ? relatedSymbol : { kind: undefined, symbol: relatedSymbol }; // eslint-disable-line no-in-operator + const addRef = state.referenceAdder(symbol); + if (state.options.implementations) { + addImplementationReferences(referenceLocation, addRef, state); + } + else { + addRef(referenceLocation, kind); + } + } + /** Adds references when a constructor is used with `new this()` in its own class and `super()` calls in subclasses. */ + function addConstructorReferences(referenceLocation: Node, sourceFile: SourceFile, search: Search, state: State): void { + if (isNewExpressionTarget(referenceLocation)) { addReference(referenceLocation, search.symbol, state); - const classLike = referenceLocation.parent; - if (state.options.use === FindReferencesUse.Rename || !isClassLike(classLike)) return; - Debug.assert(classLike.name === referenceLocation); - const addRef = state.referenceAdder(search.symbol); - for (const member of classLike.members) { - if (!(isMethodOrAccessor(member) && hasModifier(member, ModifierFlags.Static))) { - continue; - } - if (member.body) { - member.body.forEachChild(function cb(node) { - if (node.kind === SyntaxKind.ThisKeyword) { - addRef(node); - } - else if (!isFunctionLike(node) && !isClassLike(node)) { - node.forEachChild(cb); - } - }); - } - } } - - /** - * `classSymbol` is the class where the constructor was defined. - * Reference the constructor and all calls to `new this()`. - */ - function findOwnConstructorReferences(classSymbol: Symbol, sourceFile: SourceFile, addNode: (node: Node) => void): void { - const constructorSymbol = getClassConstructorSymbol(classSymbol); - if (constructorSymbol && constructorSymbol.declarations) { - for (const decl of constructorSymbol.declarations) { - const ctrKeyword = findChildOfKind(decl, SyntaxKind.ConstructorKeyword, sourceFile)!; - Debug.assert(decl.kind === SyntaxKind.Constructor && !!ctrKeyword); - addNode(ctrKeyword); - } + const pusher = () => state.referenceAdder(search.symbol); + if (isClassLike(referenceLocation.parent)) { + Debug.assert(referenceLocation.kind === SyntaxKind.DefaultKeyword || referenceLocation.parent.name === referenceLocation); + // This is the class declaration containing the constructor. + findOwnConstructorReferences(search.symbol, sourceFile, pusher()); + } + else { + // If this class appears in `extends C`, then the extending class' "super" calls are references. + const classExtending = tryGetClassByExtendingIdentifier(referenceLocation); + if (classExtending) { + findSuperConstructorAccesses(classExtending, pusher()); + findInheritedConstructorReferences(classExtending, state); } - - if (classSymbol.exports) { - classSymbol.exports.forEach(member => { - const decl = member.valueDeclaration; - if (decl && decl.kind === SyntaxKind.MethodDeclaration) { - const body = (decl).body; - if (body) { - forEachDescendantOfKind(body, SyntaxKind.ThisKeyword, thisKeyword => { - if (isNewExpressionTarget(thisKeyword)) { - addNode(thisKeyword); - } - }); - } + } + } + function addClassStaticThisReferences(referenceLocation: Node, search: Search, state: State): void { + addReference(referenceLocation, search.symbol, state); + const classLike = referenceLocation.parent; + if (state.options.use === FindReferencesUse.Rename || !isClassLike(classLike)) + return; + Debug.assert(classLike.name === referenceLocation); + const addRef = state.referenceAdder(search.symbol); + for (const member of classLike.members) { + if (!(isMethodOrAccessor(member) && hasModifier(member, ModifierFlags.Static))) { + continue; + } + if (member.body) { + member.body.forEachChild(function cb(node) { + if (node.kind === SyntaxKind.ThisKeyword) { + addRef(node); + } + else if (!isFunctionLike(node) && !isClassLike(node)) { + node.forEachChild(cb); } }); } } - - function getClassConstructorSymbol(classSymbol: Symbol): Symbol | undefined { - return classSymbol.members && classSymbol.members.get(InternalSymbolName.Constructor); - } - - /** Find references to `super` in the constructor of an extending class. */ - function findSuperConstructorAccesses(classDeclaration: ClassLikeDeclaration, addNode: (node: Node) => void): void { - const constructor = getClassConstructorSymbol(classDeclaration.symbol); - if (!(constructor && constructor.declarations)) { - return; - } - - for (const decl of constructor.declarations) { - Debug.assert(decl.kind === SyntaxKind.Constructor); - const body = (decl).body; - if (body) { - forEachDescendantOfKind(body, SyntaxKind.SuperKeyword, node => { - if (isCallExpressionTarget(node)) { - addNode(node); - } - }); - } - } - } - - function hasOwnConstructor(classDeclaration: ClassLikeDeclaration): boolean { - return !!getClassConstructorSymbol(classDeclaration.symbol); - } - - function findInheritedConstructorReferences(classDeclaration: ClassLikeDeclaration, state: State): void { - if (hasOwnConstructor(classDeclaration)) return; - const classSymbol = classDeclaration.symbol; - const search = state.createSearch(/*location*/ undefined, classSymbol, /*comingFrom*/ undefined); - getReferencesInContainerOrFiles(classSymbol, state, search); - } - - function addImplementationReferences(refNode: Node, addReference: (node: Node) => void, state: State): void { - // Check if we found a function/propertyAssignment/method with an implementation or initializer - if (isDeclarationName(refNode) && isImplementation(refNode.parent)) { - addReference(refNode); - return; - } - - if (refNode.kind !== SyntaxKind.Identifier) { - return; - } - - if (refNode.parent.kind === SyntaxKind.ShorthandPropertyAssignment) { - // Go ahead and dereference the shorthand assignment by going to its definition - getReferenceEntriesForShorthandPropertyAssignment(refNode, state.checker, addReference); - } - - // Check if the node is within an extends or implements clause - const containingClass = getContainingClassIfInHeritageClause(refNode); - if (containingClass) { - addReference(containingClass); - return; - } - - // If we got a type reference, try and see if the reference applies to any expressions that can implement an interface - // Find the first node whose parent isn't a type node -- i.e., the highest type node. - const typeNode = findAncestor(refNode, a => !isQualifiedName(a.parent) && !isTypeNode(a.parent) && !isTypeElement(a.parent))!; - const typeHavingNode = typeNode.parent; - if (hasType(typeHavingNode) && typeHavingNode.type === typeNode && state.markSeenContainingTypeReference(typeHavingNode)) { - if (hasInitializer(typeHavingNode)) { - addIfImplementation(typeHavingNode.initializer!); - } - else if (isFunctionLike(typeHavingNode) && (typeHavingNode as FunctionLikeDeclaration).body) { - const body = (typeHavingNode as FunctionLikeDeclaration).body!; - if (body.kind === SyntaxKind.Block) { - forEachReturnStatement(body, returnStatement => { - if (returnStatement.expression) addIfImplementation(returnStatement.expression); + } + /** + * `classSymbol` is the class where the constructor was defined. + * Reference the constructor and all calls to `new this()`. + */ + function findOwnConstructorReferences(classSymbol: Symbol, sourceFile: SourceFile, addNode: (node: Node) => void): void { + const constructorSymbol = getClassConstructorSymbol(classSymbol); + if (constructorSymbol && constructorSymbol.declarations) { + for (const decl of constructorSymbol.declarations) { + const ctrKeyword = (findChildOfKind(decl, SyntaxKind.ConstructorKeyword, sourceFile)!); + Debug.assert(decl.kind === SyntaxKind.Constructor && !!ctrKeyword); + addNode(ctrKeyword); + } + } + if (classSymbol.exports) { + classSymbol.exports.forEach(member => { + const decl = member.valueDeclaration; + if (decl && decl.kind === SyntaxKind.MethodDeclaration) { + const body = (decl).body; + if (body) { + forEachDescendantOfKind(body, SyntaxKind.ThisKeyword, thisKeyword => { + if (isNewExpressionTarget(thisKeyword)) { + addNode(thisKeyword); + } }); } - else { - addIfImplementation(body); + } + }); + } + } + function getClassConstructorSymbol(classSymbol: Symbol): Symbol | undefined { + return classSymbol.members && classSymbol.members.get(InternalSymbolName.Constructor); + } + /** Find references to `super` in the constructor of an extending class. */ + function findSuperConstructorAccesses(classDeclaration: ClassLikeDeclaration, addNode: (node: Node) => void): void { + const constructor = getClassConstructorSymbol(classDeclaration.symbol); + if (!(constructor && constructor.declarations)) { + return; + } + for (const decl of constructor.declarations) { + Debug.assert(decl.kind === SyntaxKind.Constructor); + const body = (decl).body; + if (body) { + forEachDescendantOfKind(body, SyntaxKind.SuperKeyword, node => { + if (isCallExpressionTarget(node)) { + addNode(node); } + }); + } + } + } + function hasOwnConstructor(classDeclaration: ClassLikeDeclaration): boolean { + return !!getClassConstructorSymbol(classDeclaration.symbol); + } + function findInheritedConstructorReferences(classDeclaration: ClassLikeDeclaration, state: State): void { + if (hasOwnConstructor(classDeclaration)) + return; + const classSymbol = classDeclaration.symbol; + const search = state.createSearch(/*location*/ undefined, classSymbol, /*comingFrom*/ undefined); + getReferencesInContainerOrFiles(classSymbol, state, search); + } + function addImplementationReferences(refNode: Node, addReference: (node: Node) => void, state: State): void { + // Check if we found a function/propertyAssignment/method with an implementation or initializer + if (isDeclarationName(refNode) && isImplementation(refNode.parent)) { + addReference(refNode); + return; + } + if (refNode.kind !== SyntaxKind.Identifier) { + return; + } + if (refNode.parent.kind === SyntaxKind.ShorthandPropertyAssignment) { + // Go ahead and dereference the shorthand assignment by going to its definition + getReferenceEntriesForShorthandPropertyAssignment(refNode, state.checker, addReference); + } + // Check if the node is within an extends or implements clause + const containingClass = getContainingClassIfInHeritageClause(refNode); + if (containingClass) { + addReference(containingClass); + return; + } + // If we got a type reference, try and see if the reference applies to any expressions that can implement an interface + // Find the first node whose parent isn't a type node -- i.e., the highest type node. + const typeNode = (findAncestor(refNode, a => !isQualifiedName(a.parent) && !isTypeNode(a.parent) && !isTypeElement(a.parent))!); + const typeHavingNode = typeNode.parent; + if (hasType(typeHavingNode) && typeHavingNode.type === typeNode && state.markSeenContainingTypeReference(typeHavingNode)) { + if (hasInitializer(typeHavingNode)) { + addIfImplementation(typeHavingNode.initializer!); + } + else if (isFunctionLike(typeHavingNode) && (typeHavingNode as FunctionLikeDeclaration).body) { + const body = ((typeHavingNode as FunctionLikeDeclaration).body!); + if (body.kind === SyntaxKind.Block) { + forEachReturnStatement((body), returnStatement => { + if (returnStatement.expression) + addIfImplementation(returnStatement.expression); + }); } - else if (isAssertionExpression(typeHavingNode)) { - addIfImplementation(typeHavingNode.expression); + else { + addIfImplementation(body); } } - - function addIfImplementation(e: Expression): void { - if (isImplementationExpression(e)) addReference(e); + else if (isAssertionExpression(typeHavingNode)) { + addIfImplementation(typeHavingNode.expression); } } - - function getContainingClassIfInHeritageClause(node: Node): ClassLikeDeclaration | InterfaceDeclaration | undefined { - return isIdentifier(node) || isPropertyAccessExpression(node) ? getContainingClassIfInHeritageClause(node.parent) - : isExpressionWithTypeArguments(node) ? tryCast(node.parent.parent, isClassLike) : undefined; - } - - /** - * Returns true if this is an expression that can be considered an implementation - */ - function isImplementationExpression(node: Expression): boolean { - switch (node.kind) { - case SyntaxKind.ParenthesizedExpression: - return isImplementationExpression((node).expression); - case SyntaxKind.ArrowFunction: - case SyntaxKind.FunctionExpression: - case SyntaxKind.ObjectLiteralExpression: - case SyntaxKind.ClassExpression: - case SyntaxKind.ArrayLiteralExpression: - return true; - default: - return false; - } + function addIfImplementation(e: Expression): void { + if (isImplementationExpression(e)) + addReference(e); } - - /** - * Determines if the parent symbol occurs somewhere in the child's ancestry. If the parent symbol - * is an interface, determines if some ancestor of the child symbol extends or inherits from it. - * Also takes in a cache of previous results which makes this slightly more efficient and is - * necessary to avoid potential loops like so: - * class A extends B { } - * class B extends A { } - * - * We traverse the AST rather than using the type checker because users are typically only interested - * in explicit implementations of an interface/class when calling "Go to Implementation". Sibling - * implementations of types that share a common ancestor with the type whose implementation we are - * searching for need to be filtered out of the results. The type checker doesn't let us make the - * distinction between structurally compatible implementations and explicit implementations, so we - * must use the AST. - * - * @param symbol A class or interface Symbol - * @param parent Another class or interface Symbol - * @param cachedResults A map of symbol id pairs (i.e. "child,parent") to booleans indicating previous results - */ - function explicitlyInheritsFrom(symbol: Symbol, parent: Symbol, cachedResults: Map, checker: TypeChecker): boolean { - if (symbol === parent) { + } + function getContainingClassIfInHeritageClause(node: Node): ClassLikeDeclaration | InterfaceDeclaration | undefined { + return isIdentifier(node) || isPropertyAccessExpression(node) ? getContainingClassIfInHeritageClause(node.parent) + : isExpressionWithTypeArguments(node) ? tryCast(node.parent.parent, isClassLike) : undefined; + } + /** + * Returns true if this is an expression that can be considered an implementation + */ + function isImplementationExpression(node: Expression): boolean { + switch (node.kind) { + case SyntaxKind.ParenthesizedExpression: + return isImplementationExpression((node).expression); + case SyntaxKind.ArrowFunction: + case SyntaxKind.FunctionExpression: + case SyntaxKind.ObjectLiteralExpression: + case SyntaxKind.ClassExpression: + case SyntaxKind.ArrayLiteralExpression: return true; - } - - const key = getSymbolId(symbol) + "," + getSymbolId(parent); - const cached = cachedResults.get(key); - if (cached !== undefined) { - return cached; - } - - // Set the key so that we don't infinitely recurse - cachedResults.set(key, false); - - const inherits = !!symbol.declarations && symbol.declarations.some(declaration => - getAllSuperTypeNodes(declaration).some(typeReference => { - const type = checker.getTypeAtLocation(typeReference); - return !!type && !!type.symbol && explicitlyInheritsFrom(type.symbol, parent, cachedResults, checker); - })); - cachedResults.set(key, inherits); - return inherits; - } - - function getReferencesForSuperKeyword(superKeyword: Node): SymbolAndEntries[] | undefined { - let searchSpaceNode = getSuperContainer(superKeyword, /*stopOnFunctions*/ false); - if (!searchSpaceNode) { + default: + return false; + } + } + /** + * Determines if the parent symbol occurs somewhere in the child's ancestry. If the parent symbol + * is an interface, determines if some ancestor of the child symbol extends or inherits from it. + * Also takes in a cache of previous results which makes this slightly more efficient and is + * necessary to avoid potential loops like so: + * class A extends B { } + * class B extends A { } + * + * We traverse the AST rather than using the type checker because users are typically only interested + * in explicit implementations of an interface/class when calling "Go to Implementation". Sibling + * implementations of types that share a common ancestor with the type whose implementation we are + * searching for need to be filtered out of the results. The type checker doesn't let us make the + * distinction between structurally compatible implementations and explicit implementations, so we + * must use the AST. + * + * @param symbol A class or interface Symbol + * @param parent Another class or interface Symbol + * @param cachedResults A map of symbol id pairs (i.e. "child,parent") to booleans indicating previous results + */ + function explicitlyInheritsFrom(symbol: Symbol, parent: Symbol, cachedResults: ts.Map, checker: TypeChecker): boolean { + if (symbol === parent) { + return true; + } + const key = getSymbolId(symbol) + "," + getSymbolId(parent); + const cached = cachedResults.get(key); + if (cached !== undefined) { + return cached; + } + // Set the key so that we don't infinitely recurse + cachedResults.set(key, false); + const inherits = !!symbol.declarations && symbol.declarations.some(declaration => getAllSuperTypeNodes(declaration).some(typeReference => { + const type = checker.getTypeAtLocation(typeReference); + return !!type && !!type.symbol && explicitlyInheritsFrom(type.symbol, parent, cachedResults, checker); + })); + cachedResults.set(key, inherits); + return inherits; + } + function getReferencesForSuperKeyword(superKeyword: Node): SymbolAndEntries[] | undefined { + let searchSpaceNode = getSuperContainer(superKeyword, /*stopOnFunctions*/ false); + if (!searchSpaceNode) { + return undefined; + } + // Whether 'super' occurs in a static context within a class. + let staticFlag = ModifierFlags.Static; + switch (searchSpaceNode.kind) { + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.PropertySignature: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: + case SyntaxKind.Constructor: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + staticFlag &= getModifierFlags(searchSpaceNode); + searchSpaceNode = searchSpaceNode.parent; // re-assign to be the owning class + break; + default: return undefined; + } + const sourceFile = searchSpaceNode.getSourceFile(); + const references = mapDefined(getPossibleSymbolReferenceNodes(sourceFile, "super", searchSpaceNode), node => { + if (node.kind !== SyntaxKind.SuperKeyword) { + return; } - // Whether 'super' occurs in a static context within a class. - let staticFlag = ModifierFlags.Static; - - switch (searchSpaceNode.kind) { - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.PropertySignature: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.MethodSignature: - case SyntaxKind.Constructor: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - staticFlag &= getModifierFlags(searchSpaceNode); - searchSpaceNode = searchSpaceNode.parent; // re-assign to be the owning class + const container = getSuperContainer(node, /*stopOnFunctions*/ false); + // If we have a 'super' container, we must have an enclosing class. + // Now make sure the owning class is the same as the search-space + // and has the same static qualifier as the original 'super's owner. + return container && (ModifierFlags.Static & getModifierFlags(container)) === staticFlag && container.parent.symbol === searchSpaceNode.symbol ? nodeEntry(node) : undefined; + }); + return [{ definition: { type: DefinitionKind.Symbol, symbol: searchSpaceNode.symbol }, references }]; + } + function isParameterName(node: Node) { + return node.kind === SyntaxKind.Identifier && node.parent.kind === SyntaxKind.Parameter && (node.parent).name === node; + } + function getReferencesForThisKeyword(thisOrSuperKeyword: Node, sourceFiles: readonly SourceFile[], cancellationToken: CancellationToken): SymbolAndEntries[] | undefined { + let searchSpaceNode = getThisContainer(thisOrSuperKeyword, /* includeArrowFunctions */ false); + // Whether 'this' occurs in a static context within a class. + let staticFlag = ModifierFlags.Static; + switch (searchSpaceNode.kind) { + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: + if (isObjectLiteralMethod(searchSpaceNode)) { break; - default: + } + // falls through + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.PropertySignature: + case SyntaxKind.Constructor: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + staticFlag &= getModifierFlags(searchSpaceNode); + searchSpaceNode = searchSpaceNode.parent; // re-assign to be the owning class + break; + case SyntaxKind.SourceFile: + if (isExternalModule((searchSpaceNode)) || isParameterName(thisOrSuperKeyword)) { return undefined; - } - - const sourceFile = searchSpaceNode.getSourceFile(); - const references = mapDefined(getPossibleSymbolReferenceNodes(sourceFile, "super", searchSpaceNode), node => { - if (node.kind !== SyntaxKind.SuperKeyword) { - return; } - - const container = getSuperContainer(node, /*stopOnFunctions*/ false); - - // If we have a 'super' container, we must have an enclosing class. - // Now make sure the owning class is the same as the search-space - // and has the same static qualifier as the original 'super's owner. - return container && (ModifierFlags.Static & getModifierFlags(container)) === staticFlag && container.parent.symbol === searchSpaceNode.symbol ? nodeEntry(node) : undefined; + // falls through + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.FunctionExpression: + break; + // Computed properties in classes are not handled here because references to this are illegal, + // so there is no point finding references to them. + default: + return undefined; + } + const references = flatMap(searchSpaceNode.kind === SyntaxKind.SourceFile ? sourceFiles : [searchSpaceNode.getSourceFile()], sourceFile => { + cancellationToken.throwIfCancellationRequested(); + return getPossibleSymbolReferenceNodes(sourceFile, "this", isSourceFile(searchSpaceNode) ? sourceFile : searchSpaceNode).filter(node => { + if (!isThis(node)) { + return false; + } + const container = getThisContainer(node, /* includeArrowFunctions */ false); + switch (searchSpaceNode.kind) { + case SyntaxKind.FunctionExpression: + case SyntaxKind.FunctionDeclaration: + return searchSpaceNode.symbol === container.symbol; + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: + return isObjectLiteralMethod(searchSpaceNode) && searchSpaceNode.symbol === container.symbol; + case SyntaxKind.ClassExpression: + case SyntaxKind.ClassDeclaration: + // Make sure the container belongs to the same class + // and has the appropriate static modifier from the original container. + return container.parent && searchSpaceNode.symbol === container.parent.symbol && (getModifierFlags(container) & ModifierFlags.Static) === staticFlag; + case SyntaxKind.SourceFile: + return container.kind === SyntaxKind.SourceFile && !isExternalModule((container)) && !isParameterName(node); + } }); - - return [{ definition: { type: DefinitionKind.Symbol, symbol: searchSpaceNode.symbol }, references }]; - } - - function isParameterName(node: Node) { - return node.kind === SyntaxKind.Identifier && node.parent.kind === SyntaxKind.Parameter && (node.parent).name === node; - } - - function getReferencesForThisKeyword(thisOrSuperKeyword: Node, sourceFiles: readonly SourceFile[], cancellationToken: CancellationToken): SymbolAndEntries[] | undefined { - let searchSpaceNode = getThisContainer(thisOrSuperKeyword, /* includeArrowFunctions */ false); - - // Whether 'this' occurs in a static context within a class. - let staticFlag = ModifierFlags.Static; - - switch (searchSpaceNode.kind) { - case SyntaxKind.MethodDeclaration: - case SyntaxKind.MethodSignature: - if (isObjectLiteralMethod(searchSpaceNode)) { - break; - } - // falls through - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.PropertySignature: - case SyntaxKind.Constructor: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - staticFlag &= getModifierFlags(searchSpaceNode); - searchSpaceNode = searchSpaceNode.parent; // re-assign to be the owning class - break; - case SyntaxKind.SourceFile: - if (isExternalModule(searchSpaceNode) || isParameterName(thisOrSuperKeyword)) { - return undefined; - } - // falls through - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.FunctionExpression: - break; - // Computed properties in classes are not handled here because references to this are illegal, - // so there is no point finding references to them. - default: - return undefined; - } - - const references = flatMap(searchSpaceNode.kind === SyntaxKind.SourceFile ? sourceFiles : [searchSpaceNode.getSourceFile()], sourceFile => { - cancellationToken.throwIfCancellationRequested(); - return getPossibleSymbolReferenceNodes(sourceFile, "this", isSourceFile(searchSpaceNode) ? sourceFile : searchSpaceNode).filter(node => { - if (!isThis(node)) { - return false; - } - const container = getThisContainer(node, /* includeArrowFunctions */ false); - switch (searchSpaceNode.kind) { - case SyntaxKind.FunctionExpression: - case SyntaxKind.FunctionDeclaration: - return searchSpaceNode.symbol === container.symbol; - case SyntaxKind.MethodDeclaration: - case SyntaxKind.MethodSignature: - return isObjectLiteralMethod(searchSpaceNode) && searchSpaceNode.symbol === container.symbol; - case SyntaxKind.ClassExpression: - case SyntaxKind.ClassDeclaration: - // Make sure the container belongs to the same class - // and has the appropriate static modifier from the original container. - return container.parent && searchSpaceNode.symbol === container.parent.symbol && (getModifierFlags(container) & ModifierFlags.Static) === staticFlag; - case SyntaxKind.SourceFile: - return container.kind === SyntaxKind.SourceFile && !isExternalModule(container) && !isParameterName(node); - } - }); - }).map(n => nodeEntry(n)); - - const thisParameter = firstDefined(references, r => isParameter(r.node.parent) ? r.node : undefined); - return [{ + }).map(n => nodeEntry(n)); + const thisParameter = firstDefined(references, r => isParameter(r.node.parent) ? r.node : undefined); + return [{ definition: { type: DefinitionKind.This, node: thisParameter || thisOrSuperKeyword }, references }]; - } - - function getReferencesForStringLiteral(node: StringLiteral, sourceFiles: readonly SourceFile[], cancellationToken: CancellationToken): SymbolAndEntries[] { - const references = flatMap(sourceFiles, sourceFile => { - cancellationToken.throwIfCancellationRequested(); - return mapDefined(getPossibleSymbolReferenceNodes(sourceFile, node.text), ref => - isStringLiteral(ref) && ref.text === node.text ? nodeEntry(ref, EntryKind.StringLiteral) : undefined); - }); - - return [{ + } + function getReferencesForStringLiteral(node: StringLiteral, sourceFiles: readonly SourceFile[], cancellationToken: CancellationToken): SymbolAndEntries[] { + const references = flatMap(sourceFiles, sourceFile => { + cancellationToken.throwIfCancellationRequested(); + return mapDefined(getPossibleSymbolReferenceNodes(sourceFile, node.text), ref => isStringLiteral(ref) && ref.text === node.text ? nodeEntry(ref, EntryKind.StringLiteral) : undefined); + }); + return [{ definition: { type: DefinitionKind.String, node }, references }]; - } - - // For certain symbol kinds, we need to include other symbols in the search set. - // This is not needed when searching for re-exports. - function populateSearchSymbolSet(symbol: Symbol, location: Node, checker: TypeChecker, isForRename: boolean, providePrefixAndSuffixText: boolean, implementations: boolean): Symbol[] { - const result: Symbol[] = []; - forEachRelatedSymbol(symbol, location, checker, isForRename, !(isForRename && providePrefixAndSuffixText), - (sym, root, base) => { result.push(base || root || sym); }, - /*allowBaseTypes*/ () => !implementations); - return result; - } - - function forEachRelatedSymbol( - symbol: Symbol, location: Node, checker: TypeChecker, isForRenamePopulateSearchSymbolSet: boolean, onlyIncludeBindingElementAtReferenceLocation: boolean, - cbSymbol: (symbol: Symbol, rootSymbol?: Symbol, baseSymbol?: Symbol, kind?: NodeEntryKind) => T | undefined, - allowBaseTypes: (rootSymbol: Symbol) => boolean, - ): T | undefined { - const containingObjectLiteralElement = getContainingObjectLiteralElement(location); - if (containingObjectLiteralElement) { - /* Because in short-hand property assignment, location has two meaning : property name and as value of the property - * When we do findAllReference at the position of the short-hand property assignment, we would want to have references to position of - * property name and variable declaration of the identifier. - * Like in below example, when querying for all references for an identifier 'name', of the property assignment, the language service - * should show both 'name' in 'obj' and 'name' in variable declaration - * const name = "Foo"; - * const obj = { name }; - * In order to do that, we will populate the search set with the value symbol of the identifier as a value of the property assignment - * so that when matching with potential reference symbol, both symbols from property declaration and variable declaration - * will be included correctly. - */ - const shorthandValueSymbol = checker.getShorthandAssignmentValueSymbol(location.parent); // gets the local symbol - if (shorthandValueSymbol && isForRenamePopulateSearchSymbolSet) { - // When renaming 'x' in `const o = { x }`, just rename the local variable, not the property. - return cbSymbol(shorthandValueSymbol, /*rootSymbol*/ undefined, /*baseSymbol*/ undefined, EntryKind.SearchedLocalFoundProperty); - } - - // If the location is in a context sensitive location (i.e. in an object literal) try - // to get a contextual type for it, and add the property symbol from the contextual - // type to the search set - const contextualType = checker.getContextualType(containingObjectLiteralElement.parent); - const res = contextualType && firstDefined( - getPropertySymbolsFromContextualType(containingObjectLiteralElement, checker, contextualType, /*unionSymbolOk*/ true), - sym => fromRoot(sym, EntryKind.SearchedPropertyFoundLocal)); - if (res) return res; - - // If the location is name of property symbol from object literal destructuring pattern - // Search the property symbol - // for ( { property: p2 } of elems) { } - const propertySymbol = getPropertySymbolOfDestructuringAssignment(location, checker); - const res1 = propertySymbol && cbSymbol(propertySymbol, /*rootSymbol*/ undefined, /*baseSymbol*/ undefined, EntryKind.SearchedPropertyFoundLocal); - if (res1) return res1; - - const res2 = shorthandValueSymbol && cbSymbol(shorthandValueSymbol, /*rootSymbol*/ undefined, /*baseSymbol*/ undefined, EntryKind.SearchedLocalFoundProperty); - if (res2) return res2; - } - - const aliasedSymbol = getMergedAliasedSymbolOfNamespaceExportDeclaration(location, symbol, checker); - if (aliasedSymbol) { - // In case of UMD module and global merging, search for global as well - const res = cbSymbol(aliasedSymbol, /*rootSymbol*/ undefined, /*baseSymbol*/ undefined, EntryKind.Node); - if (res) return res; - } - - const res = fromRoot(symbol); - if (res) return res; - - if (symbol.valueDeclaration && isParameterPropertyDeclaration(symbol.valueDeclaration, symbol.valueDeclaration.parent)) { - // For a parameter property, now try on the other symbol (property if this was a parameter, parameter if this was a property). - const paramProps = checker.getSymbolsOfParameterPropertyDeclaration(cast(symbol.valueDeclaration, isParameter), symbol.name); - Debug.assert(paramProps.length === 2 && !!(paramProps[0].flags & SymbolFlags.FunctionScopedVariable) && !!(paramProps[1].flags & SymbolFlags.Property)); // is [parameter, property] - return fromRoot(symbol.flags & SymbolFlags.FunctionScopedVariable ? paramProps[1] : paramProps[0]); - } - - const exportSpecifier = getDeclarationOfKind(symbol, SyntaxKind.ExportSpecifier); - if (!isForRenamePopulateSearchSymbolSet || exportSpecifier && !exportSpecifier.propertyName) { - const localSymbol = exportSpecifier && checker.getExportSpecifierLocalTargetSymbol(exportSpecifier); - if (localSymbol) { - const res = cbSymbol(localSymbol, /*rootSymbol*/ undefined, /*baseSymbol*/ undefined, EntryKind.Node); - if (res) return res; - } - } - - // symbolAtLocation for a binding element is the local symbol. See if the search symbol is the property. - // Don't do this when populating search set for a rename when prefix and suffix text will be provided -- just rename the local. - if (!isForRenamePopulateSearchSymbolSet) { - let bindingElementPropertySymbol: Symbol | undefined; - if (onlyIncludeBindingElementAtReferenceLocation) { - bindingElementPropertySymbol = isObjectBindingElementWithoutPropertyName(location.parent) ? getPropertySymbolFromBindingElement(checker, location.parent) : undefined; - } - else { - bindingElementPropertySymbol = getPropertySymbolOfObjectBindingPatternWithoutPropertyName(symbol, checker); - } - return bindingElementPropertySymbol && fromRoot(bindingElementPropertySymbol, EntryKind.SearchedPropertyFoundLocal); - } - - Debug.assert(isForRenamePopulateSearchSymbolSet); - // due to the above assert and the arguments at the uses of this function, - // (onlyIncludeBindingElementAtReferenceLocation <=> !providePrefixAndSuffixTextForRename) holds - const includeOriginalSymbolOfBindingElement = onlyIncludeBindingElementAtReferenceLocation; - - if (includeOriginalSymbolOfBindingElement) { - const bindingElementPropertySymbol = getPropertySymbolOfObjectBindingPatternWithoutPropertyName(symbol, checker); - return bindingElementPropertySymbol && fromRoot(bindingElementPropertySymbol, EntryKind.SearchedPropertyFoundLocal); - } - - function fromRoot(sym: Symbol, kind?: NodeEntryKind): T | undefined { - // If this is a union property: - // - In populateSearchSymbolsSet we will add all the symbols from all its source symbols in all unioned types. - // - In findRelatedSymbol, we will just use the union symbol if any source symbol is included in the search. - // If the symbol is an instantiation from a another symbol (e.g. widened symbol): - // - In populateSearchSymbolsSet, add the root the list - // - In findRelatedSymbol, return the source symbol if that is in the search. (Do not return the instantiation symbol.) - return firstDefined(checker.getRootSymbols(sym), rootSymbol => - cbSymbol(sym, rootSymbol, /*baseSymbol*/ undefined, kind) - // Add symbol of properties/methods of the same name in base classes and implemented interfaces definitions - || (rootSymbol.parent && rootSymbol.parent.flags & (SymbolFlags.Class | SymbolFlags.Interface) && allowBaseTypes(rootSymbol) - ? getPropertySymbolsFromBaseTypes(rootSymbol.parent, rootSymbol.name, checker, base => cbSymbol(sym, rootSymbol, base, kind)) - : undefined)); + } + // For certain symbol kinds, we need to include other symbols in the search set. + // This is not needed when searching for re-exports. + function populateSearchSymbolSet(symbol: Symbol, location: Node, checker: TypeChecker, isForRename: boolean, providePrefixAndSuffixText: boolean, implementations: boolean): Symbol[] { + const result: Symbol[] = []; + forEachRelatedSymbol(symbol, location, checker, isForRename, !(isForRename && providePrefixAndSuffixText), (sym, root, base) => { result.push(base || root || sym); }, + /*allowBaseTypes*/ () => !implementations); + return result; + } + function forEachRelatedSymbol(symbol: Symbol, location: Node, checker: TypeChecker, isForRenamePopulateSearchSymbolSet: boolean, onlyIncludeBindingElementAtReferenceLocation: boolean, cbSymbol: (symbol: Symbol, rootSymbol?: Symbol, baseSymbol?: Symbol, kind?: NodeEntryKind) => T | undefined, allowBaseTypes: (rootSymbol: Symbol) => boolean): T | undefined { + const containingObjectLiteralElement = getContainingObjectLiteralElement(location); + if (containingObjectLiteralElement) { + /* Because in short-hand property assignment, location has two meaning : property name and as value of the property + * When we do findAllReference at the position of the short-hand property assignment, we would want to have references to position of + * property name and variable declaration of the identifier. + * Like in below example, when querying for all references for an identifier 'name', of the property assignment, the language service + * should show both 'name' in 'obj' and 'name' in variable declaration + * const name = "Foo"; + * const obj = { name }; + * In order to do that, we will populate the search set with the value symbol of the identifier as a value of the property assignment + * so that when matching with potential reference symbol, both symbols from property declaration and variable declaration + * will be included correctly. + */ + const shorthandValueSymbol = checker.getShorthandAssignmentValueSymbol(location.parent); // gets the local symbol + if (shorthandValueSymbol && isForRenamePopulateSearchSymbolSet) { + // When renaming 'x' in `const o = { x }`, just rename the local variable, not the property. + return cbSymbol(shorthandValueSymbol, /*rootSymbol*/ undefined, /*baseSymbol*/ undefined, EntryKind.SearchedLocalFoundProperty); + } + // If the location is in a context sensitive location (i.e. in an object literal) try + // to get a contextual type for it, and add the property symbol from the contextual + // type to the search set + const contextualType = checker.getContextualType(containingObjectLiteralElement.parent); + const res = contextualType && firstDefined(getPropertySymbolsFromContextualType(containingObjectLiteralElement, checker, contextualType, /*unionSymbolOk*/ true), sym => fromRoot(sym, EntryKind.SearchedPropertyFoundLocal)); + if (res) + return res; + // If the location is name of property symbol from object literal destructuring pattern + // Search the property symbol + // for ( { property: p2 } of elems) { } + const propertySymbol = getPropertySymbolOfDestructuringAssignment(location, checker); + const res1 = propertySymbol && cbSymbol(propertySymbol, /*rootSymbol*/ undefined, /*baseSymbol*/ undefined, EntryKind.SearchedPropertyFoundLocal); + if (res1) + return res1; + const res2 = shorthandValueSymbol && cbSymbol(shorthandValueSymbol, /*rootSymbol*/ undefined, /*baseSymbol*/ undefined, EntryKind.SearchedLocalFoundProperty); + if (res2) + return res2; + } + const aliasedSymbol = getMergedAliasedSymbolOfNamespaceExportDeclaration(location, symbol, checker); + if (aliasedSymbol) { + // In case of UMD module and global merging, search for global as well + const res = cbSymbol(aliasedSymbol, /*rootSymbol*/ undefined, /*baseSymbol*/ undefined, EntryKind.Node); + if (res) + return res; + } + const res = fromRoot(symbol); + if (res) + return res; + if (symbol.valueDeclaration && isParameterPropertyDeclaration(symbol.valueDeclaration, symbol.valueDeclaration.parent)) { + // For a parameter property, now try on the other symbol (property if this was a parameter, parameter if this was a property). + const paramProps = checker.getSymbolsOfParameterPropertyDeclaration(cast(symbol.valueDeclaration, isParameter), symbol.name); + Debug.assert(paramProps.length === 2 && !!(paramProps[0].flags & SymbolFlags.FunctionScopedVariable) && !!(paramProps[1].flags & SymbolFlags.Property)); // is [parameter, property] + return fromRoot(symbol.flags & SymbolFlags.FunctionScopedVariable ? paramProps[1] : paramProps[0]); + } + const exportSpecifier = getDeclarationOfKind(symbol, SyntaxKind.ExportSpecifier); + if (!isForRenamePopulateSearchSymbolSet || exportSpecifier && !exportSpecifier.propertyName) { + const localSymbol = exportSpecifier && checker.getExportSpecifierLocalTargetSymbol(exportSpecifier); + if (localSymbol) { + const res = cbSymbol(localSymbol, /*rootSymbol*/ undefined, /*baseSymbol*/ undefined, EntryKind.Node); + if (res) + return res; + } + } + // symbolAtLocation for a binding element is the local symbol. See if the search symbol is the property. + // Don't do this when populating search set for a rename when prefix and suffix text will be provided -- just rename the local. + if (!isForRenamePopulateSearchSymbolSet) { + let bindingElementPropertySymbol: Symbol | undefined; + if (onlyIncludeBindingElementAtReferenceLocation) { + bindingElementPropertySymbol = isObjectBindingElementWithoutPropertyName(location.parent) ? getPropertySymbolFromBindingElement(checker, location.parent) : undefined; } - - function getPropertySymbolOfObjectBindingPatternWithoutPropertyName(symbol: Symbol, checker: TypeChecker): Symbol | undefined { - const bindingElement = getDeclarationOfKind(symbol, SyntaxKind.BindingElement); - if (bindingElement && isObjectBindingElementWithoutPropertyName(bindingElement)) { - return getPropertySymbolFromBindingElement(checker, bindingElement); - } + else { + bindingElementPropertySymbol = getPropertySymbolOfObjectBindingPatternWithoutPropertyName(symbol, checker); + } + return bindingElementPropertySymbol && fromRoot(bindingElementPropertySymbol, EntryKind.SearchedPropertyFoundLocal); + } + Debug.assert(isForRenamePopulateSearchSymbolSet); + // due to the above assert and the arguments at the uses of this function, + // (onlyIncludeBindingElementAtReferenceLocation <=> !providePrefixAndSuffixTextForRename) holds + const includeOriginalSymbolOfBindingElement = onlyIncludeBindingElementAtReferenceLocation; + if (includeOriginalSymbolOfBindingElement) { + const bindingElementPropertySymbol = getPropertySymbolOfObjectBindingPatternWithoutPropertyName(symbol, checker); + return bindingElementPropertySymbol && fromRoot(bindingElementPropertySymbol, EntryKind.SearchedPropertyFoundLocal); + } + function fromRoot(sym: Symbol, kind?: NodeEntryKind): T | undefined { + // If this is a union property: + // - In populateSearchSymbolsSet we will add all the symbols from all its source symbols in all unioned types. + // - In findRelatedSymbol, we will just use the union symbol if any source symbol is included in the search. + // If the symbol is an instantiation from a another symbol (e.g. widened symbol): + // - In populateSearchSymbolsSet, add the root the list + // - In findRelatedSymbol, return the source symbol if that is in the search. (Do not return the instantiation symbol.) + return firstDefined(checker.getRootSymbols(sym), rootSymbol => cbSymbol(sym, rootSymbol, /*baseSymbol*/ undefined, kind) + // Add symbol of properties/methods of the same name in base classes and implemented interfaces definitions + || (rootSymbol.parent && rootSymbol.parent.flags & (SymbolFlags.Class | SymbolFlags.Interface) && allowBaseTypes(rootSymbol) + ? getPropertySymbolsFromBaseTypes(rootSymbol.parent, rootSymbol.name, checker, base => cbSymbol(sym, rootSymbol, base, kind)) + : undefined)); + } + function getPropertySymbolOfObjectBindingPatternWithoutPropertyName(symbol: Symbol, checker: TypeChecker): Symbol | undefined { + const bindingElement = getDeclarationOfKind(symbol, SyntaxKind.BindingElement); + if (bindingElement && isObjectBindingElementWithoutPropertyName(bindingElement)) { + return getPropertySymbolFromBindingElement(checker, bindingElement); } } - - interface RelatedSymbol { - readonly symbol: Symbol; - readonly kind: NodeEntryKind | undefined; - } - function getRelatedSymbol(search: Search, referenceSymbol: Symbol, referenceLocation: Node, state: State): RelatedSymbol | undefined { - const { checker } = state; - return forEachRelatedSymbol(referenceSymbol, referenceLocation, checker, /*isForRenamePopulateSearchSymbolSet*/ false, - /*onlyIncludeBindingElementAtReferenceLocation*/ state.options.use !== FindReferencesUse.Rename || !!state.options.providePrefixAndSuffixTextForRename, - (sym, rootSymbol, baseSymbol, kind): RelatedSymbol | undefined => search.includes(baseSymbol || rootSymbol || sym) - // For a base type, use the symbol for the derived type. For a synthetic (e.g. union) property, use the union symbol. - ? { symbol: rootSymbol && !(getCheckFlags(sym) & CheckFlags.Synthetic) ? rootSymbol : sym, kind } - : undefined, - /*allowBaseTypes*/ rootSymbol => - !(search.parents && !search.parents.some(parent => explicitlyInheritsFrom(rootSymbol.parent!, parent, state.inheritsFromCache, checker)))); - } - - /** - * Given an initial searchMeaning, extracted from a location, widen the search scope based on the declarations - * of the corresponding symbol. e.g. if we are searching for "Foo" in value position, but "Foo" references a class - * then we need to widen the search to include type positions as well. - * On the contrary, if we are searching for "Bar" in type position and we trace bar to an interface, and an uninstantiated - * module, we want to keep the search limited to only types, as the two declarations (interface and uninstantiated module) - * do not intersect in any of the three spaces. - */ - export function getIntersectingMeaningFromDeclarations(node: Node, symbol: Symbol): SemanticMeaning { - let meaning = getMeaningFromLocation(node); - const { declarations } = symbol; - if (declarations) { - let lastIterationMeaning: SemanticMeaning; - do { - // The result is order-sensitive, for instance if initialMeaning === Namespace, and declarations = [class, instantiated module] - // we need to consider both as they initialMeaning intersects with the module in the namespace space, and the module - // intersects with the class in the value space. - // To achieve that we will keep iterating until the result stabilizes. - - // Remember the last meaning - lastIterationMeaning = meaning; - - for (const declaration of declarations) { - const declarationMeaning = getMeaningFromDeclaration(declaration); - - if (declarationMeaning & meaning) { - meaning |= declarationMeaning; - } + } + interface RelatedSymbol { + readonly symbol: Symbol; + readonly kind: NodeEntryKind | undefined; + } + function getRelatedSymbol(search: Search, referenceSymbol: Symbol, referenceLocation: Node, state: State): RelatedSymbol | undefined { + const { checker } = state; + return forEachRelatedSymbol(referenceSymbol, referenceLocation, checker, /*isForRenamePopulateSearchSymbolSet*/ false, + /*onlyIncludeBindingElementAtReferenceLocation*/ state.options.use !== FindReferencesUse.Rename || !!state.options.providePrefixAndSuffixTextForRename, (sym, rootSymbol, baseSymbol, kind): RelatedSymbol | undefined => search.includes(baseSymbol || rootSymbol || sym) + // For a base type, use the symbol for the derived type. For a synthetic (e.g. union) property, use the union symbol. + ? { symbol: rootSymbol && !(getCheckFlags(sym) & CheckFlags.Synthetic) ? rootSymbol : sym, kind } + : undefined, + /*allowBaseTypes*/ rootSymbol => !(search.parents && !search.parents.some(parent => explicitlyInheritsFrom(rootSymbol.parent!, parent, state.inheritsFromCache, checker)))); + } + /** + * Given an initial searchMeaning, extracted from a location, widen the search scope based on the declarations + * of the corresponding symbol. e.g. if we are searching for "Foo" in value position, but "Foo" references a class + * then we need to widen the search to include type positions as well. + * On the contrary, if we are searching for "Bar" in type position and we trace bar to an interface, and an uninstantiated + * module, we want to keep the search limited to only types, as the two declarations (interface and uninstantiated module) + * do not intersect in any of the three spaces. + */ + export function getIntersectingMeaningFromDeclarations(node: Node, symbol: Symbol): SemanticMeaning { + let meaning = getMeaningFromLocation(node); + const { declarations } = symbol; + if (declarations) { + let lastIterationMeaning: SemanticMeaning; + do { + // The result is order-sensitive, for instance if initialMeaning === Namespace, and declarations = [class, instantiated module] + // we need to consider both as they initialMeaning intersects with the module in the namespace space, and the module + // intersects with the class in the value space. + // To achieve that we will keep iterating until the result stabilizes. + // Remember the last meaning + lastIterationMeaning = meaning; + for (const declaration of declarations) { + const declarationMeaning = getMeaningFromDeclaration(declaration); + if (declarationMeaning & meaning) { + meaning |= declarationMeaning; } } - while (meaning !== lastIterationMeaning); - } - return meaning; + } while (meaning !== lastIterationMeaning); } - - function isImplementation(node: Node): boolean { - return !!(node.flags & NodeFlags.Ambient) ? !(isInterfaceDeclaration(node) || isTypeAliasDeclaration(node)) : - (isVariableLike(node) ? hasInitializer(node) : + return meaning; + } + function isImplementation(node: Node): boolean { + return !!(node.flags & NodeFlags.Ambient) ? !(isInterfaceDeclaration(node) || isTypeAliasDeclaration(node)) : + (isVariableLike(node) ? hasInitializer(node) : isFunctionLikeDeclaration(node) ? !!node.body : - isClassLike(node) || isModuleOrEnumDeclaration(node)); - } - - export function getReferenceEntriesForShorthandPropertyAssignment(node: Node, checker: TypeChecker, addReference: (node: Node) => void): void { - const refSymbol = checker.getSymbolAtLocation(node)!; - const shorthandSymbol = checker.getShorthandAssignmentValueSymbol(refSymbol.valueDeclaration); - - if (shorthandSymbol) { - for (const declaration of shorthandSymbol.getDeclarations()!) { - if (getMeaningFromDeclaration(declaration) & SemanticMeaning.Value) { - addReference(declaration); - } + isClassLike(node) || isModuleOrEnumDeclaration(node)); + } + export function getReferenceEntriesForShorthandPropertyAssignment(node: Node, checker: TypeChecker, addReference: (node: Node) => void): void { + const refSymbol = checker.getSymbolAtLocation(node)!; + const shorthandSymbol = checker.getShorthandAssignmentValueSymbol(refSymbol.valueDeclaration); + if (shorthandSymbol) { + for (const declaration of shorthandSymbol.getDeclarations()!) { + if (getMeaningFromDeclaration(declaration) & SemanticMeaning.Value) { + addReference(declaration); } } } - - function forEachDescendantOfKind(node: Node, kind: SyntaxKind, action: (node: Node) => void): void { - forEachChild(node, child => { - if (child.kind === kind) { - action(child); - } - forEachDescendantOfKind(child, kind, action); - }); - } - - /** Get `C` given `N` if `N` is in the position `class C extends N` or `class C extends foo.N` where `N` is an identifier. */ - function tryGetClassByExtendingIdentifier(node: Node): ClassLikeDeclaration | undefined { - return tryGetClassExtendingExpressionWithTypeArguments(climbPastPropertyAccess(node).parent); - } - - /** - * If we are just looking for implementations and this is a property access expression, we need to get the - * symbol of the local type of the symbol the property is being accessed on. This is because our search - * symbol may have a different parent symbol if the local type's symbol does not declare the property - * being accessed (i.e. it is declared in some parent class or interface) - */ - function getParentSymbolsOfPropertyAccess(location: Node, symbol: Symbol, checker: TypeChecker): readonly Symbol[] | undefined { - const propertyAccessExpression = isRightSideOfPropertyAccess(location) ? location.parent : undefined; - const lhsType = propertyAccessExpression && checker.getTypeAtLocation(propertyAccessExpression.expression); - const res = mapDefined(lhsType && (lhsType.isUnionOrIntersection() ? lhsType.types : lhsType.symbol === symbol.parent ? undefined : [lhsType]), t => - t.symbol && t.symbol.flags & (SymbolFlags.Class | SymbolFlags.Interface) ? t.symbol : undefined); - return res.length === 0 ? undefined : res; - } - - function isForRenameWithPrefixAndSuffixText(options: Options) { - return options.use === FindReferencesUse.Rename && options.providePrefixAndSuffixTextForRename; - } + } + function forEachDescendantOfKind(node: Node, kind: SyntaxKind, action: (node: Node) => void): void { + forEachChild(node, child => { + if (child.kind === kind) { + action(child); + } + forEachDescendantOfKind(child, kind, action); + }); + } + /** Get `C` given `N` if `N` is in the position `class C extends N` or `class C extends foo.N` where `N` is an identifier. */ + function tryGetClassByExtendingIdentifier(node: Node): ClassLikeDeclaration | undefined { + return tryGetClassExtendingExpressionWithTypeArguments(climbPastPropertyAccess(node).parent); + } + /** + * If we are just looking for implementations and this is a property access expression, we need to get the + * symbol of the local type of the symbol the property is being accessed on. This is because our search + * symbol may have a different parent symbol if the local type's symbol does not declare the property + * being accessed (i.e. it is declared in some parent class or interface) + */ + function getParentSymbolsOfPropertyAccess(location: Node, symbol: Symbol, checker: TypeChecker): readonly Symbol[] | undefined { + const propertyAccessExpression = isRightSideOfPropertyAccess(location) ? location.parent : undefined; + const lhsType = propertyAccessExpression && checker.getTypeAtLocation(propertyAccessExpression.expression); + const res = mapDefined(lhsType && (lhsType.isUnionOrIntersection() ? lhsType.types : lhsType.symbol === symbol.parent ? undefined : [lhsType]), t => t.symbol && t.symbol.flags & (SymbolFlags.Class | SymbolFlags.Interface) ? t.symbol : undefined); + return res.length === 0 ? undefined : res; + } + function isForRenameWithPrefixAndSuffixText(options: Options) { + return options.use === FindReferencesUse.Rename && options.providePrefixAndSuffixTextForRename; } } diff --git a/src/services/formatting/formatting.ts b/src/services/formatting/formatting.ts index bdd56acd960b0..6bc43a33c134b 100644 --- a/src/services/formatting/formatting.ts +++ b/src/services/formatting/formatting.ts @@ -1,1388 +1,1172 @@ +import { FormatCodeSettings, SyntaxKind, TextRange, TriviaKind, Debug, Node, SourceFile, TextChange, getEndLinePosition, isWhiteSpaceSingleLine, isLineBreak, getStartPositionOfLine, getLineStartPositionForPosition, findPrecedingToken, rangeContainsRange, InterfaceDeclaration, ModuleDeclaration, Block, CatchClause, forEachChild, startEndContainsRange, Diagnostic, rangeOverlapsWithStartEnd, startEndOverlapsWithStartEnd, SourceFileLike, LanguageVariant, getNonDecoratorTokenPosOfNode, rangeContainsStartEnd, MethodDeclaration, getNameOfDeclaration, Declaration, isToken, findIndex, NodeArray, isNodeArray, isCallLikeExpression, isComment, LineAndCharacter, forEachRight, CharacterCodes, isStringOrRegularExpressionOrTemplateLiteral, createTextChangeFromStartLength, getTokenAtPosition, CommentRange, findAncestor, isJSDoc, getTrailingCommentRanges, getLeadingCommentRangesOfNode, concatenate, find, rangeContainsPositionExclusive, FunctionDeclaration, CallExpression, TypeReferenceNode, EditorSettings, repeatString } from "../ts"; +import { RulesMap, FormattingRequestKind, SmartIndenter, getFormattingScanner, FormattingScanner, FormattingContext, RuleAction, RuleFlags, Rule } from "../ts.formatting"; /* @internal */ -namespace ts.formatting { - export interface FormatContext { - readonly options: FormatCodeSettings; - readonly getRules: RulesMap; +export interface FormatContext { + readonly options: FormatCodeSettings; + readonly getRules: RulesMap; +} +/* @internal */ +export interface TextRangeWithKind extends TextRange { + kind: T; +} +/* @internal */ +export type TextRangeWithTriviaKind = TextRangeWithKind; +/* @internal */ +export interface TokenInfo { + leadingTrivia: TextRangeWithTriviaKind[] | undefined; + token: TextRangeWithKind; + trailingTrivia: TextRangeWithTriviaKind[] | undefined; +} +/* @internal */ +export function createTextRangeWithKind(pos: number, end: number, kind: T): TextRangeWithKind { + const textRangeWithKind: TextRangeWithKind = { pos, end, kind }; + if (Debug.isDebugging) { + Object.defineProperty(textRangeWithKind, "__debugKind", { + get: () => Debug.formatSyntaxKind(kind), + }); } - - export interface TextRangeWithKind extends TextRange { - kind: T; + return textRangeWithKind; +} +/* @internal */ +const enum Constants { + Unknown = -1 +} +/* + * Indentation for the scope that can be dynamically recomputed. + * i.e + * while(true) + * { let x; + * } + * Normally indentation is applied only to the first token in line so at glance 'let' should not be touched. + * However if some format rule adds new line between '}' and 'let' 'let' will become + * the first token in line so it should be indented + */ +/* @internal */ +interface DynamicIndentation { + getIndentationForToken(tokenLine: number, tokenKind: SyntaxKind, container: Node, suppressDelta: boolean): number; + getIndentationForComment(owningToken: SyntaxKind, tokenIndentation: number, container: Node): number; + /** + * Indentation for open and close tokens of the node if it is block or another node that needs special indentation + * ... { + * ......... + * ....} + * ____ - indentation + * ____ - delta + */ + getIndentation(): number; + /** + * Prefered relative indentation for child nodes. + * Delta is used to carry the indentation info + * foo(bar({ + * $ + * })) + * Both 'foo', 'bar' introduce new indentation with delta = 4, but total indentation in $ is not 8. + * foo: { indentation: 0, delta: 4 } + * bar: { indentation: foo.indentation + foo.delta = 4, delta: 4} however 'foo' and 'bar' are on the same line + * so bar inherits indentation from foo and bar.delta will be 4 + * + */ + getDelta(child: TextRangeWithKind): number; + /** + * Formatter calls this function when rule adds or deletes new lines from the text + * so indentation scope can adjust values of indentation and delta. + */ + recomputeIndentation(lineAddedByFormatting: boolean): void; +} +/* @internal */ +export function formatOnEnter(position: number, sourceFile: SourceFile, formatContext: FormatContext): TextChange[] { + const line = sourceFile.getLineAndCharacterOfPosition(position).line; + if (line === 0) { + return []; } - - export type TextRangeWithTriviaKind = TextRangeWithKind; - - export interface TokenInfo { - leadingTrivia: TextRangeWithTriviaKind[] | undefined; - token: TextRangeWithKind; - trailingTrivia: TextRangeWithTriviaKind[] | undefined; + // After the enter key, the cursor is now at a new line. The new line may or may not contain non-whitespace characters. + // If the new line has only whitespaces, we won't want to format this line, because that would remove the indentation as + // trailing whitespaces. So the end of the formatting span should be the later one between: + // 1. the end of the previous line + // 2. the last non-whitespace character in the current line + let endOfFormatSpan = getEndLinePosition(line, sourceFile); + while (isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(endOfFormatSpan))) { + endOfFormatSpan--; } - - export function createTextRangeWithKind(pos: number, end: number, kind: T): TextRangeWithKind { - const textRangeWithKind: TextRangeWithKind = { pos, end, kind }; - if (Debug.isDebugging) { - Object.defineProperty(textRangeWithKind, "__debugKind", { - get: () => Debug.formatSyntaxKind(kind), - }); - } - return textRangeWithKind; + // if the character at the end of the span is a line break, we shouldn't include it, because it indicates we don't want to + // touch the current line at all. Also, on some OSes the line break consists of two characters (\r\n), we should test if the + // previous character before the end of format span is line break character as well. + if (isLineBreak(sourceFile.text.charCodeAt(endOfFormatSpan))) { + endOfFormatSpan--; } - - const enum Constants { - Unknown = -1 + const span = { + // get start position for the previous line + pos: getStartPositionOfLine(line - 1, sourceFile), + // end value is exclusive so add 1 to the result + end: endOfFormatSpan + 1 + }; + return formatSpan(span, sourceFile, formatContext, FormattingRequestKind.FormatOnEnter); +} +/* @internal */ +export function formatOnSemicolon(position: number, sourceFile: SourceFile, formatContext: FormatContext): TextChange[] { + const semicolon = findImmediatelyPrecedingTokenOfKind(position, SyntaxKind.SemicolonToken, sourceFile); + return formatNodeLines(findOutermostNodeWithinListLevel(semicolon), sourceFile, formatContext, FormattingRequestKind.FormatOnSemicolon); +} +/* @internal */ +export function formatOnOpeningCurly(position: number, sourceFile: SourceFile, formatContext: FormatContext): TextChange[] { + const openingCurly = findImmediatelyPrecedingTokenOfKind(position, SyntaxKind.OpenBraceToken, sourceFile); + if (!openingCurly) { + return []; } - - /* - * Indentation for the scope that can be dynamically recomputed. - * i.e - * while(true) - * { let x; + const curlyBraceRange = openingCurly.parent; + const outermostNode = findOutermostNodeWithinListLevel(curlyBraceRange); + /** + * We limit the span to end at the opening curly to handle the case where + * the brace matched to that just typed will be incorrect after further edits. + * For example, we could type the opening curly for the following method + * body without brace-matching activated: + * ``` + * class C { + * foo() * } - * Normally indentation is applied only to the first token in line so at glance 'let' should not be touched. - * However if some format rule adds new line between '}' and 'let' 'let' will become - * the first token in line so it should be indented + * ``` + * and we wouldn't want to move the closing brace. */ - interface DynamicIndentation { - getIndentationForToken(tokenLine: number, tokenKind: SyntaxKind, container: Node, suppressDelta: boolean): number; - getIndentationForComment(owningToken: SyntaxKind, tokenIndentation: number, container: Node): number; - /** - * Indentation for open and close tokens of the node if it is block or another node that needs special indentation - * ... { - * ......... - * ....} - * ____ - indentation - * ____ - delta - */ - getIndentation(): number; - /** - * Prefered relative indentation for child nodes. - * Delta is used to carry the indentation info - * foo(bar({ - * $ - * })) - * Both 'foo', 'bar' introduce new indentation with delta = 4, but total indentation in $ is not 8. - * foo: { indentation: 0, delta: 4 } - * bar: { indentation: foo.indentation + foo.delta = 4, delta: 4} however 'foo' and 'bar' are on the same line - * so bar inherits indentation from foo and bar.delta will be 4 - * - */ - getDelta(child: TextRangeWithKind): number; - /** - * Formatter calls this function when rule adds or deletes new lines from the text - * so indentation scope can adjust values of indentation and delta. - */ - recomputeIndentation(lineAddedByFormatting: boolean): void; + const textRange: TextRange = { + pos: getLineStartPositionForPosition(outermostNode!.getStart(sourceFile), sourceFile), + end: position + }; + return formatSpan(textRange, sourceFile, formatContext, FormattingRequestKind.FormatOnOpeningCurlyBrace); +} +/* @internal */ +export function formatOnClosingCurly(position: number, sourceFile: SourceFile, formatContext: FormatContext): TextChange[] { + const precedingToken = findImmediatelyPrecedingTokenOfKind(position, SyntaxKind.CloseBraceToken, sourceFile); + return formatNodeLines(findOutermostNodeWithinListLevel(precedingToken), sourceFile, formatContext, FormattingRequestKind.FormatOnClosingCurlyBrace); +} +/* @internal */ +export function formatDocument(sourceFile: SourceFile, formatContext: FormatContext): TextChange[] { + const span = { + pos: 0, + end: sourceFile.text.length + }; + return formatSpan(span, sourceFile, formatContext, FormattingRequestKind.FormatDocument); +} +/* @internal */ +export function formatSelection(start: number, end: number, sourceFile: SourceFile, formatContext: FormatContext): TextChange[] { + // format from the beginning of the line + const span = { + pos: getLineStartPositionForPosition(start, sourceFile), + end, + }; + return formatSpan(span, sourceFile, formatContext, FormattingRequestKind.FormatSelection); +} +/** + * Validating `expectedTokenKind` ensures the token was typed in the context we expect (eg: not a comment). + * @param expectedTokenKind The kind of the last token constituting the desired parent node. + */ +/* @internal */ +function findImmediatelyPrecedingTokenOfKind(end: number, expectedTokenKind: SyntaxKind, sourceFile: SourceFile): Node | undefined { + const precedingToken = findPrecedingToken(end, sourceFile); + return precedingToken && precedingToken.kind === expectedTokenKind && end === precedingToken.getEnd() ? + precedingToken : + undefined; +} +/** + * Finds the highest node enclosing `node` at the same list level as `node` + * and whose end does not exceed `node.end`. + * + * Consider typing the following + * ``` + * let x = 1; + * while (true) { + * } + * ``` + * Upon typing the closing curly, we want to format the entire `while`-statement, but not the preceding + * variable declaration. + */ +/* @internal */ +function findOutermostNodeWithinListLevel(node: Node | undefined) { + let current = node; + while (current && + current.parent && + current.parent.end === node!.end && + !isListElement(current.parent, current)) { + current = current.parent; } - - export function formatOnEnter(position: number, sourceFile: SourceFile, formatContext: FormatContext): TextChange[] { - const line = sourceFile.getLineAndCharacterOfPosition(position).line; - if (line === 0) { - return []; - } - // After the enter key, the cursor is now at a new line. The new line may or may not contain non-whitespace characters. - // If the new line has only whitespaces, we won't want to format this line, because that would remove the indentation as - // trailing whitespaces. So the end of the formatting span should be the later one between: - // 1. the end of the previous line - // 2. the last non-whitespace character in the current line - let endOfFormatSpan = getEndLinePosition(line, sourceFile); - while (isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(endOfFormatSpan))) { - endOfFormatSpan--; - } - // if the character at the end of the span is a line break, we shouldn't include it, because it indicates we don't want to - // touch the current line at all. Also, on some OSes the line break consists of two characters (\r\n), we should test if the - // previous character before the end of format span is line break character as well. - if (isLineBreak(sourceFile.text.charCodeAt(endOfFormatSpan))) { - endOfFormatSpan--; + return current; +} +// Returns true if node is a element in some list in parent +// i.e. parent is class declaration with the list of members and node is one of members. +/* @internal */ +function isListElement(parent: Node, node: Node): boolean { + switch (parent.kind) { + case SyntaxKind.ClassDeclaration: + case SyntaxKind.InterfaceDeclaration: + return rangeContainsRange((parent).members, node); + case SyntaxKind.ModuleDeclaration: + const body = (parent).body; + return !!body && body.kind === SyntaxKind.ModuleBlock && rangeContainsRange(body.statements, node); + case SyntaxKind.SourceFile: + case SyntaxKind.Block: + case SyntaxKind.ModuleBlock: + return rangeContainsRange((parent).statements, node); + case SyntaxKind.CatchClause: + return rangeContainsRange((parent).block.statements, node); + } + return false; +} +/** find node that fully contains given text range */ +/* @internal */ +function findEnclosingNode(range: TextRange, sourceFile: SourceFile): Node { + return find(sourceFile); + function find(n: Node): Node { + const candidate = forEachChild(n, c => startEndContainsRange(c.getStart(sourceFile), c.end, range) && c); + if (candidate) { + const result = find(candidate); + if (result) { + return result; + } } - const span = { - // get start position for the previous line - pos: getStartPositionOfLine(line - 1, sourceFile), - // end value is exclusive so add 1 to the result - end: endOfFormatSpan + 1 - }; - return formatSpan(span, sourceFile, formatContext, FormattingRequestKind.FormatOnEnter); + return n; } - - export function formatOnSemicolon(position: number, sourceFile: SourceFile, formatContext: FormatContext): TextChange[] { - const semicolon = findImmediatelyPrecedingTokenOfKind(position, SyntaxKind.SemicolonToken, sourceFile); - return formatNodeLines(findOutermostNodeWithinListLevel(semicolon), sourceFile, formatContext, FormattingRequestKind.FormatOnSemicolon); +} +/** formatting is not applied to ranges that contain parse errors. + * This function will return a predicate that for a given text range will tell + * if there are any parse errors that overlap with the range. + */ +/* @internal */ +function prepareRangeContainsErrorFunction(errors: readonly Diagnostic[], originalRange: TextRange): (r: TextRange) => boolean { + if (!errors.length) { + return rangeHasNoErrors; } - - export function formatOnOpeningCurly(position: number, sourceFile: SourceFile, formatContext: FormatContext): TextChange[] { - const openingCurly = findImmediatelyPrecedingTokenOfKind(position, SyntaxKind.OpenBraceToken, sourceFile); - if (!openingCurly) { - return []; - } - const curlyBraceRange = openingCurly.parent; - const outermostNode = findOutermostNodeWithinListLevel(curlyBraceRange); - - /** - * We limit the span to end at the opening curly to handle the case where - * the brace matched to that just typed will be incorrect after further edits. - * For example, we could type the opening curly for the following method - * body without brace-matching activated: - * ``` - * class C { - * foo() - * } - * ``` - * and we wouldn't want to move the closing brace. - */ - const textRange: TextRange = { - pos: getLineStartPositionForPosition(outermostNode!.getStart(sourceFile), sourceFile), // TODO: GH#18217 - end: position - }; - - return formatSpan(textRange, sourceFile, formatContext, FormattingRequestKind.FormatOnOpeningCurlyBrace); + // pick only errors that fall in range + const sorted = errors + .filter(d => rangeOverlapsWithStartEnd(originalRange, (d.start!), d.start! + d.length!)) // TODO: GH#18217 + .sort((e1, e2) => e1.start! - e2.start!); + if (!sorted.length) { + return rangeHasNoErrors; } - - export function formatOnClosingCurly(position: number, sourceFile: SourceFile, formatContext: FormatContext): TextChange[] { - const precedingToken = findImmediatelyPrecedingTokenOfKind(position, SyntaxKind.CloseBraceToken, sourceFile); - return formatNodeLines(findOutermostNodeWithinListLevel(precedingToken), sourceFile, formatContext, FormattingRequestKind.FormatOnClosingCurlyBrace); + let index = 0; + return r => { + // in current implementation sequence of arguments [r1, r2...] is monotonically increasing. + // 'index' tracks the index of the most recent error that was checked. + while (true) { + if (index >= sorted.length) { + // all errors in the range were already checked -> no error in specified range + return false; + } + const error = sorted[index]; + if (r.end <= error.start!) { + // specified range ends before the error referred by 'index' - no error in range + return false; + } + if (startEndOverlapsWithStartEnd(r.pos, r.end, (error.start!), error.start! + error.length!)) { + // specified range overlaps with error range + return true; + } + index++; + } + }; + function rangeHasNoErrors(): boolean { + return false; } - - export function formatDocument(sourceFile: SourceFile, formatContext: FormatContext): TextChange[] { - const span = { - pos: 0, - end: sourceFile.text.length - }; - return formatSpan(span, sourceFile, formatContext, FormattingRequestKind.FormatDocument); +} +/** + * Start of the original range might fall inside the comment - scanner will not yield appropriate results + * This function will look for token that is located before the start of target range + * and return its end as start position for the scanner. + */ +/* @internal */ +function getScanStartPosition(enclosingNode: Node, originalRange: TextRange, sourceFile: SourceFile): number { + const start = enclosingNode.getStart(sourceFile); + if (start === originalRange.pos && enclosingNode.end === originalRange.end) { + return start; } - - export function formatSelection(start: number, end: number, sourceFile: SourceFile, formatContext: FormatContext): TextChange[] { - // format from the beginning of the line - const span = { - pos: getLineStartPositionForPosition(start, sourceFile), - end, - }; - return formatSpan(span, sourceFile, formatContext, FormattingRequestKind.FormatSelection); + const precedingToken = findPrecedingToken(originalRange.pos, sourceFile); + if (!precedingToken) { + // no preceding token found - start from the beginning of enclosing node + return enclosingNode.pos; } - - /** - * Validating `expectedTokenKind` ensures the token was typed in the context we expect (eg: not a comment). - * @param expectedTokenKind The kind of the last token constituting the desired parent node. - */ - function findImmediatelyPrecedingTokenOfKind(end: number, expectedTokenKind: SyntaxKind, sourceFile: SourceFile): Node | undefined { - const precedingToken = findPrecedingToken(end, sourceFile); - - return precedingToken && precedingToken.kind === expectedTokenKind && end === precedingToken.getEnd() ? - precedingToken : - undefined; + // preceding token ends after the start of original range (i.e when originalRange.pos falls in the middle of literal) + // start from the beginning of enclosingNode to handle the entire 'originalRange' + if (precedingToken.end >= originalRange.pos) { + return enclosingNode.pos; } - - /** - * Finds the highest node enclosing `node` at the same list level as `node` - * and whose end does not exceed `node.end`. - * - * Consider typing the following - * ``` - * let x = 1; - * while (true) { - * } - * ``` - * Upon typing the closing curly, we want to format the entire `while`-statement, but not the preceding - * variable declaration. - */ - function findOutermostNodeWithinListLevel(node: Node | undefined) { - let current = node; - while (current && - current.parent && - current.parent.end === node!.end && - !isListElement(current.parent, current)) { - current = current.parent; + return precedingToken.end; +} +/* + * For cases like + * if (a || + * b ||$ + * c) {...} + * If we hit Enter at $ we want line ' b ||' to be indented. + * Formatting will be applied to the last two lines. + * Node that fully encloses these lines is binary expression 'a ||...'. + * Initial indentation for this node will be 0. + * Binary expressions don't introduce new indentation scopes, however it is possible + * that some parent node on the same line does - like if statement in this case. + * Note that we are considering parents only from the same line with initial node - + * if parent is on the different line - its delta was already contributed + * to the initial indentation. + */ +/* @internal */ +function getOwnOrInheritedDelta(n: Node, options: FormatCodeSettings, sourceFile: SourceFile): number { + let previousLine = Constants.Unknown; + let child: Node | undefined; + while (n) { + const line = sourceFile.getLineAndCharacterOfPosition(n.getStart(sourceFile)).line; + if (previousLine !== Constants.Unknown && line !== previousLine) { + break; } - - return current; - } - - // Returns true if node is a element in some list in parent - // i.e. parent is class declaration with the list of members and node is one of members. - function isListElement(parent: Node, node: Node): boolean { - switch (parent.kind) { - case SyntaxKind.ClassDeclaration: - case SyntaxKind.InterfaceDeclaration: - return rangeContainsRange((parent).members, node); - case SyntaxKind.ModuleDeclaration: - const body = (parent).body; - return !!body && body.kind === SyntaxKind.ModuleBlock && rangeContainsRange(body.statements, node); - case SyntaxKind.SourceFile: - case SyntaxKind.Block: - case SyntaxKind.ModuleBlock: - return rangeContainsRange((parent).statements, node); - case SyntaxKind.CatchClause: - return rangeContainsRange((parent).block.statements, node); + if (SmartIndenter.shouldIndentChildNode(options, n, child, sourceFile)) { + return options.indentSize!; } - - return false; + previousLine = line; + child = n; + n = n.parent; } - - /** find node that fully contains given text range */ - function findEnclosingNode(range: TextRange, sourceFile: SourceFile): Node { - return find(sourceFile); - - function find(n: Node): Node { - const candidate = forEachChild(n, c => startEndContainsRange(c.getStart(sourceFile), c.end, range) && c); - if (candidate) { - const result = find(candidate); - if (result) { - return result; - } - } - - return n; - } + return 0; +} +/* @internal */ +export function formatNodeGivenIndentation(node: Node, sourceFileLike: SourceFileLike, languageVariant: LanguageVariant, initialIndentation: number, delta: number, formatContext: FormatContext): TextChange[] { + const range = { pos: 0, end: sourceFileLike.text.length }; + return getFormattingScanner(sourceFileLike.text, languageVariant, range.pos, range.end, scanner => formatSpanWorker(range, node, initialIndentation, delta, scanner, formatContext, FormattingRequestKind.FormatSelection, _ => false, // assume that node does not have any errors + sourceFileLike)); +} +/* @internal */ +function formatNodeLines(node: Node | undefined, sourceFile: SourceFile, formatContext: FormatContext, requestKind: FormattingRequestKind): TextChange[] { + if (!node) { + return []; } - - /** formatting is not applied to ranges that contain parse errors. - * This function will return a predicate that for a given text range will tell - * if there are any parse errors that overlap with the range. - */ - function prepareRangeContainsErrorFunction(errors: readonly Diagnostic[], originalRange: TextRange): (r: TextRange) => boolean { - if (!errors.length) { - return rangeHasNoErrors; - } - - // pick only errors that fall in range - const sorted = errors - .filter(d => rangeOverlapsWithStartEnd(originalRange, d.start!, d.start! + d.length!)) // TODO: GH#18217 - .sort((e1, e2) => e1.start! - e2.start!); - - if (!sorted.length) { - return rangeHasNoErrors; - } - - let index = 0; - - return r => { - // in current implementation sequence of arguments [r1, r2...] is monotonically increasing. - // 'index' tracks the index of the most recent error that was checked. - while (true) { - if (index >= sorted.length) { - // all errors in the range were already checked -> no error in specified range - return false; - } - - const error = sorted[index]; - if (r.end <= error.start!) { - // specified range ends before the error referred by 'index' - no error in range - return false; - } - - if (startEndOverlapsWithStartEnd(r.pos, r.end, error.start!, error.start! + error.length!)) { - // specified range overlaps with error range - return true; - } - - index++; - } - }; - - function rangeHasNoErrors(): boolean { - return false; + const span = { + pos: getLineStartPositionForPosition(node.getStart(sourceFile), sourceFile), + end: node.end + }; + return formatSpan(span, sourceFile, formatContext, requestKind); +} +/* @internal */ +function formatSpan(originalRange: TextRange, sourceFile: SourceFile, formatContext: FormatContext, requestKind: FormattingRequestKind): TextChange[] { + // find the smallest node that fully wraps the range and compute the initial indentation for the node + const enclosingNode = findEnclosingNode(originalRange, sourceFile); + return getFormattingScanner(sourceFile.text, sourceFile.languageVariant, getScanStartPosition(enclosingNode, originalRange, sourceFile), originalRange.end, scanner => formatSpanWorker(originalRange, enclosingNode, SmartIndenter.getIndentationForNode(enclosingNode, originalRange, sourceFile, formatContext.options), getOwnOrInheritedDelta(enclosingNode, formatContext.options, sourceFile), scanner, formatContext, requestKind, prepareRangeContainsErrorFunction(sourceFile.parseDiagnostics, originalRange), sourceFile)); +} +/* @internal */ +function formatSpanWorker(originalRange: TextRange, enclosingNode: Node, initialIndentation: number, delta: number, formattingScanner: FormattingScanner, { options, getRules }: FormatContext, requestKind: FormattingRequestKind, rangeContainsError: (r: TextRange) => boolean, sourceFile: SourceFileLike): TextChange[] { + // formatting context is used by rules provider + const formattingContext = new FormattingContext(sourceFile, requestKind, options); + let previousRange: TextRangeWithKind; + let previousParent: Node; + let previousRangeStartLine: number; + let lastIndentedLine: number; + let indentationOnLastIndentedLine = Constants.Unknown; + const edits: TextChange[] = []; + formattingScanner.advance(); + if (formattingScanner.isOnToken()) { + const startLine = sourceFile.getLineAndCharacterOfPosition(enclosingNode.getStart(sourceFile)).line; + let undecoratedStartLine = startLine; + if (enclosingNode.decorators) { + undecoratedStartLine = sourceFile.getLineAndCharacterOfPosition(getNonDecoratorTokenPosOfNode(enclosingNode, sourceFile)).line; } + processNode(enclosingNode, enclosingNode, startLine, undecoratedStartLine, initialIndentation, delta); } - - /** - * Start of the original range might fall inside the comment - scanner will not yield appropriate results - * This function will look for token that is located before the start of target range - * and return its end as start position for the scanner. - */ - function getScanStartPosition(enclosingNode: Node, originalRange: TextRange, sourceFile: SourceFile): number { - const start = enclosingNode.getStart(sourceFile); - if (start === originalRange.pos && enclosingNode.end === originalRange.end) { - return start; - } - - const precedingToken = findPrecedingToken(originalRange.pos, sourceFile); - if (!precedingToken) { - // no preceding token found - start from the beginning of enclosing node - return enclosingNode.pos; + if (!formattingScanner.isOnToken()) { + const leadingTrivia = formattingScanner.getCurrentLeadingTrivia(); + if (leadingTrivia) { + indentTriviaItems(leadingTrivia, initialIndentation, /*indentNextTokenOrTrivia*/ false, item => processRange(item, sourceFile.getLineAndCharacterOfPosition(item.pos), enclosingNode, enclosingNode, /*dynamicIndentation*/ undefined!)); + trimTrailingWhitespacesForRemainingRange(); } - - // preceding token ends after the start of original range (i.e when originalRange.pos falls in the middle of literal) - // start from the beginning of enclosingNode to handle the entire 'originalRange' - if (precedingToken.end >= originalRange.pos) { - return enclosingNode.pos; - } - - return precedingToken.end; } - - /* - * For cases like - * if (a || - * b ||$ - * c) {...} - * If we hit Enter at $ we want line ' b ||' to be indented. - * Formatting will be applied to the last two lines. - * Node that fully encloses these lines is binary expression 'a ||...'. - * Initial indentation for this node will be 0. - * Binary expressions don't introduce new indentation scopes, however it is possible - * that some parent node on the same line does - like if statement in this case. - * Note that we are considering parents only from the same line with initial node - - * if parent is on the different line - its delta was already contributed - * to the initial indentation. + return edits; + // local functions + /** Tries to compute the indentation for a list element. + * If list element is not in range then + * function will pick its actual indentation + * so it can be pushed downstream as inherited indentation. + * If list element is in the range - its indentation will be equal + * to inherited indentation from its predecessors. */ - function getOwnOrInheritedDelta(n: Node, options: FormatCodeSettings, sourceFile: SourceFile): number { - let previousLine = Constants.Unknown; - let child: Node | undefined; - while (n) { - const line = sourceFile.getLineAndCharacterOfPosition(n.getStart(sourceFile)).line; - if (previousLine !== Constants.Unknown && line !== previousLine) { - break; + function tryComputeIndentationForListItem(startPos: number, endPos: number, parentStartLine: number, range: TextRange, inheritedIndentation: number): number { + if (rangeOverlapsWithStartEnd(range, startPos, endPos) || + rangeContainsStartEnd(range, startPos, endPos) /* Not to miss zero-range nodes e.g. JsxText */) { + if (inheritedIndentation !== Constants.Unknown) { + return inheritedIndentation; } - - if (SmartIndenter.shouldIndentChildNode(options, n, child, sourceFile)) { - return options.indentSize!; + } + else { + const startLine = sourceFile.getLineAndCharacterOfPosition(startPos).line; + const startLinePosition = getLineStartPositionForPosition(startPos, sourceFile); + const column = SmartIndenter.findFirstNonWhitespaceColumn(startLinePosition, startPos, sourceFile, options); + if (startLine !== parentStartLine || startPos === column) { + // Use the base indent size if it is greater than + // the indentation of the inherited predecessor. + const baseIndentSize = SmartIndenter.getBaseIndentation(options); + return baseIndentSize > column ? baseIndentSize : column; } - - previousLine = line; - child = n; - n = n.parent; } - return 0; + return Constants.Unknown; } - - export function formatNodeGivenIndentation(node: Node, sourceFileLike: SourceFileLike, languageVariant: LanguageVariant, initialIndentation: number, delta: number, formatContext: FormatContext): TextChange[] { - const range = { pos: 0, end: sourceFileLike.text.length }; - return getFormattingScanner(sourceFileLike.text, languageVariant, range.pos, range.end, scanner => formatSpanWorker( - range, - node, - initialIndentation, - delta, - scanner, - formatContext, - FormattingRequestKind.FormatSelection, - _ => false, // assume that node does not have any errors - sourceFileLike)); - } - - function formatNodeLines(node: Node | undefined, sourceFile: SourceFile, formatContext: FormatContext, requestKind: FormattingRequestKind): TextChange[] { - if (!node) { - return []; + function computeIndentation(node: TextRangeWithKind, startLine: number, inheritedIndentation: number, parent: Node, parentDynamicIndentation: DynamicIndentation, effectiveParentStartLine: number): { + indentation: number; + delta: number; + } { + const delta = SmartIndenter.shouldIndentChildNode(options, node) ? options.indentSize! : 0; + if (effectiveParentStartLine === startLine) { + // if node is located on the same line with the parent + // - inherit indentation from the parent + // - push children if either parent of node itself has non-zero delta + return { + indentation: startLine === lastIndentedLine ? indentationOnLastIndentedLine : parentDynamicIndentation.getIndentation(), + delta: Math.min(options.indentSize!, parentDynamicIndentation.getDelta(node) + delta) + }; } - - const span = { - pos: getLineStartPositionForPosition(node.getStart(sourceFile), sourceFile), - end: node.end - }; - - return formatSpan(span, sourceFile, formatContext, requestKind); - } - - function formatSpan(originalRange: TextRange, sourceFile: SourceFile, formatContext: FormatContext, requestKind: FormattingRequestKind): TextChange[] { - // find the smallest node that fully wraps the range and compute the initial indentation for the node - const enclosingNode = findEnclosingNode(originalRange, sourceFile); - return getFormattingScanner( - sourceFile.text, - sourceFile.languageVariant, - getScanStartPosition(enclosingNode, originalRange, sourceFile), - originalRange.end, - scanner => formatSpanWorker( - originalRange, - enclosingNode, - SmartIndenter.getIndentationForNode(enclosingNode, originalRange, sourceFile, formatContext.options), - getOwnOrInheritedDelta(enclosingNode, formatContext.options, sourceFile), - scanner, - formatContext, - requestKind, - prepareRangeContainsErrorFunction(sourceFile.parseDiagnostics, originalRange), - sourceFile)); - } - - function formatSpanWorker(originalRange: TextRange, - enclosingNode: Node, - initialIndentation: number, - delta: number, - formattingScanner: FormattingScanner, - { options, getRules }: FormatContext, - requestKind: FormattingRequestKind, - rangeContainsError: (r: TextRange) => boolean, - sourceFile: SourceFileLike): TextChange[] { - - // formatting context is used by rules provider - const formattingContext = new FormattingContext(sourceFile, requestKind, options); - let previousRange: TextRangeWithKind; - let previousParent: Node; - let previousRangeStartLine: number; - - let lastIndentedLine: number; - let indentationOnLastIndentedLine = Constants.Unknown; - - const edits: TextChange[] = []; - - formattingScanner.advance(); - - if (formattingScanner.isOnToken()) { - const startLine = sourceFile.getLineAndCharacterOfPosition(enclosingNode.getStart(sourceFile)).line; - let undecoratedStartLine = startLine; - if (enclosingNode.decorators) { - undecoratedStartLine = sourceFile.getLineAndCharacterOfPosition(getNonDecoratorTokenPosOfNode(enclosingNode, sourceFile)).line; + else if (inheritedIndentation === Constants.Unknown) { + if (node.kind === SyntaxKind.OpenParenToken && startLine === lastIndentedLine) { + // the is used for chaining methods formatting + // - we need to get the indentation on last line and the delta of parent + return { indentation: indentationOnLastIndentedLine, delta: parentDynamicIndentation.getDelta(node) }; } - - processNode(enclosingNode, enclosingNode, startLine, undecoratedStartLine, initialIndentation, delta); - } - - if (!formattingScanner.isOnToken()) { - const leadingTrivia = formattingScanner.getCurrentLeadingTrivia(); - if (leadingTrivia) { - indentTriviaItems(leadingTrivia, initialIndentation, /*indentNextTokenOrTrivia*/ false, - item => processRange(item, sourceFile.getLineAndCharacterOfPosition(item.pos), enclosingNode, enclosingNode, /*dynamicIndentation*/ undefined!)); - trimTrailingWhitespacesForRemainingRange(); + else if (SmartIndenter.childStartsOnTheSameLineWithElseInIfStatement(parent, node, startLine, sourceFile)) { + return { indentation: parentDynamicIndentation.getIndentation(), delta }; } - } - - return edits; - - // local functions - - /** Tries to compute the indentation for a list element. - * If list element is not in range then - * function will pick its actual indentation - * so it can be pushed downstream as inherited indentation. - * If list element is in the range - its indentation will be equal - * to inherited indentation from its predecessors. - */ - function tryComputeIndentationForListItem(startPos: number, - endPos: number, - parentStartLine: number, - range: TextRange, - inheritedIndentation: number): number { - - if (rangeOverlapsWithStartEnd(range, startPos, endPos) || - rangeContainsStartEnd(range, startPos, endPos) /* Not to miss zero-range nodes e.g. JsxText */) { - - if (inheritedIndentation !== Constants.Unknown) { - return inheritedIndentation; - } + else if (SmartIndenter.argumentStartsOnSameLineAsPreviousArgument(parent, node, startLine, sourceFile)) { + return { indentation: parentDynamicIndentation.getIndentation(), delta }; } else { - const startLine = sourceFile.getLineAndCharacterOfPosition(startPos).line; - const startLinePosition = getLineStartPositionForPosition(startPos, sourceFile); - const column = SmartIndenter.findFirstNonWhitespaceColumn(startLinePosition, startPos, sourceFile, options); - if (startLine !== parentStartLine || startPos === column) { - // Use the base indent size if it is greater than - // the indentation of the inherited predecessor. - const baseIndentSize = SmartIndenter.getBaseIndentation(options); - return baseIndentSize > column ? baseIndentSize : column; - } + return { indentation: parentDynamicIndentation.getIndentation() + parentDynamicIndentation.getDelta(node), delta }; } - - return Constants.Unknown; } - - function computeIndentation( - node: TextRangeWithKind, - startLine: number, - inheritedIndentation: number, - parent: Node, - parentDynamicIndentation: DynamicIndentation, - effectiveParentStartLine: number - ): { indentation: number, delta: number; } { - const delta = SmartIndenter.shouldIndentChildNode(options, node) ? options.indentSize! : 0; - - if (effectiveParentStartLine === startLine) { - // if node is located on the same line with the parent - // - inherit indentation from the parent - // - push children if either parent of node itself has non-zero delta - return { - indentation: startLine === lastIndentedLine ? indentationOnLastIndentedLine : parentDynamicIndentation.getIndentation(), - delta: Math.min(options.indentSize!, parentDynamicIndentation.getDelta(node) + delta) - }; - } - else if (inheritedIndentation === Constants.Unknown) { - if (node.kind === SyntaxKind.OpenParenToken && startLine === lastIndentedLine) { - // the is used for chaining methods formatting - // - we need to get the indentation on last line and the delta of parent - return { indentation: indentationOnLastIndentedLine, delta: parentDynamicIndentation.getDelta(node) }; + else { + return { indentation: inheritedIndentation, delta }; + } + } + function getFirstNonDecoratorTokenOfNode(node: Node) { + if (node.modifiers && node.modifiers.length) { + return node.modifiers[0].kind; + } + switch (node.kind) { + case SyntaxKind.ClassDeclaration: return SyntaxKind.ClassKeyword; + case SyntaxKind.InterfaceDeclaration: return SyntaxKind.InterfaceKeyword; + case SyntaxKind.FunctionDeclaration: return SyntaxKind.FunctionKeyword; + case SyntaxKind.EnumDeclaration: return SyntaxKind.EnumDeclaration; + case SyntaxKind.GetAccessor: return SyntaxKind.GetKeyword; + case SyntaxKind.SetAccessor: return SyntaxKind.SetKeyword; + case SyntaxKind.MethodDeclaration: + if ((node).asteriskToken) { + return SyntaxKind.AsteriskToken; } - else if (SmartIndenter.childStartsOnTheSameLineWithElseInIfStatement(parent, node, startLine, sourceFile)) { - return { indentation: parentDynamicIndentation.getIndentation(), delta }; + // falls through + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.Parameter: + const name = getNameOfDeclaration((node)); + if (name) { + return name.kind; } - else if (SmartIndenter.argumentStartsOnSameLineAsPreviousArgument(parent, node, startLine, sourceFile)) { - return { indentation: parentDynamicIndentation.getIndentation(), delta }; + } + } + function getDynamicIndentation(node: Node, nodeStartLine: number, indentation: number, delta: number): DynamicIndentation { + return { + getIndentationForComment: (kind, tokenIndentation, container) => { + switch (kind) { + // preceding comment to the token that closes the indentation scope inherits the indentation from the scope + // .. { + // // comment + // } + case SyntaxKind.CloseBraceToken: + case SyntaxKind.CloseBracketToken: + case SyntaxKind.CloseParenToken: + return indentation + getDelta(container); } - else { - return { indentation: parentDynamicIndentation.getIndentation() + parentDynamicIndentation.getDelta(node), delta }; + return tokenIndentation !== Constants.Unknown ? tokenIndentation : indentation; + }, + // if list end token is LessThanToken '>' then its delta should be explicitly suppressed + // so that LessThanToken as a binary operator can still be indented. + // foo.then + // < + // number, + // string, + // >(); + // vs + // var a = xValue + // > yValue; + getIndentationForToken: (line, kind, container, suppressDelta) => !suppressDelta && shouldAddDelta(line, kind, container) ? indentation + getDelta(container) : indentation, + getIndentation: () => indentation, + getDelta, + recomputeIndentation: lineAdded => { + if (node.parent && SmartIndenter.shouldIndentChildNode(options, node.parent, node, sourceFile)) { + indentation += lineAdded ? options.indentSize! : -options.indentSize!; + delta = SmartIndenter.shouldIndentChildNode(options, node) ? options.indentSize! : 0; } } - else { - return { indentation: inheritedIndentation, delta }; + }; + function shouldAddDelta(line: number, kind: SyntaxKind, container: Node): boolean { + switch (kind) { + // open and close brace, 'else' and 'while' (in do statement) tokens has indentation of the parent + case SyntaxKind.OpenBraceToken: + case SyntaxKind.CloseBraceToken: + case SyntaxKind.CloseParenToken: + case SyntaxKind.ElseKeyword: + case SyntaxKind.WhileKeyword: + case SyntaxKind.AtToken: + return false; + case SyntaxKind.SlashToken: + case SyntaxKind.GreaterThanToken: + switch (container.kind) { + case SyntaxKind.JsxOpeningElement: + case SyntaxKind.JsxClosingElement: + case SyntaxKind.JsxSelfClosingElement: + return false; + } + break; + case SyntaxKind.OpenBracketToken: + case SyntaxKind.CloseBracketToken: + if (container.kind !== SyntaxKind.MappedType) { + return false; + } + break; } + // if token line equals to the line of containing node (this is a first token in the node) - use node indentation + return nodeStartLine !== line + // if this token is the first token following the list of decorators, we do not need to indent + && !(node.decorators && kind === getFirstNonDecoratorTokenOfNode(node)); + } + function getDelta(child: TextRangeWithKind) { + // Delta value should be zero when the node explicitly prevents indentation of the child node + return SmartIndenter.nodeWillIndentChild(options, node, child, sourceFile, /*indentByDefault*/ true) ? delta : 0; + } + } + function processNode(node: Node, contextNode: Node, nodeStartLine: number, undecoratedNodeStartLine: number, indentation: number, delta: number) { + if (!rangeOverlapsWithStartEnd(originalRange, node.getStart(sourceFile), node.getEnd())) { + return; } - - function getFirstNonDecoratorTokenOfNode(node: Node) { - if (node.modifiers && node.modifiers.length) { - return node.modifiers[0].kind; + const nodeDynamicIndentation = getDynamicIndentation(node, nodeStartLine, indentation, delta); + // a useful observations when tracking context node + // / + // [a] + // / | \ + // [b] [c] [d] + // node 'a' is a context node for nodes 'b', 'c', 'd' + // except for the leftmost leaf token in [b] - in this case context node ('e') is located somewhere above 'a' + // this rule can be applied recursively to child nodes of 'a'. + // + // context node is set to parent node value after processing every child node + // context node is set to parent of the token after processing every token + let childContextNode = contextNode; + // if there are any tokens that logically belong to node and interleave child nodes + // such tokens will be consumed in processChildNode for the child that follows them + forEachChild(node, child => { + processChildNode(child, /*inheritedIndentation*/ Constants.Unknown, node, nodeDynamicIndentation, nodeStartLine, undecoratedNodeStartLine, /*isListItem*/ false); + }, nodes => { + processChildNodes(nodes, node, nodeStartLine, nodeDynamicIndentation); + }); + // proceed any tokens in the node that are located after child nodes + while (formattingScanner.isOnToken()) { + const tokenInfo = formattingScanner.readTokenInfo(node); + if (tokenInfo.token.end > node.end) { + break; } - switch (node.kind) { - case SyntaxKind.ClassDeclaration: return SyntaxKind.ClassKeyword; - case SyntaxKind.InterfaceDeclaration: return SyntaxKind.InterfaceKeyword; - case SyntaxKind.FunctionDeclaration: return SyntaxKind.FunctionKeyword; - case SyntaxKind.EnumDeclaration: return SyntaxKind.EnumDeclaration; - case SyntaxKind.GetAccessor: return SyntaxKind.GetKeyword; - case SyntaxKind.SetAccessor: return SyntaxKind.SetKeyword; - case SyntaxKind.MethodDeclaration: - if ((node).asteriskToken) { - return SyntaxKind.AsteriskToken; - } - // falls through - - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.Parameter: - const name = getNameOfDeclaration(node); - if (name) { - return name.kind; - } + if (node.kind === SyntaxKind.JsxText) { + // Intentation rules for jsx text are handled by `indentMultilineCommentOrJsxText` inside `processChildNode`; just fastforward past it here + formattingScanner.advance(); + continue; } + consumeTokenAndAdvanceScanner(tokenInfo, node, nodeDynamicIndentation, node); } - - function getDynamicIndentation(node: Node, nodeStartLine: number, indentation: number, delta: number): DynamicIndentation { - return { - getIndentationForComment: (kind, tokenIndentation, container) => { - switch (kind) { - // preceding comment to the token that closes the indentation scope inherits the indentation from the scope - // .. { - // // comment - // } - case SyntaxKind.CloseBraceToken: - case SyntaxKind.CloseBracketToken: - case SyntaxKind.CloseParenToken: - return indentation + getDelta(container); - } - return tokenIndentation !== Constants.Unknown ? tokenIndentation : indentation; - }, - // if list end token is LessThanToken '>' then its delta should be explicitly suppressed - // so that LessThanToken as a binary operator can still be indented. - // foo.then - // < - // number, - // string, - // >(); - // vs - // var a = xValue - // > yValue; - getIndentationForToken: (line, kind, container, suppressDelta) => - !suppressDelta && shouldAddDelta(line, kind, container) ? indentation + getDelta(container) : indentation, - getIndentation: () => indentation, - getDelta, - recomputeIndentation: lineAdded => { - if (node.parent && SmartIndenter.shouldIndentChildNode(options, node.parent, node, sourceFile)) { - indentation += lineAdded ? options.indentSize! : -options.indentSize!; - delta = SmartIndenter.shouldIndentChildNode(options, node) ? options.indentSize! : 0; - } - } - }; - - function shouldAddDelta(line: number, kind: SyntaxKind, container: Node): boolean { - switch (kind) { - // open and close brace, 'else' and 'while' (in do statement) tokens has indentation of the parent - case SyntaxKind.OpenBraceToken: - case SyntaxKind.CloseBraceToken: - case SyntaxKind.CloseParenToken: - case SyntaxKind.ElseKeyword: - case SyntaxKind.WhileKeyword: - case SyntaxKind.AtToken: - return false; - case SyntaxKind.SlashToken: - case SyntaxKind.GreaterThanToken: - switch (container.kind) { - case SyntaxKind.JsxOpeningElement: - case SyntaxKind.JsxClosingElement: - case SyntaxKind.JsxSelfClosingElement: - return false; - } - break; - case SyntaxKind.OpenBracketToken: - case SyntaxKind.CloseBracketToken: - if (container.kind !== SyntaxKind.MappedType) { - return false; - } - break; + if (!node.parent && formattingScanner.isOnEOF()) { + const token = formattingScanner.readEOFTokenRange(); + if (token.end <= node.end && previousRange) { + processPair(token, sourceFile.getLineAndCharacterOfPosition(token.pos).line, node, previousRange, previousRangeStartLine, previousParent, contextNode, nodeDynamicIndentation); + } + } + function processChildNode(child: Node, inheritedIndentation: number, parent: Node, parentDynamicIndentation: DynamicIndentation, parentStartLine: number, undecoratedParentStartLine: number, isListItem: boolean, isFirstListItem?: boolean): number { + const childStartPos = child.getStart(sourceFile); + const childStartLine = sourceFile.getLineAndCharacterOfPosition(childStartPos).line; + let undecoratedChildStartLine = childStartLine; + if (child.decorators) { + undecoratedChildStartLine = sourceFile.getLineAndCharacterOfPosition(getNonDecoratorTokenPosOfNode(child, sourceFile)).line; + } + // if child is a list item - try to get its indentation, only if parent is within the original range. + let childIndentationAmount = Constants.Unknown; + if (isListItem && rangeContainsRange(originalRange, parent)) { + childIndentationAmount = tryComputeIndentationForListItem(childStartPos, child.end, parentStartLine, originalRange, inheritedIndentation); + if (childIndentationAmount !== Constants.Unknown) { + inheritedIndentation = childIndentationAmount; } - // if token line equals to the line of containing node (this is a first token in the node) - use node indentation - return nodeStartLine !== line - // if this token is the first token following the list of decorators, we do not need to indent - && !(node.decorators && kind === getFirstNonDecoratorTokenOfNode(node)); } - - function getDelta(child: TextRangeWithKind) { - // Delta value should be zero when the node explicitly prevents indentation of the child node - return SmartIndenter.nodeWillIndentChild(options, node, child, sourceFile, /*indentByDefault*/ true) ? delta : 0; + // child node is outside the target range - do not dive inside + if (!rangeOverlapsWithStartEnd(originalRange, child.pos, child.end)) { + if (child.end < originalRange.pos) { + formattingScanner.skipToEndOf(child); + } + return inheritedIndentation; } - } - - function processNode(node: Node, contextNode: Node, nodeStartLine: number, undecoratedNodeStartLine: number, indentation: number, delta: number) { - if (!rangeOverlapsWithStartEnd(originalRange, node.getStart(sourceFile), node.getEnd())) { - return; + if (child.getFullWidth() === 0) { + return inheritedIndentation; } - - const nodeDynamicIndentation = getDynamicIndentation(node, nodeStartLine, indentation, delta); - - // a useful observations when tracking context node - // / - // [a] - // / | \ - // [b] [c] [d] - // node 'a' is a context node for nodes 'b', 'c', 'd' - // except for the leftmost leaf token in [b] - in this case context node ('e') is located somewhere above 'a' - // this rule can be applied recursively to child nodes of 'a'. - // - // context node is set to parent node value after processing every child node - // context node is set to parent of the token after processing every token - - let childContextNode = contextNode; - - // if there are any tokens that logically belong to node and interleave child nodes - // such tokens will be consumed in processChildNode for the child that follows them - forEachChild( - node, - child => { - processChildNode(child, /*inheritedIndentation*/ Constants.Unknown, node, nodeDynamicIndentation, nodeStartLine, undecoratedNodeStartLine, /*isListItem*/ false); - }, - nodes => { - processChildNodes(nodes, node, nodeStartLine, nodeDynamicIndentation); - }); - - // proceed any tokens in the node that are located after child nodes while (formattingScanner.isOnToken()) { + // proceed any parent tokens that are located prior to child.getStart() const tokenInfo = formattingScanner.readTokenInfo(node); - if (tokenInfo.token.end > node.end) { + if (tokenInfo.token.end > childStartPos) { + // stop when formatting scanner advances past the beginning of the child break; } - if (node.kind === SyntaxKind.JsxText) { - // Intentation rules for jsx text are handled by `indentMultilineCommentOrJsxText` inside `processChildNode`; just fastforward past it here - formattingScanner.advance(); - continue; - } - consumeTokenAndAdvanceScanner(tokenInfo, node, nodeDynamicIndentation, node); + consumeTokenAndAdvanceScanner(tokenInfo, node, parentDynamicIndentation, node); } - - if (!node.parent && formattingScanner.isOnEOF()) { - const token = formattingScanner.readEOFTokenRange(); - if (token.end <= node.end && previousRange) { - processPair( - token, - sourceFile.getLineAndCharacterOfPosition(token.pos).line, - node, - previousRange, - previousRangeStartLine, - previousParent, - contextNode, - nodeDynamicIndentation); - } + if (!formattingScanner.isOnToken()) { + return inheritedIndentation; } - - function processChildNode( - child: Node, - inheritedIndentation: number, - parent: Node, - parentDynamicIndentation: DynamicIndentation, - parentStartLine: number, - undecoratedParentStartLine: number, - isListItem: boolean, - isFirstListItem?: boolean): number { - - const childStartPos = child.getStart(sourceFile); - - const childStartLine = sourceFile.getLineAndCharacterOfPosition(childStartPos).line; - - let undecoratedChildStartLine = childStartLine; - if (child.decorators) { - undecoratedChildStartLine = sourceFile.getLineAndCharacterOfPosition(getNonDecoratorTokenPosOfNode(child, sourceFile)).line; - } - - // if child is a list item - try to get its indentation, only if parent is within the original range. - let childIndentationAmount = Constants.Unknown; - - if (isListItem && rangeContainsRange(originalRange, parent)) { - childIndentationAmount = tryComputeIndentationForListItem(childStartPos, child.end, parentStartLine, originalRange, inheritedIndentation); - if (childIndentationAmount !== Constants.Unknown) { - inheritedIndentation = childIndentationAmount; - } - } - - // child node is outside the target range - do not dive inside - if (!rangeOverlapsWithStartEnd(originalRange, child.pos, child.end)) { - if (child.end < originalRange.pos) { - formattingScanner.skipToEndOf(child); + // JSX text shouldn't affect indenting + if (isToken(child) && child.kind !== SyntaxKind.JsxText) { + // if child node is a token, it does not impact indentation, proceed it using parent indentation scope rules + const tokenInfo = formattingScanner.readTokenInfo(child); + Debug.assert(tokenInfo.token.end === child.end, "Token end is child end"); + consumeTokenAndAdvanceScanner(tokenInfo, node, parentDynamicIndentation, child); + return inheritedIndentation; + } + const effectiveParentStartLine = child.kind === SyntaxKind.Decorator ? childStartLine : undecoratedParentStartLine; + const childIndentation = computeIndentation(child, childStartLine, childIndentationAmount, node, parentDynamicIndentation, effectiveParentStartLine); + processNode(child, childContextNode, childStartLine, undecoratedChildStartLine, childIndentation.indentation, childIndentation.delta); + if (child.kind === SyntaxKind.JsxText) { + const range: TextRange = { pos: child.getStart(), end: child.getEnd() }; + if (range.pos !== range.end) { // don't indent zero-width jsx text + const siblings = parent.getChildren(sourceFile); + const currentIndex = findIndex(siblings, arg => arg.pos === child.pos); + const previousNode = siblings[currentIndex - 1]; + if (previousNode) { + // The jsx text needs no indentation whatsoever if it ends on the same line the previous sibling ends on + if (sourceFile.getLineAndCharacterOfPosition(range.end).line !== sourceFile.getLineAndCharacterOfPosition(previousNode.end).line) { + // The first line is (already) "indented" if the text starts on the same line as the previous sibling element ends on + const firstLineIsIndented = sourceFile.getLineAndCharacterOfPosition(range.pos).line === sourceFile.getLineAndCharacterOfPosition(previousNode.end).line; + indentMultilineCommentOrJsxText(range, childIndentation.indentation, firstLineIsIndented, /*indentFinalLine*/ false, /*jsxStyle*/ true); + } } - return inheritedIndentation; } - - if (child.getFullWidth() === 0) { - return inheritedIndentation; - } - + } + childContextNode = node; + if (isFirstListItem && parent.kind === SyntaxKind.ArrayLiteralExpression && inheritedIndentation === Constants.Unknown) { + inheritedIndentation = childIndentation.indentation; + } + return inheritedIndentation; + } + function processChildNodes(nodes: NodeArray, parent: Node, parentStartLine: number, parentDynamicIndentation: DynamicIndentation): void { + Debug.assert(isNodeArray(nodes)); + const listStartToken = getOpenTokenForList(parent, nodes); + let listDynamicIndentation = parentDynamicIndentation; + let startLine = parentStartLine; + if (listStartToken !== SyntaxKind.Unknown) { + // introduce a new indentation scope for lists (including list start and end tokens) while (formattingScanner.isOnToken()) { - // proceed any parent tokens that are located prior to child.getStart() - const tokenInfo = formattingScanner.readTokenInfo(node); - if (tokenInfo.token.end > childStartPos) { - // stop when formatting scanner advances past the beginning of the child + const tokenInfo = formattingScanner.readTokenInfo(parent); + if (tokenInfo.token.end > nodes.pos) { + // stop when formatting scanner moves past the beginning of node list break; } - - consumeTokenAndAdvanceScanner(tokenInfo, node, parentDynamicIndentation, node); - } - - if (!formattingScanner.isOnToken()) { - return inheritedIndentation; - } - - // JSX text shouldn't affect indenting - if (isToken(child) && child.kind !== SyntaxKind.JsxText) { - // if child node is a token, it does not impact indentation, proceed it using parent indentation scope rules - const tokenInfo = formattingScanner.readTokenInfo(child); - Debug.assert(tokenInfo.token.end === child.end, "Token end is child end"); - consumeTokenAndAdvanceScanner(tokenInfo, node, parentDynamicIndentation, child); - return inheritedIndentation; - } - - const effectiveParentStartLine = child.kind === SyntaxKind.Decorator ? childStartLine : undecoratedParentStartLine; - const childIndentation = computeIndentation(child, childStartLine, childIndentationAmount, node, parentDynamicIndentation, effectiveParentStartLine); - - processNode(child, childContextNode, childStartLine, undecoratedChildStartLine, childIndentation.indentation, childIndentation.delta); - if (child.kind === SyntaxKind.JsxText) { - const range: TextRange = { pos: child.getStart(), end: child.getEnd() }; - if (range.pos !== range.end) { // don't indent zero-width jsx text - const siblings = parent.getChildren(sourceFile); - const currentIndex = findIndex(siblings, arg => arg.pos === child.pos); - const previousNode = siblings[currentIndex - 1]; - if (previousNode) { - // The jsx text needs no indentation whatsoever if it ends on the same line the previous sibling ends on - if (sourceFile.getLineAndCharacterOfPosition(range.end).line !== sourceFile.getLineAndCharacterOfPosition(previousNode.end).line) { - // The first line is (already) "indented" if the text starts on the same line as the previous sibling element ends on - const firstLineIsIndented = sourceFile.getLineAndCharacterOfPosition(range.pos).line === sourceFile.getLineAndCharacterOfPosition(previousNode.end).line; - indentMultilineCommentOrJsxText(range, childIndentation.indentation, firstLineIsIndented, /*indentFinalLine*/ false, /*jsxStyle*/ true); - } + else if (tokenInfo.token.kind === listStartToken) { + // consume list start token + startLine = sourceFile.getLineAndCharacterOfPosition(tokenInfo.token.pos).line; + consumeTokenAndAdvanceScanner(tokenInfo, parent, parentDynamicIndentation, parent); + let indentationOnListStartToken: number; + if (indentationOnLastIndentedLine !== Constants.Unknown) { + // scanner just processed list start token so consider last indentation as list indentation + // function foo(): { // last indentation was 0, list item will be indented based on this value + // foo: number; + // }: {}; + indentationOnListStartToken = indentationOnLastIndentedLine; } + else { + const startLinePosition = getLineStartPositionForPosition(tokenInfo.token.pos, sourceFile); + indentationOnListStartToken = SmartIndenter.findFirstNonWhitespaceColumn(startLinePosition, tokenInfo.token.pos, sourceFile, options); + } + listDynamicIndentation = getDynamicIndentation(parent, parentStartLine, indentationOnListStartToken, options.indentSize!); // TODO: GH#18217 + } + else { + // consume any tokens that precede the list as child elements of 'node' using its indentation scope + consumeTokenAndAdvanceScanner(tokenInfo, parent, parentDynamicIndentation, parent); } } - - childContextNode = node; - - if (isFirstListItem && parent.kind === SyntaxKind.ArrayLiteralExpression && inheritedIndentation === Constants.Unknown) { - inheritedIndentation = childIndentation.indentation; - } - - return inheritedIndentation; } - - function processChildNodes(nodes: NodeArray, - parent: Node, - parentStartLine: number, - parentDynamicIndentation: DynamicIndentation): void { - Debug.assert(isNodeArray(nodes)); - - const listStartToken = getOpenTokenForList(parent, nodes); - - let listDynamicIndentation = parentDynamicIndentation; - let startLine = parentStartLine; - - if (listStartToken !== SyntaxKind.Unknown) { - // introduce a new indentation scope for lists (including list start and end tokens) - while (formattingScanner.isOnToken()) { - const tokenInfo = formattingScanner.readTokenInfo(parent); - if (tokenInfo.token.end > nodes.pos) { - // stop when formatting scanner moves past the beginning of node list - break; - } - else if (tokenInfo.token.kind === listStartToken) { - // consume list start token - startLine = sourceFile.getLineAndCharacterOfPosition(tokenInfo.token.pos).line; - - consumeTokenAndAdvanceScanner(tokenInfo, parent, parentDynamicIndentation, parent); - - let indentationOnListStartToken: number; - if (indentationOnLastIndentedLine !== Constants.Unknown) { - // scanner just processed list start token so consider last indentation as list indentation - // function foo(): { // last indentation was 0, list item will be indented based on this value - // foo: number; - // }: {}; - indentationOnListStartToken = indentationOnLastIndentedLine; - } - else { - const startLinePosition = getLineStartPositionForPosition(tokenInfo.token.pos, sourceFile); - indentationOnListStartToken = SmartIndenter.findFirstNonWhitespaceColumn(startLinePosition, tokenInfo.token.pos, sourceFile, options); - } - - listDynamicIndentation = getDynamicIndentation(parent, parentStartLine, indentationOnListStartToken, options.indentSize!); // TODO: GH#18217 - } - else { - // consume any tokens that precede the list as child elements of 'node' using its indentation scope - consumeTokenAndAdvanceScanner(tokenInfo, parent, parentDynamicIndentation, parent); - } + let inheritedIndentation = Constants.Unknown; + for (let i = 0; i < nodes.length; i++) { + const child = nodes[i]; + inheritedIndentation = processChildNode(child, inheritedIndentation, node, listDynamicIndentation, startLine, startLine, /*isListItem*/ true, /*isFirstListItem*/ i === 0); + } + const listEndToken = getCloseTokenForOpenToken(listStartToken); + if (listEndToken !== SyntaxKind.Unknown && formattingScanner.isOnToken()) { + let tokenInfo: TokenInfo | undefined = formattingScanner.readTokenInfo(parent); + if (tokenInfo.token.kind === SyntaxKind.CommaToken && isCallLikeExpression(parent)) { + const commaTokenLine = sourceFile.getLineAndCharacterOfPosition(tokenInfo.token.pos).line; + if (startLine !== commaTokenLine) { + formattingScanner.advance(); + tokenInfo = formattingScanner.isOnToken() ? formattingScanner.readTokenInfo(parent) : undefined; } } - - let inheritedIndentation = Constants.Unknown; - for (let i = 0; i < nodes.length; i++) { - const child = nodes[i]; - inheritedIndentation = processChildNode(child, inheritedIndentation, node, listDynamicIndentation, startLine, startLine, /*isListItem*/ true, /*isFirstListItem*/ i === 0); + // consume the list end token only if it is still belong to the parent + // there might be the case when current token matches end token but does not considered as one + // function (x: function) <-- + // without this check close paren will be interpreted as list end token for function expression which is wrong + if (tokenInfo && tokenInfo.token.kind === listEndToken && rangeContainsRange(parent, tokenInfo.token)) { + // consume list end token + consumeTokenAndAdvanceScanner(tokenInfo, parent, listDynamicIndentation, parent, /*isListEndToken*/ true); } - - const listEndToken = getCloseTokenForOpenToken(listStartToken); - if (listEndToken !== SyntaxKind.Unknown && formattingScanner.isOnToken()) { - let tokenInfo: TokenInfo | undefined = formattingScanner.readTokenInfo(parent); - if (tokenInfo.token.kind === SyntaxKind.CommaToken && isCallLikeExpression(parent)) { - const commaTokenLine = sourceFile.getLineAndCharacterOfPosition(tokenInfo.token.pos).line; - if (startLine !== commaTokenLine) { - formattingScanner.advance(); - tokenInfo = formattingScanner.isOnToken() ? formattingScanner.readTokenInfo(parent) : undefined; - } + } + } + function consumeTokenAndAdvanceScanner(currentTokenInfo: TokenInfo, parent: Node, dynamicIndentation: DynamicIndentation, container: Node, isListEndToken?: boolean): void { + Debug.assert(rangeContainsRange(parent, currentTokenInfo.token)); + const lastTriviaWasNewLine = formattingScanner.lastTrailingTriviaWasNewLine(); + let indentToken = false; + if (currentTokenInfo.leadingTrivia) { + processTrivia(currentTokenInfo.leadingTrivia, parent, childContextNode, dynamicIndentation); + } + let lineAction = LineAction.None; + const isTokenInRange = rangeContainsRange(originalRange, currentTokenInfo.token); + const tokenStart = sourceFile.getLineAndCharacterOfPosition(currentTokenInfo.token.pos); + if (isTokenInRange) { + const rangeHasError = rangeContainsError(currentTokenInfo.token); + // save previousRange since processRange will overwrite this value with current one + const savePreviousRange = previousRange; + lineAction = processRange(currentTokenInfo.token, tokenStart, parent, childContextNode, dynamicIndentation); + // do not indent comments\token if token range overlaps with some error + if (!rangeHasError) { + if (lineAction === LineAction.None) { + // indent token only if end line of previous range does not match start line of the token + const prevEndLine = savePreviousRange && sourceFile.getLineAndCharacterOfPosition(savePreviousRange.end).line; + indentToken = lastTriviaWasNewLine && tokenStart.line !== prevEndLine; } - - // consume the list end token only if it is still belong to the parent - // there might be the case when current token matches end token but does not considered as one - // function (x: function) <-- - // without this check close paren will be interpreted as list end token for function expression which is wrong - if (tokenInfo && tokenInfo.token.kind === listEndToken && rangeContainsRange(parent, tokenInfo.token)) { - // consume list end token - consumeTokenAndAdvanceScanner(tokenInfo, parent, listDynamicIndentation, parent, /*isListEndToken*/ true); + else { + indentToken = lineAction === LineAction.LineAdded; } } } - - function consumeTokenAndAdvanceScanner(currentTokenInfo: TokenInfo, parent: Node, dynamicIndentation: DynamicIndentation, container: Node, isListEndToken?: boolean): void { - Debug.assert(rangeContainsRange(parent, currentTokenInfo.token)); - - const lastTriviaWasNewLine = formattingScanner.lastTrailingTriviaWasNewLine(); - let indentToken = false; - + if (currentTokenInfo.trailingTrivia) { + processTrivia(currentTokenInfo.trailingTrivia, parent, childContextNode, dynamicIndentation); + } + if (indentToken) { + const tokenIndentation = (isTokenInRange && !rangeContainsError(currentTokenInfo.token)) ? + dynamicIndentation.getIndentationForToken(tokenStart.line, currentTokenInfo.token.kind, container, !!isListEndToken) : + Constants.Unknown; + let indentNextTokenOrTrivia = true; if (currentTokenInfo.leadingTrivia) { - processTrivia(currentTokenInfo.leadingTrivia, parent, childContextNode, dynamicIndentation); - } - - let lineAction = LineAction.None; - const isTokenInRange = rangeContainsRange(originalRange, currentTokenInfo.token); - - const tokenStart = sourceFile.getLineAndCharacterOfPosition(currentTokenInfo.token.pos); - if (isTokenInRange) { - const rangeHasError = rangeContainsError(currentTokenInfo.token); - // save previousRange since processRange will overwrite this value with current one - const savePreviousRange = previousRange; - lineAction = processRange(currentTokenInfo.token, tokenStart, parent, childContextNode, dynamicIndentation); - // do not indent comments\token if token range overlaps with some error - if (!rangeHasError) { - if (lineAction === LineAction.None) { - // indent token only if end line of previous range does not match start line of the token - const prevEndLine = savePreviousRange && sourceFile.getLineAndCharacterOfPosition(savePreviousRange.end).line; - indentToken = lastTriviaWasNewLine && tokenStart.line !== prevEndLine; - } - else { - indentToken = lineAction === LineAction.LineAdded; - } - } + const commentIndentation = dynamicIndentation.getIndentationForComment(currentTokenInfo.token.kind, tokenIndentation, container); + indentNextTokenOrTrivia = indentTriviaItems(currentTokenInfo.leadingTrivia, commentIndentation, indentNextTokenOrTrivia, item => insertIndentation(item.pos, commentIndentation, /*lineAdded*/ false)); } - - if (currentTokenInfo.trailingTrivia) { - processTrivia(currentTokenInfo.trailingTrivia, parent, childContextNode, dynamicIndentation); + // indent token only if is it is in target range and does not overlap with any error ranges + if (tokenIndentation !== Constants.Unknown && indentNextTokenOrTrivia) { + insertIndentation(currentTokenInfo.token.pos, tokenIndentation, lineAction === LineAction.LineAdded); + lastIndentedLine = tokenStart.line; + indentationOnLastIndentedLine = tokenIndentation; } - - if (indentToken) { - const tokenIndentation = (isTokenInRange && !rangeContainsError(currentTokenInfo.token)) ? - dynamicIndentation.getIndentationForToken(tokenStart.line, currentTokenInfo.token.kind, container, !!isListEndToken) : - Constants.Unknown; - - let indentNextTokenOrTrivia = true; - if (currentTokenInfo.leadingTrivia) { - const commentIndentation = dynamicIndentation.getIndentationForComment(currentTokenInfo.token.kind, tokenIndentation, container); - indentNextTokenOrTrivia = indentTriviaItems(currentTokenInfo.leadingTrivia, commentIndentation, indentNextTokenOrTrivia, - item => insertIndentation(item.pos, commentIndentation, /*lineAdded*/ false)); + } + formattingScanner.advance(); + childContextNode = parent; + } + } + function indentTriviaItems(trivia: TextRangeWithKind[], commentIndentation: number, indentNextTokenOrTrivia: boolean, indentSingleLine: (item: TextRangeWithKind) => void) { + for (const triviaItem of trivia) { + const triviaInRange = rangeContainsRange(originalRange, triviaItem); + switch (triviaItem.kind) { + case SyntaxKind.MultiLineCommentTrivia: + if (triviaInRange) { + indentMultilineCommentOrJsxText(triviaItem, commentIndentation, /*firstLineIsIndented*/ !indentNextTokenOrTrivia); } - - // indent token only if is it is in target range and does not overlap with any error ranges - if (tokenIndentation !== Constants.Unknown && indentNextTokenOrTrivia) { - insertIndentation(currentTokenInfo.token.pos, tokenIndentation, lineAction === LineAction.LineAdded); - - lastIndentedLine = tokenStart.line; - indentationOnLastIndentedLine = tokenIndentation; + indentNextTokenOrTrivia = false; + break; + case SyntaxKind.SingleLineCommentTrivia: + if (indentNextTokenOrTrivia && triviaInRange) { + indentSingleLine(triviaItem); } - } - - formattingScanner.advance(); - - childContextNode = parent; + indentNextTokenOrTrivia = false; + break; + case SyntaxKind.NewLineTrivia: + indentNextTokenOrTrivia = true; + break; + } + } + return indentNextTokenOrTrivia; + } + function processTrivia(trivia: TextRangeWithKind[], parent: Node, contextNode: Node, dynamicIndentation: DynamicIndentation): void { + for (const triviaItem of trivia) { + if (isComment(triviaItem.kind) && rangeContainsRange(originalRange, triviaItem)) { + const triviaItemStart = sourceFile.getLineAndCharacterOfPosition(triviaItem.pos); + processRange(triviaItem, triviaItemStart, parent, contextNode, dynamicIndentation); + } + } + } + function processRange(range: TextRangeWithKind, rangeStart: LineAndCharacter, parent: Node, contextNode: Node, dynamicIndentation: DynamicIndentation): LineAction { + const rangeHasError = rangeContainsError(range); + let lineAction = LineAction.None; + if (!rangeHasError) { + if (!previousRange) { + // trim whitespaces starting from the beginning of the span up to the current line + const originalStart = sourceFile.getLineAndCharacterOfPosition(originalRange.pos); + trimTrailingWhitespacesForLines(originalStart.line, rangeStart.line); + } + else { + lineAction = + processPair(range, rangeStart.line, parent, previousRange, previousRangeStartLine, previousParent, contextNode, dynamicIndentation); } } - - function indentTriviaItems( - trivia: TextRangeWithKind[], - commentIndentation: number, - indentNextTokenOrTrivia: boolean, - indentSingleLine: (item: TextRangeWithKind) => void) { - for (const triviaItem of trivia) { - const triviaInRange = rangeContainsRange(originalRange, triviaItem); - switch (triviaItem.kind) { - case SyntaxKind.MultiLineCommentTrivia: - if (triviaInRange) { - indentMultilineCommentOrJsxText(triviaItem, commentIndentation, /*firstLineIsIndented*/ !indentNextTokenOrTrivia); + previousRange = range; + previousParent = parent; + previousRangeStartLine = rangeStart.line; + return lineAction; + } + function processPair(currentItem: TextRangeWithKind, currentStartLine: number, currentParent: Node, previousItem: TextRangeWithKind, previousStartLine: number, previousParent: Node, contextNode: Node, dynamicIndentation: DynamicIndentation): LineAction { + formattingContext.updateContext(previousItem, previousParent, currentItem, currentParent, contextNode); + const rules = getRules(formattingContext); + let trimTrailingWhitespaces = false; + let lineAction = LineAction.None; + if (rules) { + // Apply rules in reverse order so that higher priority rules (which are first in the array) + // win in a conflict with lower priority rules. + forEachRight(rules, rule => { + lineAction = applyRuleEdits(rule, previousItem, previousStartLine, currentItem, currentStartLine); + switch (lineAction) { + case LineAction.LineRemoved: + // Handle the case where the next line is moved to be the end of this line. + // In this case we don't indent the next line in the next pass. + if (currentParent.getStart(sourceFile) === currentItem.pos) { + dynamicIndentation.recomputeIndentation(/*lineAddedByFormatting*/ false); } - indentNextTokenOrTrivia = false; break; - case SyntaxKind.SingleLineCommentTrivia: - if (indentNextTokenOrTrivia && triviaInRange) { - indentSingleLine(triviaItem); + case LineAction.LineAdded: + // Handle the case where token2 is moved to the new line. + // In this case we indent token2 in the next pass but we set + // sameLineIndent flag to notify the indenter that the indentation is within the line. + if (currentParent.getStart(sourceFile) === currentItem.pos) { + dynamicIndentation.recomputeIndentation(/*lineAddedByFormatting*/ true); } - indentNextTokenOrTrivia = false; - break; - case SyntaxKind.NewLineTrivia: - indentNextTokenOrTrivia = true; break; + default: + Debug.assert(lineAction === LineAction.None); } - } - return indentNextTokenOrTrivia; + // We need to trim trailing whitespace between the tokens if they were on different lines, and no rule was applied to put them on the same line + trimTrailingWhitespaces = !(rule.action & RuleAction.DeleteSpace) && rule.flags !== RuleFlags.CanDeleteNewLines; + }); } - - function processTrivia(trivia: TextRangeWithKind[], parent: Node, contextNode: Node, dynamicIndentation: DynamicIndentation): void { - for (const triviaItem of trivia) { - if (isComment(triviaItem.kind) && rangeContainsRange(originalRange, triviaItem)) { - const triviaItemStart = sourceFile.getLineAndCharacterOfPosition(triviaItem.pos); - processRange(triviaItem, triviaItemStart, parent, contextNode, dynamicIndentation); - } - } + else { + trimTrailingWhitespaces = currentItem.kind !== SyntaxKind.EndOfFileToken; } - - function processRange(range: TextRangeWithKind, - rangeStart: LineAndCharacter, - parent: Node, - contextNode: Node, - dynamicIndentation: DynamicIndentation): LineAction { - - const rangeHasError = rangeContainsError(range); - let lineAction = LineAction.None; - if (!rangeHasError) { - if (!previousRange) { - // trim whitespaces starting from the beginning of the span up to the current line - const originalStart = sourceFile.getLineAndCharacterOfPosition(originalRange.pos); - trimTrailingWhitespacesForLines(originalStart.line, rangeStart.line); - } - else { - lineAction = - processPair(range, rangeStart.line, parent, previousRange, previousRangeStartLine, previousParent, contextNode, dynamicIndentation); - } - } - - previousRange = range; - previousParent = parent; - previousRangeStartLine = rangeStart.line; - - return lineAction; + if (currentStartLine !== previousStartLine && trimTrailingWhitespaces) { + // We need to trim trailing whitespace between the tokens if they were on different lines, and no rule was applied to put them on the same line + trimTrailingWhitespacesForLines(previousStartLine, currentStartLine, previousItem); } - - function processPair(currentItem: TextRangeWithKind, - currentStartLine: number, - currentParent: Node, - previousItem: TextRangeWithKind, - previousStartLine: number, - previousParent: Node, - contextNode: Node, - dynamicIndentation: DynamicIndentation): LineAction { - - formattingContext.updateContext(previousItem, previousParent, currentItem, currentParent, contextNode); - - const rules = getRules(formattingContext); - - let trimTrailingWhitespaces = false; - let lineAction = LineAction.None; - if (rules) { - // Apply rules in reverse order so that higher priority rules (which are first in the array) - // win in a conflict with lower priority rules. - forEachRight(rules, rule => { - lineAction = applyRuleEdits(rule, previousItem, previousStartLine, currentItem, currentStartLine); - switch (lineAction) { - case LineAction.LineRemoved: - // Handle the case where the next line is moved to be the end of this line. - // In this case we don't indent the next line in the next pass. - if (currentParent.getStart(sourceFile) === currentItem.pos) { - dynamicIndentation.recomputeIndentation(/*lineAddedByFormatting*/ false); - } - break; - case LineAction.LineAdded: - // Handle the case where token2 is moved to the new line. - // In this case we indent token2 in the next pass but we set - // sameLineIndent flag to notify the indenter that the indentation is within the line. - if (currentParent.getStart(sourceFile) === currentItem.pos) { - dynamicIndentation.recomputeIndentation(/*lineAddedByFormatting*/ true); - } - break; - default: - Debug.assert(lineAction === LineAction.None); - } - - // We need to trim trailing whitespace between the tokens if they were on different lines, and no rule was applied to put them on the same line - trimTrailingWhitespaces = !(rule.action & RuleAction.DeleteSpace) && rule.flags !== RuleFlags.CanDeleteNewLines; - }); - } - else { - trimTrailingWhitespaces = currentItem.kind !== SyntaxKind.EndOfFileToken; - } - - if (currentStartLine !== previousStartLine && trimTrailingWhitespaces) { - // We need to trim trailing whitespace between the tokens if they were on different lines, and no rule was applied to put them on the same line - trimTrailingWhitespacesForLines(previousStartLine, currentStartLine, previousItem); + return lineAction; + } + function insertIndentation(pos: number, indentation: number, lineAdded: boolean | undefined): void { + const indentationString = getIndentationString(indentation, options); + if (lineAdded) { + // new line is added before the token by the formatting rules + // insert indentation string at the very beginning of the token + recordReplace(pos, 0, indentationString); + } + else { + const tokenStart = sourceFile.getLineAndCharacterOfPosition(pos); + const startLinePosition = getStartPositionOfLine(tokenStart.line, sourceFile); + if (indentation !== characterToColumn(startLinePosition, tokenStart.character) || indentationIsDifferent(indentationString, startLinePosition)) { + recordReplace(startLinePosition, tokenStart.character, indentationString); } - - return lineAction; } - - function insertIndentation(pos: number, indentation: number, lineAdded: boolean | undefined): void { - const indentationString = getIndentationString(indentation, options); - if (lineAdded) { - // new line is added before the token by the formatting rules - // insert indentation string at the very beginning of the token - recordReplace(pos, 0, indentationString); + } + function characterToColumn(startLinePosition: number, characterInLine: number): number { + let column = 0; + for (let i = 0; i < characterInLine; i++) { + if (sourceFile.text.charCodeAt(startLinePosition + i) === CharacterCodes.tab) { + column += options.tabSize! - column % options.tabSize!; } else { - const tokenStart = sourceFile.getLineAndCharacterOfPosition(pos); - const startLinePosition = getStartPositionOfLine(tokenStart.line, sourceFile); - if (indentation !== characterToColumn(startLinePosition, tokenStart.character) || indentationIsDifferent(indentationString, startLinePosition)) { - recordReplace(startLinePosition, tokenStart.character, indentationString); - } + column++; } } - - function characterToColumn(startLinePosition: number, characterInLine: number): number { - let column = 0; - for (let i = 0; i < characterInLine; i++) { - if (sourceFile.text.charCodeAt(startLinePosition + i) === CharacterCodes.tab) { - column += options.tabSize! - column % options.tabSize!; - } - else { - column++; - } + return column; + } + function indentationIsDifferent(indentationString: string, startLinePosition: number): boolean { + return indentationString !== sourceFile.text.substr(startLinePosition, indentationString.length); + } + function indentMultilineCommentOrJsxText(commentRange: TextRange, indentation: number, firstLineIsIndented: boolean, indentFinalLine = true, jsxTextStyleIndent?: boolean) { + // split comment in lines + let startLine = sourceFile.getLineAndCharacterOfPosition(commentRange.pos).line; + const endLine = sourceFile.getLineAndCharacterOfPosition(commentRange.end).line; + if (startLine === endLine) { + if (!firstLineIsIndented) { + // treat as single line comment + insertIndentation(commentRange.pos, indentation, /*lineAdded*/ false); } - return column; + return; } - - function indentationIsDifferent(indentationString: string, startLinePosition: number): boolean { - return indentationString !== sourceFile.text.substr(startLinePosition, indentationString.length); + const parts: TextRange[] = []; + let startPos = commentRange.pos; + for (let line = startLine; line < endLine; line++) { + const endOfLine = getEndLinePosition(line, sourceFile); + parts.push({ pos: startPos, end: endOfLine }); + startPos = getStartPositionOfLine(line + 1, sourceFile); } - - function indentMultilineCommentOrJsxText(commentRange: TextRange, indentation: number, firstLineIsIndented: boolean, indentFinalLine = true, jsxTextStyleIndent?: boolean) { - // split comment in lines - let startLine = sourceFile.getLineAndCharacterOfPosition(commentRange.pos).line; - const endLine = sourceFile.getLineAndCharacterOfPosition(commentRange.end).line; - if (startLine === endLine) { - if (!firstLineIsIndented) { - // treat as single line comment - insertIndentation(commentRange.pos, indentation, /*lineAdded*/ false); - } - return; - } - - const parts: TextRange[] = []; - let startPos = commentRange.pos; - for (let line = startLine; line < endLine; line++) { - const endOfLine = getEndLinePosition(line, sourceFile); - parts.push({ pos: startPos, end: endOfLine }); - startPos = getStartPositionOfLine(line + 1, sourceFile); - } - - if (indentFinalLine) { - parts.push({ pos: startPos, end: commentRange.end }); - } - - if (parts.length === 0) return; - - const startLinePos = getStartPositionOfLine(startLine, sourceFile); - - const nonWhitespaceColumnInFirstPart = - SmartIndenter.findFirstNonWhitespaceCharacterAndColumn(startLinePos, parts[0].pos, sourceFile, options); - - if (indentation === nonWhitespaceColumnInFirstPart.column && !jsxTextStyleIndent) { - return; - } - - let startIndex = 0; - if (firstLineIsIndented) { - startIndex = 1; - startLine++; - } - - // shift all parts on the delta size - let delta = indentation - nonWhitespaceColumnInFirstPart.column; - for (let i = startIndex; i < parts.length; i++ , startLine++) { - const startLinePos = getStartPositionOfLine(startLine, sourceFile); - const nonWhitespaceCharacterAndColumn = - i === 0 - ? nonWhitespaceColumnInFirstPart - : SmartIndenter.findFirstNonWhitespaceCharacterAndColumn(parts[i].pos, parts[i].end, sourceFile, options); - if (jsxTextStyleIndent) { - // skip adding indentation to blank lines - if (isLineBreak(sourceFile.text.charCodeAt(getStartPositionOfLine(startLine, sourceFile)))) continue; - // reset delta on every line - delta = indentation - nonWhitespaceCharacterAndColumn.column; - } - const newIndentation = nonWhitespaceCharacterAndColumn.column + delta; - if (newIndentation > 0) { - const indentationString = getIndentationString(newIndentation, options); - recordReplace(startLinePos, nonWhitespaceCharacterAndColumn.character, indentationString); - } - else { - recordDelete(startLinePos, nonWhitespaceCharacterAndColumn.character); - } - } + if (indentFinalLine) { + parts.push({ pos: startPos, end: commentRange.end }); + } + if (parts.length === 0) + return; + const startLinePos = getStartPositionOfLine(startLine, sourceFile); + const nonWhitespaceColumnInFirstPart = SmartIndenter.findFirstNonWhitespaceCharacterAndColumn(startLinePos, parts[0].pos, sourceFile, options); + if (indentation === nonWhitespaceColumnInFirstPart.column && !jsxTextStyleIndent) { + return; } - - function trimTrailingWhitespacesForLines(line1: number, line2: number, range?: TextRangeWithKind) { - for (let line = line1; line < line2; line++) { - const lineStartPosition = getStartPositionOfLine(line, sourceFile); - const lineEndPosition = getEndLinePosition(line, sourceFile); - - // do not trim whitespaces in comments or template expression - if (range && (isComment(range.kind) || isStringOrRegularExpressionOrTemplateLiteral(range.kind)) && range.pos <= lineEndPosition && range.end > lineEndPosition) { + let startIndex = 0; + if (firstLineIsIndented) { + startIndex = 1; + startLine++; + } + // shift all parts on the delta size + let delta = indentation - nonWhitespaceColumnInFirstPart.column; + for (let i = startIndex; i < parts.length; i++, startLine++) { + const startLinePos = getStartPositionOfLine(startLine, sourceFile); + const nonWhitespaceCharacterAndColumn = i === 0 + ? nonWhitespaceColumnInFirstPart + : SmartIndenter.findFirstNonWhitespaceCharacterAndColumn(parts[i].pos, parts[i].end, sourceFile, options); + if (jsxTextStyleIndent) { + // skip adding indentation to blank lines + if (isLineBreak(sourceFile.text.charCodeAt(getStartPositionOfLine(startLine, sourceFile)))) continue; - } - - const whitespaceStart = getTrailingWhitespaceStartPosition(lineStartPosition, lineEndPosition); - if (whitespaceStart !== -1) { - Debug.assert(whitespaceStart === lineStartPosition || !isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(whitespaceStart - 1))); - recordDelete(whitespaceStart, lineEndPosition + 1 - whitespaceStart); - } + // reset delta on every line + delta = indentation - nonWhitespaceCharacterAndColumn.column; } - } - - /** - * @param start The position of the first character in range - * @param end The position of the last character in range - */ - function getTrailingWhitespaceStartPosition(start: number, end: number) { - let pos = end; - while (pos >= start && isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(pos))) { - pos--; + const newIndentation = nonWhitespaceCharacterAndColumn.column + delta; + if (newIndentation > 0) { + const indentationString = getIndentationString(newIndentation, options); + recordReplace(startLinePos, nonWhitespaceCharacterAndColumn.character, indentationString); } - if (pos !== end) { - return pos + 1; + else { + recordDelete(startLinePos, nonWhitespaceCharacterAndColumn.character); } - return -1; - } - - /** - * Trimming will be done for lines after the previous range - */ - function trimTrailingWhitespacesForRemainingRange() { - const startPosition = previousRange ? previousRange.end : originalRange.pos; - - const startLine = sourceFile.getLineAndCharacterOfPosition(startPosition).line; - const endLine = sourceFile.getLineAndCharacterOfPosition(originalRange.end).line; - - trimTrailingWhitespacesForLines(startLine, endLine + 1, previousRange); } - - function recordDelete(start: number, len: number) { - if (len) { - edits.push(createTextChangeFromStartLength(start, len, "")); + } + function trimTrailingWhitespacesForLines(line1: number, line2: number, range?: TextRangeWithKind) { + for (let line = line1; line < line2; line++) { + const lineStartPosition = getStartPositionOfLine(line, sourceFile); + const lineEndPosition = getEndLinePosition(line, sourceFile); + // do not trim whitespaces in comments or template expression + if (range && (isComment(range.kind) || isStringOrRegularExpressionOrTemplateLiteral(range.kind)) && range.pos <= lineEndPosition && range.end > lineEndPosition) { + continue; } - } - - function recordReplace(start: number, len: number, newText: string) { - if (len || newText) { - edits.push(createTextChangeFromStartLength(start, len, newText)); + const whitespaceStart = getTrailingWhitespaceStartPosition(lineStartPosition, lineEndPosition); + if (whitespaceStart !== -1) { + Debug.assert(whitespaceStart === lineStartPosition || !isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(whitespaceStart - 1))); + recordDelete(whitespaceStart, lineEndPosition + 1 - whitespaceStart); } } - - function recordInsert(start: number, text: string) { - if (text) { - edits.push(createTextChangeFromStartLength(start, 0, text)); - } + } + /** + * @param start The position of the first character in range + * @param end The position of the last character in range + */ + function getTrailingWhitespaceStartPosition(start: number, end: number) { + let pos = end; + while (pos >= start && isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(pos))) { + pos--; } - - function applyRuleEdits(rule: Rule, - previousRange: TextRangeWithKind, - previousStartLine: number, - currentRange: TextRangeWithKind, - currentStartLine: number, - ): LineAction { - const onLaterLine = currentStartLine !== previousStartLine; - switch (rule.action) { - case RuleAction.StopProcessingSpaceActions: - // no action required - return LineAction.None; - case RuleAction.DeleteSpace: - if (previousRange.end !== currentRange.pos) { - // delete characters starting from t1.end up to t2.pos exclusive - recordDelete(previousRange.end, currentRange.pos - previousRange.end); - return onLaterLine ? LineAction.LineRemoved : LineAction.None; - } - break; - case RuleAction.DeleteToken: - recordDelete(previousRange.pos, previousRange.end - previousRange.pos); - break; - case RuleAction.InsertNewLine: - // exit early if we on different lines and rule cannot change number of newlines - // if line1 and line2 are on subsequent lines then no edits are required - ok to exit - // if line1 and line2 are separated with more than one newline - ok to exit since we cannot delete extra new lines - if (rule.flags !== RuleFlags.CanDeleteNewLines && previousStartLine !== currentStartLine) { - return LineAction.None; - } - - // edit should not be applied if we have one line feed between elements - const lineDelta = currentStartLine - previousStartLine; - if (lineDelta !== 1) { - recordReplace(previousRange.end, currentRange.pos - previousRange.end, options.newLineCharacter!); - return onLaterLine ? LineAction.None : LineAction.LineAdded; - } - break; - case RuleAction.InsertSpace: - // exit early if we on different lines and rule cannot change number of newlines - if (rule.flags !== RuleFlags.CanDeleteNewLines && previousStartLine !== currentStartLine) { - return LineAction.None; - } - - const posDelta = currentRange.pos - previousRange.end; - if (posDelta !== 1 || sourceFile.text.charCodeAt(previousRange.end) !== CharacterCodes.space) { - recordReplace(previousRange.end, currentRange.pos - previousRange.end, " "); - return onLaterLine ? LineAction.LineRemoved : LineAction.None; - } - break; - case RuleAction.InsertTrailingSemicolon: - recordInsert(previousRange.end, ";"); - } - return LineAction.None; + if (pos !== end) { + return pos + 1; } + return -1; } - - const enum LineAction { None, LineAdded, LineRemoved } - /** - * @param precedingToken pass `null` if preceding token was already computed and result was `undefined`. + * Trimming will be done for lines after the previous range */ - export function getRangeOfEnclosingComment( - sourceFile: SourceFile, - position: number, - precedingToken?: Node | null, - tokenAtPosition = getTokenAtPosition(sourceFile, position), - ): CommentRange | undefined { - const jsdoc = findAncestor(tokenAtPosition, isJSDoc); - if (jsdoc) tokenAtPosition = jsdoc.parent; - const tokenStart = tokenAtPosition.getStart(sourceFile); - if (tokenStart <= position && position < tokenAtPosition.getEnd()) { - return undefined; + function trimTrailingWhitespacesForRemainingRange() { + const startPosition = previousRange ? previousRange.end : originalRange.pos; + const startLine = sourceFile.getLineAndCharacterOfPosition(startPosition).line; + const endLine = sourceFile.getLineAndCharacterOfPosition(originalRange.end).line; + trimTrailingWhitespacesForLines(startLine, endLine + 1, previousRange); + } + function recordDelete(start: number, len: number) { + if (len) { + edits.push(createTextChangeFromStartLength(start, len, "")); } - - // eslint-disable-next-line no-null/no-null - precedingToken = precedingToken === null ? undefined : precedingToken === undefined ? findPrecedingToken(position, sourceFile) : precedingToken; - - // Between two consecutive tokens, all comments are either trailing on the former - // or leading on the latter (and none are in both lists). - const trailingRangesOfPreviousToken = precedingToken && getTrailingCommentRanges(sourceFile.text, precedingToken.end); - const leadingCommentRangesOfNextToken = getLeadingCommentRangesOfNode(tokenAtPosition, sourceFile); - const commentRanges = concatenate(trailingRangesOfPreviousToken, leadingCommentRangesOfNextToken); - return commentRanges && find(commentRanges, range => rangeContainsPositionExclusive(range, position) || - // The end marker of a single-line comment does not include the newline character. - // With caret at `^`, in the following case, we are inside a comment (^ denotes the cursor position): - // - // // asdf ^\n - // - // But for closed multi-line comments, we don't want to be inside the comment in the following case: - // - // /* asdf */^ - // - // However, unterminated multi-line comments *do* contain their end. - // - // Internally, we represent the end of the comment at the newline and closing '/', respectively. - // - position === range.end && (range.kind === SyntaxKind.SingleLineCommentTrivia || position === sourceFile.getFullWidth())); } - - function getOpenTokenForList(node: Node, list: readonly Node[]) { - switch (node.kind) { - case SyntaxKind.Constructor: - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.FunctionExpression: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.MethodSignature: - case SyntaxKind.ArrowFunction: - if ((node).typeParameters === list) { - return SyntaxKind.LessThanToken; - } - else if ((node).parameters === list) { - return SyntaxKind.OpenParenToken; + function recordReplace(start: number, len: number, newText: string) { + if (len || newText) { + edits.push(createTextChangeFromStartLength(start, len, newText)); + } + } + function recordInsert(start: number, text: string) { + if (text) { + edits.push(createTextChangeFromStartLength(start, 0, text)); + } + } + function applyRuleEdits(rule: Rule, previousRange: TextRangeWithKind, previousStartLine: number, currentRange: TextRangeWithKind, currentStartLine: number): LineAction { + const onLaterLine = currentStartLine !== previousStartLine; + switch (rule.action) { + case RuleAction.StopProcessingSpaceActions: + // no action required + return LineAction.None; + case RuleAction.DeleteSpace: + if (previousRange.end !== currentRange.pos) { + // delete characters starting from t1.end up to t2.pos exclusive + recordDelete(previousRange.end, currentRange.pos - previousRange.end); + return onLaterLine ? LineAction.LineRemoved : LineAction.None; } break; - case SyntaxKind.CallExpression: - case SyntaxKind.NewExpression: - if ((node).typeArguments === list) { - return SyntaxKind.LessThanToken; + case RuleAction.DeleteToken: + recordDelete(previousRange.pos, previousRange.end - previousRange.pos); + break; + case RuleAction.InsertNewLine: + // exit early if we on different lines and rule cannot change number of newlines + // if line1 and line2 are on subsequent lines then no edits are required - ok to exit + // if line1 and line2 are separated with more than one newline - ok to exit since we cannot delete extra new lines + if (rule.flags !== RuleFlags.CanDeleteNewLines && previousStartLine !== currentStartLine) { + return LineAction.None; } - else if ((node).arguments === list) { - return SyntaxKind.OpenParenToken; + // edit should not be applied if we have one line feed between elements + const lineDelta = currentStartLine - previousStartLine; + if (lineDelta !== 1) { + recordReplace(previousRange.end, currentRange.pos - previousRange.end, options.newLineCharacter!); + return onLaterLine ? LineAction.None : LineAction.LineAdded; } break; - case SyntaxKind.TypeReference: - if ((node).typeArguments === list) { - return SyntaxKind.LessThanToken; + case RuleAction.InsertSpace: + // exit early if we on different lines and rule cannot change number of newlines + if (rule.flags !== RuleFlags.CanDeleteNewLines && previousStartLine !== currentStartLine) { + return LineAction.None; + } + const posDelta = currentRange.pos - previousRange.end; + if (posDelta !== 1 || sourceFile.text.charCodeAt(previousRange.end) !== CharacterCodes.space) { + recordReplace(previousRange.end, currentRange.pos - previousRange.end, " "); + return onLaterLine ? LineAction.LineRemoved : LineAction.None; } break; - case SyntaxKind.TypeLiteral: - return SyntaxKind.OpenBraceToken; + case RuleAction.InsertTrailingSemicolon: + recordInsert(previousRange.end, ";"); } - - return SyntaxKind.Unknown; + return LineAction.None; } - - function getCloseTokenForOpenToken(kind: SyntaxKind) { - switch (kind) { - case SyntaxKind.OpenParenToken: - return SyntaxKind.CloseParenToken; - case SyntaxKind.LessThanToken: - return SyntaxKind.GreaterThanToken; - case SyntaxKind.OpenBraceToken: - return SyntaxKind.CloseBraceToken; - } - - return SyntaxKind.Unknown; +} +/* @internal */ +const enum LineAction { + None, + LineAdded, + LineRemoved +} +/** + * @param precedingToken pass `null` if preceding token was already computed and result was `undefined`. + */ +/* @internal */ +export function getRangeOfEnclosingComment(sourceFile: SourceFile, position: number, precedingToken?: Node | null, tokenAtPosition = getTokenAtPosition(sourceFile, position)): CommentRange | undefined { + const jsdoc = findAncestor(tokenAtPosition, isJSDoc); + if (jsdoc) + tokenAtPosition = jsdoc.parent; + const tokenStart = tokenAtPosition.getStart(sourceFile); + if (tokenStart <= position && position < tokenAtPosition.getEnd()) { + return undefined; } - - let internedSizes: { tabSize: number; indentSize: number; }; - let internedTabsIndentation: string[] | undefined; - let internedSpacesIndentation: string[] | undefined; - - export function getIndentationString(indentation: number, options: EditorSettings): string { - // reset interned strings if FormatCodeOptions were changed - const resetInternedStrings = - !internedSizes || (internedSizes.tabSize !== options.tabSize || internedSizes.indentSize !== options.indentSize); - - if (resetInternedStrings) { - internedSizes = { tabSize: options.tabSize!, indentSize: options.indentSize! }; - internedTabsIndentation = internedSpacesIndentation = undefined; - } - - if (!options.convertTabsToSpaces) { - const tabs = Math.floor(indentation / options.tabSize!); - const spaces = indentation - tabs * options.tabSize!; - - let tabString: string; - if (!internedTabsIndentation) { - internedTabsIndentation = []; + // eslint-disable-next-line no-null/no-null + precedingToken = precedingToken === null ? undefined : precedingToken === undefined ? findPrecedingToken(position, sourceFile) : precedingToken; + // Between two consecutive tokens, all comments are either trailing on the former + // or leading on the latter (and none are in both lists). + const trailingRangesOfPreviousToken = precedingToken && getTrailingCommentRanges(sourceFile.text, precedingToken.end); + const leadingCommentRangesOfNextToken = getLeadingCommentRangesOfNode(tokenAtPosition, sourceFile); + const commentRanges = concatenate(trailingRangesOfPreviousToken, leadingCommentRangesOfNextToken); + return commentRanges && find(commentRanges, range => rangeContainsPositionExclusive(range, position) || + // The end marker of a single-line comment does not include the newline character. + // With caret at `^`, in the following case, we are inside a comment (^ denotes the cursor position): + // + // // asdf ^\n + // + // But for closed multi-line comments, we don't want to be inside the comment in the following case: + // + // /* asdf */^ + // + // However, unterminated multi-line comments *do* contain their end. + // + // Internally, we represent the end of the comment at the newline and closing '/', respectively. + // + position === range.end && (range.kind === SyntaxKind.SingleLineCommentTrivia || position === sourceFile.getFullWidth())); +} +/* @internal */ +function getOpenTokenForList(node: Node, list: readonly Node[]) { + switch (node.kind) { + case SyntaxKind.Constructor: + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.FunctionExpression: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: + case SyntaxKind.ArrowFunction: + if ((node).typeParameters === list) { + return SyntaxKind.LessThanToken; } - - if (internedTabsIndentation[tabs] === undefined) { - internedTabsIndentation[tabs] = tabString = repeatString("\t", tabs); + else if ((node).parameters === list) { + return SyntaxKind.OpenParenToken; } - else { - tabString = internedTabsIndentation[tabs]; - } - - return spaces ? tabString + repeatString(" ", spaces) : tabString; - } - else { - let spacesString: string; - const quotient = Math.floor(indentation / options.indentSize!); - const remainder = indentation % options.indentSize!; - if (!internedSpacesIndentation) { - internedSpacesIndentation = []; + break; + case SyntaxKind.CallExpression: + case SyntaxKind.NewExpression: + if ((node).typeArguments === list) { + return SyntaxKind.LessThanToken; } - - if (internedSpacesIndentation[quotient] === undefined) { - spacesString = repeatString(" ", options.indentSize! * quotient); - internedSpacesIndentation[quotient] = spacesString; + else if ((node).arguments === list) { + return SyntaxKind.OpenParenToken; } - else { - spacesString = internedSpacesIndentation[quotient]; + break; + case SyntaxKind.TypeReference: + if ((node).typeArguments === list) { + return SyntaxKind.LessThanToken; } - - return remainder ? spacesString + repeatString(" ", remainder) : spacesString; + break; + case SyntaxKind.TypeLiteral: + return SyntaxKind.OpenBraceToken; + } + return SyntaxKind.Unknown; +} +/* @internal */ +function getCloseTokenForOpenToken(kind: SyntaxKind) { + switch (kind) { + case SyntaxKind.OpenParenToken: + return SyntaxKind.CloseParenToken; + case SyntaxKind.LessThanToken: + return SyntaxKind.GreaterThanToken; + case SyntaxKind.OpenBraceToken: + return SyntaxKind.CloseBraceToken; + } + return SyntaxKind.Unknown; +} +/* @internal */ +let internedSizes: { + tabSize: number; + indentSize: number; +}; +/* @internal */ +let internedTabsIndentation: string[] | undefined; +/* @internal */ +let internedSpacesIndentation: string[] | undefined; +/* @internal */ +export function getIndentationString(indentation: number, options: EditorSettings): string { + // reset interned strings if FormatCodeOptions were changed + const resetInternedStrings = !internedSizes || (internedSizes.tabSize !== options.tabSize || internedSizes.indentSize !== options.indentSize); + if (resetInternedStrings) { + internedSizes = { tabSize: options.tabSize!, indentSize: options.indentSize! }; + internedTabsIndentation = internedSpacesIndentation = undefined; + } + if (!options.convertTabsToSpaces) { + const tabs = Math.floor(indentation / options.tabSize!); + const spaces = indentation - tabs * options.tabSize!; + let tabString: string; + if (!internedTabsIndentation) { + internedTabsIndentation = []; + } + if (internedTabsIndentation[tabs] === undefined) { + internedTabsIndentation[tabs] = tabString = repeatString("\t", tabs); + } + else { + tabString = internedTabsIndentation[tabs]; + } + return spaces ? tabString + repeatString(" ", spaces) : tabString; + } + else { + let spacesString: string; + const quotient = Math.floor(indentation / options.indentSize!); + const remainder = indentation % options.indentSize!; + if (!internedSpacesIndentation) { + internedSpacesIndentation = []; + } + if (internedSpacesIndentation[quotient] === undefined) { + spacesString = repeatString(" ", options.indentSize! * quotient); + internedSpacesIndentation[quotient] = spacesString; + } + else { + spacesString = internedSpacesIndentation[quotient]; } + return remainder ? spacesString + repeatString(" ", remainder) : spacesString; } } diff --git a/src/services/formatting/formattingContext.ts b/src/services/formatting/formattingContext.ts index 5701f0cd0ad0e..d17e7f766547f 100644 --- a/src/services/formatting/formattingContext.ts +++ b/src/services/formatting/formattingContext.ts @@ -1,102 +1,86 @@ +import { TextRangeWithKind } from "../ts.formatting"; +import { Node, SourceFileLike, FormatCodeSettings, Debug, findChildOfKind, SyntaxKind } from "../ts"; /* @internal */ -namespace ts.formatting { - export const enum FormattingRequestKind { - FormatDocument, - FormatSelection, - FormatOnEnter, - FormatOnSemicolon, - FormatOnOpeningCurlyBrace, - FormatOnClosingCurlyBrace +export const enum FormattingRequestKind { + FormatDocument, + FormatSelection, + FormatOnEnter, + FormatOnSemicolon, + FormatOnOpeningCurlyBrace, + FormatOnClosingCurlyBrace +} +/* @internal */ +export class FormattingContext { + public currentTokenSpan!: TextRangeWithKind; + public nextTokenSpan!: TextRangeWithKind; + public contextNode!: Node; + public currentTokenParent!: Node; + public nextTokenParent!: Node; + private contextNodeAllOnSameLine: boolean | undefined; + private nextNodeAllOnSameLine: boolean | undefined; + private tokensAreOnSameLine: boolean | undefined; + private contextNodeBlockIsOnOneLine: boolean | undefined; + private nextNodeBlockIsOnOneLine: boolean | undefined; + constructor(public readonly sourceFile: SourceFileLike, public formattingRequestKind: FormattingRequestKind, public options: FormatCodeSettings) { } - - export class FormattingContext { - public currentTokenSpan!: TextRangeWithKind; - public nextTokenSpan!: TextRangeWithKind; - public contextNode!: Node; - public currentTokenParent!: Node; - public nextTokenParent!: Node; - - private contextNodeAllOnSameLine: boolean | undefined; - private nextNodeAllOnSameLine: boolean | undefined; - private tokensAreOnSameLine: boolean | undefined; - private contextNodeBlockIsOnOneLine: boolean | undefined; - private nextNodeBlockIsOnOneLine: boolean | undefined; - - constructor(public readonly sourceFile: SourceFileLike, public formattingRequestKind: FormattingRequestKind, public options: FormatCodeSettings) { - } - - public updateContext(currentRange: TextRangeWithKind, currentTokenParent: Node, nextRange: TextRangeWithKind, nextTokenParent: Node, commonParent: Node) { - this.currentTokenSpan = Debug.checkDefined(currentRange); - this.currentTokenParent = Debug.checkDefined(currentTokenParent); - this.nextTokenSpan = Debug.checkDefined(nextRange); - this.nextTokenParent = Debug.checkDefined(nextTokenParent); - this.contextNode = Debug.checkDefined(commonParent); - - // drop cached results - this.contextNodeAllOnSameLine = undefined; - this.nextNodeAllOnSameLine = undefined; - this.tokensAreOnSameLine = undefined; - this.contextNodeBlockIsOnOneLine = undefined; - this.nextNodeBlockIsOnOneLine = undefined; - } - - public ContextNodeAllOnSameLine(): boolean { - if (this.contextNodeAllOnSameLine === undefined) { - this.contextNodeAllOnSameLine = this.NodeIsOnOneLine(this.contextNode); - } - - return this.contextNodeAllOnSameLine; + public updateContext(currentRange: TextRangeWithKind, currentTokenParent: Node, nextRange: TextRangeWithKind, nextTokenParent: Node, commonParent: Node) { + this.currentTokenSpan = Debug.checkDefined(currentRange); + this.currentTokenParent = Debug.checkDefined(currentTokenParent); + this.nextTokenSpan = Debug.checkDefined(nextRange); + this.nextTokenParent = Debug.checkDefined(nextTokenParent); + this.contextNode = Debug.checkDefined(commonParent); + // drop cached results + this.contextNodeAllOnSameLine = undefined; + this.nextNodeAllOnSameLine = undefined; + this.tokensAreOnSameLine = undefined; + this.contextNodeBlockIsOnOneLine = undefined; + this.nextNodeBlockIsOnOneLine = undefined; + } + public ContextNodeAllOnSameLine(): boolean { + if (this.contextNodeAllOnSameLine === undefined) { + this.contextNodeAllOnSameLine = this.NodeIsOnOneLine(this.contextNode); } - - public NextNodeAllOnSameLine(): boolean { - if (this.nextNodeAllOnSameLine === undefined) { - this.nextNodeAllOnSameLine = this.NodeIsOnOneLine(this.nextTokenParent); - } - - return this.nextNodeAllOnSameLine; + return this.contextNodeAllOnSameLine; + } + public NextNodeAllOnSameLine(): boolean { + if (this.nextNodeAllOnSameLine === undefined) { + this.nextNodeAllOnSameLine = this.NodeIsOnOneLine(this.nextTokenParent); } - - public TokensAreOnSameLine(): boolean { - if (this.tokensAreOnSameLine === undefined) { - const startLine = this.sourceFile.getLineAndCharacterOfPosition(this.currentTokenSpan.pos).line; - const endLine = this.sourceFile.getLineAndCharacterOfPosition(this.nextTokenSpan.pos).line; - this.tokensAreOnSameLine = (startLine === endLine); - } - - return this.tokensAreOnSameLine; + return this.nextNodeAllOnSameLine; + } + public TokensAreOnSameLine(): boolean { + if (this.tokensAreOnSameLine === undefined) { + const startLine = this.sourceFile.getLineAndCharacterOfPosition(this.currentTokenSpan.pos).line; + const endLine = this.sourceFile.getLineAndCharacterOfPosition(this.nextTokenSpan.pos).line; + this.tokensAreOnSameLine = (startLine === endLine); } - - public ContextNodeBlockIsOnOneLine() { - if (this.contextNodeBlockIsOnOneLine === undefined) { - this.contextNodeBlockIsOnOneLine = this.BlockIsOnOneLine(this.contextNode); - } - - return this.contextNodeBlockIsOnOneLine; + return this.tokensAreOnSameLine; + } + public ContextNodeBlockIsOnOneLine() { + if (this.contextNodeBlockIsOnOneLine === undefined) { + this.contextNodeBlockIsOnOneLine = this.BlockIsOnOneLine(this.contextNode); } - - public NextNodeBlockIsOnOneLine() { - if (this.nextNodeBlockIsOnOneLine === undefined) { - this.nextNodeBlockIsOnOneLine = this.BlockIsOnOneLine(this.nextTokenParent); - } - - return this.nextNodeBlockIsOnOneLine; + return this.contextNodeBlockIsOnOneLine; + } + public NextNodeBlockIsOnOneLine() { + if (this.nextNodeBlockIsOnOneLine === undefined) { + this.nextNodeBlockIsOnOneLine = this.BlockIsOnOneLine(this.nextTokenParent); } - - private NodeIsOnOneLine(node: Node): boolean { - const startLine = this.sourceFile.getLineAndCharacterOfPosition(node.getStart(this.sourceFile)).line; - const endLine = this.sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line; + return this.nextNodeBlockIsOnOneLine; + } + private NodeIsOnOneLine(node: Node): boolean { + const startLine = this.sourceFile.getLineAndCharacterOfPosition(node.getStart(this.sourceFile)).line; + const endLine = this.sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line; + return startLine === endLine; + } + private BlockIsOnOneLine(node: Node): boolean { + const openBrace = findChildOfKind(node, SyntaxKind.OpenBraceToken, this.sourceFile); + const closeBrace = findChildOfKind(node, SyntaxKind.CloseBraceToken, this.sourceFile); + if (openBrace && closeBrace) { + const startLine = this.sourceFile.getLineAndCharacterOfPosition(openBrace.getEnd()).line; + const endLine = this.sourceFile.getLineAndCharacterOfPosition(closeBrace.getStart(this.sourceFile)).line; return startLine === endLine; } - - private BlockIsOnOneLine(node: Node): boolean { - const openBrace = findChildOfKind(node, SyntaxKind.OpenBraceToken, this.sourceFile); - const closeBrace = findChildOfKind(node, SyntaxKind.CloseBraceToken, this.sourceFile); - if (openBrace && closeBrace) { - const startLine = this.sourceFile.getLineAndCharacterOfPosition(openBrace.getEnd()).line; - const endLine = this.sourceFile.getLineAndCharacterOfPosition(closeBrace.getStart(this.sourceFile)).line; - return startLine === endLine; - } - return false; - } + return false; } } diff --git a/src/services/formatting/formattingScanner.ts b/src/services/formatting/formattingScanner.ts index 64e257f4a0834..a501f79ac8ded 100644 --- a/src/services/formatting/formattingScanner.ts +++ b/src/services/formatting/formattingScanner.ts @@ -1,302 +1,254 @@ +import { createScanner, ScriptTarget, LanguageVariant, Node, last, SyntaxKind, isTrivia, append, isKeyword, isJsxText, findAncestor, isJsxElement, isParenthesizedExpression, isJsxAttribute, Debug, isToken } from "../ts"; +import { TokenInfo, TextRangeWithKind, TextRangeWithTriviaKind, createTextRangeWithKind } from "../ts.formatting"; /* @internal */ -namespace ts.formatting { - const standardScanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ false, LanguageVariant.Standard); - const jsxScanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ false, LanguageVariant.JSX); - - export interface FormattingScanner { - advance(): void; - isOnToken(): boolean; - isOnEOF(): boolean; - readTokenInfo(n: Node): TokenInfo; - readEOFTokenRange(): TextRangeWithKind; - getCurrentLeadingTrivia(): TextRangeWithKind[] | undefined; - lastTrailingTriviaWasNewLine(): boolean; - skipToEndOf(node: Node): void; - } - - const enum ScanAction { - Scan, - RescanGreaterThanToken, - RescanSlashToken, - RescanTemplateToken, - RescanJsxIdentifier, - RescanJsxText, - RescanJsxAttributeValue, - } - - export function getFormattingScanner(text: string, languageVariant: LanguageVariant, startPos: number, endPos: number, cb: (scanner: FormattingScanner) => T): T { - const scanner = languageVariant === LanguageVariant.JSX ? jsxScanner : standardScanner; - - scanner.setText(text); - scanner.setTextPos(startPos); - - let wasNewLine = true; - let leadingTrivia: TextRangeWithTriviaKind[] | undefined; - let trailingTrivia: TextRangeWithTriviaKind[] | undefined; - - let savedPos: number; - let lastScanAction: ScanAction | undefined; - let lastTokenInfo: TokenInfo | undefined; - - const res = cb({ - advance, - readTokenInfo, - readEOFTokenRange, - isOnToken, - isOnEOF, - getCurrentLeadingTrivia: () => leadingTrivia, - lastTrailingTriviaWasNewLine: () => wasNewLine, - skipToEndOf, - }); - +const standardScanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ false, LanguageVariant.Standard); +/* @internal */ +const jsxScanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ false, LanguageVariant.JSX); +/* @internal */ +export interface FormattingScanner { + advance(): void; + isOnToken(): boolean; + isOnEOF(): boolean; + readTokenInfo(n: Node): TokenInfo; + readEOFTokenRange(): TextRangeWithKind; + getCurrentLeadingTrivia(): TextRangeWithKind[] | undefined; + lastTrailingTriviaWasNewLine(): boolean; + skipToEndOf(node: Node): void; +} +/* @internal */ +const enum ScanAction { + Scan, + RescanGreaterThanToken, + RescanSlashToken, + RescanTemplateToken, + RescanJsxIdentifier, + RescanJsxText, + RescanJsxAttributeValue +} +/* @internal */ +export function getFormattingScanner(text: string, languageVariant: LanguageVariant, startPos: number, endPos: number, cb: (scanner: FormattingScanner) => T): T { + const scanner = languageVariant === LanguageVariant.JSX ? jsxScanner : standardScanner; + scanner.setText(text); + scanner.setTextPos(startPos); + let wasNewLine = true; + let leadingTrivia: TextRangeWithTriviaKind[] | undefined; + let trailingTrivia: TextRangeWithTriviaKind[] | undefined; + let savedPos: number; + let lastScanAction: ScanAction | undefined; + let lastTokenInfo: TokenInfo | undefined; + const res = cb({ + advance, + readTokenInfo, + readEOFTokenRange, + isOnToken, + isOnEOF, + getCurrentLeadingTrivia: () => leadingTrivia, + lastTrailingTriviaWasNewLine: () => wasNewLine, + skipToEndOf, + }); + lastTokenInfo = undefined; + scanner.setText(undefined); + return res; + function advance(): void { lastTokenInfo = undefined; - scanner.setText(undefined); - - return res; - - function advance(): void { - lastTokenInfo = undefined; - const isStarted = scanner.getStartPos() !== startPos; - - if (isStarted) { - wasNewLine = !!trailingTrivia && last(trailingTrivia).kind === SyntaxKind.NewLineTrivia; - } - else { - scanner.scan(); - } - - leadingTrivia = undefined; - trailingTrivia = undefined; - - let pos = scanner.getStartPos(); - - // Read leading trivia and token - while (pos < endPos) { - const t = scanner.getToken(); - if (!isTrivia(t)) { - break; - } - - // consume leading trivia - scanner.scan(); - const item: TextRangeWithTriviaKind = { - pos, - end: scanner.getStartPos(), - kind: t - }; - - pos = scanner.getStartPos(); - - leadingTrivia = append(leadingTrivia, item); - } - - savedPos = scanner.getStartPos(); + const isStarted = scanner.getStartPos() !== startPos; + if (isStarted) { + wasNewLine = !!trailingTrivia && last(trailingTrivia).kind === SyntaxKind.NewLineTrivia; } - - function shouldRescanGreaterThanToken(node: Node): boolean { - switch (node.kind) { - case SyntaxKind.GreaterThanEqualsToken: - case SyntaxKind.GreaterThanGreaterThanEqualsToken: - case SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: - case SyntaxKind.GreaterThanGreaterThanGreaterThanToken: - case SyntaxKind.GreaterThanGreaterThanToken: - return true; - } - - return false; + else { + scanner.scan(); } - - function shouldRescanJsxIdentifier(node: Node): boolean { - if (node.parent) { - switch (node.parent.kind) { - case SyntaxKind.JsxAttribute: - case SyntaxKind.JsxOpeningElement: - case SyntaxKind.JsxClosingElement: - case SyntaxKind.JsxSelfClosingElement: - // May parse an identifier like `module-layout`; that will be scanned as a keyword at first, but we should parse the whole thing to get an identifier. - return isKeyword(node.kind) || node.kind === SyntaxKind.Identifier; - } + leadingTrivia = undefined; + trailingTrivia = undefined; + let pos = scanner.getStartPos(); + // Read leading trivia and token + while (pos < endPos) { + const t = scanner.getToken(); + if (!isTrivia(t)) { + break; } - - return false; + // consume leading trivia + scanner.scan(); + const item: TextRangeWithTriviaKind = { + pos, + end: scanner.getStartPos(), + kind: t + }; + pos = scanner.getStartPos(); + leadingTrivia = append(leadingTrivia, item); + } + savedPos = scanner.getStartPos(); + } + function shouldRescanGreaterThanToken(node: Node): boolean { + switch (node.kind) { + case SyntaxKind.GreaterThanEqualsToken: + case SyntaxKind.GreaterThanGreaterThanEqualsToken: + case SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: + case SyntaxKind.GreaterThanGreaterThanGreaterThanToken: + case SyntaxKind.GreaterThanGreaterThanToken: + return true; } - - function shouldRescanJsxText(node: Node): boolean { - const isJSXText = isJsxText(node); - if (isJSXText) { - const containingElement = findAncestor(node.parent, p => isJsxElement(p)); - if (!containingElement) return false; // should never happen - return !isParenthesizedExpression(containingElement.parent); + return false; + } + function shouldRescanJsxIdentifier(node: Node): boolean { + if (node.parent) { + switch (node.parent.kind) { + case SyntaxKind.JsxAttribute: + case SyntaxKind.JsxOpeningElement: + case SyntaxKind.JsxClosingElement: + case SyntaxKind.JsxSelfClosingElement: + // May parse an identifier like `module-layout`; that will be scanned as a keyword at first, but we should parse the whole thing to get an identifier. + return isKeyword(node.kind) || node.kind === SyntaxKind.Identifier; } - return false; } - - function shouldRescanSlashToken(container: Node): boolean { - return container.kind === SyntaxKind.RegularExpressionLiteral; + return false; + } + function shouldRescanJsxText(node: Node): boolean { + const isJSXText = isJsxText(node); + if (isJSXText) { + const containingElement = findAncestor(node.parent, p => isJsxElement(p)); + if (!containingElement) + return false; // should never happen + return !isParenthesizedExpression(containingElement.parent); } - - function shouldRescanTemplateToken(container: Node): boolean { - return container.kind === SyntaxKind.TemplateMiddle || - container.kind === SyntaxKind.TemplateTail; + return false; + } + function shouldRescanSlashToken(container: Node): boolean { + return container.kind === SyntaxKind.RegularExpressionLiteral; + } + function shouldRescanTemplateToken(container: Node): boolean { + return container.kind === SyntaxKind.TemplateMiddle || + container.kind === SyntaxKind.TemplateTail; + } + function shouldRescanJsxAttributeValue(node: Node): boolean { + return node.parent && isJsxAttribute(node.parent) && node.parent.initializer === node; + } + function startsWithSlashToken(t: SyntaxKind): boolean { + return t === SyntaxKind.SlashToken || t === SyntaxKind.SlashEqualsToken; + } + function readTokenInfo(n: Node): TokenInfo { + Debug.assert(isOnToken()); + // normally scanner returns the smallest available token + // check the kind of context node to determine if scanner should have more greedy behavior and consume more text. + const expectedScanAction = shouldRescanGreaterThanToken(n) ? ScanAction.RescanGreaterThanToken : + shouldRescanSlashToken(n) ? ScanAction.RescanSlashToken : + shouldRescanTemplateToken(n) ? ScanAction.RescanTemplateToken : + shouldRescanJsxIdentifier(n) ? ScanAction.RescanJsxIdentifier : + shouldRescanJsxText(n) ? ScanAction.RescanJsxText : + shouldRescanJsxAttributeValue(n) ? ScanAction.RescanJsxAttributeValue : + ScanAction.Scan; + if (lastTokenInfo && expectedScanAction === lastScanAction) { + // readTokenInfo was called before with the same expected scan action. + // No need to re-scan text, return existing 'lastTokenInfo' + // it is ok to call fixTokenKind here since it does not affect + // what portion of text is consumed. In contrast rescanning can change it, + // i.e. for '>=' when originally scanner eats just one character + // and rescanning forces it to consume more. + return fixTokenKind(lastTokenInfo, n); } - - function shouldRescanJsxAttributeValue(node: Node): boolean { - return node.parent && isJsxAttribute(node.parent) && node.parent.initializer === node; + if (scanner.getStartPos() !== savedPos) { + Debug.assert(lastTokenInfo !== undefined); + // readTokenInfo was called before but scan action differs - rescan text + scanner.setTextPos(savedPos); + scanner.scan(); } - - function startsWithSlashToken(t: SyntaxKind): boolean { - return t === SyntaxKind.SlashToken || t === SyntaxKind.SlashEqualsToken; + let currentToken = getNextToken(n, expectedScanAction); + const token = createTextRangeWithKind(scanner.getStartPos(), scanner.getTextPos(), currentToken); + // consume trailing trivia + if (trailingTrivia) { + trailingTrivia = undefined; } - - function readTokenInfo(n: Node): TokenInfo { - Debug.assert(isOnToken()); - - // normally scanner returns the smallest available token - // check the kind of context node to determine if scanner should have more greedy behavior and consume more text. - const expectedScanAction = shouldRescanGreaterThanToken(n) ? ScanAction.RescanGreaterThanToken : - shouldRescanSlashToken(n) ? ScanAction.RescanSlashToken : - shouldRescanTemplateToken(n) ? ScanAction.RescanTemplateToken : - shouldRescanJsxIdentifier(n) ? ScanAction.RescanJsxIdentifier : - shouldRescanJsxText(n) ? ScanAction.RescanJsxText : - shouldRescanJsxAttributeValue(n) ? ScanAction.RescanJsxAttributeValue : - ScanAction.Scan; - - if (lastTokenInfo && expectedScanAction === lastScanAction) { - // readTokenInfo was called before with the same expected scan action. - // No need to re-scan text, return existing 'lastTokenInfo' - // it is ok to call fixTokenKind here since it does not affect - // what portion of text is consumed. In contrast rescanning can change it, - // i.e. for '>=' when originally scanner eats just one character - // and rescanning forces it to consume more. - return fixTokenKind(lastTokenInfo, n); + while (scanner.getStartPos() < endPos) { + currentToken = scanner.scan(); + if (!isTrivia(currentToken)) { + break; } - - if (scanner.getStartPos() !== savedPos) { - Debug.assert(lastTokenInfo !== undefined); - // readTokenInfo was called before but scan action differs - rescan text - scanner.setTextPos(savedPos); - scanner.scan(); + const trivia = createTextRangeWithKind(scanner.getStartPos(), scanner.getTextPos(), currentToken); + if (!trailingTrivia) { + trailingTrivia = []; } - - let currentToken = getNextToken(n, expectedScanAction); - - const token = createTextRangeWithKind( - scanner.getStartPos(), - scanner.getTextPos(), - currentToken, - ); - - // consume trailing trivia - if (trailingTrivia) { - trailingTrivia = undefined; + trailingTrivia.push(trivia); + if (currentToken === SyntaxKind.NewLineTrivia) { + // move past new line + scanner.scan(); + break; } - while (scanner.getStartPos() < endPos) { - currentToken = scanner.scan(); - if (!isTrivia(currentToken)) { - break; + } + lastTokenInfo = { leadingTrivia, trailingTrivia, token }; + return fixTokenKind(lastTokenInfo, n); + } + function getNextToken(n: Node, expectedScanAction: ScanAction): SyntaxKind { + const token = scanner.getToken(); + lastScanAction = ScanAction.Scan; + switch (expectedScanAction) { + case ScanAction.RescanGreaterThanToken: + if (token === SyntaxKind.GreaterThanToken) { + lastScanAction = ScanAction.RescanGreaterThanToken; + const newToken = scanner.reScanGreaterToken(); + Debug.assert(n.kind === newToken); + return newToken; } - const trivia = createTextRangeWithKind( - scanner.getStartPos(), - scanner.getTextPos(), - currentToken, - ); - - if (!trailingTrivia) { - trailingTrivia = []; + break; + case ScanAction.RescanSlashToken: + if (startsWithSlashToken(token)) { + lastScanAction = ScanAction.RescanSlashToken; + const newToken = scanner.reScanSlashToken(); + Debug.assert(n.kind === newToken); + return newToken; } - - trailingTrivia.push(trivia); - - if (currentToken === SyntaxKind.NewLineTrivia) { - // move past new line - scanner.scan(); - break; + break; + case ScanAction.RescanTemplateToken: + if (token === SyntaxKind.CloseBraceToken) { + lastScanAction = ScanAction.RescanTemplateToken; + return scanner.reScanTemplateToken(/* isTaggedTemplate */ false); } - } - - lastTokenInfo = { leadingTrivia, trailingTrivia, token }; - - return fixTokenKind(lastTokenInfo, n); - } - - function getNextToken(n: Node, expectedScanAction: ScanAction): SyntaxKind { - const token = scanner.getToken(); - lastScanAction = ScanAction.Scan; - switch (expectedScanAction) { - case ScanAction.RescanGreaterThanToken: - if (token === SyntaxKind.GreaterThanToken) { - lastScanAction = ScanAction.RescanGreaterThanToken; - const newToken = scanner.reScanGreaterToken(); - Debug.assert(n.kind === newToken); - return newToken; - } - break; - case ScanAction.RescanSlashToken: - if (startsWithSlashToken(token)) { - lastScanAction = ScanAction.RescanSlashToken; - const newToken = scanner.reScanSlashToken(); - Debug.assert(n.kind === newToken); - return newToken; - } - break; - case ScanAction.RescanTemplateToken: - if (token === SyntaxKind.CloseBraceToken) { - lastScanAction = ScanAction.RescanTemplateToken; - return scanner.reScanTemplateToken(/* isTaggedTemplate */ false); - } - break; - case ScanAction.RescanJsxIdentifier: - lastScanAction = ScanAction.RescanJsxIdentifier; - return scanner.scanJsxIdentifier(); - case ScanAction.RescanJsxText: - lastScanAction = ScanAction.RescanJsxText; - return scanner.reScanJsxToken(); - case ScanAction.RescanJsxAttributeValue: - lastScanAction = ScanAction.RescanJsxAttributeValue; - return scanner.reScanJsxAttributeValue(); - case ScanAction.Scan: - break; - default: - Debug.assertNever(expectedScanAction); - } - return token; + break; + case ScanAction.RescanJsxIdentifier: + lastScanAction = ScanAction.RescanJsxIdentifier; + return scanner.scanJsxIdentifier(); + case ScanAction.RescanJsxText: + lastScanAction = ScanAction.RescanJsxText; + return scanner.reScanJsxToken(); + case ScanAction.RescanJsxAttributeValue: + lastScanAction = ScanAction.RescanJsxAttributeValue; + return scanner.reScanJsxAttributeValue(); + case ScanAction.Scan: + break; + default: + Debug.assertNever(expectedScanAction); } - - function readEOFTokenRange(): TextRangeWithKind { - Debug.assert(isOnEOF()); - return createTextRangeWithKind(scanner.getStartPos(), scanner.getTextPos(), SyntaxKind.EndOfFileToken); - } - - function isOnToken(): boolean { - const current = lastTokenInfo ? lastTokenInfo.token.kind : scanner.getToken(); - const startPos = lastTokenInfo ? lastTokenInfo.token.pos : scanner.getStartPos(); - return startPos < endPos && current !== SyntaxKind.EndOfFileToken && !isTrivia(current); - } - - function isOnEOF(): boolean { - const current = lastTokenInfo ? lastTokenInfo.token.kind : scanner.getToken(); - return current === SyntaxKind.EndOfFileToken; - } - - // when containing node in the tree is token - // but its kind differs from the kind that was returned by the scanner, - // then kind needs to be fixed. This might happen in cases - // when parser interprets token differently, i.e keyword treated as identifier - function fixTokenKind(tokenInfo: TokenInfo, container: Node): TokenInfo { - if (isToken(container) && tokenInfo.token.kind !== container.kind) { - tokenInfo.token.kind = container.kind; - } - return tokenInfo; - } - - function skipToEndOf(node: Node): void { - scanner.setTextPos(node.end); - savedPos = scanner.getStartPos(); - lastScanAction = undefined; - lastTokenInfo = undefined; - wasNewLine = false; - leadingTrivia = undefined; - trailingTrivia = undefined; + return token; + } + function readEOFTokenRange(): TextRangeWithKind { + Debug.assert(isOnEOF()); + return createTextRangeWithKind(scanner.getStartPos(), scanner.getTextPos(), SyntaxKind.EndOfFileToken); + } + function isOnToken(): boolean { + const current = lastTokenInfo ? lastTokenInfo.token.kind : scanner.getToken(); + const startPos = lastTokenInfo ? lastTokenInfo.token.pos : scanner.getStartPos(); + return startPos < endPos && current !== SyntaxKind.EndOfFileToken && !isTrivia(current); + } + function isOnEOF(): boolean { + const current = lastTokenInfo ? lastTokenInfo.token.kind : scanner.getToken(); + return current === SyntaxKind.EndOfFileToken; + } + // when containing node in the tree is token + // but its kind differs from the kind that was returned by the scanner, + // then kind needs to be fixed. This might happen in cases + // when parser interprets token differently, i.e keyword treated as identifier + function fixTokenKind(tokenInfo: TokenInfo, container: Node): TokenInfo { + if (isToken(container) && tokenInfo.token.kind !== container.kind) { + tokenInfo.token.kind = container.kind; } + return tokenInfo; + } + function skipToEndOf(node: Node): void { + scanner.setTextPos(node.end); + savedPos = scanner.getStartPos(); + lastScanAction = undefined; + lastTokenInfo = undefined; + wasNewLine = false; + leadingTrivia = undefined; + trailingTrivia = undefined; } } diff --git a/src/services/formatting/rule.ts b/src/services/formatting/rule.ts index ef98461738891..bbf773ae505bf 100644 --- a/src/services/formatting/rule.ts +++ b/src/services/formatting/rule.ts @@ -1,37 +1,37 @@ +import { FormattingContext } from "../ts.formatting"; +import { emptyArray, SyntaxKind } from "../ts"; /* @internal */ -namespace ts.formatting { - export interface Rule { - // Used for debugging to identify each rule based on the property name it's assigned to. - readonly debugName: string; - readonly context: readonly ContextPredicate[]; - readonly action: RuleAction; - readonly flags: RuleFlags; - } - - export type ContextPredicate = (context: FormattingContext) => boolean; - export const anyContext: readonly ContextPredicate[] = emptyArray; - - export const enum RuleAction { - StopProcessingSpaceActions = 1 << 0, - StopProcessingTokenActions = 1 << 1, - InsertSpace = 1 << 2, - InsertNewLine = 1 << 3, - DeleteSpace = 1 << 4, - DeleteToken = 1 << 5, - InsertTrailingSemicolon = 1 << 6, - - StopAction = StopProcessingSpaceActions | StopProcessingTokenActions, - ModifySpaceAction = InsertSpace | InsertNewLine | DeleteSpace, - ModifyTokenAction = DeleteToken | InsertTrailingSemicolon, - } - - export const enum RuleFlags { - None, - CanDeleteNewLines, - } - - export interface TokenRange { - readonly tokens: readonly SyntaxKind[]; - readonly isSpecific: boolean; - } +export interface Rule { + // Used for debugging to identify each rule based on the property name it's assigned to. + readonly debugName: string; + readonly context: readonly ContextPredicate[]; + readonly action: RuleAction; + readonly flags: RuleFlags; +} +/* @internal */ +export type ContextPredicate = (context: FormattingContext) => boolean; +/* @internal */ +export const anyContext: readonly ContextPredicate[] = emptyArray; +/* @internal */ +export const enum RuleAction { + StopProcessingSpaceActions = 1 << 0, + StopProcessingTokenActions = 1 << 1, + InsertSpace = 1 << 2, + InsertNewLine = 1 << 3, + DeleteSpace = 1 << 4, + DeleteToken = 1 << 5, + InsertTrailingSemicolon = 1 << 6, + StopAction = StopProcessingSpaceActions | StopProcessingTokenActions, + ModifySpaceAction = InsertSpace | InsertNewLine | DeleteSpace, + ModifyTokenAction = DeleteToken | InsertTrailingSemicolon +} +/* @internal */ +export const enum RuleFlags { + None, + CanDeleteNewLines +} +/* @internal */ +export interface TokenRange { + readonly tokens: readonly SyntaxKind[]; + readonly isSpecific: boolean; } diff --git a/src/services/formatting/rules.ts b/src/services/formatting/rules.ts index 80fcc6bd9e922..dae9b40abda7d 100644 --- a/src/services/formatting/rules.ts +++ b/src/services/formatting/rules.ts @@ -1,883 +1,781 @@ +import { TokenRange, Rule, anyContext, RuleAction, RuleFlags, ContextPredicate, FormattingContext, FormattingRequestKind, TextRangeWithKind } from "../ts.formatting"; +import { SyntaxKind, typeKeywords, SemicolonPreference, isArray, contains, FormatCodeSettings, BinaryExpression, isFunctionLikeKind, Node, isExpressionNode, YieldExpression, isTrivia, findNextToken, findAncestor, isPropertySignature, isPropertyDeclaration, positionIsASICandidate } from "../ts"; /* @internal */ -namespace ts.formatting { - export interface RuleSpec { - readonly leftTokenRange: TokenRange; - readonly rightTokenRange: TokenRange; - readonly rule: Rule; - } - - export function getAllRules(): RuleSpec[] { - const allTokens: SyntaxKind[] = []; - for (let token = SyntaxKind.FirstToken; token <= SyntaxKind.LastToken; token++) { - if (token !== SyntaxKind.EndOfFileToken) { - allTokens.push(token); - } - } - function anyTokenExcept(...tokens: SyntaxKind[]): TokenRange { - return { tokens: allTokens.filter(t => !tokens.some(t2 => t2 === t)), isSpecific: false }; - } - - const anyToken: TokenRange = { tokens: allTokens, isSpecific: false }; - const anyTokenIncludingMultilineComments = tokenRangeFrom([...allTokens, SyntaxKind.MultiLineCommentTrivia]); - const anyTokenIncludingEOF = tokenRangeFrom([...allTokens, SyntaxKind.EndOfFileToken]); - const keywords = tokenRangeFromRange(SyntaxKind.FirstKeyword, SyntaxKind.LastKeyword); - const binaryOperators = tokenRangeFromRange(SyntaxKind.FirstBinaryOperator, SyntaxKind.LastBinaryOperator); - const binaryKeywordOperators = [SyntaxKind.InKeyword, SyntaxKind.InstanceOfKeyword, SyntaxKind.OfKeyword, SyntaxKind.AsKeyword, SyntaxKind.IsKeyword]; - const unaryPrefixOperators = [SyntaxKind.PlusPlusToken, SyntaxKind.MinusMinusToken, SyntaxKind.TildeToken, SyntaxKind.ExclamationToken]; - const unaryPrefixExpressions = [ - SyntaxKind.NumericLiteral, SyntaxKind.BigIntLiteral, SyntaxKind.Identifier, SyntaxKind.OpenParenToken, - SyntaxKind.OpenBracketToken, SyntaxKind.OpenBraceToken, SyntaxKind.ThisKeyword, SyntaxKind.NewKeyword]; - const unaryPreincrementExpressions = [SyntaxKind.Identifier, SyntaxKind.OpenParenToken, SyntaxKind.ThisKeyword, SyntaxKind.NewKeyword]; - const unaryPostincrementExpressions = [SyntaxKind.Identifier, SyntaxKind.CloseParenToken, SyntaxKind.CloseBracketToken, SyntaxKind.NewKeyword]; - const unaryPredecrementExpressions = [SyntaxKind.Identifier, SyntaxKind.OpenParenToken, SyntaxKind.ThisKeyword, SyntaxKind.NewKeyword]; - const unaryPostdecrementExpressions = [SyntaxKind.Identifier, SyntaxKind.CloseParenToken, SyntaxKind.CloseBracketToken, SyntaxKind.NewKeyword]; - const comments = [SyntaxKind.SingleLineCommentTrivia, SyntaxKind.MultiLineCommentTrivia]; - const typeNames = [SyntaxKind.Identifier, ...typeKeywords]; - - // Place a space before open brace in a function declaration - // TypeScript: Function can have return types, which can be made of tons of different token kinds - const functionOpenBraceLeftTokenRange = anyTokenIncludingMultilineComments; - - // Place a space before open brace in a TypeScript declaration that has braces as children (class, module, enum, etc) - const typeScriptOpenBraceLeftTokenRange = tokenRangeFrom([SyntaxKind.Identifier, SyntaxKind.MultiLineCommentTrivia, SyntaxKind.ClassKeyword, SyntaxKind.ExportKeyword, SyntaxKind.ImportKeyword]); - - // Place a space before open brace in a control flow construct - const controlOpenBraceLeftTokenRange = tokenRangeFrom([SyntaxKind.CloseParenToken, SyntaxKind.MultiLineCommentTrivia, SyntaxKind.DoKeyword, SyntaxKind.TryKeyword, SyntaxKind.FinallyKeyword, SyntaxKind.ElseKeyword]); - - // These rules are higher in priority than user-configurable - const highPriorityCommonRules = [ - // Leave comments alone - rule("IgnoreBeforeComment", anyToken, comments, anyContext, RuleAction.StopProcessingSpaceActions), - rule("IgnoreAfterLineComment", SyntaxKind.SingleLineCommentTrivia, anyToken, anyContext, RuleAction.StopProcessingSpaceActions), - - rule("NotSpaceBeforeColon", anyToken, SyntaxKind.ColonToken, [isNonJsxSameLineTokenContext, isNotBinaryOpContext, isNotTypeAnnotationContext], RuleAction.DeleteSpace), - rule("SpaceAfterColon", SyntaxKind.ColonToken, anyToken, [isNonJsxSameLineTokenContext, isNotBinaryOpContext], RuleAction.InsertSpace), - rule("NoSpaceBeforeQuestionMark", anyToken, SyntaxKind.QuestionToken, [isNonJsxSameLineTokenContext, isNotBinaryOpContext], RuleAction.DeleteSpace), - // insert space after '?' only when it is used in conditional operator - rule("SpaceAfterQuestionMarkInConditionalOperator", SyntaxKind.QuestionToken, anyToken, [isNonJsxSameLineTokenContext, isConditionalOperatorContext], RuleAction.InsertSpace), - - // in other cases there should be no space between '?' and next token - rule("NoSpaceAfterQuestionMark", SyntaxKind.QuestionToken, anyToken, [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), - - rule("NoSpaceBeforeDot", anyToken, [SyntaxKind.DotToken, SyntaxKind.QuestionDotToken], [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), - rule("NoSpaceAfterDot", [SyntaxKind.DotToken, SyntaxKind.QuestionDotToken], anyToken, [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), - - rule("NoSpaceBetweenImportParenInImportType", SyntaxKind.ImportKeyword, SyntaxKind.OpenParenToken, [isNonJsxSameLineTokenContext, isImportTypeContext], RuleAction.DeleteSpace), - - // Special handling of unary operators. - // Prefix operators generally shouldn't have a space between - // them and their target unary expression. - rule("NoSpaceAfterUnaryPrefixOperator", unaryPrefixOperators, unaryPrefixExpressions, [isNonJsxSameLineTokenContext, isNotBinaryOpContext], RuleAction.DeleteSpace), - rule("NoSpaceAfterUnaryPreincrementOperator", SyntaxKind.PlusPlusToken, unaryPreincrementExpressions, [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), - rule("NoSpaceAfterUnaryPredecrementOperator", SyntaxKind.MinusMinusToken, unaryPredecrementExpressions, [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), - rule("NoSpaceBeforeUnaryPostincrementOperator", unaryPostincrementExpressions, SyntaxKind.PlusPlusToken, [isNonJsxSameLineTokenContext, isNotStatementConditionContext], RuleAction.DeleteSpace), - rule("NoSpaceBeforeUnaryPostdecrementOperator", unaryPostdecrementExpressions, SyntaxKind.MinusMinusToken, [isNonJsxSameLineTokenContext, isNotStatementConditionContext], RuleAction.DeleteSpace), - - // More unary operator special-casing. - // DevDiv 181814: Be careful when removing leading whitespace - // around unary operators. Examples: - // 1 - -2 --X--> 1--2 - // a + ++b --X--> a+++b - rule("SpaceAfterPostincrementWhenFollowedByAdd", SyntaxKind.PlusPlusToken, SyntaxKind.PlusToken, [isNonJsxSameLineTokenContext, isBinaryOpContext], RuleAction.InsertSpace), - rule("SpaceAfterAddWhenFollowedByUnaryPlus", SyntaxKind.PlusToken, SyntaxKind.PlusToken, [isNonJsxSameLineTokenContext, isBinaryOpContext], RuleAction.InsertSpace), - rule("SpaceAfterAddWhenFollowedByPreincrement", SyntaxKind.PlusToken, SyntaxKind.PlusPlusToken, [isNonJsxSameLineTokenContext, isBinaryOpContext], RuleAction.InsertSpace), - rule("SpaceAfterPostdecrementWhenFollowedBySubtract", SyntaxKind.MinusMinusToken, SyntaxKind.MinusToken, [isNonJsxSameLineTokenContext, isBinaryOpContext], RuleAction.InsertSpace), - rule("SpaceAfterSubtractWhenFollowedByUnaryMinus", SyntaxKind.MinusToken, SyntaxKind.MinusToken, [isNonJsxSameLineTokenContext, isBinaryOpContext], RuleAction.InsertSpace), - rule("SpaceAfterSubtractWhenFollowedByPredecrement", SyntaxKind.MinusToken, SyntaxKind.MinusMinusToken, [isNonJsxSameLineTokenContext, isBinaryOpContext], RuleAction.InsertSpace), - - rule("NoSpaceAfterCloseBrace", SyntaxKind.CloseBraceToken, [SyntaxKind.CommaToken, SyntaxKind.SemicolonToken], [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), - // For functions and control block place } on a new line [multi-line rule] - rule("NewLineBeforeCloseBraceInBlockContext", anyTokenIncludingMultilineComments, SyntaxKind.CloseBraceToken, [isMultilineBlockContext], RuleAction.InsertNewLine), - - // Space/new line after }. - rule("SpaceAfterCloseBrace", SyntaxKind.CloseBraceToken, anyTokenExcept(SyntaxKind.CloseParenToken), [isNonJsxSameLineTokenContext, isAfterCodeBlockContext], RuleAction.InsertSpace), - // Special case for (}, else) and (}, while) since else & while tokens are not part of the tree which makes SpaceAfterCloseBrace rule not applied - // Also should not apply to }) - rule("SpaceBetweenCloseBraceAndElse", SyntaxKind.CloseBraceToken, SyntaxKind.ElseKeyword, [isNonJsxSameLineTokenContext], RuleAction.InsertSpace), - rule("SpaceBetweenCloseBraceAndWhile", SyntaxKind.CloseBraceToken, SyntaxKind.WhileKeyword, [isNonJsxSameLineTokenContext], RuleAction.InsertSpace), - rule("NoSpaceBetweenEmptyBraceBrackets", SyntaxKind.OpenBraceToken, SyntaxKind.CloseBraceToken, [isNonJsxSameLineTokenContext, isObjectContext], RuleAction.DeleteSpace), - - // Add a space after control dec context if the next character is an open bracket ex: 'if (false)[a, b] = [1, 2];' -> 'if (false) [a, b] = [1, 2];' - rule("SpaceAfterConditionalClosingParen", SyntaxKind.CloseParenToken, SyntaxKind.OpenBracketToken, [isControlDeclContext], RuleAction.InsertSpace), - - rule("NoSpaceBetweenFunctionKeywordAndStar", SyntaxKind.FunctionKeyword, SyntaxKind.AsteriskToken, [isFunctionDeclarationOrFunctionExpressionContext], RuleAction.DeleteSpace), - rule("SpaceAfterStarInGeneratorDeclaration", SyntaxKind.AsteriskToken, SyntaxKind.Identifier, [isFunctionDeclarationOrFunctionExpressionContext], RuleAction.InsertSpace), - - rule("SpaceAfterFunctionInFuncDecl", SyntaxKind.FunctionKeyword, anyToken, [isFunctionDeclContext], RuleAction.InsertSpace), - // Insert new line after { and before } in multi-line contexts. - rule("NewLineAfterOpenBraceInBlockContext", SyntaxKind.OpenBraceToken, anyToken, [isMultilineBlockContext], RuleAction.InsertNewLine), - - // For get/set members, we check for (identifier,identifier) since get/set don't have tokens and they are represented as just an identifier token. - // Though, we do extra check on the context to make sure we are dealing with get/set node. Example: - // get x() {} - // set x(val) {} - rule("SpaceAfterGetSetInMember", [SyntaxKind.GetKeyword, SyntaxKind.SetKeyword], SyntaxKind.Identifier, [isFunctionDeclContext], RuleAction.InsertSpace), - - rule("NoSpaceBetweenYieldKeywordAndStar", SyntaxKind.YieldKeyword, SyntaxKind.AsteriskToken, [isNonJsxSameLineTokenContext, isYieldOrYieldStarWithOperand], RuleAction.DeleteSpace), - rule("SpaceBetweenYieldOrYieldStarAndOperand", [SyntaxKind.YieldKeyword, SyntaxKind.AsteriskToken], anyToken, [isNonJsxSameLineTokenContext, isYieldOrYieldStarWithOperand], RuleAction.InsertSpace), - - rule("NoSpaceBetweenReturnAndSemicolon", SyntaxKind.ReturnKeyword, SyntaxKind.SemicolonToken, [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), - rule("SpaceAfterCertainKeywords", [SyntaxKind.VarKeyword, SyntaxKind.ThrowKeyword, SyntaxKind.NewKeyword, SyntaxKind.DeleteKeyword, SyntaxKind.ReturnKeyword, SyntaxKind.TypeOfKeyword, SyntaxKind.AwaitKeyword], anyToken, [isNonJsxSameLineTokenContext], RuleAction.InsertSpace), - rule("SpaceAfterLetConstInVariableDeclaration", [SyntaxKind.LetKeyword, SyntaxKind.ConstKeyword], anyToken, [isNonJsxSameLineTokenContext, isStartOfVariableDeclarationList], RuleAction.InsertSpace), - rule("NoSpaceBeforeOpenParenInFuncCall", anyToken, SyntaxKind.OpenParenToken, [isNonJsxSameLineTokenContext, isFunctionCallOrNewContext, isPreviousTokenNotComma], RuleAction.DeleteSpace), - - // Special case for binary operators (that are keywords). For these we have to add a space and shouldn't follow any user options. - rule("SpaceBeforeBinaryKeywordOperator", anyToken, binaryKeywordOperators, [isNonJsxSameLineTokenContext, isBinaryOpContext], RuleAction.InsertSpace), - rule("SpaceAfterBinaryKeywordOperator", binaryKeywordOperators, anyToken, [isNonJsxSameLineTokenContext, isBinaryOpContext], RuleAction.InsertSpace), - - rule("SpaceAfterVoidOperator", SyntaxKind.VoidKeyword, anyToken, [isNonJsxSameLineTokenContext, isVoidOpContext], RuleAction.InsertSpace), - - // Async-await - rule("SpaceBetweenAsyncAndOpenParen", SyntaxKind.AsyncKeyword, SyntaxKind.OpenParenToken, [isArrowFunctionContext, isNonJsxSameLineTokenContext], RuleAction.InsertSpace), - rule("SpaceBetweenAsyncAndFunctionKeyword", SyntaxKind.AsyncKeyword, SyntaxKind.FunctionKeyword, [isNonJsxSameLineTokenContext], RuleAction.InsertSpace), - - // Template string - rule("NoSpaceBetweenTagAndTemplateString", [SyntaxKind.Identifier, SyntaxKind.CloseParenToken], [SyntaxKind.NoSubstitutionTemplateLiteral, SyntaxKind.TemplateHead], [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), - - // JSX opening elements - rule("SpaceBeforeJsxAttribute", anyToken, SyntaxKind.Identifier, [isNextTokenParentJsxAttribute, isNonJsxSameLineTokenContext], RuleAction.InsertSpace), - rule("SpaceBeforeSlashInJsxOpeningElement", anyToken, SyntaxKind.SlashToken, [isJsxSelfClosingElementContext, isNonJsxSameLineTokenContext], RuleAction.InsertSpace), - rule("NoSpaceBeforeGreaterThanTokenInJsxOpeningElement", SyntaxKind.SlashToken, SyntaxKind.GreaterThanToken, [isJsxSelfClosingElementContext, isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), - rule("NoSpaceBeforeEqualInJsxAttribute", anyToken, SyntaxKind.EqualsToken, [isJsxAttributeContext, isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), - rule("NoSpaceAfterEqualInJsxAttribute", SyntaxKind.EqualsToken, anyToken, [isJsxAttributeContext, isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), - - // TypeScript-specific rules - // Use of module as a function call. e.g.: import m2 = module("m2"); - rule("NoSpaceAfterModuleImport", [SyntaxKind.ModuleKeyword, SyntaxKind.RequireKeyword], SyntaxKind.OpenParenToken, [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), - // Add a space around certain TypeScript keywords - rule( - "SpaceAfterCertainTypeScriptKeywords", - [ - SyntaxKind.AbstractKeyword, - SyntaxKind.ClassKeyword, - SyntaxKind.DeclareKeyword, - SyntaxKind.DefaultKeyword, - SyntaxKind.EnumKeyword, - SyntaxKind.ExportKeyword, - SyntaxKind.ExtendsKeyword, - SyntaxKind.GetKeyword, - SyntaxKind.ImplementsKeyword, - SyntaxKind.ImportKeyword, - SyntaxKind.InterfaceKeyword, - SyntaxKind.ModuleKeyword, - SyntaxKind.NamespaceKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.ReadonlyKeyword, - SyntaxKind.SetKeyword, - SyntaxKind.StaticKeyword, - SyntaxKind.TypeKeyword, - SyntaxKind.FromKeyword, - SyntaxKind.KeyOfKeyword, - SyntaxKind.InferKeyword, - ], - anyToken, - [isNonJsxSameLineTokenContext], - RuleAction.InsertSpace), - rule( - "SpaceBeforeCertainTypeScriptKeywords", - anyToken, - [SyntaxKind.ExtendsKeyword, SyntaxKind.ImplementsKeyword, SyntaxKind.FromKeyword], - [isNonJsxSameLineTokenContext], - RuleAction.InsertSpace), - // Treat string literals in module names as identifiers, and add a space between the literal and the opening Brace braces, e.g.: module "m2" { - rule("SpaceAfterModuleName", SyntaxKind.StringLiteral, SyntaxKind.OpenBraceToken, [isModuleDeclContext], RuleAction.InsertSpace), - - // Lambda expressions - rule("SpaceBeforeArrow", anyToken, SyntaxKind.EqualsGreaterThanToken, [isNonJsxSameLineTokenContext], RuleAction.InsertSpace), - rule("SpaceAfterArrow", SyntaxKind.EqualsGreaterThanToken, anyToken, [isNonJsxSameLineTokenContext], RuleAction.InsertSpace), - - // Optional parameters and let args - rule("NoSpaceAfterEllipsis", SyntaxKind.DotDotDotToken, SyntaxKind.Identifier, [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), - rule("NoSpaceAfterOptionalParameters", SyntaxKind.QuestionToken, [SyntaxKind.CloseParenToken, SyntaxKind.CommaToken], [isNonJsxSameLineTokenContext, isNotBinaryOpContext], RuleAction.DeleteSpace), - - // Remove spaces in empty interface literals. e.g.: x: {} - rule("NoSpaceBetweenEmptyInterfaceBraceBrackets", SyntaxKind.OpenBraceToken, SyntaxKind.CloseBraceToken, [isNonJsxSameLineTokenContext, isObjectTypeContext], RuleAction.DeleteSpace), - - // generics and type assertions - rule("NoSpaceBeforeOpenAngularBracket", typeNames, SyntaxKind.LessThanToken, [isNonJsxSameLineTokenContext, isTypeArgumentOrParameterOrAssertionContext], RuleAction.DeleteSpace), - rule("NoSpaceBetweenCloseParenAndAngularBracket", SyntaxKind.CloseParenToken, SyntaxKind.LessThanToken, [isNonJsxSameLineTokenContext, isTypeArgumentOrParameterOrAssertionContext], RuleAction.DeleteSpace), - rule("NoSpaceAfterOpenAngularBracket", SyntaxKind.LessThanToken, anyToken, [isNonJsxSameLineTokenContext, isTypeArgumentOrParameterOrAssertionContext], RuleAction.DeleteSpace), - rule("NoSpaceBeforeCloseAngularBracket", anyToken, SyntaxKind.GreaterThanToken, [isNonJsxSameLineTokenContext, isTypeArgumentOrParameterOrAssertionContext], RuleAction.DeleteSpace), - rule("NoSpaceAfterCloseAngularBracket", - SyntaxKind.GreaterThanToken, - [SyntaxKind.OpenParenToken, SyntaxKind.OpenBracketToken, SyntaxKind.GreaterThanToken, SyntaxKind.CommaToken], - [isNonJsxSameLineTokenContext, isTypeArgumentOrParameterOrAssertionContext, isNotFunctionDeclContext /*To prevent an interference with the SpaceBeforeOpenParenInFuncDecl rule*/], - RuleAction.DeleteSpace), - - // decorators - rule("SpaceBeforeAt", [SyntaxKind.CloseParenToken, SyntaxKind.Identifier], SyntaxKind.AtToken, [isNonJsxSameLineTokenContext], RuleAction.InsertSpace), - rule("NoSpaceAfterAt", SyntaxKind.AtToken, anyToken, [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), - // Insert space after @ in decorator - rule("SpaceAfterDecorator", - anyToken, - [ - SyntaxKind.AbstractKeyword, - SyntaxKind.Identifier, - SyntaxKind.ExportKeyword, - SyntaxKind.DefaultKeyword, - SyntaxKind.ClassKeyword, - SyntaxKind.StaticKeyword, - SyntaxKind.PublicKeyword, - SyntaxKind.PrivateKeyword, - SyntaxKind.ProtectedKeyword, - SyntaxKind.GetKeyword, - SyntaxKind.SetKeyword, - SyntaxKind.OpenBracketToken, - SyntaxKind.AsteriskToken, - ], - [isEndOfDecoratorContextOnSameLine], - RuleAction.InsertSpace), - - rule("NoSpaceBeforeNonNullAssertionOperator", anyToken, SyntaxKind.ExclamationToken, [isNonJsxSameLineTokenContext, isNonNullAssertionContext], RuleAction.DeleteSpace), - rule("NoSpaceAfterNewKeywordOnConstructorSignature", SyntaxKind.NewKeyword, SyntaxKind.OpenParenToken, [isNonJsxSameLineTokenContext, isConstructorSignatureContext], RuleAction.DeleteSpace), - rule("SpaceLessThanAndNonJSXTypeAnnotation", SyntaxKind.LessThanToken, SyntaxKind.LessThanToken, [isNonJsxSameLineTokenContext], RuleAction.InsertSpace), - ]; - - // These rules are applied after high priority - const userConfigurableRules = [ - // Treat constructor as an identifier in a function declaration, and remove spaces between constructor and following left parentheses - rule("SpaceAfterConstructor", SyntaxKind.ConstructorKeyword, SyntaxKind.OpenParenToken, [isOptionEnabled("insertSpaceAfterConstructor"), isNonJsxSameLineTokenContext], RuleAction.InsertSpace), - rule("NoSpaceAfterConstructor", SyntaxKind.ConstructorKeyword, SyntaxKind.OpenParenToken, [isOptionDisabledOrUndefined("insertSpaceAfterConstructor"), isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), - - rule("SpaceAfterComma", SyntaxKind.CommaToken, anyToken, [isOptionEnabled("insertSpaceAfterCommaDelimiter"), isNonJsxSameLineTokenContext, isNonJsxElementOrFragmentContext, isNextTokenNotCloseBracket, isNextTokenNotCloseParen], RuleAction.InsertSpace), - rule("NoSpaceAfterComma", SyntaxKind.CommaToken, anyToken, [isOptionDisabledOrUndefined("insertSpaceAfterCommaDelimiter"), isNonJsxSameLineTokenContext, isNonJsxElementOrFragmentContext], RuleAction.DeleteSpace), - - // Insert space after function keyword for anonymous functions - rule("SpaceAfterAnonymousFunctionKeyword", [SyntaxKind.FunctionKeyword, SyntaxKind.AsteriskToken], SyntaxKind.OpenParenToken, [isOptionEnabled("insertSpaceAfterFunctionKeywordForAnonymousFunctions"), isFunctionDeclContext], RuleAction.InsertSpace), - rule("NoSpaceAfterAnonymousFunctionKeyword", [SyntaxKind.FunctionKeyword, SyntaxKind.AsteriskToken], SyntaxKind.OpenParenToken, [isOptionDisabledOrUndefined("insertSpaceAfterFunctionKeywordForAnonymousFunctions"), isFunctionDeclContext], RuleAction.DeleteSpace), - - // Insert space after keywords in control flow statements - rule("SpaceAfterKeywordInControl", keywords, SyntaxKind.OpenParenToken, [isOptionEnabled("insertSpaceAfterKeywordsInControlFlowStatements"), isControlDeclContext], RuleAction.InsertSpace), - rule("NoSpaceAfterKeywordInControl", keywords, SyntaxKind.OpenParenToken, [isOptionDisabledOrUndefined("insertSpaceAfterKeywordsInControlFlowStatements"), isControlDeclContext], RuleAction.DeleteSpace), - - // Insert space after opening and before closing nonempty parenthesis - rule("SpaceAfterOpenParen", SyntaxKind.OpenParenToken, anyToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis"), isNonJsxSameLineTokenContext], RuleAction.InsertSpace), - rule("SpaceBeforeCloseParen", anyToken, SyntaxKind.CloseParenToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis"), isNonJsxSameLineTokenContext], RuleAction.InsertSpace), - rule("SpaceBetweenOpenParens", SyntaxKind.OpenParenToken, SyntaxKind.OpenParenToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis"), isNonJsxSameLineTokenContext], RuleAction.InsertSpace), - rule("NoSpaceBetweenParens", SyntaxKind.OpenParenToken, SyntaxKind.CloseParenToken, [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), - rule("NoSpaceAfterOpenParen", SyntaxKind.OpenParenToken, anyToken, [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis"), isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), - rule("NoSpaceBeforeCloseParen", anyToken, SyntaxKind.CloseParenToken, [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis"), isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), - - // Insert space after opening and before closing nonempty brackets - rule("SpaceAfterOpenBracket", SyntaxKind.OpenBracketToken, anyToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets"), isNonJsxSameLineTokenContext], RuleAction.InsertSpace), - rule("SpaceBeforeCloseBracket", anyToken, SyntaxKind.CloseBracketToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets"), isNonJsxSameLineTokenContext], RuleAction.InsertSpace), - rule("NoSpaceBetweenBrackets", SyntaxKind.OpenBracketToken, SyntaxKind.CloseBracketToken, [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), - rule("NoSpaceAfterOpenBracket", SyntaxKind.OpenBracketToken, anyToken, [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets"), isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), - rule("NoSpaceBeforeCloseBracket", anyToken, SyntaxKind.CloseBracketToken, [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets"), isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), - - // Insert a space after { and before } in single-line contexts, but remove space from empty object literals {}. - rule("SpaceAfterOpenBrace", SyntaxKind.OpenBraceToken, anyToken, [isOptionEnabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces"), isBraceWrappedContext], RuleAction.InsertSpace), - rule("SpaceBeforeCloseBrace", anyToken, SyntaxKind.CloseBraceToken, [isOptionEnabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces"), isBraceWrappedContext], RuleAction.InsertSpace), - rule("NoSpaceBetweenEmptyBraceBrackets", SyntaxKind.OpenBraceToken, SyntaxKind.CloseBraceToken, [isNonJsxSameLineTokenContext, isObjectContext], RuleAction.DeleteSpace), - rule("NoSpaceAfterOpenBrace", SyntaxKind.OpenBraceToken, anyToken, [isOptionDisabled("insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces"), isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), - rule("NoSpaceBeforeCloseBrace", anyToken, SyntaxKind.CloseBraceToken, [isOptionDisabled("insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces"), isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), - - // Insert space after opening and before closing template string braces - rule("SpaceAfterTemplateHeadAndMiddle", [SyntaxKind.TemplateHead, SyntaxKind.TemplateMiddle], anyToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces"), isNonJsxSameLineTokenContext], RuleAction.InsertSpace), - rule("SpaceBeforeTemplateMiddleAndTail", anyToken, [SyntaxKind.TemplateMiddle, SyntaxKind.TemplateTail], [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces"), isNonJsxSameLineTokenContext], RuleAction.InsertSpace), - rule("NoSpaceAfterTemplateHeadAndMiddle", [SyntaxKind.TemplateHead, SyntaxKind.TemplateMiddle], anyToken, [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces"), isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), - rule("NoSpaceBeforeTemplateMiddleAndTail", anyToken, [SyntaxKind.TemplateMiddle, SyntaxKind.TemplateTail], [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces"), isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), - - // No space after { and before } in JSX expression - rule("SpaceAfterOpenBraceInJsxExpression", SyntaxKind.OpenBraceToken, anyToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces"), isNonJsxSameLineTokenContext, isJsxExpressionContext], RuleAction.InsertSpace), - rule("SpaceBeforeCloseBraceInJsxExpression", anyToken, SyntaxKind.CloseBraceToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces"), isNonJsxSameLineTokenContext, isJsxExpressionContext], RuleAction.InsertSpace), - rule("NoSpaceAfterOpenBraceInJsxExpression", SyntaxKind.OpenBraceToken, anyToken, [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces"), isNonJsxSameLineTokenContext, isJsxExpressionContext], RuleAction.DeleteSpace), - rule("NoSpaceBeforeCloseBraceInJsxExpression", anyToken, SyntaxKind.CloseBraceToken, [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces"), isNonJsxSameLineTokenContext, isJsxExpressionContext], RuleAction.DeleteSpace), - - // Insert space after semicolon in for statement - rule("SpaceAfterSemicolonInFor", SyntaxKind.SemicolonToken, anyToken, [isOptionEnabled("insertSpaceAfterSemicolonInForStatements"), isNonJsxSameLineTokenContext, isForContext], RuleAction.InsertSpace), - rule("NoSpaceAfterSemicolonInFor", SyntaxKind.SemicolonToken, anyToken, [isOptionDisabledOrUndefined("insertSpaceAfterSemicolonInForStatements"), isNonJsxSameLineTokenContext, isForContext], RuleAction.DeleteSpace), - - // Insert space before and after binary operators - rule("SpaceBeforeBinaryOperator", anyToken, binaryOperators, [isOptionEnabled("insertSpaceBeforeAndAfterBinaryOperators"), isNonJsxSameLineTokenContext, isBinaryOpContext], RuleAction.InsertSpace), - rule("SpaceAfterBinaryOperator", binaryOperators, anyToken, [isOptionEnabled("insertSpaceBeforeAndAfterBinaryOperators"), isNonJsxSameLineTokenContext, isBinaryOpContext], RuleAction.InsertSpace), - rule("NoSpaceBeforeBinaryOperator", anyToken, binaryOperators, [isOptionDisabledOrUndefined("insertSpaceBeforeAndAfterBinaryOperators"), isNonJsxSameLineTokenContext, isBinaryOpContext], RuleAction.DeleteSpace), - rule("NoSpaceAfterBinaryOperator", binaryOperators, anyToken, [isOptionDisabledOrUndefined("insertSpaceBeforeAndAfterBinaryOperators"), isNonJsxSameLineTokenContext, isBinaryOpContext], RuleAction.DeleteSpace), - - rule("SpaceBeforeOpenParenInFuncDecl", anyToken, SyntaxKind.OpenParenToken, [isOptionEnabled("insertSpaceBeforeFunctionParenthesis"), isNonJsxSameLineTokenContext, isFunctionDeclContext], RuleAction.InsertSpace), - rule("NoSpaceBeforeOpenParenInFuncDecl", anyToken, SyntaxKind.OpenParenToken, [isOptionDisabledOrUndefined("insertSpaceBeforeFunctionParenthesis"), isNonJsxSameLineTokenContext, isFunctionDeclContext], RuleAction.DeleteSpace), - - // Open Brace braces after control block - rule("NewLineBeforeOpenBraceInControl", controlOpenBraceLeftTokenRange, SyntaxKind.OpenBraceToken, [isOptionEnabled("placeOpenBraceOnNewLineForControlBlocks"), isControlDeclContext, isBeforeMultilineBlockContext], RuleAction.InsertNewLine, RuleFlags.CanDeleteNewLines), - - // Open Brace braces after function - // TypeScript: Function can have return types, which can be made of tons of different token kinds - rule("NewLineBeforeOpenBraceInFunction", functionOpenBraceLeftTokenRange, SyntaxKind.OpenBraceToken, [isOptionEnabled("placeOpenBraceOnNewLineForFunctions"), isFunctionDeclContext, isBeforeMultilineBlockContext], RuleAction.InsertNewLine, RuleFlags.CanDeleteNewLines), - // Open Brace braces after TypeScript module/class/interface - rule("NewLineBeforeOpenBraceInTypeScriptDeclWithBlock", typeScriptOpenBraceLeftTokenRange, SyntaxKind.OpenBraceToken, [isOptionEnabled("placeOpenBraceOnNewLineForFunctions"), isTypeScriptDeclWithBlockContext, isBeforeMultilineBlockContext], RuleAction.InsertNewLine, RuleFlags.CanDeleteNewLines), - - rule("SpaceAfterTypeAssertion", SyntaxKind.GreaterThanToken, anyToken, [isOptionEnabled("insertSpaceAfterTypeAssertion"), isNonJsxSameLineTokenContext, isTypeAssertionContext], RuleAction.InsertSpace), - rule("NoSpaceAfterTypeAssertion", SyntaxKind.GreaterThanToken, anyToken, [isOptionDisabledOrUndefined("insertSpaceAfterTypeAssertion"), isNonJsxSameLineTokenContext, isTypeAssertionContext], RuleAction.DeleteSpace), - - rule("SpaceBeforeTypeAnnotation", anyToken, SyntaxKind.ColonToken, [isOptionEnabled("insertSpaceBeforeTypeAnnotation"), isNonJsxSameLineTokenContext, isTypeAnnotationContext], RuleAction.InsertSpace), - rule("NoSpaceBeforeTypeAnnotation", anyToken, SyntaxKind.ColonToken, [isOptionDisabledOrUndefined("insertSpaceBeforeTypeAnnotation"), isNonJsxSameLineTokenContext, isTypeAnnotationContext], RuleAction.DeleteSpace), - rule("NoOptionalSemicolon", SyntaxKind.SemicolonToken, anyTokenIncludingEOF, [optionEquals("semicolons", SemicolonPreference.Remove), isSemicolonDeletionContext], RuleAction.DeleteToken), - rule("OptionalSemicolon", anyToken, anyTokenIncludingEOF, [optionEquals("semicolons", SemicolonPreference.Insert), isSemicolonInsertionContext], RuleAction.InsertTrailingSemicolon), - ]; - - // These rules are lower in priority than user-configurable. Rules earlier in this list have priority over rules later in the list. - const lowPriorityCommonRules = [ - // Space after keyword but not before ; or : or ? - rule("NoSpaceBeforeSemicolon", anyToken, SyntaxKind.SemicolonToken, [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), - - rule("SpaceBeforeOpenBraceInControl", controlOpenBraceLeftTokenRange, SyntaxKind.OpenBraceToken, [isOptionDisabledOrUndefinedOrTokensOnSameLine("placeOpenBraceOnNewLineForControlBlocks"), isControlDeclContext, isNotFormatOnEnter, isSameLineTokenOrBeforeBlockContext], RuleAction.InsertSpace, RuleFlags.CanDeleteNewLines), - rule("SpaceBeforeOpenBraceInFunction", functionOpenBraceLeftTokenRange, SyntaxKind.OpenBraceToken, [isOptionDisabledOrUndefinedOrTokensOnSameLine("placeOpenBraceOnNewLineForFunctions"), isFunctionDeclContext, isBeforeBlockContext, isNotFormatOnEnter, isSameLineTokenOrBeforeBlockContext], RuleAction.InsertSpace, RuleFlags.CanDeleteNewLines), - rule("SpaceBeforeOpenBraceInTypeScriptDeclWithBlock", typeScriptOpenBraceLeftTokenRange, SyntaxKind.OpenBraceToken, [isOptionDisabledOrUndefinedOrTokensOnSameLine("placeOpenBraceOnNewLineForFunctions"), isTypeScriptDeclWithBlockContext, isNotFormatOnEnter, isSameLineTokenOrBeforeBlockContext], RuleAction.InsertSpace, RuleFlags.CanDeleteNewLines), - - rule("NoSpaceBeforeComma", anyToken, SyntaxKind.CommaToken, [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), - - // No space before and after indexer `x[]` - rule("NoSpaceBeforeOpenBracket", anyTokenExcept(SyntaxKind.AsyncKeyword, SyntaxKind.CaseKeyword), SyntaxKind.OpenBracketToken, [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), - rule("NoSpaceAfterCloseBracket", SyntaxKind.CloseBracketToken, anyToken, [isNonJsxSameLineTokenContext, isNotBeforeBlockInFunctionDeclarationContext], RuleAction.DeleteSpace), - rule("SpaceAfterSemicolon", SyntaxKind.SemicolonToken, anyToken, [isNonJsxSameLineTokenContext], RuleAction.InsertSpace), - - // Remove extra space between for and await - rule("SpaceBetweenForAndAwaitKeyword", SyntaxKind.ForKeyword, SyntaxKind.AwaitKeyword, [isNonJsxSameLineTokenContext], RuleAction.InsertSpace), - - // Add a space between statements. All keywords except (do,else,case) has open/close parens after them. - // So, we have a rule to add a space for [),Any], [do,Any], [else,Any], and [case,Any] - rule( - "SpaceBetweenStatements", - [SyntaxKind.CloseParenToken, SyntaxKind.DoKeyword, SyntaxKind.ElseKeyword, SyntaxKind.CaseKeyword], - anyToken, - [isNonJsxSameLineTokenContext, isNonJsxElementOrFragmentContext, isNotForContext], - RuleAction.InsertSpace), - // This low-pri rule takes care of "try {" and "finally {" in case the rule SpaceBeforeOpenBraceInControl didn't execute on FormatOnEnter. - rule("SpaceAfterTryFinally", [SyntaxKind.TryKeyword, SyntaxKind.FinallyKeyword], SyntaxKind.OpenBraceToken, [isNonJsxSameLineTokenContext], RuleAction.InsertSpace), - ]; - - return [ - ...highPriorityCommonRules, - ...userConfigurableRules, - ...lowPriorityCommonRules, - ]; - } - - /** - * A rule takes a two tokens (left/right) and a particular context - * for which you're meant to look at them. You then declare what should the - * whitespace annotation be between these tokens via the action param. - * - * @param debugName Name to print - * @param left The left side of the comparison - * @param right The right side of the comparison - * @param context A set of filters to narrow down the space in which this formatter rule applies - * @param action a declaration of the expected whitespace - * @param flags whether the rule deletes a line or not, defaults to no-op - */ - function rule( - debugName: string, - left: SyntaxKind | readonly SyntaxKind[] | TokenRange, - right: SyntaxKind | readonly SyntaxKind[] | TokenRange, - context: readonly ContextPredicate[], - action: RuleAction, - flags: RuleFlags = RuleFlags.None, - ): RuleSpec { - return { leftTokenRange: toTokenRange(left), rightTokenRange: toTokenRange(right), rule: { debugName, context, action, flags } }; - } - - function tokenRangeFrom(tokens: readonly SyntaxKind[]): TokenRange { - return { tokens, isSpecific: true }; - } - - function toTokenRange(arg: SyntaxKind | readonly SyntaxKind[] | TokenRange): TokenRange { - return typeof arg === "number" ? tokenRangeFrom([arg]) : isArray(arg) ? tokenRangeFrom(arg) : arg; - } - - function tokenRangeFromRange(from: SyntaxKind, to: SyntaxKind, except: readonly SyntaxKind[] = []): TokenRange { - const tokens: SyntaxKind[] = []; - for (let token = from; token <= to; token++) { - if (!contains(except, token)) { - tokens.push(token); - } +export interface RuleSpec { + readonly leftTokenRange: TokenRange; + readonly rightTokenRange: TokenRange; + readonly rule: Rule; +} +/* @internal */ +export function getAllRules(): RuleSpec[] { + const allTokens: SyntaxKind[] = []; + for (let token = SyntaxKind.FirstToken; token <= SyntaxKind.LastToken; token++) { + if (token !== SyntaxKind.EndOfFileToken) { + allTokens.push(token); } - return tokenRangeFrom(tokens); - } - - /// - /// Contexts - /// - - function optionEquals(optionName: K, optionValue: FormatCodeSettings[K]): (context: FormattingContext) => boolean { - return (context) => context.options && context.options[optionName] === optionValue; - } - - function isOptionEnabled(optionName: keyof FormatCodeSettings): (context: FormattingContext) => boolean { - return (context) => context.options && context.options.hasOwnProperty(optionName) && !!context.options[optionName]; - } - - function isOptionDisabled(optionName: keyof FormatCodeSettings): (context: FormattingContext) => boolean { - return (context) => context.options && context.options.hasOwnProperty(optionName) && !context.options[optionName]; } - - function isOptionDisabledOrUndefined(optionName: keyof FormatCodeSettings): (context: FormattingContext) => boolean { - return (context) => !context.options || !context.options.hasOwnProperty(optionName) || !context.options[optionName]; - } - - function isOptionDisabledOrUndefinedOrTokensOnSameLine(optionName: keyof FormatCodeSettings): (context: FormattingContext) => boolean { - return (context) => !context.options || !context.options.hasOwnProperty(optionName) || !context.options[optionName] || context.TokensAreOnSameLine(); - } - - function isOptionEnabledOrUndefined(optionName: keyof FormatCodeSettings): (context: FormattingContext) => boolean { - return (context) => !context.options || !context.options.hasOwnProperty(optionName) || !!context.options[optionName]; - } - - function isForContext(context: FormattingContext): boolean { - return context.contextNode.kind === SyntaxKind.ForStatement; - } - - function isNotForContext(context: FormattingContext): boolean { - return !isForContext(context); - } - - function isBinaryOpContext(context: FormattingContext): boolean { - switch (context.contextNode.kind) { - case SyntaxKind.BinaryExpression: - return (context.contextNode).operatorToken.kind !== SyntaxKind.CommaToken; - case SyntaxKind.ConditionalExpression: - case SyntaxKind.ConditionalType: - case SyntaxKind.AsExpression: - case SyntaxKind.ExportSpecifier: - case SyntaxKind.ImportSpecifier: - case SyntaxKind.TypePredicate: - case SyntaxKind.UnionType: - case SyntaxKind.IntersectionType: - return true; - - // equals in binding elements: function foo([[x, y] = [1, 2]]) - case SyntaxKind.BindingElement: - // equals in type X = ... - // falls through - case SyntaxKind.TypeAliasDeclaration: - // equal in import a = module('a'); - // falls through - case SyntaxKind.ImportEqualsDeclaration: - // equal in let a = 0 - // falls through - case SyntaxKind.VariableDeclaration: - // equal in p = 0 - // falls through - case SyntaxKind.Parameter: - case SyntaxKind.EnumMember: - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.PropertySignature: - return context.currentTokenSpan.kind === SyntaxKind.EqualsToken || context.nextTokenSpan.kind === SyntaxKind.EqualsToken; - // "in" keyword in for (let x in []) { } - case SyntaxKind.ForInStatement: - // "in" keyword in [P in keyof T]: T[P] - // falls through - case SyntaxKind.TypeParameter: - return context.currentTokenSpan.kind === SyntaxKind.InKeyword || context.nextTokenSpan.kind === SyntaxKind.InKeyword || context.currentTokenSpan.kind === SyntaxKind.EqualsToken || context.nextTokenSpan.kind === SyntaxKind.EqualsToken; - // Technically, "of" is not a binary operator, but format it the same way as "in" - case SyntaxKind.ForOfStatement: - return context.currentTokenSpan.kind === SyntaxKind.OfKeyword || context.nextTokenSpan.kind === SyntaxKind.OfKeyword; + function anyTokenExcept(...tokens: SyntaxKind[]): TokenRange { + return { tokens: allTokens.filter(t => !tokens.some(t2 => t2 === t)), isSpecific: false }; + } + const anyToken: TokenRange = { tokens: allTokens, isSpecific: false }; + const anyTokenIncludingMultilineComments = tokenRangeFrom([...allTokens, SyntaxKind.MultiLineCommentTrivia]); + const anyTokenIncludingEOF = tokenRangeFrom([...allTokens, SyntaxKind.EndOfFileToken]); + const keywords = tokenRangeFromRange(SyntaxKind.FirstKeyword, SyntaxKind.LastKeyword); + const binaryOperators = tokenRangeFromRange(SyntaxKind.FirstBinaryOperator, SyntaxKind.LastBinaryOperator); + const binaryKeywordOperators = [SyntaxKind.InKeyword, SyntaxKind.InstanceOfKeyword, SyntaxKind.OfKeyword, SyntaxKind.AsKeyword, SyntaxKind.IsKeyword]; + const unaryPrefixOperators = [SyntaxKind.PlusPlusToken, SyntaxKind.MinusMinusToken, SyntaxKind.TildeToken, SyntaxKind.ExclamationToken]; + const unaryPrefixExpressions = [ + SyntaxKind.NumericLiteral, SyntaxKind.BigIntLiteral, SyntaxKind.Identifier, SyntaxKind.OpenParenToken, + SyntaxKind.OpenBracketToken, SyntaxKind.OpenBraceToken, SyntaxKind.ThisKeyword, SyntaxKind.NewKeyword + ]; + const unaryPreincrementExpressions = [SyntaxKind.Identifier, SyntaxKind.OpenParenToken, SyntaxKind.ThisKeyword, SyntaxKind.NewKeyword]; + const unaryPostincrementExpressions = [SyntaxKind.Identifier, SyntaxKind.CloseParenToken, SyntaxKind.CloseBracketToken, SyntaxKind.NewKeyword]; + const unaryPredecrementExpressions = [SyntaxKind.Identifier, SyntaxKind.OpenParenToken, SyntaxKind.ThisKeyword, SyntaxKind.NewKeyword]; + const unaryPostdecrementExpressions = [SyntaxKind.Identifier, SyntaxKind.CloseParenToken, SyntaxKind.CloseBracketToken, SyntaxKind.NewKeyword]; + const comments = [SyntaxKind.SingleLineCommentTrivia, SyntaxKind.MultiLineCommentTrivia]; + const typeNames = [SyntaxKind.Identifier, ...typeKeywords]; + // Place a space before open brace in a function declaration + // TypeScript: Function can have return types, which can be made of tons of different token kinds + const functionOpenBraceLeftTokenRange = anyTokenIncludingMultilineComments; + // Place a space before open brace in a TypeScript declaration that has braces as children (class, module, enum, etc) + const typeScriptOpenBraceLeftTokenRange = tokenRangeFrom([SyntaxKind.Identifier, SyntaxKind.MultiLineCommentTrivia, SyntaxKind.ClassKeyword, SyntaxKind.ExportKeyword, SyntaxKind.ImportKeyword]); + // Place a space before open brace in a control flow construct + const controlOpenBraceLeftTokenRange = tokenRangeFrom([SyntaxKind.CloseParenToken, SyntaxKind.MultiLineCommentTrivia, SyntaxKind.DoKeyword, SyntaxKind.TryKeyword, SyntaxKind.FinallyKeyword, SyntaxKind.ElseKeyword]); + // These rules are higher in priority than user-configurable + const highPriorityCommonRules = [ + // Leave comments alone + rule("IgnoreBeforeComment", anyToken, comments, anyContext, RuleAction.StopProcessingSpaceActions), + rule("IgnoreAfterLineComment", SyntaxKind.SingleLineCommentTrivia, anyToken, anyContext, RuleAction.StopProcessingSpaceActions), + rule("NotSpaceBeforeColon", anyToken, SyntaxKind.ColonToken, [isNonJsxSameLineTokenContext, isNotBinaryOpContext, isNotTypeAnnotationContext], RuleAction.DeleteSpace), + rule("SpaceAfterColon", SyntaxKind.ColonToken, anyToken, [isNonJsxSameLineTokenContext, isNotBinaryOpContext], RuleAction.InsertSpace), + rule("NoSpaceBeforeQuestionMark", anyToken, SyntaxKind.QuestionToken, [isNonJsxSameLineTokenContext, isNotBinaryOpContext], RuleAction.DeleteSpace), + // insert space after '?' only when it is used in conditional operator + rule("SpaceAfterQuestionMarkInConditionalOperator", SyntaxKind.QuestionToken, anyToken, [isNonJsxSameLineTokenContext, isConditionalOperatorContext], RuleAction.InsertSpace), + // in other cases there should be no space between '?' and next token + rule("NoSpaceAfterQuestionMark", SyntaxKind.QuestionToken, anyToken, [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), + rule("NoSpaceBeforeDot", anyToken, [SyntaxKind.DotToken, SyntaxKind.QuestionDotToken], [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), + rule("NoSpaceAfterDot", [SyntaxKind.DotToken, SyntaxKind.QuestionDotToken], anyToken, [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), + rule("NoSpaceBetweenImportParenInImportType", SyntaxKind.ImportKeyword, SyntaxKind.OpenParenToken, [isNonJsxSameLineTokenContext, isImportTypeContext], RuleAction.DeleteSpace), + // Special handling of unary operators. + // Prefix operators generally shouldn't have a space between + // them and their target unary expression. + rule("NoSpaceAfterUnaryPrefixOperator", unaryPrefixOperators, unaryPrefixExpressions, [isNonJsxSameLineTokenContext, isNotBinaryOpContext], RuleAction.DeleteSpace), + rule("NoSpaceAfterUnaryPreincrementOperator", SyntaxKind.PlusPlusToken, unaryPreincrementExpressions, [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), + rule("NoSpaceAfterUnaryPredecrementOperator", SyntaxKind.MinusMinusToken, unaryPredecrementExpressions, [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), + rule("NoSpaceBeforeUnaryPostincrementOperator", unaryPostincrementExpressions, SyntaxKind.PlusPlusToken, [isNonJsxSameLineTokenContext, isNotStatementConditionContext], RuleAction.DeleteSpace), + rule("NoSpaceBeforeUnaryPostdecrementOperator", unaryPostdecrementExpressions, SyntaxKind.MinusMinusToken, [isNonJsxSameLineTokenContext, isNotStatementConditionContext], RuleAction.DeleteSpace), + // More unary operator special-casing. + // DevDiv 181814: Be careful when removing leading whitespace + // around unary operators. Examples: + // 1 - -2 --X--> 1--2 + // a + ++b --X--> a+++b + rule("SpaceAfterPostincrementWhenFollowedByAdd", SyntaxKind.PlusPlusToken, SyntaxKind.PlusToken, [isNonJsxSameLineTokenContext, isBinaryOpContext], RuleAction.InsertSpace), + rule("SpaceAfterAddWhenFollowedByUnaryPlus", SyntaxKind.PlusToken, SyntaxKind.PlusToken, [isNonJsxSameLineTokenContext, isBinaryOpContext], RuleAction.InsertSpace), + rule("SpaceAfterAddWhenFollowedByPreincrement", SyntaxKind.PlusToken, SyntaxKind.PlusPlusToken, [isNonJsxSameLineTokenContext, isBinaryOpContext], RuleAction.InsertSpace), + rule("SpaceAfterPostdecrementWhenFollowedBySubtract", SyntaxKind.MinusMinusToken, SyntaxKind.MinusToken, [isNonJsxSameLineTokenContext, isBinaryOpContext], RuleAction.InsertSpace), + rule("SpaceAfterSubtractWhenFollowedByUnaryMinus", SyntaxKind.MinusToken, SyntaxKind.MinusToken, [isNonJsxSameLineTokenContext, isBinaryOpContext], RuleAction.InsertSpace), + rule("SpaceAfterSubtractWhenFollowedByPredecrement", SyntaxKind.MinusToken, SyntaxKind.MinusMinusToken, [isNonJsxSameLineTokenContext, isBinaryOpContext], RuleAction.InsertSpace), + rule("NoSpaceAfterCloseBrace", SyntaxKind.CloseBraceToken, [SyntaxKind.CommaToken, SyntaxKind.SemicolonToken], [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), + // For functions and control block place } on a new line [multi-line rule] + rule("NewLineBeforeCloseBraceInBlockContext", anyTokenIncludingMultilineComments, SyntaxKind.CloseBraceToken, [isMultilineBlockContext], RuleAction.InsertNewLine), + // Space/new line after }. + rule("SpaceAfterCloseBrace", SyntaxKind.CloseBraceToken, anyTokenExcept(SyntaxKind.CloseParenToken), [isNonJsxSameLineTokenContext, isAfterCodeBlockContext], RuleAction.InsertSpace), + // Special case for (}, else) and (}, while) since else & while tokens are not part of the tree which makes SpaceAfterCloseBrace rule not applied + // Also should not apply to }) + rule("SpaceBetweenCloseBraceAndElse", SyntaxKind.CloseBraceToken, SyntaxKind.ElseKeyword, [isNonJsxSameLineTokenContext], RuleAction.InsertSpace), + rule("SpaceBetweenCloseBraceAndWhile", SyntaxKind.CloseBraceToken, SyntaxKind.WhileKeyword, [isNonJsxSameLineTokenContext], RuleAction.InsertSpace), + rule("NoSpaceBetweenEmptyBraceBrackets", SyntaxKind.OpenBraceToken, SyntaxKind.CloseBraceToken, [isNonJsxSameLineTokenContext, isObjectContext], RuleAction.DeleteSpace), + // Add a space after control dec context if the next character is an open bracket ex: 'if (false)[a, b] = [1, 2];' -> 'if (false) [a, b] = [1, 2];' + rule("SpaceAfterConditionalClosingParen", SyntaxKind.CloseParenToken, SyntaxKind.OpenBracketToken, [isControlDeclContext], RuleAction.InsertSpace), + rule("NoSpaceBetweenFunctionKeywordAndStar", SyntaxKind.FunctionKeyword, SyntaxKind.AsteriskToken, [isFunctionDeclarationOrFunctionExpressionContext], RuleAction.DeleteSpace), + rule("SpaceAfterStarInGeneratorDeclaration", SyntaxKind.AsteriskToken, SyntaxKind.Identifier, [isFunctionDeclarationOrFunctionExpressionContext], RuleAction.InsertSpace), + rule("SpaceAfterFunctionInFuncDecl", SyntaxKind.FunctionKeyword, anyToken, [isFunctionDeclContext], RuleAction.InsertSpace), + // Insert new line after { and before } in multi-line contexts. + rule("NewLineAfterOpenBraceInBlockContext", SyntaxKind.OpenBraceToken, anyToken, [isMultilineBlockContext], RuleAction.InsertNewLine), + // For get/set members, we check for (identifier,identifier) since get/set don't have tokens and they are represented as just an identifier token. + // Though, we do extra check on the context to make sure we are dealing with get/set node. Example: + // get x() {} + // set x(val) {} + rule("SpaceAfterGetSetInMember", [SyntaxKind.GetKeyword, SyntaxKind.SetKeyword], SyntaxKind.Identifier, [isFunctionDeclContext], RuleAction.InsertSpace), + rule("NoSpaceBetweenYieldKeywordAndStar", SyntaxKind.YieldKeyword, SyntaxKind.AsteriskToken, [isNonJsxSameLineTokenContext, isYieldOrYieldStarWithOperand], RuleAction.DeleteSpace), + rule("SpaceBetweenYieldOrYieldStarAndOperand", [SyntaxKind.YieldKeyword, SyntaxKind.AsteriskToken], anyToken, [isNonJsxSameLineTokenContext, isYieldOrYieldStarWithOperand], RuleAction.InsertSpace), + rule("NoSpaceBetweenReturnAndSemicolon", SyntaxKind.ReturnKeyword, SyntaxKind.SemicolonToken, [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), + rule("SpaceAfterCertainKeywords", [SyntaxKind.VarKeyword, SyntaxKind.ThrowKeyword, SyntaxKind.NewKeyword, SyntaxKind.DeleteKeyword, SyntaxKind.ReturnKeyword, SyntaxKind.TypeOfKeyword, SyntaxKind.AwaitKeyword], anyToken, [isNonJsxSameLineTokenContext], RuleAction.InsertSpace), + rule("SpaceAfterLetConstInVariableDeclaration", [SyntaxKind.LetKeyword, SyntaxKind.ConstKeyword], anyToken, [isNonJsxSameLineTokenContext, isStartOfVariableDeclarationList], RuleAction.InsertSpace), + rule("NoSpaceBeforeOpenParenInFuncCall", anyToken, SyntaxKind.OpenParenToken, [isNonJsxSameLineTokenContext, isFunctionCallOrNewContext, isPreviousTokenNotComma], RuleAction.DeleteSpace), + // Special case for binary operators (that are keywords). For these we have to add a space and shouldn't follow any user options. + rule("SpaceBeforeBinaryKeywordOperator", anyToken, binaryKeywordOperators, [isNonJsxSameLineTokenContext, isBinaryOpContext], RuleAction.InsertSpace), + rule("SpaceAfterBinaryKeywordOperator", binaryKeywordOperators, anyToken, [isNonJsxSameLineTokenContext, isBinaryOpContext], RuleAction.InsertSpace), + rule("SpaceAfterVoidOperator", SyntaxKind.VoidKeyword, anyToken, [isNonJsxSameLineTokenContext, isVoidOpContext], RuleAction.InsertSpace), + // Async-await + rule("SpaceBetweenAsyncAndOpenParen", SyntaxKind.AsyncKeyword, SyntaxKind.OpenParenToken, [isArrowFunctionContext, isNonJsxSameLineTokenContext], RuleAction.InsertSpace), + rule("SpaceBetweenAsyncAndFunctionKeyword", SyntaxKind.AsyncKeyword, SyntaxKind.FunctionKeyword, [isNonJsxSameLineTokenContext], RuleAction.InsertSpace), + // Template string + rule("NoSpaceBetweenTagAndTemplateString", [SyntaxKind.Identifier, SyntaxKind.CloseParenToken], [SyntaxKind.NoSubstitutionTemplateLiteral, SyntaxKind.TemplateHead], [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), + // JSX opening elements + rule("SpaceBeforeJsxAttribute", anyToken, SyntaxKind.Identifier, [isNextTokenParentJsxAttribute, isNonJsxSameLineTokenContext], RuleAction.InsertSpace), + rule("SpaceBeforeSlashInJsxOpeningElement", anyToken, SyntaxKind.SlashToken, [isJsxSelfClosingElementContext, isNonJsxSameLineTokenContext], RuleAction.InsertSpace), + rule("NoSpaceBeforeGreaterThanTokenInJsxOpeningElement", SyntaxKind.SlashToken, SyntaxKind.GreaterThanToken, [isJsxSelfClosingElementContext, isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), + rule("NoSpaceBeforeEqualInJsxAttribute", anyToken, SyntaxKind.EqualsToken, [isJsxAttributeContext, isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), + rule("NoSpaceAfterEqualInJsxAttribute", SyntaxKind.EqualsToken, anyToken, [isJsxAttributeContext, isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), + // TypeScript-specific rules + // Use of module as a function call. e.g.: import m2 = module("m2"); + rule("NoSpaceAfterModuleImport", [SyntaxKind.ModuleKeyword, SyntaxKind.RequireKeyword], SyntaxKind.OpenParenToken, [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), + // Add a space around certain TypeScript keywords + rule("SpaceAfterCertainTypeScriptKeywords", [ + SyntaxKind.AbstractKeyword, + SyntaxKind.ClassKeyword, + SyntaxKind.DeclareKeyword, + SyntaxKind.DefaultKeyword, + SyntaxKind.EnumKeyword, + SyntaxKind.ExportKeyword, + SyntaxKind.ExtendsKeyword, + SyntaxKind.GetKeyword, + SyntaxKind.ImplementsKeyword, + SyntaxKind.ImportKeyword, + SyntaxKind.InterfaceKeyword, + SyntaxKind.ModuleKeyword, + SyntaxKind.NamespaceKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.ReadonlyKeyword, + SyntaxKind.SetKeyword, + SyntaxKind.StaticKeyword, + SyntaxKind.TypeKeyword, + SyntaxKind.FromKeyword, + SyntaxKind.KeyOfKeyword, + SyntaxKind.InferKeyword, + ], anyToken, [isNonJsxSameLineTokenContext], RuleAction.InsertSpace), + rule("SpaceBeforeCertainTypeScriptKeywords", anyToken, [SyntaxKind.ExtendsKeyword, SyntaxKind.ImplementsKeyword, SyntaxKind.FromKeyword], [isNonJsxSameLineTokenContext], RuleAction.InsertSpace), + // Treat string literals in module names as identifiers, and add a space between the literal and the opening Brace braces, e.g.: module "m2" { + rule("SpaceAfterModuleName", SyntaxKind.StringLiteral, SyntaxKind.OpenBraceToken, [isModuleDeclContext], RuleAction.InsertSpace), + // Lambda expressions + rule("SpaceBeforeArrow", anyToken, SyntaxKind.EqualsGreaterThanToken, [isNonJsxSameLineTokenContext], RuleAction.InsertSpace), + rule("SpaceAfterArrow", SyntaxKind.EqualsGreaterThanToken, anyToken, [isNonJsxSameLineTokenContext], RuleAction.InsertSpace), + // Optional parameters and let args + rule("NoSpaceAfterEllipsis", SyntaxKind.DotDotDotToken, SyntaxKind.Identifier, [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), + rule("NoSpaceAfterOptionalParameters", SyntaxKind.QuestionToken, [SyntaxKind.CloseParenToken, SyntaxKind.CommaToken], [isNonJsxSameLineTokenContext, isNotBinaryOpContext], RuleAction.DeleteSpace), + // Remove spaces in empty interface literals. e.g.: x: {} + rule("NoSpaceBetweenEmptyInterfaceBraceBrackets", SyntaxKind.OpenBraceToken, SyntaxKind.CloseBraceToken, [isNonJsxSameLineTokenContext, isObjectTypeContext], RuleAction.DeleteSpace), + // generics and type assertions + rule("NoSpaceBeforeOpenAngularBracket", typeNames, SyntaxKind.LessThanToken, [isNonJsxSameLineTokenContext, isTypeArgumentOrParameterOrAssertionContext], RuleAction.DeleteSpace), + rule("NoSpaceBetweenCloseParenAndAngularBracket", SyntaxKind.CloseParenToken, SyntaxKind.LessThanToken, [isNonJsxSameLineTokenContext, isTypeArgumentOrParameterOrAssertionContext], RuleAction.DeleteSpace), + rule("NoSpaceAfterOpenAngularBracket", SyntaxKind.LessThanToken, anyToken, [isNonJsxSameLineTokenContext, isTypeArgumentOrParameterOrAssertionContext], RuleAction.DeleteSpace), + rule("NoSpaceBeforeCloseAngularBracket", anyToken, SyntaxKind.GreaterThanToken, [isNonJsxSameLineTokenContext, isTypeArgumentOrParameterOrAssertionContext], RuleAction.DeleteSpace), + rule("NoSpaceAfterCloseAngularBracket", SyntaxKind.GreaterThanToken, [SyntaxKind.OpenParenToken, SyntaxKind.OpenBracketToken, SyntaxKind.GreaterThanToken, SyntaxKind.CommaToken], [isNonJsxSameLineTokenContext, isTypeArgumentOrParameterOrAssertionContext, isNotFunctionDeclContext /*To prevent an interference with the SpaceBeforeOpenParenInFuncDecl rule*/], RuleAction.DeleteSpace), + // decorators + rule("SpaceBeforeAt", [SyntaxKind.CloseParenToken, SyntaxKind.Identifier], SyntaxKind.AtToken, [isNonJsxSameLineTokenContext], RuleAction.InsertSpace), + rule("NoSpaceAfterAt", SyntaxKind.AtToken, anyToken, [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), + // Insert space after @ in decorator + rule("SpaceAfterDecorator", anyToken, [ + SyntaxKind.AbstractKeyword, + SyntaxKind.Identifier, + SyntaxKind.ExportKeyword, + SyntaxKind.DefaultKeyword, + SyntaxKind.ClassKeyword, + SyntaxKind.StaticKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.GetKeyword, + SyntaxKind.SetKeyword, + SyntaxKind.OpenBracketToken, + SyntaxKind.AsteriskToken, + ], [isEndOfDecoratorContextOnSameLine], RuleAction.InsertSpace), + rule("NoSpaceBeforeNonNullAssertionOperator", anyToken, SyntaxKind.ExclamationToken, [isNonJsxSameLineTokenContext, isNonNullAssertionContext], RuleAction.DeleteSpace), + rule("NoSpaceAfterNewKeywordOnConstructorSignature", SyntaxKind.NewKeyword, SyntaxKind.OpenParenToken, [isNonJsxSameLineTokenContext, isConstructorSignatureContext], RuleAction.DeleteSpace), + rule("SpaceLessThanAndNonJSXTypeAnnotation", SyntaxKind.LessThanToken, SyntaxKind.LessThanToken, [isNonJsxSameLineTokenContext], RuleAction.InsertSpace), + ]; + // These rules are applied after high priority + const userConfigurableRules = [ + // Treat constructor as an identifier in a function declaration, and remove spaces between constructor and following left parentheses + rule("SpaceAfterConstructor", SyntaxKind.ConstructorKeyword, SyntaxKind.OpenParenToken, [isOptionEnabled("insertSpaceAfterConstructor"), isNonJsxSameLineTokenContext], RuleAction.InsertSpace), + rule("NoSpaceAfterConstructor", SyntaxKind.ConstructorKeyword, SyntaxKind.OpenParenToken, [isOptionDisabledOrUndefined("insertSpaceAfterConstructor"), isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), + rule("SpaceAfterComma", SyntaxKind.CommaToken, anyToken, [isOptionEnabled("insertSpaceAfterCommaDelimiter"), isNonJsxSameLineTokenContext, isNonJsxElementOrFragmentContext, isNextTokenNotCloseBracket, isNextTokenNotCloseParen], RuleAction.InsertSpace), + rule("NoSpaceAfterComma", SyntaxKind.CommaToken, anyToken, [isOptionDisabledOrUndefined("insertSpaceAfterCommaDelimiter"), isNonJsxSameLineTokenContext, isNonJsxElementOrFragmentContext], RuleAction.DeleteSpace), + // Insert space after function keyword for anonymous functions + rule("SpaceAfterAnonymousFunctionKeyword", [SyntaxKind.FunctionKeyword, SyntaxKind.AsteriskToken], SyntaxKind.OpenParenToken, [isOptionEnabled("insertSpaceAfterFunctionKeywordForAnonymousFunctions"), isFunctionDeclContext], RuleAction.InsertSpace), + rule("NoSpaceAfterAnonymousFunctionKeyword", [SyntaxKind.FunctionKeyword, SyntaxKind.AsteriskToken], SyntaxKind.OpenParenToken, [isOptionDisabledOrUndefined("insertSpaceAfterFunctionKeywordForAnonymousFunctions"), isFunctionDeclContext], RuleAction.DeleteSpace), + // Insert space after keywords in control flow statements + rule("SpaceAfterKeywordInControl", keywords, SyntaxKind.OpenParenToken, [isOptionEnabled("insertSpaceAfterKeywordsInControlFlowStatements"), isControlDeclContext], RuleAction.InsertSpace), + rule("NoSpaceAfterKeywordInControl", keywords, SyntaxKind.OpenParenToken, [isOptionDisabledOrUndefined("insertSpaceAfterKeywordsInControlFlowStatements"), isControlDeclContext], RuleAction.DeleteSpace), + // Insert space after opening and before closing nonempty parenthesis + rule("SpaceAfterOpenParen", SyntaxKind.OpenParenToken, anyToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis"), isNonJsxSameLineTokenContext], RuleAction.InsertSpace), + rule("SpaceBeforeCloseParen", anyToken, SyntaxKind.CloseParenToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis"), isNonJsxSameLineTokenContext], RuleAction.InsertSpace), + rule("SpaceBetweenOpenParens", SyntaxKind.OpenParenToken, SyntaxKind.OpenParenToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis"), isNonJsxSameLineTokenContext], RuleAction.InsertSpace), + rule("NoSpaceBetweenParens", SyntaxKind.OpenParenToken, SyntaxKind.CloseParenToken, [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), + rule("NoSpaceAfterOpenParen", SyntaxKind.OpenParenToken, anyToken, [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis"), isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), + rule("NoSpaceBeforeCloseParen", anyToken, SyntaxKind.CloseParenToken, [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis"), isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), + // Insert space after opening and before closing nonempty brackets + rule("SpaceAfterOpenBracket", SyntaxKind.OpenBracketToken, anyToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets"), isNonJsxSameLineTokenContext], RuleAction.InsertSpace), + rule("SpaceBeforeCloseBracket", anyToken, SyntaxKind.CloseBracketToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets"), isNonJsxSameLineTokenContext], RuleAction.InsertSpace), + rule("NoSpaceBetweenBrackets", SyntaxKind.OpenBracketToken, SyntaxKind.CloseBracketToken, [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), + rule("NoSpaceAfterOpenBracket", SyntaxKind.OpenBracketToken, anyToken, [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets"), isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), + rule("NoSpaceBeforeCloseBracket", anyToken, SyntaxKind.CloseBracketToken, [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets"), isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), + // Insert a space after { and before } in single-line contexts, but remove space from empty object literals {}. + rule("SpaceAfterOpenBrace", SyntaxKind.OpenBraceToken, anyToken, [isOptionEnabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces"), isBraceWrappedContext], RuleAction.InsertSpace), + rule("SpaceBeforeCloseBrace", anyToken, SyntaxKind.CloseBraceToken, [isOptionEnabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces"), isBraceWrappedContext], RuleAction.InsertSpace), + rule("NoSpaceBetweenEmptyBraceBrackets", SyntaxKind.OpenBraceToken, SyntaxKind.CloseBraceToken, [isNonJsxSameLineTokenContext, isObjectContext], RuleAction.DeleteSpace), + rule("NoSpaceAfterOpenBrace", SyntaxKind.OpenBraceToken, anyToken, [isOptionDisabled("insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces"), isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), + rule("NoSpaceBeforeCloseBrace", anyToken, SyntaxKind.CloseBraceToken, [isOptionDisabled("insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces"), isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), + // Insert space after opening and before closing template string braces + rule("SpaceAfterTemplateHeadAndMiddle", [SyntaxKind.TemplateHead, SyntaxKind.TemplateMiddle], anyToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces"), isNonJsxSameLineTokenContext], RuleAction.InsertSpace), + rule("SpaceBeforeTemplateMiddleAndTail", anyToken, [SyntaxKind.TemplateMiddle, SyntaxKind.TemplateTail], [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces"), isNonJsxSameLineTokenContext], RuleAction.InsertSpace), + rule("NoSpaceAfterTemplateHeadAndMiddle", [SyntaxKind.TemplateHead, SyntaxKind.TemplateMiddle], anyToken, [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces"), isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), + rule("NoSpaceBeforeTemplateMiddleAndTail", anyToken, [SyntaxKind.TemplateMiddle, SyntaxKind.TemplateTail], [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces"), isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), + // No space after { and before } in JSX expression + rule("SpaceAfterOpenBraceInJsxExpression", SyntaxKind.OpenBraceToken, anyToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces"), isNonJsxSameLineTokenContext, isJsxExpressionContext], RuleAction.InsertSpace), + rule("SpaceBeforeCloseBraceInJsxExpression", anyToken, SyntaxKind.CloseBraceToken, [isOptionEnabled("insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces"), isNonJsxSameLineTokenContext, isJsxExpressionContext], RuleAction.InsertSpace), + rule("NoSpaceAfterOpenBraceInJsxExpression", SyntaxKind.OpenBraceToken, anyToken, [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces"), isNonJsxSameLineTokenContext, isJsxExpressionContext], RuleAction.DeleteSpace), + rule("NoSpaceBeforeCloseBraceInJsxExpression", anyToken, SyntaxKind.CloseBraceToken, [isOptionDisabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces"), isNonJsxSameLineTokenContext, isJsxExpressionContext], RuleAction.DeleteSpace), + // Insert space after semicolon in for statement + rule("SpaceAfterSemicolonInFor", SyntaxKind.SemicolonToken, anyToken, [isOptionEnabled("insertSpaceAfterSemicolonInForStatements"), isNonJsxSameLineTokenContext, isForContext], RuleAction.InsertSpace), + rule("NoSpaceAfterSemicolonInFor", SyntaxKind.SemicolonToken, anyToken, [isOptionDisabledOrUndefined("insertSpaceAfterSemicolonInForStatements"), isNonJsxSameLineTokenContext, isForContext], RuleAction.DeleteSpace), + // Insert space before and after binary operators + rule("SpaceBeforeBinaryOperator", anyToken, binaryOperators, [isOptionEnabled("insertSpaceBeforeAndAfterBinaryOperators"), isNonJsxSameLineTokenContext, isBinaryOpContext], RuleAction.InsertSpace), + rule("SpaceAfterBinaryOperator", binaryOperators, anyToken, [isOptionEnabled("insertSpaceBeforeAndAfterBinaryOperators"), isNonJsxSameLineTokenContext, isBinaryOpContext], RuleAction.InsertSpace), + rule("NoSpaceBeforeBinaryOperator", anyToken, binaryOperators, [isOptionDisabledOrUndefined("insertSpaceBeforeAndAfterBinaryOperators"), isNonJsxSameLineTokenContext, isBinaryOpContext], RuleAction.DeleteSpace), + rule("NoSpaceAfterBinaryOperator", binaryOperators, anyToken, [isOptionDisabledOrUndefined("insertSpaceBeforeAndAfterBinaryOperators"), isNonJsxSameLineTokenContext, isBinaryOpContext], RuleAction.DeleteSpace), + rule("SpaceBeforeOpenParenInFuncDecl", anyToken, SyntaxKind.OpenParenToken, [isOptionEnabled("insertSpaceBeforeFunctionParenthesis"), isNonJsxSameLineTokenContext, isFunctionDeclContext], RuleAction.InsertSpace), + rule("NoSpaceBeforeOpenParenInFuncDecl", anyToken, SyntaxKind.OpenParenToken, [isOptionDisabledOrUndefined("insertSpaceBeforeFunctionParenthesis"), isNonJsxSameLineTokenContext, isFunctionDeclContext], RuleAction.DeleteSpace), + // Open Brace braces after control block + rule("NewLineBeforeOpenBraceInControl", controlOpenBraceLeftTokenRange, SyntaxKind.OpenBraceToken, [isOptionEnabled("placeOpenBraceOnNewLineForControlBlocks"), isControlDeclContext, isBeforeMultilineBlockContext], RuleAction.InsertNewLine, RuleFlags.CanDeleteNewLines), + // Open Brace braces after function + // TypeScript: Function can have return types, which can be made of tons of different token kinds + rule("NewLineBeforeOpenBraceInFunction", functionOpenBraceLeftTokenRange, SyntaxKind.OpenBraceToken, [isOptionEnabled("placeOpenBraceOnNewLineForFunctions"), isFunctionDeclContext, isBeforeMultilineBlockContext], RuleAction.InsertNewLine, RuleFlags.CanDeleteNewLines), + // Open Brace braces after TypeScript module/class/interface + rule("NewLineBeforeOpenBraceInTypeScriptDeclWithBlock", typeScriptOpenBraceLeftTokenRange, SyntaxKind.OpenBraceToken, [isOptionEnabled("placeOpenBraceOnNewLineForFunctions"), isTypeScriptDeclWithBlockContext, isBeforeMultilineBlockContext], RuleAction.InsertNewLine, RuleFlags.CanDeleteNewLines), + rule("SpaceAfterTypeAssertion", SyntaxKind.GreaterThanToken, anyToken, [isOptionEnabled("insertSpaceAfterTypeAssertion"), isNonJsxSameLineTokenContext, isTypeAssertionContext], RuleAction.InsertSpace), + rule("NoSpaceAfterTypeAssertion", SyntaxKind.GreaterThanToken, anyToken, [isOptionDisabledOrUndefined("insertSpaceAfterTypeAssertion"), isNonJsxSameLineTokenContext, isTypeAssertionContext], RuleAction.DeleteSpace), + rule("SpaceBeforeTypeAnnotation", anyToken, SyntaxKind.ColonToken, [isOptionEnabled("insertSpaceBeforeTypeAnnotation"), isNonJsxSameLineTokenContext, isTypeAnnotationContext], RuleAction.InsertSpace), + rule("NoSpaceBeforeTypeAnnotation", anyToken, SyntaxKind.ColonToken, [isOptionDisabledOrUndefined("insertSpaceBeforeTypeAnnotation"), isNonJsxSameLineTokenContext, isTypeAnnotationContext], RuleAction.DeleteSpace), + rule("NoOptionalSemicolon", SyntaxKind.SemicolonToken, anyTokenIncludingEOF, [optionEquals("semicolons", SemicolonPreference.Remove), isSemicolonDeletionContext], RuleAction.DeleteToken), + rule("OptionalSemicolon", anyToken, anyTokenIncludingEOF, [optionEquals("semicolons", SemicolonPreference.Insert), isSemicolonInsertionContext], RuleAction.InsertTrailingSemicolon), + ]; + // These rules are lower in priority than user-configurable. Rules earlier in this list have priority over rules later in the list. + const lowPriorityCommonRules = [ + // Space after keyword but not before ; or : or ? + rule("NoSpaceBeforeSemicolon", anyToken, SyntaxKind.SemicolonToken, [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), + rule("SpaceBeforeOpenBraceInControl", controlOpenBraceLeftTokenRange, SyntaxKind.OpenBraceToken, [isOptionDisabledOrUndefinedOrTokensOnSameLine("placeOpenBraceOnNewLineForControlBlocks"), isControlDeclContext, isNotFormatOnEnter, isSameLineTokenOrBeforeBlockContext], RuleAction.InsertSpace, RuleFlags.CanDeleteNewLines), + rule("SpaceBeforeOpenBraceInFunction", functionOpenBraceLeftTokenRange, SyntaxKind.OpenBraceToken, [isOptionDisabledOrUndefinedOrTokensOnSameLine("placeOpenBraceOnNewLineForFunctions"), isFunctionDeclContext, isBeforeBlockContext, isNotFormatOnEnter, isSameLineTokenOrBeforeBlockContext], RuleAction.InsertSpace, RuleFlags.CanDeleteNewLines), + rule("SpaceBeforeOpenBraceInTypeScriptDeclWithBlock", typeScriptOpenBraceLeftTokenRange, SyntaxKind.OpenBraceToken, [isOptionDisabledOrUndefinedOrTokensOnSameLine("placeOpenBraceOnNewLineForFunctions"), isTypeScriptDeclWithBlockContext, isNotFormatOnEnter, isSameLineTokenOrBeforeBlockContext], RuleAction.InsertSpace, RuleFlags.CanDeleteNewLines), + rule("NoSpaceBeforeComma", anyToken, SyntaxKind.CommaToken, [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), + // No space before and after indexer `x[]` + rule("NoSpaceBeforeOpenBracket", anyTokenExcept(SyntaxKind.AsyncKeyword, SyntaxKind.CaseKeyword), SyntaxKind.OpenBracketToken, [isNonJsxSameLineTokenContext], RuleAction.DeleteSpace), + rule("NoSpaceAfterCloseBracket", SyntaxKind.CloseBracketToken, anyToken, [isNonJsxSameLineTokenContext, isNotBeforeBlockInFunctionDeclarationContext], RuleAction.DeleteSpace), + rule("SpaceAfterSemicolon", SyntaxKind.SemicolonToken, anyToken, [isNonJsxSameLineTokenContext], RuleAction.InsertSpace), + // Remove extra space between for and await + rule("SpaceBetweenForAndAwaitKeyword", SyntaxKind.ForKeyword, SyntaxKind.AwaitKeyword, [isNonJsxSameLineTokenContext], RuleAction.InsertSpace), + // Add a space between statements. All keywords except (do,else,case) has open/close parens after them. + // So, we have a rule to add a space for [),Any], [do,Any], [else,Any], and [case,Any] + rule("SpaceBetweenStatements", [SyntaxKind.CloseParenToken, SyntaxKind.DoKeyword, SyntaxKind.ElseKeyword, SyntaxKind.CaseKeyword], anyToken, [isNonJsxSameLineTokenContext, isNonJsxElementOrFragmentContext, isNotForContext], RuleAction.InsertSpace), + // This low-pri rule takes care of "try {" and "finally {" in case the rule SpaceBeforeOpenBraceInControl didn't execute on FormatOnEnter. + rule("SpaceAfterTryFinally", [SyntaxKind.TryKeyword, SyntaxKind.FinallyKeyword], SyntaxKind.OpenBraceToken, [isNonJsxSameLineTokenContext], RuleAction.InsertSpace), + ]; + return [ + ...highPriorityCommonRules, + ...userConfigurableRules, + ...lowPriorityCommonRules, + ]; +} +/** + * A rule takes a two tokens (left/right) and a particular context + * for which you're meant to look at them. You then declare what should the + * whitespace annotation be between these tokens via the action param. + * + * @param debugName Name to print + * @param left The left side of the comparison + * @param right The right side of the comparison + * @param context A set of filters to narrow down the space in which this formatter rule applies + * @param action a declaration of the expected whitespace + * @param flags whether the rule deletes a line or not, defaults to no-op + */ +/* @internal */ +function rule(debugName: string, left: SyntaxKind | readonly SyntaxKind[] | TokenRange, right: SyntaxKind | readonly SyntaxKind[] | TokenRange, context: readonly ContextPredicate[], action: RuleAction, flags: RuleFlags = RuleFlags.None): RuleSpec { + return { leftTokenRange: toTokenRange(left), rightTokenRange: toTokenRange(right), rule: { debugName, context, action, flags } }; +} +/* @internal */ +function tokenRangeFrom(tokens: readonly SyntaxKind[]): TokenRange { + return { tokens, isSpecific: true }; +} +/* @internal */ +function toTokenRange(arg: SyntaxKind | readonly SyntaxKind[] | TokenRange): TokenRange { + return typeof arg === "number" ? tokenRangeFrom([arg]) : isArray(arg) ? tokenRangeFrom(arg) : arg; +} +/* @internal */ +function tokenRangeFromRange(from: SyntaxKind, to: SyntaxKind, except: readonly SyntaxKind[] = []): TokenRange { + const tokens: SyntaxKind[] = []; + for (let token = from; token <= to; token++) { + if (!contains(except, token)) { + tokens.push(token); } - return false; - } - - function isNotBinaryOpContext(context: FormattingContext): boolean { - return !isBinaryOpContext(context); - } - - function isNotTypeAnnotationContext(context: FormattingContext): boolean { - return !isTypeAnnotationContext(context); - } - - function isTypeAnnotationContext(context: FormattingContext): boolean { - const contextKind = context.contextNode.kind; - return contextKind === SyntaxKind.PropertyDeclaration || - contextKind === SyntaxKind.PropertySignature || - contextKind === SyntaxKind.Parameter || - contextKind === SyntaxKind.VariableDeclaration || - isFunctionLikeKind(contextKind); - } - - function isConditionalOperatorContext(context: FormattingContext): boolean { - return context.contextNode.kind === SyntaxKind.ConditionalExpression || - context.contextNode.kind === SyntaxKind.ConditionalType; - } - - function isSameLineTokenOrBeforeBlockContext(context: FormattingContext): boolean { - return context.TokensAreOnSameLine() || isBeforeBlockContext(context); - } - - function isBraceWrappedContext(context: FormattingContext): boolean { - return context.contextNode.kind === SyntaxKind.ObjectBindingPattern || - context.contextNode.kind === SyntaxKind.MappedType || - isSingleLineBlockContext(context); } - - // This check is done before an open brace in a control construct, a function, or a typescript block declaration - function isBeforeMultilineBlockContext(context: FormattingContext): boolean { - return isBeforeBlockContext(context) && !(context.NextNodeAllOnSameLine() || context.NextNodeBlockIsOnOneLine()); - } - - function isMultilineBlockContext(context: FormattingContext): boolean { - return isBlockContext(context) && !(context.ContextNodeAllOnSameLine() || context.ContextNodeBlockIsOnOneLine()); - } - - function isSingleLineBlockContext(context: FormattingContext): boolean { - return isBlockContext(context) && (context.ContextNodeAllOnSameLine() || context.ContextNodeBlockIsOnOneLine()); - } - - function isBlockContext(context: FormattingContext): boolean { - return nodeIsBlockContext(context.contextNode); - } - - function isBeforeBlockContext(context: FormattingContext): boolean { - return nodeIsBlockContext(context.nextTokenParent); - } - - // IMPORTANT!!! This method must return true ONLY for nodes with open and close braces as immediate children - function nodeIsBlockContext(node: Node): boolean { - if (nodeIsTypeScriptDeclWithBlockContext(node)) { - // This means we are in a context that looks like a block to the user, but in the grammar is actually not a node (it's a class, module, enum, object type literal, etc). + return tokenRangeFrom(tokens); +} +/// +/// Contexts +/// +/* @internal */ +function optionEquals(optionName: K, optionValue: FormatCodeSettings[K]): (context: FormattingContext) => boolean { + return (context) => context.options && context.options[optionName] === optionValue; +} +/* @internal */ +function isOptionEnabled(optionName: keyof FormatCodeSettings): (context: FormattingContext) => boolean { + return (context) => context.options && context.options.hasOwnProperty(optionName) && !!context.options[optionName]; +} +/* @internal */ +function isOptionDisabled(optionName: keyof FormatCodeSettings): (context: FormattingContext) => boolean { + return (context) => context.options && context.options.hasOwnProperty(optionName) && !context.options[optionName]; +} +/* @internal */ +function isOptionDisabledOrUndefined(optionName: keyof FormatCodeSettings): (context: FormattingContext) => boolean { + return (context) => !context.options || !context.options.hasOwnProperty(optionName) || !context.options[optionName]; +} +/* @internal */ +function isOptionDisabledOrUndefinedOrTokensOnSameLine(optionName: keyof FormatCodeSettings): (context: FormattingContext) => boolean { + return (context) => !context.options || !context.options.hasOwnProperty(optionName) || !context.options[optionName] || context.TokensAreOnSameLine(); +} +/* @internal */ +function isOptionEnabledOrUndefined(optionName: keyof FormatCodeSettings): (context: FormattingContext) => boolean { + return (context) => !context.options || !context.options.hasOwnProperty(optionName) || !!context.options[optionName]; +} +/* @internal */ +function isForContext(context: FormattingContext): boolean { + return context.contextNode.kind === SyntaxKind.ForStatement; +} +/* @internal */ +function isNotForContext(context: FormattingContext): boolean { + return !isForContext(context); +} +/* @internal */ +function isBinaryOpContext(context: FormattingContext): boolean { + switch (context.contextNode.kind) { + case SyntaxKind.BinaryExpression: + return (context.contextNode).operatorToken.kind !== SyntaxKind.CommaToken; + case SyntaxKind.ConditionalExpression: + case SyntaxKind.ConditionalType: + case SyntaxKind.AsExpression: + case SyntaxKind.ExportSpecifier: + case SyntaxKind.ImportSpecifier: + case SyntaxKind.TypePredicate: + case SyntaxKind.UnionType: + case SyntaxKind.IntersectionType: + return true; + // equals in binding elements: function foo([[x, y] = [1, 2]]) + case SyntaxKind.BindingElement: + // equals in type X = ... + // falls through + case SyntaxKind.TypeAliasDeclaration: + // equal in import a = module('a'); + // falls through + case SyntaxKind.ImportEqualsDeclaration: + // equal in let a = 0 + // falls through + case SyntaxKind.VariableDeclaration: + // equal in p = 0 + // falls through + case SyntaxKind.Parameter: + case SyntaxKind.EnumMember: + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.PropertySignature: + return context.currentTokenSpan.kind === SyntaxKind.EqualsToken || context.nextTokenSpan.kind === SyntaxKind.EqualsToken; + // "in" keyword in for (let x in []) { } + case SyntaxKind.ForInStatement: + // "in" keyword in [P in keyof T]: T[P] + // falls through + case SyntaxKind.TypeParameter: + return context.currentTokenSpan.kind === SyntaxKind.InKeyword || context.nextTokenSpan.kind === SyntaxKind.InKeyword || context.currentTokenSpan.kind === SyntaxKind.EqualsToken || context.nextTokenSpan.kind === SyntaxKind.EqualsToken; + // Technically, "of" is not a binary operator, but format it the same way as "in" + case SyntaxKind.ForOfStatement: + return context.currentTokenSpan.kind === SyntaxKind.OfKeyword || context.nextTokenSpan.kind === SyntaxKind.OfKeyword; + } + return false; +} +/* @internal */ +function isNotBinaryOpContext(context: FormattingContext): boolean { + return !isBinaryOpContext(context); +} +/* @internal */ +function isNotTypeAnnotationContext(context: FormattingContext): boolean { + return !isTypeAnnotationContext(context); +} +/* @internal */ +function isTypeAnnotationContext(context: FormattingContext): boolean { + const contextKind = context.contextNode.kind; + return contextKind === SyntaxKind.PropertyDeclaration || + contextKind === SyntaxKind.PropertySignature || + contextKind === SyntaxKind.Parameter || + contextKind === SyntaxKind.VariableDeclaration || + isFunctionLikeKind(contextKind); +} +/* @internal */ +function isConditionalOperatorContext(context: FormattingContext): boolean { + return context.contextNode.kind === SyntaxKind.ConditionalExpression || + context.contextNode.kind === SyntaxKind.ConditionalType; +} +/* @internal */ +function isSameLineTokenOrBeforeBlockContext(context: FormattingContext): boolean { + return context.TokensAreOnSameLine() || isBeforeBlockContext(context); +} +/* @internal */ +function isBraceWrappedContext(context: FormattingContext): boolean { + return context.contextNode.kind === SyntaxKind.ObjectBindingPattern || + context.contextNode.kind === SyntaxKind.MappedType || + isSingleLineBlockContext(context); +} +// This check is done before an open brace in a control construct, a function, or a typescript block declaration +/* @internal */ +function isBeforeMultilineBlockContext(context: FormattingContext): boolean { + return isBeforeBlockContext(context) && !(context.NextNodeAllOnSameLine() || context.NextNodeBlockIsOnOneLine()); +} +/* @internal */ +function isMultilineBlockContext(context: FormattingContext): boolean { + return isBlockContext(context) && !(context.ContextNodeAllOnSameLine() || context.ContextNodeBlockIsOnOneLine()); +} +/* @internal */ +function isSingleLineBlockContext(context: FormattingContext): boolean { + return isBlockContext(context) && (context.ContextNodeAllOnSameLine() || context.ContextNodeBlockIsOnOneLine()); +} +/* @internal */ +function isBlockContext(context: FormattingContext): boolean { + return nodeIsBlockContext(context.contextNode); +} +/* @internal */ +function isBeforeBlockContext(context: FormattingContext): boolean { + return nodeIsBlockContext(context.nextTokenParent); +} +// IMPORTANT!!! This method must return true ONLY for nodes with open and close braces as immediate children +/* @internal */ +function nodeIsBlockContext(node: Node): boolean { + if (nodeIsTypeScriptDeclWithBlockContext(node)) { + // This means we are in a context that looks like a block to the user, but in the grammar is actually not a node (it's a class, module, enum, object type literal, etc). + return true; + } + switch (node.kind) { + case SyntaxKind.Block: + case SyntaxKind.CaseBlock: + case SyntaxKind.ObjectLiteralExpression: + case SyntaxKind.ModuleBlock: return true; - } - - switch (node.kind) { - case SyntaxKind.Block: - case SyntaxKind.CaseBlock: - case SyntaxKind.ObjectLiteralExpression: - case SyntaxKind.ModuleBlock: - return true; - } - - return false; - } - - function isFunctionDeclContext(context: FormattingContext): boolean { - switch (context.contextNode.kind) { - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.MethodSignature: - // case SyntaxKind.MemberFunctionDeclaration: - // falls through - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - // case SyntaxKind.MethodSignature: - // falls through - case SyntaxKind.CallSignature: - case SyntaxKind.FunctionExpression: - case SyntaxKind.Constructor: - case SyntaxKind.ArrowFunction: - // case SyntaxKind.ConstructorDeclaration: - // case SyntaxKind.SimpleArrowFunctionExpression: - // case SyntaxKind.ParenthesizedArrowFunctionExpression: - // falls through - case SyntaxKind.InterfaceDeclaration: // This one is not truly a function, but for formatting purposes, it acts just like one - return true; - } - - return false; - } - - function isNotFunctionDeclContext(context: FormattingContext): boolean { - return !isFunctionDeclContext(context); - } - - function isFunctionDeclarationOrFunctionExpressionContext(context: FormattingContext): boolean { - return context.contextNode.kind === SyntaxKind.FunctionDeclaration || context.contextNode.kind === SyntaxKind.FunctionExpression; } - - function isTypeScriptDeclWithBlockContext(context: FormattingContext): boolean { - return nodeIsTypeScriptDeclWithBlockContext(context.contextNode); + return false; +} +/* @internal */ +function isFunctionDeclContext(context: FormattingContext): boolean { + switch (context.contextNode.kind) { + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: + // case SyntaxKind.MemberFunctionDeclaration: + // falls through + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + // case SyntaxKind.MethodSignature: + // falls through + case SyntaxKind.CallSignature: + case SyntaxKind.FunctionExpression: + case SyntaxKind.Constructor: + case SyntaxKind.ArrowFunction: + // case SyntaxKind.ConstructorDeclaration: + // case SyntaxKind.SimpleArrowFunctionExpression: + // case SyntaxKind.ParenthesizedArrowFunctionExpression: + // falls through + case SyntaxKind.InterfaceDeclaration: // This one is not truly a function, but for formatting purposes, it acts just like one + return true; } - - function nodeIsTypeScriptDeclWithBlockContext(node: Node): boolean { - switch (node.kind) { - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ClassExpression: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.TypeLiteral: - case SyntaxKind.ModuleDeclaration: - case SyntaxKind.ExportDeclaration: - case SyntaxKind.NamedExports: - case SyntaxKind.ImportDeclaration: - case SyntaxKind.NamedImports: - return true; - } - - return false; + return false; +} +/* @internal */ +function isNotFunctionDeclContext(context: FormattingContext): boolean { + return !isFunctionDeclContext(context); +} +/* @internal */ +function isFunctionDeclarationOrFunctionExpressionContext(context: FormattingContext): boolean { + return context.contextNode.kind === SyntaxKind.FunctionDeclaration || context.contextNode.kind === SyntaxKind.FunctionExpression; +} +/* @internal */ +function isTypeScriptDeclWithBlockContext(context: FormattingContext): boolean { + return nodeIsTypeScriptDeclWithBlockContext(context.contextNode); +} +/* @internal */ +function nodeIsTypeScriptDeclWithBlockContext(node: Node): boolean { + switch (node.kind) { + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ClassExpression: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.TypeLiteral: + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.ExportDeclaration: + case SyntaxKind.NamedExports: + case SyntaxKind.ImportDeclaration: + case SyntaxKind.NamedImports: + return true; } - - function isAfterCodeBlockContext(context: FormattingContext): boolean { - switch (context.currentTokenParent.kind) { - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ModuleDeclaration: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.CatchClause: - case SyntaxKind.ModuleBlock: - case SyntaxKind.SwitchStatement: + return false; +} +/* @internal */ +function isAfterCodeBlockContext(context: FormattingContext): boolean { + switch (context.currentTokenParent.kind) { + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.CatchClause: + case SyntaxKind.ModuleBlock: + case SyntaxKind.SwitchStatement: + return true; + case SyntaxKind.Block: { + const blockParent = context.currentTokenParent.parent; + // In a codefix scenario, we can't rely on parents being set. So just always return true. + if (!blockParent || blockParent.kind !== SyntaxKind.ArrowFunction && blockParent.kind !== SyntaxKind.FunctionExpression) { return true; - case SyntaxKind.Block: { - const blockParent = context.currentTokenParent.parent; - // In a codefix scenario, we can't rely on parents being set. So just always return true. - if (!blockParent || blockParent.kind !== SyntaxKind.ArrowFunction && blockParent.kind !== SyntaxKind.FunctionExpression) { - return true; - } } } - return false; } - - function isControlDeclContext(context: FormattingContext): boolean { - switch (context.contextNode.kind) { - case SyntaxKind.IfStatement: - case SyntaxKind.SwitchStatement: - case SyntaxKind.ForStatement: - case SyntaxKind.ForInStatement: - case SyntaxKind.ForOfStatement: - case SyntaxKind.WhileStatement: - case SyntaxKind.TryStatement: - case SyntaxKind.DoStatement: - case SyntaxKind.WithStatement: - // TODO - // case SyntaxKind.ElseClause: - // falls through - case SyntaxKind.CatchClause: - return true; - - default: - return false; - } - } - - function isObjectContext(context: FormattingContext): boolean { - return context.contextNode.kind === SyntaxKind.ObjectLiteralExpression; - } - - function isFunctionCallContext(context: FormattingContext): boolean { - return context.contextNode.kind === SyntaxKind.CallExpression; - } - - function isNewContext(context: FormattingContext): boolean { - return context.contextNode.kind === SyntaxKind.NewExpression; - } - - function isFunctionCallOrNewContext(context: FormattingContext): boolean { - return isFunctionCallContext(context) || isNewContext(context); - } - - function isPreviousTokenNotComma(context: FormattingContext): boolean { - return context.currentTokenSpan.kind !== SyntaxKind.CommaToken; - } - - function isNextTokenNotCloseBracket(context: FormattingContext): boolean { - return context.nextTokenSpan.kind !== SyntaxKind.CloseBracketToken; - } - - function isNextTokenNotCloseParen(context: FormattingContext): boolean { - return context.nextTokenSpan.kind !== SyntaxKind.CloseParenToken; - } - - function isArrowFunctionContext(context: FormattingContext): boolean { - return context.contextNode.kind === SyntaxKind.ArrowFunction; - } - - function isImportTypeContext(context: FormattingContext): boolean { - return context.contextNode.kind === SyntaxKind.ImportType; - } - - function isNonJsxSameLineTokenContext(context: FormattingContext): boolean { - return context.TokensAreOnSameLine() && context.contextNode.kind !== SyntaxKind.JsxText; - } - - function isNonJsxElementOrFragmentContext(context: FormattingContext): boolean { - return context.contextNode.kind !== SyntaxKind.JsxElement && context.contextNode.kind !== SyntaxKind.JsxFragment; - } - - function isJsxExpressionContext(context: FormattingContext): boolean { - return context.contextNode.kind === SyntaxKind.JsxExpression || context.contextNode.kind === SyntaxKind.JsxSpreadAttribute; - } - - function isNextTokenParentJsxAttribute(context: FormattingContext): boolean { - return context.nextTokenParent.kind === SyntaxKind.JsxAttribute; - } - - function isJsxAttributeContext(context: FormattingContext): boolean { - return context.contextNode.kind === SyntaxKind.JsxAttribute; - } - - function isJsxSelfClosingElementContext(context: FormattingContext): boolean { - return context.contextNode.kind === SyntaxKind.JsxSelfClosingElement; - } - - function isNotBeforeBlockInFunctionDeclarationContext(context: FormattingContext): boolean { - return !isFunctionDeclContext(context) && !isBeforeBlockContext(context); - } - - function isEndOfDecoratorContextOnSameLine(context: FormattingContext): boolean { - return context.TokensAreOnSameLine() && - !!context.contextNode.decorators && - nodeIsInDecoratorContext(context.currentTokenParent) && - !nodeIsInDecoratorContext(context.nextTokenParent); - } - - function nodeIsInDecoratorContext(node: Node): boolean { - while (isExpressionNode(node)) { - node = node.parent; - } - return node.kind === SyntaxKind.Decorator; - } - - function isStartOfVariableDeclarationList(context: FormattingContext): boolean { - return context.currentTokenParent.kind === SyntaxKind.VariableDeclarationList && - context.currentTokenParent.getStart(context.sourceFile) === context.currentTokenSpan.pos; - } - - function isNotFormatOnEnter(context: FormattingContext): boolean { - return context.formattingRequestKind !== FormattingRequestKind.FormatOnEnter; - } - - function isModuleDeclContext(context: FormattingContext): boolean { - return context.contextNode.kind === SyntaxKind.ModuleDeclaration; - } - - function isObjectTypeContext(context: FormattingContext): boolean { - return context.contextNode.kind === SyntaxKind.TypeLiteral; // && context.contextNode.parent.kind !== SyntaxKind.InterfaceDeclaration; - } - - function isConstructorSignatureContext(context: FormattingContext): boolean { - return context.contextNode.kind === SyntaxKind.ConstructSignature; - } - - function isTypeArgumentOrParameterOrAssertion(token: TextRangeWithKind, parent: Node): boolean { - if (token.kind !== SyntaxKind.LessThanToken && token.kind !== SyntaxKind.GreaterThanToken) { + return false; +} +/* @internal */ +function isControlDeclContext(context: FormattingContext): boolean { + switch (context.contextNode.kind) { + case SyntaxKind.IfStatement: + case SyntaxKind.SwitchStatement: + case SyntaxKind.ForStatement: + case SyntaxKind.ForInStatement: + case SyntaxKind.ForOfStatement: + case SyntaxKind.WhileStatement: + case SyntaxKind.TryStatement: + case SyntaxKind.DoStatement: + case SyntaxKind.WithStatement: + // TODO + // case SyntaxKind.ElseClause: + // falls through + case SyntaxKind.CatchClause: + return true; + default: return false; - } - switch (parent.kind) { - case SyntaxKind.TypeReference: - case SyntaxKind.TypeAssertionExpression: - case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ClassExpression: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.FunctionExpression: - case SyntaxKind.ArrowFunction: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.MethodSignature: - case SyntaxKind.CallSignature: - case SyntaxKind.ConstructSignature: - case SyntaxKind.CallExpression: - case SyntaxKind.NewExpression: - case SyntaxKind.ExpressionWithTypeArguments: - return true; - default: - return false; - - } - } - - function isTypeArgumentOrParameterOrAssertionContext(context: FormattingContext): boolean { - return isTypeArgumentOrParameterOrAssertion(context.currentTokenSpan, context.currentTokenParent) || - isTypeArgumentOrParameterOrAssertion(context.nextTokenSpan, context.nextTokenParent); - } - - function isTypeAssertionContext(context: FormattingContext): boolean { - return context.contextNode.kind === SyntaxKind.TypeAssertionExpression; } - - function isVoidOpContext(context: FormattingContext): boolean { - return context.currentTokenSpan.kind === SyntaxKind.VoidKeyword && context.currentTokenParent.kind === SyntaxKind.VoidExpression; +} +/* @internal */ +function isObjectContext(context: FormattingContext): boolean { + return context.contextNode.kind === SyntaxKind.ObjectLiteralExpression; +} +/* @internal */ +function isFunctionCallContext(context: FormattingContext): boolean { + return context.contextNode.kind === SyntaxKind.CallExpression; +} +/* @internal */ +function isNewContext(context: FormattingContext): boolean { + return context.contextNode.kind === SyntaxKind.NewExpression; +} +/* @internal */ +function isFunctionCallOrNewContext(context: FormattingContext): boolean { + return isFunctionCallContext(context) || isNewContext(context); +} +/* @internal */ +function isPreviousTokenNotComma(context: FormattingContext): boolean { + return context.currentTokenSpan.kind !== SyntaxKind.CommaToken; +} +/* @internal */ +function isNextTokenNotCloseBracket(context: FormattingContext): boolean { + return context.nextTokenSpan.kind !== SyntaxKind.CloseBracketToken; +} +/* @internal */ +function isNextTokenNotCloseParen(context: FormattingContext): boolean { + return context.nextTokenSpan.kind !== SyntaxKind.CloseParenToken; +} +/* @internal */ +function isArrowFunctionContext(context: FormattingContext): boolean { + return context.contextNode.kind === SyntaxKind.ArrowFunction; +} +/* @internal */ +function isImportTypeContext(context: FormattingContext): boolean { + return context.contextNode.kind === SyntaxKind.ImportType; +} +/* @internal */ +function isNonJsxSameLineTokenContext(context: FormattingContext): boolean { + return context.TokensAreOnSameLine() && context.contextNode.kind !== SyntaxKind.JsxText; +} +/* @internal */ +function isNonJsxElementOrFragmentContext(context: FormattingContext): boolean { + return context.contextNode.kind !== SyntaxKind.JsxElement && context.contextNode.kind !== SyntaxKind.JsxFragment; +} +/* @internal */ +function isJsxExpressionContext(context: FormattingContext): boolean { + return context.contextNode.kind === SyntaxKind.JsxExpression || context.contextNode.kind === SyntaxKind.JsxSpreadAttribute; +} +/* @internal */ +function isNextTokenParentJsxAttribute(context: FormattingContext): boolean { + return context.nextTokenParent.kind === SyntaxKind.JsxAttribute; +} +/* @internal */ +function isJsxAttributeContext(context: FormattingContext): boolean { + return context.contextNode.kind === SyntaxKind.JsxAttribute; +} +/* @internal */ +function isJsxSelfClosingElementContext(context: FormattingContext): boolean { + return context.contextNode.kind === SyntaxKind.JsxSelfClosingElement; +} +/* @internal */ +function isNotBeforeBlockInFunctionDeclarationContext(context: FormattingContext): boolean { + return !isFunctionDeclContext(context) && !isBeforeBlockContext(context); +} +/* @internal */ +function isEndOfDecoratorContextOnSameLine(context: FormattingContext): boolean { + return context.TokensAreOnSameLine() && + !!context.contextNode.decorators && + nodeIsInDecoratorContext(context.currentTokenParent) && + !nodeIsInDecoratorContext(context.nextTokenParent); +} +/* @internal */ +function nodeIsInDecoratorContext(node: Node): boolean { + while (isExpressionNode(node)) { + node = node.parent; } - - function isYieldOrYieldStarWithOperand(context: FormattingContext): boolean { - return context.contextNode.kind === SyntaxKind.YieldExpression && (context.contextNode).expression !== undefined; + return node.kind === SyntaxKind.Decorator; +} +/* @internal */ +function isStartOfVariableDeclarationList(context: FormattingContext): boolean { + return context.currentTokenParent.kind === SyntaxKind.VariableDeclarationList && + context.currentTokenParent.getStart(context.sourceFile) === context.currentTokenSpan.pos; +} +/* @internal */ +function isNotFormatOnEnter(context: FormattingContext): boolean { + return context.formattingRequestKind !== FormattingRequestKind.FormatOnEnter; +} +/* @internal */ +function isModuleDeclContext(context: FormattingContext): boolean { + return context.contextNode.kind === SyntaxKind.ModuleDeclaration; +} +/* @internal */ +function isObjectTypeContext(context: FormattingContext): boolean { + return context.contextNode.kind === SyntaxKind.TypeLiteral; // && context.contextNode.parent.kind !== SyntaxKind.InterfaceDeclaration; +} +/* @internal */ +function isConstructorSignatureContext(context: FormattingContext): boolean { + return context.contextNode.kind === SyntaxKind.ConstructSignature; +} +/* @internal */ +function isTypeArgumentOrParameterOrAssertion(token: TextRangeWithKind, parent: Node): boolean { + if (token.kind !== SyntaxKind.LessThanToken && token.kind !== SyntaxKind.GreaterThanToken) { + return false; } - - function isNonNullAssertionContext(context: FormattingContext): boolean { - return context.contextNode.kind === SyntaxKind.NonNullExpression; + switch (parent.kind) { + case SyntaxKind.TypeReference: + case SyntaxKind.TypeAssertionExpression: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ClassExpression: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.FunctionExpression: + case SyntaxKind.ArrowFunction: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: + case SyntaxKind.CallSignature: + case SyntaxKind.ConstructSignature: + case SyntaxKind.CallExpression: + case SyntaxKind.NewExpression: + case SyntaxKind.ExpressionWithTypeArguments: + return true; + default: + return false; } - - function isNotStatementConditionContext(context: FormattingContext): boolean { - return !isStatementConditionContext(context); +} +/* @internal */ +function isTypeArgumentOrParameterOrAssertionContext(context: FormattingContext): boolean { + return isTypeArgumentOrParameterOrAssertion(context.currentTokenSpan, context.currentTokenParent) || + isTypeArgumentOrParameterOrAssertion(context.nextTokenSpan, context.nextTokenParent); +} +/* @internal */ +function isTypeAssertionContext(context: FormattingContext): boolean { + return context.contextNode.kind === SyntaxKind.TypeAssertionExpression; +} +/* @internal */ +function isVoidOpContext(context: FormattingContext): boolean { + return context.currentTokenSpan.kind === SyntaxKind.VoidKeyword && context.currentTokenParent.kind === SyntaxKind.VoidExpression; +} +/* @internal */ +function isYieldOrYieldStarWithOperand(context: FormattingContext): boolean { + return context.contextNode.kind === SyntaxKind.YieldExpression && (context.contextNode).expression !== undefined; +} +/* @internal */ +function isNonNullAssertionContext(context: FormattingContext): boolean { + return context.contextNode.kind === SyntaxKind.NonNullExpression; +} +/* @internal */ +function isNotStatementConditionContext(context: FormattingContext): boolean { + return !isStatementConditionContext(context); +} +/* @internal */ +function isStatementConditionContext(context: FormattingContext): boolean { + switch (context.contextNode.kind) { + case SyntaxKind.IfStatement: + case SyntaxKind.ForStatement: + case SyntaxKind.ForInStatement: + case SyntaxKind.ForOfStatement: + case SyntaxKind.DoStatement: + case SyntaxKind.WhileStatement: + return true; + default: + return false; } - - function isStatementConditionContext(context: FormattingContext): boolean { - switch (context.contextNode.kind) { - case SyntaxKind.IfStatement: - case SyntaxKind.ForStatement: - case SyntaxKind.ForInStatement: - case SyntaxKind.ForOfStatement: - case SyntaxKind.DoStatement: - case SyntaxKind.WhileStatement: - return true; - - default: - return false; +} +/* @internal */ +function isSemicolonDeletionContext(context: FormattingContext): boolean { + let nextTokenKind = context.nextTokenSpan.kind; + let nextTokenStart = context.nextTokenSpan.pos; + if (isTrivia(nextTokenKind)) { + const nextRealToken = context.nextTokenParent === context.currentTokenParent + ? findNextToken(context.currentTokenParent, (findAncestor(context.currentTokenParent, a => !a.parent)!), context.sourceFile) + : context.nextTokenParent.getFirstToken(context.sourceFile); + if (!nextRealToken) { + return true; } + nextTokenKind = nextRealToken.kind; + nextTokenStart = nextRealToken.getStart(context.sourceFile); } - - function isSemicolonDeletionContext(context: FormattingContext): boolean { - let nextTokenKind = context.nextTokenSpan.kind; - let nextTokenStart = context.nextTokenSpan.pos; - if (isTrivia(nextTokenKind)) { - const nextRealToken = context.nextTokenParent === context.currentTokenParent - ? findNextToken( - context.currentTokenParent, - findAncestor(context.currentTokenParent, a => !a.parent)!, - context.sourceFile) - : context.nextTokenParent.getFirstToken(context.sourceFile); - if (!nextRealToken) { - return true; - } - nextTokenKind = nextRealToken.kind; - nextTokenStart = nextRealToken.getStart(context.sourceFile); - } - - const startLine = context.sourceFile.getLineAndCharacterOfPosition(context.currentTokenSpan.pos).line; - const endLine = context.sourceFile.getLineAndCharacterOfPosition(nextTokenStart).line; - if (startLine === endLine) { - return nextTokenKind === SyntaxKind.CloseBraceToken - || nextTokenKind === SyntaxKind.EndOfFileToken; - } - - if (nextTokenKind === SyntaxKind.SemicolonClassElement || - nextTokenKind === SyntaxKind.SemicolonToken - ) { - return false; - } - - if (context.contextNode.kind === SyntaxKind.InterfaceDeclaration || - context.contextNode.kind === SyntaxKind.TypeAliasDeclaration - ) { - // Can’t remove semicolon after `foo`; it would parse as a method declaration: - // - // interface I { - // foo; - // (): void - // } - return !isPropertySignature(context.currentTokenParent) - || !!context.currentTokenParent.type - || nextTokenKind !== SyntaxKind.OpenParenToken; - } - - if (isPropertyDeclaration(context.currentTokenParent)) { - return !context.currentTokenParent.initializer; - } - - return context.currentTokenParent.kind !== SyntaxKind.ForStatement - && context.currentTokenParent.kind !== SyntaxKind.EmptyStatement - && context.currentTokenParent.kind !== SyntaxKind.SemicolonClassElement - && nextTokenKind !== SyntaxKind.OpenBracketToken - && nextTokenKind !== SyntaxKind.OpenParenToken - && nextTokenKind !== SyntaxKind.PlusToken - && nextTokenKind !== SyntaxKind.MinusToken - && nextTokenKind !== SyntaxKind.SlashToken - && nextTokenKind !== SyntaxKind.RegularExpressionLiteral - && nextTokenKind !== SyntaxKind.CommaToken - && nextTokenKind !== SyntaxKind.TemplateExpression - && nextTokenKind !== SyntaxKind.TemplateHead - && nextTokenKind !== SyntaxKind.NoSubstitutionTemplateLiteral - && nextTokenKind !== SyntaxKind.DotToken; + const startLine = context.sourceFile.getLineAndCharacterOfPosition(context.currentTokenSpan.pos).line; + const endLine = context.sourceFile.getLineAndCharacterOfPosition(nextTokenStart).line; + if (startLine === endLine) { + return nextTokenKind === SyntaxKind.CloseBraceToken + || nextTokenKind === SyntaxKind.EndOfFileToken; } - - function isSemicolonInsertionContext(context: FormattingContext): boolean { - return positionIsASICandidate(context.currentTokenSpan.end, context.currentTokenParent, context.sourceFile); + if (nextTokenKind === SyntaxKind.SemicolonClassElement || + nextTokenKind === SyntaxKind.SemicolonToken) { + return false; } + if (context.contextNode.kind === SyntaxKind.InterfaceDeclaration || + context.contextNode.kind === SyntaxKind.TypeAliasDeclaration) { + // Can’t remove semicolon after `foo`; it would parse as a method declaration: + // + // interface I { + // foo; + // (): void + // } + return !isPropertySignature(context.currentTokenParent) + || !!context.currentTokenParent.type + || nextTokenKind !== SyntaxKind.OpenParenToken; + } + if (isPropertyDeclaration(context.currentTokenParent)) { + return !context.currentTokenParent.initializer; + } + return context.currentTokenParent.kind !== SyntaxKind.ForStatement + && context.currentTokenParent.kind !== SyntaxKind.EmptyStatement + && context.currentTokenParent.kind !== SyntaxKind.SemicolonClassElement + && nextTokenKind !== SyntaxKind.OpenBracketToken + && nextTokenKind !== SyntaxKind.OpenParenToken + && nextTokenKind !== SyntaxKind.PlusToken + && nextTokenKind !== SyntaxKind.MinusToken + && nextTokenKind !== SyntaxKind.SlashToken + && nextTokenKind !== SyntaxKind.RegularExpressionLiteral + && nextTokenKind !== SyntaxKind.CommaToken + && nextTokenKind !== SyntaxKind.TemplateExpression + && nextTokenKind !== SyntaxKind.TemplateHead + && nextTokenKind !== SyntaxKind.NoSubstitutionTemplateLiteral + && nextTokenKind !== SyntaxKind.DotToken; +} +/* @internal */ +function isSemicolonInsertionContext(context: FormattingContext): boolean { + return positionIsASICandidate(context.currentTokenSpan.end, context.currentTokenParent, context.sourceFile); } diff --git a/src/services/formatting/rulesMap.ts b/src/services/formatting/rulesMap.ts index f466b397d20dd..cc2179300530d 100644 --- a/src/services/formatting/rulesMap.ts +++ b/src/services/formatting/rulesMap.ts @@ -1,140 +1,141 @@ +import { FormatCodeSettings, every, Debug, SyntaxKind } from "../ts"; +import { FormatContext, getAllRules, RuleAction, FormattingContext, Rule, RuleSpec, anyContext } from "../ts.formatting"; /* @internal */ -namespace ts.formatting { - export function getFormatContext(options: FormatCodeSettings): FormatContext { - return { options, getRules: getRulesMap() }; +export function getFormatContext(options: FormatCodeSettings): FormatContext { + return { options, getRules: getRulesMap() }; +} +/* @internal */ +let rulesMapCache: RulesMap | undefined; +/* @internal */ +function getRulesMap(): RulesMap { + if (rulesMapCache === undefined) { + rulesMapCache = createRulesMap(getAllRules()); } - - let rulesMapCache: RulesMap | undefined; - - function getRulesMap(): RulesMap { - if (rulesMapCache === undefined) { - rulesMapCache = createRulesMap(getAllRules()); - } - return rulesMapCache; + return rulesMapCache; +} +/** + * For a given rule action, gets a mask of other rule actions that + * cannot be applied at the same position. + */ +/* @internal */ +function getRuleActionExclusion(ruleAction: RuleAction): RuleAction { + let mask: RuleAction = 0; + if (ruleAction & RuleAction.StopProcessingSpaceActions) { + mask |= RuleAction.ModifySpaceAction; } - - /** - * For a given rule action, gets a mask of other rule actions that - * cannot be applied at the same position. - */ - function getRuleActionExclusion(ruleAction: RuleAction): RuleAction { - let mask: RuleAction = 0; - if (ruleAction & RuleAction.StopProcessingSpaceActions) { - mask |= RuleAction.ModifySpaceAction; - } - if (ruleAction & RuleAction.StopProcessingTokenActions) { - mask |= RuleAction.ModifyTokenAction; - } - if (ruleAction & RuleAction.ModifySpaceAction) { - mask |= RuleAction.ModifySpaceAction; - } - if (ruleAction & RuleAction.ModifyTokenAction) { - mask |= RuleAction.ModifyTokenAction; - } - return mask; + if (ruleAction & RuleAction.StopProcessingTokenActions) { + mask |= RuleAction.ModifyTokenAction; } - - export type RulesMap = (context: FormattingContext) => readonly Rule[] | undefined; - function createRulesMap(rules: readonly RuleSpec[]): RulesMap { - const map = buildMap(rules); - return context => { - const bucket = map[getRuleBucketIndex(context.currentTokenSpan.kind, context.nextTokenSpan.kind)]; - if (bucket) { - const rules: Rule[] = []; - let ruleActionMask: RuleAction = 0; - for (const rule of bucket) { - const acceptRuleActions = ~getRuleActionExclusion(ruleActionMask); - if (rule.action & acceptRuleActions && every(rule.context, c => c(context))) { - rules.push(rule); - ruleActionMask |= rule.action; - } - } - if (rules.length) { - return rules; - } - } - }; + if (ruleAction & RuleAction.ModifySpaceAction) { + mask |= RuleAction.ModifySpaceAction; } - - function buildMap(rules: readonly RuleSpec[]): readonly (readonly Rule[])[] { - // Map from bucket index to array of rules - const map: Rule[][] = new Array(mapRowLength * mapRowLength); - // This array is used only during construction of the rulesbucket in the map - const rulesBucketConstructionStateList = new Array(map.length); - for (const rule of rules) { - const specificRule = rule.leftTokenRange.isSpecific && rule.rightTokenRange.isSpecific; - - for (const left of rule.leftTokenRange.tokens) { - for (const right of rule.rightTokenRange.tokens) { - const index = getRuleBucketIndex(left, right); - let rulesBucket = map[index]; - if (rulesBucket === undefined) { - rulesBucket = map[index] = []; - } - addRule(rulesBucket, rule.rule, specificRule, rulesBucketConstructionStateList, index); + if (ruleAction & RuleAction.ModifyTokenAction) { + mask |= RuleAction.ModifyTokenAction; + } + return mask; +} +/* @internal */ +export type RulesMap = (context: FormattingContext) => readonly Rule[] | undefined; +/* @internal */ +function createRulesMap(rules: readonly RuleSpec[]): RulesMap { + const map = buildMap(rules); + return context => { + const bucket = map[getRuleBucketIndex(context.currentTokenSpan.kind, context.nextTokenSpan.kind)]; + if (bucket) { + const rules: Rule[] = []; + let ruleActionMask: RuleAction = 0; + for (const rule of bucket) { + const acceptRuleActions = ~getRuleActionExclusion(ruleActionMask); + if (rule.action & acceptRuleActions && every(rule.context, c => c(context))) { + rules.push(rule); + ruleActionMask |= rule.action; } } + if (rules.length) { + return rules; + } } - return map; - } - - function getRuleBucketIndex(row: number, column: number): number { - Debug.assert(row <= SyntaxKind.LastKeyword && column <= SyntaxKind.LastKeyword, "Must compute formatting context from tokens"); - return (row * mapRowLength) + column; - } - - const maskBitSize = 5; - const mask = 0b11111; // MaskBitSize bits - const mapRowLength = SyntaxKind.LastToken + 1; - - enum RulesPosition { - StopRulesSpecific = 0, - StopRulesAny = maskBitSize * 1, - ContextRulesSpecific = maskBitSize * 2, - ContextRulesAny = maskBitSize * 3, - NoContextRulesSpecific = maskBitSize * 4, - NoContextRulesAny = maskBitSize * 5 - } - - // The Rules list contains all the inserted rules into a rulebucket in the following order: - // 1- Ignore rules with specific token combination - // 2- Ignore rules with any token combination - // 3- Context rules with specific token combination - // 4- Context rules with any token combination - // 5- Non-context rules with specific token combination - // 6- Non-context rules with any token combination - // - // The member rulesInsertionIndexBitmap is used to describe the number of rules - // in each sub-bucket (above) hence can be used to know the index of where to insert - // the next rule. It's a bitmap which contains 6 different sections each is given 5 bits. - // - // Example: - // In order to insert a rule to the end of sub-bucket (3), we get the index by adding - // the values in the bitmap segments 3rd, 2nd, and 1st. - function addRule(rules: Rule[], rule: Rule, specificTokens: boolean, constructionState: number[], rulesBucketIndex: number): void { - const position = rule.action & RuleAction.StopAction ? - specificTokens ? RulesPosition.StopRulesSpecific : RulesPosition.StopRulesAny : - rule.context !== anyContext ? - specificTokens ? RulesPosition.ContextRulesSpecific : RulesPosition.ContextRulesAny : - specificTokens ? RulesPosition.NoContextRulesSpecific : RulesPosition.NoContextRulesAny; - - const state = constructionState[rulesBucketIndex] || 0; - rules.splice(getInsertionIndex(state, position), 0, rule); - constructionState[rulesBucketIndex] = increaseInsertionIndex(state, position); - } - - function getInsertionIndex(indexBitmap: number, maskPosition: RulesPosition) { - let index = 0; - for (let pos = 0; pos <= maskPosition; pos += maskBitSize) { - index += indexBitmap & mask; - indexBitmap >>= maskBitSize; + }; +} +/* @internal */ +function buildMap(rules: readonly RuleSpec[]): readonly (readonly Rule[])[] { + // Map from bucket index to array of rules + const map: Rule[][] = new Array(mapRowLength * mapRowLength); + // This array is used only during construction of the rulesbucket in the map + const rulesBucketConstructionStateList = new Array(map.length); + for (const rule of rules) { + const specificRule = rule.leftTokenRange.isSpecific && rule.rightTokenRange.isSpecific; + for (const left of rule.leftTokenRange.tokens) { + for (const right of rule.rightTokenRange.tokens) { + const index = getRuleBucketIndex(left, right); + let rulesBucket = map[index]; + if (rulesBucket === undefined) { + rulesBucket = map[index] = []; + } + addRule(rulesBucket, rule.rule, specificRule, rulesBucketConstructionStateList, index); + } } - return index; } - - function increaseInsertionIndex(indexBitmap: number, maskPosition: RulesPosition): number { - const value = ((indexBitmap >> maskPosition) & mask) + 1; - Debug.assert((value & mask) === value, "Adding more rules into the sub-bucket than allowed. Maximum allowed is 32 rules."); - return (indexBitmap & ~(mask << maskPosition)) | (value << maskPosition); + return map; +} +/* @internal */ +function getRuleBucketIndex(row: number, column: number): number { + Debug.assert(row <= SyntaxKind.LastKeyword && column <= SyntaxKind.LastKeyword, "Must compute formatting context from tokens"); + return (row * mapRowLength) + column; +} +/* @internal */ +const maskBitSize = 5; +/* @internal */ +const mask = 0b11111; // MaskBitSize bits +/* @internal */ +const mapRowLength = SyntaxKind.LastToken + 1; +/* @internal */ +enum RulesPosition { + StopRulesSpecific = 0, + StopRulesAny = maskBitSize * 1, + ContextRulesSpecific = maskBitSize * 2, + ContextRulesAny = maskBitSize * 3, + NoContextRulesSpecific = maskBitSize * 4, + NoContextRulesAny = maskBitSize * 5 +} +// The Rules list contains all the inserted rules into a rulebucket in the following order: +// 1- Ignore rules with specific token combination +// 2- Ignore rules with any token combination +// 3- Context rules with specific token combination +// 4- Context rules with any token combination +// 5- Non-context rules with specific token combination +// 6- Non-context rules with any token combination +// +// The member rulesInsertionIndexBitmap is used to describe the number of rules +// in each sub-bucket (above) hence can be used to know the index of where to insert +// the next rule. It's a bitmap which contains 6 different sections each is given 5 bits. +// +// Example: +// In order to insert a rule to the end of sub-bucket (3), we get the index by adding +// the values in the bitmap segments 3rd, 2nd, and 1st. +/* @internal */ +function addRule(rules: Rule[], rule: Rule, specificTokens: boolean, constructionState: number[], rulesBucketIndex: number): void { + const position = rule.action & RuleAction.StopAction ? + specificTokens ? RulesPosition.StopRulesSpecific : RulesPosition.StopRulesAny : + rule.context !== anyContext ? + specificTokens ? RulesPosition.ContextRulesSpecific : RulesPosition.ContextRulesAny : + specificTokens ? RulesPosition.NoContextRulesSpecific : RulesPosition.NoContextRulesAny; + const state = constructionState[rulesBucketIndex] || 0; + rules.splice(getInsertionIndex(state, position), 0, rule); + constructionState[rulesBucketIndex] = increaseInsertionIndex(state, position); +} +/* @internal */ +function getInsertionIndex(indexBitmap: number, maskPosition: RulesPosition) { + let index = 0; + for (let pos = 0; pos <= maskPosition; pos += maskBitSize) { + index += indexBitmap & mask; + indexBitmap >>= maskBitSize; } + return index; +} +/* @internal */ +function increaseInsertionIndex(indexBitmap: number, maskPosition: RulesPosition): number { + const value = ((indexBitmap >> maskPosition) & mask) + 1; + Debug.assert((value & mask) === value, "Adding more rules into the sub-bucket than allowed. Maximum allowed is 32 rules."); + return (indexBitmap & ~(mask << maskPosition)) | (value << maskPosition); } diff --git a/src/services/formatting/smartIndenter.ts b/src/services/formatting/smartIndenter.ts index 5539a8e30cb2d..7915ed5e7fa29 100644 --- a/src/services/formatting/smartIndenter.ts +++ b/src/services/formatting/smartIndenter.ts @@ -1,614 +1,525 @@ +import { SourceFile, EditorSettings, IndentStyle, findPrecedingToken, SyntaxKind, isStringOrRegularExpressionOrTemplateLiteral, rangeContainsRange, CommentRange, getLineAndCharacterOfPosition, Debug, getStartPositionOfLine, CharacterCodes, isWhiteSpaceLike, getLineStartPositionForPosition, Node, positionBelongsToNode, TextRange, LineAndCharacter, findListItemInfo, isDeclaration, isStatementButNotDeclaration, findNextToken, SourceFileLike, isCallExpression, contains, IfStatement, findChildOfKind, isCallOrNewExpression, find, NodeArray, TypeReferenceNode, ObjectLiteralExpression, ArrayLiteralExpression, TypeLiteralNode, SignatureDeclaration, ClassDeclaration, ClassExpression, InterfaceDeclaration, TypeAliasDeclaration, JSDocTemplateTag, CallExpression, VariableDeclarationList, NamedImportsOrExports, ObjectBindingPattern, ArrayBindingPattern, rangeContainsStartEnd, isWhiteSpaceSingleLine, FormatCodeSettings, ImportClause, skipTrivia } from "../ts"; +import { getRangeOfEnclosingComment, TextRangeWithKind } from "../ts.formatting"; /* @internal */ -namespace ts.formatting { - export namespace SmartIndenter { - - const enum Value { - Unknown = -1 - } - - /** - * @param assumeNewLineBeforeCloseBrace - * `false` when called on text from a real source file. - * `true` when we need to assume `position` is on a newline. - * - * This is useful for codefixes. Consider - * ``` - * function f() { - * |} - * ``` - * with `position` at `|`. - * - * When inserting some text after an open brace, we would like to get indentation as if a newline was already there. - * By default indentation at `position` will be 0 so 'assumeNewLineBeforeCloseBrace' overrides this behavior. - */ - export function getIndentation(position: number, sourceFile: SourceFile, options: EditorSettings, assumeNewLineBeforeCloseBrace = false): number { - if (position > sourceFile.text.length) { - return getBaseIndentation(options); // past EOF - } - - // no indentation when the indent style is set to none, - // so we can return fast - if (options.indentStyle === IndentStyle.None) { - return 0; - } - - const precedingToken = findPrecedingToken(position, sourceFile, /*startNode*/ undefined, /*excludeJsdoc*/ true); - - // eslint-disable-next-line no-null/no-null - const enclosingCommentRange = getRangeOfEnclosingComment(sourceFile, position, precedingToken || null); - if (enclosingCommentRange && enclosingCommentRange.kind === SyntaxKind.MultiLineCommentTrivia) { - return getCommentIndent(sourceFile, position, options, enclosingCommentRange); - } - - if (!precedingToken) { - return getBaseIndentation(options); - } - - // no indentation in string \regex\template literals - const precedingTokenIsLiteral = isStringOrRegularExpressionOrTemplateLiteral(precedingToken.kind); - if (precedingTokenIsLiteral && precedingToken.getStart(sourceFile) <= position && position < precedingToken.end) { - return 0; - } - - const lineAtPosition = sourceFile.getLineAndCharacterOfPosition(position).line; - - // indentation is first non-whitespace character in a previous line - // for block indentation, we should look for a line which contains something that's not - // whitespace. - if (options.indentStyle === IndentStyle.Block) { - return getBlockIndent(sourceFile, position, options); +export namespace SmartIndenter { + const enum Value { + Unknown = -1 + } + /** + * @param assumeNewLineBeforeCloseBrace + * `false` when called on text from a real source file. + * `true` when we need to assume `position` is on a newline. + * + * This is useful for codefixes. Consider + * ``` + * function f() { + * |} + * ``` + * with `position` at `|`. + * + * When inserting some text after an open brace, we would like to get indentation as if a newline was already there. + * By default indentation at `position` will be 0 so 'assumeNewLineBeforeCloseBrace' overrides this behavior. + */ + export function getIndentation(position: number, sourceFile: SourceFile, options: EditorSettings, assumeNewLineBeforeCloseBrace = false): number { + if (position > sourceFile.text.length) { + return getBaseIndentation(options); // past EOF + } + // no indentation when the indent style is set to none, + // so we can return fast + if (options.indentStyle === IndentStyle.None) { + return 0; + } + const precedingToken = findPrecedingToken(position, sourceFile, /*startNode*/ undefined, /*excludeJsdoc*/ true); + // eslint-disable-next-line no-null/no-null + const enclosingCommentRange = getRangeOfEnclosingComment(sourceFile, position, precedingToken || null); + if (enclosingCommentRange && enclosingCommentRange.kind === SyntaxKind.MultiLineCommentTrivia) { + return getCommentIndent(sourceFile, position, options, enclosingCommentRange); + } + if (!precedingToken) { + return getBaseIndentation(options); + } + // no indentation in string \regex\template literals + const precedingTokenIsLiteral = isStringOrRegularExpressionOrTemplateLiteral(precedingToken.kind); + if (precedingTokenIsLiteral && precedingToken.getStart(sourceFile) <= position && position < precedingToken.end) { + return 0; + } + const lineAtPosition = sourceFile.getLineAndCharacterOfPosition(position).line; + // indentation is first non-whitespace character in a previous line + // for block indentation, we should look for a line which contains something that's not + // whitespace. + if (options.indentStyle === IndentStyle.Block) { + return getBlockIndent(sourceFile, position, options); + } + if (precedingToken.kind === SyntaxKind.CommaToken && precedingToken.parent.kind !== SyntaxKind.BinaryExpression) { + // previous token is comma that separates items in list - find the previous item and try to derive indentation from it + const actualIndentation = getActualIndentationForListItemBeforeComma(precedingToken, sourceFile, options); + if (actualIndentation !== Value.Unknown) { + return actualIndentation; } - - if (precedingToken.kind === SyntaxKind.CommaToken && precedingToken.parent.kind !== SyntaxKind.BinaryExpression) { - // previous token is comma that separates items in list - find the previous item and try to derive indentation from it - const actualIndentation = getActualIndentationForListItemBeforeComma(precedingToken, sourceFile, options); + } + const containerList = getListByPosition(position, precedingToken.parent, sourceFile); + // use list position if the preceding token is before any list items + if (containerList && !rangeContainsRange(containerList, precedingToken)) { + return getActualIndentationForListStartLine(containerList, sourceFile, options) + options.indentSize!; // TODO: GH#18217 + } + return getSmartIndent(sourceFile, position, precedingToken, lineAtPosition, assumeNewLineBeforeCloseBrace, options); + } + function getCommentIndent(sourceFile: SourceFile, position: number, options: EditorSettings, enclosingCommentRange: CommentRange): number { + const previousLine = getLineAndCharacterOfPosition(sourceFile, position).line - 1; + const commentStartLine = getLineAndCharacterOfPosition(sourceFile, enclosingCommentRange.pos).line; + Debug.assert(commentStartLine >= 0); + if (previousLine <= commentStartLine) { + return findFirstNonWhitespaceColumn(getStartPositionOfLine(commentStartLine, sourceFile), position, sourceFile, options); + } + const startPositionOfLine = getStartPositionOfLine(previousLine, sourceFile); + const { column, character } = findFirstNonWhitespaceCharacterAndColumn(startPositionOfLine, position, sourceFile, options); + if (column === 0) { + return column; + } + const firstNonWhitespaceCharacterCode = sourceFile.text.charCodeAt(startPositionOfLine + character); + return firstNonWhitespaceCharacterCode === CharacterCodes.asterisk ? column - 1 : column; + } + function getBlockIndent(sourceFile: SourceFile, position: number, options: EditorSettings): number { + // move backwards until we find a line with a non-whitespace character, + // then find the first non-whitespace character for that line. + let current = position; + while (current > 0) { + const char = sourceFile.text.charCodeAt(current); + if (!isWhiteSpaceLike(char)) { + break; + } + current--; + } + const lineStart = getLineStartPositionForPosition(current, sourceFile); + return findFirstNonWhitespaceColumn(lineStart, current, sourceFile, options); + } + function getSmartIndent(sourceFile: SourceFile, position: number, precedingToken: Node, lineAtPosition: number, assumeNewLineBeforeCloseBrace: boolean, options: EditorSettings): number { + // try to find node that can contribute to indentation and includes 'position' starting from 'precedingToken' + // if such node is found - compute initial indentation for 'position' inside this node + let previous: Node | undefined; + let current = precedingToken; + while (current) { + if (positionBelongsToNode(current, position, sourceFile) && shouldIndentChildNode(options, current, previous, sourceFile, /*isNextChild*/ true)) { + const currentStart = getStartLineAndCharacterForNode(current, sourceFile); + const nextTokenKind = nextTokenIsCurlyBraceOnSameLineAsCursor(precedingToken, current, lineAtPosition, sourceFile); + const indentationDelta = nextTokenKind !== NextTokenKind.Unknown + // handle cases when codefix is about to be inserted before the close brace + ? assumeNewLineBeforeCloseBrace && nextTokenKind === NextTokenKind.CloseBrace ? options.indentSize : 0 + : lineAtPosition !== currentStart.line ? options.indentSize : 0; + return getIndentationForNodeWorker(current, currentStart, /*ignoreActualIndentationRange*/ undefined, indentationDelta!, sourceFile, /*isNextChild*/ true, options); // TODO: GH#18217 + } + // check if current node is a list item - if yes, take indentation from it + // do not consider parent-child line sharing yet: + // function foo(a + // | preceding node 'a' does share line with its parent but indentation is expected + const actualIndentation = getActualIndentationForListItem(current, sourceFile, options, /*listIndentsChild*/ true); + if (actualIndentation !== Value.Unknown) { + return actualIndentation; + } + previous = current; + current = current.parent; + } + // no parent was found - return the base indentation of the SourceFile + return getBaseIndentation(options); + } + export function getIndentationForNode(n: Node, ignoreActualIndentationRange: TextRange, sourceFile: SourceFile, options: EditorSettings): number { + const start = sourceFile.getLineAndCharacterOfPosition(n.getStart(sourceFile)); + return getIndentationForNodeWorker(n, start, ignoreActualIndentationRange, /*indentationDelta*/ 0, sourceFile, /*isNextChild*/ false, options); + } + export function getBaseIndentation(options: EditorSettings) { + return options.baseIndentSize || 0; + } + function getIndentationForNodeWorker(current: Node, currentStart: LineAndCharacter, ignoreActualIndentationRange: TextRange | undefined, indentationDelta: number, sourceFile: SourceFile, isNextChild: boolean, options: EditorSettings): number { + let parent = current.parent; + // Walk up the tree and collect indentation for parent-child node pairs. Indentation is not added if + // * parent and child nodes start on the same line, or + // * parent is an IfStatement and child starts on the same line as an 'else clause'. + while (parent) { + let useActualIndentation = true; + if (ignoreActualIndentationRange) { + const start = current.getStart(sourceFile); + useActualIndentation = start < ignoreActualIndentationRange.pos || start > ignoreActualIndentationRange.end; + } + const containingListOrParentStart = getContainingListOrParentStart(parent, current, sourceFile); + const parentAndChildShareLine = containingListOrParentStart.line === currentStart.line || + childStartsOnTheSameLineWithElseInIfStatement(parent, current, currentStart.line, sourceFile); + if (useActualIndentation) { + // check if current node is a list item - if yes, take indentation from it + let actualIndentation = getActualIndentationForListItem(current, sourceFile, options, !parentAndChildShareLine); if (actualIndentation !== Value.Unknown) { - return actualIndentation; + return actualIndentation + indentationDelta; } - } - - const containerList = getListByPosition(position, precedingToken.parent, sourceFile); - // use list position if the preceding token is before any list items - if (containerList && !rangeContainsRange(containerList, precedingToken)) { - return getActualIndentationForListStartLine(containerList, sourceFile, options) + options.indentSize!; // TODO: GH#18217 - } - - return getSmartIndent(sourceFile, position, precedingToken, lineAtPosition, assumeNewLineBeforeCloseBrace, options); - } - - function getCommentIndent(sourceFile: SourceFile, position: number, options: EditorSettings, enclosingCommentRange: CommentRange): number { - const previousLine = getLineAndCharacterOfPosition(sourceFile, position).line - 1; - const commentStartLine = getLineAndCharacterOfPosition(sourceFile, enclosingCommentRange.pos).line; - - Debug.assert(commentStartLine >= 0); - - if (previousLine <= commentStartLine) { - return findFirstNonWhitespaceColumn(getStartPositionOfLine(commentStartLine, sourceFile), position, sourceFile, options); - } - - const startPositionOfLine = getStartPositionOfLine(previousLine, sourceFile); - const { column, character } = findFirstNonWhitespaceCharacterAndColumn(startPositionOfLine, position, sourceFile, options); - - if (column === 0) { - return column; - } - - const firstNonWhitespaceCharacterCode = sourceFile.text.charCodeAt(startPositionOfLine + character); - return firstNonWhitespaceCharacterCode === CharacterCodes.asterisk ? column - 1 : column; - } - - function getBlockIndent(sourceFile: SourceFile, position: number, options: EditorSettings): number { - // move backwards until we find a line with a non-whitespace character, - // then find the first non-whitespace character for that line. - let current = position; - while (current > 0) { - const char = sourceFile.text.charCodeAt(current); - if (!isWhiteSpaceLike(char)) { - break; - } - current--; - } - - const lineStart = getLineStartPositionForPosition(current, sourceFile); - return findFirstNonWhitespaceColumn(lineStart, current, sourceFile, options); - } - - function getSmartIndent(sourceFile: SourceFile, position: number, precedingToken: Node, lineAtPosition: number, assumeNewLineBeforeCloseBrace: boolean, options: EditorSettings): number { - // try to find node that can contribute to indentation and includes 'position' starting from 'precedingToken' - // if such node is found - compute initial indentation for 'position' inside this node - let previous: Node | undefined; - let current = precedingToken; - - while (current) { - if (positionBelongsToNode(current, position, sourceFile) && shouldIndentChildNode(options, current, previous, sourceFile, /*isNextChild*/ true)) { - const currentStart = getStartLineAndCharacterForNode(current, sourceFile); - const nextTokenKind = nextTokenIsCurlyBraceOnSameLineAsCursor(precedingToken, current, lineAtPosition, sourceFile); - const indentationDelta = nextTokenKind !== NextTokenKind.Unknown - // handle cases when codefix is about to be inserted before the close brace - ? assumeNewLineBeforeCloseBrace && nextTokenKind === NextTokenKind.CloseBrace ? options.indentSize : 0 - : lineAtPosition !== currentStart.line ? options.indentSize : 0; - return getIndentationForNodeWorker(current, currentStart, /*ignoreActualIndentationRange*/ undefined, indentationDelta!, sourceFile, /*isNextChild*/ true, options); // TODO: GH#18217 - } - - // check if current node is a list item - if yes, take indentation from it - // do not consider parent-child line sharing yet: - // function foo(a - // | preceding node 'a' does share line with its parent but indentation is expected - const actualIndentation = getActualIndentationForListItem(current, sourceFile, options, /*listIndentsChild*/ true); + // try to fetch actual indentation for current node from source text + actualIndentation = getActualIndentationForNode(current, parent, currentStart, parentAndChildShareLine, sourceFile, options); if (actualIndentation !== Value.Unknown) { - return actualIndentation; + return actualIndentation + indentationDelta; } - - previous = current; - current = current.parent; } - // no parent was found - return the base indentation of the SourceFile - return getBaseIndentation(options); + // increase indentation if parent node wants its content to be indented and parent and child nodes don't start on the same line + if (shouldIndentChildNode(options, parent, current, sourceFile, isNextChild) && !parentAndChildShareLine) { + indentationDelta += options.indentSize!; + } + // In our AST, a call argument's `parent` is the call-expression, not the argument list. + // We would like to increase indentation based on the relationship between an argument and its argument-list, + // so we spoof the starting position of the (parent) call-expression to match the (non-parent) argument-list. + // But, the spoofed start-value could then cause a problem when comparing the start position of the call-expression + // to *its* parent (in the case of an iife, an expression statement), adding an extra level of indentation. + // + // Instead, when at an argument, we unspoof the starting position of the enclosing call expression + // *after* applying indentation for the argument. + const useTrueStart = isArgumentAndStartLineOverlapsExpressionBeingCalled(parent, current, currentStart.line, sourceFile); + current = parent; + parent = current.parent; + currentStart = useTrueStart ? sourceFile.getLineAndCharacterOfPosition(current.getStart(sourceFile)) : containingListOrParentStart; } - - export function getIndentationForNode(n: Node, ignoreActualIndentationRange: TextRange, sourceFile: SourceFile, options: EditorSettings): number { - const start = sourceFile.getLineAndCharacterOfPosition(n.getStart(sourceFile)); - return getIndentationForNodeWorker(n, start, ignoreActualIndentationRange, /*indentationDelta*/ 0, sourceFile, /*isNextChild*/ false, options); - } - - export function getBaseIndentation(options: EditorSettings) { - return options.baseIndentSize || 0; - } - - function getIndentationForNodeWorker( - current: Node, - currentStart: LineAndCharacter, - ignoreActualIndentationRange: TextRange | undefined, - indentationDelta: number, - sourceFile: SourceFile, - isNextChild: boolean, - options: EditorSettings): number { - let parent = current.parent; - - // Walk up the tree and collect indentation for parent-child node pairs. Indentation is not added if - // * parent and child nodes start on the same line, or - // * parent is an IfStatement and child starts on the same line as an 'else clause'. - while (parent) { - let useActualIndentation = true; - if (ignoreActualIndentationRange) { - const start = current.getStart(sourceFile); - useActualIndentation = start < ignoreActualIndentationRange.pos || start > ignoreActualIndentationRange.end; - } - - const containingListOrParentStart = getContainingListOrParentStart(parent, current, sourceFile); - const parentAndChildShareLine = - containingListOrParentStart.line === currentStart.line || - childStartsOnTheSameLineWithElseInIfStatement(parent, current, currentStart.line, sourceFile); - - if (useActualIndentation) { - // check if current node is a list item - if yes, take indentation from it - let actualIndentation = getActualIndentationForListItem(current, sourceFile, options, !parentAndChildShareLine); - if (actualIndentation !== Value.Unknown) { - return actualIndentation + indentationDelta; - } - - // try to fetch actual indentation for current node from source text - actualIndentation = getActualIndentationForNode(current, parent, currentStart, parentAndChildShareLine, sourceFile, options); - if (actualIndentation !== Value.Unknown) { - return actualIndentation + indentationDelta; - } - } - - // increase indentation if parent node wants its content to be indented and parent and child nodes don't start on the same line - if (shouldIndentChildNode(options, parent, current, sourceFile, isNextChild) && !parentAndChildShareLine) { - indentationDelta += options.indentSize!; - } - - // In our AST, a call argument's `parent` is the call-expression, not the argument list. - // We would like to increase indentation based on the relationship between an argument and its argument-list, - // so we spoof the starting position of the (parent) call-expression to match the (non-parent) argument-list. - // But, the spoofed start-value could then cause a problem when comparing the start position of the call-expression - // to *its* parent (in the case of an iife, an expression statement), adding an extra level of indentation. - // - // Instead, when at an argument, we unspoof the starting position of the enclosing call expression - // *after* applying indentation for the argument. - - const useTrueStart = - isArgumentAndStartLineOverlapsExpressionBeingCalled(parent, current, currentStart.line, sourceFile); - - current = parent; - parent = current.parent; - currentStart = useTrueStart ? sourceFile.getLineAndCharacterOfPosition(current.getStart(sourceFile)) : containingListOrParentStart; - } - - return indentationDelta + getBaseIndentation(options); - } - - function getContainingListOrParentStart(parent: Node, child: Node, sourceFile: SourceFile): LineAndCharacter { - const containingList = getContainingList(child, sourceFile); - const startPos = containingList ? containingList.pos : parent.getStart(sourceFile); - return sourceFile.getLineAndCharacterOfPosition(startPos); - } - - /* - * Function returns Value.Unknown if indentation cannot be determined - */ - function getActualIndentationForListItemBeforeComma(commaToken: Node, sourceFile: SourceFile, options: EditorSettings): number { - // previous token is comma that separates items in list - find the previous item and try to derive indentation from it - const commaItemInfo = findListItemInfo(commaToken); - if (commaItemInfo && commaItemInfo.listItemIndex > 0) { - return deriveActualIndentationFromList(commaItemInfo.list.getChildren(), commaItemInfo.listItemIndex - 1, sourceFile, options); - } - else { - // handle broken code gracefully - return Value.Unknown; - } + return indentationDelta + getBaseIndentation(options); + } + function getContainingListOrParentStart(parent: Node, child: Node, sourceFile: SourceFile): LineAndCharacter { + const containingList = getContainingList(child, sourceFile); + const startPos = containingList ? containingList.pos : parent.getStart(sourceFile); + return sourceFile.getLineAndCharacterOfPosition(startPos); + } + /* + * Function returns Value.Unknown if indentation cannot be determined + */ + function getActualIndentationForListItemBeforeComma(commaToken: Node, sourceFile: SourceFile, options: EditorSettings): number { + // previous token is comma that separates items in list - find the previous item and try to derive indentation from it + const commaItemInfo = findListItemInfo(commaToken); + if (commaItemInfo && commaItemInfo.listItemIndex > 0) { + return deriveActualIndentationFromList(commaItemInfo.list.getChildren(), commaItemInfo.listItemIndex - 1, sourceFile, options); } - - /* - * Function returns Value.Unknown if actual indentation for node should not be used (i.e because node is nested expression) - */ - function getActualIndentationForNode(current: Node, - parent: Node, - currentLineAndChar: LineAndCharacter, - parentAndChildShareLine: boolean, - sourceFile: SourceFile, - options: EditorSettings): number { - - // actual indentation is used for statements\declarations if one of cases below is true: - // - parent is SourceFile - by default immediate children of SourceFile are not indented except when user indents them manually - // - parent and child are not on the same line - const useActualIndentation = - (isDeclaration(current) || isStatementButNotDeclaration(current)) && - (parent.kind === SyntaxKind.SourceFile || !parentAndChildShareLine); - - if (!useActualIndentation) { - return Value.Unknown; - } - - return findColumnForFirstNonWhitespaceCharacterInLine(currentLineAndChar, sourceFile, options); - } - - const enum NextTokenKind { - Unknown, - OpenBrace, - CloseBrace - } - - function nextTokenIsCurlyBraceOnSameLineAsCursor(precedingToken: Node, current: Node, lineAtPosition: number, sourceFile: SourceFile): NextTokenKind { - const nextToken = findNextToken(precedingToken, current, sourceFile); - if (!nextToken) { - return NextTokenKind.Unknown; - } - - if (nextToken.kind === SyntaxKind.OpenBraceToken) { - // open braces are always indented at the parent level - return NextTokenKind.OpenBrace; - } - else if (nextToken.kind === SyntaxKind.CloseBraceToken) { - // close braces are indented at the parent level if they are located on the same line with cursor - // this means that if new line will be added at $ position, this case will be indented - // class A { - // $ - // } - /// and this one - not - // class A { - // $} - - const nextTokenStartLine = getStartLineAndCharacterForNode(nextToken, sourceFile).line; - return lineAtPosition === nextTokenStartLine ? NextTokenKind.CloseBrace : NextTokenKind.Unknown; - } - + else { + // handle broken code gracefully + return Value.Unknown; + } + } + /* + * Function returns Value.Unknown if actual indentation for node should not be used (i.e because node is nested expression) + */ + function getActualIndentationForNode(current: Node, parent: Node, currentLineAndChar: LineAndCharacter, parentAndChildShareLine: boolean, sourceFile: SourceFile, options: EditorSettings): number { + // actual indentation is used for statements\declarations if one of cases below is true: + // - parent is SourceFile - by default immediate children of SourceFile are not indented except when user indents them manually + // - parent and child are not on the same line + const useActualIndentation = (isDeclaration(current) || isStatementButNotDeclaration(current)) && + (parent.kind === SyntaxKind.SourceFile || !parentAndChildShareLine); + if (!useActualIndentation) { + return Value.Unknown; + } + return findColumnForFirstNonWhitespaceCharacterInLine(currentLineAndChar, sourceFile, options); + } + const enum NextTokenKind { + Unknown, + OpenBrace, + CloseBrace + } + function nextTokenIsCurlyBraceOnSameLineAsCursor(precedingToken: Node, current: Node, lineAtPosition: number, sourceFile: SourceFile): NextTokenKind { + const nextToken = findNextToken(precedingToken, current, sourceFile); + if (!nextToken) { return NextTokenKind.Unknown; } - - function getStartLineAndCharacterForNode(n: Node, sourceFile: SourceFileLike): LineAndCharacter { - return sourceFile.getLineAndCharacterOfPosition(n.getStart(sourceFile)); + if (nextToken.kind === SyntaxKind.OpenBraceToken) { + // open braces are always indented at the parent level + return NextTokenKind.OpenBrace; } - - export function isArgumentAndStartLineOverlapsExpressionBeingCalled(parent: Node, child: Node, childStartLine: number, sourceFile: SourceFileLike): boolean { - if (!(isCallExpression(parent) && contains(parent.arguments, child))) { - return false; - } - - const expressionOfCallExpressionEnd = parent.expression.getEnd(); - const expressionOfCallExpressionEndLine = getLineAndCharacterOfPosition(sourceFile, expressionOfCallExpressionEnd).line; - return expressionOfCallExpressionEndLine === childStartLine; - } - - export function childStartsOnTheSameLineWithElseInIfStatement(parent: Node, child: TextRangeWithKind, childStartLine: number, sourceFile: SourceFileLike): boolean { - if (parent.kind === SyntaxKind.IfStatement && (parent).elseStatement === child) { - const elseKeyword = findChildOfKind(parent, SyntaxKind.ElseKeyword, sourceFile)!; - Debug.assert(elseKeyword !== undefined); - - const elseKeywordStartLine = getStartLineAndCharacterForNode(elseKeyword, sourceFile).line; - return elseKeywordStartLine === childStartLine; - } - - return false; + else if (nextToken.kind === SyntaxKind.CloseBraceToken) { + // close braces are indented at the parent level if they are located on the same line with cursor + // this means that if new line will be added at $ position, this case will be indented + // class A { + // $ + // } + /// and this one - not + // class A { + // $} + const nextTokenStartLine = getStartLineAndCharacterForNode(nextToken, sourceFile).line; + return lineAtPosition === nextTokenStartLine ? NextTokenKind.CloseBrace : NextTokenKind.Unknown; } - - export function argumentStartsOnSameLineAsPreviousArgument(parent: Node, child: TextRangeWithKind, childStartLine: number, sourceFile: SourceFileLike): boolean { - if (isCallOrNewExpression(parent)) { - if (!parent.arguments) return false; - const currentNode = find(parent.arguments, arg => arg.pos === child.pos); - // If it's not one of the arguments, don't look past this - if (!currentNode) return false; - const currentIndex = parent.arguments.indexOf(currentNode); - if (currentIndex === 0) return false; // Can't look at previous node if first - - const previousNode = parent.arguments[currentIndex - 1]; - const lineOfPreviousNode = getLineAndCharacterOfPosition(sourceFile, previousNode.getEnd()).line; - - if (childStartLine === lineOfPreviousNode) { - return true; - } - } - + return NextTokenKind.Unknown; + } + function getStartLineAndCharacterForNode(n: Node, sourceFile: SourceFileLike): LineAndCharacter { + return sourceFile.getLineAndCharacterOfPosition(n.getStart(sourceFile)); + } + export function isArgumentAndStartLineOverlapsExpressionBeingCalled(parent: Node, child: Node, childStartLine: number, sourceFile: SourceFileLike): boolean { + if (!(isCallExpression(parent) && contains(parent.arguments, child))) { return false; } - - export function getContainingList(node: Node, sourceFile: SourceFile): NodeArray | undefined { - return node.parent && getListByRange(node.getStart(sourceFile), node.getEnd(), node.parent, sourceFile); - } - - function getListByPosition(pos: number, node: Node, sourceFile: SourceFile): NodeArray | undefined { - return node && getListByRange(pos, pos, node, sourceFile); - } - - function getListByRange(start: number, end: number, node: Node, sourceFile: SourceFile): NodeArray | undefined { - switch (node.kind) { - case SyntaxKind.TypeReference: - return getList((node).typeArguments); - case SyntaxKind.ObjectLiteralExpression: - return getList((node).properties); - case SyntaxKind.ArrayLiteralExpression: - return getList((node).elements); - case SyntaxKind.TypeLiteral: - return getList((node).members); - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.FunctionExpression: - case SyntaxKind.ArrowFunction: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.MethodSignature: - case SyntaxKind.CallSignature: - case SyntaxKind.Constructor: - case SyntaxKind.ConstructorType: - case SyntaxKind.ConstructSignature: - return getList((node).typeParameters) || getList((node).parameters); - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ClassExpression: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.JSDocTemplateTag: - return getList((node).typeParameters); - case SyntaxKind.NewExpression: - case SyntaxKind.CallExpression: - return getList((node).typeArguments) || getList((node).arguments); - case SyntaxKind.VariableDeclarationList: - return getList((node).declarations); - case SyntaxKind.NamedImports: - case SyntaxKind.NamedExports: - return getList((node).elements); - case SyntaxKind.ObjectBindingPattern: - case SyntaxKind.ArrayBindingPattern: - return getList((node).elements); - } - - function getList(list: NodeArray | undefined): NodeArray | undefined { - return list && rangeContainsStartEnd(getVisualListRange(node, list, sourceFile), start, end) ? list : undefined; - } + const expressionOfCallExpressionEnd = parent.expression.getEnd(); + const expressionOfCallExpressionEndLine = getLineAndCharacterOfPosition(sourceFile, expressionOfCallExpressionEnd).line; + return expressionOfCallExpressionEndLine === childStartLine; + } + export function childStartsOnTheSameLineWithElseInIfStatement(parent: Node, child: TextRangeWithKind, childStartLine: number, sourceFile: SourceFileLike): boolean { + if (parent.kind === SyntaxKind.IfStatement && (parent).elseStatement === child) { + const elseKeyword = (findChildOfKind(parent, SyntaxKind.ElseKeyword, sourceFile)!); + Debug.assert(elseKeyword !== undefined); + const elseKeywordStartLine = getStartLineAndCharacterForNode(elseKeyword, sourceFile).line; + return elseKeywordStartLine === childStartLine; } - - function getVisualListRange(node: Node, list: TextRange, sourceFile: SourceFile): TextRange { - const children = node.getChildren(sourceFile); - for (let i = 1; i < children.length - 1; i++) { - if (children[i].pos === list.pos && children[i].end === list.end) { - return { pos: children[i - 1].end, end: children[i + 1].getStart(sourceFile) }; - } + return false; + } + export function argumentStartsOnSameLineAsPreviousArgument(parent: Node, child: TextRangeWithKind, childStartLine: number, sourceFile: SourceFileLike): boolean { + if (isCallOrNewExpression(parent)) { + if (!parent.arguments) + return false; + const currentNode = find(parent.arguments, arg => arg.pos === child.pos); + // If it's not one of the arguments, don't look past this + if (!currentNode) + return false; + const currentIndex = parent.arguments.indexOf(currentNode); + if (currentIndex === 0) + return false; // Can't look at previous node if first + const previousNode = parent.arguments[currentIndex - 1]; + const lineOfPreviousNode = getLineAndCharacterOfPosition(sourceFile, previousNode.getEnd()).line; + if (childStartLine === lineOfPreviousNode) { + return true; } - return list; } - - function getActualIndentationForListStartLine(list: NodeArray, sourceFile: SourceFile, options: EditorSettings): number { - if (!list) { - return Value.Unknown; - } - return findColumnForFirstNonWhitespaceCharacterInLine(sourceFile.getLineAndCharacterOfPosition(list.pos), sourceFile, options); + return false; + } + export function getContainingList(node: Node, sourceFile: SourceFile): NodeArray | undefined { + return node.parent && getListByRange(node.getStart(sourceFile), node.getEnd(), node.parent, sourceFile); + } + function getListByPosition(pos: number, node: Node, sourceFile: SourceFile): NodeArray | undefined { + return node && getListByRange(pos, pos, node, sourceFile); + } + function getListByRange(start: number, end: number, node: Node, sourceFile: SourceFile): NodeArray | undefined { + switch (node.kind) { + case SyntaxKind.TypeReference: + return getList((node).typeArguments); + case SyntaxKind.ObjectLiteralExpression: + return getList((node).properties); + case SyntaxKind.ArrayLiteralExpression: + return getList((node).elements); + case SyntaxKind.TypeLiteral: + return getList((node).members); + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.FunctionExpression: + case SyntaxKind.ArrowFunction: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: + case SyntaxKind.CallSignature: + case SyntaxKind.Constructor: + case SyntaxKind.ConstructorType: + case SyntaxKind.ConstructSignature: + return getList((node).typeParameters) || getList((node).parameters); + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ClassExpression: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.JSDocTemplateTag: + return getList((node).typeParameters); + case SyntaxKind.NewExpression: + case SyntaxKind.CallExpression: + return getList((node).typeArguments) || getList((node).arguments); + case SyntaxKind.VariableDeclarationList: + return getList((node).declarations); + case SyntaxKind.NamedImports: + case SyntaxKind.NamedExports: + return getList((node).elements); + case SyntaxKind.ObjectBindingPattern: + case SyntaxKind.ArrayBindingPattern: + return getList((node).elements); } - - function getActualIndentationForListItem(node: Node, sourceFile: SourceFile, options: EditorSettings, listIndentsChild: boolean): number { - if (node.parent && node.parent.kind === SyntaxKind.VariableDeclarationList) { - // VariableDeclarationList has no wrapping tokens - return Value.Unknown; - } - const containingList = getContainingList(node, sourceFile); - if (containingList) { - const index = containingList.indexOf(node); - if (index !== -1) { - const result = deriveActualIndentationFromList(containingList, index, sourceFile, options); - if (result !== Value.Unknown) { - return result; - } - } - return getActualIndentationForListStartLine(containingList, sourceFile, options) + (listIndentsChild ? options.indentSize! : 0); // TODO: GH#18217 + function getList(list: NodeArray | undefined): NodeArray | undefined { + return list && rangeContainsStartEnd(getVisualListRange(node, list, sourceFile), start, end) ? list : undefined; + } + } + function getVisualListRange(node: Node, list: TextRange, sourceFile: SourceFile): TextRange { + const children = node.getChildren(sourceFile); + for (let i = 1; i < children.length - 1; i++) { + if (children[i].pos === list.pos && children[i].end === list.end) { + return { pos: children[i - 1].end, end: children[i + 1].getStart(sourceFile) }; } + } + return list; + } + function getActualIndentationForListStartLine(list: NodeArray, sourceFile: SourceFile, options: EditorSettings): number { + if (!list) { return Value.Unknown; } - - function deriveActualIndentationFromList(list: readonly Node[], index: number, sourceFile: SourceFile, options: EditorSettings): number { - Debug.assert(index >= 0 && index < list.length); - const node = list[index]; - - // walk toward the start of the list starting from current node and check if the line is the same for all items. - // if end line for item [i - 1] differs from the start line for item [i] - find column of the first non-whitespace character on the line of item [i] - let lineAndCharacter = getStartLineAndCharacterForNode(node, sourceFile); - for (let i = index - 1; i >= 0; i--) { - if (list[i].kind === SyntaxKind.CommaToken) { - continue; - } - // skip list items that ends on the same line with the current list element - const prevEndLine = sourceFile.getLineAndCharacterOfPosition(list[i].end).line; - if (prevEndLine !== lineAndCharacter.line) { - return findColumnForFirstNonWhitespaceCharacterInLine(lineAndCharacter, sourceFile, options); - } - - lineAndCharacter = getStartLineAndCharacterForNode(list[i], sourceFile); - } + return findColumnForFirstNonWhitespaceCharacterInLine(sourceFile.getLineAndCharacterOfPosition(list.pos), sourceFile, options); + } + function getActualIndentationForListItem(node: Node, sourceFile: SourceFile, options: EditorSettings, listIndentsChild: boolean): number { + if (node.parent && node.parent.kind === SyntaxKind.VariableDeclarationList) { + // VariableDeclarationList has no wrapping tokens return Value.Unknown; } - - function findColumnForFirstNonWhitespaceCharacterInLine(lineAndCharacter: LineAndCharacter, sourceFile: SourceFile, options: EditorSettings): number { - const lineStart = sourceFile.getPositionOfLineAndCharacter(lineAndCharacter.line, 0); - return findFirstNonWhitespaceColumn(lineStart, lineStart + lineAndCharacter.character, sourceFile, options); - } - - /** - * Character is the actual index of the character since the beginning of the line. - * Column - position of the character after expanding tabs to spaces. - * "0\t2$" - * value of 'character' for '$' is 3 - * value of 'column' for '$' is 6 (assuming that tab size is 4) - */ - export function findFirstNonWhitespaceCharacterAndColumn(startPos: number, endPos: number, sourceFile: SourceFileLike, options: EditorSettings) { - let character = 0; - let column = 0; - for (let pos = startPos; pos < endPos; pos++) { - const ch = sourceFile.text.charCodeAt(pos); - if (!isWhiteSpaceSingleLine(ch)) { - break; - } - - if (ch === CharacterCodes.tab) { - column += options.tabSize! + (column % options.tabSize!); - } - else { - column++; + const containingList = getContainingList(node, sourceFile); + if (containingList) { + const index = containingList.indexOf(node); + if (index !== -1) { + const result = deriveActualIndentationFromList(containingList, index, sourceFile, options); + if (result !== Value.Unknown) { + return result; } - - character++; } - return { column, character }; - } - - export function findFirstNonWhitespaceColumn(startPos: number, endPos: number, sourceFile: SourceFileLike, options: EditorSettings): number { - return findFirstNonWhitespaceCharacterAndColumn(startPos, endPos, sourceFile, options).column; - } - - export function nodeWillIndentChild(settings: FormatCodeSettings, parent: TextRangeWithKind, child: TextRangeWithKind | undefined, sourceFile: SourceFileLike | undefined, indentByDefault: boolean): boolean { - const childKind = child ? child.kind : SyntaxKind.Unknown; - - switch (parent.kind) { - case SyntaxKind.ExpressionStatement: - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ClassExpression: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.ArrayLiteralExpression: - case SyntaxKind.Block: - case SyntaxKind.ModuleBlock: - case SyntaxKind.ObjectLiteralExpression: - case SyntaxKind.TypeLiteral: - case SyntaxKind.MappedType: - case SyntaxKind.TupleType: - case SyntaxKind.CaseBlock: - case SyntaxKind.DefaultClause: - case SyntaxKind.CaseClause: - case SyntaxKind.ParenthesizedExpression: - case SyntaxKind.PropertyAccessExpression: - case SyntaxKind.CallExpression: - case SyntaxKind.NewExpression: - case SyntaxKind.VariableStatement: - case SyntaxKind.ExportAssignment: - case SyntaxKind.ReturnStatement: - case SyntaxKind.ConditionalExpression: - case SyntaxKind.ArrayBindingPattern: - case SyntaxKind.ObjectBindingPattern: - case SyntaxKind.JsxOpeningElement: - case SyntaxKind.JsxOpeningFragment: - case SyntaxKind.JsxSelfClosingElement: - case SyntaxKind.JsxExpression: - case SyntaxKind.MethodSignature: - case SyntaxKind.CallSignature: - case SyntaxKind.ConstructSignature: - case SyntaxKind.Parameter: - case SyntaxKind.FunctionType: - case SyntaxKind.ConstructorType: - case SyntaxKind.ParenthesizedType: - case SyntaxKind.TaggedTemplateExpression: - case SyntaxKind.AwaitExpression: - case SyntaxKind.NamedExports: - case SyntaxKind.NamedImports: - case SyntaxKind.ExportSpecifier: - case SyntaxKind.ImportSpecifier: - case SyntaxKind.PropertyDeclaration: - return true; - case SyntaxKind.VariableDeclaration: - case SyntaxKind.PropertyAssignment: - case SyntaxKind.BinaryExpression: - if (!settings.indentMultiLineObjectLiteralBeginningOnBlankLine && sourceFile && childKind === SyntaxKind.ObjectLiteralExpression) { // TODO: GH#18217 - return rangeIsOnOneLine(sourceFile, child!); - } - if (parent.kind !== SyntaxKind.BinaryExpression) { - return true; - } - break; - case SyntaxKind.DoStatement: - case SyntaxKind.WhileStatement: - case SyntaxKind.ForInStatement: - case SyntaxKind.ForOfStatement: - case SyntaxKind.ForStatement: - case SyntaxKind.IfStatement: - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.FunctionExpression: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.ArrowFunction: - case SyntaxKind.Constructor: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - return childKind !== SyntaxKind.Block; - case SyntaxKind.ExportDeclaration: - return childKind !== SyntaxKind.NamedExports; - case SyntaxKind.ImportDeclaration: - return childKind !== SyntaxKind.ImportClause || - (!!(child).namedBindings && (child).namedBindings!.kind !== SyntaxKind.NamedImports); - case SyntaxKind.JsxElement: - return childKind !== SyntaxKind.JsxClosingElement; - case SyntaxKind.JsxFragment: - return childKind !== SyntaxKind.JsxClosingFragment; - case SyntaxKind.IntersectionType: - case SyntaxKind.UnionType: - if (childKind === SyntaxKind.TypeLiteral) { - return false; - } - // falls through + return getActualIndentationForListStartLine(containingList, sourceFile, options) + (listIndentsChild ? options.indentSize! : 0); // TODO: GH#18217 + } + return Value.Unknown; + } + function deriveActualIndentationFromList(list: readonly Node[], index: number, sourceFile: SourceFile, options: EditorSettings): number { + Debug.assert(index >= 0 && index < list.length); + const node = list[index]; + // walk toward the start of the list starting from current node and check if the line is the same for all items. + // if end line for item [i - 1] differs from the start line for item [i] - find column of the first non-whitespace character on the line of item [i] + let lineAndCharacter = getStartLineAndCharacterForNode(node, sourceFile); + for (let i = index - 1; i >= 0; i--) { + if (list[i].kind === SyntaxKind.CommaToken) { + continue; + } + // skip list items that ends on the same line with the current list element + const prevEndLine = sourceFile.getLineAndCharacterOfPosition(list[i].end).line; + if (prevEndLine !== lineAndCharacter.line) { + return findColumnForFirstNonWhitespaceCharacterInLine(lineAndCharacter, sourceFile, options); + } + lineAndCharacter = getStartLineAndCharacterForNode(list[i], sourceFile); + } + return Value.Unknown; + } + function findColumnForFirstNonWhitespaceCharacterInLine(lineAndCharacter: LineAndCharacter, sourceFile: SourceFile, options: EditorSettings): number { + const lineStart = sourceFile.getPositionOfLineAndCharacter(lineAndCharacter.line, 0); + return findFirstNonWhitespaceColumn(lineStart, lineStart + lineAndCharacter.character, sourceFile, options); + } + /** + * Character is the actual index of the character since the beginning of the line. + * Column - position of the character after expanding tabs to spaces. + * "0\t2$" + * value of 'character' for '$' is 3 + * value of 'column' for '$' is 6 (assuming that tab size is 4) + */ + export function findFirstNonWhitespaceCharacterAndColumn(startPos: number, endPos: number, sourceFile: SourceFileLike, options: EditorSettings) { + let character = 0; + let column = 0; + for (let pos = startPos; pos < endPos; pos++) { + const ch = sourceFile.text.charCodeAt(pos); + if (!isWhiteSpaceSingleLine(ch)) { + break; + } + if (ch === CharacterCodes.tab) { + column += options.tabSize! + (column % options.tabSize!); } - // No explicit rule for given nodes so the result will follow the default value argument - return indentByDefault; - } - - function isControlFlowEndingStatement(kind: SyntaxKind, parent: TextRangeWithKind): boolean { - switch (kind) { - case SyntaxKind.ReturnStatement: - case SyntaxKind.ThrowStatement: - case SyntaxKind.ContinueStatement: - case SyntaxKind.BreakStatement: - return parent.kind !== SyntaxKind.Block; - default: - return false; + else { + column++; } + character++; } - - /** - * True when the parent node should indent the given child by an explicit rule. - * @param isNextChild If true, we are judging indent of a hypothetical child *after* this one, not the current child. - */ - export function shouldIndentChildNode(settings: FormatCodeSettings, parent: TextRangeWithKind, child?: Node, sourceFile?: SourceFileLike, isNextChild = false): boolean { - return nodeWillIndentChild(settings, parent, child, sourceFile, /*indentByDefault*/ false) - && !(isNextChild && child && isControlFlowEndingStatement(child.kind, parent)); - } - - function rangeIsOnOneLine(sourceFile: SourceFileLike, range: TextRangeWithKind) { - const rangeStart = skipTrivia(sourceFile.text, range.pos); - const startLine = sourceFile.getLineAndCharacterOfPosition(rangeStart).line; - const endLine = sourceFile.getLineAndCharacterOfPosition(range.end).line; - return startLine === endLine; + return { column, character }; + } + export function findFirstNonWhitespaceColumn(startPos: number, endPos: number, sourceFile: SourceFileLike, options: EditorSettings): number { + return findFirstNonWhitespaceCharacterAndColumn(startPos, endPos, sourceFile, options).column; + } + export function nodeWillIndentChild(settings: FormatCodeSettings, parent: TextRangeWithKind, child: TextRangeWithKind | undefined, sourceFile: SourceFileLike | undefined, indentByDefault: boolean): boolean { + const childKind = child ? child.kind : SyntaxKind.Unknown; + switch (parent.kind) { + case SyntaxKind.ExpressionStatement: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ClassExpression: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.ArrayLiteralExpression: + case SyntaxKind.Block: + case SyntaxKind.ModuleBlock: + case SyntaxKind.ObjectLiteralExpression: + case SyntaxKind.TypeLiteral: + case SyntaxKind.MappedType: + case SyntaxKind.TupleType: + case SyntaxKind.CaseBlock: + case SyntaxKind.DefaultClause: + case SyntaxKind.CaseClause: + case SyntaxKind.ParenthesizedExpression: + case SyntaxKind.PropertyAccessExpression: + case SyntaxKind.CallExpression: + case SyntaxKind.NewExpression: + case SyntaxKind.VariableStatement: + case SyntaxKind.ExportAssignment: + case SyntaxKind.ReturnStatement: + case SyntaxKind.ConditionalExpression: + case SyntaxKind.ArrayBindingPattern: + case SyntaxKind.ObjectBindingPattern: + case SyntaxKind.JsxOpeningElement: + case SyntaxKind.JsxOpeningFragment: + case SyntaxKind.JsxSelfClosingElement: + case SyntaxKind.JsxExpression: + case SyntaxKind.MethodSignature: + case SyntaxKind.CallSignature: + case SyntaxKind.ConstructSignature: + case SyntaxKind.Parameter: + case SyntaxKind.FunctionType: + case SyntaxKind.ConstructorType: + case SyntaxKind.ParenthesizedType: + case SyntaxKind.TaggedTemplateExpression: + case SyntaxKind.AwaitExpression: + case SyntaxKind.NamedExports: + case SyntaxKind.NamedImports: + case SyntaxKind.ExportSpecifier: + case SyntaxKind.ImportSpecifier: + case SyntaxKind.PropertyDeclaration: + return true; + case SyntaxKind.VariableDeclaration: + case SyntaxKind.PropertyAssignment: + case SyntaxKind.BinaryExpression: + if (!settings.indentMultiLineObjectLiteralBeginningOnBlankLine && sourceFile && childKind === SyntaxKind.ObjectLiteralExpression) { // TODO: GH#18217 + return rangeIsOnOneLine(sourceFile, child!); + } + if (parent.kind !== SyntaxKind.BinaryExpression) { + return true; + } + break; + case SyntaxKind.DoStatement: + case SyntaxKind.WhileStatement: + case SyntaxKind.ForInStatement: + case SyntaxKind.ForOfStatement: + case SyntaxKind.ForStatement: + case SyntaxKind.IfStatement: + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.FunctionExpression: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.ArrowFunction: + case SyntaxKind.Constructor: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + return childKind !== SyntaxKind.Block; + case SyntaxKind.ExportDeclaration: + return childKind !== SyntaxKind.NamedExports; + case SyntaxKind.ImportDeclaration: + return childKind !== SyntaxKind.ImportClause || + (!!(child).namedBindings && (child).namedBindings!.kind !== SyntaxKind.NamedImports); + case SyntaxKind.JsxElement: + return childKind !== SyntaxKind.JsxClosingElement; + case SyntaxKind.JsxFragment: + return childKind !== SyntaxKind.JsxClosingFragment; + case SyntaxKind.IntersectionType: + case SyntaxKind.UnionType: + if (childKind === SyntaxKind.TypeLiteral) { + return false; + } + // falls through + } + // No explicit rule for given nodes so the result will follow the default value argument + return indentByDefault; + } + function isControlFlowEndingStatement(kind: SyntaxKind, parent: TextRangeWithKind): boolean { + switch (kind) { + case SyntaxKind.ReturnStatement: + case SyntaxKind.ThrowStatement: + case SyntaxKind.ContinueStatement: + case SyntaxKind.BreakStatement: + return parent.kind !== SyntaxKind.Block; + default: + return false; } } + /** + * True when the parent node should indent the given child by an explicit rule. + * @param isNextChild If true, we are judging indent of a hypothetical child *after* this one, not the current child. + */ + export function shouldIndentChildNode(settings: FormatCodeSettings, parent: TextRangeWithKind, child?: Node, sourceFile?: SourceFileLike, isNextChild = false): boolean { + return nodeWillIndentChild(settings, parent, child, sourceFile, /*indentByDefault*/ false) + && !(isNextChild && child && isControlFlowEndingStatement(child.kind, parent)); + } + function rangeIsOnOneLine(sourceFile: SourceFileLike, range: TextRangeWithKind) { + const rangeStart = skipTrivia(sourceFile.text, range.pos); + const startLine = sourceFile.getLineAndCharacterOfPosition(rangeStart).line; + const endLine = sourceFile.getLineAndCharacterOfPosition(range.end).line; + return startLine === endLine; + } } diff --git a/src/services/getEditsForFileRename.ts b/src/services/getEditsForFileRename.ts index ed553ea56d5f5..be356f79852b8 100644 --- a/src/services/getEditsForFileRename.ts +++ b/src/services/getEditsForFileRename.ts @@ -1,260 +1,233 @@ +import { Program, LanguageServiceHost, UserPreferences, SourceMapper, FileTextChanges, hostUsesCaseSensitiveFileNames, createGetCanonicalFileName, GetCanonicalFileName, tryRemoveDirectoryPrefix, getRelativePathFromFile, getDirectoryPath, getTsConfigObjectLiteralExpression, isArrayLiteralExpression, mapDefined, isStringLiteral, getFileMatcherPatterns, getRegexFromPattern, Debug, last, createStringLiteral, getOptionFromName, PropertyAssignment, Expression, getRelativePathFromDirectory, Path, pathIsRelative, ensurePathIsNonModuleName, isAmbientModule, resolveModuleName, ModuleResolutionHost, moduleSpecifiers, normalizePath, combinePaths, Symbol, StringLiteralLike, SourceFile, find, isSourceFile, ResolvedModuleWithFailedLookupLocations, forEach, endsWith, emptyArray, SourceFileLike, TextRange, createRange, isObjectLiteralExpression, isPropertyAssignment } from "./ts"; +import { FormatContext } from "./ts.formatting"; +import { ChangeTracker } from "./ts.textChanges"; /* @internal */ -namespace ts { - export function getEditsForFileRename( - program: Program, - oldFileOrDirPath: string, - newFileOrDirPath: string, - host: LanguageServiceHost, - formatContext: formatting.FormatContext, - preferences: UserPreferences, - sourceMapper: SourceMapper, - ): readonly FileTextChanges[] { - const useCaseSensitiveFileNames = hostUsesCaseSensitiveFileNames(host); - const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); - const oldToNew = getPathUpdater(oldFileOrDirPath, newFileOrDirPath, getCanonicalFileName, sourceMapper); - const newToOld = getPathUpdater(newFileOrDirPath, oldFileOrDirPath, getCanonicalFileName, sourceMapper); - return textChanges.ChangeTracker.with({ host, formatContext, preferences }, changeTracker => { - updateTsconfigFiles(program, changeTracker, oldToNew, oldFileOrDirPath, newFileOrDirPath, host.getCurrentDirectory(), useCaseSensitiveFileNames); - updateImports(program, changeTracker, oldToNew, newToOld, host, getCanonicalFileName); - }); - } - - /** If 'path' refers to an old directory, returns path in the new directory. */ - type PathUpdater = (path: string) => string | undefined; - // exported for tests - export function getPathUpdater(oldFileOrDirPath: string, newFileOrDirPath: string, getCanonicalFileName: GetCanonicalFileName, sourceMapper: SourceMapper | undefined): PathUpdater { - const canonicalOldPath = getCanonicalFileName(oldFileOrDirPath); - return path => { - const originalPath = sourceMapper && sourceMapper.tryGetSourcePosition({ fileName: path, pos: 0 }); - const updatedPath = getUpdatedPath(originalPath ? originalPath.fileName : path); - return originalPath - ? updatedPath === undefined ? undefined : makeCorrespondingRelativeChange(originalPath.fileName, updatedPath, path, getCanonicalFileName) - : updatedPath; - }; - - function getUpdatedPath(pathToUpdate: string): string | undefined { - if (getCanonicalFileName(pathToUpdate) === canonicalOldPath) return newFileOrDirPath; - const suffix = tryRemoveDirectoryPrefix(pathToUpdate, canonicalOldPath, getCanonicalFileName); - return suffix === undefined ? undefined : newFileOrDirPath + "/" + suffix; - } - } - - // Relative path from a0 to b0 should be same as relative path from a1 to b1. Returns b1. - function makeCorrespondingRelativeChange(a0: string, b0: string, a1: string, getCanonicalFileName: GetCanonicalFileName): string { - const rel = getRelativePathFromFile(a0, b0, getCanonicalFileName); - return combinePathsSafe(getDirectoryPath(a1), rel); +export function getEditsForFileRename(program: Program, oldFileOrDirPath: string, newFileOrDirPath: string, host: LanguageServiceHost, formatContext: FormatContext, preferences: UserPreferences, sourceMapper: SourceMapper): readonly FileTextChanges[] { + const useCaseSensitiveFileNames = hostUsesCaseSensitiveFileNames(host); + const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); + const oldToNew = getPathUpdater(oldFileOrDirPath, newFileOrDirPath, getCanonicalFileName, sourceMapper); + const newToOld = getPathUpdater(newFileOrDirPath, oldFileOrDirPath, getCanonicalFileName, sourceMapper); + return ChangeTracker.with({ host, formatContext, preferences }, changeTracker => { + updateTsconfigFiles(program, changeTracker, oldToNew, oldFileOrDirPath, newFileOrDirPath, host.getCurrentDirectory(), useCaseSensitiveFileNames); + updateImports(program, changeTracker, oldToNew, newToOld, host, getCanonicalFileName); + }); +} +/** If 'path' refers to an old directory, returns path in the new directory. */ +/* @internal */ +type PathUpdater = (path: string) => string | undefined; +// exported for tests +/* @internal */ +export function getPathUpdater(oldFileOrDirPath: string, newFileOrDirPath: string, getCanonicalFileName: GetCanonicalFileName, sourceMapper: SourceMapper | undefined): PathUpdater { + const canonicalOldPath = getCanonicalFileName(oldFileOrDirPath); + return path => { + const originalPath = sourceMapper && sourceMapper.tryGetSourcePosition({ fileName: path, pos: 0 }); + const updatedPath = getUpdatedPath(originalPath ? originalPath.fileName : path); + return originalPath + ? updatedPath === undefined ? undefined : makeCorrespondingRelativeChange(originalPath.fileName, updatedPath, path, getCanonicalFileName) + : updatedPath; + }; + function getUpdatedPath(pathToUpdate: string): string | undefined { + if (getCanonicalFileName(pathToUpdate) === canonicalOldPath) + return newFileOrDirPath; + const suffix = tryRemoveDirectoryPrefix(pathToUpdate, canonicalOldPath, getCanonicalFileName); + return suffix === undefined ? undefined : newFileOrDirPath + "/" + suffix; } - - function updateTsconfigFiles(program: Program, changeTracker: textChanges.ChangeTracker, oldToNew: PathUpdater, oldFileOrDirPath: string, newFileOrDirPath: string, currentDirectory: string, useCaseSensitiveFileNames: boolean): void { - const { configFile } = program.getCompilerOptions(); - if (!configFile) return; - const configDir = getDirectoryPath(configFile.fileName); - - const jsonObjectLiteral = getTsConfigObjectLiteralExpression(configFile); - if (!jsonObjectLiteral) return; - - forEachProperty(jsonObjectLiteral, (property, propertyName) => { - switch (propertyName) { - case "files": - case "include": - case "exclude": { - const foundExactMatch = updatePaths(property); - if (!foundExactMatch && propertyName === "include" && isArrayLiteralExpression(property.initializer)) { - const includes = mapDefined(property.initializer.elements, e => isStringLiteral(e) ? e.text : undefined); - const matchers = getFileMatcherPatterns(configDir, /*excludes*/ [], includes, useCaseSensitiveFileNames, currentDirectory); - // If there isn't some include for this, add a new one. - if (getRegexFromPattern(Debug.checkDefined(matchers.includeFilePattern), useCaseSensitiveFileNames).test(oldFileOrDirPath) && - !getRegexFromPattern(Debug.checkDefined(matchers.includeFilePattern), useCaseSensitiveFileNames).test(newFileOrDirPath)) { - changeTracker.insertNodeAfter(configFile, last(property.initializer.elements), createStringLiteral(relativePath(newFileOrDirPath))); - } +} +// Relative path from a0 to b0 should be same as relative path from a1 to b1. Returns b1. +/* @internal */ +function makeCorrespondingRelativeChange(a0: string, b0: string, a1: string, getCanonicalFileName: GetCanonicalFileName): string { + const rel = getRelativePathFromFile(a0, b0, getCanonicalFileName); + return combinePathsSafe(getDirectoryPath(a1), rel); +} +/* @internal */ +function updateTsconfigFiles(program: Program, changeTracker: ChangeTracker, oldToNew: PathUpdater, oldFileOrDirPath: string, newFileOrDirPath: string, currentDirectory: string, useCaseSensitiveFileNames: boolean): void { + const { configFile } = program.getCompilerOptions(); + if (!configFile) + return; + const configDir = getDirectoryPath(configFile.fileName); + const jsonObjectLiteral = getTsConfigObjectLiteralExpression(configFile); + if (!jsonObjectLiteral) + return; + forEachProperty(jsonObjectLiteral, (property, propertyName) => { + switch (propertyName) { + case "files": + case "include": + case "exclude": { + const foundExactMatch = updatePaths(property); + if (!foundExactMatch && propertyName === "include" && isArrayLiteralExpression(property.initializer)) { + const includes = mapDefined(property.initializer.elements, e => isStringLiteral(e) ? e.text : undefined); + const matchers = getFileMatcherPatterns(configDir, /*excludes*/ [], includes, useCaseSensitiveFileNames, currentDirectory); + // If there isn't some include for this, add a new one. + if (getRegexFromPattern(Debug.checkDefined(matchers.includeFilePattern), useCaseSensitiveFileNames).test(oldFileOrDirPath) && + !getRegexFromPattern(Debug.checkDefined(matchers.includeFilePattern), useCaseSensitiveFileNames).test(newFileOrDirPath)) { + changeTracker.insertNodeAfter(configFile, last(property.initializer.elements), createStringLiteral(relativePath(newFileOrDirPath))); } - break; } - case "compilerOptions": - forEachProperty(property.initializer, (property, propertyName) => { - const option = getOptionFromName(propertyName); - if (option && (option.isFilePath || option.type === "list" && option.element.isFilePath)) { - updatePaths(property); - } - else if (propertyName === "paths") { - forEachProperty(property.initializer, (pathsProperty) => { - if (!isArrayLiteralExpression(pathsProperty.initializer)) return; - for (const e of pathsProperty.initializer.elements) { - tryUpdateString(e); - } - }); - } - }); - break; - } - }); - - function updatePaths(property: PropertyAssignment): boolean { - // Type annotation needed due to #7294 - const elements: readonly Expression[] = isArrayLiteralExpression(property.initializer) ? property.initializer.elements : [property.initializer]; - let foundExactMatch = false; - for (const element of elements) { - foundExactMatch = tryUpdateString(element) || foundExactMatch; + break; } - return foundExactMatch; - } - - function tryUpdateString(element: Expression): boolean { - if (!isStringLiteral(element)) return false; - const elementFileName = combinePathsSafe(configDir, element.text); - - const updated = oldToNew(elementFileName); - if (updated !== undefined) { - changeTracker.replaceRangeWithText(configFile!, createStringRange(element, configFile!), relativePath(updated)); - return true; - } - return false; + case "compilerOptions": + forEachProperty(property.initializer, (property, propertyName) => { + const option = getOptionFromName(propertyName); + if (option && (option.isFilePath || option.type === "list" && option.element.isFilePath)) { + updatePaths(property); + } + else if (propertyName === "paths") { + forEachProperty(property.initializer, (pathsProperty) => { + if (!isArrayLiteralExpression(pathsProperty.initializer)) + return; + for (const e of pathsProperty.initializer.elements) { + tryUpdateString(e); + } + }); + } + }); + break; } - - function relativePath(path: string): string { - return getRelativePathFromDirectory(configDir, path, /*ignoreCase*/ !useCaseSensitiveFileNames); + }); + function updatePaths(property: PropertyAssignment): boolean { + // Type annotation needed due to #7294 + const elements: readonly Expression[] = isArrayLiteralExpression(property.initializer) ? property.initializer.elements : [property.initializer]; + let foundExactMatch = false; + for (const element of elements) { + foundExactMatch = tryUpdateString(element) || foundExactMatch; } + return foundExactMatch; } - - function updateImports( - program: Program, - changeTracker: textChanges.ChangeTracker, - oldToNew: PathUpdater, - newToOld: PathUpdater, - host: LanguageServiceHost, - getCanonicalFileName: GetCanonicalFileName, - ): void { - const allFiles = program.getSourceFiles(); - for (const sourceFile of allFiles) { - const newFromOld = oldToNew(sourceFile.path) as Path; - const newImportFromPath = newFromOld !== undefined ? newFromOld : sourceFile.path; - const newImportFromDirectory = getDirectoryPath(newImportFromPath); - - const oldFromNew: string | undefined = newToOld(sourceFile.fileName); - const oldImportFromPath: string = oldFromNew || sourceFile.fileName; - const oldImportFromDirectory = getDirectoryPath(oldImportFromPath); - - const importingSourceFileMoved = newFromOld !== undefined || oldFromNew !== undefined; - - updateImportsWorker(sourceFile, changeTracker, - referenceText => { - if (!pathIsRelative(referenceText)) return undefined; - const oldAbsolute = combinePathsSafe(oldImportFromDirectory, referenceText); - const newAbsolute = oldToNew(oldAbsolute); - return newAbsolute === undefined ? undefined : ensurePathIsNonModuleName(getRelativePathFromDirectory(newImportFromDirectory, newAbsolute, getCanonicalFileName)); - }, - importLiteral => { - const importedModuleSymbol = program.getTypeChecker().getSymbolAtLocation(importLiteral); - // No need to update if it's an ambient module^M - if (importedModuleSymbol && importedModuleSymbol.declarations.some(d => isAmbientModule(d))) return undefined; - - const toImport = oldFromNew !== undefined - // If we're at the new location (file was already renamed), need to redo module resolution starting from the old location. - // TODO:GH#18217 - ? getSourceFileToImportFromResolved(resolveModuleName(importLiteral.text, oldImportFromPath, program.getCompilerOptions(), host as ModuleResolutionHost), - oldToNew, allFiles) - : getSourceFileToImport(importedModuleSymbol, importLiteral, sourceFile, program, host, oldToNew); - - // Need an update if the imported file moved, or the importing file moved and was using a relative path. - return toImport !== undefined && (toImport.updated || (importingSourceFileMoved && pathIsRelative(importLiteral.text))) - ? moduleSpecifiers.updateModuleSpecifier(program.getCompilerOptions(), newImportFromPath, toImport.newFileName, host, allFiles, program.redirectTargetsMap, importLiteral.text) - : undefined; - }); + function tryUpdateString(element: Expression): boolean { + if (!isStringLiteral(element)) + return false; + const elementFileName = combinePathsSafe(configDir, element.text); + const updated = oldToNew(elementFileName); + if (updated !== undefined) { + changeTracker.replaceRangeWithText(configFile!, createStringRange(element, configFile!), relativePath(updated)); + return true; } + return false; + } + function relativePath(path: string): string { + return getRelativePathFromDirectory(configDir, path, /*ignoreCase*/ !useCaseSensitiveFileNames); + } +} +/* @internal */ +function updateImports(program: Program, changeTracker: ChangeTracker, oldToNew: PathUpdater, newToOld: PathUpdater, host: LanguageServiceHost, getCanonicalFileName: GetCanonicalFileName): void { + const allFiles = program.getSourceFiles(); + for (const sourceFile of allFiles) { + const newFromOld = (oldToNew(sourceFile.path) as Path); + const newImportFromPath = newFromOld !== undefined ? newFromOld : sourceFile.path; + const newImportFromDirectory = getDirectoryPath(newImportFromPath); + const oldFromNew: string | undefined = newToOld(sourceFile.fileName); + const oldImportFromPath: string = oldFromNew || sourceFile.fileName; + const oldImportFromDirectory = getDirectoryPath(oldImportFromPath); + const importingSourceFileMoved = newFromOld !== undefined || oldFromNew !== undefined; + updateImportsWorker(sourceFile, changeTracker, referenceText => { + if (!pathIsRelative(referenceText)) + return undefined; + const oldAbsolute = combinePathsSafe(oldImportFromDirectory, referenceText); + const newAbsolute = oldToNew(oldAbsolute); + return newAbsolute === undefined ? undefined : ensurePathIsNonModuleName(getRelativePathFromDirectory(newImportFromDirectory, newAbsolute, getCanonicalFileName)); + }, importLiteral => { + const importedModuleSymbol = program.getTypeChecker().getSymbolAtLocation(importLiteral); + // No need to update if it's an ambient module^M + if (importedModuleSymbol && importedModuleSymbol.declarations.some(d => isAmbientModule(d))) + return undefined; + const toImport = oldFromNew !== undefined + // If we're at the new location (file was already renamed), need to redo module resolution starting from the old location. + // TODO:GH#18217 + ? getSourceFileToImportFromResolved(resolveModuleName(importLiteral.text, oldImportFromPath, program.getCompilerOptions(), (host as ModuleResolutionHost)), oldToNew, allFiles) + : getSourceFileToImport(importedModuleSymbol, importLiteral, sourceFile, program, host, oldToNew); + // Need an update if the imported file moved, or the importing file moved and was using a relative path. + return toImport !== undefined && (toImport.updated || (importingSourceFileMoved && pathIsRelative(importLiteral.text))) + ? moduleSpecifiers.updateModuleSpecifier(program.getCompilerOptions(), newImportFromPath, toImport.newFileName, host, allFiles, program.redirectTargetsMap, importLiteral.text) + : undefined; + }); } - - function combineNormal(pathA: string, pathB: string): string { - return normalizePath(combinePaths(pathA, pathB)); +} +/* @internal */ +function combineNormal(pathA: string, pathB: string): string { + return normalizePath(combinePaths(pathA, pathB)); +} +/* @internal */ +function combinePathsSafe(pathA: string, pathB: string): string { + return ensurePathIsNonModuleName(combineNormal(pathA, pathB)); +} +/* @internal */ +interface ToImport { + readonly newFileName: string; + /** True if the imported file was renamed. */ + readonly updated: boolean; +} +/* @internal */ +function getSourceFileToImport(importedModuleSymbol: Symbol | undefined, importLiteral: StringLiteralLike, importingSourceFile: SourceFile, program: Program, host: LanguageServiceHost, oldToNew: PathUpdater): ToImport | undefined { + if (importedModuleSymbol) { + // `find` should succeed because we checked for ambient modules before calling this function. + const oldFileName = find(importedModuleSymbol.declarations, isSourceFile)!.fileName; + const newFileName = oldToNew(oldFileName); + return newFileName === undefined ? { newFileName: oldFileName, updated: false } : { newFileName, updated: true }; } - function combinePathsSafe(pathA: string, pathB: string): string { - return ensurePathIsNonModuleName(combineNormal(pathA, pathB)); + else { + const resolved = host.resolveModuleNames + ? host.getResolvedModuleWithFailedLookupLocationsFromCache && host.getResolvedModuleWithFailedLookupLocationsFromCache(importLiteral.text, importingSourceFile.fileName) + : program.getResolvedModuleWithFailedLookupLocationsFromCache(importLiteral.text, importingSourceFile.fileName); + return getSourceFileToImportFromResolved(resolved, oldToNew, program.getSourceFiles()); } - - interface ToImport { - readonly newFileName: string; - /** True if the imported file was renamed. */ - readonly updated: boolean; +} +/* @internal */ +function getSourceFileToImportFromResolved(resolved: ResolvedModuleWithFailedLookupLocations | undefined, oldToNew: PathUpdater, sourceFiles: readonly SourceFile[]): ToImport | undefined { + // Search through all locations looking for a moved file, and only then test already existing files. + // This is because if `a.ts` is compiled to `a.js` and `a.ts` is moved, we don't want to resolve anything to `a.js`, but to `a.ts`'s new location. + if (!resolved) + return undefined; + // First try resolved module + if (resolved.resolvedModule) { + const result = tryChange(resolved.resolvedModule.resolvedFileName); + if (result) + return result; } - function getSourceFileToImport( - importedModuleSymbol: Symbol | undefined, - importLiteral: StringLiteralLike, - importingSourceFile: SourceFile, - program: Program, - host: LanguageServiceHost, - oldToNew: PathUpdater, - ): ToImport | undefined { - if (importedModuleSymbol) { - // `find` should succeed because we checked for ambient modules before calling this function. - const oldFileName = find(importedModuleSymbol.declarations, isSourceFile)!.fileName; - const newFileName = oldToNew(oldFileName); - return newFileName === undefined ? { newFileName: oldFileName, updated: false } : { newFileName, updated: true }; - } - else { - const resolved = host.resolveModuleNames - ? host.getResolvedModuleWithFailedLookupLocationsFromCache && host.getResolvedModuleWithFailedLookupLocationsFromCache(importLiteral.text, importingSourceFile.fileName) - : program.getResolvedModuleWithFailedLookupLocationsFromCache(importLiteral.text, importingSourceFile.fileName); - return getSourceFileToImportFromResolved(resolved, oldToNew, program.getSourceFiles()); - } + // Then failed lookups that are in the list of sources + const result = forEach(resolved.failedLookupLocations, tryChangeWithIgnoringPackageJsonExisting) + // Then failed lookups except package.json since we dont want to touch them (only included ts/js files) + || forEach(resolved.failedLookupLocations, tryChangeWithIgnoringPackageJson); + if (result) + return result; + // If nothing changed, then result is resolved module file thats not updated + return resolved.resolvedModule && { newFileName: resolved.resolvedModule.resolvedFileName, updated: false }; + function tryChangeWithIgnoringPackageJsonExisting(oldFileName: string) { + const newFileName = oldToNew(oldFileName); + return newFileName && find(sourceFiles, src => src.fileName === newFileName) + ? tryChangeWithIgnoringPackageJson(oldFileName) : undefined; } - - function getSourceFileToImportFromResolved(resolved: ResolvedModuleWithFailedLookupLocations | undefined, oldToNew: PathUpdater, sourceFiles: readonly SourceFile[]): ToImport | undefined { - // Search through all locations looking for a moved file, and only then test already existing files. - // This is because if `a.ts` is compiled to `a.js` and `a.ts` is moved, we don't want to resolve anything to `a.js`, but to `a.ts`'s new location. - if (!resolved) return undefined; - - // First try resolved module - if (resolved.resolvedModule) { - const result = tryChange(resolved.resolvedModule.resolvedFileName); - if (result) return result; - } - - // Then failed lookups that are in the list of sources - const result = forEach(resolved.failedLookupLocations, tryChangeWithIgnoringPackageJsonExisting) - // Then failed lookups except package.json since we dont want to touch them (only included ts/js files) - || forEach(resolved.failedLookupLocations, tryChangeWithIgnoringPackageJson); - if (result) return result; - - // If nothing changed, then result is resolved module file thats not updated - return resolved.resolvedModule && { newFileName: resolved.resolvedModule.resolvedFileName, updated: false }; - - function tryChangeWithIgnoringPackageJsonExisting(oldFileName: string) { - const newFileName = oldToNew(oldFileName); - return newFileName && find(sourceFiles, src => src.fileName === newFileName) - ? tryChangeWithIgnoringPackageJson(oldFileName) : undefined; - } - - function tryChangeWithIgnoringPackageJson(oldFileName: string) { - return !endsWith(oldFileName, "/package.json") ? tryChange(oldFileName) : undefined; - } - - function tryChange(oldFileName: string) { - const newFileName = oldToNew(oldFileName); - return newFileName && { newFileName, updated: true }; - } + function tryChangeWithIgnoringPackageJson(oldFileName: string) { + return !endsWith(oldFileName, "/package.json") ? tryChange(oldFileName) : undefined; } - - function updateImportsWorker(sourceFile: SourceFile, changeTracker: textChanges.ChangeTracker, updateRef: (refText: string) => string | undefined, updateImport: (importLiteral: StringLiteralLike) => string | undefined) { - for (const ref of sourceFile.referencedFiles || emptyArray) { // TODO: GH#26162 - const updated = updateRef(ref.fileName); - if (updated !== undefined && updated !== sourceFile.text.slice(ref.pos, ref.end)) changeTracker.replaceRangeWithText(sourceFile, ref, updated); - } - - for (const importStringLiteral of sourceFile.imports) { - const updated = updateImport(importStringLiteral); - if (updated !== undefined && updated !== importStringLiteral.text) changeTracker.replaceRangeWithText(sourceFile, createStringRange(importStringLiteral, sourceFile), updated); - } + function tryChange(oldFileName: string) { + const newFileName = oldToNew(oldFileName); + return newFileName && { newFileName, updated: true }; } - - function createStringRange(node: StringLiteralLike, sourceFile: SourceFileLike): TextRange { - return createRange(node.getStart(sourceFile) + 1, node.end - 1); +} +/* @internal */ +function updateImportsWorker(sourceFile: SourceFile, changeTracker: ChangeTracker, updateRef: (refText: string) => string | undefined, updateImport: (importLiteral: StringLiteralLike) => string | undefined) { + for (const ref of sourceFile.referencedFiles || emptyArray) { // TODO: GH#26162 + const updated = updateRef(ref.fileName); + if (updated !== undefined && updated !== sourceFile.text.slice(ref.pos, ref.end)) + changeTracker.replaceRangeWithText(sourceFile, ref, updated); } - - function forEachProperty(objectLiteral: Expression, cb: (property: PropertyAssignment, propertyName: string) => void) { - if (!isObjectLiteralExpression(objectLiteral)) return; - for (const property of objectLiteral.properties) { - if (isPropertyAssignment(property) && isStringLiteral(property.name)) { - cb(property, property.name.text); - } + for (const importStringLiteral of sourceFile.imports) { + const updated = updateImport(importStringLiteral); + if (updated !== undefined && updated !== importStringLiteral.text) + changeTracker.replaceRangeWithText(sourceFile, createStringRange(importStringLiteral, sourceFile), updated); + } +} +/* @internal */ +function createStringRange(node: StringLiteralLike, sourceFile: SourceFileLike): TextRange { + return createRange(node.getStart(sourceFile) + 1, node.end - 1); +} +/* @internal */ +function forEachProperty(objectLiteral: Expression, cb: (property: PropertyAssignment, propertyName: string) => void) { + if (!isObjectLiteralExpression(objectLiteral)) + return; + for (const property of objectLiteral.properties) { + if (isPropertyAssignment(property) && isStringLiteral(property.name)) { + cb(property, property.name.text); } } } diff --git a/src/services/globalThisShim.ts b/src/services/globalThisShim.ts index 899bb009895c0..6206d8b965942 100644 --- a/src/services/globalThisShim.ts +++ b/src/services/globalThisShim.ts @@ -1,15 +1,18 @@ -// We polyfill `globalThis` here so re can reliably patch the global scope -// in the contexts we want to in the same way across script and module formats - -// https://mathiasbynens.be/notes/globalthis - -// #region The polyfill starts here. -/* eslint-disable no-var */ +import { TypeScriptServicesFactory, versionMajorMinor } from "./ts"; /* @internal */ -declare var window: {}; +declare global { + // We polyfill `globalThis` here so re can reliably patch the global scope + // in the contexts we want to in the same way across script and module formats + // https://mathiasbynens.be/notes/globalthis + // #region The polyfill starts here. + /* eslint-disable no-var */ + /* @internal */ + var window: {}; +} /* eslint-enable no-var */ ((() => { - if (typeof globalThis === "object") return; + if (typeof globalThis === "object") + return; try { Object.defineProperty(Object.prototype, "__magic__", { get() { @@ -38,22 +41,18 @@ declare var window: {}; } })()); // #endregion The polyfill ends here. - // if `process` is undefined, we're probably not running in node - patch legacy members onto the global scope // @ts-ignore if (typeof process === "undefined" || process.browser) { /// TODO: this is used by VS, clean this up on both sides of the interface - //@ts-ignore globalThis.TypeScript = globalThis.TypeScript || {}; //@ts-ignore globalThis.TypeScript.Services = globalThis.TypeScript.Services || {}; //@ts-ignore - globalThis.TypeScript.Services.TypeScriptServicesFactory = ts.TypeScriptServicesFactory; - + globalThis.TypeScript.Services.TypeScriptServicesFactory = TypeScriptServicesFactory; // 'toolsVersion' gets consumed by the managed side, so it's not unused. // TODO: it should be moved into a namespace though. - //@ts-ignore - globalThis.toolsVersion = ts.versionMajorMinor; -} \ No newline at end of file + globalThis.toolsVersion = versionMajorMinor; +} diff --git a/src/services/goToDefinition.ts b/src/services/goToDefinition.ts index 110c90f05d022..ff6a783dd8878 100644 --- a/src/services/goToDefinition.ts +++ b/src/services/goToDefinition.ts @@ -1,359 +1,337 @@ +import { Program, SourceFile, DefinitionInfo, getTouchingPropertyName, isJumpStatementTarget, getTargetLabel, ScriptElementKind, isJsxOpeningLikeElement, isVariableDeclaration, isRequireCall, emptyArray, SyntaxKind, isPropertyName, isBindingElement, isObjectBindingPattern, getNameFromPropertyName, flatMap, getContainingObjectLiteralElement, getPropertySymbolsFromContextualType, Symbol, SignatureDeclaration, isCallLikeExpression, TypeChecker, Type, Node, TypeFlags, first, DefinitionInfoAndBoundSpan, createTextSpanFromRange, createTextSpan, isPropertyAccessExpression, mapDefined, IndexKind, SymbolFlags, isInJSFile, forEach, filter, isAssignmentDeclaration, map, isNewExpressionTarget, find, isClassLike, Debug, isCallOrNewExpressionTarget, isNameOfFunctionDeclaration, Declaration, isConstructorDeclaration, isFunctionLike, FunctionLikeDeclaration, last, getNameOfDeclaration, createTextSpanFromNode, FileReference, textRangeContainsPositionInclusive, createTextSpanFromBounds, CallLikeExpression, getInvokedExpression, isRightSideOfPropertyAccess, tryCast, isFunctionTypeNode } from "./ts"; +import { getSymbolKind } from "./ts.SymbolDisplay"; +import { toContextSpan, getContextNode } from "./ts.FindAllReferences"; /* @internal */ -namespace ts.GoToDefinition { - export function getDefinitionAtPosition(program: Program, sourceFile: SourceFile, position: number): readonly DefinitionInfo[] | undefined { - const reference = getReferenceAtPosition(sourceFile, position, program); - if (reference) { - return [getDefinitionInfoForFileReference(reference.fileName, reference.file.fileName)]; - } - - const node = getTouchingPropertyName(sourceFile, position); - if (node === sourceFile) { - return undefined; - } - const { parent } = node; - - const typeChecker = program.getTypeChecker(); - - // Labels - if (isJumpStatementTarget(node)) { - const label = getTargetLabel(node.parent, node.text); - return label ? [createDefinitionInfoFromName(typeChecker, label, ScriptElementKind.label, node.text, /*containerName*/ undefined!)] : undefined; // TODO: GH#18217 - } - - const symbol = getSymbol(node, typeChecker); - - // Could not find a symbol e.g. node is string or number keyword, - // or the symbol was an internal symbol and does not have a declaration e.g. undefined symbol - if (!symbol) { - return getDefinitionInfoForIndexSignatures(node, typeChecker); - } - - const calledDeclaration = tryGetSignatureDeclaration(typeChecker, node); - // Don't go to the component constructor definition for a JSX element, just go to the component definition. - if (calledDeclaration && !(isJsxOpeningLikeElement(node.parent) && isConstructorLike(calledDeclaration))) { - const sigInfo = createDefinitionFromSignatureDeclaration(typeChecker, calledDeclaration); - // For a function, if this is the original function definition, return just sigInfo. - // If this is the original constructor definition, parent is the class. - if (typeChecker.getRootSymbols(symbol).some(s => symbolMatchesSignature(s, calledDeclaration)) || - // TODO: GH#25533 Following check shouldn't be necessary if 'require' is an alias - symbol.declarations && symbol.declarations.some(d => isVariableDeclaration(d) && !!d.initializer && isRequireCall(d.initializer, /*checkArgumentIsStringLiteralLike*/ false))) { - return [sigInfo]; - } - else { - const defs = getDefinitionFromSymbol(typeChecker, symbol, node, calledDeclaration) || emptyArray; - // For a 'super()' call, put the signature first, else put the variable first. - return node.kind === SyntaxKind.SuperKeyword ? [sigInfo, ...defs] : [...defs, sigInfo]; - } - } - - // Because name in short-hand property assignment has two different meanings: property name and property value, - // using go-to-definition at such position should go to the variable declaration of the property value rather than - // go to the declaration of the property name (in this case stay at the same position). However, if go-to-definition - // is performed at the location of property access, we would like to go to definition of the property in the short-hand - // assignment. This case and others are handled by the following code. - if (node.parent.kind === SyntaxKind.ShorthandPropertyAssignment) { - const shorthandSymbol = typeChecker.getShorthandAssignmentValueSymbol(symbol.valueDeclaration); - return shorthandSymbol ? shorthandSymbol.declarations.map(decl => createDefinitionInfo(decl, typeChecker, shorthandSymbol, node)) : []; - } - - // If the node is the name of a BindingElement within an ObjectBindingPattern instead of just returning the - // declaration the symbol (which is itself), we should try to get to the original type of the ObjectBindingPattern - // and return the property declaration for the referenced property. - // For example: - // import('./foo').then(({ b/*goto*/ar }) => undefined); => should get use to the declaration in file "./foo" - // - // function bar(onfulfilled: (value: T) => void) { //....} - // interface Test { - // pr/*destination*/op1: number - // } - // bar(({pr/*goto*/op1})=>{}); - if (isPropertyName(node) && isBindingElement(parent) && isObjectBindingPattern(parent.parent) && - (node === (parent.propertyName || parent.name))) { - const name = getNameFromPropertyName(node); - const type = typeChecker.getTypeAtLocation(parent.parent); - return name === undefined ? emptyArray : flatMap(type.isUnion() ? type.types : [type], t => { - const prop = t.getProperty(name); - return prop && getDefinitionFromSymbol(typeChecker, prop, node); - }); - } - - // If the current location we want to find its definition is in an object literal, try to get the contextual type for the - // object literal, lookup the property symbol in the contextual type, and use this for goto-definition. - // For example - // interface Props{ - // /*first*/prop1: number - // prop2: boolean - // } - // function Foo(arg: Props) {} - // Foo( { pr/*1*/op1: 10, prop2: true }) - const element = getContainingObjectLiteralElement(node); - if (element) { - const contextualType = element && typeChecker.getContextualType(element.parent); - if (contextualType) { - return flatMap(getPropertySymbolsFromContextualType(element, typeChecker, contextualType, /*unionSymbolOk*/ false), propertySymbol => - getDefinitionFromSymbol(typeChecker, propertySymbol, node)); - } - } - return getDefinitionFromSymbol(typeChecker, symbol, node); +export function getDefinitionAtPosition(program: Program, sourceFile: SourceFile, position: number): readonly DefinitionInfo[] | undefined { + const reference = getReferenceAtPosition(sourceFile, position, program); + if (reference) { + return [getDefinitionInfoForFileReference(reference.fileName, reference.file.fileName)]; } - - /** - * True if we should not add definitions for both the signature symbol and the definition symbol. - * True for `const |f = |() => 0`, false for `function |f() {} const |g = f;`. - */ - function symbolMatchesSignature(s: Symbol, calledDeclaration: SignatureDeclaration) { - return s === calledDeclaration.symbol || s === calledDeclaration.symbol.parent || - !isCallLikeExpression(calledDeclaration.parent) && s === calledDeclaration.parent.symbol; + const node = getTouchingPropertyName(sourceFile, position); + if (node === sourceFile) { + return undefined; } - - export function getReferenceAtPosition(sourceFile: SourceFile, position: number, program: Program): { fileName: string, file: SourceFile } | undefined { - const referencePath = findReferenceInPosition(sourceFile.referencedFiles, position); - if (referencePath) { - const file = program.getSourceFileFromReference(sourceFile, referencePath); - return file && { fileName: referencePath.fileName, file }; - } - - const typeReferenceDirective = findReferenceInPosition(sourceFile.typeReferenceDirectives, position); - if (typeReferenceDirective) { - const reference = program.getResolvedTypeReferenceDirectives().get(typeReferenceDirective.fileName); - const file = reference && program.getSourceFile(reference.resolvedFileName!); // TODO:GH#18217 - return file && { fileName: typeReferenceDirective.fileName, file }; + const { parent } = node; + const typeChecker = program.getTypeChecker(); + // Labels + if (isJumpStatementTarget(node)) { + const label = getTargetLabel(node.parent, node.text); + return label ? [createDefinitionInfoFromName(typeChecker, label, ScriptElementKind.label, node.text, /*containerName*/ (undefined!))] : undefined; // TODO: GH#18217 + } + const symbol = getSymbol(node, typeChecker); + // Could not find a symbol e.g. node is string or number keyword, + // or the symbol was an internal symbol and does not have a declaration e.g. undefined symbol + if (!symbol) { + return getDefinitionInfoForIndexSignatures(node, typeChecker); + } + const calledDeclaration = tryGetSignatureDeclaration(typeChecker, node); + // Don't go to the component constructor definition for a JSX element, just go to the component definition. + if (calledDeclaration && !(isJsxOpeningLikeElement(node.parent) && isConstructorLike(calledDeclaration))) { + const sigInfo = createDefinitionFromSignatureDeclaration(typeChecker, calledDeclaration); + // For a function, if this is the original function definition, return just sigInfo. + // If this is the original constructor definition, parent is the class. + if (typeChecker.getRootSymbols(symbol).some(s => symbolMatchesSignature(s, calledDeclaration)) || + // TODO: GH#25533 Following check shouldn't be necessary if 'require' is an alias + symbol.declarations && symbol.declarations.some(d => isVariableDeclaration(d) && !!d.initializer && isRequireCall(d.initializer, /*checkArgumentIsStringLiteralLike*/ false))) { + return [sigInfo]; } - - const libReferenceDirective = findReferenceInPosition(sourceFile.libReferenceDirectives, position); - if (libReferenceDirective) { - const file = program.getLibFileFromReference(libReferenceDirective); - return file && { fileName: libReferenceDirective.fileName, file }; + else { + const defs = getDefinitionFromSymbol(typeChecker, symbol, node, calledDeclaration) || emptyArray; + // For a 'super()' call, put the signature first, else put the variable first. + return node.kind === SyntaxKind.SuperKeyword ? [sigInfo, ...defs] : [...defs, sigInfo]; } - - return undefined; } - - /// Goto type - export function getTypeDefinitionAtPosition(typeChecker: TypeChecker, sourceFile: SourceFile, position: number): readonly DefinitionInfo[] | undefined { - const node = getTouchingPropertyName(sourceFile, position); - if (node === sourceFile) { - return undefined; - } - - const symbol = typeChecker.getSymbolAtLocation(node); - if (!symbol) return undefined; - - const typeAtLocation = typeChecker.getTypeOfSymbolAtLocation(symbol, node); - const returnType = tryGetReturnTypeOfFunction(symbol, typeAtLocation, typeChecker); - const fromReturnType = returnType && definitionFromType(returnType, typeChecker, node); - // If a function returns 'void' or some other type with no definition, just return the function definition. - return fromReturnType && fromReturnType.length !== 0 ? fromReturnType : definitionFromType(typeAtLocation, typeChecker, node); + // Because name in short-hand property assignment has two different meanings: property name and property value, + // using go-to-definition at such position should go to the variable declaration of the property value rather than + // go to the declaration of the property name (in this case stay at the same position). However, if go-to-definition + // is performed at the location of property access, we would like to go to definition of the property in the short-hand + // assignment. This case and others are handled by the following code. + if (node.parent.kind === SyntaxKind.ShorthandPropertyAssignment) { + const shorthandSymbol = typeChecker.getShorthandAssignmentValueSymbol(symbol.valueDeclaration); + return shorthandSymbol ? shorthandSymbol.declarations.map(decl => createDefinitionInfo(decl, typeChecker, shorthandSymbol, node)) : []; } - - function definitionFromType(type: Type, checker: TypeChecker, node: Node): readonly DefinitionInfo[] { - return flatMap(type.isUnion() && !(type.flags & TypeFlags.Enum) ? type.types : [type], t => - t.symbol && getDefinitionFromSymbol(checker, t.symbol, node)); + // If the node is the name of a BindingElement within an ObjectBindingPattern instead of just returning the + // declaration the symbol (which is itself), we should try to get to the original type of the ObjectBindingPattern + // and return the property declaration for the referenced property. + // For example: + // import('./foo').then(({ b/*goto*/ar }) => undefined); => should get use to the declaration in file "./foo" + // + // function bar(onfulfilled: (value: T) => void) { //....} + // interface Test { + // pr/*destination*/op1: number + // } + // bar(({pr/*goto*/op1})=>{}); + if (isPropertyName(node) && isBindingElement(parent) && isObjectBindingPattern(parent.parent) && + (node === (parent.propertyName || parent.name))) { + const name = getNameFromPropertyName(node); + const type = typeChecker.getTypeAtLocation(parent.parent); + return name === undefined ? emptyArray : flatMap(type.isUnion() ? type.types : [type], t => { + const prop = t.getProperty(name); + return prop && getDefinitionFromSymbol(typeChecker, prop, node); + }); } - - function tryGetReturnTypeOfFunction(symbol: Symbol, type: Type, checker: TypeChecker): Type | undefined { - // If the type is just a function's inferred type, - // go-to-type should go to the return type instead, since go-to-definition takes you to the function anyway. - if (type.symbol === symbol || - // At `const f = () => {}`, the symbol is `f` and the type symbol is at `() => {}` - symbol.valueDeclaration && type.symbol && isVariableDeclaration(symbol.valueDeclaration) && symbol.valueDeclaration.initializer === type.symbol.valueDeclaration as Node) { - const sigs = type.getCallSignatures(); - if (sigs.length === 1) return checker.getReturnTypeOfSignature(first(sigs)); + // If the current location we want to find its definition is in an object literal, try to get the contextual type for the + // object literal, lookup the property symbol in the contextual type, and use this for goto-definition. + // For example + // interface Props{ + // /*first*/prop1: number + // prop2: boolean + // } + // function Foo(arg: Props) {} + // Foo( { pr/*1*/op1: 10, prop2: true }) + const element = getContainingObjectLiteralElement(node); + if (element) { + const contextualType = element && typeChecker.getContextualType(element.parent); + if (contextualType) { + return flatMap(getPropertySymbolsFromContextualType(element, typeChecker, contextualType, /*unionSymbolOk*/ false), propertySymbol => getDefinitionFromSymbol(typeChecker, propertySymbol, node)); } + } + return getDefinitionFromSymbol(typeChecker, symbol, node); +} +/** + * True if we should not add definitions for both the signature symbol and the definition symbol. + * True for `const |f = |() => 0`, false for `function |f() {} const |g = f;`. + */ +/* @internal */ +function symbolMatchesSignature(s: Symbol, calledDeclaration: SignatureDeclaration) { + return s === calledDeclaration.symbol || s === calledDeclaration.symbol.parent || + !isCallLikeExpression(calledDeclaration.parent) && s === calledDeclaration.parent.symbol; +} +/* @internal */ +export function getReferenceAtPosition(sourceFile: SourceFile, position: number, program: Program): { + fileName: string; + file: SourceFile; +} | undefined { + const referencePath = findReferenceInPosition(sourceFile.referencedFiles, position); + if (referencePath) { + const file = program.getSourceFileFromReference(sourceFile, referencePath); + return file && { fileName: referencePath.fileName, file }; + } + const typeReferenceDirective = findReferenceInPosition(sourceFile.typeReferenceDirectives, position); + if (typeReferenceDirective) { + const reference = program.getResolvedTypeReferenceDirectives().get(typeReferenceDirective.fileName); + const file = reference && program.getSourceFile(reference.resolvedFileName!); // TODO:GH#18217 + return file && { fileName: typeReferenceDirective.fileName, file }; + } + const libReferenceDirective = findReferenceInPosition(sourceFile.libReferenceDirectives, position); + if (libReferenceDirective) { + const file = program.getLibFileFromReference(libReferenceDirective); + return file && { fileName: libReferenceDirective.fileName, file }; + } + return undefined; +} +/// Goto type +/* @internal */ +export function getTypeDefinitionAtPosition(typeChecker: TypeChecker, sourceFile: SourceFile, position: number): readonly DefinitionInfo[] | undefined { + const node = getTouchingPropertyName(sourceFile, position); + if (node === sourceFile) { return undefined; } - - export function getDefinitionAndBoundSpan(program: Program, sourceFile: SourceFile, position: number): DefinitionInfoAndBoundSpan | undefined { - const definitions = getDefinitionAtPosition(program, sourceFile, position); - - if (!definitions || definitions.length === 0) { - return undefined; - } - - // Check if position is on triple slash reference. - const comment = findReferenceInPosition(sourceFile.referencedFiles, position) || - findReferenceInPosition(sourceFile.typeReferenceDirectives, position) || - findReferenceInPosition(sourceFile.libReferenceDirectives, position); - - if (comment) { - return { definitions, textSpan: createTextSpanFromRange(comment) }; - } - - const node = getTouchingPropertyName(sourceFile, position); - const textSpan = createTextSpan(node.getStart(), node.getWidth()); - - return { definitions, textSpan }; + const symbol = typeChecker.getSymbolAtLocation(node); + if (!symbol) + return undefined; + const typeAtLocation = typeChecker.getTypeOfSymbolAtLocation(symbol, node); + const returnType = tryGetReturnTypeOfFunction(symbol, typeAtLocation, typeChecker); + const fromReturnType = returnType && definitionFromType(returnType, typeChecker, node); + // If a function returns 'void' or some other type with no definition, just return the function definition. + return fromReturnType && fromReturnType.length !== 0 ? fromReturnType : definitionFromType(typeAtLocation, typeChecker, node); +} +/* @internal */ +function definitionFromType(type: Type, checker: TypeChecker, node: Node): readonly DefinitionInfo[] { + return flatMap(type.isUnion() && !(type.flags & TypeFlags.Enum) ? type.types : [type], t => t.symbol && getDefinitionFromSymbol(checker, t.symbol, node)); +} +/* @internal */ +function tryGetReturnTypeOfFunction(symbol: Symbol, type: Type, checker: TypeChecker): Type | undefined { + // If the type is just a function's inferred type, + // go-to-type should go to the return type instead, since go-to-definition takes you to the function anyway. + if (type.symbol === symbol || + // At `const f = () => {}`, the symbol is `f` and the type symbol is at `() => {}` + symbol.valueDeclaration && type.symbol && isVariableDeclaration(symbol.valueDeclaration) && symbol.valueDeclaration.initializer === (type.symbol.valueDeclaration as Node)) { + const sigs = type.getCallSignatures(); + if (sigs.length === 1) + return checker.getReturnTypeOfSignature(first(sigs)); } - - // At 'x.foo', see if the type of 'x' has an index signature, and if so find its declarations. - function getDefinitionInfoForIndexSignatures(node: Node, checker: TypeChecker): DefinitionInfo[] | undefined { - if (!isPropertyAccessExpression(node.parent) || node.parent.name !== node) return; - const type = checker.getTypeAtLocation(node.parent.expression); - return mapDefined(type.isUnionOrIntersection() ? type.types : [type], nonUnionType => { - const info = checker.getIndexInfoOfType(nonUnionType, IndexKind.String); - return info && info.declaration && createDefinitionFromSignatureDeclaration(checker, info.declaration); - }); + return undefined; +} +/* @internal */ +export function getDefinitionAndBoundSpan(program: Program, sourceFile: SourceFile, position: number): DefinitionInfoAndBoundSpan | undefined { + const definitions = getDefinitionAtPosition(program, sourceFile, position); + if (!definitions || definitions.length === 0) { + return undefined; } - - function getSymbol(node: Node, checker: TypeChecker): Symbol | undefined { - const symbol = checker.getSymbolAtLocation(node); - // If this is an alias, and the request came at the declaration location - // get the aliased symbol instead. This allows for goto def on an import e.g. - // import {A, B} from "mod"; - // to jump to the implementation directly. - if (symbol && symbol.flags & SymbolFlags.Alias && shouldSkipAlias(node, symbol.declarations[0])) { - const aliased = checker.getAliasedSymbol(symbol); - if (aliased.declarations) { - return aliased; - } - } - if (symbol && isInJSFile(node)) { - const requireCall = forEach(symbol.declarations, d => isVariableDeclaration(d) && !!d.initializer && isRequireCall(d.initializer, /*checkArgumentIsStringLiteralLike*/ true) ? d.initializer : undefined); - if (requireCall) { - const moduleSymbol = checker.getSymbolAtLocation(requireCall.arguments[0]); - if (moduleSymbol) { - return checker.resolveExternalModuleSymbol(moduleSymbol); - } - } - } - return symbol; + // Check if position is on triple slash reference. + const comment = findReferenceInPosition(sourceFile.referencedFiles, position) || + findReferenceInPosition(sourceFile.typeReferenceDirectives, position) || + findReferenceInPosition(sourceFile.libReferenceDirectives, position); + if (comment) { + return { definitions, textSpan: createTextSpanFromRange(comment) }; } - - // Go to the original declaration for cases: - // - // (1) when the aliased symbol was declared in the location(parent). - // (2) when the aliased symbol is originating from an import. - // - function shouldSkipAlias(node: Node, declaration: Node): boolean { - if (node.kind !== SyntaxKind.Identifier) { - return false; - } - if (node.parent === declaration) { - return true; - } - switch (declaration.kind) { - case SyntaxKind.ImportClause: - case SyntaxKind.ImportEqualsDeclaration: - return true; - case SyntaxKind.ImportSpecifier: - return declaration.parent.kind === SyntaxKind.NamedImports; - default: - return false; + const node = getTouchingPropertyName(sourceFile, position); + const textSpan = createTextSpan(node.getStart(), node.getWidth()); + return { definitions, textSpan }; +} +// At 'x.foo', see if the type of 'x' has an index signature, and if so find its declarations. +/* @internal */ +function getDefinitionInfoForIndexSignatures(node: Node, checker: TypeChecker): DefinitionInfo[] | undefined { + if (!isPropertyAccessExpression(node.parent) || node.parent.name !== node) + return; + const type = checker.getTypeAtLocation(node.parent.expression); + return mapDefined(type.isUnionOrIntersection() ? type.types : [type], nonUnionType => { + const info = checker.getIndexInfoOfType(nonUnionType, IndexKind.String); + return info && info.declaration && createDefinitionFromSignatureDeclaration(checker, info.declaration); + }); +} +/* @internal */ +function getSymbol(node: Node, checker: TypeChecker): Symbol | undefined { + const symbol = checker.getSymbolAtLocation(node); + // If this is an alias, and the request came at the declaration location + // get the aliased symbol instead. This allows for goto def on an import e.g. + // import {A, B} from "mod"; + // to jump to the implementation directly. + if (symbol && symbol.flags & SymbolFlags.Alias && shouldSkipAlias(node, symbol.declarations[0])) { + const aliased = checker.getAliasedSymbol(symbol); + if (aliased.declarations) { + return aliased; } } - - function getDefinitionFromSymbol(typeChecker: TypeChecker, symbol: Symbol, node: Node, declarationNode?: Node): DefinitionInfo[] | undefined { - // There are cases when you extend a function by adding properties to it afterwards, - // we want to strip those extra properties. - // For deduping purposes, we also want to exclude any declarationNodes if provided. - const filteredDeclarations = filter(symbol.declarations, d => d !== declarationNode && (!isAssignmentDeclaration(d) || d === symbol.valueDeclaration)) || undefined; - return getConstructSignatureDefinition() || getCallSignatureDefinition() || map(filteredDeclarations, declaration => createDefinitionInfo(declaration, typeChecker, symbol, node)); - - function getConstructSignatureDefinition(): DefinitionInfo[] | undefined { - // Applicable only if we are in a new expression, or we are on a constructor declaration - // and in either case the symbol has a construct signature definition, i.e. class - if (symbol.flags & SymbolFlags.Class && !(symbol.flags & SymbolFlags.Function) && (isNewExpressionTarget(node) || node.kind === SyntaxKind.ConstructorKeyword)) { - const cls = find(filteredDeclarations, isClassLike) || Debug.fail("Expected declaration to have at least one class-like declaration"); - return getSignatureDefinition(cls.members, /*selectConstructors*/ true); + if (symbol && isInJSFile(node)) { + const requireCall = forEach(symbol.declarations, d => isVariableDeclaration(d) && !!d.initializer && isRequireCall(d.initializer, /*checkArgumentIsStringLiteralLike*/ true) ? d.initializer : undefined); + if (requireCall) { + const moduleSymbol = checker.getSymbolAtLocation(requireCall.arguments[0]); + if (moduleSymbol) { + return checker.resolveExternalModuleSymbol(moduleSymbol); } } - - function getCallSignatureDefinition(): DefinitionInfo[] | undefined { - return isCallOrNewExpressionTarget(node) || isNameOfFunctionDeclaration(node) - ? getSignatureDefinition(filteredDeclarations, /*selectConstructors*/ false) - : undefined; - } - - function getSignatureDefinition(signatureDeclarations: readonly Declaration[] | undefined, selectConstructors: boolean): DefinitionInfo[] | undefined { - if (!signatureDeclarations) { - return undefined; - } - const declarations = signatureDeclarations.filter(selectConstructors ? isConstructorDeclaration : isFunctionLike); - const declarationsWithBody = declarations.filter(d => !!(d).body); - - // declarations defined on the global scope can be defined on multiple files. Get all of them. - return declarations.length - ? declarationsWithBody.length !== 0 - ? declarationsWithBody.map(x => createDefinitionInfo(x, typeChecker, symbol, node)) - : [createDefinitionInfo(last(declarations), typeChecker, symbol, node)] - : undefined; - } - } - - /** Creates a DefinitionInfo from a Declaration, using the declaration's name if possible. */ - function createDefinitionInfo(declaration: Declaration, checker: TypeChecker, symbol: Symbol, node: Node): DefinitionInfo { - const symbolName = checker.symbolToString(symbol); // Do not get scoped name, just the name of the symbol - const symbolKind = SymbolDisplay.getSymbolKind(checker, symbol, node); - const containerName = symbol.parent ? checker.symbolToString(symbol.parent, node) : ""; - return createDefinitionInfoFromName(checker, declaration, symbolKind, symbolName, containerName); } - - /** Creates a DefinitionInfo directly from the name of a declaration. */ - function createDefinitionInfoFromName(checker: TypeChecker, declaration: Declaration, symbolKind: ScriptElementKind, symbolName: string, containerName: string): DefinitionInfo { - const name = getNameOfDeclaration(declaration) || declaration; - const sourceFile = name.getSourceFile(); - const textSpan = createTextSpanFromNode(name, sourceFile); - return { - fileName: sourceFile.fileName, - textSpan, - kind: symbolKind, - name: symbolName, - containerKind: undefined!, // TODO: GH#18217 - containerName, - ...FindAllReferences.toContextSpan( - textSpan, - sourceFile, - FindAllReferences.getContextNode(declaration) - ), - isLocal: !checker.isDeclarationVisible(declaration) - }; - } - - function createDefinitionFromSignatureDeclaration(typeChecker: TypeChecker, decl: SignatureDeclaration): DefinitionInfo { - return createDefinitionInfo(decl, typeChecker, decl.symbol, decl); - } - - export function findReferenceInPosition(refs: readonly FileReference[], pos: number): FileReference | undefined { - return find(refs, ref => textRangeContainsPositionInclusive(ref, pos)); + return symbol; +} +// Go to the original declaration for cases: +// +// (1) when the aliased symbol was declared in the location(parent). +// (2) when the aliased symbol is originating from an import. +// +/* @internal */ +function shouldSkipAlias(node: Node, declaration: Node): boolean { + if (node.kind !== SyntaxKind.Identifier) { + return false; } - - function getDefinitionInfoForFileReference(name: string, targetFileName: string): DefinitionInfo { - return { - fileName: targetFileName, - textSpan: createTextSpanFromBounds(0, 0), - kind: ScriptElementKind.scriptElement, - name, - containerName: undefined!, - containerKind: undefined!, // TODO: GH#18217 - }; + if (node.parent === declaration) { + return true; } - - /** Returns a CallLikeExpression where `node` is the target being invoked. */ - function getAncestorCallLikeExpression(node: Node): CallLikeExpression | undefined { - const target = climbPastManyPropertyAccesses(node); - const callLike = target.parent; - return callLike && isCallLikeExpression(callLike) && getInvokedExpression(callLike) === target ? callLike : undefined; + switch (declaration.kind) { + case SyntaxKind.ImportClause: + case SyntaxKind.ImportEqualsDeclaration: + return true; + case SyntaxKind.ImportSpecifier: + return declaration.parent.kind === SyntaxKind.NamedImports; + default: + return false; } - - function climbPastManyPropertyAccesses(node: Node): Node { - return isRightSideOfPropertyAccess(node) ? climbPastManyPropertyAccesses(node.parent) : node; +} +/* @internal */ +function getDefinitionFromSymbol(typeChecker: TypeChecker, symbol: Symbol, node: Node, declarationNode?: Node): DefinitionInfo[] | undefined { + // There are cases when you extend a function by adding properties to it afterwards, + // we want to strip those extra properties. + // For deduping purposes, we also want to exclude any declarationNodes if provided. + const filteredDeclarations = filter(symbol.declarations, d => d !== declarationNode && (!isAssignmentDeclaration(d) || d === symbol.valueDeclaration)) || undefined; + return getConstructSignatureDefinition() || getCallSignatureDefinition() || map(filteredDeclarations, declaration => createDefinitionInfo(declaration, typeChecker, symbol, node)); + function getConstructSignatureDefinition(): DefinitionInfo[] | undefined { + // Applicable only if we are in a new expression, or we are on a constructor declaration + // and in either case the symbol has a construct signature definition, i.e. class + if (symbol.flags & SymbolFlags.Class && !(symbol.flags & SymbolFlags.Function) && (isNewExpressionTarget(node) || node.kind === SyntaxKind.ConstructorKeyword)) { + const cls = find(filteredDeclarations, isClassLike) || Debug.fail("Expected declaration to have at least one class-like declaration"); + return getSignatureDefinition(cls.members, /*selectConstructors*/ true); + } } - - function tryGetSignatureDeclaration(typeChecker: TypeChecker, node: Node): SignatureDeclaration | undefined { - const callLike = getAncestorCallLikeExpression(node); - const signature = callLike && typeChecker.getResolvedSignature(callLike); - // Don't go to a function type, go to the value having that type. - return tryCast(signature && signature.declaration, (d): d is SignatureDeclaration => isFunctionLike(d) && !isFunctionTypeNode(d)); + function getCallSignatureDefinition(): DefinitionInfo[] | undefined { + return isCallOrNewExpressionTarget(node) || isNameOfFunctionDeclaration(node) + ? getSignatureDefinition(filteredDeclarations, /*selectConstructors*/ false) + : undefined; } - - function isConstructorLike(node: Node): boolean { - switch (node.kind) { - case SyntaxKind.Constructor: - case SyntaxKind.ConstructorType: - case SyntaxKind.ConstructSignature: - return true; - default: - return false; + function getSignatureDefinition(signatureDeclarations: readonly Declaration[] | undefined, selectConstructors: boolean): DefinitionInfo[] | undefined { + if (!signatureDeclarations) { + return undefined; } + const declarations = signatureDeclarations.filter(selectConstructors ? isConstructorDeclaration : isFunctionLike); + const declarationsWithBody = declarations.filter(d => !!(d).body); + // declarations defined on the global scope can be defined on multiple files. Get all of them. + return declarations.length + ? declarationsWithBody.length !== 0 + ? declarationsWithBody.map(x => createDefinitionInfo(x, typeChecker, symbol, node)) + : [createDefinitionInfo(last(declarations), typeChecker, symbol, node)] + : undefined; + } +} +/** Creates a DefinitionInfo from a Declaration, using the declaration's name if possible. */ +/* @internal */ +function createDefinitionInfo(declaration: Declaration, checker: TypeChecker, symbol: Symbol, node: Node): DefinitionInfo { + const symbolName = checker.symbolToString(symbol); // Do not get scoped name, just the name of the symbol + const symbolKind = getSymbolKind(checker, symbol, node); + const containerName = symbol.parent ? checker.symbolToString(symbol.parent, node) : ""; + return createDefinitionInfoFromName(checker, declaration, symbolKind, symbolName, containerName); +} +/** Creates a DefinitionInfo directly from the name of a declaration. */ +/* @internal */ +function createDefinitionInfoFromName(checker: TypeChecker, declaration: Declaration, symbolKind: ScriptElementKind, symbolName: string, containerName: string): DefinitionInfo { + const name = getNameOfDeclaration(declaration) || declaration; + const sourceFile = name.getSourceFile(); + const textSpan = createTextSpanFromNode(name, sourceFile); + return { + fileName: sourceFile.fileName, + textSpan, + kind: symbolKind, + name: symbolName, + containerKind: undefined!, + containerName, + ...toContextSpan(textSpan, sourceFile, getContextNode(declaration)), + isLocal: !checker.isDeclarationVisible(declaration) + }; +} +/* @internal */ +function createDefinitionFromSignatureDeclaration(typeChecker: TypeChecker, decl: SignatureDeclaration): DefinitionInfo { + return createDefinitionInfo(decl, typeChecker, decl.symbol, decl); +} +/* @internal */ +export function findReferenceInPosition(refs: readonly FileReference[], pos: number): FileReference | undefined { + return find(refs, ref => textRangeContainsPositionInclusive(ref, pos)); +} +/* @internal */ +function getDefinitionInfoForFileReference(name: string, targetFileName: string): DefinitionInfo { + return { + fileName: targetFileName, + textSpan: createTextSpanFromBounds(0, 0), + kind: ScriptElementKind.scriptElement, + name, + containerName: undefined!, + containerKind: undefined!, + }; +} +/** Returns a CallLikeExpression where `node` is the target being invoked. */ +/* @internal */ +function getAncestorCallLikeExpression(node: Node): CallLikeExpression | undefined { + const target = climbPastManyPropertyAccesses(node); + const callLike = target.parent; + return callLike && isCallLikeExpression(callLike) && getInvokedExpression(callLike) === target ? callLike : undefined; +} +/* @internal */ +function climbPastManyPropertyAccesses(node: Node): Node { + return isRightSideOfPropertyAccess(node) ? climbPastManyPropertyAccesses(node.parent) : node; +} +/* @internal */ +function tryGetSignatureDeclaration(typeChecker: TypeChecker, node: Node): SignatureDeclaration | undefined { + const callLike = getAncestorCallLikeExpression(node); + const signature = callLike && typeChecker.getResolvedSignature(callLike); + // Don't go to a function type, go to the value having that type. + return tryCast(signature && signature.declaration, (d): d is SignatureDeclaration => isFunctionLike(d) && !isFunctionTypeNode(d)); +} +/* @internal */ +function isConstructorLike(node: Node): boolean { + switch (node.kind) { + case SyntaxKind.Constructor: + case SyntaxKind.ConstructorType: + case SyntaxKind.ConstructSignature: + return true; + default: + return false; } } diff --git a/src/services/importTracker.ts b/src/services/importTracker.ts index 91b53ea418a19..b766778cbad6b 100644 --- a/src/services/importTracker.ts +++ b/src/services/importTracker.ts @@ -1,653 +1,629 @@ +import { Identifier, Symbol, StringLiteral, SourceFile, TypeChecker, CancellationToken, ModuleDeclaration, ModuleBlock, AnyImportOrReExport, ValidImportTypeNode, CallExpression, nodeSeenTracker, isExternalModuleAugmentation, getSourceFileOfNode, SyntaxKind, VariableDeclaration, hasModifier, ModifierFlags, isDefaultImport, Debug, ImportEqualsDeclaration, ImportDeclaration, SymbolFlags, isImportTypeNode, getSymbolId, getFirstIdentifier, symbolName, isNamedExports, symbolEscapedNameNoDefault, NamedImportsOrExports, __String, InternalSymbolName, isExportDeclaration, StringLiteralLike, FileReference, Program, createMap, Statement, forEach, importFromModuleSpecifier, ExportDeclaration, isStringLiteral, Node, isBinaryExpression, isImportEqualsDeclaration, isExportAssignment, isJSDocTypedefTag, ExportAssignment, BinaryExpression, getAssignmentDeclarationKind, AssignmentDeclarationKind, getNameOfAccessExpression, cast, isAccessExpression, isSourceFile, isVariableDeclaration, isBindingElement, walkUpBindingElementsAndPatterns, BindingElement, isCatchClause, isVariableStatement, ImportSpecifier, ImportClause, NamespaceImport, isExternalModuleSymbol, isExportSpecifier } from "./ts"; +import * as ts from "./ts"; /* Code for finding imports of an exported symbol. Used only by FindAllReferences. */ /* @internal */ -namespace ts.FindAllReferences { - export interface ImportsResult { - /** For every import of the symbol, the location and local symbol for the import. */ - importSearches: readonly [Identifier, Symbol][]; - /** For rename imports/exports `{ foo as bar }`, `foo` is not a local, so it may be added as a reference immediately without further searching. */ - singleReferences: readonly (Identifier | StringLiteral)[]; - /** List of source files that may (or may not) use the symbol via a namespace. (For UMD modules this is every file.) */ - indirectUsers: readonly SourceFile[]; - } - export type ImportTracker = (exportSymbol: Symbol, exportInfo: ExportInfo, isForRename: boolean) => ImportsResult; - - /** Creates the imports map and returns an ImportTracker that uses it. Call this lazily to avoid calling `getDirectImportsMap` unnecessarily. */ - export function createImportTracker(sourceFiles: readonly SourceFile[], sourceFilesSet: ReadonlyMap, checker: TypeChecker, cancellationToken: CancellationToken | undefined): ImportTracker { - const allDirectImports = getDirectImportsMap(sourceFiles, checker, cancellationToken); - return (exportSymbol, exportInfo, isForRename) => { - const { directImports, indirectUsers } = getImportersForExport(sourceFiles, sourceFilesSet, allDirectImports, exportInfo, checker, cancellationToken); - return { indirectUsers, ...getSearchesFromDirectImports(directImports, exportSymbol, exportInfo.exportKind, checker, isForRename) }; - }; - } - - /** Info about an exported symbol to perform recursive search on. */ - export interface ExportInfo { - exportingModuleSymbol: Symbol; - exportKind: ExportKind; - } - - export const enum ExportKind { Named, Default, ExportEquals } - - export const enum ImportExport { Import, Export } - - interface AmbientModuleDeclaration extends ModuleDeclaration { body?: ModuleBlock; } - type SourceFileLike = SourceFile | AmbientModuleDeclaration; - // Identifier for the case of `const x = require("y")`. - type Importer = AnyImportOrReExport | ValidImportTypeNode | Identifier; - type ImporterOrCallExpression = Importer | CallExpression; - - /** Returns import statements that directly reference the exporting module, and a list of files that may access the module through a namespace. */ - function getImportersForExport( - sourceFiles: readonly SourceFile[], - sourceFilesSet: ReadonlyMap, - allDirectImports: Map, - { exportingModuleSymbol, exportKind }: ExportInfo, - checker: TypeChecker, - cancellationToken: CancellationToken | undefined, - ): { directImports: Importer[], indirectUsers: readonly SourceFile[] } { - const markSeenDirectImport = nodeSeenTracker(); - const markSeenIndirectUser = nodeSeenTracker(); - const directImports: Importer[] = []; - const isAvailableThroughGlobal = !!exportingModuleSymbol.globalExports; - const indirectUserDeclarations: SourceFileLike[] | undefined = isAvailableThroughGlobal ? undefined : []; - - handleDirectImports(exportingModuleSymbol); - - return { directImports, indirectUsers: getIndirectUsers() }; - - function getIndirectUsers(): readonly SourceFile[] { - if (isAvailableThroughGlobal) { - // It has `export as namespace`, so anything could potentially use it. - return sourceFiles; - } - - // Module augmentations may use this module's exports without importing it. - for (const decl of exportingModuleSymbol.declarations) { - if (isExternalModuleAugmentation(decl) && sourceFilesSet.has(decl.getSourceFile().fileName)) { - addIndirectUser(decl); - } +export interface ImportsResult { + /** For every import of the symbol, the location and local symbol for the import. */ + importSearches: readonly [Identifier, Symbol][]; + /** For rename imports/exports `{ foo as bar }`, `foo` is not a local, so it may be added as a reference immediately without further searching. */ + singleReferences: readonly (Identifier | StringLiteral)[]; + /** List of source files that may (or may not) use the symbol via a namespace. (For UMD modules this is every file.) */ + indirectUsers: readonly SourceFile[]; +} +/* @internal */ +export type ImportTracker = (exportSymbol: Symbol, exportInfo: ExportInfo, isForRename: boolean) => ImportsResult; +/** Creates the imports map and returns an ImportTracker that uses it. Call this lazily to avoid calling `getDirectImportsMap` unnecessarily. */ +/* @internal */ +export function createImportTracker(sourceFiles: readonly SourceFile[], sourceFilesSet: ts.ReadonlyMap, checker: TypeChecker, cancellationToken: CancellationToken | undefined): ImportTracker { + const allDirectImports = getDirectImportsMap(sourceFiles, checker, cancellationToken); + return (exportSymbol, exportInfo, isForRename) => { + const { directImports, indirectUsers } = getImportersForExport(sourceFiles, sourceFilesSet, allDirectImports, exportInfo, checker, cancellationToken); + return { indirectUsers, ...getSearchesFromDirectImports(directImports, exportSymbol, exportInfo.exportKind, checker, isForRename) }; + }; +} +/** Info about an exported symbol to perform recursive search on. */ +/* @internal */ +export interface ExportInfo { + exportingModuleSymbol: Symbol; + exportKind: ExportKind; +} +/* @internal */ +export const enum ExportKind { + Named, + Default, + ExportEquals +} +/* @internal */ +export const enum ImportExport { + Import, + Export +} +/* @internal */ +interface AmbientModuleDeclaration extends ModuleDeclaration { + body?: ModuleBlock; +} +/* @internal */ +type SourceFileLike = SourceFile | AmbientModuleDeclaration; +// Identifier for the case of `const x = require("y")`. +/* @internal */ +type Importer = AnyImportOrReExport | ValidImportTypeNode | Identifier; +/* @internal */ +type ImporterOrCallExpression = Importer | CallExpression; +/** Returns import statements that directly reference the exporting module, and a list of files that may access the module through a namespace. */ +/* @internal */ +function getImportersForExport(sourceFiles: readonly SourceFile[], sourceFilesSet: ts.ReadonlyMap, allDirectImports: ts.Map, { exportingModuleSymbol, exportKind }: ExportInfo, checker: TypeChecker, cancellationToken: CancellationToken | undefined): { + directImports: Importer[]; + indirectUsers: readonly SourceFile[]; +} { + const markSeenDirectImport = nodeSeenTracker(); + const markSeenIndirectUser = nodeSeenTracker(); + const directImports: Importer[] = []; + const isAvailableThroughGlobal = !!exportingModuleSymbol.globalExports; + const indirectUserDeclarations: SourceFileLike[] | undefined = isAvailableThroughGlobal ? undefined : []; + handleDirectImports(exportingModuleSymbol); + return { directImports, indirectUsers: getIndirectUsers() }; + function getIndirectUsers(): readonly SourceFile[] { + if (isAvailableThroughGlobal) { + // It has `export as namespace`, so anything could potentially use it. + return sourceFiles; + } + // Module augmentations may use this module's exports without importing it. + for (const decl of exportingModuleSymbol.declarations) { + if (isExternalModuleAugmentation(decl) && sourceFilesSet.has(decl.getSourceFile().fileName)) { + addIndirectUser(decl); } - - // This may return duplicates (if there are multiple module declarations in a single source file, all importing the same thing as a namespace), but `State.markSearchedSymbol` will handle that. - return indirectUserDeclarations!.map(getSourceFileOfNode); } - - function handleDirectImports(exportingModuleSymbol: Symbol): void { - const theseDirectImports = getDirectImports(exportingModuleSymbol); - if (theseDirectImports) { - for (const direct of theseDirectImports) { - if (!markSeenDirectImport(direct)) { - continue; - } - - if (cancellationToken) cancellationToken.throwIfCancellationRequested(); - - switch (direct.kind) { - case SyntaxKind.CallExpression: - if (!isAvailableThroughGlobal) { - const parent = direct.parent; - if (exportKind === ExportKind.ExportEquals && parent.kind === SyntaxKind.VariableDeclaration) { - const { name } = parent as VariableDeclaration; - if (name.kind === SyntaxKind.Identifier) { - directImports.push(name); - break; - } + // This may return duplicates (if there are multiple module declarations in a single source file, all importing the same thing as a namespace), but `State.markSearchedSymbol` will handle that. + return indirectUserDeclarations!.map(getSourceFileOfNode); + } + function handleDirectImports(exportingModuleSymbol: Symbol): void { + const theseDirectImports = getDirectImports(exportingModuleSymbol); + if (theseDirectImports) { + for (const direct of theseDirectImports) { + if (!markSeenDirectImport(direct)) { + continue; + } + if (cancellationToken) + cancellationToken.throwIfCancellationRequested(); + switch (direct.kind) { + case SyntaxKind.CallExpression: + if (!isAvailableThroughGlobal) { + const parent = direct.parent; + if (exportKind === ExportKind.ExportEquals && parent.kind === SyntaxKind.VariableDeclaration) { + const { name } = (parent as VariableDeclaration); + if (name.kind === SyntaxKind.Identifier) { + directImports.push(name); + break; } - - // Don't support re-exporting 'require()' calls, so just add a single indirect user. - addIndirectUser(direct.getSourceFile()); - } - break; - - case SyntaxKind.Identifier: // for 'const x = require("y"); - break; // TODO: GH#23879 - - case SyntaxKind.ImportEqualsDeclaration: - handleNamespaceImport(direct, direct.name, hasModifier(direct, ModifierFlags.Export), /*alreadyAddedDirect*/ false); - break; - - case SyntaxKind.ImportDeclaration: - directImports.push(direct); - const namedBindings = direct.importClause && direct.importClause.namedBindings; - if (namedBindings && namedBindings.kind === SyntaxKind.NamespaceImport) { - handleNamespaceImport(direct, namedBindings.name, /*isReExport*/ false, /*alreadyAddedDirect*/ true); - } - else if (!isAvailableThroughGlobal && isDefaultImport(direct)) { - addIndirectUser(getSourceFileLikeForImportDeclaration(direct)); // Add a check for indirect uses to handle synthetic default imports - } - break; - - case SyntaxKind.ExportDeclaration: - if (!direct.exportClause) { - // This is `export * from "foo"`, so imports of this module may import the export too. - handleDirectImports(getContainingModuleSymbol(direct, checker)); - } - else { - // This is `export { foo } from "foo"` and creates an alias symbol, so recursive search will get handle re-exports. - directImports.push(direct); } - break; - - case SyntaxKind.ImportType: + // Don't support re-exporting 'require()' calls, so just add a single indirect user. + addIndirectUser(direct.getSourceFile()); + } + break; + case SyntaxKind.Identifier: // for 'const x = require("y"); + break; // TODO: GH#23879 + case SyntaxKind.ImportEqualsDeclaration: + handleNamespaceImport(direct, direct.name, hasModifier(direct, ModifierFlags.Export), /*alreadyAddedDirect*/ false); + break; + case SyntaxKind.ImportDeclaration: + directImports.push(direct); + const namedBindings = direct.importClause && direct.importClause.namedBindings; + if (namedBindings && namedBindings.kind === SyntaxKind.NamespaceImport) { + handleNamespaceImport(direct, namedBindings.name, /*isReExport*/ false, /*alreadyAddedDirect*/ true); + } + else if (!isAvailableThroughGlobal && isDefaultImport(direct)) { + addIndirectUser(getSourceFileLikeForImportDeclaration(direct)); // Add a check for indirect uses to handle synthetic default imports + } + break; + case SyntaxKind.ExportDeclaration: + if (!direct.exportClause) { + // This is `export * from "foo"`, so imports of this module may import the export too. + handleDirectImports(getContainingModuleSymbol(direct, checker)); + } + else { + // This is `export { foo } from "foo"` and creates an alias symbol, so recursive search will get handle re-exports. directImports.push(direct); - break; - - default: - Debug.failBadSyntaxKind(direct, "Unexpected import kind."); - } + } + break; + case SyntaxKind.ImportType: + directImports.push(direct); + break; + default: + Debug.failBadSyntaxKind(direct, "Unexpected import kind."); } } } - - function handleNamespaceImport(importDeclaration: ImportEqualsDeclaration | ImportDeclaration, name: Identifier, isReExport: boolean, alreadyAddedDirect: boolean): void { - if (exportKind === ExportKind.ExportEquals) { - // This is a direct import, not import-as-namespace. - if (!alreadyAddedDirect) directImports.push(importDeclaration); + } + function handleNamespaceImport(importDeclaration: ImportEqualsDeclaration | ImportDeclaration, name: Identifier, isReExport: boolean, alreadyAddedDirect: boolean): void { + if (exportKind === ExportKind.ExportEquals) { + // This is a direct import, not import-as-namespace. + if (!alreadyAddedDirect) + directImports.push(importDeclaration); + } + else if (!isAvailableThroughGlobal) { + const sourceFileLike = getSourceFileLikeForImportDeclaration(importDeclaration); + Debug.assert(sourceFileLike.kind === SyntaxKind.SourceFile || sourceFileLike.kind === SyntaxKind.ModuleDeclaration); + if (isReExport || findNamespaceReExports(sourceFileLike, name, checker)) { + addIndirectUsers(sourceFileLike); } - else if (!isAvailableThroughGlobal) { - const sourceFileLike = getSourceFileLikeForImportDeclaration(importDeclaration); - Debug.assert(sourceFileLike.kind === SyntaxKind.SourceFile || sourceFileLike.kind === SyntaxKind.ModuleDeclaration); - if (isReExport || findNamespaceReExports(sourceFileLike, name, checker)) { - addIndirectUsers(sourceFileLike); - } - else { - addIndirectUser(sourceFileLike); + else { + addIndirectUser(sourceFileLike); + } + } + } + function addIndirectUser(sourceFileLike: SourceFileLike): boolean { + Debug.assert(!isAvailableThroughGlobal); + const isNew = markSeenIndirectUser(sourceFileLike); + if (isNew) { + indirectUserDeclarations!.push(sourceFileLike); // TODO: GH#18217 + } + return isNew; + } + /** Adds a module and all of its transitive dependencies as possible indirect users. */ + function addIndirectUsers(sourceFileLike: SourceFileLike): void { + if (!addIndirectUser(sourceFileLike)) { + return; + } + const moduleSymbol = checker.getMergedSymbol(sourceFileLike.symbol); + Debug.assert(!!(moduleSymbol.flags & SymbolFlags.Module)); + const directImports = getDirectImports(moduleSymbol); + if (directImports) { + for (const directImport of directImports) { + if (!isImportTypeNode(directImport)) { + addIndirectUsers(getSourceFileLikeForImportDeclaration(directImport)); } } } - - function addIndirectUser(sourceFileLike: SourceFileLike): boolean { - Debug.assert(!isAvailableThroughGlobal); - const isNew = markSeenIndirectUser(sourceFileLike); - if (isNew) { - indirectUserDeclarations!.push(sourceFileLike); // TODO: GH#18217 + } + function getDirectImports(moduleSymbol: Symbol): ImporterOrCallExpression[] | undefined { + return allDirectImports.get(getSymbolId(moduleSymbol).toString()); + } +} +/** + * Given the set of direct imports of a module, we need to find which ones import the particular exported symbol. + * The returned `importSearches` will result in the entire source file being searched. + * But re-exports will be placed in 'singleReferences' since they cannot be locally referenced. + */ +/* @internal */ +function getSearchesFromDirectImports(directImports: Importer[], exportSymbol: Symbol, exportKind: ExportKind, checker: TypeChecker, isForRename: boolean): Pick { + const importSearches: [Identifier, Symbol][] = []; + const singleReferences: (Identifier | StringLiteral)[] = []; + function addSearch(location: Identifier, symbol: Symbol): void { + importSearches.push([location, symbol]); + } + if (directImports) { + for (const decl of directImports) { + handleImport(decl); + } + } + return { importSearches, singleReferences }; + function handleImport(decl: Importer): void { + if (decl.kind === SyntaxKind.ImportEqualsDeclaration) { + if (isExternalModuleImportEquals(decl)) { + handleNamespaceImportLike(decl.name); + } + return; + } + if (decl.kind === SyntaxKind.Identifier) { + handleNamespaceImportLike(decl); + return; + } + if (decl.kind === SyntaxKind.ImportType) { + if (decl.qualifier) { + const firstIdentifier = getFirstIdentifier(decl.qualifier); + if (firstIdentifier.escapedText === symbolName(exportSymbol)) { + singleReferences.push(firstIdentifier); + } + } + else if (exportKind === ExportKind.ExportEquals) { + singleReferences.push(decl.argument.literal); } - return isNew; + return; } - - /** Adds a module and all of its transitive dependencies as possible indirect users. */ - function addIndirectUsers(sourceFileLike: SourceFileLike): void { - if (!addIndirectUser(sourceFileLike)) { - return; + // Ignore if there's a grammar error + if (decl.moduleSpecifier!.kind !== SyntaxKind.StringLiteral) { + return; + } + if (decl.kind === SyntaxKind.ExportDeclaration) { + if (decl.exportClause && isNamedExports(decl.exportClause)) { + searchForNamedImport(decl.exportClause); } - - const moduleSymbol = checker.getMergedSymbol(sourceFileLike.symbol); - Debug.assert(!!(moduleSymbol.flags & SymbolFlags.Module)); - const directImports = getDirectImports(moduleSymbol); - if (directImports) { - for (const directImport of directImports) { - if (!isImportTypeNode(directImport)) { - addIndirectUsers(getSourceFileLikeForImportDeclaration(directImport)); + return; + } + const { name, namedBindings } = decl.importClause || { name: undefined, namedBindings: undefined }; + if (namedBindings) { + switch (namedBindings.kind) { + case SyntaxKind.NamespaceImport: + handleNamespaceImportLike(namedBindings.name); + break; + case SyntaxKind.NamedImports: + // 'default' might be accessed as a named import `{ default as foo }`. + if (exportKind === ExportKind.Named || exportKind === ExportKind.Default) { + searchForNamedImport(namedBindings); } - } + break; + default: + Debug.assertNever(namedBindings); } } - - function getDirectImports(moduleSymbol: Symbol): ImporterOrCallExpression[] | undefined { - return allDirectImports.get(getSymbolId(moduleSymbol).toString()); + // `export =` might be imported by a default import if `--allowSyntheticDefaultImports` is on, so this handles both ExportKind.Default and ExportKind.ExportEquals. + // If a default import has the same name as the default export, allow to rename it. + // Given `import f` and `export default function f`, we will rename both, but for `import g` we will rename just that. + if (name && (exportKind === ExportKind.Default || exportKind === ExportKind.ExportEquals) && (!isForRename || name.escapedText === symbolEscapedNameNoDefault(exportSymbol))) { + const defaultImportAlias = checker.getSymbolAtLocation(name)!; + addSearch(name, defaultImportAlias); } } - /** - * Given the set of direct imports of a module, we need to find which ones import the particular exported symbol. - * The returned `importSearches` will result in the entire source file being searched. - * But re-exports will be placed in 'singleReferences' since they cannot be locally referenced. + * `import x = require("./x")` or `import * as x from "./x"`. + * An `export =` may be imported by this syntax, so it may be a direct import. + * If it's not a direct import, it will be in `indirectUsers`, so we don't have to do anything here. */ - function getSearchesFromDirectImports(directImports: Importer[], exportSymbol: Symbol, exportKind: ExportKind, checker: TypeChecker, isForRename: boolean): Pick { - const importSearches: [Identifier, Symbol][] = []; - const singleReferences: (Identifier | StringLiteral)[] = []; - function addSearch(location: Identifier, symbol: Symbol): void { - importSearches.push([location, symbol]); + function handleNamespaceImportLike(importName: Identifier): void { + // Don't rename an import that already has a different name than the export. + if (exportKind === ExportKind.ExportEquals && (!isForRename || isNameMatch(importName.escapedText))) { + addSearch(importName, checker.getSymbolAtLocation(importName)!); } - - if (directImports) { - for (const decl of directImports) { - handleImport(decl); - } + } + function searchForNamedImport(namedBindings: NamedImportsOrExports | undefined): void { + if (!namedBindings) { + return; } - - return { importSearches, singleReferences }; - - function handleImport(decl: Importer): void { - if (decl.kind === SyntaxKind.ImportEqualsDeclaration) { - if (isExternalModuleImportEquals(decl)) { - handleNamespaceImportLike(decl.name); - } - return; - } - - if (decl.kind === SyntaxKind.Identifier) { - handleNamespaceImportLike(decl); - return; + for (const element of namedBindings.elements) { + const { name, propertyName } = element; + if (!isNameMatch((propertyName || name).escapedText)) { + continue; } - - if (decl.kind === SyntaxKind.ImportType) { - if (decl.qualifier) { - const firstIdentifier = getFirstIdentifier(decl.qualifier); - if (firstIdentifier.escapedText === symbolName(exportSymbol)) { - singleReferences.push(firstIdentifier); - } + if (propertyName) { + // This is `import { foo as bar } from "./a"` or `export { foo as bar } from "./a"`. `foo` isn't a local in the file, so just add it as a single reference. + singleReferences.push(propertyName); + // If renaming `{ foo as bar }`, don't touch `bar`, just `foo`. + // But do rename `foo` in ` { default as foo }` if that's the original export name. + if (!isForRename || name.escapedText === exportSymbol.escapedName) { + // Search locally for `bar`. + addSearch(name, checker.getSymbolAtLocation(name)!); } - else if (exportKind === ExportKind.ExportEquals) { - singleReferences.push(decl.argument.literal); - } - return; } - - // Ignore if there's a grammar error - if (decl.moduleSpecifier!.kind !== SyntaxKind.StringLiteral) { - return; + else { + const localSymbol = element.kind === SyntaxKind.ExportSpecifier && element.propertyName + ? checker.getExportSpecifierLocalTargetSymbol(element)! // For re-exporting under a different name, we want to get the re-exported symbol. + : checker.getSymbolAtLocation(name)!; + addSearch(name, localSymbol); } - - if (decl.kind === SyntaxKind.ExportDeclaration) { - if (decl.exportClause && isNamedExports(decl.exportClause)) { - searchForNamedImport(decl.exportClause); + } + } + function isNameMatch(name: __String): boolean { + // Use name of "default" even in `export =` case because we may have allowSyntheticDefaultImports + return name === exportSymbol.escapedName || exportKind !== ExportKind.Named && name === InternalSymbolName.Default; + } +} +/** Returns 'true' is the namespace 'name' is re-exported from this module, and 'false' if it is only used locally. */ +/* @internal */ +function findNamespaceReExports(sourceFileLike: SourceFileLike, name: Identifier, checker: TypeChecker): boolean { + const namespaceImportSymbol = checker.getSymbolAtLocation(name); + return !!forEachPossibleImportOrExportStatement(sourceFileLike, statement => { + if (!isExportDeclaration(statement)) + return; + const { exportClause, moduleSpecifier } = statement; + return !moduleSpecifier && exportClause && isNamedExports(exportClause) && + exportClause.elements.some(element => checker.getExportSpecifierLocalTargetSymbol(element) === namespaceImportSymbol); + }); +} +/* @internal */ +export type ModuleReference = +/** "import" also includes require() calls. */ +{ + kind: "import"; + literal: StringLiteralLike; +} +/** or */ + | { + kind: "reference"; + referencingFile: SourceFile; + ref: FileReference; +}; +/* @internal */ +export function findModuleReferences(program: Program, sourceFiles: readonly SourceFile[], searchModuleSymbol: Symbol): ModuleReference[] { + const refs: ModuleReference[] = []; + const checker = program.getTypeChecker(); + for (const referencingFile of sourceFiles) { + const searchSourceFile = searchModuleSymbol.valueDeclaration; + if (searchSourceFile.kind === SyntaxKind.SourceFile) { + for (const ref of referencingFile.referencedFiles) { + if (program.getSourceFileFromReference(referencingFile, ref) === searchSourceFile) { + refs.push({ kind: "reference", referencingFile, ref }); } - return; } - - const { name, namedBindings } = decl.importClause || { name: undefined, namedBindings: undefined }; - - if (namedBindings) { - switch (namedBindings.kind) { - case SyntaxKind.NamespaceImport: - handleNamespaceImportLike(namedBindings.name); - break; - case SyntaxKind.NamedImports: - // 'default' might be accessed as a named import `{ default as foo }`. - if (exportKind === ExportKind.Named || exportKind === ExportKind.Default) { - searchForNamedImport(namedBindings); - } - break; - default: - Debug.assertNever(namedBindings); + for (const ref of referencingFile.typeReferenceDirectives) { + const referenced = program.getResolvedTypeReferenceDirectives().get(ref.fileName); + if (referenced !== undefined && referenced.resolvedFileName === (searchSourceFile as SourceFile).fileName) { + refs.push({ kind: "reference", referencingFile, ref }); } } - - // `export =` might be imported by a default import if `--allowSyntheticDefaultImports` is on, so this handles both ExportKind.Default and ExportKind.ExportEquals. - // If a default import has the same name as the default export, allow to rename it. - // Given `import f` and `export default function f`, we will rename both, but for `import g` we will rename just that. - if (name && (exportKind === ExportKind.Default || exportKind === ExportKind.ExportEquals) && (!isForRename || name.escapedText === symbolEscapedNameNoDefault(exportSymbol))) { - const defaultImportAlias = checker.getSymbolAtLocation(name)!; - addSearch(name, defaultImportAlias); - } - } - - /** - * `import x = require("./x")` or `import * as x from "./x"`. - * An `export =` may be imported by this syntax, so it may be a direct import. - * If it's not a direct import, it will be in `indirectUsers`, so we don't have to do anything here. - */ - function handleNamespaceImportLike(importName: Identifier): void { - // Don't rename an import that already has a different name than the export. - if (exportKind === ExportKind.ExportEquals && (!isForRename || isNameMatch(importName.escapedText))) { - addSearch(importName, checker.getSymbolAtLocation(importName)!); - } } - - function searchForNamedImport(namedBindings: NamedImportsOrExports | undefined): void { - if (!namedBindings) { - return; + forEachImport(referencingFile, (_importDecl, moduleSpecifier) => { + const moduleSymbol = checker.getSymbolAtLocation(moduleSpecifier); + if (moduleSymbol === searchModuleSymbol) { + refs.push({ kind: "import", literal: moduleSpecifier }); } - - for (const element of namedBindings.elements) { - const { name, propertyName } = element; - if (!isNameMatch((propertyName || name).escapedText)) { - continue; - } - - if (propertyName) { - // This is `import { foo as bar } from "./a"` or `export { foo as bar } from "./a"`. `foo` isn't a local in the file, so just add it as a single reference. - singleReferences.push(propertyName); - // If renaming `{ foo as bar }`, don't touch `bar`, just `foo`. - // But do rename `foo` in ` { default as foo }` if that's the original export name. - if (!isForRename || name.escapedText === exportSymbol.escapedName) { - // Search locally for `bar`. - addSearch(name, checker.getSymbolAtLocation(name)!); - } - } - else { - const localSymbol = element.kind === SyntaxKind.ExportSpecifier && element.propertyName - ? checker.getExportSpecifierLocalTargetSymbol(element)! // For re-exporting under a different name, we want to get the re-exported symbol. - : checker.getSymbolAtLocation(name)!; - addSearch(name, localSymbol); + }); + } + return refs; +} +/** Returns a map from a module symbol Id to all import statements that directly reference the module. */ +/* @internal */ +function getDirectImportsMap(sourceFiles: readonly SourceFile[], checker: TypeChecker, cancellationToken: CancellationToken | undefined): ts.Map { + const map = createMap(); + for (const sourceFile of sourceFiles) { + if (cancellationToken) + cancellationToken.throwIfCancellationRequested(); + forEachImport(sourceFile, (importDecl, moduleSpecifier) => { + const moduleSymbol = checker.getSymbolAtLocation(moduleSpecifier); + if (moduleSymbol) { + const id = getSymbolId(moduleSymbol).toString(); + let imports = map.get(id); + if (!imports) { + map.set(id, imports = []); } + imports.push(importDecl); } - } - - function isNameMatch(name: __String): boolean { - // Use name of "default" even in `export =` case because we may have allowSyntheticDefaultImports - return name === exportSymbol.escapedName || exportKind !== ExportKind.Named && name === InternalSymbolName.Default; - } - } - - /** Returns 'true' is the namespace 'name' is re-exported from this module, and 'false' if it is only used locally. */ - function findNamespaceReExports(sourceFileLike: SourceFileLike, name: Identifier, checker: TypeChecker): boolean { - const namespaceImportSymbol = checker.getSymbolAtLocation(name); - - return !!forEachPossibleImportOrExportStatement(sourceFileLike, statement => { - if (!isExportDeclaration(statement)) return; - const { exportClause, moduleSpecifier } = statement; - return !moduleSpecifier && exportClause && isNamedExports(exportClause) && - exportClause.elements.some(element => checker.getExportSpecifierLocalTargetSymbol(element) === namespaceImportSymbol); }); } - - export type ModuleReference = - /** "import" also includes require() calls. */ - | { kind: "import", literal: StringLiteralLike } - /** or */ - | { kind: "reference", referencingFile: SourceFile, ref: FileReference }; - export function findModuleReferences(program: Program, sourceFiles: readonly SourceFile[], searchModuleSymbol: Symbol): ModuleReference[] { - const refs: ModuleReference[] = []; - const checker = program.getTypeChecker(); - for (const referencingFile of sourceFiles) { - const searchSourceFile = searchModuleSymbol.valueDeclaration; - if (searchSourceFile.kind === SyntaxKind.SourceFile) { - for (const ref of referencingFile.referencedFiles) { - if (program.getSourceFileFromReference(referencingFile, ref) === searchSourceFile) { - refs.push({ kind: "reference", referencingFile, ref }); + return map; +} +/** Iterates over all statements at the top level or in module declarations. Returns the first truthy result. */ +/* @internal */ +function forEachPossibleImportOrExportStatement(sourceFileLike: SourceFileLike, action: (statement: Statement) => T) { + return forEach(sourceFileLike.kind === SyntaxKind.SourceFile ? sourceFileLike.statements : sourceFileLike.body!.statements, statement => // TODO: GH#18217 + action(statement) || (isAmbientModuleDeclaration(statement) && forEach(statement.body && statement.body.statements, action))); +} +/** Calls `action` for each import, re-export, or require() in a file. */ +/* @internal */ +function forEachImport(sourceFile: SourceFile, action: (importStatement: ImporterOrCallExpression, imported: StringLiteralLike) => void): void { + if (sourceFile.externalModuleIndicator || sourceFile.imports !== undefined) { + for (const i of sourceFile.imports) { + action(importFromModuleSpecifier(i), i); + } + } + else { + forEachPossibleImportOrExportStatement(sourceFile, statement => { + switch (statement.kind) { + case SyntaxKind.ExportDeclaration: + case SyntaxKind.ImportDeclaration: { + const decl = (statement as ImportDeclaration | ExportDeclaration); + if (decl.moduleSpecifier && isStringLiteral(decl.moduleSpecifier)) { + action(decl, decl.moduleSpecifier); } + break; } - for (const ref of referencingFile.typeReferenceDirectives) { - const referenced = program.getResolvedTypeReferenceDirectives().get(ref.fileName); - if (referenced !== undefined && referenced.resolvedFileName === (searchSourceFile as SourceFile).fileName) { - refs.push({ kind: "reference", referencingFile, ref }); + case SyntaxKind.ImportEqualsDeclaration: { + const decl = (statement as ImportEqualsDeclaration); + if (isExternalModuleImportEquals(decl)) { + action(decl, decl.moduleReference.expression); } + break; } } - - forEachImport(referencingFile, (_importDecl, moduleSpecifier) => { - const moduleSymbol = checker.getSymbolAtLocation(moduleSpecifier); - if (moduleSymbol === searchModuleSymbol) { - refs.push({ kind: "import", literal: moduleSpecifier }); - } - }); - } - return refs; - } - - /** Returns a map from a module symbol Id to all import statements that directly reference the module. */ - function getDirectImportsMap(sourceFiles: readonly SourceFile[], checker: TypeChecker, cancellationToken: CancellationToken | undefined): Map { - const map = createMap(); - - for (const sourceFile of sourceFiles) { - if (cancellationToken) cancellationToken.throwIfCancellationRequested(); - forEachImport(sourceFile, (importDecl, moduleSpecifier) => { - const moduleSymbol = checker.getSymbolAtLocation(moduleSpecifier); - if (moduleSymbol) { - const id = getSymbolId(moduleSymbol).toString(); - let imports = map.get(id); - if (!imports) { - map.set(id, imports = []); - } - imports.push(importDecl); - } - }); - } - - return map; - } - - /** Iterates over all statements at the top level or in module declarations. Returns the first truthy result. */ - function forEachPossibleImportOrExportStatement(sourceFileLike: SourceFileLike, action: (statement: Statement) => T) { - return forEach(sourceFileLike.kind === SyntaxKind.SourceFile ? sourceFileLike.statements : sourceFileLike.body!.statements, statement => // TODO: GH#18217 - action(statement) || (isAmbientModuleDeclaration(statement) && forEach(statement.body && statement.body.statements, action))); + }); } - - /** Calls `action` for each import, re-export, or require() in a file. */ - function forEachImport(sourceFile: SourceFile, action: (importStatement: ImporterOrCallExpression, imported: StringLiteralLike) => void): void { - if (sourceFile.externalModuleIndicator || sourceFile.imports !== undefined) { - for (const i of sourceFile.imports) { - action(importFromModuleSpecifier(i), i); +} +/* @internal */ +export interface ImportedSymbol { + kind: ImportExport.Import; + symbol: Symbol; +} +/* @internal */ +export interface ExportedSymbol { + kind: ImportExport.Export; + symbol: Symbol; + exportInfo: ExportInfo; +} +/** + * Given a local reference, we might notice that it's an import/export and recursively search for references of that. + * If at an import, look locally for the symbol it imports. + * If at an export, look for all imports of it. + * This doesn't handle export specifiers; that is done in `getReferencesAtExportSpecifier`. + * @param comingFromExport If we are doing a search for all exports, don't bother looking backwards for the imported symbol, since that's the reason we're here. + */ +/* @internal */ +export function getImportOrExportSymbol(node: Node, symbol: Symbol, checker: TypeChecker, comingFromExport: boolean): ImportedSymbol | ExportedSymbol | undefined { + return comingFromExport ? getExport() : getExport() || getImport(); + function getExport(): ExportedSymbol | ImportedSymbol | undefined { + const { parent } = node; + const grandParent = parent.parent; + if (symbol.exportSymbol) { + if (parent.kind === SyntaxKind.PropertyAccessExpression) { + // When accessing an export of a JS module, there's no alias. The symbol will still be flagged as an export even though we're at the use. + // So check that we are at the declaration. + return symbol.declarations.some(d => d === parent) && isBinaryExpression(grandParent) + ? getSpecialPropertyExport(grandParent, /*useLhsSymbol*/ false) + : undefined; + } + else { + return exportInfo(symbol.exportSymbol, getExportKindForDeclaration(parent)); } } else { - forEachPossibleImportOrExportStatement(sourceFile, statement => { - switch (statement.kind) { - case SyntaxKind.ExportDeclaration: - case SyntaxKind.ImportDeclaration: { - const decl = statement as ImportDeclaration | ExportDeclaration; - if (decl.moduleSpecifier && isStringLiteral(decl.moduleSpecifier)) { - action(decl, decl.moduleSpecifier); - } - break; - } - - case SyntaxKind.ImportEqualsDeclaration: { - const decl = statement as ImportEqualsDeclaration; - if (isExternalModuleImportEquals(decl)) { - action(decl, decl.moduleReference.expression); - } - break; + const exportNode = getExportNode(parent, node); + if (exportNode && hasModifier(exportNode, ModifierFlags.Export)) { + if (isImportEqualsDeclaration(exportNode) && exportNode.moduleReference === node) { + // We're at `Y` in `export import X = Y`. This is not the exported symbol, the left-hand-side is. So treat this as an import statement. + if (comingFromExport) { + return undefined; } - } - }); - } - } - - export interface ImportedSymbol { - kind: ImportExport.Import; - symbol: Symbol; - } - export interface ExportedSymbol { - kind: ImportExport.Export; - symbol: Symbol; - exportInfo: ExportInfo; - } - - /** - * Given a local reference, we might notice that it's an import/export and recursively search for references of that. - * If at an import, look locally for the symbol it imports. - * If at an export, look for all imports of it. - * This doesn't handle export specifiers; that is done in `getReferencesAtExportSpecifier`. - * @param comingFromExport If we are doing a search for all exports, don't bother looking backwards for the imported symbol, since that's the reason we're here. - */ - export function getImportOrExportSymbol(node: Node, symbol: Symbol, checker: TypeChecker, comingFromExport: boolean): ImportedSymbol | ExportedSymbol | undefined { - return comingFromExport ? getExport() : getExport() || getImport(); - - function getExport(): ExportedSymbol | ImportedSymbol | undefined { - const { parent } = node; - const grandParent = parent.parent; - if (symbol.exportSymbol) { - if (parent.kind === SyntaxKind.PropertyAccessExpression) { - // When accessing an export of a JS module, there's no alias. The symbol will still be flagged as an export even though we're at the use. - // So check that we are at the declaration. - return symbol.declarations.some(d => d === parent) && isBinaryExpression(grandParent) - ? getSpecialPropertyExport(grandParent, /*useLhsSymbol*/ false) - : undefined; + const lhsSymbol = checker.getSymbolAtLocation(exportNode.name)!; + return { kind: ImportExport.Import, symbol: lhsSymbol }; } else { - return exportInfo(symbol.exportSymbol, getExportKindForDeclaration(parent)); + return exportInfo(symbol, getExportKindForDeclaration(exportNode)); } } - else { - const exportNode = getExportNode(parent, node); - if (exportNode && hasModifier(exportNode, ModifierFlags.Export)) { - if (isImportEqualsDeclaration(exportNode) && exportNode.moduleReference === node) { - // We're at `Y` in `export import X = Y`. This is not the exported symbol, the left-hand-side is. So treat this as an import statement. - if (comingFromExport) { - return undefined; - } - - const lhsSymbol = checker.getSymbolAtLocation(exportNode.name)!; - return { kind: ImportExport.Import, symbol: lhsSymbol }; - } - else { - return exportInfo(symbol, getExportKindForDeclaration(exportNode)); - } - } - // If we are in `export = a;` or `export default a;`, `parent` is the export assignment. - else if (isExportAssignment(parent)) { - return getExportAssignmentExport(parent); - } - // If we are in `export = class A {};` (or `export = class A {};`) at `A`, `parent.parent` is the export assignment. - else if (isExportAssignment(grandParent)) { - return getExportAssignmentExport(grandParent); - } - // Similar for `module.exports =` and `exports.A =`. - else if (isBinaryExpression(parent)) { - return getSpecialPropertyExport(parent, /*useLhsSymbol*/ true); - } - else if (isBinaryExpression(grandParent)) { - return getSpecialPropertyExport(grandParent, /*useLhsSymbol*/ true); - } - else if (isJSDocTypedefTag(parent)) { - return exportInfo(symbol, ExportKind.Named); - } + // If we are in `export = a;` or `export default a;`, `parent` is the export assignment. + else if (isExportAssignment(parent)) { + return getExportAssignmentExport(parent); } - - function getExportAssignmentExport(ex: ExportAssignment): ExportedSymbol { - // Get the symbol for the `export =` node; its parent is the module it's the export of. - const exportingModuleSymbol = Debug.checkDefined(ex.symbol.parent, "Expected export symbol to have a parent"); - const exportKind = ex.isExportEquals ? ExportKind.ExportEquals : ExportKind.Default; - return { kind: ImportExport.Export, symbol, exportInfo: { exportingModuleSymbol, exportKind } }; + // If we are in `export = class A {};` (or `export = class A {};`) at `A`, `parent.parent` is the export assignment. + else if (isExportAssignment(grandParent)) { + return getExportAssignmentExport(grandParent); } - - function getSpecialPropertyExport(node: BinaryExpression, useLhsSymbol: boolean): ExportedSymbol | undefined { - let kind: ExportKind; - switch (getAssignmentDeclarationKind(node)) { - case AssignmentDeclarationKind.ExportsProperty: - kind = ExportKind.Named; - break; - case AssignmentDeclarationKind.ModuleExports: - kind = ExportKind.ExportEquals; - break; - default: - return undefined; - } - - const sym = useLhsSymbol ? checker.getSymbolAtLocation(getNameOfAccessExpression(cast(node.left, isAccessExpression))) : symbol; - return sym && exportInfo(sym, kind); + // Similar for `module.exports =` and `exports.A =`. + else if (isBinaryExpression(parent)) { + return getSpecialPropertyExport(parent, /*useLhsSymbol*/ true); } - } - - function getImport(): ImportedSymbol | undefined { - const isImport = isNodeImport(node); - if (!isImport) return undefined; - - // A symbol being imported is always an alias. So get what that aliases to find the local symbol. - let importedSymbol = checker.getImmediateAliasedSymbol(symbol); - if (!importedSymbol) return undefined; - - // Search on the local symbol in the exporting module, not the exported symbol. - importedSymbol = skipExportSpecifierSymbol(importedSymbol, checker); - // Similarly, skip past the symbol for 'export =' - if (importedSymbol.escapedName === "export=") { - importedSymbol = getExportEqualsLocalSymbol(importedSymbol, checker); + else if (isBinaryExpression(grandParent)) { + return getSpecialPropertyExport(grandParent, /*useLhsSymbol*/ true); } - - // If the import has a different name than the export, do not continue searching. - // If `importedName` is undefined, do continue searching as the export is anonymous. - // (All imports returned from this function will be ignored anyway if we are in rename and this is a not a named export.) - const importedName = symbolEscapedNameNoDefault(importedSymbol); - if (importedName === undefined || importedName === InternalSymbolName.Default || importedName === symbol.escapedName) { - return { kind: ImportExport.Import, symbol: importedSymbol }; + else if (isJSDocTypedefTag(parent)) { + return exportInfo(symbol, ExportKind.Named); } } - - function exportInfo(symbol: Symbol, kind: ExportKind): ExportedSymbol | undefined { - const exportInfo = getExportInfo(symbol, kind, checker); - return exportInfo && { kind: ImportExport.Export, symbol, exportInfo }; + function getExportAssignmentExport(ex: ExportAssignment): ExportedSymbol { + // Get the symbol for the `export =` node; its parent is the module it's the export of. + const exportingModuleSymbol = Debug.checkDefined(ex.symbol.parent, "Expected export symbol to have a parent"); + const exportKind = ex.isExportEquals ? ExportKind.ExportEquals : ExportKind.Default; + return { kind: ImportExport.Export, symbol, exportInfo: { exportingModuleSymbol, exportKind } }; } - - // Not meant for use with export specifiers or export assignment. - function getExportKindForDeclaration(node: Node): ExportKind { - return hasModifier(node, ModifierFlags.Default) ? ExportKind.Default : ExportKind.Named; + function getSpecialPropertyExport(node: BinaryExpression, useLhsSymbol: boolean): ExportedSymbol | undefined { + let kind: ExportKind; + switch (getAssignmentDeclarationKind(node)) { + case AssignmentDeclarationKind.ExportsProperty: + kind = ExportKind.Named; + break; + case AssignmentDeclarationKind.ModuleExports: + kind = ExportKind.ExportEquals; + break; + default: + return undefined; + } + const sym = useLhsSymbol ? checker.getSymbolAtLocation(getNameOfAccessExpression(cast(node.left, isAccessExpression))) : symbol; + return sym && exportInfo(sym, kind); } } - - function getExportEqualsLocalSymbol(importedSymbol: Symbol, checker: TypeChecker): Symbol { - if (importedSymbol.flags & SymbolFlags.Alias) { - return Debug.checkDefined(checker.getImmediateAliasedSymbol(importedSymbol)); - } - - const decl = importedSymbol.valueDeclaration; - if (isExportAssignment(decl)) { // `export = class {}` - return Debug.checkDefined(decl.expression.symbol); - } - else if (isBinaryExpression(decl)) { // `module.exports = class {}` - return Debug.checkDefined(decl.right.symbol); + function getImport(): ImportedSymbol | undefined { + const isImport = isNodeImport(node); + if (!isImport) + return undefined; + // A symbol being imported is always an alias. So get what that aliases to find the local symbol. + let importedSymbol = checker.getImmediateAliasedSymbol(symbol); + if (!importedSymbol) + return undefined; + // Search on the local symbol in the exporting module, not the exported symbol. + importedSymbol = skipExportSpecifierSymbol(importedSymbol, checker); + // Similarly, skip past the symbol for 'export =' + if (importedSymbol.escapedName === "export=") { + importedSymbol = getExportEqualsLocalSymbol(importedSymbol, checker); } - else if (isSourceFile(decl)) { // json module - return Debug.checkDefined(decl.symbol); + // If the import has a different name than the export, do not continue searching. + // If `importedName` is undefined, do continue searching as the export is anonymous. + // (All imports returned from this function will be ignored anyway if we are in rename and this is a not a named export.) + const importedName = symbolEscapedNameNoDefault(importedSymbol); + if (importedName === undefined || importedName === InternalSymbolName.Default || importedName === symbol.escapedName) { + return { kind: ImportExport.Import, symbol: importedSymbol }; } - return Debug.fail(); } - - // If a reference is a class expression, the exported node would be its parent. - // If a reference is a variable declaration, the exported node would be the variable statement. - function getExportNode(parent: Node, node: Node): Node | undefined { - const declaration = isVariableDeclaration(parent) ? parent : isBindingElement(parent) ? walkUpBindingElementsAndPatterns(parent) : undefined; - if (declaration) { - return (parent as VariableDeclaration | BindingElement).name !== node ? undefined : - isCatchClause(declaration.parent) ? undefined : isVariableStatement(declaration.parent.parent) ? declaration.parent.parent : undefined; - } - else { - return parent; - } + function exportInfo(symbol: Symbol, kind: ExportKind): ExportedSymbol | undefined { + const exportInfo = getExportInfo(symbol, kind, checker); + return exportInfo && { kind: ImportExport.Export, symbol, exportInfo }; } - - function isNodeImport(node: Node): boolean { - const { parent } = node; - switch (parent.kind) { - case SyntaxKind.ImportEqualsDeclaration: - return (parent as ImportEqualsDeclaration).name === node && isExternalModuleImportEquals(parent as ImportEqualsDeclaration); - case SyntaxKind.ImportSpecifier: - // For a rename import `{ foo as bar }`, don't search for the imported symbol. Just find local uses of `bar`. - return !(parent as ImportSpecifier).propertyName; - case SyntaxKind.ImportClause: - case SyntaxKind.NamespaceImport: - Debug.assert((parent as ImportClause | NamespaceImport).name === node); - return true; - default: - return false; - } + // Not meant for use with export specifiers or export assignment. + function getExportKindForDeclaration(node: Node): ExportKind { + return hasModifier(node, ModifierFlags.Default) ? ExportKind.Default : ExportKind.Named; } - - export function getExportInfo(exportSymbol: Symbol, exportKind: ExportKind, checker: TypeChecker): ExportInfo | undefined { - const moduleSymbol = exportSymbol.parent; - if (!moduleSymbol) return undefined; // This can happen if an `export` is not at the top-level (which is a compile error). - const exportingModuleSymbol = checker.getMergedSymbol(moduleSymbol); // Need to get merged symbol in case there's an augmentation. - // `export` may appear in a namespace. In that case, just rely on global search. - return isExternalModuleSymbol(exportingModuleSymbol) ? { exportingModuleSymbol, exportKind } : undefined; +} +/* @internal */ +function getExportEqualsLocalSymbol(importedSymbol: Symbol, checker: TypeChecker): Symbol { + if (importedSymbol.flags & SymbolFlags.Alias) { + return Debug.checkDefined(checker.getImmediateAliasedSymbol(importedSymbol)); } - - /** If at an export specifier, go to the symbol it refers to. */ - function skipExportSpecifierSymbol(symbol: Symbol, checker: TypeChecker): Symbol { - // For `export { foo } from './bar", there's nothing to skip, because it does not create a new alias. But `export { foo } does. - if (symbol.declarations) { - for (const declaration of symbol.declarations) { - if (isExportSpecifier(declaration) && !declaration.propertyName && !declaration.parent.parent.moduleSpecifier) { - return checker.getExportSpecifierLocalTargetSymbol(declaration)!; - } - } - } - return symbol; + const decl = importedSymbol.valueDeclaration; + if (isExportAssignment(decl)) { // `export = class {}` + return Debug.checkDefined(decl.expression.symbol); } - - function getContainingModuleSymbol(importer: Importer, checker: TypeChecker): Symbol { - return checker.getMergedSymbol(getSourceFileLikeForImportDeclaration(importer).symbol); + else if (isBinaryExpression(decl)) { // `module.exports = class {}` + return Debug.checkDefined(decl.right.symbol); } - - function getSourceFileLikeForImportDeclaration(node: ImporterOrCallExpression): SourceFileLike { - if (node.kind === SyntaxKind.CallExpression) { - return node.getSourceFile(); - } - - const { parent } = node; - if (parent.kind === SyntaxKind.SourceFile) { - return parent as SourceFile; + else if (isSourceFile(decl)) { // json module + return Debug.checkDefined(decl.symbol); + } + return Debug.fail(); +} +// If a reference is a class expression, the exported node would be its parent. +// If a reference is a variable declaration, the exported node would be the variable statement. +/* @internal */ +function getExportNode(parent: Node, node: Node): Node | undefined { + const declaration = isVariableDeclaration(parent) ? parent : isBindingElement(parent) ? walkUpBindingElementsAndPatterns(parent) : undefined; + if (declaration) { + return (parent as VariableDeclaration | BindingElement).name !== node ? undefined : + isCatchClause(declaration.parent) ? undefined : isVariableStatement(declaration.parent.parent) ? declaration.parent.parent : undefined; + } + else { + return parent; + } +} +/* @internal */ +function isNodeImport(node: Node): boolean { + const { parent } = node; + switch (parent.kind) { + case SyntaxKind.ImportEqualsDeclaration: + return (parent as ImportEqualsDeclaration).name === node && isExternalModuleImportEquals((parent as ImportEqualsDeclaration)); + case SyntaxKind.ImportSpecifier: + // For a rename import `{ foo as bar }`, don't search for the imported symbol. Just find local uses of `bar`. + return !(parent as ImportSpecifier).propertyName; + case SyntaxKind.ImportClause: + case SyntaxKind.NamespaceImport: + Debug.assert((parent as ImportClause | NamespaceImport).name === node); + return true; + default: + return false; + } +} +/* @internal */ +export function getExportInfo(exportSymbol: Symbol, exportKind: ExportKind, checker: TypeChecker): ExportInfo | undefined { + const moduleSymbol = exportSymbol.parent; + if (!moduleSymbol) + return undefined; // This can happen if an `export` is not at the top-level (which is a compile error). + const exportingModuleSymbol = checker.getMergedSymbol(moduleSymbol); // Need to get merged symbol in case there's an augmentation. + // `export` may appear in a namespace. In that case, just rely on global search. + return isExternalModuleSymbol(exportingModuleSymbol) ? { exportingModuleSymbol, exportKind } : undefined; +} +/** If at an export specifier, go to the symbol it refers to. */ +/* @internal */ +function skipExportSpecifierSymbol(symbol: Symbol, checker: TypeChecker): Symbol { + // For `export { foo } from './bar", there's nothing to skip, because it does not create a new alias. But `export { foo } does. + if (symbol.declarations) { + for (const declaration of symbol.declarations) { + if (isExportSpecifier(declaration) && !declaration.propertyName && !declaration.parent.parent.moduleSpecifier) { + return checker.getExportSpecifierLocalTargetSymbol(declaration)!; + } } - Debug.assert(parent.kind === SyntaxKind.ModuleBlock); - return cast(parent.parent, isAmbientModuleDeclaration); } - - function isAmbientModuleDeclaration(node: Node): node is AmbientModuleDeclaration { - return node.kind === SyntaxKind.ModuleDeclaration && (node as ModuleDeclaration).name.kind === SyntaxKind.StringLiteral; + return symbol; +} +/* @internal */ +function getContainingModuleSymbol(importer: Importer, checker: TypeChecker): Symbol { + return checker.getMergedSymbol(getSourceFileLikeForImportDeclaration(importer).symbol); +} +/* @internal */ +function getSourceFileLikeForImportDeclaration(node: ImporterOrCallExpression): SourceFileLike { + if (node.kind === SyntaxKind.CallExpression) { + return node.getSourceFile(); } - - function isExternalModuleImportEquals(eq: ImportEqualsDeclaration): eq is ImportEqualsDeclaration & { moduleReference: { expression: StringLiteral } } { - return eq.moduleReference.kind === SyntaxKind.ExternalModuleReference && eq.moduleReference.expression.kind === SyntaxKind.StringLiteral; + const { parent } = node; + if (parent.kind === SyntaxKind.SourceFile) { + return parent as SourceFile; } + Debug.assert(parent.kind === SyntaxKind.ModuleBlock); + return cast(parent.parent, isAmbientModuleDeclaration); +} +/* @internal */ +function isAmbientModuleDeclaration(node: Node): node is AmbientModuleDeclaration { + return node.kind === SyntaxKind.ModuleDeclaration && (node as ModuleDeclaration).name.kind === SyntaxKind.StringLiteral; +} +/* @internal */ +function isExternalModuleImportEquals(eq: ImportEqualsDeclaration): eq is ImportEqualsDeclaration & { + moduleReference: { + expression: StringLiteral; + }; +} { + return eq.moduleReference.kind === SyntaxKind.ExternalModuleReference && eq.moduleReference.expression.kind === SyntaxKind.StringLiteral; } diff --git a/src/services/jsDoc.ts b/src/services/jsDoc.ts index 7118fe8b76181..ad76468afbd2f 100644 --- a/src/services/jsDoc.ts +++ b/src/services/jsDoc.ts @@ -1,407 +1,393 @@ +import { CompletionEntry, Declaration, SymbolDisplayPart, forEachUnique, lineBreakPart, textPart, JSDoc, JSDocTag, SyntaxKind, JSDocPropertyTag, JSDocTypedefTag, getJSDocCommentsAndTags, JSDocTagInfo, getJSDocTags, JSDocImplementsTag, JSDocAugmentsTag, JSDocTemplateTag, JSDocTypeTag, JSDocParameterTag, Node, NodeArray, map, ScriptElementKind, CompletionEntryDetails, emptyArray, isIdentifier, isFunctionLike, mapDefined, isJSDocParameterTag, startsWith, SourceFile, TextInsertion, getTokenAtPosition, findAncestor, isJSDoc, length, hasJSFileExtension, getLineStartPositionForPosition, isWhiteSpaceSingleLine, ParameterDeclaration, forEachAncestor, FunctionDeclaration, MethodDeclaration, ConstructorDeclaration, MethodSignature, PropertyAssignment, VariableStatement, BinaryExpression, getAssignmentDeclarationKind, AssignmentDeclarationKind, PropertyDeclaration, isFunctionExpression, isArrowFunction, Expression, ParenthesizedExpression, FunctionExpression, find, ClassExpression, isConstructorDeclaration } from "./ts"; /* @internal */ -namespace ts.JsDoc { - const jsDocTagNames = [ - "abstract", - "access", - "alias", - "argument", - "async", - "augments", - "author", - "borrows", - "callback", - "class", - "classdesc", - "constant", - "constructor", - "constructs", - "copyright", - "default", - "deprecated", - "description", - "emits", - "enum", - "event", - "example", - "exports", - "extends", - "external", - "field", - "file", - "fileoverview", - "fires", - "function", - "generator", - "global", - "hideconstructor", - "host", - "ignore", - "implements", - "inheritdoc", - "inner", - "instance", - "interface", - "kind", - "lends", - "license", - "listens", - "member", - "memberof", - "method", - "mixes", - "module", - "name", - "namespace", - "override", - "package", - "param", - "private", - "property", - "protected", - "public", - "readonly", - "requires", - "returns", - "see", - "since", - "static", - "summary", - "template", - "this", - "throws", - "todo", - "tutorial", - "type", - "typedef", - "var", - "variation", - "version", - "virtual", - "yields" - ]; - let jsDocTagNameCompletionEntries: CompletionEntry[]; - let jsDocTagCompletionEntries: CompletionEntry[]; - - export function getJsDocCommentsFromDeclarations(declarations: readonly Declaration[]): SymbolDisplayPart[] { - // Only collect doc comments from duplicate declarations once: - // In case of a union property there might be same declaration multiple times - // which only varies in type parameter - // Eg. const a: Array | Array; a.length - // The property length will have two declarations of property length coming - // from Array - Array and Array - const documentationComment: SymbolDisplayPart[] = []; - forEachUnique(declarations, declaration => { - for (const { comment } of getCommentHavingNodes(declaration)) { - if (comment === undefined) continue; - if (documentationComment.length) { - documentationComment.push(lineBreakPart()); - } - documentationComment.push(textPart(comment)); +const jsDocTagNames = [ + "abstract", + "access", + "alias", + "argument", + "async", + "augments", + "author", + "borrows", + "callback", + "class", + "classdesc", + "constant", + "constructor", + "constructs", + "copyright", + "default", + "deprecated", + "description", + "emits", + "enum", + "event", + "example", + "exports", + "extends", + "external", + "field", + "file", + "fileoverview", + "fires", + "function", + "generator", + "global", + "hideconstructor", + "host", + "ignore", + "implements", + "inheritdoc", + "inner", + "instance", + "interface", + "kind", + "lends", + "license", + "listens", + "member", + "memberof", + "method", + "mixes", + "module", + "name", + "namespace", + "override", + "package", + "param", + "private", + "property", + "protected", + "public", + "readonly", + "requires", + "returns", + "see", + "since", + "static", + "summary", + "template", + "this", + "throws", + "todo", + "tutorial", + "type", + "typedef", + "var", + "variation", + "version", + "virtual", + "yields" +]; +/* @internal */ +let jsDocTagNameCompletionEntries: CompletionEntry[]; +/* @internal */ +let jsDocTagCompletionEntries: CompletionEntry[]; +/* @internal */ +export function getJsDocCommentsFromDeclarations(declarations: readonly Declaration[]): SymbolDisplayPart[] { + // Only collect doc comments from duplicate declarations once: + // In case of a union property there might be same declaration multiple times + // which only varies in type parameter + // Eg. const a: Array | Array; a.length + // The property length will have two declarations of property length coming + // from Array - Array and Array + const documentationComment: SymbolDisplayPart[] = []; + forEachUnique(declarations, declaration => { + for (const { comment } of getCommentHavingNodes(declaration)) { + if (comment === undefined) + continue; + if (documentationComment.length) { + documentationComment.push(lineBreakPart()); } - }); - return documentationComment; - } - - function getCommentHavingNodes(declaration: Declaration): readonly (JSDoc | JSDocTag)[] { - switch (declaration.kind) { - case SyntaxKind.JSDocParameterTag: - case SyntaxKind.JSDocPropertyTag: - return [declaration as JSDocPropertyTag]; - case SyntaxKind.JSDocCallbackTag: - case SyntaxKind.JSDocTypedefTag: - return [(declaration as JSDocTypedefTag), (declaration as JSDocTypedefTag).parent]; - default: - return getJSDocCommentsAndTags(declaration); + documentationComment.push(textPart(comment)); } + }); + return documentationComment; +} +/* @internal */ +function getCommentHavingNodes(declaration: Declaration): readonly (JSDoc | JSDocTag)[] { + switch (declaration.kind) { + case SyntaxKind.JSDocParameterTag: + case SyntaxKind.JSDocPropertyTag: + return [(declaration as JSDocPropertyTag)]; + case SyntaxKind.JSDocCallbackTag: + case SyntaxKind.JSDocTypedefTag: + return [(declaration as JSDocTypedefTag), (declaration as JSDocTypedefTag).parent]; + default: + return getJSDocCommentsAndTags(declaration); } - - export function getJsDocTagsFromDeclarations(declarations?: Declaration[]): JSDocTagInfo[] { - // Only collect doc comments from duplicate declarations once. - const tags: JSDocTagInfo[] = []; - forEachUnique(declarations, declaration => { - for (const tag of getJSDocTags(declaration)) { - tags.push({ name: tag.tagName.text, text: getCommentText(tag) }); - } - }); - return tags; - } - - function getCommentText(tag: JSDocTag): string | undefined { - const { comment } = tag; - switch (tag.kind) { - case SyntaxKind.JSDocImplementsTag: - return withNode((tag as JSDocImplementsTag).class); - case SyntaxKind.JSDocAugmentsTag: - return withNode((tag as JSDocAugmentsTag).class); - case SyntaxKind.JSDocTemplateTag: - return withList((tag as JSDocTemplateTag).typeParameters); - case SyntaxKind.JSDocTypeTag: - return withNode((tag as JSDocTypeTag).typeExpression); - case SyntaxKind.JSDocTypedefTag: - case SyntaxKind.JSDocCallbackTag: - case SyntaxKind.JSDocPropertyTag: - case SyntaxKind.JSDocParameterTag: - const { name } = tag as JSDocTypedefTag | JSDocPropertyTag | JSDocParameterTag; - return name ? withNode(name) : comment; - default: - return comment; - } - - function withNode(node: Node) { - return addComment(node.getText()); - } - - function withList(list: NodeArray): string { - return addComment(list.map(x => x.getText()).join(", ")); - } - - function addComment(s: string) { - return comment === undefined ? s : `${s} ${comment}`; +} +/* @internal */ +export function getJsDocTagsFromDeclarations(declarations?: Declaration[]): JSDocTagInfo[] { + // Only collect doc comments from duplicate declarations once. + const tags: JSDocTagInfo[] = []; + forEachUnique(declarations, declaration => { + for (const tag of getJSDocTags(declaration)) { + tags.push({ name: tag.tagName.text, text: getCommentText(tag) }); } + }); + return tags; +} +/* @internal */ +function getCommentText(tag: JSDocTag): string | undefined { + const { comment } = tag; + switch (tag.kind) { + case SyntaxKind.JSDocImplementsTag: + return withNode((tag as JSDocImplementsTag).class); + case SyntaxKind.JSDocAugmentsTag: + return withNode((tag as JSDocAugmentsTag).class); + case SyntaxKind.JSDocTemplateTag: + return withList((tag as JSDocTemplateTag).typeParameters); + case SyntaxKind.JSDocTypeTag: + return withNode((tag as JSDocTypeTag).typeExpression); + case SyntaxKind.JSDocTypedefTag: + case SyntaxKind.JSDocCallbackTag: + case SyntaxKind.JSDocPropertyTag: + case SyntaxKind.JSDocParameterTag: + const { name } = (tag as JSDocTypedefTag | JSDocPropertyTag | JSDocParameterTag); + return name ? withNode(name) : comment; + default: + return comment; } - - export function getJSDocTagNameCompletions(): CompletionEntry[] { - return jsDocTagNameCompletionEntries || (jsDocTagNameCompletionEntries = map(jsDocTagNames, tagName => { - return { - name: tagName, - kind: ScriptElementKind.keyword, - kindModifiers: "", - sortText: "0", - }; - })); + function withNode(node: Node) { + return addComment(node.getText()); } - - export const getJSDocTagNameCompletionDetails = getJSDocTagCompletionDetails; - - export function getJSDocTagCompletions(): CompletionEntry[] { - return jsDocTagCompletionEntries || (jsDocTagCompletionEntries = map(jsDocTagNames, tagName => { - return { - name: `@${tagName}`, - kind: ScriptElementKind.keyword, - kindModifiers: "", - sortText: "0" - }; - })); + function withList(list: NodeArray): string { + return addComment(list.map(x => x.getText()).join(", ")); } - - export function getJSDocTagCompletionDetails(name: string): CompletionEntryDetails { + function addComment(s: string) { + return comment === undefined ? s : `${s} ${comment}`; + } +} +/* @internal */ +export function getJSDocTagNameCompletions(): CompletionEntry[] { + return jsDocTagNameCompletionEntries || (jsDocTagNameCompletionEntries = map(jsDocTagNames, tagName => { return { - name, - kind: ScriptElementKind.unknown, // TODO: should have its own kind? + name: tagName, + kind: ScriptElementKind.keyword, kindModifiers: "", - displayParts: [textPart(name)], - documentation: emptyArray, - tags: undefined, - codeActions: undefined, + sortText: "0", }; - } - - export function getJSDocParameterNameCompletions(tag: JSDocParameterTag): CompletionEntry[] { - if (!isIdentifier(tag.name)) { - return emptyArray; - } - const nameThusFar = tag.name.text; - const jsdoc = tag.parent; - const fn = jsdoc.parent; - if (!isFunctionLike(fn)) return []; - - return mapDefined(fn.parameters, param => { - if (!isIdentifier(param.name)) return undefined; - - const name = param.name.text; - if (jsdoc.tags!.some(t => t !== tag && isJSDocParameterTag(t) && isIdentifier(t.name) && t.name.escapedText === name) // TODO: GH#18217 - || nameThusFar !== undefined && !startsWith(name, nameThusFar)) { - return undefined; - } - - return { name, kind: ScriptElementKind.parameterElement, kindModifiers: "", sortText: "0" }; - }); - } - - export function getJSDocParameterNameCompletionDetails(name: string): CompletionEntryDetails { + })); +} +/* @internal */ +export const getJSDocTagNameCompletionDetails = getJSDocTagCompletionDetails; +/* @internal */ +export function getJSDocTagCompletions(): CompletionEntry[] { + return jsDocTagCompletionEntries || (jsDocTagCompletionEntries = map(jsDocTagNames, tagName => { return { - name, - kind: ScriptElementKind.parameterElement, + name: `@${tagName}`, + kind: ScriptElementKind.keyword, kindModifiers: "", - displayParts: [textPart(name)], - documentation: emptyArray, - tags: undefined, - codeActions: undefined, + sortText: "0" }; + })); +} +/* @internal */ +export function getJSDocTagCompletionDetails(name: string): CompletionEntryDetails { + return { + name, + kind: ScriptElementKind.unknown, + kindModifiers: "", + displayParts: [textPart(name)], + documentation: emptyArray, + tags: undefined, + codeActions: undefined, + }; +} +/* @internal */ +export function getJSDocParameterNameCompletions(tag: JSDocParameterTag): CompletionEntry[] { + if (!isIdentifier(tag.name)) { + return emptyArray; } - - /** - * Checks if position points to a valid position to add JSDoc comments, and if so, - * returns the appropriate template. Otherwise returns an empty string. - * Valid positions are - * - outside of comments, statements, and expressions, and - * - preceding a: - * - function/constructor/method declaration - * - class declarations - * - variable statements - * - namespace declarations - * - interface declarations - * - method signatures - * - type alias declarations - * - * Hosts should ideally check that: - * - The line is all whitespace up to 'position' before performing the insertion. - * - If the keystroke sequence "/\*\*" induced the call, we also check that the next - * non-whitespace character is '*', which (approximately) indicates whether we added - * the second '*' to complete an existing (JSDoc) comment. - * @param fileName The file in which to perform the check. - * @param position The (character-indexed) position in the file where the check should - * be performed. - */ - export function getDocCommentTemplateAtPosition(newLine: string, sourceFile: SourceFile, position: number): TextInsertion | undefined { - const tokenAtPos = getTokenAtPosition(sourceFile, position); - const existingDocComment = findAncestor(tokenAtPos, isJSDoc); - if (existingDocComment && (existingDocComment.comment !== undefined || length(existingDocComment.tags))) { - // Non-empty comment already exists. + const nameThusFar = tag.name.text; + const jsdoc = tag.parent; + const fn = jsdoc.parent; + if (!isFunctionLike(fn)) + return []; + return mapDefined(fn.parameters, param => { + if (!isIdentifier(param.name)) return undefined; - } - - const tokenStart = tokenAtPos.getStart(sourceFile); - // Don't provide a doc comment template based on a *previous* node. (But an existing empty jsdoc comment will likely start before `position`.) - if (!existingDocComment && tokenStart < position) { - return undefined; - } - - const commentOwnerInfo = getCommentOwnerInfo(tokenAtPos); - if (!commentOwnerInfo) { - return undefined; - } - const { commentOwner, parameters } = commentOwnerInfo; - if (commentOwner.getStart(sourceFile) < position) { + const name = param.name.text; + if (jsdoc.tags!.some(t => t !== tag && isJSDocParameterTag(t) && isIdentifier(t.name) && t.name.escapedText === name) // TODO: GH#18217 + || nameThusFar !== undefined && !startsWith(name, nameThusFar)) { return undefined; } - - if (!parameters || parameters.length === 0) { - // if there are no parameters, just complete to a single line JSDoc comment - const singleLineResult = "/** */"; - return { newText: singleLineResult, caretOffset: 3 }; - } - - const indentationStr = getIndentationStringAtPosition(sourceFile, position); - - // A doc comment consists of the following - // * The opening comment line - // * the first line (without a param) for the object's untagged info (this is also where the caret ends up) - // * the '@param'-tagged lines - // * TODO: other tags. - // * the closing comment line - // * if the caret was directly in front of the object, then we add an extra line and indentation. - const preamble = "/**" + newLine + indentationStr + " * "; - const result = - preamble + newLine + - parameterDocComments(parameters, hasJSFileExtension(sourceFile.fileName), indentationStr, newLine) + - indentationStr + " */" + - (tokenStart === position ? newLine + indentationStr : ""); - - return { newText: result, caretOffset: preamble.length }; + return { name, kind: ScriptElementKind.parameterElement, kindModifiers: "", sortText: "0" }; + }); +} +/* @internal */ +export function getJSDocParameterNameCompletionDetails(name: string): CompletionEntryDetails { + return { + name, + kind: ScriptElementKind.parameterElement, + kindModifiers: "", + displayParts: [textPart(name)], + documentation: emptyArray, + tags: undefined, + codeActions: undefined, + }; +} +/** + * Checks if position points to a valid position to add JSDoc comments, and if so, + * returns the appropriate template. Otherwise returns an empty string. + * Valid positions are + * - outside of comments, statements, and expressions, and + * - preceding a: + * - function/constructor/method declaration + * - class declarations + * - variable statements + * - namespace declarations + * - interface declarations + * - method signatures + * - type alias declarations + * + * Hosts should ideally check that: + * - The line is all whitespace up to 'position' before performing the insertion. + * - If the keystroke sequence "/\*\*" induced the call, we also check that the next + * non-whitespace character is '*', which (approximately) indicates whether we added + * the second '*' to complete an existing (JSDoc) comment. + * @param fileName The file in which to perform the check. + * @param position The (character-indexed) position in the file where the check should + * be performed. + */ +/* @internal */ +export function getDocCommentTemplateAtPosition(newLine: string, sourceFile: SourceFile, position: number): TextInsertion | undefined { + const tokenAtPos = getTokenAtPosition(sourceFile, position); + const existingDocComment = findAncestor(tokenAtPos, isJSDoc); + if (existingDocComment && (existingDocComment.comment !== undefined || length(existingDocComment.tags))) { + // Non-empty comment already exists. + return undefined; } - - function getIndentationStringAtPosition(sourceFile: SourceFile, position: number): string { - const { text } = sourceFile; - const lineStart = getLineStartPositionForPosition(position, sourceFile); - let pos = lineStart; - for (; pos <= position && isWhiteSpaceSingleLine(text.charCodeAt(pos)); pos++); - return text.slice(lineStart, pos); + const tokenStart = tokenAtPos.getStart(sourceFile); + // Don't provide a doc comment template based on a *previous* node. (But an existing empty jsdoc comment will likely start before `position`.) + if (!existingDocComment && tokenStart < position) { + return undefined; } - - function parameterDocComments(parameters: readonly ParameterDeclaration[], isJavaScriptFile: boolean, indentationStr: string, newLine: string): string { - return parameters.map(({ name, dotDotDotToken }, i) => { - const paramName = name.kind === SyntaxKind.Identifier ? name.text : "param" + i; - const type = isJavaScriptFile ? (dotDotDotToken ? "{...any} " : "{any} ") : ""; - return `${indentationStr} * @param ${type}${paramName}${newLine}`; - }).join(""); + const commentOwnerInfo = getCommentOwnerInfo(tokenAtPos); + if (!commentOwnerInfo) { + return undefined; } - - interface CommentOwnerInfo { - readonly commentOwner: Node; - readonly parameters?: readonly ParameterDeclaration[]; + const { commentOwner, parameters } = commentOwnerInfo; + if (commentOwner.getStart(sourceFile) < position) { + return undefined; } - function getCommentOwnerInfo(tokenAtPos: Node): CommentOwnerInfo | undefined { - return forEachAncestor(tokenAtPos, getCommentOwnerInfoWorker); + if (!parameters || parameters.length === 0) { + // if there are no parameters, just complete to a single line JSDoc comment + const singleLineResult = "/** */"; + return { newText: singleLineResult, caretOffset: 3 }; } - function getCommentOwnerInfoWorker(commentOwner: Node): CommentOwnerInfo | undefined | "quit" { - switch (commentOwner.kind) { - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.FunctionExpression: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.Constructor: - case SyntaxKind.MethodSignature: - const { parameters } = commentOwner as FunctionDeclaration | MethodDeclaration | ConstructorDeclaration | MethodSignature; - return { commentOwner, parameters }; - - case SyntaxKind.PropertyAssignment: - return getCommentOwnerInfoWorker((commentOwner as PropertyAssignment).initializer); - - case SyntaxKind.ClassDeclaration: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.PropertySignature: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.EnumMember: - case SyntaxKind.TypeAliasDeclaration: - return { commentOwner }; - - case SyntaxKind.VariableStatement: { - const varStatement = commentOwner; - const varDeclarations = varStatement.declarationList.declarations; - const parameters = varDeclarations.length === 1 && varDeclarations[0].initializer - ? getParametersFromRightHandSideOfAssignment(varDeclarations[0].initializer) - : undefined; - return { commentOwner, parameters }; - } - - case SyntaxKind.SourceFile: + const indentationStr = getIndentationStringAtPosition(sourceFile, position); + // A doc comment consists of the following + // * The opening comment line + // * the first line (without a param) for the object's untagged info (this is also where the caret ends up) + // * the '@param'-tagged lines + // * TODO: other tags. + // * the closing comment line + // * if the caret was directly in front of the object, then we add an extra line and indentation. + const preamble = "/**" + newLine + indentationStr + " * "; + const result = preamble + newLine + + parameterDocComments(parameters, hasJSFileExtension(sourceFile.fileName), indentationStr, newLine) + + indentationStr + " */" + + (tokenStart === position ? newLine + indentationStr : ""); + return { newText: result, caretOffset: preamble.length }; +} +/* @internal */ +function getIndentationStringAtPosition(sourceFile: SourceFile, position: number): string { + const { text } = sourceFile; + const lineStart = getLineStartPositionForPosition(position, sourceFile); + let pos = lineStart; + for (; pos <= position && isWhiteSpaceSingleLine(text.charCodeAt(pos)); pos++) + ; + return text.slice(lineStart, pos); +} +/* @internal */ +function parameterDocComments(parameters: readonly ParameterDeclaration[], isJavaScriptFile: boolean, indentationStr: string, newLine: string): string { + return parameters.map(({ name, dotDotDotToken }, i) => { + const paramName = name.kind === SyntaxKind.Identifier ? name.text : "param" + i; + const type = isJavaScriptFile ? (dotDotDotToken ? "{...any} " : "{any} ") : ""; + return `${indentationStr} * @param ${type}${paramName}${newLine}`; + }).join(""); +} +/* @internal */ +interface CommentOwnerInfo { + readonly commentOwner: Node; + readonly parameters?: readonly ParameterDeclaration[]; +} +/* @internal */ +function getCommentOwnerInfo(tokenAtPos: Node): CommentOwnerInfo | undefined { + return forEachAncestor(tokenAtPos, getCommentOwnerInfoWorker); +} +/* @internal */ +function getCommentOwnerInfoWorker(commentOwner: Node): CommentOwnerInfo | undefined | "quit" { + switch (commentOwner.kind) { + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.FunctionExpression: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.Constructor: + case SyntaxKind.MethodSignature: + const { parameters } = (commentOwner as FunctionDeclaration | MethodDeclaration | ConstructorDeclaration | MethodSignature); + return { commentOwner, parameters }; + case SyntaxKind.PropertyAssignment: + return getCommentOwnerInfoWorker((commentOwner as PropertyAssignment).initializer); + case SyntaxKind.ClassDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.PropertySignature: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.EnumMember: + case SyntaxKind.TypeAliasDeclaration: + return { commentOwner }; + case SyntaxKind.VariableStatement: { + const varStatement = (commentOwner); + const varDeclarations = varStatement.declarationList.declarations; + const parameters = varDeclarations.length === 1 && varDeclarations[0].initializer + ? getParametersFromRightHandSideOfAssignment(varDeclarations[0].initializer) + : undefined; + return { commentOwner, parameters }; + } + case SyntaxKind.SourceFile: + return "quit"; + case SyntaxKind.ModuleDeclaration: + // If in walking up the tree, we hit a a nested namespace declaration, + // then we must be somewhere within a dotted namespace name; however we don't + // want to give back a JSDoc template for the 'b' or 'c' in 'namespace a.b.c { }'. + return commentOwner.parent.kind === SyntaxKind.ModuleDeclaration ? undefined : { commentOwner }; + case SyntaxKind.BinaryExpression: { + const be = (commentOwner as BinaryExpression); + if (getAssignmentDeclarationKind(be) === AssignmentDeclarationKind.None) { return "quit"; - - case SyntaxKind.ModuleDeclaration: - // If in walking up the tree, we hit a a nested namespace declaration, - // then we must be somewhere within a dotted namespace name; however we don't - // want to give back a JSDoc template for the 'b' or 'c' in 'namespace a.b.c { }'. - return commentOwner.parent.kind === SyntaxKind.ModuleDeclaration ? undefined : { commentOwner }; - - case SyntaxKind.BinaryExpression: { - const be = commentOwner as BinaryExpression; - if (getAssignmentDeclarationKind(be) === AssignmentDeclarationKind.None) { - return "quit"; - } - const parameters = isFunctionLike(be.right) ? be.right.parameters : emptyArray; - return { commentOwner, parameters }; } - case SyntaxKind.PropertyDeclaration: - const init = (commentOwner as PropertyDeclaration).initializer; - if (init && (isFunctionExpression(init) || isArrowFunction(init))) { - return { commentOwner, parameters: init.parameters }; - } + const parameters = isFunctionLike(be.right) ? be.right.parameters : emptyArray; + return { commentOwner, parameters }; } - } - - /** - * Digs into an an initializer or RHS operand of an assignment operation - * to get the parameters of an apt signature corresponding to a - * function expression or a class expression. - * - * @param rightHandSide the expression which may contain an appropriate set of parameters - * @returns the parameters of a signature found on the RHS if one exists; otherwise 'emptyArray'. - */ - function getParametersFromRightHandSideOfAssignment(rightHandSide: Expression): readonly ParameterDeclaration[] { - while (rightHandSide.kind === SyntaxKind.ParenthesizedExpression) { - rightHandSide = (rightHandSide).expression; - } - - switch (rightHandSide.kind) { - case SyntaxKind.FunctionExpression: - case SyntaxKind.ArrowFunction: - return (rightHandSide).parameters; - case SyntaxKind.ClassExpression: { - const ctr = find((rightHandSide as ClassExpression).members, isConstructorDeclaration); - return ctr ? ctr.parameters : emptyArray; + case SyntaxKind.PropertyDeclaration: + const init = (commentOwner as PropertyDeclaration).initializer; + if (init && (isFunctionExpression(init) || isArrowFunction(init))) { + return { commentOwner, parameters: init.parameters }; } + } +} +/** + * Digs into an an initializer or RHS operand of an assignment operation + * to get the parameters of an apt signature corresponding to a + * function expression or a class expression. + * + * @param rightHandSide the expression which may contain an appropriate set of parameters + * @returns the parameters of a signature found on the RHS if one exists; otherwise 'emptyArray'. + */ +/* @internal */ +function getParametersFromRightHandSideOfAssignment(rightHandSide: Expression): readonly ParameterDeclaration[] { + while (rightHandSide.kind === SyntaxKind.ParenthesizedExpression) { + rightHandSide = (rightHandSide).expression; + } + switch (rightHandSide.kind) { + case SyntaxKind.FunctionExpression: + case SyntaxKind.ArrowFunction: + return (rightHandSide).parameters; + case SyntaxKind.ClassExpression: { + const ctr = find((rightHandSide as ClassExpression).members, isConstructorDeclaration); + return ctr ? ctr.parameters : emptyArray; } - - return emptyArray; } + return emptyArray; } diff --git a/src/services/navigateTo.ts b/src/services/navigateTo.ts index bcd0fbe273682..85cbc3dfb9390 100644 --- a/src/services/navigateTo.ts +++ b/src/services/navigateTo.ts @@ -1,136 +1,126 @@ +import { PatternMatchKind, Declaration, SourceFile, TypeChecker, CancellationToken, NavigateToItem, createPatternMatcher, emptyArray, PatternMatcher, Push, SyntaxKind, ImportClause, ImportSpecifier, ImportEqualsDeclaration, getNameOfDeclaration, Expression, isPropertyAccessExpression, Node, isPropertyNameLiteral, getTextOfIdentifierOrLiteral, getContainerNode, compareValues, compareStringsCaseSensitiveUI, getNodeKind, getNodeModifiers, createTextSpanFromNode, Identifier, ScriptElementKind } from "./ts"; /* @internal */ -namespace ts.NavigateTo { - interface RawNavigateToItem { - readonly name: string; - readonly fileName: string; - readonly matchKind: PatternMatchKind; - readonly isCaseSensitive: boolean; - readonly declaration: Declaration; - } - - export function getNavigateToItems(sourceFiles: readonly SourceFile[], checker: TypeChecker, cancellationToken: CancellationToken, searchValue: string, maxResultCount: number | undefined, excludeDtsFiles: boolean): NavigateToItem[] { - const patternMatcher = createPatternMatcher(searchValue); - if (!patternMatcher) return emptyArray; - const rawItems: RawNavigateToItem[] = []; - - // Search the declarations in all files and output matched NavigateToItem into array of NavigateToItem[] - for (const sourceFile of sourceFiles) { - cancellationToken.throwIfCancellationRequested(); - - if (excludeDtsFiles && sourceFile.isDeclarationFile) { - continue; - } - - sourceFile.getNamedDeclarations().forEach((declarations, name) => { - getItemsFromNamedDeclaration(patternMatcher, name, declarations, checker, sourceFile.fileName, rawItems); - }); +interface RawNavigateToItem { + readonly name: string; + readonly fileName: string; + readonly matchKind: PatternMatchKind; + readonly isCaseSensitive: boolean; + readonly declaration: Declaration; +} +/* @internal */ +export function getNavigateToItems(sourceFiles: readonly SourceFile[], checker: TypeChecker, cancellationToken: CancellationToken, searchValue: string, maxResultCount: number | undefined, excludeDtsFiles: boolean): NavigateToItem[] { + const patternMatcher = createPatternMatcher(searchValue); + if (!patternMatcher) + return emptyArray; + const rawItems: RawNavigateToItem[] = []; + // Search the declarations in all files and output matched NavigateToItem into array of NavigateToItem[] + for (const sourceFile of sourceFiles) { + cancellationToken.throwIfCancellationRequested(); + if (excludeDtsFiles && sourceFile.isDeclarationFile) { + continue; } - - rawItems.sort(compareNavigateToItems); - return (maxResultCount === undefined ? rawItems : rawItems.slice(0, maxResultCount)).map(createNavigateToItem); + sourceFile.getNamedDeclarations().forEach((declarations, name) => { + getItemsFromNamedDeclaration(patternMatcher, name, declarations, checker, sourceFile.fileName, rawItems); + }); } - - function getItemsFromNamedDeclaration(patternMatcher: PatternMatcher, name: string, declarations: readonly Declaration[], checker: TypeChecker, fileName: string, rawItems: Push): void { - // First do a quick check to see if the name of the declaration matches the - // last portion of the (possibly) dotted name they're searching for. - const match = patternMatcher.getMatchForLastSegmentOfPattern(name); - if (!match) { - return; // continue to next named declarations - } - - for (const declaration of declarations) { - if (!shouldKeepItem(declaration, checker)) continue; - - if (patternMatcher.patternContainsDots) { - // If the pattern has dots in it, then also see if the declaration container matches as well. - const fullMatch = patternMatcher.getFullMatch(getContainers(declaration), name); - if (fullMatch) { - rawItems.push({ name, fileName, matchKind: fullMatch.kind, isCaseSensitive: fullMatch.isCaseSensitive, declaration }); - } - } - else { - rawItems.push({ name, fileName, matchKind: match.kind, isCaseSensitive: match.isCaseSensitive, declaration }); + rawItems.sort(compareNavigateToItems); + return (maxResultCount === undefined ? rawItems : rawItems.slice(0, maxResultCount)).map(createNavigateToItem); +} +/* @internal */ +function getItemsFromNamedDeclaration(patternMatcher: PatternMatcher, name: string, declarations: readonly Declaration[], checker: TypeChecker, fileName: string, rawItems: Push): void { + // First do a quick check to see if the name of the declaration matches the + // last portion of the (possibly) dotted name they're searching for. + const match = patternMatcher.getMatchForLastSegmentOfPattern(name); + if (!match) { + return; // continue to next named declarations + } + for (const declaration of declarations) { + if (!shouldKeepItem(declaration, checker)) + continue; + if (patternMatcher.patternContainsDots) { + // If the pattern has dots in it, then also see if the declaration container matches as well. + const fullMatch = patternMatcher.getFullMatch(getContainers(declaration), name); + if (fullMatch) { + rawItems.push({ name, fileName, matchKind: fullMatch.kind, isCaseSensitive: fullMatch.isCaseSensitive, declaration }); } } - } - - function shouldKeepItem(declaration: Declaration, checker: TypeChecker): boolean { - switch (declaration.kind) { - case SyntaxKind.ImportClause: - case SyntaxKind.ImportSpecifier: - case SyntaxKind.ImportEqualsDeclaration: - const importer = checker.getSymbolAtLocation((declaration as ImportClause | ImportSpecifier | ImportEqualsDeclaration).name!)!; // TODO: GH#18217 - const imported = checker.getAliasedSymbol(importer); - return importer.escapedName !== imported.escapedName; - default: - return true; + else { + rawItems.push({ name, fileName, matchKind: match.kind, isCaseSensitive: match.isCaseSensitive, declaration }); } } - - function tryAddSingleDeclarationName(declaration: Declaration, containers: Push): boolean { - const name = getNameOfDeclaration(declaration); - return !!name && (pushLiteral(name, containers) || name.kind === SyntaxKind.ComputedPropertyName && tryAddComputedPropertyName(name.expression, containers)); - } - - // Only added the names of computed properties if they're simple dotted expressions, like: - // - // [X.Y.Z]() { } - function tryAddComputedPropertyName(expression: Expression, containers: Push): boolean { - return pushLiteral(expression, containers) - || isPropertyAccessExpression(expression) && (containers.push(expression.name.text), true) && tryAddComputedPropertyName(expression.expression, containers); +} +/* @internal */ +function shouldKeepItem(declaration: Declaration, checker: TypeChecker): boolean { + switch (declaration.kind) { + case SyntaxKind.ImportClause: + case SyntaxKind.ImportSpecifier: + case SyntaxKind.ImportEqualsDeclaration: + const importer = (checker.getSymbolAtLocation(((declaration as ImportClause | ImportSpecifier | ImportEqualsDeclaration).name!))!); // TODO: GH#18217 + const imported = checker.getAliasedSymbol(importer); + return importer.escapedName !== imported.escapedName; + default: + return true; } - - function pushLiteral(node: Node, containers: Push): boolean { - return isPropertyNameLiteral(node) && (containers.push(getTextOfIdentifierOrLiteral(node)), true); +} +/* @internal */ +function tryAddSingleDeclarationName(declaration: Declaration, containers: Push): boolean { + const name = getNameOfDeclaration(declaration); + return !!name && (pushLiteral(name, containers) || name.kind === SyntaxKind.ComputedPropertyName && tryAddComputedPropertyName(name.expression, containers)); +} +// Only added the names of computed properties if they're simple dotted expressions, like: +// +// [X.Y.Z]() { } +/* @internal */ +function tryAddComputedPropertyName(expression: Expression, containers: Push): boolean { + return pushLiteral(expression, containers) + || isPropertyAccessExpression(expression) && (containers.push(expression.name.text), true) && tryAddComputedPropertyName(expression.expression, containers); +} +/* @internal */ +function pushLiteral(node: Node, containers: Push): boolean { + return isPropertyNameLiteral(node) && (containers.push(getTextOfIdentifierOrLiteral(node)), true); +} +/* @internal */ +function getContainers(declaration: Declaration): readonly string[] { + const containers: string[] = []; + // First, if we started with a computed property name, then add all but the last + // portion into the container array. + const name = getNameOfDeclaration(declaration); + if (name && name.kind === SyntaxKind.ComputedPropertyName && !tryAddComputedPropertyName(name.expression, containers)) { + return emptyArray; } - - function getContainers(declaration: Declaration): readonly string[] { - const containers: string[] = []; - - // First, if we started with a computed property name, then add all but the last - // portion into the container array. - const name = getNameOfDeclaration(declaration); - if (name && name.kind === SyntaxKind.ComputedPropertyName && !tryAddComputedPropertyName(name.expression, containers)) { + // Don't include the last portion. + containers.shift(); + // Now, walk up our containers, adding all their names to the container array. + let container = getContainerNode(declaration); + while (container) { + if (!tryAddSingleDeclarationName(container, containers)) { return emptyArray; } - // Don't include the last portion. - containers.shift(); - - // Now, walk up our containers, adding all their names to the container array. - let container = getContainerNode(declaration); - - while (container) { - if (!tryAddSingleDeclarationName(container, containers)) { - return emptyArray; - } - - container = getContainerNode(container); - } - - return containers.reverse(); - } - - function compareNavigateToItems(i1: RawNavigateToItem, i2: RawNavigateToItem) { - // TODO(cyrusn): get the gamut of comparisons that VS already uses here. - return compareValues(i1.matchKind, i2.matchKind) - || compareStringsCaseSensitiveUI(i1.name, i2.name); - } - - function createNavigateToItem(rawItem: RawNavigateToItem): NavigateToItem { - const declaration = rawItem.declaration; - const container = getContainerNode(declaration); - const containerName = container && getNameOfDeclaration(container); - return { - name: rawItem.name, - kind: getNodeKind(declaration), - kindModifiers: getNodeModifiers(declaration), - matchKind: PatternMatchKind[rawItem.matchKind] as keyof typeof PatternMatchKind, - isCaseSensitive: rawItem.isCaseSensitive, - fileName: rawItem.fileName, - textSpan: createTextSpanFromNode(declaration), - // TODO(jfreeman): What should be the containerName when the container has a computed name? - containerName: containerName ? (containerName).text : "", - containerKind: containerName ? getNodeKind(container!) : ScriptElementKind.unknown, // TODO: GH#18217 Just use `container ? ...` - }; + container = getContainerNode(container); } + return containers.reverse(); +} +/* @internal */ +function compareNavigateToItems(i1: RawNavigateToItem, i2: RawNavigateToItem) { + // TODO(cyrusn): get the gamut of comparisons that VS already uses here. + return compareValues(i1.matchKind, i2.matchKind) + || compareStringsCaseSensitiveUI(i1.name, i2.name); +} +/* @internal */ +function createNavigateToItem(rawItem: RawNavigateToItem): NavigateToItem { + const declaration = rawItem.declaration; + const container = getContainerNode(declaration); + const containerName = container && getNameOfDeclaration(container); + return { + name: rawItem.name, + kind: getNodeKind(declaration), + kindModifiers: getNodeModifiers(declaration), + matchKind: (PatternMatchKind[rawItem.matchKind] as keyof typeof PatternMatchKind), + isCaseSensitive: rawItem.isCaseSensitive, + fileName: rawItem.fileName, + textSpan: createTextSpanFromNode(declaration), + // TODO(jfreeman): What should be the containerName when the container has a computed name? + containerName: containerName ? (containerName).text : "", + containerKind: containerName ? getNodeKind((container!)) : ScriptElementKind.unknown, + }; } diff --git a/src/services/navigationBar.ts b/src/services/navigationBar.ts index 38faf25da7f99..bc438cb369edb 100644 --- a/src/services/navigationBar.ts +++ b/src/services/navigationBar.ts @@ -1,945 +1,896 @@ +import { CancellationToken, SourceFile, NavigationBarItem, Node, DeclarationName, map, NavigationTree, SyntaxKind, Debug, isDeclaration, isExpression, getNameOfDeclaration, createMap, BindableStaticNameExpression, PropertyNameLiteral, WellKnownSymbolExpression, isPropertyNameLiteral, getNameOrArgument, getElementOrPropertyAccessName, isPrivateIdentifier, isToken, ConstructorDeclaration, isParameterPropertyDeclaration, hasDynamicName, ClassElement, TypeElement, FunctionLikeDeclaration, ImportClause, ShorthandPropertyAssignment, SpreadAssignment, isIdentifier, VariableDeclaration, PropertyAssignment, BindingElement, isBindingPattern, forEachChild, EnumDeclaration, InterfaceDeclaration, ModuleDeclaration, getAssignmentDeclarationKind, BinaryExpression, AssignmentDeclarationKind, PropertyAccessExpression, EntityNameExpression, isObjectLiteralExpression, isFunctionExpression, isArrowFunction, BindableObjectDefinePropertyCall, setTextRange, createIdentifier, CallExpression, BindableElementAccessExpression, isBindableStaticAccessExpression, hasJSDocNodes, forEach, isJSDocTypeAlias, filterMutate, Declaration, isFunctionDeclaration, isVariableDeclaration, isBinaryExpression, isCallExpression, isClassDeclaration, lastOrUndefined, createConstructor, concatenate, createClassDeclaration, Identifier, hasModifier, ModifierFlags, isModuleBlock, contains, compareStringsCaseSensitiveUI, compareValues, isPropertyName, getPropertyNameForPropertyNameNode, unescapeLeadingUnderscores, FunctionExpression, ArrowFunction, ClassExpression, isElementAccessExpression, isExternalModule, escapeString, getBaseFileName, removeFileExtension, normalizePath, isExportAssignment, InternalSymbolName, getModifierFlags, FunctionDeclaration, getNodeKind, getNodeModifiers, TextSpan, isAmbientModule, getTextOfNode, getTextOfIdentifierOrLiteral, isModuleDeclaration, EnumMember, createTextSpanFromRange, createTextSpanFromNode, ClassLikeDeclaration, getFullWidth, declarationNameToString, isPropertyAssignment, isClassLike, mapDefined, isStringLiteralLike, Expression, isPropertyAccessExpression } from "./ts"; +import * as ts from "./ts"; /* @internal */ -namespace ts.NavigationBar { - /** - * Matches all whitespace characters in a string. Eg: - * - * "app. - * - * onactivated" - * - * matches because of the newline, whereas - * - * "app.onactivated" - * - * does not match. - */ - const whiteSpaceRegex = /\s+/g; - - /** - * Maximum amount of characters to return - * The amount was chosen arbitrarily. - */ - const maxLength = 150; - - // Keep sourceFile handy so we don't have to search for it every time we need to call `getText`. - let curCancellationToken: CancellationToken; - let curSourceFile: SourceFile; - - /** - * For performance, we keep navigation bar parents on a stack rather than passing them through each recursion. - * `parent` is the current parent and is *not* stored in parentsStack. - * `startNode` sets a new parent and `endNode` returns to the previous parent. - */ - let parentsStack: NavigationBarNode[] = []; - let parent: NavigationBarNode; - - const trackedEs5ClassesStack: (Map | undefined)[] = []; - let trackedEs5Classes: Map | undefined; - - // NavigationBarItem requires an array, but will not mutate it, so just give it this for performance. - let emptyChildItemArray: NavigationBarItem[] = []; - - /** - * Represents a navigation bar item and its children. - * The returned NavigationBarItem is more complicated and doesn't include 'parent', so we use these to do work before converting. - */ - interface NavigationBarNode { - node: Node; - name: DeclarationName | undefined; - additionalNodes: Node[] | undefined; - parent: NavigationBarNode | undefined; // Present for all but root node - children: NavigationBarNode[] | undefined; - indent: number; // # of parents - } - - export function getNavigationBarItems(sourceFile: SourceFile, cancellationToken: CancellationToken): NavigationBarItem[] { - curCancellationToken = cancellationToken; - curSourceFile = sourceFile; - try { - return map(primaryNavBarMenuItems(rootNavigationBarNode(sourceFile)), convertToPrimaryNavBarMenuItem); - } - finally { - reset(); - } +/** + * Matches all whitespace characters in a string. Eg: + * + * "app. + * + * onactivated" + * + * matches because of the newline, whereas + * + * "app.onactivated" + * + * does not match. + */ +const whiteSpaceRegex = /\s+/g; +/** + * Maximum amount of characters to return + * The amount was chosen arbitrarily. + */ +/* @internal */ +const maxLength = 150; +// Keep sourceFile handy so we don't have to search for it every time we need to call `getText`. +/* @internal */ +let curCancellationToken: CancellationToken; +/* @internal */ +let curSourceFile: SourceFile; +/** + * For performance, we keep navigation bar parents on a stack rather than passing them through each recursion. + * `parent` is the current parent and is *not* stored in parentsStack. + * `startNode` sets a new parent and `endNode` returns to the previous parent. + */ +/* @internal */ +let parentsStack: NavigationBarNode[] = []; +/* @internal */ +let parent: NavigationBarNode; +/* @internal */ +const trackedEs5ClassesStack: (ts.Map | undefined)[] = []; +/* @internal */ +let trackedEs5Classes: ts.Map | undefined; +// NavigationBarItem requires an array, but will not mutate it, so just give it this for performance. +/* @internal */ +let emptyChildItemArray: NavigationBarItem[] = []; +/** + * Represents a navigation bar item and its children. + * The returned NavigationBarItem is more complicated and doesn't include 'parent', so we use these to do work before converting. + */ +/* @internal */ +interface NavigationBarNode { + node: Node; + name: DeclarationName | undefined; + additionalNodes: Node[] | undefined; + parent: NavigationBarNode | undefined; // Present for all but root node + children: NavigationBarNode[] | undefined; + indent: number; // # of parents +} +/* @internal */ +export function getNavigationBarItems(sourceFile: SourceFile, cancellationToken: CancellationToken): NavigationBarItem[] { + curCancellationToken = cancellationToken; + curSourceFile = sourceFile; + try { + return map(primaryNavBarMenuItems(rootNavigationBarNode(sourceFile)), convertToPrimaryNavBarMenuItem); } - - export function getNavigationTree(sourceFile: SourceFile, cancellationToken: CancellationToken): NavigationTree { - curCancellationToken = cancellationToken; - curSourceFile = sourceFile; - try { - return convertToTree(rootNavigationBarNode(sourceFile)); - } - finally { - reset(); - } + finally { + reset(); } - - function reset() { - curSourceFile = undefined!; - curCancellationToken = undefined!; - parentsStack = []; - parent = undefined!; - emptyChildItemArray = []; - } - - function nodeText(node: Node): string { - return cleanText(node.getText(curSourceFile)); - } - - function navigationBarNodeKind(n: NavigationBarNode): SyntaxKind { - return n.node.kind; - } - - function pushChild(parent: NavigationBarNode, child: NavigationBarNode): void { - if (parent.children) { - parent.children.push(child); - } - else { - parent.children = [child]; - } +} +/* @internal */ +export function getNavigationTree(sourceFile: SourceFile, cancellationToken: CancellationToken): NavigationTree { + curCancellationToken = cancellationToken; + curSourceFile = sourceFile; + try { + return convertToTree(rootNavigationBarNode(sourceFile)); } - - function rootNavigationBarNode(sourceFile: SourceFile): NavigationBarNode { - Debug.assert(!parentsStack.length); - const root: NavigationBarNode = { node: sourceFile, name: undefined, additionalNodes: undefined, parent: undefined, children: undefined, indent: 0 }; - parent = root; - for (const statement of sourceFile.statements) { - addChildrenRecursively(statement); - } - endNode(); - Debug.assert(!parent && !parentsStack.length); - return root; + finally { + reset(); } - - function addLeafNode(node: Node, name?: DeclarationName): void { - pushChild(parent, emptyNavigationBarNode(node, name)); +} +/* @internal */ +function reset() { + curSourceFile = undefined!; + curCancellationToken = undefined!; + parentsStack = []; + parent = undefined!; + emptyChildItemArray = []; +} +/* @internal */ +function nodeText(node: Node): string { + return cleanText(node.getText(curSourceFile)); +} +/* @internal */ +function navigationBarNodeKind(n: NavigationBarNode): SyntaxKind { + return n.node.kind; +} +/* @internal */ +function pushChild(parent: NavigationBarNode, child: NavigationBarNode): void { + if (parent.children) { + parent.children.push(child); } - - function emptyNavigationBarNode(node: Node, name?: DeclarationName): NavigationBarNode { - return { - node, - name: name || (isDeclaration(node) || isExpression(node) ? getNameOfDeclaration(node) : undefined), - additionalNodes: undefined, - parent, - children: undefined, - indent: parent.indent + 1 - }; + else { + parent.children = [child]; } - - function addTrackedEs5Class(name: string) { - if (!trackedEs5Classes) { - trackedEs5Classes = createMap(); - } - trackedEs5Classes.set(name, true); - } - function endNestedNodes(depth: number): void { - for (let i = 0; i < depth; i++) endNode(); - } - function startNestedNodes(targetNode: Node, entityName: BindableStaticNameExpression) { - const names: (PropertyNameLiteral | WellKnownSymbolExpression)[] = []; - while (!isPropertyNameLiteral(entityName)) { - const name = getNameOrArgument(entityName); - const nameText = getElementOrPropertyAccessName(entityName); - entityName = entityName.expression; - if (nameText === "prototype" || isPrivateIdentifier(name)) continue; - names.push(name); - } - names.push(entityName); - for (let i = names.length - 1; i > 0; i--) { - const name = names[i]; - startNode(targetNode, name); - } - return [names.length - 1, names[0]] as const; - } - - /** - * Add a new level of NavigationBarNodes. - * This pushes to the stack, so you must call `endNode` when you are done adding to this node. - */ - function startNode(node: Node, name?: DeclarationName): void { - const navNode: NavigationBarNode = emptyNavigationBarNode(node, name); - pushChild(parent, navNode); - - // Save the old parent - parentsStack.push(parent); - trackedEs5ClassesStack.push(trackedEs5Classes); - parent = navNode; - } - - /** Call after calling `startNode` and adding children to it. */ - function endNode(): void { - if (parent.children) { - mergeChildren(parent.children, parent); - sortChildren(parent.children); - } - parent = parentsStack.pop()!; - trackedEs5Classes = trackedEs5ClassesStack.pop(); +} +/* @internal */ +function rootNavigationBarNode(sourceFile: SourceFile): NavigationBarNode { + Debug.assert(!parentsStack.length); + const root: NavigationBarNode = { node: sourceFile, name: undefined, additionalNodes: undefined, parent: undefined, children: undefined, indent: 0 }; + parent = root; + for (const statement of sourceFile.statements) { + addChildrenRecursively(statement); + } + endNode(); + Debug.assert(!parent && !parentsStack.length); + return root; +} +/* @internal */ +function addLeafNode(node: Node, name?: DeclarationName): void { + pushChild(parent, emptyNavigationBarNode(node, name)); +} +/* @internal */ +function emptyNavigationBarNode(node: Node, name?: DeclarationName): NavigationBarNode { + return { + node, + name: name || (isDeclaration(node) || isExpression(node) ? getNameOfDeclaration(node) : undefined), + additionalNodes: undefined, + parent, + children: undefined, + indent: parent.indent + 1 + }; +} +/* @internal */ +function addTrackedEs5Class(name: string) { + if (!trackedEs5Classes) { + trackedEs5Classes = createMap(); } - - function addNodeWithRecursiveChild(node: Node, child: Node | undefined, name?: DeclarationName): void { - startNode(node, name); - addChildrenRecursively(child); + trackedEs5Classes.set(name, true); +} +/* @internal */ +function endNestedNodes(depth: number): void { + for (let i = 0; i < depth; i++) endNode(); +} +/* @internal */ +function startNestedNodes(targetNode: Node, entityName: BindableStaticNameExpression) { + const names: (PropertyNameLiteral | WellKnownSymbolExpression)[] = []; + while (!isPropertyNameLiteral(entityName)) { + const name = getNameOrArgument(entityName); + const nameText = getElementOrPropertyAccessName(entityName); + entityName = entityName.expression; + if (nameText === "prototype" || isPrivateIdentifier(name)) + continue; + names.push(name); + } + names.push(entityName); + for (let i = names.length - 1; i > 0; i--) { + const name = names[i]; + startNode(targetNode, name); + } + return [names.length - 1, names[0]] as const; +} +/** + * Add a new level of NavigationBarNodes. + * This pushes to the stack, so you must call `endNode` when you are done adding to this node. + */ +/* @internal */ +function startNode(node: Node, name?: DeclarationName): void { + const navNode: NavigationBarNode = emptyNavigationBarNode(node, name); + pushChild(parent, navNode); + // Save the old parent + parentsStack.push(parent); + trackedEs5ClassesStack.push(trackedEs5Classes); + parent = navNode; +} +/** Call after calling `startNode` and adding children to it. */ +/* @internal */ +function endNode(): void { + if (parent.children) { + mergeChildren(parent.children, parent); + sortChildren(parent.children); } - - /** Look for navigation bar items in node's subtree, adding them to the current `parent`. */ - function addChildrenRecursively(node: Node | undefined): void { - curCancellationToken.throwIfCancellationRequested(); - - if (!node || isToken(node)) { - return; - } - - switch (node.kind) { - case SyntaxKind.Constructor: - // Get parameter properties, and treat them as being on the *same* level as the constructor, not under it. - const ctr = node; - addNodeWithRecursiveChild(ctr, ctr.body); - - // Parameter properties are children of the class, not the constructor. - for (const param of ctr.parameters) { - if (isParameterPropertyDeclaration(param, ctr)) { - addLeafNode(param); - } + parent = parentsStack.pop()!; + trackedEs5Classes = trackedEs5ClassesStack.pop(); +} +/* @internal */ +function addNodeWithRecursiveChild(node: Node, child: Node | undefined, name?: DeclarationName): void { + startNode(node, name); + addChildrenRecursively(child); + endNode(); +} +/** Look for navigation bar items in node's subtree, adding them to the current `parent`. */ +/* @internal */ +function addChildrenRecursively(node: Node | undefined): void { + curCancellationToken.throwIfCancellationRequested(); + if (!node || isToken(node)) { + return; + } + switch (node.kind) { + case SyntaxKind.Constructor: + // Get parameter properties, and treat them as being on the *same* level as the constructor, not under it. + const ctr = (node); + addNodeWithRecursiveChild(ctr, ctr.body); + // Parameter properties are children of the class, not the constructor. + for (const param of ctr.parameters) { + if (isParameterPropertyDeclaration(param, ctr)) { + addLeafNode(param); } - break; - - case SyntaxKind.MethodDeclaration: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - case SyntaxKind.MethodSignature: - if (!hasDynamicName((node))) { - addNodeWithRecursiveChild(node, (node).body); + } + break; + case SyntaxKind.MethodDeclaration: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + case SyntaxKind.MethodSignature: + if (!hasDynamicName((node))) { + addNodeWithRecursiveChild(node, (node).body); + } + break; + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.PropertySignature: + if (!hasDynamicName((node))) { + addLeafNode(node); + } + break; + case SyntaxKind.ImportClause: + const importClause = (node); + // Handle default import case e.g.: + // import d from "mod"; + if (importClause.name) { + addLeafNode(importClause.name); + } + // Handle named bindings in imports e.g.: + // import * as NS from "mod"; + // import {a, b as B} from "mod"; + const { namedBindings } = importClause; + if (namedBindings) { + if (namedBindings.kind === SyntaxKind.NamespaceImport) { + addLeafNode(namedBindings); } - break; - - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.PropertySignature: - if (!hasDynamicName((node))) { - addLeafNode(node); + else { + for (const element of namedBindings.elements) { + addLeafNode(element); + } } - break; - - case SyntaxKind.ImportClause: - const importClause = node; - // Handle default import case e.g.: - // import d from "mod"; - if (importClause.name) { - addLeafNode(importClause.name); + } + break; + case SyntaxKind.ShorthandPropertyAssignment: + addNodeWithRecursiveChild(node, (node).name); + break; + case SyntaxKind.SpreadAssignment: + const { expression } = (node); + // Use the expression as the name of the SpreadAssignment, otherwise show as . + isIdentifier(expression) ? addLeafNode(node, expression) : addLeafNode(node); + break; + case SyntaxKind.BindingElement: + case SyntaxKind.PropertyAssignment: + case SyntaxKind.VariableDeclaration: + const { name, initializer } = (node); + if (isBindingPattern(name)) { + addChildrenRecursively(name); + } + else if (initializer && isFunctionOrClassExpression(initializer)) { + // Add a node for the VariableDeclaration, but not for the initializer. + startNode(node); + forEachChild(initializer, addChildrenRecursively); + endNode(); + } + else { + addNodeWithRecursiveChild(node, initializer); + } + break; + case SyntaxKind.FunctionDeclaration: + const nameNode = (node).name; + // If we see a function declaration track as a possible ES5 class + if (nameNode && isIdentifier(nameNode)) { + addTrackedEs5Class(nameNode.text); + } + addNodeWithRecursiveChild(node, (node).body); + break; + case SyntaxKind.ArrowFunction: + case SyntaxKind.FunctionExpression: + addNodeWithRecursiveChild(node, (node).body); + break; + case SyntaxKind.EnumDeclaration: + startNode(node); + for (const member of (node).members) { + if (!isComputedProperty(member)) { + addLeafNode(member); } - - // Handle named bindings in imports e.g.: - // import * as NS from "mod"; - // import {a, b as B} from "mod"; - const { namedBindings } = importClause; - if (namedBindings) { - if (namedBindings.kind === SyntaxKind.NamespaceImport) { - addLeafNode(namedBindings); + } + endNode(); + break; + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ClassExpression: + case SyntaxKind.InterfaceDeclaration: + startNode(node); + for (const member of (node).members) { + addChildrenRecursively(member); + } + endNode(); + break; + case SyntaxKind.ModuleDeclaration: + addNodeWithRecursiveChild(node, getInteriorModule((node)).body); + break; + case SyntaxKind.ExportAssignment: + case SyntaxKind.ExportSpecifier: + case SyntaxKind.ImportEqualsDeclaration: + case SyntaxKind.IndexSignature: + case SyntaxKind.CallSignature: + case SyntaxKind.ConstructSignature: + case SyntaxKind.TypeAliasDeclaration: + addLeafNode(node); + break; + case SyntaxKind.CallExpression: + case SyntaxKind.BinaryExpression: { + const special = getAssignmentDeclarationKind((node as BinaryExpression)); + switch (special) { + case AssignmentDeclarationKind.ExportsProperty: + case AssignmentDeclarationKind.ModuleExports: + addNodeWithRecursiveChild(node, (node as BinaryExpression).right); + return; + case AssignmentDeclarationKind.Prototype: + case AssignmentDeclarationKind.PrototypeProperty: { + const binaryExpression = (node as BinaryExpression); + const assignmentTarget = (binaryExpression.left as PropertyAccessExpression); + const prototypeAccess = special === AssignmentDeclarationKind.PrototypeProperty ? + assignmentTarget.expression as PropertyAccessExpression : + assignmentTarget; + let depth = 0; + let className: PropertyNameLiteral | WellKnownSymbolExpression; + // If we see a prototype assignment, start tracking the target as a class + // This is only done for simple classes not nested assignments. + if (isIdentifier(prototypeAccess.expression)) { + addTrackedEs5Class(prototypeAccess.expression.text); + className = prototypeAccess.expression; } else { - for (const element of namedBindings.elements) { - addLeafNode(element); + [depth, className] = startNestedNodes(binaryExpression, (prototypeAccess.expression as EntityNameExpression)); + } + if (special === AssignmentDeclarationKind.Prototype) { + if (isObjectLiteralExpression(binaryExpression.right)) { + if (binaryExpression.right.properties.length > 0) { + startNode(binaryExpression, className); + forEachChild(binaryExpression.right, addChildrenRecursively); + endNode(); + } } } - } - break; - - case SyntaxKind.ShorthandPropertyAssignment: - addNodeWithRecursiveChild(node, (node).name); - break; - case SyntaxKind.SpreadAssignment: - const { expression } = node; - // Use the expression as the name of the SpreadAssignment, otherwise show as . - isIdentifier(expression) ? addLeafNode(node, expression) : addLeafNode(node); - break; - case SyntaxKind.BindingElement: - case SyntaxKind.PropertyAssignment: - case SyntaxKind.VariableDeclaration: - const { name, initializer } = node; - if (isBindingPattern(name)) { - addChildrenRecursively(name); - } - else if (initializer && isFunctionOrClassExpression(initializer)) { - // Add a node for the VariableDeclaration, but not for the initializer. - startNode(node); - forEachChild(initializer, addChildrenRecursively); - endNode(); - } - else { - addNodeWithRecursiveChild(node, initializer); - } - break; - - case SyntaxKind.FunctionDeclaration: - const nameNode = (node).name; - // If we see a function declaration track as a possible ES5 class - if (nameNode && isIdentifier(nameNode)) { - addTrackedEs5Class(nameNode.text); - } - addNodeWithRecursiveChild(node, (node).body); - break; - case SyntaxKind.ArrowFunction: - case SyntaxKind.FunctionExpression: - addNodeWithRecursiveChild(node, (node).body); - break; - - case SyntaxKind.EnumDeclaration: - startNode(node); - for (const member of (node).members) { - if (!isComputedProperty(member)) { - addLeafNode(member); + else if (isFunctionExpression(binaryExpression.right) || isArrowFunction(binaryExpression.right)) { + addNodeWithRecursiveChild(node, binaryExpression.right, className); + } + else { + startNode(binaryExpression, className); + addNodeWithRecursiveChild(node, binaryExpression.right, assignmentTarget.name); + endNode(); } + endNestedNodes(depth); + return; } - endNode(); - break; - - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ClassExpression: - case SyntaxKind.InterfaceDeclaration: - startNode(node); - for (const member of (node).members) { - addChildrenRecursively(member); + case AssignmentDeclarationKind.ObjectDefinePropertyValue: + case AssignmentDeclarationKind.ObjectDefinePrototypeProperty: { + const defineCall = (node as BindableObjectDefinePropertyCall); + const className = special === AssignmentDeclarationKind.ObjectDefinePropertyValue ? + defineCall.arguments[0] : + (defineCall.arguments[0] as PropertyAccessExpression).expression as EntityNameExpression; + const memberName = defineCall.arguments[1]; + const [depth, classNameIdentifier] = startNestedNodes(node, className); + startNode(node, classNameIdentifier); + startNode(node, setTextRange(createIdentifier(memberName.text), memberName)); + addChildrenRecursively((node as CallExpression).arguments[2]); + endNode(); + endNode(); + endNestedNodes(depth); + return; } - endNode(); - break; - - case SyntaxKind.ModuleDeclaration: - addNodeWithRecursiveChild(node, getInteriorModule(node).body); - break; - - case SyntaxKind.ExportAssignment: - case SyntaxKind.ExportSpecifier: - case SyntaxKind.ImportEqualsDeclaration: - case SyntaxKind.IndexSignature: - case SyntaxKind.CallSignature: - case SyntaxKind.ConstructSignature: - case SyntaxKind.TypeAliasDeclaration: - addLeafNode(node); - break; - - case SyntaxKind.CallExpression: - case SyntaxKind.BinaryExpression: { - const special = getAssignmentDeclarationKind(node as BinaryExpression); - switch (special) { - case AssignmentDeclarationKind.ExportsProperty: - case AssignmentDeclarationKind.ModuleExports: - addNodeWithRecursiveChild(node, (node as BinaryExpression).right); - return; - case AssignmentDeclarationKind.Prototype: - case AssignmentDeclarationKind.PrototypeProperty: { - const binaryExpression = (node as BinaryExpression); - const assignmentTarget = binaryExpression.left as PropertyAccessExpression; - - const prototypeAccess = special === AssignmentDeclarationKind.PrototypeProperty ? - assignmentTarget.expression as PropertyAccessExpression : - assignmentTarget; - - let depth = 0; - let className: PropertyNameLiteral | WellKnownSymbolExpression; - // If we see a prototype assignment, start tracking the target as a class - // This is only done for simple classes not nested assignments. - if (isIdentifier(prototypeAccess.expression)) { - addTrackedEs5Class(prototypeAccess.expression.text); - className = prototypeAccess.expression; + case AssignmentDeclarationKind.Property: { + const binaryExpression = (node as BinaryExpression); + const assignmentTarget = (binaryExpression.left as PropertyAccessExpression | BindableElementAccessExpression); + const targetFunction = assignmentTarget.expression; + if (isIdentifier(targetFunction) && getElementOrPropertyAccessName(assignmentTarget) !== "prototype" && + trackedEs5Classes && trackedEs5Classes.has(targetFunction.text)) { + if (isFunctionExpression(binaryExpression.right) || isArrowFunction(binaryExpression.right)) { + addNodeWithRecursiveChild(node, binaryExpression.right, targetFunction); } - else { - [depth, className] = startNestedNodes(binaryExpression, prototypeAccess.expression as EntityNameExpression); - } - if (special === AssignmentDeclarationKind.Prototype) { - if (isObjectLiteralExpression(binaryExpression.right)) { - if (binaryExpression.right.properties.length > 0) { - startNode(binaryExpression, className); - forEachChild(binaryExpression.right, addChildrenRecursively); - endNode(); - } - } - } - else if (isFunctionExpression(binaryExpression.right) || isArrowFunction(binaryExpression.right)) { - addNodeWithRecursiveChild(node, - binaryExpression.right, - className); - } - else { - startNode(binaryExpression, className); - addNodeWithRecursiveChild(node, binaryExpression.right, assignmentTarget.name); + else if (isBindableStaticAccessExpression(assignmentTarget)) { + startNode(binaryExpression, targetFunction); + addNodeWithRecursiveChild(binaryExpression.left, binaryExpression.right, getNameOrArgument(assignmentTarget)); endNode(); } - endNestedNodes(depth); - return; - } - case AssignmentDeclarationKind.ObjectDefinePropertyValue: - case AssignmentDeclarationKind.ObjectDefinePrototypeProperty: { - const defineCall = node as BindableObjectDefinePropertyCall; - const className = special === AssignmentDeclarationKind.ObjectDefinePropertyValue ? - defineCall.arguments[0] : - (defineCall.arguments[0] as PropertyAccessExpression).expression as EntityNameExpression; - - const memberName = defineCall.arguments[1]; - const [depth, classNameIdentifier] = startNestedNodes(node, className); - startNode(node, classNameIdentifier); - startNode(node, setTextRange(createIdentifier(memberName.text), memberName)); - addChildrenRecursively((node as CallExpression).arguments[2]); - endNode(); - endNode(); - endNestedNodes(depth); return; } - case AssignmentDeclarationKind.Property: { - const binaryExpression = (node as BinaryExpression); - const assignmentTarget = binaryExpression.left as PropertyAccessExpression | BindableElementAccessExpression; - const targetFunction = assignmentTarget.expression; - if (isIdentifier(targetFunction) && getElementOrPropertyAccessName(assignmentTarget) !== "prototype" && - trackedEs5Classes && trackedEs5Classes.has(targetFunction.text)) { - if (isFunctionExpression(binaryExpression.right) || isArrowFunction(binaryExpression.right)) { - addNodeWithRecursiveChild(node, binaryExpression.right, targetFunction); - } - else if (isBindableStaticAccessExpression(assignmentTarget)) { - startNode(binaryExpression, targetFunction); - addNodeWithRecursiveChild(binaryExpression.left, binaryExpression.right, getNameOrArgument(assignmentTarget)); - endNode(); - } - return; - } - break; - } - case AssignmentDeclarationKind.ThisProperty: - case AssignmentDeclarationKind.None: - case AssignmentDeclarationKind.ObjectDefinePropertyExports: - break; - default: - Debug.assertNever(special); + break; } + case AssignmentDeclarationKind.ThisProperty: + case AssignmentDeclarationKind.None: + case AssignmentDeclarationKind.ObjectDefinePropertyExports: + break; + default: + Debug.assertNever(special); } - // falls through - - default: - if (hasJSDocNodes(node)) { - forEach(node.jsDoc, jsDoc => { - forEach(jsDoc.tags, tag => { - if (isJSDocTypeAlias(tag)) { - addLeafNode(tag); - } - }); - }); - } - - forEachChild(node, addChildrenRecursively); } - } - - /** Merge declarations of the same kind. */ - function mergeChildren(children: NavigationBarNode[], node: NavigationBarNode): void { - const nameToItems = createMap(); - filterMutate(children, (child, index) => { - const declName = child.name || getNameOfDeclaration(child.node); - const name = declName && nodeText(declName); - if (!name) { - // Anonymous items are never merged. - return true; - } - - const itemsWithSameName = nameToItems.get(name); - if (!itemsWithSameName) { - nameToItems.set(name, child); - return true; - } - - if (itemsWithSameName instanceof Array) { - for (const itemWithSameName of itemsWithSameName) { - if (tryMerge(itemWithSameName, child, index, node)) { - return false; - } - } - itemsWithSameName.push(child); - return true; + // falls through + default: + if (hasJSDocNodes(node)) { + forEach(node.jsDoc, jsDoc => { + forEach(jsDoc.tags, tag => { + if (isJSDocTypeAlias(tag)) { + addLeafNode(tag); + } + }); + }); } - else { - const itemWithSameName = itemsWithSameName; + forEachChild(node, addChildrenRecursively); + } +} +/** Merge declarations of the same kind. */ +/* @internal */ +function mergeChildren(children: NavigationBarNode[], node: NavigationBarNode): void { + const nameToItems = createMap(); + filterMutate(children, (child, index) => { + const declName = child.name || getNameOfDeclaration((child.node)); + const name = declName && nodeText(declName); + if (!name) { + // Anonymous items are never merged. + return true; + } + const itemsWithSameName = nameToItems.get(name); + if (!itemsWithSameName) { + nameToItems.set(name, child); + return true; + } + if (itemsWithSameName instanceof Array) { + for (const itemWithSameName of itemsWithSameName) { if (tryMerge(itemWithSameName, child, index, node)) { return false; } - nameToItems.set(name, [itemWithSameName, child]); - return true; } - }); - } - const isEs5ClassMember: Record = { - [AssignmentDeclarationKind.Property]: true, - [AssignmentDeclarationKind.PrototypeProperty]: true, - [AssignmentDeclarationKind.ObjectDefinePropertyValue]: true, - [AssignmentDeclarationKind.ObjectDefinePrototypeProperty]: true, - [AssignmentDeclarationKind.None]: false, - [AssignmentDeclarationKind.ExportsProperty]: false, - [AssignmentDeclarationKind.ModuleExports]: false, - [AssignmentDeclarationKind.ObjectDefinePropertyExports]: false, - [AssignmentDeclarationKind.Prototype]: true, - [AssignmentDeclarationKind.ThisProperty]: false, - }; - function tryMergeEs5Class(a: NavigationBarNode, b: NavigationBarNode, bIndex: number, parent: NavigationBarNode): boolean | undefined { - - function isPossibleConstructor(node: Node) { - return isFunctionExpression(node) || isFunctionDeclaration(node) || isVariableDeclaration(node); + itemsWithSameName.push(child); + return true; + } + else { + const itemWithSameName = itemsWithSameName; + if (tryMerge(itemWithSameName, child, index, node)) { + return false; + } + nameToItems.set(name, [itemWithSameName, child]); + return true; } - const bAssignmentDeclarationKind = isBinaryExpression(b.node) || isCallExpression(b.node) ? - getAssignmentDeclarationKind(b.node) : - AssignmentDeclarationKind.None; - - const aAssignmentDeclarationKind = isBinaryExpression(a.node) || isCallExpression(a.node) ? - getAssignmentDeclarationKind(a.node) : - AssignmentDeclarationKind.None; - - // We treat this as an es5 class and merge the nodes in in one of several cases - if ((isEs5ClassMember[bAssignmentDeclarationKind] && isEs5ClassMember[aAssignmentDeclarationKind]) // merge two class elements - || (isPossibleConstructor(a.node) && isEs5ClassMember[bAssignmentDeclarationKind]) // ctor function & member - || (isPossibleConstructor(b.node) && isEs5ClassMember[aAssignmentDeclarationKind]) // member & ctor function - || (isClassDeclaration(a.node) && isEs5ClassMember[bAssignmentDeclarationKind]) // class (generated) & member - || (isClassDeclaration(b.node) && isEs5ClassMember[aAssignmentDeclarationKind]) // member & class (generated) - || (isClassDeclaration(a.node) && isPossibleConstructor(b.node)) // class (generated) & ctor - || (isClassDeclaration(b.node) && isPossibleConstructor(a.node)) // ctor & class (generated) - ) { - - let lastANode = a.additionalNodes && lastOrUndefined(a.additionalNodes) || a.node; - - if ((!isClassDeclaration(a.node) && !isClassDeclaration(b.node)) // If neither outline node is a class - || isPossibleConstructor(a.node) || isPossibleConstructor(b.node) // If either function is a constructor function - ) { - const ctorFunction = isPossibleConstructor(a.node) ? a.node : - isPossibleConstructor(b.node) ? b.node : + }); +} +/* @internal */ +const isEs5ClassMember: Record = { + [AssignmentDeclarationKind.Property]: true, + [AssignmentDeclarationKind.PrototypeProperty]: true, + [AssignmentDeclarationKind.ObjectDefinePropertyValue]: true, + [AssignmentDeclarationKind.ObjectDefinePrototypeProperty]: true, + [AssignmentDeclarationKind.None]: false, + [AssignmentDeclarationKind.ExportsProperty]: false, + [AssignmentDeclarationKind.ModuleExports]: false, + [AssignmentDeclarationKind.ObjectDefinePropertyExports]: false, + [AssignmentDeclarationKind.Prototype]: true, + [AssignmentDeclarationKind.ThisProperty]: false, +}; +/* @internal */ +function tryMergeEs5Class(a: NavigationBarNode, b: NavigationBarNode, bIndex: number, parent: NavigationBarNode): boolean | undefined { + function isPossibleConstructor(node: Node) { + return isFunctionExpression(node) || isFunctionDeclaration(node) || isVariableDeclaration(node); + } + const bAssignmentDeclarationKind = isBinaryExpression(b.node) || isCallExpression(b.node) ? + getAssignmentDeclarationKind(b.node) : + AssignmentDeclarationKind.None; + const aAssignmentDeclarationKind = isBinaryExpression(a.node) || isCallExpression(a.node) ? + getAssignmentDeclarationKind(a.node) : + AssignmentDeclarationKind.None; + // We treat this as an es5 class and merge the nodes in in one of several cases + if ((isEs5ClassMember[bAssignmentDeclarationKind] && isEs5ClassMember[aAssignmentDeclarationKind]) // merge two class elements + || (isPossibleConstructor(a.node) && isEs5ClassMember[bAssignmentDeclarationKind]) // ctor function & member + || (isPossibleConstructor(b.node) && isEs5ClassMember[aAssignmentDeclarationKind]) // member & ctor function + || (isClassDeclaration(a.node) && isEs5ClassMember[bAssignmentDeclarationKind]) // class (generated) & member + || (isClassDeclaration(b.node) && isEs5ClassMember[aAssignmentDeclarationKind]) // member & class (generated) + || (isClassDeclaration(a.node) && isPossibleConstructor(b.node)) // class (generated) & ctor + || (isClassDeclaration(b.node) && isPossibleConstructor(a.node)) // ctor & class (generated) + ) { + let lastANode = a.additionalNodes && lastOrUndefined(a.additionalNodes) || a.node; + if ((!isClassDeclaration(a.node) && !isClassDeclaration(b.node)) // If neither outline node is a class + || isPossibleConstructor(a.node) || isPossibleConstructor(b.node) // If either function is a constructor function + ) { + const ctorFunction = isPossibleConstructor(a.node) ? a.node : + isPossibleConstructor(b.node) ? b.node : undefined; - - if (ctorFunction !== undefined) { - const ctorNode = setTextRange( - createConstructor(/* decorators */ undefined, /* modifiers */ undefined, [], /* body */ undefined), - ctorFunction); - const ctor = emptyNavigationBarNode(ctorNode); - ctor.indent = a.indent + 1; - ctor.children = a.node === ctorFunction ? a.children : b.children; - a.children = a.node === ctorFunction ? concatenate([ctor], b.children || [b]) : concatenate(a.children || [a], [ctor]); - } - else { - if (a.children || b.children) { - a.children = concatenate(a.children || [a], b.children || [b]); - if (a.children) { - mergeChildren(a.children, a); - sortChildren(a.children); - } - } - } - - lastANode = a.node = setTextRange(createClassDeclaration( - /* decorators */ undefined, - /* modifiers */ undefined, - a.name as Identifier || createIdentifier("__class__"), - /* typeParameters */ undefined, - /* heritageClauses */ undefined, - [] - ), a.node); + if (ctorFunction !== undefined) { + const ctorNode = setTextRange(createConstructor(/* decorators */ undefined, /* modifiers */ undefined, [], /* body */ undefined), ctorFunction); + const ctor = emptyNavigationBarNode(ctorNode); + ctor.indent = a.indent + 1; + ctor.children = a.node === ctorFunction ? a.children : b.children; + a.children = a.node === ctorFunction ? concatenate([ctor], b.children || [b]) : concatenate(a.children || [a], [ctor]); } else { - a.children = concatenate(a.children, b.children); - if (a.children) { - mergeChildren(a.children, a); + if (a.children || b.children) { + a.children = concatenate(a.children || [a], b.children || [b]); + if (a.children) { + mergeChildren(a.children, a); + sortChildren(a.children); + } } } - - const bNode = b.node; - // We merge if the outline node previous to b (bIndex - 1) is already part of the current class - // We do this so that statements between class members that do not generate outline nodes do not split up the class outline: - // Ex This should produce one outline node C: - // function C() {}; a = 1; C.prototype.m = function () {} - // Ex This will produce 3 outline nodes: C, a, C - // function C() {}; let a = 1; C.prototype.m = function () {} - if (parent.children![bIndex - 1].node.end === lastANode.end) { - setTextRange(lastANode, { pos: lastANode.pos, end: bNode.end }); - } - else { - if (!a.additionalNodes) a.additionalNodes = []; - a.additionalNodes.push(setTextRange(createClassDeclaration( - /* decorators */ undefined, - /* modifiers */ undefined, - a.name as Identifier || createIdentifier("__class__"), - /* typeParameters */ undefined, - /* heritageClauses */ undefined, - [] - ), b.node)); + lastANode = a.node = setTextRange(createClassDeclaration( + /* decorators */ undefined, + /* modifiers */ undefined, (a.name as Identifier) || createIdentifier("__class__"), + /* typeParameters */ undefined, + /* heritageClauses */ undefined, []), a.node); + } + else { + a.children = concatenate(a.children, b.children); + if (a.children) { + mergeChildren(a.children, a); } - return true; } - return bAssignmentDeclarationKind === AssignmentDeclarationKind.None ? false : true; - } - - function tryMerge(a: NavigationBarNode, b: NavigationBarNode, bIndex: number, parent: NavigationBarNode): boolean { - // const v = false as boolean; - if (tryMergeEs5Class(a, b, bIndex, parent)) { - return true; + const bNode = b.node; + // We merge if the outline node previous to b (bIndex - 1) is already part of the current class + // We do this so that statements between class members that do not generate outline nodes do not split up the class outline: + // Ex This should produce one outline node C: + // function C() {}; a = 1; C.prototype.m = function () {} + // Ex This will produce 3 outline nodes: C, a, C + // function C() {}; let a = 1; C.prototype.m = function () {} + if (parent.children![bIndex - 1].node.end === lastANode.end) { + setTextRange(lastANode, { pos: lastANode.pos, end: bNode.end }); } - if (shouldReallyMerge(a.node, b.node, parent)) { - merge(a, b); - return true; + else { + if (!a.additionalNodes) + a.additionalNodes = []; + a.additionalNodes.push(setTextRange(createClassDeclaration( + /* decorators */ undefined, + /* modifiers */ undefined, (a.name as Identifier) || createIdentifier("__class__"), + /* typeParameters */ undefined, + /* heritageClauses */ undefined, []), b.node)); } + return true; + } + return bAssignmentDeclarationKind === AssignmentDeclarationKind.None ? false : true; +} +/* @internal */ +function tryMerge(a: NavigationBarNode, b: NavigationBarNode, bIndex: number, parent: NavigationBarNode): boolean { + // const v = false as boolean; + if (tryMergeEs5Class(a, b, bIndex, parent)) { + return true; + } + if (shouldReallyMerge(a.node, b.node, parent)) { + merge(a, b); + return true; + } + return false; +} +/** a and b have the same name, but they may not be mergeable. */ +/* @internal */ +function shouldReallyMerge(a: Node, b: Node, parent: NavigationBarNode): boolean { + if (a.kind !== b.kind || a.parent !== b.parent && !(isOwnChild(a, parent) && isOwnChild(b, parent))) { return false; } - - /** a and b have the same name, but they may not be mergeable. */ - function shouldReallyMerge(a: Node, b: Node, parent: NavigationBarNode): boolean { - if (a.kind !== b.kind || a.parent !== b.parent && !(isOwnChild(a, parent) && isOwnChild(b, parent))) { - return false; - } - switch (a.kind) { - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - return hasModifier(a, ModifierFlags.Static) === hasModifier(b, ModifierFlags.Static); - case SyntaxKind.ModuleDeclaration: - return areSameModule(a, b); - default: - return true; - } + switch (a.kind) { + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + return hasModifier(a, ModifierFlags.Static) === hasModifier(b, ModifierFlags.Static); + case SyntaxKind.ModuleDeclaration: + return areSameModule((a), (b)); + default: + return true; } - - // We want to merge own children like `I` in in `module A { interface I {} } module A { interface I {} }` - // We don't want to merge unrelated children like `m` in `const o = { a: { m() {} }, b: { m() {} } };` - function isOwnChild(n: Node, parent: NavigationBarNode): boolean { - const par = isModuleBlock(n.parent) ? n.parent.parent : n.parent; - return par === parent.node || contains(parent.additionalNodes, par); - } - - // We use 1 NavNode to represent 'A.B.C', but there are multiple source nodes. - // Only merge module nodes that have the same chain. Don't merge 'A.B.C' with 'A'! - function areSameModule(a: ModuleDeclaration, b: ModuleDeclaration): boolean { - // TODO: GH#18217 - return a.body!.kind === b.body!.kind && (a.body!.kind !== SyntaxKind.ModuleDeclaration || areSameModule(a.body, b.body)); - } - - /** Merge source into target. Source should be thrown away after this is called. */ - function merge(target: NavigationBarNode, source: NavigationBarNode): void { - target.additionalNodes = target.additionalNodes || []; - target.additionalNodes.push(source.node); - if (source.additionalNodes) { - target.additionalNodes.push(...source.additionalNodes); - } - - target.children = concatenate(target.children, source.children); - if (target.children) { - mergeChildren(target.children, target); - sortChildren(target.children); - } +} +// We want to merge own children like `I` in in `module A { interface I {} } module A { interface I {} }` +// We don't want to merge unrelated children like `m` in `const o = { a: { m() {} }, b: { m() {} } };` +/* @internal */ +function isOwnChild(n: Node, parent: NavigationBarNode): boolean { + const par = isModuleBlock(n.parent) ? n.parent.parent : n.parent; + return par === parent.node || contains(parent.additionalNodes, par); +} +// We use 1 NavNode to represent 'A.B.C', but there are multiple source nodes. +// Only merge module nodes that have the same chain. Don't merge 'A.B.C' with 'A'! +/* @internal */ +function areSameModule(a: ModuleDeclaration, b: ModuleDeclaration): boolean { + // TODO: GH#18217 + return a.body!.kind === b.body!.kind && (a.body!.kind !== SyntaxKind.ModuleDeclaration || areSameModule((a.body), (b.body))); +} +/** Merge source into target. Source should be thrown away after this is called. */ +/* @internal */ +function merge(target: NavigationBarNode, source: NavigationBarNode): void { + target.additionalNodes = target.additionalNodes || []; + target.additionalNodes.push(source.node); + if (source.additionalNodes) { + target.additionalNodes.push(...source.additionalNodes); } - - /** Recursively ensure that each NavNode's children are in sorted order. */ - function sortChildren(children: NavigationBarNode[]): void { - children.sort(compareChildren); - } - - function compareChildren(child1: NavigationBarNode, child2: NavigationBarNode) { - return compareStringsCaseSensitiveUI(tryGetName(child1.node)!, tryGetName(child2.node)!) // TODO: GH#18217 - || compareValues(navigationBarNodeKind(child1), navigationBarNodeKind(child2)); - } - - /** - * This differs from getItemName because this is just used for sorting. - * We only sort nodes by name that have a more-or-less "direct" name, as opposed to `new()` and the like. - * So `new()` can still come before an `aardvark` method. - */ - function tryGetName(node: Node): string | undefined { - if (node.kind === SyntaxKind.ModuleDeclaration) { - return getModuleName(node); - } - - const declName = getNameOfDeclaration(node); - if (declName && isPropertyName(declName)) { - const propertyName = getPropertyNameForPropertyNameNode(declName); - return propertyName && unescapeLeadingUnderscores(propertyName); - } - switch (node.kind) { - case SyntaxKind.FunctionExpression: - case SyntaxKind.ArrowFunction: - case SyntaxKind.ClassExpression: - return getFunctionOrClassName(node); - default: - return undefined; - } + target.children = concatenate(target.children, source.children); + if (target.children) { + mergeChildren(target.children, target); + sortChildren(target.children); } - - function getItemName(node: Node, name: Node | undefined): string { - if (node.kind === SyntaxKind.ModuleDeclaration) { - return cleanText(getModuleName(node)); - } - - if (name) { - const text = isIdentifier(name) ? name.text - : isElementAccessExpression(name) ? `[${nodeText(name.argumentExpression)}]` +} +/** Recursively ensure that each NavNode's children are in sorted order. */ +/* @internal */ +function sortChildren(children: NavigationBarNode[]): void { + children.sort(compareChildren); +} +/* @internal */ +function compareChildren(child1: NavigationBarNode, child2: NavigationBarNode) { + return compareStringsCaseSensitiveUI((tryGetName(child1.node)!), (tryGetName(child2.node)!)) // TODO: GH#18217 + || compareValues(navigationBarNodeKind(child1), navigationBarNodeKind(child2)); +} +/** + * This differs from getItemName because this is just used for sorting. + * We only sort nodes by name that have a more-or-less "direct" name, as opposed to `new()` and the like. + * So `new()` can still come before an `aardvark` method. + */ +/* @internal */ +function tryGetName(node: Node): string | undefined { + if (node.kind === SyntaxKind.ModuleDeclaration) { + return getModuleName((node)); + } + const declName = getNameOfDeclaration((node)); + if (declName && isPropertyName(declName)) { + const propertyName = getPropertyNameForPropertyNameNode(declName); + return propertyName && unescapeLeadingUnderscores(propertyName); + } + switch (node.kind) { + case SyntaxKind.FunctionExpression: + case SyntaxKind.ArrowFunction: + case SyntaxKind.ClassExpression: + return getFunctionOrClassName((node)); + default: + return undefined; + } +} +/* @internal */ +function getItemName(node: Node, name: Node | undefined): string { + if (node.kind === SyntaxKind.ModuleDeclaration) { + return cleanText(getModuleName((node))); + } + if (name) { + const text = isIdentifier(name) ? name.text + : isElementAccessExpression(name) ? `[${nodeText(name.argumentExpression)}]` : nodeText(name); - if (text.length > 0) { - return cleanText(text); + if (text.length > 0) { + return cleanText(text); + } + } + switch (node.kind) { + case SyntaxKind.SourceFile: + const sourceFile = (node); + return isExternalModule(sourceFile) + ? `"${escapeString(getBaseFileName(removeFileExtension(normalizePath(sourceFile.fileName))))}"` + : ""; + case SyntaxKind.ExportAssignment: + return isExportAssignment(node) && node.isExportEquals ? InternalSymbolName.ExportEquals : InternalSymbolName.Default; + case SyntaxKind.ArrowFunction: + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.FunctionExpression: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ClassExpression: + if (getModifierFlags(node) & ModifierFlags.Default) { + return "default"; + } + // We may get a string with newlines or other whitespace in the case of an object dereference + // (eg: "app\n.onactivated"), so we should remove the whitespace for readabiltiy in the + // navigation bar. + return getFunctionOrClassName((node)); + case SyntaxKind.Constructor: + return "constructor"; + case SyntaxKind.ConstructSignature: + return "new()"; + case SyntaxKind.CallSignature: + return "()"; + case SyntaxKind.IndexSignature: + return "[]"; + default: + return ""; + } +} +/** Flattens the NavNode tree to a list of items to appear in the primary navbar menu. */ +/* @internal */ +function primaryNavBarMenuItems(root: NavigationBarNode): NavigationBarNode[] { + // The primary (middle) navbar menu displays the general code navigation hierarchy, similar to the navtree. + // The secondary (right) navbar menu displays the child items of whichever primary item is selected. + // Some less interesting items without their own child navigation items (e.g. a local variable declaration) only show up in the secondary menu. + const primaryNavBarMenuItems: NavigationBarNode[] = []; + function recur(item: NavigationBarNode) { + if (shouldAppearInPrimaryNavBarMenu(item)) { + primaryNavBarMenuItems.push(item); + if (item.children) { + for (const child of item.children) { + recur(child); + } } } - - switch (node.kind) { + } + recur(root); + return primaryNavBarMenuItems; + /** Determines if a node should appear in the primary navbar menu. */ + function shouldAppearInPrimaryNavBarMenu(item: NavigationBarNode): boolean { + // Items with children should always appear in the primary navbar menu. + if (item.children) { + return true; + } + // Some nodes are otherwise important enough to always include in the primary navigation menu. + switch (navigationBarNodeKind(item)) { + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ClassExpression: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.ModuleDeclaration: case SyntaxKind.SourceFile: - const sourceFile = node; - return isExternalModule(sourceFile) - ? `"${escapeString(getBaseFileName(removeFileExtension(normalizePath(sourceFile.fileName))))}"` - : ""; - case SyntaxKind.ExportAssignment: - return isExportAssignment(node) && node.isExportEquals ? InternalSymbolName.ExportEquals : InternalSymbolName.Default; - + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.JSDocTypedefTag: + case SyntaxKind.JSDocCallbackTag: + return true; case SyntaxKind.ArrowFunction: case SyntaxKind.FunctionDeclaration: case SyntaxKind.FunctionExpression: - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ClassExpression: - if (getModifierFlags(node) & ModifierFlags.Default) { - return "default"; - } - // We may get a string with newlines or other whitespace in the case of an object dereference - // (eg: "app\n.onactivated"), so we should remove the whitespace for readabiltiy in the - // navigation bar. - return getFunctionOrClassName(node); - case SyntaxKind.Constructor: - return "constructor"; - case SyntaxKind.ConstructSignature: - return "new()"; - case SyntaxKind.CallSignature: - return "()"; - case SyntaxKind.IndexSignature: - return "[]"; + return isTopLevelFunctionDeclaration(item); default: - return ""; - } - } - - /** Flattens the NavNode tree to a list of items to appear in the primary navbar menu. */ - function primaryNavBarMenuItems(root: NavigationBarNode): NavigationBarNode[] { - // The primary (middle) navbar menu displays the general code navigation hierarchy, similar to the navtree. - // The secondary (right) navbar menu displays the child items of whichever primary item is selected. - // Some less interesting items without their own child navigation items (e.g. a local variable declaration) only show up in the secondary menu. - const primaryNavBarMenuItems: NavigationBarNode[] = []; - function recur(item: NavigationBarNode) { - if (shouldAppearInPrimaryNavBarMenu(item)) { - primaryNavBarMenuItems.push(item); - if (item.children) { - for (const child of item.children) { - recur(child); - } - } - } + return false; } - recur(root); - return primaryNavBarMenuItems; - - /** Determines if a node should appear in the primary navbar menu. */ - function shouldAppearInPrimaryNavBarMenu(item: NavigationBarNode): boolean { - // Items with children should always appear in the primary navbar menu. - if (item.children) { - return true; + function isTopLevelFunctionDeclaration(item: NavigationBarNode): boolean { + if (!(item.node).body) { + return false; } - - // Some nodes are otherwise important enough to always include in the primary navigation menu. - switch (navigationBarNodeKind(item)) { - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ClassExpression: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.ModuleDeclaration: + switch (navigationBarNodeKind(item.parent!)) { + case SyntaxKind.ModuleBlock: case SyntaxKind.SourceFile: - case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.JSDocTypedefTag: - case SyntaxKind.JSDocCallbackTag: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.Constructor: return true; - - case SyntaxKind.ArrowFunction: - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.FunctionExpression: - return isTopLevelFunctionDeclaration(item); - default: return false; } - function isTopLevelFunctionDeclaration(item: NavigationBarNode): boolean { - if (!(item.node).body) { - return false; - } - - switch (navigationBarNodeKind(item.parent!)) { - case SyntaxKind.ModuleBlock: - case SyntaxKind.SourceFile: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.Constructor: - return true; - default: - return false; - } - } } } - - function convertToTree(n: NavigationBarNode): NavigationTree { - return { - text: getItemName(n.node, n.name), - kind: getNodeKind(n.node), - kindModifiers: getModifiers(n.node), - spans: getSpans(n), - nameSpan: n.name && getNodeSpan(n.name), - childItems: map(n.children, convertToTree) - }; - } - - function convertToPrimaryNavBarMenuItem(n: NavigationBarNode): NavigationBarItem { +} +/* @internal */ +function convertToTree(n: NavigationBarNode): NavigationTree { + return { + text: getItemName(n.node, n.name), + kind: getNodeKind(n.node), + kindModifiers: getModifiers(n.node), + spans: getSpans(n), + nameSpan: n.name && getNodeSpan(n.name), + childItems: map(n.children, convertToTree) + }; +} +/* @internal */ +function convertToPrimaryNavBarMenuItem(n: NavigationBarNode): NavigationBarItem { + return { + text: getItemName(n.node, n.name), + kind: getNodeKind(n.node), + kindModifiers: getModifiers(n.node), + spans: getSpans(n), + childItems: map(n.children, convertToSecondaryNavBarMenuItem) || emptyChildItemArray, + indent: n.indent, + bolded: false, + grayed: false + }; + function convertToSecondaryNavBarMenuItem(n: NavigationBarNode): NavigationBarItem { return { text: getItemName(n.node, n.name), kind: getNodeKind(n.node), - kindModifiers: getModifiers(n.node), + kindModifiers: getNodeModifiers(n.node), spans: getSpans(n), - childItems: map(n.children, convertToSecondaryNavBarMenuItem) || emptyChildItemArray, - indent: n.indent, + childItems: emptyChildItemArray, + indent: 0, bolded: false, grayed: false }; - - function convertToSecondaryNavBarMenuItem(n: NavigationBarNode): NavigationBarItem { - return { - text: getItemName(n.node, n.name), - kind: getNodeKind(n.node), - kindModifiers: getNodeModifiers(n.node), - spans: getSpans(n), - childItems: emptyChildItemArray, - indent: 0, - bolded: false, - grayed: false - }; - } } - - function getSpans(n: NavigationBarNode): TextSpan[] { - const spans = [getNodeSpan(n.node)]; - if (n.additionalNodes) { - for (const node of n.additionalNodes) { - spans.push(getNodeSpan(node)); - } +} +/* @internal */ +function getSpans(n: NavigationBarNode): TextSpan[] { + const spans = [getNodeSpan(n.node)]; + if (n.additionalNodes) { + for (const node of n.additionalNodes) { + spans.push(getNodeSpan(node)); } - return spans; } - - function getModuleName(moduleDeclaration: ModuleDeclaration): string { - // We want to maintain quotation marks. - if (isAmbientModule(moduleDeclaration)) { - return getTextOfNode(moduleDeclaration.name); - } - - // Otherwise, we need to aggregate each identifier to build up the qualified name. - const result: string[] = []; - + return spans; +} +/* @internal */ +function getModuleName(moduleDeclaration: ModuleDeclaration): string { + // We want to maintain quotation marks. + if (isAmbientModule(moduleDeclaration)) { + return getTextOfNode(moduleDeclaration.name); + } + // Otherwise, we need to aggregate each identifier to build up the qualified name. + const result: string[] = []; + result.push(getTextOfIdentifierOrLiteral(moduleDeclaration.name)); + while (moduleDeclaration.body && moduleDeclaration.body.kind === SyntaxKind.ModuleDeclaration) { + moduleDeclaration = (moduleDeclaration.body); result.push(getTextOfIdentifierOrLiteral(moduleDeclaration.name)); - - while (moduleDeclaration.body && moduleDeclaration.body.kind === SyntaxKind.ModuleDeclaration) { - moduleDeclaration = moduleDeclaration.body; - - result.push(getTextOfIdentifierOrLiteral(moduleDeclaration.name)); - } - - return result.join("."); - } - - /** - * For 'module A.B.C', we want to get the node for 'C'. - * We store 'A' as associated with a NavNode, and use getModuleName to traverse down again. - */ - function getInteriorModule(decl: ModuleDeclaration): ModuleDeclaration { - return decl.body && isModuleDeclaration(decl.body) ? getInteriorModule(decl.body) : decl; - } - - function isComputedProperty(member: EnumMember): boolean { - return !member.name || member.name.kind === SyntaxKind.ComputedPropertyName; - } - - function getNodeSpan(node: Node): TextSpan { - return node.kind === SyntaxKind.SourceFile ? createTextSpanFromRange(node) : createTextSpanFromNode(node, curSourceFile); - } - - function getModifiers(node: Node): string { - if (node.parent && node.parent.kind === SyntaxKind.VariableDeclaration) { - node = node.parent; - } - return getNodeModifiers(node); } - - function getFunctionOrClassName(node: FunctionExpression | FunctionDeclaration | ArrowFunction | ClassLikeDeclaration): string { - const { parent } = node; - if (node.name && getFullWidth(node.name) > 0) { - return cleanText(declarationNameToString(node.name)); - } - // See if it is a var initializer. If so, use the var name. - else if (isVariableDeclaration(parent)) { - return cleanText(declarationNameToString(parent.name)); - } - // See if it is of the form " = function(){...}". If so, use the text from the left-hand side. - else if (isBinaryExpression(parent) && parent.operatorToken.kind === SyntaxKind.EqualsToken) { - return nodeText(parent.left).replace(whiteSpaceRegex, ""); - } - // See if it is a property assignment, and if so use the property name - else if (isPropertyAssignment(parent)) { - return nodeText(parent.name); - } - // Default exports are named "default" - else if (getModifierFlags(node) & ModifierFlags.Default) { - return "default"; - } - else if (isClassLike(node)) { - return ""; - } - else if (isCallExpression(parent)) { - let name = getCalledExpressionName(parent.expression); - if (name !== undefined) { - name = cleanText(name); - - if (name.length > maxLength) { - return `${name} callback`; - } - - const args = cleanText(mapDefined(parent.arguments, a => isStringLiteralLike(a) ? a.getText(curSourceFile) : undefined).join(", ")); - return `${name}(${args}) callback`; + return result.join("."); +} +/** + * For 'module A.B.C', we want to get the node for 'C'. + * We store 'A' as associated with a NavNode, and use getModuleName to traverse down again. + */ +/* @internal */ +function getInteriorModule(decl: ModuleDeclaration): ModuleDeclaration { + return decl.body && isModuleDeclaration(decl.body) ? getInteriorModule(decl.body) : decl; +} +/* @internal */ +function isComputedProperty(member: EnumMember): boolean { + return !member.name || member.name.kind === SyntaxKind.ComputedPropertyName; +} +/* @internal */ +function getNodeSpan(node: Node): TextSpan { + return node.kind === SyntaxKind.SourceFile ? createTextSpanFromRange(node) : createTextSpanFromNode(node, curSourceFile); +} +/* @internal */ +function getModifiers(node: Node): string { + if (node.parent && node.parent.kind === SyntaxKind.VariableDeclaration) { + node = node.parent; + } + return getNodeModifiers(node); +} +/* @internal */ +function getFunctionOrClassName(node: FunctionExpression | FunctionDeclaration | ArrowFunction | ClassLikeDeclaration): string { + const { parent } = node; + if (node.name && getFullWidth(node.name) > 0) { + return cleanText(declarationNameToString(node.name)); + } + // See if it is a var initializer. If so, use the var name. + else if (isVariableDeclaration(parent)) { + return cleanText(declarationNameToString(parent.name)); + } + // See if it is of the form " = function(){...}". If so, use the text from the left-hand side. + else if (isBinaryExpression(parent) && parent.operatorToken.kind === SyntaxKind.EqualsToken) { + return nodeText(parent.left).replace(whiteSpaceRegex, ""); + } + // See if it is a property assignment, and if so use the property name + else if (isPropertyAssignment(parent)) { + return nodeText(parent.name); + } + // Default exports are named "default" + else if (getModifierFlags(node) & ModifierFlags.Default) { + return "default"; + } + else if (isClassLike(node)) { + return ""; + } + else if (isCallExpression(parent)) { + let name = getCalledExpressionName(parent.expression); + if (name !== undefined) { + name = cleanText(name); + if (name.length > maxLength) { + return `${name} callback`; } + const args = cleanText(mapDefined(parent.arguments, a => isStringLiteralLike(a) ? a.getText(curSourceFile) : undefined).join(", ")); + return `${name}(${args}) callback`; } - return ""; } - - // See also 'tryGetPropertyAccessOrIdentifierToString' - function getCalledExpressionName(expr: Expression): string | undefined { - if (isIdentifier(expr)) { - return expr.text; - } - else if (isPropertyAccessExpression(expr)) { - const left = getCalledExpressionName(expr.expression); - const right = expr.name.text; - return left === undefined ? right : `${left}.${right}`; - } - else { - return undefined; - } + return ""; +} +// See also 'tryGetPropertyAccessOrIdentifierToString' +/* @internal */ +function getCalledExpressionName(expr: Expression): string | undefined { + if (isIdentifier(expr)) { + return expr.text; } - - function isFunctionOrClassExpression(node: Node): node is ArrowFunction | FunctionExpression | ClassExpression { - switch (node.kind) { - case SyntaxKind.ArrowFunction: - case SyntaxKind.FunctionExpression: - case SyntaxKind.ClassExpression: - return true; - default: - return false; - } + else if (isPropertyAccessExpression(expr)) { + const left = getCalledExpressionName(expr.expression); + const right = expr.name.text; + return left === undefined ? right : `${left}.${right}`; } - - function cleanText(text: string): string { - // Truncate to maximum amount of characters as we don't want to do a big replace operation. - text = text.length > maxLength ? text.substring(0, maxLength) + "..." : text; - - // Replaces ECMAScript line terminators and removes the trailing `\` from each line: - // \n - Line Feed - // \r - Carriage Return - // \u2028 - Line separator - // \u2029 - Paragraph separator - return text.replace(/\\?(\r?\n|\r|\u2028|\u2029)/g, ""); + else { + return undefined; } } +/* @internal */ +function isFunctionOrClassExpression(node: Node): node is ArrowFunction | FunctionExpression | ClassExpression { + switch (node.kind) { + case SyntaxKind.ArrowFunction: + case SyntaxKind.FunctionExpression: + case SyntaxKind.ClassExpression: + return true; + default: + return false; + } +} +/* @internal */ +function cleanText(text: string): string { + // Truncate to maximum amount of characters as we don't want to do a big replace operation. + text = text.length > maxLength ? text.substring(0, maxLength) + "..." : text; + // Replaces ECMAScript line terminators and removes the trailing `\` from each line: + // \n - Line Feed + // \r - Carriage Return + // \u2028 - Line separator + // \u2029 - Paragraph separator + return text.replace(/\\?(\r?\n|\r|\u2028|\u2029)/g, ""); +} diff --git a/src/services/organizeImports.ts b/src/services/organizeImports.ts index 142bdbe879100..1ad1463a809c7 100644 --- a/src/services/organizeImports.ts +++ b/src/services/organizeImports.ts @@ -1,415 +1,326 @@ +import { SourceFile, LanguageServiceHost, Program, UserPreferences, ImportDeclaration, isImportDeclaration, isExportDeclaration, isAmbientModule, ExportDeclaration, length, suppressLeadingTrivia, group, stableSort, flatMap, getNewLineOrDefaultFromHost, TransformFlags, isNamespaceImport, updateNamedImports, createImportDeclaration, Identifier, Expression, isStringLiteral, isString, some, isStringLiteralLike, NamespaceImport, ImportSpecifier, createImportSpecifier, createIdentifier, NamedImports, createNamedImports, emptyArray, ExportSpecifier, isNamedExports, updateExportDeclaration, updateNamedExports, updateNamespaceExport, NamedImportBindings, updateImportDeclaration, updateImportClause, ImportOrExportSpecifier, compareBooleans, isExternalModuleNameRelative, compareStringsCaseInsensitive } from "./ts"; +import { FormatContext } from "./ts.formatting"; +import { ChangeTracker, LeadingTriviaOption, TrailingTriviaOption } from "./ts.textChanges"; +import { Core } from "./ts.FindAllReferences"; /* @internal */ -namespace ts.OrganizeImports { - - /** - * Organize imports by: - * 1) Removing unused imports - * 2) Coalescing imports from the same module - * 3) Sorting imports - */ - export function organizeImports( - sourceFile: SourceFile, - formatContext: formatting.FormatContext, - host: LanguageServiceHost, - program: Program, - preferences: UserPreferences, - ) { - - const changeTracker = textChanges.ChangeTracker.fromContext({ host, formatContext, preferences }); - - const coalesceAndOrganizeImports = (importGroup: readonly ImportDeclaration[]) => coalesceImports(removeUnusedImports(importGroup, sourceFile, program)); - - // All of the old ImportDeclarations in the file, in syntactic order. - const topLevelImportDecls = sourceFile.statements.filter(isImportDeclaration); - organizeImportsWorker(topLevelImportDecls, coalesceAndOrganizeImports); - - // All of the old ExportDeclarations in the file, in syntactic order. - const topLevelExportDecls = sourceFile.statements.filter(isExportDeclaration); - organizeImportsWorker(topLevelExportDecls, coalesceExports); - - for (const ambientModule of sourceFile.statements.filter(isAmbientModule)) { - if (!ambientModule.body) { continue; } - - const ambientModuleImportDecls = ambientModule.body.statements.filter(isImportDeclaration); - organizeImportsWorker(ambientModuleImportDecls, coalesceAndOrganizeImports); - - const ambientModuleExportDecls = ambientModule.body.statements.filter(isExportDeclaration); - organizeImportsWorker(ambientModuleExportDecls, coalesceExports); +/** + * Organize imports by: + * 1) Removing unused imports + * 2) Coalescing imports from the same module + * 3) Sorting imports + */ +export function organizeImports(sourceFile: SourceFile, formatContext: FormatContext, host: LanguageServiceHost, program: Program, preferences: UserPreferences) { + const changeTracker = ChangeTracker.fromContext({ host, formatContext, preferences }); + const coalesceAndOrganizeImports = (importGroup: readonly ImportDeclaration[]) => coalesceImports(removeUnusedImports(importGroup, sourceFile, program)); + // All of the old ImportDeclarations in the file, in syntactic order. + const topLevelImportDecls = sourceFile.statements.filter(isImportDeclaration); + organizeImportsWorker(topLevelImportDecls, coalesceAndOrganizeImports); + // All of the old ExportDeclarations in the file, in syntactic order. + const topLevelExportDecls = sourceFile.statements.filter(isExportDeclaration); + organizeImportsWorker(topLevelExportDecls, coalesceExports); + for (const ambientModule of sourceFile.statements.filter(isAmbientModule)) { + if (!ambientModule.body) { + continue; } - - return changeTracker.getChanges(); - - function organizeImportsWorker( - oldImportDecls: readonly T[], - coalesce: (group: readonly T[]) => readonly T[]) { - - if (length(oldImportDecls) === 0) { - return; - } - - // Special case: normally, we'd expect leading and trailing trivia to follow each import - // around as it's sorted. However, we do not want this to happen for leading trivia - // on the first import because it is probably the header comment for the file. - // Consider: we could do a more careful check that this trivia is actually a header, - // but the consequences of being wrong are very minor. - suppressLeadingTrivia(oldImportDecls[0]); - - const oldImportGroups = group(oldImportDecls, importDecl => getExternalModuleName(importDecl.moduleSpecifier!)!); - const sortedImportGroups = stableSort(oldImportGroups, (group1, group2) => compareModuleSpecifiers(group1[0].moduleSpecifier!, group2[0].moduleSpecifier!)); - const newImportDecls = flatMap(sortedImportGroups, importGroup => - getExternalModuleName(importGroup[0].moduleSpecifier!) - ? coalesce(importGroup) - : importGroup); - - // Delete or replace the first import. - if (newImportDecls.length === 0) { - changeTracker.delete(sourceFile, oldImportDecls[0]); - } - else { - // Note: Delete the surrounding trivia because it will have been retained in newImportDecls. - changeTracker.replaceNodeWithNodes(sourceFile, oldImportDecls[0], newImportDecls, { - leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude, // Leave header comment in place - trailingTriviaOption: textChanges.TrailingTriviaOption.Include, - suffix: getNewLineOrDefaultFromHost(host, formatContext.options), - }); - } - - // Delete any subsequent imports. - for (let i = 1; i < oldImportDecls.length; i++) { - changeTracker.delete(sourceFile, oldImportDecls[i]); - } + const ambientModuleImportDecls = ambientModule.body.statements.filter(isImportDeclaration); + organizeImportsWorker(ambientModuleImportDecls, coalesceAndOrganizeImports); + const ambientModuleExportDecls = ambientModule.body.statements.filter(isExportDeclaration); + organizeImportsWorker(ambientModuleExportDecls, coalesceExports); + } + return changeTracker.getChanges(); + function organizeImportsWorker(oldImportDecls: readonly T[], coalesce: (group: readonly T[]) => readonly T[]) { + if (length(oldImportDecls) === 0) { + return; + } + // Special case: normally, we'd expect leading and trailing trivia to follow each import + // around as it's sorted. However, we do not want this to happen for leading trivia + // on the first import because it is probably the header comment for the file. + // Consider: we could do a more careful check that this trivia is actually a header, + // but the consequences of being wrong are very minor. + suppressLeadingTrivia(oldImportDecls[0]); + const oldImportGroups = group(oldImportDecls, importDecl => getExternalModuleName(importDecl.moduleSpecifier!)!); + const sortedImportGroups = stableSort(oldImportGroups, (group1, group2) => compareModuleSpecifiers(group1[0].moduleSpecifier!, group2[0].moduleSpecifier!)); + const newImportDecls = flatMap(sortedImportGroups, importGroup => getExternalModuleName(importGroup[0].moduleSpecifier!) + ? coalesce(importGroup) + : importGroup); + // Delete or replace the first import. + if (newImportDecls.length === 0) { + changeTracker.delete(sourceFile, oldImportDecls[0]); + } + else { + // Note: Delete the surrounding trivia because it will have been retained in newImportDecls. + changeTracker.replaceNodeWithNodes(sourceFile, oldImportDecls[0], newImportDecls, { + leadingTriviaOption: LeadingTriviaOption.Exclude, + trailingTriviaOption: TrailingTriviaOption.Include, + suffix: getNewLineOrDefaultFromHost(host, formatContext.options), + }); + } + // Delete any subsequent imports. + for (let i = 1; i < oldImportDecls.length; i++) { + changeTracker.delete(sourceFile, oldImportDecls[i]); } } - - function removeUnusedImports(oldImports: readonly ImportDeclaration[], sourceFile: SourceFile, program: Program) { - const typeChecker = program.getTypeChecker(); - const jsxNamespace = typeChecker.getJsxNamespace(sourceFile); - const jsxElementsPresent = !!(sourceFile.transformFlags & TransformFlags.ContainsJsx); - - const usedImports: ImportDeclaration[] = []; - - for (const importDecl of oldImports) { - const { importClause, moduleSpecifier } = importDecl; - - if (!importClause) { - // Imports without import clauses are assumed to be included for their side effects and are not removed. - usedImports.push(importDecl); - continue; - } - - let { name, namedBindings } = importClause; - - // Default import - if (name && !isDeclarationUsed(name)) { - name = undefined; - } - - if (namedBindings) { - if (isNamespaceImport(namedBindings)) { - // Namespace import - if (!isDeclarationUsed(namedBindings.name)) { - namedBindings = undefined; - } - } - else { - // List of named imports - const newElements = namedBindings.elements.filter(e => isDeclarationUsed(e.name)); - if (newElements.length < namedBindings.elements.length) { - namedBindings = newElements.length - ? updateNamedImports(namedBindings, newElements) - : undefined; - } +} +/* @internal */ +function removeUnusedImports(oldImports: readonly ImportDeclaration[], sourceFile: SourceFile, program: Program) { + const typeChecker = program.getTypeChecker(); + const jsxNamespace = typeChecker.getJsxNamespace(sourceFile); + const jsxElementsPresent = !!(sourceFile.transformFlags & TransformFlags.ContainsJsx); + const usedImports: ImportDeclaration[] = []; + for (const importDecl of oldImports) { + const { importClause, moduleSpecifier } = importDecl; + if (!importClause) { + // Imports without import clauses are assumed to be included for their side effects and are not removed. + usedImports.push(importDecl); + continue; + } + let { name, namedBindings } = importClause; + // Default import + if (name && !isDeclarationUsed(name)) { + name = undefined; + } + if (namedBindings) { + if (isNamespaceImport(namedBindings)) { + // Namespace import + if (!isDeclarationUsed(namedBindings.name)) { + namedBindings = undefined; } } - - if (name || namedBindings) { - usedImports.push(updateImportDeclarationAndClause(importDecl, name, namedBindings)); - } - // If a module is imported to be augmented, it’s used - else if (hasModuleDeclarationMatchingSpecifier(sourceFile, moduleSpecifier)) { - // If we’re in a declaration file, it’s safe to remove the import clause from it - if (sourceFile.isDeclarationFile) { - usedImports.push(createImportDeclaration( - importDecl.decorators, - importDecl.modifiers, - /*importClause*/ undefined, - moduleSpecifier)); - } - // If we’re not in a declaration file, we can’t remove the import clause even though - // the imported symbols are unused, because removing them makes it look like the import - // declaration has side effects, which will cause it to be preserved in the JS emit. - else { - usedImports.push(importDecl); + else { + // List of named imports + const newElements = namedBindings.elements.filter(e => isDeclarationUsed(e.name)); + if (newElements.length < namedBindings.elements.length) { + namedBindings = newElements.length + ? updateNamedImports(namedBindings, newElements) + : undefined; } } } - - return usedImports; - - function isDeclarationUsed(identifier: Identifier) { - // The JSX factory symbol is always used if JSX elements are present - even if they are not allowed. - return jsxElementsPresent && (identifier.text === jsxNamespace) || FindAllReferences.Core.isSymbolReferencedInFile(identifier, typeChecker, sourceFile); + if (name || namedBindings) { + usedImports.push(updateImportDeclarationAndClause(importDecl, name, namedBindings)); + } + // If a module is imported to be augmented, it’s used + else if (hasModuleDeclarationMatchingSpecifier(sourceFile, moduleSpecifier)) { + // If we’re in a declaration file, it’s safe to remove the import clause from it + if (sourceFile.isDeclarationFile) { + usedImports.push(createImportDeclaration(importDecl.decorators, importDecl.modifiers, + /*importClause*/ undefined, moduleSpecifier)); + } + // If we’re not in a declaration file, we can’t remove the import clause even though + // the imported symbols are unused, because removing them makes it look like the import + // declaration has side effects, which will cause it to be preserved in the JS emit. + else { + usedImports.push(importDecl); + } } } - - function hasModuleDeclarationMatchingSpecifier(sourceFile: SourceFile, moduleSpecifier: Expression) { - const moduleSpecifierText = isStringLiteral(moduleSpecifier) && moduleSpecifier.text; - return isString(moduleSpecifierText) && some(sourceFile.moduleAugmentations, moduleName => - isStringLiteral(moduleName) - && moduleName.text === moduleSpecifierText); + return usedImports; + function isDeclarationUsed(identifier: Identifier) { + // The JSX factory symbol is always used if JSX elements are present - even if they are not allowed. + return jsxElementsPresent && (identifier.text === jsxNamespace) || Core.isSymbolReferencedInFile(identifier, typeChecker, sourceFile); } - - function getExternalModuleName(specifier: Expression) { - return specifier !== undefined && isStringLiteralLike(specifier) - ? specifier.text - : undefined; +} +/* @internal */ +function hasModuleDeclarationMatchingSpecifier(sourceFile: SourceFile, moduleSpecifier: Expression) { + const moduleSpecifierText = isStringLiteral(moduleSpecifier) && moduleSpecifier.text; + return isString(moduleSpecifierText) && some(sourceFile.moduleAugmentations, moduleName => isStringLiteral(moduleName) + && moduleName.text === moduleSpecifierText); +} +/* @internal */ +function getExternalModuleName(specifier: Expression) { + return specifier !== undefined && isStringLiteralLike(specifier) + ? specifier.text + : undefined; +} +// Internal for testing +/** + * @param importGroup a list of ImportDeclarations, all with the same module name. + */ +/* @internal */ +export function coalesceImports(importGroup: readonly ImportDeclaration[]) { + if (importGroup.length === 0) { + return importGroup; } - - // Internal for testing - /** - * @param importGroup a list of ImportDeclarations, all with the same module name. - */ - export function coalesceImports(importGroup: readonly ImportDeclaration[]) { - if (importGroup.length === 0) { - return importGroup; + const { importWithoutClause, typeOnlyImports, regularImports } = getCategorizedImports(importGroup); + const coalescedImports: ImportDeclaration[] = []; + if (importWithoutClause) { + coalescedImports.push(importWithoutClause); + } + for (const group of [regularImports, typeOnlyImports]) { + const isTypeOnly = group === typeOnlyImports; + const { defaultImports, namespaceImports, namedImports } = group; + // Normally, we don't combine default and namespace imports, but it would be silly to + // produce two import declarations in this special case. + if (!isTypeOnly && defaultImports.length === 1 && namespaceImports.length === 1 && namedImports.length === 0) { + // Add the namespace import to the existing default ImportDeclaration. + const defaultImport = defaultImports[0]; + coalescedImports.push(updateImportDeclarationAndClause(defaultImport, defaultImport.importClause!.name, namespaceImports[0].importClause!.namedBindings)); // TODO: GH#18217 + continue; } - - const { importWithoutClause, typeOnlyImports, regularImports } = getCategorizedImports(importGroup); - - const coalescedImports: ImportDeclaration[] = []; - - if (importWithoutClause) { - coalescedImports.push(importWithoutClause); + const sortedNamespaceImports = stableSort(namespaceImports, (i1, i2) => compareIdentifiers((i1.importClause!.namedBindings as NamespaceImport).name, (i2.importClause!.namedBindings as NamespaceImport).name)); // TODO: GH#18217 + for (const namespaceImport of sortedNamespaceImports) { + // Drop the name, if any + coalescedImports.push(updateImportDeclarationAndClause(namespaceImport, /*name*/ undefined, namespaceImport.importClause!.namedBindings)); // TODO: GH#18217 } - - for (const group of [regularImports, typeOnlyImports]) { - const isTypeOnly = group === typeOnlyImports; - const { defaultImports, namespaceImports, namedImports } = group; - // Normally, we don't combine default and namespace imports, but it would be silly to - // produce two import declarations in this special case. - if (!isTypeOnly && defaultImports.length === 1 && namespaceImports.length === 1 && namedImports.length === 0) { - // Add the namespace import to the existing default ImportDeclaration. - const defaultImport = defaultImports[0]; - coalescedImports.push( - updateImportDeclarationAndClause(defaultImport, defaultImport.importClause!.name, namespaceImports[0].importClause!.namedBindings)); // TODO: GH#18217 - - continue; - } - - const sortedNamespaceImports = stableSort(namespaceImports, (i1, i2) => - compareIdentifiers((i1.importClause!.namedBindings as NamespaceImport).name, (i2.importClause!.namedBindings as NamespaceImport).name)); // TODO: GH#18217 - - for (const namespaceImport of sortedNamespaceImports) { - // Drop the name, if any - coalescedImports.push( - updateImportDeclarationAndClause(namespaceImport, /*name*/ undefined, namespaceImport.importClause!.namedBindings)); // TODO: GH#18217 - } - - if (defaultImports.length === 0 && namedImports.length === 0) { - continue; - } - - let newDefaultImport: Identifier | undefined; - const newImportSpecifiers: ImportSpecifier[] = []; - if (defaultImports.length === 1) { - newDefaultImport = defaultImports[0].importClause!.name; - } - else { - for (const defaultImport of defaultImports) { - newImportSpecifiers.push( - createImportSpecifier(createIdentifier("default"), defaultImport.importClause!.name!)); // TODO: GH#18217 - } + if (defaultImports.length === 0 && namedImports.length === 0) { + continue; + } + let newDefaultImport: Identifier | undefined; + const newImportSpecifiers: ImportSpecifier[] = []; + if (defaultImports.length === 1) { + newDefaultImport = defaultImports[0].importClause!.name; + } + else { + for (const defaultImport of defaultImports) { + newImportSpecifiers.push(createImportSpecifier(createIdentifier("default"), (defaultImport.importClause!.name!))); // TODO: GH#18217 } - - newImportSpecifiers.push(...flatMap(namedImports, i => (i.importClause!.namedBindings as NamedImports).elements)); // TODO: GH#18217 - - const sortedImportSpecifiers = sortSpecifiers(newImportSpecifiers); - - const importDecl = defaultImports.length > 0 - ? defaultImports[0] - : namedImports[0]; - - const newNamedImports = sortedImportSpecifiers.length === 0 - ? newDefaultImport - ? undefined - : createNamedImports(emptyArray) - : namedImports.length === 0 - ? createNamedImports(sortedImportSpecifiers) - : updateNamedImports(namedImports[0].importClause!.namedBindings as NamedImports, sortedImportSpecifiers); // TODO: GH#18217 - - // Type-only imports are not allowed to mix default, namespace, and named imports in any combination. - // We could rewrite a default import as a named import (`import { default as name }`), but we currently - // choose not to as a stylistic preference. - if (isTypeOnly && newDefaultImport && newNamedImports) { - coalescedImports.push( - updateImportDeclarationAndClause(importDecl, newDefaultImport, /*namedBindings*/ undefined)); - coalescedImports.push( - updateImportDeclarationAndClause(namedImports[0] ?? importDecl, /*name*/ undefined, newNamedImports)); + } + newImportSpecifiers.push(...flatMap(namedImports, i => (i.importClause!.namedBindings as NamedImports).elements)); // TODO: GH#18217 + const sortedImportSpecifiers = sortSpecifiers(newImportSpecifiers); + const importDecl = defaultImports.length > 0 + ? defaultImports[0] + : namedImports[0]; + const newNamedImports = sortedImportSpecifiers.length === 0 + ? newDefaultImport + ? undefined + : createNamedImports(emptyArray) + : namedImports.length === 0 + ? createNamedImports(sortedImportSpecifiers) + : updateNamedImports((namedImports[0].importClause!.namedBindings as NamedImports), sortedImportSpecifiers); // TODO: GH#18217 + // Type-only imports are not allowed to mix default, namespace, and named imports in any combination. + // We could rewrite a default import as a named import (`import { default as name }`), but we currently + // choose not to as a stylistic preference. + if (isTypeOnly && newDefaultImport && newNamedImports) { + coalescedImports.push(updateImportDeclarationAndClause(importDecl, newDefaultImport, /*namedBindings*/ undefined)); + coalescedImports.push(updateImportDeclarationAndClause(namedImports[0] ?? importDecl, /*name*/ undefined, newNamedImports)); + } + else { + coalescedImports.push(updateImportDeclarationAndClause(importDecl, newDefaultImport, newNamedImports)); + } + } + return coalescedImports; +} +/* @internal */ +interface ImportGroup { + defaultImports: ImportDeclaration[]; + namespaceImports: ImportDeclaration[]; + namedImports: ImportDeclaration[]; +} +/* + * Returns entire import declarations because they may already have been rewritten and + * may lack parent pointers. The desired parts can easily be recovered based on the + * categorization. + * + * NB: There may be overlap between `defaultImports` and `namespaceImports`/`namedImports`. + */ +/* @internal */ +function getCategorizedImports(importGroup: readonly ImportDeclaration[]) { + let importWithoutClause: ImportDeclaration | undefined; + const typeOnlyImports: ImportGroup = { defaultImports: [], namespaceImports: [], namedImports: [] }; + const regularImports: ImportGroup = { defaultImports: [], namespaceImports: [], namedImports: [] }; + for (const importDeclaration of importGroup) { + if (importDeclaration.importClause === undefined) { + // Only the first such import is interesting - the others are redundant. + // Note: Unfortunately, we will lose trivia that was on this node. + importWithoutClause = importWithoutClause || importDeclaration; + continue; + } + const group = importDeclaration.importClause.isTypeOnly ? typeOnlyImports : regularImports; + const { name, namedBindings } = importDeclaration.importClause; + if (name) { + group.defaultImports.push(importDeclaration); + } + if (namedBindings) { + if (isNamespaceImport(namedBindings)) { + group.namespaceImports.push(importDeclaration); } else { - coalescedImports.push( - updateImportDeclarationAndClause(importDecl, newDefaultImport, newNamedImports)); + group.namedImports.push(importDeclaration); } } - - return coalescedImports; - } - - interface ImportGroup { - defaultImports: ImportDeclaration[]; - namespaceImports: ImportDeclaration[]; - namedImports: ImportDeclaration[]; + return { + importWithoutClause, + typeOnlyImports, + regularImports, + }; +} +// Internal for testing +/** + * @param exportGroup a list of ExportDeclarations, all with the same module name. + */ +/* @internal */ +export function coalesceExports(exportGroup: readonly ExportDeclaration[]) { + if (exportGroup.length === 0) { + return exportGroup; } - + const { exportWithoutClause, namedExports, typeOnlyExports } = getCategorizedExports(exportGroup); + const coalescedExports: ExportDeclaration[] = []; + if (exportWithoutClause) { + coalescedExports.push(exportWithoutClause); + } + for (const exportGroup of [namedExports, typeOnlyExports]) { + if (exportGroup.length === 0) { + continue; + } + const newExportSpecifiers: ExportSpecifier[] = []; + newExportSpecifiers.push(...flatMap(exportGroup, i => i.exportClause && isNamedExports(i.exportClause) ? i.exportClause.elements : emptyArray)); + const sortedExportSpecifiers = sortSpecifiers(newExportSpecifiers); + const exportDecl = exportGroup[0]; + coalescedExports.push(updateExportDeclaration(exportDecl, exportDecl.decorators, exportDecl.modifiers, exportDecl.exportClause && (isNamedExports(exportDecl.exportClause) ? + updateNamedExports(exportDecl.exportClause, sortedExportSpecifiers) : + updateNamespaceExport(exportDecl.exportClause, exportDecl.exportClause.name)), exportDecl.moduleSpecifier, exportDecl.isTypeOnly)); + } + return coalescedExports; /* - * Returns entire import declarations because they may already have been rewritten and + * Returns entire export declarations because they may already have been rewritten and * may lack parent pointers. The desired parts can easily be recovered based on the * categorization. - * - * NB: There may be overlap between `defaultImports` and `namespaceImports`/`namedImports`. */ - function getCategorizedImports(importGroup: readonly ImportDeclaration[]) { - let importWithoutClause: ImportDeclaration | undefined; - const typeOnlyImports: ImportGroup = { defaultImports: [], namespaceImports: [], namedImports: [] }; - const regularImports: ImportGroup = { defaultImports: [], namespaceImports: [], namedImports: [] }; - - for (const importDeclaration of importGroup) { - if (importDeclaration.importClause === undefined) { - // Only the first such import is interesting - the others are redundant. + function getCategorizedExports(exportGroup: readonly ExportDeclaration[]) { + let exportWithoutClause: ExportDeclaration | undefined; + const namedExports: ExportDeclaration[] = []; + const typeOnlyExports: ExportDeclaration[] = []; + for (const exportDeclaration of exportGroup) { + if (exportDeclaration.exportClause === undefined) { + // Only the first such export is interesting - the others are redundant. // Note: Unfortunately, we will lose trivia that was on this node. - importWithoutClause = importWithoutClause || importDeclaration; - continue; + exportWithoutClause = exportWithoutClause || exportDeclaration; } - - const group = importDeclaration.importClause.isTypeOnly ? typeOnlyImports : regularImports; - const { name, namedBindings } = importDeclaration.importClause; - - if (name) { - group.defaultImports.push(importDeclaration); + else if (exportDeclaration.isTypeOnly) { + typeOnlyExports.push(exportDeclaration); } - - if (namedBindings) { - if (isNamespaceImport(namedBindings)) { - group.namespaceImports.push(importDeclaration); - } - else { - group.namedImports.push(importDeclaration); - } + else { + namedExports.push(exportDeclaration); } } - return { - importWithoutClause, - typeOnlyImports, - regularImports, + exportWithoutClause, + namedExports, + typeOnlyExports, }; } - - // Internal for testing - /** - * @param exportGroup a list of ExportDeclarations, all with the same module name. - */ - export function coalesceExports(exportGroup: readonly ExportDeclaration[]) { - if (exportGroup.length === 0) { - return exportGroup; - } - - const { exportWithoutClause, namedExports, typeOnlyExports } = getCategorizedExports(exportGroup); - - const coalescedExports: ExportDeclaration[] = []; - - if (exportWithoutClause) { - coalescedExports.push(exportWithoutClause); - } - - for (const exportGroup of [namedExports, typeOnlyExports]) { - if (exportGroup.length === 0) { - continue; - } - const newExportSpecifiers: ExportSpecifier[] = []; - newExportSpecifiers.push(...flatMap(exportGroup, i => i.exportClause && isNamedExports(i.exportClause) ? i.exportClause.elements : emptyArray)); - - const sortedExportSpecifiers = sortSpecifiers(newExportSpecifiers); - - const exportDecl = exportGroup[0]; - coalescedExports.push( - updateExportDeclaration( - exportDecl, - exportDecl.decorators, - exportDecl.modifiers, - exportDecl.exportClause && ( - isNamedExports(exportDecl.exportClause) ? - updateNamedExports(exportDecl.exportClause, sortedExportSpecifiers) : - updateNamespaceExport(exportDecl.exportClause, exportDecl.exportClause.name) - ), - exportDecl.moduleSpecifier, - exportDecl.isTypeOnly)); - } - - return coalescedExports; - - /* - * Returns entire export declarations because they may already have been rewritten and - * may lack parent pointers. The desired parts can easily be recovered based on the - * categorization. - */ - function getCategorizedExports(exportGroup: readonly ExportDeclaration[]) { - let exportWithoutClause: ExportDeclaration | undefined; - const namedExports: ExportDeclaration[] = []; - const typeOnlyExports: ExportDeclaration[] = []; - - for (const exportDeclaration of exportGroup) { - if (exportDeclaration.exportClause === undefined) { - // Only the first such export is interesting - the others are redundant. - // Note: Unfortunately, we will lose trivia that was on this node. - exportWithoutClause = exportWithoutClause || exportDeclaration; - } - else if (exportDeclaration.isTypeOnly) { - typeOnlyExports.push(exportDeclaration); - } - else { - namedExports.push(exportDeclaration); - } - } - - return { - exportWithoutClause, - namedExports, - typeOnlyExports, - }; - } - } - - function updateImportDeclarationAndClause( - importDeclaration: ImportDeclaration, - name: Identifier | undefined, - namedBindings: NamedImportBindings | undefined) { - - return updateImportDeclaration( - importDeclaration, - importDeclaration.decorators, - importDeclaration.modifiers, - updateImportClause(importDeclaration.importClause!, name, namedBindings, importDeclaration.importClause!.isTypeOnly), // TODO: GH#18217 - importDeclaration.moduleSpecifier); - } - - function sortSpecifiers(specifiers: readonly T[]) { - return stableSort(specifiers, (s1, s2) => - compareIdentifiers(s1.propertyName || s1.name, s2.propertyName || s2.name) || - compareIdentifiers(s1.name, s2.name)); - } - - /* internal */ // Exported for testing - export function compareModuleSpecifiers(m1: Expression, m2: Expression) { - const name1 = getExternalModuleName(m1); - const name2 = getExternalModuleName(m2); - return compareBooleans(name1 === undefined, name2 === undefined) || - compareBooleans(isExternalModuleNameRelative(name1!), isExternalModuleNameRelative(name2!)) || - compareStringsCaseInsensitive(name1!, name2!); - } - - function compareIdentifiers(s1: Identifier, s2: Identifier) { - return compareStringsCaseInsensitive(s1.text, s2.text); - } +} +/* @internal */ +function updateImportDeclarationAndClause(importDeclaration: ImportDeclaration, name: Identifier | undefined, namedBindings: NamedImportBindings | undefined) { + return updateImportDeclaration(importDeclaration, importDeclaration.decorators, importDeclaration.modifiers, updateImportClause((importDeclaration.importClause!), name, namedBindings, importDeclaration.importClause!.isTypeOnly), // TODO: GH#18217 + importDeclaration.moduleSpecifier); +} +/* @internal */ +function sortSpecifiers(specifiers: readonly T[]) { + return stableSort(specifiers, (s1, s2) => compareIdentifiers(s1.propertyName || s1.name, s2.propertyName || s2.name) || + compareIdentifiers(s1.name, s2.name)); +} +/* internal */ // Exported for testing +/* @internal */ +export function compareModuleSpecifiers(m1: Expression, m2: Expression) { + const name1 = getExternalModuleName(m1); + const name2 = getExternalModuleName(m2); + return compareBooleans(name1 === undefined, name2 === undefined) || + compareBooleans(isExternalModuleNameRelative((name1!)), isExternalModuleNameRelative((name2!))) || + compareStringsCaseInsensitive((name1!), (name2!)); +} +/* @internal */ +function compareIdentifiers(s1: Identifier, s2: Identifier) { + return compareStringsCaseInsensitive(s1.text, s2.text); } diff --git a/src/services/outliningElementsCollector.ts b/src/services/outliningElementsCollector.ts index ab8cfb3f6c33e..813f032d6f6f8 100644 --- a/src/services/outliningElementsCollector.ts +++ b/src/services/outliningElementsCollector.ts @@ -1,277 +1,266 @@ +import { SourceFile, CancellationToken, OutliningSpan, Push, isAnyImportSyntax, findChildOfKind, SyntaxKind, OutliningSpanKind, Node, isDeclaration, isCallExpression, isIfStatement, isFunctionExpression, isArrowFunction, findAncestor, isVariableStatement, getSingleInitializerOfVariableStatementOrPropertyDeclaration, isInComment, createTextSpanFromBounds, getLeadingCommentRangesOfNode, Debug, isFunctionLike, Block, TryStatement, createTextSpanFromNode, JsxElement, JsxFragment, JsxOpeningLikeElement, TemplateExpression, NoSubstitutionTemplateLiteral, JsxAttributes, isArrayLiteralExpression, FunctionLike, isNodeArrayMultiLine, TextSpan } from "./ts"; /* @internal */ -namespace ts.OutliningElementsCollector { - export function collectElements(sourceFile: SourceFile, cancellationToken: CancellationToken): OutliningSpan[] { - const res: OutliningSpan[] = []; - addNodeOutliningSpans(sourceFile, cancellationToken, res); - addRegionOutliningSpans(sourceFile, res); - return res.sort((span1, span2) => span1.textSpan.start - span2.textSpan.start); +export function collectElements(sourceFile: SourceFile, cancellationToken: CancellationToken): OutliningSpan[] { + const res: OutliningSpan[] = []; + addNodeOutliningSpans(sourceFile, cancellationToken, res); + addRegionOutliningSpans(sourceFile, res); + return res.sort((span1, span2) => span1.textSpan.start - span2.textSpan.start); +} +/* @internal */ +function addNodeOutliningSpans(sourceFile: SourceFile, cancellationToken: CancellationToken, out: Push): void { + let depthRemaining = 40; + let current = 0; + // Includes the EOF Token so that comments which aren't attached to statements are included + const statements = [...sourceFile.statements, sourceFile.endOfFileToken]; + const n = statements.length; + while (current < n) { + while (current < n && !isAnyImportSyntax(statements[current])) { + visitNonImportNode(statements[current]); + current++; + } + if (current === n) + break; + const firstImport = current; + while (current < n && isAnyImportSyntax(statements[current])) { + addOutliningForLeadingCommentsForNode(statements[current], sourceFile, cancellationToken, out); + current++; + } + const lastImport = current - 1; + if (lastImport !== firstImport) { + out.push(createOutliningSpanFromBounds(findChildOfKind(statements[firstImport], SyntaxKind.ImportKeyword, sourceFile)!.getStart(sourceFile), statements[lastImport].getEnd(), OutliningSpanKind.Imports)); + } } - - function addNodeOutliningSpans(sourceFile: SourceFile, cancellationToken: CancellationToken, out: Push): void { - let depthRemaining = 40; - let current = 0; - // Includes the EOF Token so that comments which aren't attached to statements are included - const statements = [...sourceFile.statements, sourceFile.endOfFileToken]; - const n = statements.length; - while (current < n) { - while (current < n && !isAnyImportSyntax(statements[current])) { - visitNonImportNode(statements[current]); - current++; - } - if (current === n) break; - const firstImport = current; - while (current < n && isAnyImportSyntax(statements[current])) { - addOutliningForLeadingCommentsForNode(statements[current], sourceFile, cancellationToken, out); - current++; - } - const lastImport = current - 1; - if (lastImport !== firstImport) { - out.push(createOutliningSpanFromBounds(findChildOfKind(statements[firstImport], SyntaxKind.ImportKeyword, sourceFile)!.getStart(sourceFile), statements[lastImport].getEnd(), OutliningSpanKind.Imports)); - } + function visitNonImportNode(n: Node) { + if (depthRemaining === 0) + return; + cancellationToken.throwIfCancellationRequested(); + if (isDeclaration(n) || n.kind === SyntaxKind.EndOfFileToken) { + addOutliningForLeadingCommentsForNode(n, sourceFile, cancellationToken, out); } - - function visitNonImportNode(n: Node) { - if (depthRemaining === 0) return; - cancellationToken.throwIfCancellationRequested(); - - if (isDeclaration(n) || n.kind === SyntaxKind.EndOfFileToken) { - addOutliningForLeadingCommentsForNode(n, sourceFile, cancellationToken, out); - } - - if (isFunctionExpressionAssignedToVariable(n)) { - addOutliningForLeadingCommentsForNode(n.parent.parent.parent, sourceFile, cancellationToken, out); - } - - const span = getOutliningSpanForNode(n, sourceFile); - if (span) out.push(span); - + if (isFunctionExpressionAssignedToVariable(n)) { + addOutliningForLeadingCommentsForNode(n.parent.parent.parent, sourceFile, cancellationToken, out); + } + const span = getOutliningSpanForNode(n, sourceFile); + if (span) + out.push(span); + depthRemaining--; + if (isCallExpression(n)) { + depthRemaining++; + visitNonImportNode(n.expression); depthRemaining--; - if (isCallExpression(n)) { - depthRemaining++; - visitNonImportNode(n.expression); - depthRemaining--; - n.arguments.forEach(visitNonImportNode); - n.typeArguments?.forEach(visitNonImportNode); - } - else if (isIfStatement(n) && n.elseStatement && isIfStatement(n.elseStatement)) { - // Consider an 'else if' to be on the same depth as the 'if'. - visitNonImportNode(n.expression); - visitNonImportNode(n.thenStatement); - depthRemaining++; - visitNonImportNode(n.elseStatement); - depthRemaining--; - } - else { - n.forEachChild(visitNonImportNode); - } + n.arguments.forEach(visitNonImportNode); + n.typeArguments?.forEach(visitNonImportNode); + } + else if (isIfStatement(n) && n.elseStatement && isIfStatement(n.elseStatement)) { + // Consider an 'else if' to be on the same depth as the 'if'. + visitNonImportNode(n.expression); + visitNonImportNode(n.thenStatement); depthRemaining++; + visitNonImportNode(n.elseStatement); + depthRemaining--; } - - function isFunctionExpressionAssignedToVariable(n: Node) { - if (!isFunctionExpression(n) && !isArrowFunction(n)) { - return false; - } - const ancestor = findAncestor(n, isVariableStatement); - return !!ancestor && getSingleInitializerOfVariableStatementOrPropertyDeclaration(ancestor) === n; + else { + n.forEachChild(visitNonImportNode); } + depthRemaining++; } - - function addRegionOutliningSpans(sourceFile: SourceFile, out: Push): void { - const regions: OutliningSpan[] = []; - const lineStarts = sourceFile.getLineStarts(); - for (const currentLineStart of lineStarts) { - const lineEnd = sourceFile.getLineEndOfPosition(currentLineStart); - const lineText = sourceFile.text.substring(currentLineStart, lineEnd); - const result = isRegionDelimiter(lineText); - if (!result || isInComment(sourceFile, currentLineStart)) { - continue; - } - - if (!result[1]) { - const span = createTextSpanFromBounds(sourceFile.text.indexOf("//", currentLineStart), lineEnd); - regions.push(createOutliningSpan(span, OutliningSpanKind.Region, span, /*autoCollapse*/ false, result[2] || "#region")); - } - else { - const region = regions.pop(); - if (region) { - region.textSpan.length = lineEnd - region.textSpan.start; - region.hintSpan.length = lineEnd - region.textSpan.start; - out.push(region); - } - } + function isFunctionExpressionAssignedToVariable(n: Node) { + if (!isFunctionExpression(n) && !isArrowFunction(n)) { + return false; } + const ancestor = findAncestor(n, isVariableStatement); + return !!ancestor && getSingleInitializerOfVariableStatementOrPropertyDeclaration(ancestor) === n; } - - const regionDelimiterRegExp = /^\s*\/\/\s*#(end)?region(?:\s+(.*))?(?:\r)?$/; - function isRegionDelimiter(lineText: string) { - return regionDelimiterRegExp.exec(lineText); - } - - function addOutliningForLeadingCommentsForNode(n: Node, sourceFile: SourceFile, cancellationToken: CancellationToken, out: Push): void { - const comments = getLeadingCommentRangesOfNode(n, sourceFile); - if (!comments) return; - let firstSingleLineCommentStart = -1; - let lastSingleLineCommentEnd = -1; - let singleLineCommentCount = 0; - const sourceText = sourceFile.getFullText(); - for (const { kind, pos, end } of comments) { - cancellationToken.throwIfCancellationRequested(); - switch (kind) { - case SyntaxKind.SingleLineCommentTrivia: - // never fold region delimiters into single-line comment regions - const commentText = sourceText.slice(pos, end); - if (isRegionDelimiter(commentText)) { - combineAndAddMultipleSingleLineComments(); - singleLineCommentCount = 0; - break; - } - - // For single line comments, combine consecutive ones (2 or more) into - // a single span from the start of the first till the end of the last - if (singleLineCommentCount === 0) { - firstSingleLineCommentStart = pos; - } - lastSingleLineCommentEnd = end; - singleLineCommentCount++; - break; - case SyntaxKind.MultiLineCommentTrivia: - combineAndAddMultipleSingleLineComments(); - out.push(createOutliningSpanFromBounds(pos, end, OutliningSpanKind.Comment)); - singleLineCommentCount = 0; - break; - default: - Debug.assertNever(kind); - } +} +/* @internal */ +function addRegionOutliningSpans(sourceFile: SourceFile, out: Push): void { + const regions: OutliningSpan[] = []; + const lineStarts = sourceFile.getLineStarts(); + for (const currentLineStart of lineStarts) { + const lineEnd = sourceFile.getLineEndOfPosition(currentLineStart); + const lineText = sourceFile.text.substring(currentLineStart, lineEnd); + const result = isRegionDelimiter(lineText); + if (!result || isInComment(sourceFile, currentLineStart)) { + continue; + } + if (!result[1]) { + const span = createTextSpanFromBounds(sourceFile.text.indexOf("//", currentLineStart), lineEnd); + regions.push(createOutliningSpan(span, OutliningSpanKind.Region, span, /*autoCollapse*/ false, result[2] || "#region")); } - combineAndAddMultipleSingleLineComments(); - - function combineAndAddMultipleSingleLineComments(): void { - // Only outline spans of two or more consecutive single line comments - if (singleLineCommentCount > 1) { - out.push(createOutliningSpanFromBounds(firstSingleLineCommentStart, lastSingleLineCommentEnd, OutliningSpanKind.Comment)); + else { + const region = regions.pop(); + if (region) { + region.textSpan.length = lineEnd - region.textSpan.start; + region.hintSpan.length = lineEnd - region.textSpan.start; + out.push(region); } } } - - function createOutliningSpanFromBounds(pos: number, end: number, kind: OutliningSpanKind): OutliningSpan { - return createOutliningSpan(createTextSpanFromBounds(pos, end), kind); - } - - function getOutliningSpanForNode(n: Node, sourceFile: SourceFile): OutliningSpan | undefined { - switch (n.kind) { - case SyntaxKind.Block: - if (isFunctionLike(n.parent)) { - return functionSpan(n.parent, n as Block, sourceFile); +} +/* @internal */ +const regionDelimiterRegExp = /^\s*\/\/\s*#(end)?region(?:\s+(.*))?(?:\r)?$/; +/* @internal */ +function isRegionDelimiter(lineText: string) { + return regionDelimiterRegExp.exec(lineText); +} +/* @internal */ +function addOutliningForLeadingCommentsForNode(n: Node, sourceFile: SourceFile, cancellationToken: CancellationToken, out: Push): void { + const comments = getLeadingCommentRangesOfNode(n, sourceFile); + if (!comments) + return; + let firstSingleLineCommentStart = -1; + let lastSingleLineCommentEnd = -1; + let singleLineCommentCount = 0; + const sourceText = sourceFile.getFullText(); + for (const { kind, pos, end } of comments) { + cancellationToken.throwIfCancellationRequested(); + switch (kind) { + case SyntaxKind.SingleLineCommentTrivia: + // never fold region delimiters into single-line comment regions + const commentText = sourceText.slice(pos, end); + if (isRegionDelimiter(commentText)) { + combineAndAddMultipleSingleLineComments(); + singleLineCommentCount = 0; + break; } - // Check if the block is standalone, or 'attached' to some parent statement. - // If the latter, we want to collapse the block, but consider its hint span - // to be the entire span of the parent. - switch (n.parent.kind) { - case SyntaxKind.DoStatement: - case SyntaxKind.ForInStatement: - case SyntaxKind.ForOfStatement: - case SyntaxKind.ForStatement: - case SyntaxKind.IfStatement: - case SyntaxKind.WhileStatement: - case SyntaxKind.WithStatement: - case SyntaxKind.CatchClause: - return spanForNode(n.parent); - case SyntaxKind.TryStatement: - // Could be the try-block, or the finally-block. - const tryStatement = n.parent; - if (tryStatement.tryBlock === n) { - return spanForNode(n.parent); - } - else if (tryStatement.finallyBlock === n) { - const node = findChildOfKind(tryStatement, SyntaxKind.FinallyKeyword, sourceFile); - if (node) return spanForNode(node); - } - // falls through - default: - // Block was a standalone block. In this case we want to only collapse - // the span of the block, independent of any parent span. - return createOutliningSpan(createTextSpanFromNode(n, sourceFile), OutliningSpanKind.Code); + // For single line comments, combine consecutive ones (2 or more) into + // a single span from the start of the first till the end of the last + if (singleLineCommentCount === 0) { + firstSingleLineCommentStart = pos; } - case SyntaxKind.ModuleBlock: - return spanForNode(n.parent); - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ClassExpression: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.CaseBlock: - return spanForNode(n); - case SyntaxKind.ObjectLiteralExpression: - return spanForObjectOrArrayLiteral(n); - case SyntaxKind.ArrayLiteralExpression: - return spanForObjectOrArrayLiteral(n, SyntaxKind.OpenBracketToken); - case SyntaxKind.JsxElement: - return spanForJSXElement(n); - case SyntaxKind.JsxFragment: - return spanForJSXFragment(n); - case SyntaxKind.JsxSelfClosingElement: - case SyntaxKind.JsxOpeningElement: - return spanForJSXAttributes((n).attributes); - case SyntaxKind.TemplateExpression: - case SyntaxKind.NoSubstitutionTemplateLiteral: - return spanForTemplateLiteral(n); - } - - function spanForJSXElement(node: JsxElement): OutliningSpan | undefined { - const textSpan = createTextSpanFromBounds(node.openingElement.getStart(sourceFile), node.closingElement.getEnd()); - const tagName = node.openingElement.tagName.getText(sourceFile); - const bannerText = "<" + tagName + ">..."; - return createOutliningSpan(textSpan, OutliningSpanKind.Code, textSpan, /*autoCollapse*/ false, bannerText); + lastSingleLineCommentEnd = end; + singleLineCommentCount++; + break; + case SyntaxKind.MultiLineCommentTrivia: + combineAndAddMultipleSingleLineComments(); + out.push(createOutliningSpanFromBounds(pos, end, OutliningSpanKind.Comment)); + singleLineCommentCount = 0; + break; + default: + Debug.assertNever(kind); } - - function spanForJSXFragment(node: JsxFragment): OutliningSpan | undefined { - const textSpan = createTextSpanFromBounds(node.openingFragment.getStart(sourceFile), node.closingFragment.getEnd()); - const bannerText = "<>..."; - return createOutliningSpan(textSpan, OutliningSpanKind.Code, textSpan, /*autoCollapse*/ false, bannerText); + } + combineAndAddMultipleSingleLineComments(); + function combineAndAddMultipleSingleLineComments(): void { + // Only outline spans of two or more consecutive single line comments + if (singleLineCommentCount > 1) { + out.push(createOutliningSpanFromBounds(firstSingleLineCommentStart, lastSingleLineCommentEnd, OutliningSpanKind.Comment)); } - - function spanForJSXAttributes(node: JsxAttributes): OutliningSpan | undefined { - if (node.properties.length === 0) { - return undefined; + } +} +/* @internal */ +function createOutliningSpanFromBounds(pos: number, end: number, kind: OutliningSpanKind): OutliningSpan { + return createOutliningSpan(createTextSpanFromBounds(pos, end), kind); +} +/* @internal */ +function getOutliningSpanForNode(n: Node, sourceFile: SourceFile): OutliningSpan | undefined { + switch (n.kind) { + case SyntaxKind.Block: + if (isFunctionLike(n.parent)) { + return functionSpan(n.parent, (n as Block), sourceFile); } - - return createOutliningSpanFromBounds(node.getStart(sourceFile), node.getEnd(), OutliningSpanKind.Code); - } - - function spanForTemplateLiteral(node: TemplateExpression | NoSubstitutionTemplateLiteral) { - if (node.kind === SyntaxKind.NoSubstitutionTemplateLiteral && node.text.length === 0) { - return undefined; + // Check if the block is standalone, or 'attached' to some parent statement. + // If the latter, we want to collapse the block, but consider its hint span + // to be the entire span of the parent. + switch (n.parent.kind) { + case SyntaxKind.DoStatement: + case SyntaxKind.ForInStatement: + case SyntaxKind.ForOfStatement: + case SyntaxKind.ForStatement: + case SyntaxKind.IfStatement: + case SyntaxKind.WhileStatement: + case SyntaxKind.WithStatement: + case SyntaxKind.CatchClause: + return spanForNode(n.parent); + case SyntaxKind.TryStatement: + // Could be the try-block, or the finally-block. + const tryStatement = (n.parent); + if (tryStatement.tryBlock === n) { + return spanForNode(n.parent); + } + else if (tryStatement.finallyBlock === n) { + const node = findChildOfKind(tryStatement, SyntaxKind.FinallyKeyword, sourceFile); + if (node) + return spanForNode(node); + } + // falls through + default: + // Block was a standalone block. In this case we want to only collapse + // the span of the block, independent of any parent span. + return createOutliningSpan(createTextSpanFromNode(n, sourceFile), OutliningSpanKind.Code); } - return createOutliningSpanFromBounds(node.getStart(sourceFile), node.getEnd(), OutliningSpanKind.Code); - } - - function spanForObjectOrArrayLiteral(node: Node, open: SyntaxKind.OpenBraceToken | SyntaxKind.OpenBracketToken = SyntaxKind.OpenBraceToken): OutliningSpan | undefined { - // If the block has no leading keywords and is inside an array literal or call expression, - // we only want to collapse the span of the block. - // Otherwise, the collapsed section will include the end of the previous line. - return spanForNode(node, /*autoCollapse*/ false, /*useFullStart*/ !isArrayLiteralExpression(node.parent) && !isCallExpression(node.parent), open); - } - - function spanForNode(hintSpanNode: Node, autoCollapse = false, useFullStart = true, open: SyntaxKind.OpenBraceToken | SyntaxKind.OpenBracketToken = SyntaxKind.OpenBraceToken, close: SyntaxKind = open === SyntaxKind.OpenBraceToken ? SyntaxKind.CloseBraceToken : SyntaxKind.CloseBracketToken): OutliningSpan | undefined { - const openToken = findChildOfKind(n, open, sourceFile); - const closeToken = findChildOfKind(n, close, sourceFile); - return openToken && closeToken && spanBetweenTokens(openToken, closeToken, hintSpanNode, sourceFile, autoCollapse, useFullStart); + case SyntaxKind.ModuleBlock: + return spanForNode(n.parent); + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ClassExpression: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.CaseBlock: + return spanForNode(n); + case SyntaxKind.ObjectLiteralExpression: + return spanForObjectOrArrayLiteral(n); + case SyntaxKind.ArrayLiteralExpression: + return spanForObjectOrArrayLiteral(n, SyntaxKind.OpenBracketToken); + case SyntaxKind.JsxElement: + return spanForJSXElement((n)); + case SyntaxKind.JsxFragment: + return spanForJSXFragment((n)); + case SyntaxKind.JsxSelfClosingElement: + case SyntaxKind.JsxOpeningElement: + return spanForJSXAttributes((n).attributes); + case SyntaxKind.TemplateExpression: + case SyntaxKind.NoSubstitutionTemplateLiteral: + return spanForTemplateLiteral((n)); + } + function spanForJSXElement(node: JsxElement): OutliningSpan | undefined { + const textSpan = createTextSpanFromBounds(node.openingElement.getStart(sourceFile), node.closingElement.getEnd()); + const tagName = node.openingElement.tagName.getText(sourceFile); + const bannerText = "<" + tagName + ">..."; + return createOutliningSpan(textSpan, OutliningSpanKind.Code, textSpan, /*autoCollapse*/ false, bannerText); + } + function spanForJSXFragment(node: JsxFragment): OutliningSpan | undefined { + const textSpan = createTextSpanFromBounds(node.openingFragment.getStart(sourceFile), node.closingFragment.getEnd()); + const bannerText = "<>..."; + return createOutliningSpan(textSpan, OutliningSpanKind.Code, textSpan, /*autoCollapse*/ false, bannerText); + } + function spanForJSXAttributes(node: JsxAttributes): OutliningSpan | undefined { + if (node.properties.length === 0) { + return undefined; } + return createOutliningSpanFromBounds(node.getStart(sourceFile), node.getEnd(), OutliningSpanKind.Code); } - - function functionSpan(node: FunctionLike, body: Block, sourceFile: SourceFile): OutliningSpan | undefined { - const openToken = isNodeArrayMultiLine(node.parameters, sourceFile) - ? findChildOfKind(node, SyntaxKind.OpenParenToken, sourceFile) - : findChildOfKind(body, SyntaxKind.OpenBraceToken, sourceFile); - const closeToken = findChildOfKind(body, SyntaxKind.CloseBraceToken, sourceFile); - return openToken && closeToken && spanBetweenTokens(openToken, closeToken, node, sourceFile, /*autoCollapse*/ node.kind !== SyntaxKind.ArrowFunction); + function spanForTemplateLiteral(node: TemplateExpression | NoSubstitutionTemplateLiteral) { + if (node.kind === SyntaxKind.NoSubstitutionTemplateLiteral && node.text.length === 0) { + return undefined; + } + return createOutliningSpanFromBounds(node.getStart(sourceFile), node.getEnd(), OutliningSpanKind.Code); } - - function spanBetweenTokens(openToken: Node, closeToken: Node, hintSpanNode: Node, sourceFile: SourceFile, autoCollapse = false, useFullStart = true): OutliningSpan { - const textSpan = createTextSpanFromBounds(useFullStart ? openToken.getFullStart() : openToken.getStart(sourceFile), closeToken.getEnd()); - return createOutliningSpan(textSpan, OutliningSpanKind.Code, createTextSpanFromNode(hintSpanNode, sourceFile), autoCollapse); + function spanForObjectOrArrayLiteral(node: Node, open: SyntaxKind.OpenBraceToken | SyntaxKind.OpenBracketToken = SyntaxKind.OpenBraceToken): OutliningSpan | undefined { + // If the block has no leading keywords and is inside an array literal or call expression, + // we only want to collapse the span of the block. + // Otherwise, the collapsed section will include the end of the previous line. + return spanForNode(node, /*autoCollapse*/ false, /*useFullStart*/ !isArrayLiteralExpression(node.parent) && !isCallExpression(node.parent), open); } - - function createOutliningSpan(textSpan: TextSpan, kind: OutliningSpanKind, hintSpan: TextSpan = textSpan, autoCollapse = false, bannerText = "..."): OutliningSpan { - return { textSpan, kind, hintSpan, bannerText, autoCollapse }; + function spanForNode(hintSpanNode: Node, autoCollapse = false, useFullStart = true, open: SyntaxKind.OpenBraceToken | SyntaxKind.OpenBracketToken = SyntaxKind.OpenBraceToken, close: SyntaxKind = open === SyntaxKind.OpenBraceToken ? SyntaxKind.CloseBraceToken : SyntaxKind.CloseBracketToken): OutliningSpan | undefined { + const openToken = findChildOfKind(n, open, sourceFile); + const closeToken = findChildOfKind(n, close, sourceFile); + return openToken && closeToken && spanBetweenTokens(openToken, closeToken, hintSpanNode, sourceFile, autoCollapse, useFullStart); } } +/* @internal */ +function functionSpan(node: FunctionLike, body: Block, sourceFile: SourceFile): OutliningSpan | undefined { + const openToken = isNodeArrayMultiLine(node.parameters, sourceFile) + ? findChildOfKind(node, SyntaxKind.OpenParenToken, sourceFile) + : findChildOfKind(body, SyntaxKind.OpenBraceToken, sourceFile); + const closeToken = findChildOfKind(body, SyntaxKind.CloseBraceToken, sourceFile); + return openToken && closeToken && spanBetweenTokens(openToken, closeToken, node, sourceFile, /*autoCollapse*/ node.kind !== SyntaxKind.ArrowFunction); +} +/* @internal */ +function spanBetweenTokens(openToken: Node, closeToken: Node, hintSpanNode: Node, sourceFile: SourceFile, autoCollapse = false, useFullStart = true): OutliningSpan { + const textSpan = createTextSpanFromBounds(useFullStart ? openToken.getFullStart() : openToken.getStart(sourceFile), closeToken.getEnd()); + return createOutliningSpan(textSpan, OutliningSpanKind.Code, createTextSpanFromNode(hintSpanNode, sourceFile), autoCollapse); +} +/* @internal */ +function createOutliningSpan(textSpan: TextSpan, kind: OutliningSpanKind, hintSpan: TextSpan = textSpan, autoCollapse = false, bannerText = "..."): OutliningSpan { + return { textSpan, kind, hintSpan, bannerText, autoCollapse }; +} diff --git a/src/services/patternMatcher.ts b/src/services/patternMatcher.ts index 254a684fc4975..c0f1de7b7d23e 100644 --- a/src/services/patternMatcher.ts +++ b/src/services/patternMatcher.ts @@ -1,593 +1,548 @@ +import { TextSpan, createMap, last, startsWith, CharacterCodes, min, Comparison, compareValues, compareBooleans, createTextSpan, isUnicodeIdentifierStart, ScriptTarget } from "./ts"; +import * as ts from "./ts"; /* @internal */ -namespace ts { - // Note(cyrusn): this enum is ordered from strongest match type to weakest match type. - export enum PatternMatchKind { - exact, - prefix, - substring, - camelCase - } - - // Information about a match made by the pattern matcher between a candidate and the - // search pattern. - export interface PatternMatch { - // What kind of match this was. Exact matches are better than prefix matches which are - // better than substring matches which are better than CamelCase matches. - kind: PatternMatchKind; - - // If this was a match where all constituent parts of the candidate and search pattern - // matched case sensitively or case insensitively. Case sensitive matches of the kind - // are better matches than insensitive matches. - isCaseSensitive: boolean; - } - - // The pattern matcher maintains an internal cache of information as it is used. Therefore, - // you should not keep it around forever and should get and release the matcher appropriately - // once you no longer need it. - export interface PatternMatcher { - // Used to match a candidate against the last segment of a possibly dotted pattern. This - // is useful as a quick check to prevent having to compute a container before calling - // "getMatches". - // - // For example, if the search pattern is "ts.c.SK" and the candidate is "SyntaxKind", then - // this will return a successful match, having only tested "SK" against "SyntaxKind". At - // that point a call can be made to 'getMatches("SyntaxKind", "ts.compiler")', with the - // work to create 'ts.compiler' only being done once the first match succeeded. - getMatchForLastSegmentOfPattern(candidate: string): PatternMatch | undefined; - - // Fully checks a candidate, with an dotted container, against the search pattern. - // The candidate must match the last part of the search pattern, and the dotted container - // must match the preceding segments of the pattern. - getFullMatch(candidateContainers: readonly string[], candidate: string): PatternMatch | undefined; - - // Whether or not the pattern contained dots or not. Clients can use this to determine - // If they should call getMatches, or if getMatchesForLastSegmentOfPattern is sufficient. - patternContainsDots: boolean; - } - - // First we break up the pattern given by dots. Each portion of the pattern between the - // dots is a 'Segment'. The 'Segment' contains information about the entire section of - // text between the dots, as well as information about any individual 'Words' that we - // can break the segment into. A 'Word' is simply a contiguous sequence of characters - // that can appear in a typescript identifier. So "GetKeyword" would be one word, while - // "Get Keyword" would be two words. Once we have the individual 'words', we break those - // into constituent 'character spans' of interest. For example, while 'UIElement' is one - // word, it make character spans corresponding to "U", "I" and "Element". These spans - // are then used when doing camel cased matches against candidate patterns. - interface Segment { - // Information about the entire piece of text between the dots. For example, if the - // text between the dots is 'GetKeyword', then TotalTextChunk.Text will be 'GetKeyword' and - // TotalTextChunk.CharacterSpans will correspond to 'Get', 'Keyword'. - totalTextChunk: TextChunk; - - // Information about the subwords compromising the total word. For example, if the - // text between the dots is 'GetFoo KeywordBar', then the subwords will be 'GetFoo' - // and 'KeywordBar'. Those individual words will have CharacterSpans of ('Get' and - // 'Foo') and('Keyword' and 'Bar') respectively. - subWordTextChunks: TextChunk[]; - } - - // Information about a chunk of text from the pattern. The chunk is a piece of text, with - // cached information about the character spans within in. Character spans are used for - // camel case matching. - interface TextChunk { - // The text of the chunk. This should be a contiguous sequence of character that could - // occur in a symbol name. - text: string; - - // The text of a chunk in lower case. Cached because it is needed often to check for - // case insensitive matches. - textLowerCase: string; - - // Whether or not this chunk is entirely lowercase. We have different rules when searching - // for something entirely lowercase or not. - isLowerCase: boolean; - - // The spans in this text chunk that we think are of interest and should be matched - // independently. For example, if the chunk is for "UIElement" the the spans of interest - // correspond to "U", "I" and "Element". If "UIElement" isn't found as an exact, prefix. - // or substring match, then the character spans will be used to attempt a camel case match. - characterSpans: TextSpan[]; - } - - function createPatternMatch(kind: PatternMatchKind, isCaseSensitive: boolean): PatternMatch { - return { - kind, - isCaseSensitive - }; - } - - export function createPatternMatcher(pattern: string): PatternMatcher | undefined { - // We'll often see the same candidate string many times when searching (For example, when - // we see the name of a module that is used everywhere, or the name of an overload). As - // such, we cache the information we compute about the candidate for the life of this - // pattern matcher so we don't have to compute it multiple times. - const stringToWordSpans = createMap(); - - const dotSeparatedSegments = pattern.trim().split(".").map(p => createSegment(p.trim())); - // A segment is considered invalid if we couldn't find any words in it. - if (dotSeparatedSegments.some(segment => !segment.subWordTextChunks.length)) return undefined; - - return { - getFullMatch: (containers, candidate) => getFullMatch(containers, candidate, dotSeparatedSegments, stringToWordSpans), - getMatchForLastSegmentOfPattern: candidate => matchSegment(candidate, last(dotSeparatedSegments), stringToWordSpans), - patternContainsDots: dotSeparatedSegments.length > 1 - }; +// Note(cyrusn): this enum is ordered from strongest match type to weakest match type. +export enum PatternMatchKind { + exact, + prefix, + substring, + camelCase +} +// Information about a match made by the pattern matcher between a candidate and the +// search pattern. +/* @internal */ +export interface PatternMatch { + // What kind of match this was. Exact matches are better than prefix matches which are + // better than substring matches which are better than CamelCase matches. + kind: PatternMatchKind; + // If this was a match where all constituent parts of the candidate and search pattern + // matched case sensitively or case insensitively. Case sensitive matches of the kind + // are better matches than insensitive matches. + isCaseSensitive: boolean; +} +// The pattern matcher maintains an internal cache of information as it is used. Therefore, +// you should not keep it around forever and should get and release the matcher appropriately +// once you no longer need it. +/* @internal */ +export interface PatternMatcher { + // Used to match a candidate against the last segment of a possibly dotted pattern. This + // is useful as a quick check to prevent having to compute a container before calling + // "getMatches". + // + // For example, if the search pattern is "ts.c.SK" and the candidate is "SyntaxKind", then + // this will return a successful match, having only tested "SK" against "SyntaxKind". At + // that point a call can be made to 'getMatches("SyntaxKind", "ts.compiler")', with the + // work to create 'ts.compiler' only being done once the first match succeeded. + getMatchForLastSegmentOfPattern(candidate: string): PatternMatch | undefined; + // Fully checks a candidate, with an dotted container, against the search pattern. + // The candidate must match the last part of the search pattern, and the dotted container + // must match the preceding segments of the pattern. + getFullMatch(candidateContainers: readonly string[], candidate: string): PatternMatch | undefined; + // Whether or not the pattern contained dots or not. Clients can use this to determine + // If they should call getMatches, or if getMatchesForLastSegmentOfPattern is sufficient. + patternContainsDots: boolean; +} +// First we break up the pattern given by dots. Each portion of the pattern between the +// dots is a 'Segment'. The 'Segment' contains information about the entire section of +// text between the dots, as well as information about any individual 'Words' that we +// can break the segment into. A 'Word' is simply a contiguous sequence of characters +// that can appear in a typescript identifier. So "GetKeyword" would be one word, while +// "Get Keyword" would be two words. Once we have the individual 'words', we break those +// into constituent 'character spans' of interest. For example, while 'UIElement' is one +// word, it make character spans corresponding to "U", "I" and "Element". These spans +// are then used when doing camel cased matches against candidate patterns. +/* @internal */ +interface Segment { + // Information about the entire piece of text between the dots. For example, if the + // text between the dots is 'GetKeyword', then TotalTextChunk.Text will be 'GetKeyword' and + // TotalTextChunk.CharacterSpans will correspond to 'Get', 'Keyword'. + totalTextChunk: TextChunk; + // Information about the subwords compromising the total word. For example, if the + // text between the dots is 'GetFoo KeywordBar', then the subwords will be 'GetFoo' + // and 'KeywordBar'. Those individual words will have CharacterSpans of ('Get' and + // 'Foo') and('Keyword' and 'Bar') respectively. + subWordTextChunks: TextChunk[]; +} +// Information about a chunk of text from the pattern. The chunk is a piece of text, with +// cached information about the character spans within in. Character spans are used for +// camel case matching. +/* @internal */ +interface TextChunk { + // The text of the chunk. This should be a contiguous sequence of character that could + // occur in a symbol name. + text: string; + // The text of a chunk in lower case. Cached because it is needed often to check for + // case insensitive matches. + textLowerCase: string; + // Whether or not this chunk is entirely lowercase. We have different rules when searching + // for something entirely lowercase or not. + isLowerCase: boolean; + // The spans in this text chunk that we think are of interest and should be matched + // independently. For example, if the chunk is for "UIElement" the the spans of interest + // correspond to "U", "I" and "Element". If "UIElement" isn't found as an exact, prefix. + // or substring match, then the character spans will be used to attempt a camel case match. + characterSpans: TextSpan[]; +} +/* @internal */ +function createPatternMatch(kind: PatternMatchKind, isCaseSensitive: boolean): PatternMatch { + return { + kind, + isCaseSensitive + }; +} +/* @internal */ +export function createPatternMatcher(pattern: string): PatternMatcher | undefined { + // We'll often see the same candidate string many times when searching (For example, when + // we see the name of a module that is used everywhere, or the name of an overload). As + // such, we cache the information we compute about the candidate for the life of this + // pattern matcher so we don't have to compute it multiple times. + const stringToWordSpans = createMap(); + const dotSeparatedSegments = pattern.trim().split(".").map(p => createSegment(p.trim())); + // A segment is considered invalid if we couldn't find any words in it. + if (dotSeparatedSegments.some(segment => !segment.subWordTextChunks.length)) + return undefined; + return { + getFullMatch: (containers, candidate) => getFullMatch(containers, candidate, dotSeparatedSegments, stringToWordSpans), + getMatchForLastSegmentOfPattern: candidate => matchSegment(candidate, last(dotSeparatedSegments), stringToWordSpans), + patternContainsDots: dotSeparatedSegments.length > 1 + }; +} +/* @internal */ +function getFullMatch(candidateContainers: readonly string[], candidate: string, dotSeparatedSegments: readonly Segment[], stringToWordSpans: ts.Map): PatternMatch | undefined { + // First, check that the last part of the dot separated pattern matches the name of the + // candidate. If not, then there's no point in proceeding and doing the more + // expensive work. + const candidateMatch = matchSegment(candidate, last(dotSeparatedSegments), stringToWordSpans); + if (!candidateMatch) { + return undefined; + } + // -1 because the last part was checked against the name, and only the rest + // of the parts are checked against the container. + if (dotSeparatedSegments.length - 1 > candidateContainers.length) { + // There weren't enough container parts to match against the pattern parts. + // So this definitely doesn't match. + return undefined; + } + let bestMatch: PatternMatch | undefined; + for (let i = dotSeparatedSegments.length - 2, j = candidateContainers.length - 1; i >= 0; i -= 1, j -= 1) { + bestMatch = betterMatch(bestMatch, matchSegment(candidateContainers[j], dotSeparatedSegments[i], stringToWordSpans)); + } + return bestMatch; +} +/* @internal */ +function getWordSpans(word: string, stringToWordSpans: ts.Map): TextSpan[] { + let spans = stringToWordSpans.get(word); + if (!spans) { + stringToWordSpans.set(word, spans = breakIntoWordSpans(word)); } - - function getFullMatch(candidateContainers: readonly string[], candidate: string, dotSeparatedSegments: readonly Segment[], stringToWordSpans: Map): PatternMatch | undefined { - // First, check that the last part of the dot separated pattern matches the name of the - // candidate. If not, then there's no point in proceeding and doing the more - // expensive work. - const candidateMatch = matchSegment(candidate, last(dotSeparatedSegments), stringToWordSpans); - if (!candidateMatch) { - return undefined; - } - - // -1 because the last part was checked against the name, and only the rest - // of the parts are checked against the container. - if (dotSeparatedSegments.length - 1 > candidateContainers.length) { - // There weren't enough container parts to match against the pattern parts. - // So this definitely doesn't match. + return spans; +} +/* @internal */ +function matchTextChunk(candidate: string, chunk: TextChunk, stringToWordSpans: ts.Map): PatternMatch | undefined { + const index = indexOfIgnoringCase(candidate, chunk.textLowerCase); + if (index === 0) { + // a) Check if the word is a prefix of the candidate, in a case insensitive or + // sensitive manner. If it does, return that there was an exact match if the word and candidate are the same length, else a prefix match. + return createPatternMatch(chunk.text.length === candidate.length ? PatternMatchKind.exact : PatternMatchKind.prefix, /*isCaseSensitive:*/ startsWith(candidate, chunk.text)); + } + if (chunk.isLowerCase) { + if (index === -1) return undefined; + // b) If the part is entirely lowercase, then check if it is contained anywhere in the + // candidate in a case insensitive manner. If so, return that there was a substring + // match. + // + // Note: We only have a substring match if the lowercase part is prefix match of some + // word part. That way we don't match something like 'Class' when the user types 'a'. + // But we would match 'FooAttribute' (since 'Attribute' starts with 'a'). + const wordSpans = getWordSpans(candidate, stringToWordSpans); + for (const span of wordSpans) { + if (partStartsWith(candidate, span, chunk.text, /*ignoreCase:*/ true)) { + return createPatternMatch(PatternMatchKind.substring, /*isCaseSensitive:*/ partStartsWith(candidate, span, chunk.text, /*ignoreCase:*/ false)); + } } - - let bestMatch: PatternMatch | undefined; - for (let i = dotSeparatedSegments.length - 2, j = candidateContainers.length - 1; - i >= 0; - i -= 1, j -= 1) { - bestMatch = betterMatch(bestMatch, matchSegment(candidateContainers[j], dotSeparatedSegments[i], stringToWordSpans)); - } - return bestMatch; - } - - function getWordSpans(word: string, stringToWordSpans: Map): TextSpan[] { - let spans = stringToWordSpans.get(word); - if (!spans) { - stringToWordSpans.set(word, spans = breakIntoWordSpans(word)); + // c) Is the pattern a substring of the candidate starting on one of the candidate's word boundaries? + // We could check every character boundary start of the candidate for the pattern. However, that's + // an m * n operation in the wost case. Instead, find the first instance of the pattern + // substring, and see if it starts on a capital letter. It seems unlikely that the user will try to + // filter the list based on a substring that starts on a capital letter and also with a lowercase one. + // (Pattern: fogbar, Candidate: quuxfogbarFogBar). + if (chunk.text.length < candidate.length && isUpperCaseLetter(candidate.charCodeAt(index))) { + return createPatternMatch(PatternMatchKind.substring, /*isCaseSensitive:*/ false); } - return spans; } - - function matchTextChunk(candidate: string, chunk: TextChunk, stringToWordSpans: Map): PatternMatch | undefined { - const index = indexOfIgnoringCase(candidate, chunk.textLowerCase); - if (index === 0) { - // a) Check if the word is a prefix of the candidate, in a case insensitive or - // sensitive manner. If it does, return that there was an exact match if the word and candidate are the same length, else a prefix match. - return createPatternMatch(chunk.text.length === candidate.length ? PatternMatchKind.exact : PatternMatchKind.prefix, /*isCaseSensitive:*/ startsWith(candidate, chunk.text)); + else { + // d) If the part was not entirely lowercase, then check if it is contained in the + // candidate in a case *sensitive* manner. If so, return that there was a substring + // match. + if (candidate.indexOf(chunk.text) > 0) { + return createPatternMatch(PatternMatchKind.substring, /*isCaseSensitive:*/ true); } - - if (chunk.isLowerCase) { - if (index === -1) return undefined; - // b) If the part is entirely lowercase, then check if it is contained anywhere in the - // candidate in a case insensitive manner. If so, return that there was a substring - // match. - // - // Note: We only have a substring match if the lowercase part is prefix match of some - // word part. That way we don't match something like 'Class' when the user types 'a'. - // But we would match 'FooAttribute' (since 'Attribute' starts with 'a'). - const wordSpans = getWordSpans(candidate, stringToWordSpans); - for (const span of wordSpans) { - if (partStartsWith(candidate, span, chunk.text, /*ignoreCase:*/ true)) { - return createPatternMatch(PatternMatchKind.substring, /*isCaseSensitive:*/ partStartsWith(candidate, span, chunk.text, /*ignoreCase:*/ false)); - } - } - // c) Is the pattern a substring of the candidate starting on one of the candidate's word boundaries? - // We could check every character boundary start of the candidate for the pattern. However, that's - // an m * n operation in the wost case. Instead, find the first instance of the pattern - // substring, and see if it starts on a capital letter. It seems unlikely that the user will try to - // filter the list based on a substring that starts on a capital letter and also with a lowercase one. - // (Pattern: fogbar, Candidate: quuxfogbarFogBar). - if (chunk.text.length < candidate.length && isUpperCaseLetter(candidate.charCodeAt(index))) { - return createPatternMatch(PatternMatchKind.substring, /*isCaseSensitive:*/ false); - } - } - else { - // d) If the part was not entirely lowercase, then check if it is contained in the - // candidate in a case *sensitive* manner. If so, return that there was a substring - // match. - if (candidate.indexOf(chunk.text) > 0) { - return createPatternMatch(PatternMatchKind.substring, /*isCaseSensitive:*/ true); - } - // e) If the part was not entirely lowercase, then attempt a camel cased match as well. - if (chunk.characterSpans.length > 0) { - const candidateParts = getWordSpans(candidate, stringToWordSpans); - const isCaseSensitive = tryCamelCaseMatch(candidate, candidateParts, chunk, /*ignoreCase:*/ false) ? true - : tryCamelCaseMatch(candidate, candidateParts, chunk, /*ignoreCase:*/ true) ? false : undefined; - if (isCaseSensitive !== undefined) { - return createPatternMatch(PatternMatchKind.camelCase, isCaseSensitive); - } + // e) If the part was not entirely lowercase, then attempt a camel cased match as well. + if (chunk.characterSpans.length > 0) { + const candidateParts = getWordSpans(candidate, stringToWordSpans); + const isCaseSensitive = tryCamelCaseMatch(candidate, candidateParts, chunk, /*ignoreCase:*/ false) ? true + : tryCamelCaseMatch(candidate, candidateParts, chunk, /*ignoreCase:*/ true) ? false : undefined; + if (isCaseSensitive !== undefined) { + return createPatternMatch(PatternMatchKind.camelCase, isCaseSensitive); } } } - - function matchSegment(candidate: string, segment: Segment, stringToWordSpans: Map): PatternMatch | undefined { - // First check if the segment matches as is. This is also useful if the segment contains - // characters we would normally strip when splitting into parts that we also may want to - // match in the candidate. For example if the segment is "@int" and the candidate is - // "@int", then that will show up as an exact match here. - // - // Note: if the segment contains a space or an asterisk then we must assume that it's a - // multi-word segment. - if (every(segment.totalTextChunk.text, ch => ch !== CharacterCodes.space && ch !== CharacterCodes.asterisk)) { - const match = matchTextChunk(candidate, segment.totalTextChunk, stringToWordSpans); - if (match) return match; +} +/* @internal */ +function matchSegment(candidate: string, segment: Segment, stringToWordSpans: ts.Map): PatternMatch | undefined { + // First check if the segment matches as is. This is also useful if the segment contains + // characters we would normally strip when splitting into parts that we also may want to + // match in the candidate. For example if the segment is "@int" and the candidate is + // "@int", then that will show up as an exact match here. + // + // Note: if the segment contains a space or an asterisk then we must assume that it's a + // multi-word segment. + if (every(segment.totalTextChunk.text, ch => ch !== CharacterCodes.space && ch !== CharacterCodes.asterisk)) { + const match = matchTextChunk(candidate, segment.totalTextChunk, stringToWordSpans); + if (match) + return match; + } + // The logic for pattern matching is now as follows: + // + // 1) Break the segment passed in into words. Breaking is rather simple and a + // good way to think about it that if gives you all the individual alphanumeric words + // of the pattern. + // + // 2) For each word try to match the word against the candidate value. + // + // 3) Matching is as follows: + // + // a) Check if the word is a prefix of the candidate, in a case insensitive or + // sensitive manner. If it does, return that there was an exact match if the word and candidate are the same length, else a prefix match. + // + // If the word is entirely lowercase: + // b) Then check if it is contained anywhere in the + // candidate in a case insensitive manner. If so, return that there was a substring + // match. + // + // Note: We only have a substring match if the lowercase part is prefix match of + // some word part. That way we don't match something like 'Class' when the user + // types 'a'. But we would match 'FooAttribute' (since 'Attribute' starts with + // 'a'). + // + // c) The word is all lower case. Is it a case insensitive substring of the candidate starting + // on a part boundary of the candidate? + // + // Else: + // d) If the word was not entirely lowercase, then check if it is contained in the + // candidate in a case *sensitive* manner. If so, return that there was a substring + // match. + // + // e) If the word was not entirely lowercase, then attempt a camel cased match as + // well. + // + // Only if all words have some sort of match is the pattern considered matched. + const subWordTextChunks = segment.subWordTextChunks; + let bestMatch: PatternMatch | undefined; + for (const subWordTextChunk of subWordTextChunks) { + bestMatch = betterMatch(bestMatch, matchTextChunk(candidate, subWordTextChunk, stringToWordSpans)); + } + return bestMatch; +} +/* @internal */ +function betterMatch(a: PatternMatch | undefined, b: PatternMatch | undefined): PatternMatch | undefined { + return min(a, b, compareMatches); +} +/* @internal */ +function compareMatches(a: PatternMatch | undefined, b: PatternMatch | undefined): Comparison { + return a === undefined ? Comparison.GreaterThan : b === undefined ? Comparison.LessThan + : compareValues(a.kind, b.kind) || compareBooleans(!a.isCaseSensitive, !b.isCaseSensitive); +} +/* @internal */ +function partStartsWith(candidate: string, candidateSpan: TextSpan, pattern: string, ignoreCase: boolean, patternSpan: TextSpan = { start: 0, length: pattern.length }): boolean { + return patternSpan.length <= candidateSpan.length // If pattern part is longer than the candidate part there can never be a match. + && everyInRange(0, patternSpan.length, i => equalChars(pattern.charCodeAt(patternSpan.start + i), candidate.charCodeAt(candidateSpan.start + i), ignoreCase)); +} +/* @internal */ +function equalChars(ch1: number, ch2: number, ignoreCase: boolean): boolean { + return ignoreCase ? toLowerCase(ch1) === toLowerCase(ch2) : ch1 === ch2; +} +/* @internal */ +function tryCamelCaseMatch(candidate: string, candidateParts: TextSpan[], chunk: TextChunk, ignoreCase: boolean): boolean { + const chunkCharacterSpans = chunk.characterSpans; + // Note: we may have more pattern parts than candidate parts. This is because multiple + // pattern parts may match a candidate part. For example "SiUI" against "SimpleUI". + // We'll have 3 pattern parts Si/U/I against two candidate parts Simple/UI. However, U + // and I will both match in UI. + let currentCandidate = 0; + let currentChunkSpan = 0; + let firstMatch: number | undefined; + let contiguous: boolean | undefined; + while (true) { + // Let's consider our termination cases + if (currentChunkSpan === chunkCharacterSpans.length) { + return true; } - - // The logic for pattern matching is now as follows: - // - // 1) Break the segment passed in into words. Breaking is rather simple and a - // good way to think about it that if gives you all the individual alphanumeric words - // of the pattern. - // - // 2) For each word try to match the word against the candidate value. - // - // 3) Matching is as follows: - // - // a) Check if the word is a prefix of the candidate, in a case insensitive or - // sensitive manner. If it does, return that there was an exact match if the word and candidate are the same length, else a prefix match. - // - // If the word is entirely lowercase: - // b) Then check if it is contained anywhere in the - // candidate in a case insensitive manner. If so, return that there was a substring - // match. - // - // Note: We only have a substring match if the lowercase part is prefix match of - // some word part. That way we don't match something like 'Class' when the user - // types 'a'. But we would match 'FooAttribute' (since 'Attribute' starts with - // 'a'). - // - // c) The word is all lower case. Is it a case insensitive substring of the candidate starting - // on a part boundary of the candidate? - // - // Else: - // d) If the word was not entirely lowercase, then check if it is contained in the - // candidate in a case *sensitive* manner. If so, return that there was a substring - // match. - // - // e) If the word was not entirely lowercase, then attempt a camel cased match as - // well. - // - // Only if all words have some sort of match is the pattern considered matched. - - const subWordTextChunks = segment.subWordTextChunks; - let bestMatch: PatternMatch | undefined; - for (const subWordTextChunk of subWordTextChunks) { - bestMatch = betterMatch(bestMatch, matchTextChunk(candidate, subWordTextChunk, stringToWordSpans)); + else if (currentCandidate === candidateParts.length) { + // No match, since we still have more of the pattern to hit + return false; } - return bestMatch; - } - - function betterMatch(a: PatternMatch | undefined, b: PatternMatch | undefined): PatternMatch | undefined { - return min(a, b, compareMatches); - } - function compareMatches(a: PatternMatch | undefined, b: PatternMatch | undefined): Comparison { - return a === undefined ? Comparison.GreaterThan : b === undefined ? Comparison.LessThan - : compareValues(a.kind, b.kind) || compareBooleans(!a.isCaseSensitive, !b.isCaseSensitive); - } - - function partStartsWith(candidate: string, candidateSpan: TextSpan, pattern: string, ignoreCase: boolean, patternSpan: TextSpan = { start: 0, length: pattern.length }): boolean { - return patternSpan.length <= candidateSpan.length // If pattern part is longer than the candidate part there can never be a match. - && everyInRange(0, patternSpan.length, i => equalChars(pattern.charCodeAt(patternSpan.start + i), candidate.charCodeAt(candidateSpan.start + i), ignoreCase)); - } - - function equalChars(ch1: number, ch2: number, ignoreCase: boolean): boolean { - return ignoreCase ? toLowerCase(ch1) === toLowerCase(ch2) : ch1 === ch2; - } - - function tryCamelCaseMatch(candidate: string, candidateParts: TextSpan[], chunk: TextChunk, ignoreCase: boolean): boolean { - const chunkCharacterSpans = chunk.characterSpans; - - // Note: we may have more pattern parts than candidate parts. This is because multiple - // pattern parts may match a candidate part. For example "SiUI" against "SimpleUI". - // We'll have 3 pattern parts Si/U/I against two candidate parts Simple/UI. However, U - // and I will both match in UI. - - let currentCandidate = 0; - let currentChunkSpan = 0; - let firstMatch: number | undefined; - let contiguous: boolean | undefined; - - while (true) { - // Let's consider our termination cases - if (currentChunkSpan === chunkCharacterSpans.length) { - return true; - } - else if (currentCandidate === candidateParts.length) { - // No match, since we still have more of the pattern to hit - return false; - } - - let candidatePart = candidateParts[currentCandidate]; - let gotOneMatchThisCandidate = false; - - // Consider the case of matching SiUI against SimpleUIElement. The candidate parts - // will be Simple/UI/Element, and the pattern parts will be Si/U/I. We'll match 'Si' - // against 'Simple' first. Then we'll match 'U' against 'UI'. However, we want to - // still keep matching pattern parts against that candidate part. - for (; currentChunkSpan < chunkCharacterSpans.length; currentChunkSpan++) { - const chunkCharacterSpan = chunkCharacterSpans[currentChunkSpan]; - - if (gotOneMatchThisCandidate) { - // We've already gotten one pattern part match in this candidate. We will - // only continue trying to consumer pattern parts if the last part and this - // part are both upper case. - if (!isUpperCaseLetter(chunk.text.charCodeAt(chunkCharacterSpans[currentChunkSpan - 1].start)) || - !isUpperCaseLetter(chunk.text.charCodeAt(chunkCharacterSpans[currentChunkSpan].start))) { - break; - } - } - - if (!partStartsWith(candidate, candidatePart, chunk.text, ignoreCase, chunkCharacterSpan)) { + let candidatePart = candidateParts[currentCandidate]; + let gotOneMatchThisCandidate = false; + // Consider the case of matching SiUI against SimpleUIElement. The candidate parts + // will be Simple/UI/Element, and the pattern parts will be Si/U/I. We'll match 'Si' + // against 'Simple' first. Then we'll match 'U' against 'UI'. However, we want to + // still keep matching pattern parts against that candidate part. + for (; currentChunkSpan < chunkCharacterSpans.length; currentChunkSpan++) { + const chunkCharacterSpan = chunkCharacterSpans[currentChunkSpan]; + if (gotOneMatchThisCandidate) { + // We've already gotten one pattern part match in this candidate. We will + // only continue trying to consumer pattern parts if the last part and this + // part are both upper case. + if (!isUpperCaseLetter(chunk.text.charCodeAt(chunkCharacterSpans[currentChunkSpan - 1].start)) || + !isUpperCaseLetter(chunk.text.charCodeAt(chunkCharacterSpans[currentChunkSpan].start))) { break; } - - gotOneMatchThisCandidate = true; - - firstMatch = firstMatch === undefined ? currentCandidate : firstMatch; - - // If we were contiguous, then keep that value. If we weren't, then keep that - // value. If we don't know, then set the value to 'true' as an initial match is - // obviously contiguous. - contiguous = contiguous === undefined ? true : contiguous; - - candidatePart = createTextSpan(candidatePart.start + chunkCharacterSpan.length, candidatePart.length - chunkCharacterSpan.length); } - - // Check if we matched anything at all. If we didn't, then we need to unset the - // contiguous bit if we currently had it set. - // If we haven't set the bit yet, then that means we haven't matched anything so - // far, and we don't want to change that. - if (!gotOneMatchThisCandidate && contiguous !== undefined) { - contiguous = false; + if (!partStartsWith(candidate, candidatePart, chunk.text, ignoreCase, chunkCharacterSpan)) { + break; } - - // Move onto the next candidate. - currentCandidate++; + gotOneMatchThisCandidate = true; + firstMatch = firstMatch === undefined ? currentCandidate : firstMatch; + // If we were contiguous, then keep that value. If we weren't, then keep that + // value. If we don't know, then set the value to 'true' as an initial match is + // obviously contiguous. + contiguous = contiguous === undefined ? true : contiguous; + candidatePart = createTextSpan(candidatePart.start + chunkCharacterSpan.length, candidatePart.length - chunkCharacterSpan.length); } - } - - function createSegment(text: string): Segment { - return { - totalTextChunk: createTextChunk(text), - subWordTextChunks: breakPatternIntoTextChunks(text) - }; - } - - function isUpperCaseLetter(ch: number) { - // Fast check for the ascii range. - if (ch >= CharacterCodes.A && ch <= CharacterCodes.Z) { - return true; + // Check if we matched anything at all. If we didn't, then we need to unset the + // contiguous bit if we currently had it set. + // If we haven't set the bit yet, then that means we haven't matched anything so + // far, and we don't want to change that. + if (!gotOneMatchThisCandidate && contiguous !== undefined) { + contiguous = false; } - - if (ch < CharacterCodes.maxAsciiCharacter || !isUnicodeIdentifierStart(ch, ScriptTarget.Latest)) { - return false; - } - - // TODO: find a way to determine this for any unicode characters in a - // non-allocating manner. - const str = String.fromCharCode(ch); - return str === str.toUpperCase(); - } - - function isLowerCaseLetter(ch: number) { - // Fast check for the ascii range. - if (ch >= CharacterCodes.a && ch <= CharacterCodes.z) { - return true; - } - - if (ch < CharacterCodes.maxAsciiCharacter || !isUnicodeIdentifierStart(ch, ScriptTarget.Latest)) { - return false; - } - - - // TODO: find a way to determine this for any unicode characters in a - // non-allocating manner. - const str = String.fromCharCode(ch); - return str === str.toLowerCase(); + // Move onto the next candidate. + currentCandidate++; } - - // Assumes 'value' is already lowercase. - function indexOfIgnoringCase(str: string, value: string): number { - const n = str.length - value.length; - for (let start = 0; start <= n; start++) { - if (every(value, (valueChar, i) => toLowerCase(str.charCodeAt(i + start)) === valueChar)) { - return start; - } - } - - return -1; +} +/* @internal */ +function createSegment(text: string): Segment { + return { + totalTextChunk: createTextChunk(text), + subWordTextChunks: breakPatternIntoTextChunks(text) + }; +} +/* @internal */ +function isUpperCaseLetter(ch: number) { + // Fast check for the ascii range. + if (ch >= CharacterCodes.A && ch <= CharacterCodes.Z) { + return true; } - - function toLowerCase(ch: number): number { - // Fast convert for the ascii range. - if (ch >= CharacterCodes.A && ch <= CharacterCodes.Z) { - return CharacterCodes.a + (ch - CharacterCodes.A); - } - - if (ch < CharacterCodes.maxAsciiCharacter) { - return ch; - } - - // TODO: find a way to compute this for any unicode characters in a - // non-allocating manner. - return String.fromCharCode(ch).toLowerCase().charCodeAt(0); + if (ch < CharacterCodes.maxAsciiCharacter || !isUnicodeIdentifierStart(ch, ScriptTarget.Latest)) { + return false; } - - function isDigit(ch: number) { - // TODO(cyrusn): Find a way to support this for unicode digits. - return ch >= CharacterCodes._0 && ch <= CharacterCodes._9; + // TODO: find a way to determine this for any unicode characters in a + // non-allocating manner. + const str = String.fromCharCode(ch); + return str === str.toUpperCase(); +} +/* @internal */ +function isLowerCaseLetter(ch: number) { + // Fast check for the ascii range. + if (ch >= CharacterCodes.a && ch <= CharacterCodes.z) { + return true; } - - function isWordChar(ch: number) { - return isUpperCaseLetter(ch) || isLowerCaseLetter(ch) || isDigit(ch) || ch === CharacterCodes._ || ch === CharacterCodes.$; + if (ch < CharacterCodes.maxAsciiCharacter || !isUnicodeIdentifierStart(ch, ScriptTarget.Latest)) { + return false; } - - function breakPatternIntoTextChunks(pattern: string): TextChunk[] { - const result: TextChunk[] = []; - let wordStart = 0; - let wordLength = 0; - - for (let i = 0; i < pattern.length; i++) { - const ch = pattern.charCodeAt(i); - if (isWordChar(ch)) { - if (wordLength === 0) { - wordStart = i; - } - wordLength++; - } - else { - if (wordLength > 0) { - result.push(createTextChunk(pattern.substr(wordStart, wordLength))); - wordLength = 0; - } - } - } - - if (wordLength > 0) { - result.push(createTextChunk(pattern.substr(wordStart, wordLength))); + // TODO: find a way to determine this for any unicode characters in a + // non-allocating manner. + const str = String.fromCharCode(ch); + return str === str.toLowerCase(); +} +// Assumes 'value' is already lowercase. +/* @internal */ +function indexOfIgnoringCase(str: string, value: string): number { + const n = str.length - value.length; + for (let start = 0; start <= n; start++) { + if (every(value, (valueChar, i) => toLowerCase(str.charCodeAt(i + start)) === valueChar)) { + return start; } - - return result; - } - - function createTextChunk(text: string): TextChunk { - const textLowerCase = text.toLowerCase(); - return { - text, - textLowerCase, - isLowerCase: text === textLowerCase, - characterSpans: breakIntoCharacterSpans(text) - }; } - - export function breakIntoCharacterSpans(identifier: string): TextSpan[] { - return breakIntoSpans(identifier, /*word:*/ false); + return -1; +} +/* @internal */ +function toLowerCase(ch: number): number { + // Fast convert for the ascii range. + if (ch >= CharacterCodes.A && ch <= CharacterCodes.Z) { + return CharacterCodes.a + (ch - CharacterCodes.A); } - - export function breakIntoWordSpans(identifier: string): TextSpan[] { - return breakIntoSpans(identifier, /*word:*/ true); + if (ch < CharacterCodes.maxAsciiCharacter) { + return ch; } - - function breakIntoSpans(identifier: string, word: boolean): TextSpan[] { - const result: TextSpan[] = []; - - let wordStart = 0; - for (let i = 1; i < identifier.length; i++) { - const lastIsDigit = isDigit(identifier.charCodeAt(i - 1)); - const currentIsDigit = isDigit(identifier.charCodeAt(i)); - - const hasTransitionFromLowerToUpper = transitionFromLowerToUpper(identifier, word, i); - const hasTransitionFromUpperToLower = word && transitionFromUpperToLower(identifier, i, wordStart); - - if (charIsPunctuation(identifier.charCodeAt(i - 1)) || - charIsPunctuation(identifier.charCodeAt(i)) || - lastIsDigit !== currentIsDigit || - hasTransitionFromLowerToUpper || - hasTransitionFromUpperToLower) { - - if (!isAllPunctuation(identifier, wordStart, i)) { - result.push(createTextSpan(wordStart, i - wordStart)); - } - + // TODO: find a way to compute this for any unicode characters in a + // non-allocating manner. + return String.fromCharCode(ch).toLowerCase().charCodeAt(0); +} +/* @internal */ +function isDigit(ch: number) { + // TODO(cyrusn): Find a way to support this for unicode digits. + return ch >= CharacterCodes._0 && ch <= CharacterCodes._9; +} +/* @internal */ +function isWordChar(ch: number) { + return isUpperCaseLetter(ch) || isLowerCaseLetter(ch) || isDigit(ch) || ch === CharacterCodes._ || ch === CharacterCodes.$; +} +/* @internal */ +function breakPatternIntoTextChunks(pattern: string): TextChunk[] { + const result: TextChunk[] = []; + let wordStart = 0; + let wordLength = 0; + for (let i = 0; i < pattern.length; i++) { + const ch = pattern.charCodeAt(i); + if (isWordChar(ch)) { + if (wordLength === 0) { wordStart = i; } + wordLength++; } - - if (!isAllPunctuation(identifier, wordStart, identifier.length)) { - result.push(createTextSpan(wordStart, identifier.length - wordStart)); + else { + if (wordLength > 0) { + result.push(createTextChunk(pattern.substr(wordStart, wordLength))); + wordLength = 0; + } } - - return result; } - - function charIsPunctuation(ch: number) { - switch (ch) { - case CharacterCodes.exclamation: - case CharacterCodes.doubleQuote: - case CharacterCodes.hash: - case CharacterCodes.percent: - case CharacterCodes.ampersand: - case CharacterCodes.singleQuote: - case CharacterCodes.openParen: - case CharacterCodes.closeParen: - case CharacterCodes.asterisk: - case CharacterCodes.comma: - case CharacterCodes.minus: - case CharacterCodes.dot: - case CharacterCodes.slash: - case CharacterCodes.colon: - case CharacterCodes.semicolon: - case CharacterCodes.question: - case CharacterCodes.at: - case CharacterCodes.openBracket: - case CharacterCodes.backslash: - case CharacterCodes.closeBracket: - case CharacterCodes._: - case CharacterCodes.openBrace: - case CharacterCodes.closeBrace: - return true; - } - - return false; + if (wordLength > 0) { + result.push(createTextChunk(pattern.substr(wordStart, wordLength))); } - - function isAllPunctuation(identifier: string, start: number, end: number): boolean { - return every(identifier, ch => charIsPunctuation(ch) && ch !== CharacterCodes._, start, end); + return result; +} +/* @internal */ +function createTextChunk(text: string): TextChunk { + const textLowerCase = text.toLowerCase(); + return { + text, + textLowerCase, + isLowerCase: text === textLowerCase, + characterSpans: breakIntoCharacterSpans(text) + }; +} +/* @internal */ +export function breakIntoCharacterSpans(identifier: string): TextSpan[] { + return breakIntoSpans(identifier, /*word:*/ false); +} +/* @internal */ +export function breakIntoWordSpans(identifier: string): TextSpan[] { + return breakIntoSpans(identifier, /*word:*/ true); +} +/* @internal */ +function breakIntoSpans(identifier: string, word: boolean): TextSpan[] { + const result: TextSpan[] = []; + let wordStart = 0; + for (let i = 1; i < identifier.length; i++) { + const lastIsDigit = isDigit(identifier.charCodeAt(i - 1)); + const currentIsDigit = isDigit(identifier.charCodeAt(i)); + const hasTransitionFromLowerToUpper = transitionFromLowerToUpper(identifier, word, i); + const hasTransitionFromUpperToLower = word && transitionFromUpperToLower(identifier, i, wordStart); + if (charIsPunctuation(identifier.charCodeAt(i - 1)) || + charIsPunctuation(identifier.charCodeAt(i)) || + lastIsDigit !== currentIsDigit || + hasTransitionFromLowerToUpper || + hasTransitionFromUpperToLower) { + if (!isAllPunctuation(identifier, wordStart, i)) { + result.push(createTextSpan(wordStart, i - wordStart)); + } + wordStart = i; + } } - - function transitionFromUpperToLower(identifier: string, index: number, wordStart: number): boolean { - // Cases this supports: - // 1) IDisposable -> I, Disposable - // 2) UIElement -> UI, Element - // 3) HTMLDocument -> HTML, Document - // - // etc. - // We have a transition from an upper to a lower letter here. But we only - // want to break if all the letters that preceded are uppercase. i.e. if we - // have "Foo" we don't want to break that into "F, oo". But if we have - // "IFoo" or "UIFoo", then we want to break that into "I, Foo" and "UI, - // Foo". i.e. the last uppercase letter belongs to the lowercase letters - // that follows. Note: this will make the following not split properly: - // "HELLOthere". However, these sorts of names do not show up in .Net - // programs. - return index !== wordStart - && index + 1 < identifier.length - && isUpperCaseLetter(identifier.charCodeAt(index)) - && isLowerCaseLetter(identifier.charCodeAt(index + 1)) - && every(identifier, isUpperCaseLetter, wordStart, index); + if (!isAllPunctuation(identifier, wordStart, identifier.length)) { + result.push(createTextSpan(wordStart, identifier.length - wordStart)); } - - function transitionFromLowerToUpper(identifier: string, word: boolean, index: number): boolean { - const lastIsUpper = isUpperCaseLetter(identifier.charCodeAt(index - 1)); - const currentIsUpper = isUpperCaseLetter(identifier.charCodeAt(index)); - - // See if the casing indicates we're starting a new word. Note: if we're breaking on - // words, then just seeing an upper case character isn't enough. Instead, it has to - // be uppercase and the previous character can't be uppercase. - // - // For example, breaking "AddMetadata" on words would make: Add Metadata - // - // on characters would be: A dd M etadata - // - // Break "AM" on words would be: AM - // - // on characters would be: A M - // - // We break the search string on characters. But we break the symbol name on words. - return currentIsUpper && (!word || !lastIsUpper); + return result; +} +/* @internal */ +function charIsPunctuation(ch: number) { + switch (ch) { + case CharacterCodes.exclamation: + case CharacterCodes.doubleQuote: + case CharacterCodes.hash: + case CharacterCodes.percent: + case CharacterCodes.ampersand: + case CharacterCodes.singleQuote: + case CharacterCodes.openParen: + case CharacterCodes.closeParen: + case CharacterCodes.asterisk: + case CharacterCodes.comma: + case CharacterCodes.minus: + case CharacterCodes.dot: + case CharacterCodes.slash: + case CharacterCodes.colon: + case CharacterCodes.semicolon: + case CharacterCodes.question: + case CharacterCodes.at: + case CharacterCodes.openBracket: + case CharacterCodes.backslash: + case CharacterCodes.closeBracket: + case CharacterCodes._: + case CharacterCodes.openBrace: + case CharacterCodes.closeBrace: + return true; } - - function everyInRange(start: number, end: number, pred: (n: number) => boolean): boolean { - for (let i = start; i < end; i++) { - if (!pred(i)) { - return false; - } + return false; +} +/* @internal */ +function isAllPunctuation(identifier: string, start: number, end: number): boolean { + return every(identifier, ch => charIsPunctuation(ch) && ch !== CharacterCodes._, start, end); +} +/* @internal */ +function transitionFromUpperToLower(identifier: string, index: number, wordStart: number): boolean { + // Cases this supports: + // 1) IDisposable -> I, Disposable + // 2) UIElement -> UI, Element + // 3) HTMLDocument -> HTML, Document + // + // etc. + // We have a transition from an upper to a lower letter here. But we only + // want to break if all the letters that preceded are uppercase. i.e. if we + // have "Foo" we don't want to break that into "F, oo". But if we have + // "IFoo" or "UIFoo", then we want to break that into "I, Foo" and "UI, + // Foo". i.e. the last uppercase letter belongs to the lowercase letters + // that follows. Note: this will make the following not split properly: + // "HELLOthere". However, these sorts of names do not show up in .Net + // programs. + return index !== wordStart + && index + 1 < identifier.length + && isUpperCaseLetter(identifier.charCodeAt(index)) + && isLowerCaseLetter(identifier.charCodeAt(index + 1)) + && every(identifier, isUpperCaseLetter, wordStart, index); +} +/* @internal */ +function transitionFromLowerToUpper(identifier: string, word: boolean, index: number): boolean { + const lastIsUpper = isUpperCaseLetter(identifier.charCodeAt(index - 1)); + const currentIsUpper = isUpperCaseLetter(identifier.charCodeAt(index)); + // See if the casing indicates we're starting a new word. Note: if we're breaking on + // words, then just seeing an upper case character isn't enough. Instead, it has to + // be uppercase and the previous character can't be uppercase. + // + // For example, breaking "AddMetadata" on words would make: Add Metadata + // + // on characters would be: A dd M etadata + // + // Break "AM" on words would be: AM + // + // on characters would be: A M + // + // We break the search string on characters. But we break the symbol name on words. + return currentIsUpper && (!word || !lastIsUpper); +} +/* @internal */ +function everyInRange(start: number, end: number, pred: (n: number) => boolean): boolean { + for (let i = start; i < end; i++) { + if (!pred(i)) { + return false; } - return true; - } - - function every(s: string, pred: (ch: number, index: number) => boolean, start = 0, end = s.length): boolean { - return everyInRange(start, end, i => pred(s.charCodeAt(i), i)); } + return true; +} +/* @internal */ +function every(s: string, pred: (ch: number, index: number) => boolean, start = 0, end = s.length): boolean { + return everyInRange(start, end, i => pred(s.charCodeAt(i), i)); } diff --git a/src/services/preProcess.ts b/src/services/preProcess.ts index fc22b274ddbbd..98daf12bbfd04 100644 --- a/src/services/preProcess.ts +++ b/src/services/preProcess.ts @@ -1,196 +1,138 @@ -namespace ts { - export function preProcessFile(sourceText: string, readImportFiles = true, detectJavaScriptImports = false): PreProcessedFileInfo { - const pragmaContext: PragmaContext = { - languageVersion: ScriptTarget.ES5, // controls whether the token scanner considers unicode identifiers or not - shouldn't matter, since we're only using it for trivia - pragmas: undefined, - checkJsDirective: undefined, - referencedFiles: [], - typeReferenceDirectives: [], - libReferenceDirectives: [], - amdDependencies: [], - hasNoDefaultLib: undefined, - moduleName: undefined - }; - const importedFiles: FileReference[] = []; - let ambientExternalModules: { ref: FileReference, depth: number }[] | undefined; - let lastToken: SyntaxKind; - let currentToken: SyntaxKind; - let braceNesting = 0; - // assume that text represent an external module if it contains at least one top level import/export - // ambient modules that are found inside external modules are interpreted as module augmentations - let externalModule = false; - - function nextToken() { - lastToken = currentToken; - currentToken = scanner.scan(); - if (currentToken === SyntaxKind.OpenBraceToken) { - braceNesting++; - } - else if (currentToken === SyntaxKind.CloseBraceToken) { - braceNesting--; - } - return currentToken; +import { PreProcessedFileInfo, PragmaContext, ScriptTarget, FileReference, SyntaxKind, scanner, isKeyword, processCommentPragmas, processPragmasIntoFields, noop } from "./ts"; +export function preProcessFile(sourceText: string, readImportFiles = true, detectJavaScriptImports = false): PreProcessedFileInfo { + const pragmaContext: PragmaContext = { + languageVersion: ScriptTarget.ES5, + pragmas: undefined, + checkJsDirective: undefined, + referencedFiles: [], + typeReferenceDirectives: [], + libReferenceDirectives: [], + amdDependencies: [], + hasNoDefaultLib: undefined, + moduleName: undefined + }; + const importedFiles: FileReference[] = []; + let ambientExternalModules: { + ref: FileReference; + depth: number; + }[] | undefined; + let lastToken: SyntaxKind; + let currentToken: SyntaxKind; + let braceNesting = 0; + // assume that text represent an external module if it contains at least one top level import/export + // ambient modules that are found inside external modules are interpreted as module augmentations + let externalModule = false; + function nextToken() { + lastToken = currentToken; + currentToken = scanner.scan(); + if (currentToken === SyntaxKind.OpenBraceToken) { + braceNesting++; } - - function getFileReference() { - const fileName = scanner.getTokenValue(); - const pos = scanner.getTokenPos(); - return { fileName, pos, end: pos + fileName.length }; + else if (currentToken === SyntaxKind.CloseBraceToken) { + braceNesting--; } - - function recordAmbientExternalModule(): void { - if (!ambientExternalModules) { - ambientExternalModules = []; - } - ambientExternalModules.push({ ref: getFileReference(), depth: braceNesting }); - } - - function recordModuleName() { - importedFiles.push(getFileReference()); - - markAsExternalModuleIfTopLevel(); + return currentToken; + } + function getFileReference() { + const fileName = scanner.getTokenValue(); + const pos = scanner.getTokenPos(); + return { fileName, pos, end: pos + fileName.length }; + } + function recordAmbientExternalModule(): void { + if (!ambientExternalModules) { + ambientExternalModules = []; } - - function markAsExternalModuleIfTopLevel() { - if (braceNesting === 0) { - externalModule = true; - } + ambientExternalModules.push({ ref: getFileReference(), depth: braceNesting }); + } + function recordModuleName() { + importedFiles.push(getFileReference()); + markAsExternalModuleIfTopLevel(); + } + function markAsExternalModuleIfTopLevel() { + if (braceNesting === 0) { + externalModule = true; } - - /** - * Returns true if at least one token was consumed from the stream - */ - function tryConsumeDeclare(): boolean { - let token = scanner.getToken(); - if (token === SyntaxKind.DeclareKeyword) { - // declare module "mod" + } + /** + * Returns true if at least one token was consumed from the stream + */ + function tryConsumeDeclare(): boolean { + let token = scanner.getToken(); + if (token === SyntaxKind.DeclareKeyword) { + // declare module "mod" + token = nextToken(); + if (token === SyntaxKind.ModuleKeyword) { token = nextToken(); - if (token === SyntaxKind.ModuleKeyword) { - token = nextToken(); - if (token === SyntaxKind.StringLiteral) { - recordAmbientExternalModule(); - } + if (token === SyntaxKind.StringLiteral) { + recordAmbientExternalModule(); } - return true; } - + return true; + } + return false; + } + /** + * Returns true if at least one token was consumed from the stream + */ + function tryConsumeImport(): boolean { + if (lastToken === SyntaxKind.DotToken) { return false; } - - /** - * Returns true if at least one token was consumed from the stream - */ - function tryConsumeImport(): boolean { - if (lastToken === SyntaxKind.DotToken) { - return false; - } - let token = scanner.getToken(); - if (token === SyntaxKind.ImportKeyword) { + let token = scanner.getToken(); + if (token === SyntaxKind.ImportKeyword) { + token = nextToken(); + if (token === SyntaxKind.OpenParenToken) { token = nextToken(); - if (token === SyntaxKind.OpenParenToken) { - token = nextToken(); - if (token === SyntaxKind.StringLiteral) { - // import("mod"); - recordModuleName(); - return true; - } - } - else if (token === SyntaxKind.StringLiteral) { - // import "mod"; + if (token === SyntaxKind.StringLiteral) { + // import("mod"); recordModuleName(); return true; } - else { - if (token === SyntaxKind.Identifier || isKeyword(token)) { + } + else if (token === SyntaxKind.StringLiteral) { + // import "mod"; + recordModuleName(); + return true; + } + else { + if (token === SyntaxKind.Identifier || isKeyword(token)) { + token = nextToken(); + if (token === SyntaxKind.FromKeyword) { token = nextToken(); - if (token === SyntaxKind.FromKeyword) { - token = nextToken(); - if (token === SyntaxKind.StringLiteral) { - // import d from "mod"; - recordModuleName(); - return true; - } - } - else if (token === SyntaxKind.EqualsToken) { - if (tryConsumeRequireCall(/*skipCurrentToken*/ true)) { - return true; - } - } - else if (token === SyntaxKind.CommaToken) { - // consume comma and keep going - token = nextToken(); - } - else { - // unknown syntax + if (token === SyntaxKind.StringLiteral) { + // import d from "mod"; + recordModuleName(); return true; } } - - if (token === SyntaxKind.OpenBraceToken) { - token = nextToken(); - // consume "{ a as B, c, d as D}" clauses - // make sure that it stops on EOF - while (token !== SyntaxKind.CloseBraceToken && token !== SyntaxKind.EndOfFileToken) { - token = nextToken(); - } - - if (token === SyntaxKind.CloseBraceToken) { - token = nextToken(); - if (token === SyntaxKind.FromKeyword) { - token = nextToken(); - if (token === SyntaxKind.StringLiteral) { - // import {a as A} from "mod"; - // import d, {a, b as B} from "mod" - recordModuleName(); - } - } + else if (token === SyntaxKind.EqualsToken) { + if (tryConsumeRequireCall(/*skipCurrentToken*/ true)) { + return true; } } - else if (token === SyntaxKind.AsteriskToken) { + else if (token === SyntaxKind.CommaToken) { + // consume comma and keep going token = nextToken(); - if (token === SyntaxKind.AsKeyword) { - token = nextToken(); - if (token === SyntaxKind.Identifier || isKeyword(token)) { - token = nextToken(); - if (token === SyntaxKind.FromKeyword) { - token = nextToken(); - if (token === SyntaxKind.StringLiteral) { - // import * as NS from "mod" - // import d, * as NS from "mod" - recordModuleName(); - } - } - } - } + } + else { + // unknown syntax + return true; } } - - return true; - } - - return false; - } - - function tryConsumeExport(): boolean { - let token = scanner.getToken(); - if (token === SyntaxKind.ExportKeyword) { - markAsExternalModuleIfTopLevel(); - token = nextToken(); if (token === SyntaxKind.OpenBraceToken) { token = nextToken(); // consume "{ a as B, c, d as D}" clauses - // make sure it stops on EOF + // make sure that it stops on EOF while (token !== SyntaxKind.CloseBraceToken && token !== SyntaxKind.EndOfFileToken) { token = nextToken(); } - if (token === SyntaxKind.CloseBraceToken) { token = nextToken(); if (token === SyntaxKind.FromKeyword) { token = nextToken(); if (token === SyntaxKind.StringLiteral) { - // export {a as A} from "mod"; - // export {a, b as B} from "mod" + // import {a as A} from "mod"; + // import d, {a, b as B} from "mod" recordModuleName(); } } @@ -198,164 +140,193 @@ namespace ts { } else if (token === SyntaxKind.AsteriskToken) { token = nextToken(); - if (token === SyntaxKind.FromKeyword) { + if (token === SyntaxKind.AsKeyword) { token = nextToken(); - if (token === SyntaxKind.StringLiteral) { - // export * from "mod" - recordModuleName(); + if (token === SyntaxKind.Identifier || isKeyword(token)) { + token = nextToken(); + if (token === SyntaxKind.FromKeyword) { + token = nextToken(); + if (token === SyntaxKind.StringLiteral) { + // import * as NS from "mod" + // import d, * as NS from "mod" + recordModuleName(); + } + } } } } - else if (token === SyntaxKind.ImportKeyword) { + } + return true; + } + return false; + } + function tryConsumeExport(): boolean { + let token = scanner.getToken(); + if (token === SyntaxKind.ExportKeyword) { + markAsExternalModuleIfTopLevel(); + token = nextToken(); + if (token === SyntaxKind.OpenBraceToken) { + token = nextToken(); + // consume "{ a as B, c, d as D}" clauses + // make sure it stops on EOF + while (token !== SyntaxKind.CloseBraceToken && token !== SyntaxKind.EndOfFileToken) { token = nextToken(); - if (token === SyntaxKind.Identifier || isKeyword(token)) { + } + if (token === SyntaxKind.CloseBraceToken) { + token = nextToken(); + if (token === SyntaxKind.FromKeyword) { token = nextToken(); - if (token === SyntaxKind.EqualsToken) { - if (tryConsumeRequireCall(/*skipCurrentToken*/ true)) { - return true; - } + if (token === SyntaxKind.StringLiteral) { + // export {a as A} from "mod"; + // export {a, b as B} from "mod" + recordModuleName(); } } } - - return true; } - - return false; - } - - function tryConsumeRequireCall(skipCurrentToken: boolean): boolean { - let token = skipCurrentToken ? nextToken() : scanner.getToken(); - if (token === SyntaxKind.RequireKeyword) { + else if (token === SyntaxKind.AsteriskToken) { token = nextToken(); - if (token === SyntaxKind.OpenParenToken) { + if (token === SyntaxKind.FromKeyword) { token = nextToken(); if (token === SyntaxKind.StringLiteral) { - // require("mod"); + // export * from "mod" recordModuleName(); } } - return true; } - return false; - } - - function tryConsumeDefine(): boolean { - let token = scanner.getToken(); - if (token === SyntaxKind.Identifier && scanner.getTokenValue() === "define") { - token = nextToken(); - if (token !== SyntaxKind.OpenParenToken) { - return true; - } - + else if (token === SyntaxKind.ImportKeyword) { token = nextToken(); - if (token === SyntaxKind.StringLiteral) { - // looks like define ("modname", ... - skip string literal and comma + if (token === SyntaxKind.Identifier || isKeyword(token)) { token = nextToken(); - if (token === SyntaxKind.CommaToken) { - token = nextToken(); - } - else { - // unexpected token - return true; + if (token === SyntaxKind.EqualsToken) { + if (tryConsumeRequireCall(/*skipCurrentToken*/ true)) { + return true; + } } } - - // should be start of dependency list - if (token !== SyntaxKind.OpenBracketToken) { - return true; - } - - // skip open bracket + } + return true; + } + return false; + } + function tryConsumeRequireCall(skipCurrentToken: boolean): boolean { + let token = skipCurrentToken ? nextToken() : scanner.getToken(); + if (token === SyntaxKind.RequireKeyword) { + token = nextToken(); + if (token === SyntaxKind.OpenParenToken) { token = nextToken(); - // scan until ']' or EOF - while (token !== SyntaxKind.CloseBracketToken && token !== SyntaxKind.EndOfFileToken) { - // record string literals as module names - if (token === SyntaxKind.StringLiteral) { - recordModuleName(); - } - - token = nextToken(); + if (token === SyntaxKind.StringLiteral) { + // require("mod"); + recordModuleName(); } - return true; - } - return false; + return true; } - - function processImports(): void { - scanner.setText(sourceText); - nextToken(); - // Look for: - // import "mod"; - // import d from "mod" - // import {a as A } from "mod"; - // import * as NS from "mod" - // import d, {a, b as B} from "mod" - // import i = require("mod"); - // import("mod"); - - // export * from "mod" - // export {a as b} from "mod" - // export import i = require("mod") - // (for JavaScript files) require("mod") - - // Do not look for: - // AnySymbol.import("mod") - // AnySymbol.nested.import("mod") - - while (true) { - if (scanner.getToken() === SyntaxKind.EndOfFileToken) { - break; - } - - // check if at least one of alternative have moved scanner forward - if (tryConsumeDeclare() || - tryConsumeImport() || - tryConsumeExport() || - (detectJavaScriptImports && (tryConsumeRequireCall(/*skipCurrentToken*/ false) || tryConsumeDefine()))) { - continue; + return false; + } + function tryConsumeDefine(): boolean { + let token = scanner.getToken(); + if (token === SyntaxKind.Identifier && scanner.getTokenValue() === "define") { + token = nextToken(); + if (token !== SyntaxKind.OpenParenToken) { + return true; + } + token = nextToken(); + if (token === SyntaxKind.StringLiteral) { + // looks like define ("modname", ... - skip string literal and comma + token = nextToken(); + if (token === SyntaxKind.CommaToken) { + token = nextToken(); } else { - nextToken(); + // unexpected token + return true; + } + } + // should be start of dependency list + if (token !== SyntaxKind.OpenBracketToken) { + return true; + } + // skip open bracket + token = nextToken(); + // scan until ']' or EOF + while (token !== SyntaxKind.CloseBracketToken && token !== SyntaxKind.EndOfFileToken) { + // record string literals as module names + if (token === SyntaxKind.StringLiteral) { + recordModuleName(); } + token = nextToken(); } - - scanner.setText(undefined); + return true; } - - if (readImportFiles) { - processImports(); + return false; + } + function processImports(): void { + scanner.setText(sourceText); + nextToken(); + // Look for: + // import "mod"; + // import d from "mod" + // import {a as A } from "mod"; + // import * as NS from "mod" + // import d, {a, b as B} from "mod" + // import i = require("mod"); + // import("mod"); + // export * from "mod" + // export {a as b} from "mod" + // export import i = require("mod") + // (for JavaScript files) require("mod") + // Do not look for: + // AnySymbol.import("mod") + // AnySymbol.nested.import("mod") + while (true) { + if (scanner.getToken() === SyntaxKind.EndOfFileToken) { + break; + } + // check if at least one of alternative have moved scanner forward + if (tryConsumeDeclare() || + tryConsumeImport() || + tryConsumeExport() || + (detectJavaScriptImports && (tryConsumeRequireCall(/*skipCurrentToken*/ false) || tryConsumeDefine()))) { + continue; + } + else { + nextToken(); + } } - processCommentPragmas(pragmaContext, sourceText); - processPragmasIntoFields(pragmaContext, noop); - if (externalModule) { - // for external modules module all nested ambient modules are augmentations - if (ambientExternalModules) { - // move all detected ambient modules to imported files since they need to be resolved - for (const decl of ambientExternalModules) { - importedFiles.push(decl.ref); - } + scanner.setText(undefined); + } + if (readImportFiles) { + processImports(); + } + processCommentPragmas(pragmaContext, sourceText); + processPragmasIntoFields(pragmaContext, noop); + if (externalModule) { + // for external modules module all nested ambient modules are augmentations + if (ambientExternalModules) { + // move all detected ambient modules to imported files since they need to be resolved + for (const decl of ambientExternalModules) { + importedFiles.push(decl.ref); } - return { referencedFiles: pragmaContext.referencedFiles, typeReferenceDirectives: pragmaContext.typeReferenceDirectives, libReferenceDirectives: pragmaContext.libReferenceDirectives, importedFiles, isLibFile: !!pragmaContext.hasNoDefaultLib, ambientExternalModules: undefined }; } - else { - // for global scripts ambient modules still can have augmentations - look for ambient modules with depth > 0 - let ambientModuleNames: string[] | undefined; - if (ambientExternalModules) { - for (const decl of ambientExternalModules) { - if (decl.depth === 0) { - if (!ambientModuleNames) { - ambientModuleNames = []; - } - ambientModuleNames.push(decl.ref.fileName); - } - else { - importedFiles.push(decl.ref); + return { referencedFiles: pragmaContext.referencedFiles, typeReferenceDirectives: pragmaContext.typeReferenceDirectives, libReferenceDirectives: pragmaContext.libReferenceDirectives, importedFiles, isLibFile: !!pragmaContext.hasNoDefaultLib, ambientExternalModules: undefined }; + } + else { + // for global scripts ambient modules still can have augmentations - look for ambient modules with depth > 0 + let ambientModuleNames: string[] | undefined; + if (ambientExternalModules) { + for (const decl of ambientExternalModules) { + if (decl.depth === 0) { + if (!ambientModuleNames) { + ambientModuleNames = []; } + ambientModuleNames.push(decl.ref.fileName); + } + else { + importedFiles.push(decl.ref); } } - return { referencedFiles: pragmaContext.referencedFiles, typeReferenceDirectives: pragmaContext.typeReferenceDirectives, libReferenceDirectives: pragmaContext.libReferenceDirectives, importedFiles, isLibFile: !!pragmaContext.hasNoDefaultLib, ambientExternalModules: ambientModuleNames }; } + return { referencedFiles: pragmaContext.referencedFiles, typeReferenceDirectives: pragmaContext.typeReferenceDirectives, libReferenceDirectives: pragmaContext.libReferenceDirectives, importedFiles, isLibFile: !!pragmaContext.hasNoDefaultLib, ambientExternalModules: ambientModuleNames }; } } diff --git a/src/services/refactorProvider.ts b/src/services/refactorProvider.ts index 1418cb53807b6..8045e16c3323d 100644 --- a/src/services/refactorProvider.ts +++ b/src/services/refactorProvider.ts @@ -1,21 +1,20 @@ +import { Refactor, createMap, RefactorContext, ApplicableRefactorInfo, arrayFrom, flatMapIterator, RefactorEditInfo } from "./ts"; +import * as ts from "./ts"; /* @internal */ -namespace ts.refactor { - // A map with the refactor code as key, the refactor itself as value - // e.g. nonSuggestableRefactors[refactorCode] -> the refactor you want - const refactors: Map = createMap(); - - /** @param name An unique code associated with each refactor. Does not have to be human-readable. */ - export function registerRefactor(name: string, refactor: Refactor) { - refactors.set(name, refactor); - } - - export function getApplicableRefactors(context: RefactorContext): ApplicableRefactorInfo[] { - return arrayFrom(flatMapIterator(refactors.values(), refactor => - context.cancellationToken && context.cancellationToken.isCancellationRequested() ? undefined : refactor.getAvailableActions(context))); - } - - export function getEditsForRefactor(context: RefactorContext, refactorName: string, actionName: string): RefactorEditInfo | undefined { - const refactor = refactors.get(refactorName); - return refactor && refactor.getEditsForAction(context, actionName); - } +// A map with the refactor code as key, the refactor itself as value +// e.g. nonSuggestableRefactors[refactorCode] -> the refactor you want +const refactors: ts.Map = createMap(); +/** @param name An unique code associated with each refactor. Does not have to be human-readable. */ +/* @internal */ +export function registerRefactor(name: string, refactor: Refactor) { + refactors.set(name, refactor); +} +/* @internal */ +export function getApplicableRefactors(context: RefactorContext): ApplicableRefactorInfo[] { + return arrayFrom(flatMapIterator(refactors.values(), refactor => context.cancellationToken && context.cancellationToken.isCancellationRequested() ? undefined : refactor.getAvailableActions(context))); +} +/* @internal */ +export function getEditsForRefactor(context: RefactorContext, refactorName: string, actionName: string): RefactorEditInfo | undefined { + const refactor = refactors.get(refactorName); + return refactor && refactor.getEditsForAction(context, actionName); } diff --git a/src/services/refactors/addOrRemoveBracesToArrowFunction.ts b/src/services/refactors/addOrRemoveBracesToArrowFunction.ts index 89a03c3c23989..b7fe2244a410e 100644 --- a/src/services/refactors/addOrRemoveBracesToArrowFunction.ts +++ b/src/services/refactors/addOrRemoveBracesToArrowFunction.ts @@ -1,26 +1,34 @@ +import { Diagnostics, ArrowFunction, Expression, ReturnStatement, RefactorContext, ApplicableRefactorInfo, emptyArray, RefactorEditInfo, ConciseBody, createReturn, createBlock, suppressLeadingAndTrailingTrivia, copyLeadingComments, SyntaxKind, createVoidZero, createParen, Debug, isBinaryExpression, isObjectLiteralExpression, SourceFile, getTokenAtPosition, getContainingFunction, isArrowFunction, rangeContainsRange, isExpression, first, isReturnStatement } from "../ts"; +import { registerRefactor } from "../ts.refactor"; +import { ChangeTracker } from "../ts.textChanges"; /* @internal */ -namespace ts.refactor.addOrRemoveBracesToArrowFunction { - const refactorName = "Add or remove braces in an arrow function"; - const refactorDescription = Diagnostics.Add_or_remove_braces_in_an_arrow_function.message; - const addBracesActionName = "Add braces to arrow function"; - const removeBracesActionName = "Remove braces from arrow function"; - const addBracesActionDescription = Diagnostics.Add_braces_to_arrow_function.message; - const removeBracesActionDescription = Diagnostics.Remove_braces_from_arrow_function.message; - registerRefactor(refactorName, { getEditsForAction, getAvailableActions }); - - interface Info { - func: ArrowFunction; - expression: Expression | undefined; - returnStatement?: ReturnStatement; - addBraces: boolean; - } - - function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] { - const { file, startPosition } = context; - const info = getConvertibleArrowFunctionAtPosition(file, startPosition); - if (!info) return emptyArray; - - return [{ +const refactorName = "Add or remove braces in an arrow function"; +/* @internal */ +const refactorDescription = Diagnostics.Add_or_remove_braces_in_an_arrow_function.message; +/* @internal */ +const addBracesActionName = "Add braces to arrow function"; +/* @internal */ +const removeBracesActionName = "Remove braces from arrow function"; +/* @internal */ +const addBracesActionDescription = Diagnostics.Add_braces_to_arrow_function.message; +/* @internal */ +const removeBracesActionDescription = Diagnostics.Remove_braces_from_arrow_function.message; +/* @internal */ +registerRefactor(refactorName, { getEditsForAction, getAvailableActions }); +/* @internal */ +interface Info { + func: ArrowFunction; + expression: Expression | undefined; + returnStatement?: ReturnStatement; + addBraces: boolean; +} +/* @internal */ +function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] { + const { file, startPosition } = context; + const info = getConvertibleArrowFunctionAtPosition(file, startPosition); + if (!info) + return emptyArray; + return [{ name: refactorName, description: refactorDescription, actions: [ @@ -29,68 +37,65 @@ namespace ts.refactor.addOrRemoveBracesToArrowFunction { name: addBracesActionName, description: addBracesActionDescription } : { - name: removeBracesActionName, - description: removeBracesActionDescription - } + name: removeBracesActionName, + description: removeBracesActionDescription + } ] }]; +} +/* @internal */ +function getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined { + const { file, startPosition } = context; + const info = getConvertibleArrowFunctionAtPosition(file, startPosition); + if (!info) + return undefined; + const { expression, returnStatement, func } = info; + let body: ConciseBody; + if (actionName === addBracesActionName) { + const returnStatement = createReturn(expression); + body = createBlock([returnStatement], /* multiLine */ true); + suppressLeadingAndTrailingTrivia(body); + copyLeadingComments((expression!), returnStatement, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ true); } - - function getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined { - const { file, startPosition } = context; - const info = getConvertibleArrowFunctionAtPosition(file, startPosition); - if (!info) return undefined; - - const { expression, returnStatement, func } = info; - - let body: ConciseBody; - if (actionName === addBracesActionName) { - const returnStatement = createReturn(expression); - body = createBlock([returnStatement], /* multiLine */ true); - suppressLeadingAndTrailingTrivia(body); - copyLeadingComments(expression!, returnStatement, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ true); - } - else if (actionName === removeBracesActionName && returnStatement) { - const actualExpression = expression || createVoidZero(); - body = needsParentheses(actualExpression) ? createParen(actualExpression) : actualExpression; - suppressLeadingAndTrailingTrivia(body); - copyLeadingComments(returnStatement, body, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); - } - else { - Debug.fail("invalid action"); - } - - const edits = textChanges.ChangeTracker.with(context, t => t.replaceNode(file, func.body, body)); - return { renameFilename: undefined, renameLocation: undefined, edits }; + else if (actionName === removeBracesActionName && returnStatement) { + const actualExpression = expression || createVoidZero(); + body = needsParentheses(actualExpression) ? createParen(actualExpression) : actualExpression; + suppressLeadingAndTrailingTrivia(body); + copyLeadingComments(returnStatement, body, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); + } + else { + Debug.fail("invalid action"); } - - function needsParentheses(expression: Expression) { - return isBinaryExpression(expression) && expression.operatorToken.kind === SyntaxKind.CommaToken || isObjectLiteralExpression(expression); + const edits = ChangeTracker.with(context, t => t.replaceNode(file, func.body, body)); + return { renameFilename: undefined, renameLocation: undefined, edits }; +} +/* @internal */ +function needsParentheses(expression: Expression) { + return isBinaryExpression(expression) && expression.operatorToken.kind === SyntaxKind.CommaToken || isObjectLiteralExpression(expression); +} +/* @internal */ +function getConvertibleArrowFunctionAtPosition(file: SourceFile, startPosition: number): Info | undefined { + const node = getTokenAtPosition(file, startPosition); + const func = getContainingFunction(node); + if (!func || !isArrowFunction(func) || (!rangeContainsRange(func, node) || rangeContainsRange(func.body, node))) + return undefined; + if (isExpression(func.body)) { + return { + func, + addBraces: true, + expression: func.body + }; } - - function getConvertibleArrowFunctionAtPosition(file: SourceFile, startPosition: number): Info | undefined { - const node = getTokenAtPosition(file, startPosition); - const func = getContainingFunction(node); - if (!func || !isArrowFunction(func) || (!rangeContainsRange(func, node) || rangeContainsRange(func.body, node))) return undefined; - - if (isExpression(func.body)) { + else if (func.body.statements.length === 1) { + const firstStatement = first(func.body.statements); + if (isReturnStatement(firstStatement)) { return { func, - addBraces: true, - expression: func.body + addBraces: false, + expression: firstStatement.expression, + returnStatement: firstStatement }; } - else if (func.body.statements.length === 1) { - const firstStatement = first(func.body.statements); - if (isReturnStatement(firstStatement)) { - return { - func, - addBraces: false, - expression: firstStatement.expression, - returnStatement: firstStatement - }; - } - } - return undefined; } + return undefined; } diff --git a/src/services/refactors/convertExport.ts b/src/services/refactors/convertExport.ts index c08ae05bce8af..51f1ad6119543 100644 --- a/src/services/refactors/convertExport.ts +++ b/src/services/refactors/convertExport.ts @@ -1,209 +1,213 @@ +import { registerRefactor } from "../ts.refactor"; +import { ApplicableRefactorInfo, emptyArray, Diagnostics, RefactorEditInfo, Debug, FunctionDeclaration, ClassDeclaration, InterfaceDeclaration, EnumDeclaration, NamespaceDeclaration, TypeAliasDeclaration, VariableStatement, Identifier, Symbol, RefactorContext, getRefactorContextSpan, getTokenAtPosition, getParentNodeInSpan, isSourceFile, isModuleBlock, isAmbientModule, getModifierFlags, ModifierFlags, InternalSymbolName, SyntaxKind, isIdentifier, NodeFlags, first, SourceFile, Program, CancellationToken, TypeChecker, findModifier, createToken, createExportDefault, createIdentifier, ImportSpecifier, ExportSpecifier, ImportClause, createNamedImports, isStringLiteral, quotePreferenceFromString, QuotePreference, makeImport, PropertyAccessExpression, Node, createImportSpecifier, createExportSpecifier } from "../ts"; +import { ChangeTracker } from "../ts.textChanges"; +import { Core } from "../ts.FindAllReferences"; /* @internal */ -namespace ts.refactor { - const refactorName = "Convert export"; - const actionNameDefaultToNamed = "Convert default export to named export"; - const actionNameNamedToDefault = "Convert named export to default export"; - registerRefactor(refactorName, { - getAvailableActions(context): readonly ApplicableRefactorInfo[] { - const info = getInfo(context); - if (!info) return emptyArray; - const description = info.wasDefault ? Diagnostics.Convert_default_export_to_named_export.message : Diagnostics.Convert_named_export_to_default_export.message; - const actionName = info.wasDefault ? actionNameDefaultToNamed : actionNameNamedToDefault; - return [{ name: refactorName, description, actions: [{ name: actionName, description }] }]; - }, - getEditsForAction(context, actionName): RefactorEditInfo { - Debug.assert(actionName === actionNameDefaultToNamed || actionName === actionNameNamedToDefault, "Unexpected action name"); - const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, Debug.checkDefined(getInfo(context), "context must have info"), t, context.cancellationToken)); - return { edits, renameFilename: undefined, renameLocation: undefined }; - }, - }); - - // If a VariableStatement, will have exactly one VariableDeclaration, with an Identifier for a name. - type ExportToConvert = FunctionDeclaration | ClassDeclaration | InterfaceDeclaration | EnumDeclaration | NamespaceDeclaration | TypeAliasDeclaration | VariableStatement; - interface Info { - readonly exportNode: ExportToConvert; - readonly exportName: Identifier; // This is exportNode.name except for VariableStatement_s. - readonly wasDefault: boolean; - readonly exportingModuleSymbol: Symbol; +const refactorName = "Convert export"; +/* @internal */ +const actionNameDefaultToNamed = "Convert default export to named export"; +/* @internal */ +const actionNameNamedToDefault = "Convert named export to default export"; +/* @internal */ +registerRefactor(refactorName, { + getAvailableActions(context): readonly ApplicableRefactorInfo[] { + const info = getInfo(context); + if (!info) + return emptyArray; + const description = info.wasDefault ? Diagnostics.Convert_default_export_to_named_export.message : Diagnostics.Convert_named_export_to_default_export.message; + const actionName = info.wasDefault ? actionNameDefaultToNamed : actionNameNamedToDefault; + return [{ name: refactorName, description, actions: [{ name: actionName, description }] }]; + }, + getEditsForAction(context, actionName): RefactorEditInfo { + Debug.assert(actionName === actionNameDefaultToNamed || actionName === actionNameNamedToDefault, "Unexpected action name"); + const edits = ChangeTracker.with(context, t => doChange(context.file, context.program, Debug.checkDefined(getInfo(context), "context must have info"), t, context.cancellationToken)); + return { edits, renameFilename: undefined, renameLocation: undefined }; + }, +}); +// If a VariableStatement, will have exactly one VariableDeclaration, with an Identifier for a name. +/* @internal */ +type ExportToConvert = FunctionDeclaration | ClassDeclaration | InterfaceDeclaration | EnumDeclaration | NamespaceDeclaration | TypeAliasDeclaration | VariableStatement; +/* @internal */ +interface Info { + readonly exportNode: ExportToConvert; + readonly exportName: Identifier; // This is exportNode.name except for VariableStatement_s. + readonly wasDefault: boolean; + readonly exportingModuleSymbol: Symbol; +} +/* @internal */ +function getInfo(context: RefactorContext): Info | undefined { + const { file } = context; + const span = getRefactorContextSpan(context); + const token = getTokenAtPosition(file, span.start); + const exportNode = getParentNodeInSpan(token, file, span); + if (!exportNode || (!isSourceFile(exportNode.parent) && !(isModuleBlock(exportNode.parent) && isAmbientModule(exportNode.parent.parent)))) { + return undefined; } - - function getInfo(context: RefactorContext): Info | undefined { - const { file } = context; - const span = getRefactorContextSpan(context); - const token = getTokenAtPosition(file, span.start); - const exportNode = getParentNodeInSpan(token, file, span); - if (!exportNode || (!isSourceFile(exportNode.parent) && !(isModuleBlock(exportNode.parent) && isAmbientModule(exportNode.parent.parent)))) { - return undefined; + const exportingModuleSymbol = isSourceFile(exportNode.parent) ? exportNode.parent.symbol : exportNode.parent.parent.symbol; + const flags = getModifierFlags(exportNode); + const wasDefault = !!(flags & ModifierFlags.Default); + // If source file already has a default export, don't offer refactor. + if (!(flags & ModifierFlags.Export) || !wasDefault && exportingModuleSymbol.exports!.has(InternalSymbolName.Default)) { + return undefined; + } + switch (exportNode.kind) { + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.ModuleDeclaration: { + const node = (exportNode as FunctionDeclaration | ClassDeclaration | InterfaceDeclaration | EnumDeclaration | TypeAliasDeclaration | NamespaceDeclaration); + return node.name && isIdentifier(node.name) ? { exportNode: node, exportName: node.name, wasDefault, exportingModuleSymbol } : undefined; } - - const exportingModuleSymbol = isSourceFile(exportNode.parent) ? exportNode.parent.symbol : exportNode.parent.parent.symbol; - - const flags = getModifierFlags(exportNode); - const wasDefault = !!(flags & ModifierFlags.Default); - // If source file already has a default export, don't offer refactor. - if (!(flags & ModifierFlags.Export) || !wasDefault && exportingModuleSymbol.exports!.has(InternalSymbolName.Default)) { - return undefined; + case SyntaxKind.VariableStatement: { + const vs = (exportNode as VariableStatement); + // Must be `export const x = something;`. + if (!(vs.declarationList.flags & NodeFlags.Const) || vs.declarationList.declarations.length !== 1) { + return undefined; + } + const decl = first(vs.declarationList.declarations); + if (!decl.initializer) + return undefined; + Debug.assert(!wasDefault, "Can't have a default flag here"); + return isIdentifier(decl.name) ? { exportNode: vs, exportName: decl.name, wasDefault, exportingModuleSymbol } : undefined; } - + default: + return undefined; + } +} +/* @internal */ +function doChange(exportingSourceFile: SourceFile, program: Program, info: Info, changes: ChangeTracker, cancellationToken: CancellationToken | undefined): void { + changeExport(exportingSourceFile, info, changes, program.getTypeChecker()); + changeImports(program, info, changes, cancellationToken); +} +/* @internal */ +function changeExport(exportingSourceFile: SourceFile, { wasDefault, exportNode, exportName }: Info, changes: ChangeTracker, checker: TypeChecker): void { + if (wasDefault) { + changes.delete(exportingSourceFile, Debug.checkDefined(findModifier(exportNode, SyntaxKind.DefaultKeyword), "Should find a default keyword in modifier list")); + } + else { + const exportKeyword = Debug.checkDefined(findModifier(exportNode, SyntaxKind.ExportKeyword), "Should find an export keyword in modifier list"); switch (exportNode.kind) { case SyntaxKind.FunctionDeclaration: case SyntaxKind.ClassDeclaration: case SyntaxKind.InterfaceDeclaration: + changes.insertNodeAfter(exportingSourceFile, exportKeyword, createToken(SyntaxKind.DefaultKeyword)); + break; + case SyntaxKind.VariableStatement: + // If 'x' isn't used in this file, `export const x = 0;` --> `export default 0;` + if (!Core.isSymbolReferencedInFile(exportName, checker, exportingSourceFile)) { + // We checked in `getInfo` that an initializer exists. + changes.replaceNode(exportingSourceFile, exportNode, createExportDefault(Debug.checkDefined(first(exportNode.declarationList.declarations).initializer, "Initializer was previously known to be present"))); + break; + } + // falls through case SyntaxKind.EnumDeclaration: case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.ModuleDeclaration: { - const node = exportNode as FunctionDeclaration | ClassDeclaration | InterfaceDeclaration | EnumDeclaration | TypeAliasDeclaration | NamespaceDeclaration; - return node.name && isIdentifier(node.name) ? { exportNode: node, exportName: node.name, wasDefault, exportingModuleSymbol } : undefined; - } - case SyntaxKind.VariableStatement: { - const vs = exportNode as VariableStatement; - // Must be `export const x = something;`. - if (!(vs.declarationList.flags & NodeFlags.Const) || vs.declarationList.declarations.length !== 1) { - return undefined; - } - const decl = first(vs.declarationList.declarations); - if (!decl.initializer) return undefined; - Debug.assert(!wasDefault, "Can't have a default flag here"); - return isIdentifier(decl.name) ? { exportNode: vs, exportName: decl.name, wasDefault, exportingModuleSymbol } : undefined; - } + case SyntaxKind.ModuleDeclaration: + // `export type T = number;` -> `type T = number; export default T;` + changes.deleteModifier(exportingSourceFile, exportKeyword); + changes.insertNodeAfter(exportingSourceFile, exportNode, createExportDefault(createIdentifier(exportName.text))); + break; default: - return undefined; + Debug.assertNever(exportNode, `Unexpected exportNode kind ${(exportNode as ExportToConvert).kind}`); } } - - function doChange(exportingSourceFile: SourceFile, program: Program, info: Info, changes: textChanges.ChangeTracker, cancellationToken: CancellationToken | undefined): void { - changeExport(exportingSourceFile, info, changes, program.getTypeChecker()); - changeImports(program, info, changes, cancellationToken); - } - - function changeExport(exportingSourceFile: SourceFile, { wasDefault, exportNode, exportName }: Info, changes: textChanges.ChangeTracker, checker: TypeChecker): void { +} +/* @internal */ +function changeImports(program: Program, { wasDefault, exportName, exportingModuleSymbol }: Info, changes: ChangeTracker, cancellationToken: CancellationToken | undefined): void { + const checker = program.getTypeChecker(); + const exportSymbol = Debug.checkDefined(checker.getSymbolAtLocation(exportName), "Export name should resolve to a symbol"); + Core.eachExportReference(program.getSourceFiles(), checker, cancellationToken, exportSymbol, exportingModuleSymbol, exportName.text, wasDefault, ref => { + const importingSourceFile = ref.getSourceFile(); if (wasDefault) { - changes.delete(exportingSourceFile, Debug.checkDefined(findModifier(exportNode, SyntaxKind.DefaultKeyword), "Should find a default keyword in modifier list")); + changeDefaultToNamedImport(importingSourceFile, ref, changes, exportName.text); } else { - const exportKeyword = Debug.checkDefined(findModifier(exportNode, SyntaxKind.ExportKeyword), "Should find an export keyword in modifier list"); - switch (exportNode.kind) { - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.ClassDeclaration: - case SyntaxKind.InterfaceDeclaration: - changes.insertNodeAfter(exportingSourceFile, exportKeyword, createToken(SyntaxKind.DefaultKeyword)); - break; - case SyntaxKind.VariableStatement: - // If 'x' isn't used in this file, `export const x = 0;` --> `export default 0;` - if (!FindAllReferences.Core.isSymbolReferencedInFile(exportName, checker, exportingSourceFile)) { - // We checked in `getInfo` that an initializer exists. - changes.replaceNode(exportingSourceFile, exportNode, createExportDefault(Debug.checkDefined(first(exportNode.declarationList.declarations).initializer, "Initializer was previously known to be present"))); - break; - } - // falls through - case SyntaxKind.EnumDeclaration: - case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.ModuleDeclaration: - // `export type T = number;` -> `type T = number; export default T;` - changes.deleteModifier(exportingSourceFile, exportKeyword); - changes.insertNodeAfter(exportingSourceFile, exportNode, createExportDefault(createIdentifier(exportName.text))); - break; - default: - Debug.assertNever(exportNode, `Unexpected exportNode kind ${(exportNode as ExportToConvert).kind}`); - } + changeNamedToDefaultImport(importingSourceFile, ref, changes); } - } - - function changeImports(program: Program, { wasDefault, exportName, exportingModuleSymbol }: Info, changes: textChanges.ChangeTracker, cancellationToken: CancellationToken | undefined): void { - const checker = program.getTypeChecker(); - const exportSymbol = Debug.checkDefined(checker.getSymbolAtLocation(exportName), "Export name should resolve to a symbol"); - FindAllReferences.Core.eachExportReference(program.getSourceFiles(), checker, cancellationToken, exportSymbol, exportingModuleSymbol, exportName.text, wasDefault, ref => { - const importingSourceFile = ref.getSourceFile(); - if (wasDefault) { - changeDefaultToNamedImport(importingSourceFile, ref, changes, exportName.text); + }); +} +/* @internal */ +function changeDefaultToNamedImport(importingSourceFile: SourceFile, ref: Identifier, changes: ChangeTracker, exportName: string): void { + const { parent } = ref; + switch (parent.kind) { + case SyntaxKind.PropertyAccessExpression: + // `a.default` --> `a.foo` + changes.replaceNode(importingSourceFile, ref, createIdentifier(exportName)); + break; + case SyntaxKind.ImportSpecifier: + case SyntaxKind.ExportSpecifier: { + const spec = (parent as ImportSpecifier | ExportSpecifier); + // `default as foo` --> `foo`, `default as bar` --> `foo as bar` + changes.replaceNode(importingSourceFile, spec, makeImportSpecifier(exportName, spec.name.text)); + break; + } + case SyntaxKind.ImportClause: { + const clause = (parent as ImportClause); + Debug.assert(clause.name === ref, "Import clause name should match provided ref"); + const spec = makeImportSpecifier(exportName, ref.text); + const { namedBindings } = clause; + if (!namedBindings) { + // `import foo from "./a";` --> `import { foo } from "./a";` + changes.replaceNode(importingSourceFile, ref, createNamedImports([spec])); } - else { - changeNamedToDefaultImport(importingSourceFile, ref, changes); + else if (namedBindings.kind === SyntaxKind.NamespaceImport) { + // `import foo, * as a from "./a";` --> `import * as a from ".a/"; import { foo } from "./a";` + changes.deleteRange(importingSourceFile, { pos: ref.getStart(importingSourceFile), end: namedBindings.getStart(importingSourceFile) }); + const quotePreference = isStringLiteral(clause.parent.moduleSpecifier) ? quotePreferenceFromString(clause.parent.moduleSpecifier, importingSourceFile) : QuotePreference.Double; + const newImport = makeImport(/*default*/ undefined, [makeImportSpecifier(exportName, ref.text)], clause.parent.moduleSpecifier, quotePreference); + changes.insertNodeAfter(importingSourceFile, clause.parent, newImport); } - }); - } - - function changeDefaultToNamedImport(importingSourceFile: SourceFile, ref: Identifier, changes: textChanges.ChangeTracker, exportName: string): void { - const { parent } = ref; - switch (parent.kind) { - case SyntaxKind.PropertyAccessExpression: - // `a.default` --> `a.foo` - changes.replaceNode(importingSourceFile, ref, createIdentifier(exportName)); - break; - case SyntaxKind.ImportSpecifier: - case SyntaxKind.ExportSpecifier: { - const spec = parent as ImportSpecifier | ExportSpecifier; - // `default as foo` --> `foo`, `default as bar` --> `foo as bar` - changes.replaceNode(importingSourceFile, spec, makeImportSpecifier(exportName, spec.name.text)); - break; - } - case SyntaxKind.ImportClause: { - const clause = parent as ImportClause; - Debug.assert(clause.name === ref, "Import clause name should match provided ref"); - const spec = makeImportSpecifier(exportName, ref.text); - const { namedBindings } = clause; - if (!namedBindings) { - // `import foo from "./a";` --> `import { foo } from "./a";` - changes.replaceNode(importingSourceFile, ref, createNamedImports([spec])); - } - else if (namedBindings.kind === SyntaxKind.NamespaceImport) { - // `import foo, * as a from "./a";` --> `import * as a from ".a/"; import { foo } from "./a";` - changes.deleteRange(importingSourceFile, { pos: ref.getStart(importingSourceFile), end: namedBindings.getStart(importingSourceFile) }); - const quotePreference = isStringLiteral(clause.parent.moduleSpecifier) ? quotePreferenceFromString(clause.parent.moduleSpecifier, importingSourceFile) : QuotePreference.Double; - const newImport = makeImport(/*default*/ undefined, [makeImportSpecifier(exportName, ref.text)], clause.parent.moduleSpecifier, quotePreference); - changes.insertNodeAfter(importingSourceFile, clause.parent, newImport); - } - else { - // `import foo, { bar } from "./a"` --> `import { bar, foo } from "./a";` - changes.delete(importingSourceFile, ref); - changes.insertNodeAtEndOfList(importingSourceFile, namedBindings.elements, spec); - } - break; + else { + // `import foo, { bar } from "./a"` --> `import { bar, foo } from "./a";` + changes.delete(importingSourceFile, ref); + changes.insertNodeAtEndOfList(importingSourceFile, namedBindings.elements, spec); } - default: - Debug.failBadSyntaxKind(parent); + break; } + default: + Debug.failBadSyntaxKind(parent); } - - function changeNamedToDefaultImport(importingSourceFile: SourceFile, ref: Identifier, changes: textChanges.ChangeTracker): void { - const parent = ref.parent as PropertyAccessExpression | ImportSpecifier | ExportSpecifier; - switch (parent.kind) { - case SyntaxKind.PropertyAccessExpression: - // `a.foo` --> `a.default` - changes.replaceNode(importingSourceFile, ref, createIdentifier("default")); - break; - case SyntaxKind.ImportSpecifier: { - // `import { foo } from "./a";` --> `import foo from "./a";` - // `import { foo as bar } from "./a";` --> `import bar from "./a";` - const defaultImport = createIdentifier(parent.name.text); - if (parent.parent.elements.length === 1) { - changes.replaceNode(importingSourceFile, parent.parent, defaultImport); - } - else { - changes.delete(importingSourceFile, parent); - changes.insertNodeBefore(importingSourceFile, parent.parent, defaultImport); - } - break; +} +/* @internal */ +function changeNamedToDefaultImport(importingSourceFile: SourceFile, ref: Identifier, changes: ChangeTracker): void { + const parent = (ref.parent as PropertyAccessExpression | ImportSpecifier | ExportSpecifier); + switch (parent.kind) { + case SyntaxKind.PropertyAccessExpression: + // `a.foo` --> `a.default` + changes.replaceNode(importingSourceFile, ref, createIdentifier("default")); + break; + case SyntaxKind.ImportSpecifier: { + // `import { foo } from "./a";` --> `import foo from "./a";` + // `import { foo as bar } from "./a";` --> `import bar from "./a";` + const defaultImport = createIdentifier(parent.name.text); + if (parent.parent.elements.length === 1) { + changes.replaceNode(importingSourceFile, parent.parent, defaultImport); } - case SyntaxKind.ExportSpecifier: { - // `export { foo } from "./a";` --> `export { default as foo } from "./a";` - // `export { foo as bar } from "./a";` --> `export { default as bar } from "./a";` - // `export { foo as default } from "./a";` --> `export { default } from "./a";` - // (Because `export foo from "./a";` isn't valid syntax.) - changes.replaceNode(importingSourceFile, parent, makeExportSpecifier("default", parent.name.text)); - break; + else { + changes.delete(importingSourceFile, parent); + changes.insertNodeBefore(importingSourceFile, parent.parent, defaultImport); } - default: - Debug.assertNever(parent, `Unexpected parent kind ${(parent as Node).kind}`); + break; } - - } - - function makeImportSpecifier(propertyName: string, name: string): ImportSpecifier { - return createImportSpecifier(propertyName === name ? undefined : createIdentifier(propertyName), createIdentifier(name)); - } - - function makeExportSpecifier(propertyName: string, name: string): ExportSpecifier { - return createExportSpecifier(propertyName === name ? undefined : createIdentifier(propertyName), createIdentifier(name)); + case SyntaxKind.ExportSpecifier: { + // `export { foo } from "./a";` --> `export { default as foo } from "./a";` + // `export { foo as bar } from "./a";` --> `export { default as bar } from "./a";` + // `export { foo as default } from "./a";` --> `export { default } from "./a";` + // (Because `export foo from "./a";` isn't valid syntax.) + changes.replaceNode(importingSourceFile, parent, makeExportSpecifier("default", parent.name.text)); + break; + } + default: + Debug.assertNever(parent, `Unexpected parent kind ${(parent as Node).kind}`); } } +/* @internal */ +function makeImportSpecifier(propertyName: string, name: string): ImportSpecifier { + return createImportSpecifier(propertyName === name ? undefined : createIdentifier(propertyName), createIdentifier(name)); +} +/* @internal */ +function makeExportSpecifier(propertyName: string, name: string): ExportSpecifier { + return createExportSpecifier(propertyName === name ? undefined : createIdentifier(propertyName), createIdentifier(name)); +} diff --git a/src/services/refactors/convertImport.ts b/src/services/refactors/convertImport.ts index 29cefdc3210e3..a4aed6b14ed2e 100644 --- a/src/services/refactors/convertImport.ts +++ b/src/services/refactors/convertImport.ts @@ -1,130 +1,125 @@ +import { registerRefactor } from "../ts.refactor"; +import { ApplicableRefactorInfo, emptyArray, SyntaxKind, Diagnostics, RefactorEditInfo, Debug, RefactorContext, NamedImportBindings, getRefactorContextSpan, getTokenAtPosition, getParentNodeInSpan, isImportDeclaration, SourceFile, Program, getAllowSyntheticDefaultImports, TypeChecker, NamespaceImport, PropertyAccessExpression, createMap, isPropertyAccessExpression, cast, SymbolFlags, getUniqueName, createIdentifier, ImportSpecifier, createImportSpecifier, NamedImports, isStringLiteral, ScriptTarget, createPropertyAccess, isShorthandPropertyAssignment, createPropertyAssignment, isExportSpecifier, createNamespaceImport, ImportDeclaration, Identifier, createImportDeclaration, createImportClause, createNamedImports } from "../ts"; +import { ChangeTracker } from "../ts.textChanges"; +import { Core } from "../ts.FindAllReferences"; +import { moduleSpecifierToValidIdentifier } from "../ts.codefix"; /* @internal */ -namespace ts.refactor { - const refactorName = "Convert import"; - const actionNameNamespaceToNamed = "Convert namespace import to named imports"; - const actionNameNamedToNamespace = "Convert named imports to namespace import"; - registerRefactor(refactorName, { - getAvailableActions(context): readonly ApplicableRefactorInfo[] { - const i = getImportToConvert(context); - if (!i) return emptyArray; - const description = i.kind === SyntaxKind.NamespaceImport ? Diagnostics.Convert_namespace_import_to_named_imports.message : Diagnostics.Convert_named_imports_to_namespace_import.message; - const actionName = i.kind === SyntaxKind.NamespaceImport ? actionNameNamespaceToNamed : actionNameNamedToNamespace; - return [{ name: refactorName, description, actions: [{ name: actionName, description }] }]; - }, - getEditsForAction(context, actionName): RefactorEditInfo { - Debug.assert(actionName === actionNameNamespaceToNamed || actionName === actionNameNamedToNamespace, "Unexpected action name"); - const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, t, Debug.checkDefined(getImportToConvert(context), "Context must provide an import to convert"))); - return { edits, renameFilename: undefined, renameLocation: undefined }; - } - }); - - // Can convert imports of the form `import * as m from "m";` or `import d, { x, y } from "m";`. - function getImportToConvert(context: RefactorContext): NamedImportBindings | undefined { - const { file } = context; - const span = getRefactorContextSpan(context); - const token = getTokenAtPosition(file, span.start); - const importDecl = getParentNodeInSpan(token, file, span); - if (!importDecl || !isImportDeclaration(importDecl)) return undefined; - const { importClause } = importDecl; - return importClause && importClause.namedBindings; +const refactorName = "Convert import"; +/* @internal */ +const actionNameNamespaceToNamed = "Convert namespace import to named imports"; +/* @internal */ +const actionNameNamedToNamespace = "Convert named imports to namespace import"; +/* @internal */ +registerRefactor(refactorName, { + getAvailableActions(context): readonly ApplicableRefactorInfo[] { + const i = getImportToConvert(context); + if (!i) + return emptyArray; + const description = i.kind === SyntaxKind.NamespaceImport ? Diagnostics.Convert_namespace_import_to_named_imports.message : Diagnostics.Convert_named_imports_to_namespace_import.message; + const actionName = i.kind === SyntaxKind.NamespaceImport ? actionNameNamespaceToNamed : actionNameNamedToNamespace; + return [{ name: refactorName, description, actions: [{ name: actionName, description }] }]; + }, + getEditsForAction(context, actionName): RefactorEditInfo { + Debug.assert(actionName === actionNameNamespaceToNamed || actionName === actionNameNamedToNamespace, "Unexpected action name"); + const edits = ChangeTracker.with(context, t => doChange(context.file, context.program, t, Debug.checkDefined(getImportToConvert(context), "Context must provide an import to convert"))); + return { edits, renameFilename: undefined, renameLocation: undefined }; + } +}); +// Can convert imports of the form `import * as m from "m";` or `import d, { x, y } from "m";`. +/* @internal */ +function getImportToConvert(context: RefactorContext): NamedImportBindings | undefined { + const { file } = context; + const span = getRefactorContextSpan(context); + const token = getTokenAtPosition(file, span.start); + const importDecl = getParentNodeInSpan(token, file, span); + if (!importDecl || !isImportDeclaration(importDecl)) + return undefined; + const { importClause } = importDecl; + return importClause && importClause.namedBindings; +} +/* @internal */ +function doChange(sourceFile: SourceFile, program: Program, changes: ChangeTracker, toConvert: NamedImportBindings): void { + const checker = program.getTypeChecker(); + if (toConvert.kind === SyntaxKind.NamespaceImport) { + doChangeNamespaceToNamed(sourceFile, checker, changes, toConvert, getAllowSyntheticDefaultImports(program.getCompilerOptions())); } - - function doChange(sourceFile: SourceFile, program: Program, changes: textChanges.ChangeTracker, toConvert: NamedImportBindings): void { - const checker = program.getTypeChecker(); - if (toConvert.kind === SyntaxKind.NamespaceImport) { - doChangeNamespaceToNamed(sourceFile, checker, changes, toConvert, getAllowSyntheticDefaultImports(program.getCompilerOptions())); + else { + doChangeNamedToNamespace(sourceFile, checker, changes, toConvert); + } +} +/* @internal */ +function doChangeNamespaceToNamed(sourceFile: SourceFile, checker: TypeChecker, changes: ChangeTracker, toConvert: NamespaceImport, allowSyntheticDefaultImports: boolean): void { + let usedAsNamespaceOrDefault = false; + const nodesToReplace: PropertyAccessExpression[] = []; + const conflictingNames = createMap(); + Core.eachSymbolReferenceInFile(toConvert.name, checker, sourceFile, id => { + if (!isPropertyAccessExpression(id.parent)) { + usedAsNamespaceOrDefault = true; } else { - doChangeNamedToNamespace(sourceFile, checker, changes, toConvert); + const parent = cast(id.parent, isPropertyAccessExpression); + const exportName = parent.name.text; + if (checker.resolveName(exportName, id, SymbolFlags.All, /*excludeGlobals*/ true)) { + conflictingNames.set(exportName, true); + } + Debug.assert(parent.expression === id, "Parent expression should match id"); + nodesToReplace.push(parent); } + }); + // We may need to change `mod.x` to `_x` to avoid a name conflict. + const exportNameToImportName = createMap(); + for (const propertyAccess of nodesToReplace) { + const exportName = propertyAccess.name.text; + let importName = exportNameToImportName.get(exportName); + if (importName === undefined) { + exportNameToImportName.set(exportName, importName = conflictingNames.has(exportName) ? getUniqueName(exportName, sourceFile) : exportName); + } + changes.replaceNode(sourceFile, propertyAccess, createIdentifier(importName)); + } + const importSpecifiers: ImportSpecifier[] = []; + exportNameToImportName.forEach((name, propertyName) => { + importSpecifiers.push(createImportSpecifier(name === propertyName ? undefined : createIdentifier(propertyName), createIdentifier(name))); + }); + const importDecl = toConvert.parent.parent; + if (usedAsNamespaceOrDefault && !allowSyntheticDefaultImports) { + // Need to leave the namespace import alone + changes.insertNodeAfter(sourceFile, importDecl, updateImport(importDecl, /*defaultImportName*/ undefined, importSpecifiers)); + } + else { + changes.replaceNode(sourceFile, importDecl, updateImport(importDecl, usedAsNamespaceOrDefault ? createIdentifier(toConvert.name.text) : undefined, importSpecifiers)); } - - function doChangeNamespaceToNamed(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, toConvert: NamespaceImport, allowSyntheticDefaultImports: boolean): void { - let usedAsNamespaceOrDefault = false; - - const nodesToReplace: PropertyAccessExpression[] = []; - const conflictingNames = createMap(); - - FindAllReferences.Core.eachSymbolReferenceInFile(toConvert.name, checker, sourceFile, id => { - if (!isPropertyAccessExpression(id.parent)) { - usedAsNamespaceOrDefault = true; +} +/* @internal */ +function doChangeNamedToNamespace(sourceFile: SourceFile, checker: TypeChecker, changes: ChangeTracker, toConvert: NamedImports): void { + const importDecl = toConvert.parent.parent; + const { moduleSpecifier } = importDecl; + const preferredName = moduleSpecifier && isStringLiteral(moduleSpecifier) ? moduleSpecifierToValidIdentifier(moduleSpecifier.text, ScriptTarget.ESNext) : "module"; + const namespaceNameConflicts = toConvert.elements.some(element => Core.eachSymbolReferenceInFile(element.name, checker, sourceFile, id => !!checker.resolveName(preferredName, id, SymbolFlags.All, /*excludeGlobals*/ true)) || false); + const namespaceImportName = namespaceNameConflicts ? getUniqueName(preferredName, sourceFile) : preferredName; + const neededNamedImports: ImportSpecifier[] = []; + for (const element of toConvert.elements) { + const propertyName = (element.propertyName || element.name).text; + Core.eachSymbolReferenceInFile(element.name, checker, sourceFile, id => { + const access = createPropertyAccess(createIdentifier(namespaceImportName), propertyName); + if (isShorthandPropertyAssignment(id.parent)) { + changes.replaceNode(sourceFile, id.parent, createPropertyAssignment(id.text, access)); } - else { - const parent = cast(id.parent, isPropertyAccessExpression); - const exportName = parent.name.text; - if (checker.resolveName(exportName, id, SymbolFlags.All, /*excludeGlobals*/ true)) { - conflictingNames.set(exportName, true); + else if (isExportSpecifier(id.parent) && !id.parent.propertyName) { + if (!neededNamedImports.some(n => n.name === element.name)) { + neededNamedImports.push(createImportSpecifier(element.propertyName && createIdentifier(element.propertyName.text), createIdentifier(element.name.text))); } - Debug.assert(parent.expression === id, "Parent expression should match id"); - nodesToReplace.push(parent); } - }); - - // We may need to change `mod.x` to `_x` to avoid a name conflict. - const exportNameToImportName = createMap(); - - for (const propertyAccess of nodesToReplace) { - const exportName = propertyAccess.name.text; - let importName = exportNameToImportName.get(exportName); - if (importName === undefined) { - exportNameToImportName.set(exportName, importName = conflictingNames.has(exportName) ? getUniqueName(exportName, sourceFile) : exportName); + else { + changes.replaceNode(sourceFile, id, access); } - changes.replaceNode(sourceFile, propertyAccess, createIdentifier(importName)); - } - - const importSpecifiers: ImportSpecifier[] = []; - exportNameToImportName.forEach((name, propertyName) => { - importSpecifiers.push(createImportSpecifier(name === propertyName ? undefined : createIdentifier(propertyName), createIdentifier(name))); }); - - const importDecl = toConvert.parent.parent; - if (usedAsNamespaceOrDefault && !allowSyntheticDefaultImports) { - // Need to leave the namespace import alone - changes.insertNodeAfter(sourceFile, importDecl, updateImport(importDecl, /*defaultImportName*/ undefined, importSpecifiers)); - } - else { - changes.replaceNode(sourceFile, importDecl, updateImport(importDecl, usedAsNamespaceOrDefault ? createIdentifier(toConvert.name.text) : undefined, importSpecifiers)); - } } - - function doChangeNamedToNamespace(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, toConvert: NamedImports): void { - const importDecl = toConvert.parent.parent; - const { moduleSpecifier } = importDecl; - - const preferredName = moduleSpecifier && isStringLiteral(moduleSpecifier) ? codefix.moduleSpecifierToValidIdentifier(moduleSpecifier.text, ScriptTarget.ESNext) : "module"; - const namespaceNameConflicts = toConvert.elements.some(element => - FindAllReferences.Core.eachSymbolReferenceInFile(element.name, checker, sourceFile, id => - !!checker.resolveName(preferredName, id, SymbolFlags.All, /*excludeGlobals*/ true)) || false); - const namespaceImportName = namespaceNameConflicts ? getUniqueName(preferredName, sourceFile) : preferredName; - - const neededNamedImports: ImportSpecifier[] = []; - - for (const element of toConvert.elements) { - const propertyName = (element.propertyName || element.name).text; - FindAllReferences.Core.eachSymbolReferenceInFile(element.name, checker, sourceFile, id => { - const access = createPropertyAccess(createIdentifier(namespaceImportName), propertyName); - if (isShorthandPropertyAssignment(id.parent)) { - changes.replaceNode(sourceFile, id.parent, createPropertyAssignment(id.text, access)); - } - else if (isExportSpecifier(id.parent) && !id.parent.propertyName) { - if (!neededNamedImports.some(n => n.name === element.name)) { - neededNamedImports.push(createImportSpecifier(element.propertyName && createIdentifier(element.propertyName.text), createIdentifier(element.name.text))); - } - } - else { - changes.replaceNode(sourceFile, id, access); - } - }); - } - - changes.replaceNode(sourceFile, toConvert, createNamespaceImport(createIdentifier(namespaceImportName))); - if (neededNamedImports.length) { - changes.insertNodeAfter(sourceFile, toConvert.parent.parent, updateImport(importDecl, /*defaultImportName*/ undefined, neededNamedImports)); - } - } - - function updateImport(old: ImportDeclaration, defaultImportName: Identifier | undefined, elements: readonly ImportSpecifier[] | undefined): ImportDeclaration { - return createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, - createImportClause(defaultImportName, elements && elements.length ? createNamedImports(elements) : undefined), old.moduleSpecifier); + changes.replaceNode(sourceFile, toConvert, createNamespaceImport(createIdentifier(namespaceImportName))); + if (neededNamedImports.length) { + changes.insertNodeAfter(sourceFile, toConvert.parent.parent, updateImport(importDecl, /*defaultImportName*/ undefined, neededNamedImports)); } } +/* @internal */ +function updateImport(old: ImportDeclaration, defaultImportName: Identifier | undefined, elements: readonly ImportSpecifier[] | undefined): ImportDeclaration { + return createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, createImportClause(defaultImportName, elements && elements.length ? createNamedImports(elements) : undefined), old.moduleSpecifier); +} diff --git a/src/services/refactors/convertParamsToDestructuredObject.ts b/src/services/refactors/convertParamsToDestructuredObject.ts index 803a767761090..989390a5db1f8 100644 --- a/src/services/refactors/convertParamsToDestructuredObject.ts +++ b/src/services/refactors/convertParamsToDestructuredObject.ts @@ -1,621 +1,564 @@ +import { registerRefactor } from "../ts.refactor"; +import { RefactorContext, ApplicableRefactorInfo, isSourceFileJS, emptyArray, getLocaleSpecificMessage, Diagnostics, RefactorEditInfo, Debug, SourceFile, Program, LanguageServiceHost, map, getSynthesizedDeepClone, first, last, sortAndDeduplicate, compareValues, getSourceFileOfNode, CancellationToken, isConstructorDeclaration, deduplicate, equateValues, flatMap, every, contains, isNewExpressionTarget, isClassDeclaration, Node, getSymbolTarget, isImportSpecifier, isImportClause, isImportEqualsDeclaration, isNamespaceImport, isExportSpecifier, isExportAssignment, isDeclaration, CallExpression, NewExpression, SyntaxKind, tryCast, isCallOrNewExpression, isPropertyAccessExpression, isElementAccessExpression, ElementAccessExpression, PropertyAccessExpression, getMeaningFromLocation, SemanticMeaning, isExpressionWithTypeArgumentsInClassExtendsClause, TypeChecker, getTouchingToken, getContainingFunctionDeclaration, rangeContainsRange, findAncestor, isJSDocNode, isFunctionLikeDeclaration, FunctionLikeDeclaration, FunctionDeclaration, ClassDeclaration, findModifier, NodeArray, ParameterDeclaration, isRestParameter, isIdentifier, isVariableDeclaration, isVarConst, isThis, createNodeArray, Expression, PropertyAssignment, ShorthandPropertyAssignment, getTextOfIdentifierOrLiteral, createShorthandPropertyAssignment, createPropertyAssignment, ObjectLiteralExpression, suppressLeadingAndTrailingTrivia, isPropertyAssignment, createArrayLiteral, createObjectLiteral, createObjectBindingPattern, createParameter, BindingElement, createBindingElement, TypeLiteralNode, addEmitFlags, createTypeLiteralNode, EmitFlags, PropertySignature, createPropertySignature, createToken, TypeNode, getTypeNodeIfAccessible, copyLeadingComments, copyTrailingAsLeadingComments, copyTrailingComments, CharacterCodes, Identifier, Modifier, findChildOfKind, VariableDeclaration, ConstructorDeclaration, ClassExpression, FunctionBody, MethodDeclaration, FunctionExpression, ArrowFunction } from "../ts"; +import { ChangeTracker, LeadingTriviaOption, TrailingTriviaOption } from "../ts.textChanges"; +import { getReferenceEntriesForNode, Entry, EntryKind, NodeEntry } from "../ts.FindAllReferences"; /* @internal */ -namespace ts.refactor.convertParamsToDestructuredObject { - const refactorName = "Convert parameters to destructured object"; - const minimumParameterLength = 2; - registerRefactor(refactorName, { getEditsForAction, getAvailableActions }); - - - function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] { - const { file, startPosition } = context; - const isJSFile = isSourceFileJS(file); - if (isJSFile) return emptyArray; // TODO: GH#30113 - const functionDeclaration = getFunctionDeclarationAtPosition(file, startPosition, context.program.getTypeChecker()); - if (!functionDeclaration) return emptyArray; - - const description = getLocaleSpecificMessage(Diagnostics.Convert_parameters_to_destructured_object); - return [{ +const refactorName = "Convert parameters to destructured object"; +/* @internal */ +const minimumParameterLength = 2; +/* @internal */ +registerRefactor(refactorName, { getEditsForAction, getAvailableActions }); +/* @internal */ +function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] { + const { file, startPosition } = context; + const isJSFile = isSourceFileJS(file); + if (isJSFile) + return emptyArray; // TODO: GH#30113 + const functionDeclaration = getFunctionDeclarationAtPosition(file, startPosition, context.program.getTypeChecker()); + if (!functionDeclaration) + return emptyArray; + const description = getLocaleSpecificMessage(Diagnostics.Convert_parameters_to_destructured_object); + return [{ name: refactorName, description, actions: [{ - name: refactorName, - description - }] + name: refactorName, + description + }] }]; +} +/* @internal */ +function getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined { + Debug.assert(actionName === refactorName, "Unexpected action name"); + const { file, startPosition, program, cancellationToken, host } = context; + const functionDeclaration = getFunctionDeclarationAtPosition(file, startPosition, program.getTypeChecker()); + if (!functionDeclaration || !cancellationToken) + return undefined; + const groupedReferences = getGroupedReferences(functionDeclaration, program, cancellationToken); + if (groupedReferences.valid) { + const edits = ChangeTracker.with(context, t => doChange(file, program, host, t, functionDeclaration, groupedReferences)); + return { renameFilename: undefined, renameLocation: undefined, edits }; } - - function getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined { - Debug.assert(actionName === refactorName, "Unexpected action name"); - const { file, startPosition, program, cancellationToken, host } = context; - const functionDeclaration = getFunctionDeclarationAtPosition(file, startPosition, program.getTypeChecker()); - if (!functionDeclaration || !cancellationToken) return undefined; - - const groupedReferences = getGroupedReferences(functionDeclaration, program, cancellationToken); - if (groupedReferences.valid) { - const edits = textChanges.ChangeTracker.with(context, t => doChange(file, program, host, t, functionDeclaration, groupedReferences)); - return { renameFilename: undefined, renameLocation: undefined, edits }; + return { edits: [] }; // TODO: GH#30113 +} +/* @internal */ +function doChange(sourceFile: SourceFile, program: Program, host: LanguageServiceHost, changes: ChangeTracker, functionDeclaration: ValidFunctionDeclaration, groupedReferences: GroupedReferences): void { + const newParamDeclaration = map(createNewParameters(functionDeclaration, program, host), param => getSynthesizedDeepClone(param)); + changes.replaceNodeRangeWithNodes(sourceFile, first(functionDeclaration.parameters), last(functionDeclaration.parameters), newParamDeclaration, { joiner: ", ", + // indentation is set to 0 because otherwise the object parameter will be indented if there is a `this` parameter + indentation: 0, + leadingTriviaOption: LeadingTriviaOption.IncludeAll, + trailingTriviaOption: TrailingTriviaOption.Include + }); + const functionCalls = sortAndDeduplicate(groupedReferences.functionCalls, /*comparer*/ (a, b) => compareValues(a.pos, b.pos)); + for (const call of functionCalls) { + if (call.arguments && call.arguments.length) { + const newArgument = getSynthesizedDeepClone(createNewArgument(functionDeclaration, call.arguments), /*includeTrivia*/ true); + changes.replaceNodeRange(getSourceFileOfNode(call), first(call.arguments), last(call.arguments), newArgument, { leadingTriviaOption: LeadingTriviaOption.IncludeAll, trailingTriviaOption: TrailingTriviaOption.Include }); } - - return { edits: [] }; // TODO: GH#30113 } - - function doChange( - sourceFile: SourceFile, - program: Program, - host: LanguageServiceHost, - changes: textChanges.ChangeTracker, - functionDeclaration: ValidFunctionDeclaration, - groupedReferences: GroupedReferences): void { - const newParamDeclaration = map(createNewParameters(functionDeclaration, program, host), param => getSynthesizedDeepClone(param)); - changes.replaceNodeRangeWithNodes( - sourceFile, - first(functionDeclaration.parameters), - last(functionDeclaration.parameters), - newParamDeclaration, - { joiner: ", ", - // indentation is set to 0 because otherwise the object parameter will be indented if there is a `this` parameter - indentation: 0, - leadingTriviaOption: textChanges.LeadingTriviaOption.IncludeAll, - trailingTriviaOption: textChanges.TrailingTriviaOption.Include - }); - - const functionCalls = sortAndDeduplicate(groupedReferences.functionCalls, /*comparer*/ (a, b) => compareValues(a.pos, b.pos)); - for (const call of functionCalls) { - if (call.arguments && call.arguments.length) { - const newArgument = getSynthesizedDeepClone(createNewArgument(functionDeclaration, call.arguments), /*includeTrivia*/ true); - changes.replaceNodeRange( - getSourceFileOfNode(call), - first(call.arguments), - last(call.arguments), - newArgument, - { leadingTriviaOption: textChanges.LeadingTriviaOption.IncludeAll, trailingTriviaOption: textChanges.TrailingTriviaOption.Include }); +} +/* @internal */ +function getGroupedReferences(functionDeclaration: ValidFunctionDeclaration, program: Program, cancellationToken: CancellationToken): GroupedReferences { + const functionNames = getFunctionNames(functionDeclaration); + const classNames = isConstructorDeclaration(functionDeclaration) ? getClassNames(functionDeclaration) : []; + const names = deduplicate([...functionNames, ...classNames], equateValues); + const checker = program.getTypeChecker(); + const references = flatMap(names, /*mapfn*/ /*mapfn*/ /*mapfn*/ /*mapfn*/ /*mapfn*/ /*mapfn*/ /*mapfn*/ /*mapfn*/ name => getReferenceEntriesForNode(-1, name, program, program.getSourceFiles(), cancellationToken)); + const groupedReferences = groupReferences(references); + if (!every(groupedReferences.declarations, /*callback*/ /*callback*/ /*callback*/ /*callback*/ /*callback*/ /*callback*/ /*callback*/ /*callback*/ decl => contains(names, decl))) { + groupedReferences.valid = false; + } + return groupedReferences; + function groupReferences(referenceEntries: readonly Entry[]): GroupedReferences { + const classReferences: ClassReferences = { accessExpressions: [], typeUsages: [] }; + const groupedReferences: GroupedReferences = { functionCalls: [], declarations: [], classReferences, valid: true }; + const functionSymbols = map(functionNames, getSymbolTargetAtLocation); + const classSymbols = map(classNames, getSymbolTargetAtLocation); + const isConstructor = isConstructorDeclaration(functionDeclaration); + for (const entry of referenceEntries) { + if (entry.kind !== EntryKind.Node) { + groupedReferences.valid = false; + continue; } - } - } - - function getGroupedReferences(functionDeclaration: ValidFunctionDeclaration, program: Program, cancellationToken: CancellationToken): GroupedReferences { - const functionNames = getFunctionNames(functionDeclaration); - const classNames = isConstructorDeclaration(functionDeclaration) ? getClassNames(functionDeclaration) : []; - const names = deduplicate([...functionNames, ...classNames], equateValues); - const checker = program.getTypeChecker(); - - const references = flatMap(names, /*mapfn*/ name => FindAllReferences.getReferenceEntriesForNode(-1, name, program, program.getSourceFiles(), cancellationToken)); - const groupedReferences = groupReferences(references); - - if (!every(groupedReferences.declarations, /*callback*/ decl => contains(names, decl))) { - groupedReferences.valid = false; - } - - return groupedReferences; - - function groupReferences(referenceEntries: readonly FindAllReferences.Entry[]): GroupedReferences { - const classReferences: ClassReferences = { accessExpressions: [], typeUsages: [] }; - const groupedReferences: GroupedReferences = { functionCalls: [], declarations: [], classReferences, valid: true }; - const functionSymbols = map(functionNames, getSymbolTargetAtLocation); - const classSymbols = map(classNames, getSymbolTargetAtLocation); - const isConstructor = isConstructorDeclaration(functionDeclaration); - - for (const entry of referenceEntries) { - if (entry.kind !== FindAllReferences.EntryKind.Node) { - groupedReferences.valid = false; + /* We compare symbols because in some cases find all references wil return a reference that may or may not be to the refactored function. + Example from the refactorConvertParamsToDestructuredObject_methodCallUnion.ts test: + class A { foo(a: number, b: number) { return a + b; } } + class B { foo(c: number, d: number) { return c + d; } } + declare const ab: A | B; + ab.foo(1, 2); + Find all references will return `ab.foo(1, 2)` as a reference to A's `foo` but we could be calling B's `foo`. + When looking for constructor calls, however, the symbol on the constructor call reference is going to be the corresponding class symbol. + So we need to add a special case for this because when calling a constructor of a class through one of its subclasses, + the symbols are going to be different. + */ + if (contains(functionSymbols, getSymbolTargetAtLocation(entry.node)) || isNewExpressionTarget(entry.node)) { + const importOrExportReference = entryToImportOrExport(entry); + if (importOrExportReference) { continue; } - - /* We compare symbols because in some cases find all references wil return a reference that may or may not be to the refactored function. - Example from the refactorConvertParamsToDestructuredObject_methodCallUnion.ts test: - class A { foo(a: number, b: number) { return a + b; } } - class B { foo(c: number, d: number) { return c + d; } } - declare const ab: A | B; - ab.foo(1, 2); - Find all references will return `ab.foo(1, 2)` as a reference to A's `foo` but we could be calling B's `foo`. - When looking for constructor calls, however, the symbol on the constructor call reference is going to be the corresponding class symbol. - So we need to add a special case for this because when calling a constructor of a class through one of its subclasses, - the symbols are going to be different. - */ - if (contains(functionSymbols, getSymbolTargetAtLocation(entry.node)) || isNewExpressionTarget(entry.node)) { - const importOrExportReference = entryToImportOrExport(entry); - if (importOrExportReference) { - continue; - } - const decl = entryToDeclaration(entry); - if (decl) { - groupedReferences.declarations.push(decl); - continue; - } - - const call = entryToFunctionCall(entry); - if (call) { - groupedReferences.functionCalls.push(call); - continue; - } + const decl = entryToDeclaration(entry); + if (decl) { + groupedReferences.declarations.push(decl); + continue; } - // if the refactored function is a constructor, we must also check if the references to its class are valid - if (isConstructor && contains(classSymbols, getSymbolTargetAtLocation(entry.node))) { - const importOrExportReference = entryToImportOrExport(entry); - if (importOrExportReference) { - continue; - } - - const decl = entryToDeclaration(entry); - if (decl) { - groupedReferences.declarations.push(decl); - continue; - } - - const accessExpression = entryToAccessExpression(entry); - if (accessExpression) { - classReferences.accessExpressions.push(accessExpression); + const call = entryToFunctionCall(entry); + if (call) { + groupedReferences.functionCalls.push(call); + continue; + } + } + // if the refactored function is a constructor, we must also check if the references to its class are valid + if (isConstructor && contains(classSymbols, getSymbolTargetAtLocation(entry.node))) { + const importOrExportReference = entryToImportOrExport(entry); + if (importOrExportReference) { + continue; + } + const decl = entryToDeclaration(entry); + if (decl) { + groupedReferences.declarations.push(decl); + continue; + } + const accessExpression = entryToAccessExpression(entry); + if (accessExpression) { + classReferences.accessExpressions.push(accessExpression); + continue; + } + // Only class declarations are allowed to be used as a type (in a heritage clause), + // otherwise `findAllReferences` might not be able to track constructor calls. + if (isClassDeclaration(functionDeclaration.parent)) { + const type = entryToType(entry); + if (type) { + classReferences.typeUsages.push(type); continue; } - - // Only class declarations are allowed to be used as a type (in a heritage clause), - // otherwise `findAllReferences` might not be able to track constructor calls. - if (isClassDeclaration(functionDeclaration.parent)) { - const type = entryToType(entry); - if (type) { - classReferences.typeUsages.push(type); - continue; - } - } } - groupedReferences.valid = false; } - - return groupedReferences; - } - - function getSymbolTargetAtLocation(node: Node) { - const symbol = checker.getSymbolAtLocation(node); - return symbol && getSymbolTarget(symbol, checker); + groupedReferences.valid = false; } + return groupedReferences; } - - function entryToImportOrExport(entry: FindAllReferences.NodeEntry): Node | undefined { - const node = entry.node; - - if (isImportSpecifier(node.parent) - || isImportClause(node.parent) - || isImportEqualsDeclaration(node.parent) - || isNamespaceImport(node.parent)) { - return node; - } - - if (isExportSpecifier(node.parent) || isExportAssignment(node.parent)) { - return node; - } - return undefined; + function getSymbolTargetAtLocation(node: Node) { + const symbol = checker.getSymbolAtLocation(node); + return symbol && getSymbolTarget(symbol, checker); } - - function entryToDeclaration(entry: FindAllReferences.NodeEntry): Node | undefined { - if (isDeclaration(entry.node.parent)) { - return entry.node; - } - return undefined; +} +/* @internal */ +function entryToImportOrExport(entry: NodeEntry): Node | undefined { + const node = entry.node; + if (isImportSpecifier(node.parent) + || isImportClause(node.parent) + || isImportEqualsDeclaration(node.parent) + || isNamespaceImport(node.parent)) { + return node; + } + if (isExportSpecifier(node.parent) || isExportAssignment(node.parent)) { + return node; + } + return undefined; +} +/* @internal */ +function entryToDeclaration(entry: NodeEntry): Node | undefined { + if (isDeclaration(entry.node.parent)) { + return entry.node; } - - function entryToFunctionCall(entry: FindAllReferences.NodeEntry): CallExpression | NewExpression | undefined { - if (entry.node.parent) { - const functionReference = entry.node; - const parent = functionReference.parent; - switch (parent.kind) { - // foo(...) or super(...) or new Foo(...) - case SyntaxKind.CallExpression: - case SyntaxKind.NewExpression: - const callOrNewExpression = tryCast(parent, isCallOrNewExpression); - if (callOrNewExpression && callOrNewExpression.expression === functionReference) { + return undefined; +} +/* @internal */ +function entryToFunctionCall(entry: NodeEntry): CallExpression | NewExpression | undefined { + if (entry.node.parent) { + const functionReference = entry.node; + const parent = functionReference.parent; + switch (parent.kind) { + // foo(...) or super(...) or new Foo(...) + case SyntaxKind.CallExpression: + case SyntaxKind.NewExpression: + const callOrNewExpression = tryCast(parent, isCallOrNewExpression); + if (callOrNewExpression && callOrNewExpression.expression === functionReference) { + return callOrNewExpression; + } + break; + // x.foo(...) + case SyntaxKind.PropertyAccessExpression: + const propertyAccessExpression = tryCast(parent, isPropertyAccessExpression); + if (propertyAccessExpression && propertyAccessExpression.parent && propertyAccessExpression.name === functionReference) { + const callOrNewExpression = tryCast(propertyAccessExpression.parent, isCallOrNewExpression); + if (callOrNewExpression && callOrNewExpression.expression === propertyAccessExpression) { return callOrNewExpression; } - break; - // x.foo(...) - case SyntaxKind.PropertyAccessExpression: - const propertyAccessExpression = tryCast(parent, isPropertyAccessExpression); - if (propertyAccessExpression && propertyAccessExpression.parent && propertyAccessExpression.name === functionReference) { - const callOrNewExpression = tryCast(propertyAccessExpression.parent, isCallOrNewExpression); - if (callOrNewExpression && callOrNewExpression.expression === propertyAccessExpression) { - return callOrNewExpression; - } - } - break; - // x["foo"](...) - case SyntaxKind.ElementAccessExpression: - const elementAccessExpression = tryCast(parent, isElementAccessExpression); - if (elementAccessExpression && elementAccessExpression.parent && elementAccessExpression.argumentExpression === functionReference) { - const callOrNewExpression = tryCast(elementAccessExpression.parent, isCallOrNewExpression); - if (callOrNewExpression && callOrNewExpression.expression === elementAccessExpression) { - return callOrNewExpression; - } - } - break; - } - } - return undefined; - } - - function entryToAccessExpression(entry: FindAllReferences.NodeEntry): ElementAccessExpression | PropertyAccessExpression | undefined { - if (entry.node.parent) { - const reference = entry.node; - const parent = reference.parent; - switch (parent.kind) { - // `C.foo` - case SyntaxKind.PropertyAccessExpression: - const propertyAccessExpression = tryCast(parent, isPropertyAccessExpression); - if (propertyAccessExpression && propertyAccessExpression.expression === reference) { - return propertyAccessExpression; - } - break; - // `C["foo"]` - case SyntaxKind.ElementAccessExpression: - const elementAccessExpression = tryCast(parent, isElementAccessExpression); - if (elementAccessExpression && elementAccessExpression.expression === reference) { - return elementAccessExpression; + } + break; + // x["foo"](...) + case SyntaxKind.ElementAccessExpression: + const elementAccessExpression = tryCast(parent, isElementAccessExpression); + if (elementAccessExpression && elementAccessExpression.parent && elementAccessExpression.argumentExpression === functionReference) { + const callOrNewExpression = tryCast(elementAccessExpression.parent, isCallOrNewExpression); + if (callOrNewExpression && callOrNewExpression.expression === elementAccessExpression) { + return callOrNewExpression; } - break; - } + } + break; } - return undefined; } - - function entryToType(entry: FindAllReferences.NodeEntry): Node | undefined { + return undefined; +} +/* @internal */ +function entryToAccessExpression(entry: NodeEntry): ElementAccessExpression | PropertyAccessExpression | undefined { + if (entry.node.parent) { const reference = entry.node; - if (getMeaningFromLocation(reference) === SemanticMeaning.Type || isExpressionWithTypeArgumentsInClassExtendsClause(reference.parent)) { - return reference; - } - return undefined; - } - - function getFunctionDeclarationAtPosition(file: SourceFile, startPosition: number, checker: TypeChecker): ValidFunctionDeclaration | undefined { - const node = getTouchingToken(file, startPosition); - const functionDeclaration = getContainingFunctionDeclaration(node); - - // don't offer refactor on top-level JSDoc - if (isTopLevelJSDoc(node)) return undefined; - - if (functionDeclaration - && isValidFunctionDeclaration(functionDeclaration, checker) - && rangeContainsRange(functionDeclaration, node) - && !(functionDeclaration.body && rangeContainsRange(functionDeclaration.body, node))) return functionDeclaration; - - return undefined; - } - - function isTopLevelJSDoc(node: Node): boolean { - const containingJSDoc = findAncestor(node, isJSDocNode); - if (containingJSDoc) { - const containingNonJSDoc = findAncestor(containingJSDoc, n => !isJSDocNode(n)); - return !!containingNonJSDoc && isFunctionLikeDeclaration(containingNonJSDoc); - } - return false; - } - - function isValidFunctionDeclaration( - functionDeclaration: FunctionLikeDeclaration, - checker: TypeChecker): functionDeclaration is ValidFunctionDeclaration { - if (!isValidParameterNodeArray(functionDeclaration.parameters, checker)) return false; - switch (functionDeclaration.kind) { - case SyntaxKind.FunctionDeclaration: - return hasNameOrDefault(functionDeclaration) && isSingleImplementation(functionDeclaration, checker); - case SyntaxKind.MethodDeclaration: - return isSingleImplementation(functionDeclaration, checker); - case SyntaxKind.Constructor: - if (isClassDeclaration(functionDeclaration.parent)) { - return hasNameOrDefault(functionDeclaration.parent) && isSingleImplementation(functionDeclaration, checker); + const parent = reference.parent; + switch (parent.kind) { + // `C.foo` + case SyntaxKind.PropertyAccessExpression: + const propertyAccessExpression = tryCast(parent, isPropertyAccessExpression); + if (propertyAccessExpression && propertyAccessExpression.expression === reference) { + return propertyAccessExpression; } - else { - return isValidVariableDeclaration(functionDeclaration.parent.parent) - && isSingleImplementation(functionDeclaration, checker); + break; + // `C["foo"]` + case SyntaxKind.ElementAccessExpression: + const elementAccessExpression = tryCast(parent, isElementAccessExpression); + if (elementAccessExpression && elementAccessExpression.expression === reference) { + return elementAccessExpression; } - case SyntaxKind.FunctionExpression: - case SyntaxKind.ArrowFunction: - return isValidVariableDeclaration(functionDeclaration.parent); + break; } - return false; } - - function isSingleImplementation(functionDeclaration: FunctionLikeDeclaration, checker: TypeChecker): boolean { - return !!functionDeclaration.body && !checker.isImplementationOfOverload(functionDeclaration); + return undefined; +} +/* @internal */ +function entryToType(entry: NodeEntry): Node | undefined { + const reference = entry.node; + if (getMeaningFromLocation(reference) === SemanticMeaning.Type || isExpressionWithTypeArgumentsInClassExtendsClause(reference.parent)) { + return reference; } - - function hasNameOrDefault(functionOrClassDeclaration: FunctionDeclaration | ClassDeclaration): boolean { - if (!functionOrClassDeclaration.name) { - const defaultKeyword = findModifier(functionOrClassDeclaration, SyntaxKind.DefaultKeyword); - return !!defaultKeyword; - } - return true; + return undefined; +} +/* @internal */ +function getFunctionDeclarationAtPosition(file: SourceFile, startPosition: number, checker: TypeChecker): ValidFunctionDeclaration | undefined { + const node = getTouchingToken(file, startPosition); + const functionDeclaration = getContainingFunctionDeclaration(node); + // don't offer refactor on top-level JSDoc + if (isTopLevelJSDoc(node)) + return undefined; + if (functionDeclaration + && isValidFunctionDeclaration(functionDeclaration, checker) + && rangeContainsRange(functionDeclaration, node) + && !(functionDeclaration.body && rangeContainsRange(functionDeclaration.body, node))) + return functionDeclaration; + return undefined; +} +/* @internal */ +function isTopLevelJSDoc(node: Node): boolean { + const containingJSDoc = findAncestor(node, isJSDocNode); + if (containingJSDoc) { + const containingNonJSDoc = findAncestor(containingJSDoc, n => !isJSDocNode(n)); + return !!containingNonJSDoc && isFunctionLikeDeclaration(containingNonJSDoc); } - - function isValidParameterNodeArray( - parameters: NodeArray, - checker: TypeChecker): parameters is ValidParameterNodeArray { - return getRefactorableParametersLength(parameters) >= minimumParameterLength - && every(parameters, /*callback*/ paramDecl => isValidParameterDeclaration(paramDecl, checker)); + return false; +} +/* @internal */ +function isValidFunctionDeclaration(functionDeclaration: FunctionLikeDeclaration, checker: TypeChecker): functionDeclaration is ValidFunctionDeclaration { + if (!isValidParameterNodeArray(functionDeclaration.parameters, checker)) + return false; + switch (functionDeclaration.kind) { + case SyntaxKind.FunctionDeclaration: + return hasNameOrDefault(functionDeclaration) && isSingleImplementation(functionDeclaration, checker); + case SyntaxKind.MethodDeclaration: + return isSingleImplementation(functionDeclaration, checker); + case SyntaxKind.Constructor: + if (isClassDeclaration(functionDeclaration.parent)) { + return hasNameOrDefault(functionDeclaration.parent) && isSingleImplementation(functionDeclaration, checker); + } + else { + return isValidVariableDeclaration(functionDeclaration.parent.parent) + && isSingleImplementation(functionDeclaration, checker); + } + case SyntaxKind.FunctionExpression: + case SyntaxKind.ArrowFunction: + return isValidVariableDeclaration(functionDeclaration.parent); } - - function isValidParameterDeclaration( - parameterDeclaration: ParameterDeclaration, - checker: TypeChecker): parameterDeclaration is ValidParameterDeclaration { - if (isRestParameter(parameterDeclaration)) { - const type = checker.getTypeAtLocation(parameterDeclaration); - if (!checker.isArrayType(type) && !checker.isTupleType(type)) return false; - } - return !parameterDeclaration.modifiers && !parameterDeclaration.decorators && isIdentifier(parameterDeclaration.name); + return false; +} +/* @internal */ +function isSingleImplementation(functionDeclaration: FunctionLikeDeclaration, checker: TypeChecker): boolean { + return !!functionDeclaration.body && !checker.isImplementationOfOverload(functionDeclaration); +} +/* @internal */ +function hasNameOrDefault(functionOrClassDeclaration: FunctionDeclaration | ClassDeclaration): boolean { + if (!functionOrClassDeclaration.name) { + const defaultKeyword = findModifier(functionOrClassDeclaration, SyntaxKind.DefaultKeyword); + return !!defaultKeyword; } - - function isValidVariableDeclaration(node: Node): node is ValidVariableDeclaration { - return isVariableDeclaration(node) && isVarConst(node) && isIdentifier(node.name) && !node.type; // TODO: GH#30113 + return true; +} +/* @internal */ +function isValidParameterNodeArray(parameters: NodeArray, checker: TypeChecker): parameters is ValidParameterNodeArray { + return getRefactorableParametersLength(parameters) >= minimumParameterLength + && every(parameters, /*callback*/ /*callback*/ /*callback*/ /*callback*/ /*callback*/ /*callback*/ /*callback*/ /*callback*/ paramDecl => isValidParameterDeclaration(paramDecl, checker)); +} +/* @internal */ +function isValidParameterDeclaration(parameterDeclaration: ParameterDeclaration, checker: TypeChecker): parameterDeclaration is ValidParameterDeclaration { + if (isRestParameter(parameterDeclaration)) { + const type = checker.getTypeAtLocation(parameterDeclaration); + if (!checker.isArrayType(type) && !checker.isTupleType(type)) + return false; } - - function hasThisParameter(parameters: NodeArray): boolean { - return parameters.length > 0 && isThis(parameters[0].name); + return !parameterDeclaration.modifiers && !parameterDeclaration.decorators && isIdentifier(parameterDeclaration.name); +} +/* @internal */ +function isValidVariableDeclaration(node: Node): node is ValidVariableDeclaration { + return isVariableDeclaration(node) && isVarConst(node) && isIdentifier(node.name) && !node.type; // TODO: GH#30113 +} +/* @internal */ +function hasThisParameter(parameters: NodeArray): boolean { + return parameters.length > 0 && isThis(parameters[0].name); +} +/* @internal */ +function getRefactorableParametersLength(parameters: NodeArray): number { + if (hasThisParameter(parameters)) { + return parameters.length - 1; } - - function getRefactorableParametersLength(parameters: NodeArray): number { - if (hasThisParameter(parameters)) { - return parameters.length - 1; - } - return parameters.length; + return parameters.length; +} +/* @internal */ +function getRefactorableParameters(parameters: NodeArray): NodeArray { + if (hasThisParameter(parameters)) { + parameters = createNodeArray(parameters.slice(1), parameters.hasTrailingComma); } - - function getRefactorableParameters(parameters: NodeArray): NodeArray { - if (hasThisParameter(parameters)) { - parameters = createNodeArray(parameters.slice(1), parameters.hasTrailingComma); - } - return parameters; + return parameters; +} +/* @internal */ +function createPropertyOrShorthandAssignment(name: string, initializer: Expression): PropertyAssignment | ShorthandPropertyAssignment { + if (isIdentifier(initializer) && getTextOfIdentifierOrLiteral(initializer) === name) { + return createShorthandPropertyAssignment(name); } - - function createPropertyOrShorthandAssignment(name: string, initializer: Expression): PropertyAssignment | ShorthandPropertyAssignment { - if (isIdentifier(initializer) && getTextOfIdentifierOrLiteral(initializer) === name) { - return createShorthandPropertyAssignment(name); + return createPropertyAssignment(name, initializer); +} +/* @internal */ +function createNewArgument(functionDeclaration: ValidFunctionDeclaration, functionArguments: NodeArray): ObjectLiteralExpression { + const parameters = getRefactorableParameters(functionDeclaration.parameters); + const hasRestParameter = isRestParameter(last(parameters)); + const nonRestArguments = hasRestParameter ? functionArguments.slice(0, parameters.length - 1) : functionArguments; + const properties = map(nonRestArguments, (arg, i) => { + const parameterName = getParameterName(parameters[i]); + const property = createPropertyOrShorthandAssignment(parameterName, arg); + suppressLeadingAndTrailingTrivia(property.name); + if (isPropertyAssignment(property)) + suppressLeadingAndTrailingTrivia(property.initializer); + copyComments(arg, property); + return property; + }); + if (hasRestParameter && functionArguments.length >= parameters.length) { + const restArguments = functionArguments.slice(parameters.length - 1); + const restProperty = createPropertyAssignment(getParameterName(last(parameters)), createArrayLiteral(restArguments)); + properties.push(restProperty); + } + const objectLiteral = createObjectLiteral(properties, /*multiLine*/ false); + return objectLiteral; +} +/* @internal */ +function createNewParameters(functionDeclaration: ValidFunctionDeclaration, program: Program, host: LanguageServiceHost): NodeArray { + const checker = program.getTypeChecker(); + const refactorableParameters = getRefactorableParameters(functionDeclaration.parameters); + const bindingElements = map(refactorableParameters, createBindingElementFromParameterDeclaration); + const objectParameterName = createObjectBindingPattern(bindingElements); + const objectParameterType = createParameterTypeNode(refactorableParameters); + let objectInitializer: Expression | undefined; + // If every parameter in the original function was optional, add an empty object initializer to the new object parameter + if (every(refactorableParameters, isOptionalParameter)) { + objectInitializer = createObjectLiteral(); + } + const objectParameter = createParameter( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, objectParameterName, + /*questionToken*/ undefined, objectParameterType, objectInitializer); + if (hasThisParameter(functionDeclaration.parameters)) { + const thisParameter = functionDeclaration.parameters[0]; + const newThisParameter = createParameter( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, thisParameter.name, + /*questionToken*/ undefined, thisParameter.type); + suppressLeadingAndTrailingTrivia(newThisParameter.name); + copyComments(thisParameter.name, newThisParameter.name); + if (thisParameter.type) { + suppressLeadingAndTrailingTrivia((newThisParameter.type!)); + copyComments(thisParameter.type, newThisParameter.type!); } - return createPropertyAssignment(name, initializer); - } - - function createNewArgument(functionDeclaration: ValidFunctionDeclaration, functionArguments: NodeArray): ObjectLiteralExpression { - const parameters = getRefactorableParameters(functionDeclaration.parameters); - const hasRestParameter = isRestParameter(last(parameters)); - const nonRestArguments = hasRestParameter ? functionArguments.slice(0, parameters.length - 1) : functionArguments; - const properties = map(nonRestArguments, (arg, i) => { - const parameterName = getParameterName(parameters[i]); - const property = createPropertyOrShorthandAssignment(parameterName, arg); - - suppressLeadingAndTrailingTrivia(property.name); - if (isPropertyAssignment(property)) suppressLeadingAndTrailingTrivia(property.initializer); - copyComments(arg, property); - return property; - }); - - if (hasRestParameter && functionArguments.length >= parameters.length) { - const restArguments = functionArguments.slice(parameters.length - 1); - const restProperty = createPropertyAssignment(getParameterName(last(parameters)), createArrayLiteral(restArguments)); - properties.push(restProperty); + return createNodeArray([newThisParameter, objectParameter]); + } + return createNodeArray([objectParameter]); + function createBindingElementFromParameterDeclaration(parameterDeclaration: ValidParameterDeclaration): BindingElement { + const element = createBindingElement( + /*dotDotDotToken*/ undefined, + /*propertyName*/ undefined, getParameterName(parameterDeclaration), isRestParameter(parameterDeclaration) && isOptionalParameter(parameterDeclaration) ? createArrayLiteral() : parameterDeclaration.initializer); + suppressLeadingAndTrailingTrivia(element); + if (parameterDeclaration.initializer && element.initializer) { + copyComments(parameterDeclaration.initializer, element.initializer); } - - const objectLiteral = createObjectLiteral(properties, /*multiLine*/ false); - return objectLiteral; + return element; } - - function createNewParameters(functionDeclaration: ValidFunctionDeclaration, program: Program, host: LanguageServiceHost): NodeArray { - const checker = program.getTypeChecker(); - const refactorableParameters = getRefactorableParameters(functionDeclaration.parameters); - const bindingElements = map(refactorableParameters, createBindingElementFromParameterDeclaration); - const objectParameterName = createObjectBindingPattern(bindingElements); - const objectParameterType = createParameterTypeNode(refactorableParameters); - - let objectInitializer: Expression | undefined; - // If every parameter in the original function was optional, add an empty object initializer to the new object parameter - if (every(refactorableParameters, isOptionalParameter)) { - objectInitializer = createObjectLiteral(); - } - - const objectParameter = createParameter( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, - objectParameterName, - /*questionToken*/ undefined, - objectParameterType, - objectInitializer); - - if (hasThisParameter(functionDeclaration.parameters)) { - const thisParameter = functionDeclaration.parameters[0]; - const newThisParameter = createParameter( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, - thisParameter.name, - /*questionToken*/ undefined, - thisParameter.type); - - suppressLeadingAndTrailingTrivia(newThisParameter.name); - copyComments(thisParameter.name, newThisParameter.name); - if (thisParameter.type) { - suppressLeadingAndTrailingTrivia(newThisParameter.type!); - copyComments(thisParameter.type, newThisParameter.type!); - } - - return createNodeArray([newThisParameter, objectParameter]); - } - return createNodeArray([objectParameter]); - - function createBindingElementFromParameterDeclaration(parameterDeclaration: ValidParameterDeclaration): BindingElement { - const element = createBindingElement( - /*dotDotDotToken*/ undefined, - /*propertyName*/ undefined, - getParameterName(parameterDeclaration), - isRestParameter(parameterDeclaration) && isOptionalParameter(parameterDeclaration) ? createArrayLiteral() : parameterDeclaration.initializer); - - suppressLeadingAndTrailingTrivia(element); - if (parameterDeclaration.initializer && element.initializer) { - copyComments(parameterDeclaration.initializer, element.initializer); - } - return element; - } - - function createParameterTypeNode(parameters: NodeArray): TypeLiteralNode { - const members = map(parameters, createPropertySignatureFromParameterDeclaration); - const typeNode = addEmitFlags(createTypeLiteralNode(members), EmitFlags.SingleLine); - return typeNode; - } - - function createPropertySignatureFromParameterDeclaration(parameterDeclaration: ValidParameterDeclaration): PropertySignature { - let parameterType = parameterDeclaration.type; - if (!parameterType && (parameterDeclaration.initializer || isRestParameter(parameterDeclaration))) { - parameterType = getTypeNode(parameterDeclaration); - } - - const propertySignature = createPropertySignature( - /*modifiers*/ undefined, - getParameterName(parameterDeclaration), - isOptionalParameter(parameterDeclaration) ? createToken(SyntaxKind.QuestionToken) : parameterDeclaration.questionToken, - parameterType, - /*initializer*/ undefined); - - suppressLeadingAndTrailingTrivia(propertySignature); - copyComments(parameterDeclaration.name, propertySignature.name); - if (parameterDeclaration.type && propertySignature.type) { - copyComments(parameterDeclaration.type, propertySignature.type); - } - - return propertySignature; - } - - function getTypeNode(node: Node): TypeNode | undefined { - const type = checker.getTypeAtLocation(node); - return getTypeNodeIfAccessible(type, node, program, host); - } - - function isOptionalParameter(parameterDeclaration: ValidParameterDeclaration): boolean { - if (isRestParameter(parameterDeclaration)) { - const type = checker.getTypeAtLocation(parameterDeclaration); - return !checker.isTupleType(type); - } - return checker.isOptionalParameter(parameterDeclaration); - } + function createParameterTypeNode(parameters: NodeArray): TypeLiteralNode { + const members = map(parameters, createPropertySignatureFromParameterDeclaration); + const typeNode = addEmitFlags(createTypeLiteralNode(members), EmitFlags.SingleLine); + return typeNode; } - - function copyComments(sourceNode: Node, targetNode: Node) { - const sourceFile = sourceNode.getSourceFile(); - const text = sourceFile.text; - if (hasLeadingLineBreak(sourceNode, text)) { - copyLeadingComments(sourceNode, targetNode, sourceFile); + function createPropertySignatureFromParameterDeclaration(parameterDeclaration: ValidParameterDeclaration): PropertySignature { + let parameterType = parameterDeclaration.type; + if (!parameterType && (parameterDeclaration.initializer || isRestParameter(parameterDeclaration))) { + parameterType = getTypeNode(parameterDeclaration); } - else { - copyTrailingAsLeadingComments(sourceNode, targetNode, sourceFile); + const propertySignature = createPropertySignature( + /*modifiers*/ undefined, getParameterName(parameterDeclaration), isOptionalParameter(parameterDeclaration) ? createToken(SyntaxKind.QuestionToken) : parameterDeclaration.questionToken, parameterType, + /*initializer*/ undefined); + suppressLeadingAndTrailingTrivia(propertySignature); + copyComments(parameterDeclaration.name, propertySignature.name); + if (parameterDeclaration.type && propertySignature.type) { + copyComments(parameterDeclaration.type, propertySignature.type); } - copyTrailingComments(sourceNode, targetNode, sourceFile); - } - - function hasLeadingLineBreak(node: Node, text: string) { - const start = node.getFullStart(); - const end = node.getStart(); - for (let i = start; i < end; i++) { - if (text.charCodeAt(i) === CharacterCodes.lineFeed) return true; - } - return false; + return propertySignature; } - - function getParameterName(paramDeclaration: ValidParameterDeclaration) { - return getTextOfIdentifierOrLiteral(paramDeclaration.name); + function getTypeNode(node: Node): TypeNode | undefined { + const type = checker.getTypeAtLocation(node); + return getTypeNodeIfAccessible(type, node, program, host); } - - function getClassNames(constructorDeclaration: ValidConstructor): (Identifier | Modifier)[] { - switch (constructorDeclaration.parent.kind) { - case SyntaxKind.ClassDeclaration: - const classDeclaration = constructorDeclaration.parent; - if (classDeclaration.name) return [classDeclaration.name]; - // If the class declaration doesn't have a name, it should have a default modifier. - // We validated this in `isValidFunctionDeclaration` through `hasNameOrDefault` - const defaultModifier = Debug.checkDefined( - findModifier(classDeclaration, SyntaxKind.DefaultKeyword), - "Nameless class declaration should be a default export"); - return [defaultModifier]; - case SyntaxKind.ClassExpression: - const classExpression = constructorDeclaration.parent; - const variableDeclaration = constructorDeclaration.parent.parent; - const className = classExpression.name; - if (className) return [className, variableDeclaration.name]; - return [variableDeclaration.name]; - } - } - - function getFunctionNames(functionDeclaration: ValidFunctionDeclaration): Node[] { - switch (functionDeclaration.kind) { - case SyntaxKind.FunctionDeclaration: - if (functionDeclaration.name) return [functionDeclaration.name]; - // If the function declaration doesn't have a name, it should have a default modifier. - // We validated this in `isValidFunctionDeclaration` through `hasNameOrDefault` - const defaultModifier = Debug.checkDefined( - findModifier(functionDeclaration, SyntaxKind.DefaultKeyword), - "Nameless function declaration should be a default export"); - return [defaultModifier]; - case SyntaxKind.MethodDeclaration: - return [functionDeclaration.name]; - case SyntaxKind.Constructor: - const ctrKeyword = Debug.checkDefined( - findChildOfKind(functionDeclaration, SyntaxKind.ConstructorKeyword, functionDeclaration.getSourceFile()), - "Constructor declaration should have constructor keyword"); - if (functionDeclaration.parent.kind === SyntaxKind.ClassExpression) { - const variableDeclaration = functionDeclaration.parent.parent; - return [variableDeclaration.name, ctrKeyword]; - } - return [ctrKeyword]; - case SyntaxKind.ArrowFunction: - return [functionDeclaration.parent.name]; - case SyntaxKind.FunctionExpression: - if (functionDeclaration.name) return [functionDeclaration.name, functionDeclaration.parent.name]; - return [functionDeclaration.parent.name]; - default: - return Debug.assertNever(functionDeclaration, `Unexpected function declaration kind ${(functionDeclaration as ValidFunctionDeclaration).kind}`); + function isOptionalParameter(parameterDeclaration: ValidParameterDeclaration): boolean { + if (isRestParameter(parameterDeclaration)) { + const type = checker.getTypeAtLocation(parameterDeclaration); + return !checker.isTupleType(type); } + return checker.isOptionalParameter(parameterDeclaration); } - - type ValidParameterNodeArray = NodeArray; - - interface ValidVariableDeclaration extends VariableDeclaration { - name: Identifier; - type: undefined; - } - - interface ValidConstructor extends ConstructorDeclaration { - parent: ClassDeclaration | (ClassExpression & { parent: ValidVariableDeclaration }); - parameters: NodeArray; - body: FunctionBody; +} +/* @internal */ +function copyComments(sourceNode: Node, targetNode: Node) { + const sourceFile = sourceNode.getSourceFile(); + const text = sourceFile.text; + if (hasLeadingLineBreak(sourceNode, text)) { + copyLeadingComments(sourceNode, targetNode, sourceFile); } - - interface ValidFunction extends FunctionDeclaration { - parameters: NodeArray; - body: FunctionBody; + else { + copyTrailingAsLeadingComments(sourceNode, targetNode, sourceFile); } - - interface ValidMethod extends MethodDeclaration { - parameters: NodeArray; - body: FunctionBody; + copyTrailingComments(sourceNode, targetNode, sourceFile); +} +/* @internal */ +function hasLeadingLineBreak(node: Node, text: string) { + const start = node.getFullStart(); + const end = node.getStart(); + for (let i = start; i < end; i++) { + if (text.charCodeAt(i) === CharacterCodes.lineFeed) + return true; + } + return false; +} +/* @internal */ +function getParameterName(paramDeclaration: ValidParameterDeclaration) { + return getTextOfIdentifierOrLiteral(paramDeclaration.name); +} +/* @internal */ +function getClassNames(constructorDeclaration: ValidConstructor): (Identifier | Modifier)[] { + switch (constructorDeclaration.parent.kind) { + case SyntaxKind.ClassDeclaration: + const classDeclaration = constructorDeclaration.parent; + if (classDeclaration.name) + return [classDeclaration.name]; + // If the class declaration doesn't have a name, it should have a default modifier. + // We validated this in `isValidFunctionDeclaration` through `hasNameOrDefault` + const defaultModifier = Debug.checkDefined(findModifier(classDeclaration, SyntaxKind.DefaultKeyword), "Nameless class declaration should be a default export"); + return [defaultModifier]; + case SyntaxKind.ClassExpression: + const classExpression = constructorDeclaration.parent; + const variableDeclaration = constructorDeclaration.parent.parent; + const className = classExpression.name; + if (className) + return [className, variableDeclaration.name]; + return [variableDeclaration.name]; } - - interface ValidFunctionExpression extends FunctionExpression { - parent: ValidVariableDeclaration; - parameters: NodeArray; +} +/* @internal */ +function getFunctionNames(functionDeclaration: ValidFunctionDeclaration): Node[] { + switch (functionDeclaration.kind) { + case SyntaxKind.FunctionDeclaration: + if (functionDeclaration.name) + return [functionDeclaration.name]; + // If the function declaration doesn't have a name, it should have a default modifier. + // We validated this in `isValidFunctionDeclaration` through `hasNameOrDefault` + const defaultModifier = Debug.checkDefined(findModifier(functionDeclaration, SyntaxKind.DefaultKeyword), "Nameless function declaration should be a default export"); + return [defaultModifier]; + case SyntaxKind.MethodDeclaration: + return [functionDeclaration.name]; + case SyntaxKind.Constructor: + const ctrKeyword = Debug.checkDefined(findChildOfKind(functionDeclaration, SyntaxKind.ConstructorKeyword, functionDeclaration.getSourceFile()), "Constructor declaration should have constructor keyword"); + if (functionDeclaration.parent.kind === SyntaxKind.ClassExpression) { + const variableDeclaration = functionDeclaration.parent.parent; + return [variableDeclaration.name, ctrKeyword]; + } + return [ctrKeyword]; + case SyntaxKind.ArrowFunction: + return [functionDeclaration.parent.name]; + case SyntaxKind.FunctionExpression: + if (functionDeclaration.name) + return [functionDeclaration.name, functionDeclaration.parent.name]; + return [functionDeclaration.parent.name]; + default: + return Debug.assertNever(functionDeclaration, `Unexpected function declaration kind ${(functionDeclaration as ValidFunctionDeclaration).kind}`); } - - interface ValidArrowFunction extends ArrowFunction { +} +/* @internal */ +type ValidParameterNodeArray = NodeArray; +/* @internal */ +interface ValidVariableDeclaration extends VariableDeclaration { + name: Identifier; + type: undefined; +} +/* @internal */ +interface ValidConstructor extends ConstructorDeclaration { + parent: ClassDeclaration | (ClassExpression & { parent: ValidVariableDeclaration; - parameters: NodeArray; - } - - type ValidFunctionDeclaration = ValidConstructor | ValidFunction | ValidMethod | ValidArrowFunction | ValidFunctionExpression; - - interface ValidParameterDeclaration extends ParameterDeclaration { - name: Identifier; - modifiers: undefined; - decorators: undefined; - } - - interface GroupedReferences { - functionCalls: (CallExpression | NewExpression)[]; - declarations: Node[]; - classReferences?: ClassReferences; - valid: boolean; - } - interface ClassReferences { - accessExpressions: Node[]; - typeUsages: Node[]; - } + }); + parameters: NodeArray; + body: FunctionBody; +} +/* @internal */ +interface ValidFunction extends FunctionDeclaration { + parameters: NodeArray; + body: FunctionBody; +} +/* @internal */ +interface ValidMethod extends MethodDeclaration { + parameters: NodeArray; + body: FunctionBody; +} +/* @internal */ +interface ValidFunctionExpression extends FunctionExpression { + parent: ValidVariableDeclaration; + parameters: NodeArray; +} +/* @internal */ +interface ValidArrowFunction extends ArrowFunction { + parent: ValidVariableDeclaration; + parameters: NodeArray; +} +/* @internal */ +type ValidFunctionDeclaration = ValidConstructor | ValidFunction | ValidMethod | ValidArrowFunction | ValidFunctionExpression; +/* @internal */ +interface ValidParameterDeclaration extends ParameterDeclaration { + name: Identifier; + modifiers: undefined; + decorators: undefined; +} +/* @internal */ +interface GroupedReferences { + functionCalls: (CallExpression | NewExpression)[]; + declarations: Node[]; + classReferences?: ClassReferences; + valid: boolean; +} +/* @internal */ +interface ClassReferences { + accessExpressions: Node[]; + typeUsages: Node[]; } diff --git a/src/services/refactors/convertStringOrTemplateLiteral.ts b/src/services/refactors/convertStringOrTemplateLiteral.ts index 58a62a77d7e3c..b93b9e8e73237 100644 --- a/src/services/refactors/convertStringOrTemplateLiteral.ts +++ b/src/services/refactors/convertStringOrTemplateLiteral.ts @@ -1,186 +1,174 @@ +import { getLocaleSpecificMessage, Diagnostics, RefactorContext, ApplicableRefactorInfo, isBinaryExpression, emptyArray, SourceFile, getTokenAtPosition, isParenthesizedExpression, RefactorEditInfo, Debug, Node, getTrailingCommentRanges, BinaryExpression, SyntaxKind, Expression, Token, BinaryOperator, isStringLiteral, copyTrailingComments, StringLiteral, escapeString, createNoSubstitutionTemplateLiteral, TemplateSpan, createTemplateHead, createTemplateTail, createTemplateMiddle, createTemplateSpan, createTemplateExpression, ParenthesizedExpression, copyTrailingAsLeadingComments } from "../ts"; +import { registerRefactor } from "../ts.refactor"; +import { ChangeTracker } from "../ts.textChanges"; /* @internal */ -namespace ts.refactor.convertStringOrTemplateLiteral { - const refactorName = "Convert to template string"; - const refactorDescription = getLocaleSpecificMessage(Diagnostics.Convert_to_template_string); - - registerRefactor(refactorName, { getEditsForAction, getAvailableActions }); - - function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] { - const { file, startPosition } = context; - const node = getNodeOrParentOfParentheses(file, startPosition); - const maybeBinary = getParentBinaryExpression(node); - const refactorInfo: ApplicableRefactorInfo = { name: refactorName, description: refactorDescription, actions: [] }; - - if (isBinaryExpression(maybeBinary) && isStringConcatenationValid(maybeBinary)) { - refactorInfo.actions.push({ name: refactorName, description: refactorDescription }); - return [refactorInfo]; - } - return emptyArray; +const refactorName = "Convert to template string"; +/* @internal */ +const refactorDescription = getLocaleSpecificMessage(Diagnostics.Convert_to_template_string); +/* @internal */ +registerRefactor(refactorName, { getEditsForAction, getAvailableActions }); +/* @internal */ +function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] { + const { file, startPosition } = context; + const node = getNodeOrParentOfParentheses(file, startPosition); + const maybeBinary = getParentBinaryExpression(node); + const refactorInfo: ApplicableRefactorInfo = { name: refactorName, description: refactorDescription, actions: [] }; + if (isBinaryExpression(maybeBinary) && isStringConcatenationValid(maybeBinary)) { + refactorInfo.actions.push({ name: refactorName, description: refactorDescription }); + return [refactorInfo]; } - - function getNodeOrParentOfParentheses(file: SourceFile, startPosition: number) { - const node = getTokenAtPosition(file, startPosition); - const nestedBinary = getParentBinaryExpression(node); - const isNonStringBinary = !isStringConcatenationValid(nestedBinary); - - if ( - isNonStringBinary && - isParenthesizedExpression(nestedBinary.parent) && - isBinaryExpression(nestedBinary.parent.parent) - ) { - return nestedBinary.parent.parent; - } - return node; + return emptyArray; +} +/* @internal */ +function getNodeOrParentOfParentheses(file: SourceFile, startPosition: number) { + const node = getTokenAtPosition(file, startPosition); + const nestedBinary = getParentBinaryExpression(node); + const isNonStringBinary = !isStringConcatenationValid(nestedBinary); + if (isNonStringBinary && + isParenthesizedExpression(nestedBinary.parent) && + isBinaryExpression(nestedBinary.parent.parent)) { + return nestedBinary.parent.parent; } - - function getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined { - const { file, startPosition } = context; - const node = getNodeOrParentOfParentheses(file, startPosition); - - switch (actionName) { - case refactorDescription: - return { edits: getEditsForToTemplateLiteral(context, node) }; - default: - return Debug.fail("invalid action"); - } + return node; +} +/* @internal */ +function getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined { + const { file, startPosition } = context; + const node = getNodeOrParentOfParentheses(file, startPosition); + switch (actionName) { + case refactorDescription: + return { edits: getEditsForToTemplateLiteral(context, node) }; + default: + return Debug.fail("invalid action"); } - - function getEditsForToTemplateLiteral(context: RefactorContext, node: Node) { - const maybeBinary = getParentBinaryExpression(node); - const file = context.file; - - const templateLiteral = nodesToTemplate(treeToArray(maybeBinary), file); - const trailingCommentRanges = getTrailingCommentRanges(file.text, maybeBinary.end); - - if (trailingCommentRanges) { - const lastComment = trailingCommentRanges[trailingCommentRanges.length - 1]; - const trailingRange = { pos: trailingCommentRanges[0].pos, end: lastComment.end }; - - // since suppressTrailingTrivia(maybeBinary) does not work, the trailing comment is removed manually - // otherwise it would have the trailing comment twice - return textChanges.ChangeTracker.with(context, t => { - t.deleteRange(file, trailingRange); - t.replaceNode(file, maybeBinary, templateLiteral); - }); - } - else { - return textChanges.ChangeTracker.with(context, t => t.replaceNode(file, maybeBinary, templateLiteral)); - } +} +/* @internal */ +function getEditsForToTemplateLiteral(context: RefactorContext, node: Node) { + const maybeBinary = getParentBinaryExpression(node); + const file = context.file; + const templateLiteral = nodesToTemplate(treeToArray(maybeBinary), file); + const trailingCommentRanges = getTrailingCommentRanges(file.text, maybeBinary.end); + if (trailingCommentRanges) { + const lastComment = trailingCommentRanges[trailingCommentRanges.length - 1]; + const trailingRange = { pos: trailingCommentRanges[0].pos, end: lastComment.end }; + // since suppressTrailingTrivia(maybeBinary) does not work, the trailing comment is removed manually + // otherwise it would have the trailing comment twice + return ChangeTracker.with(context, t => { + t.deleteRange(file, trailingRange); + t.replaceNode(file, maybeBinary, templateLiteral); + }); + } + else { + return ChangeTracker.with(context, t => t.replaceNode(file, maybeBinary, templateLiteral)); } - - function isNotEqualsOperator(node: BinaryExpression) { - return node.operatorToken.kind !== SyntaxKind.EqualsToken; +} +/* @internal */ +function isNotEqualsOperator(node: BinaryExpression) { + return node.operatorToken.kind !== SyntaxKind.EqualsToken; +} +/* @internal */ +function getParentBinaryExpression(expr: Node) { + while (isBinaryExpression(expr.parent) && isNotEqualsOperator(expr.parent)) { + expr = expr.parent; } - - function getParentBinaryExpression(expr: Node) { - while (isBinaryExpression(expr.parent) && isNotEqualsOperator(expr.parent)) { - expr = expr.parent; + return expr; +} +/* @internal */ +function isStringConcatenationValid(node: Node): boolean { + const { containsString, areOperatorsValid } = treeToArray(node); + return containsString && areOperatorsValid; +} +/* @internal */ +function treeToArray(current: Node): { + nodes: Expression[]; + operators: Token[]; + containsString: boolean; + areOperatorsValid: boolean; +} { + if (isBinaryExpression(current)) { + const { nodes, operators, containsString: leftHasString, areOperatorsValid: leftOperatorValid } = treeToArray(current.left); + if (!leftHasString && !isStringLiteral(current.right)) { + return { nodes: [current], operators: [], containsString: false, areOperatorsValid: true }; } - return expr; + const currentOperatorValid = current.operatorToken.kind === SyntaxKind.PlusToken; + const areOperatorsValid = leftOperatorValid && currentOperatorValid; + nodes.push(current.right); + operators.push(current.operatorToken); + return { nodes, operators, containsString: true, areOperatorsValid }; } - - function isStringConcatenationValid(node: Node): boolean { - const { containsString, areOperatorsValid } = treeToArray(node); - return containsString && areOperatorsValid; + return { nodes: [(current as Expression)], operators: [], containsString: isStringLiteral(current), areOperatorsValid: true }; +} +// to copy comments following the operator +// "foo" + /* comment */ "bar" +/* @internal */ +const copyTrailingOperatorComments = (operators: Token[], file: SourceFile) => (index: number, targetNode: Node) => { + if (index < operators.length) { + copyTrailingComments(operators[index], targetNode, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); } - - function treeToArray(current: Node): { nodes: Expression[], operators: Token[], containsString: boolean, areOperatorsValid: boolean} { - if (isBinaryExpression(current)) { - const { nodes, operators, containsString: leftHasString, areOperatorsValid: leftOperatorValid } = treeToArray(current.left); - - if (!leftHasString && !isStringLiteral(current.right)) { - return { nodes: [current], operators: [], containsString: false, areOperatorsValid: true }; - } - - const currentOperatorValid = current.operatorToken.kind === SyntaxKind.PlusToken; - const areOperatorsValid = leftOperatorValid && currentOperatorValid; - - nodes.push(current.right); - operators.push(current.operatorToken); - - return { nodes, operators, containsString: true, areOperatorsValid }; - } - - return { nodes: [current as Expression], operators: [], containsString: isStringLiteral(current), areOperatorsValid: true }; +}; +// to copy comments following the string +// "foo" /* comment */ + "bar" /* comment */ + "bar2" +/* @internal */ +const copyCommentFromMultiNode = (nodes: readonly Expression[], file: SourceFile, copyOperatorComments: (index: number, targetNode: Node) => void) => (indexes: number[], targetNode: Node) => { + while (indexes.length > 0) { + const index = indexes.shift()!; + copyTrailingComments(nodes[index], targetNode, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); + copyOperatorComments(index, targetNode); } - - // to copy comments following the operator - // "foo" + /* comment */ "bar" - const copyTrailingOperatorComments = (operators: Token[], file: SourceFile) => (index: number, targetNode: Node) => { - if (index < operators.length) { - copyTrailingComments(operators[index], targetNode, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); - } - }; - - // to copy comments following the string - // "foo" /* comment */ + "bar" /* comment */ + "bar2" - const copyCommentFromMultiNode = (nodes: readonly Expression[], file: SourceFile, copyOperatorComments: (index: number, targetNode: Node) => void) => - (indexes: number[], targetNode: Node) => { - while (indexes.length > 0) { - const index = indexes.shift()!; - copyTrailingComments(nodes[index], targetNode, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); - copyOperatorComments(index, targetNode); - } - }; - - function concatConsecutiveString(index: number, nodes: readonly Expression[]): [number, string, number[]] { - let text = ""; - const indexes = []; - - while (index < nodes.length && isStringLiteral(nodes[index])) { - const stringNode = nodes[index] as StringLiteral; - text = text + stringNode.text; - indexes.push(index); - index++; - } - - text = escapeString(text); - return [index, text, indexes]; +}; +/* @internal */ +function concatConsecutiveString(index: number, nodes: readonly Expression[]): [number, string, number[]] { + let text = ""; + const indexes = []; + while (index < nodes.length && isStringLiteral(nodes[index])) { + const stringNode = (nodes[index] as StringLiteral); + text = text + stringNode.text; + indexes.push(index); + index++; } - - function nodesToTemplate({nodes, operators}: {nodes: readonly Expression[], operators: Token[]}, file: SourceFile) { - const copyOperatorComments = copyTrailingOperatorComments(operators, file); - const copyCommentFromStringLiterals = copyCommentFromMultiNode(nodes, file, copyOperatorComments); - const [begin, headText, headIndexes] = concatConsecutiveString(0, nodes); - - if (begin === nodes.length) { - const noSubstitutionTemplateLiteral = createNoSubstitutionTemplateLiteral(headText); - copyCommentFromStringLiterals(headIndexes, noSubstitutionTemplateLiteral); - return noSubstitutionTemplateLiteral; - } - - const templateSpans: TemplateSpan[] = []; - const templateHead = createTemplateHead(headText); - copyCommentFromStringLiterals(headIndexes, templateHead); - - for (let i = begin; i < nodes.length; i++) { - const currentNode = getExpressionFromParenthesesOrExpression(nodes[i]); - copyOperatorComments(i, currentNode); - - const [newIndex, subsequentText, stringIndexes] = concatConsecutiveString(i + 1, nodes); - i = newIndex - 1; - - const templatePart = i === nodes.length - 1 ? createTemplateTail(subsequentText) : createTemplateMiddle(subsequentText); - copyCommentFromStringLiterals(stringIndexes, templatePart); - templateSpans.push(createTemplateSpan(currentNode, templatePart)); - } - - return createTemplateExpression(templateHead, templateSpans); + text = escapeString(text); + return [index, text, indexes]; +} +/* @internal */ +function nodesToTemplate({ nodes, operators }: { + nodes: readonly Expression[]; + operators: Token[]; +}, file: SourceFile) { + const copyOperatorComments = copyTrailingOperatorComments(operators, file); + const copyCommentFromStringLiterals = copyCommentFromMultiNode(nodes, file, copyOperatorComments); + const [begin, headText, headIndexes] = concatConsecutiveString(0, nodes); + if (begin === nodes.length) { + const noSubstitutionTemplateLiteral = createNoSubstitutionTemplateLiteral(headText); + copyCommentFromStringLiterals(headIndexes, noSubstitutionTemplateLiteral); + return noSubstitutionTemplateLiteral; } - - // to copy comments following the opening & closing parentheses - // "foo" + ( /* comment */ 5 + 5 ) /* comment */ + "bar" - function copyCommentsWhenParenthesized(node: ParenthesizedExpression) { - const file = node.getSourceFile(); - copyTrailingComments(node, node.expression, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); - copyTrailingAsLeadingComments(node.expression, node.expression, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); + const templateSpans: TemplateSpan[] = []; + const templateHead = createTemplateHead(headText); + copyCommentFromStringLiterals(headIndexes, templateHead); + for (let i = begin; i < nodes.length; i++) { + const currentNode = getExpressionFromParenthesesOrExpression(nodes[i]); + copyOperatorComments(i, currentNode); + const [newIndex, subsequentText, stringIndexes] = concatConsecutiveString(i + 1, nodes); + i = newIndex - 1; + const templatePart = i === nodes.length - 1 ? createTemplateTail(subsequentText) : createTemplateMiddle(subsequentText); + copyCommentFromStringLiterals(stringIndexes, templatePart); + templateSpans.push(createTemplateSpan(currentNode, templatePart)); } - - function getExpressionFromParenthesesOrExpression(node: Expression) { - if (isParenthesizedExpression(node)) { - copyCommentsWhenParenthesized(node); - node = node.expression; - } - return node; + return createTemplateExpression(templateHead, templateSpans); +} +// to copy comments following the opening & closing parentheses +// "foo" + ( /* comment */ 5 + 5 ) /* comment */ + "bar" +/* @internal */ +function copyCommentsWhenParenthesized(node: ParenthesizedExpression) { + const file = node.getSourceFile(); + copyTrailingComments(node, node.expression, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); + copyTrailingAsLeadingComments(node.expression, node.expression, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); +} +/* @internal */ +function getExpressionFromParenthesesOrExpression(node: Expression) { + if (isParenthesizedExpression(node)) { + copyCommentsWhenParenthesized(node); + node = node.expression; } + return node; } diff --git a/src/services/refactors/extractSymbol.ts b/src/services/refactors/extractSymbol.ts index 799a44ea525be..3a37957760f87 100644 --- a/src/services/refactors/extractSymbol.ts +++ b/src/services/refactors/extractSymbol.ts @@ -1,1864 +1,1640 @@ +import { registerRefactor } from "../ts.refactor"; +import { RefactorContext, ApplicableRefactorInfo, getRefactorContextSpan, emptyArray, RefactorActionInfo, createMap, getLocaleSpecificMessage, Diagnostics, RefactorEditInfo, Debug, DiagnosticMessage, DiagnosticCategory, Expression, Statement, Symbol, Diagnostic, FunctionLikeDeclaration, SourceFile, ModuleBlock, ClassLikeDeclaration, TextSpan, createFileDiagnostic, getParentNodeInSpan, getTokenAtPosition, findTokenOnLeftOfPosition, textSpanEnd, BlockLike, isJSDoc, isReturnStatement, Node, isVariableStatement, isVariableDeclaration, isIdentifier, isExpressionStatement, createDiagnosticForNode, SyntaxKind, hasModifier, ModifierFlags, getContainingFunction, positionIsSynthesized, isStatement, isExpressionNode, NodeFlags, getContainingClass, __String, isDeclaration, isFunctionLikeDeclaration, isClassLike, isSourceFile, TryStatement, isIterationStatement, LabeledStatement, forEachChild, BreakStatement, ContinueStatement, contains, isModuleBlock, first, findAncestor, isExpression, ExpressionStatement, formatStringFromArgs, Block, VariableDeclaration, getUniqueName, isInJSFile, createIdentifier, TypeNode, ParameterDeclaration, Identifier, NodeBuilderFlags, createParameter, arrayFrom, TypeParameterDeclaration, createTypeReferenceNode, suppressLeadingAndTrailingTrivia, MethodDeclaration, FunctionDeclaration, Modifier, createToken, createMethod, createFunctionDeclaration, last, createCall, createYield, createAwait, createVariableStatement, createVariableDeclarationList, createVariableDeclaration, getSynthesizedDeepClone, BindingElement, TypeElement, createBindingElement, createPropertySignature, TypeLiteralNode, createTypeLiteralNode, setEmitFlags, EmitFlags, createObjectBindingPattern, createShorthandPropertyAssignment, createStatement, createAssignment, createReturn, createObjectLiteral, getRenameLocation, isParenthesizedTypeNode, isUnionTypeNode, find, createUnionTypeNode, createKeywordTypeNode, createProperty, createPropertyAccess, createThis, isFunctionExpression, isArrowFunction, singleOrUndefined, SignatureKind, updateParameter, updateArrowFunction, firstOrUndefined, updateFunctionExpression, isVariableDeclarationList, Type, Declaration, compareProperties, compareValues, compareStringsCaseSensitive, isBlock, createBlock, createNodeArray, visitNodes, VisitResult, ObjectLiteralElementLike, ReturnStatement, createPropertyAssignment, visitNode, getNodeId, visitEachChild, nullTransformationContext, ClassElement, assertType, isConstructorDeclaration, isPropertyDeclaration, isCaseClause, isSwitchStatement, ShorthandPropertyAssignment, map, isArray, TextRange, TypeParameter, TypeChecker, CancellationToken, NamedDeclaration, TypeFlags, isDeclarationWithTypeParameters, getEffectiveTypeParameterDeclarations, isBlockScope, getEnclosingBlockScopeContainer, SymbolFlags, isAssignmentExpression, isUnaryExpressionWithWrite, isPropertyAccessExpression, isElementAccessExpression, isQualifiedName, isPartOfTypeNode, getSymbolId, rangeContainsStartEnd, isShorthandPropertyAssignment, PropertyAccessExpression, EntityName, createQualifiedName } from "../ts"; +import { ChangeTracker } from "../ts.textChanges"; +import * as ts from "../ts"; /* @internal */ -namespace ts.refactor.extractSymbol { - const refactorName = "Extract Symbol"; - registerRefactor(refactorName, { getAvailableActions, getEditsForAction }); - - /** - * Compute the associated code actions - * Exported for tests. - */ - export function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] { - const rangeToExtract = getRangeToExtract(context.file, getRefactorContextSpan(context)); - - const targetRange = rangeToExtract.targetRange; - if (targetRange === undefined) { - return emptyArray; - } - - const extractions = getPossibleExtractions(targetRange, context); - if (extractions === undefined) { - // No extractions possible - return emptyArray; - } - - const functionActions: RefactorActionInfo[] = []; - const usedFunctionNames: Map = createMap(); - - const constantActions: RefactorActionInfo[] = []; - const usedConstantNames: Map = createMap(); - - let i = 0; - for (const {functionExtraction, constantExtraction} of extractions) { - // Skip these since we don't have a way to report errors yet - if (functionExtraction.errors.length === 0) { - // Don't issue refactorings with duplicated names. - // Scopes come back in "innermost first" order, so extractions will - // preferentially go into nearer scopes - const description = functionExtraction.description; - if (!usedFunctionNames.has(description)) { - usedFunctionNames.set(description, true); - functionActions.push({ - description, - name: `function_scope_${i}` - }); - } - } - - // Skip these since we don't have a way to report errors yet - if (constantExtraction.errors.length === 0) { - // Don't issue refactorings with duplicated names. - // Scopes come back in "innermost first" order, so extractions will - // preferentially go into nearer scopes - const description = constantExtraction.description; - if (!usedConstantNames.has(description)) { - usedConstantNames.set(description, true); - constantActions.push({ - description, - name: `constant_scope_${i}` - }); - } - } - - // *do* increment i anyway because we'll look for the i-th scope - // later when actually doing the refactoring if the user requests it - i++; - } - - const infos: ApplicableRefactorInfo[] = []; - - if (constantActions.length) { - infos.push({ - name: refactorName, - description: getLocaleSpecificMessage(Diagnostics.Extract_constant), - actions: constantActions - }); - } - - if (functionActions.length) { - infos.push({ - name: refactorName, - description: getLocaleSpecificMessage(Diagnostics.Extract_function), - actions: functionActions - }); - } - - return infos.length ? infos : emptyArray; - } - - /* Exported for tests */ - export function getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined { - const rangeToExtract = getRangeToExtract(context.file, getRefactorContextSpan(context)); - const targetRange = rangeToExtract.targetRange!; // TODO:GH#18217 - - const parsedFunctionIndexMatch = /^function_scope_(\d+)$/.exec(actionName); - if (parsedFunctionIndexMatch) { - const index = +parsedFunctionIndexMatch[1]; - Debug.assert(isFinite(index), "Expected to parse a finite number from the function scope index"); - return getFunctionExtractionAtIndex(targetRange, context, index); - } - - const parsedConstantIndexMatch = /^constant_scope_(\d+)$/.exec(actionName); - if (parsedConstantIndexMatch) { - const index = +parsedConstantIndexMatch[1]; - Debug.assert(isFinite(index), "Expected to parse a finite number from the constant scope index"); - return getConstantExtractionAtIndex(targetRange, context, index); - } - - Debug.fail("Unrecognized action name"); - } - - // Move these into diagnostic messages if they become user-facing - export namespace Messages { - function createMessage(message: string): DiagnosticMessage { - return { message, code: 0, category: DiagnosticCategory.Message, key: message }; - } - - export const cannotExtractRange: DiagnosticMessage = createMessage("Cannot extract range."); - export const cannotExtractImport: DiagnosticMessage = createMessage("Cannot extract import statement."); - export const cannotExtractSuper: DiagnosticMessage = createMessage("Cannot extract super call."); - export const cannotExtractJSDoc: DiagnosticMessage = createMessage("Cannot extract JSDoc."); - export const cannotExtractEmpty: DiagnosticMessage = createMessage("Cannot extract empty range."); - export const expressionExpected: DiagnosticMessage = createMessage("expression expected."); - export const uselessConstantType: DiagnosticMessage = createMessage("No reason to extract constant of type."); - export const statementOrExpressionExpected: DiagnosticMessage = createMessage("Statement or expression expected."); - export const cannotExtractRangeContainingConditionalBreakOrContinueStatements: DiagnosticMessage = createMessage("Cannot extract range containing conditional break or continue statements."); - export const cannotExtractRangeContainingConditionalReturnStatement: DiagnosticMessage = createMessage("Cannot extract range containing conditional return statement."); - export const cannotExtractRangeContainingLabeledBreakOrContinueStatementWithTargetOutsideOfTheRange: DiagnosticMessage = createMessage("Cannot extract range containing labeled break or continue with target outside of the range."); - export const cannotExtractRangeThatContainsWritesToReferencesLocatedOutsideOfTheTargetRangeInGenerators: DiagnosticMessage = createMessage("Cannot extract range containing writes to references located outside of the target range in generators."); - export const typeWillNotBeVisibleInTheNewScope = createMessage("Type will not visible in the new scope."); - export const functionWillNotBeVisibleInTheNewScope = createMessage("Function will not visible in the new scope."); - export const cannotExtractIdentifier = createMessage("Select more than a single identifier."); - export const cannotExtractExportedEntity = createMessage("Cannot extract exported declaration"); - export const cannotWriteInExpression = createMessage("Cannot write back side-effects when extracting an expression"); - export const cannotExtractReadonlyPropertyInitializerOutsideConstructor = createMessage("Cannot move initialization of read-only class property outside of the constructor"); - export const cannotExtractAmbientBlock = createMessage("Cannot extract code from ambient contexts"); - export const cannotAccessVariablesFromNestedScopes = createMessage("Cannot access variables from nested scopes"); - export const cannotExtractToOtherFunctionLike = createMessage("Cannot extract method to a function-like scope that is not a function"); - export const cannotExtractToJSClass = createMessage("Cannot extract constant to a class scope in JS"); - export const cannotExtractToExpressionArrowFunction = createMessage("Cannot extract constant to an arrow function without a block"); - } - - enum RangeFacts { - None = 0, - HasReturn = 1 << 0, - IsGenerator = 1 << 1, - IsAsyncFunction = 1 << 2, - UsesThis = 1 << 3, - /** - * The range is in a function which needs the 'static' modifier in a class - */ - InStaticRegion = 1 << 4 - } - - /** - * Represents an expression or a list of statements that should be extracted with some extra information - */ - interface TargetRange { - readonly range: Expression | Statement[]; - readonly facts: RangeFacts; - /** - * A list of symbols that are declared in the selected range which are visible in the containing lexical scope - * Used to ensure we don't turn something used outside the range free (or worse, resolve to a different entity). - */ - readonly declarations: Symbol[]; - } - +const refactorName = "Extract Symbol"; +/* @internal */ +registerRefactor(refactorName, { getAvailableActions, getEditsForAction }); +/** + * Compute the associated code actions + * Exported for tests. + */ +/* @internal */ +export function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] { + const rangeToExtract = getRangeToExtract(context.file, getRefactorContextSpan(context)); + const targetRange = rangeToExtract.targetRange; + if (targetRange === undefined) { + return emptyArray; + } + const extractions = getPossibleExtractions(targetRange, context); + if (extractions === undefined) { + // No extractions possible + return emptyArray; + } + const functionActions: RefactorActionInfo[] = []; + const usedFunctionNames: ts.Map = createMap(); + const constantActions: RefactorActionInfo[] = []; + const usedConstantNames: ts.Map = createMap(); + let i = 0; + for (const { functionExtraction, constantExtraction } of extractions) { + // Skip these since we don't have a way to report errors yet + if (functionExtraction.errors.length === 0) { + // Don't issue refactorings with duplicated names. + // Scopes come back in "innermost first" order, so extractions will + // preferentially go into nearer scopes + const description = functionExtraction.description; + if (!usedFunctionNames.has(description)) { + usedFunctionNames.set(description, true); + functionActions.push({ + description, + name: `function_scope_${i}` + }); + } + } + // Skip these since we don't have a way to report errors yet + if (constantExtraction.errors.length === 0) { + // Don't issue refactorings with duplicated names. + // Scopes come back in "innermost first" order, so extractions will + // preferentially go into nearer scopes + const description = constantExtraction.description; + if (!usedConstantNames.has(description)) { + usedConstantNames.set(description, true); + constantActions.push({ + description, + name: `constant_scope_${i}` + }); + } + } + // *do* increment i anyway because we'll look for the i-th scope + // later when actually doing the refactoring if the user requests it + i++; + } + const infos: ApplicableRefactorInfo[] = []; + if (constantActions.length) { + infos.push({ + name: refactorName, + description: getLocaleSpecificMessage(Diagnostics.Extract_constant), + actions: constantActions + }); + } + if (functionActions.length) { + infos.push({ + name: refactorName, + description: getLocaleSpecificMessage(Diagnostics.Extract_function), + actions: functionActions + }); + } + return infos.length ? infos : emptyArray; +} +/* Exported for tests */ +/* @internal */ +export function getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined { + const rangeToExtract = getRangeToExtract(context.file, getRefactorContextSpan(context)); + const targetRange = rangeToExtract.targetRange!; // TODO:GH#18217 + const parsedFunctionIndexMatch = /^function_scope_(\d+)$/.exec(actionName); + if (parsedFunctionIndexMatch) { + const index = +parsedFunctionIndexMatch[1]; + Debug.assert(isFinite(index), "Expected to parse a finite number from the function scope index"); + return getFunctionExtractionAtIndex(targetRange, context, index); + } + const parsedConstantIndexMatch = /^constant_scope_(\d+)$/.exec(actionName); + if (parsedConstantIndexMatch) { + const index = +parsedConstantIndexMatch[1]; + Debug.assert(isFinite(index), "Expected to parse a finite number from the constant scope index"); + return getConstantExtractionAtIndex(targetRange, context, index); + } + Debug.fail("Unrecognized action name"); +} +// Move these into diagnostic messages if they become user-facing +/* @internal */ +export namespace Messages { + function createMessage(message: string): DiagnosticMessage { + return { message, code: 0, category: DiagnosticCategory.Message, key: message }; + } + export const cannotExtractRange: DiagnosticMessage = createMessage("Cannot extract range."); + export const cannotExtractImport: DiagnosticMessage = createMessage("Cannot extract import statement."); + export const cannotExtractSuper: DiagnosticMessage = createMessage("Cannot extract super call."); + export const cannotExtractJSDoc: DiagnosticMessage = createMessage("Cannot extract JSDoc."); + export const cannotExtractEmpty: DiagnosticMessage = createMessage("Cannot extract empty range."); + export const expressionExpected: DiagnosticMessage = createMessage("expression expected."); + export const uselessConstantType: DiagnosticMessage = createMessage("No reason to extract constant of type."); + export const statementOrExpressionExpected: DiagnosticMessage = createMessage("Statement or expression expected."); + export const cannotExtractRangeContainingConditionalBreakOrContinueStatements: DiagnosticMessage = createMessage("Cannot extract range containing conditional break or continue statements."); + export const cannotExtractRangeContainingConditionalReturnStatement: DiagnosticMessage = createMessage("Cannot extract range containing conditional return statement."); + export const cannotExtractRangeContainingLabeledBreakOrContinueStatementWithTargetOutsideOfTheRange: DiagnosticMessage = createMessage("Cannot extract range containing labeled break or continue with target outside of the range."); + export const cannotExtractRangeThatContainsWritesToReferencesLocatedOutsideOfTheTargetRangeInGenerators: DiagnosticMessage = createMessage("Cannot extract range containing writes to references located outside of the target range in generators."); + export const typeWillNotBeVisibleInTheNewScope = createMessage("Type will not visible in the new scope."); + export const functionWillNotBeVisibleInTheNewScope = createMessage("Function will not visible in the new scope."); + export const cannotExtractIdentifier = createMessage("Select more than a single identifier."); + export const cannotExtractExportedEntity = createMessage("Cannot extract exported declaration"); + export const cannotWriteInExpression = createMessage("Cannot write back side-effects when extracting an expression"); + export const cannotExtractReadonlyPropertyInitializerOutsideConstructor = createMessage("Cannot move initialization of read-only class property outside of the constructor"); + export const cannotExtractAmbientBlock = createMessage("Cannot extract code from ambient contexts"); + export const cannotAccessVariablesFromNestedScopes = createMessage("Cannot access variables from nested scopes"); + export const cannotExtractToOtherFunctionLike = createMessage("Cannot extract method to a function-like scope that is not a function"); + export const cannotExtractToJSClass = createMessage("Cannot extract constant to a class scope in JS"); + export const cannotExtractToExpressionArrowFunction = createMessage("Cannot extract constant to an arrow function without a block"); +} +/* @internal */ +enum RangeFacts { + None = 0, + HasReturn = 1 << 0, + IsGenerator = 1 << 1, + IsAsyncFunction = 1 << 2, + UsesThis = 1 << 3, /** - * Result of 'getRangeToExtract' operation: contains either a range or a list of errors + * The range is in a function which needs the 'static' modifier in a class */ - type RangeToExtract = { - readonly targetRange?: never; - readonly errors: readonly Diagnostic[]; - } | { - readonly targetRange: TargetRange; - readonly errors?: never; - }; - - /* - * Scopes that can store newly extracted method - */ - type Scope = FunctionLikeDeclaration | SourceFile | ModuleBlock | ClassLikeDeclaration; - + InStaticRegion = 1 << 4 +} +/** + * Represents an expression or a list of statements that should be extracted with some extra information + */ +/* @internal */ +interface TargetRange { + readonly range: Expression | Statement[]; + readonly facts: RangeFacts; /** - * getRangeToExtract takes a span inside a text file and returns either an expression or an array - * of statements representing the minimum set of nodes needed to extract the entire span. This - * process may fail, in which case a set of errors is returned instead (these are currently - * not shown to the user, but can be used by us diagnostically) + * A list of symbols that are declared in the selected range which are visible in the containing lexical scope + * Used to ensure we don't turn something used outside the range free (or worse, resolve to a different entity). */ - // exported only for tests - export function getRangeToExtract(sourceFile: SourceFile, span: TextSpan): RangeToExtract { - const { length } = span; - - if (length === 0) { - return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractEmpty)] }; - } - - // Walk up starting from the the start position until we find a non-SourceFile node that subsumes the selected span. - // This may fail (e.g. you select two statements in the root of a source file) - const start = getParentNodeInSpan(getTokenAtPosition(sourceFile, span.start), sourceFile, span); - // Do the same for the ending position - const end = getParentNodeInSpan(findTokenOnLeftOfPosition(sourceFile, textSpanEnd(span)), sourceFile, span); - - const declarations: Symbol[] = []; - - // We'll modify these flags as we walk the tree to collect data - // about what things need to be done as part of the extraction. - let rangeFacts = RangeFacts.None; - - if (!start || !end) { - // cannot find either start or end node - return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] }; - } - - if (start.parent !== end.parent) { - // start and end nodes belong to different subtrees + readonly declarations: Symbol[]; +} +/** + * Result of 'getRangeToExtract' operation: contains either a range or a list of errors + */ +/* @internal */ +type RangeToExtract = { + readonly targetRange?: never; + readonly errors: readonly Diagnostic[]; +} | { + readonly targetRange: TargetRange; + readonly errors?: never; +}; +/* + * Scopes that can store newly extracted method + */ +/* @internal */ +type Scope = FunctionLikeDeclaration | SourceFile | ModuleBlock | ClassLikeDeclaration; +/** + * getRangeToExtract takes a span inside a text file and returns either an expression or an array + * of statements representing the minimum set of nodes needed to extract the entire span. This + * process may fail, in which case a set of errors is returned instead (these are currently + * not shown to the user, but can be used by us diagnostically) + */ +// exported only for tests +/* @internal */ +export function getRangeToExtract(sourceFile: SourceFile, span: TextSpan): RangeToExtract { + const { length } = span; + if (length === 0) { + return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractEmpty)] }; + } + // Walk up starting from the the start position until we find a non-SourceFile node that subsumes the selected span. + // This may fail (e.g. you select two statements in the root of a source file) + const start = getParentNodeInSpan(getTokenAtPosition(sourceFile, span.start), sourceFile, span); + // Do the same for the ending position + const end = getParentNodeInSpan(findTokenOnLeftOfPosition(sourceFile, textSpanEnd(span)), sourceFile, span); + const declarations: Symbol[] = []; + // We'll modify these flags as we walk the tree to collect data + // about what things need to be done as part of the extraction. + let rangeFacts = RangeFacts.None; + if (!start || !end) { + // cannot find either start or end node + return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] }; + } + if (start.parent !== end.parent) { + // start and end nodes belong to different subtrees + return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] }; + } + if (start !== end) { + // start and end should be statements and parent should be either block or a source file + if (!isBlockLike(start.parent)) { return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] }; } - - if (start !== end) { - // start and end should be statements and parent should be either block or a source file - if (!isBlockLike(start.parent)) { - return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] }; - } - const statements: Statement[] = []; - const start2 = start; // TODO: GH#18217 Need to alias `start` to get this to compile. See https://github.com/Microsoft/TypeScript/issues/19955#issuecomment-344118248 - for (const statement of (start2.parent as BlockLike).statements) { - if (statement === start || statements.length) { - const errors = checkNode(statement); - if (errors) { - return { errors }; - } - statements.push(statement); - } - if (statement === end) { - break; + const statements: Statement[] = []; + const start2 = start; // TODO: GH#18217 Need to alias `start` to get this to compile. See https://github.com/Microsoft/TypeScript/issues/19955#issuecomment-344118248 + for (const statement of (start2.parent as BlockLike).statements) { + if (statement === start || statements.length) { + const errors = checkNode(statement); + if (errors) { + return { errors }; } + statements.push(statement); } - - if (!statements.length) { - // https://github.com/Microsoft/TypeScript/issues/20559 - // Ranges like [|case 1: break;|] will fail to populate `statements` because - // they will never find `start` in `start.parent.statements`. - // Consider: We could support ranges like [|case 1:|] by refining them to just - // the expression. - return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] }; + if (statement === end) { + break; } - - return { targetRange: { range: statements, facts: rangeFacts, declarations } }; - } - - if (isJSDoc(start)) { - return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractJSDoc)] }; } - - if (isReturnStatement(start) && !start.expression) { - // Makes no sense to extract an expression-less return statement. + if (!statements.length) { + // https://github.com/Microsoft/TypeScript/issues/20559 + // Ranges like [|case 1: break;|] will fail to populate `statements` because + // they will never find `start` in `start.parent.statements`. + // Consider: We could support ranges like [|case 1:|] by refining them to just + // the expression. return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] }; } - - // We have a single node (start) - const node = refineNode(start); - - const errors = checkRootNode(node) || checkNode(node); - if (errors) { - return { errors }; - } - return { targetRange: { range: getStatementOrExpressionRange(node)!, facts: rangeFacts, declarations } }; // TODO: GH#18217 - - /** - * Attempt to refine the extraction node (generally, by shrinking it) to produce better results. - * @param node The unrefined extraction node. - */ - function refineNode(node: Node): Node { - if (isReturnStatement(node)) { - if (node.expression) { - return node.expression; - } - } - else if (isVariableStatement(node)) { - let numInitializers = 0; - let lastInitializer: Expression | undefined; - for (const declaration of node.declarationList.declarations) { - if (declaration.initializer) { - numInitializers++; - lastInitializer = declaration.initializer; - } - } - if (numInitializers === 1) { - return lastInitializer!; + return { targetRange: { range: statements, facts: rangeFacts, declarations } }; + } + if (isJSDoc(start)) { + return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractJSDoc)] }; + } + if (isReturnStatement(start) && !start.expression) { + // Makes no sense to extract an expression-less return statement. + return { errors: [createFileDiagnostic(sourceFile, span.start, length, Messages.cannotExtractRange)] }; + } + // We have a single node (start) + const node = refineNode(start); + const errors = checkRootNode(node) || checkNode(node); + if (errors) { + return { errors }; + } + return { targetRange: { range: getStatementOrExpressionRange(node)!, facts: rangeFacts, declarations } }; // TODO: GH#18217 + /** + * Attempt to refine the extraction node (generally, by shrinking it) to produce better results. + * @param node The unrefined extraction node. + */ + function refineNode(node: Node): Node { + if (isReturnStatement(node)) { + if (node.expression) { + return node.expression; + } + } + else if (isVariableStatement(node)) { + let numInitializers = 0; + let lastInitializer: Expression | undefined; + for (const declaration of node.declarationList.declarations) { + if (declaration.initializer) { + numInitializers++; + lastInitializer = declaration.initializer; } - // No special handling if there are multiple initializers. } - else if (isVariableDeclaration(node)) { - if (node.initializer) { - return node.initializer; - } + if (numInitializers === 1) { + return lastInitializer!; } - - return node; + // No special handling if there are multiple initializers. } - - function checkRootNode(node: Node): Diagnostic[] | undefined { - if (isIdentifier(isExpressionStatement(node) ? node.expression : node)) { - return [createDiagnosticForNode(node, Messages.cannotExtractIdentifier)]; + else if (isVariableDeclaration(node)) { + if (node.initializer) { + return node.initializer; } - return undefined; } - - function checkForStaticContext(nodeToCheck: Node, containingClass: Node) { - let current: Node = nodeToCheck; - while (current !== containingClass) { - if (current.kind === SyntaxKind.PropertyDeclaration) { - if (hasModifier(current, ModifierFlags.Static)) { - rangeFacts |= RangeFacts.InStaticRegion; - } - break; + return node; + } + function checkRootNode(node: Node): Diagnostic[] | undefined { + if (isIdentifier(isExpressionStatement(node) ? node.expression : node)) { + return [createDiagnosticForNode(node, Messages.cannotExtractIdentifier)]; + } + return undefined; + } + function checkForStaticContext(nodeToCheck: Node, containingClass: Node) { + let current: Node = nodeToCheck; + while (current !== containingClass) { + if (current.kind === SyntaxKind.PropertyDeclaration) { + if (hasModifier(current, ModifierFlags.Static)) { + rangeFacts |= RangeFacts.InStaticRegion; } - else if (current.kind === SyntaxKind.Parameter) { - const ctorOrMethod = getContainingFunction(current)!; - if (ctorOrMethod.kind === SyntaxKind.Constructor) { - rangeFacts |= RangeFacts.InStaticRegion; - } - break; + break; + } + else if (current.kind === SyntaxKind.Parameter) { + const ctorOrMethod = (getContainingFunction(current)!); + if (ctorOrMethod.kind === SyntaxKind.Constructor) { + rangeFacts |= RangeFacts.InStaticRegion; } - else if (current.kind === SyntaxKind.MethodDeclaration) { - if (hasModifier(current, ModifierFlags.Static)) { - rangeFacts |= RangeFacts.InStaticRegion; - } + break; + } + else if (current.kind === SyntaxKind.MethodDeclaration) { + if (hasModifier(current, ModifierFlags.Static)) { + rangeFacts |= RangeFacts.InStaticRegion; } - current = current.parent; } + current = current.parent; } - - // Verifies whether we can actually extract this node or not. - function checkNode(nodeToCheck: Node): Diagnostic[] | undefined { - const enum PermittedJumps { - None = 0, - Break = 1 << 0, - Continue = 1 << 1, - Return = 1 << 2 - } - - // We believe it's true because the node is from the (unmodified) tree. - Debug.assert(nodeToCheck.pos <= nodeToCheck.end, "This failure could trigger https://github.com/Microsoft/TypeScript/issues/20809 (1)"); - - // For understanding how skipTrivia functioned: - Debug.assert(!positionIsSynthesized(nodeToCheck.pos), "This failure could trigger https://github.com/Microsoft/TypeScript/issues/20809 (2)"); - - if (!isStatement(nodeToCheck) && !(isExpressionNode(nodeToCheck) && isExtractableExpression(nodeToCheck))) { - return [createDiagnosticForNode(nodeToCheck, Messages.statementOrExpressionExpected)]; - } - - if (nodeToCheck.flags & NodeFlags.Ambient) { - return [createDiagnosticForNode(nodeToCheck, Messages.cannotExtractAmbientBlock)]; - } - - // If we're in a class, see whether we're in a static region (static property initializer, static method, class constructor parameter default) - const containingClass = getContainingClass(nodeToCheck); - if (containingClass) { - checkForStaticContext(nodeToCheck, containingClass); + } + // Verifies whether we can actually extract this node or not. + function checkNode(nodeToCheck: Node): Diagnostic[] | undefined { + const enum PermittedJumps { + None = 0, + Break = 1 << 0, + Continue = 1 << 1, + Return = 1 << 2 + } + // We believe it's true because the node is from the (unmodified) tree. + Debug.assert(nodeToCheck.pos <= nodeToCheck.end, "This failure could trigger https://github.com/Microsoft/TypeScript/issues/20809 (1)"); + // For understanding how skipTrivia functioned: + Debug.assert(!positionIsSynthesized(nodeToCheck.pos), "This failure could trigger https://github.com/Microsoft/TypeScript/issues/20809 (2)"); + if (!isStatement(nodeToCheck) && !(isExpressionNode(nodeToCheck) && isExtractableExpression(nodeToCheck))) { + return [createDiagnosticForNode(nodeToCheck, Messages.statementOrExpressionExpected)]; + } + if (nodeToCheck.flags & NodeFlags.Ambient) { + return [createDiagnosticForNode(nodeToCheck, Messages.cannotExtractAmbientBlock)]; + } + // If we're in a class, see whether we're in a static region (static property initializer, static method, class constructor parameter default) + const containingClass = getContainingClass(nodeToCheck); + if (containingClass) { + checkForStaticContext(nodeToCheck, containingClass); + } + let errors: Diagnostic[] | undefined; + let permittedJumps = PermittedJumps.Return; + let seenLabels: __String[]; + visit(nodeToCheck); + return errors; + function visit(node: Node) { + if (errors) { + // already found an error - can stop now + return true; } - - let errors: Diagnostic[] | undefined; - let permittedJumps = PermittedJumps.Return; - let seenLabels: __String[]; - - visit(nodeToCheck); - - return errors; - - function visit(node: Node) { - if (errors) { - // already found an error - can stop now + if (isDeclaration(node)) { + const declaringNode = (node.kind === SyntaxKind.VariableDeclaration) ? node.parent.parent : node; + if (hasModifier(declaringNode, ModifierFlags.Export)) { + // TODO: GH#18217 Silly to use `errors ||` since it's definitely not defined (see top of `visit`) + // Also, if we're only pushing one error, just use `let error: Diagnostic | undefined`! + // Also TODO: GH#19956 + (errors || (errors = ([] as Diagnostic[]))).push(createDiagnosticForNode(node, Messages.cannotExtractExportedEntity)); return true; } - - if (isDeclaration(node)) { - const declaringNode = (node.kind === SyntaxKind.VariableDeclaration) ? node.parent.parent : node; - if (hasModifier(declaringNode, ModifierFlags.Export)) { - // TODO: GH#18217 Silly to use `errors ||` since it's definitely not defined (see top of `visit`) - // Also, if we're only pushing one error, just use `let error: Diagnostic | undefined`! - // Also TODO: GH#19956 - (errors || (errors = [] as Diagnostic[])).push(createDiagnosticForNode(node, Messages.cannotExtractExportedEntity)); - return true; + declarations.push(node.symbol); + } + // Some things can't be extracted in certain situations + switch (node.kind) { + case SyntaxKind.ImportDeclaration: + (errors || (errors = ([] as Diagnostic[]))).push(createDiagnosticForNode(node, Messages.cannotExtractImport)); + return true; + case SyntaxKind.SuperKeyword: + // For a super *constructor call*, we have to be extracting the entire class, + // but a super *method call* simply implies a 'this' reference + if (node.parent.kind === SyntaxKind.CallExpression) { + // Super constructor call + const containingClass = (getContainingClass(node)!); // TODO:GH#18217 + if (containingClass.pos < span.start || containingClass.end >= (span.start + span.length)) { + (errors || (errors = ([] as Diagnostic[]))).push(createDiagnosticForNode(node, Messages.cannotExtractSuper)); + return true; + } } - declarations.push(node.symbol); - } - - // Some things can't be extracted in certain situations + else { + rangeFacts |= RangeFacts.UsesThis; + } + break; + } + if (isFunctionLikeDeclaration(node) || isClassLike(node)) { switch (node.kind) { - case SyntaxKind.ImportDeclaration: - (errors || (errors = [] as Diagnostic[])).push(createDiagnosticForNode(node, Messages.cannotExtractImport)); - return true; - case SyntaxKind.SuperKeyword: - // For a super *constructor call*, we have to be extracting the entire class, - // but a super *method call* simply implies a 'this' reference - if (node.parent.kind === SyntaxKind.CallExpression) { - // Super constructor call - const containingClass = getContainingClass(node)!; // TODO:GH#18217 - if (containingClass.pos < span.start || containingClass.end >= (span.start + span.length)) { - (errors || (errors = [] as Diagnostic[])).push(createDiagnosticForNode(node, Messages.cannotExtractSuper)); - return true; - } - } - else { - rangeFacts |= RangeFacts.UsesThis; + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.ClassDeclaration: + if (isSourceFile(node.parent) && node.parent.externalModuleIndicator === undefined) { + // You cannot extract global declarations + (errors || (errors = ([] as Diagnostic[]))).push(createDiagnosticForNode(node, Messages.functionWillNotBeVisibleInTheNewScope)); } break; } - - if (isFunctionLikeDeclaration(node) || isClassLike(node)) { - switch (node.kind) { - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.ClassDeclaration: - if (isSourceFile(node.parent) && node.parent.externalModuleIndicator === undefined) { - // You cannot extract global declarations - (errors || (errors = [] as Diagnostic[])).push(createDiagnosticForNode(node, Messages.functionWillNotBeVisibleInTheNewScope)); - } - break; + // do not dive into functions or classes + return false; + } + const savedPermittedJumps = permittedJumps; + switch (node.kind) { + case SyntaxKind.IfStatement: + permittedJumps = PermittedJumps.None; + break; + case SyntaxKind.TryStatement: + // forbid all jumps inside try blocks + permittedJumps = PermittedJumps.None; + break; + case SyntaxKind.Block: + if (node.parent && node.parent.kind === SyntaxKind.TryStatement && (node.parent).finallyBlock === node) { + // allow unconditional returns from finally blocks + permittedJumps = PermittedJumps.Return; + } + break; + case SyntaxKind.DefaultClause: + case SyntaxKind.CaseClause: + // allow unlabeled break inside case clauses + permittedJumps |= PermittedJumps.Break; + break; + default: + if (isIterationStatement(node, /*lookInLabeledStatements*/ false)) { + // allow unlabeled break/continue inside loops + permittedJumps |= PermittedJumps.Break | PermittedJumps.Continue; } - - // do not dive into functions or classes - return false; + break; + } + switch (node.kind) { + case SyntaxKind.ThisType: + case SyntaxKind.ThisKeyword: + rangeFacts |= RangeFacts.UsesThis; + break; + case SyntaxKind.LabeledStatement: { + const label = (node).label; + (seenLabels || (seenLabels = [])).push(label.escapedText); + forEachChild(node, visit); + seenLabels.pop(); + break; } - const savedPermittedJumps = permittedJumps; - - switch (node.kind) { - case SyntaxKind.IfStatement: - permittedJumps = PermittedJumps.None; - break; - case SyntaxKind.TryStatement: - // forbid all jumps inside try blocks - permittedJumps = PermittedJumps.None; - break; - case SyntaxKind.Block: - if (node.parent && node.parent.kind === SyntaxKind.TryStatement && (node.parent).finallyBlock === node) { - // allow unconditional returns from finally blocks - permittedJumps = PermittedJumps.Return; + case SyntaxKind.BreakStatement: + case SyntaxKind.ContinueStatement: { + const label = (node).label; + if (label) { + if (!contains(seenLabels, label.escapedText)) { + // attempts to jump to label that is not in range to be extracted + (errors || (errors = ([] as Diagnostic[]))).push(createDiagnosticForNode(node, Messages.cannotExtractRangeContainingLabeledBreakOrContinueStatementWithTargetOutsideOfTheRange)); } - break; - case SyntaxKind.DefaultClause: - case SyntaxKind.CaseClause: - // allow unlabeled break inside case clauses - permittedJumps |= PermittedJumps.Break; - break; - default: - if (isIterationStatement(node, /*lookInLabeledStatements*/ false)) { - // allow unlabeled break/continue inside loops - permittedJumps |= PermittedJumps.Break | PermittedJumps.Continue; - } - break; - } - - switch (node.kind) { - case SyntaxKind.ThisType: - case SyntaxKind.ThisKeyword: - rangeFacts |= RangeFacts.UsesThis; - break; - case SyntaxKind.LabeledStatement: { - const label = (node).label; - (seenLabels || (seenLabels = [])).push(label.escapedText); - forEachChild(node, visit); - seenLabels.pop(); - break; } - case SyntaxKind.BreakStatement: - case SyntaxKind.ContinueStatement: { - const label = (node).label; - if (label) { - if (!contains(seenLabels, label.escapedText)) { - // attempts to jump to label that is not in range to be extracted - (errors || (errors = [] as Diagnostic[])).push(createDiagnosticForNode(node, Messages.cannotExtractRangeContainingLabeledBreakOrContinueStatementWithTargetOutsideOfTheRange)); - } - } - else { - if (!(permittedJumps & (node.kind === SyntaxKind.BreakStatement ? PermittedJumps.Break : PermittedJumps.Continue))) { - // attempt to break or continue in a forbidden context - (errors || (errors = [] as Diagnostic[])).push(createDiagnosticForNode(node, Messages.cannotExtractRangeContainingConditionalBreakOrContinueStatements)); - } + else { + if (!(permittedJumps & (node.kind === SyntaxKind.BreakStatement ? PermittedJumps.Break : PermittedJumps.Continue))) { + // attempt to break or continue in a forbidden context + (errors || (errors = ([] as Diagnostic[]))).push(createDiagnosticForNode(node, Messages.cannotExtractRangeContainingConditionalBreakOrContinueStatements)); } - break; } - case SyntaxKind.AwaitExpression: - rangeFacts |= RangeFacts.IsAsyncFunction; - break; - case SyntaxKind.YieldExpression: - rangeFacts |= RangeFacts.IsGenerator; - break; - case SyntaxKind.ReturnStatement: - if (permittedJumps & PermittedJumps.Return) { - rangeFacts |= RangeFacts.HasReturn; - } - else { - (errors || (errors = [] as Diagnostic[])).push(createDiagnosticForNode(node, Messages.cannotExtractRangeContainingConditionalReturnStatement)); - } - break; - default: - forEachChild(node, visit); - break; + break; } - - permittedJumps = savedPermittedJumps; + case SyntaxKind.AwaitExpression: + rangeFacts |= RangeFacts.IsAsyncFunction; + break; + case SyntaxKind.YieldExpression: + rangeFacts |= RangeFacts.IsGenerator; + break; + case SyntaxKind.ReturnStatement: + if (permittedJumps & PermittedJumps.Return) { + rangeFacts |= RangeFacts.HasReturn; + } + else { + (errors || (errors = ([] as Diagnostic[]))).push(createDiagnosticForNode(node, Messages.cannotExtractRangeContainingConditionalReturnStatement)); + } + break; + default: + forEachChild(node, visit); + break; } + permittedJumps = savedPermittedJumps; } } - - function getStatementOrExpressionRange(node: Node): Statement[] | Expression | undefined { - if (isStatement(node)) { - return [node]; - } - else if (isExpressionNode(node)) { - // If our selection is the expression in an ExpressionStatement, expand - // the selection to include the enclosing Statement (this stops us - // from trying to care about the return value of the extracted function - // and eliminates double semicolon insertion in certain scenarios) - return isExpressionStatement(node.parent) ? [node.parent] : node as Expression; - } - return undefined; +} +/* @internal */ +function getStatementOrExpressionRange(node: Node): Statement[] | Expression | undefined { + if (isStatement(node)) { + return [node]; } - - function isScope(node: Node): node is Scope { - return isFunctionLikeDeclaration(node) || isSourceFile(node) || isModuleBlock(node) || isClassLike(node); + else if (isExpressionNode(node)) { + // If our selection is the expression in an ExpressionStatement, expand + // the selection to include the enclosing Statement (this stops us + // from trying to care about the return value of the extracted function + // and eliminates double semicolon insertion in certain scenarios) + return isExpressionStatement(node.parent) ? [node.parent] : node as Expression; } - - /** - * Computes possible places we could extract the function into. For example, - * you may be able to extract into a class method *or* local closure *or* namespace function, - * depending on what's in the extracted body. - */ - function collectEnclosingScopes(range: TargetRange): Scope[] { - let current: Node = isReadonlyArray(range.range) ? first(range.range) : range.range; - if (range.facts & RangeFacts.UsesThis) { - // if range uses this as keyword or as type inside the class then it can only be extracted to a method of the containing class - const containingClass = getContainingClass(current); - if (containingClass) { - const containingFunction = findAncestor(current, isFunctionLikeDeclaration); - return containingFunction - ? [containingFunction, containingClass] - : [containingClass]; - } + return undefined; +} +/* @internal */ +function isScope(node: Node): node is Scope { + return isFunctionLikeDeclaration(node) || isSourceFile(node) || isModuleBlock(node) || isClassLike(node); +} +/** + * Computes possible places we could extract the function into. For example, + * you may be able to extract into a class method *or* local closure *or* namespace function, + * depending on what's in the extracted body. + */ +/* @internal */ +function collectEnclosingScopes(range: TargetRange): Scope[] { + let current: Node = isReadonlyArray(range.range) ? first(range.range) : range.range; + if (range.facts & RangeFacts.UsesThis) { + // if range uses this as keyword or as type inside the class then it can only be extracted to a method of the containing class + const containingClass = getContainingClass(current); + if (containingClass) { + const containingFunction = findAncestor(current, isFunctionLikeDeclaration); + return containingFunction + ? [containingFunction, containingClass] + : [containingClass]; } - - const scopes: Scope[] = []; - while (true) { - current = current.parent; - // A function parameter's initializer is actually in the outer scope, not the function declaration - if (current.kind === SyntaxKind.Parameter) { - // Skip all the way to the outer scope of the function that declared this parameter - current = findAncestor(current, parent => isFunctionLikeDeclaration(parent))!.parent; - } - - // We want to find the nearest parent where we can place an "equivalent" sibling to the node we're extracting out of. - // Walk up to the closest parent of a place where we can logically put a sibling: - // * Function declaration - // * Class declaration or expression - // * Module/namespace or source file - if (isScope(current)) { - scopes.push(current); - if (current.kind === SyntaxKind.SourceFile) { - return scopes; - } + } + const scopes: Scope[] = []; + while (true) { + current = current.parent; + // A function parameter's initializer is actually in the outer scope, not the function declaration + if (current.kind === SyntaxKind.Parameter) { + // Skip all the way to the outer scope of the function that declared this parameter + current = findAncestor(current, parent => isFunctionLikeDeclaration(parent))!.parent; + } + // We want to find the nearest parent where we can place an "equivalent" sibling to the node we're extracting out of. + // Walk up to the closest parent of a place where we can logically put a sibling: + // * Function declaration + // * Class declaration or expression + // * Module/namespace or source file + if (isScope(current)) { + scopes.push(current); + if (current.kind === SyntaxKind.SourceFile) { + return scopes; } } } - - function getFunctionExtractionAtIndex(targetRange: TargetRange, context: RefactorContext, requestedChangesIndex: number): RefactorEditInfo { - const { scopes, readsAndWrites: { target, usagesPerScope, functionErrorsPerScope, exposedVariableDeclarations } } = getPossibleExtractionsWorker(targetRange, context); - Debug.assert(!functionErrorsPerScope[requestedChangesIndex].length, "The extraction went missing? How?"); - context.cancellationToken!.throwIfCancellationRequested(); // TODO: GH#18217 - return extractFunctionInScope(target, scopes[requestedChangesIndex], usagesPerScope[requestedChangesIndex], exposedVariableDeclarations, targetRange, context); - } - - function getConstantExtractionAtIndex(targetRange: TargetRange, context: RefactorContext, requestedChangesIndex: number): RefactorEditInfo { - const { scopes, readsAndWrites: { target, usagesPerScope, constantErrorsPerScope, exposedVariableDeclarations } } = getPossibleExtractionsWorker(targetRange, context); - Debug.assert(!constantErrorsPerScope[requestedChangesIndex].length, "The extraction went missing? How?"); - Debug.assert(exposedVariableDeclarations.length === 0, "Extract constant accepted a range containing a variable declaration?"); - context.cancellationToken!.throwIfCancellationRequested(); - const expression = isExpression(target) - ? target - : (target.statements[0] as ExpressionStatement).expression; - return extractConstantInScope(expression, scopes[requestedChangesIndex], usagesPerScope[requestedChangesIndex], targetRange.facts, context); - } - - interface Extraction { - readonly description: string; - readonly errors: readonly Diagnostic[]; - } - - interface ScopeExtractions { - readonly functionExtraction: Extraction; - readonly constantExtraction: Extraction; - } - - /** - * Given a piece of text to extract ('targetRange'), computes a list of possible extractions. - * Each returned ExtractResultForScope corresponds to a possible target scope and is either a set of changes - * or an error explaining why we can't extract into that scope. - */ - function getPossibleExtractions(targetRange: TargetRange, context: RefactorContext): readonly ScopeExtractions[] | undefined { - const { scopes, readsAndWrites: { functionErrorsPerScope, constantErrorsPerScope } } = getPossibleExtractionsWorker(targetRange, context); - // Need the inner type annotation to avoid https://github.com/Microsoft/TypeScript/issues/7547 - const extractions = scopes.map((scope, i): ScopeExtractions => { - const functionDescriptionPart = getDescriptionForFunctionInScope(scope); - const constantDescriptionPart = getDescriptionForConstantInScope(scope); - - const scopeDescription = isFunctionLikeDeclaration(scope) - ? getDescriptionForFunctionLikeDeclaration(scope) - : isClassLike(scope) - ? getDescriptionForClassLikeDeclaration(scope) - : getDescriptionForModuleLikeDeclaration(scope); - - let functionDescription: string; - let constantDescription: string; - if (scopeDescription === SpecialScope.Global) { - functionDescription = formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Extract_to_0_in_1_scope), [functionDescriptionPart, "global"]); - constantDescription = formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Extract_to_0_in_1_scope), [constantDescriptionPart, "global"]); - } - else if (scopeDescription === SpecialScope.Module) { - functionDescription = formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Extract_to_0_in_1_scope), [functionDescriptionPart, "module"]); - constantDescription = formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Extract_to_0_in_1_scope), [constantDescriptionPart, "module"]); - } - else { - functionDescription = formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Extract_to_0_in_1), [functionDescriptionPart, scopeDescription]); - constantDescription = formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Extract_to_0_in_1), [constantDescriptionPart, scopeDescription]); - } - - // Customize the phrasing for the innermost scope to increase clarity. - if (i === 0 && !isClassLike(scope)) { - constantDescription = formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Extract_to_0_in_enclosing_scope), [constantDescriptionPart]); - } - - return { - functionExtraction: { - description: functionDescription, - errors: functionErrorsPerScope[i], - }, - constantExtraction: { - description: constantDescription, - errors: constantErrorsPerScope[i], - }, - }; - }); - return extractions; - } - - function getPossibleExtractionsWorker(targetRange: TargetRange, context: RefactorContext): { readonly scopes: Scope[], readonly readsAndWrites: ReadsAndWrites } { - const { file: sourceFile } = context; - - const scopes = collectEnclosingScopes(targetRange); - const enclosingTextRange = getEnclosingTextRange(targetRange, sourceFile); - const readsAndWrites = collectReadsAndWrites( - targetRange, - scopes, - enclosingTextRange, - sourceFile, - context.program.getTypeChecker(), - context.cancellationToken!); - return { scopes, readsAndWrites }; - } - - function getDescriptionForFunctionInScope(scope: Scope): string { - return isFunctionLikeDeclaration(scope) - ? "inner function" +} +/* @internal */ +function getFunctionExtractionAtIndex(targetRange: TargetRange, context: RefactorContext, requestedChangesIndex: number): RefactorEditInfo { + const { scopes, readsAndWrites: { target, usagesPerScope, functionErrorsPerScope, exposedVariableDeclarations } } = getPossibleExtractionsWorker(targetRange, context); + Debug.assert(!functionErrorsPerScope[requestedChangesIndex].length, "The extraction went missing? How?"); + context.cancellationToken!.throwIfCancellationRequested(); // TODO: GH#18217 + return extractFunctionInScope(target, scopes[requestedChangesIndex], usagesPerScope[requestedChangesIndex], exposedVariableDeclarations, targetRange, context); +} +/* @internal */ +function getConstantExtractionAtIndex(targetRange: TargetRange, context: RefactorContext, requestedChangesIndex: number): RefactorEditInfo { + const { scopes, readsAndWrites: { target, usagesPerScope, constantErrorsPerScope, exposedVariableDeclarations } } = getPossibleExtractionsWorker(targetRange, context); + Debug.assert(!constantErrorsPerScope[requestedChangesIndex].length, "The extraction went missing? How?"); + Debug.assert(exposedVariableDeclarations.length === 0, "Extract constant accepted a range containing a variable declaration?"); + context.cancellationToken!.throwIfCancellationRequested(); + const expression = isExpression(target) + ? target + : (target.statements[0] as ExpressionStatement).expression; + return extractConstantInScope(expression, scopes[requestedChangesIndex], usagesPerScope[requestedChangesIndex], targetRange.facts, context); +} +/* @internal */ +interface Extraction { + readonly description: string; + readonly errors: readonly Diagnostic[]; +} +/* @internal */ +interface ScopeExtractions { + readonly functionExtraction: Extraction; + readonly constantExtraction: Extraction; +} +/** + * Given a piece of text to extract ('targetRange'), computes a list of possible extractions. + * Each returned ExtractResultForScope corresponds to a possible target scope and is either a set of changes + * or an error explaining why we can't extract into that scope. + */ +/* @internal */ +function getPossibleExtractions(targetRange: TargetRange, context: RefactorContext): readonly ScopeExtractions[] | undefined { + const { scopes, readsAndWrites: { functionErrorsPerScope, constantErrorsPerScope } } = getPossibleExtractionsWorker(targetRange, context); + // Need the inner type annotation to avoid https://github.com/Microsoft/TypeScript/issues/7547 + const extractions = scopes.map((scope, i): ScopeExtractions => { + const functionDescriptionPart = getDescriptionForFunctionInScope(scope); + const constantDescriptionPart = getDescriptionForConstantInScope(scope); + const scopeDescription = isFunctionLikeDeclaration(scope) + ? getDescriptionForFunctionLikeDeclaration(scope) : isClassLike(scope) - ? "method" - : "function"; - } - function getDescriptionForConstantInScope(scope: Scope): string { - return isClassLike(scope) - ? "readonly field" - : "constant"; - } - function getDescriptionForFunctionLikeDeclaration(scope: FunctionLikeDeclaration): string { - switch (scope.kind) { - case SyntaxKind.Constructor: - return "constructor"; - case SyntaxKind.FunctionExpression: - case SyntaxKind.FunctionDeclaration: - return scope.name - ? `function '${scope.name.text}'` - : "anonymous function"; - case SyntaxKind.ArrowFunction: - return "arrow function"; - case SyntaxKind.MethodDeclaration: - return `method '${scope.name.getText()}'`; - case SyntaxKind.GetAccessor: - return `'get ${scope.name.getText()}'`; - case SyntaxKind.SetAccessor: - return `'set ${scope.name.getText()}'`; - default: - throw Debug.assertNever(scope, `Unexpected scope kind ${(scope as FunctionLikeDeclaration).kind}`); - } - } - function getDescriptionForClassLikeDeclaration(scope: ClassLikeDeclaration): string { - return scope.kind === SyntaxKind.ClassDeclaration - ? scope.name ? `class '${scope.name.text}'` : "anonymous class declaration" - : scope.name ? `class expression '${scope.name.text}'` : "anonymous class expression"; - } - function getDescriptionForModuleLikeDeclaration(scope: SourceFile | ModuleBlock): string | SpecialScope { - return scope.kind === SyntaxKind.ModuleBlock - ? `namespace '${scope.parent.name.getText()}'` - : scope.externalModuleIndicator ? SpecialScope.Module : SpecialScope.Global; - } - - const enum SpecialScope { - Module, - Global, - } - - /** - * Result of 'extractRange' operation for a specific scope. - * Stores either a list of changes that should be applied to extract a range or a list of errors - */ - function extractFunctionInScope( - node: Statement | Expression | Block, - scope: Scope, - { usages: usagesInScope, typeParameterUsages, substitutions }: ScopeUsages, - exposedVariableDeclarations: readonly VariableDeclaration[], - range: TargetRange, - context: RefactorContext): RefactorEditInfo { - - const checker = context.program.getTypeChecker(); - - // Make a unique name for the extracted function - const file = scope.getSourceFile(); - const functionNameText = getUniqueName(isClassLike(scope) ? "newMethod" : "newFunction", file); - const isJS = isInJSFile(scope); - - const functionName = createIdentifier(functionNameText); - - let returnType: TypeNode | undefined; - const parameters: ParameterDeclaration[] = []; - const callArguments: Identifier[] = []; - let writes: UsageEntry[] | undefined; - usagesInScope.forEach((usage, name) => { - let typeNode: TypeNode | undefined; - if (!isJS) { - let type = checker.getTypeOfSymbolAtLocation(usage.symbol, usage.node); - // Widen the type so we don't emit nonsense annotations like "function fn(x: 3) {" - type = checker.getBaseTypeOfLiteralType(type); - typeNode = checker.typeToTypeNode(type, scope, NodeBuilderFlags.NoTruncation); - } - - const paramDecl = createParameter( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, - /*name*/ name, - /*questionToken*/ undefined, - typeNode - ); - parameters.push(paramDecl); - if (usage.usage === Usage.Write) { - (writes || (writes = [])).push(usage); - } - callArguments.push(createIdentifier(name)); - }); - - const typeParametersAndDeclarations = arrayFrom(typeParameterUsages.values()).map(type => ({ type, declaration: getFirstDeclaration(type) })); - const sortedTypeParametersAndDeclarations = typeParametersAndDeclarations.sort(compareTypesByDeclarationOrder); - - const typeParameters: readonly TypeParameterDeclaration[] | undefined = sortedTypeParametersAndDeclarations.length === 0 - ? undefined - : sortedTypeParametersAndDeclarations.map(t => t.declaration as TypeParameterDeclaration); - - // Strictly speaking, we should check whether each name actually binds to the appropriate type - // parameter. In cases of shadowing, they may not. - const callTypeArguments: readonly TypeNode[] | undefined = typeParameters !== undefined - ? typeParameters.map(decl => createTypeReferenceNode(decl.name, /*typeArguments*/ undefined)) - : undefined; - - // Provide explicit return types for contextually-typed functions - // to avoid problems when there are literal types present - if (isExpression(node) && !isJS) { - const contextualType = checker.getContextualType(node); - returnType = checker.typeToTypeNode(contextualType!, scope, NodeBuilderFlags.NoTruncation); // TODO: GH#18217 - } - - const { body, returnValueProperty } = transformFunctionBody(node, exposedVariableDeclarations, writes, substitutions, !!(range.facts & RangeFacts.HasReturn)); - suppressLeadingAndTrailingTrivia(body); - - let newFunction: MethodDeclaration | FunctionDeclaration; - - if (isClassLike(scope)) { - // always create private method in TypeScript files - const modifiers: Modifier[] = isJS ? [] : [createToken(SyntaxKind.PrivateKeyword)]; - if (range.facts & RangeFacts.InStaticRegion) { - modifiers.push(createToken(SyntaxKind.StaticKeyword)); - } - if (range.facts & RangeFacts.IsAsyncFunction) { - modifiers.push(createToken(SyntaxKind.AsyncKeyword)); - } - newFunction = createMethod( - /*decorators*/ undefined, - modifiers.length ? modifiers : undefined, - range.facts & RangeFacts.IsGenerator ? createToken(SyntaxKind.AsteriskToken) : undefined, - functionName, - /*questionToken*/ undefined, - typeParameters, - parameters, - returnType, - body - ); + ? getDescriptionForClassLikeDeclaration(scope) + : getDescriptionForModuleLikeDeclaration(scope); + let functionDescription: string; + let constantDescription: string; + if (scopeDescription === SpecialScope.Global) { + functionDescription = formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Extract_to_0_in_1_scope), [functionDescriptionPart, "global"]); + constantDescription = formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Extract_to_0_in_1_scope), [constantDescriptionPart, "global"]); + } + else if (scopeDescription === SpecialScope.Module) { + functionDescription = formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Extract_to_0_in_1_scope), [functionDescriptionPart, "module"]); + constantDescription = formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Extract_to_0_in_1_scope), [constantDescriptionPart, "module"]); } else { - newFunction = createFunctionDeclaration( - /*decorators*/ undefined, - range.facts & RangeFacts.IsAsyncFunction ? [createToken(SyntaxKind.AsyncKeyword)] : undefined, - range.facts & RangeFacts.IsGenerator ? createToken(SyntaxKind.AsteriskToken) : undefined, - functionName, - typeParameters, - parameters, - returnType, - body - ); - } - - const changeTracker = textChanges.ChangeTracker.fromContext(context); - const minInsertionPos = (isReadonlyArray(range.range) ? last(range.range) : range.range).end; - const nodeToInsertBefore = getNodeToInsertFunctionBefore(minInsertionPos, scope); - if (nodeToInsertBefore) { - changeTracker.insertNodeBefore(context.file, nodeToInsertBefore, newFunction, /*blankLineBetween*/ true); + functionDescription = formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Extract_to_0_in_1), [functionDescriptionPart, scopeDescription]); + constantDescription = formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Extract_to_0_in_1), [constantDescriptionPart, scopeDescription]); + } + // Customize the phrasing for the innermost scope to increase clarity. + if (i === 0 && !isClassLike(scope)) { + constantDescription = formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Extract_to_0_in_enclosing_scope), [constantDescriptionPart]); + } + return { + functionExtraction: { + description: functionDescription, + errors: functionErrorsPerScope[i], + }, + constantExtraction: { + description: constantDescription, + errors: constantErrorsPerScope[i], + }, + }; + }); + return extractions; +} +/* @internal */ +function getPossibleExtractionsWorker(targetRange: TargetRange, context: RefactorContext): { + readonly scopes: Scope[]; + readonly readsAndWrites: ReadsAndWrites; +} { + const { file: sourceFile } = context; + const scopes = collectEnclosingScopes(targetRange); + const enclosingTextRange = getEnclosingTextRange(targetRange, sourceFile); + const readsAndWrites = collectReadsAndWrites(targetRange, scopes, enclosingTextRange, sourceFile, context.program.getTypeChecker(), context.cancellationToken!); + return { scopes, readsAndWrites }; +} +/* @internal */ +function getDescriptionForFunctionInScope(scope: Scope): string { + return isFunctionLikeDeclaration(scope) + ? "inner function" + : isClassLike(scope) + ? "method" + : "function"; +} +/* @internal */ +function getDescriptionForConstantInScope(scope: Scope): string { + return isClassLike(scope) + ? "readonly field" + : "constant"; +} +/* @internal */ +function getDescriptionForFunctionLikeDeclaration(scope: FunctionLikeDeclaration): string { + switch (scope.kind) { + case SyntaxKind.Constructor: + return "constructor"; + case SyntaxKind.FunctionExpression: + case SyntaxKind.FunctionDeclaration: + return scope.name + ? `function '${scope.name.text}'` + : "anonymous function"; + case SyntaxKind.ArrowFunction: + return "arrow function"; + case SyntaxKind.MethodDeclaration: + return `method '${scope.name.getText()}'`; + case SyntaxKind.GetAccessor: + return `'get ${scope.name.getText()}'`; + case SyntaxKind.SetAccessor: + return `'set ${scope.name.getText()}'`; + default: + throw Debug.assertNever(scope, `Unexpected scope kind ${(scope as FunctionLikeDeclaration).kind}`); + } +} +/* @internal */ +function getDescriptionForClassLikeDeclaration(scope: ClassLikeDeclaration): string { + return scope.kind === SyntaxKind.ClassDeclaration + ? scope.name ? `class '${scope.name.text}'` : "anonymous class declaration" + : scope.name ? `class expression '${scope.name.text}'` : "anonymous class expression"; +} +/* @internal */ +function getDescriptionForModuleLikeDeclaration(scope: SourceFile | ModuleBlock): string | SpecialScope { + return scope.kind === SyntaxKind.ModuleBlock + ? `namespace '${scope.parent.name.getText()}'` + : scope.externalModuleIndicator ? SpecialScope.Module : SpecialScope.Global; +} +/* @internal */ +const enum SpecialScope { + Module, + Global +} +/** + * Result of 'extractRange' operation for a specific scope. + * Stores either a list of changes that should be applied to extract a range or a list of errors + */ +/* @internal */ +function extractFunctionInScope(node: Statement | Expression | Block, scope: Scope, { usages: usagesInScope, typeParameterUsages, substitutions }: ScopeUsages, exposedVariableDeclarations: readonly VariableDeclaration[], range: TargetRange, context: RefactorContext): RefactorEditInfo { + const checker = context.program.getTypeChecker(); + // Make a unique name for the extracted function + const file = scope.getSourceFile(); + const functionNameText = getUniqueName(isClassLike(scope) ? "newMethod" : "newFunction", file); + const isJS = isInJSFile(scope); + const functionName = createIdentifier(functionNameText); + let returnType: TypeNode | undefined; + const parameters: ParameterDeclaration[] = []; + const callArguments: Identifier[] = []; + let writes: UsageEntry[] | undefined; + usagesInScope.forEach((usage, name) => { + let typeNode: TypeNode | undefined; + if (!isJS) { + let type = checker.getTypeOfSymbolAtLocation(usage.symbol, usage.node); + // Widen the type so we don't emit nonsense annotations like "function fn(x: 3) {" + type = checker.getBaseTypeOfLiteralType(type); + typeNode = checker.typeToTypeNode(type, scope, NodeBuilderFlags.NoTruncation); + } + const paramDecl = createParameter( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, + /*name*/ name, + /*questionToken*/ undefined, typeNode); + parameters.push(paramDecl); + if (usage.usage === Usage.Write) { + (writes || (writes = [])).push(usage); + } + callArguments.push(createIdentifier(name)); + }); + const typeParametersAndDeclarations = arrayFrom(typeParameterUsages.values()).map(type => ({ type, declaration: getFirstDeclaration(type) })); + const sortedTypeParametersAndDeclarations = typeParametersAndDeclarations.sort(compareTypesByDeclarationOrder); + const typeParameters: readonly TypeParameterDeclaration[] | undefined = sortedTypeParametersAndDeclarations.length === 0 + ? undefined + : sortedTypeParametersAndDeclarations.map(t => t.declaration as TypeParameterDeclaration); + // Strictly speaking, we should check whether each name actually binds to the appropriate type + // parameter. In cases of shadowing, they may not. + const callTypeArguments: readonly TypeNode[] | undefined = typeParameters !== undefined + ? typeParameters.map(decl => createTypeReferenceNode(decl.name, /*typeArguments*/ undefined)) + : undefined; + // Provide explicit return types for contextually-typed functions + // to avoid problems when there are literal types present + if (isExpression(node) && !isJS) { + const contextualType = checker.getContextualType(node); + returnType = checker.typeToTypeNode((contextualType!), scope, NodeBuilderFlags.NoTruncation); // TODO: GH#18217 + } + const { body, returnValueProperty } = transformFunctionBody(node, exposedVariableDeclarations, writes, substitutions, !!(range.facts & RangeFacts.HasReturn)); + suppressLeadingAndTrailingTrivia(body); + let newFunction: MethodDeclaration | FunctionDeclaration; + if (isClassLike(scope)) { + // always create private method in TypeScript files + const modifiers: Modifier[] = isJS ? [] : [createToken(SyntaxKind.PrivateKeyword)]; + if (range.facts & RangeFacts.InStaticRegion) { + modifiers.push(createToken(SyntaxKind.StaticKeyword)); + } + if (range.facts & RangeFacts.IsAsyncFunction) { + modifiers.push(createToken(SyntaxKind.AsyncKeyword)); + } + newFunction = createMethod( + /*decorators*/ undefined, modifiers.length ? modifiers : undefined, range.facts & RangeFacts.IsGenerator ? createToken(SyntaxKind.AsteriskToken) : undefined, functionName, + /*questionToken*/ undefined, typeParameters, parameters, returnType, body); + } + else { + newFunction = createFunctionDeclaration( + /*decorators*/ undefined, range.facts & RangeFacts.IsAsyncFunction ? [createToken(SyntaxKind.AsyncKeyword)] : undefined, range.facts & RangeFacts.IsGenerator ? createToken(SyntaxKind.AsteriskToken) : undefined, functionName, typeParameters, parameters, returnType, body); + } + const changeTracker = ChangeTracker.fromContext(context); + const minInsertionPos = (isReadonlyArray(range.range) ? last(range.range) : range.range).end; + const nodeToInsertBefore = getNodeToInsertFunctionBefore(minInsertionPos, scope); + if (nodeToInsertBefore) { + changeTracker.insertNodeBefore(context.file, nodeToInsertBefore, newFunction, /*blankLineBetween*/ true); + } + else { + changeTracker.insertNodeAtEndOfScope(context.file, scope, newFunction); + } + const newNodes: Node[] = []; + // replace range with function call + const called = getCalledExpression(scope, range, functionNameText); + let call: Expression = createCall(called, callTypeArguments, // Note that no attempt is made to take advantage of type argument inference + callArguments); + if (range.facts & RangeFacts.IsGenerator) { + call = createYield(createToken(SyntaxKind.AsteriskToken), call); + } + if (range.facts & RangeFacts.IsAsyncFunction) { + call = createAwait(call); + } + if (exposedVariableDeclarations.length && !writes) { + // No need to mix declarations and writes. + // How could any variables be exposed if there's a return statement? + Debug.assert(!returnValueProperty, "Expected no returnValueProperty"); + Debug.assert(!(range.facts & RangeFacts.HasReturn), "Expected RangeFacts.HasReturn flag to be unset"); + if (exposedVariableDeclarations.length === 1) { + // Declaring exactly one variable: let x = newFunction(); + const variableDeclaration = exposedVariableDeclarations[0]; + newNodes.push(createVariableStatement( + /*modifiers*/ undefined, createVariableDeclarationList([createVariableDeclaration(getSynthesizedDeepClone(variableDeclaration.name), /*type*/ getSynthesizedDeepClone(variableDeclaration.type), /*initializer*/ call)], // TODO (acasey): test binding patterns + variableDeclaration.parent.flags))); } else { - changeTracker.insertNodeAtEndOfScope(context.file, scope, newFunction); - } - - const newNodes: Node[] = []; - // replace range with function call - const called = getCalledExpression(scope, range, functionNameText); - - let call: Expression = createCall( - called, - callTypeArguments, // Note that no attempt is made to take advantage of type argument inference - callArguments); - if (range.facts & RangeFacts.IsGenerator) { - call = createYield(createToken(SyntaxKind.AsteriskToken), call); + // Declaring multiple variables / return properties: + // let {x, y} = newFunction(); + const bindingElements: BindingElement[] = []; + const typeElements: TypeElement[] = []; + let commonNodeFlags = exposedVariableDeclarations[0].parent.flags; + let sawExplicitType = false; + for (const variableDeclaration of exposedVariableDeclarations) { + bindingElements.push(createBindingElement( + /*dotDotDotToken*/ undefined, + /*propertyName*/ undefined, + /*name*/ getSynthesizedDeepClone(variableDeclaration.name))); + // Being returned through an object literal will have widened the type. + const variableType: TypeNode | undefined = checker.typeToTypeNode(checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(variableDeclaration)), scope, NodeBuilderFlags.NoTruncation); + typeElements.push(createPropertySignature( + /*modifiers*/ undefined, + /*name*/ variableDeclaration.symbol.name, + /*questionToken*/ undefined, + /*type*/ variableType, + /*initializer*/ undefined)); + sawExplicitType = sawExplicitType || variableDeclaration.type !== undefined; + commonNodeFlags = commonNodeFlags & variableDeclaration.parent.flags; + } + const typeLiteral: TypeLiteralNode | undefined = sawExplicitType ? createTypeLiteralNode(typeElements) : undefined; + if (typeLiteral) { + setEmitFlags(typeLiteral, EmitFlags.SingleLine); + } + newNodes.push(createVariableStatement( + /*modifiers*/ undefined, createVariableDeclarationList([createVariableDeclaration(createObjectBindingPattern(bindingElements), + /*type*/ typeLiteral, + /*initializer*/ call)], commonNodeFlags))); } - if (range.facts & RangeFacts.IsAsyncFunction) { - call = createAwait(call); - } - - if (exposedVariableDeclarations.length && !writes) { - // No need to mix declarations and writes. - - // How could any variables be exposed if there's a return statement? - Debug.assert(!returnValueProperty, "Expected no returnValueProperty"); - Debug.assert(!(range.facts & RangeFacts.HasReturn), "Expected RangeFacts.HasReturn flag to be unset"); - - if (exposedVariableDeclarations.length === 1) { - // Declaring exactly one variable: let x = newFunction(); - const variableDeclaration = exposedVariableDeclarations[0]; - newNodes.push(createVariableStatement( - /*modifiers*/ undefined, - createVariableDeclarationList( - [createVariableDeclaration(getSynthesizedDeepClone(variableDeclaration.name), /*type*/ getSynthesizedDeepClone(variableDeclaration.type), /*initializer*/ call)], // TODO (acasey): test binding patterns - variableDeclaration.parent.flags))); - } - else { - // Declaring multiple variables / return properties: - // let {x, y} = newFunction(); - const bindingElements: BindingElement[] = []; - const typeElements: TypeElement[] = []; - let commonNodeFlags = exposedVariableDeclarations[0].parent.flags; - let sawExplicitType = false; - for (const variableDeclaration of exposedVariableDeclarations) { - bindingElements.push(createBindingElement( - /*dotDotDotToken*/ undefined, - /*propertyName*/ undefined, - /*name*/ getSynthesizedDeepClone(variableDeclaration.name))); - - // Being returned through an object literal will have widened the type. - const variableType: TypeNode | undefined = checker.typeToTypeNode( - checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(variableDeclaration)), - scope, - NodeBuilderFlags.NoTruncation); - - typeElements.push(createPropertySignature( - /*modifiers*/ undefined, - /*name*/ variableDeclaration.symbol.name, - /*questionToken*/ undefined, - /*type*/ variableType, - /*initializer*/ undefined)); - sawExplicitType = sawExplicitType || variableDeclaration.type !== undefined; - commonNodeFlags = commonNodeFlags & variableDeclaration.parent.flags; - } - - const typeLiteral: TypeLiteralNode | undefined = sawExplicitType ? createTypeLiteralNode(typeElements) : undefined; - if (typeLiteral) { - setEmitFlags(typeLiteral, EmitFlags.SingleLine); + } + else if (exposedVariableDeclarations.length || writes) { + if (exposedVariableDeclarations.length) { + // CONSIDER: we're going to create one statement per variable, but we could actually preserve their original grouping. + for (const variableDeclaration of exposedVariableDeclarations) { + let flags: NodeFlags = variableDeclaration.parent.flags; + if (flags & NodeFlags.Const) { + flags = (flags & ~NodeFlags.Const) | NodeFlags.Let; } - newNodes.push(createVariableStatement( - /*modifiers*/ undefined, - createVariableDeclarationList( - [createVariableDeclaration( - createObjectBindingPattern(bindingElements), - /*type*/ typeLiteral, - /*initializer*/call)], - commonNodeFlags))); + /*modifiers*/ undefined, createVariableDeclarationList([createVariableDeclaration(variableDeclaration.symbol.name, getTypeDeepCloneUnionUndefined(variableDeclaration.type))], flags))); } } - else if (exposedVariableDeclarations.length || writes) { - if (exposedVariableDeclarations.length) { - // CONSIDER: we're going to create one statement per variable, but we could actually preserve their original grouping. - for (const variableDeclaration of exposedVariableDeclarations) { - let flags: NodeFlags = variableDeclaration.parent.flags; - if (flags & NodeFlags.Const) { - flags = (flags & ~NodeFlags.Const) | NodeFlags.Let; - } - - newNodes.push(createVariableStatement( - /*modifiers*/ undefined, - createVariableDeclarationList( - [createVariableDeclaration(variableDeclaration.symbol.name, getTypeDeepCloneUnionUndefined(variableDeclaration.type))], - flags))); - } - } - - if (returnValueProperty) { - // has both writes and return, need to create variable declaration to hold return value; - newNodes.push(createVariableStatement( - /*modifiers*/ undefined, - createVariableDeclarationList( - [createVariableDeclaration(returnValueProperty, getTypeDeepCloneUnionUndefined(returnType))], - NodeFlags.Let))); - } - - const assignments = getPropertyAssignmentsForWritesAndVariableDeclarations(exposedVariableDeclarations, writes); - if (returnValueProperty) { - assignments.unshift(createShorthandPropertyAssignment(returnValueProperty)); - } - - // propagate writes back - if (assignments.length === 1) { - // We would only have introduced a return value property if there had been - // other assignments to make. - Debug.assert(!returnValueProperty, "Shouldn't have returnValueProperty here"); - - newNodes.push(createStatement(createAssignment(assignments[0].name, call))); - - if (range.facts & RangeFacts.HasReturn) { - newNodes.push(createReturn()); - } - } - else { - // emit e.g. - // { a, b, __return } = newFunction(a, b); - // return __return; - newNodes.push(createStatement(createAssignment(createObjectLiteral(assignments), call))); - if (returnValueProperty) { - newNodes.push(createReturn(createIdentifier(returnValueProperty))); - } - } + if (returnValueProperty) { + // has both writes and return, need to create variable declaration to hold return value; + newNodes.push(createVariableStatement( + /*modifiers*/ undefined, createVariableDeclarationList([createVariableDeclaration(returnValueProperty, getTypeDeepCloneUnionUndefined(returnType))], NodeFlags.Let))); } - else { + const assignments = getPropertyAssignmentsForWritesAndVariableDeclarations(exposedVariableDeclarations, writes); + if (returnValueProperty) { + assignments.unshift(createShorthandPropertyAssignment(returnValueProperty)); + } + // propagate writes back + if (assignments.length === 1) { + // We would only have introduced a return value property if there had been + // other assignments to make. + Debug.assert(!returnValueProperty, "Shouldn't have returnValueProperty here"); + newNodes.push(createStatement(createAssignment(assignments[0].name, call))); if (range.facts & RangeFacts.HasReturn) { - newNodes.push(createReturn(call)); + newNodes.push(createReturn()); } - else if (isReadonlyArray(range.range)) { - newNodes.push(createStatement(call)); - } - else { - newNodes.push(call); + } + else { + // emit e.g. + // { a, b, __return } = newFunction(a, b); + // return __return; + newNodes.push(createStatement(createAssignment(createObjectLiteral(assignments), call))); + if (returnValueProperty) { + newNodes.push(createReturn(createIdentifier(returnValueProperty))); } } - - if (isReadonlyArray(range.range)) { - changeTracker.replaceNodeRangeWithNodes(context.file, first(range.range), last(range.range), newNodes); + } + else { + if (range.facts & RangeFacts.HasReturn) { + newNodes.push(createReturn(call)); + } + else if (isReadonlyArray(range.range)) { + newNodes.push(createStatement(call)); } else { - changeTracker.replaceNodeWithNodes(context.file, range.range, newNodes); - } - - const edits = changeTracker.getChanges(); - const renameRange = isReadonlyArray(range.range) ? first(range.range) : range.range; - - const renameFilename = renameRange.getSourceFile().fileName; - const renameLocation = getRenameLocation(edits, renameFilename, functionNameText, /*isDeclaredBeforeUse*/ false); - return { renameFilename, renameLocation, edits }; - - function getTypeDeepCloneUnionUndefined(typeNode: TypeNode | undefined): TypeNode | undefined { - if (typeNode === undefined) { - return undefined; - } - - const clone = getSynthesizedDeepClone(typeNode); - let withoutParens = clone; - while (isParenthesizedTypeNode(withoutParens)) { - withoutParens = withoutParens.type; - } - return isUnionTypeNode(withoutParens) && find(withoutParens.types, t => t.kind === SyntaxKind.UndefinedKeyword) - ? clone - : createUnionTypeNode([clone, createKeywordTypeNode(SyntaxKind.UndefinedKeyword)]); + newNodes.push(call); } } - - /** - * Result of 'extractRange' operation for a specific scope. - * Stores either a list of changes that should be applied to extract a range or a list of errors - */ - function extractConstantInScope( - node: Expression, - scope: Scope, - { substitutions }: ScopeUsages, - rangeFacts: RangeFacts, - context: RefactorContext): RefactorEditInfo { - - const checker = context.program.getTypeChecker(); - - // Make a unique name for the extracted variable - const file = scope.getSourceFile(); - const localNameText = getUniqueName(isClassLike(scope) ? "newProperty" : "newLocal", file); - const isJS = isInJSFile(scope); - - let variableType = isJS || !checker.isContextSensitive(node) - ? undefined - : checker.typeToTypeNode(checker.getContextualType(node)!, scope, NodeBuilderFlags.NoTruncation); // TODO: GH#18217 - - let initializer = transformConstantInitializer(node, substitutions); - - ({ variableType, initializer } = transformFunctionInitializerAndType(variableType, initializer)); - - suppressLeadingAndTrailingTrivia(initializer); - - const changeTracker = textChanges.ChangeTracker.fromContext(context); - - if (isClassLike(scope)) { - Debug.assert(!isJS, "Cannot extract to a JS class"); // See CannotExtractToJSClass - const modifiers: Modifier[] = []; - modifiers.push(createToken(SyntaxKind.PrivateKeyword)); - if (rangeFacts & RangeFacts.InStaticRegion) { - modifiers.push(createToken(SyntaxKind.StaticKeyword)); - } - modifiers.push(createToken(SyntaxKind.ReadonlyKeyword)); - - const newVariable = createProperty( - /*decorators*/ undefined, - modifiers, - localNameText, - /*questionToken*/ undefined, - variableType, - initializer); - - const localReference = createPropertyAccess( - rangeFacts & RangeFacts.InStaticRegion - ? createIdentifier(scope.name!.getText()) // TODO: GH#18217 - : createThis(), - createIdentifier(localNameText)); - + if (isReadonlyArray(range.range)) { + changeTracker.replaceNodeRangeWithNodes(context.file, first(range.range), last(range.range), newNodes); + } + else { + changeTracker.replaceNodeWithNodes(context.file, range.range, newNodes); + } + const edits = changeTracker.getChanges(); + const renameRange = isReadonlyArray(range.range) ? first(range.range) : range.range; + const renameFilename = renameRange.getSourceFile().fileName; + const renameLocation = getRenameLocation(edits, renameFilename, functionNameText, /*isDeclaredBeforeUse*/ false); + return { renameFilename, renameLocation, edits }; + function getTypeDeepCloneUnionUndefined(typeNode: TypeNode | undefined): TypeNode | undefined { + if (typeNode === undefined) { + return undefined; + } + const clone = getSynthesizedDeepClone(typeNode); + let withoutParens = clone; + while (isParenthesizedTypeNode(withoutParens)) { + withoutParens = withoutParens.type; + } + return isUnionTypeNode(withoutParens) && find(withoutParens.types, t => t.kind === SyntaxKind.UndefinedKeyword) + ? clone + : createUnionTypeNode([clone, createKeywordTypeNode(SyntaxKind.UndefinedKeyword)]); + } +} +/** + * Result of 'extractRange' operation for a specific scope. + * Stores either a list of changes that should be applied to extract a range or a list of errors + */ +/* @internal */ +function extractConstantInScope(node: Expression, scope: Scope, { substitutions }: ScopeUsages, rangeFacts: RangeFacts, context: RefactorContext): RefactorEditInfo { + const checker = context.program.getTypeChecker(); + // Make a unique name for the extracted variable + const file = scope.getSourceFile(); + const localNameText = getUniqueName(isClassLike(scope) ? "newProperty" : "newLocal", file); + const isJS = isInJSFile(scope); + let variableType = isJS || !checker.isContextSensitive(node) + ? undefined + : checker.typeToTypeNode((checker.getContextualType(node)!), scope, NodeBuilderFlags.NoTruncation); // TODO: GH#18217 + let initializer = transformConstantInitializer(node, substitutions); + ({ variableType, initializer } = transformFunctionInitializerAndType(variableType, initializer)); + suppressLeadingAndTrailingTrivia(initializer); + const changeTracker = ChangeTracker.fromContext(context); + if (isClassLike(scope)) { + Debug.assert(!isJS, "Cannot extract to a JS class"); // See CannotExtractToJSClass + const modifiers: Modifier[] = []; + modifiers.push(createToken(SyntaxKind.PrivateKeyword)); + if (rangeFacts & RangeFacts.InStaticRegion) { + modifiers.push(createToken(SyntaxKind.StaticKeyword)); + } + modifiers.push(createToken(SyntaxKind.ReadonlyKeyword)); + const newVariable = createProperty( + /*decorators*/ undefined, modifiers, localNameText, + /*questionToken*/ undefined, variableType, initializer); + const localReference = createPropertyAccess(rangeFacts & RangeFacts.InStaticRegion + ? createIdentifier(scope.name!.getText()) // TODO: GH#18217 + : createThis(), createIdentifier(localNameText)); + // Declare + const maxInsertionPos = node.pos; + const nodeToInsertBefore = getNodeToInsertPropertyBefore(maxInsertionPos, scope); + changeTracker.insertNodeBefore(context.file, nodeToInsertBefore, newVariable, /*blankLineBetween*/ true); + // Consume + changeTracker.replaceNode(context.file, node, localReference); + } + else { + const newVariableDeclaration = createVariableDeclaration(localNameText, variableType, initializer); + // If the node is part of an initializer in a list of variable declarations, insert a new + // variable declaration into the list (in case it depends on earlier ones). + // CONSIDER: If the declaration list isn't const, we might want to split it into multiple + // lists so that the newly extracted one can be const. + const oldVariableDeclaration = getContainingVariableDeclarationIfInList(node, scope); + if (oldVariableDeclaration) { // Declare - const maxInsertionPos = node.pos; - const nodeToInsertBefore = getNodeToInsertPropertyBefore(maxInsertionPos, scope); - changeTracker.insertNodeBefore(context.file, nodeToInsertBefore, newVariable, /*blankLineBetween*/ true); - + // CONSIDER: could detect that each is on a separate line (See `extractConstant_VariableList_MultipleLines` in `extractConstants.ts`) + changeTracker.insertNodeBefore(context.file, oldVariableDeclaration, newVariableDeclaration); // Consume + const localReference = createIdentifier(localNameText); changeTracker.replaceNode(context.file, node, localReference); } + else if (node.parent.kind === SyntaxKind.ExpressionStatement && scope === findAncestor(node, isScope)) { + // If the parent is an expression statement and the target scope is the immediately enclosing one, + // replace the statement with the declaration. + const newVariableStatement = createVariableStatement( + /*modifiers*/ undefined, createVariableDeclarationList([newVariableDeclaration], NodeFlags.Const)); + changeTracker.replaceNode(context.file, node.parent, newVariableStatement); + } else { - const newVariableDeclaration = createVariableDeclaration(localNameText, variableType, initializer); - - // If the node is part of an initializer in a list of variable declarations, insert a new - // variable declaration into the list (in case it depends on earlier ones). - // CONSIDER: If the declaration list isn't const, we might want to split it into multiple - // lists so that the newly extracted one can be const. - const oldVariableDeclaration = getContainingVariableDeclarationIfInList(node, scope); - if (oldVariableDeclaration) { - // Declare - // CONSIDER: could detect that each is on a separate line (See `extractConstant_VariableList_MultipleLines` in `extractConstants.ts`) - changeTracker.insertNodeBefore(context.file, oldVariableDeclaration, newVariableDeclaration); - - // Consume - const localReference = createIdentifier(localNameText); - changeTracker.replaceNode(context.file, node, localReference); - } - else if (node.parent.kind === SyntaxKind.ExpressionStatement && scope === findAncestor(node, isScope)) { - // If the parent is an expression statement and the target scope is the immediately enclosing one, - // replace the statement with the declaration. - const newVariableStatement = createVariableStatement( - /*modifiers*/ undefined, - createVariableDeclarationList([newVariableDeclaration], NodeFlags.Const)); - changeTracker.replaceNode(context.file, node.parent, newVariableStatement); + const newVariableStatement = createVariableStatement( + /*modifiers*/ undefined, createVariableDeclarationList([newVariableDeclaration], NodeFlags.Const)); + // Declare + const nodeToInsertBefore = getNodeToInsertConstantBefore(node, scope); + if (nodeToInsertBefore.pos === 0) { + changeTracker.insertNodeAtTopOfFile(context.file, newVariableStatement, /*blankLineBetween*/ false); } else { - const newVariableStatement = createVariableStatement( - /*modifiers*/ undefined, - createVariableDeclarationList([newVariableDeclaration], NodeFlags.Const)); - - // Declare - const nodeToInsertBefore = getNodeToInsertConstantBefore(node, scope); - if (nodeToInsertBefore.pos === 0) { - changeTracker.insertNodeAtTopOfFile(context.file, newVariableStatement, /*blankLineBetween*/ false); - } - else { - changeTracker.insertNodeBefore(context.file, nodeToInsertBefore, newVariableStatement, /*blankLineBetween*/ false); - } - - // Consume - if (node.parent.kind === SyntaxKind.ExpressionStatement) { - // If the parent is an expression statement, delete it. - changeTracker.delete(context.file, node.parent); - } - else { - const localReference = createIdentifier(localNameText); - changeTracker.replaceNode(context.file, node, localReference); - } + changeTracker.insertNodeBefore(context.file, nodeToInsertBefore, newVariableStatement, /*blankLineBetween*/ false); } - } - - const edits = changeTracker.getChanges(); - - const renameFilename = node.getSourceFile().fileName; - const renameLocation = getRenameLocation(edits, renameFilename, localNameText, /*isDeclaredBeforeUse*/ true); - return { renameFilename, renameLocation, edits }; - - function transformFunctionInitializerAndType(variableType: TypeNode | undefined, initializer: Expression): { variableType: TypeNode | undefined, initializer: Expression } { - // If no contextual type exists there is nothing to transfer to the function signature - if (variableType === undefined) return { variableType, initializer }; - // Only do this for function expressions and arrow functions that are not generic - if (!isFunctionExpression(initializer) && !isArrowFunction(initializer) || !!initializer.typeParameters) return { variableType, initializer }; - const functionType = checker.getTypeAtLocation(node); - const functionSignature = singleOrUndefined(checker.getSignaturesOfType(functionType, SignatureKind.Call)); - - // If no function signature, maybe there was an error, do nothing - if (!functionSignature) return { variableType, initializer }; - // If the function signature has generic type parameters we don't attempt to move the parameters - if (!!functionSignature.getTypeParameters()) return { variableType, initializer }; - - // We add parameter types if needed - const parameters: ParameterDeclaration[] = []; - let hasAny = false; - for (const p of initializer.parameters) { - if (p.type) { - parameters.push(p); - } - else { - const paramType = checker.getTypeAtLocation(p); - if (paramType === checker.getAnyType()) hasAny = true; - - parameters.push(updateParameter(p, - p.decorators, p.modifiers, p.dotDotDotToken, - p.name, p.questionToken, p.type || checker.typeToTypeNode(paramType, scope, NodeBuilderFlags.NoTruncation), p.initializer)); - } - } - // If a parameter was inferred as any we skip adding function parameters at all. - // Turning an implicit any (which under common settings is a error) to an explicit - // is probably actually a worse refactor outcome. - if (hasAny) return { variableType, initializer }; - variableType = undefined; - if (isArrowFunction(initializer)) { - initializer = updateArrowFunction(initializer, node.modifiers, initializer.typeParameters, - parameters, - initializer.type || checker.typeToTypeNode(functionSignature.getReturnType(), scope, NodeBuilderFlags.NoTruncation), - initializer.equalsGreaterThanToken, - initializer.body); + // Consume + if (node.parent.kind === SyntaxKind.ExpressionStatement) { + // If the parent is an expression statement, delete it. + changeTracker.delete(context.file, node.parent); } else { - if (functionSignature && !!functionSignature.thisParameter) { - const firstParameter = firstOrUndefined(parameters); - // If the function signature has a this parameter and if the first defined parameter is not the this parameter, we must add it - // Note: If this parameter was already there, it would have been previously updated with the type if not type was present - if ((!firstParameter || (isIdentifier(firstParameter.name) && firstParameter.name.escapedText !== "this"))) { - const thisType = checker.getTypeOfSymbolAtLocation(functionSignature.thisParameter, node); - parameters.splice(0, 0, createParameter( - /* decorators */ undefined, - /* modifiers */ undefined, - /* dotDotDotToken */ undefined, - "this", - /* questionToken */ undefined, - checker.typeToTypeNode(thisType, scope, NodeBuilderFlags.NoTruncation) - )); - } - } - initializer = updateFunctionExpression(initializer, node.modifiers, initializer.asteriskToken, - initializer.name, initializer.typeParameters, - parameters, - initializer.type || checker.typeToTypeNode(functionSignature.getReturnType(), scope, NodeBuilderFlags.NoTruncation), - initializer.body); + const localReference = createIdentifier(localNameText); + changeTracker.replaceNode(context.file, node, localReference); } - return { variableType, initializer }; } } - - function getContainingVariableDeclarationIfInList(node: Node, scope: Scope) { - let prevNode; - while (node !== undefined && node !== scope) { - if (isVariableDeclaration(node) && - node.initializer === prevNode && - isVariableDeclarationList(node.parent) && - node.parent.declarations.length > 1) { - - return node; + const edits = changeTracker.getChanges(); + const renameFilename = node.getSourceFile().fileName; + const renameLocation = getRenameLocation(edits, renameFilename, localNameText, /*isDeclaredBeforeUse*/ true); + return { renameFilename, renameLocation, edits }; + function transformFunctionInitializerAndType(variableType: TypeNode | undefined, initializer: Expression): { + variableType: TypeNode | undefined; + initializer: Expression; + } { + // If no contextual type exists there is nothing to transfer to the function signature + if (variableType === undefined) + return { variableType, initializer }; + // Only do this for function expressions and arrow functions that are not generic + if (!isFunctionExpression(initializer) && !isArrowFunction(initializer) || !!initializer.typeParameters) + return { variableType, initializer }; + const functionType = checker.getTypeAtLocation(node); + const functionSignature = singleOrUndefined(checker.getSignaturesOfType(functionType, SignatureKind.Call)); + // If no function signature, maybe there was an error, do nothing + if (!functionSignature) + return { variableType, initializer }; + // If the function signature has generic type parameters we don't attempt to move the parameters + if (!!functionSignature.getTypeParameters()) + return { variableType, initializer }; + // We add parameter types if needed + const parameters: ParameterDeclaration[] = []; + let hasAny = false; + for (const p of initializer.parameters) { + if (p.type) { + parameters.push(p); } - - prevNode = node; - node = node.parent; - } - } - - function getFirstDeclaration(type: Type): Declaration | undefined { - let firstDeclaration; - - const symbol = type.symbol; - if (symbol && symbol.declarations) { - for (const declaration of symbol.declarations) { - if (firstDeclaration === undefined || declaration.pos < firstDeclaration.pos) { - firstDeclaration = declaration; - } + else { + const paramType = checker.getTypeAtLocation(p); + if (paramType === checker.getAnyType()) + hasAny = true; + parameters.push(updateParameter(p, p.decorators, p.modifiers, p.dotDotDotToken, p.name, p.questionToken, p.type || checker.typeToTypeNode(paramType, scope, NodeBuilderFlags.NoTruncation), p.initializer)); } } - - return firstDeclaration; - } - - function compareTypesByDeclarationOrder( - {type: type1, declaration: declaration1}: {type: Type, declaration?: Declaration}, - {type: type2, declaration: declaration2}: {type: Type, declaration?: Declaration}) { - - return compareProperties(declaration1, declaration2, "pos", compareValues) - || compareStringsCaseSensitive( - type1.symbol ? type1.symbol.getName() : "", - type2.symbol ? type2.symbol.getName() : "") - || compareValues(type1.id, type2.id); - } - - function getCalledExpression(scope: Node, range: TargetRange, functionNameText: string): Expression { - const functionReference = createIdentifier(functionNameText); - if (isClassLike(scope)) { - const lhs = range.facts & RangeFacts.InStaticRegion ? createIdentifier(scope.name!.text) : createThis(); // TODO: GH#18217 - return createPropertyAccess(lhs, functionReference); - } - else { - return functionReference; - } - } - - function transformFunctionBody(body: Node, exposedVariableDeclarations: readonly VariableDeclaration[], writes: readonly UsageEntry[] | undefined, substitutions: ReadonlyMap, hasReturn: boolean): { body: Block, returnValueProperty: string | undefined } { - const hasWritesOrVariableDeclarations = writes !== undefined || exposedVariableDeclarations.length > 0; - if (isBlock(body) && !hasWritesOrVariableDeclarations && substitutions.size === 0) { - // already block, no declarations or writes to propagate back, no substitutions - can use node as is - return { body: createBlock(body.statements, /*multLine*/ true), returnValueProperty: undefined }; - } - let returnValueProperty: string | undefined; - let ignoreReturns = false; - const statements = createNodeArray(isBlock(body) ? body.statements.slice(0) : [isStatement(body) ? body : createReturn(body)]); - // rewrite body if either there are writes that should be propagated back via return statements or there are substitutions - if (hasWritesOrVariableDeclarations || substitutions.size) { - const rewrittenStatements = visitNodes(statements, visitor).slice(); - if (hasWritesOrVariableDeclarations && !hasReturn && isStatement(body)) { - // add return at the end to propagate writes back in case if control flow falls out of the function body - // it is ok to know that range has at least one return since it we only allow unconditional returns - const assignments = getPropertyAssignmentsForWritesAndVariableDeclarations(exposedVariableDeclarations, writes); - if (assignments.length === 1) { - rewrittenStatements.push(createReturn(assignments[0].name)); - } - else { - rewrittenStatements.push(createReturn(createObjectLiteral(assignments))); - } - } - return { body: createBlock(rewrittenStatements, /*multiLine*/ true), returnValueProperty }; + // If a parameter was inferred as any we skip adding function parameters at all. + // Turning an implicit any (which under common settings is a error) to an explicit + // is probably actually a worse refactor outcome. + if (hasAny) + return { variableType, initializer }; + variableType = undefined; + if (isArrowFunction(initializer)) { + initializer = updateArrowFunction(initializer, node.modifiers, initializer.typeParameters, parameters, initializer.type || checker.typeToTypeNode(functionSignature.getReturnType(), scope, NodeBuilderFlags.NoTruncation), initializer.equalsGreaterThanToken, initializer.body); } else { - return { body: createBlock(statements, /*multiLine*/ true), returnValueProperty: undefined }; - } - - function visitor(node: Node): VisitResult { - if (!ignoreReturns && node.kind === SyntaxKind.ReturnStatement && hasWritesOrVariableDeclarations) { - const assignments: ObjectLiteralElementLike[] = getPropertyAssignmentsForWritesAndVariableDeclarations(exposedVariableDeclarations, writes); - if ((node).expression) { - if (!returnValueProperty) { - returnValueProperty = "__return"; - } - assignments.unshift(createPropertyAssignment(returnValueProperty, visitNode((node).expression, visitor))); - } - if (assignments.length === 1) { - return createReturn(assignments[0].name as Expression); - } - else { - return createReturn(createObjectLiteral(assignments)); + if (functionSignature && !!functionSignature.thisParameter) { + const firstParameter = firstOrUndefined(parameters); + // If the function signature has a this parameter and if the first defined parameter is not the this parameter, we must add it + // Note: If this parameter was already there, it would have been previously updated with the type if not type was present + if ((!firstParameter || (isIdentifier(firstParameter.name) && firstParameter.name.escapedText !== "this"))) { + const thisType = checker.getTypeOfSymbolAtLocation(functionSignature.thisParameter, node); + parameters.splice(0, 0, createParameter( + /* decorators */ undefined, + /* modifiers */ undefined, + /* dotDotDotToken */ undefined, "this", + /* questionToken */ undefined, checker.typeToTypeNode(thisType, scope, NodeBuilderFlags.NoTruncation))); } } - else { - const oldIgnoreReturns = ignoreReturns; - ignoreReturns = ignoreReturns || isFunctionLikeDeclaration(node) || isClassLike(node); - const substitution = substitutions.get(getNodeId(node).toString()); - const result = substitution ? getSynthesizedDeepClone(substitution) : visitEachChild(node, visitor, nullTransformationContext); - ignoreReturns = oldIgnoreReturns; - return result; - } + initializer = updateFunctionExpression(initializer, node.modifiers, initializer.asteriskToken, initializer.name, initializer.typeParameters, parameters, initializer.type || checker.typeToTypeNode(functionSignature.getReturnType(), scope, NodeBuilderFlags.NoTruncation), initializer.body); } + return { variableType, initializer }; } - - function transformConstantInitializer(initializer: Expression, substitutions: ReadonlyMap): Expression { - return substitutions.size - ? visitor(initializer) as Expression - : initializer; - - function visitor(node: Node): VisitResult { - const substitution = substitutions.get(getNodeId(node).toString()); - return substitution ? getSynthesizedDeepClone(substitution) : visitEachChild(node, visitor, nullTransformationContext); +} +/* @internal */ +function getContainingVariableDeclarationIfInList(node: Node, scope: Scope) { + let prevNode; + while (node !== undefined && node !== scope) { + if (isVariableDeclaration(node) && + node.initializer === prevNode && + isVariableDeclarationList(node.parent) && + node.parent.declarations.length > 1) { + return node; } + prevNode = node; + node = node.parent; } - - function getStatementsOrClassElements(scope: Scope): readonly Statement[] | readonly ClassElement[] { - if (isFunctionLikeDeclaration(scope)) { - const body = scope.body!; // TODO: GH#18217 - if (isBlock(body)) { - return body.statements; +} +/* @internal */ +function getFirstDeclaration(type: Type): Declaration | undefined { + let firstDeclaration; + const symbol = type.symbol; + if (symbol && symbol.declarations) { + for (const declaration of symbol.declarations) { + if (firstDeclaration === undefined || declaration.pos < firstDeclaration.pos) { + firstDeclaration = declaration; } } - else if (isModuleBlock(scope) || isSourceFile(scope)) { - return scope.statements; - } - else if (isClassLike(scope)) { - return scope.members; - } - else { - assertType(scope); - } - - return emptyArray; } - - /** - * If `scope` contains a function after `minPos`, then return the first such function. - * Otherwise, return `undefined`. - */ - function getNodeToInsertFunctionBefore(minPos: number, scope: Scope): Statement | ClassElement | undefined { - return find(getStatementsOrClassElements(scope), child => - child.pos >= minPos && isFunctionLikeDeclaration(child) && !isConstructorDeclaration(child)); - } - - function getNodeToInsertPropertyBefore(maxPos: number, scope: ClassLikeDeclaration): ClassElement { - const members = scope.members; - Debug.assert(members.length > 0, "Found no members"); // There must be at least one child, since we extracted from one. - - let prevMember: ClassElement | undefined; - let allProperties = true; - for (const member of members) { - if (member.pos > maxPos) { - return prevMember || members[0]; - } - if (allProperties && !isPropertyDeclaration(member)) { - // If it is non-vacuously true that all preceding members are properties, - // insert before the current member (i.e. at the end of the list of properties). - if (prevMember !== undefined) { - return member; - } - - allProperties = false; + return firstDeclaration; +} +/* @internal */ +function compareTypesByDeclarationOrder({ type: type1, declaration: declaration1 }: { + type: Type; + declaration?: Declaration; +}, { type: type2, declaration: declaration2 }: { + type: Type; + declaration?: Declaration; +}) { + return compareProperties(declaration1, declaration2, "pos", compareValues) + || compareStringsCaseSensitive(type1.symbol ? type1.symbol.getName() : "", type2.symbol ? type2.symbol.getName() : "") + || compareValues(type1.id, type2.id); +} +/* @internal */ +function getCalledExpression(scope: Node, range: TargetRange, functionNameText: string): Expression { + const functionReference = createIdentifier(functionNameText); + if (isClassLike(scope)) { + const lhs = range.facts & RangeFacts.InStaticRegion ? createIdentifier(scope.name!.text) : createThis(); // TODO: GH#18217 + return createPropertyAccess(lhs, functionReference); + } + else { + return functionReference; + } +} +/* @internal */ +function transformFunctionBody(body: Node, exposedVariableDeclarations: readonly VariableDeclaration[], writes: readonly UsageEntry[] | undefined, substitutions: ts.ReadonlyMap, hasReturn: boolean): { + body: Block; + returnValueProperty: string | undefined; +} { + const hasWritesOrVariableDeclarations = writes !== undefined || exposedVariableDeclarations.length > 0; + if (isBlock(body) && !hasWritesOrVariableDeclarations && substitutions.size === 0) { + // already block, no declarations or writes to propagate back, no substitutions - can use node as is + return { body: createBlock(body.statements, /*multLine*/ true), returnValueProperty: undefined }; + } + let returnValueProperty: string | undefined; + let ignoreReturns = false; + const statements = createNodeArray(isBlock(body) ? body.statements.slice(0) : [isStatement(body) ? body : createReturn((body))]); + // rewrite body if either there are writes that should be propagated back via return statements or there are substitutions + if (hasWritesOrVariableDeclarations || substitutions.size) { + const rewrittenStatements = visitNodes(statements, visitor).slice(); + if (hasWritesOrVariableDeclarations && !hasReturn && isStatement(body)) { + // add return at the end to propagate writes back in case if control flow falls out of the function body + // it is ok to know that range has at least one return since it we only allow unconditional returns + const assignments = getPropertyAssignmentsForWritesAndVariableDeclarations(exposedVariableDeclarations, writes); + if (assignments.length === 1) { + rewrittenStatements.push(createReturn(assignments[0].name)); } - prevMember = member; - } - - if (prevMember === undefined) return Debug.fail(); // If the loop didn't return, then it did set prevMember. - return prevMember; - } - - function getNodeToInsertConstantBefore(node: Node, scope: Scope): Statement { - Debug.assert(!isClassLike(scope)); - - let prevScope: Scope | undefined; - for (let curr = node; curr !== scope; curr = curr.parent) { - if (isScope(curr)) { - prevScope = curr; + else { + rewrittenStatements.push(createReturn(createObjectLiteral(assignments))); } } - - for (let curr = (prevScope || node).parent; ; curr = curr.parent) { - if (isBlockLike(curr)) { - let prevStatement: Statement | undefined; - for (const statement of curr.statements) { - if (statement.pos > node.pos) { - break; - } - prevStatement = statement; - } - - if (!prevStatement && isCaseClause(curr)) { - // We must have been in the expression of the case clause. - Debug.assert(isSwitchStatement(curr.parent.parent), "Grandparent isn't a switch statement"); - return curr.parent.parent; + return { body: createBlock(rewrittenStatements, /*multiLine*/ true), returnValueProperty }; + } + else { + return { body: createBlock(statements, /*multiLine*/ true), returnValueProperty: undefined }; + } + function visitor(node: Node): VisitResult { + if (!ignoreReturns && node.kind === SyntaxKind.ReturnStatement && hasWritesOrVariableDeclarations) { + const assignments: ObjectLiteralElementLike[] = getPropertyAssignmentsForWritesAndVariableDeclarations(exposedVariableDeclarations, writes); + if ((node).expression) { + if (!returnValueProperty) { + returnValueProperty = "__return"; } - - // There must be at least one statement since we started in one. - return Debug.checkDefined(prevStatement, "prevStatement failed to get set"); + assignments.unshift(createPropertyAssignment(returnValueProperty, visitNode((node).expression, visitor))); } - - Debug.assert(curr !== scope, "Didn't encounter a block-like before encountering scope"); - } - } - - function getPropertyAssignmentsForWritesAndVariableDeclarations( - exposedVariableDeclarations: readonly VariableDeclaration[], - writes: readonly UsageEntry[] | undefined - ): ShorthandPropertyAssignment[] { - const variableAssignments = map(exposedVariableDeclarations, v => createShorthandPropertyAssignment(v.symbol.name)); - const writeAssignments = map(writes, w => createShorthandPropertyAssignment(w.symbol.name)); - - // TODO: GH#18217 `variableAssignments` not possibly undefined! - return variableAssignments === undefined - ? writeAssignments! - : writeAssignments === undefined - ? variableAssignments - : variableAssignments.concat(writeAssignments); - } - - function isReadonlyArray(v: any): v is readonly any[] { - return isArray(v); - } - - /** - * Produces a range that spans the entirety of nodes, given a selection - * that might start/end in the middle of nodes. - * - * For example, when the user makes a selection like this - * v---v - * var someThing = foo + bar; - * this returns ^-------^ - */ - function getEnclosingTextRange(targetRange: TargetRange, sourceFile: SourceFile): TextRange { - return isReadonlyArray(targetRange.range) - ? { pos: first(targetRange.range).getStart(sourceFile), end: last(targetRange.range).getEnd() } - : targetRange.range; - } - - const enum Usage { - // value should be passed to extracted method - Read = 1, - // value should be passed to extracted method and propagated back - Write = 2 - } - - interface UsageEntry { - readonly usage: Usage; - readonly symbol: Symbol; - readonly node: Node; - } - - interface ScopeUsages { - readonly usages: Map; - readonly typeParameterUsages: Map; // Key is type ID - readonly substitutions: Map; - } - - interface ReadsAndWrites { - readonly target: Expression | Block; - readonly usagesPerScope: readonly ScopeUsages[]; - readonly functionErrorsPerScope: readonly (readonly Diagnostic[])[]; - readonly constantErrorsPerScope: readonly (readonly Diagnostic[])[]; - readonly exposedVariableDeclarations: readonly VariableDeclaration[]; - } - function collectReadsAndWrites( - targetRange: TargetRange, - scopes: Scope[], - enclosingTextRange: TextRange, - sourceFile: SourceFile, - checker: TypeChecker, - cancellationToken: CancellationToken): ReadsAndWrites { - - const allTypeParameterUsages = createMap(); // Key is type ID - const usagesPerScope: ScopeUsages[] = []; - const substitutionsPerScope: Map[] = []; - const functionErrorsPerScope: Diagnostic[][] = []; - const constantErrorsPerScope: Diagnostic[][] = []; - const visibleDeclarationsInExtractedRange: NamedDeclaration[] = []; - const exposedVariableSymbolSet = createMap(); // Key is symbol ID - const exposedVariableDeclarations: VariableDeclaration[] = []; - let firstExposedNonVariableDeclaration: NamedDeclaration | undefined; - - const expression = !isReadonlyArray(targetRange.range) - ? targetRange.range - : targetRange.range.length === 1 && isExpressionStatement(targetRange.range[0]) - ? (targetRange.range[0] as ExpressionStatement).expression - : undefined; - - let expressionDiagnostic: Diagnostic | undefined; - if (expression === undefined) { - const statements = targetRange.range as readonly Statement[]; - const start = first(statements).getStart(); - const end = last(statements).end; - expressionDiagnostic = createFileDiagnostic(sourceFile, start, end - start, Messages.expressionExpected); - } - else if (checker.getTypeAtLocation(expression).flags & (TypeFlags.Void | TypeFlags.Never)) { - expressionDiagnostic = createDiagnosticForNode(expression, Messages.uselessConstantType); - } - - // initialize results - for (const scope of scopes) { - usagesPerScope.push({ usages: createMap(), typeParameterUsages: createMap(), substitutions: createMap() }); - substitutionsPerScope.push(createMap()); - - functionErrorsPerScope.push( - isFunctionLikeDeclaration(scope) && scope.kind !== SyntaxKind.FunctionDeclaration - ? [createDiagnosticForNode(scope, Messages.cannotExtractToOtherFunctionLike)] - : []); - - const constantErrors = []; - if (expressionDiagnostic) { - constantErrors.push(expressionDiagnostic); - } - if (isClassLike(scope) && isInJSFile(scope)) { - constantErrors.push(createDiagnosticForNode(scope, Messages.cannotExtractToJSClass)); + if (assignments.length === 1) { + return createReturn((assignments[0].name as Expression)); } - if (isArrowFunction(scope) && !isBlock(scope.body)) { - // TODO (https://github.com/Microsoft/TypeScript/issues/18924): allow this - constantErrors.push(createDiagnosticForNode(scope, Messages.cannotExtractToExpressionArrowFunction)); + else { + return createReturn(createObjectLiteral(assignments)); } - constantErrorsPerScope.push(constantErrors); - } - - const seenUsages = createMap(); - const target = isReadonlyArray(targetRange.range) ? createBlock(targetRange.range) : targetRange.range; - - const unmodifiedNode = isReadonlyArray(targetRange.range) ? first(targetRange.range) : targetRange.range; - const inGenericContext = isInGenericContext(unmodifiedNode); - - collectUsages(target); - - // Unfortunately, this code takes advantage of the knowledge that the generated method - // will use the contextual type of an expression as the return type of the extracted - // method (and will therefore "use" all the types involved). - if (inGenericContext && !isReadonlyArray(targetRange.range)) { - const contextualType = checker.getContextualType(targetRange.range)!; // TODO: GH#18217 - recordTypeParameterUsages(contextualType); - } - - if (allTypeParameterUsages.size > 0) { - const seenTypeParameterUsages = createMap(); // Key is type ID - - let i = 0; - for (let curr: Node = unmodifiedNode; curr !== undefined && i < scopes.length; curr = curr.parent) { - if (curr === scopes[i]) { - // Copy current contents of seenTypeParameterUsages into scope. - seenTypeParameterUsages.forEach((typeParameter, id) => { - usagesPerScope[i].typeParameterUsages.set(id, typeParameter); - }); - - i++; - } - - // Note that we add the current node's type parameters *after* updating the corresponding scope. - if (isDeclarationWithTypeParameters(curr)) { - for (const typeParameterDecl of getEffectiveTypeParameterDeclarations(curr)) { - const typeParameter = checker.getTypeAtLocation(typeParameterDecl) as TypeParameter; - if (allTypeParameterUsages.has(typeParameter.id.toString())) { - seenTypeParameterUsages.set(typeParameter.id.toString(), typeParameter); - } - } + } + else { + const oldIgnoreReturns = ignoreReturns; + ignoreReturns = ignoreReturns || isFunctionLikeDeclaration(node) || isClassLike(node); + const substitution = substitutions.get(getNodeId(node).toString()); + const result = substitution ? getSynthesizedDeepClone(substitution) : visitEachChild(node, visitor, nullTransformationContext); + ignoreReturns = oldIgnoreReturns; + return result; + } + } +} +/* @internal */ +function transformConstantInitializer(initializer: Expression, substitutions: ts.ReadonlyMap): Expression { + return substitutions.size + ? visitor(initializer) as Expression + : initializer; + function visitor(node: Node): VisitResult { + const substitution = substitutions.get(getNodeId(node).toString()); + return substitution ? getSynthesizedDeepClone(substitution) : visitEachChild(node, visitor, nullTransformationContext); + } +} +/* @internal */ +function getStatementsOrClassElements(scope: Scope): readonly Statement[] | readonly ClassElement[] { + if (isFunctionLikeDeclaration(scope)) { + const body = scope.body!; // TODO: GH#18217 + if (isBlock(body)) { + return body.statements; + } + } + else if (isModuleBlock(scope) || isSourceFile(scope)) { + return scope.statements; + } + else if (isClassLike(scope)) { + return scope.members; + } + else { + assertType(scope); + } + return emptyArray; +} +/** + * If `scope` contains a function after `minPos`, then return the first such function. + * Otherwise, return `undefined`. + */ +/* @internal */ +function getNodeToInsertFunctionBefore(minPos: number, scope: Scope): Statement | ClassElement | undefined { + return find(getStatementsOrClassElements(scope), child => child.pos >= minPos && isFunctionLikeDeclaration(child) && !isConstructorDeclaration(child)); +} +/* @internal */ +function getNodeToInsertPropertyBefore(maxPos: number, scope: ClassLikeDeclaration): ClassElement { + const members = scope.members; + Debug.assert(members.length > 0, "Found no members"); // There must be at least one child, since we extracted from one. + let prevMember: ClassElement | undefined; + let allProperties = true; + for (const member of members) { + if (member.pos > maxPos) { + return prevMember || members[0]; + } + if (allProperties && !isPropertyDeclaration(member)) { + // If it is non-vacuously true that all preceding members are properties, + // insert before the current member (i.e. at the end of the list of properties). + if (prevMember !== undefined) { + return member; + } + allProperties = false; + } + prevMember = member; + } + if (prevMember === undefined) + return Debug.fail(); // If the loop didn't return, then it did set prevMember. + return prevMember; +} +/* @internal */ +function getNodeToInsertConstantBefore(node: Node, scope: Scope): Statement { + Debug.assert(!isClassLike(scope)); + let prevScope: Scope | undefined; + for (let curr = node; curr !== scope; curr = curr.parent) { + if (isScope(curr)) { + prevScope = curr; + } + } + for (let curr = (prevScope || node).parent;; curr = curr.parent) { + if (isBlockLike(curr)) { + let prevStatement: Statement | undefined; + for (const statement of curr.statements) { + if (statement.pos > node.pos) { + break; } + prevStatement = statement; } - - // If we didn't get through all the scopes, then there were some that weren't in our - // parent chain (impossible at time of writing). A conservative solution would be to - // copy allTypeParameterUsages into all remaining scopes. - Debug.assert(i === scopes.length, "Should have iterated all scopes"); - } - - // If there are any declarations in the extracted block that are used in the same enclosing - // lexical scope, we can't move the extraction "up" as those declarations will become unreachable - if (visibleDeclarationsInExtractedRange.length) { - const containingLexicalScopeOfExtraction = isBlockScope(scopes[0], scopes[0].parent) - ? scopes[0] - : getEnclosingBlockScopeContainer(scopes[0]); - forEachChild(containingLexicalScopeOfExtraction, checkForUsedDeclarations); - } - - for (let i = 0; i < scopes.length; i++) { - const scopeUsages = usagesPerScope[i]; - // Special case: in the innermost scope, all usages are available. - // (The computed value reflects the value at the top-level of the scope, but the - // local will actually be declared at the same level as the extracted expression). - if (i > 0 && (scopeUsages.usages.size > 0 || scopeUsages.typeParameterUsages.size > 0)) { - const errorNode = isReadonlyArray(targetRange.range) ? targetRange.range[0] : targetRange.range; - constantErrorsPerScope[i].push(createDiagnosticForNode(errorNode, Messages.cannotAccessVariablesFromNestedScopes)); + if (!prevStatement && isCaseClause(curr)) { + // We must have been in the expression of the case clause. + Debug.assert(isSwitchStatement(curr.parent.parent), "Grandparent isn't a switch statement"); + return curr.parent.parent; } - - let hasWrite = false; - let readonlyClassPropertyWrite: Declaration | undefined; - usagesPerScope[i].usages.forEach(value => { - if (value.usage === Usage.Write) { - hasWrite = true; - if (value.symbol.flags & SymbolFlags.ClassMember && - value.symbol.valueDeclaration && - hasModifier(value.symbol.valueDeclaration, ModifierFlags.Readonly)) { - readonlyClassPropertyWrite = value.symbol.valueDeclaration; + // There must be at least one statement since we started in one. + return Debug.checkDefined(prevStatement, "prevStatement failed to get set"); + } + Debug.assert(curr !== scope, "Didn't encounter a block-like before encountering scope"); + } +} +/* @internal */ +function getPropertyAssignmentsForWritesAndVariableDeclarations(exposedVariableDeclarations: readonly VariableDeclaration[], writes: readonly UsageEntry[] | undefined): ShorthandPropertyAssignment[] { + const variableAssignments = map(exposedVariableDeclarations, v => createShorthandPropertyAssignment(v.symbol.name)); + const writeAssignments = map(writes, w => createShorthandPropertyAssignment(w.symbol.name)); + // TODO: GH#18217 `variableAssignments` not possibly undefined! + return variableAssignments === undefined + ? writeAssignments! + : writeAssignments === undefined + ? variableAssignments + : variableAssignments.concat(writeAssignments); +} +/* @internal */ +function isReadonlyArray(v: any): v is readonly any[] { + return isArray(v); +} +/** + * Produces a range that spans the entirety of nodes, given a selection + * that might start/end in the middle of nodes. + * + * For example, when the user makes a selection like this + * v---v + * var someThing = foo + bar; + * this returns ^-------^ + */ +/* @internal */ +function getEnclosingTextRange(targetRange: TargetRange, sourceFile: SourceFile): TextRange { + return isReadonlyArray(targetRange.range) + ? { pos: first(targetRange.range).getStart(sourceFile), end: last(targetRange.range).getEnd() } + : targetRange.range; +} +/* @internal */ +const enum Usage { + // value should be passed to extracted method + Read = 1, + // value should be passed to extracted method and propagated back + Write = 2 +} +/* @internal */ +interface UsageEntry { + readonly usage: Usage; + readonly symbol: Symbol; + readonly node: Node; +} +/* @internal */ +interface ScopeUsages { + readonly usages: ts.Map; + readonly typeParameterUsages: ts.Map; // Key is type ID + readonly substitutions: ts.Map; +} +/* @internal */ +interface ReadsAndWrites { + readonly target: Expression | Block; + readonly usagesPerScope: readonly ScopeUsages[]; + readonly functionErrorsPerScope: readonly (readonly Diagnostic[])[]; + readonly constantErrorsPerScope: readonly (readonly Diagnostic[])[]; + readonly exposedVariableDeclarations: readonly VariableDeclaration[]; +} +/* @internal */ +function collectReadsAndWrites(targetRange: TargetRange, scopes: Scope[], enclosingTextRange: TextRange, sourceFile: SourceFile, checker: TypeChecker, cancellationToken: CancellationToken): ReadsAndWrites { + const allTypeParameterUsages = createMap(); // Key is type ID + const usagesPerScope: ScopeUsages[] = []; + const substitutionsPerScope: ts.Map[] = []; + const functionErrorsPerScope: Diagnostic[][] = []; + const constantErrorsPerScope: Diagnostic[][] = []; + const visibleDeclarationsInExtractedRange: NamedDeclaration[] = []; + const exposedVariableSymbolSet = createMap(); // Key is symbol ID + const exposedVariableDeclarations: VariableDeclaration[] = []; + let firstExposedNonVariableDeclaration: NamedDeclaration | undefined; + const expression = !isReadonlyArray(targetRange.range) + ? targetRange.range + : targetRange.range.length === 1 && isExpressionStatement(targetRange.range[0]) + ? (targetRange.range[0] as ExpressionStatement).expression + : undefined; + let expressionDiagnostic: Diagnostic | undefined; + if (expression === undefined) { + const statements = (targetRange.range as readonly Statement[]); + const start = first(statements).getStart(); + const end = last(statements).end; + expressionDiagnostic = createFileDiagnostic(sourceFile, start, end - start, Messages.expressionExpected); + } + else if (checker.getTypeAtLocation(expression).flags & (TypeFlags.Void | TypeFlags.Never)) { + expressionDiagnostic = createDiagnosticForNode(expression, Messages.uselessConstantType); + } + // initialize results + for (const scope of scopes) { + usagesPerScope.push({ usages: createMap(), typeParameterUsages: createMap(), substitutions: createMap() }); + substitutionsPerScope.push(createMap()); + functionErrorsPerScope.push(isFunctionLikeDeclaration(scope) && scope.kind !== SyntaxKind.FunctionDeclaration + ? [createDiagnosticForNode(scope, Messages.cannotExtractToOtherFunctionLike)] + : []); + const constantErrors = []; + if (expressionDiagnostic) { + constantErrors.push(expressionDiagnostic); + } + if (isClassLike(scope) && isInJSFile(scope)) { + constantErrors.push(createDiagnosticForNode(scope, Messages.cannotExtractToJSClass)); + } + if (isArrowFunction(scope) && !isBlock(scope.body)) { + // TODO (https://github.com/Microsoft/TypeScript/issues/18924): allow this + constantErrors.push(createDiagnosticForNode(scope, Messages.cannotExtractToExpressionArrowFunction)); + } + constantErrorsPerScope.push(constantErrors); + } + const seenUsages = createMap(); + const target = isReadonlyArray(targetRange.range) ? createBlock(targetRange.range) : targetRange.range; + const unmodifiedNode = isReadonlyArray(targetRange.range) ? first(targetRange.range) : targetRange.range; + const inGenericContext = isInGenericContext(unmodifiedNode); + collectUsages(target); + // Unfortunately, this code takes advantage of the knowledge that the generated method + // will use the contextual type of an expression as the return type of the extracted + // method (and will therefore "use" all the types involved). + if (inGenericContext && !isReadonlyArray(targetRange.range)) { + const contextualType = checker.getContextualType(targetRange.range)!; // TODO: GH#18217 + recordTypeParameterUsages(contextualType); + } + if (allTypeParameterUsages.size > 0) { + const seenTypeParameterUsages = createMap(); // Key is type ID + let i = 0; + for (let curr: Node = unmodifiedNode; curr !== undefined && i < scopes.length; curr = curr.parent) { + if (curr === scopes[i]) { + // Copy current contents of seenTypeParameterUsages into scope. + seenTypeParameterUsages.forEach((typeParameter, id) => { + usagesPerScope[i].typeParameterUsages.set(id, typeParameter); + }); + i++; + } + // Note that we add the current node's type parameters *after* updating the corresponding scope. + if (isDeclarationWithTypeParameters(curr)) { + for (const typeParameterDecl of getEffectiveTypeParameterDeclarations(curr)) { + const typeParameter = (checker.getTypeAtLocation(typeParameterDecl) as TypeParameter); + if (allTypeParameterUsages.has(typeParameter.id.toString())) { + seenTypeParameterUsages.set(typeParameter.id.toString(), typeParameter); } } - }); - - // If an expression was extracted, then there shouldn't have been any variable declarations. - Debug.assert(isReadonlyArray(targetRange.range) || exposedVariableDeclarations.length === 0, "No variable declarations expected if something was extracted"); - - if (hasWrite && !isReadonlyArray(targetRange.range)) { - const diag = createDiagnosticForNode(targetRange.range, Messages.cannotWriteInExpression); - functionErrorsPerScope[i].push(diag); - constantErrorsPerScope[i].push(diag); - } - else if (readonlyClassPropertyWrite && i > 0) { - const diag = createDiagnosticForNode(readonlyClassPropertyWrite, Messages.cannotExtractReadonlyPropertyInitializerOutsideConstructor); - functionErrorsPerScope[i].push(diag); - constantErrorsPerScope[i].push(diag); - } - else if (firstExposedNonVariableDeclaration) { - const diag = createDiagnosticForNode(firstExposedNonVariableDeclaration, Messages.cannotExtractExportedEntity); - functionErrorsPerScope[i].push(diag); - constantErrorsPerScope[i].push(diag); } } - - return { target, usagesPerScope, functionErrorsPerScope, constantErrorsPerScope, exposedVariableDeclarations }; - - function isInGenericContext(node: Node) { - return !!findAncestor(node, n => isDeclarationWithTypeParameters(n) && getEffectiveTypeParameterDeclarations(n).length !== 0); - } - - function recordTypeParameterUsages(type: Type) { - // PERF: This is potentially very expensive. `type` could be a library type with - // a lot of properties, each of which the walker will visit. Unfortunately, the - // solution isn't as trivial as filtering to user types because of (e.g.) Array. - const symbolWalker = checker.getSymbolWalker(() => (cancellationToken.throwIfCancellationRequested(), true)); - const {visitedTypes} = symbolWalker.walkType(type); - - for (const visitedType of visitedTypes) { - if (visitedType.isTypeParameter()) { - allTypeParameterUsages.set(visitedType.id.toString(), visitedType); + // If we didn't get through all the scopes, then there were some that weren't in our + // parent chain (impossible at time of writing). A conservative solution would be to + // copy allTypeParameterUsages into all remaining scopes. + Debug.assert(i === scopes.length, "Should have iterated all scopes"); + } + // If there are any declarations in the extracted block that are used in the same enclosing + // lexical scope, we can't move the extraction "up" as those declarations will become unreachable + if (visibleDeclarationsInExtractedRange.length) { + const containingLexicalScopeOfExtraction = isBlockScope(scopes[0], scopes[0].parent) + ? scopes[0] + : getEnclosingBlockScopeContainer(scopes[0]); + forEachChild(containingLexicalScopeOfExtraction, checkForUsedDeclarations); + } + for (let i = 0; i < scopes.length; i++) { + const scopeUsages = usagesPerScope[i]; + // Special case: in the innermost scope, all usages are available. + // (The computed value reflects the value at the top-level of the scope, but the + // local will actually be declared at the same level as the extracted expression). + if (i > 0 && (scopeUsages.usages.size > 0 || scopeUsages.typeParameterUsages.size > 0)) { + const errorNode = isReadonlyArray(targetRange.range) ? targetRange.range[0] : targetRange.range; + constantErrorsPerScope[i].push(createDiagnosticForNode(errorNode, Messages.cannotAccessVariablesFromNestedScopes)); + } + let hasWrite = false; + let readonlyClassPropertyWrite: Declaration | undefined; + usagesPerScope[i].usages.forEach(value => { + if (value.usage === Usage.Write) { + hasWrite = true; + if (value.symbol.flags & SymbolFlags.ClassMember && + value.symbol.valueDeclaration && + hasModifier(value.symbol.valueDeclaration, ModifierFlags.Readonly)) { + readonlyClassPropertyWrite = value.symbol.valueDeclaration; } } + }); + // If an expression was extracted, then there shouldn't have been any variable declarations. + Debug.assert(isReadonlyArray(targetRange.range) || exposedVariableDeclarations.length === 0, "No variable declarations expected if something was extracted"); + if (hasWrite && !isReadonlyArray(targetRange.range)) { + const diag = createDiagnosticForNode(targetRange.range, Messages.cannotWriteInExpression); + functionErrorsPerScope[i].push(diag); + constantErrorsPerScope[i].push(diag); + } + else if (readonlyClassPropertyWrite && i > 0) { + const diag = createDiagnosticForNode(readonlyClassPropertyWrite, Messages.cannotExtractReadonlyPropertyInitializerOutsideConstructor); + functionErrorsPerScope[i].push(diag); + constantErrorsPerScope[i].push(diag); + } + else if (firstExposedNonVariableDeclaration) { + const diag = createDiagnosticForNode(firstExposedNonVariableDeclaration, Messages.cannotExtractExportedEntity); + functionErrorsPerScope[i].push(diag); + constantErrorsPerScope[i].push(diag); } - - function collectUsages(node: Node, valueUsage = Usage.Read) { - if (inGenericContext) { - const type = checker.getTypeAtLocation(node); - recordTypeParameterUsages(type); - } - - if (isDeclaration(node) && node.symbol) { - visibleDeclarationsInExtractedRange.push(node); - } - - if (isAssignmentExpression(node)) { - // use 'write' as default usage for values - collectUsages(node.left, Usage.Write); - collectUsages(node.right); - } - else if (isUnaryExpressionWithWrite(node)) { - collectUsages(node.operand, Usage.Write); + } + return { target, usagesPerScope, functionErrorsPerScope, constantErrorsPerScope, exposedVariableDeclarations }; + function isInGenericContext(node: Node) { + return !!findAncestor(node, n => isDeclarationWithTypeParameters(n) && getEffectiveTypeParameterDeclarations(n).length !== 0); + } + function recordTypeParameterUsages(type: Type) { + // PERF: This is potentially very expensive. `type` could be a library type with + // a lot of properties, each of which the walker will visit. Unfortunately, the + // solution isn't as trivial as filtering to user types because of (e.g.) Array. + const symbolWalker = checker.getSymbolWalker(() => (cancellationToken.throwIfCancellationRequested(), true)); + const { visitedTypes } = symbolWalker.walkType(type); + for (const visitedType of visitedTypes) { + if (visitedType.isTypeParameter()) { + allTypeParameterUsages.set(visitedType.id.toString(), visitedType); } - else if (isPropertyAccessExpression(node) || isElementAccessExpression(node)) { - // use 'write' as default usage for values - forEachChild(node, collectUsages); + } + } + function collectUsages(node: Node, valueUsage = Usage.Read) { + if (inGenericContext) { + const type = checker.getTypeAtLocation(node); + recordTypeParameterUsages(type); + } + if (isDeclaration(node) && node.symbol) { + visibleDeclarationsInExtractedRange.push(node); + } + if (isAssignmentExpression(node)) { + // use 'write' as default usage for values + collectUsages(node.left, Usage.Write); + collectUsages(node.right); + } + else if (isUnaryExpressionWithWrite(node)) { + collectUsages(node.operand, Usage.Write); + } + else if (isPropertyAccessExpression(node) || isElementAccessExpression(node)) { + // use 'write' as default usage for values + forEachChild(node, collectUsages); + } + else if (isIdentifier(node)) { + if (!node.parent) { + return; } - else if (isIdentifier(node)) { - if (!node.parent) { - return; - } - if (isQualifiedName(node.parent) && node !== node.parent.left) { - return; - } - if (isPropertyAccessExpression(node.parent) && node !== node.parent.expression) { - return; - } - recordUsage(node, valueUsage, /*isTypeNode*/ isPartOfTypeNode(node)); + if (isQualifiedName(node.parent) && node !== node.parent.left) { + return; } - else { - forEachChild(node, collectUsages); + if (isPropertyAccessExpression(node.parent) && node !== node.parent.expression) { + return; } + recordUsage(node, valueUsage, /*isTypeNode*/ isPartOfTypeNode(node)); } - - function recordUsage(n: Identifier, usage: Usage, isTypeNode: boolean) { - const symbolId = recordUsagebySymbol(n, usage, isTypeNode); - if (symbolId) { - for (let i = 0; i < scopes.length; i++) { - // push substitution from map to map to simplify rewriting - const substitution = substitutionsPerScope[i].get(symbolId); - if (substitution) { - usagesPerScope[i].substitutions.set(getNodeId(n).toString(), substitution); - } + else { + forEachChild(node, collectUsages); + } + } + function recordUsage(n: Identifier, usage: Usage, isTypeNode: boolean) { + const symbolId = recordUsagebySymbol(n, usage, isTypeNode); + if (symbolId) { + for (let i = 0; i < scopes.length; i++) { + // push substitution from map to map to simplify rewriting + const substitution = substitutionsPerScope[i].get(symbolId); + if (substitution) { + usagesPerScope[i].substitutions.set(getNodeId(n).toString(), substitution); } } } - - function recordUsagebySymbol(identifier: Identifier, usage: Usage, isTypeName: boolean) { - const symbol = getSymbolReferencedByIdentifier(identifier); - if (!symbol) { - // cannot find symbol - do nothing - return undefined; - } - const symbolId = getSymbolId(symbol).toString(); - const lastUsage = seenUsages.get(symbolId); - // there are two kinds of value usages - // - reads - if range contains a read from the value located outside of the range then value should be passed as a parameter - // - writes - if range contains a write to a value located outside the range the value should be passed as a parameter and - // returned as a return value - // 'write' case is a superset of 'read' so if we already have processed 'write' of some symbol there is not need to handle 'read' - // since all information is already recorded - if (lastUsage && lastUsage >= usage) { - return symbolId; - } - - seenUsages.set(symbolId, usage); - if (lastUsage) { - // if we get here this means that we are trying to handle 'write' and 'read' was already processed - // walk scopes and update existing records. - for (const perScope of usagesPerScope) { - const prevEntry = perScope.usages.get(identifier.text); - if (prevEntry) { - perScope.usages.set(identifier.text, { usage, symbol, node: identifier }); - } + } + function recordUsagebySymbol(identifier: Identifier, usage: Usage, isTypeName: boolean) { + const symbol = getSymbolReferencedByIdentifier(identifier); + if (!symbol) { + // cannot find symbol - do nothing + return undefined; + } + const symbolId = getSymbolId(symbol).toString(); + const lastUsage = seenUsages.get(symbolId); + // there are two kinds of value usages + // - reads - if range contains a read from the value located outside of the range then value should be passed as a parameter + // - writes - if range contains a write to a value located outside the range the value should be passed as a parameter and + // returned as a return value + // 'write' case is a superset of 'read' so if we already have processed 'write' of some symbol there is not need to handle 'read' + // since all information is already recorded + if (lastUsage && lastUsage >= usage) { + return symbolId; + } + seenUsages.set(symbolId, usage); + if (lastUsage) { + // if we get here this means that we are trying to handle 'write' and 'read' was already processed + // walk scopes and update existing records. + for (const perScope of usagesPerScope) { + const prevEntry = perScope.usages.get(identifier.text); + if (prevEntry) { + perScope.usages.set(identifier.text, { usage, symbol, node: identifier }); } - return symbolId; } - // find first declaration in this file - const decls = symbol.getDeclarations(); - const declInFile = decls && find(decls, d => d.getSourceFile() === sourceFile); - if (!declInFile) { - return undefined; - } - if (rangeContainsStartEnd(enclosingTextRange, declInFile.getStart(), declInFile.end)) { - // declaration is located in range to be extracted - do nothing - return undefined; + return symbolId; + } + // find first declaration in this file + const decls = symbol.getDeclarations(); + const declInFile = decls && find(decls, d => d.getSourceFile() === sourceFile); + if (!declInFile) { + return undefined; + } + if (rangeContainsStartEnd(enclosingTextRange, declInFile.getStart(), declInFile.end)) { + // declaration is located in range to be extracted - do nothing + return undefined; + } + if (targetRange.facts & RangeFacts.IsGenerator && usage === Usage.Write) { + // this is write to a reference located outside of the target scope and range is extracted into generator + // currently this is unsupported scenario + const diag = createDiagnosticForNode(identifier, Messages.cannotExtractRangeThatContainsWritesToReferencesLocatedOutsideOfTheTargetRangeInGenerators); + for (const errors of functionErrorsPerScope) { + errors.push(diag); } - if (targetRange.facts & RangeFacts.IsGenerator && usage === Usage.Write) { - // this is write to a reference located outside of the target scope and range is extracted into generator - // currently this is unsupported scenario - const diag = createDiagnosticForNode(identifier, Messages.cannotExtractRangeThatContainsWritesToReferencesLocatedOutsideOfTheTargetRangeInGenerators); - for (const errors of functionErrorsPerScope) { - errors.push(diag); - } - for (const errors of constantErrorsPerScope) { - errors.push(diag); - } + for (const errors of constantErrorsPerScope) { + errors.push(diag); } - for (let i = 0; i < scopes.length; i++) { - const scope = scopes[i]; - const resolvedSymbol = checker.resolveName(symbol.name, scope, symbol.flags, /*excludeGlobals*/ false); - if (resolvedSymbol === symbol) { - continue; + } + for (let i = 0; i < scopes.length; i++) { + const scope = scopes[i]; + const resolvedSymbol = checker.resolveName(symbol.name, scope, symbol.flags, /*excludeGlobals*/ false); + if (resolvedSymbol === symbol) { + continue; + } + if (!substitutionsPerScope[i].has(symbolId)) { + const substitution = tryReplaceWithQualifiedNameOrPropertyAccess(symbol.exportSymbol || symbol, scope, isTypeName); + if (substitution) { + substitutionsPerScope[i].set(symbolId, substitution); } - if (!substitutionsPerScope[i].has(symbolId)) { - const substitution = tryReplaceWithQualifiedNameOrPropertyAccess(symbol.exportSymbol || symbol, scope, isTypeName); - if (substitution) { - substitutionsPerScope[i].set(symbolId, substitution); - } - else if (isTypeName) { - // If the symbol is a type parameter that won't be in scope, we'll pass it as a type argument - // so there's no problem. - if (!(symbol.flags & SymbolFlags.TypeParameter)) { - const diag = createDiagnosticForNode(identifier, Messages.typeWillNotBeVisibleInTheNewScope); - functionErrorsPerScope[i].push(diag); - constantErrorsPerScope[i].push(diag); - } - } - else { - usagesPerScope[i].usages.set(identifier.text, { usage, symbol, node: identifier }); + else if (isTypeName) { + // If the symbol is a type parameter that won't be in scope, we'll pass it as a type argument + // so there's no problem. + if (!(symbol.flags & SymbolFlags.TypeParameter)) { + const diag = createDiagnosticForNode(identifier, Messages.typeWillNotBeVisibleInTheNewScope); + functionErrorsPerScope[i].push(diag); + constantErrorsPerScope[i].push(diag); } } + else { + usagesPerScope[i].usages.set(identifier.text, { usage, symbol, node: identifier }); + } } - return symbolId; } - - function checkForUsedDeclarations(node: Node) { - // If this node is entirely within the original extraction range, we don't need to do anything. - if (node === targetRange.range || (isReadonlyArray(targetRange.range) && targetRange.range.indexOf(node as Statement) >= 0)) { - return; - } - - // Otherwise check and recurse. - const sym = isIdentifier(node) - ? getSymbolReferencedByIdentifier(node) - : checker.getSymbolAtLocation(node); - if (sym) { - const decl = find(visibleDeclarationsInExtractedRange, d => d.symbol === sym); - if (decl) { - if (isVariableDeclaration(decl)) { - const idString = decl.symbol.id!.toString(); - if (!exposedVariableSymbolSet.has(idString)) { - exposedVariableDeclarations.push(decl); - exposedVariableSymbolSet.set(idString, true); - } - } - else { - // CONSIDER: this includes binding elements, which we could - // expose in the same way as variables. - firstExposedNonVariableDeclaration = firstExposedNonVariableDeclaration || decl; + return symbolId; + } + function checkForUsedDeclarations(node: Node) { + // If this node is entirely within the original extraction range, we don't need to do anything. + if (node === targetRange.range || (isReadonlyArray(targetRange.range) && targetRange.range.indexOf((node as Statement)) >= 0)) { + return; + } + // Otherwise check and recurse. + const sym = isIdentifier(node) + ? getSymbolReferencedByIdentifier(node) + : checker.getSymbolAtLocation(node); + if (sym) { + const decl = find(visibleDeclarationsInExtractedRange, d => d.symbol === sym); + if (decl) { + if (isVariableDeclaration(decl)) { + const idString = decl.symbol.id!.toString(); + if (!exposedVariableSymbolSet.has(idString)) { + exposedVariableDeclarations.push(decl); + exposedVariableSymbolSet.set(idString, true); } } + else { + // CONSIDER: this includes binding elements, which we could + // expose in the same way as variables. + firstExposedNonVariableDeclaration = firstExposedNonVariableDeclaration || decl; + } } - - forEachChild(node, checkForUsedDeclarations); - } - - /** - * Return the symbol referenced by an identifier (even if it declares a different symbol). - */ - function getSymbolReferencedByIdentifier(identifier: Identifier) { - // If the identifier is both a property name and its value, we're only interested in its value - // (since the name is a declaration and will be included in the extracted range). - return identifier.parent && isShorthandPropertyAssignment(identifier.parent) && identifier.parent.name === identifier - ? checker.getShorthandAssignmentValueSymbol(identifier.parent) - : checker.getSymbolAtLocation(identifier); - } - - function tryReplaceWithQualifiedNameOrPropertyAccess(symbol: Symbol | undefined, scopeDecl: Node, isTypeNode: boolean): PropertyAccessExpression | EntityName | undefined { - if (!symbol) { - return undefined; - } - const decls = symbol.getDeclarations(); - if (decls && decls.some(d => d.parent === scopeDecl)) { - return createIdentifier(symbol.name); - } - const prefix = tryReplaceWithQualifiedNameOrPropertyAccess(symbol.parent, scopeDecl, isTypeNode); - if (prefix === undefined) { - return undefined; - } - return isTypeNode - ? createQualifiedName(prefix, createIdentifier(symbol.name)) - : createPropertyAccess(prefix, symbol.name); } + forEachChild(node, checkForUsedDeclarations); } - /** - * Computes whether or not a node represents an expression in a position where it could - * be extracted. - * The isExpression() in utilities.ts returns some false positives we need to handle, - * such as `import x from 'y'` -- the 'y' is a StringLiteral but is *not* an expression - * in the sense of something that you could extract on + * Return the symbol referenced by an identifier (even if it declares a different symbol). */ - function isExtractableExpression(node: Node): boolean { - const { parent } = node; - switch (parent.kind) { - case SyntaxKind.EnumMember: - return false; + function getSymbolReferencedByIdentifier(identifier: Identifier) { + // If the identifier is both a property name and its value, we're only interested in its value + // (since the name is a declaration and will be included in the extracted range). + return identifier.parent && isShorthandPropertyAssignment(identifier.parent) && identifier.parent.name === identifier + ? checker.getShorthandAssignmentValueSymbol(identifier.parent) + : checker.getSymbolAtLocation(identifier); + } + function tryReplaceWithQualifiedNameOrPropertyAccess(symbol: Symbol | undefined, scopeDecl: Node, isTypeNode: boolean): PropertyAccessExpression | EntityName | undefined { + if (!symbol) { + return undefined; } - - switch (node.kind) { - case SyntaxKind.StringLiteral: - return parent.kind !== SyntaxKind.ImportDeclaration && - parent.kind !== SyntaxKind.ImportSpecifier; - - case SyntaxKind.SpreadElement: - case SyntaxKind.ObjectBindingPattern: - case SyntaxKind.BindingElement: - return false; - - case SyntaxKind.Identifier: - return parent.kind !== SyntaxKind.BindingElement && - parent.kind !== SyntaxKind.ImportSpecifier && - parent.kind !== SyntaxKind.ExportSpecifier; - } - return true; - } - - function isBlockLike(node: Node): node is BlockLike { - switch (node.kind) { - case SyntaxKind.Block: - case SyntaxKind.SourceFile: - case SyntaxKind.ModuleBlock: - case SyntaxKind.CaseClause: - return true; - default: - return false; + const decls = symbol.getDeclarations(); + if (decls && decls.some(d => d.parent === scopeDecl)) { + return createIdentifier(symbol.name); + } + const prefix = tryReplaceWithQualifiedNameOrPropertyAccess(symbol.parent, scopeDecl, isTypeNode); + if (prefix === undefined) { + return undefined; } + return isTypeNode + ? createQualifiedName((prefix), createIdentifier(symbol.name)) + : createPropertyAccess((prefix), symbol.name); + } +} +/** + * Computes whether or not a node represents an expression in a position where it could + * be extracted. + * The isExpression() in utilities.ts returns some false positives we need to handle, + * such as `import x from 'y'` -- the 'y' is a StringLiteral but is *not* an expression + * in the sense of something that you could extract on + */ +/* @internal */ +function isExtractableExpression(node: Node): boolean { + const { parent } = node; + switch (parent.kind) { + case SyntaxKind.EnumMember: + return false; + } + switch (node.kind) { + case SyntaxKind.StringLiteral: + return parent.kind !== SyntaxKind.ImportDeclaration && + parent.kind !== SyntaxKind.ImportSpecifier; + case SyntaxKind.SpreadElement: + case SyntaxKind.ObjectBindingPattern: + case SyntaxKind.BindingElement: + return false; + case SyntaxKind.Identifier: + return parent.kind !== SyntaxKind.BindingElement && + parent.kind !== SyntaxKind.ImportSpecifier && + parent.kind !== SyntaxKind.ExportSpecifier; + } + return true; +} +/* @internal */ +function isBlockLike(node: Node): node is BlockLike { + switch (node.kind) { + case SyntaxKind.Block: + case SyntaxKind.SourceFile: + case SyntaxKind.ModuleBlock: + case SyntaxKind.CaseClause: + return true; + default: + return false; } } diff --git a/src/services/refactors/extractType.ts b/src/services/refactors/extractType.ts index 51049f6b7cdd1..3d9c5b3963169 100644 --- a/src/services/refactors/extractType.ts +++ b/src/services/refactors/extractType.ts @@ -1,208 +1,201 @@ +import { registerRefactor } from "../ts.refactor"; +import { ApplicableRefactorInfo, emptyArray, getLocaleSpecificMessage, Diagnostics, append, RefactorEditInfo, Debug, getUniqueName, getRenameLocation, TypeNode, Statement, TypeParameterDeclaration, TypeElement, RefactorContext, isSourceFileJS, getTokenAtPosition, createTextRangeFromSpan, getRefactorContextSpan, findAncestor, isTypeNode, isStatement, TypeChecker, isIntersectionTypeNode, createMap, addToSeen, getNameFromPropertyName, addRange, isParenthesizedTypeNode, isTypeLiteralNode, TextRange, Node, SourceFile, rangeContainsStartEnd, skipTrivia, isTypeReferenceNode, isIdentifier, SymbolFlags, cast, first, isTypeParameterDeclaration, isInferTypeNode, isConditionalTypeNode, isTypePredicateNode, isThisTypeNode, isFunctionLike, isTypeQueryNode, isThisIdentifier, forEachChild, createTypeAliasDeclaration, updateTypeParameterDeclaration, createTypeReferenceNode, createInterfaceDeclaration, JSDocTypedefTag, createNode, SyntaxKind, createIdentifier, createJSDocTypeExpression, JSDocTemplateTag, forEach, getEffectiveConstraintOfTypeParameter, isJSDocTypeExpression, createNodeArray, createJSDocComment, concatenate, JSDocTag } from "../ts"; +import { ChangeTracker } from "../ts.textChanges"; /* @internal */ -namespace ts.refactor { - const refactorName = "Extract type"; - const extractToTypeAlias = "Extract to type alias"; - const extractToInterface = "Extract to interface"; - const extractToTypeDef = "Extract to typedef"; - registerRefactor(refactorName, { - getAvailableActions(context): readonly ApplicableRefactorInfo[] { - const info = getRangeToExtract(context); - if (!info) return emptyArray; - - return [{ +const refactorName = "Extract type"; +/* @internal */ +const extractToTypeAlias = "Extract to type alias"; +/* @internal */ +const extractToInterface = "Extract to interface"; +/* @internal */ +const extractToTypeDef = "Extract to typedef"; +/* @internal */ +registerRefactor(refactorName, { + getAvailableActions(context): readonly ApplicableRefactorInfo[] { + const info = getRangeToExtract(context); + if (!info) + return emptyArray; + return [{ name: refactorName, description: getLocaleSpecificMessage(Diagnostics.Extract_type), actions: info.isJS ? [{ - name: extractToTypeDef, description: getLocaleSpecificMessage(Diagnostics.Extract_to_typedef) - }] : append([{ - name: extractToTypeAlias, description: getLocaleSpecificMessage(Diagnostics.Extract_to_type_alias) - }], info.typeElements && { + name: extractToTypeDef, description: getLocaleSpecificMessage(Diagnostics.Extract_to_typedef) + }] : append([{ + name: extractToTypeAlias, description: getLocaleSpecificMessage(Diagnostics.Extract_to_type_alias) + }], info.typeElements && { name: extractToInterface, description: getLocaleSpecificMessage(Diagnostics.Extract_to_interface) }) }]; - }, - getEditsForAction(context, actionName): RefactorEditInfo { - const { file } = context; - const info = Debug.checkDefined(getRangeToExtract(context), "Expected to find a range to extract"); - - const name = getUniqueName("NewType", file); - const edits = textChanges.ChangeTracker.with(context, changes => { - switch (actionName) { - case extractToTypeAlias: - Debug.assert(!info.isJS, "Invalid actionName/JS combo"); - return doTypeAliasChange(changes, file, name, info); - case extractToTypeDef: - Debug.assert(info.isJS, "Invalid actionName/JS combo"); - return doTypedefChange(changes, file, name, info); - case extractToInterface: - Debug.assert(!info.isJS && !!info.typeElements, "Invalid actionName/JS combo"); - return doInterfaceChange(changes, file, name, info as InterfaceInfo); - default: - Debug.fail("Unexpected action name"); - } - }); - - const renameFilename = file.fileName; - const renameLocation = getRenameLocation(edits, renameFilename, name, /*preferLastLocation*/ false); - return { edits, renameFilename, renameLocation }; + }, + getEditsForAction(context, actionName): RefactorEditInfo { + const { file } = context; + const info = Debug.checkDefined(getRangeToExtract(context), "Expected to find a range to extract"); + const name = getUniqueName("NewType", file); + const edits = ChangeTracker.with(context, changes => { + switch (actionName) { + case extractToTypeAlias: + Debug.assert(!info.isJS, "Invalid actionName/JS combo"); + return doTypeAliasChange(changes, file, name, info); + case extractToTypeDef: + Debug.assert(info.isJS, "Invalid actionName/JS combo"); + return doTypedefChange(changes, file, name, info); + case extractToInterface: + Debug.assert(!info.isJS && !!info.typeElements, "Invalid actionName/JS combo"); + return doInterfaceChange(changes, file, name, info as InterfaceInfo); + default: + Debug.fail("Unexpected action name"); + } + }); + const renameFilename = file.fileName; + const renameLocation = getRenameLocation(edits, renameFilename, name, /*preferLastLocation*/ false); + return { edits, renameFilename, renameLocation }; + } +}); +/* @internal */ +interface TypeAliasInfo { + isJS: boolean; + selection: TypeNode; + firstStatement: Statement; + typeParameters: readonly TypeParameterDeclaration[]; + typeElements?: readonly TypeElement[]; +} +/* @internal */ +interface InterfaceInfo { + isJS: boolean; + selection: TypeNode; + firstStatement: Statement; + typeParameters: readonly TypeParameterDeclaration[]; + typeElements: readonly TypeElement[]; +} +/* @internal */ +type Info = TypeAliasInfo | InterfaceInfo; +/* @internal */ +function getRangeToExtract(context: RefactorContext): Info | undefined { + const { file, startPosition } = context; + const isJS = isSourceFileJS(file); + const current = getTokenAtPosition(file, startPosition); + const range = createTextRangeFromSpan(getRefactorContextSpan(context)); + const selection = findAncestor(current, (node => node.parent && rangeContainsSkipTrivia(range, node, file) && !rangeContainsSkipTrivia(range, node.parent, file))); + if (!selection || !isTypeNode(selection)) + return undefined; + const checker = context.program.getTypeChecker(); + const firstStatement = Debug.checkDefined(findAncestor(selection, isStatement), "Should find a statement"); + const typeParameters = collectTypeParameters(checker, selection, firstStatement, file); + if (!typeParameters) + return undefined; + const typeElements = flattenTypeLiteralNodeReference(checker, selection); + return { isJS, selection, firstStatement, typeParameters, typeElements }; +} +/* @internal */ +function flattenTypeLiteralNodeReference(checker: TypeChecker, node: TypeNode | undefined): readonly TypeElement[] | undefined { + if (!node) + return undefined; + if (isIntersectionTypeNode(node)) { + const result: TypeElement[] = []; + const seen = createMap(); + for (const type of node.types) { + const flattenedTypeMembers = flattenTypeLiteralNodeReference(checker, type); + if (!flattenedTypeMembers || !flattenedTypeMembers.every(type => type.name && addToSeen(seen, (getNameFromPropertyName(type.name) as string)))) { + return undefined; + } + addRange(result, flattenedTypeMembers); } - }); - - interface TypeAliasInfo { - isJS: boolean; selection: TypeNode; firstStatement: Statement; typeParameters: readonly TypeParameterDeclaration[]; typeElements?: readonly TypeElement[]; + return result; } - - interface InterfaceInfo { - isJS: boolean; selection: TypeNode; firstStatement: Statement; typeParameters: readonly TypeParameterDeclaration[]; typeElements: readonly TypeElement[]; + else if (isParenthesizedTypeNode(node)) { + return flattenTypeLiteralNodeReference(checker, node.type); } - - type Info = TypeAliasInfo | InterfaceInfo; - - function getRangeToExtract(context: RefactorContext): Info | undefined { - const { file, startPosition } = context; - const isJS = isSourceFileJS(file); - const current = getTokenAtPosition(file, startPosition); - const range = createTextRangeFromSpan(getRefactorContextSpan(context)); - - const selection = findAncestor(current, (node => node.parent && rangeContainsSkipTrivia(range, node, file) && !rangeContainsSkipTrivia(range, node.parent, file))); - if (!selection || !isTypeNode(selection)) return undefined; - - const checker = context.program.getTypeChecker(); - const firstStatement = Debug.checkDefined(findAncestor(selection, isStatement), "Should find a statement"); - const typeParameters = collectTypeParameters(checker, selection, firstStatement, file); - if (!typeParameters) return undefined; - - const typeElements = flattenTypeLiteralNodeReference(checker, selection); - return { isJS, selection, firstStatement, typeParameters, typeElements }; + else if (isTypeLiteralNode(node)) { + return node.members; } - - function flattenTypeLiteralNodeReference(checker: TypeChecker, node: TypeNode | undefined): readonly TypeElement[] | undefined { - if (!node) return undefined; - if (isIntersectionTypeNode(node)) { - const result: TypeElement[] = []; - const seen = createMap(); - for (const type of node.types) { - const flattenedTypeMembers = flattenTypeLiteralNodeReference(checker, type); - if (!flattenedTypeMembers || !flattenedTypeMembers.every(type => type.name && addToSeen(seen, getNameFromPropertyName(type.name) as string))) { - return undefined; + return undefined; +} +/* @internal */ +function rangeContainsSkipTrivia(r1: TextRange, node: Node, file: SourceFile): boolean { + return rangeContainsStartEnd(r1, skipTrivia(file.text, node.pos), node.end); +} +/* @internal */ +function collectTypeParameters(checker: TypeChecker, selection: TypeNode, statement: Statement, file: SourceFile): TypeParameterDeclaration[] | undefined { + const result: TypeParameterDeclaration[] = []; + return visitor(selection) ? undefined : result; + function visitor(node: Node): true | undefined { + if (isTypeReferenceNode(node)) { + if (isIdentifier(node.typeName)) { + const symbol = checker.resolveName(node.typeName.text, node.typeName, SymbolFlags.TypeParameter, /* excludeGlobals */ true); + if (symbol) { + const declaration = cast(first(symbol.declarations), isTypeParameterDeclaration); + if (rangeContainsSkipTrivia(statement, declaration, file) && !rangeContainsSkipTrivia(selection, declaration, file)) { + result.push(declaration); + } } - - addRange(result, flattenedTypeMembers); } - return result; - } - else if (isParenthesizedTypeNode(node)) { - return flattenTypeLiteralNodeReference(checker, node.type); } - else if (isTypeLiteralNode(node)) { - return node.members; + else if (isInferTypeNode(node)) { + const conditionalTypeNode = findAncestor(node, n => isConditionalTypeNode(n) && rangeContainsSkipTrivia(n.extendsType, node, file)); + if (!conditionalTypeNode || !rangeContainsSkipTrivia(selection, conditionalTypeNode, file)) { + return true; + } } - return undefined; - } - - function rangeContainsSkipTrivia(r1: TextRange, node: Node, file: SourceFile): boolean { - return rangeContainsStartEnd(r1, skipTrivia(file.text, node.pos), node.end); - } - - function collectTypeParameters(checker: TypeChecker, selection: TypeNode, statement: Statement, file: SourceFile): TypeParameterDeclaration[] | undefined { - const result: TypeParameterDeclaration[] = []; - return visitor(selection) ? undefined : result; - - function visitor(node: Node): true | undefined { - if (isTypeReferenceNode(node)) { - if (isIdentifier(node.typeName)) { - const symbol = checker.resolveName(node.typeName.text, node.typeName, SymbolFlags.TypeParameter, /* excludeGlobals */ true); - if (symbol) { - const declaration = cast(first(symbol.declarations), isTypeParameterDeclaration); - if (rangeContainsSkipTrivia(statement, declaration, file) && !rangeContainsSkipTrivia(selection, declaration, file)) { - result.push(declaration); - } - } - } + else if ((isTypePredicateNode(node) || isThisTypeNode(node))) { + const functionLikeNode = findAncestor(node.parent, isFunctionLike); + if (functionLikeNode && functionLikeNode.type && rangeContainsSkipTrivia(functionLikeNode.type, node, file) && !rangeContainsSkipTrivia(selection, functionLikeNode, file)) { + return true; } - else if (isInferTypeNode(node)) { - const conditionalTypeNode = findAncestor(node, n => isConditionalTypeNode(n) && rangeContainsSkipTrivia(n.extendsType, node, file)); - if (!conditionalTypeNode || !rangeContainsSkipTrivia(selection, conditionalTypeNode, file)) { + } + else if (isTypeQueryNode(node)) { + if (isIdentifier(node.exprName)) { + const symbol = checker.resolveName(node.exprName.text, node.exprName, SymbolFlags.Value, /* excludeGlobals */ false); + if (symbol && rangeContainsSkipTrivia(statement, symbol.valueDeclaration, file) && !rangeContainsSkipTrivia(selection, symbol.valueDeclaration, file)) { return true; } } - else if ((isTypePredicateNode(node) || isThisTypeNode(node))) { - const functionLikeNode = findAncestor(node.parent, isFunctionLike); - if (functionLikeNode && functionLikeNode.type && rangeContainsSkipTrivia(functionLikeNode.type, node, file) && !rangeContainsSkipTrivia(selection, functionLikeNode, file)) { + else { + if (isThisIdentifier(node.exprName.left) && !rangeContainsSkipTrivia(selection, node.parent, file)) { return true; } } - else if (isTypeQueryNode(node)) { - if (isIdentifier(node.exprName)) { - const symbol = checker.resolveName(node.exprName.text, node.exprName, SymbolFlags.Value, /* excludeGlobals */ false); - if (symbol && rangeContainsSkipTrivia(statement, symbol.valueDeclaration, file) && !rangeContainsSkipTrivia(selection, symbol.valueDeclaration, file)) { - return true; - } - } - else { - if (isThisIdentifier(node.exprName.left) && !rangeContainsSkipTrivia(selection, node.parent, file)) { - return true; - } - } - } - return forEachChild(node, visitor); } + return forEachChild(node, visitor); } - - function doTypeAliasChange(changes: textChanges.ChangeTracker, file: SourceFile, name: string, info: TypeAliasInfo) { - const { firstStatement, selection, typeParameters } = info; - - const newTypeNode = createTypeAliasDeclaration( - /* decorators */ undefined, - /* modifiers */ undefined, - name, - typeParameters.map(id => updateTypeParameterDeclaration(id, id.name, id.constraint, /* defaultType */ undefined)), - selection - ); - changes.insertNodeBefore(file, firstStatement, newTypeNode, /* blankLineBetween */ true); - changes.replaceNode(file, selection, createTypeReferenceNode(name, typeParameters.map(id => createTypeReferenceNode(id.name, /* typeArguments */ undefined)))); - } - - function doInterfaceChange(changes: textChanges.ChangeTracker, file: SourceFile, name: string, info: InterfaceInfo) { - const { firstStatement, selection, typeParameters, typeElements } = info; - - const newTypeNode = createInterfaceDeclaration( - /* decorators */ undefined, - /* modifiers */ undefined, - name, - typeParameters, - /* heritageClauses */ undefined, - typeElements - ); - changes.insertNodeBefore(file, firstStatement, newTypeNode, /* blankLineBetween */ true); - changes.replaceNode(file, selection, createTypeReferenceNode(name, typeParameters.map(id => createTypeReferenceNode(id.name, /* typeArguments */ undefined)))); - } - - function doTypedefChange(changes: textChanges.ChangeTracker, file: SourceFile, name: string, info: Info) { - const { firstStatement, selection, typeParameters } = info; - - const node = createNode(SyntaxKind.JSDocTypedefTag); - node.tagName = createIdentifier("typedef"); // TODO: jsdoc factory https://github.com/Microsoft/TypeScript/pull/29539 - node.fullName = createIdentifier(name); - node.name = node.fullName; - node.typeExpression = createJSDocTypeExpression(selection); - - const templates: JSDocTemplateTag[] = []; - forEach(typeParameters, typeParameter => { - const constraint = getEffectiveConstraintOfTypeParameter(typeParameter); - - const template = createNode(SyntaxKind.JSDocTemplateTag); - template.tagName = createIdentifier("template"); - template.constraint = constraint && cast(constraint, isJSDocTypeExpression); - - const parameter = createNode(SyntaxKind.TypeParameter); - parameter.name = typeParameter.name; - template.typeParameters = createNodeArray([parameter]); - - templates.push(template); - }); - - changes.insertNodeBefore(file, firstStatement, createJSDocComment(/* comment */ undefined, createNodeArray(concatenate(templates, [node]))), /* blankLineBetween */ true); - changes.replaceNode(file, selection, createTypeReferenceNode(name, typeParameters.map(id => createTypeReferenceNode(id.name, /* typeArguments */ undefined)))); - } +} +/* @internal */ +function doTypeAliasChange(changes: ChangeTracker, file: SourceFile, name: string, info: TypeAliasInfo) { + const { firstStatement, selection, typeParameters } = info; + const newTypeNode = createTypeAliasDeclaration( + /* decorators */ undefined, + /* modifiers */ undefined, name, typeParameters.map(id => updateTypeParameterDeclaration(id, id.name, id.constraint, /* defaultType */ undefined)), selection); + changes.insertNodeBefore(file, firstStatement, newTypeNode, /* blankLineBetween */ true); + changes.replaceNode(file, selection, createTypeReferenceNode(name, typeParameters.map(id => createTypeReferenceNode(id.name, /* typeArguments */ undefined)))); +} +/* @internal */ +function doInterfaceChange(changes: ChangeTracker, file: SourceFile, name: string, info: InterfaceInfo) { + const { firstStatement, selection, typeParameters, typeElements } = info; + const newTypeNode = createInterfaceDeclaration( + /* decorators */ undefined, + /* modifiers */ undefined, name, typeParameters, + /* heritageClauses */ undefined, typeElements); + changes.insertNodeBefore(file, firstStatement, newTypeNode, /* blankLineBetween */ true); + changes.replaceNode(file, selection, createTypeReferenceNode(name, typeParameters.map(id => createTypeReferenceNode(id.name, /* typeArguments */ undefined)))); +} +/* @internal */ +function doTypedefChange(changes: ChangeTracker, file: SourceFile, name: string, info: Info) { + const { firstStatement, selection, typeParameters } = info; + const node = (createNode(SyntaxKind.JSDocTypedefTag)); + node.tagName = createIdentifier("typedef"); // TODO: jsdoc factory https://github.com/Microsoft/TypeScript/pull/29539 + node.fullName = createIdentifier(name); + node.name = node.fullName; + node.typeExpression = createJSDocTypeExpression(selection); + const templates: JSDocTemplateTag[] = []; + forEach(typeParameters, typeParameter => { + const constraint = getEffectiveConstraintOfTypeParameter(typeParameter); + const template = (createNode(SyntaxKind.JSDocTemplateTag)); + template.tagName = createIdentifier("template"); + template.constraint = constraint && cast(constraint, isJSDocTypeExpression); + const parameter = (createNode(SyntaxKind.TypeParameter)); + parameter.name = typeParameter.name; + template.typeParameters = createNodeArray([parameter]); + templates.push(template); + }); + changes.insertNodeBefore(file, firstStatement, createJSDocComment(/* comment */ undefined, createNodeArray(concatenate(templates, [node]))), /* blankLineBetween */ true); + changes.replaceNode(file, selection, createTypeReferenceNode(name, typeParameters.map(id => createTypeReferenceNode(id.name, /* typeArguments */ undefined)))); } diff --git a/src/services/refactors/generateGetAccessorAndSetAccessor.ts b/src/services/refactors/generateGetAccessorAndSetAccessor.ts index 3a00f528bef6a..d4f3d77277a16 100644 --- a/src/services/refactors/generateGetAccessorAndSetAccessor.ts +++ b/src/services/refactors/generateGetAccessorAndSetAccessor.ts @@ -1,29 +1,35 @@ +import { Diagnostics, ParameterPropertyDeclaration, PropertyDeclaration, PropertyAssignment, Identifier, StringLiteral, ClassLikeDeclaration, ObjectLiteralExpression, TypeNode, RefactorContext, ApplicableRefactorInfo, emptyArray, RefactorEditInfo, isSourceFileJS, suppressLeadingAndTrailingTrivia, isClassLike, getModifierFlags, ModifierFlags, SyntaxKind, createNodeArray, createModifiersFromModifierFlags, getFirstConstructorWithBody, isIdentifier, getRenameLocation, isParameter, DeclarationName, isStringLiteral, Node, isParameterPropertyDeclaration, isPropertyDeclaration, isPropertyAssignment, createIdentifier, createLiteral, createThis, createPropertyAccess, createElementAccess, NodeArray, Modifier, append, createToken, Token, CharacterCodes, getTokenAtPosition, findAncestor, nodeOverlapsWithStartEnd, getUniqueName, hasStaticModifier, hasReadonlyModifier, getTypeAnnotationNode, ModifiersArray, createGetAccessor, createBlock, createReturn, createSetAccessor, createParameter, createStatement, createAssignment, SourceFile, updateProperty, updatePropertyAssignment, updateParameter, cast, AccessorDeclaration, ConstructorDeclaration, isElementAccessExpression, isWriteAccess, createStringLiteral, isPropertyAccessExpression, isFunctionLike } from "../ts"; +import { registerRefactor } from "../ts.refactor"; +import { ChangeTracker } from "../ts.textChanges"; /* @internal */ -namespace ts.refactor.generateGetAccessorAndSetAccessor { - const actionName = "Generate 'get' and 'set' accessors"; - const actionDescription = Diagnostics.Generate_get_and_set_accessors.message; - registerRefactor(actionName, { getEditsForAction, getAvailableActions }); - - type AcceptedDeclaration = ParameterPropertyDeclaration | PropertyDeclaration | PropertyAssignment; - type AcceptedNameType = Identifier | StringLiteral; - type ContainerDeclaration = ClassLikeDeclaration | ObjectLiteralExpression; - - interface Info { - readonly container: ContainerDeclaration; - readonly isStatic: boolean; - readonly isReadonly: boolean; - readonly type: TypeNode | undefined; - readonly declaration: AcceptedDeclaration; - readonly fieldName: AcceptedNameType; - readonly accessorName: AcceptedNameType; - readonly originalName: string; - readonly renameAccessor: boolean; - } - - function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] { - if (!getConvertibleFieldAtPosition(context)) return emptyArray; - - return [{ +const actionName = "Generate 'get' and 'set' accessors"; +/* @internal */ +const actionDescription = Diagnostics.Generate_get_and_set_accessors.message; +/* @internal */ +registerRefactor(actionName, { getEditsForAction, getAvailableActions }); +/* @internal */ +type AcceptedDeclaration = ParameterPropertyDeclaration | PropertyDeclaration | PropertyAssignment; +/* @internal */ +type AcceptedNameType = Identifier | StringLiteral; +/* @internal */ +type ContainerDeclaration = ClassLikeDeclaration | ObjectLiteralExpression; +/* @internal */ +interface Info { + readonly container: ContainerDeclaration; + readonly isStatic: boolean; + readonly isReadonly: boolean; + readonly type: TypeNode | undefined; + readonly declaration: AcceptedDeclaration; + readonly fieldName: AcceptedNameType; + readonly accessorName: AcceptedNameType; + readonly originalName: string; + readonly renameAccessor: boolean; +} +/* @internal */ +function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] { + if (!getConvertibleFieldAtPosition(context)) + return emptyArray; + return [{ name: actionName, description: actionDescription, actions: [ @@ -33,208 +39,168 @@ namespace ts.refactor.generateGetAccessorAndSetAccessor { } ] }]; - } - - function getEditsForAction(context: RefactorContext, _actionName: string): RefactorEditInfo | undefined { - const { file } = context; - - const fieldInfo = getConvertibleFieldAtPosition(context); - if (!fieldInfo) return undefined; - - const isJS = isSourceFileJS(file); - const changeTracker = textChanges.ChangeTracker.fromContext(context); - const { isStatic, isReadonly, fieldName, accessorName, originalName, type, container, declaration, renameAccessor } = fieldInfo; - - suppressLeadingAndTrailingTrivia(fieldName); - suppressLeadingAndTrailingTrivia(declaration); - suppressLeadingAndTrailingTrivia(container); - - const isInClassLike = isClassLike(container); - // avoid Readonly modifier because it will convert to get accessor - const modifierFlags = getModifierFlags(declaration) & ~ModifierFlags.Readonly; - const accessorModifiers = isInClassLike - ? !modifierFlags || modifierFlags & ModifierFlags.Private - ? getModifiers(isJS, isStatic, SyntaxKind.PublicKeyword) - : createNodeArray(createModifiersFromModifierFlags(modifierFlags)) - : undefined; - const fieldModifiers = isInClassLike ? getModifiers(isJS, isStatic, SyntaxKind.PrivateKeyword) : undefined; - - updateFieldDeclaration(changeTracker, file, declaration, fieldName, fieldModifiers); - - const getAccessor = generateGetAccessor(fieldName, accessorName, type, accessorModifiers, isStatic, container); - suppressLeadingAndTrailingTrivia(getAccessor); - insertAccessor(changeTracker, file, getAccessor, declaration, container); - - if (isReadonly) { - // readonly modifier only existed in classLikeDeclaration - const constructor = getFirstConstructorWithBody(container); - if (constructor) { - updateReadonlyPropertyInitializerStatementConstructor(changeTracker, file, constructor, fieldName.text, originalName); - } - } - else { - const setAccessor = generateSetAccessor(fieldName, accessorName, type, accessorModifiers, isStatic, container); - suppressLeadingAndTrailingTrivia(setAccessor); - insertAccessor(changeTracker, file, setAccessor, declaration, container); +} +/* @internal */ +function getEditsForAction(context: RefactorContext, _actionName: string): RefactorEditInfo | undefined { + const { file } = context; + const fieldInfo = getConvertibleFieldAtPosition(context); + if (!fieldInfo) + return undefined; + const isJS = isSourceFileJS(file); + const changeTracker = ChangeTracker.fromContext(context); + const { isStatic, isReadonly, fieldName, accessorName, originalName, type, container, declaration, renameAccessor } = fieldInfo; + suppressLeadingAndTrailingTrivia(fieldName); + suppressLeadingAndTrailingTrivia(declaration); + suppressLeadingAndTrailingTrivia(container); + const isInClassLike = isClassLike(container); + // avoid Readonly modifier because it will convert to get accessor + const modifierFlags = getModifierFlags(declaration) & ~ModifierFlags.Readonly; + const accessorModifiers = isInClassLike + ? !modifierFlags || modifierFlags & ModifierFlags.Private + ? getModifiers(isJS, isStatic, SyntaxKind.PublicKeyword) + : createNodeArray(createModifiersFromModifierFlags(modifierFlags)) + : undefined; + const fieldModifiers = isInClassLike ? getModifiers(isJS, isStatic, SyntaxKind.PrivateKeyword) : undefined; + updateFieldDeclaration(changeTracker, file, declaration, fieldName, fieldModifiers); + const getAccessor = generateGetAccessor(fieldName, accessorName, type, accessorModifiers, isStatic, container); + suppressLeadingAndTrailingTrivia(getAccessor); + insertAccessor(changeTracker, file, getAccessor, declaration, container); + if (isReadonly) { + // readonly modifier only existed in classLikeDeclaration + const constructor = getFirstConstructorWithBody((container)); + if (constructor) { + updateReadonlyPropertyInitializerStatementConstructor(changeTracker, file, constructor, fieldName.text, originalName); } - - const edits = changeTracker.getChanges(); - const renameFilename = file.fileName; - - const nameNeedRename = renameAccessor ? accessorName : fieldName; - const renameLocationOffset = isIdentifier(nameNeedRename) ? 0 : -1; - const renameLocation = renameLocationOffset + getRenameLocation(edits, renameFilename, nameNeedRename.text, /*preferLastLocation*/ isParameter(declaration)); - return { renameFilename, renameLocation, edits }; - } - - function isConvertibleName(name: DeclarationName): name is AcceptedNameType { - return isIdentifier(name) || isStringLiteral(name); } - - function isAcceptedDeclaration(node: Node): node is AcceptedDeclaration { - return isParameterPropertyDeclaration(node, node.parent) || isPropertyDeclaration(node) || isPropertyAssignment(node); - } - - function createPropertyName(name: string, originalName: AcceptedNameType) { - return isIdentifier(originalName) ? createIdentifier(name) : createLiteral(name); - } - - function createAccessorAccessExpression(fieldName: AcceptedNameType, isStatic: boolean, container: ContainerDeclaration) { - const leftHead = isStatic ? (container).name! : createThis(); // TODO: GH#18217 - return isIdentifier(fieldName) ? createPropertyAccess(leftHead, fieldName) : createElementAccess(leftHead, createLiteral(fieldName)); - } - - function getModifiers(isJS: boolean, isStatic: boolean, accessModifier: SyntaxKind.PublicKeyword | SyntaxKind.PrivateKeyword): NodeArray | undefined { - const modifiers = append( - !isJS ? [createToken(accessModifier) as Token | Token] : undefined, - isStatic ? createToken(SyntaxKind.StaticKeyword) : undefined - ); - return modifiers && createNodeArray(modifiers); - } - - function startsWithUnderscore(name: string): boolean { - return name.charCodeAt(0) === CharacterCodes._; - } - - function getConvertibleFieldAtPosition(context: RefactorContext): Info | undefined { - const { file, startPosition, endPosition } = context; - - const node = getTokenAtPosition(file, startPosition); - const declaration = findAncestor(node.parent, isAcceptedDeclaration); - // make sure declaration have AccessibilityModifier or Static Modifier or Readonly Modifier - const meaning = ModifierFlags.AccessibilityModifier | ModifierFlags.Static | ModifierFlags.Readonly; - if (!declaration || !nodeOverlapsWithStartEnd(declaration.name, file, startPosition, endPosition!) // TODO: GH#18217 - || !isConvertibleName(declaration.name) || (getModifierFlags(declaration) | meaning) !== meaning) return undefined; - - const name = declaration.name.text; - const startWithUnderscore = startsWithUnderscore(name); - const fieldName = createPropertyName(startWithUnderscore ? name : getUniqueName(`_${name}`, file), declaration.name); - const accessorName = createPropertyName(startWithUnderscore ? getUniqueName(name.substring(1), file) : name, declaration.name); - return { - isStatic: hasStaticModifier(declaration), - isReadonly: hasReadonlyModifier(declaration), - type: getTypeAnnotationNode(declaration), - container: declaration.kind === SyntaxKind.Parameter ? declaration.parent.parent : declaration.parent, - originalName: (declaration.name).text, - declaration, - fieldName, - accessorName, - renameAccessor: startWithUnderscore - }; - } - - function generateGetAccessor(fieldName: AcceptedNameType, accessorName: AcceptedNameType, type: TypeNode | undefined, modifiers: ModifiersArray | undefined, isStatic: boolean, container: ContainerDeclaration) { - return createGetAccessor( - /*decorators*/ undefined, - modifiers, - accessorName, - /*parameters*/ undefined!, // TODO: GH#18217 - type, - createBlock([ - createReturn( - createAccessorAccessExpression(fieldName, isStatic, container) - ) - ], /*multiLine*/ true) - ); - } - - function generateSetAccessor(fieldName: AcceptedNameType, accessorName: AcceptedNameType, type: TypeNode | undefined, modifiers: ModifiersArray | undefined, isStatic: boolean, container: ContainerDeclaration) { - return createSetAccessor( - /*decorators*/ undefined, - modifiers, - accessorName, - [createParameter( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, - createIdentifier("value"), - /*questionToken*/ undefined, - type - )], - createBlock([ - createStatement( - createAssignment( - createAccessorAccessExpression(fieldName, isStatic, container), - createIdentifier("value") - ) - ) - ], /*multiLine*/ true) - ); + else { + const setAccessor = generateSetAccessor(fieldName, accessorName, type, accessorModifiers, isStatic, container); + suppressLeadingAndTrailingTrivia(setAccessor); + insertAccessor(changeTracker, file, setAccessor, declaration, container); + } + const edits = changeTracker.getChanges(); + const renameFilename = file.fileName; + const nameNeedRename = renameAccessor ? accessorName : fieldName; + const renameLocationOffset = isIdentifier(nameNeedRename) ? 0 : -1; + const renameLocation = renameLocationOffset + getRenameLocation(edits, renameFilename, nameNeedRename.text, /*preferLastLocation*/ isParameter(declaration)); + return { renameFilename, renameLocation, edits }; +} +/* @internal */ +function isConvertibleName(name: DeclarationName): name is AcceptedNameType { + return isIdentifier(name) || isStringLiteral(name); +} +/* @internal */ +function isAcceptedDeclaration(node: Node): node is AcceptedDeclaration { + return isParameterPropertyDeclaration(node, node.parent) || isPropertyDeclaration(node) || isPropertyAssignment(node); +} +/* @internal */ +function createPropertyName(name: string, originalName: AcceptedNameType) { + return isIdentifier(originalName) ? createIdentifier(name) : createLiteral(name); +} +/* @internal */ +function createAccessorAccessExpression(fieldName: AcceptedNameType, isStatic: boolean, container: ContainerDeclaration) { + const leftHead = isStatic ? (container).name! : createThis(); // TODO: GH#18217 + return isIdentifier(fieldName) ? createPropertyAccess(leftHead, fieldName) : createElementAccess(leftHead, createLiteral(fieldName)); +} +/* @internal */ +function getModifiers(isJS: boolean, isStatic: boolean, accessModifier: SyntaxKind.PublicKeyword | SyntaxKind.PrivateKeyword): NodeArray | undefined { + const modifiers = append(!isJS ? [(createToken(accessModifier) as Token | Token)] : undefined, isStatic ? createToken(SyntaxKind.StaticKeyword) : undefined); + return modifiers && createNodeArray(modifiers); +} +/* @internal */ +function startsWithUnderscore(name: string): boolean { + return name.charCodeAt(0) === CharacterCodes._; +} +/* @internal */ +function getConvertibleFieldAtPosition(context: RefactorContext): Info | undefined { + const { file, startPosition, endPosition } = context; + const node = getTokenAtPosition(file, startPosition); + const declaration = findAncestor(node.parent, isAcceptedDeclaration); + // make sure declaration have AccessibilityModifier or Static Modifier or Readonly Modifier + const meaning = ModifierFlags.AccessibilityModifier | ModifierFlags.Static | ModifierFlags.Readonly; + if (!declaration || !nodeOverlapsWithStartEnd(declaration.name, file, startPosition, (endPosition!)) // TODO: GH#18217 + || !isConvertibleName(declaration.name) || (getModifierFlags(declaration) | meaning) !== meaning) + return undefined; + const name = declaration.name.text; + const startWithUnderscore = startsWithUnderscore(name); + const fieldName = createPropertyName(startWithUnderscore ? name : getUniqueName(`_${name}`, file), declaration.name); + const accessorName = createPropertyName(startWithUnderscore ? getUniqueName(name.substring(1), file) : name, declaration.name); + return { + isStatic: hasStaticModifier(declaration), + isReadonly: hasReadonlyModifier(declaration), + type: getTypeAnnotationNode(declaration), + container: declaration.kind === SyntaxKind.Parameter ? declaration.parent.parent : declaration.parent, + originalName: (declaration.name).text, + declaration, + fieldName, + accessorName, + renameAccessor: startWithUnderscore + }; +} +/* @internal */ +function generateGetAccessor(fieldName: AcceptedNameType, accessorName: AcceptedNameType, type: TypeNode | undefined, modifiers: ModifiersArray | undefined, isStatic: boolean, container: ContainerDeclaration) { + return createGetAccessor( + /*decorators*/ undefined, modifiers, accessorName, + /*parameters*/ (undefined!), // TODO: GH#18217 + type, createBlock([ + createReturn(createAccessorAccessExpression(fieldName, isStatic, container)) + ], /*multiLine*/ true)); +} +/* @internal */ +function generateSetAccessor(fieldName: AcceptedNameType, accessorName: AcceptedNameType, type: TypeNode | undefined, modifiers: ModifiersArray | undefined, isStatic: boolean, container: ContainerDeclaration) { + return createSetAccessor( + /*decorators*/ undefined, modifiers, accessorName, [createParameter( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, createIdentifier("value"), + /*questionToken*/ undefined, type)], createBlock([ + createStatement(createAssignment(createAccessorAccessExpression(fieldName, isStatic, container), createIdentifier("value"))) + ], /*multiLine*/ true)); +} +/* @internal */ +function updatePropertyDeclaration(changeTracker: ChangeTracker, file: SourceFile, declaration: PropertyDeclaration, fieldName: AcceptedNameType, modifiers: ModifiersArray | undefined) { + const property = updateProperty(declaration, declaration.decorators, modifiers, fieldName, declaration.questionToken || declaration.exclamationToken, declaration.type, declaration.initializer); + changeTracker.replaceNode(file, declaration, property); +} +/* @internal */ +function updatePropertyAssignmentDeclaration(changeTracker: ChangeTracker, file: SourceFile, declaration: PropertyAssignment, fieldName: AcceptedNameType) { + const assignment = updatePropertyAssignment(declaration, fieldName, declaration.initializer); + changeTracker.replacePropertyAssignment(file, declaration, assignment); +} +/* @internal */ +function updateFieldDeclaration(changeTracker: ChangeTracker, file: SourceFile, declaration: AcceptedDeclaration, fieldName: AcceptedNameType, modifiers: ModifiersArray | undefined) { + if (isPropertyDeclaration(declaration)) { + updatePropertyDeclaration(changeTracker, file, declaration, fieldName, modifiers); } - - function updatePropertyDeclaration(changeTracker: textChanges.ChangeTracker, file: SourceFile, declaration: PropertyDeclaration, fieldName: AcceptedNameType, modifiers: ModifiersArray | undefined) { - const property = updateProperty( - declaration, - declaration.decorators, - modifiers, - fieldName, - declaration.questionToken || declaration.exclamationToken, - declaration.type, - declaration.initializer - ); - changeTracker.replaceNode(file, declaration, property); + else if (isPropertyAssignment(declaration)) { + updatePropertyAssignmentDeclaration(changeTracker, file, declaration, fieldName); } - - function updatePropertyAssignmentDeclaration(changeTracker: textChanges.ChangeTracker, file: SourceFile, declaration: PropertyAssignment, fieldName: AcceptedNameType) { - const assignment = updatePropertyAssignment(declaration, fieldName, declaration.initializer); - changeTracker.replacePropertyAssignment(file, declaration, assignment); + else { + changeTracker.replaceNode(file, declaration, updateParameter(declaration, declaration.decorators, modifiers, declaration.dotDotDotToken, cast(fieldName, isIdentifier), declaration.questionToken, declaration.type, declaration.initializer)); } - - function updateFieldDeclaration(changeTracker: textChanges.ChangeTracker, file: SourceFile, declaration: AcceptedDeclaration, fieldName: AcceptedNameType, modifiers: ModifiersArray | undefined) { - if (isPropertyDeclaration(declaration)) { - updatePropertyDeclaration(changeTracker, file, declaration, fieldName, modifiers); +} +/* @internal */ +function insertAccessor(changeTracker: ChangeTracker, file: SourceFile, accessor: AccessorDeclaration, declaration: AcceptedDeclaration, container: ContainerDeclaration) { + isParameterPropertyDeclaration(declaration, declaration.parent) ? changeTracker.insertNodeAtClassStart(file, (container), accessor) : + isPropertyAssignment(declaration) ? changeTracker.insertNodeAfterComma(file, declaration, accessor) : + changeTracker.insertNodeAfter(file, declaration, accessor); +} +/* @internal */ +function updateReadonlyPropertyInitializerStatementConstructor(changeTracker: ChangeTracker, file: SourceFile, constructor: ConstructorDeclaration, fieldName: string, originalName: string) { + if (!constructor.body) + return; + constructor.body.forEachChild(function recur(node) { + if (isElementAccessExpression(node) && + node.expression.kind === SyntaxKind.ThisKeyword && + isStringLiteral(node.argumentExpression) && + node.argumentExpression.text === originalName && + isWriteAccess(node)) { + changeTracker.replaceNode(file, node.argumentExpression, createStringLiteral(fieldName)); } - else if (isPropertyAssignment(declaration)) { - updatePropertyAssignmentDeclaration(changeTracker, file, declaration, fieldName); + if (isPropertyAccessExpression(node) && node.expression.kind === SyntaxKind.ThisKeyword && node.name.text === originalName && isWriteAccess(node)) { + changeTracker.replaceNode(file, node.name, createIdentifier(fieldName)); } - else { - changeTracker.replaceNode(file, declaration, - updateParameter(declaration, declaration.decorators, modifiers, declaration.dotDotDotToken, cast(fieldName, isIdentifier), declaration.questionToken, declaration.type, declaration.initializer)); + if (!isFunctionLike(node) && !isClassLike(node)) { + node.forEachChild(recur); } - } - - function insertAccessor(changeTracker: textChanges.ChangeTracker, file: SourceFile, accessor: AccessorDeclaration, declaration: AcceptedDeclaration, container: ContainerDeclaration) { - isParameterPropertyDeclaration(declaration, declaration.parent) ? changeTracker.insertNodeAtClassStart(file, container, accessor) : - isPropertyAssignment(declaration) ? changeTracker.insertNodeAfterComma(file, declaration, accessor) : - changeTracker.insertNodeAfter(file, declaration, accessor); - } - - function updateReadonlyPropertyInitializerStatementConstructor(changeTracker: textChanges.ChangeTracker, file: SourceFile, constructor: ConstructorDeclaration, fieldName: string, originalName: string) { - if (!constructor.body) return; - constructor.body.forEachChild(function recur(node) { - if (isElementAccessExpression(node) && - node.expression.kind === SyntaxKind.ThisKeyword && - isStringLiteral(node.argumentExpression) && - node.argumentExpression.text === originalName && - isWriteAccess(node)) { - changeTracker.replaceNode(file, node.argumentExpression, createStringLiteral(fieldName)); - } - if (isPropertyAccessExpression(node) && node.expression.kind === SyntaxKind.ThisKeyword && node.name.text === originalName && isWriteAccess(node)) { - changeTracker.replaceNode(file, node.name, createIdentifier(fieldName)); - } - if (!isFunctionLike(node) && !isClassLike(node)) { - node.forEachChild(recur); - } - }); - } + }); } diff --git a/src/services/refactors/moveToNewFile.ts b/src/services/refactors/moveToNewFile.ts index a9b8171cbc98b..a76ffaee3a28b 100644 --- a/src/services/refactors/moveToNewFile.ts +++ b/src/services/refactors/moveToNewFile.ts @@ -1,805 +1,793 @@ +import { registerRefactor } from "../ts.refactor"; +import { ApplicableRefactorInfo, emptyArray, getLocaleSpecificMessage, Diagnostics, RefactorEditInfo, Debug, Statement, RefactorContext, createTextRangeFromSpan, getRefactorContextSpan, findIndex, isNamedDeclaration, rangeContainsRange, SourceFile, Program, LanguageServiceHost, UserPreferences, getDirectoryPath, extensionFromPath, combinePaths, hostGetCanonicalFileName, getRangesWhere, Node, SyntaxKind, hasModifier, ModifierFlags, VariableStatement, isRequireCall, GetCanonicalFileName, normalizePath, getRelativePathFromFile, tryCast, isObjectLiteralExpression, find, PropertyAssignment, isPropertyAssignment, isStringLiteral, isArrayLiteralExpression, last, createLiteral, getQuotePreference, insertImport, TypeChecker, contains, Identifier, isBindingElement, getPropertySymbolFromBindingElement, ObjectBindingElementWithoutPropertyName, skipAlias, isIdentifier, ScriptTarget, isPropertyAccessExpression, SymbolFlags, getUniqueName, createIdentifier, createImportDeclaration, createImportClause, createNamespaceImport, createImportEqualsDeclaration, createExternalModuleReference, createVariableDeclaration, StringLiteralLike, isImportDeclaration, isImportEqualsDeclaration, isExternalModuleReference, isStringLiteralLike, isVariableStatement, ImportDeclaration, ImportEqualsDeclaration, ExternalModuleReference, VariableDeclaration, RequireOrImportCall, QuotePreference, InternalSymbolName, symbolNameNoDefault, ensurePathIsNonModuleName, createImportSpecifier, makeImportIfNecessary, createBindingElement, createObjectBindingPattern, BindingName, TypeNode, Expression, NodeFlags, createVariableStatement, createVariableDeclarationList, CallExpression, createCall, flatMap, updateImportClause, isVariableDeclarationList, append, nodeSeenTracker, removeFileExtension, getBaseFileName, TransformFlags, isExpressionStatement, some, Declaration, isVariableDeclaration, isSourceFile, NamedImportBindings, createNamedImports, Symbol, isDeclarationName, createMap, getSymbolId, forEachEntry, copyEntries, ExpressionStatement, BinaryExpression, PropertyAccessExpression, FunctionDeclaration, ClassDeclaration, EnumDeclaration, TypeAliasDeclaration, InterfaceDeclaration, ModuleDeclaration, VariableDeclarationList, BindingElement, firstDefined, isBinaryExpression, getAssignmentDeclarationKind, AssignmentDeclarationKind, cast, isOmittedExpression, escapeLeadingUnderscores, concatenate, createModifier, updateFunctionDeclaration, updateClassDeclaration, updateVariableStatement, updateModuleDeclaration, updateEnumDeclaration, updateTypeAliasDeclaration, updateInterfaceDeclaration, updateImportEqualsDeclaration, DeclarationStatement, mapDefined, createExpressionStatement, createBinary, createPropertyAccess } from "../ts"; +import { ChangeTracker } from "../ts.textChanges"; +import { moduleSpecifierToValidIdentifier } from "../ts.codefix"; +import { Core } from "../ts.FindAllReferences"; /* @internal */ -namespace ts.refactor { - const refactorName = "Move to a new file"; - registerRefactor(refactorName, { - getAvailableActions(context): readonly ApplicableRefactorInfo[] { - if (!context.preferences.allowTextChangesInNewFiles || getStatementsToMove(context) === undefined) return emptyArray; - const description = getLocaleSpecificMessage(Diagnostics.Move_to_a_new_file); - return [{ name: refactorName, description, actions: [{ name: refactorName, description }] }]; - }, - getEditsForAction(context, actionName): RefactorEditInfo { - Debug.assert(actionName === refactorName, "Wrong refactor invoked"); - const statements = Debug.checkDefined(getStatementsToMove(context)); - const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, statements, t, context.host, context.preferences)); - return { edits, renameFilename: undefined, renameLocation: undefined }; - } +const refactorName = "Move to a new file"; +/* @internal */ +registerRefactor(refactorName, { + getAvailableActions(context): readonly ApplicableRefactorInfo[] { + if (!context.preferences.allowTextChangesInNewFiles || getStatementsToMove(context) === undefined) + return emptyArray; + const description = getLocaleSpecificMessage(Diagnostics.Move_to_a_new_file); + return [{ name: refactorName, description, actions: [{ name: refactorName, description }] }]; + }, + getEditsForAction(context, actionName): RefactorEditInfo { + Debug.assert(actionName === refactorName, "Wrong refactor invoked"); + const statements = Debug.checkDefined(getStatementsToMove(context)); + const edits = ChangeTracker.with(context, t => doChange(context.file, context.program, statements, t, context.host, context.preferences)); + return { edits, renameFilename: undefined, renameLocation: undefined }; + } +}); +/* @internal */ +interface RangeToMove { + readonly toMove: readonly Statement[]; + readonly afterLast: Statement | undefined; +} +/* @internal */ +function getRangeToMove(context: RefactorContext): RangeToMove | undefined { + const { file } = context; + const range = createTextRangeFromSpan(getRefactorContextSpan(context)); + const { statements } = file; + const startNodeIndex = findIndex(statements, s => s.end > range.pos); + if (startNodeIndex === -1) + return undefined; + const startStatement = statements[startNodeIndex]; + if (isNamedDeclaration(startStatement) && startStatement.name && rangeContainsRange(startStatement.name, range)) { + return { toMove: [statements[startNodeIndex]], afterLast: statements[startNodeIndex + 1] }; + } + // Can't only partially include the start node or be partially into the next node + if (range.pos > startStatement.getStart(file)) + return undefined; + const afterEndNodeIndex = findIndex(statements, s => s.end > range.end, startNodeIndex); + // Can't be partially into the next node + if (afterEndNodeIndex !== -1 && (afterEndNodeIndex === 0 || statements[afterEndNodeIndex].getStart(file) < range.end)) + return undefined; + return { + toMove: statements.slice(startNodeIndex, afterEndNodeIndex === -1 ? statements.length : afterEndNodeIndex), + afterLast: afterEndNodeIndex === -1 ? undefined : statements[afterEndNodeIndex], + }; +} +/* @internal */ +function doChange(oldFile: SourceFile, program: Program, toMove: ToMove, changes: ChangeTracker, host: LanguageServiceHost, preferences: UserPreferences): void { + const checker = program.getTypeChecker(); + const usage = getUsageInfo(oldFile, toMove.all, checker); + const currentDirectory = getDirectoryPath(oldFile.fileName); + const extension = extensionFromPath(oldFile.fileName); + const newModuleName = makeUniqueModuleName(getNewModuleName(usage.movedSymbols), extension, currentDirectory, host); + const newFileNameWithExtension = newModuleName + extension; + // If previous file was global, this is easy. + changes.createNewFile(oldFile, combinePaths(currentDirectory, newFileNameWithExtension), getNewStatementsAndRemoveFromOldFile(oldFile, usage, changes, toMove, program, newModuleName, preferences)); + addNewFileToTsconfig(program, changes, oldFile.fileName, newFileNameWithExtension, hostGetCanonicalFileName(host)); +} +/* @internal */ +interface StatementRange { + readonly first: Statement; + readonly afterLast: Statement | undefined; +} +/* @internal */ +interface ToMove { + readonly all: readonly Statement[]; + readonly ranges: readonly StatementRange[]; +} +// Filters imports out of the range of statements to move. Imports will be copied to the new file anyway, and may still be needed in the old file. +/* @internal */ +function getStatementsToMove(context: RefactorContext): ToMove | undefined { + const rangeToMove = getRangeToMove(context); + if (rangeToMove === undefined) + return undefined; + const all: Statement[] = []; + const ranges: StatementRange[] = []; + const { toMove, afterLast } = rangeToMove; + getRangesWhere(toMove, s => !isPureImport(s), (start, afterEndIndex) => { + for (let i = start; i < afterEndIndex; i++) + all.push(toMove[i]); + ranges.push({ first: toMove[start], afterLast }); }); - - interface RangeToMove { readonly toMove: readonly Statement[]; readonly afterLast: Statement | undefined; } - function getRangeToMove(context: RefactorContext): RangeToMove | undefined { - const { file } = context; - const range = createTextRangeFromSpan(getRefactorContextSpan(context)); - const { statements } = file; - - const startNodeIndex = findIndex(statements, s => s.end > range.pos); - if (startNodeIndex === -1) return undefined; - - const startStatement = statements[startNodeIndex]; - if (isNamedDeclaration(startStatement) && startStatement.name && rangeContainsRange(startStatement.name, range)) { - return { toMove: [statements[startNodeIndex]], afterLast: statements[startNodeIndex + 1] }; - } - - // Can't only partially include the start node or be partially into the next node - if (range.pos > startStatement.getStart(file)) return undefined; - const afterEndNodeIndex = findIndex(statements, s => s.end > range.end, startNodeIndex); - // Can't be partially into the next node - if (afterEndNodeIndex !== -1 && (afterEndNodeIndex === 0 || statements[afterEndNodeIndex].getStart(file) < range.end)) return undefined; - - return { - toMove: statements.slice(startNodeIndex, afterEndNodeIndex === -1 ? statements.length : afterEndNodeIndex), - afterLast: afterEndNodeIndex === -1 ? undefined : statements[afterEndNodeIndex], - }; - } - - function doChange(oldFile: SourceFile, program: Program, toMove: ToMove, changes: textChanges.ChangeTracker, host: LanguageServiceHost, preferences: UserPreferences): void { - const checker = program.getTypeChecker(); - const usage = getUsageInfo(oldFile, toMove.all, checker); - - const currentDirectory = getDirectoryPath(oldFile.fileName); - const extension = extensionFromPath(oldFile.fileName); - const newModuleName = makeUniqueModuleName(getNewModuleName(usage.movedSymbols), extension, currentDirectory, host); - const newFileNameWithExtension = newModuleName + extension; - - // If previous file was global, this is easy. - changes.createNewFile(oldFile, combinePaths(currentDirectory, newFileNameWithExtension), getNewStatementsAndRemoveFromOldFile(oldFile, usage, changes, toMove, program, newModuleName, preferences)); - - addNewFileToTsconfig(program, changes, oldFile.fileName, newFileNameWithExtension, hostGetCanonicalFileName(host)); - } - - interface StatementRange { - readonly first: Statement; - readonly afterLast: Statement | undefined; - } - interface ToMove { - readonly all: readonly Statement[]; - readonly ranges: readonly StatementRange[]; - } - - // Filters imports out of the range of statements to move. Imports will be copied to the new file anyway, and may still be needed in the old file. - function getStatementsToMove(context: RefactorContext): ToMove | undefined { - const rangeToMove = getRangeToMove(context); - if (rangeToMove === undefined) return undefined; - const all: Statement[] = []; - const ranges: StatementRange[] = []; - const { toMove, afterLast } = rangeToMove; - getRangesWhere(toMove, s => !isPureImport(s), (start, afterEndIndex) => { - for (let i = start; i < afterEndIndex; i++) all.push(toMove[i]); - ranges.push({ first: toMove[start], afterLast }); - }); - return all.length === 0 ? undefined : { all, ranges }; - } - - function isPureImport(node: Node): boolean { - switch (node.kind) { - case SyntaxKind.ImportDeclaration: - return true; - case SyntaxKind.ImportEqualsDeclaration: - return !hasModifier(node, ModifierFlags.Export); - case SyntaxKind.VariableStatement: - return (node as VariableStatement).declarationList.declarations.every(d => !!d.initializer && isRequireCall(d.initializer, /*checkArgumentIsStringLiteralLike*/ true)); - default: - return false; - } + return all.length === 0 ? undefined : { all, ranges }; +} +/* @internal */ +function isPureImport(node: Node): boolean { + switch (node.kind) { + case SyntaxKind.ImportDeclaration: + return true; + case SyntaxKind.ImportEqualsDeclaration: + return !hasModifier(node, ModifierFlags.Export); + case SyntaxKind.VariableStatement: + return (node as VariableStatement).declarationList.declarations.every(d => !!d.initializer && isRequireCall(d.initializer, /*checkArgumentIsStringLiteralLike*/ true)); + default: + return false; } - - function addNewFileToTsconfig(program: Program, changes: textChanges.ChangeTracker, oldFileName: string, newFileNameWithExtension: string, getCanonicalFileName: GetCanonicalFileName): void { - const cfg = program.getCompilerOptions().configFile; - if (!cfg) return; - - const newFileAbsolutePath = normalizePath(combinePaths(oldFileName, "..", newFileNameWithExtension)); - const newFilePath = getRelativePathFromFile(cfg.fileName, newFileAbsolutePath, getCanonicalFileName); - - const cfgObject = cfg.statements[0] && tryCast(cfg.statements[0].expression, isObjectLiteralExpression); - const filesProp = cfgObject && find(cfgObject.properties, (prop): prop is PropertyAssignment => - isPropertyAssignment(prop) && isStringLiteral(prop.name) && prop.name.text === "files"); - if (filesProp && isArrayLiteralExpression(filesProp.initializer)) { - changes.insertNodeInListAfter(cfg, last(filesProp.initializer.elements), createLiteral(newFilePath), filesProp.initializer.elements); - } +} +/* @internal */ +function addNewFileToTsconfig(program: Program, changes: ChangeTracker, oldFileName: string, newFileNameWithExtension: string, getCanonicalFileName: GetCanonicalFileName): void { + const cfg = program.getCompilerOptions().configFile; + if (!cfg) + return; + const newFileAbsolutePath = normalizePath(combinePaths(oldFileName, "..", newFileNameWithExtension)); + const newFilePath = getRelativePathFromFile(cfg.fileName, newFileAbsolutePath, getCanonicalFileName); + const cfgObject = cfg.statements[0] && tryCast(cfg.statements[0].expression, isObjectLiteralExpression); + const filesProp = cfgObject && find(cfgObject.properties, (prop): prop is PropertyAssignment => isPropertyAssignment(prop) && isStringLiteral(prop.name) && prop.name.text === "files"); + if (filesProp && isArrayLiteralExpression(filesProp.initializer)) { + changes.insertNodeInListAfter(cfg, last(filesProp.initializer.elements), createLiteral(newFilePath), filesProp.initializer.elements); } - - function getNewStatementsAndRemoveFromOldFile( - oldFile: SourceFile, usage: UsageInfo, changes: textChanges.ChangeTracker, toMove: ToMove, program: Program, newModuleName: string, preferences: UserPreferences, - ): readonly Statement[] { - const checker = program.getTypeChecker(); - - if (!oldFile.externalModuleIndicator && !oldFile.commonJsModuleIndicator) { - deleteMovedStatements(oldFile, toMove.ranges, changes); - return toMove.all; - } - - const useEs6ModuleSyntax = !!oldFile.externalModuleIndicator; - const quotePreference = getQuotePreference(oldFile, preferences); - const importsFromNewFile = createOldFileImportsFromNewFile(usage.oldFileImportsFromNewFile, newModuleName, useEs6ModuleSyntax, quotePreference); - if (importsFromNewFile) { - insertImport(changes, oldFile, importsFromNewFile, /*blankLineBetween*/ true); - } - - deleteUnusedOldImports(oldFile, toMove.all, changes, usage.unusedImportsFromOldFile, checker); +} +/* @internal */ +function getNewStatementsAndRemoveFromOldFile(oldFile: SourceFile, usage: UsageInfo, changes: ChangeTracker, toMove: ToMove, program: Program, newModuleName: string, preferences: UserPreferences): readonly Statement[] { + const checker = program.getTypeChecker(); + if (!oldFile.externalModuleIndicator && !oldFile.commonJsModuleIndicator) { deleteMovedStatements(oldFile, toMove.ranges, changes); - - updateImportsInOtherFiles(changes, program, oldFile, usage.movedSymbols, newModuleName); - - return [ - ...getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, useEs6ModuleSyntax, quotePreference), - ...addExports(oldFile, toMove.all, usage.oldFileImportsFromNewFile, useEs6ModuleSyntax), - ]; - } - - function deleteMovedStatements(sourceFile: SourceFile, moved: readonly StatementRange[], changes: textChanges.ChangeTracker) { - for (const { first, afterLast } of moved) { - changes.deleteNodeRangeExcludingEnd(sourceFile, first, afterLast); - } + return toMove.all; + } + const useEs6ModuleSyntax = !!oldFile.externalModuleIndicator; + const quotePreference = getQuotePreference(oldFile, preferences); + const importsFromNewFile = createOldFileImportsFromNewFile(usage.oldFileImportsFromNewFile, newModuleName, useEs6ModuleSyntax, quotePreference); + if (importsFromNewFile) { + insertImport(changes, oldFile, importsFromNewFile, /*blankLineBetween*/ true); + } + deleteUnusedOldImports(oldFile, toMove.all, changes, usage.unusedImportsFromOldFile, checker); + deleteMovedStatements(oldFile, toMove.ranges, changes); + updateImportsInOtherFiles(changes, program, oldFile, usage.movedSymbols, newModuleName); + return [ + ...getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, useEs6ModuleSyntax, quotePreference), + ...addExports(oldFile, toMove.all, usage.oldFileImportsFromNewFile, useEs6ModuleSyntax), + ]; +} +/* @internal */ +function deleteMovedStatements(sourceFile: SourceFile, moved: readonly StatementRange[], changes: ChangeTracker) { + for (const { first, afterLast } of moved) { + changes.deleteNodeRangeExcludingEnd(sourceFile, first, afterLast); } - - function deleteUnusedOldImports(oldFile: SourceFile, toMove: readonly Statement[], changes: textChanges.ChangeTracker, toDelete: ReadonlySymbolSet, checker: TypeChecker) { - for (const statement of oldFile.statements) { - if (contains(toMove, statement)) continue; - forEachImportInStatement(statement, i => deleteUnusedImports(oldFile, i, changes, name => toDelete.has(checker.getSymbolAtLocation(name)!))); - } +} +/* @internal */ +function deleteUnusedOldImports(oldFile: SourceFile, toMove: readonly Statement[], changes: ChangeTracker, toDelete: ReadonlySymbolSet, checker: TypeChecker) { + for (const statement of oldFile.statements) { + if (contains(toMove, statement)) + continue; + forEachImportInStatement(statement, i => deleteUnusedImports(oldFile, i, changes, name => toDelete.has(checker.getSymbolAtLocation(name)!))); } - - function updateImportsInOtherFiles(changes: textChanges.ChangeTracker, program: Program, oldFile: SourceFile, movedSymbols: ReadonlySymbolSet, newModuleName: string): void { - const checker = program.getTypeChecker(); - for (const sourceFile of program.getSourceFiles()) { - if (sourceFile === oldFile) continue; - for (const statement of sourceFile.statements) { - forEachImportInStatement(statement, importNode => { - if (checker.getSymbolAtLocation(moduleSpecifierFromImport(importNode)) !== oldFile.symbol) return; - - const shouldMove = (name: Identifier): boolean => { - const symbol = isBindingElement(name.parent) - ? getPropertySymbolFromBindingElement(checker, name.parent as ObjectBindingElementWithoutPropertyName) - : skipAlias(checker.getSymbolAtLocation(name)!, checker); // TODO: GH#18217 - return !!symbol && movedSymbols.has(symbol); - }; - deleteUnusedImports(sourceFile, importNode, changes, shouldMove); // These will be changed to imports from the new file - const newModuleSpecifier = combinePaths(getDirectoryPath(moduleSpecifierFromImport(importNode).text), newModuleName); - const newImportDeclaration = filterImport(importNode, createLiteral(newModuleSpecifier), shouldMove); - if (newImportDeclaration) changes.insertNodeAfter(sourceFile, statement, newImportDeclaration); - - const ns = getNamespaceLikeImport(importNode); - if (ns) updateNamespaceLikeImport(changes, sourceFile, checker, movedSymbols, newModuleName, newModuleSpecifier, ns, importNode); - }); - } +} +/* @internal */ +function updateImportsInOtherFiles(changes: ChangeTracker, program: Program, oldFile: SourceFile, movedSymbols: ReadonlySymbolSet, newModuleName: string): void { + const checker = program.getTypeChecker(); + for (const sourceFile of program.getSourceFiles()) { + if (sourceFile === oldFile) + continue; + for (const statement of sourceFile.statements) { + forEachImportInStatement(statement, importNode => { + if (checker.getSymbolAtLocation(moduleSpecifierFromImport(importNode)) !== oldFile.symbol) + return; + const shouldMove = (name: Identifier): boolean => { + const symbol = isBindingElement(name.parent) + ? getPropertySymbolFromBindingElement(checker, (name.parent as ObjectBindingElementWithoutPropertyName)) + : skipAlias((checker.getSymbolAtLocation(name)!), checker); // TODO: GH#18217 + return !!symbol && movedSymbols.has(symbol); + }; + deleteUnusedImports(sourceFile, importNode, changes, shouldMove); // These will be changed to imports from the new file + const newModuleSpecifier = combinePaths(getDirectoryPath(moduleSpecifierFromImport(importNode).text), newModuleName); + const newImportDeclaration = filterImport(importNode, createLiteral(newModuleSpecifier), shouldMove); + if (newImportDeclaration) + changes.insertNodeAfter(sourceFile, statement, newImportDeclaration); + const ns = getNamespaceLikeImport(importNode); + if (ns) + updateNamespaceLikeImport(changes, sourceFile, checker, movedSymbols, newModuleName, newModuleSpecifier, ns, importNode); + }); } } - - function getNamespaceLikeImport(node: SupportedImport): Identifier | undefined { - switch (node.kind) { - case SyntaxKind.ImportDeclaration: - return node.importClause && node.importClause.namedBindings && node.importClause.namedBindings.kind === SyntaxKind.NamespaceImport ? - node.importClause.namedBindings.name : undefined; - case SyntaxKind.ImportEqualsDeclaration: - return node.name; - case SyntaxKind.VariableDeclaration: - return tryCast(node.name, isIdentifier); - default: - return Debug.assertNever(node, `Unexpected node kind ${(node as SupportedImport).kind}`); - } +} +/* @internal */ +function getNamespaceLikeImport(node: SupportedImport): Identifier | undefined { + switch (node.kind) { + case SyntaxKind.ImportDeclaration: + return node.importClause && node.importClause.namedBindings && node.importClause.namedBindings.kind === SyntaxKind.NamespaceImport ? + node.importClause.namedBindings.name : undefined; + case SyntaxKind.ImportEqualsDeclaration: + return node.name; + case SyntaxKind.VariableDeclaration: + return tryCast(node.name, isIdentifier); + default: + return Debug.assertNever(node, `Unexpected node kind ${(node as SupportedImport).kind}`); } - - function updateNamespaceLikeImport( - changes: textChanges.ChangeTracker, - sourceFile: SourceFile, - checker: TypeChecker, - movedSymbols: ReadonlySymbolSet, - newModuleName: string, - newModuleSpecifier: string, - oldImportId: Identifier, - oldImportNode: SupportedImport, - ): void { - const preferredNewNamespaceName = codefix.moduleSpecifierToValidIdentifier(newModuleName, ScriptTarget.ESNext); - let needUniqueName = false; - const toChange: Identifier[] = []; - FindAllReferences.Core.eachSymbolReferenceInFile(oldImportId, checker, sourceFile, ref => { - if (!isPropertyAccessExpression(ref.parent)) return; - needUniqueName = needUniqueName || !!checker.resolveName(preferredNewNamespaceName, ref, SymbolFlags.All, /*excludeGlobals*/ true); - if (movedSymbols.has(checker.getSymbolAtLocation(ref.parent.name)!)) { - toChange.push(ref); - } - }); - - if (toChange.length) { - const newNamespaceName = needUniqueName ? getUniqueName(preferredNewNamespaceName, sourceFile) : preferredNewNamespaceName; - for (const ref of toChange) { - changes.replaceNode(sourceFile, ref, createIdentifier(newNamespaceName)); - } - changes.insertNodeAfter(sourceFile, oldImportNode, updateNamespaceLikeImportNode(oldImportNode, newModuleName, newModuleSpecifier)); +} +/* @internal */ +function updateNamespaceLikeImport(changes: ChangeTracker, sourceFile: SourceFile, checker: TypeChecker, movedSymbols: ReadonlySymbolSet, newModuleName: string, newModuleSpecifier: string, oldImportId: Identifier, oldImportNode: SupportedImport): void { + const preferredNewNamespaceName = moduleSpecifierToValidIdentifier(newModuleName, ScriptTarget.ESNext); + let needUniqueName = false; + const toChange: Identifier[] = []; + Core.eachSymbolReferenceInFile(oldImportId, checker, sourceFile, ref => { + if (!isPropertyAccessExpression(ref.parent)) + return; + needUniqueName = needUniqueName || !!checker.resolveName(preferredNewNamespaceName, ref, SymbolFlags.All, /*excludeGlobals*/ true); + if (movedSymbols.has(checker.getSymbolAtLocation(ref.parent.name)!)) { + toChange.push(ref); } - } - - function updateNamespaceLikeImportNode(node: SupportedImport, newNamespaceName: string, newModuleSpecifier: string): Node { - const newNamespaceId = createIdentifier(newNamespaceName); - const newModuleString = createLiteral(newModuleSpecifier); - switch (node.kind) { - case SyntaxKind.ImportDeclaration: - return createImportDeclaration( - /*decorators*/ undefined, /*modifiers*/ undefined, - createImportClause(/*name*/ undefined, createNamespaceImport(newNamespaceId)), - newModuleString); - case SyntaxKind.ImportEqualsDeclaration: - return createImportEqualsDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, newNamespaceId, createExternalModuleReference(newModuleString)); - case SyntaxKind.VariableDeclaration: - return createVariableDeclaration(newNamespaceId, /*type*/ undefined, createRequireCall(newModuleString)); - default: - return Debug.assertNever(node, `Unexpected node kind ${(node as SupportedImport).kind}`); + }); + if (toChange.length) { + const newNamespaceName = needUniqueName ? getUniqueName(preferredNewNamespaceName, sourceFile) : preferredNewNamespaceName; + for (const ref of toChange) { + changes.replaceNode(sourceFile, ref, createIdentifier(newNamespaceName)); } + changes.insertNodeAfter(sourceFile, oldImportNode, updateNamespaceLikeImportNode(oldImportNode, newModuleName, newModuleSpecifier)); + } +} +/* @internal */ +function updateNamespaceLikeImportNode(node: SupportedImport, newNamespaceName: string, newModuleSpecifier: string): Node { + const newNamespaceId = createIdentifier(newNamespaceName); + const newModuleString = createLiteral(newModuleSpecifier); + switch (node.kind) { + case SyntaxKind.ImportDeclaration: + return createImportDeclaration( + /*decorators*/ undefined, /*modifiers*/ undefined, createImportClause(/*name*/ undefined, createNamespaceImport(newNamespaceId)), newModuleString); + case SyntaxKind.ImportEqualsDeclaration: + return createImportEqualsDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, newNamespaceId, createExternalModuleReference(newModuleString)); + case SyntaxKind.VariableDeclaration: + return createVariableDeclaration(newNamespaceId, /*type*/ undefined, createRequireCall(newModuleString)); + default: + return Debug.assertNever(node, `Unexpected node kind ${(node as SupportedImport).kind}`); } - - function moduleSpecifierFromImport(i: SupportedImport): StringLiteralLike { - return (i.kind === SyntaxKind.ImportDeclaration ? i.moduleSpecifier - : i.kind === SyntaxKind.ImportEqualsDeclaration ? i.moduleReference.expression +} +/* @internal */ +function moduleSpecifierFromImport(i: SupportedImport): StringLiteralLike { + return (i.kind === SyntaxKind.ImportDeclaration ? i.moduleSpecifier + : i.kind === SyntaxKind.ImportEqualsDeclaration ? i.moduleReference.expression : i.initializer.arguments[0]); +} +/* @internal */ +function forEachImportInStatement(statement: Statement, cb: (importNode: SupportedImport) => void): void { + if (isImportDeclaration(statement)) { + if (isStringLiteral(statement.moduleSpecifier)) + cb(statement as SupportedImport); } - - function forEachImportInStatement(statement: Statement, cb: (importNode: SupportedImport) => void): void { - if (isImportDeclaration(statement)) { - if (isStringLiteral(statement.moduleSpecifier)) cb(statement as SupportedImport); - } - else if (isImportEqualsDeclaration(statement)) { - if (isExternalModuleReference(statement.moduleReference) && isStringLiteralLike(statement.moduleReference.expression)) { - cb(statement as SupportedImport); - } + else if (isImportEqualsDeclaration(statement)) { + if (isExternalModuleReference(statement.moduleReference) && isStringLiteralLike(statement.moduleReference.expression)) { + cb(statement as SupportedImport); } - else if (isVariableStatement(statement)) { - for (const decl of statement.declarationList.declarations) { - if (decl.initializer && isRequireCall(decl.initializer, /*checkArgumentIsStringLiteralLike*/ true)) { - cb(decl as SupportedImport); - } + } + else if (isVariableStatement(statement)) { + for (const decl of statement.declarationList.declarations) { + if (decl.initializer && isRequireCall(decl.initializer, /*checkArgumentIsStringLiteralLike*/ true)) { + cb(decl as SupportedImport); } } } - - type SupportedImport = - | ImportDeclaration & { moduleSpecifier: StringLiteralLike } - | ImportEqualsDeclaration & { moduleReference: ExternalModuleReference & { expression: StringLiteralLike } } - | VariableDeclaration & { initializer: RequireOrImportCall }; - type SupportedImportStatement = - | ImportDeclaration - | ImportEqualsDeclaration - | VariableStatement; - - function createOldFileImportsFromNewFile(newFileNeedExport: ReadonlySymbolSet, newFileNameWithExtension: string, useEs6Imports: boolean, quotePreference: QuotePreference): Statement | undefined { - let defaultImport: Identifier | undefined; - const imports: string[] = []; - newFileNeedExport.forEach(symbol => { - if (symbol.escapedName === InternalSymbolName.Default) { - defaultImport = createIdentifier(symbolNameNoDefault(symbol)!); // TODO: GH#18217 - } - else { - imports.push(symbol.name); - } - }); - return makeImportOrRequire(defaultImport, imports, newFileNameWithExtension, useEs6Imports, quotePreference); - } - - function makeImportOrRequire(defaultImport: Identifier | undefined, imports: readonly string[], path: string, useEs6Imports: boolean, quotePreference: QuotePreference): Statement | undefined { - path = ensurePathIsNonModuleName(path); - if (useEs6Imports) { - const specifiers = imports.map(i => createImportSpecifier(/*propertyName*/ undefined, createIdentifier(i))); - return makeImportIfNecessary(defaultImport, specifiers, path, quotePreference); +} +/* @internal */ +type SupportedImport = (ImportDeclaration & { + moduleSpecifier: StringLiteralLike; +}) | (ImportEqualsDeclaration & { + moduleReference: ExternalModuleReference & { + expression: StringLiteralLike; + }; +}) | (VariableDeclaration & { + initializer: RequireOrImportCall; +}); +/* @internal */ +type SupportedImportStatement = ImportDeclaration | ImportEqualsDeclaration | VariableStatement; +/* @internal */ +function createOldFileImportsFromNewFile(newFileNeedExport: ReadonlySymbolSet, newFileNameWithExtension: string, useEs6Imports: boolean, quotePreference: QuotePreference): Statement | undefined { + let defaultImport: Identifier | undefined; + const imports: string[] = []; + newFileNeedExport.forEach(symbol => { + if (symbol.escapedName === InternalSymbolName.Default) { + defaultImport = createIdentifier((symbolNameNoDefault(symbol)!)); // TODO: GH#18217 } else { - Debug.assert(!defaultImport, "No default import should exist"); // If there's a default export, it should have been an es6 module. - const bindingElements = imports.map(i => createBindingElement(/*dotDotDotToken*/ undefined, /*propertyName*/ undefined, i)); - return bindingElements.length - ? makeVariableStatement(createObjectBindingPattern(bindingElements), /*type*/ undefined, createRequireCall(createLiteral(path))) - : undefined; + imports.push(symbol.name); } + }); + return makeImportOrRequire(defaultImport, imports, newFileNameWithExtension, useEs6Imports, quotePreference); +} +/* @internal */ +function makeImportOrRequire(defaultImport: Identifier | undefined, imports: readonly string[], path: string, useEs6Imports: boolean, quotePreference: QuotePreference): Statement | undefined { + path = ensurePathIsNonModuleName(path); + if (useEs6Imports) { + const specifiers = imports.map(i => createImportSpecifier(/*propertyName*/ undefined, createIdentifier(i))); + return makeImportIfNecessary(defaultImport, specifiers, path, quotePreference); + } + else { + Debug.assert(!defaultImport, "No default import should exist"); // If there's a default export, it should have been an es6 module. + const bindingElements = imports.map(i => createBindingElement(/*dotDotDotToken*/ undefined, /*propertyName*/ undefined, i)); + return bindingElements.length + ? makeVariableStatement(createObjectBindingPattern(bindingElements), /*type*/ undefined, createRequireCall(createLiteral(path))) + : undefined; } - - function makeVariableStatement(name: BindingName, type: TypeNode | undefined, initializer: Expression | undefined, flags: NodeFlags = NodeFlags.Const) { - return createVariableStatement(/*modifiers*/ undefined, createVariableDeclarationList([createVariableDeclaration(name, type, initializer)], flags)); - } - - function createRequireCall(moduleSpecifier: StringLiteralLike): CallExpression { - return createCall(createIdentifier("require"), /*typeArguments*/ undefined, [moduleSpecifier]); - } - - function addExports(sourceFile: SourceFile, toMove: readonly Statement[], needExport: ReadonlySymbolSet, useEs6Exports: boolean): readonly Statement[] { - return flatMap(toMove, statement => { - if (isTopLevelDeclarationStatement(statement) && - !isExported(sourceFile, statement, useEs6Exports) && - forEachTopLevelDeclaration(statement, d => needExport.has(Debug.checkDefined(d.symbol)))) { - const exports = addExport(statement, useEs6Exports); - if (exports) return exports; +} +/* @internal */ +function makeVariableStatement(name: BindingName, type: TypeNode | undefined, initializer: Expression | undefined, flags: NodeFlags = NodeFlags.Const) { + return createVariableStatement(/*modifiers*/ undefined, createVariableDeclarationList([createVariableDeclaration(name, type, initializer)], flags)); +} +/* @internal */ +function createRequireCall(moduleSpecifier: StringLiteralLike): CallExpression { + return createCall(createIdentifier("require"), /*typeArguments*/ undefined, [moduleSpecifier]); +} +/* @internal */ +function addExports(sourceFile: SourceFile, toMove: readonly Statement[], needExport: ReadonlySymbolSet, useEs6Exports: boolean): readonly Statement[] { + return flatMap(toMove, statement => { + if (isTopLevelDeclarationStatement(statement) && + !isExported(sourceFile, statement, useEs6Exports) && + forEachTopLevelDeclaration(statement, d => needExport.has(Debug.checkDefined(d.symbol)))) { + const exports = addExport(statement, useEs6Exports); + if (exports) + return exports; + } + return statement; + }); +} +/* @internal */ +function deleteUnusedImports(sourceFile: SourceFile, importDecl: SupportedImport, changes: ChangeTracker, isUnused: (name: Identifier) => boolean): void { + switch (importDecl.kind) { + case SyntaxKind.ImportDeclaration: + deleteUnusedImportsInDeclaration(sourceFile, importDecl, changes, isUnused); + break; + case SyntaxKind.ImportEqualsDeclaration: + if (isUnused(importDecl.name)) { + changes.delete(sourceFile, importDecl); } - return statement; - }); + break; + case SyntaxKind.VariableDeclaration: + deleteUnusedImportsInVariableDeclaration(sourceFile, importDecl, changes, isUnused); + break; + default: + Debug.assertNever(importDecl, `Unexpected import decl kind ${(importDecl as SupportedImport).kind}`); } - - function deleteUnusedImports(sourceFile: SourceFile, importDecl: SupportedImport, changes: textChanges.ChangeTracker, isUnused: (name: Identifier) => boolean): void { - switch (importDecl.kind) { - case SyntaxKind.ImportDeclaration: - deleteUnusedImportsInDeclaration(sourceFile, importDecl, changes, isUnused); - break; - case SyntaxKind.ImportEqualsDeclaration: - if (isUnused(importDecl.name)) { - changes.delete(sourceFile, importDecl); +} +/* @internal */ +function deleteUnusedImportsInDeclaration(sourceFile: SourceFile, importDecl: ImportDeclaration, changes: ChangeTracker, isUnused: (name: Identifier) => boolean): void { + if (!importDecl.importClause) + return; + const { name, namedBindings } = importDecl.importClause; + const defaultUnused = !name || isUnused(name); + const namedBindingsUnused = !namedBindings || + (namedBindings.kind === SyntaxKind.NamespaceImport ? isUnused(namedBindings.name) : namedBindings.elements.length !== 0 && namedBindings.elements.every(e => isUnused(e.name))); + if (defaultUnused && namedBindingsUnused) { + changes.delete(sourceFile, importDecl); + } + else { + if (name && defaultUnused) { + changes.delete(sourceFile, name); + } + if (namedBindings) { + if (namedBindingsUnused) { + changes.replaceNode(sourceFile, importDecl.importClause, updateImportClause(importDecl.importClause, name, /*namedBindings*/ undefined, importDecl.importClause.isTypeOnly)); + } + else if (namedBindings.kind === SyntaxKind.NamedImports) { + for (const element of namedBindings.elements) { + if (isUnused(element.name)) + changes.delete(sourceFile, element); } - break; - case SyntaxKind.VariableDeclaration: - deleteUnusedImportsInVariableDeclaration(sourceFile, importDecl, changes, isUnused); - break; - default: - Debug.assertNever(importDecl, `Unexpected import decl kind ${(importDecl as SupportedImport).kind}`); + } } } - function deleteUnusedImportsInDeclaration(sourceFile: SourceFile, importDecl: ImportDeclaration, changes: textChanges.ChangeTracker, isUnused: (name: Identifier) => boolean): void { - if (!importDecl.importClause) return; - const { name, namedBindings } = importDecl.importClause; - const defaultUnused = !name || isUnused(name); - const namedBindingsUnused = !namedBindings || - (namedBindings.kind === SyntaxKind.NamespaceImport ? isUnused(namedBindings.name) : namedBindings.elements.length !== 0 && namedBindings.elements.every(e => isUnused(e.name))); - if (defaultUnused && namedBindingsUnused) { - changes.delete(sourceFile, importDecl); - } - else { - if (name && defaultUnused) { +} +/* @internal */ +function deleteUnusedImportsInVariableDeclaration(sourceFile: SourceFile, varDecl: VariableDeclaration, changes: ChangeTracker, isUnused: (name: Identifier) => boolean) { + const { name } = varDecl; + switch (name.kind) { + case SyntaxKind.Identifier: + if (isUnused(name)) { changes.delete(sourceFile, name); } - if (namedBindings) { - if (namedBindingsUnused) { - changes.replaceNode( - sourceFile, - importDecl.importClause, - updateImportClause(importDecl.importClause, name, /*namedBindings*/ undefined, importDecl.importClause.isTypeOnly) - ); - } - else if (namedBindings.kind === SyntaxKind.NamedImports) { - for (const element of namedBindings.elements) { - if (isUnused(element.name)) changes.delete(sourceFile, element); + break; + case SyntaxKind.ArrayBindingPattern: + break; + case SyntaxKind.ObjectBindingPattern: + if (name.elements.every(e => isIdentifier(e.name) && isUnused(e.name))) { + changes.delete(sourceFile, isVariableDeclarationList(varDecl.parent) && varDecl.parent.declarations.length === 1 ? varDecl.parent.parent : varDecl); + } + else { + for (const element of name.elements) { + if (isIdentifier(element.name) && isUnused(element.name)) { + changes.delete(sourceFile, element.name); } } } - } + break; } - function deleteUnusedImportsInVariableDeclaration(sourceFile: SourceFile, varDecl: VariableDeclaration, changes: textChanges.ChangeTracker, isUnused: (name: Identifier) => boolean) { - const { name } = varDecl; - switch (name.kind) { - case SyntaxKind.Identifier: - if (isUnused(name)) { - changes.delete(sourceFile, name); - } - break; - case SyntaxKind.ArrayBindingPattern: - break; - case SyntaxKind.ObjectBindingPattern: - if (name.elements.every(e => isIdentifier(e.name) && isUnused(e.name))) { - changes.delete(sourceFile, - isVariableDeclarationList(varDecl.parent) && varDecl.parent.declarations.length === 1 ? varDecl.parent.parent : varDecl); - } - else { - for (const element of name.elements) { - if (isIdentifier(element.name) && isUnused(element.name)) { - changes.delete(sourceFile, element.name); - } - } - } - break; - } +} +/* @internal */ +function getNewFileImportsAndAddExportInOldFile(oldFile: SourceFile, importsToCopy: ReadonlySymbolSet, newFileImportsFromOldFile: ReadonlySymbolSet, changes: ChangeTracker, checker: TypeChecker, useEs6ModuleSyntax: boolean, quotePreference: QuotePreference): readonly SupportedImportStatement[] { + const copiedOldImports: SupportedImportStatement[] = []; + for (const oldStatement of oldFile.statements) { + forEachImportInStatement(oldStatement, i => { + append(copiedOldImports, filterImport(i, moduleSpecifierFromImport(i), name => importsToCopy.has(checker.getSymbolAtLocation(name)!))); + }); } - - function getNewFileImportsAndAddExportInOldFile( - oldFile: SourceFile, - importsToCopy: ReadonlySymbolSet, - newFileImportsFromOldFile: ReadonlySymbolSet, - changes: textChanges.ChangeTracker, - checker: TypeChecker, - useEs6ModuleSyntax: boolean, - quotePreference: QuotePreference, - ): readonly SupportedImportStatement[] { - const copiedOldImports: SupportedImportStatement[] = []; - for (const oldStatement of oldFile.statements) { - forEachImportInStatement(oldStatement, i => { - append(copiedOldImports, filterImport(i, moduleSpecifierFromImport(i), name => importsToCopy.has(checker.getSymbolAtLocation(name)!))); - }); + // Also, import things used from the old file, and insert 'export' modifiers as necessary in the old file. + let oldFileDefault: Identifier | undefined; + const oldFileNamedImports: string[] = []; + const markSeenTop = nodeSeenTracker(); // Needed because multiple declarations may appear in `const x = 0, y = 1;`. + newFileImportsFromOldFile.forEach(symbol => { + for (const decl of symbol.declarations) { + if (!isTopLevelDeclaration(decl)) + continue; + const name = nameOfTopLevelDeclaration(decl); + if (!name) + continue; + const top = getTopLevelDeclarationStatement(decl); + if (markSeenTop(top)) { + addExportToChanges(oldFile, top, changes, useEs6ModuleSyntax); + } + if (hasModifier(decl, ModifierFlags.Default)) { + oldFileDefault = name; + } + else { + oldFileNamedImports.push(name.text); + } } - - // Also, import things used from the old file, and insert 'export' modifiers as necessary in the old file. - let oldFileDefault: Identifier | undefined; - const oldFileNamedImports: string[] = []; - const markSeenTop = nodeSeenTracker(); // Needed because multiple declarations may appear in `const x = 0, y = 1;`. - newFileImportsFromOldFile.forEach(symbol => { + }); + append(copiedOldImports, makeImportOrRequire(oldFileDefault, oldFileNamedImports, removeFileExtension(getBaseFileName(oldFile.fileName)), useEs6ModuleSyntax, quotePreference)); + return copiedOldImports; +} +/* @internal */ +function makeUniqueModuleName(moduleName: string, extension: string, inDirectory: string, host: LanguageServiceHost): string { + let newModuleName = moduleName; + for (let i = 1;; i++) { + const name = combinePaths(inDirectory, newModuleName + extension); + if (!host.fileExists!(name)) + return newModuleName; // TODO: GH#18217 + newModuleName = `${moduleName}.${i}`; + } +} +/* @internal */ +function getNewModuleName(movedSymbols: ReadonlySymbolSet): string { + return movedSymbols.forEachEntry(symbolNameNoDefault) || "newFile"; +} +/* @internal */ +interface UsageInfo { + // Symbols whose declarations are moved from the old file to the new file. + readonly movedSymbols: ReadonlySymbolSet; + // Symbols declared in the old file that must be imported by the new file. (May not already be exported.) + readonly newFileImportsFromOldFile: ReadonlySymbolSet; + // Subset of movedSymbols that are still used elsewhere in the old file and must be imported back. + readonly oldFileImportsFromNewFile: ReadonlySymbolSet; + readonly oldImportsNeededByNewFile: ReadonlySymbolSet; + // Subset of oldImportsNeededByNewFile that are will no longer be used in the old file. + readonly unusedImportsFromOldFile: ReadonlySymbolSet; +} +/* @internal */ +function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], checker: TypeChecker): UsageInfo { + const movedSymbols = new SymbolSet(); + const oldImportsNeededByNewFile = new SymbolSet(); + const newFileImportsFromOldFile = new SymbolSet(); + const containsJsx = find(toMove, statement => !!(statement.transformFlags & TransformFlags.ContainsJsx)); + const jsxNamespaceSymbol = getJsxNamespaceSymbol(containsJsx); + if (jsxNamespaceSymbol) { // Might not exist (e.g. in non-compiling code) + oldImportsNeededByNewFile.add(jsxNamespaceSymbol); + } + for (const statement of toMove) { + forEachTopLevelDeclaration(statement, decl => { + movedSymbols.add(Debug.checkDefined(isExpressionStatement(decl) ? checker.getSymbolAtLocation(decl.expression.left) : decl.symbol, "Need a symbol here")); + }); + } + for (const statement of toMove) { + forEachReference(statement, checker, symbol => { + if (!symbol.declarations) + return; for (const decl of symbol.declarations) { - if (!isTopLevelDeclaration(decl)) continue; - const name = nameOfTopLevelDeclaration(decl); - if (!name) continue; - - const top = getTopLevelDeclarationStatement(decl); - if (markSeenTop(top)) { - addExportToChanges(oldFile, top, changes, useEs6ModuleSyntax); + if (isInImport(decl)) { + oldImportsNeededByNewFile.add(symbol); } - if (hasModifier(decl, ModifierFlags.Default)) { - oldFileDefault = name; - } - else { - oldFileNamedImports.push(name.text); + else if (isTopLevelDeclaration(decl) && sourceFileOfTopLevelDeclaration(decl) === oldFile && !movedSymbols.has(symbol)) { + newFileImportsFromOldFile.add(symbol); } } }); - - append(copiedOldImports, makeImportOrRequire(oldFileDefault, oldFileNamedImports, removeFileExtension(getBaseFileName(oldFile.fileName)), useEs6ModuleSyntax, quotePreference)); - return copiedOldImports; - } - - function makeUniqueModuleName(moduleName: string, extension: string, inDirectory: string, host: LanguageServiceHost): string { - let newModuleName = moduleName; - for (let i = 1; ; i++) { - const name = combinePaths(inDirectory, newModuleName + extension); - if (!host.fileExists!(name)) return newModuleName; // TODO: GH#18217 - newModuleName = `${moduleName}.${i}`; - } } - - function getNewModuleName(movedSymbols: ReadonlySymbolSet): string { - return movedSymbols.forEachEntry(symbolNameNoDefault) || "newFile"; - } - - interface UsageInfo { - // Symbols whose declarations are moved from the old file to the new file. - readonly movedSymbols: ReadonlySymbolSet; - - // Symbols declared in the old file that must be imported by the new file. (May not already be exported.) - readonly newFileImportsFromOldFile: ReadonlySymbolSet; - // Subset of movedSymbols that are still used elsewhere in the old file and must be imported back. - readonly oldFileImportsFromNewFile: ReadonlySymbolSet; - - readonly oldImportsNeededByNewFile: ReadonlySymbolSet; - // Subset of oldImportsNeededByNewFile that are will no longer be used in the old file. - readonly unusedImportsFromOldFile: ReadonlySymbolSet; - } - function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], checker: TypeChecker): UsageInfo { - const movedSymbols = new SymbolSet(); - const oldImportsNeededByNewFile = new SymbolSet(); - const newFileImportsFromOldFile = new SymbolSet(); - - const containsJsx = find(toMove, statement => !!(statement.transformFlags & TransformFlags.ContainsJsx)); - const jsxNamespaceSymbol = getJsxNamespaceSymbol(containsJsx); - if (jsxNamespaceSymbol) { // Might not exist (e.g. in non-compiling code) - oldImportsNeededByNewFile.add(jsxNamespaceSymbol); - } - - for (const statement of toMove) { - forEachTopLevelDeclaration(statement, decl => { - movedSymbols.add(Debug.checkDefined(isExpressionStatement(decl) ? checker.getSymbolAtLocation(decl.expression.left) : decl.symbol, "Need a symbol here")); - }); - } - for (const statement of toMove) { - forEachReference(statement, checker, symbol => { - if (!symbol.declarations) return; - for (const decl of symbol.declarations) { - if (isInImport(decl)) { - oldImportsNeededByNewFile.add(symbol); - } - else if (isTopLevelDeclaration(decl) && sourceFileOfTopLevelDeclaration(decl) === oldFile && !movedSymbols.has(symbol)) { - newFileImportsFromOldFile.add(symbol); - } - } - }); - } - - const unusedImportsFromOldFile = oldImportsNeededByNewFile.clone(); - - const oldFileImportsFromNewFile = new SymbolSet(); - for (const statement of oldFile.statements) { - if (contains(toMove, statement)) continue; - - // jsxNamespaceSymbol will only be set iff it is in oldImportsNeededByNewFile. - if (jsxNamespaceSymbol && !!(statement.transformFlags & TransformFlags.ContainsJsx)) { - unusedImportsFromOldFile.delete(jsxNamespaceSymbol); - } - - forEachReference(statement, checker, symbol => { - if (movedSymbols.has(symbol)) oldFileImportsFromNewFile.add(symbol); - unusedImportsFromOldFile.delete(symbol); - }); + const unusedImportsFromOldFile = oldImportsNeededByNewFile.clone(); + const oldFileImportsFromNewFile = new SymbolSet(); + for (const statement of oldFile.statements) { + if (contains(toMove, statement)) + continue; + // jsxNamespaceSymbol will only be set iff it is in oldImportsNeededByNewFile. + if (jsxNamespaceSymbol && !!(statement.transformFlags & TransformFlags.ContainsJsx)) { + unusedImportsFromOldFile.delete(jsxNamespaceSymbol); + } + forEachReference(statement, checker, symbol => { + if (movedSymbols.has(symbol)) + oldFileImportsFromNewFile.add(symbol); + unusedImportsFromOldFile.delete(symbol); + }); + } + return { movedSymbols, newFileImportsFromOldFile, oldFileImportsFromNewFile, oldImportsNeededByNewFile, unusedImportsFromOldFile }; + function getJsxNamespaceSymbol(containsJsx: Node | undefined) { + if (containsJsx === undefined) { + return undefined; } - - return { movedSymbols, newFileImportsFromOldFile, oldFileImportsFromNewFile, oldImportsNeededByNewFile, unusedImportsFromOldFile }; - - function getJsxNamespaceSymbol(containsJsx: Node | undefined) { - if (containsJsx === undefined) { + const jsxNamespace = checker.getJsxNamespace(containsJsx); + // Strictly speaking, this could resolve to a symbol other than the JSX namespace. + // This will produce erroneous output (probably, an incorrectly copied import) but + // is expected to be very rare and easily reversible. + const jsxNamespaceSymbol = checker.resolveName(jsxNamespace, containsJsx, SymbolFlags.Namespace, /*excludeGlobals*/ true); + return !!jsxNamespaceSymbol && some(jsxNamespaceSymbol.declarations, isInImport) + ? jsxNamespaceSymbol + : undefined; + } +} +// Below should all be utilities +/* @internal */ +function isInImport(decl: Declaration) { + switch (decl.kind) { + case SyntaxKind.ImportEqualsDeclaration: + case SyntaxKind.ImportSpecifier: + case SyntaxKind.ImportClause: + return true; + case SyntaxKind.VariableDeclaration: + return isVariableDeclarationInImport((decl as VariableDeclaration)); + case SyntaxKind.BindingElement: + return isVariableDeclaration(decl.parent.parent) && isVariableDeclarationInImport(decl.parent.parent); + default: + return false; + } +} +/* @internal */ +function isVariableDeclarationInImport(decl: VariableDeclaration) { + return isSourceFile(decl.parent.parent.parent) && + !!decl.initializer && isRequireCall(decl.initializer, /*checkArgumentIsStringLiteralLike*/ true); +} +/* @internal */ +function filterImport(i: SupportedImport, moduleSpecifier: StringLiteralLike, keep: (name: Identifier) => boolean): SupportedImportStatement | undefined { + switch (i.kind) { + case SyntaxKind.ImportDeclaration: { + const clause = i.importClause; + if (!clause) return undefined; - } - - const jsxNamespace = checker.getJsxNamespace(containsJsx); - - // Strictly speaking, this could resolve to a symbol other than the JSX namespace. - // This will produce erroneous output (probably, an incorrectly copied import) but - // is expected to be very rare and easily reversible. - const jsxNamespaceSymbol = checker.resolveName(jsxNamespace, containsJsx, SymbolFlags.Namespace, /*excludeGlobals*/ true); - - return !!jsxNamespaceSymbol && some(jsxNamespaceSymbol.declarations, isInImport) - ? jsxNamespaceSymbol + const defaultImport = clause.name && keep(clause.name) ? clause.name : undefined; + const namedBindings = clause.namedBindings && filterNamedBindings(clause.namedBindings, keep); + return defaultImport || namedBindings + ? createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, createImportClause(defaultImport, namedBindings), moduleSpecifier) : undefined; } - } - - // Below should all be utilities - - function isInImport(decl: Declaration) { - switch (decl.kind) { - case SyntaxKind.ImportEqualsDeclaration: - case SyntaxKind.ImportSpecifier: - case SyntaxKind.ImportClause: - return true; - case SyntaxKind.VariableDeclaration: - return isVariableDeclarationInImport(decl as VariableDeclaration); - case SyntaxKind.BindingElement: - return isVariableDeclaration(decl.parent.parent) && isVariableDeclarationInImport(decl.parent.parent); - default: - return false; + case SyntaxKind.ImportEqualsDeclaration: + return keep(i.name) ? i : undefined; + case SyntaxKind.VariableDeclaration: { + const name = filterBindingName(i.name, keep); + return name ? makeVariableStatement(name, i.type, createRequireCall(moduleSpecifier), i.parent.flags) : undefined; } + default: + return Debug.assertNever(i, `Unexpected import kind ${(i as SupportedImport).kind}`); } - function isVariableDeclarationInImport(decl: VariableDeclaration) { - return isSourceFile(decl.parent.parent.parent) && - !!decl.initializer && isRequireCall(decl.initializer, /*checkArgumentIsStringLiteralLike*/ true); - } - - function filterImport(i: SupportedImport, moduleSpecifier: StringLiteralLike, keep: (name: Identifier) => boolean): SupportedImportStatement | undefined { - switch (i.kind) { - case SyntaxKind.ImportDeclaration: { - const clause = i.importClause; - if (!clause) return undefined; - const defaultImport = clause.name && keep(clause.name) ? clause.name : undefined; - const namedBindings = clause.namedBindings && filterNamedBindings(clause.namedBindings, keep); - return defaultImport || namedBindings - ? createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, createImportClause(defaultImport, namedBindings), moduleSpecifier) - : undefined; - } - case SyntaxKind.ImportEqualsDeclaration: - return keep(i.name) ? i : undefined; - case SyntaxKind.VariableDeclaration: { - const name = filterBindingName(i.name, keep); - return name ? makeVariableStatement(name, i.type, createRequireCall(moduleSpecifier), i.parent.flags) : undefined; - } - default: - return Debug.assertNever(i, `Unexpected import kind ${(i as SupportedImport).kind}`); +} +/* @internal */ +function filterNamedBindings(namedBindings: NamedImportBindings, keep: (name: Identifier) => boolean): NamedImportBindings | undefined { + if (namedBindings.kind === SyntaxKind.NamespaceImport) { + return keep(namedBindings.name) ? namedBindings : undefined; + } + else { + const newElements = namedBindings.elements.filter(e => keep(e.name)); + return newElements.length ? createNamedImports(newElements) : undefined; + } +} +/* @internal */ +function filterBindingName(name: BindingName, keep: (name: Identifier) => boolean): BindingName | undefined { + switch (name.kind) { + case SyntaxKind.Identifier: + return keep(name) ? name : undefined; + case SyntaxKind.ArrayBindingPattern: + return name; + case SyntaxKind.ObjectBindingPattern: { + // We can't handle nested destructurings or property names well here, so just copy them all. + const newElements = name.elements.filter(prop => prop.propertyName || !isIdentifier(prop.name) || keep(prop.name)); + return newElements.length ? createObjectBindingPattern(newElements) : undefined; } } - function filterNamedBindings(namedBindings: NamedImportBindings, keep: (name: Identifier) => boolean): NamedImportBindings | undefined { - if (namedBindings.kind === SyntaxKind.NamespaceImport) { - return keep(namedBindings.name) ? namedBindings : undefined; +} +/* @internal */ +function forEachReference(node: Node, checker: TypeChecker, onReference: (s: Symbol) => void) { + node.forEachChild(function cb(node) { + if (isIdentifier(node) && !isDeclarationName(node)) { + const sym = checker.getSymbolAtLocation(node); + if (sym) + onReference(sym); } else { - const newElements = namedBindings.elements.filter(e => keep(e.name)); - return newElements.length ? createNamedImports(newElements) : undefined; + node.forEachChild(cb); } + }); +} +/* @internal */ +interface ReadonlySymbolSet { + has(symbol: Symbol): boolean; + forEach(cb: (symbol: Symbol) => void): void; + forEachEntry(cb: (symbol: Symbol) => T | undefined): T | undefined; +} +/* @internal */ +class SymbolSet implements ReadonlySymbolSet { + private map = createMap(); + add(symbol: Symbol): void { + this.map.set(String(getSymbolId(symbol)), symbol); } - function filterBindingName(name: BindingName, keep: (name: Identifier) => boolean): BindingName | undefined { - switch (name.kind) { - case SyntaxKind.Identifier: - return keep(name) ? name : undefined; - case SyntaxKind.ArrayBindingPattern: - return name; - case SyntaxKind.ObjectBindingPattern: { - // We can't handle nested destructurings or property names well here, so just copy them all. - const newElements = name.elements.filter(prop => prop.propertyName || !isIdentifier(prop.name) || keep(prop.name)); - return newElements.length ? createObjectBindingPattern(newElements) : undefined; - } - } + has(symbol: Symbol): boolean { + return this.map.has(String(getSymbolId(symbol))); } - - function forEachReference(node: Node, checker: TypeChecker, onReference: (s: Symbol) => void) { - node.forEachChild(function cb(node) { - if (isIdentifier(node) && !isDeclarationName(node)) { - const sym = checker.getSymbolAtLocation(node); - if (sym) onReference(sym); - } - else { - node.forEachChild(cb); - } - }); + delete(symbol: Symbol): void { + this.map.delete(String(getSymbolId(symbol))); } - - interface ReadonlySymbolSet { - has(symbol: Symbol): boolean; - forEach(cb: (symbol: Symbol) => void): void; - forEachEntry(cb: (symbol: Symbol) => T | undefined): T | undefined; + forEach(cb: (symbol: Symbol) => void): void { + this.map.forEach(cb); } - class SymbolSet implements ReadonlySymbolSet { - private map = createMap(); - add(symbol: Symbol): void { - this.map.set(String(getSymbolId(symbol)), symbol); - } - has(symbol: Symbol): boolean { - return this.map.has(String(getSymbolId(symbol))); - } - delete(symbol: Symbol): void { - this.map.delete(String(getSymbolId(symbol))); - } - forEach(cb: (symbol: Symbol) => void): void { - this.map.forEach(cb); - } - forEachEntry(cb: (symbol: Symbol) => T | undefined): T | undefined { - return forEachEntry(this.map, cb); - } - clone(): SymbolSet { - const clone = new SymbolSet(); - copyEntries(this.map, clone.map); - return clone; - } + forEachEntry(cb: (symbol: Symbol) => T | undefined): T | undefined { + return forEachEntry(this.map, cb); } - - type TopLevelExpressionStatement = ExpressionStatement & { expression: BinaryExpression & { left: PropertyAccessExpression } }; // 'exports.x = ...' - type NonVariableTopLevelDeclaration = - | FunctionDeclaration - | ClassDeclaration - | EnumDeclaration - | TypeAliasDeclaration - | InterfaceDeclaration - | ModuleDeclaration - | TopLevelExpressionStatement - | ImportEqualsDeclaration; - type TopLevelDeclarationStatement = NonVariableTopLevelDeclaration | VariableStatement; - interface TopLevelVariableDeclaration extends VariableDeclaration { parent: VariableDeclarationList & { parent: VariableStatement; }; } - type TopLevelDeclaration = NonVariableTopLevelDeclaration | TopLevelVariableDeclaration | BindingElement; - function isTopLevelDeclaration(node: Node): node is TopLevelDeclaration { - return isNonVariableTopLevelDeclaration(node) && isSourceFile(node.parent) || isVariableDeclaration(node) && isSourceFile(node.parent.parent.parent); - } - - function sourceFileOfTopLevelDeclaration(node: TopLevelDeclaration): Node { - return isVariableDeclaration(node) ? node.parent.parent.parent : node.parent; - } - - function isTopLevelDeclarationStatement(node: Node): node is TopLevelDeclarationStatement { - Debug.assert(isSourceFile(node.parent), "Node parent should be a SourceFile"); - return isNonVariableTopLevelDeclaration(node) || isVariableStatement(node); - } - - function isNonVariableTopLevelDeclaration(node: Node): node is NonVariableTopLevelDeclaration { - switch (node.kind) { - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ModuleDeclaration: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.ImportEqualsDeclaration: - return true; - default: - return false; - } + clone(): SymbolSet { + const clone = new SymbolSet(); + copyEntries(this.map, clone.map); + return clone; } - - function forEachTopLevelDeclaration(statement: Statement, cb: (node: TopLevelDeclaration) => T): T | undefined { - switch (statement.kind) { - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ModuleDeclaration: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.ImportEqualsDeclaration: - return cb(statement as FunctionDeclaration | ClassDeclaration | EnumDeclaration | ModuleDeclaration | TypeAliasDeclaration | InterfaceDeclaration | ImportEqualsDeclaration); - - case SyntaxKind.VariableStatement: - return firstDefined((statement as VariableStatement).declarationList.declarations, decl => forEachTopLevelDeclarationInBindingName(decl.name, cb)); - - case SyntaxKind.ExpressionStatement: { - const { expression } = statement as ExpressionStatement; - return isBinaryExpression(expression) && getAssignmentDeclarationKind(expression) === AssignmentDeclarationKind.ExportsProperty - ? cb(statement as TopLevelExpressionStatement) - : undefined; - } - } +} +/* @internal */ +type TopLevelExpressionStatement = ExpressionStatement & { + expression: BinaryExpression & { + left: PropertyAccessExpression; + }; +}; // 'exports.x = ...' +/* @internal */ +type NonVariableTopLevelDeclaration = FunctionDeclaration | ClassDeclaration | EnumDeclaration | TypeAliasDeclaration | InterfaceDeclaration | ModuleDeclaration | TopLevelExpressionStatement | ImportEqualsDeclaration; +/* @internal */ +type TopLevelDeclarationStatement = NonVariableTopLevelDeclaration | VariableStatement; +/* @internal */ +interface TopLevelVariableDeclaration extends VariableDeclaration { + parent: VariableDeclarationList & { + parent: VariableStatement; + }; +} +/* @internal */ +type TopLevelDeclaration = NonVariableTopLevelDeclaration | TopLevelVariableDeclaration | BindingElement; +/* @internal */ +function isTopLevelDeclaration(node: Node): node is TopLevelDeclaration { + return isNonVariableTopLevelDeclaration(node) && isSourceFile(node.parent) || isVariableDeclaration(node) && isSourceFile(node.parent.parent.parent); +} +/* @internal */ +function sourceFileOfTopLevelDeclaration(node: TopLevelDeclaration): Node { + return isVariableDeclaration(node) ? node.parent.parent.parent : node.parent; +} +/* @internal */ +function isTopLevelDeclarationStatement(node: Node): node is TopLevelDeclarationStatement { + Debug.assert(isSourceFile(node.parent), "Node parent should be a SourceFile"); + return isNonVariableTopLevelDeclaration(node) || isVariableStatement(node); +} +/* @internal */ +function isNonVariableTopLevelDeclaration(node: Node): node is NonVariableTopLevelDeclaration { + switch (node.kind) { + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.ImportEqualsDeclaration: + return true; + default: + return false; } - function forEachTopLevelDeclarationInBindingName(name: BindingName, cb: (node: TopLevelDeclaration) => T): T | undefined { - switch (name.kind) { - case SyntaxKind.Identifier: - return cb(cast(name.parent, (x): x is TopLevelVariableDeclaration | BindingElement => isVariableDeclaration(x) || isBindingElement(x))); - case SyntaxKind.ArrayBindingPattern: - case SyntaxKind.ObjectBindingPattern: - return firstDefined(name.elements, em => isOmittedExpression(em) ? undefined : forEachTopLevelDeclarationInBindingName(em.name, cb)); - default: - return Debug.assertNever(name, `Unexpected name kind ${(name as BindingName).kind}`); +} +/* @internal */ +function forEachTopLevelDeclaration(statement: Statement, cb: (node: TopLevelDeclaration) => T): T | undefined { + switch (statement.kind) { + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.ImportEqualsDeclaration: + return cb((statement as FunctionDeclaration | ClassDeclaration | EnumDeclaration | ModuleDeclaration | TypeAliasDeclaration | InterfaceDeclaration | ImportEqualsDeclaration)); + case SyntaxKind.VariableStatement: + return firstDefined((statement as VariableStatement).declarationList.declarations, decl => forEachTopLevelDeclarationInBindingName(decl.name, cb)); + case SyntaxKind.ExpressionStatement: { + const { expression } = (statement as ExpressionStatement); + return isBinaryExpression(expression) && getAssignmentDeclarationKind(expression) === AssignmentDeclarationKind.ExportsProperty + ? cb(statement as TopLevelExpressionStatement) + : undefined; } } - - function nameOfTopLevelDeclaration(d: TopLevelDeclaration): Identifier | undefined { - return isExpressionStatement(d) ? tryCast(d.expression.left.name, isIdentifier) : tryCast(d.name, isIdentifier); - } - - function getTopLevelDeclarationStatement(d: TopLevelDeclaration): TopLevelDeclarationStatement { - switch (d.kind) { - case SyntaxKind.VariableDeclaration: - return d.parent.parent; - case SyntaxKind.BindingElement: - return getTopLevelDeclarationStatement( - cast(d.parent.parent, (p): p is TopLevelVariableDeclaration | BindingElement => isVariableDeclaration(p) || isBindingElement(p))); - default: - return d; - } +} +/* @internal */ +function forEachTopLevelDeclarationInBindingName(name: BindingName, cb: (node: TopLevelDeclaration) => T): T | undefined { + switch (name.kind) { + case SyntaxKind.Identifier: + return cb(cast(name.parent, (x): x is TopLevelVariableDeclaration | BindingElement => isVariableDeclaration(x) || isBindingElement(x))); + case SyntaxKind.ArrayBindingPattern: + case SyntaxKind.ObjectBindingPattern: + return firstDefined(name.elements, em => isOmittedExpression(em) ? undefined : forEachTopLevelDeclarationInBindingName(em.name, cb)); + default: + return Debug.assertNever(name, `Unexpected name kind ${(name as BindingName).kind}`); } - - function addExportToChanges(sourceFile: SourceFile, decl: TopLevelDeclarationStatement, changes: textChanges.ChangeTracker, useEs6Exports: boolean): void { - if (isExported(sourceFile, decl, useEs6Exports)) return; - if (useEs6Exports) { - if (!isExpressionStatement(decl)) changes.insertExportModifier(sourceFile, decl); - } - else { - const names = getNamesToExportInCommonJS(decl); - if (names.length !== 0) changes.insertNodesAfter(sourceFile, decl, names.map(createExportAssignment)); - } +} +/* @internal */ +function nameOfTopLevelDeclaration(d: TopLevelDeclaration): Identifier | undefined { + return isExpressionStatement(d) ? tryCast(d.expression.left.name, isIdentifier) : tryCast(d.name, isIdentifier); +} +/* @internal */ +function getTopLevelDeclarationStatement(d: TopLevelDeclaration): TopLevelDeclarationStatement { + switch (d.kind) { + case SyntaxKind.VariableDeclaration: + return d.parent.parent; + case SyntaxKind.BindingElement: + return getTopLevelDeclarationStatement(cast(d.parent.parent, (p): p is TopLevelVariableDeclaration | BindingElement => isVariableDeclaration(p) || isBindingElement(p))); + default: + return d; } - - function isExported(sourceFile: SourceFile, decl: TopLevelDeclarationStatement, useEs6Exports: boolean): boolean { - if (useEs6Exports) { - return !isExpressionStatement(decl) && hasModifier(decl, ModifierFlags.Export); - } - else { - return getNamesToExportInCommonJS(decl).some(name => sourceFile.symbol.exports!.has(escapeLeadingUnderscores(name))); - } +} +/* @internal */ +function addExportToChanges(sourceFile: SourceFile, decl: TopLevelDeclarationStatement, changes: ChangeTracker, useEs6Exports: boolean): void { + if (isExported(sourceFile, decl, useEs6Exports)) + return; + if (useEs6Exports) { + if (!isExpressionStatement(decl)) + changes.insertExportModifier(sourceFile, decl); + } + else { + const names = getNamesToExportInCommonJS(decl); + if (names.length !== 0) + changes.insertNodesAfter(sourceFile, decl, names.map(createExportAssignment)); } - - function addExport(decl: TopLevelDeclarationStatement, useEs6Exports: boolean): readonly Statement[] | undefined { - return useEs6Exports ? [addEs6Export(decl)] : addCommonjsExport(decl); - } - function addEs6Export(d: TopLevelDeclarationStatement): TopLevelDeclarationStatement { - const modifiers = concatenate([createModifier(SyntaxKind.ExportKeyword)], d.modifiers); - switch (d.kind) { - case SyntaxKind.FunctionDeclaration: - return updateFunctionDeclaration(d, d.decorators, modifiers, d.asteriskToken, d.name, d.typeParameters, d.parameters, d.type, d.body); - case SyntaxKind.ClassDeclaration: - return updateClassDeclaration(d, d.decorators, modifiers, d.name, d.typeParameters, d.heritageClauses, d.members); - case SyntaxKind.VariableStatement: - return updateVariableStatement(d, modifiers, d.declarationList); - case SyntaxKind.ModuleDeclaration: - return updateModuleDeclaration(d, d.decorators, modifiers, d.name, d.body); - case SyntaxKind.EnumDeclaration: - return updateEnumDeclaration(d, d.decorators, modifiers, d.name, d.members); - case SyntaxKind.TypeAliasDeclaration: - return updateTypeAliasDeclaration(d, d.decorators, modifiers, d.name, d.typeParameters, d.type); - case SyntaxKind.InterfaceDeclaration: - return updateInterfaceDeclaration(d, d.decorators, modifiers, d.name, d.typeParameters, d.heritageClauses, d.members); - case SyntaxKind.ImportEqualsDeclaration: - return updateImportEqualsDeclaration(d, d.decorators, modifiers, d.name, d.moduleReference); - case SyntaxKind.ExpressionStatement: - return Debug.fail(); // Shouldn't try to add 'export' keyword to `exports.x = ...` - default: - return Debug.assertNever(d, `Unexpected declaration kind ${(d as DeclarationStatement).kind}`); - } +} +/* @internal */ +function isExported(sourceFile: SourceFile, decl: TopLevelDeclarationStatement, useEs6Exports: boolean): boolean { + if (useEs6Exports) { + return !isExpressionStatement(decl) && hasModifier(decl, ModifierFlags.Export); } - function addCommonjsExport(decl: TopLevelDeclarationStatement): readonly Statement[] | undefined { - return [decl, ...getNamesToExportInCommonJS(decl).map(createExportAssignment)]; - } - function getNamesToExportInCommonJS(decl: TopLevelDeclarationStatement): readonly string[] { - switch (decl.kind) { - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.ClassDeclaration: - return [decl.name!.text]; // TODO: GH#18217 - case SyntaxKind.VariableStatement: - return mapDefined(decl.declarationList.declarations, d => isIdentifier(d.name) ? d.name.text : undefined); - case SyntaxKind.ModuleDeclaration: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.ImportEqualsDeclaration: - return emptyArray; - case SyntaxKind.ExpressionStatement: - return Debug.fail("Can't export an ExpressionStatement"); // Shouldn't try to add 'export' keyword to `exports.x = ...` - default: - return Debug.assertNever(decl, `Unexpected decl kind ${(decl as TopLevelDeclarationStatement).kind}`); - } + else { + return getNamesToExportInCommonJS(decl).some(name => sourceFile.symbol.exports!.has(escapeLeadingUnderscores(name))); } - - /** Creates `exports.x = x;` */ - function createExportAssignment(name: string): Statement { - return createExpressionStatement( - createBinary( - createPropertyAccess(createIdentifier("exports"), createIdentifier(name)), - SyntaxKind.EqualsToken, - createIdentifier(name))); +} +/* @internal */ +function addExport(decl: TopLevelDeclarationStatement, useEs6Exports: boolean): readonly Statement[] | undefined { + return useEs6Exports ? [addEs6Export(decl)] : addCommonjsExport(decl); +} +/* @internal */ +function addEs6Export(d: TopLevelDeclarationStatement): TopLevelDeclarationStatement { + const modifiers = concatenate([createModifier(SyntaxKind.ExportKeyword)], d.modifiers); + switch (d.kind) { + case SyntaxKind.FunctionDeclaration: + return updateFunctionDeclaration(d, d.decorators, modifiers, d.asteriskToken, d.name, d.typeParameters, d.parameters, d.type, d.body); + case SyntaxKind.ClassDeclaration: + return updateClassDeclaration(d, d.decorators, modifiers, d.name, d.typeParameters, d.heritageClauses, d.members); + case SyntaxKind.VariableStatement: + return updateVariableStatement(d, modifiers, d.declarationList); + case SyntaxKind.ModuleDeclaration: + return updateModuleDeclaration(d, d.decorators, modifiers, d.name, d.body); + case SyntaxKind.EnumDeclaration: + return updateEnumDeclaration(d, d.decorators, modifiers, d.name, d.members); + case SyntaxKind.TypeAliasDeclaration: + return updateTypeAliasDeclaration(d, d.decorators, modifiers, d.name, d.typeParameters, d.type); + case SyntaxKind.InterfaceDeclaration: + return updateInterfaceDeclaration(d, d.decorators, modifiers, d.name, d.typeParameters, d.heritageClauses, d.members); + case SyntaxKind.ImportEqualsDeclaration: + return updateImportEqualsDeclaration(d, d.decorators, modifiers, d.name, d.moduleReference); + case SyntaxKind.ExpressionStatement: + return Debug.fail(); // Shouldn't try to add 'export' keyword to `exports.x = ...` + default: + return Debug.assertNever(d, `Unexpected declaration kind ${(d as DeclarationStatement).kind}`); } } +/* @internal */ +function addCommonjsExport(decl: TopLevelDeclarationStatement): readonly Statement[] | undefined { + return [decl, ...getNamesToExportInCommonJS(decl).map(createExportAssignment)]; +} +/* @internal */ +function getNamesToExportInCommonJS(decl: TopLevelDeclarationStatement): readonly string[] { + switch (decl.kind) { + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.ClassDeclaration: + return [decl.name!.text]; // TODO: GH#18217 + case SyntaxKind.VariableStatement: + return mapDefined(decl.declarationList.declarations, d => isIdentifier(d.name) ? d.name.text : undefined); + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.ImportEqualsDeclaration: + return emptyArray; + case SyntaxKind.ExpressionStatement: + return Debug.fail("Can't export an ExpressionStatement"); // Shouldn't try to add 'export' keyword to `exports.x = ...` + default: + return Debug.assertNever(decl, `Unexpected decl kind ${(decl as TopLevelDeclarationStatement).kind}`); + } +} +/** Creates `exports.x = x;` */ +/* @internal */ +function createExportAssignment(name: string): Statement { + return createExpressionStatement(createBinary(createPropertyAccess(createIdentifier("exports"), createIdentifier(name)), SyntaxKind.EqualsToken, createIdentifier(name))); +} diff --git a/src/services/rename.ts b/src/services/rename.ts index 037c7f8eb236f..d1226a49943ab 100644 --- a/src/services/rename.ts +++ b/src/services/rename.ts @@ -1,109 +1,107 @@ +import { Program, SourceFile, RenameInfoOptions, RenameInfo, getAdjustedRenameLocation, getTouchingPropertyName, Diagnostics, Node, TypeChecker, isIdentifier, SyntaxKind, SymbolFlags, isStringLiteralLike, tryGetImportFromModuleSpecifier, isImportOrExportSpecifierName, isStringOrNumericLiteralLike, stripQuotes, getTextOfIdentifierOrLiteral, StringLiteralLike, Symbol, isExternalModuleNameRelative, find, isSourceFile, endsWith, tryRemoveSuffix, removeFileExtension, ScriptElementKind, createTextSpan, ScriptElementKindModifier, RenameInfoSuccess, DiagnosticMessage, RenameInfoFailure, getLocaleSpecificMessage, isLiteralNameOfPropertyDeclarationOrIndexAccess, NumericLiteral } from "./ts"; +import { getSymbolKind, getSymbolModifiers } from "./ts.SymbolDisplay"; /* @internal */ -namespace ts.Rename { - export function getRenameInfo(program: Program, sourceFile: SourceFile, position: number, options?: RenameInfoOptions): RenameInfo { - const node = getAdjustedRenameLocation(getTouchingPropertyName(sourceFile, position)); - if (nodeIsEligibleForRename(node)) { - const renameInfo = getRenameInfoForNode(node, program.getTypeChecker(), sourceFile, declaration => program.isSourceFileDefaultLibrary(declaration.getSourceFile()), options); - if (renameInfo) { - return renameInfo; - } +export function getRenameInfo(program: Program, sourceFile: SourceFile, position: number, options?: RenameInfoOptions): RenameInfo { + const node = getAdjustedRenameLocation(getTouchingPropertyName(sourceFile, position)); + if (nodeIsEligibleForRename(node)) { + const renameInfo = getRenameInfoForNode(node, program.getTypeChecker(), sourceFile, declaration => program.isSourceFileDefaultLibrary(declaration.getSourceFile()), options); + if (renameInfo) { + return renameInfo; } - return getRenameInfoError(Diagnostics.You_cannot_rename_this_element); } - - function getRenameInfoForNode(node: Node, typeChecker: TypeChecker, sourceFile: SourceFile, isDefinedInLibraryFile: (declaration: Node) => boolean, options?: RenameInfoOptions): RenameInfo | undefined { - const symbol = typeChecker.getSymbolAtLocation(node); - if (!symbol) return; - // Only allow a symbol to be renamed if it actually has at least one declaration. - const { declarations } = symbol; - if (!declarations || declarations.length === 0) return; - - // Disallow rename for elements that are defined in the standard TypeScript library. - if (declarations.some(isDefinedInLibraryFile)) { - return getRenameInfoError(Diagnostics.You_cannot_rename_elements_that_are_defined_in_the_standard_TypeScript_library); - } - - // Cannot rename `default` as in `import { default as foo } from "./someModule"; - if (isIdentifier(node) && node.originalKeywordKind === SyntaxKind.DefaultKeyword && symbol.parent!.flags & SymbolFlags.Module) { - return undefined; - } - - if (isStringLiteralLike(node) && tryGetImportFromModuleSpecifier(node)) { - return options && options.allowRenameOfImportPath ? getRenameInfoForModule(node, sourceFile, symbol) : undefined; - } - - const kind = SymbolDisplay.getSymbolKind(typeChecker, symbol, node); - const specifierName = (isImportOrExportSpecifierName(node) || isStringOrNumericLiteralLike(node) && node.parent.kind === SyntaxKind.ComputedPropertyName) - ? stripQuotes(getTextOfIdentifierOrLiteral(node)) - : undefined; - const displayName = specifierName || typeChecker.symbolToString(symbol); - const fullDisplayName = specifierName || typeChecker.getFullyQualifiedName(symbol); - return getRenameInfoSuccess(displayName, fullDisplayName, kind, SymbolDisplay.getSymbolModifiers(symbol), node, sourceFile); + return getRenameInfoError(Diagnostics.You_cannot_rename_this_element); +} +/* @internal */ +function getRenameInfoForNode(node: Node, typeChecker: TypeChecker, sourceFile: SourceFile, isDefinedInLibraryFile: (declaration: Node) => boolean, options?: RenameInfoOptions): RenameInfo | undefined { + const symbol = typeChecker.getSymbolAtLocation(node); + if (!symbol) + return; + // Only allow a symbol to be renamed if it actually has at least one declaration. + const { declarations } = symbol; + if (!declarations || declarations.length === 0) + return; + // Disallow rename for elements that are defined in the standard TypeScript library. + if (declarations.some(isDefinedInLibraryFile)) { + return getRenameInfoError(Diagnostics.You_cannot_rename_elements_that_are_defined_in_the_standard_TypeScript_library); } - - function getRenameInfoForModule(node: StringLiteralLike, sourceFile: SourceFile, moduleSymbol: Symbol): RenameInfo | undefined { - if (!isExternalModuleNameRelative(node.text)) { - return getRenameInfoError(Diagnostics.You_cannot_rename_a_module_via_a_global_import); - } - - const moduleSourceFile = find(moduleSymbol.declarations, isSourceFile); - if (!moduleSourceFile) return undefined; - const withoutIndex = endsWith(node.text, "/index") || endsWith(node.text, "/index.js") ? undefined : tryRemoveSuffix(removeFileExtension(moduleSourceFile.fileName), "/index"); - const name = withoutIndex === undefined ? moduleSourceFile.fileName : withoutIndex; - const kind = withoutIndex === undefined ? ScriptElementKind.moduleElement : ScriptElementKind.directory; - const indexAfterLastSlash = node.text.lastIndexOf("/") + 1; - // Span should only be the last component of the path. + 1 to account for the quote character. - const triggerSpan = createTextSpan(node.getStart(sourceFile) + 1 + indexAfterLastSlash, node.text.length - indexAfterLastSlash); - return { - canRename: true, - fileToRename: name, - kind, - displayName: name, - fullDisplayName: name, - kindModifiers: ScriptElementKindModifier.none, - triggerSpan, - }; + // Cannot rename `default` as in `import { default as foo } from "./someModule"; + if (isIdentifier(node) && node.originalKeywordKind === SyntaxKind.DefaultKeyword && symbol.parent!.flags & SymbolFlags.Module) { + return undefined; } - - function getRenameInfoSuccess(displayName: string, fullDisplayName: string, kind: ScriptElementKind, kindModifiers: string, node: Node, sourceFile: SourceFile): RenameInfoSuccess { - return { - canRename: true, - fileToRename: undefined, - kind, - displayName, - fullDisplayName, - kindModifiers, - triggerSpan: createTriggerSpanForNode(node, sourceFile) - }; + if (isStringLiteralLike(node) && tryGetImportFromModuleSpecifier(node)) { + return options && options.allowRenameOfImportPath ? getRenameInfoForModule(node, sourceFile, symbol) : undefined; } - - function getRenameInfoError(diagnostic: DiagnosticMessage): RenameInfoFailure { - return { canRename: false, localizedErrorMessage: getLocaleSpecificMessage(diagnostic) }; + const kind = getSymbolKind(typeChecker, symbol, node); + const specifierName = (isImportOrExportSpecifierName(node) || isStringOrNumericLiteralLike(node) && node.parent.kind === SyntaxKind.ComputedPropertyName) + ? stripQuotes(getTextOfIdentifierOrLiteral(node)) + : undefined; + const displayName = specifierName || typeChecker.symbolToString(symbol); + const fullDisplayName = specifierName || typeChecker.getFullyQualifiedName(symbol); + return getRenameInfoSuccess(displayName, fullDisplayName, kind, getSymbolModifiers(symbol), node, sourceFile); +} +/* @internal */ +function getRenameInfoForModule(node: StringLiteralLike, sourceFile: SourceFile, moduleSymbol: Symbol): RenameInfo | undefined { + if (!isExternalModuleNameRelative(node.text)) { + return getRenameInfoError(Diagnostics.You_cannot_rename_a_module_via_a_global_import); } - - function createTriggerSpanForNode(node: Node, sourceFile: SourceFile) { - let start = node.getStart(sourceFile); - let width = node.getWidth(sourceFile); - if (isStringLiteralLike(node)) { - // Exclude the quotes - start += 1; - width -= 2; - } - return createTextSpan(start, width); + const moduleSourceFile = find(moduleSymbol.declarations, isSourceFile); + if (!moduleSourceFile) + return undefined; + const withoutIndex = endsWith(node.text, "/index") || endsWith(node.text, "/index.js") ? undefined : tryRemoveSuffix(removeFileExtension(moduleSourceFile.fileName), "/index"); + const name = withoutIndex === undefined ? moduleSourceFile.fileName : withoutIndex; + const kind = withoutIndex === undefined ? ScriptElementKind.moduleElement : ScriptElementKind.directory; + const indexAfterLastSlash = node.text.lastIndexOf("/") + 1; + // Span should only be the last component of the path. + 1 to account for the quote character. + const triggerSpan = createTextSpan(node.getStart(sourceFile) + 1 + indexAfterLastSlash, node.text.length - indexAfterLastSlash); + return { + canRename: true, + fileToRename: name, + kind, + displayName: name, + fullDisplayName: name, + kindModifiers: ScriptElementKindModifier.none, + triggerSpan, + }; +} +/* @internal */ +function getRenameInfoSuccess(displayName: string, fullDisplayName: string, kind: ScriptElementKind, kindModifiers: string, node: Node, sourceFile: SourceFile): RenameInfoSuccess { + return { + canRename: true, + fileToRename: undefined, + kind, + displayName, + fullDisplayName, + kindModifiers, + triggerSpan: createTriggerSpanForNode(node, sourceFile) + }; +} +/* @internal */ +function getRenameInfoError(diagnostic: DiagnosticMessage): RenameInfoFailure { + return { canRename: false, localizedErrorMessage: getLocaleSpecificMessage(diagnostic) }; +} +/* @internal */ +function createTriggerSpanForNode(node: Node, sourceFile: SourceFile) { + let start = node.getStart(sourceFile); + let width = node.getWidth(sourceFile); + if (isStringLiteralLike(node)) { + // Exclude the quotes + start += 1; + width -= 2; } - - function nodeIsEligibleForRename(node: Node): boolean { - switch (node.kind) { - case SyntaxKind.Identifier: - case SyntaxKind.PrivateIdentifier: - case SyntaxKind.StringLiteral: - case SyntaxKind.NoSubstitutionTemplateLiteral: - case SyntaxKind.ThisKeyword: - return true; - case SyntaxKind.NumericLiteral: - return isLiteralNameOfPropertyDeclarationOrIndexAccess(node as NumericLiteral); - default: - return false; - } + return createTextSpan(start, width); +} +/* @internal */ +function nodeIsEligibleForRename(node: Node): boolean { + switch (node.kind) { + case SyntaxKind.Identifier: + case SyntaxKind.PrivateIdentifier: + case SyntaxKind.StringLiteral: + case SyntaxKind.NoSubstitutionTemplateLiteral: + case SyntaxKind.ThisKeyword: + return true; + case SyntaxKind.NumericLiteral: + return isLiteralNameOfPropertyDeclarationOrIndexAccess((node as NumericLiteral)); + default: + return false; } } diff --git a/src/services/services.ts b/src/services/services.ts index f6e2698045dd0..a3d09231a92a2 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1,2364 +1,2014 @@ -namespace ts { - /** The version of the language service API */ - export const servicesVersion = "0.8"; - - function createNode(kind: TKind, pos: number, end: number, parent: Node): NodeObject | TokenObject | IdentifierObject | PrivateIdentifierObject { - const node = isNodeKind(kind) ? new NodeObject(kind, pos, end) : - kind === SyntaxKind.Identifier ? new IdentifierObject(SyntaxKind.Identifier, pos, end) : +import { SyntaxKind, Node, isNodeKind, NodeFlags, ModifierFlags, TransformFlags, Symbol, JSDoc, Debug, positionIsSynthesized, SourceFile, getSourceFileOfNode, SourceFileLike, getTokenPosOfNode, find, lastOrUndefined, NodeArray, forEachChild, emptyArray, isJSDocCommentContainingNode, scanner, forEach, JSDocContainer, Push, SyntaxList, EndOfFileToken, SymbolFlags, __String, Declaration, SymbolDisplayPart, JSDocTagInfo, symbolName, TypeChecker, Token, Identifier, GeneratedIdentifierFlags, TypeNode, idText, PrivateIdentifier, Type, TypeFlags, ObjectFlags, Signature, SignatureKind, IndexKind, BaseType, UnionType, IntersectionType, UnionOrIntersectionType, LiteralType, StringLiteralType, NumberLiteralType, TypeParameter, InterfaceType, getObjectFlags, TypeReference, SignatureFlags, SignatureDeclaration, TypePredicate, singleElementArray, getJSDocTags, forEachUnique, lineBreakPart, firstDefined, getAllSuperTypeNodes, Path, IScriptSnapshot, Statement, FileReference, DiagnosticWithLocation, ScriptKind, ScriptTarget, LanguageVariant, UnderscoreEscapedMap, ResolvedModuleFull, ResolvedTypeReferenceDirective, StringLiteralLike, StringLiteral, CheckJsDirective, PragmaMap, EntityName, TextChangeRange, updateSourceFile, LineAndCharacter, getLineAndCharacterOfPosition, getLineStarts, computePositionOfLineAndCharacter, createMultiMap, getNonAssignedNameOfDeclaration, isComputedPropertyName, isPropertyAccessExpression, isPropertyName, getNameFromPropertyName, FunctionLikeDeclaration, hasModifier, VariableDeclaration, isBindingPattern, ExportDeclaration, isNamedExports, ImportDeclaration, getAssignmentDeclarationKind, BinaryExpression, AssignmentDeclarationKind, SourceMapSource, ObjectAllocator, EmitTextWriter, FormatCodeOptions, FormatCodeSettings, EditorOptions, EditorSettings, MapLike, hasProperty, map, CompilerOptions, JsxEmit, LanguageServiceHost, GetCanonicalFileName, createMap, toPath, ProjectReference, getScriptKind, isString, createSourceFile, getSnapshotText, textSpanEnd, CancellationToken, HostCancellationToken, OperationCanceledException, timestamp, DocumentRegistry, createDocumentRegistry, LanguageService, Program, localizedDiagnosticMessages, setLocalizedDiagnosticMessages, hostUsesCaseSensitiveFileNames, createGetCanonicalFileName, getSourceMapper, maybeBind, PossibleProgramFileInfo, HasInvalidatedResolution, returnFalse, isProgramUptoDate, CompilerHost, getNewLineCharacter, getNewLineOrDefaultFromHost, noop, directoryProbablyExists, CreateProgramOptions, createProgram, Diagnostic, getEmitDeclarations, computeSuggestionDiagnostics, GetCompletionsAtPositionOptions, emptyOptions, CompletionInfo, UserPreferences, identity, Completions, CompletionEntryDetails, QuickInfo, getTouchingPropertyName, ScriptElementKind, ScriptElementKindModifier, createTextSpanFromNode, typeToDisplayParts, getContainerNode, isNewExpression, isLabelName, isTagName, isInComment, DefinitionInfo, GoToDefinition, DefinitionInfoAndBoundSpan, ImplementationLocation, ReferenceEntry, flatMap, HighlightSpanKind, DocumentHighlights, normalizePath, mapDefined, RenameLocation, getAdjustedRenameLocation, isIdentifier, isJsxOpeningElement, isJsxClosingElement, isIntrinsicJsxName, ReferencedSymbol, NavigateToItem, NavigateTo, getFileEmitOutput, SignatureHelpItemsOptions, SignatureHelpItems, SignatureHelp, TextSpan, isRightSideOfPropertyAccess, isRightSideOfQualifiedName, isNameOfModuleDeclaration, ModuleDeclaration, createTextSpanFromBounds, NavigationBarItem, NavigationBar, NavigationTree, ClassifiedSpan, Classifications, EndOfLineState, OutliningSpan, createMapFromTemplate, getTouchingToken, findChildOfKind, TextChange, CodeFixAction, deduplicate, equateValues, compareValues, CombinedCodeFixScope, CombinedCodeActions, OrganizeImportsScope, FileTextChanges, OrganizeImports, CodeActionCommand, ApplyCodeActionCommandResult, isArray, TextInsertion, JsDoc, CharacterCodes, isInString, isInsideJsxElementOrAttribute, isInTemplateString, JsxClosingTagInfo, findPrecedingToken, isJsxText, JsxElement, tagNamesAreEquivalent, isJsxElement, createTextSpanFromRange, TodoCommentDescriptor, TodoComment, stringContains, RenameInfoOptions, RenameInfo, Rename, TextRange, RefactorContext, SelectionRange, SmartSelectionRange, ApplicableRefactorInfo, refactor, RefactorEditInfo, CallHierarchyItem, mapOneOrMany, CallHierarchyIncomingCall, firstOrOnly, CallHierarchyOutgoingCall, createUnderscoreEscapedMap, isStringOrNumericLiteralLike, getEscapedTextOfIdentifierOrLiteral, isPrivateIdentifier, hasJSDocNodes, NumericLiteral, isDeclarationName, isLiteralComputedPropertyDeclarationName, isObjectLiteralExpression, isJsxAttributes, ObjectLiteralElement, isObjectLiteralElement, PropertyName, ObjectLiteralExpression, JsxAttributes, first, ElementAccessExpression, directorySeparator, getDefaultLibFileName, setObjectAllocator } from "./ts"; +import { getJsDocTagsFromDeclarations, getJsDocCommentsFromDeclarations } from "./ts.JsDoc"; +import { getSupportedErrorCodes, getFixes, getAllFixes } from "./ts.codefix"; +import { getFormatContext, SmartIndenter, formatSelection, formatDocument, formatOnOpeningCurly, formatOnClosingCurly, formatOnSemicolon, formatOnEnter, getRangeOfEnclosingComment } from "./ts.formatting"; +import { getSymbolDisplayPartsDocumentationAndSymbolKind, getSymbolModifiers } from "./ts.SymbolDisplay"; +import { getImplementationsAtPosition, toContextSpan, FindReferencesUse, toRenameLocation, toReferenceEntry, Options, ToReferenceOrRenameEntry, findReferenceOrRenameEntries, findReferencedSymbols } from "./ts.FindAllReferences"; +import { spanInSourceFileAtLocation } from "./ts.BreakpointResolver"; +import { collectElements } from "./ts.OutliningElementsCollector"; +import { resolveCallHierarchyDeclaration, createCallHierarchyItem, getIncomingCalls, getOutgoingCalls } from "./ts.CallHierarchy"; +import * as ts from "./ts"; +/** The version of the language service API */ +export const servicesVersion = "0.8"; +function createNode(kind: TKind, pos: number, end: number, parent: Node): NodeObject | TokenObject | IdentifierObject | PrivateIdentifierObject { + const node = isNodeKind(kind) ? new NodeObject(kind, pos, end) : + kind === SyntaxKind.Identifier ? new IdentifierObject(SyntaxKind.Identifier, pos, end) : kind === SyntaxKind.PrivateIdentifier ? new PrivateIdentifierObject(SyntaxKind.PrivateIdentifier, pos, end) : new TokenObject(kind, pos, end); - node.parent = parent; - node.flags = parent.flags & NodeFlags.ContextFlags; - return node; + node.parent = parent; + node.flags = parent.flags & NodeFlags.ContextFlags; + return node; +} +class NodeObject implements Node { + public kind: SyntaxKind; + public pos: number; + public end: number; + public flags: NodeFlags; + public modifierFlagsCache: ModifierFlags; + public transformFlags: TransformFlags; + public parent: Node; + public symbol!: Symbol; // Actually optional, but it was too annoying to access `node.symbol!` everywhere since in many cases we know it must be defined + public jsDoc?: JSDoc[]; + public original?: Node; + private _children: Node[] | undefined; + constructor(kind: SyntaxKind, pos: number, end: number) { + this.pos = pos; + this.end = end; + this.flags = NodeFlags.None; + this.modifierFlagsCache = ModifierFlags.None; + this.transformFlags = TransformFlags.None; + this.parent = undefined!; + this.kind = kind; } - - class NodeObject implements Node { - public kind: SyntaxKind; - public pos: number; - public end: number; - public flags: NodeFlags; - public modifierFlagsCache: ModifierFlags; - public transformFlags: TransformFlags; - public parent: Node; - public symbol!: Symbol; // Actually optional, but it was too annoying to access `node.symbol!` everywhere since in many cases we know it must be defined - public jsDoc?: JSDoc[]; - public original?: Node; - private _children: Node[] | undefined; - - constructor(kind: SyntaxKind, pos: number, end: number) { - this.pos = pos; - this.end = end; - this.flags = NodeFlags.None; - this.modifierFlagsCache = ModifierFlags.None; - this.transformFlags = TransformFlags.None; - this.parent = undefined!; - this.kind = kind; - } - - private assertHasRealPosition(message?: string) { - // eslint-disable-next-line debug-assert - Debug.assert(!positionIsSynthesized(this.pos) && !positionIsSynthesized(this.end), message || "Node must have a real position for this operation"); - } - - public getSourceFile(): SourceFile { - return getSourceFileOfNode(this); - } - - public getStart(sourceFile?: SourceFileLike, includeJsDocComment?: boolean): number { - this.assertHasRealPosition(); - return getTokenPosOfNode(this, sourceFile, includeJsDocComment); - } - - public getFullStart(): number { - this.assertHasRealPosition(); - return this.pos; - } - - public getEnd(): number { - this.assertHasRealPosition(); - return this.end; - } - - public getWidth(sourceFile?: SourceFile): number { - this.assertHasRealPosition(); - return this.getEnd() - this.getStart(sourceFile); - } - - public getFullWidth(): number { - this.assertHasRealPosition(); - return this.end - this.pos; - } - - public getLeadingTriviaWidth(sourceFile?: SourceFile): number { - this.assertHasRealPosition(); - return this.getStart(sourceFile) - this.pos; - } - - public getFullText(sourceFile?: SourceFile): string { - this.assertHasRealPosition(); - return (sourceFile || this.getSourceFile()).text.substring(this.pos, this.end); - } - - public getText(sourceFile?: SourceFile): string { - this.assertHasRealPosition(); - if (!sourceFile) { - sourceFile = this.getSourceFile(); - } - return sourceFile.text.substring(this.getStart(sourceFile), this.getEnd()); - } - - public getChildCount(sourceFile?: SourceFile): number { - return this.getChildren(sourceFile).length; - } - - public getChildAt(index: number, sourceFile?: SourceFile): Node { - return this.getChildren(sourceFile)[index]; - } - - public getChildren(sourceFile?: SourceFileLike): Node[] { - this.assertHasRealPosition("Node without a real position cannot be scanned and thus has no token nodes - use forEachChild and collect the result if that's fine"); - return this._children || (this._children = createChildren(this, sourceFile)); - } - - public getFirstToken(sourceFile?: SourceFileLike): Node | undefined { - this.assertHasRealPosition(); - const children = this.getChildren(sourceFile); - if (!children.length) { - return undefined; - } - - const child = find(children, kid => kid.kind < SyntaxKind.FirstJSDocNode || kid.kind > SyntaxKind.LastJSDocNode)!; - return child.kind < SyntaxKind.FirstNode ? - child : - child.getFirstToken(sourceFile); - } - - public getLastToken(sourceFile?: SourceFileLike): Node | undefined { - this.assertHasRealPosition(); - const children = this.getChildren(sourceFile); - - const child = lastOrUndefined(children); - if (!child) { - return undefined; - } - - return child.kind < SyntaxKind.FirstNode ? child : child.getLastToken(sourceFile); - } - - public forEachChild(cbNode: (node: Node) => T, cbNodeArray?: (nodes: NodeArray) => T): T | undefined { - return forEachChild(this, cbNode, cbNodeArray); - } - } - - function createChildren(node: Node, sourceFile: SourceFileLike | undefined): Node[] { - if (!isNodeKind(node.kind)) { - return emptyArray; - } - - const children: Node[] = []; - - if (isJSDocCommentContainingNode(node)) { - /** Don't add trivia for "tokens" since this is in a comment. */ - node.forEachChild(child => { children.push(child); }); - return children; - } - - scanner.setText((sourceFile || node.getSourceFile()).text); - let pos = node.pos; - const processNode = (child: Node) => { - addSyntheticNodes(children, pos, child.pos, node); - children.push(child); - pos = child.end; - }; - const processNodes = (nodes: NodeArray) => { - addSyntheticNodes(children, pos, nodes.pos, node); - children.push(createSyntaxList(nodes, node)); - pos = nodes.end; - }; - // jsDocComments need to be the first children - forEach((node as JSDocContainer).jsDoc, processNode); - // For syntactic classifications, all trivia are classified together, including jsdoc comments. - // For that to work, the jsdoc comments should still be the leading trivia of the first child. - // Restoring the scanner position ensures that. - pos = node.pos; - node.forEachChild(processNode, processNodes); - addSyntheticNodes(children, pos, node.end, node); - scanner.setText(undefined); - return children; + private assertHasRealPosition(message?: string) { + // eslint-disable-next-line debug-assert + Debug.assert(!positionIsSynthesized(this.pos) && !positionIsSynthesized(this.end), message || "Node must have a real position for this operation"); } - - function addSyntheticNodes(nodes: Push, pos: number, end: number, parent: Node): void { - scanner.setTextPos(pos); - while (pos < end) { - const token = scanner.scan(); - const textPos = scanner.getTextPos(); - if (textPos <= end) { - if (token === SyntaxKind.Identifier) { - Debug.fail(`Did not expect ${Debug.formatSyntaxKind(parent.kind)} to have an Identifier in its trivia`); - } - nodes.push(createNode(token, pos, textPos, parent)); - } - pos = textPos; - if (token === SyntaxKind.EndOfFileToken) { - break; - } - } + public getSourceFile(): SourceFile { + return getSourceFileOfNode(this); } - - function createSyntaxList(nodes: NodeArray, parent: Node): Node { - const list = createNode(SyntaxKind.SyntaxList, nodes.pos, nodes.end, parent) as any as SyntaxList; - list._children = []; - let pos = nodes.pos; - for (const node of nodes) { - addSyntheticNodes(list._children, pos, node.pos, parent); - list._children.push(node); - pos = node.end; - } - addSyntheticNodes(list._children, pos, nodes.end, parent); - return list; - } - - class TokenOrIdentifierObject implements Node { - public kind!: SyntaxKind; - public pos: number; - public end: number; - public flags: NodeFlags; - public modifierFlagsCache: ModifierFlags; - public transformFlags: TransformFlags; - public parent: Node; - public symbol!: Symbol; - public jsDocComments?: JSDoc[]; - - constructor(pos: number, end: number) { - // Set properties in same order as NodeObject - this.pos = pos; - this.end = end; - this.flags = NodeFlags.None; - this.modifierFlagsCache = ModifierFlags.None; - this.transformFlags = TransformFlags.None; - this.parent = undefined!; - } - - public getSourceFile(): SourceFile { - return getSourceFileOfNode(this); - } - - public getStart(sourceFile?: SourceFileLike, includeJsDocComment?: boolean): number { - return getTokenPosOfNode(this, sourceFile, includeJsDocComment); - } - - public getFullStart(): number { - return this.pos; - } - - public getEnd(): number { - return this.end; - } - - public getWidth(sourceFile?: SourceFile): number { - return this.getEnd() - this.getStart(sourceFile); - } - - public getFullWidth(): number { - return this.end - this.pos; - } - - public getLeadingTriviaWidth(sourceFile?: SourceFile): number { - return this.getStart(sourceFile) - this.pos; - } - - public getFullText(sourceFile?: SourceFile): string { - return (sourceFile || this.getSourceFile()).text.substring(this.pos, this.end); - } - - public getText(sourceFile?: SourceFile): string { - if (!sourceFile) { - sourceFile = this.getSourceFile(); - } - return sourceFile.text.substring(this.getStart(sourceFile), this.getEnd()); - } - - public getChildCount(): number { - return 0; - } - - public getChildAt(): Node { - return undefined!; // TODO: GH#18217 - } - - public getChildren(): Node[] { - return this.kind === SyntaxKind.EndOfFileToken ? (this as EndOfFileToken).jsDoc || emptyArray : emptyArray; - } - - public getFirstToken(): Node | undefined { - return undefined; + public getStart(sourceFile?: SourceFileLike, includeJsDocComment?: boolean): number { + this.assertHasRealPosition(); + return getTokenPosOfNode(this, sourceFile, includeJsDocComment); + } + public getFullStart(): number { + this.assertHasRealPosition(); + return this.pos; + } + public getEnd(): number { + this.assertHasRealPosition(); + return this.end; + } + public getWidth(sourceFile?: SourceFile): number { + this.assertHasRealPosition(); + return this.getEnd() - this.getStart(sourceFile); + } + public getFullWidth(): number { + this.assertHasRealPosition(); + return this.end - this.pos; + } + public getLeadingTriviaWidth(sourceFile?: SourceFile): number { + this.assertHasRealPosition(); + return this.getStart(sourceFile) - this.pos; + } + public getFullText(sourceFile?: SourceFile): string { + this.assertHasRealPosition(); + return (sourceFile || this.getSourceFile()).text.substring(this.pos, this.end); + } + public getText(sourceFile?: SourceFile): string { + this.assertHasRealPosition(); + if (!sourceFile) { + sourceFile = this.getSourceFile(); } - - public getLastToken(): Node | undefined { + return sourceFile.text.substring(this.getStart(sourceFile), this.getEnd()); + } + public getChildCount(sourceFile?: SourceFile): number { + return this.getChildren(sourceFile).length; + } + public getChildAt(index: number, sourceFile?: SourceFile): Node { + return this.getChildren(sourceFile)[index]; + } + public getChildren(sourceFile?: SourceFileLike): Node[] { + this.assertHasRealPosition("Node without a real position cannot be scanned and thus has no token nodes - use forEachChild and collect the result if that's fine"); + return this._children || (this._children = createChildren(this, sourceFile)); + } + public getFirstToken(sourceFile?: SourceFileLike): Node | undefined { + this.assertHasRealPosition(); + const children = this.getChildren(sourceFile); + if (!children.length) { return undefined; } - - public forEachChild(): T | undefined { + const child = (find(children, kid => kid.kind < SyntaxKind.FirstJSDocNode || kid.kind > SyntaxKind.LastJSDocNode)!); + return child.kind < SyntaxKind.FirstNode ? + child : + child.getFirstToken(sourceFile); + } + public getLastToken(sourceFile?: SourceFileLike): Node | undefined { + this.assertHasRealPosition(); + const children = this.getChildren(sourceFile); + const child = lastOrUndefined(children); + if (!child) { return undefined; } + return child.kind < SyntaxKind.FirstNode ? child : child.getLastToken(sourceFile); + } + public forEachChild(cbNode: (node: Node) => T, cbNodeArray?: (nodes: NodeArray) => T): T | undefined { + return forEachChild(this, cbNode, cbNodeArray); + } +} +function createChildren(node: Node, sourceFile: SourceFileLike | undefined): Node[] { + if (!isNodeKind(node.kind)) { + return emptyArray; + } + const children: Node[] = []; + if (isJSDocCommentContainingNode(node)) { + /** Don't add trivia for "tokens" since this is in a comment. */ + node.forEachChild(child => { children.push(child); }); + return children; } - - class SymbolObject implements Symbol { - flags: SymbolFlags; - escapedName: __String; - declarations!: Declaration[]; - valueDeclaration!: Declaration; - - // Undefined is used to indicate the value has not been computed. If, after computing, the - // symbol has no doc comment, then the empty array will be returned. - documentationComment?: SymbolDisplayPart[]; - - // Undefined is used to indicate the value has not been computed. If, after computing, the - // symbol has no JSDoc tags, then the empty array will be returned. - tags?: JSDocTagInfo[]; - - constructor(flags: SymbolFlags, name: __String) { - this.flags = flags; - this.escapedName = name; - } - - getFlags(): SymbolFlags { - return this.flags; - } - - get name(): string { - return symbolName(this); - } - - getEscapedName(): __String { - return this.escapedName; - } - - getName(): string { - return this.name; - } - - getDeclarations(): Declaration[] | undefined { - return this.declarations; - } - - getDocumentationComment(checker: TypeChecker | undefined): SymbolDisplayPart[] { - if (!this.documentationComment) { - this.documentationComment = emptyArray; // Set temporarily to avoid an infinite loop finding inherited docs - this.documentationComment = getDocumentationComment(this.declarations, checker); + scanner.setText((sourceFile || node.getSourceFile()).text); + let pos = node.pos; + const processNode = (child: Node) => { + addSyntheticNodes(children, pos, child.pos, node); + children.push(child); + pos = child.end; + }; + const processNodes = (nodes: NodeArray) => { + addSyntheticNodes(children, pos, nodes.pos, node); + children.push(createSyntaxList(nodes, node)); + pos = nodes.end; + }; + // jsDocComments need to be the first children + forEach((node as JSDocContainer).jsDoc, processNode); + // For syntactic classifications, all trivia are classified together, including jsdoc comments. + // For that to work, the jsdoc comments should still be the leading trivia of the first child. + // Restoring the scanner position ensures that. + pos = node.pos; + node.forEachChild(processNode, processNodes); + addSyntheticNodes(children, pos, node.end, node); + scanner.setText(undefined); + return children; +} +function addSyntheticNodes(nodes: Push, pos: number, end: number, parent: Node): void { + scanner.setTextPos(pos); + while (pos < end) { + const token = scanner.scan(); + const textPos = scanner.getTextPos(); + if (textPos <= end) { + if (token === SyntaxKind.Identifier) { + Debug.fail(`Did not expect ${Debug.formatSyntaxKind(parent.kind)} to have an Identifier in its trivia`); } - return this.documentationComment; + nodes.push(createNode(token, pos, textPos, parent)); } - - getJsDocTags(): JSDocTagInfo[] { - if (this.tags === undefined) { - this.tags = JsDoc.getJsDocTagsFromDeclarations(this.declarations); - } - - return this.tags; + pos = textPos; + if (token === SyntaxKind.EndOfFileToken) { + break; } } - - class TokenObject extends TokenOrIdentifierObject implements Token { - public kind: TKind; - - constructor(kind: TKind, pos: number, end: number) { - super(pos, end); - this.kind = kind; - } +} +function createSyntaxList(nodes: NodeArray, parent: Node): Node { + const list = (createNode(SyntaxKind.SyntaxList, nodes.pos, nodes.end, parent) as any as SyntaxList); + list._children = []; + let pos = nodes.pos; + for (const node of nodes) { + addSyntheticNodes(list._children, pos, node.pos, parent); + list._children.push(node); + pos = node.end; } - - class IdentifierObject extends TokenOrIdentifierObject implements Identifier { - public kind: SyntaxKind.Identifier = SyntaxKind.Identifier; - public escapedText!: __String; - public autoGenerateFlags!: GeneratedIdentifierFlags; - _primaryExpressionBrand: any; - _memberExpressionBrand: any; - _leftHandSideExpressionBrand: any; - _updateExpressionBrand: any; - _unaryExpressionBrand: any; - _expressionBrand: any; - _declarationBrand: any; - /*@internal*/typeArguments!: NodeArray; - constructor(_kind: SyntaxKind.Identifier, pos: number, end: number) { - super(pos, end); - } - - get text(): string { - return idText(this); - } + addSyntheticNodes(list._children, pos, nodes.end, parent); + return list; +} +class TokenOrIdentifierObject implements Node { + public kind!: SyntaxKind; + public pos: number; + public end: number; + public flags: NodeFlags; + public modifierFlagsCache: ModifierFlags; + public transformFlags: TransformFlags; + public parent: Node; + public symbol!: Symbol; + public jsDocComments?: JSDoc[]; + constructor(pos: number, end: number) { + // Set properties in same order as NodeObject + this.pos = pos; + this.end = end; + this.flags = NodeFlags.None; + this.modifierFlagsCache = ModifierFlags.None; + this.transformFlags = TransformFlags.None; + this.parent = undefined!; } - IdentifierObject.prototype.kind = SyntaxKind.Identifier; - class PrivateIdentifierObject extends TokenOrIdentifierObject implements PrivateIdentifier { - public kind!: SyntaxKind.PrivateIdentifier; - public escapedText!: __String; - public symbol!: Symbol; - constructor(_kind: SyntaxKind.PrivateIdentifier, pos: number, end: number) { - super(pos, end); - } - - get text(): string { - return idText(this); - } + public getSourceFile(): SourceFile { + return getSourceFileOfNode(this); } - PrivateIdentifierObject.prototype.kind = SyntaxKind.PrivateIdentifier; - - class TypeObject implements Type { - checker: TypeChecker; - flags: TypeFlags; - objectFlags?: ObjectFlags; - id!: number; - symbol!: Symbol; - constructor(checker: TypeChecker, flags: TypeFlags) { - this.checker = checker; - this.flags = flags; - } - getFlags(): TypeFlags { - return this.flags; - } - getSymbol(): Symbol | undefined { - return this.symbol; - } - getProperties(): Symbol[] { - return this.checker.getPropertiesOfType(this); - } - getProperty(propertyName: string): Symbol | undefined { - return this.checker.getPropertyOfType(this, propertyName); - } - getApparentProperties(): Symbol[] { - return this.checker.getAugmentedPropertiesOfType(this); - } - getCallSignatures(): readonly Signature[] { - return this.checker.getSignaturesOfType(this, SignatureKind.Call); - } - getConstructSignatures(): readonly Signature[] { - return this.checker.getSignaturesOfType(this, SignatureKind.Construct); - } - getStringIndexType(): Type | undefined { - return this.checker.getIndexTypeOfType(this, IndexKind.String); - } - getNumberIndexType(): Type | undefined { - return this.checker.getIndexTypeOfType(this, IndexKind.Number); - } - getBaseTypes(): BaseType[] | undefined { - return this.isClassOrInterface() ? this.checker.getBaseTypes(this) : undefined; - } - isNullableType(): boolean { - return this.checker.isNullableType(this); - } - getNonNullableType(): Type { - return this.checker.getNonNullableType(this); - } - getNonOptionalType(): Type { - return this.checker.getNonOptionalType(this); - } - getConstraint(): Type | undefined { - return this.checker.getBaseConstraintOfType(this); - } - getDefault(): Type | undefined { - return this.checker.getDefaultFromTypeParameter(this); - } - - isUnion(): this is UnionType { - return !!(this.flags & TypeFlags.Union); - } - isIntersection(): this is IntersectionType { - return !!(this.flags & TypeFlags.Intersection); - } - isUnionOrIntersection(): this is UnionOrIntersectionType { - return !!(this.flags & TypeFlags.UnionOrIntersection); - } - isLiteral(): this is LiteralType { - return !!(this.flags & TypeFlags.StringOrNumberLiteral); - } - isStringLiteral(): this is StringLiteralType { - return !!(this.flags & TypeFlags.StringLiteral); - } - isNumberLiteral(): this is NumberLiteralType { - return !!(this.flags & TypeFlags.NumberLiteral); - } - isTypeParameter(): this is TypeParameter { - return !!(this.flags & TypeFlags.TypeParameter); - } - isClassOrInterface(): this is InterfaceType { - return !!(getObjectFlags(this) & ObjectFlags.ClassOrInterface); - } - isClass(): this is InterfaceType { - return !!(getObjectFlags(this) & ObjectFlags.Class); + public getStart(sourceFile?: SourceFileLike, includeJsDocComment?: boolean): number { + return getTokenPosOfNode(this, sourceFile, includeJsDocComment); + } + public getFullStart(): number { + return this.pos; + } + public getEnd(): number { + return this.end; + } + public getWidth(sourceFile?: SourceFile): number { + return this.getEnd() - this.getStart(sourceFile); + } + public getFullWidth(): number { + return this.end - this.pos; + } + public getLeadingTriviaWidth(sourceFile?: SourceFile): number { + return this.getStart(sourceFile) - this.pos; + } + public getFullText(sourceFile?: SourceFile): string { + return (sourceFile || this.getSourceFile()).text.substring(this.pos, this.end); + } + public getText(sourceFile?: SourceFile): string { + if (!sourceFile) { + sourceFile = this.getSourceFile(); } - /** - * This polyfills `referenceType.typeArguments` for API consumers - */ - get typeArguments() { - if (getObjectFlags(this) & ObjectFlags.Reference) { - return this.checker.getTypeArguments(this as Type as TypeReference); - } - return undefined; + return sourceFile.text.substring(this.getStart(sourceFile), this.getEnd()); + } + public getChildCount(): number { + return 0; + } + public getChildAt(): Node { + return undefined!; // TODO: GH#18217 + } + public getChildren(): Node[] { + return this.kind === SyntaxKind.EndOfFileToken ? (this as EndOfFileToken).jsDoc || emptyArray : emptyArray; + } + public getFirstToken(): Node | undefined { + return undefined; + } + public getLastToken(): Node | undefined { + return undefined; + } + public forEachChild(): T | undefined { + return undefined; + } +} +class SymbolObject implements Symbol { + flags: SymbolFlags; + escapedName: __String; + declarations!: Declaration[]; + valueDeclaration!: Declaration; + // Undefined is used to indicate the value has not been computed. If, after computing, the + // symbol has no doc comment, then the empty array will be returned. + documentationComment?: SymbolDisplayPart[]; + // Undefined is used to indicate the value has not been computed. If, after computing, the + // symbol has no JSDoc tags, then the empty array will be returned. + tags?: JSDocTagInfo[]; + constructor(flags: SymbolFlags, name: __String) { + this.flags = flags; + this.escapedName = name; + } + getFlags(): SymbolFlags { + return this.flags; + } + get name(): string { + return symbolName(this); + } + getEscapedName(): __String { + return this.escapedName; + } + getName(): string { + return this.name; + } + getDeclarations(): Declaration[] | undefined { + return this.declarations; + } + getDocumentationComment(checker: TypeChecker | undefined): SymbolDisplayPart[] { + if (!this.documentationComment) { + this.documentationComment = emptyArray; // Set temporarily to avoid an infinite loop finding inherited docs + this.documentationComment = getDocumentationComment(this.declarations, checker); } + return this.documentationComment; } - - class SignatureObject implements Signature { - flags: SignatureFlags; - checker: TypeChecker; - declaration!: SignatureDeclaration; - typeParameters?: TypeParameter[]; - parameters!: Symbol[]; - thisParameter!: Symbol; - resolvedReturnType!: Type; - resolvedTypePredicate: TypePredicate | undefined; - minTypeArgumentCount!: number; - minArgumentCount!: number; - - // Undefined is used to indicate the value has not been computed. If, after computing, the - // symbol has no doc comment, then the empty array will be returned. - documentationComment?: SymbolDisplayPart[]; - - // Undefined is used to indicate the value has not been computed. If, after computing, the - // symbol has no doc comment, then the empty array will be returned. - jsDocTags?: JSDocTagInfo[]; - - constructor(checker: TypeChecker, flags: SignatureFlags) { - this.checker = checker; - this.flags = flags; - } - - getDeclaration(): SignatureDeclaration { - return this.declaration; - } - getTypeParameters(): TypeParameter[] | undefined { - return this.typeParameters; - } - getParameters(): Symbol[] { - return this.parameters; - } - getReturnType(): Type { - return this.checker.getReturnTypeOfSignature(this); - } - - getDocumentationComment(): SymbolDisplayPart[] { - return this.documentationComment || (this.documentationComment = getDocumentationComment(singleElementArray(this.declaration), this.checker)); - } - - getJsDocTags(): JSDocTagInfo[] { - if (this.jsDocTags === undefined) { - this.jsDocTags = this.declaration ? JsDoc.getJsDocTagsFromDeclarations([this.declaration]) : []; - } - - return this.jsDocTags; + getJsDocTags(): JSDocTagInfo[] { + if (this.tags === undefined) { + this.tags = getJsDocTagsFromDeclarations(this.declarations); } + return this.tags; + } +} +class TokenObject extends TokenOrIdentifierObject implements Token { + public kind: TKind; + constructor(kind: TKind, pos: number, end: number) { + super(pos, end); + this.kind = kind; + } +} +class IdentifierObject extends TokenOrIdentifierObject implements Identifier { + public kind: SyntaxKind.Identifier = SyntaxKind.Identifier; + public escapedText!: __String; + public autoGenerateFlags!: GeneratedIdentifierFlags; + _primaryExpressionBrand: any; + _memberExpressionBrand: any; + _leftHandSideExpressionBrand: any; + _updateExpressionBrand: any; + _unaryExpressionBrand: any; + _expressionBrand: any; + _declarationBrand: any; + /*@internal*/ typeArguments!: NodeArray; + constructor(_kind: SyntaxKind.Identifier, pos: number, end: number) { + super(pos, end); + } + get text(): string { + return idText(this); + } +} +IdentifierObject.prototype.kind = SyntaxKind.Identifier; +class PrivateIdentifierObject extends TokenOrIdentifierObject implements PrivateIdentifier { + public kind!: SyntaxKind.PrivateIdentifier; + public escapedText!: __String; + public symbol!: Symbol; + constructor(_kind: SyntaxKind.PrivateIdentifier, pos: number, end: number) { + super(pos, end); + } + get text(): string { + return idText(this); + } +} +PrivateIdentifierObject.prototype.kind = SyntaxKind.PrivateIdentifier; +class TypeObject implements Type { + checker: TypeChecker; + flags: TypeFlags; + objectFlags?: ObjectFlags; + id!: number; + symbol!: Symbol; + constructor(checker: TypeChecker, flags: TypeFlags) { + this.checker = checker; + this.flags = flags; + } + getFlags(): TypeFlags { + return this.flags; + } + getSymbol(): Symbol | undefined { + return this.symbol; + } + getProperties(): Symbol[] { + return this.checker.getPropertiesOfType(this); + } + getProperty(propertyName: string): Symbol | undefined { + return this.checker.getPropertyOfType(this, propertyName); + } + getApparentProperties(): Symbol[] { + return this.checker.getAugmentedPropertiesOfType(this); + } + getCallSignatures(): readonly Signature[] { + return this.checker.getSignaturesOfType(this, SignatureKind.Call); + } + getConstructSignatures(): readonly Signature[] { + return this.checker.getSignaturesOfType(this, SignatureKind.Construct); + } + getStringIndexType(): Type | undefined { + return this.checker.getIndexTypeOfType(this, IndexKind.String); + } + getNumberIndexType(): Type | undefined { + return this.checker.getIndexTypeOfType(this, IndexKind.Number); + } + getBaseTypes(): BaseType[] | undefined { + return this.isClassOrInterface() ? this.checker.getBaseTypes(this) : undefined; + } + isNullableType(): boolean { + return this.checker.isNullableType(this); + } + getNonNullableType(): Type { + return this.checker.getNonNullableType(this); + } + getNonOptionalType(): Type { + return this.checker.getNonOptionalType(this); + } + getConstraint(): Type | undefined { + return this.checker.getBaseConstraintOfType(this); + } + getDefault(): Type | undefined { + return this.checker.getDefaultFromTypeParameter(this); + } + isUnion(): this is UnionType { + return !!(this.flags & TypeFlags.Union); + } + isIntersection(): this is IntersectionType { + return !!(this.flags & TypeFlags.Intersection); + } + isUnionOrIntersection(): this is UnionOrIntersectionType { + return !!(this.flags & TypeFlags.UnionOrIntersection); + } + isLiteral(): this is LiteralType { + return !!(this.flags & TypeFlags.StringOrNumberLiteral); + } + isStringLiteral(): this is StringLiteralType { + return !!(this.flags & TypeFlags.StringLiteral); + } + isNumberLiteral(): this is NumberLiteralType { + return !!(this.flags & TypeFlags.NumberLiteral); + } + isTypeParameter(): this is TypeParameter { + return !!(this.flags & TypeFlags.TypeParameter); + } + isClassOrInterface(): this is InterfaceType { + return !!(getObjectFlags(this) & ObjectFlags.ClassOrInterface); + } + isClass(): this is InterfaceType { + return !!(getObjectFlags(this) & ObjectFlags.Class); } - /** - * Returns whether or not the given node has a JSDoc "inheritDoc" tag on it. - * @param node the Node in question. - * @returns `true` if `node` has a JSDoc "inheritDoc" tag on it, otherwise `false`. + * This polyfills `referenceType.typeArguments` for API consumers */ - function hasJSDocInheritDocTag(node: Node) { - return getJSDocTags(node).some(tag => tag.tagName.text === "inheritDoc"); - } - - function getDocumentationComment(declarations: readonly Declaration[] | undefined, checker: TypeChecker | undefined): SymbolDisplayPart[] { - if (!declarations) return emptyArray; - - let doc = JsDoc.getJsDocCommentsFromDeclarations(declarations); - if (doc.length === 0 || declarations.some(hasJSDocInheritDocTag)) { - forEachUnique(declarations, declaration => { - const inheritedDocs = findInheritedJSDocComments(declaration, declaration.symbol.name, checker!); // TODO: GH#18217 - // TODO: GH#16312 Return a ReadonlyArray, avoid copying inheritedDocs - if (inheritedDocs) doc = doc.length === 0 ? inheritedDocs.slice() : inheritedDocs.concat(lineBreakPart(), doc); - }); + get typeArguments() { + if (getObjectFlags(this) & ObjectFlags.Reference) { + return this.checker.getTypeArguments((this as Type as TypeReference)); } - return doc; + return undefined; } - - /** - * Attempts to find JSDoc comments for possibly-inherited properties. Checks superclasses then traverses - * implemented interfaces until a symbol is found with the same name and with documentation. - * @param declaration The possibly-inherited declaration to find comments for. - * @param propertyName The name of the possibly-inherited property. - * @param typeChecker A TypeChecker, used to find inherited properties. - * @returns A filled array of documentation comments if any were found, otherwise an empty array. - */ - function findInheritedJSDocComments(declaration: Declaration, propertyName: string, typeChecker: TypeChecker): readonly SymbolDisplayPart[] | undefined { - return firstDefined(declaration.parent ? getAllSuperTypeNodes(declaration.parent) : emptyArray, superTypeNode => { - const superType = typeChecker.getTypeAtLocation(superTypeNode); - const baseProperty = superType && typeChecker.getPropertyOfType(superType, propertyName); - const inheritedDocs = baseProperty && baseProperty.getDocumentationComment(typeChecker); - return inheritedDocs && inheritedDocs.length ? inheritedDocs : undefined; +} +class SignatureObject implements Signature { + flags: SignatureFlags; + checker: TypeChecker; + declaration!: SignatureDeclaration; + typeParameters?: TypeParameter[]; + parameters!: Symbol[]; + thisParameter!: Symbol; + resolvedReturnType!: Type; + resolvedTypePredicate: TypePredicate | undefined; + minTypeArgumentCount!: number; + minArgumentCount!: number; + // Undefined is used to indicate the value has not been computed. If, after computing, the + // symbol has no doc comment, then the empty array will be returned. + documentationComment?: SymbolDisplayPart[]; + // Undefined is used to indicate the value has not been computed. If, after computing, the + // symbol has no doc comment, then the empty array will be returned. + jsDocTags?: JSDocTagInfo[]; + constructor(checker: TypeChecker, flags: SignatureFlags) { + this.checker = checker; + this.flags = flags; + } + getDeclaration(): SignatureDeclaration { + return this.declaration; + } + getTypeParameters(): TypeParameter[] | undefined { + return this.typeParameters; + } + getParameters(): Symbol[] { + return this.parameters; + } + getReturnType(): Type { + return this.checker.getReturnTypeOfSignature(this); + } + getDocumentationComment(): SymbolDisplayPart[] { + return this.documentationComment || (this.documentationComment = getDocumentationComment(singleElementArray(this.declaration), this.checker)); + } + getJsDocTags(): JSDocTagInfo[] { + if (this.jsDocTags === undefined) { + this.jsDocTags = this.declaration ? getJsDocTagsFromDeclarations([this.declaration]) : []; + } + return this.jsDocTags; + } +} +/** + * Returns whether or not the given node has a JSDoc "inheritDoc" tag on it. + * @param node the Node in question. + * @returns `true` if `node` has a JSDoc "inheritDoc" tag on it, otherwise `false`. + */ +function hasJSDocInheritDocTag(node: Node) { + return getJSDocTags(node).some(tag => tag.tagName.text === "inheritDoc"); +} +function getDocumentationComment(declarations: readonly Declaration[] | undefined, checker: TypeChecker | undefined): SymbolDisplayPart[] { + if (!declarations) + return emptyArray; + let doc = getJsDocCommentsFromDeclarations(declarations); + if (doc.length === 0 || declarations.some(hasJSDocInheritDocTag)) { + forEachUnique(declarations, declaration => { + const inheritedDocs = findInheritedJSDocComments(declaration, declaration.symbol.name, checker!); // TODO: GH#18217 + // TODO: GH#16312 Return a ReadonlyArray, avoid copying inheritedDocs + if (inheritedDocs) + doc = doc.length === 0 ? inheritedDocs.slice() : inheritedDocs.concat(lineBreakPart(), doc); }); } - - class SourceFileObject extends NodeObject implements SourceFile { - public kind: SyntaxKind.SourceFile = SyntaxKind.SourceFile; - public _declarationBrand: any; - public fileName!: string; - public path!: Path; - public resolvedPath!: Path; - public originalFileName!: string; - public text!: string; - public scriptSnapshot!: IScriptSnapshot; - public lineMap!: readonly number[]; - - public statements!: NodeArray; - public endOfFileToken!: Token; - - public amdDependencies!: { name: string; path: string }[]; - public moduleName!: string; - public referencedFiles!: FileReference[]; - public typeReferenceDirectives!: FileReference[]; - public libReferenceDirectives!: FileReference[]; - - public syntacticDiagnostics!: DiagnosticWithLocation[]; - public parseDiagnostics!: DiagnosticWithLocation[]; - public bindDiagnostics!: DiagnosticWithLocation[]; - public bindSuggestionDiagnostics?: DiagnosticWithLocation[]; - - public isDeclarationFile!: boolean; - public isDefaultLib!: boolean; - public hasNoDefaultLib!: boolean; - public externalModuleIndicator!: Node; // The first node that causes this file to be an external module - public commonJsModuleIndicator!: Node; // The first node that causes this file to be a CommonJS module - public nodeCount!: number; - public identifierCount!: number; - public symbolCount!: number; - public version!: string; - public scriptKind!: ScriptKind; - public languageVersion!: ScriptTarget; - public languageVariant!: LanguageVariant; - public identifiers!: Map; - public nameTable: UnderscoreEscapedMap | undefined; - public resolvedModules: Map | undefined; - public resolvedTypeReferenceDirectiveNames!: Map; - public imports!: readonly StringLiteralLike[]; - public moduleAugmentations!: StringLiteral[]; - private namedDeclarations: Map | undefined; - public ambientModuleNames!: string[]; - public checkJsDirective: CheckJsDirective | undefined; - public possiblyContainDynamicImport?: boolean; - public pragmas!: PragmaMap; - public localJsxFactory: EntityName | undefined; - public localJsxNamespace: __String | undefined; - - constructor(kind: SyntaxKind, pos: number, end: number) { - super(kind, pos, end); - } - - public update(newText: string, textChangeRange: TextChangeRange): SourceFile { - return updateSourceFile(this, newText, textChangeRange); - } - - public getLineAndCharacterOfPosition(position: number): LineAndCharacter { - return getLineAndCharacterOfPosition(this, position); - } - - public getLineStarts(): readonly number[] { - return getLineStarts(this); - } - - public getPositionOfLineAndCharacter(line: number, character: number, allowEdits?: true): number { - return computePositionOfLineAndCharacter(getLineStarts(this), line, character, this.text, allowEdits); - } - - public getLineEndOfPosition(pos: number): number { - const { line } = this.getLineAndCharacterOfPosition(pos); - const lineStarts = this.getLineStarts(); - - let lastCharPos: number | undefined; - if (line + 1 >= lineStarts.length) { - lastCharPos = this.getEnd(); - } - if (!lastCharPos) { - lastCharPos = lineStarts[line + 1] - 1; - } - - const fullText = this.getFullText(); - // if the new line is "\r\n", we should return the last non-new-line-character position - return fullText[lastCharPos] === "\n" && fullText[lastCharPos - 1] === "\r" ? lastCharPos - 1 : lastCharPos; - } - - public getNamedDeclarations(): Map { - if (!this.namedDeclarations) { - this.namedDeclarations = this.computeNamedDeclarations(); - } - - return this.namedDeclarations; - } - - private computeNamedDeclarations(): Map { - const result = createMultiMap(); - - this.forEachChild(visit); - - return result; - - function addDeclaration(declaration: Declaration) { - const name = getDeclarationName(declaration); - if (name) { - result.add(name, declaration); - } - } - - function getDeclarations(name: string) { - let declarations = result.get(name); - if (!declarations) { - result.set(name, declarations = []); - } - return declarations; + return doc; +} +/** + * Attempts to find JSDoc comments for possibly-inherited properties. Checks superclasses then traverses + * implemented interfaces until a symbol is found with the same name and with documentation. + * @param declaration The possibly-inherited declaration to find comments for. + * @param propertyName The name of the possibly-inherited property. + * @param typeChecker A TypeChecker, used to find inherited properties. + * @returns A filled array of documentation comments if any were found, otherwise an empty array. + */ +function findInheritedJSDocComments(declaration: Declaration, propertyName: string, typeChecker: TypeChecker): readonly SymbolDisplayPart[] | undefined { + return firstDefined(declaration.parent ? getAllSuperTypeNodes(declaration.parent) : emptyArray, superTypeNode => { + const superType = typeChecker.getTypeAtLocation(superTypeNode); + const baseProperty = superType && typeChecker.getPropertyOfType(superType, propertyName); + const inheritedDocs = baseProperty && baseProperty.getDocumentationComment(typeChecker); + return inheritedDocs && inheritedDocs.length ? inheritedDocs : undefined; + }); +} +class SourceFileObject extends NodeObject implements SourceFile { + public kind: SyntaxKind.SourceFile = SyntaxKind.SourceFile; + public _declarationBrand: any; + public fileName!: string; + public path!: Path; + public resolvedPath!: Path; + public originalFileName!: string; + public text!: string; + public scriptSnapshot!: IScriptSnapshot; + public lineMap!: readonly number[]; + public statements!: NodeArray; + public endOfFileToken!: Token; + public amdDependencies!: { + name: string; + path: string; + }[]; + public moduleName!: string; + public referencedFiles!: FileReference[]; + public typeReferenceDirectives!: FileReference[]; + public libReferenceDirectives!: FileReference[]; + public syntacticDiagnostics!: DiagnosticWithLocation[]; + public parseDiagnostics!: DiagnosticWithLocation[]; + public bindDiagnostics!: DiagnosticWithLocation[]; + public bindSuggestionDiagnostics?: DiagnosticWithLocation[]; + public isDeclarationFile!: boolean; + public isDefaultLib!: boolean; + public hasNoDefaultLib!: boolean; + public externalModuleIndicator!: Node; // The first node that causes this file to be an external module + public commonJsModuleIndicator!: Node; // The first node that causes this file to be a CommonJS module + public nodeCount!: number; + public identifierCount!: number; + public symbolCount!: number; + public version!: string; + public scriptKind!: ScriptKind; + public languageVersion!: ScriptTarget; + public languageVariant!: LanguageVariant; + public identifiers!: ts.Map; + public nameTable: UnderscoreEscapedMap | undefined; + public resolvedModules: ts.Map | undefined; + public resolvedTypeReferenceDirectiveNames!: ts.Map; + public imports!: readonly StringLiteralLike[]; + public moduleAugmentations!: StringLiteral[]; + private namedDeclarations: ts.Map | undefined; + public ambientModuleNames!: string[]; + public checkJsDirective: CheckJsDirective | undefined; + public possiblyContainDynamicImport?: boolean; + public pragmas!: PragmaMap; + public localJsxFactory: EntityName | undefined; + public localJsxNamespace: __String | undefined; + constructor(kind: SyntaxKind, pos: number, end: number) { + super(kind, pos, end); + } + public update(newText: string, textChangeRange: TextChangeRange): SourceFile { + return updateSourceFile(this, newText, textChangeRange); + } + public getLineAndCharacterOfPosition(position: number): LineAndCharacter { + return getLineAndCharacterOfPosition(this, position); + } + public getLineStarts(): readonly number[] { + return getLineStarts(this); + } + public getPositionOfLineAndCharacter(line: number, character: number, allowEdits?: true): number { + return computePositionOfLineAndCharacter(getLineStarts(this), line, character, this.text, allowEdits); + } + public getLineEndOfPosition(pos: number): number { + const { line } = this.getLineAndCharacterOfPosition(pos); + const lineStarts = this.getLineStarts(); + let lastCharPos: number | undefined; + if (line + 1 >= lineStarts.length) { + lastCharPos = this.getEnd(); + } + if (!lastCharPos) { + lastCharPos = lineStarts[line + 1] - 1; + } + const fullText = this.getFullText(); + // if the new line is "\r\n", we should return the last non-new-line-character position + return fullText[lastCharPos] === "\n" && fullText[lastCharPos - 1] === "\r" ? lastCharPos - 1 : lastCharPos; + } + public getNamedDeclarations(): ts.Map { + if (!this.namedDeclarations) { + this.namedDeclarations = this.computeNamedDeclarations(); + } + return this.namedDeclarations; + } + private computeNamedDeclarations(): ts.Map { + const result = createMultiMap(); + this.forEachChild(visit); + return result; + function addDeclaration(declaration: Declaration) { + const name = getDeclarationName(declaration); + if (name) { + result.add(name, declaration); } - - function getDeclarationName(declaration: Declaration) { - const name = getNonAssignedNameOfDeclaration(declaration); - return name && (isComputedPropertyName(name) && isPropertyAccessExpression(name.expression) ? name.expression.name.text - : isPropertyName(name) ? getNameFromPropertyName(name) : undefined); + } + function getDeclarations(name: string) { + let declarations = result.get(name); + if (!declarations) { + result.set(name, declarations = []); } - - function visit(node: Node): void { - switch (node.kind) { - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.FunctionExpression: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.MethodSignature: - const functionDeclaration = node; - const declarationName = getDeclarationName(functionDeclaration); - - if (declarationName) { - const declarations = getDeclarations(declarationName); - const lastDeclaration = lastOrUndefined(declarations); - - // Check whether this declaration belongs to an "overload group". - if (lastDeclaration && functionDeclaration.parent === lastDeclaration.parent && functionDeclaration.symbol === lastDeclaration.symbol) { - // Overwrite the last declaration if it was an overload - // and this one is an implementation. - if (functionDeclaration.body && !(lastDeclaration).body) { - declarations[declarations.length - 1] = functionDeclaration; - } - } - else { - declarations.push(functionDeclaration); + return declarations; + } + function getDeclarationName(declaration: Declaration) { + const name = getNonAssignedNameOfDeclaration(declaration); + return name && (isComputedPropertyName(name) && isPropertyAccessExpression(name.expression) ? name.expression.name.text + : isPropertyName(name) ? getNameFromPropertyName(name) : undefined); + } + function visit(node: Node): void { + switch (node.kind) { + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.FunctionExpression: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: + const functionDeclaration = (node); + const declarationName = getDeclarationName(functionDeclaration); + if (declarationName) { + const declarations = getDeclarations(declarationName); + const lastDeclaration = lastOrUndefined(declarations); + // Check whether this declaration belongs to an "overload group". + if (lastDeclaration && functionDeclaration.parent === lastDeclaration.parent && functionDeclaration.symbol === lastDeclaration.symbol) { + // Overwrite the last declaration if it was an overload + // and this one is an implementation. + if (functionDeclaration.body && !(lastDeclaration).body) { + declarations[declarations.length - 1] = functionDeclaration; } } - forEachChild(node, visit); + else { + declarations.push(functionDeclaration); + } + } + forEachChild(node, visit); + break; + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ClassExpression: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.ImportEqualsDeclaration: + case SyntaxKind.ExportSpecifier: + case SyntaxKind.ImportSpecifier: + case SyntaxKind.ImportClause: + case SyntaxKind.NamespaceImport: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + case SyntaxKind.TypeLiteral: + addDeclaration((node)); + forEachChild(node, visit); + break; + case SyntaxKind.Parameter: + // Only consider parameter properties + if (!hasModifier(node, ModifierFlags.ParameterPropertyModifier)) { break; - - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ClassExpression: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.ModuleDeclaration: - case SyntaxKind.ImportEqualsDeclaration: - case SyntaxKind.ExportSpecifier: - case SyntaxKind.ImportSpecifier: - case SyntaxKind.ImportClause: - case SyntaxKind.NamespaceImport: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - case SyntaxKind.TypeLiteral: - addDeclaration(node); - forEachChild(node, visit); + } + // falls through + case SyntaxKind.VariableDeclaration: + case SyntaxKind.BindingElement: { + const decl = (node); + if (isBindingPattern(decl.name)) { + forEachChild(decl.name, visit); break; - - case SyntaxKind.Parameter: - // Only consider parameter properties - if (!hasModifier(node, ModifierFlags.ParameterPropertyModifier)) { - break; - } - // falls through - - case SyntaxKind.VariableDeclaration: - case SyntaxKind.BindingElement: { - const decl = node; - if (isBindingPattern(decl.name)) { - forEachChild(decl.name, visit); - break; + } + if (decl.initializer) { + visit(decl.initializer); + } + } + // falls through + case SyntaxKind.EnumMember: + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.PropertySignature: + addDeclaration((node)); + break; + case SyntaxKind.ExportDeclaration: + // Handle named exports case e.g.: + // export {a, b as B} from "mod"; + const exportDeclaration = (node); + if (exportDeclaration.exportClause) { + if (isNamedExports(exportDeclaration.exportClause)) { + forEach(exportDeclaration.exportClause.elements, visit); } - if (decl.initializer) { - visit(decl.initializer); + else { + visit(exportDeclaration.exportClause.name); } } - // falls through - case SyntaxKind.EnumMember: - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.PropertySignature: - addDeclaration(node); - break; - - case SyntaxKind.ExportDeclaration: - // Handle named exports case e.g.: - // export {a, b as B} from "mod"; - const exportDeclaration = (node); - if (exportDeclaration.exportClause) { - if (isNamedExports(exportDeclaration.exportClause)) { - forEach(exportDeclaration.exportClause.elements, visit); - } - else { - visit(exportDeclaration.exportClause.name); - } + break; + case SyntaxKind.ImportDeclaration: + const importClause = (node).importClause; + if (importClause) { + // Handle default import case e.g.: + // import d from "mod"; + if (importClause.name) { + addDeclaration(importClause.name); } - break; - - case SyntaxKind.ImportDeclaration: - const importClause = (node).importClause; - if (importClause) { - // Handle default import case e.g.: - // import d from "mod"; - if (importClause.name) { - addDeclaration(importClause.name); + // Handle named bindings in imports e.g.: + // import * as NS from "mod"; + // import {a, b as B} from "mod"; + if (importClause.namedBindings) { + if (importClause.namedBindings.kind === SyntaxKind.NamespaceImport) { + addDeclaration(importClause.namedBindings); } - - // Handle named bindings in imports e.g.: - // import * as NS from "mod"; - // import {a, b as B} from "mod"; - if (importClause.namedBindings) { - if (importClause.namedBindings.kind === SyntaxKind.NamespaceImport) { - addDeclaration(importClause.namedBindings); - } - else { - forEach(importClause.namedBindings.elements, visit); - } + else { + forEach(importClause.namedBindings.elements, visit); } } - break; - - case SyntaxKind.BinaryExpression: - if (getAssignmentDeclarationKind(node as BinaryExpression) !== AssignmentDeclarationKind.None) { - addDeclaration(node as BinaryExpression); - } - // falls through - - default: - forEachChild(node, visit); - } + } + break; + case SyntaxKind.BinaryExpression: + if (getAssignmentDeclarationKind((node as BinaryExpression)) !== AssignmentDeclarationKind.None) { + addDeclaration((node as BinaryExpression)); + } + // falls through + default: + forEachChild(node, visit); } } } - - class SourceMapSourceObject implements SourceMapSource { - lineMap!: number[]; - constructor(public fileName: string, public text: string, public skipTrivia?: (pos: number) => number) { } - - public getLineAndCharacterOfPosition(pos: number): LineAndCharacter { - return getLineAndCharacterOfPosition(this, pos); +} +class SourceMapSourceObject implements SourceMapSource { + lineMap!: number[]; + constructor(public fileName: string, public text: string, public skipTrivia?: (pos: number) => number) { } + public getLineAndCharacterOfPosition(pos: number): LineAndCharacter { + return getLineAndCharacterOfPosition(this, pos); + } +} +function getServicesObjectAllocator(): ObjectAllocator { + return { + getNodeConstructor: () => NodeObject, + getTokenConstructor: () => TokenObject, + getIdentifierConstructor: () => IdentifierObject, + getPrivateIdentifierConstructor: () => PrivateIdentifierObject, + getSourceFileConstructor: () => SourceFileObject, + getSymbolConstructor: () => SymbolObject, + getTypeConstructor: () => TypeObject, + getSignatureConstructor: () => SignatureObject, + getSourceMapSourceConstructor: () => SourceMapSourceObject, + }; +} +/// Language Service +// Information about a specific host file. +interface HostFileInformation { + hostFileName: string; + version: string; + scriptSnapshot: IScriptSnapshot; + scriptKind: ScriptKind; +} +/* @internal */ +export interface DisplayPartsSymbolWriter extends EmitTextWriter { + displayParts(): SymbolDisplayPart[]; +} +/* @internal */ +export function toEditorSettings(options: FormatCodeOptions | FormatCodeSettings): FormatCodeSettings; +export function toEditorSettings(options: EditorOptions | EditorSettings): EditorSettings; +export function toEditorSettings(optionsAsMap: MapLike): MapLike { + let allPropertiesAreCamelCased = true; + for (const key in optionsAsMap) { + if (hasProperty(optionsAsMap, key) && !isCamelCase(key)) { + allPropertiesAreCamelCased = false; + break; } } - - function getServicesObjectAllocator(): ObjectAllocator { - return { - getNodeConstructor: () => NodeObject, - getTokenConstructor: () => TokenObject, - - getIdentifierConstructor: () => IdentifierObject, - getPrivateIdentifierConstructor: () => PrivateIdentifierObject, - getSourceFileConstructor: () => SourceFileObject, - getSymbolConstructor: () => SymbolObject, - getTypeConstructor: () => TypeObject, - getSignatureConstructor: () => SignatureObject, - getSourceMapSourceConstructor: () => SourceMapSourceObject, - }; + if (allPropertiesAreCamelCased) { + return optionsAsMap; } - - /// Language Service - - // Information about a specific host file. - interface HostFileInformation { - hostFileName: string; - version: string; - scriptSnapshot: IScriptSnapshot; - scriptKind: ScriptKind; - } - - /* @internal */ - export interface DisplayPartsSymbolWriter extends EmitTextWriter { - displayParts(): SymbolDisplayPart[]; - } - - /* @internal */ - export function toEditorSettings(options: FormatCodeOptions | FormatCodeSettings): FormatCodeSettings; - export function toEditorSettings(options: EditorOptions | EditorSettings): EditorSettings; - export function toEditorSettings(optionsAsMap: MapLike): MapLike { - let allPropertiesAreCamelCased = true; - for (const key in optionsAsMap) { - if (hasProperty(optionsAsMap, key) && !isCamelCase(key)) { - allPropertiesAreCamelCased = false; - break; - } + const settings: MapLike = {}; + for (const key in optionsAsMap) { + if (hasProperty(optionsAsMap, key)) { + const newKey = isCamelCase(key) ? key : key.charAt(0).toLowerCase() + key.substr(1); + settings[newKey] = optionsAsMap[key]; } - if (allPropertiesAreCamelCased) { - return optionsAsMap; + } + return settings; +} +function isCamelCase(s: string) { + return !s.length || s.charAt(0) === s.charAt(0).toLowerCase(); +} +export function displayPartsToString(displayParts: SymbolDisplayPart[] | undefined) { + if (displayParts) { + return map(displayParts, displayPart => displayPart.text).join(""); + } + return ""; +} +export function getDefaultCompilerOptions(): CompilerOptions { + // Always default to "ScriptTarget.ES5" for the language service + return { + target: ScriptTarget.ES5, + jsx: JsxEmit.Preserve + }; +} +export function getSupportedCodeFixes() { + return getSupportedErrorCodes(); +} +// Either it will be file name if host doesnt have file or it will be the host's file information +type CachedHostFileInformation = HostFileInformation | string; +// Cache host information about script Should be refreshed +// at each language service public entry point, since we don't know when +// the set of scripts handled by the host changes. +class HostCache { + private fileNameToEntry: ts.Map; + private _compilationSettings: CompilerOptions; + private currentDirectory: string; + constructor(private host: LanguageServiceHost, getCanonicalFileName: GetCanonicalFileName) { + // script id => script index + this.currentDirectory = host.getCurrentDirectory(); + this.fileNameToEntry = createMap(); + // Initialize the list with the root file names + const rootFileNames = host.getScriptFileNames(); + for (const fileName of rootFileNames) { + this.createEntry(fileName, toPath(fileName, this.currentDirectory, getCanonicalFileName)); + } + // store the compilation settings + this._compilationSettings = host.getCompilationSettings() || getDefaultCompilerOptions(); + } + public compilationSettings() { + return this._compilationSettings; + } + public getProjectReferences(): readonly ProjectReference[] | undefined { + return this.host.getProjectReferences && this.host.getProjectReferences(); + } + private createEntry(fileName: string, path: Path) { + let entry: CachedHostFileInformation; + const scriptSnapshot = this.host.getScriptSnapshot(fileName); + if (scriptSnapshot) { + entry = { + hostFileName: fileName, + version: this.host.getScriptVersion(fileName), + scriptSnapshot, + scriptKind: getScriptKind(fileName, this.host) + }; } - const settings: MapLike = {}; - for (const key in optionsAsMap) { - if (hasProperty(optionsAsMap, key)) { - const newKey = isCamelCase(key) ? key : key.charAt(0).toLowerCase() + key.substr(1); - settings[newKey] = optionsAsMap[key]; - } + else { + entry = fileName; } - return settings; + this.fileNameToEntry.set(path, entry); + return entry; } - - function isCamelCase(s: string) { - return !s.length || s.charAt(0) === s.charAt(0).toLowerCase(); + public getEntryByPath(path: Path): CachedHostFileInformation | undefined { + return this.fileNameToEntry.get(path); } - - export function displayPartsToString(displayParts: SymbolDisplayPart[] | undefined) { - if (displayParts) { - return map(displayParts, displayPart => displayPart.text).join(""); - } - - return ""; + public getHostFileInformation(path: Path): HostFileInformation | undefined { + const entry = this.fileNameToEntry.get(path); + return !isString(entry) ? entry : undefined; } - - export function getDefaultCompilerOptions(): CompilerOptions { - // Always default to "ScriptTarget.ES5" for the language service - return { - target: ScriptTarget.ES5, - jsx: JsxEmit.Preserve - }; + public getOrCreateEntryByPath(fileName: string, path: Path): HostFileInformation { + const info = this.getEntryByPath(path) || this.createEntry(fileName, path); + return isString(info) ? undefined! : info; // TODO: GH#18217 } - - export function getSupportedCodeFixes() { - return codefix.getSupportedErrorCodes(); - } - - // Either it will be file name if host doesnt have file or it will be the host's file information - type CachedHostFileInformation = HostFileInformation | string; - - // Cache host information about script Should be refreshed - // at each language service public entry point, since we don't know when - // the set of scripts handled by the host changes. - class HostCache { - private fileNameToEntry: Map; - private _compilationSettings: CompilerOptions; - private currentDirectory: string; - - constructor(private host: LanguageServiceHost, getCanonicalFileName: GetCanonicalFileName) { - // script id => script index - this.currentDirectory = host.getCurrentDirectory(); - this.fileNameToEntry = createMap(); - - // Initialize the list with the root file names - const rootFileNames = host.getScriptFileNames(); - for (const fileName of rootFileNames) { - this.createEntry(fileName, toPath(fileName, this.currentDirectory, getCanonicalFileName)); - } - - // store the compilation settings - this._compilationSettings = host.getCompilationSettings() || getDefaultCompilerOptions(); - } - - public compilationSettings() { - return this._compilationSettings; - } - - public getProjectReferences(): readonly ProjectReference[] | undefined { - return this.host.getProjectReferences && this.host.getProjectReferences(); - } - - private createEntry(fileName: string, path: Path) { - let entry: CachedHostFileInformation; - const scriptSnapshot = this.host.getScriptSnapshot(fileName); - if (scriptSnapshot) { - entry = { - hostFileName: fileName, - version: this.host.getScriptVersion(fileName), - scriptSnapshot, - scriptKind: getScriptKind(fileName, this.host) - }; + public getRootFileNames(): string[] { + const names: string[] = []; + this.fileNameToEntry.forEach(entry => { + if (isString(entry)) { + names.push(entry); } else { - entry = fileName; + names.push(entry.hostFileName); } - - this.fileNameToEntry.set(path, entry); - return entry; - } - - public getEntryByPath(path: Path): CachedHostFileInformation | undefined { - return this.fileNameToEntry.get(path); - } - - public getHostFileInformation(path: Path): HostFileInformation | undefined { - const entry = this.fileNameToEntry.get(path); - return !isString(entry) ? entry : undefined; - } - - public getOrCreateEntryByPath(fileName: string, path: Path): HostFileInformation { - const info = this.getEntryByPath(path) || this.createEntry(fileName, path); - return isString(info) ? undefined! : info; // TODO: GH#18217 - } - - public getRootFileNames(): string[] { - const names: string[] = []; - this.fileNameToEntry.forEach(entry => { - if (isString(entry)) { - names.push(entry); - } - else { - names.push(entry.hostFileName); - } - }); - return names; - } - - public getScriptSnapshot(path: Path): IScriptSnapshot { - const file = this.getHostFileInformation(path); - return (file && file.scriptSnapshot)!; // TODO: GH#18217 - } - } - - class SyntaxTreeCache { - // For our syntactic only features, we also keep a cache of the syntax tree for the - // currently edited file. - private currentFileName: string | undefined; - private currentFileVersion: string | undefined; - private currentFileScriptSnapshot: IScriptSnapshot | undefined; - private currentSourceFile: SourceFile | undefined; - - constructor(private host: LanguageServiceHost) { - } - - public getCurrentSourceFile(fileName: string): SourceFile { - const scriptSnapshot = this.host.getScriptSnapshot(fileName); - if (!scriptSnapshot) { - // The host does not know about this file. - throw new Error("Could not find file: '" + fileName + "'."); - } - - const scriptKind = getScriptKind(fileName, this.host); - const version = this.host.getScriptVersion(fileName); - let sourceFile: SourceFile | undefined; - - if (this.currentFileName !== fileName) { - // This is a new file, just parse it - sourceFile = createLanguageServiceSourceFile(fileName, scriptSnapshot, ScriptTarget.Latest, version, /*setNodeParents*/ true, scriptKind); - } - else if (this.currentFileVersion !== version) { - // This is the same file, just a newer version. Incrementally parse the file. - const editRange = scriptSnapshot.getChangeRange(this.currentFileScriptSnapshot!); - sourceFile = updateLanguageServiceSourceFile(this.currentSourceFile!, scriptSnapshot, version, editRange); - } - - if (sourceFile) { - // All done, ensure state is up to date - this.currentFileVersion = version; - this.currentFileName = fileName; - this.currentFileScriptSnapshot = scriptSnapshot; - this.currentSourceFile = sourceFile; - } - - return this.currentSourceFile!; - } + }); + return names; } - - function setSourceFileFields(sourceFile: SourceFile, scriptSnapshot: IScriptSnapshot, version: string) { - sourceFile.version = version; - sourceFile.scriptSnapshot = scriptSnapshot; + public getScriptSnapshot(path: Path): IScriptSnapshot { + const file = this.getHostFileInformation(path); + return (file && file.scriptSnapshot)!; // TODO: GH#18217 } - - export function createLanguageServiceSourceFile(fileName: string, scriptSnapshot: IScriptSnapshot, scriptTarget: ScriptTarget, version: string, setNodeParents: boolean, scriptKind?: ScriptKind): SourceFile { - const sourceFile = createSourceFile(fileName, getSnapshotText(scriptSnapshot), scriptTarget, setNodeParents, scriptKind); - setSourceFileFields(sourceFile, scriptSnapshot, version); - return sourceFile; +} +class SyntaxTreeCache { + // For our syntactic only features, we also keep a cache of the syntax tree for the + // currently edited file. + private currentFileName: string | undefined; + private currentFileVersion: string | undefined; + private currentFileScriptSnapshot: IScriptSnapshot | undefined; + private currentSourceFile: SourceFile | undefined; + constructor(private host: LanguageServiceHost) { } - - export function updateLanguageServiceSourceFile(sourceFile: SourceFile, scriptSnapshot: IScriptSnapshot, version: string, textChangeRange: TextChangeRange | undefined, aggressiveChecks?: boolean): SourceFile { - // If we were given a text change range, and our version or open-ness changed, then - // incrementally parse this file. - if (textChangeRange) { - if (version !== sourceFile.version) { - let newText: string; - - // grab the fragment from the beginning of the original text to the beginning of the span - const prefix = textChangeRange.span.start !== 0 - ? sourceFile.text.substr(0, textChangeRange.span.start) - : ""; - - // grab the fragment from the end of the span till the end of the original text - const suffix = textSpanEnd(textChangeRange.span) !== sourceFile.text.length - ? sourceFile.text.substr(textSpanEnd(textChangeRange.span)) - : ""; - - if (textChangeRange.newLength === 0) { - // edit was a deletion - just combine prefix and suffix - newText = prefix && suffix ? prefix + suffix : prefix || suffix; - } - else { - // it was actual edit, fetch the fragment of new text that correspond to new span - const changedText = scriptSnapshot.getText(textChangeRange.span.start, textChangeRange.span.start + textChangeRange.newLength); - // combine prefix, changed text and suffix - newText = prefix && suffix - ? prefix + changedText + suffix - : prefix - ? (prefix + changedText) - : (changedText + suffix); - } - - const newSourceFile = updateSourceFile(sourceFile, newText, textChangeRange, aggressiveChecks); - setSourceFileFields(newSourceFile, scriptSnapshot, version); - // after incremental parsing nameTable might not be up-to-date - // drop it so it can be lazily recreated later - newSourceFile.nameTable = undefined; - - // dispose all resources held by old script snapshot - if (sourceFile !== newSourceFile && sourceFile.scriptSnapshot) { - if (sourceFile.scriptSnapshot.dispose) { - sourceFile.scriptSnapshot.dispose(); - } - - sourceFile.scriptSnapshot = undefined; + public getCurrentSourceFile(fileName: string): SourceFile { + const scriptSnapshot = this.host.getScriptSnapshot(fileName); + if (!scriptSnapshot) { + // The host does not know about this file. + throw new Error("Could not find file: '" + fileName + "'."); + } + const scriptKind = getScriptKind(fileName, this.host); + const version = this.host.getScriptVersion(fileName); + let sourceFile: SourceFile | undefined; + if (this.currentFileName !== fileName) { + // This is a new file, just parse it + sourceFile = createLanguageServiceSourceFile(fileName, scriptSnapshot, ScriptTarget.Latest, version, /*setNodeParents*/ true, scriptKind); + } + else if (this.currentFileVersion !== version) { + // This is the same file, just a newer version. Incrementally parse the file. + const editRange = scriptSnapshot.getChangeRange(this.currentFileScriptSnapshot!); + sourceFile = updateLanguageServiceSourceFile(this.currentSourceFile!, scriptSnapshot, version, editRange); + } + if (sourceFile) { + // All done, ensure state is up to date + this.currentFileVersion = version; + this.currentFileName = fileName; + this.currentFileScriptSnapshot = scriptSnapshot; + this.currentSourceFile = sourceFile; + } + return this.currentSourceFile!; + } +} +function setSourceFileFields(sourceFile: SourceFile, scriptSnapshot: IScriptSnapshot, version: string) { + sourceFile.version = version; + sourceFile.scriptSnapshot = scriptSnapshot; +} +export function createLanguageServiceSourceFile(fileName: string, scriptSnapshot: IScriptSnapshot, scriptTarget: ScriptTarget, version: string, setNodeParents: boolean, scriptKind?: ScriptKind): SourceFile { + const sourceFile = createSourceFile(fileName, getSnapshotText(scriptSnapshot), scriptTarget, setNodeParents, scriptKind); + setSourceFileFields(sourceFile, scriptSnapshot, version); + return sourceFile; +} +export function updateLanguageServiceSourceFile(sourceFile: SourceFile, scriptSnapshot: IScriptSnapshot, version: string, textChangeRange: TextChangeRange | undefined, aggressiveChecks?: boolean): SourceFile { + // If we were given a text change range, and our version or open-ness changed, then + // incrementally parse this file. + if (textChangeRange) { + if (version !== sourceFile.version) { + let newText: string; + // grab the fragment from the beginning of the original text to the beginning of the span + const prefix = textChangeRange.span.start !== 0 + ? sourceFile.text.substr(0, textChangeRange.span.start) + : ""; + // grab the fragment from the end of the span till the end of the original text + const suffix = textSpanEnd(textChangeRange.span) !== sourceFile.text.length + ? sourceFile.text.substr(textSpanEnd(textChangeRange.span)) + : ""; + if (textChangeRange.newLength === 0) { + // edit was a deletion - just combine prefix and suffix + newText = prefix && suffix ? prefix + suffix : prefix || suffix; + } + else { + // it was actual edit, fetch the fragment of new text that correspond to new span + const changedText = scriptSnapshot.getText(textChangeRange.span.start, textChangeRange.span.start + textChangeRange.newLength); + // combine prefix, changed text and suffix + newText = prefix && suffix + ? prefix + changedText + suffix + : prefix + ? (prefix + changedText) + : (changedText + suffix); + } + const newSourceFile = updateSourceFile(sourceFile, newText, textChangeRange, aggressiveChecks); + setSourceFileFields(newSourceFile, scriptSnapshot, version); + // after incremental parsing nameTable might not be up-to-date + // drop it so it can be lazily recreated later + newSourceFile.nameTable = undefined; + // dispose all resources held by old script snapshot + if (sourceFile !== newSourceFile && sourceFile.scriptSnapshot) { + if (sourceFile.scriptSnapshot.dispose) { + sourceFile.scriptSnapshot.dispose(); } - - return newSourceFile; + sourceFile.scriptSnapshot = undefined; } + return newSourceFile; } - - // Otherwise, just create a new source file. - return createLanguageServiceSourceFile(sourceFile.fileName, scriptSnapshot, sourceFile.languageVersion, version, /*setNodeParents*/ true, sourceFile.scriptKind); } - - class CancellationTokenObject implements CancellationToken { - constructor(private cancellationToken: HostCancellationToken | undefined) { - } - - public isCancellationRequested(): boolean { - return !!this.cancellationToken && this.cancellationToken.isCancellationRequested(); + // Otherwise, just create a new source file. + return createLanguageServiceSourceFile(sourceFile.fileName, scriptSnapshot, sourceFile.languageVersion, version, /*setNodeParents*/ true, sourceFile.scriptKind); +} +class CancellationTokenObject implements CancellationToken { + constructor(private cancellationToken: HostCancellationToken | undefined) { + } + public isCancellationRequested(): boolean { + return !!this.cancellationToken && this.cancellationToken.isCancellationRequested(); + } + public throwIfCancellationRequested(): void { + if (this.isCancellationRequested()) { + throw new OperationCanceledException(); } - - public throwIfCancellationRequested(): void { - if (this.isCancellationRequested()) { - throw new OperationCanceledException(); - } + } +} +/* @internal */ +/** A cancellation that throttles calls to the host */ +export class ThrottledCancellationToken implements CancellationToken { + // Store when we last tried to cancel. Checking cancellation can be expensive (as we have + // to marshall over to the host layer). So we only bother actually checking once enough + // time has passed. + private lastCancellationCheckTime = 0; + constructor(private hostCancellationToken: HostCancellationToken, private readonly throttleWaitMilliseconds = 20) { + } + public isCancellationRequested(): boolean { + const time = timestamp(); + const duration = Math.abs(time - this.lastCancellationCheckTime); + if (duration >= this.throttleWaitMilliseconds) { + // Check no more than once every throttle wait milliseconds + this.lastCancellationCheckTime = time; + return this.hostCancellationToken.isCancellationRequested(); + } + return false; + } + public throwIfCancellationRequested(): void { + if (this.isCancellationRequested()) { + throw new OperationCanceledException(); } } - - /* @internal */ - /** A cancellation that throttles calls to the host */ - export class ThrottledCancellationToken implements CancellationToken { - // Store when we last tried to cancel. Checking cancellation can be expensive (as we have - // to marshall over to the host layer). So we only bother actually checking once enough - // time has passed. - private lastCancellationCheckTime = 0; - - constructor(private hostCancellationToken: HostCancellationToken, private readonly throttleWaitMilliseconds = 20) { - } - - public isCancellationRequested(): boolean { - const time = timestamp(); - const duration = Math.abs(time - this.lastCancellationCheckTime); - if (duration >= this.throttleWaitMilliseconds) { - // Check no more than once every throttle wait milliseconds - this.lastCancellationCheckTime = time; - return this.hostCancellationToken.isCancellationRequested(); - } - - return false; +} +export function createLanguageService(host: LanguageServiceHost, documentRegistry: DocumentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames(), host.getCurrentDirectory()), syntaxOnly = false): LanguageService { + const syntaxTreeCache: SyntaxTreeCache = new SyntaxTreeCache(host); + let program: Program; + let lastProjectVersion: string; + let lastTypesRootVersion = 0; + const cancellationToken = new CancellationTokenObject(host.getCancellationToken && host.getCancellationToken()); + const currentDirectory = host.getCurrentDirectory(); + // Check if the localized messages json is set, otherwise query the host for it + if (!localizedDiagnosticMessages && host.getLocalizedDiagnosticMessages) { + setLocalizedDiagnosticMessages(host.getLocalizedDiagnosticMessages()); + } + function log(message: string) { + if (host.log) { + host.log(message); } - - public throwIfCancellationRequested(): void { - if (this.isCancellationRequested()) { - throw new OperationCanceledException(); - } + } + const useCaseSensitiveFileNames = hostUsesCaseSensitiveFileNames(host); + const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); + const sourceMapper = getSourceMapper({ + useCaseSensitiveFileNames: () => useCaseSensitiveFileNames, + getCurrentDirectory: () => currentDirectory, + getProgram, + fileExists: maybeBind(host, host.fileExists), + readFile: maybeBind(host, host.readFile), + getDocumentPositionMapper: maybeBind(host, host.getDocumentPositionMapper), + getSourceFileLike: maybeBind(host, host.getSourceFileLike), + log + }); + function getValidSourceFile(fileName: string): SourceFile { + const sourceFile = program.getSourceFile(fileName); + if (!sourceFile) { + const error: Error & PossibleProgramFileInfo = new Error(`Could not find source file: '${fileName}'.`); + // We've been having trouble debugging this, so attach sidecar data for the tsserver log. + // See https://github.com/microsoft/TypeScript/issues/30180. + error.ProgramFiles = program.getSourceFiles().map(f => f.fileName); + throw error; } + return sourceFile; } - - export function createLanguageService( - host: LanguageServiceHost, - documentRegistry: DocumentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames(), host.getCurrentDirectory()), - syntaxOnly = false): LanguageService { - - const syntaxTreeCache: SyntaxTreeCache = new SyntaxTreeCache(host); - let program: Program; - let lastProjectVersion: string; - let lastTypesRootVersion = 0; - - const cancellationToken = new CancellationTokenObject(host.getCancellationToken && host.getCancellationToken()); - - const currentDirectory = host.getCurrentDirectory(); - // Check if the localized messages json is set, otherwise query the host for it - if (!localizedDiagnosticMessages && host.getLocalizedDiagnosticMessages) { - setLocalizedDiagnosticMessages(host.getLocalizedDiagnosticMessages()); - } - - function log(message: string) { - if (host.log) { - host.log(message); + function synchronizeHostData(): void { + Debug.assert(!syntaxOnly); + // perform fast check if host supports it + if (host.getProjectVersion) { + const hostProjectVersion = host.getProjectVersion(); + if (hostProjectVersion) { + if (lastProjectVersion === hostProjectVersion && !host.hasChangedAutomaticTypeDirectiveNames) { + return; + } + lastProjectVersion = hostProjectVersion; } } - - const useCaseSensitiveFileNames = hostUsesCaseSensitiveFileNames(host); - const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); - - const sourceMapper = getSourceMapper({ + const typeRootsVersion = host.getTypeRootsVersion ? host.getTypeRootsVersion() : 0; + if (lastTypesRootVersion !== typeRootsVersion) { + log("TypeRoots version has changed; provide new program"); + program = undefined!; // TODO: GH#18217 + lastTypesRootVersion = typeRootsVersion; + } + // Get a fresh cache of the host information + let hostCache: HostCache | undefined = new HostCache(host, getCanonicalFileName); + const rootFileNames = hostCache.getRootFileNames(); + const hasInvalidatedResolution: HasInvalidatedResolution = host.hasInvalidatedResolution || returnFalse; + const projectReferences = hostCache.getProjectReferences(); + // If the program is already up-to-date, we can reuse it + if (isProgramUptoDate(program, rootFileNames, hostCache.compilationSettings(), (_path, fileName) => host.getScriptVersion(fileName), fileExists, hasInvalidatedResolution, !!host.hasChangedAutomaticTypeDirectiveNames, projectReferences)) { + return; + } + // IMPORTANT - It is critical from this moment onward that we do not check + // cancellation tokens. We are about to mutate source files from a previous program + // instance. If we cancel midway through, we may end up in an inconsistent state where + // the program points to old source files that have been invalidated because of + // incremental parsing. + const newSettings = hostCache.compilationSettings(); + // Now create a new compiler + const compilerHost: CompilerHost = { + getSourceFile: getOrCreateSourceFile, + getSourceFileByPath: getOrCreateSourceFileByPath, + getCancellationToken: () => cancellationToken, + getCanonicalFileName, useCaseSensitiveFileNames: () => useCaseSensitiveFileNames, + getNewLine: () => getNewLineCharacter(newSettings, () => getNewLineOrDefaultFromHost(host)), + getDefaultLibFileName: (options) => host.getDefaultLibFileName(options), + writeFile: noop, getCurrentDirectory: () => currentDirectory, - getProgram, - fileExists: maybeBind(host, host.fileExists), - readFile: maybeBind(host, host.readFile), - getDocumentPositionMapper: maybeBind(host, host.getDocumentPositionMapper), - getSourceFileLike: maybeBind(host, host.getSourceFileLike), - log - }); - - function getValidSourceFile(fileName: string): SourceFile { - const sourceFile = program.getSourceFile(fileName); - if (!sourceFile) { - const error: Error & PossibleProgramFileInfo = new Error(`Could not find source file: '${fileName}'.`); - - // We've been having trouble debugging this, so attach sidecar data for the tsserver log. - // See https://github.com/microsoft/TypeScript/issues/30180. - error.ProgramFiles = program.getSourceFiles().map(f => f.fileName); - - throw error; - } - return sourceFile; - } - - function synchronizeHostData(): void { - Debug.assert(!syntaxOnly); - - // perform fast check if host supports it - if (host.getProjectVersion) { - const hostProjectVersion = host.getProjectVersion(); - if (hostProjectVersion) { - if (lastProjectVersion === hostProjectVersion && !host.hasChangedAutomaticTypeDirectiveNames) { - return; - } - - lastProjectVersion = hostProjectVersion; - } - } - - const typeRootsVersion = host.getTypeRootsVersion ? host.getTypeRootsVersion() : 0; - if (lastTypesRootVersion !== typeRootsVersion) { - log("TypeRoots version has changed; provide new program"); - program = undefined!; // TODO: GH#18217 - lastTypesRootVersion = typeRootsVersion; - } - - // Get a fresh cache of the host information - let hostCache: HostCache | undefined = new HostCache(host, getCanonicalFileName); - const rootFileNames = hostCache.getRootFileNames(); - - const hasInvalidatedResolution: HasInvalidatedResolution = host.hasInvalidatedResolution || returnFalse; - const projectReferences = hostCache.getProjectReferences(); - - // If the program is already up-to-date, we can reuse it - if (isProgramUptoDate(program, rootFileNames, hostCache.compilationSettings(), (_path, fileName) => host.getScriptVersion(fileName), fileExists, hasInvalidatedResolution, !!host.hasChangedAutomaticTypeDirectiveNames, projectReferences)) { - return; - } - - // IMPORTANT - It is critical from this moment onward that we do not check - // cancellation tokens. We are about to mutate source files from a previous program - // instance. If we cancel midway through, we may end up in an inconsistent state where - // the program points to old source files that have been invalidated because of - // incremental parsing. - - const newSettings = hostCache.compilationSettings(); - - // Now create a new compiler - const compilerHost: CompilerHost = { - getSourceFile: getOrCreateSourceFile, - getSourceFileByPath: getOrCreateSourceFileByPath, - getCancellationToken: () => cancellationToken, - getCanonicalFileName, - useCaseSensitiveFileNames: () => useCaseSensitiveFileNames, - getNewLine: () => getNewLineCharacter(newSettings, () => getNewLineOrDefaultFromHost(host)), - getDefaultLibFileName: (options) => host.getDefaultLibFileName(options), - writeFile: noop, - getCurrentDirectory: () => currentDirectory, - fileExists, - readFile, - realpath: host.realpath && (path => host.realpath!(path)), - directoryExists: directoryName => { - return directoryProbablyExists(directoryName, host); - }, - getDirectories: path => { - return host.getDirectories ? host.getDirectories(path) : []; - }, - readDirectory(path, extensions, exclude, include, depth) { - Debug.checkDefined(host.readDirectory, "'LanguageServiceHost.readDirectory' must be implemented to correctly process 'projectReferences'"); - return host.readDirectory!(path, extensions, exclude, include, depth); - }, - onReleaseOldSourceFile, - hasInvalidatedResolution, - hasChangedAutomaticTypeDirectiveNames: host.hasChangedAutomaticTypeDirectiveNames - }; - if (host.trace) { - compilerHost.trace = message => host.trace!(message); - } - - if (host.resolveModuleNames) { - compilerHost.resolveModuleNames = (...args) => host.resolveModuleNames!(...args); - } - if (host.resolveTypeReferenceDirectives) { - compilerHost.resolveTypeReferenceDirectives = (...args) => host.resolveTypeReferenceDirectives!(...args); - } - if (host.setResolvedProjectReferenceCallbacks) { - compilerHost.setResolvedProjectReferenceCallbacks = callbacks => host.setResolvedProjectReferenceCallbacks!(callbacks); - } - if (host.useSourceOfProjectReferenceRedirect) { - compilerHost.useSourceOfProjectReferenceRedirect = () => host.useSourceOfProjectReferenceRedirect!(); - } - - const documentRegistryBucketKey = documentRegistry.getKeyForCompilationSettings(newSettings); - const options: CreateProgramOptions = { - rootNames: rootFileNames, - options: newSettings, - host: compilerHost, - oldProgram: program, - projectReferences - }; - program = createProgram(options); - - // hostCache is captured in the closure for 'getOrCreateSourceFile' but it should not be used past this point. - // It needs to be cleared to allow all collected snapshots to be released - hostCache = undefined; - - // We reset this cache on structure invalidation so we don't hold on to outdated files for long; however we can't use the `compilerHost` above, - // Because it only functions until `hostCache` is cleared, while we'll potentially need the functionality to lazily read sourcemap files during - // the course of whatever called `synchronizeHostData` - sourceMapper.clearCache(); - - // Make sure all the nodes in the program are both bound, and have their parent - // pointers set property. - program.getTypeChecker(); - return; - - function fileExists(fileName: string): boolean { - const path = toPath(fileName, currentDirectory, getCanonicalFileName); - const entry = hostCache && hostCache.getEntryByPath(path); - return entry ? - !isString(entry) : - (!!host.fileExists && host.fileExists(fileName)); - } - - function readFile(fileName: string) { - // stub missing host functionality - const path = toPath(fileName, currentDirectory, getCanonicalFileName); - const entry = hostCache && hostCache.getEntryByPath(path); - if (entry) { - return isString(entry) ? undefined : getSnapshotText(entry.scriptSnapshot); - } - return host.readFile && host.readFile(fileName); - } - - // Release any files we have acquired in the old program but are - // not part of the new program. - function onReleaseOldSourceFile(oldSourceFile: SourceFile, oldOptions: CompilerOptions) { - const oldSettingsKey = documentRegistry.getKeyForCompilationSettings(oldOptions); - documentRegistry.releaseDocumentWithKey(oldSourceFile.resolvedPath, oldSettingsKey); - } - - function getOrCreateSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile | undefined { - return getOrCreateSourceFileByPath(fileName, toPath(fileName, currentDirectory, getCanonicalFileName), languageVersion, onError, shouldCreateNewSourceFile); + fileExists, + readFile, + realpath: host.realpath && (path => host.realpath!(path)), + directoryExists: directoryName => { + return directoryProbablyExists(directoryName, host); + }, + getDirectories: path => { + return host.getDirectories ? host.getDirectories(path) : []; + }, + readDirectory(path, extensions, exclude, include, depth) { + Debug.checkDefined(host.readDirectory, "'LanguageServiceHost.readDirectory' must be implemented to correctly process 'projectReferences'"); + return host.readDirectory!(path, extensions, exclude, include, depth); + }, + onReleaseOldSourceFile, + hasInvalidatedResolution, + hasChangedAutomaticTypeDirectiveNames: host.hasChangedAutomaticTypeDirectiveNames + }; + if (host.trace) { + compilerHost.trace = message => host.trace!(message); + } + if (host.resolveModuleNames) { + compilerHost.resolveModuleNames = (...args) => host.resolveModuleNames!(...args); + } + if (host.resolveTypeReferenceDirectives) { + compilerHost.resolveTypeReferenceDirectives = (...args) => host.resolveTypeReferenceDirectives!(...args); + } + if (host.setResolvedProjectReferenceCallbacks) { + compilerHost.setResolvedProjectReferenceCallbacks = callbacks => host.setResolvedProjectReferenceCallbacks!(callbacks); + } + if (host.useSourceOfProjectReferenceRedirect) { + compilerHost.useSourceOfProjectReferenceRedirect = () => host.useSourceOfProjectReferenceRedirect!(); + } + const documentRegistryBucketKey = documentRegistry.getKeyForCompilationSettings(newSettings); + const options: CreateProgramOptions = { + rootNames: rootFileNames, + options: newSettings, + host: compilerHost, + oldProgram: program, + projectReferences + }; + program = createProgram(options); + // hostCache is captured in the closure for 'getOrCreateSourceFile' but it should not be used past this point. + // It needs to be cleared to allow all collected snapshots to be released + hostCache = undefined; + // We reset this cache on structure invalidation so we don't hold on to outdated files for long; however we can't use the `compilerHost` above, + // Because it only functions until `hostCache` is cleared, while we'll potentially need the functionality to lazily read sourcemap files during + // the course of whatever called `synchronizeHostData` + sourceMapper.clearCache(); + // Make sure all the nodes in the program are both bound, and have their parent + // pointers set property. + program.getTypeChecker(); + return; + function fileExists(fileName: string): boolean { + const path = toPath(fileName, currentDirectory, getCanonicalFileName); + const entry = hostCache && hostCache.getEntryByPath(path); + return entry ? + !isString(entry) : + (!!host.fileExists && host.fileExists(fileName)); + } + function readFile(fileName: string) { + // stub missing host functionality + const path = toPath(fileName, currentDirectory, getCanonicalFileName); + const entry = hostCache && hostCache.getEntryByPath(path); + if (entry) { + return isString(entry) ? undefined : getSnapshotText(entry.scriptSnapshot); + } + return host.readFile && host.readFile(fileName); + } + // Release any files we have acquired in the old program but are + // not part of the new program. + function onReleaseOldSourceFile(oldSourceFile: SourceFile, oldOptions: CompilerOptions) { + const oldSettingsKey = documentRegistry.getKeyForCompilationSettings(oldOptions); + documentRegistry.releaseDocumentWithKey(oldSourceFile.resolvedPath, oldSettingsKey); + } + function getOrCreateSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile | undefined { + return getOrCreateSourceFileByPath(fileName, toPath(fileName, currentDirectory, getCanonicalFileName), languageVersion, onError, shouldCreateNewSourceFile); + } + function getOrCreateSourceFileByPath(fileName: string, path: Path, _languageVersion: ScriptTarget, _onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile | undefined { + Debug.assert(hostCache !== undefined, "getOrCreateSourceFileByPath called after typical CompilerHost lifetime, check the callstack something with a reference to an old host."); + // The program is asking for this file, check first if the host can locate it. + // If the host can not locate the file, then it does not exist. return undefined + // to the program to allow reporting of errors for missing files. + const hostFileInformation = hostCache && hostCache.getOrCreateEntryByPath(fileName, path); + if (!hostFileInformation) { + return undefined; } - - function getOrCreateSourceFileByPath(fileName: string, path: Path, _languageVersion: ScriptTarget, _onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile | undefined { - Debug.assert(hostCache !== undefined, "getOrCreateSourceFileByPath called after typical CompilerHost lifetime, check the callstack something with a reference to an old host."); - // The program is asking for this file, check first if the host can locate it. - // If the host can not locate the file, then it does not exist. return undefined - // to the program to allow reporting of errors for missing files. - const hostFileInformation = hostCache && hostCache.getOrCreateEntryByPath(fileName, path); - if (!hostFileInformation) { - return undefined; - } - - // Check if the language version has changed since we last created a program; if they are the same, - // it is safe to reuse the sourceFiles; if not, then the shape of the AST can change, and the oldSourceFile - // can not be reused. we have to dump all syntax trees and create new ones. - if (!shouldCreateNewSourceFile) { - // Check if the old program had this file already - const oldSourceFile = program && program.getSourceFileByPath(path); - if (oldSourceFile) { - // We already had a source file for this file name. Go to the registry to - // ensure that we get the right up to date version of it. We need this to - // address the following race-condition. Specifically, say we have the following: - // - // LS1 - // \ - // DocumentRegistry - // / - // LS2 - // - // Each LS has a reference to file 'foo.ts' at version 1. LS2 then updates - // it's version of 'foo.ts' to version 2. This will cause LS2 and the - // DocumentRegistry to have version 2 of the document. HOwever, LS1 will - // have version 1. And *importantly* this source file will be *corrupt*. - // The act of creating version 2 of the file irrevocably damages the version - // 1 file. - // - // So, later when we call into LS1, we need to make sure that it doesn't use - // it's source file any more, and instead defers to DocumentRegistry to get - // either version 1, version 2 (or some other version) depending on what the - // host says should be used. - - // We do not support the scenario where a host can modify a registered - // file's script kind, i.e. in one project some file is treated as ".ts" - // and in another as ".js" - Debug.assertEqual(hostFileInformation.scriptKind, oldSourceFile.scriptKind, "Registered script kind should match new script kind.", path); - - return documentRegistry.updateDocumentWithKey(fileName, path, newSettings, documentRegistryBucketKey, hostFileInformation.scriptSnapshot, hostFileInformation.version, hostFileInformation.scriptKind); - } - - // We didn't already have the file. Fall through and acquire it from the registry. + // Check if the language version has changed since we last created a program; if they are the same, + // it is safe to reuse the sourceFiles; if not, then the shape of the AST can change, and the oldSourceFile + // can not be reused. we have to dump all syntax trees and create new ones. + if (!shouldCreateNewSourceFile) { + // Check if the old program had this file already + const oldSourceFile = program && program.getSourceFileByPath(path); + if (oldSourceFile) { + // We already had a source file for this file name. Go to the registry to + // ensure that we get the right up to date version of it. We need this to + // address the following race-condition. Specifically, say we have the following: + // + // LS1 + // \ + // DocumentRegistry + // / + // LS2 + // + // Each LS has a reference to file 'foo.ts' at version 1. LS2 then updates + // it's version of 'foo.ts' to version 2. This will cause LS2 and the + // DocumentRegistry to have version 2 of the document. HOwever, LS1 will + // have version 1. And *importantly* this source file will be *corrupt*. + // The act of creating version 2 of the file irrevocably damages the version + // 1 file. + // + // So, later when we call into LS1, we need to make sure that it doesn't use + // it's source file any more, and instead defers to DocumentRegistry to get + // either version 1, version 2 (or some other version) depending on what the + // host says should be used. + // We do not support the scenario where a host can modify a registered + // file's script kind, i.e. in one project some file is treated as ".ts" + // and in another as ".js" + Debug.assertEqual(hostFileInformation.scriptKind, oldSourceFile.scriptKind, "Registered script kind should match new script kind.", path); + return documentRegistry.updateDocumentWithKey(fileName, path, newSettings, documentRegistryBucketKey, hostFileInformation.scriptSnapshot, hostFileInformation.version, hostFileInformation.scriptKind); } - - // Could not find this file in the old program, create a new SourceFile for it. - return documentRegistry.acquireDocumentWithKey(fileName, path, newSettings, documentRegistryBucketKey, hostFileInformation.scriptSnapshot, hostFileInformation.version, hostFileInformation.scriptKind); + // We didn't already have the file. Fall through and acquire it from the registry. } + // Could not find this file in the old program, create a new SourceFile for it. + return documentRegistry.acquireDocumentWithKey(fileName, path, newSettings, documentRegistryBucketKey, hostFileInformation.scriptSnapshot, hostFileInformation.version, hostFileInformation.scriptKind); } - - // TODO: GH#18217 frequently asserted as defined - function getProgram(): Program | undefined { - if (syntaxOnly) { - Debug.assert(program === undefined); - return undefined; - } - - synchronizeHostData(); - - return program; + } + // TODO: GH#18217 frequently asserted as defined + function getProgram(): Program | undefined { + if (syntaxOnly) { + Debug.assert(program === undefined); + return undefined; } - - function cleanupSemanticCache(): void { + synchronizeHostData(); + return program; + } + function cleanupSemanticCache(): void { + program = undefined!; // TODO: GH#18217 + } + function dispose(): void { + if (program) { + forEach(program.getSourceFiles(), f => documentRegistry.releaseDocument(f.fileName, program.getCompilerOptions())); program = undefined!; // TODO: GH#18217 } - - function dispose(): void { - if (program) { - forEach(program.getSourceFiles(), f => - documentRegistry.releaseDocument(f.fileName, program.getCompilerOptions())); - program = undefined!; // TODO: GH#18217 - } - host = undefined!; - } - - /// Diagnostics - function getSyntacticDiagnostics(fileName: string): DiagnosticWithLocation[] { - synchronizeHostData(); - - return program.getSyntacticDiagnostics(getValidSourceFile(fileName), cancellationToken).slice(); - } - - /** - * getSemanticDiagnostics return array of Diagnostics. If '-d' is not enabled, only report semantic errors - * If '-d' enabled, report both semantic and emitter errors - */ - function getSemanticDiagnostics(fileName: string): Diagnostic[] { - synchronizeHostData(); - - const targetSourceFile = getValidSourceFile(fileName); - - // Only perform the action per file regardless of '-out' flag as LanguageServiceHost is expected to call this function per file. - // Therefore only get diagnostics for given file. - - const semanticDiagnostics = program.getSemanticDiagnostics(targetSourceFile, cancellationToken); - if (!getEmitDeclarations(program.getCompilerOptions())) { - return semanticDiagnostics.slice(); - } - - // If '-d' is enabled, check for emitter error. One example of emitter error is export class implements non-export interface - const declarationDiagnostics = program.getDeclarationDiagnostics(targetSourceFile, cancellationToken); - return [...semanticDiagnostics, ...declarationDiagnostics]; - } - - function getSuggestionDiagnostics(fileName: string): DiagnosticWithLocation[] { - synchronizeHostData(); - return computeSuggestionDiagnostics(getValidSourceFile(fileName), program, cancellationToken); - } - - function getCompilerOptionsDiagnostics() { - synchronizeHostData(); - return [...program.getOptionsDiagnostics(cancellationToken), ...program.getGlobalDiagnostics(cancellationToken)]; - } - - function getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions = emptyOptions): CompletionInfo | undefined { - // Convert from deprecated options names to new names - const fullPreferences: UserPreferences = { - ...identity(options), // avoid excess property check - includeCompletionsForModuleExports: options.includeCompletionsForModuleExports || options.includeExternalModuleExports, - includeCompletionsWithInsertText: options.includeCompletionsWithInsertText || options.includeInsertTextCompletions, - }; - synchronizeHostData(); - return Completions.getCompletionsAtPosition( - host, - program, - log, - getValidSourceFile(fileName), - position, - fullPreferences, - options.triggerCharacter); - } - - function getCompletionEntryDetails(fileName: string, position: number, name: string, formattingOptions: FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences = emptyOptions): CompletionEntryDetails | undefined { - synchronizeHostData(); - return Completions.getCompletionEntryDetails( - program, - log, - getValidSourceFile(fileName), - position, - { name, source }, - host, - (formattingOptions && formatting.getFormatContext(formattingOptions))!, // TODO: GH#18217 - preferences, - cancellationToken, - ); - } - - function getCompletionEntrySymbol(fileName: string, position: number, name: string, source?: string, preferences: UserPreferences = emptyOptions): Symbol | undefined { - synchronizeHostData(); - return Completions.getCompletionEntrySymbol(program, log, getValidSourceFile(fileName), position, { name, source }, host, preferences); - } - - function getQuickInfoAtPosition(fileName: string, position: number): QuickInfo | undefined { - synchronizeHostData(); - - const sourceFile = getValidSourceFile(fileName); - const node = getTouchingPropertyName(sourceFile, position); - if (node === sourceFile) { - // Avoid giving quickInfo for the sourceFile as a whole. - return undefined; - } - - const typeChecker = program.getTypeChecker(); - const nodeForQuickInfo = getNodeForQuickInfo(node); - const symbol = getSymbolAtLocationForQuickInfo(nodeForQuickInfo, typeChecker); - - if (!symbol || typeChecker.isUnknownSymbol(symbol)) { - const type = shouldGetType(sourceFile, nodeForQuickInfo, position) ? typeChecker.getTypeAtLocation(nodeForQuickInfo) : undefined; - return type && { - kind: ScriptElementKind.unknown, - kindModifiers: ScriptElementKindModifier.none, - textSpan: createTextSpanFromNode(nodeForQuickInfo, sourceFile), - displayParts: typeChecker.runWithCancellationToken(cancellationToken, typeChecker => typeToDisplayParts(typeChecker, type, getContainerNode(nodeForQuickInfo))), - documentation: type.symbol ? type.symbol.getDocumentationComment(typeChecker) : undefined, - tags: type.symbol ? type.symbol.getJsDocTags() : undefined - }; - } - - const { symbolKind, displayParts, documentation, tags } = typeChecker.runWithCancellationToken(cancellationToken, typeChecker => - SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, sourceFile, getContainerNode(nodeForQuickInfo), nodeForQuickInfo) - ); - return { - kind: symbolKind, - kindModifiers: SymbolDisplay.getSymbolModifiers(symbol), + host = undefined!; + } + /// Diagnostics + function getSyntacticDiagnostics(fileName: string): DiagnosticWithLocation[] { + synchronizeHostData(); + return program.getSyntacticDiagnostics(getValidSourceFile(fileName), cancellationToken).slice(); + } + /** + * getSemanticDiagnostics return array of Diagnostics. If '-d' is not enabled, only report semantic errors + * If '-d' enabled, report both semantic and emitter errors + */ + function getSemanticDiagnostics(fileName: string): Diagnostic[] { + synchronizeHostData(); + const targetSourceFile = getValidSourceFile(fileName); + // Only perform the action per file regardless of '-out' flag as LanguageServiceHost is expected to call this function per file. + // Therefore only get diagnostics for given file. + const semanticDiagnostics = program.getSemanticDiagnostics(targetSourceFile, cancellationToken); + if (!getEmitDeclarations(program.getCompilerOptions())) { + return semanticDiagnostics.slice(); + } + // If '-d' is enabled, check for emitter error. One example of emitter error is export class implements non-export interface + const declarationDiagnostics = program.getDeclarationDiagnostics(targetSourceFile, cancellationToken); + return [...semanticDiagnostics, ...declarationDiagnostics]; + } + function getSuggestionDiagnostics(fileName: string): DiagnosticWithLocation[] { + synchronizeHostData(); + return computeSuggestionDiagnostics(getValidSourceFile(fileName), program, cancellationToken); + } + function getCompilerOptionsDiagnostics() { + synchronizeHostData(); + return [...program.getOptionsDiagnostics(cancellationToken), ...program.getGlobalDiagnostics(cancellationToken)]; + } + function getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions = emptyOptions): CompletionInfo | undefined { + // Convert from deprecated options names to new names + const fullPreferences: UserPreferences = { + ...identity(options), + includeCompletionsForModuleExports: options.includeCompletionsForModuleExports || options.includeExternalModuleExports, + includeCompletionsWithInsertText: options.includeCompletionsWithInsertText || options.includeInsertTextCompletions, + }; + synchronizeHostData(); + return Completions.getCompletionsAtPosition(host, program, log, getValidSourceFile(fileName), position, fullPreferences, options.triggerCharacter); + } + function getCompletionEntryDetails(fileName: string, position: number, name: string, formattingOptions: FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences = emptyOptions): CompletionEntryDetails | undefined { + synchronizeHostData(); + return Completions.getCompletionEntryDetails(program, log, getValidSourceFile(fileName), position, { name, source }, host, ((formattingOptions && getFormatContext(formattingOptions))!), // TODO: GH#18217 + preferences, cancellationToken); + } + function getCompletionEntrySymbol(fileName: string, position: number, name: string, source?: string, preferences: UserPreferences = emptyOptions): Symbol | undefined { + synchronizeHostData(); + return Completions.getCompletionEntrySymbol(program, log, getValidSourceFile(fileName), position, { name, source }, host, preferences); + } + function getQuickInfoAtPosition(fileName: string, position: number): QuickInfo | undefined { + synchronizeHostData(); + const sourceFile = getValidSourceFile(fileName); + const node = getTouchingPropertyName(sourceFile, position); + if (node === sourceFile) { + // Avoid giving quickInfo for the sourceFile as a whole. + return undefined; + } + const typeChecker = program.getTypeChecker(); + const nodeForQuickInfo = getNodeForQuickInfo(node); + const symbol = getSymbolAtLocationForQuickInfo(nodeForQuickInfo, typeChecker); + if (!symbol || typeChecker.isUnknownSymbol(symbol)) { + const type = shouldGetType(sourceFile, nodeForQuickInfo, position) ? typeChecker.getTypeAtLocation(nodeForQuickInfo) : undefined; + return type && { + kind: ScriptElementKind.unknown, + kindModifiers: ScriptElementKindModifier.none, textSpan: createTextSpanFromNode(nodeForQuickInfo, sourceFile), - displayParts, - documentation, - tags, + displayParts: typeChecker.runWithCancellationToken(cancellationToken, typeChecker => typeToDisplayParts(typeChecker, type, getContainerNode(nodeForQuickInfo))), + documentation: type.symbol ? type.symbol.getDocumentationComment(typeChecker) : undefined, + tags: type.symbol ? type.symbol.getJsDocTags() : undefined }; } - - function getNodeForQuickInfo(node: Node): Node { - if (isNewExpression(node.parent) && node.pos === node.parent.pos) { - return node.parent.expression; - } - return node; + const { symbolKind, displayParts, documentation, tags } = typeChecker.runWithCancellationToken(cancellationToken, typeChecker => getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, sourceFile, getContainerNode(nodeForQuickInfo), nodeForQuickInfo)); + return { + kind: symbolKind, + kindModifiers: getSymbolModifiers(symbol), + textSpan: createTextSpanFromNode(nodeForQuickInfo, sourceFile), + displayParts, + documentation, + tags, + }; + } + function getNodeForQuickInfo(node: Node): Node { + if (isNewExpression(node.parent) && node.pos === node.parent.pos) { + return node.parent.expression; } - - function shouldGetType(sourceFile: SourceFile, node: Node, position: number): boolean { - switch (node.kind) { - case SyntaxKind.Identifier: - return !isLabelName(node) && !isTagName(node); - case SyntaxKind.PropertyAccessExpression: - case SyntaxKind.QualifiedName: - // Don't return quickInfo if inside the comment in `a/**/.b` - return !isInComment(sourceFile, position); - case SyntaxKind.ThisKeyword: - case SyntaxKind.ThisType: - case SyntaxKind.SuperKeyword: - return true; - default: - return false; - } + return node; + } + function shouldGetType(sourceFile: SourceFile, node: Node, position: number): boolean { + switch (node.kind) { + case SyntaxKind.Identifier: + return !isLabelName(node) && !isTagName(node); + case SyntaxKind.PropertyAccessExpression: + case SyntaxKind.QualifiedName: + // Don't return quickInfo if inside the comment in `a/**/.b` + return !isInComment(sourceFile, position); + case SyntaxKind.ThisKeyword: + case SyntaxKind.ThisType: + case SyntaxKind.SuperKeyword: + return true; + default: + return false; } - - /// Goto definition - function getDefinitionAtPosition(fileName: string, position: number): readonly DefinitionInfo[] | undefined { - synchronizeHostData(); - return GoToDefinition.getDefinitionAtPosition(program, getValidSourceFile(fileName), position); - } - - function getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan | undefined { - synchronizeHostData(); - return GoToDefinition.getDefinitionAndBoundSpan(program, getValidSourceFile(fileName), position); - } - - function getTypeDefinitionAtPosition(fileName: string, position: number): readonly DefinitionInfo[] | undefined { - synchronizeHostData(); - return GoToDefinition.getTypeDefinitionAtPosition(program.getTypeChecker(), getValidSourceFile(fileName), position); - } - - /// Goto implementation - - function getImplementationAtPosition(fileName: string, position: number): ImplementationLocation[] | undefined { - synchronizeHostData(); - return FindAllReferences.getImplementationsAtPosition(program, cancellationToken, program.getSourceFiles(), getValidSourceFile(fileName), position); - } - - /// References and Occurrences - function getOccurrencesAtPosition(fileName: string, position: number): readonly ReferenceEntry[] | undefined { - return flatMap( - getDocumentHighlights(fileName, position, [fileName]), - entry => entry.highlightSpans.map(highlightSpan => ({ - fileName: entry.fileName, - textSpan: highlightSpan.textSpan, - isWriteAccess: highlightSpan.kind === HighlightSpanKind.writtenReference, - isDefinition: false, - ...highlightSpan.isInString && { isInString: true }, - ...highlightSpan.contextSpan && { contextSpan: highlightSpan.contextSpan } - })) - ); - } - - function getDocumentHighlights(fileName: string, position: number, filesToSearch: readonly string[]): DocumentHighlights[] | undefined { - const normalizedFileName = normalizePath(fileName); - Debug.assert(filesToSearch.some(f => normalizePath(f) === normalizedFileName)); - synchronizeHostData(); - const sourceFilesToSearch = mapDefined(filesToSearch, fileName => program.getSourceFile(fileName)); - const sourceFile = getValidSourceFile(fileName); - return DocumentHighlights.getDocumentHighlights(program, cancellationToken, sourceFile, position, sourceFilesToSearch); - } - - function findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): RenameLocation[] | undefined { - synchronizeHostData(); - const sourceFile = getValidSourceFile(fileName); - const node = getAdjustedRenameLocation(getTouchingPropertyName(sourceFile, position)); - if (isIdentifier(node) && (isJsxOpeningElement(node.parent) || isJsxClosingElement(node.parent)) && isIntrinsicJsxName(node.escapedText)) { - const { openingElement, closingElement } = node.parent.parent; - return [openingElement, closingElement].map((node): RenameLocation => { - const textSpan = createTextSpanFromNode(node.tagName, sourceFile); - return { - fileName: sourceFile.fileName, - textSpan, - ...FindAllReferences.toContextSpan(textSpan, sourceFile, node.parent) - }; - }); - } - else { - return getReferencesWorker(node, position, { findInStrings, findInComments, providePrefixAndSuffixTextForRename, use: FindAllReferences.FindReferencesUse.Rename }, - (entry, originalNode, checker) => FindAllReferences.toRenameLocation(entry, originalNode, checker, providePrefixAndSuffixTextForRename || false)); - } + } + /// Goto definition + function getDefinitionAtPosition(fileName: string, position: number): readonly DefinitionInfo[] | undefined { + synchronizeHostData(); + return GoToDefinition.getDefinitionAtPosition(program, getValidSourceFile(fileName), position); + } + function getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan | undefined { + synchronizeHostData(); + return GoToDefinition.getDefinitionAndBoundSpan(program, getValidSourceFile(fileName), position); + } + function getTypeDefinitionAtPosition(fileName: string, position: number): readonly DefinitionInfo[] | undefined { + synchronizeHostData(); + return GoToDefinition.getTypeDefinitionAtPosition(program.getTypeChecker(), getValidSourceFile(fileName), position); + } + /// Goto implementation + function getImplementationAtPosition(fileName: string, position: number): ImplementationLocation[] | undefined { + synchronizeHostData(); + return getImplementationsAtPosition(program, cancellationToken, program.getSourceFiles(), getValidSourceFile(fileName), position); + } + /// References and Occurrences + function getOccurrencesAtPosition(fileName: string, position: number): readonly ReferenceEntry[] | undefined { + return flatMap(getDocumentHighlights(fileName, position, [fileName]), entry => entry.highlightSpans.map(highlightSpan => ({ + fileName: entry.fileName, + textSpan: highlightSpan.textSpan, + isWriteAccess: highlightSpan.kind === HighlightSpanKind.writtenReference, + isDefinition: false, + ...highlightSpan.isInString && { isInString: true }, + ...highlightSpan.contextSpan && { contextSpan: highlightSpan.contextSpan } + }))); + } + function getDocumentHighlights(fileName: string, position: number, filesToSearch: readonly string[]): DocumentHighlights[] | undefined { + const normalizedFileName = normalizePath(fileName); + Debug.assert(filesToSearch.some(f => normalizePath(f) === normalizedFileName)); + synchronizeHostData(); + const sourceFilesToSearch = mapDefined(filesToSearch, fileName => program.getSourceFile(fileName)); + const sourceFile = getValidSourceFile(fileName); + return DocumentHighlights.getDocumentHighlights(program, cancellationToken, sourceFile, position, sourceFilesToSearch); + } + function findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): RenameLocation[] | undefined { + synchronizeHostData(); + const sourceFile = getValidSourceFile(fileName); + const node = getAdjustedRenameLocation(getTouchingPropertyName(sourceFile, position)); + if (isIdentifier(node) && (isJsxOpeningElement(node.parent) || isJsxClosingElement(node.parent)) && isIntrinsicJsxName(node.escapedText)) { + const { openingElement, closingElement } = node.parent.parent; + return [openingElement, closingElement].map((node): RenameLocation => { + const textSpan = createTextSpanFromNode(node.tagName, sourceFile); + return { + fileName: sourceFile.fileName, + textSpan, + ...toContextSpan(textSpan, sourceFile, node.parent) + }; + }); + } + else { + return getReferencesWorker(node, position, { findInStrings, findInComments, providePrefixAndSuffixTextForRename, use: FindReferencesUse.Rename }, (entry, originalNode, checker) => toRenameLocation(entry, originalNode, checker, providePrefixAndSuffixTextForRename || false)); } - - function getReferencesAtPosition(fileName: string, position: number): ReferenceEntry[] | undefined { - synchronizeHostData(); - return getReferencesWorker(getTouchingPropertyName(getValidSourceFile(fileName), position), position, { use: FindAllReferences.FindReferencesUse.References }, FindAllReferences.toReferenceEntry); - } - - function getReferencesWorker(node: Node, position: number, options: FindAllReferences.Options, cb: FindAllReferences.ToReferenceOrRenameEntry): T[] | undefined { - synchronizeHostData(); - - // Exclude default library when renaming as commonly user don't want to change that file. - const sourceFiles = options && options.use === FindAllReferences.FindReferencesUse.Rename - ? program.getSourceFiles().filter(sourceFile => !program.isSourceFileDefaultLibrary(sourceFile)) - : program.getSourceFiles(); - - return FindAllReferences.findReferenceOrRenameEntries(program, cancellationToken, sourceFiles, node, position, options, cb); - } - - function findReferences(fileName: string, position: number): ReferencedSymbol[] | undefined { - synchronizeHostData(); - return FindAllReferences.findReferencedSymbols(program, cancellationToken, program.getSourceFiles(), getValidSourceFile(fileName), position); - } - - function getNavigateToItems(searchValue: string, maxResultCount?: number, fileName?: string, excludeDtsFiles = false): NavigateToItem[] { - synchronizeHostData(); - const sourceFiles = fileName ? [getValidSourceFile(fileName)] : program.getSourceFiles(); - return NavigateTo.getNavigateToItems(sourceFiles, program.getTypeChecker(), cancellationToken, searchValue, maxResultCount, excludeDtsFiles); - } - - function getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, forceDtsEmit?: boolean) { - synchronizeHostData(); - - const sourceFile = getValidSourceFile(fileName); - const customTransformers = host.getCustomTransformers && host.getCustomTransformers(); - return getFileEmitOutput(program, sourceFile, !!emitOnlyDtsFiles, cancellationToken, customTransformers, forceDtsEmit); - } - - // Signature help - /** - * This is a semantic operation. - */ - function getSignatureHelpItems(fileName: string, position: number, { triggerReason }: SignatureHelpItemsOptions = emptyOptions): SignatureHelpItems | undefined { - synchronizeHostData(); - - const sourceFile = getValidSourceFile(fileName); - - return SignatureHelp.getSignatureHelpItems(program, sourceFile, position, triggerReason, cancellationToken); - } - - /// Syntactic features - function getNonBoundSourceFile(fileName: string): SourceFile { - return syntaxTreeCache.getCurrentSourceFile(fileName); - } - - function getNameOrDottedNameSpan(fileName: string, startPos: number, _endPos: number): TextSpan | undefined { - const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); - - // Get node at the location - const node = getTouchingPropertyName(sourceFile, startPos); - - if (node === sourceFile) { + } + function getReferencesAtPosition(fileName: string, position: number): ReferenceEntry[] | undefined { + synchronizeHostData(); + return getReferencesWorker(getTouchingPropertyName(getValidSourceFile(fileName), position), position, { use: FindReferencesUse.References }, toReferenceEntry); + } + function getReferencesWorker(node: Node, position: number, options: Options, cb: ToReferenceOrRenameEntry): T[] | undefined { + synchronizeHostData(); + // Exclude default library when renaming as commonly user don't want to change that file. + const sourceFiles = options && options.use === FindReferencesUse.Rename + ? program.getSourceFiles().filter(sourceFile => !program.isSourceFileDefaultLibrary(sourceFile)) + : program.getSourceFiles(); + return findReferenceOrRenameEntries(program, cancellationToken, sourceFiles, node, position, options, cb); + } + function findReferences(fileName: string, position: number): ReferencedSymbol[] | undefined { + synchronizeHostData(); + return findReferencedSymbols(program, cancellationToken, program.getSourceFiles(), getValidSourceFile(fileName), position); + } + function getNavigateToItems(searchValue: string, maxResultCount?: number, fileName?: string, excludeDtsFiles = false): NavigateToItem[] { + synchronizeHostData(); + const sourceFiles = fileName ? [getValidSourceFile(fileName)] : program.getSourceFiles(); + return NavigateTo.getNavigateToItems(sourceFiles, program.getTypeChecker(), cancellationToken, searchValue, maxResultCount, excludeDtsFiles); + } + function getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, forceDtsEmit?: boolean) { + synchronizeHostData(); + const sourceFile = getValidSourceFile(fileName); + const customTransformers = host.getCustomTransformers && host.getCustomTransformers(); + return getFileEmitOutput(program, sourceFile, !!emitOnlyDtsFiles, cancellationToken, customTransformers, forceDtsEmit); + } + // Signature help + /** + * This is a semantic operation. + */ + function getSignatureHelpItems(fileName: string, position: number, { triggerReason }: SignatureHelpItemsOptions = emptyOptions): SignatureHelpItems | undefined { + synchronizeHostData(); + const sourceFile = getValidSourceFile(fileName); + return SignatureHelp.getSignatureHelpItems(program, sourceFile, position, triggerReason, cancellationToken); + } + /// Syntactic features + function getNonBoundSourceFile(fileName: string): SourceFile { + return syntaxTreeCache.getCurrentSourceFile(fileName); + } + function getNameOrDottedNameSpan(fileName: string, startPos: number, _endPos: number): TextSpan | undefined { + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + // Get node at the location + const node = getTouchingPropertyName(sourceFile, startPos); + if (node === sourceFile) { + return undefined; + } + switch (node.kind) { + case SyntaxKind.PropertyAccessExpression: + case SyntaxKind.QualifiedName: + case SyntaxKind.StringLiteral: + case SyntaxKind.FalseKeyword: + case SyntaxKind.TrueKeyword: + case SyntaxKind.NullKeyword: + case SyntaxKind.SuperKeyword: + case SyntaxKind.ThisKeyword: + case SyntaxKind.ThisType: + case SyntaxKind.Identifier: + break; + // Cant create the text span + default: return undefined; - } - - switch (node.kind) { - case SyntaxKind.PropertyAccessExpression: - case SyntaxKind.QualifiedName: - case SyntaxKind.StringLiteral: - case SyntaxKind.FalseKeyword: - case SyntaxKind.TrueKeyword: - case SyntaxKind.NullKeyword: - case SyntaxKind.SuperKeyword: - case SyntaxKind.ThisKeyword: - case SyntaxKind.ThisType: - case SyntaxKind.Identifier: - break; - - // Cant create the text span - default: - return undefined; - } - - let nodeForStartPos = node; - while (true) { - if (isRightSideOfPropertyAccess(nodeForStartPos) || isRightSideOfQualifiedName(nodeForStartPos)) { - // If on the span is in right side of the the property or qualified name, return the span from the qualified name pos to end of this node - nodeForStartPos = nodeForStartPos.parent; - } - else if (isNameOfModuleDeclaration(nodeForStartPos)) { - // If this is name of a module declarations, check if this is right side of dotted module name - // If parent of the module declaration which is parent of this node is module declaration and its body is the module declaration that this node is name of - // Then this name is name from dotted module - if (nodeForStartPos.parent.parent.kind === SyntaxKind.ModuleDeclaration && - (nodeForStartPos.parent.parent).body === nodeForStartPos.parent) { - // Use parent module declarations name for start pos - nodeForStartPos = (nodeForStartPos.parent.parent).name; - } - else { - // We have to use this name for start pos - break; - } + } + let nodeForStartPos = node; + while (true) { + if (isRightSideOfPropertyAccess(nodeForStartPos) || isRightSideOfQualifiedName(nodeForStartPos)) { + // If on the span is in right side of the the property or qualified name, return the span from the qualified name pos to end of this node + nodeForStartPos = nodeForStartPos.parent; + } + else if (isNameOfModuleDeclaration(nodeForStartPos)) { + // If this is name of a module declarations, check if this is right side of dotted module name + // If parent of the module declaration which is parent of this node is module declaration and its body is the module declaration that this node is name of + // Then this name is name from dotted module + if (nodeForStartPos.parent.parent.kind === SyntaxKind.ModuleDeclaration && + (nodeForStartPos.parent.parent).body === nodeForStartPos.parent) { + // Use parent module declarations name for start pos + nodeForStartPos = (nodeForStartPos.parent.parent).name; } else { - // Is not a member expression so we have found the node for start pos + // We have to use this name for start pos break; } } - - return createTextSpanFromBounds(nodeForStartPos.getStart(), node.getEnd()); - } - - function getBreakpointStatementAtPosition(fileName: string, position: number): TextSpan | undefined { - // doesn't use compiler - no need to synchronize with host - const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); - - return BreakpointResolver.spanInSourceFileAtLocation(sourceFile, position); - } - - function getNavigationBarItems(fileName: string): NavigationBarItem[] { - return NavigationBar.getNavigationBarItems(syntaxTreeCache.getCurrentSourceFile(fileName), cancellationToken); - } - - function getNavigationTree(fileName: string): NavigationTree { - return NavigationBar.getNavigationTree(syntaxTreeCache.getCurrentSourceFile(fileName), cancellationToken); - } - - function isTsOrTsxFile(fileName: string): boolean { - const kind = getScriptKind(fileName, host); - return kind === ScriptKind.TS || kind === ScriptKind.TSX; - } - - function getSemanticClassifications(fileName: string, span: TextSpan): ClassifiedSpan[] { - if (!isTsOrTsxFile(fileName)) { - // do not run semantic classification on non-ts-or-tsx files - return []; - } - synchronizeHostData(); - return ts.getSemanticClassifications(program.getTypeChecker(), cancellationToken, getValidSourceFile(fileName), program.getClassifiableNames(), span); - } - - function getEncodedSemanticClassifications(fileName: string, span: TextSpan): Classifications { - if (!isTsOrTsxFile(fileName)) { - // do not run semantic classification on non-ts-or-tsx files - return { spans: [], endOfLineState: EndOfLineState.None }; - } - synchronizeHostData(); - return ts.getEncodedSemanticClassifications(program.getTypeChecker(), cancellationToken, getValidSourceFile(fileName), program.getClassifiableNames(), span); - } - - function getSyntacticClassifications(fileName: string, span: TextSpan): ClassifiedSpan[] { - // doesn't use compiler - no need to synchronize with host - return ts.getSyntacticClassifications(cancellationToken, syntaxTreeCache.getCurrentSourceFile(fileName), span); - } - - function getEncodedSyntacticClassifications(fileName: string, span: TextSpan): Classifications { - // doesn't use compiler - no need to synchronize with host - return ts.getEncodedSyntacticClassifications(cancellationToken, syntaxTreeCache.getCurrentSourceFile(fileName), span); - } - - function getOutliningSpans(fileName: string): OutliningSpan[] { - // doesn't use compiler - no need to synchronize with host - const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); - return OutliningElementsCollector.collectElements(sourceFile, cancellationToken); - } - - const braceMatching = createMapFromTemplate({ - [SyntaxKind.OpenBraceToken]: SyntaxKind.CloseBraceToken, - [SyntaxKind.OpenParenToken]: SyntaxKind.CloseParenToken, - [SyntaxKind.OpenBracketToken]: SyntaxKind.CloseBracketToken, - [SyntaxKind.GreaterThanToken]: SyntaxKind.LessThanToken, - }); - braceMatching.forEach((value, key) => braceMatching.set(value.toString(), Number(key) as SyntaxKind)); - - function getBraceMatchingAtPosition(fileName: string, position: number): TextSpan[] { - const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); - const token = getTouchingToken(sourceFile, position); - const matchKind = token.getStart(sourceFile) === position ? braceMatching.get(token.kind.toString()) : undefined; - const match = matchKind && findChildOfKind(token.parent, matchKind, sourceFile); - // We want to order the braces when we return the result. - return match ? [createTextSpanFromNode(token, sourceFile), createTextSpanFromNode(match, sourceFile)].sort((a, b) => a.start - b.start) : emptyArray; - } - - function getIndentationAtPosition(fileName: string, position: number, editorOptions: EditorOptions | EditorSettings) { - let start = timestamp(); - const settings = toEditorSettings(editorOptions); - const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); - log("getIndentationAtPosition: getCurrentSourceFile: " + (timestamp() - start)); - - start = timestamp(); - - const result = formatting.SmartIndenter.getIndentation(position, sourceFile, settings); - log("getIndentationAtPosition: computeIndentation : " + (timestamp() - start)); - - return result; - } - - function getFormattingEditsForRange(fileName: string, start: number, end: number, options: FormatCodeOptions | FormatCodeSettings): TextChange[] { - const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); - return formatting.formatSelection(start, end, sourceFile, formatting.getFormatContext(toEditorSettings(options))); - } - - function getFormattingEditsForDocument(fileName: string, options: FormatCodeOptions | FormatCodeSettings): TextChange[] { - return formatting.formatDocument(syntaxTreeCache.getCurrentSourceFile(fileName), formatting.getFormatContext(toEditorSettings(options))); - } - - function getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: FormatCodeOptions | FormatCodeSettings): TextChange[] { - const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); - const formatContext = formatting.getFormatContext(toEditorSettings(options)); - - if (!isInComment(sourceFile, position)) { - switch (key) { - case "{": - return formatting.formatOnOpeningCurly(position, sourceFile, formatContext); - case "}": - return formatting.formatOnClosingCurly(position, sourceFile, formatContext); - case ";": - return formatting.formatOnSemicolon(position, sourceFile, formatContext); - case "\n": - return formatting.formatOnEnter(position, sourceFile, formatContext); - } + else { + // Is not a member expression so we have found the node for start pos + break; } - - return []; } - - function getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: readonly number[], formatOptions: FormatCodeSettings, preferences: UserPreferences = emptyOptions): readonly CodeFixAction[] { - synchronizeHostData(); - const sourceFile = getValidSourceFile(fileName); - const span = createTextSpanFromBounds(start, end); - const formatContext = formatting.getFormatContext(formatOptions); - - return flatMap(deduplicate(errorCodes, equateValues, compareValues), errorCode => { - cancellationToken.throwIfCancellationRequested(); - return codefix.getFixes({ errorCode, sourceFile, span, program, host, cancellationToken, formatContext, preferences }); - }); + return createTextSpanFromBounds(nodeForStartPos.getStart(), node.getEnd()); + } + function getBreakpointStatementAtPosition(fileName: string, position: number): TextSpan | undefined { + // doesn't use compiler - no need to synchronize with host + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + return spanInSourceFileAtLocation(sourceFile, position); + } + function getNavigationBarItems(fileName: string): NavigationBarItem[] { + return NavigationBar.getNavigationBarItems(syntaxTreeCache.getCurrentSourceFile(fileName), cancellationToken); + } + function getNavigationTree(fileName: string): NavigationTree { + return NavigationBar.getNavigationTree(syntaxTreeCache.getCurrentSourceFile(fileName), cancellationToken); + } + function isTsOrTsxFile(fileName: string): boolean { + const kind = getScriptKind(fileName, host); + return kind === ScriptKind.TS || kind === ScriptKind.TSX; + } + function getSemanticClassifications(fileName: string, span: TextSpan): ClassifiedSpan[] { + if (!isTsOrTsxFile(fileName)) { + // do not run semantic classification on non-ts-or-tsx files + return []; } - - function getCombinedCodeFix(scope: CombinedCodeFixScope, fixId: {}, formatOptions: FormatCodeSettings, preferences: UserPreferences = emptyOptions): CombinedCodeActions { - synchronizeHostData(); - Debug.assert(scope.type === "file"); - const sourceFile = getValidSourceFile(scope.fileName); - const formatContext = formatting.getFormatContext(formatOptions); - - return codefix.getAllFixes({ fixId, sourceFile, program, host, cancellationToken, formatContext, preferences }); - } - - function organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences = emptyOptions): readonly FileTextChanges[] { - synchronizeHostData(); - Debug.assert(scope.type === "file"); - const sourceFile = getValidSourceFile(scope.fileName); - const formatContext = formatting.getFormatContext(formatOptions); - - return OrganizeImports.organizeImports(sourceFile, formatContext, host, program, preferences); - } - - function getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences = emptyOptions): readonly FileTextChanges[] { - return ts.getEditsForFileRename(getProgram()!, oldFilePath, newFilePath, host, formatting.getFormatContext(formatOptions), preferences, sourceMapper); - } - - function applyCodeActionCommand(action: CodeActionCommand, formatSettings?: FormatCodeSettings): Promise; - function applyCodeActionCommand(action: CodeActionCommand[], formatSettings?: FormatCodeSettings): Promise; - function applyCodeActionCommand(action: CodeActionCommand | CodeActionCommand[], formatSettings?: FormatCodeSettings): Promise; - function applyCodeActionCommand(fileName: Path, action: CodeActionCommand): Promise; - function applyCodeActionCommand(fileName: Path, action: CodeActionCommand[]): Promise; - function applyCodeActionCommand(fileName: Path | CodeActionCommand | CodeActionCommand[], actionOrFormatSettingsOrUndefined?: CodeActionCommand | CodeActionCommand[] | FormatCodeSettings): Promise { - const action = typeof fileName === "string" ? actionOrFormatSettingsOrUndefined as CodeActionCommand | CodeActionCommand[] : fileName as CodeActionCommand[]; - return isArray(action) ? Promise.all(action.map(a => applySingleCodeActionCommand(a))) : applySingleCodeActionCommand(action); - } - - function applySingleCodeActionCommand(action: CodeActionCommand): Promise { - const getPath = (path: string): Path => toPath(path, currentDirectory, getCanonicalFileName); - Debug.assertEqual(action.type, "install package"); - return host.installPackage - ? host.installPackage({ fileName: getPath(action.file), packageName: action.packageName }) - : Promise.reject("Host does not implement `installPackage`"); - } - - function getDocCommentTemplateAtPosition(fileName: string, position: number): TextInsertion | undefined { - return JsDoc.getDocCommentTemplateAtPosition(getNewLineOrDefaultFromHost(host), syntaxTreeCache.getCurrentSourceFile(fileName), position); - } - - function isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean { - // '<' is currently not supported, figuring out if we're in a Generic Type vs. a comparison is too - // expensive to do during typing scenarios - // i.e. whether we're dealing with: - // var x = new foo<| ( with class foo{} ) - // or - // var y = 3 <| - if (openingBrace === CharacterCodes.lessThan) { - return false; - } - - const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); - - // Check if in a context where we don't want to perform any insertion - if (isInString(sourceFile, position)) { - return false; - } - - if (isInsideJsxElementOrAttribute(sourceFile, position)) { - return openingBrace === CharacterCodes.openBrace; - } - - if (isInTemplateString(sourceFile, position)) { - return false; - } - - switch (openingBrace) { - case CharacterCodes.singleQuote: - case CharacterCodes.doubleQuote: - case CharacterCodes.backtick: - return !isInComment(sourceFile, position); - } - - return true; - } - - function getJsxClosingTagAtPosition(fileName: string, position: number): JsxClosingTagInfo | undefined { - const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); - const token = findPrecedingToken(position, sourceFile); - if (!token) return undefined; - const element = token.kind === SyntaxKind.GreaterThanToken && isJsxOpeningElement(token.parent) ? token.parent.parent - : isJsxText(token) ? token.parent : undefined; - if (element && isUnclosedTag(element)) { - return { newText: `` }; - } + synchronizeHostData(); + return ts.getSemanticClassifications(program.getTypeChecker(), cancellationToken, getValidSourceFile(fileName), program.getClassifiableNames(), span); + } + function getEncodedSemanticClassifications(fileName: string, span: TextSpan): Classifications { + if (!isTsOrTsxFile(fileName)) { + // do not run semantic classification on non-ts-or-tsx files + return { spans: [], endOfLineState: EndOfLineState.None }; } - - function isUnclosedTag({ openingElement, closingElement, parent }: JsxElement): boolean { - return !tagNamesAreEquivalent(openingElement.tagName, closingElement.tagName) || - isJsxElement(parent) && tagNamesAreEquivalent(openingElement.tagName, parent.openingElement.tagName) && isUnclosedTag(parent); - } - - function getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan | undefined { - const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); - const range = formatting.getRangeOfEnclosingComment(sourceFile, position); - return range && (!onlyMultiLine || range.kind === SyntaxKind.MultiLineCommentTrivia) ? createTextSpanFromRange(range) : undefined; - } - - function getTodoComments(fileName: string, descriptors: TodoCommentDescriptor[]): TodoComment[] { - // Note: while getting todo comments seems like a syntactic operation, we actually - // treat it as a semantic operation here. This is because we expect our host to call - // this on every single file. If we treat this syntactically, then that will cause - // us to populate and throw away the tree in our syntax tree cache for each file. By - // treating this as a semantic operation, we can access any tree without throwing - // anything away. - synchronizeHostData(); - - const sourceFile = getValidSourceFile(fileName); - + synchronizeHostData(); + return ts.getEncodedSemanticClassifications(program.getTypeChecker(), cancellationToken, getValidSourceFile(fileName), program.getClassifiableNames(), span); + } + function getSyntacticClassifications(fileName: string, span: TextSpan): ClassifiedSpan[] { + // doesn't use compiler - no need to synchronize with host + return ts.getSyntacticClassifications(cancellationToken, syntaxTreeCache.getCurrentSourceFile(fileName), span); + } + function getEncodedSyntacticClassifications(fileName: string, span: TextSpan): Classifications { + // doesn't use compiler - no need to synchronize with host + return ts.getEncodedSyntacticClassifications(cancellationToken, syntaxTreeCache.getCurrentSourceFile(fileName), span); + } + function getOutliningSpans(fileName: string): OutliningSpan[] { + // doesn't use compiler - no need to synchronize with host + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + return collectElements(sourceFile, cancellationToken); + } + const braceMatching = createMapFromTemplate({ + [SyntaxKind.OpenBraceToken]: SyntaxKind.CloseBraceToken, + [SyntaxKind.OpenParenToken]: SyntaxKind.CloseParenToken, + [SyntaxKind.OpenBracketToken]: SyntaxKind.CloseBracketToken, + [SyntaxKind.GreaterThanToken]: SyntaxKind.LessThanToken, + }); + braceMatching.forEach((value, key) => braceMatching.set(value.toString(), (Number(key) as SyntaxKind))); + function getBraceMatchingAtPosition(fileName: string, position: number): TextSpan[] { + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + const token = getTouchingToken(sourceFile, position); + const matchKind = token.getStart(sourceFile) === position ? braceMatching.get(token.kind.toString()) : undefined; + const match = matchKind && findChildOfKind(token.parent, matchKind, sourceFile); + // We want to order the braces when we return the result. + return match ? [createTextSpanFromNode(token, sourceFile), createTextSpanFromNode(match, sourceFile)].sort((a, b) => a.start - b.start) : emptyArray; + } + function getIndentationAtPosition(fileName: string, position: number, editorOptions: EditorOptions | EditorSettings) { + let start = timestamp(); + const settings = toEditorSettings(editorOptions); + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + log("getIndentationAtPosition: getCurrentSourceFile: " + (timestamp() - start)); + start = timestamp(); + const result = SmartIndenter.getIndentation(position, sourceFile, settings); + log("getIndentationAtPosition: computeIndentation : " + (timestamp() - start)); + return result; + } + function getFormattingEditsForRange(fileName: string, start: number, end: number, options: FormatCodeOptions | FormatCodeSettings): TextChange[] { + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + return formatSelection(start, end, sourceFile, getFormatContext(toEditorSettings(options))); + } + function getFormattingEditsForDocument(fileName: string, options: FormatCodeOptions | FormatCodeSettings): TextChange[] { + return formatDocument(syntaxTreeCache.getCurrentSourceFile(fileName), getFormatContext(toEditorSettings(options))); + } + function getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: FormatCodeOptions | FormatCodeSettings): TextChange[] { + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + const formatContext = getFormatContext(toEditorSettings(options)); + if (!isInComment(sourceFile, position)) { + switch (key) { + case "{": + return formatOnOpeningCurly(position, sourceFile, formatContext); + case "}": + return formatOnClosingCurly(position, sourceFile, formatContext); + case ";": + return formatOnSemicolon(position, sourceFile, formatContext); + case "\n": + return formatOnEnter(position, sourceFile, formatContext); + } + } + return []; + } + function getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: readonly number[], formatOptions: FormatCodeSettings, preferences: UserPreferences = emptyOptions): readonly CodeFixAction[] { + synchronizeHostData(); + const sourceFile = getValidSourceFile(fileName); + const span = createTextSpanFromBounds(start, end); + const formatContext = getFormatContext(formatOptions); + return flatMap(deduplicate(errorCodes, equateValues, compareValues), errorCode => { cancellationToken.throwIfCancellationRequested(); - - const fileContents = sourceFile.text; - const result: TodoComment[] = []; - - // Exclude node_modules files as we don't want to show the todos of external libraries. - if (descriptors.length > 0 && !isNodeModulesFile(sourceFile.fileName)) { - const regExp = getTodoCommentsRegExp(); - - let matchArray: RegExpExecArray | null; - while (matchArray = regExp.exec(fileContents)) { - cancellationToken.throwIfCancellationRequested(); - - // If we got a match, here is what the match array will look like. Say the source text is: - // - // " // hack 1" - // - // The result array with the regexp: will be: - // - // ["// hack 1", "// ", "hack 1", undefined, "hack"] - // - // Here are the relevant capture groups: - // 0) The full match for the entire regexp. - // 1) The preamble to the message portion. - // 2) The message portion. - // 3...N) The descriptor that was matched - by index. 'undefined' for each - // descriptor that didn't match. an actual value if it did match. - // - // i.e. 'undefined' in position 3 above means TODO(jason) didn't match. - // "hack" in position 4 means HACK did match. - const firstDescriptorCaptureIndex = 3; - Debug.assert(matchArray.length === descriptors.length + firstDescriptorCaptureIndex); - - const preamble = matchArray[1]; - const matchPosition = matchArray.index + preamble.length; - - // OK, we have found a match in the file. This is only an acceptable match if - // it is contained within a comment. - if (!isInComment(sourceFile, matchPosition)) { - continue; - } - - let descriptor: TodoCommentDescriptor | undefined; - for (let i = 0; i < descriptors.length; i++) { - if (matchArray[i + firstDescriptorCaptureIndex]) { - descriptor = descriptors[i]; - } - } - if (descriptor === undefined) return Debug.fail(); - - // We don't want to match something like 'TODOBY', so we make sure a non - // letter/digit follows the match. - if (isLetterOrDigit(fileContents.charCodeAt(matchPosition + descriptor.text.length))) { - continue; - } - - const message = matchArray[2]; - result.push({ descriptor, message, position: matchPosition }); - } - } - - return result; - - function escapeRegExp(str: string): string { - return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); - } - - function getTodoCommentsRegExp(): RegExp { - // NOTE: `?:` means 'non-capture group'. It allows us to have groups without having to - // filter them out later in the final result array. - - // TODO comments can appear in one of the following forms: - // - // 1) // TODO or /////////// TODO - // - // 2) /* TODO or /********** TODO - // - // 3) /* - // * TODO - // */ + return getFixes({ errorCode, sourceFile, span, program, host, cancellationToken, formatContext, preferences }); + }); + } + function getCombinedCodeFix(scope: CombinedCodeFixScope, fixId: {}, formatOptions: FormatCodeSettings, preferences: UserPreferences = emptyOptions): CombinedCodeActions { + synchronizeHostData(); + Debug.assert(scope.type === "file"); + const sourceFile = getValidSourceFile(scope.fileName); + const formatContext = getFormatContext(formatOptions); + return getAllFixes({ fixId, sourceFile, program, host, cancellationToken, formatContext, preferences }); + } + function organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences = emptyOptions): readonly FileTextChanges[] { + synchronizeHostData(); + Debug.assert(scope.type === "file"); + const sourceFile = getValidSourceFile(scope.fileName); + const formatContext = getFormatContext(formatOptions); + return OrganizeImports.organizeImports(sourceFile, formatContext, host, program, preferences); + } + function getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences = emptyOptions): readonly FileTextChanges[] { + return ts.getEditsForFileRename((getProgram()!), oldFilePath, newFilePath, host, getFormatContext(formatOptions), preferences, sourceMapper); + } + function applyCodeActionCommand(action: CodeActionCommand, formatSettings?: FormatCodeSettings): Promise; + function applyCodeActionCommand(action: CodeActionCommand[], formatSettings?: FormatCodeSettings): Promise; + function applyCodeActionCommand(action: CodeActionCommand | CodeActionCommand[], formatSettings?: FormatCodeSettings): Promise; + function applyCodeActionCommand(fileName: Path, action: CodeActionCommand): Promise; + function applyCodeActionCommand(fileName: Path, action: CodeActionCommand[]): Promise; + function applyCodeActionCommand(fileName: Path | CodeActionCommand | CodeActionCommand[], actionOrFormatSettingsOrUndefined?: CodeActionCommand | CodeActionCommand[] | FormatCodeSettings): Promise { + const action = typeof fileName === "string" ? actionOrFormatSettingsOrUndefined as CodeActionCommand | CodeActionCommand[] : fileName as CodeActionCommand[]; + return isArray(action) ? Promise.all(action.map(a => applySingleCodeActionCommand(a))) : applySingleCodeActionCommand(action); + } + function applySingleCodeActionCommand(action: CodeActionCommand): Promise { + const getPath = (path: string): Path => toPath(path, currentDirectory, getCanonicalFileName); + Debug.assertEqual(action.type, "install package"); + return host.installPackage + ? host.installPackage({ fileName: getPath(action.file), packageName: action.packageName }) + : Promise.reject("Host does not implement `installPackage`"); + } + function getDocCommentTemplateAtPosition(fileName: string, position: number): TextInsertion | undefined { + return JsDoc.getDocCommentTemplateAtPosition(getNewLineOrDefaultFromHost(host), syntaxTreeCache.getCurrentSourceFile(fileName), position); + } + function isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean { + // '<' is currently not supported, figuring out if we're in a Generic Type vs. a comparison is too + // expensive to do during typing scenarios + // i.e. whether we're dealing with: + // var x = new foo<| ( with class foo{} ) + // or + // var y = 3 <| + if (openingBrace === CharacterCodes.lessThan) { + return false; + } + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + // Check if in a context where we don't want to perform any insertion + if (isInString(sourceFile, position)) { + return false; + } + if (isInsideJsxElementOrAttribute(sourceFile, position)) { + return openingBrace === CharacterCodes.openBrace; + } + if (isInTemplateString(sourceFile, position)) { + return false; + } + switch (openingBrace) { + case CharacterCodes.singleQuote: + case CharacterCodes.doubleQuote: + case CharacterCodes.backtick: + return !isInComment(sourceFile, position); + } + return true; + } + function getJsxClosingTagAtPosition(fileName: string, position: number): JsxClosingTagInfo | undefined { + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + const token = findPrecedingToken(position, sourceFile); + if (!token) + return undefined; + const element = token.kind === SyntaxKind.GreaterThanToken && isJsxOpeningElement(token.parent) ? token.parent.parent + : isJsxText(token) ? token.parent : undefined; + if (element && isUnclosedTag(element)) { + return { newText: `` }; + } + } + function isUnclosedTag({ openingElement, closingElement, parent }: JsxElement): boolean { + return !tagNamesAreEquivalent(openingElement.tagName, closingElement.tagName) || + isJsxElement(parent) && tagNamesAreEquivalent(openingElement.tagName, parent.openingElement.tagName) && isUnclosedTag(parent); + } + function getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan | undefined { + const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); + const range = getRangeOfEnclosingComment(sourceFile, position); + return range && (!onlyMultiLine || range.kind === SyntaxKind.MultiLineCommentTrivia) ? createTextSpanFromRange(range) : undefined; + } + function getTodoComments(fileName: string, descriptors: TodoCommentDescriptor[]): TodoComment[] { + // Note: while getting todo comments seems like a syntactic operation, we actually + // treat it as a semantic operation here. This is because we expect our host to call + // this on every single file. If we treat this syntactically, then that will cause + // us to populate and throw away the tree in our syntax tree cache for each file. By + // treating this as a semantic operation, we can access any tree without throwing + // anything away. + synchronizeHostData(); + const sourceFile = getValidSourceFile(fileName); + cancellationToken.throwIfCancellationRequested(); + const fileContents = sourceFile.text; + const result: TodoComment[] = []; + // Exclude node_modules files as we don't want to show the todos of external libraries. + if (descriptors.length > 0 && !isNodeModulesFile(sourceFile.fileName)) { + const regExp = getTodoCommentsRegExp(); + let matchArray: RegExpExecArray | null; + while (matchArray = regExp.exec(fileContents)) { + cancellationToken.throwIfCancellationRequested(); + // If we got a match, here is what the match array will look like. Say the source text is: // - // The following three regexps are used to match the start of the text up to the TODO - // comment portion. - const singleLineCommentStart = /(?:\/\/+\s*)/.source; - const multiLineCommentStart = /(?:\/\*+\s*)/.source; - const anyNumberOfSpacesAndAsterisksAtStartOfLine = /(?:^(?:\s|\*)*)/.source; - - // Match any of the above three TODO comment start regexps. - // Note that the outermost group *is* a capture group. We want to capture the preamble - // so that we can determine the starting position of the TODO comment match. - const preamble = "(" + anyNumberOfSpacesAndAsterisksAtStartOfLine + "|" + singleLineCommentStart + "|" + multiLineCommentStart + ")"; - - // Takes the descriptors and forms a regexp that matches them as if they were literals. - // For example, if the descriptors are "TODO(jason)" and "HACK", then this will be: + // " // hack 1" // - // (?:(TODO\(jason\))|(HACK)) + // The result array with the regexp: will be: // - // Note that the outermost group is *not* a capture group, but the innermost groups - // *are* capture groups. By capturing the inner literals we can determine after - // matching which descriptor we are dealing with. - const literals = "(?:" + map(descriptors, d => "(" + escapeRegExp(d.text) + ")").join("|") + ")"; - - // After matching a descriptor literal, the following regexp matches the rest of the - // text up to the end of the line (or */). - const endOfLineOrEndOfComment = /(?:$|\*\/)/.source; - const messageRemainder = /(?:.*?)/.source; - - // This is the portion of the match we'll return as part of the TODO comment result. We - // match the literal portion up to the end of the line or end of comment. - const messagePortion = "(" + literals + messageRemainder + ")"; - const regExpString = preamble + messagePortion + endOfLineOrEndOfComment; - - // The final regexp will look like this: - // /((?:\/\/+\s*)|(?:\/\*+\s*)|(?:^(?:\s|\*)*))((?:(TODO\(jason\))|(HACK))(?:.*?))(?:$|\*\/)/gim - - // The flags of the regexp are important here. - // 'g' is so that we are doing a global search and can find matches several times - // in the input. + // ["// hack 1", "// ", "hack 1", undefined, "hack"] // - // 'i' is for case insensitivity (We do this to match C# TODO comment code). + // Here are the relevant capture groups: + // 0) The full match for the entire regexp. + // 1) The preamble to the message portion. + // 2) The message portion. + // 3...N) The descriptor that was matched - by index. 'undefined' for each + // descriptor that didn't match. an actual value if it did match. // - // 'm' is so we can find matches in a multi-line input. - return new RegExp(regExpString, "gim"); - } - - function isLetterOrDigit(char: number): boolean { - return (char >= CharacterCodes.a && char <= CharacterCodes.z) || - (char >= CharacterCodes.A && char <= CharacterCodes.Z) || - (char >= CharacterCodes._0 && char <= CharacterCodes._9); - } - - function isNodeModulesFile(path: string): boolean { - return stringContains(path, "/node_modules/"); - } - } - - function getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): RenameInfo { - synchronizeHostData(); - return Rename.getRenameInfo(program, getValidSourceFile(fileName), position, options); - } - - function getRefactorContext(file: SourceFile, positionOrRange: number | TextRange, preferences: UserPreferences, formatOptions?: FormatCodeSettings): RefactorContext { - const [startPosition, endPosition] = typeof positionOrRange === "number" ? [positionOrRange, undefined] : [positionOrRange.pos, positionOrRange.end]; - return { - file, - startPosition, - endPosition, - program: getProgram()!, - host, - formatContext: formatting.getFormatContext(formatOptions!), // TODO: GH#18217 - cancellationToken, - preferences, - }; + // i.e. 'undefined' in position 3 above means TODO(jason) didn't match. + // "hack" in position 4 means HACK did match. + const firstDescriptorCaptureIndex = 3; + Debug.assert(matchArray.length === descriptors.length + firstDescriptorCaptureIndex); + const preamble = matchArray[1]; + const matchPosition = matchArray.index + preamble.length; + // OK, we have found a match in the file. This is only an acceptable match if + // it is contained within a comment. + if (!isInComment(sourceFile, matchPosition)) { + continue; + } + let descriptor: TodoCommentDescriptor | undefined; + for (let i = 0; i < descriptors.length; i++) { + if (matchArray[i + firstDescriptorCaptureIndex]) { + descriptor = descriptors[i]; + } + } + if (descriptor === undefined) + return Debug.fail(); + // We don't want to match something like 'TODOBY', so we make sure a non + // letter/digit follows the match. + if (isLetterOrDigit(fileContents.charCodeAt(matchPosition + descriptor.text.length))) { + continue; + } + const message = matchArray[2]; + result.push({ descriptor, message, position: matchPosition }); + } + } + return result; + function escapeRegExp(str: string): string { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + } + function getTodoCommentsRegExp(): RegExp { + // NOTE: `?:` means 'non-capture group'. It allows us to have groups without having to + // filter them out later in the final result array. + // TODO comments can appear in one of the following forms: + // + // 1) // TODO or /////////// TODO + // + // 2) /* TODO or /********** TODO + // + // 3) /* + // * TODO + // */ + // + // The following three regexps are used to match the start of the text up to the TODO + // comment portion. + const singleLineCommentStart = /(?:\/\/+\s*)/.source; + const multiLineCommentStart = /(?:\/\*+\s*)/.source; + const anyNumberOfSpacesAndAsterisksAtStartOfLine = /(?:^(?:\s|\*)*)/.source; + // Match any of the above three TODO comment start regexps. + // Note that the outermost group *is* a capture group. We want to capture the preamble + // so that we can determine the starting position of the TODO comment match. + const preamble = "(" + anyNumberOfSpacesAndAsterisksAtStartOfLine + "|" + singleLineCommentStart + "|" + multiLineCommentStart + ")"; + // Takes the descriptors and forms a regexp that matches them as if they were literals. + // For example, if the descriptors are "TODO(jason)" and "HACK", then this will be: + // + // (?:(TODO\(jason\))|(HACK)) + // + // Note that the outermost group is *not* a capture group, but the innermost groups + // *are* capture groups. By capturing the inner literals we can determine after + // matching which descriptor we are dealing with. + const literals = "(?:" + map(descriptors, d => "(" + escapeRegExp(d.text) + ")").join("|") + ")"; + // After matching a descriptor literal, the following regexp matches the rest of the + // text up to the end of the line (or */). + const endOfLineOrEndOfComment = /(?:$|\*\/)/.source; + const messageRemainder = /(?:.*?)/.source; + // This is the portion of the match we'll return as part of the TODO comment result. We + // match the literal portion up to the end of the line or end of comment. + const messagePortion = "(" + literals + messageRemainder + ")"; + const regExpString = preamble + messagePortion + endOfLineOrEndOfComment; + // The final regexp will look like this: + // /((?:\/\/+\s*)|(?:\/\*+\s*)|(?:^(?:\s|\*)*))((?:(TODO\(jason\))|(HACK))(?:.*?))(?:$|\*\/)/gim + // The flags of the regexp are important here. + // 'g' is so that we are doing a global search and can find matches several times + // in the input. + // + // 'i' is for case insensitivity (We do this to match C# TODO comment code). + // + // 'm' is so we can find matches in a multi-line input. + return new RegExp(regExpString, "gim"); + } + function isLetterOrDigit(char: number): boolean { + return (char >= CharacterCodes.a && char <= CharacterCodes.z) || + (char >= CharacterCodes.A && char <= CharacterCodes.Z) || + (char >= CharacterCodes._0 && char <= CharacterCodes._9); + } + function isNodeModulesFile(path: string): boolean { + return stringContains(path, "/node_modules/"); } - - function getSmartSelectionRange(fileName: string, position: number): SelectionRange { - return SmartSelectionRange.getSmartSelectionRange(position, syntaxTreeCache.getCurrentSourceFile(fileName)); - } - - function getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences = emptyOptions): ApplicableRefactorInfo[] { - synchronizeHostData(); - const file = getValidSourceFile(fileName); - return refactor.getApplicableRefactors(getRefactorContext(file, positionOrRange, preferences)); - } - - function getEditsForRefactor( - fileName: string, - formatOptions: FormatCodeSettings, - positionOrRange: number | TextRange, - refactorName: string, - actionName: string, - preferences: UserPreferences = emptyOptions, - ): RefactorEditInfo | undefined { - synchronizeHostData(); - const file = getValidSourceFile(fileName); - return refactor.getEditsForRefactor(getRefactorContext(file, positionOrRange, preferences, formatOptions), refactorName, actionName); - } - - function prepareCallHierarchy(fileName: string, position: number): CallHierarchyItem | CallHierarchyItem[] | undefined { - synchronizeHostData(); - const declarations = CallHierarchy.resolveCallHierarchyDeclaration(program, getTouchingPropertyName(getValidSourceFile(fileName), position)); - return declarations && mapOneOrMany(declarations, declaration => CallHierarchy.createCallHierarchyItem(program, declaration)); - } - - function provideCallHierarchyIncomingCalls(fileName: string, position: number): CallHierarchyIncomingCall[] { - synchronizeHostData(); - const sourceFile = getValidSourceFile(fileName); - const declaration = firstOrOnly(CallHierarchy.resolveCallHierarchyDeclaration(program, position === 0 ? sourceFile : getTouchingPropertyName(sourceFile, position))); - return declaration ? CallHierarchy.getIncomingCalls(program, declaration, cancellationToken) : []; - } - - function provideCallHierarchyOutgoingCalls(fileName: string, position: number): CallHierarchyOutgoingCall[] { - synchronizeHostData(); - const sourceFile = getValidSourceFile(fileName); - const declaration = firstOrOnly(CallHierarchy.resolveCallHierarchyDeclaration(program, position === 0 ? sourceFile : getTouchingPropertyName(sourceFile, position))); - return declaration ? CallHierarchy.getOutgoingCalls(program, declaration) : []; - } - + } + function getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): RenameInfo { + synchronizeHostData(); + return Rename.getRenameInfo(program, getValidSourceFile(fileName), position, options); + } + function getRefactorContext(file: SourceFile, positionOrRange: number | TextRange, preferences: UserPreferences, formatOptions?: FormatCodeSettings): RefactorContext { + const [startPosition, endPosition] = typeof positionOrRange === "number" ? [positionOrRange, undefined] : [positionOrRange.pos, positionOrRange.end]; return { - dispose, - cleanupSemanticCache, - getSyntacticDiagnostics, - getSemanticDiagnostics, - getSuggestionDiagnostics, - getCompilerOptionsDiagnostics, - getSyntacticClassifications, - getSemanticClassifications, - getEncodedSyntacticClassifications, - getEncodedSemanticClassifications, - getCompletionsAtPosition, - getCompletionEntryDetails, - getCompletionEntrySymbol, - getSignatureHelpItems, - getQuickInfoAtPosition, - getDefinitionAtPosition, - getDefinitionAndBoundSpan, - getImplementationAtPosition, - getTypeDefinitionAtPosition, - getReferencesAtPosition, - findReferences, - getOccurrencesAtPosition, - getDocumentHighlights, - getNameOrDottedNameSpan, - getBreakpointStatementAtPosition, - getNavigateToItems, - getRenameInfo, - getSmartSelectionRange, - findRenameLocations, - getNavigationBarItems, - getNavigationTree, - getOutliningSpans, - getTodoComments, - getBraceMatchingAtPosition, - getIndentationAtPosition, - getFormattingEditsForRange, - getFormattingEditsForDocument, - getFormattingEditsAfterKeystroke, - getDocCommentTemplateAtPosition, - isValidBraceCompletionAtPosition, - getJsxClosingTagAtPosition, - getSpanOfEnclosingComment, - getCodeFixesAtPosition, - getCombinedCodeFix, - applyCodeActionCommand, - organizeImports, - getEditsForFileRename, - getEmitOutput, - getNonBoundSourceFile, - getProgram, - getApplicableRefactors, - getEditsForRefactor, - toLineColumnOffset: sourceMapper.toLineColumnOffset, - getSourceMapper: () => sourceMapper, - clearSourceMapperCache: () => sourceMapper.clearCache(), - prepareCallHierarchy, - provideCallHierarchyIncomingCalls, - provideCallHierarchyOutgoingCalls + file, + startPosition, + endPosition, + program: getProgram()!, + host, + formatContext: getFormatContext((formatOptions!)), + cancellationToken, + preferences, }; } - - /* @internal */ - /** Names in the name table are escaped, so an identifier `__foo` will have a name table entry `___foo`. */ - export function getNameTable(sourceFile: SourceFile): UnderscoreEscapedMap { - if (!sourceFile.nameTable) { - initializeNameTable(sourceFile); - } - - return sourceFile.nameTable!; // TODO: GH#18217 - } - - function initializeNameTable(sourceFile: SourceFile): void { - const nameTable = sourceFile.nameTable = createUnderscoreEscapedMap(); - sourceFile.forEachChild(function walk(node) { - if (isIdentifier(node) && !isTagName(node) && node.escapedText || isStringOrNumericLiteralLike(node) && literalIsName(node)) { - const text = getEscapedTextOfIdentifierOrLiteral(node); - nameTable.set(text, nameTable.get(text) === undefined ? node.pos : -1); - } - else if (isPrivateIdentifier(node)) { - const text = node.escapedText; - nameTable.set(text, nameTable.get(text) === undefined ? node.pos : -1); - } - - forEachChild(node, walk); - if (hasJSDocNodes(node)) { - for (const jsDoc of node.jsDoc!) { - forEachChild(jsDoc, walk); - } - } - }); + function getSmartSelectionRange(fileName: string, position: number): SelectionRange { + return SmartSelectionRange.getSmartSelectionRange(position, syntaxTreeCache.getCurrentSourceFile(fileName)); } - - /** - * We want to store any numbers/strings if they were a name that could be - * related to a declaration. So, if we have 'import x = require("something")' - * then we want 'something' to be in the name table. Similarly, if we have - * "a['propname']" then we want to store "propname" in the name table. - */ - function literalIsName(node: StringLiteralLike | NumericLiteral): boolean { - return isDeclarationName(node) || - node.parent.kind === SyntaxKind.ExternalModuleReference || - isArgumentOfElementAccessExpression(node) || - isLiteralComputedPropertyDeclarationName(node); + function getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences = emptyOptions): ApplicableRefactorInfo[] { + synchronizeHostData(); + const file = getValidSourceFile(fileName); + return refactor.getApplicableRefactors(getRefactorContext(file, positionOrRange, preferences)); } - - /** - * Returns the containing object literal property declaration given a possible name node, e.g. "a" in x = { "a": 1 } - */ - /* @internal */ - export function getContainingObjectLiteralElement(node: Node): ObjectLiteralElementWithName | undefined { - const element = getContainingObjectLiteralElementWorker(node); - return element && (isObjectLiteralExpression(element.parent) || isJsxAttributes(element.parent)) ? element as ObjectLiteralElementWithName : undefined; + function getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences = emptyOptions): RefactorEditInfo | undefined { + synchronizeHostData(); + const file = getValidSourceFile(fileName); + return refactor.getEditsForRefactor(getRefactorContext(file, positionOrRange, preferences, formatOptions), refactorName, actionName); } - function getContainingObjectLiteralElementWorker(node: Node): ObjectLiteralElement | undefined { - switch (node.kind) { - case SyntaxKind.StringLiteral: - case SyntaxKind.NoSubstitutionTemplateLiteral: - case SyntaxKind.NumericLiteral: - if (node.parent.kind === SyntaxKind.ComputedPropertyName) { - return isObjectLiteralElement(node.parent.parent) ? node.parent.parent : undefined; - } - // falls through - - case SyntaxKind.Identifier: - return isObjectLiteralElement(node.parent) && - (node.parent.parent.kind === SyntaxKind.ObjectLiteralExpression || node.parent.parent.kind === SyntaxKind.JsxAttributes) && - node.parent.name === node ? node.parent : undefined; - } - return undefined; + function prepareCallHierarchy(fileName: string, position: number): CallHierarchyItem | CallHierarchyItem[] | undefined { + synchronizeHostData(); + const declarations = resolveCallHierarchyDeclaration(program, getTouchingPropertyName(getValidSourceFile(fileName), position)); + return declarations && mapOneOrMany(declarations, declaration => createCallHierarchyItem(program, declaration)); + } + function provideCallHierarchyIncomingCalls(fileName: string, position: number): CallHierarchyIncomingCall[] { + synchronizeHostData(); + const sourceFile = getValidSourceFile(fileName); + const declaration = firstOrOnly(resolveCallHierarchyDeclaration(program, position === 0 ? sourceFile : getTouchingPropertyName(sourceFile, position))); + return declaration ? getIncomingCalls(program, declaration, cancellationToken) : []; } - - /* @internal */ - export type ObjectLiteralElementWithName = ObjectLiteralElement & { name: PropertyName; parent: ObjectLiteralExpression | JsxAttributes }; - - function getSymbolAtLocationForQuickInfo(node: Node, checker: TypeChecker): Symbol | undefined { - const object = getContainingObjectLiteralElement(node); - if (object) { - const contextualType = checker.getContextualType(object.parent); - const properties = contextualType && getPropertySymbolsFromContextualType(object, checker, contextualType, /*unionSymbolOk*/ false); - if (properties && properties.length === 1) { - return first(properties); + function provideCallHierarchyOutgoingCalls(fileName: string, position: number): CallHierarchyOutgoingCall[] { + synchronizeHostData(); + const sourceFile = getValidSourceFile(fileName); + const declaration = firstOrOnly(resolveCallHierarchyDeclaration(program, position === 0 ? sourceFile : getTouchingPropertyName(sourceFile, position))); + return declaration ? getOutgoingCalls(program, declaration) : []; + } + return { + dispose, + cleanupSemanticCache, + getSyntacticDiagnostics, + getSemanticDiagnostics, + getSuggestionDiagnostics, + getCompilerOptionsDiagnostics, + getSyntacticClassifications, + getSemanticClassifications, + getEncodedSyntacticClassifications, + getEncodedSemanticClassifications, + getCompletionsAtPosition, + getCompletionEntryDetails, + getCompletionEntrySymbol, + getSignatureHelpItems, + getQuickInfoAtPosition, + getDefinitionAtPosition, + getDefinitionAndBoundSpan, + getImplementationAtPosition, + getTypeDefinitionAtPosition, + getReferencesAtPosition, + findReferences, + getOccurrencesAtPosition, + getDocumentHighlights, + getNameOrDottedNameSpan, + getBreakpointStatementAtPosition, + getNavigateToItems, + getRenameInfo, + getSmartSelectionRange, + findRenameLocations, + getNavigationBarItems, + getNavigationTree, + getOutliningSpans, + getTodoComments, + getBraceMatchingAtPosition, + getIndentationAtPosition, + getFormattingEditsForRange, + getFormattingEditsForDocument, + getFormattingEditsAfterKeystroke, + getDocCommentTemplateAtPosition, + isValidBraceCompletionAtPosition, + getJsxClosingTagAtPosition, + getSpanOfEnclosingComment, + getCodeFixesAtPosition, + getCombinedCodeFix, + applyCodeActionCommand, + organizeImports, + getEditsForFileRename, + getEmitOutput, + getNonBoundSourceFile, + getProgram, + getApplicableRefactors, + getEditsForRefactor, + toLineColumnOffset: sourceMapper.toLineColumnOffset, + getSourceMapper: () => sourceMapper, + clearSourceMapperCache: () => sourceMapper.clearCache(), + prepareCallHierarchy, + provideCallHierarchyIncomingCalls, + provideCallHierarchyOutgoingCalls + }; +} +/* @internal */ +/** Names in the name table are escaped, so an identifier `__foo` will have a name table entry `___foo`. */ +export function getNameTable(sourceFile: SourceFile): UnderscoreEscapedMap { + if (!sourceFile.nameTable) { + initializeNameTable(sourceFile); + } + return sourceFile.nameTable!; // TODO: GH#18217 +} +function initializeNameTable(sourceFile: SourceFile): void { + const nameTable = sourceFile.nameTable = createUnderscoreEscapedMap(); + sourceFile.forEachChild(function walk(node) { + if (isIdentifier(node) && !isTagName(node) && node.escapedText || isStringOrNumericLiteralLike(node) && literalIsName(node)) { + const text = getEscapedTextOfIdentifierOrLiteral(node); + nameTable.set(text, nameTable.get(text) === undefined ? node.pos : -1); + } + else if (isPrivateIdentifier(node)) { + const text = node.escapedText; + nameTable.set(text, nameTable.get(text) === undefined ? node.pos : -1); + } + forEachChild(node, walk); + if (hasJSDocNodes(node)) { + for (const jsDoc of node.jsDoc!) { + forEachChild(jsDoc, walk); } } - return checker.getSymbolAtLocation(node); - } - - /** Gets all symbols for one property. Does not get symbols for every property. */ - /* @internal */ - export function getPropertySymbolsFromContextualType(node: ObjectLiteralElementWithName, checker: TypeChecker, contextualType: Type, unionSymbolOk: boolean): readonly Symbol[] { - const name = getNameFromPropertyName(node.name); - if (!name) return emptyArray; - if (!contextualType.isUnion()) { - const symbol = contextualType.getProperty(name); - return symbol ? [symbol] : emptyArray; - } - - const discriminatedPropertySymbols = mapDefined(contextualType.types, t => isObjectLiteralExpression(node.parent) && checker.isTypeInvalidDueToUnionDiscriminant(t, node.parent) ? undefined : t.getProperty(name)); - if (unionSymbolOk && (discriminatedPropertySymbols.length === 0 || discriminatedPropertySymbols.length === contextualType.types.length)) { - const symbol = contextualType.getProperty(name); - if (symbol) return [symbol]; - } - if (discriminatedPropertySymbols.length === 0) { - // Bad discriminant -- do again without discriminating - return mapDefined(contextualType.types, t => t.getProperty(name)); - } - return discriminatedPropertySymbols; - } - - function isArgumentOfElementAccessExpression(node: Node) { - return node && - node.parent && - node.parent.kind === SyntaxKind.ElementAccessExpression && - (node.parent).argumentExpression === node; - } - - /// getDefaultLibraryFilePath - declare const __dirname: string; - - /** - * Get the path of the default library files (lib.d.ts) as distributed with the typescript - * node package. - * The functionality is not supported if the ts module is consumed outside of a node module. - */ - export function getDefaultLibFilePath(options: CompilerOptions): string { - // Check __dirname is defined and that we are on a node.js system. - if (typeof __dirname !== "undefined") { - return __dirname + directorySeparator + getDefaultLibFileName(options); + }); +} +/** + * We want to store any numbers/strings if they were a name that could be + * related to a declaration. So, if we have 'import x = require("something")' + * then we want 'something' to be in the name table. Similarly, if we have + * "a['propname']" then we want to store "propname" in the name table. + */ +function literalIsName(node: StringLiteralLike | NumericLiteral): boolean { + return isDeclarationName(node) || + node.parent.kind === SyntaxKind.ExternalModuleReference || + isArgumentOfElementAccessExpression(node) || + isLiteralComputedPropertyDeclarationName(node); +} +/** + * Returns the containing object literal property declaration given a possible name node, e.g. "a" in x = { "a": 1 } + */ +/* @internal */ +export function getContainingObjectLiteralElement(node: Node): ObjectLiteralElementWithName | undefined { + const element = getContainingObjectLiteralElementWorker(node); + return element && (isObjectLiteralExpression(element.parent) || isJsxAttributes(element.parent)) ? element as ObjectLiteralElementWithName : undefined; +} +function getContainingObjectLiteralElementWorker(node: Node): ObjectLiteralElement | undefined { + switch (node.kind) { + case SyntaxKind.StringLiteral: + case SyntaxKind.NoSubstitutionTemplateLiteral: + case SyntaxKind.NumericLiteral: + if (node.parent.kind === SyntaxKind.ComputedPropertyName) { + return isObjectLiteralElement(node.parent.parent) ? node.parent.parent : undefined; + } + // falls through + case SyntaxKind.Identifier: + return isObjectLiteralElement(node.parent) && + (node.parent.parent.kind === SyntaxKind.ObjectLiteralExpression || node.parent.parent.kind === SyntaxKind.JsxAttributes) && + node.parent.name === node ? node.parent : undefined; + } + return undefined; +} +/* @internal */ +export type ObjectLiteralElementWithName = ObjectLiteralElement & { + name: PropertyName; + parent: ObjectLiteralExpression | JsxAttributes; +}; +function getSymbolAtLocationForQuickInfo(node: Node, checker: TypeChecker): Symbol | undefined { + const object = getContainingObjectLiteralElement(node); + if (object) { + const contextualType = checker.getContextualType(object.parent); + const properties = contextualType && getPropertySymbolsFromContextualType(object, checker, contextualType, /*unionSymbolOk*/ false); + if (properties && properties.length === 1) { + return first(properties); } - - throw new Error("getDefaultLibFilePath is only supported when consumed as a node module. "); } - - setObjectAllocator(getServicesObjectAllocator()); + return checker.getSymbolAtLocation(node); +} +/** Gets all symbols for one property. Does not get symbols for every property. */ +/* @internal */ +export function getPropertySymbolsFromContextualType(node: ObjectLiteralElementWithName, checker: TypeChecker, contextualType: Type, unionSymbolOk: boolean): readonly Symbol[] { + const name = getNameFromPropertyName(node.name); + if (!name) + return emptyArray; + if (!contextualType.isUnion()) { + const symbol = contextualType.getProperty(name); + return symbol ? [symbol] : emptyArray; + } + const discriminatedPropertySymbols = mapDefined(contextualType.types, t => isObjectLiteralExpression(node.parent) && checker.isTypeInvalidDueToUnionDiscriminant(t, node.parent) ? undefined : t.getProperty(name)); + if (unionSymbolOk && (discriminatedPropertySymbols.length === 0 || discriminatedPropertySymbols.length === contextualType.types.length)) { + const symbol = contextualType.getProperty(name); + if (symbol) + return [symbol]; + } + if (discriminatedPropertySymbols.length === 0) { + // Bad discriminant -- do again without discriminating + return mapDefined(contextualType.types, t => t.getProperty(name)); + } + return discriminatedPropertySymbols; +} +function isArgumentOfElementAccessExpression(node: Node) { + return node && + node.parent && + node.parent.kind === SyntaxKind.ElementAccessExpression && + (node.parent).argumentExpression === node; +} +/// getDefaultLibraryFilePath +declare const __dirname: string; +/** + * Get the path of the default library files (lib.d.ts) as distributed with the typescript + * node package. + * The functionality is not supported if the ts module is consumed outside of a node module. + */ +export function getDefaultLibFilePath(options: CompilerOptions): string { + // Check __dirname is defined and that we are on a node.js system. + if (typeof __dirname !== "undefined") { + return __dirname + directorySeparator + getDefaultLibFileName(options); + } + throw new Error("getDefaultLibFilePath is only supported when consumed as a node module. "); } +setObjectAllocator(getServicesObjectAllocator()); diff --git a/src/services/shims.ts b/src/services/shims.ts index c447b65854be4..35b40be4b3dce 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -1,3 +1,5 @@ +import { JsTyping, TypeAcquisition, CompilerOptions, MapLike, ScriptKind, HostCancellationToken, LanguageService, UserPreferences, SignatureHelpItemsOptions, RenameInfoOptions, EmitOutput, EndOfLineState, IScriptSnapshot, TextChangeRange, createTextChangeRange, createTextSpan, LanguageServiceHost, ResolvedModuleFull, ResolvedTypeReferenceDirective, map, getProperty, extensionFromPath, ThrottledCancellationToken, getFileMatcherPatterns, ParseConfigHost, ModuleResolutionHost, timestamp, isString, OperationCanceledException, Diagnostic, flattenDiagnosticMessageText, diagnosticCategoryName, getNewLineOrDefaultFromHost, EditorOptions, toFileNameLowerCase, normalizeSlashes, filter, GetCompletionsAtPositionOptions, FormatCodeOptions, Classifications, Classifier, createClassifier, resolveModuleName, Extension, resolveTypeReferenceDirective, preProcessFile, getSnapshotText, getAutomaticTypeDirectiveNames, FileReference, parseJsonText, parseJsonSourceFileConfigFileContent, getDirectoryPath, getDefaultCompilerOptions, createGetCanonicalFileName, toPath, DocumentRegistry, servicesVersion, createDocumentRegistry, createLanguageService, clear } from "./ts"; +import * as ts from "./ts"; // // Copyright (c) Microsoft Corporation. All rights reserved. // @@ -12,1287 +14,970 @@ // See the License for the specific language governing permissions and // limitations under the License. // - /* @internal */ -let debugObjectHost: { CollectGarbage(): void } = (function (this: any) { return this; })(); // eslint-disable-line prefer-const - +let debugObjectHost: { + CollectGarbage(): void; +} = (function (this: any) { return this; })(); // eslint-disable-line prefer-const // We need to use 'null' to interface with the managed side. /* eslint-disable no-in-operator */ - /* @internal */ -namespace ts { - interface DiscoverTypingsInfo { - fileNames: string[]; // The file names that belong to the same project. - projectRootPath: string; // The path to the project root directory - safeListPath: string; // The path used to retrieve the safe list - packageNameToTypingLocation: Map; // The map of package names to their cached typing locations and installed versions - typeAcquisition: TypeAcquisition; // Used to customize the type acquisition process - compilerOptions: CompilerOptions; // Used as a source for typing inference - unresolvedImports: readonly string[]; // List of unresolved module ids from imports - typesRegistry: ReadonlyMap>; // The map of available typings in npm to maps of TS versions to their latest supported versions - } - - export interface ScriptSnapshotShim { - /** Gets a portion of the script snapshot specified by [start, end). */ - getText(start: number, end: number): string; - - /** Gets the length of this script snapshot. */ - getLength(): number; - - /** - * Returns a JSON-encoded value of the type: - * { span: { start: number; length: number }; newLength: number } - * - * Or undefined value if there was no change. - */ - getChangeRange(oldSnapshot: ScriptSnapshotShim): string | undefined; - - /** Releases all resources held by this script snapshot */ - dispose?(): void; - } - - export interface Logger { - log(s: string): void; - trace(s: string): void; - error(s: string): void; - } - - /** Public interface of the host of a language service shim instance. */ - export interface LanguageServiceShimHost extends Logger { - getCompilationSettings(): string; - - /** Returns a JSON-encoded value of the type: string[] */ - getScriptFileNames(): string; - getScriptKind?(fileName: string): ScriptKind; - getScriptVersion(fileName: string): string; - getScriptSnapshot(fileName: string): ScriptSnapshotShim; - getLocalizedDiagnosticMessages(): string; - getCancellationToken(): HostCancellationToken; - getCurrentDirectory(): string; - getDirectories(path: string): string; - getDefaultLibFileName(options: string): string; - getNewLine?(): string; - getProjectVersion?(): string; - useCaseSensitiveFileNames?(): boolean; - - getTypeRootsVersion?(): number; - readDirectory(rootDir: string, extension: string, basePaths?: string, excludeEx?: string, includeFileEx?: string, includeDirEx?: string, depth?: number): string; - readFile(path: string, encoding?: string): string | undefined; - fileExists(path: string): boolean; - - getModuleResolutionsForFile?(fileName: string): string; - getTypeReferenceDirectiveResolutionsForFile?(fileName: string): string; - directoryExists(directoryName: string): boolean; - } - - /** Public interface of the core-services host instance used in managed side */ - export interface CoreServicesShimHost extends Logger { - directoryExists(directoryName: string): boolean; - fileExists(fileName: string): boolean; - getCurrentDirectory(): string; - getDirectories(path: string): string; - - /** - * Returns a JSON-encoded value of the type: string[] - * - * @param exclude A JSON encoded string[] containing the paths to exclude - * when enumerating the directory. - */ - readDirectory(rootDir: string, extension: string, basePaths?: string, excludeEx?: string, includeFileEx?: string, includeDirEx?: string, depth?: number): string; - - /** - * Read arbitrary text files on disk, i.e. when resolution procedure needs the content of 'package.json' to determine location of bundled typings for node modules - */ - readFile(fileName: string): string | undefined; - realpath?(path: string): string; - trace(s: string): void; - useCaseSensitiveFileNames?(): boolean; - } - - /// - /// Pre-processing - /// - // Note: This is being using by the host (VS) and is marshaled back and forth. - // When changing this make sure the changes are reflected in the managed side as well - export interface ShimsFileReference { - path: string; - position: number; - length: number; +interface DiscoverTypingsInfo { + fileNames: string[]; // The file names that belong to the same project. + projectRootPath: string; // The path to the project root directory + safeListPath: string; // The path used to retrieve the safe list + packageNameToTypingLocation: ts.Map; // The map of package names to their cached typing locations and installed versions + typeAcquisition: TypeAcquisition; // Used to customize the type acquisition process + compilerOptions: CompilerOptions; // Used as a source for typing inference + unresolvedImports: readonly string[]; // List of unresolved module ids from imports + typesRegistry: ts.ReadonlyMap>; // The map of available typings in npm to maps of TS versions to their latest supported versions +} +/* @internal */ +export interface ScriptSnapshotShim { + /** Gets a portion of the script snapshot specified by [start, end). */ + getText(start: number, end: number): string; + /** Gets the length of this script snapshot. */ + getLength(): number; + /** + * Returns a JSON-encoded value of the type: + * { span: { start: number; length: number }; newLength: number } + * + * Or undefined value if there was no change. + */ + getChangeRange(oldSnapshot: ScriptSnapshotShim): string | undefined; + /** Releases all resources held by this script snapshot */ + dispose?(): void; +} +/* @internal */ +export interface Logger { + log(s: string): void; + trace(s: string): void; + error(s: string): void; +} +/** Public interface of the host of a language service shim instance. */ +/* @internal */ +export interface LanguageServiceShimHost extends Logger { + getCompilationSettings(): string; + /** Returns a JSON-encoded value of the type: string[] */ + getScriptFileNames(): string; + getScriptKind?(fileName: string): ScriptKind; + getScriptVersion(fileName: string): string; + getScriptSnapshot(fileName: string): ScriptSnapshotShim; + getLocalizedDiagnosticMessages(): string; + getCancellationToken(): HostCancellationToken; + getCurrentDirectory(): string; + getDirectories(path: string): string; + getDefaultLibFileName(options: string): string; + getNewLine?(): string; + getProjectVersion?(): string; + useCaseSensitiveFileNames?(): boolean; + getTypeRootsVersion?(): number; + readDirectory(rootDir: string, extension: string, basePaths?: string, excludeEx?: string, includeFileEx?: string, includeDirEx?: string, depth?: number): string; + readFile(path: string, encoding?: string): string | undefined; + fileExists(path: string): boolean; + getModuleResolutionsForFile?(fileName: string): string; + getTypeReferenceDirectiveResolutionsForFile?(fileName: string): string; + directoryExists(directoryName: string): boolean; +} +/** Public interface of the core-services host instance used in managed side */ +/* @internal */ +export interface CoreServicesShimHost extends Logger { + directoryExists(directoryName: string): boolean; + fileExists(fileName: string): boolean; + getCurrentDirectory(): string; + getDirectories(path: string): string; + /** + * Returns a JSON-encoded value of the type: string[] + * + * @param exclude A JSON encoded string[] containing the paths to exclude + * when enumerating the directory. + */ + readDirectory(rootDir: string, extension: string, basePaths?: string, excludeEx?: string, includeFileEx?: string, includeDirEx?: string, depth?: number): string; + /** + * Read arbitrary text files on disk, i.e. when resolution procedure needs the content of 'package.json' to determine location of bundled typings for node modules + */ + readFile(fileName: string): string | undefined; + realpath?(path: string): string; + trace(s: string): void; + useCaseSensitiveFileNames?(): boolean; +} +/// +/// Pre-processing +/// +// Note: This is being using by the host (VS) and is marshaled back and forth. +// When changing this make sure the changes are reflected in the managed side as well +/* @internal */ +export interface ShimsFileReference { + path: string; + position: number; + length: number; +} +/** Public interface of a language service instance shim. */ +/* @internal */ +export interface ShimFactory { + registerShim(shim: Shim): void; + unregisterShim(shim: Shim): void; +} +/* @internal */ +export interface Shim { + dispose(_dummy: {}): void; +} +/* @internal */ +export interface LanguageServiceShim extends Shim { + languageService: LanguageService; + dispose(_dummy: {}): void; + refresh(throwOnError: boolean): void; + cleanupSemanticCache(): void; + getSyntacticDiagnostics(fileName: string): string; + getSemanticDiagnostics(fileName: string): string; + getSuggestionDiagnostics(fileName: string): string; + getCompilerOptionsDiagnostics(): string; + getSyntacticClassifications(fileName: string, start: number, length: number): string; + getSemanticClassifications(fileName: string, start: number, length: number): string; + getEncodedSyntacticClassifications(fileName: string, start: number, length: number): string; + getEncodedSemanticClassifications(fileName: string, start: number, length: number): string; + getCompletionsAtPosition(fileName: string, position: number, preferences: UserPreferences | undefined): string; + getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: string /*Services.FormatCodeOptions*/ | undefined, source: string | undefined, preferences: UserPreferences | undefined): string; + getQuickInfoAtPosition(fileName: string, position: number): string; + getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): string; + getBreakpointStatementAtPosition(fileName: string, position: number): string; + getSignatureHelpItems(fileName: string, position: number, options: SignatureHelpItemsOptions | undefined): string; + /** + * Returns a JSON-encoded value of the type: + * { canRename: boolean, localizedErrorMessage: string, displayName: string, fullDisplayName: string, kind: string, kindModifiers: string, triggerSpan: { start; length } } + */ + getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): string; + getSmartSelectionRange(fileName: string, position: number): string; + /** + * Returns a JSON-encoded value of the type: + * { fileName: string, textSpan: { start: number, length: number } }[] + */ + findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): string; + /** + * Returns a JSON-encoded value of the type: + * { fileName: string; textSpan: { start: number; length: number}; kind: string; name: string; containerKind: string; containerName: string } + * + * Or undefined value if no definition can be found. + */ + getDefinitionAtPosition(fileName: string, position: number): string; + getDefinitionAndBoundSpan(fileName: string, position: number): string; + /** + * Returns a JSON-encoded value of the type: + * { fileName: string; textSpan: { start: number; length: number}; kind: string; name: string; containerKind: string; containerName: string } + * + * Or undefined value if no definition can be found. + */ + getTypeDefinitionAtPosition(fileName: string, position: number): string; + /** + * Returns a JSON-encoded value of the type: + * { fileName: string; textSpan: { start: number; length: number}; }[] + */ + getImplementationAtPosition(fileName: string, position: number): string; + /** + * Returns a JSON-encoded value of the type: + * { fileName: string; textSpan: { start: number; length: number}; isWriteAccess: boolean, isDefinition?: boolean }[] + */ + getReferencesAtPosition(fileName: string, position: number): string; + /** + * Returns a JSON-encoded value of the type: + * { definition: ; references: [] }[] + */ + findReferences(fileName: string, position: number): string; + /** + * @deprecated + * Returns a JSON-encoded value of the type: + * { fileName: string; textSpan: { start: number; length: number}; isWriteAccess: boolean }[] + */ + getOccurrencesAtPosition(fileName: string, position: number): string; + /** + * Returns a JSON-encoded value of the type: + * { fileName: string; highlights: { start: number; length: number, isDefinition: boolean }[] }[] + * + * @param fileToSearch A JSON encoded string[] containing the file names that should be + * considered when searching. + */ + getDocumentHighlights(fileName: string, position: number, filesToSearch: string): string; + /** + * Returns a JSON-encoded value of the type: + * { name: string; kind: string; kindModifiers: string; containerName: string; containerKind: string; matchKind: string; fileName: string; textSpan: { start: number; length: number}; } [] = []; + */ + getNavigateToItems(searchValue: string, maxResultCount?: number, fileName?: string): string; + /** + * Returns a JSON-encoded value of the type: + * { text: string; kind: string; kindModifiers: string; bolded: boolean; grayed: boolean; indent: number; spans: { start: number; length: number; }[]; childItems: [] } [] = []; + */ + getNavigationBarItems(fileName: string): string; + /** Returns a JSON-encoded value of the type ts.NavigationTree. */ + getNavigationTree(fileName: string): string; + /** + * Returns a JSON-encoded value of the type: + * { textSpan: { start: number, length: number }; hintSpan: { start: number, length: number }; bannerText: string; autoCollapse: boolean } [] = []; + */ + getOutliningSpans(fileName: string): string; + getTodoComments(fileName: string, todoCommentDescriptors: string): string; + getBraceMatchingAtPosition(fileName: string, position: number): string; + getIndentationAtPosition(fileName: string, position: number, options: string /*Services.EditorOptions*/): string; + getFormattingEditsForRange(fileName: string, start: number, end: number, options: string /*Services.FormatCodeOptions*/): string; + getFormattingEditsForDocument(fileName: string, options: string /*Services.FormatCodeOptions*/): string; + getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: string /*Services.FormatCodeOptions*/): string; + /** + * Returns JSON-encoded value of the type TextInsertion. + */ + getDocCommentTemplateAtPosition(fileName: string, position: number): string; + /** + * Returns JSON-encoded boolean to indicate whether we should support brace location + * at the current position. + * E.g. we don't want brace completion inside string-literals, comments, etc. + */ + isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): string; + /** + * Returns a JSON-encoded TextSpan | undefined indicating the range of the enclosing comment, if it exists. + */ + getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): string; + prepareCallHierarchy(fileName: string, position: number): string; + provideCallHierarchyIncomingCalls(fileName: string, position: number): string; + provideCallHierarchyOutgoingCalls(fileName: string, position: number): string; + getEmitOutput(fileName: string): string; + getEmitOutputObject(fileName: string): EmitOutput; +} +/* @internal */ +export interface ClassifierShim extends Shim { + getEncodedLexicalClassifications(text: string, lexState: EndOfLineState, syntacticClassifierAbsent?: boolean): string; + getClassificationsForLine(text: string, lexState: EndOfLineState, syntacticClassifierAbsent?: boolean): string; +} +/* @internal */ +export interface CoreServicesShim extends Shim { + getAutomaticTypeDirectiveNames(compilerOptionsJson: string): string; + getPreProcessedFileInfo(fileName: string, sourceText: IScriptSnapshot): string; + getTSConfigFileInfo(fileName: string, sourceText: IScriptSnapshot): string; + getDefaultCompilationSettings(): string; + discoverTypings(discoverTypingsJson: string): string; +} +/* @internal */ +function logInternalError(logger: Logger, err: Error) { + if (logger) { + logger.log("*INTERNAL ERROR* - Exception in typescript services: " + err.message); } - - /** Public interface of a language service instance shim. */ - export interface ShimFactory { - registerShim(shim: Shim): void; - unregisterShim(shim: Shim): void; - } - - export interface Shim { - dispose(_dummy: {}): void; - } - - export interface LanguageServiceShim extends Shim { - languageService: LanguageService; - - dispose(_dummy: {}): void; - - refresh(throwOnError: boolean): void; - - cleanupSemanticCache(): void; - - getSyntacticDiagnostics(fileName: string): string; - getSemanticDiagnostics(fileName: string): string; - getSuggestionDiagnostics(fileName: string): string; - getCompilerOptionsDiagnostics(): string; - - getSyntacticClassifications(fileName: string, start: number, length: number): string; - getSemanticClassifications(fileName: string, start: number, length: number): string; - getEncodedSyntacticClassifications(fileName: string, start: number, length: number): string; - getEncodedSemanticClassifications(fileName: string, start: number, length: number): string; - - getCompletionsAtPosition(fileName: string, position: number, preferences: UserPreferences | undefined): string; - getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: string/*Services.FormatCodeOptions*/ | undefined, source: string | undefined, preferences: UserPreferences | undefined): string; - - getQuickInfoAtPosition(fileName: string, position: number): string; - - getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): string; - getBreakpointStatementAtPosition(fileName: string, position: number): string; - - getSignatureHelpItems(fileName: string, position: number, options: SignatureHelpItemsOptions | undefined): string; - - /** - * Returns a JSON-encoded value of the type: - * { canRename: boolean, localizedErrorMessage: string, displayName: string, fullDisplayName: string, kind: string, kindModifiers: string, triggerSpan: { start; length } } - */ - getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): string; - getSmartSelectionRange(fileName: string, position: number): string; - - /** - * Returns a JSON-encoded value of the type: - * { fileName: string, textSpan: { start: number, length: number } }[] - */ - findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): string; - - /** - * Returns a JSON-encoded value of the type: - * { fileName: string; textSpan: { start: number; length: number}; kind: string; name: string; containerKind: string; containerName: string } - * - * Or undefined value if no definition can be found. - */ - getDefinitionAtPosition(fileName: string, position: number): string; - - getDefinitionAndBoundSpan(fileName: string, position: number): string; - - /** - * Returns a JSON-encoded value of the type: - * { fileName: string; textSpan: { start: number; length: number}; kind: string; name: string; containerKind: string; containerName: string } - * - * Or undefined value if no definition can be found. - */ - getTypeDefinitionAtPosition(fileName: string, position: number): string; - - /** - * Returns a JSON-encoded value of the type: - * { fileName: string; textSpan: { start: number; length: number}; }[] - */ - getImplementationAtPosition(fileName: string, position: number): string; - - /** - * Returns a JSON-encoded value of the type: - * { fileName: string; textSpan: { start: number; length: number}; isWriteAccess: boolean, isDefinition?: boolean }[] - */ - getReferencesAtPosition(fileName: string, position: number): string; - - /** - * Returns a JSON-encoded value of the type: - * { definition: ; references: [] }[] - */ - findReferences(fileName: string, position: number): string; - - /** - * @deprecated - * Returns a JSON-encoded value of the type: - * { fileName: string; textSpan: { start: number; length: number}; isWriteAccess: boolean }[] - */ - getOccurrencesAtPosition(fileName: string, position: number): string; - - /** - * Returns a JSON-encoded value of the type: - * { fileName: string; highlights: { start: number; length: number, isDefinition: boolean }[] }[] - * - * @param fileToSearch A JSON encoded string[] containing the file names that should be - * considered when searching. - */ - getDocumentHighlights(fileName: string, position: number, filesToSearch: string): string; - - /** - * Returns a JSON-encoded value of the type: - * { name: string; kind: string; kindModifiers: string; containerName: string; containerKind: string; matchKind: string; fileName: string; textSpan: { start: number; length: number}; } [] = []; - */ - getNavigateToItems(searchValue: string, maxResultCount?: number, fileName?: string): string; - - /** - * Returns a JSON-encoded value of the type: - * { text: string; kind: string; kindModifiers: string; bolded: boolean; grayed: boolean; indent: number; spans: { start: number; length: number; }[]; childItems: [] } [] = []; - */ - getNavigationBarItems(fileName: string): string; - - /** Returns a JSON-encoded value of the type ts.NavigationTree. */ - getNavigationTree(fileName: string): string; - - /** - * Returns a JSON-encoded value of the type: - * { textSpan: { start: number, length: number }; hintSpan: { start: number, length: number }; bannerText: string; autoCollapse: boolean } [] = []; - */ - getOutliningSpans(fileName: string): string; - - getTodoComments(fileName: string, todoCommentDescriptors: string): string; - - getBraceMatchingAtPosition(fileName: string, position: number): string; - getIndentationAtPosition(fileName: string, position: number, options: string/*Services.EditorOptions*/): string; - - getFormattingEditsForRange(fileName: string, start: number, end: number, options: string/*Services.FormatCodeOptions*/): string; - getFormattingEditsForDocument(fileName: string, options: string/*Services.FormatCodeOptions*/): string; - getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: string/*Services.FormatCodeOptions*/): string; - - /** - * Returns JSON-encoded value of the type TextInsertion. - */ - getDocCommentTemplateAtPosition(fileName: string, position: number): string; - - /** - * Returns JSON-encoded boolean to indicate whether we should support brace location - * at the current position. - * E.g. we don't want brace completion inside string-literals, comments, etc. - */ - isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): string; - - /** - * Returns a JSON-encoded TextSpan | undefined indicating the range of the enclosing comment, if it exists. - */ - getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): string; - - prepareCallHierarchy(fileName: string, position: number): string; - provideCallHierarchyIncomingCalls(fileName: string, position: number): string; - provideCallHierarchyOutgoingCalls(fileName: string, position: number): string; - - getEmitOutput(fileName: string): string; - getEmitOutputObject(fileName: string): EmitOutput; - } - - export interface ClassifierShim extends Shim { - getEncodedLexicalClassifications(text: string, lexState: EndOfLineState, syntacticClassifierAbsent?: boolean): string; - getClassificationsForLine(text: string, lexState: EndOfLineState, syntacticClassifierAbsent?: boolean): string; - } - - export interface CoreServicesShim extends Shim { - getAutomaticTypeDirectiveNames(compilerOptionsJson: string): string; - getPreProcessedFileInfo(fileName: string, sourceText: IScriptSnapshot): string; - getTSConfigFileInfo(fileName: string, sourceText: IScriptSnapshot): string; - getDefaultCompilationSettings(): string; - discoverTypings(discoverTypingsJson: string): string; - } - - function logInternalError(logger: Logger, err: Error) { - if (logger) { - logger.log("*INTERNAL ERROR* - Exception in typescript services: " + err.message); - } +} +/* @internal */ +class ScriptSnapshotShimAdapter implements IScriptSnapshot { + constructor(private scriptSnapshotShim: ScriptSnapshotShim) { } - - class ScriptSnapshotShimAdapter implements IScriptSnapshot { - constructor(private scriptSnapshotShim: ScriptSnapshotShim) { - } - - public getText(start: number, end: number): string { - return this.scriptSnapshotShim.getText(start, end); - } - - public getLength(): number { - return this.scriptSnapshotShim.getLength(); - } - - public getChangeRange(oldSnapshot: IScriptSnapshot): TextChangeRange | undefined { - const oldSnapshotShim = oldSnapshot; - const encoded = this.scriptSnapshotShim.getChangeRange(oldSnapshotShim.scriptSnapshotShim); - /* eslint-disable no-null/no-null */ - if (encoded === null) { - return null!; // TODO: GH#18217 - } - /* eslint-enable no-null/no-null */ - - const decoded: { span: { start: number; length: number; }; newLength: number; } = JSON.parse(encoded!); // TODO: GH#18217 - return createTextChangeRange( - createTextSpan(decoded.span.start, decoded.span.length), decoded.newLength); - } - - public dispose(): void { - // if scriptSnapshotShim is a COM object then property check becomes method call with no arguments - // 'in' does not have this effect - if ("dispose" in this.scriptSnapshotShim) { - this.scriptSnapshotShim.dispose!(); // TODO: GH#18217 Can we just use `if (this.scriptSnapshotShim.dispose)`? - } - } + public getText(start: number, end: number): string { + return this.scriptSnapshotShim.getText(start, end); } - - export class LanguageServiceShimHostAdapter implements LanguageServiceHost { - private loggingEnabled = false; - private tracingEnabled = false; - - public resolveModuleNames: ((moduleName: string[], containingFile: string) => (ResolvedModuleFull | undefined)[]) | undefined; - public resolveTypeReferenceDirectives: ((typeDirectiveNames: string[], containingFile: string) => (ResolvedTypeReferenceDirective | undefined)[]) | undefined; - public directoryExists: ((directoryName: string) => boolean) | undefined; - - constructor(private shimHost: LanguageServiceShimHost) { - // if shimHost is a COM object then property check will become method call with no arguments. - // 'in' does not have this effect. - if ("getModuleResolutionsForFile" in this.shimHost) { - this.resolveModuleNames = (moduleNames, containingFile) => { - const resolutionsInFile = >JSON.parse(this.shimHost.getModuleResolutionsForFile!(containingFile)); // TODO: GH#18217 - return map(moduleNames, name => { - const result = getProperty(resolutionsInFile, name); - return result ? { resolvedFileName: result, extension: extensionFromPath(result), isExternalLibraryImport: false } : undefined; - }); - }; - } - if ("directoryExists" in this.shimHost) { - this.directoryExists = directoryName => this.shimHost.directoryExists(directoryName); - } - if ("getTypeReferenceDirectiveResolutionsForFile" in this.shimHost) { - this.resolveTypeReferenceDirectives = (typeDirectiveNames, containingFile) => { - const typeDirectivesForFile = >JSON.parse(this.shimHost.getTypeReferenceDirectiveResolutionsForFile!(containingFile)); // TODO: GH#18217 - return map(typeDirectiveNames, name => getProperty(typeDirectivesForFile, name)); - }; - } - } - - public log(s: string): void { - if (this.loggingEnabled) { - this.shimHost.log(s); - } - } - - public trace(s: string): void { - if (this.tracingEnabled) { - this.shimHost.trace(s); - } - } - - public error(s: string): void { - this.shimHost.error(s); - } - - public getProjectVersion(): string { - if (!this.shimHost.getProjectVersion) { - // shimmed host does not support getProjectVersion - return undefined!; // TODO: GH#18217 - } - - return this.shimHost.getProjectVersion(); - } - - public getTypeRootsVersion(): number { - if (!this.shimHost.getTypeRootsVersion) { - return 0; - } - return this.shimHost.getTypeRootsVersion(); - } - - public useCaseSensitiveFileNames(): boolean { - return this.shimHost.useCaseSensitiveFileNames ? this.shimHost.useCaseSensitiveFileNames() : false; + public getLength(): number { + return this.scriptSnapshotShim.getLength(); + } + public getChangeRange(oldSnapshot: IScriptSnapshot): TextChangeRange | undefined { + const oldSnapshotShim = oldSnapshot; + const encoded = this.scriptSnapshotShim.getChangeRange(oldSnapshotShim.scriptSnapshotShim); + /* eslint-disable no-null/no-null */ + if (encoded === null) { + return null!; // TODO: GH#18217 + } + /* eslint-enable no-null/no-null */ + const decoded: { + span: { + start: number; + length: number; + }; + newLength: number; + } = JSON.parse(encoded!); // TODO: GH#18217 + return createTextChangeRange(createTextSpan(decoded.span.start, decoded.span.length), decoded.newLength); + } + public dispose(): void { + // if scriptSnapshotShim is a COM object then property check becomes method call with no arguments + // 'in' does not have this effect + if ("dispose" in this.scriptSnapshotShim) { + this.scriptSnapshotShim.dispose!(); // TODO: GH#18217 Can we just use `if (this.scriptSnapshotShim.dispose)`? } - - public getCompilationSettings(): CompilerOptions { - const settingsJson = this.shimHost.getCompilationSettings(); - // eslint-disable-next-line no-null/no-null - if (settingsJson === null || settingsJson === "") { - throw Error("LanguageServiceShimHostAdapter.getCompilationSettings: empty compilationSettings"); - } - const compilerOptions = JSON.parse(settingsJson); - // permit language service to handle all files (filtering should be performed on the host side) - compilerOptions.allowNonTsExtensions = true; - return compilerOptions; + } +} +/* @internal */ +export class LanguageServiceShimHostAdapter implements LanguageServiceHost { + private loggingEnabled = false; + private tracingEnabled = false; + public resolveModuleNames: ((moduleName: string[], containingFile: string) => (ResolvedModuleFull | undefined)[]) | undefined; + public resolveTypeReferenceDirectives: ((typeDirectiveNames: string[], containingFile: string) => (ResolvedTypeReferenceDirective | undefined)[]) | undefined; + public directoryExists: ((directoryName: string) => boolean) | undefined; + constructor(private shimHost: LanguageServiceShimHost) { + // if shimHost is a COM object then property check will become method call with no arguments. + // 'in' does not have this effect. + if ("getModuleResolutionsForFile" in this.shimHost) { + this.resolveModuleNames = (moduleNames, containingFile) => { + const resolutionsInFile = (>JSON.parse(this.shimHost.getModuleResolutionsForFile!(containingFile))); // TODO: GH#18217 + return map(moduleNames, name => { + const result = getProperty(resolutionsInFile, name); + return result ? { resolvedFileName: result, extension: extensionFromPath(result), isExternalLibraryImport: false } : undefined; + }); + }; } - - public getScriptFileNames(): string[] { - const encoded = this.shimHost.getScriptFileNames(); - return JSON.parse(encoded); + if ("directoryExists" in this.shimHost) { + this.directoryExists = directoryName => this.shimHost.directoryExists(directoryName); } - - public getScriptSnapshot(fileName: string): IScriptSnapshot | undefined { - const scriptSnapshot = this.shimHost.getScriptSnapshot(fileName); - return scriptSnapshot && new ScriptSnapshotShimAdapter(scriptSnapshot); + if ("getTypeReferenceDirectiveResolutionsForFile" in this.shimHost) { + this.resolveTypeReferenceDirectives = (typeDirectiveNames, containingFile) => { + const typeDirectivesForFile = (>JSON.parse(this.shimHost.getTypeReferenceDirectiveResolutionsForFile!(containingFile))); // TODO: GH#18217 + return map(typeDirectiveNames, name => getProperty(typeDirectivesForFile, name)); + }; } - - public getScriptKind(fileName: string): ScriptKind { - if ("getScriptKind" in this.shimHost) { - return this.shimHost.getScriptKind!(fileName); // TODO: GH#18217 - } - else { - return ScriptKind.Unknown; - } - } - - public getScriptVersion(fileName: string): string { - return this.shimHost.getScriptVersion(fileName); + } + public log(s: string): void { + if (this.loggingEnabled) { + this.shimHost.log(s); } - - public getLocalizedDiagnosticMessages() { - /* eslint-disable no-null/no-null */ - const diagnosticMessagesJson = this.shimHost.getLocalizedDiagnosticMessages(); - if (diagnosticMessagesJson === null || diagnosticMessagesJson === "") { - return null; - } - - try { - return JSON.parse(diagnosticMessagesJson); - } - catch (e) { - this.log(e.description || "diagnosticMessages.generated.json has invalid JSON format"); - return null; - } - /* eslint-enable no-null/no-null */ + } + public trace(s: string): void { + if (this.tracingEnabled) { + this.shimHost.trace(s); } - - public getCancellationToken(): HostCancellationToken { - const hostCancellationToken = this.shimHost.getCancellationToken(); - return new ThrottledCancellationToken(hostCancellationToken); + } + public error(s: string): void { + this.shimHost.error(s); + } + public getProjectVersion(): string { + if (!this.shimHost.getProjectVersion) { + // shimmed host does not support getProjectVersion + return undefined!; // TODO: GH#18217 } - - public getCurrentDirectory(): string { - return this.shimHost.getCurrentDirectory(); + return this.shimHost.getProjectVersion(); + } + public getTypeRootsVersion(): number { + if (!this.shimHost.getTypeRootsVersion) { + return 0; } - - public getDirectories(path: string): string[] { - return JSON.parse(this.shimHost.getDirectories(path)); + return this.shimHost.getTypeRootsVersion(); + } + public useCaseSensitiveFileNames(): boolean { + return this.shimHost.useCaseSensitiveFileNames ? this.shimHost.useCaseSensitiveFileNames() : false; + } + public getCompilationSettings(): CompilerOptions { + const settingsJson = this.shimHost.getCompilationSettings(); + // eslint-disable-next-line no-null/no-null + if (settingsJson === null || settingsJson === "") { + throw Error("LanguageServiceShimHostAdapter.getCompilationSettings: empty compilationSettings"); + } + const compilerOptions = (JSON.parse(settingsJson)); + // permit language service to handle all files (filtering should be performed on the host side) + compilerOptions.allowNonTsExtensions = true; + return compilerOptions; + } + public getScriptFileNames(): string[] { + const encoded = this.shimHost.getScriptFileNames(); + return JSON.parse(encoded); + } + public getScriptSnapshot(fileName: string): IScriptSnapshot | undefined { + const scriptSnapshot = this.shimHost.getScriptSnapshot(fileName); + return scriptSnapshot && new ScriptSnapshotShimAdapter(scriptSnapshot); + } + public getScriptKind(fileName: string): ScriptKind { + if ("getScriptKind" in this.shimHost) { + return this.shimHost.getScriptKind!(fileName); // TODO: GH#18217 } - - public getDefaultLibFileName(options: CompilerOptions): string { - return this.shimHost.getDefaultLibFileName(JSON.stringify(options)); + else { + return ScriptKind.Unknown; } - - public readDirectory(path: string, extensions?: readonly string[], exclude?: string[], include?: string[], depth?: number): string[] { - const pattern = getFileMatcherPatterns(path, exclude, include, - this.shimHost.useCaseSensitiveFileNames!(), this.shimHost.getCurrentDirectory()); // TODO: GH#18217 - return JSON.parse(this.shimHost.readDirectory( - path, - JSON.stringify(extensions), - JSON.stringify(pattern.basePaths), - pattern.excludePattern, - pattern.includeFilePattern, - pattern.includeDirectoryPattern, - depth - )); + } + public getScriptVersion(fileName: string): string { + return this.shimHost.getScriptVersion(fileName); + } + public getLocalizedDiagnosticMessages() { + /* eslint-disable no-null/no-null */ + const diagnosticMessagesJson = this.shimHost.getLocalizedDiagnosticMessages(); + if (diagnosticMessagesJson === null || diagnosticMessagesJson === "") { + return null; } - - public readFile(path: string, encoding?: string): string | undefined { - return this.shimHost.readFile(path, encoding); + try { + return JSON.parse(diagnosticMessagesJson); } - - public fileExists(path: string): boolean { - return this.shimHost.fileExists(path); + catch (e) { + this.log(e.description || "diagnosticMessages.generated.json has invalid JSON format"); + return null; } + /* eslint-enable no-null/no-null */ } - - export class CoreServicesShimHostAdapter implements ParseConfigHost, ModuleResolutionHost, JsTyping.TypingResolutionHost { - - public directoryExists: (directoryName: string) => boolean; - public realpath: (path: string) => string; - public useCaseSensitiveFileNames: boolean; - - constructor(private shimHost: CoreServicesShimHost) { - this.useCaseSensitiveFileNames = this.shimHost.useCaseSensitiveFileNames ? this.shimHost.useCaseSensitiveFileNames() : false; - if ("directoryExists" in this.shimHost) { - this.directoryExists = directoryName => this.shimHost.directoryExists(directoryName); - } - else { - this.directoryExists = undefined!; // TODO: GH#18217 - } - if ("realpath" in this.shimHost) { - this.realpath = path => this.shimHost.realpath!(path); // TODO: GH#18217 - } - else { - this.realpath = undefined!; // TODO: GH#18217 - } - } - - public readDirectory(rootDir: string, extensions: readonly string[], exclude: readonly string[], include: readonly string[], depth?: number): string[] { - const pattern = getFileMatcherPatterns(rootDir, exclude, include, - this.shimHost.useCaseSensitiveFileNames!(), this.shimHost.getCurrentDirectory()); // TODO: GH#18217 - return JSON.parse(this.shimHost.readDirectory( - rootDir, - JSON.stringify(extensions), - JSON.stringify(pattern.basePaths), - pattern.excludePattern, - pattern.includeFilePattern, - pattern.includeDirectoryPattern, - depth - )); + public getCancellationToken(): HostCancellationToken { + const hostCancellationToken = this.shimHost.getCancellationToken(); + return new ThrottledCancellationToken(hostCancellationToken); + } + public getCurrentDirectory(): string { + return this.shimHost.getCurrentDirectory(); + } + public getDirectories(path: string): string[] { + return JSON.parse(this.shimHost.getDirectories(path)); + } + public getDefaultLibFileName(options: CompilerOptions): string { + return this.shimHost.getDefaultLibFileName(JSON.stringify(options)); + } + public readDirectory(path: string, extensions?: readonly string[], exclude?: string[], include?: string[], depth?: number): string[] { + const pattern = getFileMatcherPatterns(path, exclude, include, this.shimHost.useCaseSensitiveFileNames!(), this.shimHost.getCurrentDirectory()); // TODO: GH#18217 + return JSON.parse(this.shimHost.readDirectory(path, JSON.stringify(extensions), JSON.stringify(pattern.basePaths), pattern.excludePattern, pattern.includeFilePattern, pattern.includeDirectoryPattern, depth)); + } + public readFile(path: string, encoding?: string): string | undefined { + return this.shimHost.readFile(path, encoding); + } + public fileExists(path: string): boolean { + return this.shimHost.fileExists(path); + } +} +/* @internal */ +export class CoreServicesShimHostAdapter implements ParseConfigHost, ModuleResolutionHost, JsTyping.TypingResolutionHost { + public directoryExists: (directoryName: string) => boolean; + public realpath: (path: string) => string; + public useCaseSensitiveFileNames: boolean; + constructor(private shimHost: CoreServicesShimHost) { + this.useCaseSensitiveFileNames = this.shimHost.useCaseSensitiveFileNames ? this.shimHost.useCaseSensitiveFileNames() : false; + if ("directoryExists" in this.shimHost) { + this.directoryExists = directoryName => this.shimHost.directoryExists(directoryName); } - - public fileExists(fileName: string): boolean { - return this.shimHost.fileExists(fileName); + else { + this.directoryExists = undefined!; // TODO: GH#18217 } - - public readFile(fileName: string): string | undefined { - return this.shimHost.readFile(fileName); + if ("realpath" in this.shimHost) { + this.realpath = path => this.shimHost.realpath!(path); // TODO: GH#18217 } - - public getDirectories(path: string): string[] { - return JSON.parse(this.shimHost.getDirectories(path)); + else { + this.realpath = undefined!; // TODO: GH#18217 } } - - function simpleForwardCall(logger: Logger, actionDescription: string, action: () => {}, logPerformance: boolean): {} { - let start: number | undefined; - if (logPerformance) { - logger.log(actionDescription); - start = timestamp(); - } - - const result = action(); - - if (logPerformance) { - const end = timestamp(); - logger.log(`${actionDescription} completed in ${end - start!} msec`); - if (isString(result)) { - let str = result; - if (str.length > 128) { - str = str.substring(0, 128) + "..."; - } - logger.log(` result.length=${str.length}, result='${JSON.stringify(str)}'`); - } - } - - return result; + public readDirectory(rootDir: string, extensions: readonly string[], exclude: readonly string[], include: readonly string[], depth?: number): string[] { + const pattern = getFileMatcherPatterns(rootDir, exclude, include, this.shimHost.useCaseSensitiveFileNames!(), this.shimHost.getCurrentDirectory()); // TODO: GH#18217 + return JSON.parse(this.shimHost.readDirectory(rootDir, JSON.stringify(extensions), JSON.stringify(pattern.basePaths), pattern.excludePattern, pattern.includeFilePattern, pattern.includeDirectoryPattern, depth)); } - - function forwardJSONCall(logger: Logger, actionDescription: string, action: () => {} | null | undefined, logPerformance: boolean): string { - return forwardCall(logger, actionDescription, /*returnJson*/ true, action, logPerformance); + public fileExists(fileName: string): boolean { + return this.shimHost.fileExists(fileName); } - - function forwardCall(logger: Logger, actionDescription: string, returnJson: boolean, action: () => T, logPerformance: boolean): T | string { - try { - const result = simpleForwardCall(logger, actionDescription, action, logPerformance); - return returnJson ? JSON.stringify({ result }) : result as T; - } - catch (err) { - if (err instanceof OperationCanceledException) { - return JSON.stringify({ canceled: true }); + public readFile(fileName: string): string | undefined { + return this.shimHost.readFile(fileName); + } + public getDirectories(path: string): string[] { + return JSON.parse(this.shimHost.getDirectories(path)); + } +} +/* @internal */ +function simpleForwardCall(logger: Logger, actionDescription: string, action: () => {}, logPerformance: boolean): {} { + let start: number | undefined; + if (logPerformance) { + logger.log(actionDescription); + start = timestamp(); + } + const result = action(); + if (logPerformance) { + const end = timestamp(); + logger.log(`${actionDescription} completed in ${end - start!} msec`); + if (isString(result)) { + let str = result; + if (str.length > 128) { + str = str.substring(0, 128) + "..."; } - logInternalError(logger, err); - err.description = actionDescription; - return JSON.stringify({ error: err }); + logger.log(` result.length=${str.length}, result='${JSON.stringify(str)}'`); } } - - - class ShimBase implements Shim { - constructor(private factory: ShimFactory) { - factory.registerShim(this); - } - public dispose(_dummy: {}): void { - this.factory.unregisterShim(this); + return result; +} +/* @internal */ +function forwardJSONCall(logger: Logger, actionDescription: string, action: () => {} | null | undefined, logPerformance: boolean): string { + return forwardCall(logger, actionDescription, /*returnJson*/ true, action, logPerformance); +} +/* @internal */ +function forwardCall(logger: Logger, actionDescription: string, returnJson: boolean, action: () => T, logPerformance: boolean): T | string { + try { + const result = simpleForwardCall(logger, actionDescription, action, logPerformance); + return returnJson ? JSON.stringify({ result }) : result as T; + } + catch (err) { + if (err instanceof OperationCanceledException) { + return JSON.stringify({ canceled: true }); } + logInternalError(logger, err); + err.description = actionDescription; + return JSON.stringify({ error: err }); } - - export interface RealizedDiagnostic { +} +/* @internal */ +class ShimBase implements Shim { + constructor(private factory: ShimFactory) { + factory.registerShim(this); + } + public dispose(_dummy: {}): void { + this.factory.unregisterShim(this); + } +} +/* @internal */ +export interface RealizedDiagnostic { + message: string; + start: number; + length: number; + category: string; + code: number; + reportsUnnecessary?: {}; +} +/* @internal */ +export function realizeDiagnostics(diagnostics: readonly Diagnostic[], newLine: string): RealizedDiagnostic[] { + return diagnostics.map(d => realizeDiagnostic(d, newLine)); +} +/* @internal */ +function realizeDiagnostic(diagnostic: Diagnostic, newLine: string): RealizedDiagnostic { + return { + message: flattenDiagnosticMessageText(diagnostic.messageText, newLine), + start: diagnostic.start!, + length: diagnostic.length!, + category: diagnosticCategoryName(diagnostic), + code: diagnostic.code, + reportsUnnecessary: diagnostic.reportsUnnecessary, + }; +} +/* @internal */ +class LanguageServiceShimObject extends ShimBase implements LanguageServiceShim { + private logger: Logger; + private logPerformance = false; + constructor(factory: ShimFactory, private host: LanguageServiceShimHost, public languageService: LanguageService) { + super(factory); + this.logger = this.host; + } + public forwardJSONCall(actionDescription: string, action: () => {} | null | undefined): string { + return forwardJSONCall(this.logger, actionDescription, action, this.logPerformance); + } + /// DISPOSE + /** + * Ensure (almost) deterministic release of internal Javascript resources when + * some external native objects holds onto us (e.g. Com/Interop). + */ + public dispose(dummy: {}): void { + this.logger.log("dispose()"); + this.languageService.dispose(); + this.languageService = null!; // eslint-disable-line no-null/no-null + // force a GC + if (debugObjectHost && debugObjectHost.CollectGarbage) { + debugObjectHost.CollectGarbage(); + this.logger.log("CollectGarbage()"); + } + this.logger = null!; // eslint-disable-line no-null/no-null + super.dispose(dummy); + } + /// REFRESH + /** + * Update the list of scripts known to the compiler + */ + public refresh(throwOnError: boolean): void { + this.forwardJSONCall(`refresh(${throwOnError})`, () => null // eslint-disable-line no-null/no-null + ); + } + public cleanupSemanticCache(): void { + this.forwardJSONCall("cleanupSemanticCache()", () => { + this.languageService.cleanupSemanticCache(); + return null; // eslint-disable-line no-null/no-null + }); + } + private realizeDiagnostics(diagnostics: readonly Diagnostic[]): { message: string; start: number; length: number; category: string; - code: number; - reportsUnnecessary?: {}; - } - export function realizeDiagnostics(diagnostics: readonly Diagnostic[], newLine: string): RealizedDiagnostic[] { - return diagnostics.map(d => realizeDiagnostic(d, newLine)); - } - - function realizeDiagnostic(diagnostic: Diagnostic, newLine: string): RealizedDiagnostic { - return { - message: flattenDiagnosticMessageText(diagnostic.messageText, newLine), - start: diagnostic.start!, // TODO: GH#18217 - length: diagnostic.length!, // TODO: GH#18217 - category: diagnosticCategoryName(diagnostic), - code: diagnostic.code, - reportsUnnecessary: diagnostic.reportsUnnecessary, - }; - } - - class LanguageServiceShimObject extends ShimBase implements LanguageServiceShim { - private logger: Logger; - private logPerformance = false; - - constructor(factory: ShimFactory, - private host: LanguageServiceShimHost, - public languageService: LanguageService) { - super(factory); - this.logger = this.host; - } - - public forwardJSONCall(actionDescription: string, action: () => {} | null | undefined): string { - return forwardJSONCall(this.logger, actionDescription, action, this.logPerformance); - } - - /// DISPOSE - - /** - * Ensure (almost) deterministic release of internal Javascript resources when - * some external native objects holds onto us (e.g. Com/Interop). - */ - public dispose(dummy: {}): void { - this.logger.log("dispose()"); - this.languageService.dispose(); - this.languageService = null!; // eslint-disable-line no-null/no-null - - // force a GC - if (debugObjectHost && debugObjectHost.CollectGarbage) { - debugObjectHost.CollectGarbage(); - this.logger.log("CollectGarbage()"); - } - - this.logger = null!; // eslint-disable-line no-null/no-null - - super.dispose(dummy); - } - - /// REFRESH - - /** - * Update the list of scripts known to the compiler - */ - public refresh(throwOnError: boolean): void { - this.forwardJSONCall( - `refresh(${throwOnError})`, - () => null // eslint-disable-line no-null/no-null - ); - } - - public cleanupSemanticCache(): void { - this.forwardJSONCall( - "cleanupSemanticCache()", - () => { - this.languageService.cleanupSemanticCache(); - return null; // eslint-disable-line no-null/no-null - }); - } - - private realizeDiagnostics(diagnostics: readonly Diagnostic[]): { message: string; start: number; length: number; category: string; }[] { - const newLine = getNewLineOrDefaultFromHost(this.host); - return realizeDiagnostics(diagnostics, newLine); - } - - public getSyntacticClassifications(fileName: string, start: number, length: number): string { - return this.forwardJSONCall( - `getSyntacticClassifications('${fileName}', ${start}, ${length})`, - () => this.languageService.getSyntacticClassifications(fileName, createTextSpan(start, length)) - ); - } - - public getSemanticClassifications(fileName: string, start: number, length: number): string { - return this.forwardJSONCall( - `getSemanticClassifications('${fileName}', ${start}, ${length})`, - () => this.languageService.getSemanticClassifications(fileName, createTextSpan(start, length)) - ); - } - - public getEncodedSyntacticClassifications(fileName: string, start: number, length: number): string { - return this.forwardJSONCall( - `getEncodedSyntacticClassifications('${fileName}', ${start}, ${length})`, - // directly serialize the spans out to a string. This is much faster to decode - // on the managed side versus a full JSON array. - () => convertClassifications(this.languageService.getEncodedSyntacticClassifications(fileName, createTextSpan(start, length))) - ); - } - - public getEncodedSemanticClassifications(fileName: string, start: number, length: number): string { - return this.forwardJSONCall( - `getEncodedSemanticClassifications('${fileName}', ${start}, ${length})`, - // directly serialize the spans out to a string. This is much faster to decode - // on the managed side versus a full JSON array. - () => convertClassifications(this.languageService.getEncodedSemanticClassifications(fileName, createTextSpan(start, length))) - ); - } - - public getSyntacticDiagnostics(fileName: string): string { - return this.forwardJSONCall( - `getSyntacticDiagnostics('${fileName}')`, - () => { - const diagnostics = this.languageService.getSyntacticDiagnostics(fileName); - return this.realizeDiagnostics(diagnostics); - }); - } - - public getSemanticDiagnostics(fileName: string): string { - return this.forwardJSONCall( - `getSemanticDiagnostics('${fileName}')`, - () => { - const diagnostics = this.languageService.getSemanticDiagnostics(fileName); - return this.realizeDiagnostics(diagnostics); - }); - } - - public getSuggestionDiagnostics(fileName: string): string { - return this.forwardJSONCall(`getSuggestionDiagnostics('${fileName}')`, () => this.realizeDiagnostics(this.languageService.getSuggestionDiagnostics(fileName))); - } - - public getCompilerOptionsDiagnostics(): string { - return this.forwardJSONCall( - "getCompilerOptionsDiagnostics()", - () => { - const diagnostics = this.languageService.getCompilerOptionsDiagnostics(); - return this.realizeDiagnostics(diagnostics); - }); - } - - /// QUICKINFO - - /** - * Computes a string representation of the type at the requested position - * in the active file. - */ - public getQuickInfoAtPosition(fileName: string, position: number): string { - return this.forwardJSONCall( - `getQuickInfoAtPosition('${fileName}', ${position})`, - () => this.languageService.getQuickInfoAtPosition(fileName, position) - ); - } - - - /// NAMEORDOTTEDNAMESPAN - - /** - * Computes span information of the name or dotted name at the requested position - * in the active file. - */ - public getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): string { - return this.forwardJSONCall( - `getNameOrDottedNameSpan('${fileName}', ${startPos}, ${endPos})`, - () => this.languageService.getNameOrDottedNameSpan(fileName, startPos, endPos) - ); - } - - /** - * STATEMENTSPAN - * Computes span information of statement at the requested position in the active file. - */ - public getBreakpointStatementAtPosition(fileName: string, position: number): string { - return this.forwardJSONCall( - `getBreakpointStatementAtPosition('${fileName}', ${position})`, - () => this.languageService.getBreakpointStatementAtPosition(fileName, position) - ); - } - - /// SIGNATUREHELP - - public getSignatureHelpItems(fileName: string, position: number, options: SignatureHelpItemsOptions | undefined): string { - return this.forwardJSONCall( - `getSignatureHelpItems('${fileName}', ${position})`, - () => this.languageService.getSignatureHelpItems(fileName, position, options) - ); - } - - /// GOTO DEFINITION - - /** - * Computes the definition location and file for the symbol - * at the requested position. - */ - public getDefinitionAtPosition(fileName: string, position: number): string { - return this.forwardJSONCall( - `getDefinitionAtPosition('${fileName}', ${position})`, - () => this.languageService.getDefinitionAtPosition(fileName, position) - ); - } - - /** - * Computes the definition location and file for the symbol - * at the requested position. - */ - public getDefinitionAndBoundSpan(fileName: string, position: number): string { - return this.forwardJSONCall( - `getDefinitionAndBoundSpan('${fileName}', ${position})`, - () => this.languageService.getDefinitionAndBoundSpan(fileName, position) - ); - } - - /// GOTO Type - - /** - * Computes the definition location of the type of the symbol - * at the requested position. - */ - public getTypeDefinitionAtPosition(fileName: string, position: number): string { - return this.forwardJSONCall( - `getTypeDefinitionAtPosition('${fileName}', ${position})`, - () => this.languageService.getTypeDefinitionAtPosition(fileName, position) - ); - } - - /// GOTO Implementation - - /** - * Computes the implementation location of the symbol - * at the requested position. - */ - public getImplementationAtPosition(fileName: string, position: number): string { - return this.forwardJSONCall( - `getImplementationAtPosition('${fileName}', ${position})`, - () => this.languageService.getImplementationAtPosition(fileName, position) - ); - } - - public getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): string { - return this.forwardJSONCall( - `getRenameInfo('${fileName}', ${position})`, - () => this.languageService.getRenameInfo(fileName, position, options) - ); - } - - public getSmartSelectionRange(fileName: string, position: number): string { - return this.forwardJSONCall( - `getSmartSelectionRange('${fileName}', ${position})`, - () => this.languageService.getSmartSelectionRange(fileName, position) - ); - } - - public findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): string { - return this.forwardJSONCall( - `findRenameLocations('${fileName}', ${position}, ${findInStrings}, ${findInComments}, ${providePrefixAndSuffixTextForRename})`, - () => this.languageService.findRenameLocations(fileName, position, findInStrings, findInComments, providePrefixAndSuffixTextForRename) - ); - } - - /// GET BRACE MATCHING - public getBraceMatchingAtPosition(fileName: string, position: number): string { - return this.forwardJSONCall( - `getBraceMatchingAtPosition('${fileName}', ${position})`, - () => this.languageService.getBraceMatchingAtPosition(fileName, position) - ); - } - - public isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): string { - return this.forwardJSONCall( - `isValidBraceCompletionAtPosition('${fileName}', ${position}, ${openingBrace})`, - () => this.languageService.isValidBraceCompletionAtPosition(fileName, position, openingBrace) - ); - } - - public getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): string { - return this.forwardJSONCall( - `getSpanOfEnclosingComment('${fileName}', ${position})`, - () => this.languageService.getSpanOfEnclosingComment(fileName, position, onlyMultiLine) - ); - } - - /// GET SMART INDENT - public getIndentationAtPosition(fileName: string, position: number, options: string /*Services.EditorOptions*/): string { - return this.forwardJSONCall( - `getIndentationAtPosition('${fileName}', ${position})`, - () => { - const localOptions: EditorOptions = JSON.parse(options); - return this.languageService.getIndentationAtPosition(fileName, position, localOptions); - }); - } - - /// GET REFERENCES - - public getReferencesAtPosition(fileName: string, position: number): string { - return this.forwardJSONCall( - `getReferencesAtPosition('${fileName}', ${position})`, - () => this.languageService.getReferencesAtPosition(fileName, position) - ); - } - - public findReferences(fileName: string, position: number): string { - return this.forwardJSONCall( - `findReferences('${fileName}', ${position})`, - () => this.languageService.findReferences(fileName, position) - ); - } - - public getOccurrencesAtPosition(fileName: string, position: number): string { - return this.forwardJSONCall( - `getOccurrencesAtPosition('${fileName}', ${position})`, - () => this.languageService.getOccurrencesAtPosition(fileName, position) - ); - } - - public getDocumentHighlights(fileName: string, position: number, filesToSearch: string): string { - return this.forwardJSONCall( - `getDocumentHighlights('${fileName}', ${position})`, - () => { - const results = this.languageService.getDocumentHighlights(fileName, position, JSON.parse(filesToSearch)); - // workaround for VS document highlighting issue - keep only items from the initial file - const normalizedName = toFileNameLowerCase(normalizeSlashes(fileName)); - return filter(results, r => toFileNameLowerCase(normalizeSlashes(r.fileName)) === normalizedName); - }); - } - - /// COMPLETION LISTS - - /** - * Get a string based representation of the completions - * to provide at the given source position and providing a member completion - * list if requested. - */ - public getCompletionsAtPosition(fileName: string, position: number, preferences: GetCompletionsAtPositionOptions | undefined) { - return this.forwardJSONCall( - `getCompletionsAtPosition('${fileName}', ${position}, ${preferences})`, - () => this.languageService.getCompletionsAtPosition(fileName, position, preferences) - ); - } - - /** Get a string based representation of a completion list entry details */ - public getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: string/*Services.FormatCodeOptions*/ | undefined, source: string | undefined, preferences: UserPreferences | undefined) { - return this.forwardJSONCall( - `getCompletionEntryDetails('${fileName}', ${position}, '${entryName}')`, - () => { - const localOptions: FormatCodeOptions = formatOptions === undefined ? undefined : JSON.parse(formatOptions); - return this.languageService.getCompletionEntryDetails(fileName, position, entryName, localOptions, source, preferences); - } - ); - } - - public getFormattingEditsForRange(fileName: string, start: number, end: number, options: string/*Services.FormatCodeOptions*/): string { - return this.forwardJSONCall( - `getFormattingEditsForRange('${fileName}', ${start}, ${end})`, - () => { - const localOptions: FormatCodeOptions = JSON.parse(options); - return this.languageService.getFormattingEditsForRange(fileName, start, end, localOptions); - }); - } - - public getFormattingEditsForDocument(fileName: string, options: string/*Services.FormatCodeOptions*/): string { - return this.forwardJSONCall( - `getFormattingEditsForDocument('${fileName}')`, - () => { - const localOptions: FormatCodeOptions = JSON.parse(options); - return this.languageService.getFormattingEditsForDocument(fileName, localOptions); - }); - } - - public getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: string/*Services.FormatCodeOptions*/): string { - return this.forwardJSONCall( - `getFormattingEditsAfterKeystroke('${fileName}', ${position}, '${key}')`, - () => { - const localOptions: FormatCodeOptions = JSON.parse(options); - return this.languageService.getFormattingEditsAfterKeystroke(fileName, position, key, localOptions); - }); - } - - public getDocCommentTemplateAtPosition(fileName: string, position: number): string { - return this.forwardJSONCall( - `getDocCommentTemplateAtPosition('${fileName}', ${position})`, - () => this.languageService.getDocCommentTemplateAtPosition(fileName, position) - ); - } - - /// NAVIGATE TO - - /** Return a list of symbols that are interesting to navigate to */ - public getNavigateToItems(searchValue: string, maxResultCount?: number, fileName?: string): string { - return this.forwardJSONCall( - `getNavigateToItems('${searchValue}', ${maxResultCount}, ${fileName})`, - () => this.languageService.getNavigateToItems(searchValue, maxResultCount, fileName) - ); - } - - public getNavigationBarItems(fileName: string): string { - return this.forwardJSONCall( - `getNavigationBarItems('${fileName}')`, - () => this.languageService.getNavigationBarItems(fileName) - ); - } - - public getNavigationTree(fileName: string): string { - return this.forwardJSONCall( - `getNavigationTree('${fileName}')`, - () => this.languageService.getNavigationTree(fileName) - ); - } - - public getOutliningSpans(fileName: string): string { - return this.forwardJSONCall( - `getOutliningSpans('${fileName}')`, - () => this.languageService.getOutliningSpans(fileName) - ); - } - - public getTodoComments(fileName: string, descriptors: string): string { - return this.forwardJSONCall( - `getTodoComments('${fileName}')`, - () => this.languageService.getTodoComments(fileName, JSON.parse(descriptors)) - ); - } - - /// CALL HIERARCHY - - public prepareCallHierarchy(fileName: string, position: number): string { - return this.forwardJSONCall( - `prepareCallHierarchy('${fileName}', ${position})`, - () => this.languageService.prepareCallHierarchy(fileName, position) - ); - } - - public provideCallHierarchyIncomingCalls(fileName: string, position: number): string { - return this.forwardJSONCall( - `provideCallHierarchyIncomingCalls('${fileName}', ${position})`, - () => this.languageService.provideCallHierarchyIncomingCalls(fileName, position) - ); - } - - public provideCallHierarchyOutgoingCalls(fileName: string, position: number): string { - return this.forwardJSONCall( - `provideCallHierarchyOutgoingCalls('${fileName}', ${position})`, - () => this.languageService.provideCallHierarchyOutgoingCalls(fileName, position) - ); - } - - /// Emit - public getEmitOutput(fileName: string): string { - return this.forwardJSONCall( - `getEmitOutput('${fileName}')`, - () => this.languageService.getEmitOutput(fileName) - ); - } - - public getEmitOutputObject(fileName: string): EmitOutput { - return forwardCall( - this.logger, - `getEmitOutput('${fileName}')`, - /*returnJson*/ false, - () => this.languageService.getEmitOutput(fileName), - this.logPerformance) as EmitOutput; - } + }[] { + const newLine = getNewLineOrDefaultFromHost(this.host); + return realizeDiagnostics(diagnostics, newLine); } - - function convertClassifications(classifications: Classifications): { spans: string, endOfLineState: EndOfLineState } { - return { spans: classifications.spans.join(","), endOfLineState: classifications.endOfLineState }; - } - - class ClassifierShimObject extends ShimBase implements ClassifierShim { - public classifier: Classifier; - private logPerformance = false; - - constructor(factory: ShimFactory, private logger: Logger) { - super(factory); - this.classifier = createClassifier(); - } - - public getEncodedLexicalClassifications(text: string, lexState: EndOfLineState, syntacticClassifierAbsent = false): string { - return forwardJSONCall(this.logger, "getEncodedLexicalClassifications", - () => convertClassifications(this.classifier.getEncodedLexicalClassifications(text, lexState, syntacticClassifierAbsent)), - this.logPerformance); - } - - /// COLORIZATION - public getClassificationsForLine(text: string, lexState: EndOfLineState, classifyKeywordsInGenerics = false): string { - const classification = this.classifier.getClassificationsForLine(text, lexState, classifyKeywordsInGenerics); - let result = ""; - for (const item of classification.entries) { - result += item.length + "\n"; - result += item.classification + "\n"; - } - result += classification.finalLexState; - return result; - } + public getSyntacticClassifications(fileName: string, start: number, length: number): string { + return this.forwardJSONCall(`getSyntacticClassifications('${fileName}', ${start}, ${length})`, () => this.languageService.getSyntacticClassifications(fileName, createTextSpan(start, length))); } - - class CoreServicesShimObject extends ShimBase implements CoreServicesShim { - private logPerformance = false; - private safeList: JsTyping.SafeList | undefined; - - constructor(factory: ShimFactory, public readonly logger: Logger, private readonly host: CoreServicesShimHostAdapter) { - super(factory); - } - - private forwardJSONCall(actionDescription: string, action: () => {}): string { - return forwardJSONCall(this.logger, actionDescription, action, this.logPerformance); - } - - public resolveModuleName(fileName: string, moduleName: string, compilerOptionsJson: string): string { - return this.forwardJSONCall(`resolveModuleName('${fileName}')`, () => { - const compilerOptions = JSON.parse(compilerOptionsJson); - const result = resolveModuleName(moduleName, normalizeSlashes(fileName), compilerOptions, this.host); - let resolvedFileName = result.resolvedModule ? result.resolvedModule.resolvedFileName : undefined; - if (result.resolvedModule && result.resolvedModule.extension !== Extension.Ts && result.resolvedModule.extension !== Extension.Tsx && result.resolvedModule.extension !== Extension.Dts) { - resolvedFileName = undefined; - } - - return { - resolvedFileName, - failedLookupLocations: result.failedLookupLocations - }; - }); - } - - public resolveTypeReferenceDirective(fileName: string, typeReferenceDirective: string, compilerOptionsJson: string): string { - return this.forwardJSONCall(`resolveTypeReferenceDirective(${fileName})`, () => { - const compilerOptions = JSON.parse(compilerOptionsJson); - const result = resolveTypeReferenceDirective(typeReferenceDirective, normalizeSlashes(fileName), compilerOptions, this.host); - return { - resolvedFileName: result.resolvedTypeReferenceDirective ? result.resolvedTypeReferenceDirective.resolvedFileName : undefined, - primary: result.resolvedTypeReferenceDirective ? result.resolvedTypeReferenceDirective.primary : true, - failedLookupLocations: result.failedLookupLocations - }; + public getSemanticClassifications(fileName: string, start: number, length: number): string { + return this.forwardJSONCall(`getSemanticClassifications('${fileName}', ${start}, ${length})`, () => this.languageService.getSemanticClassifications(fileName, createTextSpan(start, length))); + } + public getEncodedSyntacticClassifications(fileName: string, start: number, length: number): string { + return this.forwardJSONCall(`getEncodedSyntacticClassifications('${fileName}', ${start}, ${length})`, + // directly serialize the spans out to a string. This is much faster to decode + // on the managed side versus a full JSON array. + () => convertClassifications(this.languageService.getEncodedSyntacticClassifications(fileName, createTextSpan(start, length)))); + } + public getEncodedSemanticClassifications(fileName: string, start: number, length: number): string { + return this.forwardJSONCall(`getEncodedSemanticClassifications('${fileName}', ${start}, ${length})`, + // directly serialize the spans out to a string. This is much faster to decode + // on the managed side versus a full JSON array. + () => convertClassifications(this.languageService.getEncodedSemanticClassifications(fileName, createTextSpan(start, length)))); + } + public getSyntacticDiagnostics(fileName: string): string { + return this.forwardJSONCall(`getSyntacticDiagnostics('${fileName}')`, () => { + const diagnostics = this.languageService.getSyntacticDiagnostics(fileName); + return this.realizeDiagnostics(diagnostics); + }); + } + public getSemanticDiagnostics(fileName: string): string { + return this.forwardJSONCall(`getSemanticDiagnostics('${fileName}')`, () => { + const diagnostics = this.languageService.getSemanticDiagnostics(fileName); + return this.realizeDiagnostics(diagnostics); + }); + } + public getSuggestionDiagnostics(fileName: string): string { + return this.forwardJSONCall(`getSuggestionDiagnostics('${fileName}')`, () => this.realizeDiagnostics(this.languageService.getSuggestionDiagnostics(fileName))); + } + public getCompilerOptionsDiagnostics(): string { + return this.forwardJSONCall("getCompilerOptionsDiagnostics()", () => { + const diagnostics = this.languageService.getCompilerOptionsDiagnostics(); + return this.realizeDiagnostics(diagnostics); + }); + } + /// QUICKINFO + /** + * Computes a string representation of the type at the requested position + * in the active file. + */ + public getQuickInfoAtPosition(fileName: string, position: number): string { + return this.forwardJSONCall(`getQuickInfoAtPosition('${fileName}', ${position})`, () => this.languageService.getQuickInfoAtPosition(fileName, position)); + } + /// NAMEORDOTTEDNAMESPAN + /** + * Computes span information of the name or dotted name at the requested position + * in the active file. + */ + public getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): string { + return this.forwardJSONCall(`getNameOrDottedNameSpan('${fileName}', ${startPos}, ${endPos})`, () => this.languageService.getNameOrDottedNameSpan(fileName, startPos, endPos)); + } + /** + * STATEMENTSPAN + * Computes span information of statement at the requested position in the active file. + */ + public getBreakpointStatementAtPosition(fileName: string, position: number): string { + return this.forwardJSONCall(`getBreakpointStatementAtPosition('${fileName}', ${position})`, () => this.languageService.getBreakpointStatementAtPosition(fileName, position)); + } + /// SIGNATUREHELP + public getSignatureHelpItems(fileName: string, position: number, options: SignatureHelpItemsOptions | undefined): string { + return this.forwardJSONCall(`getSignatureHelpItems('${fileName}', ${position})`, () => this.languageService.getSignatureHelpItems(fileName, position, options)); + } + /// GOTO DEFINITION + /** + * Computes the definition location and file for the symbol + * at the requested position. + */ + public getDefinitionAtPosition(fileName: string, position: number): string { + return this.forwardJSONCall(`getDefinitionAtPosition('${fileName}', ${position})`, () => this.languageService.getDefinitionAtPosition(fileName, position)); + } + /** + * Computes the definition location and file for the symbol + * at the requested position. + */ + public getDefinitionAndBoundSpan(fileName: string, position: number): string { + return this.forwardJSONCall(`getDefinitionAndBoundSpan('${fileName}', ${position})`, () => this.languageService.getDefinitionAndBoundSpan(fileName, position)); + } + /// GOTO Type + /** + * Computes the definition location of the type of the symbol + * at the requested position. + */ + public getTypeDefinitionAtPosition(fileName: string, position: number): string { + return this.forwardJSONCall(`getTypeDefinitionAtPosition('${fileName}', ${position})`, () => this.languageService.getTypeDefinitionAtPosition(fileName, position)); + } + /// GOTO Implementation + /** + * Computes the implementation location of the symbol + * at the requested position. + */ + public getImplementationAtPosition(fileName: string, position: number): string { + return this.forwardJSONCall(`getImplementationAtPosition('${fileName}', ${position})`, () => this.languageService.getImplementationAtPosition(fileName, position)); + } + public getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): string { + return this.forwardJSONCall(`getRenameInfo('${fileName}', ${position})`, () => this.languageService.getRenameInfo(fileName, position, options)); + } + public getSmartSelectionRange(fileName: string, position: number): string { + return this.forwardJSONCall(`getSmartSelectionRange('${fileName}', ${position})`, () => this.languageService.getSmartSelectionRange(fileName, position)); + } + public findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): string { + return this.forwardJSONCall(`findRenameLocations('${fileName}', ${position}, ${findInStrings}, ${findInComments}, ${providePrefixAndSuffixTextForRename})`, () => this.languageService.findRenameLocations(fileName, position, findInStrings, findInComments, providePrefixAndSuffixTextForRename)); + } + /// GET BRACE MATCHING + public getBraceMatchingAtPosition(fileName: string, position: number): string { + return this.forwardJSONCall(`getBraceMatchingAtPosition('${fileName}', ${position})`, () => this.languageService.getBraceMatchingAtPosition(fileName, position)); + } + public isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): string { + return this.forwardJSONCall(`isValidBraceCompletionAtPosition('${fileName}', ${position}, ${openingBrace})`, () => this.languageService.isValidBraceCompletionAtPosition(fileName, position, openingBrace)); + } + public getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): string { + return this.forwardJSONCall(`getSpanOfEnclosingComment('${fileName}', ${position})`, () => this.languageService.getSpanOfEnclosingComment(fileName, position, onlyMultiLine)); + } + /// GET SMART INDENT + public getIndentationAtPosition(fileName: string, position: number, options: string /*Services.EditorOptions*/): string { + return this.forwardJSONCall(`getIndentationAtPosition('${fileName}', ${position})`, () => { + const localOptions: EditorOptions = JSON.parse(options); + return this.languageService.getIndentationAtPosition(fileName, position, localOptions); + }); + } + /// GET REFERENCES + public getReferencesAtPosition(fileName: string, position: number): string { + return this.forwardJSONCall(`getReferencesAtPosition('${fileName}', ${position})`, () => this.languageService.getReferencesAtPosition(fileName, position)); + } + public findReferences(fileName: string, position: number): string { + return this.forwardJSONCall(`findReferences('${fileName}', ${position})`, () => this.languageService.findReferences(fileName, position)); + } + public getOccurrencesAtPosition(fileName: string, position: number): string { + return this.forwardJSONCall(`getOccurrencesAtPosition('${fileName}', ${position})`, () => this.languageService.getOccurrencesAtPosition(fileName, position)); + } + public getDocumentHighlights(fileName: string, position: number, filesToSearch: string): string { + return this.forwardJSONCall(`getDocumentHighlights('${fileName}', ${position})`, () => { + const results = this.languageService.getDocumentHighlights(fileName, position, JSON.parse(filesToSearch)); + // workaround for VS document highlighting issue - keep only items from the initial file + const normalizedName = toFileNameLowerCase(normalizeSlashes(fileName)); + return filter(results, r => toFileNameLowerCase(normalizeSlashes(r.fileName)) === normalizedName); + }); + } + /// COMPLETION LISTS + /** + * Get a string based representation of the completions + * to provide at the given source position and providing a member completion + * list if requested. + */ + public getCompletionsAtPosition(fileName: string, position: number, preferences: GetCompletionsAtPositionOptions | undefined) { + return this.forwardJSONCall(`getCompletionsAtPosition('${fileName}', ${position}, ${preferences})`, () => this.languageService.getCompletionsAtPosition(fileName, position, preferences)); + } + /** Get a string based representation of a completion list entry details */ + public getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: string /*Services.FormatCodeOptions*/ | undefined, source: string | undefined, preferences: UserPreferences | undefined) { + return this.forwardJSONCall(`getCompletionEntryDetails('${fileName}', ${position}, '${entryName}')`, () => { + const localOptions: FormatCodeOptions = formatOptions === undefined ? undefined : JSON.parse(formatOptions); + return this.languageService.getCompletionEntryDetails(fileName, position, entryName, localOptions, source, preferences); + }); + } + public getFormattingEditsForRange(fileName: string, start: number, end: number, options: string /*Services.FormatCodeOptions*/): string { + return this.forwardJSONCall(`getFormattingEditsForRange('${fileName}', ${start}, ${end})`, () => { + const localOptions: FormatCodeOptions = JSON.parse(options); + return this.languageService.getFormattingEditsForRange(fileName, start, end, localOptions); + }); + } + public getFormattingEditsForDocument(fileName: string, options: string /*Services.FormatCodeOptions*/): string { + return this.forwardJSONCall(`getFormattingEditsForDocument('${fileName}')`, () => { + const localOptions: FormatCodeOptions = JSON.parse(options); + return this.languageService.getFormattingEditsForDocument(fileName, localOptions); + }); + } + public getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: string /*Services.FormatCodeOptions*/): string { + return this.forwardJSONCall(`getFormattingEditsAfterKeystroke('${fileName}', ${position}, '${key}')`, () => { + const localOptions: FormatCodeOptions = JSON.parse(options); + return this.languageService.getFormattingEditsAfterKeystroke(fileName, position, key, localOptions); + }); + } + public getDocCommentTemplateAtPosition(fileName: string, position: number): string { + return this.forwardJSONCall(`getDocCommentTemplateAtPosition('${fileName}', ${position})`, () => this.languageService.getDocCommentTemplateAtPosition(fileName, position)); + } + /// NAVIGATE TO + /** Return a list of symbols that are interesting to navigate to */ + public getNavigateToItems(searchValue: string, maxResultCount?: number, fileName?: string): string { + return this.forwardJSONCall(`getNavigateToItems('${searchValue}', ${maxResultCount}, ${fileName})`, () => this.languageService.getNavigateToItems(searchValue, maxResultCount, fileName)); + } + public getNavigationBarItems(fileName: string): string { + return this.forwardJSONCall(`getNavigationBarItems('${fileName}')`, () => this.languageService.getNavigationBarItems(fileName)); + } + public getNavigationTree(fileName: string): string { + return this.forwardJSONCall(`getNavigationTree('${fileName}')`, () => this.languageService.getNavigationTree(fileName)); + } + public getOutliningSpans(fileName: string): string { + return this.forwardJSONCall(`getOutliningSpans('${fileName}')`, () => this.languageService.getOutliningSpans(fileName)); + } + public getTodoComments(fileName: string, descriptors: string): string { + return this.forwardJSONCall(`getTodoComments('${fileName}')`, () => this.languageService.getTodoComments(fileName, JSON.parse(descriptors))); + } + /// CALL HIERARCHY + public prepareCallHierarchy(fileName: string, position: number): string { + return this.forwardJSONCall(`prepareCallHierarchy('${fileName}', ${position})`, () => this.languageService.prepareCallHierarchy(fileName, position)); + } + public provideCallHierarchyIncomingCalls(fileName: string, position: number): string { + return this.forwardJSONCall(`provideCallHierarchyIncomingCalls('${fileName}', ${position})`, () => this.languageService.provideCallHierarchyIncomingCalls(fileName, position)); + } + public provideCallHierarchyOutgoingCalls(fileName: string, position: number): string { + return this.forwardJSONCall(`provideCallHierarchyOutgoingCalls('${fileName}', ${position})`, () => this.languageService.provideCallHierarchyOutgoingCalls(fileName, position)); + } + /// Emit + public getEmitOutput(fileName: string): string { + return this.forwardJSONCall(`getEmitOutput('${fileName}')`, () => this.languageService.getEmitOutput(fileName)); + } + public getEmitOutputObject(fileName: string): EmitOutput { + return forwardCall(this.logger, `getEmitOutput('${fileName}')`, + /*returnJson*/ false, () => this.languageService.getEmitOutput(fileName), this.logPerformance) as EmitOutput; + } +} +/* @internal */ +function convertClassifications(classifications: Classifications): { + spans: string; + endOfLineState: EndOfLineState; +} { + return { spans: classifications.spans.join(","), endOfLineState: classifications.endOfLineState }; +} +/* @internal */ +class ClassifierShimObject extends ShimBase implements ClassifierShim { + public classifier: Classifier; + private logPerformance = false; + constructor(factory: ShimFactory, private logger: Logger) { + super(factory); + this.classifier = createClassifier(); + } + public getEncodedLexicalClassifications(text: string, lexState: EndOfLineState, syntacticClassifierAbsent = false): string { + return forwardJSONCall(this.logger, "getEncodedLexicalClassifications", () => convertClassifications(this.classifier.getEncodedLexicalClassifications(text, lexState, syntacticClassifierAbsent)), this.logPerformance); + } + /// COLORIZATION + public getClassificationsForLine(text: string, lexState: EndOfLineState, classifyKeywordsInGenerics = false): string { + const classification = this.classifier.getClassificationsForLine(text, lexState, classifyKeywordsInGenerics); + let result = ""; + for (const item of classification.entries) { + result += item.length + "\n"; + result += item.classification + "\n"; + } + result += classification.finalLexState; + return result; + } +} +/* @internal */ +class CoreServicesShimObject extends ShimBase implements CoreServicesShim { + private logPerformance = false; + private safeList: JsTyping.SafeList | undefined; + constructor(factory: ShimFactory, public readonly logger: Logger, private readonly host: CoreServicesShimHostAdapter) { + super(factory); + } + private forwardJSONCall(actionDescription: string, action: () => {}): string { + return forwardJSONCall(this.logger, actionDescription, action, this.logPerformance); + } + public resolveModuleName(fileName: string, moduleName: string, compilerOptionsJson: string): string { + return this.forwardJSONCall(`resolveModuleName('${fileName}')`, () => { + const compilerOptions = (JSON.parse(compilerOptionsJson)); + const result = resolveModuleName(moduleName, normalizeSlashes(fileName), compilerOptions, this.host); + let resolvedFileName = result.resolvedModule ? result.resolvedModule.resolvedFileName : undefined; + if (result.resolvedModule && result.resolvedModule.extension !== Extension.Ts && result.resolvedModule.extension !== Extension.Tsx && result.resolvedModule.extension !== Extension.Dts) { + resolvedFileName = undefined; + } + return { + resolvedFileName, + failedLookupLocations: result.failedLookupLocations + }; + }); + } + public resolveTypeReferenceDirective(fileName: string, typeReferenceDirective: string, compilerOptionsJson: string): string { + return this.forwardJSONCall(`resolveTypeReferenceDirective(${fileName})`, () => { + const compilerOptions = (JSON.parse(compilerOptionsJson)); + const result = resolveTypeReferenceDirective(typeReferenceDirective, normalizeSlashes(fileName), compilerOptions, this.host); + return { + resolvedFileName: result.resolvedTypeReferenceDirective ? result.resolvedTypeReferenceDirective.resolvedFileName : undefined, + primary: result.resolvedTypeReferenceDirective ? result.resolvedTypeReferenceDirective.primary : true, + failedLookupLocations: result.failedLookupLocations + }; + }); + } + public getPreProcessedFileInfo(fileName: string, sourceTextSnapshot: IScriptSnapshot): string { + return this.forwardJSONCall(`getPreProcessedFileInfo('${fileName}')`, () => { + // for now treat files as JavaScript + const result = preProcessFile(getSnapshotText(sourceTextSnapshot), /* readImportFiles */ true, /* detectJavaScriptImports */ true); + return { + referencedFiles: this.convertFileReferences(result.referencedFiles), + importedFiles: this.convertFileReferences(result.importedFiles), + ambientExternalModules: result.ambientExternalModules, + isLibFile: result.isLibFile, + typeReferenceDirectives: this.convertFileReferences(result.typeReferenceDirectives), + libReferenceDirectives: this.convertFileReferences(result.libReferenceDirectives) + }; + }); + } + public getAutomaticTypeDirectiveNames(compilerOptionsJson: string): string { + return this.forwardJSONCall(`getAutomaticTypeDirectiveNames('${compilerOptionsJson}')`, () => { + const compilerOptions = (JSON.parse(compilerOptionsJson)); + return getAutomaticTypeDirectiveNames(compilerOptions, this.host); + }); + } + private convertFileReferences(refs: FileReference[]): ShimsFileReference[] | undefined { + if (!refs) { + return undefined; + } + const result: ShimsFileReference[] = []; + for (const ref of refs) { + result.push({ + path: normalizeSlashes(ref.fileName), + position: ref.pos, + length: ref.end - ref.pos }); } - - public getPreProcessedFileInfo(fileName: string, sourceTextSnapshot: IScriptSnapshot): string { - return this.forwardJSONCall( - `getPreProcessedFileInfo('${fileName}')`, - () => { - // for now treat files as JavaScript - const result = preProcessFile(getSnapshotText(sourceTextSnapshot), /* readImportFiles */ true, /* detectJavaScriptImports */ true); - return { - referencedFiles: this.convertFileReferences(result.referencedFiles), - importedFiles: this.convertFileReferences(result.importedFiles), - ambientExternalModules: result.ambientExternalModules, - isLibFile: result.isLibFile, - typeReferenceDirectives: this.convertFileReferences(result.typeReferenceDirectives), - libReferenceDirectives: this.convertFileReferences(result.libReferenceDirectives) - }; - }); - } - - public getAutomaticTypeDirectiveNames(compilerOptionsJson: string): string { - return this.forwardJSONCall( - `getAutomaticTypeDirectiveNames('${compilerOptionsJson}')`, - () => { - const compilerOptions = JSON.parse(compilerOptionsJson); - return getAutomaticTypeDirectiveNames(compilerOptions, this.host); - } - ); - } - - private convertFileReferences(refs: FileReference[]): ShimsFileReference[] | undefined { - if (!refs) { - return undefined; - } - const result: ShimsFileReference[] = []; - for (const ref of refs) { - result.push({ - path: normalizeSlashes(ref.fileName), - position: ref.pos, - length: ref.end - ref.pos - }); + return result; + } + public getTSConfigFileInfo(fileName: string, sourceTextSnapshot: IScriptSnapshot): string { + return this.forwardJSONCall(`getTSConfigFileInfo('${fileName}')`, () => { + const result = parseJsonText(fileName, getSnapshotText(sourceTextSnapshot)); + const normalizedFileName = normalizeSlashes(fileName); + const configFile = parseJsonSourceFileConfigFileContent(result, this.host, getDirectoryPath(normalizedFileName), /*existingOptions*/ {}, normalizedFileName); + return { + options: configFile.options, + typeAcquisition: configFile.typeAcquisition, + files: configFile.fileNames, + raw: configFile.raw, + errors: realizeDiagnostics([...result.parseDiagnostics, ...configFile.errors], "\r\n") + }; + }); + } + public getDefaultCompilationSettings(): string { + return this.forwardJSONCall("getDefaultCompilationSettings()", () => getDefaultCompilerOptions()); + } + public discoverTypings(discoverTypingsJson: string): string { + const getCanonicalFileName = createGetCanonicalFileName(/*useCaseSensitivefileNames:*/ false); + return this.forwardJSONCall("discoverTypings()", () => { + const info = JSON.parse(discoverTypingsJson); + if (this.safeList === undefined) { + this.safeList = JsTyping.loadSafeList(this.host, toPath(info.safeListPath, info.safeListPath, getCanonicalFileName)); + } + return JsTyping.discoverTypings(this.host, msg => this.logger.log(msg), info.fileNames, toPath(info.projectRootPath, info.projectRootPath, getCanonicalFileName), this.safeList, info.packageNameToTypingLocation, info.typeAcquisition, info.unresolvedImports, info.typesRegistry); + }); + } +} +/* @internal */ +export class TypeScriptServicesFactory implements ShimFactory { + private _shims: Shim[] = []; + private documentRegistry: DocumentRegistry | undefined; + /* + * Returns script API version. + */ + public getServicesVersion(): string { + return servicesVersion; + } + public createLanguageServiceShim(host: LanguageServiceShimHost): LanguageServiceShim { + try { + if (this.documentRegistry === undefined) { + this.documentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames(), host.getCurrentDirectory()); } - return result; + const hostAdapter = new LanguageServiceShimHostAdapter(host); + const languageService = createLanguageService(hostAdapter, this.documentRegistry, /*syntaxOnly*/ false); + return new LanguageServiceShimObject(this, host, languageService); } - - public getTSConfigFileInfo(fileName: string, sourceTextSnapshot: IScriptSnapshot): string { - return this.forwardJSONCall( - `getTSConfigFileInfo('${fileName}')`, - () => { - const result = parseJsonText(fileName, getSnapshotText(sourceTextSnapshot)); - const normalizedFileName = normalizeSlashes(fileName); - const configFile = parseJsonSourceFileConfigFileContent(result, this.host, getDirectoryPath(normalizedFileName), /*existingOptions*/ {}, normalizedFileName); - - return { - options: configFile.options, - typeAcquisition: configFile.typeAcquisition, - files: configFile.fileNames, - raw: configFile.raw, - errors: realizeDiagnostics([...result.parseDiagnostics, ...configFile.errors], "\r\n") - }; - }); - } - - public getDefaultCompilationSettings(): string { - return this.forwardJSONCall( - "getDefaultCompilationSettings()", - () => getDefaultCompilerOptions() - ); - } - - public discoverTypings(discoverTypingsJson: string): string { - const getCanonicalFileName = createGetCanonicalFileName(/*useCaseSensitivefileNames:*/ false); - return this.forwardJSONCall("discoverTypings()", () => { - const info = JSON.parse(discoverTypingsJson); - if (this.safeList === undefined) { - this.safeList = JsTyping.loadSafeList(this.host, toPath(info.safeListPath, info.safeListPath, getCanonicalFileName)); - } - return JsTyping.discoverTypings( - this.host, - msg => this.logger.log(msg), - info.fileNames, - toPath(info.projectRootPath, info.projectRootPath, getCanonicalFileName), - this.safeList, - info.packageNameToTypingLocation, - info.typeAcquisition, - info.unresolvedImports, - info.typesRegistry); - }); + catch (err) { + logInternalError(host, err); + throw err; } } - - export class TypeScriptServicesFactory implements ShimFactory { - private _shims: Shim[] = []; - private documentRegistry: DocumentRegistry | undefined; - - /* - * Returns script API version. - */ - public getServicesVersion(): string { - return servicesVersion; - } - - public createLanguageServiceShim(host: LanguageServiceShimHost): LanguageServiceShim { - try { - if (this.documentRegistry === undefined) { - this.documentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames(), host.getCurrentDirectory()); - } - const hostAdapter = new LanguageServiceShimHostAdapter(host); - const languageService = createLanguageService(hostAdapter, this.documentRegistry, /*syntaxOnly*/ false); - return new LanguageServiceShimObject(this, host, languageService); - } - catch (err) { - logInternalError(host, err); - throw err; - } - } - - public createClassifierShim(logger: Logger): ClassifierShim { - try { - return new ClassifierShimObject(this, logger); - } - catch (err) { - logInternalError(logger, err); - throw err; - } + public createClassifierShim(logger: Logger): ClassifierShim { + try { + return new ClassifierShimObject(this, logger); } - - public createCoreServicesShim(host: CoreServicesShimHost): CoreServicesShim { - try { - const adapter = new CoreServicesShimHostAdapter(host); - return new CoreServicesShimObject(this, host, adapter); - } - catch (err) { - logInternalError(host, err); - throw err; - } + catch (err) { + logInternalError(logger, err); + throw err; } - - public close(): void { - // Forget all the registered shims - clear(this._shims); - this.documentRegistry = undefined!; + } + public createCoreServicesShim(host: CoreServicesShimHost): CoreServicesShim { + try { + const adapter = new CoreServicesShimHostAdapter(host); + return new CoreServicesShimObject(this, host, adapter); } - - public registerShim(shim: Shim): void { - this._shims.push(shim); + catch (err) { + logInternalError(host, err); + throw err; } - - public unregisterShim(shim: Shim): void { - for (let i = 0; i < this._shims.length; i++) { - if (this._shims[i] === shim) { - delete this._shims[i]; - return; - } + } + public close(): void { + // Forget all the registered shims + clear(this._shims); + this.documentRegistry = undefined!; + } + public registerShim(shim: Shim): void { + this._shims.push(shim); + } + public unregisterShim(shim: Shim): void { + for (let i = 0; i < this._shims.length; i++) { + if (this._shims[i] === shim) { + delete this._shims[i]; + return; } - - throw new Error("Invalid operation"); } + throw new Error("Invalid operation"); } } - /* eslint-enable no-in-operator */ diff --git a/src/services/signatureHelp.ts b/src/services/signatureHelp.ts index 799fff86ecf0d..7ad0775824325 100644 --- a/src/services/signatureHelp.ts +++ b/src/services/signatureHelp.ts @@ -1,607 +1,617 @@ +import { CallLikeExpression, Identifier, Signature, Node, Symbol, TextSpan, Program, SourceFile, SignatureHelpTriggerReason, CancellationToken, SignatureHelpItems, findTokenOnLeftOfPosition, isInString, isInComment, isSourceFileJS, TypeChecker, isIdentifier, getPossibleGenericSignatures, first, Debug, isCallOrNewExpression, SyntaxKind, contains, findContainingList, isPropertyAccessExpression, firstDefined, findPrecedingToken, rangeContainsRange, isNoSubstitutionTemplateLiteral, isTaggedTemplateExpression, isInsideTemplateLiteral, isTemplateHead, TemplateExpression, TaggedTemplateExpression, isTemplateSpan, isTemplateTail, isJsxOpeningLikeElement, skipTrivia, createTextSpan, getPossibleTypeArgumentsInfo, createTextSpanFromBounds, BinaryExpression, isBinaryExpression, Type, isMethodDeclaration, ParenthesizedExpression, FunctionExpression, ArrowFunction, createTextSpanFromNode, InternalSymbolName, isFunctionTypeNode, countWhere, last, isTemplateLiteralToken, isSourceFile, isBlock, Expression, getInvokedExpression, NodeBuilderFlags, symbolToDisplayParts, emptyArray, TypeParameter, SignatureHelpItem, createPrinter, punctuationPart, SymbolDisplayPart, spacePart, mapToDisplayParts, SignatureHelpParameter, createNodeArray, ListFormat, Printer, EmitHint, ParameterDeclaration } from "./ts"; /* @internal */ -namespace ts.SignatureHelp { - const enum InvocationKind { Call, TypeArgs, Contextual } - interface CallInvocation { readonly kind: InvocationKind.Call; readonly node: CallLikeExpression; } - interface TypeArgsInvocation { readonly kind: InvocationKind.TypeArgs; readonly called: Identifier; } - interface ContextualInvocation { - readonly kind: InvocationKind.Contextual; - readonly signature: Signature; - readonly node: Node; // Just for enclosingDeclaration for printing types - readonly symbol: Symbol; - } - type Invocation = CallInvocation | TypeArgsInvocation | ContextualInvocation; - - interface ArgumentListInfo { - readonly isTypeParameterList: boolean; - readonly invocation: Invocation; - readonly argumentsSpan: TextSpan; - readonly argumentIndex: number; - /** argumentCount is the *apparent* number of arguments. */ - readonly argumentCount: number; - } - - export function getSignatureHelpItems(program: Program, sourceFile: SourceFile, position: number, triggerReason: SignatureHelpTriggerReason | undefined, cancellationToken: CancellationToken): SignatureHelpItems | undefined { - const typeChecker = program.getTypeChecker(); - - // Decide whether to show signature help - const startingToken = findTokenOnLeftOfPosition(sourceFile, position); - if (!startingToken) { - // We are at the beginning of the file - return undefined; - } - - // Only need to be careful if the user typed a character and signature help wasn't showing. - const onlyUseSyntacticOwners = !!triggerReason && triggerReason.kind === "characterTyped"; - - // Bail out quickly in the middle of a string or comment, don't provide signature help unless the user explicitly requested it. - if (onlyUseSyntacticOwners && (isInString(sourceFile, position, startingToken) || isInComment(sourceFile, position))) { - return undefined; - } - - const isManuallyInvoked = !!triggerReason && triggerReason.kind === "invoked"; - const argumentInfo = getContainingArgumentInfo(startingToken, position, sourceFile, typeChecker, isManuallyInvoked); - if (!argumentInfo) return undefined; - - cancellationToken.throwIfCancellationRequested(); - - // Extra syntactic and semantic filtering of signature help - const candidateInfo = getCandidateOrTypeInfo(argumentInfo, typeChecker, sourceFile, startingToken, onlyUseSyntacticOwners); - cancellationToken.throwIfCancellationRequested(); - - if (!candidateInfo) { - // We didn't have any sig help items produced by the TS compiler. If this is a JS - // file, then see if we can figure out anything better. - return isSourceFileJS(sourceFile) ? createJSSignatureHelpItems(argumentInfo, program, cancellationToken) : undefined; - } - - return typeChecker.runWithCancellationToken(cancellationToken, typeChecker => - candidateInfo.kind === CandidateOrTypeKind.Candidate - ? createSignatureHelpItems(candidateInfo.candidates, candidateInfo.resolvedSignature, argumentInfo, sourceFile, typeChecker) - : createTypeHelpItems(candidateInfo.symbol, argumentInfo, sourceFile, typeChecker)); - } - - const enum CandidateOrTypeKind { Candidate, Type } - interface CandidateInfo { - readonly kind: CandidateOrTypeKind.Candidate; - readonly candidates: readonly Signature[]; - readonly resolvedSignature: Signature; +const enum InvocationKind { + Call, + TypeArgs, + Contextual +} +/* @internal */ +interface CallInvocation { + readonly kind: InvocationKind.Call; + readonly node: CallLikeExpression; +} +/* @internal */ +interface TypeArgsInvocation { + readonly kind: InvocationKind.TypeArgs; + readonly called: Identifier; +} +/* @internal */ +interface ContextualInvocation { + readonly kind: InvocationKind.Contextual; + readonly signature: Signature; + readonly node: Node; // Just for enclosingDeclaration for printing types + readonly symbol: Symbol; +} +/* @internal */ +type Invocation = CallInvocation | TypeArgsInvocation | ContextualInvocation; +/* @internal */ +interface ArgumentListInfo { + readonly isTypeParameterList: boolean; + readonly invocation: Invocation; + readonly argumentsSpan: TextSpan; + readonly argumentIndex: number; + /** argumentCount is the *apparent* number of arguments. */ + readonly argumentCount: number; +} +/* @internal */ +export function getSignatureHelpItems(program: Program, sourceFile: SourceFile, position: number, triggerReason: SignatureHelpTriggerReason | undefined, cancellationToken: CancellationToken): SignatureHelpItems | undefined { + const typeChecker = program.getTypeChecker(); + // Decide whether to show signature help + const startingToken = findTokenOnLeftOfPosition(sourceFile, position); + if (!startingToken) { + // We are at the beginning of the file + return undefined; } - interface TypeInfo { - readonly kind: CandidateOrTypeKind.Type; - readonly symbol: Symbol; + // Only need to be careful if the user typed a character and signature help wasn't showing. + const onlyUseSyntacticOwners = !!triggerReason && triggerReason.kind === "characterTyped"; + // Bail out quickly in the middle of a string or comment, don't provide signature help unless the user explicitly requested it. + if (onlyUseSyntacticOwners && (isInString(sourceFile, position, startingToken) || isInComment(sourceFile, position))) { + return undefined; } - - function getCandidateOrTypeInfo({ invocation, argumentCount }: ArgumentListInfo, checker: TypeChecker, sourceFile: SourceFile, startingToken: Node, onlyUseSyntacticOwners: boolean): CandidateInfo | TypeInfo | undefined { - switch (invocation.kind) { - case InvocationKind.Call: { - if (onlyUseSyntacticOwners && !isSyntacticOwner(startingToken, invocation.node, sourceFile)) { - return undefined; - } - const candidates: Signature[] = []; - const resolvedSignature = checker.getResolvedSignatureForSignatureHelp(invocation.node, candidates, argumentCount)!; // TODO: GH#18217 - return candidates.length === 0 ? undefined : { kind: CandidateOrTypeKind.Candidate, candidates, resolvedSignature }; - } - case InvocationKind.TypeArgs: { - const { called } = invocation; - if (onlyUseSyntacticOwners && !containsPrecedingToken(startingToken, sourceFile, isIdentifier(called) ? called.parent : called)) { - return undefined; - } - const candidates = getPossibleGenericSignatures(called, argumentCount, checker); - if (candidates.length !== 0) return { kind: CandidateOrTypeKind.Candidate, candidates, resolvedSignature: first(candidates) }; - - const symbol = checker.getSymbolAtLocation(called); - return symbol && { kind: CandidateOrTypeKind.Type, symbol }; + const isManuallyInvoked = !!triggerReason && triggerReason.kind === "invoked"; + const argumentInfo = getContainingArgumentInfo(startingToken, position, sourceFile, typeChecker, isManuallyInvoked); + if (!argumentInfo) + return undefined; + cancellationToken.throwIfCancellationRequested(); + // Extra syntactic and semantic filtering of signature help + const candidateInfo = getCandidateOrTypeInfo(argumentInfo, typeChecker, sourceFile, startingToken, onlyUseSyntacticOwners); + cancellationToken.throwIfCancellationRequested(); + if (!candidateInfo) { + // We didn't have any sig help items produced by the TS compiler. If this is a JS + // file, then see if we can figure out anything better. + return isSourceFileJS(sourceFile) ? createJSSignatureHelpItems(argumentInfo, program, cancellationToken) : undefined; + } + return typeChecker.runWithCancellationToken(cancellationToken, typeChecker => candidateInfo.kind === CandidateOrTypeKind.Candidate + ? createSignatureHelpItems(candidateInfo.candidates, candidateInfo.resolvedSignature, argumentInfo, sourceFile, typeChecker) + : createTypeHelpItems(candidateInfo.symbol, argumentInfo, sourceFile, typeChecker)); +} +/* @internal */ +const enum CandidateOrTypeKind { + Candidate, + Type +} +/* @internal */ +interface CandidateInfo { + readonly kind: CandidateOrTypeKind.Candidate; + readonly candidates: readonly Signature[]; + readonly resolvedSignature: Signature; +} +/* @internal */ +interface TypeInfo { + readonly kind: CandidateOrTypeKind.Type; + readonly symbol: Symbol; +} +/* @internal */ +function getCandidateOrTypeInfo({ invocation, argumentCount }: ArgumentListInfo, checker: TypeChecker, sourceFile: SourceFile, startingToken: Node, onlyUseSyntacticOwners: boolean): CandidateInfo | TypeInfo | undefined { + switch (invocation.kind) { + case InvocationKind.Call: { + if (onlyUseSyntacticOwners && !isSyntacticOwner(startingToken, invocation.node, sourceFile)) { + return undefined; } - case InvocationKind.Contextual: - return { kind: CandidateOrTypeKind.Candidate, candidates: [invocation.signature], resolvedSignature: invocation.signature }; - default: - return Debug.assertNever(invocation); + const candidates: Signature[] = []; + const resolvedSignature = checker.getResolvedSignatureForSignatureHelp(invocation.node, candidates, argumentCount)!; // TODO: GH#18217 + return candidates.length === 0 ? undefined : { kind: CandidateOrTypeKind.Candidate, candidates, resolvedSignature }; } - } - - function isSyntacticOwner(startingToken: Node, node: CallLikeExpression, sourceFile: SourceFile): boolean { - if (!isCallOrNewExpression(node)) return false; - const invocationChildren = node.getChildren(sourceFile); - switch (startingToken.kind) { - case SyntaxKind.OpenParenToken: - return contains(invocationChildren, startingToken); - case SyntaxKind.CommaToken: { - const containingList = findContainingList(startingToken); - return !!containingList && contains(invocationChildren, containingList); + case InvocationKind.TypeArgs: { + const { called } = invocation; + if (onlyUseSyntacticOwners && !containsPrecedingToken(startingToken, sourceFile, isIdentifier(called) ? called.parent : called)) { + return undefined; } - case SyntaxKind.LessThanToken: - return containsPrecedingToken(startingToken, sourceFile, node.expression); - default: - return false; + const candidates = getPossibleGenericSignatures(called, argumentCount, checker); + if (candidates.length !== 0) + return { kind: CandidateOrTypeKind.Candidate, candidates, resolvedSignature: first(candidates) }; + const symbol = checker.getSymbolAtLocation(called); + return symbol && { kind: CandidateOrTypeKind.Type, symbol }; } + case InvocationKind.Contextual: + return { kind: CandidateOrTypeKind.Candidate, candidates: [invocation.signature], resolvedSignature: invocation.signature }; + default: + return Debug.assertNever(invocation); } - - function createJSSignatureHelpItems(argumentInfo: ArgumentListInfo, program: Program, cancellationToken: CancellationToken): SignatureHelpItems | undefined { - if (argumentInfo.invocation.kind === InvocationKind.Contextual) return undefined; - // See if we can find some symbol with the call expression name that has call signatures. - const expression = getExpressionFromInvocation(argumentInfo.invocation); - const name = isIdentifier(expression) ? expression.text : isPropertyAccessExpression(expression) ? expression.name.text : undefined; - const typeChecker = program.getTypeChecker(); - return name === undefined ? undefined : firstDefined(program.getSourceFiles(), sourceFile => - firstDefined(sourceFile.getNamedDeclarations().get(name), declaration => { - const type = declaration.symbol && typeChecker.getTypeOfSymbolAtLocation(declaration.symbol, declaration); - const callSignatures = type && type.getCallSignatures(); - if (callSignatures && callSignatures.length) { - return typeChecker.runWithCancellationToken(cancellationToken, typeChecker => createSignatureHelpItems(callSignatures, callSignatures[0], argumentInfo, sourceFile, typeChecker)); - } - })); - } - - function containsPrecedingToken(startingToken: Node, sourceFile: SourceFile, container: Node) { - const pos = startingToken.getFullStart(); - // There’s a possibility that `startingToken.parent` contains only `startingToken` and - // missing nodes, none of which are valid to be returned by `findPrecedingToken`. In that - // case, the preceding token we want is actually higher up the tree—almost definitely the - // next parent, but theoretically the situation with missing nodes might be happening on - // multiple nested levels. - let currentParent: Node | undefined = startingToken.parent; - while (currentParent) { - const precedingToken = findPrecedingToken(pos, sourceFile, currentParent, /*excludeJsdoc*/ true); - if (precedingToken) { - return rangeContainsRange(container, precedingToken); - } - currentParent = currentParent.parent; +} +/* @internal */ +function isSyntacticOwner(startingToken: Node, node: CallLikeExpression, sourceFile: SourceFile): boolean { + if (!isCallOrNewExpression(node)) + return false; + const invocationChildren = node.getChildren(sourceFile); + switch (startingToken.kind) { + case SyntaxKind.OpenParenToken: + return contains(invocationChildren, startingToken); + case SyntaxKind.CommaToken: { + const containingList = findContainingList(startingToken); + return !!containingList && contains(invocationChildren, containingList); } - return Debug.fail("Could not find preceding token"); + case SyntaxKind.LessThanToken: + return containsPrecedingToken(startingToken, sourceFile, node.expression); + default: + return false; } - - export interface ArgumentInfoForCompletions { - readonly invocation: CallLikeExpression; - readonly argumentIndex: number; - readonly argumentCount: number; +} +/* @internal */ +function createJSSignatureHelpItems(argumentInfo: ArgumentListInfo, program: Program, cancellationToken: CancellationToken): SignatureHelpItems | undefined { + if (argumentInfo.invocation.kind === InvocationKind.Contextual) + return undefined; + // See if we can find some symbol with the call expression name that has call signatures. + const expression = getExpressionFromInvocation(argumentInfo.invocation); + const name = isIdentifier(expression) ? expression.text : isPropertyAccessExpression(expression) ? expression.name.text : undefined; + const typeChecker = program.getTypeChecker(); + return name === undefined ? undefined : firstDefined(program.getSourceFiles(), sourceFile => firstDefined(sourceFile.getNamedDeclarations().get(name), declaration => { + const type = declaration.symbol && typeChecker.getTypeOfSymbolAtLocation(declaration.symbol, declaration); + const callSignatures = type && type.getCallSignatures(); + if (callSignatures && callSignatures.length) { + return typeChecker.runWithCancellationToken(cancellationToken, typeChecker => createSignatureHelpItems(callSignatures, callSignatures[0], argumentInfo, sourceFile, typeChecker)); + } + })); +} +/* @internal */ +function containsPrecedingToken(startingToken: Node, sourceFile: SourceFile, container: Node) { + const pos = startingToken.getFullStart(); + // There’s a possibility that `startingToken.parent` contains only `startingToken` and + // missing nodes, none of which are valid to be returned by `findPrecedingToken`. In that + // case, the preceding token we want is actually higher up the tree—almost definitely the + // next parent, but theoretically the situation with missing nodes might be happening on + // multiple nested levels. + let currentParent: Node | undefined = startingToken.parent; + while (currentParent) { + const precedingToken = findPrecedingToken(pos, sourceFile, currentParent, /*excludeJsdoc*/ true); + if (precedingToken) { + return rangeContainsRange(container, precedingToken); + } + currentParent = currentParent.parent; } - export function getArgumentInfoForCompletions(node: Node, position: number, sourceFile: SourceFile): ArgumentInfoForCompletions | undefined { - const info = getImmediatelyContainingArgumentInfo(node, position, sourceFile); - return !info || info.isTypeParameterList || info.invocation.kind !== InvocationKind.Call ? undefined - : { invocation: info.invocation.node, argumentCount: info.argumentCount, argumentIndex: info.argumentIndex }; + return Debug.fail("Could not find preceding token"); +} +/* @internal */ +export interface ArgumentInfoForCompletions { + readonly invocation: CallLikeExpression; + readonly argumentIndex: number; + readonly argumentCount: number; +} +/* @internal */ +export function getArgumentInfoForCompletions(node: Node, position: number, sourceFile: SourceFile): ArgumentInfoForCompletions | undefined { + const info = getImmediatelyContainingArgumentInfo(node, position, sourceFile); + return !info || info.isTypeParameterList || info.invocation.kind !== InvocationKind.Call ? undefined + : { invocation: info.invocation.node, argumentCount: info.argumentCount, argumentIndex: info.argumentIndex }; +} +/* @internal */ +function getArgumentOrParameterListInfo(node: Node, sourceFile: SourceFile): { + readonly list: Node; + readonly argumentIndex: number; + readonly argumentCount: number; + readonly argumentsSpan: TextSpan; +} | undefined { + const info = getArgumentOrParameterListAndIndex(node, sourceFile); + if (!info) + return undefined; + const { list, argumentIndex } = info; + const argumentCount = getArgumentCount(list); + if (argumentIndex !== 0) { + Debug.assertLessThan(argumentIndex, argumentCount); } - - function getArgumentOrParameterListInfo(node: Node, sourceFile: SourceFile): { readonly list: Node, readonly argumentIndex: number, readonly argumentCount: number, readonly argumentsSpan: TextSpan } | undefined { - const info = getArgumentOrParameterListAndIndex(node, sourceFile); - if (!info) return undefined; - const { list, argumentIndex } = info; - - const argumentCount = getArgumentCount(list); - if (argumentIndex !== 0) { - Debug.assertLessThan(argumentIndex, argumentCount); - } - const argumentsSpan = getApplicableSpanForArguments(list, sourceFile); - return { list, argumentIndex, argumentCount, argumentsSpan }; + const argumentsSpan = getApplicableSpanForArguments(list, sourceFile); + return { list, argumentIndex, argumentCount, argumentsSpan }; +} +/* @internal */ +function getArgumentOrParameterListAndIndex(node: Node, sourceFile: SourceFile): { + readonly list: Node; + readonly argumentIndex: number; +} | undefined { + if (node.kind === SyntaxKind.LessThanToken || node.kind === SyntaxKind.OpenParenToken) { + // Find the list that starts right *after* the < or ( token. + // If the user has just opened a list, consider this item 0. + return { list: getChildListThatStartsWithOpenerToken(node.parent, node, sourceFile), argumentIndex: 0 }; + } + else { + // findListItemInfo can return undefined if we are not in parent's argument list + // or type argument list. This includes cases where the cursor is: + // - To the right of the closing parenthesis, non-substitution template, or template tail. + // - Between the type arguments and the arguments (greater than token) + // - On the target of the call (parent.func) + // - On the 'new' keyword in a 'new' expression + const list = findContainingList(node); + return list && { list, argumentIndex: getArgumentIndex(list, node) }; } - function getArgumentOrParameterListAndIndex(node: Node, sourceFile: SourceFile): { readonly list: Node, readonly argumentIndex: number } | undefined { - if (node.kind === SyntaxKind.LessThanToken || node.kind === SyntaxKind.OpenParenToken) { - // Find the list that starts right *after* the < or ( token. - // If the user has just opened a list, consider this item 0. - return { list: getChildListThatStartsWithOpenerToken(node.parent, node, sourceFile), argumentIndex: 0 }; - } - else { - // findListItemInfo can return undefined if we are not in parent's argument list - // or type argument list. This includes cases where the cursor is: - // - To the right of the closing parenthesis, non-substitution template, or template tail. - // - Between the type arguments and the arguments (greater than token) - // - On the target of the call (parent.func) - // - On the 'new' keyword in a 'new' expression - const list = findContainingList(node); - return list && { list, argumentIndex: getArgumentIndex(list, node) }; +} +/** + * Returns relevant information for the argument list and the current argument if we are + * in the argument of an invocation; returns undefined otherwise. + */ +/* @internal */ +function getImmediatelyContainingArgumentInfo(node: Node, position: number, sourceFile: SourceFile): ArgumentListInfo | undefined { + const { parent } = node; + if (isCallOrNewExpression(parent)) { + const invocation = parent; + // There are 3 cases to handle: + // 1. The token introduces a list, and should begin a signature help session + // 2. The token is either not associated with a list, or ends a list, so the session should end + // 3. The token is buried inside a list, and should give signature help + // + // The following are examples of each: + // + // Case 1: + // foo<#T, U>(#a, b) -> The token introduces a list, and should begin a signature help session + // Case 2: + // fo#o#(a, b)# -> The token is either not associated with a list, or ends a list, so the session should end + // Case 3: + // foo(a#, #b#) -> The token is buried inside a list, and should give signature help + // Find out if 'node' is an argument, a type argument, or neither + const info = getArgumentOrParameterListInfo(node, sourceFile); + if (!info) + return undefined; + const { list, argumentIndex, argumentCount, argumentsSpan } = info; + const isTypeParameterList = !!parent.typeArguments && parent.typeArguments.pos === list.pos; + return { isTypeParameterList, invocation: { kind: InvocationKind.Call, node: invocation }, argumentsSpan, argumentIndex, argumentCount }; + } + else if (isNoSubstitutionTemplateLiteral(node) && isTaggedTemplateExpression(parent)) { + // Check if we're actually inside the template; + // otherwise we'll fall out and return undefined. + if (isInsideTemplateLiteral(node, position, sourceFile)) { + return getArgumentListInfoForTemplate(parent, /*argumentIndex*/ 0, sourceFile); } + return undefined; } - - /** - * Returns relevant information for the argument list and the current argument if we are - * in the argument of an invocation; returns undefined otherwise. - */ - function getImmediatelyContainingArgumentInfo(node: Node, position: number, sourceFile: SourceFile): ArgumentListInfo | undefined { - const { parent } = node; - if (isCallOrNewExpression(parent)) { - const invocation = parent; - - // There are 3 cases to handle: - // 1. The token introduces a list, and should begin a signature help session - // 2. The token is either not associated with a list, or ends a list, so the session should end - // 3. The token is buried inside a list, and should give signature help - // - // The following are examples of each: - // - // Case 1: - // foo<#T, U>(#a, b) -> The token introduces a list, and should begin a signature help session - // Case 2: - // fo#o#(a, b)# -> The token is either not associated with a list, or ends a list, so the session should end - // Case 3: - // foo(a#, #b#) -> The token is buried inside a list, and should give signature help - // Find out if 'node' is an argument, a type argument, or neither - const info = getArgumentOrParameterListInfo(node, sourceFile); - if (!info) return undefined; - const { list, argumentIndex, argumentCount, argumentsSpan } = info; - const isTypeParameterList = !!parent.typeArguments && parent.typeArguments.pos === list.pos; - return { isTypeParameterList, invocation: { kind: InvocationKind.Call, node: invocation }, argumentsSpan, argumentIndex, argumentCount }; - } - else if (isNoSubstitutionTemplateLiteral(node) && isTaggedTemplateExpression(parent)) { - // Check if we're actually inside the template; - // otherwise we'll fall out and return undefined. - if (isInsideTemplateLiteral(node, position, sourceFile)) { - return getArgumentListInfoForTemplate(parent, /*argumentIndex*/ 0, sourceFile); - } + else if (isTemplateHead(node) && parent.parent.kind === SyntaxKind.TaggedTemplateExpression) { + const templateExpression = (parent); + const tagExpression = (templateExpression.parent); + Debug.assert(templateExpression.kind === SyntaxKind.TemplateExpression); + const argumentIndex = isInsideTemplateLiteral(node, position, sourceFile) ? 0 : 1; + return getArgumentListInfoForTemplate(tagExpression, argumentIndex, sourceFile); + } + else if (isTemplateSpan(parent) && isTaggedTemplateExpression(parent.parent.parent)) { + const templateSpan = parent; + const tagExpression = parent.parent.parent; + // If we're just after a template tail, don't show signature help. + if (isTemplateTail(node) && !isInsideTemplateLiteral(node, position, sourceFile)) { return undefined; } - else if (isTemplateHead(node) && parent.parent.kind === SyntaxKind.TaggedTemplateExpression) { - const templateExpression = parent; - const tagExpression = templateExpression.parent; - Debug.assert(templateExpression.kind === SyntaxKind.TemplateExpression); - - const argumentIndex = isInsideTemplateLiteral(node, position, sourceFile) ? 0 : 1; - - return getArgumentListInfoForTemplate(tagExpression, argumentIndex, sourceFile); + const spanIndex = templateSpan.parent.templateSpans.indexOf(templateSpan); + const argumentIndex = getArgumentIndexForTemplatePiece(spanIndex, node, position, sourceFile); + return getArgumentListInfoForTemplate(tagExpression, argumentIndex, sourceFile); + } + else if (isJsxOpeningLikeElement(parent)) { + // Provide a signature help for JSX opening element or JSX self-closing element. + // This is not guarantee that JSX tag-name is resolved into stateless function component. (that is done in "getSignatureHelpItems") + // i.e + // export function MainButton(props: ButtonProps, context: any): JSX.Element { ... } + // isFunctionTypeNode(d) ? d.parent.symbol : undefined) || s + : s; +} +/* @internal */ +function getArgumentIndex(argumentsList: Node, node: Node) { + // The list we got back can include commas. In the presence of errors it may + // also just have nodes without commas. For example "Foo(a b c)" will have 3 + // args without commas. We want to find what index we're at. So we count + // forward until we hit ourselves, only incrementing the index if it isn't a + // comma. + // + // Note: the subtlety around trailing commas (in getArgumentCount) does not apply + // here. That's because we're only walking forward until we hit the node we're + // on. In that case, even if we're after the trailing comma, we'll still see + // that trailing comma in the list, and we'll have generated the appropriate + // arg index. + let argumentIndex = 0; + for (const child of argumentsList.getChildren()) { + if (child === node) { + break; } - } - - // The type of a function type node has a symbol at that node, but it's better to use the symbol for a parameter or type alias. - function chooseBetterSymbol(s: Symbol): Symbol { - return s.name === InternalSymbolName.Type - ? firstDefined(s.declarations, d => isFunctionTypeNode(d) ? d.parent.symbol : undefined) || s - : s; - } - - function getArgumentIndex(argumentsList: Node, node: Node) { - // The list we got back can include commas. In the presence of errors it may - // also just have nodes without commas. For example "Foo(a b c)" will have 3 - // args without commas. We want to find what index we're at. So we count - // forward until we hit ourselves, only incrementing the index if it isn't a - // comma. - // - // Note: the subtlety around trailing commas (in getArgumentCount) does not apply - // here. That's because we're only walking forward until we hit the node we're - // on. In that case, even if we're after the trailing comma, we'll still see - // that trailing comma in the list, and we'll have generated the appropriate - // arg index. - let argumentIndex = 0; - for (const child of argumentsList.getChildren()) { - if (child === node) { - break; - } - if (child.kind !== SyntaxKind.CommaToken) { - argumentIndex++; - } + if (child.kind !== SyntaxKind.CommaToken) { + argumentIndex++; } - - return argumentIndex; } - - function getArgumentCount(argumentsList: Node) { - // The argument count for a list is normally the number of non-comma children it has. - // For example, if you have "Foo(a,b)" then there will be three children of the arg - // list 'a' '' 'b'. So, in this case the arg count will be 2. However, there - // is a small subtlety. If you have "Foo(a,)", then the child list will just have - // 'a' ''. So, in the case where the last child is a comma, we increase the - // arg count by one to compensate. - // - // Note: this subtlety only applies to the last comma. If you had "Foo(a,," then - // we'll have: 'a' '' '' - // That will give us 2 non-commas. We then add one for the last comma, giving us an - // arg count of 3. - const listChildren = argumentsList.getChildren(); - - let argumentCount = countWhere(listChildren, arg => arg.kind !== SyntaxKind.CommaToken); - if (listChildren.length > 0 && last(listChildren).kind === SyntaxKind.CommaToken) { - argumentCount++; + return argumentIndex; +} +/* @internal */ +function getArgumentCount(argumentsList: Node) { + // The argument count for a list is normally the number of non-comma children it has. + // For example, if you have "Foo(a,b)" then there will be three children of the arg + // list 'a' '' 'b'. So, in this case the arg count will be 2. However, there + // is a small subtlety. If you have "Foo(a,)", then the child list will just have + // 'a' ''. So, in the case where the last child is a comma, we increase the + // arg count by one to compensate. + // + // Note: this subtlety only applies to the last comma. If you had "Foo(a,," then + // we'll have: 'a' '' '' + // That will give us 2 non-commas. We then add one for the last comma, giving us an + // arg count of 3. + const listChildren = argumentsList.getChildren(); + let argumentCount = countWhere(listChildren, arg => arg.kind !== SyntaxKind.CommaToken); + if (listChildren.length > 0 && last(listChildren).kind === SyntaxKind.CommaToken) { + argumentCount++; + } + return argumentCount; +} +// spanIndex is either the index for a given template span. +// This does not give appropriate results for a NoSubstitutionTemplateLiteral +/* @internal */ +function getArgumentIndexForTemplatePiece(spanIndex: number, node: Node, position: number, sourceFile: SourceFile): number { + // Because the TemplateStringsArray is the first argument, we have to offset each substitution expression by 1. + // There are three cases we can encounter: + // 1. We are precisely in the template literal (argIndex = 0). + // 2. We are in or to the right of the substitution expression (argIndex = spanIndex + 1). + // 3. We are directly to the right of the template literal, but because we look for the token on the left, + // not enough to put us in the substitution expression; we should consider ourselves part of + // the *next* span's expression by offsetting the index (argIndex = (spanIndex + 1) + 1). + // + /* eslint-disable no-double-space */ + // Example: f `# abcd $#{# 1 + 1# }# efghi ${ #"#hello"# } # ` + // ^ ^ ^ ^ ^ ^ ^ ^ ^ + // Case: 1 1 3 2 1 3 2 2 1 + /* eslint-enable no-double-space */ + Debug.assert(position >= node.getStart(), "Assumed 'position' could not occur before node."); + if (isTemplateLiteralToken(node)) { + if (isInsideTemplateLiteral(node, position, sourceFile)) { + return 0; } - - return argumentCount; + return spanIndex + 2; } - - // spanIndex is either the index for a given template span. - // This does not give appropriate results for a NoSubstitutionTemplateLiteral - function getArgumentIndexForTemplatePiece(spanIndex: number, node: Node, position: number, sourceFile: SourceFile): number { - // Because the TemplateStringsArray is the first argument, we have to offset each substitution expression by 1. - // There are three cases we can encounter: - // 1. We are precisely in the template literal (argIndex = 0). - // 2. We are in or to the right of the substitution expression (argIndex = spanIndex + 1). - // 3. We are directly to the right of the template literal, but because we look for the token on the left, - // not enough to put us in the substitution expression; we should consider ourselves part of - // the *next* span's expression by offsetting the index (argIndex = (spanIndex + 1) + 1). - // - /* eslint-disable no-double-space */ - // Example: f `# abcd $#{# 1 + 1# }# efghi ${ #"#hello"# } # ` - // ^ ^ ^ ^ ^ ^ ^ ^ ^ - // Case: 1 1 3 2 1 3 2 2 1 - /* eslint-enable no-double-space */ - Debug.assert(position >= node.getStart(), "Assumed 'position' could not occur before node."); - if (isTemplateLiteralToken(node)) { - if (isInsideTemplateLiteral(node, position, sourceFile)) { - return 0; - } - return spanIndex + 2; + return spanIndex + 1; +} +/* @internal */ +function getArgumentListInfoForTemplate(tagExpression: TaggedTemplateExpression, argumentIndex: number, sourceFile: SourceFile): ArgumentListInfo { + // argumentCount is either 1 or (numSpans + 1) to account for the template strings array argument. + const argumentCount = isNoSubstitutionTemplateLiteral(tagExpression.template) ? 1 : tagExpression.template.templateSpans.length + 1; + if (argumentIndex !== 0) { + Debug.assertLessThan(argumentIndex, argumentCount); + } + return { + isTypeParameterList: false, + invocation: { kind: InvocationKind.Call, node: tagExpression }, + argumentsSpan: getApplicableSpanForTaggedTemplate(tagExpression, sourceFile), + argumentIndex, + argumentCount + }; +} +/* @internal */ +function getApplicableSpanForArguments(argumentsList: Node, sourceFile: SourceFile): TextSpan { + // We use full start and skip trivia on the end because we want to include trivia on + // both sides. For example, + // + // foo( /*comment */ a, b, c /*comment*/ ) + // | | + // + // The applicable span is from the first bar to the second bar (inclusive, + // but not including parentheses) + const applicableSpanStart = argumentsList.getFullStart(); + const applicableSpanEnd = skipTrivia(sourceFile.text, argumentsList.getEnd(), /*stopAfterLineBreak*/ false); + return createTextSpan(applicableSpanStart, applicableSpanEnd - applicableSpanStart); +} +/* @internal */ +function getApplicableSpanForTaggedTemplate(taggedTemplate: TaggedTemplateExpression, sourceFile: SourceFile): TextSpan { + const template = taggedTemplate.template; + const applicableSpanStart = template.getStart(); + let applicableSpanEnd = template.getEnd(); + // We need to adjust the end position for the case where the template does not have a tail. + // Otherwise, we will not show signature help past the expression. + // For example, + // + // ` ${ 1 + 1 foo(10) + // | | + // This is because a Missing node has no width. However, what we actually want is to include trivia + // leading up to the next token in case the user is about to type in a TemplateMiddle or TemplateTail. + if (template.kind === SyntaxKind.TemplateExpression) { + const lastSpan = last(template.templateSpans); + if (lastSpan.literal.getFullWidth() === 0) { + applicableSpanEnd = skipTrivia(sourceFile.text, applicableSpanEnd, /*stopAfterLineBreak*/ false); } - return spanIndex + 1; } - - function getArgumentListInfoForTemplate(tagExpression: TaggedTemplateExpression, argumentIndex: number, sourceFile: SourceFile): ArgumentListInfo { - // argumentCount is either 1 or (numSpans + 1) to account for the template strings array argument. - const argumentCount = isNoSubstitutionTemplateLiteral(tagExpression.template) ? 1 : tagExpression.template.templateSpans.length + 1; - if (argumentIndex !== 0) { - Debug.assertLessThan(argumentIndex, argumentCount); + return createTextSpan(applicableSpanStart, applicableSpanEnd - applicableSpanStart); +} +/* @internal */ +function getContainingArgumentInfo(node: Node, position: number, sourceFile: SourceFile, checker: TypeChecker, isManuallyInvoked: boolean): ArgumentListInfo | undefined { + for (let n = node; !isSourceFile(n) && (isManuallyInvoked || !isBlock(n)); n = n.parent) { + // If the node is not a subspan of its parent, this is a big problem. + // There have been crashes that might be caused by this violation. + Debug.assert(rangeContainsRange(n.parent, n), "Not a subspan", () => `Child: ${Debug.formatSyntaxKind(n.kind)}, parent: ${Debug.formatSyntaxKind(n.parent.kind)}`); + const argumentInfo = getImmediatelyContainingArgumentOrContextualParameterInfo(n, position, sourceFile, checker); + if (argumentInfo) { + return argumentInfo; } - return { - isTypeParameterList: false, - invocation: { kind: InvocationKind.Call, node: tagExpression }, - argumentsSpan: getApplicableSpanForTaggedTemplate(tagExpression, sourceFile), - argumentIndex, - argumentCount - }; - } - - function getApplicableSpanForArguments(argumentsList: Node, sourceFile: SourceFile): TextSpan { - // We use full start and skip trivia on the end because we want to include trivia on - // both sides. For example, - // - // foo( /*comment */ a, b, c /*comment*/ ) - // | | - // - // The applicable span is from the first bar to the second bar (inclusive, - // but not including parentheses) - const applicableSpanStart = argumentsList.getFullStart(); - const applicableSpanEnd = skipTrivia(sourceFile.text, argumentsList.getEnd(), /*stopAfterLineBreak*/ false); - return createTextSpan(applicableSpanStart, applicableSpanEnd - applicableSpanStart); } - - function getApplicableSpanForTaggedTemplate(taggedTemplate: TaggedTemplateExpression, sourceFile: SourceFile): TextSpan { - const template = taggedTemplate.template; - const applicableSpanStart = template.getStart(); - let applicableSpanEnd = template.getEnd(); - - // We need to adjust the end position for the case where the template does not have a tail. - // Otherwise, we will not show signature help past the expression. - // For example, - // - // ` ${ 1 + 1 foo(10) - // | | - // This is because a Missing node has no width. However, what we actually want is to include trivia - // leading up to the next token in case the user is about to type in a TemplateMiddle or TemplateTail. - if (template.kind === SyntaxKind.TemplateExpression) { - const lastSpan = last(template.templateSpans); - if (lastSpan.literal.getFullWidth() === 0) { - applicableSpanEnd = skipTrivia(sourceFile.text, applicableSpanEnd, /*stopAfterLineBreak*/ false); - } + return undefined; +} +/* @internal */ +function getChildListThatStartsWithOpenerToken(parent: Node, openerToken: Node, sourceFile: SourceFile): Node { + const children = parent.getChildren(sourceFile); + const indexOfOpenerToken = children.indexOf(openerToken); + Debug.assert(indexOfOpenerToken >= 0 && children.length > indexOfOpenerToken + 1); + return children[indexOfOpenerToken + 1]; +} +/* @internal */ +function getExpressionFromInvocation(invocation: CallInvocation | TypeArgsInvocation): Expression { + return invocation.kind === InvocationKind.Call ? getInvokedExpression(invocation.node) : invocation.called; +} +/* @internal */ +function getEnclosingDeclarationFromInvocation(invocation: Invocation): Node { + return invocation.kind === InvocationKind.Call ? invocation.node : invocation.kind === InvocationKind.TypeArgs ? invocation.called : invocation.node; +} +/* @internal */ +const signatureHelpNodeBuilderFlags = NodeBuilderFlags.OmitParameterModifiers | NodeBuilderFlags.IgnoreErrors | NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope; +/* @internal */ +function createSignatureHelpItems(candidates: readonly Signature[], resolvedSignature: Signature, { isTypeParameterList, argumentCount, argumentsSpan: applicableSpan, invocation, argumentIndex }: ArgumentListInfo, sourceFile: SourceFile, typeChecker: TypeChecker): SignatureHelpItems { + const enclosingDeclaration = getEnclosingDeclarationFromInvocation(invocation); + const callTargetSymbol = invocation.kind === InvocationKind.Contextual ? invocation.symbol : typeChecker.getSymbolAtLocation(getExpressionFromInvocation(invocation)); + const callTargetDisplayParts = callTargetSymbol ? symbolToDisplayParts(typeChecker, callTargetSymbol, /*enclosingDeclaration*/ undefined, /*meaning*/ undefined) : emptyArray; + const items = candidates.map(candidateSignature => getSignatureHelpItem(candidateSignature, callTargetDisplayParts, isTypeParameterList, typeChecker, enclosingDeclaration, sourceFile)); + if (argumentIndex !== 0) { + Debug.assertLessThan(argumentIndex, argumentCount); + } + const selectedItemIndex = candidates.indexOf(resolvedSignature); + Debug.assert(selectedItemIndex !== -1); // If candidates is non-empty it should always include bestSignature. We check for an empty candidates before calling this function. + return { items, applicableSpan, selectedItemIndex, argumentIndex, argumentCount }; +} +/* @internal */ +function createTypeHelpItems(symbol: Symbol, { argumentCount, argumentsSpan: applicableSpan, invocation, argumentIndex }: ArgumentListInfo, sourceFile: SourceFile, checker: TypeChecker): SignatureHelpItems | undefined { + const typeParameters = checker.getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol); + if (!typeParameters) + return undefined; + const items = [getTypeHelpItem(symbol, typeParameters, checker, getEnclosingDeclarationFromInvocation(invocation), sourceFile)]; + return { items, applicableSpan, selectedItemIndex: 0, argumentIndex, argumentCount }; +} +/* @internal */ +function getTypeHelpItem(symbol: Symbol, typeParameters: readonly TypeParameter[], checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItem { + const typeSymbolDisplay = symbolToDisplayParts(checker, symbol); + const printer = createPrinter({ removeComments: true }); + const parameters = typeParameters.map(t => createSignatureHelpParameterForTypeParameter(t, checker, enclosingDeclaration, sourceFile, printer)); + const documentation = symbol.getDocumentationComment(checker); + const tags = symbol.getJsDocTags(); + const prefixDisplayParts = [...typeSymbolDisplay, punctuationPart(SyntaxKind.LessThanToken)]; + return { isVariadic: false, prefixDisplayParts, suffixDisplayParts: [punctuationPart(SyntaxKind.GreaterThanToken)], separatorDisplayParts, parameters, documentation, tags }; +} +/* @internal */ +const separatorDisplayParts: SymbolDisplayPart[] = [punctuationPart(SyntaxKind.CommaToken), spacePart()]; +/* @internal */ +function getSignatureHelpItem(candidateSignature: Signature, callTargetDisplayParts: readonly SymbolDisplayPart[], isTypeParameterList: boolean, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItem { + const { isVariadic, parameters, prefix, suffix } = (isTypeParameterList ? itemInfoForTypeParameters : itemInfoForParameters)(candidateSignature, checker, enclosingDeclaration, sourceFile); + const prefixDisplayParts = [...callTargetDisplayParts, ...prefix]; + const suffixDisplayParts = [...suffix, ...returnTypeToDisplayParts(candidateSignature, enclosingDeclaration, checker)]; + const documentation = candidateSignature.getDocumentationComment(checker); + const tags = candidateSignature.getJsDocTags(); + return { isVariadic, prefixDisplayParts, suffixDisplayParts, separatorDisplayParts, parameters, documentation, tags }; +} +/* @internal */ +function returnTypeToDisplayParts(candidateSignature: Signature, enclosingDeclaration: Node, checker: TypeChecker): readonly SymbolDisplayPart[] { + return mapToDisplayParts(writer => { + writer.writePunctuation(":"); + writer.writeSpace(" "); + const predicate = checker.getTypePredicateOfSignature(candidateSignature); + if (predicate) { + checker.writeTypePredicate(predicate, enclosingDeclaration, /*flags*/ undefined, writer); } - - return createTextSpan(applicableSpanStart, applicableSpanEnd - applicableSpanStart); - } - - function getContainingArgumentInfo(node: Node, position: number, sourceFile: SourceFile, checker: TypeChecker, isManuallyInvoked: boolean): ArgumentListInfo | undefined { - for (let n = node; !isSourceFile(n) && (isManuallyInvoked || !isBlock(n)); n = n.parent) { - // If the node is not a subspan of its parent, this is a big problem. - // There have been crashes that might be caused by this violation. - Debug.assert(rangeContainsRange(n.parent, n), "Not a subspan", () => `Child: ${Debug.formatSyntaxKind(n.kind)}, parent: ${Debug.formatSyntaxKind(n.parent.kind)}`); - const argumentInfo = getImmediatelyContainingArgumentOrContextualParameterInfo(n, position, sourceFile, checker); - if (argumentInfo) { - return argumentInfo; - } + else { + checker.writeType(checker.getReturnTypeOfSignature(candidateSignature), enclosingDeclaration, /*flags*/ undefined, writer); } - return undefined; - } - - function getChildListThatStartsWithOpenerToken(parent: Node, openerToken: Node, sourceFile: SourceFile): Node { - const children = parent.getChildren(sourceFile); - const indexOfOpenerToken = children.indexOf(openerToken); - Debug.assert(indexOfOpenerToken >= 0 && children.length > indexOfOpenerToken + 1); - return children[indexOfOpenerToken + 1]; - } - - function getExpressionFromInvocation(invocation: CallInvocation | TypeArgsInvocation): Expression { - return invocation.kind === InvocationKind.Call ? getInvokedExpression(invocation.node) : invocation.called; - } - - function getEnclosingDeclarationFromInvocation(invocation: Invocation): Node { - return invocation.kind === InvocationKind.Call ? invocation.node : invocation.kind === InvocationKind.TypeArgs ? invocation.called : invocation.node; - } - - const signatureHelpNodeBuilderFlags = NodeBuilderFlags.OmitParameterModifiers | NodeBuilderFlags.IgnoreErrors | NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope; - function createSignatureHelpItems( - candidates: readonly Signature[], - resolvedSignature: Signature, - { isTypeParameterList, argumentCount, argumentsSpan: applicableSpan, invocation, argumentIndex }: ArgumentListInfo, - sourceFile: SourceFile, - typeChecker: TypeChecker, - ): SignatureHelpItems { - const enclosingDeclaration = getEnclosingDeclarationFromInvocation(invocation); - const callTargetSymbol = invocation.kind === InvocationKind.Contextual ? invocation.symbol : typeChecker.getSymbolAtLocation(getExpressionFromInvocation(invocation)); - const callTargetDisplayParts = callTargetSymbol ? symbolToDisplayParts(typeChecker, callTargetSymbol, /*enclosingDeclaration*/ undefined, /*meaning*/ undefined) : emptyArray; - const items = candidates.map(candidateSignature => getSignatureHelpItem(candidateSignature, callTargetDisplayParts, isTypeParameterList, typeChecker, enclosingDeclaration, sourceFile)); - - if (argumentIndex !== 0) { - Debug.assertLessThan(argumentIndex, argumentCount); + }); +} +/* @internal */ +interface SignatureHelpItemInfo { + readonly isVariadic: boolean; + readonly parameters: SignatureHelpParameter[]; + readonly prefix: readonly SymbolDisplayPart[]; + readonly suffix: readonly SymbolDisplayPart[]; +} +/* @internal */ +function itemInfoForTypeParameters(candidateSignature: Signature, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItemInfo { + const typeParameters = (candidateSignature.target || candidateSignature).typeParameters; + const printer = createPrinter({ removeComments: true }); + const parameters = (typeParameters || emptyArray).map(t => createSignatureHelpParameterForTypeParameter(t, checker, enclosingDeclaration, sourceFile, printer)); + const parameterParts = mapToDisplayParts(writer => { + const thisParameter = candidateSignature.thisParameter ? [checker.symbolToParameterDeclaration(candidateSignature.thisParameter, enclosingDeclaration, signatureHelpNodeBuilderFlags)!] : []; + const params = createNodeArray([...thisParameter, ...checker.getExpandedParameters(candidateSignature).map(param => checker.symbolToParameterDeclaration(param, enclosingDeclaration, signatureHelpNodeBuilderFlags)!)]); + printer.writeList(ListFormat.CallExpressionArguments, params, sourceFile, writer); + }); + return { isVariadic: false, parameters, prefix: [punctuationPart(SyntaxKind.LessThanToken)], suffix: [punctuationPart(SyntaxKind.GreaterThanToken), ...parameterParts] }; +} +/* @internal */ +function itemInfoForParameters(candidateSignature: Signature, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItemInfo { + const isVariadic = checker.hasEffectiveRestParameter(candidateSignature); + const printer = createPrinter({ removeComments: true }); + const typeParameterParts = mapToDisplayParts(writer => { + if (candidateSignature.typeParameters && candidateSignature.typeParameters.length) { + const args = createNodeArray(candidateSignature.typeParameters.map(p => checker.typeParameterToDeclaration(p, enclosingDeclaration)!)); + printer.writeList(ListFormat.TypeParameters, args, sourceFile, writer); } - - const selectedItemIndex = candidates.indexOf(resolvedSignature); - Debug.assert(selectedItemIndex !== -1); // If candidates is non-empty it should always include bestSignature. We check for an empty candidates before calling this function. - - return { items, applicableSpan, selectedItemIndex, argumentIndex, argumentCount }; - } - - function createTypeHelpItems( - symbol: Symbol, - { argumentCount, argumentsSpan: applicableSpan, invocation, argumentIndex }: ArgumentListInfo, - sourceFile: SourceFile, - checker: TypeChecker - ): SignatureHelpItems | undefined { - const typeParameters = checker.getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol); - if (!typeParameters) return undefined; - const items = [getTypeHelpItem(symbol, typeParameters, checker, getEnclosingDeclarationFromInvocation(invocation), sourceFile)]; - return { items, applicableSpan, selectedItemIndex: 0, argumentIndex, argumentCount }; - } - - function getTypeHelpItem(symbol: Symbol, typeParameters: readonly TypeParameter[], checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItem { - const typeSymbolDisplay = symbolToDisplayParts(checker, symbol); - - const printer = createPrinter({ removeComments: true }); - const parameters = typeParameters.map(t => createSignatureHelpParameterForTypeParameter(t, checker, enclosingDeclaration, sourceFile, printer)); - - const documentation = symbol.getDocumentationComment(checker); - const tags = symbol.getJsDocTags(); - const prefixDisplayParts = [...typeSymbolDisplay, punctuationPart(SyntaxKind.LessThanToken)]; - return { isVariadic: false, prefixDisplayParts, suffixDisplayParts: [punctuationPart(SyntaxKind.GreaterThanToken)], separatorDisplayParts, parameters, documentation, tags }; - } - - const separatorDisplayParts: SymbolDisplayPart[] = [punctuationPart(SyntaxKind.CommaToken), spacePart()]; - - function getSignatureHelpItem(candidateSignature: Signature, callTargetDisplayParts: readonly SymbolDisplayPart[], isTypeParameterList: boolean, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItem { - const { isVariadic, parameters, prefix, suffix } = (isTypeParameterList ? itemInfoForTypeParameters : itemInfoForParameters)(candidateSignature, checker, enclosingDeclaration, sourceFile); - const prefixDisplayParts = [...callTargetDisplayParts, ...prefix]; - const suffixDisplayParts = [...suffix, ...returnTypeToDisplayParts(candidateSignature, enclosingDeclaration, checker)]; - const documentation = candidateSignature.getDocumentationComment(checker); - const tags = candidateSignature.getJsDocTags(); - return { isVariadic, prefixDisplayParts, suffixDisplayParts, separatorDisplayParts, parameters, documentation, tags }; - } - - function returnTypeToDisplayParts(candidateSignature: Signature, enclosingDeclaration: Node, checker: TypeChecker): readonly SymbolDisplayPart[] { - return mapToDisplayParts(writer => { - writer.writePunctuation(":"); - writer.writeSpace(" "); - const predicate = checker.getTypePredicateOfSignature(candidateSignature); - if (predicate) { - checker.writeTypePredicate(predicate, enclosingDeclaration, /*flags*/ undefined, writer); - } - else { - checker.writeType(checker.getReturnTypeOfSignature(candidateSignature), enclosingDeclaration, /*flags*/ undefined, writer); - } - }); - } - - interface SignatureHelpItemInfo { readonly isVariadic: boolean; readonly parameters: SignatureHelpParameter[]; readonly prefix: readonly SymbolDisplayPart[]; readonly suffix: readonly SymbolDisplayPart[]; } - - function itemInfoForTypeParameters(candidateSignature: Signature, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItemInfo { - const typeParameters = (candidateSignature.target || candidateSignature).typeParameters; - const printer = createPrinter({ removeComments: true }); - const parameters = (typeParameters || emptyArray).map(t => createSignatureHelpParameterForTypeParameter(t, checker, enclosingDeclaration, sourceFile, printer)); - const parameterParts = mapToDisplayParts(writer => { - const thisParameter = candidateSignature.thisParameter ? [checker.symbolToParameterDeclaration(candidateSignature.thisParameter, enclosingDeclaration, signatureHelpNodeBuilderFlags)!] : []; - const params = createNodeArray([...thisParameter, ...checker.getExpandedParameters(candidateSignature).map(param => checker.symbolToParameterDeclaration(param, enclosingDeclaration, signatureHelpNodeBuilderFlags)!)]); - printer.writeList(ListFormat.CallExpressionArguments, params, sourceFile, writer); - }); - return { isVariadic: false, parameters, prefix: [punctuationPart(SyntaxKind.LessThanToken)], suffix: [punctuationPart(SyntaxKind.GreaterThanToken), ...parameterParts] }; - } - - function itemInfoForParameters(candidateSignature: Signature, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile): SignatureHelpItemInfo { - const isVariadic = checker.hasEffectiveRestParameter(candidateSignature); - const printer = createPrinter({ removeComments: true }); - const typeParameterParts = mapToDisplayParts(writer => { - if (candidateSignature.typeParameters && candidateSignature.typeParameters.length) { - const args = createNodeArray(candidateSignature.typeParameters.map(p => checker.typeParameterToDeclaration(p, enclosingDeclaration)!)); - printer.writeList(ListFormat.TypeParameters, args, sourceFile, writer); - } - }); - const parameters = checker.getExpandedParameters(candidateSignature).map(p => createSignatureHelpParameterForParameter(p, checker, enclosingDeclaration, sourceFile, printer)); - return { isVariadic, parameters, prefix: [...typeParameterParts, punctuationPart(SyntaxKind.OpenParenToken)], suffix: [punctuationPart(SyntaxKind.CloseParenToken)] }; - } - - function createSignatureHelpParameterForParameter(parameter: Symbol, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile, printer: Printer): SignatureHelpParameter { - const displayParts = mapToDisplayParts(writer => { - const param = checker.symbolToParameterDeclaration(parameter, enclosingDeclaration, signatureHelpNodeBuilderFlags)!; - printer.writeNode(EmitHint.Unspecified, param, sourceFile, writer); - }); - const isOptional = checker.isOptionalParameter(parameter.valueDeclaration); - return { name: parameter.name, documentation: parameter.getDocumentationComment(checker), displayParts, isOptional }; - } - - function createSignatureHelpParameterForTypeParameter(typeParameter: TypeParameter, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile, printer: Printer): SignatureHelpParameter { - const displayParts = mapToDisplayParts(writer => { - const param = checker.typeParameterToDeclaration(typeParameter, enclosingDeclaration)!; - printer.writeNode(EmitHint.Unspecified, param, sourceFile, writer); - }); - return { name: typeParameter.symbol.name, documentation: typeParameter.symbol.getDocumentationComment(checker), displayParts, isOptional: false }; - } + }); + const parameters = checker.getExpandedParameters(candidateSignature).map(p => createSignatureHelpParameterForParameter(p, checker, enclosingDeclaration, sourceFile, printer)); + return { isVariadic, parameters, prefix: [...typeParameterParts, punctuationPart(SyntaxKind.OpenParenToken)], suffix: [punctuationPart(SyntaxKind.CloseParenToken)] }; +} +/* @internal */ +function createSignatureHelpParameterForParameter(parameter: Symbol, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile, printer: Printer): SignatureHelpParameter { + const displayParts = mapToDisplayParts(writer => { + const param = checker.symbolToParameterDeclaration(parameter, enclosingDeclaration, signatureHelpNodeBuilderFlags)!; + printer.writeNode(EmitHint.Unspecified, param, sourceFile, writer); + }); + const isOptional = checker.isOptionalParameter((parameter.valueDeclaration)); + return { name: parameter.name, documentation: parameter.getDocumentationComment(checker), displayParts, isOptional }; +} +/* @internal */ +function createSignatureHelpParameterForTypeParameter(typeParameter: TypeParameter, checker: TypeChecker, enclosingDeclaration: Node, sourceFile: SourceFile, printer: Printer): SignatureHelpParameter { + const displayParts = mapToDisplayParts(writer => { + const param = checker.typeParameterToDeclaration(typeParameter, enclosingDeclaration)!; + printer.writeNode(EmitHint.Unspecified, param, sourceFile, writer); + }); + return { name: typeParameter.symbol.name, documentation: typeParameter.symbol.getDocumentationComment(checker), displayParts, isOptional: false }; } diff --git a/src/services/smartSelection.ts b/src/services/smartSelection.ts index d7df7037e38c4..edae643112b1d 100644 --- a/src/services/smartSelection.ts +++ b/src/services/smartSelection.ts @@ -1,275 +1,253 @@ +import { SourceFile, SelectionRange, createTextSpanFromBounds, Node, isBlock, isTemplateSpan, isTemplateHead, isTemplateTail, isVariableDeclarationList, isVariableStatement, isSyntaxList, isVariableDeclaration, isTemplateMiddleOrTemplateTail, positionsAreOnSameLine, hasJSDocNodes, isNumber, isStringLiteral, isTemplateLiteral, textSpansEqual, textSpanIntersectsWithPosition, Debug, getTouchingPropertyName, or, isImportDeclaration, isImportEqualsDeclaration, isSourceFile, isMappedTypeNode, SyntaxKind, isPropertySignature, contains, isParameter, isBindingElement, findIndex, last, compact, SyntaxList, createNode } from "./ts"; /* @internal */ -namespace ts.SmartSelectionRange { - export function getSmartSelectionRange(pos: number, sourceFile: SourceFile): SelectionRange { - let selectionRange: SelectionRange = { - textSpan: createTextSpanFromBounds(sourceFile.getFullStart(), sourceFile.getEnd()) - }; - - let parentNode: Node = sourceFile; - outer: while (true) { - const children = getSelectionChildren(parentNode); - if (!children.length) break; - for (let i = 0; i < children.length; i++) { - const prevNode: Node | undefined = children[i - 1]; - const node: Node = children[i]; - const nextNode: Node | undefined = children[i + 1]; - if (node.getStart(sourceFile) > pos) { - break outer; - } - - if (positionShouldSnapToNode(sourceFile, pos, node)) { - // 1. Blocks are effectively redundant with SyntaxLists. - // 2. TemplateSpans, along with the SyntaxLists containing them, are a somewhat unintuitive grouping - // of things that should be considered independently. - // 3. A VariableStatement’s children are just a VaraiableDeclarationList and a semicolon. - // 4. A lone VariableDeclaration in a VaraibleDeclaration feels redundant with the VariableStatement. - // - // Dive in without pushing a selection range. - if (isBlock(node) - || isTemplateSpan(node) || isTemplateHead(node) || isTemplateTail(node) - || prevNode && isTemplateHead(prevNode) - || isVariableDeclarationList(node) && isVariableStatement(parentNode) - || isSyntaxList(node) && isVariableDeclarationList(parentNode) - || isVariableDeclaration(node) && isSyntaxList(parentNode) && children.length === 1) { - parentNode = node; - break; - } - - // Synthesize a stop for '${ ... }' since '${' and '}' actually belong to siblings. - if (isTemplateSpan(parentNode) && nextNode && isTemplateMiddleOrTemplateTail(nextNode)) { - const start = node.getFullStart() - "${".length; - const end = nextNode.getStart() + "}".length; - pushSelectionRange(start, end); - } - - // Blocks with braces, brackets, parens, or JSX tags on separate lines should be - // selected from open to close, including whitespace but not including the braces/etc. themselves. - const isBetweenMultiLineBookends = isSyntaxList(node) - && isListOpener(prevNode) - && isListCloser(nextNode) - && !positionsAreOnSameLine(prevNode.getStart(), nextNode.getStart(), sourceFile); - const jsDocCommentStart = hasJSDocNodes(node) && node.jsDoc![0].getStart(); - const start = isBetweenMultiLineBookends ? prevNode.getEnd() : node.getStart(); - const end = isBetweenMultiLineBookends ? nextNode.getStart() : node.getEnd(); - if (isNumber(jsDocCommentStart)) { - pushSelectionRange(jsDocCommentStart, end); - } - pushSelectionRange(start, end); - - // String literals should have a stop both inside and outside their quotes. - if (isStringLiteral(node) || isTemplateLiteral(node)) { - pushSelectionRange(start + 1, end - 1); - } - +export function getSmartSelectionRange(pos: number, sourceFile: SourceFile): SelectionRange { + let selectionRange: SelectionRange = { + textSpan: createTextSpanFromBounds(sourceFile.getFullStart(), sourceFile.getEnd()) + }; + let parentNode: Node = sourceFile; + outer: while (true) { + const children = getSelectionChildren(parentNode); + if (!children.length) + break; + for (let i = 0; i < children.length; i++) { + const prevNode: Node | undefined = children[i - 1]; + const node: Node = children[i]; + const nextNode: Node | undefined = children[i + 1]; + if (node.getStart(sourceFile) > pos) { + break outer; + } + if (positionShouldSnapToNode(sourceFile, pos, node)) { + // 1. Blocks are effectively redundant with SyntaxLists. + // 2. TemplateSpans, along with the SyntaxLists containing them, are a somewhat unintuitive grouping + // of things that should be considered independently. + // 3. A VariableStatement’s children are just a VaraiableDeclarationList and a semicolon. + // 4. A lone VariableDeclaration in a VaraibleDeclaration feels redundant with the VariableStatement. + // + // Dive in without pushing a selection range. + if (isBlock(node) + || isTemplateSpan(node) || isTemplateHead(node) || isTemplateTail(node) + || prevNode && isTemplateHead(prevNode) + || isVariableDeclarationList(node) && isVariableStatement(parentNode) + || isSyntaxList(node) && isVariableDeclarationList(parentNode) + || isVariableDeclaration(node) && isSyntaxList(parentNode) && children.length === 1) { parentNode = node; break; } - - // If we made it to the end of the for loop, we’re done. - // In practice, I’ve only seen this happen at the very end - // of a SourceFile. - if (i === children.length - 1) { - break outer; + // Synthesize a stop for '${ ... }' since '${' and '}' actually belong to siblings. + if (isTemplateSpan(parentNode) && nextNode && isTemplateMiddleOrTemplateTail(nextNode)) { + const start = node.getFullStart() - "${".length; + const end = nextNode.getStart() + "}".length; + pushSelectionRange(start, end); } - } - } - - return selectionRange; - - function pushSelectionRange(start: number, end: number): void { - // Skip empty ranges - if (start !== end) { - const textSpan = createTextSpanFromBounds(start, end); - if (!selectionRange || ( - // Skip ranges that are identical to the parent - !textSpansEqual(textSpan, selectionRange.textSpan) && - // Skip ranges that don’t contain the original position - textSpanIntersectsWithPosition(textSpan, pos) - )) { - selectionRange = { textSpan, ...selectionRange && { parent: selectionRange } }; + // Blocks with braces, brackets, parens, or JSX tags on separate lines should be + // selected from open to close, including whitespace but not including the braces/etc. themselves. + const isBetweenMultiLineBookends = isSyntaxList(node) + && isListOpener(prevNode) + && isListCloser(nextNode) + && !positionsAreOnSameLine(prevNode.getStart(), nextNode.getStart(), sourceFile); + const jsDocCommentStart = hasJSDocNodes(node) && node.jsDoc![0].getStart(); + const start = isBetweenMultiLineBookends ? prevNode.getEnd() : node.getStart(); + const end = isBetweenMultiLineBookends ? nextNode.getStart() : node.getEnd(); + if (isNumber(jsDocCommentStart)) { + pushSelectionRange(jsDocCommentStart, end); } + pushSelectionRange(start, end); + // String literals should have a stop both inside and outside their quotes. + if (isStringLiteral(node) || isTemplateLiteral(node)) { + pushSelectionRange(start + 1, end - 1); + } + parentNode = node; + break; + } + // If we made it to the end of the for loop, we’re done. + // In practice, I’ve only seen this happen at the very end + // of a SourceFile. + if (i === children.length - 1) { + break outer; } } } - - /** - * Like `ts.positionBelongsToNode`, except positions immediately after nodes - * count too, unless that position belongs to the next node. In effect, makes - * selections able to snap to preceding tokens when the cursor is on the tail - * end of them with only whitespace ahead. - * @param sourceFile The source file containing the nodes. - * @param pos The position to check. - * @param node The candidate node to snap to. - */ - function positionShouldSnapToNode(sourceFile: SourceFile, pos: number, node: Node) { - // Can’t use 'ts.positionBelongsToNode()' here because it cleverly accounts - // for missing nodes, which can’t really be considered when deciding what - // to select. - Debug.assert(node.pos <= pos); - if (pos < node.end) { - return true; - } - const nodeEnd = node.getEnd(); - if (nodeEnd === pos) { - return getTouchingPropertyName(sourceFile, pos).pos < node.end; + return selectionRange; + function pushSelectionRange(start: number, end: number): void { + // Skip empty ranges + if (start !== end) { + const textSpan = createTextSpanFromBounds(start, end); + if (!selectionRange || ( + // Skip ranges that are identical to the parent + !textSpansEqual(textSpan, selectionRange.textSpan) && + // Skip ranges that don’t contain the original position + textSpanIntersectsWithPosition(textSpan, pos))) { + selectionRange = { textSpan, ...selectionRange && { parent: selectionRange } }; + } } - return false; } - - const isImport = or(isImportDeclaration, isImportEqualsDeclaration); - - /** - * Gets the children of a node to be considered for selection ranging, - * transforming them into an artificial tree according to their intuitive - * grouping where no grouping actually exists in the parse tree. For example, - * top-level imports are grouped into their own SyntaxList so they can be - * selected all together, even though in the AST they’re just siblings of each - * other as well as of other top-level statements and declarations. - */ - function getSelectionChildren(node: Node): readonly Node[] { - // Group top-level imports - if (isSourceFile(node)) { - return groupChildren(node.getChildAt(0).getChildren(), isImport); - } - - // Mapped types _look_ like ObjectTypes with a single member, - // but in fact don’t contain a SyntaxList or a node containing - // the “key/value” pair like ObjectTypes do, but it seems intuitive - // that the selection would snap to those points. The philosophy - // of choosing a selection range is not so much about what the - // syntax currently _is_ as what the syntax might easily become - // if the user is making a selection; e.g., we synthesize a selection - // around the “key/value” pair not because there’s a node there, but - // because it allows the mapped type to become an object type with a - // few keystrokes. - if (isMappedTypeNode(node)) { - const [openBraceToken, ...children] = node.getChildren(); - const closeBraceToken = Debug.checkDefined(children.pop()); - Debug.assertEqual(openBraceToken.kind, SyntaxKind.OpenBraceToken); - Debug.assertEqual(closeBraceToken.kind, SyntaxKind.CloseBraceToken); - // Group `-/+readonly` and `-/+?` - const groupedWithPlusMinusTokens = groupChildren(children, child => - child === node.readonlyToken || child.kind === SyntaxKind.ReadonlyKeyword || - child === node.questionToken || child.kind === SyntaxKind.QuestionToken); - // Group type parameter with surrounding brackets - const groupedWithBrackets = groupChildren(groupedWithPlusMinusTokens, ({ kind }) => - kind === SyntaxKind.OpenBracketToken || - kind === SyntaxKind.TypeParameter || - kind === SyntaxKind.CloseBracketToken - ); - return [ - openBraceToken, - // Pivot on `:` - createSyntaxList(splitChildren(groupedWithBrackets, ({ kind }) => kind === SyntaxKind.ColonToken)), - closeBraceToken, - ]; - } - - // Group modifiers and property name, then pivot on `:`. - if (isPropertySignature(node)) { - const children = groupChildren(node.getChildren(), child => - child === node.name || contains(node.modifiers, child)); - return splitChildren(children, ({ kind }) => kind === SyntaxKind.ColonToken); - } - - // Group the parameter name with its `...`, then that group with its `?`, then pivot on `=`. - if (isParameter(node)) { - const groupedDotDotDotAndName = groupChildren(node.getChildren(), child => - child === node.dotDotDotToken || child === node.name); - const groupedWithQuestionToken = groupChildren(groupedDotDotDotAndName, child => - child === groupedDotDotDotAndName[0] || child === node.questionToken); - return splitChildren(groupedWithQuestionToken, ({ kind }) => kind === SyntaxKind.EqualsToken); - } - - // Pivot on '=' - if (isBindingElement(node)) { - return splitChildren(node.getChildren(), ({ kind }) => kind === SyntaxKind.EqualsToken); - } - - return node.getChildren(); +} +/** + * Like `ts.positionBelongsToNode`, except positions immediately after nodes + * count too, unless that position belongs to the next node. In effect, makes + * selections able to snap to preceding tokens when the cursor is on the tail + * end of them with only whitespace ahead. + * @param sourceFile The source file containing the nodes. + * @param pos The position to check. + * @param node The candidate node to snap to. + */ +/* @internal */ +function positionShouldSnapToNode(sourceFile: SourceFile, pos: number, node: Node) { + // Can’t use 'ts.positionBelongsToNode()' here because it cleverly accounts + // for missing nodes, which can’t really be considered when deciding what + // to select. + Debug.assert(node.pos <= pos); + if (pos < node.end) { + return true; } - - /** - * Groups sibling nodes together into their own SyntaxList if they - * a) are adjacent, AND b) match a predicate function. - */ - function groupChildren(children: Node[], groupOn: (child: Node) => boolean): Node[] { - const result: Node[] = []; - let group: Node[] | undefined; - for (const child of children) { - if (groupOn(child)) { - group = group || []; - group.push(child); - } - else { - if (group) { - result.push(createSyntaxList(group)); - group = undefined; - } - result.push(child); - } - } - if (group) { - result.push(createSyntaxList(group)); - } - - return result; + const nodeEnd = node.getEnd(); + if (nodeEnd === pos) { + return getTouchingPropertyName(sourceFile, pos).pos < node.end; + } + return false; +} +/* @internal */ +const isImport = or(isImportDeclaration, isImportEqualsDeclaration); +/** + * Gets the children of a node to be considered for selection ranging, + * transforming them into an artificial tree according to their intuitive + * grouping where no grouping actually exists in the parse tree. For example, + * top-level imports are grouped into their own SyntaxList so they can be + * selected all together, even though in the AST they’re just siblings of each + * other as well as of other top-level statements and declarations. + */ +/* @internal */ +function getSelectionChildren(node: Node): readonly Node[] { + // Group top-level imports + if (isSourceFile(node)) { + return groupChildren(node.getChildAt(0).getChildren(), isImport); } - - /** - * Splits sibling nodes into up to four partitions: - * 1) everything left of the first node matched by `pivotOn`, - * 2) the first node matched by `pivotOn`, - * 3) everything right of the first node matched by `pivotOn`, - * 4) a trailing semicolon, if `separateTrailingSemicolon` is enabled. - * The left and right groups, if not empty, will each be grouped into their own containing SyntaxList. - * @param children The sibling nodes to split. - * @param pivotOn The predicate function to match the node to be the pivot. The first node that matches - * the predicate will be used; any others that may match will be included into the right-hand group. - * @param separateTrailingSemicolon If the last token is a semicolon, it will be returned as a separate - * child rather than be included in the right-hand group. - */ - function splitChildren(children: Node[], pivotOn: (child: Node) => boolean, separateTrailingSemicolon = true): Node[] { - if (children.length < 2) { - return children; + // Mapped types _look_ like ObjectTypes with a single member, + // but in fact don’t contain a SyntaxList or a node containing + // the “key/value” pair like ObjectTypes do, but it seems intuitive + // that the selection would snap to those points. The philosophy + // of choosing a selection range is not so much about what the + // syntax currently _is_ as what the syntax might easily become + // if the user is making a selection; e.g., we synthesize a selection + // around the “key/value” pair not because there’s a node there, but + // because it allows the mapped type to become an object type with a + // few keystrokes. + if (isMappedTypeNode(node)) { + const [openBraceToken, ...children] = node.getChildren(); + const closeBraceToken = Debug.checkDefined(children.pop()); + Debug.assertEqual(openBraceToken.kind, SyntaxKind.OpenBraceToken); + Debug.assertEqual(closeBraceToken.kind, SyntaxKind.CloseBraceToken); + // Group `-/+readonly` and `-/+?` + const groupedWithPlusMinusTokens = groupChildren(children, child => child === node.readonlyToken || child.kind === SyntaxKind.ReadonlyKeyword || + child === node.questionToken || child.kind === SyntaxKind.QuestionToken); + // Group type parameter with surrounding brackets + const groupedWithBrackets = groupChildren(groupedWithPlusMinusTokens, ({ kind }) => kind === SyntaxKind.OpenBracketToken || + kind === SyntaxKind.TypeParameter || + kind === SyntaxKind.CloseBracketToken); + return [ + openBraceToken, + // Pivot on `:` + createSyntaxList(splitChildren(groupedWithBrackets, ({ kind }) => kind === SyntaxKind.ColonToken)), + closeBraceToken, + ]; + } + // Group modifiers and property name, then pivot on `:`. + if (isPropertySignature(node)) { + const children = groupChildren(node.getChildren(), child => child === node.name || contains(node.modifiers, child)); + return splitChildren(children, ({ kind }) => kind === SyntaxKind.ColonToken); + } + // Group the parameter name with its `...`, then that group with its `?`, then pivot on `=`. + if (isParameter(node)) { + const groupedDotDotDotAndName = groupChildren(node.getChildren(), child => child === node.dotDotDotToken || child === node.name); + const groupedWithQuestionToken = groupChildren(groupedDotDotDotAndName, child => child === groupedDotDotDotAndName[0] || child === node.questionToken); + return splitChildren(groupedWithQuestionToken, ({ kind }) => kind === SyntaxKind.EqualsToken); + } + // Pivot on '=' + if (isBindingElement(node)) { + return splitChildren(node.getChildren(), ({ kind }) => kind === SyntaxKind.EqualsToken); + } + return node.getChildren(); +} +/** + * Groups sibling nodes together into their own SyntaxList if they + * a) are adjacent, AND b) match a predicate function. + */ +/* @internal */ +function groupChildren(children: Node[], groupOn: (child: Node) => boolean): Node[] { + const result: Node[] = []; + let group: Node[] | undefined; + for (const child of children) { + if (groupOn(child)) { + group = group || []; + group.push(child); } - const splitTokenIndex = findIndex(children, pivotOn); - if (splitTokenIndex === -1) { - return children; + else { + if (group) { + result.push(createSyntaxList(group)); + group = undefined; + } + result.push(child); } - const leftChildren = children.slice(0, splitTokenIndex); - const splitToken = children[splitTokenIndex]; - const lastToken = last(children); - const separateLastToken = separateTrailingSemicolon && lastToken.kind === SyntaxKind.SemicolonToken; - const rightChildren = children.slice(splitTokenIndex + 1, separateLastToken ? children.length - 1 : undefined); - const result = compact([ - leftChildren.length ? createSyntaxList(leftChildren) : undefined, - splitToken, - rightChildren.length ? createSyntaxList(rightChildren) : undefined, - ]); - return separateLastToken ? result.concat(lastToken) : result; } - - function createSyntaxList(children: Node[]): SyntaxList { - Debug.assertGreaterThanOrEqual(children.length, 1); - const syntaxList = createNode(SyntaxKind.SyntaxList, children[0].pos, last(children).end) as SyntaxList; - syntaxList._children = children; - return syntaxList; + if (group) { + result.push(createSyntaxList(group)); } - - function isListOpener(token: Node | undefined): token is Node { - const kind = token && token.kind; - return kind === SyntaxKind.OpenBraceToken - || kind === SyntaxKind.OpenBracketToken - || kind === SyntaxKind.OpenParenToken - || kind === SyntaxKind.JsxOpeningElement; + return result; +} +/** + * Splits sibling nodes into up to four partitions: + * 1) everything left of the first node matched by `pivotOn`, + * 2) the first node matched by `pivotOn`, + * 3) everything right of the first node matched by `pivotOn`, + * 4) a trailing semicolon, if `separateTrailingSemicolon` is enabled. + * The left and right groups, if not empty, will each be grouped into their own containing SyntaxList. + * @param children The sibling nodes to split. + * @param pivotOn The predicate function to match the node to be the pivot. The first node that matches + * the predicate will be used; any others that may match will be included into the right-hand group. + * @param separateTrailingSemicolon If the last token is a semicolon, it will be returned as a separate + * child rather than be included in the right-hand group. + */ +/* @internal */ +function splitChildren(children: Node[], pivotOn: (child: Node) => boolean, separateTrailingSemicolon = true): Node[] { + if (children.length < 2) { + return children; } - - function isListCloser(token: Node | undefined): token is Node { - const kind = token && token.kind; - return kind === SyntaxKind.CloseBraceToken - || kind === SyntaxKind.CloseBracketToken - || kind === SyntaxKind.CloseParenToken - || kind === SyntaxKind.JsxClosingElement; + const splitTokenIndex = findIndex(children, pivotOn); + if (splitTokenIndex === -1) { + return children; } + const leftChildren = children.slice(0, splitTokenIndex); + const splitToken = children[splitTokenIndex]; + const lastToken = last(children); + const separateLastToken = separateTrailingSemicolon && lastToken.kind === SyntaxKind.SemicolonToken; + const rightChildren = children.slice(splitTokenIndex + 1, separateLastToken ? children.length - 1 : undefined); + const result = compact([ + leftChildren.length ? createSyntaxList(leftChildren) : undefined, + splitToken, + rightChildren.length ? createSyntaxList(rightChildren) : undefined, + ]); + return separateLastToken ? result.concat(lastToken) : result; +} +/* @internal */ +function createSyntaxList(children: Node[]): SyntaxList { + Debug.assertGreaterThanOrEqual(children.length, 1); + const syntaxList = (createNode(SyntaxKind.SyntaxList, children[0].pos, last(children).end) as SyntaxList); + syntaxList._children = children; + return syntaxList; +} +/* @internal */ +function isListOpener(token: Node | undefined): token is Node { + const kind = token && token.kind; + return kind === SyntaxKind.OpenBraceToken + || kind === SyntaxKind.OpenBracketToken + || kind === SyntaxKind.OpenParenToken + || kind === SyntaxKind.JsxOpeningElement; +} +/* @internal */ +function isListCloser(token: Node | undefined): token is Node { + const kind = token && token.kind; + return kind === SyntaxKind.CloseBraceToken + || kind === SyntaxKind.CloseBracketToken + || kind === SyntaxKind.CloseParenToken + || kind === SyntaxKind.JsxClosingElement; } diff --git a/src/services/sourcemaps.ts b/src/services/sourcemaps.ts index b582ba0065d86..5d4d318dae49f 100644 --- a/src/services/sourcemaps.ts +++ b/src/services/sourcemaps.ts @@ -1,199 +1,177 @@ +import { LineAndCharacter, DocumentPosition, Program, SourceFileLike, DocumentPositionMapper, createGetCanonicalFileName, createMap, getLineInfo, getLineStarts, identitySourceMapConsumer, isDeclarationFileName, removeFileExtension, Extension, getDeclarationEmitOutputFilePathWorker, DocumentPositionMapperHost, LineInfo, tryGetSourceMappingURL, base64decode, sys, getNormalizedAbsolutePath, getDirectoryPath, isString, tryParseRawSourceMap, createDocumentPositionMapper, computeLineAndCharacterOfPosition } from "./ts"; +import * as ts from "./ts"; /* @internal */ -namespace ts { - const base64UrlRegExp = /^data:(?:application\/json(?:;charset=[uU][tT][fF]-8);base64,([A-Za-z0-9+\/=]+)$)?/; - - export interface SourceMapper { - toLineColumnOffset(fileName: string, position: number): LineAndCharacter; - tryGetSourcePosition(info: DocumentPosition): DocumentPosition | undefined; - tryGetGeneratedPosition(info: DocumentPosition): DocumentPosition | undefined; - clearCache(): void; - } - - export interface SourceMapperHost { - useCaseSensitiveFileNames(): boolean; - getCurrentDirectory(): string; - getProgram(): Program | undefined; - fileExists?(path: string): boolean; - readFile?(path: string, encoding?: string): string | undefined; - getSourceFileLike?(fileName: string): SourceFileLike | undefined; - getDocumentPositionMapper?(generatedFileName: string, sourceFileName?: string): DocumentPositionMapper | undefined; - log(s: string): void; +const base64UrlRegExp = /^data:(?:application\/json(?:;charset=[uU][tT][fF]-8);base64,([A-Za-z0-9+\/=]+)$)?/; +/* @internal */ +export interface SourceMapper { + toLineColumnOffset(fileName: string, position: number): LineAndCharacter; + tryGetSourcePosition(info: DocumentPosition): DocumentPosition | undefined; + tryGetGeneratedPosition(info: DocumentPosition): DocumentPosition | undefined; + clearCache(): void; +} +/* @internal */ +export interface SourceMapperHost { + useCaseSensitiveFileNames(): boolean; + getCurrentDirectory(): string; + getProgram(): Program | undefined; + fileExists?(path: string): boolean; + readFile?(path: string, encoding?: string): string | undefined; + getSourceFileLike?(fileName: string): SourceFileLike | undefined; + getDocumentPositionMapper?(generatedFileName: string, sourceFileName?: string): DocumentPositionMapper | undefined; + log(s: string): void; +} +/* @internal */ +export function getSourceMapper(host: SourceMapperHost): SourceMapper { + const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames()); + const currentDirectory = host.getCurrentDirectory(); + const sourceFileLike = createMap(); + const documentPositionMappers = createMap(); + return { tryGetSourcePosition, tryGetGeneratedPosition, toLineColumnOffset, clearCache }; + function toPath(fileName: string) { + return ts.toPath(fileName, currentDirectory, getCanonicalFileName); } - - export function getSourceMapper(host: SourceMapperHost): SourceMapper { - const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames()); - const currentDirectory = host.getCurrentDirectory(); - const sourceFileLike = createMap(); - const documentPositionMappers = createMap(); - return { tryGetSourcePosition, tryGetGeneratedPosition, toLineColumnOffset, clearCache }; - - function toPath(fileName: string) { - return ts.toPath(fileName, currentDirectory, getCanonicalFileName); - } - - function getDocumentPositionMapper(generatedFileName: string, sourceFileName?: string) { - const path = toPath(generatedFileName); - const value = documentPositionMappers.get(path); - if (value) return value; - - let mapper: DocumentPositionMapper | undefined; - if (host.getDocumentPositionMapper) { - mapper = host.getDocumentPositionMapper(generatedFileName, sourceFileName); - } - else if (host.readFile) { - const file = getSourceFileLike(generatedFileName); - mapper = file && ts.getDocumentPositionMapper( - { getSourceFileLike, getCanonicalFileName, log: s => host.log(s) }, - generatedFileName, - getLineInfo(file.text, getLineStarts(file)), - f => !host.fileExists || host.fileExists(f) ? host.readFile!(f) : undefined - ); - } - documentPositionMappers.set(path, mapper || identitySourceMapConsumer); - return mapper || identitySourceMapConsumer; - } - - function tryGetSourcePosition(info: DocumentPosition): DocumentPosition | undefined { - if (!isDeclarationFileName(info.fileName)) return undefined; - - const file = getSourceFile(info.fileName); - if (!file) return undefined; - - const newLoc = getDocumentPositionMapper(info.fileName).getSourcePosition(info); - return !newLoc || newLoc === info ? undefined : tryGetSourcePosition(newLoc) || newLoc; - } - - function tryGetGeneratedPosition(info: DocumentPosition): DocumentPosition | undefined { - if (isDeclarationFileName(info.fileName)) return undefined; - - const sourceFile = getSourceFile(info.fileName); - if (!sourceFile) return undefined; - - const program = host.getProgram()!; - // If this is source file of project reference source (instead of redirect) there is no generated position - if (program.isSourceOfProjectReferenceRedirect(sourceFile.fileName)) { - return undefined; - } - - const options = program.getCompilerOptions(); - const outPath = options.outFile || options.out; - - const declarationPath = outPath ? - removeFileExtension(outPath) + Extension.Dts : - getDeclarationEmitOutputFilePathWorker(info.fileName, program.getCompilerOptions(), currentDirectory, program.getCommonSourceDirectory(), getCanonicalFileName); - if (declarationPath === undefined) return undefined; - - const newLoc = getDocumentPositionMapper(declarationPath, info.fileName).getGeneratedPosition(info); - return newLoc === info ? undefined : newLoc; + function getDocumentPositionMapper(generatedFileName: string, sourceFileName?: string) { + const path = toPath(generatedFileName); + const value = documentPositionMappers.get(path); + if (value) + return value; + let mapper: DocumentPositionMapper | undefined; + if (host.getDocumentPositionMapper) { + mapper = host.getDocumentPositionMapper(generatedFileName, sourceFileName); } - - function getSourceFile(fileName: string) { - const program = host.getProgram(); - if (!program) return undefined; - - const path = toPath(fileName); - // file returned here could be .d.ts when asked for .ts file if projectReferences and module resolution created this source file - const file = program.getSourceFileByPath(path); - return file && file.resolvedPath === path ? file : undefined; + else if (host.readFile) { + const file = getSourceFileLike(generatedFileName); + mapper = file && ts.getDocumentPositionMapper({ getSourceFileLike, getCanonicalFileName, log: s => host.log(s) }, generatedFileName, getLineInfo(file.text, getLineStarts(file)), f => !host.fileExists || host.fileExists(f) ? host.readFile!(f) : undefined); } - - function getOrCreateSourceFileLike(fileName: string): SourceFileLike | undefined { - const path = toPath(fileName); - const fileFromCache = sourceFileLike.get(path); - if (fileFromCache !== undefined) return fileFromCache ? fileFromCache : undefined; - - if (!host.readFile || host.fileExists && !host.fileExists(path)) { - sourceFileLike.set(path, false); - return undefined; - } - - // And failing that, check the disk - const text = host.readFile(path); - const file = text ? createSourceFileLike(text) : false; - sourceFileLike.set(path, file); - return file ? file : undefined; - } - - // This can be called from source mapper in either source program or program that includes generated file - function getSourceFileLike(fileName: string) { - return !host.getSourceFileLike ? - getSourceFile(fileName) || getOrCreateSourceFileLike(fileName) : - host.getSourceFileLike(fileName); - } - - function toLineColumnOffset(fileName: string, position: number): LineAndCharacter { - const file = getSourceFileLike(fileName)!; // TODO: GH#18217 - return file.getLineAndCharacterOfPosition(position); + documentPositionMappers.set(path, mapper || identitySourceMapConsumer); + return mapper || identitySourceMapConsumer; + } + function tryGetSourcePosition(info: DocumentPosition): DocumentPosition | undefined { + if (!isDeclarationFileName(info.fileName)) + return undefined; + const file = getSourceFile(info.fileName); + if (!file) + return undefined; + const newLoc = getDocumentPositionMapper(info.fileName).getSourcePosition(info); + return !newLoc || newLoc === info ? undefined : tryGetSourcePosition(newLoc) || newLoc; + } + function tryGetGeneratedPosition(info: DocumentPosition): DocumentPosition | undefined { + if (isDeclarationFileName(info.fileName)) + return undefined; + const sourceFile = getSourceFile(info.fileName); + if (!sourceFile) + return undefined; + const program = host.getProgram()!; + // If this is source file of project reference source (instead of redirect) there is no generated position + if (program.isSourceOfProjectReferenceRedirect(sourceFile.fileName)) { + return undefined; } - - function clearCache(): void { - sourceFileLike.clear(); - documentPositionMappers.clear(); + const options = program.getCompilerOptions(); + const outPath = options.outFile || options.out; + const declarationPath = outPath ? + removeFileExtension(outPath) + Extension.Dts : + getDeclarationEmitOutputFilePathWorker(info.fileName, program.getCompilerOptions(), currentDirectory, program.getCommonSourceDirectory(), getCanonicalFileName); + if (declarationPath === undefined) + return undefined; + const newLoc = getDocumentPositionMapper(declarationPath, info.fileName).getGeneratedPosition(info); + return newLoc === info ? undefined : newLoc; + } + function getSourceFile(fileName: string) { + const program = host.getProgram(); + if (!program) + return undefined; + const path = toPath(fileName); + // file returned here could be .d.ts when asked for .ts file if projectReferences and module resolution created this source file + const file = program.getSourceFileByPath(path); + return file && file.resolvedPath === path ? file : undefined; + } + function getOrCreateSourceFileLike(fileName: string): SourceFileLike | undefined { + const path = toPath(fileName); + const fileFromCache = sourceFileLike.get(path); + if (fileFromCache !== undefined) + return fileFromCache ? fileFromCache : undefined; + if (!host.readFile || host.fileExists && !host.fileExists(path)) { + sourceFileLike.set(path, false); + return undefined; } + // And failing that, check the disk + const text = host.readFile(path); + const file = text ? createSourceFileLike(text) : false; + sourceFileLike.set(path, file); + return file ? file : undefined; + } + // This can be called from source mapper in either source program or program that includes generated file + function getSourceFileLike(fileName: string) { + return !host.getSourceFileLike ? + getSourceFile(fileName) || getOrCreateSourceFileLike(fileName) : + host.getSourceFileLike(fileName); + } + function toLineColumnOffset(fileName: string, position: number): LineAndCharacter { + const file = getSourceFileLike(fileName)!; // TODO: GH#18217 + return file.getLineAndCharacterOfPosition(position); + } + function clearCache(): void { + sourceFileLike.clear(); + documentPositionMappers.clear(); } - - /** - * string | undefined to contents of map file to create DocumentPositionMapper from it - * DocumentPositionMapper | false to give back cached DocumentPositionMapper - */ - export type ReadMapFile = (mapFileName: string, mapFileNameFromDts: string | undefined) => string | undefined | DocumentPositionMapper | false; - - export function getDocumentPositionMapper( - host: DocumentPositionMapperHost, - generatedFileName: string, - generatedFileLineInfo: LineInfo, - readMapFile: ReadMapFile) { - let mapFileName = tryGetSourceMappingURL(generatedFileLineInfo); - if (mapFileName) { - const match = base64UrlRegExp.exec(mapFileName); - if (match) { - if (match[1]) { - const base64Object = match[1]; - return convertDocumentToSourceMapper(host, base64decode(sys, base64Object), generatedFileName); - } - // Not a data URL we can parse, skip it - mapFileName = undefined; +} +/** + * string | undefined to contents of map file to create DocumentPositionMapper from it + * DocumentPositionMapper | false to give back cached DocumentPositionMapper + */ +/* @internal */ +export type ReadMapFile = (mapFileName: string, mapFileNameFromDts: string | undefined) => string | undefined | DocumentPositionMapper | false; +/* @internal */ +export function getDocumentPositionMapper(host: DocumentPositionMapperHost, generatedFileName: string, generatedFileLineInfo: LineInfo, readMapFile: ReadMapFile) { + let mapFileName = tryGetSourceMappingURL(generatedFileLineInfo); + if (mapFileName) { + const match = base64UrlRegExp.exec(mapFileName); + if (match) { + if (match[1]) { + const base64Object = match[1]; + return convertDocumentToSourceMapper(host, base64decode(sys, base64Object), generatedFileName); } + // Not a data URL we can parse, skip it + mapFileName = undefined; } - const possibleMapLocations: string[] = []; - if (mapFileName) { - possibleMapLocations.push(mapFileName); + } + const possibleMapLocations: string[] = []; + if (mapFileName) { + possibleMapLocations.push(mapFileName); + } + possibleMapLocations.push(generatedFileName + ".map"); + const originalMapFileName = mapFileName && getNormalizedAbsolutePath(mapFileName, getDirectoryPath(generatedFileName)); + for (const location of possibleMapLocations) { + const mapFileName = getNormalizedAbsolutePath(location, getDirectoryPath(generatedFileName)); + const mapFileContents = readMapFile(mapFileName, originalMapFileName); + if (isString(mapFileContents)) { + return convertDocumentToSourceMapper(host, mapFileContents, mapFileName); } - possibleMapLocations.push(generatedFileName + ".map"); - const originalMapFileName = mapFileName && getNormalizedAbsolutePath(mapFileName, getDirectoryPath(generatedFileName)); - for (const location of possibleMapLocations) { - const mapFileName = getNormalizedAbsolutePath(location, getDirectoryPath(generatedFileName)); - const mapFileContents = readMapFile(mapFileName, originalMapFileName); - if (isString(mapFileContents)) { - return convertDocumentToSourceMapper(host, mapFileContents, mapFileName); - } - if (mapFileContents !== undefined) { - return mapFileContents || undefined; - } + if (mapFileContents !== undefined) { + return mapFileContents || undefined; } + } + return undefined; +} +/* @internal */ +function convertDocumentToSourceMapper(host: DocumentPositionMapperHost, contents: string, mapFileName: string) { + const map = tryParseRawSourceMap(contents); + if (!map || !map.sources || !map.file || !map.mappings) { + // obviously invalid map return undefined; } - - function convertDocumentToSourceMapper(host: DocumentPositionMapperHost, contents: string, mapFileName: string) { - const map = tryParseRawSourceMap(contents); - if (!map || !map.sources || !map.file || !map.mappings) { - // obviously invalid map - return undefined; + // Dont support sourcemaps that contain inlined sources + if (map.sourcesContent && map.sourcesContent.some(isString)) + return undefined; + return createDocumentPositionMapper(host, map, mapFileName); +} +/* @internal */ +function createSourceFileLike(text: string, lineMap?: SourceFileLike["lineMap"]): SourceFileLike { + return { + text, + lineMap, + getLineAndCharacterOfPosition(pos: number) { + return computeLineAndCharacterOfPosition(getLineStarts(this), pos); } - - // Dont support sourcemaps that contain inlined sources - if (map.sourcesContent && map.sourcesContent.some(isString)) return undefined; - - return createDocumentPositionMapper(host, map, mapFileName); - } - - function createSourceFileLike(text: string, lineMap?: SourceFileLike["lineMap"]): SourceFileLike { - return { - text, - lineMap, - getLineAndCharacterOfPosition(pos: number) { - return computeLineAndCharacterOfPosition(getLineStarts(this), pos); - } - }; - } + }; } diff --git a/src/services/stringCompletions.ts b/src/services/stringCompletions.ts index 313b37430e2ed..ffafd856e9d31 100644 --- a/src/services/stringCompletions.ts +++ b/src/services/stringCompletions.ts @@ -1,689 +1,649 @@ +import { SourceFile, Node, TypeChecker, CompilerOptions, LanguageServiceHost, UserPreferences, CompletionInfo, isInReferenceComment, isInString, isStringLiteralLike, CompletionEntry, ScriptTarget, ScriptElementKindModifier, ScriptElementKind, Debug, CancellationToken, CompletionEntryDetails, find, textPart, Extension, Symbol, StringLiteralType, StringLiteralLike, SyntaxKind, LiteralTypeNode, IndexedAccessTypeNode, isTypeReferenceNode, UnionTypeNode, contains, isObjectLiteralExpression, PropertyAssignment, ElementAccessExpression, isRequireCall, isImportCall, getContextualTypeFromParent, mapDefined, isLiteralTypeNode, isStringLiteral, createMap, Signature, flatMap, signatureHasRestParameter, TypeFlags, Type, isPrivateIdentifierPropertyDeclaration, hasIndexSignature, emptyArray, skipConstraint, addToSeen, TextSpan, LiteralExpression, normalizeSlashes, getDirectoryPath, isRootedDiskPath, isUrl, Path, getSupportedExtensions, getEmitModuleResolutionKind, ModuleResolutionKind, normalizePath, combinePaths, firstDefined, containsPath, deduplicate, equateStringsCaseSensitive, compareStringsCaseSensitive, hasTrailingDirectorySeparator, directorySeparator, ensureTrailingDirectorySeparator, resolvePath, tryDirectoryExists, tryReadDirectory, comparePaths, Comparison, fileExtensionIs, getBaseFileName, removeFileExtension, tryGetExtensionFromPath, tryGetDirectories, findPackageJson, readJson, getPackageJsonTypesVersionsPaths, MapLike, hasProperty, forEachAncestorDirectory, endsWith, stringContains, tryRemovePrefix, startsWith, hasZeroOrOneAsteriskCharacter, tryParsePattern, stripQuotes, removePrefix, getTokenAtPosition, getLeadingCommentRanges, tryAndIgnoreErrors, getEffectiveTypeRoots, findPackageJsons, unmangleScopedPackageName, tryRemoveDirectoryPrefix, hostGetCanonicalFileName, isIdentifierText, createTextSpan, CharacterCodes } from "./ts"; +import { Log, getCompletionEntriesFromSymbols, CompletionKind, createCompletionDetails, createCompletionDetailsForSymbol, SortText } from "./ts.Completions"; +import { getArgumentInfoForCompletions, ArgumentInfoForCompletions } from "./ts.SignatureHelp"; /* @internal */ -namespace ts.Completions.StringCompletions { - export function getStringLiteralCompletions(sourceFile: SourceFile, position: number, contextToken: Node | undefined, checker: TypeChecker, options: CompilerOptions, host: LanguageServiceHost, log: Log, preferences: UserPreferences): CompletionInfo | undefined { - if (isInReferenceComment(sourceFile, position)) { - const entries = getTripleSlashReferenceCompletion(sourceFile, position, options, host); - return entries && convertPathCompletions(entries); - } - if (isInString(sourceFile, position, contextToken)) { - if (!contextToken || !isStringLiteralLike(contextToken)) return undefined; - const entries = getStringLiteralCompletionEntries(sourceFile, contextToken, position, checker, options, host); - return convertStringLiteralCompletions(entries, sourceFile, checker, log, preferences); - } +export function getStringLiteralCompletions(sourceFile: SourceFile, position: number, contextToken: Node | undefined, checker: TypeChecker, options: CompilerOptions, host: LanguageServiceHost, log: Log, preferences: UserPreferences): CompletionInfo | undefined { + if (isInReferenceComment(sourceFile, position)) { + const entries = getTripleSlashReferenceCompletion(sourceFile, position, options, host); + return entries && convertPathCompletions(entries); } - - function convertStringLiteralCompletions(completion: StringLiteralCompletion | undefined, sourceFile: SourceFile, checker: TypeChecker, log: Log, preferences: UserPreferences): CompletionInfo | undefined { - if (completion === undefined) { + if (isInString(sourceFile, position, contextToken)) { + if (!contextToken || !isStringLiteralLike(contextToken)) return undefined; + const entries = getStringLiteralCompletionEntries(sourceFile, contextToken, position, checker, options, host); + return convertStringLiteralCompletions(entries, sourceFile, checker, log, preferences); + } +} +/* @internal */ +function convertStringLiteralCompletions(completion: StringLiteralCompletion | undefined, sourceFile: SourceFile, checker: TypeChecker, log: Log, preferences: UserPreferences): CompletionInfo | undefined { + if (completion === undefined) { + return undefined; + } + switch (completion.kind) { + case StringLiteralCompletionKind.Paths: + return convertPathCompletions(completion.paths); + case StringLiteralCompletionKind.Properties: { + const entries: CompletionEntry[] = []; + getCompletionEntriesFromSymbols(completion.symbols, entries, sourceFile, sourceFile, checker, ScriptTarget.ESNext, log, CompletionKind.String, preferences); // Target will not be used, so arbitrary + return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: completion.hasIndexSignature, entries }; } - switch (completion.kind) { - case StringLiteralCompletionKind.Paths: - return convertPathCompletions(completion.paths); - case StringLiteralCompletionKind.Properties: { - const entries: CompletionEntry[] = []; - getCompletionEntriesFromSymbols( - completion.symbols, - entries, - sourceFile, - sourceFile, - checker, - ScriptTarget.ESNext, - log, - CompletionKind.String, - preferences - ); // Target will not be used, so arbitrary - return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: completion.hasIndexSignature, entries }; - } - case StringLiteralCompletionKind.Types: { - const entries = completion.types.map(type => ({ name: type.value, kindModifiers: ScriptElementKindModifier.none, kind: ScriptElementKind.string, sortText: "0" })); - return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: completion.isNewIdentifier, entries }; - } - default: - return Debug.assertNever(completion); + case StringLiteralCompletionKind.Types: { + const entries = completion.types.map(type => ({ name: type.value, kindModifiers: ScriptElementKindModifier.none, kind: ScriptElementKind.string, sortText: "0" })); + return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: completion.isNewIdentifier, entries }; } + default: + return Debug.assertNever(completion); } - - export function getStringLiteralCompletionDetails(name: string, sourceFile: SourceFile, position: number, contextToken: Node | undefined, checker: TypeChecker, options: CompilerOptions, host: LanguageServiceHost, cancellationToken: CancellationToken) { - if (!contextToken || !isStringLiteralLike(contextToken)) return undefined; - const completions = getStringLiteralCompletionEntries(sourceFile, contextToken, position, checker, options, host); - return completions && stringLiteralCompletionDetails(name, contextToken, completions, sourceFile, checker, cancellationToken); - } - - function stringLiteralCompletionDetails(name: string, location: Node, completion: StringLiteralCompletion, sourceFile: SourceFile, checker: TypeChecker, cancellationToken: CancellationToken): CompletionEntryDetails | undefined { - switch (completion.kind) { - case StringLiteralCompletionKind.Paths: { - const match = find(completion.paths, p => p.name === name); - return match && createCompletionDetails(name, kindModifiersFromExtension(match.extension), match.kind, [textPart(name)]); - } - case StringLiteralCompletionKind.Properties: { - const match = find(completion.symbols, s => s.name === name); - return match && createCompletionDetailsForSymbol(match, checker, sourceFile, location, cancellationToken); - } - case StringLiteralCompletionKind.Types: - return find(completion.types, t => t.value === name) ? createCompletionDetails(name, ScriptElementKindModifier.none, ScriptElementKind.typeElement, [textPart(name)]) : undefined; - default: - return Debug.assertNever(completion); +} +/* @internal */ +export function getStringLiteralCompletionDetails(name: string, sourceFile: SourceFile, position: number, contextToken: Node | undefined, checker: TypeChecker, options: CompilerOptions, host: LanguageServiceHost, cancellationToken: CancellationToken) { + if (!contextToken || !isStringLiteralLike(contextToken)) + return undefined; + const completions = getStringLiteralCompletionEntries(sourceFile, contextToken, position, checker, options, host); + return completions && stringLiteralCompletionDetails(name, contextToken, completions, sourceFile, checker, cancellationToken); +} +/* @internal */ +function stringLiteralCompletionDetails(name: string, location: Node, completion: StringLiteralCompletion, sourceFile: SourceFile, checker: TypeChecker, cancellationToken: CancellationToken): CompletionEntryDetails | undefined { + switch (completion.kind) { + case StringLiteralCompletionKind.Paths: { + const match = find(completion.paths, p => p.name === name); + return match && createCompletionDetails(name, kindModifiersFromExtension(match.extension), match.kind, [textPart(name)]); } - } - - function convertPathCompletions(pathCompletions: readonly PathCompletion[]): CompletionInfo { - const isGlobalCompletion = false; // We don't want the editor to offer any other completions, such as snippets, inside a comment. - const isNewIdentifierLocation = true; // The user may type in a path that doesn't yet exist, creating a "new identifier" with respect to the collection of identifiers the server is aware of. - const entries = pathCompletions.map(({ name, kind, span, extension }): CompletionEntry => - ({ name, kind, kindModifiers: kindModifiersFromExtension(extension), sortText: SortText.LocationPriority, replacementSpan: span })); - return { isGlobalCompletion, isMemberCompletion: false, isNewIdentifierLocation, entries }; - } - function kindModifiersFromExtension(extension: Extension | undefined): ScriptElementKindModifier { - switch (extension) { - case Extension.Dts: return ScriptElementKindModifier.dtsModifier; - case Extension.Js: return ScriptElementKindModifier.jsModifier; - case Extension.Json: return ScriptElementKindModifier.jsonModifier; - case Extension.Jsx: return ScriptElementKindModifier.jsxModifier; - case Extension.Ts: return ScriptElementKindModifier.tsModifier; - case Extension.Tsx: return ScriptElementKindModifier.tsxModifier; - case Extension.TsBuildInfo: return Debug.fail(`Extension ${Extension.TsBuildInfo} is unsupported.`); - case undefined: return ScriptElementKindModifier.none; - default: - return Debug.assertNever(extension); + case StringLiteralCompletionKind.Properties: { + const match = find(completion.symbols, s => s.name === name); + return match && createCompletionDetailsForSymbol(match, checker, sourceFile, location, cancellationToken); } + case StringLiteralCompletionKind.Types: + return find(completion.types, t => t.value === name) ? createCompletionDetails(name, ScriptElementKindModifier.none, ScriptElementKind.typeElement, [textPart(name)]) : undefined; + default: + return Debug.assertNever(completion); } - - const enum StringLiteralCompletionKind { Paths, Properties, Types } - interface StringLiteralCompletionsFromProperties { - readonly kind: StringLiteralCompletionKind.Properties; - readonly symbols: readonly Symbol[]; - readonly hasIndexSignature: boolean; - } - interface StringLiteralCompletionsFromTypes { - readonly kind: StringLiteralCompletionKind.Types; - readonly types: readonly StringLiteralType[]; - readonly isNewIdentifier: boolean; - } - type StringLiteralCompletion = { readonly kind: StringLiteralCompletionKind.Paths, readonly paths: readonly PathCompletion[] } | StringLiteralCompletionsFromProperties | StringLiteralCompletionsFromTypes; - function getStringLiteralCompletionEntries(sourceFile: SourceFile, node: StringLiteralLike, position: number, typeChecker: TypeChecker, compilerOptions: CompilerOptions, host: LanguageServiceHost): StringLiteralCompletion | undefined { - const { parent } = node; - switch (parent.kind) { - case SyntaxKind.LiteralType: - switch (parent.parent.kind) { - case SyntaxKind.TypeReference: - return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(typeChecker.getTypeArgumentConstraint(parent as LiteralTypeNode)), isNewIdentifier: false }; - case SyntaxKind.IndexedAccessType: - // Get all apparent property names - // i.e. interface Foo { - // foo: string; - // bar: string; - // } - // let x: Foo["/*completion position*/"] - return stringLiteralCompletionsFromProperties(typeChecker.getTypeFromTypeNode((parent.parent as IndexedAccessTypeNode).objectType)); - case SyntaxKind.ImportType: - return { kind: StringLiteralCompletionKind.Paths, paths: getStringLiteralCompletionsFromModuleNames(sourceFile, node, compilerOptions, host, typeChecker) }; - case SyntaxKind.UnionType: { - if (!isTypeReferenceNode(parent.parent.parent)) return undefined; - const alreadyUsedTypes = getAlreadyUsedTypesInStringLiteralUnion(parent.parent as UnionTypeNode, parent as LiteralTypeNode); - const types = getStringLiteralTypes(typeChecker.getTypeArgumentConstraint(parent.parent as UnionTypeNode)).filter(t => !contains(alreadyUsedTypes, t.value)); - return { kind: StringLiteralCompletionKind.Types, types, isNewIdentifier: false }; - } - default: - return undefined; - } - - case SyntaxKind.PropertyAssignment: - if (isObjectLiteralExpression(parent.parent) && (parent).name === node) { - // Get quoted name of properties of the object literal expression - // i.e. interface ConfigFiles { - // 'jspm:dev': string - // } - // let files: ConfigFiles = { - // '/*completion position*/' +} +/* @internal */ +function convertPathCompletions(pathCompletions: readonly PathCompletion[]): CompletionInfo { + const isGlobalCompletion = false; // We don't want the editor to offer any other completions, such as snippets, inside a comment. + const isNewIdentifierLocation = true; // The user may type in a path that doesn't yet exist, creating a "new identifier" with respect to the collection of identifiers the server is aware of. + const entries = pathCompletions.map(({ name, kind, span, extension }): CompletionEntry => ({ name, kind, kindModifiers: kindModifiersFromExtension(extension), sortText: SortText.LocationPriority, replacementSpan: span })); + return { isGlobalCompletion, isMemberCompletion: false, isNewIdentifierLocation, entries }; +} +/* @internal */ +function kindModifiersFromExtension(extension: Extension | undefined): ScriptElementKindModifier { + switch (extension) { + case Extension.Dts: return ScriptElementKindModifier.dtsModifier; + case Extension.Js: return ScriptElementKindModifier.jsModifier; + case Extension.Json: return ScriptElementKindModifier.jsonModifier; + case Extension.Jsx: return ScriptElementKindModifier.jsxModifier; + case Extension.Ts: return ScriptElementKindModifier.tsModifier; + case Extension.Tsx: return ScriptElementKindModifier.tsxModifier; + case Extension.TsBuildInfo: return Debug.fail(`Extension ${Extension.TsBuildInfo} is unsupported.`); + case undefined: return ScriptElementKindModifier.none; + default: + return Debug.assertNever(extension); + } +} +/* @internal */ +const enum StringLiteralCompletionKind { + Paths, + Properties, + Types +} +/* @internal */ +interface StringLiteralCompletionsFromProperties { + readonly kind: StringLiteralCompletionKind.Properties; + readonly symbols: readonly Symbol[]; + readonly hasIndexSignature: boolean; +} +/* @internal */ +interface StringLiteralCompletionsFromTypes { + readonly kind: StringLiteralCompletionKind.Types; + readonly types: readonly StringLiteralType[]; + readonly isNewIdentifier: boolean; +} +/* @internal */ +type StringLiteralCompletion = { + readonly kind: StringLiteralCompletionKind.Paths; + readonly paths: readonly PathCompletion[]; +} | StringLiteralCompletionsFromProperties | StringLiteralCompletionsFromTypes; +/* @internal */ +function getStringLiteralCompletionEntries(sourceFile: SourceFile, node: StringLiteralLike, position: number, typeChecker: TypeChecker, compilerOptions: CompilerOptions, host: LanguageServiceHost): StringLiteralCompletion | undefined { + const { parent } = node; + switch (parent.kind) { + case SyntaxKind.LiteralType: + switch (parent.parent.kind) { + case SyntaxKind.TypeReference: + return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(typeChecker.getTypeArgumentConstraint((parent as LiteralTypeNode))), isNewIdentifier: false }; + case SyntaxKind.IndexedAccessType: + // Get all apparent property names + // i.e. interface Foo { + // foo: string; + // bar: string; // } - // - // function foo(c: ConfigFiles) {} - // foo({ - // '/*completion position*/' - // }); - return stringLiteralCompletionsFromProperties(typeChecker.getContextualType(parent.parent)); - } - return fromContextualType(); - - case SyntaxKind.ElementAccessExpression: { - const { expression, argumentExpression } = parent as ElementAccessExpression; - if (node === argumentExpression) { - // Get all names of properties on the expression - // i.e. interface A { - // 'prop1': string - // } - // let a: A; - // a['/*completion position*/'] - return stringLiteralCompletionsFromProperties(typeChecker.getTypeAtLocation(expression)); + // let x: Foo["/*completion position*/"] + return stringLiteralCompletionsFromProperties(typeChecker.getTypeFromTypeNode((parent.parent as IndexedAccessTypeNode).objectType)); + case SyntaxKind.ImportType: + return { kind: StringLiteralCompletionKind.Paths, paths: getStringLiteralCompletionsFromModuleNames(sourceFile, node, compilerOptions, host, typeChecker) }; + case SyntaxKind.UnionType: { + if (!isTypeReferenceNode(parent.parent.parent)) + return undefined; + const alreadyUsedTypes = getAlreadyUsedTypesInStringLiteralUnion((parent.parent as UnionTypeNode), (parent as LiteralTypeNode)); + const types = getStringLiteralTypes(typeChecker.getTypeArgumentConstraint((parent.parent as UnionTypeNode))).filter(t => !contains(alreadyUsedTypes, t.value)); + return { kind: StringLiteralCompletionKind.Types, types, isNewIdentifier: false }; } - return undefined; + default: + return undefined; } - - case SyntaxKind.CallExpression: - case SyntaxKind.NewExpression: - if (!isRequireCall(parent, /*checkArgumentIsStringLiteralLike*/ false) && !isImportCall(parent)) { - const argumentInfo = SignatureHelp.getArgumentInfoForCompletions(node, position, sourceFile); - // Get string literal completions from specialized signatures of the target - // i.e. declare function f(a: 'A'); - // f("/*completion position*/") - return argumentInfo ? getStringLiteralCompletionsFromSignature(argumentInfo, typeChecker) : fromContextualType(); - } - // falls through (is `require("")` or `import("")`) - - case SyntaxKind.ImportDeclaration: - case SyntaxKind.ExportDeclaration: - case SyntaxKind.ExternalModuleReference: - // Get all known external module names or complete a path to a module - // i.e. import * as ns from "/*completion position*/"; - // var y = import("/*completion position*/"); - // import x = require("/*completion position*/"); - // var y = require("/*completion position*/"); - // export * from "/*completion position*/"; - return { kind: StringLiteralCompletionKind.Paths, paths: getStringLiteralCompletionsFromModuleNames(sourceFile, node, compilerOptions, host, typeChecker) }; - - default: - return fromContextualType(); - } - - function fromContextualType(): StringLiteralCompletion { - // Get completion for string literal from string literal type - // i.e. var x: "hi" | "hello" = "/*completion position*/" - return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(getContextualTypeFromParent(node, typeChecker)), isNewIdentifier: false }; + case SyntaxKind.PropertyAssignment: + if (isObjectLiteralExpression(parent.parent) && (parent).name === node) { + // Get quoted name of properties of the object literal expression + // i.e. interface ConfigFiles { + // 'jspm:dev': string + // } + // let files: ConfigFiles = { + // '/*completion position*/' + // } + // + // function foo(c: ConfigFiles) {} + // foo({ + // '/*completion position*/' + // }); + return stringLiteralCompletionsFromProperties(typeChecker.getContextualType(parent.parent)); + } + return fromContextualType(); + case SyntaxKind.ElementAccessExpression: { + const { expression, argumentExpression } = (parent as ElementAccessExpression); + if (node === argumentExpression) { + // Get all names of properties on the expression + // i.e. interface A { + // 'prop1': string + // } + // let a: A; + // a['/*completion position*/'] + return stringLiteralCompletionsFromProperties(typeChecker.getTypeAtLocation(expression)); + } + return undefined; } + case SyntaxKind.CallExpression: + case SyntaxKind.NewExpression: + if (!isRequireCall(parent, /*checkArgumentIsStringLiteralLike*/ false) && !isImportCall(parent)) { + const argumentInfo = getArgumentInfoForCompletions(node, position, sourceFile); + // Get string literal completions from specialized signatures of the target + // i.e. declare function f(a: 'A'); + // f("/*completion position*/") + return argumentInfo ? getStringLiteralCompletionsFromSignature(argumentInfo, typeChecker) : fromContextualType(); + } + // falls through (is `require("")` or `import("")`) + case SyntaxKind.ImportDeclaration: + case SyntaxKind.ExportDeclaration: + case SyntaxKind.ExternalModuleReference: + // Get all known external module names or complete a path to a module + // i.e. import * as ns from "/*completion position*/"; + // var y = import("/*completion position*/"); + // import x = require("/*completion position*/"); + // var y = require("/*completion position*/"); + // export * from "/*completion position*/"; + return { kind: StringLiteralCompletionKind.Paths, paths: getStringLiteralCompletionsFromModuleNames(sourceFile, node, compilerOptions, host, typeChecker) }; + default: + return fromContextualType(); + } + function fromContextualType(): StringLiteralCompletion { + // Get completion for string literal from string literal type + // i.e. var x: "hi" | "hello" = "/*completion position*/" + return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(getContextualTypeFromParent(node, typeChecker)), isNewIdentifier: false }; } - - function getAlreadyUsedTypesInStringLiteralUnion(union: UnionTypeNode, current: LiteralTypeNode): readonly string[] { - return mapDefined(union.types, type => - type !== current && isLiteralTypeNode(type) && isStringLiteral(type.literal) ? type.literal.text : undefined); - } - - function getStringLiteralCompletionsFromSignature(argumentInfo: SignatureHelp.ArgumentInfoForCompletions, checker: TypeChecker): StringLiteralCompletionsFromTypes { - let isNewIdentifier = false; - - const uniques = createMap(); - const candidates: Signature[] = []; - checker.getResolvedSignature(argumentInfo.invocation, candidates, argumentInfo.argumentCount); - const types = flatMap(candidates, candidate => { - if (!signatureHasRestParameter(candidate) && argumentInfo.argumentCount > candidate.parameters.length) return; - const type = checker.getParameterType(candidate, argumentInfo.argumentIndex); - isNewIdentifier = isNewIdentifier || !!(type.flags & TypeFlags.String); - return getStringLiteralTypes(type, uniques); - }); - - return { kind: StringLiteralCompletionKind.Types, types, isNewIdentifier }; - } - - function stringLiteralCompletionsFromProperties(type: Type | undefined): StringLiteralCompletionsFromProperties | undefined { - return type && { - kind: StringLiteralCompletionKind.Properties, - symbols: type.getApparentProperties().filter(prop => !isPrivateIdentifierPropertyDeclaration(prop.valueDeclaration)), - hasIndexSignature: hasIndexSignature(type) - }; - } - - function getStringLiteralTypes(type: Type | undefined, uniques = createMap()): readonly StringLiteralType[] { - if (!type) return emptyArray; - type = skipConstraint(type); - return type.isUnion() ? flatMap(type.types, t => getStringLiteralTypes(t, uniques)) : - type.isStringLiteral() && !(type.flags & TypeFlags.EnumLiteral) && addToSeen(uniques, type.value) ? [type] : emptyArray; - } - - interface NameAndKind { - readonly name: string; - readonly kind: ScriptElementKind.scriptElement | ScriptElementKind.directory | ScriptElementKind.externalModuleName; - readonly extension: Extension | undefined; - } - interface PathCompletion extends NameAndKind { - readonly span: TextSpan | undefined; - } - - function nameAndKind(name: string, kind: NameAndKind["kind"], extension: Extension | undefined): NameAndKind { - return { name, kind, extension }; - } - function directoryResult(name: string): NameAndKind { - return nameAndKind(name, ScriptElementKind.directory, /*extension*/ undefined); - } - - function addReplacementSpans(text: string, textStart: number, names: readonly NameAndKind[]): readonly PathCompletion[] { - const span = getDirectoryFragmentTextSpan(text, textStart); - return names.map(({ name, kind, extension }): PathCompletion => ({ name, kind, extension, span })); - } - - function getStringLiteralCompletionsFromModuleNames(sourceFile: SourceFile, node: LiteralExpression, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): readonly PathCompletion[] { - return addReplacementSpans(node.text, node.getStart(sourceFile) + 1, getStringLiteralCompletionsFromModuleNamesWorker(sourceFile, node, compilerOptions, host, typeChecker)); - } - - function getStringLiteralCompletionsFromModuleNamesWorker(sourceFile: SourceFile, node: LiteralExpression, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): readonly NameAndKind[] { - const literalValue = normalizeSlashes(node.text); - - const scriptPath = sourceFile.path; - const scriptDirectory = getDirectoryPath(scriptPath); - - return isPathRelativeToScript(literalValue) || !compilerOptions.baseUrl && (isRootedDiskPath(literalValue) || isUrl(literalValue)) - ? getCompletionEntriesForRelativeModules(literalValue, scriptDirectory, compilerOptions, host, scriptPath) - : getCompletionEntriesForNonRelativeModules(literalValue, scriptDirectory, compilerOptions, host, typeChecker); - } - - interface ExtensionOptions { - readonly extensions: readonly Extension[]; - readonly includeExtensions: boolean; - } - function getExtensionOptions(compilerOptions: CompilerOptions, includeExtensions = false): ExtensionOptions { - return { extensions: getSupportedExtensionsForModuleResolution(compilerOptions), includeExtensions }; - } - function getCompletionEntriesForRelativeModules(literalValue: string, scriptDirectory: string, compilerOptions: CompilerOptions, host: LanguageServiceHost, scriptPath: Path) { - const extensionOptions = getExtensionOptions(compilerOptions); - if (compilerOptions.rootDirs) { - return getCompletionEntriesForDirectoryFragmentWithRootDirs( - compilerOptions.rootDirs, literalValue, scriptDirectory, extensionOptions, compilerOptions, host, scriptPath); - } - else { - return getCompletionEntriesForDirectoryFragment(literalValue, scriptDirectory, extensionOptions, host, scriptPath); - } +} +/* @internal */ +function getAlreadyUsedTypesInStringLiteralUnion(union: UnionTypeNode, current: LiteralTypeNode): readonly string[] { + return mapDefined(union.types, type => type !== current && isLiteralTypeNode(type) && isStringLiteral(type.literal) ? type.literal.text : undefined); +} +/* @internal */ +function getStringLiteralCompletionsFromSignature(argumentInfo: ArgumentInfoForCompletions, checker: TypeChecker): StringLiteralCompletionsFromTypes { + let isNewIdentifier = false; + const uniques = createMap(); + const candidates: Signature[] = []; + checker.getResolvedSignature(argumentInfo.invocation, candidates, argumentInfo.argumentCount); + const types = flatMap(candidates, candidate => { + if (!signatureHasRestParameter(candidate) && argumentInfo.argumentCount > candidate.parameters.length) + return; + const type = checker.getParameterType(candidate, argumentInfo.argumentIndex); + isNewIdentifier = isNewIdentifier || !!(type.flags & TypeFlags.String); + return getStringLiteralTypes(type, uniques); + }); + return { kind: StringLiteralCompletionKind.Types, types, isNewIdentifier }; +} +/* @internal */ +function stringLiteralCompletionsFromProperties(type: Type | undefined): StringLiteralCompletionsFromProperties | undefined { + return type && { + kind: StringLiteralCompletionKind.Properties, + symbols: type.getApparentProperties().filter(prop => !isPrivateIdentifierPropertyDeclaration(prop.valueDeclaration)), + hasIndexSignature: hasIndexSignature(type) + }; +} +/* @internal */ +function getStringLiteralTypes(type: Type | undefined, uniques = createMap()): readonly StringLiteralType[] { + if (!type) + return emptyArray; + type = skipConstraint(type); + return type.isUnion() ? flatMap(type.types, t => getStringLiteralTypes(t, uniques)) : + type.isStringLiteral() && !(type.flags & TypeFlags.EnumLiteral) && addToSeen(uniques, type.value) ? [type] : emptyArray; +} +/* @internal */ +interface NameAndKind { + readonly name: string; + readonly kind: ScriptElementKind.scriptElement | ScriptElementKind.directory | ScriptElementKind.externalModuleName; + readonly extension: Extension | undefined; +} +/* @internal */ +interface PathCompletion extends NameAndKind { + readonly span: TextSpan | undefined; +} +/* @internal */ +function nameAndKind(name: string, kind: NameAndKind["kind"], extension: Extension | undefined): NameAndKind { + return { name, kind, extension }; +} +/* @internal */ +function directoryResult(name: string): NameAndKind { + return nameAndKind(name, ScriptElementKind.directory, /*extension*/ undefined); +} +/* @internal */ +function addReplacementSpans(text: string, textStart: number, names: readonly NameAndKind[]): readonly PathCompletion[] { + const span = getDirectoryFragmentTextSpan(text, textStart); + return names.map(({ name, kind, extension }): PathCompletion => ({ name, kind, extension, span })); +} +/* @internal */ +function getStringLiteralCompletionsFromModuleNames(sourceFile: SourceFile, node: LiteralExpression, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): readonly PathCompletion[] { + return addReplacementSpans(node.text, node.getStart(sourceFile) + 1, getStringLiteralCompletionsFromModuleNamesWorker(sourceFile, node, compilerOptions, host, typeChecker)); +} +/* @internal */ +function getStringLiteralCompletionsFromModuleNamesWorker(sourceFile: SourceFile, node: LiteralExpression, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): readonly NameAndKind[] { + const literalValue = normalizeSlashes(node.text); + const scriptPath = sourceFile.path; + const scriptDirectory = getDirectoryPath(scriptPath); + return isPathRelativeToScript(literalValue) || !compilerOptions.baseUrl && (isRootedDiskPath(literalValue) || isUrl(literalValue)) + ? getCompletionEntriesForRelativeModules(literalValue, scriptDirectory, compilerOptions, host, scriptPath) + : getCompletionEntriesForNonRelativeModules(literalValue, scriptDirectory, compilerOptions, host, typeChecker); +} +/* @internal */ +interface ExtensionOptions { + readonly extensions: readonly Extension[]; + readonly includeExtensions: boolean; +} +/* @internal */ +function getExtensionOptions(compilerOptions: CompilerOptions, includeExtensions = false): ExtensionOptions { + return { extensions: getSupportedExtensionsForModuleResolution(compilerOptions), includeExtensions }; +} +/* @internal */ +function getCompletionEntriesForRelativeModules(literalValue: string, scriptDirectory: string, compilerOptions: CompilerOptions, host: LanguageServiceHost, scriptPath: Path) { + const extensionOptions = getExtensionOptions(compilerOptions); + if (compilerOptions.rootDirs) { + return getCompletionEntriesForDirectoryFragmentWithRootDirs(compilerOptions.rootDirs, literalValue, scriptDirectory, extensionOptions, compilerOptions, host, scriptPath); } - - function getSupportedExtensionsForModuleResolution(compilerOptions: CompilerOptions): readonly Extension[] { - const extensions = getSupportedExtensions(compilerOptions); - return compilerOptions.resolveJsonModule && getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs ? - extensions.concat(Extension.Json) : - extensions; + else { + return getCompletionEntriesForDirectoryFragment(literalValue, scriptDirectory, extensionOptions, host, scriptPath); } - - /** - * Takes a script path and returns paths for all potential folders that could be merged with its - * containing folder via the "rootDirs" compiler option - */ - function getBaseDirectoriesFromRootDirs(rootDirs: string[], basePath: string, scriptDirectory: string, ignoreCase: boolean): readonly string[] { - // Make all paths absolute/normalized if they are not already - rootDirs = rootDirs.map(rootDirectory => normalizePath(isRootedDiskPath(rootDirectory) ? rootDirectory : combinePaths(basePath, rootDirectory))); - - // Determine the path to the directory containing the script relative to the root directory it is contained within - const relativeDirectory = firstDefined(rootDirs, rootDirectory => - containsPath(rootDirectory, scriptDirectory, basePath, ignoreCase) ? scriptDirectory.substr(rootDirectory.length) : undefined)!; // TODO: GH#18217 - - // Now find a path for each potential directory that is to be merged with the one containing the script - return deduplicate( - [...rootDirs.map(rootDirectory => combinePaths(rootDirectory, relativeDirectory)), scriptDirectory], - equateStringsCaseSensitive, - compareStringsCaseSensitive); - } - - function getCompletionEntriesForDirectoryFragmentWithRootDirs(rootDirs: string[], fragment: string, scriptDirectory: string, extensionOptions: ExtensionOptions, compilerOptions: CompilerOptions, host: LanguageServiceHost, exclude: string): readonly NameAndKind[] { - const basePath = compilerOptions.project || host.getCurrentDirectory(); - const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames()); - const baseDirectories = getBaseDirectoriesFromRootDirs(rootDirs, basePath, scriptDirectory, ignoreCase); - return flatMap(baseDirectories, baseDirectory => getCompletionEntriesForDirectoryFragment(fragment, baseDirectory, extensionOptions, host, exclude)); - } - +} +/* @internal */ +function getSupportedExtensionsForModuleResolution(compilerOptions: CompilerOptions): readonly Extension[] { + const extensions = getSupportedExtensions(compilerOptions); + return compilerOptions.resolveJsonModule && getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs ? + extensions.concat(Extension.Json) : + extensions; +} +/** + * Takes a script path and returns paths for all potential folders that could be merged with its + * containing folder via the "rootDirs" compiler option + */ +/* @internal */ +function getBaseDirectoriesFromRootDirs(rootDirs: string[], basePath: string, scriptDirectory: string, ignoreCase: boolean): readonly string[] { + // Make all paths absolute/normalized if they are not already + rootDirs = rootDirs.map(rootDirectory => normalizePath(isRootedDiskPath(rootDirectory) ? rootDirectory : combinePaths(basePath, rootDirectory))); + // Determine the path to the directory containing the script relative to the root directory it is contained within + const relativeDirectory = (firstDefined(rootDirs, rootDirectory => containsPath(rootDirectory, scriptDirectory, basePath, ignoreCase) ? scriptDirectory.substr(rootDirectory.length) : undefined)!); // TODO: GH#18217 + // Now find a path for each potential directory that is to be merged with the one containing the script + return deduplicate([...rootDirs.map(rootDirectory => combinePaths(rootDirectory, relativeDirectory)), scriptDirectory], equateStringsCaseSensitive, compareStringsCaseSensitive); +} +/* @internal */ +function getCompletionEntriesForDirectoryFragmentWithRootDirs(rootDirs: string[], fragment: string, scriptDirectory: string, extensionOptions: ExtensionOptions, compilerOptions: CompilerOptions, host: LanguageServiceHost, exclude: string): readonly NameAndKind[] { + const basePath = compilerOptions.project || host.getCurrentDirectory(); + const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames()); + const baseDirectories = getBaseDirectoriesFromRootDirs(rootDirs, basePath, scriptDirectory, ignoreCase); + return flatMap(baseDirectories, baseDirectory => getCompletionEntriesForDirectoryFragment(fragment, baseDirectory, extensionOptions, host, exclude)); +} +/** + * Given a path ending at a directory, gets the completions for the path, and filters for those entries containing the basename. + */ +/* @internal */ +function getCompletionEntriesForDirectoryFragment(fragment: string, scriptPath: string, { extensions, includeExtensions }: ExtensionOptions, host: LanguageServiceHost, exclude?: string, result: NameAndKind[] = []): NameAndKind[] { + if (fragment === undefined) { + fragment = ""; + } + fragment = normalizeSlashes(fragment); /** - * Given a path ending at a directory, gets the completions for the path, and filters for those entries containing the basename. + * Remove the basename from the path. Note that we don't use the basename to filter completions; + * the client is responsible for refining completions. */ - function getCompletionEntriesForDirectoryFragment(fragment: string, scriptPath: string, { extensions, includeExtensions }: ExtensionOptions, host: LanguageServiceHost, exclude?: string, result: NameAndKind[] = []): NameAndKind[] { - if (fragment === undefined) { - fragment = ""; - } - - fragment = normalizeSlashes(fragment); - + if (!hasTrailingDirectorySeparator(fragment)) { + fragment = getDirectoryPath(fragment); + } + if (fragment === "") { + fragment = "." + directorySeparator; + } + fragment = ensureTrailingDirectorySeparator(fragment); + // const absolutePath = normalizeAndPreserveTrailingSlash(isRootedDiskPath(fragment) ? fragment : combinePaths(scriptPath, fragment)); // TODO(rbuckton): should use resolvePaths + const absolutePath = resolvePath(scriptPath, fragment); + const baseDirectory = hasTrailingDirectorySeparator(absolutePath) ? absolutePath : getDirectoryPath(absolutePath); + const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames()); + if (!tryDirectoryExists(host, baseDirectory)) + return result; + // Enumerate the available files if possible + const files = tryReadDirectory(host, baseDirectory, extensions, /*exclude*/ undefined, /*include*/ ["./*"]); + if (files) { /** - * Remove the basename from the path. Note that we don't use the basename to filter completions; - * the client is responsible for refining completions. + * Multiple file entries might map to the same truncated name once we remove extensions + * (happens iff includeExtensions === false)so we use a set-like data structure. Eg: + * + * both foo.ts and foo.tsx become foo */ - if (!hasTrailingDirectorySeparator(fragment)) { - fragment = getDirectoryPath(fragment); - } - - if (fragment === "") { - fragment = "." + directorySeparator; - } - - fragment = ensureTrailingDirectorySeparator(fragment); - - // const absolutePath = normalizeAndPreserveTrailingSlash(isRootedDiskPath(fragment) ? fragment : combinePaths(scriptPath, fragment)); // TODO(rbuckton): should use resolvePaths - const absolutePath = resolvePath(scriptPath, fragment); - const baseDirectory = hasTrailingDirectorySeparator(absolutePath) ? absolutePath : getDirectoryPath(absolutePath); - - const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames()); - if (!tryDirectoryExists(host, baseDirectory)) return result; - - // Enumerate the available files if possible - const files = tryReadDirectory(host, baseDirectory, extensions, /*exclude*/ undefined, /*include*/ ["./*"]); - - if (files) { - /** - * Multiple file entries might map to the same truncated name once we remove extensions - * (happens iff includeExtensions === false)so we use a set-like data structure. Eg: - * - * both foo.ts and foo.tsx become foo - */ - const foundFiles = createMap(); // maps file to its extension - for (let filePath of files) { - filePath = normalizePath(filePath); - if (exclude && comparePaths(filePath, exclude, scriptPath, ignoreCase) === Comparison.EqualTo) { - continue; - } - - const foundFileName = includeExtensions || fileExtensionIs(filePath, Extension.Json) ? getBaseFileName(filePath) : removeFileExtension(getBaseFileName(filePath)); - foundFiles.set(foundFileName, tryGetExtensionFromPath(filePath)); + const foundFiles = createMap(); // maps file to its extension + for (let filePath of files) { + filePath = normalizePath(filePath); + if (exclude && comparePaths(filePath, exclude, scriptPath, ignoreCase) === Comparison.EqualTo) { + continue; } - - foundFiles.forEach((ext, foundFile) => { - result.push(nameAndKind(foundFile, ScriptElementKind.scriptElement, ext)); - }); + const foundFileName = includeExtensions || fileExtensionIs(filePath, Extension.Json) ? getBaseFileName(filePath) : removeFileExtension(getBaseFileName(filePath)); + foundFiles.set(foundFileName, tryGetExtensionFromPath(filePath)); } - - // If possible, get folder completion as well - const directories = tryGetDirectories(host, baseDirectory); - - if (directories) { - for (const directory of directories) { - const directoryName = getBaseFileName(normalizePath(directory)); - if (directoryName !== "@types") { - result.push(directoryResult(directoryName)); - } + foundFiles.forEach((ext, foundFile) => { + result.push(nameAndKind(foundFile, ScriptElementKind.scriptElement, ext)); + }); + } + // If possible, get folder completion as well + const directories = tryGetDirectories(host, baseDirectory); + if (directories) { + for (const directory of directories) { + const directoryName = getBaseFileName(normalizePath(directory)); + if (directoryName !== "@types") { + result.push(directoryResult(directoryName)); } } - - // check for a version redirect - const packageJsonPath = findPackageJson(baseDirectory, host); - if (packageJsonPath) { - const packageJson = readJson(packageJsonPath, host as { readFile: (filename: string) => string | undefined }); - const typesVersions = (packageJson as any).typesVersions; - if (typeof typesVersions === "object") { - const versionResult = getPackageJsonTypesVersionsPaths(typesVersions); - const versionPaths = versionResult && versionResult.paths; - const rest = absolutePath.slice(ensureTrailingDirectorySeparator(baseDirectory).length); - if (versionPaths) { - addCompletionEntriesFromPaths(result, rest, baseDirectory, extensions, versionPaths, host); - } + } + // check for a version redirect + const packageJsonPath = findPackageJson(baseDirectory, host); + if (packageJsonPath) { + const packageJson = readJson(packageJsonPath, (host as { + readFile: (filename: string) => string | undefined; + })); + const typesVersions = (packageJson as any).typesVersions; + if (typeof typesVersions === "object") { + const versionResult = getPackageJsonTypesVersionsPaths(typesVersions); + const versionPaths = versionResult && versionResult.paths; + const rest = absolutePath.slice(ensureTrailingDirectorySeparator(baseDirectory).length); + if (versionPaths) { + addCompletionEntriesFromPaths(result, rest, baseDirectory, extensions, versionPaths, host); } } - - return result; } - - function addCompletionEntriesFromPaths(result: NameAndKind[], fragment: string, baseDirectory: string, fileExtensions: readonly string[], paths: MapLike, host: LanguageServiceHost) { - for (const path in paths) { - if (!hasProperty(paths, path)) continue; - const patterns = paths[path]; - if (patterns) { - for (const { name, kind, extension } of getCompletionsForPathMapping(path, patterns, fragment, baseDirectory, fileExtensions, host)) { - // Path mappings may provide a duplicate way to get to something we've already added, so don't add again. - if (!result.some(entry => entry.name === name)) { - result.push(nameAndKind(name, kind, extension)); - } + return result; +} +/* @internal */ +function addCompletionEntriesFromPaths(result: NameAndKind[], fragment: string, baseDirectory: string, fileExtensions: readonly string[], paths: MapLike, host: LanguageServiceHost) { + for (const path in paths) { + if (!hasProperty(paths, path)) + continue; + const patterns = paths[path]; + if (patterns) { + for (const { name, kind, extension } of getCompletionsForPathMapping(path, patterns, fragment, baseDirectory, fileExtensions, host)) { + // Path mappings may provide a duplicate way to get to something we've already added, so don't add again. + if (!result.some(entry => entry.name === name)) { + result.push(nameAndKind(name, kind, extension)); } } } } - - /** - * Check all of the declared modules and those in node modules. Possible sources of modules: - * Modules that are found by the type checker - * Modules found relative to "baseUrl" compliler options (including patterns from "paths" compiler option) - * Modules from node_modules (i.e. those listed in package.json) - * This includes all files that are found in node_modules/moduleName/ with acceptable file extensions - */ - function getCompletionEntriesForNonRelativeModules(fragment: string, scriptPath: string, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): readonly NameAndKind[] { - const { baseUrl, paths } = compilerOptions; - - const result: NameAndKind[] = []; - - const extensionOptions = getExtensionOptions(compilerOptions); - if (baseUrl) { - const projectDir = compilerOptions.project || host.getCurrentDirectory(); - const absolute = normalizePath(combinePaths(projectDir, baseUrl)); - getCompletionEntriesForDirectoryFragment(fragment, absolute, extensionOptions, host, /*exclude*/ undefined, result); - if (paths) { - addCompletionEntriesFromPaths(result, fragment, absolute, extensionOptions.extensions, paths, host); +} +/** + * Check all of the declared modules and those in node modules. Possible sources of modules: + * Modules that are found by the type checker + * Modules found relative to "baseUrl" compliler options (including patterns from "paths" compiler option) + * Modules from node_modules (i.e. those listed in package.json) + * This includes all files that are found in node_modules/moduleName/ with acceptable file extensions + */ +/* @internal */ +function getCompletionEntriesForNonRelativeModules(fragment: string, scriptPath: string, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): readonly NameAndKind[] { + const { baseUrl, paths } = compilerOptions; + const result: NameAndKind[] = []; + const extensionOptions = getExtensionOptions(compilerOptions); + if (baseUrl) { + const projectDir = compilerOptions.project || host.getCurrentDirectory(); + const absolute = normalizePath(combinePaths(projectDir, baseUrl)); + getCompletionEntriesForDirectoryFragment(fragment, absolute, extensionOptions, host, /*exclude*/ undefined, result); + if (paths) { + addCompletionEntriesFromPaths(result, fragment, absolute, extensionOptions.extensions, paths, host); + } + } + const fragmentDirectory = getFragmentDirectory(fragment); + for (const ambientName of getAmbientModuleCompletions(fragment, fragmentDirectory, typeChecker)) { + result.push(nameAndKind(ambientName, ScriptElementKind.externalModuleName, /*extension*/ undefined)); + } + getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, fragmentDirectory, extensionOptions, result); + if (getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs) { + // If looking for a global package name, don't just include everything in `node_modules` because that includes dependencies' own dependencies. + // (But do if we didn't find anything, e.g. 'package.json' missing.) + let foundGlobal = false; + if (fragmentDirectory === undefined) { + for (const moduleName of enumerateNodeModulesVisibleToScript(host, scriptPath)) { + if (!result.some(entry => entry.name === moduleName)) { + foundGlobal = true; + result.push(nameAndKind(moduleName, ScriptElementKind.externalModuleName, /*extension*/ undefined)); + } } } - - const fragmentDirectory = getFragmentDirectory(fragment); - for (const ambientName of getAmbientModuleCompletions(fragment, fragmentDirectory, typeChecker)) { - result.push(nameAndKind(ambientName, ScriptElementKind.externalModuleName, /*extension*/ undefined)); - } - - getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, fragmentDirectory, extensionOptions, result); - - if (getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs) { - // If looking for a global package name, don't just include everything in `node_modules` because that includes dependencies' own dependencies. - // (But do if we didn't find anything, e.g. 'package.json' missing.) - let foundGlobal = false; - if (fragmentDirectory === undefined) { - for (const moduleName of enumerateNodeModulesVisibleToScript(host, scriptPath)) { - if (!result.some(entry => entry.name === moduleName)) { - foundGlobal = true; - result.push(nameAndKind(moduleName, ScriptElementKind.externalModuleName, /*extension*/ undefined)); - } + if (!foundGlobal) { + forEachAncestorDirectory(scriptPath, ancestor => { + const nodeModules = combinePaths(ancestor, "node_modules"); + if (tryDirectoryExists(host, nodeModules)) { + getCompletionEntriesForDirectoryFragment(fragment, nodeModules, extensionOptions, host, /*exclude*/ undefined, result); } - } - if (!foundGlobal) { - forEachAncestorDirectory(scriptPath, ancestor => { - const nodeModules = combinePaths(ancestor, "node_modules"); - if (tryDirectoryExists(host, nodeModules)) { - getCompletionEntriesForDirectoryFragment(fragment, nodeModules, extensionOptions, host, /*exclude*/ undefined, result); - } - }); - } + }); } - - return result; } - - function getFragmentDirectory(fragment: string): string | undefined { - return containsSlash(fragment) ? hasTrailingDirectorySeparator(fragment) ? fragment : getDirectoryPath(fragment) : undefined; - } - - function getCompletionsForPathMapping( - path: string, patterns: readonly string[], fragment: string, baseUrl: string, fileExtensions: readonly string[], host: LanguageServiceHost, - ): readonly NameAndKind[] { - if (!endsWith(path, "*")) { - // For a path mapping "foo": ["/x/y/z.ts"], add "foo" itself as a completion. - return !stringContains(path, "*") ? justPathMappingName(path) : emptyArray; - } - - const pathPrefix = path.slice(0, path.length - 1); - const remainingFragment = tryRemovePrefix(fragment, pathPrefix); - return remainingFragment === undefined ? justPathMappingName(pathPrefix) : flatMap(patterns, pattern => - getModulesForPathsPattern(remainingFragment, baseUrl, pattern, fileExtensions, host)); - - function justPathMappingName(name: string): readonly NameAndKind[] { - return startsWith(name, fragment) ? [directoryResult(name)] : emptyArray; - } + return result; +} +/* @internal */ +function getFragmentDirectory(fragment: string): string | undefined { + return containsSlash(fragment) ? hasTrailingDirectorySeparator(fragment) ? fragment : getDirectoryPath(fragment) : undefined; +} +/* @internal */ +function getCompletionsForPathMapping(path: string, patterns: readonly string[], fragment: string, baseUrl: string, fileExtensions: readonly string[], host: LanguageServiceHost): readonly NameAndKind[] { + if (!endsWith(path, "*")) { + // For a path mapping "foo": ["/x/y/z.ts"], add "foo" itself as a completion. + return !stringContains(path, "*") ? justPathMappingName(path) : emptyArray; } - - function getModulesForPathsPattern(fragment: string, baseUrl: string, pattern: string, fileExtensions: readonly string[], host: LanguageServiceHost): readonly NameAndKind[] | undefined { - if (!host.readDirectory) { - return undefined; - } - - const parsed = hasZeroOrOneAsteriskCharacter(pattern) ? tryParsePattern(pattern) : undefined; - if (!parsed) { - return undefined; - } - - // The prefix has two effective parts: the directory path and the base component after the filepath that is not a - // full directory component. For example: directory/path/of/prefix/base* - const normalizedPrefix = resolvePath(parsed.prefix); - const normalizedPrefixDirectory = hasTrailingDirectorySeparator(parsed.prefix) ? normalizedPrefix : getDirectoryPath(normalizedPrefix); - const normalizedPrefixBase = hasTrailingDirectorySeparator(parsed.prefix) ? "" : getBaseFileName(normalizedPrefix); - - const fragmentHasPath = containsSlash(fragment); - const fragmentDirectory = fragmentHasPath ? hasTrailingDirectorySeparator(fragment) ? fragment : getDirectoryPath(fragment) : undefined; - - // Try and expand the prefix to include any path from the fragment so that we can limit the readDirectory call - const expandedPrefixDirectory = fragmentHasPath ? combinePaths(normalizedPrefixDirectory, normalizedPrefixBase + fragmentDirectory) : normalizedPrefixDirectory; - - const normalizedSuffix = normalizePath(parsed.suffix); - // Need to normalize after combining: If we combinePaths("a", "../b"), we want "b" and not "a/../b". - const baseDirectory = normalizePath(combinePaths(baseUrl, expandedPrefixDirectory)); - const completePrefix = fragmentHasPath ? baseDirectory : ensureTrailingDirectorySeparator(baseDirectory) + normalizedPrefixBase; - - // If we have a suffix, then we need to read the directory all the way down. We could create a glob - // that encodes the suffix, but we would have to escape the character "?" which readDirectory - // doesn't support. For now, this is safer but slower - const includeGlob = normalizedSuffix ? "**/*" : "./*"; - - const matches = mapDefined(tryReadDirectory(host, baseDirectory, fileExtensions, /*exclude*/ undefined, [includeGlob]), match => { - const extension = tryGetExtensionFromPath(match); - const name = trimPrefixAndSuffix(match); - return name === undefined ? undefined : nameAndKind(removeFileExtension(name), ScriptElementKind.scriptElement, extension); - }); - const directories = mapDefined(tryGetDirectories(host, baseDirectory).map(d => combinePaths(baseDirectory, d)), dir => { - const name = trimPrefixAndSuffix(dir); - return name === undefined ? undefined : directoryResult(name); - }); - return [...matches, ...directories]; - - function trimPrefixAndSuffix(path: string): string | undefined { - const inner = withoutStartAndEnd(normalizePath(path), completePrefix, normalizedSuffix); - return inner === undefined ? undefined : removeLeadingDirectorySeparator(inner); - } + const pathPrefix = path.slice(0, path.length - 1); + const remainingFragment = tryRemovePrefix(fragment, pathPrefix); + return remainingFragment === undefined ? justPathMappingName(pathPrefix) : flatMap(patterns, pattern => getModulesForPathsPattern(remainingFragment, baseUrl, pattern, fileExtensions, host)); + function justPathMappingName(name: string): readonly NameAndKind[] { + return startsWith(name, fragment) ? [directoryResult(name)] : emptyArray; } - - function withoutStartAndEnd(s: string, start: string, end: string): string | undefined { - return startsWith(s, start) && endsWith(s, end) ? s.slice(start.length, s.length - end.length) : undefined; - } - - function removeLeadingDirectorySeparator(path: string): string { - return path[0] === directorySeparator ? path.slice(1) : path; - } - - function getAmbientModuleCompletions(fragment: string, fragmentDirectory: string | undefined, checker: TypeChecker): readonly string[] { - // Get modules that the type checker picked up - const ambientModules = checker.getAmbientModules().map(sym => stripQuotes(sym.name)); - const nonRelativeModuleNames = ambientModules.filter(moduleName => startsWith(moduleName, fragment)); - - // Nested modules of the form "module-name/sub" need to be adjusted to only return the string - // after the last '/' that appears in the fragment because that's where the replacement span - // starts - if (fragmentDirectory !== undefined) { - const moduleNameWithSeparator = ensureTrailingDirectorySeparator(fragmentDirectory); - return nonRelativeModuleNames.map(nonRelativeModuleName => removePrefix(nonRelativeModuleName, moduleNameWithSeparator)); - } - return nonRelativeModuleNames; - } - - function getTripleSlashReferenceCompletion(sourceFile: SourceFile, position: number, compilerOptions: CompilerOptions, host: LanguageServiceHost): readonly PathCompletion[] | undefined { - const token = getTokenAtPosition(sourceFile, position); - const commentRanges = getLeadingCommentRanges(sourceFile.text, token.pos); - const range = commentRanges && find(commentRanges, commentRange => position >= commentRange.pos && position <= commentRange.end); - if (!range) { - return undefined; - } - const text = sourceFile.text.slice(range.pos, position); - const match = tripleSlashDirectiveFragmentRegex.exec(text); - if (!match) { - return undefined; - } - - const [, prefix, kind, toComplete] = match; - const scriptPath = getDirectoryPath(sourceFile.path); - const names = kind === "path" ? getCompletionEntriesForDirectoryFragment(toComplete, scriptPath, getExtensionOptions(compilerOptions, /*includeExtensions*/ true), host, sourceFile.path) - : kind === "types" ? getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, getFragmentDirectory(toComplete), getExtensionOptions(compilerOptions)) +} +/* @internal */ +function getModulesForPathsPattern(fragment: string, baseUrl: string, pattern: string, fileExtensions: readonly string[], host: LanguageServiceHost): readonly NameAndKind[] | undefined { + if (!host.readDirectory) { + return undefined; + } + const parsed = hasZeroOrOneAsteriskCharacter(pattern) ? tryParsePattern(pattern) : undefined; + if (!parsed) { + return undefined; + } + // The prefix has two effective parts: the directory path and the base component after the filepath that is not a + // full directory component. For example: directory/path/of/prefix/base* + const normalizedPrefix = resolvePath(parsed.prefix); + const normalizedPrefixDirectory = hasTrailingDirectorySeparator(parsed.prefix) ? normalizedPrefix : getDirectoryPath(normalizedPrefix); + const normalizedPrefixBase = hasTrailingDirectorySeparator(parsed.prefix) ? "" : getBaseFileName(normalizedPrefix); + const fragmentHasPath = containsSlash(fragment); + const fragmentDirectory = fragmentHasPath ? hasTrailingDirectorySeparator(fragment) ? fragment : getDirectoryPath(fragment) : undefined; + // Try and expand the prefix to include any path from the fragment so that we can limit the readDirectory call + const expandedPrefixDirectory = fragmentHasPath ? combinePaths(normalizedPrefixDirectory, normalizedPrefixBase + fragmentDirectory) : normalizedPrefixDirectory; + const normalizedSuffix = normalizePath(parsed.suffix); + // Need to normalize after combining: If we combinePaths("a", "../b"), we want "b" and not "a/../b". + const baseDirectory = normalizePath(combinePaths(baseUrl, expandedPrefixDirectory)); + const completePrefix = fragmentHasPath ? baseDirectory : ensureTrailingDirectorySeparator(baseDirectory) + normalizedPrefixBase; + // If we have a suffix, then we need to read the directory all the way down. We could create a glob + // that encodes the suffix, but we would have to escape the character "?" which readDirectory + // doesn't support. For now, this is safer but slower + const includeGlob = normalizedSuffix ? "**/*" : "./*"; + const matches = mapDefined(tryReadDirectory(host, baseDirectory, fileExtensions, /*exclude*/ undefined, [includeGlob]), match => { + const extension = tryGetExtensionFromPath(match); + const name = trimPrefixAndSuffix(match); + return name === undefined ? undefined : nameAndKind(removeFileExtension(name), ScriptElementKind.scriptElement, extension); + }); + const directories = mapDefined(tryGetDirectories(host, baseDirectory).map(d => combinePaths(baseDirectory, d)), dir => { + const name = trimPrefixAndSuffix(dir); + return name === undefined ? undefined : directoryResult(name); + }); + return [...matches, ...directories]; + function trimPrefixAndSuffix(path: string): string | undefined { + const inner = withoutStartAndEnd(normalizePath(path), completePrefix, normalizedSuffix); + return inner === undefined ? undefined : removeLeadingDirectorySeparator(inner); + } +} +/* @internal */ +function withoutStartAndEnd(s: string, start: string, end: string): string | undefined { + return startsWith(s, start) && endsWith(s, end) ? s.slice(start.length, s.length - end.length) : undefined; +} +/* @internal */ +function removeLeadingDirectorySeparator(path: string): string { + return path[0] === directorySeparator ? path.slice(1) : path; +} +/* @internal */ +function getAmbientModuleCompletions(fragment: string, fragmentDirectory: string | undefined, checker: TypeChecker): readonly string[] { + // Get modules that the type checker picked up + const ambientModules = checker.getAmbientModules().map(sym => stripQuotes(sym.name)); + const nonRelativeModuleNames = ambientModules.filter(moduleName => startsWith(moduleName, fragment)); + // Nested modules of the form "module-name/sub" need to be adjusted to only return the string + // after the last '/' that appears in the fragment because that's where the replacement span + // starts + if (fragmentDirectory !== undefined) { + const moduleNameWithSeparator = ensureTrailingDirectorySeparator(fragmentDirectory); + return nonRelativeModuleNames.map(nonRelativeModuleName => removePrefix(nonRelativeModuleName, moduleNameWithSeparator)); + } + return nonRelativeModuleNames; +} +/* @internal */ +function getTripleSlashReferenceCompletion(sourceFile: SourceFile, position: number, compilerOptions: CompilerOptions, host: LanguageServiceHost): readonly PathCompletion[] | undefined { + const token = getTokenAtPosition(sourceFile, position); + const commentRanges = getLeadingCommentRanges(sourceFile.text, token.pos); + const range = commentRanges && find(commentRanges, commentRange => position >= commentRange.pos && position <= commentRange.end); + if (!range) { + return undefined; + } + const text = sourceFile.text.slice(range.pos, position); + const match = tripleSlashDirectiveFragmentRegex.exec(text); + if (!match) { + return undefined; + } + const [, prefix, kind, toComplete] = match; + const scriptPath = getDirectoryPath(sourceFile.path); + const names = kind === "path" ? getCompletionEntriesForDirectoryFragment(toComplete, scriptPath, getExtensionOptions(compilerOptions, /*includeExtensions*/ true), host, sourceFile.path) + : kind === "types" ? getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, getFragmentDirectory(toComplete), getExtensionOptions(compilerOptions)) : Debug.fail(); - return addReplacementSpans(toComplete, range.pos + prefix.length, names); - } - - function getCompletionEntriesFromTypings(host: LanguageServiceHost, options: CompilerOptions, scriptPath: string, fragmentDirectory: string | undefined, extensionOptions: ExtensionOptions, result: NameAndKind[] = []): readonly NameAndKind[] { - // Check for typings specified in compiler options - const seen = createMap(); - - const typeRoots = tryAndIgnoreErrors(() => getEffectiveTypeRoots(options, host)) || emptyArray; - - for (const root of typeRoots) { - getCompletionEntriesFromDirectories(root); - } - - // Also get all @types typings installed in visible node_modules directories - for (const packageJson of findPackageJsons(scriptPath, host)) { - const typesDir = combinePaths(getDirectoryPath(packageJson), "node_modules/@types"); - getCompletionEntriesFromDirectories(typesDir); - } - - return result; - - function getCompletionEntriesFromDirectories(directory: string): void { - if (!tryDirectoryExists(host, directory)) return; - - for (const typeDirectoryName of tryGetDirectories(host, directory)) { - const packageName = unmangleScopedPackageName(typeDirectoryName); - if (options.types && !contains(options.types, packageName)) continue; - - if (fragmentDirectory === undefined) { - if (!seen.has(packageName)) { - result.push(nameAndKind(packageName, ScriptElementKind.externalModuleName, /*extension*/ undefined)); - seen.set(packageName, true); - } + return addReplacementSpans(toComplete, range.pos + prefix.length, names); +} +/* @internal */ +function getCompletionEntriesFromTypings(host: LanguageServiceHost, options: CompilerOptions, scriptPath: string, fragmentDirectory: string | undefined, extensionOptions: ExtensionOptions, result: NameAndKind[] = []): readonly NameAndKind[] { + // Check for typings specified in compiler options + const seen = createMap(); + const typeRoots = tryAndIgnoreErrors(() => getEffectiveTypeRoots(options, host)) || emptyArray; + for (const root of typeRoots) { + getCompletionEntriesFromDirectories(root); + } + // Also get all @types typings installed in visible node_modules directories + for (const packageJson of findPackageJsons(scriptPath, host)) { + const typesDir = combinePaths(getDirectoryPath(packageJson), "node_modules/@types"); + getCompletionEntriesFromDirectories(typesDir); + } + return result; + function getCompletionEntriesFromDirectories(directory: string): void { + if (!tryDirectoryExists(host, directory)) + return; + for (const typeDirectoryName of tryGetDirectories(host, directory)) { + const packageName = unmangleScopedPackageName(typeDirectoryName); + if (options.types && !contains(options.types, packageName)) + continue; + if (fragmentDirectory === undefined) { + if (!seen.has(packageName)) { + result.push(nameAndKind(packageName, ScriptElementKind.externalModuleName, /*extension*/ undefined)); + seen.set(packageName, true); } - else { - const baseDirectory = combinePaths(directory, typeDirectoryName); - const remainingFragment = tryRemoveDirectoryPrefix(fragmentDirectory, packageName, hostGetCanonicalFileName(host)); - if (remainingFragment !== undefined) { - getCompletionEntriesForDirectoryFragment(remainingFragment, baseDirectory, extensionOptions, host, /*exclude*/ undefined, result); - } + } + else { + const baseDirectory = combinePaths(directory, typeDirectoryName); + const remainingFragment = tryRemoveDirectoryPrefix(fragmentDirectory, packageName, hostGetCanonicalFileName(host)); + if (remainingFragment !== undefined) { + getCompletionEntriesForDirectoryFragment(remainingFragment, baseDirectory, extensionOptions, host, /*exclude*/ undefined, result); } } } } - - function enumerateNodeModulesVisibleToScript(host: LanguageServiceHost, scriptPath: string): readonly string[] { - if (!host.readFile || !host.fileExists) return emptyArray; - - const result: string[] = []; - for (const packageJson of findPackageJsons(scriptPath, host)) { - const contents = readJson(packageJson, host as { readFile: (filename: string) => string | undefined }); // Cast to assert that readFile is defined - // Provide completions for all non @types dependencies - for (const key of nodeModulesDependencyKeys) { - const dependencies: object | undefined = (contents as any)[key]; - if (!dependencies) continue; - for (const dep in dependencies) { - if (dependencies.hasOwnProperty(dep) && !startsWith(dep, "@types/")) { - result.push(dep); - } +} +/* @internal */ +function enumerateNodeModulesVisibleToScript(host: LanguageServiceHost, scriptPath: string): readonly string[] { + if (!host.readFile || !host.fileExists) + return emptyArray; + const result: string[] = []; + for (const packageJson of findPackageJsons(scriptPath, host)) { + const contents = readJson(packageJson, (host as { + readFile: (filename: string) => string | undefined; + })); // Cast to assert that readFile is defined + // Provide completions for all non @types dependencies + for (const key of nodeModulesDependencyKeys) { + const dependencies: object | undefined = (contents as any)[key]; + if (!dependencies) + continue; + for (const dep in dependencies) { + if (dependencies.hasOwnProperty(dep) && !startsWith(dep, "@types/")) { + result.push(dep); } } } - return result; } - - // Replace everything after the last directory separator that appears - function getDirectoryFragmentTextSpan(text: string, textStart: number): TextSpan | undefined { - const index = Math.max(text.lastIndexOf(directorySeparator), text.lastIndexOf("\\")); - const offset = index !== -1 ? index + 1 : 0; - // If the range is an identifier, span is unnecessary. - const length = text.length - offset; - return length === 0 || isIdentifierText(text.substr(offset, length), ScriptTarget.ESNext) ? undefined : createTextSpan(textStart + offset, length); - } - - // Returns true if the path is explicitly relative to the script (i.e. relative to . or ..) - function isPathRelativeToScript(path: string) { - if (path && path.length >= 2 && path.charCodeAt(0) === CharacterCodes.dot) { - const slashIndex = path.length >= 3 && path.charCodeAt(1) === CharacterCodes.dot ? 2 : 1; - const slashCharCode = path.charCodeAt(slashIndex); - return slashCharCode === CharacterCodes.slash || slashCharCode === CharacterCodes.backslash; - } - return false; - } - - /** - * Matches a triple slash reference directive with an incomplete string literal for its path. Used - * to determine if the caret is currently within the string literal and capture the literal fragment - * for completions. - * For example, this matches - * - * /// = 2 && path.charCodeAt(0) === CharacterCodes.dot) { + const slashIndex = path.length >= 3 && path.charCodeAt(1) === CharacterCodes.dot ? 2 : 1; + const slashCharCode = path.charCodeAt(slashIndex); + return slashCharCode === CharacterCodes.slash || slashCharCode === CharacterCodes.backslash; } + return false; +} +/** + * Matches a triple slash reference directive with an incomplete string literal for its path. Used + * to determine if the caret is currently within the string literal and capture the literal fragment + * for completions. + * For example, this matches + * + * /// (); - - export function computeSuggestionDiagnostics(sourceFile: SourceFile, program: Program, cancellationToken: CancellationToken): DiagnosticWithLocation[] { - program.getSemanticDiagnostics(sourceFile, cancellationToken); - const diags: DiagnosticWithLocation[] = []; - const checker = program.getTypeChecker(); - - if (sourceFile.commonJsModuleIndicator && - (programContainsEs6Modules(program) || compilerOptionsIndicateEs6Modules(program.getCompilerOptions())) && - containsTopLevelCommonjs(sourceFile)) { - diags.push(createDiagnosticForNode(getErrorNodeFromCommonJsIndicator(sourceFile.commonJsModuleIndicator), Diagnostics.File_is_a_CommonJS_module_it_may_be_converted_to_an_ES6_module)); - } - - const isJsFile = isSourceFileJS(sourceFile); - - visitedNestedConvertibleFunctions.clear(); - check(sourceFile); - - if (getAllowSyntheticDefaultImports(program.getCompilerOptions())) { - for (const moduleSpecifier of sourceFile.imports) { - const importNode = importFromModuleSpecifier(moduleSpecifier); - const name = importNameForConvertToDefaultImport(importNode); - if (!name) continue; - const module = getResolvedModule(sourceFile, moduleSpecifier.text); - const resolvedFile = module && program.getSourceFile(module.resolvedFileName); - if (resolvedFile && resolvedFile.externalModuleIndicator && isExportAssignment(resolvedFile.externalModuleIndicator) && resolvedFile.externalModuleIndicator.isExportEquals) { - diags.push(createDiagnosticForNode(name, Diagnostics.Import_may_be_converted_to_a_default_import)); - } +const visitedNestedConvertibleFunctions = createMap(); +/* @internal */ +export function computeSuggestionDiagnostics(sourceFile: SourceFile, program: Program, cancellationToken: CancellationToken): DiagnosticWithLocation[] { + program.getSemanticDiagnostics(sourceFile, cancellationToken); + const diags: DiagnosticWithLocation[] = []; + const checker = program.getTypeChecker(); + if (sourceFile.commonJsModuleIndicator && + (programContainsEs6Modules(program) || compilerOptionsIndicateEs6Modules(program.getCompilerOptions())) && + containsTopLevelCommonjs(sourceFile)) { + diags.push(createDiagnosticForNode(getErrorNodeFromCommonJsIndicator(sourceFile.commonJsModuleIndicator), Diagnostics.File_is_a_CommonJS_module_it_may_be_converted_to_an_ES6_module)); + } + const isJsFile = isSourceFileJS(sourceFile); + visitedNestedConvertibleFunctions.clear(); + check(sourceFile); + if (getAllowSyntheticDefaultImports(program.getCompilerOptions())) { + for (const moduleSpecifier of sourceFile.imports) { + const importNode = importFromModuleSpecifier(moduleSpecifier); + const name = importNameForConvertToDefaultImport(importNode); + if (!name) + continue; + const module = getResolvedModule(sourceFile, moduleSpecifier.text); + const resolvedFile = module && program.getSourceFile(module.resolvedFileName); + if (resolvedFile && resolvedFile.externalModuleIndicator && isExportAssignment(resolvedFile.externalModuleIndicator) && resolvedFile.externalModuleIndicator.isExportEquals) { + diags.push(createDiagnosticForNode(name, Diagnostics.Import_may_be_converted_to_a_default_import)); } } - - addRange(diags, sourceFile.bindSuggestionDiagnostics); - addRange(diags, program.getSuggestionDiagnostics(sourceFile, cancellationToken)); - return diags.sort((d1, d2) => d1.start - d2.start); - - function check(node: Node) { - if (isJsFile) { - if (canBeConvertedToClass(node)) { - diags.push(createDiagnosticForNode(isVariableDeclaration(node.parent) ? node.parent.name : node, Diagnostics.This_constructor_function_may_be_converted_to_a_class_declaration)); - } + } + addRange(diags, sourceFile.bindSuggestionDiagnostics); + addRange(diags, program.getSuggestionDiagnostics(sourceFile, cancellationToken)); + return diags.sort((d1, d2) => d1.start - d2.start); + function check(node: Node) { + if (isJsFile) { + if (canBeConvertedToClass(node)) { + diags.push(createDiagnosticForNode(isVariableDeclaration(node.parent) ? node.parent.name : node, Diagnostics.This_constructor_function_may_be_converted_to_a_class_declaration)); } - else { - if (isVariableStatement(node) && - node.parent === sourceFile && - node.declarationList.flags & NodeFlags.Const && - node.declarationList.declarations.length === 1) { - const init = node.declarationList.declarations[0].initializer; - if (init && isRequireCall(init, /*checkArgumentIsStringLiteralLike*/ true)) { - diags.push(createDiagnosticForNode(init, Diagnostics.require_call_may_be_converted_to_an_import)); - } - } - - if (codefix.parameterShouldGetTypeFromJSDoc(node)) { - diags.push(createDiagnosticForNode(node.name || node, Diagnostics.JSDoc_types_may_be_moved_to_TypeScript_types)); + } + else { + if (isVariableStatement(node) && + node.parent === sourceFile && + node.declarationList.flags & NodeFlags.Const && + node.declarationList.declarations.length === 1) { + const init = node.declarationList.declarations[0].initializer; + if (init && isRequireCall(init, /*checkArgumentIsStringLiteralLike*/ true)) { + diags.push(createDiagnosticForNode(init, Diagnostics.require_call_may_be_converted_to_an_import)); } } - - if (isFunctionLikeDeclaration(node)) { - addConvertToAsyncFunctionDiagnostics(node, checker, diags); + if (parameterShouldGetTypeFromJSDoc(node)) { + diags.push(createDiagnosticForNode(node.name || node, Diagnostics.JSDoc_types_may_be_moved_to_TypeScript_types)); } - node.forEachChild(check); } + if (isFunctionLikeDeclaration(node)) { + addConvertToAsyncFunctionDiagnostics(node, checker, diags); + } + node.forEachChild(check); } - - // convertToEs6Module only works on top-level, so don't trigger it if commonjs code only appears in nested scopes. - function containsTopLevelCommonjs(sourceFile: SourceFile): boolean { - return sourceFile.statements.some(statement => { - switch (statement.kind) { - case SyntaxKind.VariableStatement: - return (statement as VariableStatement).declarationList.declarations.some(decl => - !!decl.initializer && isRequireCall(propertyAccessLeftHandSide(decl.initializer), /*checkArgumentIsStringLiteralLike*/ true)); - case SyntaxKind.ExpressionStatement: { - const { expression } = statement as ExpressionStatement; - if (!isBinaryExpression(expression)) return isRequireCall(expression, /*checkArgumentIsStringLiteralLike*/ true); - const kind = getAssignmentDeclarationKind(expression); - return kind === AssignmentDeclarationKind.ExportsProperty || kind === AssignmentDeclarationKind.ModuleExports; - } - default: - return false; +} +// convertToEs6Module only works on top-level, so don't trigger it if commonjs code only appears in nested scopes. +/* @internal */ +function containsTopLevelCommonjs(sourceFile: SourceFile): boolean { + return sourceFile.statements.some(statement => { + switch (statement.kind) { + case SyntaxKind.VariableStatement: + return (statement as VariableStatement).declarationList.declarations.some(decl => !!decl.initializer && isRequireCall(propertyAccessLeftHandSide(decl.initializer), /*checkArgumentIsStringLiteralLike*/ true)); + case SyntaxKind.ExpressionStatement: { + const { expression } = (statement as ExpressionStatement); + if (!isBinaryExpression(expression)) + return isRequireCall(expression, /*checkArgumentIsStringLiteralLike*/ true); + const kind = getAssignmentDeclarationKind(expression); + return kind === AssignmentDeclarationKind.ExportsProperty || kind === AssignmentDeclarationKind.ModuleExports; } - }); - } - - function propertyAccessLeftHandSide(node: Expression): Expression { - return isPropertyAccessExpression(node) ? propertyAccessLeftHandSide(node.expression) : node; - } - - function importNameForConvertToDefaultImport(node: AnyValidImportOrReExport): Identifier | undefined { - switch (node.kind) { - case SyntaxKind.ImportDeclaration: - const { importClause, moduleSpecifier } = node; - return importClause && !importClause.name && importClause.namedBindings && importClause.namedBindings.kind === SyntaxKind.NamespaceImport && isStringLiteral(moduleSpecifier) - ? importClause.namedBindings.name - : undefined; - case SyntaxKind.ImportEqualsDeclaration: - return node.name; default: - return undefined; - } - } - - function addConvertToAsyncFunctionDiagnostics(node: FunctionLikeDeclaration, checker: TypeChecker, diags: Push): void { - // need to check function before checking map so that deeper levels of nested callbacks are checked - if (isConvertibleFunction(node, checker) && !visitedNestedConvertibleFunctions.has(getKeyFromNode(node))) { - diags.push(createDiagnosticForNode( - !node.name && isVariableDeclaration(node.parent) && isIdentifier(node.parent.name) ? node.parent.name : node, - Diagnostics.This_may_be_converted_to_an_async_function)); + return false; } + }); +} +/* @internal */ +function propertyAccessLeftHandSide(node: Expression): Expression { + return isPropertyAccessExpression(node) ? propertyAccessLeftHandSide(node.expression) : node; +} +/* @internal */ +function importNameForConvertToDefaultImport(node: AnyValidImportOrReExport): Identifier | undefined { + switch (node.kind) { + case SyntaxKind.ImportDeclaration: + const { importClause, moduleSpecifier } = node; + return importClause && !importClause.name && importClause.namedBindings && importClause.namedBindings.kind === SyntaxKind.NamespaceImport && isStringLiteral(moduleSpecifier) + ? importClause.namedBindings.name + : undefined; + case SyntaxKind.ImportEqualsDeclaration: + return node.name; + default: + return undefined; } - - function isConvertibleFunction(node: FunctionLikeDeclaration, checker: TypeChecker) { - return !isAsyncFunction(node) && - node.body && - isBlock(node.body) && - hasReturnStatementWithPromiseHandler(node.body) && - returnsPromise(node, checker); - } - - function returnsPromise(node: FunctionLikeDeclaration, checker: TypeChecker): boolean { - const functionType = checker.getTypeAtLocation(node); - const callSignatures = checker.getSignaturesOfType(functionType, SignatureKind.Call); - const returnType = callSignatures.length ? checker.getReturnTypeOfSignature(callSignatures[0]) : undefined; - return !!returnType && !!checker.getPromisedTypeOfPromise(returnType); - } - - function getErrorNodeFromCommonJsIndicator(commonJsModuleIndicator: Node): Node { - return isBinaryExpression(commonJsModuleIndicator) ? commonJsModuleIndicator.left : commonJsModuleIndicator; - } - - function hasReturnStatementWithPromiseHandler(body: Block): boolean { - return !!forEachReturnStatement(body, isReturnStatementWithFixablePromiseHandler); +} +/* @internal */ +function addConvertToAsyncFunctionDiagnostics(node: FunctionLikeDeclaration, checker: TypeChecker, diags: Push): void { + // need to check function before checking map so that deeper levels of nested callbacks are checked + if (isConvertibleFunction(node, checker) && !visitedNestedConvertibleFunctions.has(getKeyFromNode(node))) { + diags.push(createDiagnosticForNode(!node.name && isVariableDeclaration(node.parent) && isIdentifier(node.parent.name) ? node.parent.name : node, Diagnostics.This_may_be_converted_to_an_async_function)); } - - export function isReturnStatementWithFixablePromiseHandler(node: Node): node is ReturnStatement { - return isReturnStatement(node) && !!node.expression && isFixablePromiseHandler(node.expression); +} +/* @internal */ +function isConvertibleFunction(node: FunctionLikeDeclaration, checker: TypeChecker) { + return !isAsyncFunction(node) && + node.body && + isBlock(node.body) && + hasReturnStatementWithPromiseHandler(node.body) && + returnsPromise(node, checker); +} +/* @internal */ +function returnsPromise(node: FunctionLikeDeclaration, checker: TypeChecker): boolean { + const functionType = checker.getTypeAtLocation(node); + const callSignatures = checker.getSignaturesOfType(functionType, SignatureKind.Call); + const returnType = callSignatures.length ? checker.getReturnTypeOfSignature(callSignatures[0]) : undefined; + return !!returnType && !!checker.getPromisedTypeOfPromise(returnType); +} +/* @internal */ +function getErrorNodeFromCommonJsIndicator(commonJsModuleIndicator: Node): Node { + return isBinaryExpression(commonJsModuleIndicator) ? commonJsModuleIndicator.left : commonJsModuleIndicator; +} +/* @internal */ +function hasReturnStatementWithPromiseHandler(body: Block): boolean { + return !!forEachReturnStatement(body, isReturnStatementWithFixablePromiseHandler); +} +/* @internal */ +export function isReturnStatementWithFixablePromiseHandler(node: Node): node is ReturnStatement { + return isReturnStatement(node) && !!node.expression && isFixablePromiseHandler(node.expression); +} +// Should be kept up to date with transformExpression in convertToAsyncFunction.ts +/* @internal */ +export function isFixablePromiseHandler(node: Node): boolean { + // ensure outermost call exists and is a promise handler + if (!isPromiseHandler(node) || !node.arguments.every(isFixablePromiseArgument)) { + return false; } - - // Should be kept up to date with transformExpression in convertToAsyncFunction.ts - export function isFixablePromiseHandler(node: Node): boolean { - // ensure outermost call exists and is a promise handler - if (!isPromiseHandler(node) || !node.arguments.every(isFixablePromiseArgument)) { + // ensure all chained calls are valid + let currentNode = node.expression; + while (isPromiseHandler(currentNode) || isPropertyAccessExpression(currentNode)) { + if (isCallExpression(currentNode) && !currentNode.arguments.every(isFixablePromiseArgument)) { return false; } - - // ensure all chained calls are valid - let currentNode = node.expression; - while (isPromiseHandler(currentNode) || isPropertyAccessExpression(currentNode)) { - if (isCallExpression(currentNode) && !currentNode.arguments.every(isFixablePromiseArgument)) { - return false; - } - currentNode = currentNode.expression; - } - return true; + currentNode = currentNode.expression; } - - function isPromiseHandler(node: Node): node is CallExpression { - return isCallExpression(node) && (hasPropertyAccessExpressionWithName(node, "then") || hasPropertyAccessExpressionWithName(node, "catch")); + return true; +} +/* @internal */ +function isPromiseHandler(node: Node): node is CallExpression { + return isCallExpression(node) && (hasPropertyAccessExpressionWithName(node, "then") || hasPropertyAccessExpressionWithName(node, "catch")); +} +// should be kept up to date with getTransformationBody in convertToAsyncFunction.ts +/* @internal */ +function isFixablePromiseArgument(arg: Expression): boolean { + switch (arg.kind) { + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.FunctionExpression: + case SyntaxKind.ArrowFunction: + visitedNestedConvertibleFunctions.set(getKeyFromNode((arg as FunctionLikeDeclaration)), true); + // falls through + case SyntaxKind.NullKeyword: + case SyntaxKind.Identifier: // identifier includes undefined + return true; + default: + return false; } - - // should be kept up to date with getTransformationBody in convertToAsyncFunction.ts - function isFixablePromiseArgument(arg: Expression): boolean { - switch (arg.kind) { - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.FunctionExpression: - case SyntaxKind.ArrowFunction: - visitedNestedConvertibleFunctions.set(getKeyFromNode(arg as FunctionLikeDeclaration), true); - // falls through - case SyntaxKind.NullKeyword: - case SyntaxKind.Identifier: // identifier includes undefined - return true; - default: - return false; +} +/* @internal */ +function getKeyFromNode(exp: FunctionLikeDeclaration) { + return `${exp.pos.toString()}:${exp.end.toString()}`; +} +/* @internal */ +function canBeConvertedToClass(node: Node): boolean { + if (node.kind === SyntaxKind.FunctionExpression) { + if (isVariableDeclaration(node.parent) && node.symbol.members?.size) { + return true; } + const decl = getDeclarationOfExpando(node); + const symbol = decl?.symbol; + return !!(symbol && (symbol.exports?.size || symbol.members?.size)); } - - function getKeyFromNode(exp: FunctionLikeDeclaration) { - return `${exp.pos.toString()}:${exp.end.toString()}`; - } - - function canBeConvertedToClass(node: Node): boolean { - if (node.kind === SyntaxKind.FunctionExpression) { - if (isVariableDeclaration(node.parent) && node.symbol.members?.size) { - return true; - } - - const decl = getDeclarationOfExpando(node); - const symbol = decl?.symbol; - return !!(symbol && (symbol.exports?.size || symbol.members?.size)); - } - - if (node.kind === SyntaxKind.FunctionDeclaration) { - return !!node.symbol.members?.size; - } - - return false; + if (node.kind === SyntaxKind.FunctionDeclaration) { + return !!node.symbol.members?.size; } + return false; } diff --git a/src/services/symbolDisplay.ts b/src/services/symbolDisplay.ts index a51c159f29763..1626ffbf5c350 100644 --- a/src/services/symbolDisplay.ts +++ b/src/services/symbolDisplay.ts @@ -1,668 +1,621 @@ +import { TypeChecker, Symbol, Node, ScriptElementKind, getCombinedLocalAndExportSymbolFlags, SymbolFlags, getDeclarationOfKind, SyntaxKind, first, isExpression, isFirstDeclarationOfSymbolParameter, isVarConst, VariableDeclaration, forEach, isLet, TransientSymbol, CheckFlags, Debug, getNodeModifiers, ScriptElementKindModifier, SymbolDisplayPart, JSDocTagInfo, SourceFile, getMeaningFromLocation, SemanticMeaning, isInExpressionContext, Type, Printer, keywordPart, Signature, PropertyAccessExpression, CallExpression, NewExpression, JsxOpeningLikeElement, isCallOrNewExpression, isCallExpressionTarget, isNewExpressionTarget, isJsxOpeningLikeElement, isFunctionLike, isCallExpression, contains, spacePart, punctuationPart, getObjectFlags, ObjectFlags, addRange, symbolToDisplayParts, SymbolFormatFlags, lineBreakPart, TypeFormatFlags, isNameOfFunctionDeclaration, FunctionLike, find, operatorPart, typeToDisplayParts, some, isEnumDeclaration, isEnumConst, ModuleDeclaration, textPart, isFunctionLikeKind, SignatureDeclaration, signatureToDisplayParts, EnumMember, displayPart, getTextOfConstantValue, SymbolDisplayPartKind, getNameOfDeclaration, isModuleWithStringLiteralName, hasModifier, ModifierFlags, getSourceFileOfNode, ExportAssignment, ImportEqualsDeclaration, isExternalModuleImportEqualsDeclaration, getTextOfNode, getExternalModuleImportEqualsDeclarationExpression, mapToDisplayParts, TypeParameter, EmitHint, getParseTreeNode, BinaryExpression, createPrinter, isArrowFunction, isFunctionExpression, isClassExpression, textOrKeywordPart, ListFormat, isFunctionBlock } from "./ts"; /* @internal */ -namespace ts.SymbolDisplay { - // TODO(drosen): use contextual SemanticMeaning. - export function getSymbolKind(typeChecker: TypeChecker, symbol: Symbol, location: Node): ScriptElementKind { - const result = getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker, symbol, location); - if (result !== ScriptElementKind.unknown) { - return result; - } - - const flags = getCombinedLocalAndExportSymbolFlags(symbol); - if (flags & SymbolFlags.Class) { - return getDeclarationOfKind(symbol, SyntaxKind.ClassExpression) ? - ScriptElementKind.localClassElement : ScriptElementKind.classElement; - } - if (flags & SymbolFlags.Enum) return ScriptElementKind.enumElement; - if (flags & SymbolFlags.TypeAlias) return ScriptElementKind.typeElement; - if (flags & SymbolFlags.Interface) return ScriptElementKind.interfaceElement; - if (flags & SymbolFlags.TypeParameter) return ScriptElementKind.typeParameterElement; - if (flags & SymbolFlags.EnumMember) return ScriptElementKind.enumMemberElement; - if (flags & SymbolFlags.Alias) return ScriptElementKind.alias; - if (flags & SymbolFlags.Module) return ScriptElementKind.moduleElement; - +// TODO(drosen): use contextual SemanticMeaning. +export function getSymbolKind(typeChecker: TypeChecker, symbol: Symbol, location: Node): ScriptElementKind { + const result = getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker, symbol, location); + if (result !== ScriptElementKind.unknown) { return result; } - - function getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker: TypeChecker, symbol: Symbol, location: Node): ScriptElementKind { - const roots = typeChecker.getRootSymbols(symbol); - // If this is a method from a mapped type, leave as a method so long as it still has a call signature. - if (roots.length === 1 - && first(roots).flags & SymbolFlags.Method - // Ensure the mapped version is still a method, as opposed to `{ [K in keyof I]: number }`. - && typeChecker.getTypeOfSymbolAtLocation(symbol, location).getNonNullableType().getCallSignatures().length !== 0) { - return ScriptElementKind.memberFunctionElement; - } - - if (typeChecker.isUndefinedSymbol(symbol)) { - return ScriptElementKind.variableElement; + const flags = getCombinedLocalAndExportSymbolFlags(symbol); + if (flags & SymbolFlags.Class) { + return getDeclarationOfKind(symbol, SyntaxKind.ClassExpression) ? + ScriptElementKind.localClassElement : ScriptElementKind.classElement; + } + if (flags & SymbolFlags.Enum) + return ScriptElementKind.enumElement; + if (flags & SymbolFlags.TypeAlias) + return ScriptElementKind.typeElement; + if (flags & SymbolFlags.Interface) + return ScriptElementKind.interfaceElement; + if (flags & SymbolFlags.TypeParameter) + return ScriptElementKind.typeParameterElement; + if (flags & SymbolFlags.EnumMember) + return ScriptElementKind.enumMemberElement; + if (flags & SymbolFlags.Alias) + return ScriptElementKind.alias; + if (flags & SymbolFlags.Module) + return ScriptElementKind.moduleElement; + return result; +} +/* @internal */ +function getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker: TypeChecker, symbol: Symbol, location: Node): ScriptElementKind { + const roots = typeChecker.getRootSymbols(symbol); + // If this is a method from a mapped type, leave as a method so long as it still has a call signature. + if (roots.length === 1 + && first(roots).flags & SymbolFlags.Method + // Ensure the mapped version is still a method, as opposed to `{ [K in keyof I]: number }`. + && typeChecker.getTypeOfSymbolAtLocation(symbol, location).getNonNullableType().getCallSignatures().length !== 0) { + return ScriptElementKind.memberFunctionElement; + } + if (typeChecker.isUndefinedSymbol(symbol)) { + return ScriptElementKind.variableElement; + } + if (typeChecker.isArgumentsSymbol(symbol)) { + return ScriptElementKind.localVariableElement; + } + if (location.kind === SyntaxKind.ThisKeyword && isExpression(location)) { + return ScriptElementKind.parameterElement; + } + const flags = getCombinedLocalAndExportSymbolFlags(symbol); + if (flags & SymbolFlags.Variable) { + if (isFirstDeclarationOfSymbolParameter(symbol)) { + return ScriptElementKind.parameterElement; } - if (typeChecker.isArgumentsSymbol(symbol)) { - return ScriptElementKind.localVariableElement; + else if (symbol.valueDeclaration && isVarConst((symbol.valueDeclaration as VariableDeclaration))) { + return ScriptElementKind.constElement; } - if (location.kind === SyntaxKind.ThisKeyword && isExpression(location)) { - return ScriptElementKind.parameterElement; + else if (forEach(symbol.declarations, isLet)) { + return ScriptElementKind.letElement; } - const flags = getCombinedLocalAndExportSymbolFlags(symbol); - if (flags & SymbolFlags.Variable) { - if (isFirstDeclarationOfSymbolParameter(symbol)) { - return ScriptElementKind.parameterElement; - } - else if (symbol.valueDeclaration && isVarConst(symbol.valueDeclaration as VariableDeclaration)) { - return ScriptElementKind.constElement; - } - else if (forEach(symbol.declarations, isLet)) { - return ScriptElementKind.letElement; - } - return isLocalVariableOrFunction(symbol) ? ScriptElementKind.localVariableElement : ScriptElementKind.variableElement; - } - if (flags & SymbolFlags.Function) return isLocalVariableOrFunction(symbol) ? ScriptElementKind.localFunctionElement : ScriptElementKind.functionElement; - if (flags & SymbolFlags.GetAccessor) return ScriptElementKind.memberGetAccessorElement; - if (flags & SymbolFlags.SetAccessor) return ScriptElementKind.memberSetAccessorElement; - if (flags & SymbolFlags.Method) return ScriptElementKind.memberFunctionElement; - if (flags & SymbolFlags.Constructor) return ScriptElementKind.constructorImplementationElement; - - if (flags & SymbolFlags.Property) { - if (flags & SymbolFlags.Transient && (symbol).checkFlags & CheckFlags.Synthetic) { - // If union property is result of union of non method (property/accessors/variables), it is labeled as property - const unionPropertyKind = forEach(typeChecker.getRootSymbols(symbol), rootSymbol => { - const rootSymbolFlags = rootSymbol.getFlags(); - if (rootSymbolFlags & (SymbolFlags.PropertyOrAccessor | SymbolFlags.Variable)) { - return ScriptElementKind.memberVariableElement; - } - // May be a Function if this was from `typeof N` with `namespace N { function f();. }`. - Debug.assert(!!(rootSymbolFlags & (SymbolFlags.Method | SymbolFlags.Function))); - }); - if (!unionPropertyKind) { - // If this was union of all methods, - // make sure it has call signatures before we can label it as method - const typeOfUnionProperty = typeChecker.getTypeOfSymbolAtLocation(symbol, location); - if (typeOfUnionProperty.getCallSignatures().length) { - return ScriptElementKind.memberFunctionElement; - } + return isLocalVariableOrFunction(symbol) ? ScriptElementKind.localVariableElement : ScriptElementKind.variableElement; + } + if (flags & SymbolFlags.Function) + return isLocalVariableOrFunction(symbol) ? ScriptElementKind.localFunctionElement : ScriptElementKind.functionElement; + if (flags & SymbolFlags.GetAccessor) + return ScriptElementKind.memberGetAccessorElement; + if (flags & SymbolFlags.SetAccessor) + return ScriptElementKind.memberSetAccessorElement; + if (flags & SymbolFlags.Method) + return ScriptElementKind.memberFunctionElement; + if (flags & SymbolFlags.Constructor) + return ScriptElementKind.constructorImplementationElement; + if (flags & SymbolFlags.Property) { + if (flags & SymbolFlags.Transient && (symbol).checkFlags & CheckFlags.Synthetic) { + // If union property is result of union of non method (property/accessors/variables), it is labeled as property + const unionPropertyKind = forEach(typeChecker.getRootSymbols(symbol), rootSymbol => { + const rootSymbolFlags = rootSymbol.getFlags(); + if (rootSymbolFlags & (SymbolFlags.PropertyOrAccessor | SymbolFlags.Variable)) { return ScriptElementKind.memberVariableElement; } - return unionPropertyKind; - } - // If we requested completions after `x.` at the top-level, we may be at a source file location. - switch (location.parent && location.parent.kind) { - // If we've typed a character of the attribute name, will be 'JsxAttribute', else will be 'JsxOpeningElement'. - case SyntaxKind.JsxOpeningElement: - case SyntaxKind.JsxElement: - case SyntaxKind.JsxSelfClosingElement: - return location.kind === SyntaxKind.Identifier ? ScriptElementKind.memberVariableElement : ScriptElementKind.jsxAttribute; - case SyntaxKind.JsxAttribute: - return ScriptElementKind.jsxAttribute; - default: - return ScriptElementKind.memberVariableElement; - } - } - - return ScriptElementKind.unknown; - } - - export function getSymbolModifiers(symbol: Symbol): string { - const nodeModifiers = symbol && symbol.declarations && symbol.declarations.length > 0 - ? getNodeModifiers(symbol.declarations[0]) - : ScriptElementKindModifier.none; - - const symbolModifiers = symbol && symbol.flags & SymbolFlags.Optional ? - ScriptElementKindModifier.optionalModifier - : ScriptElementKindModifier.none; - return nodeModifiers && symbolModifiers ? nodeModifiers + "," + symbolModifiers : nodeModifiers || symbolModifiers; - } - - interface SymbolDisplayPartsDocumentationAndSymbolKind { - displayParts: SymbolDisplayPart[]; - documentation: SymbolDisplayPart[]; - symbolKind: ScriptElementKind; - tags: JSDocTagInfo[] | undefined; - } - - // TODO(drosen): Currently completion entry details passes the SemanticMeaning.All instead of using semanticMeaning of location - export function getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker: TypeChecker, symbol: Symbol, sourceFile: SourceFile, enclosingDeclaration: Node | undefined, - location: Node, semanticMeaning = getMeaningFromLocation(location), alias?: Symbol): SymbolDisplayPartsDocumentationAndSymbolKind { - const displayParts: SymbolDisplayPart[] = []; - let documentation: SymbolDisplayPart[] = []; - let tags: JSDocTagInfo[] = []; - const symbolFlags = getCombinedLocalAndExportSymbolFlags(symbol); - let symbolKind = semanticMeaning & SemanticMeaning.Value ? getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker, symbol, location) : ScriptElementKind.unknown; - let hasAddedSymbolInfo = false; - const isThisExpression = location.kind === SyntaxKind.ThisKeyword && isInExpressionContext(location); - let type: Type | undefined; - let printer: Printer; - let documentationFromAlias: SymbolDisplayPart[] | undefined; - let tagsFromAlias: JSDocTagInfo[] | undefined; - let hasMultipleSignatures = false; - - if (location.kind === SyntaxKind.ThisKeyword && !isThisExpression) { - return { displayParts: [keywordPart(SyntaxKind.ThisKeyword)], documentation: [], symbolKind: ScriptElementKind.primitiveType, tags: undefined }; - } - - // Class at constructor site need to be shown as constructor apart from property,method, vars - if (symbolKind !== ScriptElementKind.unknown || symbolFlags & SymbolFlags.Class || symbolFlags & SymbolFlags.Alias) { - // If it is accessor they are allowed only if location is at name of the accessor - if (symbolKind === ScriptElementKind.memberGetAccessorElement || symbolKind === ScriptElementKind.memberSetAccessorElement) { - symbolKind = ScriptElementKind.memberVariableElement; - } - - let signature: Signature | undefined; - type = isThisExpression ? typeChecker.getTypeAtLocation(location) : typeChecker.getTypeOfSymbolAtLocation(symbol.exportSymbol || symbol, location); - - if (location.parent && location.parent.kind === SyntaxKind.PropertyAccessExpression) { - const right = (location.parent).name; - // Either the location is on the right of a property access, or on the left and the right is missing - if (right === location || (right && right.getFullWidth() === 0)) { - location = location.parent; + // May be a Function if this was from `typeof N` with `namespace N { function f();. }`. + Debug.assert(!!(rootSymbolFlags & (SymbolFlags.Method | SymbolFlags.Function))); + }); + if (!unionPropertyKind) { + // If this was union of all methods, + // make sure it has call signatures before we can label it as method + const typeOfUnionProperty = typeChecker.getTypeOfSymbolAtLocation(symbol, location); + if (typeOfUnionProperty.getCallSignatures().length) { + return ScriptElementKind.memberFunctionElement; } - } - - // try get the call/construct signature from the type if it matches - let callExpressionLike: CallExpression | NewExpression | JsxOpeningLikeElement | undefined; - if (isCallOrNewExpression(location)) { - callExpressionLike = location; - } - else if (isCallExpressionTarget(location) || isNewExpressionTarget(location)) { - callExpressionLike = location.parent; - } - else if (location.parent && isJsxOpeningLikeElement(location.parent) && isFunctionLike(symbol.valueDeclaration)) { - callExpressionLike = location.parent; - } - - if (callExpressionLike) { - signature = typeChecker.getResolvedSignature(callExpressionLike)!; // TODO: GH#18217 - - const useConstructSignatures = callExpressionLike.kind === SyntaxKind.NewExpression || (isCallExpression(callExpressionLike) && callExpressionLike.expression.kind === SyntaxKind.SuperKeyword); - - const allSignatures = useConstructSignatures ? type.getConstructSignatures() : type.getCallSignatures(); - - if (!contains(allSignatures, signature.target) && !contains(allSignatures, signature)) { - // Get the first signature if there is one -- allSignatures may contain - // either the original signature or its target, so check for either - signature = allSignatures.length ? allSignatures[0] : undefined; + return ScriptElementKind.memberVariableElement; + } + return unionPropertyKind; + } + // If we requested completions after `x.` at the top-level, we may be at a source file location. + switch (location.parent && location.parent.kind) { + // If we've typed a character of the attribute name, will be 'JsxAttribute', else will be 'JsxOpeningElement'. + case SyntaxKind.JsxOpeningElement: + case SyntaxKind.JsxElement: + case SyntaxKind.JsxSelfClosingElement: + return location.kind === SyntaxKind.Identifier ? ScriptElementKind.memberVariableElement : ScriptElementKind.jsxAttribute; + case SyntaxKind.JsxAttribute: + return ScriptElementKind.jsxAttribute; + default: + return ScriptElementKind.memberVariableElement; + } + } + return ScriptElementKind.unknown; +} +/* @internal */ +export function getSymbolModifiers(symbol: Symbol): string { + const nodeModifiers = symbol && symbol.declarations && symbol.declarations.length > 0 + ? getNodeModifiers(symbol.declarations[0]) + : ScriptElementKindModifier.none; + const symbolModifiers = symbol && symbol.flags & SymbolFlags.Optional ? + ScriptElementKindModifier.optionalModifier + : ScriptElementKindModifier.none; + return nodeModifiers && symbolModifiers ? nodeModifiers + "," + symbolModifiers : nodeModifiers || symbolModifiers; +} +/* @internal */ +interface SymbolDisplayPartsDocumentationAndSymbolKind { + displayParts: SymbolDisplayPart[]; + documentation: SymbolDisplayPart[]; + symbolKind: ScriptElementKind; + tags: JSDocTagInfo[] | undefined; +} +// TODO(drosen): Currently completion entry details passes the SemanticMeaning.All instead of using semanticMeaning of location +/* @internal */ +export function getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker: TypeChecker, symbol: Symbol, sourceFile: SourceFile, enclosingDeclaration: Node | undefined, location: Node, semanticMeaning = getMeaningFromLocation(location), alias?: Symbol): SymbolDisplayPartsDocumentationAndSymbolKind { + const displayParts: SymbolDisplayPart[] = []; + let documentation: SymbolDisplayPart[] = []; + let tags: JSDocTagInfo[] = []; + const symbolFlags = getCombinedLocalAndExportSymbolFlags(symbol); + let symbolKind = semanticMeaning & SemanticMeaning.Value ? getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker, symbol, location) : ScriptElementKind.unknown; + let hasAddedSymbolInfo = false; + const isThisExpression = location.kind === SyntaxKind.ThisKeyword && isInExpressionContext(location); + let type: Type | undefined; + let printer: Printer; + let documentationFromAlias: SymbolDisplayPart[] | undefined; + let tagsFromAlias: JSDocTagInfo[] | undefined; + let hasMultipleSignatures = false; + if (location.kind === SyntaxKind.ThisKeyword && !isThisExpression) { + return { displayParts: [keywordPart(SyntaxKind.ThisKeyword)], documentation: [], symbolKind: ScriptElementKind.primitiveType, tags: undefined }; + } + // Class at constructor site need to be shown as constructor apart from property,method, vars + if (symbolKind !== ScriptElementKind.unknown || symbolFlags & SymbolFlags.Class || symbolFlags & SymbolFlags.Alias) { + // If it is accessor they are allowed only if location is at name of the accessor + if (symbolKind === ScriptElementKind.memberGetAccessorElement || symbolKind === ScriptElementKind.memberSetAccessorElement) { + symbolKind = ScriptElementKind.memberVariableElement; + } + let signature: Signature | undefined; + type = isThisExpression ? typeChecker.getTypeAtLocation(location) : typeChecker.getTypeOfSymbolAtLocation(symbol.exportSymbol || symbol, location); + if (location.parent && location.parent.kind === SyntaxKind.PropertyAccessExpression) { + const right = (location.parent).name; + // Either the location is on the right of a property access, or on the left and the right is missing + if (right === location || (right && right.getFullWidth() === 0)) { + location = location.parent; + } + } + // try get the call/construct signature from the type if it matches + let callExpressionLike: CallExpression | NewExpression | JsxOpeningLikeElement | undefined; + if (isCallOrNewExpression(location)) { + callExpressionLike = location; + } + else if (isCallExpressionTarget(location) || isNewExpressionTarget(location)) { + callExpressionLike = (location.parent); + } + else if (location.parent && isJsxOpeningLikeElement(location.parent) && isFunctionLike(symbol.valueDeclaration)) { + callExpressionLike = location.parent; + } + if (callExpressionLike) { + signature = typeChecker.getResolvedSignature(callExpressionLike)!; // TODO: GH#18217 + const useConstructSignatures = callExpressionLike.kind === SyntaxKind.NewExpression || (isCallExpression(callExpressionLike) && callExpressionLike.expression.kind === SyntaxKind.SuperKeyword); + const allSignatures = useConstructSignatures ? type.getConstructSignatures() : type.getCallSignatures(); + if (!contains(allSignatures, signature.target) && !contains(allSignatures, signature)) { + // Get the first signature if there is one -- allSignatures may contain + // either the original signature or its target, so check for either + signature = allSignatures.length ? allSignatures[0] : undefined; + } + if (signature) { + if (useConstructSignatures && (symbolFlags & SymbolFlags.Class)) { + // Constructor + symbolKind = ScriptElementKind.constructorImplementationElement; + addPrefixForAnyFunctionOrVar(type.symbol, symbolKind); } - - if (signature) { - if (useConstructSignatures && (symbolFlags & SymbolFlags.Class)) { - // Constructor - symbolKind = ScriptElementKind.constructorImplementationElement; - addPrefixForAnyFunctionOrVar(type.symbol, symbolKind); + else if (symbolFlags & SymbolFlags.Alias) { + symbolKind = ScriptElementKind.alias; + pushSymbolKind(symbolKind); + displayParts.push(spacePart()); + if (useConstructSignatures) { + displayParts.push(keywordPart(SyntaxKind.NewKeyword)); + displayParts.push(spacePart()); } - else if (symbolFlags & SymbolFlags.Alias) { - symbolKind = ScriptElementKind.alias; - pushSymbolKind(symbolKind); + addFullSymbolName(symbol); + } + else { + addPrefixForAnyFunctionOrVar(symbol, symbolKind); + } + switch (symbolKind) { + case ScriptElementKind.jsxAttribute: + case ScriptElementKind.memberVariableElement: + case ScriptElementKind.variableElement: + case ScriptElementKind.constElement: + case ScriptElementKind.letElement: + case ScriptElementKind.parameterElement: + case ScriptElementKind.localVariableElement: + // If it is call or construct signature of lambda's write type name + displayParts.push(punctuationPart(SyntaxKind.ColonToken)); displayParts.push(spacePart()); + if (!(getObjectFlags(type) & ObjectFlags.Anonymous) && type.symbol) { + addRange(displayParts, symbolToDisplayParts(typeChecker, type.symbol, enclosingDeclaration, /*meaning*/ undefined, SymbolFormatFlags.AllowAnyNodeKind | SymbolFormatFlags.WriteTypeParametersOrArguments)); + displayParts.push(lineBreakPart()); + } if (useConstructSignatures) { displayParts.push(keywordPart(SyntaxKind.NewKeyword)); displayParts.push(spacePart()); } - addFullSymbolName(symbol); - } - else { - addPrefixForAnyFunctionOrVar(symbol, symbolKind); - } - - switch (symbolKind) { - case ScriptElementKind.jsxAttribute: - case ScriptElementKind.memberVariableElement: - case ScriptElementKind.variableElement: - case ScriptElementKind.constElement: - case ScriptElementKind.letElement: - case ScriptElementKind.parameterElement: - case ScriptElementKind.localVariableElement: - // If it is call or construct signature of lambda's write type name - displayParts.push(punctuationPart(SyntaxKind.ColonToken)); - displayParts.push(spacePart()); - if (!(getObjectFlags(type) & ObjectFlags.Anonymous) && type.symbol) { - addRange(displayParts, symbolToDisplayParts(typeChecker, type.symbol, enclosingDeclaration, /*meaning*/ undefined, SymbolFormatFlags.AllowAnyNodeKind | SymbolFormatFlags.WriteTypeParametersOrArguments)); - displayParts.push(lineBreakPart()); - } - if (useConstructSignatures) { - displayParts.push(keywordPart(SyntaxKind.NewKeyword)); - displayParts.push(spacePart()); - } - addSignatureDisplayParts(signature, allSignatures, TypeFormatFlags.WriteArrowStyleSignature); - break; - - default: - // Just signature - addSignatureDisplayParts(signature, allSignatures); - } - hasAddedSymbolInfo = true; - hasMultipleSignatures = allSignatures.length > 1; + addSignatureDisplayParts(signature, allSignatures, TypeFormatFlags.WriteArrowStyleSignature); + break; + default: + // Just signature + addSignatureDisplayParts(signature, allSignatures); } - } - else if ((isNameOfFunctionDeclaration(location) && !(symbolFlags & SymbolFlags.Accessor)) || // name of function declaration - (location.kind === SyntaxKind.ConstructorKeyword && location.parent.kind === SyntaxKind.Constructor)) { // At constructor keyword of constructor declaration - // get the signature from the declaration and write it - const functionDeclaration = location.parent; - // Use function declaration to write the signatures only if the symbol corresponding to this declaration - const locationIsSymbolDeclaration = symbol.declarations && find(symbol.declarations, declaration => - declaration === (location.kind === SyntaxKind.ConstructorKeyword ? functionDeclaration.parent : functionDeclaration)); - - if (locationIsSymbolDeclaration) { - const allSignatures = functionDeclaration.kind === SyntaxKind.Constructor ? type.getNonNullableType().getConstructSignatures() : type.getNonNullableType().getCallSignatures(); - if (!typeChecker.isImplementationOfOverload(functionDeclaration)) { - signature = typeChecker.getSignatureFromDeclaration(functionDeclaration)!; // TODO: GH#18217 - } - else { - signature = allSignatures[0]; - } - - if (functionDeclaration.kind === SyntaxKind.Constructor) { - // show (constructor) Type(...) signature - symbolKind = ScriptElementKind.constructorImplementationElement; - addPrefixForAnyFunctionOrVar(type.symbol, symbolKind); - } - else { - // (function/method) symbol(..signature) - addPrefixForAnyFunctionOrVar(functionDeclaration.kind === SyntaxKind.CallSignature && - !(type.symbol.flags & SymbolFlags.TypeLiteral || type.symbol.flags & SymbolFlags.ObjectLiteral) ? type.symbol : symbol, symbolKind); - } - - addSignatureDisplayParts(signature, allSignatures); - hasAddedSymbolInfo = true; - hasMultipleSignatures = allSignatures.length > 1; + hasAddedSymbolInfo = true; + hasMultipleSignatures = allSignatures.length > 1; + } + } + else if ((isNameOfFunctionDeclaration(location) && !(symbolFlags & SymbolFlags.Accessor)) || // name of function declaration + (location.kind === SyntaxKind.ConstructorKeyword && location.parent.kind === SyntaxKind.Constructor)) { // At constructor keyword of constructor declaration + // get the signature from the declaration and write it + const functionDeclaration = (location.parent); + // Use function declaration to write the signatures only if the symbol corresponding to this declaration + const locationIsSymbolDeclaration = symbol.declarations && find(symbol.declarations, declaration => declaration === (location.kind === SyntaxKind.ConstructorKeyword ? functionDeclaration.parent : functionDeclaration)); + if (locationIsSymbolDeclaration) { + const allSignatures = functionDeclaration.kind === SyntaxKind.Constructor ? type.getNonNullableType().getConstructSignatures() : type.getNonNullableType().getCallSignatures(); + if (!typeChecker.isImplementationOfOverload(functionDeclaration)) { + signature = typeChecker.getSignatureFromDeclaration(functionDeclaration)!; // TODO: GH#18217 } + else { + signature = allSignatures[0]; + } + if (functionDeclaration.kind === SyntaxKind.Constructor) { + // show (constructor) Type(...) signature + symbolKind = ScriptElementKind.constructorImplementationElement; + addPrefixForAnyFunctionOrVar(type.symbol, symbolKind); + } + else { + // (function/method) symbol(..signature) + addPrefixForAnyFunctionOrVar(functionDeclaration.kind === SyntaxKind.CallSignature && + !(type.symbol.flags & SymbolFlags.TypeLiteral || type.symbol.flags & SymbolFlags.ObjectLiteral) ? type.symbol : symbol, symbolKind); + } + addSignatureDisplayParts(signature, allSignatures); + hasAddedSymbolInfo = true; + hasMultipleSignatures = allSignatures.length > 1; } } - if (symbolFlags & SymbolFlags.Class && !hasAddedSymbolInfo && !isThisExpression) { - addAliasPrefixIfNecessary(); - if (getDeclarationOfKind(symbol, SyntaxKind.ClassExpression)) { - // Special case for class expressions because we would like to indicate that - // the class name is local to the class body (similar to function expression) - // (local class) class - pushSymbolKind(ScriptElementKind.localClassElement); - } - else { - // Class declaration has name which is not local. - displayParts.push(keywordPart(SyntaxKind.ClassKeyword)); - } - displayParts.push(spacePart()); - addFullSymbolName(symbol); - writeTypeParametersOfSymbol(symbol, sourceFile); - } - if ((symbolFlags & SymbolFlags.Interface) && (semanticMeaning & SemanticMeaning.Type)) { - prefixNextMeaning(); - displayParts.push(keywordPart(SyntaxKind.InterfaceKeyword)); - displayParts.push(spacePart()); - addFullSymbolName(symbol); - writeTypeParametersOfSymbol(symbol, sourceFile); - } - if ((symbolFlags & SymbolFlags.TypeAlias) && (semanticMeaning & SemanticMeaning.Type)) { - prefixNextMeaning(); - displayParts.push(keywordPart(SyntaxKind.TypeKeyword)); - displayParts.push(spacePart()); - addFullSymbolName(symbol); - writeTypeParametersOfSymbol(symbol, sourceFile); - displayParts.push(spacePart()); - displayParts.push(operatorPart(SyntaxKind.EqualsToken)); - displayParts.push(spacePart()); - addRange(displayParts, typeToDisplayParts(typeChecker, typeChecker.getDeclaredTypeOfSymbol(symbol), enclosingDeclaration, TypeFormatFlags.InTypeAlias)); - } - if (symbolFlags & SymbolFlags.Enum) { - prefixNextMeaning(); - if (some(symbol.declarations, d => isEnumDeclaration(d) && isEnumConst(d))) { - displayParts.push(keywordPart(SyntaxKind.ConstKeyword)); - displayParts.push(spacePart()); - } - displayParts.push(keywordPart(SyntaxKind.EnumKeyword)); - displayParts.push(spacePart()); - addFullSymbolName(symbol); - } - if (symbolFlags & SymbolFlags.Module && !isThisExpression) { - prefixNextMeaning(); - const declaration = getDeclarationOfKind(symbol, SyntaxKind.ModuleDeclaration); - const isNamespace = declaration && declaration.name && declaration.name.kind === SyntaxKind.Identifier; - displayParts.push(keywordPart(isNamespace ? SyntaxKind.NamespaceKeyword : SyntaxKind.ModuleKeyword)); + } + if (symbolFlags & SymbolFlags.Class && !hasAddedSymbolInfo && !isThisExpression) { + addAliasPrefixIfNecessary(); + if (getDeclarationOfKind(symbol, SyntaxKind.ClassExpression)) { + // Special case for class expressions because we would like to indicate that + // the class name is local to the class body (similar to function expression) + // (local class) class + pushSymbolKind(ScriptElementKind.localClassElement); + } + else { + // Class declaration has name which is not local. + displayParts.push(keywordPart(SyntaxKind.ClassKeyword)); + } + displayParts.push(spacePart()); + addFullSymbolName(symbol); + writeTypeParametersOfSymbol(symbol, sourceFile); + } + if ((symbolFlags & SymbolFlags.Interface) && (semanticMeaning & SemanticMeaning.Type)) { + prefixNextMeaning(); + displayParts.push(keywordPart(SyntaxKind.InterfaceKeyword)); + displayParts.push(spacePart()); + addFullSymbolName(symbol); + writeTypeParametersOfSymbol(symbol, sourceFile); + } + if ((symbolFlags & SymbolFlags.TypeAlias) && (semanticMeaning & SemanticMeaning.Type)) { + prefixNextMeaning(); + displayParts.push(keywordPart(SyntaxKind.TypeKeyword)); + displayParts.push(spacePart()); + addFullSymbolName(symbol); + writeTypeParametersOfSymbol(symbol, sourceFile); + displayParts.push(spacePart()); + displayParts.push(operatorPart(SyntaxKind.EqualsToken)); + displayParts.push(spacePart()); + addRange(displayParts, typeToDisplayParts(typeChecker, typeChecker.getDeclaredTypeOfSymbol(symbol), enclosingDeclaration, TypeFormatFlags.InTypeAlias)); + } + if (symbolFlags & SymbolFlags.Enum) { + prefixNextMeaning(); + if (some(symbol.declarations, d => isEnumDeclaration(d) && isEnumConst(d))) { + displayParts.push(keywordPart(SyntaxKind.ConstKeyword)); displayParts.push(spacePart()); - addFullSymbolName(symbol); } - if ((symbolFlags & SymbolFlags.TypeParameter) && (semanticMeaning & SemanticMeaning.Type)) { - prefixNextMeaning(); - displayParts.push(punctuationPart(SyntaxKind.OpenParenToken)); - displayParts.push(textPart("type parameter")); - displayParts.push(punctuationPart(SyntaxKind.CloseParenToken)); - displayParts.push(spacePart()); - addFullSymbolName(symbol); - if (symbol.parent) { - // Class/Interface type parameter - addInPrefix(); - addFullSymbolName(symbol.parent, enclosingDeclaration); - writeTypeParametersOfSymbol(symbol.parent, enclosingDeclaration); - } - else { - // Method/function type parameter - const decl = getDeclarationOfKind(symbol, SyntaxKind.TypeParameter); - if (decl === undefined) return Debug.fail(); - const declaration = decl.parent; - - if (declaration) { - if (isFunctionLikeKind(declaration.kind)) { - addInPrefix(); - const signature = typeChecker.getSignatureFromDeclaration(declaration)!; // TODO: GH#18217 - if (declaration.kind === SyntaxKind.ConstructSignature) { - displayParts.push(keywordPart(SyntaxKind.NewKeyword)); - displayParts.push(spacePart()); - } - else if (declaration.kind !== SyntaxKind.CallSignature && (declaration).name) { - addFullSymbolName(declaration.symbol); - } - addRange(displayParts, signatureToDisplayParts(typeChecker, signature, sourceFile, TypeFormatFlags.WriteTypeArgumentsOfSignature)); - } - else if (declaration.kind === SyntaxKind.TypeAliasDeclaration) { - // Type alias type parameter - // For example - // type list = T[]; // Both T will go through same code path - addInPrefix(); - displayParts.push(keywordPart(SyntaxKind.TypeKeyword)); + displayParts.push(keywordPart(SyntaxKind.EnumKeyword)); + displayParts.push(spacePart()); + addFullSymbolName(symbol); + } + if (symbolFlags & SymbolFlags.Module && !isThisExpression) { + prefixNextMeaning(); + const declaration = getDeclarationOfKind(symbol, SyntaxKind.ModuleDeclaration); + const isNamespace = declaration && declaration.name && declaration.name.kind === SyntaxKind.Identifier; + displayParts.push(keywordPart(isNamespace ? SyntaxKind.NamespaceKeyword : SyntaxKind.ModuleKeyword)); + displayParts.push(spacePart()); + addFullSymbolName(symbol); + } + if ((symbolFlags & SymbolFlags.TypeParameter) && (semanticMeaning & SemanticMeaning.Type)) { + prefixNextMeaning(); + displayParts.push(punctuationPart(SyntaxKind.OpenParenToken)); + displayParts.push(textPart("type parameter")); + displayParts.push(punctuationPart(SyntaxKind.CloseParenToken)); + displayParts.push(spacePart()); + addFullSymbolName(symbol); + if (symbol.parent) { + // Class/Interface type parameter + addInPrefix(); + addFullSymbolName(symbol.parent, enclosingDeclaration); + writeTypeParametersOfSymbol(symbol.parent, enclosingDeclaration); + } + else { + // Method/function type parameter + const decl = getDeclarationOfKind(symbol, SyntaxKind.TypeParameter); + if (decl === undefined) + return Debug.fail(); + const declaration = decl.parent; + if (declaration) { + if (isFunctionLikeKind(declaration.kind)) { + addInPrefix(); + const signature = (typeChecker.getSignatureFromDeclaration((declaration))!); // TODO: GH#18217 + if (declaration.kind === SyntaxKind.ConstructSignature) { + displayParts.push(keywordPart(SyntaxKind.NewKeyword)); displayParts.push(spacePart()); + } + else if (declaration.kind !== SyntaxKind.CallSignature && (declaration).name) { addFullSymbolName(declaration.symbol); - writeTypeParametersOfSymbol(declaration.symbol, sourceFile); } + addRange(displayParts, signatureToDisplayParts(typeChecker, signature, sourceFile, TypeFormatFlags.WriteTypeArgumentsOfSignature)); } - } - } - if (symbolFlags & SymbolFlags.EnumMember) { - symbolKind = ScriptElementKind.enumMemberElement; - addPrefixForAnyFunctionOrVar(symbol, "enum member"); - const declaration = symbol.declarations[0]; - if (declaration.kind === SyntaxKind.EnumMember) { - const constantValue = typeChecker.getConstantValue(declaration); - if (constantValue !== undefined) { + else if (declaration.kind === SyntaxKind.TypeAliasDeclaration) { + // Type alias type parameter + // For example + // type list = T[]; // Both T will go through same code path + addInPrefix(); + displayParts.push(keywordPart(SyntaxKind.TypeKeyword)); displayParts.push(spacePart()); - displayParts.push(operatorPart(SyntaxKind.EqualsToken)); - displayParts.push(spacePart()); - displayParts.push(displayPart(getTextOfConstantValue(constantValue), - typeof constantValue === "number" ? SymbolDisplayPartKind.numericLiteral : SymbolDisplayPartKind.stringLiteral)); + addFullSymbolName(declaration.symbol); + writeTypeParametersOfSymbol(declaration.symbol, sourceFile); } } } - if (symbolFlags & SymbolFlags.Alias) { - prefixNextMeaning(); - if (!hasAddedSymbolInfo) { - const resolvedSymbol = typeChecker.getAliasedSymbol(symbol); - if (resolvedSymbol !== symbol && resolvedSymbol.declarations && resolvedSymbol.declarations.length > 0) { - const resolvedNode = resolvedSymbol.declarations[0]; - const declarationName = getNameOfDeclaration(resolvedNode); - if (declarationName) { - const isExternalModuleDeclaration = - isModuleWithStringLiteralName(resolvedNode) && - hasModifier(resolvedNode, ModifierFlags.Ambient); - const shouldUseAliasName = symbol.name !== "default" && !isExternalModuleDeclaration; - const resolvedInfo = getSymbolDisplayPartsDocumentationAndSymbolKind( - typeChecker, - resolvedSymbol, - getSourceFileOfNode(resolvedNode), - resolvedNode, - declarationName, - semanticMeaning, - shouldUseAliasName ? symbol : resolvedSymbol); - displayParts.push(...resolvedInfo.displayParts); - displayParts.push(lineBreakPart()); - documentationFromAlias = resolvedInfo.documentation; - tagsFromAlias = resolvedInfo.tags; - } + } + if (symbolFlags & SymbolFlags.EnumMember) { + symbolKind = ScriptElementKind.enumMemberElement; + addPrefixForAnyFunctionOrVar(symbol, "enum member"); + const declaration = symbol.declarations[0]; + if (declaration.kind === SyntaxKind.EnumMember) { + const constantValue = typeChecker.getConstantValue((declaration)); + if (constantValue !== undefined) { + displayParts.push(spacePart()); + displayParts.push(operatorPart(SyntaxKind.EqualsToken)); + displayParts.push(spacePart()); + displayParts.push(displayPart(getTextOfConstantValue(constantValue), typeof constantValue === "number" ? SymbolDisplayPartKind.numericLiteral : SymbolDisplayPartKind.stringLiteral)); + } + } + } + if (symbolFlags & SymbolFlags.Alias) { + prefixNextMeaning(); + if (!hasAddedSymbolInfo) { + const resolvedSymbol = typeChecker.getAliasedSymbol(symbol); + if (resolvedSymbol !== symbol && resolvedSymbol.declarations && resolvedSymbol.declarations.length > 0) { + const resolvedNode = resolvedSymbol.declarations[0]; + const declarationName = getNameOfDeclaration(resolvedNode); + if (declarationName) { + const isExternalModuleDeclaration = isModuleWithStringLiteralName(resolvedNode) && + hasModifier(resolvedNode, ModifierFlags.Ambient); + const shouldUseAliasName = symbol.name !== "default" && !isExternalModuleDeclaration; + const resolvedInfo = getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, resolvedSymbol, getSourceFileOfNode(resolvedNode), resolvedNode, declarationName, semanticMeaning, shouldUseAliasName ? symbol : resolvedSymbol); + displayParts.push(...resolvedInfo.displayParts); + displayParts.push(lineBreakPart()); + documentationFromAlias = resolvedInfo.documentation; + tagsFromAlias = resolvedInfo.tags; } } - - switch (symbol.declarations[0].kind) { - case SyntaxKind.NamespaceExportDeclaration: - displayParts.push(keywordPart(SyntaxKind.ExportKeyword)); + } + switch (symbol.declarations[0].kind) { + case SyntaxKind.NamespaceExportDeclaration: + displayParts.push(keywordPart(SyntaxKind.ExportKeyword)); + displayParts.push(spacePart()); + displayParts.push(keywordPart(SyntaxKind.NamespaceKeyword)); + break; + case SyntaxKind.ExportAssignment: + displayParts.push(keywordPart(SyntaxKind.ExportKeyword)); + displayParts.push(spacePart()); + displayParts.push(keywordPart((symbol.declarations[0] as ExportAssignment).isExportEquals ? SyntaxKind.EqualsToken : SyntaxKind.DefaultKeyword)); + break; + case SyntaxKind.ExportSpecifier: + displayParts.push(keywordPart(SyntaxKind.ExportKeyword)); + break; + default: + displayParts.push(keywordPart(SyntaxKind.ImportKeyword)); + } + displayParts.push(spacePart()); + addFullSymbolName(symbol); + forEach(symbol.declarations, declaration => { + if (declaration.kind === SyntaxKind.ImportEqualsDeclaration) { + const importEqualsDeclaration = (declaration); + if (isExternalModuleImportEqualsDeclaration(importEqualsDeclaration)) { displayParts.push(spacePart()); - displayParts.push(keywordPart(SyntaxKind.NamespaceKeyword)); - break; - case SyntaxKind.ExportAssignment: - displayParts.push(keywordPart(SyntaxKind.ExportKeyword)); + displayParts.push(operatorPart(SyntaxKind.EqualsToken)); displayParts.push(spacePart()); - displayParts.push(keywordPart((symbol.declarations[0] as ExportAssignment).isExportEquals ? SyntaxKind.EqualsToken : SyntaxKind.DefaultKeyword)); - break; - case SyntaxKind.ExportSpecifier: - displayParts.push(keywordPart(SyntaxKind.ExportKeyword)); - break; - default: - displayParts.push(keywordPart(SyntaxKind.ImportKeyword)); - } - displayParts.push(spacePart()); - addFullSymbolName(symbol); - forEach(symbol.declarations, declaration => { - if (declaration.kind === SyntaxKind.ImportEqualsDeclaration) { - const importEqualsDeclaration = declaration; - if (isExternalModuleImportEqualsDeclaration(importEqualsDeclaration)) { + displayParts.push(keywordPart(SyntaxKind.RequireKeyword)); + displayParts.push(punctuationPart(SyntaxKind.OpenParenToken)); + displayParts.push(displayPart(getTextOfNode(getExternalModuleImportEqualsDeclarationExpression(importEqualsDeclaration)), SymbolDisplayPartKind.stringLiteral)); + displayParts.push(punctuationPart(SyntaxKind.CloseParenToken)); + } + else { + const internalAliasSymbol = typeChecker.getSymbolAtLocation(importEqualsDeclaration.moduleReference); + if (internalAliasSymbol) { displayParts.push(spacePart()); displayParts.push(operatorPart(SyntaxKind.EqualsToken)); displayParts.push(spacePart()); - displayParts.push(keywordPart(SyntaxKind.RequireKeyword)); - displayParts.push(punctuationPart(SyntaxKind.OpenParenToken)); - displayParts.push(displayPart(getTextOfNode(getExternalModuleImportEqualsDeclarationExpression(importEqualsDeclaration)), SymbolDisplayPartKind.stringLiteral)); - displayParts.push(punctuationPart(SyntaxKind.CloseParenToken)); + addFullSymbolName(internalAliasSymbol, enclosingDeclaration); } - else { - const internalAliasSymbol = typeChecker.getSymbolAtLocation(importEqualsDeclaration.moduleReference); - if (internalAliasSymbol) { - displayParts.push(spacePart()); - displayParts.push(operatorPart(SyntaxKind.EqualsToken)); - displayParts.push(spacePart()); - addFullSymbolName(internalAliasSymbol, enclosingDeclaration); - } - } - return true; } - }); - } - if (!hasAddedSymbolInfo) { - if (symbolKind !== ScriptElementKind.unknown) { - if (type) { - if (isThisExpression) { - prefixNextMeaning(); - displayParts.push(keywordPart(SyntaxKind.ThisKeyword)); + return true; + } + }); + } + if (!hasAddedSymbolInfo) { + if (symbolKind !== ScriptElementKind.unknown) { + if (type) { + if (isThisExpression) { + prefixNextMeaning(); + displayParts.push(keywordPart(SyntaxKind.ThisKeyword)); + } + else { + addPrefixForAnyFunctionOrVar(symbol, symbolKind); + } + // For properties, variables and local vars: show the type + if (symbolKind === ScriptElementKind.memberVariableElement || + symbolKind === ScriptElementKind.jsxAttribute || + symbolFlags & SymbolFlags.Variable || + symbolKind === ScriptElementKind.localVariableElement || + isThisExpression) { + displayParts.push(punctuationPart(SyntaxKind.ColonToken)); + displayParts.push(spacePart()); + // If the type is type parameter, format it specially + if (type.symbol && type.symbol.flags & SymbolFlags.TypeParameter) { + const typeParameterParts = mapToDisplayParts(writer => { + const param = (typeChecker.typeParameterToDeclaration((type as TypeParameter), enclosingDeclaration)!); + getPrinter().writeNode(EmitHint.Unspecified, param, getSourceFileOfNode(getParseTreeNode(enclosingDeclaration)), writer); + }); + addRange(displayParts, typeParameterParts); } else { - addPrefixForAnyFunctionOrVar(symbol, symbolKind); - } - - // For properties, variables and local vars: show the type - if (symbolKind === ScriptElementKind.memberVariableElement || - symbolKind === ScriptElementKind.jsxAttribute || - symbolFlags & SymbolFlags.Variable || - symbolKind === ScriptElementKind.localVariableElement || - isThisExpression) { - displayParts.push(punctuationPart(SyntaxKind.ColonToken)); - displayParts.push(spacePart()); - // If the type is type parameter, format it specially - if (type.symbol && type.symbol.flags & SymbolFlags.TypeParameter) { - const typeParameterParts = mapToDisplayParts(writer => { - const param = typeChecker.typeParameterToDeclaration(type as TypeParameter, enclosingDeclaration)!; - getPrinter().writeNode(EmitHint.Unspecified, param, getSourceFileOfNode(getParseTreeNode(enclosingDeclaration)), writer); - }); - addRange(displayParts, typeParameterParts); - } - else { - addRange(displayParts, typeToDisplayParts(typeChecker, type, enclosingDeclaration)); - } - } - else if (symbolFlags & SymbolFlags.Function || - symbolFlags & SymbolFlags.Method || - symbolFlags & SymbolFlags.Constructor || - symbolFlags & SymbolFlags.Signature || - symbolFlags & SymbolFlags.Accessor || - symbolKind === ScriptElementKind.memberFunctionElement) { - const allSignatures = type.getNonNullableType().getCallSignatures(); - if (allSignatures.length) { - addSignatureDisplayParts(allSignatures[0], allSignatures); - hasMultipleSignatures = allSignatures.length > 1; - } + addRange(displayParts, typeToDisplayParts(typeChecker, type, enclosingDeclaration)); } } - } - else { - symbolKind = getSymbolKind(typeChecker, symbol, location); - } - } - - if (documentation.length === 0 && !hasMultipleSignatures) { - documentation = symbol.getDocumentationComment(typeChecker); - } - - if (documentation.length === 0 && symbolFlags & SymbolFlags.Property) { - // For some special property access expressions like `exports.foo = foo` or `module.exports.foo = foo` - // there documentation comments might be attached to the right hand side symbol of their declarations. - // The pattern of such special property access is that the parent symbol is the symbol of the file. - if (symbol.parent && forEach(symbol.parent.declarations, declaration => declaration.kind === SyntaxKind.SourceFile)) { - for (const declaration of symbol.declarations) { - if (!declaration.parent || declaration.parent.kind !== SyntaxKind.BinaryExpression) { - continue; - } - - const rhsSymbol = typeChecker.getSymbolAtLocation((declaration.parent).right); - if (!rhsSymbol) { - continue; - } - - documentation = rhsSymbol.getDocumentationComment(typeChecker); - tags = rhsSymbol.getJsDocTags(); - if (documentation.length > 0) { - break; + else if (symbolFlags & SymbolFlags.Function || + symbolFlags & SymbolFlags.Method || + symbolFlags & SymbolFlags.Constructor || + symbolFlags & SymbolFlags.Signature || + symbolFlags & SymbolFlags.Accessor || + symbolKind === ScriptElementKind.memberFunctionElement) { + const allSignatures = type.getNonNullableType().getCallSignatures(); + if (allSignatures.length) { + addSignatureDisplayParts(allSignatures[0], allSignatures); + hasMultipleSignatures = allSignatures.length > 1; } } } } - - if (tags.length === 0 && !hasMultipleSignatures) { - tags = symbol.getJsDocTags(); + else { + symbolKind = getSymbolKind(typeChecker, symbol, location); } - - if (documentation.length === 0 && documentationFromAlias) { - documentation = documentationFromAlias; - } - - if (tags.length === 0 && tagsFromAlias) { - tags = tagsFromAlias; - } - - return { displayParts, documentation, symbolKind, tags: tags.length === 0 ? undefined : tags }; - - function getPrinter() { - if (!printer) { - printer = createPrinter({ removeComments: true }); + } + if (documentation.length === 0 && !hasMultipleSignatures) { + documentation = symbol.getDocumentationComment(typeChecker); + } + if (documentation.length === 0 && symbolFlags & SymbolFlags.Property) { + // For some special property access expressions like `exports.foo = foo` or `module.exports.foo = foo` + // there documentation comments might be attached to the right hand side symbol of their declarations. + // The pattern of such special property access is that the parent symbol is the symbol of the file. + if (symbol.parent && forEach(symbol.parent.declarations, declaration => declaration.kind === SyntaxKind.SourceFile)) { + for (const declaration of symbol.declarations) { + if (!declaration.parent || declaration.parent.kind !== SyntaxKind.BinaryExpression) { + continue; + } + const rhsSymbol = typeChecker.getSymbolAtLocation((declaration.parent).right); + if (!rhsSymbol) { + continue; + } + documentation = rhsSymbol.getDocumentationComment(typeChecker); + tags = rhsSymbol.getJsDocTags(); + if (documentation.length > 0) { + break; + } } - return printer; } - - function prefixNextMeaning() { - if (displayParts.length) { - displayParts.push(lineBreakPart()); - } - addAliasPrefixIfNecessary(); + } + if (tags.length === 0 && !hasMultipleSignatures) { + tags = symbol.getJsDocTags(); + } + if (documentation.length === 0 && documentationFromAlias) { + documentation = documentationFromAlias; + } + if (tags.length === 0 && tagsFromAlias) { + tags = tagsFromAlias; + } + return { displayParts, documentation, symbolKind, tags: tags.length === 0 ? undefined : tags }; + function getPrinter() { + if (!printer) { + printer = createPrinter({ removeComments: true }); } - - function addAliasPrefixIfNecessary() { - if (alias) { - pushSymbolKind(ScriptElementKind.alias); - displayParts.push(spacePart()); - } + return printer; + } + function prefixNextMeaning() { + if (displayParts.length) { + displayParts.push(lineBreakPart()); } - - function addInPrefix() { - displayParts.push(spacePart()); - displayParts.push(keywordPart(SyntaxKind.InKeyword)); + addAliasPrefixIfNecessary(); + } + function addAliasPrefixIfNecessary() { + if (alias) { + pushSymbolKind(ScriptElementKind.alias); displayParts.push(spacePart()); } - - function addFullSymbolName(symbolToDisplay: Symbol, enclosingDeclaration?: Node) { - if (alias && symbolToDisplay === symbol) { - symbolToDisplay = alias; - } - const fullSymbolDisplayParts = symbolToDisplayParts(typeChecker, symbolToDisplay, enclosingDeclaration || sourceFile, /*meaning*/ undefined, - SymbolFormatFlags.WriteTypeParametersOrArguments | SymbolFormatFlags.UseOnlyExternalAliasing | SymbolFormatFlags.AllowAnyNodeKind); - addRange(displayParts, fullSymbolDisplayParts); - - if (symbol.flags & SymbolFlags.Optional) { - displayParts.push(punctuationPart(SyntaxKind.QuestionToken)); - } + } + function addInPrefix() { + displayParts.push(spacePart()); + displayParts.push(keywordPart(SyntaxKind.InKeyword)); + displayParts.push(spacePart()); + } + function addFullSymbolName(symbolToDisplay: Symbol, enclosingDeclaration?: Node) { + if (alias && symbolToDisplay === symbol) { + symbolToDisplay = alias; } - - function addPrefixForAnyFunctionOrVar(symbol: Symbol, symbolKind: string) { - prefixNextMeaning(); - if (symbolKind) { - pushSymbolKind(symbolKind); - if (symbol && !some(symbol.declarations, d => isArrowFunction(d) || (isFunctionExpression(d) || isClassExpression(d)) && !d.name)) { - displayParts.push(spacePart()); - addFullSymbolName(symbol); - } - } + const fullSymbolDisplayParts = symbolToDisplayParts(typeChecker, symbolToDisplay, enclosingDeclaration || sourceFile, /*meaning*/ undefined, SymbolFormatFlags.WriteTypeParametersOrArguments | SymbolFormatFlags.UseOnlyExternalAliasing | SymbolFormatFlags.AllowAnyNodeKind); + addRange(displayParts, fullSymbolDisplayParts); + if (symbol.flags & SymbolFlags.Optional) { + displayParts.push(punctuationPart(SyntaxKind.QuestionToken)); } - - function pushSymbolKind(symbolKind: string) { - switch (symbolKind) { - case ScriptElementKind.variableElement: - case ScriptElementKind.functionElement: - case ScriptElementKind.letElement: - case ScriptElementKind.constElement: - case ScriptElementKind.constructorImplementationElement: - displayParts.push(textOrKeywordPart(symbolKind)); - return; - default: - displayParts.push(punctuationPart(SyntaxKind.OpenParenToken)); - displayParts.push(textOrKeywordPart(symbolKind)); - displayParts.push(punctuationPart(SyntaxKind.CloseParenToken)); - return; + } + function addPrefixForAnyFunctionOrVar(symbol: Symbol, symbolKind: string) { + prefixNextMeaning(); + if (symbolKind) { + pushSymbolKind(symbolKind); + if (symbol && !some(symbol.declarations, d => isArrowFunction(d) || (isFunctionExpression(d) || isClassExpression(d)) && !d.name)) { + displayParts.push(spacePart()); + addFullSymbolName(symbol); } } - - function addSignatureDisplayParts(signature: Signature, allSignatures: readonly Signature[], flags = TypeFormatFlags.None) { - addRange(displayParts, signatureToDisplayParts(typeChecker, signature, enclosingDeclaration, flags | TypeFormatFlags.WriteTypeArgumentsOfSignature)); - if (allSignatures.length > 1) { - displayParts.push(spacePart()); + } + function pushSymbolKind(symbolKind: string) { + switch (symbolKind) { + case ScriptElementKind.variableElement: + case ScriptElementKind.functionElement: + case ScriptElementKind.letElement: + case ScriptElementKind.constElement: + case ScriptElementKind.constructorImplementationElement: + displayParts.push(textOrKeywordPart(symbolKind)); + return; + default: displayParts.push(punctuationPart(SyntaxKind.OpenParenToken)); - displayParts.push(operatorPart(SyntaxKind.PlusToken)); - displayParts.push(displayPart((allSignatures.length - 1).toString(), SymbolDisplayPartKind.numericLiteral)); - displayParts.push(spacePart()); - displayParts.push(textPart(allSignatures.length === 2 ? "overload" : "overloads")); + displayParts.push(textOrKeywordPart(symbolKind)); displayParts.push(punctuationPart(SyntaxKind.CloseParenToken)); - } - documentation = signature.getDocumentationComment(typeChecker); - tags = signature.getJsDocTags(); - - if (allSignatures.length > 1 && documentation.length === 0 && tags.length === 0) { - documentation = allSignatures[0].getDocumentationComment(typeChecker); - tags = allSignatures[0].getJsDocTags(); - } + return; } - - function writeTypeParametersOfSymbol(symbol: Symbol, enclosingDeclaration: Node | undefined) { - const typeParameterParts = mapToDisplayParts(writer => { - const params = typeChecker.symbolToTypeParameterDeclarations(symbol, enclosingDeclaration); - getPrinter().writeList(ListFormat.TypeParameters, params, getSourceFileOfNode(getParseTreeNode(enclosingDeclaration)), writer); - }); - addRange(displayParts, typeParameterParts); + } + function addSignatureDisplayParts(signature: Signature, allSignatures: readonly Signature[], flags = TypeFormatFlags.None) { + addRange(displayParts, signatureToDisplayParts(typeChecker, signature, enclosingDeclaration, flags | TypeFormatFlags.WriteTypeArgumentsOfSignature)); + if (allSignatures.length > 1) { + displayParts.push(spacePart()); + displayParts.push(punctuationPart(SyntaxKind.OpenParenToken)); + displayParts.push(operatorPart(SyntaxKind.PlusToken)); + displayParts.push(displayPart((allSignatures.length - 1).toString(), SymbolDisplayPartKind.numericLiteral)); + displayParts.push(spacePart()); + displayParts.push(textPart(allSignatures.length === 2 ? "overload" : "overloads")); + displayParts.push(punctuationPart(SyntaxKind.CloseParenToken)); + } + documentation = signature.getDocumentationComment(typeChecker); + tags = signature.getJsDocTags(); + if (allSignatures.length > 1 && documentation.length === 0 && tags.length === 0) { + documentation = allSignatures[0].getDocumentationComment(typeChecker); + tags = allSignatures[0].getJsDocTags(); } } - - function isLocalVariableOrFunction(symbol: Symbol) { - if (symbol.parent) { - return false; // This is exported symbol + function writeTypeParametersOfSymbol(symbol: Symbol, enclosingDeclaration: Node | undefined) { + const typeParameterParts = mapToDisplayParts(writer => { + const params = typeChecker.symbolToTypeParameterDeclarations(symbol, enclosingDeclaration); + getPrinter().writeList(ListFormat.TypeParameters, params, getSourceFileOfNode(getParseTreeNode(enclosingDeclaration)), writer); + }); + addRange(displayParts, typeParameterParts); + } +} +/* @internal */ +function isLocalVariableOrFunction(symbol: Symbol) { + if (symbol.parent) { + return false; // This is exported symbol + } + return forEach(symbol.declarations, declaration => { + // Function expressions are local + if (declaration.kind === SyntaxKind.FunctionExpression) { + return true; } - - return forEach(symbol.declarations, declaration => { - // Function expressions are local - if (declaration.kind === SyntaxKind.FunctionExpression) { - return true; - } - - if (declaration.kind !== SyntaxKind.VariableDeclaration && declaration.kind !== SyntaxKind.FunctionDeclaration) { + if (declaration.kind !== SyntaxKind.VariableDeclaration && declaration.kind !== SyntaxKind.FunctionDeclaration) { + return false; + } + // If the parent is not sourceFile or module block it is local variable + for (let parent = declaration.parent; !isFunctionBlock(parent); parent = parent.parent) { + // Reached source file or module block + if (parent.kind === SyntaxKind.SourceFile || parent.kind === SyntaxKind.ModuleBlock) { return false; } - - // If the parent is not sourceFile or module block it is local variable - for (let parent = declaration.parent; !isFunctionBlock(parent); parent = parent.parent) { - // Reached source file or module block - if (parent.kind === SyntaxKind.SourceFile || parent.kind === SyntaxKind.ModuleBlock) { - return false; - } - } - - // parent is in function block - return true; - }); - } + } + // parent is in function block + return true; + }); } diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index 49f75cf41ecb4..29cdef9e7e406 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -1,1391 +1,1304 @@ +import { TextRange, Debug, skipTrivia, isWhiteSpaceSingleLine, CharacterCodes, SourceFile, Node, getLineStartPositionForPosition, getStartPositionOfLine, getLineOfLocalPosition, isExpression, isLineBreak, Token, SyntaxKind, LanguageServiceHost, UserPreferences, SignatureDeclaration, VariableDeclaration, ParameterDeclaration, PropertyDeclaration, PropertySignature, FunctionDeclaration, FunctionExpression, FunctionLike, isFunctionExpression, isFunctionDeclaration, Statement, createMap, ClassDeclaration, InterfaceDeclaration, ObjectLiteralExpression, NodeArray, TypeParameterDeclaration, getNewLineOrDefaultFromHost, FileTextChanges, createTextRangeFromSpan, Modifier, findNextToken, PropertyAssignment, createRange, createToken, getFirstNonSpaceCharacterPosition, getTouchingToken, HasJSDoc, JSDoc, getPrecedingNonSpaceCharacterPosition, TypeNode, isFunctionLike, findChildOfKind, isArrowFunction, first, isStatement, isClassElement, isVariableDeclaration, isParameter, isStringLiteral, isImportDeclaration, isNamedImports, ConstructorDeclaration, firstOrUndefined, lastOrUndefined, createBlock, ClassLikeDeclaration, ClassElement, ObjectLiteralElementLike, rangeStartPositionsAreOnSameLine, addToSeen, getNodeId, isObjectLiteralExpression, isJsonSourceFile, isClassOrTypeElement, ClassExpression, ArrowFunction, createIdentifier, DeclarationStatement, VariableStatement, indexOfNode, getTokenAtPosition, getLineAndCharacterOfPosition, tokenToString, findPrecedingToken, Expression, rangeOfNode, createParen, positionsAreOnSameLine, NodeSet, rangeContainsRangeExclusive, isArray, rangeOfTypeParameters, last, findLastIndex, ScriptKind, group, stableSort, createTextChange, createTextSpanFromRange, getScriptKindFromFileName, createTextSpan, createSourceFile, ScriptTarget, removeSuffix, FormatCodeSettings, SemicolonPreference, probablyUsesSemicolons, SourceFileLike, NewLineKind, createPrinter, EmitHint, TextChange, textSpanEnd, visitEachChild, nullTransformationContext, nodeIsSynthesized, Visitor, visitNodes, createNodeArray, EmitTextWriter, PrintHandlers, createTextWriter, isWhiteSpaceLike, Symbol, PrologueDirective, isPrologueDirective, getShebang, getLeadingCommentRanges, CommentRange, isPinnedComment, isRecognizedTripleSlashComment, isInComment, isInString, isInTemplateString, isInJSXText, isPropertySignature, isPropertyDeclaration, isStatementButNotDeclaration, find, BindingElement, ImportSpecifier, NamespaceImport, isImportClause, isCallLikeExpression, ImportClause, NamedImportBindings, getAncestor, createObjectLiteral } from "./ts"; +import { FormatContext, SmartIndenter, formatDocument, formatNodeGivenIndentation } from "./ts.formatting"; /* @internal */ -namespace ts.textChanges { - - /** - * Currently for simplicity we store recovered positions on the node itself. - * It can be changed to side-table later if we decide that current design is too invasive. +/** + * Currently for simplicity we store recovered positions on the node itself. + * It can be changed to side-table later if we decide that current design is too invasive. + */ +function getPos(n: TextRange): number { + const result = (n).__pos; + Debug.assert(typeof result === "number"); + return result; +} +/* @internal */ +function setPos(n: TextRange, pos: number): void { + Debug.assert(typeof pos === "number"); + (n).__pos = pos; +} +/* @internal */ +function getEnd(n: TextRange): number { + const result = (n).__end; + Debug.assert(typeof result === "number"); + return result; +} +/* @internal */ +function setEnd(n: TextRange, end: number): void { + Debug.assert(typeof end === "number"); + (n).__end = end; +} +/* @internal */ +export interface ConfigurableStart { + leadingTriviaOption?: LeadingTriviaOption; +} +/* @internal */ +export interface ConfigurableEnd { + trailingTriviaOption?: TrailingTriviaOption; +} +/* @internal */ +export enum LeadingTriviaOption { + /** Exclude all leading trivia (use getStart()) */ + Exclude, + /** Include leading trivia and, + * if there are no line breaks between the node and the previous token, + * include all trivia between the node and the previous token */ - function getPos(n: TextRange): number { - const result = (n).__pos; - Debug.assert(typeof result === "number"); - return result; - } - - function setPos(n: TextRange, pos: number): void { - Debug.assert(typeof pos === "number"); - (n).__pos = pos; - } - - function getEnd(n: TextRange): number { - const result = (n).__end; - Debug.assert(typeof result === "number"); - return result; - } - - function setEnd(n: TextRange, end: number): void { - Debug.assert(typeof end === "number"); - (n).__end = end; - } - - export interface ConfigurableStart { - leadingTriviaOption?: LeadingTriviaOption; - } - export interface ConfigurableEnd { - trailingTriviaOption?: TrailingTriviaOption; - } - - export enum LeadingTriviaOption { - /** Exclude all leading trivia (use getStart()) */ - Exclude, - /** Include leading trivia and, - * if there are no line breaks between the node and the previous token, - * include all trivia between the node and the previous token - */ - IncludeAll, - } - - export enum TrailingTriviaOption { - /** Exclude all trailing trivia (use getEnd()) */ - Exclude, - /** Include trailing trivia */ - Include, - } - - function skipWhitespacesAndLineBreaks(text: string, start: number) { - return skipTrivia(text, start, /*stopAfterLineBreak*/ false, /*stopAtComments*/ true); - } - - function hasCommentsBeforeLineBreak(text: string, start: number) { - let i = start; - while (i < text.length) { - const ch = text.charCodeAt(i); - if (isWhiteSpaceSingleLine(ch)) { - i++; - continue; - } - return ch === CharacterCodes.slash; - } - return false; + IncludeAll +} +/* @internal */ +export enum TrailingTriviaOption { + /** Exclude all trailing trivia (use getEnd()) */ + Exclude, + /** Include trailing trivia */ + Include +} +/* @internal */ +function skipWhitespacesAndLineBreaks(text: string, start: number) { + return skipTrivia(text, start, /*stopAfterLineBreak*/ false, /*stopAtComments*/ true); +} +/* @internal */ +function hasCommentsBeforeLineBreak(text: string, start: number) { + let i = start; + while (i < text.length) { + const ch = text.charCodeAt(i); + if (isWhiteSpaceSingleLine(ch)) { + i++; + continue; + } + return ch === CharacterCodes.slash; } - + return false; +} +/** + * Usually node.pos points to a position immediately after the previous token. + * If this position is used as a beginning of the span to remove - it might lead to removing the trailing trivia of the previous node, i.e: + * const x; // this is x + * ^ - pos for the next variable declaration will point here + * const y; // this is y + * ^ - end for previous variable declaration + * Usually leading trivia of the variable declaration 'y' should not include trailing trivia (whitespace, comment 'this is x' and newline) from the preceding + * variable declaration and trailing trivia for 'y' should include (whitespace, comment 'this is y', newline). + * By default when removing nodes we adjust start and end positions to respect specification of the trivia above. + * If pos\end should be interpreted literally (that is, withouth including leading and trailing trivia), `leadingTriviaOption` should be set to `LeadingTriviaOption.Exclude` + * and `trailingTriviaOption` to `TrailingTriviaOption.Exclude`. + */ +/* @internal */ +export interface ConfigurableStartEnd extends ConfigurableStart, ConfigurableEnd { +} +/* @internal */ +const useNonAdjustedPositions: ConfigurableStartEnd = { + leadingTriviaOption: LeadingTriviaOption.Exclude, + trailingTriviaOption: TrailingTriviaOption.Exclude, +}; +/* @internal */ +export interface InsertNodeOptions { /** - * Usually node.pos points to a position immediately after the previous token. - * If this position is used as a beginning of the span to remove - it might lead to removing the trailing trivia of the previous node, i.e: - * const x; // this is x - * ^ - pos for the next variable declaration will point here - * const y; // this is y - * ^ - end for previous variable declaration - * Usually leading trivia of the variable declaration 'y' should not include trailing trivia (whitespace, comment 'this is x' and newline) from the preceding - * variable declaration and trailing trivia for 'y' should include (whitespace, comment 'this is y', newline). - * By default when removing nodes we adjust start and end positions to respect specification of the trivia above. - * If pos\end should be interpreted literally (that is, withouth including leading and trailing trivia), `leadingTriviaOption` should be set to `LeadingTriviaOption.Exclude` - * and `trailingTriviaOption` to `TrailingTriviaOption.Exclude`. + * Text to be inserted before the new node */ - export interface ConfigurableStartEnd extends ConfigurableStart, ConfigurableEnd {} - - const useNonAdjustedPositions: ConfigurableStartEnd = { - leadingTriviaOption: LeadingTriviaOption.Exclude, - trailingTriviaOption: TrailingTriviaOption.Exclude, - }; - - export interface InsertNodeOptions { - /** - * Text to be inserted before the new node - */ - prefix?: string; - /** - * Text to be inserted after the new node - */ - suffix?: string; - /** - * Text of inserted node will be formatted with this indentation, otherwise indentation will be inferred from the old node - */ - indentation?: number; - /** - * Text of inserted node will be formatted with this delta, otherwise delta will be inferred from the new node kind - */ - delta?: number; - /** - * Do not trim leading white spaces in the edit range - */ - preserveLeadingWhitespace?: boolean; - } - - export interface ReplaceWithMultipleNodesOptions extends InsertNodeOptions { - readonly joiner?: string; - } - - enum ChangeKind { - Remove, - ReplaceWithSingleNode, - ReplaceWithMultipleNodes, - Text, - } - - type Change = ReplaceWithSingleNode | ReplaceWithMultipleNodes | RemoveNode | ChangeText; - - interface BaseChange { - readonly sourceFile: SourceFile; - readonly range: TextRange; - } - - export interface ChangeNodeOptions extends ConfigurableStartEnd, InsertNodeOptions {} - interface ReplaceWithSingleNode extends BaseChange { - readonly kind: ChangeKind.ReplaceWithSingleNode; - readonly node: Node; - readonly options?: InsertNodeOptions; - } - - interface RemoveNode extends BaseChange { - readonly kind: ChangeKind.Remove; - readonly node?: never; - readonly options?: never; - } - - interface ReplaceWithMultipleNodes extends BaseChange { - readonly kind: ChangeKind.ReplaceWithMultipleNodes; - readonly nodes: readonly Node[]; - readonly options?: ReplaceWithMultipleNodesOptions; - } - - interface ChangeText extends BaseChange { - readonly kind: ChangeKind.Text; - readonly text: string; - } - - function getAdjustedRange(sourceFile: SourceFile, startNode: Node, endNode: Node, options: ConfigurableStartEnd): TextRange { - return { pos: getAdjustedStartPosition(sourceFile, startNode, options), end: getAdjustedEndPosition(sourceFile, endNode, options) }; - } - - function getAdjustedStartPosition(sourceFile: SourceFile, node: Node, options: ConfigurableStart) { - const { leadingTriviaOption } = options; - if (leadingTriviaOption === LeadingTriviaOption.Exclude) { - return node.getStart(sourceFile); - } - const fullStart = node.getFullStart(); - const start = node.getStart(sourceFile); - if (fullStart === start) { - return start; - } - const fullStartLine = getLineStartPositionForPosition(fullStart, sourceFile); - const startLine = getLineStartPositionForPosition(start, sourceFile); - if (startLine === fullStartLine) { - // full start and start of the node are on the same line - // a, b; - // ^ ^ - // | start - // fullstart - // when b is replaced - we usually want to keep the leading trvia - // when b is deleted - we delete it - return leadingTriviaOption === LeadingTriviaOption.IncludeAll ? fullStart : start; - } - // get start position of the line following the line that contains fullstart position - // (but only if the fullstart isn't the very beginning of the file) - const nextLineStart = fullStart > 0 ? 1 : 0; - let adjustedStartPosition = getStartPositionOfLine(getLineOfLocalPosition(sourceFile, fullStartLine) + nextLineStart, sourceFile); - // skip whitespaces/newlines - adjustedStartPosition = skipWhitespacesAndLineBreaks(sourceFile.text, adjustedStartPosition); - return getStartPositionOfLine(getLineOfLocalPosition(sourceFile, adjustedStartPosition), sourceFile); - } - - function getAdjustedEndPosition(sourceFile: SourceFile, node: Node, options: ConfigurableEnd) { - const { end } = node; - const { trailingTriviaOption } = options; - if (trailingTriviaOption === TrailingTriviaOption.Exclude || (isExpression(node) && trailingTriviaOption !== TrailingTriviaOption.Include)) { - return end; - } - const newEnd = skipTrivia(sourceFile.text, end, /*stopAfterLineBreak*/ true); - return newEnd !== end && (trailingTriviaOption === TrailingTriviaOption.Include || isLineBreak(sourceFile.text.charCodeAt(newEnd - 1))) - ? newEnd - : end; - } - + prefix?: string; /** - * Checks if 'candidate' argument is a legal separator in the list that contains 'node' as an element + * Text to be inserted after the new node */ - function isSeparator(node: Node, candidate: Node | undefined): candidate is Token { - return !!candidate && !!node.parent && (candidate.kind === SyntaxKind.CommaToken || (candidate.kind === SyntaxKind.SemicolonToken && node.parent.kind === SyntaxKind.ObjectLiteralExpression)); - } - - function spaces(count: number) { - let s = ""; - for (let i = 0; i < count; i++) { - s += " "; - } - return s; - } - - export interface TextChangesContext { - host: LanguageServiceHost; - formatContext: formatting.FormatContext; - preferences: UserPreferences; - } - - export type TypeAnnotatable = SignatureDeclaration | VariableDeclaration | ParameterDeclaration | PropertyDeclaration | PropertySignature; - - export type ThisTypeAnnotatable = FunctionDeclaration | FunctionExpression; - - export function isThisTypeAnnotatable(containingFunction: FunctionLike): containingFunction is ThisTypeAnnotatable { - return isFunctionExpression(containingFunction) || isFunctionDeclaration(containingFunction); - } - - export class ChangeTracker { - private readonly changes: Change[] = []; - private readonly newFiles: { readonly oldFile: SourceFile | undefined, readonly fileName: string, readonly statements: readonly Statement[] }[] = []; - private readonly classesWithNodesInsertedAtStart = createMap<{ readonly node: ClassDeclaration | InterfaceDeclaration | ObjectLiteralExpression, readonly sourceFile: SourceFile }>(); // Set implemented as Map - private readonly deletedNodes: { readonly sourceFile: SourceFile, readonly node: Node | NodeArray }[] = []; - - public static fromContext(context: TextChangesContext): ChangeTracker { - return new ChangeTracker(getNewLineOrDefaultFromHost(context.host, context.formatContext.options), context.formatContext); + suffix?: string; + /** + * Text of inserted node will be formatted with this indentation, otherwise indentation will be inferred from the old node + */ + indentation?: number; + /** + * Text of inserted node will be formatted with this delta, otherwise delta will be inferred from the new node kind + */ + delta?: number; + /** + * Do not trim leading white spaces in the edit range + */ + preserveLeadingWhitespace?: boolean; +} +/* @internal */ +export interface ReplaceWithMultipleNodesOptions extends InsertNodeOptions { + readonly joiner?: string; +} +/* @internal */ +enum ChangeKind { + Remove, + ReplaceWithSingleNode, + ReplaceWithMultipleNodes, + Text +} +/* @internal */ +type Change = ReplaceWithSingleNode | ReplaceWithMultipleNodes | RemoveNode | ChangeText; +/* @internal */ +interface BaseChange { + readonly sourceFile: SourceFile; + readonly range: TextRange; +} +/* @internal */ +export interface ChangeNodeOptions extends ConfigurableStartEnd, InsertNodeOptions { +} +/* @internal */ +interface ReplaceWithSingleNode extends BaseChange { + readonly kind: ChangeKind.ReplaceWithSingleNode; + readonly node: Node; + readonly options?: InsertNodeOptions; +} +/* @internal */ +interface RemoveNode extends BaseChange { + readonly kind: ChangeKind.Remove; + readonly node?: never; + readonly options?: never; +} +/* @internal */ +interface ReplaceWithMultipleNodes extends BaseChange { + readonly kind: ChangeKind.ReplaceWithMultipleNodes; + readonly nodes: readonly Node[]; + readonly options?: ReplaceWithMultipleNodesOptions; +} +/* @internal */ +interface ChangeText extends BaseChange { + readonly kind: ChangeKind.Text; + readonly text: string; +} +/* @internal */ +function getAdjustedRange(sourceFile: SourceFile, startNode: Node, endNode: Node, options: ConfigurableStartEnd): TextRange { + return { pos: getAdjustedStartPosition(sourceFile, startNode, options), end: getAdjustedEndPosition(sourceFile, endNode, options) }; +} +/* @internal */ +function getAdjustedStartPosition(sourceFile: SourceFile, node: Node, options: ConfigurableStart) { + const { leadingTriviaOption } = options; + if (leadingTriviaOption === LeadingTriviaOption.Exclude) { + return node.getStart(sourceFile); + } + const fullStart = node.getFullStart(); + const start = node.getStart(sourceFile); + if (fullStart === start) { + return start; + } + const fullStartLine = getLineStartPositionForPosition(fullStart, sourceFile); + const startLine = getLineStartPositionForPosition(start, sourceFile); + if (startLine === fullStartLine) { + // full start and start of the node are on the same line + // a, b; + // ^ ^ + // | start + // fullstart + // when b is replaced - we usually want to keep the leading trvia + // when b is deleted - we delete it + return leadingTriviaOption === LeadingTriviaOption.IncludeAll ? fullStart : start; + } + // get start position of the line following the line that contains fullstart position + // (but only if the fullstart isn't the very beginning of the file) + const nextLineStart = fullStart > 0 ? 1 : 0; + let adjustedStartPosition = getStartPositionOfLine(getLineOfLocalPosition(sourceFile, fullStartLine) + nextLineStart, sourceFile); + // skip whitespaces/newlines + adjustedStartPosition = skipWhitespacesAndLineBreaks(sourceFile.text, adjustedStartPosition); + return getStartPositionOfLine(getLineOfLocalPosition(sourceFile, adjustedStartPosition), sourceFile); +} +/* @internal */ +function getAdjustedEndPosition(sourceFile: SourceFile, node: Node, options: ConfigurableEnd) { + const { end } = node; + const { trailingTriviaOption } = options; + if (trailingTriviaOption === TrailingTriviaOption.Exclude || (isExpression(node) && trailingTriviaOption !== TrailingTriviaOption.Include)) { + return end; + } + const newEnd = skipTrivia(sourceFile.text, end, /*stopAfterLineBreak*/ true); + return newEnd !== end && (trailingTriviaOption === TrailingTriviaOption.Include || isLineBreak(sourceFile.text.charCodeAt(newEnd - 1))) + ? newEnd + : end; +} +/** + * Checks if 'candidate' argument is a legal separator in the list that contains 'node' as an element + */ +/* @internal */ +function isSeparator(node: Node, candidate: Node | undefined): candidate is Token { + return !!candidate && !!node.parent && (candidate.kind === SyntaxKind.CommaToken || (candidate.kind === SyntaxKind.SemicolonToken && node.parent.kind === SyntaxKind.ObjectLiteralExpression)); +} +/* @internal */ +function spaces(count: number) { + let s = ""; + for (let i = 0; i < count; i++) { + s += " "; + } + return s; +} +/* @internal */ +export interface TextChangesContext { + host: LanguageServiceHost; + formatContext: FormatContext; + preferences: UserPreferences; +} +/* @internal */ +export type TypeAnnotatable = SignatureDeclaration | VariableDeclaration | ParameterDeclaration | PropertyDeclaration | PropertySignature; +/* @internal */ +export type ThisTypeAnnotatable = FunctionDeclaration | FunctionExpression; +/* @internal */ +export function isThisTypeAnnotatable(containingFunction: FunctionLike): containingFunction is ThisTypeAnnotatable { + return isFunctionExpression(containingFunction) || isFunctionDeclaration(containingFunction); +} +/* @internal */ +export class ChangeTracker { + private readonly changes: Change[] = []; + private readonly newFiles: { + readonly oldFile: SourceFile | undefined; + readonly fileName: string; + readonly statements: readonly Statement[]; + }[] = []; + private readonly classesWithNodesInsertedAtStart = createMap<{ + readonly node: ClassDeclaration | InterfaceDeclaration | ObjectLiteralExpression; + readonly sourceFile: SourceFile; + }>(); // Set implemented as Map + private readonly deletedNodes: { + readonly sourceFile: SourceFile; + readonly node: Node | NodeArray; + }[] = []; + public static fromContext(context: TextChangesContext): ChangeTracker { + return new ChangeTracker(getNewLineOrDefaultFromHost(context.host, context.formatContext.options), context.formatContext); + } + public static with(context: TextChangesContext, cb: (tracker: ChangeTracker) => void): FileTextChanges[] { + const tracker = ChangeTracker.fromContext(context); + cb(tracker); + return tracker.getChanges(); + } + /** Public for tests only. Other callers should use `ChangeTracker.with`. */ + constructor(private readonly newLineCharacter: string, private readonly formatContext: FormatContext) { } + public pushRaw(sourceFile: SourceFile, change: FileTextChanges) { + Debug.assertEqual(sourceFile.fileName, change.fileName); + for (const c of change.textChanges) { + this.changes.push({ + kind: ChangeKind.Text, + sourceFile, + text: c.newText, + range: createTextRangeFromSpan(c.span), + }); } - - public static with(context: TextChangesContext, cb: (tracker: ChangeTracker) => void): FileTextChanges[] { - const tracker = ChangeTracker.fromContext(context); - cb(tracker); - return tracker.getChanges(); + } + public deleteRange(sourceFile: SourceFile, range: TextRange): void { + this.changes.push({ kind: ChangeKind.Remove, sourceFile, range }); + } + delete(sourceFile: SourceFile, node: Node | NodeArray): void { + this.deletedNodes.push({ sourceFile, node }); + } + public deleteModifier(sourceFile: SourceFile, modifier: Modifier): void { + this.deleteRange(sourceFile, { pos: modifier.getStart(sourceFile), end: skipTrivia(sourceFile.text, modifier.end, /*stopAfterLineBreak*/ true) }); + } + public deleteNodeRange(sourceFile: SourceFile, startNode: Node, endNode: Node, options: ConfigurableStartEnd = { leadingTriviaOption: LeadingTriviaOption.IncludeAll }): void { + const startPosition = getAdjustedStartPosition(sourceFile, startNode, options); + const endPosition = getAdjustedEndPosition(sourceFile, endNode, options); + this.deleteRange(sourceFile, { pos: startPosition, end: endPosition }); + } + public deleteNodeRangeExcludingEnd(sourceFile: SourceFile, startNode: Node, afterEndNode: Node | undefined, options: ConfigurableStartEnd = { leadingTriviaOption: LeadingTriviaOption.IncludeAll }): void { + const startPosition = getAdjustedStartPosition(sourceFile, startNode, options); + const endPosition = afterEndNode === undefined ? sourceFile.text.length : getAdjustedStartPosition(sourceFile, afterEndNode, options); + this.deleteRange(sourceFile, { pos: startPosition, end: endPosition }); + } + public replaceRange(sourceFile: SourceFile, range: TextRange, newNode: Node, options: InsertNodeOptions = {}): void { + this.changes.push({ kind: ChangeKind.ReplaceWithSingleNode, sourceFile, range, options, node: newNode }); + } + public replaceNode(sourceFile: SourceFile, oldNode: Node, newNode: Node, options: ChangeNodeOptions = useNonAdjustedPositions): void { + this.replaceRange(sourceFile, getAdjustedRange(sourceFile, oldNode, oldNode, options), newNode, options); + } + public replaceNodeRange(sourceFile: SourceFile, startNode: Node, endNode: Node, newNode: Node, options: ChangeNodeOptions = useNonAdjustedPositions): void { + this.replaceRange(sourceFile, getAdjustedRange(sourceFile, startNode, endNode, options), newNode, options); + } + private replaceRangeWithNodes(sourceFile: SourceFile, range: TextRange, newNodes: readonly Node[], options: ReplaceWithMultipleNodesOptions & ConfigurableStartEnd = {}): void { + this.changes.push({ kind: ChangeKind.ReplaceWithMultipleNodes, sourceFile, range, options, nodes: newNodes }); + } + public replaceNodeWithNodes(sourceFile: SourceFile, oldNode: Node, newNodes: readonly Node[], options: ChangeNodeOptions = useNonAdjustedPositions): void { + this.replaceRangeWithNodes(sourceFile, getAdjustedRange(sourceFile, oldNode, oldNode, options), newNodes, options); + } + public replaceNodeWithText(sourceFile: SourceFile, oldNode: Node, text: string): void { + this.replaceRangeWithText(sourceFile, getAdjustedRange(sourceFile, oldNode, oldNode, useNonAdjustedPositions), text); + } + public replaceNodeRangeWithNodes(sourceFile: SourceFile, startNode: Node, endNode: Node, newNodes: readonly Node[], options: ReplaceWithMultipleNodesOptions & ConfigurableStartEnd = useNonAdjustedPositions): void { + this.replaceRangeWithNodes(sourceFile, getAdjustedRange(sourceFile, startNode, endNode, options), newNodes, options); + } + private nextCommaToken(sourceFile: SourceFile, node: Node): Node | undefined { + const next = findNextToken(node, node.parent, sourceFile); + return next && next.kind === SyntaxKind.CommaToken ? next : undefined; + } + public replacePropertyAssignment(sourceFile: SourceFile, oldNode: PropertyAssignment, newNode: PropertyAssignment): void { + const suffix = this.nextCommaToken(sourceFile, oldNode) ? "" : ("," + this.newLineCharacter); + this.replaceNode(sourceFile, oldNode, newNode, { suffix }); + } + public insertNodeAt(sourceFile: SourceFile, pos: number, newNode: Node, options: InsertNodeOptions = {}): void { + this.replaceRange(sourceFile, createRange(pos), newNode, options); + } + private insertNodesAt(sourceFile: SourceFile, pos: number, newNodes: readonly Node[], options: ReplaceWithMultipleNodesOptions = {}): void { + this.replaceRangeWithNodes(sourceFile, createRange(pos), newNodes, options); + } + public insertNodeAtTopOfFile(sourceFile: SourceFile, newNode: Statement, blankLineBetween: boolean): void { + const pos = getInsertionPositionAtSourceFileTop(sourceFile); + this.insertNodeAt(sourceFile, pos, newNode, { + prefix: pos === 0 ? undefined : this.newLineCharacter, + suffix: (isLineBreak(sourceFile.text.charCodeAt(pos)) ? "" : this.newLineCharacter) + (blankLineBetween ? this.newLineCharacter : ""), + }); + } + public insertNodeBefore(sourceFile: SourceFile, before: Node, newNode: Node, blankLineBetween = false): void { + this.insertNodeAt(sourceFile, getAdjustedStartPosition(sourceFile, before, {}), newNode, this.getOptionsForInsertNodeBefore(before, blankLineBetween)); + } + public insertModifierBefore(sourceFile: SourceFile, modifier: SyntaxKind, before: Node): void { + const pos = before.getStart(sourceFile); + this.insertNodeAt(sourceFile, pos, createToken(modifier), { suffix: " " }); + } + public insertLastModifierBefore(sourceFile: SourceFile, modifier: SyntaxKind, before: Node): void { + if (!before.modifiers) { + this.insertModifierBefore(sourceFile, modifier, before); + return; } - - /** Public for tests only. Other callers should use `ChangeTracker.with`. */ - constructor(private readonly newLineCharacter: string, private readonly formatContext: formatting.FormatContext) {} - - public pushRaw(sourceFile: SourceFile, change: FileTextChanges) { - Debug.assertEqual(sourceFile.fileName, change.fileName); - for (const c of change.textChanges) { - this.changes.push({ - kind: ChangeKind.Text, - sourceFile, - text: c.newText, - range: createTextRangeFromSpan(c.span), + const pos = before.modifiers.end; + this.insertNodeAt(sourceFile, pos, createToken(modifier), { prefix: " " }); + } + public insertCommentBeforeLine(sourceFile: SourceFile, lineNumber: number, position: number, commentText: string): void { + const lineStartPosition = getStartPositionOfLine(lineNumber, sourceFile); + const startPosition = getFirstNonSpaceCharacterPosition(sourceFile.text, lineStartPosition); + // First try to see if we can put the comment on the previous line. + // We need to make sure that we are not in the middle of a string literal or a comment. + // If so, we do not want to separate the node from its comment if we can. + // Otherwise, add an extra new line immediately before the error span. + const insertAtLineStart = isValidLocationToAddComment(sourceFile, startPosition); + const token = getTouchingToken(sourceFile, insertAtLineStart ? startPosition : position); + const indent = sourceFile.text.slice(lineStartPosition, startPosition); + const text = `${insertAtLineStart ? "" : this.newLineCharacter}//${commentText}${this.newLineCharacter}${indent}`; + this.insertText(sourceFile, token.getStart(sourceFile), text); + } + public insertJsdocCommentBefore(sourceFile: SourceFile, node: HasJSDoc, tag: JSDoc): void { + const fnStart = node.getStart(sourceFile); + if (node.jsDoc) { + for (const jsdoc of node.jsDoc) { + this.deleteRange(sourceFile, { + pos: getLineStartPositionForPosition(jsdoc.getStart(sourceFile), sourceFile), + end: getAdjustedEndPosition(sourceFile, jsdoc, /*options*/ {}) }); } } - - public deleteRange(sourceFile: SourceFile, range: TextRange): void { - this.changes.push({ kind: ChangeKind.Remove, sourceFile, range }); - } - - delete(sourceFile: SourceFile, node: Node | NodeArray): void { - this.deletedNodes.push({ sourceFile, node }); - } - - public deleteModifier(sourceFile: SourceFile, modifier: Modifier): void { - this.deleteRange(sourceFile, { pos: modifier.getStart(sourceFile), end: skipTrivia(sourceFile.text, modifier.end, /*stopAfterLineBreak*/ true) }); - } - - public deleteNodeRange(sourceFile: SourceFile, startNode: Node, endNode: Node, options: ConfigurableStartEnd = { leadingTriviaOption: LeadingTriviaOption.IncludeAll }): void { - const startPosition = getAdjustedStartPosition(sourceFile, startNode, options); - const endPosition = getAdjustedEndPosition(sourceFile, endNode, options); - this.deleteRange(sourceFile, { pos: startPosition, end: endPosition }); - } - - public deleteNodeRangeExcludingEnd(sourceFile: SourceFile, startNode: Node, afterEndNode: Node | undefined, options: ConfigurableStartEnd = { leadingTriviaOption: LeadingTriviaOption.IncludeAll }): void { - const startPosition = getAdjustedStartPosition(sourceFile, startNode, options); - const endPosition = afterEndNode === undefined ? sourceFile.text.length : getAdjustedStartPosition(sourceFile, afterEndNode, options); - this.deleteRange(sourceFile, { pos: startPosition, end: endPosition }); - } - - public replaceRange(sourceFile: SourceFile, range: TextRange, newNode: Node, options: InsertNodeOptions = {}): void { - this.changes.push({ kind: ChangeKind.ReplaceWithSingleNode, sourceFile, range, options, node: newNode }); - } - - public replaceNode(sourceFile: SourceFile, oldNode: Node, newNode: Node, options: ChangeNodeOptions = useNonAdjustedPositions): void { - this.replaceRange(sourceFile, getAdjustedRange(sourceFile, oldNode, oldNode, options), newNode, options); - } - - public replaceNodeRange(sourceFile: SourceFile, startNode: Node, endNode: Node, newNode: Node, options: ChangeNodeOptions = useNonAdjustedPositions): void { - this.replaceRange(sourceFile, getAdjustedRange(sourceFile, startNode, endNode, options), newNode, options); - } - - private replaceRangeWithNodes(sourceFile: SourceFile, range: TextRange, newNodes: readonly Node[], options: ReplaceWithMultipleNodesOptions & ConfigurableStartEnd = {}): void { - this.changes.push({ kind: ChangeKind.ReplaceWithMultipleNodes, sourceFile, range, options, nodes: newNodes }); - } - - public replaceNodeWithNodes(sourceFile: SourceFile, oldNode: Node, newNodes: readonly Node[], options: ChangeNodeOptions = useNonAdjustedPositions): void { - this.replaceRangeWithNodes(sourceFile, getAdjustedRange(sourceFile, oldNode, oldNode, options), newNodes, options); - } - - public replaceNodeWithText(sourceFile: SourceFile, oldNode: Node, text: string): void { - this.replaceRangeWithText(sourceFile, getAdjustedRange(sourceFile, oldNode, oldNode, useNonAdjustedPositions), text); + const startPosition = getPrecedingNonSpaceCharacterPosition(sourceFile.text, fnStart - 1); + const indent = sourceFile.text.slice(startPosition, fnStart); + this.insertNodeAt(sourceFile, fnStart, tag, { preserveLeadingWhitespace: false, suffix: this.newLineCharacter + indent }); + } + public replaceRangeWithText(sourceFile: SourceFile, range: TextRange, text: string): void { + this.changes.push({ kind: ChangeKind.Text, sourceFile, range, text }); + } + public insertText(sourceFile: SourceFile, pos: number, text: string): void { + this.replaceRangeWithText(sourceFile, createRange(pos), text); + } + /** Prefer this over replacing a node with another that has a type annotation, as it avoids reformatting the other parts of the node. */ + public tryInsertTypeAnnotation(sourceFile: SourceFile, node: TypeAnnotatable, type: TypeNode): boolean { + let endNode: Node | undefined; + if (isFunctionLike(node)) { + endNode = findChildOfKind(node, SyntaxKind.CloseParenToken, sourceFile); + if (!endNode) { + if (!isArrowFunction(node)) + return false; // Function missing parentheses, give up + // If no `)`, is an arrow function `x => x`, so use the end of the first parameter + endNode = first(node.parameters); + } } - - public replaceNodeRangeWithNodes(sourceFile: SourceFile, startNode: Node, endNode: Node, newNodes: readonly Node[], options: ReplaceWithMultipleNodesOptions & ConfigurableStartEnd = useNonAdjustedPositions): void { - this.replaceRangeWithNodes(sourceFile, getAdjustedRange(sourceFile, startNode, endNode, options), newNodes, options); + else { + endNode = node.kind !== SyntaxKind.VariableDeclaration && node.questionToken ? node.questionToken : node.name; } - - private nextCommaToken(sourceFile: SourceFile, node: Node): Node | undefined { - const next = findNextToken(node, node.parent, sourceFile); - return next && next.kind === SyntaxKind.CommaToken ? next : undefined; + this.insertNodeAt(sourceFile, endNode.end, type, { prefix: ": " }); + return true; + } + public tryInsertThisTypeAnnotation(sourceFile: SourceFile, node: ThisTypeAnnotatable, type: TypeNode): void { + const start = findChildOfKind(node, SyntaxKind.OpenParenToken, sourceFile)!.getStart(sourceFile) + 1; + const suffix = node.parameters.length ? ", " : ""; + this.insertNodeAt(sourceFile, start, type, { prefix: "this: ", suffix }); + } + public insertTypeParameters(sourceFile: SourceFile, node: SignatureDeclaration, typeParameters: readonly TypeParameterDeclaration[]): void { + // If no `(`, is an arrow function `x => x`, so use the pos of the first parameter + const start = (findChildOfKind(node, SyntaxKind.OpenParenToken, sourceFile) || first(node.parameters)).getStart(sourceFile); + this.insertNodesAt(sourceFile, start, typeParameters, { prefix: "<", suffix: ">" }); + } + private getOptionsForInsertNodeBefore(before: Node, doubleNewlines: boolean): InsertNodeOptions { + if (isStatement(before) || isClassElement(before)) { + return { suffix: doubleNewlines ? this.newLineCharacter + this.newLineCharacter : this.newLineCharacter }; } - - public replacePropertyAssignment(sourceFile: SourceFile, oldNode: PropertyAssignment, newNode: PropertyAssignment): void { - const suffix = this.nextCommaToken(sourceFile, oldNode) ? "" : ("," + this.newLineCharacter); - this.replaceNode(sourceFile, oldNode, newNode, { suffix }); + else if (isVariableDeclaration(before)) { // insert `x = 1, ` into `const x = 1, y = 2; + return { suffix: ", " }; } - - public insertNodeAt(sourceFile: SourceFile, pos: number, newNode: Node, options: InsertNodeOptions = {}): void { - this.replaceRange(sourceFile, createRange(pos), newNode, options); + else if (isParameter(before)) { + return {}; } - - private insertNodesAt(sourceFile: SourceFile, pos: number, newNodes: readonly Node[], options: ReplaceWithMultipleNodesOptions = {}): void { - this.replaceRangeWithNodes(sourceFile, createRange(pos), newNodes, options); + else if (isStringLiteral(before) && isImportDeclaration(before.parent) || isNamedImports(before)) { + return { suffix: ", " }; } - - public insertNodeAtTopOfFile(sourceFile: SourceFile, newNode: Statement, blankLineBetween: boolean): void { - const pos = getInsertionPositionAtSourceFileTop(sourceFile); - this.insertNodeAt(sourceFile, pos, newNode, { - prefix: pos === 0 ? undefined : this.newLineCharacter, - suffix: (isLineBreak(sourceFile.text.charCodeAt(pos)) ? "" : this.newLineCharacter) + (blankLineBetween ? this.newLineCharacter : ""), - }); - } - - public insertNodeBefore(sourceFile: SourceFile, before: Node, newNode: Node, blankLineBetween = false): void { - this.insertNodeAt(sourceFile, getAdjustedStartPosition(sourceFile, before, {}), newNode, this.getOptionsForInsertNodeBefore(before, blankLineBetween)); + return Debug.failBadSyntaxKind(before); // We haven't handled this kind of node yet -- add it + } + public insertNodeAtConstructorStart(sourceFile: SourceFile, ctr: ConstructorDeclaration, newStatement: Statement): void { + const firstStatement = firstOrUndefined(ctr.body!.statements); + if (!firstStatement || !ctr.body!.multiLine) { + this.replaceConstructorBody(sourceFile, ctr, [newStatement, ...ctr.body!.statements]); } - - public insertModifierBefore(sourceFile: SourceFile, modifier: SyntaxKind, before: Node): void { - const pos = before.getStart(sourceFile); - this.insertNodeAt(sourceFile, pos, createToken(modifier), { suffix: " " }); + else { + this.insertNodeBefore(sourceFile, firstStatement, newStatement); } - - public insertLastModifierBefore(sourceFile: SourceFile, modifier: SyntaxKind, before: Node): void { - if (!before.modifiers) { - this.insertModifierBefore(sourceFile, modifier, before); - return; - } - - const pos = before.modifiers.end; - this.insertNodeAt(sourceFile, pos, createToken(modifier), { prefix: " " }); + } + public insertNodeAtConstructorEnd(sourceFile: SourceFile, ctr: ConstructorDeclaration, newStatement: Statement): void { + const lastStatement = lastOrUndefined(ctr.body!.statements); + if (!lastStatement || !ctr.body!.multiLine) { + this.replaceConstructorBody(sourceFile, ctr, [...ctr.body!.statements, newStatement]); } - - public insertCommentBeforeLine(sourceFile: SourceFile, lineNumber: number, position: number, commentText: string): void { - const lineStartPosition = getStartPositionOfLine(lineNumber, sourceFile); - const startPosition = getFirstNonSpaceCharacterPosition(sourceFile.text, lineStartPosition); - // First try to see if we can put the comment on the previous line. - // We need to make sure that we are not in the middle of a string literal or a comment. - // If so, we do not want to separate the node from its comment if we can. - // Otherwise, add an extra new line immediately before the error span. - const insertAtLineStart = isValidLocationToAddComment(sourceFile, startPosition); - const token = getTouchingToken(sourceFile, insertAtLineStart ? startPosition : position); - const indent = sourceFile.text.slice(lineStartPosition, startPosition); - const text = `${insertAtLineStart ? "" : this.newLineCharacter}//${commentText}${this.newLineCharacter}${indent}`; - this.insertText(sourceFile, token.getStart(sourceFile), text); + else { + this.insertNodeAfter(sourceFile, lastStatement, newStatement); } - - public insertJsdocCommentBefore(sourceFile: SourceFile, node: HasJSDoc, tag: JSDoc): void { - const fnStart = node.getStart(sourceFile); - if (node.jsDoc) { - for (const jsdoc of node.jsDoc) { - this.deleteRange(sourceFile, { - pos: getLineStartPositionForPosition(jsdoc.getStart(sourceFile), sourceFile), - end: getAdjustedEndPosition(sourceFile, jsdoc, /*options*/ {}) - }); - } + } + private replaceConstructorBody(sourceFile: SourceFile, ctr: ConstructorDeclaration, statements: readonly Statement[]): void { + this.replaceNode(sourceFile, (ctr.body!), createBlock(statements, /*multiLine*/ true)); + } + public insertNodeAtEndOfScope(sourceFile: SourceFile, scope: Node, newNode: Node): void { + const pos = getAdjustedStartPosition(sourceFile, scope.getLastToken()!, {}); + this.insertNodeAt(sourceFile, pos, newNode, { + prefix: isLineBreak(sourceFile.text.charCodeAt(scope.getLastToken()!.pos)) ? this.newLineCharacter : this.newLineCharacter + this.newLineCharacter, + suffix: this.newLineCharacter + }); + } + public insertNodeAtClassStart(sourceFile: SourceFile, cls: ClassLikeDeclaration | InterfaceDeclaration, newElement: ClassElement): void { + this.insertNodeAtStartWorker(sourceFile, cls, newElement); + } + public insertNodeAtObjectStart(sourceFile: SourceFile, obj: ObjectLiteralExpression, newElement: ObjectLiteralElementLike): void { + this.insertNodeAtStartWorker(sourceFile, obj, newElement); + } + private insertNodeAtStartWorker(sourceFile: SourceFile, cls: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression, newElement: ClassElement | ObjectLiteralElementLike): void { + const indentation = this.guessIndentationFromExistingMembers(sourceFile, cls) ?? this.computeIndentationForNewMember(sourceFile, cls); + this.insertNodeAt(sourceFile, getMembersOrProperties(cls).pos, newElement, this.getInsertNodeAtStartInsertOptions(sourceFile, cls, indentation)); + } + /** + * Tries to guess the indentation from the existing members of a class/interface/object. All members must be on + * new lines and must share the same indentation. + */ + private guessIndentationFromExistingMembers(sourceFile: SourceFile, cls: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression) { + let indentation: number | undefined; + let lastRange: TextRange = cls; + for (const member of getMembersOrProperties(cls)) { + if (rangeStartPositionsAreOnSameLine(lastRange, member, sourceFile)) { + // each indented member must be on a new line + return undefined; } - const startPosition = getPrecedingNonSpaceCharacterPosition(sourceFile.text, fnStart - 1); - const indent = sourceFile.text.slice(startPosition, fnStart); - this.insertNodeAt(sourceFile, fnStart, tag, { preserveLeadingWhitespace: false, suffix: this.newLineCharacter + indent }); - } - - public replaceRangeWithText(sourceFile: SourceFile, range: TextRange, text: string): void { - this.changes.push({ kind: ChangeKind.Text, sourceFile, range, text }); - } - - public insertText(sourceFile: SourceFile, pos: number, text: string): void { - this.replaceRangeWithText(sourceFile, createRange(pos), text); - } - - /** Prefer this over replacing a node with another that has a type annotation, as it avoids reformatting the other parts of the node. */ - public tryInsertTypeAnnotation(sourceFile: SourceFile, node: TypeAnnotatable, type: TypeNode): boolean { - let endNode: Node | undefined; - if (isFunctionLike(node)) { - endNode = findChildOfKind(node, SyntaxKind.CloseParenToken, sourceFile); - if (!endNode) { - if (!isArrowFunction(node)) return false; // Function missing parentheses, give up - // If no `)`, is an arrow function `x => x`, so use the end of the first parameter - endNode = first(node.parameters); - } + const memberStart = member.getStart(sourceFile); + const memberIndentation = SmartIndenter.findFirstNonWhitespaceColumn(getLineStartPositionForPosition(memberStart, sourceFile), memberStart, sourceFile, this.formatContext.options); + if (indentation === undefined) { + indentation = memberIndentation; } - else { - endNode = node.kind !== SyntaxKind.VariableDeclaration && node.questionToken ? node.questionToken : node.name; + else if (memberIndentation !== indentation) { + // indentation of multiple members is not consistent + return undefined; } - - this.insertNodeAt(sourceFile, endNode.end, type, { prefix: ": " }); - return true; - } - - public tryInsertThisTypeAnnotation(sourceFile: SourceFile, node: ThisTypeAnnotatable, type: TypeNode): void { - const start = findChildOfKind(node, SyntaxKind.OpenParenToken, sourceFile)!.getStart(sourceFile) + 1; - const suffix = node.parameters.length ? ", " : ""; - - this.insertNodeAt(sourceFile, start, type, { prefix: "this: ", suffix }); - } - - public insertTypeParameters(sourceFile: SourceFile, node: SignatureDeclaration, typeParameters: readonly TypeParameterDeclaration[]): void { - // If no `(`, is an arrow function `x => x`, so use the pos of the first parameter - const start = (findChildOfKind(node, SyntaxKind.OpenParenToken, sourceFile) || first(node.parameters)).getStart(sourceFile); - this.insertNodesAt(sourceFile, start, typeParameters, { prefix: "<", suffix: ">" }); + lastRange = member; } - - private getOptionsForInsertNodeBefore(before: Node, doubleNewlines: boolean): InsertNodeOptions { - if (isStatement(before) || isClassElement(before)) { - return { suffix: doubleNewlines ? this.newLineCharacter + this.newLineCharacter : this.newLineCharacter }; - } - else if (isVariableDeclaration(before)) { // insert `x = 1, ` into `const x = 1, y = 2; - return { suffix: ", " }; + return indentation; + } + private computeIndentationForNewMember(sourceFile: SourceFile, cls: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression) { + const clsStart = cls.getStart(sourceFile); + return SmartIndenter.findFirstNonWhitespaceColumn(getLineStartPositionForPosition(clsStart, sourceFile), clsStart, sourceFile, this.formatContext.options) + + (this.formatContext.options.indentSize ?? 4); + } + private getInsertNodeAtStartInsertOptions(sourceFile: SourceFile, cls: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression, indentation: number): InsertNodeOptions { + // Rules: + // - Always insert leading newline. + // - For object literals: + // - Add a trailing comma if there are existing members in the node, or the source file is not a JSON file + // (because trailing commas are generally illegal in a JSON file). + // - Add a leading comma if the source file is not a JSON file, there are existing insertions, + // and the node is empty (because we didn't add a trailing comma per the previous rule). + // - Only insert a trailing newline if body is single-line and there are no other insertions for the node. + // NOTE: This is handled in `finishClassesWithNodesInsertedAtStart`. + const members = getMembersOrProperties(cls); + const isEmpty = members.length === 0; + const isFirstInsertion = addToSeen(this.classesWithNodesInsertedAtStart, getNodeId(cls), { node: cls, sourceFile }); + const insertTrailingComma = isObjectLiteralExpression(cls) && (!isJsonSourceFile(sourceFile) || !isEmpty); + const insertLeadingComma = isObjectLiteralExpression(cls) && isJsonSourceFile(sourceFile) && isEmpty && !isFirstInsertion; + return { + indentation, + prefix: (insertLeadingComma ? "," : "") + this.newLineCharacter, + suffix: insertTrailingComma ? "," : "" + }; + } + public insertNodeAfterComma(sourceFile: SourceFile, after: Node, newNode: Node): void { + const endPosition = this.insertNodeAfterWorker(sourceFile, this.nextCommaToken(sourceFile, after) || after, newNode); + this.insertNodeAt(sourceFile, endPosition, newNode, this.getInsertNodeAfterOptions(sourceFile, after)); + } + public insertNodeAfter(sourceFile: SourceFile, after: Node, newNode: Node): void { + const endPosition = this.insertNodeAfterWorker(sourceFile, after, newNode); + this.insertNodeAt(sourceFile, endPosition, newNode, this.getInsertNodeAfterOptions(sourceFile, after)); + } + public insertNodeAtEndOfList(sourceFile: SourceFile, list: NodeArray, newNode: Node): void { + this.insertNodeAt(sourceFile, list.end, newNode, { prefix: ", " }); + } + public insertNodesAfter(sourceFile: SourceFile, after: Node, newNodes: readonly Node[]): void { + const endPosition = this.insertNodeAfterWorker(sourceFile, after, first(newNodes)); + this.insertNodesAt(sourceFile, endPosition, newNodes, this.getInsertNodeAfterOptions(sourceFile, after)); + } + private insertNodeAfterWorker(sourceFile: SourceFile, after: Node, newNode: Node): number { + if (needSemicolonBetween(after, newNode)) { + // check if previous statement ends with semicolon + // if not - insert semicolon to preserve the code from changing the meaning due to ASI + if (sourceFile.text.charCodeAt(after.end - 1) !== CharacterCodes.semicolon) { + this.replaceRange(sourceFile, createRange(after.end), createToken(SyntaxKind.SemicolonToken)); } - else if (isParameter(before)) { + } + const endPosition = getAdjustedEndPosition(sourceFile, after, {}); + return endPosition; + } + private getInsertNodeAfterOptions(sourceFile: SourceFile, after: Node): InsertNodeOptions { + const options = this.getInsertNodeAfterOptionsWorker(after); + return { + ...options, + prefix: after.end === sourceFile.end && isStatement(after) ? (options.prefix ? `\n${options.prefix}` : "\n") : options.prefix, + }; + } + private getInsertNodeAfterOptionsWorker(node: Node): InsertNodeOptions { + switch (node.kind) { + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ModuleDeclaration: + return { prefix: this.newLineCharacter, suffix: this.newLineCharacter }; + case SyntaxKind.VariableDeclaration: + case SyntaxKind.StringLiteral: + case SyntaxKind.Identifier: + return { prefix: ", " }; + case SyntaxKind.PropertyAssignment: + return { suffix: "," + this.newLineCharacter }; + case SyntaxKind.ExportKeyword: + return { prefix: " " }; + case SyntaxKind.Parameter: return {}; - } - else if (isStringLiteral(before) && isImportDeclaration(before.parent) || isNamedImports(before)) { - return { suffix: ", " }; - } - return Debug.failBadSyntaxKind(before); // We haven't handled this kind of node yet -- add it + default: + Debug.assert(isStatement(node) || isClassOrTypeElement(node)); // Else we haven't handled this kind of node yet -- add it + return { suffix: this.newLineCharacter }; } - - public insertNodeAtConstructorStart(sourceFile: SourceFile, ctr: ConstructorDeclaration, newStatement: Statement): void { - const firstStatement = firstOrUndefined(ctr.body!.statements); - if (!firstStatement || !ctr.body!.multiLine) { - this.replaceConstructorBody(sourceFile, ctr, [newStatement, ...ctr.body!.statements]); + } + public insertName(sourceFile: SourceFile, node: FunctionExpression | ClassExpression | ArrowFunction, name: string): void { + Debug.assert(!node.name); + if (node.kind === SyntaxKind.ArrowFunction) { + const arrow = (findChildOfKind(node, SyntaxKind.EqualsGreaterThanToken, sourceFile)!); + const lparen = findChildOfKind(node, SyntaxKind.OpenParenToken, sourceFile); + if (lparen) { + // `() => {}` --> `function f() {}` + this.insertNodesAt(sourceFile, lparen.getStart(sourceFile), [createToken(SyntaxKind.FunctionKeyword), createIdentifier(name)], { joiner: " " }); + deleteNode(this, sourceFile, arrow); } else { - this.insertNodeBefore(sourceFile, firstStatement, newStatement); + // `x => {}` -> `function f(x) {}` + this.insertText(sourceFile, first(node.parameters).getStart(sourceFile), `function ${name}(`); + // Replacing full range of arrow to get rid of the leading space -- replace ` =>` with `)` + this.replaceRange(sourceFile, arrow, createToken(SyntaxKind.CloseParenToken)); } - } - - public insertNodeAtConstructorEnd(sourceFile: SourceFile, ctr: ConstructorDeclaration, newStatement: Statement): void { - const lastStatement = lastOrUndefined(ctr.body!.statements); - if (!lastStatement || !ctr.body!.multiLine) { - this.replaceConstructorBody(sourceFile, ctr, [...ctr.body!.statements, newStatement]); + if (node.body.kind !== SyntaxKind.Block) { + // `() => 0` => `function f() { return 0; }` + this.insertNodesAt(sourceFile, node.body.getStart(sourceFile), [createToken(SyntaxKind.OpenBraceToken), createToken(SyntaxKind.ReturnKeyword)], { joiner: " ", suffix: " " }); + this.insertNodesAt(sourceFile, node.body.end, [createToken(SyntaxKind.SemicolonToken), createToken(SyntaxKind.CloseBraceToken)], { joiner: " " }); } - else { - this.insertNodeAfter(sourceFile, lastStatement, newStatement); - } - } - - private replaceConstructorBody(sourceFile: SourceFile, ctr: ConstructorDeclaration, statements: readonly Statement[]): void { - this.replaceNode(sourceFile, ctr.body!, createBlock(statements, /*multiLine*/ true)); } - - public insertNodeAtEndOfScope(sourceFile: SourceFile, scope: Node, newNode: Node): void { - const pos = getAdjustedStartPosition(sourceFile, scope.getLastToken()!, {}); - this.insertNodeAt(sourceFile, pos, newNode, { - prefix: isLineBreak(sourceFile.text.charCodeAt(scope.getLastToken()!.pos)) ? this.newLineCharacter : this.newLineCharacter + this.newLineCharacter, - suffix: this.newLineCharacter - }); + else { + const pos = findChildOfKind(node, node.kind === SyntaxKind.FunctionExpression ? SyntaxKind.FunctionKeyword : SyntaxKind.ClassKeyword, sourceFile)!.end; + this.insertNodeAt(sourceFile, pos, createIdentifier(name), { prefix: " " }); } - - public insertNodeAtClassStart(sourceFile: SourceFile, cls: ClassLikeDeclaration | InterfaceDeclaration, newElement: ClassElement): void { - this.insertNodeAtStartWorker(sourceFile, cls, newElement); - } - - public insertNodeAtObjectStart(sourceFile: SourceFile, obj: ObjectLiteralExpression, newElement: ObjectLiteralElementLike): void { - this.insertNodeAtStartWorker(sourceFile, obj, newElement); + } + public insertExportModifier(sourceFile: SourceFile, node: DeclarationStatement | VariableStatement): void { + this.insertText(sourceFile, node.getStart(sourceFile), "export "); + } + /** + * This function should be used to insert nodes in lists when nodes don't carry separators as the part of the node range, + * i.e. arguments in arguments lists, parameters in parameter lists etc. + * Note that separators are part of the node in statements and class elements. + */ + public insertNodeInListAfter(sourceFile: SourceFile, after: Node, newNode: Node, containingList = SmartIndenter.getContainingList(after, sourceFile)): void { + if (!containingList) { + Debug.fail("node is not a list element"); + return; } - - private insertNodeAtStartWorker(sourceFile: SourceFile, cls: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression, newElement: ClassElement | ObjectLiteralElementLike): void { - const indentation = this.guessIndentationFromExistingMembers(sourceFile, cls) ?? this.computeIndentationForNewMember(sourceFile, cls); - this.insertNodeAt(sourceFile, getMembersOrProperties(cls).pos, newElement, this.getInsertNodeAtStartInsertOptions(sourceFile, cls, indentation)); + const index = indexOfNode(containingList, after); + if (index < 0) { + return; } - - /** - * Tries to guess the indentation from the existing members of a class/interface/object. All members must be on - * new lines and must share the same indentation. - */ - private guessIndentationFromExistingMembers(sourceFile: SourceFile, cls: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression) { - let indentation: number | undefined; - let lastRange: TextRange = cls; - for (const member of getMembersOrProperties(cls)) { - if (rangeStartPositionsAreOnSameLine(lastRange, member, sourceFile)) { - // each indented member must be on a new line - return undefined; - } - const memberStart = member.getStart(sourceFile); - const memberIndentation = formatting.SmartIndenter.findFirstNonWhitespaceColumn(getLineStartPositionForPosition(memberStart, sourceFile), memberStart, sourceFile, this.formatContext.options); - if (indentation === undefined) { - indentation = memberIndentation; + const end = after.getEnd(); + if (index !== containingList.length - 1) { + // any element except the last one + // use next sibling as an anchor + const nextToken = getTokenAtPosition(sourceFile, after.end); + if (nextToken && isSeparator(after, nextToken)) { + // for list + // a, b, c + // create change for adding 'e' after 'a' as + // - find start of next element after a (it is b) + // - use this start as start and end position in final change + // - build text of change by formatting the text of node + separator + whitespace trivia of b + // in multiline case it will work as + // a, + // b, + // c, + // result - '*' denotes leading trivia that will be inserted after new text (displayed as '#') + // a,* + // ***insertedtext# + // ###b, + // c, + // find line and character of the next element + const lineAndCharOfNextElement = getLineAndCharacterOfPosition(sourceFile, skipWhitespacesAndLineBreaks(sourceFile.text, containingList[index + 1].getFullStart())); + // find line and character of the token that precedes next element (usually it is separator) + const lineAndCharOfNextToken = getLineAndCharacterOfPosition(sourceFile, nextToken.end); + let prefix: string | undefined; + let startPos: number; + if (lineAndCharOfNextToken.line === lineAndCharOfNextElement.line) { + // next element is located on the same line with separator: + // a,$$$$b + // ^ ^ + // | |-next element + // |-separator + // where $$$ is some leading trivia + // for a newly inserted node we'll maintain the same relative position comparing to separator and replace leading trivia with spaces + // a, x,$$$$b + // ^ ^ ^ + // | | |-next element + // | |-new inserted node padded with spaces + // |-separator + startPos = nextToken.end; + prefix = spaces(lineAndCharOfNextElement.character - lineAndCharOfNextToken.character); } - else if (memberIndentation !== indentation) { - // indentation of multiple members is not consistent - return undefined; - } - lastRange = member; - } - return indentation; - } - - private computeIndentationForNewMember(sourceFile: SourceFile, cls: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression) { - const clsStart = cls.getStart(sourceFile); - return formatting.SmartIndenter.findFirstNonWhitespaceColumn(getLineStartPositionForPosition(clsStart, sourceFile), clsStart, sourceFile, this.formatContext.options) - + (this.formatContext.options.indentSize ?? 4); - } - - private getInsertNodeAtStartInsertOptions(sourceFile: SourceFile, cls: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression, indentation: number): InsertNodeOptions { - // Rules: - // - Always insert leading newline. - // - For object literals: - // - Add a trailing comma if there are existing members in the node, or the source file is not a JSON file - // (because trailing commas are generally illegal in a JSON file). - // - Add a leading comma if the source file is not a JSON file, there are existing insertions, - // and the node is empty (because we didn't add a trailing comma per the previous rule). - // - Only insert a trailing newline if body is single-line and there are no other insertions for the node. - // NOTE: This is handled in `finishClassesWithNodesInsertedAtStart`. - - const members = getMembersOrProperties(cls); - const isEmpty = members.length === 0; - const isFirstInsertion = addToSeen(this.classesWithNodesInsertedAtStart, getNodeId(cls), { node: cls, sourceFile }); - const insertTrailingComma = isObjectLiteralExpression(cls) && (!isJsonSourceFile(sourceFile) || !isEmpty); - const insertLeadingComma = isObjectLiteralExpression(cls) && isJsonSourceFile(sourceFile) && isEmpty && !isFirstInsertion; - return { - indentation, - prefix: (insertLeadingComma ? "," : "") + this.newLineCharacter, - suffix: insertTrailingComma ? "," : "" - }; - } - - public insertNodeAfterComma(sourceFile: SourceFile, after: Node, newNode: Node): void { - const endPosition = this.insertNodeAfterWorker(sourceFile, this.nextCommaToken(sourceFile, after) || after, newNode); - this.insertNodeAt(sourceFile, endPosition, newNode, this.getInsertNodeAfterOptions(sourceFile, after)); - } - - public insertNodeAfter(sourceFile: SourceFile, after: Node, newNode: Node): void { - const endPosition = this.insertNodeAfterWorker(sourceFile, after, newNode); - this.insertNodeAt(sourceFile, endPosition, newNode, this.getInsertNodeAfterOptions(sourceFile, after)); - } - - public insertNodeAtEndOfList(sourceFile: SourceFile, list: NodeArray, newNode: Node): void { - this.insertNodeAt(sourceFile, list.end, newNode, { prefix: ", " }); - } - - public insertNodesAfter(sourceFile: SourceFile, after: Node, newNodes: readonly Node[]): void { - const endPosition = this.insertNodeAfterWorker(sourceFile, after, first(newNodes)); - this.insertNodesAt(sourceFile, endPosition, newNodes, this.getInsertNodeAfterOptions(sourceFile, after)); - } - - private insertNodeAfterWorker(sourceFile: SourceFile, after: Node, newNode: Node): number { - if (needSemicolonBetween(after, newNode)) { - // check if previous statement ends with semicolon - // if not - insert semicolon to preserve the code from changing the meaning due to ASI - if (sourceFile.text.charCodeAt(after.end - 1) !== CharacterCodes.semicolon) { - this.replaceRange(sourceFile, createRange(after.end), createToken(SyntaxKind.SemicolonToken)); + else { + // next element is located on different line that separator + // let insert position be the beginning of the line that contains next element + startPos = getStartPositionOfLine(lineAndCharOfNextElement.line, sourceFile); } + // write separator and leading trivia of the next element as suffix + const suffix = `${tokenToString(nextToken.kind)}${sourceFile.text.substring(nextToken.end, containingList[index + 1].getStart(sourceFile))}`; + this.replaceRange(sourceFile, createRange(startPos, containingList[index + 1].getStart(sourceFile)), newNode, { prefix, suffix }); } - const endPosition = getAdjustedEndPosition(sourceFile, after, {}); - return endPosition; - } - - private getInsertNodeAfterOptions(sourceFile: SourceFile, after: Node): InsertNodeOptions { - const options = this.getInsertNodeAfterOptionsWorker(after); - return { - ...options, - prefix: after.end === sourceFile.end && isStatement(after) ? (options.prefix ? `\n${options.prefix}` : "\n") : options.prefix, - }; } - - private getInsertNodeAfterOptionsWorker(node: Node): InsertNodeOptions { - switch (node.kind) { - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ModuleDeclaration: - return { prefix: this.newLineCharacter, suffix: this.newLineCharacter }; - - case SyntaxKind.VariableDeclaration: - case SyntaxKind.StringLiteral: - case SyntaxKind.Identifier: - return { prefix: ", " }; - - case SyntaxKind.PropertyAssignment: - return { suffix: "," + this.newLineCharacter }; - - case SyntaxKind.ExportKeyword: - return { prefix: " " }; - - case SyntaxKind.Parameter: - return {}; - - default: - Debug.assert(isStatement(node) || isClassOrTypeElement(node)); // Else we haven't handled this kind of node yet -- add it - return { suffix: this.newLineCharacter }; - } - } - - public insertName(sourceFile: SourceFile, node: FunctionExpression | ClassExpression | ArrowFunction, name: string): void { - Debug.assert(!node.name); - if (node.kind === SyntaxKind.ArrowFunction) { - const arrow = findChildOfKind(node, SyntaxKind.EqualsGreaterThanToken, sourceFile)!; - const lparen = findChildOfKind(node, SyntaxKind.OpenParenToken, sourceFile); - if (lparen) { - // `() => {}` --> `function f() {}` - this.insertNodesAt(sourceFile, lparen.getStart(sourceFile), [createToken(SyntaxKind.FunctionKeyword), createIdentifier(name)], { joiner: " " }); - deleteNode(this, sourceFile, arrow); - } - else { - // `x => {}` -> `function f(x) {}` - this.insertText(sourceFile, first(node.parameters).getStart(sourceFile), `function ${name}(`); - // Replacing full range of arrow to get rid of the leading space -- replace ` =>` with `)` - this.replaceRange(sourceFile, arrow, createToken(SyntaxKind.CloseParenToken)); - } - - if (node.body.kind !== SyntaxKind.Block) { - // `() => 0` => `function f() { return 0; }` - this.insertNodesAt(sourceFile, node.body.getStart(sourceFile), [createToken(SyntaxKind.OpenBraceToken), createToken(SyntaxKind.ReturnKeyword)], { joiner: " ", suffix: " " }); - this.insertNodesAt(sourceFile, node.body.end, [createToken(SyntaxKind.SemicolonToken), createToken(SyntaxKind.CloseBraceToken)], { joiner: " " }); - } + else { + const afterStart = after.getStart(sourceFile); + const afterStartLinePosition = getLineStartPositionForPosition(afterStart, sourceFile); + let separator: SyntaxKind.CommaToken | SyntaxKind.SemicolonToken | undefined; + let multilineList = false; + // insert element after the last element in the list that has more than one item + // pick the element preceding the after element to: + // - pick the separator + // - determine if list is a multiline + if (containingList.length === 1) { + // if list has only one element then we'll format is as multiline if node has comment in trailing trivia, or as singleline otherwise + // i.e. var x = 1 // this is x + // | new element will be inserted at this position + separator = SyntaxKind.CommaToken; } else { - const pos = findChildOfKind(node, node.kind === SyntaxKind.FunctionExpression ? SyntaxKind.FunctionKeyword : SyntaxKind.ClassKeyword, sourceFile)!.end; - this.insertNodeAt(sourceFile, pos, createIdentifier(name), { prefix: " " }); + // element has more than one element, pick separator from the list + const tokenBeforeInsertPosition = findPrecedingToken(after.pos, sourceFile); + separator = isSeparator(after, tokenBeforeInsertPosition) ? tokenBeforeInsertPosition.kind : SyntaxKind.CommaToken; + // determine if list is multiline by checking lines of after element and element that precedes it. + const afterMinusOneStartLinePosition = getLineStartPositionForPosition(containingList[index - 1].getStart(sourceFile), sourceFile); + multilineList = afterMinusOneStartLinePosition !== afterStartLinePosition; } - } - - public insertExportModifier(sourceFile: SourceFile, node: DeclarationStatement | VariableStatement): void { - this.insertText(sourceFile, node.getStart(sourceFile), "export "); - } - - /** - * This function should be used to insert nodes in lists when nodes don't carry separators as the part of the node range, - * i.e. arguments in arguments lists, parameters in parameter lists etc. - * Note that separators are part of the node in statements and class elements. - */ - public insertNodeInListAfter(sourceFile: SourceFile, after: Node, newNode: Node, containingList = formatting.SmartIndenter.getContainingList(after, sourceFile)): void { - if (!containingList) { - Debug.fail("node is not a list element"); - return; + if (hasCommentsBeforeLineBreak(sourceFile.text, after.end)) { + // in this case we'll always treat containing list as multiline + multilineList = true; } - const index = indexOfNode(containingList, after); - if (index < 0) { - return; - } - const end = after.getEnd(); - if (index !== containingList.length - 1) { - // any element except the last one - // use next sibling as an anchor - const nextToken = getTokenAtPosition(sourceFile, after.end); - if (nextToken && isSeparator(after, nextToken)) { - // for list - // a, b, c - // create change for adding 'e' after 'a' as - // - find start of next element after a (it is b) - // - use this start as start and end position in final change - // - build text of change by formatting the text of node + separator + whitespace trivia of b - - // in multiline case it will work as - // a, - // b, - // c, - // result - '*' denotes leading trivia that will be inserted after new text (displayed as '#') - // a,* - // ***insertedtext# - // ###b, - // c, - // find line and character of the next element - const lineAndCharOfNextElement = getLineAndCharacterOfPosition(sourceFile, skipWhitespacesAndLineBreaks(sourceFile.text, containingList[index + 1].getFullStart())); - // find line and character of the token that precedes next element (usually it is separator) - const lineAndCharOfNextToken = getLineAndCharacterOfPosition(sourceFile, nextToken.end); - let prefix: string | undefined; - let startPos: number; - if (lineAndCharOfNextToken.line === lineAndCharOfNextElement.line) { - // next element is located on the same line with separator: - // a,$$$$b - // ^ ^ - // | |-next element - // |-separator - // where $$$ is some leading trivia - // for a newly inserted node we'll maintain the same relative position comparing to separator and replace leading trivia with spaces - // a, x,$$$$b - // ^ ^ ^ - // | | |-next element - // | |-new inserted node padded with spaces - // |-separator - startPos = nextToken.end; - prefix = spaces(lineAndCharOfNextElement.character - lineAndCharOfNextToken.character); - } - else { - // next element is located on different line that separator - // let insert position be the beginning of the line that contains next element - startPos = getStartPositionOfLine(lineAndCharOfNextElement.line, sourceFile); - } - - // write separator and leading trivia of the next element as suffix - const suffix = `${tokenToString(nextToken.kind)}${sourceFile.text.substring(nextToken.end, containingList[index + 1].getStart(sourceFile))}`; - this.replaceRange(sourceFile, createRange(startPos, containingList[index + 1].getStart(sourceFile)), newNode, { prefix, suffix }); + if (multilineList) { + // insert separator immediately following the 'after' node to preserve comments in trailing trivia + this.replaceRange(sourceFile, createRange(end), createToken(separator)); + // use the same indentation as 'after' item + const indentation = SmartIndenter.findFirstNonWhitespaceColumn(afterStartLinePosition, afterStart, sourceFile, this.formatContext.options); + // insert element before the line break on the line that contains 'after' element + let insertPos = skipTrivia(sourceFile.text, end, /*stopAfterLineBreak*/ true, /*stopAtComments*/ false); + if (insertPos !== end && isLineBreak(sourceFile.text.charCodeAt(insertPos - 1))) { + insertPos--; } + this.replaceRange(sourceFile, createRange(insertPos), newNode, { indentation, prefix: this.newLineCharacter }); } else { - const afterStart = after.getStart(sourceFile); - const afterStartLinePosition = getLineStartPositionForPosition(afterStart, sourceFile); - - let separator: SyntaxKind.CommaToken | SyntaxKind.SemicolonToken | undefined; - let multilineList = false; - - // insert element after the last element in the list that has more than one item - // pick the element preceding the after element to: - // - pick the separator - // - determine if list is a multiline - if (containingList.length === 1) { - // if list has only one element then we'll format is as multiline if node has comment in trailing trivia, or as singleline otherwise - // i.e. var x = 1 // this is x - // | new element will be inserted at this position - separator = SyntaxKind.CommaToken; - } - else { - // element has more than one element, pick separator from the list - const tokenBeforeInsertPosition = findPrecedingToken(after.pos, sourceFile); - separator = isSeparator(after, tokenBeforeInsertPosition) ? tokenBeforeInsertPosition.kind : SyntaxKind.CommaToken; - // determine if list is multiline by checking lines of after element and element that precedes it. - const afterMinusOneStartLinePosition = getLineStartPositionForPosition(containingList[index - 1].getStart(sourceFile), sourceFile); - multilineList = afterMinusOneStartLinePosition !== afterStartLinePosition; - } - if (hasCommentsBeforeLineBreak(sourceFile.text, after.end)) { - // in this case we'll always treat containing list as multiline - multilineList = true; - } - if (multilineList) { - // insert separator immediately following the 'after' node to preserve comments in trailing trivia - this.replaceRange(sourceFile, createRange(end), createToken(separator)); - // use the same indentation as 'after' item - const indentation = formatting.SmartIndenter.findFirstNonWhitespaceColumn(afterStartLinePosition, afterStart, sourceFile, this.formatContext.options); - // insert element before the line break on the line that contains 'after' element - let insertPos = skipTrivia(sourceFile.text, end, /*stopAfterLineBreak*/ true, /*stopAtComments*/ false); - if (insertPos !== end && isLineBreak(sourceFile.text.charCodeAt(insertPos - 1))) { - insertPos--; - } - this.replaceRange(sourceFile, createRange(insertPos), newNode, { indentation, prefix: this.newLineCharacter }); - } - else { - this.replaceRange(sourceFile, createRange(end), newNode, { prefix: `${tokenToString(separator)} ` }); - } + this.replaceRange(sourceFile, createRange(end), newNode, { prefix: `${tokenToString(separator)} ` }); } } - - public parenthesizeExpression(sourceFile: SourceFile, expression: Expression) { - this.replaceRange(sourceFile, rangeOfNode(expression), createParen(expression)); - } - - private finishClassesWithNodesInsertedAtStart(): void { - this.classesWithNodesInsertedAtStart.forEach(({ node, sourceFile }) => { - const [openBraceEnd, closeBraceEnd] = getClassOrObjectBraceEnds(node, sourceFile); - if (openBraceEnd !== undefined && closeBraceEnd !== undefined) { - const isEmpty = getMembersOrProperties(node).length === 0; - const isSingleLine = positionsAreOnSameLine(openBraceEnd, closeBraceEnd, sourceFile); - if (isEmpty && isSingleLine && openBraceEnd !== closeBraceEnd - 1) { - // For `class C { }` remove the whitespace inside the braces. - this.deleteRange(sourceFile, createRange(openBraceEnd, closeBraceEnd - 1)); - } - if (isSingleLine) { - this.insertText(sourceFile, closeBraceEnd - 1, this.newLineCharacter); - } - } - }); - } - - private finishDeleteDeclarations(): void { - const deletedNodesInLists = new NodeSet(); // Stores nodes in lists that we already deleted. Used to avoid deleting `, ` twice in `a, b`. - for (const { sourceFile, node } of this.deletedNodes) { - if (!this.deletedNodes.some(d => d.sourceFile === sourceFile && rangeContainsRangeExclusive(d.node, node))) { - if (isArray(node)) { - this.deleteRange(sourceFile, rangeOfTypeParameters(node)); - } - else { - deleteDeclaration.deleteDeclaration(this, deletedNodesInLists, sourceFile, node); - } + } + public parenthesizeExpression(sourceFile: SourceFile, expression: Expression) { + this.replaceRange(sourceFile, rangeOfNode(expression), createParen(expression)); + } + private finishClassesWithNodesInsertedAtStart(): void { + this.classesWithNodesInsertedAtStart.forEach(({ node, sourceFile }) => { + const [openBraceEnd, closeBraceEnd] = getClassOrObjectBraceEnds(node, sourceFile); + if (openBraceEnd !== undefined && closeBraceEnd !== undefined) { + const isEmpty = getMembersOrProperties(node).length === 0; + const isSingleLine = positionsAreOnSameLine(openBraceEnd, closeBraceEnd, sourceFile); + if (isEmpty && isSingleLine && openBraceEnd !== closeBraceEnd - 1) { + // For `class C { }` remove the whitespace inside the braces. + this.deleteRange(sourceFile, createRange(openBraceEnd, closeBraceEnd - 1)); } - } - - deletedNodesInLists.forEach(node => { - const sourceFile = node.getSourceFile(); - const list = formatting.SmartIndenter.getContainingList(node, sourceFile)!; - if (node !== last(list)) return; - - const lastNonDeletedIndex = findLastIndex(list, n => !deletedNodesInLists.has(n), list.length - 2); - if (lastNonDeletedIndex !== -1) { - this.deleteRange(sourceFile, { pos: list[lastNonDeletedIndex].end, end: startPositionToDeleteNodeInList(sourceFile, list[lastNonDeletedIndex + 1]) }); + if (isSingleLine) { + this.insertText(sourceFile, closeBraceEnd - 1, this.newLineCharacter); } - }); - } - - /** - * Note: after calling this, the TextChanges object must be discarded! - * @param validate only for tests - * The reason we must validate as part of this method is that `getNonFormattedText` changes the node's positions, - * so we can only call this once and can't get the non-formatted text separately. - */ - public getChanges(validate?: ValidateNonFormattedText): FileTextChanges[] { - this.finishDeleteDeclarations(); - this.finishClassesWithNodesInsertedAtStart(); - const changes = changesToText.getTextChangesFromChanges(this.changes, this.newLineCharacter, this.formatContext, validate); - for (const { oldFile, fileName, statements } of this.newFiles) { - changes.push(changesToText.newFileChanges(oldFile, fileName, statements, this.newLineCharacter, this.formatContext)); } - return changes; - } - - public createNewFile(oldFile: SourceFile | undefined, fileName: string, statements: readonly Statement[]): void { - this.newFiles.push({ oldFile, fileName, statements }); - } + }); } - - // find first non-whitespace position in the leading trivia of the node - function startPositionToDeleteNodeInList(sourceFile: SourceFile, node: Node): number { - return skipTrivia(sourceFile.text, getAdjustedStartPosition(sourceFile, node, { leadingTriviaOption: LeadingTriviaOption.IncludeAll }), /*stopAfterLineBreak*/ false, /*stopAtComments*/ true); - } - - function getClassOrObjectBraceEnds(cls: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression, sourceFile: SourceFile): [number | undefined, number | undefined] { - const open = findChildOfKind(cls, SyntaxKind.OpenBraceToken, sourceFile); - const close = findChildOfKind(cls, SyntaxKind.CloseBraceToken, sourceFile); - return [open?.end, close?.end]; - } - function getMembersOrProperties(cls: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression): NodeArray { - return isObjectLiteralExpression(cls) ? cls.properties : cls.members; - } - - export type ValidateNonFormattedText = (node: Node, text: string) => void; - - export function getNewFileText(statements: readonly Statement[], scriptKind: ScriptKind, newLineCharacter: string, formatContext: formatting.FormatContext): string { - return changesToText.newFileChangesWorker(/*oldFile*/ undefined, scriptKind, statements, newLineCharacter, formatContext); - } - - namespace changesToText { - export function getTextChangesFromChanges(changes: readonly Change[], newLineCharacter: string, formatContext: formatting.FormatContext, validate: ValidateNonFormattedText | undefined): FileTextChanges[] { - return group(changes, c => c.sourceFile.path).map(changesInFile => { - const sourceFile = changesInFile[0].sourceFile; - // order changes by start position - // If the start position is the same, put the shorter range first, since an empty range (x, x) may precede (x, y) but not vice-versa. - const normalized = stableSort(changesInFile, (a, b) => (a.range.pos - b.range.pos) || (a.range.end - b.range.end)); - // verify that change intervals do not overlap, except possibly at end points. - for (let i = 0; i < normalized.length - 1; i++) { - Debug.assert(normalized[i].range.end <= normalized[i + 1].range.pos, "Changes overlap", () => - `${JSON.stringify(normalized[i].range)} and ${JSON.stringify(normalized[i + 1].range)}`); + private finishDeleteDeclarations(): void { + const deletedNodesInLists = new NodeSet(); // Stores nodes in lists that we already deleted. Used to avoid deleting `, ` twice in `a, b`. + for (const { sourceFile, node } of this.deletedNodes) { + if (!this.deletedNodes.some(d => d.sourceFile === sourceFile && rangeContainsRangeExclusive(d.node, node))) { + if (isArray(node)) { + this.deleteRange(sourceFile, rangeOfTypeParameters(node)); + } + else { + deleteDeclaration.deleteDeclaration(this, deletedNodesInLists, sourceFile, node); } - const textChanges = normalized.map(c => - createTextChange(createTextSpanFromRange(c.range), computeNewText(c, sourceFile, newLineCharacter, formatContext, validate))); - return { fileName: sourceFile.fileName, textChanges }; - }); - } - - export function newFileChanges(oldFile: SourceFile | undefined, fileName: string, statements: readonly Statement[], newLineCharacter: string, formatContext: formatting.FormatContext): FileTextChanges { - const text = newFileChangesWorker(oldFile, getScriptKindFromFileName(fileName), statements, newLineCharacter, formatContext); - return { fileName, textChanges: [createTextChange(createTextSpan(0, 0), text)], isNewFile: true }; - } - - export function newFileChangesWorker(oldFile: SourceFile | undefined, scriptKind: ScriptKind, statements: readonly Statement[], newLineCharacter: string, formatContext: formatting.FormatContext): string { - // TODO: this emits the file, parses it back, then formats it that -- may be a less roundabout way to do this - const nonFormattedText = statements.map(s => getNonformattedText(s, oldFile, newLineCharacter).text).join(newLineCharacter); - const sourceFile = createSourceFile("any file name", nonFormattedText, ScriptTarget.ESNext, /*setParentNodes*/ true, scriptKind); - const changes = formatting.formatDocument(sourceFile, formatContext); - return applyChanges(nonFormattedText, changes) + newLineCharacter; - } - - function computeNewText(change: Change, sourceFile: SourceFile, newLineCharacter: string, formatContext: formatting.FormatContext, validate: ValidateNonFormattedText | undefined): string { - if (change.kind === ChangeKind.Remove) { - return ""; - } - if (change.kind === ChangeKind.Text) { - return change.text; } - - const { options = {}, range: { pos } } = change; - const format = (n: Node) => getFormattedTextOfNode(n, sourceFile, pos, options, newLineCharacter, formatContext, validate); - const text = change.kind === ChangeKind.ReplaceWithMultipleNodes - ? change.nodes.map(n => removeSuffix(format(n), newLineCharacter)).join(change.options!.joiner || newLineCharacter) // TODO: GH#18217 - : format(change.node); - // strip initial indentation (spaces or tabs) if text will be inserted in the middle of the line - const noIndent = (options.preserveLeadingWhitespace || options.indentation !== undefined || getLineStartPositionForPosition(pos, sourceFile) === pos) ? text : text.replace(/^\s+/, ""); - return (options.prefix || "") + noIndent + (options.suffix || ""); - } - - function getFormatCodeSettingsForWriting({ options }: formatting.FormatContext, sourceFile: SourceFile): FormatCodeSettings { - const shouldAutoDetectSemicolonPreference = !options.semicolons || options.semicolons === SemicolonPreference.Ignore; - const shouldRemoveSemicolons = options.semicolons === SemicolonPreference.Remove || shouldAutoDetectSemicolonPreference && !probablyUsesSemicolons(sourceFile); - return { - ...options, - semicolons: shouldRemoveSemicolons ? SemicolonPreference.Remove : SemicolonPreference.Ignore, - }; } - - /** Note: this may mutate `nodeIn`. */ - function getFormattedTextOfNode(nodeIn: Node, sourceFile: SourceFile, pos: number, { indentation, prefix, delta }: InsertNodeOptions, newLineCharacter: string, formatContext: formatting.FormatContext, validate: ValidateNonFormattedText | undefined): string { - const { node, text } = getNonformattedText(nodeIn, sourceFile, newLineCharacter); - if (validate) validate(node, text); - const formatOptions = getFormatCodeSettingsForWriting(formatContext, sourceFile); - const initialIndentation = - indentation !== undefined - ? indentation - : formatting.SmartIndenter.getIndentation(pos, sourceFile, formatOptions, prefix === newLineCharacter || getLineStartPositionForPosition(pos, sourceFile) === pos); - if (delta === undefined) { - delta = formatting.SmartIndenter.shouldIndentChildNode(formatOptions, nodeIn) ? (formatOptions.indentSize || 0) : 0; + deletedNodesInLists.forEach(node => { + const sourceFile = node.getSourceFile(); + const list = (SmartIndenter.getContainingList(node, sourceFile)!); + if (node !== last(list)) + return; + const lastNonDeletedIndex = findLastIndex(list, n => !deletedNodesInLists.has(n), list.length - 2); + if (lastNonDeletedIndex !== -1) { + this.deleteRange(sourceFile, { pos: list[lastNonDeletedIndex].end, end: startPositionToDeleteNodeInList(sourceFile, list[lastNonDeletedIndex + 1]) }); } - - const file: SourceFileLike = { text, getLineAndCharacterOfPosition(pos) { return getLineAndCharacterOfPosition(this, pos); } }; - const changes = formatting.formatNodeGivenIndentation(node, file, sourceFile.languageVariant, initialIndentation, delta, { ...formatContext, options: formatOptions }); - return applyChanges(text, changes); - } - - /** Note: output node may be mutated input node. */ - export function getNonformattedText(node: Node, sourceFile: SourceFile | undefined, newLineCharacter: string): { text: string, node: Node } { - const writer = createWriter(newLineCharacter); - const newLine = newLineCharacter === "\n" ? NewLineKind.LineFeed : NewLineKind.CarriageReturnLineFeed; - createPrinter({ newLine, neverAsciiEscape: true }, writer).writeNode(EmitHint.Unspecified, node, sourceFile, writer); - return { text: writer.getText(), node: assignPositionsToNode(node) }; - } + }); } - - export function applyChanges(text: string, changes: readonly TextChange[]): string { - for (let i = changes.length - 1; i >= 0; i--) { - const { span, newText } = changes[i]; - text = `${text.substring(0, span.start)}${newText}${text.substring(textSpanEnd(span))}`; - } - return text; - } - - function isTrivia(s: string) { - return skipTrivia(s, 0) === s.length; - } - - function assignPositionsToNode(node: Node): Node { - const visited = visitEachChild(node, assignPositionsToNode, nullTransformationContext, assignPositionsToNodeArray, assignPositionsToNode)!; // TODO: GH#18217 - // create proxy node for non synthesized nodes - const newNode = nodeIsSynthesized(visited) ? visited : Object.create(visited) as Node; - newNode.pos = getPos(node); - newNode.end = getEnd(node); - return newNode; - } - - function assignPositionsToNodeArray(nodes: NodeArray, visitor: Visitor, test?: (node: Node) => boolean, start?: number, count?: number) { - const visited = visitNodes(nodes, visitor, test, start, count); - if (!visited) { - return visited; - } - // clone nodearray if necessary - const nodeArray = visited === nodes ? createNodeArray(visited.slice(0)) : visited; - nodeArray.pos = getPos(nodes); - nodeArray.end = getEnd(nodes); - return nodeArray; - } - - interface TextChangesWriter extends EmitTextWriter, PrintHandlers {} - - function createWriter(newLine: string): TextChangesWriter { - let lastNonTriviaPosition = 0; - - - const writer = createTextWriter(newLine); - const onEmitNode: PrintHandlers["onEmitNode"] = (hint, node, printCallback) => { - if (node) { - setPos(node, lastNonTriviaPosition); - } - printCallback(hint, node); - if (node) { - setEnd(node, lastNonTriviaPosition); - } - }; - const onBeforeEmitNodeArray: PrintHandlers["onBeforeEmitNodeArray"] = nodes => { - if (nodes) { - setPos(nodes, lastNonTriviaPosition); - } - }; - const onAfterEmitNodeArray: PrintHandlers["onAfterEmitNodeArray"] = nodes => { - if (nodes) { - setEnd(nodes, lastNonTriviaPosition); - } - }; - const onBeforeEmitToken: PrintHandlers["onBeforeEmitToken"] = node => { - if (node) { - setPos(node, lastNonTriviaPosition); - } - }; - const onAfterEmitToken: PrintHandlers["onAfterEmitToken"] = node => { - if (node) { - setEnd(node, lastNonTriviaPosition); + /** + * Note: after calling this, the TextChanges object must be discarded! + * @param validate only for tests + * The reason we must validate as part of this method is that `getNonFormattedText` changes the node's positions, + * so we can only call this once and can't get the non-formatted text separately. + */ + public getChanges(validate?: ValidateNonFormattedText): FileTextChanges[] { + this.finishDeleteDeclarations(); + this.finishClassesWithNodesInsertedAtStart(); + const changes = changesToText.getTextChangesFromChanges(this.changes, this.newLineCharacter, this.formatContext, validate); + for (const { oldFile, fileName, statements } of this.newFiles) { + changes.push(changesToText.newFileChanges(oldFile, fileName, statements, this.newLineCharacter, this.formatContext)); + } + return changes; + } + public createNewFile(oldFile: SourceFile | undefined, fileName: string, statements: readonly Statement[]): void { + this.newFiles.push({ oldFile, fileName, statements }); + } +} +// find first non-whitespace position in the leading trivia of the node +/* @internal */ +function startPositionToDeleteNodeInList(sourceFile: SourceFile, node: Node): number { + return skipTrivia(sourceFile.text, getAdjustedStartPosition(sourceFile, node, { leadingTriviaOption: LeadingTriviaOption.IncludeAll }), /*stopAfterLineBreak*/ false, /*stopAtComments*/ true); +} +/* @internal */ +function getClassOrObjectBraceEnds(cls: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression, sourceFile: SourceFile): [number | undefined, number | undefined] { + const open = findChildOfKind(cls, SyntaxKind.OpenBraceToken, sourceFile); + const close = findChildOfKind(cls, SyntaxKind.CloseBraceToken, sourceFile); + return [open?.end, close?.end]; +} +/* @internal */ +function getMembersOrProperties(cls: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression): NodeArray { + return isObjectLiteralExpression(cls) ? cls.properties : cls.members; +} +/* @internal */ +export type ValidateNonFormattedText = (node: Node, text: string) => void; +/* @internal */ +export function getNewFileText(statements: readonly Statement[], scriptKind: ScriptKind, newLineCharacter: string, formatContext: FormatContext): string { + return changesToText.newFileChangesWorker(/*oldFile*/ undefined, scriptKind, statements, newLineCharacter, formatContext); +} +/* @internal */ +namespace changesToText { + export function getTextChangesFromChanges(changes: readonly Change[], newLineCharacter: string, formatContext: FormatContext, validate: ValidateNonFormattedText | undefined): FileTextChanges[] { + return group(changes, c => c.sourceFile.path).map(changesInFile => { + const sourceFile = changesInFile[0].sourceFile; + // order changes by start position + // If the start position is the same, put the shorter range first, since an empty range (x, x) may precede (x, y) but not vice-versa. + const normalized = stableSort(changesInFile, (a, b) => (a.range.pos - b.range.pos) || (a.range.end - b.range.end)); + // verify that change intervals do not overlap, except possibly at end points. + for (let i = 0; i < normalized.length - 1; i++) { + Debug.assert(normalized[i].range.end <= normalized[i + 1].range.pos, "Changes overlap", () => `${JSON.stringify(normalized[i].range)} and ${JSON.stringify(normalized[i + 1].range)}`); } + const textChanges = normalized.map(c => createTextChange(createTextSpanFromRange(c.range), computeNewText(c, sourceFile, newLineCharacter, formatContext, validate))); + return { fileName: sourceFile.fileName, textChanges }; + }); + } + export function newFileChanges(oldFile: SourceFile | undefined, fileName: string, statements: readonly Statement[], newLineCharacter: string, formatContext: FormatContext): FileTextChanges { + const text = newFileChangesWorker(oldFile, getScriptKindFromFileName(fileName), statements, newLineCharacter, formatContext); + return { fileName, textChanges: [createTextChange(createTextSpan(0, 0), text)], isNewFile: true }; + } + export function newFileChangesWorker(oldFile: SourceFile | undefined, scriptKind: ScriptKind, statements: readonly Statement[], newLineCharacter: string, formatContext: FormatContext): string { + // TODO: this emits the file, parses it back, then formats it that -- may be a less roundabout way to do this + const nonFormattedText = statements.map(s => getNonformattedText(s, oldFile, newLineCharacter).text).join(newLineCharacter); + const sourceFile = createSourceFile("any file name", nonFormattedText, ScriptTarget.ESNext, /*setParentNodes*/ true, scriptKind); + const changes = formatDocument(sourceFile, formatContext); + return applyChanges(nonFormattedText, changes) + newLineCharacter; + } + function computeNewText(change: Change, sourceFile: SourceFile, newLineCharacter: string, formatContext: FormatContext, validate: ValidateNonFormattedText | undefined): string { + if (change.kind === ChangeKind.Remove) { + return ""; + } + if (change.kind === ChangeKind.Text) { + return change.text; + } + const { options = {}, range: { pos } } = change; + const format = (n: Node) => getFormattedTextOfNode(n, sourceFile, pos, options, newLineCharacter, formatContext, validate); + const text = change.kind === ChangeKind.ReplaceWithMultipleNodes + ? change.nodes.map(n => removeSuffix(format(n), newLineCharacter)).join(change.options!.joiner || newLineCharacter) // TODO: GH#18217 + : format(change.node); + // strip initial indentation (spaces or tabs) if text will be inserted in the middle of the line + const noIndent = (options.preserveLeadingWhitespace || options.indentation !== undefined || getLineStartPositionForPosition(pos, sourceFile) === pos) ? text : text.replace(/^\s+/, ""); + return (options.prefix || "") + noIndent + (options.suffix || ""); + } + function getFormatCodeSettingsForWriting({ options }: FormatContext, sourceFile: SourceFile): FormatCodeSettings { + const shouldAutoDetectSemicolonPreference = !options.semicolons || options.semicolons === SemicolonPreference.Ignore; + const shouldRemoveSemicolons = options.semicolons === SemicolonPreference.Remove || shouldAutoDetectSemicolonPreference && !probablyUsesSemicolons(sourceFile); + return { + ...options, + semicolons: shouldRemoveSemicolons ? SemicolonPreference.Remove : SemicolonPreference.Ignore, }; - - function setLastNonTriviaPosition(s: string, force: boolean) { - if (force || !isTrivia(s)) { - lastNonTriviaPosition = writer.getTextPos(); - let i = 0; - while (isWhiteSpaceLike(s.charCodeAt(s.length - i - 1))) { - i++; - } - // trim trailing whitespaces - lastNonTriviaPosition -= i; - } - } - - function write(s: string): void { - writer.write(s); - setLastNonTriviaPosition(s, /*force*/ false); - } - function writeComment(s: string): void { - writer.writeComment(s); - } - function writeKeyword(s: string): void { - writer.writeKeyword(s); - setLastNonTriviaPosition(s, /*force*/ false); - } - function writeOperator(s: string): void { - writer.writeOperator(s); - setLastNonTriviaPosition(s, /*force*/ false); - } - function writePunctuation(s: string): void { - writer.writePunctuation(s); - setLastNonTriviaPosition(s, /*force*/ false); - } - function writeTrailingSemicolon(s: string): void { - writer.writeTrailingSemicolon(s); - setLastNonTriviaPosition(s, /*force*/ false); - } - function writeParameter(s: string): void { - writer.writeParameter(s); - setLastNonTriviaPosition(s, /*force*/ false); - } - function writeProperty(s: string): void { - writer.writeProperty(s); - setLastNonTriviaPosition(s, /*force*/ false); - } - function writeSpace(s: string): void { - writer.writeSpace(s); - setLastNonTriviaPosition(s, /*force*/ false); - } - function writeStringLiteral(s: string): void { - writer.writeStringLiteral(s); - setLastNonTriviaPosition(s, /*force*/ false); - } - function writeSymbol(s: string, sym: Symbol): void { - writer.writeSymbol(s, sym); - setLastNonTriviaPosition(s, /*force*/ false); - } - function writeLine(): void { - writer.writeLine(); - } - function increaseIndent(): void { - writer.increaseIndent(); - } - function decreaseIndent(): void { - writer.decreaseIndent(); - } - function getText(): string { - return writer.getText(); - } - function rawWrite(s: string): void { - writer.rawWrite(s); - setLastNonTriviaPosition(s, /*force*/ false); - } - function writeLiteral(s: string): void { - writer.writeLiteral(s); - setLastNonTriviaPosition(s, /*force*/ true); - } - function getTextPos(): number { - return writer.getTextPos(); + } + /** Note: this may mutate `nodeIn`. */ + function getFormattedTextOfNode(nodeIn: Node, sourceFile: SourceFile, pos: number, { indentation, prefix, delta }: InsertNodeOptions, newLineCharacter: string, formatContext: FormatContext, validate: ValidateNonFormattedText | undefined): string { + const { node, text } = getNonformattedText(nodeIn, sourceFile, newLineCharacter); + if (validate) + validate(node, text); + const formatOptions = getFormatCodeSettingsForWriting(formatContext, sourceFile); + const initialIndentation = indentation !== undefined + ? indentation + : SmartIndenter.getIndentation(pos, sourceFile, formatOptions, prefix === newLineCharacter || getLineStartPositionForPosition(pos, sourceFile) === pos); + if (delta === undefined) { + delta = SmartIndenter.shouldIndentChildNode(formatOptions, nodeIn) ? (formatOptions.indentSize || 0) : 0; + } + const file: SourceFileLike = { text, getLineAndCharacterOfPosition(pos) { return getLineAndCharacterOfPosition(this, pos); } }; + const changes = formatNodeGivenIndentation(node, file, sourceFile.languageVariant, initialIndentation, delta, { ...formatContext, options: formatOptions }); + return applyChanges(text, changes); + } + /** Note: output node may be mutated input node. */ + export function getNonformattedText(node: Node, sourceFile: SourceFile | undefined, newLineCharacter: string): { + text: string; + node: Node; + } { + const writer = createWriter(newLineCharacter); + const newLine = newLineCharacter === "\n" ? NewLineKind.LineFeed : NewLineKind.CarriageReturnLineFeed; + createPrinter({ newLine, neverAsciiEscape: true }, writer).writeNode(EmitHint.Unspecified, node, sourceFile, writer); + return { text: writer.getText(), node: assignPositionsToNode(node) }; + } +} +/* @internal */ +export function applyChanges(text: string, changes: readonly TextChange[]): string { + for (let i = changes.length - 1; i >= 0; i--) { + const { span, newText } = changes[i]; + text = `${text.substring(0, span.start)}${newText}${text.substring(textSpanEnd(span))}`; + } + return text; +} +/* @internal */ +function isTrivia(s: string) { + return skipTrivia(s, 0) === s.length; +} +/* @internal */ +function assignPositionsToNode(node: Node): Node { + const visited = (visitEachChild(node, assignPositionsToNode, nullTransformationContext, assignPositionsToNodeArray, assignPositionsToNode)!); // TODO: GH#18217 + // create proxy node for non synthesized nodes + const newNode = nodeIsSynthesized(visited) ? visited : Object.create(visited) as Node; + newNode.pos = getPos(node); + newNode.end = getEnd(node); + return newNode; +} +/* @internal */ +function assignPositionsToNodeArray(nodes: NodeArray, visitor: Visitor, test?: (node: Node) => boolean, start?: number, count?: number) { + const visited = visitNodes(nodes, visitor, test, start, count); + if (!visited) { + return visited; + } + // clone nodearray if necessary + const nodeArray = visited === nodes ? createNodeArray(visited.slice(0)) : visited; + nodeArray.pos = getPos(nodes); + nodeArray.end = getEnd(nodes); + return nodeArray; +} +/* @internal */ +interface TextChangesWriter extends EmitTextWriter, PrintHandlers { +} +/* @internal */ +function createWriter(newLine: string): TextChangesWriter { + let lastNonTriviaPosition = 0; + const writer = createTextWriter(newLine); + const onEmitNode: PrintHandlers["onEmitNode"] = (hint, node, printCallback) => { + if (node) { + setPos(node, lastNonTriviaPosition); } - function getLine(): number { - return writer.getLine(); + printCallback(hint, node); + if (node) { + setEnd(node, lastNonTriviaPosition); } - function getColumn(): number { - return writer.getColumn(); + }; + const onBeforeEmitNodeArray: PrintHandlers["onBeforeEmitNodeArray"] = nodes => { + if (nodes) { + setPos(nodes, lastNonTriviaPosition); } - function getIndent(): number { - return writer.getIndent(); + }; + const onAfterEmitNodeArray: PrintHandlers["onAfterEmitNodeArray"] = nodes => { + if (nodes) { + setEnd(nodes, lastNonTriviaPosition); } - function isAtStartOfLine(): boolean { - return writer.isAtStartOfLine(); + }; + const onBeforeEmitToken: PrintHandlers["onBeforeEmitToken"] = node => { + if (node) { + setPos(node, lastNonTriviaPosition); } - function clear(): void { - writer.clear(); - lastNonTriviaPosition = 0; + }; + const onAfterEmitToken: PrintHandlers["onAfterEmitToken"] = node => { + if (node) { + setEnd(node, lastNonTriviaPosition); } - - return { - onEmitNode, - onBeforeEmitNodeArray, - onAfterEmitNodeArray, - onBeforeEmitToken, - onAfterEmitToken, - write, - writeComment, - writeKeyword, - writeOperator, - writePunctuation, - writeTrailingSemicolon, - writeParameter, - writeProperty, - writeSpace, - writeStringLiteral, - writeSymbol, - writeLine, - increaseIndent, - decreaseIndent, - getText, - rawWrite, - writeLiteral, - getTextPos, - getLine, - getColumn, - getIndent, - isAtStartOfLine, - hasTrailingComment: () => writer.hasTrailingComment(), - hasTrailingWhitespace: () => writer.hasTrailingWhitespace(), - clear - }; - } - - function getInsertionPositionAtSourceFileTop(sourceFile: SourceFile): number { - let lastPrologue: PrologueDirective | undefined; - for (const node of sourceFile.statements) { - if (isPrologueDirective(node)) { - lastPrologue = node; - } - else { - break; + }; + function setLastNonTriviaPosition(s: string, force: boolean) { + if (force || !isTrivia(s)) { + lastNonTriviaPosition = writer.getTextPos(); + let i = 0; + while (isWhiteSpaceLike(s.charCodeAt(s.length - i - 1))) { + i++; } + // trim trailing whitespaces + lastNonTriviaPosition -= i; } - - let position = 0; - const text = sourceFile.text; - if (lastPrologue) { - position = lastPrologue.end; - advancePastLineBreak(); - return position; + } + function write(s: string): void { + writer.write(s); + setLastNonTriviaPosition(s, /*force*/ false); + } + function writeComment(s: string): void { + writer.writeComment(s); + } + function writeKeyword(s: string): void { + writer.writeKeyword(s); + setLastNonTriviaPosition(s, /*force*/ false); + } + function writeOperator(s: string): void { + writer.writeOperator(s); + setLastNonTriviaPosition(s, /*force*/ false); + } + function writePunctuation(s: string): void { + writer.writePunctuation(s); + setLastNonTriviaPosition(s, /*force*/ false); + } + function writeTrailingSemicolon(s: string): void { + writer.writeTrailingSemicolon(s); + setLastNonTriviaPosition(s, /*force*/ false); + } + function writeParameter(s: string): void { + writer.writeParameter(s); + setLastNonTriviaPosition(s, /*force*/ false); + } + function writeProperty(s: string): void { + writer.writeProperty(s); + setLastNonTriviaPosition(s, /*force*/ false); + } + function writeSpace(s: string): void { + writer.writeSpace(s); + setLastNonTriviaPosition(s, /*force*/ false); + } + function writeStringLiteral(s: string): void { + writer.writeStringLiteral(s); + setLastNonTriviaPosition(s, /*force*/ false); + } + function writeSymbol(s: string, sym: Symbol): void { + writer.writeSymbol(s, sym); + setLastNonTriviaPosition(s, /*force*/ false); + } + function writeLine(): void { + writer.writeLine(); + } + function increaseIndent(): void { + writer.increaseIndent(); + } + function decreaseIndent(): void { + writer.decreaseIndent(); + } + function getText(): string { + return writer.getText(); + } + function rawWrite(s: string): void { + writer.rawWrite(s); + setLastNonTriviaPosition(s, /*force*/ false); + } + function writeLiteral(s: string): void { + writer.writeLiteral(s); + setLastNonTriviaPosition(s, /*force*/ true); + } + function getTextPos(): number { + return writer.getTextPos(); + } + function getLine(): number { + return writer.getLine(); + } + function getColumn(): number { + return writer.getColumn(); + } + function getIndent(): number { + return writer.getIndent(); + } + function isAtStartOfLine(): boolean { + return writer.isAtStartOfLine(); + } + function clear(): void { + writer.clear(); + lastNonTriviaPosition = 0; + } + return { + onEmitNode, + onBeforeEmitNodeArray, + onAfterEmitNodeArray, + onBeforeEmitToken, + onAfterEmitToken, + write, + writeComment, + writeKeyword, + writeOperator, + writePunctuation, + writeTrailingSemicolon, + writeParameter, + writeProperty, + writeSpace, + writeStringLiteral, + writeSymbol, + writeLine, + increaseIndent, + decreaseIndent, + getText, + rawWrite, + writeLiteral, + getTextPos, + getLine, + getColumn, + getIndent, + isAtStartOfLine, + hasTrailingComment: () => writer.hasTrailingComment(), + hasTrailingWhitespace: () => writer.hasTrailingWhitespace(), + clear + }; +} +/* @internal */ +function getInsertionPositionAtSourceFileTop(sourceFile: SourceFile): number { + let lastPrologue: PrologueDirective | undefined; + for (const node of sourceFile.statements) { + if (isPrologueDirective(node)) { + lastPrologue = node; } - - const shebang = getShebang(text); - if (shebang !== undefined) { - position = shebang.length; - advancePastLineBreak(); + else { + break; } - - const ranges = getLeadingCommentRanges(text, position); - if (!ranges) return position; - - // Find the first attached comment to the first node and add before it - let lastComment: { range: CommentRange; pinnedOrTripleSlash: boolean; } | undefined; - let firstNodeLine: number | undefined; - for (const range of ranges) { - if (range.kind === SyntaxKind.MultiLineCommentTrivia) { - if (isPinnedComment(text, range.pos)) { - lastComment = { range, pinnedOrTripleSlash: true }; - continue; - } - } - else if (isRecognizedTripleSlashComment(text, range.pos, range.end)) { + } + let position = 0; + const text = sourceFile.text; + if (lastPrologue) { + position = lastPrologue.end; + advancePastLineBreak(); + return position; + } + const shebang = getShebang(text); + if (shebang !== undefined) { + position = shebang.length; + advancePastLineBreak(); + } + const ranges = getLeadingCommentRanges(text, position); + if (!ranges) + return position; + // Find the first attached comment to the first node and add before it + let lastComment: { + range: CommentRange; + pinnedOrTripleSlash: boolean; + } | undefined; + let firstNodeLine: number | undefined; + for (const range of ranges) { + if (range.kind === SyntaxKind.MultiLineCommentTrivia) { + if (isPinnedComment(text, range.pos)) { lastComment = { range, pinnedOrTripleSlash: true }; continue; } - - if (lastComment) { - // Always insert after pinned or triple slash comments - if (lastComment.pinnedOrTripleSlash) break; - - // There was a blank line between the last comment and this comment. - // This comment is not part of the copyright comments - const commentLine = sourceFile.getLineAndCharacterOfPosition(range.pos).line; - const lastCommentEndLine = sourceFile.getLineAndCharacterOfPosition(lastComment.range.end).line; - if (commentLine >= lastCommentEndLine + 2) break; - } - - if (sourceFile.statements.length) { - if (firstNodeLine === undefined) firstNodeLine = sourceFile.getLineAndCharacterOfPosition(sourceFile.statements[0].getStart()).line; - const commentEndLine = sourceFile.getLineAndCharacterOfPosition(range.end).line; - if (firstNodeLine < commentEndLine + 2) break; - } - lastComment = { range, pinnedOrTripleSlash: false }; } - + else if (isRecognizedTripleSlashComment(text, range.pos, range.end)) { + lastComment = { range, pinnedOrTripleSlash: true }; + continue; + } if (lastComment) { - position = lastComment.range.end; - advancePastLineBreak(); + // Always insert after pinned or triple slash comments + if (lastComment.pinnedOrTripleSlash) + break; + // There was a blank line between the last comment and this comment. + // This comment is not part of the copyright comments + const commentLine = sourceFile.getLineAndCharacterOfPosition(range.pos).line; + const lastCommentEndLine = sourceFile.getLineAndCharacterOfPosition(lastComment.range.end).line; + if (commentLine >= lastCommentEndLine + 2) + break; } - return position; - - function advancePastLineBreak() { - if (position < text.length) { - const charCode = text.charCodeAt(position); - if (isLineBreak(charCode)) { + if (sourceFile.statements.length) { + if (firstNodeLine === undefined) + firstNodeLine = sourceFile.getLineAndCharacterOfPosition(sourceFile.statements[0].getStart()).line; + const commentEndLine = sourceFile.getLineAndCharacterOfPosition(range.end).line; + if (firstNodeLine < commentEndLine + 2) + break; + } + lastComment = { range, pinnedOrTripleSlash: false }; + } + if (lastComment) { + position = lastComment.range.end; + advancePastLineBreak(); + } + return position; + function advancePastLineBreak() { + if (position < text.length) { + const charCode = text.charCodeAt(position); + if (isLineBreak(charCode)) { + position++; + if (position < text.length && charCode === CharacterCodes.carriageReturn && text.charCodeAt(position) === CharacterCodes.lineFeed) { position++; - - if (position < text.length && charCode === CharacterCodes.carriageReturn && text.charCodeAt(position) === CharacterCodes.lineFeed) { - position++; - } } } } } - - export function isValidLocationToAddComment(sourceFile: SourceFile, position: number) { - return !isInComment(sourceFile, position) && !isInString(sourceFile, position) && !isInTemplateString(sourceFile, position) && !isInJSXText(sourceFile, position); - } - - function needSemicolonBetween(a: Node, b: Node): boolean { - return (isPropertySignature(a) || isPropertyDeclaration(a)) && isClassOrTypeElement(b) && b.name!.kind === SyntaxKind.ComputedPropertyName - || isStatementButNotDeclaration(a) && isStatementButNotDeclaration(b); // TODO: only if b would start with a `(` or `[` - } - - namespace deleteDeclaration { - export function deleteDeclaration(changes: ChangeTracker, deletedNodesInLists: NodeSet, sourceFile: SourceFile, node: Node): void { - switch (node.kind) { - case SyntaxKind.Parameter: { - const oldFunction = node.parent; - if (isArrowFunction(oldFunction) && - oldFunction.parameters.length === 1 && - !findChildOfKind(oldFunction, SyntaxKind.OpenParenToken, sourceFile)) { - // Lambdas with exactly one parameter are special because, after removal, there - // must be an empty parameter list (i.e. `()`) and this won't necessarily be the - // case if the parameter is simply removed (e.g. in `x => 1`). - changes.replaceNodeWithText(sourceFile, node, "()"); - } - else { - deleteNodeInList(changes, deletedNodesInLists, sourceFile, node); - } - break; +} +/* @internal */ +export function isValidLocationToAddComment(sourceFile: SourceFile, position: number) { + return !isInComment(sourceFile, position) && !isInString(sourceFile, position) && !isInTemplateString(sourceFile, position) && !isInJSXText(sourceFile, position); +} +/* @internal */ +function needSemicolonBetween(a: Node, b: Node): boolean { + return (isPropertySignature(a) || isPropertyDeclaration(a)) && isClassOrTypeElement(b) && b.name!.kind === SyntaxKind.ComputedPropertyName + || isStatementButNotDeclaration(a) && isStatementButNotDeclaration(b); // TODO: only if b would start with a `(` or `[` +} +/* @internal */ +namespace deleteDeclaration { + export function deleteDeclaration(changes: ChangeTracker, deletedNodesInLists: NodeSet, sourceFile: SourceFile, node: Node): void { + switch (node.kind) { + case SyntaxKind.Parameter: { + const oldFunction = node.parent; + if (isArrowFunction(oldFunction) && + oldFunction.parameters.length === 1 && + !findChildOfKind(oldFunction, SyntaxKind.OpenParenToken, sourceFile)) { + // Lambdas with exactly one parameter are special because, after removal, there + // must be an empty parameter list (i.e. `()`) and this won't necessarily be the + // case if the parameter is simply removed (e.g. in `x => 1`). + changes.replaceNodeWithText(sourceFile, node, "()"); } - - case SyntaxKind.ImportDeclaration: - const isFirstImport = sourceFile.imports.length && node === first(sourceFile.imports).parent || node === find(sourceFile.statements, isImportDeclaration); - deleteNode(changes, sourceFile, node, - // For first import, leave header comment in place - isFirstImport ? { leadingTriviaOption: LeadingTriviaOption.Exclude } : undefined); - break; - - case SyntaxKind.BindingElement: - const pattern = (node as BindingElement).parent; - const preserveComma = pattern.kind === SyntaxKind.ArrayBindingPattern && node !== last(pattern.elements); - if (preserveComma) { - deleteNode(changes, sourceFile, node); - } - else { - deleteNodeInList(changes, deletedNodesInLists, sourceFile, node); - } - break; - - case SyntaxKind.VariableDeclaration: - deleteVariableDeclaration(changes, deletedNodesInLists, sourceFile, node as VariableDeclaration); - break; - - case SyntaxKind.TypeParameter: + else { deleteNodeInList(changes, deletedNodesInLists, sourceFile, node); - break; - - case SyntaxKind.ImportSpecifier: - const namedImports = (node as ImportSpecifier).parent; - if (namedImports.elements.length === 1) { - deleteImportBinding(changes, sourceFile, namedImports); - } - else { - deleteNodeInList(changes, deletedNodesInLists, sourceFile, node); - } - break; - - case SyntaxKind.NamespaceImport: - deleteImportBinding(changes, sourceFile, node as NamespaceImport); - break; - - default: - if (isImportClause(node.parent) && node.parent.name === node) { - deleteDefaultImport(changes, sourceFile, node.parent); - } - else if (isCallLikeExpression(node.parent)) { - deleteNodeInList(changes, deletedNodesInLists, sourceFile, node); - } - else { - deleteNode(changes, sourceFile, node, node.kind === SyntaxKind.SemicolonToken ? { trailingTriviaOption: TrailingTriviaOption.Exclude } : undefined); - } - } - } - - function deleteDefaultImport(changes: ChangeTracker, sourceFile: SourceFile, importClause: ImportClause): void { - if (!importClause.namedBindings) { - // Delete the whole import - deleteNode(changes, sourceFile, importClause.parent); + } + break; } - else { - // import |d,| * as ns from './file' - const start = importClause.name!.getStart(sourceFile); - const nextToken = getTokenAtPosition(sourceFile, importClause.name!.end); - if (nextToken && nextToken.kind === SyntaxKind.CommaToken) { - // shift first non-whitespace position after comma to the start position of the node - const end = skipTrivia(sourceFile.text, nextToken.end, /*stopAfterLineBreaks*/ false, /*stopAtComments*/ true); - changes.deleteRange(sourceFile, { pos: start, end }); + case SyntaxKind.ImportDeclaration: + const isFirstImport = sourceFile.imports.length && node === first(sourceFile.imports).parent || node === find(sourceFile.statements, isImportDeclaration); + deleteNode(changes, sourceFile, node, + // For first import, leave header comment in place + isFirstImport ? { leadingTriviaOption: LeadingTriviaOption.Exclude } : undefined); + break; + case SyntaxKind.BindingElement: + const pattern = (node as BindingElement).parent; + const preserveComma = pattern.kind === SyntaxKind.ArrayBindingPattern && node !== last(pattern.elements); + if (preserveComma) { + deleteNode(changes, sourceFile, node); } else { - deleteNode(changes, sourceFile, importClause.name!); + deleteNodeInList(changes, deletedNodesInLists, sourceFile, node); + } + break; + case SyntaxKind.VariableDeclaration: + deleteVariableDeclaration(changes, deletedNodesInLists, sourceFile, (node as VariableDeclaration)); + break; + case SyntaxKind.TypeParameter: + deleteNodeInList(changes, deletedNodesInLists, sourceFile, node); + break; + case SyntaxKind.ImportSpecifier: + const namedImports = (node as ImportSpecifier).parent; + if (namedImports.elements.length === 1) { + deleteImportBinding(changes, sourceFile, namedImports); + } + else { + deleteNodeInList(changes, deletedNodesInLists, sourceFile, node); + } + break; + case SyntaxKind.NamespaceImport: + deleteImportBinding(changes, sourceFile, (node as NamespaceImport)); + break; + default: + if (isImportClause(node.parent) && node.parent.name === node) { + deleteDefaultImport(changes, sourceFile, node.parent); + } + else if (isCallLikeExpression(node.parent)) { + deleteNodeInList(changes, deletedNodesInLists, sourceFile, node); + } + else { + deleteNode(changes, sourceFile, node, node.kind === SyntaxKind.SemicolonToken ? { trailingTriviaOption: TrailingTriviaOption.Exclude } : undefined); } - } } - - function deleteImportBinding(changes: ChangeTracker, sourceFile: SourceFile, node: NamedImportBindings): void { - if (node.parent.name) { - // Delete named imports while preserving the default import - // import d|, * as ns| from './file' - // import d|, { a }| from './file' - const previousToken = Debug.checkDefined(getTokenAtPosition(sourceFile, node.pos - 1)); - changes.deleteRange(sourceFile, { pos: previousToken.getStart(sourceFile), end: node.end }); + } + function deleteDefaultImport(changes: ChangeTracker, sourceFile: SourceFile, importClause: ImportClause): void { + if (!importClause.namedBindings) { + // Delete the whole import + deleteNode(changes, sourceFile, importClause.parent); + } + else { + // import |d,| * as ns from './file' + const start = importClause.name!.getStart(sourceFile); + const nextToken = getTokenAtPosition(sourceFile, importClause.name!.end); + if (nextToken && nextToken.kind === SyntaxKind.CommaToken) { + // shift first non-whitespace position after comma to the start position of the node + const end = skipTrivia(sourceFile.text, nextToken.end, /*stopAfterLineBreaks*/ false, /*stopAtComments*/ true); + changes.deleteRange(sourceFile, { pos: start, end }); } else { - // Delete the entire import declaration - // |import * as ns from './file'| - // |import { a } from './file'| - const importDecl = getAncestor(node, SyntaxKind.ImportDeclaration)!; - deleteNode(changes, sourceFile, importDecl); + deleteNode(changes, sourceFile, importClause.name!); } } - - function deleteVariableDeclaration(changes: ChangeTracker, deletedNodesInLists: NodeSet, sourceFile: SourceFile, node: VariableDeclaration): void { - const { parent } = node; - - if (parent.kind === SyntaxKind.CatchClause) { - // TODO: There's currently no unused diagnostic for this, could be a suggestion - changes.deleteNodeRange(sourceFile, findChildOfKind(parent, SyntaxKind.OpenParenToken, sourceFile)!, findChildOfKind(parent, SyntaxKind.CloseParenToken, sourceFile)!); - return; - } - - if (parent.declarations.length !== 1) { - deleteNodeInList(changes, deletedNodesInLists, sourceFile, node); - return; - } - - const gp = parent.parent; - switch (gp.kind) { - case SyntaxKind.ForOfStatement: - case SyntaxKind.ForInStatement: - changes.replaceNode(sourceFile, node, createObjectLiteral()); - break; - - case SyntaxKind.ForStatement: - deleteNode(changes, sourceFile, parent); - break; - - case SyntaxKind.VariableStatement: - deleteNode(changes, sourceFile, gp); - break; - - default: - Debug.assertNever(gp); - } + } + function deleteImportBinding(changes: ChangeTracker, sourceFile: SourceFile, node: NamedImportBindings): void { + if (node.parent.name) { + // Delete named imports while preserving the default import + // import d|, * as ns| from './file' + // import d|, { a }| from './file' + const previousToken = Debug.checkDefined(getTokenAtPosition(sourceFile, node.pos - 1)); + changes.deleteRange(sourceFile, { pos: previousToken.getStart(sourceFile), end: node.end }); + } + else { + // Delete the entire import declaration + // |import * as ns from './file'| + // |import { a } from './file'| + const importDecl = (getAncestor(node, SyntaxKind.ImportDeclaration)!); + deleteNode(changes, sourceFile, importDecl); } } - - /** Warning: This deletes comments too. See `copyComments` in `convertFunctionToEs6Class`. */ - // Exported for tests only! (TODO: improve tests to not need this) - export function deleteNode(changes: ChangeTracker, sourceFile: SourceFile, node: Node, options: ConfigurableStartEnd = { leadingTriviaOption: LeadingTriviaOption.IncludeAll }): void { - const startPosition = getAdjustedStartPosition(sourceFile, node, options); - const endPosition = getAdjustedEndPosition(sourceFile, node, options); - changes.deleteRange(sourceFile, { pos: startPosition, end: endPosition }); - } - - function deleteNodeInList(changes: ChangeTracker, deletedNodesInLists: NodeSet, sourceFile: SourceFile, node: Node): void { - const containingList = Debug.checkDefined(formatting.SmartIndenter.getContainingList(node, sourceFile)); - const index = indexOfNode(containingList, node); - Debug.assert(index !== -1); - if (containingList.length === 1) { - deleteNode(changes, sourceFile, node); + function deleteVariableDeclaration(changes: ChangeTracker, deletedNodesInLists: NodeSet, sourceFile: SourceFile, node: VariableDeclaration): void { + const { parent } = node; + if (parent.kind === SyntaxKind.CatchClause) { + // TODO: There's currently no unused diagnostic for this, could be a suggestion + changes.deleteNodeRange(sourceFile, (findChildOfKind(parent, SyntaxKind.OpenParenToken, sourceFile)!), (findChildOfKind(parent, SyntaxKind.CloseParenToken, sourceFile)!)); return; } - - // Note: We will only delete a comma *after* a node. This will leave a trailing comma if we delete the last node. - // That's handled in the end by `finishTrailingCommaAfterDeletingNodesInList`. - Debug.assert(!deletedNodesInLists.has(node), "Deleting a node twice"); - deletedNodesInLists.add(node); - changes.deleteRange(sourceFile, { - pos: startPositionToDeleteNodeInList(sourceFile, node), - end: index === containingList.length - 1 ? getAdjustedEndPosition(sourceFile, node, {}) : startPositionToDeleteNodeInList(sourceFile, containingList[index + 1]), - }); + if (parent.declarations.length !== 1) { + deleteNodeInList(changes, deletedNodesInLists, sourceFile, node); + return; + } + const gp = parent.parent; + switch (gp.kind) { + case SyntaxKind.ForOfStatement: + case SyntaxKind.ForInStatement: + changes.replaceNode(sourceFile, node, createObjectLiteral()); + break; + case SyntaxKind.ForStatement: + deleteNode(changes, sourceFile, parent); + break; + case SyntaxKind.VariableStatement: + deleteNode(changes, sourceFile, gp); + break; + default: + Debug.assertNever(gp); + } + } +} +/** Warning: This deletes comments too. See `copyComments` in `convertFunctionToEs6Class`. */ +// Exported for tests only! (TODO: improve tests to not need this) +/* @internal */ +export function deleteNode(changes: ChangeTracker, sourceFile: SourceFile, node: Node, options: ConfigurableStartEnd = { leadingTriviaOption: LeadingTriviaOption.IncludeAll }): void { + const startPosition = getAdjustedStartPosition(sourceFile, node, options); + const endPosition = getAdjustedEndPosition(sourceFile, node, options); + changes.deleteRange(sourceFile, { pos: startPosition, end: endPosition }); +} +/* @internal */ +function deleteNodeInList(changes: ChangeTracker, deletedNodesInLists: NodeSet, sourceFile: SourceFile, node: Node): void { + const containingList = Debug.checkDefined(SmartIndenter.getContainingList(node, sourceFile)); + const index = indexOfNode(containingList, node); + Debug.assert(index !== -1); + if (containingList.length === 1) { + deleteNode(changes, sourceFile, node); + return; } + // Note: We will only delete a comma *after* a node. This will leave a trailing comma if we delete the last node. + // That's handled in the end by `finishTrailingCommaAfterDeletingNodesInList`. + Debug.assert(!deletedNodesInLists.has(node), "Deleting a node twice"); + deletedNodesInLists.add(node); + changes.deleteRange(sourceFile, { + pos: startPositionToDeleteNodeInList(sourceFile, node), + end: index === containingList.length - 1 ? getAdjustedEndPosition(sourceFile, node, {}) : startPositionToDeleteNodeInList(sourceFile, containingList[index + 1]), + }); } diff --git a/src/services/transform.ts b/src/services/transform.ts index a3d0d7b10c3f0..3354272e603eb 100644 --- a/src/services/transform.ts +++ b/src/services/transform.ts @@ -1,16 +1,15 @@ -namespace ts { - /** - * Transform one or more nodes using the supplied transformers. - * @param source A single `Node` or an array of `Node` objects. - * @param transformers An array of `TransformerFactory` callbacks used to process the transformation. - * @param compilerOptions Optional compiler options. - */ - export function transform(source: T | T[], transformers: TransformerFactory[], compilerOptions?: CompilerOptions) { - const diagnostics: DiagnosticWithLocation[] = []; - compilerOptions = fixupCompilerOptions(compilerOptions!, diagnostics); // TODO: GH#18217 - const nodes = isArray(source) ? source : [source]; - const result = transformNodes(/*resolver*/ undefined, /*emitHost*/ undefined, compilerOptions, nodes, transformers, /*allowDtsFiles*/ true); - result.diagnostics = concatenate(result.diagnostics, diagnostics); - return result; - } -} \ No newline at end of file +import { Node, TransformerFactory, CompilerOptions, DiagnosticWithLocation, fixupCompilerOptions, isArray, transformNodes, concatenate } from "./ts"; +/** + * Transform one or more nodes using the supplied transformers. + * @param source A single `Node` or an array of `Node` objects. + * @param transformers An array of `TransformerFactory` callbacks used to process the transformation. + * @param compilerOptions Optional compiler options. + */ +export function transform(source: T | T[], transformers: TransformerFactory[], compilerOptions?: CompilerOptions) { + const diagnostics: DiagnosticWithLocation[] = []; + compilerOptions = fixupCompilerOptions((compilerOptions!), diagnostics); // TODO: GH#18217 + const nodes = isArray(source) ? source : [source]; + const result = transformNodes(/*resolver*/ undefined, /*emitHost*/ undefined, compilerOptions, nodes, transformers, /*allowDtsFiles*/ true); + result.diagnostics = concatenate(result.diagnostics, diagnostics); + return result; +} diff --git a/src/services/transpile.ts b/src/services/transpile.ts index c739c8394f2c3..138434966fd18 100644 --- a/src/services/transpile.ts +++ b/src/services/transpile.ts @@ -1,146 +1,122 @@ -namespace ts { - export interface TranspileOptions { - compilerOptions?: CompilerOptions; - fileName?: string; - reportDiagnostics?: boolean; - moduleName?: string; - renamedDependencies?: MapLike; - transformers?: CustomTransformers; +import { CompilerOptions, MapLike, CustomTransformers, Diagnostic, getDefaultCompilerOptions, hasProperty, transpileOptionValueCompilerOptions, createSourceFile, createMapFromTemplate, getNewLineCharacter, CompilerHost, normalizePath, fileExtensionIs, Debug, createProgram, addRange, CommandLineOptionOfCustomType, filter, optionDeclarations, forEachEntry, cloneCompilerOptions, isString, parseCustomTypeOption, createCompilerDiagnosticForInvalidCustomType } from "./ts"; +export interface TranspileOptions { + compilerOptions?: CompilerOptions; + fileName?: string; + reportDiagnostics?: boolean; + moduleName?: string; + renamedDependencies?: MapLike; + transformers?: CustomTransformers; +} +export interface TranspileOutput { + outputText: string; + diagnostics?: Diagnostic[]; + sourceMapText?: string; +} +/* + * This function will compile source text from 'input' argument using specified compiler options. + * If not options are provided - it will use a set of default compiler options. + * Extra compiler options that will unconditionally be used by this function are: + * - isolatedModules = true + * - allowNonTsExtensions = true + * - noLib = true + * - noResolve = true + */ +export function transpileModule(input: string, transpileOptions: TranspileOptions): TranspileOutput { + const diagnostics: Diagnostic[] = []; + const options: CompilerOptions = transpileOptions.compilerOptions ? fixupCompilerOptions(transpileOptions.compilerOptions, diagnostics) : {}; + // mix in default options + const defaultOptions = getDefaultCompilerOptions(); + for (const key in defaultOptions) { + if (hasProperty(defaultOptions, key) && options[key] === undefined) { + options[key] = defaultOptions[key]; + } } - - export interface TranspileOutput { - outputText: string; - diagnostics?: Diagnostic[]; - sourceMapText?: string; + for (const option of transpileOptionValueCompilerOptions) { + options[option.name] = option.transpileOptionValue; } - - /* - * This function will compile source text from 'input' argument using specified compiler options. - * If not options are provided - it will use a set of default compiler options. - * Extra compiler options that will unconditionally be used by this function are: - * - isolatedModules = true - * - allowNonTsExtensions = true - * - noLib = true - * - noResolve = true - */ - export function transpileModule(input: string, transpileOptions: TranspileOptions): TranspileOutput { - const diagnostics: Diagnostic[] = []; - - const options: CompilerOptions = transpileOptions.compilerOptions ? fixupCompilerOptions(transpileOptions.compilerOptions, diagnostics) : {}; - - // mix in default options - const defaultOptions = getDefaultCompilerOptions(); - for (const key in defaultOptions) { - if (hasProperty(defaultOptions, key) && options[key] === undefined) { - options[key] = defaultOptions[key]; - } - } - - for (const option of transpileOptionValueCompilerOptions) { - options[option.name] = option.transpileOptionValue; - } - - // transpileModule does not write anything to disk so there is no need to verify that there are no conflicts between input and output paths. - options.suppressOutputPathCheck = true; - - // Filename can be non-ts file. - options.allowNonTsExtensions = true; - - // if jsx is specified then treat file as .tsx - const inputFileName = transpileOptions.fileName || (transpileOptions.compilerOptions && transpileOptions.compilerOptions.jsx ? "module.tsx" : "module.ts"); - const sourceFile = createSourceFile(inputFileName, input, options.target!); // TODO: GH#18217 - if (transpileOptions.moduleName) { - sourceFile.moduleName = transpileOptions.moduleName; - } - - if (transpileOptions.renamedDependencies) { - sourceFile.renamedDependencies = createMapFromTemplate(transpileOptions.renamedDependencies); - } - - const newLine = getNewLineCharacter(options); - - // Output - let outputText: string | undefined; - let sourceMapText: string | undefined; - - // Create a compilerHost object to allow the compiler to read and write files - const compilerHost: CompilerHost = { - getSourceFile: (fileName) => fileName === normalizePath(inputFileName) ? sourceFile : undefined, - writeFile: (name, text) => { - if (fileExtensionIs(name, ".map")) { - Debug.assertEqual(sourceMapText, undefined, "Unexpected multiple source map outputs, file:", name); - sourceMapText = text; - } - else { - Debug.assertEqual(outputText, undefined, "Unexpected multiple outputs, file:", name); - outputText = text; - } - }, - getDefaultLibFileName: () => "lib.d.ts", - useCaseSensitiveFileNames: () => false, - getCanonicalFileName: fileName => fileName, - getCurrentDirectory: () => "", - getNewLine: () => newLine, - fileExists: (fileName): boolean => fileName === inputFileName, - readFile: () => "", - directoryExists: () => true, - getDirectories: () => [] - }; - - const program = createProgram([inputFileName], options, compilerHost); - - if (transpileOptions.reportDiagnostics) { - addRange(/*to*/ diagnostics, /*from*/ program.getSyntacticDiagnostics(sourceFile)); - addRange(/*to*/ diagnostics, /*from*/ program.getOptionsDiagnostics()); - } - // Emit - program.emit(/*targetSourceFile*/ undefined, /*writeFile*/ undefined, /*cancellationToken*/ undefined, /*emitOnlyDtsFiles*/ undefined, transpileOptions.transformers); - - if (outputText === undefined) return Debug.fail("Output generation failed"); - - return { outputText, diagnostics, sourceMapText }; + // transpileModule does not write anything to disk so there is no need to verify that there are no conflicts between input and output paths. + options.suppressOutputPathCheck = true; + // Filename can be non-ts file. + options.allowNonTsExtensions = true; + // if jsx is specified then treat file as .tsx + const inputFileName = transpileOptions.fileName || (transpileOptions.compilerOptions && transpileOptions.compilerOptions.jsx ? "module.tsx" : "module.ts"); + const sourceFile = createSourceFile(inputFileName, input, (options.target!)); // TODO: GH#18217 + if (transpileOptions.moduleName) { + sourceFile.moduleName = transpileOptions.moduleName; } - - /* - * This is a shortcut function for transpileModule - it accepts transpileOptions as parameters and returns only outputText part of the result. - */ - export function transpile(input: string, compilerOptions?: CompilerOptions, fileName?: string, diagnostics?: Diagnostic[], moduleName?: string): string { - const output = transpileModule(input, { compilerOptions, fileName, reportDiagnostics: !!diagnostics, moduleName }); - // addRange correctly handles cases when wither 'from' or 'to' argument is missing - addRange(diagnostics, output.diagnostics); - return output.outputText; + if (transpileOptions.renamedDependencies) { + sourceFile.renamedDependencies = createMapFromTemplate(transpileOptions.renamedDependencies); } - - let commandLineOptionsStringToEnum: CommandLineOptionOfCustomType[]; - - /** JS users may pass in string values for enum compiler options (such as ModuleKind), so convert. */ - /*@internal*/ - export function fixupCompilerOptions(options: CompilerOptions, diagnostics: Diagnostic[]): CompilerOptions { - // Lazily create this value to fix module loading errors. - commandLineOptionsStringToEnum = commandLineOptionsStringToEnum || filter(optionDeclarations, o => - typeof o.type === "object" && !forEachEntry(o.type, v => typeof v !== "number")); - - options = cloneCompilerOptions(options); - - for (const opt of commandLineOptionsStringToEnum) { - if (!hasProperty(options, opt.name)) { - continue; - } - - const value = options[opt.name]; - // Value should be a key of opt.type - if (isString(value)) { - // If value is not a string, this will fail - options[opt.name] = parseCustomTypeOption(opt, value, diagnostics); + const newLine = getNewLineCharacter(options); + // Output + let outputText: string | undefined; + let sourceMapText: string | undefined; + // Create a compilerHost object to allow the compiler to read and write files + const compilerHost: CompilerHost = { + getSourceFile: (fileName) => fileName === normalizePath(inputFileName) ? sourceFile : undefined, + writeFile: (name, text) => { + if (fileExtensionIs(name, ".map")) { + Debug.assertEqual(sourceMapText, undefined, "Unexpected multiple source map outputs, file:", name); + sourceMapText = text; } else { - if (!forEachEntry(opt.type, v => v === value)) { - // Supplied value isn't a valid enum value. - diagnostics.push(createCompilerDiagnosticForInvalidCustomType(opt)); - } + Debug.assertEqual(outputText, undefined, "Unexpected multiple outputs, file:", name); + outputText = text; + } + }, + getDefaultLibFileName: () => "lib.d.ts", + useCaseSensitiveFileNames: () => false, + getCanonicalFileName: fileName => fileName, + getCurrentDirectory: () => "", + getNewLine: () => newLine, + fileExists: (fileName): boolean => fileName === inputFileName, + readFile: () => "", + directoryExists: () => true, + getDirectories: () => [] + }; + const program = createProgram([inputFileName], options, compilerHost); + if (transpileOptions.reportDiagnostics) { + addRange(/*to*/ diagnostics, /*from*/ program.getSyntacticDiagnostics(sourceFile)); + addRange(/*to*/ diagnostics, /*from*/ program.getOptionsDiagnostics()); + } + // Emit + program.emit(/*targetSourceFile*/ undefined, /*writeFile*/ undefined, /*cancellationToken*/ undefined, /*emitOnlyDtsFiles*/ undefined, transpileOptions.transformers); + if (outputText === undefined) + return Debug.fail("Output generation failed"); + return { outputText, diagnostics, sourceMapText }; +} +/* + * This is a shortcut function for transpileModule - it accepts transpileOptions as parameters and returns only outputText part of the result. + */ +export function transpile(input: string, compilerOptions?: CompilerOptions, fileName?: string, diagnostics?: Diagnostic[], moduleName?: string): string { + const output = transpileModule(input, { compilerOptions, fileName, reportDiagnostics: !!diagnostics, moduleName }); + // addRange correctly handles cases when wither 'from' or 'to' argument is missing + addRange(diagnostics, output.diagnostics); + return output.outputText; +} +let commandLineOptionsStringToEnum: CommandLineOptionOfCustomType[]; +/** JS users may pass in string values for enum compiler options (such as ModuleKind), so convert. */ +/*@internal*/ +export function fixupCompilerOptions(options: CompilerOptions, diagnostics: Diagnostic[]): CompilerOptions { + // Lazily create this value to fix module loading errors. + commandLineOptionsStringToEnum = commandLineOptionsStringToEnum || (filter(optionDeclarations, o => typeof o.type === "object" && !forEachEntry(o.type, v => typeof v !== "number"))); + options = cloneCompilerOptions(options); + for (const opt of commandLineOptionsStringToEnum) { + if (!hasProperty(options, opt.name)) { + continue; + } + const value = options[opt.name]; + // Value should be a key of opt.type + if (isString(value)) { + // If value is not a string, this will fail + options[opt.name] = parseCustomTypeOption(opt, value, diagnostics); + } + else { + if (!forEachEntry(opt.type, v => v === value)) { + // Supplied value isn't a valid enum value. + diagnostics.push(createCompilerDiagnosticForInvalidCustomType(opt)); } } - - return options; } + return options; } diff --git a/src/services/ts.BreakpointResolver.ts b/src/services/ts.BreakpointResolver.ts new file mode 100644 index 0000000000000..fdc405d65e5f7 --- /dev/null +++ b/src/services/ts.BreakpointResolver.ts @@ -0,0 +1 @@ +export * from "./breakpoints"; diff --git a/src/services/ts.CallHierarchy.ts b/src/services/ts.CallHierarchy.ts new file mode 100644 index 0000000000000..6d2f81ffb54a7 --- /dev/null +++ b/src/services/ts.CallHierarchy.ts @@ -0,0 +1 @@ +export * from "./callHierarchy"; diff --git a/src/services/ts.Completions.StringCompletions.ts b/src/services/ts.Completions.StringCompletions.ts new file mode 100644 index 0000000000000..596993e47673a --- /dev/null +++ b/src/services/ts.Completions.StringCompletions.ts @@ -0,0 +1 @@ +export * from "./stringCompletions"; diff --git a/src/services/ts.Completions.ts b/src/services/ts.Completions.ts new file mode 100644 index 0000000000000..304e779504c8f --- /dev/null +++ b/src/services/ts.Completions.ts @@ -0,0 +1,3 @@ +export * from "./completions"; +import * as StringCompletions from "./ts.Completions.StringCompletions"; +export { StringCompletions }; diff --git a/src/services/ts.FindAllReferences.ts b/src/services/ts.FindAllReferences.ts new file mode 100644 index 0000000000000..e826bfd9af9a4 --- /dev/null +++ b/src/services/ts.FindAllReferences.ts @@ -0,0 +1,2 @@ +export * from "./importTracker"; +export * from "./findAllReferences"; diff --git a/src/services/ts.GoToDefinition.ts b/src/services/ts.GoToDefinition.ts new file mode 100644 index 0000000000000..194590ceb7fc9 --- /dev/null +++ b/src/services/ts.GoToDefinition.ts @@ -0,0 +1 @@ +export * from "./goToDefinition"; diff --git a/src/services/ts.JsDoc.ts b/src/services/ts.JsDoc.ts new file mode 100644 index 0000000000000..34e5e723d2d18 --- /dev/null +++ b/src/services/ts.JsDoc.ts @@ -0,0 +1 @@ +export * from "./jsDoc"; diff --git a/src/services/ts.NavigateTo.ts b/src/services/ts.NavigateTo.ts new file mode 100644 index 0000000000000..705ce413fdc60 --- /dev/null +++ b/src/services/ts.NavigateTo.ts @@ -0,0 +1 @@ +export * from "./navigateTo"; diff --git a/src/services/ts.NavigationBar.ts b/src/services/ts.NavigationBar.ts new file mode 100644 index 0000000000000..931b97494fbf8 --- /dev/null +++ b/src/services/ts.NavigationBar.ts @@ -0,0 +1 @@ +export * from "./navigationBar"; diff --git a/src/services/ts.OrganizeImports.ts b/src/services/ts.OrganizeImports.ts new file mode 100644 index 0000000000000..f09ada71eebb4 --- /dev/null +++ b/src/services/ts.OrganizeImports.ts @@ -0,0 +1 @@ +export * from "./organizeImports"; diff --git a/src/services/ts.OutliningElementsCollector.ts b/src/services/ts.OutliningElementsCollector.ts new file mode 100644 index 0000000000000..27e96c9bca17b --- /dev/null +++ b/src/services/ts.OutliningElementsCollector.ts @@ -0,0 +1 @@ +export * from "./outliningElementsCollector"; diff --git a/src/services/ts.Rename.ts b/src/services/ts.Rename.ts new file mode 100644 index 0000000000000..9d80d3774199a --- /dev/null +++ b/src/services/ts.Rename.ts @@ -0,0 +1 @@ +export * from "./rename"; diff --git a/src/services/ts.SignatureHelp.ts b/src/services/ts.SignatureHelp.ts new file mode 100644 index 0000000000000..3bd243b696b29 --- /dev/null +++ b/src/services/ts.SignatureHelp.ts @@ -0,0 +1 @@ +export * from "./signatureHelp"; diff --git a/src/services/ts.SmartSelectionRange.ts b/src/services/ts.SmartSelectionRange.ts new file mode 100644 index 0000000000000..447d6a3121e8b --- /dev/null +++ b/src/services/ts.SmartSelectionRange.ts @@ -0,0 +1 @@ +export * from "./smartSelection"; diff --git a/src/services/ts.SymbolDisplay.ts b/src/services/ts.SymbolDisplay.ts new file mode 100644 index 0000000000000..aa6ab47d88ca5 --- /dev/null +++ b/src/services/ts.SymbolDisplay.ts @@ -0,0 +1 @@ +export * from "./symbolDisplay"; diff --git a/src/services/ts.codefix.ts b/src/services/ts.codefix.ts new file mode 100644 index 0000000000000..38f4227f351b2 --- /dev/null +++ b/src/services/ts.codefix.ts @@ -0,0 +1,48 @@ +export * from "./codeFixProvider"; +export * from "./codefixes/addConvertToUnknownForNonOverlappingTypes"; +export * from "./codefixes/addEmptyExportDeclaration"; +export * from "./codefixes/addMissingAsync"; +export * from "./codefixes/addMissingAwait"; +export * from "./codefixes/addMissingConst"; +export * from "./codefixes/addMissingDeclareProperty"; +export * from "./codefixes/addMissingInvocationForDecorator"; +export * from "./codefixes/addNameToNamelessParameter"; +export * from "./codefixes/annotateWithTypeFromJSDoc"; +export * from "./codefixes/inferFromUsage"; +export * from "./codefixes/convertFunctionToEs6Class"; +export * from "./codefixes/convertToAsyncFunction"; +export * from "./codefixes/convertToEs6Module"; +export * from "./codefixes/correctQualifiedNameToIndexedAccessType"; +export * from "./codefixes/convertToTypeOnlyExport"; +export * from "./codefixes/fixClassIncorrectlyImplementsInterface"; +export * from "./codefixes/importFixes"; +export * from "./codefixes/fixSpelling"; +export * from "./codefixes/fixAddMissingMember"; +export * from "./codefixes/fixAddMissingNewOperator"; +export * from "./codefixes/fixCannotFindModule"; +export * from "./codefixes/fixClassDoesntImplementInheritedAbstractMember"; +export * from "./codefixes/fixClassSuperMustPrecedeThisAccess"; +export * from "./codefixes/fixConstructorForDerivedNeedSuperCall"; +export * from "./codefixes/fixEnableExperimentalDecorators"; +export * from "./codefixes/fixEnableJsxFlag"; +export * from "./codefixes/fixModuleAndTargetOptions"; +export * from "./codefixes/fixExtendsInterfaceBecomesImplements"; +export * from "./codefixes/fixForgottenThisPropertyAccess"; +export * from "./codefixes/fixInvalidJsxCharacters"; +export * from "./codefixes/fixUnusedIdentifier"; +export * from "./codefixes/fixUnreachableCode"; +export * from "./codefixes/fixUnusedLabel"; +export * from "./codefixes/fixJSDocTypes"; +export * from "./codefixes/fixAwaitInSyncFunction"; +export * from "./codefixes/disableJsDiagnostics"; +export * from "./codefixes/helpers"; +export * from "./codefixes/fixInvalidImportSyntax"; +export * from "./codefixes/fixStrictClassInitialization"; +export * from "./codefixes/requireInTs"; +export * from "./codefixes/useDefaultImport"; +export * from "./codefixes/useBigintLiteral"; +export * from "./codefixes/fixAddModuleReferTypeMissingTypeof"; +export * from "./codefixes/convertToMappedObjectType"; +export * from "./codefixes/removeUnnecessaryAwait"; +export * from "./codefixes/splitTypeOnlyImport"; +export * from "./codefixes/convertConstToLet"; diff --git a/src/services/ts.formatting.ts b/src/services/ts.formatting.ts new file mode 100644 index 0000000000000..78562949a6593 --- /dev/null +++ b/src/services/ts.formatting.ts @@ -0,0 +1,7 @@ +export * from "./formatting/formattingContext"; +export * from "./formatting/formattingScanner"; +export * from "./formatting/rule"; +export * from "./formatting/rules"; +export * from "./formatting/rulesMap"; +export * from "./formatting/formatting"; +export * from "./formatting/smartIndenter"; diff --git a/src/services/ts.refactor.addOrRemoveBracesToArrowFunction.ts b/src/services/ts.refactor.addOrRemoveBracesToArrowFunction.ts new file mode 100644 index 0000000000000..becd25afb9dc5 --- /dev/null +++ b/src/services/ts.refactor.addOrRemoveBracesToArrowFunction.ts @@ -0,0 +1 @@ +export * from "./refactors/addOrRemoveBracesToArrowFunction"; diff --git a/src/services/ts.refactor.convertParamsToDestructuredObject.ts b/src/services/ts.refactor.convertParamsToDestructuredObject.ts new file mode 100644 index 0000000000000..a9c66859a4fa6 --- /dev/null +++ b/src/services/ts.refactor.convertParamsToDestructuredObject.ts @@ -0,0 +1 @@ +export * from "./refactors/convertParamsToDestructuredObject"; diff --git a/src/services/ts.refactor.convertStringOrTemplateLiteral.ts b/src/services/ts.refactor.convertStringOrTemplateLiteral.ts new file mode 100644 index 0000000000000..e6a4cee5c1fe7 --- /dev/null +++ b/src/services/ts.refactor.convertStringOrTemplateLiteral.ts @@ -0,0 +1 @@ +export * from "./refactors/convertStringOrTemplateLiteral"; diff --git a/src/services/ts.refactor.extractSymbol.ts b/src/services/ts.refactor.extractSymbol.ts new file mode 100644 index 0000000000000..6f406a5164d6d --- /dev/null +++ b/src/services/ts.refactor.extractSymbol.ts @@ -0,0 +1 @@ +export * from "./refactors/extractSymbol"; diff --git a/src/services/ts.refactor.generateGetAccessorAndSetAccessor.ts b/src/services/ts.refactor.generateGetAccessorAndSetAccessor.ts new file mode 100644 index 0000000000000..0c0700dc70c5e --- /dev/null +++ b/src/services/ts.refactor.generateGetAccessorAndSetAccessor.ts @@ -0,0 +1 @@ +export * from "./refactors/generateGetAccessorAndSetAccessor"; diff --git a/src/services/ts.refactor.ts b/src/services/ts.refactor.ts new file mode 100644 index 0000000000000..b4e7eb4051929 --- /dev/null +++ b/src/services/ts.refactor.ts @@ -0,0 +1,15 @@ +export * from "./refactorProvider"; +export * from "./refactors/convertExport"; +export * from "./refactors/convertImport"; +export * from "./refactors/extractType"; +export * from "./refactors/moveToNewFile"; +import * as extractSymbol from "./ts.refactor.extractSymbol"; +export { extractSymbol }; +import * as generateGetAccessorAndSetAccessor from "./ts.refactor.generateGetAccessorAndSetAccessor"; +export { generateGetAccessorAndSetAccessor }; +import * as addOrRemoveBracesToArrowFunction from "./ts.refactor.addOrRemoveBracesToArrowFunction"; +export { addOrRemoveBracesToArrowFunction }; +import * as convertParamsToDestructuredObject from "./ts.refactor.convertParamsToDestructuredObject"; +export { convertParamsToDestructuredObject }; +import * as convertStringOrTemplateLiteral from "./ts.refactor.convertStringOrTemplateLiteral"; +export { convertStringOrTemplateLiteral }; diff --git a/src/services/ts.textChanges.ts b/src/services/ts.textChanges.ts new file mode 100644 index 0000000000000..25f2d2ca3c428 --- /dev/null +++ b/src/services/ts.textChanges.ts @@ -0,0 +1 @@ +export * from "./textChanges"; diff --git a/src/services/ts.ts b/src/services/ts.ts new file mode 100644 index 0000000000000..60274feba2a89 --- /dev/null +++ b/src/services/ts.ts @@ -0,0 +1,53 @@ +export * from "../shims/ts"; +export * from "../compiler/ts"; +export * from "../jsTyping/ts"; +export * from "./types"; +export * from "./utilities"; +export * from "./classifier"; +export * from "./documentHighlights"; +export * from "./documentRegistry"; +export * from "./getEditsForFileRename"; +export * from "./patternMatcher"; +export * from "./preProcess"; +export * from "./sourcemaps"; +export * from "./suggestionDiagnostics"; +export * from "./transpile"; +export * from "./services"; +export * from "./transform"; +export * from "./shims"; +import * as Completions from "./ts.Completions"; +export { Completions }; +import * as FindAllReferences from "./ts.FindAllReferences"; +export { FindAllReferences }; +import * as CallHierarchy from "./ts.CallHierarchy"; +export { CallHierarchy }; +import * as GoToDefinition from "./ts.GoToDefinition"; +export { GoToDefinition }; +import * as JsDoc from "./ts.JsDoc"; +export { JsDoc }; +import * as NavigateTo from "./ts.NavigateTo"; +export { NavigateTo }; +import * as NavigationBar from "./ts.NavigationBar"; +export { NavigationBar }; +import * as OrganizeImports from "./ts.OrganizeImports"; +export { OrganizeImports }; +import * as OutliningElementsCollector from "./ts.OutliningElementsCollector"; +export { OutliningElementsCollector }; +import * as Rename from "./ts.Rename"; +export { Rename }; +import * as SmartSelectionRange from "./ts.SmartSelectionRange"; +export { SmartSelectionRange }; +import * as SignatureHelp from "./ts.SignatureHelp"; +export { SignatureHelp }; +import * as SymbolDisplay from "./ts.SymbolDisplay"; +export { SymbolDisplay }; +import * as formatting from "./ts.formatting"; +export { formatting }; +import * as textChanges from "./ts.textChanges"; +export { textChanges }; +import * as codefix from "./ts.codefix"; +export { codefix }; +import * as refactor from "./ts.refactor"; +export { refactor }; +import * as BreakpointResolver from "./ts.BreakpointResolver"; +export { BreakpointResolver }; diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index 58e43ee9921d5..f7c635fa76505 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../tsconfig-base", "compilerOptions": { - "outFile": "../../built/local/services.js" + "outDir": "../../built/local" }, "references": [ { "path": "../shims" }, @@ -107,6 +107,31 @@ "transform.ts", "shims.ts", "globalThisShim.ts", - "exportAsModule.ts" + "exportAsModule.ts", + "./ts.ts", + "./ts.Completions.StringCompletions.ts", + "./ts.Completions.ts", + "./ts.FindAllReferences.ts", + "./ts.CallHierarchy.ts", + "./ts.GoToDefinition.ts", + "./ts.JsDoc.ts", + "./ts.NavigateTo.ts", + "./ts.NavigationBar.ts", + "./ts.OrganizeImports.ts", + "./ts.OutliningElementsCollector.ts", + "./ts.Rename.ts", + "./ts.SmartSelectionRange.ts", + "./ts.SignatureHelp.ts", + "./ts.SymbolDisplay.ts", + "./ts.formatting.ts", + "./ts.textChanges.ts", + "./ts.codefix.ts", + "./ts.refactor.ts", + "./ts.refactor.extractSymbol.ts", + "./ts.refactor.generateGetAccessorAndSetAccessor.ts", + "./ts.refactor.addOrRemoveBracesToArrowFunction.ts", + "./ts.refactor.convertParamsToDestructuredObject.ts", + "./ts.refactor.convertStringOrTemplateLiteral.ts", + "./ts.BreakpointResolver.ts" ] } diff --git a/src/services/types.ts b/src/services/types.ts index 1df82f8a7e9d9..d723d2c26b151 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -1,1328 +1,1148 @@ -namespace ts { +import { TextChangeRange, FileReference, Path, ModuleSpecifierResolutionHost, CompilerOptions, ScriptKind, ProjectReference, ResolvedProjectReference, ResolvedModule, ResolvedModuleWithFailedLookupLocations, ResolvedTypeReferenceDirective, HasInvalidatedResolution, CustomTransformers, DocumentPositionMapper, SourceFileLike, ResolvedProjectReferenceCallbacks, DiagnosticWithLocation, Diagnostic, TextSpan, UserPreferences, Symbol, DocumentHighlights, LineAndCharacter, SourceMapper, TextRange, EmitOutput, Program, SourceFile, CancellationToken } from "./ts"; +import { ImportSuggestionsForFileCache } from "./ts.Completions"; +import { TextChangesContext } from "./ts.textChanges"; +import * as ts from "./ts"; +declare module "../compiler/types" { export interface Node { - getSourceFile(): SourceFile; - getChildCount(sourceFile?: SourceFile): number; - getChildAt(index: number, sourceFile?: SourceFile): Node; - getChildren(sourceFile?: SourceFile): Node[]; + getSourceFile(): ts.SourceFile; + getChildCount(sourceFile?: ts.SourceFile): number; + getChildAt(index: number, sourceFile?: ts.SourceFile): ts.Node; + getChildren(sourceFile?: ts.SourceFile): ts.Node[]; /* @internal */ - getChildren(sourceFile?: SourceFileLike): Node[]; // eslint-disable-line @typescript-eslint/unified-signatures - getStart(sourceFile?: SourceFile, includeJsDocComment?: boolean): number; + getChildren(sourceFile?: ts.SourceFileLike): ts.Node[]; // eslint-disable-line @typescript-eslint/unified-signatures + getStart(sourceFile?: ts.SourceFile, includeJsDocComment?: boolean): number; /* @internal */ - getStart(sourceFile?: SourceFileLike, includeJsDocComment?: boolean): number; // eslint-disable-line @typescript-eslint/unified-signatures + getStart(sourceFile?: ts.SourceFileLike, includeJsDocComment?: boolean): number; // eslint-disable-line @typescript-eslint/unified-signatures getFullStart(): number; getEnd(): number; - getWidth(sourceFile?: SourceFileLike): number; + getWidth(sourceFile?: ts.SourceFileLike): number; getFullWidth(): number; - getLeadingTriviaWidth(sourceFile?: SourceFile): number; - getFullText(sourceFile?: SourceFile): string; - getText(sourceFile?: SourceFile): string; - getFirstToken(sourceFile?: SourceFile): Node | undefined; + getLeadingTriviaWidth(sourceFile?: ts.SourceFile): number; + getFullText(sourceFile?: ts.SourceFile): string; + getText(sourceFile?: ts.SourceFile): string; + getFirstToken(sourceFile?: ts.SourceFile): ts.Node | undefined; /* @internal */ - getFirstToken(sourceFile?: SourceFileLike): Node | undefined; // eslint-disable-line @typescript-eslint/unified-signatures - getLastToken(sourceFile?: SourceFile): Node | undefined; + getFirstToken(sourceFile?: ts.SourceFileLike): ts.Node | undefined; // eslint-disable-line @typescript-eslint/unified-signatures + getLastToken(sourceFile?: ts.SourceFile): ts.Node | undefined; /* @internal */ - getLastToken(sourceFile?: SourceFileLike): Node | undefined; // eslint-disable-line @typescript-eslint/unified-signatures + getLastToken(sourceFile?: ts.SourceFileLike): ts.Node | undefined; // eslint-disable-line @typescript-eslint/unified-signatures // See ts.forEachChild for documentation. - forEachChild(cbNode: (node: Node) => T | undefined, cbNodeArray?: (nodes: NodeArray) => T | undefined): T | undefined; + forEachChild(cbNode: (node: ts.Node) => T | undefined, cbNodeArray?: (nodes: ts.NodeArray) => T | undefined): T | undefined; } - +} +declare module "../compiler/types" { export interface Identifier { readonly text: string; } - +} +declare module "../compiler/types" { export interface PrivateIdentifier { readonly text: string; } - +} +declare module "../compiler/types" { export interface Symbol { readonly name: string; - getFlags(): SymbolFlags; - getEscapedName(): __String; + getFlags(): ts.SymbolFlags; + getEscapedName(): ts.__String; getName(): string; - getDeclarations(): Declaration[] | undefined; - getDocumentationComment(typeChecker: TypeChecker | undefined): SymbolDisplayPart[]; + getDeclarations(): ts.Declaration[] | undefined; + getDocumentationComment(typeChecker: ts.TypeChecker | undefined): SymbolDisplayPart[]; getJsDocTags(): JSDocTagInfo[]; } - +} +declare module "../compiler/types" { export interface Type { - getFlags(): TypeFlags; - getSymbol(): Symbol | undefined; - getProperties(): Symbol[]; - getProperty(propertyName: string): Symbol | undefined; - getApparentProperties(): Symbol[]; - getCallSignatures(): readonly Signature[]; - getConstructSignatures(): readonly Signature[]; - getStringIndexType(): Type | undefined; - getNumberIndexType(): Type | undefined; - getBaseTypes(): BaseType[] | undefined; - getNonNullableType(): Type; - /*@internal*/ getNonOptionalType(): Type; + getFlags(): ts.TypeFlags; + getSymbol(): ts.Symbol | undefined; + getProperties(): ts.Symbol[]; + getProperty(propertyName: string): ts.Symbol | undefined; + getApparentProperties(): ts.Symbol[]; + getCallSignatures(): readonly ts.Signature[]; + getConstructSignatures(): readonly ts.Signature[]; + getStringIndexType(): ts.Type | undefined; + getNumberIndexType(): ts.Type | undefined; + getBaseTypes(): ts.BaseType[] | undefined; + getNonNullableType(): ts.Type; + /*@internal*/ getNonOptionalType(): ts.Type; /*@internal*/ isNullableType(): boolean; - getConstraint(): Type | undefined; - getDefault(): Type | undefined; - - isUnion(): this is UnionType; - isIntersection(): this is IntersectionType; - isUnionOrIntersection(): this is UnionOrIntersectionType; - isLiteral(): this is LiteralType; - isStringLiteral(): this is StringLiteralType; - isNumberLiteral(): this is NumberLiteralType; - isTypeParameter(): this is TypeParameter; - isClassOrInterface(): this is InterfaceType; - isClass(): this is InterfaceType; + getConstraint(): ts.Type | undefined; + getDefault(): ts.Type | undefined; + isUnion(): this is ts.UnionType; + isIntersection(): this is ts.IntersectionType; + isUnionOrIntersection(): this is ts.UnionOrIntersectionType; + isLiteral(): this is ts.LiteralType; + isStringLiteral(): this is ts.StringLiteralType; + isNumberLiteral(): this is ts.NumberLiteralType; + isTypeParameter(): this is ts.TypeParameter; + isClassOrInterface(): this is ts.InterfaceType; + isClass(): this is ts.InterfaceType; } - +} +declare module "../compiler/types" { export interface TypeReference { - typeArguments?: readonly Type[]; + typeArguments?: readonly ts.Type[]; } - +} +declare module "../compiler/types" { export interface Signature { - getDeclaration(): SignatureDeclaration; - getTypeParameters(): TypeParameter[] | undefined; - getParameters(): Symbol[]; - getReturnType(): Type; - getDocumentationComment(typeChecker: TypeChecker | undefined): SymbolDisplayPart[]; + getDeclaration(): ts.SignatureDeclaration; + getTypeParameters(): ts.TypeParameter[] | undefined; + getParameters(): ts.Symbol[]; + getReturnType(): ts.Type; + getDocumentationComment(typeChecker: ts.TypeChecker | undefined): SymbolDisplayPart[]; getJsDocTags(): JSDocTagInfo[]; } - +} +declare module "../compiler/types" { export interface SourceFile { /* @internal */ version: string; /* @internal */ scriptSnapshot: IScriptSnapshot | undefined; - /* @internal */ nameTable: UnderscoreEscapedMap | undefined; - - /* @internal */ getNamedDeclarations(): Map; - - getLineAndCharacterOfPosition(pos: number): LineAndCharacter; + /* @internal */ nameTable: ts.UnderscoreEscapedMap | undefined; + /* @internal */ getNamedDeclarations(): ts.Map; + getLineAndCharacterOfPosition(pos: number): ts.LineAndCharacter; getLineEndOfPosition(pos: number): number; getLineStarts(): readonly number[]; getPositionOfLineAndCharacter(line: number, character: number): number; - update(newText: string, textChangeRange: TextChangeRange): SourceFile; - - /* @internal */ sourceMapper?: DocumentPositionMapper; + update(newText: string, textChangeRange: ts.TextChangeRange): ts.SourceFile; + /* @internal */ sourceMapper?: ts.DocumentPositionMapper; } - +} +declare module "../compiler/types" { export interface SourceFileLike { - getLineAndCharacterOfPosition(pos: number): LineAndCharacter; + getLineAndCharacterOfPosition(pos: number): ts.LineAndCharacter; } - +} +declare module "../compiler/types" { export interface SourceMapSource { - getLineAndCharacterOfPosition(pos: number): LineAndCharacter; + getLineAndCharacterOfPosition(pos: number): ts.LineAndCharacter; } - +} +/** + * Represents an immutable snapshot of a script at a specified time.Once acquired, the + * snapshot is observably immutable. i.e. the same calls with the same parameters will return + * the same values. + */ +// eslint-disable-next-line @typescript-eslint/interface-name-prefix +export interface IScriptSnapshot { + /** Gets a portion of the script snapshot specified by [start, end). */ + getText(start: number, end: number): string; + /** Gets the length of this script snapshot. */ + getLength(): number; /** - * Represents an immutable snapshot of a script at a specified time.Once acquired, the - * snapshot is observably immutable. i.e. the same calls with the same parameters will return - * the same values. + * Gets the TextChangeRange that describe how the text changed between this text and + * an older version. This information is used by the incremental parser to determine + * what sections of the script need to be re-parsed. 'undefined' can be returned if the + * change range cannot be determined. However, in that case, incremental parsing will + * not happen and the entire document will be re - parsed. */ - // eslint-disable-next-line @typescript-eslint/interface-name-prefix - export interface IScriptSnapshot { - /** Gets a portion of the script snapshot specified by [start, end). */ - getText(start: number, end: number): string; - - /** Gets the length of this script snapshot. */ - getLength(): number; - - /** - * Gets the TextChangeRange that describe how the text changed between this text and - * an older version. This information is used by the incremental parser to determine - * what sections of the script need to be re-parsed. 'undefined' can be returned if the - * change range cannot be determined. However, in that case, incremental parsing will - * not happen and the entire document will be re - parsed. - */ - getChangeRange(oldSnapshot: IScriptSnapshot): TextChangeRange | undefined; - - /** Releases all resources held by this script snapshot */ - dispose?(): void; - } - - export namespace ScriptSnapshot { - class StringScriptSnapshot implements IScriptSnapshot { - - constructor(private text: string) { - } - - public getText(start: number, end: number): string { - return start === 0 && end === this.text.length - ? this.text - : this.text.substring(start, end); - } - - public getLength(): number { - return this.text.length; - } - - public getChangeRange(): TextChangeRange | undefined { - // Text-based snapshots do not support incremental parsing. Return undefined - // to signal that to the caller. - return undefined; - } + getChangeRange(oldSnapshot: IScriptSnapshot): TextChangeRange | undefined; + /** Releases all resources held by this script snapshot */ + dispose?(): void; +} +export namespace ScriptSnapshot { + class StringScriptSnapshot implements IScriptSnapshot { + constructor(private text: string) { } - - export function fromString(text: string): IScriptSnapshot { - return new StringScriptSnapshot(text); + public getText(start: number, end: number): string { + return start === 0 && end === this.text.length + ? this.text + : this.text.substring(start, end); + } + public getLength(): number { + return this.text.length; + } + public getChangeRange(): TextChangeRange | undefined { + // Text-based snapshots do not support incremental parsing. Return undefined + // to signal that to the caller. + return undefined; } } - - export interface PreProcessedFileInfo { - referencedFiles: FileReference[]; - typeReferenceDirectives: FileReference[]; - libReferenceDirectives: FileReference[]; - importedFiles: FileReference[]; - ambientExternalModules?: string[]; - isLibFile: boolean; - } - - export interface HostCancellationToken { - isCancellationRequested(): boolean; - } - - export interface InstallPackageOptions { - fileName: Path; - packageName: string; + export function fromString(text: string): IScriptSnapshot { + return new StringScriptSnapshot(text); } - +} +export interface PreProcessedFileInfo { + referencedFiles: FileReference[]; + typeReferenceDirectives: FileReference[]; + libReferenceDirectives: FileReference[]; + importedFiles: FileReference[]; + ambientExternalModules?: string[]; + isLibFile: boolean; +} +export interface HostCancellationToken { + isCancellationRequested(): boolean; +} +export interface InstallPackageOptions { + fileName: Path; + packageName: string; +} +/* @internal */ +export const enum PackageJsonDependencyGroup { + Dependencies = 1 << 0, + DevDependencies = 1 << 1, + PeerDependencies = 1 << 2, + OptionalDependencies = 1 << 3, + All = Dependencies | DevDependencies | PeerDependencies | OptionalDependencies +} +/* @internal */ +export interface PackageJsonInfo { + fileName: string; + dependencies?: ts.Map; + devDependencies?: ts.Map; + peerDependencies?: ts.Map; + optionalDependencies?: ts.Map; + get(dependencyName: string, inGroups?: PackageJsonDependencyGroup): string | undefined; + has(dependencyName: string, inGroups?: PackageJsonDependencyGroup): boolean; +} +// +// Public interface of the host of a language service instance. +// +export interface LanguageServiceHost extends ModuleSpecifierResolutionHost { + getCompilationSettings(): CompilerOptions; + getNewLine?(): string; + getProjectVersion?(): string; + getScriptFileNames(): string[]; + getScriptKind?(fileName: string): ScriptKind; + getScriptVersion(fileName: string): string; + getScriptSnapshot(fileName: string): IScriptSnapshot | undefined; + getProjectReferences?(): readonly ProjectReference[] | undefined; + getLocalizedDiagnosticMessages?(): any; + getCancellationToken?(): HostCancellationToken; + getCurrentDirectory(): string; + getDefaultLibFileName(options: CompilerOptions): string; + log?(s: string): void; + trace?(s: string): void; + error?(s: string): void; + /* + * LS host can optionally implement these methods to support completions for module specifiers. + * Without these methods, only completions for ambient modules will be provided. + */ + readDirectory?(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[]; + readFile?(path: string, encoding?: string): string | undefined; + realpath?(path: string): string; + fileExists?(path: string): boolean; + /* + * LS host can optionally implement these methods to support automatic updating when new type libraries are installed + */ + getTypeRootsVersion?(): number; + /* + * LS host can optionally implement this method if it wants to be completely in charge of module name resolution. + * if implementation is omitted then language service will use built-in module resolution logic and get answers to + * host specific questions using 'getScriptSnapshot'. + * + * If this is implemented, `getResolvedModuleWithFailedLookupLocationsFromCache` should be too. + */ + resolveModuleNames?(moduleNames: string[], containingFile: string, reusedNames: string[] | undefined, redirectedReference: ResolvedProjectReference | undefined, options: CompilerOptions): (ResolvedModule | undefined)[]; + getResolvedModuleWithFailedLookupLocationsFromCache?(modulename: string, containingFile: string): ResolvedModuleWithFailedLookupLocations | undefined; + resolveTypeReferenceDirectives?(typeDirectiveNames: string[], containingFile: string, redirectedReference: ResolvedProjectReference | undefined, options: CompilerOptions): (ResolvedTypeReferenceDirective | undefined)[]; + /* @internal */ hasInvalidatedResolution?: HasInvalidatedResolution; + /* @internal */ hasChangedAutomaticTypeDirectiveNames?: boolean; /* @internal */ - export const enum PackageJsonDependencyGroup { - Dependencies = 1 << 0, - DevDependencies = 1 << 1, - PeerDependencies = 1 << 2, - OptionalDependencies = 1 << 3, - All = Dependencies | DevDependencies | PeerDependencies | OptionalDependencies, - } - + getGlobalTypingsCacheLocation?(): string | undefined; + /* + * Required for full import and type reference completions. + * These should be unprefixed names. E.g. `getDirectories("/foo/bar")` should return `["a", "b"]`, not `["/foo/bar/a", "/foo/bar/b"]`. + */ + getDirectories?(directoryName: string): string[]; + /** + * Gets a set of custom transformers to use during emit. + */ + getCustomTransformers?(): CustomTransformers | undefined; + isKnownTypesPackageName?(name: string): boolean; + installPackage?(options: InstallPackageOptions): Promise; + writeFile?(fileName: string, content: string): void; /* @internal */ - export interface PackageJsonInfo { - fileName: string; - dependencies?: Map; - devDependencies?: Map; - peerDependencies?: Map; - optionalDependencies?: Map; - get(dependencyName: string, inGroups?: PackageJsonDependencyGroup): string | undefined; - has(dependencyName: string, inGroups?: PackageJsonDependencyGroup): boolean; - } - - // - // Public interface of the host of a language service instance. - // - export interface LanguageServiceHost extends ModuleSpecifierResolutionHost { - getCompilationSettings(): CompilerOptions; - getNewLine?(): string; - getProjectVersion?(): string; - getScriptFileNames(): string[]; - getScriptKind?(fileName: string): ScriptKind; - getScriptVersion(fileName: string): string; - getScriptSnapshot(fileName: string): IScriptSnapshot | undefined; - getProjectReferences?(): readonly ProjectReference[] | undefined; - getLocalizedDiagnosticMessages?(): any; - getCancellationToken?(): HostCancellationToken; - getCurrentDirectory(): string; - getDefaultLibFileName(options: CompilerOptions): string; - log?(s: string): void; - trace?(s: string): void; - error?(s: string): void; - - /* - * LS host can optionally implement these methods to support completions for module specifiers. - * Without these methods, only completions for ambient modules will be provided. - */ - readDirectory?(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[]; - readFile?(path: string, encoding?: string): string | undefined; - realpath?(path: string): string; - fileExists?(path: string): boolean; - - /* - * LS host can optionally implement these methods to support automatic updating when new type libraries are installed - */ - getTypeRootsVersion?(): number; - - /* - * LS host can optionally implement this method if it wants to be completely in charge of module name resolution. - * if implementation is omitted then language service will use built-in module resolution logic and get answers to - * host specific questions using 'getScriptSnapshot'. - * - * If this is implemented, `getResolvedModuleWithFailedLookupLocationsFromCache` should be too. - */ - resolveModuleNames?(moduleNames: string[], containingFile: string, reusedNames: string[] | undefined, redirectedReference: ResolvedProjectReference | undefined, options: CompilerOptions): (ResolvedModule | undefined)[]; - getResolvedModuleWithFailedLookupLocationsFromCache?(modulename: string, containingFile: string): ResolvedModuleWithFailedLookupLocations | undefined; - resolveTypeReferenceDirectives?(typeDirectiveNames: string[], containingFile: string, redirectedReference: ResolvedProjectReference | undefined, options: CompilerOptions): (ResolvedTypeReferenceDirective | undefined)[]; - /* @internal */ hasInvalidatedResolution?: HasInvalidatedResolution; - /* @internal */ hasChangedAutomaticTypeDirectiveNames?: boolean; - /* @internal */ - getGlobalTypingsCacheLocation?(): string | undefined; - - /* - * Required for full import and type reference completions. - * These should be unprefixed names. E.g. `getDirectories("/foo/bar")` should return `["a", "b"]`, not `["/foo/bar/a", "/foo/bar/b"]`. - */ - getDirectories?(directoryName: string): string[]; - - /** - * Gets a set of custom transformers to use during emit. - */ - getCustomTransformers?(): CustomTransformers | undefined; - - isKnownTypesPackageName?(name: string): boolean; - installPackage?(options: InstallPackageOptions): Promise; - writeFile?(fileName: string, content: string): void; - - /* @internal */ - getDocumentPositionMapper?(generatedFileName: string, sourceFileName?: string): DocumentPositionMapper | undefined; - /* @internal */ - getSourceFileLike?(fileName: string): SourceFileLike | undefined; - /* @internal */ - getPackageJsonsVisibleToFile?(fileName: string, rootDir?: string): readonly PackageJsonInfo[]; - /* @internal */ - getImportSuggestionsCache?(): Completions.ImportSuggestionsForFileCache; - /* @internal */ - setResolvedProjectReferenceCallbacks?(callbacks: ResolvedProjectReferenceCallbacks): void; - /* @internal */ - useSourceOfProjectReferenceRedirect?(): boolean; - } - + getDocumentPositionMapper?(generatedFileName: string, sourceFileName?: string): DocumentPositionMapper | undefined; /* @internal */ - export const emptyOptions = {}; - - export type WithMetadata = T & { metadata?: unknown; }; - - // - // Public services of a language service instance associated - // with a language service host instance - // - export interface LanguageService { - cleanupSemanticCache(): void; - - getSyntacticDiagnostics(fileName: string): DiagnosticWithLocation[]; - /** The first time this is called, it will return global diagnostics (no location). */ - getSemanticDiagnostics(fileName: string): Diagnostic[]; - getSuggestionDiagnostics(fileName: string): DiagnosticWithLocation[]; - - // TODO: Rename this to getProgramDiagnostics to better indicate that these are any - // diagnostics present for the program level, and not just 'options' diagnostics. - getCompilerOptionsDiagnostics(): Diagnostic[]; - - /** - * @deprecated Use getEncodedSyntacticClassifications instead. - */ - getSyntacticClassifications(fileName: string, span: TextSpan): ClassifiedSpan[]; - - /** - * @deprecated Use getEncodedSemanticClassifications instead. - */ - getSemanticClassifications(fileName: string, span: TextSpan): ClassifiedSpan[]; - - // Encoded as triples of [start, length, ClassificationType]. - getEncodedSyntacticClassifications(fileName: string, span: TextSpan): Classifications; - getEncodedSemanticClassifications(fileName: string, span: TextSpan): Classifications; - - getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions | undefined): WithMetadata | undefined; - // "options" and "source" are optional only for backwards-compatibility - getCompletionEntryDetails( - fileName: string, - position: number, - name: string, - formatOptions: FormatCodeOptions | FormatCodeSettings | undefined, - source: string | undefined, - preferences: UserPreferences | undefined, - ): CompletionEntryDetails | undefined; - getCompletionEntrySymbol(fileName: string, position: number, name: string, source: string | undefined): Symbol | undefined; - - getQuickInfoAtPosition(fileName: string, position: number): QuickInfo | undefined; - - getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): TextSpan | undefined; - - getBreakpointStatementAtPosition(fileName: string, position: number): TextSpan | undefined; - - getSignatureHelpItems(fileName: string, position: number, options: SignatureHelpItemsOptions | undefined): SignatureHelpItems | undefined; - - getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): RenameInfo; - findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): readonly RenameLocation[] | undefined; - - getSmartSelectionRange(fileName: string, position: number): SelectionRange; - - getDefinitionAtPosition(fileName: string, position: number): readonly DefinitionInfo[] | undefined; - getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan | undefined; - getTypeDefinitionAtPosition(fileName: string, position: number): readonly DefinitionInfo[] | undefined; - getImplementationAtPosition(fileName: string, position: number): readonly ImplementationLocation[] | undefined; - - getReferencesAtPosition(fileName: string, position: number): ReferenceEntry[] | undefined; - findReferences(fileName: string, position: number): ReferencedSymbol[] | undefined; - getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): DocumentHighlights[] | undefined; - - /** @deprecated */ - getOccurrencesAtPosition(fileName: string, position: number): readonly ReferenceEntry[] | undefined; - - getNavigateToItems(searchValue: string, maxResultCount?: number, fileName?: string, excludeDtsFiles?: boolean): NavigateToItem[]; - getNavigationBarItems(fileName: string): NavigationBarItem[]; - getNavigationTree(fileName: string): NavigationTree; - - prepareCallHierarchy(fileName: string, position: number): CallHierarchyItem | CallHierarchyItem[] | undefined; - provideCallHierarchyIncomingCalls(fileName: string, position: number): CallHierarchyIncomingCall[]; - provideCallHierarchyOutgoingCalls(fileName: string, position: number): CallHierarchyOutgoingCall[]; - - getOutliningSpans(fileName: string): OutliningSpan[]; - getTodoComments(fileName: string, descriptors: TodoCommentDescriptor[]): TodoComment[]; - getBraceMatchingAtPosition(fileName: string, position: number): TextSpan[]; - getIndentationAtPosition(fileName: string, position: number, options: EditorOptions | EditorSettings): number; - - getFormattingEditsForRange(fileName: string, start: number, end: number, options: FormatCodeOptions | FormatCodeSettings): TextChange[]; - getFormattingEditsForDocument(fileName: string, options: FormatCodeOptions | FormatCodeSettings): TextChange[]; - getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: FormatCodeOptions | FormatCodeSettings): TextChange[]; - - getDocCommentTemplateAtPosition(fileName: string, position: number): TextInsertion | undefined; - - isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean; - /** - * This will return a defined result if the position is after the `>` of the opening tag, or somewhere in the text, of a JSXElement with no closing tag. - * Editors should call this after `>` is typed. - */ - getJsxClosingTagAtPosition(fileName: string, position: number): JsxClosingTagInfo | undefined; - - getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan | undefined; - - toLineColumnOffset?(fileName: string, position: number): LineAndCharacter; - /** @internal */ - getSourceMapper(): SourceMapper; - /** @internal */ - clearSourceMapperCache(): void; - - getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: readonly number[], formatOptions: FormatCodeSettings, preferences: UserPreferences): readonly CodeFixAction[]; - getCombinedCodeFix(scope: CombinedCodeFixScope, fixId: {}, formatOptions: FormatCodeSettings, preferences: UserPreferences): CombinedCodeActions; - - applyCodeActionCommand(action: CodeActionCommand, formatSettings?: FormatCodeSettings): Promise; - applyCodeActionCommand(action: CodeActionCommand[], formatSettings?: FormatCodeSettings): Promise; - applyCodeActionCommand(action: CodeActionCommand | CodeActionCommand[], formatSettings?: FormatCodeSettings): Promise; - /** @deprecated `fileName` will be ignored */ - applyCodeActionCommand(fileName: string, action: CodeActionCommand): Promise; - /** @deprecated `fileName` will be ignored */ - applyCodeActionCommand(fileName: string, action: CodeActionCommand[]): Promise; - /** @deprecated `fileName` will be ignored */ - applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise; - - getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined): ApplicableRefactorInfo[]; - getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; - organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; - getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; - - getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, forceDtsEmit?: boolean): EmitOutput; - - getProgram(): Program | undefined; - - /* @internal */ getNonBoundSourceFile(fileName: string): SourceFile; - - dispose(): void; - } - - export interface JsxClosingTagInfo { - readonly newText: string; - } - - export interface CombinedCodeFixScope { type: "file"; fileName: string; } - - export type OrganizeImportsScope = CombinedCodeFixScope; - - export type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#"; - - export interface GetCompletionsAtPositionOptions extends UserPreferences { - /** - * If the editor is asking for completions because a certain character was typed - * (as opposed to when the user explicitly requested them) this should be set. - */ - triggerCharacter?: CompletionsTriggerCharacter; - /** @deprecated Use includeCompletionsForModuleExports */ - includeExternalModuleExports?: boolean; - /** @deprecated Use includeCompletionsWithInsertText */ - includeInsertTextCompletions?: boolean; - } - - export type SignatureHelpTriggerCharacter = "," | "(" | "<"; - export type SignatureHelpRetriggerCharacter = SignatureHelpTriggerCharacter | ")"; - - export interface SignatureHelpItemsOptions { - triggerReason?: SignatureHelpTriggerReason; - } - - export type SignatureHelpTriggerReason = - | SignatureHelpInvokedReason - | SignatureHelpCharacterTypedReason - | SignatureHelpRetriggeredReason; - + getSourceFileLike?(fileName: string): SourceFileLike | undefined; + /* @internal */ + getPackageJsonsVisibleToFile?(fileName: string, rootDir?: string): readonly PackageJsonInfo[]; + /* @internal */ + getImportSuggestionsCache?(): ImportSuggestionsForFileCache; + /* @internal */ + setResolvedProjectReferenceCallbacks?(callbacks: ResolvedProjectReferenceCallbacks): void; + /* @internal */ + useSourceOfProjectReferenceRedirect?(): boolean; +} +/* @internal */ +export const emptyOptions = {}; +export type WithMetadata = T & { + metadata?: unknown; +}; +// +// Public services of a language service instance associated +// with a language service host instance +// +export interface LanguageService { + cleanupSemanticCache(): void; + getSyntacticDiagnostics(fileName: string): DiagnosticWithLocation[]; + /** The first time this is called, it will return global diagnostics (no location). */ + getSemanticDiagnostics(fileName: string): Diagnostic[]; + getSuggestionDiagnostics(fileName: string): DiagnosticWithLocation[]; + // TODO: Rename this to getProgramDiagnostics to better indicate that these are any + // diagnostics present for the program level, and not just 'options' diagnostics. + getCompilerOptionsDiagnostics(): Diagnostic[]; /** - * Signals that the user manually requested signature help. - * The language service will unconditionally attempt to provide a result. + * @deprecated Use getEncodedSyntacticClassifications instead. */ - export interface SignatureHelpInvokedReason { - kind: "invoked"; - triggerCharacter?: undefined; - } - + getSyntacticClassifications(fileName: string, span: TextSpan): ClassifiedSpan[]; /** - * Signals that the signature help request came from a user typing a character. - * Depending on the character and the syntactic context, the request may or may not be served a result. + * @deprecated Use getEncodedSemanticClassifications instead. */ - export interface SignatureHelpCharacterTypedReason { - kind: "characterTyped"; - /** - * Character that was responsible for triggering signature help. - */ - triggerCharacter: SignatureHelpTriggerCharacter; - } - + getSemanticClassifications(fileName: string, span: TextSpan): ClassifiedSpan[]; + // Encoded as triples of [start, length, ClassificationType]. + getEncodedSyntacticClassifications(fileName: string, span: TextSpan): Classifications; + getEncodedSemanticClassifications(fileName: string, span: TextSpan): Classifications; + getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions | undefined): WithMetadata | undefined; + // "options" and "source" are optional only for backwards-compatibility + getCompletionEntryDetails(fileName: string, position: number, name: string, formatOptions: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences | undefined): CompletionEntryDetails | undefined; + getCompletionEntrySymbol(fileName: string, position: number, name: string, source: string | undefined): Symbol | undefined; + getQuickInfoAtPosition(fileName: string, position: number): QuickInfo | undefined; + getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): TextSpan | undefined; + getBreakpointStatementAtPosition(fileName: string, position: number): TextSpan | undefined; + getSignatureHelpItems(fileName: string, position: number, options: SignatureHelpItemsOptions | undefined): SignatureHelpItems | undefined; + getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): RenameInfo; + findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): readonly RenameLocation[] | undefined; + getSmartSelectionRange(fileName: string, position: number): SelectionRange; + getDefinitionAtPosition(fileName: string, position: number): readonly DefinitionInfo[] | undefined; + getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan | undefined; + getTypeDefinitionAtPosition(fileName: string, position: number): readonly DefinitionInfo[] | undefined; + getImplementationAtPosition(fileName: string, position: number): readonly ImplementationLocation[] | undefined; + getReferencesAtPosition(fileName: string, position: number): ReferenceEntry[] | undefined; + findReferences(fileName: string, position: number): ReferencedSymbol[] | undefined; + getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): DocumentHighlights[] | undefined; + /** @deprecated */ + getOccurrencesAtPosition(fileName: string, position: number): readonly ReferenceEntry[] | undefined; + getNavigateToItems(searchValue: string, maxResultCount?: number, fileName?: string, excludeDtsFiles?: boolean): NavigateToItem[]; + getNavigationBarItems(fileName: string): NavigationBarItem[]; + getNavigationTree(fileName: string): NavigationTree; + prepareCallHierarchy(fileName: string, position: number): CallHierarchyItem | CallHierarchyItem[] | undefined; + provideCallHierarchyIncomingCalls(fileName: string, position: number): CallHierarchyIncomingCall[]; + provideCallHierarchyOutgoingCalls(fileName: string, position: number): CallHierarchyOutgoingCall[]; + getOutliningSpans(fileName: string): OutliningSpan[]; + getTodoComments(fileName: string, descriptors: TodoCommentDescriptor[]): TodoComment[]; + getBraceMatchingAtPosition(fileName: string, position: number): TextSpan[]; + getIndentationAtPosition(fileName: string, position: number, options: EditorOptions | EditorSettings): number; + getFormattingEditsForRange(fileName: string, start: number, end: number, options: FormatCodeOptions | FormatCodeSettings): TextChange[]; + getFormattingEditsForDocument(fileName: string, options: FormatCodeOptions | FormatCodeSettings): TextChange[]; + getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: FormatCodeOptions | FormatCodeSettings): TextChange[]; + getDocCommentTemplateAtPosition(fileName: string, position: number): TextInsertion | undefined; + isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean; /** - * Signals that this signature help request came from typing a character or moving the cursor. - * This should only occur if a signature help session was already active and the editor needs to see if it should adjust. - * The language service will unconditionally attempt to provide a result. - * `triggerCharacter` can be `undefined` for a retrigger caused by a cursor move. + * This will return a defined result if the position is after the `>` of the opening tag, or somewhere in the text, of a JSXElement with no closing tag. + * Editors should call this after `>` is typed. */ - export interface SignatureHelpRetriggeredReason { - kind: "retrigger"; - /** - * Character that was responsible for triggering signature help. - */ - triggerCharacter?: SignatureHelpRetriggerCharacter; - } - - export interface ApplyCodeActionCommandResult { - successMessage: string; - } - - export interface Classifications { - spans: number[]; - endOfLineState: EndOfLineState; - } - - export interface ClassifiedSpan { - textSpan: TextSpan; - classificationType: ClassificationTypeNames; - } - + getJsxClosingTagAtPosition(fileName: string, position: number): JsxClosingTagInfo | undefined; + getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan | undefined; + toLineColumnOffset?(fileName: string, position: number): LineAndCharacter; + /** @internal */ + getSourceMapper(): SourceMapper; + /** @internal */ + clearSourceMapperCache(): void; + getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: readonly number[], formatOptions: FormatCodeSettings, preferences: UserPreferences): readonly CodeFixAction[]; + getCombinedCodeFix(scope: CombinedCodeFixScope, fixId: {}, formatOptions: FormatCodeSettings, preferences: UserPreferences): CombinedCodeActions; + applyCodeActionCommand(action: CodeActionCommand, formatSettings?: FormatCodeSettings): Promise; + applyCodeActionCommand(action: CodeActionCommand[], formatSettings?: FormatCodeSettings): Promise; + applyCodeActionCommand(action: CodeActionCommand | CodeActionCommand[], formatSettings?: FormatCodeSettings): Promise; + /** @deprecated `fileName` will be ignored */ + applyCodeActionCommand(fileName: string, action: CodeActionCommand): Promise; + /** @deprecated `fileName` will be ignored */ + applyCodeActionCommand(fileName: string, action: CodeActionCommand[]): Promise; + /** @deprecated `fileName` will be ignored */ + applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise; + getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined): ApplicableRefactorInfo[]; + getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; + organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; + getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; + getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, forceDtsEmit?: boolean): EmitOutput; + getProgram(): Program | undefined; + /* @internal */ getNonBoundSourceFile(fileName: string): SourceFile; + dispose(): void; +} +export interface JsxClosingTagInfo { + readonly newText: string; +} +export interface CombinedCodeFixScope { + type: "file"; + fileName: string; +} +export type OrganizeImportsScope = CombinedCodeFixScope; +export type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#"; +export interface GetCompletionsAtPositionOptions extends UserPreferences { /** - * Navigation bar interface designed for visual studio's dual-column layout. - * This does not form a proper tree. - * The navbar is returned as a list of top-level items, each of which has a list of child items. - * Child items always have an empty array for their `childItems`. + * If the editor is asking for completions because a certain character was typed + * (as opposed to when the user explicitly requested them) this should be set. */ - export interface NavigationBarItem { - text: string; - kind: ScriptElementKind; - kindModifiers: string; - spans: TextSpan[]; - childItems: NavigationBarItem[]; - indent: number; - bolded: boolean; - grayed: boolean; - } - + triggerCharacter?: CompletionsTriggerCharacter; + /** @deprecated Use includeCompletionsForModuleExports */ + includeExternalModuleExports?: boolean; + /** @deprecated Use includeCompletionsWithInsertText */ + includeInsertTextCompletions?: boolean; +} +export type SignatureHelpTriggerCharacter = "," | "(" | "<"; +export type SignatureHelpRetriggerCharacter = SignatureHelpTriggerCharacter | ")"; +export interface SignatureHelpItemsOptions { + triggerReason?: SignatureHelpTriggerReason; +} +export type SignatureHelpTriggerReason = SignatureHelpInvokedReason | SignatureHelpCharacterTypedReason | SignatureHelpRetriggeredReason; +/** + * Signals that the user manually requested signature help. + * The language service will unconditionally attempt to provide a result. + */ +export interface SignatureHelpInvokedReason { + kind: "invoked"; + triggerCharacter?: undefined; +} +/** + * Signals that the signature help request came from a user typing a character. + * Depending on the character and the syntactic context, the request may or may not be served a result. + */ +export interface SignatureHelpCharacterTypedReason { + kind: "characterTyped"; /** - * Node in a tree of nested declarations in a file. - * The top node is always a script or module node. + * Character that was responsible for triggering signature help. */ - export interface NavigationTree { - /** Name of the declaration, or a short description, e.g. "". */ - text: string; - kind: ScriptElementKind; - /** ScriptElementKindModifier separated by commas, e.g. "public,abstract" */ - kindModifiers: string; - /** - * Spans of the nodes that generated this declaration. - * There will be more than one if this is the result of merging. - */ - spans: TextSpan[]; - nameSpan: TextSpan | undefined; - /** Present if non-empty */ - childItems?: NavigationTree[]; - } - - export interface CallHierarchyItem { - name: string; - kind: ScriptElementKind; - file: string; - span: TextSpan; - selectionSpan: TextSpan; - } - - export interface CallHierarchyIncomingCall { - from: CallHierarchyItem; - fromSpans: TextSpan[]; - } - - export interface CallHierarchyOutgoingCall { - to: CallHierarchyItem; - fromSpans: TextSpan[]; - } - - export interface TodoCommentDescriptor { - text: string; - priority: number; - } - - export interface TodoComment { - descriptor: TodoCommentDescriptor; - message: string; - position: number; - } - - export interface TextChange { - span: TextSpan; - newText: string; - } - - export interface FileTextChanges { - fileName: string; - textChanges: readonly TextChange[]; - isNewFile?: boolean; - } - - export interface CodeAction { - /** Description of the code action to display in the UI of the editor */ - description: string; - /** Text changes to apply to each file as part of the code action */ - changes: FileTextChanges[]; - /** - * If the user accepts the code fix, the editor should send the action back in a `applyAction` request. - * This allows the language service to have side effects (e.g. installing dependencies) upon a code fix. - */ - commands?: CodeActionCommand[]; - } - - export interface CodeFixAction extends CodeAction { - /** Short name to identify the fix, for use by telemetry. */ - fixName: string; - /** - * If present, one may call 'getCombinedCodeFix' with this fixId. - * This may be omitted to indicate that the code fix can't be applied in a group. - */ - fixId?: {}; - fixAllDescription?: string; - } - - export interface CombinedCodeActions { - changes: readonly FileTextChanges[]; - commands?: readonly CodeActionCommand[]; - } - - // Publicly, this type is just `{}`. Internally it is a union of all the actions we use. - // See `commands?: {}[]` in protocol.ts - export type CodeActionCommand = InstallPackageAction; - - export interface InstallPackageAction { - /* @internal */ readonly type: "install package"; - /* @internal */ readonly file: string; - /* @internal */ readonly packageName: string; - } - + triggerCharacter: SignatureHelpTriggerCharacter; +} +/** + * Signals that this signature help request came from typing a character or moving the cursor. + * This should only occur if a signature help session was already active and the editor needs to see if it should adjust. + * The language service will unconditionally attempt to provide a result. + * `triggerCharacter` can be `undefined` for a retrigger caused by a cursor move. + */ +export interface SignatureHelpRetriggeredReason { + kind: "retrigger"; /** - * A set of one or more available refactoring actions, grouped under a parent refactoring. + * Character that was responsible for triggering signature help. */ - export interface ApplicableRefactorInfo { - /** - * The programmatic name of the refactoring - */ - name: string; - /** - * A description of this refactoring category to show to the user. - * If the refactoring gets inlined (see below), this text will not be visible. - */ - description: string; - /** - * Inlineable refactorings can have their actions hoisted out to the top level - * of a context menu. Non-inlineanable refactorings should always be shown inside - * their parent grouping. - * - * If not specified, this value is assumed to be 'true' - */ - inlineable?: boolean; - - actions: RefactorActionInfo[]; - } - + triggerCharacter?: SignatureHelpRetriggerCharacter; +} +export interface ApplyCodeActionCommandResult { + successMessage: string; +} +export interface Classifications { + spans: number[]; + endOfLineState: EndOfLineState; +} +export interface ClassifiedSpan { + textSpan: TextSpan; + classificationType: ClassificationTypeNames; +} +/** + * Navigation bar interface designed for visual studio's dual-column layout. + * This does not form a proper tree. + * The navbar is returned as a list of top-level items, each of which has a list of child items. + * Child items always have an empty array for their `childItems`. + */ +export interface NavigationBarItem { + text: string; + kind: ScriptElementKind; + kindModifiers: string; + spans: TextSpan[]; + childItems: NavigationBarItem[]; + indent: number; + bolded: boolean; + grayed: boolean; +} +/** + * Node in a tree of nested declarations in a file. + * The top node is always a script or module node. + */ +export interface NavigationTree { + /** Name of the declaration, or a short description, e.g. "". */ + text: string; + kind: ScriptElementKind; + /** ScriptElementKindModifier separated by commas, e.g. "public,abstract" */ + kindModifiers: string; /** - * Represents a single refactoring action - for example, the "Extract Method..." refactor might - * offer several actions, each corresponding to a surround class or closure to extract into. + * Spans of the nodes that generated this declaration. + * There will be more than one if this is the result of merging. */ - export interface RefactorActionInfo { - /** - * The programmatic name of the refactoring action - */ - name: string; - - /** - * A description of this refactoring action to show to the user. - * If the parent refactoring is inlined away, this will be the only text shown, - * so this description should make sense by itself if the parent is inlineable=true - */ - description: string; - } - + spans: TextSpan[]; + nameSpan: TextSpan | undefined; + /** Present if non-empty */ + childItems?: NavigationTree[]; +} +export interface CallHierarchyItem { + name: string; + kind: ScriptElementKind; + file: string; + span: TextSpan; + selectionSpan: TextSpan; +} +export interface CallHierarchyIncomingCall { + from: CallHierarchyItem; + fromSpans: TextSpan[]; +} +export interface CallHierarchyOutgoingCall { + to: CallHierarchyItem; + fromSpans: TextSpan[]; +} +export interface TodoCommentDescriptor { + text: string; + priority: number; +} +export interface TodoComment { + descriptor: TodoCommentDescriptor; + message: string; + position: number; +} +export interface TextChange { + span: TextSpan; + newText: string; +} +export interface FileTextChanges { + fileName: string; + textChanges: readonly TextChange[]; + isNewFile?: boolean; +} +export interface CodeAction { + /** Description of the code action to display in the UI of the editor */ + description: string; + /** Text changes to apply to each file as part of the code action */ + changes: FileTextChanges[]; /** - * A set of edits to make in response to a refactor action, plus an optional - * location where renaming should be invoked from + * If the user accepts the code fix, the editor should send the action back in a `applyAction` request. + * This allows the language service to have side effects (e.g. installing dependencies) upon a code fix. */ - export interface RefactorEditInfo { - edits: FileTextChanges[]; - renameFilename?: string ; - renameLocation?: number; - commands?: CodeActionCommand[]; - } - - export interface TextInsertion { - newText: string; - /** The position in newText the caret should point to after the insertion. */ - caretOffset: number; - } - - export interface DocumentSpan { - textSpan: TextSpan; - fileName: string; - - /** - * If the span represents a location that was remapped (e.g. via a .d.ts.map file), - * then the original filename and span will be specified here - */ - originalTextSpan?: TextSpan; - originalFileName?: string; - - /** - * If DocumentSpan.textSpan is the span for name of the declaration, - * then this is the span for relevant declaration - */ - contextSpan?: TextSpan; - originalContextSpan?: TextSpan; - } - - export interface RenameLocation extends DocumentSpan { - readonly prefixText?: string; - readonly suffixText?: string; - } - - export interface ReferenceEntry extends DocumentSpan { - isWriteAccess: boolean; - isDefinition: boolean; - isInString?: true; - } - - export interface ImplementationLocation extends DocumentSpan { - kind: ScriptElementKind; - displayParts: SymbolDisplayPart[]; - } - - export const enum HighlightSpanKind { - none = "none", - definition = "definition", - reference = "reference", - writtenReference = "writtenReference", - } - - export interface HighlightSpan { - fileName?: string; - isInString?: true; - textSpan: TextSpan; - contextSpan?: TextSpan; - kind: HighlightSpanKind; - } - - export interface NavigateToItem { - name: string; - kind: ScriptElementKind; - kindModifiers: string; - matchKind: "exact" | "prefix" | "substring" | "camelCase"; - isCaseSensitive: boolean; - fileName: string; - textSpan: TextSpan; - containerName: string; - containerKind: ScriptElementKind; - } - - export enum IndentStyle { - None = 0, - Block = 1, - Smart = 2, - } - - export enum SemicolonPreference { - Ignore = "ignore", - Insert = "insert", - Remove = "remove", - } - - /* @deprecated - consider using EditorSettings instead */ - export interface EditorOptions { - BaseIndentSize?: number; - IndentSize: number; - TabSize: number; - NewLineCharacter: string; - ConvertTabsToSpaces: boolean; - IndentStyle: IndentStyle; - } - - // TODO: GH#18217 These are frequently asserted as defined - export interface EditorSettings { - baseIndentSize?: number; - indentSize?: number; - tabSize?: number; - newLineCharacter?: string; - convertTabsToSpaces?: boolean; - indentStyle?: IndentStyle; - } - - /* @deprecated - consider using FormatCodeSettings instead */ - export interface FormatCodeOptions extends EditorOptions { - InsertSpaceAfterCommaDelimiter: boolean; - InsertSpaceAfterSemicolonInForStatements: boolean; - InsertSpaceBeforeAndAfterBinaryOperators: boolean; - InsertSpaceAfterConstructor?: boolean; - InsertSpaceAfterKeywordsInControlFlowStatements: boolean; - InsertSpaceAfterFunctionKeywordForAnonymousFunctions: boolean; - InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: boolean; - InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: boolean; - InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces?: boolean; - InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: boolean; - InsertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces?: boolean; - InsertSpaceAfterTypeAssertion?: boolean; - InsertSpaceBeforeFunctionParenthesis?: boolean; - PlaceOpenBraceOnNewLineForFunctions: boolean; - PlaceOpenBraceOnNewLineForControlBlocks: boolean; - insertSpaceBeforeTypeAnnotation?: boolean; - } - - export interface FormatCodeSettings extends EditorSettings { - readonly insertSpaceAfterCommaDelimiter?: boolean; - readonly insertSpaceAfterSemicolonInForStatements?: boolean; - readonly insertSpaceBeforeAndAfterBinaryOperators?: boolean; - readonly insertSpaceAfterConstructor?: boolean; - readonly insertSpaceAfterKeywordsInControlFlowStatements?: boolean; - readonly insertSpaceAfterFunctionKeywordForAnonymousFunctions?: boolean; - readonly insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis?: boolean; - readonly insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets?: boolean; - readonly insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces?: boolean; - readonly insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces?: boolean; - readonly insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces?: boolean; - readonly insertSpaceAfterTypeAssertion?: boolean; - readonly insertSpaceBeforeFunctionParenthesis?: boolean; - readonly placeOpenBraceOnNewLineForFunctions?: boolean; - readonly placeOpenBraceOnNewLineForControlBlocks?: boolean; - readonly insertSpaceBeforeTypeAnnotation?: boolean; - readonly indentMultiLineObjectLiteralBeginningOnBlankLine?: boolean; - readonly semicolons?: SemicolonPreference; - } - - export function getDefaultFormatCodeSettings(newLineCharacter?: string): FormatCodeSettings { - return { - indentSize: 4, - tabSize: 4, - newLineCharacter: newLineCharacter || "\n", - convertTabsToSpaces: true, - indentStyle: IndentStyle.Smart, - insertSpaceAfterConstructor: false, - insertSpaceAfterCommaDelimiter: true, - insertSpaceAfterSemicolonInForStatements: true, - insertSpaceBeforeAndAfterBinaryOperators: true, - insertSpaceAfterKeywordsInControlFlowStatements: true, - insertSpaceAfterFunctionKeywordForAnonymousFunctions: false, - insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false, - insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false, - insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: true, - insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: false, - insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: false, - insertSpaceBeforeFunctionParenthesis: false, - placeOpenBraceOnNewLineForFunctions: false, - placeOpenBraceOnNewLineForControlBlocks: false, - semicolons: SemicolonPreference.Ignore, - }; - } - - /* @internal */ - export const testFormatSettings = getDefaultFormatCodeSettings("\n"); - - export interface DefinitionInfo extends DocumentSpan { - kind: ScriptElementKind; - name: string; - containerKind: ScriptElementKind; - containerName: string; - /* @internal */ isLocal?: boolean; - } - - export interface DefinitionInfoAndBoundSpan { - definitions?: readonly DefinitionInfo[]; - textSpan: TextSpan; - } - - export interface ReferencedSymbolDefinitionInfo extends DefinitionInfo { - displayParts: SymbolDisplayPart[]; - } - - export interface ReferencedSymbol { - definition: ReferencedSymbolDefinitionInfo; - references: ReferenceEntry[]; - } - - export enum SymbolDisplayPartKind { - aliasName, - className, - enumName, - fieldName, - interfaceName, - keyword, - lineBreak, - numericLiteral, - stringLiteral, - localName, - methodName, - moduleName, - operator, - parameterName, - propertyName, - punctuation, - space, - text, - typeParameterName, - enumMemberName, - functionName, - regularExpressionLiteral, - } - - export interface SymbolDisplayPart { - text: string; - kind: string; - } - - export interface JSDocTagInfo { - name: string; - text?: string; - } - - export interface QuickInfo { - kind: ScriptElementKind; - kindModifiers: string; - textSpan: TextSpan; - displayParts?: SymbolDisplayPart[]; - documentation?: SymbolDisplayPart[]; - tags?: JSDocTagInfo[]; - } - - export type RenameInfo = RenameInfoSuccess | RenameInfoFailure; - export interface RenameInfoSuccess { - canRename: true; - /** - * File or directory to rename. - * If set, `getEditsForFileRename` should be called instead of `findRenameLocations`. - */ - fileToRename?: string; - displayName: string; - fullDisplayName: string; - kind: ScriptElementKind; - kindModifiers: string; - triggerSpan: TextSpan; - } - export interface RenameInfoFailure { - canRename: false; - localizedErrorMessage: string; - } - - export interface RenameInfoOptions { - readonly allowRenameOfImportPath?: boolean; - } - - export interface SignatureHelpParameter { - name: string; - documentation: SymbolDisplayPart[]; - displayParts: SymbolDisplayPart[]; - isOptional: boolean; - } - - export interface SelectionRange { - textSpan: TextSpan; - parent?: SelectionRange; - } - + commands?: CodeActionCommand[]; +} +export interface CodeFixAction extends CodeAction { + /** Short name to identify the fix, for use by telemetry. */ + fixName: string; /** - * Represents a single signature to show in signature help. - * The id is used for subsequent calls into the language service to ask questions about the - * signature help item in the context of any documents that have been updated. i.e. after - * an edit has happened, while signature help is still active, the host can ask important - * questions like 'what parameter is the user currently contained within?'. + * If present, one may call 'getCombinedCodeFix' with this fixId. + * This may be omitted to indicate that the code fix can't be applied in a group. */ - export interface SignatureHelpItem { - isVariadic: boolean; - prefixDisplayParts: SymbolDisplayPart[]; - suffixDisplayParts: SymbolDisplayPart[]; - separatorDisplayParts: SymbolDisplayPart[]; - parameters: SignatureHelpParameter[]; - documentation: SymbolDisplayPart[]; - tags: JSDocTagInfo[]; - } - + fixId?: {}; + fixAllDescription?: string; +} +export interface CombinedCodeActions { + changes: readonly FileTextChanges[]; + commands?: readonly CodeActionCommand[]; +} +// Publicly, this type is just `{}`. Internally it is a union of all the actions we use. +// See `commands?: {}[]` in protocol.ts +export type CodeActionCommand = InstallPackageAction; +export interface InstallPackageAction { + /* @internal */ readonly type: "install package"; + /* @internal */ readonly file: string; + /* @internal */ readonly packageName: string; +} +/** + * A set of one or more available refactoring actions, grouped under a parent refactoring. + */ +export interface ApplicableRefactorInfo { /** - * Represents a set of signature help items, and the preferred item that should be selected. + * The programmatic name of the refactoring */ - export interface SignatureHelpItems { - items: SignatureHelpItem[]; - applicableSpan: TextSpan; - selectedItemIndex: number; - argumentIndex: number; - argumentCount: number; - } - - export interface CompletionInfo { - /** Not true for all global completions. This will be true if the enclosing scope matches a few syntax kinds. See `isSnippetScope`. */ - isGlobalCompletion: boolean; - isMemberCompletion: boolean; - - /** - * true when the current location also allows for a new identifier - */ - isNewIdentifierLocation: boolean; - entries: CompletionEntry[]; - } - - // see comments in protocol.ts - export interface CompletionEntry { - name: string; - kind: ScriptElementKind; - kindModifiers?: string; // see ScriptElementKindModifier, comma separated - sortText: string; - insertText?: string; - /** - * An optional span that indicates the text to be replaced by this completion item. - * If present, this span should be used instead of the default one. - * It will be set if the required span differs from the one generated by the default replacement behavior. - */ - replacementSpan?: TextSpan; - hasAction?: true; - source?: string; - isRecommended?: true; - isFromUncheckedFile?: true; - } - - export interface CompletionEntryDetails { - name: string; - kind: ScriptElementKind; - kindModifiers: string; // see ScriptElementKindModifier, comma separated - displayParts: SymbolDisplayPart[]; - documentation?: SymbolDisplayPart[]; - tags?: JSDocTagInfo[]; - codeActions?: CodeAction[]; - source?: SymbolDisplayPart[]; - } - - export interface OutliningSpan { - /** The span of the document to actually collapse. */ - textSpan: TextSpan; - - /** The span of the document to display when the user hovers over the collapsed span. */ - hintSpan: TextSpan; - - /** The text to display in the editor for the collapsed region. */ - bannerText: string; - - /** - * Whether or not this region should be automatically collapsed when - * the 'Collapse to Definitions' command is invoked. - */ - autoCollapse: boolean; - - /** - * Classification of the contents of the span - */ - kind: OutliningSpanKind; - } - - export const enum OutliningSpanKind { - /** Single or multi-line comments */ - Comment = "comment", - - /** Sections marked by '// #region' and '// #endregion' comments */ - Region = "region", - - /** Declarations and expressions */ - Code = "code", - - /** Contiguous blocks of import declarations */ - Imports = "imports" - } - - export const enum OutputFileType { - JavaScript, - SourceMap, - Declaration - } - - export const enum EndOfLineState { - None, - InMultiLineCommentTrivia, - InSingleQuoteStringLiteral, - InDoubleQuoteStringLiteral, - InTemplateHeadOrNoSubstitutionTemplate, - InTemplateMiddleOrTail, - InTemplateSubstitutionPosition, - } - - export enum TokenClass { - Punctuation, - Keyword, - Operator, - Comment, - Whitespace, - Identifier, - NumberLiteral, - BigIntLiteral, - StringLiteral, - RegExpLiteral, - } - - export interface ClassificationResult { - finalLexState: EndOfLineState; - entries: ClassificationInfo[]; - } - - export interface ClassificationInfo { - length: number; - classification: TokenClass; - } - - export interface Classifier { - /** - * Gives lexical classifications of tokens on a line without any syntactic context. - * For instance, a token consisting of the text 'string' can be either an identifier - * named 'string' or the keyword 'string', however, because this classifier is not aware, - * it relies on certain heuristics to give acceptable results. For classifications where - * speed trumps accuracy, this function is preferable; however, for true accuracy, the - * syntactic classifier is ideal. In fact, in certain editing scenarios, combining the - * lexical, syntactic, and semantic classifiers may issue the best user experience. - * - * @param text The text of a line to classify. - * @param lexState The state of the lexical classifier at the end of the previous line. - * @param syntacticClassifierAbsent Whether the client is *not* using a syntactic classifier. - * If there is no syntactic classifier (syntacticClassifierAbsent=true), - * certain heuristics may be used in its place; however, if there is a - * syntactic classifier (syntacticClassifierAbsent=false), certain - * classifications which may be incorrectly categorized will be given - * back as Identifiers in order to allow the syntactic classifier to - * subsume the classification. - * @deprecated Use getLexicalClassifications instead. - */ - getClassificationsForLine(text: string, lexState: EndOfLineState, syntacticClassifierAbsent: boolean): ClassificationResult; - getEncodedLexicalClassifications(text: string, endOfLineState: EndOfLineState, syntacticClassifierAbsent: boolean): Classifications; - } - - export const enum ScriptElementKind { - unknown = "", - warning = "warning", - - /** predefined type (void) or keyword (class) */ - keyword = "keyword", - - /** top level script node */ - scriptElement = "script", - - /** module foo {} */ - moduleElement = "module", - - /** class X {} */ - classElement = "class", - - /** var x = class X {} */ - localClassElement = "local class", - - /** interface Y {} */ - interfaceElement = "interface", - - /** type T = ... */ - typeElement = "type", - - /** enum E */ - enumElement = "enum", - enumMemberElement = "enum member", - - /** - * Inside module and script only - * const v = .. - */ - variableElement = "var", - - /** Inside function */ - localVariableElement = "local var", - - /** - * Inside module and script only - * function f() { } - */ - functionElement = "function", - - /** Inside function */ - localFunctionElement = "local function", - - /** class X { [public|private]* foo() {} } */ - memberFunctionElement = "method", - - /** class X { [public|private]* [get|set] foo:number; } */ - memberGetAccessorElement = "getter", - memberSetAccessorElement = "setter", - - /** - * class X { [public|private]* foo:number; } - * interface Y { foo:number; } - */ - memberVariableElement = "property", - - /** class X { constructor() { } } */ - constructorImplementationElement = "constructor", - - /** interface Y { ():number; } */ - callSignatureElement = "call", - - /** interface Y { []:number; } */ - indexSignatureElement = "index", - - /** interface Y { new():Y; } */ - constructSignatureElement = "construct", - - /** function foo(*Y*: string) */ - parameterElement = "parameter", - - typeParameterElement = "type parameter", - - primitiveType = "primitive type", - - label = "label", - - alias = "alias", - - constElement = "const", - - letElement = "let", - - directory = "directory", - - externalModuleName = "external module name", - - /** - * - */ - jsxAttribute = "JSX attribute", - - /** String literal */ - string = "string", - } - - export const enum ScriptElementKindModifier { - none = "", - publicMemberModifier = "public", - privateMemberModifier = "private", - protectedMemberModifier = "protected", - exportedModifier = "export", - ambientModifier = "declare", - staticModifier = "static", - abstractModifier = "abstract", - optionalModifier = "optional", - - dtsModifier = ".d.ts", - tsModifier = ".ts", - tsxModifier = ".tsx", - jsModifier = ".js", - jsxModifier = ".jsx", - jsonModifier = ".json", - } - - export const enum ClassificationTypeNames { - comment = "comment", - identifier = "identifier", - keyword = "keyword", - numericLiteral = "number", - bigintLiteral = "bigint", - operator = "operator", - stringLiteral = "string", - whiteSpace = "whitespace", - text = "text", - - punctuation = "punctuation", - - className = "class name", - enumName = "enum name", - interfaceName = "interface name", - moduleName = "module name", - typeParameterName = "type parameter name", - typeAliasName = "type alias name", - parameterName = "parameter name", - docCommentTagName = "doc comment tag name", - jsxOpenTagName = "jsx open tag name", - jsxCloseTagName = "jsx close tag name", - jsxSelfClosingTagName = "jsx self closing tag name", - jsxAttribute = "jsx attribute", - jsxText = "jsx text", - jsxAttributeStringLiteralValue = "jsx attribute string literal value", - } - - export const enum ClassificationType { - comment = 1, - identifier = 2, - keyword = 3, - numericLiteral = 4, - operator = 5, - stringLiteral = 6, - regularExpressionLiteral = 7, - whiteSpace = 8, - text = 9, - punctuation = 10, - className = 11, - enumName = 12, - interfaceName = 13, - moduleName = 14, - typeParameterName = 15, - typeAliasName = 16, - parameterName = 17, - docCommentTagName = 18, - jsxOpenTagName = 19, - jsxCloseTagName = 20, - jsxSelfClosingTagName = 21, - jsxAttribute = 22, - jsxText = 23, - jsxAttributeStringLiteralValue = 24, - bigintLiteral = 25, - } - - /** @internal */ - export interface CodeFixRegistration { - errorCodes: readonly number[]; - getCodeActions(context: CodeFixContext): CodeFixAction[] | undefined; - fixIds?: readonly string[]; - getAllCodeActions?(context: CodeFixAllContext): CombinedCodeActions; - } - - /** @internal */ - export interface CodeFixContextBase extends textChanges.TextChangesContext { - sourceFile: SourceFile; - program: Program; - cancellationToken: CancellationToken; - preferences: UserPreferences; - } - - /** @internal */ - export interface CodeFixAllContext extends CodeFixContextBase { - fixId: {}; - } - - /** @internal */ - export interface CodeFixContext extends CodeFixContextBase { - errorCode: number; - span: TextSpan; - } - - /** @internal */ - export interface Refactor { - /** Compute the associated code actions */ - getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined; - - /** Compute (quickly) which actions are available here */ - getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[]; - } - - /** @internal */ - export interface RefactorContext extends textChanges.TextChangesContext { - file: SourceFile; - startPosition: number; - endPosition?: number; - program: Program; - cancellationToken?: CancellationToken; - preferences: UserPreferences; - } + name: string; + /** + * A description of this refactoring category to show to the user. + * If the refactoring gets inlined (see below), this text will not be visible. + */ + description: string; + /** + * Inlineable refactorings can have their actions hoisted out to the top level + * of a context menu. Non-inlineanable refactorings should always be shown inside + * their parent grouping. + * + * If not specified, this value is assumed to be 'true' + */ + inlineable?: boolean; + actions: RefactorActionInfo[]; +} +/** + * Represents a single refactoring action - for example, the "Extract Method..." refactor might + * offer several actions, each corresponding to a surround class or closure to extract into. + */ +export interface RefactorActionInfo { + /** + * The programmatic name of the refactoring action + */ + name: string; + /** + * A description of this refactoring action to show to the user. + * If the parent refactoring is inlined away, this will be the only text shown, + * so this description should make sense by itself if the parent is inlineable=true + */ + description: string; +} +/** + * A set of edits to make in response to a refactor action, plus an optional + * location where renaming should be invoked from + */ +export interface RefactorEditInfo { + edits: FileTextChanges[]; + renameFilename?: string; + renameLocation?: number; + commands?: CodeActionCommand[]; +} +export interface TextInsertion { + newText: string; + /** The position in newText the caret should point to after the insertion. */ + caretOffset: number; +} +export interface DocumentSpan { + textSpan: TextSpan; + fileName: string; + /** + * If the span represents a location that was remapped (e.g. via a .d.ts.map file), + * then the original filename and span will be specified here + */ + originalTextSpan?: TextSpan; + originalFileName?: string; + /** + * If DocumentSpan.textSpan is the span for name of the declaration, + * then this is the span for relevant declaration + */ + contextSpan?: TextSpan; + originalContextSpan?: TextSpan; +} +export interface RenameLocation extends DocumentSpan { + readonly prefixText?: string; + readonly suffixText?: string; +} +export interface ReferenceEntry extends DocumentSpan { + isWriteAccess: boolean; + isDefinition: boolean; + isInString?: true; +} +export interface ImplementationLocation extends DocumentSpan { + kind: ScriptElementKind; + displayParts: SymbolDisplayPart[]; +} +export const enum HighlightSpanKind { + none = "none", + definition = "definition", + reference = "reference", + writtenReference = "writtenReference" +} +export interface HighlightSpan { + fileName?: string; + isInString?: true; + textSpan: TextSpan; + contextSpan?: TextSpan; + kind: HighlightSpanKind; +} +export interface NavigateToItem { + name: string; + kind: ScriptElementKind; + kindModifiers: string; + matchKind: "exact" | "prefix" | "substring" | "camelCase"; + isCaseSensitive: boolean; + fileName: string; + textSpan: TextSpan; + containerName: string; + containerKind: ScriptElementKind; +} +export enum IndentStyle { + None = 0, + Block = 1, + Smart = 2 +} +export enum SemicolonPreference { + Ignore = "ignore", + Insert = "insert", + Remove = "remove" +} +/* @deprecated - consider using EditorSettings instead */ +export interface EditorOptions { + BaseIndentSize?: number; + IndentSize: number; + TabSize: number; + NewLineCharacter: string; + ConvertTabsToSpaces: boolean; + IndentStyle: IndentStyle; +} +// TODO: GH#18217 These are frequently asserted as defined +export interface EditorSettings { + baseIndentSize?: number; + indentSize?: number; + tabSize?: number; + newLineCharacter?: string; + convertTabsToSpaces?: boolean; + indentStyle?: IndentStyle; +} +/* @deprecated - consider using FormatCodeSettings instead */ +export interface FormatCodeOptions extends EditorOptions { + InsertSpaceAfterCommaDelimiter: boolean; + InsertSpaceAfterSemicolonInForStatements: boolean; + InsertSpaceBeforeAndAfterBinaryOperators: boolean; + InsertSpaceAfterConstructor?: boolean; + InsertSpaceAfterKeywordsInControlFlowStatements: boolean; + InsertSpaceAfterFunctionKeywordForAnonymousFunctions: boolean; + InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: boolean; + InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: boolean; + InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces?: boolean; + InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: boolean; + InsertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces?: boolean; + InsertSpaceAfterTypeAssertion?: boolean; + InsertSpaceBeforeFunctionParenthesis?: boolean; + PlaceOpenBraceOnNewLineForFunctions: boolean; + PlaceOpenBraceOnNewLineForControlBlocks: boolean; + insertSpaceBeforeTypeAnnotation?: boolean; +} +export interface FormatCodeSettings extends EditorSettings { + readonly insertSpaceAfterCommaDelimiter?: boolean; + readonly insertSpaceAfterSemicolonInForStatements?: boolean; + readonly insertSpaceBeforeAndAfterBinaryOperators?: boolean; + readonly insertSpaceAfterConstructor?: boolean; + readonly insertSpaceAfterKeywordsInControlFlowStatements?: boolean; + readonly insertSpaceAfterFunctionKeywordForAnonymousFunctions?: boolean; + readonly insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis?: boolean; + readonly insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets?: boolean; + readonly insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces?: boolean; + readonly insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces?: boolean; + readonly insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces?: boolean; + readonly insertSpaceAfterTypeAssertion?: boolean; + readonly insertSpaceBeforeFunctionParenthesis?: boolean; + readonly placeOpenBraceOnNewLineForFunctions?: boolean; + readonly placeOpenBraceOnNewLineForControlBlocks?: boolean; + readonly insertSpaceBeforeTypeAnnotation?: boolean; + readonly indentMultiLineObjectLiteralBeginningOnBlankLine?: boolean; + readonly semicolons?: SemicolonPreference; +} +export function getDefaultFormatCodeSettings(newLineCharacter?: string): FormatCodeSettings { + return { + indentSize: 4, + tabSize: 4, + newLineCharacter: newLineCharacter || "\n", + convertTabsToSpaces: true, + indentStyle: IndentStyle.Smart, + insertSpaceAfterConstructor: false, + insertSpaceAfterCommaDelimiter: true, + insertSpaceAfterSemicolonInForStatements: true, + insertSpaceBeforeAndAfterBinaryOperators: true, + insertSpaceAfterKeywordsInControlFlowStatements: true, + insertSpaceAfterFunctionKeywordForAnonymousFunctions: false, + insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false, + insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false, + insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: true, + insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: false, + insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: false, + insertSpaceBeforeFunctionParenthesis: false, + placeOpenBraceOnNewLineForFunctions: false, + placeOpenBraceOnNewLineForControlBlocks: false, + semicolons: SemicolonPreference.Ignore, + }; +} +/* @internal */ +export const testFormatSettings = getDefaultFormatCodeSettings("\n"); +export interface DefinitionInfo extends DocumentSpan { + kind: ScriptElementKind; + name: string; + containerKind: ScriptElementKind; + containerName: string; + /* @internal */ isLocal?: boolean; +} +export interface DefinitionInfoAndBoundSpan { + definitions?: readonly DefinitionInfo[]; + textSpan: TextSpan; +} +export interface ReferencedSymbolDefinitionInfo extends DefinitionInfo { + displayParts: SymbolDisplayPart[]; +} +export interface ReferencedSymbol { + definition: ReferencedSymbolDefinitionInfo; + references: ReferenceEntry[]; +} +export enum SymbolDisplayPartKind { + aliasName, + className, + enumName, + fieldName, + interfaceName, + keyword, + lineBreak, + numericLiteral, + stringLiteral, + localName, + methodName, + moduleName, + operator, + parameterName, + propertyName, + punctuation, + space, + text, + typeParameterName, + enumMemberName, + functionName, + regularExpressionLiteral +} +export interface SymbolDisplayPart { + text: string; + kind: string; +} +export interface JSDocTagInfo { + name: string; + text?: string; +} +export interface QuickInfo { + kind: ScriptElementKind; + kindModifiers: string; + textSpan: TextSpan; + displayParts?: SymbolDisplayPart[]; + documentation?: SymbolDisplayPart[]; + tags?: JSDocTagInfo[]; +} +export type RenameInfo = RenameInfoSuccess | RenameInfoFailure; +export interface RenameInfoSuccess { + canRename: true; + /** + * File or directory to rename. + * If set, `getEditsForFileRename` should be called instead of `findRenameLocations`. + */ + fileToRename?: string; + displayName: string; + fullDisplayName: string; + kind: ScriptElementKind; + kindModifiers: string; + triggerSpan: TextSpan; +} +export interface RenameInfoFailure { + canRename: false; + localizedErrorMessage: string; +} +export interface RenameInfoOptions { + readonly allowRenameOfImportPath?: boolean; +} +export interface SignatureHelpParameter { + name: string; + documentation: SymbolDisplayPart[]; + displayParts: SymbolDisplayPart[]; + isOptional: boolean; +} +export interface SelectionRange { + textSpan: TextSpan; + parent?: SelectionRange; +} +/** + * Represents a single signature to show in signature help. + * The id is used for subsequent calls into the language service to ask questions about the + * signature help item in the context of any documents that have been updated. i.e. after + * an edit has happened, while signature help is still active, the host can ask important + * questions like 'what parameter is the user currently contained within?'. + */ +export interface SignatureHelpItem { + isVariadic: boolean; + prefixDisplayParts: SymbolDisplayPart[]; + suffixDisplayParts: SymbolDisplayPart[]; + separatorDisplayParts: SymbolDisplayPart[]; + parameters: SignatureHelpParameter[]; + documentation: SymbolDisplayPart[]; + tags: JSDocTagInfo[]; +} +/** + * Represents a set of signature help items, and the preferred item that should be selected. + */ +export interface SignatureHelpItems { + items: SignatureHelpItem[]; + applicableSpan: TextSpan; + selectedItemIndex: number; + argumentIndex: number; + argumentCount: number; +} +export interface CompletionInfo { + /** Not true for all global completions. This will be true if the enclosing scope matches a few syntax kinds. See `isSnippetScope`. */ + isGlobalCompletion: boolean; + isMemberCompletion: boolean; + /** + * true when the current location also allows for a new identifier + */ + isNewIdentifierLocation: boolean; + entries: CompletionEntry[]; +} +// see comments in protocol.ts +export interface CompletionEntry { + name: string; + kind: ScriptElementKind; + kindModifiers?: string; // see ScriptElementKindModifier, comma separated + sortText: string; + insertText?: string; + /** + * An optional span that indicates the text to be replaced by this completion item. + * If present, this span should be used instead of the default one. + * It will be set if the required span differs from the one generated by the default replacement behavior. + */ + replacementSpan?: TextSpan; + hasAction?: true; + source?: string; + isRecommended?: true; + isFromUncheckedFile?: true; +} +export interface CompletionEntryDetails { + name: string; + kind: ScriptElementKind; + kindModifiers: string; // see ScriptElementKindModifier, comma separated + displayParts: SymbolDisplayPart[]; + documentation?: SymbolDisplayPart[]; + tags?: JSDocTagInfo[]; + codeActions?: CodeAction[]; + source?: SymbolDisplayPart[]; +} +export interface OutliningSpan { + /** The span of the document to actually collapse. */ + textSpan: TextSpan; + /** The span of the document to display when the user hovers over the collapsed span. */ + hintSpan: TextSpan; + /** The text to display in the editor for the collapsed region. */ + bannerText: string; + /** + * Whether or not this region should be automatically collapsed when + * the 'Collapse to Definitions' command is invoked. + */ + autoCollapse: boolean; + /** + * Classification of the contents of the span + */ + kind: OutliningSpanKind; +} +export const enum OutliningSpanKind { + /** Single or multi-line comments */ + Comment = "comment", + /** Sections marked by '// #region' and '// #endregion' comments */ + Region = "region", + /** Declarations and expressions */ + Code = "code", + /** Contiguous blocks of import declarations */ + Imports = "imports" +} +export const enum OutputFileType { + JavaScript, + SourceMap, + Declaration +} +export const enum EndOfLineState { + None, + InMultiLineCommentTrivia, + InSingleQuoteStringLiteral, + InDoubleQuoteStringLiteral, + InTemplateHeadOrNoSubstitutionTemplate, + InTemplateMiddleOrTail, + InTemplateSubstitutionPosition +} +export enum TokenClass { + Punctuation, + Keyword, + Operator, + Comment, + Whitespace, + Identifier, + NumberLiteral, + BigIntLiteral, + StringLiteral, + RegExpLiteral +} +export interface ClassificationResult { + finalLexState: EndOfLineState; + entries: ClassificationInfo[]; +} +export interface ClassificationInfo { + length: number; + classification: TokenClass; +} +export interface Classifier { + /** + * Gives lexical classifications of tokens on a line without any syntactic context. + * For instance, a token consisting of the text 'string' can be either an identifier + * named 'string' or the keyword 'string', however, because this classifier is not aware, + * it relies on certain heuristics to give acceptable results. For classifications where + * speed trumps accuracy, this function is preferable; however, for true accuracy, the + * syntactic classifier is ideal. In fact, in certain editing scenarios, combining the + * lexical, syntactic, and semantic classifiers may issue the best user experience. + * + * @param text The text of a line to classify. + * @param lexState The state of the lexical classifier at the end of the previous line. + * @param syntacticClassifierAbsent Whether the client is *not* using a syntactic classifier. + * If there is no syntactic classifier (syntacticClassifierAbsent=true), + * certain heuristics may be used in its place; however, if there is a + * syntactic classifier (syntacticClassifierAbsent=false), certain + * classifications which may be incorrectly categorized will be given + * back as Identifiers in order to allow the syntactic classifier to + * subsume the classification. + * @deprecated Use getLexicalClassifications instead. + */ + getClassificationsForLine(text: string, lexState: EndOfLineState, syntacticClassifierAbsent: boolean): ClassificationResult; + getEncodedLexicalClassifications(text: string, endOfLineState: EndOfLineState, syntacticClassifierAbsent: boolean): Classifications; +} +export const enum ScriptElementKind { + unknown = "", + warning = "warning", + /** predefined type (void) or keyword (class) */ + keyword = "keyword", + /** top level script node */ + scriptElement = "script", + /** module foo {} */ + moduleElement = "module", + /** class X {} */ + classElement = "class", + /** var x = class X {} */ + localClassElement = "local class", + /** interface Y {} */ + interfaceElement = "interface", + /** type T = ... */ + typeElement = "type", + /** enum E */ + enumElement = "enum", + enumMemberElement = "enum member", + /** + * Inside module and script only + * const v = .. + */ + variableElement = "var", + /** Inside function */ + localVariableElement = "local var", + /** + * Inside module and script only + * function f() { } + */ + functionElement = "function", + /** Inside function */ + localFunctionElement = "local function", + /** class X { [public|private]* foo() {} } */ + memberFunctionElement = "method", + /** class X { [public|private]* [get|set] foo:number; } */ + memberGetAccessorElement = "getter", + memberSetAccessorElement = "setter", + /** + * class X { [public|private]* foo:number; } + * interface Y { foo:number; } + */ + memberVariableElement = "property", + /** class X { constructor() { } } */ + constructorImplementationElement = "constructor", + /** interface Y { ():number; } */ + callSignatureElement = "call", + /** interface Y { []:number; } */ + indexSignatureElement = "index", + /** interface Y { new():Y; } */ + constructSignatureElement = "construct", + /** function foo(*Y*: string) */ + parameterElement = "parameter", + typeParameterElement = "type parameter", + primitiveType = "primitive type", + label = "label", + alias = "alias", + constElement = "const", + letElement = "let", + directory = "directory", + externalModuleName = "external module name", + /** + * + */ + jsxAttribute = "JSX attribute", + /** String literal */ + string = "string" +} +export const enum ScriptElementKindModifier { + none = "", + publicMemberModifier = "public", + privateMemberModifier = "private", + protectedMemberModifier = "protected", + exportedModifier = "export", + ambientModifier = "declare", + staticModifier = "static", + abstractModifier = "abstract", + optionalModifier = "optional", + dtsModifier = ".d.ts", + tsModifier = ".ts", + tsxModifier = ".tsx", + jsModifier = ".js", + jsxModifier = ".jsx", + jsonModifier = ".json" +} +export const enum ClassificationTypeNames { + comment = "comment", + identifier = "identifier", + keyword = "keyword", + numericLiteral = "number", + bigintLiteral = "bigint", + operator = "operator", + stringLiteral = "string", + whiteSpace = "whitespace", + text = "text", + punctuation = "punctuation", + className = "class name", + enumName = "enum name", + interfaceName = "interface name", + moduleName = "module name", + typeParameterName = "type parameter name", + typeAliasName = "type alias name", + parameterName = "parameter name", + docCommentTagName = "doc comment tag name", + jsxOpenTagName = "jsx open tag name", + jsxCloseTagName = "jsx close tag name", + jsxSelfClosingTagName = "jsx self closing tag name", + jsxAttribute = "jsx attribute", + jsxText = "jsx text", + jsxAttributeStringLiteralValue = "jsx attribute string literal value" +} +export const enum ClassificationType { + comment = 1, + identifier = 2, + keyword = 3, + numericLiteral = 4, + operator = 5, + stringLiteral = 6, + regularExpressionLiteral = 7, + whiteSpace = 8, + text = 9, + punctuation = 10, + className = 11, + enumName = 12, + interfaceName = 13, + moduleName = 14, + typeParameterName = 15, + typeAliasName = 16, + parameterName = 17, + docCommentTagName = 18, + jsxOpenTagName = 19, + jsxCloseTagName = 20, + jsxSelfClosingTagName = 21, + jsxAttribute = 22, + jsxText = 23, + jsxAttributeStringLiteralValue = 24, + bigintLiteral = 25 +} +/** @internal */ +export interface CodeFixRegistration { + errorCodes: readonly number[]; + getCodeActions(context: CodeFixContext): CodeFixAction[] | undefined; + fixIds?: readonly string[]; + getAllCodeActions?(context: CodeFixAllContext): CombinedCodeActions; +} +/** @internal */ +export interface CodeFixContextBase extends TextChangesContext { + sourceFile: SourceFile; + program: Program; + cancellationToken: CancellationToken; + preferences: UserPreferences; +} +/** @internal */ +export interface CodeFixAllContext extends CodeFixContextBase { + fixId: {}; +} +/** @internal */ +export interface CodeFixContext extends CodeFixContextBase { + errorCode: number; + span: TextSpan; +} +/** @internal */ +export interface Refactor { + /** Compute the associated code actions */ + getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined; + /** Compute (quickly) which actions are available here */ + getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[]; +} +/** @internal */ +export interface RefactorContext extends TextChangesContext { + file: SourceFile; + startPosition: number; + endPosition?: number; + program: Program; + cancellationToken?: CancellationToken; + preferences: UserPreferences; } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 2e1b8a5795d20..841852e03cfe8 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -1,2758 +1,2702 @@ -/* @internal */ // Don't expose that we use this -// Based on lib.es6.d.ts -interface PromiseConstructor { - new (executor: (resolve: (value?: T | PromiseLike) => void, reject: (reason?: any) => void) => void): Promise; - reject(reason: any): Promise; - all(values: (T | PromiseLike)[]): Promise; -} -/* @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(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; +import { Scanner, createScanner, ScriptTarget, Node, SyntaxKind, isInJSFile, getJSDocEnumTag, JSDocTypedefTag, isAmbientModule, ModuleDeclaration, getModuleInstanceState, ModuleInstanceState, Identifier, isDeclarationName, isTypeParameterDeclaration, Debug, isJSDocTemplateTag, isLiteralTypeNode, isQualifiedName, isInternalModuleImportEqualsDeclaration, QualifiedName, PropertyAccessExpression, HeritageClause, isRightSideOfQualifiedNameOrPropertyAccess, isExpressionNode, ImportTypeNode, isExpressionWithTypeArgumentsInClassExtendsClause, ExpressionWithTypeArguments, isCallExpression, isNewExpression, isCallOrNewExpression, isTaggedTemplateExpression, isDecorator, isJsxOpeningLikeElement, CallExpression, NewExpression, Decorator, TaggedTemplateExpression, JsxOpeningLikeElement, Expression, skipOuterExpressions, LabeledStatement, isPropertyAccessExpression, BreakOrContinueStatement, isIdentifier, tryCast, isBreakOrContinueStatement, isLabeledStatement, isJSDocTag, isElementAccessExpression, isModuleDeclaration, isFunctionLike, StringLiteral, NumericLiteral, NoSubstitutionTemplateLiteral, getNameOfDeclaration, Declaration, ElementAccessExpression, isExternalModuleImportEqualsDeclaration, getExternalModuleImportEqualsDeclarationExpression, isJSDocTypeAlias, ScriptElementKind, isExternalModule, SourceFile, VariableDeclaration, getRootDeclaration, PropertyAssignment, hasModifier, ModifierFlags, getAssignmentDeclarationKind, BinaryExpression, AssignmentDeclarationKind, isFunctionExpression, assertType, isImportClause, ExportAssignment, isVarConst, isLet, identifierIsThisKeyword, SourceFileLike, getLineStarts, TextRange, nodeIsMissing, CatchClause, SignatureDeclaration, FunctionLikeDeclaration, IfStatement, ExpressionStatement, IndexSignatureDeclaration, IterationStatement, DoStatement, TypeQueryNode, TypeOfExpression, DeleteExpression, VoidExpression, YieldExpression, SpreadElement, lastOrUndefined, TemplateExpression, nodeIsPresent, TemplateSpan, ExportDeclaration, ImportDeclaration, PrefixUnaryExpression, ConditionalExpression, last, indexOfNode, find, SyntaxList, isSyntaxList, contains, ClassDeclaration, ClassExpression, isNamedDeclaration, isClassDeclaration, isClassExpression, FunctionDeclaration, FunctionExpression, isFunctionDeclaration, isNamedImports, singleOrUndefined, isNamespaceImport, isNamedExports, isNamespaceExport, isModifier, isInterfaceDeclaration, isEnumDeclaration, isTypeAliasDeclaration, isImportEqualsDeclaration, isGetAccessorDeclaration, isSetAccessorDeclaration, isVariableDeclarationList, isExportDeclaration, isImportSpecifier, isExportSpecifier, isImportDeclaration, isExportAssignment, isExternalModuleReference, isHeritageClause, isTypeReferenceNode, isConditionalTypeNode, isInferTypeNode, isMappedTypeNode, isTypeOperatorNode, isArrayTypeNode, isVoidExpression, isTypeOfExpression, isAwaitExpression, isYieldExpression, isDeleteExpression, isBinaryExpression, isAsExpression, isForInStatement, isForOfStatement, isPropertyNameLiteral, isKeyword, isPrivateIdentifier, isToken, firstDefined, isJSDocCommentContainingNode, isStringTextContainingNode, LiteralExpression, isJsxText, isTemplateLiteralKind, isJsxExpression, isJsxElement, Type, TypeChecker, isPartOfTypeNode, Signature, isOptionalChain, isTypeNode, CommentRange, findAncestor, isJSDoc, EndOfFileToken, isDeclaration, getCombinedModifierFlags, ScriptElementKindModifier, NodeFlags, NodeArray, TemplateLiteralToken, CompilerOptions, clone, setConfigFileInOptions, ForOfStatement, TextSpan, createTextSpanFromBounds, createRange, TextChange, createTextSpan, Token, Symbol, SymbolFlags, CharacterCodes, getNodeId, IScriptSnapshot, PropertyName, isStringOrNumericLiteralLike, idText, getTextOfIdentifierOrLiteral, Program, GetCanonicalFileName, createGetCanonicalFileName, ImportSpecifier, createImportDeclaration, createImportClause, createNamedImports, createLiteral, isStringDoubleQuoted, UserPreferences, isStringLiteral, unescapeLeadingUnderscores, __String, InternalSymbolName, BindingElement, isBindingElement, isObjectBindingPattern, createMap, addToSeen, getSymbolId, getAllSuperTypeNodes, isSourceFile, textSpanContainsPosition, textSpanEnd, Modifier, Statement, findLast, isAnyImportSyntax, ImportClause, cast, DocumentSpan, DisplayPartsSymbolWriter, defaultMaximumTruncationLength, SymbolDisplayPart, SymbolDisplayPartKind, isWhiteSpaceLike, notImplemented, noop, getIndentString, tokenToString, stringToToken, LanguageServiceHost, LanguageServiceShimHost, FormatCodeSettings, TypeFormatFlags, SymbolFormatFlags, isImportOrExportSpecifier, ScriptKind, some, ensureScriptKind, skipAlias, TransientSymbol, isWhiteSpaceSingleLine, createBindingElement, createIdentifier, visitEachChild, nullTransformationContext, getSynthesizedClone, isNumericLiteral, setTextRange, createNodeArray, EmitFlags, getLastChild, addEmitFlags, isFileLevelUniqueName, FileTextChanges, CommentKind, forEachLeadingCommentRange, addSyntheticLeadingComment, forEachTrailingCommentRange, addSyntheticTrailingComment, startsWith, CaseClause, stripQuotes, EqualityOperator, StringLiteralLike, TypeNode, SymbolAccessibility, or, isModuleBlock, isFunctionBlock, forEachChild, emptyArray, directoryProbablyExists, forEachAncestorDirectory, combinePaths, findConfigFile, PackageJsonInfo, getDirectoryPath, PackageJsonDependencyGroup, JsTyping, getPathComponents, Diagnostic, DiagnosticWithLocation, binarySearchKey, identity, compareTextSpans, compareValues, textSpanContainsTextSpan, RefactorContext, isArray, map, first } from "./ts"; +import { getRangeOfEnclosingComment } from "./ts.formatting"; +import { ChangeTracker } from "./ts.textChanges"; +import { moduleSymbolToValidIdentifier } from "./ts.codefix"; +import * as ts from "./ts"; +/* @internal */ +declare global { + /* @internal */ // Don't expose that we use this + // Based on lib.es6.d.ts + interface PromiseConstructor { + new (executor: (resolve: (value?: T | PromiseLike) => void, reject: (reason?: any) => void) => void): Promise; + reject(reason: any): Promise; + all(values: (T | PromiseLike)[]): Promise; } - - export function getMeaningFromLocation(node: Node): SemanticMeaning { - node = getAdjustedReferenceLocation(node); - if (node.kind === SyntaxKind.SourceFile) { +} +/* @internal */ +declare global { + /* @internal */ + var Promise: PromiseConstructor; // eslint-disable-line no-var +} +/* @internal */ +// These utilities are common to multiple language service features. +//#region +export const scanner: Scanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ true); +/* @internal */ +export const enum SemanticMeaning { + None = 0x0, + Value = 0x1, + Type = 0x2, + Namespace = 0x4, + All = Value | Type | Namespace +} +/* @internal */ +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; - } - 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 + case SyntaxKind.TypeParameter: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.TypeLiteral: 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; + 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((node))) { + return SemanticMeaning.Namespace | SemanticMeaning.Value; } - - isLastClause = (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; + else if (getModuleInstanceState((node as ModuleDeclaration)) === ModuleInstanceState.Instantiated) { + return SemanticMeaning.Namespace | SemanticMeaning.Value; } - - isLastClause = (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 && (root.parent.parent).token === SyntaxKind.ImplementsKeyword) || - (decl.kind === SyntaxKind.InterfaceDeclaration && (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(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); + 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; } - - export function isCallOrNewExpressionTarget(node: Node, includeElementAccess = false, skipPastOuterExpressions = false): boolean { - return isCalleeWorker(node, isCallOrNewExpression, selectExpressionOfCallOrNewExpressionOrDecorator, includeElementAccess, skipPastOuterExpressions); + return SemanticMeaning.All; +} +/* @internal */ +export function getMeaningFromLocation(node: Node): SemanticMeaning { + node = getAdjustedReferenceLocation(node); + if (node.kind === SyntaxKind.SourceFile) { + return SemanticMeaning.Value; } - - export function isTaggedTemplateTag(node: Node, includeElementAccess = false, skipPastOuterExpressions = false): boolean { - return isCalleeWorker(node, isTaggedTemplateExpression, selectTagOfTaggedTemplateExpression, includeElementAccess, skipPastOuterExpressions); + else if (node.parent.kind === SyntaxKind.ExportAssignment || node.parent.kind === SyntaxKind.ExternalModuleReference) { + return SemanticMeaning.All; } - - export function isDecoratorTarget(node: Node, includeElementAccess = false, skipPastOuterExpressions = false): boolean { - return isCalleeWorker(node, isDecorator, selectExpressionOfCallOrNewExpressionOrDecorator, includeElementAccess, skipPastOuterExpressions); + else if (isInRightSideOfInternalImportEqualsDeclaration(node)) { + return getMeaningFromRightHandSideOfImportEquals((node as Identifier)); } - - export function isJsxOpeningLikeElementTagName(node: Node, includeElementAccess = false, skipPastOuterExpressions = false): boolean { - return isCalleeWorker(node, isJsxOpeningLikeElement, selectTagNameOfJsxOpeningLikeElement, includeElementAccess, skipPastOuterExpressions); + else if (isDeclarationName(node)) { + return getMeaningFromDeclaration(node.parent); } - - function selectExpressionOfCallOrNewExpressionOrDecorator(node: CallExpression | NewExpression | Decorator) { - return node.expression; + else if (isTypeReference(node)) { + return SemanticMeaning.Type; } - - function selectTagOfTaggedTemplateExpression(node: TaggedTemplateExpression) { - return node.tag; + else if (isNamespaceReference(node)) { + return SemanticMeaning.Namespace; } - - function selectTagNameOfJsxOpeningLikeElement(node: JsxOpeningLikeElement) { - return node.tagName; + else if (isTypeParameterDeclaration(node.parent)) { + Debug.assert(isJSDocTemplateTag(node.parent.parent)); // Else would be handled by isDeclarationName + return SemanticMeaning.Type; } - - function isCalleeWorker(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; + 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; } - - export function climbPastPropertyAccess(node: Node) { - return isRightSideOfPropertyAccess(node) ? node.parent : node; + else { + return SemanticMeaning.Value; } - - export function climbPastPropertyOrElementAccess(node: Node) { - return isRightSideOfPropertyAccess(node) || isArgumentExpressionOfElementAccess(node) ? node.parent : node; +} +/* @internal */ +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; +} +/* @internal */ +export function isInRightSideOfInternalImportEqualsDeclaration(node: Node) { + while (node.parent.kind === SyntaxKind.QualifiedName) { + node = node.parent; } - - export function getTargetLabel(referenceNode: Node, labelName: string): Identifier | undefined { - while (referenceNode) { - if (referenceNode.kind === SyntaxKind.LabeledStatement && (referenceNode).label.escapedText === labelName) { - return (referenceNode).label; - } - referenceNode = referenceNode.parent; + return isInternalModuleImportEqualsDeclaration(node.parent) && node.parent.moduleReference === node; +} +/* @internal */ +function isNamespaceReference(node: Node): boolean { + return isQualifiedNameNamespaceReference(node) || isPropertyAccessNamespaceReference(node); +} +/* @internal */ +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; } - return undefined; + isLastClause = (root).right === node; } - - export function hasPropertyAccessExpressionWithName(node: CallExpression, funcName: string): boolean { - if (!isPropertyAccessExpression(node.expression)) { - return false; + return root.parent.kind === SyntaxKind.TypeReference && !isLastClause; +} +/* @internal */ +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; } - - return node.expression.name.text === funcName; + isLastClause = (root).name === node; } - - export function isJumpStatementTarget(node: Node): node is Identifier & { parent: BreakOrContinueStatement } { - return isIdentifier(node) && tryCast(node.parent, isBreakOrContinueStatement)?.label === 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 && (root.parent.parent).token === SyntaxKind.ImplementsKeyword) || + (decl.kind === SyntaxKind.InterfaceDeclaration && (root.parent.parent).token === SyntaxKind.ExtendsKeyword); } - - 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; + return false; +} +/* @internal */ +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; } - - export function isRightSideOfQualifiedName(node: Node) { - return tryCast(node.parent, isQualifiedName)?.right === node; + switch (node.parent.kind) { + case SyntaxKind.TypeReference: + return true; + case SyntaxKind.ImportType: + return !(node.parent as ImportTypeNode).isTypeOf; + case SyntaxKind.ExpressionWithTypeArguments: + return !isExpressionWithTypeArgumentsInClassExtendsClause((node.parent)); } - - export function isRightSideOfPropertyAccess(node: Node) { - return tryCast(node.parent, isPropertyAccessExpression)?.name === node; + return false; +} +/* @internal */ +export function isCallExpressionTarget(node: Node, includeElementAccess = false, skipPastOuterExpressions = false): boolean { + return isCalleeWorker(node, isCallExpression, selectExpressionOfCallOrNewExpressionOrDecorator, includeElementAccess, skipPastOuterExpressions); +} +/* @internal */ +export function isNewExpressionTarget(node: Node, includeElementAccess = false, skipPastOuterExpressions = false): boolean { + return isCalleeWorker(node, isNewExpression, selectExpressionOfCallOrNewExpressionOrDecorator, includeElementAccess, skipPastOuterExpressions); +} +/* @internal */ +export function isCallOrNewExpressionTarget(node: Node, includeElementAccess = false, skipPastOuterExpressions = false): boolean { + return isCalleeWorker(node, isCallOrNewExpression, selectExpressionOfCallOrNewExpressionOrDecorator, includeElementAccess, skipPastOuterExpressions); +} +/* @internal */ +export function isTaggedTemplateTag(node: Node, includeElementAccess = false, skipPastOuterExpressions = false): boolean { + return isCalleeWorker(node, isTaggedTemplateExpression, selectTagOfTaggedTemplateExpression, includeElementAccess, skipPastOuterExpressions); +} +/* @internal */ +export function isDecoratorTarget(node: Node, includeElementAccess = false, skipPastOuterExpressions = false): boolean { + return isCalleeWorker(node, isDecorator, selectExpressionOfCallOrNewExpressionOrDecorator, includeElementAccess, skipPastOuterExpressions); +} +/* @internal */ +export function isJsxOpeningLikeElementTagName(node: Node, includeElementAccess = false, skipPastOuterExpressions = false): boolean { + return isCalleeWorker(node, isJsxOpeningLikeElement, selectTagNameOfJsxOpeningLikeElement, includeElementAccess, skipPastOuterExpressions); +} +/* @internal */ +function selectExpressionOfCallOrNewExpressionOrDecorator(node: CallExpression | NewExpression | Decorator) { + return node.expression; +} +/* @internal */ +function selectTagOfTaggedTemplateExpression(node: TaggedTemplateExpression) { + return node.tag; +} +/* @internal */ +function selectTagNameOfJsxOpeningLikeElement(node: JsxOpeningLikeElement) { + return node.tagName; +} +/* @internal */ +function isCalleeWorker(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); } - - export function isArgumentExpressionOfElementAccess(node: Node) { - return tryCast(node.parent, isElementAccessExpression)?.argumentExpression === node; + return !!target && !!target.parent && pred(target.parent) && calleeSelector(target.parent) === target; +} +/* @internal */ +export function climbPastPropertyAccess(node: Node) { + return isRightSideOfPropertyAccess(node) ? node.parent : node; +} +/* @internal */ +export function climbPastPropertyOrElementAccess(node: Node) { + return isRightSideOfPropertyAccess(node) || isArgumentExpressionOfElementAccess(node) ? node.parent : node; +} +/* @internal */ +export function getTargetLabel(referenceNode: Node, labelName: string): Identifier | undefined { + while (referenceNode) { + if (referenceNode.kind === SyntaxKind.LabeledStatement && (referenceNode).label.escapedText === labelName) { + return (referenceNode).label; + } + referenceNode = referenceNode.parent; } - - export function isNameOfModuleDeclaration(node: Node) { - return tryCast(node.parent, isModuleDeclaration)?.name === node; + return undefined; +} +/* @internal */ +export function hasPropertyAccessExpressionWithName(node: CallExpression, funcName: string): boolean { + if (!isPropertyAccessExpression(node.expression)) { + return false; } - - export function isNameOfFunctionDeclaration(node: Node): boolean { - return isIdentifier(node) && tryCast(node.parent, isFunctionLike)?.name === node; + return node.expression.name.text === funcName; +} +/* @internal */ +export function isJumpStatementTarget(node: Node): node is Identifier & { + parent: BreakOrContinueStatement; +} { + return isIdentifier(node) && tryCast(node.parent, isBreakOrContinueStatement)?.label === node; +} +/* @internal */ +export function isLabelOfLabeledStatement(node: Node): node is Identifier { + return isIdentifier(node) && tryCast(node.parent, isLabeledStatement)?.label === node; +} +/* @internal */ +export function isLabelName(node: Node): boolean { + return isLabelOfLabeledStatement(node) || isJumpStatementTarget(node); +} +/* @internal */ +export function isTagName(node: Node): boolean { + return tryCast(node.parent, isJSDocTag)?.tagName === node; +} +/* @internal */ +export function isRightSideOfQualifiedName(node: Node) { + return tryCast(node.parent, isQualifiedName)?.right === node; +} +/* @internal */ +export function isRightSideOfPropertyAccess(node: Node) { + return tryCast(node.parent, isPropertyAccessExpression)?.name === node; +} +/* @internal */ +export function isArgumentExpressionOfElementAccess(node: Node) { + return tryCast(node.parent, isElementAccessExpression)?.argumentExpression === node; +} +/* @internal */ +export function isNameOfModuleDeclaration(node: Node) { + return tryCast(node.parent, isModuleDeclaration)?.name === node; +} +/* @internal */ +export function isNameOfFunctionDeclaration(node: Node): boolean { + return isIdentifier(node) && tryCast(node.parent, isFunctionLike)?.name === node; +} +/* @internal */ +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((node.parent)) === node; + case SyntaxKind.ElementAccessExpression: + return (node.parent).argumentExpression === node; + case SyntaxKind.ComputedPropertyName: + return true; + case SyntaxKind.LiteralType: + return node.parent.parent.kind === SyntaxKind.IndexedAccessType; + default: + return false; } - - 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(node.parent) === node; - case SyntaxKind.ElementAccessExpression: - return (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 node; - } +} +/* @internal */ +export function isExpressionOfExternalModuleImportEqualsDeclaration(node: Node) { + return isExternalModuleImportEqualsDeclaration(node.parent.parent) && + getExternalModuleImportEqualsDeclarationExpression(node.parent.parent) === node; +} +/* @internal */ +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; } - } - - export function getNodeKind(node: Node): ScriptElementKind { switch (node.kind) { case SyntaxKind.SourceFile: - return isExternalModule(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(node); - case SyntaxKind.BindingElement: - return getKindOfVariableDeclaration(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(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*= 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.FunctionDeclaration: + case SyntaxKind.FunctionExpression: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: 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((n).block, sourceFile); - case SyntaxKind.NewExpression: - if (!(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((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 ((n).body) { - return isCompletedNode((n).body, sourceFile); - } - - if ((n).type) { - return isCompletedNode((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 !!(n).body && isCompletedNode((n).body, sourceFile); - - case SyntaxKind.IfStatement: - if ((n).elseStatement) { - return isCompletedNode((n).elseStatement, sourceFile); - } - return isCompletedNode((n).thenStatement, sourceFile); - - case SyntaxKind.ExpressionStatement: - return isCompletedNode((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 ((n).type) { - return isCompletedNode((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((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((n).statement, sourceFile); - - case SyntaxKind.TypeQuery: - return isCompletedNode((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((n).template, sourceFile); - case SyntaxKind.TemplateExpression: - const lastSpan = lastOrUndefined((n).templateSpans); - return isCompletedNode(lastSpan, sourceFile); - case SyntaxKind.TemplateSpan: - return nodeIsPresent((n).literal); - - case SyntaxKind.ExportDeclaration: - case SyntaxKind.ImportDeclaration: - return nodeIsPresent((n).moduleSpecifier); - - case SyntaxKind.PrefixUnaryExpression: - return isCompletedNode((n).operand, sourceFile); - case SyntaxKind.BinaryExpression: - return isCompletedNode((n).right, sourceFile); - case SyntaxKind.ConditionalExpression: - return isCompletedNode((n).whenFalse, sourceFile); - - default: - return true; + return node; } } - - /* - * 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; +} +/* @internal */ +export function getNodeKind(node: Node): ScriptElementKind { + switch (node.kind) { + case SyntaxKind.SourceFile: + return isExternalModule((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((node)); + case SyntaxKind.BindingElement: + return getKindOfVariableDeclaration((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(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; + } +} +/* @internal */ +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 +/* @internal */ +const tripleSlashDirectivePrefixRegex = /^\/\/\/\s*= range.end; +} +/* @internal */ +export function rangeContainsStartEnd(range: TextRange, start: number, end: number): boolean { + return range.pos <= start && range.end >= end; +} +/* @internal */ +export function rangeOverlapsWithStartEnd(r1: TextRange, start: number, end: number) { + return startEndOverlapsWithStartEnd(r1.pos, r1.end, start, end); +} +/* @internal */ +export function nodeOverlapsWithStartEnd(node: Node, sourceFile: SourceFile, start: number, end: number) { + return startEndOverlapsWithStartEnd(node.getStart(sourceFile), node.end, start, end); +} +/* @internal */ +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. + */ +/* @internal */ +export function positionBelongsToNode(candidate: Node, position: number, sourceFile: SourceFile): boolean { + Debug.assert(candidate.pos <= position); + return position < candidate.end || !isCompletedNode(candidate, sourceFile); +} +/* @internal */ +function isCompletedNode(n: Node | undefined, sourceFile: SourceFile): boolean { + if (n === undefined || nodeIsMissing(n)) { 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(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); + 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((n).block, sourceFile); + case SyntaxKind.NewExpression: + if (!(n).arguments) { + return true; } - } - if (isNamedDeclaration(node)) { - return node.name; - } + // 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((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 ((n).body) { + return isCompletedNode((n).body, sourceFile); + } + if ((n).type) { + return isCompletedNode((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 !!(n).body && isCompletedNode((n).body, sourceFile); + case SyntaxKind.IfStatement: + if ((n).elseStatement) { + return isCompletedNode((n).elseStatement, sourceFile); + } + return isCompletedNode((n).thenStatement, sourceFile); + case SyntaxKind.ExpressionStatement: + return isCompletedNode((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 ((n).type) { + return isCompletedNode((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((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((n).statement, sourceFile); + case SyntaxKind.TypeQuery: + return isCompletedNode((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((n).template, sourceFile); + case SyntaxKind.TemplateExpression: + const lastSpan = lastOrUndefined((n).templateSpans); + return isCompletedNode(lastSpan, sourceFile); + case SyntaxKind.TemplateSpan: + return nodeIsPresent((n).literal); + case SyntaxKind.ExportDeclaration: + case SyntaxKind.ImportDeclaration: + return nodeIsPresent((n).moduleSpecifier); + case SyntaxKind.PrefixUnaryExpression: + return isCompletedNode((n).operand, sourceFile); + case SyntaxKind.BinaryExpression: + return isCompletedNode((n).right, sourceFile); + case SyntaxKind.ConditionalExpression: + return isCompletedNode((n).whenFalse, sourceFile); + default: + return true; } - - 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; - // /**/ [|name|] ... - // /**/ [|name|] ... - // /**/ [|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; - } +} +/* + * 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'. + */ +/* @internal */ +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; } - // /**/ [|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; - } + else if (lastChild.kind === SyntaxKind.SemicolonToken && children.length !== 1) { + return children[children.length - 2].kind === expectedLastToken; } - 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; - } - } + } + return false; +} +/* @internal */ +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 + }; +} +/* @internal */ +export function hasChildOfKind(n: Node, kind: SyntaxKind, sourceFile: SourceFile): boolean { + return !!findChildOfKind(n, kind, sourceFile); +} +/* @internal */ +export function findChildOfKind(n: Node, kind: T["kind"], sourceFile: SourceFileLike): T | undefined { + return find(n.getChildren(sourceFile), (c): c is T => c.kind === kind); +} +/* @internal */ +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; +} +/* @internal */ +function isDefaultModifier(node: Node) { + return node.kind === SyntaxKind.DefaultKeyword; +} +/* @internal */ +function isClassKeyword(node: Node) { + return node.kind === SyntaxKind.ClassKeyword; +} +/* @internal */ +function isFunctionKeyword(node: Node) { + return node.kind === SyntaxKind.FunctionKeyword; +} +/* @internal */ +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; + } +} +/* @internal */ +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; + } +} +/* @internal */ +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)); } - // 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; - } + } + if (isNamedDeclaration(node)) { + return node.name; + } +} +/* @internal */ +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 ... 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; + // /**/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; } - // 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); + else if (isNamespaceImport(node.importClause.namedBindings)) { + return node.importClause.namedBindings.name; } } - // 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 (!forRename) { + // /**/import "[|module|]"; + // /**/import ... from "[|module|]"; + // import /**/type ... from "[|module|]"; + return node.moduleSpecifier; + } +} +/* @internal */ +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; + } +} +/* @internal */ +function getAdjustedLocationForHeritageClause(node: HeritageClause) { + // /**/extends [|name|] + // /**/implements [|name|] + if (node.types.length === 1) { + return node.types[0].expression; + } + // /**/extends name1, name2 ... + // /**/implements name1, name2 ... +} +/* @internal */ +function getAdjustedLocation(node: Node, forRename: boolean): Node { + const { parent } = node; + // /**/ [|name|] ... + // /**/ [|name|] ... + // /**/ [|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; + } + } + // /**/ [|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; } } - if (node.kind === SyntaxKind.ExtendsKeyword) { - // ... ... - 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; + // 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; } } - // ... 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)) { + } + // 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; } - // /**/keyof [|T|] - if (node.kind === SyntaxKind.KeyOfKeyword && isTypeOperatorNode(parent) && parent.operator === SyntaxKind.KeyOfKeyword && - isTypeReferenceNode(parent.type)) { - return parent.type.typeName; + if (isExportDeclaration(parent) && parent.exportClause && isNamespaceExport(parent.exportClause)) { + return parent.exportClause.name; } - // /**/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; + } + // /**/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; } - // for (... /**/in [|name|]) - // for (... /**/of [|name|]) - if (node.kind === SyntaxKind.InKeyword && isForInStatement(parent) || - node.kind === SyntaxKind.OfKeyword && isForOfStatement(parent)) { + } + // 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) { + // ... ... + 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); } } - 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 |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; + // 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. + */ +/* @internal */ +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. + */ +/* @internal */ +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)) + */ +/* @internal */ +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 + */ +/* @internal */ +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) */ +/* @internal */ +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 */ +/* @internal */ +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; } - 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 end = child.getEnd(); + if (position < end || (position === end && (child.kind === SyntaxKind.EndOfFileToken || includeEndPosition))) { + current = child; + continue outer; } - - 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); - } + else if (includePrecedingTokenAtEndPosition && end === position) { + const previousToken = findPrecedingToken(position, sourceFile, child); + if (previousToken && includePrecedingTokenAtEndPosition(previousToken)) { + return previousToken; } } - - 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 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 |bar -> will return foo + * + */ +/* @internal */ +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); +} +/* @internal */ +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; } - - const children = n.getChildren(sourceFile); - const candidate = findRightmostChildNodeWithTokens(children, /*exclusiveStartPosition*/ children.length, sourceFile); - return candidate && findRightmostToken(candidate, sourceFile); + 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 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--) { +} +/** + * Finds the rightmost token satisfying `token.end <= position`, + * excluding `JsxText` tokens containing only whitespace. + */ +/* @internal */ +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]; - - 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]; + // 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); } - - 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 !!(previousToken).isUnterminated; - } - } - - return false; +} +/* @internal */ +function isNonWhitespaceToken(n: Node): boolean { + return isToken(n) && !isWhiteSpaceOnlyJsxText(n); +} +/* @internal */ +function findRightmostToken(n: Node, sourceFile: SourceFile): Node | undefined { + if (isNonWhitespaceToken(n)) { + return n; } - - /** - * 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; + 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. + */ +/* @internal */ +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`"); } - - if (token.kind === SyntaxKind.JsxText) { - return true; + else if (nodeHasTokens(children[i], sourceFile)) { + return children[i]; } - - //
Hello |
- if (token.kind === SyntaxKind.LessThanToken && token.parent.kind === SyntaxKind.JsxText) { + } +} +/* @internal */ +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; } - - //
{ |
or
- if (token.kind === SyntaxKind.LessThanToken && token.parent.kind === SyntaxKind.JsxExpression) { - return true; + if (position === end) { + return !!(previousToken).isUnterminated; } - - //
{ - // | - // } < /div> - if (token && token.kind === SyntaxKind.CloseBraceToken && token.parent.kind === SyntaxKind.JsxExpression) { - return true; + } + return false; +} +/** + * returns true if the position is in between the open and close elements of an JSX expression. + */ +/* @internal */ +export function isInsideJsxElementOrAttribute(sourceFile: SourceFile, position: number) { + const token = getTokenAtPosition(sourceFile, position); + if (!token) { + return false; + } + if (token.kind === SyntaxKind.JsxText) { + return true; + } + //
Hello |
+ if (token.kind === SyntaxKind.LessThanToken && token.parent.kind === SyntaxKind.JsxText) { + return true; + } + //
{ |
or
+ if (token.kind === SyntaxKind.LessThanToken && token.parent.kind === SyntaxKind.JsxExpression) { + return true; + } + //
{ + // | + // } < /div> + if (token && token.kind === SyntaxKind.CloseBraceToken && token.parent.kind === SyntaxKind.JsxExpression) { + return true; + } + //
|
+ if (token.kind === SyntaxKind.LessThanToken && token.parent.kind === SyntaxKind.JsxClosingElement) { + return true; + } + return false; +} +/* @internal */ +function isWhiteSpaceOnlyJsxText(node: Node): boolean { + return isJsxText(node) && node.containsOnlyTriviaWhiteSpaces; +} +/* @internal */ +export function isInTemplateString(sourceFile: SourceFile, position: number) { + const token = getTokenAtPosition(sourceFile, position); + return isTemplateLiteralKind(token.kind) && position > token.getStart(sourceFile); +} +/* @internal */ +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; +} +/* @internal */ +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; } - - //
|
- if (token.kind === SyntaxKind.LessThanToken && token.parent.kind === SyntaxKind.JsxClosingElement) { - return true; + token = preceding; + if (token.kind === matchingTokenKind) { + if (remainingMatchingTokens === 0) { + return token; + } + remainingMatchingTokens--; } - - return false; + else if (token.kind === tokenKind) { + remainingMatchingTokens++; + } + } +} +/* @internal */ +export function removeOptionality(type: Type, isOptionalExpression: boolean, isOptionalChain: boolean) { + return isOptionalExpression ? type.getNonNullableType() : + isOptionalChain ? type.getNonOptionalType() : + type; +} +/* @internal */ +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)); +} +/* @internal */ +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); +} +/* @internal */ +export interface PossibleTypeArgumentInfo { + readonly called: Identifier; + readonly nTypeArguments: number; +} +/* @internal */ +export interface PossibleProgramFileInfo { + ProgramFiles?: string[]; +} +// Get info for an expression like `f <` that may be the start of type arguments. +/* @internal */ +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); } - - function isWhiteSpaceOnlyJsxText(node: Node): boolean { - return isJsxText(node) && node.containsOnlyTriviaWhiteSpaces; + 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. + */ +/* @internal */ +export function isInComment(sourceFile: SourceFile, position: number, tokenAtPosition?: Node): CommentRange | undefined { + return getRangeOfEnclosingComment(sourceFile, position, /*precedingToken*/ undefined, tokenAtPosition); +} +/* @internal */ +export function hasDocComment(sourceFile: SourceFile, position: number): boolean { + const token = getTokenAtPosition(sourceFile, position); + return !!findAncestor(token, isJSDoc); +} +/* @internal */ +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; +} +/* @internal */ +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; +} +/* @internal */ +export function getTypeArgumentOrTypeParameterList(node: Node): NodeArray | undefined { + if (node.kind === SyntaxKind.TypeReference || node.kind === SyntaxKind.CallExpression) { + return (node).typeArguments; + } + if (isFunctionLike(node) || node.kind === SyntaxKind.ClassDeclaration || node.kind === SyntaxKind.InterfaceDeclaration) { + return (node).typeParameters; } - - export function isInTemplateString(sourceFile: SourceFile, position: number) { - const token = getTokenAtPosition(sourceFile, position); - return isTemplateLiteralKind(token.kind) && position > token.getStart(sourceFile); + return undefined; +} +/* @internal */ +export function isComment(kind: SyntaxKind): boolean { + return kind === SyntaxKind.SingleLineCommentTrivia || kind === SyntaxKind.MultiLineCommentTrivia; +} +/* @internal */ +export function isStringOrRegularExpressionOrTemplateLiteral(kind: SyntaxKind): boolean { + if (kind === SyntaxKind.StringLiteral + || kind === SyntaxKind.RegularExpressionLiteral + || isTemplateLiteralKind(kind)) { + return true; + } + return false; +} +/* @internal */ +export function isPunctuation(kind: SyntaxKind): boolean { + return SyntaxKind.FirstPunctuation <= kind && kind <= SyntaxKind.LastPunctuation; +} +/* @internal */ +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); +} +/* @internal */ +export function isAccessibilityModifier(kind: SyntaxKind) { + switch (kind) { + case SyntaxKind.PublicKeyword: + case SyntaxKind.PrivateKeyword: + case SyntaxKind.ProtectedKeyword: + return true; } - - export function isInJSXText(sourceFile: SourceFile, position: number) { - const token = getTokenAtPosition(sourceFile, position); - if (isJsxText(token)) { + return false; +} +/* @internal */ +export function cloneCompilerOptions(options: CompilerOptions): CompilerOptions { + const result = clone(options); + setConfigFileInOptions(result, options && options.configFile); + return result; +} +/* @internal */ +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 && + (node.parent).left === node && + (node.parent).operatorToken.kind === SyntaxKind.EqualsToken) { return true; } - if (token.kind === SyntaxKind.OpenBraceToken && isJsxExpression(token.parent) && isJsxElement(token.parent.parent)) { + // [a, b, c] from: + // for([a, b, c] of expression) + if (node.parent.kind === SyntaxKind.ForOfStatement && + (node.parent).initializer === node) { return true; } - if (token.kind === SyntaxKind.LessThanToken && isJsxOpeningLikeElement(token.parent) && isJsxElement(token.parent.parent)) { + // [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 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; + return false; +} +/* @internal */ +export function isInReferenceComment(sourceFile: SourceFile, position: number): boolean { + return isInReferenceCommentWorker(sourceFile, position, /*shouldBeReference*/ true); +} +/* @internal */ +export function isInNonReferenceComment(sourceFile: SourceFile, position: number): boolean { + return isInReferenceCommentWorker(sourceFile, position, /*shouldBeReference*/ false); +} +/* @internal */ +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)); +} +/* @internal */ +export function createTextSpanFromNode(node: Node, sourceFile?: SourceFile, endNode?: Node): TextSpan { + return createTextSpanFromBounds(node.getStart(sourceFile), (endNode || node).getEnd()); +} +/* @internal */ +export function createTextRangeFromNode(node: Node, sourceFile: SourceFile): TextRange { + return createRange(node.getStart(sourceFile), node.end); +} +/* @internal */ +export function createTextSpanFromRange(range: TextRange): TextSpan { + return createTextSpanFromBounds(range.pos, range.end); +} +/* @internal */ +export function createTextRangeFromSpan(span: TextSpan): TextRange { + return createRange(span.start, span.start + span.length); +} +/* @internal */ +export function createTextChangeFromStartLength(start: number, length: number, newText: string): TextChange { + return createTextChange(createTextSpan(start, length), newText); +} +/* @internal */ +export function createTextChange(span: TextSpan, newText: string): TextChange { + return { span, newText }; +} +/* @internal */ +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, +]; +/* @internal */ +export function isTypeKeyword(kind: SyntaxKind): boolean { + return contains(typeKeywords, kind); +} +/* @internal */ +export function isTypeKeywordToken(node: Node): node is Token { + return node.kind === SyntaxKind.TypeKeyword; +} +/** True if the symbol is for an external module, as opposed to a namespace. */ +/* @internal */ +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. */ +/* @internal */ +export type NodeSeenTracker = (node: T) => boolean; +/* @internal */ +export function nodeSeenTracker(): NodeSeenTracker { + const seen: true[] = []; + return node => { + const id = getNodeId(node); + return !seen[id] && (seen[id] = true); + }; +} +/* @internal */ +export function getSnapshotText(snap: IScriptSnapshot): string { + return snap.getText(0, snap.getLength()); +} +/* @internal */ +export function repeatString(str: string, count: number): string { + let result = ""; + for (let i = 0; i < count; i++) { + result += str; + } + return result; +} +/* @internal */ +export function skipConstraint(type: Type): Type { + return type.isTypeParameter() ? type.getConstraint() || type : type; +} +/* @internal */ +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); +} +/* @internal */ +export function programContainsEs6Modules(program: Program): boolean { + return program.getSourceFiles().some(s => !s.isDeclarationFile && !program.isSourceFileFromExternalLibrary(s) && !!s.externalModuleIndicator); +} +/* @internal */ +export function compilerOptionsIndicateEs6Modules(compilerOptions: CompilerOptions): boolean { + return !!compilerOptions.module || (compilerOptions.target!) >= ScriptTarget.ES2015 || !!compilerOptions.noEmit; +} +/* @internal */ +export function hostUsesCaseSensitiveFileNames(host: { + useCaseSensitiveFileNames?(): boolean; +}): boolean { + return host.useCaseSensitiveFileNames ? host.useCaseSensitiveFileNames() : false; +} +/* @internal */ +export function hostGetCanonicalFileName(host: { + useCaseSensitiveFileNames?(): boolean; +}): GetCanonicalFileName { + return createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host)); +} +/* @internal */ +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; +} +/* @internal */ +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); +} +/* @internal */ +export function makeStringLiteral(text: string, quotePreference: QuotePreference): StringLiteral { + return createLiteral(text, quotePreference === QuotePreference.Single); +} +/* @internal */ +export const enum QuotePreference { + Single, + Double +} +/* @internal */ +export function quotePreferenceFromString(str: StringLiteral, sourceFile: SourceFile): QuotePreference { + return isStringDoubleQuoted(str, sourceFile) ? QuotePreference.Double : QuotePreference.Single; +} +/* @internal */ +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; + } +} +/* @internal */ +export function getQuoteFromPreference(qp: QuotePreference): string { + switch (qp) { + case QuotePreference.Single: return "'"; + case QuotePreference.Double: return '"'; + default: return Debug.assertNever(qp); + } +} +/* @internal */ +export function symbolNameNoDefault(symbol: Symbol): string | undefined { + const escaped = symbolEscapedNameNoDefault(symbol); + return escaped === undefined ? undefined : unescapeLeadingUnderscores(escaped); +} +/* @internal */ +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; + }); +} +/* @internal */ +export type ObjectBindingElementWithoutPropertyName = BindingElement & { + name: Identifier; +}; +/* @internal */ +export function isObjectBindingElementWithoutPropertyName(bindingElement: Node): bindingElement is ObjectBindingElementWithoutPropertyName { + return isBindingElement(bindingElement) && + isObjectBindingPattern(bindingElement.parent) && + isIdentifier(bindingElement.name) && + !bindingElement.propertyName; +} +/* @internal */ +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. + */ +/* @internal */ +export function getPropertySymbolsFromBaseTypes(symbol: Symbol, propertyName: string, checker: TypeChecker, cb: (symbol: Symbol) => T | undefined): T | undefined { + const seen = createMap(); + 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)); + })); + } +} +/* @internal */ +export function isMemberSymbolInBaseType(memberSymbol: Symbol, checker: TypeChecker): boolean { + return getPropertySymbolsFromBaseTypes(memberSymbol.parent!, memberSymbol.name, checker, _ => true) || false; +} +/* @internal */ +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; + } +} +/* @internal */ +function spanContainsNode(span: TextSpan, node: Node, file: SourceFile): boolean { + return textSpanContainsPosition(span, node.getStart(file)) && + node.getEnd() <= textSpanEnd(span); +} +/* @internal */ +export function findModifier(node: Node, kind: Modifier["kind"]): Modifier | undefined { + return node.modifiers && find(node.modifiers, m => m.kind === kind); +} +/* @internal */ +export function insertImport(changes: 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); + } +} +/* @internal */ +export function getTypeKeywordOfTypeOnlyImport(importClause: ImportClause, sourceFile: SourceFile): Token { + Debug.assert(importClause.isTypeOnly); + return cast(importClause.getChildAt(0, sourceFile), isTypeKeywordToken); +} +/* @internal */ +export function textSpansEqual(a: TextSpan | undefined, b: TextSpan | undefined): boolean { + return !!a && !!b && a.start === b.start && a.length === b.length; +} +/* @internal */ +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. + */ +/* @internal */ +export function forEachUnique(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; } - - remainingMatchingTokens--; - } - else if (token.kind === tokenKind) { - remainingMatchingTokens++; } } } - - export function removeOptionality(type: Type, isOptionalExpression: boolean, isOptionalChain: boolean) { - return isOptionalExpression ? type.getNonNullableType() : - isOptionalChain ? type.getNonOptionalType() : - type; + return undefined; +} +// #endregion +// Display-part writer helpers +// #region +/* @internal */ +export function isFirstDeclarationOfSymbolParameter(symbol: Symbol) { + return symbol.declarations && symbol.declarations.length > 0 && symbol.declarations[0].kind === SyntaxKind.Parameter; +} +/* @internal */ +const displayPartWriter = getDisplayPartWriter(); +/* @internal */ +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 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); +} +/* @internal */ +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; + } +} +/* @internal */ +export function displayPart(text: string, kind: SymbolDisplayPartKind): SymbolDisplayPart { + return { text, kind: SymbolDisplayPartKind[kind] }; +} +/* @internal */ +export function spacePart() { + return displayPart(" ", SymbolDisplayPartKind.space); +} +/* @internal */ +export function keywordPart(kind: SyntaxKind) { + return displayPart((tokenToString(kind)!), SymbolDisplayPartKind.keyword); +} +/* @internal */ +export function punctuationPart(kind: SyntaxKind) { + return displayPart((tokenToString(kind)!), SymbolDisplayPartKind.punctuation); +} +/* @internal */ +export function operatorPart(kind: SyntaxKind) { + return displayPart((tokenToString(kind)!), SymbolDisplayPartKind.operator); +} +/* @internal */ +export function textOrKeywordPart(text: string) { + const kind = stringToToken(text); + return kind === undefined + ? textPart(text) + : keywordPart(kind); +} +/* @internal */ +export function textPart(text: string) { + return displayPart(text, SymbolDisplayPartKind.text); +} +/* @internal */ +const carriageReturnLineFeed = "\r\n"; +/** + * The default is CRLF. + */ +/* @internal */ +export function getNewLineOrDefaultFromHost(host: LanguageServiceHost | LanguageServiceShimHost, formatSettings?: FormatCodeSettings) { + return (formatSettings && formatSettings.newLineCharacter) || + (host.getNewLine && host.getNewLine()) || + carriageReturnLineFeed; +} +/* @internal */ +export function lineBreakPart() { + return displayPart("\n", SymbolDisplayPartKind.lineBreak); +} +/* @internal */ +export function mapToDisplayParts(writeDisplayParts: (writer: DisplayPartsSymbolWriter) => void): SymbolDisplayPart[] { + try { + writeDisplayParts(displayPartWriter); + return displayPartWriter.displayParts(); + } + finally { + displayPartWriter.clear(); + } +} +/* @internal */ +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); + }); +} +/* @internal */ +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); + }); +} +/* @internal */ +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); + }); +} +/* @internal */ +export function isImportOrExportSpecifierName(location: Node): location is Identifier { + return !!location.parent && isImportOrExportSpecifier(location.parent) && location.parent.propertyName === location; +} +/* @internal */ +export function scriptKindIs(fileName: string, host: LanguageServiceHost, ...scriptKinds: ScriptKind[]): boolean { + const scriptKind = getScriptKind(fileName, host); + return some(scriptKinds, k => k === scriptKind); +} +/* @internal */ +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)); +} +/* @internal */ +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 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 | undefined { - if (node.kind === SyntaxKind.TypeReference || node.kind === SyntaxKind.CallExpression) { - return (node).typeArguments; - } - - if (isFunctionLike(node) || node.kind === SyntaxKind.ClassDeclaration || node.kind === SyntaxKind.InterfaceDeclaration) { - return (node).typeParameters; - } - - return undefined; + return next; +} +/* @internal */ +function isTransientSymbol(symbol: Symbol): symbol is TransientSymbol { + return (symbol.flags & SymbolFlags.Transient) !== 0; +} +/* @internal */ +function isAliasSymbol(symbol: Symbol): boolean { + return (symbol.flags & SymbolFlags.Alias) !== 0; +} +/* @internal */ +export function getUniqueSymbolId(symbol: Symbol, checker: TypeChecker) { + return getSymbolId(skipAlias(symbol, checker)); +} +/* @internal */ +export function getFirstNonSpaceCharacterPosition(text: string, position: number) { + while (isWhiteSpaceLike(text.charCodeAt(position))) { + position += 1; } - - export function isComment(kind: SyntaxKind): boolean { - return kind === SyntaxKind.SingleLineCommentTrivia || kind === SyntaxKind.MultiLineCommentTrivia; + return position; +} +/* @internal */ +export function getPrecedingNonSpaceCharacterPosition(text: string, position: number) { + while (position > -1 && isWhiteSpaceSingleLine(text.charCodeAt(position))) { + position -= 1; } - - export function isStringOrRegularExpressionOrTemplateLiteral(kind: SyntaxKind): boolean { - if (kind === SyntaxKind.StringLiteral - || kind === SyntaxKind.RegularExpressionLiteral - || isTemplateLiteralKind(kind)) { - return true; - } - return false; + 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). + */ +/* @internal */ +export function getSynthesizedDeepClone(node: T, includeTrivia = true): T { + const clone = node && getSynthesizedDeepCloneWorker(node as NonNullable); + if (clone && !includeTrivia) + suppressLeadingAndTrailingTrivia(clone); + return clone; +} +/* @internal */ +export function getSynthesizedDeepCloneWithRenames(node: T, includeTrivia = true, renameMap?: ts.Map, 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, renameMap, checker, callback); + } + if (clone && !includeTrivia) + suppressLeadingAndTrailingTrivia(clone); + if (callback && clone) + callback(node, clone); + return clone as T; +} +/* @internal */ +function getSynthesizedDeepCloneWorker(node: T, renameMap?: ts.Map, 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 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; +} +/* @internal */ +export function getSynthesizedDeepClones(nodes: NodeArray, includeTrivia?: boolean): NodeArray; +/* @internal */ +export function getSynthesizedDeepClones(nodes: NodeArray | undefined, includeTrivia?: boolean): NodeArray | undefined; +/* @internal */ +export function getSynthesizedDeepClones(nodes: NodeArray | undefined, includeTrivia = true): NodeArray | undefined { + return nodes && createNodeArray(nodes.map(n => getSynthesizedDeepClone(n, includeTrivia)), nodes.hasTrailingComma); +} +/** + * Sets EmitFlags to suppress leading and trailing trivia on the node. + */ +/* @internal */ +export function suppressLeadingAndTrailingTrivia(node: Node) { + suppressLeadingTrivia(node); + suppressTrailingTrivia(node); +} +/** + * Sets EmitFlags to suppress leading trivia on the node. + */ +/* @internal */ +export function suppressLeadingTrivia(node: Node) { + addEmitFlagsRecursively(node, EmitFlags.NoLeadingComments, getFirstChild); +} +/** + * Sets EmitFlags to suppress trailing trivia on the node. + */ +/* @internal */ +export function suppressTrailingTrivia(node: Node) { + addEmitFlagsRecursively(node, EmitFlags.NoTrailingComments, getLastChild); +} +/* @internal */ +function addEmitFlagsRecursively(node: Node, flag: EmitFlags, getChild: (n: Node) => Node | undefined) { + addEmitFlags(node, flag); + const child = getChild(node); + if (child) + addEmitFlagsRecursively(child, flag, getChild); +} +/* @internal */ +function getFirstChild(node: Node): Node | undefined { + return node.forEachChild(child => child); +} +/* @internal */ +export function getUniqueName(baseName: string, sourceFile: SourceFile): string { + let nameText = baseName; + for (let i = 1; !isFileLevelUniqueName(sourceFile, nameText); i++) { + nameText = `${baseName}_${i}`; } - - 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 && - (node.parent).left === node && - (node.parent).operatorToken.kind === SyntaxKind.EqualsToken) { - return true; - } - - // [a, b, c] from: - // for([a, b, c] of expression) - if (node.parent.kind === SyntaxKind.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 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. + */ +/* @internal */ +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; } - - 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 { - 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 = (node: T) => boolean; - export function nodeSeenTracker(): NodeSeenTracker { - 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; + // If the declaration comes first, return the position of the last occurrence. + Debug.assert(preferLastLocation); + Debug.assert(lastPos >= 0); + return lastPos; +} +/* @internal */ +export function copyLeadingComments(sourceNode: Node, targetNode: Node, sourceFile: SourceFile, commentKind?: CommentKind, hasTrailingNewLine?: boolean) { + forEachLeadingCommentRange(sourceFile.text, sourceNode.pos, getAddCommentsFunction(targetNode, sourceFile, commentKind, hasTrailingNewLine, addSyntheticLeadingComment)); +} +/* @internal */ +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. + */ +/* @internal */ +export function copyTrailingAsLeadingComments(sourceNode: Node, targetNode: Node, sourceFile: SourceFile, commentKind?: CommentKind, hasTrailingNewLine?: boolean) { + forEachTrailingCommentRange(sourceFile.text, sourceNode.pos, getAddCommentsFunction(targetNode, sourceFile, commentKind, hasTrailingNewLine, addSyntheticLeadingComment)); +} +/* @internal */ +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 { - 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); + // Remove leading // + pos += 2; } + cb(targetNode, commentKind || kind, sourceFile.text.slice(pos, end), hasTrailingNewLine !== undefined ? hasTrailingNewLine : htnl); + }; +} +/* @internal */ +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; +} +/* @internal */ +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 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; - }); +} +/* @internal */ +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 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(symbol: Symbol, propertyName: string, checker: TypeChecker, cb: (symbol: Symbol) => T | undefined): T | undefined { - const seen = createMap(); - 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; - } +} +/* @internal */ +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; } - - function spanContainsNode(span: TextSpan, node: Node, file: SourceFile): boolean { - return textSpanContainsPosition(span, node.getStart(file)) && - node.getEnd() <= textSpanEnd(span); +} +/* @internal */ +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 findModifier(node: Node, kind: Modifier["kind"]): Modifier | undefined { - return node.modifiers && find(node.modifiers, m => m.kind === kind); +} +/* @internal */ +export function hasIndexSignature(type: Type): boolean { + return !!type.getStringIndexType() || !!type.getNumberIndexType(); +} +/* @internal */ +export function getSwitchedType(caseClause: CaseClause, checker: TypeChecker): Type | undefined { + return checker.getTypeAtLocation(caseClause.parent.parent.expression); +} +/* @internal */ +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; +} +/* @internal */ +export function syntaxRequiresTrailingCommaOrSemicolonOrASI(kind: SyntaxKind) { + return kind === SyntaxKind.CallSignature + || kind === SyntaxKind.ConstructSignature + || kind === SyntaxKind.IndexSignature + || kind === SyntaxKind.PropertySignature + || kind === SyntaxKind.MethodSignature; +} +/* @internal */ +export function syntaxRequiresTrailingFunctionBlockOrSemicolonOrASI(kind: SyntaxKind) { + return kind === SyntaxKind.FunctionDeclaration + || kind === SyntaxKind.Constructor + || kind === SyntaxKind.MethodDeclaration + || kind === SyntaxKind.GetAccessor + || kind === SyntaxKind.SetAccessor; +} +/* @internal */ +export function syntaxRequiresTrailingModuleBlockOrSemicolonOrASI(kind: SyntaxKind) { + return kind === SyntaxKind.ModuleDeclaration; +} +/* @internal */ +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; +} +/* @internal */ +export const syntaxMayBeASICandidate = or(syntaxRequiresTrailingCommaOrSemicolonOrASI, syntaxRequiresTrailingFunctionBlockOrSemicolonOrASI, syntaxRequiresTrailingModuleBlockOrSemicolonOrASI, syntaxRequiresTrailingSemicolonOrASI); +/* @internal */ +function nodeIsASICandidate(node: Node, sourceFile: SourceFileLike): boolean { + const lastToken = node.getLastToken(sourceFile); + if (lastToken && lastToken.kind === SyntaxKind.SemicolonToken) { + return false; } - - 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); + if (syntaxRequiresTrailingCommaOrSemicolonOrASI(node.kind)) { + if (lastToken && lastToken.kind === SyntaxKind.CommaToken) { + return false; } - else { - changes.insertNodeAtTopOfFile(sourceFile, importDecl, blankLineBetween); - } - } - - export function getTypeKeywordOfTypeOnlyImport(importClause: ImportClause, sourceFile: SourceFile): Token { - 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(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; - } - } - } + } + else if (syntaxRequiresTrailingModuleBlockOrSemicolonOrASI(node.kind)) { + const lastChild = last(node.getChildren(sourceFile)); + if (lastChild && isModuleBlock(lastChild)) { + return false; } - 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; - } + else if (syntaxRequiresTrailingFunctionBlockOrSemicolonOrASI(node.kind)) { + const lastChild = last(node.getChildren(sourceFile)); + if (lastChild && isFunctionBlock(lastChild)) { + return 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); - }); + else if (!syntaxRequiresTrailingSemicolonOrASI(node.kind)) { + return false; } - - 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); - }); + // See comment in parser’s `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; } - - 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; + const startLine = sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line; + const endLine = sourceFile.getLineAndCharacterOfPosition(nextToken.getStart(sourceFile)).line; + return startLine !== endLine; +} +/* @internal */ +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); +} +/* @internal */ +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 { - 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(node: T, includeTrivia = true): T { - const clone = node && getSynthesizedDeepCloneWorker(node as NonNullable); - if (clone && !includeTrivia) suppressLeadingAndTrailingTrivia(clone); - return clone; - } - - export function getSynthesizedDeepCloneWithRenames(node: T, includeTrivia = true, renameMap?: Map, 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, renameMap, checker, callback); - } - - if (clone && !includeTrivia) suppressLeadingAndTrailingTrivia(clone); - if (callback && clone) callback(node, clone); - - return clone as T; - } - - - function getSynthesizedDeepCloneWorker(node: T, renameMap?: Map, 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(nodes: NodeArray, includeTrivia?: boolean): NodeArray; - export function getSynthesizedDeepClones(nodes: NodeArray | undefined, includeTrivia?: boolean): NodeArray | undefined; - export function getSynthesizedDeepClones(nodes: NodeArray | undefined, includeTrivia = true): NodeArray | 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; + withoutSemicolon++; } } - - // 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); + if (withSemicolon + withoutSemicolon >= nStatementsToObserve) { + return true; } + return forEachChild(node, visit); + }); + // One statement missing a semicolon isn’t sufficient evidence to say the user + // doesn’t want semicolons, because they may not even be done writing that statement. + if (withSemicolon === 0 && withoutSemicolon <= 1) { + return true; } - - 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; - } + // If even 2/5 places have a semicolon, the user probably wants semicolons + return withSemicolon / withoutSemicolon > 1 / nStatementsToObserve; +} +/* @internal */ +export function tryGetDirectories(host: Pick, directoryName: string): string[] { + return tryIOAndConsumeErrors(host, host.getDirectories, directoryName) || []; +} +/* @internal */ +export function tryReadDirectory(host: Pick, path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[]): readonly string[] { + return tryIOAndConsumeErrors(host, host.readDirectory, path, extensions, exclude, include) || emptyArray; +} +/* @internal */ +export function tryFileExists(host: Pick, path: string): boolean { + return tryIOAndConsumeErrors(host, host.fileExists, path); +} +/* @internal */ +export function tryDirectoryExists(host: LanguageServiceHost, path: string): boolean { + return tryAndIgnoreErrors(() => directoryProbablyExists(path, host)) || false; +} +/* @internal */ +export function tryAndIgnoreErrors(cb: () => T): T | undefined { + try { + return cb(); } - - 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 parser’s `parseDoStatement` - if (node.kind === SyntaxKind.DoStatement) { + catch { + return undefined; + } +} +/* @internal */ +export function tryIOAndConsumeErrors(host: unknown, toApply: ((...a: any[]) => T) | undefined, ...args: any[]) { + return tryAndIgnoreErrors(() => toApply && toApply.apply(host, args)); +} +/* @internal */ +export function findPackageJsons(startDirectory: string, host: Pick, stopDirectory?: string): string[] { + const paths: string[] = []; + forEachAncestorDirectory(startDirectory, ancestor => { + if (ancestor === stopDirectory) { return true; } - - const topNode = findAncestor(node, ancestor => !ancestor.parent)!; - const nextToken = findNextToken(node, topNode, sourceFile); - if (!nextToken || nextToken.kind === SyntaxKind.CloseBraceToken) { - return true; + const currentConfigPath = combinePaths(ancestor, "package.json"); + if (tryFileExists(host, currentConfigPath)) { + paths.push(currentConfigPath); } - - 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 isn’t sufficient evidence to say the user - // doesn’t want semicolons, because they may not even be done writing that statement. - if (withSemicolon === 0 && withoutSemicolon <= 1) { + }); + return paths; +} +/* @internal */ +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 } - - // If even 2/5 places have a semicolon, the user probably wants semicolons - return withSemicolon / withoutSemicolon > 1 / nStatementsToObserve; - } - - export function tryGetDirectories(host: Pick, directoryName: string): string[] { - return tryIOAndConsumeErrors(host, host.getDirectories, directoryName) || []; - } - - export function tryReadDirectory(host: Pick, 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, 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(cb: () => T): T | undefined { - try { return cb(); } - catch { return undefined; } - } - - export function tryIOAndConsumeErrors(host: unknown, toApply: ((...a: any[]) => T) | undefined, ...args: any[]) { - return tryAndIgnoreErrors(() => toApply && toApply.apply(host, args)); + }); + return packageJson; +} +/* @internal */ +export function getPackageJsonsVisibleToFile(fileName: string, host: LanguageServiceHost): readonly PackageJsonInfo[] { + if (!host.fileExists) { + return []; } - - export function findPackageJsons(startDirectory: string, host: Pick, 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); - } + 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 | 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 = {}; - for (const key of dependencyKeys) { - const dependencies = content[key]; - if (!dependencies) { - continue; - } - const dependencyMap = createMap(); - 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; - } + }); + return packageJsons; +} +/* @internal */ +export function createPackageJsonInfo(fileName: string, host: LanguageServiceHost): PackageJsonInfo | false | undefined { + if (!host.readFile) { + return undefined; + } + type PackageJsonRaw = Record | 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 = {}; + for (const key of dependencyKeys) { + const dependencies = content[key]; + if (!dependencies) { + continue; + } + const dependencyMap = createMap(); + 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; - } +} +/* @internal */ +function tryParseJson(text: string) { + try { + return JSON.parse(text); } - - export function consumesNodeCoreModules(sourceFile: SourceFile): boolean { - return some(sourceFile.imports, ({ text }) => JsTyping.nodeCoreModules.has(text)); + catch { + return undefined; } - - export function isInsideNodeModules(fileOrDirectory: string): boolean { - return contains(getPathComponents(fileOrDirectory), "node_modules"); +} +/* @internal */ +export function consumesNodeCoreModules(sourceFile: SourceFile): boolean { + return some(sourceFile.imports, ({ text }) => JsTyping.nodeCoreModules.has(text)); +} +/* @internal */ +export function isInsideNodeModules(fileOrDirectory: string): boolean { + return contains(getPathComponents(fileOrDirectory), "node_modules"); +} +/* @internal */ +export function isDiagnosticWithLocation(diagnostic: Diagnostic): diagnostic is DiagnosticWithLocation { + return diagnostic.file !== undefined && diagnostic.start !== undefined && diagnostic.length !== undefined; +} +/* @internal */ +export function findDiagnosticForNode(node: Node, sortedFileDiagnostics: readonly Diagnostic[]): DiagnosticWithLocation | undefined { + const span: Partial = 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 isDiagnosticWithLocation(diagnostic: Diagnostic): diagnostic is DiagnosticWithLocation { - return diagnostic.file !== undefined && diagnostic.start !== undefined && diagnostic.length !== undefined; +} +/* @internal */ +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; } - - export function findDiagnosticForNode(node: Node, sortedFileDiagnostics: readonly Diagnostic[]): DiagnosticWithLocation | undefined { - const span: Partial = 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); - } + while (sortedFileDiagnostics[index - 1]?.start === span.start) { + index--; } - - 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; } - - 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++; + if (textSpanContainsTextSpan(span, diagnostic)) { + result.push(diagnostic); } - - return result; + index++; } - - /* @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(valueOrArray: T | readonly T[], f: (x: T, i: number) => U): U | U[]; - export function mapOneOrMany(valueOrArray: T | readonly T[] | undefined, f: (x: T, i: number) => U): U | U[] | undefined; - export function mapOneOrMany(valueOrArray: T | readonly T[], f: (x: T, i: number) => U, resultSelector: (x: U[]) => U): U; - export function mapOneOrMany(valueOrArray: T | readonly T[] | undefined, f: (x: T, i: number) => U, resultSelector: (x: U[]) => U): U | undefined; - export function mapOneOrMany(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(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.checkDefined(symbol.parent), scriptTarget); - } - return symbol.name; - } - - // #endregion + return result; +} +/* @internal */ +/* @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. + */ +/* @internal */ +export function mapOneOrMany(valueOrArray: T | readonly T[], f: (x: T, i: number) => U): U | U[]; +/* @internal */ +export function mapOneOrMany(valueOrArray: T | readonly T[] | undefined, f: (x: T, i: number) => U): U | U[] | undefined; +/* @internal */ +export function mapOneOrMany(valueOrArray: T | readonly T[], f: (x: T, i: number) => U, resultSelector: (x: U[]) => U): U; +/* @internal */ +export function mapOneOrMany(valueOrArray: T | readonly T[] | undefined, f: (x: T, i: number) => U, resultSelector: (x: U[]) => U): U | undefined; +/* @internal */ +export function mapOneOrMany(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. + */ +/* @internal */ +export function firstOrOnly(valueOrArray: T | readonly T[]): T { + return isArray(valueOrArray) ? first(valueOrArray) : valueOrArray; +} +/* @internal */ +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) + || moduleSymbolToValidIdentifier(Debug.checkDefined(symbol.parent), scriptTarget); + } + return symbol.name; } diff --git a/src/shims/mapShim.ts b/src/shims/mapShim.ts index 23391150941b7..fbf2a194ece2f 100644 --- a/src/shims/mapShim.ts +++ b/src/shims/mapShim.ts @@ -1,220 +1,199 @@ /* @internal */ -namespace ts { - interface IteratorShim { - next(): { value: T, done?: false } | { value: never, done: true }; +interface IteratorShim { + next(): { + value: T; + done?: false; + } | { + value: never; + done: true; + }; +} +/* @internal */ +interface MapShim { + readonly size: number; + get(key: string): T | undefined; + set(key: string, value: T): this; + has(key: string): boolean; + delete(key: string): boolean; + clear(): void; + keys(): IteratorShim; + values(): IteratorShim; + entries(): IteratorShim<[string, T]>; + forEach(action: (value: T, key: string) => void): void; +} +/* @internal */ +export function createMapShim(): new () => MapShim { + /** Create a MapLike with good performance. */ + function createDictionaryObject(): Record { + const map = Object.create(/*prototype*/ null); // eslint-disable-line no-null/no-null + // Using 'delete' on an object causes V8 to put the object in dictionary mode. + // This disables creation of hidden classes, which are expensive when an object is + // constantly changing shape. + map.__ = undefined; + delete map.__; + return map; } - interface MapShim { - readonly size: number; - get(key: string): T | undefined; - set(key: string, value: T): this; - has(key: string): boolean; - delete(key: string): boolean; - clear(): void; - keys(): IteratorShim; - values(): IteratorShim; - entries(): IteratorShim<[string, T]>; - forEach(action: (value: T, key: string) => void): void; + interface MapEntry { + readonly key?: string; + value?: T; + // Linked list references for iterators. + nextEntry?: MapEntry; + previousEntry?: MapEntry; + /** + * Specifies if iterators should skip the next entry. + * This will be set when an entry is deleted. + * See https://github.com/Microsoft/TypeScript/pull/27292 for more information. + */ + skipNext?: boolean; } - export function createMapShim(): new () => MapShim { - /** Create a MapLike with good performance. */ - function createDictionaryObject(): Record { - const map = Object.create(/*prototype*/ null); // eslint-disable-line no-null/no-null - - // Using 'delete' on an object causes V8 to put the object in dictionary mode. - // This disables creation of hidden classes, which are expensive when an object is - // constantly changing shape. - map.__ = undefined; - delete map.__; - - return map; + class MapIterator { + private currentEntry?: MapEntry; + private selector: (key: string, value: T) => U; + constructor(currentEntry: MapEntry, selector: (key: string, value: T) => U) { + this.currentEntry = currentEntry; + this.selector = selector; } - - interface MapEntry { - readonly key?: string; - value?: T; - - // Linked list references for iterators. - nextEntry?: MapEntry; - previousEntry?: MapEntry; - - /** - * Specifies if iterators should skip the next entry. - * This will be set when an entry is deleted. - * See https://github.com/Microsoft/TypeScript/pull/27292 for more information. - */ - skipNext?: boolean; - } - - class MapIterator { - private currentEntry?: MapEntry; - private selector: (key: string, value: T) => U; - - constructor(currentEntry: MapEntry, selector: (key: string, value: T) => U) { - this.currentEntry = currentEntry; - this.selector = selector; - } - - public next(): { value: U, done?: false } | { value: never, done: true } { - // Navigate to the next entry. - while (this.currentEntry) { - const skipNext = !!this.currentEntry.skipNext; - this.currentEntry = this.currentEntry.nextEntry; - - if (!skipNext) { - break; - } - } - - if (this.currentEntry) { - return { value: this.selector(this.currentEntry.key!, this.currentEntry.value!), done: false }; - } - else { - return { value: undefined as never, done: true }; + public next(): { + value: U; + done?: false; + } | { + value: never; + done: true; + } { + // Navigate to the next entry. + while (this.currentEntry) { + const skipNext = !!this.currentEntry.skipNext; + this.currentEntry = this.currentEntry.nextEntry; + if (!skipNext) { + break; } } - } - - return class implements MapShim { - private data = createDictionaryObject>(); - public size = 0; - - // Linked list references for iterators. - // See https://github.com/Microsoft/TypeScript/pull/27292 - // for more information. - - /** - * The first entry in the linked list. - * Note that this is only a stub that serves as starting point - * for iterators and doesn't contain a key and a value. - */ - private readonly firstEntry: MapEntry; - private lastEntry: MapEntry; - - constructor() { - // Create a first (stub) map entry that will not contain a key - // and value but serves as starting point for iterators. - this.firstEntry = {}; - // When the map is empty, the last entry is the same as the - // first one. - this.lastEntry = this.firstEntry; + if (this.currentEntry) { + return { value: this.selector(this.currentEntry.key!, this.currentEntry.value!), done: false }; } - - get(key: string): T | undefined { - const entry = this.data[key] as MapEntry | undefined; - return entry && entry.value!; + else { + return { value: undefined as never, done: true }; } - - set(key: string, value: T): this { - if (!this.has(key)) { - this.size++; - - // Create a new entry that will be appended at the - // end of the linked list. - const newEntry: MapEntry = { - key, - value - }; - this.data[key] = newEntry; - - // Adjust the references. - const previousLastEntry = this.lastEntry; - previousLastEntry.nextEntry = newEntry; - newEntry.previousEntry = previousLastEntry; - this.lastEntry = newEntry; - } - else { - this.data[key].value = value; - } - - return this; + } + } + return class implements MapShim { + private data = createDictionaryObject>(); + public size = 0; + // Linked list references for iterators. + // See https://github.com/Microsoft/TypeScript/pull/27292 + // for more information. + /** + * The first entry in the linked list. + * Note that this is only a stub that serves as starting point + * for iterators and doesn't contain a key and a value. + */ + private readonly firstEntry: MapEntry; + private lastEntry: MapEntry; + constructor() { + // Create a first (stub) map entry that will not contain a key + // and value but serves as starting point for iterators. + this.firstEntry = {}; + // When the map is empty, the last entry is the same as the + // first one. + this.lastEntry = this.firstEntry; + } + get(key: string): T | undefined { + const entry = this.data[key] as MapEntry | undefined; + return entry && entry.value!; + } + set(key: string, value: T): this { + if (!this.has(key)) { + this.size++; + // Create a new entry that will be appended at the + // end of the linked list. + const newEntry: MapEntry = { + key, + value + }; + this.data[key] = newEntry; + // Adjust the references. + const previousLastEntry = this.lastEntry; + previousLastEntry.nextEntry = newEntry; + newEntry.previousEntry = previousLastEntry; + this.lastEntry = newEntry; } - - has(key: string): boolean { - // eslint-disable-next-line no-in-operator - return key in this.data; + else { + this.data[key].value = value; } - - delete(key: string): boolean { - if (this.has(key)) { - this.size--; - const entry = this.data[key]; - delete this.data[key]; - - // Adjust the linked list references of the neighbor entries. - const previousEntry = entry.previousEntry!; - previousEntry.nextEntry = entry.nextEntry; - if (entry.nextEntry) { - entry.nextEntry.previousEntry = previousEntry; - } - - // When the deleted entry was the last one, we need to - // adjust the lastEntry reference. - if (this.lastEntry === entry) { - this.lastEntry = previousEntry; - } - - // Adjust the forward reference of the deleted entry - // in case an iterator still references it. This allows us - // to throw away the entry, but when an active iterator - // (which points to the current entry) continues, it will - // navigate to the entry that originally came before the - // current one and skip it. - entry.previousEntry = undefined; - entry.nextEntry = previousEntry; - entry.skipNext = true; - - return true; + return this; + } + has(key: string): boolean { + // eslint-disable-next-line no-in-operator + return key in this.data; + } + delete(key: string): boolean { + if (this.has(key)) { + this.size--; + const entry = this.data[key]; + delete this.data[key]; + // Adjust the linked list references of the neighbor entries. + const previousEntry = entry.previousEntry!; + previousEntry.nextEntry = entry.nextEntry; + if (entry.nextEntry) { + entry.nextEntry.previousEntry = previousEntry; } - return false; - } - - clear(): void { - this.data = createDictionaryObject>(); - this.size = 0; - - // Reset the linked list. Note that we must adjust the forward - // references of the deleted entries to ensure iterators stuck - // in the middle of the list don't continue with deleted entries, - // but can continue with new entries added after the clear() - // operation. - const firstEntry = this.firstEntry; - let currentEntry = firstEntry.nextEntry; - while (currentEntry) { - const nextEntry = currentEntry.nextEntry; - currentEntry.previousEntry = undefined; - currentEntry.nextEntry = firstEntry; - currentEntry.skipNext = true; - - currentEntry = nextEntry; + // When the deleted entry was the last one, we need to + // adjust the lastEntry reference. + if (this.lastEntry === entry) { + this.lastEntry = previousEntry; } - firstEntry.nextEntry = undefined; - this.lastEntry = firstEntry; + // Adjust the forward reference of the deleted entry + // in case an iterator still references it. This allows us + // to throw away the entry, but when an active iterator + // (which points to the current entry) continues, it will + // navigate to the entry that originally came before the + // current one and skip it. + entry.previousEntry = undefined; + entry.nextEntry = previousEntry; + entry.skipNext = true; + return true; } - - keys(): IteratorShim { - return new MapIterator(this.firstEntry, key => key); - } - - values(): IteratorShim { - return new MapIterator(this.firstEntry, (_key, value) => value); - } - - entries(): IteratorShim<[string, T]> { - return new MapIterator(this.firstEntry, (key, value) => [key, value] as [string, T]); + return false; + } + clear(): void { + this.data = createDictionaryObject>(); + this.size = 0; + // Reset the linked list. Note that we must adjust the forward + // references of the deleted entries to ensure iterators stuck + // in the middle of the list don't continue with deleted entries, + // but can continue with new entries added after the clear() + // operation. + const firstEntry = this.firstEntry; + let currentEntry = firstEntry.nextEntry; + while (currentEntry) { + const nextEntry = currentEntry.nextEntry; + currentEntry.previousEntry = undefined; + currentEntry.nextEntry = firstEntry; + currentEntry.skipNext = true; + currentEntry = nextEntry; } - - forEach(action: (value: T, key: string) => void): void { - const iterator = this.entries(); - while (true) { - const iterResult = iterator.next(); - if (iterResult.done) { - break; - } - - const [key, value] = iterResult.value; - action(value, key); + firstEntry.nextEntry = undefined; + this.lastEntry = firstEntry; + } + keys(): IteratorShim { + return new MapIterator(this.firstEntry, key => key); + } + values(): IteratorShim { + return new MapIterator(this.firstEntry, (_key, value) => value); + } + entries(): IteratorShim<[string, T]> { + return new MapIterator(this.firstEntry, (key, value) => [key, value] as [string, T]); + } + forEach(action: (value: T, key: string) => void): void { + const iterator = this.entries(); + while (true) { + const iterResult = iterator.next(); + if (iterResult.done) { + break; } + const [key, value] = iterResult.value; + action(value, key); } - }; - } -} \ No newline at end of file + } + }; +} diff --git a/src/shims/ts.ts b/src/shims/ts.ts new file mode 100644 index 0000000000000..fe05ba64a46bb --- /dev/null +++ b/src/shims/ts.ts @@ -0,0 +1 @@ +export * from "./mapShim"; diff --git a/src/shims/tsconfig.json b/src/shims/tsconfig.json index cb058762fd616..103494946e7cc 100644 --- a/src/shims/tsconfig.json +++ b/src/shims/tsconfig.json @@ -1,9 +1,10 @@ { "extends": "../tsconfig-base", "compilerOptions": { - "outFile": "../../built/local/shims.js" + "outDir": "../../built/local" }, "files": [ - "mapShim.ts" + "mapShim.ts", + "./ts.ts" ] } diff --git a/src/testRunner/FourSlash.ts b/src/testRunner/FourSlash.ts new file mode 100644 index 0000000000000..7936a0d67dcef --- /dev/null +++ b/src/testRunner/FourSlash.ts @@ -0,0 +1,2 @@ +export * from "../harness/FourSlash"; +export * from "./fourslashRef"; diff --git a/src/testRunner/Harness.Parallel.Host.ts b/src/testRunner/Harness.Parallel.Host.ts new file mode 100644 index 0000000000000..6f1bfa2ebfad0 --- /dev/null +++ b/src/testRunner/Harness.Parallel.Host.ts @@ -0,0 +1 @@ +export * from "./parallel/host"; diff --git a/src/testRunner/Harness.Parallel.Worker.ts b/src/testRunner/Harness.Parallel.Worker.ts new file mode 100644 index 0000000000000..f40e6550c69a6 --- /dev/null +++ b/src/testRunner/Harness.Parallel.Worker.ts @@ -0,0 +1 @@ +export * from "./parallel/worker"; diff --git a/src/testRunner/Harness.Parallel.ts b/src/testRunner/Harness.Parallel.ts new file mode 100644 index 0000000000000..e0d6bf43f3dac --- /dev/null +++ b/src/testRunner/Harness.Parallel.ts @@ -0,0 +1,5 @@ +export * from "./parallel/shared"; +import * as Host from "./Harness.Parallel.Host"; +export { Host }; +import * as Worker from "./Harness.Parallel.Worker"; +export { Worker }; diff --git a/src/testRunner/Harness.ts b/src/testRunner/Harness.ts new file mode 100644 index 0000000000000..654718d3eb88a --- /dev/null +++ b/src/testRunner/Harness.ts @@ -0,0 +1,8 @@ +export * from "../harness/Harness"; +export * from "./fourslashRunner"; +export * from "./compilerRunner"; +export * from "./externalCompileRunner"; +export * from "./test262Runner"; +export * from "./runner"; +import * as Parallel from "./Harness.Parallel"; +export { Parallel }; diff --git a/src/testRunner/Playback.ts b/src/testRunner/Playback.ts new file mode 100644 index 0000000000000..564b29427057c --- /dev/null +++ b/src/testRunner/Playback.ts @@ -0,0 +1,2 @@ +export * from "../harness/Playback"; +export * from "./playbackRef"; diff --git a/src/testRunner/RWC.ts b/src/testRunner/RWC.ts new file mode 100644 index 0000000000000..aaab83dbe034d --- /dev/null +++ b/src/testRunner/RWC.ts @@ -0,0 +1 @@ +export * from "./rwcRunner"; diff --git a/src/testRunner/Utils.ts b/src/testRunner/Utils.ts new file mode 100644 index 0000000000000..a4068f01affd4 --- /dev/null +++ b/src/testRunner/Utils.ts @@ -0,0 +1,2 @@ +export * from "../harness/Utils"; +export * from "./utilsRef"; diff --git a/src/testRunner/compiler.ts b/src/testRunner/compiler.ts new file mode 100644 index 0000000000000..4685c15378d45 --- /dev/null +++ b/src/testRunner/compiler.ts @@ -0,0 +1,2 @@ +export * from "../harness/compiler"; +export * from "./compilerRef"; diff --git a/src/testRunner/compilerRef.ts b/src/testRunner/compilerRef.ts index b76e4e72fbb88..5d97e653c3abd 100644 --- a/src/testRunner/compilerRef.ts +++ b/src/testRunner/compilerRef.ts @@ -1,2 +1,2 @@ // empty ref to compiler so it can be referenced by unittests -namespace compiler {} \ No newline at end of file +export {}; diff --git a/src/testRunner/compilerRunner.ts b/src/testRunner/compilerRunner.ts index ef0abb1c45699..783070934b924 100644 --- a/src/testRunner/compilerRunner.ts +++ b/src/testRunner/compilerRunner.ts @@ -1,335 +1,279 @@ -namespace Harness { - export const enum CompilerTestType { - Conformance, - Regressions, - Test262 - } - - interface CompilerFileBasedTest extends FileBasedTest { - readonly content?: string; - } - - export class CompilerBaselineRunner extends RunnerBase { - private basePath = "tests/cases"; - private testSuiteName: TestRunnerKind; - private emit: boolean; - - public options: string | undefined; - - constructor(public testType: CompilerTestType) { - super(); - this.emit = true; - if (testType === CompilerTestType.Conformance) { - this.testSuiteName = "conformance"; - } - else if (testType === CompilerTestType.Regressions) { - this.testSuiteName = "compiler"; - } - else if (testType === CompilerTestType.Test262) { - this.testSuiteName = "test262"; - } - else { - this.testSuiteName = "compiler"; // default to this for historical reasons - } - this.basePath += "/" + this.testSuiteName; +import { FileBasedTest, RunnerBase, TestRunnerKind, IO, getFileBasedTestConfigurationDescription, FileBasedTestConfiguration, TestCaseParser, Compiler, getFileBasedTestConfigurations, Baseline } from "./Harness"; +import { normalizeSeparators, basename } from "./vpath"; +import { some, getDirectoryPath, CompilerOptions, cloneCompilerOptions, combinePaths, isRootedDiskPath, getNormalizedAbsolutePath, fileExtensionIs, Extension, length, toPath, identity } from "./ts"; +import { CompilationResult } from "./compiler"; +import { sanitizeTraceResolutionLogEntry, removeTestPathPrefixes } from "./Utils"; +import * as vpath from "./vpath"; +export const enum CompilerTestType { + Conformance, + Regressions, + Test262 +} +interface CompilerFileBasedTest extends FileBasedTest { + readonly content?: string; +} +export class CompilerBaselineRunner extends RunnerBase { + private basePath = "tests/cases"; + private testSuiteName: TestRunnerKind; + private emit: boolean; + public options: string | undefined; + constructor(public testType: CompilerTestType) { + super(); + this.emit = true; + if (testType === CompilerTestType.Conformance) { + this.testSuiteName = "conformance"; } - - public kind() { - return this.testSuiteName; + else if (testType === CompilerTestType.Regressions) { + this.testSuiteName = "compiler"; } - - public enumerateTestFiles() { - // see also: `enumerateTestFiles` in tests/webTestServer.ts - return this.enumerateFiles(this.basePath, /\.tsx?$/, { recursive: true }).map(CompilerTest.getConfigurations); + else if (testType === CompilerTestType.Test262) { + this.testSuiteName = "test262"; } - - public initializeTests() { - describe(this.testSuiteName + " tests", () => { - describe("Setup compiler for compiler baselines", () => { - this.parseOptions(); - }); - - // this will set up a series of describe/it blocks to run between the setup and cleanup phases - const files = this.tests.length > 0 ? this.tests : IO.enumerateTestFiles(this); - files.forEach(test => { - const file = typeof test === "string" ? test : test.file; - this.checkTestCodeOutput(vpath.normalizeSeparators(file), typeof test === "string" ? CompilerTest.getConfigurations(test) : test); - }); - }); + else { + this.testSuiteName = "compiler"; // default to this for historical reasons } - - public checkTestCodeOutput(fileName: string, test?: CompilerFileBasedTest) { - if (test && ts.some(test.configurations)) { - test.configurations.forEach(configuration => { - describe(`${this.testSuiteName} tests for ${fileName}${configuration ? ` (${getFileBasedTestConfigurationDescription(configuration)})` : ``}`, () => { - this.runSuite(fileName, test, configuration); - }); - }); - } - else { - describe(`${this.testSuiteName} tests for ${fileName}`, () => { - this.runSuite(fileName, test); + this.basePath += "/" + this.testSuiteName; + } + public kind() { + return this.testSuiteName; + } + public enumerateTestFiles() { + // see also: `enumerateTestFiles` in tests/webTestServer.ts + return this.enumerateFiles(this.basePath, /\.tsx?$/, { recursive: true }).map(CompilerTest.getConfigurations); + } + public initializeTests() { + describe(this.testSuiteName + " tests", () => { + describe("Setup compiler for compiler baselines", () => { + this.parseOptions(); + }); + // this will set up a series of describe/it blocks to run between the setup and cleanup phases + const files = this.tests.length > 0 ? this.tests : IO.enumerateTestFiles(this); + files.forEach(test => { + const file = typeof test === "string" ? test : test.file; + this.checkTestCodeOutput(normalizeSeparators(file), typeof test === "string" ? CompilerTest.getConfigurations(test) : test); + }); + }); + } + public checkTestCodeOutput(fileName: string, test?: CompilerFileBasedTest) { + if (test && some(test.configurations)) { + test.configurations.forEach(configuration => { + describe(`${this.testSuiteName} tests for ${fileName}${configuration ? ` (${getFileBasedTestConfigurationDescription(configuration)})` : ``}`, () => { + this.runSuite(fileName, test, configuration); }); - } + }); } - - private runSuite(fileName: string, test?: CompilerFileBasedTest, configuration?: FileBasedTestConfiguration) { - // Mocha holds onto the closure environment of the describe callback even after the test is done. - // Everything declared here should be cleared out in the "after" callback. - let compilerTest!: CompilerTest; - before(() => { - let payload; - if (test && test.content) { - const rootDir = test.file.indexOf("conformance") === -1 ? "tests/cases/compiler/" : ts.getDirectoryPath(test.file) + "/"; - payload = TestCaseParser.makeUnitsFromTest(test.content, test.file, rootDir); - } - compilerTest = new CompilerTest(fileName, payload, configuration); + else { + describe(`${this.testSuiteName} tests for ${fileName}`, () => { + this.runSuite(fileName, test); }); - it(`Correct errors for ${fileName}`, () => { compilerTest.verifyDiagnostics(); }); - it(`Correct module resolution tracing for ${fileName}`, () => { compilerTest.verifyModuleResolution(); }); - it(`Correct sourcemap content for ${fileName}`, () => { compilerTest.verifySourceMapRecord(); }); - it(`Correct JS output for ${fileName}`, () => { if (this.emit) compilerTest.verifyJavaScriptOutput(); }); - it(`Correct Sourcemap output for ${fileName}`, () => { compilerTest.verifySourceMapOutput(); }); - it(`Correct type/symbol baselines for ${fileName}`, () => { compilerTest.verifyTypesAndSymbols(); }); - after(() => { compilerTest = undefined!; }); } - - private parseOptions() { - if (this.options && this.options.length > 0) { - this.emit = false; - - const opts = this.options.split(","); - for (const opt of opts) { - switch (opt) { - case "emit": - this.emit = true; - break; - default: - throw new Error("unsupported flag"); - } + } + private runSuite(fileName: string, test?: CompilerFileBasedTest, configuration?: FileBasedTestConfiguration) { + // Mocha holds onto the closure environment of the describe callback even after the test is done. + // Everything declared here should be cleared out in the "after" callback. + let compilerTest!: CompilerTest; + before(() => { + let payload; + if (test && test.content) { + const rootDir = test.file.indexOf("conformance") === -1 ? "tests/cases/compiler/" : getDirectoryPath(test.file) + "/"; + payload = TestCaseParser.makeUnitsFromTest(test.content, test.file, rootDir); + } + compilerTest = new CompilerTest(fileName, payload, configuration); + }); + it(`Correct errors for ${fileName}`, () => { compilerTest.verifyDiagnostics(); }); + it(`Correct module resolution tracing for ${fileName}`, () => { compilerTest.verifyModuleResolution(); }); + it(`Correct sourcemap content for ${fileName}`, () => { compilerTest.verifySourceMapRecord(); }); + it(`Correct JS output for ${fileName}`, () => { + if (this.emit) + compilerTest.verifyJavaScriptOutput(); + }); + it(`Correct Sourcemap output for ${fileName}`, () => { compilerTest.verifySourceMapOutput(); }); + it(`Correct type/symbol baselines for ${fileName}`, () => { compilerTest.verifyTypesAndSymbols(); }); + after(() => { compilerTest = undefined!; }); + } + private parseOptions() { + if (this.options && this.options.length > 0) { + this.emit = false; + const opts = this.options.split(","); + for (const opt of opts) { + switch (opt) { + case "emit": + this.emit = true; + break; + default: + throw new Error("unsupported flag"); } } } } - - class CompilerTest { - private static varyBy: readonly string[] = [ - "module", - "target", - "jsx", - "removeComments", - "importHelpers", - "importHelpers", - "downlevelIteration", - "isolatedModules", - "strict", - "noImplicitAny", - "strictNullChecks", - "strictFunctionTypes", - "strictBindCallApply", - "strictPropertyInitialization", - "noImplicitThis", - "alwaysStrict", - "allowSyntheticDefaultImports", - "esModuleInterop", - "emitDecoratorMetadata", - "skipDefaultLibCheck", - "preserveConstEnums", - "skipLibCheck", - ]; - private fileName: string; - private justName: string; - private configuredName: string; - private lastUnit: TestCaseParser.TestUnitData; - private harnessSettings: TestCaseParser.CompilerSettings; - private hasNonDtsFiles: boolean; - private result: compiler.CompilationResult; - private options: ts.CompilerOptions; - private tsConfigFiles: Compiler.TestFile[]; - // equivalent to the files that will be passed on the command line - private toBeCompiled: Compiler.TestFile[]; - // equivalent to other files on the file system not directly passed to the compiler (ie things that are referenced by other files) - private otherFiles: Compiler.TestFile[]; - - constructor(fileName: string, testCaseContent?: TestCaseParser.TestCaseContent, configurationOverrides?: TestCaseParser.CompilerSettings) { - this.fileName = fileName; - this.justName = vpath.basename(fileName); - this.configuredName = this.justName; - if (configurationOverrides) { - let configuredName = ""; - const keys = Object - .keys(configurationOverrides) - .map(k => k.toLowerCase()) - .sort(); - for (const key of keys) { - if (configuredName) { - configuredName += ","; - } - configuredName += `${key}=${configurationOverrides[key].toLowerCase()}`; - } +} +class CompilerTest { + private static varyBy: readonly string[] = [ + "module", + "target", + "jsx", + "removeComments", + "importHelpers", + "importHelpers", + "downlevelIteration", + "isolatedModules", + "strict", + "noImplicitAny", + "strictNullChecks", + "strictFunctionTypes", + "strictBindCallApply", + "strictPropertyInitialization", + "noImplicitThis", + "alwaysStrict", + "allowSyntheticDefaultImports", + "esModuleInterop", + "emitDecoratorMetadata", + "skipDefaultLibCheck", + "preserveConstEnums", + "skipLibCheck", + ]; + private fileName: string; + private justName: string; + private configuredName: string; + private lastUnit: TestCaseParser.TestUnitData; + private harnessSettings: TestCaseParser.CompilerSettings; + private hasNonDtsFiles: boolean; + private result: CompilationResult; + private options: CompilerOptions; + private tsConfigFiles: Compiler.TestFile[]; + // equivalent to the files that will be passed on the command line + private toBeCompiled: Compiler.TestFile[]; + // equivalent to other files on the file system not directly passed to the compiler (ie things that are referenced by other files) + private otherFiles: Compiler.TestFile[]; + constructor(fileName: string, testCaseContent?: TestCaseParser.TestCaseContent, configurationOverrides?: TestCaseParser.CompilerSettings) { + this.fileName = fileName; + this.justName = basename(fileName); + this.configuredName = this.justName; + if (configurationOverrides) { + let configuredName = ""; + const keys = Object + .keys(configurationOverrides) + .map(k => k.toLowerCase()) + .sort(); + for (const key of keys) { if (configuredName) { - const extname = vpath.extname(this.justName); - const basename = vpath.basename(this.justName, extname, /*ignoreCase*/ true); - this.configuredName = `${basename}(${configuredName})${extname}`; - } - } - - const rootDir = fileName.indexOf("conformance") === -1 ? "tests/cases/compiler/" : ts.getDirectoryPath(fileName) + "/"; - - if (testCaseContent === undefined) { - testCaseContent = TestCaseParser.makeUnitsFromTest(IO.readFile(fileName)!, fileName, rootDir); - } - - if (configurationOverrides) { - testCaseContent = { ...testCaseContent, settings: { ...testCaseContent.settings, ...configurationOverrides } }; - } - - const units = testCaseContent.testUnitData; - this.harnessSettings = testCaseContent.settings; - let tsConfigOptions: ts.CompilerOptions | undefined; - this.tsConfigFiles = []; - if (testCaseContent.tsConfig) { - assert.equal(testCaseContent.tsConfig.fileNames.length, 0, `list of files in tsconfig is not currently supported`); - assert.equal(testCaseContent.tsConfig.raw.exclude, undefined, `exclude in tsconfig is not currently supported`); - - tsConfigOptions = ts.cloneCompilerOptions(testCaseContent.tsConfig.options); - this.tsConfigFiles.push(this.createHarnessTestFile(testCaseContent.tsConfigFileUnitData!, rootDir, ts.combinePaths(rootDir, tsConfigOptions.configFilePath))); - } - else { - const baseUrl = this.harnessSettings.baseUrl; - if (baseUrl !== undefined && !ts.isRootedDiskPath(baseUrl)) { - this.harnessSettings.baseUrl = ts.getNormalizedAbsolutePath(baseUrl, rootDir); + configuredName += ","; } + configuredName += `${key}=${configurationOverrides[key].toLowerCase()}`; } - - this.lastUnit = units[units.length - 1]; - this.hasNonDtsFiles = units.some(unit => !ts.fileExtensionIs(unit.name, ts.Extension.Dts)); - // We need to assemble the list of input files for the compiler and other related files on the 'filesystem' (ie in a multi-file test) - // If the last file in a test uses require or a triple slash reference we'll assume all other files will be brought in via references, - // otherwise, assume all files are just meant to be in the same compilation session without explicit references to one another. - this.toBeCompiled = []; - this.otherFiles = []; - - if (testCaseContent.settings.noImplicitReferences || /require\(/.test(this.lastUnit.content) || /reference\spath/.test(this.lastUnit.content)) { - this.toBeCompiled.push(this.createHarnessTestFile(this.lastUnit, rootDir)); - units.forEach(unit => { - if (unit.name !== this.lastUnit.name) { - this.otherFiles.push(this.createHarnessTestFile(unit, rootDir)); - } - }); - } - else { - this.toBeCompiled = units.map(unit => { - return this.createHarnessTestFile(unit, rootDir); - }); - } - - if (tsConfigOptions && tsConfigOptions.configFilePath !== undefined) { - tsConfigOptions.configFilePath = ts.combinePaths(rootDir, tsConfigOptions.configFilePath); - tsConfigOptions.configFile!.fileName = tsConfigOptions.configFilePath; + if (configuredName) { + const extname = vpath.extname(this.justName); + const basename = vpath.basename(this.justName, extname, /*ignoreCase*/ true); + this.configuredName = `${basename}(${configuredName})${extname}`; } - - this.result = Compiler.compileFiles( - this.toBeCompiled, - this.otherFiles, - this.harnessSettings, - /*options*/ tsConfigOptions, - /*currentDirectory*/ this.harnessSettings.currentDirectory, - testCaseContent.symlinks - ); - - this.options = this.result.options; } - - public static getConfigurations(file: string): CompilerFileBasedTest { - // also see `parseCompilerTestConfigurations` in tests/webTestServer.ts - const content = IO.readFile(file)!; - const settings = TestCaseParser.extractCompilerSettings(content); - const configurations = getFileBasedTestConfigurations(settings, CompilerTest.varyBy); - return { file, configurations, content }; + const rootDir = fileName.indexOf("conformance") === -1 ? "tests/cases/compiler/" : getDirectoryPath(fileName) + "/"; + if (testCaseContent === undefined) { + testCaseContent = TestCaseParser.makeUnitsFromTest((IO.readFile(fileName)!), fileName, rootDir); } - - public verifyDiagnostics() { - // check errors - Compiler.doErrorBaseline( - this.configuredName, - this.tsConfigFiles.concat(this.toBeCompiled, this.otherFiles), - this.result.diagnostics, - !!this.options.pretty); + if (configurationOverrides) { + testCaseContent = { ...testCaseContent, settings: { ...testCaseContent.settings, ...configurationOverrides } }; } - - public verifyModuleResolution() { - if (this.options.traceResolution) { - Baseline.runBaseline(this.configuredName.replace(/\.tsx?$/, ".trace.json"), - JSON.stringify(this.result.traces.map(Utils.sanitizeTraceResolutionLogEntry), undefined, 4)); - } + const units = testCaseContent.testUnitData; + this.harnessSettings = testCaseContent.settings; + let tsConfigOptions: CompilerOptions | undefined; + this.tsConfigFiles = []; + if (testCaseContent.tsConfig) { + assert.equal(testCaseContent.tsConfig.fileNames.length, 0, `list of files in tsconfig is not currently supported`); + assert.equal(testCaseContent.tsConfig.raw.exclude, undefined, `exclude in tsconfig is not currently supported`); + tsConfigOptions = cloneCompilerOptions(testCaseContent.tsConfig.options); + this.tsConfigFiles.push(this.createHarnessTestFile((testCaseContent.tsConfigFileUnitData!), rootDir, combinePaths(rootDir, tsConfigOptions.configFilePath))); } - - public verifySourceMapRecord() { - if (this.options.sourceMap || this.options.inlineSourceMap || this.options.declarationMap) { - const record = Utils.removeTestPathPrefixes(this.result.getSourceMapRecord()!); - const baseline = (this.options.noEmitOnError && this.result.diagnostics.length !== 0) || record === undefined - // Because of the noEmitOnError option no files are created. We need to return null because baselining isn't required. - ? null // eslint-disable-line no-null/no-null - : record; - Baseline.runBaseline(this.configuredName.replace(/\.tsx?$/, ".sourcemap.txt"), baseline); + else { + const baseUrl = this.harnessSettings.baseUrl; + if (baseUrl !== undefined && !isRootedDiskPath(baseUrl)) { + this.harnessSettings.baseUrl = getNormalizedAbsolutePath(baseUrl, rootDir); } } - - public verifyJavaScriptOutput() { - if (this.hasNonDtsFiles) { - Compiler.doJsEmitBaseline( - this.configuredName, - this.fileName, - this.options, - this.result, - this.tsConfigFiles, - this.toBeCompiled, - this.otherFiles, - this.harnessSettings); - } + this.lastUnit = units[units.length - 1]; + this.hasNonDtsFiles = units.some(unit => !fileExtensionIs(unit.name, Extension.Dts)); + // We need to assemble the list of input files for the compiler and other related files on the 'filesystem' (ie in a multi-file test) + // If the last file in a test uses require or a triple slash reference we'll assume all other files will be brought in via references, + // otherwise, assume all files are just meant to be in the same compilation session without explicit references to one another. + this.toBeCompiled = []; + this.otherFiles = []; + if (testCaseContent.settings.noImplicitReferences || /require\(/.test(this.lastUnit.content) || /reference\spath/.test(this.lastUnit.content)) { + this.toBeCompiled.push(this.createHarnessTestFile(this.lastUnit, rootDir)); + units.forEach(unit => { + if (unit.name !== this.lastUnit.name) { + this.otherFiles.push(this.createHarnessTestFile(unit, rootDir)); + } + }); } - - public verifySourceMapOutput() { - Compiler.doSourcemapBaseline( - this.configuredName, - this.options, - this.result, - this.harnessSettings); + else { + this.toBeCompiled = units.map(unit => { + return this.createHarnessTestFile(unit, rootDir); + }); } - - public verifyTypesAndSymbols() { - if (this.fileName.indexOf("APISample") >= 0) { - return; - } - - const noTypesAndSymbols = - this.harnessSettings.noTypesAndSymbols && - this.harnessSettings.noTypesAndSymbols.toLowerCase() === "true"; - if (noTypesAndSymbols) { - return; - } - - Compiler.doTypeAndSymbolBaseline( - this.configuredName, - this.result.program!, - this.toBeCompiled.concat(this.otherFiles).filter(file => !!this.result.program!.getSourceFile(file.unitName)), - /*opts*/ undefined, - /*multifile*/ undefined, - /*skipTypeBaselines*/ undefined, - /*skipSymbolBaselines*/ undefined, - !!ts.length(this.result.diagnostics) - ); + if (tsConfigOptions && tsConfigOptions.configFilePath !== undefined) { + tsConfigOptions.configFilePath = combinePaths(rootDir, tsConfigOptions.configFilePath); + tsConfigOptions.configFile!.fileName = tsConfigOptions.configFilePath; } - - private makeUnitName(name: string, root: string) { - const path = ts.toPath(name, root, ts.identity); - const pathStart = ts.toPath(IO.getCurrentDirectory(), "", ts.identity); - return pathStart ? path.replace(pathStart, "/") : path; + this.result = Compiler.compileFiles(this.toBeCompiled, this.otherFiles, this.harnessSettings, + /*options*/ tsConfigOptions, + /*currentDirectory*/ this.harnessSettings.currentDirectory, testCaseContent.symlinks); + this.options = this.result.options; + } + public static getConfigurations(file: string): CompilerFileBasedTest { + // also see `parseCompilerTestConfigurations` in tests/webTestServer.ts + const content = (IO.readFile(file)!); + const settings = TestCaseParser.extractCompilerSettings(content); + const configurations = getFileBasedTestConfigurations(settings, CompilerTest.varyBy); + return { file, configurations, content }; + } + public verifyDiagnostics() { + // check errors + Compiler.doErrorBaseline(this.configuredName, this.tsConfigFiles.concat(this.toBeCompiled, this.otherFiles), this.result.diagnostics, !!this.options.pretty); + } + public verifyModuleResolution() { + if (this.options.traceResolution) { + Baseline.runBaseline(this.configuredName.replace(/\.tsx?$/, ".trace.json"), JSON.stringify(this.result.traces.map(sanitizeTraceResolutionLogEntry), undefined, 4)); } - - private createHarnessTestFile(lastUnit: TestCaseParser.TestUnitData, rootDir: string, unitName?: string): Compiler.TestFile { - return { unitName: unitName || this.makeUnitName(lastUnit.name, rootDir), content: lastUnit.content, fileOptions: lastUnit.fileOptions }; + } + public verifySourceMapRecord() { + if (this.options.sourceMap || this.options.inlineSourceMap || this.options.declarationMap) { + const record = removeTestPathPrefixes((this.result.getSourceMapRecord()!)); + const baseline = (this.options.noEmitOnError && this.result.diagnostics.length !== 0) || record === undefined + // Because of the noEmitOnError option no files are created. We need to return null because baselining isn't required. + ? null // eslint-disable-line no-null/no-null + : record; + Baseline.runBaseline(this.configuredName.replace(/\.tsx?$/, ".sourcemap.txt"), baseline); + } + } + public verifyJavaScriptOutput() { + if (this.hasNonDtsFiles) { + Compiler.doJsEmitBaseline(this.configuredName, this.fileName, this.options, this.result, this.tsConfigFiles, this.toBeCompiled, this.otherFiles, this.harnessSettings); } } -} \ No newline at end of file + public verifySourceMapOutput() { + Compiler.doSourcemapBaseline(this.configuredName, this.options, this.result, this.harnessSettings); + } + public verifyTypesAndSymbols() { + if (this.fileName.indexOf("APISample") >= 0) { + return; + } + const noTypesAndSymbols = this.harnessSettings.noTypesAndSymbols && + this.harnessSettings.noTypesAndSymbols.toLowerCase() === "true"; + if (noTypesAndSymbols) { + return; + } + Compiler.doTypeAndSymbolBaseline(this.configuredName, (this.result.program!), this.toBeCompiled.concat(this.otherFiles).filter(file => !!this.result.program!.getSourceFile(file.unitName)), + /*opts*/ undefined, + /*multifile*/ undefined, + /*skipTypeBaselines*/ undefined, + /*skipSymbolBaselines*/ undefined, !!length(this.result.diagnostics)); + } + private makeUnitName(name: string, root: string) { + const path = toPath(name, root, identity); + const pathStart = toPath(IO.getCurrentDirectory(), "", identity); + return pathStart ? path.replace(pathStart, "/") : path; + } + private createHarnessTestFile(lastUnit: TestCaseParser.TestUnitData, rootDir: string, unitName?: string): Compiler.TestFile { + return { unitName: unitName || this.makeUnitName(lastUnit.name, rootDir), content: lastUnit.content, fileOptions: lastUnit.fileOptions }; + } +} diff --git a/src/testRunner/documents.ts b/src/testRunner/documents.ts new file mode 100644 index 0000000000000..0ea5a3e34680a --- /dev/null +++ b/src/testRunner/documents.ts @@ -0,0 +1,2 @@ +export * from "../harness/documents"; +export * from "./documentsRef"; diff --git a/src/testRunner/documentsRef.ts b/src/testRunner/documentsRef.ts index d3d92746b4ed7..9c6722ed62df3 100644 --- a/src/testRunner/documentsRef.ts +++ b/src/testRunner/documentsRef.ts @@ -1,2 +1,2 @@ // empty ref to documents so it can be referenced by unittests -namespace documents {} \ No newline at end of file +export {}; diff --git a/src/testRunner/evaluator.ts b/src/testRunner/evaluator.ts new file mode 100644 index 0000000000000..0c98f0103dd32 --- /dev/null +++ b/src/testRunner/evaluator.ts @@ -0,0 +1,2 @@ +export * from "../harness/evaluator"; +export * from "./evaluatorRef"; diff --git a/src/testRunner/evaluatorRef.ts b/src/testRunner/evaluatorRef.ts index cc81ec2402db5..74712e1d9c650 100644 --- a/src/testRunner/evaluatorRef.ts +++ b/src/testRunner/evaluatorRef.ts @@ -1,2 +1,2 @@ // empty ref to evaluator so it can be referenced by unittests -namespace evaluator {} \ No newline at end of file +export {}; diff --git a/src/testRunner/externalCompileRunner.ts b/src/testRunner/externalCompileRunner.ts index b720ba61f2c0f..dd21e4d18e5fe 100644 --- a/src/testRunner/externalCompileRunner.ts +++ b/src/testRunner/externalCompileRunner.ts @@ -1,345 +1,323 @@ -namespace Harness { - const fs: typeof import("fs") = require("fs"); - const path: typeof import("path") = require("path"); - const del: typeof import("del") = require("del"); - - interface ExecResult { - stdout: Buffer; - stderr: Buffer; - status: number | null; +import { RunnerBase, IO, isWorker, Baseline, TestRunnerKind } from "./Harness"; +import { Debug, flatten, comparePathsCaseSensitive, compareValues, compareStringsCaseSensitive, stringContains } from "./ts"; +const fs: typeof import("fs") = require("fs"); +const path: typeof import("path") = require("path"); +const del: typeof import("del") = require("del"); +interface ExecResult { + stdout: Buffer; + stderr: Buffer; + status: number | null; +} +interface UserConfig { + types: string[]; + cloneUrl: string; + path?: string; +} +abstract class ExternalCompileRunnerBase extends RunnerBase { + abstract testDir: string; + abstract report(result: ExecResult, cwd: string): string | null; + enumerateTestFiles() { + return IO.getDirectories(this.testDir); } - - interface UserConfig { - types: string[]; - cloneUrl: string; - path?: string; + /** Setup the runner's tests so that they are ready to be executed by the harness + * The first test should be a describe/it block that sets up the harness's compiler instance appropriately + */ + initializeTests(): void { + // Read in and evaluate the test list + const testList = this.tests && this.tests.length ? this.tests : this.getTestFiles(); + // eslint-disable-next-line @typescript-eslint/no-this-alias + const cls = this; + describe(`${this.kind()} code samples`, function (this: Mocha.ISuiteCallbackContext) { + this.timeout(600000); // 10 minutes + for (const test of testList) { + cls.runTest(typeof test === "string" ? test : test.file); + } + }); } - - abstract class ExternalCompileRunnerBase extends RunnerBase { - abstract testDir: string; - abstract report(result: ExecResult, cwd: string): string | null; - enumerateTestFiles() { - return IO.getDirectories(this.testDir); - } - /** Setup the runner's tests so that they are ready to be executed by the harness - * The first test should be a describe/it block that sets up the harness's compiler instance appropriately - */ - initializeTests(): void { - // Read in and evaluate the test list - const testList = this.tests && this.tests.length ? this.tests : this.getTestFiles(); - - // eslint-disable-next-line @typescript-eslint/no-this-alias - const cls = this; - describe(`${this.kind()} code samples`, function (this: Mocha.ISuiteCallbackContext) { - this.timeout(600_000); // 10 minutes - for (const test of testList) { - cls.runTest(typeof test === "string" ? test : test.file); + private runTest(directoryName: string) { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const cls = this; + const timeout = 600000; // 10 minutes + describe(directoryName, function (this: Mocha.ISuiteCallbackContext) { + this.timeout(timeout); + const cp: typeof import("child_process") = require("child_process"); + it("should build successfully", () => { + let cwd = path.join(IO.getWorkspaceRoot(), cls.testDir, directoryName); + const originalCwd = cwd; + const stdio = isWorker ? "pipe" : "inherit"; + let types: string[] | undefined; + if (fs.existsSync(path.join(cwd, "test.json"))) { + const config = JSON.parse(fs.readFileSync(path.join(cwd, "test.json"), { encoding: "utf8" })) as UserConfig; + Debug.assert(!!config.types, "Bad format from test.json: Types field must be present."); + Debug.assert(!!config.cloneUrl, "Bad format from test.json: cloneUrl field must be present."); + const submoduleDir = path.join(cwd, directoryName); + if (!fs.existsSync(submoduleDir)) { + exec("git", ["--work-tree", submoduleDir, "clone", config.cloneUrl, path.join(submoduleDir, ".git")], { cwd }); + } + else { + exec("git", ["--git-dir", path.join(submoduleDir, ".git"), "--work-tree", submoduleDir, "reset", "HEAD", "--hard"], { cwd: submoduleDir }); + exec("git", ["--git-dir", path.join(submoduleDir, ".git"), "--work-tree", submoduleDir, "clean", "-f"], { cwd: submoduleDir }); + exec("git", ["--git-dir", path.join(submoduleDir, ".git"), "--work-tree", submoduleDir, "pull", "-f"], { cwd: submoduleDir }); + } + types = config.types; + cwd = config.path ? path.join(cwd, config.path) : submoduleDir; } - }); - } - private runTest(directoryName: string) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - const cls = this; - const timeout = 600_000; // 10 minutes - describe(directoryName, function (this: Mocha.ISuiteCallbackContext) { - this.timeout(timeout); - const cp: typeof import("child_process") = require("child_process"); - - it("should build successfully", () => { - let cwd = path.join(IO.getWorkspaceRoot(), cls.testDir, directoryName); - const originalCwd = cwd; - const stdio = isWorker ? "pipe" : "inherit"; - let types: string[] | undefined; - if (fs.existsSync(path.join(cwd, "test.json"))) { - const config = JSON.parse(fs.readFileSync(path.join(cwd, "test.json"), { encoding: "utf8" })) as UserConfig; - ts.Debug.assert(!!config.types, "Bad format from test.json: Types field must be present."); - ts.Debug.assert(!!config.cloneUrl, "Bad format from test.json: cloneUrl field must be present."); - const submoduleDir = path.join(cwd, directoryName); - if (!fs.existsSync(submoduleDir)) { - exec("git", ["--work-tree", submoduleDir, "clone", config.cloneUrl, path.join(submoduleDir, ".git")], { cwd }); - } - else { - exec("git", ["--git-dir", path.join(submoduleDir, ".git"), "--work-tree", submoduleDir, "reset", "HEAD", "--hard"], { cwd: submoduleDir }); - exec("git", ["--git-dir", path.join(submoduleDir, ".git"), "--work-tree", submoduleDir, "clean", "-f"], { cwd: submoduleDir }); - exec("git", ["--git-dir", path.join(submoduleDir, ".git"), "--work-tree", submoduleDir, "pull", "-f"], { cwd: submoduleDir }); - } - - types = config.types; - - cwd = config.path ? path.join(cwd, config.path) : submoduleDir; + if (fs.existsSync(path.join(cwd, "package.json"))) { + if (fs.existsSync(path.join(cwd, "package-lock.json"))) { + fs.unlinkSync(path.join(cwd, "package-lock.json")); } - if (fs.existsSync(path.join(cwd, "package.json"))) { - if (fs.existsSync(path.join(cwd, "package-lock.json"))) { - fs.unlinkSync(path.join(cwd, "package-lock.json")); - } - if (fs.existsSync(path.join(cwd, "node_modules"))) { - del.sync(path.join(cwd, "node_modules"), { force: true }); - } - exec("npm", ["i", "--ignore-scripts"], { cwd, timeout: timeout / 2 }); // NPM shouldn't take the entire timeout - if it takes a long time, it should be terminated and we should log the failure + if (fs.existsSync(path.join(cwd, "node_modules"))) { + del.sync(path.join(cwd, "node_modules"), { force: true }); } - const args = [path.join(IO.getWorkspaceRoot(), "built/local/tsc.js")]; - if (types) { - args.push("--types", types.join(",")); - // Also actually install those types (for, eg, the js projects which need node) - if (types.length) { - exec("npm", ["i", ...types.map(t => `@types/${t}`), "--no-save", "--ignore-scripts"], { cwd: originalCwd, timeout: timeout / 2 }); // NPM shouldn't take the entire timeout - if it takes a long time, it should be terminated and we should log the failure - } + exec("npm", ["i", "--ignore-scripts"], { cwd, timeout: timeout / 2 }); // NPM shouldn't take the entire timeout - if it takes a long time, it should be terminated and we should log the failure + } + const args = [path.join(IO.getWorkspaceRoot(), "built/local/tsc.js")]; + if (types) { + args.push("--types", types.join(",")); + // Also actually install those types (for, eg, the js projects which need node) + if (types.length) { + exec("npm", ["i", ...types.map(t => `@types/${t}`), "--no-save", "--ignore-scripts"], { cwd: originalCwd, timeout: timeout / 2 }); // NPM shouldn't take the entire timeout - if it takes a long time, it should be terminated and we should log the failure } - args.push("--noEmit"); - Baseline.runBaseline(`${cls.kind()}/${directoryName}.log`, cls.report(cp.spawnSync(`node`, args, { cwd, timeout, shell: true }), cwd)); - - function exec(command: string, args: string[], options: { cwd: string, timeout?: number }): void { - const res = cp.spawnSync(isWorker ? `${command} 2>&1` : command, args, { shell: true, stdio, ...options }); - if (res.status !== 0) { - throw new Error(`${command} ${args.join(" ")} for ${directoryName} failed: ${res.stdout && res.stdout.toString()}`); - } + } + args.push("--noEmit"); + Baseline.runBaseline(`${cls.kind()}/${directoryName}.log`, cls.report(cp.spawnSync(`node`, args, { cwd, timeout, shell: true }), cwd)); + function exec(command: string, args: string[], options: { + cwd: string; + timeout?: number; + }): void { + const res = cp.spawnSync(isWorker ? `${command} 2>&1` : command, args, { shell: true, stdio, ...options }); + if (res.status !== 0) { + throw new Error(`${command} ${args.join(" ")} for ${directoryName} failed: ${res.stdout && res.stdout.toString()}`); } - }); + } }); - } + }); } - - export class UserCodeRunner extends ExternalCompileRunnerBase { - readonly testDir = "tests/cases/user/"; - kind(): TestRunnerKind { - return "user"; - } - report(result: ExecResult) { - // eslint-disable-next-line no-null/no-null - return result.status === 0 && !result.stdout.length && !result.stderr.length ? null : `Exit Code: ${result.status} +} +export class UserCodeRunner extends ExternalCompileRunnerBase { + readonly testDir = "tests/cases/user/"; + kind(): TestRunnerKind { + return "user"; + } + report(result: ExecResult) { + // eslint-disable-next-line no-null/no-null + return result.status === 0 && !result.stdout.length && !result.stderr.length ? null : `Exit Code: ${result.status} Standard output: ${sortErrors(stripAbsoluteImportPaths(result.stdout.toString().replace(/\r\n/g, "\n")))} Standard error: ${stripAbsoluteImportPaths(result.stderr.toString().replace(/\r\n/g, "\n"))}`; - } } - - export class DockerfileRunner extends ExternalCompileRunnerBase { - readonly testDir = "tests/cases/docker/"; - kind(): TestRunnerKind { - return "docker"; - } - initializeTests(): void { - // Read in and evaluate the test list - const testList = this.tests && this.tests.length ? this.tests : this.getTestFiles(); - - // eslint-disable-next-line @typescript-eslint/no-this-alias - const cls = this; - describe(`${this.kind()} code samples`, function (this: Mocha.ISuiteCallbackContext) { - this.timeout(cls.timeout); // 20 minutes - before(() => { - cls.exec("docker", ["build", ".", "-t", "typescript/typescript"], { cwd: IO.getWorkspaceRoot() }); // cached because workspace is hashed to determine cacheability - }); - for (const test of testList) { - const directory = typeof test === "string" ? test : test.file; - const cwd = path.join(IO.getWorkspaceRoot(), cls.testDir, directory); - it(`should build ${directory} successfully`, () => { - const imageName = `tstest/${directory}`; - cls.exec("docker", ["build", "--no-cache", ".", "-t", imageName], { cwd }); // --no-cache so the latest version of the repos referenced is always fetched - const cp: typeof import("child_process") = require("child_process"); - Baseline.runBaseline(`${cls.kind()}/${directory}.log`, cls.report(cp.spawnSync(`docker`, ["run", imageName], { cwd, timeout: cls.timeout, shell: true }))); - }); - } +} +export class DockerfileRunner extends ExternalCompileRunnerBase { + readonly testDir = "tests/cases/docker/"; + kind(): TestRunnerKind { + return "docker"; + } + initializeTests(): void { + // Read in and evaluate the test list + const testList = this.tests && this.tests.length ? this.tests : this.getTestFiles(); + // eslint-disable-next-line @typescript-eslint/no-this-alias + const cls = this; + describe(`${this.kind()} code samples`, function (this: Mocha.ISuiteCallbackContext) { + this.timeout(cls.timeout); // 20 minutes + before(() => { + cls.exec("docker", ["build", ".", "-t", "typescript/typescript"], { cwd: IO.getWorkspaceRoot() }); // cached because workspace is hashed to determine cacheability }); - } - - private timeout = 1_200_000; // 20 minutes; - private exec(command: string, args: string[], options: { cwd: string }): void { - const cp: typeof import("child_process") = require("child_process"); - const stdio = isWorker ? "pipe" : "inherit"; - const res = cp.spawnSync(isWorker ? `${command} 2>&1` : command, args, { timeout: this.timeout, shell: true, stdio, ...options }); - if (res.status !== 0) { - throw new Error(`${command} ${args.join(" ")} for ${options.cwd} failed: ${res.stdout && res.stdout.toString()}`); + for (const test of testList) { + const directory = typeof test === "string" ? test : test.file; + const cwd = path.join(IO.getWorkspaceRoot(), cls.testDir, directory); + it(`should build ${directory} successfully`, () => { + const imageName = `tstest/${directory}`; + cls.exec("docker", ["build", "--no-cache", ".", "-t", imageName], { cwd }); // --no-cache so the latest version of the repos referenced is always fetched + const cp: typeof import("child_process") = require("child_process"); + Baseline.runBaseline(`${cls.kind()}/${directory}.log`, cls.report(cp.spawnSync(`docker`, ["run", imageName], { cwd, timeout: cls.timeout, shell: true }))); + }); } + }); + } + private timeout = 1200000; // 20 minutes; + private exec(command: string, args: string[], options: { + cwd: string; + }): void { + const cp: typeof import("child_process") = require("child_process"); + const stdio = isWorker ? "pipe" : "inherit"; + const res = cp.spawnSync(isWorker ? `${command} 2>&1` : command, args, { timeout: this.timeout, shell: true, stdio, ...options }); + if (res.status !== 0) { + throw new Error(`${command} ${args.join(" ")} for ${options.cwd} failed: ${res.stdout && res.stdout.toString()}`); } - report(result: ExecResult) { - // eslint-disable-next-line no-null/no-null - return result.status === 0 && !result.stdout.length && !result.stderr.length ? null : `Exit Code: ${result.status} + } + report(result: ExecResult) { + // eslint-disable-next-line no-null/no-null + return result.status === 0 && !result.stdout.length && !result.stderr.length ? null : `Exit Code: ${result.status} Standard output: ${sanitizeDockerfileOutput(result.stdout.toString())} Standard error: ${sanitizeDockerfileOutput(result.stderr.toString())}`; - } - } - - function sanitizeDockerfileOutput(result: string): string { - return [ - normalizeNewlines, - stripANSIEscapes, - stripRushStageNumbers, - stripWebpackHash, - sanitizeVersionSpecifiers, - sanitizeTimestamps, - sanitizeSizes, - sanitizeUnimportantGulpOutput, - stripAbsoluteImportPaths, - ].reduce((result, f) => f(result), result); - } - - function normalizeNewlines(result: string): string { - return result.replace(/\r\n/g, "\n"); } - - function stripANSIEscapes(result: string): string { - return result.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, ""); - } - - function stripRushStageNumbers(result: string): string { - return result.replace(/\d+ of \d+:/g, "XX of XX:"); - } - - function stripWebpackHash(result: string): string { - return result.replace(/Hash: \w+/g, "Hash: [redacted]"); - } - - function sanitizeSizes(result: string): string { - return result.replace(/\d+(\.\d+)? ((Ki|M)B|bytes)/g, "X KiB"); - } - - /** - * Gulp's output order within a `parallel` block is nondeterministic (and there's no way to configure it to execute in series), - * so we purge as much of the gulp output as we can - */ - function sanitizeUnimportantGulpOutput(result: string): string { - return result.replace(/^.*(\] (Starting)|(Finished)).*$/gm, "") // "gulp" task start/end messages (nondeterministic order) - .replace(/^.*(\] . (finished)|(started)).*$/gm, "") // "just" task start/end messages (nondeterministic order) - .replace(/^.*\] Respawned to PID: \d+.*$/gm, "") // PID of child is OS and system-load dependent (likely stableish in a container but still dangerous) - .replace(/\n+/g, "\n") - .replace(/\/tmp\/yarn--.*?\/node/g, ""); - } - - function sanitizeTimestamps(result: string): string { - return result.replace(/\[\d?\d:\d\d:\d\d (A|P)M\]/g, "[XX:XX:XX XM]") - .replace(/\[\d?\d:\d\d:\d\d\]/g, "[XX:XX:XX]") - .replace(/\/\d+-\d+-[\d_TZ]+-debug.log/g, "\/XXXX-XX-XXXXXXXXX-debug.log") - .replace(/\d+\/\d+\/\d+ \d+:\d+:\d+ (AM|PM)/g, "XX/XX/XX XX:XX:XX XM") - .replace(/\d+(\.\d+)? sec(onds?)?/g, "? seconds") - .replace(/\d+(\.\d+)? min(utes?)?/g, "") - .replace(/\d+(\.\d+)? ?m?s/g, "?s") - .replace(/ \(\?s\)/g, ""); - } - - function sanitizeVersionSpecifiers(result: string): string { - return result - .replace(/\d+.\d+.\d+-insiders.\d\d\d\d\d\d\d\d/g, "X.X.X-insiders.xxxxxxxx") - .replace(/Rush Multi-Project Build Tool (\d+)\.\d+\.\d+/g, "Rush Multi-Project Build Tool $1.X.X") - .replace(/([@v\()])\d+\.\d+\.\d+/g, "$1X.X.X") - .replace(/webpack (\d+)\.\d+\.\d+/g, "webpack $1.X.X") - .replace(/Webpack version: (\d+)\.\d+\.\d+/g, "Webpack version: $1.X.X"); - } - - /** - * Import types and some other error messages use absolute paths in errors as they have no context to be written relative to; - * This is problematic for error baselines, so we grep for them and strip them out. - */ - function stripAbsoluteImportPaths(result: string) { - const workspaceRegexp = new RegExp(IO.getWorkspaceRoot().replace(/\\/g, "\\\\"), "g"); - return result - .replace(/import\(".*?\/tests\/cases\/user\//g, `import("/`) - .replace(/Module '".*?\/tests\/cases\/user\//g, `Module '"/`) - .replace(workspaceRegexp, "../../.."); +} +function sanitizeDockerfileOutput(result: string): string { + return [ + normalizeNewlines, + stripANSIEscapes, + stripRushStageNumbers, + stripWebpackHash, + sanitizeVersionSpecifiers, + sanitizeTimestamps, + sanitizeSizes, + sanitizeUnimportantGulpOutput, + stripAbsoluteImportPaths, + ].reduce((result, f) => f(result), result); +} +function normalizeNewlines(result: string): string { + return result.replace(/\r\n/g, "\n"); +} +function stripANSIEscapes(result: string): string { + return result.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, ""); +} +function stripRushStageNumbers(result: string): string { + return result.replace(/\d+ of \d+:/g, "XX of XX:"); +} +function stripWebpackHash(result: string): string { + return result.replace(/Hash: \w+/g, "Hash: [redacted]"); +} +function sanitizeSizes(result: string): string { + return result.replace(/\d+(\.\d+)? ((Ki|M)B|bytes)/g, "X KiB"); +} +/** + * Gulp's output order within a `parallel` block is nondeterministic (and there's no way to configure it to execute in series), + * so we purge as much of the gulp output as we can + */ +function sanitizeUnimportantGulpOutput(result: string): string { + return result.replace(/^.*(\] (Starting)|(Finished)).*$/gm, "") // "gulp" task start/end messages (nondeterministic order) + .replace(/^.*(\] . (finished)|(started)).*$/gm, "") // "just" task start/end messages (nondeterministic order) + .replace(/^.*\] Respawned to PID: \d+.*$/gm, "") // PID of child is OS and system-load dependent (likely stableish in a container but still dangerous) + .replace(/\n+/g, "\n") + .replace(/\/tmp\/yarn--.*?\/node/g, ""); +} +function sanitizeTimestamps(result: string): string { + return result.replace(/\[\d?\d:\d\d:\d\d (A|P)M\]/g, "[XX:XX:XX XM]") + .replace(/\[\d?\d:\d\d:\d\d\]/g, "[XX:XX:XX]") + .replace(/\/\d+-\d+-[\d_TZ]+-debug.log/g, "\/XXXX-XX-XXXXXXXXX-debug.log") + .replace(/\d+\/\d+\/\d+ \d+:\d+:\d+ (AM|PM)/g, "XX/XX/XX XX:XX:XX XM") + .replace(/\d+(\.\d+)? sec(onds?)?/g, "? seconds") + .replace(/\d+(\.\d+)? min(utes?)?/g, "") + .replace(/\d+(\.\d+)? ?m?s/g, "?s") + .replace(/ \(\?s\)/g, ""); +} +function sanitizeVersionSpecifiers(result: string): string { + return result + .replace(/\d+.\d+.\d+-insiders.\d\d\d\d\d\d\d\d/g, "X.X.X-insiders.xxxxxxxx") + .replace(/Rush Multi-Project Build Tool (\d+)\.\d+\.\d+/g, "Rush Multi-Project Build Tool $1.X.X") + .replace(/([@v\()])\d+\.\d+\.\d+/g, "$1X.X.X") + .replace(/webpack (\d+)\.\d+\.\d+/g, "webpack $1.X.X") + .replace(/Webpack version: (\d+)\.\d+\.\d+/g, "Webpack version: $1.X.X"); +} +/** + * Import types and some other error messages use absolute paths in errors as they have no context to be written relative to; + * This is problematic for error baselines, so we grep for them and strip them out. + */ +function stripAbsoluteImportPaths(result: string) { + const workspaceRegexp = new RegExp(IO.getWorkspaceRoot().replace(/\\/g, "\\\\"), "g"); + return result + .replace(/import\(".*?\/tests\/cases\/user\//g, `import("/`) + .replace(/Module '".*?\/tests\/cases\/user\//g, `Module '"/`) + .replace(workspaceRegexp, "../../.."); +} +function sortErrors(result: string) { + return flatten(splitBy(result.split("\n"), s => /^\S+/.test(s)).sort(compareErrorStrings)).join("\n"); +} +const errorRegexp = /^(.+\.[tj]sx?)\((\d+),(\d+)\)(: error TS.*)/; +function compareErrorStrings(a: string[], b: string[]) { + Debug.assertGreaterThanOrEqual(a.length, 1); + Debug.assertGreaterThanOrEqual(b.length, 1); + const matchA = a[0].match(errorRegexp); + if (!matchA) { + return -1; } - - function sortErrors(result: string) { - return ts.flatten(splitBy(result.split("\n"), s => /^\S+/.test(s)).sort(compareErrorStrings)).join("\n"); + const matchB = b[0].match(errorRegexp); + if (!matchB) { + return 1; } - - const errorRegexp = /^(.+\.[tj]sx?)\((\d+),(\d+)\)(: error TS.*)/; - function compareErrorStrings(a: string[], b: string[]) { - ts.Debug.assertGreaterThanOrEqual(a.length, 1); - ts.Debug.assertGreaterThanOrEqual(b.length, 1); - const matchA = a[0].match(errorRegexp); - if (!matchA) { - return -1; - } - const matchB = b[0].match(errorRegexp); - if (!matchB) { - return 1; - } - const [, errorFileA, lineNumberStringA, columnNumberStringA, remainderA] = matchA; - const [, errorFileB, lineNumberStringB, columnNumberStringB, remainderB] = matchB; - return ts.comparePathsCaseSensitive(errorFileA, errorFileB) || - ts.compareValues(parseInt(lineNumberStringA), parseInt(lineNumberStringB)) || - ts.compareValues(parseInt(columnNumberStringA), parseInt(columnNumberStringB)) || - ts.compareStringsCaseSensitive(remainderA, remainderB) || - ts.compareStringsCaseSensitive(a.slice(1).join("\n"), b.slice(1).join("\n")); + const [, errorFileA, lineNumberStringA, columnNumberStringA, remainderA] = matchA; + const [, errorFileB, lineNumberStringB, columnNumberStringB, remainderB] = matchB; + return comparePathsCaseSensitive(errorFileA, errorFileB) || + compareValues(parseInt(lineNumberStringA), parseInt(lineNumberStringB)) || + compareValues(parseInt(columnNumberStringA), parseInt(columnNumberStringB)) || + compareStringsCaseSensitive(remainderA, remainderB) || + compareStringsCaseSensitive(a.slice(1).join("\n"), b.slice(1).join("\n")); +} +export class DefinitelyTypedRunner extends ExternalCompileRunnerBase { + readonly testDir = "../DefinitelyTyped/types/"; + workingDirectory = this.testDir; + kind(): TestRunnerKind { + return "dt"; } - - export class DefinitelyTypedRunner extends ExternalCompileRunnerBase { - readonly testDir = "../DefinitelyTyped/types/"; - workingDirectory = this.testDir; - kind(): TestRunnerKind { - return "dt"; - } - report(result: ExecResult, cwd: string) { - const stdout = removeExpectedErrors(result.stdout.toString(), cwd); - const stderr = result.stderr.toString(); - - // eslint-disable-next-line no-null/no-null - return !stdout.length && !stderr.length ? null : `Exit Code: ${result.status} + report(result: ExecResult, cwd: string) { + const stdout = removeExpectedErrors(result.stdout.toString(), cwd); + const stderr = result.stderr.toString(); + // eslint-disable-next-line no-null/no-null + return !stdout.length && !stderr.length ? null : `Exit Code: ${result.status} Standard output: ${stdout.replace(/\r\n/g, "\n")} Standard error: ${stderr.replace(/\r\n/g, "\n")}`; - } - } - - function removeExpectedErrors(errors: string, cwd: string): string { - return ts.flatten(splitBy(errors.split("\n"), s => /^\S+/.test(s)).filter(isUnexpectedError(cwd))).join("\n"); - } - /** - * Returns true if the line that caused the error contains '$ExpectError', - * or if the line before that one contains '$ExpectError'. - * '$ExpectError' is a marker used in Definitely Typed tests, - * meaning that the error should not contribute toward our error baslines. - */ - function isUnexpectedError(cwd: string) { - return (error: string[]) => { - ts.Debug.assertGreaterThanOrEqual(error.length, 1); - const match = error[0].match(/(.+\.tsx?)\((\d+),\d+\): error TS/); - if (!match) { - return true; - } - const [, errorFile, lineNumberString] = match; - const lines = fs.readFileSync(path.join(cwd, errorFile), { encoding: "utf8" }).split("\n"); - const lineNumber = parseInt(lineNumberString) - 1; - ts.Debug.assertGreaterThanOrEqual(lineNumber, 0); - ts.Debug.assertLessThan(lineNumber, lines.length); - const previousLine = lineNumber - 1 > 0 ? lines[lineNumber - 1] : ""; - return !ts.stringContains(lines[lineNumber], "$ExpectError") && !ts.stringContains(previousLine, "$ExpectError"); - }; } - /** - * Split an array into multiple arrays whenever `isStart` returns true. - * @example - * splitBy([1,2,3,4,5,6], isOdd) - * ==> [[1, 2], [3, 4], [5, 6]] - * where - * const isOdd = n => !!(n % 2) - */ - function splitBy(xs: T[], isStart: (x: T) => boolean): T[][] { - const result = []; - let group: T[] = []; - for (const x of xs) { - if (isStart(x)) { - if (group.length) { - result.push(group); - } - group = [x]; - } - else { - group.push(x); +} +function removeExpectedErrors(errors: string, cwd: string): string { + return flatten(splitBy(errors.split("\n"), s => /^\S+/.test(s)).filter(isUnexpectedError(cwd))).join("\n"); +} +/** + * Returns true if the line that caused the error contains '$ExpectError', + * or if the line before that one contains '$ExpectError'. + * '$ExpectError' is a marker used in Definitely Typed tests, + * meaning that the error should not contribute toward our error baslines. + */ +function isUnexpectedError(cwd: string) { + return (error: string[]) => { + Debug.assertGreaterThanOrEqual(error.length, 1); + const match = error[0].match(/(.+\.tsx?)\((\d+),\d+\): error TS/); + if (!match) { + return true; + } + const [, errorFile, lineNumberString] = match; + const lines = fs.readFileSync(path.join(cwd, errorFile), { encoding: "utf8" }).split("\n"); + const lineNumber = parseInt(lineNumberString) - 1; + Debug.assertGreaterThanOrEqual(lineNumber, 0); + Debug.assertLessThan(lineNumber, lines.length); + const previousLine = lineNumber - 1 > 0 ? lines[lineNumber - 1] : ""; + return !stringContains(lines[lineNumber], "$ExpectError") && !stringContains(previousLine, "$ExpectError"); + }; +} +/** + * Split an array into multiple arrays whenever `isStart` returns true. + * @example + * splitBy([1,2,3,4,5,6], isOdd) + * ==> [[1, 2], [3, 4], [5, 6]] + * where + * const isOdd = n => !!(n % 2) + */ +function splitBy(xs: T[], isStart: (x: T) => boolean): T[][] { + const result = []; + let group: T[] = []; + for (const x of xs) { + if (isStart(x)) { + if (group.length) { + result.push(group); } + group = [x]; } - if (group.length) { - result.push(group); + else { + group.push(x); } - return result; } + if (group.length) { + result.push(group); + } + return result; } diff --git a/src/testRunner/fakes.ts b/src/testRunner/fakes.ts new file mode 100644 index 0000000000000..88c82ccce9682 --- /dev/null +++ b/src/testRunner/fakes.ts @@ -0,0 +1,2 @@ +export * from "../harness/fakes"; +export * from "./fakesRef"; diff --git a/src/testRunner/fakesRef.ts b/src/testRunner/fakesRef.ts index b19d4cc8c80ed..5514be55a0fb9 100644 --- a/src/testRunner/fakesRef.ts +++ b/src/testRunner/fakesRef.ts @@ -1,2 +1,2 @@ // empty ref to fakes so it can be referenced by unittests -namespace fakes {} \ No newline at end of file +export {}; diff --git a/src/testRunner/fourslashRef.ts b/src/testRunner/fourslashRef.ts index 23a50810a46af..0dae2ad927987 100644 --- a/src/testRunner/fourslashRef.ts +++ b/src/testRunner/fourslashRef.ts @@ -1,2 +1,2 @@ // empty ref to FourSlash so it can be referenced by unittests -namespace FourSlash {} \ No newline at end of file +export {}; diff --git a/src/testRunner/fourslashRunner.ts b/src/testRunner/fourslashRunner.ts index 4916ec6693f6e..bacf2356f7316 100644 --- a/src/testRunner/fourslashRunner.ts +++ b/src/testRunner/fourslashRunner.ts @@ -1,72 +1,66 @@ -namespace Harness { - export class FourSlashRunner extends RunnerBase { - protected basePath: string; - protected testSuiteName: TestRunnerKind; - - constructor(private testType: FourSlash.FourSlashTestType) { - super(); - switch (testType) { - case FourSlash.FourSlashTestType.Native: - this.basePath = "tests/cases/fourslash"; - this.testSuiteName = "fourslash"; - break; - case FourSlash.FourSlashTestType.Shims: - this.basePath = "tests/cases/fourslash/shims"; - this.testSuiteName = "fourslash-shims"; - break; - case FourSlash.FourSlashTestType.ShimsWithPreprocess: - this.basePath = "tests/cases/fourslash/shims-pp"; - this.testSuiteName = "fourslash-shims-pp"; - break; - case FourSlash.FourSlashTestType.Server: - this.basePath = "tests/cases/fourslash/server"; - this.testSuiteName = "fourslash-server"; - break; - default: - throw ts.Debug.assertNever(testType); - } +import { RunnerBase, TestRunnerKind, IO } from "./Harness"; +import { FourSlashTestType, runFourSlashTest } from "./FourSlash"; +import { Debug, normalizeSlashes } from "./ts"; +export class FourSlashRunner extends RunnerBase { + protected basePath: string; + protected testSuiteName: TestRunnerKind; + constructor(private testType: FourSlashTestType) { + super(); + switch (testType) { + case FourSlashTestType.Native: + this.basePath = "tests/cases/fourslash"; + this.testSuiteName = "fourslash"; + break; + case FourSlashTestType.Shims: + this.basePath = "tests/cases/fourslash/shims"; + this.testSuiteName = "fourslash-shims"; + break; + case FourSlashTestType.ShimsWithPreprocess: + this.basePath = "tests/cases/fourslash/shims-pp"; + this.testSuiteName = "fourslash-shims-pp"; + break; + case FourSlashTestType.Server: + this.basePath = "tests/cases/fourslash/server"; + this.testSuiteName = "fourslash-server"; + break; + default: + throw Debug.assertNever(testType); } - - public enumerateTestFiles() { - // see also: `enumerateTestFiles` in tests/webTestServer.ts - return this.enumerateFiles(this.basePath, /\.ts/i, { recursive: false }); - } - - public kind() { - return this.testSuiteName; + } + public enumerateTestFiles() { + // see also: `enumerateTestFiles` in tests/webTestServer.ts + return this.enumerateFiles(this.basePath, /\.ts/i, { recursive: false }); + } + public kind() { + return this.testSuiteName; + } + public initializeTests() { + if (this.tests.length === 0) { + this.tests = IO.enumerateTestFiles(this); } - - public initializeTests() { - if (this.tests.length === 0) { - this.tests = IO.enumerateTestFiles(this); - } - - describe(this.testSuiteName + " tests", () => { - this.tests.forEach(test => { - const file = typeof test === "string" ? test : test.file; - describe(file, () => { - let fn = ts.normalizeSlashes(file); - const justName = fn.replace(/^.*[\\\/]/, ""); - - // Convert to relative path - const testIndex = fn.indexOf("tests/"); - if (testIndex >= 0) fn = fn.substr(testIndex); - - if (justName && !justName.match(/fourslash\.ts$/i) && !justName.match(/\.d\.ts$/i)) { - it(this.testSuiteName + " test " + justName + " runs correctly", () => { - FourSlash.runFourSlashTest(this.basePath, this.testType, fn); - }); - } - }); + describe(this.testSuiteName + " tests", () => { + this.tests.forEach(test => { + const file = typeof test === "string" ? test : test.file; + describe(file, () => { + let fn = normalizeSlashes(file); + const justName = fn.replace(/^.*[\\\/]/, ""); + // Convert to relative path + const testIndex = fn.indexOf("tests/"); + if (testIndex >= 0) + fn = fn.substr(testIndex); + if (justName && !justName.match(/fourslash\.ts$/i) && !justName.match(/\.d\.ts$/i)) { + it(this.testSuiteName + " test " + justName + " runs correctly", () => { + runFourSlashTest(this.basePath, this.testType, fn); + }); + } }); }); - } + }); } - - export class GeneratedFourslashRunner extends FourSlashRunner { - constructor(testType: FourSlash.FourSlashTestType) { - super(testType); - this.basePath += "/generated/"; - } +} +export class GeneratedFourslashRunner extends FourSlashRunner { + constructor(testType: FourSlashTestType) { + super(testType); + this.basePath += "/generated/"; } } diff --git a/src/testRunner/parallel/host.ts b/src/testRunner/parallel/host.ts index bb4b31b9f058d..835b8a9e8a1fa 100644 --- a/src/testRunner/parallel/host.ts +++ b/src/testRunner/parallel/host.ts @@ -1,629 +1,589 @@ -namespace Harness.Parallel.Host { - export function start() { - const Mocha = require("mocha") as typeof import("mocha"); - const Base = Mocha.reporters.Base; - const color = Base.color; - const cursor = Base.cursor; - const ms = require("ms") as typeof import("ms"); - const readline = require("readline") as typeof import("readline"); - const os = require("os") as typeof import("os"); - const tty = require("tty") as typeof import("tty"); - const isatty = tty.isatty(1) && tty.isatty(2); - const path = require("path") as typeof import("path"); - const { fork } = require("child_process") as typeof import("child_process"); - const { statSync } = require("fs") as typeof import("fs"); - - // NOTE: paths for module and types for FailedTestReporter _do not_ line up due to our use of --outFile for run.js - const FailedTestReporter = require(path.resolve(__dirname, "../../scripts/failed-tests")) as typeof import("../../../scripts/failed-tests"); - - const perfdataFileNameFragment = ".parallelperf"; - const perfData = readSavedPerfData(configOption); - const newTasks: Task[] = []; - let tasks: Task[] = []; - let unknownValue: string | undefined; - let totalCost = 0; - - class RemoteSuite extends Mocha.Suite { - suiteMap = ts.createMap(); - constructor(title: string) { - super(title); - this.pending = false; - this.delayed = false; - } - addSuite(suite: RemoteSuite) { - super.addSuite(suite); - this.suiteMap.set(suite.title, suite); - return this; - } - addTest(test: RemoteTest) { - return super.addTest(test); - } +import { configOption, IO, TestRunnerKind, runners, workerCount, noColors, TestConfig, lightMode, runUnitTests, stackTraceLimit, globalTimeout, taskConfigsFolder, keepFailed } from "../Harness"; +import { Task, ErrorInfo, TestInfo, TaskTimeout, ParallelClientMessage, ParallelHostMessage, shimNoopTestInterface } from "../Harness.Parallel"; +import { createMap, combinePaths, Debug } from "../ts"; +export function start() { + const Mocha = require("mocha") as typeof import("mocha"); + const Base = Mocha.reporters.Base; + const color = Base.color; + const cursor = Base.cursor; + const ms = require("ms") as typeof import("ms"); + const readline = require("readline") as typeof import("readline"); + const os = require("os") as typeof import("os"); + const tty = require("tty") as typeof import("tty"); + const isatty = tty.isatty(1) && tty.isatty(2); + const path = require("path") as typeof import("path"); + const { fork } = require("child_process") as typeof import("child_process"); + const { statSync } = require("fs") as typeof import("fs"); + // NOTE: paths for module and types for FailedTestReporter _do not_ line up due to our use of --outFile for run.js + const FailedTestReporter = require(path.resolve(__dirname, "../../scripts/failed-tests")) as typeof import("../../../scripts/failed-tests"); + const perfdataFileNameFragment = ".parallelperf"; + const perfData = readSavedPerfData(configOption); + const newTasks: Task[] = []; + let tasks: Task[] = []; + let unknownValue: string | undefined; + let totalCost = 0; + class RemoteSuite extends Mocha.Suite { + suiteMap = createMap(); + constructor(title: string) { + super(title); + this.pending = false; + this.delayed = false; } - - class RemoteTest extends Mocha.Test { - info: ErrorInfo | TestInfo; - constructor(info: ErrorInfo | TestInfo) { - super(info.name[info.name.length - 1]); - this.info = info; - this.state = "error" in info ? "failed" : "passed"; // eslint-disable-line no-in-operator - this.pending = false; - } + addSuite(suite: RemoteSuite) { + super.addSuite(suite); + this.suiteMap.set(suite.title, suite); + return this; } - - interface Worker { - process: import("child_process").ChildProcess; - accumulatedOutput: string; - currentTasks?: { file: string }[]; - timer?: any; + addTest(test: RemoteTest) { + return super.addTest(test); } - - interface ProgressBarsOptions { - open: string; - close: string; - complete: string; - incomplete: string; - width: number; - noColors: boolean; + } + class RemoteTest extends Mocha.Test { + info: ErrorInfo | TestInfo; + constructor(info: ErrorInfo | TestInfo) { + super(info.name[info.name.length - 1]); + this.info = info; + this.state = "error" in info ? "failed" : "passed"; // eslint-disable-line no-in-operator + this.pending = false; + } + } + interface Worker { + process: import("child_process").ChildProcess; + accumulatedOutput: string; + currentTasks?: { + file: string; + }[]; + timer?: any; + } + interface ProgressBarsOptions { + open: string; + close: string; + complete: string; + incomplete: string; + width: number; + noColors: boolean; + } + interface ProgressBar { + lastN?: number; + title?: string; + progressColor?: string; + text?: string; + } + class ProgressBars { + public readonly _options: Readonly; + private _enabled: boolean; + private _lineCount: number; + private _progressBars: ProgressBar[]; + constructor(options?: Partial) { + if (!options) + options = {}; + const open = options.open || "["; + const close = options.close || "]"; + const complete = options.complete || "▬"; + const incomplete = options.incomplete || Base.symbols.dot; + const maxWidth = Base.window.width - open.length - close.length - 34; + const width = minMax(options.width || maxWidth, 10, maxWidth); + this._options = { + open, + complete, + incomplete, + close, + width, + noColors: options.noColors || false + }; + this._progressBars = []; + this._lineCount = 0; + this._enabled = false; } - - interface ProgressBar { - lastN?: number; - title?: string; - progressColor?: string; - text?: string; + enable() { + if (!this._enabled) { + process.stdout.write(os.EOL); + this._enabled = true; + } } - - class ProgressBars { - public readonly _options: Readonly; - private _enabled: boolean; - private _lineCount: number; - private _progressBars: ProgressBar[]; - constructor(options?: Partial) { - if (!options) options = {}; - const open = options.open || "["; - const close = options.close || "]"; - const complete = options.complete || "▬"; - const incomplete = options.incomplete || Base.symbols.dot; - const maxWidth = Base.window.width - open.length - close.length - 34; - const width = minMax(options.width || maxWidth, 10, maxWidth); - this._options = { - open, - complete, - incomplete, - close, - width, - noColors: options.noColors || false - }; - - this._progressBars = []; - this._lineCount = 0; + disable() { + if (this._enabled) { + process.stdout.write(os.EOL); this._enabled = false; } - enable() { - if (!this._enabled) { - process.stdout.write(os.EOL); - this._enabled = true; - } + } + update(index: number, percentComplete: number, color: string, title: string | undefined, titleColor?: string) { + percentComplete = minMax(percentComplete, 0, 1); + const progressBar = this._progressBars[index] || (this._progressBars[index] = {}); + const width = this._options.width; + const n = Math.floor(width * percentComplete); + const i = width - n; + if (n === progressBar.lastN && title === progressBar.title && color === progressBar.progressColor) { + return; } - disable() { - if (this._enabled) { - process.stdout.write(os.EOL); - this._enabled = false; - } + progressBar.lastN = n; + progressBar.title = title; + progressBar.progressColor = color; + let progress = " "; + progress += this._color("progress", this._options.open); + progress += this._color(color, fill(this._options.complete, n)); + progress += this._color("progress", fill(this._options.incomplete, i)); + progress += this._color("progress", this._options.close); + if (title) { + progress += this._color(titleColor || "progress", " " + title); } - update(index: number, percentComplete: number, color: string, title: string | undefined, titleColor?: string) { - percentComplete = minMax(percentComplete, 0, 1); - - const progressBar = this._progressBars[index] || (this._progressBars[index] = {}); - const width = this._options.width; - const n = Math.floor(width * percentComplete); - const i = width - n; - if (n === progressBar.lastN && title === progressBar.title && color === progressBar.progressColor) { - return; - } - - progressBar.lastN = n; - progressBar.title = title; - progressBar.progressColor = color; - - let progress = " "; - progress += this._color("progress", this._options.open); - progress += this._color(color, fill(this._options.complete, n)); - progress += this._color("progress", fill(this._options.incomplete, i)); - progress += this._color("progress", this._options.close); - - if (title) { - progress += this._color(titleColor || "progress", " " + title); - } - - if (progressBar.text !== progress) { - progressBar.text = progress; - this._render(index); - } + if (progressBar.text !== progress) { + progressBar.text = progress; + this._render(index); } - private _render(index: number) { - if (!this._enabled || !isatty) { - return; + } + private _render(index: number) { + if (!this._enabled || !isatty) { + return; + } + cursor.hide(); + readline.moveCursor(process.stdout, -process.stdout.columns, -this._lineCount); + let lineCount = 0; + const numProgressBars = this._progressBars.length; + for (let i = 0; i < numProgressBars; i++) { + if (i === index) { + readline.clearLine(process.stdout, 1); + process.stdout.write(this._progressBars[i].text + os.EOL); } - - cursor.hide(); - readline.moveCursor(process.stdout, -process.stdout.columns, -this._lineCount); - let lineCount = 0; - const numProgressBars = this._progressBars.length; - for (let i = 0; i < numProgressBars; i++) { - if (i === index) { - readline.clearLine(process.stdout, 1); - process.stdout.write(this._progressBars[i].text + os.EOL); - } - else { - readline.moveCursor(process.stdout, -process.stdout.columns, +1); - } - - lineCount++; + else { + readline.moveCursor(process.stdout, -process.stdout.columns, +1); } - - this._lineCount = lineCount; - cursor.show(); + lineCount++; } - private _color(type: string, text: string) { - return type && !this._options.noColors ? color(type, text) : text; - } - } - - function perfdataFileName(target?: string) { - return `${perfdataFileNameFragment}${target ? `.${target}` : ""}.json`; + this._lineCount = lineCount; + cursor.show(); } - - function readSavedPerfData(target?: string): { [testHash: string]: number } | undefined { - const perfDataContents = IO.readFile(perfdataFileName(target)); - if (perfDataContents) { - return JSON.parse(perfDataContents); - } - return undefined; + private _color(type: string, text: string) { + return type && !this._options.noColors ? color(type, text) : text; } - - function hashName(runner: TestRunnerKind | "unittest", test: string) { - return `tsrunner-${runner}://${test}`; + } + function perfdataFileName(target?: string) { + return `${perfdataFileNameFragment}${target ? `.${target}` : ""}.json`; + } + function readSavedPerfData(target?: string): { + [testHash: string]: number; + } | undefined { + const perfDataContents = IO.readFile(perfdataFileName(target)); + if (perfDataContents) { + return JSON.parse(perfDataContents); } - - function startDelayed(perfData: { [testHash: string]: number } | undefined, totalCost: number) { - console.log(`Discovered ${tasks.length} unittest suites` + (newTasks.length ? ` and ${newTasks.length} new suites.` : ".")); - console.log("Discovering runner-based tests..."); - const discoverStart = +(new Date()); - for (const runner of runners) { - for (const test of runner.getTestFiles()) { - const file = typeof test === "string" ? test : test.file; - let size: number; - if (!perfData) { + return undefined; + } + function hashName(runner: TestRunnerKind | "unittest", test: string) { + return `tsrunner-${runner}://${test}`; + } + function startDelayed(perfData: { + [testHash: string]: number; + } | undefined, totalCost: number) { + console.log(`Discovered ${tasks.length} unittest suites` + (newTasks.length ? ` and ${newTasks.length} new suites.` : ".")); + console.log("Discovering runner-based tests..."); + const discoverStart = +(new Date()); + for (const runner of runners) { + for (const test of runner.getTestFiles()) { + const file = typeof test === "string" ? test : test.file; + let size: number; + if (!perfData) { + try { + size = statSync(path.join(runner.workingDirectory, file)).size; + } + catch { + // May be a directory try { - size = statSync(path.join(runner.workingDirectory, file)).size; + size = IO.listFiles(path.join(runner.workingDirectory, file), /.*/g, { recursive: true }).reduce((acc, elem) => acc + statSync(elem).size, 0); } catch { - // May be a directory - try { - size = IO.listFiles(path.join(runner.workingDirectory, file), /.*/g, { recursive: true }).reduce((acc, elem) => acc + statSync(elem).size, 0); - } - catch { - // Unknown test kind, just return 0 and let the historical analysis take over after one run - size = 0; - } - } - } - else { - const hashedName = hashName(runner.kind(), file); - size = perfData[hashedName]; - if (size === undefined) { + // Unknown test kind, just return 0 and let the historical analysis take over after one run size = 0; - unknownValue = hashedName; - newTasks.push({ runner: runner.kind(), file, size }); - continue; } } - tasks.push({ runner: runner.kind(), file, size }); - totalCost += size; } + else { + const hashedName = hashName(runner.kind(), file); + size = perfData[hashedName]; + if (size === undefined) { + size = 0; + unknownValue = hashedName; + newTasks.push({ runner: runner.kind(), file, size }); + continue; + } + } + tasks.push({ runner: runner.kind(), file, size }); + totalCost += size; } - tasks.sort((a, b) => a.size - b.size); - tasks = tasks.concat(newTasks); - const batchCount = workerCount; - const packfraction = 0.9; - const chunkSize = 1000; // ~1KB or 1s for sending batches near the end of a test - const batchSize = (totalCost / workerCount) * packfraction; // Keep spare tests for unittest thread in reserve - console.log(`Discovered ${tasks.length} test files in ${+(new Date()) - discoverStart}ms.`); - console.log(`Starting to run tests using ${workerCount} threads...`); - - const totalFiles = tasks.length; - let passingFiles = 0; - let failingFiles = 0; - let errorResults: ErrorInfo[] = []; - let passingResults: { name: string[] }[] = []; - let totalPassing = 0; - const startDate = new Date(); - - const progressBars = new ProgressBars({ noColors: Harness.noColors }); // eslint-disable-line @typescript-eslint/no-unnecessary-qualifier - const progressUpdateInterval = 1 / progressBars._options.width; - let nextProgress = progressUpdateInterval; - - const newPerfData: { [testHash: string]: number } = {}; - - const workers: Worker[] = []; - let closedWorkers = 0; - for (let i = 0; i < workerCount; i++) { - // TODO: Just send the config over the IPC channel or in the command line arguments - const config: TestConfig = { light: lightMode, listenForWork: true, runUnitTests: Harness.runUnitTests, stackTraceLimit: Harness.stackTraceLimit, timeout: globalTimeout }; // eslint-disable-line @typescript-eslint/no-unnecessary-qualifier - const configPath = ts.combinePaths(taskConfigsFolder, `task-config${i}.json`); - IO.writeFile(configPath, JSON.stringify(config)); - const worker: Worker = { - process: fork(__filename, [`--config="${configPath}"`], { stdio: ["pipe", "pipe", "pipe", "ipc"] }), - accumulatedOutput: "", - currentTasks: undefined, - timer: undefined - }; - const appendOutput = (d: Buffer) => { - worker.accumulatedOutput += d.toString(); - console.log(`[Worker ${i}]`, d.toString()); - }; - worker.process.stderr!.on("data", appendOutput); - worker.process.stdout!.on("data", appendOutput); - const killChild = (timeout: TaskTimeout) => { - worker.process.kill(); - console.error(`Worker exceeded ${timeout.duration}ms timeout ${worker.currentTasks && worker.currentTasks.length ? `while running test '${worker.currentTasks[0].file}'.` : `during test setup.`}`); - return process.exit(2); - }; - worker.process.on("error", err => { - console.error("Unexpected error in child process:"); - console.error(err); - return process.exit(2); - }); - worker.process.on("exit", (code, _signal) => { - if (code !== 0) { - console.error(`Test worker process exited with nonzero exit code! Output: + } + tasks.sort((a, b) => a.size - b.size); + tasks = tasks.concat(newTasks); + const batchCount = workerCount; + const packfraction = 0.9; + const chunkSize = 1000; // ~1KB or 1s for sending batches near the end of a test + const batchSize = (totalCost / workerCount) * packfraction; // Keep spare tests for unittest thread in reserve + console.log(`Discovered ${tasks.length} test files in ${+(new Date()) - discoverStart}ms.`); + console.log(`Starting to run tests using ${workerCount} threads...`); + const totalFiles = tasks.length; + let passingFiles = 0; + let failingFiles = 0; + let errorResults: ErrorInfo[] = []; + let passingResults: { + name: string[]; + }[] = []; + let totalPassing = 0; + const startDate = new Date(); + const progressBars = new ProgressBars({ noColors: noColors }); // eslint-disable-line @typescript-eslint/no-unnecessary-qualifier + const progressUpdateInterval = 1 / progressBars._options.width; + let nextProgress = progressUpdateInterval; + const newPerfData: { + [testHash: string]: number; + } = {}; + const workers: Worker[] = []; + let closedWorkers = 0; + for (let i = 0; i < workerCount; i++) { + // TODO: Just send the config over the IPC channel or in the command line arguments + const config: TestConfig = { light: lightMode, listenForWork: true, runUnitTests: runUnitTests, stackTraceLimit: stackTraceLimit, timeout: globalTimeout }; // eslint-disable-line @typescript-eslint/no-unnecessary-qualifier + const configPath = combinePaths(taskConfigsFolder, `task-config${i}.json`); + IO.writeFile(configPath, JSON.stringify(config)); + const worker: Worker = { + process: fork(__filename, [`--config="${configPath}"`], { stdio: ["pipe", "pipe", "pipe", "ipc"] }), + accumulatedOutput: "", + currentTasks: undefined, + timer: undefined + }; + const appendOutput = (d: Buffer) => { + worker.accumulatedOutput += d.toString(); + console.log(`[Worker ${i}]`, d.toString()); + }; + worker.process.stderr!.on("data", appendOutput); + worker.process.stdout!.on("data", appendOutput); + const killChild = (timeout: TaskTimeout) => { + worker.process.kill(); + console.error(`Worker exceeded ${timeout.duration}ms timeout ${worker.currentTasks && worker.currentTasks.length ? `while running test '${worker.currentTasks[0].file}'.` : `during test setup.`}`); + return process.exit(2); + }; + worker.process.on("error", err => { + console.error("Unexpected error in child process:"); + console.error(err); + return process.exit(2); + }); + worker.process.on("exit", (code, _signal) => { + if (code !== 0) { + console.error(`Test worker process exited with nonzero exit code! Output: ${worker.accumulatedOutput}`); - return process.exit(2); - } - }); - worker.process.on("message", (data: ParallelClientMessage) => { - switch (data.type) { - case "error": { - console.error(`Test worker encounted unexpected error${data.payload.name ? ` during the execution of test ${data.payload.name}` : ""} and was forced to close: + return process.exit(2); + } + }); + worker.process.on("message", (data: ParallelClientMessage) => { + switch (data.type) { + case "error": { + console.error(`Test worker encounted unexpected error${data.payload.name ? ` during the execution of test ${data.payload.name}` : ""} and was forced to close: Message: ${data.payload.error} Stack: ${data.payload.stack}`); - return process.exit(2); + return process.exit(2); + } + case "timeout": { + if (worker.timer) { + // eslint-disable-next-line no-restricted-globals + clearTimeout(worker.timer); } - case "timeout": { - if (worker.timer) { - // eslint-disable-next-line no-restricted-globals - clearTimeout(worker.timer); - } - if (data.payload.duration === "reset") { - worker.timer = undefined; - } - else { - // eslint-disable-next-line no-restricted-globals - worker.timer = setTimeout(killChild, data.payload.duration, data.payload); - } - break; + if (data.payload.duration === "reset") { + worker.timer = undefined; } - case "progress": - case "result": { - if (worker.currentTasks) { - worker.currentTasks.shift(); - } - totalPassing += data.payload.passing; - if (data.payload.errors.length) { - errorResults = errorResults.concat(data.payload.errors); - passingResults = passingResults.concat(data.payload.passes); - failingFiles++; - } - else { - passingResults = passingResults.concat(data.payload.passes); - passingFiles++; - } - newPerfData[hashName(data.payload.task.runner, data.payload.task.file)] = data.payload.duration; - - const progress = (failingFiles + passingFiles) / totalFiles; - if (progress >= nextProgress) { - while (nextProgress < progress) { - nextProgress += progressUpdateInterval; - } - updateProgress(progress, errorResults.length ? `${errorResults.length} failing` : `${totalPassing} passing`, errorResults.length ? "fail" : undefined); - } - - if (data.type === "result") { - if (tasks.length === 0) { - // No more tasks to distribute - worker.process.send({ type: "close" }); - closedWorkers++; - if (closedWorkers === workerCount) { - outputFinalResult(); - } - return; - } - // Send tasks in blocks if the tasks are small - const taskList = [tasks.pop()!]; - while (tasks.length && taskList.reduce((p, c) => p + c.size, 0) < chunkSize) { - taskList.push(tasks.pop()!); - } - worker.currentTasks = taskList; - if (taskList.length === 1) { - worker.process.send({ type: "test", payload: taskList[0] } as ParallelHostMessage); // TODO: GH#18217 - } - else { - worker.process.send({ type: "batch", payload: taskList } as ParallelHostMessage); // TODO: GH#18217 - } - } + else { + // eslint-disable-next-line no-restricted-globals + worker.timer = setTimeout(killChild, data.payload.duration, data.payload); } + break; } - }); - workers.push(worker); - } - - // It's only really worth doing an initial batching if there are a ton of files to go through (and they have estimates) - if (totalFiles > 1000 && batchSize > 0) { - console.log("Batching initial test lists..."); - const batches: { runner: TestRunnerKind | "unittest", file: string, size: number }[][] = new Array(batchCount); - const doneBatching = new Array(batchCount); - let scheduledTotal = 0; - batcher: while (true) { - for (let i = 0; i < batchCount; i++) { - if (tasks.length <= workerCount) { // Keep a small reserve even in the suboptimally packed case - console.log(`Suboptimal packing detected: no tests remain to be stolen. Reduce packing fraction from ${packfraction} to fix.`); - break batcher; + case "progress": + case "result": { + if (worker.currentTasks) { + worker.currentTasks.shift(); } - if (doneBatching[i]) { - continue; + totalPassing += data.payload.passing; + if (data.payload.errors.length) { + errorResults = errorResults.concat(data.payload.errors); + passingResults = passingResults.concat(data.payload.passes); + failingFiles++; } - if (!batches[i]) { - batches[i] = []; + else { + passingResults = passingResults.concat(data.payload.passes); + passingFiles++; } - const total = batches[i].reduce((p, c) => p + c.size, 0); - if (total >= batchSize) { - doneBatching[i] = true; - continue; + newPerfData[hashName(data.payload.task.runner, data.payload.task.file)] = data.payload.duration; + const progress = (failingFiles + passingFiles) / totalFiles; + if (progress >= nextProgress) { + while (nextProgress < progress) { + nextProgress += progressUpdateInterval; + } + updateProgress(progress, errorResults.length ? `${errorResults.length} failing` : `${totalPassing} passing`, errorResults.length ? "fail" : undefined); } - const task = tasks.pop()!; - batches[i].push(task); - scheduledTotal += task.size; - } - for (let j = 0; j < batchCount; j++) { - if (!doneBatching[j]) { - continue batcher; + if (data.type === "result") { + if (tasks.length === 0) { + // No more tasks to distribute + worker.process.send({ type: "close" }); + closedWorkers++; + if (closedWorkers === workerCount) { + outputFinalResult(); + } + return; + } + // Send tasks in blocks if the tasks are small + const taskList = [tasks.pop()!]; + while (tasks.length && taskList.reduce((p, c) => p + c.size, 0) < chunkSize) { + taskList.push(tasks.pop()!); + } + worker.currentTasks = taskList; + if (taskList.length === 1) { + worker.process.send(({ type: "test", payload: taskList[0] } as ParallelHostMessage)); // TODO: GH#18217 + } + else { + worker.process.send(({ type: "batch", payload: taskList } as ParallelHostMessage)); // TODO: GH#18217 + } } } - break; - } - const prefix = `Batched into ${batchCount} groups`; - if (unknownValue) { - console.log(`${prefix}. Unprofiled tests including ${unknownValue} will be run first.`); - } - else { - console.log(`${prefix} with approximate total ${perfData ? "time" : "file sizes"} of ${perfData ? ms(batchSize) : `${Math.floor(batchSize)} bytes`} in each group. (${(scheduledTotal / totalCost * 100).toFixed(1)}% of total tests batched)`); } - for (const worker of workers) { - const payload = batches.pop(); - if (payload) { - worker.currentTasks = payload; - worker.process.send({ type: "batch", payload }); + }); + workers.push(worker); + } + // It's only really worth doing an initial batching if there are a ton of files to go through (and they have estimates) + if (totalFiles > 1000 && batchSize > 0) { + console.log("Batching initial test lists..."); + const batches: { + runner: TestRunnerKind | "unittest"; + file: string; + size: number; + }[][] = new Array(batchCount); + const doneBatching = new Array(batchCount); + let scheduledTotal = 0; + batcher: while (true) { + for (let i = 0; i < batchCount; i++) { + if (tasks.length <= workerCount) { // Keep a small reserve even in the suboptimally packed case + console.log(`Suboptimal packing detected: no tests remain to be stolen. Reduce packing fraction from ${packfraction} to fix.`); + break batcher; } - else { // Out of batches, send off just one test - const payload = tasks.pop()!; - ts.Debug.assert(!!payload); // The reserve kept above should ensure there is always an initial task available, even in suboptimal scenarios - worker.currentTasks = [payload]; - worker.process.send({ type: "test", payload }); + if (doneBatching[i]) { + continue; + } + if (!batches[i]) { + batches[i] = []; + } + const total = batches[i].reduce((p, c) => p + c.size, 0); + if (total >= batchSize) { + doneBatching[i] = true; + continue; } - } - } - else { - for (let i = 0; i < workerCount; i++) { const task = tasks.pop()!; - workers[i].currentTasks = [task]; - workers[i].process.send({ type: "test", payload: task }); + batches[i].push(task); + scheduledTotal += task.size; + } + for (let j = 0; j < batchCount; j++) { + if (!doneBatching[j]) { + continue batcher; + } } + break; } - - progressBars.enable(); - updateProgress(0); - let duration: number; - let endDate: Date; - - function completeBar() { - const isPartitionFail = failingFiles !== 0; - const summaryColor = isPartitionFail ? "fail" : "green"; - const summarySymbol = isPartitionFail ? Base.symbols.err : Base.symbols.ok; - - const summaryTests = (isPartitionFail ? totalPassing + "/" + (errorResults.length + totalPassing) : totalPassing) + " passing"; - const summaryDuration = "(" + ms(duration) + ")"; - const savedUseColors = Base.useColors; - Base.useColors = !noColors; - - const summary = color(summaryColor, summarySymbol + " " + summaryTests) + " " + color("light", summaryDuration); - Base.useColors = savedUseColors; - - updateProgress(1, summary); + const prefix = `Batched into ${batchCount} groups`; + if (unknownValue) { + console.log(`${prefix}. Unprofiled tests including ${unknownValue} will be run first.`); } - - function updateProgress(percentComplete: number, title?: string, titleColor?: string) { - let progressColor = "pending"; - if (failingFiles) { - progressColor = "fail"; - } - - progressBars.update( - 0, - percentComplete, - progressColor, - title, - titleColor - ); + else { + console.log(`${prefix} with approximate total ${perfData ? "time" : "file sizes"} of ${perfData ? ms(batchSize) : `${Math.floor(batchSize)} bytes`} in each group. (${(scheduledTotal / totalCost * 100).toFixed(1)}% of total tests batched)`); } - - function outputFinalResult() { - function patchStats(stats: Mocha.Stats) { - Object.defineProperties(stats, { - start: { - configurable: true, enumerable: true, - get() { return startDate; }, - set(_: Date) { /*do nothing*/ } - }, - end: { - configurable: true, enumerable: true, - get() { return endDate; }, - set(_: Date) { /*do nothing*/ } - }, - duration: { - configurable: true, enumerable: true, - get() { return duration; }, - set(_: number) { /*do nothing*/ } - } - }); + for (const worker of workers) { + const payload = batches.pop(); + if (payload) { + worker.currentTasks = payload; + worker.process.send({ type: "batch", payload }); } - - function rebuildSuite(failures: ErrorInfo[], passes: TestInfo[]) { - const root = new RemoteSuite(""); - for (const result of [...failures, ...passes] as (ErrorInfo | TestInfo)[]) { - getSuite(root, result.name.slice(0, -1)).addTest(new RemoteTest(result)); - } - return root; - function getSuite(parent: RemoteSuite, titlePath: string[]): Mocha.Suite { - const title = titlePath[0]; - let suite = parent.suiteMap.get(title); - if (!suite) parent.addSuite(suite = new RemoteSuite(title)); - return titlePath.length === 1 ? suite : getSuite(suite, titlePath.slice(1)); - } - } - - function rebuildError(result: ErrorInfo) { - const error = new Error(result.error); - error.stack = result.stack; - return error; - } - - function replaySuite(runner: Mocha.Runner, suite: RemoteSuite) { - runner.emit("suite", suite); - for (const test of suite.tests) { - replayTest(runner, test as RemoteTest); - } - for (const child of suite.suites) { - replaySuite(runner, child as RemoteSuite); - } - runner.emit("suite end", suite); + else { // Out of batches, send off just one test + const payload = tasks.pop()!; + Debug.assert(!!payload); // The reserve kept above should ensure there is always an initial task available, even in suboptimal scenarios + worker.currentTasks = [payload]; + worker.process.send({ type: "test", payload }); } - - function replayTest(runner: Mocha.Runner, test: RemoteTest) { - runner.emit("test", test); - if (test.isFailed()) { - runner.emit("fail", test, "error" in test.info ? rebuildError(test.info) : new Error("Unknown error")); // eslint-disable-line no-in-operator - } - else { - runner.emit("pass", test); + } + } + else { + for (let i = 0; i < workerCount; i++) { + const task = tasks.pop()!; + workers[i].currentTasks = [task]; + workers[i].process.send({ type: "test", payload: task }); + } + } + progressBars.enable(); + updateProgress(0); + let duration: number; + let endDate: Date; + function completeBar() { + const isPartitionFail = failingFiles !== 0; + const summaryColor = isPartitionFail ? "fail" : "green"; + const summarySymbol = isPartitionFail ? Base.symbols.err : Base.symbols.ok; + const summaryTests = (isPartitionFail ? totalPassing + "/" + (errorResults.length + totalPassing) : totalPassing) + " passing"; + const summaryDuration = "(" + ms(duration) + ")"; + const savedUseColors = Base.useColors; + Base.useColors = !noColors; + const summary = color(summaryColor, summarySymbol + " " + summaryTests) + " " + color("light", summaryDuration); + Base.useColors = savedUseColors; + updateProgress(1, summary); + } + function updateProgress(percentComplete: number, title?: string, titleColor?: string) { + let progressColor = "pending"; + if (failingFiles) { + progressColor = "fail"; + } + progressBars.update(0, percentComplete, progressColor, title, titleColor); + } + function outputFinalResult() { + function patchStats(stats: Mocha.Stats) { + Object.defineProperties(stats, { + start: { + configurable: true, enumerable: true, + get() { return startDate; }, + set(_: Date) { } + }, + end: { + configurable: true, enumerable: true, + get() { return endDate; }, + set(_: Date) { } + }, + duration: { + configurable: true, enumerable: true, + get() { return duration; }, + set(_: number) { } } - runner.emit("test end", test); + }); + } + function rebuildSuite(failures: ErrorInfo[], passes: TestInfo[]) { + const root = new RemoteSuite(""); + for (const result of [...failures, ...passes] as (ErrorInfo | TestInfo)[]) { + getSuite(root, result.name.slice(0, -1)).addTest(new RemoteTest(result)); } - - endDate = new Date(); - duration = +endDate - +startDate; - completeBar(); - progressBars.disable(); - - const replayRunner = new Mocha.Runner(new Mocha.Suite(""), /*delay*/ false); - replayRunner.started = true; - const createStatsCollector = require("mocha/lib/stats-collector"); - createStatsCollector(replayRunner); // manually init stats collector like mocha.run would - - const consoleReporter = new Base(replayRunner); - patchStats(consoleReporter.stats); - - let xunitReporter: import("mocha").reporters.XUnit | undefined; - let failedTestReporter: import("../../../scripts/failed-tests") | undefined; - if (process.env.CI === "true") { - xunitReporter = new Mocha.reporters.XUnit(replayRunner, { - reporterOptions: { - suiteName: "Tests", - output: "./TEST-results.xml" - } - }); - patchStats(xunitReporter.stats); - xunitReporter.write(`\n`); + return root; + function getSuite(parent: RemoteSuite, titlePath: string[]): Mocha.Suite { + const title = titlePath[0]; + let suite = parent.suiteMap.get(title); + if (!suite) + parent.addSuite(suite = new RemoteSuite(title)); + return titlePath.length === 1 ? suite : getSuite(suite, titlePath.slice(1)); } - else { - failedTestReporter = new FailedTestReporter(replayRunner, { - reporterOptions: { - file: path.resolve(".failed-tests"), - keepFailed: Harness.keepFailed // eslint-disable-line @typescript-eslint/no-unnecessary-qualifier - } - }); + } + function rebuildError(result: ErrorInfo) { + const error = new Error(result.error); + error.stack = result.stack; + return error; + } + function replaySuite(runner: Mocha.Runner, suite: RemoteSuite) { + runner.emit("suite", suite); + for (const test of suite.tests) { + replayTest(runner, test as RemoteTest); } - - const savedUseColors = Base.useColors; - if (noColors) Base.useColors = false; - replayRunner.started = true; - replayRunner.emit("start"); - replaySuite(replayRunner, rebuildSuite(errorResults, passingResults)); - replayRunner.emit("end"); - consoleReporter.epilogue(); - if (noColors) Base.useColors = savedUseColors; - - // eslint-disable-next-line no-null/no-null - IO.writeFile(perfdataFileName(configOption), JSON.stringify(newPerfData, null, 4)); - - if (xunitReporter) { - xunitReporter.done(errorResults.length, failures => process.exit(failures)); + for (const child of suite.suites) { + replaySuite(runner, child as RemoteSuite); } - else if (failedTestReporter) { - failedTestReporter.done(errorResults.length, failures => process.exit(failures)); + runner.emit("suite end", suite); + } + function replayTest(runner: Mocha.Runner, test: RemoteTest) { + runner.emit("test", test); + if (test.isFailed()) { + runner.emit("fail", test, "error" in test.info ? rebuildError(test.info) : new Error("Unknown error")); // eslint-disable-line no-in-operator } else { - process.exit(errorResults.length); + runner.emit("pass", test); } + runner.emit("test end", test); } - } - - function fill(ch: string, size: number) { - let s = ""; - while (s.length < size) { - s += ch; + endDate = new Date(); + duration = +endDate - +startDate; + completeBar(); + progressBars.disable(); + const replayRunner = new Mocha.Runner(new Mocha.Suite(""), /*delay*/ false); + replayRunner.started = true; + const createStatsCollector = require("mocha/lib/stats-collector"); + createStatsCollector(replayRunner); // manually init stats collector like mocha.run would + const consoleReporter = new Base(replayRunner); + patchStats(consoleReporter.stats); + let xunitReporter: import("mocha").reporters.XUnit | undefined; + let failedTestReporter: import("../../../scripts/failed-tests") | undefined; + if (process.env.CI === "true") { + xunitReporter = new Mocha.reporters.XUnit(replayRunner, { + reporterOptions: { + suiteName: "Tests", + output: "./TEST-results.xml" + } + }); + patchStats(xunitReporter.stats); + xunitReporter.write(`\n`); } - - return s.length > size ? s.substr(0, size) : s; - } - - function minMax(value: number, min: number, max: number) { - if (value < min) return min; - if (value > max) return max; - return value; - } - - function shimDiscoveryInterface(context: Mocha.MochaGlobals) { - shimNoopTestInterface(context); - - const perfData = readSavedPerfData(configOption); - context.describe = addSuite as Mocha.SuiteFunction; - context.it = addSuite as Mocha.TestFunction; - - function addSuite(title: string) { - // Note, sub-suites are not indexed (we assume such granularity is not required) - let size = 0; - if (perfData) { - size = perfData[hashName("unittest", title)]; - if (size === undefined) { - newTasks.push({ runner: "unittest", file: title, size: 0 }); - unknownValue = title; - return; + else { + failedTestReporter = new FailedTestReporter(replayRunner, { + reporterOptions: { + file: path.resolve(".failed-tests"), + keepFailed: keepFailed // eslint-disable-line @typescript-eslint/no-unnecessary-qualifier } - } - tasks.push({ runner: "unittest", file: title, size }); - totalCost += size; + }); + } + const savedUseColors = Base.useColors; + if (noColors) + Base.useColors = false; + replayRunner.started = true; + replayRunner.emit("start"); + replaySuite(replayRunner, rebuildSuite(errorResults, passingResults)); + replayRunner.emit("end"); + consoleReporter.epilogue(); + if (noColors) + Base.useColors = savedUseColors; + // eslint-disable-next-line no-null/no-null + IO.writeFile(perfdataFileName(configOption), JSON.stringify(newPerfData, null, 4)); + if (xunitReporter) { + xunitReporter.done(errorResults.length, failures => process.exit(failures)); + } + else if (failedTestReporter) { + failedTestReporter.done(errorResults.length, failures => process.exit(failures)); + } + else { + process.exit(errorResults.length); } } - - if (runUnitTests) { - shimDiscoveryInterface(global); + } + function fill(ch: string, size: number) { + let s = ""; + while (s.length < size) { + s += ch; } - else { - shimNoopTestInterface(global); + return s.length > size ? s.substr(0, size) : s; + } + function minMax(value: number, min: number, max: number) { + if (value < min) + return min; + if (value > max) + return max; + return value; + } + function shimDiscoveryInterface(context: Mocha.MochaGlobals) { + shimNoopTestInterface(context); + const perfData = readSavedPerfData(configOption); + context.describe = addSuite as Mocha.SuiteFunction; + context.it = addSuite as Mocha.TestFunction; + function addSuite(title: string) { + // Note, sub-suites are not indexed (we assume such granularity is not required) + let size = 0; + if (perfData) { + size = perfData[hashName("unittest", title)]; + if (size === undefined) { + newTasks.push({ runner: "unittest", file: title, size: 0 }); + unknownValue = title; + return; + } + } + tasks.push({ runner: "unittest", file: title, size }); + totalCost += size; } - - // eslint-disable-next-line no-restricted-globals - setTimeout(() => startDelayed(perfData, totalCost), 0); // Do real startup on next tick, so all unit tests have been collected } + if (runUnitTests) { + shimDiscoveryInterface(global); + } + else { + shimNoopTestInterface(global); + } + // eslint-disable-next-line no-restricted-globals + setTimeout(() => startDelayed(perfData, totalCost), 0); // Do real startup on next tick, so all unit tests have been collected } diff --git a/src/testRunner/parallel/shared.ts b/src/testRunner/parallel/shared.ts index bfcef09b4de3f..8505dd474cae2 100644 --- a/src/testRunner/parallel/shared.ts +++ b/src/testRunner/parallel/shared.ts @@ -1,88 +1,76 @@ -namespace Harness.Parallel { - export interface RunnerTask { - runner: TestRunnerKind; - file: string; - size: number; - } - - export interface UnitTestTask { - runner: "unittest"; - file: string; - size: number; - } - - export type Task = RunnerTask | UnitTestTask; - - export interface TestInfo { - name: string[]; - } - - export interface ErrorInfo { - name: string[]; +import { TestRunnerKind } from "../Harness"; +import { noop } from "../ts"; +export interface RunnerTask { + runner: TestRunnerKind; + file: string; + size: number; +} +export interface UnitTestTask { + runner: "unittest"; + file: string; + size: number; +} +export type Task = RunnerTask | UnitTestTask; +export interface TestInfo { + name: string[]; +} +export interface ErrorInfo { + name: string[]; + error: string; + stack: string; +} +export interface TaskTimeout { + duration: number | "reset"; +} +export interface TaskResult { + passing: number; + errors: ErrorInfo[]; + passes: TestInfo[]; + duration: number; + task: Task; +} +export interface ParallelTestMessage { + type: "test"; + payload: Task; +} +export interface ParallelBatchMessage { + type: "batch"; + payload: Task[]; +} +export interface ParallelCloseMessage { + type: "close"; +} +export type ParallelHostMessage = ParallelTestMessage | ParallelCloseMessage | ParallelBatchMessage; +export interface ParallelErrorMessage { + type: "error"; + payload: { error: string; stack: string; - } - - export interface TaskTimeout { - duration: number | "reset"; - } - - export interface TaskResult { - passing: number; - errors: ErrorInfo[]; - passes: TestInfo[]; - duration: number; - task: Task; - } - - export interface ParallelTestMessage { - type: "test"; - payload: Task; - } - - export interface ParallelBatchMessage { - type: "batch"; - payload: Task[]; - } - - export interface ParallelCloseMessage { - type: "close"; - } - - export type ParallelHostMessage = ParallelTestMessage | ParallelCloseMessage | ParallelBatchMessage; - - export interface ParallelErrorMessage { - type: "error"; - payload: { error: string, stack: string, name?: string[] }; - } - - export interface ParallelResultMessage { - type: "result"; - payload: TaskResult; - } - - export interface ParallelBatchProgressMessage { - type: "progress"; - payload: TaskResult; - } - - export interface ParallelTimeoutChangeMessage { - type: "timeout"; - payload: TaskTimeout; - } - - export type ParallelClientMessage = ParallelErrorMessage | ParallelResultMessage | ParallelBatchProgressMessage | ParallelTimeoutChangeMessage; - - export function shimNoopTestInterface(global: Mocha.MochaGlobals) { - global.before = ts.noop; - global.after = ts.noop; - global.beforeEach = ts.noop; - global.afterEach = ts.noop; - global.describe = global.context = ((_: any, __: any) => { /*empty*/ }) as Mocha.SuiteFunction; - global.describe.skip = global.xdescribe = global.xcontext = ts.noop as Mocha.PendingSuiteFunction; - global.describe.only = ts.noop as Mocha.ExclusiveSuiteFunction; - global.it = global.specify = ((_: any, __: any) => { /*empty*/ }) as Mocha.TestFunction; - global.it.skip = global.xit = global.xspecify = ts.noop as Mocha.PendingTestFunction; - global.it.only = ts.noop as Mocha.ExclusiveTestFunction; - } + name?: string[]; + }; +} +export interface ParallelResultMessage { + type: "result"; + payload: TaskResult; +} +export interface ParallelBatchProgressMessage { + type: "progress"; + payload: TaskResult; +} +export interface ParallelTimeoutChangeMessage { + type: "timeout"; + payload: TaskTimeout; +} +export type ParallelClientMessage = ParallelErrorMessage | ParallelResultMessage | ParallelBatchProgressMessage | ParallelTimeoutChangeMessage; +export function shimNoopTestInterface(global: Mocha.MochaGlobals) { + global.before = noop; + global.after = noop; + global.beforeEach = noop; + global.afterEach = noop; + global.describe = global.context = ((_: any, __: any) => { }) as Mocha.SuiteFunction; + global.describe.skip = global.xdescribe = global.xcontext = (noop as Mocha.PendingSuiteFunction); + global.describe.only = (noop as Mocha.ExclusiveSuiteFunction); + global.it = global.specify = ((_: any, __: any) => { }) as Mocha.TestFunction; + global.it.skip = global.xit = global.xspecify = (noop as Mocha.PendingTestFunction); + global.it.only = (noop as Mocha.ExclusiveTestFunction); } diff --git a/src/testRunner/parallel/worker.ts b/src/testRunner/parallel/worker.ts index 450f15730b69e..b4e0d472c56cd 100644 --- a/src/testRunner/parallel/worker.ts +++ b/src/testRunner/parallel/worker.ts @@ -1,320 +1,291 @@ -namespace Harness.Parallel.Worker { - export function start() { - function hookUncaughtExceptions() { - if (!exceptionsHooked) { - process.on("uncaughtException", handleUncaughtException); - process.on("unhandledRejection", handleUncaughtException); - exceptionsHooked = true; - } - } - - function unhookUncaughtExceptions() { - if (exceptionsHooked) { - process.removeListener("uncaughtException", handleUncaughtException); - process.removeListener("unhandledRejection", handleUncaughtException); - exceptionsHooked = false; - } +import { Task, TaskResult, UnitTestTask, RunnerTask, ErrorInfo, TestInfo, ParallelHostMessage, ParallelClientMessage, shimNoopTestInterface } from "../Harness.Parallel"; +import { createMap } from "../ts"; +import { globalTimeout, createRunner, RunnerBase, runUnitTests } from "../Harness"; +import * as ts from "../ts"; +export function start() { + function hookUncaughtExceptions() { + if (!exceptionsHooked) { + process.on("uncaughtException", handleUncaughtException); + process.on("unhandledRejection", handleUncaughtException); + exceptionsHooked = true; } - - let exceptionsHooked = false; - hookUncaughtExceptions(); - - // Capitalization is aligned with the global `Mocha` namespace for typespace/namespace references. - const Mocha = require("mocha") as typeof import("mocha"); - - /** - * Mixin helper. - * @param base The base class constructor. - * @param mixins The mixins to apply to the constructor. - */ - function mixin any>(base: T, ...mixins: ((klass: T) => T)[]) { - for (const mixin of mixins) { - base = mixin(base); - } - return base; + } + function unhookUncaughtExceptions() { + if (exceptionsHooked) { + process.removeListener("uncaughtException", handleUncaughtException); + process.removeListener("unhandledRejection", handleUncaughtException); + exceptionsHooked = false; } - - /** - * Mixes in overrides for `resetTimeout` and `clearTimeout` to support parallel test execution in a worker. - */ - function Timeout(base: T) { - return class extends (base as typeof Mocha.Runnable) { - resetTimeout() { - this.clearTimeout(); - if (this.enableTimeouts()) { - sendMessage({ type: "timeout", payload: { duration: this.timeout() || 1e9 } }); - this.timer = true; - } - } - clearTimeout() { - if (this.timer) { - sendMessage({ type: "timeout", payload: { duration: "reset" } }); - this.timer = false; - } - } - } as T; + } + let exceptionsHooked = false; + hookUncaughtExceptions(); + // Capitalization is aligned with the global `Mocha` namespace for typespace/namespace references. + const Mocha = require("mocha") as typeof import("mocha"); + /** + * Mixin helper. + * @param base The base class constructor. + * @param mixins The mixins to apply to the constructor. + */ + function mixin any>(base: T, ...mixins: ((klass: T) => T)[]) { + for (const mixin of mixins) { + base = mixin(base); } - - /** - * Mixes in an override for `clone` to support parallel test execution in a worker. - */ - function Clone(base: T) { - return class extends (base as new (...args: any[]) => { clone(): any; }) { - clone() { - const cloned = super.clone(); - Object.setPrototypeOf(cloned, this.constructor.prototype); - return cloned; + return base; + } + /** + * Mixes in overrides for `resetTimeout` and `clearTimeout` to support parallel test execution in a worker. + */ + function Timeout(base: T) { + return class extends (base as typeof Mocha.Runnable) { + resetTimeout() { + this.clearTimeout(); + if (this.enableTimeouts()) { + sendMessage({ type: "timeout", payload: { duration: this.timeout() || 1e9 } }); + this.timer = true; } - } as T; - } - - /** - * A `Mocha.Suite` subclass to support parallel test execution in a worker. - */ - class Suite extends mixin(Mocha.Suite, Clone) { - _createHook(title: string, fn?: Mocha.Func | Mocha.AsyncFunc) { - const hook = super._createHook(title, fn); - Object.setPrototypeOf(hook, Hook.prototype); - return hook; } - } - - /** - * A `Mocha.Hook` subclass to support parallel test execution in a worker. - */ - class Hook extends mixin(Mocha.Hook, Timeout) { - } - - /** - * A `Mocha.Test` subclass to support parallel test execution in a worker. - */ - class Test extends mixin(Mocha.Test, Timeout, Clone) { - } - - /** - * Shims a 'bdd'-style test interface to support parallel test execution in a worker. - * @param rootSuite The root suite. - * @param context The test context (usually the NodeJS `global` object). - */ - function shimTestInterface(rootSuite: Mocha.Suite, context: Mocha.MochaGlobals) { - const suites = [rootSuite]; - context.before = (title: string | Mocha.Func | Mocha.AsyncFunc, fn?: Mocha.Func | Mocha.AsyncFunc) => { suites[0].beforeAll(title as string, fn); }; - context.after = (title: string | Mocha.Func | Mocha.AsyncFunc, fn?: Mocha.Func | Mocha.AsyncFunc) => { suites[0].afterAll(title as string, fn); }; - context.beforeEach = (title: string | Mocha.Func | Mocha.AsyncFunc, fn?: Mocha.Func | Mocha.AsyncFunc) => { suites[0].beforeEach(title as string, fn); }; - context.afterEach = (title: string | Mocha.Func | Mocha.AsyncFunc, fn?: Mocha.Func | Mocha.AsyncFunc) => { suites[0].afterEach(title as string, fn); }; - context.describe = context.context = ((title: string, fn: (this: Mocha.Suite) => void) => addSuite(title, fn)) as Mocha.SuiteFunction; - context.describe.skip = context.xdescribe = context.xcontext = (title: string) => addSuite(title, /*fn*/ undefined); - context.describe.only = (title: string, fn?: (this: Mocha.Suite) => void) => addSuite(title, fn); - context.it = context.specify = ((title: string | Mocha.Func | Mocha.AsyncFunc, fn?: Mocha.Func | Mocha.AsyncFunc) => addTest(title, fn)) as Mocha.TestFunction; - context.it.skip = context.xit = context.xspecify = (title: string | Mocha.Func | Mocha.AsyncFunc) => addTest(typeof title === "function" ? title.name : title, /*fn*/ undefined); - context.it.only = (title: string | Mocha.Func | Mocha.AsyncFunc, fn?: Mocha.Func | Mocha.AsyncFunc) => addTest(title, fn); - - function addSuite(title: string, fn: ((this: Mocha.Suite) => void) | undefined): Mocha.Suite { - const suite = new Suite(title, suites[0].ctx); - suites[0].addSuite(suite); - suite.pending = !fn; - suites.unshift(suite); - if (fn) { - fn.call(suite); + clearTimeout() { + if (this.timer) { + sendMessage({ type: "timeout", payload: { duration: "reset" } }); + this.timer = false; } - suites.shift(); - return suite; } - - function addTest(title: string | Mocha.Func | Mocha.AsyncFunc, fn: Mocha.Func | Mocha.AsyncFunc | undefined): Mocha.Test { - if (typeof title === "function") { - fn = title; - title = fn.name; - } - const test = new Test(title, suites[0].pending ? undefined : fn); - suites[0].addTest(test); - return test; + } as T; + } + /** + * Mixes in an override for `clone` to support parallel test execution in a worker. + */ + function Clone(base: T) { + return class extends (base as new (...args: any[]) => { + clone(): any; + }) { + clone() { + const cloned = super.clone(); + Object.setPrototypeOf(cloned, this.constructor.prototype); + return cloned; } + } as T; + } + /** + * A `Mocha.Suite` subclass to support parallel test execution in a worker. + */ + class Suite extends mixin(Mocha.Suite, Clone) { + _createHook(title: string, fn?: Mocha.Func | Mocha.AsyncFunc) { + const hook = super._createHook(title, fn); + Object.setPrototypeOf(hook, Hook.prototype); + return hook; } - - /** - * Run the tests in the requested task. - */ - function runTests(task: Task, fn: (payload: TaskResult) => void) { - if (task.runner === "unittest") { - return executeUnitTests(task, fn); - } - else { - return runFileTests(task, fn); + } + /** + * A `Mocha.Hook` subclass to support parallel test execution in a worker. + */ + class Hook extends mixin(Mocha.Hook, Timeout) { + } + /** + * A `Mocha.Test` subclass to support parallel test execution in a worker. + */ + class Test extends mixin(Mocha.Test, Timeout, Clone) { + } + /** + * Shims a 'bdd'-style test interface to support parallel test execution in a worker. + * @param rootSuite The root suite. + * @param context The test context (usually the NodeJS `global` object). + */ + function shimTestInterface(rootSuite: Mocha.Suite, context: Mocha.MochaGlobals) { + const suites = [rootSuite]; + context.before = (title: string | Mocha.Func | Mocha.AsyncFunc, fn?: Mocha.Func | Mocha.AsyncFunc) => { suites[0].beforeAll(title as string, fn); }; + context.after = (title: string | Mocha.Func | Mocha.AsyncFunc, fn?: Mocha.Func | Mocha.AsyncFunc) => { suites[0].afterAll(title as string, fn); }; + context.beforeEach = (title: string | Mocha.Func | Mocha.AsyncFunc, fn?: Mocha.Func | Mocha.AsyncFunc) => { suites[0].beforeEach(title as string, fn); }; + context.afterEach = (title: string | Mocha.Func | Mocha.AsyncFunc, fn?: Mocha.Func | Mocha.AsyncFunc) => { suites[0].afterEach(title as string, fn); }; + context.describe = context.context = ((title: string, fn: (this: Mocha.Suite) => void) => addSuite(title, fn)) as Mocha.SuiteFunction; + context.describe.skip = context.xdescribe = context.xcontext = (title: string) => addSuite(title, /*fn*/ undefined); + context.describe.only = (title: string, fn?: (this: Mocha.Suite) => void) => addSuite(title, fn); + context.it = context.specify = ((title: string | Mocha.Func | Mocha.AsyncFunc, fn?: Mocha.Func | Mocha.AsyncFunc) => addTest(title, fn)) as Mocha.TestFunction; + context.it.skip = context.xit = context.xspecify = (title: string | Mocha.Func | Mocha.AsyncFunc) => addTest(typeof title === "function" ? title.name : title, /*fn*/ undefined); + context.it.only = (title: string | Mocha.Func | Mocha.AsyncFunc, fn?: Mocha.Func | Mocha.AsyncFunc) => addTest(title, fn); + function addSuite(title: string, fn: ((this: Mocha.Suite) => void) | undefined): Mocha.Suite { + const suite = new Suite(title, suites[0].ctx); + suites[0].addSuite(suite); + suite.pending = !fn; + suites.unshift(suite); + if (fn) { + fn.call(suite); } + suites.shift(); + return suite; } - - function executeUnitTests(task: UnitTestTask, fn: (payload: TaskResult) => void) { - if (!unitTestSuiteMap && unitTestSuite.suites.length) { - unitTestSuiteMap = ts.createMap(); - for (const suite of unitTestSuite.suites) { - unitTestSuiteMap.set(suite.title, suite); - } - } - if (!unitTestTestMap && unitTestSuite.tests.length) { - unitTestTestMap = ts.createMap(); - for (const test of unitTestSuite.tests) { - unitTestTestMap.set(test.title, test); - } - } - - if (!unitTestSuiteMap && !unitTestTestMap) { - throw new Error(`Asked to run unit test ${task.file}, but no unit tests were discovered!`); + function addTest(title: string | Mocha.Func | Mocha.AsyncFunc, fn: Mocha.Func | Mocha.AsyncFunc | undefined): Mocha.Test { + if (typeof title === "function") { + fn = title; + title = fn.name; } - - let suite = unitTestSuiteMap.get(task.file); - const test = unitTestTestMap.get(task.file); - if (!suite && !test) { - throw new Error(`Unit test with name "${task.file}" was asked to be run, but such a test does not exist!`); - } - - const root = new Suite("", new Mocha.Context()); - root.timeout(globalTimeout || 40_000); - if (suite) { - root.addSuite(suite); - Object.setPrototypeOf(suite.ctx, root.ctx); - } - else if (test) { - const newSuite = new Suite("", new Mocha.Context()); - newSuite.addTest(test); - root.addSuite(newSuite); - Object.setPrototypeOf(newSuite.ctx, root.ctx); - Object.setPrototypeOf(test.ctx, root.ctx); - test.parent = newSuite; - suite = newSuite; - } - - runSuite(task, suite!, payload => { - suite!.parent = unitTestSuite; - Object.setPrototypeOf(suite!.ctx, unitTestSuite.ctx); - fn(payload); - }); + const test = new Test(title, suites[0].pending ? undefined : fn); + suites[0].addTest(test); + return test; } - - function runFileTests(task: RunnerTask, fn: (result: TaskResult) => void) { - let instance = runners.get(task.runner); - if (!instance) runners.set(task.runner, instance = createRunner(task.runner)); - instance.tests = [task.file]; - - const suite = new Suite("", new Mocha.Context()); - suite.timeout(globalTimeout || 40_000); - - shimTestInterface(suite, global); - instance.initializeTests(); - - runSuite(task, suite, fn); + } + /** + * Run the tests in the requested task. + */ + function runTests(task: Task, fn: (payload: TaskResult) => void) { + if (task.runner === "unittest") { + return executeUnitTests(task, fn); } - - function runSuite(task: Task, suite: Mocha.Suite, fn: (result: TaskResult) => void) { - const errors: ErrorInfo[] = []; - const passes: TestInfo[] = []; - const start = +new Date(); - const runner = new Mocha.Runner(suite, /*delay*/ false); - const uncaught = (err: any) => runner.uncaught(err); - - runner - .on("start", () => { - unhookUncaughtExceptions(); // turn off global uncaught handling - process.on("unhandledRejection", uncaught); // turn on unhandled rejection handling (not currently handled in mocha) - }) - .on("pass", (test: Mocha.Test) => { - passes.push({ name: test.titlePath() }); - }) - .on("fail", (test: Mocha.Test | Mocha.Hook, err: any) => { - errors.push({ name: test.titlePath(), error: err.message, stack: err.stack }); - }) - .on("end", () => { - process.removeListener("unhandledRejection", uncaught); - hookUncaughtExceptions(); - }) - .run(() => { - fn({ task, errors, passes, passing: passes.length, duration: +new Date() - start }); - }); + else { + return runFileTests(task, fn); } - - /** - * Validates a message received from the host is well-formed. - */ - function validateHostMessage(message: ParallelHostMessage) { - switch (message.type) { - case "test": return validateTest(message.payload); - case "batch": return validateBatch(message.payload); - case "close": return true; - default: return false; + } + function executeUnitTests(task: UnitTestTask, fn: (payload: TaskResult) => void) { + if (!unitTestSuiteMap && unitTestSuite.suites.length) { + unitTestSuiteMap = createMap(); + for (const suite of unitTestSuite.suites) { + unitTestSuiteMap.set(suite.title, suite); } } - - /** - * Validates a test task is well formed. - */ - function validateTest(task: Task) { - return !!task && !!task.runner && !!task.file; - } - - /** - * Validates a batch of test tasks are well formed. - */ - function validateBatch(tasks: Task[]) { - return !!tasks && Array.isArray(tasks) && tasks.length > 0 && tasks.every(validateTest); - } - - function processHostMessage(message: ParallelHostMessage) { - if (!validateHostMessage(message)) { - console.log("Invalid message:", message); - return; - } - - switch (message.type) { - case "test": return processTest(message.payload, /*last*/ true); - case "batch": return processBatch(message.payload); - case "close": return process.exit(0); + if (!unitTestTestMap && unitTestSuite.tests.length) { + unitTestTestMap = createMap(); + for (const test of unitTestSuite.tests) { + unitTestTestMap.set(test.title, test); } } - - function processTest(task: Task, last: boolean, fn?: () => void) { - runTests(task, payload => { - sendMessage(last ? { type: "result", payload } : { type: "progress", payload }); - if (fn) fn(); - }); + if (!unitTestSuiteMap && !unitTestTestMap) { + throw new Error(`Asked to run unit test ${task.file}, but no unit tests were discovered!`); } - - function processBatch(tasks: Task[], fn?: () => void) { - const next = () => { - const task = tasks.shift(); - if (task) return processTest(task, tasks.length === 0, next); - if (fn) fn(); - }; - next(); + let suite = unitTestSuiteMap.get(task.file); + const test = unitTestTestMap.get(task.file); + if (!suite && !test) { + throw new Error(`Unit test with name "${task.file}" was asked to be run, but such a test does not exist!`); } - - function handleUncaughtException(err: any) { - const error = err instanceof Error ? err : new Error("" + err); - sendMessage({ type: "error", payload: { error: error.message, stack: error.stack! } }); + const root = new Suite("", new Mocha.Context()); + root.timeout(globalTimeout || 40000); + if (suite) { + root.addSuite(suite); + Object.setPrototypeOf(suite.ctx, root.ctx); } - - function sendMessage(message: ParallelClientMessage) { - process.send!(message); + else if (test) { + const newSuite = new Suite("", new Mocha.Context()); + newSuite.addTest(test); + root.addSuite(newSuite); + Object.setPrototypeOf(newSuite.ctx, root.ctx); + Object.setPrototypeOf(test.ctx, root.ctx); + test.parent = newSuite; + suite = newSuite; } - - // A cache of test harness Runner instances. - const runners = ts.createMap(); - - // The root suite for all unit tests. - let unitTestSuite: Suite; - let unitTestSuiteMap: ts.Map; - // (Unit) Tests directly within the root suite - let unitTestTestMap: ts.Map; - - if (runUnitTests) { - unitTestSuite = new Suite("", new Mocha.Context()); - unitTestSuite.timeout(globalTimeout || 40_000); - shimTestInterface(unitTestSuite, global); + runSuite(task, suite!, payload => { + suite!.parent = unitTestSuite; + Object.setPrototypeOf(suite!.ctx, unitTestSuite.ctx); + fn(payload); + }); + } + function runFileTests(task: RunnerTask, fn: (result: TaskResult) => void) { + let instance = runners.get(task.runner); + if (!instance) + runners.set(task.runner, instance = createRunner(task.runner)); + instance.tests = [task.file]; + const suite = new Suite("", new Mocha.Context()); + suite.timeout(globalTimeout || 40000); + shimTestInterface(suite, global); + instance.initializeTests(); + runSuite(task, suite, fn); + } + function runSuite(task: Task, suite: Mocha.Suite, fn: (result: TaskResult) => void) { + const errors: ErrorInfo[] = []; + const passes: TestInfo[] = []; + const start = +new Date(); + const runner = new Mocha.Runner(suite, /*delay*/ false); + const uncaught = (err: any) => runner.uncaught(err); + runner + .on("start", () => { + unhookUncaughtExceptions(); // turn off global uncaught handling + process.on("unhandledRejection", uncaught); // turn on unhandled rejection handling (not currently handled in mocha) + }) + .on("pass", (test: Mocha.Test) => { + passes.push({ name: test.titlePath() }); + }) + .on("fail", (test: Mocha.Test | Mocha.Hook, err: any) => { + errors.push({ name: test.titlePath(), error: err.message, stack: err.stack }); + }) + .on("end", () => { + process.removeListener("unhandledRejection", uncaught); + hookUncaughtExceptions(); + }) + .run(() => { + fn({ task, errors, passes, passing: passes.length, duration: +new Date() - start }); + }); + } + /** + * Validates a message received from the host is well-formed. + */ + function validateHostMessage(message: ParallelHostMessage) { + switch (message.type) { + case "test": return validateTest(message.payload); + case "batch": return validateBatch(message.payload); + case "close": return true; + default: return false; } - else { - // ensure unit tests do not get run - shimNoopTestInterface(global); + } + /** + * Validates a test task is well formed. + */ + function validateTest(task: Task) { + return !!task && !!task.runner && !!task.file; + } + /** + * Validates a batch of test tasks are well formed. + */ + function validateBatch(tasks: Task[]) { + return !!tasks && Array.isArray(tasks) && tasks.length > 0 && tasks.every(validateTest); + } + function processHostMessage(message: ParallelHostMessage) { + if (!validateHostMessage(message)) { + console.log("Invalid message:", message); + return; + } + switch (message.type) { + case "test": return processTest(message.payload, /*last*/ true); + case "batch": return processBatch(message.payload); + case "close": return process.exit(0); } - - process.on("message", processHostMessage); } + function processTest(task: Task, last: boolean, fn?: () => void) { + runTests(task, payload => { + sendMessage(last ? { type: "result", payload } : { type: "progress", payload }); + if (fn) + fn(); + }); + } + function processBatch(tasks: Task[], fn?: () => void) { + const next = () => { + const task = tasks.shift(); + if (task) + return processTest(task, tasks.length === 0, next); + if (fn) + fn(); + }; + next(); + } + function handleUncaughtException(err: any) { + const error = err instanceof Error ? err : new Error("" + err); + sendMessage({ type: "error", payload: { error: error.message, stack: error.stack! } }); + } + function sendMessage(message: ParallelClientMessage) { + process.send!(message); + } + // A cache of test harness Runner instances. + const runners = createMap(); + // The root suite for all unit tests. + let unitTestSuite: Suite; + let unitTestSuiteMap: ts.Map; + // (Unit) Tests directly within the root suite + let unitTestTestMap: ts.Map; + if (runUnitTests) { + unitTestSuite = new Suite("", new Mocha.Context()); + unitTestSuite.timeout(globalTimeout || 40000); + shimTestInterface(unitTestSuite, global); + } + else { + // ensure unit tests do not get run + shimNoopTestInterface(global); + } + process.on("message", processHostMessage); } diff --git a/src/testRunner/playbackRef.ts b/src/testRunner/playbackRef.ts index 8fcb9965e0923..10644a98e1b95 100644 --- a/src/testRunner/playbackRef.ts +++ b/src/testRunner/playbackRef.ts @@ -1,2 +1,2 @@ // empty ref to Playback so it can be referenced by unittests -namespace Playback {} \ No newline at end of file +export {}; diff --git a/src/testRunner/project.ts b/src/testRunner/project.ts new file mode 100644 index 0000000000000..cdeca1d49f7b1 --- /dev/null +++ b/src/testRunner/project.ts @@ -0,0 +1 @@ +export * from "./projectsRunner"; diff --git a/src/testRunner/projectsRunner.ts b/src/testRunner/projectsRunner.ts index d90972fe09a76..1ea08c227fa7e 100644 --- a/src/testRunner/projectsRunner.ts +++ b/src/testRunner/projectsRunner.ts @@ -1,462 +1,400 @@ -namespace project { - // Test case is json of below type in tests/cases/project/ - interface ProjectRunnerTestCase { - scenario: string; - projectRoot: string; // project where it lives - this also is the current directory when compiling - inputFiles: readonly string[]; // list of input files to be given to program - resolveMapRoot?: boolean; // should we resolve this map root and give compiler the absolute disk path as map root? - resolveSourceRoot?: boolean; // should we resolve this source root and give compiler the absolute disk path as map root? - baselineCheck?: boolean; // Verify the baselines of output files, if this is false, we will write to output to the disk but there is no verification of baselines - runTest?: boolean; // Run the resulting test - bug?: string; // If there is any bug associated with this test case - } - - interface ProjectRunnerTestCaseResolutionInfo extends ProjectRunnerTestCase { - // Apart from actual test case the results of the resolution - resolvedInputFiles: readonly string[]; // List of files that were asked to read by compiler - emittedFiles: readonly string[]; // List of files that were emitted by the compiler - } - - interface CompileProjectFilesResult { - configFileSourceFiles: readonly ts.SourceFile[]; - moduleKind: ts.ModuleKind; - program?: ts.Program; - compilerOptions?: ts.CompilerOptions; - errors: readonly ts.Diagnostic[]; - sourceMapData?: readonly ts.SourceMapEmitResult[]; +import { SourceFile, ModuleKind, Program, CompilerOptions, Diagnostic, SourceMapEmitResult, normalizePath, combinePaths, findConfigFile, readJsonConfigFile, parseJsonSourceFileConfigFileContent, getDirectoryPath, concatenate, normalizeSlashes, CharacterCodes, createProgram, getPreEmitDiagnostics, forEach, getNormalizedAbsolutePath, removeFileExtension, Extension, contains, isRootedDiskPath, ModuleResolutionKind, NewLineKind, arrayToMap, optionDeclarations, isString } from "./ts"; +import { TextDocument } from "./documents"; +import { RunnerBase, shards, shardId, TestRunnerKind, IO, Baseline, isDefaultLibraryFile, Compiler } from "./Harness"; +import { CompilerHost, System, ParseConfigHost } from "./fakes"; +import { FileSystem, srcFolder, createFromFileSystem, createResolver, builtFolder, testLibFolder } from "./vfs"; +import { resolve, relative, combine, beneath, isAbsolute, extname, isDefaultLibrary } from "./vpath"; +import { removeTestPathPrefixes } from "./Utils"; +import * as ts from "./ts"; +// Test case is json of below type in tests/cases/project/ +interface ProjectRunnerTestCase { + scenario: string; + projectRoot: string; // project where it lives - this also is the current directory when compiling + inputFiles: readonly string[]; // list of input files to be given to program + resolveMapRoot?: boolean; // should we resolve this map root and give compiler the absolute disk path as map root? + resolveSourceRoot?: boolean; // should we resolve this source root and give compiler the absolute disk path as map root? + baselineCheck?: boolean; // Verify the baselines of output files, if this is false, we will write to output to the disk but there is no verification of baselines + runTest?: boolean; // Run the resulting test + bug?: string; // If there is any bug associated with this test case +} +interface ProjectRunnerTestCaseResolutionInfo extends ProjectRunnerTestCase { + // Apart from actual test case the results of the resolution + resolvedInputFiles: readonly string[]; // List of files that were asked to read by compiler + emittedFiles: readonly string[]; // List of files that were emitted by the compiler +} +interface CompileProjectFilesResult { + configFileSourceFiles: readonly SourceFile[]; + moduleKind: ModuleKind; + program?: Program; + compilerOptions?: CompilerOptions; + errors: readonly Diagnostic[]; + sourceMapData?: readonly SourceMapEmitResult[]; +} +interface BatchCompileProjectTestCaseResult extends CompileProjectFilesResult { + outputFiles?: readonly TextDocument[]; +} +export class ProjectRunner extends RunnerBase { + public enumerateTestFiles() { + const all = this.enumerateFiles("tests/cases/project", /\.json$/, { recursive: true }); + if (shards === 1) { + return all; + } + return all.filter((_val, idx) => idx % shards === (shardId - 1)); } - - interface BatchCompileProjectTestCaseResult extends CompileProjectFilesResult { - outputFiles?: readonly documents.TextDocument[]; + public kind(): TestRunnerKind { + return "project"; } - - export class ProjectRunner extends Harness.RunnerBase { - public enumerateTestFiles() { - const all = this.enumerateFiles("tests/cases/project", /\.json$/, { recursive: true }); - if (Harness.shards === 1) { - return all; + public initializeTests() { + describe("projects tests", () => { + const tests = this.tests.length === 0 ? this.enumerateTestFiles() : this.tests; + for (const test of tests) { + this.runProjectTestCase(typeof test === "string" ? test : test.file); } - return all.filter((_val, idx) => idx % Harness.shards === (Harness.shardId - 1)); - } - - public kind(): Harness.TestRunnerKind { - return "project"; - } - - public initializeTests() { - describe("projects tests", () => { - const tests = this.tests.length === 0 ? this.enumerateTestFiles() : this.tests; - for (const test of tests) { - this.runProjectTestCase(typeof test === "string" ? test : test.file); - } + }); + } + private runProjectTestCase(testCaseFileName: string) { + for (const { name, payload } of ProjectTestCase.getConfigurations(testCaseFileName)) { + describe("Compiling project for " + payload.testCase.scenario + ": testcase " + testCaseFileName + (name ? ` (${name})` : ``), () => { + let projectTestCase: ProjectTestCase | undefined; + before(() => { projectTestCase = new ProjectTestCase(testCaseFileName, payload); }); + it(`Correct module resolution tracing for ${testCaseFileName}`, () => projectTestCase && projectTestCase.verifyResolution()); + it(`Correct errors for ${testCaseFileName}`, () => projectTestCase && projectTestCase.verifyDiagnostics()); + it(`Correct JS output for ${testCaseFileName}`, () => projectTestCase && projectTestCase.verifyJavaScriptOutput()); + // NOTE: This check was commented out in previous code. Leaving this here to eventually be restored if needed. + // it(`Correct sourcemap content for ${testCaseFileName}`, () => projectTestCase && projectTestCase.verifySourceMapRecord()); + it(`Correct declarations for ${testCaseFileName}`, () => projectTestCase && projectTestCase.verifyDeclarations()); + after(() => { projectTestCase = undefined; }); }); } - - private runProjectTestCase(testCaseFileName: string) { - for (const { name, payload } of ProjectTestCase.getConfigurations(testCaseFileName)) { - describe("Compiling project for " + payload.testCase.scenario + ": testcase " + testCaseFileName + (name ? ` (${name})` : ``), () => { - let projectTestCase: ProjectTestCase | undefined; - before(() => { projectTestCase = new ProjectTestCase(testCaseFileName, payload); }); - it(`Correct module resolution tracing for ${testCaseFileName}`, () => projectTestCase && projectTestCase.verifyResolution()); - it(`Correct errors for ${testCaseFileName}`, () => projectTestCase && projectTestCase.verifyDiagnostics()); - it(`Correct JS output for ${testCaseFileName}`, () => projectTestCase && projectTestCase.verifyJavaScriptOutput()); - // NOTE: This check was commented out in previous code. Leaving this here to eventually be restored if needed. - // it(`Correct sourcemap content for ${testCaseFileName}`, () => projectTestCase && projectTestCase.verifySourceMapRecord()); - it(`Correct declarations for ${testCaseFileName}`, () => projectTestCase && projectTestCase.verifyDeclarations()); - after(() => { projectTestCase = undefined; }); - }); - } - } } - - class ProjectCompilerHost extends fakes.CompilerHost { - private _testCase: ProjectRunnerTestCase & ts.CompilerOptions; - private _projectParseConfigHost: ProjectParseConfigHost | undefined; - - constructor(sys: fakes.System | vfs.FileSystem, compilerOptions: ts.CompilerOptions, _testCaseJustName: string, testCase: ProjectRunnerTestCase & ts.CompilerOptions, _moduleKind: ts.ModuleKind) { - super(sys, compilerOptions); - this._testCase = testCase; +} +class ProjectCompilerHost extends CompilerHost { + private _testCase: ProjectRunnerTestCase & CompilerOptions; + private _projectParseConfigHost: ProjectParseConfigHost | undefined; + constructor(sys: System | FileSystem, compilerOptions: CompilerOptions, _testCaseJustName: string, testCase: ProjectRunnerTestCase & CompilerOptions, _moduleKind: ModuleKind) { + super(sys, compilerOptions); + this._testCase = testCase; + } + public get parseConfigHost(): ParseConfigHost { + return this._projectParseConfigHost || (this._projectParseConfigHost = new ProjectParseConfigHost(this.sys, this._testCase)); + } + public getDefaultLibFileName(_options: CompilerOptions) { + return resolve(this.getDefaultLibLocation(), "lib.es5.d.ts"); + } +} +class ProjectParseConfigHost extends ParseConfigHost { + private _testCase: ProjectRunnerTestCase & CompilerOptions; + constructor(sys: System, testCase: ProjectRunnerTestCase & CompilerOptions) { + super(sys); + this._testCase = testCase; + } + public readDirectory(path: string, extensions: string[], excludes: string[], includes: string[], depth: number): string[] { + const result = super.readDirectory(path, extensions, excludes, includes, depth); + const projectRoot = resolve(srcFolder, this._testCase.projectRoot); + return result.map(item => relative(projectRoot, resolve(projectRoot, item), this.vfs.ignoreCase)); + } +} +interface ProjectTestConfiguration { + name: string; + payload: ProjectTestPayload; +} +interface ProjectTestPayload { + testCase: ProjectRunnerTestCase & CompilerOptions; + moduleKind: ModuleKind; + vfs: FileSystem; +} +class ProjectTestCase { + private testCase: ProjectRunnerTestCase & CompilerOptions; + private testCaseJustName: string; + private sys: System; + private compilerOptions: CompilerOptions; + private compilerResult: BatchCompileProjectTestCaseResult; + constructor(testCaseFileName: string, { testCase, moduleKind, vfs }: ProjectTestPayload) { + this.testCase = testCase; + this.testCaseJustName = testCaseFileName.replace(/^.*[\\\/]/, "").replace(/\.json/, ""); + this.compilerOptions = createCompilerOptions(testCase, moduleKind); + this.sys = new System(vfs); + let configFileName: string | undefined; + let inputFiles = testCase.inputFiles; + if (this.compilerOptions.project) { + // Parse project + configFileName = normalizePath(combinePaths(this.compilerOptions.project, "tsconfig.json")); + assert(!inputFiles || inputFiles.length === 0, "cannot specify input files and project option together"); } - - public get parseConfigHost(): fakes.ParseConfigHost { - return this._projectParseConfigHost || (this._projectParseConfigHost = new ProjectParseConfigHost(this.sys, this._testCase)); + else if (!inputFiles || inputFiles.length === 0) { + configFileName = findConfigFile("", path => this.sys.fileExists(path)); } - - public getDefaultLibFileName(_options: ts.CompilerOptions) { - return vpath.resolve(this.getDefaultLibLocation(), "lib.es5.d.ts"); + let errors: Diagnostic[] | undefined; + const configFileSourceFiles: SourceFile[] = []; + if (configFileName) { + const result = readJsonConfigFile(configFileName, path => this.sys.readFile(path)); + configFileSourceFiles.push(result); + const configParseHost = new ProjectParseConfigHost(this.sys, this.testCase); + const configParseResult = parseJsonSourceFileConfigFileContent(result, configParseHost, getDirectoryPath(configFileName), this.compilerOptions); + inputFiles = configParseResult.fileNames; + this.compilerOptions = configParseResult.options; + errors = [...result.parseDiagnostics, ...configParseResult.errors]; } + const compilerHost = new ProjectCompilerHost(this.sys, this.compilerOptions, this.testCaseJustName, this.testCase, moduleKind); + const projectCompilerResult = this.compileProjectFiles(moduleKind, configFileSourceFiles, () => inputFiles, compilerHost, this.compilerOptions); + this.compilerResult = { + configFileSourceFiles, + moduleKind, + program: projectCompilerResult.program, + compilerOptions: this.compilerOptions, + sourceMapData: projectCompilerResult.sourceMapData, + outputFiles: compilerHost.outputs, + errors: errors ? concatenate(errors, projectCompilerResult.errors) : projectCompilerResult.errors, + }; + } + private get vfs() { + return this.sys.vfs; } - - class ProjectParseConfigHost extends fakes.ParseConfigHost { - private _testCase: ProjectRunnerTestCase & ts.CompilerOptions; - - constructor(sys: fakes.System, testCase: ProjectRunnerTestCase & ts.CompilerOptions) { - super(sys); - this._testCase = testCase; + public static getConfigurations(testCaseFileName: string): ProjectTestConfiguration[] { + let testCase: ProjectRunnerTestCase & CompilerOptions; + let testFileText: string | undefined; + try { + testFileText = IO.readFile(testCaseFileName); + } + catch (e) { + assert(false, "Unable to open testcase file: " + testCaseFileName + ": " + e.message); + } + try { + testCase = (JSON.parse(testFileText!)); } - - public readDirectory(path: string, extensions: string[], excludes: string[], includes: string[], depth: number): string[] { - const result = super.readDirectory(path, extensions, excludes, includes, depth); - const projectRoot = vpath.resolve(vfs.srcFolder, this._testCase.projectRoot); - return result.map(item => vpath.relative( - projectRoot, - vpath.resolve(projectRoot, item), - this.vfs.ignoreCase - )); + catch (e) { + throw assert(false, "Testcase: " + testCaseFileName + " does not contain valid json format: " + e.message); } + const fs = createFromFileSystem(IO, /*ignoreCase*/ false); + fs.mountSync(resolve(IO.getWorkspaceRoot(), "tests"), combine(srcFolder, "tests"), createResolver(IO)); + fs.mkdirpSync(combine(srcFolder, testCase.projectRoot)); + fs.chdir(combine(srcFolder, testCase.projectRoot)); + fs.makeReadonly(); + return [ + { name: `@module: commonjs`, payload: { testCase, moduleKind: ModuleKind.CommonJS, vfs: fs } }, + { name: `@module: amd`, payload: { testCase, moduleKind: ModuleKind.AMD, vfs: fs } } + ]; } - - interface ProjectTestConfiguration { - name: string; - payload: ProjectTestPayload; + public verifyResolution() { + const cwd = this.vfs.cwd(); + const ignoreCase = this.vfs.ignoreCase; + const resolutionInfo: ProjectRunnerTestCaseResolutionInfo & CompilerOptions = JSON.parse(JSON.stringify(this.testCase)); + resolutionInfo.resolvedInputFiles = this.compilerResult.program!.getSourceFiles() + .map(({ fileName: input }) => beneath(builtFolder, input, this.vfs.ignoreCase) || beneath(testLibFolder, input, this.vfs.ignoreCase) ? removeTestPathPrefixes(input) : + isAbsolute(input) ? relative(cwd, input, ignoreCase) : + input); + resolutionInfo.emittedFiles = this.compilerResult.outputFiles! + .map(output => output.meta.get("fileName") || output.file) + .map(output => removeTestPathPrefixes(isAbsolute(output) ? relative(cwd, output, ignoreCase) : output)); + const content = JSON.stringify(resolutionInfo, undefined, " "); + Baseline.runBaseline(this.getBaselineFolder(this.compilerResult.moduleKind) + this.testCaseJustName + ".json", content); } - - interface ProjectTestPayload { - testCase: ProjectRunnerTestCase & ts.CompilerOptions; - moduleKind: ts.ModuleKind; - vfs: vfs.FileSystem; + public verifyDiagnostics() { + if (this.compilerResult.errors.length) { + Baseline.runBaseline(this.getBaselineFolder(this.compilerResult.moduleKind) + this.testCaseJustName + ".errors.txt", getErrorsBaseline(this.compilerResult)); + } } - - class ProjectTestCase { - private testCase: ProjectRunnerTestCase & ts.CompilerOptions; - private testCaseJustName: string; - private sys: fakes.System; - private compilerOptions: ts.CompilerOptions; - private compilerResult: BatchCompileProjectTestCaseResult; - - constructor(testCaseFileName: string, { testCase, moduleKind, vfs }: ProjectTestPayload) { - this.testCase = testCase; - this.testCaseJustName = testCaseFileName.replace(/^.*[\\\/]/, "").replace(/\.json/, ""); - this.compilerOptions = createCompilerOptions(testCase, moduleKind); - this.sys = new fakes.System(vfs); - - let configFileName: string | undefined; - let inputFiles = testCase.inputFiles; - if (this.compilerOptions.project) { - // Parse project - configFileName = ts.normalizePath(ts.combinePaths(this.compilerOptions.project, "tsconfig.json")); - assert(!inputFiles || inputFiles.length === 0, "cannot specify input files and project option together"); - } - else if (!inputFiles || inputFiles.length === 0) { - configFileName = ts.findConfigFile("", path => this.sys.fileExists(path)); + public verifyJavaScriptOutput() { + if (this.testCase.baselineCheck) { + const errs: Error[] = []; + let nonSubfolderDiskFiles = 0; + for (const output of this.compilerResult.outputFiles!) { + try { + // convert file name to rooted name + // if filename is not rooted - concat it with project root and then expand project root relative to current directory + const fileName = output.meta.get("fileName") || output.file; + const diskFileName = isAbsolute(fileName) ? fileName : resolve(this.vfs.cwd(), fileName); + // compute file name relative to current directory (expanded project root) + let diskRelativeName = relative(this.vfs.cwd(), diskFileName, this.vfs.ignoreCase); + if (isAbsolute(diskRelativeName) || diskRelativeName.startsWith("../")) { + // If the generated output file resides in the parent folder or is rooted path, + // we need to instead create files that can live in the project reference folder + // but make sure extension of these files matches with the fileName the compiler asked to write + diskRelativeName = `diskFile${nonSubfolderDiskFiles}${extname(fileName, [".js.map", ".js", ".d.ts"], this.vfs.ignoreCase)}`; + nonSubfolderDiskFiles++; + } + const content = removeTestPathPrefixes(output.text, /*retainTrailingDirectorySeparator*/ true); + Baseline.runBaseline(this.getBaselineFolder(this.compilerResult.moduleKind) + diskRelativeName, (content as string | null)); // TODO: GH#18217 + } + catch (e) { + errs.push(e); + } } - - let errors: ts.Diagnostic[] | undefined; - const configFileSourceFiles: ts.SourceFile[] = []; - if (configFileName) { - const result = ts.readJsonConfigFile(configFileName, path => this.sys.readFile(path)); - configFileSourceFiles.push(result); - const configParseHost = new ProjectParseConfigHost(this.sys, this.testCase); - const configParseResult = ts.parseJsonSourceFileConfigFileContent(result, configParseHost, ts.getDirectoryPath(configFileName), this.compilerOptions); - inputFiles = configParseResult.fileNames; - this.compilerOptions = configParseResult.options; - errors = [...result.parseDiagnostics, ...configParseResult.errors]; + if (errs.length) { + throw Error(errs.join("\n ")); } - - const compilerHost = new ProjectCompilerHost(this.sys, this.compilerOptions, this.testCaseJustName, this.testCase, moduleKind); - const projectCompilerResult = this.compileProjectFiles(moduleKind, configFileSourceFiles, () => inputFiles, compilerHost, this.compilerOptions); - - this.compilerResult = { - configFileSourceFiles, - moduleKind, - program: projectCompilerResult.program, - compilerOptions: this.compilerOptions, - sourceMapData: projectCompilerResult.sourceMapData, - outputFiles: compilerHost.outputs, - errors: errors ? ts.concatenate(errors, projectCompilerResult.errors) : projectCompilerResult.errors, - }; } - - private get vfs() { - return this.sys.vfs; - } - - public static getConfigurations(testCaseFileName: string): ProjectTestConfiguration[] { - let testCase: ProjectRunnerTestCase & ts.CompilerOptions; - - let testFileText: string | undefined; - try { - testFileText = Harness.IO.readFile(testCaseFileName); - } - catch (e) { - assert(false, "Unable to open testcase file: " + testCaseFileName + ": " + e.message); - } - - try { - testCase = JSON.parse(testFileText!); - } - catch (e) { - throw assert(false, "Testcase: " + testCaseFileName + " does not contain valid json format: " + e.message); + } + public verifySourceMapRecord() { + // NOTE: This check was commented out in previous code. Leaving this here to eventually be restored if needed. + // if (compilerResult.sourceMapData) { + // Harness.Baseline.runBaseline(getBaselineFolder(compilerResult.moduleKind) + testCaseJustName + ".sourcemap.txt", () => { + // return Harness.SourceMapRecorder.getSourceMapRecord(compilerResult.sourceMapData, compilerResult.program, + // ts.filter(compilerResult.outputFiles, outputFile => Harness.Compiler.isJS(outputFile.emittedFileName))); + // }); + // } + } + public verifyDeclarations() { + if (!this.compilerResult.errors.length && this.testCase.declaration) { + const dTsCompileResult = this.compileDeclarations(this.compilerResult); + if (dTsCompileResult && dTsCompileResult.errors.length) { + Baseline.runBaseline(this.getBaselineFolder(this.compilerResult.moduleKind) + this.testCaseJustName + ".dts.errors.txt", getErrorsBaseline(dTsCompileResult)); } - - const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false); - fs.mountSync(vpath.resolve(Harness.IO.getWorkspaceRoot(), "tests"), vpath.combine(vfs.srcFolder, "tests"), vfs.createResolver(Harness.IO)); - fs.mkdirpSync(vpath.combine(vfs.srcFolder, testCase.projectRoot)); - fs.chdir(vpath.combine(vfs.srcFolder, testCase.projectRoot)); - fs.makeReadonly(); - - return [ - { name: `@module: commonjs`, payload: { testCase, moduleKind: ts.ModuleKind.CommonJS, vfs: fs } }, - { name: `@module: amd`, payload: { testCase, moduleKind: ts.ModuleKind.AMD, vfs: fs } } - ]; - } - - public verifyResolution() { - const cwd = this.vfs.cwd(); - const ignoreCase = this.vfs.ignoreCase; - const resolutionInfo: ProjectRunnerTestCaseResolutionInfo & ts.CompilerOptions = JSON.parse(JSON.stringify(this.testCase)); - resolutionInfo.resolvedInputFiles = this.compilerResult.program!.getSourceFiles() - .map(({ fileName: input }) => - vpath.beneath(vfs.builtFolder, input, this.vfs.ignoreCase) || vpath.beneath(vfs.testLibFolder, input, this.vfs.ignoreCase) ? Utils.removeTestPathPrefixes(input) : - vpath.isAbsolute(input) ? vpath.relative(cwd, input, ignoreCase) : - input); - - resolutionInfo.emittedFiles = this.compilerResult.outputFiles! - .map(output => output.meta.get("fileName") || output.file) - .map(output => Utils.removeTestPathPrefixes(vpath.isAbsolute(output) ? vpath.relative(cwd, output, ignoreCase) : output)); - - const content = JSON.stringify(resolutionInfo, undefined, " "); - Harness.Baseline.runBaseline(this.getBaselineFolder(this.compilerResult.moduleKind) + this.testCaseJustName + ".json", content); } - - public verifyDiagnostics() { - if (this.compilerResult.errors.length) { - Harness.Baseline.runBaseline(this.getBaselineFolder(this.compilerResult.moduleKind) + this.testCaseJustName + ".errors.txt", getErrorsBaseline(this.compilerResult)); + } + // Project baselines verified go in project/testCaseName/moduleKind/ + private getBaselineFolder(moduleKind: ModuleKind) { + return "project/" + this.testCaseJustName + "/" + moduleNameToString(moduleKind) + "/"; + } + private cleanProjectUrl(url: string) { + let diskProjectPath = normalizeSlashes((IO.resolvePath(this.testCase.projectRoot)!)); + let projectRootUrl = "file:///" + diskProjectPath; + const normalizedProjectRoot = normalizeSlashes(this.testCase.projectRoot); + diskProjectPath = diskProjectPath.substr(0, diskProjectPath.lastIndexOf(normalizedProjectRoot)); + projectRootUrl = projectRootUrl.substr(0, projectRootUrl.lastIndexOf(normalizedProjectRoot)); + if (url && url.length) { + if (url.indexOf(projectRootUrl) === 0) { + // replace the disk specific project url path into project root url + url = "file:///" + url.substr(projectRootUrl.length); } - } - - public verifyJavaScriptOutput() { - if (this.testCase.baselineCheck) { - const errs: Error[] = []; - let nonSubfolderDiskFiles = 0; - for (const output of this.compilerResult.outputFiles!) { - try { - // convert file name to rooted name - // if filename is not rooted - concat it with project root and then expand project root relative to current directory - const fileName = output.meta.get("fileName") || output.file; - const diskFileName = vpath.isAbsolute(fileName) ? fileName : vpath.resolve(this.vfs.cwd(), fileName); - - // compute file name relative to current directory (expanded project root) - let diskRelativeName = vpath.relative(this.vfs.cwd(), diskFileName, this.vfs.ignoreCase); - if (vpath.isAbsolute(diskRelativeName) || diskRelativeName.startsWith("../")) { - // If the generated output file resides in the parent folder or is rooted path, - // we need to instead create files that can live in the project reference folder - // but make sure extension of these files matches with the fileName the compiler asked to write - diskRelativeName = `diskFile${nonSubfolderDiskFiles}${vpath.extname(fileName, [".js.map", ".js", ".d.ts"], this.vfs.ignoreCase)}`; - nonSubfolderDiskFiles++; - } - - const content = Utils.removeTestPathPrefixes(output.text, /*retainTrailingDirectorySeparator*/ true); - Harness.Baseline.runBaseline(this.getBaselineFolder(this.compilerResult.moduleKind) + diskRelativeName, content as string | null); // TODO: GH#18217 - } - catch (e) { - errs.push(e); - } - } - - if (errs.length) { - throw Error(errs.join("\n ")); + else if (url.indexOf(diskProjectPath) === 0) { + // Replace the disk specific path into the project root path + url = url.substr(diskProjectPath.length); + if (url.charCodeAt(0) !== CharacterCodes.slash) { + url = "/" + url; } } } - - public verifySourceMapRecord() { - // NOTE: This check was commented out in previous code. Leaving this here to eventually be restored if needed. - // if (compilerResult.sourceMapData) { - // Harness.Baseline.runBaseline(getBaselineFolder(compilerResult.moduleKind) + testCaseJustName + ".sourcemap.txt", () => { - // return Harness.SourceMapRecorder.getSourceMapRecord(compilerResult.sourceMapData, compilerResult.program, - // ts.filter(compilerResult.outputFiles, outputFile => Harness.Compiler.isJS(outputFile.emittedFileName))); - // }); - // } - } - - public verifyDeclarations() { - if (!this.compilerResult.errors.length && this.testCase.declaration) { - const dTsCompileResult = this.compileDeclarations(this.compilerResult); - if (dTsCompileResult && dTsCompileResult.errors.length) { - Harness.Baseline.runBaseline(this.getBaselineFolder(this.compilerResult.moduleKind) + this.testCaseJustName + ".dts.errors.txt", getErrorsBaseline(dTsCompileResult)); - } + return url; + } + private compileProjectFiles(moduleKind: ModuleKind, configFileSourceFiles: readonly SourceFile[], getInputFiles: () => readonly string[], compilerHost: ts.CompilerHost, compilerOptions: CompilerOptions): CompileProjectFilesResult { + const program = createProgram(getInputFiles(), compilerOptions, compilerHost); + const errors = getPreEmitDiagnostics(program); + const { sourceMaps: sourceMapData, diagnostics: emitDiagnostics } = program.emit(); + // Clean up source map data that will be used in baselining + if (sourceMapData) { + for (const data of sourceMapData) { + data.sourceMap = { + ...data.sourceMap, + sources: data.sourceMap.sources.map(source => this.cleanProjectUrl(source)), + sourceRoot: data.sourceMap.sourceRoot && this.cleanProjectUrl(data.sourceMap.sourceRoot) + }; } } - - // Project baselines verified go in project/testCaseName/moduleKind/ - private getBaselineFolder(moduleKind: ts.ModuleKind) { - return "project/" + this.testCaseJustName + "/" + moduleNameToString(moduleKind) + "/"; + return { + configFileSourceFiles, + moduleKind, + program, + errors: concatenate(errors, emitDiagnostics), + sourceMapData + }; + } + private compileDeclarations(compilerResult: BatchCompileProjectTestCaseResult) { + if (!compilerResult.program) { + return; } - - private cleanProjectUrl(url: string) { - let diskProjectPath = ts.normalizeSlashes(Harness.IO.resolvePath(this.testCase.projectRoot)!); - let projectRootUrl = "file:///" + diskProjectPath; - const normalizedProjectRoot = ts.normalizeSlashes(this.testCase.projectRoot); - diskProjectPath = diskProjectPath.substr(0, diskProjectPath.lastIndexOf(normalizedProjectRoot)); - projectRootUrl = projectRootUrl.substr(0, projectRootUrl.lastIndexOf(normalizedProjectRoot)); - if (url && url.length) { - if (url.indexOf(projectRootUrl) === 0) { - // replace the disk specific project url path into project root url - url = "file:///" + url.substr(projectRootUrl.length); - } - else if (url.indexOf(diskProjectPath) === 0) { - // Replace the disk specific path into the project root path - url = url.substr(diskProjectPath.length); - if (url.charCodeAt(0) !== ts.CharacterCodes.slash) { - url = "/" + url; - } + const compilerOptions = compilerResult.program.getCompilerOptions(); + const allInputFiles: TextDocument[] = []; + const rootFiles: string[] = []; + forEach(compilerResult.program.getSourceFiles(), sourceFile => { + if (sourceFile.isDeclarationFile) { + if (!isDefaultLibrary(sourceFile.fileName)) { + allInputFiles.unshift(new TextDocument(sourceFile.fileName, sourceFile.text)); } + rootFiles.unshift(sourceFile.fileName); } - - return url; - } - - private compileProjectFiles(moduleKind: ts.ModuleKind, configFileSourceFiles: readonly ts.SourceFile[], - getInputFiles: () => readonly string[], - compilerHost: ts.CompilerHost, - compilerOptions: ts.CompilerOptions): CompileProjectFilesResult { - - const program = ts.createProgram(getInputFiles(), compilerOptions, compilerHost); - const errors = ts.getPreEmitDiagnostics(program); - - const { sourceMaps: sourceMapData, diagnostics: emitDiagnostics } = program.emit(); - - // Clean up source map data that will be used in baselining - if (sourceMapData) { - for (const data of sourceMapData) { - data.sourceMap = { - ...data.sourceMap, - sources: data.sourceMap.sources.map(source => this.cleanProjectUrl(source)), - sourceRoot: data.sourceMap.sourceRoot && this.cleanProjectUrl(data.sourceMap.sourceRoot) - }; + else if (!(compilerOptions.outFile || compilerOptions.out)) { + let emitOutputFilePathWithoutExtension: string | undefined; + if (compilerOptions.outDir) { + let sourceFilePath = getNormalizedAbsolutePath(sourceFile.fileName, compilerResult.program!.getCurrentDirectory()); + sourceFilePath = sourceFilePath.replace(compilerResult.program!.getCommonSourceDirectory(), ""); + emitOutputFilePathWithoutExtension = removeFileExtension(combinePaths(compilerOptions.outDir, sourceFilePath)); } - } - - return { - configFileSourceFiles, - moduleKind, - program, - errors: ts.concatenate(errors, emitDiagnostics), - sourceMapData - }; - } - - private compileDeclarations(compilerResult: BatchCompileProjectTestCaseResult) { - if (!compilerResult.program) { - return; - } - - const compilerOptions = compilerResult.program.getCompilerOptions(); - const allInputFiles: documents.TextDocument[] = []; - const rootFiles: string[] = []; - ts.forEach(compilerResult.program.getSourceFiles(), sourceFile => { - if (sourceFile.isDeclarationFile) { - if (!vpath.isDefaultLibrary(sourceFile.fileName)) { - allInputFiles.unshift(new documents.TextDocument(sourceFile.fileName, sourceFile.text)); - } - rootFiles.unshift(sourceFile.fileName); + else { + emitOutputFilePathWithoutExtension = removeFileExtension(sourceFile.fileName); } - else if (!(compilerOptions.outFile || compilerOptions.out)) { - let emitOutputFilePathWithoutExtension: string | undefined; - if (compilerOptions.outDir) { - let sourceFilePath = ts.getNormalizedAbsolutePath(sourceFile.fileName, compilerResult.program!.getCurrentDirectory()); - sourceFilePath = sourceFilePath.replace(compilerResult.program!.getCommonSourceDirectory(), ""); - emitOutputFilePathWithoutExtension = ts.removeFileExtension(ts.combinePaths(compilerOptions.outDir, sourceFilePath)); - } - else { - emitOutputFilePathWithoutExtension = ts.removeFileExtension(sourceFile.fileName); - } - - const outputDtsFileName = emitOutputFilePathWithoutExtension + ts.Extension.Dts; - const file = findOutputDtsFile(outputDtsFileName); - if (file) { - allInputFiles.unshift(file); - rootFiles.unshift(file.meta.get("fileName") || file.file); - } + const outputDtsFileName = emitOutputFilePathWithoutExtension + Extension.Dts; + const file = findOutputDtsFile(outputDtsFileName); + if (file) { + allInputFiles.unshift(file); + rootFiles.unshift(file.meta.get("fileName") || file.file); } - else { - const outputDtsFileName = ts.removeFileExtension(compilerOptions.outFile || compilerOptions.out!) + ts.Extension.Dts; - const outputDtsFile = findOutputDtsFile(outputDtsFileName)!; - if (!ts.contains(allInputFiles, outputDtsFile)) { - allInputFiles.unshift(outputDtsFile); - rootFiles.unshift(outputDtsFile.meta.get("fileName") || outputDtsFile.file); - } + } + else { + const outputDtsFileName = removeFileExtension(compilerOptions.outFile || compilerOptions.out!) + Extension.Dts; + const outputDtsFile = findOutputDtsFile(outputDtsFileName)!; + if (!contains(allInputFiles, outputDtsFile)) { + allInputFiles.unshift(outputDtsFile); + rootFiles.unshift(outputDtsFile.meta.get("fileName") || outputDtsFile.file); } - }); - - const _vfs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false, { - documents: allInputFiles, - cwd: vpath.combine(vfs.srcFolder, this.testCase.projectRoot) - }); - - // Dont allow config files since we are compiling existing source options - const compilerHost = new ProjectCompilerHost(_vfs, compilerResult.compilerOptions!, this.testCaseJustName, this.testCase, compilerResult.moduleKind); - return this.compileProjectFiles(compilerResult.moduleKind, compilerResult.configFileSourceFiles, () => rootFiles, compilerHost, compilerResult.compilerOptions!); - - function findOutputDtsFile(fileName: string) { - return ts.forEach(compilerResult.outputFiles, outputFile => outputFile.meta.get("fileName") === fileName ? outputFile : undefined); } + }); + const _vfs = createFromFileSystem(IO, /*ignoreCase*/ false, { + documents: allInputFiles, + cwd: combine(srcFolder, this.testCase.projectRoot) + }); + // Dont allow config files since we are compiling existing source options + const compilerHost = new ProjectCompilerHost(_vfs, compilerResult.compilerOptions!, this.testCaseJustName, this.testCase, compilerResult.moduleKind); + return this.compileProjectFiles(compilerResult.moduleKind, compilerResult.configFileSourceFiles, () => rootFiles, compilerHost, compilerResult.compilerOptions!); + function findOutputDtsFile(fileName: string) { + return forEach(compilerResult.outputFiles, outputFile => outputFile.meta.get("fileName") === fileName ? outputFile : undefined); } } - - function moduleNameToString(moduleKind: ts.ModuleKind) { - return moduleKind === ts.ModuleKind.AMD ? "amd" : - moduleKind === ts.ModuleKind.CommonJS ? "node" : "none"; - } - - function getErrorsBaseline(compilerResult: CompileProjectFilesResult) { - const inputSourceFiles = compilerResult.configFileSourceFiles.slice(); - if (compilerResult.program) { - for (const sourceFile of compilerResult.program.getSourceFiles()) { - if (!Harness.isDefaultLibraryFile(sourceFile.fileName)) { - inputSourceFiles.push(sourceFile); - } +} +function moduleNameToString(moduleKind: ModuleKind) { + return moduleKind === ModuleKind.AMD ? "amd" : + moduleKind === ModuleKind.CommonJS ? "node" : "none"; +} +function getErrorsBaseline(compilerResult: CompileProjectFilesResult) { + const inputSourceFiles = compilerResult.configFileSourceFiles.slice(); + if (compilerResult.program) { + for (const sourceFile of compilerResult.program.getSourceFiles()) { + if (!isDefaultLibraryFile(sourceFile.fileName)) { + inputSourceFiles.push(sourceFile); } } - - const inputFiles = inputSourceFiles.map(sourceFile => ({ - unitName: ts.isRootedDiskPath(sourceFile.fileName) ? - Harness.RunnerBase.removeFullPaths(sourceFile.fileName) : - sourceFile.fileName, - content: sourceFile.text - })); - - return Harness.Compiler.getErrorBaseline(inputFiles, compilerResult.errors); } - - function createCompilerOptions(testCase: ProjectRunnerTestCase & ts.CompilerOptions, moduleKind: ts.ModuleKind) { - // Set the special options that depend on other testcase options - const compilerOptions: ts.CompilerOptions = { - noErrorTruncation: false, - skipDefaultLibCheck: false, - moduleResolution: ts.ModuleResolutionKind.Classic, - module: moduleKind, - newLine: ts.NewLineKind.CarriageReturnLineFeed, - mapRoot: testCase.resolveMapRoot && testCase.mapRoot - ? vpath.resolve(vfs.srcFolder, testCase.mapRoot) - : testCase.mapRoot, - - sourceRoot: testCase.resolveSourceRoot && testCase.sourceRoot - ? vpath.resolve(vfs.srcFolder, testCase.sourceRoot) - : testCase.sourceRoot - }; - - // Set the values specified using json - const optionNameMap = ts.arrayToMap(ts.optionDeclarations, option => option.name); - for (const name in testCase) { - if (name !== "mapRoot" && name !== "sourceRoot") { - const option = optionNameMap.get(name); - if (option) { - const optType = option.type; - let value = testCase[name]; - if (!ts.isString(optType)) { - const key = value.toLowerCase(); - const optTypeValue = optType.get(key); - if (optTypeValue) { - value = optTypeValue; - } + const inputFiles = inputSourceFiles.map(sourceFile => ({ + unitName: isRootedDiskPath(sourceFile.fileName) ? + RunnerBase.removeFullPaths(sourceFile.fileName) : + sourceFile.fileName, + content: sourceFile.text + })); + return Compiler.getErrorBaseline(inputFiles, compilerResult.errors); +} +function createCompilerOptions(testCase: ProjectRunnerTestCase & CompilerOptions, moduleKind: ModuleKind) { + // Set the special options that depend on other testcase options + const compilerOptions: CompilerOptions = { + noErrorTruncation: false, + skipDefaultLibCheck: false, + moduleResolution: ModuleResolutionKind.Classic, + module: moduleKind, + newLine: NewLineKind.CarriageReturnLineFeed, + mapRoot: testCase.resolveMapRoot && testCase.mapRoot + ? resolve(srcFolder, testCase.mapRoot) + : testCase.mapRoot, + sourceRoot: testCase.resolveSourceRoot && testCase.sourceRoot + ? resolve(srcFolder, testCase.sourceRoot) + : testCase.sourceRoot + }; + // Set the values specified using json + const optionNameMap = arrayToMap(optionDeclarations, option => option.name); + for (const name in testCase) { + if (name !== "mapRoot" && name !== "sourceRoot") { + const option = optionNameMap.get(name); + if (option) { + const optType = option.type; + let value = testCase[name]; + if (!isString(optType)) { + const key = value.toLowerCase(); + const optTypeValue = optType.get(key); + if (optTypeValue) { + value = optTypeValue; } - compilerOptions[option.name] = value; } + compilerOptions[option.name] = value; } } - - return compilerOptions; } + return compilerOptions; } diff --git a/src/testRunner/runner.ts b/src/testRunner/runner.ts index 6c85e0c53199d..60c6f0aca6380 100644 --- a/src/testRunner/runner.ts +++ b/src/testRunner/runner.ts @@ -1,259 +1,239 @@ -namespace Harness { - /* eslint-disable prefer-const */ - export let runners: RunnerBase[] = []; - export let iterations = 1; - /* eslint-enable prefer-const */ - - function runTests(runners: RunnerBase[]) { - for (let i = iterations; i > 0; i--) { - for (const runner of runners) { - runner.initializeTests(); - } +import { RunnerBase, TestRunnerKind, CompilerBaselineRunner, CompilerTestType, FourSlashRunner, Test262BaselineRunner, UserCodeRunner, DefinitelyTypedRunner, DockerfileRunner, IO, setLightMode, setShardId, setShards, GeneratedFourslashRunner } from "./Harness"; +import { forEach, Debug, getUILocale, setUILocale, noop } from "./ts"; +import { FourSlashTestType } from "./FourSlash"; +import { ProjectRunner } from "./project"; +import { RWCRunner } from "./RWC"; +import { start } from "./Harness.Parallel.Worker"; +import { Host } from "./Harness.Parallel"; +/* eslint-disable prefer-const */ +export let runners: RunnerBase[] = []; +export let iterations = 1; +/* eslint-enable prefer-const */ +function runTests(runners: RunnerBase[]) { + for (let i = iterations; i > 0; i--) { + for (const runner of runners) { + runner.initializeTests(); } } - - function tryGetConfig(args: string[]) { - const prefix = "--config="; - const configPath = ts.forEach(args, arg => arg.lastIndexOf(prefix, 0) === 0 && arg.substr(prefix.length)); - // strip leading and trailing quotes from the path (necessary on Windows since shell does not do it automatically) - return configPath && configPath.replace(/(^[\"'])|([\"']$)/g, ""); +} +function tryGetConfig(args: string[]) { + const prefix = "--config="; + const configPath = forEach(args, arg => arg.lastIndexOf(prefix, 0) === 0 && arg.substr(prefix.length)); + // strip leading and trailing quotes from the path (necessary on Windows since shell does not do it automatically) + return configPath && configPath.replace(/(^[\"'])|([\"']$)/g, ""); +} +export function createRunner(kind: TestRunnerKind): RunnerBase { + switch (kind) { + case "conformance": + return new CompilerBaselineRunner(CompilerTestType.Conformance); + case "compiler": + return new CompilerBaselineRunner(CompilerTestType.Regressions); + case "fourslash": + return new FourSlashRunner(FourSlashTestType.Native); + case "fourslash-shims": + return new FourSlashRunner(FourSlashTestType.Shims); + case "fourslash-shims-pp": + return new FourSlashRunner(FourSlashTestType.ShimsWithPreprocess); + case "fourslash-server": + return new FourSlashRunner(FourSlashTestType.Server); + case "project": + return new ProjectRunner(); + case "rwc": + return new RWCRunner(); + case "test262": + return new Test262BaselineRunner(); + case "user": + return new UserCodeRunner(); + case "dt": + return new DefinitelyTypedRunner(); + case "docker": + return new DockerfileRunner(); } - - export function createRunner(kind: TestRunnerKind): RunnerBase { - switch (kind) { - case "conformance": - return new CompilerBaselineRunner(CompilerTestType.Conformance); - case "compiler": - return new CompilerBaselineRunner(CompilerTestType.Regressions); - case "fourslash": - return new FourSlashRunner(FourSlash.FourSlashTestType.Native); - case "fourslash-shims": - return new FourSlashRunner(FourSlash.FourSlashTestType.Shims); - case "fourslash-shims-pp": - return new FourSlashRunner(FourSlash.FourSlashTestType.ShimsWithPreprocess); - case "fourslash-server": - return new FourSlashRunner(FourSlash.FourSlashTestType.Server); - case "project": - return new project.ProjectRunner(); - case "rwc": - return new RWC.RWCRunner(); - case "test262": - return new Test262BaselineRunner(); - case "user": - return new UserCodeRunner(); - case "dt": - return new DefinitelyTypedRunner(); - case "docker": - return new DockerfileRunner(); + return Debug.fail(`Unknown runner kind ${kind}`); +} +// users can define tests to run in mytest.config that will override cmd line args, otherwise use cmd line args (test.config), otherwise no options +const mytestconfigFileName = "mytest.config"; +const testconfigFileName = "test.config"; +const customConfig = tryGetConfig(IO.args()); +const testConfigContent = customConfig && IO.fileExists(customConfig) + ? IO.readFile(customConfig)! + : IO.fileExists(mytestconfigFileName) + ? IO.readFile(mytestconfigFileName)! + : IO.fileExists(testconfigFileName) ? IO.readFile(testconfigFileName)! : ""; +export let taskConfigsFolder: string; +export let workerCount: number; +export let runUnitTests: boolean | undefined; +export let stackTraceLimit: number | "full" | undefined; +export let noColors = false; +export let keepFailed = false; +export interface TestConfig { + light?: boolean; + taskConfigsFolder?: string; + listenForWork?: boolean; + workerCount?: number; + stackTraceLimit?: number | "full"; + test?: string[]; + runners?: string[]; + runUnitTests?: boolean; + noColors?: boolean; + timeout?: number; + keepFailed?: boolean; + shardId?: number; + shards?: number; +} +export interface TaskSet { + runner: TestRunnerKind; + files: string[]; +} +export let configOption: string; +export let globalTimeout: number; +function handleTestConfig() { + if (testConfigContent !== "") { + const testConfig = JSON.parse(testConfigContent); + if (testConfig.light) { + setLightMode(true); } - return ts.Debug.fail(`Unknown runner kind ${kind}`); - } - - // users can define tests to run in mytest.config that will override cmd line args, otherwise use cmd line args (test.config), otherwise no options - - const mytestconfigFileName = "mytest.config"; - const testconfigFileName = "test.config"; - - const customConfig = tryGetConfig(IO.args()); - const testConfigContent = - customConfig && IO.fileExists(customConfig) - ? IO.readFile(customConfig)! - : IO.fileExists(mytestconfigFileName) - ? IO.readFile(mytestconfigFileName)! - : IO.fileExists(testconfigFileName) ? IO.readFile(testconfigFileName)! : ""; - - export let taskConfigsFolder: string; - export let workerCount: number; - export let runUnitTests: boolean | undefined; - export let stackTraceLimit: number | "full" | undefined; - export let noColors = false; - export let keepFailed = false; - - export interface TestConfig { - light?: boolean; - taskConfigsFolder?: string; - listenForWork?: boolean; - workerCount?: number; - stackTraceLimit?: number | "full"; - test?: string[]; - runners?: string[]; - runUnitTests?: boolean; - noColors?: boolean; - timeout?: number; - keepFailed?: boolean; - shardId?: number; - shards?: number; - } - - export interface TaskSet { - runner: TestRunnerKind; - files: string[]; - } - - export let configOption: string; - export let globalTimeout: number; - function handleTestConfig() { - if (testConfigContent !== "") { - const testConfig = JSON.parse(testConfigContent); - if (testConfig.light) { - setLightMode(true); - } - if (testConfig.timeout) { - globalTimeout = testConfig.timeout; - } - runUnitTests = testConfig.runUnitTests; - if (testConfig.workerCount) { - workerCount = +testConfig.workerCount; - } - if (testConfig.taskConfigsFolder) { - taskConfigsFolder = testConfig.taskConfigsFolder; - } - if (testConfig.noColors !== undefined) { - noColors = testConfig.noColors; - } - if (testConfig.keepFailed) { - keepFailed = true; - } - if (testConfig.shardId) { - setShardId(testConfig.shardId); - } - if (testConfig.shards) { - setShards(testConfig.shards); - } - - if (testConfig.stackTraceLimit === "full") { - (Error).stackTraceLimit = Infinity; - stackTraceLimit = testConfig.stackTraceLimit; - } - else if ((+testConfig.stackTraceLimit! | 0) > 0) { - (Error).stackTraceLimit = +testConfig.stackTraceLimit! | 0; - stackTraceLimit = +testConfig.stackTraceLimit! | 0; - } - if (testConfig.listenForWork) { - return true; + if (testConfig.timeout) { + globalTimeout = testConfig.timeout; + } + runUnitTests = testConfig.runUnitTests; + if (testConfig.workerCount) { + workerCount = +testConfig.workerCount; + } + if (testConfig.taskConfigsFolder) { + taskConfigsFolder = testConfig.taskConfigsFolder; + } + if (testConfig.noColors !== undefined) { + noColors = testConfig.noColors; + } + if (testConfig.keepFailed) { + keepFailed = true; + } + if (testConfig.shardId) { + setShardId(testConfig.shardId); + } + if (testConfig.shards) { + setShards(testConfig.shards); + } + if (testConfig.stackTraceLimit === "full") { + (Error).stackTraceLimit = Infinity; + stackTraceLimit = testConfig.stackTraceLimit; + } + else if ((+testConfig.stackTraceLimit! | 0) > 0) { + (Error).stackTraceLimit = +testConfig.stackTraceLimit! | 0; + stackTraceLimit = +testConfig.stackTraceLimit! | 0; + } + if (testConfig.listenForWork) { + return true; + } + const runnerConfig = testConfig.runners || testConfig.test; + if (runnerConfig && runnerConfig.length > 0) { + if (testConfig.runners) { + runUnitTests = runnerConfig.indexOf("unittest") !== -1; } - - const runnerConfig = testConfig.runners || testConfig.test; - if (runnerConfig && runnerConfig.length > 0) { - if (testConfig.runners) { - runUnitTests = runnerConfig.indexOf("unittest") !== -1; + for (const option of runnerConfig) { + if (!option) { + continue; } - for (const option of runnerConfig) { - if (!option) { - continue; - } - - if (!configOption) { - configOption = option; - } - else { - configOption += "+" + option; - } - - switch (option) { - case "compiler": - runners.push(new CompilerBaselineRunner(CompilerTestType.Conformance)); - runners.push(new CompilerBaselineRunner(CompilerTestType.Regressions)); - break; - case "conformance": - runners.push(new CompilerBaselineRunner(CompilerTestType.Conformance)); - break; - case "project": - runners.push(new project.ProjectRunner()); - break; - case "fourslash": - runners.push(new FourSlashRunner(FourSlash.FourSlashTestType.Native)); - break; - case "fourslash-shims": - runners.push(new FourSlashRunner(FourSlash.FourSlashTestType.Shims)); - break; - case "fourslash-shims-pp": - runners.push(new FourSlashRunner(FourSlash.FourSlashTestType.ShimsWithPreprocess)); - break; - case "fourslash-server": - runners.push(new FourSlashRunner(FourSlash.FourSlashTestType.Server)); - break; - case "fourslash-generated": - runners.push(new GeneratedFourslashRunner(FourSlash.FourSlashTestType.Native)); - break; - case "rwc": - runners.push(new RWC.RWCRunner()); - break; - case "test262": - runners.push(new Test262BaselineRunner()); - break; - case "user": - runners.push(new UserCodeRunner()); - break; - case "dt": - runners.push(new DefinitelyTypedRunner()); - break; - case "docker": - runners.push(new DockerfileRunner()); - break; - } + if (!configOption) { + configOption = option; + } + else { + configOption += "+" + option; + } + switch (option) { + case "compiler": + runners.push(new CompilerBaselineRunner(CompilerTestType.Conformance)); + runners.push(new CompilerBaselineRunner(CompilerTestType.Regressions)); + break; + case "conformance": + runners.push(new CompilerBaselineRunner(CompilerTestType.Conformance)); + break; + case "project": + runners.push(new ProjectRunner()); + break; + case "fourslash": + runners.push(new FourSlashRunner(FourSlashTestType.Native)); + break; + case "fourslash-shims": + runners.push(new FourSlashRunner(FourSlashTestType.Shims)); + break; + case "fourslash-shims-pp": + runners.push(new FourSlashRunner(FourSlashTestType.ShimsWithPreprocess)); + break; + case "fourslash-server": + runners.push(new FourSlashRunner(FourSlashTestType.Server)); + break; + case "fourslash-generated": + runners.push(new GeneratedFourslashRunner(FourSlashTestType.Native)); + break; + case "rwc": + runners.push(new RWCRunner()); + break; + case "test262": + runners.push(new Test262BaselineRunner()); + break; + case "user": + runners.push(new UserCodeRunner()); + break; + case "dt": + runners.push(new DefinitelyTypedRunner()); + break; + case "docker": + runners.push(new DockerfileRunner()); + break; } } } - - if (runners.length === 0) { - // compiler - runners.push(new CompilerBaselineRunner(CompilerTestType.Conformance)); - runners.push(new CompilerBaselineRunner(CompilerTestType.Regressions)); - - runners.push(new project.ProjectRunner()); - - // language services - runners.push(new FourSlashRunner(FourSlash.FourSlashTestType.Native)); - runners.push(new FourSlashRunner(FourSlash.FourSlashTestType.Shims)); - runners.push(new FourSlashRunner(FourSlash.FourSlashTestType.ShimsWithPreprocess)); - runners.push(new FourSlashRunner(FourSlash.FourSlashTestType.Server)); - // runners.push(new GeneratedFourslashRunner()); - - // CRON-only tests - if (process.env.TRAVIS_EVENT_TYPE === "cron") { - runners.push(new UserCodeRunner()); - runners.push(new DockerfileRunner()); - } - } - if (runUnitTests === undefined) { - runUnitTests = runners.length !== 1; // Don't run unit tests when running only one runner if unit tests were not explicitly asked for - } - return false; } - - function beginTests() { - if (ts.Debug.isDebugging) { - ts.Debug.enableDebugInfo(); - } - - // run tests in en-US by default. - let savedUILocale: string | undefined; - beforeEach(() => { - savedUILocale = ts.getUILocale(); - ts.setUILocale("en-US"); - }); - afterEach(() => ts.setUILocale(savedUILocale)); - - runTests(runners); - - if (!runUnitTests) { - // patch `describe` to skip unit tests - (global as any).describe = ts.noop; + if (runners.length === 0) { + // compiler + runners.push(new CompilerBaselineRunner(CompilerTestType.Conformance)); + runners.push(new CompilerBaselineRunner(CompilerTestType.Regressions)); + runners.push(new ProjectRunner()); + // language services + runners.push(new FourSlashRunner(FourSlashTestType.Native)); + runners.push(new FourSlashRunner(FourSlashTestType.Shims)); + runners.push(new FourSlashRunner(FourSlashTestType.ShimsWithPreprocess)); + runners.push(new FourSlashRunner(FourSlashTestType.Server)); + // runners.push(new GeneratedFourslashRunner()); + // CRON-only tests + if (process.env.TRAVIS_EVENT_TYPE === "cron") { + runners.push(new UserCodeRunner()); + runners.push(new DockerfileRunner()); } } - - export let isWorker: boolean; - function startTestEnvironment() { - isWorker = handleTestConfig(); - if (isWorker) { - return Parallel.Worker.start(); - } - else if (taskConfigsFolder && workerCount && workerCount > 1) { - return Parallel.Host.start(); - } - beginTests(); + if (runUnitTests === undefined) { + runUnitTests = runners.length !== 1; // Don't run unit tests when running only one runner if unit tests were not explicitly asked for + } + return false; +} +function beginTests() { + if (Debug.isDebugging) { + Debug.enableDebugInfo(); + } + // run tests in en-US by default. + let savedUILocale: string | undefined; + beforeEach(() => { + savedUILocale = getUILocale(); + setUILocale("en-US"); + }); + afterEach(() => setUILocale(savedUILocale)); + runTests(runners); + if (!runUnitTests) { + // patch `describe` to skip unit tests + (global as any).describe = noop; + } +} +export let isWorker: boolean; +function startTestEnvironment() { + isWorker = handleTestConfig(); + if (isWorker) { + return start(); + } + else if (taskConfigsFolder && workerCount && workerCount > 1) { + return Host.start(); } - - startTestEnvironment(); -} \ No newline at end of file + beginTests(); +} +startTestEnvironment(); diff --git a/src/testRunner/rwcRunner.ts b/src/testRunner/rwcRunner.ts index a50447b2d0065..f7c634222b37f 100644 --- a/src/testRunner/rwcRunner.ts +++ b/src/testRunner/rwcRunner.ts @@ -1,235 +1,207 @@ +import { IoLog, wrapIO, newStyleLogIntoOldStyleLog } from "./Playback"; +import { IO, setHarnessIO, Compiler, Baseline, isDefaultLibraryFile, RunnerBase, TestRunnerKind } from "./Harness"; +import { CompilationResult } from "./compiler"; +import { CompilerOptions, getBaseFileName, ParsedCommandLine, parseCommandLine, forEach, parseJsonText, ParseConfigHost, parseJsonSourceFileConfigFileContent, getDirectoryPath, extend, setConfigFileInOptions, createMap, normalizeSlashes } from "./ts"; +import { isTsConfigFile } from "./vpath"; // In harness baselines, null is different than undefined. See `generateActual` in `harness.ts`. -namespace RWC { - function runWithIOLog(ioLog: Playback.IoLog, fn: (oldIO: Harness.IO) => void) { - const oldIO = Harness.IO; - - const wrappedIO = Playback.wrapIO(oldIO); - wrappedIO.startReplayFromData(ioLog); - Harness.setHarnessIO(wrappedIO); - - try { - fn(oldIO); - } - finally { - wrappedIO.endReplay(); - Harness.setHarnessIO(oldIO); - } +function runWithIOLog(ioLog: IoLog, fn: (oldIO: IO) => void) { + const oldIO = IO; + const wrappedIO = wrapIO(oldIO); + wrappedIO.startReplayFromData(ioLog); + setHarnessIO(wrappedIO); + try { + fn(oldIO); + } + finally { + wrappedIO.endReplay(); + setHarnessIO(oldIO); } - - export function runRWCTest(jsonPath: string) { - describe("Testing a rwc project: " + jsonPath, () => { - let inputFiles: Harness.Compiler.TestFile[] = []; - let otherFiles: Harness.Compiler.TestFile[] = []; - let tsconfigFiles: Harness.Compiler.TestFile[] = []; - let compilerResult: compiler.CompilationResult; - let compilerOptions: ts.CompilerOptions; - const baselineOpts: Harness.Baseline.BaselineOptions = { - Subfolder: "rwc", - Baselinefolder: "internal/baselines" - }; - const baseName = ts.getBaseFileName(jsonPath); - let currentDirectory: string; - let useCustomLibraryFile: boolean; - let caseSensitive: boolean; - after(() => { - // Mocha holds onto the closure environment of the describe callback even after the test is done. - // Therefore we have to clean out large objects after the test is done. - inputFiles = []; - otherFiles = []; - tsconfigFiles = []; - compilerResult = undefined!; - compilerOptions = undefined!; - currentDirectory = undefined!; - // useCustomLibraryFile is a flag specified in the json object to indicate whether to use built/local/lib.d.ts - // or to use lib.d.ts inside the json object. If the flag is true, use the lib.d.ts inside json file - // otherwise use the lib.d.ts from built/local - useCustomLibraryFile = false; - caseSensitive = false; +} +export function runRWCTest(jsonPath: string) { + describe("Testing a rwc project: " + jsonPath, () => { + let inputFiles: Compiler.TestFile[] = []; + let otherFiles: Compiler.TestFile[] = []; + let tsconfigFiles: Compiler.TestFile[] = []; + let compilerResult: CompilationResult; + let compilerOptions: CompilerOptions; + const baselineOpts: Baseline.BaselineOptions = { + Subfolder: "rwc", + Baselinefolder: "internal/baselines" + }; + const baseName = getBaseFileName(jsonPath); + let currentDirectory: string; + let useCustomLibraryFile: boolean; + let caseSensitive: boolean; + after(() => { + // Mocha holds onto the closure environment of the describe callback even after the test is done. + // Therefore we have to clean out large objects after the test is done. + inputFiles = []; + otherFiles = []; + tsconfigFiles = []; + compilerResult = undefined!; + compilerOptions = undefined!; + currentDirectory = undefined!; + // useCustomLibraryFile is a flag specified in the json object to indicate whether to use built/local/lib.d.ts + // or to use lib.d.ts inside the json object. If the flag is true, use the lib.d.ts inside json file + // otherwise use the lib.d.ts from built/local + useCustomLibraryFile = false; + caseSensitive = false; + }); + it("can compile", function (this: Mocha.ITestCallbackContext) { + this.timeout(800000); // Allow long timeouts for RWC compilations + let opts!: ParsedCommandLine; + const ioLog: IoLog = newStyleLogIntoOldStyleLog(JSON.parse((IO.readFile(`internal/cases/rwc/${jsonPath}/test.json`)!)), IO, `internal/cases/rwc/${baseName}`); + currentDirectory = ioLog.currentDirectory; + useCustomLibraryFile = !!ioLog.useCustomLibraryFile; + runWithIOLog(ioLog, () => { + opts = parseCommandLine(ioLog.arguments, fileName => IO.readFile(fileName)); + assert.equal(opts.errors.length, 0); + // To provide test coverage of output javascript file, + // we will set noEmitOnError flag to be false. + opts.options.noEmitOnError = false; }); - - it("can compile", function (this: Mocha.ITestCallbackContext) { - this.timeout(800_000); // Allow long timeouts for RWC compilations - let opts!: ts.ParsedCommandLine; - - const ioLog: Playback.IoLog = Playback.newStyleLogIntoOldStyleLog(JSON.parse(Harness.IO.readFile(`internal/cases/rwc/${jsonPath}/test.json`)!), Harness.IO, `internal/cases/rwc/${baseName}`); - currentDirectory = ioLog.currentDirectory; - useCustomLibraryFile = !!ioLog.useCustomLibraryFile; - runWithIOLog(ioLog, () => { - opts = ts.parseCommandLine(ioLog.arguments, fileName => Harness.IO.readFile(fileName)); - assert.equal(opts.errors.length, 0); - - // To provide test coverage of output javascript file, - // we will set noEmitOnError flag to be false. - opts.options.noEmitOnError = false; - }); - let fileNames = opts.fileNames; - - runWithIOLog(ioLog, () => { - const tsconfigFile = ts.forEach(ioLog.filesRead, f => vpath.isTsConfigFile(f.path) ? f : undefined); - if (tsconfigFile) { - const tsconfigFileContents = getHarnessCompilerInputUnit(tsconfigFile.path); - tsconfigFiles.push({ unitName: tsconfigFile.path, content: tsconfigFileContents.content }); - const parsedTsconfigFileContents = ts.parseJsonText(tsconfigFile.path, tsconfigFileContents.content); - const configParseHost: ts.ParseConfigHost = { - useCaseSensitiveFileNames: Harness.IO.useCaseSensitiveFileNames(), - fileExists: Harness.IO.fileExists, - readDirectory: Harness.IO.readDirectory, - readFile: Harness.IO.readFile - }; - const configParseResult = ts.parseJsonSourceFileConfigFileContent(parsedTsconfigFileContents, configParseHost, ts.getDirectoryPath(tsconfigFile.path)); - fileNames = configParseResult.fileNames; - opts.options = ts.extend(opts.options, configParseResult.options); - ts.setConfigFileInOptions(opts.options, configParseResult.options.configFile); - } - - // Deduplicate files so they are only printed once in baselines (they are deduplicated within the compiler already) - const uniqueNames = ts.createMap(); - for (const fileName of fileNames) { - // Must maintain order, build result list while checking map - const normalized = ts.normalizeSlashes(Harness.IO.resolvePath(fileName)!); - if (!uniqueNames.has(normalized)) { - uniqueNames.set(normalized, true); - // Load the file - inputFiles.push(getHarnessCompilerInputUnit(fileName)); - } - } - - // Add files to compilation - for (const fileRead of ioLog.filesRead) { - const unitName = ts.normalizeSlashes(Harness.IO.resolvePath(fileRead.path)!); - if (!uniqueNames.has(unitName) && !Harness.isDefaultLibraryFile(fileRead.path)) { - uniqueNames.set(unitName, true); - otherFiles.push(getHarnessCompilerInputUnit(fileRead.path)); - } - else if (!opts.options.noLib && Harness.isDefaultLibraryFile(fileRead.path) && !uniqueNames.has(unitName) && useCustomLibraryFile) { - // If useCustomLibraryFile is true, we will use lib.d.ts from json object - // otherwise use the lib.d.ts from built/local - // Majority of RWC code will be using built/local/lib.d.ts instead of - // lib.d.ts inside json file. However, some RWC cases will still use - // their own version of lib.d.ts because they have customized lib.d.ts - uniqueNames.set(unitName, true); - inputFiles.push(getHarnessCompilerInputUnit(fileRead.path)); - } - } - }); - - if (useCustomLibraryFile) { - // do not use lib since we already read it in above - opts.options.lib = undefined; - opts.options.noLib = true; + let fileNames = opts.fileNames; + runWithIOLog(ioLog, () => { + const tsconfigFile = forEach(ioLog.filesRead, f => isTsConfigFile(f.path) ? f : undefined); + if (tsconfigFile) { + const tsconfigFileContents = getHarnessCompilerInputUnit(tsconfigFile.path); + tsconfigFiles.push({ unitName: tsconfigFile.path, content: tsconfigFileContents.content }); + const parsedTsconfigFileContents = parseJsonText(tsconfigFile.path, tsconfigFileContents.content); + const configParseHost: ParseConfigHost = { + useCaseSensitiveFileNames: IO.useCaseSensitiveFileNames(), + fileExists: IO.fileExists, + readDirectory: IO.readDirectory, + readFile: IO.readFile + }; + const configParseResult = parseJsonSourceFileConfigFileContent(parsedTsconfigFileContents, configParseHost, getDirectoryPath(tsconfigFile.path)); + fileNames = configParseResult.fileNames; + opts.options = extend(opts.options, configParseResult.options); + setConfigFileInOptions(opts.options, configParseResult.options.configFile); } - - caseSensitive = ioLog.useCaseSensitiveFileNames || false; - // Emit the results - compilerResult = Harness.Compiler.compileFiles( - inputFiles, - otherFiles, - { useCaseSensitiveFileNames: "" + caseSensitive }, - opts.options, - // Since each RWC json file specifies its current directory in its json file, we need - // to pass this information in explicitly instead of acquiring it from the process. - currentDirectory); - compilerOptions = compilerResult.options; - - function getHarnessCompilerInputUnit(fileName: string): Harness.Compiler.TestFile { - const unitName = ts.normalizeSlashes(Harness.IO.resolvePath(fileName)!); - let content: string; - try { - content = Harness.IO.readFile(unitName)!; + // Deduplicate files so they are only printed once in baselines (they are deduplicated within the compiler already) + const uniqueNames = createMap(); + for (const fileName of fileNames) { + // Must maintain order, build result list while checking map + const normalized = normalizeSlashes((IO.resolvePath(fileName)!)); + if (!uniqueNames.has(normalized)) { + uniqueNames.set(normalized, true); + // Load the file + inputFiles.push(getHarnessCompilerInputUnit(fileName)); } - catch (e) { - content = Harness.IO.readFile(fileName)!; - } - return { unitName, content }; } - }); - - - it("has the expected emitted code", function (this: Mocha.ITestCallbackContext) { - this.timeout(100_000); // Allow longer timeouts for RWC js verification - Harness.Baseline.runMultifileBaseline(baseName, "", () => { - return Harness.Compiler.iterateOutputs(compilerResult.js.values()); - }, baselineOpts, [".js", ".jsx"]); - }); - - it("has the expected declaration file content", () => { - Harness.Baseline.runMultifileBaseline(baseName, "", () => { - if (!compilerResult.dts.size) { - return null; // eslint-disable-line no-null/no-null + // Add files to compilation + for (const fileRead of ioLog.filesRead) { + const unitName = normalizeSlashes((IO.resolvePath(fileRead.path)!)); + if (!uniqueNames.has(unitName) && !isDefaultLibraryFile(fileRead.path)) { + uniqueNames.set(unitName, true); + otherFiles.push(getHarnessCompilerInputUnit(fileRead.path)); } - - return Harness.Compiler.iterateOutputs(compilerResult.dts.values()); - }, baselineOpts, [".d.ts"]); - }); - - it("has the expected source maps", () => { - Harness.Baseline.runMultifileBaseline(baseName, "", () => { - if (!compilerResult.maps.size) { - return null; // eslint-disable-line no-null/no-null + else if (!opts.options.noLib && isDefaultLibraryFile(fileRead.path) && !uniqueNames.has(unitName) && useCustomLibraryFile) { + // If useCustomLibraryFile is true, we will use lib.d.ts from json object + // otherwise use the lib.d.ts from built/local + // Majority of RWC code will be using built/local/lib.d.ts instead of + // lib.d.ts inside json file. However, some RWC cases will still use + // their own version of lib.d.ts because they have customized lib.d.ts + uniqueNames.set(unitName, true); + inputFiles.push(getHarnessCompilerInputUnit(fileRead.path)); } - - return Harness.Compiler.iterateOutputs(compilerResult.maps.values()); - }, baselineOpts, [".map"]); + } }); - - it("has the expected errors", () => { - Harness.Baseline.runMultifileBaseline(baseName, ".errors.txt", () => { + if (useCustomLibraryFile) { + // do not use lib since we already read it in above + opts.options.lib = undefined; + opts.options.noLib = true; + } + caseSensitive = ioLog.useCaseSensitiveFileNames || false; + // Emit the results + compilerResult = Compiler.compileFiles(inputFiles, otherFiles, { useCaseSensitiveFileNames: "" + caseSensitive }, opts.options, + // Since each RWC json file specifies its current directory in its json file, we need + // to pass this information in explicitly instead of acquiring it from the process. + currentDirectory); + compilerOptions = compilerResult.options; + function getHarnessCompilerInputUnit(fileName: string): Compiler.TestFile { + const unitName = normalizeSlashes((IO.resolvePath(fileName)!)); + let content: string; + try { + content = (IO.readFile(unitName)!); + } + catch (e) { + content = (IO.readFile(fileName)!); + } + return { unitName, content }; + } + }); + it("has the expected emitted code", function (this: Mocha.ITestCallbackContext) { + this.timeout(100000); // Allow longer timeouts for RWC js verification + Baseline.runMultifileBaseline(baseName, "", () => { + return Compiler.iterateOutputs(compilerResult.js.values()); + }, baselineOpts, [".js", ".jsx"]); + }); + it("has the expected declaration file content", () => { + Baseline.runMultifileBaseline(baseName, "", () => { + if (!compilerResult.dts.size) { + return null; // eslint-disable-line no-null/no-null + } + return Compiler.iterateOutputs(compilerResult.dts.values()); + }, baselineOpts, [".d.ts"]); + }); + it("has the expected source maps", () => { + Baseline.runMultifileBaseline(baseName, "", () => { + if (!compilerResult.maps.size) { + return null; // eslint-disable-line no-null/no-null + } + return Compiler.iterateOutputs(compilerResult.maps.values()); + }, baselineOpts, [".map"]); + }); + it("has the expected errors", () => { + Baseline.runMultifileBaseline(baseName, ".errors.txt", () => { + if (compilerResult.diagnostics.length === 0) { + return null; // eslint-disable-line no-null/no-null + } + // Do not include the library in the baselines to avoid noise + const baselineFiles = tsconfigFiles.concat(inputFiles, otherFiles).filter(f => !isDefaultLibraryFile(f.unitName)); + const errors = compilerResult.diagnostics.filter(e => !e.file || !isDefaultLibraryFile(e.file.fileName)); + return Compiler.iterateErrorBaseline(baselineFiles, errors, { caseSensitive, currentDirectory }); + }, baselineOpts); + }); + // Ideally, a generated declaration file will have no errors. But we allow generated + // declaration file errors as part of the baseline. + it("has the expected errors in generated declaration files", () => { + if (compilerOptions.declaration && !compilerResult.diagnostics.length) { + Baseline.runMultifileBaseline(baseName, ".dts.errors.txt", () => { if (compilerResult.diagnostics.length === 0) { return null; // eslint-disable-line no-null/no-null } - // Do not include the library in the baselines to avoid noise - const baselineFiles = tsconfigFiles.concat(inputFiles, otherFiles).filter(f => !Harness.isDefaultLibraryFile(f.unitName)); - const errors = compilerResult.diagnostics.filter(e => !e.file || !Harness.isDefaultLibraryFile(e.file.fileName)); - return Harness.Compiler.iterateErrorBaseline(baselineFiles, errors, { caseSensitive, currentDirectory }); + const declContext = Compiler.prepareDeclarationCompilationContext(inputFiles, otherFiles, compilerResult, /*harnessSettings*/ (undefined!), compilerOptions, currentDirectory // TODO: GH#18217 + ); + // Reset compilerResult before calling into `compileDeclarationFiles` so the memory from the original compilation can be freed + const links = compilerResult.symlinks; + compilerResult = undefined!; + const declFileCompilationResult = (Compiler.compileDeclarationFiles(declContext, links)!); + return Compiler.iterateErrorBaseline(tsconfigFiles.concat(declFileCompilationResult.declInputFiles, declFileCompilationResult.declOtherFiles), declFileCompilationResult.declResult.diagnostics, { caseSensitive, currentDirectory }); }, baselineOpts); - }); - - // Ideally, a generated declaration file will have no errors. But we allow generated - // declaration file errors as part of the baseline. - it("has the expected errors in generated declaration files", () => { - if (compilerOptions.declaration && !compilerResult.diagnostics.length) { - Harness.Baseline.runMultifileBaseline(baseName, ".dts.errors.txt", () => { - if (compilerResult.diagnostics.length === 0) { - return null; // eslint-disable-line no-null/no-null - } - - const declContext = Harness.Compiler.prepareDeclarationCompilationContext( - inputFiles, otherFiles, compilerResult, /*harnessSettings*/ undefined!, compilerOptions, currentDirectory // TODO: GH#18217 - ); - // Reset compilerResult before calling into `compileDeclarationFiles` so the memory from the original compilation can be freed - const links = compilerResult.symlinks; - compilerResult = undefined!; - const declFileCompilationResult = Harness.Compiler.compileDeclarationFiles(declContext, links)!; - - return Harness.Compiler.iterateErrorBaseline(tsconfigFiles.concat(declFileCompilationResult.declInputFiles, declFileCompilationResult.declOtherFiles), declFileCompilationResult.declResult.diagnostics, { caseSensitive, currentDirectory }); - }, baselineOpts); - } - }); + } }); + }); +} +export class RWCRunner extends RunnerBase { + public enumerateTestFiles() { + // see also: `enumerateTestFiles` in tests/webTestServer.ts + return IO.getDirectories("internal/cases/rwc/"); } - - export class RWCRunner extends Harness.RunnerBase { - public enumerateTestFiles() { - // see also: `enumerateTestFiles` in tests/webTestServer.ts - return Harness.IO.getDirectories("internal/cases/rwc/"); - } - - public kind(): Harness.TestRunnerKind { - return "rwc"; - } - - /** Setup the runner's tests so that they are ready to be executed by the harness - * The first test should be a describe/it block that sets up the harness's compiler instance appropriately - */ - public initializeTests(): void { - // Read in and evaluate the test list - for (const test of this.tests && this.tests.length ? this.tests : this.getTestFiles()) { - this.runTest(typeof test === "string" ? test : test.file); - } - } - - private runTest(jsonFileName: string) { - runRWCTest(jsonFileName); + public kind(): TestRunnerKind { + return "rwc"; + } + /** Setup the runner's tests so that they are ready to be executed by the harness + * The first test should be a describe/it block that sets up the harness's compiler instance appropriately + */ + public initializeTests(): void { + // Read in and evaluate the test list + for (const test of this.tests && this.tests.length ? this.tests : this.getTestFiles()) { + this.runTest(typeof test === "string" ? test : test.file); } } + private runTest(jsonFileName: string) { + runRWCTest(jsonFileName); + } } diff --git a/src/testRunner/test262Runner.ts b/src/testRunner/test262Runner.ts index 728d3fb322790..f593077a81e0f 100644 --- a/src/testRunner/test262Runner.ts +++ b/src/testRunner/test262Runner.ts @@ -1,110 +1,96 @@ -namespace Harness { - // In harness baselines, null is different than undefined. See `generateActual` in `harness.ts`. - export class Test262BaselineRunner extends RunnerBase { - private static readonly basePath = "internal/cases/test262"; - private static readonly helpersFilePath = "tests/cases/test262-harness/helpers.d.ts"; - private static readonly helperFile: Compiler.TestFile = { - unitName: Test262BaselineRunner.helpersFilePath, - content: IO.readFile(Test262BaselineRunner.helpersFilePath)!, - }; - private static readonly testFileExtensionRegex = /\.js$/; - private static readonly options: ts.CompilerOptions = { - allowNonTsExtensions: true, - target: ts.ScriptTarget.Latest, - module: ts.ModuleKind.CommonJS - }; - private static readonly baselineOptions: Baseline.BaselineOptions = { - Subfolder: "test262", - Baselinefolder: "internal/baselines" - }; - - private static getTestFilePath(filename: string): string { - return Test262BaselineRunner.basePath + "/" + filename; - } - - private runTest(filePath: string) { - describe("test262 test for " + filePath, () => { - // Mocha holds onto the closure environment of the describe callback even after the test is done. - // Everything declared here should be cleared out in the "after" callback. - let testState: { - filename: string; - compilerResult: compiler.CompilationResult; - inputFiles: Compiler.TestFile[]; - }; - - before(() => { - const content = IO.readFile(filePath)!; - const testFilename = ts.removeFileExtension(filePath).replace(/\//g, "_") + ".test"; - const testCaseContent = TestCaseParser.makeUnitsFromTest(content, testFilename); - - const inputFiles: Compiler.TestFile[] = testCaseContent.testUnitData.map(unit => { - const unitName = Test262BaselineRunner.getTestFilePath(unit.name); - return { unitName, content: unit.content }; - }); - - // Emit the results - testState = { - filename: testFilename, - inputFiles, - compilerResult: undefined!, // TODO: GH#18217 - }; - - testState.compilerResult = Compiler.compileFiles( - [Test262BaselineRunner.helperFile].concat(inputFiles), - /*otherFiles*/ [], - /* harnessOptions */ undefined, - Test262BaselineRunner.options, - /* currentDirectory */ undefined); - }); - - after(() => { - testState = undefined!; - }); - - it("has the expected emitted code", () => { - const files = Array.from(testState.compilerResult.js.values()).filter(f => f.file !== Test262BaselineRunner.helpersFilePath); - Baseline.runBaseline(testState.filename + ".output.js", Compiler.collateOutputs(files), Test262BaselineRunner.baselineOptions); - }); - - it("has the expected errors", () => { - const errors = testState.compilerResult.diagnostics; - // eslint-disable-next-line no-null/no-null - const baseline = errors.length === 0 ? null : Compiler.getErrorBaseline(testState.inputFiles, errors); - Baseline.runBaseline(testState.filename + ".errors.txt", baseline, Test262BaselineRunner.baselineOptions); - }); - - it("satisfies invariants", () => { - const sourceFile = testState.compilerResult.program!.getSourceFile(Test262BaselineRunner.getTestFilePath(testState.filename)); - Utils.assertInvariants(sourceFile, /*parent:*/ undefined); - }); - - it("has the expected AST", () => { - const sourceFile = testState.compilerResult.program!.getSourceFile(Test262BaselineRunner.getTestFilePath(testState.filename))!; - Baseline.runBaseline(testState.filename + ".AST.txt", Utils.sourceFileToJSON(sourceFile), Test262BaselineRunner.baselineOptions); +import { RunnerBase, Compiler, IO, Baseline, TestCaseParser, TestRunnerKind } from "./Harness"; +import { CompilerOptions, ScriptTarget, ModuleKind, removeFileExtension, map, normalizePath } from "./ts"; +import { CompilationResult } from "./compiler"; +import { assertInvariants, sourceFileToJSON } from "./Utils"; +// In harness baselines, null is different than undefined. See `generateActual` in `harness.ts`. +export class Test262BaselineRunner extends RunnerBase { + private static readonly basePath = "internal/cases/test262"; + private static readonly helpersFilePath = "tests/cases/test262-harness/helpers.d.ts"; + private static readonly helperFile: Compiler.TestFile = { + unitName: Test262BaselineRunner.helpersFilePath, + content: (IO.readFile(Test262BaselineRunner.helpersFilePath)!), + }; + private static readonly testFileExtensionRegex = /\.js$/; + private static readonly options: CompilerOptions = { + allowNonTsExtensions: true, + target: ScriptTarget.Latest, + module: ModuleKind.CommonJS + }; + private static readonly baselineOptions: Baseline.BaselineOptions = { + Subfolder: "test262", + Baselinefolder: "internal/baselines" + }; + private static getTestFilePath(filename: string): string { + return Test262BaselineRunner.basePath + "/" + filename; + } + private runTest(filePath: string) { + describe("test262 test for " + filePath, () => { + // Mocha holds onto the closure environment of the describe callback even after the test is done. + // Everything declared here should be cleared out in the "after" callback. + let testState: { + filename: string; + compilerResult: CompilationResult; + inputFiles: Compiler.TestFile[]; + }; + before(() => { + const content = (IO.readFile(filePath)!); + const testFilename = removeFileExtension(filePath).replace(/\//g, "_") + ".test"; + const testCaseContent = TestCaseParser.makeUnitsFromTest(content, testFilename); + const inputFiles: Compiler.TestFile[] = testCaseContent.testUnitData.map(unit => { + const unitName = Test262BaselineRunner.getTestFilePath(unit.name); + return { unitName, content: unit.content }; }); + // Emit the results + testState = { + filename: testFilename, + inputFiles, + compilerResult: undefined!, + }; + testState.compilerResult = Compiler.compileFiles([Test262BaselineRunner.helperFile].concat(inputFiles), + /*otherFiles*/ [], + /* harnessOptions */ undefined, Test262BaselineRunner.options, + /* currentDirectory */ undefined); + }); + after(() => { + testState = undefined!; + }); + it("has the expected emitted code", () => { + const files = Array.from(testState.compilerResult.js.values()).filter(f => f.file !== Test262BaselineRunner.helpersFilePath); + Baseline.runBaseline(testState.filename + ".output.js", Compiler.collateOutputs(files), Test262BaselineRunner.baselineOptions); + }); + it("has the expected errors", () => { + const errors = testState.compilerResult.diagnostics; + // eslint-disable-next-line no-null/no-null + const baseline = errors.length === 0 ? null : Compiler.getErrorBaseline(testState.inputFiles, errors); + Baseline.runBaseline(testState.filename + ".errors.txt", baseline, Test262BaselineRunner.baselineOptions); + }); + it("satisfies invariants", () => { + const sourceFile = testState.compilerResult.program!.getSourceFile(Test262BaselineRunner.getTestFilePath(testState.filename)); + assertInvariants(sourceFile, /*parent:*/ undefined); + }); + it("has the expected AST", () => { + const sourceFile = testState.compilerResult.program!.getSourceFile(Test262BaselineRunner.getTestFilePath(testState.filename))!; + Baseline.runBaseline(testState.filename + ".AST.txt", sourceFileToJSON(sourceFile), Test262BaselineRunner.baselineOptions); + }); + }); + } + public kind(): TestRunnerKind { + return "test262"; + } + public enumerateTestFiles() { + // see also: `enumerateTestFiles` in tests/webTestServer.ts + return map(this.enumerateFiles(Test262BaselineRunner.basePath, Test262BaselineRunner.testFileExtensionRegex, { recursive: true }), normalizePath); + } + public initializeTests() { + // this will set up a series of describe/it blocks to run between the setup and cleanup phases + if (this.tests.length === 0) { + const testFiles = this.getTestFiles(); + testFiles.forEach(fn => { + this.runTest(fn); }); } - - public kind(): TestRunnerKind { - return "test262"; - } - - public enumerateTestFiles() { - // see also: `enumerateTestFiles` in tests/webTestServer.ts - return ts.map(this.enumerateFiles(Test262BaselineRunner.basePath, Test262BaselineRunner.testFileExtensionRegex, { recursive: true }), ts.normalizePath); - } - - public initializeTests() { - // this will set up a series of describe/it blocks to run between the setup and cleanup phases - if (this.tests.length === 0) { - const testFiles = this.getTestFiles(); - testFiles.forEach(fn => { - this.runTest(fn); - }); - } - else { - this.tests.forEach(test => this.runTest(typeof test === "string" ? test : test.file)); - } + else { + this.tests.forEach(test => this.runTest(typeof test === "string" ? test : test.file)); } } -} \ No newline at end of file +} diff --git a/src/testRunner/ts.projectSystem.ts b/src/testRunner/ts.projectSystem.ts new file mode 100644 index 0000000000000..0c78e1b603b30 --- /dev/null +++ b/src/testRunner/ts.projectSystem.ts @@ -0,0 +1,51 @@ +export * from "./unittests/tsserver/helpers"; +export * from "./unittests/tsserver/applyChangesToOpenFiles"; +export * from "./unittests/tsserver/cachingFileSystemInformation"; +export * from "./unittests/tsserver/cancellationToken"; +export * from "./unittests/tsserver/compileOnSave"; +export * from "./unittests/tsserver/completions"; +export * from "./unittests/tsserver/configFileSearch"; +export * from "./unittests/tsserver/configuredProjects"; +export * from "./unittests/tsserver/declarationFileMaps"; +export * from "./unittests/tsserver/documentRegistry"; +export * from "./unittests/tsserver/duplicatePackages"; +export * from "./unittests/tsserver/dynamicFiles"; +export * from "./unittests/tsserver/events/largeFileReferenced"; +export * from "./unittests/tsserver/events/projectLanguageServiceState"; +export * from "./unittests/tsserver/events/projectLoading"; +export * from "./unittests/tsserver/events/projectUpdatedInBackground"; +export * from "./unittests/tsserver/externalProjects"; +export * from "./unittests/tsserver/forceConsistentCasingInFileNames"; +export * from "./unittests/tsserver/formatSettings"; +export * from "./unittests/tsserver/getApplicableRefactors"; +export * from "./unittests/tsserver/getEditsForFileRename"; +export * from "./unittests/tsserver/getExportReferences"; +export * from "./unittests/tsserver/importHelpers"; +export * from "./unittests/tsserver/importSuggestionsCache"; +export * from "./unittests/tsserver/inferredProjects"; +export * from "./unittests/tsserver/languageService"; +export * from "./unittests/tsserver/maxNodeModuleJsDepth"; +export * from "./unittests/tsserver/metadataInResponse"; +export * from "./unittests/tsserver/navTo"; +export * from "./unittests/tsserver/occurences"; +export * from "./unittests/tsserver/openFile"; +export * from "./unittests/tsserver/packageJsonInfo"; +export * from "./unittests/tsserver/projectErrors"; +export * from "./unittests/tsserver/projectReferenceCompileOnSave"; +export * from "./unittests/tsserver/projectReferenceErrors"; +export * from "./unittests/tsserver/projectReferences"; +export * from "./unittests/tsserver/projects"; +export * from "./unittests/tsserver/refactors"; +export * from "./unittests/tsserver/reload"; +export * from "./unittests/tsserver/rename"; +export * from "./unittests/tsserver/resolutionCache"; +export * from "./unittests/tsserver/smartSelection"; +export * from "./unittests/tsserver/skipLibCheck"; +export * from "./unittests/tsserver/symLinks"; +export * from "./unittests/tsserver/syntaxOperations"; +export * from "./unittests/tsserver/telemetry"; +export * from "./unittests/tsserver/typeAquisition"; +export * from "./unittests/tsserver/typeOnlyImportChains"; +export * from "./unittests/tsserver/typeReferenceDirectives"; +export * from "./unittests/tsserver/typingsInstaller"; +export * from "./unittests/tsserver/watchEnvironment"; diff --git a/src/testRunner/ts.server.ts b/src/testRunner/ts.server.ts new file mode 100644 index 0000000000000..a8b685b65a8cf --- /dev/null +++ b/src/testRunner/ts.server.ts @@ -0,0 +1,5 @@ +export * from "../jsTyping/ts.server"; +export * from "../server/ts.server"; +export * from "../typingsInstallerCore/ts.server"; +export * from "../harness/ts.server"; +export * from "./unittests/tsserver/session"; diff --git a/src/testRunner/ts.textStorage.ts b/src/testRunner/ts.textStorage.ts new file mode 100644 index 0000000000000..21bcc33fdc922 --- /dev/null +++ b/src/testRunner/ts.textStorage.ts @@ -0,0 +1 @@ +export * from "./unittests/tsserver/textStorage"; diff --git a/src/testRunner/ts.ts b/src/testRunner/ts.ts new file mode 100644 index 0000000000000..b8a6ff45cc84b --- /dev/null +++ b/src/testRunner/ts.ts @@ -0,0 +1,82 @@ +export * from "../shims/ts"; +export * from "../compiler/ts"; +export * from "../executeCommandLine/ts"; +export * from "../services/ts"; +export * from "../jsTyping/ts"; +export * from "../server/ts"; +export * from "../typingsInstallerCore/ts"; +export * from "../harness/ts"; +export * from "./unittests/services/extract/helpers"; +export * from "./unittests/tsbuild/helpers"; +export * from "./unittests/tsc/helpers"; +export * from "./unittests/asserts"; +export * from "./unittests/base64"; +export * from "./unittests/builder"; +export * from "./unittests/comments"; +export * from "./unittests/compilerCore"; +export * from "./unittests/convertToBase64"; +export * from "./unittests/customTransforms"; +export * from "./unittests/factory"; +export * from "./unittests/incrementalParser"; +export * from "./unittests/jsDocParsing"; +export * from "./unittests/moduleResolution"; +export * from "./unittests/parsePseudoBigInt"; +export * from "./unittests/printer"; +export * from "./unittests/programApi"; +export * from "./unittests/reuseProgramStructure"; +export * from "./unittests/semver"; +export * from "./unittests/createMapShim"; +export * from "./unittests/transform"; +export * from "./unittests/config/commandLineParsing"; +export * from "./unittests/config/configurationExtension"; +export * from "./unittests/config/convertCompilerOptionsFromJson"; +export * from "./unittests/config/convertTypeAcquisitionFromJson"; +export * from "./unittests/config/initializeTSConfig"; +export * from "./unittests/config/matchFiles"; +export * from "./unittests/config/projectReferences"; +export * from "./unittests/config/showConfig"; +export * from "./unittests/config/tsconfigParsing"; +export * from "./unittests/config/tsconfigParsingWatchOptions"; +export * from "./unittests/services/cancellableLanguageServiceOperations"; +export * from "./unittests/services/convertToAsyncFunction"; +export * from "./unittests/services/extract/constants"; +export * from "./unittests/services/extract/functions"; +export * from "./unittests/services/extract/symbolWalker"; +export * from "./unittests/services/extract/ranges"; +export * from "./unittests/services/hostNewLineSupport"; +export * from "./unittests/services/languageService"; +export * from "./unittests/services/organizeImports"; +export * from "./unittests/services/textChanges"; +export * from "./unittests/services/transpile"; +export * from "./unittests/tsbuild/amdModulesWithOut"; +export * from "./unittests/tsbuild/configFileErrors"; +export * from "./unittests/tsbuild/containerOnlyReferenced"; +export * from "./unittests/tsbuild/demo"; +export * from "./unittests/tsbuild/emitDeclarationOnly"; +export * from "./unittests/tsbuild/emptyFiles"; +export * from "./unittests/tsbuild/exitCodeOnBogusFile"; +export * from "./unittests/tsbuild/graphOrdering"; +export * from "./unittests/tsbuild/inferredTypeFromTransitiveModule"; +export * from "./unittests/tsbuild/javascriptProjectEmit"; +export * from "./unittests/tsbuild/lateBoundSymbol"; +export * from "./unittests/tsbuild/moduleSpecifiers"; +export * from "./unittests/tsbuild/noEmitOnError"; +export * from "./unittests/tsbuild/outFile"; +export * from "./unittests/tsbuild/referencesWithRootDirInParent"; +export * from "./unittests/tsbuild/resolveJsonModule"; +export * from "./unittests/tsbuild/sample"; +export * from "./unittests/tsbuild/transitiveReferences"; +export * from "./unittests/tsc/composite"; +export * from "./unittests/tsc/declarationEmit"; +export * from "./unittests/tsc/incremental"; +export * from "./unittests/tsc/listFilesOnly"; +export * from "./unittests/tsc/runWithoutArgs"; +export * from "./unittests/tsserver/versionCache"; +import * as tscWatch from "./ts.tscWatch"; +export { tscWatch }; +import * as projectSystem from "./ts.projectSystem"; +export { projectSystem }; +import * as server from "./ts.server"; +export { server }; +import * as textStorage from "./ts.textStorage"; +export { textStorage }; diff --git a/src/testRunner/ts.tscWatch.ts b/src/testRunner/ts.tscWatch.ts new file mode 100644 index 0000000000000..ff90f8b8ec2a0 --- /dev/null +++ b/src/testRunner/ts.tscWatch.ts @@ -0,0 +1,12 @@ +export * from "./unittests/tscWatch/helpers"; +export * from "./unittests/tsbuild/watchEnvironment"; +export * from "./unittests/tsbuild/watchMode"; +export * from "./unittests/tscWatch/consoleClearing"; +export * from "./unittests/tscWatch/emit"; +export * from "./unittests/tscWatch/emitAndErrorUpdates"; +export * from "./unittests/tscWatch/forceConsistentCasingInFileNames"; +export * from "./unittests/tscWatch/incremental"; +export * from "./unittests/tscWatch/programUpdates"; +export * from "./unittests/tscWatch/resolutionCache"; +export * from "./unittests/tscWatch/watchEnvironment"; +export * from "./unittests/tscWatch/watchApi"; diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index 5fdc05ae1ce03..c7ad79149540f 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../tsconfig-noncomposite-base", "compilerOptions": { - "outFile": "../../built/local/run.js", + "outDir": "../../built/local", "composite": false, "declaration": false, "declarationMap": false, @@ -14,16 +14,15 @@ ] }, "references": [ - { "path": "../shims", "prepend": true }, - { "path": "../compiler", "prepend": true }, - { "path": "../executeCommandLine", "prepend": true }, - { "path": "../services", "prepend": true }, - { "path": "../jsTyping", "prepend": true }, - { "path": "../server", "prepend": true }, - { "path": "../typingsInstallerCore", "prepend": true }, - { "path": "../harness", "prepend": true } + { "path": "../shims" }, + { "path": "../compiler" }, + { "path": "../executeCommandLine" }, + { "path": "../services" }, + { "path": "../jsTyping" }, + { "path": "../server" }, + { "path": "../typingsInstallerCore" }, + { "path": "../harness" } ], - "files": [ "compilerRef.ts", "evaluatorRef.ts", @@ -34,26 +33,21 @@ "playbackRef.ts", "utilsRef.ts", "documentsRef.ts", - "fourslashRunner.ts", "compilerRunner.ts", "projectsRunner.ts", "rwcRunner.ts", "externalCompileRunner.ts", "test262Runner.ts", - "parallel/host.ts", "parallel/worker.ts", "parallel/shared.ts", - "runner.ts", - "unittests/services/extract/helpers.ts", "unittests/tsbuild/helpers.ts", "unittests/tsc/helpers.ts", "unittests/tscWatch/helpers.ts", "unittests/tsserver/helpers.ts", - "unittests/asserts.ts", "unittests/base64.ts", "unittests/builder.ts", @@ -192,6 +186,26 @@ "unittests/tsserver/typeReferenceDirectives.ts", "unittests/tsserver/typingsInstaller.ts", "unittests/tsserver/versionCache.ts", - "unittests/tsserver/watchEnvironment.ts" + "unittests/tsserver/watchEnvironment.ts", + "./compiler.ts", + "./evaluator.ts", + "./fakes.ts", + "./vpath.ts", + "./vfs.ts", + "./FourSlash.ts", + "./Playback.ts", + "./Utils.ts", + "./documents.ts", + "./Harness.ts", + "./project.ts", + "./RWC.ts", + "./Harness.Parallel.Host.ts", + "./Harness.Parallel.ts", + "./Harness.Parallel.Worker.ts", + "./ts.ts", + "./ts.tscWatch.ts", + "./ts.projectSystem.ts", + "./ts.server.ts", + "./ts.textStorage.ts" ] } diff --git a/src/testRunner/unittests/asserts.ts b/src/testRunner/unittests/asserts.ts index 116a74cf17911..b476b2d2e6fa0 100644 --- a/src/testRunner/unittests/asserts.ts +++ b/src/testRunner/unittests/asserts.ts @@ -1,12 +1,11 @@ -namespace ts { - describe("unittests:: assert", () => { - it("deepEqual", () => { - assert.throws(() => assert.deepEqual(createNodeArray([createIdentifier("A")]), createNodeArray([createIdentifier("B")]))); - assert.throws(() => assert.deepEqual(createNodeArray([], /*hasTrailingComma*/ true), createNodeArray([], /*hasTrailingComma*/ false))); - assert.deepEqual(createNodeArray([createIdentifier("A")], /*hasTrailingComma*/ true), createNodeArray([createIdentifier("A")], /*hasTrailingComma*/ true)); - }); - it("assertNever on string has correct error", () => { - assert.throws(() => Debug.assertNever("hi" as never), "Debug Failure. Illegal value: \"hi\""); - }); +import { createNodeArray, createIdentifier, Debug } from "../ts"; +describe("unittests:: assert", () => { + it("deepEqual", () => { + assert.throws(() => assert.deepEqual(createNodeArray([createIdentifier("A")]), createNodeArray([createIdentifier("B")]))); + assert.throws(() => assert.deepEqual(createNodeArray([], /*hasTrailingComma*/ true), createNodeArray([], /*hasTrailingComma*/ false))); + assert.deepEqual(createNodeArray([createIdentifier("A")], /*hasTrailingComma*/ true), createNodeArray([createIdentifier("A")], /*hasTrailingComma*/ true)); }); -} + it("assertNever on string has correct error", () => { + assert.throws(() => Debug.assertNever(("hi" as never)), "Debug Failure. Illegal value: \"hi\""); + }); +}); diff --git a/src/testRunner/unittests/base64.ts b/src/testRunner/unittests/base64.ts index 1dc51a8fbf5ae..10a2ddb5597bb 100644 --- a/src/testRunner/unittests/base64.ts +++ b/src/testRunner/unittests/base64.ts @@ -1,22 +1,21 @@ -namespace ts { - describe("unittests:: base64", () => { - describe("base64decode", () => { - it("can decode input strings correctly without needing a host implementation", () => { - const tests = [ - // "a", - // "this is a test", - // " !\"#$ %&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", - "日本語", - "🐱", - "\x00\x01", - "\t\n\r\\\"\'\u0062", - "====", - "", - ]; - for (const test of tests) { - assert.equal(base64decode({}, convertToBase64(test)), test); - } - }); +import { base64decode, convertToBase64 } from "../ts"; +describe("unittests:: base64", () => { + describe("base64decode", () => { + it("can decode input strings correctly without needing a host implementation", () => { + const tests = [ + // "a", + // "this is a test", + // " !\"#$ %&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", + "日本語", + "🐱", + "\x00\x01", + "\t\n\r\\\"\'\u0062", + "====", + "", + ]; + for (const test of tests) { + assert.equal(base64decode({}, convertToBase64(test)), test); + } }); }); -} +}); diff --git a/src/testRunner/unittests/builder.ts b/src/testRunner/unittests/builder.ts index bf0415f13598c..fdaece38b483c 100644 --- a/src/testRunner/unittests/builder.ts +++ b/src/testRunner/unittests/builder.ts @@ -1,130 +1,112 @@ -namespace ts { - describe("unittests:: builder", () => { - it("emits dependent files", () => { - const files: NamedSourceText[] = [ - { name: "/a.ts", text: SourceText.New("", 'import { b } from "./b";', "") }, - { name: "/b.ts", text: SourceText.New("", ' import { c } from "./c";', "export const b = c;") }, - { name: "/c.ts", text: SourceText.New("", "", "export const c = 0;") }, - ]; - - let program = newProgram(files, ["/a.ts"], {}); - const assertChanges = makeAssertChanges(() => program); - - assertChanges(["/c.js", "/b.js", "/a.js"]); - - program = updateProgramFile(program, "/a.ts", "//comment"); - assertChanges(["/a.js"]); - - program = updateProgramFile(program, "/b.ts", "export const b = c + 1;"); - assertChanges(["/b.js", "/a.js"]); - - program = updateProgramFile(program, "/c.ts", "export const c = 1;"); - assertChanges(["/c.js", "/b.js"]); - }); - - it("if emitting all files, emits the changed file first", () => { - const files: NamedSourceText[] = [ - { name: "/a.ts", text: SourceText.New("", "", "namespace A { export const x = 0; }") }, - { name: "/b.ts", text: SourceText.New("", "", "namespace B { export const x = 0; }") }, - ]; - - let program = newProgram(files, ["/a.ts", "/b.ts"], {}); - const assertChanges = makeAssertChanges(() => program); - - assertChanges(["/a.js", "/b.js"]); - - program = updateProgramFile(program, "/a.ts", "namespace A { export const x = 1; }"); - assertChanges(["/a.js", "/b.js"]); - - program = updateProgramFile(program, "/b.ts", "namespace B { export const x = 1; }"); - assertChanges(["/b.js", "/a.js"]); - }); - - it("keeps the file in affected files if cancellation token throws during the operation", () => { - const files: NamedSourceText[] = [ - { name: "/a.ts", text: SourceText.New("", 'import { b } from "./b";', "") }, - { name: "/b.ts", text: SourceText.New("", ' import { c } from "./c";', "export const b = c;") }, - { name: "/c.ts", text: SourceText.New("", "", "export const c = 0;") }, - { name: "/d.ts", text: SourceText.New("", "", "export const dd = 0;") }, - { name: "/e.ts", text: SourceText.New("", "", "export const ee = 0;") }, - ]; - - let program = newProgram(files, ["/d.ts", "/e.ts", "/a.ts"], {}); - const assertChanges = makeAssertChangesWithCancellationToken(() => program); - // No cancellation - assertChanges(["/d.js", "/e.js", "/c.js", "/b.js", "/a.js"]); - - // cancel when emitting a.ts - program = updateProgramFile(program, "/a.ts", "export function foo() { }"); - assertChanges(["/a.js"], 0); - // Change d.ts and verify previously pending a.ts is emitted as well - program = updateProgramFile(program, "/d.ts", "export function bar() { }"); - assertChanges(["/a.js", "/d.js"]); - - // Cancel when emitting b.js - program = updateProgramFile(program, "/b.ts", "export class b { foo() { c + 1; } }"); - program = updateProgramFile(program, "/d.ts", "export function bar2() { }"); - assertChanges(["/d.js", "/b.js", "/a.js"], 1); - // Change e.ts and verify previously b.js as well as a.js get emitted again since previous change was consumed completely but not d.ts - program = updateProgramFile(program, "/e.ts", "export function bar3() { }"); - assertChanges(["/b.js", "/a.js", "/e.js"]); - }); +import { NamedSourceText, SourceText, newProgram, Program, BuilderProgramHost, returnTrue, EmitAndSemanticDiagnosticsBuilderProgram, createEmitAndSemanticDiagnosticsBuilderProgram, CancellationToken, OperationCanceledException, ProgramWithSourceTexts, updateProgram, updateProgramText } from "../ts"; +describe("unittests:: builder", () => { + it("emits dependent files", () => { + const files: NamedSourceText[] = [ + { name: "/a.ts", text: SourceText.New("", 'import { b } from "./b";', "") }, + { name: "/b.ts", text: SourceText.New("", ' import { c } from "./c";', "export const b = c;") }, + { name: "/c.ts", text: SourceText.New("", "", "export const c = 0;") }, + ]; + let program = newProgram(files, ["/a.ts"], {}); + const assertChanges = makeAssertChanges(() => program); + assertChanges(["/c.js", "/b.js", "/a.js"]); + program = updateProgramFile(program, "/a.ts", "//comment"); + assertChanges(["/a.js"]); + program = updateProgramFile(program, "/b.ts", "export const b = c + 1;"); + assertChanges(["/b.js", "/a.js"]); + program = updateProgramFile(program, "/c.ts", "export const c = 1;"); + assertChanges(["/c.js", "/b.js"]); }); - - function makeAssertChanges(getProgram: () => Program): (fileNames: readonly string[]) => void { - const host: BuilderProgramHost = { useCaseSensitiveFileNames: returnTrue }; - let builderProgram: EmitAndSemanticDiagnosticsBuilderProgram | undefined; - return fileNames => { - const program = getProgram(); - builderProgram = createEmitAndSemanticDiagnosticsBuilderProgram(program, host, builderProgram); - const outputFileNames: string[] = []; - // eslint-disable-next-line no-empty - while (builderProgram.emitNextAffectedFile(fileName => outputFileNames.push(fileName))) { + it("if emitting all files, emits the changed file first", () => { + const files: NamedSourceText[] = [ + { name: "/a.ts", text: SourceText.New("", "", "namespace A { export const x = 0; }") }, + { name: "/b.ts", text: SourceText.New("", "", "namespace B { export const x = 0; }") }, + ]; + let program = newProgram(files, ["/a.ts", "/b.ts"], {}); + const assertChanges = makeAssertChanges(() => program); + assertChanges(["/a.js", "/b.js"]); + program = updateProgramFile(program, "/a.ts", "namespace A { export const x = 1; }"); + assertChanges(["/a.js", "/b.js"]); + program = updateProgramFile(program, "/b.ts", "namespace B { export const x = 1; }"); + assertChanges(["/b.js", "/a.js"]); + }); + it("keeps the file in affected files if cancellation token throws during the operation", () => { + const files: NamedSourceText[] = [ + { name: "/a.ts", text: SourceText.New("", 'import { b } from "./b";', "") }, + { name: "/b.ts", text: SourceText.New("", ' import { c } from "./c";', "export const b = c;") }, + { name: "/c.ts", text: SourceText.New("", "", "export const c = 0;") }, + { name: "/d.ts", text: SourceText.New("", "", "export const dd = 0;") }, + { name: "/e.ts", text: SourceText.New("", "", "export const ee = 0;") }, + ]; + let program = newProgram(files, ["/d.ts", "/e.ts", "/a.ts"], {}); + const assertChanges = makeAssertChangesWithCancellationToken(() => program); + // No cancellation + assertChanges(["/d.js", "/e.js", "/c.js", "/b.js", "/a.js"]); + // cancel when emitting a.ts + program = updateProgramFile(program, "/a.ts", "export function foo() { }"); + assertChanges(["/a.js"], 0); + // Change d.ts and verify previously pending a.ts is emitted as well + program = updateProgramFile(program, "/d.ts", "export function bar() { }"); + assertChanges(["/a.js", "/d.js"]); + // Cancel when emitting b.js + program = updateProgramFile(program, "/b.ts", "export class b { foo() { c + 1; } }"); + program = updateProgramFile(program, "/d.ts", "export function bar2() { }"); + assertChanges(["/d.js", "/b.js", "/a.js"], 1); + // Change e.ts and verify previously b.js as well as a.js get emitted again since previous change was consumed completely but not d.ts + program = updateProgramFile(program, "/e.ts", "export function bar3() { }"); + assertChanges(["/b.js", "/a.js", "/e.js"]); + }); +}); +function makeAssertChanges(getProgram: () => Program): (fileNames: readonly string[]) => void { + const host: BuilderProgramHost = { useCaseSensitiveFileNames: returnTrue }; + let builderProgram: EmitAndSemanticDiagnosticsBuilderProgram | undefined; + return fileNames => { + const program = getProgram(); + builderProgram = createEmitAndSemanticDiagnosticsBuilderProgram(program, host, builderProgram); + const outputFileNames: string[] = []; + // eslint-disable-next-line no-empty + while (builderProgram.emitNextAffectedFile(fileName => outputFileNames.push(fileName))) { + } + assert.deepEqual(outputFileNames, fileNames); + }; +} +function makeAssertChangesWithCancellationToken(getProgram: () => Program): (fileNames: readonly string[], cancelAfterEmitLength?: number) => void { + const host: BuilderProgramHost = { useCaseSensitiveFileNames: returnTrue }; + let builderProgram: EmitAndSemanticDiagnosticsBuilderProgram | undefined; + let cancel = false; + const cancellationToken: CancellationToken = { + isCancellationRequested: () => cancel, + throwIfCancellationRequested: () => { + if (cancel) { + throw new OperationCanceledException(); } - assert.deepEqual(outputFileNames, fileNames); - }; - } - - function makeAssertChangesWithCancellationToken(getProgram: () => Program): (fileNames: readonly string[], cancelAfterEmitLength?: number) => void { - const host: BuilderProgramHost = { useCaseSensitiveFileNames: returnTrue }; - let builderProgram: EmitAndSemanticDiagnosticsBuilderProgram | undefined; - let cancel = false; - const cancellationToken: CancellationToken = { - isCancellationRequested: () => cancel, - throwIfCancellationRequested: () => { - if (cancel) { - throw new OperationCanceledException(); + }, + }; + return (fileNames, cancelAfterEmitLength?: number) => { + cancel = false; + let operationWasCancelled = false; + const program = getProgram(); + builderProgram = createEmitAndSemanticDiagnosticsBuilderProgram(program, host, builderProgram); + const outputFileNames: string[] = []; + try { + do { + assert.isFalse(cancel); + if (outputFileNames.length === cancelAfterEmitLength) { + cancel = true; } - }, - }; - return (fileNames, cancelAfterEmitLength?: number) => { - cancel = false; - let operationWasCancelled = false; - const program = getProgram(); - builderProgram = createEmitAndSemanticDiagnosticsBuilderProgram(program, host, builderProgram); - const outputFileNames: string[] = []; - try { - do { - assert.isFalse(cancel); - if (outputFileNames.length === cancelAfterEmitLength) { - cancel = true; - } - } while (builderProgram.emitNextAffectedFile(fileName => outputFileNames.push(fileName), cancellationToken)); - } - catch (e) { - assert.isFalse(operationWasCancelled); - assert(e instanceof OperationCanceledException, e.toString()); - operationWasCancelled = true; - } - assert.equal(cancel, operationWasCancelled); - assert.equal(operationWasCancelled, fileNames.length > cancelAfterEmitLength!); - assert.deepEqual(outputFileNames, fileNames.slice(0, cancelAfterEmitLength)); - }; - } - - function updateProgramFile(program: ProgramWithSourceTexts, fileName: string, fileContent: string): ProgramWithSourceTexts { - return updateProgram(program, program.getRootFileNames(), program.getCompilerOptions(), files => { - updateProgramText(files, fileName, fileContent); - }); - } + } while (builderProgram.emitNextAffectedFile(fileName => outputFileNames.push(fileName), cancellationToken)); + } + catch (e) { + assert.isFalse(operationWasCancelled); + assert(e instanceof OperationCanceledException, e.toString()); + operationWasCancelled = true; + } + assert.equal(cancel, operationWasCancelled); + assert.equal(operationWasCancelled, fileNames.length > cancelAfterEmitLength!); + assert.deepEqual(outputFileNames, fileNames.slice(0, cancelAfterEmitLength)); + }; +} +function updateProgramFile(program: ProgramWithSourceTexts, fileName: string, fileContent: string): ProgramWithSourceTexts { + return updateProgram(program, program.getRootFileNames(), program.getCompilerOptions(), files => { + updateProgramText(files, fileName, fileContent); + }); } diff --git a/src/testRunner/unittests/comments.ts b/src/testRunner/unittests/comments.ts index 59f3020c112bc..f471e442273c7 100644 --- a/src/testRunner/unittests/comments.ts +++ b/src/testRunner/unittests/comments.ts @@ -1,32 +1,29 @@ -namespace ts { - describe("comment parsing", () => { - const withShebang = `#! node +import { getLeadingCommentRanges, SyntaxKind } from "../ts"; +describe("comment parsing", () => { + const withShebang = `#! node /** comment */ // another one ;`; - const noShebang = `/** comment */ + const noShebang = `/** comment */ // another one ;`; - const withTrailing = `;/* comment */ + const withTrailing = `;/* comment */ // another one `; - it("skips shebang", () => { - const result = getLeadingCommentRanges(withShebang, 0); - assert.isDefined(result); - assert.strictEqual(result!.length, 2); - }); - - it("treats all comments at start of file as leading comments", () => { - const result = getLeadingCommentRanges(noShebang, 0); - assert.isDefined(result); - assert.strictEqual(result!.length, 2); - }); - - it("returns leading comments if position is not 0", () => { - const result = getLeadingCommentRanges(withTrailing, 1); - assert.isDefined(result); - assert.strictEqual(result!.length, 1); - assert.strictEqual(result![0].kind, SyntaxKind.SingleLineCommentTrivia); - }); + it("skips shebang", () => { + const result = getLeadingCommentRanges(withShebang, 0); + assert.isDefined(result); + assert.strictEqual(result!.length, 2); }); -} + it("treats all comments at start of file as leading comments", () => { + const result = getLeadingCommentRanges(noShebang, 0); + assert.isDefined(result); + assert.strictEqual(result!.length, 2); + }); + it("returns leading comments if position is not 0", () => { + const result = getLeadingCommentRanges(withTrailing, 1); + assert.isDefined(result); + assert.strictEqual(result!.length, 1); + assert.strictEqual(result![0].kind, SyntaxKind.SingleLineCommentTrivia); + }); +}); diff --git a/src/testRunner/unittests/compilerCore.ts b/src/testRunner/unittests/compilerCore.ts index 4c3918c3149f3..27294151592c6 100644 --- a/src/testRunner/unittests/compilerCore.ts +++ b/src/testRunner/unittests/compilerCore.ts @@ -1,33 +1,32 @@ -namespace ts { - describe("unittests:: compilerCore", () => { - describe("equalOwnProperties", () => { - it("correctly equates objects", () => { - assert.isTrue(equalOwnProperties({}, {})); - assert.isTrue(equalOwnProperties({ a: 1 }, { a: 1 })); - assert.isTrue(equalOwnProperties({ a: 1, b: 2 }, { b: 2, a: 1 })); - }); - it("correctly identifies unmatched objects", () => { - assert.isFalse(equalOwnProperties({}, { a: 1 }), "missing left property"); - assert.isFalse(equalOwnProperties({ a: 1 }, {}), "missing right property"); - assert.isFalse(equalOwnProperties({ a: 1 }, { a: 2 }), "differing property"); - }); - it("correctly identifies undefined vs hasOwnProperty", () => { - assert.isFalse(equalOwnProperties({}, { a: undefined }), "missing left property"); - assert.isFalse(equalOwnProperties({ a: undefined }, {}), "missing right property"); - }); - it("truthiness", () => { - const trythyTest = (l: any, r: any) => !!l === !!r; - assert.isFalse(equalOwnProperties({}, { a: 1 }, trythyTest), "missing left truthy property"); - assert.isFalse(equalOwnProperties({}, { a: 0 }, trythyTest), "missing left falsey property"); - assert.isFalse(equalOwnProperties({ a: 1 }, {}, trythyTest), "missing right truthy property"); - assert.isFalse(equalOwnProperties({ a: 0 }, {}, trythyTest), "missing right falsey property"); - assert.isTrue(equalOwnProperties({ a: 1 }, { a: "foo" }, trythyTest), "valid equality"); - }); - it("all equal", () => { - assert.isFalse(equalOwnProperties({}, { a: 1 }, () => true), "missing left property"); - assert.isFalse(equalOwnProperties({ a: 1 }, {}, () => true), "missing right property"); - assert.isTrue(equalOwnProperties({ a: 1 }, { a: 2 }, () => true), "valid equality"); - }); +import { equalOwnProperties } from "../ts"; +describe("unittests:: compilerCore", () => { + describe("equalOwnProperties", () => { + it("correctly equates objects", () => { + assert.isTrue(equalOwnProperties({}, {})); + assert.isTrue(equalOwnProperties({ a: 1 }, { a: 1 })); + assert.isTrue(equalOwnProperties({ a: 1, b: 2 }, { b: 2, a: 1 })); + }); + it("correctly identifies unmatched objects", () => { + assert.isFalse(equalOwnProperties({}, { a: 1 }), "missing left property"); + assert.isFalse(equalOwnProperties({ a: 1 }, {}), "missing right property"); + assert.isFalse(equalOwnProperties({ a: 1 }, { a: 2 }), "differing property"); + }); + it("correctly identifies undefined vs hasOwnProperty", () => { + assert.isFalse(equalOwnProperties({}, { a: undefined }), "missing left property"); + assert.isFalse(equalOwnProperties({ a: undefined }, {}), "missing right property"); + }); + it("truthiness", () => { + const trythyTest = (l: any, r: any) => !!l === !!r; + assert.isFalse(equalOwnProperties({}, { a: 1 }, trythyTest), "missing left truthy property"); + assert.isFalse(equalOwnProperties({}, { a: 0 }, trythyTest), "missing left falsey property"); + assert.isFalse(equalOwnProperties({ a: 1 }, {}, trythyTest), "missing right truthy property"); + assert.isFalse(equalOwnProperties({ a: 0 }, {}, trythyTest), "missing right falsey property"); + assert.isTrue(equalOwnProperties({ a: 1 }, { a: "foo" }, trythyTest), "valid equality"); + }); + it("all equal", () => { + assert.isFalse(equalOwnProperties({}, { a: 1 }, () => true), "missing left property"); + assert.isFalse(equalOwnProperties({ a: 1 }, {}, () => true), "missing right property"); + assert.isTrue(equalOwnProperties({ a: 1 }, { a: 2 }, () => true), "valid equality"); }); }); -} +}); diff --git a/src/testRunner/unittests/config/commandLineParsing.ts b/src/testRunner/unittests/config/commandLineParsing.ts index 8dbbffaf20d84..86a1bc1ab8cba 100644 --- a/src/testRunner/unittests/config/commandLineParsing.ts +++ b/src/testRunner/unittests/config/commandLineParsing.ts @@ -1,482 +1,378 @@ -namespace ts { - describe("unittests:: config:: commandLineParsing:: parseCommandLine", () => { - - function assertParseResult(commandLine: string[], expectedParsedCommandLine: ParsedCommandLine, workerDiagnostic?: () => ParseCommandLineWorkerDiagnostics) { - const parsed = parseCommandLineWorker(workerDiagnostic?.() || compilerOptionsDidYouMeanDiagnostics, commandLine); - assert.deepEqual(parsed.options, expectedParsedCommandLine.options); - assert.deepEqual(parsed.watchOptions, expectedParsedCommandLine.watchOptions); - - const parsedErrors = parsed.errors; - const expectedErrors = expectedParsedCommandLine.errors; - assert.isTrue(parsedErrors.length === expectedErrors.length, `Expected error: ${JSON.stringify(expectedErrors)}. Actual error: ${JSON.stringify(parsedErrors)}.`); - for (let i = 0; i < parsedErrors.length; i++) { - const parsedError = parsedErrors[i]; - const expectedError = expectedErrors[i]; - assert.equal(parsedError.code, expectedError.code); - assert.equal(parsedError.category, expectedError.category); - assert.equal(parsedError.messageText, expectedError.messageText); - } - - const parsedFileNames = parsed.fileNames; - const expectedFileNames = expectedParsedCommandLine.fileNames; - assert.isTrue(parsedFileNames.length === expectedFileNames.length, `Expected fileNames: [${JSON.stringify(expectedFileNames)}]. Actual fileNames: [${JSON.stringify(parsedFileNames)}].`); - for (let i = 0; i < parsedFileNames.length; i++) { - const parsedFileName = parsedFileNames[i]; - const expectedFileName = expectedFileNames[i]; - assert.equal(parsedFileName, expectedFileName); - } +import { ParsedCommandLine, ParseCommandLineWorkerDiagnostics, parseCommandLineWorker, compilerOptionsDidYouMeanDiagnostics, Diagnostics, ScriptTarget, ModuleKind, DiagnosticMessage, formatStringFromArgs, createOptionNameMap, createMapFromTemplate, ModuleResolutionKind, WatchFileKind, WatchDirectoryKind, PollingWatchKind, ParsedBuildCommand, parseBuildCommand, BuildOptions } from "../../ts"; +import * as ts from "../../ts"; +describe("unittests:: config:: commandLineParsing:: parseCommandLine", () => { + function assertParseResult(commandLine: string[], expectedParsedCommandLine: ParsedCommandLine, workerDiagnostic?: () => ParseCommandLineWorkerDiagnostics) { + const parsed = parseCommandLineWorker(workerDiagnostic?.() || compilerOptionsDidYouMeanDiagnostics, commandLine); + assert.deepEqual(parsed.options, expectedParsedCommandLine.options); + assert.deepEqual(parsed.watchOptions, expectedParsedCommandLine.watchOptions); + const parsedErrors = parsed.errors; + const expectedErrors = expectedParsedCommandLine.errors; + assert.isTrue(parsedErrors.length === expectedErrors.length, `Expected error: ${JSON.stringify(expectedErrors)}. Actual error: ${JSON.stringify(parsedErrors)}.`); + for (let i = 0; i < parsedErrors.length; i++) { + const parsedError = parsedErrors[i]; + const expectedError = expectedErrors[i]; + assert.equal(parsedError.code, expectedError.code); + assert.equal(parsedError.category, expectedError.category); + assert.equal(parsedError.messageText, expectedError.messageText); } - - it("Parse single option of library flag ", () => { - // --lib es6 0.ts - assertParseResult(["--lib", "es6", "0.ts"], - { - errors: [], - fileNames: ["0.ts"], - options: { - lib: ["lib.es2015.d.ts"] - } - }); + const parsedFileNames = parsed.fileNames; + const expectedFileNames = expectedParsedCommandLine.fileNames; + assert.isTrue(parsedFileNames.length === expectedFileNames.length, `Expected fileNames: [${JSON.stringify(expectedFileNames)}]. Actual fileNames: [${JSON.stringify(parsedFileNames)}].`); + for (let i = 0; i < parsedFileNames.length; i++) { + const parsedFileName = parsedFileNames[i]; + const expectedFileName = expectedFileNames[i]; + assert.equal(parsedFileName, expectedFileName); + } + } + it("Parse single option of library flag ", () => { + // --lib es6 0.ts + assertParseResult(["--lib", "es6", "0.ts"], { + errors: [], + fileNames: ["0.ts"], + options: { + lib: ["lib.es2015.d.ts"] + } }); - - it("Handles 'did you mean?' for misspelt flags", () => { - // --declarations --allowTS - assertParseResult(["--declarations", "--allowTS"], { - errors: [ - { - messageText: "Unknown compiler option '--declarations'. Did you mean 'declaration'?", - category: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1.category, - code: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1.code, - file: undefined, - start: undefined, - length: undefined - }, - { - messageText: "Unknown compiler option '--allowTS'. Did you mean 'allowJs'?", - category: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1.category, - code: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1.code, - file: undefined, - start: undefined, - length: undefined - } - ], - fileNames: [], - options: {} - }); + }); + it("Handles 'did you mean?' for misspelt flags", () => { + // --declarations --allowTS + assertParseResult(["--declarations", "--allowTS"], { + errors: [ + { + messageText: "Unknown compiler option '--declarations'. Did you mean 'declaration'?", + category: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1.category, + code: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1.code, + file: undefined, + start: undefined, + length: undefined + }, + { + messageText: "Unknown compiler option '--allowTS'. Did you mean 'allowJs'?", + category: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1.category, + code: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1.code, + file: undefined, + start: undefined, + length: undefined + } + ], + fileNames: [], + options: {} }); - - - it("Parse multiple options of library flags ", () => { - // --lib es5,es2015.symbol.wellknown 0.ts - assertParseResult(["--lib", "es5,es2015.symbol.wellknown", "0.ts"], - { - errors: [], - fileNames: ["0.ts"], - options: { - lib: ["lib.es5.d.ts", "lib.es2015.symbol.wellknown.d.ts"] - } - }); + }); + it("Parse multiple options of library flags ", () => { + // --lib es5,es2015.symbol.wellknown 0.ts + assertParseResult(["--lib", "es5,es2015.symbol.wellknown", "0.ts"], { + errors: [], + fileNames: ["0.ts"], + options: { + lib: ["lib.es5.d.ts", "lib.es2015.symbol.wellknown.d.ts"] + } }); - - it("Parse invalid option of library flags ", () => { - // --lib es5,invalidOption 0.ts - assertParseResult(["--lib", "es5,invalidOption", "0.ts"], - { - errors: [{ - messageText: "Argument for '--lib' option must be: 'es5', 'es6', 'es2015', 'es7', 'es2016', 'es2017', 'es2018', 'es2019', 'es2020', 'esnext', 'dom', 'dom.iterable', 'webworker', 'webworker.importscripts', 'scripthost', 'es2015.core', 'es2015.collection', 'es2015.generator', 'es2015.iterable', 'es2015.promise', 'es2015.proxy', 'es2015.reflect', 'es2015.symbol', 'es2015.symbol.wellknown', 'es2016.array.include', 'es2017.object', 'es2017.sharedmemory', 'es2017.string', 'es2017.intl', 'es2017.typedarrays', 'es2018.asyncgenerator', 'es2018.asynciterable', 'es2018.intl', 'es2018.promise', 'es2018.regexp', 'es2019.array', 'es2019.object', 'es2019.string', 'es2019.symbol', 'es2020.bigint', 'es2020.promise', 'es2020.string', 'es2020.symbol.wellknown', 'esnext.array', 'esnext.symbol', 'esnext.asynciterable', 'esnext.intl', 'esnext.bigint'.", - category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category, - code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - file: undefined, - start: undefined, - length: undefined, - }], - fileNames: ["0.ts"], - options: { - lib: ["lib.es5.d.ts"] - } - }); + }); + it("Parse invalid option of library flags ", () => { + // --lib es5,invalidOption 0.ts + assertParseResult(["--lib", "es5,invalidOption", "0.ts"], { + errors: [{ + messageText: "Argument for '--lib' option must be: 'es5', 'es6', 'es2015', 'es7', 'es2016', 'es2017', 'es2018', 'es2019', 'es2020', 'esnext', 'dom', 'dom.iterable', 'webworker', 'webworker.importscripts', 'scripthost', 'es2015.core', 'es2015.collection', 'es2015.generator', 'es2015.iterable', 'es2015.promise', 'es2015.proxy', 'es2015.reflect', 'es2015.symbol', 'es2015.symbol.wellknown', 'es2016.array.include', 'es2017.object', 'es2017.sharedmemory', 'es2017.string', 'es2017.intl', 'es2017.typedarrays', 'es2018.asyncgenerator', 'es2018.asynciterable', 'es2018.intl', 'es2018.promise', 'es2018.regexp', 'es2019.array', 'es2019.object', 'es2019.string', 'es2019.symbol', 'es2020.bigint', 'es2020.promise', 'es2020.string', 'es2020.symbol.wellknown', 'esnext.array', 'esnext.symbol', 'esnext.asynciterable', 'esnext.intl', 'esnext.bigint'.", + category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category, + code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + file: undefined, + start: undefined, + length: undefined, + }], + fileNames: ["0.ts"], + options: { + lib: ["lib.es5.d.ts"] + } }); - it("Parse empty options of --jsx ", () => { - // 0.ts --jsx - assertParseResult(["0.ts", "--jsx"], - { - errors: [{ - messageText: "Compiler option 'jsx' expects an argument.", - category: Diagnostics.Compiler_option_0_expects_an_argument.category, - code: Diagnostics.Compiler_option_0_expects_an_argument.code, - - file: undefined, - start: undefined, - length: undefined, - }, { - messageText: "Argument for '--jsx' option must be: 'preserve', 'react-native', 'react'.", - category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category, - code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - - file: undefined, - start: undefined, - length: undefined, - }], - fileNames: ["0.ts"], - options: { jsx: undefined } - }); + }); + it("Parse empty options of --jsx ", () => { + // 0.ts --jsx + assertParseResult(["0.ts", "--jsx"], { + errors: [{ + messageText: "Compiler option 'jsx' expects an argument.", + category: Diagnostics.Compiler_option_0_expects_an_argument.category, + code: Diagnostics.Compiler_option_0_expects_an_argument.code, + file: undefined, + start: undefined, + length: undefined, + }, { + messageText: "Argument for '--jsx' option must be: 'preserve', 'react-native', 'react'.", + category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category, + code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + file: undefined, + start: undefined, + length: undefined, + }], + fileNames: ["0.ts"], + options: { jsx: undefined } }); - - it("Parse empty options of --module ", () => { - // 0.ts -- - assertParseResult(["0.ts", "--module"], - { - errors: [{ - messageText: "Compiler option 'module' expects an argument.", - category: Diagnostics.Compiler_option_0_expects_an_argument.category, - code: Diagnostics.Compiler_option_0_expects_an_argument.code, - - file: undefined, - start: undefined, - length: undefined, - }, { - messageText: "Argument for '--module' option must be: 'none', 'commonjs', 'amd', 'system', 'umd', 'es6', 'es2015', 'es2020', 'esnext'.", - category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category, - code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - - file: undefined, - start: undefined, - length: undefined, - }], - fileNames: ["0.ts"], - options: { module: undefined } - }); + }); + it("Parse empty options of --module ", () => { + // 0.ts -- + assertParseResult(["0.ts", "--module"], { + errors: [{ + messageText: "Compiler option 'module' expects an argument.", + category: Diagnostics.Compiler_option_0_expects_an_argument.category, + code: Diagnostics.Compiler_option_0_expects_an_argument.code, + file: undefined, + start: undefined, + length: undefined, + }, { + messageText: "Argument for '--module' option must be: 'none', 'commonjs', 'amd', 'system', 'umd', 'es6', 'es2015', 'es2020', 'esnext'.", + category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category, + code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + file: undefined, + start: undefined, + length: undefined, + }], + fileNames: ["0.ts"], + options: { module: undefined } }); - - it("Parse empty options of --newLine ", () => { - // 0.ts --newLine - assertParseResult(["0.ts", "--newLine"], - { - errors: [{ - messageText: "Compiler option 'newLine' expects an argument.", - category: Diagnostics.Compiler_option_0_expects_an_argument.category, - code: Diagnostics.Compiler_option_0_expects_an_argument.code, - - file: undefined, - start: undefined, - length: undefined, - }, { - messageText: "Argument for '--newLine' option must be: 'crlf', 'lf'.", - category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category, - code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - - file: undefined, - start: undefined, - length: undefined, - }], - fileNames: ["0.ts"], - options: { newLine: undefined } - }); + }); + it("Parse empty options of --newLine ", () => { + // 0.ts --newLine + assertParseResult(["0.ts", "--newLine"], { + errors: [{ + messageText: "Compiler option 'newLine' expects an argument.", + category: Diagnostics.Compiler_option_0_expects_an_argument.category, + code: Diagnostics.Compiler_option_0_expects_an_argument.code, + file: undefined, + start: undefined, + length: undefined, + }, { + messageText: "Argument for '--newLine' option must be: 'crlf', 'lf'.", + category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category, + code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + file: undefined, + start: undefined, + length: undefined, + }], + fileNames: ["0.ts"], + options: { newLine: undefined } }); - - it("Parse empty options of --target ", () => { - // 0.ts --target - assertParseResult(["0.ts", "--target"], - { - errors: [{ - messageText: "Compiler option 'target' expects an argument.", - category: Diagnostics.Compiler_option_0_expects_an_argument.category, - code: Diagnostics.Compiler_option_0_expects_an_argument.code, - - file: undefined, - start: undefined, - length: undefined, - }, { - messageText: "Argument for '--target' option must be: 'es3', 'es5', 'es6', 'es2015', 'es2016', 'es2017', 'es2018', 'es2019', 'es2020', 'esnext'.", - category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category, - code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - - file: undefined, - start: undefined, - length: undefined, - }], - fileNames: ["0.ts"], - options: { target: undefined } - }); + }); + it("Parse empty options of --target ", () => { + // 0.ts --target + assertParseResult(["0.ts", "--target"], { + errors: [{ + messageText: "Compiler option 'target' expects an argument.", + category: Diagnostics.Compiler_option_0_expects_an_argument.category, + code: Diagnostics.Compiler_option_0_expects_an_argument.code, + file: undefined, + start: undefined, + length: undefined, + }, { + messageText: "Argument for '--target' option must be: 'es3', 'es5', 'es6', 'es2015', 'es2016', 'es2017', 'es2018', 'es2019', 'es2020', 'esnext'.", + category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category, + code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + file: undefined, + start: undefined, + length: undefined, + }], + fileNames: ["0.ts"], + options: { target: undefined } }); - - it("Parse empty options of --moduleResolution ", () => { - // 0.ts --moduleResolution - assertParseResult(["0.ts", "--moduleResolution"], - { - errors: [{ - messageText: "Compiler option 'moduleResolution' expects an argument.", - category: Diagnostics.Compiler_option_0_expects_an_argument.category, - code: Diagnostics.Compiler_option_0_expects_an_argument.code, - - file: undefined, - start: undefined, - length: undefined, - }, { - messageText: "Argument for '--moduleResolution' option must be: 'node', 'classic'.", - category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category, - code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - - file: undefined, - start: undefined, - length: undefined, - }], - fileNames: ["0.ts"], - options: { moduleResolution: undefined } - }); + }); + it("Parse empty options of --moduleResolution ", () => { + // 0.ts --moduleResolution + assertParseResult(["0.ts", "--moduleResolution"], { + errors: [{ + messageText: "Compiler option 'moduleResolution' expects an argument.", + category: Diagnostics.Compiler_option_0_expects_an_argument.category, + code: Diagnostics.Compiler_option_0_expects_an_argument.code, + file: undefined, + start: undefined, + length: undefined, + }, { + messageText: "Argument for '--moduleResolution' option must be: 'node', 'classic'.", + category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category, + code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + file: undefined, + start: undefined, + length: undefined, + }], + fileNames: ["0.ts"], + options: { moduleResolution: undefined } }); - - it("Parse empty options of --lib ", () => { - // 0.ts --lib - assertParseResult(["0.ts", "--lib"], - { - errors: [{ - messageText: "Compiler option 'lib' expects an argument.", - category: Diagnostics.Compiler_option_0_expects_an_argument.category, - code: Diagnostics.Compiler_option_0_expects_an_argument.code, - - file: undefined, - start: undefined, - length: undefined, - }], - fileNames: ["0.ts"], - options: { - lib: [] - } - }); + }); + it("Parse empty options of --lib ", () => { + // 0.ts --lib + assertParseResult(["0.ts", "--lib"], { + errors: [{ + messageText: "Compiler option 'lib' expects an argument.", + category: Diagnostics.Compiler_option_0_expects_an_argument.category, + code: Diagnostics.Compiler_option_0_expects_an_argument.code, + file: undefined, + start: undefined, + length: undefined, + }], + fileNames: ["0.ts"], + options: { + lib: [] + } }); - - it("Parse empty string of --lib ", () => { - // 0.ts --lib - // This test is an error because the empty string is falsey - assertParseResult(["0.ts", "--lib", ""], - { - errors: [{ - messageText: "Compiler option 'lib' expects an argument.", - category: Diagnostics.Compiler_option_0_expects_an_argument.category, - code: Diagnostics.Compiler_option_0_expects_an_argument.code, - - file: undefined, - start: undefined, - length: undefined, - }], - fileNames: ["0.ts"], - options: { - lib: [] - } - }); + }); + it("Parse empty string of --lib ", () => { + // 0.ts --lib + // This test is an error because the empty string is falsey + assertParseResult(["0.ts", "--lib", ""], { + errors: [{ + messageText: "Compiler option 'lib' expects an argument.", + category: Diagnostics.Compiler_option_0_expects_an_argument.category, + code: Diagnostics.Compiler_option_0_expects_an_argument.code, + file: undefined, + start: undefined, + length: undefined, + }], + fileNames: ["0.ts"], + options: { + lib: [] + } }); - - it("Parse immediately following command line argument of --lib ", () => { - // 0.ts --lib - assertParseResult(["0.ts", "--lib", "--sourcemap"], - { - errors: [], - fileNames: ["0.ts"], - options: { - lib: [], - sourceMap: true - } - }); + }); + it("Parse immediately following command line argument of --lib ", () => { + // 0.ts --lib + assertParseResult(["0.ts", "--lib", "--sourcemap"], { + errors: [], + fileNames: ["0.ts"], + options: { + lib: [], + sourceMap: true + } }); - - it("Parse --lib option with extra comma ", () => { - // --lib es5, es7 0.ts - assertParseResult(["--lib", "es5,", "es7", "0.ts"], - { - errors: [{ - messageText: "Argument for '--lib' option must be: 'es5', 'es6', 'es2015', 'es7', 'es2016', 'es2017', 'es2018', 'es2019', 'es2020', 'esnext', 'dom', 'dom.iterable', 'webworker', 'webworker.importscripts', 'scripthost', 'es2015.core', 'es2015.collection', 'es2015.generator', 'es2015.iterable', 'es2015.promise', 'es2015.proxy', 'es2015.reflect', 'es2015.symbol', 'es2015.symbol.wellknown', 'es2016.array.include', 'es2017.object', 'es2017.sharedmemory', 'es2017.string', 'es2017.intl', 'es2017.typedarrays', 'es2018.asyncgenerator', 'es2018.asynciterable', 'es2018.intl', 'es2018.promise', 'es2018.regexp', 'es2019.array', 'es2019.object', 'es2019.string', 'es2019.symbol', 'es2020.bigint', 'es2020.promise', 'es2020.string', 'es2020.symbol.wellknown', 'esnext.array', 'esnext.symbol', 'esnext.asynciterable', 'esnext.intl', 'esnext.bigint'.", - category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category, - code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - file: undefined, - start: undefined, - length: undefined, - }], - fileNames: ["es7", "0.ts"], - options: { - lib: ["lib.es5.d.ts"] - } - }); + }); + it("Parse --lib option with extra comma ", () => { + // --lib es5, es7 0.ts + assertParseResult(["--lib", "es5,", "es7", "0.ts"], { + errors: [{ + messageText: "Argument for '--lib' option must be: 'es5', 'es6', 'es2015', 'es7', 'es2016', 'es2017', 'es2018', 'es2019', 'es2020', 'esnext', 'dom', 'dom.iterable', 'webworker', 'webworker.importscripts', 'scripthost', 'es2015.core', 'es2015.collection', 'es2015.generator', 'es2015.iterable', 'es2015.promise', 'es2015.proxy', 'es2015.reflect', 'es2015.symbol', 'es2015.symbol.wellknown', 'es2016.array.include', 'es2017.object', 'es2017.sharedmemory', 'es2017.string', 'es2017.intl', 'es2017.typedarrays', 'es2018.asyncgenerator', 'es2018.asynciterable', 'es2018.intl', 'es2018.promise', 'es2018.regexp', 'es2019.array', 'es2019.object', 'es2019.string', 'es2019.symbol', 'es2020.bigint', 'es2020.promise', 'es2020.string', 'es2020.symbol.wellknown', 'esnext.array', 'esnext.symbol', 'esnext.asynciterable', 'esnext.intl', 'esnext.bigint'.", + category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category, + code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + file: undefined, + start: undefined, + length: undefined, + }], + fileNames: ["es7", "0.ts"], + options: { + lib: ["lib.es5.d.ts"] + } }); - - it("Parse --lib option with trailing white-space ", () => { - // --lib es5, es7 0.ts - assertParseResult(["--lib", "es5, ", "es7", "0.ts"], - { - errors: [{ - messageText: "Argument for '--lib' option must be: 'es5', 'es6', 'es2015', 'es7', 'es2016', 'es2017', 'es2018', 'es2019', 'es2020', 'esnext', 'dom', 'dom.iterable', 'webworker', 'webworker.importscripts', 'scripthost', 'es2015.core', 'es2015.collection', 'es2015.generator', 'es2015.iterable', 'es2015.promise', 'es2015.proxy', 'es2015.reflect', 'es2015.symbol', 'es2015.symbol.wellknown', 'es2016.array.include', 'es2017.object', 'es2017.sharedmemory', 'es2017.string', 'es2017.intl', 'es2017.typedarrays', 'es2018.asyncgenerator', 'es2018.asynciterable', 'es2018.intl', 'es2018.promise', 'es2018.regexp', 'es2019.array', 'es2019.object', 'es2019.string', 'es2019.symbol', 'es2020.bigint', 'es2020.promise', 'es2020.string', 'es2020.symbol.wellknown', 'esnext.array', 'esnext.symbol', 'esnext.asynciterable', 'esnext.intl', 'esnext.bigint'.", - category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category, - code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - file: undefined, - start: undefined, - length: undefined, - }], - fileNames: ["es7", "0.ts"], - options: { - lib: ["lib.es5.d.ts"] - } - }); + }); + it("Parse --lib option with trailing white-space ", () => { + // --lib es5, es7 0.ts + assertParseResult(["--lib", "es5, ", "es7", "0.ts"], { + errors: [{ + messageText: "Argument for '--lib' option must be: 'es5', 'es6', 'es2015', 'es7', 'es2016', 'es2017', 'es2018', 'es2019', 'es2020', 'esnext', 'dom', 'dom.iterable', 'webworker', 'webworker.importscripts', 'scripthost', 'es2015.core', 'es2015.collection', 'es2015.generator', 'es2015.iterable', 'es2015.promise', 'es2015.proxy', 'es2015.reflect', 'es2015.symbol', 'es2015.symbol.wellknown', 'es2016.array.include', 'es2017.object', 'es2017.sharedmemory', 'es2017.string', 'es2017.intl', 'es2017.typedarrays', 'es2018.asyncgenerator', 'es2018.asynciterable', 'es2018.intl', 'es2018.promise', 'es2018.regexp', 'es2019.array', 'es2019.object', 'es2019.string', 'es2019.symbol', 'es2020.bigint', 'es2020.promise', 'es2020.string', 'es2020.symbol.wellknown', 'esnext.array', 'esnext.symbol', 'esnext.asynciterable', 'esnext.intl', 'esnext.bigint'.", + category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category, + code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + file: undefined, + start: undefined, + length: undefined, + }], + fileNames: ["es7", "0.ts"], + options: { + lib: ["lib.es5.d.ts"] + } }); - - it("Parse multiple compiler flags with input files at the end", () => { - // --lib es5,es2015.symbol.wellknown --target es5 0.ts - assertParseResult(["--lib", "es5,es2015.symbol.wellknown", "--target", "es5", "0.ts"], - { - errors: [], - fileNames: ["0.ts"], - options: { - lib: ["lib.es5.d.ts", "lib.es2015.symbol.wellknown.d.ts"], - target: ScriptTarget.ES5, - } - }); + }); + it("Parse multiple compiler flags with input files at the end", () => { + // --lib es5,es2015.symbol.wellknown --target es5 0.ts + assertParseResult(["--lib", "es5,es2015.symbol.wellknown", "--target", "es5", "0.ts"], { + errors: [], + fileNames: ["0.ts"], + options: { + lib: ["lib.es5.d.ts", "lib.es2015.symbol.wellknown.d.ts"], + target: ScriptTarget.ES5, + } }); - - it("Parse multiple compiler flags with input files in the middle", () => { - // --module commonjs --target es5 0.ts --lib es5,es2015.symbol.wellknown - assertParseResult(["--module", "commonjs", "--target", "es5", "0.ts", "--lib", "es5,es2015.symbol.wellknown"], - { - errors: [], - fileNames: ["0.ts"], - options: { - module: ModuleKind.CommonJS, - target: ScriptTarget.ES5, - lib: ["lib.es5.d.ts", "lib.es2015.symbol.wellknown.d.ts"], - } - }); + }); + it("Parse multiple compiler flags with input files in the middle", () => { + // --module commonjs --target es5 0.ts --lib es5,es2015.symbol.wellknown + assertParseResult(["--module", "commonjs", "--target", "es5", "0.ts", "--lib", "es5,es2015.symbol.wellknown"], { + errors: [], + fileNames: ["0.ts"], + options: { + module: ModuleKind.CommonJS, + target: ScriptTarget.ES5, + lib: ["lib.es5.d.ts", "lib.es2015.symbol.wellknown.d.ts"], + } }); - - it("Parse multiple library compiler flags ", () => { - // --module commonjs --target es5 --lib es5 0.ts --library es2015.array,es2015.symbol.wellknown - assertParseResult(["--module", "commonjs", "--target", "es5", "--lib", "es5", "0.ts", "--lib", "es2015.core, es2015.symbol.wellknown "], - { - errors: [], - fileNames: ["0.ts"], - options: { - module: ModuleKind.CommonJS, - target: ScriptTarget.ES5, - lib: ["lib.es2015.core.d.ts", "lib.es2015.symbol.wellknown.d.ts"], - } - }); + }); + it("Parse multiple library compiler flags ", () => { + // --module commonjs --target es5 --lib es5 0.ts --library es2015.array,es2015.symbol.wellknown + assertParseResult(["--module", "commonjs", "--target", "es5", "--lib", "es5", "0.ts", "--lib", "es2015.core, es2015.symbol.wellknown "], { + errors: [], + fileNames: ["0.ts"], + options: { + module: ModuleKind.CommonJS, + target: ScriptTarget.ES5, + lib: ["lib.es2015.core.d.ts", "lib.es2015.symbol.wellknown.d.ts"], + } }); - - it("Parse explicit boolean flag value", () => { - assertParseResult(["--strictNullChecks", "false", "0.ts"], - { - errors: [], - fileNames: ["0.ts"], - options: { - strictNullChecks: false, - } - }); + }); + it("Parse explicit boolean flag value", () => { + assertParseResult(["--strictNullChecks", "false", "0.ts"], { + errors: [], + fileNames: ["0.ts"], + options: { + strictNullChecks: false, + } }); - - it("Parse non boolean argument after boolean flag", () => { - assertParseResult(["--noImplicitAny", "t", "0.ts"], - { - errors: [], - fileNames: ["t", "0.ts"], - options: { - noImplicitAny: true, - } - }); + }); + it("Parse non boolean argument after boolean flag", () => { + assertParseResult(["--noImplicitAny", "t", "0.ts"], { + errors: [], + fileNames: ["t", "0.ts"], + options: { + noImplicitAny: true, + } }); - - it("Parse implicit boolean flag value", () => { - assertParseResult(["--strictNullChecks"], - { - errors: [], - fileNames: [], - options: { - strictNullChecks: true, - } - }); + }); + it("Parse implicit boolean flag value", () => { + assertParseResult(["--strictNullChecks"], { + errors: [], + fileNames: [], + options: { + strictNullChecks: true, + } }); - - it("parse --incremental", () => { - // --lib es6 0.ts - assertParseResult(["--incremental", "0.ts"], - { - errors: [], - fileNames: ["0.ts"], - options: { incremental: true } - }); + }); + it("parse --incremental", () => { + // --lib es6 0.ts + assertParseResult(["--incremental", "0.ts"], { + errors: [], + fileNames: ["0.ts"], + options: { incremental: true } }); - - it("parse --tsBuildInfoFile", () => { - // --lib es6 0.ts - assertParseResult(["--tsBuildInfoFile", "build.tsbuildinfo", "0.ts"], - { + }); + it("parse --tsBuildInfoFile", () => { + // --lib es6 0.ts + assertParseResult(["--tsBuildInfoFile", "build.tsbuildinfo", "0.ts"], { + errors: [], + fileNames: ["0.ts"], + options: { tsBuildInfoFile: "build.tsbuildinfo" } + }); + }); + describe("parses command line null for tsconfig only option", () => { + interface VerifyNull { + optionName: string; + nonNullValue?: string; + workerDiagnostic?: () => ParseCommandLineWorkerDiagnostics; + diagnosticMessage: DiagnosticMessage; + } + function verifyNull({ optionName, nonNullValue, workerDiagnostic, diagnosticMessage }: VerifyNull) { + it("allows setting it to null", () => { + assertParseResult([`--${optionName}`, "null", "0.ts"], { errors: [], fileNames: ["0.ts"], - options: { tsBuildInfoFile: "build.tsbuildinfo" } - }); - }); - - describe("parses command line null for tsconfig only option", () => { - interface VerifyNull { - optionName: string; - nonNullValue?: string; - workerDiagnostic?: () => ParseCommandLineWorkerDiagnostics; - diagnosticMessage: DiagnosticMessage; - } - function verifyNull({ optionName, nonNullValue, workerDiagnostic, diagnosticMessage }: VerifyNull) { - it("allows setting it to null", () => { - assertParseResult( - [`--${optionName}`, "null", "0.ts"], - { - errors: [], - fileNames: ["0.ts"], - options: { [optionName]: undefined } - }, - workerDiagnostic - ); - }); - - if (nonNullValue) { - it("errors if non null value is passed", () => { - assertParseResult( - [`--${optionName}`, nonNullValue, "0.ts"], - { - errors: [{ - messageText: formatStringFromArgs(diagnosticMessage.message, [optionName]), - category: diagnosticMessage.category, - code: diagnosticMessage.code, - file: undefined, - start: undefined, - length: undefined - }], - fileNames: ["0.ts"], - options: {} - }, - workerDiagnostic - ); - }); - } - - it("errors if its followed by another option", () => { - assertParseResult( - ["0.ts", "--strictNullChecks", `--${optionName}`], - { - errors: [{ - messageText: formatStringFromArgs(diagnosticMessage.message, [optionName]), - category: diagnosticMessage.category, - code: diagnosticMessage.code, - file: undefined, - start: undefined, - length: undefined - }], - fileNames: ["0.ts"], - options: { strictNullChecks: true } - }, - workerDiagnostic - ); - }); - - it("errors if its last option", () => { - assertParseResult( - ["0.ts", `--${optionName}`], - { - errors: [{ + options: { [optionName]: undefined } + }, workerDiagnostic); + }); + if (nonNullValue) { + it("errors if non null value is passed", () => { + assertParseResult([`--${optionName}`, nonNullValue, "0.ts"], { + errors: [{ messageText: formatStringFromArgs(diagnosticMessage.message, [optionName]), category: diagnosticMessage.category, code: diagnosticMessage.code, @@ -484,427 +380,395 @@ namespace ts { start: undefined, length: undefined }], - fileNames: ["0.ts"], - options: {} - }, - workerDiagnostic - ); - }); - } - - interface VerifyNullNonIncludedOption { - type: () => "string" | "number" | Map; - nonNullValue?: string; - } - function verifyNullNonIncludedOption({ type, nonNullValue }: VerifyNullNonIncludedOption) { - verifyNull({ - optionName: "optionName", - nonNullValue, - diagnosticMessage: Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_null_on_command_line, - workerDiagnostic: () => { - const optionDeclarations = [ - ...compilerOptionsDidYouMeanDiagnostics.optionDeclarations, - { - name: "optionName", - type: type(), - isTSConfigOnly: true, - category: Diagnostics.Basic_Options, - description: Diagnostics.Enable_project_compilation, - } - ]; - return { - ...compilerOptionsDidYouMeanDiagnostics, - optionDeclarations, - getOptionsNameMap: () => createOptionNameMap(optionDeclarations) - }; - } + fileNames: ["0.ts"], + options: {} + }, workerDiagnostic); }); } - - describe("option of type boolean", () => { - it("allows setting it to false", () => { - assertParseResult( - ["--composite", "false", "0.ts"], + it("errors if its followed by another option", () => { + assertParseResult(["0.ts", "--strictNullChecks", `--${optionName}`], { + errors: [{ + messageText: formatStringFromArgs(diagnosticMessage.message, [optionName]), + category: diagnosticMessage.category, + code: diagnosticMessage.code, + file: undefined, + start: undefined, + length: undefined + }], + fileNames: ["0.ts"], + options: { strictNullChecks: true } + }, workerDiagnostic); + }); + it("errors if its last option", () => { + assertParseResult(["0.ts", `--${optionName}`], { + errors: [{ + messageText: formatStringFromArgs(diagnosticMessage.message, [optionName]), + category: diagnosticMessage.category, + code: diagnosticMessage.code, + file: undefined, + start: undefined, + length: undefined + }], + fileNames: ["0.ts"], + options: {} + }, workerDiagnostic); + }); + } + interface VerifyNullNonIncludedOption { + type: () => "string" | "number" | ts.Map; + nonNullValue?: string; + } + function verifyNullNonIncludedOption({ type, nonNullValue }: VerifyNullNonIncludedOption) { + verifyNull({ + optionName: "optionName", + nonNullValue, + diagnosticMessage: Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_null_on_command_line, + workerDiagnostic: () => { + const optionDeclarations = [ + ...compilerOptionsDidYouMeanDiagnostics.optionDeclarations, { - errors: [], - fileNames: ["0.ts"], - options: { composite: false } + name: "optionName", + type: type(), + isTSConfigOnly: true, + category: Diagnostics.Basic_Options, + description: Diagnostics.Enable_project_compilation, } - ); - }); - - verifyNull({ - optionName: "composite", - nonNullValue: "true", - diagnosticMessage: Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_false_or_null_on_command_line - }); + ]; + return { + ...compilerOptionsDidYouMeanDiagnostics, + optionDeclarations, + getOptionsNameMap: () => createOptionNameMap(optionDeclarations) + }; + } }); - - describe("option of type object", () => { - verifyNull({ - optionName: "paths", - diagnosticMessage: Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_null_on_command_line + } + describe("option of type boolean", () => { + it("allows setting it to false", () => { + assertParseResult(["--composite", "false", "0.ts"], { + errors: [], + fileNames: ["0.ts"], + options: { composite: false } }); }); - - describe("option of type list", () => { - verifyNull({ - optionName: "rootDirs", - nonNullValue: "abc,xyz", - diagnosticMessage: Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_null_on_command_line - }); + verifyNull({ + optionName: "composite", + nonNullValue: "true", + diagnosticMessage: Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_false_or_null_on_command_line }); - - describe("option of type string", () => { - verifyNullNonIncludedOption({ - type: () => "string", - nonNullValue: "hello" - }); + }); + describe("option of type object", () => { + verifyNull({ + optionName: "paths", + diagnosticMessage: Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_null_on_command_line }); - - describe("option of type number", () => { - verifyNullNonIncludedOption({ - type: () => "number", - nonNullValue: "10" - }); + }); + describe("option of type list", () => { + verifyNull({ + optionName: "rootDirs", + nonNullValue: "abc,xyz", + diagnosticMessage: Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_null_on_command_line }); - - describe("option of type Map", () => { - verifyNullNonIncludedOption({ - type: () => createMapFromTemplate({ - node: ModuleResolutionKind.NodeJs, - classic: ModuleResolutionKind.Classic, - }), - nonNullValue: "node" - }); + }); + describe("option of type string", () => { + verifyNullNonIncludedOption({ + type: () => "string", + nonNullValue: "hello" }); }); - - it("allows tsconfig only option to be set to null", () => { - assertParseResult(["--composite", "null", "-tsBuildInfoFile", "null", "0.ts"], - { - errors: [], - fileNames: ["0.ts"], - options: { composite: undefined, tsBuildInfoFile: undefined } - }); + describe("option of type number", () => { + verifyNullNonIncludedOption({ + type: () => "number", + nonNullValue: "10" + }); }); - - describe("Watch options", () => { - it("parse --watchFile", () => { - assertParseResult(["--watchFile", "UseFsEvents", "0.ts"], - { - errors: [], - fileNames: ["0.ts"], - options: {}, - watchOptions: { watchFile: WatchFileKind.UseFsEvents } - }); + describe("option of type Map", () => { + verifyNullNonIncludedOption({ + type: () => createMapFromTemplate({ + node: ModuleResolutionKind.NodeJs, + classic: ModuleResolutionKind.Classic, + }), + nonNullValue: "node" }); - - it("parse --watchDirectory", () => { - assertParseResult(["--watchDirectory", "FixedPollingInterval", "0.ts"], - { - errors: [], - fileNames: ["0.ts"], - options: {}, - watchOptions: { watchDirectory: WatchDirectoryKind.FixedPollingInterval } - }); + }); + }); + it("allows tsconfig only option to be set to null", () => { + assertParseResult(["--composite", "null", "-tsBuildInfoFile", "null", "0.ts"], { + errors: [], + fileNames: ["0.ts"], + options: { composite: undefined, tsBuildInfoFile: undefined } + }); + }); + describe("Watch options", () => { + it("parse --watchFile", () => { + assertParseResult(["--watchFile", "UseFsEvents", "0.ts"], { + errors: [], + fileNames: ["0.ts"], + options: {}, + watchOptions: { watchFile: WatchFileKind.UseFsEvents } }); - - it("parse --fallbackPolling", () => { - assertParseResult(["--fallbackPolling", "PriorityInterval", "0.ts"], - { - errors: [], - fileNames: ["0.ts"], - options: {}, - watchOptions: { fallbackPolling: PollingWatchKind.PriorityInterval } - }); + }); + it("parse --watchDirectory", () => { + assertParseResult(["--watchDirectory", "FixedPollingInterval", "0.ts"], { + errors: [], + fileNames: ["0.ts"], + options: {}, + watchOptions: { watchDirectory: WatchDirectoryKind.FixedPollingInterval } }); - - it("parse --synchronousWatchDirectory", () => { - assertParseResult(["--synchronousWatchDirectory", "0.ts"], - { - errors: [], - fileNames: ["0.ts"], - options: {}, - watchOptions: { synchronousWatchDirectory: true } - }); + }); + it("parse --fallbackPolling", () => { + assertParseResult(["--fallbackPolling", "PriorityInterval", "0.ts"], { + errors: [], + fileNames: ["0.ts"], + options: {}, + watchOptions: { fallbackPolling: PollingWatchKind.PriorityInterval } }); - - it("errors on missing argument to --fallbackPolling", () => { - assertParseResult(["0.ts", "--fallbackPolling"], + }); + it("parse --synchronousWatchDirectory", () => { + assertParseResult(["--synchronousWatchDirectory", "0.ts"], { + errors: [], + fileNames: ["0.ts"], + options: {}, + watchOptions: { synchronousWatchDirectory: true } + }); + }); + it("errors on missing argument to --fallbackPolling", () => { + assertParseResult(["0.ts", "--fallbackPolling"], { + errors: [ { - errors: [ - { - messageText: "Watch option 'fallbackPolling' requires a value of type string.", - category: Diagnostics.Watch_option_0_requires_a_value_of_type_1.category, - code: Diagnostics.Watch_option_0_requires_a_value_of_type_1.code, - file: undefined, - start: undefined, - length: undefined - }, - { - messageText: "Argument for '--fallbackPolling' option must be: 'fixedinterval', 'priorityinterval', 'dynamicpriority'.", - category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category, - code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - file: undefined, - start: undefined, - length: undefined - } - ], - fileNames: ["0.ts"], - options: {}, - watchOptions: { fallbackPolling: undefined } - }); + messageText: "Watch option 'fallbackPolling' requires a value of type string.", + category: Diagnostics.Watch_option_0_requires_a_value_of_type_1.category, + code: Diagnostics.Watch_option_0_requires_a_value_of_type_1.code, + file: undefined, + start: undefined, + length: undefined + }, + { + messageText: "Argument for '--fallbackPolling' option must be: 'fixedinterval', 'priorityinterval', 'dynamicpriority'.", + category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category, + code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + file: undefined, + start: undefined, + length: undefined + } + ], + fileNames: ["0.ts"], + options: {}, + watchOptions: { fallbackPolling: undefined } }); }); }); - - describe("unittests:: config:: commandLineParsing:: parseBuildOptions", () => { - function assertParseResult(commandLine: string[], expectedParsedBuildCommand: ParsedBuildCommand) { - const parsed = parseBuildCommand(commandLine); - assert.deepEqual(parsed.buildOptions, expectedParsedBuildCommand.buildOptions); - assert.deepEqual(parsed.watchOptions, expectedParsedBuildCommand.watchOptions); - - const parsedErrors = parsed.errors; - const expectedErrors = expectedParsedBuildCommand.errors; - assert.isTrue(parsedErrors.length === expectedErrors.length, `Expected error: ${JSON.stringify(expectedErrors)}. Actual error: ${JSON.stringify(parsedErrors)}.`); - for (let i = 0; i < parsedErrors.length; i++) { - const parsedError = parsedErrors[i]; - const expectedError = expectedErrors[i]; - assert.equal(parsedError.code, expectedError.code); - assert.equal(parsedError.category, expectedError.category); - assert.equal(parsedError.messageText, expectedError.messageText); - } - - const parsedProjects = parsed.projects; - const expectedProjects = expectedParsedBuildCommand.projects; - assert.deepEqual(parsedProjects, expectedProjects, `Expected projects: [${JSON.stringify(expectedProjects)}]. Actual projects: [${JSON.stringify(parsedProjects)}].`); +}); +describe("unittests:: config:: commandLineParsing:: parseBuildOptions", () => { + function assertParseResult(commandLine: string[], expectedParsedBuildCommand: ParsedBuildCommand) { + const parsed = parseBuildCommand(commandLine); + assert.deepEqual(parsed.buildOptions, expectedParsedBuildCommand.buildOptions); + assert.deepEqual(parsed.watchOptions, expectedParsedBuildCommand.watchOptions); + const parsedErrors = parsed.errors; + const expectedErrors = expectedParsedBuildCommand.errors; + assert.isTrue(parsedErrors.length === expectedErrors.length, `Expected error: ${JSON.stringify(expectedErrors)}. Actual error: ${JSON.stringify(parsedErrors)}.`); + for (let i = 0; i < parsedErrors.length; i++) { + const parsedError = parsedErrors[i]; + const expectedError = expectedErrors[i]; + assert.equal(parsedError.code, expectedError.code); + assert.equal(parsedError.category, expectedError.category); + assert.equal(parsedError.messageText, expectedError.messageText); } - it("parse build without any options ", () => { - // --lib es6 0.ts - assertParseResult([], - { - errors: [], - projects: ["."], - buildOptions: {}, - watchOptions: undefined - }); + const parsedProjects = parsed.projects; + const expectedProjects = expectedParsedBuildCommand.projects; + assert.deepEqual(parsedProjects, expectedProjects, `Expected projects: [${JSON.stringify(expectedProjects)}]. Actual projects: [${JSON.stringify(parsedProjects)}].`); + } + it("parse build without any options ", () => { + // --lib es6 0.ts + assertParseResult([], { + errors: [], + projects: ["."], + buildOptions: {}, + watchOptions: undefined }); - - it("Parse multiple options", () => { - // --lib es5,es2015.symbol.wellknown 0.ts - assertParseResult(["--verbose", "--force", "tests"], - { - errors: [], - projects: ["tests"], - buildOptions: { verbose: true, force: true }, - watchOptions: undefined - }); + }); + it("Parse multiple options", () => { + // --lib es5,es2015.symbol.wellknown 0.ts + assertParseResult(["--verbose", "--force", "tests"], { + errors: [], + projects: ["tests"], + buildOptions: { verbose: true, force: true }, + watchOptions: undefined }); - - it("Parse option with invalid option ", () => { - // --lib es5,invalidOption 0.ts - assertParseResult(["--verbose", "--invalidOption"], - { - errors: [{ - messageText: "Unknown build option '--invalidOption'.", - category: Diagnostics.Unknown_build_option_0.category, - code: Diagnostics.Unknown_build_option_0.code, - file: undefined, - start: undefined, - length: undefined, - }], - projects: ["."], - buildOptions: { verbose: true }, - watchOptions: undefined - }); + }); + it("Parse option with invalid option ", () => { + // --lib es5,invalidOption 0.ts + assertParseResult(["--verbose", "--invalidOption"], { + errors: [{ + messageText: "Unknown build option '--invalidOption'.", + category: Diagnostics.Unknown_build_option_0.category, + code: Diagnostics.Unknown_build_option_0.code, + file: undefined, + start: undefined, + length: undefined, + }], + projects: ["."], + buildOptions: { verbose: true }, + watchOptions: undefined }); - - it("parse build with listFilesOnly ", () => { - // --lib es6 0.ts - assertParseResult(["--listFilesOnly"], - { - errors: [{ - messageText: "Unknown build option '--listFilesOnly'.", - category: Diagnostics.Unknown_build_option_0.category, - code: Diagnostics.Unknown_build_option_0.code, - file: undefined, - start: undefined, - length: undefined, - }], - projects: ["."], - buildOptions: {}, - watchOptions: undefined, - }); + }); + it("parse build with listFilesOnly ", () => { + // --lib es6 0.ts + assertParseResult(["--listFilesOnly"], { + errors: [{ + messageText: "Unknown build option '--listFilesOnly'.", + category: Diagnostics.Unknown_build_option_0.category, + code: Diagnostics.Unknown_build_option_0.code, + file: undefined, + start: undefined, + length: undefined, + }], + projects: ["."], + buildOptions: {}, + watchOptions: undefined, }); - - it("Parse multiple flags with input projects at the end", () => { - // --lib es5,es2015.symbol.wellknown --target es5 0.ts - assertParseResult(["--force", "--verbose", "src", "tests"], - { - errors: [], - projects: ["src", "tests"], - buildOptions: { force: true, verbose: true }, - watchOptions: undefined, - }); + }); + it("Parse multiple flags with input projects at the end", () => { + // --lib es5,es2015.symbol.wellknown --target es5 0.ts + assertParseResult(["--force", "--verbose", "src", "tests"], { + errors: [], + projects: ["src", "tests"], + buildOptions: { force: true, verbose: true }, + watchOptions: undefined, }); - - it("Parse multiple flags with input projects in the middle", () => { - // --module commonjs --target es5 0.ts --lib es5,es2015.symbol.wellknown - assertParseResult(["--force", "src", "tests", "--verbose"], - { - errors: [], - projects: ["src", "tests"], - buildOptions: { force: true, verbose: true }, - watchOptions: undefined, - }); + }); + it("Parse multiple flags with input projects in the middle", () => { + // --module commonjs --target es5 0.ts --lib es5,es2015.symbol.wellknown + assertParseResult(["--force", "src", "tests", "--verbose"], { + errors: [], + projects: ["src", "tests"], + buildOptions: { force: true, verbose: true }, + watchOptions: undefined, }); - - it("Parse multiple flags with input projects in the beginning", () => { - // --module commonjs --target es5 0.ts --lib es5,es2015.symbol.wellknown - assertParseResult(["src", "tests", "--force", "--verbose"], - { - errors: [], - projects: ["src", "tests"], - buildOptions: { force: true, verbose: true }, - watchOptions: undefined, - }); + }); + it("Parse multiple flags with input projects in the beginning", () => { + // --module commonjs --target es5 0.ts --lib es5,es2015.symbol.wellknown + assertParseResult(["src", "tests", "--force", "--verbose"], { + errors: [], + projects: ["src", "tests"], + buildOptions: { force: true, verbose: true }, + watchOptions: undefined, }); - - it("parse build with --incremental", () => { - // --lib es6 0.ts - assertParseResult(["--incremental", "tests"], - { - errors: [], - projects: ["tests"], - buildOptions: { incremental: true }, - watchOptions: undefined, - }); + }); + it("parse build with --incremental", () => { + // --lib es6 0.ts + assertParseResult(["--incremental", "tests"], { + errors: [], + projects: ["tests"], + buildOptions: { incremental: true }, + watchOptions: undefined, }); - - it("parse build with --locale en-us", () => { - // --lib es6 0.ts - assertParseResult(["--locale", "en-us", "src"], - { - errors: [], - projects: ["src"], - buildOptions: { locale: "en-us" }, - watchOptions: undefined, - }); + }); + it("parse build with --locale en-us", () => { + // --lib es6 0.ts + assertParseResult(["--locale", "en-us", "src"], { + errors: [], + projects: ["src"], + buildOptions: { locale: "en-us" }, + watchOptions: undefined, }); - - it("parse build with --tsBuildInfoFile", () => { - // --lib es6 0.ts - assertParseResult(["--tsBuildInfoFile", "build.tsbuildinfo", "tests"], - { + }); + it("parse build with --tsBuildInfoFile", () => { + // --lib es6 0.ts + assertParseResult(["--tsBuildInfoFile", "build.tsbuildinfo", "tests"], { + errors: [{ + messageText: "Unknown build option '--tsBuildInfoFile'.", + category: Diagnostics.Unknown_build_option_0.category, + code: Diagnostics.Unknown_build_option_0.code, + file: undefined, + start: undefined, + length: undefined + }], + projects: ["build.tsbuildinfo", "tests"], + buildOptions: {}, + watchOptions: undefined, + }); + }); + describe("Combining options that make no sense together", () => { + function verifyInvalidCombination(flag1: keyof BuildOptions, flag2: keyof BuildOptions) { + it(`--${flag1} and --${flag2} together is invalid`, () => { + // --module commonjs --target es5 0.ts --lib es5,es2015.symbol.wellknown + assertParseResult([`--${flag1}`, `--${flag2}`], { errors: [{ - messageText: "Unknown build option '--tsBuildInfoFile'.", - category: Diagnostics.Unknown_build_option_0.category, - code: Diagnostics.Unknown_build_option_0.code, - file: undefined, - start: undefined, - length: undefined - }], - projects: ["build.tsbuildinfo", "tests"], - buildOptions: {}, + messageText: `Options '${flag1}' and '${flag2}' cannot be combined.`, + category: Diagnostics.Options_0_and_1_cannot_be_combined.category, + code: Diagnostics.Options_0_and_1_cannot_be_combined.code, + file: undefined, + start: undefined, + length: undefined, + }], + projects: ["."], + buildOptions: { [flag1]: true, [flag2]: true }, watchOptions: undefined, }); + }); + } + verifyInvalidCombination("clean", "force"); + verifyInvalidCombination("clean", "verbose"); + verifyInvalidCombination("clean", "watch"); + verifyInvalidCombination("watch", "dry"); + }); + describe("Watch options", () => { + it("parse --watchFile", () => { + assertParseResult(["--watchFile", "UseFsEvents", "--verbose"], { + errors: [], + projects: ["."], + buildOptions: { verbose: true }, + watchOptions: { watchFile: WatchFileKind.UseFsEvents } + }); }); - - describe("Combining options that make no sense together", () => { - function verifyInvalidCombination(flag1: keyof BuildOptions, flag2: keyof BuildOptions) { - it(`--${flag1} and --${flag2} together is invalid`, () => { - // --module commonjs --target es5 0.ts --lib es5,es2015.symbol.wellknown - assertParseResult([`--${flag1}`, `--${flag2}`], - { - errors: [{ - messageText: `Options '${flag1}' and '${flag2}' cannot be combined.`, - category: Diagnostics.Options_0_and_1_cannot_be_combined.category, - code: Diagnostics.Options_0_and_1_cannot_be_combined.code, - file: undefined, - start: undefined, - length: undefined, - }], - projects: ["."], - buildOptions: { [flag1]: true, [flag2]: true }, - watchOptions: undefined, - }); - }); - } - - verifyInvalidCombination("clean", "force"); - verifyInvalidCombination("clean", "verbose"); - verifyInvalidCombination("clean", "watch"); - verifyInvalidCombination("watch", "dry"); - }); - - describe("Watch options", () => { - it("parse --watchFile", () => { - assertParseResult(["--watchFile", "UseFsEvents", "--verbose"], - { - errors: [], - projects: ["."], - buildOptions: { verbose: true }, - watchOptions: { watchFile: WatchFileKind.UseFsEvents } - }); + it("parse --watchDirectory", () => { + assertParseResult(["--watchDirectory", "FixedPollingInterval", "--verbose"], { + errors: [], + projects: ["."], + buildOptions: { verbose: true }, + watchOptions: { watchDirectory: WatchDirectoryKind.FixedPollingInterval } }); - - it("parse --watchDirectory", () => { - assertParseResult(["--watchDirectory", "FixedPollingInterval", "--verbose"], - { - errors: [], - projects: ["."], - buildOptions: { verbose: true }, - watchOptions: { watchDirectory: WatchDirectoryKind.FixedPollingInterval } - }); + }); + it("parse --fallbackPolling", () => { + assertParseResult(["--fallbackPolling", "PriorityInterval", "--verbose"], { + errors: [], + projects: ["."], + buildOptions: { verbose: true }, + watchOptions: { fallbackPolling: PollingWatchKind.PriorityInterval } }); - - it("parse --fallbackPolling", () => { - assertParseResult(["--fallbackPolling", "PriorityInterval", "--verbose"], - { - errors: [], - projects: ["."], - buildOptions: { verbose: true }, - watchOptions: { fallbackPolling: PollingWatchKind.PriorityInterval } - }); + }); + it("parse --synchronousWatchDirectory", () => { + assertParseResult(["--synchronousWatchDirectory", "--verbose"], { + errors: [], + projects: ["."], + buildOptions: { verbose: true }, + watchOptions: { synchronousWatchDirectory: true } }); - - it("parse --synchronousWatchDirectory", () => { - assertParseResult(["--synchronousWatchDirectory", "--verbose"], + }); + it("errors on missing argument", () => { + assertParseResult(["--verbose", "--fallbackPolling"], { + errors: [ { - errors: [], - projects: ["."], - buildOptions: { verbose: true }, - watchOptions: { synchronousWatchDirectory: true } - }); - }); - - it("errors on missing argument", () => { - assertParseResult(["--verbose", "--fallbackPolling"], + messageText: "Watch option 'fallbackPolling' requires a value of type string.", + category: Diagnostics.Watch_option_0_requires_a_value_of_type_1.category, + code: Diagnostics.Watch_option_0_requires_a_value_of_type_1.code, + file: undefined, + start: undefined, + length: undefined + }, { - errors: [ - { - messageText: "Watch option 'fallbackPolling' requires a value of type string.", - category: Diagnostics.Watch_option_0_requires_a_value_of_type_1.category, - code: Diagnostics.Watch_option_0_requires_a_value_of_type_1.code, - file: undefined, - start: undefined, - length: undefined - }, - { - messageText: "Argument for '--fallbackPolling' option must be: 'fixedinterval', 'priorityinterval', 'dynamicpriority'.", - category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category, - code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - file: undefined, - start: undefined, - length: undefined - } - ], - projects: ["."], - buildOptions: { verbose: true }, - watchOptions: { fallbackPolling: undefined } - }); + messageText: "Argument for '--fallbackPolling' option must be: 'fixedinterval', 'priorityinterval', 'dynamicpriority'.", + category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category, + code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + file: undefined, + start: undefined, + length: undefined + } + ], + projects: ["."], + buildOptions: { verbose: true }, + watchOptions: { fallbackPolling: undefined } }); }); }); -} +}); diff --git a/src/testRunner/unittests/config/configurationExtension.ts b/src/testRunner/unittests/config/configurationExtension.ts index ad906411ffbca..6c38a1c09df5d 100644 --- a/src/testRunner/unittests/config/configurationExtension.ts +++ b/src/testRunner/unittests/config/configurationExtension.ts @@ -1,354 +1,341 @@ -namespace ts { - function createFileSystem(ignoreCase: boolean, cwd: string, root: string) { - return new vfs.FileSystem(ignoreCase, { - cwd, - files: { - [root]: { - "dev/node_modules/config-box/package.json": JSON.stringify({ - name: "config-box", - version: "1.0.0", - tsconfig: "./strict.json" - }), - "dev/node_modules/config-box/strict.json": JSON.stringify({ - compilerOptions: { - strict: true, - } - }), - "dev/node_modules/config-box/unstrict.json": JSON.stringify({ - compilerOptions: { - strict: false, - } - }), - "dev/tsconfig.extendsBox.json": JSON.stringify({ - extends: "config-box", - files: [ - "main.ts", - ] - }), - "dev/tsconfig.extendsStrict.json": JSON.stringify({ - extends: "config-box/strict", - files: [ - "main.ts", - ] - }), - "dev/tsconfig.extendsUnStrict.json": JSON.stringify({ - extends: "config-box/unstrict", - files: [ - "main.ts", - ] - }), - "dev/tsconfig.extendsStrictExtension.json": JSON.stringify({ - extends: "config-box/strict.json", - files: [ - "main.ts", - ] - }), - "dev/node_modules/config-box-implied/package.json": JSON.stringify({ - name: "config-box-implied", - version: "1.0.0", - }), - "dev/node_modules/config-box-implied/tsconfig.json": JSON.stringify({ - compilerOptions: { - strict: true, - } - }), - "dev/node_modules/config-box-implied/unstrict/tsconfig.json": JSON.stringify({ - compilerOptions: { - strict: false, - } - }), - "dev/tsconfig.extendsBoxImplied.json": JSON.stringify({ - extends: "config-box-implied", - files: [ - "main.ts", - ] - }), - "dev/tsconfig.extendsBoxImpliedUnstrict.json": JSON.stringify({ - extends: "config-box-implied/unstrict", - files: [ - "main.ts", - ] - }), - "dev/tsconfig.extendsBoxImpliedUnstrictExtension.json": JSON.stringify({ - extends: "config-box-implied/unstrict/tsconfig", - files: [ - "main.ts", - ] - }), - "dev/tsconfig.extendsBoxImpliedPath.json": JSON.stringify({ - extends: "config-box-implied/tsconfig.json", - files: [ - "main.ts", - ] - }), - "dev/tsconfig.json": JSON.stringify({ - extends: "./configs/base", - files: [ - "main.ts", - "supplemental.ts" - ] - }), - "dev/tsconfig.nostrictnull.json": JSON.stringify({ - extends: "./tsconfig", - compilerOptions: { - strictNullChecks: false - } - }), - "dev/configs/base.json": JSON.stringify({ - compilerOptions: { - allowJs: true, - noImplicitAny: true, - strictNullChecks: true - } - }), - "dev/configs/tests.json": JSON.stringify({ - compilerOptions: { - preserveConstEnums: true, - removeComments: false, - sourceMap: true - }, - exclude: [ - "../tests/baselines", - "../tests/scenarios" - ], - include: [ - "../tests/**/*.ts" - ] - }), - "dev/circular.json": JSON.stringify({ - extends: "./circular2", - compilerOptions: { - module: "amd" - } - }), - "dev/circular2.json": JSON.stringify({ - extends: "./circular", - compilerOptions: { - module: "commonjs" - } - }), - "dev/missing.json": JSON.stringify({ - extends: "./missing2", - compilerOptions: { - types: [] - } - }), - "dev/failure.json": JSON.stringify({ - extends: "./failure2.json", - compilerOptions: { - typeRoots: [] - } - }), - "dev/failure2.json": JSON.stringify({ - excludes: ["*.js"] - }), - "dev/configs/first.json": JSON.stringify({ - extends: "./base", - compilerOptions: { - module: "commonjs" - }, - files: ["../main.ts"] - }), - "dev/configs/second.json": JSON.stringify({ - extends: "./base", - compilerOptions: { - module: "amd" - }, - include: ["../supplemental.*"] - }), - "dev/configs/third.json": JSON.stringify({ - extends: "./second", - compilerOptions: { - module: null // eslint-disable-line no-null/no-null - }, - include: ["../supplemental.*"] - }), - "dev/configs/fourth.json": JSON.stringify({ - extends: "./third", - compilerOptions: { - module: "system" - }, - include: null, // eslint-disable-line no-null/no-null - files: ["../main.ts"] - }), - "dev/configs/fifth.json": JSON.stringify({ - extends: "./fourth", - include: ["../tests/utils.ts"], - files: [] - }), - "dev/extends.json": JSON.stringify({ extends: 42 }), - "dev/extends2.json": JSON.stringify({ extends: "configs/base" }), - "dev/main.ts": "", - "dev/supplemental.ts": "", - "dev/tests/unit/spec.ts": "", - "dev/tests/utils.ts": "", - "dev/tests/scenarios/first.json": "", - "dev/tests/baselines/first/output.ts": "" - } +import { FileSystem } from "../../vfs"; +import { ParseConfigHost } from "../../fakes"; +import { Diagnostic, DiagnosticCategory, flattenDiagnosticMessageText, forEach, readConfigFile, parseJsonConfigFileContent, readJsonConfigFile, parseJsonSourceFileConfigFileContent, CompilerOptions, combinePaths, ModuleKind } from "../../ts"; +function createFileSystem(ignoreCase: boolean, cwd: string, root: string) { + return new FileSystem(ignoreCase, { + cwd, + files: { + [root]: { + "dev/node_modules/config-box/package.json": JSON.stringify({ + name: "config-box", + version: "1.0.0", + tsconfig: "./strict.json" + }), + "dev/node_modules/config-box/strict.json": JSON.stringify({ + compilerOptions: { + strict: true, + } + }), + "dev/node_modules/config-box/unstrict.json": JSON.stringify({ + compilerOptions: { + strict: false, + } + }), + "dev/tsconfig.extendsBox.json": JSON.stringify({ + extends: "config-box", + files: [ + "main.ts", + ] + }), + "dev/tsconfig.extendsStrict.json": JSON.stringify({ + extends: "config-box/strict", + files: [ + "main.ts", + ] + }), + "dev/tsconfig.extendsUnStrict.json": JSON.stringify({ + extends: "config-box/unstrict", + files: [ + "main.ts", + ] + }), + "dev/tsconfig.extendsStrictExtension.json": JSON.stringify({ + extends: "config-box/strict.json", + files: [ + "main.ts", + ] + }), + "dev/node_modules/config-box-implied/package.json": JSON.stringify({ + name: "config-box-implied", + version: "1.0.0", + }), + "dev/node_modules/config-box-implied/tsconfig.json": JSON.stringify({ + compilerOptions: { + strict: true, + } + }), + "dev/node_modules/config-box-implied/unstrict/tsconfig.json": JSON.stringify({ + compilerOptions: { + strict: false, + } + }), + "dev/tsconfig.extendsBoxImplied.json": JSON.stringify({ + extends: "config-box-implied", + files: [ + "main.ts", + ] + }), + "dev/tsconfig.extendsBoxImpliedUnstrict.json": JSON.stringify({ + extends: "config-box-implied/unstrict", + files: [ + "main.ts", + ] + }), + "dev/tsconfig.extendsBoxImpliedUnstrictExtension.json": JSON.stringify({ + extends: "config-box-implied/unstrict/tsconfig", + files: [ + "main.ts", + ] + }), + "dev/tsconfig.extendsBoxImpliedPath.json": JSON.stringify({ + extends: "config-box-implied/tsconfig.json", + files: [ + "main.ts", + ] + }), + "dev/tsconfig.json": JSON.stringify({ + extends: "./configs/base", + files: [ + "main.ts", + "supplemental.ts" + ] + }), + "dev/tsconfig.nostrictnull.json": JSON.stringify({ + extends: "./tsconfig", + compilerOptions: { + strictNullChecks: false + } + }), + "dev/configs/base.json": JSON.stringify({ + compilerOptions: { + allowJs: true, + noImplicitAny: true, + strictNullChecks: true + } + }), + "dev/configs/tests.json": JSON.stringify({ + compilerOptions: { + preserveConstEnums: true, + removeComments: false, + sourceMap: true + }, + exclude: [ + "../tests/baselines", + "../tests/scenarios" + ], + include: [ + "../tests/**/*.ts" + ] + }), + "dev/circular.json": JSON.stringify({ + extends: "./circular2", + compilerOptions: { + module: "amd" + } + }), + "dev/circular2.json": JSON.stringify({ + extends: "./circular", + compilerOptions: { + module: "commonjs" + } + }), + "dev/missing.json": JSON.stringify({ + extends: "./missing2", + compilerOptions: { + types: [] + } + }), + "dev/failure.json": JSON.stringify({ + extends: "./failure2.json", + compilerOptions: { + typeRoots: [] + } + }), + "dev/failure2.json": JSON.stringify({ + excludes: ["*.js"] + }), + "dev/configs/first.json": JSON.stringify({ + extends: "./base", + compilerOptions: { + module: "commonjs" + }, + files: ["../main.ts"] + }), + "dev/configs/second.json": JSON.stringify({ + extends: "./base", + compilerOptions: { + module: "amd" + }, + include: ["../supplemental.*"] + }), + "dev/configs/third.json": JSON.stringify({ + extends: "./second", + compilerOptions: { + module: null // eslint-disable-line no-null/no-null + }, + include: ["../supplemental.*"] + }), + "dev/configs/fourth.json": JSON.stringify({ + extends: "./third", + compilerOptions: { + module: "system" + }, + include: null, + files: ["../main.ts"] + }), + "dev/configs/fifth.json": JSON.stringify({ + extends: "./fourth", + include: ["../tests/utils.ts"], + files: [] + }), + "dev/extends.json": JSON.stringify({ extends: 42 }), + "dev/extends2.json": JSON.stringify({ extends: "configs/base" }), + "dev/main.ts": "", + "dev/supplemental.ts": "", + "dev/tests/unit/spec.ts": "", + "dev/tests/utils.ts": "", + "dev/tests/scenarios/first.json": "", + "dev/tests/baselines/first/output.ts": "" } - }); - } - - const caseInsensitiveBasePath = "c:/dev/"; - const caseInsensitiveHost = new fakes.ParseConfigHost(createFileSystem(/*ignoreCase*/ true, caseInsensitiveBasePath, "c:/")); - - const caseSensitiveBasePath = "/dev/"; - const caseSensitiveHost = new fakes.ParseConfigHost(createFileSystem(/*ignoreCase*/ false, caseSensitiveBasePath, "/")); - - function verifyDiagnostics(actual: Diagnostic[], expected: { code: number; messageText: string; }[]) { - assert.isTrue(expected.length === actual.length, `Expected error: ${JSON.stringify(expected)}. Actual error: ${JSON.stringify(actual)}.`); - for (let i = 0; i < actual.length; i++) { - const actualError = actual[i]; - const expectedError = expected[i]; - assert.equal(actualError.code, expectedError.code, "Error code mismatch"); - assert.equal(actualError.category, DiagnosticCategory.Error, "Category mismatch"); // Should always be error - assert.equal(flattenDiagnosticMessageText(actualError.messageText, "\n"), expectedError.messageText); } + }); +} +const caseInsensitiveBasePath = "c:/dev/"; +const caseInsensitiveHost = new ParseConfigHost(createFileSystem(/*ignoreCase*/ true, caseInsensitiveBasePath, "c:/")); +const caseSensitiveBasePath = "/dev/"; +const caseSensitiveHost = new ParseConfigHost(createFileSystem(/*ignoreCase*/ false, caseSensitiveBasePath, "/")); +function verifyDiagnostics(actual: Diagnostic[], expected: { + code: number; + messageText: string; +}[]) { + assert.isTrue(expected.length === actual.length, `Expected error: ${JSON.stringify(expected)}. Actual error: ${JSON.stringify(actual)}.`); + for (let i = 0; i < actual.length; i++) { + const actualError = actual[i]; + const expectedError = expected[i]; + assert.equal(actualError.code, expectedError.code, "Error code mismatch"); + assert.equal(actualError.category, DiagnosticCategory.Error, "Category mismatch"); // Should always be error + assert.equal(flattenDiagnosticMessageText(actualError.messageText, "\n"), expectedError.messageText); } - - describe("unittests:: config:: configurationExtension", () => { - forEach<[string, string, fakes.ParseConfigHost], void>([ - ["under a case insensitive host", caseInsensitiveBasePath, caseInsensitiveHost], - ["under a case sensitive host", caseSensitiveBasePath, caseSensitiveHost] - ], ([testName, basePath, host]) => { - function getParseCommandLine(entry: string) { - const {config, error} = readConfigFile(entry, name => host.readFile(name)); - assert(config && !error, flattenDiagnosticMessageText(error && error.messageText, "\n")); - return parseJsonConfigFileContent(config, host, basePath, {}, entry); - } - - function getParseCommandLineJsonSourceFile(entry: string) { - const jsonSourceFile = readJsonConfigFile(entry, name => host.readFile(name)); - assert(jsonSourceFile.endOfFileToken && !jsonSourceFile.parseDiagnostics.length, flattenDiagnosticMessageText(jsonSourceFile.parseDiagnostics[0] && jsonSourceFile.parseDiagnostics[0].messageText, "\n")); - return { - jsonSourceFile, - parsed: parseJsonSourceFileConfigFileContent(jsonSourceFile, host, basePath, {}, entry) - }; - } - - function testSuccess(name: string, entry: string, expected: CompilerOptions, expectedFiles: string[]) { - expected.configFilePath = entry; - it(name, () => { - const parsed = getParseCommandLine(entry); - assert(!parsed.errors.length, flattenDiagnosticMessageText(parsed.errors[0] && parsed.errors[0].messageText, "\n")); - assert.deepEqual(parsed.options, expected); - assert.deepEqual(parsed.fileNames, expectedFiles); - }); - - it(name + " with jsonSourceFile", () => { - const { parsed, jsonSourceFile } = getParseCommandLineJsonSourceFile(entry); - assert(!parsed.errors.length, flattenDiagnosticMessageText(parsed.errors[0] && parsed.errors[0].messageText, "\n")); - assert.deepEqual(parsed.options, expected); - assert.equal(parsed.options.configFile, jsonSourceFile); - assert.deepEqual(parsed.fileNames, expectedFiles); - }); - } - - function testFailure(name: string, entry: string, expectedDiagnostics: { code: number; messageText: string; }[]) { - it(name, () => { - const parsed = getParseCommandLine(entry); - verifyDiagnostics(parsed.errors, expectedDiagnostics); - }); - - it(name + " with jsonSourceFile", () => { - const { parsed } = getParseCommandLineJsonSourceFile(entry); - verifyDiagnostics(parsed.errors, expectedDiagnostics); - }); - } - - describe(testName, () => { - testSuccess("can resolve an extension with a base extension", "tsconfig.json", { - allowJs: true, - noImplicitAny: true, - strictNullChecks: true, - }, [ - combinePaths(basePath, "main.ts"), - combinePaths(basePath, "supplemental.ts"), - ]); - - testSuccess("can resolve an extension with a base extension that overrides options", "tsconfig.nostrictnull.json", { - allowJs: true, - noImplicitAny: true, - strictNullChecks: false, - }, [ - combinePaths(basePath, "main.ts"), - combinePaths(basePath, "supplemental.ts"), - ]); - - testFailure("can report errors on circular imports", "circular.json", [ - { - code: 18000, - messageText: `Circularity detected while resolving configuration: ${[combinePaths(basePath, "circular.json"), combinePaths(basePath, "circular2.json"), combinePaths(basePath, "circular.json")].join(" -> ")}` - } - ]); - - testFailure("can report missing configurations", "missing.json", [{ +} +describe("unittests:: config:: configurationExtension", () => { + forEach<[string, string, ParseConfigHost], void>([ + ["under a case insensitive host", caseInsensitiveBasePath, caseInsensitiveHost], + ["under a case sensitive host", caseSensitiveBasePath, caseSensitiveHost] + ], ([testName, basePath, host]) => { + function getParseCommandLine(entry: string) { + const { config, error } = readConfigFile(entry, name => host.readFile(name)); + assert(config && !error, flattenDiagnosticMessageText(error && error.messageText, "\n")); + return parseJsonConfigFileContent(config, host, basePath, {}, entry); + } + function getParseCommandLineJsonSourceFile(entry: string) { + const jsonSourceFile = readJsonConfigFile(entry, name => host.readFile(name)); + assert(jsonSourceFile.endOfFileToken && !jsonSourceFile.parseDiagnostics.length, flattenDiagnosticMessageText(jsonSourceFile.parseDiagnostics[0] && jsonSourceFile.parseDiagnostics[0].messageText, "\n")); + return { + jsonSourceFile, + parsed: parseJsonSourceFileConfigFileContent(jsonSourceFile, host, basePath, {}, entry) + }; + } + function testSuccess(name: string, entry: string, expected: CompilerOptions, expectedFiles: string[]) { + expected.configFilePath = entry; + it(name, () => { + const parsed = getParseCommandLine(entry); + assert(!parsed.errors.length, flattenDiagnosticMessageText(parsed.errors[0] && parsed.errors[0].messageText, "\n")); + assert.deepEqual(parsed.options, expected); + assert.deepEqual(parsed.fileNames, expectedFiles); + }); + it(name + " with jsonSourceFile", () => { + const { parsed, jsonSourceFile } = getParseCommandLineJsonSourceFile(entry); + assert(!parsed.errors.length, flattenDiagnosticMessageText(parsed.errors[0] && parsed.errors[0].messageText, "\n")); + assert.deepEqual(parsed.options, expected); + assert.equal(parsed.options.configFile, jsonSourceFile); + assert.deepEqual(parsed.fileNames, expectedFiles); + }); + } + function testFailure(name: string, entry: string, expectedDiagnostics: { + code: number; + messageText: string; + }[]) { + it(name, () => { + const parsed = getParseCommandLine(entry); + verifyDiagnostics(parsed.errors, expectedDiagnostics); + }); + it(name + " with jsonSourceFile", () => { + const { parsed } = getParseCommandLineJsonSourceFile(entry); + verifyDiagnostics(parsed.errors, expectedDiagnostics); + }); + } + describe(testName, () => { + testSuccess("can resolve an extension with a base extension", "tsconfig.json", { + allowJs: true, + noImplicitAny: true, + strictNullChecks: true, + }, [ + combinePaths(basePath, "main.ts"), + combinePaths(basePath, "supplemental.ts"), + ]); + testSuccess("can resolve an extension with a base extension that overrides options", "tsconfig.nostrictnull.json", { + allowJs: true, + noImplicitAny: true, + strictNullChecks: false, + }, [ + combinePaths(basePath, "main.ts"), + combinePaths(basePath, "supplemental.ts"), + ]); + testFailure("can report errors on circular imports", "circular.json", [ + { + code: 18000, + messageText: `Circularity detected while resolving configuration: ${[combinePaths(basePath, "circular.json"), combinePaths(basePath, "circular2.json"), combinePaths(basePath, "circular.json")].join(" -> ")}` + } + ]); + testFailure("can report missing configurations", "missing.json", [{ code: 6053, messageText: `File './missing2' not found.` }]); - - testFailure("can report errors in extended configs", "failure.json", [{ + testFailure("can report errors in extended configs", "failure.json", [{ code: 6114, messageText: `Unknown option 'excludes'. Did you mean 'exclude'?` }]); - - testFailure("can error when 'extends' is not a string", "extends.json", [{ + testFailure("can error when 'extends' is not a string", "extends.json", [{ code: 5024, messageText: `Compiler option 'extends' requires a value of type string.` }]); - - testSuccess("can overwrite compiler options using extended 'null'", "configs/third.json", { - allowJs: true, - noImplicitAny: true, - strictNullChecks: true, - module: undefined // Technically, this is distinct from the key never being set; but within the compiler we don't make the distinction - }, [ - combinePaths(basePath, "supplemental.ts") - ]); - - testSuccess("can overwrite top-level options using extended 'null'", "configs/fourth.json", { - allowJs: true, - noImplicitAny: true, - strictNullChecks: true, - module: ModuleKind.System - }, [ - combinePaths(basePath, "main.ts") - ]); - - testSuccess("can overwrite top-level files using extended []", "configs/fifth.json", { - allowJs: true, - noImplicitAny: true, - strictNullChecks: true, - module: ModuleKind.System - }, [ - combinePaths(basePath, "tests/utils.ts") - ]); - - describe("finding extended configs from node_modules", () => { - testSuccess("can lookup via tsconfig field", "tsconfig.extendsBox.json", { strict: true }, [combinePaths(basePath, "main.ts")]); - testSuccess("can lookup via package-relative path", "tsconfig.extendsStrict.json", { strict: true }, [combinePaths(basePath, "main.ts")]); - testSuccess("can lookup via non-redirected-to package-relative path", "tsconfig.extendsUnStrict.json", { strict: false }, [combinePaths(basePath, "main.ts")]); - testSuccess("can lookup via package-relative path with extension", "tsconfig.extendsStrictExtension.json", { strict: true }, [combinePaths(basePath, "main.ts")]); - testSuccess("can lookup via an implicit tsconfig", "tsconfig.extendsBoxImplied.json", { strict: true }, [combinePaths(basePath, "main.ts")]); - testSuccess("can lookup via an implicit tsconfig in a package-relative directory", "tsconfig.extendsBoxImpliedUnstrict.json", { strict: false }, [combinePaths(basePath, "main.ts")]); - testSuccess("can lookup via an implicit tsconfig in a package-relative directory with name", "tsconfig.extendsBoxImpliedUnstrictExtension.json", { strict: false }, [combinePaths(basePath, "main.ts")]); - testSuccess("can lookup via an implicit tsconfig in a package-relative directory with extension", "tsconfig.extendsBoxImpliedPath.json", { strict: true }, [combinePaths(basePath, "main.ts")]); - }); - - it("adds extendedSourceFiles only once", () => { - const sourceFile = readJsonConfigFile("configs/fourth.json", (path) => host.readFile(path)); - const dir = combinePaths(basePath, "configs"); - const expected = [ - combinePaths(dir, "third.json"), - combinePaths(dir, "second.json"), - combinePaths(dir, "base.json"), - ]; - parseJsonSourceFileConfigFileContent(sourceFile, host, dir, {}, "fourth.json"); - assert.deepEqual(sourceFile.extendedSourceFiles, expected); - parseJsonSourceFileConfigFileContent(sourceFile, host, dir, {}, "fourth.json"); - assert.deepEqual(sourceFile.extendedSourceFiles, expected); - }); + testSuccess("can overwrite compiler options using extended 'null'", "configs/third.json", { + allowJs: true, + noImplicitAny: true, + strictNullChecks: true, + module: undefined // Technically, this is distinct from the key never being set; but within the compiler we don't make the distinction + }, [ + combinePaths(basePath, "supplemental.ts") + ]); + testSuccess("can overwrite top-level options using extended 'null'", "configs/fourth.json", { + allowJs: true, + noImplicitAny: true, + strictNullChecks: true, + module: ModuleKind.System + }, [ + combinePaths(basePath, "main.ts") + ]); + testSuccess("can overwrite top-level files using extended []", "configs/fifth.json", { + allowJs: true, + noImplicitAny: true, + strictNullChecks: true, + module: ModuleKind.System + }, [ + combinePaths(basePath, "tests/utils.ts") + ]); + describe("finding extended configs from node_modules", () => { + testSuccess("can lookup via tsconfig field", "tsconfig.extendsBox.json", { strict: true }, [combinePaths(basePath, "main.ts")]); + testSuccess("can lookup via package-relative path", "tsconfig.extendsStrict.json", { strict: true }, [combinePaths(basePath, "main.ts")]); + testSuccess("can lookup via non-redirected-to package-relative path", "tsconfig.extendsUnStrict.json", { strict: false }, [combinePaths(basePath, "main.ts")]); + testSuccess("can lookup via package-relative path with extension", "tsconfig.extendsStrictExtension.json", { strict: true }, [combinePaths(basePath, "main.ts")]); + testSuccess("can lookup via an implicit tsconfig", "tsconfig.extendsBoxImplied.json", { strict: true }, [combinePaths(basePath, "main.ts")]); + testSuccess("can lookup via an implicit tsconfig in a package-relative directory", "tsconfig.extendsBoxImpliedUnstrict.json", { strict: false }, [combinePaths(basePath, "main.ts")]); + testSuccess("can lookup via an implicit tsconfig in a package-relative directory with name", "tsconfig.extendsBoxImpliedUnstrictExtension.json", { strict: false }, [combinePaths(basePath, "main.ts")]); + testSuccess("can lookup via an implicit tsconfig in a package-relative directory with extension", "tsconfig.extendsBoxImpliedPath.json", { strict: true }, [combinePaths(basePath, "main.ts")]); + }); + it("adds extendedSourceFiles only once", () => { + const sourceFile = readJsonConfigFile("configs/fourth.json", (path) => host.readFile(path)); + const dir = combinePaths(basePath, "configs"); + const expected = [ + combinePaths(dir, "third.json"), + combinePaths(dir, "second.json"), + combinePaths(dir, "base.json"), + ]; + parseJsonSourceFileConfigFileContent(sourceFile, host, dir, {}, "fourth.json"); + assert.deepEqual(sourceFile.extendedSourceFiles, expected); + parseJsonSourceFileConfigFileContent(sourceFile, host, dir, {}, "fourth.json"); + assert.deepEqual(sourceFile.extendedSourceFiles, expected); }); }); }); -} +}); diff --git a/src/testRunner/unittests/config/convertCompilerOptionsFromJson.ts b/src/testRunner/unittests/config/convertCompilerOptionsFromJson.ts index ffc972d8c4f83..b6f0f07f13b29 100644 --- a/src/testRunner/unittests/config/convertCompilerOptionsFromJson.ts +++ b/src/testRunner/unittests/config/convertCompilerOptionsFromJson.ts @@ -1,579 +1,489 @@ -namespace ts { - describe("unittests:: config:: convertCompilerOptionsFromJson", () => { - const formatDiagnosticHost: FormatDiagnosticsHost = { - getCurrentDirectory: () => "/apath/", - getCanonicalFileName: createGetCanonicalFileName(/*useCaseSensitiveFileNames*/ true), - getNewLine: () => "\n" - }; - - interface ExpectedResultWithParsingSuccess { - compilerOptions: CompilerOptions; - errors: readonly Diagnostic[]; +import { FormatDiagnosticsHost, createGetCanonicalFileName, CompilerOptions, Diagnostic, convertCompilerOptionsFromJson, parseJsonText, ParseConfigHost, parseJsonSourceFileConfigFileContent, Diagnostics, formatDiagnostic, ModuleKind, ScriptTarget } from "../../ts"; +import { FileSystem } from "../../vfs"; +import * as fakes from "../../fakes"; +describe("unittests:: config:: convertCompilerOptionsFromJson", () => { + const formatDiagnosticHost: FormatDiagnosticsHost = { + getCurrentDirectory: () => "/apath/", + getCanonicalFileName: createGetCanonicalFileName(/*useCaseSensitiveFileNames*/ true), + getNewLine: () => "\n" + }; + interface ExpectedResultWithParsingSuccess { + compilerOptions: CompilerOptions; + errors: readonly Diagnostic[]; + } + interface ExpectedResultWithParsingFailure { + compilerOptions: CompilerOptions; + hasParseErrors: true; + } + type ExpectedResult = ExpectedResultWithParsingSuccess | ExpectedResultWithParsingFailure; + function isExpectedResultWithParsingFailure(expectedResult: ExpectedResult): expectedResult is ExpectedResultWithParsingFailure { + return !!(expectedResult as ExpectedResultWithParsingFailure).hasParseErrors; + } + function assertCompilerOptions(json: any, configFileName: string, expectedResult: ExpectedResultWithParsingSuccess) { + assertCompilerOptionsWithJson(json, configFileName, expectedResult); + assertCompilerOptionsWithJsonNode(json, configFileName, expectedResult); + } + function assertCompilerOptionsWithJson(json: any, configFileName: string, expectedResult: ExpectedResultWithParsingSuccess) { + const { options: actualCompilerOptions, errors: actualErrors } = convertCompilerOptionsFromJson(json.compilerOptions, "/apath/", configFileName); + const parsedCompilerOptions = JSON.stringify(actualCompilerOptions); + const expectedCompilerOptions = JSON.stringify({ ...expectedResult.compilerOptions, configFilePath: configFileName }); + assert.equal(parsedCompilerOptions, expectedCompilerOptions); + verifyErrors(actualErrors, expectedResult.errors, /*ignoreLocation*/ true); + } + function assertCompilerOptionsWithJsonNode(json: any, configFileName: string, expectedResult: ExpectedResultWithParsingSuccess) { + assertCompilerOptionsWithJsonText(JSON.stringify(json), configFileName, expectedResult); + } + function assertCompilerOptionsWithJsonText(fileText: string, configFileName: string, expectedResult: ExpectedResult) { + const result = parseJsonText(configFileName, fileText); + assert(!!result.endOfFileToken); + assert.equal(!!result.parseDiagnostics.length, isExpectedResultWithParsingFailure(expectedResult)); + const host: ParseConfigHost = new fakes.ParseConfigHost(new FileSystem(/*ignoreCase*/ false, { cwd: "/apath/" })); + const { options: actualCompilerOptions, errors: actualParseErrors } = parseJsonSourceFileConfigFileContent(result, host, "/apath/", /*existingOptions*/ undefined, configFileName); + expectedResult.compilerOptions.configFilePath = configFileName; + const parsedCompilerOptions = JSON.stringify(actualCompilerOptions); + const expectedCompilerOptions = JSON.stringify(expectedResult.compilerOptions); + assert.equal(parsedCompilerOptions, expectedCompilerOptions); + assert.equal(actualCompilerOptions.configFile, result); + if (!isExpectedResultWithParsingFailure(expectedResult)) { + verifyErrors(actualParseErrors.filter(error => error.code !== Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code), expectedResult.errors); } - - interface ExpectedResultWithParsingFailure { - compilerOptions: CompilerOptions; - hasParseErrors: true; - } - - type ExpectedResult = ExpectedResultWithParsingSuccess | ExpectedResultWithParsingFailure; - - function isExpectedResultWithParsingFailure(expectedResult: ExpectedResult): expectedResult is ExpectedResultWithParsingFailure { - return !!(expectedResult as ExpectedResultWithParsingFailure).hasParseErrors; - } - - function assertCompilerOptions(json: any, configFileName: string, expectedResult: ExpectedResultWithParsingSuccess) { - assertCompilerOptionsWithJson(json, configFileName, expectedResult); - assertCompilerOptionsWithJsonNode(json, configFileName, expectedResult); - } - - function assertCompilerOptionsWithJson(json: any, configFileName: string, expectedResult: ExpectedResultWithParsingSuccess) { - const { options: actualCompilerOptions, errors: actualErrors } = convertCompilerOptionsFromJson(json.compilerOptions, "/apath/", configFileName); - - const parsedCompilerOptions = JSON.stringify(actualCompilerOptions); - const expectedCompilerOptions = JSON.stringify({ ...expectedResult.compilerOptions, configFilePath: configFileName }); - assert.equal(parsedCompilerOptions, expectedCompilerOptions); - - verifyErrors(actualErrors, expectedResult.errors, /*ignoreLocation*/ true); - } - - function assertCompilerOptionsWithJsonNode(json: any, configFileName: string, expectedResult: ExpectedResultWithParsingSuccess) { - assertCompilerOptionsWithJsonText(JSON.stringify(json), configFileName, expectedResult); - } - - function assertCompilerOptionsWithJsonText(fileText: string, configFileName: string, expectedResult: ExpectedResult) { - const result = parseJsonText(configFileName, fileText); - assert(!!result.endOfFileToken); - assert.equal(!!result.parseDiagnostics.length, isExpectedResultWithParsingFailure(expectedResult)); - const host: ParseConfigHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ false, { cwd: "/apath/" })); - const { options: actualCompilerOptions, errors: actualParseErrors } = parseJsonSourceFileConfigFileContent(result, host, "/apath/", /*existingOptions*/ undefined, configFileName); - expectedResult.compilerOptions.configFilePath = configFileName; - - const parsedCompilerOptions = JSON.stringify(actualCompilerOptions); - const expectedCompilerOptions = JSON.stringify(expectedResult.compilerOptions); - assert.equal(parsedCompilerOptions, expectedCompilerOptions); - assert.equal(actualCompilerOptions.configFile, result); - - if (!isExpectedResultWithParsingFailure(expectedResult)) { - verifyErrors(actualParseErrors.filter(error => error.code !== Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code), expectedResult.errors); + } + function verifyErrors(actualErrors: Diagnostic[], expectedErrors: readonly Diagnostic[], ignoreLocation?: boolean) { + assert.isTrue(expectedErrors.length === actualErrors.length, `Expected error: ${JSON.stringify(expectedErrors.map(getDiagnosticString), undefined, " ")}. Actual error: ${JSON.stringify(actualErrors.map(getDiagnosticString), undefined, " ")}.`); + for (let i = 0; i < actualErrors.length; i++) { + const actualError = actualErrors[i]; + const expectedError = expectedErrors[i]; + assert.equal(actualError.code, expectedError.code, `Expected error-code: ${JSON.stringify(expectedError.code)}. Actual error-code: ${JSON.stringify(actualError.code)}.`); + assert.equal(actualError.category, expectedError.category, `Expected error-category: ${JSON.stringify(expectedError.category)}. Actual error-category: ${JSON.stringify(actualError.category)}.`); + if (!ignoreLocation) { + assert(actualError.file); + assert(actualError.start); + assert(actualError.length); } } - - function verifyErrors(actualErrors: Diagnostic[], expectedErrors: readonly Diagnostic[], ignoreLocation?: boolean) { - assert.isTrue(expectedErrors.length === actualErrors.length, `Expected error: ${JSON.stringify(expectedErrors.map(getDiagnosticString), undefined, " ")}. Actual error: ${JSON.stringify(actualErrors.map(getDiagnosticString), undefined, " ")}.`); - for (let i = 0; i < actualErrors.length; i++) { - const actualError = actualErrors[i]; - const expectedError = expectedErrors[i]; - - assert.equal(actualError.code, expectedError.code, `Expected error-code: ${JSON.stringify(expectedError.code)}. Actual error-code: ${JSON.stringify(actualError.code)}.`); - assert.equal(actualError.category, expectedError.category, `Expected error-category: ${JSON.stringify(expectedError.category)}. Actual error-category: ${JSON.stringify(actualError.category)}.`); - if (!ignoreLocation) { - assert(actualError.file); - assert(actualError.start); - assert(actualError.length); - } - } - - function getDiagnosticString(diagnostic: Diagnostic) { - if (ignoreLocation) { - const { file, ...rest } = diagnostic; - diagnostic = { file: undefined, ...rest }; - } - return formatDiagnostic(diagnostic, formatDiagnosticHost); + function getDiagnosticString(diagnostic: Diagnostic) { + if (ignoreLocation) { + const { file, ...rest } = diagnostic; + diagnostic = { file: undefined, ...rest }; } + return formatDiagnostic(diagnostic, formatDiagnosticHost); } - - // tsconfig.json tests - it("Convert correctly format tsconfig.json to compiler-options ", () => { - assertCompilerOptions( - { - compilerOptions: { - module: "commonjs", - target: "es5", - noImplicitAny: false, - sourceMap: false, - lib: ["es5", "es2015.core", "es2015.symbol"] - } - }, "tsconfig.json", - { - compilerOptions: { - module: ModuleKind.CommonJS, - target: ScriptTarget.ES5, - noImplicitAny: false, - sourceMap: false, - lib: ["lib.es5.d.ts", "lib.es2015.core.d.ts", "lib.es2015.symbol.d.ts"] - }, - errors: [] - } - ); + } + // tsconfig.json tests + it("Convert correctly format tsconfig.json to compiler-options ", () => { + assertCompilerOptions({ + compilerOptions: { + module: "commonjs", + target: "es5", + noImplicitAny: false, + sourceMap: false, + lib: ["es5", "es2015.core", "es2015.symbol"] + } + }, "tsconfig.json", { + compilerOptions: { + module: ModuleKind.CommonJS, + target: ScriptTarget.ES5, + noImplicitAny: false, + sourceMap: false, + lib: ["lib.es5.d.ts", "lib.es2015.core.d.ts", "lib.es2015.symbol.d.ts"] + }, + errors: [] }); - - it("Convert correctly format tsconfig.json with allowJs is false to compiler-options ", () => { - assertCompilerOptions( - { - compilerOptions: { - module: "commonjs", - target: "es5", - noImplicitAny: false, - sourceMap: false, - allowJs: false, - lib: ["es5", "es2015.core", "es2015.symbol"] - } - }, "tsconfig.json", - { - compilerOptions: { - module: ModuleKind.CommonJS, - target: ScriptTarget.ES5, - noImplicitAny: false, - sourceMap: false, - allowJs: false, - lib: ["lib.es5.d.ts", "lib.es2015.core.d.ts", "lib.es2015.symbol.d.ts"] - }, - errors: [] - } - ); + }); + it("Convert correctly format tsconfig.json with allowJs is false to compiler-options ", () => { + assertCompilerOptions({ + compilerOptions: { + module: "commonjs", + target: "es5", + noImplicitAny: false, + sourceMap: false, + allowJs: false, + lib: ["es5", "es2015.core", "es2015.symbol"] + } + }, "tsconfig.json", { + compilerOptions: { + module: ModuleKind.CommonJS, + target: ScriptTarget.ES5, + noImplicitAny: false, + sourceMap: false, + allowJs: false, + lib: ["lib.es5.d.ts", "lib.es2015.core.d.ts", "lib.es2015.symbol.d.ts"] + }, + errors: [] }); - - it("Convert incorrect option of jsx to compiler-options ", () => { - assertCompilerOptions( - { - compilerOptions: { - module: "commonjs", - target: "es5", - noImplicitAny: false, - sourceMap: false, - jsx: "" - } - }, "tsconfig.json", - { - compilerOptions: { - module: ModuleKind.CommonJS, - target: ScriptTarget.ES5, - noImplicitAny: false, - sourceMap: false, - }, - errors: [{ - file: undefined, - start: 0, - length: 0, - messageText: "Argument for '--jsx' option must be: 'preserve', 'react-native', 'react'.", - code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category - }] - } - ); + }); + it("Convert incorrect option of jsx to compiler-options ", () => { + assertCompilerOptions({ + compilerOptions: { + module: "commonjs", + target: "es5", + noImplicitAny: false, + sourceMap: false, + jsx: "" + } + }, "tsconfig.json", { + compilerOptions: { + module: ModuleKind.CommonJS, + target: ScriptTarget.ES5, + noImplicitAny: false, + sourceMap: false, + }, + errors: [{ + file: undefined, + start: 0, + length: 0, + messageText: "Argument for '--jsx' option must be: 'preserve', 'react-native', 'react'.", + code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category + }] }); - - it("Convert incorrect option of module to compiler-options ", () => { - assertCompilerOptions( - { - compilerOptions: { - module: "", - target: "es5", - noImplicitAny: false, - sourceMap: false, - } - }, "tsconfig.json", - { - compilerOptions: { - target: ScriptTarget.ES5, - noImplicitAny: false, - sourceMap: false, - }, - errors: [{ - file: undefined, - start: 0, - length: 0, - messageText: "Argument for '--module' option must be: 'none', 'commonjs', 'amd', 'system', 'umd', 'es6', 'es2015', 'esnext'.", - code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category - }] - } - ); + }); + it("Convert incorrect option of module to compiler-options ", () => { + assertCompilerOptions({ + compilerOptions: { + module: "", + target: "es5", + noImplicitAny: false, + sourceMap: false, + } + }, "tsconfig.json", { + compilerOptions: { + target: ScriptTarget.ES5, + noImplicitAny: false, + sourceMap: false, + }, + errors: [{ + file: undefined, + start: 0, + length: 0, + messageText: "Argument for '--module' option must be: 'none', 'commonjs', 'amd', 'system', 'umd', 'es6', 'es2015', 'esnext'.", + code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category + }] }); - - it("Convert incorrect option of newLine to compiler-options ", () => { - assertCompilerOptions( - { - compilerOptions: { - newLine: "", - target: "es5", - noImplicitAny: false, - sourceMap: false, - } - }, "tsconfig.json", - { - compilerOptions: { - target: ScriptTarget.ES5, - noImplicitAny: false, - sourceMap: false, - }, - errors: [{ - file: undefined, - start: 0, - length: 0, - messageText: "Argument for '--newLine' option must be: 'crlf', 'lf'.", - code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category - }] - } - ); + }); + it("Convert incorrect option of newLine to compiler-options ", () => { + assertCompilerOptions({ + compilerOptions: { + newLine: "", + target: "es5", + noImplicitAny: false, + sourceMap: false, + } + }, "tsconfig.json", { + compilerOptions: { + target: ScriptTarget.ES5, + noImplicitAny: false, + sourceMap: false, + }, + errors: [{ + file: undefined, + start: 0, + length: 0, + messageText: "Argument for '--newLine' option must be: 'crlf', 'lf'.", + code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category + }] }); - - it("Convert incorrect option of target to compiler-options ", () => { - assertCompilerOptions( - { - compilerOptions: { - target: "", - noImplicitAny: false, - sourceMap: false, - } - }, "tsconfig.json", - { - compilerOptions: { - noImplicitAny: false, - sourceMap: false, - }, - errors: [{ - file: undefined, - start: 0, - length: 0, - messageText: "Argument for '--target' option must be: 'es3', 'es5', 'es6', 'es2015', 'es2016', 'es2017', 'es2018', 'es2019', 'esnext'.", - code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category - }] - } - ); + }); + it("Convert incorrect option of target to compiler-options ", () => { + assertCompilerOptions({ + compilerOptions: { + target: "", + noImplicitAny: false, + sourceMap: false, + } + }, "tsconfig.json", { + compilerOptions: { + noImplicitAny: false, + sourceMap: false, + }, + errors: [{ + file: undefined, + start: 0, + length: 0, + messageText: "Argument for '--target' option must be: 'es3', 'es5', 'es6', 'es2015', 'es2016', 'es2017', 'es2018', 'es2019', 'esnext'.", + code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category + }] }); - - it("Convert incorrect option of module-resolution to compiler-options ", () => { - assertCompilerOptions( - { - compilerOptions: { - moduleResolution: "", - noImplicitAny: false, - sourceMap: false, - } - }, "tsconfig.json", - { - compilerOptions: { - noImplicitAny: false, - sourceMap: false, - }, - errors: [{ - file: undefined, - start: 0, - length: 0, - messageText: "Argument for '--moduleResolution' option must be: 'node', 'classic'.", - code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category - }] - } - ); + }); + it("Convert incorrect option of module-resolution to compiler-options ", () => { + assertCompilerOptions({ + compilerOptions: { + moduleResolution: "", + noImplicitAny: false, + sourceMap: false, + } + }, "tsconfig.json", { + compilerOptions: { + noImplicitAny: false, + sourceMap: false, + }, + errors: [{ + file: undefined, + start: 0, + length: 0, + messageText: "Argument for '--moduleResolution' option must be: 'node', 'classic'.", + code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category + }] }); - - it("Convert incorrect option of libs to compiler-options ", () => { - assertCompilerOptions( - { - compilerOptions: { - module: "commonjs", - target: "es5", - noImplicitAny: false, - sourceMap: false, - lib: ["es5", "es2015.core", "incorrectLib"] - } - }, "tsconfig.json", - { - compilerOptions: { - module: ModuleKind.CommonJS, - target: ScriptTarget.ES5, - noImplicitAny: false, - sourceMap: false, - lib: ["lib.es5.d.ts", "lib.es2015.core.d.ts"] - }, - errors: [{ - file: undefined, - start: 0, - length: 0, - messageText: "Argument for '--lib' option must be: 'es5', 'es6', 'es2015', 'es7', 'es2016', 'es2017', 'es2018', 'esnext', 'dom', 'dom.iterable', 'webworker', 'webworker.importscripts', 'scripthost', 'es2015.core', 'es2015.collection', 'es2015.generator', 'es2015.iterable', 'es2015.promise', 'es2015.proxy', 'es2015.reflect', 'es2015.symbol', 'es2015.symbol.wellknown', 'es2016.array.include', 'es2017.object', 'es2017.sharedmemory', 'es2017.string', 'es2017.intl', 'es2017.typedarrays', 'es2018.asynciterable', 'es2018.intl', 'es2018.promise', 'es2018.regexp', 'esnext.array', 'esnext.symbol', 'esnext.intl', 'esnext.bigint'.", - code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category - }] - } - ); + }); + it("Convert incorrect option of libs to compiler-options ", () => { + assertCompilerOptions({ + compilerOptions: { + module: "commonjs", + target: "es5", + noImplicitAny: false, + sourceMap: false, + lib: ["es5", "es2015.core", "incorrectLib"] + } + }, "tsconfig.json", { + compilerOptions: { + module: ModuleKind.CommonJS, + target: ScriptTarget.ES5, + noImplicitAny: false, + sourceMap: false, + lib: ["lib.es5.d.ts", "lib.es2015.core.d.ts"] + }, + errors: [{ + file: undefined, + start: 0, + length: 0, + messageText: "Argument for '--lib' option must be: 'es5', 'es6', 'es2015', 'es7', 'es2016', 'es2017', 'es2018', 'esnext', 'dom', 'dom.iterable', 'webworker', 'webworker.importscripts', 'scripthost', 'es2015.core', 'es2015.collection', 'es2015.generator', 'es2015.iterable', 'es2015.promise', 'es2015.proxy', 'es2015.reflect', 'es2015.symbol', 'es2015.symbol.wellknown', 'es2016.array.include', 'es2017.object', 'es2017.sharedmemory', 'es2017.string', 'es2017.intl', 'es2017.typedarrays', 'es2018.asynciterable', 'es2018.intl', 'es2018.promise', 'es2018.regexp', 'esnext.array', 'esnext.symbol', 'esnext.intl', 'esnext.bigint'.", + code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category + }] }); - - it("Convert empty string option of libs to compiler-options ", () => { - assertCompilerOptions( - { - compilerOptions: { - module: "commonjs", - target: "es5", - noImplicitAny: false, - sourceMap: false, - lib: ["es5", ""] - } - }, "tsconfig.json", - { - compilerOptions: { - module: ModuleKind.CommonJS, - target: ScriptTarget.ES5, - noImplicitAny: false, - sourceMap: false, - lib: ["lib.es5.d.ts"] - }, - errors: [{ - file: undefined, - start: 0, - length: 0, - messageText: "Argument for '--lib' option must be: 'es5', 'es6', 'es2015', 'es7', 'es2016', 'es2017', 'es2018', 'esnext', 'dom', 'dom.iterable', 'webworker', 'webworker.importscripts', 'scripthost', 'es2015.core', 'es2015.collection', 'es2015.generator', 'es2015.iterable', 'es2015.promise', 'es2015.proxy', 'es2015.reflect', 'es2015.symbol', 'es2015.symbol.wellknown', 'es2016.array.include', 'es2017.object', 'es2017.sharedmemory', 'es2017.string', 'es2017.intl', 'es2017.typedarrays', 'es2018.asynciterable', 'es2018.intl', 'es2018.promise', 'es2018.regexp', 'esnext.array', 'esnext.symbol', 'esnext.intl', 'esnext.bigint'.", - code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category - }] - } - ); + }); + it("Convert empty string option of libs to compiler-options ", () => { + assertCompilerOptions({ + compilerOptions: { + module: "commonjs", + target: "es5", + noImplicitAny: false, + sourceMap: false, + lib: ["es5", ""] + } + }, "tsconfig.json", { + compilerOptions: { + module: ModuleKind.CommonJS, + target: ScriptTarget.ES5, + noImplicitAny: false, + sourceMap: false, + lib: ["lib.es5.d.ts"] + }, + errors: [{ + file: undefined, + start: 0, + length: 0, + messageText: "Argument for '--lib' option must be: 'es5', 'es6', 'es2015', 'es7', 'es2016', 'es2017', 'es2018', 'esnext', 'dom', 'dom.iterable', 'webworker', 'webworker.importscripts', 'scripthost', 'es2015.core', 'es2015.collection', 'es2015.generator', 'es2015.iterable', 'es2015.promise', 'es2015.proxy', 'es2015.reflect', 'es2015.symbol', 'es2015.symbol.wellknown', 'es2016.array.include', 'es2017.object', 'es2017.sharedmemory', 'es2017.string', 'es2017.intl', 'es2017.typedarrays', 'es2018.asynciterable', 'es2018.intl', 'es2018.promise', 'es2018.regexp', 'esnext.array', 'esnext.symbol', 'esnext.intl', 'esnext.bigint'.", + code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category + }] }); - - it("Convert empty string option of libs to compiler-options ", () => { - assertCompilerOptions( - { - compilerOptions: { - module: "commonjs", - target: "es5", - noImplicitAny: false, - sourceMap: false, - lib: [""] - } - }, "tsconfig.json", - { - compilerOptions: { - module: ModuleKind.CommonJS, - target: ScriptTarget.ES5, - noImplicitAny: false, - sourceMap: false, - lib: [] - }, - errors: [{ - file: undefined, - start: 0, - length: 0, - messageText: "Argument for '--lib' option must be: 'es5', 'es6', 'es2015', 'es7', 'es2016', 'es2017', 'es2018', 'esnext', 'dom', 'dom.iterable', 'webworker', 'webworker.importscripts', 'scripthost', 'es2015.core', 'es2015.collection', 'es2015.generator', 'es2015.iterable', 'es2015.promise', 'es2015.proxy', 'es2015.reflect', 'es2015.symbol', 'es2015.symbol.wellknown', 'es2016.array.include', 'es2017.object', 'es2017.sharedmemory', 'es2017.string', 'es2017.intl', 'es2017.typedarrays', 'es2018.asynciterable', 'es2018.intl', 'es2018.promise', 'es2018.regexp', 'esnext.array', 'esnext.symbol', 'esnext.intl', 'esnext.bigint'.", - code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category - }] - } - ); + }); + it("Convert empty string option of libs to compiler-options ", () => { + assertCompilerOptions({ + compilerOptions: { + module: "commonjs", + target: "es5", + noImplicitAny: false, + sourceMap: false, + lib: [""] + } + }, "tsconfig.json", { + compilerOptions: { + module: ModuleKind.CommonJS, + target: ScriptTarget.ES5, + noImplicitAny: false, + sourceMap: false, + lib: [] + }, + errors: [{ + file: undefined, + start: 0, + length: 0, + messageText: "Argument for '--lib' option must be: 'es5', 'es6', 'es2015', 'es7', 'es2016', 'es2017', 'es2018', 'esnext', 'dom', 'dom.iterable', 'webworker', 'webworker.importscripts', 'scripthost', 'es2015.core', 'es2015.collection', 'es2015.generator', 'es2015.iterable', 'es2015.promise', 'es2015.proxy', 'es2015.reflect', 'es2015.symbol', 'es2015.symbol.wellknown', 'es2016.array.include', 'es2017.object', 'es2017.sharedmemory', 'es2017.string', 'es2017.intl', 'es2017.typedarrays', 'es2018.asynciterable', 'es2018.intl', 'es2018.promise', 'es2018.regexp', 'esnext.array', 'esnext.symbol', 'esnext.intl', 'esnext.bigint'.", + code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category + }] }); - - it("Convert trailing-whitespace string option of libs to compiler-options ", () => { - assertCompilerOptions( - { - compilerOptions: { - module: "commonjs", - target: "es5", - noImplicitAny: false, - sourceMap: false, - lib: [" "] - } - }, "tsconfig.json", - { - compilerOptions: { - module: ModuleKind.CommonJS, - target: ScriptTarget.ES5, - noImplicitAny: false, - sourceMap: false, - lib: [] - }, - errors: [{ - file: undefined, - start: 0, - length: 0, - messageText: "Argument for '--lib' option must be: 'es5', 'es6', 'es2015', 'es7', 'es2016', 'es2017', 'es2018', 'esnext', 'dom', 'dom.iterable', 'webworker', 'webworker.importscripts', 'scripthost', 'es2015.core', 'es2015.collection', 'es2015.generator', 'es2015.iterable', 'es2015.promise', 'es2015.proxy', 'es2015.reflect', 'es2015.symbol', 'es2015.symbol.wellknown', 'es2016.array.include', 'es2017.object', 'es2017.sharedmemory', 'es2017.string', 'es2017.intl', 'es2017.typedarrays', 'es2018.asynciterable', 'es2018.intl', 'es2018.promise', 'es2018.regexp', 'esnext.array', 'esnext.symbol', 'esnext.intl', 'esnext.bigint'.", - code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, - category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category - }] - } - ); + }); + it("Convert trailing-whitespace string option of libs to compiler-options ", () => { + assertCompilerOptions({ + compilerOptions: { + module: "commonjs", + target: "es5", + noImplicitAny: false, + sourceMap: false, + lib: [" "] + } + }, "tsconfig.json", { + compilerOptions: { + module: ModuleKind.CommonJS, + target: ScriptTarget.ES5, + noImplicitAny: false, + sourceMap: false, + lib: [] + }, + errors: [{ + file: undefined, + start: 0, + length: 0, + messageText: "Argument for '--lib' option must be: 'es5', 'es6', 'es2015', 'es7', 'es2016', 'es2017', 'es2018', 'esnext', 'dom', 'dom.iterable', 'webworker', 'webworker.importscripts', 'scripthost', 'es2015.core', 'es2015.collection', 'es2015.generator', 'es2015.iterable', 'es2015.promise', 'es2015.proxy', 'es2015.reflect', 'es2015.symbol', 'es2015.symbol.wellknown', 'es2016.array.include', 'es2017.object', 'es2017.sharedmemory', 'es2017.string', 'es2017.intl', 'es2017.typedarrays', 'es2018.asynciterable', 'es2018.intl', 'es2018.promise', 'es2018.regexp', 'esnext.array', 'esnext.symbol', 'esnext.intl', 'esnext.bigint'.", + code: Diagnostics.Argument_for_0_option_must_be_Colon_1.code, + category: Diagnostics.Argument_for_0_option_must_be_Colon_1.category + }] }); - - it("Convert empty option of libs to compiler-options ", () => { - assertCompilerOptions( - { - compilerOptions: { - module: "commonjs", - target: "es5", - noImplicitAny: false, - sourceMap: false, - lib: [] - } - }, "tsconfig.json", - { - compilerOptions: { - module: ModuleKind.CommonJS, - target: ScriptTarget.ES5, - noImplicitAny: false, - sourceMap: false, - lib: [] - }, - errors: [] - } - ); + }); + it("Convert empty option of libs to compiler-options ", () => { + assertCompilerOptions({ + compilerOptions: { + module: "commonjs", + target: "es5", + noImplicitAny: false, + sourceMap: false, + lib: [] + } + }, "tsconfig.json", { + compilerOptions: { + module: ModuleKind.CommonJS, + target: ScriptTarget.ES5, + noImplicitAny: false, + sourceMap: false, + lib: [] + }, + errors: [] }); - - it("Convert incorrectly format tsconfig.json to compiler-options ", () => { - assertCompilerOptions( - { - compilerOptions: { - modu: "commonjs", - } - }, "tsconfig.json", - { - compilerOptions: {}, - errors: [{ - file: undefined, - start: 0, - length: 0, - messageText: "Unknown compiler option 'modu'.", - code: Diagnostics.Unknown_compiler_option_0.code, - category: Diagnostics.Unknown_compiler_option_0.category - }] - } - ); + }); + it("Convert incorrectly format tsconfig.json to compiler-options ", () => { + assertCompilerOptions({ + compilerOptions: { + modu: "commonjs", + } + }, "tsconfig.json", { + compilerOptions: {}, + errors: [{ + file: undefined, + start: 0, + length: 0, + messageText: "Unknown compiler option 'modu'.", + code: Diagnostics.Unknown_compiler_option_0.code, + category: Diagnostics.Unknown_compiler_option_0.category + }] }); - - it("Convert default tsconfig.json to compiler-options ", () => { - assertCompilerOptions({}, "tsconfig.json", - { - compilerOptions: {}, - errors: [] - } - ); + }); + it("Convert default tsconfig.json to compiler-options ", () => { + assertCompilerOptions({}, "tsconfig.json", { + compilerOptions: {}, + errors: [] }); - - it("Convert negative numbers in tsconfig.json ", () => { - assertCompilerOptions( - { - compilerOptions: { - allowJs: true, - maxNodeModuleJsDepth: -1 - } - }, "tsconfig.json", - { - compilerOptions: { - allowJs: true, - maxNodeModuleJsDepth: -1 - }, - errors: [] - } - ); + }); + it("Convert negative numbers in tsconfig.json ", () => { + assertCompilerOptions({ + compilerOptions: { + allowJs: true, + maxNodeModuleJsDepth: -1 + } + }, "tsconfig.json", { + compilerOptions: { + allowJs: true, + maxNodeModuleJsDepth: -1 + }, + errors: [] }); - - // jsconfig.json - it("Convert correctly format jsconfig.json to compiler-options ", () => { - assertCompilerOptions( - { - compilerOptions: { - module: "commonjs", - target: "es5", - noImplicitAny: false, - sourceMap: false, - lib: ["es5", "es2015.core", "es2015.symbol"] - } - }, "jsconfig.json", - { - compilerOptions: { - allowJs: true, - maxNodeModuleJsDepth: 2, - allowSyntheticDefaultImports: true, - skipLibCheck: true, - noEmit: true, - module: ModuleKind.CommonJS, - target: ScriptTarget.ES5, - noImplicitAny: false, - sourceMap: false, - lib: ["lib.es5.d.ts", "lib.es2015.core.d.ts", "lib.es2015.symbol.d.ts"] - }, - errors: [] - } - ); + }); + // jsconfig.json + it("Convert correctly format jsconfig.json to compiler-options ", () => { + assertCompilerOptions({ + compilerOptions: { + module: "commonjs", + target: "es5", + noImplicitAny: false, + sourceMap: false, + lib: ["es5", "es2015.core", "es2015.symbol"] + } + }, "jsconfig.json", { + compilerOptions: { + allowJs: true, + maxNodeModuleJsDepth: 2, + allowSyntheticDefaultImports: true, + skipLibCheck: true, + noEmit: true, + module: ModuleKind.CommonJS, + target: ScriptTarget.ES5, + noImplicitAny: false, + sourceMap: false, + lib: ["lib.es5.d.ts", "lib.es2015.core.d.ts", "lib.es2015.symbol.d.ts"] + }, + errors: [] }); - - it("Convert correctly format jsconfig.json with allowJs is false to compiler-options ", () => { - assertCompilerOptions( - { - compilerOptions: { - module: "commonjs", - target: "es5", - noImplicitAny: false, - sourceMap: false, - allowJs: false, - lib: ["es5", "es2015.core", "es2015.symbol"] - } - }, "jsconfig.json", - { - compilerOptions: { - allowJs: false, - maxNodeModuleJsDepth: 2, - allowSyntheticDefaultImports: true, - skipLibCheck: true, - noEmit: true, - module: ModuleKind.CommonJS, - target: ScriptTarget.ES5, - noImplicitAny: false, - sourceMap: false, - lib: ["lib.es5.d.ts", "lib.es2015.core.d.ts", "lib.es2015.symbol.d.ts"] - }, - errors: [] - } - ); + }); + it("Convert correctly format jsconfig.json with allowJs is false to compiler-options ", () => { + assertCompilerOptions({ + compilerOptions: { + module: "commonjs", + target: "es5", + noImplicitAny: false, + sourceMap: false, + allowJs: false, + lib: ["es5", "es2015.core", "es2015.symbol"] + } + }, "jsconfig.json", { + compilerOptions: { + allowJs: false, + maxNodeModuleJsDepth: 2, + allowSyntheticDefaultImports: true, + skipLibCheck: true, + noEmit: true, + module: ModuleKind.CommonJS, + target: ScriptTarget.ES5, + noImplicitAny: false, + sourceMap: false, + lib: ["lib.es5.d.ts", "lib.es2015.core.d.ts", "lib.es2015.symbol.d.ts"] + }, + errors: [] }); - - it("Convert incorrectly format jsconfig.json to compiler-options ", () => { - assertCompilerOptions( - { - compilerOptions: { - modu: "commonjs", - } - }, "jsconfig.json", - { - compilerOptions: - { - allowJs: true, - maxNodeModuleJsDepth: 2, - allowSyntheticDefaultImports: true, - skipLibCheck: true, - noEmit: true - }, - errors: [{ - file: undefined, - start: 0, - length: 0, - messageText: "Unknown compiler option 'modu'.", - code: Diagnostics.Unknown_compiler_option_0.code, - category: Diagnostics.Unknown_compiler_option_0.category - }] - } - ); + }); + it("Convert incorrectly format jsconfig.json to compiler-options ", () => { + assertCompilerOptions({ + compilerOptions: { + modu: "commonjs", + } + }, "jsconfig.json", { + compilerOptions: { + allowJs: true, + maxNodeModuleJsDepth: 2, + allowSyntheticDefaultImports: true, + skipLibCheck: true, + noEmit: true + }, + errors: [{ + file: undefined, + start: 0, + length: 0, + messageText: "Unknown compiler option 'modu'.", + code: Diagnostics.Unknown_compiler_option_0.code, + category: Diagnostics.Unknown_compiler_option_0.category + }] }); - - it("Convert default jsconfig.json to compiler-options ", () => { - assertCompilerOptions({}, "jsconfig.json", - { - compilerOptions: - { - allowJs: true, - maxNodeModuleJsDepth: 2, - allowSyntheticDefaultImports: true, - skipLibCheck: true, - noEmit: true - }, - errors: [] - } - ); + }); + it("Convert default jsconfig.json to compiler-options ", () => { + assertCompilerOptions({}, "jsconfig.json", { + compilerOptions: { + allowJs: true, + maxNodeModuleJsDepth: 2, + allowSyntheticDefaultImports: true, + skipLibCheck: true, + noEmit: true + }, + errors: [] }); - - it("Convert tsconfig options when there are multiple invalid strings", () => { - assertCompilerOptionsWithJsonText(`{ + }); + it("Convert tsconfig options when there are multiple invalid strings", () => { + assertCompilerOptionsWithJsonText(`{ "compilerOptions": { "target": "<%- options.useTsWithBabel ? 'esnext' : 'es5' %>", "module": "esnext", @@ -592,16 +502,13 @@ namespace ts { ] } } -`, - "tsconfig.json", - { - compilerOptions: { - target: undefined, - module: ModuleKind.ESNext, - experimentalDecorators: true, - }, - hasParseErrors: true - }); +`, "tsconfig.json", { + compilerOptions: { + target: undefined, + module: ModuleKind.ESNext, + experimentalDecorators: true, + }, + hasParseErrors: true }); }); -} +}); diff --git a/src/testRunner/unittests/config/convertTypeAcquisitionFromJson.ts b/src/testRunner/unittests/config/convertTypeAcquisitionFromJson.ts index e8ca5f15d69d9..54e6b7e1fa484 100644 --- a/src/testRunner/unittests/config/convertTypeAcquisitionFromJson.ts +++ b/src/testRunner/unittests/config/convertTypeAcquisitionFromJson.ts @@ -1,239 +1,194 @@ -namespace ts { - interface ExpectedResult { typeAcquisition: TypeAcquisition; errors: Diagnostic[]; } - describe("unittests:: config:: convertTypeAcquisitionFromJson", () => { - function assertTypeAcquisition(json: any, configFileName: string, expectedResult: ExpectedResult) { - assertTypeAcquisitionWithJson(json, configFileName, expectedResult); - assertTypeAcquisitionWithJsonNode(json, configFileName, expectedResult); - } - - function verifyAcquisition(actualTypeAcquisition: TypeAcquisition | undefined, expectedResult: ExpectedResult) { - const parsedTypeAcquisition = JSON.stringify(actualTypeAcquisition); - const expectedTypeAcquisition = JSON.stringify(expectedResult.typeAcquisition); - assert.equal(parsedTypeAcquisition, expectedTypeAcquisition); - } - - function verifyErrors(actualErrors: Diagnostic[], expectedResult: ExpectedResult, hasLocation?: boolean) { - const expectedErrors = expectedResult.errors; - assert.isTrue(expectedResult.errors.length === actualErrors.length, `Expected error: ${JSON.stringify(expectedResult.errors)}. Actual error: ${JSON.stringify(actualErrors)}.`); - for (let i = 0; i < actualErrors.length; i++) { - const actualError = actualErrors[i]; - const expectedError = expectedErrors[i]; - assert.equal(actualError.code, expectedError.code, `Expected error-code: ${JSON.stringify(expectedError.code)}. Actual error-code: ${JSON.stringify(actualError.code)}.`); - assert.equal(actualError.category, expectedError.category, `Expected error-category: ${JSON.stringify(expectedError.category)}. Actual error-category: ${JSON.stringify(actualError.category)}.`); - if (hasLocation) { - assert(actualError.file); - assert(actualError.start); - assert(actualError.length); - } +import { TypeAcquisition, Diagnostic, convertTypeAcquisitionFromJson, parseJsonText, ParseConfigHost, parseJsonSourceFileConfigFileContent, filter, Diagnostics } from "../../ts"; +import { FileSystem } from "../../vfs"; +import * as fakes from "../../fakes"; +interface ExpectedResult { + typeAcquisition: TypeAcquisition; + errors: Diagnostic[]; +} +describe("unittests:: config:: convertTypeAcquisitionFromJson", () => { + function assertTypeAcquisition(json: any, configFileName: string, expectedResult: ExpectedResult) { + assertTypeAcquisitionWithJson(json, configFileName, expectedResult); + assertTypeAcquisitionWithJsonNode(json, configFileName, expectedResult); + } + function verifyAcquisition(actualTypeAcquisition: TypeAcquisition | undefined, expectedResult: ExpectedResult) { + const parsedTypeAcquisition = JSON.stringify(actualTypeAcquisition); + const expectedTypeAcquisition = JSON.stringify(expectedResult.typeAcquisition); + assert.equal(parsedTypeAcquisition, expectedTypeAcquisition); + } + function verifyErrors(actualErrors: Diagnostic[], expectedResult: ExpectedResult, hasLocation?: boolean) { + const expectedErrors = expectedResult.errors; + assert.isTrue(expectedResult.errors.length === actualErrors.length, `Expected error: ${JSON.stringify(expectedResult.errors)}. Actual error: ${JSON.stringify(actualErrors)}.`); + for (let i = 0; i < actualErrors.length; i++) { + const actualError = actualErrors[i]; + const expectedError = expectedErrors[i]; + assert.equal(actualError.code, expectedError.code, `Expected error-code: ${JSON.stringify(expectedError.code)}. Actual error-code: ${JSON.stringify(actualError.code)}.`); + assert.equal(actualError.category, expectedError.category, `Expected error-category: ${JSON.stringify(expectedError.category)}. Actual error-category: ${JSON.stringify(actualError.category)}.`); + if (hasLocation) { + assert(actualError.file); + assert(actualError.start); + assert(actualError.length); } } - - function assertTypeAcquisitionWithJson(json: any, configFileName: string, expectedResult: ExpectedResult) { - const jsonOptions = json.typeAcquisition || json.typingOptions; - const { options: actualTypeAcquisition, errors: actualErrors } = convertTypeAcquisitionFromJson(jsonOptions, "/apath/", configFileName); - verifyAcquisition(actualTypeAcquisition, expectedResult); - verifyErrors(actualErrors, expectedResult); - } - - function assertTypeAcquisitionWithJsonNode(json: any, configFileName: string, expectedResult: ExpectedResult) { - const fileText = JSON.stringify(json); - const result = parseJsonText(configFileName, fileText); - assert(!result.parseDiagnostics.length); - assert(!!result.endOfFileToken); - const host: ParseConfigHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ false, { cwd: "/apath/" })); - const { typeAcquisition: actualTypeAcquisition, errors: actualParseErrors } = parseJsonSourceFileConfigFileContent(result, host, "/apath/", /*existingOptions*/ undefined, configFileName); - verifyAcquisition(actualTypeAcquisition, expectedResult); - - const actualErrors = filter(actualParseErrors, error => error.code !== Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code); - verifyErrors(actualErrors, expectedResult, /*hasLocation*/ true); - } - - // tsconfig.json - it("Convert deprecated typingOptions.enableAutoDiscovery format tsconfig.json to typeAcquisition ", () => { - assertTypeAcquisition( - { - typingOptions: - { - enableAutoDiscovery: true, - include: ["0.d.ts", "1.d.ts"], - exclude: ["0.js", "1.js"] - } - }, - "tsconfig.json", - { - typeAcquisition: - { - enable: true, - include: ["0.d.ts", "1.d.ts"], - exclude: ["0.js", "1.js"] - }, - errors: [] - } - ); + } + function assertTypeAcquisitionWithJson(json: any, configFileName: string, expectedResult: ExpectedResult) { + const jsonOptions = json.typeAcquisition || json.typingOptions; + const { options: actualTypeAcquisition, errors: actualErrors } = convertTypeAcquisitionFromJson(jsonOptions, "/apath/", configFileName); + verifyAcquisition(actualTypeAcquisition, expectedResult); + verifyErrors(actualErrors, expectedResult); + } + function assertTypeAcquisitionWithJsonNode(json: any, configFileName: string, expectedResult: ExpectedResult) { + const fileText = JSON.stringify(json); + const result = parseJsonText(configFileName, fileText); + assert(!result.parseDiagnostics.length); + assert(!!result.endOfFileToken); + const host: ParseConfigHost = new fakes.ParseConfigHost(new FileSystem(/*ignoreCase*/ false, { cwd: "/apath/" })); + const { typeAcquisition: actualTypeAcquisition, errors: actualParseErrors } = parseJsonSourceFileConfigFileContent(result, host, "/apath/", /*existingOptions*/ undefined, configFileName); + verifyAcquisition(actualTypeAcquisition, expectedResult); + const actualErrors = filter(actualParseErrors, error => error.code !== Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code); + verifyErrors(actualErrors, expectedResult, /*hasLocation*/ true); + } + // tsconfig.json + it("Convert deprecated typingOptions.enableAutoDiscovery format tsconfig.json to typeAcquisition ", () => { + assertTypeAcquisition({ + typingOptions: { + enableAutoDiscovery: true, + include: ["0.d.ts", "1.d.ts"], + exclude: ["0.js", "1.js"] + } + }, "tsconfig.json", { + typeAcquisition: { + enable: true, + include: ["0.d.ts", "1.d.ts"], + exclude: ["0.js", "1.js"] + }, + errors: ([]) }); - - it("Convert correctly format tsconfig.json to typeAcquisition ", () => { - assertTypeAcquisition( - { - typeAcquisition: - { - enable: true, - include: ["0.d.ts", "1.d.ts"], - exclude: ["0.js", "1.js"] - } - }, - "tsconfig.json", - { - typeAcquisition: - { - enable: true, - include: ["0.d.ts", "1.d.ts"], - exclude: ["0.js", "1.js"] - }, - errors: [] - }); + }); + it("Convert correctly format tsconfig.json to typeAcquisition ", () => { + assertTypeAcquisition({ + typeAcquisition: { + enable: true, + include: ["0.d.ts", "1.d.ts"], + exclude: ["0.js", "1.js"] + } + }, "tsconfig.json", { + typeAcquisition: { + enable: true, + include: ["0.d.ts", "1.d.ts"], + exclude: ["0.js", "1.js"] + }, + errors: ([]) }); - - it("Convert incorrect format tsconfig.json to typeAcquisition ", () => { - assertTypeAcquisition( - { - typeAcquisition: - { - enableAutoDiscovy: true, - } - }, "tsconfig.json", - { - typeAcquisition: - { - enable: false, - include: [], - exclude: [] - }, - errors: [ - { - category: Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1.category, - code: Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1.code, - file: undefined, - start: 0, - length: 0, - messageText: undefined!, // TODO: GH#18217 - } - ] - }); + }); + it("Convert incorrect format tsconfig.json to typeAcquisition ", () => { + assertTypeAcquisition({ + typeAcquisition: { + enableAutoDiscovy: true, + } + }, "tsconfig.json", { + typeAcquisition: { + enable: false, + include: [], + exclude: [] + }, + errors: [ + { + category: Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1.category, + code: Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1.code, + file: undefined, + start: 0, + length: 0, + messageText: undefined!, + } + ] }); - - it("Convert default tsconfig.json to typeAcquisition ", () => { - assertTypeAcquisition({}, "tsconfig.json", - { - typeAcquisition: - { - enable: false, - include: [], - exclude: [] - }, - errors: [] - }); + }); + it("Convert default tsconfig.json to typeAcquisition ", () => { + assertTypeAcquisition({}, "tsconfig.json", { + typeAcquisition: { + enable: false, + include: [], + exclude: [] + }, + errors: ([]) }); - - it("Convert tsconfig.json with only enable property to typeAcquisition ", () => { - assertTypeAcquisition( - { - typeAcquisition: - { - enable: true - } - }, "tsconfig.json", - { - typeAcquisition: - { - enable: true, - include: [], - exclude: [] - }, - errors: [] - }); + }); + it("Convert tsconfig.json with only enable property to typeAcquisition ", () => { + assertTypeAcquisition({ + typeAcquisition: { + enable: true + } + }, "tsconfig.json", { + typeAcquisition: { + enable: true, + include: [], + exclude: [] + }, + errors: ([]) }); - - // jsconfig.json - it("Convert jsconfig.json to typeAcquisition ", () => { - assertTypeAcquisition( - { - typeAcquisition: - { - enable: false, - include: ["0.d.ts"], - exclude: ["0.js"] - } - }, "jsconfig.json", - { - typeAcquisition: - { - enable: false, - include: ["0.d.ts"], - exclude: ["0.js"] - }, - errors: [] - }); + }); + // jsconfig.json + it("Convert jsconfig.json to typeAcquisition ", () => { + assertTypeAcquisition({ + typeAcquisition: { + enable: false, + include: ["0.d.ts"], + exclude: ["0.js"] + } + }, "jsconfig.json", { + typeAcquisition: { + enable: false, + include: ["0.d.ts"], + exclude: ["0.js"] + }, + errors: ([]) }); - - it("Convert default jsconfig.json to typeAcquisition ", () => { - assertTypeAcquisition({ }, "jsconfig.json", - { - typeAcquisition: - { - enable: true, - include: [], - exclude: [] - }, - errors: [] - }); + }); + it("Convert default jsconfig.json to typeAcquisition ", () => { + assertTypeAcquisition({}, "jsconfig.json", { + typeAcquisition: { + enable: true, + include: [], + exclude: [] + }, + errors: ([]) }); - - it("Convert incorrect format jsconfig.json to typeAcquisition ", () => { - assertTypeAcquisition( - { - typeAcquisition: - { - enableAutoDiscovy: true, - } - }, "jsconfig.json", - { - typeAcquisition: - { - enable: true, - include: [], - exclude: [] - }, - errors: [ - { - category: Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1.category, - code: Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1.code, - file: undefined, - start: 0, - length: 0, - messageText: undefined!, // TODO: GH#18217 - } - ] - }); + }); + it("Convert incorrect format jsconfig.json to typeAcquisition ", () => { + assertTypeAcquisition({ + typeAcquisition: { + enableAutoDiscovy: true, + } + }, "jsconfig.json", { + typeAcquisition: { + enable: true, + include: [], + exclude: [] + }, + errors: [ + { + category: Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1.category, + code: Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1.code, + file: undefined, + start: 0, + length: 0, + messageText: undefined!, + } + ] }); - - it("Convert jsconfig.json with only enable property to typeAcquisition ", () => { - assertTypeAcquisition( - { - typeAcquisition: - { - enable: false - } - }, "jsconfig.json", - { - typeAcquisition: - { - enable: false, - include: [], - exclude: [] - }, - errors: [] - }); + }); + it("Convert jsconfig.json with only enable property to typeAcquisition ", () => { + assertTypeAcquisition({ + typeAcquisition: { + enable: false + } + }, "jsconfig.json", { + typeAcquisition: { + enable: false, + include: [], + exclude: [] + }, + errors: ([]) }); }); -} +}); diff --git a/src/testRunner/unittests/config/initializeTSConfig.ts b/src/testRunner/unittests/config/initializeTSConfig.ts index f7fb7d2a4398b..0b5ecbb982e4c 100644 --- a/src/testRunner/unittests/config/initializeTSConfig.ts +++ b/src/testRunner/unittests/config/initializeTSConfig.ts @@ -1,33 +1,23 @@ -namespace ts { - describe("unittests:: config:: initTSConfig", () => { - function initTSConfigCorrectly(name: string, commandLinesArgs: string[]) { - describe(name, () => { - const commandLine = parseCommandLine(commandLinesArgs); - const initResult = generateTSConfig(commandLine.options, commandLine.fileNames, "\n"); - const outputFileName = `tsConfig/${name.replace(/[^a-z0-9\-. ]/ig, "")}/tsconfig.json`; - - it(`Correct output for ${outputFileName}`, () => { - Harness.Baseline.runBaseline(outputFileName, initResult); - }); +import { parseCommandLine, generateTSConfig } from "../../ts"; +import { Baseline } from "../../Harness"; +describe("unittests:: config:: initTSConfig", () => { + function initTSConfigCorrectly(name: string, commandLinesArgs: string[]) { + describe(name, () => { + const commandLine = parseCommandLine(commandLinesArgs); + const initResult = generateTSConfig(commandLine.options, commandLine.fileNames, "\n"); + const outputFileName = `tsConfig/${name.replace(/[^a-z0-9\-. ]/ig, "")}/tsconfig.json`; + it(`Correct output for ${outputFileName}`, () => { + Baseline.runBaseline(outputFileName, initResult); }); - } - - initTSConfigCorrectly("Default initialized TSConfig", ["--init"]); - - initTSConfigCorrectly("Initialized TSConfig with files options", ["--init", "file0.st", "file1.ts", "file2.ts"]); - - initTSConfigCorrectly("Initialized TSConfig with boolean value compiler options", ["--init", "--noUnusedLocals"]); - - initTSConfigCorrectly("Initialized TSConfig with enum value compiler options", ["--init", "--target", "es5", "--jsx", "react"]); - - initTSConfigCorrectly("Initialized TSConfig with list compiler options", ["--init", "--types", "jquery,mocha"]); - - initTSConfigCorrectly("Initialized TSConfig with list compiler options with enum value", ["--init", "--lib", "es5,es2015.core"]); - - initTSConfigCorrectly("Initialized TSConfig with incorrect compiler option", ["--init", "--someNonExistOption"]); - - initTSConfigCorrectly("Initialized TSConfig with incorrect compiler option value", ["--init", "--lib", "nonExistLib,es5,es2015.promise"]); - - initTSConfigCorrectly("Initialized TSConfig with advanced options", ["--init", "--declaration", "--declarationDir", "lib", "--skipLibCheck", "--noErrorTruncation"]); - }); -} + }); + } + initTSConfigCorrectly("Default initialized TSConfig", ["--init"]); + initTSConfigCorrectly("Initialized TSConfig with files options", ["--init", "file0.st", "file1.ts", "file2.ts"]); + initTSConfigCorrectly("Initialized TSConfig with boolean value compiler options", ["--init", "--noUnusedLocals"]); + initTSConfigCorrectly("Initialized TSConfig with enum value compiler options", ["--init", "--target", "es5", "--jsx", "react"]); + initTSConfigCorrectly("Initialized TSConfig with list compiler options", ["--init", "--types", "jquery,mocha"]); + initTSConfigCorrectly("Initialized TSConfig with list compiler options with enum value", ["--init", "--lib", "es5,es2015.core"]); + initTSConfigCorrectly("Initialized TSConfig with incorrect compiler option", ["--init", "--someNonExistOption"]); + initTSConfigCorrectly("Initialized TSConfig with incorrect compiler option value", ["--init", "--lib", "nonExistLib,es5,es2015.promise"]); + initTSConfigCorrectly("Initialized TSConfig with advanced options", ["--init", "--declaration", "--declarationDir", "lib", "--skipLibCheck", "--noErrorTruncation"]); +}); diff --git a/src/testRunner/unittests/config/matchFiles.ts b/src/testRunner/unittests/config/matchFiles.ts index 272137af69607..15d550046cee5 100644 --- a/src/testRunner/unittests/config/matchFiles.ts +++ b/src/testRunner/unittests/config/matchFiles.ts @@ -1,7 +1,10 @@ -namespace ts { - const caseInsensitiveBasePath = "c:/dev/"; - const caseInsensitiveTsconfigPath = "c:/dev/tsconfig.json"; - const caseInsensitiveHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: caseInsensitiveBasePath, files: { +import { ParseConfigHost } from "../../fakes"; +import { FileSystem } from "../../vfs"; +import { ParsedCommandLine, CompilerOptions, Path, parseJsonText, parseJsonSourceFileConfigFileContent, parseJsonConfigFileContent, Diagnostic, DiagnosticMessage, SourceFile, SyntaxKind, createFileDiagnostic, WatchDirectoryFlags, createCompilerDiagnostic, Diagnostics, JsxEmit } from "../../ts"; +import * as ts from "../../ts"; +const caseInsensitiveBasePath = "c:/dev/"; +const caseInsensitiveTsconfigPath = "c:/dev/tsconfig.json"; +const caseInsensitiveHost = new ParseConfigHost(new FileSystem(/*ignoreCase*/ true, { cwd: caseInsensitiveBasePath, files: { "c:/dev/a.ts": "", "c:/dev/a.d.ts": "", "c:/dev/a.js": "", @@ -25,10 +28,9 @@ namespace ts { "c:/dev/js/ab.min.js": "", "c:/ext/ext.ts": "", "c:/ext/b/a..b.ts": "", - }})); - - const caseSensitiveBasePath = "/dev/"; - const caseSensitiveHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ false, { cwd: caseSensitiveBasePath, files: { + } })); +const caseSensitiveBasePath = "/dev/"; +const caseSensitiveHost = new ParseConfigHost(new FileSystem(/*ignoreCase*/ false, { cwd: caseSensitiveBasePath, files: { "/dev/a.ts": "", "/dev/a.d.ts": "", "/dev/a.js": "", @@ -50,9 +52,8 @@ namespace ts { "/dev/q/a/c/b/d.ts": "", "/dev/js/a.js": "", "/dev/js/b.js": "", - }})); - - const caseInsensitiveMixedExtensionHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: caseInsensitiveBasePath, files: { + } })); +const caseInsensitiveMixedExtensionHost = new ParseConfigHost(new FileSystem(/*ignoreCase*/ true, { cwd: caseInsensitiveBasePath, files: { "c:/dev/a.ts": "", "c:/dev/a.d.ts": "", "c:/dev/a.js": "", @@ -64,9 +65,8 @@ namespace ts { "c:/dev/d.js": "", "c:/dev/e.jsx": "", "c:/dev/f.other": "", - }})); - - const caseInsensitiveCommonFoldersHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: caseInsensitiveBasePath, files: { + } })); +const caseInsensitiveCommonFoldersHost = new ParseConfigHost(new FileSystem(/*ignoreCase*/ true, { cwd: caseInsensitiveBasePath, files: { "c:/dev/a.ts": "", "c:/dev/a.d.ts": "", "c:/dev/a.js": "", @@ -75,9 +75,8 @@ namespace ts { "c:/dev/node_modules/a.ts": "", "c:/dev/bower_components/a.ts": "", "c:/dev/jspm_packages/a.ts": "", - }})); - - const caseInsensitiveDottedFoldersHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: caseInsensitiveBasePath, files: { + } })); +const caseInsensitiveDottedFoldersHost = new ParseConfigHost(new FileSystem(/*ignoreCase*/ true, { cwd: caseInsensitiveBasePath, files: { "c:/dev/x/d.ts": "", "c:/dev/x/y/d.ts": "", "c:/dev/x/y/.e.ts": "", @@ -86,397 +85,574 @@ namespace ts { "c:/dev/.z/c.ts": "", "c:/dev/w/.u/e.ts": "", "c:/dev/g.min.js/.g/g.ts": "", - }})); - - const caseInsensitiveOrderingDiffersWithCaseHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: caseInsensitiveBasePath, files: { + } })); +const caseInsensitiveOrderingDiffersWithCaseHost = new ParseConfigHost(new FileSystem(/*ignoreCase*/ true, { cwd: caseInsensitiveBasePath, files: { "c:/dev/xylophone.ts": "", "c:/dev/Yosemite.ts": "", "c:/dev/zebra.ts": "", - }})); - - const caseSensitiveOrderingDiffersWithCaseHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ false, { cwd: caseSensitiveBasePath, files: { + } })); +const caseSensitiveOrderingDiffersWithCaseHost = new ParseConfigHost(new FileSystem(/*ignoreCase*/ false, { cwd: caseSensitiveBasePath, files: { "/dev/xylophone.ts": "", "/dev/Yosemite.ts": "", "/dev/zebra.ts": "", - }})); - - function assertParsed(actual: ParsedCommandLine, expected: ParsedCommandLine): void { - assert.deepEqual(actual.fileNames, expected.fileNames); - assert.deepEqual(actual.wildcardDirectories, expected.wildcardDirectories); - assert.deepEqual(actual.errors, expected.errors); - } - - function validateMatches(expected: ParsedCommandLine, json: any, host: ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[]) { - { - const jsonText = JSON.stringify(json); - const result = parseJsonText(caseInsensitiveTsconfigPath, jsonText); - const actual = parseJsonSourceFileConfigFileContent(result, host, basePath, existingOptions, configFileName, resolutionStack); - for (const error of expected.errors) { - if (error.file) { - error.file = result; - } + } })); +function assertParsed(actual: ParsedCommandLine, expected: ParsedCommandLine): void { + assert.deepEqual(actual.fileNames, expected.fileNames); + assert.deepEqual(actual.wildcardDirectories, expected.wildcardDirectories); + assert.deepEqual(actual.errors, expected.errors); +} +function validateMatches(expected: ParsedCommandLine, json: any, host: ts.ParseConfigHost, basePath: string, existingOptions?: CompilerOptions, configFileName?: string, resolutionStack?: Path[]) { + { + const jsonText = JSON.stringify(json); + const result = parseJsonText(caseInsensitiveTsconfigPath, jsonText); + const actual = parseJsonSourceFileConfigFileContent(result, host, basePath, existingOptions, configFileName, resolutionStack); + for (const error of expected.errors) { + if (error.file) { + error.file = result; } - assertParsed(actual, expected); - } - { - const actual = parseJsonConfigFileContent(json, host, basePath, existingOptions, configFileName, resolutionStack); - expected.errors = expected.errors.map((error): Diagnostic => ({ - category: error.category, - code: error.code, - file: undefined, - length: undefined, - messageText: error.messageText, - start: undefined, - reportsUnnecessary: undefined, - })); - assertParsed(actual, expected); } + assertParsed(actual, expected); } - - function createDiagnosticForConfigFile(json: any, start: number, length: number, diagnosticMessage: DiagnosticMessage, arg0: string) { - const text = JSON.stringify(json); - const file = { - fileName: caseInsensitiveTsconfigPath, - kind: SyntaxKind.SourceFile, - text - }; - return createFileDiagnostic(file, start, length, diagnosticMessage, arg0); + { + const actual = parseJsonConfigFileContent(json, host, basePath, existingOptions, configFileName, resolutionStack); + expected.errors = expected.errors.map((error): Diagnostic => ({ + category: error.category, + code: error.code, + file: undefined, + length: undefined, + messageText: error.messageText, + start: undefined, + reportsUnnecessary: undefined, + })); + assertParsed(actual, expected); } - - describe("unittests:: config:: matchFiles", () => { - it("with defaults", () => { - const json = {}; +} +function createDiagnosticForConfigFile(json: any, start: number, length: number, diagnosticMessage: DiagnosticMessage, arg0: string) { + const text = JSON.stringify(json); + const file = ({ + fileName: caseInsensitiveTsconfigPath, + kind: SyntaxKind.SourceFile, + text + }); + return createFileDiagnostic(file, start, length, diagnosticMessage, arg0); +} +describe("unittests:: config:: matchFiles", () => { + it("with defaults", () => { + const json = {}; + const expected: ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/a.ts", + "c:/dev/b.ts", + "c:/dev/x/a.ts" + ], + wildcardDirectories: { + "c:/dev": WatchDirectoryFlags.Recursive + }, + }; + validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); + }); + describe("with literal file list", () => { + it("without exclusions", () => { + const json = { + files: [ + "a.ts", + "b.ts" + ] + }; + const expected: ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/a.ts", + "c:/dev/b.ts" + ], + wildcardDirectories: {}, + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + it("missing files are still present", () => { + const json = { + files: [ + "z.ts", + "x.ts" + ] + }; + const expected: ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/z.ts", + "c:/dev/x.ts" + ], + wildcardDirectories: {}, + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + it("are not removed due to excludes", () => { + const json = { + files: [ + "a.ts", + "b.ts" + ], + exclude: [ + "b.ts" + ] + }; + const expected: ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/a.ts", + "c:/dev/b.ts" + ], + wildcardDirectories: {}, + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + }); + describe("with literal include list", () => { + it("without exclusions", () => { + const json = { + include: [ + "a.ts", + "b.ts" + ] + }; + const expected: ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/a.ts", + "c:/dev/b.ts" + ], + wildcardDirectories: {}, + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + it("with non .ts file extensions are excluded", () => { + const json = { + include: [ + "a.js", + "b.js" + ] + }; + const expected: ParsedCommandLine = { + options: {}, + errors: [ + createCompilerDiagnostic(Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, caseInsensitiveTsconfigPath, JSON.stringify(json.include), "[]") + ], + fileNames: [], + wildcardDirectories: {}, + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, /*existingOptions*/ undefined, caseInsensitiveTsconfigPath); + }); + it("with missing files are excluded", () => { + const json = { + include: [ + "z.ts", + "x.ts" + ] + }; + const expected: ParsedCommandLine = { + options: {}, + errors: [ + createCompilerDiagnostic(Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, caseInsensitiveTsconfigPath, JSON.stringify(json.include), "[]") + ], + fileNames: [], + wildcardDirectories: {}, + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, /*existingOptions*/ undefined, caseInsensitiveTsconfigPath); + }); + it("with literal excludes", () => { + const json = { + include: [ + "a.ts", + "b.ts" + ], + exclude: [ + "b.ts" + ] + }; + const expected: ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/a.ts" + ], + wildcardDirectories: {}, + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + it("with wildcard excludes", () => { + const json = { + include: [ + "a.ts", + "b.ts", + "z/a.ts", + "z/abz.ts", + "z/aba.ts", + "x/b.ts" + ], + exclude: [ + "*.ts", + "z/??z.ts", + "*/b.ts" + ] + }; + const expected: ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/z/a.ts", + "c:/dev/z/aba.ts" + ], + wildcardDirectories: {}, + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + it("with recursive excludes", () => { + const json = { + include: [ + "a.ts", + "b.ts", + "x/a.ts", + "x/b.ts", + "x/y/a.ts", + "x/y/b.ts" + ], + exclude: [ + "**/b.ts" + ] + }; + const expected: ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/a.ts", + "c:/dev/x/a.ts", + "c:/dev/x/y/a.ts" + ], + wildcardDirectories: {}, + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + it("with case sensitive exclude", () => { + const json = { + include: [ + "B.ts" + ], + exclude: [ + "**/b.ts" + ] + }; + const expected: ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "/dev/B.ts" + ], + wildcardDirectories: {}, + }; + validateMatches(expected, json, caseSensitiveHost, caseSensitiveBasePath); + }); + it("with common package folders and no exclusions", () => { + const json = { + include: [ + "a.ts", + "b.ts", + "node_modules/a.ts", + "bower_components/a.ts", + "jspm_packages/a.ts" + ] + }; + const expected: ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/a.ts", + "c:/dev/b.ts", + "c:/dev/node_modules/a.ts", + "c:/dev/bower_components/a.ts", + "c:/dev/jspm_packages/a.ts" + ], + wildcardDirectories: {}, + }; + validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); + }); + it("with common package folders and exclusions", () => { + const json = { + include: [ + "a.ts", + "b.ts", + "node_modules/a.ts", + "bower_components/a.ts", + "jspm_packages/a.ts" + ], + exclude: [ + "a.ts", + "b.ts" + ] + }; + const expected: ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/node_modules/a.ts", + "c:/dev/bower_components/a.ts", + "c:/dev/jspm_packages/a.ts" + ], + wildcardDirectories: {}, + }; + validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); + }); + it("with common package folders and empty exclude", () => { + const json = { + include: [ + "a.ts", + "b.ts", + "node_modules/a.ts", + "bower_components/a.ts", + "jspm_packages/a.ts" + ] + }; + const expected: ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/a.ts", + "c:/dev/b.ts", + "c:/dev/node_modules/a.ts", + "c:/dev/bower_components/a.ts", + "c:/dev/jspm_packages/a.ts" + ], + wildcardDirectories: {}, + }; + validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); + }); + }); + describe("with wildcard include list", () => { + it("is sorted in include order, then in alphabetical order", () => { + const json = { + include: [ + "z/*.ts", + "x/*.ts" + ] + }; + const expected: ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/z/a.ts", + "c:/dev/z/aba.ts", + "c:/dev/z/abz.ts", + "c:/dev/z/b.ts", + "c:/dev/z/bba.ts", + "c:/dev/z/bbz.ts", + "c:/dev/x/a.ts", + "c:/dev/x/aa.ts", + "c:/dev/x/b.ts" + ], + wildcardDirectories: { + "c:/dev/z": WatchDirectoryFlags.None, + "c:/dev/x": WatchDirectoryFlags.None + }, + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + it("same named declarations are excluded", () => { + const json = { + include: [ + "*.ts" + ] + }; + const expected: ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/a.ts", + "c:/dev/b.ts", + "c:/dev/c.d.ts" + ], + wildcardDirectories: { + "c:/dev": WatchDirectoryFlags.None + }, + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + it("`*` matches only ts files", () => { + const json = { + include: [ + "*" + ] + }; const expected: ParsedCommandLine = { options: {}, errors: [], fileNames: [ "c:/dev/a.ts", "c:/dev/b.ts", - "c:/dev/x/a.ts" + "c:/dev/c.d.ts" + ], + wildcardDirectories: { + "c:/dev": WatchDirectoryFlags.None + }, + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + it("`?` matches only a single character", () => { + const json = { + include: [ + "x/?.ts" + ] + }; + const expected: ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/x/a.ts", + "c:/dev/x/b.ts" + ], + wildcardDirectories: { + "c:/dev/x": WatchDirectoryFlags.None + }, + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + it("with recursive directory", () => { + const json = { + include: [ + "**/a.ts" + ] + }; + const expected: ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/a.ts", + "c:/dev/x/a.ts", + "c:/dev/x/y/a.ts", + "c:/dev/z/a.ts" ], wildcardDirectories: { "c:/dev": WatchDirectoryFlags.Recursive }, }; - validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); - - describe("with literal file list", () => { - it("without exclusions", () => { - const json = { - files: [ - "a.ts", - "b.ts" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/a.ts", - "c:/dev/b.ts" - ], - wildcardDirectories: {}, - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); - }); - it("missing files are still present", () => { - const json = { - files: [ - "z.ts", - "x.ts" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/z.ts", - "c:/dev/x.ts" - ], - wildcardDirectories: {}, - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); - }); - it("are not removed due to excludes", () => { - const json = { - files: [ - "a.ts", - "b.ts" - ], - exclude: [ - "b.ts" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/a.ts", - "c:/dev/b.ts" - ], - wildcardDirectories: {}, - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); - }); + it("with multiple recursive directories", () => { + const json = { + include: [ + "x/y/**/a.ts", + "x/**/a.ts", + "z/**/a.ts" + ] + }; + const expected: ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/x/y/a.ts", + "c:/dev/x/a.ts", + "c:/dev/z/a.ts" + ], + wildcardDirectories: { + "c:/dev/x": WatchDirectoryFlags.Recursive, + "c:/dev/z": WatchDirectoryFlags.Recursive + }, + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); - - describe("with literal include list", () => { - it("without exclusions", () => { - const json = { - include: [ - "a.ts", - "b.ts" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/a.ts", - "c:/dev/b.ts" - ], - wildcardDirectories: {}, - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); - }); - it("with non .ts file extensions are excluded", () => { - const json = { - include: [ - "a.js", - "b.js" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [ - createCompilerDiagnostic(Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, - caseInsensitiveTsconfigPath, JSON.stringify(json.include), "[]") - ], - fileNames: [], - wildcardDirectories: {}, - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, /*existingOptions*/ undefined, caseInsensitiveTsconfigPath); - }); - it("with missing files are excluded", () => { - const json = { - include: [ - "z.ts", - "x.ts" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [ - createCompilerDiagnostic(Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, - caseInsensitiveTsconfigPath, JSON.stringify(json.include), "[]") - ], - fileNames: [], - wildcardDirectories: {}, - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, /*existingOptions*/ undefined, caseInsensitiveTsconfigPath); - }); - it("with literal excludes", () => { - const json = { - include: [ - "a.ts", - "b.ts" - ], - exclude: [ - "b.ts" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/a.ts" - ], - wildcardDirectories: {}, - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); - }); - it("with wildcard excludes", () => { - const json = { - include: [ - "a.ts", - "b.ts", - "z/a.ts", - "z/abz.ts", - "z/aba.ts", - "x/b.ts" - ], - exclude: [ - "*.ts", - "z/??z.ts", - "*/b.ts" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/z/a.ts", - "c:/dev/z/aba.ts" - ], - wildcardDirectories: {}, - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); - }); - it("with recursive excludes", () => { - const json = { - include: [ - "a.ts", - "b.ts", - "x/a.ts", - "x/b.ts", - "x/y/a.ts", - "x/y/b.ts" - ], - exclude: [ - "**/b.ts" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/a.ts", - "c:/dev/x/a.ts", - "c:/dev/x/y/a.ts" - ], - wildcardDirectories: {}, - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); - }); - it("with case sensitive exclude", () => { - const json = { - include: [ - "B.ts" - ], - exclude: [ - "**/b.ts" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "/dev/B.ts" - ], - wildcardDirectories: {}, - }; - validateMatches(expected, json, caseSensitiveHost, caseSensitiveBasePath); - }); - it("with common package folders and no exclusions", () => { - const json = { - include: [ - "a.ts", - "b.ts", - "node_modules/a.ts", - "bower_components/a.ts", - "jspm_packages/a.ts" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/a.ts", - "c:/dev/b.ts", - "c:/dev/node_modules/a.ts", - "c:/dev/bower_components/a.ts", - "c:/dev/jspm_packages/a.ts" - ], - wildcardDirectories: {}, - }; - validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); - }); - it("with common package folders and exclusions", () => { - const json = { - include: [ - "a.ts", - "b.ts", - "node_modules/a.ts", - "bower_components/a.ts", - "jspm_packages/a.ts" - ], - exclude: [ - "a.ts", - "b.ts" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/node_modules/a.ts", - "c:/dev/bower_components/a.ts", - "c:/dev/jspm_packages/a.ts" - ], - wildcardDirectories: {}, - }; - validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); - }); - it("with common package folders and empty exclude", () => { - const json = { - include: [ - "a.ts", - "b.ts", - "node_modules/a.ts", - "bower_components/a.ts", - "jspm_packages/a.ts" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/a.ts", - "c:/dev/b.ts", - "c:/dev/node_modules/a.ts", - "c:/dev/bower_components/a.ts", - "c:/dev/jspm_packages/a.ts" - ], - wildcardDirectories: {}, - }; - validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); - }); + it("case sensitive", () => { + const json = { + include: [ + "**/A.ts" + ] + }; + const expected: ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "/dev/A.ts" + ], + wildcardDirectories: { + "/dev": WatchDirectoryFlags.Recursive + }, + }; + validateMatches(expected, json, caseSensitiveHost, caseSensitiveBasePath); + }); + it("with missing files are excluded", () => { + const json = { + include: [ + "*/z.ts" + ] + }; + const expected: ParsedCommandLine = { + options: {}, + errors: [ + createCompilerDiagnostic(Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, caseInsensitiveTsconfigPath, JSON.stringify(json.include), "[]") + ], + fileNames: [], + wildcardDirectories: { + "c:/dev": WatchDirectoryFlags.Recursive + }, + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, /*existingOptions*/ undefined, caseInsensitiveTsconfigPath); + }); + it("always include literal files", () => { + const json = { + files: [ + "a.ts" + ], + include: [ + "*/z.ts" + ], + exclude: [ + "**/a.ts" + ] + }; + const expected: ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/a.ts" + ], + wildcardDirectories: { + "c:/dev": WatchDirectoryFlags.Recursive + }, + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); - - describe("with wildcard include list", () => { - it("is sorted in include order, then in alphabetical order", () => { - const json = { - include: [ - "z/*.ts", - "x/*.ts" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/z/a.ts", - "c:/dev/z/aba.ts", - "c:/dev/z/abz.ts", - "c:/dev/z/b.ts", - "c:/dev/z/bba.ts", - "c:/dev/z/bbz.ts", - "c:/dev/x/a.ts", - "c:/dev/x/aa.ts", - "c:/dev/x/b.ts" - ], - wildcardDirectories: { - "c:/dev/z": WatchDirectoryFlags.None, - "c:/dev/x": WatchDirectoryFlags.None - }, - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); - }); - it("same named declarations are excluded", () => { + it("exclude folders", () => { + const json = { + include: [ + "**/*" + ], + exclude: [ + "z", + "x" + ] + }; + const expected: ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/a.ts", + "c:/dev/b.ts", + "c:/dev/c.d.ts" + ], + wildcardDirectories: { + "c:/dev": WatchDirectoryFlags.Recursive + } + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + describe("with common package folders", () => { + it("and no exclusions", () => { const json = { include: [ - "*.ts" + "**/a.ts" ] }; const expected: ParsedCommandLine = { @@ -484,58 +660,61 @@ namespace ts { errors: [], fileNames: [ "c:/dev/a.ts", - "c:/dev/b.ts", - "c:/dev/c.d.ts" + "c:/dev/x/a.ts" ], wildcardDirectories: { - "c:/dev": WatchDirectoryFlags.None + "c:/dev": WatchDirectoryFlags.Recursive }, }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); }); - it("`*` matches only ts files", () => { + it("and exclusions", () => { const json = { include: [ - "*" + "**/?.ts" + ], + exclude: [ + "a.ts" ] }; const expected: ParsedCommandLine = { options: {}, errors: [], fileNames: [ - "c:/dev/a.ts", "c:/dev/b.ts", - "c:/dev/c.d.ts" + "c:/dev/x/a.ts" ], wildcardDirectories: { - "c:/dev": WatchDirectoryFlags.None + "c:/dev": WatchDirectoryFlags.Recursive }, }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); }); - it("`?` matches only a single character", () => { + it("and empty exclude", () => { const json = { include: [ - "x/?.ts" - ] + "**/a.ts" + ], + exclude: [] }; const expected: ParsedCommandLine = { options: {}, errors: [], fileNames: [ - "c:/dev/x/a.ts", - "c:/dev/x/b.ts" + "c:/dev/a.ts", + "c:/dev/x/a.ts" ], wildcardDirectories: { - "c:/dev/x": WatchDirectoryFlags.None + "c:/dev": WatchDirectoryFlags.Recursive }, }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); }); - it("with recursive directory", () => { + it("and explicit recursive include", () => { const json = { include: [ - "**/a.ts" + "**/a.ts", + "**/node_modules/a.ts" ] }; const expected: ParsedCommandLine = { @@ -544,998 +723,788 @@ namespace ts { fileNames: [ "c:/dev/a.ts", "c:/dev/x/a.ts", - "c:/dev/x/y/a.ts", - "c:/dev/z/a.ts" + "c:/dev/node_modules/a.ts" ], wildcardDirectories: { "c:/dev": WatchDirectoryFlags.Recursive }, }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); }); - it("with multiple recursive directories", () => { + it("and wildcard include", () => { const json = { include: [ - "x/y/**/a.ts", - "x/**/a.ts", - "z/**/a.ts" + "*/a.ts" ] }; const expected: ParsedCommandLine = { options: {}, errors: [], fileNames: [ - "c:/dev/x/y/a.ts", - "c:/dev/x/a.ts", - "c:/dev/z/a.ts" + "c:/dev/x/a.ts" ], wildcardDirectories: { - "c:/dev/x": WatchDirectoryFlags.Recursive, - "c:/dev/z": WatchDirectoryFlags.Recursive + "c:/dev": WatchDirectoryFlags.Recursive }, }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); }); - it("case sensitive", () => { + it("and explicit wildcard include", () => { const json = { include: [ - "**/A.ts" + "*/a.ts", + "node_modules/a.ts" ] }; const expected: ParsedCommandLine = { options: {}, errors: [], fileNames: [ - "/dev/A.ts" - ], - wildcardDirectories: { - "/dev": WatchDirectoryFlags.Recursive - }, - }; - validateMatches(expected, json, caseSensitiveHost, caseSensitiveBasePath); - }); - it("with missing files are excluded", () => { - const json = { - include: [ - "*/z.ts" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [ - createCompilerDiagnostic(Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, - caseInsensitiveTsconfigPath, JSON.stringify(json.include), "[]") + "c:/dev/x/a.ts", + "c:/dev/node_modules/a.ts" ], - fileNames: [], wildcardDirectories: { "c:/dev": WatchDirectoryFlags.Recursive }, }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, /*existingOptions*/ undefined, caseInsensitiveTsconfigPath); + validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); }); - it("always include literal files", () => { - const json = { - files: [ - "a.ts" - ], + }); + it("exclude .js files when allowJs=false", () => { + const json = { + compilerOptions: { + allowJs: false + }, + include: [ + "js/*" + ] + }; + const expected: ParsedCommandLine = { + options: { + allowJs: false + }, + errors: [ + createCompilerDiagnostic(Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, caseInsensitiveTsconfigPath, JSON.stringify(json.include), "[]") + ], + fileNames: [], + wildcardDirectories: { + "c:/dev/js": WatchDirectoryFlags.None + } + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, /*existingOptions*/ undefined, caseInsensitiveTsconfigPath); + }); + it("include .js files when allowJs=true", () => { + const json = { + compilerOptions: { + allowJs: true + }, + include: [ + "js/*" + ] + }; + const expected: ParsedCommandLine = { + options: { + allowJs: true + }, + errors: [], + fileNames: [ + "c:/dev/js/a.js", + "c:/dev/js/b.js" + ], + wildcardDirectories: { + "c:/dev/js": WatchDirectoryFlags.None + } + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + it("include explicitly listed .min.js files when allowJs=true", () => { + const json = { + compilerOptions: { + allowJs: true + }, + include: [ + "js/*.min.js" + ] + }; + const expected: ParsedCommandLine = { + options: { + allowJs: true + }, + errors: [], + fileNames: [ + "c:/dev/js/ab.min.js", + "c:/dev/js/d.min.js" + ], + wildcardDirectories: { + "c:/dev/js": WatchDirectoryFlags.None + } + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + it("include paths outside of the project", () => { + const json = { + include: [ + "*", + "c:/ext/*" + ] + }; + const expected: ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/a.ts", + "c:/dev/b.ts", + "c:/dev/c.d.ts", + "c:/ext/ext.ts" + ], + wildcardDirectories: { + "c:/dev": WatchDirectoryFlags.None, + "c:/ext": WatchDirectoryFlags.None + } + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + it("include paths outside of the project using relative paths", () => { + const json = { + include: [ + "*", + "../ext/*" + ], + exclude: [ + "**" + ] + }; + const expected: ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/ext/ext.ts" + ], + wildcardDirectories: { + "c:/ext": WatchDirectoryFlags.None + } + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + it("exclude paths outside of the project using relative paths", () => { + const json = { + include: [ + "c:/**/*" + ], + exclude: [ + "../**" + ] + }; + const expected: ParsedCommandLine = { + options: {}, + errors: [ + createCompilerDiagnostic(Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, caseInsensitiveTsconfigPath, JSON.stringify(json.include), JSON.stringify(json.exclude)) + ], + fileNames: [], + wildcardDirectories: {} + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, /*existingOptions*/ undefined, caseInsensitiveTsconfigPath); + }); + it("include files with .. in their name", () => { + const json = { + include: [ + "c:/ext/b/a..b.ts" + ], + exclude: [ + "**" + ] + }; + const expected: ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/ext/b/a..b.ts" + ], + wildcardDirectories: {} + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + it("exclude files with .. in their name", () => { + const json = { + include: [ + "c:/ext/**/*" + ], + exclude: [ + "c:/ext/b/a..b.ts" + ] + }; + const expected: ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/ext/ext.ts", + ], + wildcardDirectories: { + "c:/ext": WatchDirectoryFlags.Recursive + } + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + it("with jsx=none, allowJs=false", () => { + const json = { + compilerOptions: { + allowJs: false + } + }; + const expected: ParsedCommandLine = { + options: { + allowJs: false + }, + errors: [], + fileNames: [ + "c:/dev/a.ts", + "c:/dev/b.tsx", + "c:/dev/c.tsx", + ], + wildcardDirectories: { + "c:/dev": WatchDirectoryFlags.Recursive + } + }; + validateMatches(expected, json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); + }); + it("with jsx=preserve, allowJs=false", () => { + const json = { + compilerOptions: { + jsx: "preserve", + allowJs: false + } + }; + const expected: ParsedCommandLine = { + options: { + jsx: JsxEmit.Preserve, + allowJs: false + }, + errors: [], + fileNames: [ + "c:/dev/a.ts", + "c:/dev/b.tsx", + "c:/dev/c.tsx", + ], + wildcardDirectories: { + "c:/dev": WatchDirectoryFlags.Recursive + } + }; + validateMatches(expected, json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); + }); + it("with jsx=react-native, allowJs=false", () => { + const json = { + compilerOptions: { + jsx: "react-native", + allowJs: false + } + }; + const expected: ParsedCommandLine = { + options: { + jsx: JsxEmit.ReactNative, + allowJs: false + }, + errors: [], + fileNames: [ + "c:/dev/a.ts", + "c:/dev/b.tsx", + "c:/dev/c.tsx", + ], + wildcardDirectories: { + "c:/dev": WatchDirectoryFlags.Recursive + } + }; + validateMatches(expected, json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); + }); + it("with jsx=none, allowJs=true", () => { + const json = { + compilerOptions: { + allowJs: true + } + }; + const expected: ParsedCommandLine = { + options: { + allowJs: true + }, + errors: [], + fileNames: [ + "c:/dev/a.ts", + "c:/dev/b.tsx", + "c:/dev/c.tsx", + "c:/dev/d.js", + "c:/dev/e.jsx", + ], + wildcardDirectories: { + "c:/dev": WatchDirectoryFlags.Recursive + } + }; + validateMatches(expected, json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); + }); + it("with jsx=preserve, allowJs=true", () => { + const json = { + compilerOptions: { + jsx: "preserve", + allowJs: true + } + }; + const expected: ParsedCommandLine = { + options: { + jsx: JsxEmit.Preserve, + allowJs: true + }, + errors: [], + fileNames: [ + "c:/dev/a.ts", + "c:/dev/b.tsx", + "c:/dev/c.tsx", + "c:/dev/d.js", + "c:/dev/e.jsx", + ], + wildcardDirectories: { + "c:/dev": WatchDirectoryFlags.Recursive + } + }; + validateMatches(expected, json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); + }); + it("with jsx=react-native, allowJs=true", () => { + const json = { + compilerOptions: { + jsx: "react-native", + allowJs: true + } + }; + const expected: ParsedCommandLine = { + options: { + jsx: JsxEmit.ReactNative, + allowJs: true + }, + errors: [], + fileNames: [ + "c:/dev/a.ts", + "c:/dev/b.tsx", + "c:/dev/c.tsx", + "c:/dev/d.js", + "c:/dev/e.jsx", + ], + wildcardDirectories: { + "c:/dev": WatchDirectoryFlags.Recursive + } + }; + validateMatches(expected, json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); + }); + it("exclude .min.js files using wildcards", () => { + const json = { + compilerOptions: { + allowJs: true + }, + include: [ + "js/*.min.js" + ], + exclude: [ + "js/a*" + ] + }; + const expected: ParsedCommandLine = { + options: { + allowJs: true + }, + errors: [], + fileNames: [ + "c:/dev/js/d.min.js" + ], + wildcardDirectories: { + "c:/dev/js": WatchDirectoryFlags.None + } + }; + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + }); + describe("with trailing recursive directory", () => { + it("in includes", () => { + const json = { include: [ - "*/z.ts" - ], - exclude: [ - "**/a.ts" + "**" ] }; const expected: ParsedCommandLine = { options: {}, - errors: [], - fileNames: [ - "c:/dev/a.ts" + errors: [ + createDiagnosticForConfigFile(json, 12, 4, Diagnostics.File_specification_cannot_end_in_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**"), + createCompilerDiagnostic(Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, caseInsensitiveTsconfigPath, JSON.stringify(json.include), "[]") ], - wildcardDirectories: { - "c:/dev": WatchDirectoryFlags.Recursive - }, + fileNames: [], + wildcardDirectories: {} }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, /*existingOptions*/ undefined, caseInsensitiveTsconfigPath); }); - it("exclude folders", () => { + it("in excludes", () => { const json = { include: [ "**/*" ], exclude: [ - "z", - "x" + "**" ] }; const expected: ParsedCommandLine = { options: {}, - errors: [], - fileNames: [ - "c:/dev/a.ts", - "c:/dev/b.ts", - "c:/dev/c.d.ts" - ], - wildcardDirectories: { - "c:/dev": WatchDirectoryFlags.Recursive - } - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); - }); - describe("with common package folders", () => { - it("and no exclusions", () => { - const json = { - include: [ - "**/a.ts" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/a.ts", - "c:/dev/x/a.ts" - ], - wildcardDirectories: { - "c:/dev": WatchDirectoryFlags.Recursive - }, - }; - validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); - }); - it("and exclusions", () => { - const json = { - include: [ - "**/?.ts" - ], - exclude: [ - "a.ts" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/b.ts", - "c:/dev/x/a.ts" - ], - wildcardDirectories: { - "c:/dev": WatchDirectoryFlags.Recursive - }, - }; - validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); - }); - it("and empty exclude", () => { - const json = { - include: [ - "**/a.ts" - ], - exclude: [] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/a.ts", - "c:/dev/x/a.ts" - ], - wildcardDirectories: { - "c:/dev": WatchDirectoryFlags.Recursive - }, - }; - validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); - }); - it("and explicit recursive include", () => { - const json = { - include: [ - "**/a.ts", - "**/node_modules/a.ts" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/a.ts", - "c:/dev/x/a.ts", - "c:/dev/node_modules/a.ts" - ], - wildcardDirectories: { - "c:/dev": WatchDirectoryFlags.Recursive - }, - }; - validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); - }); - it("and wildcard include", () => { - const json = { - include: [ - "*/a.ts" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/x/a.ts" - ], - wildcardDirectories: { - "c:/dev": WatchDirectoryFlags.Recursive - }, - }; - validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); - }); - it("and explicit wildcard include", () => { - const json = { - include: [ - "*/a.ts", - "node_modules/a.ts" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/x/a.ts", - "c:/dev/node_modules/a.ts" - ], - wildcardDirectories: { - "c:/dev": WatchDirectoryFlags.Recursive - }, - }; - validateMatches(expected, json, caseInsensitiveCommonFoldersHost, caseInsensitiveBasePath); - }); - }); - it("exclude .js files when allowJs=false", () => { - const json = { - compilerOptions: { - allowJs: false - }, - include: [ - "js/*" - ] - }; - const expected: ParsedCommandLine = { - options: { - allowJs: false - }, errors: [ - createCompilerDiagnostic(Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, - caseInsensitiveTsconfigPath, JSON.stringify(json.include), "[]") + createCompilerDiagnostic(Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, caseInsensitiveTsconfigPath, JSON.stringify(json.include), JSON.stringify(json.exclude)) ], fileNames: [], - wildcardDirectories: { - "c:/dev/js": WatchDirectoryFlags.None - } + wildcardDirectories: {} }; validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, /*existingOptions*/ undefined, caseInsensitiveTsconfigPath); }); - it("include .js files when allowJs=true", () => { - const json = { - compilerOptions: { - allowJs: true - }, - include: [ - "js/*" - ] - }; - const expected: ParsedCommandLine = { - options: { - allowJs: true - }, - errors: [], - fileNames: [ - "c:/dev/js/a.js", - "c:/dev/js/b.js" - ], - wildcardDirectories: { - "c:/dev/js": WatchDirectoryFlags.None - } - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); - }); - it("include explicitly listed .min.js files when allowJs=true", () => { - const json = { - compilerOptions: { - allowJs: true - }, - include: [ - "js/*.min.js" - ] - }; - const expected: ParsedCommandLine = { - options: { - allowJs: true - }, - errors: [], - fileNames: [ - "c:/dev/js/ab.min.js", - "c:/dev/js/d.min.js" - ], - wildcardDirectories: { - "c:/dev/js": WatchDirectoryFlags.None - } - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); - }); - it("include paths outside of the project", () => { + }); + describe("with multiple recursive directory patterns", () => { + it("in includes", () => { const json = { include: [ - "*", - "c:/ext/*" + "**/x/**/*" ] }; const expected: ParsedCommandLine = { options: {}, errors: [], fileNames: [ - "c:/dev/a.ts", - "c:/dev/b.ts", - "c:/dev/c.d.ts", - "c:/ext/ext.ts" + "c:/dev/x/a.ts", + "c:/dev/x/aa.ts", + "c:/dev/x/b.ts", + "c:/dev/x/y/a.ts", + "c:/dev/x/y/b.ts", ], wildcardDirectories: { - "c:/dev": WatchDirectoryFlags.None, - "c:/ext": WatchDirectoryFlags.None + "c:/dev": WatchDirectoryFlags.Recursive } }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, /*existingOptions*/ undefined, caseInsensitiveTsconfigPath); }); - it("include paths outside of the project using relative paths", () => { + it("in excludes", () => { const json = { include: [ - "*", - "../ext/*" + "**/a.ts" ], exclude: [ - "**" + "**/x/**" ] }; const expected: ParsedCommandLine = { options: {}, errors: [], fileNames: [ - "c:/ext/ext.ts" + "c:/dev/a.ts", + "c:/dev/z/a.ts" ], wildcardDirectories: { - "c:/ext": WatchDirectoryFlags.None + "c:/dev": WatchDirectoryFlags.Recursive } }; validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); - it("exclude paths outside of the project using relative paths", () => { + }); + describe("with parent directory symbols after a recursive directory pattern", () => { + it("in includes immediately after", () => { const json = { include: [ - "c:/**/*" - ], - exclude: [ - "../**" + "**/../*" ] }; const expected: ParsedCommandLine = { options: {}, errors: [ - createCompilerDiagnostic(Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, - caseInsensitiveTsconfigPath, JSON.stringify(json.include), JSON.stringify(json.exclude))] - , + createDiagnosticForConfigFile(json, 12, 9, Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/../*"), + createCompilerDiagnostic(Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, caseInsensitiveTsconfigPath, JSON.stringify(json.include), "[]") + ], fileNames: [], wildcardDirectories: {} }; validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, /*existingOptions*/ undefined, caseInsensitiveTsconfigPath); }); - it("include files with .. in their name", () => { + it("in includes after a subdirectory", () => { const json = { include: [ - "c:/ext/b/a..b.ts" - ], - exclude: [ - "**" + "**/y/../*" ] }; const expected: ParsedCommandLine = { options: {}, - errors: [], - fileNames: [ - "c:/ext/b/a..b.ts" + errors: [ + createDiagnosticForConfigFile(json, 12, 11, Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/y/../*"), + createCompilerDiagnostic(Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, caseInsensitiveTsconfigPath, JSON.stringify(json.include), "[]") ], + fileNames: [], wildcardDirectories: {} }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, /*existingOptions*/ undefined, caseInsensitiveTsconfigPath); }); - it("exclude files with .. in their name", () => { + it("in excludes immediately after", () => { const json = { include: [ - "c:/ext/**/*" + "**/a.ts" ], exclude: [ - "c:/ext/b/a..b.ts" + "**/.." ] }; const expected: ParsedCommandLine = { options: {}, - errors: [], - fileNames: [ - "c:/ext/ext.ts", - ], - wildcardDirectories: { - "c:/ext": WatchDirectoryFlags.Recursive - } - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); - }); - it("with jsx=none, allowJs=false", () => { - const json = { - compilerOptions: { - allowJs: false - } - }; - const expected: ParsedCommandLine = { - options: { - allowJs: false - }, - errors: [], - fileNames: [ - "c:/dev/a.ts", - "c:/dev/b.tsx", - "c:/dev/c.tsx", + errors: [ + createDiagnosticForConfigFile(json, 34, 7, Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/..") ], - wildcardDirectories: { - "c:/dev": WatchDirectoryFlags.Recursive - } - }; - validateMatches(expected, json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); - }); - it("with jsx=preserve, allowJs=false", () => { - const json = { - compilerOptions: { - jsx: "preserve", - allowJs: false - } - }; - const expected: ParsedCommandLine = { - options: { - jsx: JsxEmit.Preserve, - allowJs: false - }, - errors: [], fileNames: [ "c:/dev/a.ts", - "c:/dev/b.tsx", - "c:/dev/c.tsx", + "c:/dev/x/a.ts", + "c:/dev/x/y/a.ts", + "c:/dev/z/a.ts" ], wildcardDirectories: { "c:/dev": WatchDirectoryFlags.Recursive } }; - validateMatches(expected, json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); - it("with jsx=react-native, allowJs=false", () => { + it("in excludes after a subdirectory", () => { const json = { - compilerOptions: { - jsx: "react-native", - allowJs: false - } - }; - const expected: ParsedCommandLine = { - options: { - jsx: JsxEmit.ReactNative, - allowJs: false - }, - errors: [], - fileNames: [ - "c:/dev/a.ts", - "c:/dev/b.tsx", - "c:/dev/c.tsx", + include: [ + "**/a.ts" ], - wildcardDirectories: { - "c:/dev": WatchDirectoryFlags.Recursive - } - }; - validateMatches(expected, json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); - }); - it("with jsx=none, allowJs=true", () => { - const json = { - compilerOptions: { - allowJs: true - } + exclude: [ + "**/y/.." + ] }; const expected: ParsedCommandLine = { - options: { - allowJs: true - }, - errors: [], - fileNames: [ - "c:/dev/a.ts", - "c:/dev/b.tsx", - "c:/dev/c.tsx", - "c:/dev/d.js", - "c:/dev/e.jsx", + options: {}, + errors: [ + createDiagnosticForConfigFile(json, 34, 9, Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/y/..") ], - wildcardDirectories: { - "c:/dev": WatchDirectoryFlags.Recursive - } - }; - validateMatches(expected, json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); - }); - it("with jsx=preserve, allowJs=true", () => { - const json = { - compilerOptions: { - jsx: "preserve", - allowJs: true - } - }; - const expected: ParsedCommandLine = { - options: { - jsx: JsxEmit.Preserve, - allowJs: true - }, - errors: [], fileNames: [ "c:/dev/a.ts", - "c:/dev/b.tsx", - "c:/dev/c.tsx", - "c:/dev/d.js", - "c:/dev/e.jsx", + "c:/dev/x/a.ts", + "c:/dev/x/y/a.ts", + "c:/dev/z/a.ts" ], wildcardDirectories: { "c:/dev": WatchDirectoryFlags.Recursive } }; - validateMatches(expected, json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); - it("with jsx=react-native, allowJs=true", () => { + }); + describe("with implicit globbification", () => { + it("Expands 'z' to 'z/**/*'", () => { const json = { - compilerOptions: { - jsx: "react-native", - allowJs: true - } + include: ["z"] }; const expected: ParsedCommandLine = { - options: { - jsx: JsxEmit.ReactNative, - allowJs: true - }, + options: {}, errors: [], - fileNames: [ - "c:/dev/a.ts", - "c:/dev/b.tsx", - "c:/dev/c.tsx", - "c:/dev/d.js", - "c:/dev/e.jsx", - ], + fileNames: ["a.ts", "aba.ts", "abz.ts", "b.ts", "bba.ts", "bbz.ts"].map(x => `c:/dev/z/${x}`), wildcardDirectories: { - "c:/dev": WatchDirectoryFlags.Recursive + "c:/dev/z": WatchDirectoryFlags.Recursive } }; - validateMatches(expected, json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); + validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); }); - it("exclude .min.js files using wildcards", () => { + }); + }); + describe("with files or folders that begin with a .", () => { + it("that are not explicitly included", () => { + const json = { + include: [ + "x/**/*", + "w/*/*" + ] + }; + const expected: ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/x/d.ts", + "c:/dev/x/y/d.ts", + ], + wildcardDirectories: { + "c:/dev/x": WatchDirectoryFlags.Recursive, + "c:/dev/w": WatchDirectoryFlags.Recursive + } + }; + validateMatches(expected, json, caseInsensitiveDottedFoldersHost, caseInsensitiveBasePath); + }); + describe("that are explicitly included", () => { + it("without wildcards", () => { const json = { - compilerOptions: { - allowJs: true - }, include: [ - "js/*.min.js" - ], - exclude: [ - "js/a*" + "x/.y/a.ts", + "c:/dev/.z/.b.ts" ] }; const expected: ParsedCommandLine = { - options: { - allowJs: true - }, + options: {}, errors: [], fileNames: [ - "c:/dev/js/d.min.js" + "c:/dev/x/.y/a.ts", + "c:/dev/.z/.b.ts" ], - wildcardDirectories: { - "c:/dev/js": WatchDirectoryFlags.None - } + wildcardDirectories: {} }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); - }); - - describe("with trailing recursive directory", () => { - it("in includes", () => { - const json = { - include: [ - "**" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [ - createDiagnosticForConfigFile(json, 12, 4, Diagnostics.File_specification_cannot_end_in_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**"), - createCompilerDiagnostic(Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, - caseInsensitiveTsconfigPath, JSON.stringify(json.include), "[]") - ], - fileNames: [], - wildcardDirectories: {} - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, /*existingOptions*/ undefined, caseInsensitiveTsconfigPath); - }); - it("in excludes", () => { - const json = { - include: [ - "**/*" - ], - exclude: [ - "**" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [ - createCompilerDiagnostic(Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, - caseInsensitiveTsconfigPath, JSON.stringify(json.include), JSON.stringify(json.exclude)) - ], - fileNames: [], - wildcardDirectories: {} - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, /*existingOptions*/ undefined, caseInsensitiveTsconfigPath); - }); - }); - describe("with multiple recursive directory patterns", () => { - it("in includes", () => { - const json = { - include: [ - "**/x/**/*" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/x/a.ts", - "c:/dev/x/aa.ts", - "c:/dev/x/b.ts", - "c:/dev/x/y/a.ts", - "c:/dev/x/y/b.ts", - ], - wildcardDirectories: { - "c:/dev": WatchDirectoryFlags.Recursive - } - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, /*existingOptions*/ undefined, caseInsensitiveTsconfigPath); - }); - it("in excludes", () => { - const json = { - include: [ - "**/a.ts" - ], - exclude: [ - "**/x/**" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/a.ts", - "c:/dev/z/a.ts" - ], - wildcardDirectories: { - "c:/dev": WatchDirectoryFlags.Recursive - } - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); - }); - }); - - describe("with parent directory symbols after a recursive directory pattern", () => { - it("in includes immediately after", () => { - const json = { - include: [ - "**/../*" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [ - createDiagnosticForConfigFile(json, 12, 9, Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/../*"), - createCompilerDiagnostic(Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, - caseInsensitiveTsconfigPath, JSON.stringify(json.include), "[]") - ], - fileNames: [], - wildcardDirectories: {} - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, /*existingOptions*/ undefined, caseInsensitiveTsconfigPath); - }); - - it("in includes after a subdirectory", () => { - const json = { - include: [ - "**/y/../*" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [ - createDiagnosticForConfigFile(json, 12, 11, Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/y/../*"), - createCompilerDiagnostic(Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, - caseInsensitiveTsconfigPath, JSON.stringify(json.include), "[]") - ], - fileNames: [], - wildcardDirectories: {} - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath, /*existingOptions*/ undefined, caseInsensitiveTsconfigPath); - }); - - it("in excludes immediately after", () => { - const json = { - include: [ - "**/a.ts" - ], - exclude: [ - "**/.." - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [ - createDiagnosticForConfigFile(json, 34, 7, Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/..") - ], - fileNames: [ - "c:/dev/a.ts", - "c:/dev/x/a.ts", - "c:/dev/x/y/a.ts", - "c:/dev/z/a.ts" - ], - wildcardDirectories: { - "c:/dev": WatchDirectoryFlags.Recursive - } - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); - }); - - it("in excludes after a subdirectory", () => { - const json = { - include: [ - "**/a.ts" - ], - exclude: [ - "**/y/.." - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [ - createDiagnosticForConfigFile(json, 34, 9, Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/y/..") - ], - fileNames: [ - "c:/dev/a.ts", - "c:/dev/x/a.ts", - "c:/dev/x/y/a.ts", - "c:/dev/z/a.ts" - ], - wildcardDirectories: { - "c:/dev": WatchDirectoryFlags.Recursive - } - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); - }); - }); - - describe("with implicit globbification", () => { - it("Expands 'z' to 'z/**/*'", () => { - const json = { - include: ["z"] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ "a.ts", "aba.ts", "abz.ts", "b.ts", "bba.ts", "bbz.ts" ].map(x => `c:/dev/z/${x}`), - wildcardDirectories: { - "c:/dev/z": WatchDirectoryFlags.Recursive - } - }; - validateMatches(expected, json, caseInsensitiveHost, caseInsensitiveBasePath); - }); + validateMatches(expected, json, caseInsensitiveDottedFoldersHost, caseInsensitiveBasePath); }); - }); - - describe("with files or folders that begin with a .", () => { - it("that are not explicitly included", () => { + it("with recursive wildcards that match directories", () => { const json = { include: [ - "x/**/*", - "w/*/*" + "**/.*/*" ] }; const expected: ParsedCommandLine = { options: {}, errors: [], fileNames: [ - "c:/dev/x/d.ts", - "c:/dev/x/y/d.ts", + "c:/dev/.z/c.ts", + "c:/dev/g.min.js/.g/g.ts", + "c:/dev/w/.u/e.ts", + "c:/dev/x/.y/a.ts" ], wildcardDirectories: { - "c:/dev/x": WatchDirectoryFlags.Recursive, - "c:/dev/w": WatchDirectoryFlags.Recursive + "c:/dev": WatchDirectoryFlags.Recursive } }; validateMatches(expected, json, caseInsensitiveDottedFoldersHost, caseInsensitiveBasePath); }); - describe("that are explicitly included", () => { - it("without wildcards", () => { - const json = { - include: [ - "x/.y/a.ts", - "c:/dev/.z/.b.ts" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/x/.y/a.ts", - "c:/dev/.z/.b.ts" - ], - wildcardDirectories: {} - }; - validateMatches(expected, json, caseInsensitiveDottedFoldersHost, caseInsensitiveBasePath); - }); - it("with recursive wildcards that match directories", () => { - const json = { - include: [ - "**/.*/*" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/.z/c.ts", - "c:/dev/g.min.js/.g/g.ts", - "c:/dev/w/.u/e.ts", - "c:/dev/x/.y/a.ts" - ], - wildcardDirectories: { - "c:/dev": WatchDirectoryFlags.Recursive - } - }; - validateMatches(expected, json, caseInsensitiveDottedFoldersHost, caseInsensitiveBasePath); - }); - it("with recursive wildcards that match nothing", () => { - const json = { - include: [ - "x/**/.y/*", - ".z/**/.*" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [], - fileNames: [ - "c:/dev/x/.y/a.ts", - "c:/dev/.z/.b.ts" - ], - wildcardDirectories: { - "c:/dev/.z": WatchDirectoryFlags.Recursive, - "c:/dev/x": WatchDirectoryFlags.Recursive - } - }; - validateMatches(expected, json, caseInsensitiveDottedFoldersHost, caseInsensitiveBasePath); - }); - it("with wildcard excludes that implicitly exclude dotted files", () => { - const json = { - include: [ - "**/.*/*" - ], - exclude: [ - "**/*" - ] - }; - const expected: ParsedCommandLine = { - options: {}, - errors: [ - createCompilerDiagnostic(Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, - caseInsensitiveTsconfigPath, JSON.stringify(json.include), JSON.stringify(json.exclude)) - ], - fileNames: [], - wildcardDirectories: {} - }; - validateMatches(expected, json, caseInsensitiveDottedFoldersHost, caseInsensitiveBasePath, /*existingOptions*/ undefined, caseInsensitiveTsconfigPath); - }); - }); - }); - - describe("exclude or include patterns which start with **", () => { - it("can exclude dirs whose pattern starts with **", () => { + it("with recursive wildcards that match nothing", () => { const json = { - exclude: [ - "**/x" + include: [ + "x/**/.y/*", + ".z/**/.*" ] }; const expected: ParsedCommandLine = { options: {}, errors: [], fileNames: [ - "/dev/A.ts", - "/dev/B.ts", - "/dev/a.ts", - "/dev/b.ts", - "/dev/c.d.ts", - "/dev/q/a/c/b/d.ts", - "/dev/z/a.ts", - "/dev/z/aba.ts", - "/dev/z/abz.ts", - "/dev/z/b.ts", - "/dev/z/bba.ts", - "/dev/z/bbz.ts", + "c:/dev/x/.y/a.ts", + "c:/dev/.z/.b.ts" ], wildcardDirectories: { - "/dev": WatchDirectoryFlags.Recursive + "c:/dev/.z": WatchDirectoryFlags.Recursive, + "c:/dev/x": WatchDirectoryFlags.Recursive } }; - validateMatches(expected, json, caseSensitiveHost, caseSensitiveBasePath); + validateMatches(expected, json, caseInsensitiveDottedFoldersHost, caseInsensitiveBasePath); }); - it("can include dirs whose pattern starts with **", () => { + it("with wildcard excludes that implicitly exclude dotted files", () => { const json = { include: [ - "**/x", - "**/a/**/b" + "**/.*/*" + ], + exclude: [ + "**/*" ] }; const expected: ParsedCommandLine = { options: {}, - errors: [], - fileNames: [ - "/dev/x/a.ts", - "/dev/x/b.ts", - "/dev/x/y/a.ts", - "/dev/x/y/b.ts", - "/dev/q/a/c/b/d.ts", + errors: [ + createCompilerDiagnostic(Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, caseInsensitiveTsconfigPath, JSON.stringify(json.include), JSON.stringify(json.exclude)) ], - wildcardDirectories: { - "/dev": WatchDirectoryFlags.Recursive - } + fileNames: [], + wildcardDirectories: {} }; - validateMatches(expected, json, caseSensitiveHost, caseSensitiveBasePath); + validateMatches(expected, json, caseInsensitiveDottedFoldersHost, caseInsensitiveBasePath, /*existingOptions*/ undefined, caseInsensitiveTsconfigPath); }); }); - - it("can include files in the same order on multiple platforms", () => { - function getExpected(basePath: string): ParsedCommandLine { - return { - options: {}, - errors: [], - fileNames: [ - `${basePath}Yosemite.ts`, // capital always comes before lowercase letters - `${basePath}xylophone.ts`, - `${basePath}zebra.ts` - ], - wildcardDirectories: { - [basePath.slice(0, basePath.length - 1)]: WatchDirectoryFlags.Recursive - }, - }; - } - const json = {}; - validateMatches(getExpected(caseSensitiveBasePath), json, caseSensitiveOrderingDiffersWithCaseHost, caseSensitiveBasePath); - validateMatches(getExpected(caseInsensitiveBasePath), json, caseInsensitiveOrderingDiffersWithCaseHost, caseInsensitiveBasePath); - }); - - it("when recursive symlinked directories are present", () => { - const fs = new vfs.FileSystem(/*ignoreCase*/ true, { - cwd: caseInsensitiveBasePath, files: { - "c:/dev/index.ts": "" + }); + describe("exclude or include patterns which start with **", () => { + it("can exclude dirs whose pattern starts with **", () => { + const json = { + exclude: [ + "**/x" + ] + }; + const expected: ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "/dev/A.ts", + "/dev/B.ts", + "/dev/a.ts", + "/dev/b.ts", + "/dev/c.d.ts", + "/dev/q/a/c/b/d.ts", + "/dev/z/a.ts", + "/dev/z/aba.ts", + "/dev/z/abz.ts", + "/dev/z/b.ts", + "/dev/z/bba.ts", + "/dev/z/bbz.ts", + ], + wildcardDirectories: { + "/dev": WatchDirectoryFlags.Recursive } - }); - fs.mkdirpSync("c:/dev/a/b/c"); - fs.symlinkSync("c:/dev/A", "c:/dev/a/self"); - fs.symlinkSync("c:/dev/a", "c:/dev/a/b/parent"); - fs.symlinkSync("c:/dev/a", "c:/dev/a/b/c/grandparent"); - const host = new fakes.ParseConfigHost(fs); - const json = {}; + }; + validateMatches(expected, json, caseSensitiveHost, caseSensitiveBasePath); + }); + it("can include dirs whose pattern starts with **", () => { + const json = { + include: [ + "**/x", + "**/a/**/b" + ] + }; const expected: ParsedCommandLine = { options: {}, errors: [], fileNames: [ - "c:/dev/index.ts" + "/dev/x/a.ts", + "/dev/x/b.ts", + "/dev/x/y/a.ts", + "/dev/x/y/b.ts", + "/dev/q/a/c/b/d.ts", ], wildcardDirectories: { - "c:/dev": WatchDirectoryFlags.Recursive + "/dev": WatchDirectoryFlags.Recursive + } + }; + validateMatches(expected, json, caseSensitiveHost, caseSensitiveBasePath); + }); + }); + it("can include files in the same order on multiple platforms", () => { + function getExpected(basePath: string): ParsedCommandLine { + return { + options: {}, + errors: [], + fileNames: [ + `${basePath}Yosemite.ts`, + `${basePath}xylophone.ts`, + `${basePath}zebra.ts` + ], + wildcardDirectories: { + [basePath.slice(0, basePath.length - 1)]: WatchDirectoryFlags.Recursive }, }; - validateMatches(expected, json, host, caseInsensitiveBasePath); + } + const json = {}; + validateMatches(getExpected(caseSensitiveBasePath), json, caseSensitiveOrderingDiffersWithCaseHost, caseSensitiveBasePath); + validateMatches(getExpected(caseInsensitiveBasePath), json, caseInsensitiveOrderingDiffersWithCaseHost, caseInsensitiveBasePath); + }); + it("when recursive symlinked directories are present", () => { + const fs = new FileSystem(/*ignoreCase*/ true, { + cwd: caseInsensitiveBasePath, files: { + "c:/dev/index.ts": "" + } }); + fs.mkdirpSync("c:/dev/a/b/c"); + fs.symlinkSync("c:/dev/A", "c:/dev/a/self"); + fs.symlinkSync("c:/dev/a", "c:/dev/a/b/parent"); + fs.symlinkSync("c:/dev/a", "c:/dev/a/b/c/grandparent"); + const host = new ParseConfigHost(fs); + const json = {}; + const expected: ParsedCommandLine = { + options: {}, + errors: [], + fileNames: [ + "c:/dev/index.ts" + ], + wildcardDirectories: { + "c:/dev": WatchDirectoryFlags.Recursive + }, + }; + validateMatches(expected, json, host, caseInsensitiveBasePath); }); -} +}); diff --git a/src/testRunner/unittests/config/projectReferences.ts b/src/testRunner/unittests/config/projectReferences.ts index 6f9d3b3b24951..e3af6746f71a1 100644 --- a/src/testRunner/unittests/config/projectReferences.ts +++ b/src/testRunner/unittests/config/projectReferences.ts @@ -1,352 +1,333 @@ -namespace ts { - interface TestProjectSpecification { - configFileName?: string; - references?: readonly (string | ProjectReference)[]; - files: { [fileName: string]: string }; - outputFiles?: { [fileName: string]: string }; - config?: object; - options?: Partial; +import { ProjectReference, CompilerOptions, Diagnostic, DiagnosticMessage, combinePaths, Program, createMap, TestFSWithWatch, getDirectoryPath, readConfigFile, flattenDiagnosticMessageText, parseJsonConfigFileContent, parseConfigHostFromCompilerHostLike, createProgram, Diagnostics } from "../../ts"; +import { CompilerHost, System } from "../../fakes"; +import { FileSystem } from "../../vfs"; +interface TestProjectSpecification { + configFileName?: string; + references?: readonly (string | ProjectReference)[]; + files: { + [fileName: string]: string; + }; + outputFiles?: { + [fileName: string]: string; + }; + config?: object; + options?: Partial; +} +interface TestSpecification { + [path: string]: TestProjectSpecification; +} +function assertHasError(message: string, errors: readonly Diagnostic[], diag: DiagnosticMessage) { + if (!errors.some(e => e.code === diag.code)) { + const errorString = errors.map(e => ` ${e.file ? e.file.fileName : "[global]"}: ${e.messageText}`).join("\r\n"); + assert(false, `${message}: Did not find any diagnostic for ${diag.message} in:\r\n${errorString}`); } - interface TestSpecification { - [path: string]: TestProjectSpecification; +} +function assertNoErrors(message: string, errors: readonly Diagnostic[]) { + if (errors && errors.length > 0) { + assert(false, `${message}: Expected no errors, but found:\r\n${errors.map(e => ` ${e.messageText}`).join("\r\n")}`); } - - function assertHasError(message: string, errors: readonly Diagnostic[], diag: DiagnosticMessage) { - if (!errors.some(e => e.code === diag.code)) { - const errorString = errors.map(e => ` ${e.file ? e.file.fileName : "[global]"}: ${e.messageText}`).join("\r\n"); - assert(false, `${message}: Did not find any diagnostic for ${diag.message} in:\r\n${errorString}`); - } +} +function combineAllPaths(...paths: string[]) { + let result = paths[0]; + for (let i = 1; i < paths.length; i++) { + result = combinePaths(result, paths[i]); } - - function assertNoErrors(message: string, errors: readonly Diagnostic[]) { - if (errors && errors.length > 0) { - assert(false, `${message}: Expected no errors, but found:\r\n${errors.map(e => ` ${e.messageText}`).join("\r\n")}`); + return result; +} +const emptyModule = "export { };"; +/** + * Produces the text of a source file which imports all of the + * specified module names + */ +function moduleImporting(...names: string[]) { + return names.map((n, i) => `import * as mod_${i} from ${n}`).join("\r\n"); +} +function testProjectReferences(spec: TestSpecification, entryPointConfigFileName: string, checkResult: (prog: Program, host: CompilerHost) => void) { + const files = createMap(); + for (const key in spec) { + const sp = spec[key]; + const configFileName = combineAllPaths("/", key, sp.configFileName || "tsconfig.json"); + const options = { + compilerOptions: { + composite: true, + outDir: "bin", + ...sp.options + }, + references: (sp.references || []).map(r => { + if (typeof r === "string") { + return { path: r }; + } + return r; + }), + ...sp.config + }; + const configContent = JSON.stringify(options); + const outDir = options.compilerOptions.outDir; + files.set(configFileName, configContent); + for (const sourceFile of Object.keys(sp.files)) { + files.set(sourceFile, sp.files[sourceFile]); } - } - - function combineAllPaths(...paths: string[]) { - let result = paths[0]; - for (let i = 1; i < paths.length; i++) { - result = combinePaths(result, paths[i]); + if (sp.outputFiles) { + for (const outFile of Object.keys(sp.outputFiles)) { + files.set(combineAllPaths("/", key, outDir, outFile), sp.outputFiles[outFile]); + } } - return result; - } - - const emptyModule = "export { };"; - - /** - * Produces the text of a source file which imports all of the - * specified module names - */ - function moduleImporting(...names: string[]) { - return names.map((n, i) => `import * as mod_${i} from ${n}`).join("\r\n"); } - - function testProjectReferences(spec: TestSpecification, entryPointConfigFileName: string, checkResult: (prog: Program, host: fakes.CompilerHost) => void) { - const files = createMap(); - for (const key in spec) { - const sp = spec[key]; - const configFileName = combineAllPaths("/", key, sp.configFileName || "tsconfig.json"); - const options = { - compilerOptions: { - composite: true, - outDir: "bin", - ...sp.options - }, - references: (sp.references || []).map(r => { - if (typeof r === "string") { - return { path: r }; - } - return r; - }), - ...sp.config - }; - const configContent = JSON.stringify(options); - const outDir = options.compilerOptions.outDir; - files.set(configFileName, configContent); - for (const sourceFile of Object.keys(sp.files)) { - files.set(sourceFile, sp.files[sourceFile]); - } - if (sp.outputFiles) { - for (const outFile of Object.keys(sp.outputFiles)) { - files.set(combineAllPaths("/", key, outDir, outFile), sp.outputFiles[outFile]); - } + const vfsys = new FileSystem(false, { files: { "/lib.d.ts": TestFSWithWatch.libFile.content } }); + files.forEach((v, k) => { + vfsys.mkdirpSync(getDirectoryPath(k)); + vfsys.writeFileSync(k, v); + }); + const host = new CompilerHost(new System(vfsys)); + const { config, error } = readConfigFile(entryPointConfigFileName, name => host.readFile(name)); + // We shouldn't have any errors about invalid tsconfig files in these tests + assert(config && !error, flattenDiagnosticMessageText(error && error.messageText, "\n")); + const file = parseJsonConfigFileContent(config, parseConfigHostFromCompilerHostLike(host), getDirectoryPath(entryPointConfigFileName), {}, entryPointConfigFileName); + file.options.configFilePath = entryPointConfigFileName; + const prog = createProgram({ + rootNames: file.fileNames, + options: file.options, + host, + projectReferences: file.projectReferences + }); + checkResult(prog, host); +} +describe("unittests:: config:: project-references meta check", () => { + it("default setup was created correctly", () => { + const spec: TestSpecification = { + "/primary": { + files: { "/primary/a.ts": emptyModule }, + references: [] + }, + "/reference": { + files: { "/secondary/b.ts": moduleImporting("../primary/a") }, + references: ["../primary"] } - } - - const vfsys = new vfs.FileSystem(false, { files: { "/lib.d.ts": TestFSWithWatch.libFile.content } }); - files.forEach((v, k) => { - vfsys.mkdirpSync(getDirectoryPath(k)); - vfsys.writeFileSync(k, v); + }; + testProjectReferences(spec, "/primary/tsconfig.json", prog => { + assert.isTrue(!!prog, "Program should exist"); + assertNoErrors("Sanity check should not produce errors", prog.getOptionsDiagnostics()); }); - const host = new fakes.CompilerHost(new fakes.System(vfsys)); - - const { config, error } = readConfigFile(entryPointConfigFileName, name => host.readFile(name)); - - // We shouldn't have any errors about invalid tsconfig files in these tests - assert(config && !error, flattenDiagnosticMessageText(error && error.messageText, "\n")); - const file = parseJsonConfigFileContent(config, parseConfigHostFromCompilerHostLike(host), getDirectoryPath(entryPointConfigFileName), {}, entryPointConfigFileName); - file.options.configFilePath = entryPointConfigFileName; - const prog = createProgram({ - rootNames: file.fileNames, - options: file.options, - host, - projectReferences: file.projectReferences - }); - checkResult(prog, host); - } - - describe("unittests:: config:: project-references meta check", () => { - it("default setup was created correctly", () => { - const spec: TestSpecification = { - "/primary": { - files: { "/primary/a.ts": emptyModule }, - references: [] - }, - "/reference": { - files: { "/secondary/b.ts": moduleImporting("../primary/a") }, - references: ["../primary"] + }); +}); +/** + * Validate that we enforce the basic settings constraints for referenced projects + */ +describe("unittests:: config:: project-references constraint checking for settings", () => { + it("errors when declaration = false", () => { + const spec: TestSpecification = { + "/primary": { + files: { "/primary/a.ts": emptyModule }, + references: [], + options: { + declaration: false } - }; - testProjectReferences(spec, "/primary/tsconfig.json", prog => { - assert.isTrue(!!prog, "Program should exist"); - assertNoErrors("Sanity check should not produce errors", prog.getOptionsDiagnostics()); - }); + } + }; + testProjectReferences(spec, "/primary/tsconfig.json", program => { + const errs = program.getOptionsDiagnostics(); + assertHasError("Reports an error about the wrong decl setting", errs, Diagnostics.Composite_projects_may_not_disable_declaration_emit); }); }); - - /** - * Validate that we enforce the basic settings constraints for referenced projects - */ - describe("unittests:: config:: project-references constraint checking for settings", () => { - it("errors when declaration = false", () => { - const spec: TestSpecification = { - "/primary": { - files: { "/primary/a.ts": emptyModule }, - references: [], - options: { - declaration: false - } + it("errors when the referenced project doesn't have composite:true", () => { + const spec: TestSpecification = { + "/primary": { + files: { "/primary/a.ts": emptyModule }, + references: [], + options: { + composite: false } - }; - - testProjectReferences(spec, "/primary/tsconfig.json", program => { - const errs = program.getOptionsDiagnostics(); - assertHasError("Reports an error about the wrong decl setting", errs, Diagnostics.Composite_projects_may_not_disable_declaration_emit); - }); - }); - - it("errors when the referenced project doesn't have composite:true", () => { - const spec: TestSpecification = { - "/primary": { - files: { "/primary/a.ts": emptyModule }, - references: [], - options: { - composite: false - } - }, - "/reference": { - files: { "/secondary/b.ts": moduleImporting("../primary/a") }, - references: ["../primary"], - config: { - files: ["b.ts"] - } + }, + "/reference": { + files: { "/secondary/b.ts": moduleImporting("../primary/a") }, + references: ["../primary"], + config: { + files: ["b.ts"] } - }; - testProjectReferences(spec, "/reference/tsconfig.json", program => { - const errs = program.getOptionsDiagnostics(); - assertHasError("Reports an error about 'composite' not being set", errs, Diagnostics.Referenced_project_0_must_have_setting_composite_Colon_true); - }); + } + }; + testProjectReferences(spec, "/reference/tsconfig.json", program => { + const errs = program.getOptionsDiagnostics(); + assertHasError("Reports an error about 'composite' not being set", errs, Diagnostics.Referenced_project_0_must_have_setting_composite_Colon_true); }); - - it("does not error when the referenced project doesn't have composite:true if its a container project", () => { - const spec: TestSpecification = { - "/primary": { - files: { "/primary/a.ts": emptyModule }, - references: [], - options: { - composite: false - } - }, - "/reference": { - files: { "/secondary/b.ts": moduleImporting("../primary/a") }, - references: ["../primary"], + }); + it("does not error when the referenced project doesn't have composite:true if its a container project", () => { + const spec: TestSpecification = { + "/primary": { + files: { "/primary/a.ts": emptyModule }, + references: [], + options: { + composite: false } - }; - testProjectReferences(spec, "/reference/tsconfig.json", program => { - const errs = program.getOptionsDiagnostics(); - assertNoErrors("Reports an error about 'composite' not being set", errs); - }); + }, + "/reference": { + files: { "/secondary/b.ts": moduleImporting("../primary/a") }, + references: ["../primary"], + } + }; + testProjectReferences(spec, "/reference/tsconfig.json", program => { + const errs = program.getOptionsDiagnostics(); + assertNoErrors("Reports an error about 'composite' not being set", errs); }); - - it("errors when the file list is not exhaustive", () => { - const spec: TestSpecification = { - "/primary": { - files: { - "/primary/a.ts": "import * as b from './b'", - "/primary/b.ts": "export {}" - }, - config: { - files: ["a.ts"] - } + }); + it("errors when the file list is not exhaustive", () => { + const spec: TestSpecification = { + "/primary": { + files: { + "/primary/a.ts": "import * as b from './b'", + "/primary/b.ts": "export {}" + }, + config: { + files: ["a.ts"] } - }; - - testProjectReferences(spec, "/primary/tsconfig.json", program => { - const errs = program.getSemanticDiagnostics(program.getSourceFile("/primary/a.ts")); - assertHasError("Reports an error about b.ts not being in the list", errs, Diagnostics.File_0_is_not_listed_within_the_file_list_of_project_1_Projects_must_list_all_files_or_use_an_include_pattern); - }); + } + }; + testProjectReferences(spec, "/primary/tsconfig.json", program => { + const errs = program.getSemanticDiagnostics(program.getSourceFile("/primary/a.ts")); + assertHasError("Reports an error about b.ts not being in the list", errs, Diagnostics.File_0_is_not_listed_within_the_file_list_of_project_1_Projects_must_list_all_files_or_use_an_include_pattern); }); - - it("errors when the referenced project doesn't exist", () => { - const spec: TestSpecification = { - "/primary": { - files: { "/primary/a.ts": emptyModule }, - references: ["../foo"] - } - }; - testProjectReferences(spec, "/primary/tsconfig.json", program => { - const errs = program.getOptionsDiagnostics(); - assertHasError("Reports an error about a missing file", errs, Diagnostics.File_0_not_found); - }); + }); + it("errors when the referenced project doesn't exist", () => { + const spec: TestSpecification = { + "/primary": { + files: { "/primary/a.ts": emptyModule }, + references: ["../foo"] + } + }; + testProjectReferences(spec, "/primary/tsconfig.json", program => { + const errs = program.getOptionsDiagnostics(); + assertHasError("Reports an error about a missing file", errs, Diagnostics.File_0_not_found); }); - - it("errors when a prepended project reference doesn't set outFile", () => { - const spec: TestSpecification = { - "/primary": { - files: { "/primary/a.ts": emptyModule }, - references: [{ path: "../someProj", prepend: true }] - }, - "/someProj": { - files: { "/someProj/b.ts": "const x = 100;" } - } - }; - testProjectReferences(spec, "/primary/tsconfig.json", program => { - const errs = program.getOptionsDiagnostics(); - assertHasError("Reports an error about outFile not being set", errs, Diagnostics.Cannot_prepend_project_0_because_it_does_not_have_outFile_set); - }); + }); + it("errors when a prepended project reference doesn't set outFile", () => { + const spec: TestSpecification = { + "/primary": { + files: { "/primary/a.ts": emptyModule }, + references: [{ path: "../someProj", prepend: true }] + }, + "/someProj": { + files: { "/someProj/b.ts": "const x = 100;" } + } + }; + testProjectReferences(spec, "/primary/tsconfig.json", program => { + const errs = program.getOptionsDiagnostics(); + assertHasError("Reports an error about outFile not being set", errs, Diagnostics.Cannot_prepend_project_0_because_it_does_not_have_outFile_set); }); - - it("errors when a prepended project reference output doesn't exist", () => { - const spec: TestSpecification = { - "/primary": { - files: { "/primary/a.ts": "const y = x;" }, - references: [{ path: "../someProj", prepend: true }] - }, - "/someProj": { - files: { "/someProj/b.ts": "const x = 100;" }, - options: { outFile: "foo.js" } - } - }; - testProjectReferences(spec, "/primary/tsconfig.json", program => { - const errs = program.getOptionsDiagnostics(); - assertHasError("Reports an error about outFile being missing", errs, Diagnostics.Output_file_0_from_project_1_does_not_exist); - }); + }); + it("errors when a prepended project reference output doesn't exist", () => { + const spec: TestSpecification = { + "/primary": { + files: { "/primary/a.ts": "const y = x;" }, + references: [{ path: "../someProj", prepend: true }] + }, + "/someProj": { + files: { "/someProj/b.ts": "const x = 100;" }, + options: { outFile: "foo.js" } + } + }; + testProjectReferences(spec, "/primary/tsconfig.json", program => { + const errs = program.getOptionsDiagnostics(); + assertHasError("Reports an error about outFile being missing", errs, Diagnostics.Output_file_0_from_project_1_does_not_exist); }); }); - - /** - * Path mapping behavior - */ - describe("unittests:: config:: project-references path mapping", () => { - it("redirects to the output .d.ts file", () => { - const spec: TestSpecification = { - "/alpha": { - files: { "/alpha/a.ts": "export const m: number = 3;" }, - references: [], - outputFiles: { "a.d.ts": emptyModule } - }, - "/beta": { - files: { "/beta/b.ts": "import { m } from '../alpha/a'" }, - references: ["../alpha"] - } - }; - testProjectReferences(spec, "/beta/tsconfig.json", program => { - assertNoErrors("File setup should be correct", program.getOptionsDiagnostics()); - assertHasError("Found a type error", program.getSemanticDiagnostics(), Diagnostics.Module_0_has_no_exported_member_1); - }); +}); +/** + * Path mapping behavior + */ +describe("unittests:: config:: project-references path mapping", () => { + it("redirects to the output .d.ts file", () => { + const spec: TestSpecification = { + "/alpha": { + files: { "/alpha/a.ts": "export const m: number = 3;" }, + references: [], + outputFiles: { "a.d.ts": emptyModule } + }, + "/beta": { + files: { "/beta/b.ts": "import { m } from '../alpha/a'" }, + references: ["../alpha"] + } + }; + testProjectReferences(spec, "/beta/tsconfig.json", program => { + assertNoErrors("File setup should be correct", program.getOptionsDiagnostics()); + assertHasError("Found a type error", program.getSemanticDiagnostics(), Diagnostics.Module_0_has_no_exported_member_1); }); }); - - describe("unittests:: config:: project-references nice-behavior", () => { - it("issues a nice error when the input file is missing", () => { - const spec: TestSpecification = { - "/alpha": { - files: { "/alpha/a.ts": "export const m: number = 3;" }, - references: [] - }, - "/beta": { - files: { "/beta/b.ts": "import { m } from '../alpha/a'" }, - references: ["../alpha"] - } - }; - testProjectReferences(spec, "/beta/tsconfig.json", program => { - assertHasError("Issues a useful error", program.getSemanticDiagnostics(), Diagnostics.Output_file_0_has_not_been_built_from_source_file_1); - }); +}); +describe("unittests:: config:: project-references nice-behavior", () => { + it("issues a nice error when the input file is missing", () => { + const spec: TestSpecification = { + "/alpha": { + files: { "/alpha/a.ts": "export const m: number = 3;" }, + references: [] + }, + "/beta": { + files: { "/beta/b.ts": "import { m } from '../alpha/a'" }, + references: ["../alpha"] + } + }; + testProjectReferences(spec, "/beta/tsconfig.json", program => { + assertHasError("Issues a useful error", program.getSemanticDiagnostics(), Diagnostics.Output_file_0_has_not_been_built_from_source_file_1); }); - - it("issues a nice error when the input file is missing when module reference is not relative", () => { - const spec: TestSpecification = { - "/alpha": { - files: { "/alpha/a.ts": "export const m: number = 3;" }, - references: [] - }, - "/beta": { - files: { "/beta/b.ts": "import { m } from '@alpha/a'" }, - references: ["../alpha"], - options: { - baseUrl: "./", - paths: { - "@alpha/*": ["/alpha/*"] - } + }); + it("issues a nice error when the input file is missing when module reference is not relative", () => { + const spec: TestSpecification = { + "/alpha": { + files: { "/alpha/a.ts": "export const m: number = 3;" }, + references: [] + }, + "/beta": { + files: { "/beta/b.ts": "import { m } from '@alpha/a'" }, + references: ["../alpha"], + options: { + baseUrl: "./", + paths: { + "@alpha/*": ["/alpha/*"] } } - }; - testProjectReferences(spec, "/beta/tsconfig.json", program => { - assertHasError("Issues a useful error", program.getSemanticDiagnostics(), Diagnostics.Output_file_0_has_not_been_built_from_source_file_1); - }); + } + }; + testProjectReferences(spec, "/beta/tsconfig.json", program => { + assertHasError("Issues a useful error", program.getSemanticDiagnostics(), Diagnostics.Output_file_0_has_not_been_built_from_source_file_1); }); }); - - /** - * 'composite' behavior - */ - describe("unittests:: config:: project-references behavior changes under composite: true", () => { - it("doesn't infer the rootDir from source paths", () => { - const spec: TestSpecification = { - "/alpha": { - files: { "/alpha/src/a.ts": "export const m: number = 3;" }, - options: { - declaration: true, - outDir: "bin" - }, - references: [] - } - }; - testProjectReferences(spec, "/alpha/tsconfig.json", (program, host) => { - program.emit(); - assert.deepEqual(host.outputs.map(e => e.file).sort(), ["/alpha/bin/src/a.d.ts", "/alpha/bin/src/a.js", "/alpha/bin/tsconfig.tsbuildinfo"]); - }); +}); +/** + * 'composite' behavior + */ +describe("unittests:: config:: project-references behavior changes under composite: true", () => { + it("doesn't infer the rootDir from source paths", () => { + const spec: TestSpecification = { + "/alpha": { + files: { "/alpha/src/a.ts": "export const m: number = 3;" }, + options: { + declaration: true, + outDir: "bin" + }, + references: [] + } + }; + testProjectReferences(spec, "/alpha/tsconfig.json", (program, host) => { + program.emit(); + assert.deepEqual(host.outputs.map(e => e.file).sort(), ["/alpha/bin/src/a.d.ts", "/alpha/bin/src/a.js", "/alpha/bin/tsconfig.tsbuildinfo"]); }); }); - - describe("unittests:: config:: project-references errors when a file in a composite project occurs outside the root", () => { - it("Errors when a file is outside the rootdir", () => { - const spec: TestSpecification = { - "/alpha": { - files: { "/alpha/src/a.ts": "import * from '../../beta/b'", "/beta/b.ts": "export { }" }, - options: { - declaration: true, - outDir: "bin" - }, - references: [] - } - }; - testProjectReferences(spec, "/alpha/tsconfig.json", (program) => { - const semanticDiagnostics = program.getSemanticDiagnostics(program.getSourceFile("/alpha/src/a.ts")); - assertHasError("Issues an error about the rootDir", semanticDiagnostics, Diagnostics.File_0_is_not_under_rootDir_1_rootDir_is_expected_to_contain_all_source_files); - assertHasError("Issues an error about the fileList", semanticDiagnostics, Diagnostics.File_0_is_not_listed_within_the_file_list_of_project_1_Projects_must_list_all_files_or_use_an_include_pattern); - }); +}); +describe("unittests:: config:: project-references errors when a file in a composite project occurs outside the root", () => { + it("Errors when a file is outside the rootdir", () => { + const spec: TestSpecification = { + "/alpha": { + files: { "/alpha/src/a.ts": "import * from '../../beta/b'", "/beta/b.ts": "export { }" }, + options: { + declaration: true, + outDir: "bin" + }, + references: [] + } + }; + testProjectReferences(spec, "/alpha/tsconfig.json", (program) => { + const semanticDiagnostics = program.getSemanticDiagnostics(program.getSourceFile("/alpha/src/a.ts")); + assertHasError("Issues an error about the rootDir", semanticDiagnostics, Diagnostics.File_0_is_not_under_rootDir_1_rootDir_is_expected_to_contain_all_source_files); + assertHasError("Issues an error about the fileList", semanticDiagnostics, Diagnostics.File_0_is_not_listed_within_the_file_list_of_project_1_Projects_must_list_all_files_or_use_an_include_pattern); }); }); -} +}); diff --git a/src/testRunner/unittests/config/showConfig.ts b/src/testRunner/unittests/config/showConfig.ts index f8905e3412677..5bdc3f9720802 100644 --- a/src/testRunner/unittests/config/showConfig.ts +++ b/src/testRunner/unittests/config/showConfig.ts @@ -1,193 +1,175 @@ -namespace ts { - describe("unittests:: config:: showConfig", () => { - function showTSConfigCorrectly(name: string, commandLinesArgs: string[], configJson?: object) { - describe(name, () => { - const outputFileName = `showConfig/${name.replace(/[^a-z0-9\-./ ]/ig, "")}/tsconfig.json`; - - it(`Correct output for ${outputFileName}`, () => { - const cwd = `/${name}`; - const configPath = combinePaths(cwd, "tsconfig.json"); - const configContents = configJson ? JSON.stringify(configJson) : undefined; - const configParseHost: ParseConfigFileHost = { - fileExists: path => - comparePaths(getNormalizedAbsolutePath(path, cwd), configPath) === Comparison.EqualTo ? true : false, - getCurrentDirectory() { return cwd; }, - useCaseSensitiveFileNames: true, - onUnRecoverableConfigFileDiagnostic: d => { - throw new Error(flattenDiagnosticMessageText(d.messageText, "\n")); - }, - readDirectory() { return []; }, - readFile: path => - comparePaths(getNormalizedAbsolutePath(path, cwd), configPath) === Comparison.EqualTo ? configContents : undefined, - }; - let commandLine = parseCommandLine(commandLinesArgs); - if (commandLine.options.project) { - const result = getParsedCommandLineOfConfigFile(commandLine.options.project, commandLine.options, configParseHost); - if (result) { - commandLine = result; - } +import { combinePaths, ParseConfigFileHost, comparePaths, getNormalizedAbsolutePath, Comparison, flattenDiagnosticMessageText, parseCommandLine, getParsedCommandLineOfConfigFile, convertToTSConfig, optionDeclarations, optionsForWatch, CommandLineOption, Debug } from "../../ts"; +import { Baseline } from "../../Harness"; +describe("unittests:: config:: showConfig", () => { + function showTSConfigCorrectly(name: string, commandLinesArgs: string[], configJson?: object) { + describe(name, () => { + const outputFileName = `showConfig/${name.replace(/[^a-z0-9\-./ ]/ig, "")}/tsconfig.json`; + it(`Correct output for ${outputFileName}`, () => { + const cwd = `/${name}`; + const configPath = combinePaths(cwd, "tsconfig.json"); + const configContents = configJson ? JSON.stringify(configJson) : undefined; + const configParseHost: ParseConfigFileHost = { + fileExists: path => comparePaths(getNormalizedAbsolutePath(path, cwd), configPath) === Comparison.EqualTo ? true : false, + getCurrentDirectory() { return cwd; }, + useCaseSensitiveFileNames: true, + onUnRecoverableConfigFileDiagnostic: d => { + throw new Error(flattenDiagnosticMessageText(d.messageText, "\n")); + }, + readDirectory() { return []; }, + readFile: path => comparePaths(getNormalizedAbsolutePath(path, cwd), configPath) === Comparison.EqualTo ? configContents : undefined, + }; + let commandLine = parseCommandLine(commandLinesArgs); + if (commandLine.options.project) { + const result = getParsedCommandLineOfConfigFile(commandLine.options.project, commandLine.options, configParseHost); + if (result) { + commandLine = result; } - const initResult = convertToTSConfig(commandLine, configPath, configParseHost); - - // eslint-disable-next-line no-null/no-null - Harness.Baseline.runBaseline(outputFileName, JSON.stringify(initResult, null, 4) + "\n"); - }); + } + const initResult = convertToTSConfig(commandLine, configPath, configParseHost); + // eslint-disable-next-line no-null/no-null + Baseline.runBaseline(outputFileName, JSON.stringify(initResult, null, 4) + "\n"); }); - } - - showTSConfigCorrectly("Default initialized TSConfig", ["--showConfig"]); - - showTSConfigCorrectly("Show TSConfig with files options", ["--showConfig", "file0.ts", "file1.ts", "file2.ts"]); - - showTSConfigCorrectly("Show TSConfig with boolean value compiler options", ["--showConfig", "--noUnusedLocals"]); - - showTSConfigCorrectly("Show TSConfig with enum value compiler options", ["--showConfig", "--target", "es5", "--jsx", "react"]); - - showTSConfigCorrectly("Show TSConfig with list compiler options", ["--showConfig", "--types", "jquery,mocha"]); - - showTSConfigCorrectly("Show TSConfig with list compiler options with enum value", ["--showConfig", "--lib", "es5,es2015.core"]); - - showTSConfigCorrectly("Show TSConfig with incorrect compiler option", ["--showConfig", "--someNonExistOption"]); - - showTSConfigCorrectly("Show TSConfig with incorrect compiler option value", ["--showConfig", "--lib", "nonExistLib,es5,es2015.promise"]); - - showTSConfigCorrectly("Show TSConfig with advanced options", ["--showConfig", "--declaration", "--declarationDir", "lib", "--skipLibCheck", "--noErrorTruncation"]); - - showTSConfigCorrectly("Show TSConfig with compileOnSave and more", ["-p", "tsconfig.json"], { - compilerOptions: { - esModuleInterop: true, - target: "es5", - module: "commonjs", - strict: true, - }, - compileOnSave: true, - exclude: [ - "dist" - ], - files: [], - include: [ - "src/*" - ], - references: [ - { path: "./test" } - ], - }); - - // Regression test for https://github.com/Microsoft/TypeScript/issues/28836 - showTSConfigCorrectly("Show TSConfig with paths and more", ["-p", "tsconfig.json"], { - compilerOptions: { - allowJs: true, - outDir: "./lib", - esModuleInterop: true, - module: "commonjs", - moduleResolution: "node", - target: "ES2017", - sourceMap: true, - baseUrl: ".", - paths: { - "@root/*": ["./*"], - "@configs/*": ["src/configs/*"], - "@common/*": ["src/common/*"], - "*": [ - "node_modules/*", - "src/types/*" - ] - }, - experimentalDecorators: true, - emitDecoratorMetadata: true, - resolveJsonModule: true - }, - include: [ - "./src/**/*" - ] }); - - showTSConfigCorrectly("Show TSConfig with watch options", ["-p", "tsconfig.json"], { - watchOptions: { - watchFile: "DynamicPriorityPolling" + } + showTSConfigCorrectly("Default initialized TSConfig", ["--showConfig"]); + showTSConfigCorrectly("Show TSConfig with files options", ["--showConfig", "file0.ts", "file1.ts", "file2.ts"]); + showTSConfigCorrectly("Show TSConfig with boolean value compiler options", ["--showConfig", "--noUnusedLocals"]); + showTSConfigCorrectly("Show TSConfig with enum value compiler options", ["--showConfig", "--target", "es5", "--jsx", "react"]); + showTSConfigCorrectly("Show TSConfig with list compiler options", ["--showConfig", "--types", "jquery,mocha"]); + showTSConfigCorrectly("Show TSConfig with list compiler options with enum value", ["--showConfig", "--lib", "es5,es2015.core"]); + showTSConfigCorrectly("Show TSConfig with incorrect compiler option", ["--showConfig", "--someNonExistOption"]); + showTSConfigCorrectly("Show TSConfig with incorrect compiler option value", ["--showConfig", "--lib", "nonExistLib,es5,es2015.promise"]); + showTSConfigCorrectly("Show TSConfig with advanced options", ["--showConfig", "--declaration", "--declarationDir", "lib", "--skipLibCheck", "--noErrorTruncation"]); + showTSConfigCorrectly("Show TSConfig with compileOnSave and more", ["-p", "tsconfig.json"], { + compilerOptions: { + esModuleInterop: true, + target: "es5", + module: "commonjs", + strict: true, + }, + compileOnSave: true, + exclude: [ + "dist" + ], + files: [], + include: [ + "src/*" + ], + references: [ + { path: "./test" } + ], + }); + // Regression test for https://github.com/Microsoft/TypeScript/issues/28836 + showTSConfigCorrectly("Show TSConfig with paths and more", ["-p", "tsconfig.json"], { + compilerOptions: { + allowJs: true, + outDir: "./lib", + esModuleInterop: true, + module: "commonjs", + moduleResolution: "node", + target: "ES2017", + sourceMap: true, + baseUrl: ".", + paths: { + "@root/*": ["./*"], + "@configs/*": ["src/configs/*"], + "@common/*": ["src/common/*"], + "*": [ + "node_modules/*", + "src/types/*" + ] }, - include: [ - "./src/**/*" - ] - }); - - // Bulk validation of all option declarations - for (const option of optionDeclarations) { - baselineOption(option, /*isCompilerOptions*/ true); - } - - for (const option of optionsForWatch) { - baselineOption(option, /*isCompilerOptions*/ false); - } - - function baselineOption(option: CommandLineOption, isCompilerOptions: boolean) { - if (option.name === "project") return; - let args: string[]; - let optionValue: object | undefined; - switch (option.type) { - case "boolean": { - if (option.isTSConfigOnly) { - args = ["-p", "tsconfig.json"]; - optionValue = { [option.name]: true }; - } - else { - args = [`--${option.name}`]; - } - break; + experimentalDecorators: true, + emitDecoratorMetadata: true, + resolveJsonModule: true + }, + include: [ + "./src/**/*" + ] + }); + showTSConfigCorrectly("Show TSConfig with watch options", ["-p", "tsconfig.json"], { + watchOptions: { + watchFile: "DynamicPriorityPolling" + }, + include: [ + "./src/**/*" + ] + }); + // Bulk validation of all option declarations + for (const option of optionDeclarations) { + baselineOption(option, /*isCompilerOptions*/ true); + } + for (const option of optionsForWatch) { + baselineOption(option, /*isCompilerOptions*/ false); + } + function baselineOption(option: CommandLineOption, isCompilerOptions: boolean) { + if (option.name === "project") + return; + let args: string[]; + let optionValue: object | undefined; + switch (option.type) { + case "boolean": { + if (option.isTSConfigOnly) { + args = ["-p", "tsconfig.json"]; + optionValue = { [option.name]: true }; } - case "list": { - if (option.isTSConfigOnly) { - args = ["-p", "tsconfig.json"]; - optionValue = { [option.name]: [] }; - } - else { - args = [`--${option.name}`]; - } - break; + else { + args = [`--${option.name}`]; } - case "string": { - if (option.isTSConfigOnly) { - args = ["-p", "tsconfig.json"]; - optionValue = { [option.name]: "someString" }; - } - else { - args = [`--${option.name}`, "someString"]; - } - break; + break; + } + case "list": { + if (option.isTSConfigOnly) { + args = ["-p", "tsconfig.json"]; + optionValue = { [option.name]: [] }; } - case "number": { - if (option.isTSConfigOnly) { - args = ["-p", "tsconfig.json"]; - optionValue = { [option.name]: 0 }; - } - else { - args = [`--${option.name}`, "0"]; - } - break; + else { + args = [`--${option.name}`]; } - case "object": { + break; + } + case "string": { + if (option.isTSConfigOnly) { args = ["-p", "tsconfig.json"]; - optionValue = { [option.name]: {} }; - break; + optionValue = { [option.name]: "someString" }; } - default: { - const iterResult = option.type.keys().next(); - if (iterResult.done) return Debug.fail("Expected 'option.type' to have entries"); - const val = iterResult.value; - if (option.isTSConfigOnly) { - args = ["-p", "tsconfig.json"]; - optionValue = { [option.name]: val }; - } - else { - args = [`--${option.name}`, val]; - } - break; + else { + args = [`--${option.name}`, "someString"]; } + break; + } + case "number": { + if (option.isTSConfigOnly) { + args = ["-p", "tsconfig.json"]; + optionValue = { [option.name]: 0 }; + } + else { + args = [`--${option.name}`, "0"]; + } + break; + } + case "object": { + args = ["-p", "tsconfig.json"]; + optionValue = { [option.name]: {} }; + break; + } + default: { + const iterResult = option.type.keys().next(); + if (iterResult.done) + return Debug.fail("Expected 'option.type' to have entries"); + const val = iterResult.value; + if (option.isTSConfigOnly) { + args = ["-p", "tsconfig.json"]; + optionValue = { [option.name]: val }; + } + else { + args = [`--${option.name}`, val]; + } + break; } - - const configObject = optionValue && - (isCompilerOptions ? { compilerOptions: optionValue } : { watchOptions: optionValue }); - showTSConfigCorrectly(`Shows tsconfig for single option/${option.name}`, args, configObject); } - }); -} + const configObject = optionValue && + (isCompilerOptions ? { compilerOptions: optionValue } : { watchOptions: optionValue }); + showTSConfigCorrectly(`Shows tsconfig for single option/${option.name}`, args, configObject); + } +}); diff --git a/src/testRunner/unittests/config/tsconfigParsing.ts b/src/testRunner/unittests/config/tsconfigParsing.ts index 5e15bbc6e9f82..70f22ef41dd35 100644 --- a/src/testRunner/unittests/config/tsconfigParsing.ts +++ b/src/testRunner/unittests/config/tsconfigParsing.ts @@ -1,104 +1,96 @@ -namespace ts { - describe("unittests:: config:: tsconfigParsing:: parseConfigFileTextToJson", () => { - function assertParseResult(jsonText: string, expectedConfigObject: { config?: any; error?: Diagnostic[] }) { +import { Diagnostic, parseConfigFileTextToJson, parseJsonConfigFileContent, sys, Diagnostics, parseJsonText, parseJsonSourceFileConfigFileContent, ParseConfigHost, arrayIsEqualTo, createCompilerDiagnostic, convertToObject } from "../../ts"; +import { FileSet, FileSystem } from "../../vfs"; +import * as fakes from "../../fakes"; +describe("unittests:: config:: tsconfigParsing:: parseConfigFileTextToJson", () => { + function assertParseResult(jsonText: string, expectedConfigObject: { + config?: any; + error?: Diagnostic[]; + }) { + const parsed = parseConfigFileTextToJson("/apath/tsconfig.json", jsonText); + assert.equal(JSON.stringify(parsed), JSON.stringify(expectedConfigObject)); + } + function assertParseErrorWithExcludesKeyword(jsonText: string) { + { const parsed = parseConfigFileTextToJson("/apath/tsconfig.json", jsonText); - assert.equal(JSON.stringify(parsed), JSON.stringify(expectedConfigObject)); + const parsedCommand = parseJsonConfigFileContent(parsed.config, sys, "tests/cases/unittests"); + assert.isTrue(parsedCommand.errors && parsedCommand.errors.length === 1 && + parsedCommand.errors[0].code === Diagnostics.Unknown_option_excludes_Did_you_mean_exclude.code); } - - function assertParseErrorWithExcludesKeyword(jsonText: string) { - { - const parsed = parseConfigFileTextToJson("/apath/tsconfig.json", jsonText); - const parsedCommand = parseJsonConfigFileContent(parsed.config, sys, "tests/cases/unittests"); - assert.isTrue(parsedCommand.errors && parsedCommand.errors.length === 1 && - parsedCommand.errors[0].code === Diagnostics.Unknown_option_excludes_Did_you_mean_exclude.code); - } - { - const parsed = parseJsonText("/apath/tsconfig.json", jsonText); - const parsedCommand = parseJsonSourceFileConfigFileContent(parsed, sys, "tests/cases/unittests"); - assert.isTrue(parsedCommand.errors && parsedCommand.errors.length === 1 && - parsedCommand.errors[0].code === Diagnostics.Unknown_option_excludes_Did_you_mean_exclude.code); - } + { + const parsed = parseJsonText("/apath/tsconfig.json", jsonText); + const parsedCommand = parseJsonSourceFileConfigFileContent(parsed, sys, "tests/cases/unittests"); + assert.isTrue(parsedCommand.errors && parsedCommand.errors.length === 1 && + parsedCommand.errors[0].code === Diagnostics.Unknown_option_excludes_Did_you_mean_exclude.code); } - - function getParsedCommandJson(jsonText: string, configFileName: string, basePath: string, allFileList: string[]) { - const parsed = parseConfigFileTextToJson(configFileName, jsonText); - const files = allFileList.reduce((files, value) => (files[value] = "", files), {} as vfs.FileSet); - const host: ParseConfigHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ false, { cwd: basePath, files: { "/": {}, ...files } })); - return parseJsonConfigFileContent(parsed.config, host, basePath, /*existingOptions*/ undefined, configFileName); + } + function getParsedCommandJson(jsonText: string, configFileName: string, basePath: string, allFileList: string[]) { + const parsed = parseConfigFileTextToJson(configFileName, jsonText); + const files = allFileList.reduce((files, value) => (files[value] = "", files), ({} as FileSet)); + const host: ParseConfigHost = new fakes.ParseConfigHost(new FileSystem(/*ignoreCase*/ false, { cwd: basePath, files: { "/": {}, ...files } })); + return parseJsonConfigFileContent(parsed.config, host, basePath, /*existingOptions*/ undefined, configFileName); + } + function getParsedCommandJsonNode(jsonText: string, configFileName: string, basePath: string, allFileList: string[]) { + const parsed = parseJsonText(configFileName, jsonText); + const files = allFileList.reduce((files, value) => (files[value] = "", files), ({} as FileSet)); + const host: ParseConfigHost = new fakes.ParseConfigHost(new FileSystem(/*ignoreCase*/ false, { cwd: basePath, files: { "/": {}, ...files } })); + return parseJsonSourceFileConfigFileContent(parsed, host, basePath, /*existingOptions*/ undefined, configFileName); + } + function assertParseFileList(jsonText: string, configFileName: string, basePath: string, allFileList: string[], expectedFileList: string[]) { + { + const parsed = getParsedCommandJson(jsonText, configFileName, basePath, allFileList); + assert.isTrue(arrayIsEqualTo(parsed.fileNames.sort(), expectedFileList.sort())); } - - function getParsedCommandJsonNode(jsonText: string, configFileName: string, basePath: string, allFileList: string[]) { - const parsed = parseJsonText(configFileName, jsonText); - const files = allFileList.reduce((files, value) => (files[value] = "", files), {} as vfs.FileSet); - const host: ParseConfigHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ false, { cwd: basePath, files: { "/": {}, ...files } })); - return parseJsonSourceFileConfigFileContent(parsed, host, basePath, /*existingOptions*/ undefined, configFileName); + { + const parsed = getParsedCommandJsonNode(jsonText, configFileName, basePath, allFileList); + assert.isTrue(arrayIsEqualTo(parsed.fileNames.sort(), expectedFileList.sort())); } - - function assertParseFileList(jsonText: string, configFileName: string, basePath: string, allFileList: string[], expectedFileList: string[]) { - { - const parsed = getParsedCommandJson(jsonText, configFileName, basePath, allFileList); - assert.isTrue(arrayIsEqualTo(parsed.fileNames.sort(), expectedFileList.sort())); - } - { - const parsed = getParsedCommandJsonNode(jsonText, configFileName, basePath, allFileList); - assert.isTrue(arrayIsEqualTo(parsed.fileNames.sort(), expectedFileList.sort())); - } + } + function assertParseFileDiagnostics(jsonText: string, configFileName: string, basePath: string, allFileList: string[], expectedDiagnosticCode: number, noLocation?: boolean) { + { + const parsed = getParsedCommandJson(jsonText, configFileName, basePath, allFileList); + assert.isTrue(parsed.errors.length >= 0); + assert.isTrue(parsed.errors.filter(e => e.code === expectedDiagnosticCode).length > 0, `Expected error code ${expectedDiagnosticCode} to be in ${JSON.stringify(parsed.errors)}`); } - - function assertParseFileDiagnostics(jsonText: string, configFileName: string, basePath: string, allFileList: string[], expectedDiagnosticCode: number, noLocation?: boolean) { - { - const parsed = getParsedCommandJson(jsonText, configFileName, basePath, allFileList); - assert.isTrue(parsed.errors.length >= 0); - assert.isTrue(parsed.errors.filter(e => e.code === expectedDiagnosticCode).length > 0, `Expected error code ${expectedDiagnosticCode} to be in ${JSON.stringify(parsed.errors)}`); - } - { - const parsed = getParsedCommandJsonNode(jsonText, configFileName, basePath, allFileList); - assert.isTrue(parsed.errors.length >= 0); - assert.isTrue(parsed.errors.filter(e => e.code === expectedDiagnosticCode).length > 0, `Expected error code ${expectedDiagnosticCode} to be in ${JSON.stringify(parsed.errors)}`); - if (!noLocation) { - assert.isTrue(parsed.errors.filter(e => e.code === expectedDiagnosticCode && e.file && e.start && e.length).length > 0, `Expected error code ${expectedDiagnosticCode} to be in ${JSON.stringify(parsed.errors)} with location information`); - } + { + const parsed = getParsedCommandJsonNode(jsonText, configFileName, basePath, allFileList); + assert.isTrue(parsed.errors.length >= 0); + assert.isTrue(parsed.errors.filter(e => e.code === expectedDiagnosticCode).length > 0, `Expected error code ${expectedDiagnosticCode} to be in ${JSON.stringify(parsed.errors)}`); + if (!noLocation) { + assert.isTrue(parsed.errors.filter(e => e.code === expectedDiagnosticCode && e.file && e.start && e.length).length > 0, `Expected error code ${expectedDiagnosticCode} to be in ${JSON.stringify(parsed.errors)} with location information`); } } - - function assertParseFileDiagnosticsExclusion(jsonText: string, configFileName: string, basePath: string, allFileList: string[], expectedExcludedDiagnosticCode: number) { - { - const parsed = getParsedCommandJson(jsonText, configFileName, basePath, allFileList); - assert.isTrue(parsed.errors.length >= 0); - assert.isTrue(parsed.errors.findIndex(e => e.code === expectedExcludedDiagnosticCode) === -1, `Expected error code ${expectedExcludedDiagnosticCode} to not be in ${JSON.stringify(parsed.errors)}`); - } - { - const parsed = getParsedCommandJsonNode(jsonText, configFileName, basePath, allFileList); - assert.isTrue(parsed.errors.length >= 0); - assert.isTrue(parsed.errors.findIndex(e => e.code === expectedExcludedDiagnosticCode) === -1, `Expected error code ${expectedExcludedDiagnosticCode} to not be in ${JSON.stringify(parsed.errors)}`); - } + } + function assertParseFileDiagnosticsExclusion(jsonText: string, configFileName: string, basePath: string, allFileList: string[], expectedExcludedDiagnosticCode: number) { + { + const parsed = getParsedCommandJson(jsonText, configFileName, basePath, allFileList); + assert.isTrue(parsed.errors.length >= 0); + assert.isTrue(parsed.errors.findIndex(e => e.code === expectedExcludedDiagnosticCode) === -1, `Expected error code ${expectedExcludedDiagnosticCode} to not be in ${JSON.stringify(parsed.errors)}`); } - - it("returns empty config for file with only whitespaces", () => { - assertParseResult("", { config : {} }); - assertParseResult(" ", { config : {} }); - }); - - it("returns empty config for file with comments only", () => { - assertParseResult("// Comment", { config: {} }); - assertParseResult("/* Comment*/", { config: {} }); - }); - - it("returns empty config when config is empty object", () => { - assertParseResult("{}", { config: {} }); - }); - - it("returns config object without comments", () => { - assertParseResult( - `{ // Excluded files + { + const parsed = getParsedCommandJsonNode(jsonText, configFileName, basePath, allFileList); + assert.isTrue(parsed.errors.length >= 0); + assert.isTrue(parsed.errors.findIndex(e => e.code === expectedExcludedDiagnosticCode) === -1, `Expected error code ${expectedExcludedDiagnosticCode} to not be in ${JSON.stringify(parsed.errors)}`); + } + } + it("returns empty config for file with only whitespaces", () => { + assertParseResult("", { config: {} }); + assertParseResult(" ", { config: {} }); + }); + it("returns empty config for file with comments only", () => { + assertParseResult("// Comment", { config: {} }); + assertParseResult("/* Comment*/", { config: {} }); + }); + it("returns empty config when config is empty object", () => { + assertParseResult("{}", { config: {} }); + }); + it("returns config object without comments", () => { + assertParseResult(`{ // Excluded files "exclude": [ // Exclude d.ts "file.d.ts" ] }`, { config: { exclude: ["file.d.ts"] } }); - - assertParseResult( - `{ + assertParseResult(`{ /* Excluded Files */ @@ -106,74 +98,60 @@ namespace ts { /* multiline comments can be in the middle of a line */"file.d.ts" ] }`, { config: { exclude: ["file.d.ts"] } }); - }); - - it("keeps string content untouched", () => { - assertParseResult( - `{ + }); + it("keeps string content untouched", () => { + assertParseResult(`{ "exclude": [ "xx//file.d.ts" ] }`, { config: { exclude: ["xx//file.d.ts"] } }); - assertParseResult( - `{ + assertParseResult(`{ "exclude": [ "xx/*file.d.ts*/" ] }`, { config: { exclude: ["xx/*file.d.ts*/"] } }); - }); - - it("handles escaped characters in strings correctly", () => { - assertParseResult( - `{ + }); + it("handles escaped characters in strings correctly", () => { + assertParseResult(`{ "exclude": [ "xx\\"//files" ] }`, { config: { exclude: ["xx\"//files"] } }); - - assertParseResult( - `{ + assertParseResult(`{ "exclude": [ "xx\\\\" // end of line comment ] }`, { config: { exclude: ["xx\\"] } }); - }); - - it("returns object with error when json is invalid", () => { - const parsed = parseConfigFileTextToJson("/apath/tsconfig.json", "invalid"); - assert.deepEqual(parsed.config, {}); - const expected = createCompilerDiagnostic(Diagnostics._0_expected, "{"); - const error = parsed.error!; - assert.equal(error.messageText, expected.messageText); - assert.equal(error.category, expected.category); - assert.equal(error.code, expected.code); - assert.equal(error.start, 0); - assert.equal(error.length, "invalid".length); - }); - - it("returns object when users correctly specify library", () => { - assertParseResult( - `{ + }); + it("returns object with error when json is invalid", () => { + const parsed = parseConfigFileTextToJson("/apath/tsconfig.json", "invalid"); + assert.deepEqual(parsed.config, {}); + const expected = createCompilerDiagnostic(Diagnostics._0_expected, "{"); + const error = parsed.error!; + assert.equal(error.messageText, expected.messageText); + assert.equal(error.category, expected.category); + assert.equal(error.code, expected.code); + assert.equal(error.start, 0); + assert.equal(error.length, "invalid".length); + }); + it("returns object when users correctly specify library", () => { + assertParseResult(`{ "compilerOptions": { "lib": ["es5"] } }`, { - config: { compilerOptions: { lib: ["es5"] } } - }); - - assertParseResult( - `{ + config: { compilerOptions: { lib: ["es5"] } } + }); + assertParseResult(`{ "compilerOptions": { "lib": ["es5", "es6"] } }`, { - config: { compilerOptions: { lib: ["es5", "es6"] } } - }); + config: { compilerOptions: { lib: ["es5", "es6"] } } }); - - it("returns error when tsconfig have excludes", () => { - assertParseErrorWithExcludesKeyword( - `{ + }); + it("returns error when tsconfig have excludes", () => { + assertParseErrorWithExcludesKeyword(`{ "compilerOptions": { "lib": ["es5"] }, @@ -181,86 +159,56 @@ namespace ts { "foge.ts" ] }`); - }); - - it("ignore dotted files and folders", () => { - assertParseFileList( - `{}`, - "tsconfig.json", - "/apath", - ["/apath/test.ts", "/apath/.git/a.ts", "/apath/.b.ts", "/apath/..c.ts"], - ["/apath/test.ts"] - ); - }); - - it("allow dotted files and folders when explicitly requested", () => { - assertParseFileList( - `{ + }); + it("ignore dotted files and folders", () => { + assertParseFileList(`{}`, "tsconfig.json", "/apath", ["/apath/test.ts", "/apath/.git/a.ts", "/apath/.b.ts", "/apath/..c.ts"], ["/apath/test.ts"]); + }); + it("allow dotted files and folders when explicitly requested", () => { + assertParseFileList(`{ "files": ["/apath/.git/a.ts", "/apath/.b.ts", "/apath/..c.ts"] - }`, - "tsconfig.json", - "/apath", - ["/apath/test.ts", "/apath/.git/a.ts", "/apath/.b.ts", "/apath/..c.ts"], - ["/apath/.git/a.ts", "/apath/.b.ts", "/apath/..c.ts"] - ); - }); - - it("exclude outDir unless overridden", () => { - const tsconfigWithoutExclude = - `{ + }`, "tsconfig.json", "/apath", ["/apath/test.ts", "/apath/.git/a.ts", "/apath/.b.ts", "/apath/..c.ts"], ["/apath/.git/a.ts", "/apath/.b.ts", "/apath/..c.ts"]); + }); + it("exclude outDir unless overridden", () => { + const tsconfigWithoutExclude = `{ "compilerOptions": { "outDir": "bin" } }`; - const tsconfigWithExclude = - `{ + const tsconfigWithExclude = `{ "compilerOptions": { "outDir": "bin" }, "exclude": [ "obj" ] }`; - const rootDir = "/"; - const allFiles = ["/bin/a.ts", "/b.ts"]; - const expectedFiles = ["/b.ts"]; - assertParseFileList(tsconfigWithoutExclude, "tsconfig.json", rootDir, allFiles, expectedFiles); - assertParseFileList(tsconfigWithExclude, "tsconfig.json", rootDir, allFiles, allFiles); - }); - - it("exclude declarationDir unless overridden", () => { - const tsconfigWithoutExclude = - `{ + const rootDir = "/"; + const allFiles = ["/bin/a.ts", "/b.ts"]; + const expectedFiles = ["/b.ts"]; + assertParseFileList(tsconfigWithoutExclude, "tsconfig.json", rootDir, allFiles, expectedFiles); + assertParseFileList(tsconfigWithExclude, "tsconfig.json", rootDir, allFiles, allFiles); + }); + it("exclude declarationDir unless overridden", () => { + const tsconfigWithoutExclude = `{ "compilerOptions": { "declarationDir": "declarations" } }`; - const tsconfigWithExclude = - `{ + const tsconfigWithExclude = `{ "compilerOptions": { "declarationDir": "declarations" }, "exclude": [ "types" ] }`; - - const rootDir = "/"; - const allFiles = ["/declarations/a.d.ts", "/a.ts"]; - const expectedFiles = ["/a.ts"]; - - assertParseFileList(tsconfigWithoutExclude, "tsconfig.json", rootDir, allFiles, expectedFiles); - assertParseFileList(tsconfigWithExclude, "tsconfig.json", rootDir, allFiles, allFiles); - }); - - it("implicitly exclude common package folders", () => { - assertParseFileList( - `{}`, - "tsconfig.json", - "/", - ["/node_modules/a.ts", "/bower_components/b.ts", "/jspm_packages/c.ts", "/d.ts", "/folder/e.ts"], - ["/d.ts", "/folder/e.ts"] - ); - }); - - it("parse and re-emit tsconfig.json file with diagnostics", () => { - const content = `{ + const rootDir = "/"; + const allFiles = ["/declarations/a.d.ts", "/a.ts"]; + const expectedFiles = ["/a.ts"]; + assertParseFileList(tsconfigWithoutExclude, "tsconfig.json", rootDir, allFiles, expectedFiles); + assertParseFileList(tsconfigWithExclude, "tsconfig.json", rootDir, allFiles, allFiles); + }); + it("implicitly exclude common package folders", () => { + assertParseFileList(`{}`, "tsconfig.json", "/", ["/node_modules/a.ts", "/bower_components/b.ts", "/jspm_packages/c.ts", "/d.ts", "/folder/e.ts"], ["/d.ts", "/folder/e.ts"]); + }); + it("parse and re-emit tsconfig.json file with diagnostics", () => { + const content = `{ "compilerOptions": { "allowJs": true // Some comments @@ -268,118 +216,80 @@ namespace ts { } "files": ["file1.ts"] }`; - const result = parseJsonText("config.json", content); - const diagnostics = result.parseDiagnostics; - const configJsonObject = convertToObject(result, diagnostics); - const expectedResult = { - compilerOptions: { - allowJs: true, - outDir: "bin" - }, - files: ["file1.ts"] - }; - assert.isTrue(diagnostics.length === 2); - assert.equal(JSON.stringify(configJsonObject), JSON.stringify(expectedResult)); - }); - - it("generates errors for empty files list", () => { - const content = `{ + const result = parseJsonText("config.json", content); + const diagnostics = result.parseDiagnostics; + const configJsonObject = convertToObject(result, diagnostics); + const expectedResult = { + compilerOptions: { + allowJs: true, + outDir: "bin" + }, + files: ["file1.ts"] + }; + assert.isTrue(diagnostics.length === 2); + assert.equal(JSON.stringify(configJsonObject), JSON.stringify(expectedResult)); + }); + it("generates errors for empty files list", () => { + const content = `{ "files": [] }`; - assertParseFileDiagnostics(content, - "/apath/tsconfig.json", - "tests/cases/unittests", - ["/apath/a.ts"], - Diagnostics.The_files_list_in_config_file_0_is_empty.code); - }); - - it("generates errors for empty files list when no references are provided", () => { - const content = `{ + assertParseFileDiagnostics(content, "/apath/tsconfig.json", "tests/cases/unittests", ["/apath/a.ts"], Diagnostics.The_files_list_in_config_file_0_is_empty.code); + }); + it("generates errors for empty files list when no references are provided", () => { + const content = `{ "files": [], "references": [] }`; - assertParseFileDiagnostics(content, - "/apath/tsconfig.json", - "tests/cases/unittests", - ["/apath/a.ts"], - Diagnostics.The_files_list_in_config_file_0_is_empty.code); - }); - - it("does not generate errors for empty files list when one or more references are provided", () => { - const content = `{ + assertParseFileDiagnostics(content, "/apath/tsconfig.json", "tests/cases/unittests", ["/apath/a.ts"], Diagnostics.The_files_list_in_config_file_0_is_empty.code); + }); + it("does not generate errors for empty files list when one or more references are provided", () => { + const content = `{ "files": [], "references": [{ "path": "/apath" }] }`; - assertParseFileDiagnosticsExclusion(content, - "/apath/tsconfig.json", - "tests/cases/unittests", - ["/apath/a.ts"], - Diagnostics.The_files_list_in_config_file_0_is_empty.code); - }); - - it("generates errors for directory with no .ts files", () => { - const content = `{ + assertParseFileDiagnosticsExclusion(content, "/apath/tsconfig.json", "tests/cases/unittests", ["/apath/a.ts"], Diagnostics.The_files_list_in_config_file_0_is_empty.code); + }); + it("generates errors for directory with no .ts files", () => { + const content = `{ }`; - assertParseFileDiagnostics(content, - "/apath/tsconfig.json", - "tests/cases/unittests", - ["/apath/a.js"], - Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code, - /*noLocation*/ true); - }); - - it("generates errors for empty directory", () => { - const content = `{ + assertParseFileDiagnostics(content, "/apath/tsconfig.json", "tests/cases/unittests", ["/apath/a.js"], Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code, + /*noLocation*/ true); + }); + it("generates errors for empty directory", () => { + const content = `{ "compilerOptions": { "allowJs": true } }`; - assertParseFileDiagnostics(content, - "/apath/tsconfig.json", - "tests/cases/unittests", - [], - Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code, - /*noLocation*/ true); - }); - - it("generates errors for empty include", () => { - const content = `{ + assertParseFileDiagnostics(content, "/apath/tsconfig.json", "tests/cases/unittests", [], Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code, + /*noLocation*/ true); + }); + it("generates errors for empty include", () => { + const content = `{ "include": [] }`; - assertParseFileDiagnostics(content, - "/apath/tsconfig.json", - "tests/cases/unittests", - ["/apath/a.ts"], - Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code, - /*noLocation*/ true); - }); - - it("generates errors for includes with outDir", () => { - const content = `{ + assertParseFileDiagnostics(content, "/apath/tsconfig.json", "tests/cases/unittests", ["/apath/a.ts"], Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code, + /*noLocation*/ true); + }); + it("generates errors for includes with outDir", () => { + const content = `{ "compilerOptions": { "outDir": "./" }, "include": ["**/*"] }`; - assertParseFileDiagnostics(content, - "/apath/tsconfig.json", - "tests/cases/unittests", - ["/apath/a.ts"], - Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code, - /*noLocation*/ true); - }); - - - it("generates errors for when invalid comment type present in tsconfig", () => { - const jsonText = `{ + assertParseFileDiagnostics(content, "/apath/tsconfig.json", "tests/cases/unittests", ["/apath/a.ts"], Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code, + /*noLocation*/ true); + }); + it("generates errors for when invalid comment type present in tsconfig", () => { + const jsonText = `{ "compilerOptions": { ## this comment does cause issues "types" : [ ] } }`; - const parsed = getParsedCommandJsonNode(jsonText, "/apath/tsconfig.json", "tests/cases/unittests", ["/apath/a.ts"]); - assert.isTrue(parsed.errors.length >= 0); - }); + const parsed = getParsedCommandJsonNode(jsonText, "/apath/tsconfig.json", "tests/cases/unittests", ["/apath/a.ts"]); + assert.isTrue(parsed.errors.length >= 0); }); -} +}); diff --git a/src/testRunner/unittests/config/tsconfigParsingWatchOptions.ts b/src/testRunner/unittests/config/tsconfigParsingWatchOptions.ts index 83e224e492527..119576c216c7e 100644 --- a/src/testRunner/unittests/config/tsconfigParsingWatchOptions.ts +++ b/src/testRunner/unittests/config/tsconfigParsingWatchOptions.ts @@ -1,178 +1,153 @@ -namespace ts { - describe("unittests:: config:: tsconfigParsingWatchOptions:: parseConfigFileTextToJson", () => { - function createParseConfigHost(additionalFiles?: vfs.FileSet) { - return new fakes.ParseConfigHost( - new vfs.FileSystem( - /*ignoreCase*/ false, - { - cwd: "/", - files: { "/": {}, "/a.ts": "", ...additionalFiles } - } - ) - ); - } - function getParsedCommandJson(json: object, additionalFiles?: vfs.FileSet, existingWatchOptions?: WatchOptions) { - return parseJsonConfigFileContent( - json, - createParseConfigHost(additionalFiles), - "/", - /*existingOptions*/ undefined, - "tsconfig.json", - /*resolutionStack*/ undefined, - /*extraFileExtensions*/ undefined, - /*extendedConfigCache*/ undefined, - existingWatchOptions, - ); - } - - function getParsedCommandJsonNode(json: object, additionalFiles?: vfs.FileSet, existingWatchOptions?: WatchOptions) { - const parsed = parseJsonText("tsconfig.json", JSON.stringify(json)); - return parseJsonSourceFileConfigFileContent( - parsed, - createParseConfigHost(additionalFiles), - "/", - /*existingOptions*/ undefined, - "tsconfig.json", - /*resolutionStack*/ undefined, - /*extraFileExtensions*/ undefined, - /*extendedConfigCache*/ undefined, - existingWatchOptions, - ); - } - - interface VerifyWatchOptions { - json: object; - expectedOptions: WatchOptions | undefined; - additionalFiles?: vfs.FileSet; - existingWatchOptions?: WatchOptions | undefined; - } - - function verifyWatchOptions(scenario: () => VerifyWatchOptions[]) { - it("with json api", () => { - for (const { json, expectedOptions, additionalFiles, existingWatchOptions } of scenario()) { - const parsed = getParsedCommandJson(json, additionalFiles, existingWatchOptions); - assert.deepEqual(parsed.watchOptions, expectedOptions); - } - }); - - it("with json source file api", () => { - for (const { json, expectedOptions, additionalFiles, existingWatchOptions } of scenario()) { - const parsed = getParsedCommandJsonNode(json, additionalFiles, existingWatchOptions); - assert.deepEqual(parsed.watchOptions, expectedOptions); - } - }); - } - - describe("no watchOptions specified option", () => { - verifyWatchOptions(() => [{ +import { FileSet, FileSystem } from "../../vfs"; +import { ParseConfigHost } from "../../fakes"; +import { WatchOptions, parseJsonConfigFileContent, parseJsonText, parseJsonSourceFileConfigFileContent, WatchFileKind, WatchDirectoryKind, PollingWatchKind } from "../../ts"; +describe("unittests:: config:: tsconfigParsingWatchOptions:: parseConfigFileTextToJson", () => { + function createParseConfigHost(additionalFiles?: FileSet) { + return new ParseConfigHost(new FileSystem( + /*ignoreCase*/ false, { + cwd: "/", + files: { "/": {}, "/a.ts": "", ...additionalFiles } + })); + } + function getParsedCommandJson(json: object, additionalFiles?: FileSet, existingWatchOptions?: WatchOptions) { + return parseJsonConfigFileContent(json, createParseConfigHost(additionalFiles), "/", + /*existingOptions*/ undefined, "tsconfig.json", + /*resolutionStack*/ undefined, + /*extraFileExtensions*/ undefined, + /*extendedConfigCache*/ undefined, existingWatchOptions); + } + function getParsedCommandJsonNode(json: object, additionalFiles?: FileSet, existingWatchOptions?: WatchOptions) { + const parsed = parseJsonText("tsconfig.json", JSON.stringify(json)); + return parseJsonSourceFileConfigFileContent(parsed, createParseConfigHost(additionalFiles), "/", + /*existingOptions*/ undefined, "tsconfig.json", + /*resolutionStack*/ undefined, + /*extraFileExtensions*/ undefined, + /*extendedConfigCache*/ undefined, existingWatchOptions); + } + interface VerifyWatchOptions { + json: object; + expectedOptions: WatchOptions | undefined; + additionalFiles?: FileSet; + existingWatchOptions?: WatchOptions | undefined; + } + function verifyWatchOptions(scenario: () => VerifyWatchOptions[]) { + it("with json api", () => { + for (const { json, expectedOptions, additionalFiles, existingWatchOptions } of scenario()) { + const parsed = getParsedCommandJson(json, additionalFiles, existingWatchOptions); + assert.deepEqual(parsed.watchOptions, expectedOptions); + } + }); + it("with json source file api", () => { + for (const { json, expectedOptions, additionalFiles, existingWatchOptions } of scenario()) { + const parsed = getParsedCommandJsonNode(json, additionalFiles, existingWatchOptions); + assert.deepEqual(parsed.watchOptions, expectedOptions); + } + }); + } + describe("no watchOptions specified option", () => { + verifyWatchOptions(() => [{ json: {}, expectedOptions: undefined }]); - }); - - describe("empty watchOptions specified option", () => { - verifyWatchOptions(() => [{ + }); + describe("empty watchOptions specified option", () => { + verifyWatchOptions(() => [{ json: { watchOptions: {} }, expectedOptions: undefined }]); - }); - - describe("extending config file", () => { - describe("when extending config file without watchOptions", () => { - verifyWatchOptions(() => [ - { - json: { - extends: "./base.json", - watchOptions: { watchFile: "UseFsEvents" } - }, - expectedOptions: { watchFile: WatchFileKind.UseFsEvents }, - additionalFiles: { "/base.json": "{}" } - }, - { - json: { extends: "./base.json", }, - expectedOptions: undefined, - additionalFiles: { "/base.json": "{}" } - } - ]); - }); - - describe("when extending config file with watchOptions", () => { - verifyWatchOptions(() => [ - { - json: { - extends: "./base.json", - watchOptions: { - watchFile: "UseFsEvents", - } - }, - expectedOptions: { - watchFile: WatchFileKind.UseFsEvents, - watchDirectory: WatchDirectoryKind.FixedPollingInterval - }, - additionalFiles: { - "/base.json": JSON.stringify({ - watchOptions: { - watchFile: "UseFsEventsOnParentDirectory", - watchDirectory: "FixedPollingInterval" - } - }) - } - }, - { - json: { - extends: "./base.json", - }, - expectedOptions: { - watchFile: WatchFileKind.UseFsEventsOnParentDirectory, - watchDirectory: WatchDirectoryKind.FixedPollingInterval - }, - additionalFiles: { - "/base.json": JSON.stringify({ - watchOptions: { - watchFile: "UseFsEventsOnParentDirectory", - watchDirectory: "FixedPollingInterval" - } - }) - } - } - ]); - }); - }); - - describe("different options", () => { + }); + describe("extending config file", () => { + describe("when extending config file without watchOptions", () => { verifyWatchOptions(() => [ { - json: { watchOptions: { watchFile: "UseFsEvents" } }, - expectedOptions: { watchFile: WatchFileKind.UseFsEvents } - }, - { - json: { watchOptions: { watchDirectory: "UseFsEvents" } }, - expectedOptions: { watchDirectory: WatchDirectoryKind.UseFsEvents } - }, - { - json: { watchOptions: { fallbackPolling: "DynamicPriority" } }, - expectedOptions: { fallbackPolling: PollingWatchKind.DynamicPriority } + json: { + extends: "./base.json", + watchOptions: { watchFile: "UseFsEvents" } + }, + expectedOptions: { watchFile: WatchFileKind.UseFsEvents }, + additionalFiles: { "/base.json": "{}" } }, { - json: { watchOptions: { synchronousWatchDirectory: true } }, - expectedOptions: { synchronousWatchDirectory: true } + json: { extends: "./base.json", }, + expectedOptions: undefined, + additionalFiles: { "/base.json": "{}" } } ]); }); - - describe("watch options extending passed in watch options", () => { + describe("when extending config file with watchOptions", () => { verifyWatchOptions(() => [ { - json: { watchOptions: { watchFile: "UseFsEvents" } }, - expectedOptions: { watchFile: WatchFileKind.UseFsEvents, watchDirectory: WatchDirectoryKind.FixedPollingInterval }, - existingWatchOptions: { watchDirectory: WatchDirectoryKind.FixedPollingInterval } + json: { + extends: "./base.json", + watchOptions: { + watchFile: "UseFsEvents", + } + }, + expectedOptions: { + watchFile: WatchFileKind.UseFsEvents, + watchDirectory: WatchDirectoryKind.FixedPollingInterval + }, + additionalFiles: { + "/base.json": JSON.stringify({ + watchOptions: { + watchFile: "UseFsEventsOnParentDirectory", + watchDirectory: "FixedPollingInterval" + } + }) + } }, { - json: {}, - expectedOptions: { watchDirectory: WatchDirectoryKind.FixedPollingInterval }, - existingWatchOptions: { watchDirectory: WatchDirectoryKind.FixedPollingInterval } - }, + json: { + extends: "./base.json", + }, + expectedOptions: { + watchFile: WatchFileKind.UseFsEventsOnParentDirectory, + watchDirectory: WatchDirectoryKind.FixedPollingInterval + }, + additionalFiles: { + "/base.json": JSON.stringify({ + watchOptions: { + watchFile: "UseFsEventsOnParentDirectory", + watchDirectory: "FixedPollingInterval" + } + }) + } + } ]); }); }); -} + describe("different options", () => { + verifyWatchOptions(() => [ + { + json: { watchOptions: { watchFile: "UseFsEvents" } }, + expectedOptions: { watchFile: WatchFileKind.UseFsEvents } + }, + { + json: { watchOptions: { watchDirectory: "UseFsEvents" } }, + expectedOptions: { watchDirectory: WatchDirectoryKind.UseFsEvents } + }, + { + json: { watchOptions: { fallbackPolling: "DynamicPriority" } }, + expectedOptions: { fallbackPolling: PollingWatchKind.DynamicPriority } + }, + { + json: { watchOptions: { synchronousWatchDirectory: true } }, + expectedOptions: { synchronousWatchDirectory: true } + } + ]); + }); + describe("watch options extending passed in watch options", () => { + verifyWatchOptions(() => [ + { + json: { watchOptions: { watchFile: "UseFsEvents" } }, + expectedOptions: { watchFile: WatchFileKind.UseFsEvents, watchDirectory: WatchDirectoryKind.FixedPollingInterval }, + existingWatchOptions: { watchDirectory: WatchDirectoryKind.FixedPollingInterval } + }, + { + json: {}, + expectedOptions: { watchDirectory: WatchDirectoryKind.FixedPollingInterval }, + existingWatchOptions: { watchDirectory: WatchDirectoryKind.FixedPollingInterval } + }, + ]); + }); +}); diff --git a/src/testRunner/unittests/convertToBase64.ts b/src/testRunner/unittests/convertToBase64.ts index 37e38da8da7dc..f19051ffa085f 100644 --- a/src/testRunner/unittests/convertToBase64.ts +++ b/src/testRunner/unittests/convertToBase64.ts @@ -1,33 +1,27 @@ -namespace ts { - describe("unittests:: convertToBase64", () => { - function runTest(input: string): void { - const actual = convertToBase64(input); - const expected = sys.base64encode!(input); - assert.equal(actual, expected, "Encoded string using convertToBase64 does not match buffer.toString('base64')"); - } - - if (Buffer) { - it("Converts ASCII charaters correctly", () => { - runTest(" !\"#$ %&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"); - }); - - it("Converts escape sequences correctly", () => { - runTest("\t\n\r\\\"\'\u0062"); - }); - - it("Converts simple unicode characters correctly", () => { - runTest("ΠΣ ٵپ औठ ⺐⺠"); - }); - - it("Converts simple code snippet correctly", () => { - runTest(`/// +import { convertToBase64, sys } from "../ts"; +describe("unittests:: convertToBase64", () => { + function runTest(input: string): void { + const actual = convertToBase64(input); + const expected = sys.base64encode!(input); + assert.equal(actual, expected, "Encoded string using convertToBase64 does not match buffer.toString('base64')"); + } + if (Buffer) { + it("Converts ASCII charaters correctly", () => { + runTest(" !\"#$ %&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"); + }); + it("Converts escape sequences correctly", () => { + runTest("\t\n\r\\\"\'\u0062"); + }); + it("Converts simple unicode characters correctly", () => { + runTest("ΠΣ ٵپ औठ ⺐⺠"); + }); + it("Converts simple code snippet correctly", () => { + runTest(`/// var x: string = "string"; console.log(x);`); - }); - - it("Converts simple code snippet with unicode characters correctly", () => { - runTest(`var Π = 3.1415; console.log(Π);`); - }); - } - }); -} + }); + it("Converts simple code snippet with unicode characters correctly", () => { + runTest(`var Π = 3.1415; console.log(Π);`); + }); + } +}); diff --git a/src/testRunner/unittests/createMapShim.ts b/src/testRunner/unittests/createMapShim.ts index c49e1cc63ccc1..450beaecacf9b 100644 --- a/src/testRunner/unittests/createMapShim.ts +++ b/src/testRunner/unittests/createMapShim.ts @@ -1,107 +1,93 @@ -namespace ts { - describe("unittests:: createMapShim", () => { - - function testMapIterationAddedValues(map: Map, useForEach: boolean): string { - let resultString = ""; - - map.set("1", "1"); - map.set("3", "3"); - map.set("2", "2"); - map.set("4", "4"); - - let addedThree = false; - const doForEach = (value: string, key: string) => { - resultString += `${key}:${value};`; - - // Add a new key ("0") - the map should provide this - // one in the next iteration. - if (key === "1") { - map.set("1", "X1"); - map.set("0", "X0"); - map.set("4", "X4"); - } - else if (key === "3") { - if (!addedThree) { - addedThree = true; - - // Remove and re-add key "3"; the map should - // visit it after "0". - map.delete("3"); - map.set("3", "Y3"); - - // Change the value of "2"; the map should provide - // it when visiting the key. - map.set("2", "Y2"); - } - else { - // Check that an entry added when we visit the - // currently last entry will still be visited. - map.set("999", "999"); - } - } - else if (key === "999") { - // Ensure that clear() behaves correctly same as removing all keys. - map.set("A", "A"); - map.set("B", "B"); - map.set("C", "C"); - } - else if (key === "A") { - map.clear(); - map.set("Z", "Z"); +import { createMap, createMapShim } from "../ts"; +import * as ts from "../ts"; +describe("unittests:: createMapShim", () => { + function testMapIterationAddedValues(map: ts.Map, useForEach: boolean): string { + let resultString = ""; + map.set("1", "1"); + map.set("3", "3"); + map.set("2", "2"); + map.set("4", "4"); + let addedThree = false; + const doForEach = (value: string, key: string) => { + resultString += `${key}:${value};`; + // Add a new key ("0") - the map should provide this + // one in the next iteration. + if (key === "1") { + map.set("1", "X1"); + map.set("0", "X0"); + map.set("4", "X4"); + } + else if (key === "3") { + if (!addedThree) { + addedThree = true; + // Remove and re-add key "3"; the map should + // visit it after "0". + map.delete("3"); + map.set("3", "Y3"); + // Change the value of "2"; the map should provide + // it when visiting the key. + map.set("2", "Y2"); } - else if (key === "Z") { - // Check that the map behaves correctly when two items are - // added and removed immediately. - map.set("X", "X"); - map.set("X1", "X1"); - map.set("X2", "X2"); - map.delete("X1"); - map.delete("X2"); - map.set("Y", "Y"); + else { + // Check that an entry added when we visit the + // currently last entry will still be visited. + map.set("999", "999"); } - }; - - if (useForEach) { - map.forEach(doForEach); } - else { - // Use an iterator. - const iterator = map.entries(); - while (true) { - const iterResult = iterator.next(); - if (iterResult.done) { - break; - } - - const [key, value] = iterResult.value; - doForEach(value, key); + else if (key === "999") { + // Ensure that clear() behaves correctly same as removing all keys. + map.set("A", "A"); + map.set("B", "B"); + map.set("C", "C"); + } + else if (key === "A") { + map.clear(); + map.set("Z", "Z"); + } + else if (key === "Z") { + // Check that the map behaves correctly when two items are + // added and removed immediately. + map.set("X", "X"); + map.set("X1", "X1"); + map.set("X2", "X2"); + map.delete("X1"); + map.delete("X2"); + map.set("Y", "Y"); + } + }; + if (useForEach) { + map.forEach(doForEach); + } + else { + // Use an iterator. + const iterator = map.entries(); + while (true) { + const iterResult = iterator.next(); + if (iterResult.done) { + break; } + const [key, value] = iterResult.value; + doForEach(value, key); } - - return resultString; } - - it("iterates values in insertion order and handles changes", () => { - const expectedResult = "1:1;3:3;2:Y2;4:X4;0:X0;3:Y3;999:999;A:A;Z:Z;X:X;Y:Y;"; - - // First, ensure the test actually has the same behavior as a native Map. - let nativeMap = createMap(); - const nativeMapForEachResult = testMapIterationAddedValues(nativeMap, /* useForEach */ true); - assert.equal(nativeMapForEachResult, expectedResult, "nativeMap-forEach"); - - nativeMap = createMap(); - const nativeMapIteratorResult = testMapIterationAddedValues(nativeMap, /* useForEach */ false); - assert.equal(nativeMapIteratorResult, expectedResult, "nativeMap-iterator"); - - // Then, test the map shim. - const MapShim = createMapShim(); // tslint:disable-line variable-name - let localShimMap = new MapShim(); - const shimMapForEachResult = testMapIterationAddedValues(localShimMap, /* useForEach */ true); - assert.equal(shimMapForEachResult, expectedResult, "shimMap-forEach"); - - localShimMap = new MapShim(); - const shimMapIteratorResult = testMapIterationAddedValues(localShimMap, /* useForEach */ false); - assert.equal(shimMapIteratorResult, expectedResult, "shimMap-iterator"); - }); + return resultString; + } + it("iterates values in insertion order and handles changes", () => { + const expectedResult = "1:1;3:3;2:Y2;4:X4;0:X0;3:Y3;999:999;A:A;Z:Z;X:X;Y:Y;"; + // First, ensure the test actually has the same behavior as a native Map. + let nativeMap = createMap(); + const nativeMapForEachResult = testMapIterationAddedValues(nativeMap, /* useForEach */ true); + assert.equal(nativeMapForEachResult, expectedResult, "nativeMap-forEach"); + nativeMap = createMap(); + const nativeMapIteratorResult = testMapIterationAddedValues(nativeMap, /* useForEach */ false); + assert.equal(nativeMapIteratorResult, expectedResult, "nativeMap-iterator"); + // Then, test the map shim. + const MapShim = createMapShim(); // tslint:disable-line variable-name + let localShimMap = new MapShim(); + const shimMapForEachResult = testMapIterationAddedValues(localShimMap, /* useForEach */ true); + assert.equal(shimMapForEachResult, expectedResult, "shimMap-forEach"); + localShimMap = new MapShim(); + const shimMapIteratorResult = testMapIterationAddedValues(localShimMap, /* useForEach */ false); + assert.equal(shimMapIteratorResult, expectedResult, "shimMap-iterator"); }); -} +}); diff --git a/src/testRunner/unittests/customTransforms.ts b/src/testRunner/unittests/customTransforms.ts index 2a2309d4b8786..d30283948d259 100644 --- a/src/testRunner/unittests/customTransforms.ts +++ b/src/testRunner/unittests/customTransforms.ts @@ -1,36 +1,39 @@ -namespace ts { - describe("unittests:: customTransforms", () => { - function emitsCorrectly(name: string, sources: { file: string, text: string }[], customTransformers: CustomTransformers, options: CompilerOptions = {}) { - it(name, () => { - const roots = sources.map(source => createSourceFile(source.file, source.text, ScriptTarget.ES2015)); - const fileMap = arrayToMap(roots, file => file.fileName); - const outputs = createMap(); - const host: CompilerHost = { - getSourceFile: (fileName) => fileMap.get(fileName), - getDefaultLibFileName: () => "lib.d.ts", - getCurrentDirectory: () => "", - getDirectories: () => [], - getCanonicalFileName: (fileName) => fileName, - useCaseSensitiveFileNames: () => true, - getNewLine: () => "\n", - fileExists: (fileName) => fileMap.has(fileName), - readFile: (fileName) => fileMap.has(fileName) ? fileMap.get(fileName)!.text : undefined, - writeFile: (fileName, text) => outputs.set(fileName, text), - }; - - const program = createProgram(arrayFrom(fileMap.keys()), { newLine: NewLineKind.LineFeed, ...options }, host); - program.emit(/*targetSourceFile*/ undefined, host.writeFile, /*cancellationToken*/ undefined, /*emitOnlyDtsFiles*/ false, customTransformers); - let content = ""; - for (const [file, text] of arrayFrom(outputs.entries())) { - if (content) content += "\n\n"; - content += `// [${file}]\n`; - content += text; - } - Harness.Baseline.runBaseline(`customTransforms/${name}.js`, content); - }); - } - - const sources = [{ +import { CustomTransformers, CompilerOptions, createSourceFile, ScriptTarget, arrayToMap, createMap, CompilerHost, createProgram, arrayFrom, NewLineKind, TransformerFactory, SourceFile, visitEachChild, Node, VisitResult, SyntaxKind, FunctionDeclaration, addSyntheticLeadingComment, VariableStatement, visitNode, isStringLiteral, createLiteral, ModuleKind, computeLineStarts, setSourceMapRange, computeLineAndCharacterOfPosition, Transformer, isIdentifier, createIdentifier, createSourceMapSource, createBundle, map } from "../ts"; +import { Baseline } from "../Harness"; +describe("unittests:: customTransforms", () => { + function emitsCorrectly(name: string, sources: { + file: string; + text: string; + }[], customTransformers: CustomTransformers, options: CompilerOptions = {}) { + it(name, () => { + const roots = sources.map(source => createSourceFile(source.file, source.text, ScriptTarget.ES2015)); + const fileMap = arrayToMap(roots, file => file.fileName); + const outputs = createMap(); + const host: CompilerHost = { + getSourceFile: (fileName) => fileMap.get(fileName), + getDefaultLibFileName: () => "lib.d.ts", + getCurrentDirectory: () => "", + getDirectories: () => [], + getCanonicalFileName: (fileName) => fileName, + useCaseSensitiveFileNames: () => true, + getNewLine: () => "\n", + fileExists: (fileName) => fileMap.has(fileName), + readFile: (fileName) => fileMap.has(fileName) ? fileMap.get(fileName)!.text : undefined, + writeFile: (fileName, text) => outputs.set(fileName, text), + }; + const program = createProgram(arrayFrom(fileMap.keys()), { newLine: NewLineKind.LineFeed, ...options }, host); + program.emit(/*targetSourceFile*/ undefined, host.writeFile, /*cancellationToken*/ undefined, /*emitOnlyDtsFiles*/ false, customTransformers); + let content = ""; + for (const [file, text] of arrayFrom(outputs.entries())) { + if (content) + content += "\n\n"; + content += `// [${file}]\n`; + content += text; + } + Baseline.runBaseline(`customTransforms/${name}.js`, content); + }); + } + const sources = [{ file: "source.ts", text: ` function f1() { } @@ -40,44 +43,40 @@ namespace ts { function f2() { } // trailing ` }]; - - const before: TransformerFactory = context => { - return file => visitEachChild(file, visit, context); - function visit(node: Node): VisitResult { - switch (node.kind) { - case SyntaxKind.FunctionDeclaration: - return visitFunction(node); - default: - return visitEachChild(node, visit, context); - } + const before: TransformerFactory = context => { + return file => visitEachChild(file, visit, context); + function visit(node: Node): VisitResult { + switch (node.kind) { + case SyntaxKind.FunctionDeclaration: + return visitFunction((node)); + default: + return visitEachChild(node, visit, context); } - function visitFunction(node: FunctionDeclaration) { - addSyntheticLeadingComment(node, SyntaxKind.MultiLineCommentTrivia, "@before", /*hasTrailingNewLine*/ true); - return node; - } - }; - - const after: TransformerFactory = context => { - return file => visitEachChild(file, visit, context); - function visit(node: Node): VisitResult { - switch (node.kind) { - case SyntaxKind.VariableStatement: - return visitVariableStatement(node); - default: - return visitEachChild(node, visit, context); - } - } - function visitVariableStatement(node: VariableStatement) { - addSyntheticLeadingComment(node, SyntaxKind.SingleLineCommentTrivia, "@after"); - return node; + } + function visitFunction(node: FunctionDeclaration) { + addSyntheticLeadingComment(node, SyntaxKind.MultiLineCommentTrivia, "@before", /*hasTrailingNewLine*/ true); + return node; + } + }; + const after: TransformerFactory = context => { + return file => visitEachChild(file, visit, context); + function visit(node: Node): VisitResult { + switch (node.kind) { + case SyntaxKind.VariableStatement: + return visitVariableStatement((node)); + default: + return visitEachChild(node, visit, context); } - }; - - emitsCorrectly("before", sources, { before: [before] }); - emitsCorrectly("after", sources, { after: [after] }); - emitsCorrectly("both", sources, { before: [before], after: [after] }); - - emitsCorrectly("before+decorators", [{ + } + function visitVariableStatement(node: VariableStatement) { + addSyntheticLeadingComment(node, SyntaxKind.SingleLineCommentTrivia, "@after"); + return node; + } + }; + emitsCorrectly("before", sources, { before: [before] }); + emitsCorrectly("after", sources, { after: [after] }); + emitsCorrectly("both", sources, { before: [before], after: [after] }); + emitsCorrectly("before+decorators", [{ file: "source.ts", text: ` declare const dec: any; @@ -85,84 +84,73 @@ namespace ts { @dec export class C { constructor(b: B) { } } 'change' ` - }], {before: [ + }], { before: [ context => node => visitNode(node, function visitor(node: Node): Node { - if (isStringLiteral(node) && node.text === "change") return createLiteral("changed"); + if (isStringLiteral(node) && node.text === "change") + return createLiteral("changed"); return visitEachChild(node, visitor, context); }) - ]}, { - target: ScriptTarget.ES5, - module: ModuleKind.ES2015, - emitDecoratorMetadata: true, - experimentalDecorators: true - }); - - emitsCorrectly("sourceMapExternalSourceFiles", - [ - { - file: "source.ts", - // The text of length 'changed' is made to be on two lines so we know the line map change - text: `\`multi + ] }, { + target: ScriptTarget.ES5, + module: ModuleKind.ES2015, + emitDecoratorMetadata: true, + experimentalDecorators: true + }); + emitsCorrectly("sourceMapExternalSourceFiles", [ + { + file: "source.ts", + // The text of length 'changed' is made to be on two lines so we know the line map change + text: `\`multi line\` 'change'` - }, - ], - { - before: [ - context => node => visitNode(node, function visitor(node: Node): Node { - if (isStringLiteral(node) && node.text === "change") { - const text = "'changed'"; - const lineMap = computeLineStarts(text); - setSourceMapRange(node, { - pos: 0, end: text.length, source: { - text, - fileName: "another.html", - lineMap, - getLineAndCharacterOfPosition: pos => computeLineAndCharacterOfPosition(lineMap, pos) - } - }); - return node; + }, + ], { + before: [ + context => node => visitNode(node, function visitor(node: Node): Node { + if (isStringLiteral(node) && node.text === "change") { + const text = "'changed'"; + const lineMap = computeLineStarts(text); + setSourceMapRange(node, { + pos: 0, end: text.length, source: { + text, + fileName: "another.html", + lineMap, + getLineAndCharacterOfPosition: pos => computeLineAndCharacterOfPosition(lineMap, pos) } - return visitEachChild(node, visitor, context); - }) - ] - }, - { sourceMap: true } - ); - - emitsCorrectly("skipTriviaExternalSourceFiles", - [ - { - file: "source.ts", - // The source file contains preceding trivia (e.g. whitespace) to try to confuse the `skipSourceTrivia` function. - text: " original;" - }, - ], - { - before: [ - context => { - const transformSourceFile: Transformer = node => visitNode(node, function visitor(node: Node): Node { - if (isIdentifier(node) && node.text === "original") { - const newNode = createIdentifier("changed"); - setSourceMapRange(newNode, { - pos: 0, - end: 7, - // Do not provide a custom skipTrivia function for `source`. - source: createSourceMapSource("another.html", "changed;") - }); - return newNode; - } - return visitEachChild(node, visitor, context); + }); + return node; + } + return visitEachChild(node, visitor, context); + }) + ] + }, { sourceMap: true }); + emitsCorrectly("skipTriviaExternalSourceFiles", [ + { + file: "source.ts", + // The source file contains preceding trivia (e.g. whitespace) to try to confuse the `skipSourceTrivia` function. + text: " original;" + }, + ], { + before: [ + context => { + const transformSourceFile: Transformer = node => visitNode(node, function visitor(node: Node): Node { + if (isIdentifier(node) && node.text === "original") { + const newNode = createIdentifier("changed"); + setSourceMapRange(newNode, { + pos: 0, + end: 7, + // Do not provide a custom skipTrivia function for `source`. + source: createSourceMapSource("another.html", "changed;") }); - return { - transformSourceFile, - transformBundle: node => createBundle(map(node.sourceFiles, transformSourceFile), node.prepends), - }; + return newNode; } - ] - }, - { sourceMap: true, outFile: "source.js" } - ); - - }); -} + return visitEachChild(node, visitor, context); + }); + return { + transformSourceFile, + transformBundle: node => createBundle(map(node.sourceFiles, transformSourceFile), node.prepends), + }; + } + ] + }, { sourceMap: true, outFile: "source.js" }); +}); diff --git a/src/testRunner/unittests/evaluation/asyncArrow.ts b/src/testRunner/unittests/evaluation/asyncArrow.ts index 04a7af613fa38..2fef9e309dc51 100644 --- a/src/testRunner/unittests/evaluation/asyncArrow.ts +++ b/src/testRunner/unittests/evaluation/asyncArrow.ts @@ -1,7 +1,8 @@ +import { evaluateTypeScript } from "../../evaluator"; describe("unittests:: evaluation:: asyncArrowEvaluation", () => { // https://github.com/Microsoft/TypeScript/issues/24722 it("this capture (es5)", async () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` export class A { b = async (...args: any[]) => { await Promise.resolve(); diff --git a/src/testRunner/unittests/evaluation/asyncGenerator.ts b/src/testRunner/unittests/evaluation/asyncGenerator.ts index 8ca531f0759a1..bbcee59892ff4 100644 --- a/src/testRunner/unittests/evaluation/asyncGenerator.ts +++ b/src/testRunner/unittests/evaluation/asyncGenerator.ts @@ -1,6 +1,8 @@ +import { evaluateTypeScript } from "../../evaluator"; +import { ScriptTarget } from "../../ts"; describe("unittests:: evaluation:: asyncGeneratorEvaluation", () => { it("return (es5)", async () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` async function * g() { return Promise.resolve(0); } @@ -14,14 +16,14 @@ describe("unittests:: evaluation:: asyncGeneratorEvaluation", () => { ]); }); it("return (es2015)", async () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` async function * g() { return Promise.resolve(0); } export const output: any[] = []; export async function main() { output.push(await g().next()); - }`, { target: ts.ScriptTarget.ES2015 }); + }`, { target: ScriptTarget.ES2015 }); await result.main(); assert.deepEqual(result.output, [ { value: 0, done: true } diff --git a/src/testRunner/unittests/evaluation/awaiter.ts b/src/testRunner/unittests/evaluation/awaiter.ts index 65cb4e0ead945..5a0ea263d8271 100644 --- a/src/testRunner/unittests/evaluation/awaiter.ts +++ b/src/testRunner/unittests/evaluation/awaiter.ts @@ -1,7 +1,8 @@ +import { evaluateTypeScript } from "../../evaluator"; describe("unittests:: evaluation:: awaiter", () => { // NOTE: This could break if the ECMAScript spec ever changes the timing behavior for Promises (again) it("await (es5)", async () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` async function a(msg: string) { await Promise.resolve(); output.push(msg); diff --git a/src/testRunner/unittests/evaluation/forAwaitOf.ts b/src/testRunner/unittests/evaluation/forAwaitOf.ts index c7be018cbc9e6..731ce34951f1c 100644 --- a/src/testRunner/unittests/evaluation/forAwaitOf.ts +++ b/src/testRunner/unittests/evaluation/forAwaitOf.ts @@ -1,6 +1,8 @@ +import { evaluateTypeScript } from "../../evaluator"; +import { ScriptTarget } from "../../ts"; describe("unittests:: evaluation:: forAwaitOfEvaluation", () => { it("sync (es5)", async () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` let i = 0; const iterator: IterableIterator = { [Symbol.iterator]() { return this; }, @@ -24,9 +26,8 @@ describe("unittests:: evaluation:: forAwaitOfEvaluation", () => { assert.strictEqual(result.output[1], 2); assert.strictEqual(result.output[2], 3); }); - it("sync (es2015)", async () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` let i = 0; const iterator: IterableIterator = { [Symbol.iterator]() { return this; }, @@ -44,15 +45,14 @@ describe("unittests:: evaluation:: forAwaitOfEvaluation", () => { for await (const item of iterator) { output.push(item); } - }`, { target: ts.ScriptTarget.ES2015 }); + }`, { target: ScriptTarget.ES2015 }); await result.main(); assert.strictEqual(result.output[0], 1); assert.strictEqual(result.output[1], 2); assert.strictEqual(result.output[2], 3); }); - it("async (es5)", async () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` let i = 0; const iterator = { [Symbol.asyncIterator](): AsyncIterableIterator { return this; }, @@ -76,9 +76,8 @@ describe("unittests:: evaluation:: forAwaitOfEvaluation", () => { assert.instanceOf(result.output[1], Promise); assert.instanceOf(result.output[2], Promise); }); - it("async (es2015)", async () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` let i = 0; const iterator = { [Symbol.asyncIterator](): AsyncIterableIterator { return this; }, @@ -96,7 +95,7 @@ describe("unittests:: evaluation:: forAwaitOfEvaluation", () => { for await (const item of iterator) { output.push(item); } - }`, { target: ts.ScriptTarget.ES2015 }); + }`, { target: ScriptTarget.ES2015 }); await result.main(); assert.strictEqual(result.output[0], 1); assert.instanceOf(result.output[1], Promise); diff --git a/src/testRunner/unittests/evaluation/forOf.ts b/src/testRunner/unittests/evaluation/forOf.ts index 9efdf16d351cd..4068a136d9bf8 100644 --- a/src/testRunner/unittests/evaluation/forOf.ts +++ b/src/testRunner/unittests/evaluation/forOf.ts @@ -1,6 +1,8 @@ +import { evaluateTypeScript } from "../../evaluator"; +import { ScriptTarget } from "../../ts"; describe("unittests:: evaluation:: forOfEvaluation", () => { it("es5 over a array with no Symbol", () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` Symbol = undefined; export var output = []; export function main() { @@ -10,18 +12,14 @@ describe("unittests:: evaluation:: forOfEvaluation", () => { output.push(value); } } - `, { downlevelIteration: true, target: ts.ScriptTarget.ES5 }); - + `, { downlevelIteration: true, target: ScriptTarget.ES5 }); result.main(); - assert.strictEqual(result.output[0], 1); assert.strictEqual(result.output[1], 2); assert.strictEqual(result.output[2], 3); - }); - it("es5 over a string with no Symbol", () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` Symbol = undefined; export var output = []; export function main() { @@ -31,19 +29,16 @@ describe("unittests:: evaluation:: forOfEvaluation", () => { output.push(value); } } - `, { downlevelIteration: true, target: ts.ScriptTarget.ES5 }); - + `, { downlevelIteration: true, target: ScriptTarget.ES5 }); result.main(); - assert.strictEqual(result.output[0], "h"); assert.strictEqual(result.output[1], "e"); assert.strictEqual(result.output[2], "l"); assert.strictEqual(result.output[3], "l"); assert.strictEqual(result.output[4], "o"); }); - it("es5 over undefined with no Symbol", () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` Symbol = undefined; export function main() { let x = undefined; @@ -51,26 +46,22 @@ describe("unittests:: evaluation:: forOfEvaluation", () => { for (let value of x) { } } - `, { downlevelIteration: true, target: ts.ScriptTarget.ES5 }); - + `, { downlevelIteration: true, target: ScriptTarget.ES5 }); assert.throws(() => result.main(), "Symbol.iterator is not defined"); }); - it("es5 over undefined with Symbol", () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` export function main() { let x = undefined; for (let value of x) { } } - `, { downlevelIteration: true, target: ts.ScriptTarget.ES5 }); - + `, { downlevelIteration: true, target: ScriptTarget.ES5 }); assert.throws(() => result.main(), /cannot read property.*Symbol\(Symbol\.iterator\).*/i); }); - it("es5 over object with no Symbol.iterator with no Symbol", () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` Symbol = undefined; export function main() { let x = {} as any; @@ -78,26 +69,22 @@ describe("unittests:: evaluation:: forOfEvaluation", () => { for (let value of x) { } } - `, { downlevelIteration: true, target: ts.ScriptTarget.ES5 }); - + `, { downlevelIteration: true, target: ScriptTarget.ES5 }); assert.throws(() => result.main(), "Symbol.iterator is not defined"); }); - it("es5 over object with no Symbol.iterator with Symbol", () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` export function main() { let x = {} as any; for (let value of x) { } } - `, { downlevelIteration: true, target: ts.ScriptTarget.ES5 }); - + `, { downlevelIteration: true, target: ScriptTarget.ES5 }); assert.throws(() => result.main(), "Object is not iterable"); }); - it("es5 over object with Symbol.iterator", () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` export var output = []; export function main() { let thing : any = {}; @@ -111,9 +98,7 @@ describe("unittests:: evaluation:: forOfEvaluation", () => { output.push(value) } - }`, { downlevelIteration: true, target: ts.ScriptTarget.ES5 }); - + }`, { downlevelIteration: true, target: ScriptTarget.ES5 }); result.main(); }); - -}); \ No newline at end of file +}); diff --git a/src/testRunner/unittests/evaluation/objectRest.ts b/src/testRunner/unittests/evaluation/objectRest.ts index 272ba51ffbeb5..2ca6a8ff4852d 100644 --- a/src/testRunner/unittests/evaluation/objectRest.ts +++ b/src/testRunner/unittests/evaluation/objectRest.ts @@ -1,7 +1,8 @@ +import { evaluateTypeScript } from "../../evaluator"; describe("unittests:: evaluation:: objectRest", () => { // https://github.com/microsoft/TypeScript/issues/31469 it("side effects in property assignment", async () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` const k = { a: 1, b: 2 }; const o = { a: 3, ...k, b: k.a++ }; export const output = o; @@ -9,7 +10,7 @@ describe("unittests:: evaluation:: objectRest", () => { assert.deepEqual(result.output, { a: 1, b: 1 }); }); it("side effects in during spread", async () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` const k = { a: 1, get b() { l = { c: 9 }; return 2; } }; let l = { c: 3 }; const o = { ...k, ...l }; @@ -18,7 +19,7 @@ describe("unittests:: evaluation:: objectRest", () => { assert.deepEqual(result.output, { a: 1, b: 2, c: 9 }); }); it("trailing literal-valued object-literal", async () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` const k = { a: 1 } const o = { ...k, ...{ b: 2 } }; export const output = o; diff --git a/src/testRunner/unittests/evaluation/optionalCall.ts b/src/testRunner/unittests/evaluation/optionalCall.ts index d6317ad0a8ca9..b456887b9f989 100644 --- a/src/testRunner/unittests/evaluation/optionalCall.ts +++ b/src/testRunner/unittests/evaluation/optionalCall.ts @@ -1,6 +1,7 @@ +import { evaluateTypeScript } from "../../evaluator"; describe("unittests:: evaluation:: optionalCall", () => { it("f?.()", async () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` function f(a) { output.push(a); output.push(this); @@ -12,7 +13,7 @@ describe("unittests:: evaluation:: optionalCall", () => { assert.isUndefined(result.output[1]); }); it("o.f?.()", async () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` export const o = { f(a) { output.push(a); @@ -26,7 +27,7 @@ describe("unittests:: evaluation:: optionalCall", () => { assert.strictEqual(result.output[1], result.o); }); it("o.x.f?.()", async () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` export const o = { x: { f(a) { @@ -42,7 +43,7 @@ describe("unittests:: evaluation:: optionalCall", () => { assert.strictEqual(result.output[1], result.o.x); }); it("o?.f()", async () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` export const o = { f(a) { output.push(a); @@ -56,7 +57,7 @@ describe("unittests:: evaluation:: optionalCall", () => { assert.strictEqual(result.output[1], result.o); }); it("o?.f?.()", async () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` export const o = { f(a) { output.push(a); @@ -70,7 +71,7 @@ describe("unittests:: evaluation:: optionalCall", () => { assert.strictEqual(result.output[1], result.o); }); it("o.x?.f()", async () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` export const o = { x: { f(a) { @@ -86,7 +87,7 @@ describe("unittests:: evaluation:: optionalCall", () => { assert.strictEqual(result.output[1], result.o.x); }); it("o?.x.f()", async () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` export const o = { x: { f(a) { @@ -102,7 +103,7 @@ describe("unittests:: evaluation:: optionalCall", () => { assert.strictEqual(result.output[1], result.o.x); }); it("o?.x?.f()", async () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` export const o = { x: { f(a) { @@ -118,7 +119,7 @@ describe("unittests:: evaluation:: optionalCall", () => { assert.strictEqual(result.output[1], result.o.x); }); it("o?.x?.f?.()", async () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` export const o = { x: { f(a) { @@ -134,7 +135,7 @@ describe("unittests:: evaluation:: optionalCall", () => { assert.strictEqual(result.output[1], result.o.x); }); it("f?.()?.()", async () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` function g(a) { output.push(a); output.push(this); @@ -151,7 +152,7 @@ describe("unittests:: evaluation:: optionalCall", () => { assert.isUndefined(result.output[2]); }); it("f?.().f?.()", async () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` export const o = { f(a) { output.push(a); @@ -170,7 +171,7 @@ describe("unittests:: evaluation:: optionalCall", () => { assert.strictEqual(result.output[2], result.o); }); it("f?.()?.f?.()", async () => { - const result = evaluator.evaluateTypeScript(` + const result = evaluateTypeScript(` export const o = { f(a) { output.push(a); diff --git a/src/testRunner/unittests/factory.ts b/src/testRunner/unittests/factory.ts index ae4258287fedb..6d80e963ae98e 100644 --- a/src/testRunner/unittests/factory.ts +++ b/src/testRunner/unittests/factory.ts @@ -1,83 +1,67 @@ -namespace ts { - describe("unittests:: FactoryAPI", () => { - function assertSyntaxKind(node: Node, expected: SyntaxKind) { - assert.strictEqual(node.kind, expected, `Actual: ${Debug.formatSyntaxKind(node.kind)} Expected: ${Debug.formatSyntaxKind(expected)}`); - } - describe("createExportAssignment", () => { - it("parenthesizes default export if necessary", () => { - function checkExpression(expression: Expression) { - const node = createExportAssignment( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*isExportEquals*/ false, - expression, - ); - assertSyntaxKind(node.expression, SyntaxKind.ParenthesizedExpression); - } - - const clazz = createClassExpression(/*modifiers*/ undefined, "C", /*typeParameters*/ undefined, /*heritageClauses*/ undefined, [ - createProperty(/*decorators*/ undefined, [createToken(SyntaxKind.StaticKeyword)], "prop", /*questionOrExclamationToken*/ undefined, /*type*/ undefined, createLiteral("1")), - ]); - checkExpression(clazz); - checkExpression(createPropertyAccess(clazz, "prop")); - - const func = createFunctionExpression(/*modifiers*/ undefined, /*asteriskToken*/ undefined, "fn", /*typeParameters*/ undefined, /*parameters*/ undefined, /*type*/ undefined, createBlock([])); - checkExpression(func); - checkExpression(createCall(func, /*typeArguments*/ undefined, /*argumentsArray*/ undefined)); - checkExpression(createTaggedTemplate(func, createNoSubstitutionTemplateLiteral(""))); - - checkExpression(createBinary(createLiteral("a"), SyntaxKind.CommaToken, createLiteral("b"))); - checkExpression(createCommaList([createLiteral("a"), createLiteral("b")])); - }); +import { Node, SyntaxKind, Debug, Expression, createExportAssignment, createClassExpression, createProperty, createToken, createLiteral, createPropertyAccess, createFunctionExpression, createBlock, createCall, createTaggedTemplate, createNoSubstitutionTemplateLiteral, createBinary, createCommaList, ConciseBody, createArrowFunction, createObjectLiteral, createAsExpression, createTypeReferenceNode, createNonNullExpression, createIdentifier, BinaryOperator } from "../ts"; +describe("unittests:: FactoryAPI", () => { + function assertSyntaxKind(node: Node, expected: SyntaxKind) { + assert.strictEqual(node.kind, expected, `Actual: ${Debug.formatSyntaxKind(node.kind)} Expected: ${Debug.formatSyntaxKind(expected)}`); + } + describe("createExportAssignment", () => { + it("parenthesizes default export if necessary", () => { + function checkExpression(expression: Expression) { + const node = createExportAssignment( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*isExportEquals*/ false, expression); + assertSyntaxKind(node.expression, SyntaxKind.ParenthesizedExpression); + } + const clazz = createClassExpression(/*modifiers*/ undefined, "C", /*typeParameters*/ undefined, /*heritageClauses*/ undefined, [ + createProperty(/*decorators*/ undefined, [createToken(SyntaxKind.StaticKeyword)], "prop", /*questionOrExclamationToken*/ undefined, /*type*/ undefined, createLiteral("1")), + ]); + checkExpression(clazz); + checkExpression(createPropertyAccess(clazz, "prop")); + const func = createFunctionExpression(/*modifiers*/ undefined, /*asteriskToken*/ undefined, "fn", /*typeParameters*/ undefined, /*parameters*/ undefined, /*type*/ undefined, createBlock([])); + checkExpression(func); + checkExpression(createCall(func, /*typeArguments*/ undefined, /*argumentsArray*/ undefined)); + checkExpression(createTaggedTemplate(func, createNoSubstitutionTemplateLiteral(""))); + checkExpression(createBinary(createLiteral("a"), SyntaxKind.CommaToken, createLiteral("b"))); + checkExpression(createCommaList([createLiteral("a"), createLiteral("b")])); }); - - describe("createArrowFunction", () => { - it("parenthesizes concise body if necessary", () => { - function checkBody(body: ConciseBody) { - const node = createArrowFunction( - /*modifiers*/ undefined, - /*typeParameters*/ undefined, - [], - /*type*/ undefined, - /*equalsGreaterThanToken*/ undefined, - body, - ); - assertSyntaxKind(node.body, SyntaxKind.ParenthesizedExpression); - } - - checkBody(createObjectLiteral()); - checkBody(createPropertyAccess(createObjectLiteral(), "prop")); - checkBody(createAsExpression(createPropertyAccess(createObjectLiteral(), "prop"), createTypeReferenceNode("T", /*typeArguments*/ undefined))); - checkBody(createNonNullExpression(createPropertyAccess(createObjectLiteral(), "prop"))); - checkBody(createCommaList([createLiteral("a"), createLiteral("b")])); - checkBody(createBinary(createLiteral("a"), SyntaxKind.CommaToken, createLiteral("b"))); - }); + }); + describe("createArrowFunction", () => { + it("parenthesizes concise body if necessary", () => { + function checkBody(body: ConciseBody) { + const node = createArrowFunction( + /*modifiers*/ undefined, + /*typeParameters*/ undefined, [], + /*type*/ undefined, + /*equalsGreaterThanToken*/ undefined, body); + assertSyntaxKind(node.body, SyntaxKind.ParenthesizedExpression); + } + checkBody(createObjectLiteral()); + checkBody(createPropertyAccess(createObjectLiteral(), "prop")); + checkBody(createAsExpression(createPropertyAccess(createObjectLiteral(), "prop"), createTypeReferenceNode("T", /*typeArguments*/ undefined))); + checkBody(createNonNullExpression(createPropertyAccess(createObjectLiteral(), "prop"))); + checkBody(createCommaList([createLiteral("a"), createLiteral("b")])); + checkBody(createBinary(createLiteral("a"), SyntaxKind.CommaToken, createLiteral("b"))); }); - - describe("createBinaryExpression", () => { - it("parenthesizes arrow function in RHS if necessary", () => { - const lhs = createIdentifier("foo"); - const rhs = createArrowFunction( - /*modifiers*/ undefined, - /*typeParameters*/ undefined, - [], - /*type*/ undefined, - /*equalsGreaterThanToken*/ undefined, - createBlock([]), - ); - function checkRhs(operator: BinaryOperator, expectParens: boolean) { - const node = createBinary(lhs, operator, rhs); - assertSyntaxKind(node.right, expectParens ? SyntaxKind.ParenthesizedExpression : SyntaxKind.ArrowFunction); - } - - checkRhs(SyntaxKind.CommaToken, /*expectParens*/ false); - checkRhs(SyntaxKind.EqualsToken, /*expectParens*/ false); - checkRhs(SyntaxKind.PlusEqualsToken, /*expectParens*/ false); - checkRhs(SyntaxKind.BarBarToken, /*expectParens*/ true); - checkRhs(SyntaxKind.AmpersandAmpersandToken, /*expectParens*/ true); - checkRhs(SyntaxKind.QuestionQuestionToken, /*expectParens*/ true); - checkRhs(SyntaxKind.EqualsEqualsToken, /*expectParens*/ true); - }); + }); + describe("createBinaryExpression", () => { + it("parenthesizes arrow function in RHS if necessary", () => { + const lhs = createIdentifier("foo"); + const rhs = createArrowFunction( + /*modifiers*/ undefined, + /*typeParameters*/ undefined, [], + /*type*/ undefined, + /*equalsGreaterThanToken*/ undefined, createBlock([])); + function checkRhs(operator: BinaryOperator, expectParens: boolean) { + const node = createBinary(lhs, operator, rhs); + assertSyntaxKind(node.right, expectParens ? SyntaxKind.ParenthesizedExpression : SyntaxKind.ArrowFunction); + } + checkRhs(SyntaxKind.CommaToken, /*expectParens*/ false); + checkRhs(SyntaxKind.EqualsToken, /*expectParens*/ false); + checkRhs(SyntaxKind.PlusEqualsToken, /*expectParens*/ false); + checkRhs(SyntaxKind.BarBarToken, /*expectParens*/ true); + checkRhs(SyntaxKind.AmpersandAmpersandToken, /*expectParens*/ true); + checkRhs(SyntaxKind.QuestionQuestionToken, /*expectParens*/ true); + checkRhs(SyntaxKind.EqualsEqualsToken, /*expectParens*/ true); }); }); -} +}); diff --git a/src/testRunner/unittests/incrementalParser.ts b/src/testRunner/unittests/incrementalParser.ts index 8e00feefc6eee..4ad85ea16bb90 100644 --- a/src/testRunner/unittests/incrementalParser.ts +++ b/src/testRunner/unittests/incrementalParser.ts @@ -1,835 +1,613 @@ -namespace ts { - function withChange(text: IScriptSnapshot, start: number, length: number, newText: string): { text: IScriptSnapshot; textChangeRange: TextChangeRange; } { - const contents = getSnapshotText(text); - const newContents = contents.substr(0, start) + newText + contents.substring(start + length); - - return { text: ScriptSnapshot.fromString(newContents), textChangeRange: createTextChangeRange(createTextSpan(start, length), newText.length) }; - } - - function withInsert(text: IScriptSnapshot, start: number, newText: string): { text: IScriptSnapshot; textChangeRange: TextChangeRange; } { - return withChange(text, start, 0, newText); - } - - function withDelete(text: IScriptSnapshot, start: number, length: number): { text: IScriptSnapshot; textChangeRange: TextChangeRange; } { - return withChange(text, start, length, ""); - } - - function createTree(text: IScriptSnapshot, version: string) { - return createLanguageServiceSourceFile(/*fileName:*/ "", text, ScriptTarget.Latest, version, /*setNodeParents:*/ true); - } - - function assertSameDiagnostics(file1: SourceFile, file2: SourceFile) { - const diagnostics1 = file1.parseDiagnostics; - const diagnostics2 = file2.parseDiagnostics; - - assert.equal(diagnostics1.length, diagnostics2.length, "diagnostics1.length !== diagnostics2.length"); - for (let i = 0; i < diagnostics1.length; i++) { - const d1 = diagnostics1[i]; - const d2 = diagnostics2[i]; - - assert.equal(d1.file, file1, "d1.file !== file1"); - assert.equal(d2.file, file2, "d2.file !== file2"); - assert.equal(d1.start, d2.start, "d1.start !== d2.start"); - assert.equal(d1.length, d2.length, "d1.length !== d2.length"); - assert.equal(d1.messageText, d2.messageText, "d1.messageText !== d2.messageText"); - assert.equal(d1.category, d2.category, "d1.category !== d2.category"); - assert.equal(d1.code, d2.code, "d1.code !== d2.code"); - } - } - - // NOTE: 'reusedElements' is the expected count of elements reused from the old tree to the new - // tree. It may change as we tweak the parser. If the count increases then that should always - // be a good thing. If it decreases, that's not great (less reusability), but that may be - // unavoidable. If it does decrease an investigation should be done to make sure that things - // are still ok and we're still appropriately reusing most of the tree. - function compareTrees(oldText: IScriptSnapshot, newText: IScriptSnapshot, textChangeRange: TextChangeRange, expectedReusedElements: number, oldTree?: SourceFile) { - oldTree = oldTree || createTree(oldText, /*version:*/ "."); - Utils.assertInvariants(oldTree, /*parent:*/ undefined); - - // Create a tree for the new text, in a non-incremental fashion. - const newTree = createTree(newText, oldTree.version + "."); - Utils.assertInvariants(newTree, /*parent:*/ undefined); - - // Create a tree for the new text, in an incremental fashion. - const incrementalNewTree = updateLanguageServiceSourceFile(oldTree, newText, oldTree.version + ".", textChangeRange); - Utils.assertInvariants(incrementalNewTree, /*parent:*/ undefined); - - // We should get the same tree when doign a full or incremental parse. - Utils.assertStructuralEquals(newTree, incrementalNewTree); - - // We should also get the exact same set of diagnostics. - assertSameDiagnostics(newTree, incrementalNewTree); - - // There should be no reused nodes between two trees that are fully parsed. - assert.isTrue(reusedElements(oldTree, newTree) === 0); - - assert.equal(newTree.fileName, incrementalNewTree.fileName, "newTree.fileName !== incrementalNewTree.fileName"); - assert.equal(newTree.text, incrementalNewTree.text, "newTree.text !== incrementalNewTree.text"); - - if (expectedReusedElements !== -1) { - const actualReusedCount = reusedElements(oldTree, incrementalNewTree); - assert.equal(actualReusedCount, expectedReusedElements, actualReusedCount + " !== " + expectedReusedElements); - } - - return { oldTree, newTree, incrementalNewTree }; +import { IScriptSnapshot, TextChangeRange, getSnapshotText, ScriptSnapshot, createTextChangeRange, createTextSpan, createLanguageServiceSourceFile, ScriptTarget, SourceFile, updateLanguageServiceSourceFile, filter, contains, Node, forEachChild, bindSourceFile } from "../ts"; +import { assertInvariants, assertStructuralEquals } from "../Utils"; +function withChange(text: IScriptSnapshot, start: number, length: number, newText: string): { + text: IScriptSnapshot; + textChangeRange: TextChangeRange; +} { + const contents = getSnapshotText(text); + const newContents = contents.substr(0, start) + newText + contents.substring(start + length); + return { text: ScriptSnapshot.fromString(newContents), textChangeRange: createTextChangeRange(createTextSpan(start, length), newText.length) }; +} +function withInsert(text: IScriptSnapshot, start: number, newText: string): { + text: IScriptSnapshot; + textChangeRange: TextChangeRange; +} { + return withChange(text, start, 0, newText); +} +function withDelete(text: IScriptSnapshot, start: number, length: number): { + text: IScriptSnapshot; + textChangeRange: TextChangeRange; +} { + return withChange(text, start, length, ""); +} +function createTree(text: IScriptSnapshot, version: string) { + return createLanguageServiceSourceFile(/*fileName:*/ "", text, ScriptTarget.Latest, version, /*setNodeParents:*/ true); +} +function assertSameDiagnostics(file1: SourceFile, file2: SourceFile) { + const diagnostics1 = file1.parseDiagnostics; + const diagnostics2 = file2.parseDiagnostics; + assert.equal(diagnostics1.length, diagnostics2.length, "diagnostics1.length !== diagnostics2.length"); + for (let i = 0; i < diagnostics1.length; i++) { + const d1 = diagnostics1[i]; + const d2 = diagnostics2[i]; + assert.equal(d1.file, file1, "d1.file !== file1"); + assert.equal(d2.file, file2, "d2.file !== file2"); + assert.equal(d1.start, d2.start, "d1.start !== d2.start"); + assert.equal(d1.length, d2.length, "d1.length !== d2.length"); + assert.equal(d1.messageText, d2.messageText, "d1.messageText !== d2.messageText"); + assert.equal(d1.category, d2.category, "d1.category !== d2.category"); + assert.equal(d1.code, d2.code, "d1.code !== d2.code"); } - - function reusedElements(oldNode: SourceFile, newNode: SourceFile): number { - const allOldElements = collectElements(oldNode); - const allNewElements = collectElements(newNode); - - return filter(allOldElements, v => contains(allNewElements, v)).length; +} +// NOTE: 'reusedElements' is the expected count of elements reused from the old tree to the new +// tree. It may change as we tweak the parser. If the count increases then that should always +// be a good thing. If it decreases, that's not great (less reusability), but that may be +// unavoidable. If it does decrease an investigation should be done to make sure that things +// are still ok and we're still appropriately reusing most of the tree. +function compareTrees(oldText: IScriptSnapshot, newText: IScriptSnapshot, textChangeRange: TextChangeRange, expectedReusedElements: number, oldTree?: SourceFile) { + oldTree = oldTree || createTree(oldText, /*version:*/ "."); + assertInvariants(oldTree, /*parent:*/ undefined); + // Create a tree for the new text, in a non-incremental fashion. + const newTree = createTree(newText, oldTree.version + "."); + assertInvariants(newTree, /*parent:*/ undefined); + // Create a tree for the new text, in an incremental fashion. + const incrementalNewTree = updateLanguageServiceSourceFile(oldTree, newText, oldTree.version + ".", textChangeRange); + assertInvariants(incrementalNewTree, /*parent:*/ undefined); + // We should get the same tree when doign a full or incremental parse. + assertStructuralEquals(newTree, incrementalNewTree); + // We should also get the exact same set of diagnostics. + assertSameDiagnostics(newTree, incrementalNewTree); + // There should be no reused nodes between two trees that are fully parsed. + assert.isTrue(reusedElements(oldTree, newTree) === 0); + assert.equal(newTree.fileName, incrementalNewTree.fileName, "newTree.fileName !== incrementalNewTree.fileName"); + assert.equal(newTree.text, incrementalNewTree.text, "newTree.text !== incrementalNewTree.text"); + if (expectedReusedElements !== -1) { + const actualReusedCount = reusedElements(oldTree, incrementalNewTree); + assert.equal(actualReusedCount, expectedReusedElements, actualReusedCount + " !== " + expectedReusedElements); } - - function collectElements(node: Node) { - const result: Node[] = []; - visit(node); - return result; - - function visit(node: Node) { - result.push(node); - forEachChild(node, visit); - } + return { oldTree, newTree, incrementalNewTree }; +} +function reusedElements(oldNode: SourceFile, newNode: SourceFile): number { + const allOldElements = collectElements(oldNode); + const allNewElements = collectElements(newNode); + return filter(allOldElements, v => contains(allNewElements, v)).length; +} +function collectElements(node: Node) { + const result: Node[] = []; + visit(node); + return result; + function visit(node: Node) { + result.push(node); + forEachChild(node, visit); } - - function deleteCode(source: string, index: number, toDelete: string) { - const repeat = toDelete.length; - let oldTree = createTree(ScriptSnapshot.fromString(source), /*version:*/ "."); - for (let i = 0; i < repeat; i++) { - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withDelete(oldText, index, 1); - const newTree = compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1, oldTree).incrementalNewTree; - - source = getSnapshotText(newTextAndChange.text); - oldTree = newTree; - } +} +function deleteCode(source: string, index: number, toDelete: string) { + const repeat = toDelete.length; + let oldTree = createTree(ScriptSnapshot.fromString(source), /*version:*/ "."); + for (let i = 0; i < repeat; i++) { + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withDelete(oldText, index, 1); + const newTree = compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1, oldTree).incrementalNewTree; + source = getSnapshotText(newTextAndChange.text); + oldTree = newTree; } - - function insertCode(source: string, index: number, toInsert: string) { - const repeat = toInsert.length; - let oldTree = createTree(ScriptSnapshot.fromString(source), /*version:*/ "."); - for (let i = 0; i < repeat; i++) { - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, index + i, toInsert.charAt(i)); - const newTree = compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1, oldTree).incrementalNewTree; - - source = getSnapshotText(newTextAndChange.text); - oldTree = newTree; - } +} +function insertCode(source: string, index: number, toInsert: string) { + const repeat = toInsert.length; + let oldTree = createTree(ScriptSnapshot.fromString(source), /*version:*/ "."); + for (let i = 0; i < repeat; i++) { + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, index + i, toInsert.charAt(i)); + const newTree = compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1, oldTree).incrementalNewTree; + source = getSnapshotText(newTextAndChange.text); + oldTree = newTree; } - - describe("unittests:: Incremental Parser", () => { - it("Inserting into method", () => { - const source = "class C {\r\n" + - " public foo1() { }\r\n" + - " public foo2() {\r\n" + - " return 1;\r\n" + - " }\r\n" + - " public foo3() { }\r\n" + - "}"; - - const oldText = ScriptSnapshot.fromString(source); - const semicolonIndex = source.indexOf(";"); - const newTextAndChange = withInsert(oldText, semicolonIndex, " + 1"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 8); - }); - - it("Deleting from method", () => { - const source = "class C {\r\n" + - " public foo1() { }\r\n" + - " public foo2() {\r\n" + - " return 1 + 1;\r\n" + - " }\r\n" + - " public foo3() { }\r\n" + - "}"; - - const index = source.indexOf("+ 1"); - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withDelete(oldText, index, "+ 1".length); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 8); - }); - - it("Regular expression 1", () => { - const source = "class C { public foo1() { /; } public foo2() { return 1;} public foo3() { } }"; - - const semicolonIndex = source.indexOf(";}"); - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, semicolonIndex, "/"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); - - it("Regular expression 2", () => { - const source = "class C { public foo1() { ; } public foo2() { return 1/;} public foo3() { } }"; - - const semicolonIndex = source.indexOf(";"); - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, semicolonIndex, "/"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 4); - }); - - it("Comment 1", () => { - const source = "class C { public foo1() { /; } public foo2() { return 1; } public foo3() { } }"; - - const semicolonIndex = source.indexOf(";"); - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, semicolonIndex, "/"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); - - it("Comment 2", () => { - const source = "class C { public foo1() { /; } public foo2() { return 1; } public foo3() { } }"; - - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, 0, "//"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); - - it("Comment 3", () => { - const source = "//class C { public foo1() { /; } public foo2() { return 1; } public foo3() { } }"; - - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withDelete(oldText, 0, 2); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); - - it("Comment 4", () => { - const source = "class C { public foo1() { /; } public foo2() { */ return 1; } public foo3() { } }"; - - const index = source.indexOf(";"); - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, index, "*"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 4); - }); - - it("Parameter 1", () => { - // Should be able to reuse all the parameters. - const source = "class C {\r\n" + - " public foo2(a, b, c, d) {\r\n" + - " return 1;\r\n" + - " }\r\n" + - "}"; - - const semicolonIndex = source.indexOf(";"); - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, semicolonIndex, " + 1"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 8); - }); - - it("Type member 1", () => { - // Should be able to reuse most of the type members. - const source = "interface I { a: number; b: string; (c): d; new (e): f; g(): h }"; - - const index = source.indexOf(": string"); - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, index, "?"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 14); - }); - - it("Enum element 1", () => { - // Should be able to reuse most of the enum elements. - const source = "enum E { a = 1, b = 1 << 1, c = 3, e = 4, f = 5, g = 7, h = 8, i = 9, j = 10 }"; - - const index = source.indexOf("<<"); - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, index, 2, "+"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 24); - }); - - it("Strict mode 1", () => { - const source = "foo1();\r\nfoo1();\r\nfoo1();\r\package();"; - - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, 0, "'strict';\r\n"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 9); - }); - - it("Strict mode 2", () => { - const source = "foo1();\r\nfoo1();\r\nfoo1();\r\package();"; - - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, 0, "'use strict';\r\n"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 9); - }); - - it("Strict mode 3", () => { - const source = "'strict';\r\nfoo1();\r\nfoo1();\r\nfoo1();\r\npackage();"; - - const index = source.indexOf("f"); - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withDelete(oldText, 0, index); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 9); - }); - - it("Strict mode 4", () => { - const source = "'use strict';\r\nfoo1();\r\nfoo1();\r\nfoo1();\r\npackage();"; - - const index = source.indexOf("f"); - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withDelete(oldText, 0, index); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 9); - }); - - it("Strict mode 5", () => { - const source = "'use blahhh';\r\nfoo1();\r\nfoo2();\r\nfoo3();\r\nfoo4();\r\nfoo4();\r\nfoo6();\r\nfoo7();\r\nfoo8();\r\nfoo9();\r\n"; - - const index = source.indexOf("b"); - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, index, 6, "strict"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 27); - }); - - it("Strict mode 6", () => { - const source = "'use strict';\r\nfoo1();\r\nfoo2();\r\nfoo3();\r\nfoo4();\r\nfoo4();\r\nfoo6();\r\nfoo7();\r\nfoo8();\r\nfoo9();\r\n"; - - const index = source.indexOf("s"); - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, index, 6, "blahhh"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 27); - }); - - it("Strict mode 7", () => { - const source = "'use blahhh';\r\nfoo1();\r\nfoo2();\r\nfoo3();\r\nfoo4();\r\nfoo4();\r\nfoo6();\r\nfoo7();\r\nfoo8();\r\nfoo9();\r\n"; - - const index = source.indexOf("f"); - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withDelete(oldText, 0, index); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 24); - }); - - it("Parenthesized expression to arrow function 1", () => { - const source = "var v = (a, b, c, d, e)"; - - const index = source.indexOf("a"); - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, index + 1, ":"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); - - it("Parenthesized expression to arrow function 2", () => { - const source = "var v = (a, b) = c"; - - const index = source.indexOf("= c") + 1; - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, index, ">"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); - - it("Arrow function to parenthesized expression 1", () => { - const source = "var v = (a:, b, c, d, e)"; - - const index = source.indexOf(":"); - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withDelete(oldText, index, 1); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); - - it("Arrow function to parenthesized expression 2", () => { - const source = "var v = (a, b) => c"; - - const index = source.indexOf(">"); - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withDelete(oldText, index, 1); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); - - it("Speculative generic lookahead 1", () => { - const source = "var v = Fe"; - - const index = source.indexOf("b"); - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, index + 1, ",x"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1); - }); - - it("Speculative generic lookahead 2", () => { - const source = "var v = Fe"; - - const index = source.indexOf("b"); - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, index + 1, ",x"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1); - }); - - it("Speculative generic lookahead 3", () => { - const source = "var v = Fe"; - - const index = source.indexOf("b"); - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, index + 1, ",x"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1); - }); - - it("Speculative generic lookahead 4", () => { - const source = "var v = Fe"; - - const index = source.indexOf("b"); - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, index + 1, ",x"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1); - }); - - it("Assertion to arrow function", () => { - const source = "var v = (a);"; - - const index = source.indexOf(";"); - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, index, " => 1"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); - - it("Arrow function to assertion", () => { - const source = "var v = (a) => 1;"; - - const index = source.indexOf(" =>"); - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withDelete(oldText, index, " => 1".length); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); - - it("Contextual shift to shift-equals", () => { - const source = "var v = 1 >> = 2"; - - const index = source.indexOf(">> ="); - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withDelete(oldText, index + 2, 1); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); - - it("Contextual shift-equals to shift", () => { - const source = "var v = 1 >>= 2"; - - const index = source.indexOf(">>="); - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, index + 2, " "); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); - - it("Contextual shift to generic invocation", () => { - const source = "var v = T>>(2)"; - - const index = source.indexOf("T"); - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, index, "Foo { - const source = "var v = Foo>(2)"; - - const index = source.indexOf("Foo { - const source = "var v = T>>=2;"; - - const index = source.indexOf("="); - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, index, "= ".length, ": Foo { - const source = "var v : Foo>=2;"; - - const index = source.indexOf(":"); - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, index, ": Foo { - const source = "var v = new Dictionary0"; - - const index = source.indexOf("0"); - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, index, 1, "()"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); - - it("Type argument list to arithmetic operator", () => { - const source = "var v = new Dictionary()"; - - const index = source.indexOf("()"); - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withDelete(oldText, index, 2); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); - - it("Yield context 1", () => { - // We're changing from a non-generator to a genarator. We can't reuse statement nodes. - const source = "function foo() {\r\nyield(foo1);\r\n}"; - - const oldText = ScriptSnapshot.fromString(source); - const index = source.indexOf("foo"); - const newTextAndChange = withInsert(oldText, index, "*"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); - - it("Yield context 2", () => { - // We're changing from a generator to a non-genarator. We can't reuse statement nodes. - const source = "function *foo() {\r\nyield(foo1);\r\n}"; - - const oldText = ScriptSnapshot.fromString(source); - const index = source.indexOf("*"); - const newTextAndChange = withDelete(oldText, index, "*".length); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); - - it("Delete semicolon", () => { - const source = "export class Foo {\r\n}\r\n\r\nexport var foo = new Foo();\r\n\r\n export function test(foo: Foo) {\r\n return true;\r\n }\r\n"; - - const oldText = ScriptSnapshot.fromString(source); - const index = source.lastIndexOf(";"); - const newTextAndChange = withDelete(oldText, index, 1); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 14); - }); - - it("Edit after empty type parameter list", () => { - const source = "class Dictionary<> { }\r\nvar y;\r\n"; - - const oldText = ScriptSnapshot.fromString(source); - const index = source.length; - const newTextAndChange = withInsert(oldText, index, "var x;"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 2); - }); - - it("Delete parameter after comment", () => { - const source = "function fn(/* comment! */ a: number, c) { }"; - - const oldText = ScriptSnapshot.fromString(source); - const index = source.indexOf("a:"); - const newTextAndChange = withDelete(oldText, index, "a: number,".length); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); - - it("Modifier added to accessor", () => { - const source = - "class C {\ +} +describe("unittests:: Incremental Parser", () => { + it("Inserting into method", () => { + const source = "class C {\r\n" + + " public foo1() { }\r\n" + + " public foo2() {\r\n" + + " return 1;\r\n" + + " }\r\n" + + " public foo3() { }\r\n" + + "}"; + const oldText = ScriptSnapshot.fromString(source); + const semicolonIndex = source.indexOf(";"); + const newTextAndChange = withInsert(oldText, semicolonIndex, " + 1"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 8); + }); + it("Deleting from method", () => { + const source = "class C {\r\n" + + " public foo1() { }\r\n" + + " public foo2() {\r\n" + + " return 1 + 1;\r\n" + + " }\r\n" + + " public foo3() { }\r\n" + + "}"; + const index = source.indexOf("+ 1"); + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withDelete(oldText, index, "+ 1".length); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 8); + }); + it("Regular expression 1", () => { + const source = "class C { public foo1() { /; } public foo2() { return 1;} public foo3() { } }"; + const semicolonIndex = source.indexOf(";}"); + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, semicolonIndex, "/"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); + it("Regular expression 2", () => { + const source = "class C { public foo1() { ; } public foo2() { return 1/;} public foo3() { } }"; + const semicolonIndex = source.indexOf(";"); + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, semicolonIndex, "/"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 4); + }); + it("Comment 1", () => { + const source = "class C { public foo1() { /; } public foo2() { return 1; } public foo3() { } }"; + const semicolonIndex = source.indexOf(";"); + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, semicolonIndex, "/"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); + it("Comment 2", () => { + const source = "class C { public foo1() { /; } public foo2() { return 1; } public foo3() { } }"; + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, 0, "//"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); + it("Comment 3", () => { + const source = "//class C { public foo1() { /; } public foo2() { return 1; } public foo3() { } }"; + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withDelete(oldText, 0, 2); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); + it("Comment 4", () => { + const source = "class C { public foo1() { /; } public foo2() { */ return 1; } public foo3() { } }"; + const index = source.indexOf(";"); + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, index, "*"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 4); + }); + it("Parameter 1", () => { + // Should be able to reuse all the parameters. + const source = "class C {\r\n" + + " public foo2(a, b, c, d) {\r\n" + + " return 1;\r\n" + + " }\r\n" + + "}"; + const semicolonIndex = source.indexOf(";"); + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, semicolonIndex, " + 1"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 8); + }); + it("Type member 1", () => { + // Should be able to reuse most of the type members. + const source = "interface I { a: number; b: string; (c): d; new (e): f; g(): h }"; + const index = source.indexOf(": string"); + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, index, "?"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 14); + }); + it("Enum element 1", () => { + // Should be able to reuse most of the enum elements. + const source = "enum E { a = 1, b = 1 << 1, c = 3, e = 4, f = 5, g = 7, h = 8, i = 9, j = 10 }"; + const index = source.indexOf("<<"); + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, index, 2, "+"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 24); + }); + it("Strict mode 1", () => { + const source = "foo1();\r\nfoo1();\r\nfoo1();\r\package();"; + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, 0, "'strict';\r\n"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 9); + }); + it("Strict mode 2", () => { + const source = "foo1();\r\nfoo1();\r\nfoo1();\r\package();"; + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, 0, "'use strict';\r\n"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 9); + }); + it("Strict mode 3", () => { + const source = "'strict';\r\nfoo1();\r\nfoo1();\r\nfoo1();\r\npackage();"; + const index = source.indexOf("f"); + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withDelete(oldText, 0, index); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 9); + }); + it("Strict mode 4", () => { + const source = "'use strict';\r\nfoo1();\r\nfoo1();\r\nfoo1();\r\npackage();"; + const index = source.indexOf("f"); + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withDelete(oldText, 0, index); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 9); + }); + it("Strict mode 5", () => { + const source = "'use blahhh';\r\nfoo1();\r\nfoo2();\r\nfoo3();\r\nfoo4();\r\nfoo4();\r\nfoo6();\r\nfoo7();\r\nfoo8();\r\nfoo9();\r\n"; + const index = source.indexOf("b"); + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, index, 6, "strict"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 27); + }); + it("Strict mode 6", () => { + const source = "'use strict';\r\nfoo1();\r\nfoo2();\r\nfoo3();\r\nfoo4();\r\nfoo4();\r\nfoo6();\r\nfoo7();\r\nfoo8();\r\nfoo9();\r\n"; + const index = source.indexOf("s"); + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, index, 6, "blahhh"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 27); + }); + it("Strict mode 7", () => { + const source = "'use blahhh';\r\nfoo1();\r\nfoo2();\r\nfoo3();\r\nfoo4();\r\nfoo4();\r\nfoo6();\r\nfoo7();\r\nfoo8();\r\nfoo9();\r\n"; + const index = source.indexOf("f"); + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withDelete(oldText, 0, index); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 24); + }); + it("Parenthesized expression to arrow function 1", () => { + const source = "var v = (a, b, c, d, e)"; + const index = source.indexOf("a"); + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, index + 1, ":"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); + it("Parenthesized expression to arrow function 2", () => { + const source = "var v = (a, b) = c"; + const index = source.indexOf("= c") + 1; + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, index, ">"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); + it("Arrow function to parenthesized expression 1", () => { + const source = "var v = (a:, b, c, d, e)"; + const index = source.indexOf(":"); + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withDelete(oldText, index, 1); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); + it("Arrow function to parenthesized expression 2", () => { + const source = "var v = (a, b) => c"; + const index = source.indexOf(">"); + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withDelete(oldText, index, 1); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); + it("Speculative generic lookahead 1", () => { + const source = "var v = Fe"; + const index = source.indexOf("b"); + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, index + 1, ",x"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1); + }); + it("Speculative generic lookahead 2", () => { + const source = "var v = Fe"; + const index = source.indexOf("b"); + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, index + 1, ",x"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1); + }); + it("Speculative generic lookahead 3", () => { + const source = "var v = Fe"; + const index = source.indexOf("b"); + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, index + 1, ",x"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1); + }); + it("Speculative generic lookahead 4", () => { + const source = "var v = Fe"; + const index = source.indexOf("b"); + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, index + 1, ",x"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1); + }); + it("Assertion to arrow function", () => { + const source = "var v = (a);"; + const index = source.indexOf(";"); + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, index, " => 1"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); + it("Arrow function to assertion", () => { + const source = "var v = (a) => 1;"; + const index = source.indexOf(" =>"); + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withDelete(oldText, index, " => 1".length); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); + it("Contextual shift to shift-equals", () => { + const source = "var v = 1 >> = 2"; + const index = source.indexOf(">> ="); + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withDelete(oldText, index + 2, 1); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); + it("Contextual shift-equals to shift", () => { + const source = "var v = 1 >>= 2"; + const index = source.indexOf(">>="); + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, index + 2, " "); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); + it("Contextual shift to generic invocation", () => { + const source = "var v = T>>(2)"; + const index = source.indexOf("T"); + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, index, "Foo { + const source = "var v = Foo>(2)"; + const index = source.indexOf("Foo { + const source = "var v = T>>=2;"; + const index = source.indexOf("="); + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, index, "= ".length, ": Foo { + const source = "var v : Foo>=2;"; + const index = source.indexOf(":"); + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, index, ": Foo { + const source = "var v = new Dictionary0"; + const index = source.indexOf("0"); + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, index, 1, "()"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); + it("Type argument list to arithmetic operator", () => { + const source = "var v = new Dictionary()"; + const index = source.indexOf("()"); + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withDelete(oldText, index, 2); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); + it("Yield context 1", () => { + // We're changing from a non-generator to a genarator. We can't reuse statement nodes. + const source = "function foo() {\r\nyield(foo1);\r\n}"; + const oldText = ScriptSnapshot.fromString(source); + const index = source.indexOf("foo"); + const newTextAndChange = withInsert(oldText, index, "*"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); + it("Yield context 2", () => { + // We're changing from a generator to a non-genarator. We can't reuse statement nodes. + const source = "function *foo() {\r\nyield(foo1);\r\n}"; + const oldText = ScriptSnapshot.fromString(source); + const index = source.indexOf("*"); + const newTextAndChange = withDelete(oldText, index, "*".length); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); + it("Delete semicolon", () => { + const source = "export class Foo {\r\n}\r\n\r\nexport var foo = new Foo();\r\n\r\n export function test(foo: Foo) {\r\n return true;\r\n }\r\n"; + const oldText = ScriptSnapshot.fromString(source); + const index = source.lastIndexOf(";"); + const newTextAndChange = withDelete(oldText, index, 1); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 14); + }); + it("Edit after empty type parameter list", () => { + const source = "class Dictionary<> { }\r\nvar y;\r\n"; + const oldText = ScriptSnapshot.fromString(source); + const index = source.length; + const newTextAndChange = withInsert(oldText, index, "var x;"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 2); + }); + it("Delete parameter after comment", () => { + const source = "function fn(/* comment! */ a: number, c) { }"; + const oldText = ScriptSnapshot.fromString(source); + const index = source.indexOf("a:"); + const newTextAndChange = withDelete(oldText, index, "a: number,".length); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); + it("Modifier added to accessor", () => { + const source = "class C {\ set Bar(bar:string) {}\ }\ var o2 = { set Foo(val:number) { } };"; - - const oldText = ScriptSnapshot.fromString(source); - const index = source.indexOf("set"); - const newTextAndChange = withInsert(oldText, index, "public "); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 14); - }); - - it("Insert parameter ahead of parameter", () => { - const source = - "alert(100);\ + const oldText = ScriptSnapshot.fromString(source); + const index = source.indexOf("set"); + const newTextAndChange = withInsert(oldText, index, "public "); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 14); + }); + it("Insert parameter ahead of parameter", () => { + const source = "alert(100);\ \ class OverloadedMonster {\ constructor();\ constructor(name) { }\ }"; - - const oldText = ScriptSnapshot.fromString(source); - const index = source.indexOf("100"); - const newTextAndChange = withInsert(oldText, index, "'1', "); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 7); - }); - - it("Insert declare modifier before module", () => { - const source = - "module mAmbient {\ + const oldText = ScriptSnapshot.fromString(source); + const index = source.indexOf("100"); + const newTextAndChange = withInsert(oldText, index, "'1', "); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 7); + }); + it("Insert declare modifier before module", () => { + const source = "module mAmbient {\ module m3 { }\ }"; - - const oldText = ScriptSnapshot.fromString(source); - const index = 0; - const newTextAndChange = withInsert(oldText, index, "declare "); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); - - it("Insert function above arrow function with comment", () => { - const source = - "\ + const oldText = ScriptSnapshot.fromString(source); + const index = 0; + const newTextAndChange = withInsert(oldText, index, "declare "); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); + it("Insert function above arrow function with comment", () => { + const source = "\ () =>\ // do something\ 0;"; - - const oldText = ScriptSnapshot.fromString(source); - const index = 0; - const newTextAndChange = withInsert(oldText, index, "function Foo() { }"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); - - it("Finish incomplete regular expression", () => { - const source = "while (true) /3; return;"; - - const oldText = ScriptSnapshot.fromString(source); - const index = source.length - 1; - const newTextAndChange = withInsert(oldText, index, "/"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); - - it("Regular expression to divide operation", () => { - const source = "return;\r\nwhile (true) /3/g;"; - - const oldText = ScriptSnapshot.fromString(source); - const index = source.indexOf("while"); - const newTextAndChange = withDelete(oldText, index, "while ".length); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); - - it("Divide operation to regular expression", () => { - const source = "return;\r\n(true) /3/g;"; - - const oldText = ScriptSnapshot.fromString(source); - const index = source.indexOf("("); - const newTextAndChange = withInsert(oldText, index, "while "); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); - - it("Unterminated comment after keyword converted to identifier", () => { - // 'public' as a keyword should be incrementally unusable (because it has an - // unterminated comment). When we convert it to an identifier, that shouldn't - // change anything, and we should still get the same errors. - const source = "return; a.public /*"; - - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, 0, ""); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 7); - }); - - it("Class to interface", () => { - const source = "class A { public M1() { } public M2() { } public M3() { } p1 = 0; p2 = 0; p3 = 0 }"; - - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 0, "class".length, "interface"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); - - it("Interface to class", () => { - const source = "interface A { M1?(); M2?(); M3?(); p1?; p2?; p3? }"; - - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 0, "interface".length, "class"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); - - it("Surrounding function declarations with block", () => { - const source = "declare function F1() { } export function F2() { } declare export function F3() { }"; - - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withInsert(oldText, 0, "{"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 9); - }); - - it("Removing block around function declarations", () => { - const source = "{ declare function F1() { } export function F2() { } declare export function F3() { }"; - - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withDelete(oldText, 0, "{".length); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 9); - }); - - it("Moving methods from class to object literal", () => { - const source = "class C { public A() { } public B() { } public C() { } }"; - - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 0, "class C".length, "var v ="); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); - - it("Moving methods from object literal to class", () => { - const source = "var v = { public A() { } public B() { } public C() { } }"; - - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 0, "var v =".length, "class C"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 4); - }); - - it("Moving methods from object literal to class in strict mode", () => { - const source = "\"use strict\"; var v = { public A() { } public B() { } public C() { } }"; - - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 14, "var v =".length, "class C"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 4); - }); - - it("Do not move constructors from class to object-literal.", () => { - const source = "class C { public constructor() { } public constructor() { } public constructor() { } }"; - - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 0, "class C".length, "var v ="); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); - - it("Do not move methods called \"constructor\" from object literal to class", () => { - const source = "var v = { public constructor() { } public constructor() { } public constructor() { } }"; - - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 0, "var v =".length, "class C"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); - - it("Moving index signatures from class to interface", () => { - const source = "class C { public [a: number]: string; public [a: number]: string; public [a: number]: string }"; - - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 0, "class".length, "interface"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 18); - }); - - it("Moving index signatures from class to interface in strict mode", () => { - const source = "\"use strict\"; class C { public [a: number]: string; public [a: number]: string; public [a: number]: string }"; - - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 14, "class".length, "interface"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 18); - }); - - it("Moving index signatures from interface to class", () => { - const source = "interface C { public [a: number]: string; public [a: number]: string; public [a: number]: string }"; - - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 0, "interface".length, "class"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 18); - }); - - - it("Moving index signatures from interface to class in strict mode", () => { - const source = "\"use strict\"; interface C { public [a: number]: string; public [a: number]: string; public [a: number]: string }"; - - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 14, "interface".length, "class"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 18); - }); - - it("Moving accessors from class to object literal", () => { - const source = "class C { public get A() { } public get B() { } public get C() { } }"; - - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 0, "class C".length, "var v ="); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); - }); - - it("Moving accessors from object literal to class", () => { - const source = "var v = { public get A() { } public get B() { } public get C() { } }"; - - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 0, "var v =".length, "class C"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 4); - }); - - - it("Moving accessors from object literal to class in strict mode", () => { - const source = "\"use strict\"; var v = { public get A() { } public get B() { } public get C() { } }"; - - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 14, "var v =".length, "class C"); - - compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 4); - }); - - it("Reuse transformFlags of subtree during bind", () => { - const source = `class Greeter { constructor(element: HTMLElement) { } }`; - const oldText = ScriptSnapshot.fromString(source); - const newTextAndChange = withChange(oldText, 15, 0, "\n"); - const { oldTree, incrementalNewTree } = compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1); - bindSourceFile(oldTree, {}); - bindSourceFile(incrementalNewTree, {}); - assert.equal(oldTree.transformFlags, incrementalNewTree.transformFlags); - }); - - // Simulated typing tests. - - it("Type extends clause 1", () => { - const source = "interface IFoo { }\r\ninterface Array extends IFoo { }"; - - const index = source.indexOf("extends"); - deleteCode(source, index, "extends IFoo"); - }); - - it("Type after incomplete enum 1", () => { - const source = "function foo() {\r\n" + - " function getOccurrencesAtPosition() {\r\n" + - " switch (node) {\r\n" + - " enum \r\n" + - " }\r\n" + - " \r\n" + - " return undefined;\r\n" + - " \r\n" + - " function keywordToReferenceEntry() {\r\n" + - " }\r\n" + - " }\r\n" + - " \r\n" + - " return {\r\n" + - " getEmitOutput: (fileName): Bar => null,\r\n" + - " };\r\n" + - " }"; - - const index = source.indexOf("enum ") + "enum ".length; - insertCode(source, index, "Fo"); - }); + const oldText = ScriptSnapshot.fromString(source); + const index = 0; + const newTextAndChange = withInsert(oldText, index, "function Foo() { }"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); }); -} + it("Finish incomplete regular expression", () => { + const source = "while (true) /3; return;"; + const oldText = ScriptSnapshot.fromString(source); + const index = source.length - 1; + const newTextAndChange = withInsert(oldText, index, "/"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); + it("Regular expression to divide operation", () => { + const source = "return;\r\nwhile (true) /3/g;"; + const oldText = ScriptSnapshot.fromString(source); + const index = source.indexOf("while"); + const newTextAndChange = withDelete(oldText, index, "while ".length); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); + it("Divide operation to regular expression", () => { + const source = "return;\r\n(true) /3/g;"; + const oldText = ScriptSnapshot.fromString(source); + const index = source.indexOf("("); + const newTextAndChange = withInsert(oldText, index, "while "); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); + it("Unterminated comment after keyword converted to identifier", () => { + // 'public' as a keyword should be incrementally unusable (because it has an + // unterminated comment). When we convert it to an identifier, that shouldn't + // change anything, and we should still get the same errors. + const source = "return; a.public /*"; + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, 0, ""); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 7); + }); + it("Class to interface", () => { + const source = "class A { public M1() { } public M2() { } public M3() { } p1 = 0; p2 = 0; p3 = 0 }"; + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 0, "class".length, "interface"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); + it("Interface to class", () => { + const source = "interface A { M1?(); M2?(); M3?(); p1?; p2?; p3? }"; + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 0, "interface".length, "class"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); + it("Surrounding function declarations with block", () => { + const source = "declare function F1() { } export function F2() { } declare export function F3() { }"; + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withInsert(oldText, 0, "{"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 9); + }); + it("Removing block around function declarations", () => { + const source = "{ declare function F1() { } export function F2() { } declare export function F3() { }"; + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withDelete(oldText, 0, "{".length); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 9); + }); + it("Moving methods from class to object literal", () => { + const source = "class C { public A() { } public B() { } public C() { } }"; + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 0, "class C".length, "var v ="); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); + it("Moving methods from object literal to class", () => { + const source = "var v = { public A() { } public B() { } public C() { } }"; + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 0, "var v =".length, "class C"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 4); + }); + it("Moving methods from object literal to class in strict mode", () => { + const source = "\"use strict\"; var v = { public A() { } public B() { } public C() { } }"; + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 14, "var v =".length, "class C"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 4); + }); + it("Do not move constructors from class to object-literal.", () => { + const source = "class C { public constructor() { } public constructor() { } public constructor() { } }"; + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 0, "class C".length, "var v ="); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); + it("Do not move methods called \"constructor\" from object literal to class", () => { + const source = "var v = { public constructor() { } public constructor() { } public constructor() { } }"; + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 0, "var v =".length, "class C"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); + it("Moving index signatures from class to interface", () => { + const source = "class C { public [a: number]: string; public [a: number]: string; public [a: number]: string }"; + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 0, "class".length, "interface"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 18); + }); + it("Moving index signatures from class to interface in strict mode", () => { + const source = "\"use strict\"; class C { public [a: number]: string; public [a: number]: string; public [a: number]: string }"; + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 14, "class".length, "interface"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 18); + }); + it("Moving index signatures from interface to class", () => { + const source = "interface C { public [a: number]: string; public [a: number]: string; public [a: number]: string }"; + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 0, "interface".length, "class"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 18); + }); + it("Moving index signatures from interface to class in strict mode", () => { + const source = "\"use strict\"; interface C { public [a: number]: string; public [a: number]: string; public [a: number]: string }"; + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 14, "interface".length, "class"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 18); + }); + it("Moving accessors from class to object literal", () => { + const source = "class C { public get A() { } public get B() { } public get C() { } }"; + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 0, "class C".length, "var v ="); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0); + }); + it("Moving accessors from object literal to class", () => { + const source = "var v = { public get A() { } public get B() { } public get C() { } }"; + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 0, "var v =".length, "class C"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 4); + }); + it("Moving accessors from object literal to class in strict mode", () => { + const source = "\"use strict\"; var v = { public get A() { } public get B() { } public get C() { } }"; + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 14, "var v =".length, "class C"); + compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 4); + }); + it("Reuse transformFlags of subtree during bind", () => { + const source = `class Greeter { constructor(element: HTMLElement) { } }`; + const oldText = ScriptSnapshot.fromString(source); + const newTextAndChange = withChange(oldText, 15, 0, "\n"); + const { oldTree, incrementalNewTree } = compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, -1); + bindSourceFile(oldTree, {}); + bindSourceFile(incrementalNewTree, {}); + assert.equal(oldTree.transformFlags, incrementalNewTree.transformFlags); + }); + // Simulated typing tests. + it("Type extends clause 1", () => { + const source = "interface IFoo { }\r\ninterface Array extends IFoo { }"; + const index = source.indexOf("extends"); + deleteCode(source, index, "extends IFoo"); + }); + it("Type after incomplete enum 1", () => { + const source = "function foo() {\r\n" + + " function getOccurrencesAtPosition() {\r\n" + + " switch (node) {\r\n" + + " enum \r\n" + + " }\r\n" + + " \r\n" + + " return undefined;\r\n" + + " \r\n" + + " function keywordToReferenceEntry() {\r\n" + + " }\r\n" + + " }\r\n" + + " \r\n" + + " return {\r\n" + + " getEmitOutput: (fileName): Bar => null,\r\n" + + " };\r\n" + + " }"; + const index = source.indexOf("enum ") + "enum ".length; + insertCode(source, index, "Fo"); + }); +}); diff --git a/src/testRunner/unittests/jsDocParsing.ts b/src/testRunner/unittests/jsDocParsing.ts index 117e8b51a66b6..211c55d945201 100644 --- a/src/testRunner/unittests/jsDocParsing.ts +++ b/src/testRunner/unittests/jsDocParsing.ts @@ -1,346 +1,252 @@ -namespace ts { - describe("unittests:: JSDocParsing", () => { - describe("TypeExpressions", () => { - function parsesCorrectly(name: string, content: string) { - it(name, () => { - const typeAndDiagnostics = parseJSDocTypeExpressionForTests(content); - assert.isTrue(typeAndDiagnostics && typeAndDiagnostics.diagnostics.length === 0, "no errors issued"); - - Harness.Baseline.runBaseline("JSDocParsing/TypeExpressions.parsesCorrectly." + name + ".json", - Utils.sourceFileToJSON(typeAndDiagnostics!.jsDocTypeExpression.type)); - }); - } - - function parsesIncorrectly(name: string, content: string) { - it(name, () => { - const type = parseJSDocTypeExpressionForTests(content); - assert.isTrue(!type || type.diagnostics.length > 0); - }); - } - - describe("parseCorrectly", () => { - parsesCorrectly("unknownType", "{?}"); - parsesCorrectly("allType", "{*}"); - parsesCorrectly("nullableType", "{?number}"); - parsesCorrectly("nullableType2", "{number?}"); - parsesCorrectly("nonNullableType", "{!number}"); - parsesCorrectly("nonNullableType2", "{number!}"); - parsesCorrectly("recordType1", "{{}}"); - parsesCorrectly("recordType2", "{{foo}}"); - parsesCorrectly("recordType3", "{{foo: number}}"); - parsesCorrectly("recordType4", "{{foo, bar}}"); - parsesCorrectly("recordType5", "{{foo: number, bar}}"); - parsesCorrectly("recordType6", "{{foo, bar: number}}"); - parsesCorrectly("recordType7", "{{foo: number, bar: number}}"); - parsesCorrectly("recordType8", "{{function}}"); - parsesCorrectly("trailingCommaInRecordType", "{{a,}}"); - parsesCorrectly("callSignatureInRecordType", "{{(): number}}"); - parsesCorrectly("methodInRecordType", "{{foo(): number}}"); - parsesCorrectly("unionType", "{(number|string)}"); - parsesCorrectly("unionTypeWithLeadingOperator", "{( | number | string )}"); - parsesCorrectly("unionTypeWithOneElementAndLeadingOperator", "{( | number )}"); - parsesCorrectly("topLevelNoParenUnionType", "{number|string}"); - parsesCorrectly("functionType1", "{function()}"); - parsesCorrectly("functionType2", "{function(string, boolean)}"); - parsesCorrectly("functionReturnType1", "{function(string, boolean)}"); - parsesCorrectly("thisType1", "{function(this:a.b)}"); - parsesCorrectly("newType1", "{function(new:a.b)}"); - parsesCorrectly("variadicType", "{...number}"); - parsesCorrectly("optionalType", "{number=}"); - parsesCorrectly("optionalNullable", "{?=}"); - parsesCorrectly("typeReference1", "{a.}"); - parsesCorrectly("typeReference2", "{a.}"); - parsesCorrectly("typeReference3", "{a.function}"); - parsesCorrectly("arrayType1", "{a[]}"); - parsesCorrectly("arrayType2", "{a[][]}"); - parsesCorrectly("arrayType3", "{(a[][])=}"); - parsesCorrectly("keyword1", "{var}"); - parsesCorrectly("keyword2", "{null}"); - parsesCorrectly("keyword3", "{undefined}"); - parsesCorrectly("tupleType0", "{[]}"); - parsesCorrectly("tupleType1", "{[number]}"); - parsesCorrectly("tupleType2", "{[number,string]}"); - parsesCorrectly("tupleType3", "{[number,string,boolean]}"); - parsesCorrectly("tupleTypeWithTrailingComma", "{[number,]}"); - parsesCorrectly("typeOfType", "{typeof M}"); - parsesCorrectly("tsConstructorType", "{new () => string}"); - parsesCorrectly("tsFunctionType", "{() => string}"); - parsesCorrectly("typeArgumentsNotFollowingDot", "{a<>}"); - parsesCorrectly("functionTypeWithTrailingComma", "{function(a,)}"); +import { parseJSDocTypeExpressionForTests, parseIsolatedJSDocComment, Debug, createSourceFile, ScriptTarget, SyntaxKind } from "../ts"; +import { Baseline } from "../Harness"; +import { sourceFileToJSON } from "../Utils"; +describe("unittests:: JSDocParsing", () => { + describe("TypeExpressions", () => { + function parsesCorrectly(name: string, content: string) { + it(name, () => { + const typeAndDiagnostics = parseJSDocTypeExpressionForTests(content); + assert.isTrue(typeAndDiagnostics && typeAndDiagnostics.diagnostics.length === 0, "no errors issued"); + Baseline.runBaseline("JSDocParsing/TypeExpressions.parsesCorrectly." + name + ".json", sourceFileToJSON(typeAndDiagnostics!.jsDocTypeExpression.type)); }); - - describe("parsesIncorrectly", () => { - parsesIncorrectly("emptyType", "{}"); - parsesIncorrectly("unionTypeWithTrailingBar", "{(a|)}"); - parsesIncorrectly("unionTypeWithoutTypes", "{()}"); - parsesIncorrectly("nullableTypeWithoutType", "{!}"); - parsesIncorrectly("thisWithoutType", "{this:}"); - parsesIncorrectly("newWithoutType", "{new:}"); - parsesIncorrectly("variadicWithoutType", "{...}"); - parsesIncorrectly("optionalWithoutType", "{=}"); - parsesIncorrectly("allWithType", "{*foo}"); - parsesIncorrectly("namedParameter", "{function(a: number)}"); - parsesIncorrectly("tupleTypeWithComma", "{[,]}"); - parsesIncorrectly("tupleTypeWithLeadingComma", "{[,number]}"); + } + function parsesIncorrectly(name: string, content: string) { + it(name, () => { + const type = parseJSDocTypeExpressionForTests(content); + assert.isTrue(!type || type.diagnostics.length > 0); }); + } + describe("parseCorrectly", () => { + parsesCorrectly("unknownType", "{?}"); + parsesCorrectly("allType", "{*}"); + parsesCorrectly("nullableType", "{?number}"); + parsesCorrectly("nullableType2", "{number?}"); + parsesCorrectly("nonNullableType", "{!number}"); + parsesCorrectly("nonNullableType2", "{number!}"); + parsesCorrectly("recordType1", "{{}}"); + parsesCorrectly("recordType2", "{{foo}}"); + parsesCorrectly("recordType3", "{{foo: number}}"); + parsesCorrectly("recordType4", "{{foo, bar}}"); + parsesCorrectly("recordType5", "{{foo: number, bar}}"); + parsesCorrectly("recordType6", "{{foo, bar: number}}"); + parsesCorrectly("recordType7", "{{foo: number, bar: number}}"); + parsesCorrectly("recordType8", "{{function}}"); + parsesCorrectly("trailingCommaInRecordType", "{{a,}}"); + parsesCorrectly("callSignatureInRecordType", "{{(): number}}"); + parsesCorrectly("methodInRecordType", "{{foo(): number}}"); + parsesCorrectly("unionType", "{(number|string)}"); + parsesCorrectly("unionTypeWithLeadingOperator", "{( | number | string )}"); + parsesCorrectly("unionTypeWithOneElementAndLeadingOperator", "{( | number )}"); + parsesCorrectly("topLevelNoParenUnionType", "{number|string}"); + parsesCorrectly("functionType1", "{function()}"); + parsesCorrectly("functionType2", "{function(string, boolean)}"); + parsesCorrectly("functionReturnType1", "{function(string, boolean)}"); + parsesCorrectly("thisType1", "{function(this:a.b)}"); + parsesCorrectly("newType1", "{function(new:a.b)}"); + parsesCorrectly("variadicType", "{...number}"); + parsesCorrectly("optionalType", "{number=}"); + parsesCorrectly("optionalNullable", "{?=}"); + parsesCorrectly("typeReference1", "{a.}"); + parsesCorrectly("typeReference2", "{a.}"); + parsesCorrectly("typeReference3", "{a.function}"); + parsesCorrectly("arrayType1", "{a[]}"); + parsesCorrectly("arrayType2", "{a[][]}"); + parsesCorrectly("arrayType3", "{(a[][])=}"); + parsesCorrectly("keyword1", "{var}"); + parsesCorrectly("keyword2", "{null}"); + parsesCorrectly("keyword3", "{undefined}"); + parsesCorrectly("tupleType0", "{[]}"); + parsesCorrectly("tupleType1", "{[number]}"); + parsesCorrectly("tupleType2", "{[number,string]}"); + parsesCorrectly("tupleType3", "{[number,string,boolean]}"); + parsesCorrectly("tupleTypeWithTrailingComma", "{[number,]}"); + parsesCorrectly("typeOfType", "{typeof M}"); + parsesCorrectly("tsConstructorType", "{new () => string}"); + parsesCorrectly("tsFunctionType", "{() => string}"); + parsesCorrectly("typeArgumentsNotFollowingDot", "{a<>}"); + parsesCorrectly("functionTypeWithTrailingComma", "{function(a,)}"); }); - - describe("DocComments", () => { - function parsesCorrectly(name: string, content: string) { - it(name, () => { - const comment = parseIsolatedJSDocComment(content)!; - if (!comment) { - Debug.fail("Comment failed to parse entirely"); - } - if (comment.diagnostics.length > 0) { - Debug.fail("Comment has at least one diagnostic: " + comment.diagnostics[0].messageText); - } - - Harness.Baseline.runBaseline("JSDocParsing/DocComments.parsesCorrectly." + name + ".json", - JSON.stringify(comment.jsDoc, - (_, v) => v && v.pos !== undefined ? JSON.parse(Utils.sourceFileToJSON(v)) : v, 4)); - }); - } - - function parsesIncorrectly(name: string, content: string) { - it(name, () => { - const type = parseIsolatedJSDocComment(content); - assert.isTrue(!type || type.diagnostics.length > 0); - }); - } - - describe("parsesIncorrectly", () => { - parsesIncorrectly("multipleTypes", - `/** + describe("parsesIncorrectly", () => { + parsesIncorrectly("emptyType", "{}"); + parsesIncorrectly("unionTypeWithTrailingBar", "{(a|)}"); + parsesIncorrectly("unionTypeWithoutTypes", "{()}"); + parsesIncorrectly("nullableTypeWithoutType", "{!}"); + parsesIncorrectly("thisWithoutType", "{this:}"); + parsesIncorrectly("newWithoutType", "{new:}"); + parsesIncorrectly("variadicWithoutType", "{...}"); + parsesIncorrectly("optionalWithoutType", "{=}"); + parsesIncorrectly("allWithType", "{*foo}"); + parsesIncorrectly("namedParameter", "{function(a: number)}"); + parsesIncorrectly("tupleTypeWithComma", "{[,]}"); + parsesIncorrectly("tupleTypeWithLeadingComma", "{[,number]}"); + }); + }); + describe("DocComments", () => { + function parsesCorrectly(name: string, content: string) { + it(name, () => { + const comment = (parseIsolatedJSDocComment(content)!); + if (!comment) { + Debug.fail("Comment failed to parse entirely"); + } + if (comment.diagnostics.length > 0) { + Debug.fail("Comment has at least one diagnostic: " + comment.diagnostics[0].messageText); + } + Baseline.runBaseline("JSDocParsing/DocComments.parsesCorrectly." + name + ".json", JSON.stringify(comment.jsDoc, (_, v) => v && v.pos !== undefined ? JSON.parse(sourceFileToJSON(v)) : v, 4)); + }); + } + function parsesIncorrectly(name: string, content: string) { + it(name, () => { + const type = parseIsolatedJSDocComment(content); + assert.isTrue(!type || type.diagnostics.length > 0); + }); + } + describe("parsesIncorrectly", () => { + parsesIncorrectly("multipleTypes", `/** * @type {number} * @type {string} */`); - parsesIncorrectly("multipleReturnTypes", - `/** + parsesIncorrectly("multipleReturnTypes", `/** * @return {number} * @return {string} */`); - parsesIncorrectly("noTypeParameters", - `/** + parsesIncorrectly("noTypeParameters", `/** * @template */`); - parsesIncorrectly("trailingTypeParameterComma", - `/** + parsesIncorrectly("trailingTypeParameterComma", `/** * @template T, */`); - parsesIncorrectly("paramWithoutName", - `/** + parsesIncorrectly("paramWithoutName", `/** * @param {number} */`); - parsesIncorrectly("paramWithoutTypeOrName", - `/** + parsesIncorrectly("paramWithoutTypeOrName", `/** * @param */`); - - parsesIncorrectly("noType", - `/** + parsesIncorrectly("noType", `/** * @type */`); - - parsesIncorrectly("@augments with no type", - `/** + parsesIncorrectly("@augments with no type", `/** * @augments */`); - }); - - describe("parsesCorrectly", () => { - parsesCorrectly("threeAsterisks", "/*** */"); - parsesCorrectly("emptyComment", "/***/"); - parsesCorrectly("noLeadingAsterisk", - `/** + }); + describe("parsesCorrectly", () => { + parsesCorrectly("threeAsterisks", "/*** */"); + parsesCorrectly("emptyComment", "/***/"); + parsesCorrectly("noLeadingAsterisk", `/** @type {number} */`); - - - parsesCorrectly("noReturnType", - `/** + parsesCorrectly("noReturnType", `/** * @return */`); - - parsesCorrectly("leadingAsterisk", - `/** + parsesCorrectly("leadingAsterisk", `/** * @type {number} */`); - - parsesCorrectly("asteriskAfterPreamble", "/** * @type {number} */"); - - parsesCorrectly("typeTag", - `/** + parsesCorrectly("asteriskAfterPreamble", "/** * @type {number} */"); + parsesCorrectly("typeTag", `/** * @type {number} */`); - - - parsesCorrectly("returnTag1", - `/** + parsesCorrectly("returnTag1", `/** * @return {number} */`); - - - parsesCorrectly("returnTag2", - `/** + parsesCorrectly("returnTag2", `/** * @return {number} Description text follows */`); - - - parsesCorrectly("returnsTag1", - `/** + parsesCorrectly("returnsTag1", `/** * @returns {number} */`); - - - parsesCorrectly("oneParamTag", - `/** + parsesCorrectly("oneParamTag", `/** * @param {number} name1 */`); - - - parsesCorrectly("twoParamTag2", - `/** + parsesCorrectly("twoParamTag2", `/** * @param {number} name1 * @param {number} name2 */`); - - - parsesCorrectly("paramTag1", - `/** + parsesCorrectly("paramTag1", `/** * @param {number} name1 Description text follows */`); - - - parsesCorrectly("paramTagBracketedName1", - `/** + parsesCorrectly("paramTagBracketedName1", `/** * @param {number} [name1] Description text follows */`); - - - parsesCorrectly("paramTagBracketedName2", - `/** + parsesCorrectly("paramTagBracketedName2", `/** * @param {number} [ name1 = 1] Description text follows */`); - - - parsesCorrectly("twoParamTagOnSameLine", - `/** + parsesCorrectly("twoParamTagOnSameLine", `/** * @param {number} name1 @param {number} name2 */`); - - - parsesCorrectly("paramTagNameThenType1", - `/** + parsesCorrectly("paramTagNameThenType1", `/** * @param name1 {number} */`); - - - parsesCorrectly("paramTagNameThenType2", - `/** + parsesCorrectly("paramTagNameThenType2", `/** * @param name1 {number} Description */`); - - - parsesCorrectly("argSynonymForParamTag", - `/** + parsesCorrectly("argSynonymForParamTag", `/** * @arg {number} name1 Description */`); - - - parsesCorrectly("argumentSynonymForParamTag", - `/** + parsesCorrectly("argumentSynonymForParamTag", `/** * @argument {number} name1 Description */`); - - - parsesCorrectly("templateTag", - `/** + parsesCorrectly("templateTag", `/** * @template T */`); - - - parsesCorrectly("templateTag2", - `/** + parsesCorrectly("templateTag2", `/** * @template K,V */`); - - - parsesCorrectly("templateTag3", - `/** + parsesCorrectly("templateTag3", `/** * @template K ,V */`); - - - parsesCorrectly("templateTag4", - `/** + parsesCorrectly("templateTag4", `/** * @template K, V */`); - - - parsesCorrectly("templateTag5", - `/** + parsesCorrectly("templateTag5", `/** * @template K , V */`); - - parsesCorrectly("templateTag6", - `/** + parsesCorrectly("templateTag6", `/** * @template K , V Description of type parameters. */`); - - parsesCorrectly("paramWithoutType", - `/** + parsesCorrectly("paramWithoutType", `/** * @param foo */`); - parsesCorrectly("typedefTagWithChildrenTags", - `/** + parsesCorrectly("typedefTagWithChildrenTags", `/** * @typedef People * @type {Object} * @property {number} age * @property {string} name */`); - parsesCorrectly("less-than and greater-than characters", - `/** + parsesCorrectly("less-than and greater-than characters", `/** * @param x hi < > still part of the previous comment */`); - - parsesCorrectly("Nested @param tags", - `/** + parsesCorrectly("Nested @param tags", `/** * @param {object} o Doc doc * @param {string} o.f Doc for f */`); - parsesCorrectly("@link tags", - `/** + parsesCorrectly("@link tags", `/** * {@link first link} * Inside {@link link text} thing * @see {@link second link text} and {@link Foo|a foo} as well. */`); - parsesCorrectly("authorTag", - `/** + parsesCorrectly("authorTag", `/** * @author John Doe * @author John Doe unexpected comment */`); - }); }); - describe("getFirstToken", () => { - it("gets jsdoc", () => { - const root = createSourceFile("foo.ts", "/** comment */var a = true;", ScriptTarget.ES5, /*setParentNodes*/ true); - assert.isDefined(root); - assert.equal(root.kind, SyntaxKind.SourceFile); - const first = root.getFirstToken(); - assert.isDefined(first); - assert.equal(first!.kind, SyntaxKind.VarKeyword); - }); + }); + describe("getFirstToken", () => { + it("gets jsdoc", () => { + const root = createSourceFile("foo.ts", "/** comment */var a = true;", ScriptTarget.ES5, /*setParentNodes*/ true); + assert.isDefined(root); + assert.equal(root.kind, SyntaxKind.SourceFile); + const first = root.getFirstToken(); + assert.isDefined(first); + assert.equal(first!.kind, SyntaxKind.VarKeyword); }); - describe("getLastToken", () => { - it("gets jsdoc", () => { - const root = createSourceFile("foo.ts", "var a = true;/** comment */", ScriptTarget.ES5, /*setParentNodes*/ true); - assert.isDefined(root); - const last = root.getLastToken(); - assert.isDefined(last); - assert.equal(last!.kind, SyntaxKind.EndOfFileToken); - }); + }); + describe("getLastToken", () => { + it("gets jsdoc", () => { + const root = createSourceFile("foo.ts", "var a = true;/** comment */", ScriptTarget.ES5, /*setParentNodes*/ true); + assert.isDefined(root); + const last = root.getLastToken(); + assert.isDefined(last); + assert.equal(last!.kind, SyntaxKind.EndOfFileToken); }); }); -} +}); diff --git a/src/testRunner/unittests/moduleResolution.ts b/src/testRunner/unittests/moduleResolution.ts index be70424248cc9..5a28114c05a26 100644 --- a/src/testRunner/unittests/moduleResolution.ts +++ b/src/testRunner/unittests/moduleResolution.ts @@ -1,1396 +1,1126 @@ -namespace ts { - export function checkResolvedModule(actual: ResolvedModuleFull | undefined, expected: ResolvedModuleFull | undefined): boolean { - if (!expected) { - if (actual) { - assert.fail(actual, expected, "expected resolved module to be undefined"); - return false; - } - return true; - } - else if (!actual) { - assert.fail(actual, expected, "expected resolved module to be defined"); +import { ResolvedModuleFull, ResolvedModuleWithFailedLookupLocations, extensionFromPath, ModuleResolutionHost, createMap, getDirectoryPath, supportedTSExtensions, nodeModuleNameResolver, normalizePath, getRootLength, combinePaths, createModuleResolutionCache, Extension, CompilerOptions, ModuleResolutionKind, resolveModuleName, ModuleKind, CompilerHost, ScriptTarget, createSourceFile, notImplemented, createProgram, createMapFromTemplate, SourceFile, Program, Diagnostic, createGetCanonicalFileName, sortAndDeduplicateDiagnostics, emptyArray, Diagnostics, JsxEmit, resolveTypeReferenceDirective, map, arrayToMap, StructureIsReused } from "../ts"; +import { Compiler } from "../Harness"; +import { getDiagnosticOfFileFromProgram } from "../ts.tscWatch"; +import * as ts from "../ts"; +export function checkResolvedModule(actual: ResolvedModuleFull | undefined, expected: ResolvedModuleFull | undefined): boolean { + if (!expected) { + if (actual) { + assert.fail(actual, expected, "expected resolved module to be undefined"); return false; } - - assert.isTrue(actual.resolvedFileName === expected.resolvedFileName, `'resolvedFileName': expected '${actual.resolvedFileName}' to be equal to '${expected.resolvedFileName}'`); - assert.isTrue(actual.extension === expected.extension, `'ext': expected '${actual.extension}' to be equal to '${expected.extension}'`); - assert.isTrue(actual.isExternalLibraryImport === expected.isExternalLibraryImport, `'isExternalLibraryImport': expected '${actual.isExternalLibraryImport}' to be equal to '${expected.isExternalLibraryImport}'`); return true; } - - export function checkResolvedModuleWithFailedLookupLocations(actual: ResolvedModuleWithFailedLookupLocations, expectedResolvedModule: ResolvedModuleFull, expectedFailedLookupLocations: string[]): void { - assert.isTrue(actual.resolvedModule !== undefined, "module should be resolved"); - checkResolvedModule(actual.resolvedModule, expectedResolvedModule); - assert.deepEqual(actual.failedLookupLocations, expectedFailedLookupLocations, `Failed lookup locations should match - expected has ${expectedFailedLookupLocations.length}, actual has ${actual.failedLookupLocations.length}`); + else if (!actual) { + assert.fail(actual, expected, "expected resolved module to be defined"); + return false; } - - export function createResolvedModule(resolvedFileName: string, isExternalLibraryImport = false): ResolvedModuleFull { - return { resolvedFileName, extension: extensionFromPath(resolvedFileName), isExternalLibraryImport }; - } - - interface File { - name: string; - content?: string; - symlinks?: string[]; + assert.isTrue(actual.resolvedFileName === expected.resolvedFileName, `'resolvedFileName': expected '${actual.resolvedFileName}' to be equal to '${expected.resolvedFileName}'`); + assert.isTrue(actual.extension === expected.extension, `'ext': expected '${actual.extension}' to be equal to '${expected.extension}'`); + assert.isTrue(actual.isExternalLibraryImport === expected.isExternalLibraryImport, `'isExternalLibraryImport': expected '${actual.isExternalLibraryImport}' to be equal to '${expected.isExternalLibraryImport}'`); + return true; +} +export function checkResolvedModuleWithFailedLookupLocations(actual: ResolvedModuleWithFailedLookupLocations, expectedResolvedModule: ResolvedModuleFull, expectedFailedLookupLocations: string[]): void { + assert.isTrue(actual.resolvedModule !== undefined, "module should be resolved"); + checkResolvedModule(actual.resolvedModule, expectedResolvedModule); + assert.deepEqual(actual.failedLookupLocations, expectedFailedLookupLocations, `Failed lookup locations should match - expected has ${expectedFailedLookupLocations.length}, actual has ${actual.failedLookupLocations.length}`); +} +export function createResolvedModule(resolvedFileName: string, isExternalLibraryImport = false): ResolvedModuleFull { + return { resolvedFileName, extension: extensionFromPath(resolvedFileName), isExternalLibraryImport }; +} +interface File { + name: string; + content?: string; + symlinks?: string[]; +} +function createModuleResolutionHost(hasDirectoryExists: boolean, ...files: File[]): ModuleResolutionHost { + const map = createMap(); + for (const file of files) { + map.set(file.name, file); + if (file.symlinks) { + for (const symlink of file.symlinks) { + map.set(symlink, file); + } + } } - - function createModuleResolutionHost(hasDirectoryExists: boolean, ...files: File[]): ModuleResolutionHost { - const map = createMap(); - for (const file of files) { - map.set(file.name, file); - if (file.symlinks) { - for (const symlink of file.symlinks) { - map.set(symlink, file); + if (hasDirectoryExists) { + const directories = createMap(); + for (const f of files) { + let name = getDirectoryPath(f.name); + while (true) { + directories.set(name, name); + const baseName = getDirectoryPath(name); + if (baseName === name) { + break; } + name = baseName; } } - - if (hasDirectoryExists) { - const directories = createMap(); - for (const f of files) { - let name = getDirectoryPath(f.name); - while (true) { - directories.set(name, name); - const baseName = getDirectoryPath(name); - if (baseName === name) { - break; - } - name = baseName; - } + return { + readFile, + realpath, + directoryExists: path => directories.has(path), + fileExists: path => { + assert.isTrue(directories.has(getDirectoryPath(path)), `'fileExists' '${path}' request in non-existing directory`); + return map.has(path); } - return { - readFile, - realpath, - directoryExists: path => directories.has(path), - fileExists: path => { - assert.isTrue(directories.has(getDirectoryPath(path)), `'fileExists' '${path}' request in non-existing directory`); - return map.has(path); - } - }; + }; + } + else { + return { readFile, realpath, fileExists: path => map.has(path) }; + } + function readFile(path: string): string | undefined { + const file = map.get(path); + return file && file.content; + } + function realpath(path: string): string { + return map.get(path)!.name; + } +} +describe("unittests:: moduleResolution:: Node module resolution - relative paths", () => { + function testLoadAsFile(containingFileName: string, moduleFileNameNoExt: string, moduleName: string): void { + for (const ext of supportedTSExtensions) { + test(ext, /*hasDirectoryExists*/ false); + test(ext, /*hasDirectoryExists*/ true); } - else { - return { readFile, realpath, fileExists: path => map.has(path) }; + function test(ext: string, hasDirectoryExists: boolean) { + const containingFile = { name: containingFileName }; + const moduleFile = { name: moduleFileNameNoExt + ext }; + const resolution = nodeModuleNameResolver(moduleName, containingFile.name, {}, createModuleResolutionHost(hasDirectoryExists, containingFile, moduleFile)); + checkResolvedModule(resolution.resolvedModule, createResolvedModule(moduleFile.name)); + const failedLookupLocations: string[] = []; + const dir = getDirectoryPath(containingFileName); + for (const e of supportedTSExtensions) { + if (e === ext) { + break; + } + else { + failedLookupLocations.push(normalizePath(getRootLength(moduleName) === 0 ? combinePaths(dir, moduleName) : moduleName) + e); + } + } + assert.deepEqual(resolution.failedLookupLocations, failedLookupLocations); } - function readFile(path: string): string | undefined { - const file = map.get(path); - return file && file.content; + } + it("module name that starts with './' resolved as relative file name", () => { + testLoadAsFile("/foo/bar/baz.ts", "/foo/bar/foo", "./foo"); + }); + it("module name that starts with '../' resolved as relative file name", () => { + testLoadAsFile("/foo/bar/baz.ts", "/foo/foo", "../foo"); + }); + it("module name that starts with '/' script extension resolved as relative file name", () => { + testLoadAsFile("/foo/bar/baz.ts", "/foo", "/foo"); + }); + it("module name that starts with 'c:/' script extension resolved as relative file name", () => { + testLoadAsFile("c:/foo/bar/baz.ts", "c:/foo", "c:/foo"); + }); + function testLoadingFromPackageJson(containingFileName: string, packageJsonFileName: string, fieldRef: string, moduleFileName: string, moduleName: string): void { + test(/*hasDirectoryExists*/ false); + test(/*hasDirectoryExists*/ true); + function test(hasDirectoryExists: boolean) { + const containingFile = { name: containingFileName }; + const packageJson = { name: packageJsonFileName, content: JSON.stringify({ typings: fieldRef }) }; + const moduleFile = { name: moduleFileName }; + const resolution = nodeModuleNameResolver(moduleName, containingFile.name, {}, createModuleResolutionHost(hasDirectoryExists, containingFile, packageJson, moduleFile)); + checkResolvedModule(resolution.resolvedModule, createResolvedModule(moduleFile.name)); + // expect three failed lookup location - attempt to load module as file with all supported extensions + assert.equal(resolution.failedLookupLocations.length, supportedTSExtensions.length); } - function realpath(path: string): string { - return map.get(path)!.name; + } + it("module name as directory - load from 'typings'", () => { + testLoadingFromPackageJson("/a/b/c/d.ts", "/a/b/c/bar/package.json", "c/d/e.d.ts", "/a/b/c/bar/c/d/e.d.ts", "./bar"); + testLoadingFromPackageJson("/a/b/c/d.ts", "/a/bar/package.json", "e.d.ts", "/a/bar/e.d.ts", "../../bar"); + testLoadingFromPackageJson("/a/b/c/d.ts", "/bar/package.json", "e.d.ts", "/bar/e.d.ts", "/bar"); + testLoadingFromPackageJson("c:/a/b/c/d.ts", "c:/bar/package.json", "e.d.ts", "c:/bar/e.d.ts", "c:/bar"); + }); + function testTypingsIgnored(typings: any): void { + test(/*hasDirectoryExists*/ false); + test(/*hasDirectoryExists*/ true); + function test(hasDirectoryExists: boolean) { + const containingFile = { name: "/a/b.ts" }; + const packageJson = { name: "/node_modules/b/package.json", content: JSON.stringify({ typings }) }; + const moduleFile = { name: "/a/b.d.ts" }; + const indexPath = "/node_modules/b/index.d.ts"; + const indexFile = { name: indexPath }; + const resolution = nodeModuleNameResolver("b", containingFile.name, {}, createModuleResolutionHost(hasDirectoryExists, containingFile, packageJson, moduleFile, indexFile)); + checkResolvedModule(resolution.resolvedModule, createResolvedModule(indexPath, /*isExternalLibraryImport*/ true)); } } - - describe("unittests:: moduleResolution:: Node module resolution - relative paths", () => { - - function testLoadAsFile(containingFileName: string, moduleFileNameNoExt: string, moduleName: string): void { - for (const ext of supportedTSExtensions) { - test(ext, /*hasDirectoryExists*/ false); - test(ext, /*hasDirectoryExists*/ true); - } - - function test(ext: string, hasDirectoryExists: boolean) { - const containingFile = { name: containingFileName }; - const moduleFile = { name: moduleFileNameNoExt + ext }; - const resolution = nodeModuleNameResolver(moduleName, containingFile.name, {}, createModuleResolutionHost(hasDirectoryExists, containingFile, moduleFile)); - checkResolvedModule(resolution.resolvedModule, createResolvedModule(moduleFile.name)); - - const failedLookupLocations: string[] = []; - const dir = getDirectoryPath(containingFileName); - for (const e of supportedTSExtensions) { - if (e === ext) { - break; - } - else { - failedLookupLocations.push(normalizePath(getRootLength(moduleName) === 0 ? combinePaths(dir, moduleName) : moduleName) + e); - } - } - - assert.deepEqual(resolution.failedLookupLocations, failedLookupLocations); - - } + it("module name as directory - handle invalid 'typings'", () => { + testTypingsIgnored(["a", "b"]); + testTypingsIgnored({ a: "b" }); + testTypingsIgnored(/*typings*/ true); + testTypingsIgnored(/*typings*/ null); // eslint-disable-line no-null/no-null + testTypingsIgnored(/*typings*/ undefined); + }); + it("module name as directory - load index.d.ts", () => { + test(/*hasDirectoryExists*/ false); + test(/*hasDirectoryExists*/ true); + function test(hasDirectoryExists: boolean) { + const containingFile = { name: "/a/b/c.ts" }; + const packageJson = { name: "/a/b/foo/package.json", content: JSON.stringify({ main: "/c/d" }) }; + const indexFile = { name: "/a/b/foo/index.d.ts" }; + const resolution = nodeModuleNameResolver("./foo", containingFile.name, {}, createModuleResolutionHost(hasDirectoryExists, containingFile, packageJson, indexFile)); + checkResolvedModuleWithFailedLookupLocations(resolution, createResolvedModule(indexFile.name), [ + "/a/b/foo.ts", + "/a/b/foo.tsx", + "/a/b/foo.d.ts", + "/c/d", + "/c/d.ts", + "/c/d.tsx", + "/c/d.d.ts", + "/c/d/index.ts", + "/c/d/index.tsx", + "/c/d/index.d.ts", + "/a/b/foo/index.ts", + "/a/b/foo/index.tsx", + ]); } - - it("module name that starts with './' resolved as relative file name", () => { - testLoadAsFile("/foo/bar/baz.ts", "/foo/bar/foo", "./foo"); - }); - - it("module name that starts with '../' resolved as relative file name", () => { - testLoadAsFile("/foo/bar/baz.ts", "/foo/foo", "../foo"); + }); +}); +describe("unittests:: moduleResolution:: Node module resolution - non-relative paths", () => { + it("computes correct commonPrefix for moduleName cache", () => { + const resolutionCache = createModuleResolutionCache("/", (f) => f); + let cache = resolutionCache.getOrCreateCacheForModuleName("a"); + cache.set("/sub", { + resolvedModule: { + originalPath: undefined, + resolvedFileName: "/sub/node_modules/a/index.ts", + isExternalLibraryImport: true, + extension: Extension.Ts, + }, + failedLookupLocations: [], }); - - it("module name that starts with '/' script extension resolved as relative file name", () => { - testLoadAsFile("/foo/bar/baz.ts", "/foo", "/foo"); + assert.isDefined(cache.get("/sub")); + assert.isUndefined(cache.get("/")); + cache = resolutionCache.getOrCreateCacheForModuleName("b"); + cache.set("/sub/dir/foo", { + resolvedModule: { + originalPath: undefined, + resolvedFileName: "/sub/directory/node_modules/b/index.ts", + isExternalLibraryImport: true, + extension: Extension.Ts, + }, + failedLookupLocations: [], }); - - it("module name that starts with 'c:/' script extension resolved as relative file name", () => { - testLoadAsFile("c:/foo/bar/baz.ts", "c:/foo", "c:/foo"); + assert.isDefined(cache.get("/sub/dir/foo")); + assert.isDefined(cache.get("/sub/dir")); + assert.isDefined(cache.get("/sub")); + assert.isUndefined(cache.get("/")); + cache = resolutionCache.getOrCreateCacheForModuleName("c"); + cache.set("/foo/bar", { + resolvedModule: { + originalPath: undefined, + resolvedFileName: "/bar/node_modules/c/index.ts", + isExternalLibraryImport: true, + extension: Extension.Ts, + }, + failedLookupLocations: [], }); - - function testLoadingFromPackageJson(containingFileName: string, packageJsonFileName: string, fieldRef: string, moduleFileName: string, moduleName: string): void { - test(/*hasDirectoryExists*/ false); - test(/*hasDirectoryExists*/ true); - - function test(hasDirectoryExists: boolean) { - const containingFile = { name: containingFileName }; - const packageJson = { name: packageJsonFileName, content: JSON.stringify({ typings: fieldRef }) }; - const moduleFile = { name: moduleFileName }; - const resolution = nodeModuleNameResolver(moduleName, containingFile.name, {}, createModuleResolutionHost(hasDirectoryExists, containingFile, packageJson, moduleFile)); - checkResolvedModule(resolution.resolvedModule, createResolvedModule(moduleFile.name)); - // expect three failed lookup location - attempt to load module as file with all supported extensions - assert.equal(resolution.failedLookupLocations.length, supportedTSExtensions.length); - } - } - - it("module name as directory - load from 'typings'", () => { - testLoadingFromPackageJson("/a/b/c/d.ts", "/a/b/c/bar/package.json", "c/d/e.d.ts", "/a/b/c/bar/c/d/e.d.ts", "./bar"); - testLoadingFromPackageJson("/a/b/c/d.ts", "/a/bar/package.json", "e.d.ts", "/a/bar/e.d.ts", "../../bar"); - testLoadingFromPackageJson("/a/b/c/d.ts", "/bar/package.json", "e.d.ts", "/bar/e.d.ts", "/bar"); - testLoadingFromPackageJson("c:/a/b/c/d.ts", "c:/bar/package.json", "e.d.ts", "c:/bar/e.d.ts", "c:/bar"); + assert.isDefined(cache.get("/foo/bar")); + assert.isDefined(cache.get("/foo")); + assert.isDefined(cache.get("/")); + cache = resolutionCache.getOrCreateCacheForModuleName("d"); + cache.set("/foo", { + resolvedModule: { + originalPath: undefined, + resolvedFileName: "/foo/index.ts", + isExternalLibraryImport: true, + extension: Extension.Ts, + }, + failedLookupLocations: [], }); - - function testTypingsIgnored(typings: any): void { - test(/*hasDirectoryExists*/ false); - test(/*hasDirectoryExists*/ true); - - function test(hasDirectoryExists: boolean) { - const containingFile = { name: "/a/b.ts" }; - const packageJson = { name: "/node_modules/b/package.json", content: JSON.stringify({ typings }) }; - const moduleFile = { name: "/a/b.d.ts" }; - - const indexPath = "/node_modules/b/index.d.ts"; - const indexFile = { name: indexPath }; - - const resolution = nodeModuleNameResolver("b", containingFile.name, {}, createModuleResolutionHost(hasDirectoryExists, containingFile, packageJson, moduleFile, indexFile)); - - checkResolvedModule(resolution.resolvedModule, createResolvedModule(indexPath, /*isExternalLibraryImport*/ true)); - } - } - - it("module name as directory - handle invalid 'typings'", () => { - testTypingsIgnored(["a", "b"]); - testTypingsIgnored({ a: "b" }); - testTypingsIgnored(/*typings*/ true); - testTypingsIgnored(/*typings*/ null); // eslint-disable-line no-null/no-null - testTypingsIgnored(/*typings*/ undefined); + assert.isDefined(cache.get("/foo")); + assert.isUndefined(cache.get("/")); + cache = resolutionCache.getOrCreateCacheForModuleName("e"); + cache.set("c:/foo", { + resolvedModule: { + originalPath: undefined, + resolvedFileName: "d:/bar/node_modules/e/index.ts", + isExternalLibraryImport: true, + extension: Extension.Ts, + }, + failedLookupLocations: [], }); - it("module name as directory - load index.d.ts", () => { - test(/*hasDirectoryExists*/ false); - test(/*hasDirectoryExists*/ true); - - function test(hasDirectoryExists: boolean) { - const containingFile = { name: "/a/b/c.ts" }; - const packageJson = { name: "/a/b/foo/package.json", content: JSON.stringify({ main: "/c/d" }) }; - const indexFile = { name: "/a/b/foo/index.d.ts" }; - const resolution = nodeModuleNameResolver("./foo", containingFile.name, {}, createModuleResolutionHost(hasDirectoryExists, containingFile, packageJson, indexFile)); - checkResolvedModuleWithFailedLookupLocations(resolution, createResolvedModule(indexFile.name), [ - "/a/b/foo.ts", - "/a/b/foo.tsx", - "/a/b/foo.d.ts", - "/c/d", - "/c/d.ts", - "/c/d.tsx", - "/c/d.d.ts", - "/c/d/index.ts", - "/c/d/index.tsx", - "/c/d/index.d.ts", - "/a/b/foo/index.ts", - "/a/b/foo/index.tsx", - ]); - } + assert.isDefined(cache.get("c:/foo")); + assert.isDefined(cache.get("c:/")); + assert.isUndefined(cache.get("d:/")); + cache = resolutionCache.getOrCreateCacheForModuleName("f"); + cache.set("/foo/bar/baz", { + resolvedModule: undefined, + failedLookupLocations: [], }); + assert.isDefined(cache.get("/foo/bar/baz")); + assert.isDefined(cache.get("/foo/bar")); + assert.isDefined(cache.get("/foo")); + assert.isDefined(cache.get("/")); }); - - describe("unittests:: moduleResolution:: Node module resolution - non-relative paths", () => { - it("computes correct commonPrefix for moduleName cache", () => { - const resolutionCache = createModuleResolutionCache("/", (f) => f); - let cache = resolutionCache.getOrCreateCacheForModuleName("a"); - cache.set("/sub", { - resolvedModule: { - originalPath: undefined, - resolvedFileName: "/sub/node_modules/a/index.ts", - isExternalLibraryImport: true, - extension: Extension.Ts, - }, - failedLookupLocations: [], - }); - assert.isDefined(cache.get("/sub")); - assert.isUndefined(cache.get("/")); - - cache = resolutionCache.getOrCreateCacheForModuleName("b"); - cache.set("/sub/dir/foo", { - resolvedModule: { - originalPath: undefined, - resolvedFileName: "/sub/directory/node_modules/b/index.ts", - isExternalLibraryImport: true, - extension: Extension.Ts, - }, - failedLookupLocations: [], - }); - assert.isDefined(cache.get("/sub/dir/foo")); - assert.isDefined(cache.get("/sub/dir")); - assert.isDefined(cache.get("/sub")); - assert.isUndefined(cache.get("/")); - - cache = resolutionCache.getOrCreateCacheForModuleName("c"); - cache.set("/foo/bar", { - resolvedModule: { - originalPath: undefined, - resolvedFileName: "/bar/node_modules/c/index.ts", - isExternalLibraryImport: true, - extension: Extension.Ts, - }, - failedLookupLocations: [], - }); - assert.isDefined(cache.get("/foo/bar")); - assert.isDefined(cache.get("/foo")); - assert.isDefined(cache.get("/")); - - cache = resolutionCache.getOrCreateCacheForModuleName("d"); - cache.set("/foo", { - resolvedModule: { - originalPath: undefined, - resolvedFileName: "/foo/index.ts", - isExternalLibraryImport: true, - extension: Extension.Ts, - }, - failedLookupLocations: [], - }); - assert.isDefined(cache.get("/foo")); - assert.isUndefined(cache.get("/")); - - cache = resolutionCache.getOrCreateCacheForModuleName("e"); - cache.set("c:/foo", { - resolvedModule: { - originalPath: undefined, - resolvedFileName: "d:/bar/node_modules/e/index.ts", - isExternalLibraryImport: true, - extension: Extension.Ts, - }, - failedLookupLocations: [], - }); - assert.isDefined(cache.get("c:/foo")); - assert.isDefined(cache.get("c:/")); - assert.isUndefined(cache.get("d:/")); - - cache = resolutionCache.getOrCreateCacheForModuleName("f"); - cache.set("/foo/bar/baz", { - resolvedModule: undefined, - failedLookupLocations: [], - }); - assert.isDefined(cache.get("/foo/bar/baz")); - assert.isDefined(cache.get("/foo/bar")); - assert.isDefined(cache.get("/foo")); - assert.isDefined(cache.get("/")); - }); - - it("load module as file - ts files not loaded", () => { - test(/*hasDirectoryExists*/ false); - test(/*hasDirectoryExists*/ true); - - function test(hasDirectoryExists: boolean) { - const containingFile = { name: "/a/b/c/d/e.ts" }; - const moduleFile = { name: "/a/b/node_modules/foo.ts" }; - const resolution = nodeModuleNameResolver("foo", containingFile.name, {}, createModuleResolutionHost(hasDirectoryExists, containingFile, moduleFile)); - checkResolvedModuleWithFailedLookupLocations(resolution, createResolvedModule(moduleFile.name, /*isExternalLibraryImport*/ true), [ - "/a/b/c/d/node_modules/foo/package.json", - "/a/b/c/d/node_modules/foo.ts", - "/a/b/c/d/node_modules/foo.tsx", - "/a/b/c/d/node_modules/foo.d.ts", - - "/a/b/c/d/node_modules/foo/index.ts", - "/a/b/c/d/node_modules/foo/index.tsx", - "/a/b/c/d/node_modules/foo/index.d.ts", - - "/a/b/c/d/node_modules/@types/foo/package.json", - "/a/b/c/d/node_modules/@types/foo.d.ts", - - "/a/b/c/d/node_modules/@types/foo/index.d.ts", - - "/a/b/c/node_modules/foo/package.json", - "/a/b/c/node_modules/foo.ts", - "/a/b/c/node_modules/foo.tsx", - "/a/b/c/node_modules/foo.d.ts", - - "/a/b/c/node_modules/foo/index.ts", - "/a/b/c/node_modules/foo/index.tsx", - "/a/b/c/node_modules/foo/index.d.ts", - - "/a/b/c/node_modules/@types/foo/package.json", - "/a/b/c/node_modules/@types/foo.d.ts", - - "/a/b/c/node_modules/@types/foo/index.d.ts", - "/a/b/node_modules/foo/package.json", - ]); - } - }); - - it("load module as file", () => { - test(/*hasDirectoryExists*/ false); - test(/*hasDirectoryExists*/ true); - - function test(hasDirectoryExists: boolean) { - const containingFile = { name: "/a/b/c/d/e.ts" }; - const moduleFile = { name: "/a/b/node_modules/foo.d.ts" }; - const resolution = nodeModuleNameResolver("foo", containingFile.name, {}, createModuleResolutionHost(hasDirectoryExists, containingFile, moduleFile)); - checkResolvedModule(resolution.resolvedModule, createResolvedModule(moduleFile.name, /*isExternalLibraryImport*/ true)); - } - }); - - it("load module as directory", () => { - test(/*hasDirectoryExists*/ false); - test(/*hasDirectoryExists*/ true); - - function test(hasDirectoryExists: boolean) { - const containingFile: File = { name: "/a/node_modules/b/c/node_modules/d/e.ts" }; - const moduleFile: File = { name: "/a/node_modules/foo/index.d.ts" }; - const resolution = nodeModuleNameResolver("foo", containingFile.name, {}, createModuleResolutionHost(hasDirectoryExists, containingFile, moduleFile)); - checkResolvedModuleWithFailedLookupLocations(resolution, createResolvedModule(moduleFile.name, /*isExternalLibraryImport*/ true), [ - "/a/node_modules/b/c/node_modules/d/node_modules/foo/package.json", - "/a/node_modules/b/c/node_modules/d/node_modules/foo.ts", - "/a/node_modules/b/c/node_modules/d/node_modules/foo.tsx", - "/a/node_modules/b/c/node_modules/d/node_modules/foo.d.ts", - - "/a/node_modules/b/c/node_modules/d/node_modules/foo/index.ts", - "/a/node_modules/b/c/node_modules/d/node_modules/foo/index.tsx", - "/a/node_modules/b/c/node_modules/d/node_modules/foo/index.d.ts", - - "/a/node_modules/b/c/node_modules/d/node_modules/@types/foo/package.json", - "/a/node_modules/b/c/node_modules/d/node_modules/@types/foo.d.ts", - - "/a/node_modules/b/c/node_modules/d/node_modules/@types/foo/index.d.ts", - - "/a/node_modules/b/c/node_modules/foo/package.json", - "/a/node_modules/b/c/node_modules/foo.ts", - "/a/node_modules/b/c/node_modules/foo.tsx", - "/a/node_modules/b/c/node_modules/foo.d.ts", - - "/a/node_modules/b/c/node_modules/foo/index.ts", - "/a/node_modules/b/c/node_modules/foo/index.tsx", - "/a/node_modules/b/c/node_modules/foo/index.d.ts", - - "/a/node_modules/b/c/node_modules/@types/foo/package.json", - "/a/node_modules/b/c/node_modules/@types/foo.d.ts", - - "/a/node_modules/b/c/node_modules/@types/foo/index.d.ts", - - "/a/node_modules/b/node_modules/foo/package.json", - "/a/node_modules/b/node_modules/foo.ts", - "/a/node_modules/b/node_modules/foo.tsx", - "/a/node_modules/b/node_modules/foo.d.ts", - - "/a/node_modules/b/node_modules/foo/index.ts", - "/a/node_modules/b/node_modules/foo/index.tsx", - "/a/node_modules/b/node_modules/foo/index.d.ts", - - "/a/node_modules/b/node_modules/@types/foo/package.json", - "/a/node_modules/b/node_modules/@types/foo.d.ts", - - "/a/node_modules/b/node_modules/@types/foo/index.d.ts", - - "/a/node_modules/foo/package.json", - "/a/node_modules/foo.ts", - "/a/node_modules/foo.tsx", - "/a/node_modules/foo.d.ts", - - "/a/node_modules/foo/index.ts", - "/a/node_modules/foo/index.tsx" - ]); - } - }); - - testPreserveSymlinks(/*preserveSymlinks*/ false); - testPreserveSymlinks(/*preserveSymlinks*/ true); - function testPreserveSymlinks(preserveSymlinks: boolean) { - it(`preserveSymlinks: ${preserveSymlinks}`, () => { - const realFileName = "/linked/index.d.ts"; - const symlinkFileName = "/app/node_modules/linked/index.d.ts"; - const host = createModuleResolutionHost( - /*hasDirectoryExists*/ true, - { name: realFileName, symlinks: [symlinkFileName] }, - { name: "/app/node_modules/linked/package.json", content: '{"version": "0.0.0", "main": "./index"}' }, - ); - const resolution = nodeModuleNameResolver("linked", "/app/app.ts", { preserveSymlinks }, host); - const resolvedFileName = preserveSymlinks ? symlinkFileName : realFileName; - checkResolvedModule(resolution.resolvedModule, createResolvedModule(resolvedFileName, /*isExternalLibraryImport*/ true)); - }); + it("load module as file - ts files not loaded", () => { + test(/*hasDirectoryExists*/ false); + test(/*hasDirectoryExists*/ true); + function test(hasDirectoryExists: boolean) { + const containingFile = { name: "/a/b/c/d/e.ts" }; + const moduleFile = { name: "/a/b/node_modules/foo.ts" }; + const resolution = nodeModuleNameResolver("foo", containingFile.name, {}, createModuleResolutionHost(hasDirectoryExists, containingFile, moduleFile)); + checkResolvedModuleWithFailedLookupLocations(resolution, createResolvedModule(moduleFile.name, /*isExternalLibraryImport*/ true), [ + "/a/b/c/d/node_modules/foo/package.json", + "/a/b/c/d/node_modules/foo.ts", + "/a/b/c/d/node_modules/foo.tsx", + "/a/b/c/d/node_modules/foo.d.ts", + "/a/b/c/d/node_modules/foo/index.ts", + "/a/b/c/d/node_modules/foo/index.tsx", + "/a/b/c/d/node_modules/foo/index.d.ts", + "/a/b/c/d/node_modules/@types/foo/package.json", + "/a/b/c/d/node_modules/@types/foo.d.ts", + "/a/b/c/d/node_modules/@types/foo/index.d.ts", + "/a/b/c/node_modules/foo/package.json", + "/a/b/c/node_modules/foo.ts", + "/a/b/c/node_modules/foo.tsx", + "/a/b/c/node_modules/foo.d.ts", + "/a/b/c/node_modules/foo/index.ts", + "/a/b/c/node_modules/foo/index.tsx", + "/a/b/c/node_modules/foo/index.d.ts", + "/a/b/c/node_modules/@types/foo/package.json", + "/a/b/c/node_modules/@types/foo.d.ts", + "/a/b/c/node_modules/@types/foo/index.d.ts", + "/a/b/node_modules/foo/package.json", + ]); + } + }); + it("load module as file", () => { + test(/*hasDirectoryExists*/ false); + test(/*hasDirectoryExists*/ true); + function test(hasDirectoryExists: boolean) { + const containingFile = { name: "/a/b/c/d/e.ts" }; + const moduleFile = { name: "/a/b/node_modules/foo.d.ts" }; + const resolution = nodeModuleNameResolver("foo", containingFile.name, {}, createModuleResolutionHost(hasDirectoryExists, containingFile, moduleFile)); + checkResolvedModule(resolution.resolvedModule, createResolvedModule(moduleFile.name, /*isExternalLibraryImport*/ true)); + } + }); + it("load module as directory", () => { + test(/*hasDirectoryExists*/ false); + test(/*hasDirectoryExists*/ true); + function test(hasDirectoryExists: boolean) { + const containingFile: File = { name: "/a/node_modules/b/c/node_modules/d/e.ts" }; + const moduleFile: File = { name: "/a/node_modules/foo/index.d.ts" }; + const resolution = nodeModuleNameResolver("foo", containingFile.name, {}, createModuleResolutionHost(hasDirectoryExists, containingFile, moduleFile)); + checkResolvedModuleWithFailedLookupLocations(resolution, createResolvedModule(moduleFile.name, /*isExternalLibraryImport*/ true), [ + "/a/node_modules/b/c/node_modules/d/node_modules/foo/package.json", + "/a/node_modules/b/c/node_modules/d/node_modules/foo.ts", + "/a/node_modules/b/c/node_modules/d/node_modules/foo.tsx", + "/a/node_modules/b/c/node_modules/d/node_modules/foo.d.ts", + "/a/node_modules/b/c/node_modules/d/node_modules/foo/index.ts", + "/a/node_modules/b/c/node_modules/d/node_modules/foo/index.tsx", + "/a/node_modules/b/c/node_modules/d/node_modules/foo/index.d.ts", + "/a/node_modules/b/c/node_modules/d/node_modules/@types/foo/package.json", + "/a/node_modules/b/c/node_modules/d/node_modules/@types/foo.d.ts", + "/a/node_modules/b/c/node_modules/d/node_modules/@types/foo/index.d.ts", + "/a/node_modules/b/c/node_modules/foo/package.json", + "/a/node_modules/b/c/node_modules/foo.ts", + "/a/node_modules/b/c/node_modules/foo.tsx", + "/a/node_modules/b/c/node_modules/foo.d.ts", + "/a/node_modules/b/c/node_modules/foo/index.ts", + "/a/node_modules/b/c/node_modules/foo/index.tsx", + "/a/node_modules/b/c/node_modules/foo/index.d.ts", + "/a/node_modules/b/c/node_modules/@types/foo/package.json", + "/a/node_modules/b/c/node_modules/@types/foo.d.ts", + "/a/node_modules/b/c/node_modules/@types/foo/index.d.ts", + "/a/node_modules/b/node_modules/foo/package.json", + "/a/node_modules/b/node_modules/foo.ts", + "/a/node_modules/b/node_modules/foo.tsx", + "/a/node_modules/b/node_modules/foo.d.ts", + "/a/node_modules/b/node_modules/foo/index.ts", + "/a/node_modules/b/node_modules/foo/index.tsx", + "/a/node_modules/b/node_modules/foo/index.d.ts", + "/a/node_modules/b/node_modules/@types/foo/package.json", + "/a/node_modules/b/node_modules/@types/foo.d.ts", + "/a/node_modules/b/node_modules/@types/foo/index.d.ts", + "/a/node_modules/foo/package.json", + "/a/node_modules/foo.ts", + "/a/node_modules/foo.tsx", + "/a/node_modules/foo.d.ts", + "/a/node_modules/foo/index.ts", + "/a/node_modules/foo/index.tsx" + ]); } - - it("uses originalPath for caching", () => { + }); + testPreserveSymlinks(/*preserveSymlinks*/ false); + testPreserveSymlinks(/*preserveSymlinks*/ true); + function testPreserveSymlinks(preserveSymlinks: boolean) { + it(`preserveSymlinks: ${preserveSymlinks}`, () => { + const realFileName = "/linked/index.d.ts"; + const symlinkFileName = "/app/node_modules/linked/index.d.ts"; const host = createModuleResolutionHost( - /*hasDirectoryExists*/ true, - { - name: "/modules/a.ts", - symlinks: ["/sub/node_modules/a/index.ts"], - }, - { - name: "/sub/node_modules/a/package.json", - content: '{"version": "0.0.0", "main": "./index"}' - } - ); - const compilerOptions: CompilerOptions = { moduleResolution: ModuleResolutionKind.NodeJs }; - const cache = createModuleResolutionCache("/", (f) => f); - let resolution = resolveModuleName("a", "/sub/dir/foo.ts", compilerOptions, host, cache); - checkResolvedModule(resolution.resolvedModule, createResolvedModule("/modules/a.ts", /*isExternalLibraryImport*/ true)); - - resolution = resolveModuleName("a", "/sub/foo.ts", compilerOptions, host, cache); - checkResolvedModule(resolution.resolvedModule, createResolvedModule("/modules/a.ts", /*isExternalLibraryImport*/ true)); - - resolution = resolveModuleName("a", "/foo.ts", compilerOptions, host, cache); - assert.isUndefined(resolution.resolvedModule, "lookup in parent directory doesn't hit the cache"); + /*hasDirectoryExists*/ true, { name: realFileName, symlinks: [symlinkFileName] }, { name: "/app/node_modules/linked/package.json", content: '{"version": "0.0.0", "main": "./index"}' }); + const resolution = nodeModuleNameResolver("linked", "/app/app.ts", { preserveSymlinks }, host); + const resolvedFileName = preserveSymlinks ? symlinkFileName : realFileName; + checkResolvedModule(resolution.resolvedModule, createResolvedModule(resolvedFileName, /*isExternalLibraryImport*/ true)); }); - - it("preserves originalPath on cache hit", () => { - const host = createModuleResolutionHost( - /*hasDirectoryExists*/ true, - { name: "/linked/index.d.ts", symlinks: ["/app/node_modules/linked/index.d.ts"] }, - { name: "/app/node_modules/linked/package.json", content: '{"version": "0.0.0", "main": "./index"}' }, - ); - const cache = createModuleResolutionCache("/", (f) => f); - const compilerOptions: CompilerOptions = { moduleResolution: ModuleResolutionKind.NodeJs }; - checkResolution(resolveModuleName("linked", "/app/src/app.ts", compilerOptions, host, cache)); - checkResolution(resolveModuleName("linked", "/app/lib/main.ts", compilerOptions, host, cache)); - - function checkResolution(resolution: ResolvedModuleWithFailedLookupLocations) { - checkResolvedModule(resolution.resolvedModule, createResolvedModule("/linked/index.d.ts", /*isExternalLibraryImport*/ true)); - assert.strictEqual(resolution.resolvedModule!.originalPath, "/app/node_modules/linked/index.d.ts"); - } + } + it("uses originalPath for caching", () => { + const host = createModuleResolutionHost( + /*hasDirectoryExists*/ true, { + name: "/modules/a.ts", + symlinks: ["/sub/node_modules/a/index.ts"], + }, { + name: "/sub/node_modules/a/package.json", + content: '{"version": "0.0.0", "main": "./index"}' }); + const compilerOptions: CompilerOptions = { moduleResolution: ModuleResolutionKind.NodeJs }; + const cache = createModuleResolutionCache("/", (f) => f); + let resolution = resolveModuleName("a", "/sub/dir/foo.ts", compilerOptions, host, cache); + checkResolvedModule(resolution.resolvedModule, createResolvedModule("/modules/a.ts", /*isExternalLibraryImport*/ true)); + resolution = resolveModuleName("a", "/sub/foo.ts", compilerOptions, host, cache); + checkResolvedModule(resolution.resolvedModule, createResolvedModule("/modules/a.ts", /*isExternalLibraryImport*/ true)); + resolution = resolveModuleName("a", "/foo.ts", compilerOptions, host, cache); + assert.isUndefined(resolution.resolvedModule, "lookup in parent directory doesn't hit the cache"); }); - - describe("unittests:: moduleResolution:: Relative imports", () => { - function test(files: Map, currentDirectory: string, rootFiles: string[], expectedFilesCount: number, relativeNamesToCheck: string[]) { - const options: CompilerOptions = { module: ModuleKind.CommonJS }; - const host: CompilerHost = { - getSourceFile: (fileName: string, languageVersion: ScriptTarget) => { - const path = normalizePath(combinePaths(currentDirectory, fileName)); - const file = files.get(path); - return file ? createSourceFile(fileName, file, languageVersion) : undefined; - }, - getDefaultLibFileName: () => "lib.d.ts", - writeFile: notImplemented, - getCurrentDirectory: () => currentDirectory, - getDirectories: () => [], - getCanonicalFileName: fileName => fileName.toLowerCase(), - getNewLine: () => "\r\n", - useCaseSensitiveFileNames: () => false, - fileExists: fileName => { - const path = normalizePath(combinePaths(currentDirectory, fileName)); - return files.has(path); - }, - readFile: notImplemented, - }; - - const program = createProgram(rootFiles, options, host); - - assert.equal(program.getSourceFiles().length, expectedFilesCount); - const syntacticDiagnostics = program.getSyntacticDiagnostics(); - assert.equal(syntacticDiagnostics.length, 0, `expect no syntactic diagnostics, got: ${JSON.stringify(Harness.Compiler.minimalDiagnosticsToString(syntacticDiagnostics))}`); - const semanticDiagnostics = program.getSemanticDiagnostics(); - assert.equal(semanticDiagnostics.length, 0, `expect no semantic diagnostics, got: ${JSON.stringify(Harness.Compiler.minimalDiagnosticsToString(semanticDiagnostics))}`); - - // try to get file using a relative name - for (const relativeFileName of relativeNamesToCheck) { - assert.isTrue(program.getSourceFile(relativeFileName) !== undefined, `expected to get file by relative name, got undefined`); - } + it("preserves originalPath on cache hit", () => { + const host = createModuleResolutionHost( + /*hasDirectoryExists*/ true, { name: "/linked/index.d.ts", symlinks: ["/app/node_modules/linked/index.d.ts"] }, { name: "/app/node_modules/linked/package.json", content: '{"version": "0.0.0", "main": "./index"}' }); + const cache = createModuleResolutionCache("/", (f) => f); + const compilerOptions: CompilerOptions = { moduleResolution: ModuleResolutionKind.NodeJs }; + checkResolution(resolveModuleName("linked", "/app/src/app.ts", compilerOptions, host, cache)); + checkResolution(resolveModuleName("linked", "/app/lib/main.ts", compilerOptions, host, cache)); + function checkResolution(resolution: ResolvedModuleWithFailedLookupLocations) { + checkResolvedModule(resolution.resolvedModule, createResolvedModule("/linked/index.d.ts", /*isExternalLibraryImport*/ true)); + assert.strictEqual(resolution.resolvedModule!.originalPath, "/app/node_modules/linked/index.d.ts"); } - - it("should find all modules", () => { - const files = createMapFromTemplate({ - "/a/b/c/first/shared.ts": ` + }); +}); +describe("unittests:: moduleResolution:: Relative imports", () => { + function test(files: ts.Map, currentDirectory: string, rootFiles: string[], expectedFilesCount: number, relativeNamesToCheck: string[]) { + const options: CompilerOptions = { module: ModuleKind.CommonJS }; + const host: CompilerHost = { + getSourceFile: (fileName: string, languageVersion: ScriptTarget) => { + const path = normalizePath(combinePaths(currentDirectory, fileName)); + const file = files.get(path); + return file ? createSourceFile(fileName, file, languageVersion) : undefined; + }, + getDefaultLibFileName: () => "lib.d.ts", + writeFile: notImplemented, + getCurrentDirectory: () => currentDirectory, + getDirectories: () => [], + getCanonicalFileName: fileName => fileName.toLowerCase(), + getNewLine: () => "\r\n", + useCaseSensitiveFileNames: () => false, + fileExists: fileName => { + const path = normalizePath(combinePaths(currentDirectory, fileName)); + return files.has(path); + }, + readFile: notImplemented, + }; + const program = createProgram(rootFiles, options, host); + assert.equal(program.getSourceFiles().length, expectedFilesCount); + const syntacticDiagnostics = program.getSyntacticDiagnostics(); + assert.equal(syntacticDiagnostics.length, 0, `expect no syntactic diagnostics, got: ${JSON.stringify(Compiler.minimalDiagnosticsToString(syntacticDiagnostics))}`); + const semanticDiagnostics = program.getSemanticDiagnostics(); + assert.equal(semanticDiagnostics.length, 0, `expect no semantic diagnostics, got: ${JSON.stringify(Compiler.minimalDiagnosticsToString(semanticDiagnostics))}`); + // try to get file using a relative name + for (const relativeFileName of relativeNamesToCheck) { + assert.isTrue(program.getSourceFile(relativeFileName) !== undefined, `expected to get file by relative name, got undefined`); + } + } + it("should find all modules", () => { + const files = createMapFromTemplate({ + "/a/b/c/first/shared.ts": ` class A {} export = A`, - "/a/b/c/first/second/class_a.ts": ` + "/a/b/c/first/second/class_a.ts": ` import Shared = require('../shared'); import C = require('../../third/class_c'); class B {} export = B;`, - "/a/b/c/third/class_c.ts": ` + "/a/b/c/third/class_c.ts": ` import Shared = require('../first/shared'); class C {} export = C; ` - }); - test(files, "/a/b/c/first/second", ["class_a.ts"], 3, ["../../../c/third/class_c.ts"]); }); - - it("should find modules in node_modules", () => { - const files = createMapFromTemplate({ - "/parent/node_modules/mod/index.d.ts": "export var x", - "/parent/app/myapp.ts": `import {x} from "mod"` - }); - test(files, "/parent/app", ["myapp.ts"], 2, []); + test(files, "/a/b/c/first/second", ["class_a.ts"], 3, ["../../../c/third/class_c.ts"]); + }); + it("should find modules in node_modules", () => { + const files = createMapFromTemplate({ + "/parent/node_modules/mod/index.d.ts": "export var x", + "/parent/app/myapp.ts": `import {x} from "mod"` }); - - it("should find file referenced via absolute and relative names", () => { - const files = createMapFromTemplate({ - "/a/b/c.ts": `/// `, - "/a/b/b.ts": "var x" - }); - test(files, "/a/b", ["c.ts", "/a/b/b.ts"], 2, []); + test(files, "/parent/app", ["myapp.ts"], 2, []); + }); + it("should find file referenced via absolute and relative names", () => { + const files = createMapFromTemplate({ + "/a/b/c.ts": `/// `, + "/a/b/b.ts": "var x" }); + test(files, "/a/b", ["c.ts", "/a/b/b.ts"], 2, []); }); - - describe("unittests:: moduleResolution:: Files with different casing with forceConsistentCasingInFileNames", () => { - let library: SourceFile; - function test( - files: Map, - options: CompilerOptions, - currentDirectory: string, - useCaseSensitiveFileNames: boolean, - rootFiles: string[], - expectedDiagnostics: (program: Program) => readonly Diagnostic[] - ): void { - const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); - if (!useCaseSensitiveFileNames) { - const oldFiles = files; - files = createMap(); - oldFiles.forEach((file, fileName) => { - files.set(getCanonicalFileName(fileName), file); - }); - } - - const host: CompilerHost = { - getSourceFile: (fileName: string, languageVersion: ScriptTarget) => { - if (fileName === "lib.d.ts") { - if (!library) { - library = createSourceFile("lib.d.ts", "", ScriptTarget.ES5); - } - return library; - } - const path = getCanonicalFileName(normalizePath(combinePaths(currentDirectory, fileName))); - const file = files.get(path); - return file ? createSourceFile(fileName, file, languageVersion) : undefined; - }, - getDefaultLibFileName: () => "lib.d.ts", - writeFile: notImplemented, - getCurrentDirectory: () => currentDirectory, - getDirectories: () => [], - getCanonicalFileName, - getNewLine: () => "\r\n", - useCaseSensitiveFileNames: () => useCaseSensitiveFileNames, - fileExists: fileName => { - const path = getCanonicalFileName(normalizePath(combinePaths(currentDirectory, fileName))); - return files.has(path); - }, - readFile: notImplemented, - }; - const program = createProgram(rootFiles, options, host); - const diagnostics = sortAndDeduplicateDiagnostics([...program.getSemanticDiagnostics(), ...program.getOptionsDiagnostics()]); - assert.deepEqual(diagnostics, sortAndDeduplicateDiagnostics(expectedDiagnostics(program))); - } - - it("should succeed when the same file is referenced using absolute and relative names", () => { - const files = createMapFromTemplate({ - "/a/b/c.ts": `/// `, - "/a/b/d.ts": "var x" +}); +describe("unittests:: moduleResolution:: Files with different casing with forceConsistentCasingInFileNames", () => { + let library: SourceFile; + function test(files: ts.Map, options: CompilerOptions, currentDirectory: string, useCaseSensitiveFileNames: boolean, rootFiles: string[], expectedDiagnostics: (program: Program) => readonly Diagnostic[]): void { + const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); + if (!useCaseSensitiveFileNames) { + const oldFiles = files; + files = createMap(); + oldFiles.forEach((file, fileName) => { + files.set(getCanonicalFileName(fileName), file); }); - test( - files, - { module: ModuleKind.AMD }, - "/a/b", - /*useCaseSensitiveFileNames*/ false, - ["c.ts", "/a/b/d.ts"], - () => emptyArray - ); + } + const host: CompilerHost = { + getSourceFile: (fileName: string, languageVersion: ScriptTarget) => { + if (fileName === "lib.d.ts") { + if (!library) { + library = createSourceFile("lib.d.ts", "", ScriptTarget.ES5); + } + return library; + } + const path = getCanonicalFileName(normalizePath(combinePaths(currentDirectory, fileName))); + const file = files.get(path); + return file ? createSourceFile(fileName, file, languageVersion) : undefined; + }, + getDefaultLibFileName: () => "lib.d.ts", + writeFile: notImplemented, + getCurrentDirectory: () => currentDirectory, + getDirectories: () => [], + getCanonicalFileName, + getNewLine: () => "\r\n", + useCaseSensitiveFileNames: () => useCaseSensitiveFileNames, + fileExists: fileName => { + const path = getCanonicalFileName(normalizePath(combinePaths(currentDirectory, fileName))); + return files.has(path); + }, + readFile: notImplemented, + }; + const program = createProgram(rootFiles, options, host); + const diagnostics = sortAndDeduplicateDiagnostics([...program.getSemanticDiagnostics(), ...program.getOptionsDiagnostics()]); + assert.deepEqual(diagnostics, sortAndDeduplicateDiagnostics(expectedDiagnostics(program))); + } + it("should succeed when the same file is referenced using absolute and relative names", () => { + const files = createMapFromTemplate({ + "/a/b/c.ts": `/// `, + "/a/b/d.ts": "var x" }); - - it("should fail when two files used in program differ only in casing (tripleslash references)", () => { - const files = createMapFromTemplate({ - "/a/b/c.ts": `/// `, - "/a/b/d.ts": "var x" - }); - test( - files, - { module: ModuleKind.AMD, forceConsistentCasingInFileNames: true }, - "/a/b", - /*useCaseSensitiveFileNames*/ false, - ["c.ts", "d.ts"], - program => [{ - ...tscWatch.getDiagnosticOfFileFromProgram( - program, - "c.ts", - `/// `.indexOf(`D.ts`), - "D.ts".length, - Diagnostics.Already_included_file_name_0_differs_from_file_name_1_only_in_casing, - "D.ts", - "d.ts", - ), - reportsUnnecessary: undefined - }] - ); + test(files, { module: ModuleKind.AMD }, "/a/b", + /*useCaseSensitiveFileNames*/ false, ["c.ts", "/a/b/d.ts"], () => emptyArray); + }); + it("should fail when two files used in program differ only in casing (tripleslash references)", () => { + const files = createMapFromTemplate({ + "/a/b/c.ts": `/// `, + "/a/b/d.ts": "var x" }); - - it("should fail when two files used in program differ only in casing (imports)", () => { - const files = createMapFromTemplate({ - "/a/b/c.ts": `import {x} from "D"`, - "/a/b/d.ts": "export var x" - }); - test( - files, - { module: ModuleKind.AMD, forceConsistentCasingInFileNames: true }, - "/a/b", - /*useCaseSensitiveFileNames*/ false, - ["c.ts", "d.ts"], - program => [{ - ...tscWatch.getDiagnosticOfFileFromProgram( - program, - "c.ts", - `import {x} from "D"`.indexOf(`"D"`), - `"D"`.length, - Diagnostics.Already_included_file_name_0_differs_from_file_name_1_only_in_casing, - "/a/b/D.ts", - "d.ts", - ), - reportsUnnecessary: undefined - }] - ); + test(files, { module: ModuleKind.AMD, forceConsistentCasingInFileNames: true }, "/a/b", + /*useCaseSensitiveFileNames*/ false, ["c.ts", "d.ts"], program => [{ + ...getDiagnosticOfFileFromProgram(program, "c.ts", `/// `.indexOf(`D.ts`), "D.ts".length, Diagnostics.Already_included_file_name_0_differs_from_file_name_1_only_in_casing, "D.ts", "d.ts"), + reportsUnnecessary: undefined + }]); + }); + it("should fail when two files used in program differ only in casing (imports)", () => { + const files = createMapFromTemplate({ + "/a/b/c.ts": `import {x} from "D"`, + "/a/b/d.ts": "export var x" }); - - it("should fail when two files used in program differ only in casing (imports, relative module names)", () => { - const files = createMapFromTemplate({ - "moduleA.ts": `import {x} from "./ModuleB"`, - "moduleB.ts": "export var x" - }); - test( - files, - { module: ModuleKind.CommonJS, forceConsistentCasingInFileNames: true }, - "", - /*useCaseSensitiveFileNames*/ false, - ["moduleA.ts", "moduleB.ts"], - program => [{ - ...tscWatch.getDiagnosticOfFileFromProgram( - program, - "moduleA.ts", - `import {x} from "./ModuleB"`.indexOf(`"./ModuleB"`), - `"./ModuleB"`.length, - Diagnostics.Already_included_file_name_0_differs_from_file_name_1_only_in_casing, - "ModuleB.ts", - "moduleB.ts", - ), - reportsUnnecessary: undefined - }] - ); + test(files, { module: ModuleKind.AMD, forceConsistentCasingInFileNames: true }, "/a/b", + /*useCaseSensitiveFileNames*/ false, ["c.ts", "d.ts"], program => [{ + ...getDiagnosticOfFileFromProgram(program, "c.ts", `import {x} from "D"`.indexOf(`"D"`), `"D"`.length, Diagnostics.Already_included_file_name_0_differs_from_file_name_1_only_in_casing, "/a/b/D.ts", "d.ts"), + reportsUnnecessary: undefined + }]); + }); + it("should fail when two files used in program differ only in casing (imports, relative module names)", () => { + const files = createMapFromTemplate({ + "moduleA.ts": `import {x} from "./ModuleB"`, + "moduleB.ts": "export var x" }); - - it("should fail when two files exist on disk that differs only in casing", () => { - const files = createMapFromTemplate({ - "/a/b/c.ts": `import {x} from "D"`, - "/a/b/D.ts": "export var x", - "/a/b/d.ts": "export var y" - }); - test( - files, - { module: ModuleKind.AMD }, - "/a/b", - /*useCaseSensitiveFileNames*/ true, - ["c.ts", "d.ts"], - program => [{ - ...tscWatch.getDiagnosticOfFileFromProgram( - program, - "c.ts", - `import {x} from "D"`.indexOf(`"D"`), - `"D"`.length, - Diagnostics.Already_included_file_name_0_differs_from_file_name_1_only_in_casing, - "/a/b/D.ts", - "d.ts", - ), - reportsUnnecessary: undefined - }] - ); + test(files, { module: ModuleKind.CommonJS, forceConsistentCasingInFileNames: true }, "", + /*useCaseSensitiveFileNames*/ false, ["moduleA.ts", "moduleB.ts"], program => [{ + ...getDiagnosticOfFileFromProgram(program, "moduleA.ts", `import {x} from "./ModuleB"`.indexOf(`"./ModuleB"`), `"./ModuleB"`.length, Diagnostics.Already_included_file_name_0_differs_from_file_name_1_only_in_casing, "ModuleB.ts", "moduleB.ts"), + reportsUnnecessary: undefined + }]); + }); + it("should fail when two files exist on disk that differs only in casing", () => { + const files = createMapFromTemplate({ + "/a/b/c.ts": `import {x} from "D"`, + "/a/b/D.ts": "export var x", + "/a/b/d.ts": "export var y" }); - - it("should fail when module name in 'require' calls has inconsistent casing", () => { - const files = createMapFromTemplate({ - "moduleA.ts": `import a = require("./ModuleC")`, - "moduleB.ts": `import a = require("./moduleC")`, - "moduleC.ts": "export var x" - }); - test( - files, - { module: ModuleKind.CommonJS, forceConsistentCasingInFileNames: true }, - "", - /*useCaseSensitiveFileNames*/ false, - ["moduleA.ts", "moduleB.ts", "moduleC.ts"], - program => [ - { - ...tscWatch.getDiagnosticOfFileFromProgram( - program, - "moduleA.ts", - `import a = require("./ModuleC")`.indexOf(`"./ModuleC"`), - `"./ModuleC"`.length, - Diagnostics.Already_included_file_name_0_differs_from_file_name_1_only_in_casing, - "ModuleC.ts", - "moduleC.ts", - ), - reportsUnnecessary: undefined - }, - { - ...tscWatch.getDiagnosticOfFileFromProgram( - program, - "moduleB.ts", - `import a = require("./moduleC")`.indexOf(`"./moduleC"`), - `"./moduleC"`.length, - Diagnostics.File_name_0_differs_from_already_included_file_name_1_only_in_casing, - "moduleC.ts", - "ModuleC.ts" - ), - reportsUnnecessary: undefined - } - ] - ); + test(files, { module: ModuleKind.AMD }, "/a/b", + /*useCaseSensitiveFileNames*/ true, ["c.ts", "d.ts"], program => [{ + ...getDiagnosticOfFileFromProgram(program, "c.ts", `import {x} from "D"`.indexOf(`"D"`), `"D"`.length, Diagnostics.Already_included_file_name_0_differs_from_file_name_1_only_in_casing, "/a/b/D.ts", "d.ts"), + reportsUnnecessary: undefined + }]); + }); + it("should fail when module name in 'require' calls has inconsistent casing", () => { + const files = createMapFromTemplate({ + "moduleA.ts": `import a = require("./ModuleC")`, + "moduleB.ts": `import a = require("./moduleC")`, + "moduleC.ts": "export var x" }); - - it("should fail when module names in 'require' calls has inconsistent casing and current directory has uppercase chars", () => { - const files = createMapFromTemplate({ - "/a/B/c/moduleA.ts": `import a = require("./ModuleC")`, - "/a/B/c/moduleB.ts": `import a = require("./moduleC")`, - "/a/B/c/moduleC.ts": "export var x", - "/a/B/c/moduleD.ts": ` + test(files, { module: ModuleKind.CommonJS, forceConsistentCasingInFileNames: true }, "", + /*useCaseSensitiveFileNames*/ false, ["moduleA.ts", "moduleB.ts", "moduleC.ts"], program => [ + { + ...getDiagnosticOfFileFromProgram(program, "moduleA.ts", `import a = require("./ModuleC")`.indexOf(`"./ModuleC"`), `"./ModuleC"`.length, Diagnostics.Already_included_file_name_0_differs_from_file_name_1_only_in_casing, "ModuleC.ts", "moduleC.ts"), + reportsUnnecessary: undefined + }, + { + ...getDiagnosticOfFileFromProgram(program, "moduleB.ts", `import a = require("./moduleC")`.indexOf(`"./moduleC"`), `"./moduleC"`.length, Diagnostics.File_name_0_differs_from_already_included_file_name_1_only_in_casing, "moduleC.ts", "ModuleC.ts"), + reportsUnnecessary: undefined + } + ]); + }); + it("should fail when module names in 'require' calls has inconsistent casing and current directory has uppercase chars", () => { + const files = createMapFromTemplate({ + "/a/B/c/moduleA.ts": `import a = require("./ModuleC")`, + "/a/B/c/moduleB.ts": `import a = require("./moduleC")`, + "/a/B/c/moduleC.ts": "export var x", + "/a/B/c/moduleD.ts": ` import a = require("./moduleA"); import b = require("./moduleB"); ` - }); - test( - files, - { module: ModuleKind.CommonJS, forceConsistentCasingInFileNames: true }, - "/a/B/c", - /*useCaseSensitiveFileNames*/ false, - ["moduleD.ts"], - program => [{ - ...tscWatch.getDiagnosticOfFileFromProgram( - program, - "moduleB.ts", - `import a = require("./moduleC")`.indexOf(`"./moduleC"`), - `"./moduleC"`.length, - Diagnostics.File_name_0_differs_from_already_included_file_name_1_only_in_casing, - "/a/B/c/moduleC.ts", - "/a/B/c/ModuleC.ts" - ), - reportsUnnecessary: undefined - }] - ); }); - it("should not fail when module names in 'require' calls has consistent casing and current directory has uppercase chars", () => { - const files = createMapFromTemplate({ - "/a/B/c/moduleA.ts": `import a = require("./moduleC")`, - "/a/B/c/moduleB.ts": `import a = require("./moduleC")`, - "/a/B/c/moduleC.ts": "export var x", - "/a/B/c/moduleD.ts": ` + test(files, { module: ModuleKind.CommonJS, forceConsistentCasingInFileNames: true }, "/a/B/c", + /*useCaseSensitiveFileNames*/ false, ["moduleD.ts"], program => [{ + ...getDiagnosticOfFileFromProgram(program, "moduleB.ts", `import a = require("./moduleC")`.indexOf(`"./moduleC"`), `"./moduleC"`.length, Diagnostics.File_name_0_differs_from_already_included_file_name_1_only_in_casing, "/a/B/c/moduleC.ts", "/a/B/c/ModuleC.ts"), + reportsUnnecessary: undefined + }]); + }); + it("should not fail when module names in 'require' calls has consistent casing and current directory has uppercase chars", () => { + const files = createMapFromTemplate({ + "/a/B/c/moduleA.ts": `import a = require("./moduleC")`, + "/a/B/c/moduleB.ts": `import a = require("./moduleC")`, + "/a/B/c/moduleC.ts": "export var x", + "/a/B/c/moduleD.ts": ` import a = require("./moduleA"); import b = require("./moduleB"); ` - }); - test( - files, - { module: ModuleKind.CommonJS, forceConsistentCasingInFileNames: true }, - "/a/B/c", - /*useCaseSensitiveFileNames*/ false, - ["moduleD.ts"], - () => emptyArray - ); }); - - it("should succeed when the two files in program differ only in drive letter in their names", () => { - const files = createMapFromTemplate({ - "d:/someFolder/moduleA.ts": `import a = require("D:/someFolder/moduleC")`, - "d:/someFolder/moduleB.ts": `import a = require("./moduleC")`, - "D:/someFolder/moduleC.ts": "export const x = 10", - }); - test( - files, - { module: ModuleKind.CommonJS, forceConsistentCasingInFileNames: true }, - "d:/someFolder", - /*useCaseSensitiveFileNames*/ false, - ["d:/someFolder/moduleA.ts", "d:/someFolder/moduleB.ts"], - () => emptyArray - ); + test(files, { module: ModuleKind.CommonJS, forceConsistentCasingInFileNames: true }, "/a/B/c", + /*useCaseSensitiveFileNames*/ false, ["moduleD.ts"], () => emptyArray); + }); + it("should succeed when the two files in program differ only in drive letter in their names", () => { + const files = createMapFromTemplate({ + "d:/someFolder/moduleA.ts": `import a = require("D:/someFolder/moduleC")`, + "d:/someFolder/moduleB.ts": `import a = require("./moduleC")`, + "D:/someFolder/moduleC.ts": "export const x = 10", }); + test(files, { module: ModuleKind.CommonJS, forceConsistentCasingInFileNames: true }, "d:/someFolder", + /*useCaseSensitiveFileNames*/ false, ["d:/someFolder/moduleA.ts", "d:/someFolder/moduleB.ts"], () => emptyArray); }); - - describe("unittests:: moduleResolution:: baseUrl augmented module resolution", () => { - - it("module resolution without path mappings/rootDirs", () => { - test(/*hasDirectoryExists*/ false); - test(/*hasDirectoryExists*/ true); - - function test(hasDirectoryExists: boolean) { - const file1: File = { name: "/root/folder1/file1.ts" }; - const file2: File = { name: "/root/folder2/file2.ts" }; - const file3: File = { name: "/root/folder2/file3.ts" }; - const host = createModuleResolutionHost(hasDirectoryExists, file1, file2, file3); - for (const moduleResolution of [ ModuleResolutionKind.NodeJs, ModuleResolutionKind.Classic ]) { - const options: CompilerOptions = { moduleResolution, baseUrl: "/root" }; - { - const result = resolveModuleName("folder2/file2", file1.name, options, host); - checkResolvedModuleWithFailedLookupLocations(result, createResolvedModule(file2.name), []); - } - { - const result = resolveModuleName("./file3", file2.name, options, host); - checkResolvedModuleWithFailedLookupLocations(result, createResolvedModule(file3.name), []); - } - { - const result = resolveModuleName("/root/folder1/file1", file2.name, options, host); - checkResolvedModuleWithFailedLookupLocations(result, createResolvedModule(file1.name), []); - } +}); +describe("unittests:: moduleResolution:: baseUrl augmented module resolution", () => { + it("module resolution without path mappings/rootDirs", () => { + test(/*hasDirectoryExists*/ false); + test(/*hasDirectoryExists*/ true); + function test(hasDirectoryExists: boolean) { + const file1: File = { name: "/root/folder1/file1.ts" }; + const file2: File = { name: "/root/folder2/file2.ts" }; + const file3: File = { name: "/root/folder2/file3.ts" }; + const host = createModuleResolutionHost(hasDirectoryExists, file1, file2, file3); + for (const moduleResolution of [ModuleResolutionKind.NodeJs, ModuleResolutionKind.Classic]) { + const options: CompilerOptions = { moduleResolution, baseUrl: "/root" }; + { + const result = resolveModuleName("folder2/file2", file1.name, options, host); + checkResolvedModuleWithFailedLookupLocations(result, createResolvedModule(file2.name), []); } - } - // add failure tests - }); - - it("node + baseUrl", () => { - test(/*hasDirectoryExists*/ false); - test(/*hasDirectoryExists*/ true); - - function test(hasDirectoryExists: boolean) { - const main: File = { name: "/root/a/b/main.ts" }; - const m1: File = { name: "/root/m1.ts" }; // load file as module - const m2: File = { name: "/root/m2/index.d.ts" }; // load folder as module - const m3: File = { name: "/root/m3/package.json", content: JSON.stringify({ typings: "dist/typings.d.ts" }) }; - const m3Typings: File = { name: "/root/m3/dist/typings.d.ts" }; - const m4: File = { name: "/root/node_modules/m4.ts" }; // fallback to node - - const options: CompilerOptions = { moduleResolution: ModuleResolutionKind.NodeJs, baseUrl: "/root" }; - const host = createModuleResolutionHost(hasDirectoryExists, main, m1, m2, m3, m3Typings, m4); - - check("m1", main, m1); - check("m2", main, m2); - check("m3", main, m3Typings); - check("m4", main, m4, /*isExternalLibraryImport*/ true); - - function check(name: string, caller: File, expected: File, isExternalLibraryImport = false) { - const result = resolveModuleName(name, caller.name, options, host); - checkResolvedModule(result.resolvedModule, createResolvedModule(expected.name, isExternalLibraryImport)); + { + const result = resolveModuleName("./file3", file2.name, options, host); + checkResolvedModuleWithFailedLookupLocations(result, createResolvedModule(file3.name), []); } - } - }); - - it("classic + baseUrl", () => { - test(/*hasDirectoryExists*/ false); - test(/*hasDirectoryExists*/ true); - - function test(hasDirectoryExists: boolean) { - const main: File = { name: "/root/a/b/main.ts" }; - const m1: File = { name: "/root/x/m1.ts" }; // load from base url - const m2: File = { name: "/m2.ts" }; // fallback to classic - - const options: CompilerOptions = { moduleResolution: ModuleResolutionKind.Classic, baseUrl: "/root/x", jsx: JsxEmit.React }; - const host = createModuleResolutionHost(hasDirectoryExists, main, m1, m2); - - check("m1", main, m1); - check("m2", main, m2); - - function check(name: string, caller: File, expected: File) { - const result = resolveModuleName(name, caller.name, options, host); - checkResolvedModule(result.resolvedModule, createResolvedModule(expected.name)); + { + const result = resolveModuleName("/root/folder1/file1", file2.name, options, host); + checkResolvedModuleWithFailedLookupLocations(result, createResolvedModule(file1.name), []); } } - }); - - it("node + baseUrl + path mappings", () => { - test(/*hasDirectoryExists*/ false); - test(/*hasDirectoryExists*/ true); - - function test(hasDirectoryExists: boolean) { - const main: File = { name: "/root/folder1/main.ts" }; - - const file1: File = { name: "/root/folder1/file1.ts" }; - const file2: File = { name: "/root/generated/folder1/file2.ts" }; // load remapped file as module - const file3: File = { name: "/root/generated/folder2/file3/index.d.ts" }; // load folder a module - const file4Typings: File = { name: "/root/generated/folder2/file4/package.json", content: JSON.stringify({ typings: "dist/types.d.ts" }) }; - const file4: File = { name: "/root/generated/folder2/file4/dist/types.d.ts" }; // load file pointed by typings - const file5: File = { name: "/root/someanotherfolder/file5/index.d.ts" }; // load remapped module from folder - const file6: File = { name: "/root/node_modules/file6.ts" }; // fallback to node - const host = createModuleResolutionHost(hasDirectoryExists, file1, file2, file3, file4, file4Typings, file5, file6); - - const options: CompilerOptions = { - moduleResolution: ModuleResolutionKind.NodeJs, - baseUrl: "/root", - jsx: JsxEmit.React, - paths: { - "*": [ - "*", - "generated/*" - ], - "somefolder/*": [ - "someanotherfolder/*" - ], - "/rooted/*": [ - "generated/*" - ] - } - }; - check("folder1/file1", file1, []); - check("folder1/file2", file2, [ - // first try the '*' - "/root/folder1/file2.ts", - "/root/folder1/file2.tsx", - "/root/folder1/file2.d.ts", - "/root/folder1/file2/package.json", - - "/root/folder1/file2/index.ts", - "/root/folder1/file2/index.tsx", - "/root/folder1/file2/index.d.ts", - // then first attempt on 'generated/*' was successful - ]); - check("/rooted/folder1/file2", file2, []); - check("folder2/file3", file3, [ - // first try '*' - "/root/folder2/file3.ts", - "/root/folder2/file3.tsx", - "/root/folder2/file3.d.ts", - "/root/folder2/file3/package.json", - - "/root/folder2/file3/index.ts", - "/root/folder2/file3/index.tsx", - "/root/folder2/file3/index.d.ts", - - // then use remapped location - "/root/generated/folder2/file3.ts", - "/root/generated/folder2/file3.tsx", - "/root/generated/folder2/file3.d.ts", - "/root/generated/folder2/file3/package.json", - - "/root/generated/folder2/file3/index.ts", - "/root/generated/folder2/file3/index.tsx", - // success on index.d.ts - ]); - check("folder2/file4", file4, [ - // first try '*' - "/root/folder2/file4.ts", - "/root/folder2/file4.tsx", - "/root/folder2/file4.d.ts", - "/root/folder2/file4/package.json", - - "/root/folder2/file4/index.ts", - "/root/folder2/file4/index.tsx", - "/root/folder2/file4/index.d.ts", - - // try to load from file from remapped location - "/root/generated/folder2/file4.ts", - "/root/generated/folder2/file4.tsx", - "/root/generated/folder2/file4.d.ts", - // success on loading as from folder - ]); - check("somefolder/file5", file5, [ - // load from remapped location - // first load from fle - "/root/someanotherfolder/file5.ts", - "/root/someanotherfolder/file5.tsx", - "/root/someanotherfolder/file5.d.ts", - - // load from folder - "/root/someanotherfolder/file5/package.json", - "/root/someanotherfolder/file5/index.ts", - "/root/someanotherfolder/file5/index.tsx", - // success on index.d.ts - ]); - check("file6", file6, [ - // first try * - // load from file - "/root/file6.ts", - "/root/file6.tsx", - "/root/file6.d.ts", - - // load from folder - "/root/file6/package.json", - "/root/file6/index.ts", - "/root/file6/index.tsx", - "/root/file6/index.d.ts", - - // then try 'generated/*' - // load from file - "/root/generated/file6.ts", - "/root/generated/file6.tsx", - "/root/generated/file6.d.ts", - - // load from folder - "/root/generated/file6/package.json", - "/root/generated/file6/index.ts", - "/root/generated/file6/index.tsx", - "/root/generated/file6/index.d.ts", - - // fallback to standard node behavior - "/root/folder1/node_modules/file6/package.json", - - // load from file - "/root/folder1/node_modules/file6.ts", - "/root/folder1/node_modules/file6.tsx", - "/root/folder1/node_modules/file6.d.ts", - - // load from folder - "/root/folder1/node_modules/file6/index.ts", - "/root/folder1/node_modules/file6/index.tsx", - "/root/folder1/node_modules/file6/index.d.ts", - - "/root/folder1/node_modules/@types/file6/package.json", - "/root/folder1/node_modules/@types/file6.d.ts", - "/root/folder1/node_modules/@types/file6/index.d.ts", - - "/root/node_modules/file6/package.json", - // success on /root/node_modules/file6.ts - ], /*isExternalLibraryImport*/ true); - - function check(name: string, expected: File, expectedFailedLookups: string[], isExternalLibraryImport = false) { - const result = resolveModuleName(name, main.name, options, host); - checkResolvedModuleWithFailedLookupLocations(result, createResolvedModule(expected.name, isExternalLibraryImport), expectedFailedLookups); - } + } + // add failure tests + }); + it("node + baseUrl", () => { + test(/*hasDirectoryExists*/ false); + test(/*hasDirectoryExists*/ true); + function test(hasDirectoryExists: boolean) { + const main: File = { name: "/root/a/b/main.ts" }; + const m1: File = { name: "/root/m1.ts" }; // load file as module + const m2: File = { name: "/root/m2/index.d.ts" }; // load folder as module + const m3: File = { name: "/root/m3/package.json", content: JSON.stringify({ typings: "dist/typings.d.ts" }) }; + const m3Typings: File = { name: "/root/m3/dist/typings.d.ts" }; + const m4: File = { name: "/root/node_modules/m4.ts" }; // fallback to node + const options: CompilerOptions = { moduleResolution: ModuleResolutionKind.NodeJs, baseUrl: "/root" }; + const host = createModuleResolutionHost(hasDirectoryExists, main, m1, m2, m3, m3Typings, m4); + check("m1", main, m1); + check("m2", main, m2); + check("m3", main, m3Typings); + check("m4", main, m4, /*isExternalLibraryImport*/ true); + function check(name: string, caller: File, expected: File, isExternalLibraryImport = false) { + const result = resolveModuleName(name, caller.name, options, host); + checkResolvedModule(result.resolvedModule, createResolvedModule(expected.name, isExternalLibraryImport)); } - }); - - it ("classic + baseUrl + path mappings", () => { - // classic mode does not use directoryExists - test(/*hasDirectoryExists*/ false); - - function test(hasDirectoryExists: boolean) { - const main: File = { name: "/root/folder1/main.ts" }; - - const file1: File = { name: "/root/folder1/file1.ts" }; - const file2: File = { name: "/root/generated/folder1/file2.ts" }; - const file3: File = { name: "/folder1/file3.ts" }; // fallback to classic - const host = createModuleResolutionHost(hasDirectoryExists, file1, file2, file3); - - const options: CompilerOptions = { - moduleResolution: ModuleResolutionKind.Classic, - baseUrl: "/root", - jsx: JsxEmit.React, - paths: { - "*": [ - "*", - "generated/*" - ], - "somefolder/*": [ - "someanotherfolder/*" - ], - "/rooted/*": [ - "generated/*" - ] - } - }; - check("folder1/file1", file1, []); - check("folder1/file2", file2, [ - // first try '*' - "/root/folder1/file2.ts", - "/root/folder1/file2.tsx", - "/root/folder1/file2.d.ts", - // success when using 'generated/*' - ]); - check("/rooted/folder1/file2", file2, []); - check("folder1/file3", file3, [ - // first try '*' - "/root/folder1/file3.ts", - "/root/folder1/file3.tsx", - "/root/folder1/file3.d.ts", - // then try 'generated/*' - "/root/generated/folder1/file3.ts", - "/root/generated/folder1/file3.tsx", - "/root/generated/folder1/file3.d.ts", - // fallback to classic - "/root/folder1/folder1/file3.ts", - "/root/folder1/folder1/file3.tsx", - "/root/folder1/folder1/file3.d.ts", - "/root/folder1/file3.ts", - "/root/folder1/file3.tsx", - "/root/folder1/file3.d.ts", - ]); - - function check(name: string, expected: File, expectedFailedLookups: string[]) { - const result = resolveModuleName(name, main.name, options, host); - checkResolvedModuleWithFailedLookupLocations(result, createResolvedModule(expected.name), expectedFailedLookups); - } + } + }); + it("classic + baseUrl", () => { + test(/*hasDirectoryExists*/ false); + test(/*hasDirectoryExists*/ true); + function test(hasDirectoryExists: boolean) { + const main: File = { name: "/root/a/b/main.ts" }; + const m1: File = { name: "/root/x/m1.ts" }; // load from base url + const m2: File = { name: "/m2.ts" }; // fallback to classic + const options: CompilerOptions = { moduleResolution: ModuleResolutionKind.Classic, baseUrl: "/root/x", jsx: JsxEmit.React }; + const host = createModuleResolutionHost(hasDirectoryExists, main, m1, m2); + check("m1", main, m1); + check("m2", main, m2); + function check(name: string, caller: File, expected: File) { + const result = resolveModuleName(name, caller.name, options, host); + checkResolvedModule(result.resolvedModule, createResolvedModule(expected.name)); } - }); - - it ("node + rootDirs", () => { - test(/*hasDirectoryExists*/ false); - test(/*hasDirectoryExists*/ true); - - function test(hasDirectoryExists: boolean) { - const file1: File = { name: "/root/folder1/file1.ts" }; - const file1_1: File = { name: "/root/folder1/file1_1/index.d.ts" }; // eslint-disable-line @typescript-eslint/camelcase - const file2: File = { name: "/root/generated/folder1/file2.ts" }; - const file3: File = { name: "/root/generated/folder2/file3.ts" }; - const host = createModuleResolutionHost(hasDirectoryExists, file1, file1_1, file2, file3); - const options: CompilerOptions = { - moduleResolution: ModuleResolutionKind.NodeJs, - rootDirs: [ - "/root", - "/root/generated/" + } + }); + it("node + baseUrl + path mappings", () => { + test(/*hasDirectoryExists*/ false); + test(/*hasDirectoryExists*/ true); + function test(hasDirectoryExists: boolean) { + const main: File = { name: "/root/folder1/main.ts" }; + const file1: File = { name: "/root/folder1/file1.ts" }; + const file2: File = { name: "/root/generated/folder1/file2.ts" }; // load remapped file as module + const file3: File = { name: "/root/generated/folder2/file3/index.d.ts" }; // load folder a module + const file4Typings: File = { name: "/root/generated/folder2/file4/package.json", content: JSON.stringify({ typings: "dist/types.d.ts" }) }; + const file4: File = { name: "/root/generated/folder2/file4/dist/types.d.ts" }; // load file pointed by typings + const file5: File = { name: "/root/someanotherfolder/file5/index.d.ts" }; // load remapped module from folder + const file6: File = { name: "/root/node_modules/file6.ts" }; // fallback to node + const host = createModuleResolutionHost(hasDirectoryExists, file1, file2, file3, file4, file4Typings, file5, file6); + const options: CompilerOptions = { + moduleResolution: ModuleResolutionKind.NodeJs, + baseUrl: "/root", + jsx: JsxEmit.React, + paths: { + "*": [ + "*", + "generated/*" + ], + "somefolder/*": [ + "someanotherfolder/*" + ], + "/rooted/*": [ + "generated/*" ] - }; - check("./file2", file1, file2, [ - // first try current location - // load from file - "/root/folder1/file2.ts", - "/root/folder1/file2.tsx", - "/root/folder1/file2.d.ts", - // load from folder - "/root/folder1/file2/package.json", - "/root/folder1/file2/index.ts", - "/root/folder1/file2/index.tsx", - "/root/folder1/file2/index.d.ts", - // success after using alternative rootDir entry - ]); - check("../folder1/file1", file3, file1, [ - // first try current location - // load from file - "/root/generated/folder1/file1.ts", - "/root/generated/folder1/file1.tsx", - "/root/generated/folder1/file1.d.ts", - // load from module - "/root/generated/folder1/file1/package.json", - "/root/generated/folder1/file1/index.ts", - "/root/generated/folder1/file1/index.tsx", - "/root/generated/folder1/file1/index.d.ts", - // success after using alternative rootDir entry - ]); - check("../folder1/file1_1", file3, file1_1, [ - // first try current location - // load from file - "/root/generated/folder1/file1_1.ts", - "/root/generated/folder1/file1_1.tsx", - "/root/generated/folder1/file1_1.d.ts", - // load from folder - "/root/generated/folder1/file1_1/package.json", - "/root/generated/folder1/file1_1/index.ts", - "/root/generated/folder1/file1_1/index.tsx", - "/root/generated/folder1/file1_1/index.d.ts", - // try alternative rootDir entry - // load from file - "/root/folder1/file1_1.ts", - "/root/folder1/file1_1.tsx", - "/root/folder1/file1_1.d.ts", - // load from directory - "/root/folder1/file1_1/package.json", - "/root/folder1/file1_1/index.ts", - "/root/folder1/file1_1/index.tsx", - // success on loading '/root/folder1/file1_1/index.d.ts' - ]); - - function check(name: string, container: File, expected: File, expectedFailedLookups: string[]) { - const result = resolveModuleName(name, container.name, options, host); - checkResolvedModuleWithFailedLookupLocations(result, createResolvedModule(expected.name), expectedFailedLookups); } + }; + check("folder1/file1", file1, []); + check("folder1/file2", file2, [ + // first try the '*' + "/root/folder1/file2.ts", + "/root/folder1/file2.tsx", + "/root/folder1/file2.d.ts", + "/root/folder1/file2/package.json", + "/root/folder1/file2/index.ts", + "/root/folder1/file2/index.tsx", + "/root/folder1/file2/index.d.ts", + ]); + check("/rooted/folder1/file2", file2, []); + check("folder2/file3", file3, [ + // first try '*' + "/root/folder2/file3.ts", + "/root/folder2/file3.tsx", + "/root/folder2/file3.d.ts", + "/root/folder2/file3/package.json", + "/root/folder2/file3/index.ts", + "/root/folder2/file3/index.tsx", + "/root/folder2/file3/index.d.ts", + // then use remapped location + "/root/generated/folder2/file3.ts", + "/root/generated/folder2/file3.tsx", + "/root/generated/folder2/file3.d.ts", + "/root/generated/folder2/file3/package.json", + "/root/generated/folder2/file3/index.ts", + "/root/generated/folder2/file3/index.tsx", + ]); + check("folder2/file4", file4, [ + // first try '*' + "/root/folder2/file4.ts", + "/root/folder2/file4.tsx", + "/root/folder2/file4.d.ts", + "/root/folder2/file4/package.json", + "/root/folder2/file4/index.ts", + "/root/folder2/file4/index.tsx", + "/root/folder2/file4/index.d.ts", + // try to load from file from remapped location + "/root/generated/folder2/file4.ts", + "/root/generated/folder2/file4.tsx", + "/root/generated/folder2/file4.d.ts", + ]); + check("somefolder/file5", file5, [ + // load from remapped location + // first load from fle + "/root/someanotherfolder/file5.ts", + "/root/someanotherfolder/file5.tsx", + "/root/someanotherfolder/file5.d.ts", + // load from folder + "/root/someanotherfolder/file5/package.json", + "/root/someanotherfolder/file5/index.ts", + "/root/someanotherfolder/file5/index.tsx", + ]); + check("file6", file6, [ + // first try * + // load from file + "/root/file6.ts", + "/root/file6.tsx", + "/root/file6.d.ts", + // load from folder + "/root/file6/package.json", + "/root/file6/index.ts", + "/root/file6/index.tsx", + "/root/file6/index.d.ts", + // then try 'generated/*' + // load from file + "/root/generated/file6.ts", + "/root/generated/file6.tsx", + "/root/generated/file6.d.ts", + // load from folder + "/root/generated/file6/package.json", + "/root/generated/file6/index.ts", + "/root/generated/file6/index.tsx", + "/root/generated/file6/index.d.ts", + // fallback to standard node behavior + "/root/folder1/node_modules/file6/package.json", + // load from file + "/root/folder1/node_modules/file6.ts", + "/root/folder1/node_modules/file6.tsx", + "/root/folder1/node_modules/file6.d.ts", + // load from folder + "/root/folder1/node_modules/file6/index.ts", + "/root/folder1/node_modules/file6/index.tsx", + "/root/folder1/node_modules/file6/index.d.ts", + "/root/folder1/node_modules/@types/file6/package.json", + "/root/folder1/node_modules/@types/file6.d.ts", + "/root/folder1/node_modules/@types/file6/index.d.ts", + "/root/node_modules/file6/package.json", + ], /*isExternalLibraryImport*/ true); + function check(name: string, expected: File, expectedFailedLookups: string[], isExternalLibraryImport = false) { + const result = resolveModuleName(name, main.name, options, host); + checkResolvedModuleWithFailedLookupLocations(result, createResolvedModule(expected.name, isExternalLibraryImport), expectedFailedLookups); } - }); - - it ("classic + rootDirs", () => { - test(/*hasDirectoryExists*/ false); - - function test(hasDirectoryExists: boolean) { - const file1: File = { name: "/root/folder1/file1.ts" }; - const file2: File = { name: "/root/generated/folder1/file2.ts" }; - const file3: File = { name: "/root/generated/folder2/file3.ts" }; - const file4: File = { name: "/folder1/file1_1.ts" }; - const host = createModuleResolutionHost(hasDirectoryExists, file1, file2, file3, file4); - const options: CompilerOptions = { - moduleResolution: ModuleResolutionKind.Classic, - jsx: JsxEmit.React, - rootDirs: [ - "/root", - "/root/generated/" + } + }); + it("classic + baseUrl + path mappings", () => { + // classic mode does not use directoryExists + test(/*hasDirectoryExists*/ false); + function test(hasDirectoryExists: boolean) { + const main: File = { name: "/root/folder1/main.ts" }; + const file1: File = { name: "/root/folder1/file1.ts" }; + const file2: File = { name: "/root/generated/folder1/file2.ts" }; + const file3: File = { name: "/folder1/file3.ts" }; // fallback to classic + const host = createModuleResolutionHost(hasDirectoryExists, file1, file2, file3); + const options: CompilerOptions = { + moduleResolution: ModuleResolutionKind.Classic, + baseUrl: "/root", + jsx: JsxEmit.React, + paths: { + "*": [ + "*", + "generated/*" + ], + "somefolder/*": [ + "someanotherfolder/*" + ], + "/rooted/*": [ + "generated/*" ] - }; - check("./file2", file1, file2, [ - // first load from current location - "/root/folder1/file2.ts", - "/root/folder1/file2.tsx", - "/root/folder1/file2.d.ts", - // then try alternative rootDir entry - ]); - check("../folder1/file1", file3, file1, [ - // first load from current location - "/root/generated/folder1/file1.ts", - "/root/generated/folder1/file1.tsx", - "/root/generated/folder1/file1.d.ts", - // then try alternative rootDir entry - ]); - check("folder1/file1_1", file3, file4, [ - // current location - "/root/generated/folder2/folder1/file1_1.ts", - "/root/generated/folder2/folder1/file1_1.tsx", - "/root/generated/folder2/folder1/file1_1.d.ts", - // other entry in rootDirs - "/root/generated/folder1/file1_1.ts", - "/root/generated/folder1/file1_1.tsx", - "/root/generated/folder1/file1_1.d.ts", - // fallback - "/root/folder1/file1_1.ts", - "/root/folder1/file1_1.tsx", - "/root/folder1/file1_1.d.ts", - // found one - ]); - - function check(name: string, container: File, expected: File, expectedFailedLookups: string[]) { - const result = resolveModuleName(name, container.name, options, host); - checkResolvedModuleWithFailedLookupLocations(result, createResolvedModule(expected.name), expectedFailedLookups); } + }; + check("folder1/file1", file1, []); + check("folder1/file2", file2, [ + // first try '*' + "/root/folder1/file2.ts", + "/root/folder1/file2.tsx", + "/root/folder1/file2.d.ts", + ]); + check("/rooted/folder1/file2", file2, []); + check("folder1/file3", file3, [ + // first try '*' + "/root/folder1/file3.ts", + "/root/folder1/file3.tsx", + "/root/folder1/file3.d.ts", + // then try 'generated/*' + "/root/generated/folder1/file3.ts", + "/root/generated/folder1/file3.tsx", + "/root/generated/folder1/file3.d.ts", + // fallback to classic + "/root/folder1/folder1/file3.ts", + "/root/folder1/folder1/file3.tsx", + "/root/folder1/folder1/file3.d.ts", + "/root/folder1/file3.ts", + "/root/folder1/file3.tsx", + "/root/folder1/file3.d.ts", + ]); + function check(name: string, expected: File, expectedFailedLookups: string[]) { + const result = resolveModuleName(name, main.name, options, host); + checkResolvedModuleWithFailedLookupLocations(result, createResolvedModule(expected.name), expectedFailedLookups); } - }); - - it ("nested node module", () => { - test(/*hasDirectoryExists*/ false); - test(/*hasDirectoryExists*/ true); - - function test(hasDirectoryExists: boolean) { - const app: File = { name: "/root/src/app.ts" }; - const libsPackage: File = { name: "/root/src/libs/guid/package.json", content: JSON.stringify({ typings: "dist/guid.d.ts" }) }; - const libsTypings: File = { name: "/root/src/libs/guid/dist/guid.d.ts" }; - const host = createModuleResolutionHost(hasDirectoryExists, app, libsPackage, libsTypings); - const options: CompilerOptions = { - moduleResolution: ModuleResolutionKind.NodeJs, - baseUrl: "/root", - paths: { - "libs/guid": [ "src/libs/guid" ] - } - }; - const result = resolveModuleName("libs/guid", app.name, options, host); - checkResolvedModuleWithFailedLookupLocations(result, createResolvedModule(libsTypings.name), [ - // first try to load module as file - "/root/src/libs/guid.ts", - "/root/src/libs/guid.tsx", - "/root/src/libs/guid.d.ts", - ]); + } + }); + it("node + rootDirs", () => { + test(/*hasDirectoryExists*/ false); + test(/*hasDirectoryExists*/ true); + function test(hasDirectoryExists: boolean) { + const file1: File = { name: "/root/folder1/file1.ts" }; + const file1_1: File = { name: "/root/folder1/file1_1/index.d.ts" }; // eslint-disable-line @typescript-eslint/camelcase + const file2: File = { name: "/root/generated/folder1/file2.ts" }; + const file3: File = { name: "/root/generated/folder2/file3.ts" }; + const host = createModuleResolutionHost(hasDirectoryExists, file1, file1_1, file2, file3); + const options: CompilerOptions = { + moduleResolution: ModuleResolutionKind.NodeJs, + rootDirs: [ + "/root", + "/root/generated/" + ] + }; + check("./file2", file1, file2, [ + // first try current location + // load from file + "/root/folder1/file2.ts", + "/root/folder1/file2.tsx", + "/root/folder1/file2.d.ts", + // load from folder + "/root/folder1/file2/package.json", + "/root/folder1/file2/index.ts", + "/root/folder1/file2/index.tsx", + "/root/folder1/file2/index.d.ts", + ]); + check("../folder1/file1", file3, file1, [ + // first try current location + // load from file + "/root/generated/folder1/file1.ts", + "/root/generated/folder1/file1.tsx", + "/root/generated/folder1/file1.d.ts", + // load from module + "/root/generated/folder1/file1/package.json", + "/root/generated/folder1/file1/index.ts", + "/root/generated/folder1/file1/index.tsx", + "/root/generated/folder1/file1/index.d.ts", + ]); + check("../folder1/file1_1", file3, file1_1, [ + // first try current location + // load from file + "/root/generated/folder1/file1_1.ts", + "/root/generated/folder1/file1_1.tsx", + "/root/generated/folder1/file1_1.d.ts", + // load from folder + "/root/generated/folder1/file1_1/package.json", + "/root/generated/folder1/file1_1/index.ts", + "/root/generated/folder1/file1_1/index.tsx", + "/root/generated/folder1/file1_1/index.d.ts", + // try alternative rootDir entry + // load from file + "/root/folder1/file1_1.ts", + "/root/folder1/file1_1.tsx", + "/root/folder1/file1_1.d.ts", + // load from directory + "/root/folder1/file1_1/package.json", + "/root/folder1/file1_1/index.ts", + "/root/folder1/file1_1/index.tsx", + ]); + function check(name: string, container: File, expected: File, expectedFailedLookups: string[]) { + const result = resolveModuleName(name, container.name, options, host); + checkResolvedModuleWithFailedLookupLocations(result, createResolvedModule(expected.name), expectedFailedLookups); } - }); + } }); - - describe("unittests:: moduleResolution:: ModuleResolutionHost.directoryExists", () => { - it("No 'fileExists' calls if containing directory is missing", () => { - const host: ModuleResolutionHost = { - readFile: notImplemented, - fileExists: notImplemented, - directoryExists: _ => false + it("classic + rootDirs", () => { + test(/*hasDirectoryExists*/ false); + function test(hasDirectoryExists: boolean) { + const file1: File = { name: "/root/folder1/file1.ts" }; + const file2: File = { name: "/root/generated/folder1/file2.ts" }; + const file3: File = { name: "/root/generated/folder2/file3.ts" }; + const file4: File = { name: "/folder1/file1_1.ts" }; + const host = createModuleResolutionHost(hasDirectoryExists, file1, file2, file3, file4); + const options: CompilerOptions = { + moduleResolution: ModuleResolutionKind.Classic, + jsx: JsxEmit.React, + rootDirs: [ + "/root", + "/root/generated/" + ] }; - - const result = resolveModuleName("someName", "/a/b/c/d", { moduleResolution: ModuleResolutionKind.NodeJs }, host); - assert(!result.resolvedModule); - }); + check("./file2", file1, file2, [ + // first load from current location + "/root/folder1/file2.ts", + "/root/folder1/file2.tsx", + "/root/folder1/file2.d.ts", + ]); + check("../folder1/file1", file3, file1, [ + // first load from current location + "/root/generated/folder1/file1.ts", + "/root/generated/folder1/file1.tsx", + "/root/generated/folder1/file1.d.ts", + ]); + check("folder1/file1_1", file3, file4, [ + // current location + "/root/generated/folder2/folder1/file1_1.ts", + "/root/generated/folder2/folder1/file1_1.tsx", + "/root/generated/folder2/folder1/file1_1.d.ts", + // other entry in rootDirs + "/root/generated/folder1/file1_1.ts", + "/root/generated/folder1/file1_1.tsx", + "/root/generated/folder1/file1_1.d.ts", + // fallback + "/root/folder1/file1_1.ts", + "/root/folder1/file1_1.tsx", + "/root/folder1/file1_1.d.ts", + ]); + function check(name: string, container: File, expected: File, expectedFailedLookups: string[]) { + const result = resolveModuleName(name, container.name, options, host); + checkResolvedModuleWithFailedLookupLocations(result, createResolvedModule(expected.name), expectedFailedLookups); + } + } }); - - describe("unittests:: moduleResolution:: Type reference directive resolution: ", () => { - function testWorker(hasDirectoryExists: boolean, typesRoot: string | undefined, typeDirective: string, primary: boolean, initialFile: File, targetFile: File, ...otherFiles: File[]) { - const host = createModuleResolutionHost(hasDirectoryExists, ...[initialFile, targetFile].concat(...otherFiles)); - const result = resolveTypeReferenceDirective(typeDirective, initialFile.name, typesRoot ? { typeRoots: [typesRoot] } : {}, host); - assert(result.resolvedTypeReferenceDirective!.resolvedFileName !== undefined, "expected type directive to be resolved"); - assert.equal(result.resolvedTypeReferenceDirective!.resolvedFileName, targetFile.name, "unexpected result of type reference resolution"); - assert.equal(result.resolvedTypeReferenceDirective!.primary, primary, "unexpected 'primary' value"); + it("nested node module", () => { + test(/*hasDirectoryExists*/ false); + test(/*hasDirectoryExists*/ true); + function test(hasDirectoryExists: boolean) { + const app: File = { name: "/root/src/app.ts" }; + const libsPackage: File = { name: "/root/src/libs/guid/package.json", content: JSON.stringify({ typings: "dist/guid.d.ts" }) }; + const libsTypings: File = { name: "/root/src/libs/guid/dist/guid.d.ts" }; + const host = createModuleResolutionHost(hasDirectoryExists, app, libsPackage, libsTypings); + const options: CompilerOptions = { + moduleResolution: ModuleResolutionKind.NodeJs, + baseUrl: "/root", + paths: { + "libs/guid": ["src/libs/guid"] + } + }; + const result = resolveModuleName("libs/guid", app.name, options, host); + checkResolvedModuleWithFailedLookupLocations(result, createResolvedModule(libsTypings.name), [ + // first try to load module as file + "/root/src/libs/guid.ts", + "/root/src/libs/guid.tsx", + "/root/src/libs/guid.d.ts", + ]); } - - function test(typesRoot: string, typeDirective: string, primary: boolean, initialFile: File, targetFile: File, ...otherFiles: File[]) { - testWorker(/*hasDirectoryExists*/ false, typesRoot, typeDirective, primary, initialFile, targetFile, ...otherFiles); + }); +}); +describe("unittests:: moduleResolution:: ModuleResolutionHost.directoryExists", () => { + it("No 'fileExists' calls if containing directory is missing", () => { + const host: ModuleResolutionHost = { + readFile: notImplemented, + fileExists: notImplemented, + directoryExists: _ => false + }; + const result = resolveModuleName("someName", "/a/b/c/d", { moduleResolution: ModuleResolutionKind.NodeJs }, host); + assert(!result.resolvedModule); + }); +}); +describe("unittests:: moduleResolution:: Type reference directive resolution: ", () => { + function testWorker(hasDirectoryExists: boolean, typesRoot: string | undefined, typeDirective: string, primary: boolean, initialFile: File, targetFile: File, ...otherFiles: File[]) { + const host = createModuleResolutionHost(hasDirectoryExists, ...[initialFile, targetFile].concat(...otherFiles)); + const result = resolveTypeReferenceDirective(typeDirective, initialFile.name, typesRoot ? { typeRoots: [typesRoot] } : {}, host); + assert(result.resolvedTypeReferenceDirective!.resolvedFileName !== undefined, "expected type directive to be resolved"); + assert.equal(result.resolvedTypeReferenceDirective!.resolvedFileName, targetFile.name, "unexpected result of type reference resolution"); + assert.equal(result.resolvedTypeReferenceDirective!.primary, primary, "unexpected 'primary' value"); + } + function test(typesRoot: string, typeDirective: string, primary: boolean, initialFile: File, targetFile: File, ...otherFiles: File[]) { + testWorker(/*hasDirectoryExists*/ false, typesRoot, typeDirective, primary, initialFile, targetFile, ...otherFiles); + } + it("Can be resolved from primary location", () => { + { + const f1 = { name: "/root/src/app.ts" }; + const f2 = { name: "/root/src/types/lib/index.d.ts" }; + test(/*typesRoot*/ "/root/src/types", /* typeDirective */ "lib", /*primary*/ true, f1, f2); } - - it("Can be resolved from primary location", () => { - { - const f1 = { name: "/root/src/app.ts" }; - const f2 = { name: "/root/src/types/lib/index.d.ts" }; - test(/*typesRoot*/"/root/src/types", /* typeDirective */"lib", /*primary*/ true, f1, f2); - } - { - const f1 = { name: "/root/src/app.ts" }; - const f2 = { name: "/root/src/types/lib/typings/lib.d.ts" }; - const packageFile = { name: "/root/src/types/lib/package.json", content: JSON.stringify({ types: "typings/lib.d.ts" }) }; - test(/*typesRoot*/"/root/src/types", /* typeDirective */"lib", /*primary*/ true, f1, f2, packageFile); - } - { - const f1 = { name: "/root/src/app.ts" }; - const f2 = { name: "/root/src/node_modules/lib/index.d.ts" }; - test(/*typesRoot*/"/root/src/types", /* typeDirective */"lib", /*primary*/ false, f1, f2); - } - { - const f1 = { name: "/root/src/app.ts" }; - const f2 = { name: "/root/src/node_modules/lib/typings/lib.d.ts" }; - const packageFile = { name: "/root/src/node_modules/lib/package.json", content: JSON.stringify({ types: "typings/lib.d.ts" }) }; - test(/*typesRoot*/"/root/src/types", /* typeDirective */"lib", /*primary*/ false, f1, f2, packageFile); - } - { - const f1 = { name: "/root/src/app.ts" }; - const f2 = { name: "/root/src/node_modules/@types/lib/index.d.ts" }; - test(/*typesRoot*/"/root/src/types", /* typeDirective */"lib", /*primary*/ false, f1, f2); - } - { - const f1 = { name: "/root/src/app.ts" }; - const f2 = { name: "/root/src/node_modules/@types/lib/typings/lib.d.ts" }; - const packageFile = { name: "/root/src/node_modules/@types/lib/package.json", content: JSON.stringify({ types: "typings/lib.d.ts" }) }; - test(/*typesRoot*/"/root/src/types", /* typeDirective */"lib", /*primary*/ false, f1, f2, packageFile); - } - }); - it("Can be resolved from secondary location", () => { - { - const f1 = { name: "/root/src/app.ts" }; - const f2 = { name: "/root/node_modules/lib.d.ts" }; - test(/*typesRoot*/"/root/src/types", /* typeDirective */"lib", /*primary*/ false, f1, f2); - } - { - const f1 = { name: "/root/src/app.ts" }; - const f2 = { name: "/root/node_modules/lib/index.d.ts" }; - test(/*typesRoot*/"/root/src/types", /* typeDirective */"lib", /*primary*/ false, f1, f2); - } - { - const f1 = { name: "/root/src/app.ts" }; - const f2 = { name: "/root/node_modules/lib/typings/lib.d.ts" }; - const packageFile = { name: "/root/node_modules/lib/package.json", content: JSON.stringify({ typings: "typings/lib.d.ts" }) }; - test(/*typesRoot*/"/root/src/types", /* typeDirective */"lib", /*primary*/ false, f1, f2, packageFile); - } - { - const f1 = { name: "/root/src/app.ts" }; - const f2 = { name: "/root/node_modules/@types/lib/index.d.ts" }; - test(/*typesRoot*/"/root/src/types", /* typeDirective */"lib", /*primary*/ false, f1, f2); - } - { - const f1 = { name: "/root/src/app.ts" }; - const f2 = { name: "/root/node_modules/@types/lib/typings/lib.d.ts" }; - const packageFile = { name: "/root/node_modules/@types/lib/package.json", content: JSON.stringify({ typings: "typings/lib.d.ts" }) }; - test(/*typesRoot*/"/root/src/types", /* typeDirective */"lib", /*primary*/ false, f1, f2, packageFile); - } - }); - it("Primary resolution overrides secondary resolutions", () => { - { - const f1 = { name: "/root/src/a/b/c/app.ts" }; - const f2 = { name: "/root/src/types/lib/index.d.ts" }; - const f3 = { name: "/root/src/a/b/node_modules/lib.d.ts" }; - test(/*typesRoot*/"/root/src/types", /* typeDirective */"lib", /*primary*/ true, f1, f2, f3); - } - }); - it("Reused program keeps errors", () => { - const f1 = { name: "/root/src/a/b/c/d/e/app.ts", content: `/// ` }; - const f2 = { name: "/root/src/a/b/c/d/node_modules/lib/index.d.ts", content: `declare var x: number;` }; - const f3 = { name: "/root/src/a/b/c/d/f/g/app.ts", content: `/// ` }; - const f4 = { name: "/root/src/a/b/c/d/f/node_modules/lib/index.d.ts", content: `declare var x: number;` }; - const files = [f1, f2, f3, f4]; - - const names = map(files, f => f.name); - const sourceFiles = arrayToMap(map(files, f => createSourceFile(f.name, f.content, ScriptTarget.ES2015)), f => f.fileName); - const compilerHost: CompilerHost = { - fileExists: fileName => sourceFiles.has(fileName), - getSourceFile: fileName => sourceFiles.get(fileName), - getDefaultLibFileName: () => "lib.d.ts", - writeFile: notImplemented, - getCurrentDirectory: () => "/", - getDirectories: () => [], - getCanonicalFileName: f => f.toLowerCase(), - getNewLine: () => "\r\n", - useCaseSensitiveFileNames: () => false, - readFile: fileName => { - const file = sourceFiles.get(fileName); - return file && file.text; - }, - }; - const program1 = createProgram(names, {}, compilerHost); - const diagnostics1 = program1.getFileProcessingDiagnostics().getDiagnostics(); - assert.equal(diagnostics1.length, 1, "expected one diagnostic"); - - createProgram(names, {}, compilerHost, program1); - assert.isTrue(program1.structureIsReused === StructureIsReused.Completely); - const diagnostics2 = program1.getFileProcessingDiagnostics().getDiagnostics(); - assert.equal(diagnostics2.length, 1, "expected one diagnostic"); - assert.equal(diagnostics1[0].messageText, diagnostics2[0].messageText, "expected one diagnostic"); - }); - - it("Modules in the same .d.ts file are preferred to external files", () => { - const f = { - name: "/a/b/c/c/app.d.ts", - content: ` + { + const f1 = { name: "/root/src/app.ts" }; + const f2 = { name: "/root/src/types/lib/typings/lib.d.ts" }; + const packageFile = { name: "/root/src/types/lib/package.json", content: JSON.stringify({ types: "typings/lib.d.ts" }) }; + test(/*typesRoot*/ "/root/src/types", /* typeDirective */ "lib", /*primary*/ true, f1, f2, packageFile); + } + { + const f1 = { name: "/root/src/app.ts" }; + const f2 = { name: "/root/src/node_modules/lib/index.d.ts" }; + test(/*typesRoot*/ "/root/src/types", /* typeDirective */ "lib", /*primary*/ false, f1, f2); + } + { + const f1 = { name: "/root/src/app.ts" }; + const f2 = { name: "/root/src/node_modules/lib/typings/lib.d.ts" }; + const packageFile = { name: "/root/src/node_modules/lib/package.json", content: JSON.stringify({ types: "typings/lib.d.ts" }) }; + test(/*typesRoot*/ "/root/src/types", /* typeDirective */ "lib", /*primary*/ false, f1, f2, packageFile); + } + { + const f1 = { name: "/root/src/app.ts" }; + const f2 = { name: "/root/src/node_modules/@types/lib/index.d.ts" }; + test(/*typesRoot*/ "/root/src/types", /* typeDirective */ "lib", /*primary*/ false, f1, f2); + } + { + const f1 = { name: "/root/src/app.ts" }; + const f2 = { name: "/root/src/node_modules/@types/lib/typings/lib.d.ts" }; + const packageFile = { name: "/root/src/node_modules/@types/lib/package.json", content: JSON.stringify({ types: "typings/lib.d.ts" }) }; + test(/*typesRoot*/ "/root/src/types", /* typeDirective */ "lib", /*primary*/ false, f1, f2, packageFile); + } + }); + it("Can be resolved from secondary location", () => { + { + const f1 = { name: "/root/src/app.ts" }; + const f2 = { name: "/root/node_modules/lib.d.ts" }; + test(/*typesRoot*/ "/root/src/types", /* typeDirective */ "lib", /*primary*/ false, f1, f2); + } + { + const f1 = { name: "/root/src/app.ts" }; + const f2 = { name: "/root/node_modules/lib/index.d.ts" }; + test(/*typesRoot*/ "/root/src/types", /* typeDirective */ "lib", /*primary*/ false, f1, f2); + } + { + const f1 = { name: "/root/src/app.ts" }; + const f2 = { name: "/root/node_modules/lib/typings/lib.d.ts" }; + const packageFile = { name: "/root/node_modules/lib/package.json", content: JSON.stringify({ typings: "typings/lib.d.ts" }) }; + test(/*typesRoot*/ "/root/src/types", /* typeDirective */ "lib", /*primary*/ false, f1, f2, packageFile); + } + { + const f1 = { name: "/root/src/app.ts" }; + const f2 = { name: "/root/node_modules/@types/lib/index.d.ts" }; + test(/*typesRoot*/ "/root/src/types", /* typeDirective */ "lib", /*primary*/ false, f1, f2); + } + { + const f1 = { name: "/root/src/app.ts" }; + const f2 = { name: "/root/node_modules/@types/lib/typings/lib.d.ts" }; + const packageFile = { name: "/root/node_modules/@types/lib/package.json", content: JSON.stringify({ typings: "typings/lib.d.ts" }) }; + test(/*typesRoot*/ "/root/src/types", /* typeDirective */ "lib", /*primary*/ false, f1, f2, packageFile); + } + }); + it("Primary resolution overrides secondary resolutions", () => { + { + const f1 = { name: "/root/src/a/b/c/app.ts" }; + const f2 = { name: "/root/src/types/lib/index.d.ts" }; + const f3 = { name: "/root/src/a/b/node_modules/lib.d.ts" }; + test(/*typesRoot*/ "/root/src/types", /* typeDirective */ "lib", /*primary*/ true, f1, f2, f3); + } + }); + it("Reused program keeps errors", () => { + const f1 = { name: "/root/src/a/b/c/d/e/app.ts", content: `/// ` }; + const f2 = { name: "/root/src/a/b/c/d/node_modules/lib/index.d.ts", content: `declare var x: number;` }; + const f3 = { name: "/root/src/a/b/c/d/f/g/app.ts", content: `/// ` }; + const f4 = { name: "/root/src/a/b/c/d/f/node_modules/lib/index.d.ts", content: `declare var x: number;` }; + const files = [f1, f2, f3, f4]; + const names = map(files, f => f.name); + const sourceFiles = arrayToMap(map(files, f => createSourceFile(f.name, f.content, ScriptTarget.ES2015)), f => f.fileName); + const compilerHost: CompilerHost = { + fileExists: fileName => sourceFiles.has(fileName), + getSourceFile: fileName => sourceFiles.get(fileName), + getDefaultLibFileName: () => "lib.d.ts", + writeFile: notImplemented, + getCurrentDirectory: () => "/", + getDirectories: () => [], + getCanonicalFileName: f => f.toLowerCase(), + getNewLine: () => "\r\n", + useCaseSensitiveFileNames: () => false, + readFile: fileName => { + const file = sourceFiles.get(fileName); + return file && file.text; + }, + }; + const program1 = createProgram(names, {}, compilerHost); + const diagnostics1 = program1.getFileProcessingDiagnostics().getDiagnostics(); + assert.equal(diagnostics1.length, 1, "expected one diagnostic"); + createProgram(names, {}, compilerHost, program1); + assert.isTrue(program1.structureIsReused === StructureIsReused.Completely); + const diagnostics2 = program1.getFileProcessingDiagnostics().getDiagnostics(); + assert.equal(diagnostics2.length, 1, "expected one diagnostic"); + assert.equal(diagnostics1[0].messageText, diagnostics2[0].messageText, "expected one diagnostic"); + }); + it("Modules in the same .d.ts file are preferred to external files", () => { + const f = { + name: "/a/b/c/c/app.d.ts", + content: ` declare module "fs" { export interface Stat { id: number } } @@ -1398,28 +1128,27 @@ import b = require("./moduleB"); import { Stat } from "fs"; export function foo(): Stat; }` - }; - const file = createSourceFile(f.name, f.content, ScriptTarget.ES2015); - const compilerHost: CompilerHost = { - fileExists: fileName => fileName === file.fileName, - getSourceFile: fileName => fileName === file.fileName ? file : undefined, - getDefaultLibFileName: () => "lib.d.ts", - writeFile: notImplemented, - getCurrentDirectory: () => "/", - getDirectories: () => [], - getCanonicalFileName: f => f.toLowerCase(), - getNewLine: () => "\r\n", - useCaseSensitiveFileNames: () => false, - readFile: fileName => fileName === file.fileName ? file.text : undefined, - resolveModuleNames: notImplemented, - }; - createProgram([f.name], {}, compilerHost); - }); - - it("Modules in .ts file are not checked in the same file", () => { - const f = { - name: "/a/b/c/c/app.ts", - content: ` + }; + const file = createSourceFile(f.name, f.content, ScriptTarget.ES2015); + const compilerHost: CompilerHost = { + fileExists: fileName => fileName === file.fileName, + getSourceFile: fileName => fileName === file.fileName ? file : undefined, + getDefaultLibFileName: () => "lib.d.ts", + writeFile: notImplemented, + getCurrentDirectory: () => "/", + getDirectories: () => [], + getCanonicalFileName: f => f.toLowerCase(), + getNewLine: () => "\r\n", + useCaseSensitiveFileNames: () => false, + readFile: fileName => fileName === file.fileName ? file.text : undefined, + resolveModuleNames: notImplemented, + }; + createProgram([f.name], {}, compilerHost); + }); + it("Modules in .ts file are not checked in the same file", () => { + const f = { + name: "/a/b/c/c/app.ts", + content: ` declare module "fs" { export interface Stat { id: number } } @@ -1427,35 +1156,34 @@ import b = require("./moduleB"); import { Stat } from "fs"; export function foo(): Stat; }` - }; - const file = createSourceFile(f.name, f.content, ScriptTarget.ES2015); - const compilerHost: CompilerHost = { - fileExists: fileName => fileName === file.fileName, - getSourceFile: fileName => fileName === file.fileName ? file : undefined, - getDefaultLibFileName: () => "lib.d.ts", - writeFile: notImplemented, - getCurrentDirectory: () => "/", - getDirectories: () => [], - getCanonicalFileName: f => f.toLowerCase(), - getNewLine: () => "\r\n", - useCaseSensitiveFileNames: () => false, - readFile: fileName => fileName === file.fileName ? file.text : undefined, - resolveModuleNames(moduleNames: string[], _containingFile: string) { - assert.deepEqual(moduleNames, ["fs"]); - return [undefined!]; // TODO: GH#18217 - } - }; - createProgram([f.name], {}, compilerHost); + }; + const file = createSourceFile(f.name, f.content, ScriptTarget.ES2015); + const compilerHost: CompilerHost = { + fileExists: fileName => fileName === file.fileName, + getSourceFile: fileName => fileName === file.fileName ? file : undefined, + getDefaultLibFileName: () => "lib.d.ts", + writeFile: notImplemented, + getCurrentDirectory: () => "/", + getDirectories: () => [], + getCanonicalFileName: f => f.toLowerCase(), + getNewLine: () => "\r\n", + useCaseSensitiveFileNames: () => false, + readFile: fileName => fileName === file.fileName ? file.text : undefined, + resolveModuleNames(moduleNames: string[], _containingFile: string) { + assert.deepEqual(moduleNames, ["fs"]); + return [undefined!]; // TODO: GH#18217 + } + }; + createProgram([f.name], {}, compilerHost); + }); + describe("can be resolved when typeReferenceDirective is relative and in a sibling folder", () => { + const initialFile = { name: "/root/src/background/app.ts" }; + const targetFile = { name: "/root/src/typedefs/filesystem.d.ts" }; + it("when host doesnt have directoryExists", () => { + testWorker(/*hasDirectoryExists*/ false, /*typesRoot*/ undefined, /*typeDirective*/ "../typedefs/filesystem", /*primary*/ false, initialFile, targetFile); }); - describe("can be resolved when typeReferenceDirective is relative and in a sibling folder", () => { - const initialFile = { name: "/root/src/background/app.ts" }; - const targetFile = { name: "/root/src/typedefs/filesystem.d.ts" }; - it("when host doesnt have directoryExists", () => { - testWorker(/*hasDirectoryExists*/ false, /*typesRoot*/ undefined, /*typeDirective*/ "../typedefs/filesystem", /*primary*/ false, initialFile, targetFile); - }); - it("when host has directoryExists", () => { - testWorker(/*hasDirectoryExists*/ true, /*typesRoot*/ undefined, /*typeDirective*/ "../typedefs/filesystem", /*primary*/ false, initialFile, targetFile); - }); + it("when host has directoryExists", () => { + testWorker(/*hasDirectoryExists*/ true, /*typesRoot*/ undefined, /*typeDirective*/ "../typedefs/filesystem", /*primary*/ false, initialFile, targetFile); }); }); -} +}); diff --git a/src/testRunner/unittests/parsePseudoBigInt.ts b/src/testRunner/unittests/parsePseudoBigInt.ts index db1a841dc2b74..0bb2616c36eeb 100644 --- a/src/testRunner/unittests/parsePseudoBigInt.ts +++ b/src/testRunner/unittests/parsePseudoBigInt.ts @@ -1,71 +1,56 @@ -namespace ts { - describe("unittests:: BigInt literal base conversions", () => { - describe("parsePseudoBigInt", () => { - const testNumbers: number[] = []; - for (let i = 0; i < 1e3; i++) testNumbers.push(i); - for (let bits = 0; bits <= 52; bits++) { - testNumbers.push(2 ** bits, 2 ** bits - 1); - } - it("can strip base-10 strings", () => { - for (const testNumber of testNumbers) { - for (let leadingZeros = 0; leadingZeros < 10; leadingZeros++) { - assert.equal( - parsePseudoBigInt("0".repeat(leadingZeros) + testNumber + "n"), - String(testNumber) - ); - } +import { parsePseudoBigInt } from "../ts"; +describe("unittests:: BigInt literal base conversions", () => { + describe("parsePseudoBigInt", () => { + const testNumbers: number[] = []; + for (let i = 0; i < 1e3; i++) + testNumbers.push(i); + for (let bits = 0; bits <= 52; bits++) { + testNumbers.push(2 ** bits, 2 ** bits - 1); + } + it("can strip base-10 strings", () => { + for (const testNumber of testNumbers) { + for (let leadingZeros = 0; leadingZeros < 10; leadingZeros++) { + assert.equal(parsePseudoBigInt("0".repeat(leadingZeros) + testNumber + "n"), String(testNumber)); } - }); - it("can parse binary literals", () => { - for (const testNumber of testNumbers) { - for (let leadingZeros = 0; leadingZeros < 10; leadingZeros++) { - const binary = "0".repeat(leadingZeros) + testNumber.toString(2) + "n"; - for (const prefix of ["0b", "0B"]) { - assert.equal(parsePseudoBigInt(prefix + binary), String(testNumber)); - } + } + }); + it("can parse binary literals", () => { + for (const testNumber of testNumbers) { + for (let leadingZeros = 0; leadingZeros < 10; leadingZeros++) { + const binary = "0".repeat(leadingZeros) + testNumber.toString(2) + "n"; + for (const prefix of ["0b", "0B"]) { + assert.equal(parsePseudoBigInt(prefix + binary), String(testNumber)); } } - }); - it("can parse octal literals", () => { - for (const testNumber of testNumbers) { - for (let leadingZeros = 0; leadingZeros < 10; leadingZeros++) { - const octal = "0".repeat(leadingZeros) + testNumber.toString(8) + "n"; - for (const prefix of ["0o", "0O"]) { - assert.equal(parsePseudoBigInt(prefix + octal), String(testNumber)); - } + } + }); + it("can parse octal literals", () => { + for (const testNumber of testNumbers) { + for (let leadingZeros = 0; leadingZeros < 10; leadingZeros++) { + const octal = "0".repeat(leadingZeros) + testNumber.toString(8) + "n"; + for (const prefix of ["0o", "0O"]) { + assert.equal(parsePseudoBigInt(prefix + octal), String(testNumber)); } } - }); - it("can parse hex literals", () => { - for (const testNumber of testNumbers) { - for (let leadingZeros = 0; leadingZeros < 10; leadingZeros++) { - const hex = "0".repeat(leadingZeros) + testNumber.toString(16) + "n"; - for (const prefix of ["0x", "0X"]) { - for (const hexCase of [hex.toLowerCase(), hex.toUpperCase()]) { - assert.equal(parsePseudoBigInt(prefix + hexCase), String(testNumber)); - } + } + }); + it("can parse hex literals", () => { + for (const testNumber of testNumbers) { + for (let leadingZeros = 0; leadingZeros < 10; leadingZeros++) { + const hex = "0".repeat(leadingZeros) + testNumber.toString(16) + "n"; + for (const prefix of ["0x", "0X"]) { + for (const hexCase of [hex.toLowerCase(), hex.toUpperCase()]) { + assert.equal(parsePseudoBigInt(prefix + hexCase), String(testNumber)); } } } - }); - it("can parse large literals", () => { - assert.equal( - parsePseudoBigInt("123456789012345678901234567890n"), - "123456789012345678901234567890" - ); - assert.equal( - parsePseudoBigInt("0b1100011101110100100001111111101101100001101110011111000001110111001001110001111110000101011010010n"), - "123456789012345678901234567890" - ); - assert.equal( - parsePseudoBigInt("0o143564417755415637016711617605322n"), - "123456789012345678901234567890" - ); - assert.equal( - parsePseudoBigInt("0x18ee90ff6c373e0ee4e3f0ad2n"), - "123456789012345678901234567890" - ); - }); + } + }); + it("can parse large literals", () => { + assert.equal(parsePseudoBigInt("123456789012345678901234567890n"), "123456789012345678901234567890"); + assert.equal(parsePseudoBigInt("0b1100011101110100100001111111101101100001101110011111000001110111001001110001111110000101011010010n"), "123456789012345678901234567890"); + assert.equal(parsePseudoBigInt("0o143564417755415637016711617605322n"), "123456789012345678901234567890"); + assert.equal(parsePseudoBigInt("0x18ee90ff6c373e0ee4e3f0ad2n"), "123456789012345678901234567890"); }); }); -} +}); diff --git a/src/testRunner/unittests/paths.ts b/src/testRunner/unittests/paths.ts index cf2bde3acbee2..ad22e3b76dfaf 100644 --- a/src/testRunner/unittests/paths.ts +++ b/src/testRunner/unittests/paths.ts @@ -1,310 +1,299 @@ +import { normalizeSlashes, getRootLength, isUrl, isRootedDiskPath, getDirectoryPath, getBaseFileName, getAnyExtensionFromPath, getPathComponents, reducePathComponents, combinePaths, resolvePath, getRelativePathFromDirectory, toFileNameLowerCase } from "../ts"; describe("unittests:: core paths", () => { it("normalizeSlashes", () => { - assert.strictEqual(ts.normalizeSlashes("a"), "a"); - assert.strictEqual(ts.normalizeSlashes("a/b"), "a/b"); - assert.strictEqual(ts.normalizeSlashes("a\\b"), "a/b"); - assert.strictEqual(ts.normalizeSlashes("\\\\server\\path"), "//server/path"); + assert.strictEqual(normalizeSlashes("a"), "a"); + assert.strictEqual(normalizeSlashes("a/b"), "a/b"); + assert.strictEqual(normalizeSlashes("a\\b"), "a/b"); + assert.strictEqual(normalizeSlashes("\\\\server\\path"), "//server/path"); }); it("getRootLength", () => { - assert.strictEqual(ts.getRootLength("a"), 0); - assert.strictEqual(ts.getRootLength("/"), 1); - assert.strictEqual(ts.getRootLength("/path"), 1); - assert.strictEqual(ts.getRootLength("c:"), 2); - assert.strictEqual(ts.getRootLength("c:d"), 0); - assert.strictEqual(ts.getRootLength("c:/"), 3); - assert.strictEqual(ts.getRootLength("c:\\"), 3); - assert.strictEqual(ts.getRootLength("//server"), 8); - assert.strictEqual(ts.getRootLength("//server/share"), 9); - assert.strictEqual(ts.getRootLength("\\\\server"), 8); - assert.strictEqual(ts.getRootLength("\\\\server\\share"), 9); - assert.strictEqual(ts.getRootLength("file:///"), 8); - assert.strictEqual(ts.getRootLength("file:///path"), 8); - assert.strictEqual(ts.getRootLength("file:///c:"), 10); - assert.strictEqual(ts.getRootLength("file:///c:d"), 8); - assert.strictEqual(ts.getRootLength("file:///c:/path"), 11); - assert.strictEqual(ts.getRootLength("file:///c%3a"), 12); - assert.strictEqual(ts.getRootLength("file:///c%3ad"), 8); - assert.strictEqual(ts.getRootLength("file:///c%3a/path"), 13); - assert.strictEqual(ts.getRootLength("file:///c%3A"), 12); - assert.strictEqual(ts.getRootLength("file:///c%3Ad"), 8); - assert.strictEqual(ts.getRootLength("file:///c%3A/path"), 13); - assert.strictEqual(ts.getRootLength("file://localhost"), 16); - assert.strictEqual(ts.getRootLength("file://localhost/"), 17); - assert.strictEqual(ts.getRootLength("file://localhost/path"), 17); - assert.strictEqual(ts.getRootLength("file://localhost/c:"), 19); - assert.strictEqual(ts.getRootLength("file://localhost/c:d"), 17); - assert.strictEqual(ts.getRootLength("file://localhost/c:/path"), 20); - assert.strictEqual(ts.getRootLength("file://localhost/c%3a"), 21); - assert.strictEqual(ts.getRootLength("file://localhost/c%3ad"), 17); - assert.strictEqual(ts.getRootLength("file://localhost/c%3a/path"), 22); - assert.strictEqual(ts.getRootLength("file://localhost/c%3A"), 21); - assert.strictEqual(ts.getRootLength("file://localhost/c%3Ad"), 17); - assert.strictEqual(ts.getRootLength("file://localhost/c%3A/path"), 22); - assert.strictEqual(ts.getRootLength("file://server"), 13); - assert.strictEqual(ts.getRootLength("file://server/"), 14); - assert.strictEqual(ts.getRootLength("file://server/path"), 14); - assert.strictEqual(ts.getRootLength("file://server/c:"), 14); - assert.strictEqual(ts.getRootLength("file://server/c:d"), 14); - assert.strictEqual(ts.getRootLength("file://server/c:/d"), 14); - assert.strictEqual(ts.getRootLength("file://server/c%3a"), 14); - assert.strictEqual(ts.getRootLength("file://server/c%3ad"), 14); - assert.strictEqual(ts.getRootLength("file://server/c%3a/d"), 14); - assert.strictEqual(ts.getRootLength("file://server/c%3A"), 14); - assert.strictEqual(ts.getRootLength("file://server/c%3Ad"), 14); - assert.strictEqual(ts.getRootLength("file://server/c%3A/d"), 14); - assert.strictEqual(ts.getRootLength("http://server"), 13); - assert.strictEqual(ts.getRootLength("http://server/path"), 14); + assert.strictEqual(getRootLength("a"), 0); + assert.strictEqual(getRootLength("/"), 1); + assert.strictEqual(getRootLength("/path"), 1); + assert.strictEqual(getRootLength("c:"), 2); + assert.strictEqual(getRootLength("c:d"), 0); + assert.strictEqual(getRootLength("c:/"), 3); + assert.strictEqual(getRootLength("c:\\"), 3); + assert.strictEqual(getRootLength("//server"), 8); + assert.strictEqual(getRootLength("//server/share"), 9); + assert.strictEqual(getRootLength("\\\\server"), 8); + assert.strictEqual(getRootLength("\\\\server\\share"), 9); + assert.strictEqual(getRootLength("file:///"), 8); + assert.strictEqual(getRootLength("file:///path"), 8); + assert.strictEqual(getRootLength("file:///c:"), 10); + assert.strictEqual(getRootLength("file:///c:d"), 8); + assert.strictEqual(getRootLength("file:///c:/path"), 11); + assert.strictEqual(getRootLength("file:///c%3a"), 12); + assert.strictEqual(getRootLength("file:///c%3ad"), 8); + assert.strictEqual(getRootLength("file:///c%3a/path"), 13); + assert.strictEqual(getRootLength("file:///c%3A"), 12); + assert.strictEqual(getRootLength("file:///c%3Ad"), 8); + assert.strictEqual(getRootLength("file:///c%3A/path"), 13); + assert.strictEqual(getRootLength("file://localhost"), 16); + assert.strictEqual(getRootLength("file://localhost/"), 17); + assert.strictEqual(getRootLength("file://localhost/path"), 17); + assert.strictEqual(getRootLength("file://localhost/c:"), 19); + assert.strictEqual(getRootLength("file://localhost/c:d"), 17); + assert.strictEqual(getRootLength("file://localhost/c:/path"), 20); + assert.strictEqual(getRootLength("file://localhost/c%3a"), 21); + assert.strictEqual(getRootLength("file://localhost/c%3ad"), 17); + assert.strictEqual(getRootLength("file://localhost/c%3a/path"), 22); + assert.strictEqual(getRootLength("file://localhost/c%3A"), 21); + assert.strictEqual(getRootLength("file://localhost/c%3Ad"), 17); + assert.strictEqual(getRootLength("file://localhost/c%3A/path"), 22); + assert.strictEqual(getRootLength("file://server"), 13); + assert.strictEqual(getRootLength("file://server/"), 14); + assert.strictEqual(getRootLength("file://server/path"), 14); + assert.strictEqual(getRootLength("file://server/c:"), 14); + assert.strictEqual(getRootLength("file://server/c:d"), 14); + assert.strictEqual(getRootLength("file://server/c:/d"), 14); + assert.strictEqual(getRootLength("file://server/c%3a"), 14); + assert.strictEqual(getRootLength("file://server/c%3ad"), 14); + assert.strictEqual(getRootLength("file://server/c%3a/d"), 14); + assert.strictEqual(getRootLength("file://server/c%3A"), 14); + assert.strictEqual(getRootLength("file://server/c%3Ad"), 14); + assert.strictEqual(getRootLength("file://server/c%3A/d"), 14); + assert.strictEqual(getRootLength("http://server"), 13); + assert.strictEqual(getRootLength("http://server/path"), 14); }); it("isUrl", () => { - assert.isFalse(ts.isUrl("a")); - assert.isFalse(ts.isUrl("/")); - assert.isFalse(ts.isUrl("c:")); - assert.isFalse(ts.isUrl("c:d")); - assert.isFalse(ts.isUrl("c:/")); - assert.isFalse(ts.isUrl("c:\\")); - assert.isFalse(ts.isUrl("//server")); - assert.isFalse(ts.isUrl("//server/share")); - assert.isFalse(ts.isUrl("\\\\server")); - assert.isFalse(ts.isUrl("\\\\server\\share")); - assert.isTrue(ts.isUrl("file:///path")); - assert.isTrue(ts.isUrl("file:///c:")); - assert.isTrue(ts.isUrl("file:///c:d")); - assert.isTrue(ts.isUrl("file:///c:/path")); - assert.isTrue(ts.isUrl("file://server")); - assert.isTrue(ts.isUrl("file://server/path")); - assert.isTrue(ts.isUrl("http://server")); - assert.isTrue(ts.isUrl("http://server/path")); + assert.isFalse(isUrl("a")); + assert.isFalse(isUrl("/")); + assert.isFalse(isUrl("c:")); + assert.isFalse(isUrl("c:d")); + assert.isFalse(isUrl("c:/")); + assert.isFalse(isUrl("c:\\")); + assert.isFalse(isUrl("//server")); + assert.isFalse(isUrl("//server/share")); + assert.isFalse(isUrl("\\\\server")); + assert.isFalse(isUrl("\\\\server\\share")); + assert.isTrue(isUrl("file:///path")); + assert.isTrue(isUrl("file:///c:")); + assert.isTrue(isUrl("file:///c:d")); + assert.isTrue(isUrl("file:///c:/path")); + assert.isTrue(isUrl("file://server")); + assert.isTrue(isUrl("file://server/path")); + assert.isTrue(isUrl("http://server")); + assert.isTrue(isUrl("http://server/path")); }); it("isRootedDiskPath", () => { - assert.isFalse(ts.isRootedDiskPath("a")); - assert.isTrue(ts.isRootedDiskPath("/")); - assert.isTrue(ts.isRootedDiskPath("c:")); - assert.isFalse(ts.isRootedDiskPath("c:d")); - assert.isTrue(ts.isRootedDiskPath("c:/")); - assert.isTrue(ts.isRootedDiskPath("c:\\")); - assert.isTrue(ts.isRootedDiskPath("//server")); - assert.isTrue(ts.isRootedDiskPath("//server/share")); - assert.isTrue(ts.isRootedDiskPath("\\\\server")); - assert.isTrue(ts.isRootedDiskPath("\\\\server\\share")); - assert.isFalse(ts.isRootedDiskPath("file:///path")); - assert.isFalse(ts.isRootedDiskPath("file:///c:")); - assert.isFalse(ts.isRootedDiskPath("file:///c:d")); - assert.isFalse(ts.isRootedDiskPath("file:///c:/path")); - assert.isFalse(ts.isRootedDiskPath("file://server")); - assert.isFalse(ts.isRootedDiskPath("file://server/path")); - assert.isFalse(ts.isRootedDiskPath("http://server")); - assert.isFalse(ts.isRootedDiskPath("http://server/path")); + assert.isFalse(isRootedDiskPath("a")); + assert.isTrue(isRootedDiskPath("/")); + assert.isTrue(isRootedDiskPath("c:")); + assert.isFalse(isRootedDiskPath("c:d")); + assert.isTrue(isRootedDiskPath("c:/")); + assert.isTrue(isRootedDiskPath("c:\\")); + assert.isTrue(isRootedDiskPath("//server")); + assert.isTrue(isRootedDiskPath("//server/share")); + assert.isTrue(isRootedDiskPath("\\\\server")); + assert.isTrue(isRootedDiskPath("\\\\server\\share")); + assert.isFalse(isRootedDiskPath("file:///path")); + assert.isFalse(isRootedDiskPath("file:///c:")); + assert.isFalse(isRootedDiskPath("file:///c:d")); + assert.isFalse(isRootedDiskPath("file:///c:/path")); + assert.isFalse(isRootedDiskPath("file://server")); + assert.isFalse(isRootedDiskPath("file://server/path")); + assert.isFalse(isRootedDiskPath("http://server")); + assert.isFalse(isRootedDiskPath("http://server/path")); }); it("getDirectoryPath", () => { - assert.strictEqual(ts.getDirectoryPath(""), ""); - assert.strictEqual(ts.getDirectoryPath("a"), ""); - assert.strictEqual(ts.getDirectoryPath("a/b"), "a"); - assert.strictEqual(ts.getDirectoryPath("/"), "/"); - assert.strictEqual(ts.getDirectoryPath("/a"), "/"); - assert.strictEqual(ts.getDirectoryPath("/a/"), "/"); - assert.strictEqual(ts.getDirectoryPath("/a/b"), "/a"); - assert.strictEqual(ts.getDirectoryPath("/a/b/"), "/a"); - assert.strictEqual(ts.getDirectoryPath("c:"), "c:"); - assert.strictEqual(ts.getDirectoryPath("c:d"), ""); - assert.strictEqual(ts.getDirectoryPath("c:/"), "c:/"); - assert.strictEqual(ts.getDirectoryPath("c:/path"), "c:/"); - assert.strictEqual(ts.getDirectoryPath("c:/path/"), "c:/"); - assert.strictEqual(ts.getDirectoryPath("//server"), "//server"); - assert.strictEqual(ts.getDirectoryPath("//server/"), "//server/"); - assert.strictEqual(ts.getDirectoryPath("//server/share"), "//server/"); - assert.strictEqual(ts.getDirectoryPath("//server/share/"), "//server/"); - assert.strictEqual(ts.getDirectoryPath("\\\\server"), "//server"); - assert.strictEqual(ts.getDirectoryPath("\\\\server\\"), "//server/"); - assert.strictEqual(ts.getDirectoryPath("\\\\server\\share"), "//server/"); - assert.strictEqual(ts.getDirectoryPath("\\\\server\\share\\"), "//server/"); - assert.strictEqual(ts.getDirectoryPath("file:///"), "file:///"); - assert.strictEqual(ts.getDirectoryPath("file:///path"), "file:///"); - assert.strictEqual(ts.getDirectoryPath("file:///path/"), "file:///"); - assert.strictEqual(ts.getDirectoryPath("file:///c:"), "file:///c:"); - assert.strictEqual(ts.getDirectoryPath("file:///c:d"), "file:///"); - assert.strictEqual(ts.getDirectoryPath("file:///c:/"), "file:///c:/"); - assert.strictEqual(ts.getDirectoryPath("file:///c:/path"), "file:///c:/"); - assert.strictEqual(ts.getDirectoryPath("file:///c:/path/"), "file:///c:/"); - assert.strictEqual(ts.getDirectoryPath("file://server"), "file://server"); - assert.strictEqual(ts.getDirectoryPath("file://server/"), "file://server/"); - assert.strictEqual(ts.getDirectoryPath("file://server/path"), "file://server/"); - assert.strictEqual(ts.getDirectoryPath("file://server/path/"), "file://server/"); - assert.strictEqual(ts.getDirectoryPath("http://server"), "http://server"); - assert.strictEqual(ts.getDirectoryPath("http://server/"), "http://server/"); - assert.strictEqual(ts.getDirectoryPath("http://server/path"), "http://server/"); - assert.strictEqual(ts.getDirectoryPath("http://server/path/"), "http://server/"); + assert.strictEqual(getDirectoryPath(""), ""); + assert.strictEqual(getDirectoryPath("a"), ""); + assert.strictEqual(getDirectoryPath("a/b"), "a"); + assert.strictEqual(getDirectoryPath("/"), "/"); + assert.strictEqual(getDirectoryPath("/a"), "/"); + assert.strictEqual(getDirectoryPath("/a/"), "/"); + assert.strictEqual(getDirectoryPath("/a/b"), "/a"); + assert.strictEqual(getDirectoryPath("/a/b/"), "/a"); + assert.strictEqual(getDirectoryPath("c:"), "c:"); + assert.strictEqual(getDirectoryPath("c:d"), ""); + assert.strictEqual(getDirectoryPath("c:/"), "c:/"); + assert.strictEqual(getDirectoryPath("c:/path"), "c:/"); + assert.strictEqual(getDirectoryPath("c:/path/"), "c:/"); + assert.strictEqual(getDirectoryPath("//server"), "//server"); + assert.strictEqual(getDirectoryPath("//server/"), "//server/"); + assert.strictEqual(getDirectoryPath("//server/share"), "//server/"); + assert.strictEqual(getDirectoryPath("//server/share/"), "//server/"); + assert.strictEqual(getDirectoryPath("\\\\server"), "//server"); + assert.strictEqual(getDirectoryPath("\\\\server\\"), "//server/"); + assert.strictEqual(getDirectoryPath("\\\\server\\share"), "//server/"); + assert.strictEqual(getDirectoryPath("\\\\server\\share\\"), "//server/"); + assert.strictEqual(getDirectoryPath("file:///"), "file:///"); + assert.strictEqual(getDirectoryPath("file:///path"), "file:///"); + assert.strictEqual(getDirectoryPath("file:///path/"), "file:///"); + assert.strictEqual(getDirectoryPath("file:///c:"), "file:///c:"); + assert.strictEqual(getDirectoryPath("file:///c:d"), "file:///"); + assert.strictEqual(getDirectoryPath("file:///c:/"), "file:///c:/"); + assert.strictEqual(getDirectoryPath("file:///c:/path"), "file:///c:/"); + assert.strictEqual(getDirectoryPath("file:///c:/path/"), "file:///c:/"); + assert.strictEqual(getDirectoryPath("file://server"), "file://server"); + assert.strictEqual(getDirectoryPath("file://server/"), "file://server/"); + assert.strictEqual(getDirectoryPath("file://server/path"), "file://server/"); + assert.strictEqual(getDirectoryPath("file://server/path/"), "file://server/"); + assert.strictEqual(getDirectoryPath("http://server"), "http://server"); + assert.strictEqual(getDirectoryPath("http://server/"), "http://server/"); + assert.strictEqual(getDirectoryPath("http://server/path"), "http://server/"); + assert.strictEqual(getDirectoryPath("http://server/path/"), "http://server/"); }); it("getBaseFileName", () => { - assert.strictEqual(ts.getBaseFileName(""), ""); - assert.strictEqual(ts.getBaseFileName("a"), "a"); - assert.strictEqual(ts.getBaseFileName("a/"), "a"); - assert.strictEqual(ts.getBaseFileName("/"), ""); - assert.strictEqual(ts.getBaseFileName("/a"), "a"); - assert.strictEqual(ts.getBaseFileName("/a/"), "a"); - assert.strictEqual(ts.getBaseFileName("/a/b"), "b"); - assert.strictEqual(ts.getBaseFileName("c:"), ""); - assert.strictEqual(ts.getBaseFileName("c:d"), "c:d"); - assert.strictEqual(ts.getBaseFileName("c:/"), ""); - assert.strictEqual(ts.getBaseFileName("c:\\"), ""); - assert.strictEqual(ts.getBaseFileName("c:/path"), "path"); - assert.strictEqual(ts.getBaseFileName("c:/path/"), "path"); - assert.strictEqual(ts.getBaseFileName("//server"), ""); - assert.strictEqual(ts.getBaseFileName("//server/"), ""); - assert.strictEqual(ts.getBaseFileName("//server/share"), "share"); - assert.strictEqual(ts.getBaseFileName("//server/share/"), "share"); - assert.strictEqual(ts.getBaseFileName("file:///"), ""); - assert.strictEqual(ts.getBaseFileName("file:///path"), "path"); - assert.strictEqual(ts.getBaseFileName("file:///path/"), "path"); - assert.strictEqual(ts.getBaseFileName("file:///c:"), ""); - assert.strictEqual(ts.getBaseFileName("file:///c:/"), ""); - assert.strictEqual(ts.getBaseFileName("file:///c:d"), "c:d"); - assert.strictEqual(ts.getBaseFileName("file:///c:/d"), "d"); - assert.strictEqual(ts.getBaseFileName("file:///c:/d/"), "d"); - assert.strictEqual(ts.getBaseFileName("http://server"), ""); - assert.strictEqual(ts.getBaseFileName("http://server/"), ""); - assert.strictEqual(ts.getBaseFileName("http://server/a"), "a"); - assert.strictEqual(ts.getBaseFileName("http://server/a/"), "a"); - assert.strictEqual(ts.getBaseFileName("/path/a.ext", ".ext", /*ignoreCase*/ false), "a"); - assert.strictEqual(ts.getBaseFileName("/path/a.ext", ".EXT", /*ignoreCase*/ true), "a"); - assert.strictEqual(ts.getBaseFileName("/path/a.ext", "ext", /*ignoreCase*/ false), "a"); - assert.strictEqual(ts.getBaseFileName("/path/a.b", ".ext", /*ignoreCase*/ false), "a.b"); - assert.strictEqual(ts.getBaseFileName("/path/a.b", [".b", ".c"], /*ignoreCase*/ false), "a"); - assert.strictEqual(ts.getBaseFileName("/path/a.c", [".b", ".c"], /*ignoreCase*/ false), "a"); - assert.strictEqual(ts.getBaseFileName("/path/a.d", [".b", ".c"], /*ignoreCase*/ false), "a.d"); + assert.strictEqual(getBaseFileName(""), ""); + assert.strictEqual(getBaseFileName("a"), "a"); + assert.strictEqual(getBaseFileName("a/"), "a"); + assert.strictEqual(getBaseFileName("/"), ""); + assert.strictEqual(getBaseFileName("/a"), "a"); + assert.strictEqual(getBaseFileName("/a/"), "a"); + assert.strictEqual(getBaseFileName("/a/b"), "b"); + assert.strictEqual(getBaseFileName("c:"), ""); + assert.strictEqual(getBaseFileName("c:d"), "c:d"); + assert.strictEqual(getBaseFileName("c:/"), ""); + assert.strictEqual(getBaseFileName("c:\\"), ""); + assert.strictEqual(getBaseFileName("c:/path"), "path"); + assert.strictEqual(getBaseFileName("c:/path/"), "path"); + assert.strictEqual(getBaseFileName("//server"), ""); + assert.strictEqual(getBaseFileName("//server/"), ""); + assert.strictEqual(getBaseFileName("//server/share"), "share"); + assert.strictEqual(getBaseFileName("//server/share/"), "share"); + assert.strictEqual(getBaseFileName("file:///"), ""); + assert.strictEqual(getBaseFileName("file:///path"), "path"); + assert.strictEqual(getBaseFileName("file:///path/"), "path"); + assert.strictEqual(getBaseFileName("file:///c:"), ""); + assert.strictEqual(getBaseFileName("file:///c:/"), ""); + assert.strictEqual(getBaseFileName("file:///c:d"), "c:d"); + assert.strictEqual(getBaseFileName("file:///c:/d"), "d"); + assert.strictEqual(getBaseFileName("file:///c:/d/"), "d"); + assert.strictEqual(getBaseFileName("http://server"), ""); + assert.strictEqual(getBaseFileName("http://server/"), ""); + assert.strictEqual(getBaseFileName("http://server/a"), "a"); + assert.strictEqual(getBaseFileName("http://server/a/"), "a"); + assert.strictEqual(getBaseFileName("/path/a.ext", ".ext", /*ignoreCase*/ false), "a"); + assert.strictEqual(getBaseFileName("/path/a.ext", ".EXT", /*ignoreCase*/ true), "a"); + assert.strictEqual(getBaseFileName("/path/a.ext", "ext", /*ignoreCase*/ false), "a"); + assert.strictEqual(getBaseFileName("/path/a.b", ".ext", /*ignoreCase*/ false), "a.b"); + assert.strictEqual(getBaseFileName("/path/a.b", [".b", ".c"], /*ignoreCase*/ false), "a"); + assert.strictEqual(getBaseFileName("/path/a.c", [".b", ".c"], /*ignoreCase*/ false), "a"); + assert.strictEqual(getBaseFileName("/path/a.d", [".b", ".c"], /*ignoreCase*/ false), "a.d"); }); it("getAnyExtensionFromPath", () => { - assert.strictEqual(ts.getAnyExtensionFromPath(""), ""); - assert.strictEqual(ts.getAnyExtensionFromPath(".ext"), ".ext"); - assert.strictEqual(ts.getAnyExtensionFromPath("a.ext"), ".ext"); - assert.strictEqual(ts.getAnyExtensionFromPath("/a.ext"), ".ext"); - assert.strictEqual(ts.getAnyExtensionFromPath("a.ext/"), ".ext"); - assert.strictEqual(ts.getAnyExtensionFromPath("a.ext", ".ext", /*ignoreCase*/ false), ".ext"); - assert.strictEqual(ts.getAnyExtensionFromPath("a.ext", ".EXT", /*ignoreCase*/ true), ".ext"); - assert.strictEqual(ts.getAnyExtensionFromPath("a.ext", "ext", /*ignoreCase*/ false), ".ext"); - assert.strictEqual(ts.getAnyExtensionFromPath("a.b", ".ext", /*ignoreCase*/ false), ""); - assert.strictEqual(ts.getAnyExtensionFromPath("a.b", [".b", ".c"], /*ignoreCase*/ false), ".b"); - assert.strictEqual(ts.getAnyExtensionFromPath("a.c", [".b", ".c"], /*ignoreCase*/ false), ".c"); - assert.strictEqual(ts.getAnyExtensionFromPath("a.d", [".b", ".c"], /*ignoreCase*/ false), ""); + assert.strictEqual(getAnyExtensionFromPath(""), ""); + assert.strictEqual(getAnyExtensionFromPath(".ext"), ".ext"); + assert.strictEqual(getAnyExtensionFromPath("a.ext"), ".ext"); + assert.strictEqual(getAnyExtensionFromPath("/a.ext"), ".ext"); + assert.strictEqual(getAnyExtensionFromPath("a.ext/"), ".ext"); + assert.strictEqual(getAnyExtensionFromPath("a.ext", ".ext", /*ignoreCase*/ false), ".ext"); + assert.strictEqual(getAnyExtensionFromPath("a.ext", ".EXT", /*ignoreCase*/ true), ".ext"); + assert.strictEqual(getAnyExtensionFromPath("a.ext", "ext", /*ignoreCase*/ false), ".ext"); + assert.strictEqual(getAnyExtensionFromPath("a.b", ".ext", /*ignoreCase*/ false), ""); + assert.strictEqual(getAnyExtensionFromPath("a.b", [".b", ".c"], /*ignoreCase*/ false), ".b"); + assert.strictEqual(getAnyExtensionFromPath("a.c", [".b", ".c"], /*ignoreCase*/ false), ".c"); + assert.strictEqual(getAnyExtensionFromPath("a.d", [".b", ".c"], /*ignoreCase*/ false), ""); }); it("getPathComponents", () => { - assert.deepEqual(ts.getPathComponents(""), [""]); - assert.deepEqual(ts.getPathComponents("a"), ["", "a"]); - assert.deepEqual(ts.getPathComponents("./a"), ["", ".", "a"]); - assert.deepEqual(ts.getPathComponents("/"), ["/"]); - assert.deepEqual(ts.getPathComponents("/a"), ["/", "a"]); - assert.deepEqual(ts.getPathComponents("/a/"), ["/", "a"]); - assert.deepEqual(ts.getPathComponents("c:"), ["c:"]); - assert.deepEqual(ts.getPathComponents("c:d"), ["", "c:d"]); - assert.deepEqual(ts.getPathComponents("c:/"), ["c:/"]); - assert.deepEqual(ts.getPathComponents("c:/path"), ["c:/", "path"]); - assert.deepEqual(ts.getPathComponents("//server"), ["//server"]); - assert.deepEqual(ts.getPathComponents("//server/"), ["//server/"]); - assert.deepEqual(ts.getPathComponents("//server/share"), ["//server/", "share"]); - assert.deepEqual(ts.getPathComponents("file:///"), ["file:///"]); - assert.deepEqual(ts.getPathComponents("file:///path"), ["file:///", "path"]); - assert.deepEqual(ts.getPathComponents("file:///c:"), ["file:///c:"]); - assert.deepEqual(ts.getPathComponents("file:///c:d"), ["file:///", "c:d"]); - assert.deepEqual(ts.getPathComponents("file:///c:/"), ["file:///c:/"]); - assert.deepEqual(ts.getPathComponents("file:///c:/path"), ["file:///c:/", "path"]); - assert.deepEqual(ts.getPathComponents("file://server"), ["file://server"]); - assert.deepEqual(ts.getPathComponents("file://server/"), ["file://server/"]); - assert.deepEqual(ts.getPathComponents("file://server/path"), ["file://server/", "path"]); - assert.deepEqual(ts.getPathComponents("http://server"), ["http://server"]); - assert.deepEqual(ts.getPathComponents("http://server/"), ["http://server/"]); - assert.deepEqual(ts.getPathComponents("http://server/path"), ["http://server/", "path"]); + assert.deepEqual(getPathComponents(""), [""]); + assert.deepEqual(getPathComponents("a"), ["", "a"]); + assert.deepEqual(getPathComponents("./a"), ["", ".", "a"]); + assert.deepEqual(getPathComponents("/"), ["/"]); + assert.deepEqual(getPathComponents("/a"), ["/", "a"]); + assert.deepEqual(getPathComponents("/a/"), ["/", "a"]); + assert.deepEqual(getPathComponents("c:"), ["c:"]); + assert.deepEqual(getPathComponents("c:d"), ["", "c:d"]); + assert.deepEqual(getPathComponents("c:/"), ["c:/"]); + assert.deepEqual(getPathComponents("c:/path"), ["c:/", "path"]); + assert.deepEqual(getPathComponents("//server"), ["//server"]); + assert.deepEqual(getPathComponents("//server/"), ["//server/"]); + assert.deepEqual(getPathComponents("//server/share"), ["//server/", "share"]); + assert.deepEqual(getPathComponents("file:///"), ["file:///"]); + assert.deepEqual(getPathComponents("file:///path"), ["file:///", "path"]); + assert.deepEqual(getPathComponents("file:///c:"), ["file:///c:"]); + assert.deepEqual(getPathComponents("file:///c:d"), ["file:///", "c:d"]); + assert.deepEqual(getPathComponents("file:///c:/"), ["file:///c:/"]); + assert.deepEqual(getPathComponents("file:///c:/path"), ["file:///c:/", "path"]); + assert.deepEqual(getPathComponents("file://server"), ["file://server"]); + assert.deepEqual(getPathComponents("file://server/"), ["file://server/"]); + assert.deepEqual(getPathComponents("file://server/path"), ["file://server/", "path"]); + assert.deepEqual(getPathComponents("http://server"), ["http://server"]); + assert.deepEqual(getPathComponents("http://server/"), ["http://server/"]); + assert.deepEqual(getPathComponents("http://server/path"), ["http://server/", "path"]); }); it("reducePathComponents", () => { - assert.deepEqual(ts.reducePathComponents([]), []); - assert.deepEqual(ts.reducePathComponents([""]), [""]); - assert.deepEqual(ts.reducePathComponents(["", "."]), [""]); - assert.deepEqual(ts.reducePathComponents(["", ".", "a"]), ["", "a"]); - assert.deepEqual(ts.reducePathComponents(["", "a", "."]), ["", "a"]); - assert.deepEqual(ts.reducePathComponents(["", ".."]), ["", ".."]); - assert.deepEqual(ts.reducePathComponents(["", "..", ".."]), ["", "..", ".."]); - assert.deepEqual(ts.reducePathComponents(["", "..", ".", ".."]), ["", "..", ".."]); - assert.deepEqual(ts.reducePathComponents(["", "a", ".."]), [""]); - assert.deepEqual(ts.reducePathComponents(["", "..", "a"]), ["", "..", "a"]); - assert.deepEqual(ts.reducePathComponents(["/"]), ["/"]); - assert.deepEqual(ts.reducePathComponents(["/", "."]), ["/"]); - assert.deepEqual(ts.reducePathComponents(["/", ".."]), ["/"]); - assert.deepEqual(ts.reducePathComponents(["/", "a", ".."]), ["/"]); + assert.deepEqual(reducePathComponents([]), []); + assert.deepEqual(reducePathComponents([""]), [""]); + assert.deepEqual(reducePathComponents(["", "."]), [""]); + assert.deepEqual(reducePathComponents(["", ".", "a"]), ["", "a"]); + assert.deepEqual(reducePathComponents(["", "a", "."]), ["", "a"]); + assert.deepEqual(reducePathComponents(["", ".."]), ["", ".."]); + assert.deepEqual(reducePathComponents(["", "..", ".."]), ["", "..", ".."]); + assert.deepEqual(reducePathComponents(["", "..", ".", ".."]), ["", "..", ".."]); + assert.deepEqual(reducePathComponents(["", "a", ".."]), [""]); + assert.deepEqual(reducePathComponents(["", "..", "a"]), ["", "..", "a"]); + assert.deepEqual(reducePathComponents(["/"]), ["/"]); + assert.deepEqual(reducePathComponents(["/", "."]), ["/"]); + assert.deepEqual(reducePathComponents(["/", ".."]), ["/"]); + assert.deepEqual(reducePathComponents(["/", "a", ".."]), ["/"]); }); it("combinePaths", () => { - assert.strictEqual(ts.combinePaths("/", "/node_modules/@types"), "/node_modules/@types"); - assert.strictEqual(ts.combinePaths("/a/..", ""), "/a/.."); - assert.strictEqual(ts.combinePaths("/a/..", "b"), "/a/../b"); - assert.strictEqual(ts.combinePaths("/a/..", "b/"), "/a/../b/"); - assert.strictEqual(ts.combinePaths("/a/..", "/"), "/"); - assert.strictEqual(ts.combinePaths("/a/..", "/b"), "/b"); + assert.strictEqual(combinePaths("/", "/node_modules/@types"), "/node_modules/@types"); + assert.strictEqual(combinePaths("/a/..", ""), "/a/.."); + assert.strictEqual(combinePaths("/a/..", "b"), "/a/../b"); + assert.strictEqual(combinePaths("/a/..", "b/"), "/a/../b/"); + assert.strictEqual(combinePaths("/a/..", "/"), "/"); + assert.strictEqual(combinePaths("/a/..", "/b"), "/b"); }); it("resolvePath", () => { - assert.strictEqual(ts.resolvePath(""), ""); - assert.strictEqual(ts.resolvePath("."), ""); - assert.strictEqual(ts.resolvePath("./"), ""); - assert.strictEqual(ts.resolvePath(".."), ".."); - assert.strictEqual(ts.resolvePath("../"), "../"); - assert.strictEqual(ts.resolvePath("/"), "/"); - assert.strictEqual(ts.resolvePath("/."), "/"); - assert.strictEqual(ts.resolvePath("/./"), "/"); - assert.strictEqual(ts.resolvePath("/../"), "/"); - assert.strictEqual(ts.resolvePath("/a"), "/a"); - assert.strictEqual(ts.resolvePath("/a/"), "/a/"); - assert.strictEqual(ts.resolvePath("/a/."), "/a"); - assert.strictEqual(ts.resolvePath("/a/./"), "/a/"); - assert.strictEqual(ts.resolvePath("/a/./b"), "/a/b"); - assert.strictEqual(ts.resolvePath("/a/./b/"), "/a/b/"); - assert.strictEqual(ts.resolvePath("/a/.."), "/"); - assert.strictEqual(ts.resolvePath("/a/../"), "/"); - assert.strictEqual(ts.resolvePath("/a/../b"), "/b"); - assert.strictEqual(ts.resolvePath("/a/../b/"), "/b/"); - assert.strictEqual(ts.resolvePath("/a/..", "b"), "/b"); - assert.strictEqual(ts.resolvePath("/a/..", "/"), "/"); - assert.strictEqual(ts.resolvePath("/a/..", "b/"), "/b/"); - assert.strictEqual(ts.resolvePath("/a/..", "/b"), "/b"); - assert.strictEqual(ts.resolvePath("/a/.", "b"), "/a/b"); - assert.strictEqual(ts.resolvePath("/a/.", "."), "/a"); - assert.strictEqual(ts.resolvePath("a", "b", "c"), "a/b/c"); - assert.strictEqual(ts.resolvePath("a", "b", "/c"), "/c"); - assert.strictEqual(ts.resolvePath("a", "b", "../c"), "a/c"); + assert.strictEqual(resolvePath(""), ""); + assert.strictEqual(resolvePath("."), ""); + assert.strictEqual(resolvePath("./"), ""); + assert.strictEqual(resolvePath(".."), ".."); + assert.strictEqual(resolvePath("../"), "../"); + assert.strictEqual(resolvePath("/"), "/"); + assert.strictEqual(resolvePath("/."), "/"); + assert.strictEqual(resolvePath("/./"), "/"); + assert.strictEqual(resolvePath("/../"), "/"); + assert.strictEqual(resolvePath("/a"), "/a"); + assert.strictEqual(resolvePath("/a/"), "/a/"); + assert.strictEqual(resolvePath("/a/."), "/a"); + assert.strictEqual(resolvePath("/a/./"), "/a/"); + assert.strictEqual(resolvePath("/a/./b"), "/a/b"); + assert.strictEqual(resolvePath("/a/./b/"), "/a/b/"); + assert.strictEqual(resolvePath("/a/.."), "/"); + assert.strictEqual(resolvePath("/a/../"), "/"); + assert.strictEqual(resolvePath("/a/../b"), "/b"); + assert.strictEqual(resolvePath("/a/../b/"), "/b/"); + assert.strictEqual(resolvePath("/a/..", "b"), "/b"); + assert.strictEqual(resolvePath("/a/..", "/"), "/"); + assert.strictEqual(resolvePath("/a/..", "b/"), "/b/"); + assert.strictEqual(resolvePath("/a/..", "/b"), "/b"); + assert.strictEqual(resolvePath("/a/.", "b"), "/a/b"); + assert.strictEqual(resolvePath("/a/.", "."), "/a"); + assert.strictEqual(resolvePath("a", "b", "c"), "a/b/c"); + assert.strictEqual(resolvePath("a", "b", "/c"), "/c"); + assert.strictEqual(resolvePath("a", "b", "../c"), "a/c"); }); it("getPathRelativeTo", () => { - assert.strictEqual(ts.getRelativePathFromDirectory("/", "/", /*ignoreCase*/ false), ""); - assert.strictEqual(ts.getRelativePathFromDirectory("/a", "/a", /*ignoreCase*/ false), ""); - assert.strictEqual(ts.getRelativePathFromDirectory("/a/", "/a", /*ignoreCase*/ false), ""); - assert.strictEqual(ts.getRelativePathFromDirectory("/a", "/", /*ignoreCase*/ false), ".."); - assert.strictEqual(ts.getRelativePathFromDirectory("/a", "/b", /*ignoreCase*/ false), "../b"); - assert.strictEqual(ts.getRelativePathFromDirectory("/a/b", "/b", /*ignoreCase*/ false), "../../b"); - assert.strictEqual(ts.getRelativePathFromDirectory("/a/b/c", "/b", /*ignoreCase*/ false), "../../../b"); - assert.strictEqual(ts.getRelativePathFromDirectory("/a/b/c", "/b/c", /*ignoreCase*/ false), "../../../b/c"); - assert.strictEqual(ts.getRelativePathFromDirectory("/a/b/c", "/a/b", /*ignoreCase*/ false), ".."); - assert.strictEqual(ts.getRelativePathFromDirectory("c:", "d:", /*ignoreCase*/ false), "d:/"); - assert.strictEqual(ts.getRelativePathFromDirectory("file:///", "file:///", /*ignoreCase*/ false), ""); - assert.strictEqual(ts.getRelativePathFromDirectory("file:///a", "file:///a", /*ignoreCase*/ false), ""); - assert.strictEqual(ts.getRelativePathFromDirectory("file:///a/", "file:///a", /*ignoreCase*/ false), ""); - assert.strictEqual(ts.getRelativePathFromDirectory("file:///a", "file:///", /*ignoreCase*/ false), ".."); - assert.strictEqual(ts.getRelativePathFromDirectory("file:///a", "file:///b", /*ignoreCase*/ false), "../b"); - assert.strictEqual(ts.getRelativePathFromDirectory("file:///a/b", "file:///b", /*ignoreCase*/ false), "../../b"); - assert.strictEqual(ts.getRelativePathFromDirectory("file:///a/b/c", "file:///b", /*ignoreCase*/ false), "../../../b"); - assert.strictEqual(ts.getRelativePathFromDirectory("file:///a/b/c", "file:///b/c", /*ignoreCase*/ false), "../../../b/c"); - assert.strictEqual(ts.getRelativePathFromDirectory("file:///a/b/c", "file:///a/b", /*ignoreCase*/ false), ".."); - assert.strictEqual(ts.getRelativePathFromDirectory("file:///c:", "file:///d:", /*ignoreCase*/ false), "file:///d:/"); + assert.strictEqual(getRelativePathFromDirectory("/", "/", /*ignoreCase*/ false), ""); + assert.strictEqual(getRelativePathFromDirectory("/a", "/a", /*ignoreCase*/ false), ""); + assert.strictEqual(getRelativePathFromDirectory("/a/", "/a", /*ignoreCase*/ false), ""); + assert.strictEqual(getRelativePathFromDirectory("/a", "/", /*ignoreCase*/ false), ".."); + assert.strictEqual(getRelativePathFromDirectory("/a", "/b", /*ignoreCase*/ false), "../b"); + assert.strictEqual(getRelativePathFromDirectory("/a/b", "/b", /*ignoreCase*/ false), "../../b"); + assert.strictEqual(getRelativePathFromDirectory("/a/b/c", "/b", /*ignoreCase*/ false), "../../../b"); + assert.strictEqual(getRelativePathFromDirectory("/a/b/c", "/b/c", /*ignoreCase*/ false), "../../../b/c"); + assert.strictEqual(getRelativePathFromDirectory("/a/b/c", "/a/b", /*ignoreCase*/ false), ".."); + assert.strictEqual(getRelativePathFromDirectory("c:", "d:", /*ignoreCase*/ false), "d:/"); + assert.strictEqual(getRelativePathFromDirectory("file:///", "file:///", /*ignoreCase*/ false), ""); + assert.strictEqual(getRelativePathFromDirectory("file:///a", "file:///a", /*ignoreCase*/ false), ""); + assert.strictEqual(getRelativePathFromDirectory("file:///a/", "file:///a", /*ignoreCase*/ false), ""); + assert.strictEqual(getRelativePathFromDirectory("file:///a", "file:///", /*ignoreCase*/ false), ".."); + assert.strictEqual(getRelativePathFromDirectory("file:///a", "file:///b", /*ignoreCase*/ false), "../b"); + assert.strictEqual(getRelativePathFromDirectory("file:///a/b", "file:///b", /*ignoreCase*/ false), "../../b"); + assert.strictEqual(getRelativePathFromDirectory("file:///a/b/c", "file:///b", /*ignoreCase*/ false), "../../../b"); + assert.strictEqual(getRelativePathFromDirectory("file:///a/b/c", "file:///b/c", /*ignoreCase*/ false), "../../../b/c"); + assert.strictEqual(getRelativePathFromDirectory("file:///a/b/c", "file:///a/b", /*ignoreCase*/ false), ".."); + assert.strictEqual(getRelativePathFromDirectory("file:///c:", "file:///d:", /*ignoreCase*/ false), "file:///d:/"); }); it("toFileNameLowerCase", () => { - assert.strictEqual( - ts.toFileNameLowerCase("/user/UserName/projects/Project/file.ts"), - "/user/username/projects/project/file.ts" - ); - assert.strictEqual( - ts.toFileNameLowerCase("/user/UserName/projects/projectß/file.ts"), - "/user/username/projects/projectß/file.ts" - ); - assert.strictEqual( - ts.toFileNameLowerCase("/user/UserName/projects/İproject/file.ts"), - "/user/username/projects/İproject/file.ts" - ); - assert.strictEqual( - ts.toFileNameLowerCase("/user/UserName/projects/ı/file.ts"), - "/user/username/projects/ı/file.ts" - ); + assert.strictEqual(toFileNameLowerCase("/user/UserName/projects/Project/file.ts"), "/user/username/projects/project/file.ts"); + assert.strictEqual(toFileNameLowerCase("/user/UserName/projects/projectß/file.ts"), "/user/username/projects/projectß/file.ts"); + assert.strictEqual(toFileNameLowerCase("/user/UserName/projects/İproject/file.ts"), "/user/username/projects/İproject/file.ts"); + assert.strictEqual(toFileNameLowerCase("/user/UserName/projects/ı/file.ts"), "/user/username/projects/ı/file.ts"); }); }); diff --git a/src/testRunner/unittests/printer.ts b/src/testRunner/unittests/printer.ts index d04bf65603c11..f9fb3b54a900b 100644 --- a/src/testRunner/unittests/printer.ts +++ b/src/testRunner/unittests/printer.ts @@ -1,21 +1,20 @@ -namespace ts { - describe("unittests:: PrinterAPI", () => { - function makePrintsCorrectly(prefix: string) { - return function printsCorrectly(name: string, options: PrinterOptions, printCallback: (printer: Printer) => string) { - it(name, () => { - Harness.Baseline.runBaseline(`printerApi/${prefix}.${name}.js`, - printCallback(createPrinter({ newLine: NewLineKind.CarriageReturnLineFeed, ...options }))); - }); - }; - } - - describe("printFile", () => { - const printsCorrectly = makePrintsCorrectly("printsFileCorrectly"); - describe("comment handling", () => { - // Avoid eagerly creating the sourceFile so that `createSourceFile` doesn't run unless one of these tests is run. - let sourceFile: SourceFile; - before(() => { - sourceFile = createSourceFile("source.ts", ` +import { PrinterOptions, Printer, createPrinter, NewLineKind, SourceFile, createSourceFile, ScriptTarget, ScriptKind, Bundle, createBundle, EmitHint, createClassDeclaration, createIdentifier, createProperty, createNodeArray, createToken, SyntaxKind, createNamespaceExportDeclaration, createNew, createPropertyAccess, createCall, createConditional, createModuleDeclaration, createModuleBlock, emptyArray, NodeFlags, createMethod, createKeywordTypeNode, createTupleTypeNode, createFunctionTypeNode, createParameter, createTypeParameterDeclaration, createObjectBindingPattern } from "../ts"; +import { Baseline } from "../Harness"; +describe("unittests:: PrinterAPI", () => { + function makePrintsCorrectly(prefix: string) { + return function printsCorrectly(name: string, options: PrinterOptions, printCallback: (printer: Printer) => string) { + it(name, () => { + Baseline.runBaseline(`printerApi/${prefix}.${name}.js`, printCallback(createPrinter({ newLine: NewLineKind.CarriageReturnLineFeed, ...options }))); + }); + }; + } + describe("printFile", () => { + const printsCorrectly = makePrintsCorrectly("printsFileCorrectly"); + describe("comment handling", () => { + // Avoid eagerly creating the sourceFile so that `createSourceFile` doesn't run unless one of these tests is run. + let sourceFile: SourceFile; + before(() => { + sourceFile = createSourceFile("source.ts", ` interface A { // comment1 readonly prop?: T; @@ -50,252 +49,133 @@ namespace ts { // comment10 function functionWithDefaultArgValue(argument: string = "defaultValue"): void { } `, ScriptTarget.ES2015); - }); - printsCorrectly("default", {}, printer => printer.printFile(sourceFile)); - printsCorrectly("removeComments", { removeComments: true }, printer => printer.printFile(sourceFile)); }); - - // https://github.com/microsoft/TypeScript/issues/14948 - // eslint-disable-next-line no-template-curly-in-string - printsCorrectly("templateLiteral", {}, printer => printer.printFile(createSourceFile("source.ts", "let greeting = `Hi ${name}, how are you?`;", ScriptTarget.ES2017))); - - // https://github.com/microsoft/TypeScript/issues/18071 - printsCorrectly("regularExpressionLiteral", {}, printer => printer.printFile(createSourceFile("source.ts", "let regex = /abc/;", ScriptTarget.ES2017))); - - // https://github.com/microsoft/TypeScript/issues/22239 - printsCorrectly("importStatementRemoveComments", { removeComments: true }, printer => printer.printFile(createSourceFile("source.ts", "import {foo} from 'foo';", ScriptTarget.ESNext))); - printsCorrectly("classHeritageClauses", {}, printer => printer.printFile(createSourceFile( - "source.ts", - `class A extends B implements C implements D {}`, - ScriptTarget.ES2017 - ))); - - // https://github.com/microsoft/TypeScript/issues/35093 - printsCorrectly("definiteAssignmentAssertions", {}, printer => printer.printFile(createSourceFile( - "source.ts", - `class A { + printsCorrectly("default", {}, printer => printer.printFile(sourceFile)); + printsCorrectly("removeComments", { removeComments: true }, printer => printer.printFile(sourceFile)); + }); + // https://github.com/microsoft/TypeScript/issues/14948 + // eslint-disable-next-line no-template-curly-in-string + printsCorrectly("templateLiteral", {}, printer => printer.printFile(createSourceFile("source.ts", "let greeting = `Hi ${name}, how are you?`;", ScriptTarget.ES2017))); + // https://github.com/microsoft/TypeScript/issues/18071 + printsCorrectly("regularExpressionLiteral", {}, printer => printer.printFile(createSourceFile("source.ts", "let regex = /abc/;", ScriptTarget.ES2017))); + // https://github.com/microsoft/TypeScript/issues/22239 + printsCorrectly("importStatementRemoveComments", { removeComments: true }, printer => printer.printFile(createSourceFile("source.ts", "import {foo} from 'foo';", ScriptTarget.ESNext))); + printsCorrectly("classHeritageClauses", {}, printer => printer.printFile(createSourceFile("source.ts", `class A extends B implements C implements D {}`, ScriptTarget.ES2017))); + // https://github.com/microsoft/TypeScript/issues/35093 + printsCorrectly("definiteAssignmentAssertions", {}, printer => printer.printFile(createSourceFile("source.ts", `class A { prop!: string; } - let x!: string;`, - ScriptTarget.ES2017 - ))); - - // https://github.com/microsoft/TypeScript/issues/35054 - printsCorrectly("jsx attribute escaping", {}, printer => { - debugger; - return printer.printFile(createSourceFile( - "source.ts", - String.raw``, - ScriptTarget.ESNext, - /*setParentNodes*/ undefined, - ScriptKind.TSX - )); - }); + let x!: string;`, ScriptTarget.ES2017))); + // https://github.com/microsoft/TypeScript/issues/35054 + printsCorrectly("jsx attribute escaping", {}, printer => { + debugger; + return printer.printFile(createSourceFile("source.ts", String.raw ``, ScriptTarget.ESNext, + /*setParentNodes*/ undefined, ScriptKind.TSX)); }); - - describe("printBundle", () => { - const printsCorrectly = makePrintsCorrectly("printsBundleCorrectly"); - let bundle: Bundle; - before(() => { - bundle = createBundle([ - createSourceFile("a.ts", ` + }); + describe("printBundle", () => { + const printsCorrectly = makePrintsCorrectly("printsBundleCorrectly"); + let bundle: Bundle; + before(() => { + bundle = createBundle([ + createSourceFile("a.ts", ` /*! [a.ts] */ // comment0 const a = 1; `, ScriptTarget.ES2015), - createSourceFile("b.ts", ` + createSourceFile("b.ts", ` /*! [b.ts] */ // comment1 const b = 2; `, ScriptTarget.ES2015) - ]); - }); - printsCorrectly("default", {}, printer => printer.printBundle(bundle)); - printsCorrectly("removeComments", { removeComments: true }, printer => printer.printBundle(bundle)); - }); - - describe("printNode", () => { - const printsCorrectly = makePrintsCorrectly("printsNodeCorrectly"); - printsCorrectly("class", {}, printer => printer.printNode( - EmitHint.Unspecified, - createClassDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*name*/ createIdentifier("C"), - /*typeParameters*/ undefined, - /*heritageClauses*/ undefined, - [createProperty( - /*decorators*/ undefined, - createNodeArray([createToken(SyntaxKind.PublicKeyword)]), - createIdentifier("prop"), - /*questionToken*/ undefined, - /*type*/ undefined, - /*initializer*/ undefined - )] - ), - createSourceFile("source.ts", "", ScriptTarget.ES2015) - )); - - printsCorrectly("namespaceExportDeclaration", {}, printer => printer.printNode( - EmitHint.Unspecified, - createNamespaceExportDeclaration("B"), - createSourceFile("source.ts", "", ScriptTarget.ES2015) - )); - - printsCorrectly("newExpressionWithPropertyAccessOnCallExpression", {}, printer => printer.printNode( - EmitHint.Unspecified, - createNew( - createPropertyAccess( - createCall(createIdentifier("f"), /*typeArguments*/ undefined, /*argumentsArray*/ undefined), - "x"), - /*typeArguments*/ undefined, - /*argumentsArray*/ undefined - ), - createSourceFile("source.ts", "", ScriptTarget.ESNext)) - ); - - printsCorrectly("newExpressionOnConditionalExpression", {}, printer => printer.printNode( - EmitHint.Unspecified, - createNew( - createConditional( - createIdentifier("x"), createToken(SyntaxKind.QuestionToken), - createIdentifier("y"), createToken(SyntaxKind.ColonToken), - createIdentifier("z")), - /*typeArguments*/ undefined, - /*argumentsArray*/ undefined - ), - createSourceFile("source.ts", "", ScriptTarget.ESNext)) - ); - - printsCorrectly("emptyGlobalAugmentation", {}, printer => printer.printNode( - EmitHint.Unspecified, - createModuleDeclaration( - /*decorators*/ undefined, - /*modifiers*/ [createToken(SyntaxKind.DeclareKeyword)], - createIdentifier("global"), - createModuleBlock(emptyArray), - NodeFlags.GlobalAugmentation), - createSourceFile("source.ts", "", ScriptTarget.ES2015) - )); - - printsCorrectly("emptyGlobalAugmentationWithNoDeclareKeyword", {}, printer => printer.printNode( - EmitHint.Unspecified, - createModuleDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - createIdentifier("global"), - createModuleBlock(emptyArray), - NodeFlags.GlobalAugmentation), - createSourceFile("source.ts", "", ScriptTarget.ES2015) - )); - - // https://github.com/Microsoft/TypeScript/issues/15971 - printsCorrectly("classWithOptionalMethodAndProperty", {}, printer => printer.printNode( - EmitHint.Unspecified, - createClassDeclaration( - /*decorators*/ undefined, - /*modifiers*/ [createToken(SyntaxKind.DeclareKeyword)], - /*name*/ createIdentifier("X"), - /*typeParameters*/ undefined, - /*heritageClauses*/ undefined, - [ - createMethod( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*asteriskToken*/ undefined, - /*name*/ createIdentifier("method"), - /*questionToken*/ createToken(SyntaxKind.QuestionToken), - /*typeParameters*/ undefined, - [], - /*type*/ createKeywordTypeNode(SyntaxKind.VoidKeyword), - /*body*/ undefined - ), - createProperty( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*name*/ createIdentifier("property"), - /*questionToken*/ createToken(SyntaxKind.QuestionToken), - /*type*/ createKeywordTypeNode(SyntaxKind.StringKeyword), - /*initializer*/ undefined - ), - ] - ), - createSourceFile("source.ts", "", ScriptTarget.ES2015) - )); - - // https://github.com/Microsoft/TypeScript/issues/15651 - printsCorrectly("functionTypes", {}, printer => printer.printNode( - EmitHint.Unspecified, - createTupleTypeNode([ - createFunctionTypeNode( - /*typeArguments*/ undefined, - [createParameter( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, - createIdentifier("args") - )], - createKeywordTypeNode(SyntaxKind.AnyKeyword) - ), - createFunctionTypeNode( - [createTypeParameterDeclaration("T")], - [createParameter( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, - createIdentifier("args") - )], - createKeywordTypeNode(SyntaxKind.AnyKeyword) - ), - createFunctionTypeNode( - /*typeArguments*/ undefined, - [createParameter( - /*decorators*/ undefined, - /*modifiers*/ undefined, - createToken(SyntaxKind.DotDotDotToken), - createIdentifier("args") - )], - createKeywordTypeNode(SyntaxKind.AnyKeyword) - ), - createFunctionTypeNode( - /*typeArguments*/ undefined, - [createParameter( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, - createIdentifier("args"), - createToken(SyntaxKind.QuestionToken) - )], - createKeywordTypeNode(SyntaxKind.AnyKeyword) - ), - createFunctionTypeNode( - /*typeArguments*/ undefined, - [createParameter( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, - createIdentifier("args"), - /*questionToken*/ undefined, - createKeywordTypeNode(SyntaxKind.AnyKeyword) - )], - createKeywordTypeNode(SyntaxKind.AnyKeyword) - ), - createFunctionTypeNode( - /*typeArguments*/ undefined, - [createParameter( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, - createObjectBindingPattern([]) - )], - createKeywordTypeNode(SyntaxKind.AnyKeyword) - ), - ]), - createSourceFile("source.ts", "", ScriptTarget.ES2015) - )); + ]); }); + printsCorrectly("default", {}, printer => printer.printBundle(bundle)); + printsCorrectly("removeComments", { removeComments: true }, printer => printer.printBundle(bundle)); + }); + describe("printNode", () => { + const printsCorrectly = makePrintsCorrectly("printsNodeCorrectly"); + printsCorrectly("class", {}, printer => printer.printNode(EmitHint.Unspecified, createClassDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*name*/ createIdentifier("C"), + /*typeParameters*/ undefined, + /*heritageClauses*/ undefined, [createProperty( + /*decorators*/ undefined, createNodeArray([createToken(SyntaxKind.PublicKeyword)]), createIdentifier("prop"), + /*questionToken*/ undefined, + /*type*/ undefined, + /*initializer*/ undefined)]), createSourceFile("source.ts", "", ScriptTarget.ES2015))); + printsCorrectly("namespaceExportDeclaration", {}, printer => printer.printNode(EmitHint.Unspecified, createNamespaceExportDeclaration("B"), createSourceFile("source.ts", "", ScriptTarget.ES2015))); + printsCorrectly("newExpressionWithPropertyAccessOnCallExpression", {}, printer => printer.printNode(EmitHint.Unspecified, createNew(createPropertyAccess(createCall(createIdentifier("f"), /*typeArguments*/ undefined, /*argumentsArray*/ undefined), "x"), + /*typeArguments*/ undefined, + /*argumentsArray*/ undefined), createSourceFile("source.ts", "", ScriptTarget.ESNext))); + printsCorrectly("newExpressionOnConditionalExpression", {}, printer => printer.printNode(EmitHint.Unspecified, createNew(createConditional(createIdentifier("x"), createToken(SyntaxKind.QuestionToken), createIdentifier("y"), createToken(SyntaxKind.ColonToken), createIdentifier("z")), + /*typeArguments*/ undefined, + /*argumentsArray*/ undefined), createSourceFile("source.ts", "", ScriptTarget.ESNext))); + printsCorrectly("emptyGlobalAugmentation", {}, printer => printer.printNode(EmitHint.Unspecified, createModuleDeclaration( + /*decorators*/ undefined, + /*modifiers*/ [createToken(SyntaxKind.DeclareKeyword)], createIdentifier("global"), createModuleBlock(emptyArray), NodeFlags.GlobalAugmentation), createSourceFile("source.ts", "", ScriptTarget.ES2015))); + printsCorrectly("emptyGlobalAugmentationWithNoDeclareKeyword", {}, printer => printer.printNode(EmitHint.Unspecified, createModuleDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, createIdentifier("global"), createModuleBlock(emptyArray), NodeFlags.GlobalAugmentation), createSourceFile("source.ts", "", ScriptTarget.ES2015))); + // https://github.com/Microsoft/TypeScript/issues/15971 + printsCorrectly("classWithOptionalMethodAndProperty", {}, printer => printer.printNode(EmitHint.Unspecified, createClassDeclaration( + /*decorators*/ undefined, + /*modifiers*/ [createToken(SyntaxKind.DeclareKeyword)], + /*name*/ createIdentifier("X"), + /*typeParameters*/ undefined, + /*heritageClauses*/ undefined, [ + createMethod( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*asteriskToken*/ undefined, + /*name*/ createIdentifier("method"), + /*questionToken*/ createToken(SyntaxKind.QuestionToken), + /*typeParameters*/ undefined, [], + /*type*/ createKeywordTypeNode(SyntaxKind.VoidKeyword), + /*body*/ undefined), + createProperty( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*name*/ createIdentifier("property"), + /*questionToken*/ createToken(SyntaxKind.QuestionToken), + /*type*/ createKeywordTypeNode(SyntaxKind.StringKeyword), + /*initializer*/ undefined), + ]), createSourceFile("source.ts", "", ScriptTarget.ES2015))); + // https://github.com/Microsoft/TypeScript/issues/15651 + printsCorrectly("functionTypes", {}, printer => printer.printNode(EmitHint.Unspecified, createTupleTypeNode([ + createFunctionTypeNode( + /*typeArguments*/ undefined, [createParameter( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, createIdentifier("args"))], createKeywordTypeNode(SyntaxKind.AnyKeyword)), + createFunctionTypeNode([createTypeParameterDeclaration("T")], [createParameter( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, createIdentifier("args"))], createKeywordTypeNode(SyntaxKind.AnyKeyword)), + createFunctionTypeNode( + /*typeArguments*/ undefined, [createParameter( + /*decorators*/ undefined, + /*modifiers*/ undefined, createToken(SyntaxKind.DotDotDotToken), createIdentifier("args"))], createKeywordTypeNode(SyntaxKind.AnyKeyword)), + createFunctionTypeNode( + /*typeArguments*/ undefined, [createParameter( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, createIdentifier("args"), createToken(SyntaxKind.QuestionToken))], createKeywordTypeNode(SyntaxKind.AnyKeyword)), + createFunctionTypeNode( + /*typeArguments*/ undefined, [createParameter( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, createIdentifier("args"), + /*questionToken*/ undefined, createKeywordTypeNode(SyntaxKind.AnyKeyword))], createKeywordTypeNode(SyntaxKind.AnyKeyword)), + createFunctionTypeNode( + /*typeArguments*/ undefined, [createParameter( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, createObjectBindingPattern([]))], createKeywordTypeNode(SyntaxKind.AnyKeyword)), + ]), createSourceFile("source.ts", "", ScriptTarget.ES2015))); }); -} +}); diff --git a/src/testRunner/unittests/programApi.ts b/src/testRunner/unittests/programApi.ts index 8b13792d35ea5..8f76d66fb18ef 100644 --- a/src/testRunner/unittests/programApi.ts +++ b/src/testRunner/unittests/programApi.ts @@ -1,207 +1,174 @@ -namespace ts { - function verifyMissingFilePaths(missingPaths: readonly Path[], expected: readonly string[]) { - assert.isDefined(missingPaths); - const map = arrayToSet(expected) as Map; - for (const missing of missingPaths) { - const value = map.get(missing); - assert.isTrue(value, `${missing} to be ${value === undefined ? "not present" : "present only once"}, in actual: ${missingPaths} expected: ${expected}`); - map.set(missing, false); - } - const notFound = arrayFrom(mapDefinedIterator(map.keys(), k => map.get(k) === true ? k : undefined)); - assert.equal(notFound.length, 0, `Not found ${notFound} in actual: ${missingPaths} expected: ${expected}`); +import { Path, arrayToSet, arrayFrom, mapDefinedIterator, CompilerOptions, NewLineKind, createProgram, ScriptTarget, createSourceFile, sys, ModuleKind, emptyOptions, Program, ScriptKind, ExpressionStatement, AsExpression, ImportDeclaration } from "../ts"; +import { TextDocument } from "../documents"; +import { CompilerHost } from "../fakes"; +import { createFromFileSystem } from "../vfs"; +import { IO } from "../Harness"; +import * as ts from "../ts"; +function verifyMissingFilePaths(missingPaths: readonly Path[], expected: readonly string[]) { + assert.isDefined(missingPaths); + const map = (arrayToSet(expected) as ts.Map); + for (const missing of missingPaths) { + const value = map.get(missing); + assert.isTrue(value, `${missing} to be ${value === undefined ? "not present" : "present only once"}, in actual: ${missingPaths} expected: ${expected}`); + map.set(missing, false); } - - describe("unittests:: Program.getMissingFilePaths", () => { - - const options: CompilerOptions = { - noLib: true, - }; - - const emptyFileName = "empty.ts"; - const emptyFileRelativePath = "./" + emptyFileName; - - const emptyFile = new documents.TextDocument(emptyFileName, ""); - - const referenceFileName = "reference.ts"; - const referenceFileRelativePath = "./" + referenceFileName; - - const referenceFile = new documents.TextDocument(referenceFileName, - "/// \n" + // Absolute - "/// \n" + // Relative - "/// \n" + // Unqualified - "/// \n" // No extension - ); - - const testCompilerHost = new fakes.CompilerHost( - vfs.createFromFileSystem( - Harness.IO, - /*ignoreCase*/ true, - { documents: [emptyFile, referenceFile], cwd: "d:\\pretend\\" }), - { newLine: NewLineKind.LineFeed }); - - it("handles no missing root files", () => { - const program = createProgram([emptyFileRelativePath], options, testCompilerHost); - const missing = program.getMissingFilePaths(); - verifyMissingFilePaths(missing, []); - }); - - it("handles missing root file", () => { - const program = createProgram(["./nonexistent.ts"], options, testCompilerHost); - const missing = program.getMissingFilePaths(); - verifyMissingFilePaths(missing, ["d:/pretend/nonexistent.ts"]); // Absolute path - }); - - it("handles multiple missing root files", () => { - const program = createProgram(["./nonexistent0.ts", "./nonexistent1.ts"], options, testCompilerHost); - const missing = program.getMissingFilePaths(); - verifyMissingFilePaths(missing, ["d:/pretend/nonexistent0.ts", "d:/pretend/nonexistent1.ts"]); - }); - - it("handles a mix of present and missing root files", () => { - const program = createProgram(["./nonexistent0.ts", emptyFileRelativePath, "./nonexistent1.ts"], options, testCompilerHost); - const missing = program.getMissingFilePaths(); - verifyMissingFilePaths(missing, ["d:/pretend/nonexistent0.ts", "d:/pretend/nonexistent1.ts"]); - }); - - it("handles repeatedly specified root files", () => { - const program = createProgram(["./nonexistent.ts", "./nonexistent.ts"], options, testCompilerHost); - const missing = program.getMissingFilePaths(); - verifyMissingFilePaths(missing, ["d:/pretend/nonexistent.ts"]); - }); - - it("normalizes file paths", () => { - const program0 = createProgram(["./nonexistent.ts", "./NONEXISTENT.ts"], options, testCompilerHost); - const program1 = createProgram(["./NONEXISTENT.ts", "./nonexistent.ts"], options, testCompilerHost); - const missing0 = program0.getMissingFilePaths(); - const missing1 = program1.getMissingFilePaths(); - assert.equal(missing0.length, 1); - assert.deepEqual(missing0, missing1); - }); - - it("handles missing triple slash references", () => { - const program = createProgram([referenceFileRelativePath], options, testCompilerHost); - const missing = program.getMissingFilePaths(); - verifyMissingFilePaths(missing, [ - // From absolute reference - "d:/imaginary/nonexistent1.ts", - - // From relative reference - "d:/pretend/nonexistent2.ts", - - // From unqualified reference - "d:/pretend/nonexistent3.ts", - - // From no-extension reference - "d:/pretend/nonexistent4.d.ts", - "d:/pretend/nonexistent4.ts", - "d:/pretend/nonexistent4.tsx" - ]); - }); - - it("should not have missing file paths", () => { - const testSource = ` + const notFound = arrayFrom(mapDefinedIterator(map.keys(), k => map.get(k) === true ? k : undefined)); + assert.equal(notFound.length, 0, `Not found ${notFound} in actual: ${missingPaths} expected: ${expected}`); +} +describe("unittests:: Program.getMissingFilePaths", () => { + const options: CompilerOptions = { + noLib: true, + }; + const emptyFileName = "empty.ts"; + const emptyFileRelativePath = "./" + emptyFileName; + const emptyFile = new TextDocument(emptyFileName, ""); + const referenceFileName = "reference.ts"; + const referenceFileRelativePath = "./" + referenceFileName; + const referenceFile = new TextDocument(referenceFileName, "/// \n" + // Absolute + "/// \n" + // Relative + "/// \n" + // Unqualified + "/// \n" // No extension + ); + const testCompilerHost = new CompilerHost(createFromFileSystem(IO, + /*ignoreCase*/ true, { documents: [emptyFile, referenceFile], cwd: "d:\\pretend\\" }), { newLine: NewLineKind.LineFeed }); + it("handles no missing root files", () => { + const program = createProgram([emptyFileRelativePath], options, testCompilerHost); + const missing = program.getMissingFilePaths(); + verifyMissingFilePaths(missing, []); + }); + it("handles missing root file", () => { + const program = createProgram(["./nonexistent.ts"], options, testCompilerHost); + const missing = program.getMissingFilePaths(); + verifyMissingFilePaths(missing, ["d:/pretend/nonexistent.ts"]); // Absolute path + }); + it("handles multiple missing root files", () => { + const program = createProgram(["./nonexistent0.ts", "./nonexistent1.ts"], options, testCompilerHost); + const missing = program.getMissingFilePaths(); + verifyMissingFilePaths(missing, ["d:/pretend/nonexistent0.ts", "d:/pretend/nonexistent1.ts"]); + }); + it("handles a mix of present and missing root files", () => { + const program = createProgram(["./nonexistent0.ts", emptyFileRelativePath, "./nonexistent1.ts"], options, testCompilerHost); + const missing = program.getMissingFilePaths(); + verifyMissingFilePaths(missing, ["d:/pretend/nonexistent0.ts", "d:/pretend/nonexistent1.ts"]); + }); + it("handles repeatedly specified root files", () => { + const program = createProgram(["./nonexistent.ts", "./nonexistent.ts"], options, testCompilerHost); + const missing = program.getMissingFilePaths(); + verifyMissingFilePaths(missing, ["d:/pretend/nonexistent.ts"]); + }); + it("normalizes file paths", () => { + const program0 = createProgram(["./nonexistent.ts", "./NONEXISTENT.ts"], options, testCompilerHost); + const program1 = createProgram(["./NONEXISTENT.ts", "./nonexistent.ts"], options, testCompilerHost); + const missing0 = program0.getMissingFilePaths(); + const missing1 = program1.getMissingFilePaths(); + assert.equal(missing0.length, 1); + assert.deepEqual(missing0, missing1); + }); + it("handles missing triple slash references", () => { + const program = createProgram([referenceFileRelativePath], options, testCompilerHost); + const missing = program.getMissingFilePaths(); + verifyMissingFilePaths(missing, [ + // From absolute reference + "d:/imaginary/nonexistent1.ts", + // From relative reference + "d:/pretend/nonexistent2.ts", + // From unqualified reference + "d:/pretend/nonexistent3.ts", + // From no-extension reference + "d:/pretend/nonexistent4.d.ts", + "d:/pretend/nonexistent4.ts", + "d:/pretend/nonexistent4.tsx" + ]); + }); + it("should not have missing file paths", () => { + const testSource = ` class Foo extends HTMLElement { bar: string = 'baz'; }`; - - const host: CompilerHost = { - getSourceFile: (fileName: string, languageVersion: ScriptTarget, _onError?: (message: string) => void) => { - return fileName === "test.ts" ? createSourceFile(fileName, testSource, languageVersion) : undefined; - }, - getDefaultLibFileName: () => "", - writeFile: (_fileName, _content) => { throw new Error("unsupported"); }, - getCurrentDirectory: () => sys.getCurrentDirectory(), - getCanonicalFileName: fileName => sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(), - getNewLine: () => sys.newLine, - useCaseSensitiveFileNames: () => sys.useCaseSensitiveFileNames, - fileExists: fileName => fileName === "test.ts", - readFile: fileName => fileName === "test.ts" ? testSource : undefined, - resolveModuleNames: (_moduleNames: string[], _containingFile: string) => { throw new Error("unsupported"); }, - getDirectories: _path => { throw new Error("unsupported"); }, - }; - - const program = createProgram(["test.ts"], { module: ModuleKind.ES2015 }, host); - assert(program.getSourceFiles().length === 1, "expected 'getSourceFiles' length to be 1"); - assert(program.getMissingFilePaths().length === 0, "expected 'getMissingFilePaths' length to be 0"); - assert(program.getFileProcessingDiagnostics().getDiagnostics().length === 0, "expected 'getFileProcessingDiagnostics' length to be 0"); - }); + const host: ts.CompilerHost = { + getSourceFile: (fileName: string, languageVersion: ScriptTarget, _onError?: (message: string) => void) => { + return fileName === "test.ts" ? createSourceFile(fileName, testSource, languageVersion) : undefined; + }, + getDefaultLibFileName: () => "", + writeFile: (_fileName, _content) => { throw new Error("unsupported"); }, + getCurrentDirectory: () => sys.getCurrentDirectory(), + getCanonicalFileName: fileName => sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(), + getNewLine: () => sys.newLine, + useCaseSensitiveFileNames: () => sys.useCaseSensitiveFileNames, + fileExists: fileName => fileName === "test.ts", + readFile: fileName => fileName === "test.ts" ? testSource : undefined, + resolveModuleNames: (_moduleNames: string[], _containingFile: string) => { throw new Error("unsupported"); }, + getDirectories: _path => { throw new Error("unsupported"); }, + }; + const program = createProgram(["test.ts"], { module: ModuleKind.ES2015 }, host); + assert(program.getSourceFiles().length === 1, "expected 'getSourceFiles' length to be 1"); + assert(program.getMissingFilePaths().length === 0, "expected 'getMissingFilePaths' length to be 0"); + assert(program.getFileProcessingDiagnostics().getDiagnostics().length === 0, "expected 'getFileProcessingDiagnostics' length to be 0"); + }); +}); +describe("unittests:: Program.isSourceFileFromExternalLibrary", () => { + it("works on redirect files", () => { + // In this example '/node_modules/foo/index.d.ts' will redirect to '/node_modules/bar/node_modules/foo/index.d.ts'. + const a = new TextDocument("/a.ts", 'import * as bar from "bar"; import * as foo from "foo";'); + const bar = new TextDocument("/node_modules/bar/index.d.ts", 'import * as foo from "foo";'); + const fooPackageJsonText = '{ "name": "foo", "version": "1.2.3" }'; + const fooIndexText = "export const x: number;"; + const barFooPackage = new TextDocument("/node_modules/bar/node_modules/foo/package.json", fooPackageJsonText); + const barFooIndex = new TextDocument("/node_modules/bar/node_modules/foo/index.d.ts", fooIndexText); + const fooPackage = new TextDocument("/node_modules/foo/package.json", fooPackageJsonText); + const fooIndex = new TextDocument("/node_modules/foo/index.d.ts", fooIndexText); + const fs = createFromFileSystem(IO, /*ignoreCase*/ false, { documents: [a, bar, barFooPackage, barFooIndex, fooPackage, fooIndex], cwd: "/" }); + const program = createProgram(["/a.ts"], emptyOptions, new CompilerHost(fs, { newLine: NewLineKind.LineFeed })); + assertIsExternal(program, [a, bar, barFooIndex, fooIndex], f => f !== a); }); - - describe("unittests:: Program.isSourceFileFromExternalLibrary", () => { - it("works on redirect files", () => { - // In this example '/node_modules/foo/index.d.ts' will redirect to '/node_modules/bar/node_modules/foo/index.d.ts'. - const a = new documents.TextDocument("/a.ts", 'import * as bar from "bar"; import * as foo from "foo";'); - const bar = new documents.TextDocument("/node_modules/bar/index.d.ts", 'import * as foo from "foo";'); - const fooPackageJsonText = '{ "name": "foo", "version": "1.2.3" }'; - const fooIndexText = "export const x: number;"; - const barFooPackage = new documents.TextDocument("/node_modules/bar/node_modules/foo/package.json", fooPackageJsonText); - const barFooIndex = new documents.TextDocument("/node_modules/bar/node_modules/foo/index.d.ts", fooIndexText); - const fooPackage = new documents.TextDocument("/node_modules/foo/package.json", fooPackageJsonText); - const fooIndex = new documents.TextDocument("/node_modules/foo/index.d.ts", fooIndexText); - - const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false, { documents: [a, bar, barFooPackage, barFooIndex, fooPackage, fooIndex], cwd: "/" }); - const program = createProgram(["/a.ts"], emptyOptions, new fakes.CompilerHost(fs, { newLine: NewLineKind.LineFeed })); - assertIsExternal(program, [a, bar, barFooIndex, fooIndex], f => f !== a); - }); - - it('works on `/// `', () => { - const a = new documents.TextDocument("/a.ts", '/// '); - const fooIndex = new documents.TextDocument("/node_modules/foo/index.d.ts", "declare const foo: number;"); - const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false, { documents: [a, fooIndex], cwd: "/" }); - const program = createProgram(["/a.ts"], emptyOptions, new fakes.CompilerHost(fs, { newLine: NewLineKind.LineFeed })); - assertIsExternal(program, [a, fooIndex], f => f !== a); - }); - - function assertIsExternal(program: Program, files: readonly documents.TextDocument[], isExternalExpected: (file: documents.TextDocument) => boolean): void { - for (const file of files) { - const actual = program.isSourceFileFromExternalLibrary(program.getSourceFile(file.file)!); - const expected = isExternalExpected(file); - assert.equal(actual, expected, `Expected ${file.file} isSourceFileFromExternalLibrary to be ${expected}, got ${actual}`); - } + it('works on `/// `', () => { + const a = new TextDocument("/a.ts", '/// '); + const fooIndex = new TextDocument("/node_modules/foo/index.d.ts", "declare const foo: number;"); + const fs = createFromFileSystem(IO, /*ignoreCase*/ false, { documents: [a, fooIndex], cwd: "/" }); + const program = createProgram(["/a.ts"], emptyOptions, new CompilerHost(fs, { newLine: NewLineKind.LineFeed })); + assertIsExternal(program, [a, fooIndex], f => f !== a); + }); + function assertIsExternal(program: Program, files: readonly TextDocument[], isExternalExpected: (file: TextDocument) => boolean): void { + for (const file of files) { + const actual = program.isSourceFileFromExternalLibrary(program.getSourceFile(file.file)!); + const expected = isExternalExpected(file); + assert.equal(actual, expected, `Expected ${file.file} isSourceFileFromExternalLibrary to be ${expected}, got ${actual}`); } + } +}); +describe("unittests:: Program.getNodeCount / Program.getIdentifierCount", () => { + it("works on projects that have .json files", () => { + const main = new TextDocument("/main.ts", 'export { version } from "./package.json";'); + const pkg = new TextDocument("/package.json", '{"version": "1.0.0"}'); + const fs = createFromFileSystem(IO, /*ignoreCase*/ false, { documents: [main, pkg], cwd: "/" }); + const program = createProgram(["/main.ts"], { resolveJsonModule: true }, new CompilerHost(fs, { newLine: NewLineKind.LineFeed })); + const json = program.getSourceFile("/package.json")!; + assert.equal(json.scriptKind, ScriptKind.JSON); + assert.isNumber(json.nodeCount); + assert.isNumber(json.identifierCount); + assert.isNotNaN(program.getNodeCount()); + assert.isNotNaN(program.getIdentifierCount()); }); - - describe("unittests:: Program.getNodeCount / Program.getIdentifierCount", () => { - it("works on projects that have .json files", () => { - const main = new documents.TextDocument("/main.ts", 'export { version } from "./package.json";'); - const pkg = new documents.TextDocument("/package.json", '{"version": "1.0.0"}'); - - const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false, { documents: [main, pkg], cwd: "/" }); - const program = createProgram(["/main.ts"], { resolveJsonModule: true }, new fakes.CompilerHost(fs, { newLine: NewLineKind.LineFeed })); - - const json = program.getSourceFile("/package.json")!; - assert.equal(json.scriptKind, ScriptKind.JSON); - assert.isNumber(json.nodeCount); - assert.isNumber(json.identifierCount); - - assert.isNotNaN(program.getNodeCount()); - assert.isNotNaN(program.getIdentifierCount()); - }); +}); +describe("unittests:: programApi:: Program.getDiagnosticsProducingTypeChecker / Program.getSemanticDiagnostics", () => { + it("does not produce errors on `as const` it would not normally produce on the command line", () => { + const main = new TextDocument("/main.ts", "0 as const"); + const fs = createFromFileSystem(IO, /*ignoreCase*/ false, { documents: [main], cwd: "/" }); + const program = createProgram(["/main.ts"], {}, new CompilerHost(fs, { newLine: NewLineKind.LineFeed })); + const typeChecker = program.getDiagnosticsProducingTypeChecker(); + const sourceFile = program.getSourceFile("main.ts")!; + typeChecker.getTypeAtLocation(((sourceFile.statements[0] as ExpressionStatement).expression as AsExpression).type); + const diag = program.getSemanticDiagnostics(); + assert.isEmpty(diag); }); - - describe("unittests:: programApi:: Program.getDiagnosticsProducingTypeChecker / Program.getSemanticDiagnostics", () => { - it("does not produce errors on `as const` it would not normally produce on the command line", () => { - const main = new documents.TextDocument("/main.ts", "0 as const"); - - const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false, { documents: [main], cwd: "/" }); - const program = createProgram(["/main.ts"], {}, new fakes.CompilerHost(fs, { newLine: NewLineKind.LineFeed })); - const typeChecker = program.getDiagnosticsProducingTypeChecker(); - const sourceFile = program.getSourceFile("main.ts")!; - typeChecker.getTypeAtLocation(((sourceFile.statements[0] as ExpressionStatement).expression as AsExpression).type); - const diag = program.getSemanticDiagnostics(); - assert.isEmpty(diag); - }); - it("getSymbolAtLocation does not cause additional error to be added on module resolution failure", () => { - const main = new documents.TextDocument("/main.ts", "import \"./module\";"); - const mod = new documents.TextDocument("/module.d.ts", "declare const foo: any;"); - - const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false, { documents: [main, mod], cwd: "/" }); - const program = createProgram(["/main.ts"], {}, new fakes.CompilerHost(fs, { newLine: NewLineKind.LineFeed })); - - const sourceFile = program.getSourceFile("main.ts")!; - const typeChecker = program.getDiagnosticsProducingTypeChecker(); - typeChecker.getSymbolAtLocation((sourceFile.statements[0] as ImportDeclaration).moduleSpecifier); - assert.isEmpty(program.getSemanticDiagnostics()); - }); + it("getSymbolAtLocation does not cause additional error to be added on module resolution failure", () => { + const main = new TextDocument("/main.ts", "import \"./module\";"); + const mod = new TextDocument("/module.d.ts", "declare const foo: any;"); + const fs = createFromFileSystem(IO, /*ignoreCase*/ false, { documents: [main, mod], cwd: "/" }); + const program = createProgram(["/main.ts"], {}, new CompilerHost(fs, { newLine: NewLineKind.LineFeed })); + const sourceFile = program.getSourceFile("main.ts")!; + const typeChecker = program.getDiagnosticsProducingTypeChecker(); + typeChecker.getSymbolAtLocation((sourceFile.statements[0] as ImportDeclaration).moduleSpecifier); + assert.isEmpty(program.getSemanticDiagnostics()); }); -} +}); diff --git a/src/testRunner/unittests/publicApi.ts b/src/testRunner/unittests/publicApi.ts index c8f380129c6f0..87497b40ddffe 100644 --- a/src/testRunner/unittests/publicApi.ts +++ b/src/testRunner/unittests/publicApi.ts @@ -1,53 +1,53 @@ +import { IO, Baseline, Compiler } from "../Harness"; +import { createFromFileSystem, builtFolder, srcFolder } from "../vfs"; +import { System, CompilerHost } from "../fakes"; +import { compileFiles } from "../compiler"; +import { SyntaxKind, tokenToString, Debug, createPrivateIdentifier } from "../ts"; describe("Public APIs", () => { function verifyApi(fileName: string) { const builtFile = `built/local/${fileName}`; const api = `api/${fileName}`; let fileContent: string; before(() => { - fileContent = Harness.IO.readFile(builtFile)!; - if (!fileContent) throw new Error(`File ${fileName} was not present in built/local`); + fileContent = (IO.readFile(builtFile)!); + if (!fileContent) + throw new Error(`File ${fileName} was not present in built/local`); fileContent = fileContent.replace(/\r\n/g, "\n"); }); - it("should be acknowledged when they change", () => { - Harness.Baseline.runBaseline(api, fileContent); + Baseline.runBaseline(api, fileContent); }); - it("should compile", () => { - const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false); - fs.linkSync(`${vfs.builtFolder}/${fileName}`, `${vfs.srcFolder}/${fileName}`); - const sys = new fakes.System(fs); - const host = new fakes.CompilerHost(sys); - const result = compiler.compileFiles(host, [`${vfs.srcFolder}/${fileName}`], {}); - assert(!result.diagnostics || !result.diagnostics.length, Harness.Compiler.minimalDiagnosticsToString(result.diagnostics, /*pretty*/ true)); + const fs = createFromFileSystem(IO, /*ignoreCase*/ false); + fs.linkSync(`${builtFolder}/${fileName}`, `${srcFolder}/${fileName}`); + const sys = new System(fs); + const host = new CompilerHost(sys); + const result = compileFiles(host, [`${srcFolder}/${fileName}`], {}); + assert(!result.diagnostics || !result.diagnostics.length, Compiler.minimalDiagnosticsToString(result.diagnostics, /*pretty*/ true)); }); } - describe("for the language service and compiler", () => { verifyApi("typescript.d.ts"); }); - describe("for the language server", () => { verifyApi("tsserverlibrary.d.ts"); }); }); - describe("Public APIs:: token to string", () => { - function assertDefinedTokenToString(initial: ts.SyntaxKind, last: ts.SyntaxKind) { + function assertDefinedTokenToString(initial: SyntaxKind, last: SyntaxKind) { for (let t = initial; t <= last; t++) { - assert.isDefined(ts.tokenToString(t), `Expected tokenToString defined for ${ts.Debug.formatSyntaxKind(t)}`); + assert.isDefined(tokenToString(t), `Expected tokenToString defined for ${Debug.formatSyntaxKind(t)}`); } } - it("for punctuations", () => { - assertDefinedTokenToString(ts.SyntaxKind.FirstPunctuation, ts.SyntaxKind.LastPunctuation); + assertDefinedTokenToString(SyntaxKind.FirstPunctuation, SyntaxKind.LastPunctuation); }); it("for keywords", () => { - assertDefinedTokenToString(ts.SyntaxKind.FirstKeyword, ts.SyntaxKind.LastKeyword); + assertDefinedTokenToString(SyntaxKind.FirstKeyword, SyntaxKind.LastKeyword); }); }); describe("Public APIs:: createPrivateIdentifier", () => { it("throws when name doesn't start with #", () => { - assert.throw(() => ts.createPrivateIdentifier("not"), "Debug Failure. First character of private identifier must be #: not"); + assert.throw(() => createPrivateIdentifier("not"), "Debug Failure. First character of private identifier must be #: not"); }); }); diff --git a/src/testRunner/unittests/reuseProgramStructure.ts b/src/testRunner/unittests/reuseProgramStructure.ts index bcc8a12adf6fb..57e29bfbbe968 100644 --- a/src/testRunner/unittests/reuseProgramStructure.ts +++ b/src/testRunner/unittests/reuseProgramStructure.ts @@ -1,1140 +1,993 @@ -namespace ts { - - const enum ChangedPart { - references = 1 << 0, - importsAndExports = 1 << 1, - program = 1 << 2 +import { SourceFile, Program, CompilerHost, IScriptSnapshot, Debug, TextChangeRange, TextSpan, createTextSpan, createTextChangeRange, ScriptTarget, createSourceFile, arrayToMap, sys, createGetCanonicalFileName, notImplemented, mapEntries, toPath, CompilerOptions, createProgram, find, ResolvedTypeReferenceDirective, forEachEntry, forEachKey, ResolvedModule, checkResolvedModule, StructureIsReused, noop, ModuleKind, emptyArray, createMapFromTemplate, createResolvedModule, updateSourceFile, ModuleResolutionKind, TestFSWithWatch, isProgramUptoDate, returnFalse, System, createWatchProgram, createWatchCompilerHostOfFilesAndCompilerOptions, createWatchCompilerHostOfConfigFile, parseConfigFileWithSystem } from "../ts"; +import * as ts from "../ts"; +const enum ChangedPart { + references = 1 << 0, + importsAndExports = 1 << 1, + program = 1 << 2 +} +const newLine = "\r\n"; +interface SourceFileWithText extends SourceFile { + sourceText?: SourceText; +} +export interface NamedSourceText { + name: string; + text: SourceText; +} +export interface ProgramWithSourceTexts extends Program { + sourceTexts?: readonly NamedSourceText[]; + host: TestCompilerHost; +} +interface TestCompilerHost extends CompilerHost { + getTrace(): string[]; +} +export class SourceText implements IScriptSnapshot { + private fullText: string | undefined; + constructor(private references: string, private importsAndExports: string, private program: string, private changedPart: ChangedPart = 0, private version = 0) { } - - const newLine = "\r\n"; - - interface SourceFileWithText extends SourceFile { - sourceText?: SourceText; + static New(references: string, importsAndExports: string, program: string): SourceText { + Debug.assert(references !== undefined); + Debug.assert(importsAndExports !== undefined); + Debug.assert(program !== undefined); + return new SourceText(references + newLine, importsAndExports + newLine, program || ""); } - - export interface NamedSourceText { - name: string; - text: SourceText; + public getVersion(): number { + return this.version; } - - export interface ProgramWithSourceTexts extends Program { - sourceTexts?: readonly NamedSourceText[]; - host: TestCompilerHost; + public updateReferences(newReferences: string): SourceText { + Debug.assert(newReferences !== undefined); + return new SourceText(newReferences + newLine, this.importsAndExports, this.program, this.changedPart | ChangedPart.references, this.version + 1); } - - interface TestCompilerHost extends CompilerHost { - getTrace(): string[]; + public updateImportsAndExports(newImportsAndExports: string): SourceText { + Debug.assert(newImportsAndExports !== undefined); + return new SourceText(this.references, newImportsAndExports + newLine, this.program, this.changedPart | ChangedPart.importsAndExports, this.version + 1); } - - export class SourceText implements IScriptSnapshot { - private fullText: string | undefined; - - constructor(private references: string, - private importsAndExports: string, - private program: string, - private changedPart: ChangedPart = 0, - private version = 0) { - } - - static New(references: string, importsAndExports: string, program: string): SourceText { - Debug.assert(references !== undefined); - Debug.assert(importsAndExports !== undefined); - Debug.assert(program !== undefined); - return new SourceText(references + newLine, importsAndExports + newLine, program || ""); - } - - public getVersion(): number { - return this.version; - } - - public updateReferences(newReferences: string): SourceText { - Debug.assert(newReferences !== undefined); - return new SourceText(newReferences + newLine, this.importsAndExports, this.program, this.changedPart | ChangedPart.references, this.version + 1); - } - public updateImportsAndExports(newImportsAndExports: string): SourceText { - Debug.assert(newImportsAndExports !== undefined); - return new SourceText(this.references, newImportsAndExports + newLine, this.program, this.changedPart | ChangedPart.importsAndExports, this.version + 1); - } - public updateProgram(newProgram: string): SourceText { - Debug.assert(newProgram !== undefined); - return new SourceText(this.references, this.importsAndExports, newProgram, this.changedPart | ChangedPart.program, this.version + 1); - } - - public getFullText() { - return this.fullText || (this.fullText = this.references + this.importsAndExports + this.program); - } - - public getText(start: number, end: number): string { - return this.getFullText().substring(start, end); - } - - getLength(): number { - return this.getFullText().length; - } - - getChangeRange(oldSnapshot: IScriptSnapshot): TextChangeRange { - const oldText = oldSnapshot; - let oldSpan: TextSpan; - let newLength: number; - switch (oldText.changedPart ^ this.changedPart) { - case ChangedPart.references: - oldSpan = createTextSpan(0, oldText.references.length); - newLength = this.references.length; - break; - case ChangedPart.importsAndExports: - oldSpan = createTextSpan(oldText.references.length, oldText.importsAndExports.length); - newLength = this.importsAndExports.length; - break; - case ChangedPart.program: - oldSpan = createTextSpan(oldText.references.length + oldText.importsAndExports.length, oldText.program.length); - newLength = this.program.length; - break; - default: - return Debug.fail("Unexpected change"); - } - - return createTextChangeRange(oldSpan, newLength); - } + public updateProgram(newProgram: string): SourceText { + Debug.assert(newProgram !== undefined); + return new SourceText(this.references, this.importsAndExports, newProgram, this.changedPart | ChangedPart.program, this.version + 1); } - - function createSourceFileWithText(fileName: string, sourceText: SourceText, target: ScriptTarget) { - const file = createSourceFile(fileName, sourceText.getFullText(), target); - file.sourceText = sourceText; - file.version = "" + sourceText.getVersion(); - return file; + public getFullText() { + return this.fullText || (this.fullText = this.references + this.importsAndExports + this.program); } - - export function createTestCompilerHost(texts: readonly NamedSourceText[], target: ScriptTarget, oldProgram?: ProgramWithSourceTexts, useGetSourceFileByPath?: boolean) { - const files = arrayToMap(texts, t => t.name, t => { - if (oldProgram) { - let oldFile = oldProgram.getSourceFile(t.name); - if (oldFile && oldFile.redirectInfo) { - oldFile = oldFile.redirectInfo.unredirected; - } - if (oldFile && oldFile.sourceText!.getVersion() === t.text.getVersion()) { - return oldFile; - } - } - return createSourceFileWithText(t.name, t.text, target); - }); - const useCaseSensitiveFileNames = sys && sys.useCaseSensitiveFileNames; - const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); - const trace: string[] = []; - const result: TestCompilerHost = { - trace: s => trace.push(s), - getTrace: () => trace, - getSourceFile: fileName => files.get(fileName), - getDefaultLibFileName: () => "lib.d.ts", - writeFile: notImplemented, - getCurrentDirectory: () => "", - getDirectories: () => [], - getCanonicalFileName, - useCaseSensitiveFileNames: () => useCaseSensitiveFileNames, - getNewLine: () => sys ? sys.newLine : newLine, - fileExists: fileName => files.has(fileName), - readFile: fileName => { - const file = files.get(fileName); - return file && file.text; - }, - }; - if (useGetSourceFileByPath) { - const filesByPath = mapEntries(files, (fileName, file) => [toPath(fileName, "", getCanonicalFileName), file]); - result.getSourceFileByPath = (_fileName, path) => filesByPath.get(path); - } - return result; + public getText(start: number, end: number): string { + return this.getFullText().substring(start, end); } - - export function newProgram(texts: NamedSourceText[], rootNames: string[], options: CompilerOptions, useGetSourceFileByPath?: boolean): ProgramWithSourceTexts { - const host = createTestCompilerHost(texts, options.target!, /*oldProgram*/ undefined, useGetSourceFileByPath); - const program = createProgram(rootNames, options, host); - program.sourceTexts = texts; - program.host = host; - return program; + getLength(): number { + return this.getFullText().length; } - - export function updateProgram(oldProgram: ProgramWithSourceTexts, rootNames: readonly string[], options: CompilerOptions, updater: (files: NamedSourceText[]) => void, newTexts?: NamedSourceText[], useGetSourceFileByPath?: boolean) { - if (!newTexts) { - newTexts = oldProgram.sourceTexts!.slice(0); + getChangeRange(oldSnapshot: IScriptSnapshot): TextChangeRange { + const oldText = oldSnapshot; + let oldSpan: TextSpan; + let newLength: number; + switch (oldText.changedPart ^ this.changedPart) { + case ChangedPart.references: + oldSpan = createTextSpan(0, oldText.references.length); + newLength = this.references.length; + break; + case ChangedPart.importsAndExports: + oldSpan = createTextSpan(oldText.references.length, oldText.importsAndExports.length); + newLength = this.importsAndExports.length; + break; + case ChangedPart.program: + oldSpan = createTextSpan(oldText.references.length + oldText.importsAndExports.length, oldText.program.length); + newLength = this.program.length; + break; + default: + return Debug.fail("Unexpected change"); } - updater(newTexts); - const host = createTestCompilerHost(newTexts, options.target!, oldProgram, useGetSourceFileByPath); - const program = createProgram(rootNames, options, host, oldProgram); - program.sourceTexts = newTexts; - program.host = host; - return program; - } - - export function updateProgramText(files: readonly NamedSourceText[], fileName: string, newProgramText: string) { - const file = find(files, f => f.name === fileName)!; - file.text = file.text.updateProgram(newProgramText); + return createTextChangeRange(oldSpan, newLength); } - - function checkResolvedTypeDirective(actual: ResolvedTypeReferenceDirective, expected: ResolvedTypeReferenceDirective) { - assert.equal(actual.resolvedFileName, expected.resolvedFileName, `'resolvedFileName': expected '${actual.resolvedFileName}' to be equal to '${expected.resolvedFileName}'`); - assert.equal(actual.primary, expected.primary, `'primary': expected '${actual.primary}' to be equal to '${expected.primary}'`); - return true; - } - - function checkCache(caption: string, program: Program, fileName: string, expectedContent: Map | undefined, getCache: (f: SourceFile) => Map | undefined, entryChecker: (expected: T, original: T) => boolean): void { - const file = program.getSourceFile(fileName); - assert.isTrue(file !== undefined, `cannot find file ${fileName}`); - const cache = getCache(file!); - if (expectedContent === undefined) { - assert.isTrue(cache === undefined, `expected ${caption} to be undefined`); - } - else { - assert.isTrue(cache !== undefined, `expected ${caption} to be set`); - assert.isTrue(mapsAreEqual(expectedContent, cache!, entryChecker), `contents of ${caption} did not match the expected contents.`); +} +function createSourceFileWithText(fileName: string, sourceText: SourceText, target: ScriptTarget) { + const file = (createSourceFile(fileName, sourceText.getFullText(), target)); + file.sourceText = sourceText; + file.version = "" + sourceText.getVersion(); + return file; +} +export function createTestCompilerHost(texts: readonly NamedSourceText[], target: ScriptTarget, oldProgram?: ProgramWithSourceTexts, useGetSourceFileByPath?: boolean) { + const files = arrayToMap(texts, t => t.name, t => { + if (oldProgram) { + let oldFile = oldProgram.getSourceFile(t.name); + if (oldFile && oldFile.redirectInfo) { + oldFile = oldFile.redirectInfo.unredirected; + } + if (oldFile && oldFile.sourceText!.getVersion() === t.text.getVersion()) { + return oldFile; + } } + return createSourceFileWithText(t.name, t.text, target); + }); + const useCaseSensitiveFileNames = sys && sys.useCaseSensitiveFileNames; + const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); + const trace: string[] = []; + const result: TestCompilerHost = { + trace: s => trace.push(s), + getTrace: () => trace, + getSourceFile: fileName => files.get(fileName), + getDefaultLibFileName: () => "lib.d.ts", + writeFile: notImplemented, + getCurrentDirectory: () => "", + getDirectories: () => [], + getCanonicalFileName, + useCaseSensitiveFileNames: () => useCaseSensitiveFileNames, + getNewLine: () => sys ? sys.newLine : newLine, + fileExists: fileName => files.has(fileName), + readFile: fileName => { + const file = files.get(fileName); + return file && file.text; + }, + }; + if (useGetSourceFileByPath) { + const filesByPath = mapEntries(files, (fileName, file) => [toPath(fileName, "", getCanonicalFileName), file]); + result.getSourceFileByPath = (_fileName, path) => filesByPath.get(path); } - - /** True if the maps have the same keys and values. */ - function mapsAreEqual(left: Map, right: Map, valuesAreEqual?: (left: T, right: T) => boolean): boolean { - if (left === right) return true; - if (!left || !right) return false; - const someInLeftHasNoMatch = forEachEntry(left, (leftValue, leftKey) => { - if (!right.has(leftKey)) return true; - const rightValue = right.get(leftKey)!; - return !(valuesAreEqual ? valuesAreEqual(leftValue, rightValue) : leftValue === rightValue); - }); - if (someInLeftHasNoMatch) return false; - const someInRightHasNoMatch = forEachKey(right, rightKey => !left.has(rightKey)); - return !someInRightHasNoMatch; + return result; +} +export function newProgram(texts: NamedSourceText[], rootNames: string[], options: CompilerOptions, useGetSourceFileByPath?: boolean): ProgramWithSourceTexts { + const host = createTestCompilerHost(texts, options.target!, /*oldProgram*/ undefined, useGetSourceFileByPath); + const program = (createProgram(rootNames, options, host)); + program.sourceTexts = texts; + program.host = host; + return program; +} +export function updateProgram(oldProgram: ProgramWithSourceTexts, rootNames: readonly string[], options: CompilerOptions, updater: (files: NamedSourceText[]) => void, newTexts?: NamedSourceText[], useGetSourceFileByPath?: boolean) { + if (!newTexts) { + newTexts = oldProgram.sourceTexts!.slice(0); } - - function checkResolvedModulesCache(program: Program, fileName: string, expectedContent: Map | undefined): void { - checkCache("resolved modules", program, fileName, expectedContent, f => f.resolvedModules, checkResolvedModule); + updater(newTexts); + const host = createTestCompilerHost(newTexts, options.target!, oldProgram, useGetSourceFileByPath); + const program = (createProgram(rootNames, options, host, oldProgram)); + program.sourceTexts = newTexts; + program.host = host; + return program; +} +export function updateProgramText(files: readonly NamedSourceText[], fileName: string, newProgramText: string) { + const file = (find(files, f => f.name === fileName)!); + file.text = file.text.updateProgram(newProgramText); +} +function checkResolvedTypeDirective(actual: ResolvedTypeReferenceDirective, expected: ResolvedTypeReferenceDirective) { + assert.equal(actual.resolvedFileName, expected.resolvedFileName, `'resolvedFileName': expected '${actual.resolvedFileName}' to be equal to '${expected.resolvedFileName}'`); + assert.equal(actual.primary, expected.primary, `'primary': expected '${actual.primary}' to be equal to '${expected.primary}'`); + return true; +} +function checkCache(caption: string, program: Program, fileName: string, expectedContent: ts.Map | undefined, getCache: (f: SourceFile) => ts.Map | undefined, entryChecker: (expected: T, original: T) => boolean): void { + const file = program.getSourceFile(fileName); + assert.isTrue(file !== undefined, `cannot find file ${fileName}`); + const cache = getCache(file!); + if (expectedContent === undefined) { + assert.isTrue(cache === undefined, `expected ${caption} to be undefined`); } - - function checkResolvedTypeDirectivesCache(program: Program, fileName: string, expectedContent: Map | undefined): void { - checkCache("resolved type directives", program, fileName, expectedContent, f => f.resolvedTypeReferenceDirectiveNames, checkResolvedTypeDirective); + else { + assert.isTrue(cache !== undefined, `expected ${caption} to be set`); + assert.isTrue(mapsAreEqual(expectedContent, cache!, entryChecker), `contents of ${caption} did not match the expected contents.`); } - - describe("unittests:: Reuse program structure:: General", () => { - const target = ScriptTarget.Latest; - const files: NamedSourceText[] = [ - { - name: "a.ts", text: SourceText.New( - ` +} +/** True if the maps have the same keys and values. */ +function mapsAreEqual(left: ts.Map, right: ts.Map, valuesAreEqual?: (left: T, right: T) => boolean): boolean { + if (left === right) + return true; + if (!left || !right) + return false; + const someInLeftHasNoMatch = forEachEntry(left, (leftValue, leftKey) => { + if (!right.has(leftKey)) + return true; + const rightValue = right.get(leftKey)!; + return !(valuesAreEqual ? valuesAreEqual(leftValue, rightValue) : leftValue === rightValue); + }); + if (someInLeftHasNoMatch) + return false; + const someInRightHasNoMatch = forEachKey(right, rightKey => !left.has(rightKey)); + return !someInRightHasNoMatch; +} +function checkResolvedModulesCache(program: Program, fileName: string, expectedContent: ts.Map | undefined): void { + checkCache("resolved modules", program, fileName, expectedContent, f => f.resolvedModules, checkResolvedModule); +} +function checkResolvedTypeDirectivesCache(program: Program, fileName: string, expectedContent: ts.Map | undefined): void { + checkCache("resolved type directives", program, fileName, expectedContent, f => f.resolvedTypeReferenceDirectiveNames, checkResolvedTypeDirective); +} +describe("unittests:: Reuse program structure:: General", () => { + const target = ScriptTarget.Latest; + const files: NamedSourceText[] = [ + { + name: "a.ts", text: SourceText.New(` /// /// /// `, "", `var x = 1`) - }, - { name: "b.ts", text: SourceText.New(`/// `, "", `var y = 2`) }, - { name: "c.ts", text: SourceText.New("", "", `var z = 1;`) }, - { name: "types/typerefs/index.d.ts", text: SourceText.New("", "", `declare let z: number;`) }, - ]; - - it("successful if change does not affect imports", () => { - const program1 = newProgram(files, ["a.ts"], { target }); - const program2 = updateProgram(program1, ["a.ts"], { target }, files => { - files[0].text = files[0].text.updateProgram("var x = 100"); - }); - assert.equal(program1.structureIsReused, StructureIsReused.Completely); - const program1Diagnostics = program1.getSemanticDiagnostics(program1.getSourceFile("a.ts")); - const program2Diagnostics = program2.getSemanticDiagnostics(program1.getSourceFile("a.ts")); - assert.equal(program1Diagnostics.length, program2Diagnostics.length); + }, + { name: "b.ts", text: SourceText.New(`/// `, "", `var y = 2`) }, + { name: "c.ts", text: SourceText.New("", "", `var z = 1;`) }, + { name: "types/typerefs/index.d.ts", text: SourceText.New("", "", `declare let z: number;`) }, + ]; + it("successful if change does not affect imports", () => { + const program1 = newProgram(files, ["a.ts"], { target }); + const program2 = updateProgram(program1, ["a.ts"], { target }, files => { + files[0].text = files[0].text.updateProgram("var x = 100"); }); - - it("successful if change does not affect type reference directives", () => { - const program1 = newProgram(files, ["a.ts"], { target }); - const program2 = updateProgram(program1, ["a.ts"], { target }, files => { - files[0].text = files[0].text.updateProgram("var x = 100"); - }); - assert.equal(program1.structureIsReused, StructureIsReused.Completely); - const program1Diagnostics = program1.getSemanticDiagnostics(program1.getSourceFile("a.ts")); - const program2Diagnostics = program2.getSemanticDiagnostics(program1.getSourceFile("a.ts")); - assert.equal(program1Diagnostics.length, program2Diagnostics.length); + assert.equal(program1.structureIsReused, StructureIsReused.Completely); + const program1Diagnostics = program1.getSemanticDiagnostics(program1.getSourceFile("a.ts")); + const program2Diagnostics = program2.getSemanticDiagnostics(program1.getSourceFile("a.ts")); + assert.equal(program1Diagnostics.length, program2Diagnostics.length); + }); + it("successful if change does not affect type reference directives", () => { + const program1 = newProgram(files, ["a.ts"], { target }); + const program2 = updateProgram(program1, ["a.ts"], { target }, files => { + files[0].text = files[0].text.updateProgram("var x = 100"); }); - - it("fails if change affects tripleslash references", () => { - const program1 = newProgram(files, ["a.ts"], { target }); - updateProgram(program1, ["a.ts"], { target }, files => { - const newReferences = `/// + assert.equal(program1.structureIsReused, StructureIsReused.Completely); + const program1Diagnostics = program1.getSemanticDiagnostics(program1.getSourceFile("a.ts")); + const program2Diagnostics = program2.getSemanticDiagnostics(program1.getSourceFile("a.ts")); + assert.equal(program1Diagnostics.length, program2Diagnostics.length); + }); + it("fails if change affects tripleslash references", () => { + const program1 = newProgram(files, ["a.ts"], { target }); + updateProgram(program1, ["a.ts"], { target }, files => { + const newReferences = `/// /// `; - files[0].text = files[0].text.updateReferences(newReferences); - }); - assert.equal(program1.structureIsReused, StructureIsReused.SafeModules); - }); - - it("fails if change affects type references", () => { - const program1 = newProgram(files, ["a.ts"], { types: ["a"] }); - updateProgram(program1, ["a.ts"], { types: ["b"] }, noop); - assert.equal(program1.structureIsReused, StructureIsReused.Not); + files[0].text = files[0].text.updateReferences(newReferences); }); - - it("succeeds if change doesn't affect type references", () => { - const program1 = newProgram(files, ["a.ts"], { types: ["a"] }); - updateProgram(program1, ["a.ts"], { types: ["a"] }, noop); - assert.equal(program1.structureIsReused, StructureIsReused.Completely); - }); - - it("fails if change affects imports", () => { - const program1 = newProgram(files, ["a.ts"], { target }); - updateProgram(program1, ["a.ts"], { target }, files => { - files[2].text = files[2].text.updateImportsAndExports("import x from 'b'"); - }); - assert.equal(program1.structureIsReused, StructureIsReused.SafeModules); + assert.equal(program1.structureIsReused, StructureIsReused.SafeModules); + }); + it("fails if change affects type references", () => { + const program1 = newProgram(files, ["a.ts"], { types: ["a"] }); + updateProgram(program1, ["a.ts"], { types: ["b"] }, noop); + assert.equal(program1.structureIsReused, StructureIsReused.Not); + }); + it("succeeds if change doesn't affect type references", () => { + const program1 = newProgram(files, ["a.ts"], { types: ["a"] }); + updateProgram(program1, ["a.ts"], { types: ["a"] }, noop); + assert.equal(program1.structureIsReused, StructureIsReused.Completely); + }); + it("fails if change affects imports", () => { + const program1 = newProgram(files, ["a.ts"], { target }); + updateProgram(program1, ["a.ts"], { target }, files => { + files[2].text = files[2].text.updateImportsAndExports("import x from 'b'"); }); - - it("fails if change affects type directives", () => { - const program1 = newProgram(files, ["a.ts"], { target }); - updateProgram(program1, ["a.ts"], { target }, files => { - const newReferences = ` + assert.equal(program1.structureIsReused, StructureIsReused.SafeModules); + }); + it("fails if change affects type directives", () => { + const program1 = newProgram(files, ["a.ts"], { target }); + updateProgram(program1, ["a.ts"], { target }, files => { + const newReferences = ` /// /// /// `; - files[0].text = files[0].text.updateReferences(newReferences); - }); - assert.equal(program1.structureIsReused, StructureIsReused.SafeModules); + files[0].text = files[0].text.updateReferences(newReferences); }); - - it("fails if module kind changes", () => { - const program1 = newProgram(files, ["a.ts"], { target, module: ModuleKind.CommonJS }); - updateProgram(program1, ["a.ts"], { target, module: ModuleKind.AMD }, noop); - assert.equal(program1.structureIsReused, StructureIsReused.Not); - }); - - it("succeeds if rootdir changes", () => { - const program1 = newProgram(files, ["a.ts"], { target, module: ModuleKind.CommonJS, rootDir: "/a/b" }); - updateProgram(program1, ["a.ts"], { target, module: ModuleKind.CommonJS, rootDir: "/a/c" }, noop); - assert.equal(program1.structureIsReused, StructureIsReused.Completely); - }); - - it("fails if config path changes", () => { - const program1 = newProgram(files, ["a.ts"], { target, module: ModuleKind.CommonJS, configFilePath: "/a/b/tsconfig.json" }); - updateProgram(program1, ["a.ts"], { target, module: ModuleKind.CommonJS, configFilePath: "/a/c/tsconfig.json" }, noop); - assert.equal(program1.structureIsReused, StructureIsReused.Not); - }); - - it("succeeds if missing files remain missing", () => { - const options: CompilerOptions = { target, noLib: true }; - - const program1 = newProgram(files, ["a.ts"], options); - assert.notDeepEqual(emptyArray, program1.getMissingFilePaths()); - - const program2 = updateProgram(program1, ["a.ts"], options, noop); - assert.deepEqual(program1.getMissingFilePaths(), program2.getMissingFilePaths()); - - assert.equal(StructureIsReused.Completely, program1.structureIsReused); + assert.equal(program1.structureIsReused, StructureIsReused.SafeModules); + }); + it("fails if module kind changes", () => { + const program1 = newProgram(files, ["a.ts"], { target, module: ModuleKind.CommonJS }); + updateProgram(program1, ["a.ts"], { target, module: ModuleKind.AMD }, noop); + assert.equal(program1.structureIsReused, StructureIsReused.Not); + }); + it("succeeds if rootdir changes", () => { + const program1 = newProgram(files, ["a.ts"], { target, module: ModuleKind.CommonJS, rootDir: "/a/b" }); + updateProgram(program1, ["a.ts"], { target, module: ModuleKind.CommonJS, rootDir: "/a/c" }, noop); + assert.equal(program1.structureIsReused, StructureIsReused.Completely); + }); + it("fails if config path changes", () => { + const program1 = newProgram(files, ["a.ts"], { target, module: ModuleKind.CommonJS, configFilePath: "/a/b/tsconfig.json" }); + updateProgram(program1, ["a.ts"], { target, module: ModuleKind.CommonJS, configFilePath: "/a/c/tsconfig.json" }, noop); + assert.equal(program1.structureIsReused, StructureIsReused.Not); + }); + it("succeeds if missing files remain missing", () => { + const options: CompilerOptions = { target, noLib: true }; + const program1 = newProgram(files, ["a.ts"], options); + assert.notDeepEqual(emptyArray, program1.getMissingFilePaths()); + const program2 = updateProgram(program1, ["a.ts"], options, noop); + assert.deepEqual(program1.getMissingFilePaths(), program2.getMissingFilePaths()); + assert.equal(StructureIsReused.Completely, program1.structureIsReused); + }); + it("fails if missing file is created", () => { + const options: CompilerOptions = { target, noLib: true }; + const program1 = newProgram(files, ["a.ts"], options); + assert.notDeepEqual(emptyArray, program1.getMissingFilePaths()); + const newTexts: NamedSourceText[] = files.concat([{ name: "non-existing-file.ts", text: SourceText.New("", "", `var x = 1`) }]); + const program2 = updateProgram(program1, ["a.ts"], options, noop, newTexts); + assert.lengthOf(program2.getMissingFilePaths(), 0); + assert.equal(StructureIsReused.Not, program1.structureIsReused); + }); + it("resolution cache follows imports", () => { + (Error).stackTraceLimit = Infinity; + const files = [ + { name: "a.ts", text: SourceText.New("", "import {_} from 'b'", "var x = 1") }, + { name: "b.ts", text: SourceText.New("", "", "var y = 2") }, + ]; + const options: CompilerOptions = { target }; + const program1 = newProgram(files, ["a.ts"], options); + checkResolvedModulesCache(program1, "a.ts", createMapFromTemplate({ b: createResolvedModule("b.ts") })); + checkResolvedModulesCache(program1, "b.ts", /*expectedContent*/ undefined); + const program2 = updateProgram(program1, ["a.ts"], options, files => { + files[0].text = files[0].text.updateProgram("var x = 2"); }); - - it("fails if missing file is created", () => { - const options: CompilerOptions = { target, noLib: true }; - - const program1 = newProgram(files, ["a.ts"], options); - assert.notDeepEqual(emptyArray, program1.getMissingFilePaths()); - - const newTexts: NamedSourceText[] = files.concat([{ name: "non-existing-file.ts", text: SourceText.New("", "", `var x = 1`) }]); - const program2 = updateProgram(program1, ["a.ts"], options, noop, newTexts); - assert.lengthOf(program2.getMissingFilePaths(), 0); - - assert.equal(StructureIsReused.Not, program1.structureIsReused); + assert.equal(program1.structureIsReused, StructureIsReused.Completely); + // content of resolution cache should not change + checkResolvedModulesCache(program1, "a.ts", createMapFromTemplate({ b: createResolvedModule("b.ts") })); + checkResolvedModulesCache(program1, "b.ts", /*expectedContent*/ undefined); + // imports has changed - program is not reused + const program3 = updateProgram(program2, ["a.ts"], options, files => { + files[0].text = files[0].text.updateImportsAndExports(""); }); - - it("resolution cache follows imports", () => { - (Error).stackTraceLimit = Infinity; - - const files = [ - { name: "a.ts", text: SourceText.New("", "import {_} from 'b'", "var x = 1") }, - { name: "b.ts", text: SourceText.New("", "", "var y = 2") }, - ]; - const options: CompilerOptions = { target }; - - const program1 = newProgram(files, ["a.ts"], options); - checkResolvedModulesCache(program1, "a.ts", createMapFromTemplate({ b: createResolvedModule("b.ts") })); - checkResolvedModulesCache(program1, "b.ts", /*expectedContent*/ undefined); - - const program2 = updateProgram(program1, ["a.ts"], options, files => { - files[0].text = files[0].text.updateProgram("var x = 2"); - }); - assert.equal(program1.structureIsReused, StructureIsReused.Completely); - - // content of resolution cache should not change - checkResolvedModulesCache(program1, "a.ts", createMapFromTemplate({ b: createResolvedModule("b.ts") })); - checkResolvedModulesCache(program1, "b.ts", /*expectedContent*/ undefined); - - // imports has changed - program is not reused - const program3 = updateProgram(program2, ["a.ts"], options, files => { - files[0].text = files[0].text.updateImportsAndExports(""); - }); - assert.equal(program2.structureIsReused, StructureIsReused.SafeModules); - checkResolvedModulesCache(program3, "a.ts", /*expectedContent*/ undefined); - - const program4 = updateProgram(program3, ["a.ts"], options, files => { - const newImports = `import x from 'b' + assert.equal(program2.structureIsReused, StructureIsReused.SafeModules); + checkResolvedModulesCache(program3, "a.ts", /*expectedContent*/ undefined); + const program4 = updateProgram(program3, ["a.ts"], options, files => { + const newImports = `import x from 'b' import y from 'c' `; - files[0].text = files[0].text.updateImportsAndExports(newImports); - }); - assert.equal(program3.structureIsReused, StructureIsReused.SafeModules); - checkResolvedModulesCache(program4, "a.ts", createMapFromTemplate({ b: createResolvedModule("b.ts"), c: undefined })); + files[0].text = files[0].text.updateImportsAndExports(newImports); }); - - it("set the resolvedImports after re-using an ambient external module declaration", () => { - const files = [ - { name: "/a.ts", text: SourceText.New("", "", 'import * as a from "a";') }, - { name: "/types/zzz/index.d.ts", text: SourceText.New("", "", 'declare module "a" { }') }, - ]; - const options: CompilerOptions = { target, typeRoots: ["/types"] }; - const program1 = newProgram(files, ["/a.ts"], options); - const program2 = updateProgram(program1, ["/a.ts"], options, files => { - files[0].text = files[0].text.updateProgram('import * as aa from "a";'); - }); - assert.isDefined(program2.getSourceFile("/a.ts")!.resolvedModules!.get("a"), "'a' is not an unresolved module after re-use"); + assert.equal(program3.structureIsReused, StructureIsReused.SafeModules); + checkResolvedModulesCache(program4, "a.ts", createMapFromTemplate({ b: createResolvedModule("b.ts"), c: undefined })); + }); + it("set the resolvedImports after re-using an ambient external module declaration", () => { + const files = [ + { name: "/a.ts", text: SourceText.New("", "", 'import * as a from "a";') }, + { name: "/types/zzz/index.d.ts", text: SourceText.New("", "", 'declare module "a" { }') }, + ]; + const options: CompilerOptions = { target, typeRoots: ["/types"] }; + const program1 = newProgram(files, ["/a.ts"], options); + const program2 = updateProgram(program1, ["/a.ts"], options, files => { + files[0].text = files[0].text.updateProgram('import * as aa from "a";'); }); - - it("works with updated SourceFiles", () => { - // adapted repro from https://github.com/Microsoft/TypeScript/issues/26166 - const files = [ - { name: "/a.ts", text: SourceText.New("", "", 'import * as a from "a";a;') }, - { name: "/types/zzz/index.d.ts", text: SourceText.New("", "", 'declare module "a" { }') }, - ]; - const host = createTestCompilerHost(files, target); - const options: CompilerOptions = { target, typeRoots: ["/types"] }; - const program1 = createProgram(["/a.ts"], options, host); - let sourceFile = program1.getSourceFile("/a.ts")!; - assert.isDefined(sourceFile, "'/a.ts' is included in the program"); - sourceFile = updateSourceFile(sourceFile, "'use strict';" + sourceFile.text, { newLength: "'use strict';".length, span: { start: 0, length: 0 } }); - assert.strictEqual(sourceFile.statements[2].getSourceFile(), sourceFile, "parent pointers are updated"); - const updateHost: TestCompilerHost = { - ...host, - getSourceFile(fileName) { - return fileName === sourceFile.fileName ? sourceFile : program1.getSourceFile(fileName); - } - }; - const program2 = createProgram(["/a.ts"], options, updateHost, program1); - assert.isDefined(program2.getSourceFile("/a.ts")!.resolvedModules!.get("a"), "'a' is not an unresolved module after re-use"); - assert.strictEqual(sourceFile.statements[2].getSourceFile(), sourceFile, "parent pointers are not altered"); + assert.isDefined(program2.getSourceFile("/a.ts")!.resolvedModules!.get("a"), "'a' is not an unresolved module after re-use"); + }); + it("works with updated SourceFiles", () => { + // adapted repro from https://github.com/Microsoft/TypeScript/issues/26166 + const files = [ + { name: "/a.ts", text: SourceText.New("", "", 'import * as a from "a";a;') }, + { name: "/types/zzz/index.d.ts", text: SourceText.New("", "", 'declare module "a" { }') }, + ]; + const host = createTestCompilerHost(files, target); + const options: CompilerOptions = { target, typeRoots: ["/types"] }; + const program1 = createProgram(["/a.ts"], options, host); + let sourceFile = program1.getSourceFile("/a.ts")!; + assert.isDefined(sourceFile, "'/a.ts' is included in the program"); + sourceFile = updateSourceFile(sourceFile, "'use strict';" + sourceFile.text, { newLength: "'use strict';".length, span: { start: 0, length: 0 } }); + assert.strictEqual(sourceFile.statements[2].getSourceFile(), sourceFile, "parent pointers are updated"); + const updateHost: TestCompilerHost = { + ...host, + getSourceFile(fileName) { + return fileName === sourceFile.fileName ? sourceFile : program1.getSourceFile(fileName); + } + }; + const program2 = createProgram(["/a.ts"], options, updateHost, program1); + assert.isDefined(program2.getSourceFile("/a.ts")!.resolvedModules!.get("a"), "'a' is not an unresolved module after re-use"); + assert.strictEqual(sourceFile.statements[2].getSourceFile(), sourceFile, "parent pointers are not altered"); + }); + it("resolved type directives cache follows type directives", () => { + const files = [ + { name: "/a.ts", text: SourceText.New("/// ", "", "var x = $") }, + { name: "/types/typedefs/index.d.ts", text: SourceText.New("", "", "declare var $: number") }, + ]; + const options: CompilerOptions = { target, typeRoots: ["/types"] }; + const program1 = newProgram(files, ["/a.ts"], options); + checkResolvedTypeDirectivesCache(program1, "/a.ts", createMapFromTemplate({ typedefs: { resolvedFileName: "/types/typedefs/index.d.ts", primary: true } })); + checkResolvedTypeDirectivesCache(program1, "/types/typedefs/index.d.ts", /*expectedContent*/ undefined); + const program2 = updateProgram(program1, ["/a.ts"], options, files => { + files[0].text = files[0].text.updateProgram("var x = 2"); }); - - it("resolved type directives cache follows type directives", () => { - const files = [ - { name: "/a.ts", text: SourceText.New("/// ", "", "var x = $") }, - { name: "/types/typedefs/index.d.ts", text: SourceText.New("", "", "declare var $: number") }, - ]; - const options: CompilerOptions = { target, typeRoots: ["/types"] }; - - const program1 = newProgram(files, ["/a.ts"], options); - checkResolvedTypeDirectivesCache(program1, "/a.ts", createMapFromTemplate({ typedefs: { resolvedFileName: "/types/typedefs/index.d.ts", primary: true } })); - checkResolvedTypeDirectivesCache(program1, "/types/typedefs/index.d.ts", /*expectedContent*/ undefined); - - const program2 = updateProgram(program1, ["/a.ts"], options, files => { - files[0].text = files[0].text.updateProgram("var x = 2"); - }); - assert.equal(program1.structureIsReused, StructureIsReused.Completely); - - // content of resolution cache should not change - checkResolvedTypeDirectivesCache(program1, "/a.ts", createMapFromTemplate({ typedefs: { resolvedFileName: "/types/typedefs/index.d.ts", primary: true } })); - checkResolvedTypeDirectivesCache(program1, "/types/typedefs/index.d.ts", /*expectedContent*/ undefined); - - // type reference directives has changed - program is not reused - const program3 = updateProgram(program2, ["/a.ts"], options, files => { - files[0].text = files[0].text.updateReferences(""); - }); - - assert.equal(program2.structureIsReused, StructureIsReused.SafeModules); - checkResolvedTypeDirectivesCache(program3, "/a.ts", /*expectedContent*/ undefined); - - updateProgram(program3, ["/a.ts"], options, files => { - const newReferences = `/// + assert.equal(program1.structureIsReused, StructureIsReused.Completely); + // content of resolution cache should not change + checkResolvedTypeDirectivesCache(program1, "/a.ts", createMapFromTemplate({ typedefs: { resolvedFileName: "/types/typedefs/index.d.ts", primary: true } })); + checkResolvedTypeDirectivesCache(program1, "/types/typedefs/index.d.ts", /*expectedContent*/ undefined); + // type reference directives has changed - program is not reused + const program3 = updateProgram(program2, ["/a.ts"], options, files => { + files[0].text = files[0].text.updateReferences(""); + }); + assert.equal(program2.structureIsReused, StructureIsReused.SafeModules); + checkResolvedTypeDirectivesCache(program3, "/a.ts", /*expectedContent*/ undefined); + updateProgram(program3, ["/a.ts"], options, files => { + const newReferences = `/// /// `; - files[0].text = files[0].text.updateReferences(newReferences); - }); - assert.equal(program3.structureIsReused, StructureIsReused.SafeModules); - checkResolvedTypeDirectivesCache(program1, "/a.ts", createMapFromTemplate({ typedefs: { resolvedFileName: "/types/typedefs/index.d.ts", primary: true } })); + files[0].text = files[0].text.updateReferences(newReferences); }); - - it("fetches imports after npm install", () => { - const file1Ts = { name: "file1.ts", text: SourceText.New("", `import * as a from "a";`, "const myX: number = a.x;") }; - const file2Ts = { name: "file2.ts", text: SourceText.New("", "", "") }; - const indexDTS = { name: "node_modules/a/index.d.ts", text: SourceText.New("", "export declare let x: number;", "") }; - const options: CompilerOptions = { target: ScriptTarget.ES2015, traceResolution: true, moduleResolution: ModuleResolutionKind.NodeJs }; - const rootFiles = [file1Ts, file2Ts]; - const filesAfterNpmInstall = [file1Ts, file2Ts, indexDTS]; - - const initialProgram = newProgram(rootFiles, rootFiles.map(f => f.name), options); + assert.equal(program3.structureIsReused, StructureIsReused.SafeModules); + checkResolvedTypeDirectivesCache(program1, "/a.ts", createMapFromTemplate({ typedefs: { resolvedFileName: "/types/typedefs/index.d.ts", primary: true } })); + }); + it("fetches imports after npm install", () => { + const file1Ts = { name: "file1.ts", text: SourceText.New("", `import * as a from "a";`, "const myX: number = a.x;") }; + const file2Ts = { name: "file2.ts", text: SourceText.New("", "", "") }; + const indexDTS = { name: "node_modules/a/index.d.ts", text: SourceText.New("", "export declare let x: number;", "") }; + const options: CompilerOptions = { target: ScriptTarget.ES2015, traceResolution: true, moduleResolution: ModuleResolutionKind.NodeJs }; + const rootFiles = [file1Ts, file2Ts]; + const filesAfterNpmInstall = [file1Ts, file2Ts, indexDTS]; + const initialProgram = newProgram(rootFiles, rootFiles.map(f => f.name), options); + { + assert.deepEqual(initialProgram.host.getTrace(), [ + "======== Resolving module 'a' from 'file1.ts'. ========", + "Explicitly specified module resolution kind: 'NodeJs'.", + "Loading module 'a' from 'node_modules' folder, target file type 'TypeScript'.", + "File 'node_modules/a/package.json' does not exist.", + "File 'node_modules/a.ts' does not exist.", + "File 'node_modules/a.tsx' does not exist.", + "File 'node_modules/a.d.ts' does not exist.", + "File 'node_modules/a/index.ts' does not exist.", + "File 'node_modules/a/index.tsx' does not exist.", + "File 'node_modules/a/index.d.ts' does not exist.", + "File 'node_modules/@types/a/package.json' does not exist.", + "File 'node_modules/@types/a.d.ts' does not exist.", + "File 'node_modules/@types/a/index.d.ts' does not exist.", + "Loading module 'a' from 'node_modules' folder, target file type 'JavaScript'.", + "File 'node_modules/a/package.json' does not exist.", + "File 'node_modules/a.js' does not exist.", + "File 'node_modules/a.jsx' does not exist.", + "File 'node_modules/a/index.js' does not exist.", + "File 'node_modules/a/index.jsx' does not exist.", + "======== Module name 'a' was not resolved. ========" + ], "initialProgram: execute module resolution normally."); + const initialProgramDiagnostics = initialProgram.getSemanticDiagnostics(initialProgram.getSourceFile("file1.ts")); + assert.lengthOf(initialProgramDiagnostics, 1, `initialProgram: import should fail.`); + } + const afterNpmInstallProgram = updateProgram(initialProgram, rootFiles.map(f => f.name), options, f => { + f[1].text = f[1].text.updateReferences(`/// `); + }, filesAfterNpmInstall); + { + assert.deepEqual(afterNpmInstallProgram.host.getTrace(), [ + "======== Resolving module 'a' from 'file1.ts'. ========", + "Explicitly specified module resolution kind: 'NodeJs'.", + "Loading module 'a' from 'node_modules' folder, target file type 'TypeScript'.", + "File 'node_modules/a/package.json' does not exist.", + "File 'node_modules/a.ts' does not exist.", + "File 'node_modules/a.tsx' does not exist.", + "File 'node_modules/a.d.ts' does not exist.", + "File 'node_modules/a/index.ts' does not exist.", + "File 'node_modules/a/index.tsx' does not exist.", + "File 'node_modules/a/index.d.ts' exist - use it as a name resolution result.", + "======== Module name 'a' was successfully resolved to 'node_modules/a/index.d.ts'. ========" + ], "afterNpmInstallProgram: execute module resolution normally."); + const afterNpmInstallProgramDiagnostics = afterNpmInstallProgram.getSemanticDiagnostics(afterNpmInstallProgram.getSourceFile("file1.ts")); + assert.lengthOf(afterNpmInstallProgramDiagnostics, 0, `afterNpmInstallProgram: program is well-formed with import.`); + } + }); + it("can reuse ambient module declarations from non-modified files", () => { + const files = [ + { name: "/a/b/app.ts", text: SourceText.New("", "import * as fs from 'fs'", "") }, + { name: "/a/b/node.d.ts", text: SourceText.New("", "", "declare module 'fs' {}") } + ]; + const options = { target: ScriptTarget.ES2015, traceResolution: true }; + const program = newProgram(files, files.map(f => f.name), options); + assert.deepEqual(program.host.getTrace(), [ + "======== Resolving module 'fs' from '/a/b/app.ts'. ========", + "Module resolution kind is not specified, using 'Classic'.", + "File '/a/b/fs.ts' does not exist.", + "File '/a/b/fs.tsx' does not exist.", + "File '/a/b/fs.d.ts' does not exist.", + "File '/a/fs.ts' does not exist.", + "File '/a/fs.tsx' does not exist.", + "File '/a/fs.d.ts' does not exist.", + "File '/fs.ts' does not exist.", + "File '/fs.tsx' does not exist.", + "File '/fs.d.ts' does not exist.", + "File '/a/b/node_modules/@types/fs/package.json' does not exist.", + "File '/a/b/node_modules/@types/fs.d.ts' does not exist.", + "File '/a/b/node_modules/@types/fs/index.d.ts' does not exist.", + "File '/a/node_modules/@types/fs/package.json' does not exist.", + "File '/a/node_modules/@types/fs.d.ts' does not exist.", + "File '/a/node_modules/@types/fs/index.d.ts' does not exist.", + "File '/node_modules/@types/fs/package.json' does not exist.", + "File '/node_modules/@types/fs.d.ts' does not exist.", + "File '/node_modules/@types/fs/index.d.ts' does not exist.", + "File '/a/b/fs.js' does not exist.", + "File '/a/b/fs.jsx' does not exist.", + "File '/a/fs.js' does not exist.", + "File '/a/fs.jsx' does not exist.", + "File '/fs.js' does not exist.", + "File '/fs.jsx' does not exist.", + "======== Module name 'fs' was not resolved. ========", + ], "should look for 'fs'"); + const program2 = updateProgram(program, program.getRootFileNames(), options, f => { + f[0].text = f[0].text.updateProgram("var x = 1;"); + }); + assert.deepEqual(program2.host.getTrace(), [ + "Module 'fs' was resolved as ambient module declared in '/a/b/node.d.ts' since this file was not modified." + ], "should reuse 'fs' since node.d.ts was not changed"); + const program3 = updateProgram(program2, program2.getRootFileNames(), options, f => { + f[0].text = f[0].text.updateProgram("var y = 1;"); + f[1].text = f[1].text.updateProgram("declare var process: any"); + }); + assert.deepEqual(program3.host.getTrace(), [ + "======== Resolving module 'fs' from '/a/b/app.ts'. ========", + "Module resolution kind is not specified, using 'Classic'.", + "File '/a/b/fs.ts' does not exist.", + "File '/a/b/fs.tsx' does not exist.", + "File '/a/b/fs.d.ts' does not exist.", + "File '/a/fs.ts' does not exist.", + "File '/a/fs.tsx' does not exist.", + "File '/a/fs.d.ts' does not exist.", + "File '/fs.ts' does not exist.", + "File '/fs.tsx' does not exist.", + "File '/fs.d.ts' does not exist.", + "File '/a/b/node_modules/@types/fs/package.json' does not exist.", + "File '/a/b/node_modules/@types/fs.d.ts' does not exist.", + "File '/a/b/node_modules/@types/fs/index.d.ts' does not exist.", + "File '/a/node_modules/@types/fs/package.json' does not exist.", + "File '/a/node_modules/@types/fs.d.ts' does not exist.", + "File '/a/node_modules/@types/fs/index.d.ts' does not exist.", + "File '/node_modules/@types/fs/package.json' does not exist.", + "File '/node_modules/@types/fs.d.ts' does not exist.", + "File '/node_modules/@types/fs/index.d.ts' does not exist.", + "File '/a/b/fs.js' does not exist.", + "File '/a/b/fs.jsx' does not exist.", + "File '/a/fs.js' does not exist.", + "File '/a/fs.jsx' does not exist.", + "File '/fs.js' does not exist.", + "File '/fs.jsx' does not exist.", + "======== Module name 'fs' was not resolved. ========", + ], "should look for 'fs' again since node.d.ts was changed"); + }); + it("can reuse module resolutions from non-modified files", () => { + const files = [ + { name: "a1.ts", text: SourceText.New("", "", "let x = 1;") }, + { name: "a2.ts", text: SourceText.New("", "", "let x = 1;") }, + { name: "b1.ts", text: SourceText.New("", "export class B { x: number; }", "") }, + { name: "b2.ts", text: SourceText.New("", "export class B { x: number; }", "") }, + { name: "node_modules/@types/typerefs1/index.d.ts", text: SourceText.New("", "", "declare let z: string;") }, + { name: "node_modules/@types/typerefs2/index.d.ts", text: SourceText.New("", "", "declare let z: string;") }, { - assert.deepEqual(initialProgram.host.getTrace(), - [ - "======== Resolving module 'a' from 'file1.ts'. ========", - "Explicitly specified module resolution kind: 'NodeJs'.", - "Loading module 'a' from 'node_modules' folder, target file type 'TypeScript'.", - "File 'node_modules/a/package.json' does not exist.", - "File 'node_modules/a.ts' does not exist.", - "File 'node_modules/a.tsx' does not exist.", - "File 'node_modules/a.d.ts' does not exist.", - "File 'node_modules/a/index.ts' does not exist.", - "File 'node_modules/a/index.tsx' does not exist.", - "File 'node_modules/a/index.d.ts' does not exist.", - "File 'node_modules/@types/a/package.json' does not exist.", - "File 'node_modules/@types/a.d.ts' does not exist.", - "File 'node_modules/@types/a/index.d.ts' does not exist.", - "Loading module 'a' from 'node_modules' folder, target file type 'JavaScript'.", - "File 'node_modules/a/package.json' does not exist.", - "File 'node_modules/a.js' does not exist.", - "File 'node_modules/a.jsx' does not exist.", - "File 'node_modules/a/index.js' does not exist.", - "File 'node_modules/a/index.jsx' does not exist.", - "======== Module name 'a' was not resolved. ========" - ], - "initialProgram: execute module resolution normally."); - - const initialProgramDiagnostics = initialProgram.getSemanticDiagnostics(initialProgram.getSourceFile("file1.ts")); - assert.lengthOf(initialProgramDiagnostics, 1, `initialProgram: import should fail.`); - } - - const afterNpmInstallProgram = updateProgram(initialProgram, rootFiles.map(f => f.name), options, f => { - f[1].text = f[1].text.updateReferences(`/// `); - }, filesAfterNpmInstall); + name: "f1.ts", + text: SourceText.New(`/// ${newLine}/// ${newLine}/// `, `import { B } from './b1';${newLine}export let BB = B;`, "declare module './b1' { interface B { y: string; } }") + }, { - assert.deepEqual(afterNpmInstallProgram.host.getTrace(), - [ - "======== Resolving module 'a' from 'file1.ts'. ========", - "Explicitly specified module resolution kind: 'NodeJs'.", - "Loading module 'a' from 'node_modules' folder, target file type 'TypeScript'.", - "File 'node_modules/a/package.json' does not exist.", - "File 'node_modules/a.ts' does not exist.", - "File 'node_modules/a.tsx' does not exist.", - "File 'node_modules/a.d.ts' does not exist.", - "File 'node_modules/a/index.ts' does not exist.", - "File 'node_modules/a/index.tsx' does not exist.", - "File 'node_modules/a/index.d.ts' exist - use it as a name resolution result.", - "======== Module name 'a' was successfully resolved to 'node_modules/a/index.d.ts'. ========" - ], - "afterNpmInstallProgram: execute module resolution normally."); - - const afterNpmInstallProgramDiagnostics = afterNpmInstallProgram.getSemanticDiagnostics(afterNpmInstallProgram.getSourceFile("file1.ts")); - assert.lengthOf(afterNpmInstallProgramDiagnostics, 0, `afterNpmInstallProgram: program is well-formed with import.`); - } + name: "f2.ts", + text: SourceText.New(`/// ${newLine}/// `, `import { B } from './b2';${newLine}import { BB } from './f1';`, "(new BB).x; (new BB).y;") + }, + ]; + const options: CompilerOptions = { target: ScriptTarget.ES2015, traceResolution: true, moduleResolution: ModuleResolutionKind.Classic }; + const program1 = newProgram(files, files.map(f => f.name), options); + let expectedErrors = 0; + { + assert.deepEqual(program1.host.getTrace(), [ + "======== Resolving type reference directive 'typerefs1', containing file 'f1.ts', root directory 'node_modules/@types'. ========", + "Resolving with primary search path 'node_modules/@types'.", + "File 'node_modules/@types/typerefs1/package.json' does not exist.", + "File 'node_modules/@types/typerefs1/index.d.ts' exist - use it as a name resolution result.", + "======== Type reference directive 'typerefs1' was successfully resolved to 'node_modules/@types/typerefs1/index.d.ts', primary: true. ========", + "======== Resolving module './b1' from 'f1.ts'. ========", + "Explicitly specified module resolution kind: 'Classic'.", + "File 'b1.ts' exist - use it as a name resolution result.", + "======== Module name './b1' was successfully resolved to 'b1.ts'. ========", + "======== Resolving type reference directive 'typerefs2', containing file 'f2.ts', root directory 'node_modules/@types'. ========", + "Resolving with primary search path 'node_modules/@types'.", + "File 'node_modules/@types/typerefs2/package.json' does not exist.", + "File 'node_modules/@types/typerefs2/index.d.ts' exist - use it as a name resolution result.", + "======== Type reference directive 'typerefs2' was successfully resolved to 'node_modules/@types/typerefs2/index.d.ts', primary: true. ========", + "======== Resolving module './b2' from 'f2.ts'. ========", + "Explicitly specified module resolution kind: 'Classic'.", + "File 'b2.ts' exist - use it as a name resolution result.", + "======== Module name './b2' was successfully resolved to 'b2.ts'. ========", + "======== Resolving module './f1' from 'f2.ts'. ========", + "Explicitly specified module resolution kind: 'Classic'.", + "File 'f1.ts' exist - use it as a name resolution result.", + "======== Module name './f1' was successfully resolved to 'f1.ts'. ========" + ], "program1: execute module resolution normally."); + const program1Diagnostics = program1.getSemanticDiagnostics(program1.getSourceFile("f2.ts")); + assert.lengthOf(program1Diagnostics, expectedErrors, `initial program should be well-formed`); + } + const indexOfF1 = 6; + const program2 = updateProgram(program1, program1.getRootFileNames(), options, f => { + const newSourceText = f[indexOfF1].text.updateReferences(`/// ${newLine}/// `); + f[indexOfF1] = { name: "f1.ts", text: newSourceText }; }); - - it("can reuse ambient module declarations from non-modified files", () => { - const files = [ - { name: "/a/b/app.ts", text: SourceText.New("", "import * as fs from 'fs'", "") }, - { name: "/a/b/node.d.ts", text: SourceText.New("", "", "declare module 'fs' {}") } - ]; - const options = { target: ScriptTarget.ES2015, traceResolution: true }; - const program = newProgram(files, files.map(f => f.name), options); - assert.deepEqual(program.host.getTrace(), - [ - "======== Resolving module 'fs' from '/a/b/app.ts'. ========", - "Module resolution kind is not specified, using 'Classic'.", - "File '/a/b/fs.ts' does not exist.", - "File '/a/b/fs.tsx' does not exist.", - "File '/a/b/fs.d.ts' does not exist.", - "File '/a/fs.ts' does not exist.", - "File '/a/fs.tsx' does not exist.", - "File '/a/fs.d.ts' does not exist.", - "File '/fs.ts' does not exist.", - "File '/fs.tsx' does not exist.", - "File '/fs.d.ts' does not exist.", - "File '/a/b/node_modules/@types/fs/package.json' does not exist.", - "File '/a/b/node_modules/@types/fs.d.ts' does not exist.", - "File '/a/b/node_modules/@types/fs/index.d.ts' does not exist.", - "File '/a/node_modules/@types/fs/package.json' does not exist.", - "File '/a/node_modules/@types/fs.d.ts' does not exist.", - "File '/a/node_modules/@types/fs/index.d.ts' does not exist.", - "File '/node_modules/@types/fs/package.json' does not exist.", - "File '/node_modules/@types/fs.d.ts' does not exist.", - "File '/node_modules/@types/fs/index.d.ts' does not exist.", - "File '/a/b/fs.js' does not exist.", - "File '/a/b/fs.jsx' does not exist.", - "File '/a/fs.js' does not exist.", - "File '/a/fs.jsx' does not exist.", - "File '/fs.js' does not exist.", - "File '/fs.jsx' does not exist.", - "======== Module name 'fs' was not resolved. ========", - ], "should look for 'fs'"); - - const program2 = updateProgram(program, program.getRootFileNames(), options, f => { - f[0].text = f[0].text.updateProgram("var x = 1;"); - }); + { + const program2Diagnostics = program2.getSemanticDiagnostics(program2.getSourceFile("f2.ts")); + assert.lengthOf(program2Diagnostics, expectedErrors, `removing no-default-lib shouldn't affect any types used.`); assert.deepEqual(program2.host.getTrace(), [ - "Module 'fs' was resolved as ambient module declared in '/a/b/node.d.ts' since this file was not modified." - ], "should reuse 'fs' since node.d.ts was not changed"); - - const program3 = updateProgram(program2, program2.getRootFileNames(), options, f => { - f[0].text = f[0].text.updateProgram("var y = 1;"); - f[1].text = f[1].text.updateProgram("declare var process: any"); - }); - assert.deepEqual(program3.host.getTrace(), - [ - "======== Resolving module 'fs' from '/a/b/app.ts'. ========", - "Module resolution kind is not specified, using 'Classic'.", - "File '/a/b/fs.ts' does not exist.", - "File '/a/b/fs.tsx' does not exist.", - "File '/a/b/fs.d.ts' does not exist.", - "File '/a/fs.ts' does not exist.", - "File '/a/fs.tsx' does not exist.", - "File '/a/fs.d.ts' does not exist.", - "File '/fs.ts' does not exist.", - "File '/fs.tsx' does not exist.", - "File '/fs.d.ts' does not exist.", - "File '/a/b/node_modules/@types/fs/package.json' does not exist.", - "File '/a/b/node_modules/@types/fs.d.ts' does not exist.", - "File '/a/b/node_modules/@types/fs/index.d.ts' does not exist.", - "File '/a/node_modules/@types/fs/package.json' does not exist.", - "File '/a/node_modules/@types/fs.d.ts' does not exist.", - "File '/a/node_modules/@types/fs/index.d.ts' does not exist.", - "File '/node_modules/@types/fs/package.json' does not exist.", - "File '/node_modules/@types/fs.d.ts' does not exist.", - "File '/node_modules/@types/fs/index.d.ts' does not exist.", - "File '/a/b/fs.js' does not exist.", - "File '/a/b/fs.jsx' does not exist.", - "File '/a/fs.js' does not exist.", - "File '/a/fs.jsx' does not exist.", - "File '/fs.js' does not exist.", - "File '/fs.jsx' does not exist.", - "======== Module name 'fs' was not resolved. ========", - ], "should look for 'fs' again since node.d.ts was changed"); + "======== Resolving type reference directive 'typerefs1', containing file 'f1.ts', root directory 'node_modules/@types'. ========", + "Resolving with primary search path 'node_modules/@types'.", + "File 'node_modules/@types/typerefs1/package.json' does not exist.", + "File 'node_modules/@types/typerefs1/index.d.ts' exist - use it as a name resolution result.", + "======== Type reference directive 'typerefs1' was successfully resolved to 'node_modules/@types/typerefs1/index.d.ts', primary: true. ========", + "======== Resolving module './b1' from 'f1.ts'. ========", + "Explicitly specified module resolution kind: 'Classic'.", + "File 'b1.ts' exist - use it as a name resolution result.", + "======== Module name './b1' was successfully resolved to 'b1.ts'. ========", + "======== Resolving type reference directive 'typerefs2', containing file 'f2.ts', root directory 'node_modules/@types'. ========", + "Resolving with primary search path 'node_modules/@types'.", + "File 'node_modules/@types/typerefs2/package.json' does not exist.", + "File 'node_modules/@types/typerefs2/index.d.ts' exist - use it as a name resolution result.", + "======== Type reference directive 'typerefs2' was successfully resolved to 'node_modules/@types/typerefs2/index.d.ts', primary: true. ========", + "Reusing resolution of module './b2' to file 'f2.ts' from old program.", + "Reusing resolution of module './f1' to file 'f2.ts' from old program." + ], "program2: reuse module resolutions in f2 since it is unchanged"); + } + const program3 = updateProgram(program2, program2.getRootFileNames(), options, f => { + const newSourceText = f[indexOfF1].text.updateReferences(`/// `); + f[indexOfF1] = { name: "f1.ts", text: newSourceText }; + }); + { + const program3Diagnostics = program3.getSemanticDiagnostics(program3.getSourceFile("f2.ts")); + assert.lengthOf(program3Diagnostics, expectedErrors, `typerefs2 was unused, so diagnostics should be unaffected.`); + assert.deepEqual(program3.host.getTrace(), [ + "======== Resolving module './b1' from 'f1.ts'. ========", + "Explicitly specified module resolution kind: 'Classic'.", + "File 'b1.ts' exist - use it as a name resolution result.", + "======== Module name './b1' was successfully resolved to 'b1.ts'. ========", + "======== Resolving type reference directive 'typerefs2', containing file 'f2.ts', root directory 'node_modules/@types'. ========", + "Resolving with primary search path 'node_modules/@types'.", + "File 'node_modules/@types/typerefs2/package.json' does not exist.", + "File 'node_modules/@types/typerefs2/index.d.ts' exist - use it as a name resolution result.", + "======== Type reference directive 'typerefs2' was successfully resolved to 'node_modules/@types/typerefs2/index.d.ts', primary: true. ========", + "Reusing resolution of module './b2' to file 'f2.ts' from old program.", + "Reusing resolution of module './f1' to file 'f2.ts' from old program." + ], "program3: reuse module resolutions in f2 since it is unchanged"); + } + const program4 = updateProgram(program3, program3.getRootFileNames(), options, f => { + const newSourceText = f[indexOfF1].text.updateReferences(""); + f[indexOfF1] = { name: "f1.ts", text: newSourceText }; + }); + { + const program4Diagnostics = program4.getSemanticDiagnostics(program4.getSourceFile("f2.ts")); + assert.lengthOf(program4Diagnostics, expectedErrors, `a1.ts was unused, so diagnostics should be unaffected.`); + assert.deepEqual(program4.host.getTrace(), [ + "======== Resolving module './b1' from 'f1.ts'. ========", + "Explicitly specified module resolution kind: 'Classic'.", + "File 'b1.ts' exist - use it as a name resolution result.", + "======== Module name './b1' was successfully resolved to 'b1.ts'. ========", + "======== Resolving type reference directive 'typerefs2', containing file 'f2.ts', root directory 'node_modules/@types'. ========", + "Resolving with primary search path 'node_modules/@types'.", + "File 'node_modules/@types/typerefs2/package.json' does not exist.", + "File 'node_modules/@types/typerefs2/index.d.ts' exist - use it as a name resolution result.", + "======== Type reference directive 'typerefs2' was successfully resolved to 'node_modules/@types/typerefs2/index.d.ts', primary: true. ========", + "Reusing resolution of module './b2' to file 'f2.ts' from old program.", + "Reusing resolution of module './f1' to file 'f2.ts' from old program." + ], "program_4: reuse module resolutions in f2 since it is unchanged"); + } + const program5 = updateProgram(program4, program4.getRootFileNames(), options, f => { + const newSourceText = f[indexOfF1].text.updateImportsAndExports(`import { B } from './b1';`); + f[indexOfF1] = { name: "f1.ts", text: newSourceText }; + }); + { + const program5Diagnostics = program5.getSemanticDiagnostics(program5.getSourceFile("f2.ts")); + assert.lengthOf(program5Diagnostics, ++expectedErrors, `import of BB in f1 fails. BB is of type any. Add one error`); + assert.deepEqual(program5.host.getTrace(), [ + "======== Resolving module './b1' from 'f1.ts'. ========", + "Explicitly specified module resolution kind: 'Classic'.", + "File 'b1.ts' exist - use it as a name resolution result.", + "======== Module name './b1' was successfully resolved to 'b1.ts'. ========" + ], "program_5: exports do not affect program structure, so f2's resolutions are silently reused."); + } + const program6 = updateProgram(program5, program5.getRootFileNames(), options, f => { + const newSourceText = f[indexOfF1].text.updateProgram(""); + f[indexOfF1] = { name: "f1.ts", text: newSourceText }; + }); + { + const program6Diagnostics = program6.getSemanticDiagnostics(program6.getSourceFile("f2.ts")); + assert.lengthOf(program6Diagnostics, expectedErrors, `import of BB in f1 fails.`); + assert.deepEqual(program6.host.getTrace(), [ + "======== Resolving module './b1' from 'f1.ts'. ========", + "Explicitly specified module resolution kind: 'Classic'.", + "File 'b1.ts' exist - use it as a name resolution result.", + "======== Module name './b1' was successfully resolved to 'b1.ts'. ========", + "======== Resolving type reference directive 'typerefs2', containing file 'f2.ts', root directory 'node_modules/@types'. ========", + "Resolving with primary search path 'node_modules/@types'.", + "File 'node_modules/@types/typerefs2/package.json' does not exist.", + "File 'node_modules/@types/typerefs2/index.d.ts' exist - use it as a name resolution result.", + "======== Type reference directive 'typerefs2' was successfully resolved to 'node_modules/@types/typerefs2/index.d.ts', primary: true. ========", + "Reusing resolution of module './b2' to file 'f2.ts' from old program.", + "Reusing resolution of module './f1' to file 'f2.ts' from old program." + ], "program_6: reuse module resolutions in f2 since it is unchanged"); + } + const program7 = updateProgram(program6, program6.getRootFileNames(), options, f => { + const newSourceText = f[indexOfF1].text.updateImportsAndExports(""); + f[indexOfF1] = { name: "f1.ts", text: newSourceText }; }); - - it("can reuse module resolutions from non-modified files", () => { - const files = [ - { name: "a1.ts", text: SourceText.New("", "", "let x = 1;") }, - { name: "a2.ts", text: SourceText.New("", "", "let x = 1;") }, - { name: "b1.ts", text: SourceText.New("", "export class B { x: number; }", "") }, - { name: "b2.ts", text: SourceText.New("", "export class B { x: number; }", "") }, - { name: "node_modules/@types/typerefs1/index.d.ts", text: SourceText.New("", "", "declare let z: string;") }, - { name: "node_modules/@types/typerefs2/index.d.ts", text: SourceText.New("", "", "declare let z: string;") }, + { + const program7Diagnostics = program7.getSemanticDiagnostics(program7.getSourceFile("f2.ts")); + assert.lengthOf(program7Diagnostics, expectedErrors, `removing import is noop with respect to program, so no change in diagnostics.`); + assert.deepEqual(program7.host.getTrace(), [ + "======== Resolving type reference directive 'typerefs2', containing file 'f2.ts', root directory 'node_modules/@types'. ========", + "Resolving with primary search path 'node_modules/@types'.", + "File 'node_modules/@types/typerefs2/package.json' does not exist.", + "File 'node_modules/@types/typerefs2/index.d.ts' exist - use it as a name resolution result.", + "======== Type reference directive 'typerefs2' was successfully resolved to 'node_modules/@types/typerefs2/index.d.ts', primary: true. ========", + "Reusing resolution of module './b2' to file 'f2.ts' from old program.", + "Reusing resolution of module './f1' to file 'f2.ts' from old program." + ], "program_7 should reuse module resolutions in f2 since it is unchanged"); + } + }); + describe("redirects", () => { + const axIndex = "/node_modules/a/node_modules/x/index.d.ts"; + const axPackage = "/node_modules/a/node_modules/x/package.json"; + const bxIndex = "/node_modules/b/node_modules/x/index.d.ts"; + const bxPackage = "/node_modules/b/node_modules/x/package.json"; + const root = "/a.ts"; + const compilerOptions = { target, moduleResolution: ModuleResolutionKind.NodeJs }; + function createRedirectProgram(useGetSourceFileByPath: boolean, options?: { + bText: string; + bVersion: string; + }): ProgramWithSourceTexts { + const files: NamedSourceText[] = [ + { + name: "/node_modules/a/index.d.ts", + text: SourceText.New("", 'import X from "x";', "export function a(x: X): void;"), + }, { - name: "f1.ts", - text: - SourceText.New( - `/// ${newLine}/// ${newLine}/// `, - `import { B } from './b1';${newLine}export let BB = B;`, - "declare module './b1' { interface B { y: string; } }") + name: axIndex, + text: SourceText.New("", "", "export default class X { private x: number; }"), }, { - name: "f2.ts", - text: SourceText.New( - `/// ${newLine}/// `, - `import { B } from './b2';${newLine}import { BB } from './f1';`, - "(new BB).x; (new BB).y;") + name: axPackage, + text: SourceText.New("", "", JSON.stringify({ name: "x", version: "1.2.3" })), + }, + { + name: "/node_modules/b/index.d.ts", + text: SourceText.New("", 'import X from "x";', "export const b: X;"), + }, + { + name: bxIndex, + text: SourceText.New("", "", options ? options.bText : "export default class X { private x: number; }"), + }, + { + name: bxPackage, + text: SourceText.New("", "", JSON.stringify({ name: "x", version: options ? options.bVersion : "1.2.3" })), + }, + { + name: root, + text: SourceText.New("", 'import { a } from "a"; import { b } from "b";', "a(b)"), }, ]; - - const options: CompilerOptions = { target: ScriptTarget.ES2015, traceResolution: true, moduleResolution: ModuleResolutionKind.Classic }; - const program1 = newProgram(files, files.map(f => f.name), options); - let expectedErrors = 0; - { - assert.deepEqual(program1.host.getTrace(), - [ - "======== Resolving type reference directive 'typerefs1', containing file 'f1.ts', root directory 'node_modules/@types'. ========", - "Resolving with primary search path 'node_modules/@types'.", - "File 'node_modules/@types/typerefs1/package.json' does not exist.", - "File 'node_modules/@types/typerefs1/index.d.ts' exist - use it as a name resolution result.", - "======== Type reference directive 'typerefs1' was successfully resolved to 'node_modules/@types/typerefs1/index.d.ts', primary: true. ========", - "======== Resolving module './b1' from 'f1.ts'. ========", - "Explicitly specified module resolution kind: 'Classic'.", - "File 'b1.ts' exist - use it as a name resolution result.", - "======== Module name './b1' was successfully resolved to 'b1.ts'. ========", - "======== Resolving type reference directive 'typerefs2', containing file 'f2.ts', root directory 'node_modules/@types'. ========", - "Resolving with primary search path 'node_modules/@types'.", - "File 'node_modules/@types/typerefs2/package.json' does not exist.", - "File 'node_modules/@types/typerefs2/index.d.ts' exist - use it as a name resolution result.", - "======== Type reference directive 'typerefs2' was successfully resolved to 'node_modules/@types/typerefs2/index.d.ts', primary: true. ========", - "======== Resolving module './b2' from 'f2.ts'. ========", - "Explicitly specified module resolution kind: 'Classic'.", - "File 'b2.ts' exist - use it as a name resolution result.", - "======== Module name './b2' was successfully resolved to 'b2.ts'. ========", - "======== Resolving module './f1' from 'f2.ts'. ========", - "Explicitly specified module resolution kind: 'Classic'.", - "File 'f1.ts' exist - use it as a name resolution result.", - "======== Module name './f1' was successfully resolved to 'f1.ts'. ========" - ], - "program1: execute module resolution normally."); - - const program1Diagnostics = program1.getSemanticDiagnostics(program1.getSourceFile("f2.ts")); - assert.lengthOf(program1Diagnostics, expectedErrors, `initial program should be well-formed`); - } - const indexOfF1 = 6; - const program2 = updateProgram(program1, program1.getRootFileNames(), options, f => { - const newSourceText = f[indexOfF1].text.updateReferences(`/// ${newLine}/// `); - f[indexOfF1] = { name: "f1.ts", text: newSourceText }; - }); - - { - const program2Diagnostics = program2.getSemanticDiagnostics(program2.getSourceFile("f2.ts")); - assert.lengthOf(program2Diagnostics, expectedErrors, `removing no-default-lib shouldn't affect any types used.`); - - assert.deepEqual(program2.host.getTrace(), [ - "======== Resolving type reference directive 'typerefs1', containing file 'f1.ts', root directory 'node_modules/@types'. ========", - "Resolving with primary search path 'node_modules/@types'.", - "File 'node_modules/@types/typerefs1/package.json' does not exist.", - "File 'node_modules/@types/typerefs1/index.d.ts' exist - use it as a name resolution result.", - "======== Type reference directive 'typerefs1' was successfully resolved to 'node_modules/@types/typerefs1/index.d.ts', primary: true. ========", - "======== Resolving module './b1' from 'f1.ts'. ========", - "Explicitly specified module resolution kind: 'Classic'.", - "File 'b1.ts' exist - use it as a name resolution result.", - "======== Module name './b1' was successfully resolved to 'b1.ts'. ========", - "======== Resolving type reference directive 'typerefs2', containing file 'f2.ts', root directory 'node_modules/@types'. ========", - "Resolving with primary search path 'node_modules/@types'.", - "File 'node_modules/@types/typerefs2/package.json' does not exist.", - "File 'node_modules/@types/typerefs2/index.d.ts' exist - use it as a name resolution result.", - "======== Type reference directive 'typerefs2' was successfully resolved to 'node_modules/@types/typerefs2/index.d.ts', primary: true. ========", - "Reusing resolution of module './b2' to file 'f2.ts' from old program.", - "Reusing resolution of module './f1' to file 'f2.ts' from old program." - ], "program2: reuse module resolutions in f2 since it is unchanged"); - } - - const program3 = updateProgram(program2, program2.getRootFileNames(), options, f => { - const newSourceText = f[indexOfF1].text.updateReferences(`/// `); - f[indexOfF1] = { name: "f1.ts", text: newSourceText }; - }); - - { - const program3Diagnostics = program3.getSemanticDiagnostics(program3.getSourceFile("f2.ts")); - assert.lengthOf(program3Diagnostics, expectedErrors, `typerefs2 was unused, so diagnostics should be unaffected.`); - - assert.deepEqual(program3.host.getTrace(), [ - "======== Resolving module './b1' from 'f1.ts'. ========", - "Explicitly specified module resolution kind: 'Classic'.", - "File 'b1.ts' exist - use it as a name resolution result.", - "======== Module name './b1' was successfully resolved to 'b1.ts'. ========", - "======== Resolving type reference directive 'typerefs2', containing file 'f2.ts', root directory 'node_modules/@types'. ========", - "Resolving with primary search path 'node_modules/@types'.", - "File 'node_modules/@types/typerefs2/package.json' does not exist.", - "File 'node_modules/@types/typerefs2/index.d.ts' exist - use it as a name resolution result.", - "======== Type reference directive 'typerefs2' was successfully resolved to 'node_modules/@types/typerefs2/index.d.ts', primary: true. ========", - "Reusing resolution of module './b2' to file 'f2.ts' from old program.", - "Reusing resolution of module './f1' to file 'f2.ts' from old program." - ], "program3: reuse module resolutions in f2 since it is unchanged"); - } - - - const program4 = updateProgram(program3, program3.getRootFileNames(), options, f => { - const newSourceText = f[indexOfF1].text.updateReferences(""); - f[indexOfF1] = { name: "f1.ts", text: newSourceText }; + return newProgram(files, [root], compilerOptions, useGetSourceFileByPath); + } + function updateRedirectProgram(program: ProgramWithSourceTexts, updater: (files: NamedSourceText[]) => void, useGetSourceFileByPath: boolean): ProgramWithSourceTexts { + return updateProgram(program, [root], compilerOptions, updater, /*newTexts*/ undefined, useGetSourceFileByPath); + } + function verifyRedirects(useGetSourceFileByPath: boolean) { + it("No changes -> redirect not broken", () => { + const program1 = createRedirectProgram(useGetSourceFileByPath); + const program2 = updateRedirectProgram(program1, files => { + updateProgramText(files, root, "const x = 1;"); + }, useGetSourceFileByPath); + assert.equal(program1.structureIsReused, StructureIsReused.Completely); + assert.lengthOf(program2.getSemanticDiagnostics(), 0); }); - - { - const program4Diagnostics = program4.getSemanticDiagnostics(program4.getSourceFile("f2.ts")); - assert.lengthOf(program4Diagnostics, expectedErrors, `a1.ts was unused, so diagnostics should be unaffected.`); - - assert.deepEqual(program4.host.getTrace(), [ - "======== Resolving module './b1' from 'f1.ts'. ========", - "Explicitly specified module resolution kind: 'Classic'.", - "File 'b1.ts' exist - use it as a name resolution result.", - "======== Module name './b1' was successfully resolved to 'b1.ts'. ========", - "======== Resolving type reference directive 'typerefs2', containing file 'f2.ts', root directory 'node_modules/@types'. ========", - "Resolving with primary search path 'node_modules/@types'.", - "File 'node_modules/@types/typerefs2/package.json' does not exist.", - "File 'node_modules/@types/typerefs2/index.d.ts' exist - use it as a name resolution result.", - "======== Type reference directive 'typerefs2' was successfully resolved to 'node_modules/@types/typerefs2/index.d.ts', primary: true. ========", - "Reusing resolution of module './b2' to file 'f2.ts' from old program.", - "Reusing resolution of module './f1' to file 'f2.ts' from old program." - ], "program_4: reuse module resolutions in f2 since it is unchanged"); - } - - const program5 = updateProgram(program4, program4.getRootFileNames(), options, f => { - const newSourceText = f[indexOfF1].text.updateImportsAndExports(`import { B } from './b1';`); - f[indexOfF1] = { name: "f1.ts", text: newSourceText }; + it("Target changes -> redirect broken", () => { + const program1 = createRedirectProgram(useGetSourceFileByPath); + assert.lengthOf(program1.getSemanticDiagnostics(), 0); + const program2 = updateRedirectProgram(program1, files => { + updateProgramText(files, axIndex, "export default class X { private x: number; private y: number; }"); + updateProgramText(files, axPackage, JSON.stringify('{ name: "x", version: "1.2.4" }')); + }, useGetSourceFileByPath); + assert.equal(program1.structureIsReused, StructureIsReused.Not); + assert.lengthOf(program2.getSemanticDiagnostics(), 1); }); - - { - const program5Diagnostics = program5.getSemanticDiagnostics(program5.getSourceFile("f2.ts")); - assert.lengthOf(program5Diagnostics, ++expectedErrors, `import of BB in f1 fails. BB is of type any. Add one error`); - - assert.deepEqual(program5.host.getTrace(), [ - "======== Resolving module './b1' from 'f1.ts'. ========", - "Explicitly specified module resolution kind: 'Classic'.", - "File 'b1.ts' exist - use it as a name resolution result.", - "======== Module name './b1' was successfully resolved to 'b1.ts'. ========" - ], "program_5: exports do not affect program structure, so f2's resolutions are silently reused."); - } - - const program6 = updateProgram(program5, program5.getRootFileNames(), options, f => { - const newSourceText = f[indexOfF1].text.updateProgram(""); - f[indexOfF1] = { name: "f1.ts", text: newSourceText }; + it("Underlying changes -> redirect broken", () => { + const program1 = createRedirectProgram(useGetSourceFileByPath); + const program2 = updateRedirectProgram(program1, files => { + updateProgramText(files, bxIndex, "export default class X { private x: number; private y: number; }"); + updateProgramText(files, bxPackage, JSON.stringify({ name: "x", version: "1.2.4" })); + }, useGetSourceFileByPath); + assert.equal(program1.structureIsReused, StructureIsReused.Not); + assert.lengthOf(program2.getSemanticDiagnostics(), 1); }); - - { - const program6Diagnostics = program6.getSemanticDiagnostics(program6.getSourceFile("f2.ts")); - assert.lengthOf(program6Diagnostics, expectedErrors, `import of BB in f1 fails.`); - - assert.deepEqual(program6.host.getTrace(), [ - "======== Resolving module './b1' from 'f1.ts'. ========", - "Explicitly specified module resolution kind: 'Classic'.", - "File 'b1.ts' exist - use it as a name resolution result.", - "======== Module name './b1' was successfully resolved to 'b1.ts'. ========", - "======== Resolving type reference directive 'typerefs2', containing file 'f2.ts', root directory 'node_modules/@types'. ========", - "Resolving with primary search path 'node_modules/@types'.", - "File 'node_modules/@types/typerefs2/package.json' does not exist.", - "File 'node_modules/@types/typerefs2/index.d.ts' exist - use it as a name resolution result.", - "======== Type reference directive 'typerefs2' was successfully resolved to 'node_modules/@types/typerefs2/index.d.ts', primary: true. ========", - "Reusing resolution of module './b2' to file 'f2.ts' from old program.", - "Reusing resolution of module './f1' to file 'f2.ts' from old program." - ], "program_6: reuse module resolutions in f2 since it is unchanged"); - } - - const program7 = updateProgram(program6, program6.getRootFileNames(), options, f => { - const newSourceText = f[indexOfF1].text.updateImportsAndExports(""); - f[indexOfF1] = { name: "f1.ts", text: newSourceText }; + it("Previously duplicate packages -> program structure not reused", () => { + const program1 = createRedirectProgram(useGetSourceFileByPath, { bVersion: "1.2.4", bText: "export = class X { private x: number; }" }); + const program2 = updateRedirectProgram(program1, files => { + updateProgramText(files, bxIndex, "export default class X { private x: number; }"); + updateProgramText(files, bxPackage, JSON.stringify({ name: "x", version: "1.2.3" })); + }, useGetSourceFileByPath); + assert.equal(program1.structureIsReused, StructureIsReused.Not); + assert.deepEqual(program2.getSemanticDiagnostics(), []); }); - - { - const program7Diagnostics = program7.getSemanticDiagnostics(program7.getSourceFile("f2.ts")); - assert.lengthOf(program7Diagnostics, expectedErrors, `removing import is noop with respect to program, so no change in diagnostics.`); - - assert.deepEqual(program7.host.getTrace(), [ - "======== Resolving type reference directive 'typerefs2', containing file 'f2.ts', root directory 'node_modules/@types'. ========", - "Resolving with primary search path 'node_modules/@types'.", - "File 'node_modules/@types/typerefs2/package.json' does not exist.", - "File 'node_modules/@types/typerefs2/index.d.ts' exist - use it as a name resolution result.", - "======== Type reference directive 'typerefs2' was successfully resolved to 'node_modules/@types/typerefs2/index.d.ts', primary: true. ========", - "Reusing resolution of module './b2' to file 'f2.ts' from old program.", - "Reusing resolution of module './f1' to file 'f2.ts' from old program." - ], "program_7 should reuse module resolutions in f2 since it is unchanged"); - } + } + describe("when host implements getSourceFile", () => { + verifyRedirects(/*useGetSourceFileByPath*/ false); }); - - describe("redirects", () => { - const axIndex = "/node_modules/a/node_modules/x/index.d.ts"; - const axPackage = "/node_modules/a/node_modules/x/package.json"; - const bxIndex = "/node_modules/b/node_modules/x/index.d.ts"; - const bxPackage = "/node_modules/b/node_modules/x/package.json"; - const root = "/a.ts"; - const compilerOptions = { target, moduleResolution: ModuleResolutionKind.NodeJs }; - - function createRedirectProgram(useGetSourceFileByPath: boolean, options?: { bText: string, bVersion: string }): ProgramWithSourceTexts { - const files: NamedSourceText[] = [ - { - name: "/node_modules/a/index.d.ts", - text: SourceText.New("", 'import X from "x";', "export function a(x: X): void;"), - }, - { - name: axIndex, - text: SourceText.New("", "", "export default class X { private x: number; }"), - }, - { - name: axPackage, - text: SourceText.New("", "", JSON.stringify({ name: "x", version: "1.2.3" })), - }, - { - name: "/node_modules/b/index.d.ts", - text: SourceText.New("", 'import X from "x";', "export const b: X;"), - }, - { - name: bxIndex, - text: SourceText.New("", "", options ? options.bText : "export default class X { private x: number; }"), - }, - { - name: bxPackage, - text: SourceText.New("", "", JSON.stringify({ name: "x", version: options ? options.bVersion : "1.2.3" })), - }, - { - name: root, - text: SourceText.New("", 'import { a } from "a"; import { b } from "b";', "a(b)"), - }, - ]; - - return newProgram(files, [root], compilerOptions, useGetSourceFileByPath); - } - - function updateRedirectProgram(program: ProgramWithSourceTexts, updater: (files: NamedSourceText[]) => void, useGetSourceFileByPath: boolean): ProgramWithSourceTexts { - return updateProgram(program, [root], compilerOptions, updater, /*newTexts*/ undefined, useGetSourceFileByPath); - } - - function verifyRedirects(useGetSourceFileByPath: boolean) { - it("No changes -> redirect not broken", () => { - const program1 = createRedirectProgram(useGetSourceFileByPath); - - const program2 = updateRedirectProgram(program1, files => { - updateProgramText(files, root, "const x = 1;"); - }, useGetSourceFileByPath); - assert.equal(program1.structureIsReused, StructureIsReused.Completely); - assert.lengthOf(program2.getSemanticDiagnostics(), 0); - }); - - it("Target changes -> redirect broken", () => { - const program1 = createRedirectProgram(useGetSourceFileByPath); - assert.lengthOf(program1.getSemanticDiagnostics(), 0); - - const program2 = updateRedirectProgram(program1, files => { - updateProgramText(files, axIndex, "export default class X { private x: number; private y: number; }"); - updateProgramText(files, axPackage, JSON.stringify('{ name: "x", version: "1.2.4" }')); - }, useGetSourceFileByPath); - assert.equal(program1.structureIsReused, StructureIsReused.Not); - assert.lengthOf(program2.getSemanticDiagnostics(), 1); - }); - - it("Underlying changes -> redirect broken", () => { - const program1 = createRedirectProgram(useGetSourceFileByPath); - - const program2 = updateRedirectProgram(program1, files => { - updateProgramText(files, bxIndex, "export default class X { private x: number; private y: number; }"); - updateProgramText(files, bxPackage, JSON.stringify({ name: "x", version: "1.2.4" })); - }, useGetSourceFileByPath); - assert.equal(program1.structureIsReused, StructureIsReused.Not); - assert.lengthOf(program2.getSemanticDiagnostics(), 1); - }); - - it("Previously duplicate packages -> program structure not reused", () => { - const program1 = createRedirectProgram(useGetSourceFileByPath, { bVersion: "1.2.4", bText: "export = class X { private x: number; }" }); - - const program2 = updateRedirectProgram(program1, files => { - updateProgramText(files, bxIndex, "export default class X { private x: number; }"); - updateProgramText(files, bxPackage, JSON.stringify({ name: "x", version: "1.2.3" })); - }, useGetSourceFileByPath); - assert.equal(program1.structureIsReused, StructureIsReused.Not); - assert.deepEqual(program2.getSemanticDiagnostics(), []); - }); - } - - describe("when host implements getSourceFile", () => { - verifyRedirects(/*useGetSourceFileByPath*/ false); - }); - describe("when host implements getSourceFileByPath", () => { - verifyRedirects(/*useGetSourceFileByPath*/ true); - }); + describe("when host implements getSourceFileByPath", () => { + verifyRedirects(/*useGetSourceFileByPath*/ true); }); }); - - describe("unittests:: Reuse program structure:: host is optional", () => { - it("should work if host is not provided", () => { - createProgram([], {}); - }); +}); +describe("unittests:: Reuse program structure:: host is optional", () => { + it("should work if host is not provided", () => { + createProgram([], {}); }); - - type File = TestFSWithWatch.File; - import createTestSystem = TestFSWithWatch.createWatchedSystem; - import libFile = TestFSWithWatch.libFile; - - describe("unittests:: Reuse program structure:: isProgramUptoDate", () => { - function getWhetherProgramIsUptoDate( - program: Program, - newRootFileNames: string[], - newOptions: CompilerOptions - ) { - return isProgramUptoDate( - program, newRootFileNames, newOptions, - path => program.getSourceFileByPath(path)!.version, /*fileExists*/ returnFalse, - /*hasInvalidatedResolution*/ returnFalse, - /*hasChangedAutomaticTypeDirectiveNames*/ false, - /*projectReferences*/ undefined - ); +}); +type File = TestFSWithWatch.File; +import createTestSystem = ts.TestFSWithWatch.createWatchedSystem; +import libFile = ts.TestFSWithWatch.libFile; +describe("unittests:: Reuse program structure:: isProgramUptoDate", () => { + function getWhetherProgramIsUptoDate(program: Program, newRootFileNames: string[], newOptions: CompilerOptions) { + return isProgramUptoDate(program, newRootFileNames, newOptions, path => program.getSourceFileByPath(path)!.version, returnFalse, returnFalse, + /*hasChangedAutomaticTypeDirectiveNames*/ false, + /*projectReferences*/ undefined); + } + function duplicate(options: CompilerOptions): CompilerOptions; + function duplicate(fileNames: string[]): string[]; + function duplicate(filesOrOptions: CompilerOptions | string[]) { + return JSON.parse(JSON.stringify(filesOrOptions)); + } + describe("should return true when there is no change in compiler options and", () => { + function verifyProgramIsUptoDate(program: Program, newRootFileNames: string[], newOptions: CompilerOptions) { + const actual = getWhetherProgramIsUptoDate(program, newRootFileNames, newOptions); + assert.isTrue(actual); } - - function duplicate(options: CompilerOptions): CompilerOptions; - function duplicate(fileNames: string[]): string[]; - function duplicate(filesOrOptions: CompilerOptions | string[]) { - return JSON.parse(JSON.stringify(filesOrOptions)); + function verifyProgramWithoutConfigFile(system: System, rootFiles: string[], options: CompilerOptions) { + const program = createWatchProgram(createWatchCompilerHostOfFilesAndCompilerOptions(rootFiles, options, /*watchOptions*/ undefined, system)).getCurrentProgram().getProgram(); + verifyProgramIsUptoDate(program, duplicate(rootFiles), duplicate(options)); } - - describe("should return true when there is no change in compiler options and", () => { - function verifyProgramIsUptoDate( - program: Program, - newRootFileNames: string[], - newOptions: CompilerOptions - ) { - const actual = getWhetherProgramIsUptoDate(program, newRootFileNames, newOptions); - assert.isTrue(actual); - } - - function verifyProgramWithoutConfigFile(system: System, rootFiles: string[], options: CompilerOptions) { - const program = createWatchProgram(createWatchCompilerHostOfFilesAndCompilerOptions(rootFiles, options, /*watchOptions*/ undefined, system)).getCurrentProgram().getProgram(); - verifyProgramIsUptoDate(program, duplicate(rootFiles), duplicate(options)); - } - - function verifyProgramWithConfigFile(system: System, configFileName: string) { - const program = createWatchProgram(createWatchCompilerHostOfConfigFile(configFileName, {}, /*watchOptionsToExtend*/ undefined, system)).getCurrentProgram().getProgram(); - const { fileNames, options } = parseConfigFileWithSystem(configFileName, {}, /*watchOptionsToExtend*/ undefined, system, notImplemented)!; // TODO: GH#18217 - verifyProgramIsUptoDate(program, fileNames, options); - } - - function verifyProgram(files: File[], rootFiles: string[], options: CompilerOptions, configFile: string) { - const system = createTestSystem(files); - verifyProgramWithoutConfigFile(system, rootFiles, options); - verifyProgramWithConfigFile(system, configFile); - } - - it("has empty options", () => { - const file1: File = { - path: "/a/b/file1.ts", - content: "let x = 1" - }; - const file2: File = { - path: "/a/b/file2.ts", - content: "let y = 1" - }; - const configFile: File = { - path: "/a/b/tsconfig.json", - content: "{}" - }; - verifyProgram([file1, file2, libFile, configFile], [file1.path, file2.path], {}, configFile.path); - }); - - it("has lib specified in the options", () => { - const compilerOptions: CompilerOptions = { lib: ["es5", "es2015.promise"] }; - const app: File = { - path: "/src/app.ts", - content: "var x: Promise;" - }; - const configFile: File = { - path: "/src/tsconfig.json", - content: JSON.stringify({ compilerOptions }) - }; - const es5Lib: File = { - path: "/compiler/lib.es5.d.ts", - content: "declare const eval: any" - }; - const es2015Promise: File = { - path: "/compiler/lib.es2015.promise.d.ts", - content: "declare class Promise {}" - }; - - verifyProgram([app, configFile, es5Lib, es2015Promise], [app.path], compilerOptions, configFile.path); - }); - - it("has paths specified in the options", () => { - const compilerOptions: CompilerOptions = { - baseUrl: ".", - paths: { - "*": [ - "packages/mail/data/*", - "packages/styles/*", - "*" - ] - } - }; - const app: File = { - path: "/src/packages/framework/app.ts", - content: 'import classc from "module1/lib/file1";\ + function verifyProgramWithConfigFile(system: System, configFileName: string) { + const program = createWatchProgram(createWatchCompilerHostOfConfigFile(configFileName, {}, /*watchOptionsToExtend*/ undefined, system)).getCurrentProgram().getProgram(); + const { fileNames, options } = (parseConfigFileWithSystem(configFileName, {}, /*watchOptionsToExtend*/ undefined, system, notImplemented)!); // TODO: GH#18217 + verifyProgramIsUptoDate(program, fileNames, options); + } + function verifyProgram(files: File[], rootFiles: string[], options: CompilerOptions, configFile: string) { + const system = createTestSystem(files); + verifyProgramWithoutConfigFile(system, rootFiles, options); + verifyProgramWithConfigFile(system, configFile); + } + it("has empty options", () => { + const file1: File = { + path: "/a/b/file1.ts", + content: "let x = 1" + }; + const file2: File = { + path: "/a/b/file2.ts", + content: "let y = 1" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: "{}" + }; + verifyProgram([file1, file2, libFile, configFile], [file1.path, file2.path], {}, configFile.path); + }); + it("has lib specified in the options", () => { + const compilerOptions: CompilerOptions = { lib: ["es5", "es2015.promise"] }; + const app: File = { + path: "/src/app.ts", + content: "var x: Promise;" + }; + const configFile: File = { + path: "/src/tsconfig.json", + content: JSON.stringify({ compilerOptions }) + }; + const es5Lib: File = { + path: "/compiler/lib.es5.d.ts", + content: "declare const eval: any" + }; + const es2015Promise: File = { + path: "/compiler/lib.es2015.promise.d.ts", + content: "declare class Promise {}" + }; + verifyProgram([app, configFile, es5Lib, es2015Promise], [app.path], compilerOptions, configFile.path); + }); + it("has paths specified in the options", () => { + const compilerOptions: CompilerOptions = { + baseUrl: ".", + paths: { + "*": [ + "packages/mail/data/*", + "packages/styles/*", + "*" + ] + } + }; + const app: File = { + path: "/src/packages/framework/app.ts", + content: 'import classc from "module1/lib/file1";\ import classD from "module3/file3";\ let x = new classc();\ let y = new classD();' - }; - const module1: File = { - path: "/src/packages/mail/data/module1/lib/file1.ts", - content: 'import classc from "module2/file2";export default classc;', - }; - const module2: File = { - path: "/src/packages/mail/data/module1/lib/module2/file2.ts", - content: 'class classc { method2() { return "hello"; } }\nexport default classc', - }; - const module3: File = { - path: "/src/packages/styles/module3/file3.ts", - content: "class classD { method() { return 10; } }\nexport default classD;" - }; - const configFile: File = { - path: "/src/tsconfig.json", - content: JSON.stringify({ compilerOptions }) - }; - - verifyProgram([app, module1, module2, module3, libFile, configFile], [app.path], compilerOptions, configFile.path); - }); - - it("has include paths specified in tsconfig file", () => { - const compilerOptions: CompilerOptions = { - baseUrl: ".", - paths: { - "*": [ - "packages/mail/data/*", - "packages/styles/*", - "*" - ] - } - }; - const app: File = { - path: "/src/packages/framework/app.ts", - content: 'import classc from "module1/lib/file1";\ + }; + const module1: File = { + path: "/src/packages/mail/data/module1/lib/file1.ts", + content: 'import classc from "module2/file2";export default classc;', + }; + const module2: File = { + path: "/src/packages/mail/data/module1/lib/module2/file2.ts", + content: 'class classc { method2() { return "hello"; } }\nexport default classc', + }; + const module3: File = { + path: "/src/packages/styles/module3/file3.ts", + content: "class classD { method() { return 10; } }\nexport default classD;" + }; + const configFile: File = { + path: "/src/tsconfig.json", + content: JSON.stringify({ compilerOptions }) + }; + verifyProgram([app, module1, module2, module3, libFile, configFile], [app.path], compilerOptions, configFile.path); + }); + it("has include paths specified in tsconfig file", () => { + const compilerOptions: CompilerOptions = { + baseUrl: ".", + paths: { + "*": [ + "packages/mail/data/*", + "packages/styles/*", + "*" + ] + } + }; + const app: File = { + path: "/src/packages/framework/app.ts", + content: 'import classc from "module1/lib/file1";\ import classD from "module3/file3";\ let x = new classc();\ let y = new classD();' - }; - const module1: File = { - path: "/src/packages/mail/data/module1/lib/file1.ts", - content: 'import classc from "module2/file2";export default classc;', - }; - const module2: File = { - path: "/src/packages/mail/data/module1/lib/module2/file2.ts", - content: 'class classc { method2() { return "hello"; } }\nexport default classc', - }; - const module3: File = { - path: "/src/packages/styles/module3/file3.ts", - content: "class classD { method() { return 10; } }\nexport default classD;" - }; - const configFile: File = { - path: "/src/tsconfig.json", - content: JSON.stringify({ compilerOptions, include: ["packages/**/*.ts"] }) - }; - verifyProgramWithConfigFile(createTestSystem([app, module1, module2, module3, libFile, configFile]), configFile.path); - }); - it("has the same root file names", () => { - const module1: File = { - path: "/src/packages/mail/data/module1/lib/file1.ts", - content: 'import classc from "module2/file2";export default classc;', - }; - const module2: File = { - path: "/src/packages/mail/data/module1/lib/module2/file2.ts", - content: 'class classc { method2() { return "hello"; } }\nexport default classc', - }; - const module3: File = { - path: "/src/packages/styles/module3/file3.ts", - content: "class classD { method() { return 10; } }\nexport default classD;" - }; - const rootFiles = [module1.path, module2.path, module3.path]; - const system = createTestSystem([module1, module2, module3]); - const options = {}; - const program = createWatchProgram(createWatchCompilerHostOfFilesAndCompilerOptions(rootFiles, options, /*watchOptions*/ undefined, system)).getCurrentProgram().getProgram(); - verifyProgramIsUptoDate(program, duplicate(rootFiles), duplicate(options)); - }); - + }; + const module1: File = { + path: "/src/packages/mail/data/module1/lib/file1.ts", + content: 'import classc from "module2/file2";export default classc;', + }; + const module2: File = { + path: "/src/packages/mail/data/module1/lib/module2/file2.ts", + content: 'class classc { method2() { return "hello"; } }\nexport default classc', + }; + const module3: File = { + path: "/src/packages/styles/module3/file3.ts", + content: "class classD { method() { return 10; } }\nexport default classD;" + }; + const configFile: File = { + path: "/src/tsconfig.json", + content: JSON.stringify({ compilerOptions, include: ["packages/**/*.ts"] }) + }; + verifyProgramWithConfigFile(createTestSystem([app, module1, module2, module3, libFile, configFile]), configFile.path); }); - describe("should return false when there is no change in compiler options but", () => { - function verifyProgramIsNotUptoDate( - program: Program, - newRootFileNames: string[], - newOptions: CompilerOptions - ) { - const actual = getWhetherProgramIsUptoDate(program, newRootFileNames, newOptions); - assert.isFalse(actual); - } - it("has more root file names", () => { - const module1: File = { - path: "/src/packages/mail/data/module1/lib/file1.ts", - content: 'import classc from "module2/file2";export default classc;', - }; - const module2: File = { - path: "/src/packages/mail/data/module1/lib/module2/file2.ts", - content: 'class classc { method2() { return "hello"; } }\nexport default classc', - }; - const module3: File = { - path: "/src/packages/styles/module3/file3.ts", - content: "class classD { method() { return 10; } }\nexport default classD;" - }; - const rootFiles = [module1.path, module2.path]; - const newRootFiles = [module1.path, module2.path, module3.path]; - const system = createTestSystem([module1, module2, module3]); - const options = {}; - const program = createWatchProgram(createWatchCompilerHostOfFilesAndCompilerOptions(rootFiles, options, /*watchOptions*/ undefined, system)).getCurrentProgram().getProgram(); - verifyProgramIsNotUptoDate(program, duplicate(newRootFiles), duplicate(options)); - }); - it("has one root file replaced by another", () => { - const module1: File = { - path: "/src/packages/mail/data/module1/lib/file1.ts", - content: 'import classc from "module2/file2";export default classc;', - }; - const module2: File = { - path: "/src/packages/mail/data/module1/lib/module2/file2.ts", - content: 'class classc { method2() { return "hello"; } }\nexport default classc', - }; - const module3: File = { - path: "/src/packages/styles/module3/file3.ts", - content: "class classD { method() { return 10; } }\nexport default classD;" - }; - const rootFiles = [module1.path, module2.path]; - const newRootFiles = [module2.path, module3.path]; - const system = createTestSystem([module1, module2, module3]); - const options = {}; - const program = createWatchProgram(createWatchCompilerHostOfFilesAndCompilerOptions(rootFiles, options, /*watchOptions*/ undefined, system)).getCurrentProgram().getProgram(); - verifyProgramIsNotUptoDate(program, duplicate(newRootFiles), duplicate(options)); - }); + it("has the same root file names", () => { + const module1: File = { + path: "/src/packages/mail/data/module1/lib/file1.ts", + content: 'import classc from "module2/file2";export default classc;', + }; + const module2: File = { + path: "/src/packages/mail/data/module1/lib/module2/file2.ts", + content: 'class classc { method2() { return "hello"; } }\nexport default classc', + }; + const module3: File = { + path: "/src/packages/styles/module3/file3.ts", + content: "class classD { method() { return 10; } }\nexport default classD;" + }; + const rootFiles = [module1.path, module2.path, module3.path]; + const system = createTestSystem([module1, module2, module3]); + const options = {}; + const program = createWatchProgram(createWatchCompilerHostOfFilesAndCompilerOptions(rootFiles, options, /*watchOptions*/ undefined, system)).getCurrentProgram().getProgram(); + verifyProgramIsUptoDate(program, duplicate(rootFiles), duplicate(options)); }); }); -} + describe("should return false when there is no change in compiler options but", () => { + function verifyProgramIsNotUptoDate(program: Program, newRootFileNames: string[], newOptions: CompilerOptions) { + const actual = getWhetherProgramIsUptoDate(program, newRootFileNames, newOptions); + assert.isFalse(actual); + } + it("has more root file names", () => { + const module1: File = { + path: "/src/packages/mail/data/module1/lib/file1.ts", + content: 'import classc from "module2/file2";export default classc;', + }; + const module2: File = { + path: "/src/packages/mail/data/module1/lib/module2/file2.ts", + content: 'class classc { method2() { return "hello"; } }\nexport default classc', + }; + const module3: File = { + path: "/src/packages/styles/module3/file3.ts", + content: "class classD { method() { return 10; } }\nexport default classD;" + }; + const rootFiles = [module1.path, module2.path]; + const newRootFiles = [module1.path, module2.path, module3.path]; + const system = createTestSystem([module1, module2, module3]); + const options = {}; + const program = createWatchProgram(createWatchCompilerHostOfFilesAndCompilerOptions(rootFiles, options, /*watchOptions*/ undefined, system)).getCurrentProgram().getProgram(); + verifyProgramIsNotUptoDate(program, duplicate(newRootFiles), duplicate(options)); + }); + it("has one root file replaced by another", () => { + const module1: File = { + path: "/src/packages/mail/data/module1/lib/file1.ts", + content: 'import classc from "module2/file2";export default classc;', + }; + const module2: File = { + path: "/src/packages/mail/data/module1/lib/module2/file2.ts", + content: 'class classc { method2() { return "hello"; } }\nexport default classc', + }; + const module3: File = { + path: "/src/packages/styles/module3/file3.ts", + content: "class classD { method() { return 10; } }\nexport default classD;" + }; + const rootFiles = [module1.path, module2.path]; + const newRootFiles = [module2.path, module3.path]; + const system = createTestSystem([module1, module2, module3]); + const options = {}; + const program = createWatchProgram(createWatchCompilerHostOfFilesAndCompilerOptions(rootFiles, options, /*watchOptions*/ undefined, system)).getCurrentProgram().getProgram(); + verifyProgramIsNotUptoDate(program, duplicate(newRootFiles), duplicate(options)); + }); + }); +}); diff --git a/src/testRunner/unittests/semver.ts b/src/testRunner/unittests/semver.ts index b3447ca77b09e..0008ce01ad1c8 100644 --- a/src/testRunner/unittests/semver.ts +++ b/src/testRunner/unittests/semver.ts @@ -1,228 +1,221 @@ -namespace ts { - import theory = Utils.theory; - describe("unittests:: semver", () => { - describe("Version", () => { - function assertVersion(version: Version, [major, minor, patch, prerelease, build]: [number, number, number, string[]?, string[]?]) { - assert.strictEqual(version.major, major); - assert.strictEqual(version.minor, minor); - assert.strictEqual(version.patch, patch); - assert.deepEqual(version.prerelease, prerelease || emptyArray); - assert.deepEqual(version.build, build || emptyArray); - } - describe("new", () => { - it("text", () => { - assertVersion(new Version("1.2.3-pre.4+build.5"), [1, 2, 3, ["pre", "4"], ["build", "5"]]); - }); - it("parts", () => { - assertVersion(new Version(1, 2, 3, "pre.4", "build.5"), [1, 2, 3, ["pre", "4"], ["build", "5"]]); - assertVersion(new Version(1, 2, 3), [1, 2, 3]); - assertVersion(new Version(1, 2), [1, 2, 0]); - assertVersion(new Version(1), [1, 0, 0]); - }); +import { Version, emptyArray, Comparison, VersionRange } from "../ts"; +import * as Utils from "../Utils"; +import theory = Utils.theory; +describe("unittests:: semver", () => { + describe("Version", () => { + function assertVersion(version: Version, [major, minor, patch, prerelease, build]: [number, number, number, string[]?, string[]?]) { + assert.strictEqual(version.major, major); + assert.strictEqual(version.minor, minor); + assert.strictEqual(version.patch, patch); + assert.deepEqual(version.prerelease, prerelease || emptyArray); + assert.deepEqual(version.build, build || emptyArray); + } + describe("new", () => { + it("text", () => { + assertVersion(new Version("1.2.3-pre.4+build.5"), [1, 2, 3, ["pre", "4"], ["build", "5"]]); }); - it("toString", () => { - assert.strictEqual(new Version(1, 2, 3, "pre.4", "build.5").toString(), "1.2.3-pre.4+build.5"); - assert.strictEqual(new Version(1, 2, 3, "pre.4").toString(), "1.2.3-pre.4"); - assert.strictEqual(new Version(1, 2, 3, /*prerelease*/ undefined, "build.5").toString(), "1.2.3+build.5"); - assert.strictEqual(new Version(1, 2, 3).toString(), "1.2.3"); - assert.strictEqual(new Version(1, 2).toString(), "1.2.0"); - assert.strictEqual(new Version(1).toString(), "1.0.0"); - }); - it("compareTo", () => { - // https://semver.org/#spec-item-11 - // > Precedence is determined by the first difference when comparing each of these - // > identifiers from left to right as follows: Major, minor, and patch versions are - // > always compared numerically. - assert.strictEqual(new Version("1.0.0").compareTo(new Version("2.0.0")), Comparison.LessThan); - assert.strictEqual(new Version("1.0.0").compareTo(new Version("1.1.0")), Comparison.LessThan); - assert.strictEqual(new Version("1.0.0").compareTo(new Version("1.0.1")), Comparison.LessThan); - assert.strictEqual(new Version("2.0.0").compareTo(new Version("1.0.0")), Comparison.GreaterThan); - assert.strictEqual(new Version("1.1.0").compareTo(new Version("1.0.0")), Comparison.GreaterThan); - assert.strictEqual(new Version("1.0.1").compareTo(new Version("1.0.0")), Comparison.GreaterThan); - assert.strictEqual(new Version("1.0.0").compareTo(new Version("1.0.0")), Comparison.EqualTo); - - // https://semver.org/#spec-item-11 - // > When major, minor, and patch are equal, a pre-release version has lower - // > precedence than a normal version. - assert.strictEqual(new Version("1.0.0").compareTo(new Version("1.0.0-pre")), Comparison.GreaterThan); - assert.strictEqual(new Version("1.0.1-pre").compareTo(new Version("1.0.0")), Comparison.GreaterThan); - assert.strictEqual(new Version("1.0.0-pre").compareTo(new Version("1.0.0")), Comparison.LessThan); - - // https://semver.org/#spec-item-11 - // > identifiers consisting of only digits are compared numerically - assert.strictEqual(new Version("1.0.0-0").compareTo(new Version("1.0.0-1")), Comparison.LessThan); - assert.strictEqual(new Version("1.0.0-1").compareTo(new Version("1.0.0-0")), Comparison.GreaterThan); - assert.strictEqual(new Version("1.0.0-2").compareTo(new Version("1.0.0-10")), Comparison.LessThan); - assert.strictEqual(new Version("1.0.0-10").compareTo(new Version("1.0.0-2")), Comparison.GreaterThan); - assert.strictEqual(new Version("1.0.0-0").compareTo(new Version("1.0.0-0")), Comparison.EqualTo); - - // https://semver.org/#spec-item-11 - // > identifiers with letters or hyphens are compared lexically in ASCII sort order. - assert.strictEqual(new Version("1.0.0-a").compareTo(new Version("1.0.0-b")), Comparison.LessThan); - assert.strictEqual(new Version("1.0.0-a-2").compareTo(new Version("1.0.0-a-10")), Comparison.GreaterThan); - assert.strictEqual(new Version("1.0.0-b").compareTo(new Version("1.0.0-a")), Comparison.GreaterThan); - assert.strictEqual(new Version("1.0.0-a").compareTo(new Version("1.0.0-a")), Comparison.EqualTo); - assert.strictEqual(new Version("1.0.0-A").compareTo(new Version("1.0.0-a")), Comparison.LessThan); - - // https://semver.org/#spec-item-11 - // > Numeric identifiers always have lower precedence than non-numeric identifiers. - assert.strictEqual(new Version("1.0.0-0").compareTo(new Version("1.0.0-alpha")), Comparison.LessThan); - assert.strictEqual(new Version("1.0.0-alpha").compareTo(new Version("1.0.0-0")), Comparison.GreaterThan); - assert.strictEqual(new Version("1.0.0-0").compareTo(new Version("1.0.0-0")), Comparison.EqualTo); - assert.strictEqual(new Version("1.0.0-alpha").compareTo(new Version("1.0.0-alpha")), Comparison.EqualTo); - - // https://semver.org/#spec-item-11 - // > A larger set of pre-release fields has a higher precedence than a smaller set, if all - // > of the preceding identifiers are equal. - assert.strictEqual(new Version("1.0.0-alpha").compareTo(new Version("1.0.0-alpha.0")), Comparison.LessThan); - assert.strictEqual(new Version("1.0.0-alpha.0").compareTo(new Version("1.0.0-alpha")), Comparison.GreaterThan); - - // https://semver.org/#spec-item-11 - // > Precedence for two pre-release versions with the same major, minor, and patch version - // > MUST be determined by comparing each dot separated identifier from left to right until - // > a difference is found [...] - assert.strictEqual(new Version("1.0.0-a.0.b.1").compareTo(new Version("1.0.0-a.0.b.2")), Comparison.LessThan); - assert.strictEqual(new Version("1.0.0-a.0.b.1").compareTo(new Version("1.0.0-b.0.a.1")), Comparison.LessThan); - assert.strictEqual(new Version("1.0.0-a.0.b.2").compareTo(new Version("1.0.0-a.0.b.1")), Comparison.GreaterThan); - assert.strictEqual(new Version("1.0.0-b.0.a.1").compareTo(new Version("1.0.0-a.0.b.1")), Comparison.GreaterThan); - - // https://semver.org/#spec-item-11 - // > Build metadata does not figure into precedence - assert.strictEqual(new Version("1.0.0+build").compareTo(new Version("1.0.0")), Comparison.EqualTo); - }); - it("increment", () => { - assertVersion(new Version(1, 2, 3, "pre.4", "build.5").increment("major"), [2, 0, 0]); - assertVersion(new Version(1, 2, 3, "pre.4", "build.5").increment("minor"), [1, 3, 0]); - assertVersion(new Version(1, 2, 3, "pre.4", "build.5").increment("patch"), [1, 2, 4]); + it("parts", () => { + assertVersion(new Version(1, 2, 3, "pre.4", "build.5"), [1, 2, 3, ["pre", "4"], ["build", "5"]]); + assertVersion(new Version(1, 2, 3), [1, 2, 3]); + assertVersion(new Version(1, 2), [1, 2, 0]); + assertVersion(new Version(1), [1, 0, 0]); }); }); - describe("VersionRange", () => { - function assertRange(rangeText: string, versionText: string, inRange = true) { - const range = new VersionRange(rangeText); - const version = new Version(versionText); - assert.strictEqual(range.test(version), inRange, `Expected version '${version}' ${inRange ? `to be` : `to not be`} in range '${rangeText}' (${range})`); - } - theory("comparators", assertRange, [ - ["", "1.0.0"], - ["*", "1.0.0"], - ["1", "1.0.0"], - ["1", "2.0.0", false], - ["1.0", "1.0.0"], - ["1.0", "1.1.0", false], - ["1.0.0", "1.0.0"], - ["1.0.0", "1.0.1", false], - ["1.*", "1.0.0"], - ["1.*", "2.0.0", false], - ["1.x", "1.0.0"], - ["1.x", "2.0.0", false], - ["=1", "1.0.0"], - ["=1", "1.1.0"], - ["=1", "1.0.1"], - ["=1.0", "1.0.0"], - ["=1.0", "1.0.1"], - ["=1.0.0", "1.0.0"], - ["=*", "0.0.0"], - ["=*", "1.0.0"], - [">1", "2"], - [">1.0", "1.1"], - [">1.0.0", "1.0.1"], - [">1.0.0", "1.0.1-pre"], - [">*", "0.0.0", false], - [">*", "1.0.0", false], - [">=1", "1.0.0"], - [">=1.0", "1.0.0"], - [">=1.0.0", "1.0.0"], - [">=1.0.0", "1.0.1-pre"], - [">=*", "0.0.0"], - [">=*", "1.0.0"], - ["<2", "1.0.0"], - ["<2.1", "2.0.0"], - ["<2.0.1", "2.0.0"], - ["<2.0.0", "2.0.0-pre"], - ["<*", "0.0.0", false], - ["<*", "1.0.0", false], - ["<=2", "2.0.0"], - ["<=2.1", "2.1.0"], - ["<=2.0.1", "2.0.1"], - ["<=*", "0.0.0"], - ["<=*", "1.0.0"], - ]); - theory("conjunctions", assertRange, [ - [">1.0.0 <2.0.0", "1.0.1"], - [">1.0.0 <2.0.0", "2.0.0", false], - [">1.0.0 <2.0.0", "1.0.0", false], - [">1 >2", "3.0.0"], - ]); - theory("disjunctions", assertRange, [ - [">=1.0.0 <2.0.0 || >=3.0.0 <4.0.0", "1.0.0"], - [">=1.0.0 <2.0.0 || >=3.0.0 <4.0.0", "2.0.0", false], - [">=1.0.0 <2.0.0 || >=3.0.0 <4.0.0", "3.0.0"], - ]); - theory("hyphen", assertRange, [ - ["1.0.0 - 2.0.0", "1.0.0"], - ["1.0.0 - 2.0.0", "2.0.0"], - ["1.0.0 - 2.0.0", "3.0.0", false], - ]); - theory("tilde", assertRange, [ - ["~0", "0.0.0"], - ["~0", "0.1.0"], - ["~0", "0.1.2"], - ["~0", "0.1.9"], - ["~0", "1.0.0", false], - ["~0.1", "0.1.0"], - ["~0.1", "0.1.2"], - ["~0.1", "0.1.9"], - ["~0.1", "0.2.0", false], - ["~0.1.2", "0.1.2"], - ["~0.1.2", "0.1.9"], - ["~0.1.2", "0.2.0", false], - ["~1", "1.0.0"], - ["~1", "1.2.0"], - ["~1", "1.2.3"], - ["~1", "1.2.0"], - ["~1", "1.2.3"], - ["~1", "0.0.0", false], - ["~1", "2.0.0", false], - ["~1.2", "1.2.0"], - ["~1.2", "1.2.3"], - ["~1.2", "1.1.0", false], - ["~1.2", "1.3.0", false], - ["~1.2.3", "1.2.3"], - ["~1.2.3", "1.2.9"], - ["~1.2.3", "1.1.0", false], - ["~1.2.3", "1.3.0", false], - ]); - theory("caret", assertRange, [ - ["^0", "0.0.0"], - ["^0", "0.1.0"], - ["^0", "0.9.0"], - ["^0", "0.1.2"], - ["^0", "0.1.9"], - ["^0", "1.0.0", false], - ["^0.1", "0.1.0"], - ["^0.1", "0.1.2"], - ["^0.1", "0.1.9"], - ["^0.1.2", "0.1.2"], - ["^0.1.2", "0.1.9"], - ["^0.1.2", "0.0.0", false], - ["^0.1.2", "0.2.0", false], - ["^0.1.2", "1.0.0", false], - ["^1", "1.0.0"], - ["^1", "1.2.0"], - ["^1", "1.2.3"], - ["^1", "1.9.0"], - ["^1", "0.0.0", false], - ["^1", "2.0.0", false], - ["^1.2", "1.2.0"], - ["^1.2", "1.2.3"], - ["^1.2", "1.9.0"], - ["^1.2", "1.1.0", false], - ["^1.2", "2.0.0", false], - ["^1.2.3", "1.2.3"], - ["^1.2.3", "1.9.0"], - ["^1.2.3", "1.2.2", false], - ["^1.2.3", "2.0.0", false], - ]); + it("toString", () => { + assert.strictEqual(new Version(1, 2, 3, "pre.4", "build.5").toString(), "1.2.3-pre.4+build.5"); + assert.strictEqual(new Version(1, 2, 3, "pre.4").toString(), "1.2.3-pre.4"); + assert.strictEqual(new Version(1, 2, 3, /*prerelease*/ undefined, "build.5").toString(), "1.2.3+build.5"); + assert.strictEqual(new Version(1, 2, 3).toString(), "1.2.3"); + assert.strictEqual(new Version(1, 2).toString(), "1.2.0"); + assert.strictEqual(new Version(1).toString(), "1.0.0"); + }); + it("compareTo", () => { + // https://semver.org/#spec-item-11 + // > Precedence is determined by the first difference when comparing each of these + // > identifiers from left to right as follows: Major, minor, and patch versions are + // > always compared numerically. + assert.strictEqual(new Version("1.0.0").compareTo(new Version("2.0.0")), Comparison.LessThan); + assert.strictEqual(new Version("1.0.0").compareTo(new Version("1.1.0")), Comparison.LessThan); + assert.strictEqual(new Version("1.0.0").compareTo(new Version("1.0.1")), Comparison.LessThan); + assert.strictEqual(new Version("2.0.0").compareTo(new Version("1.0.0")), Comparison.GreaterThan); + assert.strictEqual(new Version("1.1.0").compareTo(new Version("1.0.0")), Comparison.GreaterThan); + assert.strictEqual(new Version("1.0.1").compareTo(new Version("1.0.0")), Comparison.GreaterThan); + assert.strictEqual(new Version("1.0.0").compareTo(new Version("1.0.0")), Comparison.EqualTo); + // https://semver.org/#spec-item-11 + // > When major, minor, and patch are equal, a pre-release version has lower + // > precedence than a normal version. + assert.strictEqual(new Version("1.0.0").compareTo(new Version("1.0.0-pre")), Comparison.GreaterThan); + assert.strictEqual(new Version("1.0.1-pre").compareTo(new Version("1.0.0")), Comparison.GreaterThan); + assert.strictEqual(new Version("1.0.0-pre").compareTo(new Version("1.0.0")), Comparison.LessThan); + // https://semver.org/#spec-item-11 + // > identifiers consisting of only digits are compared numerically + assert.strictEqual(new Version("1.0.0-0").compareTo(new Version("1.0.0-1")), Comparison.LessThan); + assert.strictEqual(new Version("1.0.0-1").compareTo(new Version("1.0.0-0")), Comparison.GreaterThan); + assert.strictEqual(new Version("1.0.0-2").compareTo(new Version("1.0.0-10")), Comparison.LessThan); + assert.strictEqual(new Version("1.0.0-10").compareTo(new Version("1.0.0-2")), Comparison.GreaterThan); + assert.strictEqual(new Version("1.0.0-0").compareTo(new Version("1.0.0-0")), Comparison.EqualTo); + // https://semver.org/#spec-item-11 + // > identifiers with letters or hyphens are compared lexically in ASCII sort order. + assert.strictEqual(new Version("1.0.0-a").compareTo(new Version("1.0.0-b")), Comparison.LessThan); + assert.strictEqual(new Version("1.0.0-a-2").compareTo(new Version("1.0.0-a-10")), Comparison.GreaterThan); + assert.strictEqual(new Version("1.0.0-b").compareTo(new Version("1.0.0-a")), Comparison.GreaterThan); + assert.strictEqual(new Version("1.0.0-a").compareTo(new Version("1.0.0-a")), Comparison.EqualTo); + assert.strictEqual(new Version("1.0.0-A").compareTo(new Version("1.0.0-a")), Comparison.LessThan); + // https://semver.org/#spec-item-11 + // > Numeric identifiers always have lower precedence than non-numeric identifiers. + assert.strictEqual(new Version("1.0.0-0").compareTo(new Version("1.0.0-alpha")), Comparison.LessThan); + assert.strictEqual(new Version("1.0.0-alpha").compareTo(new Version("1.0.0-0")), Comparison.GreaterThan); + assert.strictEqual(new Version("1.0.0-0").compareTo(new Version("1.0.0-0")), Comparison.EqualTo); + assert.strictEqual(new Version("1.0.0-alpha").compareTo(new Version("1.0.0-alpha")), Comparison.EqualTo); + // https://semver.org/#spec-item-11 + // > A larger set of pre-release fields has a higher precedence than a smaller set, if all + // > of the preceding identifiers are equal. + assert.strictEqual(new Version("1.0.0-alpha").compareTo(new Version("1.0.0-alpha.0")), Comparison.LessThan); + assert.strictEqual(new Version("1.0.0-alpha.0").compareTo(new Version("1.0.0-alpha")), Comparison.GreaterThan); + // https://semver.org/#spec-item-11 + // > Precedence for two pre-release versions with the same major, minor, and patch version + // > MUST be determined by comparing each dot separated identifier from left to right until + // > a difference is found [...] + assert.strictEqual(new Version("1.0.0-a.0.b.1").compareTo(new Version("1.0.0-a.0.b.2")), Comparison.LessThan); + assert.strictEqual(new Version("1.0.0-a.0.b.1").compareTo(new Version("1.0.0-b.0.a.1")), Comparison.LessThan); + assert.strictEqual(new Version("1.0.0-a.0.b.2").compareTo(new Version("1.0.0-a.0.b.1")), Comparison.GreaterThan); + assert.strictEqual(new Version("1.0.0-b.0.a.1").compareTo(new Version("1.0.0-a.0.b.1")), Comparison.GreaterThan); + // https://semver.org/#spec-item-11 + // > Build metadata does not figure into precedence + assert.strictEqual(new Version("1.0.0+build").compareTo(new Version("1.0.0")), Comparison.EqualTo); }); + it("increment", () => { + assertVersion(new Version(1, 2, 3, "pre.4", "build.5").increment("major"), [2, 0, 0]); + assertVersion(new Version(1, 2, 3, "pre.4", "build.5").increment("minor"), [1, 3, 0]); + assertVersion(new Version(1, 2, 3, "pre.4", "build.5").increment("patch"), [1, 2, 4]); + }); + }); + describe("VersionRange", () => { + function assertRange(rangeText: string, versionText: string, inRange = true) { + const range = new VersionRange(rangeText); + const version = new Version(versionText); + assert.strictEqual(range.test(version), inRange, `Expected version '${version}' ${inRange ? `to be` : `to not be`} in range '${rangeText}' (${range})`); + } + theory("comparators", assertRange, [ + ["", "1.0.0"], + ["*", "1.0.0"], + ["1", "1.0.0"], + ["1", "2.0.0", false], + ["1.0", "1.0.0"], + ["1.0", "1.1.0", false], + ["1.0.0", "1.0.0"], + ["1.0.0", "1.0.1", false], + ["1.*", "1.0.0"], + ["1.*", "2.0.0", false], + ["1.x", "1.0.0"], + ["1.x", "2.0.0", false], + ["=1", "1.0.0"], + ["=1", "1.1.0"], + ["=1", "1.0.1"], + ["=1.0", "1.0.0"], + ["=1.0", "1.0.1"], + ["=1.0.0", "1.0.0"], + ["=*", "0.0.0"], + ["=*", "1.0.0"], + [">1", "2"], + [">1.0", "1.1"], + [">1.0.0", "1.0.1"], + [">1.0.0", "1.0.1-pre"], + [">*", "0.0.0", false], + [">*", "1.0.0", false], + [">=1", "1.0.0"], + [">=1.0", "1.0.0"], + [">=1.0.0", "1.0.0"], + [">=1.0.0", "1.0.1-pre"], + [">=*", "0.0.0"], + [">=*", "1.0.0"], + ["<2", "1.0.0"], + ["<2.1", "2.0.0"], + ["<2.0.1", "2.0.0"], + ["<2.0.0", "2.0.0-pre"], + ["<*", "0.0.0", false], + ["<*", "1.0.0", false], + ["<=2", "2.0.0"], + ["<=2.1", "2.1.0"], + ["<=2.0.1", "2.0.1"], + ["<=*", "0.0.0"], + ["<=*", "1.0.0"], + ]); + theory("conjunctions", assertRange, [ + [">1.0.0 <2.0.0", "1.0.1"], + [">1.0.0 <2.0.0", "2.0.0", false], + [">1.0.0 <2.0.0", "1.0.0", false], + [">1 >2", "3.0.0"], + ]); + theory("disjunctions", assertRange, [ + [">=1.0.0 <2.0.0 || >=3.0.0 <4.0.0", "1.0.0"], + [">=1.0.0 <2.0.0 || >=3.0.0 <4.0.0", "2.0.0", false], + [">=1.0.0 <2.0.0 || >=3.0.0 <4.0.0", "3.0.0"], + ]); + theory("hyphen", assertRange, [ + ["1.0.0 - 2.0.0", "1.0.0"], + ["1.0.0 - 2.0.0", "2.0.0"], + ["1.0.0 - 2.0.0", "3.0.0", false], + ]); + theory("tilde", assertRange, [ + ["~0", "0.0.0"], + ["~0", "0.1.0"], + ["~0", "0.1.2"], + ["~0", "0.1.9"], + ["~0", "1.0.0", false], + ["~0.1", "0.1.0"], + ["~0.1", "0.1.2"], + ["~0.1", "0.1.9"], + ["~0.1", "0.2.0", false], + ["~0.1.2", "0.1.2"], + ["~0.1.2", "0.1.9"], + ["~0.1.2", "0.2.0", false], + ["~1", "1.0.0"], + ["~1", "1.2.0"], + ["~1", "1.2.3"], + ["~1", "1.2.0"], + ["~1", "1.2.3"], + ["~1", "0.0.0", false], + ["~1", "2.0.0", false], + ["~1.2", "1.2.0"], + ["~1.2", "1.2.3"], + ["~1.2", "1.1.0", false], + ["~1.2", "1.3.0", false], + ["~1.2.3", "1.2.3"], + ["~1.2.3", "1.2.9"], + ["~1.2.3", "1.1.0", false], + ["~1.2.3", "1.3.0", false], + ]); + theory("caret", assertRange, [ + ["^0", "0.0.0"], + ["^0", "0.1.0"], + ["^0", "0.9.0"], + ["^0", "0.1.2"], + ["^0", "0.1.9"], + ["^0", "1.0.0", false], + ["^0.1", "0.1.0"], + ["^0.1", "0.1.2"], + ["^0.1", "0.1.9"], + ["^0.1.2", "0.1.2"], + ["^0.1.2", "0.1.9"], + ["^0.1.2", "0.0.0", false], + ["^0.1.2", "0.2.0", false], + ["^0.1.2", "1.0.0", false], + ["^1", "1.0.0"], + ["^1", "1.2.0"], + ["^1", "1.2.3"], + ["^1", "1.9.0"], + ["^1", "0.0.0", false], + ["^1", "2.0.0", false], + ["^1.2", "1.2.0"], + ["^1.2", "1.2.3"], + ["^1.2", "1.9.0"], + ["^1.2", "1.1.0", false], + ["^1.2", "2.0.0", false], + ["^1.2.3", "1.2.3"], + ["^1.2.3", "1.9.0"], + ["^1.2.3", "1.2.2", false], + ["^1.2.3", "2.0.0", false], + ]); }); -} +}); diff --git a/src/testRunner/unittests/services/cancellableLanguageServiceOperations.ts b/src/testRunner/unittests/services/cancellableLanguageServiceOperations.ts index bd8ed2714acd5..204b50972141f 100644 --- a/src/testRunner/unittests/services/cancellableLanguageServiceOperations.ts +++ b/src/testRunner/unittests/services/cancellableLanguageServiceOperations.ts @@ -1,95 +1,84 @@ -namespace ts { - describe("unittests:: services:: cancellableLanguageServiceOperations", () => { - const file = ` +import { emptyOptions, FormatCodeSettings, IndentStyle, LanguageService, CompilerOptions, HostCancellationToken, OperationCanceledException } from "../../ts"; +import * as Harness from "../../Harness"; +describe("unittests:: services:: cancellableLanguageServiceOperations", () => { + const file = ` function foo(): void; function foo(x: T): T; function foo(x?: T): T | void {} foo(f); `; - it("can cancel signature help mid-request", () => { - verifyOperationCancelledAfter(file, 4, service => // Two calls are top-level in services, one is the root type, and the second should be for the parameter type - service.getSignatureHelpItems("file.ts", file.lastIndexOf("f"), emptyOptions)!, r => assert.exists(r.items[0]) - ); - }); - - it("can cancel find all references mid-request", () => { - verifyOperationCancelledAfter(file, 3, service => // Two calls are top-level in services, one is the root type - service.findReferences("file.ts", file.lastIndexOf("o"))!, r => assert.exists(r[0].definition) - ); - }); - - it("can cancel quick info mid-request", () => { - verifyOperationCancelledAfter(file, 1, service => // The LS doesn't do any top-level checks on the token for quickinfo, so the first check is within the checker - service.getQuickInfoAtPosition("file.ts", file.lastIndexOf("o"))!, r => assert.exists(r.displayParts) - ); - }); - - it("can cancel completion entry details mid-request", () => { - const options: FormatCodeSettings = { - indentSize: 4, - tabSize: 4, - newLineCharacter: "\n", - convertTabsToSpaces: true, - indentStyle: IndentStyle.Smart, - insertSpaceAfterConstructor: false, - insertSpaceAfterCommaDelimiter: true, - insertSpaceAfterSemicolonInForStatements: true, - insertSpaceBeforeAndAfterBinaryOperators: true, - insertSpaceAfterKeywordsInControlFlowStatements: true, - insertSpaceAfterFunctionKeywordForAnonymousFunctions: false, - insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false, - insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false, - insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: true, - insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: false, - insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: false, - insertSpaceBeforeFunctionParenthesis: false, - placeOpenBraceOnNewLineForFunctions: false, - placeOpenBraceOnNewLineForControlBlocks: false, - }; - verifyOperationCancelledAfter(file, 1, service => // The LS doesn't do any top-level checks on the token for completion entry details, so the first check is within the checker - service.getCompletionEntryDetails("file.ts", file.lastIndexOf("f"), "foo", options, /*content*/ undefined, {})!, r => assert.exists(r.displayParts) - ); - }); - - it("can cancel suggestion diagnostics mid-request", () => { - verifyOperationCancelledAfter(file, 1, service => // The LS doesn't do any top-level checks on the token for suggestion diagnostics, so the first check is within the checker - service.getSuggestionDiagnostics("file.js"), r => assert.notEqual(r.length, 0), "file.js", "function foo() { let a = 10; }", { allowJs: true } - ); - }); + it("can cancel signature help mid-request", () => { + verifyOperationCancelledAfter(file, 4, service => // Two calls are top-level in services, one is the root type, and the second should be for the parameter type + service.getSignatureHelpItems("file.ts", file.lastIndexOf("f"), emptyOptions)!, r => assert.exists(r.items[0])); }); - - function verifyOperationCancelledAfter(content: string, cancelAfter: number, operation: (service: LanguageService) => T, validator: (arg: T) => void, fileName?: string, fileContent?: string, options?: CompilerOptions) { - let checks = 0; - const token: HostCancellationToken = { - isCancellationRequested() { - checks++; - const result = checks >= cancelAfter; - if (result) { - checks = -Infinity; // Cancel just once, then disable cancellation, effectively - } - return result; - } + it("can cancel find all references mid-request", () => { + verifyOperationCancelledAfter(file, 3, service => // Two calls are top-level in services, one is the root type + service.findReferences("file.ts", file.lastIndexOf("o"))!, r => assert.exists(r[0].definition)); + }); + it("can cancel quick info mid-request", () => { + verifyOperationCancelledAfter(file, 1, service => // The LS doesn't do any top-level checks on the token for quickinfo, so the first check is within the checker + service.getQuickInfoAtPosition("file.ts", file.lastIndexOf("o"))!, r => assert.exists(r.displayParts)); + }); + it("can cancel completion entry details mid-request", () => { + const options: FormatCodeSettings = { + indentSize: 4, + tabSize: 4, + newLineCharacter: "\n", + convertTabsToSpaces: true, + indentStyle: IndentStyle.Smart, + insertSpaceAfterConstructor: false, + insertSpaceAfterCommaDelimiter: true, + insertSpaceAfterSemicolonInForStatements: true, + insertSpaceBeforeAndAfterBinaryOperators: true, + insertSpaceAfterKeywordsInControlFlowStatements: true, + insertSpaceAfterFunctionKeywordForAnonymousFunctions: false, + insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false, + insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false, + insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: true, + insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: false, + insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: false, + insertSpaceBeforeFunctionParenthesis: false, + placeOpenBraceOnNewLineForFunctions: false, + placeOpenBraceOnNewLineForControlBlocks: false, }; - const adapter = new Harness.LanguageService.NativeLanguageServiceAdapter(token, options); - const host = adapter.getHost(); - host.addScript(fileName || "file.ts", fileContent || content, /*isRootFile*/ true); - const service = adapter.getLanguageService(); - assertCancelled(() => operation(service)); - validator(operation(service)); - } - - /** - * We don't just use `assert.throws` because it doesn't validate instances for thrown objects which do not inherit from `Error` - */ - function assertCancelled(cb: () => void) { - let caught: any; - try { - cb(); - } - catch (e) { - caught = e; + verifyOperationCancelledAfter(file, 1, service => // The LS doesn't do any top-level checks on the token for completion entry details, so the first check is within the checker + service.getCompletionEntryDetails("file.ts", file.lastIndexOf("f"), "foo", options, /*content*/ undefined, {})!, r => assert.exists(r.displayParts)); + }); + it("can cancel suggestion diagnostics mid-request", () => { + verifyOperationCancelledAfter(file, 1, service => // The LS doesn't do any top-level checks on the token for suggestion diagnostics, so the first check is within the checker + service.getSuggestionDiagnostics("file.js"), r => assert.notEqual(r.length, 0), "file.js", "function foo() { let a = 10; }", { allowJs: true }); + }); +}); +function verifyOperationCancelledAfter(content: string, cancelAfter: number, operation: (service: LanguageService) => T, validator: (arg: T) => void, fileName?: string, fileContent?: string, options?: CompilerOptions) { + let checks = 0; + const token: HostCancellationToken = { + isCancellationRequested() { + checks++; + const result = checks >= cancelAfter; + if (result) { + checks = -Infinity; // Cancel just once, then disable cancellation, effectively + } + return result; } - assert.exists(caught, "Expected operation to be cancelled, but was not"); - assert.instanceOf(caught, OperationCanceledException); + }; + const adapter = new Harness.LanguageService.NativeLanguageServiceAdapter(token, options); + const host = adapter.getHost(); + host.addScript(fileName || "file.ts", fileContent || content, /*isRootFile*/ true); + const service = adapter.getLanguageService(); + assertCancelled(() => operation(service)); + validator(operation(service)); +} +/** + * We don't just use `assert.throws` because it doesn't validate instances for thrown objects which do not inherit from `Error` + */ +function assertCancelled(cb: () => void) { + let caught: any; + try { + cb(); + } + catch (e) { + caught = e; } + assert.exists(caught, "Expected operation to be cancelled, but was not"); + assert.instanceOf(caught, OperationCanceledException); } diff --git a/src/testRunner/unittests/services/colorization.ts b/src/testRunner/unittests/services/colorization.ts index c8052cfc17b0e..fc0aa6a0b3c0a 100644 --- a/src/testRunner/unittests/services/colorization.ts +++ b/src/testRunner/unittests/services/colorization.ts @@ -1,18 +1,17 @@ +import { TokenClass, ClassificationResult, EndOfLineState, last } from "../../ts"; +import { LanguageService } from "../../Harness"; // lots of tests use quoted code /* eslint-disable no-template-curly-in-string */ - interface ClassificationEntry { value: any; - classification: ts.TokenClass; + classification: TokenClass; position?: number; } - describe("unittests:: services:: Colorization", () => { // Use the shim adapter to ensure test coverage of the shim layer for the classifier - const languageServiceAdapter = new Harness.LanguageService.ShimLanguageServiceAdapter(/*preprocessToResolve*/ false); + const languageServiceAdapter = new LanguageService.ShimLanguageServiceAdapter(/*preprocessToResolve*/ false); const classifier = languageServiceAdapter.getClassifier(); - - function getEntryAtPosition(result: ts.ClassificationResult, position: number) { + function getEntryAtPosition(result: ClassificationResult, position: number) { let entryPosition = 0; for (const entry of result.entries) { if (entryPosition === position) { @@ -22,23 +21,20 @@ describe("unittests:: services:: Colorization", () => { } return undefined; } - - function punctuation(text: string, position?: number) { return createClassification(text, ts.TokenClass.Punctuation, position); } - function keyword(text: string, position?: number) { return createClassification(text, ts.TokenClass.Keyword, position); } - function operator(text: string, position?: number) { return createClassification(text, ts.TokenClass.Operator, position); } - function comment(text: string, position?: number) { return createClassification(text, ts.TokenClass.Comment, position); } - function whitespace(text: string, position?: number) { return createClassification(text, ts.TokenClass.Whitespace, position); } - function identifier(text: string, position?: number) { return createClassification(text, ts.TokenClass.Identifier, position); } - function numberLiteral(text: string, position?: number) { return createClassification(text, ts.TokenClass.NumberLiteral, position); } - function stringLiteral(text: string, position?: number) { return createClassification(text, ts.TokenClass.StringLiteral, position); } + function punctuation(text: string, position?: number) { return createClassification(text, TokenClass.Punctuation, position); } + function keyword(text: string, position?: number) { return createClassification(text, TokenClass.Keyword, position); } + function operator(text: string, position?: number) { return createClassification(text, TokenClass.Operator, position); } + function comment(text: string, position?: number) { return createClassification(text, TokenClass.Comment, position); } + function whitespace(text: string, position?: number) { return createClassification(text, TokenClass.Whitespace, position); } + function identifier(text: string, position?: number) { return createClassification(text, TokenClass.Identifier, position); } + function numberLiteral(text: string, position?: number) { return createClassification(text, TokenClass.NumberLiteral, position); } + function stringLiteral(text: string, position?: number) { return createClassification(text, TokenClass.StringLiteral, position); } function finalEndOfLineState(value: number): ClassificationEntry { return { value, classification: undefined!, position: 0 }; } // TODO: GH#18217 - function createClassification(value: string, classification: ts.TokenClass, position?: number): ClassificationEntry { + function createClassification(value: string, classification: TokenClass, position?: number): ClassificationEntry { return { value, classification, position }; } - - function testLexicalClassification(text: string, initialEndOfLineState: ts.EndOfLineState, ...expectedEntries: ClassificationEntry[]): void { + function testLexicalClassification(text: string, initialEndOfLineState: EndOfLineState, ...expectedEntries: ClassificationEntry[]): void { const result = classifier.getClassificationsForLine(text, initialEndOfLineState, /*syntacticClassifierAbsent*/ false); - for (const expectedEntry of expectedEntries) { if (expectedEntry.classification === undefined) { assert.equal(result.finalLexState, expectedEntry.value, "final endOfLineState does not match expected."); @@ -46,306 +42,121 @@ describe("unittests:: services:: Colorization", () => { else { const actualEntryPosition = expectedEntry.position !== undefined ? expectedEntry.position : text.indexOf(expectedEntry.value); assert(actualEntryPosition >= 0, "token: '" + expectedEntry.value + "' does not exit in text: '" + text + "'."); - const actualEntry = getEntryAtPosition(result, actualEntryPosition)!; - assert(actualEntry, "Could not find classification entry for '" + expectedEntry.value + "' at position: " + actualEntryPosition); - assert.equal(actualEntry.classification, expectedEntry.classification, "Classification class does not match expected. Expected: " + ts.TokenClass[expectedEntry.classification] + ", Actual: " + ts.TokenClass[actualEntry.classification]); - assert.equal(actualEntry.length, expectedEntry.value.length, "Classification length does not match expected. Expected: " + ts.TokenClass[expectedEntry.value.length] + ", Actual: " + ts.TokenClass[actualEntry.length]); + assert.equal(actualEntry.classification, expectedEntry.classification, "Classification class does not match expected. Expected: " + TokenClass[expectedEntry.classification] + ", Actual: " + TokenClass[actualEntry.classification]); + assert.equal(actualEntry.length, expectedEntry.value.length, "Classification length does not match expected. Expected: " + TokenClass[expectedEntry.value.length] + ", Actual: " + TokenClass[actualEntry.length]); } } } - describe("test getClassifications", () => { it("returns correct token classes", () => { - testLexicalClassification("var x: string = \"foo\" ?? \"bar\"; //Hello", - ts.EndOfLineState.None, - keyword("var"), - whitespace(" "), - identifier("x"), - punctuation(":"), - keyword("string"), - operator("="), - stringLiteral("\"foo\""), - whitespace(" "), - operator("??"), - stringLiteral("\"foo\""), - comment("//Hello"), - punctuation(";")); - }); - + testLexicalClassification("var x: string = \"foo\" ?? \"bar\"; //Hello", EndOfLineState.None, keyword("var"), whitespace(" "), identifier("x"), punctuation(":"), keyword("string"), operator("="), stringLiteral("\"foo\""), whitespace(" "), operator("??"), stringLiteral("\"foo\""), comment("//Hello"), punctuation(";")); + }); it("correctly classifies a comment after a divide operator", () => { - testLexicalClassification("1 / 2 // comment", - ts.EndOfLineState.None, - numberLiteral("1"), - whitespace(" "), - operator("/"), - numberLiteral("2"), - comment("// comment")); - }); - + testLexicalClassification("1 / 2 // comment", EndOfLineState.None, numberLiteral("1"), whitespace(" "), operator("/"), numberLiteral("2"), comment("// comment")); + }); it("correctly classifies a literal after a divide operator", () => { - testLexicalClassification("1 / 2, 3 / 4", - ts.EndOfLineState.None, - numberLiteral("1"), - whitespace(" "), - operator("/"), - numberLiteral("2"), - numberLiteral("3"), - numberLiteral("4"), - operator(",")); - }); - + testLexicalClassification("1 / 2, 3 / 4", EndOfLineState.None, numberLiteral("1"), whitespace(" "), operator("/"), numberLiteral("2"), numberLiteral("3"), numberLiteral("4"), operator(",")); + }); it("correctly classifies a multiline string with one backslash", () => { - testLexicalClassification("'line1\\", - ts.EndOfLineState.None, - stringLiteral("'line1\\"), - finalEndOfLineState(ts.EndOfLineState.InSingleQuoteStringLiteral)); + testLexicalClassification("'line1\\", EndOfLineState.None, stringLiteral("'line1\\"), finalEndOfLineState(EndOfLineState.InSingleQuoteStringLiteral)); }); - it("correctly classifies a multiline string with three backslashes", () => { - testLexicalClassification("'line1\\\\\\", - ts.EndOfLineState.None, - stringLiteral("'line1\\\\\\"), - finalEndOfLineState(ts.EndOfLineState.InSingleQuoteStringLiteral)); + testLexicalClassification("'line1\\\\\\", EndOfLineState.None, stringLiteral("'line1\\\\\\"), finalEndOfLineState(EndOfLineState.InSingleQuoteStringLiteral)); }); - it("correctly classifies an unterminated single-line string with no backslashes", () => { - testLexicalClassification("'line1", - ts.EndOfLineState.None, - stringLiteral("'line1"), - finalEndOfLineState(ts.EndOfLineState.None)); + testLexicalClassification("'line1", EndOfLineState.None, stringLiteral("'line1"), finalEndOfLineState(EndOfLineState.None)); }); - it("correctly classifies an unterminated single-line string with two backslashes", () => { - testLexicalClassification("'line1\\\\", - ts.EndOfLineState.None, - stringLiteral("'line1\\\\"), - finalEndOfLineState(ts.EndOfLineState.None)); + testLexicalClassification("'line1\\\\", EndOfLineState.None, stringLiteral("'line1\\\\"), finalEndOfLineState(EndOfLineState.None)); }); - it("correctly classifies an unterminated single-line string with four backslashes", () => { - testLexicalClassification("'line1\\\\\\\\", - ts.EndOfLineState.None, - stringLiteral("'line1\\\\\\\\"), - finalEndOfLineState(ts.EndOfLineState.None)); + testLexicalClassification("'line1\\\\\\\\", EndOfLineState.None, stringLiteral("'line1\\\\\\\\"), finalEndOfLineState(EndOfLineState.None)); }); - it("correctly classifies the continuing line of a multiline string ending in one backslash", () => { - testLexicalClassification("\\", - ts.EndOfLineState.InDoubleQuoteStringLiteral, - stringLiteral("\\"), - finalEndOfLineState(ts.EndOfLineState.InDoubleQuoteStringLiteral)); + testLexicalClassification("\\", EndOfLineState.InDoubleQuoteStringLiteral, stringLiteral("\\"), finalEndOfLineState(EndOfLineState.InDoubleQuoteStringLiteral)); }); - it("correctly classifies the continuing line of a multiline string ending in three backslashes", () => { - testLexicalClassification("\\", - ts.EndOfLineState.InDoubleQuoteStringLiteral, - stringLiteral("\\"), - finalEndOfLineState(ts.EndOfLineState.InDoubleQuoteStringLiteral)); + testLexicalClassification("\\", EndOfLineState.InDoubleQuoteStringLiteral, stringLiteral("\\"), finalEndOfLineState(EndOfLineState.InDoubleQuoteStringLiteral)); }); - it("correctly classifies the last line of an unterminated multiline string ending in no backslashes", () => { - testLexicalClassification(" ", - ts.EndOfLineState.InDoubleQuoteStringLiteral, - stringLiteral(" "), - finalEndOfLineState(ts.EndOfLineState.None)); + testLexicalClassification(" ", EndOfLineState.InDoubleQuoteStringLiteral, stringLiteral(" "), finalEndOfLineState(EndOfLineState.None)); }); - it("correctly classifies the last line of an unterminated multiline string ending in two backslashes", () => { - testLexicalClassification("\\\\", - ts.EndOfLineState.InDoubleQuoteStringLiteral, - stringLiteral("\\\\"), - finalEndOfLineState(ts.EndOfLineState.None)); + testLexicalClassification("\\\\", EndOfLineState.InDoubleQuoteStringLiteral, stringLiteral("\\\\"), finalEndOfLineState(EndOfLineState.None)); }); - it("correctly classifies the last line of an unterminated multiline string ending in four backslashes", () => { - testLexicalClassification("\\\\\\\\", - ts.EndOfLineState.InDoubleQuoteStringLiteral, - stringLiteral("\\\\\\\\"), - finalEndOfLineState(ts.EndOfLineState.None)); + testLexicalClassification("\\\\\\\\", EndOfLineState.InDoubleQuoteStringLiteral, stringLiteral("\\\\\\\\"), finalEndOfLineState(EndOfLineState.None)); }); - it("correctly classifies the last line of a multiline string", () => { - testLexicalClassification("'", - ts.EndOfLineState.InSingleQuoteStringLiteral, - stringLiteral("'"), - finalEndOfLineState(ts.EndOfLineState.None)); + testLexicalClassification("'", EndOfLineState.InSingleQuoteStringLiteral, stringLiteral("'"), finalEndOfLineState(EndOfLineState.None)); }); - it("correctly classifies an unterminated multiline comment", () => { - testLexicalClassification("/*", - ts.EndOfLineState.None, - comment("/*"), - finalEndOfLineState(ts.EndOfLineState.InMultiLineCommentTrivia)); + testLexicalClassification("/*", EndOfLineState.None, comment("/*"), finalEndOfLineState(EndOfLineState.InMultiLineCommentTrivia)); }); - it("correctly classifies the termination of a multiline comment", () => { - testLexicalClassification(" */ ", - ts.EndOfLineState.InMultiLineCommentTrivia, - comment(" */"), - finalEndOfLineState(ts.EndOfLineState.None)); + testLexicalClassification(" */ ", EndOfLineState.InMultiLineCommentTrivia, comment(" */"), finalEndOfLineState(EndOfLineState.None)); }); - it("correctly classifies the continuation of a multiline comment", () => { - testLexicalClassification("LOREM IPSUM DOLOR ", - ts.EndOfLineState.InMultiLineCommentTrivia, - comment("LOREM IPSUM DOLOR "), - finalEndOfLineState(ts.EndOfLineState.InMultiLineCommentTrivia)); + testLexicalClassification("LOREM IPSUM DOLOR ", EndOfLineState.InMultiLineCommentTrivia, comment("LOREM IPSUM DOLOR "), finalEndOfLineState(EndOfLineState.InMultiLineCommentTrivia)); }); - it("correctly classifies an unterminated multiline comment on a line ending in '/*/'", () => { - testLexicalClassification(" /*/", - ts.EndOfLineState.None, - comment("/*/"), - finalEndOfLineState(ts.EndOfLineState.InMultiLineCommentTrivia)); + testLexicalClassification(" /*/", EndOfLineState.None, comment("/*/"), finalEndOfLineState(EndOfLineState.InMultiLineCommentTrivia)); }); - it("correctly classifies an unterminated multiline comment with trailing space", () => { - testLexicalClassification("/* ", - ts.EndOfLineState.None, - comment("/* "), - finalEndOfLineState(ts.EndOfLineState.InMultiLineCommentTrivia)); + testLexicalClassification("/* ", EndOfLineState.None, comment("/* "), finalEndOfLineState(EndOfLineState.InMultiLineCommentTrivia)); }); - it("correctly classifies a keyword after a dot", () => { - testLexicalClassification("a.var", - ts.EndOfLineState.None, - identifier("var")); + testLexicalClassification("a.var", EndOfLineState.None, identifier("var")); }); - it("correctly classifies a string literal after a dot", () => { - testLexicalClassification("a.\"var\"", - ts.EndOfLineState.None, - stringLiteral("\"var\"")); + testLexicalClassification("a.\"var\"", EndOfLineState.None, stringLiteral("\"var\"")); }); - it("correctly classifies a keyword after a dot separated by comment trivia", () => { - testLexicalClassification("a./*hello world*/ var", - ts.EndOfLineState.None, - identifier("a"), - punctuation("."), - comment("/*hello world*/"), - identifier("var")); - }); - + testLexicalClassification("a./*hello world*/ var", EndOfLineState.None, identifier("a"), punctuation("."), comment("/*hello world*/"), identifier("var")); + }); it("classifies a property access with whitespace around the dot", () => { - testLexicalClassification(" x .\tfoo ()", - ts.EndOfLineState.None, - identifier("x"), - identifier("foo")); + testLexicalClassification(" x .\tfoo ()", EndOfLineState.None, identifier("x"), identifier("foo")); }); - it("classifies a keyword after a dot on previous line", () => { - testLexicalClassification("var", - ts.EndOfLineState.None, - keyword("var"), - finalEndOfLineState(ts.EndOfLineState.None)); + testLexicalClassification("var", EndOfLineState.None, keyword("var"), finalEndOfLineState(EndOfLineState.None)); }); - it("classifies multiple keywords properly", () => { - testLexicalClassification("public static", - ts.EndOfLineState.None, - keyword("public"), - keyword("static"), - finalEndOfLineState(ts.EndOfLineState.None)); - - testLexicalClassification("public var", - ts.EndOfLineState.None, - keyword("public"), - identifier("var"), - finalEndOfLineState(ts.EndOfLineState.None)); - }); - + testLexicalClassification("public static", EndOfLineState.None, keyword("public"), keyword("static"), finalEndOfLineState(EndOfLineState.None)); + testLexicalClassification("public var", EndOfLineState.None, keyword("public"), identifier("var"), finalEndOfLineState(EndOfLineState.None)); + }); it("classifies a single line no substitution template string correctly", () => { - testLexicalClassification("`number number public string`", - ts.EndOfLineState.None, - stringLiteral("`number number public string`"), - finalEndOfLineState(ts.EndOfLineState.None)); + testLexicalClassification("`number number public string`", EndOfLineState.None, stringLiteral("`number number public string`"), finalEndOfLineState(EndOfLineState.None)); }); it("classifies substitution parts of a template string correctly", () => { - testLexicalClassification("`number '${ 1 + 1 }' string '${ 'hello' }'`", - ts.EndOfLineState.None, - stringLiteral("`number '${"), - numberLiteral("1"), - operator("+"), - numberLiteral("1"), - stringLiteral("}' string '${"), - stringLiteral("'hello'"), - stringLiteral("}'`"), - finalEndOfLineState(ts.EndOfLineState.None)); + testLexicalClassification("`number '${ 1 + 1 }' string '${ 'hello' }'`", EndOfLineState.None, stringLiteral("`number '${"), numberLiteral("1"), operator("+"), numberLiteral("1"), stringLiteral("}' string '${"), stringLiteral("'hello'"), stringLiteral("}'`"), finalEndOfLineState(EndOfLineState.None)); }); it("classifies an unterminated no substitution template string correctly", () => { - testLexicalClassification("`hello world", - ts.EndOfLineState.None, - stringLiteral("`hello world"), - finalEndOfLineState(ts.EndOfLineState.InTemplateHeadOrNoSubstitutionTemplate)); + testLexicalClassification("`hello world", EndOfLineState.None, stringLiteral("`hello world"), finalEndOfLineState(EndOfLineState.InTemplateHeadOrNoSubstitutionTemplate)); }); it("classifies the entire line of an unterminated multiline no-substitution/head template", () => { - testLexicalClassification("...", - ts.EndOfLineState.InTemplateHeadOrNoSubstitutionTemplate, - stringLiteral("..."), - finalEndOfLineState(ts.EndOfLineState.InTemplateHeadOrNoSubstitutionTemplate)); + testLexicalClassification("...", EndOfLineState.InTemplateHeadOrNoSubstitutionTemplate, stringLiteral("..."), finalEndOfLineState(EndOfLineState.InTemplateHeadOrNoSubstitutionTemplate)); }); it("classifies the entire line of an unterminated multiline template middle/end", () => { - testLexicalClassification("...", - ts.EndOfLineState.InTemplateMiddleOrTail, - stringLiteral("..."), - finalEndOfLineState(ts.EndOfLineState.InTemplateMiddleOrTail)); + testLexicalClassification("...", EndOfLineState.InTemplateMiddleOrTail, stringLiteral("..."), finalEndOfLineState(EndOfLineState.InTemplateMiddleOrTail)); }); it("classifies a termination of a multiline template head", () => { - testLexicalClassification("...${", - ts.EndOfLineState.InTemplateHeadOrNoSubstitutionTemplate, - stringLiteral("...${"), - finalEndOfLineState(ts.EndOfLineState.InTemplateSubstitutionPosition)); + testLexicalClassification("...${", EndOfLineState.InTemplateHeadOrNoSubstitutionTemplate, stringLiteral("...${"), finalEndOfLineState(EndOfLineState.InTemplateSubstitutionPosition)); }); it("classifies the termination of a multiline no substitution template", () => { - testLexicalClassification("...`", - ts.EndOfLineState.InTemplateHeadOrNoSubstitutionTemplate, - stringLiteral("...`"), - finalEndOfLineState(ts.EndOfLineState.None)); + testLexicalClassification("...`", EndOfLineState.InTemplateHeadOrNoSubstitutionTemplate, stringLiteral("...`"), finalEndOfLineState(EndOfLineState.None)); }); it("classifies the substitution parts and middle/tail of a multiline template string", () => { - testLexicalClassification("${ 1 + 1 }...`", - ts.EndOfLineState.InTemplateHeadOrNoSubstitutionTemplate, - stringLiteral("${"), - numberLiteral("1"), - operator("+"), - numberLiteral("1"), - stringLiteral("}...`"), - finalEndOfLineState(ts.EndOfLineState.None)); + testLexicalClassification("${ 1 + 1 }...`", EndOfLineState.InTemplateHeadOrNoSubstitutionTemplate, stringLiteral("${"), numberLiteral("1"), operator("+"), numberLiteral("1"), stringLiteral("}...`"), finalEndOfLineState(EndOfLineState.None)); }); it("classifies a template middle and propagates the end of line state", () => { - testLexicalClassification("${ 1 + 1 }...`", - ts.EndOfLineState.InTemplateHeadOrNoSubstitutionTemplate, - stringLiteral("${"), - numberLiteral("1"), - operator("+"), - numberLiteral("1"), - stringLiteral("}...`"), - finalEndOfLineState(ts.EndOfLineState.None)); + testLexicalClassification("${ 1 + 1 }...`", EndOfLineState.InTemplateHeadOrNoSubstitutionTemplate, stringLiteral("${"), numberLiteral("1"), operator("+"), numberLiteral("1"), stringLiteral("}...`"), finalEndOfLineState(EndOfLineState.None)); }); it("classifies substitution expressions with curly braces appropriately", () => { let pos = 0; let lastLength = 0; - - testLexicalClassification("...${ () => { } } ${ { x: `1` } }...`", - ts.EndOfLineState.InTemplateHeadOrNoSubstitutionTemplate, - stringLiteral(track("...${"), pos), - punctuation(track(" ", "("), pos), - punctuation(track(")"), pos), - punctuation(track(" ", "=>"), pos), - punctuation(track(" ", "{"), pos), - punctuation(track(" ", "}"), pos), - stringLiteral(track(" ", "} ${"), pos), - punctuation(track(" ", "{"), pos), - identifier(track(" ", "x"), pos), - punctuation(track(":"), pos), - stringLiteral(track(" ", "`1`"), pos), - punctuation(track(" ", "}"), pos), - stringLiteral(track(" ", "}...`"), pos), - finalEndOfLineState(ts.EndOfLineState.None)); - + testLexicalClassification("...${ () => { } } ${ { x: `1` } }...`", EndOfLineState.InTemplateHeadOrNoSubstitutionTemplate, stringLiteral(track("...${"), pos), punctuation(track(" ", "("), pos), punctuation(track(")"), pos), punctuation(track(" ", "=>"), pos), punctuation(track(" ", "{"), pos), punctuation(track(" ", "}"), pos), stringLiteral(track(" ", "} ${"), pos), punctuation(track(" ", "{"), pos), identifier(track(" ", "x"), pos), punctuation(track(":"), pos), stringLiteral(track(" ", "`1`"), pos), punctuation(track(" ", "}"), pos), stringLiteral(track(" ", "}...`"), pos), finalEndOfLineState(EndOfLineState.None)); // Adjusts 'pos' by accounting for the length of each portion of the string, // but only return the last given string function track(...vals: string[]): string { @@ -353,80 +164,31 @@ describe("unittests:: services:: Colorization", () => { pos += lastLength; lastLength = val.length; } - return ts.last(vals); + return last(vals); } }); - it("classifies partially written generics correctly.", () => { - testLexicalClassification("Foo { // Test conflict markers. - testLexicalClassification( - "class C {\r\n\ + testLexicalClassification("class C {\r\n\ <<<<<<< HEAD\r\n\ v = 1;\r\n\ =======\r\n\ v = 2;\r\n\ >>>>>>> Branch - a\r\n\ -}", - ts.EndOfLineState.None, - keyword("class"), - identifier("C"), - punctuation("{"), - comment("<<<<<<< HEAD"), - identifier("v"), - operator("="), - numberLiteral("1"), - punctuation(";"), - comment("=======\r\n v = 2;\r\n"), - comment(">>>>>>> Branch - a"), - punctuation("}"), - finalEndOfLineState(ts.EndOfLineState.None)); - - testLexicalClassification( - "<<<<<<< HEAD\r\n\ +}", EndOfLineState.None, keyword("class"), identifier("C"), punctuation("{"), comment("<<<<<<< HEAD"), identifier("v"), operator("="), numberLiteral("1"), punctuation(";"), comment("=======\r\n v = 2;\r\n"), comment(">>>>>>> Branch - a"), punctuation("}"), finalEndOfLineState(EndOfLineState.None)); + testLexicalClassification("<<<<<<< HEAD\r\n\ class C { }\r\n\ =======\r\n\ class D { }\r\n\ ->>>>>>> Branch - a\r\n", - ts.EndOfLineState.None, - comment("<<<<<<< HEAD"), - keyword("class"), - identifier("C"), - punctuation("{"), - punctuation("}"), - comment("=======\r\nclass D { }\r\n"), - comment(">>>>>>> Branch - a"), - finalEndOfLineState(ts.EndOfLineState.None)); - - testLexicalClassification( - "class C {\r\n\ +>>>>>>> Branch - a\r\n", EndOfLineState.None, comment("<<<<<<< HEAD"), keyword("class"), identifier("C"), punctuation("{"), punctuation("}"), comment("=======\r\nclass D { }\r\n"), comment(">>>>>>> Branch - a"), finalEndOfLineState(EndOfLineState.None)); + testLexicalClassification("class C {\r\n\ <<<<<<< HEAD\r\n\ v = 1;\r\n\ ||||||| merged common ancestors\r\n\ @@ -434,55 +196,17 @@ class D { }\r\n\ =======\r\n\ v = 2;\r\n\ >>>>>>> Branch - a\r\n\ -}", - ts.EndOfLineState.None, - keyword("class"), - identifier("C"), - punctuation("{"), - comment("<<<<<<< HEAD"), - identifier("v"), - operator("="), - numberLiteral("1"), - punctuation(";"), - comment("||||||| merged common ancestors\r\n v = 3;\r\n"), - comment("=======\r\n v = 2;\r\n"), - comment(">>>>>>> Branch - a"), - punctuation("}"), - finalEndOfLineState(ts.EndOfLineState.None)); - - testLexicalClassification( - "<<<<<<< HEAD\r\n\ +}", EndOfLineState.None, keyword("class"), identifier("C"), punctuation("{"), comment("<<<<<<< HEAD"), identifier("v"), operator("="), numberLiteral("1"), punctuation(";"), comment("||||||| merged common ancestors\r\n v = 3;\r\n"), comment("=======\r\n v = 2;\r\n"), comment(">>>>>>> Branch - a"), punctuation("}"), finalEndOfLineState(EndOfLineState.None)); + testLexicalClassification("<<<<<<< HEAD\r\n\ class C { }\r\n\ ||||||| merged common ancestors\r\n\ class E { }\r\n\ =======\r\n\ class D { }\r\n\ ->>>>>>> Branch - a\r\n", - ts.EndOfLineState.None, - comment("<<<<<<< HEAD"), - keyword("class"), - identifier("C"), - punctuation("{"), - punctuation("}"), - comment("||||||| merged common ancestors\r\nclass E { }\r\n"), - comment("=======\r\nclass D { }\r\n"), - comment(">>>>>>> Branch - a"), - finalEndOfLineState(ts.EndOfLineState.None)); - }); - +>>>>>>> Branch - a\r\n", EndOfLineState.None, comment("<<<<<<< HEAD"), keyword("class"), identifier("C"), punctuation("{"), punctuation("}"), comment("||||||| merged common ancestors\r\nclass E { }\r\n"), comment("=======\r\nclass D { }\r\n"), comment(">>>>>>> Branch - a"), finalEndOfLineState(EndOfLineState.None)); + }); it("'of' keyword", () => { - testLexicalClassification("for (var of of of) { }", - ts.EndOfLineState.None, - keyword("for"), - punctuation("("), - keyword("var"), - keyword("of"), - keyword("of"), - keyword("of"), - punctuation(")"), - punctuation("{"), - punctuation("}"), - finalEndOfLineState(ts.EndOfLineState.None)); + testLexicalClassification("for (var of of of) { }", EndOfLineState.None, keyword("for"), punctuation("("), keyword("var"), keyword("of"), keyword("of"), keyword("of"), punctuation(")"), punctuation("{"), punctuation("}"), finalEndOfLineState(EndOfLineState.None)); }); }); }); diff --git a/src/testRunner/unittests/services/convertToAsyncFunction.ts b/src/testRunner/unittests/services/convertToAsyncFunction.ts index 10d44c2453b63..f31f7768d1309 100644 --- a/src/testRunner/unittests/services/convertToAsyncFunction.ts +++ b/src/testRunner/unittests/services/convertToAsyncFunction.ts @@ -1,7 +1,9 @@ -namespace ts { - const libFile: TestFSWithWatch.File = { - path: "/a/lib/lib.d.ts", - content: `/// +import { TestFSWithWatch, extractTest, Extension, CodeFixContext, noop, returnFalse, emptyOptions, notImplementedHost, formatting, testFormatSettings, find, Diagnostics, codefix, textChanges, newLineCharacter, Program, length } from "../../ts"; +import { Baseline } from "../../Harness"; +import { createServerHost, createProjectService } from "../../ts.projectSystem"; +const libFile: TestFSWithWatch.File = { + path: "/a/lib/lib.d.ts", + content: `/// interface Boolean {} interface Function {} interface IArguments {} @@ -253,124 +255,109 @@ declare var Promise: PromiseConstructor; interface RegExp {} interface String { charAt: any; } interface Array {}` - }; - - function testConvertToAsyncFunction(caption: string, text: string, baselineFolder: string, includeLib?: boolean, expectFailure = false, onlyProvideAction = false) { - const t = extractTest(text); - const selectionRange = t.ranges.get("selection")!; - if (!selectionRange) { - throw new Error(`Test ${caption} does not specify selection range`); +}; +function testConvertToAsyncFunction(caption: string, text: string, baselineFolder: string, includeLib?: boolean, expectFailure = false, onlyProvideAction = false) { + const t = extractTest(text); + const selectionRange = t.ranges.get("selection")!; + if (!selectionRange) { + throw new Error(`Test ${caption} does not specify selection range`); + } + const extensions = expectFailure ? [Extension.Ts] : [Extension.Ts, Extension.Js]; + extensions.forEach(extension => it(`${caption} [${extension}]`, () => runBaseline(extension))); + function runBaseline(extension: Extension) { + const path = "/a" + extension; + const languageService = makeLanguageService({ path, content: t.source }, includeLib); + const program = languageService.getProgram()!; + if (hasSyntacticDiagnostics(program)) { + // Don't bother generating JS baselines for inputs that aren't valid JS. + assert.equal(Extension.Js, extension, "Syntactic diagnostics found in non-JS file"); + return; } - - const extensions = expectFailure ? [Extension.Ts] : [Extension.Ts, Extension.Js]; - - extensions.forEach(extension => - it(`${caption} [${extension}]`, () => runBaseline(extension))); - - function runBaseline(extension: Extension) { - const path = "/a" + extension; - const languageService = makeLanguageService({ path, content: t.source }, includeLib); - const program = languageService.getProgram()!; - - if (hasSyntacticDiagnostics(program)) { - // Don't bother generating JS baselines for inputs that aren't valid JS. - assert.equal(Extension.Js, extension, "Syntactic diagnostics found in non-JS file"); - return; - } - - const f = { - path, - content: t.source - }; - - const sourceFile = program.getSourceFile(path)!; - const context: CodeFixContext = { - errorCode: 80006, - span: { start: selectionRange.pos, length: selectionRange.end - selectionRange.pos }, - sourceFile, - program, - cancellationToken: { throwIfCancellationRequested: noop, isCancellationRequested: returnFalse }, - preferences: emptyOptions, - host: notImplementedHost, - formatContext: formatting.getFormatContext(testFormatSettings) - }; - - const diagnostics = languageService.getSuggestionDiagnostics(f.path); - const diagnostic = find(diagnostics, diagnostic => diagnostic.messageText === Diagnostics.This_may_be_converted_to_an_async_function.message && - diagnostic.start === context.span.start && diagnostic.length === context.span.length); - if (expectFailure) { - assert.isUndefined(diagnostic); - } - else { - assert.exists(diagnostic); - } - - const actions = codefix.getFixes(context); - const action = find(actions, action => action.description === Diagnostics.Convert_to_async_function.message); - if (expectFailure && !onlyProvideAction) { - assert.isNotTrue(action && action.changes.length > 0); - return; - } - - assert.isTrue(action && action.changes.length > 0); - - const data: string[] = []; - data.push(`// ==ORIGINAL==`); - data.push(text.replace("[#|", "/*[#|*/").replace("|]", "/*|]*/")); - const changes = action!.changes; - assert.lengthOf(changes, 1); - - data.push(`// ==ASYNC FUNCTION::${action!.description}==`); - const newText = textChanges.applyChanges(sourceFile.text, changes[0].textChanges); - data.push(newText); - - const diagProgram = makeLanguageService({ path, content: newText }, includeLib).getProgram()!; - assert.isFalse(hasSyntacticDiagnostics(diagProgram)); - Harness.Baseline.runBaseline(`${baselineFolder}/${caption}${extension}`, data.join(newLineCharacter)); + const f = { + path, + content: t.source + }; + const sourceFile = program.getSourceFile(path)!; + const context: CodeFixContext = { + errorCode: 80006, + span: { start: selectionRange.pos, length: selectionRange.end - selectionRange.pos }, + sourceFile, + program, + cancellationToken: { throwIfCancellationRequested: noop, isCancellationRequested: returnFalse }, + preferences: emptyOptions, + host: notImplementedHost, + formatContext: formatting.getFormatContext(testFormatSettings) + }; + const diagnostics = languageService.getSuggestionDiagnostics(f.path); + const diagnostic = find(diagnostics, diagnostic => diagnostic.messageText === Diagnostics.This_may_be_converted_to_an_async_function.message && + diagnostic.start === context.span.start && diagnostic.length === context.span.length); + if (expectFailure) { + assert.isUndefined(diagnostic); } - - function makeLanguageService(f: { path: string, content: string }, includeLib?: boolean) { - - const host = projectSystem.createServerHost(includeLib ? [f, libFile] : [f]); // libFile is expensive to parse repeatedly - only test when required - const projectService = projectSystem.createProjectService(host); - projectService.openClientFile(f.path); - return projectService.inferredProjects[0].getLanguageService(); + else { + assert.exists(diagnostic); } - - function hasSyntacticDiagnostics(program: Program) { - const diags = program.getSyntacticDiagnostics(); - return length(diags) > 0; + const actions = codefix.getFixes(context); + const action = find(actions, action => action.description === Diagnostics.Convert_to_async_function.message); + if (expectFailure && !onlyProvideAction) { + assert.isNotTrue(action && action.changes.length > 0); + return; } + assert.isTrue(action && action.changes.length > 0); + const data: string[] = []; + data.push(`// ==ORIGINAL==`); + data.push(text.replace("[#|", "/*[#|*/").replace("|]", "/*|]*/")); + const changes = action!.changes; + assert.lengthOf(changes, 1); + data.push(`// ==ASYNC FUNCTION::${action!.description}==`); + const newText = textChanges.applyChanges(sourceFile.text, changes[0].textChanges); + data.push(newText); + const diagProgram = makeLanguageService({ path, content: newText }, includeLib).getProgram()!; + assert.isFalse(hasSyntacticDiagnostics(diagProgram)); + Baseline.runBaseline(`${baselineFolder}/${caption}${extension}`, data.join(newLineCharacter)); } - - describe("unittests:: services:: convertToAsyncFunctions", () => { - _testConvertToAsyncFunction("convertToAsyncFunction_basic", ` + function makeLanguageService(f: { + path: string; + content: string; + }, includeLib?: boolean) { + const host = createServerHost(includeLib ? [f, libFile] : [f]); // libFile is expensive to parse repeatedly - only test when required + const projectService = createProjectService(host); + projectService.openClientFile(f.path); + return projectService.inferredProjects[0].getLanguageService(); + } + function hasSyntacticDiagnostics(program: Program) { + const diags = program.getSyntacticDiagnostics(); + return length(diags) > 0; + } +} +describe("unittests:: services:: convertToAsyncFunctions", () => { + _testConvertToAsyncFunction("convertToAsyncFunction_basic", ` function [#|f|](): Promise{ return fetch('https://typescriptlang.org').then(result => { console.log(result) }); }`); - _testConvertToAsyncFunction("convertToAsyncFunction_arrayBindingPattern", ` + _testConvertToAsyncFunction("convertToAsyncFunction_arrayBindingPattern", ` function [#|f|](): Promise{ return fetch('https://typescriptlang.org').then(([result]) => { console.log(result) }); }`); - _testConvertToAsyncFunction("convertToAsyncFunction_objectBindingPattern", ` + _testConvertToAsyncFunction("convertToAsyncFunction_objectBindingPattern", ` function [#|f|](): Promise{ return fetch('https://typescriptlang.org').then(({ result }) => { console.log(result) }); }`); - _testConvertToAsyncFunction("convertToAsyncFunction_arrayBindingPatternRename", ` + _testConvertToAsyncFunction("convertToAsyncFunction_arrayBindingPatternRename", ` function [#|f|](): Promise{ const result = getResult(); return fetch('https://typescriptlang.org').then(([result]) => { console.log(result) }); }`); - _testConvertToAsyncFunction("convertToAsyncFunction_objectBindingPatternRename", ` + _testConvertToAsyncFunction("convertToAsyncFunction_objectBindingPatternRename", ` function [#|f|](): Promise{ const result = getResult(); return fetch('https://typescriptlang.org').then(({ result }) => { console.log(result) }); }`); - _testConvertToAsyncFunction("convertToAsyncFunction_basicNoReturnTypeAnnotation", ` + _testConvertToAsyncFunction("convertToAsyncFunction_basicNoReturnTypeAnnotation", ` function [#|f|]() { return fetch('https://typescriptlang.org').then(result => { console.log(result) }); }`); - _testConvertToAsyncFunction("convertToAsyncFunction_basicWithComments", ` + _testConvertToAsyncFunction("convertToAsyncFunction_basicWithComments", ` function [#|f|](): Promise{ /* Note - some of these comments are removed during the refactor. This is not ideal. */ @@ -378,24 +365,23 @@ function [#|f|](): Promise{ /*b*/ return /*c*/ fetch( /*d*/ 'https://typescriptlang.org' /*e*/).then( /*f*/ result /*g*/ => { /*h*/ console.log(/*i*/ result /*j*/) /*k*/}/*l*/); // m }`); - - _testConvertToAsyncFunction("convertToAsyncFunction_ArrowFunction", ` + _testConvertToAsyncFunction("convertToAsyncFunction_ArrowFunction", ` [#|():Promise => {|] return fetch('https://typescriptlang.org').then(result => console.log(result)); }`); - _testConvertToAsyncFunction("convertToAsyncFunction_ArrowFunctionNoAnnotation", ` + _testConvertToAsyncFunction("convertToAsyncFunction_ArrowFunctionNoAnnotation", ` [#|() => {|] return fetch('https://typescriptlang.org').then(result => console.log(result)); }`); - _testConvertToAsyncFunction("convertToAsyncFunction_Catch", ` + _testConvertToAsyncFunction("convertToAsyncFunction_Catch", ` function [#|f|]():Promise { return fetch('https://typescriptlang.org').then(result => { console.log(result); }).catch(err => { console.log(err); }); }`); - _testConvertToAsyncFunction("convertToAsyncFunction_CatchAndRej", ` + _testConvertToAsyncFunction("convertToAsyncFunction_CatchAndRej", ` function [#|f|]():Promise { return fetch('https://typescriptlang.org').then(result => { console.log(result); }, rejection => { console.log("rejected:", rejection); }).catch(err => { console.log(err) }); }`); - _testConvertToAsyncFunction("convertToAsyncFunction_CatchAndRejRef", ` + _testConvertToAsyncFunction("convertToAsyncFunction_CatchAndRejRef", ` function [#|f|]():Promise { return fetch('https://typescriptlang.org').then(res, rej).catch(catch_err) } @@ -408,7 +394,7 @@ function rej(rejection){ function catch_err(err){ console.log(err); }`); - _testConvertToAsyncFunction("convertToAsyncFunction_CatchRef", ` + _testConvertToAsyncFunction("convertToAsyncFunction_CatchRef", ` function [#|f|]():Promise { return fetch('https://typescriptlang.org').then(res).catch(catch_err) } @@ -419,48 +405,40 @@ function catch_err(err){ console.log(err); } `); - _testConvertToAsyncFunction("convertToAsyncFunction_CatchNoBrackets", ` + _testConvertToAsyncFunction("convertToAsyncFunction_CatchNoBrackets", ` function [#|f|]():Promise { return fetch('https://typescriptlang.org').then(result => console.log(result)).catch(err => console.log(err)); -}` - ); - _testConvertToAsyncFunction("convertToAsyncFunction_IgnoreArgs1", ` +}`); + _testConvertToAsyncFunction("convertToAsyncFunction_IgnoreArgs1", ` function [#|f|](): Promise { return fetch('https://typescriptlang.org').then( _ => { console.log("done"); }); -}` - ); - _testConvertToAsyncFunction("convertToAsyncFunction_IgnoreArgs2", ` +}`); + _testConvertToAsyncFunction("convertToAsyncFunction_IgnoreArgs2", ` function [#|f|](): Promise { return fetch('https://typescriptlang.org').then( () => console.log("done") ); -}` - ); - _testConvertToAsyncFunction("convertToAsyncFunction_IgnoreArgs3", ` +}`); + _testConvertToAsyncFunction("convertToAsyncFunction_IgnoreArgs3", ` function [#|f|](): Promise { return fetch('https://typescriptlang.org').then( () => console.log("almost done") ).then( () => console.log("done") ); -}` - ); - _testConvertToAsyncFunction("convertToAsyncFunction_IgnoreArgs4", ` +}`); + _testConvertToAsyncFunction("convertToAsyncFunction_IgnoreArgs4", ` function [#|f|]() { return fetch('https://typescriptlang.org').then(res); } function res(){ console.log("done"); -}` - ); - - _testConvertToAsyncFunction("convertToAsyncFunction_Method", ` +}`); + _testConvertToAsyncFunction("convertToAsyncFunction_Method", ` class Parser { [#|f|]():Promise { return fetch('https://typescriptlang.org').then(result => console.log(result)); } -}` - ); - _testConvertToAsyncFunction("convertToAsyncFunction_MultipleCatches", ` +}`); + _testConvertToAsyncFunction("convertToAsyncFunction_MultipleCatches", ` function [#|f|](): Promise { return fetch('https://typescriptlang.org').then(res => console.log(res)).catch(err => console.log("err", err)).catch(err2 => console.log("err2", err2)); -}` - ); - _testConvertToAsyncFunction("convertToAsyncFunction_MultipleThens", ` +}`); + _testConvertToAsyncFunction("convertToAsyncFunction_MultipleThens", ` function [#|f|]():Promise { return fetch('https://typescriptlang.org').then(res).then(res2); } @@ -469,9 +447,8 @@ function res(result){ } function res2(result2){ console.log(result2); -}` - ); - _testConvertToAsyncFunction("convertToAsyncFunction_MultipleThensSameVarName", ` +}`); + _testConvertToAsyncFunction("convertToAsyncFunction_MultipleThensSameVarName", ` function [#|f|]():Promise { return fetch('https://typescriptlang.org').then(res).then(res2); } @@ -481,64 +458,54 @@ function res(result){ function res2(result){ return result.bodyUsed; } -` - ); - _testConvertToAsyncFunction("convertToAsyncFunction_NoRes", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_NoRes", ` function [#|f|]():Promise { return fetch('https://typescriptlang.org').then(null, rejection => console.log("rejected:", rejection)); } -` - ); - _testConvertToAsyncFunction("convertToAsyncFunction_NoRes2", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_NoRes2", ` function [#|f|]():Promise { return fetch('https://typescriptlang.org').then(undefined).catch(rej => console.log(rej)); } -` - ); - _testConvertToAsyncFunction("convertToAsyncFunction_NoRes3", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_NoRes3", ` function [#|f|]():Promise { return fetch('https://typescriptlang.org').catch(rej => console.log(rej)); } -` - ); - _testConvertToAsyncFunction("convertToAsyncFunction_NoRes4", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_NoRes4", ` function [#|f|]() { return fetch('https://typescriptlang.org').then(undefined, rejection => console.log("rejected:", rejection)); } -` - ); - _testConvertToAsyncFunction("convertToAsyncFunction_NoCatchHandler", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_NoCatchHandler", ` function [#|f|]() { return fetch('https://typescriptlang.org').then(x => x.statusText).catch(undefined); } -` - ); - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_NoSuggestion", ` +`); + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_NoSuggestion", ` function [#|f|]():Promise { return fetch('https://typescriptlang.org'); } -` - ); - _testConvertToAsyncFunction("convertToAsyncFunction_PromiseDotAll", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_PromiseDotAll", ` function [#|f|]():Promise{ return Promise.all([fetch('https://typescriptlang.org'), fetch('https://microsoft.com'), fetch('https://youtube.com')]).then(function(vals){ vals.forEach(console.log); }); } -` - ); - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_NoSuggestionNoPromise", ` +`); + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_NoSuggestionNoPromise", ` function [#|f|]():void{ } -` - ); - _testConvertToAsyncFunction("convertToAsyncFunction_Rej", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_Rej", ` function [#|f|]():Promise { return fetch('https://typescriptlang.org').then(result => { console.log(result); }, rejection => { console.log("rejected:", rejection); }); } -` - ); - _testConvertToAsyncFunction("convertToAsyncFunction_RejRef", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_RejRef", ` function [#|f|]():Promise { return fetch('https://typescriptlang.org').then(res, rej); } @@ -548,59 +515,49 @@ function res(result){ function rej(err){ console.log(err); } -` - ); - _testConvertToAsyncFunction("convertToAsyncFunction_RejNoBrackets", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_RejNoBrackets", ` function [#|f|]():Promise { return fetch('https://typescriptlang.org').then(result => console.log(result), rejection => console.log("rejected:", rejection)); } -` - ); - _testConvertToAsyncFunction("convertToAsyncFunction_ResRef", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_ResRef", ` function [#|f|]():Promise { return fetch('https://typescriptlang.org').then(res); } function res(result){ return result.ok; } -` - ); - _testConvertToAsyncFunction("convertToAsyncFunction_ResRefNoReturnVal", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_ResRefNoReturnVal", ` function [#|f|]():Promise { return fetch('https://typescriptlang.org').then(res); } function res(result){ console.log(result); } -` - ); - _testConvertToAsyncFunction("convertToAsyncFunction_NoBrackets", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_NoBrackets", ` function [#|f|]():Promise { return fetch('https://typescriptlang.org').then(result => console.log(result)); } -` - ); - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_Finally1", ` +`); + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_Finally1", ` function [#|finallyTest|](): Promise { return fetch("https://typescriptlang.org").then(res => console.log(res)).catch(rej => console.log("error", rej)).finally(console.log("finally!")); } -` - ); - - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_Finally2", ` +`); + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_Finally2", ` function [#|finallyTest|](): Promise { return fetch("https://typescriptlang.org").then(res => console.log(res)).finally(console.log("finally!")); } -` - ); - - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_Finally3", ` +`); + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_Finally3", ` function [#|finallyTest|](): Promise { return fetch("https://typescriptlang.org").finally(console.log("finally!")); } -` - ); - _testConvertToAsyncFunction("convertToAsyncFunction_InnerPromise", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_InnerPromise", ` function [#|innerPromise|](): Promise { return fetch("https://typescriptlang.org").then(resp => { var blob2 = resp.blob().then(blob => blob.byteOffset).catch(err => 'Error'); @@ -609,9 +566,8 @@ function [#|innerPromise|](): Promise { return blob.toString(); }); } -` - ); - _testConvertToAsyncFunction("convertToAsyncFunction_InnerPromiseRet", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_InnerPromiseRet", ` function [#|innerPromise|](): Promise { return fetch("https://typescriptlang.org").then(resp => { return resp.blob().then(blob => blob.byteOffset).catch(err => 'Error'); @@ -619,10 +575,8 @@ function [#|innerPromise|](): Promise { return blob.toString(); }); } -` - ); - - _testConvertToAsyncFunction("convertToAsyncFunction_InnerPromiseRetBinding1", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_InnerPromiseRetBinding1", ` function [#|innerPromise|](): Promise { return fetch("https://typescriptlang.org").then(resp => { return resp.blob().then(({ blob }) => blob.byteOffset).catch(({ message }) => 'Error ' + message); @@ -630,10 +584,8 @@ function [#|innerPromise|](): Promise { return blob.toString(); }); } -` - ); - - _testConvertToAsyncFunction("convertToAsyncFunction_InnerPromiseRetBinding2", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_InnerPromiseRetBinding2", ` function [#|innerPromise|](): Promise { return fetch("https://typescriptlang.org").then(resp => { return resp.blob().then(blob => blob.byteOffset).catch(err => 'Error'); @@ -641,10 +593,8 @@ function [#|innerPromise|](): Promise { return x.toString(); }); } -` - ); - - _testConvertToAsyncFunction("convertToAsyncFunction_InnerPromiseRetBinding3", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_InnerPromiseRetBinding3", ` function [#|innerPromise|](): Promise { return fetch("https://typescriptlang.org").then(resp => { return resp.blob().then(({ blob }) => blob.byteOffset).catch(({ message }) => 'Error ' + message); @@ -652,10 +602,8 @@ function [#|innerPromise|](): Promise { return (x || y).toString(); }); } -` - ); - - _testConvertToAsyncFunction("convertToAsyncFunction_InnerPromiseRetBinding4", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_InnerPromiseRetBinding4", ` function [#|innerPromise|](): Promise { return fetch("https://typescriptlang.org").then(resp => { return resp.blob().then(({ blob }: { blob: { byteOffset: number } }) => [0, blob.byteOffset]).catch(({ message }: Error) => ['Error ', message]); @@ -663,25 +611,21 @@ function [#|innerPromise|](): Promise { return (x || y).toString(); }); } -` - ); - - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn01", ` +`); + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn01", ` function [#|f|]() { let blob = fetch("https://typescriptlang.org").then(resp => console.log(resp)); return blob; } -` - ); - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn02", ` +`); + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn02", ` function [#|f|]() { let blob = fetch("https://typescriptlang.org"); blob.then(resp => console.log(resp)); return blob; } -` - ); - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn03", ` +`); + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn03", ` function [#|f|]() { let blob = fetch("https://typescriptlang.org") let blob2 = blob.then(resp => console.log(resp)); @@ -692,9 +636,8 @@ function [#|f|]() { function err (rej) { console.log(rej) } -` - ); - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn04", ` +`); + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn04", ` function [#|f|]() { var blob = fetch("https://typescriptlang.org").then(res => console.log(res)), blob2 = fetch("https://microsoft.com").then(res => res.ok).catch(err); return blob; @@ -702,27 +645,21 @@ function [#|f|]() { function err (rej) { console.log(rej) } -` - ); - - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn05", ` +`); + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn05", ` function [#|f|]() { var blob = fetch("https://typescriptlang.org").then(res => console.log(res)); blob.then(x => x); return blob; } -` - ); - - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn06", ` +`); + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn06", ` function [#|f|]() { var blob = fetch("https://typescriptlang.org"); return blob; } -` - ); - - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn07", ` +`); + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn07", ` function [#|f|]() { let blob = fetch("https://typescriptlang.org"); let blob2 = fetch("https://microsoft.com"); @@ -730,10 +667,8 @@ function [#|f|]() { blob.then(resp => console.log(resp)); return blob; } -` - ); - - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn08", ` +`); + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn08", ` function [#|f|]() { let blob = fetch("https://typescriptlang.org"); if (!blob.ok){ @@ -742,10 +677,8 @@ function [#|f|]() { blob.then(resp => console.log(resp)); return blob; } -` - ); - - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn09", ` +`); + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn09", ` function [#|f|]() { let blob3; let blob = fetch("https://typescriptlang.org"); @@ -755,11 +688,8 @@ function [#|f|]() { blob3 = blob2.catch(rej => rej.ok); return blob; } -` - ); - - - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn10", ` +`); + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn10", ` function [#|f|]() { let blob3; let blob = fetch("https://typescriptlang.org"); @@ -770,20 +700,14 @@ function [#|f|]() { blob3 = blob2; return blob; } -` - ); - - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn11", ` +`); + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_VarReturn11", ` function [#|f|]() { let blob; return blob; } -` - ); - - - - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_Param1", ` +`); + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_Param1", ` function [#|f|]() { return my_print(fetch("https://typescriptlang.org").then(res => console.log(res))); } @@ -794,10 +718,8 @@ function my_print (resp) { return resp; } -` - ); - - _testConvertToAsyncFunction("convertToAsyncFunction_Param2", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_Param2", ` function [#|f|]() { return my_print(fetch("https://typescriptlang.org").then(res => console.log(res))).catch(err => console.log("Error!", err)); } @@ -809,10 +731,8 @@ function my_print (resp): Promise { } -` - ); - - _testConvertToAsyncFunction("convertToAsyncFunction_MultipleReturns1", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_MultipleReturns1", ` function [#|f|](): Promise { let x = fetch("https://microsoft.com").then(res => console.log("Microsoft:", res)); if (x.ok) { @@ -822,10 +742,8 @@ function [#|f|](): Promise { var blob = resp.blob().then(blob => blob.byteOffset).catch(err => 'Error'); }); } -` - ); - - _testConvertToAsyncFunction("convertToAsyncFunction_MultipleReturns2", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_MultipleReturns2", ` function [#|f|](): Promise { let x = fetch("https://microsoft.com").then(res => console.log("Microsoft:", res)); if (x.ok) { @@ -836,11 +754,8 @@ function [#|f|](): Promise { return fetch("https://microsoft.com").then(res => console.log("Another one!")); }); } -` - ); - - - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_SeperateLines", ` +`); + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_SeperateLines", ` function [#|f|](): Promise { var blob = fetch("https://typescriptlang.org") blob.then(resp => { @@ -852,11 +767,8 @@ function [#|f|](): Promise { return blob; } -` - ); - - - _testConvertToAsyncFunction("convertToAsyncFunction_InnerVarNameConflict", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_InnerVarNameConflict", ` function [#|f|](): Promise { return fetch("https://typescriptlang.org").then(resp => { var blob = resp.blob().then(blob => blob.byteOffset).catch(err => 'Error'); @@ -864,9 +776,8 @@ function [#|f|](): Promise { return blob.toString(); }); } -` - ); - _testConvertToAsyncFunction("convertToAsyncFunction_InnerPromiseSimple", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_InnerPromiseSimple", ` function [#|f|](): Promise { return fetch("https://typescriptlang.org").then(resp => { return resp.blob().then(blob => blob.byteOffset); @@ -874,9 +785,8 @@ function [#|f|](): Promise { return blob.toString(); }); } -` - ); - _testConvertToAsyncFunction("convertToAsyncFunction_PromiseAllAndThen", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_PromiseAllAndThen", ` function [#|f|]() { return Promise.resolve().then(function () { return Promise.all([fetch("https://typescriptlang.org"), fetch("https://microsoft.com"), Promise.resolve().then(function () { @@ -884,10 +794,8 @@ function [#|f|]() { }).then(res => res.toString())]); }); } -` - ); - - _testConvertToAsyncFunction("convertToAsyncFunction_PromiseAllAndThen2", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_PromiseAllAndThen2", ` function [#|f|]() { return Promise.resolve().then(function () { return Promise.all([fetch("https://typescriptlang.org"), fetch("https://microsoft.com"), Promise.resolve().then(function () { @@ -895,9 +803,8 @@ function [#|f|]() { })]).then(res => res.toString()); }); } -` - ); - _testConvertToAsyncFunction("convertToAsyncFunction_Scope1", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_Scope1", ` function [#|f|]() { var var1: Response, var2; return fetch('https://typescriptlang.org').then( _ => @@ -913,8 +820,7 @@ function [#|f|]() { console.log(response); } `); - - _testConvertToAsyncFunction("convertToAsyncFunction_Conditionals", ` + _testConvertToAsyncFunction("convertToAsyncFunction_Conditionals", ` function [#|f|](){ return fetch("https://typescriptlang.org").then(res => { if (res.ok) { @@ -930,10 +836,8 @@ function [#|f|](){ } }); } -` - ); - - _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThen", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThen", ` function [#|f|](){ return fetch("https://typescriptlang.org").then(res).catch(rej).then(res); } @@ -945,10 +849,8 @@ function res(result){ function rej(reject){ return reject; } -` - ); - - _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMatchingTypes01", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMatchingTypes01", ` function [#|f|](){ return fetch("https://typescriptlang.org").then(res).catch(rej).then(res); } @@ -960,10 +862,8 @@ function res(result): number { function rej(reject): number { return 3; } -` - ); - - _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMatchingTypes01NoAnnotations", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMatchingTypes01NoAnnotations", ` function [#|f|](){ return fetch("https://typescriptlang.org").then(res).catch(rej).then(res); } @@ -975,11 +875,8 @@ function res(result){ function rej(reject){ return 3; } -` - ); - - - _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMatchingTypes02", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMatchingTypes02", ` function [#|f|](){ return fetch("https://typescriptlang.org").then(res => 0).catch(rej => 1).then(res); } @@ -987,10 +884,8 @@ function [#|f|](){ function res(result): number { return 5; } -` - ); - - _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMatchingTypes02NoAnnotations", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMatchingTypes02NoAnnotations", ` function [#|f|](){ return fetch("https://typescriptlang.org").then(res => 0).catch(rej => 1).then(res); } @@ -998,10 +893,8 @@ function [#|f|](){ function res(result){ return 5; } -` - ); - - _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMismatchTypes01", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMismatchTypes01", ` function [#|f|](){ return fetch("https://typescriptlang.org").then(res).catch(rej).then(res); } @@ -1013,10 +906,8 @@ function res(result){ function rej(reject){ return "Error"; } -` - ); - - _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMismatchTypes02", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMismatchTypes02", ` function [#|f|](){ return fetch("https://typescriptlang.org").then(res).catch(rej).then(res); } @@ -1028,10 +919,8 @@ function res(result){ function rej(reject): Response{ return reject; } -` - ); - - _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMismatchTypes02NoAnnotations", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMismatchTypes02NoAnnotations", ` function [#|f|](){ return fetch("https://typescriptlang.org").then(res).catch(rej).then(res); } @@ -1043,11 +932,8 @@ function res(result){ function rej(reject){ return reject; } -` - ); - - - _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMismatchTypes03", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMismatchTypes03", ` function [#|f|](){ return fetch("https://typescriptlang.org").then(res).catch(rej).then(res); } @@ -1059,10 +945,8 @@ function res(result){ function rej(reject){ return Promise.resolve(1); } -` - ); - - _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMismatchTypes04", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_CatchFollowedByThenMismatchTypes04", ` interface a { name: string; age: number; @@ -1084,26 +968,21 @@ function res(result): b{ function rej(reject): a{ return {name: "myName", age: 27}; } -` - ); - - - - - _testConvertToAsyncFunction("convertToAsyncFunction_LocalReturn", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_LocalReturn", ` function [#|f|]() { let x = fetch("https://typescriptlang.org").then(res => console.log(res)); return x.catch(err => console.log("Error!", err)); } `); - _testConvertToAsyncFunction("convertToAsyncFunction_PromiseCallInner", ` + _testConvertToAsyncFunction("convertToAsyncFunction_PromiseCallInner", ` function [#|f|]() { return fetch(Promise.resolve(1).then(res => "https://typescriptlang.org")).catch(err => console.log(err)); } `); - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_CatchFollowedByCall", ` + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_CatchFollowedByCall", ` function [#|f|](){ return fetch("https://typescriptlang.org").then(res).catch(rej).toString(); } @@ -1115,27 +994,21 @@ function res(result){ function rej(reject){ return reject; } -` - ); - - _testConvertToAsyncFunction("convertToAsyncFunction_Scope2", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_Scope2", ` function [#|f|](){ var i:number; return fetch("https://typescriptlang.org").then(i => i.ok).then(res => i+1).catch(err => i-1) } -` - ); - - _testConvertToAsyncFunction("convertToAsyncFunction_Loop", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_Loop", ` function [#|f|](){ return fetch("https://typescriptlang.org").then(res => { for(let i=0; i<10; i++){ console.log(res); }}) } -` - ); - - _testConvertToAsyncFunction("convertToAsyncFunction_Conditional2", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_Conditional2", ` function [#|f|](){ var res = 100; if (res > 50) { @@ -1149,10 +1022,8 @@ function [#|f|](){ function res_func(result){ console.log(result); } -` - ); - - _testConvertToAsyncFunction("convertToAsyncFunction_Scope3", ` +`); + _testConvertToAsyncFunction("convertToAsyncFunction_Scope3", ` function [#|f|]() { var obj; return fetch("https://typescriptlang.org").then(function (res) { @@ -1163,10 +1034,8 @@ function [#|f|]() { }; }); } -` - ); - - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_NestedFunctionWrongLocation", ` +`); + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_NestedFunctionWrongLocation", ` function [#|f|]() { function fn2(){ function fn3(){ @@ -1177,8 +1046,7 @@ function [#|f|]() { return fn2(); } `); - - _testConvertToAsyncFunction("convertToAsyncFunction_NestedFunctionRightLocation", ` + _testConvertToAsyncFunction("convertToAsyncFunction_NestedFunctionRightLocation", ` function f() { function fn2(){ function [#|fn3|](){ @@ -1189,57 +1057,48 @@ function f() { return fn2(); } `); - - _testConvertToAsyncFunction("convertToAsyncFunction_UntypedFunction", ` + _testConvertToAsyncFunction("convertToAsyncFunction_UntypedFunction", ` function [#|f|]() { return Promise.resolve().then(res => console.log(res)); } `); - - _testConvertToAsyncFunction("convertToAsyncFunction_TernaryConditional", ` + _testConvertToAsyncFunction("convertToAsyncFunction_TernaryConditional", ` function [#|f|]() { let i; return Promise.resolve().then(res => res ? i = res : i = 100); } `); - - _testConvertToAsyncFunction("convertToAsyncFunction_ResRejNoArgsArrow", ` + _testConvertToAsyncFunction("convertToAsyncFunction_ResRejNoArgsArrow", ` function [#|f|]() { return Promise.resolve().then(() => 1, () => "a"); } `); - - _testConvertToAsyncFunction("convertToAsyncFunction_simpleFunctionExpression", ` + _testConvertToAsyncFunction("convertToAsyncFunction_simpleFunctionExpression", ` const [#|foo|] = function () { return fetch('https://typescriptlang.org').then(result => { console.log(result) }); } `); - - _testConvertToAsyncFunction("convertToAsyncFunction_simpleFunctionExpressionWithName", ` + _testConvertToAsyncFunction("convertToAsyncFunction_simpleFunctionExpressionWithName", ` const foo = function [#|f|]() { return fetch('https://typescriptlang.org').then(result => { console.log(result) }); } `); - - _testConvertToAsyncFunction("convertToAsyncFunction_simpleFunctionExpressionAssignedToBindingPattern", ` + _testConvertToAsyncFunction("convertToAsyncFunction_simpleFunctionExpressionAssignedToBindingPattern", ` const { length } = [#|function|] () { return fetch('https://typescriptlang.org').then(result => { console.log(result) }); } `); - - _testConvertToAsyncFunction("convertToAsyncFunction_catchBlockUniqueParams", ` + _testConvertToAsyncFunction("convertToAsyncFunction_catchBlockUniqueParams", ` function [#|f|]() { return Promise.resolve().then(x => 1).catch(x => "a").then(x => !!x); } `); - - _testConvertToAsyncFunction("convertToAsyncFunction_catchBlockUniqueParamsBindingPattern", ` + _testConvertToAsyncFunction("convertToAsyncFunction_catchBlockUniqueParamsBindingPattern", ` function [#|f|]() { return Promise.resolve().then(() => ({ x: 3 })).catch(() => ({ x: "a" })).then(({ x }) => !!x); } `); - - _testConvertToAsyncFunction("convertToAsyncFunction_bindingPattern", ` + _testConvertToAsyncFunction("convertToAsyncFunction_bindingPattern", ` function [#|f|]() { return fetch('https://typescriptlang.org').then(res); } @@ -1247,8 +1106,7 @@ function res({ status, trailer }){ console.log(status); } `); - - _testConvertToAsyncFunction("convertToAsyncFunction_bindingPatternNameCollision", ` + _testConvertToAsyncFunction("convertToAsyncFunction_bindingPatternNameCollision", ` function [#|f|]() { const result = 'https://typescriptlang.org'; return fetch(result).then(res); @@ -1257,20 +1115,17 @@ function res({ status, trailer }){ console.log(status); } `); - - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_thenArgumentNotFunction", ` + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_thenArgumentNotFunction", ` function [#|f|]() { return Promise.resolve().then(f ? (x => x) : (y => y)); } `); - - _testConvertToAsyncFunctionFailed("convertToAsyncFunction_thenArgumentNotFunctionNotLastInChain", ` + _testConvertToAsyncFunctionFailed("convertToAsyncFunction_thenArgumentNotFunctionNotLastInChain", ` function [#|f|]() { return Promise.resolve().then(f ? (x => x) : (y => y)).then(q => q); } `); - - _testConvertToAsyncFunction("convertToAsyncFunction_runEffectfulContinuation", ` + _testConvertToAsyncFunction("convertToAsyncFunction_runEffectfulContinuation", ` function [#|f|]() { return fetch('https://typescriptlang.org').then(res).then(_ => console.log("done")); } @@ -1278,46 +1133,39 @@ function res(result) { return Promise.resolve().then(x => console.log(result)); } `); - - _testConvertToAsyncFunction("convertToAsyncFunction_callbackReturnsPromise", ` + _testConvertToAsyncFunction("convertToAsyncFunction_callbackReturnsPromise", ` function [#|f|]() { return fetch('https://typescriptlang.org').then(s => Promise.resolve(s.statusText.length)).then(x => console.log(x + 5)); } `); - - _testConvertToAsyncFunction("convertToAsyncFunction_callbackReturnsPromiseInBlock", ` + _testConvertToAsyncFunction("convertToAsyncFunction_callbackReturnsPromiseInBlock", ` function [#|f|]() { return fetch('https://typescriptlang.org').then(s => { return Promise.resolve(s.statusText.length) }).then(x => x + 5); } `); - - _testConvertToAsyncFunction("convertToAsyncFunction_callbackReturnsFixablePromise", ` + _testConvertToAsyncFunction("convertToAsyncFunction_callbackReturnsFixablePromise", ` function [#|f|]() { return fetch('https://typescriptlang.org').then(s => Promise.resolve(s.statusText).then(st => st.length)).then(x => console.log(x + 5)); } `); - - _testConvertToAsyncFunction("convertToAsyncFunction_callbackReturnsPromiseLastInChain", ` + _testConvertToAsyncFunction("convertToAsyncFunction_callbackReturnsPromiseLastInChain", ` function [#|f|]() { return fetch('https://typescriptlang.org').then(s => Promise.resolve(s.statusText.length)); } `); - - _testConvertToAsyncFunction("convertToAsyncFunction_callbackReturnsRejectedPromiseInTryBlock", ` + _testConvertToAsyncFunction("convertToAsyncFunction_callbackReturnsRejectedPromiseInTryBlock", ` function [#|f|]() { return Promise.resolve(1) .then(x => Promise.reject(x)) .catch(err => console.log(err)); } `); - - _testConvertToAsyncFunction("convertToAsyncFunction_nestedPromises", ` + _testConvertToAsyncFunction("convertToAsyncFunction_nestedPromises", ` function [#|f|]() { return fetch('https://typescriptlang.org').then(x => Promise.resolve(3).then(y => Promise.resolve(x.statusText.length + y))); } `); - - _testConvertToAsyncFunction("convertToAsyncFunction_noArgs", ` + _testConvertToAsyncFunction("convertToAsyncFunction_noArgs", ` function delay(millis: number): Promise { throw "no" } @@ -1330,39 +1178,32 @@ function [#|main2|]() { .then(() => { console.log("."); return delay(500); }) } `); - - _testConvertToAsyncFunction("convertToAsyncFunction_exportModifier", ` + _testConvertToAsyncFunction("convertToAsyncFunction_exportModifier", ` export function [#|foo|]() { return fetch('https://typescriptlang.org').then(s => console.log(s)); } `); - - _testConvertToAsyncFunction("convertToAsyncFunction_OutermostOnlySuccess", ` + _testConvertToAsyncFunction("convertToAsyncFunction_OutermostOnlySuccess", ` function [#|foo|]() { return fetch('a').then(() => { return fetch('b').then(() => 'c'); }) } `); - - _testConvertToAsyncFunctionFailedSuggestion("convertToAsyncFunction_OutermostOnlyFailure", ` + _testConvertToAsyncFunctionFailedSuggestion("convertToAsyncFunction_OutermostOnlyFailure", ` function foo() { return fetch('a').then([#|() => {|] return fetch('b').then(() => 'c'); }) } `); - }); - - function _testConvertToAsyncFunction(caption: string, text: string) { - testConvertToAsyncFunction(caption, text, "convertToAsyncFunction", /*includeLib*/ true); - } - - function _testConvertToAsyncFunctionFailed(caption: string, text: string) { - testConvertToAsyncFunction(caption, text, "convertToAsyncFunction", /*includeLib*/ true, /*expectFailure*/ true); - } - - function _testConvertToAsyncFunctionFailedSuggestion(caption: string, text: string) { - testConvertToAsyncFunction(caption, text, "convertToAsyncFunction", /*includeLib*/ true, /*expectFailure*/ true, /*onlyProvideAction*/ true); - } +}); +function _testConvertToAsyncFunction(caption: string, text: string) { + testConvertToAsyncFunction(caption, text, "convertToAsyncFunction", /*includeLib*/ true); +} +function _testConvertToAsyncFunctionFailed(caption: string, text: string) { + testConvertToAsyncFunction(caption, text, "convertToAsyncFunction", /*includeLib*/ true, /*expectFailure*/ true); +} +function _testConvertToAsyncFunctionFailedSuggestion(caption: string, text: string) { + testConvertToAsyncFunction(caption, text, "convertToAsyncFunction", /*includeLib*/ true, /*expectFailure*/ true, /*onlyProvideAction*/ true); } diff --git a/src/testRunner/unittests/services/documentRegistry.ts b/src/testRunner/unittests/services/documentRegistry.ts index e7c80486e975a..9df241e2104dc 100644 --- a/src/testRunner/unittests/services/documentRegistry.ts +++ b/src/testRunner/unittests/services/documentRegistry.ts @@ -1,76 +1,54 @@ +import { createDocumentRegistry, getDefaultCompilerOptions, ScriptSnapshot, CompilerOptions, ScriptTarget, ModuleKind, createTextChangeRange, createTextSpan } from "../../ts"; describe("unittests:: services:: DocumentRegistry", () => { it("documents are shared between projects", () => { - const documentRegistry = ts.createDocumentRegistry(); - const defaultCompilerOptions = ts.getDefaultCompilerOptions(); - - const f1 = documentRegistry.acquireDocument("file1.ts", defaultCompilerOptions, ts.ScriptSnapshot.fromString("var x = 1;"), /* version */ "1"); - const f2 = documentRegistry.acquireDocument("file1.ts", defaultCompilerOptions, ts.ScriptSnapshot.fromString("var x = 1;"), /* version */ "1"); - + const documentRegistry = createDocumentRegistry(); + const defaultCompilerOptions = getDefaultCompilerOptions(); + const f1 = documentRegistry.acquireDocument("file1.ts", defaultCompilerOptions, ScriptSnapshot.fromString("var x = 1;"), /* version */ "1"); + const f2 = documentRegistry.acquireDocument("file1.ts", defaultCompilerOptions, ScriptSnapshot.fromString("var x = 1;"), /* version */ "1"); assert(f1 === f2, "DocumentRegistry should return the same document for the same name"); }); - it("documents are refreshed when settings in compilation settings affect syntax", () => { - const documentRegistry = ts.createDocumentRegistry(); - const compilerOptions: ts.CompilerOptions = { target: ts.ScriptTarget.ES5, module: ts.ModuleKind.AMD }; - + const documentRegistry = createDocumentRegistry(); + const compilerOptions: CompilerOptions = { target: ScriptTarget.ES5, module: ModuleKind.AMD }; // change compilation setting that doesn't affect parsing - should have the same document compilerOptions.declaration = true; - const f1 = documentRegistry.acquireDocument("file1.ts", compilerOptions, ts.ScriptSnapshot.fromString("var x = 1;"), /* version */ "1"); + const f1 = documentRegistry.acquireDocument("file1.ts", compilerOptions, ScriptSnapshot.fromString("var x = 1;"), /* version */ "1"); compilerOptions.declaration = false; - const f2 = documentRegistry.acquireDocument("file1.ts", compilerOptions, ts.ScriptSnapshot.fromString("var x = 1;"), /* version */ "1"); - + const f2 = documentRegistry.acquireDocument("file1.ts", compilerOptions, ScriptSnapshot.fromString("var x = 1;"), /* version */ "1"); assert(f1 === f2, "Expected to have the same document instance"); - - // change value of compilation setting that is used during production of AST - new document is required - compilerOptions.target = ts.ScriptTarget.ES3; - const f3 = documentRegistry.acquireDocument("file1.ts", compilerOptions, ts.ScriptSnapshot.fromString("var x = 1;"), /* version */ "1"); - + compilerOptions.target = ScriptTarget.ES3; + const f3 = documentRegistry.acquireDocument("file1.ts", compilerOptions, ScriptSnapshot.fromString("var x = 1;"), /* version */ "1"); assert(f1 !== f3, "Changed target: Expected to have different instances of document"); - compilerOptions.preserveConstEnums = true; - const f4 = documentRegistry.acquireDocument("file1.ts", compilerOptions, ts.ScriptSnapshot.fromString("var x = 1;"), /* version */ "1"); - + const f4 = documentRegistry.acquireDocument("file1.ts", compilerOptions, ScriptSnapshot.fromString("var x = 1;"), /* version */ "1"); assert(f3 === f4, "Changed preserveConstEnums: Expected to have the same instance of the document"); - - compilerOptions.module = ts.ModuleKind.System; - const f5 = documentRegistry.acquireDocument("file1.ts", compilerOptions, ts.ScriptSnapshot.fromString("var x = 1;"), /* version */ "1"); - + compilerOptions.module = ModuleKind.System; + const f5 = documentRegistry.acquireDocument("file1.ts", compilerOptions, ScriptSnapshot.fromString("var x = 1;"), /* version */ "1"); assert(f4 !== f5, "Changed module: Expected to have different instances of the document"); }); - it("Acquiring document gets correct version 1", () => { - const documentRegistry = ts.createDocumentRegistry(); - const defaultCompilerOptions = ts.getDefaultCompilerOptions(); - + const documentRegistry = createDocumentRegistry(); + const defaultCompilerOptions = getDefaultCompilerOptions(); // Simulate one LS getting the document. - documentRegistry.acquireDocument("file1.ts", defaultCompilerOptions, ts.ScriptSnapshot.fromString("var x = 1;"), /* version */ "1"); - + documentRegistry.acquireDocument("file1.ts", defaultCompilerOptions, ScriptSnapshot.fromString("var x = 1;"), /* version */ "1"); // Simulate another LS getting the document at another version. - const f2 = documentRegistry.acquireDocument("file1.ts", defaultCompilerOptions, ts.ScriptSnapshot.fromString("var x = 1;"), /* version */ "2"); - + const f2 = documentRegistry.acquireDocument("file1.ts", defaultCompilerOptions, ScriptSnapshot.fromString("var x = 1;"), /* version */ "2"); assert(f2.version === "2"); }); - it("Acquiring document gets correct version 2", () => { - const documentRegistry = ts.createDocumentRegistry(); - const defaultCompilerOptions = ts.getDefaultCompilerOptions(); - + const documentRegistry = createDocumentRegistry(); + const defaultCompilerOptions = getDefaultCompilerOptions(); const contents = "var x = 1;"; - const snapshot = ts.ScriptSnapshot.fromString(contents); - + const snapshot = ScriptSnapshot.fromString(contents); // Always treat any change as a full change. - snapshot.getChangeRange = () => ts.createTextChangeRange(ts.createTextSpan(0, contents.length), contents.length); - + snapshot.getChangeRange = () => createTextChangeRange(createTextSpan(0, contents.length), contents.length); // Simulate one LS getting the document. documentRegistry.acquireDocument("file1.ts", defaultCompilerOptions, snapshot, /* version */ "1"); - // Simulate another LS getting that document. documentRegistry.acquireDocument("file1.ts", defaultCompilerOptions, snapshot, /* version */ "1"); - // Now LS1 updates their document. documentRegistry.updateDocument("file1.ts", defaultCompilerOptions, snapshot, /* version */ "2"); - // Now LS2 tries to update their document. documentRegistry.updateDocument("file1.ts", defaultCompilerOptions, snapshot, /* version */ "3"); }); diff --git a/src/testRunner/unittests/services/extract/constants.ts b/src/testRunner/unittests/services/extract/constants.ts index d9cd5010d5b09..9572cfe9f6b91 100644 --- a/src/testRunner/unittests/services/extract/constants.ts +++ b/src/testRunner/unittests/services/extract/constants.ts @@ -1,59 +1,40 @@ -namespace ts { - describe("unittests:: services:: extract:: extractConstants", () => { - testExtractConstant("extractConstant_TopLevel", - `let x = [#|1|];`); - - testExtractConstant("extractConstant_Namespace", - `namespace N { +import { testExtractSymbol, Diagnostics, testExtractSymbolFailed } from "../../../ts"; +describe("unittests:: services:: extract:: extractConstants", () => { + testExtractConstant("extractConstant_TopLevel", `let x = [#|1|];`); + testExtractConstant("extractConstant_Namespace", `namespace N { let x = [#|1|]; }`); - - testExtractConstant("extractConstant_Class", - `class C { + testExtractConstant("extractConstant_Class", `class C { x = [#|1|]; }`); - - testExtractConstant("extractConstant_Method", - `class C { + testExtractConstant("extractConstant_Method", `class C { M() { let x = [#|1|]; } }`); - - testExtractConstant("extractConstant_Function", - `function F() { + testExtractConstant("extractConstant_Function", `function F() { let x = [#|1|]; }`); - - testExtractConstant("extractConstant_ExpressionStatement", - `[#|"hello";|]`); - - testExtractConstant("extractConstant_ExpressionStatementExpression", - `[#|"hello"|];`); - - testExtractConstant("extractConstant_ExpressionStatementInNestedScope", ` + testExtractConstant("extractConstant_ExpressionStatement", `[#|"hello";|]`); + testExtractConstant("extractConstant_ExpressionStatementExpression", `[#|"hello"|];`); + testExtractConstant("extractConstant_ExpressionStatementInNestedScope", ` let i = 0; function F() { [#|i++|]; } `); - - testExtractConstant("extractConstant_ExpressionStatementConsumesLocal", ` + testExtractConstant("extractConstant_ExpressionStatementConsumesLocal", ` function F() { let i = 0; [#|i++|]; } `); - - testExtractConstant("extractConstant_BlockScopes_NoDependencies", - `for (let i = 0; i < 10; i++) { + testExtractConstant("extractConstant_BlockScopes_NoDependencies", `for (let i = 0; i < 10; i++) { for (let j = 0; j < 10; j++) { let x = [#|1|]; } }`); - - testExtractConstant("extractConstant_ClassInsertionPosition1", - `class C { + testExtractConstant("extractConstant_ClassInsertionPosition1", `class C { a = 1; b = 2; M1() { } @@ -62,9 +43,7 @@ function F() { let x = [#|1|]; } }`); - - testExtractConstant("extractConstant_ClassInsertionPosition2", - `class C { + testExtractConstant("extractConstant_ClassInsertionPosition2", `class C { a = 1; M1() { } b = 2; @@ -73,9 +52,7 @@ function F() { let x = [#|1|]; } }`); - - testExtractConstant("extractConstant_ClassInsertionPosition3", - `class C { + testExtractConstant("extractConstant_ClassInsertionPosition3", `class C { M1() { } a = 1; b = 2; @@ -84,52 +61,37 @@ function F() { let x = [#|1|]; } }`); - - testExtractConstant("extractConstant_Parameters", - `function F() { + testExtractConstant("extractConstant_Parameters", `function F() { let w = 1; let x = [#|w + 1|]; }`); - - testExtractConstant("extractConstant_TypeParameters", - `function F(t: T) { + testExtractConstant("extractConstant_TypeParameters", `function F(t: T) { let x = [#|t + 1|]; }`); - - testExtractConstant("extractConstant_RepeatedSubstitution", - `namespace X { + testExtractConstant("extractConstant_RepeatedSubstitution", `namespace X { export const j = 10; export const y = [#|j * j|]; }`); - - testExtractConstant("extractConstant_VariableList_const", - `const a = 1, b = [#|a + 1|];`); - - // NOTE: this test isn't normative - it just documents our sub-optimal behavior. - testExtractConstant("extractConstant_VariableList_let", - `let a = 1, b = [#|a + 1|];`); - - // NOTE: this test isn't normative - it just documents our sub-optimal behavior. - testExtractConstant("extractConstant_VariableList_MultipleLines", - `const /*About A*/a = 1, + testExtractConstant("extractConstant_VariableList_const", `const a = 1, b = [#|a + 1|];`); + // NOTE: this test isn't normative - it just documents our sub-optimal behavior. + testExtractConstant("extractConstant_VariableList_let", `let a = 1, b = [#|a + 1|];`); + // NOTE: this test isn't normative - it just documents our sub-optimal behavior. + testExtractConstant("extractConstant_VariableList_MultipleLines", `const /*About A*/a = 1, /*About B*/b = [#|a + 1|];`); - - testExtractConstant("extractConstant_BlockScopeMismatch", ` + testExtractConstant("extractConstant_BlockScopeMismatch", ` for (let i = 0; i < 10; i++) { for (let j = 0; j < 10; j++) { const x = [#|i + 1|]; } } `); - - testExtractConstant("extractConstant_StatementInsertionPosition1", ` + testExtractConstant("extractConstant_StatementInsertionPosition1", ` const i = 0; for (let j = 0; j < 10; j++) { const x = [#|i + 1|]; } `); - - testExtractConstant("extractConstant_StatementInsertionPosition2", ` + testExtractConstant("extractConstant_StatementInsertionPosition2", ` const i = 0; function F() { for (let j = 0; j < 10; j++) { @@ -137,22 +99,19 @@ function F() { } } `); - - testExtractConstant("extractConstant_StatementInsertionPosition3", ` + testExtractConstant("extractConstant_StatementInsertionPosition3", ` for (let j = 0; j < 10; j++) { const x = [#|2 + 1|]; } `); - - testExtractConstant("extractConstant_StatementInsertionPosition4", ` + testExtractConstant("extractConstant_StatementInsertionPosition4", ` function F() { for (let j = 0; j < 10; j++) { const x = [#|2 + 1|]; } } `); - - testExtractConstant("extractConstant_StatementInsertionPosition5", ` + testExtractConstant("extractConstant_StatementInsertionPosition5", ` function F0() { function F1() { function F2(x = [#|2 + 1|]) { @@ -160,14 +119,12 @@ function F0() { } } `); - - testExtractConstant("extractConstant_StatementInsertionPosition6", ` + testExtractConstant("extractConstant_StatementInsertionPosition6", ` class C { x = [#|2 + 1|]; } `); - - testExtractConstant("extractConstant_StatementInsertionPosition7", ` + testExtractConstant("extractConstant_StatementInsertionPosition7", ` const i = 0; class C { M() { @@ -177,26 +134,22 @@ class C { } } `); - - testExtractConstant("extractConstant_TripleSlash", ` + testExtractConstant("extractConstant_TripleSlash", ` /// const x = [#|2 + 1|]; `); - - testExtractConstant("extractConstant_PinnedComment", ` + testExtractConstant("extractConstant_PinnedComment", ` /*! Copyright */ const x = [#|2 + 1|]; `); - - testExtractConstant("extractConstant_Directive", ` + testExtractConstant("extractConstant_Directive", ` "strict"; const x = [#|2 + 1|]; `); - - testExtractConstant("extractConstant_MultipleHeaders", ` + testExtractConstant("extractConstant_MultipleHeaders", ` /*! Copyright */ /// @@ -205,87 +158,71 @@ const x = [#|2 + 1|]; const x = [#|2 + 1|]; `); - - testExtractConstant("extractConstant_PinnedCommentAndDocComment", ` + testExtractConstant("extractConstant_PinnedCommentAndDocComment", ` /*! Copyright */ /* About x */ const x = [#|2 + 1|]; `); - - testExtractConstant("extractConstant_ArrowFunction_Block", ` + testExtractConstant("extractConstant_ArrowFunction_Block", ` const f = () => { return [#|2 + 1|]; };`); - - testExtractConstant("extractConstant_ArrowFunction_Expression", - `const f = () => [#|2 + 1|];`); - - testExtractConstant("extractConstant_PreserveTrivia", ` + testExtractConstant("extractConstant_ArrowFunction_Expression", `const f = () => [#|2 + 1|];`); + testExtractConstant("extractConstant_PreserveTrivia", ` // a var q = /*b*/ //c /*d*/ [#|1 /*e*/ //f /*g*/ + /*h*/ //i /*j*/ 2|] /*k*/ //l /*m*/; /*n*/ //o`); - - testExtractConstantFailed("extractConstant_Void", ` + testExtractConstantFailed("extractConstant_Void", ` function f(): void { } [#|f();|]`); - - testExtractConstantFailed("extractConstant_Never", ` + testExtractConstantFailed("extractConstant_Never", ` function f(): never { } [#|f();|]`); - - testExtractConstant("extractConstant_This_Constructor", ` + testExtractConstant("extractConstant_This_Constructor", ` class C { constructor() { [#|this.m2()|]; } m2() { return 1; } }`); - - testExtractConstant("extractConstant_This_Method", ` + testExtractConstant("extractConstant_This_Method", ` class C { m1() { [#|this.m2()|]; } m2() { return 1; } }`); - - testExtractConstant("extractConstant_This_Property", ` + testExtractConstant("extractConstant_This_Property", ` namespace N { // Force this test to be TS-only class C { x = 1; y = [#|this.x|]; } }`); - - // TODO (https://github.com/Microsoft/TypeScript/issues/20727): the extracted constant should have a type annotation. - testExtractConstant("extractConstant_ContextualType", ` + // TODO (https://github.com/Microsoft/TypeScript/issues/20727): the extracted constant should have a type annotation. + testExtractConstant("extractConstant_ContextualType", ` interface I { a: 1 | 2 | 3 } let i: I = [#|{ a: 1 }|]; `); - - testExtractConstant("extractConstant_ContextualType_Lambda", ` + testExtractConstant("extractConstant_ContextualType_Lambda", ` const myObj: { member(x: number, y: string): void } = { member: [#|(x, y) => x + y|], } `); - - testExtractConstant("extractConstant_CaseClauseExpression", ` + testExtractConstant("extractConstant_CaseClauseExpression", ` switch (1) { case [#|1|]: break; } `); - }); - - function testExtractConstant(caption: string, text: string) { - testExtractSymbol(caption, text, "extractConstant", Diagnostics.Extract_constant); - } - - function testExtractConstantFailed(caption: string, text: string) { - testExtractSymbolFailed(caption, text, Diagnostics.Extract_constant); - } +}); +function testExtractConstant(caption: string, text: string) { + testExtractSymbol(caption, text, "extractConstant", Diagnostics.Extract_constant); +} +function testExtractConstantFailed(caption: string, text: string) { + testExtractSymbolFailed(caption, text, Diagnostics.Extract_constant); } diff --git a/src/testRunner/unittests/services/extract/functions.ts b/src/testRunner/unittests/services/extract/functions.ts index 6f7eecce05a64..99b04f42791b2 100644 --- a/src/testRunner/unittests/services/extract/functions.ts +++ b/src/testRunner/unittests/services/extract/functions.ts @@ -1,7 +1,6 @@ -namespace ts { - describe("unittests:: services:: extract:: extractFunctions", () => { - testExtractFunction("extractFunction1", - `namespace A { +import { testExtractSymbol, Diagnostics } from "../../../ts"; +describe("unittests:: services:: extract:: extractFunctions", () => { + testExtractFunction("extractFunction1", `namespace A { let x = 1; function foo() { } @@ -16,8 +15,7 @@ namespace ts { } } }`); - testExtractFunction("extractFunction2", - `namespace A { + testExtractFunction("extractFunction2", `namespace A { let x = 1; function foo() { } @@ -30,8 +28,7 @@ namespace ts { } } }`); - testExtractFunction("extractFunction3", - `namespace A { + testExtractFunction("extractFunction3", `namespace A { function foo() { } namespace B { @@ -43,8 +40,7 @@ namespace ts { } } }`); - testExtractFunction("extractFunction4", - `namespace A { + testExtractFunction("extractFunction4", `namespace A { function foo() { } namespace B { @@ -58,8 +54,7 @@ namespace ts { } } }`); - testExtractFunction("extractFunction5", - `namespace A { + testExtractFunction("extractFunction5", `namespace A { let x = 1; export function foo() { } @@ -74,8 +69,7 @@ namespace ts { } } }`); - testExtractFunction("extractFunction6", - `namespace A { + testExtractFunction("extractFunction6", `namespace A { let x = 1; export function foo() { } @@ -90,8 +84,7 @@ namespace ts { } } }`); - testExtractFunction("extractFunction7", - `namespace A { + testExtractFunction("extractFunction7", `namespace A { let x = 1; export namespace C { export function foo() { @@ -108,8 +101,7 @@ namespace ts { } } }`); - testExtractFunction("extractFunction9", - `namespace A { + testExtractFunction("extractFunction9", `namespace A { export interface I { x: number }; namespace B { function a() { @@ -118,8 +110,7 @@ namespace ts { } } }`); - testExtractFunction("extractFunction10", - `namespace A { + testExtractFunction("extractFunction10", `namespace A { export interface I { x: number }; class C { a() { @@ -129,8 +120,7 @@ namespace ts { } } }`); - testExtractFunction("extractFunction11", - `namespace A { + testExtractFunction("extractFunction11", `namespace A { let y = 1; class C { a() { @@ -142,8 +132,7 @@ namespace ts { } } }`); - testExtractFunction("extractFunction12", - `namespace A { + testExtractFunction("extractFunction12", `namespace A { let y = 1; class C { b() {} @@ -157,13 +146,12 @@ namespace ts { } } }`); - // The "b" type parameters aren't used and shouldn't be passed to the extracted function. - // Type parameters should be in syntactic order (i.e. in order or character offset from BOF). - // In all cases, we could use type inference, rather than passing explicit type arguments. - // Note the inclusion of arrow functions to ensure that some type parameters are not from - // targetable scopes. - testExtractFunction("extractFunction13", - `(u1a: U1a, u1b: U1b) => { + // The "b" type parameters aren't used and shouldn't be passed to the extracted function. + // Type parameters should be in syntactic order (i.e. in order or character offset from BOF). + // In all cases, we could use type inference, rather than passing explicit type arguments. + // Note the inclusion of arrow functions to ensure that some type parameters are not from + // targetable scopes. + testExtractFunction("extractFunction13", `(u1a: U1a, u1b: U1b) => { function F1(t1a: T1a, t1b: T1b) { (u2a: U2a, u2b: U2b) => { function F2(t2a: T2a, t2b: T2b) { @@ -178,107 +166,92 @@ namespace ts { } } }`); - // This test is descriptive, rather than normative. The current implementation - // doesn't handle type parameter shadowing. - testExtractFunction("extractFunction14", - `function F(t1: T) { + // This test is descriptive, rather than normative. The current implementation + // doesn't handle type parameter shadowing. + testExtractFunction("extractFunction14", `function F(t1: T) { function G(t2: T) { [#|t1.toString(); t2.toString();|] } }`); - // Confirm that the constraint is preserved. - testExtractFunction("extractFunction15", - `function F(t1: T) { + // Confirm that the constraint is preserved. + testExtractFunction("extractFunction15", `function F(t1: T) { function G(t2: U) { [#|t2.toString();|] } }`, /*includeLib*/ true); - // Confirm that the contextual type of an extracted expression counts as a use. - testExtractFunction("extractFunction16", - `function F() { + // Confirm that the contextual type of an extracted expression counts as a use. + testExtractFunction("extractFunction16", `function F() { const array: T[] = [#|[]|]; }`, /*includeLib*/ true); - // Class type parameter - testExtractFunction("extractFunction17", - `class C { + // Class type parameter + testExtractFunction("extractFunction17", `class C { M(t1: T1, t2: T2) { [#|t1.toString()|]; } }`); - // Function type parameter - testExtractFunction("extractFunction18", - `class C { + // Function type parameter + testExtractFunction("extractFunction18", `class C { M(t1: T1, t2: T2) { [#|t1.toString()|]; } }`); - // Coupled constraints - testExtractFunction("extractFunction19", - `function F(v: V) { + // Coupled constraints + testExtractFunction("extractFunction19", `function F(v: V) { [#|v.toString()|]; }`, /*includeLib*/ true); - - testExtractFunction("extractFunction20", - `const _ = class { + testExtractFunction("extractFunction20", `const _ = class { a() { [#|let a1 = { x: 1 }; return a1.x + 10;|] } }`); - // Write + void return - testExtractFunction("extractFunction21", - `function foo() { + // Write + void return + testExtractFunction("extractFunction21", `function foo() { let x = 10; [#|x++; return;|] }`); - // Return in finally block - testExtractFunction("extractFunction22", - `function test() { + // Return in finally block + testExtractFunction("extractFunction22", `function test() { try { } finally { [#|return 1;|] } }`); - // Extraction position - namespace - testExtractFunction("extractFunction23", - `namespace NS { + // Extraction position - namespace + testExtractFunction("extractFunction23", `namespace NS { function M1() { } function M2() { [#|return 1;|] } function M3() { } }`); - // Extraction position - function - testExtractFunction("extractFunction24", - `function Outer() { + // Extraction position - function + testExtractFunction("extractFunction24", `function Outer() { function M1() { } function M2() { [#|return 1;|] } function M3() { } }`); - // Extraction position - file - testExtractFunction("extractFunction25", - `function M1() { } + // Extraction position - file + testExtractFunction("extractFunction25", `function M1() { } function M2() { [#|return 1;|] } function M3() { }`); - // Extraction position - class without ctor - testExtractFunction("extractFunction26", - `class C { + // Extraction position - class without ctor + testExtractFunction("extractFunction26", `class C { M1() { } M2() { [#|return 1;|] } M3() { } }`); - // Extraction position - class with ctor in middle - testExtractFunction("extractFunction27", - `class C { + // Extraction position - class with ctor in middle + testExtractFunction("extractFunction27", `class C { M1() { } M2() { [#|return 1;|] @@ -286,9 +259,8 @@ function M3() { }`); constructor() { } M3() { } }`); - // Extraction position - class with ctor at end - testExtractFunction("extractFunction28", - `class C { + // Extraction position - class with ctor at end + testExtractFunction("extractFunction28", `class C { M1() { } M2() { [#|return 1;|] @@ -296,9 +268,8 @@ function M3() { }`); M3() { } constructor() { } }`); - // Shorthand property names - testExtractFunction("extractFunction29", - `interface UnaryExpression { + // Shorthand property names + testExtractFunction("extractFunction29", `interface UnaryExpression { kind: "Unary"; operator: string; operand: any; @@ -315,14 +286,12 @@ function parseUnaryExpression(operator: string): UnaryExpression { function parsePrimaryExpression(): any { throw "Not implemented"; }`); - // Type parameter as declared type - testExtractFunction("extractFunction30", - `function F() { + // Type parameter as declared type + testExtractFunction("extractFunction30", `function F() { [#|let t: T;|] }`); - // Return in nested function - testExtractFunction("extractFunction31", - `namespace N { + // Return in nested function + testExtractFunction("extractFunction31", `namespace N { export const value = 1; @@ -333,9 +302,8 @@ function parsePrimaryExpression(): any { }|] } }`); - // Return in nested class - testExtractFunction("extractFunction32", - `namespace N { + // Return in nested class + testExtractFunction("extractFunction32", `namespace N { export const value = 1; @@ -347,154 +315,131 @@ function parsePrimaryExpression(): any { }|] } }`); - // Selection excludes leading trivia of declaration - testExtractFunction("extractFunction33", - `function F() { + // Selection excludes leading trivia of declaration + testExtractFunction("extractFunction33", `function F() { [#|function G() { }|] }`); - - testExtractFunction("extractFunction_RepeatedSubstitution", - `namespace X { + testExtractFunction("extractFunction_RepeatedSubstitution", `namespace X { export const j = 10; export const y = [#|j * j|]; }`); - - testExtractFunction("extractFunction_VariableDeclaration_Var", ` + testExtractFunction("extractFunction_VariableDeclaration_Var", ` [#|var x = 1; "hello"|] x; `); - - testExtractFunction("extractFunction_VariableDeclaration_Let_Type", ` + testExtractFunction("extractFunction_VariableDeclaration_Let_Type", ` [#|let x: number = 1; "hello";|] x; `); - - testExtractFunction("extractFunction_VariableDeclaration_Let_NoType", ` + testExtractFunction("extractFunction_VariableDeclaration_Let_NoType", ` [#|let x = 1; "hello";|] x; `); - - testExtractFunction("extractFunction_VariableDeclaration_Const_Type", ` + testExtractFunction("extractFunction_VariableDeclaration_Const_Type", ` [#|const x: number = 1; "hello";|] x; `); - - testExtractFunction("extractFunction_VariableDeclaration_Const_NoType", ` + testExtractFunction("extractFunction_VariableDeclaration_Const_NoType", ` [#|const x = 1; "hello";|] x; `); - - testExtractFunction("extractFunction_VariableDeclaration_Multiple1", ` + testExtractFunction("extractFunction_VariableDeclaration_Multiple1", ` [#|const x = 1, y: string = "a";|] x; y; `); - - testExtractFunction("extractFunction_VariableDeclaration_Multiple2", ` + testExtractFunction("extractFunction_VariableDeclaration_Multiple2", ` [#|const x = 1, y = "a"; const z = 3;|] x; y; z; `); - - testExtractFunction("extractFunction_VariableDeclaration_Multiple3", ` + testExtractFunction("extractFunction_VariableDeclaration_Multiple3", ` [#|const x = 1, y: string = "a"; let z = 3;|] x; y; z; `); - - testExtractFunction("extractFunction_VariableDeclaration_ConsumedTwice", ` + testExtractFunction("extractFunction_VariableDeclaration_ConsumedTwice", ` [#|const x: number = 1; "hello";|] x; x; `); - - testExtractFunction("extractFunction_VariableDeclaration_DeclaredTwice", ` + testExtractFunction("extractFunction_VariableDeclaration_DeclaredTwice", ` [#|var x = 1; var x = 2;|] x; `); - - testExtractFunction("extractFunction_VariableDeclaration_Writes_Var", ` + testExtractFunction("extractFunction_VariableDeclaration_Writes_Var", ` function f() { let a = 1; [#|var x = 1; a++;|] a; x; }`); - - testExtractFunction("extractFunction_VariableDeclaration_Writes_Let_NoType", ` + testExtractFunction("extractFunction_VariableDeclaration_Writes_Let_NoType", ` function f() { let a = 1; [#|let x = 1; a++;|] a; x; }`); - - testExtractFunction("extractFunction_VariableDeclaration_Writes_Let_Type", ` + testExtractFunction("extractFunction_VariableDeclaration_Writes_Let_Type", ` function f() { let a = 1; [#|let x: number = 1; a++;|] a; x; }`); - - // We propagate numericLiteralFlags, but it's not consumed by the emitter, - // so everything comes out decimal. It would be nice to improve this. - testExtractFunction("extractFunction_VariableDeclaration_Writes_Let_LiteralType1", ` + // We propagate numericLiteralFlags, but it's not consumed by the emitter, + // so everything comes out decimal. It would be nice to improve this. + testExtractFunction("extractFunction_VariableDeclaration_Writes_Let_LiteralType1", ` function f() { let a = 1; [#|let x: 0o10 | 10 | 0b10 = 10; a++;|] a; x; }`); - - testExtractFunction("extractFunction_VariableDeclaration_Writes_Let_LiteralType2", ` + testExtractFunction("extractFunction_VariableDeclaration_Writes_Let_LiteralType2", ` function f() { let a = 1; [#|let x: "a" | 'b' = 'a'; a++;|] a; x; }`); - - // We propagate numericLiteralFlags, but it's not consumed by the emitter, - // so everything comes out decimal. It would be nice to improve this. - testExtractFunction("extractFunction_VariableDeclaration_Writes_Let_LiteralType1", ` + // We propagate numericLiteralFlags, but it's not consumed by the emitter, + // so everything comes out decimal. It would be nice to improve this. + testExtractFunction("extractFunction_VariableDeclaration_Writes_Let_LiteralType1", ` function f() { let a = 1; [#|let x: 0o10 | 10 | 0b10 = 10; a++;|] a; x; }`); - - testExtractFunction("extractFunction_VariableDeclaration_Writes_Let_TypeWithComments", ` + testExtractFunction("extractFunction_VariableDeclaration_Writes_Let_TypeWithComments", ` function f() { let a = 1; [#|let x: /*A*/ "a" /*B*/ | /*C*/ 'b' /*D*/ = 'a'; a++;|] a; x; }`); - - testExtractFunction("extractFunction_VariableDeclaration_Writes_Const_NoType", ` + testExtractFunction("extractFunction_VariableDeclaration_Writes_Const_NoType", ` function f() { let a = 1; [#|const x = 1; a++;|] a; x; }`); - - testExtractFunction("extractFunction_VariableDeclaration_Writes_Const_Type", ` + testExtractFunction("extractFunction_VariableDeclaration_Writes_Const_Type", ` function f() { let a = 1; [#|const x: number = 1; a++;|] a; x; }`); - - testExtractFunction("extractFunction_VariableDeclaration_Writes_Mixed1", ` + testExtractFunction("extractFunction_VariableDeclaration_Writes_Mixed1", ` function f() { let a = 1; [#|const x = 1; @@ -502,8 +447,7 @@ function f() { a++;|] a; x; y; }`); - - testExtractFunction("extractFunction_VariableDeclaration_Writes_Mixed2", ` + testExtractFunction("extractFunction_VariableDeclaration_Writes_Mixed2", ` function f() { let a = 1; [#|var x = 1; @@ -511,8 +455,7 @@ function f() { a++;|] a; x; y; }`); - - testExtractFunction("extractFunction_VariableDeclaration_Writes_Mixed3", ` + testExtractFunction("extractFunction_VariableDeclaration_Writes_Mixed3", ` function f() { let a = 1; [#|let x: number = 1; @@ -520,8 +463,7 @@ function f() { a++;|] a; x; y; }`); - - testExtractFunction("extractFunction_VariableDeclaration_Writes_UnionUndefined", ` + testExtractFunction("extractFunction_VariableDeclaration_Writes_UnionUndefined", ` function f() { let a = 1; [#|let x: number | undefined = 1; @@ -530,35 +472,29 @@ function f() { a++;|] a; x; y; z; }`); - - testExtractFunction("extractFunction_VariableDeclaration_ShorthandProperty", ` + testExtractFunction("extractFunction_VariableDeclaration_ShorthandProperty", ` function f() { [#|let x;|] return { x }; }`); - - testExtractFunction("extractFunction_PreserveTrivia", ` + testExtractFunction("extractFunction_PreserveTrivia", ` // a var q = /*b*/ //c /*d*/ [#|1 /*e*/ //f /*g*/ + /*h*/ //i /*j*/ 2|] /*k*/ //l /*m*/; /*n*/ //o`); - - testExtractFunction("extractFunction_NamelessClass", ` + testExtractFunction("extractFunction_NamelessClass", ` export default class { M() { [#|1 + 1|]; } }`); - - testExtractFunction("extractFunction_NoDeclarations", ` + testExtractFunction("extractFunction_NoDeclarations", ` function F() { [#|arguments.length|]; // arguments has no declaration }`); - }); - - function testExtractFunction(caption: string, text: string, includeLib?: boolean) { - testExtractSymbol(caption, text, "extractFunction", Diagnostics.Extract_function, includeLib); - } +}); +function testExtractFunction(caption: string, text: string, includeLib?: boolean) { + testExtractSymbol(caption, text, "extractFunction", Diagnostics.Extract_function, includeLib); } diff --git a/src/testRunner/unittests/services/extract/helpers.ts b/src/testRunner/unittests/services/extract/helpers.ts index a998bf6f51057..850688c39d9ff 100644 --- a/src/testRunner/unittests/services/extract/helpers.ts +++ b/src/testRunner/unittests/services/extract/helpers.ts @@ -1,176 +1,164 @@ -namespace ts { - interface Range { - pos: number; - end: number; - name: string; - } - - interface Test { - source: string; - ranges: Map; - } - - export function extractTest(source: string): Test { - const activeRanges: Range[] = []; - let text = ""; - let lastPos = 0; - let pos = 0; - const ranges = createMap(); - - while (pos < source.length) { - if (source.charCodeAt(pos) === CharacterCodes.openBracket && - (source.charCodeAt(pos + 1) === CharacterCodes.hash || source.charCodeAt(pos + 1) === CharacterCodes.$)) { - const saved = pos; - pos += 2; - const s = pos; - consumeIdentifier(); - const e = pos; - if (source.charCodeAt(pos) === CharacterCodes.bar) { - pos++; - text += source.substring(lastPos, saved); - const name = s === e - ? source.charCodeAt(saved + 1) === CharacterCodes.hash ? "selection" : "extracted" - : source.substring(s, e); - activeRanges.push({ name, pos: text.length, end: undefined! }); // TODO: GH#18217 - lastPos = pos; - continue; - } - else { - pos = saved; - } - } - else if (source.charCodeAt(pos) === CharacterCodes.bar && source.charCodeAt(pos + 1) === CharacterCodes.closeBracket) { - text += source.substring(lastPos, pos); - activeRanges[activeRanges.length - 1].end = text.length; - const range = activeRanges.pop()!; - if (hasProperty(ranges, range.name)) { - throw new Error(`Duplicate name of range ${range.name}`); - } - ranges.set(range.name, range); - pos += 2; +import { createMap, CharacterCodes, hasProperty, isIdentifierPart, ScriptTarget, LanguageServiceHost, notImplemented, DiagnosticMessage, Extension, RefactorContext, noop, returnFalse, formatting, testFormatSettings, emptyOptions, refactor, createTextSpanFromRange, find, textChanges, Program, length } from "../../../ts"; +import { Baseline } from "../../../Harness"; +import { createServerHost, libFile, createProjectService } from "../../../ts.projectSystem"; +import * as ts from "../../../ts"; +interface Range { + pos: number; + end: number; + name: string; +} +interface Test { + source: string; + ranges: ts.Map; +} +export function extractTest(source: string): Test { + const activeRanges: Range[] = []; + let text = ""; + let lastPos = 0; + let pos = 0; + const ranges = createMap(); + while (pos < source.length) { + if (source.charCodeAt(pos) === CharacterCodes.openBracket && + (source.charCodeAt(pos + 1) === CharacterCodes.hash || source.charCodeAt(pos + 1) === CharacterCodes.$)) { + const saved = pos; + pos += 2; + const s = pos; + consumeIdentifier(); + const e = pos; + if (source.charCodeAt(pos) === CharacterCodes.bar) { + pos++; + text += source.substring(lastPos, saved); + const name = s === e + ? source.charCodeAt(saved + 1) === CharacterCodes.hash ? "selection" : "extracted" + : source.substring(s, e); + activeRanges.push({ name, pos: text.length, end: undefined! }); // TODO: GH#18217 lastPos = pos; continue; } - pos++; + else { + pos = saved; + } } - text += source.substring(lastPos, pos); - - function consumeIdentifier() { - while (isIdentifierPart(source.charCodeAt(pos), ScriptTarget.Latest)) { - pos++; + else if (source.charCodeAt(pos) === CharacterCodes.bar && source.charCodeAt(pos + 1) === CharacterCodes.closeBracket) { + text += source.substring(lastPos, pos); + activeRanges[activeRanges.length - 1].end = text.length; + const range = activeRanges.pop()!; + if (hasProperty(ranges, range.name)) { + throw new Error(`Duplicate name of range ${range.name}`); } + ranges.set(range.name, range); + pos += 2; + lastPos = pos; + continue; } - return { source: text, ranges }; + pos++; } - - export const newLineCharacter = "\n"; - - export const notImplementedHost: LanguageServiceHost = { - getCompilationSettings: notImplemented, - getScriptFileNames: notImplemented, - getScriptVersion: notImplemented, - getScriptSnapshot: notImplemented, - getDefaultLibFileName: notImplemented, - getCurrentDirectory: notImplemented, - }; - - export function testExtractSymbol(caption: string, text: string, baselineFolder: string, description: DiagnosticMessage, includeLib?: boolean) { - const t = extractTest(text); - const selectionRange = t.ranges.get("selection")!; - if (!selectionRange) { - throw new Error(`Test ${caption} does not specify selection range`); - } - - [Extension.Ts, Extension.Js].forEach(extension => - it(`${caption} [${extension}]`, () => runBaseline(extension))); - - function runBaseline(extension: Extension) { - const path = "/a" + extension; - const program = makeProgram({ path, content: t.source }, includeLib); - - if (hasSyntacticDiagnostics(program)) { - // Don't bother generating JS baselines for inputs that aren't valid JS. - assert.equal(Extension.Js, extension, "Syntactic diagnostics found in non-JS file"); - return; - } - - const sourceFile = program.getSourceFile(path)!; - const context: RefactorContext = { - cancellationToken: { throwIfCancellationRequested: noop, isCancellationRequested: returnFalse }, - program, - file: sourceFile, - startPosition: selectionRange.pos, - endPosition: selectionRange.end, - host: notImplementedHost, - formatContext: formatting.getFormatContext(testFormatSettings), - preferences: emptyOptions, - }; - const rangeToExtract = refactor.extractSymbol.getRangeToExtract(sourceFile, createTextSpanFromRange(selectionRange)); - assert.equal(rangeToExtract.errors, undefined, rangeToExtract.errors && "Range error: " + rangeToExtract.errors[0].messageText); - const infos = refactor.extractSymbol.getAvailableActions(context); - const actions = find(infos, info => info.description === description.message)!.actions; - - const data: string[] = []; - data.push(`// ==ORIGINAL==`); - data.push(text.replace("[#|", "/*[#|*/").replace("|]", "/*|]*/")); - for (const action of actions) { - const { renameLocation, edits } = refactor.extractSymbol.getEditsForAction(context, action.name)!; - assert.lengthOf(edits, 1); - data.push(`// ==SCOPE::${action.description}==`); - const newText = textChanges.applyChanges(sourceFile.text, edits[0].textChanges); - const newTextWithRename = newText.slice(0, renameLocation) + "/*RENAME*/" + newText.slice(renameLocation); - data.push(newTextWithRename); - - const diagProgram = makeProgram({ path, content: newText }, includeLib); - assert.isFalse(hasSyntacticDiagnostics(diagProgram)); - } - Harness.Baseline.runBaseline(`${baselineFolder}/${caption}${extension}`, data.join(newLineCharacter)); + text += source.substring(lastPos, pos); + function consumeIdentifier() { + while (isIdentifierPart(source.charCodeAt(pos), ScriptTarget.Latest)) { + pos++; } - - function makeProgram(f: {path: string, content: string }, includeLib?: boolean) { - const host = projectSystem.createServerHost(includeLib ? [f, projectSystem.libFile] : [f]); // libFile is expensive to parse repeatedly - only test when required - const projectService = projectSystem.createProjectService(host); - projectService.openClientFile(f.path); - const program = projectService.inferredProjects[0].getLanguageService().getProgram()!; - return program; + } + return { source: text, ranges }; +} +export const newLineCharacter = "\n"; +export const notImplementedHost: LanguageServiceHost = { + getCompilationSettings: notImplemented, + getScriptFileNames: notImplemented, + getScriptVersion: notImplemented, + getScriptSnapshot: notImplemented, + getDefaultLibFileName: notImplemented, + getCurrentDirectory: notImplemented, +}; +export function testExtractSymbol(caption: string, text: string, baselineFolder: string, description: DiagnosticMessage, includeLib?: boolean) { + const t = extractTest(text); + const selectionRange = t.ranges.get("selection")!; + if (!selectionRange) { + throw new Error(`Test ${caption} does not specify selection range`); + } + [Extension.Ts, Extension.Js].forEach(extension => it(`${caption} [${extension}]`, () => runBaseline(extension))); + function runBaseline(extension: Extension) { + const path = "/a" + extension; + const program = makeProgram({ path, content: t.source }, includeLib); + if (hasSyntacticDiagnostics(program)) { + // Don't bother generating JS baselines for inputs that aren't valid JS. + assert.equal(Extension.Js, extension, "Syntactic diagnostics found in non-JS file"); + return; } - - function hasSyntacticDiagnostics(program: Program) { - const diags = program.getSyntacticDiagnostics(); - return length(diags) > 0; + const sourceFile = program.getSourceFile(path)!; + const context: RefactorContext = { + cancellationToken: { throwIfCancellationRequested: noop, isCancellationRequested: returnFalse }, + program, + file: sourceFile, + startPosition: selectionRange.pos, + endPosition: selectionRange.end, + host: notImplementedHost, + formatContext: formatting.getFormatContext(testFormatSettings), + preferences: emptyOptions, + }; + const rangeToExtract = refactor.extractSymbol.getRangeToExtract(sourceFile, createTextSpanFromRange(selectionRange)); + assert.equal(rangeToExtract.errors, undefined, rangeToExtract.errors && "Range error: " + rangeToExtract.errors[0].messageText); + const infos = refactor.extractSymbol.getAvailableActions(context); + const actions = find(infos, info => info.description === description.message)!.actions; + const data: string[] = []; + data.push(`// ==ORIGINAL==`); + data.push(text.replace("[#|", "/*[#|*/").replace("|]", "/*|]*/")); + for (const action of actions) { + const { renameLocation, edits } = (refactor.extractSymbol.getEditsForAction(context, action.name)!); + assert.lengthOf(edits, 1); + data.push(`// ==SCOPE::${action.description}==`); + const newText = textChanges.applyChanges(sourceFile.text, edits[0].textChanges); + const newTextWithRename = newText.slice(0, renameLocation) + "/*RENAME*/" + newText.slice(renameLocation); + data.push(newTextWithRename); + const diagProgram = makeProgram({ path, content: newText }, includeLib); + assert.isFalse(hasSyntacticDiagnostics(diagProgram)); } + Baseline.runBaseline(`${baselineFolder}/${caption}${extension}`, data.join(newLineCharacter)); } - - export function testExtractSymbolFailed(caption: string, text: string, description: DiagnosticMessage) { - it(caption, () => { - const t = extractTest(text); - const selectionRange = t.ranges.get("selection"); - if (!selectionRange) { - throw new Error(`Test ${caption} does not specify selection range`); - } - const f = { - path: "/a.ts", - content: t.source - }; - const host = projectSystem.createServerHost([f, projectSystem.libFile]); - const projectService = projectSystem.createProjectService(host); - projectService.openClientFile(f.path); - const program = projectService.inferredProjects[0].getLanguageService().getProgram()!; - const sourceFile = program.getSourceFile(f.path)!; - const context: RefactorContext = { - cancellationToken: { throwIfCancellationRequested: noop, isCancellationRequested: returnFalse }, - program, - file: sourceFile, - startPosition: selectionRange.pos, - endPosition: selectionRange.end, - host: notImplementedHost, - formatContext: formatting.getFormatContext(testFormatSettings), - preferences: emptyOptions, - }; - const rangeToExtract = refactor.extractSymbol.getRangeToExtract(sourceFile, createTextSpanFromRange(selectionRange)); - assert.isUndefined(rangeToExtract.errors, rangeToExtract.errors && "Range error: " + rangeToExtract.errors[0].messageText); - const infos = refactor.extractSymbol.getAvailableActions(context); - assert.isUndefined(find(infos, info => info.description === description.message)); - }); + function makeProgram(f: { + path: string; + content: string; + }, includeLib?: boolean) { + const host = createServerHost(includeLib ? [f, libFile] : [f]); // libFile is expensive to parse repeatedly - only test when required + const projectService = createProjectService(host); + projectService.openClientFile(f.path); + const program = projectService.inferredProjects[0].getLanguageService().getProgram()!; + return program; } + function hasSyntacticDiagnostics(program: Program) { + const diags = program.getSyntacticDiagnostics(); + return length(diags) > 0; + } +} +export function testExtractSymbolFailed(caption: string, text: string, description: DiagnosticMessage) { + it(caption, () => { + const t = extractTest(text); + const selectionRange = t.ranges.get("selection"); + if (!selectionRange) { + throw new Error(`Test ${caption} does not specify selection range`); + } + const f = { + path: "/a.ts", + content: t.source + }; + const host = createServerHost([f, libFile]); + const projectService = createProjectService(host); + projectService.openClientFile(f.path); + const program = projectService.inferredProjects[0].getLanguageService().getProgram()!; + const sourceFile = program.getSourceFile(f.path)!; + const context: RefactorContext = { + cancellationToken: { throwIfCancellationRequested: noop, isCancellationRequested: returnFalse }, + program, + file: sourceFile, + startPosition: selectionRange.pos, + endPosition: selectionRange.end, + host: notImplementedHost, + formatContext: formatting.getFormatContext(testFormatSettings), + preferences: emptyOptions, + }; + const rangeToExtract = refactor.extractSymbol.getRangeToExtract(sourceFile, createTextSpanFromRange(selectionRange)); + assert.isUndefined(rangeToExtract.errors, rangeToExtract.errors && "Range error: " + rangeToExtract.errors[0].messageText); + const infos = refactor.extractSymbol.getAvailableActions(context); + assert.isUndefined(find(infos, info => info.description === description.message)); + }); } diff --git a/src/testRunner/unittests/services/extract/ranges.ts b/src/testRunner/unittests/services/extract/ranges.ts index ab205288b330c..3cbc30d20b557 100644 --- a/src/testRunner/unittests/services/extract/ranges.ts +++ b/src/testRunner/unittests/services/extract/ranges.ts @@ -1,98 +1,96 @@ -namespace ts { - function testExtractRangeFailed(caption: string, s: string, expectedErrors: string[]) { - return it(caption, () => { - const t = extractTest(s); - const file = createSourceFile("a.ts", t.source, ScriptTarget.Latest, /*setParentNodes*/ true); - const selectionRange = t.ranges.get("selection"); - if (!selectionRange) { - throw new Error(`Test ${s} does not specify selection range`); - } - const result = refactor.extractSymbol.getRangeToExtract(file, createTextSpanFromRange(selectionRange)); - assert(result.targetRange === undefined, "failure expected"); - const sortedErrors = result.errors!.map(e => e.messageText).sort(); - assert.deepEqual(sortedErrors, expectedErrors.sort(), "unexpected errors"); - }); - } - - function testExtractRange(s: string): void { +import { extractTest, createSourceFile, ScriptTarget, refactor, createTextSpanFromRange, isArray, last } from "../../../ts"; +function testExtractRangeFailed(caption: string, s: string, expectedErrors: string[]) { + return it(caption, () => { const t = extractTest(s); - const f = createSourceFile("a.ts", t.source, ScriptTarget.Latest, /*setParentNodes*/ true); + const file = createSourceFile("a.ts", t.source, ScriptTarget.Latest, /*setParentNodes*/ true); const selectionRange = t.ranges.get("selection"); if (!selectionRange) { throw new Error(`Test ${s} does not specify selection range`); } - const result = refactor.extractSymbol.getRangeToExtract(f, createTextSpanFromRange(selectionRange)); - const expectedRange = t.ranges.get("extracted"); - if (expectedRange) { - let pos: number, end: number; - const targetRange = result.targetRange!; - if (isArray(targetRange.range)) { - pos = targetRange.range[0].getStart(f); - end = last(targetRange.range).getEnd(); - } - else { - pos = targetRange.range.getStart(f); - end = targetRange.range.getEnd(); - } - assert.equal(pos, expectedRange.pos, "incorrect pos of range"); - assert.equal(end, expectedRange.end, "incorrect end of range"); + const result = refactor.extractSymbol.getRangeToExtract(file, createTextSpanFromRange(selectionRange)); + assert(result.targetRange === undefined, "failure expected"); + const sortedErrors = result.errors!.map(e => e.messageText).sort(); + assert.deepEqual(sortedErrors, expectedErrors.sort(), "unexpected errors"); + }); +} +function testExtractRange(s: string): void { + const t = extractTest(s); + const f = createSourceFile("a.ts", t.source, ScriptTarget.Latest, /*setParentNodes*/ true); + const selectionRange = t.ranges.get("selection"); + if (!selectionRange) { + throw new Error(`Test ${s} does not specify selection range`); + } + const result = refactor.extractSymbol.getRangeToExtract(f, createTextSpanFromRange(selectionRange)); + const expectedRange = t.ranges.get("extracted"); + if (expectedRange) { + let pos: number, end: number; + const targetRange = result.targetRange!; + if (isArray(targetRange.range)) { + pos = targetRange.range[0].getStart(f); + end = last(targetRange.range).getEnd(); } else { - assert.isTrue(!result.targetRange, `expected range to extract to be undefined`); + pos = targetRange.range.getStart(f); + end = targetRange.range.getEnd(); } + assert.equal(pos, expectedRange.pos, "incorrect pos of range"); + assert.equal(end, expectedRange.end, "incorrect end of range"); + } + else { + assert.isTrue(!result.targetRange, `expected range to extract to be undefined`); } - - describe("unittests:: services:: extract:: extractRanges", () => { - it("get extract range from selection", () => { - testExtractRange(` +} +describe("unittests:: services:: extract:: extractRanges", () => { + it("get extract range from selection", () => { + testExtractRange(` [#| [$|var x = 1; var y = 2;|]|] `); - testExtractRange(` + testExtractRange(` [#| var x = 1; var y = 2|]; `); - testExtractRange(` + testExtractRange(` [#|var x = 1|]; var y = 2; `); - testExtractRange(` + testExtractRange(` if ([#|[#extracted|a && b && c && d|]|]) { } `); - testExtractRange(` + testExtractRange(` if [#|(a && b && c && d|]) { } `); - testExtractRange(` + testExtractRange(` if (a && b && c && d) { [#| [$|var x = 1; console.log(x);|] |] } `); - testExtractRange(` + testExtractRange(` [#| if (a) { return 100; } |] `); - testExtractRange(` + testExtractRange(` function foo() { [#| [$|if (a) { } return 100|] |] } `); - testExtractRange(` + testExtractRange(` [#| [$|l1: if (x) { break l1; }|]|] `); - testExtractRange(` + testExtractRange(` [#| [$|l2: { @@ -101,21 +99,21 @@ namespace ts { break l2; }|]|] `); - testExtractRange(` + testExtractRange(` while (true) { [#| if(x) { } break; |] } `); - testExtractRange(` + testExtractRange(` while (true) { [#| if(x) { } continue; |] } `); - testExtractRange(` + testExtractRange(` l3: { [#| @@ -124,7 +122,7 @@ namespace ts { break l3; |] } `); - testExtractRange(` + testExtractRange(` function f() { while (true) { [#| @@ -134,7 +132,7 @@ namespace ts { } } `); - testExtractRange(` + testExtractRange(` function f() { while (true) { [#| @@ -145,13 +143,13 @@ namespace ts { } } `); - testExtractRange(` + testExtractRange(` function f() { return [#| [$|1 + 2|] |]+ 3; } } `); - testExtractRange(` + testExtractRange(` function f(x: number) { [#|[$|try { x++; @@ -161,23 +159,18 @@ namespace ts { }|]|] } `); - - // Variable statements - testExtractRange(`[#|let x = [$|1|];|]`); - testExtractRange(`[#|let x = [$|1|], y;|]`); - testExtractRange(`[#|[$|let x = 1, y = 1;|]|]`); - - // Variable declarations - testExtractRange(`let [#|x = [$|1|]|];`); - testExtractRange(`let [#|x = [$|1|]|], y = 2;`); - testExtractRange(`let x = 1, [#|y = [$|2|]|];`); - - // Return statements - testExtractRange(`[#|return [$|1|];|]`); - }); - - testExtractRangeFailed("extractRangeFailed1", - ` + // Variable statements + testExtractRange(`[#|let x = [$|1|];|]`); + testExtractRange(`[#|let x = [$|1|], y;|]`); + testExtractRange(`[#|[$|let x = 1, y = 1;|]|]`); + // Variable declarations + testExtractRange(`let [#|x = [$|1|]|];`); + testExtractRange(`let [#|x = [$|1|]|], y = 2;`); + testExtractRange(`let x = 1, [#|y = [$|2|]|];`); + // Return statements + testExtractRange(`[#|return [$|1|];|]`); + }); + testExtractRangeFailed("extractRangeFailed1", ` namespace A { function f() { [#| @@ -188,11 +181,8 @@ function f() { |] } } - `, - [refactor.extractSymbol.Messages.cannotExtractRangeContainingConditionalReturnStatement.message]); - - testExtractRangeFailed("extractRangeFailed2", - ` + `, [refactor.extractSymbol.Messages.cannotExtractRangeContainingConditionalReturnStatement.message]); + testExtractRangeFailed("extractRangeFailed2", ` namespace A { function f() { while (true) { @@ -205,11 +195,8 @@ function f() { } } } - `, - [refactor.extractSymbol.Messages.cannotExtractRangeContainingConditionalBreakOrContinueStatements.message]); - - testExtractRangeFailed("extractRangeFailed3", - ` + `, [refactor.extractSymbol.Messages.cannotExtractRangeContainingConditionalBreakOrContinueStatements.message]); + testExtractRangeFailed("extractRangeFailed3", ` namespace A { function f() { while (true) { @@ -222,11 +209,8 @@ function f() { } } } - `, - [refactor.extractSymbol.Messages.cannotExtractRangeContainingConditionalBreakOrContinueStatements.message]); - - testExtractRangeFailed("extractRangeFailed4", - ` + `, [refactor.extractSymbol.Messages.cannotExtractRangeContainingConditionalBreakOrContinueStatements.message]); + testExtractRangeFailed("extractRangeFailed4", ` namespace A { function f() { l1: { @@ -239,11 +223,8 @@ function f() { } } } - `, - [refactor.extractSymbol.Messages.cannotExtractRangeContainingLabeledBreakOrContinueStatementWithTargetOutsideOfTheRange.message]); - - testExtractRangeFailed("extractRangeFailed5", - ` + `, [refactor.extractSymbol.Messages.cannotExtractRangeContainingLabeledBreakOrContinueStatementWithTargetOutsideOfTheRange.message]); + testExtractRangeFailed("extractRangeFailed5", ` namespace A { function f() { [#| @@ -258,11 +239,8 @@ function f() { function f2() { } } - `, - [refactor.extractSymbol.Messages.cannotExtractRangeContainingConditionalReturnStatement.message]); - - testExtractRangeFailed("extractRangeFailed6", - ` + `, [refactor.extractSymbol.Messages.cannotExtractRangeContainingConditionalReturnStatement.message]); + testExtractRangeFailed("extractRangeFailed6", ` namespace A { function f() { [#| @@ -277,46 +255,31 @@ function f() { function f2() { } } - `, - [refactor.extractSymbol.Messages.cannotExtractRangeContainingConditionalReturnStatement.message]); - - testExtractRangeFailed("extractRangeFailed7", - ` + `, [refactor.extractSymbol.Messages.cannotExtractRangeContainingConditionalReturnStatement.message]); + testExtractRangeFailed("extractRangeFailed7", ` function test(x: number) { while (x) { x--; [#|break;|] } } - `, - [refactor.extractSymbol.Messages.cannotExtractRangeContainingConditionalBreakOrContinueStatements.message]); - - testExtractRangeFailed("extractRangeFailed8", - ` + `, [refactor.extractSymbol.Messages.cannotExtractRangeContainingConditionalBreakOrContinueStatements.message]); + testExtractRangeFailed("extractRangeFailed8", ` function test(x: number) { switch (x) { case 1: [#|break;|] } } - `, - [refactor.extractSymbol.Messages.cannotExtractRangeContainingConditionalBreakOrContinueStatements.message]); - - testExtractRangeFailed("extractRangeFailed9", - `var x = ([#||]1 + 2);`, - [refactor.extractSymbol.Messages.cannotExtractEmpty.message]); - - testExtractRangeFailed("extractRangeFailed10", - ` + `, [refactor.extractSymbol.Messages.cannotExtractRangeContainingConditionalBreakOrContinueStatements.message]); + testExtractRangeFailed("extractRangeFailed9", `var x = ([#||]1 + 2);`, [refactor.extractSymbol.Messages.cannotExtractEmpty.message]); + testExtractRangeFailed("extractRangeFailed10", ` function f() { return 1 + [#|2 + 3|]; } } - `, - [refactor.extractSymbol.Messages.cannotExtractRange.message]); - - testExtractRangeFailed("extractRangeFailed11", - ` + `, [refactor.extractSymbol.Messages.cannotExtractRange.message]); + testExtractRangeFailed("extractRangeFailed11", ` function f(x: number) { while (true) { [#|try { @@ -327,63 +290,36 @@ switch (x) { }|] } } - `, - [refactor.extractSymbol.Messages.cannotExtractRangeContainingConditionalBreakOrContinueStatements.message]); - - testExtractRangeFailed("extractRangeFailed12", - `let [#|x|];`, - [refactor.extractSymbol.Messages.statementOrExpressionExpected.message]); - - testExtractRangeFailed("extractRangeFailed13", - `[#|return;|]`, - [refactor.extractSymbol.Messages.cannotExtractRange.message]); - - testExtractRangeFailed("extractRangeFailed14", - ` + `, [refactor.extractSymbol.Messages.cannotExtractRangeContainingConditionalBreakOrContinueStatements.message]); + testExtractRangeFailed("extractRangeFailed12", `let [#|x|];`, [refactor.extractSymbol.Messages.statementOrExpressionExpected.message]); + testExtractRangeFailed("extractRangeFailed13", `[#|return;|]`, [refactor.extractSymbol.Messages.cannotExtractRange.message]); + testExtractRangeFailed("extractRangeFailed14", ` switch(1) { case [#|1: break;|] } - `, - [refactor.extractSymbol.Messages.cannotExtractRange.message]); - - testExtractRangeFailed("extractRangeFailed15", - ` + `, [refactor.extractSymbol.Messages.cannotExtractRange.message]); + testExtractRangeFailed("extractRangeFailed15", ` switch(1) { case [#|1: break|]; } - `, - [refactor.extractSymbol.Messages.cannotExtractRange.message]); - - // Documentation only - it would be nice if the result were [$|1|] - testExtractRangeFailed("extractRangeFailed16", - ` + `, [refactor.extractSymbol.Messages.cannotExtractRange.message]); + // Documentation only - it would be nice if the result were [$|1|] + testExtractRangeFailed("extractRangeFailed16", ` switch(1) { [#|case 1|]: break; } - `, - [refactor.extractSymbol.Messages.cannotExtractRange.message]); - - // Documentation only - it would be nice if the result were [$|1|] - testExtractRangeFailed("extractRangeFailed17", - ` + `, [refactor.extractSymbol.Messages.cannotExtractRange.message]); + // Documentation only - it would be nice if the result were [$|1|] + testExtractRangeFailed("extractRangeFailed17", ` switch(1) { [#|case 1:|] break; } - `, - [refactor.extractSymbol.Messages.cannotExtractRange.message]); - - testExtractRangeFailed("extractRangeFailed18", - `[#|{ 1;|] }`, - [refactor.extractSymbol.Messages.cannotExtractRange.message]); - - testExtractRangeFailed("extractRangeFailed19", - `[#|/** @type {number} */|] const foo = 1;`, - [refactor.extractSymbol.Messages.cannotExtractJSDoc.message]); - - testExtractRangeFailed("extract-method-not-for-token-expression-statement", `[#|a|]`, [refactor.extractSymbol.Messages.cannotExtractIdentifier.message]); - }); -} + `, [refactor.extractSymbol.Messages.cannotExtractRange.message]); + testExtractRangeFailed("extractRangeFailed18", `[#|{ 1;|] }`, [refactor.extractSymbol.Messages.cannotExtractRange.message]); + testExtractRangeFailed("extractRangeFailed19", `[#|/** @type {number} */|] const foo = 1;`, [refactor.extractSymbol.Messages.cannotExtractJSDoc.message]); + testExtractRangeFailed("extract-method-not-for-token-expression-statement", `[#|a|]`, [refactor.extractSymbol.Messages.cannotExtractIdentifier.message]); +}); diff --git a/src/testRunner/unittests/services/extract/symbolWalker.ts b/src/testRunner/unittests/services/extract/symbolWalker.ts index 58d9dcb577f39..be461eb0c2181 100644 --- a/src/testRunner/unittests/services/extract/symbolWalker.ts +++ b/src/testRunner/unittests/services/extract/symbolWalker.ts @@ -1,45 +1,44 @@ -namespace ts { - describe("unittests:: services:: extract:: Symbol Walker", () => { - function test(description: string, source: string, verifier: (file: SourceFile, checker: TypeChecker) => void) { - it(description, () => { - const result = Harness.Compiler.compileFiles([{ +import { SourceFile, TypeChecker, forEach, getSourceFileOfNode } from "../../../ts"; +import { Compiler } from "../../../Harness"; +describe("unittests:: services:: extract:: Symbol Walker", () => { + function test(description: string, source: string, verifier: (file: SourceFile, checker: TypeChecker) => void) { + it(description, () => { + const result = Compiler.compileFiles([{ unitName: "main.ts", content: source }], [], {}, {}, "/"); - const file = result.program!.getSourceFile("main.ts")!; - const checker = result.program!.getTypeChecker(); - verifier(file, checker); - }); - } - - test("can be created", ` + const file = result.program!.getSourceFile("main.ts")!; + const checker = result.program!.getTypeChecker(); + verifier(file, checker); + }); + } + test("can be created", ` interface Bar { x: number; y: number; history: Bar[]; } export default function foo(a: number, b: Bar): void {}`, (file, checker) => { - let foundCount = 0; - let stdLibRefSymbols = 0; - const expectedSymbols = ["default", "a", "b", "Bar", "x", "y", "history"]; - const walker = checker.getSymbolWalker(symbol => { - const isStdLibSymbol = forEach(symbol.declarations, d => { - return getSourceFileOfNode(d).hasNoDefaultLib; - }); - if (isStdLibSymbol) { - stdLibRefSymbols++; - return false; // Don't traverse into the stdlib. That's unnecessary for this test. - } - assert.equal(symbol.name, expectedSymbols[foundCount]); - foundCount++; - return true; + let foundCount = 0; + let stdLibRefSymbols = 0; + const expectedSymbols = ["default", "a", "b", "Bar", "x", "y", "history"]; + const walker = checker.getSymbolWalker(symbol => { + const isStdLibSymbol = forEach(symbol.declarations, d => { + return getSourceFileOfNode(d).hasNoDefaultLib; }); - const symbols = checker.getExportsOfModule(file.symbol); - for (const symbol of symbols) { - walker.walkSymbol(symbol); + if (isStdLibSymbol) { + stdLibRefSymbols++; + return false; // Don't traverse into the stdlib. That's unnecessary for this test. } - assert.equal(foundCount, expectedSymbols.length); - assert.equal(stdLibRefSymbols, 1); // Expect 1 stdlib entry symbol - the implicit Array referenced by Bar.history + assert.equal(symbol.name, expectedSymbols[foundCount]); + foundCount++; + return true; }); + const symbols = checker.getExportsOfModule(file.symbol); + for (const symbol of symbols) { + walker.walkSymbol(symbol); + } + assert.equal(foundCount, expectedSymbols.length); + assert.equal(stdLibRefSymbols, 1); // Expect 1 stdlib entry symbol - the implicit Array referenced by Bar.history }); -} +}); diff --git a/src/testRunner/unittests/services/hostNewLineSupport.ts b/src/testRunner/unittests/services/hostNewLineSupport.ts index 057cb60602a4b..ecb1ffd536440 100644 --- a/src/testRunner/unittests/services/hostNewLineSupport.ts +++ b/src/testRunner/unittests/services/hostNewLineSupport.ts @@ -1,71 +1,63 @@ -namespace ts { - describe("unittests:: services:: hostNewLineSupport", () => { - function testLSWithFiles(settings: CompilerOptions, files: Harness.Compiler.TestFile[]) { - function snapFor(path: string): IScriptSnapshot | undefined { - if (path === "lib.d.ts") { - return ScriptSnapshot.fromString(""); - } - const result = find(files, f => f.unitName === path); - return result && ScriptSnapshot.fromString(result.content); +import { CompilerOptions, IScriptSnapshot, ScriptSnapshot, find, LanguageServiceHost, map, createLanguageService, NewLineKind } from "../../ts"; +import { Compiler } from "../../Harness"; +describe("unittests:: services:: hostNewLineSupport", () => { + function testLSWithFiles(settings: CompilerOptions, files: Compiler.TestFile[]) { + function snapFor(path: string): IScriptSnapshot | undefined { + if (path === "lib.d.ts") { + return ScriptSnapshot.fromString(""); } - const lshost: LanguageServiceHost = { - getCompilationSettings: () => settings, - getScriptFileNames: () => map(files, f => f.unitName), - getScriptVersion: () => "1", - getScriptSnapshot: name => snapFor(name), - getDefaultLibFileName: () => "lib.d.ts", - getCurrentDirectory: () => "", - }; - return createLanguageService(lshost); + const result = find(files, f => f.unitName === path); + return result && ScriptSnapshot.fromString(result.content); } - - function verifyNewLines(content: string, options: CompilerOptions) { - const ls = testLSWithFiles(options, [{ + const lshost: LanguageServiceHost = { + getCompilationSettings: () => settings, + getScriptFileNames: () => map(files, f => f.unitName), + getScriptVersion: () => "1", + getScriptSnapshot: name => snapFor(name), + getDefaultLibFileName: () => "lib.d.ts", + getCurrentDirectory: () => "", + }; + return createLanguageService(lshost); + } + function verifyNewLines(content: string, options: CompilerOptions) { + const ls = testLSWithFiles(options, [{ content, fileOptions: {}, unitName: "input.ts" }]); - const result = ls.getEmitOutput("input.ts"); - assert(!result.emitSkipped, "emit was skipped"); - assert(result.outputFiles.length === 1, "a number of files other than 1 was output"); - assert(result.outputFiles[0].name === "input.js", `Expected output file name input.js, but got ${result.outputFiles[0].name}`); - assert(result.outputFiles[0].text.match(options.newLine === NewLineKind.CarriageReturnLineFeed ? /\r\n/ : /[^\r]\n/), "expected to find appropriate newlines"); - assert(!result.outputFiles[0].text.match(options.newLine === NewLineKind.CarriageReturnLineFeed ? /[^\r]\n/ : /\r\n/), "expected not to find inappropriate newlines"); - } - - function verifyBothNewLines(content: string) { - verifyNewLines(content, { newLine: NewLineKind.CarriageReturnLineFeed }); - verifyNewLines(content, { newLine: NewLineKind.LineFeed }); - } - - function verifyOutliningSpanNewLines(content: string, options: CompilerOptions) { - const ls = testLSWithFiles(options, [{ + const result = ls.getEmitOutput("input.ts"); + assert(!result.emitSkipped, "emit was skipped"); + assert(result.outputFiles.length === 1, "a number of files other than 1 was output"); + assert(result.outputFiles[0].name === "input.js", `Expected output file name input.js, but got ${result.outputFiles[0].name}`); + assert(result.outputFiles[0].text.match(options.newLine === NewLineKind.CarriageReturnLineFeed ? /\r\n/ : /[^\r]\n/), "expected to find appropriate newlines"); + assert(!result.outputFiles[0].text.match(options.newLine === NewLineKind.CarriageReturnLineFeed ? /[^\r]\n/ : /\r\n/), "expected not to find inappropriate newlines"); + } + function verifyBothNewLines(content: string) { + verifyNewLines(content, { newLine: NewLineKind.CarriageReturnLineFeed }); + verifyNewLines(content, { newLine: NewLineKind.LineFeed }); + } + function verifyOutliningSpanNewLines(content: string, options: CompilerOptions) { + const ls = testLSWithFiles(options, [{ content, fileOptions: {}, unitName: "input.ts" }]); - const span = ls.getOutliningSpans("input.ts")[0]; - const textAfterSpanCollapse = content.substring(span.textSpan.start + span.textSpan.length); - assert(textAfterSpanCollapse.match(options.newLine === NewLineKind.CarriageReturnLineFeed ? /\r\n/ : /[^\r]\n/), "expected to find appropriate newlines"); - assert(!textAfterSpanCollapse.match(options.newLine === NewLineKind.CarriageReturnLineFeed ? /[^\r]\n/ : /\r\n/), "expected not to find inappropriate newlines"); - } - - it("should exist and respect provided compiler options", () => { - verifyBothNewLines(` + const span = ls.getOutliningSpans("input.ts")[0]; + const textAfterSpanCollapse = content.substring(span.textSpan.start + span.textSpan.length); + assert(textAfterSpanCollapse.match(options.newLine === NewLineKind.CarriageReturnLineFeed ? /\r\n/ : /[^\r]\n/), "expected to find appropriate newlines"); + assert(!textAfterSpanCollapse.match(options.newLine === NewLineKind.CarriageReturnLineFeed ? /[^\r]\n/ : /\r\n/), "expected not to find inappropriate newlines"); + } + it("should exist and respect provided compiler options", () => { + verifyBothNewLines(` function foo() { return 2 + 2; } `); - }); - - it("should respect CRLF line endings around outlining spans", () => { - verifyOutliningSpanNewLines("// comment not included\r\n// #region name\r\nlet x: string = \"x\";\r\n// #endregion name\r\n", - { newLine: NewLineKind.CarriageReturnLineFeed }); - }); - - it("should respect LF line endings around outlining spans", () => { - verifyOutliningSpanNewLines("// comment not included\n// #region name\nlet x: string = \"x\";\n// #endregion name\n\n", - { newLine: NewLineKind.LineFeed }); - }); }); -} + it("should respect CRLF line endings around outlining spans", () => { + verifyOutliningSpanNewLines("// comment not included\r\n// #region name\r\nlet x: string = \"x\";\r\n// #endregion name\r\n", { newLine: NewLineKind.CarriageReturnLineFeed }); + }); + it("should respect LF line endings around outlining spans", () => { + verifyOutliningSpanNewLines("// comment not included\n// #region name\nlet x: string = \"x\";\n// #endregion name\n\n", { newLine: NewLineKind.LineFeed }); + }); +}); diff --git a/src/testRunner/unittests/services/languageService.ts b/src/testRunner/unittests/services/languageService.ts index 01b1a73e9d8c3..6232279161b4b 100644 --- a/src/testRunner/unittests/services/languageService.ts +++ b/src/testRunner/unittests/services/languageService.ts @@ -1,9 +1,13 @@ -namespace ts { - const _chai: typeof import("chai") = require("chai"); - const expect: typeof _chai.expect = _chai.expect; - describe("unittests:: services:: languageService", () => { - const files: {[index: string]: string} = { - "foo.ts": `import Vue from "./vue"; +import { ScriptSnapshot, getDefaultLibFilePath, emptyArray, createMap, LanguageServiceHost, returnTrue, getDefaultCompilerOptions, Program } from "../../ts"; +import { libFile } from "../../ts.projectSystem"; +import * as ts from "../../ts"; +const _chai: typeof import("chai") = require("chai"); +const expect: typeof _chai.expect = _chai.expect; +describe("unittests:: services:: languageService", () => { + const files: { + [index: string]: string; + } = { + "foo.ts": `import Vue from "./vue"; import Component from "./vue-class-component"; import { vueTemplateHtml } from "./variables"; @@ -12,129 +16,111 @@ import { vueTemplateHtml } from "./variables"; }) class Carousel extends Vue { }`, - "variables.ts": `export const vueTemplateHtml = \`
\`;`, - "vue.d.ts": `export namespace Vue { export type Config = { template: string }; }`, - "vue-class-component.d.ts": `import Vue from "./vue"; + "variables.ts": `export const vueTemplateHtml = \`
\`;`, + "vue.d.ts": `export namespace Vue { export type Config = { template: string }; }`, + "vue-class-component.d.ts": `import Vue from "./vue"; export function Component(x: Config): any;` - }; - - function createLanguageService() { - return ts.createLanguageService({ - getCompilationSettings() { - return {}; - }, - getScriptFileNames() { - return ["foo.ts", "variables.ts", "vue.d.ts", "vue-class-component.d.ts"]; - }, - getScriptVersion(_fileName) { - return ""; - }, - getScriptSnapshot(fileName) { - if (fileName === ".ts") { - return ScriptSnapshot.fromString(""); - } - return ScriptSnapshot.fromString(files[fileName] || ""); - }, - getCurrentDirectory: () => ".", - getDefaultLibFileName(options) { - return getDefaultLibFilePath(options); - }, - }); - } - // Regression test for GH #18245 - bug in single line comment writer caused a debug assertion when attempting - // to write an alias to a module's default export was referrenced across files and had no default export - it("should be able to create a language service which can respond to deinition requests without throwing", () => { - const languageService = createLanguageService(); - const definitions = languageService.getDefinitionAtPosition("foo.ts", 160); // 160 is the latter `vueTemplateHtml` position - expect(definitions).to.exist; // eslint-disable-line @typescript-eslint/no-unused-expressions - }); - - it("getEmitOutput on language service has way to force dts emit", () => { - const languageService = createLanguageService(); - assert.deepEqual( - languageService.getEmitOutput( - "foo.ts", - /*emitOnlyDtsFiles*/ true - ), - { - emitSkipped: true, - outputFiles: emptyArray, - exportedModulesFromDeclarationEmit: undefined + }; + function createLanguageService() { + return ts.createLanguageService({ + getCompilationSettings() { + return {}; + }, + getScriptFileNames() { + return ["foo.ts", "variables.ts", "vue.d.ts", "vue-class-component.d.ts"]; + }, + getScriptVersion(_fileName) { + return ""; + }, + getScriptSnapshot(fileName) { + if (fileName === ".ts") { + return ScriptSnapshot.fromString(""); } - ); - - assert.deepEqual( - languageService.getEmitOutput( - "foo.ts", - /*emitOnlyDtsFiles*/ true, - /*forceDtsEmit*/ true - ), - { - emitSkipped: false, - outputFiles: [{ - name: "foo.d.ts", - text: "export {};\r\n", - writeByteOrderMark: false - }], - exportedModulesFromDeclarationEmit: undefined - } - ); + return ScriptSnapshot.fromString(files[fileName] || ""); + }, + getCurrentDirectory: () => ".", + getDefaultLibFileName(options) { + return getDefaultLibFilePath(options); + }, }); - - describe("detects program upto date correctly", () => { - function verifyProgramUptoDate(useProjectVersion: boolean) { - let projectVersion = "1"; - const files = createMap<{ version: string, text: string; }>(); - files.set("/project/root.ts", { version: "1", text: `import { foo } from "./other"` }); - files.set("/project/other.ts", { version: "1", text: `export function foo() { }` }); - files.set("/lib/lib.d.ts", { version: "1", text: projectSystem.libFile.content }); - const host: LanguageServiceHost = { - useCaseSensitiveFileNames: returnTrue, - getCompilationSettings: getDefaultCompilerOptions, - fileExists: path => files.has(path), - getProjectVersion: !useProjectVersion ? undefined : () => projectVersion, - getScriptFileNames: () => ["/project/root.ts"], - getScriptVersion: path => files.get(path)?.version || "", - getScriptSnapshot: path => { - const text = files.get(path)?.text; - return text ? ScriptSnapshot.fromString(text) : undefined; - }, - getCurrentDirectory: () => "/project", - getDefaultLibFileName: () => "/lib/lib.d.ts" - }; - const ls = ts.createLanguageService(host); - const program1 = ls.getProgram()!; - const program2 = ls.getProgram()!; - assert.strictEqual(program1, program2); - verifyProgramFiles(program1); - - // Change other - projectVersion = "2"; - files.set("/project/other.ts", { version: "2", text: `export function foo() { } export function bar() { }` }); - const program3 = ls.getProgram()!; - assert.notStrictEqual(program2, program3); - verifyProgramFiles(program3); - - // change root - projectVersion = "3"; - files.set("/project/root.ts", { version: "2", text: `import { foo, bar } from "./other"` }); - const program4 = ls.getProgram()!; - assert.notStrictEqual(program3, program4); - verifyProgramFiles(program4); - - function verifyProgramFiles(program: Program) { - assert.deepEqual( - program.getSourceFiles().map(f => f.fileName), - ["/lib/lib.d.ts", "/project/other.ts", "/project/root.ts"] - ); - } + } + // Regression test for GH #18245 - bug in single line comment writer caused a debug assertion when attempting + // to write an alias to a module's default export was referrenced across files and had no default export + it("should be able to create a language service which can respond to deinition requests without throwing", () => { + const languageService = createLanguageService(); + const definitions = languageService.getDefinitionAtPosition("foo.ts", 160); // 160 is the latter `vueTemplateHtml` position + expect(definitions).to.exist; // eslint-disable-line @typescript-eslint/no-unused-expressions + }); + it("getEmitOutput on language service has way to force dts emit", () => { + const languageService = createLanguageService(); + assert.deepEqual(languageService.getEmitOutput("foo.ts", + /*emitOnlyDtsFiles*/ true), { + emitSkipped: true, + outputFiles: emptyArray, + exportedModulesFromDeclarationEmit: undefined + }); + assert.deepEqual(languageService.getEmitOutput("foo.ts", + /*emitOnlyDtsFiles*/ true, + /*forceDtsEmit*/ true), { + emitSkipped: false, + outputFiles: [{ + name: "foo.d.ts", + text: "export {};\r\n", + writeByteOrderMark: false + }], + exportedModulesFromDeclarationEmit: undefined + }); + }); + describe("detects program upto date correctly", () => { + function verifyProgramUptoDate(useProjectVersion: boolean) { + let projectVersion = "1"; + const files = createMap<{ + version: string; + text: string; + }>(); + files.set("/project/root.ts", { version: "1", text: `import { foo } from "./other"` }); + files.set("/project/other.ts", { version: "1", text: `export function foo() { }` }); + files.set("/lib/lib.d.ts", { version: "1", text: libFile.content }); + const host: LanguageServiceHost = { + useCaseSensitiveFileNames: returnTrue, + getCompilationSettings: getDefaultCompilerOptions, + fileExists: path => files.has(path), + getProjectVersion: !useProjectVersion ? undefined : () => projectVersion, + getScriptFileNames: () => ["/project/root.ts"], + getScriptVersion: path => files.get(path)?.version || "", + getScriptSnapshot: path => { + const text = files.get(path)?.text; + return text ? ScriptSnapshot.fromString(text) : undefined; + }, + getCurrentDirectory: () => "/project", + getDefaultLibFileName: () => "/lib/lib.d.ts" + }; + const ls = ts.createLanguageService(host); + const program1 = ls.getProgram()!; + const program2 = ls.getProgram()!; + assert.strictEqual(program1, program2); + verifyProgramFiles(program1); + // Change other + projectVersion = "2"; + files.set("/project/other.ts", { version: "2", text: `export function foo() { } export function bar() { }` }); + const program3 = ls.getProgram()!; + assert.notStrictEqual(program2, program3); + verifyProgramFiles(program3); + // change root + projectVersion = "3"; + files.set("/project/root.ts", { version: "2", text: `import { foo, bar } from "./other"` }); + const program4 = ls.getProgram()!; + assert.notStrictEqual(program3, program4); + verifyProgramFiles(program4); + function verifyProgramFiles(program: Program) { + assert.deepEqual(program.getSourceFiles().map(f => f.fileName), ["/lib/lib.d.ts", "/project/other.ts", "/project/root.ts"]); } - it("when host implements getProjectVersion", () => { - verifyProgramUptoDate(/*useProjectVersion*/ true); - }); - it("when host does not implement getProjectVersion", () => { - verifyProgramUptoDate(/*useProjectVersion*/ false); - }); + } + it("when host implements getProjectVersion", () => { + verifyProgramUptoDate(/*useProjectVersion*/ true); + }); + it("when host does not implement getProjectVersion", () => { + verifyProgramUptoDate(/*useProjectVersion*/ false); }); }); -} +}); diff --git a/src/testRunner/unittests/services/organizeImports.ts b/src/testRunner/unittests/services/organizeImports.ts index 6627616a67dfe..14890ec0fa0d3 100644 --- a/src/testRunner/unittests/services/organizeImports.ts +++ b/src/testRunner/unittests/services/organizeImports.ts @@ -1,346 +1,231 @@ -namespace ts { - describe("unittests:: services:: organizeImports", () => { - describe("Sort imports", () => { - it("Sort - non-relative vs non-relative", () => { - assertSortsBefore( - `import y from "lib1";`, - `import x from "lib2";`); - }); - - it("Sort - relative vs relative", () => { - assertSortsBefore( - `import y from "./lib1";`, - `import x from "./lib2";`); - }); - - it("Sort - relative vs non-relative", () => { - assertSortsBefore( - `import y from "lib";`, - `import x from "./lib";`); - }); - - it("Sort - case-insensitive", () => { - assertSortsBefore( - `import y from "a";`, - `import x from "Z";`); - assertSortsBefore( - `import y from "A";`, - `import x from "z";`); - }); - - function assertSortsBefore(importString1: string, importString2: string) { - const [{moduleSpecifier: moduleSpecifier1}, {moduleSpecifier: moduleSpecifier2}] = parseImports(importString1, importString2); - assert.equal(OrganizeImports.compareModuleSpecifiers(moduleSpecifier1, moduleSpecifier2), Comparison.LessThan); - assert.equal(OrganizeImports.compareModuleSpecifiers(moduleSpecifier2, moduleSpecifier1), Comparison.GreaterThan); - } +import { OrganizeImports, Comparison, testFormatSettings, emptyOptions, TestFSWithWatch, textChanges, newLineCharacter, JsxEmit, ImportDeclaration, createSourceFile, ScriptTarget, ScriptKind, filter, isImportDeclaration, ExportDeclaration, isExportDeclaration, Node, SyntaxKind, ImportClause, NamespaceImport, NamedImports, ImportSpecifier, NamedExports, ExportSpecifier, Identifier, LiteralLikeNode } from "../../ts"; +import { Baseline } from "../../Harness"; +import { createServerHost, createProjectService } from "../../ts.projectSystem"; +describe("unittests:: services:: organizeImports", () => { + describe("Sort imports", () => { + it("Sort - non-relative vs non-relative", () => { + assertSortsBefore(`import y from "lib1";`, `import x from "lib2";`); }); - - describe("Coalesce imports", () => { - it("No imports", () => { - assert.isEmpty(OrganizeImports.coalesceImports([])); - }); - - it("Sort specifiers - case-insensitive", () => { - const sortedImports = parseImports(`import { default as M, a as n, B, y, Z as O } from "lib";`); - const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); - const expectedCoalescedImports = parseImports(`import { a as n, B, default as M, y, Z as O } from "lib";`); - assertListEqual(actualCoalescedImports, expectedCoalescedImports); - }); - - it("Combine side-effect-only imports", () => { - const sortedImports = parseImports( - `import "lib";`, - `import "lib";`); - const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); - const expectedCoalescedImports = parseImports(`import "lib";`); - assertListEqual(actualCoalescedImports, expectedCoalescedImports); - }); - - it("Combine namespace imports", () => { - const sortedImports = parseImports( - `import * as x from "lib";`, - `import * as y from "lib";`); - const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); - const expectedCoalescedImports = sortedImports; - assertListEqual(actualCoalescedImports, expectedCoalescedImports); - }); - - it("Combine default imports", () => { - const sortedImports = parseImports( - `import x from "lib";`, - `import y from "lib";`); - const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); - const expectedCoalescedImports = parseImports(`import { default as x, default as y } from "lib";`); - assertListEqual(actualCoalescedImports, expectedCoalescedImports); - }); - - it("Combine property imports", () => { - const sortedImports = parseImports( - `import { x } from "lib";`, - `import { y as z } from "lib";`); - const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); - const expectedCoalescedImports = parseImports(`import { x, y as z } from "lib";`); - assertListEqual(actualCoalescedImports, expectedCoalescedImports); - }); - - it("Combine side-effect-only import with namespace import", () => { - const sortedImports = parseImports( - `import "lib";`, - `import * as x from "lib";`); - const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); - const expectedCoalescedImports = sortedImports; - assertListEqual(actualCoalescedImports, expectedCoalescedImports); - }); - - it("Combine side-effect-only import with default import", () => { - const sortedImports = parseImports( - `import "lib";`, - `import x from "lib";`); - const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); - const expectedCoalescedImports = sortedImports; - assertListEqual(actualCoalescedImports, expectedCoalescedImports); - }); - - it("Combine side-effect-only import with property import", () => { - const sortedImports = parseImports( - `import "lib";`, - `import { x } from "lib";`); - const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); - const expectedCoalescedImports = sortedImports; - assertListEqual(actualCoalescedImports, expectedCoalescedImports); - }); - - it("Combine namespace import with default import", () => { - const sortedImports = parseImports( - `import * as x from "lib";`, - `import y from "lib";`); - const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); - const expectedCoalescedImports = parseImports( - `import y, * as x from "lib";`); - assertListEqual(actualCoalescedImports, expectedCoalescedImports); - }); - - it("Combine namespace import with property import", () => { - const sortedImports = parseImports( - `import * as x from "lib";`, - `import { y } from "lib";`); - const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); - const expectedCoalescedImports = sortedImports; - assertListEqual(actualCoalescedImports, expectedCoalescedImports); - }); - - it("Combine default import with property import", () => { - const sortedImports = parseImports( - `import x from "lib";`, - `import { y } from "lib";`); - const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); - const expectedCoalescedImports = parseImports( - `import x, { y } from "lib";`); - assertListEqual(actualCoalescedImports, expectedCoalescedImports); - }); - - it("Combine many imports", () => { - const sortedImports = parseImports( - `import "lib";`, - `import * as y from "lib";`, - `import w from "lib";`, - `import { b } from "lib";`, - `import "lib";`, - `import * as x from "lib";`, - `import z from "lib";`, - `import { a } from "lib";`); - const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); - const expectedCoalescedImports = parseImports( - `import "lib";`, - `import * as x from "lib";`, - `import * as y from "lib";`, - `import { a, b, default as w, default as z } from "lib";`); - assertListEqual(actualCoalescedImports, expectedCoalescedImports); - }); - - // This is descriptive, rather than normative - it("Combine two namespace imports with one default import", () => { - const sortedImports = parseImports( - `import * as x from "lib";`, - `import * as y from "lib";`, - `import z from "lib";`); - const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); - const expectedCoalescedImports = sortedImports; - assertListEqual(actualCoalescedImports, expectedCoalescedImports); - }); - - it("Combine type-only imports separately from other imports", () => { - const sortedImports = parseImports( - `import type { x } from "lib";`, - `import type { y } from "lib";`, - `import { z } from "lib";`); - const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); - const expectedCoalescedImports = parseImports( - `import { z } from "lib";`, - `import type { x, y } from "lib";`); - assertListEqual(actualCoalescedImports, expectedCoalescedImports); - }); - - it("Do not combine type-only default, namespace, or named imports with each other", () => { - const sortedImports = parseImports( - `import type { x } from "lib";`, - `import type * as y from "lib";`, - `import type z from "lib";`); - // Default import could be rewritten as a named import to combine with `x`, - // but seems of debatable merit. - const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); - const expectedCoalescedImports = actualCoalescedImports; - assertListEqual(actualCoalescedImports, expectedCoalescedImports); - }); + it("Sort - relative vs relative", () => { + assertSortsBefore(`import y from "./lib1";`, `import x from "./lib2";`); }); - - describe("Coalesce exports", () => { - it("No exports", () => { - assert.isEmpty(OrganizeImports.coalesceExports([])); - }); - - it("Sort specifiers - case-insensitive", () => { - const sortedExports = parseExports(`export { default as M, a as n, B, y, Z as O } from "lib";`); - const actualCoalescedExports = OrganizeImports.coalesceExports(sortedExports); - const expectedCoalescedExports = parseExports(`export { a as n, B, default as M, y, Z as O } from "lib";`); - assertListEqual(actualCoalescedExports, expectedCoalescedExports); - }); - - it("Combine namespace re-exports", () => { - const sortedExports = parseExports( - `export * from "lib";`, - `export * from "lib";`); - const actualCoalescedExports = OrganizeImports.coalesceExports(sortedExports); - const expectedCoalescedExports = parseExports(`export * from "lib";`); - assertListEqual(actualCoalescedExports, expectedCoalescedExports); - }); - - it("Combine property exports", () => { - const sortedExports = parseExports( - `export { x };`, - `export { y as z };`); - const actualCoalescedExports = OrganizeImports.coalesceExports(sortedExports); - const expectedCoalescedExports = parseExports(`export { x, y as z };`); - assertListEqual(actualCoalescedExports, expectedCoalescedExports); - }); - - it("Combine property re-exports", () => { - const sortedExports = parseExports( - `export { x } from "lib";`, - `export { y as z } from "lib";`); - const actualCoalescedExports = OrganizeImports.coalesceExports(sortedExports); - const expectedCoalescedExports = parseExports(`export { x, y as z } from "lib";`); - assertListEqual(actualCoalescedExports, expectedCoalescedExports); - }); - - it("Combine namespace re-export with property re-export", () => { - const sortedExports = parseExports( - `export * from "lib";`, - `export { y } from "lib";`); - const actualCoalescedExports = OrganizeImports.coalesceExports(sortedExports); - const expectedCoalescedExports = sortedExports; - assertListEqual(actualCoalescedExports, expectedCoalescedExports); - }); - - it("Combine many exports", () => { - const sortedExports = parseExports( - `export { x };`, - `export { y as w, z as default };`, - `export { w as q };`); - const actualCoalescedExports = OrganizeImports.coalesceExports(sortedExports); - const expectedCoalescedExports = parseExports( - `export { w as q, x, y as w, z as default };`); - assertListEqual(actualCoalescedExports, expectedCoalescedExports); - }); - - it("Combine many re-exports", () => { - const sortedExports = parseExports( - `export { x as a, y } from "lib";`, - `export * from "lib";`, - `export { z as b } from "lib";`); - const actualCoalescedExports = OrganizeImports.coalesceExports(sortedExports); - const expectedCoalescedExports = parseExports( - `export * from "lib";`, - `export { x as a, y, z as b } from "lib";`); - assertListEqual(actualCoalescedExports, expectedCoalescedExports); - }); - - it("Keep type-only exports separate", () => { - const sortedExports = parseExports( - `export { x };`, - `export type { y };`); - const actualCoalescedExports = OrganizeImports.coalesceExports(sortedExports); - const expectedCoalescedExports = sortedExports; - assertListEqual(actualCoalescedExports, expectedCoalescedExports); - }); - - it("Combine type-only exports", () => { - const sortedExports = parseExports( - `export type { x };`, - `export type { y };`); - const actualCoalescedExports = OrganizeImports.coalesceExports(sortedExports); - const expectedCoalescedExports = parseExports( - `export type { x, y };`); - assertListEqual(actualCoalescedExports, expectedCoalescedExports); - }); + it("Sort - relative vs non-relative", () => { + assertSortsBefore(`import y from "lib";`, `import x from "./lib";`); }); - - describe("Baselines", () => { - - const libFile = { - path: "/lib.ts", - content: ` + it("Sort - case-insensitive", () => { + assertSortsBefore(`import y from "a";`, `import x from "Z";`); + assertSortsBefore(`import y from "A";`, `import x from "z";`); + }); + function assertSortsBefore(importString1: string, importString2: string) { + const [{ moduleSpecifier: moduleSpecifier1 }, { moduleSpecifier: moduleSpecifier2 }] = parseImports(importString1, importString2); + assert.equal(OrganizeImports.compareModuleSpecifiers(moduleSpecifier1, moduleSpecifier2), Comparison.LessThan); + assert.equal(OrganizeImports.compareModuleSpecifiers(moduleSpecifier2, moduleSpecifier1), Comparison.GreaterThan); + } + }); + describe("Coalesce imports", () => { + it("No imports", () => { + assert.isEmpty(OrganizeImports.coalesceImports([])); + }); + it("Sort specifiers - case-insensitive", () => { + const sortedImports = parseImports(`import { default as M, a as n, B, y, Z as O } from "lib";`); + const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); + const expectedCoalescedImports = parseImports(`import { a as n, B, default as M, y, Z as O } from "lib";`); + assertListEqual(actualCoalescedImports, expectedCoalescedImports); + }); + it("Combine side-effect-only imports", () => { + const sortedImports = parseImports(`import "lib";`, `import "lib";`); + const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); + const expectedCoalescedImports = parseImports(`import "lib";`); + assertListEqual(actualCoalescedImports, expectedCoalescedImports); + }); + it("Combine namespace imports", () => { + const sortedImports = parseImports(`import * as x from "lib";`, `import * as y from "lib";`); + const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); + const expectedCoalescedImports = sortedImports; + assertListEqual(actualCoalescedImports, expectedCoalescedImports); + }); + it("Combine default imports", () => { + const sortedImports = parseImports(`import x from "lib";`, `import y from "lib";`); + const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); + const expectedCoalescedImports = parseImports(`import { default as x, default as y } from "lib";`); + assertListEqual(actualCoalescedImports, expectedCoalescedImports); + }); + it("Combine property imports", () => { + const sortedImports = parseImports(`import { x } from "lib";`, `import { y as z } from "lib";`); + const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); + const expectedCoalescedImports = parseImports(`import { x, y as z } from "lib";`); + assertListEqual(actualCoalescedImports, expectedCoalescedImports); + }); + it("Combine side-effect-only import with namespace import", () => { + const sortedImports = parseImports(`import "lib";`, `import * as x from "lib";`); + const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); + const expectedCoalescedImports = sortedImports; + assertListEqual(actualCoalescedImports, expectedCoalescedImports); + }); + it("Combine side-effect-only import with default import", () => { + const sortedImports = parseImports(`import "lib";`, `import x from "lib";`); + const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); + const expectedCoalescedImports = sortedImports; + assertListEqual(actualCoalescedImports, expectedCoalescedImports); + }); + it("Combine side-effect-only import with property import", () => { + const sortedImports = parseImports(`import "lib";`, `import { x } from "lib";`); + const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); + const expectedCoalescedImports = sortedImports; + assertListEqual(actualCoalescedImports, expectedCoalescedImports); + }); + it("Combine namespace import with default import", () => { + const sortedImports = parseImports(`import * as x from "lib";`, `import y from "lib";`); + const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); + const expectedCoalescedImports = parseImports(`import y, * as x from "lib";`); + assertListEqual(actualCoalescedImports, expectedCoalescedImports); + }); + it("Combine namespace import with property import", () => { + const sortedImports = parseImports(`import * as x from "lib";`, `import { y } from "lib";`); + const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); + const expectedCoalescedImports = sortedImports; + assertListEqual(actualCoalescedImports, expectedCoalescedImports); + }); + it("Combine default import with property import", () => { + const sortedImports = parseImports(`import x from "lib";`, `import { y } from "lib";`); + const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); + const expectedCoalescedImports = parseImports(`import x, { y } from "lib";`); + assertListEqual(actualCoalescedImports, expectedCoalescedImports); + }); + it("Combine many imports", () => { + const sortedImports = parseImports(`import "lib";`, `import * as y from "lib";`, `import w from "lib";`, `import { b } from "lib";`, `import "lib";`, `import * as x from "lib";`, `import z from "lib";`, `import { a } from "lib";`); + const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); + const expectedCoalescedImports = parseImports(`import "lib";`, `import * as x from "lib";`, `import * as y from "lib";`, `import { a, b, default as w, default as z } from "lib";`); + assertListEqual(actualCoalescedImports, expectedCoalescedImports); + }); + // This is descriptive, rather than normative + it("Combine two namespace imports with one default import", () => { + const sortedImports = parseImports(`import * as x from "lib";`, `import * as y from "lib";`, `import z from "lib";`); + const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); + const expectedCoalescedImports = sortedImports; + assertListEqual(actualCoalescedImports, expectedCoalescedImports); + }); + it("Combine type-only imports separately from other imports", () => { + const sortedImports = parseImports(`import type { x } from "lib";`, `import type { y } from "lib";`, `import { z } from "lib";`); + const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); + const expectedCoalescedImports = parseImports(`import { z } from "lib";`, `import type { x, y } from "lib";`); + assertListEqual(actualCoalescedImports, expectedCoalescedImports); + }); + it("Do not combine type-only default, namespace, or named imports with each other", () => { + const sortedImports = parseImports(`import type { x } from "lib";`, `import type * as y from "lib";`, `import type z from "lib";`); + // Default import could be rewritten as a named import to combine with `x`, + // but seems of debatable merit. + const actualCoalescedImports = OrganizeImports.coalesceImports(sortedImports); + const expectedCoalescedImports = actualCoalescedImports; + assertListEqual(actualCoalescedImports, expectedCoalescedImports); + }); + }); + describe("Coalesce exports", () => { + it("No exports", () => { + assert.isEmpty(OrganizeImports.coalesceExports([])); + }); + it("Sort specifiers - case-insensitive", () => { + const sortedExports = parseExports(`export { default as M, a as n, B, y, Z as O } from "lib";`); + const actualCoalescedExports = OrganizeImports.coalesceExports(sortedExports); + const expectedCoalescedExports = parseExports(`export { a as n, B, default as M, y, Z as O } from "lib";`); + assertListEqual(actualCoalescedExports, expectedCoalescedExports); + }); + it("Combine namespace re-exports", () => { + const sortedExports = parseExports(`export * from "lib";`, `export * from "lib";`); + const actualCoalescedExports = OrganizeImports.coalesceExports(sortedExports); + const expectedCoalescedExports = parseExports(`export * from "lib";`); + assertListEqual(actualCoalescedExports, expectedCoalescedExports); + }); + it("Combine property exports", () => { + const sortedExports = parseExports(`export { x };`, `export { y as z };`); + const actualCoalescedExports = OrganizeImports.coalesceExports(sortedExports); + const expectedCoalescedExports = parseExports(`export { x, y as z };`); + assertListEqual(actualCoalescedExports, expectedCoalescedExports); + }); + it("Combine property re-exports", () => { + const sortedExports = parseExports(`export { x } from "lib";`, `export { y as z } from "lib";`); + const actualCoalescedExports = OrganizeImports.coalesceExports(sortedExports); + const expectedCoalescedExports = parseExports(`export { x, y as z } from "lib";`); + assertListEqual(actualCoalescedExports, expectedCoalescedExports); + }); + it("Combine namespace re-export with property re-export", () => { + const sortedExports = parseExports(`export * from "lib";`, `export { y } from "lib";`); + const actualCoalescedExports = OrganizeImports.coalesceExports(sortedExports); + const expectedCoalescedExports = sortedExports; + assertListEqual(actualCoalescedExports, expectedCoalescedExports); + }); + it("Combine many exports", () => { + const sortedExports = parseExports(`export { x };`, `export { y as w, z as default };`, `export { w as q };`); + const actualCoalescedExports = OrganizeImports.coalesceExports(sortedExports); + const expectedCoalescedExports = parseExports(`export { w as q, x, y as w, z as default };`); + assertListEqual(actualCoalescedExports, expectedCoalescedExports); + }); + it("Combine many re-exports", () => { + const sortedExports = parseExports(`export { x as a, y } from "lib";`, `export * from "lib";`, `export { z as b } from "lib";`); + const actualCoalescedExports = OrganizeImports.coalesceExports(sortedExports); + const expectedCoalescedExports = parseExports(`export * from "lib";`, `export { x as a, y, z as b } from "lib";`); + assertListEqual(actualCoalescedExports, expectedCoalescedExports); + }); + it("Keep type-only exports separate", () => { + const sortedExports = parseExports(`export { x };`, `export type { y };`); + const actualCoalescedExports = OrganizeImports.coalesceExports(sortedExports); + const expectedCoalescedExports = sortedExports; + assertListEqual(actualCoalescedExports, expectedCoalescedExports); + }); + it("Combine type-only exports", () => { + const sortedExports = parseExports(`export type { x };`, `export type { y };`); + const actualCoalescedExports = OrganizeImports.coalesceExports(sortedExports); + const expectedCoalescedExports = parseExports(`export type { x, y };`); + assertListEqual(actualCoalescedExports, expectedCoalescedExports); + }); + }); + describe("Baselines", () => { + const libFile = { + path: "/lib.ts", + content: ` export function F1(); export default function F2(); `, - }; - - const reactLibFile = { - path: "/react.ts", - content: ` + }; + const reactLibFile = { + path: "/react.ts", + content: ` export const React = { createElement: (_type, _props, _children) => {}, }; export const Other = 1; `, + }; + // Don't bother to actually emit a baseline for this. + it("NoImports", () => { + const testFile = { + path: "/a.ts", + content: "function F() { }", }; - - // Don't bother to actually emit a baseline for this. - it("NoImports", () => { - const testFile = { - path: "/a.ts", - content: "function F() { }", - }; - const languageService = makeLanguageService(testFile); - const changes = languageService.organizeImports({ type: "file", fileName: testFile.path }, testFormatSettings, emptyOptions); - assert.isEmpty(changes); - }); - - it("doesn't crash on shorthand ambient module", () => { - const testFile = { - path: "/a.ts", - content: "declare module '*';", - }; - const languageService = makeLanguageService(testFile); - const changes = languageService.organizeImports({ type: "file", fileName: testFile.path }, testFormatSettings, emptyOptions); - assert.isEmpty(changes); - }); - - testOrganizeImports("Renamed_used", - { - path: "/test.ts", - content: ` + const languageService = makeLanguageService(testFile); + const changes = languageService.organizeImports({ type: "file", fileName: testFile.path }, testFormatSettings, emptyOptions); + assert.isEmpty(changes); + }); + it("doesn't crash on shorthand ambient module", () => { + const testFile = { + path: "/a.ts", + content: "declare module '*';", + }; + const languageService = makeLanguageService(testFile); + const changes = languageService.organizeImports({ type: "file", fileName: testFile.path }, testFormatSettings, emptyOptions); + assert.isEmpty(changes); + }); + testOrganizeImports("Renamed_used", { + path: "/test.ts", + content: ` import { F1 as EffOne, F2 as EffTwo } from "lib"; EffOne(); `, - }, - libFile); - - testOrganizeImports("Simple", - { - path: "/test.ts", - content: ` + }, libFile); + testOrganizeImports("Simple", { + path: "/test.ts", + content: ` import { F1, F2 } from "lib"; import * as NS from "lib"; import D from "lib"; @@ -350,46 +235,34 @@ D(); F1(); F2(); `, - }, - libFile); - - testOrganizeImports("Unused_Some", - { - path: "/test.ts", - content: ` + }, libFile); + testOrganizeImports("Unused_Some", { + path: "/test.ts", + content: ` import { F1, F2 } from "lib"; import * as NS from "lib"; import D from "lib"; D(); `, - }, - libFile); - - testOrganizeImports("Unused_All", - { - path: "/test.ts", - content: ` + }, libFile); + testOrganizeImports("Unused_All", { + path: "/test.ts", + content: ` import { F1, F2 } from "lib"; import * as NS from "lib"; import D from "lib"; `, - }, - libFile); - - testOrganizeImports("Unused_Empty", - { - path: "/test.ts", - content: ` + }, libFile); + testOrganizeImports("Unused_Empty", { + path: "/test.ts", + content: ` import { } from "lib"; `, - }, - libFile); - - testOrganizeImports("Unused_false_positive_module_augmentation", - { - path: "/test.d.ts", - content: ` + }, libFile); + testOrganizeImports("Unused_false_positive_module_augmentation", { + path: "/test.d.ts", + content: ` import foo from 'foo'; import { Caseless } from 'caseless'; @@ -399,12 +272,10 @@ declare module 'caseless' { test(name: KeyType): boolean; } }` - }); - - testOrganizeImports("Unused_preserve_imports_for_module_augmentation_in_non_declaration_file", - { - path: "/test.ts", - content: ` + }); + testOrganizeImports("Unused_preserve_imports_for_module_augmentation_in_non_declaration_file", { + path: "/test.ts", + content: ` import foo from 'foo'; import { Caseless } from 'caseless'; @@ -414,30 +285,24 @@ declare module 'caseless' { test(name: KeyType): boolean; } }` - }); - - testOrganizeImports("Unused_false_positive_shorthand_assignment", - { - path: "/test.ts", - content: ` + }); + testOrganizeImports("Unused_false_positive_shorthand_assignment", { + path: "/test.ts", + content: ` import { x } from "a"; const o = { x }; ` - }); - - testOrganizeImports("Unused_false_positive_export_shorthand", - { - path: "/test.ts", - content: ` + }); + testOrganizeImports("Unused_false_positive_export_shorthand", { + path: "/test.ts", + content: ` import { x } from "a"; export { x }; ` - }); - - testOrganizeImports("MoveToTop", - { - path: "/test.ts", - content: ` + }); + testOrganizeImports("MoveToTop", { + path: "/test.ts", + content: ` import { F1, F2 } from "lib"; F1(); F2(); @@ -446,14 +311,11 @@ NS.F1(); import D from "lib"; D(); `, - }, - libFile); - - /* eslint-disable no-template-curly-in-string */ - testOrganizeImports("MoveToTop_Invalid", - { - path: "/test.ts", - content: ` + }, libFile); + /* eslint-disable no-template-curly-in-string */ + testOrganizeImports("MoveToTop_Invalid", { + path: "/test.ts", + content: ` import { F1, F2 } from "lib"; F1(); F2(); @@ -464,106 +326,77 @@ import a from ${"`${'lib'}`"}; import D from "lib"; D(); `, - }, - libFile); - /* eslint-enable no-template-curly-in-string */ - - testOrganizeImports("TypeOnly", - { - path: "/test.ts", - content: ` + }, libFile); + /* eslint-enable no-template-curly-in-string */ + testOrganizeImports("TypeOnly", { + path: "/test.ts", + content: ` import { X } from "lib"; import type Y from "lib"; import { Z } from "lib"; import type { A, B } from "lib"; export { A, B, X, Y, Z };` - }); - - testOrganizeImports("CoalesceMultipleModules", - { - path: "/test.ts", - content: ` + }); + testOrganizeImports("CoalesceMultipleModules", { + path: "/test.ts", + content: ` import { d } from "lib1"; import { b } from "lib1"; import { c } from "lib2"; import { a } from "lib2"; a + b + c + d; `, - }, - { path: "/lib1.ts", content: "export const b = 1, d = 2;" }, - { path: "/lib2.ts", content: "export const a = 3, c = 4;" }); - - testOrganizeImports("CoalesceTrivia", - { - path: "/test.ts", - content: ` + }, { path: "/lib1.ts", content: "export const b = 1, d = 2;" }, { path: "/lib2.ts", content: "export const a = 3, c = 4;" }); + testOrganizeImports("CoalesceTrivia", { + path: "/test.ts", + content: ` /*A*/import /*B*/ { /*C*/ F2 /*D*/ } /*E*/ from /*F*/ "lib" /*G*/;/*H*/ //I /*J*/import /*K*/ { /*L*/ F1 /*M*/ } /*N*/ from /*O*/ "lib" /*P*/;/*Q*/ //R F1(); F2(); `, - }, - libFile); - - testOrganizeImports("SortTrivia", - { - path: "/test.ts", - content: ` + }, libFile); + testOrganizeImports("SortTrivia", { + path: "/test.ts", + content: ` /*A*/import /*B*/ "lib2" /*C*/;/*D*/ //E /*F*/import /*G*/ "lib1" /*H*/;/*I*/ //J `, - }, - { path: "/lib1.ts", content: "" }, - { path: "/lib2.ts", content: "" }); - - testOrganizeImports("UnusedTrivia1", - { - path: "/test.ts", - content: ` + }, { path: "/lib1.ts", content: "" }, { path: "/lib2.ts", content: "" }); + testOrganizeImports("UnusedTrivia1", { + path: "/test.ts", + content: ` /*A*/import /*B*/ { /*C*/ F1 /*D*/ } /*E*/ from /*F*/ "lib" /*G*/;/*H*/ //I `, - }, - libFile); - - testOrganizeImports("UnusedTrivia2", - { - path: "/test.ts", - content: ` + }, libFile); + testOrganizeImports("UnusedTrivia2", { + path: "/test.ts", + content: ` /*A*/import /*B*/ { /*C*/ F1 /*D*/, /*E*/ F2 /*F*/ } /*G*/ from /*H*/ "lib" /*I*/;/*J*/ //K F1(); `, - }, - libFile); - - testOrganizeImports("UnusedHeaderComment", - { - path: "/test.ts", - content: ` + }, libFile); + testOrganizeImports("UnusedHeaderComment", { + path: "/test.ts", + content: ` // Header import { F1 } from "lib"; `, - }, - libFile); - - testOrganizeImports("SortHeaderComment", - { - path: "/test.ts", - content: ` + }, libFile); + testOrganizeImports("SortHeaderComment", { + path: "/test.ts", + content: ` // Header import "lib2"; import "lib1"; `, - }, - { path: "/lib1.ts", content: "" }, - { path: "/lib2.ts", content: "" }); - - testOrganizeImports("AmbientModule", - { - path: "/test.ts", - content: ` + }, { path: "/lib1.ts", content: "" }, { path: "/lib2.ts", content: "" }); + testOrganizeImports("AmbientModule", { + path: "/test.ts", + content: ` declare module "mod" { import { F1 } from "lib"; import * as NS from "lib"; @@ -572,13 +405,10 @@ declare module "mod" { function F(f1: {} = F1, f2: {} = F2) {} } `, - }, - libFile); - - testOrganizeImports("TopLevelAndAmbientModule", - { - path: "/test.ts", - content: ` + }, libFile); + testOrganizeImports("TopLevelAndAmbientModule", { + path: "/test.ts", + content: ` import D from "lib"; declare module "mod" { @@ -594,140 +424,104 @@ import "lib"; D(); `, - }, - libFile); - - testOrganizeImports("JsxFactoryUsedJsx", - { - path: "/test.jsx", - content: ` + }, libFile); + testOrganizeImports("JsxFactoryUsedJsx", { + path: "/test.jsx", + content: ` import { React, Other } from "react";
; `, - }, - reactLibFile); - - testOrganizeImports("JsxFactoryUsedJs", - { - path: "/test.js", - content: ` + }, reactLibFile); + testOrganizeImports("JsxFactoryUsedJs", { + path: "/test.js", + content: ` import { React, Other } from "react";
; `, - }, - reactLibFile); - - testOrganizeImports("JsxFactoryUsedTsx", - { - path: "/test.tsx", - content: ` + }, reactLibFile); + testOrganizeImports("JsxFactoryUsedTsx", { + path: "/test.tsx", + content: ` import { React, Other } from "react";
; `, - }, - reactLibFile); - - // TS files are not JSX contexts, so the parser does not treat - // `
` as a JSX element. - testOrganizeImports("JsxFactoryUsedTs", - { - path: "/test.ts", - content: ` + }, reactLibFile); + // TS files are not JSX contexts, so the parser does not treat + // `
` as a JSX element. + testOrganizeImports("JsxFactoryUsedTs", { + path: "/test.ts", + content: ` import { React, Other } from "react";
; `, - }, - reactLibFile); - - testOrganizeImports("JsxFactoryUnusedJsx", - { - path: "/test.jsx", - content: ` + }, reactLibFile); + testOrganizeImports("JsxFactoryUnusedJsx", { + path: "/test.jsx", + content: ` import { React, Other } from "react"; `, - }, - reactLibFile); - - // Note: Since the file extension does not end with "x", the jsx compiler option - // will not be enabled. The import should be retained regardless. - testOrganizeImports("JsxFactoryUnusedJs", - { - path: "/test.js", - content: ` + }, reactLibFile); + // Note: Since the file extension does not end with "x", the jsx compiler option + // will not be enabled. The import should be retained regardless. + testOrganizeImports("JsxFactoryUnusedJs", { + path: "/test.js", + content: ` import { React, Other } from "react"; `, - }, - reactLibFile); - - testOrganizeImports("JsxFactoryUnusedTsx", - { - path: "/test.tsx", - content: ` + }, reactLibFile); + testOrganizeImports("JsxFactoryUnusedTsx", { + path: "/test.tsx", + content: ` import { React, Other } from "react"; `, - }, - reactLibFile); - - testOrganizeImports("JsxFactoryUnusedTs", - { - path: "/test.ts", - content: ` + }, reactLibFile); + testOrganizeImports("JsxFactoryUnusedTs", { + path: "/test.ts", + content: ` import { React, Other } from "react"; `, - }, - reactLibFile); - - testOrganizeImports("JsxPragmaTsx", - { - path: "/test.tsx", - content: `/** @jsx jsx */ + }, reactLibFile); + testOrganizeImports("JsxPragmaTsx", { + path: "/test.tsx", + content: `/** @jsx jsx */ import { Global, jsx } from '@emotion/core'; import * as React from 'react'; export const App: React.FunctionComponent = _ =>

Hello!

`, - }, - { - path: "/@emotion/core/index.d.ts", - content: `import { createElement } from 'react' + }, { + path: "/@emotion/core/index.d.ts", + content: `import { createElement } from 'react' export const jsx: typeof createElement; export function Global(props: any): ReactElement;` - }, - { - path: reactLibFile.path, - content: `${reactLibFile.content} + }, { + path: reactLibFile.path, + content: `${reactLibFile.content} export namespace React { interface FunctionComponent { } } ` - } - ); - - describe("Exports", () => { - - testOrganizeExports("MoveToTop", - { - path: "/test.ts", - content: ` + }); + describe("Exports", () => { + testOrganizeExports("MoveToTop", { + path: "/test.ts", + content: ` export { F1, F2 } from "lib"; 1; export * from "lib"; 2; `, - }, - libFile); - - /* eslint-disable no-template-curly-in-string */ - testOrganizeExports("MoveToTop_Invalid", - { - path: "/test.ts", - content: ` + }, libFile); + /* eslint-disable no-template-curly-in-string */ + testOrganizeExports("MoveToTop_Invalid", { + path: "/test.ts", + content: ` export { F1, F2 } from "lib"; 1; export * from "lib"; @@ -737,14 +531,11 @@ export { a } from ${"`${'lib'}`"}; export { D } from "lib"; 3; `, - }, - libFile); - /* eslint-enable no-template-curly-in-string */ - - testOrganizeExports("MoveToTop_WithImportsFirst", - { - path: "/test.ts", - content: ` + }, libFile); + /* eslint-enable no-template-curly-in-string */ + testOrganizeExports("MoveToTop_WithImportsFirst", { + path: "/test.ts", + content: ` import { F1, F2 } from "lib"; 1; export { F1, F2 } from "lib"; @@ -755,13 +546,10 @@ export * from "lib"; 4; F1(); F2(); NS.F1(); `, - }, - libFile); - - testOrganizeExports("MoveToTop_WithExportsFirst", - { - path: "/test.ts", - content: ` + }, libFile); + testOrganizeExports("MoveToTop_WithExportsFirst", { + path: "/test.ts", + content: ` export { F1, F2 } from "lib"; 1; import { F1, F2 } from "lib"; @@ -772,72 +560,51 @@ import * as NS from "lib"; 4; F1(); F2(); NS.F1(); `, - }, - libFile); - - testOrganizeExports("CoalesceMultipleModules", - { - path: "/test.ts", - content: ` + }, libFile); + testOrganizeExports("CoalesceMultipleModules", { + path: "/test.ts", + content: ` export { d } from "lib1"; export { b } from "lib1"; export { c } from "lib2"; export { a } from "lib2"; `, - }, - { path: "/lib1.ts", content: "export const b = 1, d = 2;" }, - { path: "/lib2.ts", content: "export const a = 3, c = 4;" }); - - testOrganizeExports("CoalesceTrivia", - { - path: "/test.ts", - content: ` + }, { path: "/lib1.ts", content: "export const b = 1, d = 2;" }, { path: "/lib2.ts", content: "export const a = 3, c = 4;" }); + testOrganizeExports("CoalesceTrivia", { + path: "/test.ts", + content: ` /*A*/export /*B*/ { /*C*/ F2 /*D*/ } /*E*/ from /*F*/ "lib" /*G*/;/*H*/ //I /*J*/export /*K*/ { /*L*/ F1 /*M*/ } /*N*/ from /*O*/ "lib" /*P*/;/*Q*/ //R `, - }, - libFile); - - testOrganizeExports("SortTrivia", - { - path: "/test.ts", - content: ` + }, libFile); + testOrganizeExports("SortTrivia", { + path: "/test.ts", + content: ` /*A*/export /*B*/ * /*C*/ from /*D*/ "lib2" /*E*/;/*F*/ //G /*H*/export /*I*/ * /*J*/ from /*K*/ "lib1" /*L*/;/*M*/ //N `, - }, - { path: "/lib1.ts", content: "" }, - { path: "/lib2.ts", content: "" }); - - testOrganizeExports("SortHeaderComment", - { - path: "/test.ts", - content: ` + }, { path: "/lib1.ts", content: "" }, { path: "/lib2.ts", content: "" }); + testOrganizeExports("SortHeaderComment", { + path: "/test.ts", + content: ` // Header export * from "lib2"; export * from "lib1"; `, - }, - { path: "/lib1.ts", content: "" }, - { path: "/lib2.ts", content: "" }); - - testOrganizeExports("AmbientModule", - { - path: "/test.ts", - content: ` + }, { path: "/lib1.ts", content: "" }, { path: "/lib2.ts", content: "" }); + testOrganizeExports("AmbientModule", { + path: "/test.ts", + content: ` declare module "mod" { export { F1 } from "lib"; export * from "lib"; export { F2 } from "lib"; } `, - }, - libFile); - - testOrganizeExports("TopLevelAndAmbientModule", - { - path: "/test.ts", - content: ` + }, libFile); + testOrganizeExports("TopLevelAndAmbientModule", { + path: "/test.ts", + content: ` export { D } from "lib"; declare module "mod" { @@ -849,143 +616,129 @@ declare module "mod" { export { E } from "lib"; export * from "lib"; `, - }, - libFile); - }); - - function testOrganizeExports(testName: string, testFile: TestFSWithWatch.File, ...otherFiles: TestFSWithWatch.File[]) { - testOrganizeImports(`${testName}.exports`, testFile, ...otherFiles); - } - - function testOrganizeImports(testName: string, testFile: TestFSWithWatch.File, ...otherFiles: TestFSWithWatch.File[]) { - it(testName, () => runBaseline(`organizeImports/${testName}.ts`, testFile, ...otherFiles)); - } - - function runBaseline(baselinePath: string, testFile: TestFSWithWatch.File, ...otherFiles: TestFSWithWatch.File[]) { - const { path: testPath, content: testContent } = testFile; - const languageService = makeLanguageService(testFile, ...otherFiles); - const changes = languageService.organizeImports({ type: "file", fileName: testPath }, testFormatSettings, emptyOptions); - assert.equal(changes.length, 1); - assert.equal(changes[0].fileName, testPath); - - const newText = textChanges.applyChanges(testContent, changes[0].textChanges); - Harness.Baseline.runBaseline(baselinePath, [ - "// ==ORIGINAL==", - testContent, - "// ==ORGANIZED==", - newText, - ].join(newLineCharacter)); - } - - function makeLanguageService(...files: TestFSWithWatch.File[]) { - const host = projectSystem.createServerHost(files); - const projectService = projectSystem.createProjectService(host, { useSingleInferredProject: true }); - projectService.setCompilerOptionsForInferredProjects({ jsx: files.some(f => f.path.endsWith("x")) ? JsxEmit.React : JsxEmit.None }); - files.forEach(f => projectService.openClientFile(f.path)); - return projectService.inferredProjects[0].getLanguageService(); - } + }, libFile); }); - - function parseImports(...importStrings: string[]): readonly ImportDeclaration[] { - const sourceFile = createSourceFile("a.ts", importStrings.join("\n"), ScriptTarget.ES2015, /*setParentNodes*/ true, ScriptKind.TS); - const imports = filter(sourceFile.statements, isImportDeclaration); - assert.equal(imports.length, importStrings.length); - return imports; + function testOrganizeExports(testName: string, testFile: TestFSWithWatch.File, ...otherFiles: TestFSWithWatch.File[]) { + testOrganizeImports(`${testName}.exports`, testFile, ...otherFiles); } - - function parseExports(...exportStrings: string[]): readonly ExportDeclaration[] { - const sourceFile = createSourceFile("a.ts", exportStrings.join("\n"), ScriptTarget.ES2015, /*setParentNodes*/ true, ScriptKind.TS); - const exports = filter(sourceFile.statements, isExportDeclaration); - assert.equal(exports.length, exportStrings.length); - return exports; + function testOrganizeImports(testName: string, testFile: TestFSWithWatch.File, ...otherFiles: TestFSWithWatch.File[]) { + it(testName, () => runBaseline(`organizeImports/${testName}.ts`, testFile, ...otherFiles)); } - - function assertEqual(node1?: Node, node2?: Node) { - if (node1 === undefined) { - assert.isUndefined(node2); - return; - } - else if (node2 === undefined) { - assert.isUndefined(node1); // Guaranteed to fail - return; - } - - assert.equal(node1.kind, node2.kind); - - switch (node1.kind) { - case SyntaxKind.ImportDeclaration: - const decl1 = node1 as ImportDeclaration; - const decl2 = node2 as ImportDeclaration; - assertEqual(decl1.importClause, decl2.importClause); - assertEqual(decl1.moduleSpecifier, decl2.moduleSpecifier); - break; - case SyntaxKind.ImportClause: - const clause1 = node1 as ImportClause; - const clause2 = node2 as ImportClause; - assertEqual(clause1.name, clause2.name); - assertEqual(clause1.namedBindings, clause2.namedBindings); - break; - case SyntaxKind.NamespaceImport: - const nsi1 = node1 as NamespaceImport; - const nsi2 = node2 as NamespaceImport; - assertEqual(nsi1.name, nsi2.name); - break; - case SyntaxKind.NamedImports: - const ni1 = node1 as NamedImports; - const ni2 = node2 as NamedImports; - assertListEqual(ni1.elements, ni2.elements); - break; - case SyntaxKind.ImportSpecifier: - const is1 = node1 as ImportSpecifier; - const is2 = node2 as ImportSpecifier; - assertEqual(is1.name, is2.name); - assertEqual(is1.propertyName, is2.propertyName); - break; - case SyntaxKind.ExportDeclaration: - const ed1 = node1 as ExportDeclaration; - const ed2 = node2 as ExportDeclaration; - assertEqual(ed1.exportClause, ed2.exportClause); - assertEqual(ed1.moduleSpecifier, ed2.moduleSpecifier); - break; - case SyntaxKind.NamedExports: - const ne1 = node1 as NamedExports; - const ne2 = node2 as NamedExports; - assertListEqual(ne1.elements, ne2.elements); - break; - case SyntaxKind.ExportSpecifier: - const es1 = node1 as ExportSpecifier; - const es2 = node2 as ExportSpecifier; - assertEqual(es1.name, es2.name); - assertEqual(es1.propertyName, es2.propertyName); - break; - case SyntaxKind.Identifier: - const id1 = node1 as Identifier; - const id2 = node2 as Identifier; - assert.equal(id1.text, id2.text); - break; - case SyntaxKind.StringLiteral: - case SyntaxKind.NoSubstitutionTemplateLiteral: - const sl1 = node1 as LiteralLikeNode; - const sl2 = node2 as LiteralLikeNode; - assert.equal(sl1.text, sl2.text); - break; - default: - assert.equal(node1.getText(), node2.getText()); - break; - } + function runBaseline(baselinePath: string, testFile: TestFSWithWatch.File, ...otherFiles: TestFSWithWatch.File[]) { + const { path: testPath, content: testContent } = testFile; + const languageService = makeLanguageService(testFile, ...otherFiles); + const changes = languageService.organizeImports({ type: "file", fileName: testPath }, testFormatSettings, emptyOptions); + assert.equal(changes.length, 1); + assert.equal(changes[0].fileName, testPath); + const newText = textChanges.applyChanges(testContent, changes[0].textChanges); + Baseline.runBaseline(baselinePath, [ + "// ==ORIGINAL==", + testContent, + "// ==ORGANIZED==", + newText, + ].join(newLineCharacter)); } - - function assertListEqual(list1: readonly Node[], list2: readonly Node[]) { - if (list1 === undefined || list2 === undefined) { - assert.isUndefined(list1); - assert.isUndefined(list2); - return; - } - - assert.equal(list1.length, list2.length); - for (let i = 0; i < list1.length; i++) { - assertEqual(list1[i], list2[i]); - } + function makeLanguageService(...files: TestFSWithWatch.File[]) { + const host = createServerHost(files); + const projectService = createProjectService(host, { useSingleInferredProject: true }); + projectService.setCompilerOptionsForInferredProjects({ jsx: files.some(f => f.path.endsWith("x")) ? JsxEmit.React : JsxEmit.None }); + files.forEach(f => projectService.openClientFile(f.path)); + return projectService.inferredProjects[0].getLanguageService(); } }); -} + function parseImports(...importStrings: string[]): readonly ImportDeclaration[] { + const sourceFile = createSourceFile("a.ts", importStrings.join("\n"), ScriptTarget.ES2015, /*setParentNodes*/ true, ScriptKind.TS); + const imports = filter(sourceFile.statements, isImportDeclaration); + assert.equal(imports.length, importStrings.length); + return imports; + } + function parseExports(...exportStrings: string[]): readonly ExportDeclaration[] { + const sourceFile = createSourceFile("a.ts", exportStrings.join("\n"), ScriptTarget.ES2015, /*setParentNodes*/ true, ScriptKind.TS); + const exports = filter(sourceFile.statements, isExportDeclaration); + assert.equal(exports.length, exportStrings.length); + return exports; + } + function assertEqual(node1?: Node, node2?: Node) { + if (node1 === undefined) { + assert.isUndefined(node2); + return; + } + else if (node2 === undefined) { + assert.isUndefined(node1); // Guaranteed to fail + return; + } + assert.equal(node1.kind, node2.kind); + switch (node1.kind) { + case SyntaxKind.ImportDeclaration: + const decl1 = (node1 as ImportDeclaration); + const decl2 = (node2 as ImportDeclaration); + assertEqual(decl1.importClause, decl2.importClause); + assertEqual(decl1.moduleSpecifier, decl2.moduleSpecifier); + break; + case SyntaxKind.ImportClause: + const clause1 = (node1 as ImportClause); + const clause2 = (node2 as ImportClause); + assertEqual(clause1.name, clause2.name); + assertEqual(clause1.namedBindings, clause2.namedBindings); + break; + case SyntaxKind.NamespaceImport: + const nsi1 = (node1 as NamespaceImport); + const nsi2 = (node2 as NamespaceImport); + assertEqual(nsi1.name, nsi2.name); + break; + case SyntaxKind.NamedImports: + const ni1 = (node1 as NamedImports); + const ni2 = (node2 as NamedImports); + assertListEqual(ni1.elements, ni2.elements); + break; + case SyntaxKind.ImportSpecifier: + const is1 = (node1 as ImportSpecifier); + const is2 = (node2 as ImportSpecifier); + assertEqual(is1.name, is2.name); + assertEqual(is1.propertyName, is2.propertyName); + break; + case SyntaxKind.ExportDeclaration: + const ed1 = (node1 as ExportDeclaration); + const ed2 = (node2 as ExportDeclaration); + assertEqual(ed1.exportClause, ed2.exportClause); + assertEqual(ed1.moduleSpecifier, ed2.moduleSpecifier); + break; + case SyntaxKind.NamedExports: + const ne1 = (node1 as NamedExports); + const ne2 = (node2 as NamedExports); + assertListEqual(ne1.elements, ne2.elements); + break; + case SyntaxKind.ExportSpecifier: + const es1 = (node1 as ExportSpecifier); + const es2 = (node2 as ExportSpecifier); + assertEqual(es1.name, es2.name); + assertEqual(es1.propertyName, es2.propertyName); + break; + case SyntaxKind.Identifier: + const id1 = (node1 as Identifier); + const id2 = (node2 as Identifier); + assert.equal(id1.text, id2.text); + break; + case SyntaxKind.StringLiteral: + case SyntaxKind.NoSubstitutionTemplateLiteral: + const sl1 = (node1 as LiteralLikeNode); + const sl2 = (node2 as LiteralLikeNode); + assert.equal(sl1.text, sl2.text); + break; + default: + assert.equal(node1.getText(), node2.getText()); + break; + } + } + function assertListEqual(list1: readonly Node[], list2: readonly Node[]) { + if (list1 === undefined || list2 === undefined) { + assert.isUndefined(list1); + assert.isUndefined(list2); + return; + } + assert.equal(list1.length, list2.length); + for (let i = 0; i < list1.length; i++) { + assertEqual(list1[i], list2[i]); + } + } +}); diff --git a/src/testRunner/unittests/services/patternMatcher.ts b/src/testRunner/unittests/services/patternMatcher.ts index 9f3cbf4b6d7c6..ccc5eaad91c29 100644 --- a/src/testRunner/unittests/services/patternMatcher.ts +++ b/src/testRunner/unittests/services/patternMatcher.ts @@ -1,351 +1,269 @@ +import { PatternMatchKind, PatternMatch, createPatternMatcher, TextSpan } from "../../ts"; +import * as ts from "../../ts"; describe("unittests:: services:: PatternMatcher", () => { describe("BreakIntoCharacterSpans", () => { it("EmptyIdentifier", () => { verifyBreakIntoCharacterSpans(""); }); - it("SimpleIdentifier", () => { verifyBreakIntoCharacterSpans("foo", "foo"); }); - it("PrefixUnderscoredIdentifier", () => { verifyBreakIntoCharacterSpans("_foo", "_", "foo"); }); - it("UnderscoredIdentifier", () => { verifyBreakIntoCharacterSpans("f_oo", "f", "_", "oo"); }); - it("PostfixUnderscoredIdentifier", () => { verifyBreakIntoCharacterSpans("foo_", "foo", "_"); }); - it("PrefixUnderscoredIdentifierWithCapital", () => { verifyBreakIntoCharacterSpans("_Foo", "_", "Foo"); }); - it("MUnderscorePrefixed", () => { verifyBreakIntoCharacterSpans("m_foo", "m", "_", "foo"); }); - it("CamelCaseIdentifier", () => { verifyBreakIntoCharacterSpans("FogBar", "Fog", "Bar"); }); - it("MixedCaseIdentifier", () => { verifyBreakIntoCharacterSpans("fogBar", "fog", "Bar"); }); - it("TwoCharacterCapitalIdentifier", () => { verifyBreakIntoCharacterSpans("UIElement", "U", "I", "Element"); }); - it("NumberSuffixedIdentifier", () => { verifyBreakIntoCharacterSpans("Foo42", "Foo", "42"); }); - it("NumberContainingIdentifier", () => { verifyBreakIntoCharacterSpans("Fog42Bar", "Fog", "42", "Bar"); }); - it("NumberPrefixedIdentifier", () => { verifyBreakIntoCharacterSpans("42Bar", "42", "Bar"); }); }); - describe("BreakIntoWordSpans", () => { it("VarbatimIdentifier", () => { verifyBreakIntoWordSpans("@int:", "int"); }); - it("AllCapsConstant", () => { verifyBreakIntoWordSpans("C_STYLE_CONSTANT", "C", "_", "STYLE", "_", "CONSTANT"); }); - it("SingleLetterPrefix1", () => { verifyBreakIntoWordSpans("UInteger", "U", "Integer"); }); - it("SingleLetterPrefix2", () => { verifyBreakIntoWordSpans("IDisposable", "I", "Disposable"); }); - it("TwoCharacterCapitalIdentifier", () => { verifyBreakIntoWordSpans("UIElement", "UI", "Element"); }); - it("XDocument", () => { verifyBreakIntoWordSpans("XDocument", "X", "Document"); }); - it("XMLDocument1", () => { verifyBreakIntoWordSpans("XMLDocument", "XML", "Document"); }); - it("XMLDocument2", () => { verifyBreakIntoWordSpans("XmlDocument", "Xml", "Document"); }); - it("TwoUppercaseCharacters", () => { verifyBreakIntoWordSpans("SimpleUIElement", "Simple", "UI", "Element"); }); }); - describe("SingleWordPattern", () => { it("PreferCaseSensitiveExact", () => { - assertSegmentMatch("Foo", "Foo", { kind: ts.PatternMatchKind.exact, isCaseSensitive: true }); + assertSegmentMatch("Foo", "Foo", { kind: PatternMatchKind.exact, isCaseSensitive: true }); }); - it("PreferCaseSensitiveExactInsensitive", () => { - assertSegmentMatch("foo", "Foo", { kind: ts.PatternMatchKind.exact, isCaseSensitive: false }); + assertSegmentMatch("foo", "Foo", { kind: PatternMatchKind.exact, isCaseSensitive: false }); }); - it("PreferCaseSensitivePrefix", () => { - assertSegmentMatch("Foo", "Fo", { kind: ts.PatternMatchKind.prefix, isCaseSensitive: true }); + assertSegmentMatch("Foo", "Fo", { kind: PatternMatchKind.prefix, isCaseSensitive: true }); }); - it("PreferCaseSensitivePrefixCaseInsensitive", () => { - assertSegmentMatch("Foo", "fo", { kind: ts.PatternMatchKind.prefix, isCaseSensitive: false }); + assertSegmentMatch("Foo", "fo", { kind: PatternMatchKind.prefix, isCaseSensitive: false }); }); - it("PreferCaseSensitiveCamelCaseMatchSimple", () => { - assertSegmentMatch("FogBar", "FB", { kind: ts.PatternMatchKind.camelCase, isCaseSensitive: true }); + assertSegmentMatch("FogBar", "FB", { kind: PatternMatchKind.camelCase, isCaseSensitive: true }); }); - it("PreferCaseSensitiveCamelCaseMatchPartialPattern", () => { - assertSegmentMatch("FogBar", "FoB", { kind: ts.PatternMatchKind.camelCase, isCaseSensitive: true }); + assertSegmentMatch("FogBar", "FoB", { kind: PatternMatchKind.camelCase, isCaseSensitive: true }); }); - it("PreferCaseSensitiveCamelCaseMatchToLongPattern1", () => { assertSegmentMatch("FogBar", "FBB", undefined); }); - it("PreferCaseSensitiveCamelCaseMatchToLongPattern2", () => { assertSegmentMatch("FogBar", "FoooB", undefined); }); - it("CamelCaseMatchPartiallyUnmatched", () => { assertSegmentMatch("FogBarBaz", "FZ", undefined); }); - it("CamelCaseMatchCompletelyUnmatched", () => { assertSegmentMatch("FogBarBaz", "ZZ", undefined); }); - it("TwoUppercaseCharacters", () => { - assertSegmentMatch("SimpleUIElement", "SiUI", { kind: ts.PatternMatchKind.camelCase, isCaseSensitive: true }); + assertSegmentMatch("SimpleUIElement", "SiUI", { kind: PatternMatchKind.camelCase, isCaseSensitive: true }); }); - it("PreferCaseSensitiveLowercasePattern", () => { - assertSegmentMatch("FogBar", "b", { kind: ts.PatternMatchKind.substring, isCaseSensitive: false }); + assertSegmentMatch("FogBar", "b", { kind: PatternMatchKind.substring, isCaseSensitive: false }); }); - it("PreferCaseSensitiveLowercasePattern2", () => { - assertSegmentMatch("FogBar", "fB", { kind: ts.PatternMatchKind.camelCase, isCaseSensitive: false }); + assertSegmentMatch("FogBar", "fB", { kind: PatternMatchKind.camelCase, isCaseSensitive: false }); }); - it("PreferCaseSensitiveTryUnderscoredName", () => { - assertSegmentMatch("_fogBar", "_fB", { kind: ts.PatternMatchKind.camelCase, isCaseSensitive: true }); + assertSegmentMatch("_fogBar", "_fB", { kind: PatternMatchKind.camelCase, isCaseSensitive: true }); }); - it("PreferCaseSensitiveTryUnderscoredName2", () => { - assertSegmentMatch("_fogBar", "fB", { kind: ts.PatternMatchKind.camelCase, isCaseSensitive: true }); + assertSegmentMatch("_fogBar", "fB", { kind: PatternMatchKind.camelCase, isCaseSensitive: true }); }); - it("PreferCaseSensitiveTryUnderscoredNameInsensitive", () => { - assertSegmentMatch("_FogBar", "_fB", { kind: ts.PatternMatchKind.camelCase, isCaseSensitive: false }); + assertSegmentMatch("_FogBar", "_fB", { kind: PatternMatchKind.camelCase, isCaseSensitive: false }); }); - it("PreferCaseSensitiveMiddleUnderscore", () => { - assertSegmentMatch("Fog_Bar", "FB", { kind: ts.PatternMatchKind.camelCase, isCaseSensitive: true }); + assertSegmentMatch("Fog_Bar", "FB", { kind: PatternMatchKind.camelCase, isCaseSensitive: true }); }); - it("PreferCaseSensitiveMiddleUnderscore2", () => { - assertSegmentMatch("Fog_Bar", "F_B", { kind: ts.PatternMatchKind.camelCase, isCaseSensitive: true }); + assertSegmentMatch("Fog_Bar", "F_B", { kind: PatternMatchKind.camelCase, isCaseSensitive: true }); }); - it("PreferCaseSensitiveMiddleUnderscore3", () => { assertSegmentMatch("Fog_Bar", "F__B", undefined); }); - it("PreferCaseSensitiveMiddleUnderscore4", () => { - assertSegmentMatch("Fog_Bar", "f_B", { kind: ts.PatternMatchKind.camelCase, isCaseSensitive: false }); + assertSegmentMatch("Fog_Bar", "f_B", { kind: PatternMatchKind.camelCase, isCaseSensitive: false }); }); - it("PreferCaseSensitiveMiddleUnderscore5", () => { - assertSegmentMatch("Fog_Bar", "F_b", { kind: ts.PatternMatchKind.camelCase, isCaseSensitive: false }); + assertSegmentMatch("Fog_Bar", "F_b", { kind: PatternMatchKind.camelCase, isCaseSensitive: false }); }); - it("AllLowerPattern1", () => { - assertSegmentMatch("FogBarChangedEventArgs", "changedeventargs", { kind: ts.PatternMatchKind.substring, isCaseSensitive: false }); + assertSegmentMatch("FogBarChangedEventArgs", "changedeventargs", { kind: PatternMatchKind.substring, isCaseSensitive: false }); }); - it("AllLowerPattern2", () => { assertSegmentMatch("FogBarChangedEventArgs", "changedeventarrrgh", undefined); }); - it("AllLowerPattern3", () => { - assertSegmentMatch("ABCDEFGH", "bcd", { kind: ts.PatternMatchKind.substring, isCaseSensitive: false }); + assertSegmentMatch("ABCDEFGH", "bcd", { kind: PatternMatchKind.substring, isCaseSensitive: false }); }); - it("AllLowerPattern4", () => { assertSegmentMatch("AbcdefghijEfgHij", "efghij", undefined); }); }); - describe("MultiWordPattern", () => { it("ExactWithLowercase", () => { - assertSegmentMatch("AddMetadataReference", "addmetadatareference", { kind: ts.PatternMatchKind.exact, isCaseSensitive: false }); + assertSegmentMatch("AddMetadataReference", "addmetadatareference", { kind: PatternMatchKind.exact, isCaseSensitive: false }); }); - it("SingleLowercasedSearchWord1", () => { - assertSegmentMatch("AddMetadataReference", "add", { kind: ts.PatternMatchKind.prefix, isCaseSensitive: false }); + assertSegmentMatch("AddMetadataReference", "add", { kind: PatternMatchKind.prefix, isCaseSensitive: false }); }); - it("SingleLowercasedSearchWord2", () => { - assertSegmentMatch("AddMetadataReference", "metadata", { kind: ts.PatternMatchKind.substring, isCaseSensitive: false }); + assertSegmentMatch("AddMetadataReference", "metadata", { kind: PatternMatchKind.substring, isCaseSensitive: false }); }); - it("SingleUppercaseSearchWord1", () => { - assertSegmentMatch("AddMetadataReference", "Add", { kind: ts.PatternMatchKind.prefix, isCaseSensitive: true }); + assertSegmentMatch("AddMetadataReference", "Add", { kind: PatternMatchKind.prefix, isCaseSensitive: true }); }); - it("SingleUppercaseSearchWord2", () => { - assertSegmentMatch("AddMetadataReference", "Metadata", { kind: ts.PatternMatchKind.substring, isCaseSensitive: true }); + assertSegmentMatch("AddMetadataReference", "Metadata", { kind: PatternMatchKind.substring, isCaseSensitive: true }); }); - it("SingleUppercaseSearchLetter1", () => { - assertSegmentMatch("AddMetadataReference", "A", { kind: ts.PatternMatchKind.prefix, isCaseSensitive: true }); + assertSegmentMatch("AddMetadataReference", "A", { kind: PatternMatchKind.prefix, isCaseSensitive: true }); }); - it("SingleUppercaseSearchLetter2", () => { - assertSegmentMatch("AddMetadataReference", "M", { kind: ts.PatternMatchKind.substring, isCaseSensitive: true }); + assertSegmentMatch("AddMetadataReference", "M", { kind: PatternMatchKind.substring, isCaseSensitive: true }); }); - it("TwoLowercaseWords0", () => { - assertSegmentMatch("AddMetadataReference", "add metadata", { kind: ts.PatternMatchKind.prefix, isCaseSensitive: false }); + assertSegmentMatch("AddMetadataReference", "add metadata", { kind: PatternMatchKind.prefix, isCaseSensitive: false }); }); - it("TwoLowercaseWords1", () => { - assertSegmentMatch("AddMetadataReference", "A M", { kind: ts.PatternMatchKind.prefix, isCaseSensitive: true }); + assertSegmentMatch("AddMetadataReference", "A M", { kind: PatternMatchKind.prefix, isCaseSensitive: true }); }); - it("TwoLowercaseWords2", () => { - assertSegmentMatch("AddMetadataReference", "AM", { kind: ts.PatternMatchKind.camelCase, isCaseSensitive: true }); + assertSegmentMatch("AddMetadataReference", "AM", { kind: PatternMatchKind.camelCase, isCaseSensitive: true }); }); - it("TwoLowercaseWords3", () => { - assertSegmentMatch("AddMetadataReference", "ref Metadata", { kind: ts.PatternMatchKind.substring, isCaseSensitive: true }); + assertSegmentMatch("AddMetadataReference", "ref Metadata", { kind: PatternMatchKind.substring, isCaseSensitive: true }); }); - it("TwoLowercaseWords4", () => { - assertSegmentMatch("AddMetadataReference", "ref M", { kind: ts.PatternMatchKind.substring, isCaseSensitive: true }); + assertSegmentMatch("AddMetadataReference", "ref M", { kind: PatternMatchKind.substring, isCaseSensitive: true }); }); - it("MixedCamelCase", () => { - assertSegmentMatch("AddMetadataReference", "AMRe", { kind: ts.PatternMatchKind.camelCase, isCaseSensitive: true }); + assertSegmentMatch("AddMetadataReference", "AMRe", { kind: PatternMatchKind.camelCase, isCaseSensitive: true }); }); - it("BlankPattern", () => { assertInvalidPattern(""); }); - it("WhitespaceOnlyPattern", () => { assertInvalidPattern(" "); }); - it("EachWordSeparately1", () => { - assertSegmentMatch("AddMetadataReference", "add Meta", { kind: ts.PatternMatchKind.prefix, isCaseSensitive: false }); + assertSegmentMatch("AddMetadataReference", "add Meta", { kind: PatternMatchKind.prefix, isCaseSensitive: false }); }); - it("EachWordSeparately2", () => { - assertSegmentMatch("AddMetadataReference", "Add meta", { kind: ts.PatternMatchKind.prefix, isCaseSensitive: true }); + assertSegmentMatch("AddMetadataReference", "Add meta", { kind: PatternMatchKind.prefix, isCaseSensitive: true }); }); - it("EachWordSeparately3", () => { - assertSegmentMatch("AddMetadataReference", "Add Meta", { kind: ts.PatternMatchKind.prefix, isCaseSensitive: true }); + assertSegmentMatch("AddMetadataReference", "Add Meta", { kind: PatternMatchKind.prefix, isCaseSensitive: true }); }); - it("MixedCasing", () => { assertSegmentMatch("AddMetadataReference", "mEta", undefined); }); - it("MixedCasing2", () => { assertSegmentMatch("AddMetadataReference", "Data", undefined); }); - it("AsteriskSplit", () => { - assertSegmentMatch("GetKeyWord", "K*W", { kind: ts.PatternMatchKind.substring, isCaseSensitive: true }); + assertSegmentMatch("GetKeyWord", "K*W", { kind: PatternMatchKind.substring, isCaseSensitive: true }); }); - it("LowercaseSubstring1", () => { assertSegmentMatch("Operator", "a", undefined); }); - it("LowercaseSubstring2", () => { - assertSegmentMatch("FooAttribute", "a", { kind: ts.PatternMatchKind.substring, isCaseSensitive: false }); + assertSegmentMatch("FooAttribute", "a", { kind: PatternMatchKind.substring, isCaseSensitive: false }); }); }); - describe("DottedPattern", () => { it("DottedPattern1", () => { - assertFullMatch("Foo.Bar.Baz", "Quux", "B.Q", { kind: ts.PatternMatchKind.prefix, isCaseSensitive: true }); + assertFullMatch("Foo.Bar.Baz", "Quux", "B.Q", { kind: PatternMatchKind.prefix, isCaseSensitive: true }); }); - it("DottedPattern2", () => { assertFullMatch("Foo.Bar.Baz", "Quux", "C.Q", undefined); }); - it("DottedPattern3", () => { - assertFullMatch("Foo.Bar.Baz", "Quux", "B.B.Q", { kind: ts.PatternMatchKind.prefix, isCaseSensitive: true }); + assertFullMatch("Foo.Bar.Baz", "Quux", "B.B.Q", { kind: PatternMatchKind.prefix, isCaseSensitive: true }); }); - it("DottedPattern4", () => { - assertFullMatch("Foo.Bar.Baz", "Quux", "Baz.Quux", { kind: ts.PatternMatchKind.exact, isCaseSensitive: true }); + assertFullMatch("Foo.Bar.Baz", "Quux", "Baz.Quux", { kind: PatternMatchKind.exact, isCaseSensitive: true }); }); - it("DottedPattern5", () => { - assertFullMatch("Foo.Bar.Baz", "Quux", "F.B.B.Quux", { kind: ts.PatternMatchKind.prefix, isCaseSensitive: true }); + assertFullMatch("Foo.Bar.Baz", "Quux", "F.B.B.Quux", { kind: PatternMatchKind.prefix, isCaseSensitive: true }); }); - it("DottedPattern6", () => { assertFullMatch("Foo.Bar.Baz", "Quux", "F.F.B.B.Quux", undefined); }); - it("DottedPattern7", () => { - assertSegmentMatch("UIElement", "UIElement", { kind: ts.PatternMatchKind.exact, isCaseSensitive: true }); + assertSegmentMatch("UIElement", "UIElement", { kind: PatternMatchKind.exact, isCaseSensitive: true }); assertSegmentMatch("GetKeyword", "UIElement", undefined); }); }); - - function assertSegmentMatch(candidate: string, pattern: string, expected: ts.PatternMatch | undefined): void { - assert.deepEqual(ts.createPatternMatcher(pattern)!.getMatchForLastSegmentOfPattern(candidate), expected); + function assertSegmentMatch(candidate: string, pattern: string, expected: PatternMatch | undefined): void { + assert.deepEqual(createPatternMatcher(pattern)!.getMatchForLastSegmentOfPattern(candidate), expected); } - function assertInvalidPattern(pattern: string) { - assert.equal(ts.createPatternMatcher(pattern), undefined); + assert.equal(createPatternMatcher(pattern), undefined); } - - function assertFullMatch(dottedContainer: string, candidate: string, pattern: string, expected: ts.PatternMatch | undefined): void { - assert.deepEqual(ts.createPatternMatcher(pattern)!.getFullMatch(dottedContainer.split("."), candidate), expected); + function assertFullMatch(dottedContainer: string, candidate: string, pattern: string, expected: PatternMatch | undefined): void { + assert.deepEqual(createPatternMatcher(pattern)!.getFullMatch(dottedContainer.split("."), candidate), expected); } - - function spanListToSubstrings(identifier: string, spans: ts.TextSpan[]) { + function spanListToSubstrings(identifier: string, spans: TextSpan[]) { return spans.map(s => identifier.substr(s.start, s.length)); } - function breakIntoCharacterSpans(identifier: string) { return spanListToSubstrings(identifier, ts.breakIntoCharacterSpans(identifier)); } - function breakIntoWordSpans(identifier: string) { return spanListToSubstrings(identifier, ts.breakIntoWordSpans(identifier)); } - function verifyBreakIntoCharacterSpans(original: string, ...parts: string[]): void { assert.deepEqual(parts, breakIntoCharacterSpans(original)); } - function verifyBreakIntoWordSpans(original: string, ...parts: string[]): void { assert.deepEqual(parts, breakIntoWordSpans(original)); } diff --git a/src/testRunner/unittests/services/preProcessFile.ts b/src/testRunner/unittests/services/preProcessFile.ts index 766c7d7078beb..f090d19163b8d 100644 --- a/src/testRunner/unittests/services/preProcessFile.ts +++ b/src/testRunner/unittests/services/preProcessFile.ts @@ -1,18 +1,15 @@ +import { PreProcessedFileInfo, preProcessFile, FileReference } from "../../ts"; describe("unittests:: services:: PreProcessFile:", () => { - function test(sourceText: string, readImportFile: boolean, detectJavaScriptImports: boolean, expectedPreProcess: ts.PreProcessedFileInfo): void { - const resultPreProcess = ts.preProcessFile(sourceText, readImportFile, detectJavaScriptImports); - + function test(sourceText: string, readImportFile: boolean, detectJavaScriptImports: boolean, expectedPreProcess: PreProcessedFileInfo): void { + const resultPreProcess = preProcessFile(sourceText, readImportFile, detectJavaScriptImports); assert.equal(resultPreProcess.isLibFile, expectedPreProcess.isLibFile, "Pre-processed file has different value for isLibFile. Expected: " + expectedPreProcess.isLibFile + ". Actual: " + resultPreProcess.isLibFile); - checkFileReferenceList("Imported files", expectedPreProcess.importedFiles, resultPreProcess.importedFiles); checkFileReferenceList("Referenced files", expectedPreProcess.referencedFiles, resultPreProcess.referencedFiles); checkFileReferenceList("Type reference directives", expectedPreProcess.typeReferenceDirectives, resultPreProcess.typeReferenceDirectives); checkFileReferenceList("Lib reference directives", expectedPreProcess.libReferenceDirectives, resultPreProcess.libReferenceDirectives); - assert.deepEqual(resultPreProcess.ambientExternalModules, expectedPreProcess.ambientExternalModules); } - - function checkFileReferenceList(kind: string, expected: ts.FileReference[], actual: ts.FileReference[]) { + function checkFileReferenceList(kind: string, expected: FileReference[], actual: FileReference[]) { if (expected === actual) { return; } @@ -20,7 +17,6 @@ describe("unittests:: services:: PreProcessFile:", () => { assert.isTrue(false, `Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`); } assert.equal(actual.length, expected.length, `[${kind}] Actual array's length does not match expected length. Expected files: ${JSON.stringify(expected)}, actual files: ${JSON.stringify(actual)}`); - for (let i = 0; i < expected.length; i++) { const actualReference = actual[i]; const expectedReference = expected[i]; @@ -29,136 +25,117 @@ describe("unittests:: services:: PreProcessFile:", () => { assert.equal(actualReference.end, expectedReference.end, `[${kind}] actual file end pos does not match expected. Expected: "${expectedReference.end}". Actual: "${actualReference.end}".`); } } - describe("Test preProcessFiles,", () => { it("Correctly return referenced files from triple slash", () => { - test("///" + "\n" + "///" + "\n" + "///" + "\n" + "///", - /*readImportFile*/ true, - /*detectJavaScriptImports*/ false, - { - referencedFiles: [{ fileName: "refFile1.ts", pos: 22, end: 33 }, { fileName: "refFile2.ts", pos: 59, end: 70 }, - { fileName: "refFile3.ts", pos: 94, end: 105 }, { fileName: "..\\refFile4d.ts", pos: 131, end: 146 }], - importedFiles: [], - typeReferenceDirectives: [], - libReferenceDirectives: [], - ambientExternalModules: undefined, - isLibFile: false - }); + test("///" + "\n" + "///" + "\n" + "///" + "\n" + "///", + /*readImportFile*/ true, + /*detectJavaScriptImports*/ false, { + referencedFiles: [{ fileName: "refFile1.ts", pos: 22, end: 33 }, { fileName: "refFile2.ts", pos: 59, end: 70 }, + { fileName: "refFile3.ts", pos: 94, end: 105 }, { fileName: "..\\refFile4d.ts", pos: 131, end: 146 }], + importedFiles: ([]), + typeReferenceDirectives: [], + libReferenceDirectives: [], + ambientExternalModules: undefined, + isLibFile: false + }); }); - it("Do not return reference path because of invalid triple-slash syntax", () => { - test("///" + "\n" + "///" + "\n" + "///" + "\n" + "///", - /*readImportFile*/ true, - /*detectJavaScriptImports*/ false, - { - referencedFiles: [], - importedFiles: [], - typeReferenceDirectives: [], - libReferenceDirectives: [], - ambientExternalModules: undefined, - isLibFile: false - }); + test("///" + "\n" + "///" + "\n" + "///" + "\n" + "///", + /*readImportFile*/ true, + /*detectJavaScriptImports*/ false, { + referencedFiles: ([]), + importedFiles: ([]), + typeReferenceDirectives: [], + libReferenceDirectives: [], + ambientExternalModules: undefined, + isLibFile: false + }); }); - it("Do not return reference path of non-imports", () => { - test("Quill.import('delta');", - /*readImportFile*/ true, - /*detectJavaScriptImports*/ false, - { - referencedFiles: [], - importedFiles: [], - typeReferenceDirectives: [], - libReferenceDirectives: [], - ambientExternalModules: undefined, - isLibFile: false - }); + test("Quill.import('delta');", + /*readImportFile*/ true, + /*detectJavaScriptImports*/ false, { + referencedFiles: ([]), + importedFiles: ([]), + typeReferenceDirectives: [], + libReferenceDirectives: [], + ambientExternalModules: undefined, + isLibFile: false + }); }); - it("Do not return reference path of nested non-imports", () => { - test("a.b.import('c');", - /*readImportFile*/ true, - /*detectJavaScriptImports*/ false, - { - referencedFiles: [], - importedFiles: [], - typeReferenceDirectives: [], - libReferenceDirectives: [], - ambientExternalModules: undefined, - isLibFile: false - }); + test("a.b.import('c');", + /*readImportFile*/ true, + /*detectJavaScriptImports*/ false, { + referencedFiles: ([]), + importedFiles: ([]), + typeReferenceDirectives: [], + libReferenceDirectives: [], + ambientExternalModules: undefined, + isLibFile: false + }); }); - it("Correctly return imported files", () => { - test("import i1 = require(\"r1.ts\"); import i2 =require(\"r2.ts\"); import i3= require(\"r3.ts\"); import i4=require(\"r4.ts\"); import i5 = require (\"r5.ts\");", - /*readImportFile*/ true, - /*detectJavaScriptImports*/ false, - { - referencedFiles: [], - typeReferenceDirectives: [], - libReferenceDirectives: [], - importedFiles: [{ fileName: "r1.ts", pos: 20, end: 25 }, { fileName: "r2.ts", pos: 49, end: 54 }, { fileName: "r3.ts", pos: 78, end: 83 }, - { fileName: "r4.ts", pos: 106, end: 111 }, { fileName: "r5.ts", pos: 138, end: 143 }], - ambientExternalModules: undefined, - isLibFile: false - }); + test("import i1 = require(\"r1.ts\"); import i2 =require(\"r2.ts\"); import i3= require(\"r3.ts\"); import i4=require(\"r4.ts\"); import i5 = require (\"r5.ts\");", + /*readImportFile*/ true, + /*detectJavaScriptImports*/ false, { + referencedFiles: ([]), + typeReferenceDirectives: [], + libReferenceDirectives: [], + importedFiles: [{ fileName: "r1.ts", pos: 20, end: 25 }, { fileName: "r2.ts", pos: 49, end: 54 }, { fileName: "r3.ts", pos: 78, end: 83 }, + { fileName: "r4.ts", pos: 106, end: 111 }, { fileName: "r5.ts", pos: 138, end: 143 }], + ambientExternalModules: undefined, + isLibFile: false + }); }); - it("Do not return imported files if readImportFiles argument is false", () => { - test("import i1 = require(\"r1.ts\"); import i2 =require(\"r2.ts\"); import i3= require(\"r3.ts\"); import i4=require(\"r4.ts\"); import i5 = require (\"r5.ts\");", - /*readImportFile*/ false, - /*detectJavaScriptImports*/ false, - { - referencedFiles: [], - typeReferenceDirectives: [], - libReferenceDirectives: [], - importedFiles: [], - ambientExternalModules: undefined, - isLibFile: false - }); + test("import i1 = require(\"r1.ts\"); import i2 =require(\"r2.ts\"); import i3= require(\"r3.ts\"); import i4=require(\"r4.ts\"); import i5 = require (\"r5.ts\");", + /*readImportFile*/ false, + /*detectJavaScriptImports*/ false, { + referencedFiles: ([]), + typeReferenceDirectives: [], + libReferenceDirectives: [], + importedFiles: ([]), + ambientExternalModules: undefined, + isLibFile: false + }); }); - it("Do not return import path because of invalid import syntax", () => { - test("import i1 require(\"r1.ts\"); import = require(\"r2.ts\") import i3= require(\"r3.ts\"); import i5", - /*readImportFile*/ true, - /*detectJavaScriptImports*/ false, - { - referencedFiles: [], - typeReferenceDirectives: [], - libReferenceDirectives: [], - importedFiles: [{ fileName: "r3.ts", pos: 73, end: 78 }], - ambientExternalModules: undefined, - isLibFile: false - }); + test("import i1 require(\"r1.ts\"); import = require(\"r2.ts\") import i3= require(\"r3.ts\"); import i5", + /*readImportFile*/ true, + /*detectJavaScriptImports*/ false, { + referencedFiles: ([]), + typeReferenceDirectives: [], + libReferenceDirectives: [], + importedFiles: [{ fileName: "r3.ts", pos: 73, end: 78 }], + ambientExternalModules: undefined, + isLibFile: false + }); }); - it("Correctly return referenced files and import files", () => { - test("///" + "\n" + "///" + "\n" + "import i1 = require(\"r1.ts\"); import i2 =require(\"r2.ts\");", - /*readImportFile*/ true, - /*detectJavaScriptImports*/ false, - { - referencedFiles: [{ fileName: "refFile1.ts", pos: 20, end: 31 }, { fileName: "refFile2.ts", pos: 57, end: 68 }], - typeReferenceDirectives: [], - libReferenceDirectives: [], - importedFiles: [{ fileName: "r1.ts", pos: 92, end: 97 }, { fileName: "r2.ts", pos: 121, end: 126 }], - ambientExternalModules: undefined, - isLibFile: false - }); + test("///" + "\n" + "///" + "\n" + "import i1 = require(\"r1.ts\"); import i2 =require(\"r2.ts\");", + /*readImportFile*/ true, + /*detectJavaScriptImports*/ false, { + referencedFiles: [{ fileName: "refFile1.ts", pos: 20, end: 31 }, { fileName: "refFile2.ts", pos: 57, end: 68 }], + typeReferenceDirectives: [], + libReferenceDirectives: [], + importedFiles: [{ fileName: "r1.ts", pos: 92, end: 97 }, { fileName: "r2.ts", pos: 121, end: 126 }], + ambientExternalModules: undefined, + isLibFile: false + }); }); - it("Correctly return referenced files and import files even with some invalid syntax", () => { - test("///" + "\n" + "///" + "\n" + "import i1 = require(\"r1.ts\"); import = require(\"r2.ts\"); import i2 = require(\"r3.ts\");", - /*readImportFile*/ true, - /*detectJavaScriptImports*/ false, - { - referencedFiles: [{ fileName: "refFile1.ts", pos: 20, end: 31 }], - typeReferenceDirectives: [], - libReferenceDirectives: [], - importedFiles: [{ fileName: "r1.ts", pos: 91, end: 96 }, { fileName: "r3.ts", pos: 148, end: 153 }], - ambientExternalModules: undefined, - isLibFile: false - }); + test("///" + "\n" + "///" + "\n" + "import i1 = require(\"r1.ts\"); import = require(\"r2.ts\"); import i2 = require(\"r3.ts\");", + /*readImportFile*/ true, + /*detectJavaScriptImports*/ false, { + referencedFiles: [{ fileName: "refFile1.ts", pos: 20, end: 31 }], + typeReferenceDirectives: [], + libReferenceDirectives: [], + importedFiles: [{ fileName: "r1.ts", pos: 91, end: 96 }, { fileName: "r3.ts", pos: 148, end: 153 }], + ambientExternalModules: undefined, + isLibFile: false + }); }); - it("Correctly return ES6 imports", () => { test("import * as ns from \"m1\";" + "\n" + "import def, * as ns from \"m2\";" + "\n" + @@ -166,10 +143,9 @@ describe("unittests:: services:: PreProcessFile:", () => { "import {a} from \"m4\";" + "\n" + "import {a as A} from \"m5\";" + "\n" + "import {a as A, b, c as C} from \"m6\";" + "\n" + - "import def , {a, b, c as C} from \"m7\";" + "\n", - /*readImportFile*/ true, - /*detectJavaScriptImports*/ false, - { + "import def , {a, b, c as C} from \"m7\";" + "\n", + /*readImportFile*/ true, + /*detectJavaScriptImports*/ false, { referencedFiles: [], typeReferenceDirectives: [], libReferenceDirectives: [], @@ -186,15 +162,13 @@ describe("unittests:: services:: PreProcessFile:", () => { isLibFile: false }); }); - it("Correctly return ES6 exports", () => { test("export * from \"m1\";" + "\n" + "export {a} from \"m2\";" + "\n" + "export {a as A} from \"m3\";" + "\n" + - "export {a as A, b, c as C} from \"m4\";" + "\n", - /*readImportFile*/ true, - /*detectJavaScriptImports*/ false, - { + "export {a as A, b, c as C} from \"m4\";" + "\n", + /*readImportFile*/ true, + /*detectJavaScriptImports*/ false, { referencedFiles: [], typeReferenceDirectives: [], libReferenceDirectives: [], @@ -208,17 +182,15 @@ describe("unittests:: services:: PreProcessFile:", () => { isLibFile: false }); }); - it("Correctly return ambient external modules", () => { test(` declare module A {} declare module "B" {} function foo() { } - `, - /*readImportFile*/ true, - /*detectJavaScriptImports*/ false, - { + `, + /*readImportFile*/ true, + /*detectJavaScriptImports*/ false, { referencedFiles: [], typeReferenceDirectives: [], libReferenceDirectives: [], @@ -227,21 +199,19 @@ describe("unittests:: services:: PreProcessFile:", () => { isLibFile: false }); }); - it("Correctly handles export import declarations", () => { - test("export import a = require(\"m1\");", - /*readImportFile*/ true, - /*detectJavaScriptImports*/ false, - { - referencedFiles: [], - typeReferenceDirectives: [], - libReferenceDirectives: [], - importedFiles: [ - { fileName: "m1", pos: 26, end: 28 } - ], - ambientExternalModules: undefined, - isLibFile: false - }); + test("export import a = require(\"m1\");", + /*readImportFile*/ true, + /*detectJavaScriptImports*/ false, { + referencedFiles: [], + typeReferenceDirectives: [], + libReferenceDirectives: [], + importedFiles: [ + { fileName: "m1", pos: 26, end: 28 } + ], + ambientExternalModules: undefined, + isLibFile: false + }); }); it("Correctly handles export require calls in JavaScript files", () => { test(` @@ -249,10 +219,9 @@ describe("unittests:: services:: PreProcessFile:", () => { var x = require('m2'); foo(require('m3')); var z = { f: require('m4') } - `, - /*readImportFile*/ true, - /*detectJavaScriptImports*/ true, - { + `, + /*readImportFile*/ true, + /*detectJavaScriptImports*/ true, { referencedFiles: [], typeReferenceDirectives: [], libReferenceDirectives: [], @@ -270,10 +239,9 @@ describe("unittests:: services:: PreProcessFile:", () => { test(` define(["mod1", "mod2"], (m1, m2) => { }); - `, - /*readImportFile*/ true, - /*detectJavaScriptImports*/ true, - { + `, + /*readImportFile*/ true, + /*detectJavaScriptImports*/ true, { referencedFiles: [], typeReferenceDirectives: [], libReferenceDirectives: [], @@ -285,15 +253,13 @@ describe("unittests:: services:: PreProcessFile:", () => { isLibFile: false }); }); - it("Correctly handles dependency lists in define(modName, [deplist]) calls in JavaScript files", () => { test(` define("mod", ["mod1", "mod2"], (m1, m2) => { }); - `, - /*readImportFile*/ true, - /*detectJavaScriptImports*/ true, - { + `, + /*readImportFile*/ true, + /*detectJavaScriptImports*/ true, { referencedFiles: [], typeReferenceDirectives: [], libReferenceDirectives: [], @@ -305,7 +271,6 @@ describe("unittests:: services:: PreProcessFile:", () => { isLibFile: false }); }); - it("correctly handles augmentations in external modules - 1", () => { test(` declare module "../Observable" { @@ -313,10 +278,9 @@ describe("unittests:: services:: PreProcessFile:", () => { } export {} - `, - /*readImportFile*/ true, - /*detectJavaScriptImports*/ false, - { + `, + /*readImportFile*/ true, + /*detectJavaScriptImports*/ false, { referencedFiles: [], typeReferenceDirectives: [], libReferenceDirectives: [], @@ -334,10 +298,9 @@ describe("unittests:: services:: PreProcessFile:", () => { } import * as x from "m"; - `, - /*readImportFile*/ true, - /*detectJavaScriptImports*/ false, - { + `, + /*readImportFile*/ true, + /*detectJavaScriptImports*/ false, { referencedFiles: [], typeReferenceDirectives: [], libReferenceDirectives: [], @@ -356,10 +319,9 @@ describe("unittests:: services:: PreProcessFile:", () => { } import m = require("m"); - `, - /*readImportFile*/ true, - /*detectJavaScriptImports*/ false, - { + `, + /*readImportFile*/ true, + /*detectJavaScriptImports*/ false, { referencedFiles: [], typeReferenceDirectives: [], libReferenceDirectives: [], @@ -378,10 +340,9 @@ describe("unittests:: services:: PreProcessFile:", () => { } namespace N {} export = N; - `, - /*readImportFile*/ true, - /*detectJavaScriptImports*/ false, - { + `, + /*readImportFile*/ true, + /*detectJavaScriptImports*/ false, { referencedFiles: [], typeReferenceDirectives: [], libReferenceDirectives: [], @@ -399,10 +360,9 @@ describe("unittests:: services:: PreProcessFile:", () => { } namespace N {} export import IN = N; - `, - /*readImportFile*/ true, - /*detectJavaScriptImports*/ false, - { + `, + /*readImportFile*/ true, + /*detectJavaScriptImports*/ false, { referencedFiles: [], typeReferenceDirectives: [], libReferenceDirectives: [], @@ -419,10 +379,9 @@ describe("unittests:: services:: PreProcessFile:", () => { interface I {} } export let x = 1; - `, - /*readImportFile*/ true, - /*detectJavaScriptImports*/ false, - { + `, + /*readImportFile*/ true, + /*detectJavaScriptImports*/ false, { referencedFiles: [], typeReferenceDirectives: [], libReferenceDirectives: [], @@ -433,7 +392,7 @@ describe("unittests:: services:: PreProcessFile:", () => { isLibFile: false }); }); - it ("correctly handles augmentations in ambient external modules - 1", () => { + it("correctly handles augmentations in ambient external modules - 1", () => { test(` declare module "m1" { export * from "m2"; @@ -441,10 +400,9 @@ describe("unittests:: services:: PreProcessFile:", () => { interface I {} } } - `, - /*readImportFile*/ true, - /*detectJavaScriptImports*/ false, - { + `, + /*readImportFile*/ true, + /*detectJavaScriptImports*/ false, { referencedFiles: [], typeReferenceDirectives: [], libReferenceDirectives: [], @@ -456,7 +414,7 @@ describe("unittests:: services:: PreProcessFile:", () => { isLibFile: false }); }); - it ("correctly handles augmentations in ambient external modules - 2", () => { + it("correctly handles augmentations in ambient external modules - 2", () => { test(` namespace M { var x; } import IM = M; @@ -466,10 +424,9 @@ describe("unittests:: services:: PreProcessFile:", () => { interface I {} } } - `, - /*readImportFile*/ true, - /*detectJavaScriptImports*/ false, - { + `, + /*readImportFile*/ true, + /*detectJavaScriptImports*/ false, { referencedFiles: [], typeReferenceDirectives: [], libReferenceDirectives: [], @@ -481,16 +438,15 @@ describe("unittests:: services:: PreProcessFile:", () => { isLibFile: false }); }); - it ("correctly recognizes type reference directives", () => { + it("correctly recognizes type reference directives", () => { test(` /// /// /// /// - `, - /*readImportFile*/ true, - /*detectJavaScriptImports*/ false, - { + `, + /*readImportFile*/ true, + /*detectJavaScriptImports*/ false, { referencedFiles: [ { pos: 34, end: 35, fileName: "a" }, { pos: 112, end: 114, fileName: "a2" } @@ -505,22 +461,20 @@ describe("unittests:: services:: PreProcessFile:", () => { isLibFile: false }); }); - it ("correctly recognizes lib reference directives", () => { + it("correctly recognizes lib reference directives", () => { test(` /// /// /// /// - `, - /*readImportFile*/ true, - /*detectJavaScriptImports*/ false, - { + `, + /*readImportFile*/ true, + /*detectJavaScriptImports*/ false, { referencedFiles: [ { pos: 34, end: 35, fileName: "a" }, { pos: 110, end: 112, fileName: "a2" } ], - typeReferenceDirectives: [ - ], + typeReferenceDirectives: [], libReferenceDirectives: [ { pos: 71, end: 73, fileName: "a1" }, { pos: 148, end: 150, fileName: "a3" } @@ -532,4 +486,3 @@ describe("unittests:: services:: PreProcessFile:", () => { }); }); }); - diff --git a/src/testRunner/unittests/services/textChanges.ts b/src/testRunner/unittests/services/textChanges.ts index 2d76b64696d64..24c55ab7b2480 100644 --- a/src/testRunner/unittests/services/textChanges.ts +++ b/src/testRunner/unittests/services/textChanges.ts @@ -1,65 +1,57 @@ +import { Node, isDeclaration, isIdentifier, forEachChild, NewLineKind, getNewLineCharacter, formatting, testFormatSettings, createSourceFile, ScriptTarget, zipWith, Debug, NodeArray, isArray, forEach, SourceFile, textChanges, FunctionDeclaration, createFunctionDeclaration, emptyArray, createKeywordTypeNode, SyntaxKind, createBlock, createReturn, createCall, last, VariableStatement, cast, isVariableStatement, VariableDeclaration, isVariableDeclaration, createVariableDeclaration, createObjectLiteral, createPropertyAssignment, createLiteral, createClassDeclaration, createToken, createHeritageClause, createExpressionWithTypeArguments, createIdentifier, createProperty, ConstructorDeclaration, ClassDeclaration, find, ClassElement, isConstructorDeclaration, createSuper, createStatement, createImportSpecifier, createComputedPropertyName, createParen } from "../../ts"; +import { Baseline } from "../../Harness"; // Some tests have trailing whitespace - -namespace ts { - describe("unittests:: services:: textChanges", () => { - function findChild(name: string, n: Node) { - return find(n)!; - - function find(node: Node): Node | undefined { - if (isDeclaration(node) && node.name && isIdentifier(node.name) && node.name.escapedText === name) { - return node; - } - else { - return forEachChild(node, find); - } +describe("unittests:: services:: textChanges", () => { + function findChild(name: string, n: Node) { + return find(n)!; + function find(node: Node): Node | undefined { + if (isDeclaration(node) && node.name && isIdentifier(node.name) && node.name.escapedText === name) { + return node; } - } - - const printerOptions = { newLine: NewLineKind.LineFeed }; - const newLineCharacter = getNewLineCharacter(printerOptions); - - function getRuleProvider(placeOpenBraceOnNewLineForFunctions: boolean): formatting.FormatContext { - return formatting.getFormatContext(placeOpenBraceOnNewLineForFunctions ? { ...testFormatSettings, placeOpenBraceOnNewLineForFunctions: true } : testFormatSettings); - } - - // validate that positions that were recovered from the printed text actually match positions that will be created if the same text is parsed. - function verifyPositions(node: Node, text: string): void { - const nodeList = flattenNodes(node); - const sourceFile = createSourceFile("f.ts", text, ScriptTarget.ES2015); - const parsedNodeList = flattenNodes(sourceFile.statements[0]); - zipWith(nodeList, parsedNodeList, (left, right) => { - Debug.assert(left.pos === right.pos); - Debug.assert(left.end === right.end); - }); - - function flattenNodes(n: Node) { - const data: (Node | NodeArray)[] = []; - walk(n); - return data; - - function walk(n: Node | NodeArray): void { - data.push(n); - return isArray(n) ? forEach(n, walk) : forEachChild(n, walk, walk); - } + else { + return forEachChild(node, find); } } - - function runSingleFileTest(caption: string, placeOpenBraceOnNewLineForFunctions: boolean, text: string, validateNodes: boolean, testBlock: (sourceFile: SourceFile, changeTracker: textChanges.ChangeTracker) => void) { - it(caption, () => { - const sourceFile = createSourceFile("source.ts", text, ScriptTarget.ES2015, /*setParentNodes*/ true); - const rulesProvider = getRuleProvider(placeOpenBraceOnNewLineForFunctions); - const changeTracker = new textChanges.ChangeTracker(newLineCharacter, rulesProvider); - testBlock(sourceFile, changeTracker); - const changes = changeTracker.getChanges(validateNodes ? verifyPositions : undefined); - assert.equal(changes.length, 1); - assert.equal(changes[0].fileName, sourceFile.fileName); - const modified = textChanges.applyChanges(sourceFile.text, changes[0].textChanges); - Harness.Baseline.runBaseline(`textChanges/${caption}.js`, `===ORIGINAL===${newLineCharacter}${text}${newLineCharacter}===MODIFIED===${newLineCharacter}${modified}`); - }); + } + const printerOptions = { newLine: NewLineKind.LineFeed }; + const newLineCharacter = getNewLineCharacter(printerOptions); + function getRuleProvider(placeOpenBraceOnNewLineForFunctions: boolean): formatting.FormatContext { + return formatting.getFormatContext(placeOpenBraceOnNewLineForFunctions ? { ...testFormatSettings, placeOpenBraceOnNewLineForFunctions: true } : testFormatSettings); + } + // validate that positions that were recovered from the printed text actually match positions that will be created if the same text is parsed. + function verifyPositions(node: Node, text: string): void { + const nodeList = flattenNodes(node); + const sourceFile = createSourceFile("f.ts", text, ScriptTarget.ES2015); + const parsedNodeList = flattenNodes(sourceFile.statements[0]); + zipWith(nodeList, parsedNodeList, (left, right) => { + Debug.assert(left.pos === right.pos); + Debug.assert(left.end === right.end); + }); + function flattenNodes(n: Node) { + const data: (Node | NodeArray)[] = []; + walk(n); + return data; + function walk(n: Node | NodeArray): void { + data.push(n); + return isArray(n) ? forEach(n, walk) : forEachChild(n, walk, walk); + } } - - { - const text = ` + } + function runSingleFileTest(caption: string, placeOpenBraceOnNewLineForFunctions: boolean, text: string, validateNodes: boolean, testBlock: (sourceFile: SourceFile, changeTracker: textChanges.ChangeTracker) => void) { + it(caption, () => { + const sourceFile = createSourceFile("source.ts", text, ScriptTarget.ES2015, /*setParentNodes*/ true); + const rulesProvider = getRuleProvider(placeOpenBraceOnNewLineForFunctions); + const changeTracker = new textChanges.ChangeTracker(newLineCharacter, rulesProvider); + testBlock(sourceFile, changeTracker); + const changes = changeTracker.getChanges(validateNodes ? verifyPositions : undefined); + assert.equal(changes.length, 1); + assert.equal(changes[0].fileName, sourceFile.fileName); + const modified = textChanges.applyChanges(sourceFile.text, changes[0].textChanges); + Baseline.runBaseline(`textChanges/${caption}.js`, `===ORIGINAL===${newLineCharacter}${text}${newLineCharacter}===MODIFIED===${newLineCharacter}${modified}`); + }); + } + { + const text = ` namespace M { namespace M2 @@ -80,33 +72,26 @@ namespace M } } }`; - runSingleFileTest("extractMethodLike", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { - const statements = (findChild("foo", sourceFile)).body!.statements.slice(1); - const newFunction = createFunctionDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*asteriskToken*/ undefined, - /*name*/ "bar", - /*typeParameters*/ undefined, - /*parameters*/ emptyArray, - /*type*/ createKeywordTypeNode(SyntaxKind.AnyKeyword), - /*body */ createBlock(statements) - ); - - changeTracker.insertNodeBefore(sourceFile, /*before*/findChild("M2", sourceFile), newFunction); - - // replace statements with return statement - const newStatement = createReturn( - createCall( - /*expression*/ newFunction.name!, - /*typeArguments*/ undefined, - /*argumentsArray*/ emptyArray - )); - changeTracker.replaceNodeRange(sourceFile, statements[0], last(statements), newStatement, { suffix: newLineCharacter }); - }); - } - { - const text = ` + runSingleFileTest("extractMethodLike", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + const statements = (findChild("foo", sourceFile)).body!.statements.slice(1); + const newFunction = createFunctionDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*asteriskToken*/ undefined, + /*name*/ "bar", + /*typeParameters*/ undefined, emptyArray, + /*type*/ createKeywordTypeNode(SyntaxKind.AnyKeyword), + /*body */ createBlock(statements)); + changeTracker.insertNodeBefore(sourceFile, /*before*/ findChild("M2", sourceFile), newFunction); + // replace statements with return statement + const newStatement = createReturn(createCall( + /*expression*/ (newFunction.name!), + /*typeArguments*/ undefined, emptyArray)); + changeTracker.replaceNodeRange(sourceFile, statements[0], last(statements), newStatement, { suffix: newLineCharacter }); + }); + } + { + const text = ` function foo() { return 1; } @@ -115,19 +100,19 @@ function bar() { return 2; } `; - runSingleFileTest("deleteRange1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.deleteRange(sourceFile, { pos: text.indexOf("function foo"), end: text.indexOf("function bar") }); - }); - } - function findVariableStatementContaining(name: string, sourceFile: SourceFile): VariableStatement { - return cast(findVariableDeclarationContaining(name, sourceFile).parent.parent, isVariableStatement); - } - function findVariableDeclarationContaining(name: string, sourceFile: SourceFile): VariableDeclaration { - return cast(findChild(name, sourceFile), isVariableDeclaration); - } - const { deleteNode } = textChanges; - { - const text = ` + runSingleFileTest("deleteRange1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteRange(sourceFile, { pos: text.indexOf("function foo"), end: text.indexOf("function bar") }); + }); + } + function findVariableStatementContaining(name: string, sourceFile: SourceFile): VariableStatement { + return cast(findVariableDeclarationContaining(name, sourceFile).parent.parent, isVariableStatement); + } + function findVariableDeclarationContaining(name: string, sourceFile: SourceFile): VariableDeclaration { + return cast(findChild(name, sourceFile), isVariableDeclaration); + } + const { deleteNode } = textChanges; + { + const text = ` var x = 1; // some comment - 1 /** * comment 2 @@ -135,24 +120,24 @@ var x = 1; // some comment - 1 var y = 2; // comment 3 var z = 3; // comment 4 `; - runSingleFileTest("deleteNode1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - deleteNode(changeTracker, sourceFile, findVariableStatementContaining("y", sourceFile)); - }); - runSingleFileTest("deleteNode2", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - deleteNode(changeTracker, sourceFile, findVariableStatementContaining("y", sourceFile), { leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude }); - }); - runSingleFileTest("deleteNode3", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - deleteNode(changeTracker, sourceFile, findVariableStatementContaining("y", sourceFile), { trailingTriviaOption: textChanges.TrailingTriviaOption.Exclude }); - }); - runSingleFileTest("deleteNode4", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - deleteNode(changeTracker, sourceFile, findVariableStatementContaining("y", sourceFile), { leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: textChanges.TrailingTriviaOption.Exclude }); - }); - runSingleFileTest("deleteNode5", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - deleteNode(changeTracker, sourceFile, findVariableStatementContaining("x", sourceFile)); - }); - } - { - const text = ` + runSingleFileTest("deleteNode1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + deleteNode(changeTracker, sourceFile, findVariableStatementContaining("y", sourceFile)); + }); + runSingleFileTest("deleteNode2", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + deleteNode(changeTracker, sourceFile, findVariableStatementContaining("y", sourceFile), { leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude }); + }); + runSingleFileTest("deleteNode3", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + deleteNode(changeTracker, sourceFile, findVariableStatementContaining("y", sourceFile), { trailingTriviaOption: textChanges.TrailingTriviaOption.Exclude }); + }); + runSingleFileTest("deleteNode4", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + deleteNode(changeTracker, sourceFile, findVariableStatementContaining("y", sourceFile), { leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: textChanges.TrailingTriviaOption.Exclude }); + }); + runSingleFileTest("deleteNode5", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + deleteNode(changeTracker, sourceFile, findVariableStatementContaining("x", sourceFile)); + }); + } + { + const text = ` // comment 1 var x = 1; // comment 2 // comment 3 @@ -161,55 +146,41 @@ var z = 3; // comment 5 // comment 6 var a = 4; // comment 7 `; - runSingleFileTest("deleteNodeRange1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.deleteNodeRange(sourceFile, findVariableStatementContaining("y", sourceFile), findVariableStatementContaining("z", sourceFile)); - }); - runSingleFileTest("deleteNodeRange2", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.deleteNodeRange(sourceFile, findVariableStatementContaining("y", sourceFile), findVariableStatementContaining("z", sourceFile), - { leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude }); - }); - runSingleFileTest("deleteNodeRange3", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.deleteNodeRange(sourceFile, findVariableStatementContaining("y", sourceFile), findVariableStatementContaining("z", sourceFile), - { trailingTriviaOption: textChanges.TrailingTriviaOption.Exclude }); - }); - runSingleFileTest("deleteNodeRange4", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.deleteNodeRange(sourceFile, findVariableStatementContaining("y", sourceFile), findVariableStatementContaining("z", sourceFile), - { leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: textChanges.TrailingTriviaOption.Exclude }); - }); - } - function createTestVariableDeclaration(name: string) { - return createVariableDeclaration(name, /*type*/ undefined, createObjectLiteral([createPropertyAssignment("p1", createLiteral(1))], /*multiline*/ true)); - } - function createTestClass() { - return createClassDeclaration( - /*decorators*/ undefined, - [ - createToken(SyntaxKind.PublicKeyword) - ], - "class1", - /*typeParameters*/ undefined, - [ - createHeritageClause( - SyntaxKind.ImplementsKeyword, - [ - createExpressionWithTypeArguments(/*typeArguments*/ undefined, createIdentifier("interface1")) - ] - ) - ], - [ - createProperty( - /*decorators*/ undefined, - /*modifiers*/ undefined, - "property1", - /*questionToken*/ undefined, - createKeywordTypeNode(SyntaxKind.BooleanKeyword), - /*initializer*/ undefined - ) - ] - ); - } - { - const text = ` + runSingleFileTest("deleteNodeRange1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeRange(sourceFile, findVariableStatementContaining("y", sourceFile), findVariableStatementContaining("z", sourceFile)); + }); + runSingleFileTest("deleteNodeRange2", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeRange(sourceFile, findVariableStatementContaining("y", sourceFile), findVariableStatementContaining("z", sourceFile), { leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude }); + }); + runSingleFileTest("deleteNodeRange3", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeRange(sourceFile, findVariableStatementContaining("y", sourceFile), findVariableStatementContaining("z", sourceFile), { trailingTriviaOption: textChanges.TrailingTriviaOption.Exclude }); + }); + runSingleFileTest("deleteNodeRange4", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeRange(sourceFile, findVariableStatementContaining("y", sourceFile), findVariableStatementContaining("z", sourceFile), { leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: textChanges.TrailingTriviaOption.Exclude }); + }); + } + function createTestVariableDeclaration(name: string) { + return createVariableDeclaration(name, /*type*/ undefined, createObjectLiteral([createPropertyAssignment("p1", createLiteral(1))], /*multiline*/ true)); + } + function createTestClass() { + return createClassDeclaration( + /*decorators*/ undefined, [ + createToken(SyntaxKind.PublicKeyword) + ], "class1", + /*typeParameters*/ undefined, [ + createHeritageClause(SyntaxKind.ImplementsKeyword, [ + createExpressionWithTypeArguments(/*typeArguments*/ undefined, createIdentifier("interface1")) + ]) + ], [ + createProperty( + /*decorators*/ undefined, + /*modifiers*/ undefined, "property1", + /*questionToken*/ undefined, createKeywordTypeNode(SyntaxKind.BooleanKeyword), + /*initializer*/ undefined) + ]); + } + { + const text = ` // comment 1 var x = 1; // comment 2 // comment 3 @@ -217,31 +188,30 @@ var y = 2; // comment 4 var z = 3; // comment 5 // comment 6 var a = 4; // comment 7`; - runSingleFileTest("replaceRange", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { - changeTracker.replaceRange(sourceFile, { pos: text.indexOf("var y"), end: text.indexOf("var a") }, createTestClass(), { suffix: newLineCharacter }); - }); - runSingleFileTest("replaceRangeWithForcedIndentation", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { - changeTracker.replaceRange(sourceFile, { pos: text.indexOf("var y"), end: text.indexOf("var a") }, createTestClass(), { suffix: newLineCharacter, indentation: 8, delta: 0 }); - }); - - runSingleFileTest("replaceRangeNoLineBreakBefore", /*placeOpenBraceOnNewLineForFunctions*/ true, `const x = 1, y = "2";`, /*validateNodes*/ false, (sourceFile, changeTracker) => { - const newNode = createTestVariableDeclaration("z1"); - changeTracker.replaceRange(sourceFile, { pos: sourceFile.text.indexOf("y"), end: sourceFile.text.indexOf(";") }, newNode); - }); - } - { - const text = ` + runSingleFileTest("replaceRange", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.replaceRange(sourceFile, { pos: text.indexOf("var y"), end: text.indexOf("var a") }, createTestClass(), { suffix: newLineCharacter }); + }); + runSingleFileTest("replaceRangeWithForcedIndentation", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.replaceRange(sourceFile, { pos: text.indexOf("var y"), end: text.indexOf("var a") }, createTestClass(), { suffix: newLineCharacter, indentation: 8, delta: 0 }); + }); + runSingleFileTest("replaceRangeNoLineBreakBefore", /*placeOpenBraceOnNewLineForFunctions*/ true, `const x = 1, y = "2";`, /*validateNodes*/ false, (sourceFile, changeTracker) => { + const newNode = createTestVariableDeclaration("z1"); + changeTracker.replaceRange(sourceFile, { pos: sourceFile.text.indexOf("y"), end: sourceFile.text.indexOf(";") }, newNode); + }); + } + { + const text = ` namespace A { const x = 1, y = "2"; } `; - runSingleFileTest("replaceNode1NoLineBreakBefore", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - const newNode = createTestVariableDeclaration("z1"); - changeTracker.replaceNode(sourceFile, findChild("y", sourceFile), newNode); - }); - } - { - const text = ` + runSingleFileTest("replaceNode1NoLineBreakBefore", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + const newNode = createTestVariableDeclaration("z1"); + changeTracker.replaceNode(sourceFile, findChild("y", sourceFile), newNode); + }); + } + { + const text = ` // comment 1 var x = 1; // comment 2 // comment 3 @@ -249,24 +219,24 @@ var y = 2; // comment 4 var z = 3; // comment 5 // comment 6 var a = 4; // comment 7`; - runSingleFileTest("replaceNode1", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { - changeTracker.replaceNode(sourceFile, findVariableStatementContaining("y", sourceFile), createTestClass(), { suffix: newLineCharacter }); - }); - runSingleFileTest("replaceNode2", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { - changeTracker.replaceNode(sourceFile, findVariableStatementContaining("y", sourceFile), createTestClass(), { leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude, suffix: newLineCharacter, prefix: newLineCharacter }); - }); - runSingleFileTest("replaceNode3", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { - changeTracker.replaceNode(sourceFile, findVariableStatementContaining("y", sourceFile), createTestClass(), { trailingTriviaOption: textChanges.TrailingTriviaOption.Exclude, suffix: newLineCharacter }); - }); - runSingleFileTest("replaceNode4", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { - changeTracker.replaceNode(sourceFile, findVariableStatementContaining("y", sourceFile), createTestClass(), { leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: textChanges.TrailingTriviaOption.Exclude }); - }); - runSingleFileTest("replaceNode5", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { - changeTracker.replaceNode(sourceFile, findVariableStatementContaining("x", sourceFile), createTestClass(), { leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: textChanges.TrailingTriviaOption.Exclude }); - }); - } - { - const text = ` + runSingleFileTest("replaceNode1", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.replaceNode(sourceFile, findVariableStatementContaining("y", sourceFile), createTestClass(), { suffix: newLineCharacter }); + }); + runSingleFileTest("replaceNode2", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.replaceNode(sourceFile, findVariableStatementContaining("y", sourceFile), createTestClass(), { leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude, suffix: newLineCharacter, prefix: newLineCharacter }); + }); + runSingleFileTest("replaceNode3", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.replaceNode(sourceFile, findVariableStatementContaining("y", sourceFile), createTestClass(), { trailingTriviaOption: textChanges.TrailingTriviaOption.Exclude, suffix: newLineCharacter }); + }); + runSingleFileTest("replaceNode4", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.replaceNode(sourceFile, findVariableStatementContaining("y", sourceFile), createTestClass(), { leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: textChanges.TrailingTriviaOption.Exclude }); + }); + runSingleFileTest("replaceNode5", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.replaceNode(sourceFile, findVariableStatementContaining("x", sourceFile), createTestClass(), { leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: textChanges.TrailingTriviaOption.Exclude }); + }); + } + { + const text = ` // comment 1 var x = 1; // comment 2 // comment 3 @@ -274,21 +244,21 @@ var y = 2; // comment 4 var z = 3; // comment 5 // comment 6 var a = 4; // comment 7`; - runSingleFileTest("replaceNodeRange1", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { - changeTracker.replaceNodeRange(sourceFile, findVariableStatementContaining("y", sourceFile), findVariableStatementContaining("z", sourceFile), createTestClass(), { suffix: newLineCharacter }); - }); - runSingleFileTest("replaceNodeRange2", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { - changeTracker.replaceNodeRange(sourceFile, findVariableStatementContaining("y", sourceFile), findVariableStatementContaining("z", sourceFile), createTestClass(), { leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude, suffix: newLineCharacter, prefix: newLineCharacter }); - }); - runSingleFileTest("replaceNodeRange3", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { - changeTracker.replaceNodeRange(sourceFile, findVariableStatementContaining("y", sourceFile), findVariableStatementContaining("z", sourceFile), createTestClass(), { trailingTriviaOption: textChanges.TrailingTriviaOption.Exclude, suffix: newLineCharacter }); - }); - runSingleFileTest("replaceNodeRange4", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { - changeTracker.replaceNodeRange(sourceFile, findVariableStatementContaining("y", sourceFile), findVariableStatementContaining("z", sourceFile), createTestClass(), { leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: textChanges.TrailingTriviaOption.Exclude }); - }); - } - { - const text = ` + runSingleFileTest("replaceNodeRange1", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.replaceNodeRange(sourceFile, findVariableStatementContaining("y", sourceFile), findVariableStatementContaining("z", sourceFile), createTestClass(), { suffix: newLineCharacter }); + }); + runSingleFileTest("replaceNodeRange2", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.replaceNodeRange(sourceFile, findVariableStatementContaining("y", sourceFile), findVariableStatementContaining("z", sourceFile), createTestClass(), { leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude, suffix: newLineCharacter, prefix: newLineCharacter }); + }); + runSingleFileTest("replaceNodeRange3", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.replaceNodeRange(sourceFile, findVariableStatementContaining("y", sourceFile), findVariableStatementContaining("z", sourceFile), createTestClass(), { trailingTriviaOption: textChanges.TrailingTriviaOption.Exclude, suffix: newLineCharacter }); + }); + runSingleFileTest("replaceNodeRange4", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.replaceNodeRange(sourceFile, findVariableStatementContaining("y", sourceFile), findVariableStatementContaining("z", sourceFile), createTestClass(), { leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude, trailingTriviaOption: textChanges.TrailingTriviaOption.Exclude }); + }); + } + { + const text = ` // comment 1 var x = 1; // comment 2 // comment 3 @@ -296,15 +266,15 @@ var y; // comment 4 var z = 3; // comment 5 // comment 6 var a = 4; // comment 7`; - runSingleFileTest("insertNodeBefore3", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { - changeTracker.insertNodeBefore(sourceFile, findVariableStatementContaining("y", sourceFile), createTestClass()); - }); - runSingleFileTest("insertNodeAfterVariableDeclaration", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeAfter(sourceFile, findVariableDeclarationContaining("y", sourceFile), createTestVariableDeclaration("z1")); - }); - } - { - const text = ` + runSingleFileTest("insertNodeBefore3", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.insertNodeBefore(sourceFile, findVariableStatementContaining("y", sourceFile), createTestClass()); + }); + runSingleFileTest("insertNodeAfterVariableDeclaration", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeAfter(sourceFile, findVariableDeclarationContaining("y", sourceFile), createTestVariableDeclaration("z1")); + }); + } + { + const text = ` namespace M { // comment 1 var x = 1; // comment 2 @@ -314,107 +284,102 @@ namespace M { // comment 6 var a = 4; // comment 7 }`; - runSingleFileTest("insertNodeBefore1", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { - changeTracker.insertNodeBefore(sourceFile, findVariableStatementContaining("y", sourceFile), createTestClass()); - }); - runSingleFileTest("insertNodeBefore2", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { - changeTracker.insertNodeBefore(sourceFile, findChild("M", sourceFile), createTestClass()); - }); - runSingleFileTest("insertNodeAfter1", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { - changeTracker.insertNodeAfter(sourceFile, findVariableStatementContaining("y", sourceFile), createTestClass()); - }); - runSingleFileTest("insertNodeAfter2", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { - changeTracker.insertNodeAfter(sourceFile, findChild("M", sourceFile), createTestClass()); - }); - } - - function findConstructor(sourceFile: SourceFile): ConstructorDeclaration { - const classDecl = sourceFile.statements[0]; - return find(classDecl.members, (m): m is ConstructorDeclaration => isConstructorDeclaration(m) && !!m.body)!; - } - function createTestSuperCall() { - const superCall = createCall( - createSuper(), - /*typeArguments*/ undefined, - /*argumentsArray*/ emptyArray - ); - return createStatement(superCall); - } - - { - const text1 = ` + runSingleFileTest("insertNodeBefore1", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.insertNodeBefore(sourceFile, findVariableStatementContaining("y", sourceFile), createTestClass()); + }); + runSingleFileTest("insertNodeBefore2", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.insertNodeBefore(sourceFile, findChild("M", sourceFile), createTestClass()); + }); + runSingleFileTest("insertNodeAfter1", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.insertNodeAfter(sourceFile, findVariableStatementContaining("y", sourceFile), createTestClass()); + }); + runSingleFileTest("insertNodeAfter2", /*placeOpenBraceOnNewLineForFunctions*/ true, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.insertNodeAfter(sourceFile, findChild("M", sourceFile), createTestClass()); + }); + } + function findConstructor(sourceFile: SourceFile): ConstructorDeclaration { + const classDecl = (sourceFile.statements[0]); + return find(classDecl.members, (m): m is ConstructorDeclaration => isConstructorDeclaration(m) && !!m.body)!; + } + function createTestSuperCall() { + const superCall = createCall(createSuper(), + /*typeArguments*/ undefined, emptyArray); + return createStatement(superCall); + } + { + const text1 = ` class A { constructor() { } } `; - runSingleFileTest("insertNodeAtConstructorStart", /*placeOpenBraceOnNewLineForFunctions*/ false, text1, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeAtConstructorStart(sourceFile, findConstructor(sourceFile), createTestSuperCall()); - }); - const text2 = ` + runSingleFileTest("insertNodeAtConstructorStart", /*placeOpenBraceOnNewLineForFunctions*/ false, text1, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeAtConstructorStart(sourceFile, findConstructor(sourceFile), createTestSuperCall()); + }); + const text2 = ` class A { constructor() { var x = 1; } } `; - runSingleFileTest("insertNodeAfter4", /*placeOpenBraceOnNewLineForFunctions*/ false, text2, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeAfter(sourceFile, findVariableStatementContaining("x", sourceFile), createTestSuperCall()); - }); - const text3 = ` + runSingleFileTest("insertNodeAfter4", /*placeOpenBraceOnNewLineForFunctions*/ false, text2, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeAfter(sourceFile, findVariableStatementContaining("x", sourceFile), createTestSuperCall()); + }); + const text3 = ` class A { constructor() { } } `; - runSingleFileTest("insertNodeAtConstructorStart-block with newline", /*placeOpenBraceOnNewLineForFunctions*/ false, text3, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeAtConstructorStart(sourceFile, findConstructor(sourceFile), createTestSuperCall()); - }); - } - { - const text = `var a = 1, b = 2, c = 3;`; - runSingleFileTest("deleteNodeInList1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.delete(sourceFile, findChild("a", sourceFile)); - }); - runSingleFileTest("deleteNodeInList2", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.delete(sourceFile, findChild("b", sourceFile)); - }); - runSingleFileTest("deleteNodeInList3", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.delete(sourceFile, findChild("c", sourceFile)); - }); - } - { - const text = `var a = 1,b = 2,c = 3;`; - runSingleFileTest("deleteNodeInList1_1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.delete(sourceFile, findChild("a", sourceFile)); - }); - runSingleFileTest("deleteNodeInList2_1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.delete(sourceFile, findChild("b", sourceFile)); - }); - runSingleFileTest("deleteNodeInList3_1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.delete(sourceFile, findChild("c", sourceFile)); - }); - } - { - const text = ` + runSingleFileTest("insertNodeAtConstructorStart-block with newline", /*placeOpenBraceOnNewLineForFunctions*/ false, text3, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeAtConstructorStart(sourceFile, findConstructor(sourceFile), createTestSuperCall()); + }); + } + { + const text = `var a = 1, b = 2, c = 3;`; + runSingleFileTest("deleteNodeInList1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.delete(sourceFile, findChild("a", sourceFile)); + }); + runSingleFileTest("deleteNodeInList2", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.delete(sourceFile, findChild("b", sourceFile)); + }); + runSingleFileTest("deleteNodeInList3", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.delete(sourceFile, findChild("c", sourceFile)); + }); + } + { + const text = `var a = 1,b = 2,c = 3;`; + runSingleFileTest("deleteNodeInList1_1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.delete(sourceFile, findChild("a", sourceFile)); + }); + runSingleFileTest("deleteNodeInList2_1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.delete(sourceFile, findChild("b", sourceFile)); + }); + runSingleFileTest("deleteNodeInList3_1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.delete(sourceFile, findChild("c", sourceFile)); + }); + } + { + const text = ` namespace M { var a = 1, b = 2, c = 3; }`; - runSingleFileTest("deleteNodeInList4", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.delete(sourceFile, findChild("a", sourceFile)); - }); - runSingleFileTest("deleteNodeInList5", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.delete(sourceFile, findChild("b", sourceFile)); - }); - runSingleFileTest("deleteNodeInList6", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.delete(sourceFile, findChild("c", sourceFile)); - }); - } - { - const text = ` + runSingleFileTest("deleteNodeInList4", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.delete(sourceFile, findChild("a", sourceFile)); + }); + runSingleFileTest("deleteNodeInList5", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.delete(sourceFile, findChild("b", sourceFile)); + }); + runSingleFileTest("deleteNodeInList6", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.delete(sourceFile, findChild("c", sourceFile)); + }); + } + { + const text = ` namespace M { var a = 1, // comment 1 // comment 2 @@ -422,334 +387,324 @@ namespace M { // comment 4 c = 3; // comment 5 }`; - runSingleFileTest("deleteNodeInList4_1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.delete(sourceFile, findChild("a", sourceFile)); - }); - runSingleFileTest("deleteNodeInList5_1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.delete(sourceFile, findChild("b", sourceFile)); - }); - runSingleFileTest("deleteNodeInList6_1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.delete(sourceFile, findChild("c", sourceFile)); - }); - } - { - const text = ` + runSingleFileTest("deleteNodeInList4_1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.delete(sourceFile, findChild("a", sourceFile)); + }); + runSingleFileTest("deleteNodeInList5_1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.delete(sourceFile, findChild("b", sourceFile)); + }); + runSingleFileTest("deleteNodeInList6_1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.delete(sourceFile, findChild("c", sourceFile)); + }); + } + { + const text = ` function foo(a: number, b: string, c = true) { return 1; }`; - runSingleFileTest("deleteNodeInList7", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.delete(sourceFile, findChild("a", sourceFile)); - }); - runSingleFileTest("deleteNodeInList8", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.delete(sourceFile, findChild("b", sourceFile)); - }); - runSingleFileTest("deleteNodeInList9", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.delete(sourceFile, findChild("c", sourceFile)); - }); - } - { - const text = ` + runSingleFileTest("deleteNodeInList7", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.delete(sourceFile, findChild("a", sourceFile)); + }); + runSingleFileTest("deleteNodeInList8", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.delete(sourceFile, findChild("b", sourceFile)); + }); + runSingleFileTest("deleteNodeInList9", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.delete(sourceFile, findChild("c", sourceFile)); + }); + } + { + const text = ` function foo(a: number,b: string,c = true) { return 1; }`; - runSingleFileTest("deleteNodeInList10", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.delete(sourceFile, findChild("a", sourceFile)); - }); - runSingleFileTest("deleteNodeInList11", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.delete(sourceFile, findChild("b", sourceFile)); - }); - runSingleFileTest("deleteNodeInList12", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.delete(sourceFile, findChild("c", sourceFile)); - }); - } - { - const text = ` + runSingleFileTest("deleteNodeInList10", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.delete(sourceFile, findChild("a", sourceFile)); + }); + runSingleFileTest("deleteNodeInList11", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.delete(sourceFile, findChild("b", sourceFile)); + }); + runSingleFileTest("deleteNodeInList12", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.delete(sourceFile, findChild("c", sourceFile)); + }); + } + { + const text = ` function foo( a: number, b: string, c = true) { return 1; }`; - runSingleFileTest("deleteNodeInList13", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.delete(sourceFile, findChild("a", sourceFile)); - }); - runSingleFileTest("deleteNodeInList14", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.delete(sourceFile, findChild("b", sourceFile)); - }); - runSingleFileTest("deleteNodeInList15", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.delete(sourceFile, findChild("c", sourceFile)); - }); - } - { - const text = ` + runSingleFileTest("deleteNodeInList13", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.delete(sourceFile, findChild("a", sourceFile)); + }); + runSingleFileTest("deleteNodeInList14", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.delete(sourceFile, findChild("b", sourceFile)); + }); + runSingleFileTest("deleteNodeInList15", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.delete(sourceFile, findChild("c", sourceFile)); + }); + } + { + const text = ` const x = 1, y = 2;`; - runSingleFileTest("insertNodeInListAfter1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createVariableDeclaration("z", /*type*/ undefined, createLiteral(1))); - }); - runSingleFileTest("insertNodeInListAfter2", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("y", sourceFile), createVariableDeclaration("z", /*type*/ undefined, createLiteral(1))); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInListAfter1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createVariableDeclaration("z", /*type*/ undefined, createLiteral(1))); + }); + runSingleFileTest("insertNodeInListAfter2", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("y", sourceFile), createVariableDeclaration("z", /*type*/ undefined, createLiteral(1))); + }); + } + { + const text = ` const /*x*/ x = 1, /*y*/ y = 2;`; - runSingleFileTest("insertNodeInListAfter3", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createVariableDeclaration("z", /*type*/ undefined, createLiteral(1))); - }); - runSingleFileTest("insertNodeInListAfter4", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("y", sourceFile), createVariableDeclaration("z", /*type*/ undefined, createLiteral(1))); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInListAfter3", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createVariableDeclaration("z", /*type*/ undefined, createLiteral(1))); + }); + runSingleFileTest("insertNodeInListAfter4", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("y", sourceFile), createVariableDeclaration("z", /*type*/ undefined, createLiteral(1))); + }); + } + { + const text = ` const x = 1;`; - runSingleFileTest("insertNodeInListAfter5", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createVariableDeclaration("z", /*type*/ undefined, createLiteral(1))); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInListAfter5", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createVariableDeclaration("z", /*type*/ undefined, createLiteral(1))); + }); + } + { + const text = ` const x = 1, y = 2;`; - runSingleFileTest("insertNodeInListAfter6", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createVariableDeclaration("z", /*type*/ undefined, createLiteral(1))); - }); - runSingleFileTest("insertNodeInListAfter7", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("y", sourceFile), createVariableDeclaration("z", /*type*/ undefined, createLiteral(1))); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInListAfter6", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createVariableDeclaration("z", /*type*/ undefined, createLiteral(1))); + }); + runSingleFileTest("insertNodeInListAfter7", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("y", sourceFile), createVariableDeclaration("z", /*type*/ undefined, createLiteral(1))); + }); + } + { + const text = ` const /*x*/ x = 1, /*y*/ y = 2;`; - runSingleFileTest("insertNodeInListAfter8", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createVariableDeclaration("z", /*type*/ undefined, createLiteral(1))); - }); - runSingleFileTest("insertNodeInListAfter9", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("y", sourceFile), createVariableDeclaration("z", /*type*/ undefined, createLiteral(1))); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInListAfter8", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createVariableDeclaration("z", /*type*/ undefined, createLiteral(1))); + }); + runSingleFileTest("insertNodeInListAfter9", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("y", sourceFile), createVariableDeclaration("z", /*type*/ undefined, createLiteral(1))); + }); + } + { + const text = ` import { x } from "bar"`; - runSingleFileTest("insertNodeInListAfter10", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createImportSpecifier(createIdentifier("b"), createIdentifier("a"))); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInListAfter10", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createImportSpecifier(createIdentifier("b"), createIdentifier("a"))); + }); + } + { + const text = ` import { x // this is x } from "bar"`; - runSingleFileTest("insertNodeInListAfter11", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createImportSpecifier(createIdentifier("b"), createIdentifier("a"))); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInListAfter11", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createImportSpecifier(createIdentifier("b"), createIdentifier("a"))); + }); + } + { + const text = ` import { x } from "bar"`; - runSingleFileTest("insertNodeInListAfter12", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createImportSpecifier(undefined, createIdentifier("a"))); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInListAfter12", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createImportSpecifier(undefined, createIdentifier("a"))); + }); + } + { + const text = ` import { x // this is x } from "bar"`; - runSingleFileTest("insertNodeInListAfter13", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createImportSpecifier(undefined, createIdentifier("a"))); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInListAfter13", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createImportSpecifier(undefined, createIdentifier("a"))); + }); + } + { + const text = ` import { x0, x } from "bar"`; - runSingleFileTest("insertNodeInListAfter14", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createImportSpecifier(createIdentifier("b"), createIdentifier("a"))); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInListAfter14", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createImportSpecifier(createIdentifier("b"), createIdentifier("a"))); + }); + } + { + const text = ` import { x0, x // this is x } from "bar"`; - runSingleFileTest("insertNodeInListAfter15", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createImportSpecifier(createIdentifier("b"), createIdentifier("a"))); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInListAfter15", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createImportSpecifier(createIdentifier("b"), createIdentifier("a"))); + }); + } + { + const text = ` import { x0, x } from "bar"`; - runSingleFileTest("insertNodeInListAfter16", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createImportSpecifier(undefined, createIdentifier("a"))); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInListAfter16", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createImportSpecifier(undefined, createIdentifier("a"))); + }); + } + { + const text = ` import { x0, x // this is x } from "bar"`; - runSingleFileTest("insertNodeInListAfter17", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createImportSpecifier(undefined, createIdentifier("a"))); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInListAfter17", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createImportSpecifier(undefined, createIdentifier("a"))); + }); + } + { + const text = ` import { x0, x } from "bar"`; - runSingleFileTest("insertNodeInListAfter18", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createImportSpecifier(undefined, createIdentifier("a"))); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInListAfter18", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createImportSpecifier(undefined, createIdentifier("a"))); + }); + } + { + const text = ` class A { x; }`; - runSingleFileTest("insertNodeAfterMultipleNodes", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - const newNodes = []; - for (let i = 0; i < 11 /*error doesn't occur with fewer nodes*/; ++i) { - newNodes.push( - createProperty(undefined, undefined, i + "", undefined, undefined, undefined)); - } - const insertAfter = findChild("x", sourceFile); - for (const newNode of newNodes) { - changeTracker.insertNodeAfter(sourceFile, insertAfter, newNode); - } - }); - } - { - const text = ` + runSingleFileTest("insertNodeAfterMultipleNodes", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + const newNodes = []; + for (let i = 0; i < 11 /*error doesn't occur with fewer nodes*/; ++i) { + newNodes.push(createProperty(undefined, undefined, i + "", undefined, undefined, undefined)); + } + const insertAfter = findChild("x", sourceFile); + for (const newNode of newNodes) { + changeTracker.insertNodeAfter(sourceFile, insertAfter, newNode); + } + }); + } + { + const text = ` class A { x } `; - runSingleFileTest("insertNodeAfterInClass1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeAfter(sourceFile, findChild("x", sourceFile), createProperty(undefined, undefined, "a", undefined, createKeywordTypeNode(SyntaxKind.BooleanKeyword), undefined)); - }); - } - { - const text = ` + runSingleFileTest("insertNodeAfterInClass1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeAfter(sourceFile, findChild("x", sourceFile), createProperty(undefined, undefined, "a", undefined, createKeywordTypeNode(SyntaxKind.BooleanKeyword), undefined)); + }); + } + { + const text = ` class A { x; } `; - runSingleFileTest("insertNodeAfterInClass2", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - changeTracker.insertNodeAfter(sourceFile, findChild("x", sourceFile), createProperty(undefined, undefined, "a", undefined, createKeywordTypeNode(SyntaxKind.BooleanKeyword), undefined)); - }); - } - { - const text = ` + runSingleFileTest("insertNodeAfterInClass2", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeAfter(sourceFile, findChild("x", sourceFile), createProperty(undefined, undefined, "a", undefined, createKeywordTypeNode(SyntaxKind.BooleanKeyword), undefined)); + }); + } + { + const text = ` class A { x; y = 1; } `; - runSingleFileTest("deleteNodeAfterInClass1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - deleteNode(changeTracker, sourceFile, findChild("x", sourceFile)); - }); - } - { - const text = ` + runSingleFileTest("deleteNodeAfterInClass1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + deleteNode(changeTracker, sourceFile, findChild("x", sourceFile)); + }); + } + { + const text = ` class A { x y = 1; } `; - runSingleFileTest("deleteNodeAfterInClass2", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - deleteNode(changeTracker, sourceFile, findChild("x", sourceFile)); - }); - } - { - const text = ` + runSingleFileTest("deleteNodeAfterInClass2", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + deleteNode(changeTracker, sourceFile, findChild("x", sourceFile)); + }); + } + { + const text = ` class A { x = foo } `; - runSingleFileTest("insertNodeInClassAfterNodeWithoutSeparator1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - const newNode = createProperty( - /*decorators*/ undefined, - /*modifiers*/ undefined, - createComputedPropertyName(createLiteral(1)), - /*questionToken*/ undefined, - createKeywordTypeNode(SyntaxKind.AnyKeyword), - /*initializer*/ undefined); - changeTracker.insertNodeAfter(sourceFile, findChild("x", sourceFile), newNode); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInClassAfterNodeWithoutSeparator1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + const newNode = createProperty( + /*decorators*/ undefined, + /*modifiers*/ undefined, createComputedPropertyName(createLiteral(1)), + /*questionToken*/ undefined, createKeywordTypeNode(SyntaxKind.AnyKeyword), + /*initializer*/ undefined); + changeTracker.insertNodeAfter(sourceFile, findChild("x", sourceFile), newNode); + }); + } + { + const text = ` class A { x() { } } `; - runSingleFileTest("insertNodeInClassAfterNodeWithoutSeparator2", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - const newNode = createProperty( - /*decorators*/ undefined, - /*modifiers*/ undefined, - createComputedPropertyName(createLiteral(1)), - /*questionToken*/ undefined, - createKeywordTypeNode(SyntaxKind.AnyKeyword), - /*initializer*/ undefined); - changeTracker.insertNodeAfter(sourceFile, findChild("x", sourceFile), newNode); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInClassAfterNodeWithoutSeparator2", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + const newNode = createProperty( + /*decorators*/ undefined, + /*modifiers*/ undefined, createComputedPropertyName(createLiteral(1)), + /*questionToken*/ undefined, createKeywordTypeNode(SyntaxKind.AnyKeyword), + /*initializer*/ undefined); + changeTracker.insertNodeAfter(sourceFile, findChild("x", sourceFile), newNode); + }); + } + { + const text = ` interface A { x } `; - runSingleFileTest("insertNodeInInterfaceAfterNodeWithoutSeparator1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - const newNode = createProperty( - /*decorators*/ undefined, - /*modifiers*/ undefined, - createComputedPropertyName(createLiteral(1)), - /*questionToken*/ undefined, - createKeywordTypeNode(SyntaxKind.AnyKeyword), - /*initializer*/ undefined); - changeTracker.insertNodeAfter(sourceFile, findChild("x", sourceFile), newNode); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInInterfaceAfterNodeWithoutSeparator1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + const newNode = createProperty( + /*decorators*/ undefined, + /*modifiers*/ undefined, createComputedPropertyName(createLiteral(1)), + /*questionToken*/ undefined, createKeywordTypeNode(SyntaxKind.AnyKeyword), + /*initializer*/ undefined); + changeTracker.insertNodeAfter(sourceFile, findChild("x", sourceFile), newNode); + }); + } + { + const text = ` interface A { x() } `; - runSingleFileTest("insertNodeInInterfaceAfterNodeWithoutSeparator2", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - const newNode = createProperty( - /*decorators*/ undefined, - /*modifiers*/ undefined, - createComputedPropertyName(createLiteral(1)), - /*questionToken*/ undefined, - createKeywordTypeNode(SyntaxKind.AnyKeyword), - /*initializer*/ undefined); - changeTracker.insertNodeAfter(sourceFile, findChild("x", sourceFile), newNode); - }); - } - { - const text = ` + runSingleFileTest("insertNodeInInterfaceAfterNodeWithoutSeparator2", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + const newNode = createProperty( + /*decorators*/ undefined, + /*modifiers*/ undefined, createComputedPropertyName(createLiteral(1)), + /*questionToken*/ undefined, createKeywordTypeNode(SyntaxKind.AnyKeyword), + /*initializer*/ undefined); + changeTracker.insertNodeAfter(sourceFile, findChild("x", sourceFile), newNode); + }); + } + { + const text = ` let x = foo `; - runSingleFileTest("insertNodeInStatementListAfterNodeWithoutSeparator1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { - const newNode = createStatement(createParen(createLiteral(1))); - changeTracker.insertNodeAfter(sourceFile, findVariableStatementContaining("x", sourceFile), newNode); - }); - } - }); -} + runSingleFileTest("insertNodeInStatementListAfterNodeWithoutSeparator1", /*placeOpenBraceOnNewLineForFunctions*/ false, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + const newNode = createStatement(createParen(createLiteral(1))); + changeTracker.insertNodeAfter(sourceFile, findVariableStatementContaining("x", sourceFile), newNode); + }); + } +}); diff --git a/src/testRunner/unittests/services/transpile.ts b/src/testRunner/unittests/services/transpile.ts index 83dd367a8656e..02f91d9533994 100644 --- a/src/testRunner/unittests/services/transpile.ts +++ b/src/testRunner/unittests/services/transpile.ts @@ -1,485 +1,379 @@ -namespace ts { - describe("unittests:: services:: Transpile", () => { - - interface TranspileTestSettings { - options?: TranspileOptions; - noSetFileName?: boolean; - } - - function transpilesCorrectly(name: string, input: string, testSettings: TranspileTestSettings) { - describe(name, () => { - let transpileResult: TranspileOutput; - let oldTranspileResult: string; - let oldTranspileDiagnostics: Diagnostic[]; - - const transpileOptions: TranspileOptions = testSettings.options || {}; - if (!transpileOptions.compilerOptions) { - transpileOptions.compilerOptions = { }; - } - if (transpileOptions.compilerOptions.target === undefined) { - transpileOptions.compilerOptions.target = ScriptTarget.ES3; - } - - if (transpileOptions.compilerOptions.newLine === undefined) { - // use \r\n as default new line - transpileOptions.compilerOptions.newLine = NewLineKind.CarriageReturnLineFeed; - } - - transpileOptions.compilerOptions.sourceMap = true; - - let unitName = transpileOptions.fileName; - if (!unitName) { - unitName = transpileOptions.compilerOptions.jsx ? "file.tsx" : "file.ts"; - if (!testSettings.noSetFileName) { - transpileOptions.fileName = unitName; - } +import { TranspileOptions, TranspileOutput, Diagnostic, ScriptTarget, NewLineKind, Extension, transpileModule, transpile, ModuleKind, JsxEmit, ModuleResolutionKind } from "../../ts"; +import { Baseline, Compiler } from "../../Harness"; +describe("unittests:: services:: Transpile", () => { + interface TranspileTestSettings { + options?: TranspileOptions; + noSetFileName?: boolean; + } + function transpilesCorrectly(name: string, input: string, testSettings: TranspileTestSettings) { + describe(name, () => { + let transpileResult: TranspileOutput; + let oldTranspileResult: string; + let oldTranspileDiagnostics: Diagnostic[]; + const transpileOptions: TranspileOptions = testSettings.options || {}; + if (!transpileOptions.compilerOptions) { + transpileOptions.compilerOptions = {}; + } + if (transpileOptions.compilerOptions.target === undefined) { + transpileOptions.compilerOptions.target = ScriptTarget.ES3; + } + if (transpileOptions.compilerOptions.newLine === undefined) { + // use \r\n as default new line + transpileOptions.compilerOptions.newLine = NewLineKind.CarriageReturnLineFeed; + } + transpileOptions.compilerOptions.sourceMap = true; + let unitName = transpileOptions.fileName; + if (!unitName) { + unitName = transpileOptions.compilerOptions.jsx ? "file.tsx" : "file.ts"; + if (!testSettings.noSetFileName) { + transpileOptions.fileName = unitName; } - - transpileOptions.reportDiagnostics = true; - - const justName = "transpile/" + name.replace(/[^a-z0-9\-. ]/ig, "") + (transpileOptions.compilerOptions.jsx ? Extension.Tsx : Extension.Ts); - const toBeCompiled = [{ + } + transpileOptions.reportDiagnostics = true; + const justName = "transpile/" + name.replace(/[^a-z0-9\-. ]/ig, "") + (transpileOptions.compilerOptions.jsx ? Extension.Tsx : Extension.Ts); + const toBeCompiled = [{ unitName, content: input }]; - const canUseOldTranspile = !transpileOptions.renamedDependencies; - - before(() => { - transpileResult = transpileModule(input, transpileOptions); - - if (canUseOldTranspile) { - oldTranspileDiagnostics = []; - oldTranspileResult = transpile(input, transpileOptions.compilerOptions, transpileOptions.fileName, oldTranspileDiagnostics, transpileOptions.moduleName); - } - }); - - after(() => { - transpileResult = undefined!; - oldTranspileResult = undefined!; - oldTranspileDiagnostics = undefined!; - }); - - /* eslint-disable no-null/no-null */ - it("Correct errors for " + justName, () => { - Harness.Baseline.runBaseline(justName.replace(/\.tsx?$/, ".errors.txt"), - transpileResult.diagnostics!.length === 0 ? null : Harness.Compiler.getErrorBaseline(toBeCompiled, transpileResult.diagnostics!)); - }); - - if (canUseOldTranspile) { - it("Correct errors (old transpile) for " + justName, () => { - Harness.Baseline.runBaseline(justName.replace(/\.tsx?$/, ".oldTranspile.errors.txt"), - oldTranspileDiagnostics.length === 0 ? null : Harness.Compiler.getErrorBaseline(toBeCompiled, oldTranspileDiagnostics)); - }); - } - /* eslint-enable no-null/no-null */ - - it("Correct output for " + justName, () => { - Harness.Baseline.runBaseline(justName.replace(/\.tsx?$/, Extension.Js), transpileResult.outputText); - }); - + const canUseOldTranspile = !transpileOptions.renamedDependencies; + before(() => { + transpileResult = transpileModule(input, transpileOptions); if (canUseOldTranspile) { - it("Correct output (old transpile) for " + justName, () => { - Harness.Baseline.runBaseline(justName.replace(/\.tsx?$/, ".oldTranspile.js"), oldTranspileResult); - }); + oldTranspileDiagnostics = []; + oldTranspileResult = transpile(input, transpileOptions.compilerOptions, transpileOptions.fileName, oldTranspileDiagnostics, transpileOptions.moduleName); } }); - } - - transpilesCorrectly("Generates no diagnostics with valid inputs", `var x = 0;`, { - options: { compilerOptions: { module: ModuleKind.CommonJS } } - }); - - transpilesCorrectly("Generates no diagnostics for missing file references", `/// -var x = 0;`, { - options: { compilerOptions: { module: ModuleKind.CommonJS } } - }); - - transpilesCorrectly("Generates no diagnostics for missing module imports", `import {a} from "module2";`, { - options: { compilerOptions: { module: ModuleKind.CommonJS } } - }); - - transpilesCorrectly("Generates expected syntactic diagnostics", `a b`, { - options: { compilerOptions: { module: ModuleKind.CommonJS } } - }); - - transpilesCorrectly("Does not generate semantic diagnostics", `var x: string = 0;`, { - options: { compilerOptions: { module: ModuleKind.CommonJS } } - }); - - transpilesCorrectly("Generates module output", `var x = 0;`, { - options: { compilerOptions: { module: ModuleKind.AMD } } - }); - - transpilesCorrectly("Uses correct newLine character", `var x = 0;`, { - options: { compilerOptions: { module: ModuleKind.CommonJS, newLine: NewLineKind.LineFeed } } - }); - - transpilesCorrectly("Sets module name", "var x = 1;", { - options: { compilerOptions: { module: ModuleKind.System, newLine: NewLineKind.LineFeed }, moduleName: "NamedModule" } - }); - - transpilesCorrectly("No extra errors for file without extension", `"use strict";\r\nvar x = 0;`, { - options: { compilerOptions: { module: ModuleKind.CommonJS }, fileName: "file" } - }); - - transpilesCorrectly("Rename dependencies - System", - `import {foo} from "SomeName";\n` + - `declare function use(a: any);\n` + - `use(foo);`, { - options: { compilerOptions: { module: ModuleKind.System, newLine: NewLineKind.LineFeed }, renamedDependencies: { SomeName: "SomeOtherName" } } - }); - - transpilesCorrectly("Rename dependencies - AMD", - `import {foo} from "SomeName";\n` + - `declare function use(a: any);\n` + - `use(foo);`, { - options: { compilerOptions: { module: ModuleKind.AMD, newLine: NewLineKind.LineFeed }, renamedDependencies: { SomeName: "SomeOtherName" } } + after(() => { + transpileResult = undefined!; + oldTranspileResult = undefined!; + oldTranspileDiagnostics = undefined!; }); - - transpilesCorrectly("Rename dependencies - UMD", - `import {foo} from "SomeName";\n` + - `declare function use(a: any);\n` + - `use(foo);`, { - options: { compilerOptions: { module: ModuleKind.UMD, newLine: NewLineKind.LineFeed }, renamedDependencies: { SomeName: "SomeOtherName" } } + /* eslint-disable no-null/no-null */ + it("Correct errors for " + justName, () => { + Baseline.runBaseline(justName.replace(/\.tsx?$/, ".errors.txt"), transpileResult.diagnostics!.length === 0 ? null : Compiler.getErrorBaseline(toBeCompiled, (transpileResult.diagnostics!))); }); - - transpilesCorrectly("Transpile with emit decorators and emit metadata", - `import {db} from './db';\n` + - `function someDecorator(target) {\n` + - ` return target;\n` + - `} \n` + - `@someDecorator\n` + - `class MyClass {\n` + - ` db: db;\n` + - ` constructor(db: db) {\n` + - ` this.db = db;\n` + - ` this.db.doSomething(); \n` + - ` }\n` + - `}\n` + - `export {MyClass}; \n`, { - options: { - compilerOptions: { - module: ModuleKind.CommonJS, - newLine: NewLineKind.LineFeed, - noEmitHelpers: true, - emitDecoratorMetadata: true, - experimentalDecorators: true, - target: ScriptTarget.ES5, - } - } + if (canUseOldTranspile) { + it("Correct errors (old transpile) for " + justName, () => { + Baseline.runBaseline(justName.replace(/\.tsx?$/, ".oldTranspile.errors.txt"), oldTranspileDiagnostics.length === 0 ? null : Compiler.getErrorBaseline(toBeCompiled, oldTranspileDiagnostics)); + }); + } + /* eslint-enable no-null/no-null */ + it("Correct output for " + justName, () => { + Baseline.runBaseline(justName.replace(/\.tsx?$/, Extension.Js), transpileResult.outputText); }); - - transpilesCorrectly("Supports backslashes in file name", "var x", { - options: { fileName: "a\\b.ts" } - }); - - transpilesCorrectly("transpile file as 'tsx' if 'jsx' is specified", `var x =
`, { - options: { compilerOptions: { jsx: JsxEmit.React, newLine: NewLineKind.LineFeed } } - }); - - transpilesCorrectly("transpile .js files", "const a = 10;", { - options: { compilerOptions: { newLine: NewLineKind.LineFeed, module: ModuleKind.CommonJS }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports urls in file name", "var x", { - options: { fileName: "http://somewhere/directory//directory2/file.ts" } - }); - - transpilesCorrectly("Accepts string as enum values for compile-options", "export const x = 0", { - options: { - compilerOptions: { - module: "es6", - // Capitalization and spaces ignored - target: " Es6 " - } + if (canUseOldTranspile) { + it("Correct output (old transpile) for " + justName, () => { + Baseline.runBaseline(justName.replace(/\.tsx?$/, ".oldTranspile.js"), oldTranspileResult); + }); } }); - - transpilesCorrectly("Report an error when compiler-options module-kind is out-of-range", "", { - options: { compilerOptions: { module: 123 } } - }); - - transpilesCorrectly("Report an error when compiler-options target-script is out-of-range", "", { - options: { compilerOptions: { module: 123 } } - }); - - transpilesCorrectly("Support options with lib values", "const a = 10;", { - options: { compilerOptions: { lib: ["es6", "dom"], module: ModuleKind.CommonJS }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Support options with types values", "const a = 10;", { - options: { compilerOptions: { types: ["jquery", "typescript"], module: ModuleKind.CommonJS }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'allowJs'", "x;", { - options: { compilerOptions: { allowJs: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'allowSyntheticDefaultImports'", "x;", { - options: { compilerOptions: { allowSyntheticDefaultImports: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'allowUnreachableCode'", "x;", { - options: { compilerOptions: { allowUnreachableCode: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'allowUnusedLabels'", "x;", { - options: { compilerOptions: { allowUnusedLabels: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'alwaysStrict'", "x;", { - options: { compilerOptions: { alwaysStrict: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'baseUrl'", "x;", { - options: { compilerOptions: { baseUrl: "./folder/baseUrl" }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'charset'", "x;", { - options: { compilerOptions: { charset: "en-us" }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'declaration'", "x;", { - options: { compilerOptions: { declaration: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'declarationDir'", "x;", { - options: { compilerOptions: { declarationDir: "out/declarations" }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'emitBOM'", "x;", { - options: { compilerOptions: { emitBOM: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'emitDecoratorMetadata'", "x;", { - options: { compilerOptions: { emitDecoratorMetadata: true, experimentalDecorators: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'experimentalDecorators'", "x;", { - options: { compilerOptions: { experimentalDecorators: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'forceConsistentCasingInFileNames'", "x;", { - options: { compilerOptions: { forceConsistentCasingInFileNames: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'isolatedModules'", "x;", { - options: { compilerOptions: { isolatedModules: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'jsx'", "x;", { - options: { compilerOptions: { jsx: 1 }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'lib'", "x;", { - options: { compilerOptions: { lib: ["es2015", "dom"] }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'locale'", "x;", { - options: { compilerOptions: { locale: "en-us" }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'module'", "x;", { - options: { compilerOptions: { module: ModuleKind.CommonJS }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'moduleResolution'", "x;", { - options: { compilerOptions: { moduleResolution: ModuleResolutionKind.NodeJs }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'newLine'", "x;", { - options: { compilerOptions: { newLine: NewLineKind.CarriageReturnLineFeed }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'noEmit'", "x;", { - options: { compilerOptions: { noEmit: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'noEmitHelpers'", "x;", { - options: { compilerOptions: { noEmitHelpers: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'noEmitOnError'", "x;", { - options: { compilerOptions: { noEmitOnError: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'noErrorTruncation'", "x;", { - options: { compilerOptions: { noErrorTruncation: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'noFallthroughCasesInSwitch'", "x;", { - options: { compilerOptions: { noFallthroughCasesInSwitch: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'noImplicitAny'", "x;", { - options: { compilerOptions: { noImplicitAny: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'noImplicitReturns'", "x;", { - options: { compilerOptions: { noImplicitReturns: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'noImplicitThis'", "x;", { - options: { compilerOptions: { noImplicitThis: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'noImplicitUseStrict'", "x;", { - options: { compilerOptions: { noImplicitUseStrict: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'noLib'", "x;", { - options: { compilerOptions: { noLib: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'noResolve'", "x;", { - options: { compilerOptions: { noResolve: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'out'", "x;", { - options: { compilerOptions: { out: "./out" }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'outDir'", "x;", { - options: { compilerOptions: { outDir: "./outDir" }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'outFile'", "x;", { - options: { compilerOptions: { outFile: "./outFile" }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'paths'", "x;", { - options: { compilerOptions: { paths: { "*": ["./generated*"] } }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'preserveConstEnums'", "x;", { - options: { compilerOptions: { preserveConstEnums: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'reactNamespace'", "x;", { - options: { compilerOptions: { reactNamespace: "react" }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'jsxFactory'", "x;", { - options: { compilerOptions: { jsxFactory: "createElement" }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'removeComments'", "x;", { - options: { compilerOptions: { removeComments: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'rootDir'", "x;", { - options: { compilerOptions: { rootDir: "./rootDir" }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'rootDirs'", "x;", { - options: { compilerOptions: { rootDirs: ["./a", "./b"] }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'skipLibCheck'", "x;", { - options: { compilerOptions: { skipLibCheck: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'skipDefaultLibCheck'", "x;", { - options: { compilerOptions: { skipDefaultLibCheck: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'strictNullChecks'", "x;", { - options: { compilerOptions: { strictNullChecks: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'stripInternal'", "x;", { - options: { compilerOptions: { stripInternal: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'suppressExcessPropertyErrors'", "x;", { - options: { compilerOptions: { suppressExcessPropertyErrors: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'suppressImplicitAnyIndexErrors'", "x;", { - options: { compilerOptions: { suppressImplicitAnyIndexErrors: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'target'", "x;", { - options: { compilerOptions: { target: 2 }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'types'", "x;", { - options: { compilerOptions: { types: ["jquery", "jasmine"] }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'typeRoots'", "x;", { - options: { compilerOptions: { typeRoots: ["./folder"] }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'incremental'", "x;", { - options: { compilerOptions: { incremental: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'composite'", "x;", { - options: { compilerOptions: { composite: true }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Supports setting 'tsbuildinfo'", "x;", { - options: { compilerOptions: { incremental: true, tsBuildInfoFile: "./folder/config.tsbuildinfo" }, fileName: "input.js", reportDiagnostics: true } - }); - - transpilesCorrectly("Correctly serialize metadata when transpile with CommonJS option", - `import * as ng from "angular2/core";` + - `declare function foo(...args: any[]);` + - `@foo` + - `export class MyClass1 {` + - ` constructor(private _elementRef: ng.ElementRef){}` + - `}`, { - options: { - compilerOptions: { - target: ScriptTarget.ES5, - module: ModuleKind.CommonJS, - moduleResolution: ModuleResolutionKind.NodeJs, - emitDecoratorMetadata: true, - experimentalDecorators: true, - isolatedModules: true, - } - } + } + transpilesCorrectly("Generates no diagnostics with valid inputs", `var x = 0;`, { + options: { compilerOptions: { module: ModuleKind.CommonJS } } + }); + transpilesCorrectly("Generates no diagnostics for missing file references", `/// +var x = 0;`, { + options: { compilerOptions: { module: ModuleKind.CommonJS } } + }); + transpilesCorrectly("Generates no diagnostics for missing module imports", `import {a} from "module2";`, { + options: { compilerOptions: { module: ModuleKind.CommonJS } } + }); + transpilesCorrectly("Generates expected syntactic diagnostics", `a b`, { + options: { compilerOptions: { module: ModuleKind.CommonJS } } + }); + transpilesCorrectly("Does not generate semantic diagnostics", `var x: string = 0;`, { + options: { compilerOptions: { module: ModuleKind.CommonJS } } + }); + transpilesCorrectly("Generates module output", `var x = 0;`, { + options: { compilerOptions: { module: ModuleKind.AMD } } + }); + transpilesCorrectly("Uses correct newLine character", `var x = 0;`, { + options: { compilerOptions: { module: ModuleKind.CommonJS, newLine: NewLineKind.LineFeed } } + }); + transpilesCorrectly("Sets module name", "var x = 1;", { + options: { compilerOptions: { module: ModuleKind.System, newLine: NewLineKind.LineFeed }, moduleName: "NamedModule" } + }); + transpilesCorrectly("No extra errors for file without extension", `"use strict";\r\nvar x = 0;`, { + options: { compilerOptions: { module: ModuleKind.CommonJS }, fileName: "file" } + }); + transpilesCorrectly("Rename dependencies - System", `import {foo} from "SomeName";\n` + + `declare function use(a: any);\n` + + `use(foo);`, { + options: { compilerOptions: { module: ModuleKind.System, newLine: NewLineKind.LineFeed }, renamedDependencies: { SomeName: "SomeOtherName" } } + }); + transpilesCorrectly("Rename dependencies - AMD", `import {foo} from "SomeName";\n` + + `declare function use(a: any);\n` + + `use(foo);`, { + options: { compilerOptions: { module: ModuleKind.AMD, newLine: NewLineKind.LineFeed }, renamedDependencies: { SomeName: "SomeOtherName" } } + }); + transpilesCorrectly("Rename dependencies - UMD", `import {foo} from "SomeName";\n` + + `declare function use(a: any);\n` + + `use(foo);`, { + options: { compilerOptions: { module: ModuleKind.UMD, newLine: NewLineKind.LineFeed }, renamedDependencies: { SomeName: "SomeOtherName" } } + }); + transpilesCorrectly("Transpile with emit decorators and emit metadata", `import {db} from './db';\n` + + `function someDecorator(target) {\n` + + ` return target;\n` + + `} \n` + + `@someDecorator\n` + + `class MyClass {\n` + + ` db: db;\n` + + ` constructor(db: db) {\n` + + ` this.db = db;\n` + + ` this.db.doSomething(); \n` + + ` }\n` + + `}\n` + + `export {MyClass}; \n`, { + options: { + compilerOptions: { + module: ModuleKind.CommonJS, + newLine: NewLineKind.LineFeed, + noEmitHelpers: true, + emitDecoratorMetadata: true, + experimentalDecorators: true, + target: ScriptTarget.ES5, } - ); - - transpilesCorrectly("Correctly serialize metadata when transpile with System option", - `import * as ng from "angular2/core";` + - `declare function foo(...args: any[]);` + - `@foo` + - `export class MyClass1 {` + - ` constructor(private _elementRef: ng.ElementRef){}` + - `}`, { - options: { - compilerOptions: { - target: ScriptTarget.ES5, - module: ModuleKind.System, - moduleResolution: ModuleResolutionKind.NodeJs, - emitDecoratorMetadata: true, - experimentalDecorators: true, - isolatedModules: true, - } - } + } + }); + transpilesCorrectly("Supports backslashes in file name", "var x", { + options: { fileName: "a\\b.ts" } + }); + transpilesCorrectly("transpile file as 'tsx' if 'jsx' is specified", `var x =
`, { + options: { compilerOptions: { jsx: JsxEmit.React, newLine: NewLineKind.LineFeed } } + }); + transpilesCorrectly("transpile .js files", "const a = 10;", { + options: { compilerOptions: { newLine: NewLineKind.LineFeed, module: ModuleKind.CommonJS }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports urls in file name", "var x", { + options: { fileName: "http://somewhere/directory//directory2/file.ts" } + }); + transpilesCorrectly("Accepts string as enum values for compile-options", "export const x = 0", { + options: { + compilerOptions: { + module: ("es6"), + // Capitalization and spaces ignored + target: (" Es6 ") } - ); - - transpilesCorrectly("Supports readonly keyword for arrays", "let x: readonly string[];", { - options: { compilerOptions: { module: ModuleKind.CommonJS } } - }); - - transpilesCorrectly("Supports 'as const' arrays", `([] as const).forEach(k => console.log(k));`, { - options: { compilerOptions: { module: ModuleKind.CommonJS } } - }); - - transpilesCorrectly("Infer correct file extension", `const fn = (a: T) => a`, { - noSetFileName: true - }); - - transpilesCorrectly("Export star as ns conflict does not crash", ` + } + }); + transpilesCorrectly("Report an error when compiler-options module-kind is out-of-range", "", { + options: { compilerOptions: { module: (123) } } + }); + transpilesCorrectly("Report an error when compiler-options target-script is out-of-range", "", { + options: { compilerOptions: { module: (123) } } + }); + transpilesCorrectly("Support options with lib values", "const a = 10;", { + options: { compilerOptions: { lib: ["es6", "dom"], module: ModuleKind.CommonJS }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Support options with types values", "const a = 10;", { + options: { compilerOptions: { types: ["jquery", "typescript"], module: ModuleKind.CommonJS }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'allowJs'", "x;", { + options: { compilerOptions: { allowJs: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'allowSyntheticDefaultImports'", "x;", { + options: { compilerOptions: { allowSyntheticDefaultImports: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'allowUnreachableCode'", "x;", { + options: { compilerOptions: { allowUnreachableCode: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'allowUnusedLabels'", "x;", { + options: { compilerOptions: { allowUnusedLabels: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'alwaysStrict'", "x;", { + options: { compilerOptions: { alwaysStrict: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'baseUrl'", "x;", { + options: { compilerOptions: { baseUrl: "./folder/baseUrl" }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'charset'", "x;", { + options: { compilerOptions: { charset: "en-us" }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'declaration'", "x;", { + options: { compilerOptions: { declaration: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'declarationDir'", "x;", { + options: { compilerOptions: { declarationDir: "out/declarations" }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'emitBOM'", "x;", { + options: { compilerOptions: { emitBOM: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'emitDecoratorMetadata'", "x;", { + options: { compilerOptions: { emitDecoratorMetadata: true, experimentalDecorators: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'experimentalDecorators'", "x;", { + options: { compilerOptions: { experimentalDecorators: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'forceConsistentCasingInFileNames'", "x;", { + options: { compilerOptions: { forceConsistentCasingInFileNames: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'isolatedModules'", "x;", { + options: { compilerOptions: { isolatedModules: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'jsx'", "x;", { + options: { compilerOptions: { jsx: 1 }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'lib'", "x;", { + options: { compilerOptions: { lib: ["es2015", "dom"] }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'locale'", "x;", { + options: { compilerOptions: { locale: "en-us" }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'module'", "x;", { + options: { compilerOptions: { module: ModuleKind.CommonJS }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'moduleResolution'", "x;", { + options: { compilerOptions: { moduleResolution: ModuleResolutionKind.NodeJs }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'newLine'", "x;", { + options: { compilerOptions: { newLine: NewLineKind.CarriageReturnLineFeed }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'noEmit'", "x;", { + options: { compilerOptions: { noEmit: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'noEmitHelpers'", "x;", { + options: { compilerOptions: { noEmitHelpers: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'noEmitOnError'", "x;", { + options: { compilerOptions: { noEmitOnError: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'noErrorTruncation'", "x;", { + options: { compilerOptions: { noErrorTruncation: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'noFallthroughCasesInSwitch'", "x;", { + options: { compilerOptions: { noFallthroughCasesInSwitch: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'noImplicitAny'", "x;", { + options: { compilerOptions: { noImplicitAny: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'noImplicitReturns'", "x;", { + options: { compilerOptions: { noImplicitReturns: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'noImplicitThis'", "x;", { + options: { compilerOptions: { noImplicitThis: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'noImplicitUseStrict'", "x;", { + options: { compilerOptions: { noImplicitUseStrict: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'noLib'", "x;", { + options: { compilerOptions: { noLib: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'noResolve'", "x;", { + options: { compilerOptions: { noResolve: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'out'", "x;", { + options: { compilerOptions: { out: "./out" }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'outDir'", "x;", { + options: { compilerOptions: { outDir: "./outDir" }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'outFile'", "x;", { + options: { compilerOptions: { outFile: "./outFile" }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'paths'", "x;", { + options: { compilerOptions: { paths: { "*": ["./generated*"] } }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'preserveConstEnums'", "x;", { + options: { compilerOptions: { preserveConstEnums: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'reactNamespace'", "x;", { + options: { compilerOptions: { reactNamespace: "react" }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'jsxFactory'", "x;", { + options: { compilerOptions: { jsxFactory: "createElement" }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'removeComments'", "x;", { + options: { compilerOptions: { removeComments: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'rootDir'", "x;", { + options: { compilerOptions: { rootDir: "./rootDir" }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'rootDirs'", "x;", { + options: { compilerOptions: { rootDirs: ["./a", "./b"] }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'skipLibCheck'", "x;", { + options: { compilerOptions: { skipLibCheck: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'skipDefaultLibCheck'", "x;", { + options: { compilerOptions: { skipDefaultLibCheck: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'strictNullChecks'", "x;", { + options: { compilerOptions: { strictNullChecks: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'stripInternal'", "x;", { + options: { compilerOptions: { stripInternal: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'suppressExcessPropertyErrors'", "x;", { + options: { compilerOptions: { suppressExcessPropertyErrors: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'suppressImplicitAnyIndexErrors'", "x;", { + options: { compilerOptions: { suppressImplicitAnyIndexErrors: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'target'", "x;", { + options: { compilerOptions: { target: 2 }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'types'", "x;", { + options: { compilerOptions: { types: ["jquery", "jasmine"] }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'typeRoots'", "x;", { + options: { compilerOptions: { typeRoots: ["./folder"] }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'incremental'", "x;", { + options: { compilerOptions: { incremental: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'composite'", "x;", { + options: { compilerOptions: { composite: true }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Supports setting 'tsbuildinfo'", "x;", { + options: { compilerOptions: { incremental: true, tsBuildInfoFile: "./folder/config.tsbuildinfo" }, fileName: "input.js", reportDiagnostics: true } + }); + transpilesCorrectly("Correctly serialize metadata when transpile with CommonJS option", `import * as ng from "angular2/core";` + + `declare function foo(...args: any[]);` + + `@foo` + + `export class MyClass1 {` + + ` constructor(private _elementRef: ng.ElementRef){}` + + `}`, { + options: { + compilerOptions: { + target: ScriptTarget.ES5, + module: ModuleKind.CommonJS, + moduleResolution: ModuleResolutionKind.NodeJs, + emitDecoratorMetadata: true, + experimentalDecorators: true, + isolatedModules: true, + } + } + }); + transpilesCorrectly("Correctly serialize metadata when transpile with System option", `import * as ng from "angular2/core";` + + `declare function foo(...args: any[]);` + + `@foo` + + `export class MyClass1 {` + + ` constructor(private _elementRef: ng.ElementRef){}` + + `}`, { + options: { + compilerOptions: { + target: ScriptTarget.ES5, + module: ModuleKind.System, + moduleResolution: ModuleResolutionKind.NodeJs, + emitDecoratorMetadata: true, + experimentalDecorators: true, + isolatedModules: true, + } + } + }); + transpilesCorrectly("Supports readonly keyword for arrays", "let x: readonly string[];", { + options: { compilerOptions: { module: ModuleKind.CommonJS } } + }); + transpilesCorrectly("Supports 'as const' arrays", `([] as const).forEach(k => console.log(k));`, { + options: { compilerOptions: { module: ModuleKind.CommonJS } } + }); + transpilesCorrectly("Infer correct file extension", `const fn = (a: T) => a`, { + noSetFileName: true + }); + transpilesCorrectly("Export star as ns conflict does not crash", ` var a; export { a as alias }; export * as alias from './file';`, { - noSetFileName: true - }); + noSetFileName: true }); -} +}); diff --git a/src/testRunner/unittests/transform.ts b/src/testRunner/unittests/transform.ts index cce8c92ad4183..753879473a474 100644 --- a/src/testRunner/unittests/transform.ts +++ b/src/testRunner/unittests/transform.ts @@ -1,401 +1,367 @@ -namespace ts { - describe("unittests:: TransformAPI", () => { - function replaceUndefinedWithVoid0(context: TransformationContext) { - const previousOnSubstituteNode = context.onSubstituteNode; - context.enableSubstitution(SyntaxKind.Identifier); - context.onSubstituteNode = (hint, node) => { - node = previousOnSubstituteNode(hint, node); - if (hint === EmitHint.Expression && isIdentifier(node) && node.escapedText === "undefined") { - node = createPartiallyEmittedExpression( - addSyntheticTrailingComment( - setTextRange( - createVoidZero(), - node), - SyntaxKind.MultiLineCommentTrivia, "undefined")); - } - return node; - }; - return (file: SourceFile) => file; - } - function replaceNumberWith2(context: TransformationContext) { - function visitor(node: Node): Node { - if (isNumericLiteral(node)) { - return createNumericLiteral("2"); - } - return visitEachChild(node, visitor, context); +import { TransformationContext, SyntaxKind, EmitHint, isIdentifier, createPartiallyEmittedExpression, addSyntheticTrailingComment, setTextRange, createVoidZero, SourceFile, Node, isNumericLiteral, createNumericLiteral, visitEachChild, visitNode, createIdentifier, Visitor, TransformerFactory, transform, createSourceFile, ScriptTarget, createPrinter, NewLineKind, createBundle, VisitResult, createKeywordTypeNode, transpileModule, getMutableClone, createNodeArray, createClassDeclaration, createModuleDeclaration, createModuleBlock, createEmptyStatement, ModuleBlock, updateModuleBlock, ExportDeclaration, createExportSpecifier, createNamedExports, updateExportDeclaration, ModuleKind, createImportDeclaration, createImportClause, createNamespaceImport, createLiteral, updateSourceFileNode, createMethod, createDecorator, createBlock, createConstructor, createParameter, createModifier, TranspileOptions, createProgram, isSourceFile, setEmitFlags, EmitFlags, setSyntheticLeadingComments, isVoidExpression, isVariableStatement, isImportDeclaration, isExportDeclaration, isImportSpecifier, isExportSpecifier, isPropertyDeclaration, isParameterPropertyDeclaration, isClassDeclaration, isConstructorDeclaration, isModuleDeclaration, isVariableDeclaration, updateVariableDeclaration, isMethodDeclaration, updateMethod, Transformer, isNoSubstitutionTemplateLiteral, createNoSubstitutionTemplateLiteral } from "../ts"; +import { Baseline, IO } from "../Harness"; +import { evaluateJavaScript } from "../evaluator"; +import { createFromFileSystem } from "../vfs"; +import { TextDocument } from "../documents"; +import { CompilerHost } from "../fakes"; +describe("unittests:: TransformAPI", () => { + function replaceUndefinedWithVoid0(context: TransformationContext) { + const previousOnSubstituteNode = context.onSubstituteNode; + context.enableSubstitution(SyntaxKind.Identifier); + context.onSubstituteNode = (hint, node) => { + node = previousOnSubstituteNode(hint, node); + if (hint === EmitHint.Expression && isIdentifier(node) && node.escapedText === "undefined") { + node = createPartiallyEmittedExpression(addSyntheticTrailingComment(setTextRange(createVoidZero(), node), SyntaxKind.MultiLineCommentTrivia, "undefined")); } - return (file: SourceFile) => visitNode(file, visitor); - } - - function replaceIdentifiersNamedOldNameWithNewName(context: TransformationContext) { - const previousOnSubstituteNode = context.onSubstituteNode; - context.enableSubstitution(SyntaxKind.Identifier); - context.onSubstituteNode = (hint, node) => { - node = previousOnSubstituteNode(hint, node); - if (isIdentifier(node) && node.escapedText === "oldName") { - node = setTextRange(createIdentifier("newName"), node); - } - return node; - }; - return (file: SourceFile) => file; - } - - function replaceIdentifiersNamedOldNameWithNewName2(context: TransformationContext) { - const visitor: Visitor = (node) => { - if (isIdentifier(node) && node.text === "oldName") { - return createIdentifier("newName"); - } - return visitEachChild(node, visitor, context); - }; - return (node: SourceFile) => visitNode(node, visitor); + return node; + }; + return (file: SourceFile) => file; + } + function replaceNumberWith2(context: TransformationContext) { + function visitor(node: Node): Node { + if (isNumericLiteral(node)) { + return createNumericLiteral("2"); + } + return visitEachChild(node, visitor, context); } - - function transformSourceFile(sourceText: string, transformers: TransformerFactory[]) { - const transformed = transform(createSourceFile("source.ts", sourceText, ScriptTarget.ES2015), transformers); - const printer = createPrinter({ newLine: NewLineKind.CarriageReturnLineFeed }, { - onEmitNode: transformed.emitNodeWithNotification, - substituteNode: transformed.substituteNode + return (file: SourceFile) => visitNode(file, visitor); + } + function replaceIdentifiersNamedOldNameWithNewName(context: TransformationContext) { + const previousOnSubstituteNode = context.onSubstituteNode; + context.enableSubstitution(SyntaxKind.Identifier); + context.onSubstituteNode = (hint, node) => { + node = previousOnSubstituteNode(hint, node); + if (isIdentifier(node) && node.escapedText === "oldName") { + node = setTextRange(createIdentifier("newName"), node); + } + return node; + }; + return (file: SourceFile) => file; + } + function replaceIdentifiersNamedOldNameWithNewName2(context: TransformationContext) { + const visitor: Visitor = (node) => { + if (isIdentifier(node) && node.text === "oldName") { + return createIdentifier("newName"); + } + return visitEachChild(node, visitor, context); + }; + return (node: SourceFile) => visitNode(node, visitor); + } + function transformSourceFile(sourceText: string, transformers: TransformerFactory[]) { + const transformed = transform(createSourceFile("source.ts", sourceText, ScriptTarget.ES2015), transformers); + const printer = createPrinter({ newLine: NewLineKind.CarriageReturnLineFeed }, { + onEmitNode: transformed.emitNodeWithNotification, + substituteNode: transformed.substituteNode + }); + const result = printer.printBundle(createBundle(transformed.transformed)); + transformed.dispose(); + return result; + } + function testBaseline(testName: string, test: () => string) { + it(testName, () => { + Baseline.runBaseline(`transformApi/transformsCorrectly.${testName}.js`, test()); + }); + } + function testBaselineAndEvaluate(testName: string, test: () => string, onEvaluate: (exports: any) => void) { + describe(testName, () => { + let sourceText!: string; + before(() => { + sourceText = test(); }); - const result = printer.printBundle(createBundle(transformed.transformed)); - transformed.dispose(); - return result; - } - - function testBaseline(testName: string, test: () => string) { - it(testName, () => { - Harness.Baseline.runBaseline(`transformApi/transformsCorrectly.${testName}.js`, test()); + after(() => { + sourceText = undefined!; }); - } - - function testBaselineAndEvaluate(testName: string, test: () => string, onEvaluate: (exports: any) => void) { - describe(testName, () => { - let sourceText!: string; - before(() => { - sourceText = test(); - }); - after(() => { - sourceText = undefined!; - }); - it("compare baselines", () => { - Harness.Baseline.runBaseline(`transformApi/transformsCorrectly.${testName}.js`, sourceText); - }); - it("evaluate", () => { - onEvaluate(evaluator.evaluateJavaScript(sourceText)); - }); + it("compare baselines", () => { + Baseline.runBaseline(`transformApi/transformsCorrectly.${testName}.js`, sourceText); + }); + it("evaluate", () => { + onEvaluate(evaluateJavaScript(sourceText)); }); - } - - testBaseline("substitution", () => { - return transformSourceFile(`var a = undefined;`, [replaceUndefinedWithVoid0]); - }); - - testBaseline("types", () => { - return transformSourceFile(`let a: () => void`, [ - context => file => visitNode(file, function visitor(node: Node): VisitResult { - return visitEachChild(node, visitor, context); - }) - ]); - }); - - testBaseline("transformDefiniteAssignmentAssertions", () => { - return transformSourceFile(`let a!: () => void`, [ - context => file => visitNode(file, function visitor(node: Node): VisitResult { - if (node.kind === SyntaxKind.VoidKeyword) { - return createKeywordTypeNode(SyntaxKind.UndefinedKeyword); - } - return visitEachChild(node, visitor, context); - }) - ]); - }); - - testBaseline("fromTranspileModule", () => { - return transpileModule(`var oldName = undefined;`, { - transformers: { - before: [replaceUndefinedWithVoid0], - after: [replaceIdentifiersNamedOldNameWithNewName] - }, - compilerOptions: { - newLine: NewLineKind.CarriageReturnLineFeed - } - }).outputText; - }); - - testBaseline("issue27854", () => { - return transpileModule(`oldName<{ a: string; }>\` ... \`;`, { - transformers: { - before: [replaceIdentifiersNamedOldNameWithNewName2] - }, - compilerOptions: { - newLine: NewLineKind.CarriageReturnLineFeed, - target: ScriptTarget.Latest - } - }).outputText; }); - - testBaseline("rewrittenNamespace", () => { - return transpileModule(`namespace Reflect { const x = 1; }`, { - transformers: { - before: [forceNamespaceRewrite], - }, - compilerOptions: { - newLine: NewLineKind.CarriageReturnLineFeed, + } + testBaseline("substitution", () => { + return transformSourceFile(`var a = undefined;`, [replaceUndefinedWithVoid0]); + }); + testBaseline("types", () => { + return transformSourceFile(`let a: () => void`, [ + context => file => visitNode(file, function visitor(node: Node): VisitResult { + return visitEachChild(node, visitor, context); + }) + ]); + }); + testBaseline("transformDefiniteAssignmentAssertions", () => { + return transformSourceFile(`let a!: () => void`, [ + context => file => visitNode(file, function visitor(node: Node): VisitResult { + if (node.kind === SyntaxKind.VoidKeyword) { + return createKeywordTypeNode(SyntaxKind.UndefinedKeyword); } - }).outputText; - }); - - testBaseline("rewrittenNamespaceFollowingClass", () => { - return transpileModule(` + return visitEachChild(node, visitor, context); + }) + ]); + }); + testBaseline("fromTranspileModule", () => { + return transpileModule(`var oldName = undefined;`, { + transformers: { + before: [replaceUndefinedWithVoid0], + after: [replaceIdentifiersNamedOldNameWithNewName] + }, + compilerOptions: { + newLine: NewLineKind.CarriageReturnLineFeed + } + }).outputText; + }); + testBaseline("issue27854", () => { + return transpileModule(`oldName<{ a: string; }>\` ... \`;`, { + transformers: { + before: [replaceIdentifiersNamedOldNameWithNewName2] + }, + compilerOptions: { + newLine: NewLineKind.CarriageReturnLineFeed, + target: ScriptTarget.Latest + } + }).outputText; + }); + testBaseline("rewrittenNamespace", () => { + return transpileModule(`namespace Reflect { const x = 1; }`, { + transformers: { + before: [forceNamespaceRewrite], + }, + compilerOptions: { + newLine: NewLineKind.CarriageReturnLineFeed, + } + }).outputText; + }); + testBaseline("rewrittenNamespaceFollowingClass", () => { + return transpileModule(` class C { foo = 10; static bar = 20 } namespace C { export let x = 10; } `, { - transformers: { - before: [forceNamespaceRewrite], - }, - compilerOptions: { - target: ScriptTarget.ESNext, - newLine: NewLineKind.CarriageReturnLineFeed, - } - }).outputText; - }); - - testBaseline("transformTypesInExportDefault", () => { - return transpileModule(` + transformers: { + before: [forceNamespaceRewrite], + }, + compilerOptions: { + target: ScriptTarget.ESNext, + newLine: NewLineKind.CarriageReturnLineFeed, + } + }).outputText; + }); + testBaseline("transformTypesInExportDefault", () => { + return transpileModule(` export default (foo: string) => { return 1; } `, { - transformers: { - before: [replaceNumberWith2], - }, - compilerOptions: { - target: ScriptTarget.ESNext, - newLine: NewLineKind.CarriageReturnLineFeed, - } - }).outputText; - }); - - testBaseline("synthesizedClassAndNamespaceCombination", () => { - return transpileModule("", { - transformers: { - before: [replaceWithClassAndNamespace], - }, - compilerOptions: { - target: ScriptTarget.ESNext, - newLine: NewLineKind.CarriageReturnLineFeed, + transformers: { + before: [replaceNumberWith2], + }, + compilerOptions: { + target: ScriptTarget.ESNext, + newLine: NewLineKind.CarriageReturnLineFeed, + } + }).outputText; + }); + testBaseline("synthesizedClassAndNamespaceCombination", () => { + return transpileModule("", { + transformers: { + before: [replaceWithClassAndNamespace], + }, + compilerOptions: { + target: ScriptTarget.ESNext, + newLine: NewLineKind.CarriageReturnLineFeed, + } + }).outputText; + function replaceWithClassAndNamespace() { + return (sourceFile: SourceFile) => { + const result = getMutableClone(sourceFile); + result.statements = createNodeArray([ + createClassDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, "Foo", /*typeParameters*/ undefined, /*heritageClauses*/ undefined, /*members*/ (undefined!)), + createModuleDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, createIdentifier("Foo"), createModuleBlock([createEmptyStatement()])) + ]); + return result; + }; + } + }); + function forceNamespaceRewrite(context: TransformationContext) { + return (sourceFile: SourceFile): SourceFile => { + return visitNode(sourceFile); + function visitNode(node: T): T { + if (node.kind === SyntaxKind.ModuleBlock) { + const block = (node as T & ModuleBlock); + const statements = createNodeArray([...block.statements]); + return updateModuleBlock(block, statements) as typeof block; } - }).outputText; - - function replaceWithClassAndNamespace() { - return (sourceFile: SourceFile) => { - const result = getMutableClone(sourceFile); - result.statements = createNodeArray([ - createClassDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, "Foo", /*typeParameters*/ undefined, /*heritageClauses*/ undefined, /*members*/ undefined!), // TODO: GH#18217 - createModuleDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, createIdentifier("Foo"), createModuleBlock([createEmptyStatement()])) - ]); - return result; - }; + return visitEachChild(node, visitNode, context); } - }); - - function forceNamespaceRewrite(context: TransformationContext) { + }; + } + testBaseline("transformAwayExportStar", () => { + return transpileModule("export * from './helper';", { + transformers: { + before: [expandExportStar], + }, + compilerOptions: { + target: ScriptTarget.ESNext, + newLine: NewLineKind.CarriageReturnLineFeed, + } + }).outputText; + function expandExportStar(context: TransformationContext) { return (sourceFile: SourceFile): SourceFile => { return visitNode(sourceFile); - function visitNode(node: T): T { - if (node.kind === SyntaxKind.ModuleBlock) { - const block = node as T & ModuleBlock; - const statements = createNodeArray([...block.statements]); - return updateModuleBlock(block, statements) as typeof block; + if (node.kind === SyntaxKind.ExportDeclaration) { + const ed = (node as Node as ExportDeclaration); + const exports = [{ name: "x" }]; + const exportSpecifiers = exports.map(e => createExportSpecifier(e.name, e.name)); + const exportClause = createNamedExports(exportSpecifiers); + const newEd = updateExportDeclaration(ed, ed.decorators, ed.modifiers, exportClause, ed.moduleSpecifier, ed.isTypeOnly); + return newEd as Node as T; } return visitEachChild(node, visitNode, context); } }; } - - testBaseline("transformAwayExportStar", () => { - return transpileModule("export * from './helper';", { - transformers: { - before: [expandExportStar], - }, - compilerOptions: { - target: ScriptTarget.ESNext, - newLine: NewLineKind.CarriageReturnLineFeed, - } - }).outputText; - - function expandExportStar(context: TransformationContext) { - return (sourceFile: SourceFile): SourceFile => { - return visitNode(sourceFile); - - function visitNode(node: T): T { - if (node.kind === SyntaxKind.ExportDeclaration) { - const ed = node as Node as ExportDeclaration; - const exports = [{ name: "x" }]; - const exportSpecifiers = exports.map(e => createExportSpecifier(e.name, e.name)); - const exportClause = createNamedExports(exportSpecifiers); - const newEd = updateExportDeclaration(ed, ed.decorators, ed.modifiers, exportClause, ed.moduleSpecifier, ed.isTypeOnly); - - return newEd as Node as T; - } - return visitEachChild(node, visitNode, context); - } - }; + }); + // https://github.com/Microsoft/TypeScript/issues/19618 + testBaseline("transformAddImportStar", () => { + return transpileModule("", { + transformers: { + before: [transformAddImportStar], + }, + compilerOptions: { + target: ScriptTarget.ES5, + module: ModuleKind.System, + newLine: NewLineKind.CarriageReturnLineFeed, } - }); - - // https://github.com/Microsoft/TypeScript/issues/19618 - testBaseline("transformAddImportStar", () => { - return transpileModule("", { - transformers: { - before: [transformAddImportStar], - }, - compilerOptions: { - target: ScriptTarget.ES5, - module: ModuleKind.System, - newLine: NewLineKind.CarriageReturnLineFeed, - } - }).outputText; - - function transformAddImportStar(_context: TransformationContext) { - return (sourceFile: SourceFile): SourceFile => { - return visitNode(sourceFile); - }; - function visitNode(sf: SourceFile) { - // produce `import * as i0 from './comp'; - const importStar = createImportDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*importClause*/ createImportClause( - /*name*/ undefined, - createNamespaceImport(createIdentifier("i0")) - ), - /*moduleSpecifier*/ createLiteral("./comp1")); - return updateSourceFileNode(sf, [importStar]); - } + }).outputText; + function transformAddImportStar(_context: TransformationContext) { + return (sourceFile: SourceFile): SourceFile => { + return visitNode(sourceFile); + }; + function visitNode(sf: SourceFile) { + // produce `import * as i0 from './comp'; + const importStar = createImportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*importClause*/ createImportClause( + /*name*/ undefined, createNamespaceImport(createIdentifier("i0"))), + /*moduleSpecifier*/ createLiteral("./comp1")); + return updateSourceFileNode(sf, [importStar]); } - }); - - // https://github.com/Microsoft/TypeScript/issues/17384 - testBaseline("transformAddDecoratedNode", () => { - return transpileModule("", { - transformers: { - before: [transformAddDecoratedNode], - }, - compilerOptions: { - target: ScriptTarget.ES5, - newLine: NewLineKind.CarriageReturnLineFeed, - } - }).outputText; - - function transformAddDecoratedNode(_context: TransformationContext) { - return (sourceFile: SourceFile): SourceFile => { - return visitNode(sourceFile); - }; - function visitNode(sf: SourceFile) { - // produce `class Foo { @Bar baz() {} }`; - const classDecl = createClassDeclaration([], [], "Foo", /*typeParameters*/ undefined, /*heritageClauses*/ undefined, [ - createMethod([createDecorator(createIdentifier("Bar"))], [], /**/ undefined, "baz", /**/ undefined, /**/ undefined, [], /**/ undefined, createBlock([])) - ]); - return updateSourceFileNode(sf, [classDecl]); - } + } + }); + // https://github.com/Microsoft/TypeScript/issues/17384 + testBaseline("transformAddDecoratedNode", () => { + return transpileModule("", { + transformers: { + before: [transformAddDecoratedNode], + }, + compilerOptions: { + target: ScriptTarget.ES5, + newLine: NewLineKind.CarriageReturnLineFeed, } - }); - - testBaseline("transformDeclarationFile", () => { - return baselineDeclarationTransform(`var oldName = undefined;`, { - transformers: { - afterDeclarations: [replaceIdentifiersNamedOldNameWithNewName] - }, - compilerOptions: { - newLine: NewLineKind.CarriageReturnLineFeed, - declaration: true - } - }); - }); - - // https://github.com/microsoft/TypeScript/issues/33295 - testBaseline("transformParameterProperty", () => { - return transpileModule("", { - transformers: { - before: [transformAddParameterProperty], - }, - compilerOptions: { - target: ScriptTarget.ES5, - newLine: NewLineKind.CarriageReturnLineFeed, - } - }).outputText; - - function transformAddParameterProperty(_context: TransformationContext) { - return (sourceFile: SourceFile): SourceFile => { - return visitNode(sourceFile); - }; - function visitNode(sf: SourceFile) { - // produce `class Foo { constructor(@Dec private x) {} }`; - // The decorator is required to trigger ts.ts transformations. - const classDecl = createClassDeclaration([], [], "Foo", /*typeParameters*/ undefined, /*heritageClauses*/ undefined, [ - createConstructor(/*decorators*/ undefined, /*modifiers*/ undefined, [ - createParameter(/*decorators*/ [createDecorator(createIdentifier("Dec"))], /*modifiers*/ [createModifier(SyntaxKind.PrivateKeyword)], /*dotDotDotToken*/ undefined, "x")], createBlock([])) - ]); - return updateSourceFileNode(sf, [classDecl]); - } + }).outputText; + function transformAddDecoratedNode(_context: TransformationContext) { + return (sourceFile: SourceFile): SourceFile => { + return visitNode(sourceFile); + }; + function visitNode(sf: SourceFile) { + // produce `class Foo { @Bar baz() {} }`; + const classDecl = createClassDeclaration([], [], "Foo", /*typeParameters*/ undefined, /*heritageClauses*/ undefined, [ + createMethod([createDecorator(createIdentifier("Bar"))], [], /**/ undefined, "baz", /**/ undefined, /**/ undefined, [], /**/ undefined, createBlock([])) + ]); + return updateSourceFileNode(sf, [classDecl]); } - }); - - function baselineDeclarationTransform(text: string, opts: TranspileOptions) { - const fs = vfs.createFromFileSystem(Harness.IO, /*caseSensitive*/ true, { documents: [new documents.TextDocument("/.src/index.ts", text)] }); - const host = new fakes.CompilerHost(fs, opts.compilerOptions); - const program = createProgram(["/.src/index.ts"], opts.compilerOptions!, host); - program.emit(program.getSourceFile("/.src/index.ts"), (p, s, bom) => host.writeFile(p, s, bom), /*cancellationToken*/ undefined, /*onlyDts*/ true, opts.transformers); - return fs.readFileSync("/.src/index.d.ts").toString(); } - - function addSyntheticComment(nodeFilter: (node: Node) => boolean) { - return (context: TransformationContext) => { - return (sourceFile: SourceFile): SourceFile => { - return visitNode(sourceFile, rootTransform, isSourceFile); - }; - function rootTransform(node: T): VisitResult { - if (nodeFilter(node)) { - setEmitFlags(node, EmitFlags.NoLeadingComments); - setSyntheticLeadingComments(node, [{ kind: SyntaxKind.MultiLineCommentTrivia, text: "comment", pos: -1, end: -1, hasTrailingNewLine: true }]); - } - return visitEachChild(node, rootTransform, context); - } + }); + testBaseline("transformDeclarationFile", () => { + return baselineDeclarationTransform(`var oldName = undefined;`, { + transformers: { + afterDeclarations: [replaceIdentifiersNamedOldNameWithNewName] + }, + compilerOptions: { + newLine: NewLineKind.CarriageReturnLineFeed, + declaration: true + } + }); + }); + // https://github.com/microsoft/TypeScript/issues/33295 + testBaseline("transformParameterProperty", () => { + return transpileModule("", { + transformers: { + before: [transformAddParameterProperty], + }, + compilerOptions: { + target: ScriptTarget.ES5, + newLine: NewLineKind.CarriageReturnLineFeed, + } + }).outputText; + function transformAddParameterProperty(_context: TransformationContext) { + return (sourceFile: SourceFile): SourceFile => { + return visitNode(sourceFile); }; + function visitNode(sf: SourceFile) { + // produce `class Foo { constructor(@Dec private x) {} }`; + // The decorator is required to trigger ts.ts transformations. + const classDecl = createClassDeclaration([], [], "Foo", /*typeParameters*/ undefined, /*heritageClauses*/ undefined, [ + createConstructor(/*decorators*/ undefined, /*modifiers*/ undefined, [ + createParameter(/*decorators*/ [createDecorator(createIdentifier("Dec"))], /*modifiers*/ [createModifier(SyntaxKind.PrivateKeyword)], /*dotDotDotToken*/ undefined, "x") + ], createBlock([])) + ]); + return updateSourceFileNode(sf, [classDecl]); + } } - - // https://github.com/Microsoft/TypeScript/issues/24096 - testBaseline("transformAddCommentToArrowReturnValue", () => { - return transpileModule(`const foo = () => + }); + function baselineDeclarationTransform(text: string, opts: TranspileOptions) { + const fs = createFromFileSystem(IO, /*caseSensitive*/ true, { documents: [new TextDocument("/.src/index.ts", text)] }); + const host = new CompilerHost(fs, opts.compilerOptions); + const program = createProgram(["/.src/index.ts"], (opts.compilerOptions!), host); + program.emit(program.getSourceFile("/.src/index.ts"), (p, s, bom) => host.writeFile(p, s, bom), /*cancellationToken*/ undefined, /*onlyDts*/ true, opts.transformers); + return fs.readFileSync("/.src/index.d.ts").toString(); + } + function addSyntheticComment(nodeFilter: (node: Node) => boolean) { + return (context: TransformationContext) => { + return (sourceFile: SourceFile): SourceFile => { + return visitNode(sourceFile, rootTransform, isSourceFile); + }; + function rootTransform(node: T): VisitResult { + if (nodeFilter(node)) { + setEmitFlags(node, EmitFlags.NoLeadingComments); + setSyntheticLeadingComments(node, [{ kind: SyntaxKind.MultiLineCommentTrivia, text: "comment", pos: -1, end: -1, hasTrailingNewLine: true }]); + } + return visitEachChild(node, rootTransform, context); + } + }; + } + // https://github.com/Microsoft/TypeScript/issues/24096 + testBaseline("transformAddCommentToArrowReturnValue", () => { + return transpileModule(`const foo = () => void 0 `, { - transformers: { - before: [addSyntheticComment(isVoidExpression)], - }, - compilerOptions: { - target: ScriptTarget.ES5, - newLine: NewLineKind.CarriageReturnLineFeed, - } - }).outputText; - }); - - // https://github.com/Microsoft/TypeScript/issues/17594 - testBaseline("transformAddCommentToExportedVar", () => { - return transpileModule(`export const exportedDirectly = 1; + transformers: { + before: [addSyntheticComment(isVoidExpression)], + }, + compilerOptions: { + target: ScriptTarget.ES5, + newLine: NewLineKind.CarriageReturnLineFeed, + } + }).outputText; + }); + // https://github.com/Microsoft/TypeScript/issues/17594 + testBaseline("transformAddCommentToExportedVar", () => { + return transpileModule(`export const exportedDirectly = 1; const exportedSeparately = 2; export {exportedSeparately}; `, { - transformers: { - before: [addSyntheticComment(isVariableStatement)], - }, - compilerOptions: { - target: ScriptTarget.ES5, - newLine: NewLineKind.CarriageReturnLineFeed, - } - }).outputText; - }); - - // https://github.com/Microsoft/TypeScript/issues/17594 - testBaseline("transformAddCommentToImport", () => { - return transpileModule(` + transformers: { + before: [addSyntheticComment(isVariableStatement)], + }, + compilerOptions: { + target: ScriptTarget.ES5, + newLine: NewLineKind.CarriageReturnLineFeed, + } + }).outputText; + }); + // https://github.com/Microsoft/TypeScript/issues/17594 + testBaseline("transformAddCommentToImport", () => { + return transpileModule(` // Previous comment on import. import {Value} from 'somewhere'; import * as X from 'somewhere'; @@ -404,19 +370,18 @@ export { /* specifier comment */ X, Y} from 'somewhere'; export * from 'somewhere'; export {Value}; `, { - transformers: { - before: [addSyntheticComment(n => isImportDeclaration(n) || isExportDeclaration(n) || isImportSpecifier(n) || isExportSpecifier(n))], - }, - compilerOptions: { - target: ScriptTarget.ES5, - newLine: NewLineKind.CarriageReturnLineFeed, - } - }).outputText; - }); - - // https://github.com/Microsoft/TypeScript/issues/17594 - testBaseline("transformAddCommentToProperties", () => { - return transpileModule(` + transformers: { + before: [addSyntheticComment(n => isImportDeclaration(n) || isExportDeclaration(n) || isImportSpecifier(n) || isExportSpecifier(n))], + }, + compilerOptions: { + target: ScriptTarget.ES5, + newLine: NewLineKind.CarriageReturnLineFeed, + } + }).outputText; + }); + // https://github.com/Microsoft/TypeScript/issues/17594 + testBaseline("transformAddCommentToProperties", () => { + return transpileModule(` // class comment. class Clazz { // original comment 1. @@ -427,18 +392,17 @@ class Clazz { constructor(readonly field = 1) {} } `, { - transformers: { - before: [addSyntheticComment(n => isPropertyDeclaration(n) || isParameterPropertyDeclaration(n, n.parent) || isClassDeclaration(n) || isConstructorDeclaration(n))], - }, - compilerOptions: { - target: ScriptTarget.ES2015, - newLine: NewLineKind.CarriageReturnLineFeed, - } - }).outputText; - }); - - testBaseline("transformAddCommentToNamespace", () => { - return transpileModule(` + transformers: { + before: [addSyntheticComment(n => isPropertyDeclaration(n) || isParameterPropertyDeclaration(n, n.parent) || isClassDeclaration(n) || isConstructorDeclaration(n))], + }, + compilerOptions: { + target: ScriptTarget.ES2015, + newLine: NewLineKind.CarriageReturnLineFeed, + } + }).outputText; + }); + testBaseline("transformAddCommentToNamespace", () => { + return transpileModule(` // namespace comment. namespace Foo { export const x = 1; @@ -448,109 +412,89 @@ namespace Foo { export const y = 1; } `, { - transformers: { - before: [addSyntheticComment(n => isModuleDeclaration(n))], - }, - compilerOptions: { - target: ScriptTarget.ES2015, - newLine: NewLineKind.CarriageReturnLineFeed, - } - }).outputText; - }); - - testBaseline("transformUpdateModuleMember", () => { - return transpileModule(` + transformers: { + before: [addSyntheticComment(n => isModuleDeclaration(n))], + }, + compilerOptions: { + target: ScriptTarget.ES2015, + newLine: NewLineKind.CarriageReturnLineFeed, + } + }).outputText; + }); + testBaseline("transformUpdateModuleMember", () => { + return transpileModule(` module MyModule { const myVariable = 1; function foo(param: string) {} } `, { - transformers: { - before: [renameVariable], - }, - compilerOptions: { - target: ScriptTarget.ES2015, - newLine: NewLineKind.CarriageReturnLineFeed, + transformers: { + before: [renameVariable], + }, + compilerOptions: { + target: ScriptTarget.ES2015, + newLine: NewLineKind.CarriageReturnLineFeed, + } + }).outputText; + function renameVariable(context: TransformationContext) { + return (sourceFile: SourceFile): SourceFile => { + return visitNode(sourceFile, rootTransform, isSourceFile); + }; + function rootTransform(node: T): Node { + if (isVariableDeclaration(node)) { + return updateVariableDeclaration(node, createIdentifier("newName"), /* type */ undefined, node.initializer); } - }).outputText; - - function renameVariable(context: TransformationContext) { - return (sourceFile: SourceFile): SourceFile => { - return visitNode(sourceFile, rootTransform, isSourceFile); - }; - function rootTransform(node: T): Node { - if (isVariableDeclaration(node)) { - return updateVariableDeclaration(node, createIdentifier("newName"), /* type */ undefined, node.initializer); - } - return visitEachChild(node, rootTransform, context); - } + return visitEachChild(node, rootTransform, context); } - }); - - // https://github.com/Microsoft/TypeScript/issues/24709 - testBaseline("issue24709", () => { - const fs = vfs.createFromFileSystem(Harness.IO, /*caseSensitive*/ true); - const transformed = transform(createSourceFile("source.ts", "class X { echo(x: string) { return x; } }", ScriptTarget.ES3), [transformSourceFile]); - const transformedSourceFile = transformed.transformed[0]; - transformed.dispose(); - const host = new fakes.CompilerHost(fs); - host.getSourceFile = () => transformedSourceFile; - const program = createProgram(["source.ts"], { - target: ScriptTarget.ES3, - module: ModuleKind.None, - noLib: true - }, host); - program.emit(transformedSourceFile, (_p, s, b) => host.writeFile("source.js", s, b)); - return host.readFile("source.js")!.toString(); - - function transformSourceFile(context: TransformationContext) { - const visitor: Visitor = (node) => { - if (isMethodDeclaration(node)) { - return updateMethod( - node, - node.decorators, - node.modifiers, - node.asteriskToken, - createIdentifier("foobar"), - node.questionToken, - node.typeParameters, - node.parameters, - node.type, - node.body, - ); - } - return visitEachChild(node, visitor, context); - }; - return (node: SourceFile) => visitNode(node, visitor); + } + }); + // https://github.com/Microsoft/TypeScript/issues/24709 + testBaseline("issue24709", () => { + const fs = createFromFileSystem(IO, /*caseSensitive*/ true); + const transformed = transform(createSourceFile("source.ts", "class X { echo(x: string) { return x; } }", ScriptTarget.ES3), [transformSourceFile]); + const transformedSourceFile = transformed.transformed[0]; + transformed.dispose(); + const host = new CompilerHost(fs); + host.getSourceFile = () => transformedSourceFile; + const program = createProgram(["source.ts"], { + target: ScriptTarget.ES3, + module: ModuleKind.None, + noLib: true + }, host); + program.emit(transformedSourceFile, (_p, s, b) => host.writeFile("source.js", s, b)); + return host.readFile("source.js")!.toString(); + function transformSourceFile(context: TransformationContext) { + const visitor: Visitor = (node) => { + if (isMethodDeclaration(node)) { + return updateMethod(node, node.decorators, node.modifiers, node.asteriskToken, createIdentifier("foobar"), node.questionToken, node.typeParameters, node.parameters, node.type, node.body); + } + return visitEachChild(node, visitor, context); + }; + return (node: SourceFile) => visitNode(node, visitor); + } + }); + testBaselineAndEvaluate("templateSpans", () => { + return transpileModule("const x = String.raw`\n\nhello`; exports.stringLength = x.trim().length;", { + compilerOptions: { + target: ScriptTarget.ESNext, + newLine: NewLineKind.CarriageReturnLineFeed, + }, + transformers: { + before: [transformSourceFile] } - - }); - - testBaselineAndEvaluate("templateSpans", () => { - return transpileModule("const x = String.raw`\n\nhello`; exports.stringLength = x.trim().length;", { - compilerOptions: { - target: ScriptTarget.ESNext, - newLine: NewLineKind.CarriageReturnLineFeed, - }, - transformers: { - before: [transformSourceFile] + }).outputText; + function transformSourceFile(context: TransformationContext): Transformer { + function visitor(node: Node): VisitResult { + if (isNoSubstitutionTemplateLiteral(node)) { + return createNoSubstitutionTemplateLiteral(node.text, node.rawText); } - }).outputText; - - function transformSourceFile(context: TransformationContext): Transformer { - function visitor(node: Node): VisitResult { - if (isNoSubstitutionTemplateLiteral(node)) { - return createNoSubstitutionTemplateLiteral(node.text, node.rawText); - } - else { - return visitEachChild(node, visitor, context); - } + else { + return visitEachChild(node, visitor, context); } - return sourceFile => visitNode(sourceFile, visitor, isSourceFile); } - }, exports => { - assert.equal(exports.stringLength, 5); - }); + return sourceFile => visitNode(sourceFile, visitor, isSourceFile); + } + }, exports => { + assert.equal(exports.stringLength, 5); }); -} - +}); diff --git a/src/testRunner/unittests/tsbuild/amdModulesWithOut.ts b/src/testRunner/unittests/tsbuild/amdModulesWithOut.ts index fdcf748cf2cf5..5e9fcb3c028e9 100644 --- a/src/testRunner/unittests/tsbuild/amdModulesWithOut.ts +++ b/src/testRunner/unittests/tsbuild/amdModulesWithOut.ts @@ -1,134 +1,129 @@ -namespace ts { - describe("unittests:: tsbuild:: outFile:: on amd modules with --out", () => { - let outFileFs: vfs.FileSystem; - const enum project { lib, app } - function relName(path: string) { return path.slice(1); } - type Sources = [string, readonly string[]]; - const enum source { config, ts } - const sources: [Sources, Sources] = [ +import { FileSystem } from "../../vfs"; +import { loadProjectFromDisk, verifyTscIncrementalEdits, BuildKind, appendText, emptyArray, enableStrict, addTestPrologue, addShebang, addSpread, addRest, removeRest, addTripleSlashRef, replaceText, verifyTsc } from "../../ts"; +describe("unittests:: tsbuild:: outFile:: on amd modules with --out", () => { + let outFileFs: FileSystem; + const enum project { + lib, + app + } + function relName(path: string) { return path.slice(1); } + type Sources = [string, readonly string[]]; + const enum source { + config, + ts + } + const sources: [Sources, Sources] = [ + [ + "/src/lib/tsconfig.json", [ - "/src/lib/tsconfig.json", - [ - "/src/lib/file0.ts", - "/src/lib/file1.ts", - "/src/lib/file2.ts", - "/src/lib/global.ts", - ] - ], + "/src/lib/file0.ts", + "/src/lib/file1.ts", + "/src/lib/file2.ts", + "/src/lib/global.ts", + ] + ], + [ + "/src/app/tsconfig.json", [ - "/src/app/tsconfig.json", - [ - "/src/app/file3.ts", - "/src/app/file4.ts" - ] + "/src/app/file3.ts", + "/src/app/file4.ts" ] - ]; - before(() => { - outFileFs = loadProjectFromDisk("tests/projects/amdModulesWithOut"); - }); - after(() => { - outFileFs = undefined!; - }); - - interface VerifyOutFileScenarioInput { - subScenario: string; - modifyFs?: (fs: vfs.FileSystem) => void; - modifyAgainFs?: (fs: vfs.FileSystem) => void; - } - - function verifyOutFileScenario({ + ] + ]; + before(() => { + outFileFs = loadProjectFromDisk("tests/projects/amdModulesWithOut"); + }); + after(() => { + outFileFs = undefined!; + }); + interface VerifyOutFileScenarioInput { + subScenario: string; + modifyFs?: (fs: FileSystem) => void; + modifyAgainFs?: (fs: FileSystem) => void; + } + function verifyOutFileScenario({ subScenario, modifyFs, modifyAgainFs }: VerifyOutFileScenarioInput) { + verifyTscIncrementalEdits({ + scenario: "amdModulesWithOut", subScenario, + fs: () => outFileFs, + commandLineArgs: ["--b", "/src/app", "--verbose"], + baselineSourceMap: true, modifyFs, - modifyAgainFs - }: VerifyOutFileScenarioInput) { - verifyTscIncrementalEdits({ - scenario: "amdModulesWithOut", - subScenario, - fs: () => outFileFs, - commandLineArgs: ["--b", "/src/app", "--verbose"], - baselineSourceMap: true, - modifyFs, - incrementalScenarios: [ - { - buildKind: BuildKind.IncrementalDtsUnchanged, - modifyFs: fs => appendText(fs, relName(sources[project.lib][source.ts][1]), "console.log(x);") - }, - ...(modifyAgainFs ? [{ + incrementalScenarios: [ + { + buildKind: BuildKind.IncrementalDtsUnchanged, + modifyFs: fs => appendText(fs, relName(sources[project.lib][source.ts][1]), "console.log(x);") + }, + ...(modifyAgainFs ? [{ buildKind: BuildKind.IncrementalHeadersChange, modifyFs: modifyAgainFs }] : emptyArray), - ] - }); - } - - describe("Prepend output with .tsbuildinfo", () => { + ] + }); + } + describe("Prepend output with .tsbuildinfo", () => { + verifyOutFileScenario({ + subScenario: "modules and globals mixed in amd", + }); + // Prologues + describe("Prologues", () => { verifyOutFileScenario({ - subScenario: "modules and globals mixed in amd", - }); - - // Prologues - describe("Prologues", () => { - verifyOutFileScenario({ - subScenario: "multiple prologues in all projects", - modifyFs: fs => { - enableStrict(fs, sources[project.lib][source.config]); - addTestPrologue(fs, sources[project.lib][source.ts][0], `"myPrologue"`); - addTestPrologue(fs, sources[project.lib][source.ts][2], `"myPrologueFile"`); - addTestPrologue(fs, sources[project.lib][source.ts][3], `"myPrologue3"`); - enableStrict(fs, sources[project.app][source.config]); - addTestPrologue(fs, sources[project.app][source.ts][0], `"myPrologue"`); - addTestPrologue(fs, sources[project.app][source.ts][1], `"myPrologue2";`); - }, - modifyAgainFs: fs => addTestPrologue(fs, relName(sources[project.lib][source.ts][1]), `"myPrologue5"`) - }); + subScenario: "multiple prologues in all projects", + modifyFs: fs => { + enableStrict(fs, sources[project.lib][source.config]); + addTestPrologue(fs, sources[project.lib][source.ts][0], `"myPrologue"`); + addTestPrologue(fs, sources[project.lib][source.ts][2], `"myPrologueFile"`); + addTestPrologue(fs, sources[project.lib][source.ts][3], `"myPrologue3"`); + enableStrict(fs, sources[project.app][source.config]); + addTestPrologue(fs, sources[project.app][source.ts][0], `"myPrologue"`); + addTestPrologue(fs, sources[project.app][source.ts][1], `"myPrologue2";`); + }, + modifyAgainFs: fs => addTestPrologue(fs, relName(sources[project.lib][source.ts][1]), `"myPrologue5"`) }); - - // Shebang - describe("Shebang", () => { - // changes declaration because its emitted in .d.ts file - verifyOutFileScenario({ - subScenario: "shebang in all projects", - modifyFs: fs => { - addShebang(fs, "lib", "file0"); - addShebang(fs, "lib", "file1"); - addShebang(fs, "app", "file3"); - }, - }); + }); + // Shebang + describe("Shebang", () => { + // changes declaration because its emitted in .d.ts file + verifyOutFileScenario({ + subScenario: "shebang in all projects", + modifyFs: fs => { + addShebang(fs, "lib", "file0"); + addShebang(fs, "lib", "file1"); + addShebang(fs, "app", "file3"); + }, }); - - // emitHelpers - describe("emitHelpers", () => { - verifyOutFileScenario({ - subScenario: "multiple emitHelpers in all projects", - modifyFs: fs => { - addSpread(fs, "lib", "file0"); - addRest(fs, "lib", "file1"); - addRest(fs, "app", "file3"); - addSpread(fs, "app", "file4"); - }, - modifyAgainFs: fs => removeRest(fs, "lib", "file1") - }); + }); + // emitHelpers + describe("emitHelpers", () => { + verifyOutFileScenario({ + subScenario: "multiple emitHelpers in all projects", + modifyFs: fs => { + addSpread(fs, "lib", "file0"); + addRest(fs, "lib", "file1"); + addRest(fs, "app", "file3"); + addSpread(fs, "app", "file4"); + }, + modifyAgainFs: fs => removeRest(fs, "lib", "file1") }); - - // triple slash refs - describe("triple slash refs", () => { - // changes declaration because its emitted in .d.ts file - verifyOutFileScenario({ - subScenario: "triple slash refs in all projects", - modifyFs: fs => { - addTripleSlashRef(fs, "lib", "file0"); - addTripleSlashRef(fs, "app", "file4"); - } - }); + }); + // triple slash refs + describe("triple slash refs", () => { + // changes declaration because its emitted in .d.ts file + verifyOutFileScenario({ + subScenario: "triple slash refs in all projects", + modifyFs: fs => { + addTripleSlashRef(fs, "lib", "file0"); + addTripleSlashRef(fs, "app", "file4"); + } }); - - describe("stripInternal", () => { - function stripInternalScenario(fs: vfs.FileSystem) { - const internal = "/*@internal*/"; - replaceText(fs, sources[project.app][source.config], `"composite": true,`, `"composite": true, + }); + describe("stripInternal", () => { + function stripInternalScenario(fs: FileSystem) { + const internal = "/*@internal*/"; + replaceText(fs, sources[project.app][source.config], `"composite": true,`, `"composite": true, "stripInternal": true,`); - replaceText(fs, sources[project.lib][source.ts][0], "const", `${internal} const`); - appendText(fs, sources[project.lib][source.ts][1], ` + replaceText(fs, sources[project.lib][source.ts][0], "const", `${internal} const`); + appendText(fs, sources[project.lib][source.ts][1], ` export class normalC { ${internal} constructor() { } ${internal} prop: string; @@ -154,33 +149,29 @@ ${internal} export import internalImport = internalNamespace.someClass; ${internal} export type internalType = internalC; ${internal} export const internalConst = 10; ${internal} export enum internalEnum { a, b, c }`); - } - - // Verify initial + incremental edits - verifyOutFileScenario({ - subScenario: "stripInternal", - modifyFs: stripInternalScenario, - modifyAgainFs: fs => replaceText(fs, sources[project.lib][source.ts][1], `export const`, `/*@internal*/ export const`), - }); + } + // Verify initial + incremental edits + verifyOutFileScenario({ + subScenario: "stripInternal", + modifyFs: stripInternalScenario, + modifyAgainFs: fs => replaceText(fs, sources[project.lib][source.ts][1], `export const`, `/*@internal*/ export const`), }); - - describe("when the module resolution finds original source file", () => { - function modifyFs(fs: vfs.FileSystem) { - // Make lib to output to parent dir - replaceText(fs, sources[project.lib][source.config], `"outFile": "module.js"`, `"outFile": "../module.js", "rootDir": "../"`); - // Change reference to file1 module to resolve to lib/file1 - replaceText(fs, sources[project.app][source.ts][0], "file1", "lib/file1"); - } - - verifyTsc({ - scenario: "amdModulesWithOut", - subScenario: "when the module resolution finds original source file", - fs: () => outFileFs, - commandLineArgs: ["-b", "/src/app", "--verbose"], - modifyFs, - baselineSourceMap: true, - }); + }); + describe("when the module resolution finds original source file", () => { + function modifyFs(fs: FileSystem) { + // Make lib to output to parent dir + replaceText(fs, sources[project.lib][source.config], `"outFile": "module.js"`, `"outFile": "../module.js", "rootDir": "../"`); + // Change reference to file1 module to resolve to lib/file1 + replaceText(fs, sources[project.app][source.ts][0], "file1", "lib/file1"); + } + verifyTsc({ + scenario: "amdModulesWithOut", + subScenario: "when the module resolution finds original source file", + fs: () => outFileFs, + commandLineArgs: ["-b", "/src/app", "--verbose"], + modifyFs, + baselineSourceMap: true, }); }); }); -} +}); diff --git a/src/testRunner/unittests/tsbuild/configFileErrors.ts b/src/testRunner/unittests/tsbuild/configFileErrors.ts index 5d2229588709b..14707328bbcba 100644 --- a/src/testRunner/unittests/tsbuild/configFileErrors.ts +++ b/src/testRunner/unittests/tsbuild/configFileErrors.ts @@ -1,21 +1,21 @@ -namespace ts { - describe("unittests:: tsbuild:: configFileErrors:: when tsconfig extends the missing file", () => { - verifyTsc({ - scenario: "configFileErrors", - subScenario: "when tsconfig extends the missing file", - fs: () => loadProjectFromDisk("tests/projects/missingExtendedConfig"), - commandLineArgs: ["--b", "/src/tsconfig.json"], - }); +import { verifyTsc, loadProjectFromDisk, verifyTscIncrementalEdits, loadProjectFromFiles, BuildKind, replaceText, appendText, noChangeRun } from "../../ts"; +import { dedent } from "../../Utils"; +describe("unittests:: tsbuild:: configFileErrors:: when tsconfig extends the missing file", () => { + verifyTsc({ + scenario: "configFileErrors", + subScenario: "when tsconfig extends the missing file", + fs: () => loadProjectFromDisk("tests/projects/missingExtendedConfig"), + commandLineArgs: ["--b", "/src/tsconfig.json"], }); - - describe("unittests:: tsbuild:: configFileErrors:: reports syntax errors in config file", () => { - verifyTscIncrementalEdits({ - scenario: "configFileErrors", - subScenario: "reports syntax errors in config file", - fs: () => loadProjectFromFiles({ - "/src/a.ts": "export function foo() { }", - "/src/b.ts": "export function bar() { }", - "/src/tsconfig.json": Utils.dedent` +}); +describe("unittests:: tsbuild:: configFileErrors:: reports syntax errors in config file", () => { + verifyTscIncrementalEdits({ + scenario: "configFileErrors", + subScenario: "reports syntax errors in config file", + fs: () => loadProjectFromFiles({ + "/src/a.ts": "export function foo() { }", + "/src/b.ts": "export function bar() { }", + "/src/tsconfig.json": dedent ` { "compilerOptions": { "composite": true, @@ -25,33 +25,29 @@ namespace ts { "b.ts" ] }` - }), - commandLineArgs: ["--b", "/src/tsconfig.json"], - incrementalScenarios: [ - { - buildKind: BuildKind.IncrementalDtsUnchanged, - modifyFs: fs => replaceText(fs, "/src/tsconfig.json", ",", `, + }), + commandLineArgs: ["--b", "/src/tsconfig.json"], + incrementalScenarios: [ + { + buildKind: BuildKind.IncrementalDtsUnchanged, + modifyFs: fs => replaceText(fs, "/src/tsconfig.json", ",", `, "declaration": true,`), - subScenario: "reports syntax errors after change to config file" - }, - { - buildKind: BuildKind.IncrementalDtsUnchanged, - modifyFs: fs => appendText(fs, "/src/a.ts", "export function fooBar() { }"), - subScenario: "reports syntax errors after change to ts file" - }, - noChangeRun, - { - buildKind: BuildKind.IncrementalDtsChange, - modifyFs: fs => fs.writeFileSync( - "/src/tsconfig.json", - JSON.stringify({ - compilerOptions: { composite: true, declaration: true }, - files: ["a.ts", "b.ts"] - }) - ), - subScenario: "builds after fixing config file errors" - }, - ] - }); + subScenario: "reports syntax errors after change to config file" + }, + { + buildKind: BuildKind.IncrementalDtsUnchanged, + modifyFs: fs => appendText(fs, "/src/a.ts", "export function fooBar() { }"), + subScenario: "reports syntax errors after change to ts file" + }, + noChangeRun, + { + buildKind: BuildKind.IncrementalDtsChange, + modifyFs: fs => fs.writeFileSync("/src/tsconfig.json", JSON.stringify({ + compilerOptions: { composite: true, declaration: true }, + files: ["a.ts", "b.ts"] + })), + subScenario: "builds after fixing config file errors" + }, + ] }); -} +}); diff --git a/src/testRunner/unittests/tsbuild/containerOnlyReferenced.ts b/src/testRunner/unittests/tsbuild/containerOnlyReferenced.ts index 8a208a698fa6a..7462ae45dd564 100644 --- a/src/testRunner/unittests/tsbuild/containerOnlyReferenced.ts +++ b/src/testRunner/unittests/tsbuild/containerOnlyReferenced.ts @@ -1,11 +1,10 @@ -namespace ts { - describe("unittests:: tsbuild:: when containerOnly project is referenced", () => { - verifyTscIncrementalEdits({ - scenario: "containerOnlyReferenced", - subScenario: "verify that subsequent builds after initial build doesnt build anything", - fs: () => loadProjectFromDisk("tests/projects/containerOnlyReferenced"), - commandLineArgs: ["--b", "/src", "--verbose"], - incrementalScenarios: [noChangeRun] - }); +import { verifyTscIncrementalEdits, loadProjectFromDisk, noChangeRun } from "../../ts"; +describe("unittests:: tsbuild:: when containerOnly project is referenced", () => { + verifyTscIncrementalEdits({ + scenario: "containerOnlyReferenced", + subScenario: "verify that subsequent builds after initial build doesnt build anything", + fs: () => loadProjectFromDisk("tests/projects/containerOnlyReferenced"), + commandLineArgs: ["--b", "/src", "--verbose"], + incrementalScenarios: [noChangeRun] }); -} +}); diff --git a/src/testRunner/unittests/tsbuild/demo.ts b/src/testRunner/unittests/tsbuild/demo.ts index 18bc1eea89a3b..7325da749b281 100644 --- a/src/testRunner/unittests/tsbuild/demo.ts +++ b/src/testRunner/unittests/tsbuild/demo.ts @@ -1,49 +1,37 @@ -namespace ts { - describe("unittests:: tsbuild:: on demo project", () => { - let projFs: vfs.FileSystem; - before(() => { - projFs = loadProjectFromDisk("tests/projects/demo"); - }); - - after(() => { - projFs = undefined!; // Release the contents - }); - - verifyTsc({ - scenario: "demo", - subScenario: "in master branch with everything setup correctly and reports no error", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tsconfig.json", "--verbose"] - }); - - verifyTsc({ - scenario: "demo", - subScenario: "in circular branch reports the error about it by stopping build", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tsconfig.json", "--verbose"], - modifyFs: fs => replaceText( - fs, - "/src/core/tsconfig.json", - "}", - `}, +import { FileSystem } from "../../vfs"; +import { loadProjectFromDisk, verifyTsc, replaceText, prependText } from "../../ts"; +describe("unittests:: tsbuild:: on demo project", () => { + let projFs: FileSystem; + before(() => { + projFs = loadProjectFromDisk("tests/projects/demo"); + }); + after(() => { + projFs = undefined!; // Release the contents + }); + verifyTsc({ + scenario: "demo", + subScenario: "in master branch with everything setup correctly and reports no error", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tsconfig.json", "--verbose"] + }); + verifyTsc({ + scenario: "demo", + subScenario: "in circular branch reports the error about it by stopping build", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tsconfig.json", "--verbose"], + modifyFs: fs => replaceText(fs, "/src/core/tsconfig.json", "}", `}, "references": [ { "path": "../zoo" } - ]` - ) - }); - verifyTsc({ - scenario: "demo", - subScenario: "in bad-ref branch reports the error about files not in rootDir at the import location", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tsconfig.json", "--verbose"], - modifyFs: fs => prependText( - fs, - "/src/core/utilities.ts", - `import * as A from '../animals'; -` - ) - }); + ]`) + }); + verifyTsc({ + scenario: "demo", + subScenario: "in bad-ref branch reports the error about files not in rootDir at the import location", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tsconfig.json", "--verbose"], + modifyFs: fs => prependText(fs, "/src/core/utilities.ts", `import * as A from '../animals'; +`) }); -} +}); diff --git a/src/testRunner/unittests/tsbuild/emitDeclarationOnly.ts b/src/testRunner/unittests/tsbuild/emitDeclarationOnly.ts index e7dff56ab9fa9..9a913b0d3553f 100644 --- a/src/testRunner/unittests/tsbuild/emitDeclarationOnly.ts +++ b/src/testRunner/unittests/tsbuild/emitDeclarationOnly.ts @@ -1,53 +1,49 @@ -namespace ts { - describe("unittests:: tsbuild:: on project with emitDeclarationOnly set to true", () => { - let projFs: vfs.FileSystem; - before(() => { - projFs = loadProjectFromDisk("tests/projects/emitDeclarationOnly"); - }); - after(() => { - projFs = undefined!; - }); - - function verifyEmitDeclarationOnly(disableMap?: true) { - verifyTscIncrementalEdits({ - subScenario: `only dts output in circular import project with emitDeclarationOnly${disableMap ? "" : " and declarationMap"}`, - fs: () => projFs, - scenario: "emitDeclarationOnly", - commandLineArgs: ["--b", "/src", "--verbose"], - modifyFs: disableMap ? - (fs => replaceText(fs, "/src/tsconfig.json", `"declarationMap": true,`, "")) : - undefined, - incrementalScenarios: [{ - buildKind: BuildKind.IncrementalDtsChange, - modifyFs: fs => replaceText(fs, "/src/src/a.ts", "b: B;", "b: B; foo: any;"), - }], - }); - } - verifyEmitDeclarationOnly(); - verifyEmitDeclarationOnly(/*disableMap*/ true); - +import { FileSystem } from "../../vfs"; +import { loadProjectFromDisk, verifyTscIncrementalEdits, replaceText, BuildKind } from "../../ts"; +describe("unittests:: tsbuild:: on project with emitDeclarationOnly set to true", () => { + let projFs: FileSystem; + before(() => { + projFs = loadProjectFromDisk("tests/projects/emitDeclarationOnly"); + }); + after(() => { + projFs = undefined!; + }); + function verifyEmitDeclarationOnly(disableMap?: true) { verifyTscIncrementalEdits({ - subScenario: `only dts output in non circular imports project with emitDeclarationOnly`, + subScenario: `only dts output in circular import project with emitDeclarationOnly${disableMap ? "" : " and declarationMap"}`, fs: () => projFs, scenario: "emitDeclarationOnly", commandLineArgs: ["--b", "/src", "--verbose"], - modifyFs: fs => { - fs.rimrafSync("/src/src/index.ts"); - replaceText(fs, "/src/src/a.ts", `import { B } from "./b";`, `export class B { prop = "hello"; }`); - }, - incrementalScenarios: [ - { + modifyFs: disableMap ? + (fs => replaceText(fs, "/src/tsconfig.json", `"declarationMap": true,`, "")) : + undefined, + incrementalScenarios: [{ buildKind: BuildKind.IncrementalDtsChange, modifyFs: fs => replaceText(fs, "/src/src/a.ts", "b: B;", "b: B; foo: any;"), - - }, - { - buildKind: BuildKind.IncrementalDtsUnchanged, - modifyFs: fs => replaceText(fs, "/src/src/a.ts", "export interface A {", `class C { } -export interface A {`), - - }, - ], + }], }); + } + verifyEmitDeclarationOnly(); + verifyEmitDeclarationOnly(/*disableMap*/ true); + verifyTscIncrementalEdits({ + subScenario: `only dts output in non circular imports project with emitDeclarationOnly`, + fs: () => projFs, + scenario: "emitDeclarationOnly", + commandLineArgs: ["--b", "/src", "--verbose"], + modifyFs: fs => { + fs.rimrafSync("/src/src/index.ts"); + replaceText(fs, "/src/src/a.ts", `import { B } from "./b";`, `export class B { prop = "hello"; }`); + }, + incrementalScenarios: [ + { + buildKind: BuildKind.IncrementalDtsChange, + modifyFs: fs => replaceText(fs, "/src/src/a.ts", "b: B;", "b: B; foo: any;"), + }, + { + buildKind: BuildKind.IncrementalDtsUnchanged, + modifyFs: fs => replaceText(fs, "/src/src/a.ts", "export interface A {", `class C { } +export interface A {`), + }, + ], }); -} +}); diff --git a/src/testRunner/unittests/tsbuild/emptyFiles.ts b/src/testRunner/unittests/tsbuild/emptyFiles.ts index ffd9429a34411..e6df350abe0b5 100644 --- a/src/testRunner/unittests/tsbuild/emptyFiles.ts +++ b/src/testRunner/unittests/tsbuild/emptyFiles.ts @@ -1,25 +1,23 @@ -namespace ts { - describe("unittests:: tsbuild - empty files option in tsconfig", () => { - let projFs: vfs.FileSystem; - before(() => { - projFs = loadProjectFromDisk("tests/projects/empty-files"); - }); - after(() => { - projFs = undefined!; - }); - - verifyTsc({ - scenario: "emptyFiles", - subScenario: "has empty files diagnostic when files is empty and no references are provided", - fs: () => projFs, - commandLineArgs: ["--b", "/src/no-references"], - }); - - verifyTsc({ - scenario: "emptyFiles", - subScenario: "does not have empty files diagnostic when files is empty and references are provided", - fs: () => projFs, - commandLineArgs: ["--b", "/src/with-references"], - }); +import { FileSystem } from "../../vfs"; +import { loadProjectFromDisk, verifyTsc } from "../../ts"; +describe("unittests:: tsbuild - empty files option in tsconfig", () => { + let projFs: FileSystem; + before(() => { + projFs = loadProjectFromDisk("tests/projects/empty-files"); }); -} + after(() => { + projFs = undefined!; + }); + verifyTsc({ + scenario: "emptyFiles", + subScenario: "has empty files diagnostic when files is empty and no references are provided", + fs: () => projFs, + commandLineArgs: ["--b", "/src/no-references"], + }); + verifyTsc({ + scenario: "emptyFiles", + subScenario: "does not have empty files diagnostic when files is empty and references are provided", + fs: () => projFs, + commandLineArgs: ["--b", "/src/with-references"], + }); +}); diff --git a/src/testRunner/unittests/tsbuild/exitCodeOnBogusFile.ts b/src/testRunner/unittests/tsbuild/exitCodeOnBogusFile.ts index 487671dfae147..8395b46a1ed85 100644 --- a/src/testRunner/unittests/tsbuild/exitCodeOnBogusFile.ts +++ b/src/testRunner/unittests/tsbuild/exitCodeOnBogusFile.ts @@ -1,11 +1,10 @@ -namespace ts { - // https://github.com/microsoft/TypeScript/issues/33849 - describe("unittests:: tsbuild:: exitCodeOnBogusFile:: test exit code", () => { - verifyTsc({ - scenario: "exitCodeOnBogusFile", - subScenario: `test exit code`, - fs: () => loadProjectFromFiles({}), - commandLineArgs: ["-b", "bogus.json"] - }); +import { verifyTsc, loadProjectFromFiles } from "../../ts"; +// https://github.com/microsoft/TypeScript/issues/33849 +describe("unittests:: tsbuild:: exitCodeOnBogusFile:: test exit code", () => { + verifyTsc({ + scenario: "exitCodeOnBogusFile", + subScenario: `test exit code`, + fs: () => loadProjectFromFiles({}), + commandLineArgs: ["-b", "bogus.json"] }); -} +}); diff --git a/src/testRunner/unittests/tsbuild/graphOrdering.ts b/src/testRunner/unittests/tsbuild/graphOrdering.ts index 79ddd80d67bde..bebbb73f6bd18 100644 --- a/src/testRunner/unittests/tsbuild/graphOrdering.ts +++ b/src/testRunner/unittests/tsbuild/graphOrdering.ts @@ -1,91 +1,85 @@ -namespace ts { - describe("unittests:: tsbuild - graph-ordering", () => { - let host: fakes.SolutionBuilderHost | undefined; - const deps: [string, string][] = [ - ["A", "B"], - ["B", "C"], - ["A", "C"], - ["B", "D"], - ["C", "D"], - ["C", "E"], - ["F", "E"], - ["H", "I"], - ["I", "J"], - ["J", "H"], - ["J", "E"] - ]; - - before(() => { - const fs = new vfs.FileSystem(false); - host = fakes.SolutionBuilderHost.create(fs); - writeProjects(fs, ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"], deps); - }); - - after(() => { - host = undefined; - }); - - it("orders the graph correctly - specify two roots", () => { - checkGraphOrdering(["A", "G"], ["D", "E", "C", "B", "A", "G"]); - }); - - it("orders the graph correctly - multiple parts of the same graph in various orders", () => { - checkGraphOrdering(["A"], ["D", "E", "C", "B", "A"]); - checkGraphOrdering(["A", "C", "D"], ["D", "E", "C", "B", "A"]); - checkGraphOrdering(["D", "C", "A"], ["D", "E", "C", "B", "A"]); - }); - - it("orders the graph correctly - other orderings", () => { - checkGraphOrdering(["F"], ["E", "F"]); - checkGraphOrdering(["E"], ["E"]); - checkGraphOrdering(["F", "C", "A"], ["E", "F", "D", "C", "B", "A"]); - }); - - it("returns circular order", () => { - checkGraphOrdering(["H"], ["E", "J", "I", "H"], /*circular*/ true); - checkGraphOrdering(["A", "H"], ["D", "E", "C", "B", "A", "J", "I", "H"], /*circular*/ true); - }); - - function checkGraphOrdering(rootNames: string[], expectedBuildSet: string[], circular?: true) { - const builder = createSolutionBuilder(host!, rootNames.map(getProjectFileName), { dry: true, force: false, verbose: false }); - const buildOrder = builder.getBuildOrder(); - assert.equal(isCircularBuildOrder(buildOrder), !!circular); - const buildQueue = getBuildOrderFromAnyBuildOrder(buildOrder); - assert.deepEqual(buildQueue, expectedBuildSet.map(getProjectFileName)); - - if (!circular) { - for (const dep of deps) { - const child = getProjectFileName(dep[0]); - if (buildQueue.indexOf(child) < 0) continue; - const parent = getProjectFileName(dep[1]); - assert.isAbove(buildQueue.indexOf(child), buildQueue.indexOf(parent), `Expecting child ${child} to be built after parent ${parent}`); - } +import { SolutionBuilderHost } from "../../fakes"; +import { FileSystem } from "../../vfs"; +import { createSolutionBuilder, isCircularBuildOrder, getBuildOrderFromAnyBuildOrder, ResolvedConfigFileName } from "../../ts"; +describe("unittests:: tsbuild - graph-ordering", () => { + let host: SolutionBuilderHost | undefined; + const deps: [string, string][] = [ + ["A", "B"], + ["B", "C"], + ["A", "C"], + ["B", "D"], + ["C", "D"], + ["C", "E"], + ["F", "E"], + ["H", "I"], + ["I", "J"], + ["J", "H"], + ["J", "E"] + ]; + before(() => { + const fs = new FileSystem(false); + host = SolutionBuilderHost.create(fs); + writeProjects(fs, ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"], deps); + }); + after(() => { + host = undefined; + }); + it("orders the graph correctly - specify two roots", () => { + checkGraphOrdering(["A", "G"], ["D", "E", "C", "B", "A", "G"]); + }); + it("orders the graph correctly - multiple parts of the same graph in various orders", () => { + checkGraphOrdering(["A"], ["D", "E", "C", "B", "A"]); + checkGraphOrdering(["A", "C", "D"], ["D", "E", "C", "B", "A"]); + checkGraphOrdering(["D", "C", "A"], ["D", "E", "C", "B", "A"]); + }); + it("orders the graph correctly - other orderings", () => { + checkGraphOrdering(["F"], ["E", "F"]); + checkGraphOrdering(["E"], ["E"]); + checkGraphOrdering(["F", "C", "A"], ["E", "F", "D", "C", "B", "A"]); + }); + it("returns circular order", () => { + checkGraphOrdering(["H"], ["E", "J", "I", "H"], /*circular*/ true); + checkGraphOrdering(["A", "H"], ["D", "E", "C", "B", "A", "J", "I", "H"], /*circular*/ true); + }); + function checkGraphOrdering(rootNames: string[], expectedBuildSet: string[], circular?: true) { + const builder = createSolutionBuilder((host!), rootNames.map(getProjectFileName), { dry: true, force: false, verbose: false }); + const buildOrder = builder.getBuildOrder(); + assert.equal(isCircularBuildOrder(buildOrder), !!circular); + const buildQueue = getBuildOrderFromAnyBuildOrder(buildOrder); + assert.deepEqual(buildQueue, expectedBuildSet.map(getProjectFileName)); + if (!circular) { + for (const dep of deps) { + const child = getProjectFileName(dep[0]); + if (buildQueue.indexOf(child) < 0) + continue; + const parent = getProjectFileName(dep[1]); + assert.isAbove(buildQueue.indexOf(child), buildQueue.indexOf(parent), `Expecting child ${child} to be built after parent ${parent}`); } } - - function getProjectFileName(proj: string) { - return `/project/${proj}/tsconfig.json` as ResolvedConfigFileName; + } + function getProjectFileName(proj: string) { + return `/project/${proj}/tsconfig.json` as ResolvedConfigFileName; + } + function writeProjects(fileSystem: FileSystem, projectNames: string[], deps: [string, string][]): string[] { + const projFileNames: string[] = []; + for (const dep of deps) { + if (projectNames.indexOf(dep[0]) < 0) + throw new Error(`Invalid dependency - project ${dep[0]} does not exist`); + if (projectNames.indexOf(dep[1]) < 0) + throw new Error(`Invalid dependency - project ${dep[1]} does not exist`); } - - function writeProjects(fileSystem: vfs.FileSystem, projectNames: string[], deps: [string, string][]): string[] { - const projFileNames: string[] = []; - for (const dep of deps) { - if (projectNames.indexOf(dep[0]) < 0) throw new Error(`Invalid dependency - project ${dep[0]} does not exist`); - if (projectNames.indexOf(dep[1]) < 0) throw new Error(`Invalid dependency - project ${dep[1]} does not exist`); - } - for (const proj of projectNames) { - fileSystem.mkdirpSync(`/project/${proj}`); - fileSystem.writeFileSync(`/project/${proj}/${proj}.ts`, "export {}"); - const configFileName = getProjectFileName(proj); - const configContent = JSON.stringify({ - compilerOptions: { composite: true }, - files: [`./${proj}.ts`], - references: deps.filter(d => d[0] === proj).map(d => ({ path: `../${d[1]}` })) - }, undefined, 2); - fileSystem.writeFileSync(configFileName, configContent); - projFileNames.push(configFileName); - } - return projFileNames; + for (const proj of projectNames) { + fileSystem.mkdirpSync(`/project/${proj}`); + fileSystem.writeFileSync(`/project/${proj}/${proj}.ts`, "export {}"); + const configFileName = getProjectFileName(proj); + const configContent = JSON.stringify({ + compilerOptions: { composite: true }, + files: [`./${proj}.ts`], + references: deps.filter(d => d[0] === proj).map(d => ({ path: `../${d[1]}` })) + }, undefined, 2); + fileSystem.writeFileSync(configFileName, configContent); + projFileNames.push(configFileName); } - }); -} + return projFileNames; + } +}); diff --git a/src/testRunner/unittests/tsbuild/helpers.ts b/src/testRunner/unittests/tsbuild/helpers.ts index cadfa8bde60cc..534412ad1792b 100644 --- a/src/testRunner/unittests/tsbuild/helpers.ts +++ b/src/testRunner/unittests/tsbuild/helpers.ts @@ -1,108 +1,99 @@ -namespace ts { - export function errorDiagnostic(message: fakes.ExpectedDiagnosticMessage): fakes.ExpectedErrorDiagnostic { - return { message }; +import { ExpectedDiagnosticMessage, ExpectedErrorDiagnostic, ExpectedDiagnostic, SolutionBuilderHost, version, ExpectedDiagnosticLocation, System } from "../../fakes"; +import { Diagnostics, isBuildInfoFile, getBuildInfo, getBuildInfoText, TestFSWithWatch, notImplemented, mapDefinedIterator, BundleFileInfo, length, emptyArray, BundleFileSectionKind, Debug, first, last, BundleFileSection, CompilerOptions, getOutputPathsForBundle, BuildKind, TscCompile, TscCompileSystem, tscCompile, verifyTscBaseline, createSolutionBuilder, arrayFrom } from "../../ts"; +import { FileSystem, createResolver, Mount, FileSet, FileSystemResolverHost } from "../../vfs"; +import { IO, SourceMapRecorder, Compiler } from "../../Harness"; +import { resolve } from "../../vpath"; +import * as ts from "../../ts"; +export function errorDiagnostic(message: ExpectedDiagnosticMessage): ExpectedErrorDiagnostic { + return { message }; +} +export function getExpectedDiagnosticForProjectsInBuild(...projects: string[]): ExpectedDiagnostic { + return [Diagnostics.Projects_in_this_build_Colon_0, projects.map(p => "\r\n * " + p).join("")]; +} +export function changeCompilerVersion(host: SolutionBuilderHost) { + const originalReadFile = host.readFile; + host.readFile = path => { + const value = originalReadFile.call(host, path); + if (!value || !isBuildInfoFile(path)) + return value; + const buildInfo = getBuildInfo(value); + buildInfo.version = version; + return getBuildInfoText(buildInfo); + }; +} +export function replaceText(fs: FileSystem, path: string, oldText: string, newText: string) { + if (!fs.statSync(path).isFile()) { + throw new Error(`File ${path} does not exist`); } - - export function getExpectedDiagnosticForProjectsInBuild(...projects: string[]): fakes.ExpectedDiagnostic { - return [Diagnostics.Projects_in_this_build_Colon_0, projects.map(p => "\r\n * " + p).join("")]; + const old = fs.readFileSync(path, "utf-8"); + if (old.indexOf(oldText) < 0) { + throw new Error(`Text "${oldText}" does not exist in file ${path}`); } - - export function changeCompilerVersion(host: fakes.SolutionBuilderHost) { - const originalReadFile = host.readFile; - host.readFile = path => { - const value = originalReadFile.call(host, path); - if (!value || !isBuildInfoFile(path)) return value; - const buildInfo = getBuildInfo(value); - buildInfo.version = fakes.version; - return getBuildInfoText(buildInfo); - }; + const newContent = old.replace(oldText, newText); + fs.writeFileSync(path, newContent, "utf-8"); +} +export function prependText(fs: FileSystem, path: string, additionalContent: string) { + if (!fs.statSync(path).isFile()) { + throw new Error(`File ${path} does not exist`); } - - export function replaceText(fs: vfs.FileSystem, path: string, oldText: string, newText: string) { - if (!fs.statSync(path).isFile()) { - throw new Error(`File ${path} does not exist`); - } - const old = fs.readFileSync(path, "utf-8"); - if (old.indexOf(oldText) < 0) { - throw new Error(`Text "${oldText}" does not exist in file ${path}`); - } - const newContent = old.replace(oldText, newText); - fs.writeFileSync(path, newContent, "utf-8"); + const old = fs.readFileSync(path, "utf-8"); + fs.writeFileSync(path, `${additionalContent}${old}`, "utf-8"); +} +export function appendText(fs: FileSystem, path: string, additionalContent: string) { + if (!fs.statSync(path).isFile()) { + throw new Error(`File ${path} does not exist`); } - - export function prependText(fs: vfs.FileSystem, path: string, additionalContent: string) { - if (!fs.statSync(path).isFile()) { - throw new Error(`File ${path} does not exist`); - } - const old = fs.readFileSync(path, "utf-8"); - fs.writeFileSync(path, `${additionalContent}${old}`, "utf-8"); + const old = fs.readFileSync(path, "utf-8"); + fs.writeFileSync(path, `${old}${additionalContent}`); +} +export function indexOf(fs: FileSystem, path: string, searchStr: string) { + if (!fs.statSync(path).isFile()) { + throw new Error(`File ${path} does not exist`); } - - export function appendText(fs: vfs.FileSystem, path: string, additionalContent: string) { - if (!fs.statSync(path).isFile()) { - throw new Error(`File ${path} does not exist`); - } - const old = fs.readFileSync(path, "utf-8"); - fs.writeFileSync(path, `${old}${additionalContent}`); + const content = fs.readFileSync(path, "utf-8"); + return content.indexOf(searchStr); +} +export function lastIndexOf(fs: FileSystem, path: string, searchStr: string) { + if (!fs.statSync(path).isFile()) { + throw new Error(`File ${path} does not exist`); } - - export function indexOf(fs: vfs.FileSystem, path: string, searchStr: string) { - if (!fs.statSync(path).isFile()) { - throw new Error(`File ${path} does not exist`); - } - const content = fs.readFileSync(path, "utf-8"); - return content.indexOf(searchStr); + const content = fs.readFileSync(path, "utf-8"); + return content.lastIndexOf(searchStr); +} +export function expectedLocationIndexOf(fs: FileSystem, file: string, searchStr: string): ExpectedDiagnosticLocation { + return { + file, + start: indexOf(fs, file, searchStr), + length: searchStr.length + }; +} +export function expectedLocationLastIndexOf(fs: FileSystem, file: string, searchStr: string): ExpectedDiagnosticLocation { + return { + file, + start: lastIndexOf(fs, file, searchStr), + length: searchStr.length + }; +} +export function getTime() { + let currentTime = 100; + return { tick, time, touch }; + function tick() { + currentTime += 60000; + } + function time() { + return currentTime; } - - export function lastIndexOf(fs: vfs.FileSystem, path: string, searchStr: string) { + function touch(fs: FileSystem, path: string) { if (!fs.statSync(path).isFile()) { throw new Error(`File ${path} does not exist`); } - const content = fs.readFileSync(path, "utf-8"); - return content.lastIndexOf(searchStr); + fs.utimesSync(path, new Date(time()), new Date(time())); } - - export function expectedLocationIndexOf(fs: vfs.FileSystem, file: string, searchStr: string): fakes.ExpectedDiagnosticLocation { - return { - file, - start: indexOf(fs, file, searchStr), - length: searchStr.length - }; - } - - export function expectedLocationLastIndexOf(fs: vfs.FileSystem, file: string, searchStr: string): fakes.ExpectedDiagnosticLocation { - return { - file, - start: lastIndexOf(fs, file, searchStr), - length: searchStr.length - }; - } - - export function getTime() { - let currentTime = 100; - return { tick, time, touch }; - - function tick() { - currentTime += 60_000; - } - - function time() { - return currentTime; - } - - function touch(fs: vfs.FileSystem, path: string) { - if (!fs.statSync(path).isFile()) { - throw new Error(`File ${path} does not exist`); - } - fs.utimesSync(path, new Date(time()), new Date(time())); - } - } - - export const libContent = `${TestFSWithWatch.libFile.content} +} +export const libContent = `${TestFSWithWatch.libFile.content} interface ReadonlyArray {} declare const console: { log(msg: any): void; };`; - - export const symbolLibContent = ` +export const symbolLibContent = ` interface SymbolConstructor { readonly species: symbol; readonly toStringTag: symbol; @@ -112,313 +103,271 @@ interface Symbol { readonly [Symbol.toStringTag]: string; } `; - - /** - * Load project from disk into /src folder - */ - export function loadProjectFromDisk( - root: string, - libContentToAppend?: string - ): vfs.FileSystem { - const resolver = vfs.createResolver(Harness.IO); - const fs = new vfs.FileSystem(/*ignoreCase*/ true, { - files: { - ["/src"]: new vfs.Mount(vpath.resolve(Harness.IO.getWorkspaceRoot(), root), resolver) - }, - cwd: "/", - meta: { defaultLibLocation: "/lib" }, - }); - addLibAndMakeReadonly(fs, libContentToAppend); - return fs; - } - - /** - * All the files must be in /src - */ - export function loadProjectFromFiles( - files: vfs.FileSet, - libContentToAppend?: string - ): vfs.FileSystem { - const fs = new vfs.FileSystem(/*ignoreCase*/ true, { - files, - cwd: "/", - meta: { defaultLibLocation: "/lib" }, - }); - addLibAndMakeReadonly(fs, libContentToAppend); - return fs; - } - - function addLibAndMakeReadonly(fs: vfs.FileSystem, libContentToAppend?: string) { - fs.mkdirSync("/lib"); - fs.writeFileSync("/lib/lib.d.ts", libContentToAppend ? `${libContent}${libContentToAppend}` : libContent); - fs.makeReadonly(); - } - - /** - * Gets the FS mountuing existing fs's /src and /lib folder - */ - export function getFsWithTime(baseFs: vfs.FileSystem) { - const { time, tick } = getTime(); - const host = new fakes.System(baseFs) as any as vfs.FileSystemResolverHost; - host.getWorkspaceRoot = notImplemented; - const resolver = vfs.createResolver(host); - const fs = new vfs.FileSystem(/*ignoreCase*/ true, { - files: { - ["/src"]: new vfs.Mount("/src", resolver), - ["/lib"]: new vfs.Mount("/lib", resolver) - }, - cwd: "/", - meta: { defaultLibLocation: "/lib" }, - time - }); - return { fs, time, tick }; - } - - export function verifyOutputsPresent(fs: vfs.FileSystem, outputs: readonly string[]) { - for (const output of outputs) { - assert(fs.existsSync(output), `Expect file ${output} to exist`); - } +/** + * Load project from disk into /src folder + */ +export function loadProjectFromDisk(root: string, libContentToAppend?: string): FileSystem { + const resolver = createResolver(IO); + const fs = new FileSystem(/*ignoreCase*/ true, { + files: { + ["/src"]: new Mount(resolve(IO.getWorkspaceRoot(), root), resolver) + }, + cwd: "/", + meta: { defaultLibLocation: "/lib" }, + }); + addLibAndMakeReadonly(fs, libContentToAppend); + return fs; +} +/** + * All the files must be in /src + */ +export function loadProjectFromFiles(files: FileSet, libContentToAppend?: string): FileSystem { + const fs = new FileSystem(/*ignoreCase*/ true, { + files, + cwd: "/", + meta: { defaultLibLocation: "/lib" }, + }); + addLibAndMakeReadonly(fs, libContentToAppend); + return fs; +} +function addLibAndMakeReadonly(fs: FileSystem, libContentToAppend?: string) { + fs.mkdirSync("/lib"); + fs.writeFileSync("/lib/lib.d.ts", libContentToAppend ? `${libContent}${libContentToAppend}` : libContent); + fs.makeReadonly(); +} +/** + * Gets the FS mountuing existing fs's /src and /lib folder + */ +export function getFsWithTime(baseFs: FileSystem) { + const { time, tick } = getTime(); + const host = (new System(baseFs) as any as FileSystemResolverHost); + host.getWorkspaceRoot = notImplemented; + const resolver = createResolver(host); + const fs = new FileSystem(/*ignoreCase*/ true, { + files: { + ["/src"]: new Mount("/src", resolver), + ["/lib"]: new Mount("/lib", resolver) + }, + cwd: "/", + meta: { defaultLibLocation: "/lib" }, + time + }); + return { fs, time, tick }; +} +export function verifyOutputsPresent(fs: FileSystem, outputs: readonly string[]) { + for (const output of outputs) { + assert(fs.existsSync(output), `Expect file ${output} to exist`); } - - export function verifyOutputsAbsent(fs: vfs.FileSystem, outputs: readonly string[]) { - for (const output of outputs) { - assert.isFalse(fs.existsSync(output), `Expect file ${output} to not exist`); - } +} +export function verifyOutputsAbsent(fs: FileSystem, outputs: readonly string[]) { + for (const output of outputs) { + assert.isFalse(fs.existsSync(output), `Expect file ${output} to not exist`); } - - export function generateSourceMapBaselineFiles(sys: System & { writtenFiles: Map; }) { - const mapFileNames = mapDefinedIterator(sys.writtenFiles.keys(), f => f.endsWith(".map") ? f : undefined); - while (true) { - const { value: mapFile, done } = mapFileNames.next(); - if (done) break; - const text = Harness.SourceMapRecorder.getSourceMapRecordWithSystem(sys, mapFile); - sys.writeFile(`${mapFile}.baseline.txt`, text); - } +} +export function generateSourceMapBaselineFiles(sys: ts.System & { + writtenFiles: ts.Map; +}) { + const mapFileNames = mapDefinedIterator(sys.writtenFiles.keys(), f => f.endsWith(".map") ? f : undefined); + while (true) { + const { value: mapFile, done } = mapFileNames.next(); + if (done) + break; + const text = SourceMapRecorder.getSourceMapRecordWithSystem(sys, mapFile); + sys.writeFile(`${mapFile}.baseline.txt`, text); } - - function generateBundleFileSectionInfo(sys: System, originalReadCall: System["readFile"], baselineRecorder: Harness.Compiler.WriterAggregator, bundleFileInfo: BundleFileInfo | undefined, outFile: string | undefined) { - if (!length(bundleFileInfo && bundleFileInfo.sections) && !outFile) return; // Nothing to baseline - - const content = outFile && sys.fileExists(outFile) ? originalReadCall.call(sys, outFile, "utf8")! : ""; - baselineRecorder.WriteLine("======================================================================"); - baselineRecorder.WriteLine(`File:: ${outFile}`); - for (const section of bundleFileInfo ? bundleFileInfo.sections : emptyArray) { - baselineRecorder.WriteLine("----------------------------------------------------------------------"); - writeSectionHeader(section); - if (section.kind !== BundleFileSectionKind.Prepend) { - writeTextOfSection(section.pos, section.end); - } - else if (section.texts.length > 0) { - Debug.assert(section.pos === first(section.texts).pos); - Debug.assert(section.end === last(section.texts).end); - for (const text of section.texts) { - baselineRecorder.WriteLine(">>--------------------------------------------------------------------"); - writeSectionHeader(text); - writeTextOfSection(text.pos, text.end); - } - } - else { - Debug.assert(section.pos === section.end); - } +} +function generateBundleFileSectionInfo(sys: ts.System, originalReadCall: ts.System["readFile"], baselineRecorder: Compiler.WriterAggregator, bundleFileInfo: BundleFileInfo | undefined, outFile: string | undefined) { + if (!length(bundleFileInfo && bundleFileInfo.sections) && !outFile) + return; // Nothing to baseline + const content = outFile && sys.fileExists(outFile) ? originalReadCall.call(sys, outFile, "utf8")! : ""; + baselineRecorder.WriteLine("======================================================================"); + baselineRecorder.WriteLine(`File:: ${outFile}`); + for (const section of bundleFileInfo ? bundleFileInfo.sections : emptyArray) { + baselineRecorder.WriteLine("----------------------------------------------------------------------"); + writeSectionHeader(section); + if (section.kind !== BundleFileSectionKind.Prepend) { + writeTextOfSection(section.pos, section.end); } - baselineRecorder.WriteLine("======================================================================"); - - function writeTextOfSection(pos: number, end: number) { - const textLines = content.substring(pos, end).split(/\r?\n/); - for (const line of textLines) { - baselineRecorder.WriteLine(line); + else if (section.texts.length > 0) { + Debug.assert(section.pos === first(section.texts).pos); + Debug.assert(section.end === last(section.texts).end); + for (const text of section.texts) { + baselineRecorder.WriteLine(">>--------------------------------------------------------------------"); + writeSectionHeader(text); + writeTextOfSection(text.pos, text.end); } } - - function writeSectionHeader(section: BundleFileSection) { - baselineRecorder.WriteLine(`${section.kind}: (${section.pos}-${section.end})${section.data ? ":: " + section.data : ""}${section.kind === BundleFileSectionKind.Prepend ? " texts:: " + section.texts.length : ""}`); + else { + Debug.assert(section.pos === section.end); } } - - export function baselineBuildInfo( - options: CompilerOptions, - sys: System & { writtenFiles: Map; }, - originalReadCall?: System["readFile"] - ) { - const out = options.outFile || options.out; - if (!out) return; - const { buildInfoPath, jsFilePath, declarationFilePath } = getOutputPathsForBundle(options, /*forceDts*/ false); - if (!buildInfoPath || !sys.writtenFiles.has(buildInfoPath)) return; - if (!sys.fileExists(buildInfoPath)) return; - - const buildInfo = getBuildInfo((originalReadCall || sys.readFile).call(sys, buildInfoPath, "utf8")!); - const bundle = buildInfo.bundle; - if (!bundle || (!length(bundle.js && bundle.js.sections) && !length(bundle.dts && bundle.dts.sections))) return; - - // Write the baselines: - const baselineRecorder = new Harness.Compiler.WriterAggregator(); - generateBundleFileSectionInfo(sys, originalReadCall || sys.readFile, baselineRecorder, bundle.js, jsFilePath); - generateBundleFileSectionInfo(sys, originalReadCall || sys.readFile, baselineRecorder, bundle.dts, declarationFilePath); - baselineRecorder.Close(); - - const text = baselineRecorder.lines.join("\r\n"); - sys.writeFile(`${buildInfoPath}.baseline.txt`, text); - } - - export interface TscIncremental { - buildKind: BuildKind; - modifyFs: (fs: vfs.FileSystem) => void; - subScenario?: string; - commandLineArgs?: readonly string[]; + baselineRecorder.WriteLine("======================================================================"); + function writeTextOfSection(pos: number, end: number) { + const textLines = content.substring(pos, end).split(/\r?\n/); + for (const line of textLines) { + baselineRecorder.WriteLine(line); + } } - - export interface VerifyTsBuildInput extends TscCompile { - incrementalScenarios: TscIncremental[]; + function writeSectionHeader(section: BundleFileSection) { + baselineRecorder.WriteLine(`${section.kind}: (${section.pos}-${section.end})${section.data ? ":: " + section.data : ""}${section.kind === BundleFileSectionKind.Prepend ? " texts:: " + section.texts.length : ""}`); } - - export function verifyTscIncrementalEdits({ - subScenario, fs, scenario, commandLineArgs, - baselineSourceMap, modifyFs, baselineReadFileCalls, - incrementalScenarios - }: VerifyTsBuildInput) { - describe(`tsc ${commandLineArgs.join(" ")} ${scenario}:: ${subScenario}`, () => { - let tick: () => void; - let sys: TscCompileSystem; - before(() => { - let baseFs: vfs.FileSystem; - ({ fs: baseFs, tick } = getFsWithTime(fs())); - sys = tscCompile({ - scenario, - subScenario, - fs: () => baseFs.makeReadonly(), - commandLineArgs, - modifyFs: fs => { - if (modifyFs) modifyFs(fs); - tick(); - }, - baselineSourceMap, - baselineReadFileCalls - }); - Debug.assert(!!incrementalScenarios.length, `${scenario}/${subScenario}:: No incremental scenarios, you probably want to use verifyTsc instead.`); - }); - after(() => { - sys = undefined!; - tick = undefined!; - }); - describe("initialBuild", () => { - verifyTscBaseline(() => sys); +} +export function baselineBuildInfo(options: CompilerOptions, sys: ts.System & { + writtenFiles: ts.Map; +}, originalReadCall?: ts.System["readFile"]) { + const out = options.outFile || options.out; + if (!out) + return; + const { buildInfoPath, jsFilePath, declarationFilePath } = getOutputPathsForBundle(options, /*forceDts*/ false); + if (!buildInfoPath || !sys.writtenFiles.has(buildInfoPath)) + return; + if (!sys.fileExists(buildInfoPath)) + return; + const buildInfo = getBuildInfo(((originalReadCall || sys.readFile).call(sys, buildInfoPath, "utf8")!)); + const bundle = buildInfo.bundle; + if (!bundle || (!length(bundle.js && bundle.js.sections) && !length(bundle.dts && bundle.dts.sections))) + return; + // Write the baselines: + const baselineRecorder = new Compiler.WriterAggregator(); + generateBundleFileSectionInfo(sys, originalReadCall || sys.readFile, baselineRecorder, bundle.js, jsFilePath); + generateBundleFileSectionInfo(sys, originalReadCall || sys.readFile, baselineRecorder, bundle.dts, declarationFilePath); + baselineRecorder.Close(); + const text = baselineRecorder.lines.join("\r\n"); + sys.writeFile(`${buildInfoPath}.baseline.txt`, text); +} +export interface TscIncremental { + buildKind: BuildKind; + modifyFs: (fs: FileSystem) => void; + subScenario?: string; + commandLineArgs?: readonly string[]; +} +export interface VerifyTsBuildInput extends TscCompile { + incrementalScenarios: TscIncremental[]; +} +export function verifyTscIncrementalEdits({ subScenario, fs, scenario, commandLineArgs, baselineSourceMap, modifyFs, baselineReadFileCalls, incrementalScenarios }: VerifyTsBuildInput) { + describe(`tsc ${commandLineArgs.join(" ")} ${scenario}:: ${subScenario}`, () => { + let tick: () => void; + let sys: TscCompileSystem; + before(() => { + let baseFs: FileSystem; + ({ fs: baseFs, tick } = getFsWithTime(fs())); + sys = tscCompile({ + scenario, + subScenario, + fs: () => baseFs.makeReadonly(), + commandLineArgs, + modifyFs: fs => { + if (modifyFs) + modifyFs(fs); + tick(); + }, + baselineSourceMap, + baselineReadFileCalls }); - - for (const { - buildKind, - modifyFs, - subScenario: incrementalSubScenario, - commandLineArgs: incrementalCommandLineArgs - } of incrementalScenarios) { - describe(incrementalSubScenario || buildKind, () => { - let newSys: TscCompileSystem; - before(() => { - Debug.assert(buildKind !== BuildKind.Initial, "Incremental edit cannot be initial compilation"); - tick(); - newSys = tscCompile({ - scenario, - subScenario: incrementalSubScenario || subScenario, - buildKind, - fs: () => sys.vfs, - commandLineArgs: incrementalCommandLineArgs || commandLineArgs, - modifyFs: fs => { - tick(); - modifyFs(fs); - tick(); - }, - baselineSourceMap, - baselineReadFileCalls - }); - }); - after(() => { - newSys = undefined!; + Debug.assert(!!incrementalScenarios.length, `${scenario}/${subScenario}:: No incremental scenarios, you probably want to use verifyTsc instead.`); + }); + after(() => { + sys = undefined!; + tick = undefined!; + }); + describe("initialBuild", () => { + verifyTscBaseline(() => sys); + }); + for (const { buildKind, modifyFs, subScenario: incrementalSubScenario, commandLineArgs: incrementalCommandLineArgs } of incrementalScenarios) { + describe(incrementalSubScenario || buildKind, () => { + let newSys: TscCompileSystem; + before(() => { + Debug.assert(buildKind !== BuildKind.Initial, "Incremental edit cannot be initial compilation"); + tick(); + newSys = tscCompile({ + scenario, + subScenario: incrementalSubScenario || subScenario, + buildKind, + fs: () => sys.vfs, + commandLineArgs: incrementalCommandLineArgs || commandLineArgs, + modifyFs: fs => { + tick(); + modifyFs(fs); + tick(); + }, + baselineSourceMap, + baselineReadFileCalls }); - verifyTscBaseline(() => newSys); - it(`Verify emit output file text is same when built clean`, () => { - const sys = tscCompile({ - scenario, - subScenario, - fs: () => newSys.vfs, - commandLineArgs, - modifyFs: fs => { - tick(); - // Delete output files - const host = fakes.SolutionBuilderHost.create(fs); - const builder = createSolutionBuilder(host, commandLineArgs, { clean: true }); - builder.clean(); - }, - }); - - for (const outputFile of arrayFrom(sys.writtenFiles.keys())) { - const expectedText = sys.readFile(outputFile); - const actualText = newSys.readFile(outputFile); - assert.equal(actualText, expectedText, `File: ${outputFile}`); - } + }); + after(() => { + newSys = undefined!; + }); + verifyTscBaseline(() => newSys); + it(`Verify emit output file text is same when built clean`, () => { + const sys = tscCompile({ + scenario, + subScenario, + fs: () => newSys.vfs, + commandLineArgs, + modifyFs: fs => { + tick(); + // Delete output files + const host = SolutionBuilderHost.create(fs); + const builder = createSolutionBuilder(host, commandLineArgs, { clean: true }); + builder.clean(); + }, }); + for (const outputFile of arrayFrom(sys.writtenFiles.keys())) { + const expectedText = sys.readFile(outputFile); + const actualText = newSys.readFile(outputFile); + assert.equal(actualText, expectedText, `File: ${outputFile}`); + } }); - } - }); - } - - export function enableStrict(fs: vfs.FileSystem, path: string) { - replaceText(fs, path, `"strict": false`, `"strict": true`); - } - - export function addTestPrologue(fs: vfs.FileSystem, path: string, prologue: string) { - prependText(fs, path, `${prologue} + }); + } + }); +} +export function enableStrict(fs: FileSystem, path: string) { + replaceText(fs, path, `"strict": false`, `"strict": true`); +} +export function addTestPrologue(fs: FileSystem, path: string, prologue: string) { + prependText(fs, path, `${prologue} `); - } - - export function addShebang(fs: vfs.FileSystem, project: string, file: string) { - prependText(fs, `src/${project}/${file}.ts`, `#!someshebang ${project} ${file} +} +export function addShebang(fs: FileSystem, project: string, file: string) { + prependText(fs, `src/${project}/${file}.ts`, `#!someshebang ${project} ${file} `); - } - - export function restContent(project: string, file: string) { - return `function for${project}${file}Rest() { +} +export function restContent(project: string, file: string) { + return `function for${project}${file}Rest() { const { b, ...rest } = { a: 10, b: 30, yy: 30 }; }`; - } - - function nonrestContent(project: string, file: string) { - return `function for${project}${file}Rest() { }`; - } - - export function addRest(fs: vfs.FileSystem, project: string, file: string) { - appendText(fs, `src/${project}/${file}.ts`, restContent(project, file)); - } - - export function removeRest(fs: vfs.FileSystem, project: string, file: string) { - replaceText(fs, `src/${project}/${file}.ts`, restContent(project, file), nonrestContent(project, file)); - } - - export function addStubFoo(fs: vfs.FileSystem, project: string, file: string) { - appendText(fs, `src/${project}/${file}.ts`, nonrestContent(project, file)); - } - - export function changeStubToRest(fs: vfs.FileSystem, project: string, file: string) { - replaceText(fs, `src/${project}/${file}.ts`, nonrestContent(project, file), restContent(project, file)); - } - - export function addSpread(fs: vfs.FileSystem, project: string, file: string) { - const path = `src/${project}/${file}.ts`; - const content = fs.readFileSync(path, "utf8"); - fs.writeFileSync(path, `${content} +} +function nonrestContent(project: string, file: string) { + return `function for${project}${file}Rest() { }`; +} +export function addRest(fs: FileSystem, project: string, file: string) { + appendText(fs, `src/${project}/${file}.ts`, restContent(project, file)); +} +export function removeRest(fs: FileSystem, project: string, file: string) { + replaceText(fs, `src/${project}/${file}.ts`, restContent(project, file), nonrestContent(project, file)); +} +export function addStubFoo(fs: FileSystem, project: string, file: string) { + appendText(fs, `src/${project}/${file}.ts`, nonrestContent(project, file)); +} +export function changeStubToRest(fs: FileSystem, project: string, file: string) { + replaceText(fs, `src/${project}/${file}.ts`, nonrestContent(project, file), restContent(project, file)); +} +export function addSpread(fs: FileSystem, project: string, file: string) { + const path = `src/${project}/${file}.ts`; + const content = fs.readFileSync(path, "utf8"); + fs.writeFileSync(path, `${content} function ${project}${file}Spread(...b: number[]) { } ${project}${file}Spread(...[10, 20, 30]);`); - - replaceText(fs, `src/${project}/tsconfig.json`, `"strict": false,`, `"strict": false, + replaceText(fs, `src/${project}/tsconfig.json`, `"strict": false,`, `"strict": false, "downlevelIteration": true,`); - } - - export function getTripleSlashRef(project: string) { - return `/src/${project}/tripleRef.d.ts`; - } - - export function addTripleSlashRef(fs: vfs.FileSystem, project: string, file: string) { - fs.writeFileSync(getTripleSlashRef(project), `declare class ${project}${file} { }`); - prependText(fs, `src/${project}/${file}.ts`, `/// +} +export function getTripleSlashRef(project: string) { + return `/src/${project}/tripleRef.d.ts`; +} +export function addTripleSlashRef(fs: FileSystem, project: string, file: string) { + fs.writeFileSync(getTripleSlashRef(project), `declare class ${project}${file} { }`); + prependText(fs, `src/${project}/${file}.ts`, `/// const ${file}Const = new ${project}${file}(); `); - } } diff --git a/src/testRunner/unittests/tsbuild/inferredTypeFromTransitiveModule.ts b/src/testRunner/unittests/tsbuild/inferredTypeFromTransitiveModule.ts index eb3658d14ccaa..146faeab7cf38 100644 --- a/src/testRunner/unittests/tsbuild/inferredTypeFromTransitiveModule.ts +++ b/src/testRunner/unittests/tsbuild/inferredTypeFromTransitiveModule.ts @@ -1,59 +1,54 @@ -namespace ts { - describe("unittests:: tsbuild:: inferredTypeFromTransitiveModule::", () => { - let projFs: vfs.FileSystem; - before(() => { - projFs = loadProjectFromDisk("tests/projects/inferredTypeFromTransitiveModule"); - }); - after(() => { - projFs = undefined!; - }); - - verifyTscIncrementalEdits({ - scenario: "inferredTypeFromTransitiveModule", - subScenario: "inferred type from transitive module", - fs: () => projFs, - commandLineArgs: ["--b", "/src", "--verbose"], - incrementalScenarios: [{ +import { FileSystem } from "../../vfs"; +import { loadProjectFromDisk, verifyTscIncrementalEdits, BuildKind, appendText, replaceText } from "../../ts"; +describe("unittests:: tsbuild:: inferredTypeFromTransitiveModule::", () => { + let projFs: FileSystem; + before(() => { + projFs = loadProjectFromDisk("tests/projects/inferredTypeFromTransitiveModule"); + }); + after(() => { + projFs = undefined!; + }); + verifyTscIncrementalEdits({ + scenario: "inferredTypeFromTransitiveModule", + subScenario: "inferred type from transitive module", + fs: () => projFs, + commandLineArgs: ["--b", "/src", "--verbose"], + incrementalScenarios: [{ buildKind: BuildKind.IncrementalDtsChange, modifyFs: changeBarParam, }], - }); - - verifyTscIncrementalEdits({ - subScenario: "inferred type from transitive module with isolatedModules", - fs: () => projFs, - scenario: "inferredTypeFromTransitiveModule", - commandLineArgs: ["--b", "/src", "--verbose"], - modifyFs: changeToIsolatedModules, - incrementalScenarios: [{ + }); + verifyTscIncrementalEdits({ + subScenario: "inferred type from transitive module with isolatedModules", + fs: () => projFs, + scenario: "inferredTypeFromTransitiveModule", + commandLineArgs: ["--b", "/src", "--verbose"], + modifyFs: changeToIsolatedModules, + incrementalScenarios: [{ buildKind: BuildKind.IncrementalDtsChange, modifyFs: changeBarParam }] - }); - - verifyTscIncrementalEdits({ - scenario: "inferredTypeFromTransitiveModule", - subScenario: "reports errors in files affected by change in signature with isolatedModules", - fs: () => projFs, - commandLineArgs: ["--b", "/src", "--verbose"], - modifyFs: fs => { - changeToIsolatedModules(fs); - appendText(fs, "/src/lazyIndex.ts", ` + }); + verifyTscIncrementalEdits({ + scenario: "inferredTypeFromTransitiveModule", + subScenario: "reports errors in files affected by change in signature with isolatedModules", + fs: () => projFs, + commandLineArgs: ["--b", "/src", "--verbose"], + modifyFs: fs => { + changeToIsolatedModules(fs); + appendText(fs, "/src/lazyIndex.ts", ` import { default as bar } from './bar'; bar("hello");`); - }, - incrementalScenarios: [{ + }, + incrementalScenarios: [{ buildKind: BuildKind.IncrementalDtsChange, modifyFs: changeBarParam }] - }); }); - - function changeToIsolatedModules(fs: vfs.FileSystem) { - replaceText(fs, "/src/tsconfig.json", `"incremental": true`, `"incremental": true, "isolatedModules": true`); - } - - function changeBarParam(fs: vfs.FileSystem) { - replaceText(fs, "/src/bar.ts", "param: string", ""); - } +}); +function changeToIsolatedModules(fs: FileSystem) { + replaceText(fs, "/src/tsconfig.json", `"incremental": true`, `"incremental": true, "isolatedModules": true`); +} +function changeBarParam(fs: FileSystem) { + replaceText(fs, "/src/bar.ts", "param: string", ""); } diff --git a/src/testRunner/unittests/tsbuild/javascriptProjectEmit.ts b/src/testRunner/unittests/tsbuild/javascriptProjectEmit.ts index 2f8284849cfae..18d8137a4b325 100644 --- a/src/testRunner/unittests/tsbuild/javascriptProjectEmit.ts +++ b/src/testRunner/unittests/tsbuild/javascriptProjectEmit.ts @@ -1,17 +1,19 @@ -namespace ts { - describe("unittests:: tsbuild:: javascriptProjectEmit:: loads js-based projects and emits them correctly", () => { - verifyTsc({ - scenario: "javascriptProjectEmit", - subScenario: `loads js-based projects and emits them correctly`, - fs: () => loadProjectFromFiles({ - "/src/common/nominal.js": Utils.dedent` +import { verifyTsc, loadProjectFromFiles, symbolLibContent, verifyTscIncrementalEdits, BuildKind, replaceText } from "../../ts"; +import { dedent } from "../../Utils"; +import { FileSystem } from "../../vfs"; +describe("unittests:: tsbuild:: javascriptProjectEmit:: loads js-based projects and emits them correctly", () => { + verifyTsc({ + scenario: "javascriptProjectEmit", + subScenario: `loads js-based projects and emits them correctly`, + fs: () => loadProjectFromFiles({ + "/src/common/nominal.js": dedent ` /** * @template T, Name * @typedef {T & {[Symbol.species]: Name}} Nominal */ module.exports = {}; `, - "/src/common/tsconfig.json": Utils.dedent` + "/src/common/tsconfig.json": dedent ` { "extends": "../tsconfig.base.json", "compilerOptions": { @@ -19,14 +21,14 @@ namespace ts { }, "include": ["nominal.js"] }`, - "/src/sub-project/index.js": Utils.dedent` + "/src/sub-project/index.js": dedent ` import { Nominal } from '../common/nominal'; /** * @typedef {Nominal} MyNominal */ `, - "/src/sub-project/tsconfig.json": Utils.dedent` + "/src/sub-project/tsconfig.json": dedent ` { "extends": "../tsconfig.base.json", "compilerOptions": { @@ -37,7 +39,7 @@ namespace ts { ], "include": ["./index.js"] }`, - "/src/sub-project-2/index.js": Utils.dedent` + "/src/sub-project-2/index.js": dedent ` import { MyNominal } from '../sub-project/index'; const variable = { @@ -51,7 +53,7 @@ namespace ts { return 'key'; } `, - "/src/sub-project-2/tsconfig.json": Utils.dedent` + "/src/sub-project-2/tsconfig.json": dedent ` { "extends": "../tsconfig.base.json", "compilerOptions": { @@ -62,7 +64,7 @@ namespace ts { ], "include": ["./index.js"] }`, - "/src/tsconfig.json": Utils.dedent` + "/src/tsconfig.json": dedent ` { "compilerOptions": { "composite": true @@ -73,7 +75,7 @@ namespace ts { ], "include": [] }`, - "/src/tsconfig.base.json": Utils.dedent` + "/src/tsconfig.base.json": dedent ` { "compilerOptions": { "skipLibCheck": true, @@ -84,22 +86,21 @@ namespace ts { "declaration": true } }`, - }, symbolLibContent), - commandLineArgs: ["-b", "/src"] - }); + }, symbolLibContent), + commandLineArgs: ["-b", "/src"] }); - - describe("unittests:: tsbuild:: javascriptProjectEmit:: loads outfile js projects and concatenates them correctly", () => { - let projFs: vfs.FileSystem; - before(() => { - projFs = loadProjectFromFiles({ - "/src/common/nominal.js": Utils.dedent` +}); +describe("unittests:: tsbuild:: javascriptProjectEmit:: loads outfile js projects and concatenates them correctly", () => { + let projFs: FileSystem; + before(() => { + projFs = loadProjectFromFiles({ + "/src/common/nominal.js": dedent ` /** * @template T, Name * @typedef {T & {[Symbol.species]: Name}} Nominal */ `, - "/src/common/tsconfig.json": Utils.dedent` + "/src/common/tsconfig.json": dedent ` { "extends": "../tsconfig.base.json", "compilerOptions": { @@ -108,13 +109,13 @@ namespace ts { }, "include": ["nominal.js"] }`, - "/src/sub-project/index.js": Utils.dedent` + "/src/sub-project/index.js": dedent ` /** * @typedef {Nominal} MyNominal */ const c = /** @type {*} */(null); `, - "/src/sub-project/tsconfig.json": Utils.dedent` + "/src/sub-project/tsconfig.json": dedent ` { "extends": "../tsconfig.base.json", "compilerOptions": { @@ -126,7 +127,7 @@ namespace ts { ], "include": ["./index.js"] }`, - "/src/sub-project-2/index.js": Utils.dedent` + "/src/sub-project-2/index.js": dedent ` const variable = { key: /** @type {MyNominal} */('value'), }; @@ -138,7 +139,7 @@ namespace ts { return 'key'; } `, - "/src/sub-project-2/tsconfig.json": Utils.dedent` + "/src/sub-project-2/tsconfig.json": dedent ` { "extends": "../tsconfig.base.json", "compilerOptions": { @@ -150,7 +151,7 @@ namespace ts { ], "include": ["./index.js"] }`, - "/src/tsconfig.json": Utils.dedent` + "/src/tsconfig.json": dedent ` { "compilerOptions": { "composite": true, @@ -162,7 +163,7 @@ namespace ts { ], "include": [] }`, - "/src/tsconfig.base.json": Utils.dedent` + "/src/tsconfig.base.json": dedent ` { "compilerOptions": { "skipLibCheck": true, @@ -172,43 +173,42 @@ namespace ts { "declaration": true } }`, - }, symbolLibContent); - }); - after(() => { - projFs = undefined!; - }); - verifyTsc({ - scenario: "javascriptProjectEmit", - subScenario: `loads outfile js projects and concatenates them correctly`, - fs: () => projFs, - commandLineArgs: ["-b", "/src"] - }); - verifyTscIncrementalEdits({ - scenario: "javascriptProjectEmit", - subScenario: `modifies outfile js projects and concatenates them correctly`, - fs: () => projFs, - commandLineArgs: ["-b", "/src"], - incrementalScenarios: [{ + }, symbolLibContent); + }); + after(() => { + projFs = undefined!; + }); + verifyTsc({ + scenario: "javascriptProjectEmit", + subScenario: `loads outfile js projects and concatenates them correctly`, + fs: () => projFs, + commandLineArgs: ["-b", "/src"] + }); + verifyTscIncrementalEdits({ + scenario: "javascriptProjectEmit", + subScenario: `modifies outfile js projects and concatenates them correctly`, + fs: () => projFs, + commandLineArgs: ["-b", "/src"], + incrementalScenarios: [{ buildKind: BuildKind.IncrementalDtsUnchanged, modifyFs: fs => replaceText(fs, "/src/sub-project/index.js", "null", "undefined") }] - }); }); - - describe("unittests:: tsbuild:: javascriptProjectEmit:: loads js-based projects with non-moved json files and emits them correctly", () => { - verifyTsc({ - scenario: "javascriptProjectEmit", - subScenario: `loads js-based projects with non-moved json files and emits them correctly`, - fs: () => loadProjectFromFiles({ - "/src/common/obj.json": Utils.dedent` +}); +describe("unittests:: tsbuild:: javascriptProjectEmit:: loads js-based projects with non-moved json files and emits them correctly", () => { + verifyTsc({ + scenario: "javascriptProjectEmit", + subScenario: `loads js-based projects with non-moved json files and emits them correctly`, + fs: () => loadProjectFromFiles({ + "/src/common/obj.json": dedent ` { "val": 42 }`, - "/src/common/index.ts": Utils.dedent` + "/src/common/index.ts": dedent ` import x = require("./obj.json"); export = x; `, - "/src/common/tsconfig.json": Utils.dedent` + "/src/common/tsconfig.json": dedent ` { "extends": "../tsconfig.base.json", "compilerOptions": { @@ -217,12 +217,12 @@ namespace ts { }, "include": ["index.ts", "obj.json"] }`, - "/src/sub-project/index.js": Utils.dedent` + "/src/sub-project/index.js": dedent ` import mod from '../common'; export const m = mod; `, - "/src/sub-project/tsconfig.json": Utils.dedent` + "/src/sub-project/tsconfig.json": dedent ` { "extends": "../tsconfig.base.json", "compilerOptions": { @@ -233,7 +233,7 @@ namespace ts { ], "include": ["./index.js"] }`, - "/src/sub-project-2/index.js": Utils.dedent` + "/src/sub-project-2/index.js": dedent ` import { m } from '../sub-project/index'; const variable = { @@ -244,7 +244,7 @@ namespace ts { return variable; } `, - "/src/sub-project-2/tsconfig.json": Utils.dedent` + "/src/sub-project-2/tsconfig.json": dedent ` { "extends": "../tsconfig.base.json", "compilerOptions": { @@ -255,7 +255,7 @@ namespace ts { ], "include": ["./index.js"] }`, - "/src/tsconfig.json": Utils.dedent` + "/src/tsconfig.json": dedent ` { "compilerOptions": { "composite": true @@ -266,7 +266,7 @@ namespace ts { ], "include": [] }`, - "/src/tsconfig.base.json": Utils.dedent` + "/src/tsconfig.base.json": dedent ` { "compilerOptions": { "skipLibCheck": true, @@ -279,8 +279,7 @@ namespace ts { "declaration": true } }`, - }, symbolLibContent), - commandLineArgs: ["-b", "/src"] - }); + }, symbolLibContent), + commandLineArgs: ["-b", "/src"] }); -} +}); diff --git a/src/testRunner/unittests/tsbuild/lateBoundSymbol.ts b/src/testRunner/unittests/tsbuild/lateBoundSymbol.ts index 15ac34c222c0c..77464592899b3 100644 --- a/src/testRunner/unittests/tsbuild/lateBoundSymbol.ts +++ b/src/testRunner/unittests/tsbuild/lateBoundSymbol.ts @@ -1,14 +1,13 @@ -namespace ts { - describe("unittests:: tsbuild:: lateBoundSymbol:: interface is merged and contains late bound member", () => { - verifyTscIncrementalEdits({ - subScenario: "interface is merged and contains late bound member", - fs: () => loadProjectFromDisk("tests/projects/lateBoundSymbol"), - scenario: "lateBoundSymbol", - commandLineArgs: ["--b", "/src/tsconfig.json", "--verbose"], - incrementalScenarios: [{ +import { verifyTscIncrementalEdits, loadProjectFromDisk, BuildKind, replaceText } from "../../ts"; +describe("unittests:: tsbuild:: lateBoundSymbol:: interface is merged and contains late bound member", () => { + verifyTscIncrementalEdits({ + subScenario: "interface is merged and contains late bound member", + fs: () => loadProjectFromDisk("tests/projects/lateBoundSymbol"), + scenario: "lateBoundSymbol", + commandLineArgs: ["--b", "/src/tsconfig.json", "--verbose"], + incrementalScenarios: [{ buildKind: BuildKind.IncrementalDtsUnchanged, modifyFs: fs => replaceText(fs, "/src/src/main.ts", "const x = 10;", ""), }] - }); }); -} +}); diff --git a/src/testRunner/unittests/tsbuild/moduleSpecifiers.ts b/src/testRunner/unittests/tsbuild/moduleSpecifiers.ts index 8494fd9498a36..eeab920c9b79c 100644 --- a/src/testRunner/unittests/tsbuild/moduleSpecifiers.ts +++ b/src/testRunner/unittests/tsbuild/moduleSpecifiers.ts @@ -1,16 +1,17 @@ -namespace ts { - // https://github.com/microsoft/TypeScript/issues/31696 - describe("unittests:: tsbuild:: moduleSpecifiers:: synthesized module specifiers to referenced projects resolve correctly", () => { - verifyTsc({ - scenario: "moduleSpecifiers", - subScenario: `synthesized module specifiers resolve correctly`, - fs: () => loadProjectFromFiles({ - "/src/solution/common/nominal.ts": Utils.dedent` +import { verifyTsc, loadProjectFromFiles, symbolLibContent } from "../../ts"; +import { dedent } from "../../Utils"; +// https://github.com/microsoft/TypeScript/issues/31696 +describe("unittests:: tsbuild:: moduleSpecifiers:: synthesized module specifiers to referenced projects resolve correctly", () => { + verifyTsc({ + scenario: "moduleSpecifiers", + subScenario: `synthesized module specifiers resolve correctly`, + fs: () => loadProjectFromFiles({ + "/src/solution/common/nominal.ts": dedent ` export declare type Nominal = T & { [Symbol.species]: Name; }; `, - "/src/solution/common/tsconfig.json": Utils.dedent` + "/src/solution/common/tsconfig.json": dedent ` { "extends": "../../tsconfig.base.json", "compilerOptions": { @@ -18,12 +19,12 @@ namespace ts { }, "include": ["nominal.ts"] }`, - "/src/solution/sub-project/index.ts": Utils.dedent` + "/src/solution/sub-project/index.ts": dedent ` import { Nominal } from '../common/nominal'; export type MyNominal = Nominal; `, - "/src/solution/sub-project/tsconfig.json": Utils.dedent` + "/src/solution/sub-project/tsconfig.json": dedent ` { "extends": "../../tsconfig.base.json", "compilerOptions": { @@ -34,7 +35,7 @@ namespace ts { ], "include": ["./index.ts"] }`, - "/src/solution/sub-project-2/index.ts": Utils.dedent` + "/src/solution/sub-project-2/index.ts": dedent ` import { MyNominal } from '../sub-project/index'; const variable = { @@ -45,7 +46,7 @@ namespace ts { return 'key'; } `, - "/src/solution/sub-project-2/tsconfig.json": Utils.dedent` + "/src/solution/sub-project-2/tsconfig.json": dedent ` { "extends": "../../tsconfig.base.json", "compilerOptions": { @@ -56,7 +57,7 @@ namespace ts { ], "include": ["./index.ts"] }`, - "/src/solution/tsconfig.json": Utils.dedent` + "/src/solution/tsconfig.json": dedent ` { "compilerOptions": { "composite": true @@ -67,7 +68,7 @@ namespace ts { ], "include": [] }`, - "/src/tsconfig.base.json": Utils.dedent` + "/src/tsconfig.base.json": dedent ` { "compilerOptions": { "skipLibCheck": true, @@ -75,7 +76,7 @@ namespace ts { "outDir": "lib", } }`, - "/src/tsconfig.json": Utils.dedent`{ + "/src/tsconfig.json": dedent `{ "compilerOptions": { "composite": true }, @@ -84,8 +85,7 @@ namespace ts { ], "include": [] }` - }, symbolLibContent), - commandLineArgs: ["-b", "/src", "--verbose"] - }); + }, symbolLibContent), + commandLineArgs: ["-b", "/src", "--verbose"] }); -} +}); diff --git a/src/testRunner/unittests/tsbuild/noEmitOnError.ts b/src/testRunner/unittests/tsbuild/noEmitOnError.ts index 41afbca2dd905..8ec14524c56f7 100644 --- a/src/testRunner/unittests/tsbuild/noEmitOnError.ts +++ b/src/testRunner/unittests/tsbuild/noEmitOnError.ts @@ -1,10 +1,9 @@ -namespace ts { - describe("unittests:: tsbuild - with noEmitOnError", () => { - verifyTsc({ - scenario: "noEmitOnError", - subScenario: "has empty files diagnostic when files is empty and no references are provided", - fs: () => loadProjectFromDisk("tests/projects/noEmitOnError"), - commandLineArgs: ["--b", "/src/tsconfig.json"], - }); +import { verifyTsc, loadProjectFromDisk } from "../../ts"; +describe("unittests:: tsbuild - with noEmitOnError", () => { + verifyTsc({ + scenario: "noEmitOnError", + subScenario: "has empty files diagnostic when files is empty and no references are provided", + fs: () => loadProjectFromDisk("tests/projects/noEmitOnError"), + commandLineArgs: ["--b", "/src/tsconfig.json"], }); -} +}); diff --git a/src/testRunner/unittests/tsbuild/outFile.ts b/src/testRunner/unittests/tsbuild/outFile.ts index a4594409042d8..ee0d57af99513 100644 --- a/src/testRunner/unittests/tsbuild/outFile.ts +++ b/src/testRunner/unittests/tsbuild/outFile.ts @@ -1,469 +1,427 @@ -namespace ts { - describe("unittests:: tsbuild:: outFile::", () => { - let outFileFs: vfs.FileSystem; - const enum ext { js, jsmap, dts, dtsmap, buildinfo } - const enum project { first, second, third } - type OutputFile = [string, string, string, string, string]; - function relName(path: string) { return path.slice(1); } - const outputFiles: [OutputFile, OutputFile, OutputFile] = [ - [ - "/src/first/bin/first-output.js", - "/src/first/bin/first-output.js.map", - "/src/first/bin/first-output.d.ts", - "/src/first/bin/first-output.d.ts.map", - "/src/first/bin/first-output.tsbuildinfo" - ], - [ - "/src/2/second-output.js", - "/src/2/second-output.js.map", - "/src/2/second-output.d.ts", - "/src/2/second-output.d.ts.map", - "/src/2/second-output.tsbuildinfo" - ], +import { FileSystem } from "../../vfs"; +import { ExpectedDiagnostic, SolutionBuilderHost, version } from "../../fakes"; +import { getExpectedDiagnosticForProjectsInBuild, Diagnostics, loadProjectFromDisk, BuildOptions, TscIncremental, BuildKind, replaceText, appendText, VerifyTsBuildInput, verifyTscIncrementalEdits, verifyTsc, noChangeRun, getFsWithTime, changeCompilerVersion, verifyOutputsAbsent, verifyOutputsPresent, ExitStatus, enableStrict, addTestPrologue, addShebang, addRest, removeRest, addStubFoo, changeStubToRest, addSpread, addTripleSlashRef, prependText } from "../../ts"; +import * as ts from "../../ts"; +describe("unittests:: tsbuild:: outFile::", () => { + let outFileFs: FileSystem; + const enum ext { + js, + jsmap, + dts, + dtsmap, + buildinfo + } + const enum project { + first, + second, + third + } + type OutputFile = [string, string, string, string, string]; + function relName(path: string) { return path.slice(1); } + const outputFiles: [OutputFile, OutputFile, OutputFile] = [ + [ + "/src/first/bin/first-output.js", + "/src/first/bin/first-output.js.map", + "/src/first/bin/first-output.d.ts", + "/src/first/bin/first-output.d.ts.map", + "/src/first/bin/first-output.tsbuildinfo" + ], + [ + "/src/2/second-output.js", + "/src/2/second-output.js.map", + "/src/2/second-output.d.ts", + "/src/2/second-output.d.ts.map", + "/src/2/second-output.tsbuildinfo" + ], + [ + "/src/third/thirdjs/output/third-output.js", + "/src/third/thirdjs/output/third-output.js.map", + "/src/third/thirdjs/output/third-output.d.ts", + "/src/third/thirdjs/output/third-output.d.ts.map", + "/src/third/thirdjs/output/third-output.tsbuildinfo" + ] + ]; + const relOutputFiles = outputFiles.map(v => v.map(relName)) as [OutputFile, OutputFile, OutputFile]; + type Sources = [string, readonly string[]]; + const enum source { + config, + ts + } + const enum part { + one, + two, + three + } + const sources: [Sources, Sources, Sources] = [ + [ + "/src/first/tsconfig.json", [ - "/src/third/thirdjs/output/third-output.js", - "/src/third/thirdjs/output/third-output.js.map", - "/src/third/thirdjs/output/third-output.d.ts", - "/src/third/thirdjs/output/third-output.d.ts.map", - "/src/third/thirdjs/output/third-output.tsbuildinfo" + "/src/first/first_PART1.ts", + "/src/first/first_part2.ts", + "/src/first/first_part3.ts" ] - ]; - const relOutputFiles = outputFiles.map(v => v.map(relName)) as [OutputFile, OutputFile, OutputFile]; - type Sources = [string, readonly string[]]; - const enum source { config, ts } - const enum part { one, two, three } - const sources: [Sources, Sources, Sources] = [ - [ - "/src/first/tsconfig.json", - [ - "/src/first/first_PART1.ts", - "/src/first/first_part2.ts", - "/src/first/first_part3.ts" - ] - ], + ], + [ + "/src/second/tsconfig.json", [ - "/src/second/tsconfig.json", - [ - "/src/second/second_part1.ts", - "/src/second/second_part2.ts" - ] - ], + "/src/second/second_part1.ts", + "/src/second/second_part2.ts" + ] + ], + [ + "/src/third/tsconfig.json", [ - "/src/third/tsconfig.json", - [ - "/src/third/third_part1.ts" - ] + "/src/third/third_part1.ts" ] - ]; - const relSources = sources.map(([config, sources]) => [relName(config), sources.map(relName)]) as any as [Sources, Sources, Sources]; - let initialExpectedDiagnostics: readonly fakes.ExpectedDiagnostic[] = [ - getExpectedDiagnosticForProjectsInBuild(relSources[project.first][source.config], relSources[project.second][source.config], relSources[project.third][source.config]), - [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, relSources[project.first][source.config], relOutputFiles[project.first][ext.js]], - [Diagnostics.Building_project_0, sources[project.first][source.config]], - [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, relSources[project.second][source.config], relOutputFiles[project.second][ext.js]], - [Diagnostics.Building_project_0, sources[project.second][source.config]], - [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, relSources[project.third][source.config], relOutputFiles[project.third][ext.js]], - [Diagnostics.Building_project_0, sources[project.third][source.config]] - ]; - before(() => { - outFileFs = loadProjectFromDisk("tests/projects/outfile-concat"); - }); - after(() => { - outFileFs = undefined!; - initialExpectedDiagnostics = undefined!; - }); - - function createSolutionBuilder(host: fakes.SolutionBuilderHost, baseOptions?: BuildOptions) { - return ts.createSolutionBuilder(host, ["/src/third"], { dry: false, force: false, verbose: true, ...(baseOptions || {}) }); - } - - interface VerifyOutFileScenarioInput { - subScenario: string; - modifyFs?: (fs: vfs.FileSystem) => void; - modifyAgainFs?: (fs: vfs.FileSystem) => void; - ignoreDtsChanged?: true; - ignoreDtsUnchanged?: true; - baselineOnly?: true; + ] + ]; + const relSources = sources.map(([config, sources]) => [relName(config), sources.map(relName)]) as any as [Sources, Sources, Sources]; + let initialExpectedDiagnostics: readonly ExpectedDiagnostic[] = [ + getExpectedDiagnosticForProjectsInBuild(relSources[project.first][source.config], relSources[project.second][source.config], relSources[project.third][source.config]), + [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, relSources[project.first][source.config], relOutputFiles[project.first][ext.js]], + [Diagnostics.Building_project_0, sources[project.first][source.config]], + [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, relSources[project.second][source.config], relOutputFiles[project.second][ext.js]], + [Diagnostics.Building_project_0, sources[project.second][source.config]], + [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, relSources[project.third][source.config], relOutputFiles[project.third][ext.js]], + [Diagnostics.Building_project_0, sources[project.third][source.config]] + ]; + before(() => { + outFileFs = loadProjectFromDisk("tests/projects/outfile-concat"); + }); + after(() => { + outFileFs = undefined!; + initialExpectedDiagnostics = undefined!; + }); + function createSolutionBuilder(host: SolutionBuilderHost, baseOptions?: BuildOptions) { + return ts.createSolutionBuilder(host, ["/src/third"], { dry: false, force: false, verbose: true, ...(baseOptions || {}) }); + } + interface VerifyOutFileScenarioInput { + subScenario: string; + modifyFs?: (fs: FileSystem) => void; + modifyAgainFs?: (fs: FileSystem) => void; + ignoreDtsChanged?: true; + ignoreDtsUnchanged?: true; + baselineOnly?: true; + } + function verifyOutFileScenario({ subScenario, modifyFs, modifyAgainFs, ignoreDtsChanged, ignoreDtsUnchanged, baselineOnly }: VerifyOutFileScenarioInput) { + const incrementalScenarios: TscIncremental[] = []; + if (!ignoreDtsChanged) { + incrementalScenarios.push({ + buildKind: BuildKind.IncrementalDtsChange, + modifyFs: fs => replaceText(fs, relSources[project.first][source.ts][part.one], "Hello", "Hola"), + }); } - - function verifyOutFileScenario({ - subScenario, - modifyFs, - modifyAgainFs, - ignoreDtsChanged, - ignoreDtsUnchanged, - baselineOnly - }: VerifyOutFileScenarioInput) { - const incrementalScenarios: TscIncremental[] = []; - if (!ignoreDtsChanged) { - incrementalScenarios.push({ - buildKind: BuildKind.IncrementalDtsChange, - modifyFs: fs => replaceText(fs, relSources[project.first][source.ts][part.one], "Hello", "Hola"), - }); - } - if (!ignoreDtsUnchanged) { - incrementalScenarios.push({ - buildKind: BuildKind.IncrementalDtsUnchanged, - modifyFs: fs => appendText(fs, relSources[project.first][source.ts][part.one], "console.log(s);"), - }); - } - if (modifyAgainFs) { - incrementalScenarios.push({ - buildKind: BuildKind.IncrementalHeadersChange, - modifyFs: modifyAgainFs - }); - } - const input: VerifyTsBuildInput = { - subScenario, - fs: () => outFileFs, - scenario: "outfile-concat", - commandLineArgs: ["--b", "/src/third", "--verbose"], - baselineSourceMap: true, - modifyFs, - baselineReadFileCalls: !baselineOnly, - incrementalScenarios, - }; - return incrementalScenarios.length ? - verifyTscIncrementalEdits(input) : - verifyTsc(input); + if (!ignoreDtsUnchanged) { + incrementalScenarios.push({ + buildKind: BuildKind.IncrementalDtsUnchanged, + modifyFs: fs => appendText(fs, relSources[project.first][source.ts][part.one], "console.log(s);"), + }); } - - // Verify initial + incremental edits - verifyOutFileScenario({ - subScenario: "baseline sectioned sourcemaps", - }); - - // Verify baseline with build info + dts unChanged - verifyOutFileScenario({ - subScenario: "when final project is not composite but uses project references", - modifyFs: fs => replaceText(fs, sources[project.third][source.config], `"composite": true,`, ""), - ignoreDtsChanged: true, - baselineOnly: true - }); - - // Verify baseline with build info - verifyOutFileScenario({ - subScenario: "when final project is not composite but incremental", - modifyFs: fs => replaceText(fs, sources[project.third][source.config], `"composite": true,`, `"incremental": true,`), - ignoreDtsChanged: true, - ignoreDtsUnchanged: true, - baselineOnly: true - }); - - // Verify baseline with build info - verifyOutFileScenario({ - subScenario: "when final project specifies tsBuildInfoFile", - modifyFs: fs => replaceText(fs, sources[project.third][source.config], `"composite": true,`, `"composite": true, - "tsBuildInfoFile": "./thirdjs/output/third.tsbuildinfo",`), - ignoreDtsChanged: true, - ignoreDtsUnchanged: true, - baselineOnly: true - }); - - function getOutFileFsAfterBuild() { - const fs = outFileFs.shadow(); - const host = fakes.SolutionBuilderHost.create(fs); - const builder = createSolutionBuilder(host); - builder.build(); - fs.makeReadonly(); - return fs; + if (modifyAgainFs) { + incrementalScenarios.push({ + buildKind: BuildKind.IncrementalHeadersChange, + modifyFs: modifyAgainFs + }); } - - verifyTscIncrementalEdits({ - scenario: "outFile", - subScenario: "clean projects", - fs: getOutFileFsAfterBuild, - commandLineArgs: ["--b", "/src/third", "--clean"], - incrementalScenarios: [noChangeRun] - }); - - verifyTsc({ - scenario: "outFile", - subScenario: "verify buildInfo absence results in new build", - fs: getOutFileFsAfterBuild, - commandLineArgs: ["--b", "/src/third", "--verbose"], - modifyFs: fs => fs.unlinkSync(outputFiles[project.first][ext.buildinfo]), - }); - - verifyTsc({ - scenario: "outFile", - subScenario: "tsbuildinfo is not generated when incremental is set to false", + const input: VerifyTsBuildInput = { + subScenario, fs: () => outFileFs, + scenario: "outfile-concat", commandLineArgs: ["--b", "/src/third", "--verbose"], - modifyFs: fs => replaceText(fs, sources[project.third][source.config], `"composite": true,`, ""), - }); - - it("rebuilds completely when version in tsbuildinfo doesnt match ts version", () => { - const { fs, tick } = getFsWithTime(outFileFs); - const host = fakes.SolutionBuilderHost.create(fs); - let builder = createSolutionBuilder(host); - builder.build(); - host.assertDiagnosticMessages(...initialExpectedDiagnostics); - host.clearDiagnostics(); - tick(); - builder = createSolutionBuilder(host); - changeCompilerVersion(host); - tick(); - builder.build(); - host.assertDiagnosticMessages( - getExpectedDiagnosticForProjectsInBuild(relSources[project.first][source.config], relSources[project.second][source.config], relSources[project.third][source.config]), - [Diagnostics.Project_0_is_out_of_date_because_output_for_it_was_generated_with_version_1_that_differs_with_current_version_2, relSources[project.first][source.config], fakes.version, version], - [Diagnostics.Building_project_0, sources[project.first][source.config]], - [Diagnostics.Project_0_is_out_of_date_because_output_for_it_was_generated_with_version_1_that_differs_with_current_version_2, relSources[project.second][source.config], fakes.version, version], - [Diagnostics.Building_project_0, sources[project.second][source.config]], - [Diagnostics.Project_0_is_out_of_date_because_output_for_it_was_generated_with_version_1_that_differs_with_current_version_2, relSources[project.third][source.config], fakes.version, version], - [Diagnostics.Building_project_0, sources[project.third][source.config]], - ); - }); - - it("rebuilds completely when command line incremental flag changes between non dts changes", () => { - const { fs, tick } = getFsWithTime(outFileFs); - // Make non composite third project - replaceText(fs, sources[project.third][source.config], `"composite": true,`, ""); - - // Build with command line incremental - const host = fakes.SolutionBuilderHost.create(fs); - let builder = createSolutionBuilder(host, { incremental: true }); - builder.build(); - host.assertDiagnosticMessages(...initialExpectedDiagnostics); - host.clearDiagnostics(); - tick(); - - // Make non incremental build with change in file that doesnt affect dts - appendText(fs, relSources[project.first][source.ts][part.one], "console.log(s);"); - builder = createSolutionBuilder(host, { verbose: true }); - builder.build(); - host.assertDiagnosticMessages(getExpectedDiagnosticForProjectsInBuild(relSources[project.first][source.config], relSources[project.second][source.config], relSources[project.third][source.config]), - [Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, relSources[project.first][source.config], relOutputFiles[project.first][ext.js], relSources[project.first][source.ts][part.one]], - [Diagnostics.Building_project_0, sources[project.first][source.config]], - [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, relSources[project.second][source.config], relSources[project.second][source.ts][part.one], relOutputFiles[project.second][ext.js]], - [Diagnostics.Project_0_is_out_of_date_because_output_of_its_dependency_1_has_changed, relSources[project.third][source.config], "src/first"], - [Diagnostics.Building_project_0, sources[project.third][source.config]] - ); - host.clearDiagnostics(); - tick(); - - // Make incremental build with change in file that doesnt affect dts - appendText(fs, relSources[project.first][source.ts][part.one], "console.log(s);"); - builder = createSolutionBuilder(host, { verbose: true, incremental: true }); - builder.build(); - // Builds completely because tsbuildinfo is old. - host.assertDiagnosticMessages( - getExpectedDiagnosticForProjectsInBuild(relSources[project.first][source.config], relSources[project.second][source.config], relSources[project.third][source.config]), - [Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, relSources[project.first][source.config], relOutputFiles[project.first][ext.js], relSources[project.first][source.ts][part.one]], - [Diagnostics.Building_project_0, sources[project.first][source.config]], - [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, relSources[project.second][source.config], relSources[project.second][source.ts][part.one], relOutputFiles[project.second][ext.js]], - [Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, relSources[project.third][source.config], relOutputFiles[project.third][ext.buildinfo], "src/first"], - [Diagnostics.Building_project_0, sources[project.third][source.config]] - ); - host.clearDiagnostics(); - }); - - it("builds till project specified", () => { - const fs = outFileFs.shadow(); - const host = fakes.SolutionBuilderHost.create(fs); - const builder = createSolutionBuilder(host, { verbose: false }); - const result = builder.build(sources[project.second][source.config]); - host.assertDiagnosticMessages(/*empty*/); - // First and Third is not built - verifyOutputsAbsent(fs, [...outputFiles[project.first], ...outputFiles[project.third]]); - // second is built - verifyOutputsPresent(fs, outputFiles[project.second]); - assert.equal(result, ExitStatus.Success); + baselineSourceMap: true, + modifyFs, + baselineReadFileCalls: !baselineOnly, + incrementalScenarios, + }; + return incrementalScenarios.length ? + verifyTscIncrementalEdits(input) : + verifyTsc(input); + } + // Verify initial + incremental edits + verifyOutFileScenario({ + subScenario: "baseline sectioned sourcemaps", + }); + // Verify baseline with build info + dts unChanged + verifyOutFileScenario({ + subScenario: "when final project is not composite but uses project references", + modifyFs: fs => replaceText(fs, sources[project.third][source.config], `"composite": true,`, ""), + ignoreDtsChanged: true, + baselineOnly: true + }); + // Verify baseline with build info + verifyOutFileScenario({ + subScenario: "when final project is not composite but incremental", + modifyFs: fs => replaceText(fs, sources[project.third][source.config], `"composite": true,`, `"incremental": true,`), + ignoreDtsChanged: true, + ignoreDtsUnchanged: true, + baselineOnly: true + }); + // Verify baseline with build info + verifyOutFileScenario({ + subScenario: "when final project specifies tsBuildInfoFile", + modifyFs: fs => replaceText(fs, sources[project.third][source.config], `"composite": true,`, `"composite": true, + "tsBuildInfoFile": "./thirdjs/output/third.tsbuildinfo",`), + ignoreDtsChanged: true, + ignoreDtsUnchanged: true, + baselineOnly: true + }); + function getOutFileFsAfterBuild() { + const fs = outFileFs.shadow(); + const host = SolutionBuilderHost.create(fs); + const builder = createSolutionBuilder(host); + builder.build(); + fs.makeReadonly(); + return fs; + } + verifyTscIncrementalEdits({ + scenario: "outFile", + subScenario: "clean projects", + fs: getOutFileFsAfterBuild, + commandLineArgs: ["--b", "/src/third", "--clean"], + incrementalScenarios: [noChangeRun] + }); + verifyTsc({ + scenario: "outFile", + subScenario: "verify buildInfo absence results in new build", + fs: getOutFileFsAfterBuild, + commandLineArgs: ["--b", "/src/third", "--verbose"], + modifyFs: fs => fs.unlinkSync(outputFiles[project.first][ext.buildinfo]), + }); + verifyTsc({ + scenario: "outFile", + subScenario: "tsbuildinfo is not generated when incremental is set to false", + fs: () => outFileFs, + commandLineArgs: ["--b", "/src/third", "--verbose"], + modifyFs: fs => replaceText(fs, sources[project.third][source.config], `"composite": true,`, ""), + }); + it("rebuilds completely when version in tsbuildinfo doesnt match ts version", () => { + const { fs, tick } = getFsWithTime(outFileFs); + const host = SolutionBuilderHost.create(fs); + let builder = createSolutionBuilder(host); + builder.build(); + host.assertDiagnosticMessages(...initialExpectedDiagnostics); + host.clearDiagnostics(); + tick(); + builder = createSolutionBuilder(host); + changeCompilerVersion(host); + tick(); + builder.build(); + host.assertDiagnosticMessages(getExpectedDiagnosticForProjectsInBuild(relSources[project.first][source.config], relSources[project.second][source.config], relSources[project.third][source.config]), [Diagnostics.Project_0_is_out_of_date_because_output_for_it_was_generated_with_version_1_that_differs_with_current_version_2, relSources[project.first][source.config], version, ts.version], [Diagnostics.Building_project_0, sources[project.first][source.config]], [Diagnostics.Project_0_is_out_of_date_because_output_for_it_was_generated_with_version_1_that_differs_with_current_version_2, relSources[project.second][source.config], version, ts.version], [Diagnostics.Building_project_0, sources[project.second][source.config]], [Diagnostics.Project_0_is_out_of_date_because_output_for_it_was_generated_with_version_1_that_differs_with_current_version_2, relSources[project.third][source.config], version, ts.version], [Diagnostics.Building_project_0, sources[project.third][source.config]]); + }); + it("rebuilds completely when command line incremental flag changes between non dts changes", () => { + const { fs, tick } = getFsWithTime(outFileFs); + // Make non composite third project + replaceText(fs, sources[project.third][source.config], `"composite": true,`, ""); + // Build with command line incremental + const host = SolutionBuilderHost.create(fs); + let builder = createSolutionBuilder(host, { incremental: true }); + builder.build(); + host.assertDiagnosticMessages(...initialExpectedDiagnostics); + host.clearDiagnostics(); + tick(); + // Make non incremental build with change in file that doesnt affect dts + appendText(fs, relSources[project.first][source.ts][part.one], "console.log(s);"); + builder = createSolutionBuilder(host, { verbose: true }); + builder.build(); + host.assertDiagnosticMessages(getExpectedDiagnosticForProjectsInBuild(relSources[project.first][source.config], relSources[project.second][source.config], relSources[project.third][source.config]), [Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, relSources[project.first][source.config], relOutputFiles[project.first][ext.js], relSources[project.first][source.ts][part.one]], [Diagnostics.Building_project_0, sources[project.first][source.config]], [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, relSources[project.second][source.config], relSources[project.second][source.ts][part.one], relOutputFiles[project.second][ext.js]], [Diagnostics.Project_0_is_out_of_date_because_output_of_its_dependency_1_has_changed, relSources[project.third][source.config], "src/first"], [Diagnostics.Building_project_0, sources[project.third][source.config]]); + host.clearDiagnostics(); + tick(); + // Make incremental build with change in file that doesnt affect dts + appendText(fs, relSources[project.first][source.ts][part.one], "console.log(s);"); + builder = createSolutionBuilder(host, { verbose: true, incremental: true }); + builder.build(); + // Builds completely because tsbuildinfo is old. + host.assertDiagnosticMessages(getExpectedDiagnosticForProjectsInBuild(relSources[project.first][source.config], relSources[project.second][source.config], relSources[project.third][source.config]), [Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, relSources[project.first][source.config], relOutputFiles[project.first][ext.js], relSources[project.first][source.ts][part.one]], [Diagnostics.Building_project_0, sources[project.first][source.config]], [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, relSources[project.second][source.config], relSources[project.second][source.ts][part.one], relOutputFiles[project.second][ext.js]], [Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, relSources[project.third][source.config], relOutputFiles[project.third][ext.buildinfo], "src/first"], [Diagnostics.Building_project_0, sources[project.third][source.config]]); + host.clearDiagnostics(); + }); + it("builds till project specified", () => { + const fs = outFileFs.shadow(); + const host = SolutionBuilderHost.create(fs); + const builder = createSolutionBuilder(host, { verbose: false }); + const result = builder.build(sources[project.second][source.config]); + host.assertDiagnosticMessages( /*empty*/); + // First and Third is not built + verifyOutputsAbsent(fs, [...outputFiles[project.first], ...outputFiles[project.third]]); + // second is built + verifyOutputsPresent(fs, outputFiles[project.second]); + assert.equal(result, ExitStatus.Success); + }); + it("cleans till project specified", () => { + const fs = outFileFs.shadow(); + const host = SolutionBuilderHost.create(fs); + const builder = createSolutionBuilder(host, { verbose: false }); + builder.build(); + const result = builder.clean(sources[project.second][source.config]); + host.assertDiagnosticMessages( /*empty*/); + // First and Third output for present + verifyOutputsPresent(fs, [...outputFiles[project.first], ...outputFiles[project.third]]); + // second is cleaned + verifyOutputsAbsent(fs, outputFiles[project.second]); + assert.equal(result, ExitStatus.Success); + }); + describe("Prepend output with .tsbuildinfo", () => { + // Prologues + describe("Prologues", () => { + // Verify initial + incremental edits + verifyOutFileScenario({ + subScenario: "strict in all projects", + modifyFs: fs => { + enableStrict(fs, sources[project.first][source.config]); + enableStrict(fs, sources[project.second][source.config]); + enableStrict(fs, sources[project.third][source.config]); + }, + modifyAgainFs: fs => addTestPrologue(fs, relSources[project.first][source.ts][part.one], `"myPrologue"`) + }); + // Verify ignore dtsChanged + verifyOutFileScenario({ + subScenario: "strict in one dependency", + modifyFs: fs => enableStrict(fs, sources[project.second][source.config]), + modifyAgainFs: fs => addTestPrologue(fs, "src/first/first_PART1.ts", `"myPrologue"`), + ignoreDtsChanged: true, + baselineOnly: true + }); + // Verify initial + incremental edits - sourcemap verification + verifyOutFileScenario({ + subScenario: "multiple prologues in all projects", + modifyFs: fs => { + enableStrict(fs, sources[project.first][source.config]); + addTestPrologue(fs, sources[project.first][source.ts][part.one], `"myPrologue"`); + enableStrict(fs, sources[project.second][source.config]); + addTestPrologue(fs, sources[project.second][source.ts][part.one], `"myPrologue"`); + addTestPrologue(fs, sources[project.second][source.ts][part.two], `"myPrologue2";`); + enableStrict(fs, sources[project.third][source.config]); + addTestPrologue(fs, sources[project.third][source.ts][part.one], `"myPrologue";`); + addTestPrologue(fs, sources[project.third][source.ts][part.one], `"myPrologue3";`); + }, + modifyAgainFs: fs => addTestPrologue(fs, relSources[project.first][source.ts][part.one], `"myPrologue5"`) + }); + // Verify ignore dtsChanged + verifyOutFileScenario({ + subScenario: "multiple prologues in different projects", + modifyFs: fs => { + enableStrict(fs, sources[project.first][source.config]); + addTestPrologue(fs, sources[project.second][source.ts][part.one], `"myPrologue"`); + addTestPrologue(fs, sources[project.second][source.ts][part.two], `"myPrologue2";`); + enableStrict(fs, sources[project.third][source.config]); + }, + modifyAgainFs: fs => addTestPrologue(fs, sources[project.first][source.ts][part.one], `"myPrologue5"`), + ignoreDtsChanged: true, + baselineOnly: true + }); }); - - it("cleans till project specified", () => { - const fs = outFileFs.shadow(); - const host = fakes.SolutionBuilderHost.create(fs); - const builder = createSolutionBuilder(host, { verbose: false }); - builder.build(); - const result = builder.clean(sources[project.second][source.config]); - host.assertDiagnosticMessages(/*empty*/); - // First and Third output for present - verifyOutputsPresent(fs, [...outputFiles[project.first], ...outputFiles[project.third]]); - // second is cleaned - verifyOutputsAbsent(fs, outputFiles[project.second]); - assert.equal(result, ExitStatus.Success); + // Shebang + describe("Shebang", () => { + // changes declaration because its emitted in .d.ts file + // Verify initial + incremental edits + verifyOutFileScenario({ + subScenario: "shebang in all projects", + modifyFs: fs => { + addShebang(fs, "first", "first_PART1"); + addShebang(fs, "first", "first_part2"); + addShebang(fs, "second", "second_part1"); + addShebang(fs, "third", "third_part1"); + }, + }); + // Verify ignore dtsChanged + verifyOutFileScenario({ + subScenario: "shebang in only one dependency project", + modifyFs: fs => addShebang(fs, "second", "second_part1"), + ignoreDtsChanged: true, + baselineOnly: true + }); }); - - describe("Prepend output with .tsbuildinfo", () => { - // Prologues - describe("Prologues", () => { - // Verify initial + incremental edits - verifyOutFileScenario({ - subScenario: "strict in all projects", - modifyFs: fs => { - enableStrict(fs, sources[project.first][source.config]); - enableStrict(fs, sources[project.second][source.config]); - enableStrict(fs, sources[project.third][source.config]); - }, - modifyAgainFs: fs => addTestPrologue(fs, relSources[project.first][source.ts][part.one], `"myPrologue"`) - }); - - // Verify ignore dtsChanged - verifyOutFileScenario({ - subScenario: "strict in one dependency", - modifyFs: fs => enableStrict(fs, sources[project.second][source.config]), - modifyAgainFs: fs => addTestPrologue(fs, "src/first/first_PART1.ts", `"myPrologue"`), - ignoreDtsChanged: true, - baselineOnly: true - }); - - // Verify initial + incremental edits - sourcemap verification - verifyOutFileScenario({ - subScenario: "multiple prologues in all projects", - modifyFs: fs => { - enableStrict(fs, sources[project.first][source.config]); - addTestPrologue(fs, sources[project.first][source.ts][part.one], `"myPrologue"`); - enableStrict(fs, sources[project.second][source.config]); - addTestPrologue(fs, sources[project.second][source.ts][part.one], `"myPrologue"`); - addTestPrologue(fs, sources[project.second][source.ts][part.two], `"myPrologue2";`); - enableStrict(fs, sources[project.third][source.config]); - addTestPrologue(fs, sources[project.third][source.ts][part.one], `"myPrologue";`); - addTestPrologue(fs, sources[project.third][source.ts][part.one], `"myPrologue3";`); - }, - modifyAgainFs: fs => addTestPrologue(fs, relSources[project.first][source.ts][part.one], `"myPrologue5"`) - }); - - // Verify ignore dtsChanged - verifyOutFileScenario({ - subScenario: "multiple prologues in different projects", - modifyFs: fs => { - enableStrict(fs, sources[project.first][source.config]); - addTestPrologue(fs, sources[project.second][source.ts][part.one], `"myPrologue"`); - addTestPrologue(fs, sources[project.second][source.ts][part.two], `"myPrologue2";`); - enableStrict(fs, sources[project.third][source.config]); - }, - modifyAgainFs: fs => addTestPrologue(fs, sources[project.first][source.ts][part.one], `"myPrologue5"`), - ignoreDtsChanged: true, - baselineOnly: true - }); + // emitHelpers + describe("emitHelpers", () => { + // Verify initial + incremental edits + verifyOutFileScenario({ + subScenario: "emitHelpers in all projects", + modifyFs: fs => { + addRest(fs, "first", "first_PART1"); + addRest(fs, "second", "second_part1"); + addRest(fs, "third", "third_part1"); + }, + modifyAgainFs: fs => removeRest(fs, "first", "first_PART1") }); - - // Shebang - describe("Shebang", () => { - // changes declaration because its emitted in .d.ts file - // Verify initial + incremental edits - verifyOutFileScenario({ - subScenario: "shebang in all projects", - modifyFs: fs => { - addShebang(fs, "first", "first_PART1"); - addShebang(fs, "first", "first_part2"); - addShebang(fs, "second", "second_part1"); - addShebang(fs, "third", "third_part1"); - }, - }); - - // Verify ignore dtsChanged - verifyOutFileScenario({ - subScenario: "shebang in only one dependency project", - modifyFs: fs => addShebang(fs, "second", "second_part1"), - ignoreDtsChanged: true, - baselineOnly: true - }); + // Verify ignore dtsChanged + verifyOutFileScenario({ + subScenario: "emitHelpers in only one dependency project", + modifyFs: fs => { + addStubFoo(fs, "first", "first_PART1"); + addRest(fs, "second", "second_part1"); + }, + modifyAgainFs: fs => changeStubToRest(fs, "first", "first_PART1"), + ignoreDtsChanged: true, + baselineOnly: true }); - - // emitHelpers - describe("emitHelpers", () => { - // Verify initial + incremental edits - verifyOutFileScenario({ - subScenario: "emitHelpers in all projects", - modifyFs: fs => { - addRest(fs, "first", "first_PART1"); - addRest(fs, "second", "second_part1"); - addRest(fs, "third", "third_part1"); - }, - modifyAgainFs: fs => removeRest(fs, "first", "first_PART1") - }); - - // Verify ignore dtsChanged - verifyOutFileScenario({ - subScenario: "emitHelpers in only one dependency project", - modifyFs: fs => { - addStubFoo(fs, "first", "first_PART1"); - addRest(fs, "second", "second_part1"); - }, - modifyAgainFs: fs => changeStubToRest(fs, "first", "first_PART1"), - ignoreDtsChanged: true, - baselineOnly: true - }); - - // Verify ignore dtsChanged - verifyOutFileScenario({ - subScenario: "multiple emitHelpers in all projects", - modifyFs: fs => { - addRest(fs, "first", "first_PART1"); - addSpread(fs, "first", "first_part3"); - addRest(fs, "second", "second_part1"); - addSpread(fs, "second", "second_part2"); - addRest(fs, "third", "third_part1"); - addSpread(fs, "third", "third_part1"); - }, - modifyAgainFs: fs => removeRest(fs, "first", "first_PART1"), - ignoreDtsChanged: true, - baselineOnly: true - }); - - // Verify ignore dtsChanged - verifyOutFileScenario({ - subScenario: "multiple emitHelpers in different projects", - modifyFs: fs => { - addRest(fs, "first", "first_PART1"); - addSpread(fs, "second", "second_part1"); - addRest(fs, "third", "third_part1"); - }, - modifyAgainFs: fs => removeRest(fs, "first", "first_PART1"), - ignoreDtsChanged: true, - baselineOnly: true - }); + // Verify ignore dtsChanged + verifyOutFileScenario({ + subScenario: "multiple emitHelpers in all projects", + modifyFs: fs => { + addRest(fs, "first", "first_PART1"); + addSpread(fs, "first", "first_part3"); + addRest(fs, "second", "second_part1"); + addSpread(fs, "second", "second_part2"); + addRest(fs, "third", "third_part1"); + addSpread(fs, "third", "third_part1"); + }, + modifyAgainFs: fs => removeRest(fs, "first", "first_PART1"), + ignoreDtsChanged: true, + baselineOnly: true }); - - // triple slash refs - describe("triple slash refs", () => { - // changes declaration because its emitted in .d.ts file - // Verify initial + incremental edits - verifyOutFileScenario({ - subScenario: "triple slash refs in all projects", - modifyFs: fs => { - addTripleSlashRef(fs, "first", "first_part2"); - addTripleSlashRef(fs, "second", "second_part1"); - addTripleSlashRef(fs, "third", "third_part1"); - } - }); - - // Verify ignore dtsChanged - verifyOutFileScenario({ - subScenario: "triple slash refs in one project", - modifyFs: fs => addTripleSlashRef(fs, "second", "second_part1"), - ignoreDtsChanged: true, - baselineOnly: true - }); + // Verify ignore dtsChanged + verifyOutFileScenario({ + subScenario: "multiple emitHelpers in different projects", + modifyFs: fs => { + addRest(fs, "first", "first_PART1"); + addSpread(fs, "second", "second_part1"); + addRest(fs, "third", "third_part1"); + }, + modifyAgainFs: fs => removeRest(fs, "first", "first_PART1"), + ignoreDtsChanged: true, + baselineOnly: true }); - - describe("stripInternal", () => { - function disableRemoveComments(fs: vfs.FileSystem, file: string) { - replaceText(fs, file, `"removeComments": true`, `"removeComments": false`); - } - - function diableRemoveCommentsInAll(fs: vfs.FileSystem) { - disableRemoveComments(fs, sources[project.first][source.config]); - disableRemoveComments(fs, sources[project.second][source.config]); - disableRemoveComments(fs, sources[project.third][source.config]); + }); + // triple slash refs + describe("triple slash refs", () => { + // changes declaration because its emitted in .d.ts file + // Verify initial + incremental edits + verifyOutFileScenario({ + subScenario: "triple slash refs in all projects", + modifyFs: fs => { + addTripleSlashRef(fs, "first", "first_part2"); + addTripleSlashRef(fs, "second", "second_part1"); + addTripleSlashRef(fs, "third", "third_part1"); } - - function stripInternalOfThird(fs: vfs.FileSystem) { - replaceText(fs, sources[project.third][source.config], `"declaration": true,`, `"declaration": true, + }); + // Verify ignore dtsChanged + verifyOutFileScenario({ + subScenario: "triple slash refs in one project", + modifyFs: fs => addTripleSlashRef(fs, "second", "second_part1"), + ignoreDtsChanged: true, + baselineOnly: true + }); + }); + describe("stripInternal", () => { + function disableRemoveComments(fs: FileSystem, file: string) { + replaceText(fs, file, `"removeComments": true`, `"removeComments": false`); + } + function diableRemoveCommentsInAll(fs: FileSystem) { + disableRemoveComments(fs, sources[project.first][source.config]); + disableRemoveComments(fs, sources[project.second][source.config]); + disableRemoveComments(fs, sources[project.third][source.config]); + } + function stripInternalOfThird(fs: FileSystem) { + replaceText(fs, sources[project.third][source.config], `"declaration": true,`, `"declaration": true, "stripInternal": true,`); + } + function stripInternalScenario(fs: FileSystem, removeCommentsDisabled?: boolean, jsDocStyle?: boolean) { + const internal = jsDocStyle ? `/**@internal*/` : `/*@internal*/`; + if (removeCommentsDisabled) { + diableRemoveCommentsInAll(fs); } - - function stripInternalScenario(fs: vfs.FileSystem, removeCommentsDisabled?: boolean, jsDocStyle?: boolean) { - const internal = jsDocStyle ? `/**@internal*/` : `/*@internal*/`; - if (removeCommentsDisabled) { - diableRemoveCommentsInAll(fs); - } - stripInternalOfThird(fs); - replaceText(fs, sources[project.first][source.ts][part.one], "interface", `${internal} interface`); - appendText(fs, sources[project.second][source.ts][part.one], ` + stripInternalOfThird(fs); + replaceText(fs, sources[project.first][source.ts][part.one], "interface", `${internal} interface`); + appendText(fs, sources[project.second][source.ts][part.one], ` class normalC { ${internal} constructor() { } ${internal} prop: string; @@ -489,93 +447,82 @@ ${internal} import internalImport = internalNamespace.someClass; ${internal} type internalType = internalC; ${internal} const internalConst = 10; ${internal} enum internalEnum { a, b, c }`); + } + // Verify initial + incremental edits + verifyOutFileScenario({ + subScenario: "stripInternal", + modifyFs: stripInternalScenario, + modifyAgainFs: fs => replaceText(fs, sources[project.first][source.ts][part.one], `/*@internal*/ interface`, "interface"), + }); + // Verify ignore dtsChanged + verifyOutFileScenario({ + subScenario: "stripInternal with comments emit enabled", + modifyFs: fs => stripInternalScenario(fs, /*removeCommentsDisabled*/ true), + modifyAgainFs: fs => replaceText(fs, sources[project.first][source.ts][part.one], `/*@internal*/ interface`, "interface"), + ignoreDtsChanged: true, + baselineOnly: true + }); + // Verify ignore dtsChanged + verifyOutFileScenario({ + subScenario: "stripInternal jsdoc style comment", + modifyFs: fs => stripInternalScenario(fs, /*removeCommentsDisabled*/ false, /*jsDocStyle*/ true), + modifyAgainFs: fs => replaceText(fs, sources[project.first][source.ts][part.one], `/**@internal*/ interface`, "interface"), + ignoreDtsChanged: true, + baselineOnly: true + }); + // Verify ignore dtsChanged + verifyOutFileScenario({ + subScenario: "stripInternal jsdoc style with comments emit enabled", + modifyFs: fs => stripInternalScenario(fs, /*removeCommentsDisabled*/ true, /*jsDocStyle*/ true), + ignoreDtsChanged: true, + baselineOnly: true + }); + describe("with three levels of project dependency", () => { + function makeOneTwoThreeDependOrder(fs: FileSystem) { + replaceText(fs, sources[project.second][source.config], "[", `[ + { "path": "../first", "prepend": true }`); + replaceText(fs, sources[project.third][source.config], `{ "path": "../first", "prepend": true },`, ""); + } + function stripInternalWithDependentOrder(fs: FileSystem, removeCommentsDisabled?: boolean, jsDocStyle?: boolean) { + stripInternalScenario(fs, removeCommentsDisabled, jsDocStyle); + makeOneTwoThreeDependOrder(fs); } - // Verify initial + incremental edits verifyOutFileScenario({ - subScenario: "stripInternal", - modifyFs: stripInternalScenario, + subScenario: "stripInternal when one-two-three are prepended in order", + modifyFs: stripInternalWithDependentOrder, modifyAgainFs: fs => replaceText(fs, sources[project.first][source.ts][part.one], `/*@internal*/ interface`, "interface"), }); - // Verify ignore dtsChanged verifyOutFileScenario({ - subScenario: "stripInternal with comments emit enabled", - modifyFs: fs => stripInternalScenario(fs, /*removeCommentsDisabled*/ true), + subScenario: "stripInternal with comments emit enabled when one-two-three are prepended in order", + modifyFs: fs => stripInternalWithDependentOrder(fs, /*removeCommentsDisabled*/ true), modifyAgainFs: fs => replaceText(fs, sources[project.first][source.ts][part.one], `/*@internal*/ interface`, "interface"), ignoreDtsChanged: true, baselineOnly: true }); - // Verify ignore dtsChanged verifyOutFileScenario({ - subScenario: "stripInternal jsdoc style comment", - modifyFs: fs => stripInternalScenario(fs, /*removeCommentsDisabled*/ false, /*jsDocStyle*/ true), + subScenario: "stripInternal jsdoc style comment when one-two-three are prepended in order", + modifyFs: fs => stripInternalWithDependentOrder(fs, /*removeCommentsDisabled*/ false, /*jsDocStyle*/ true), modifyAgainFs: fs => replaceText(fs, sources[project.first][source.ts][part.one], `/**@internal*/ interface`, "interface"), ignoreDtsChanged: true, baselineOnly: true }); - // Verify ignore dtsChanged verifyOutFileScenario({ - subScenario: "stripInternal jsdoc style with comments emit enabled", - modifyFs: fs => stripInternalScenario(fs, /*removeCommentsDisabled*/ true, /*jsDocStyle*/ true), + subScenario: "stripInternal jsdoc style with comments emit enabled when one-two-three are prepended in order", + modifyFs: fs => stripInternalWithDependentOrder(fs, /*removeCommentsDisabled*/ true, /*jsDocStyle*/ true), ignoreDtsChanged: true, baselineOnly: true }); - - describe("with three levels of project dependency", () => { - function makeOneTwoThreeDependOrder(fs: vfs.FileSystem) { - replaceText(fs, sources[project.second][source.config], "[", `[ - { "path": "../first", "prepend": true }`); - replaceText(fs, sources[project.third][source.config], `{ "path": "../first", "prepend": true },`, ""); - } - - function stripInternalWithDependentOrder(fs: vfs.FileSystem, removeCommentsDisabled?: boolean, jsDocStyle?: boolean) { - stripInternalScenario(fs, removeCommentsDisabled, jsDocStyle); - makeOneTwoThreeDependOrder(fs); - } - - // Verify initial + incremental edits - verifyOutFileScenario({ - subScenario: "stripInternal when one-two-three are prepended in order", - modifyFs: stripInternalWithDependentOrder, - modifyAgainFs: fs => replaceText(fs, sources[project.first][source.ts][part.one], `/*@internal*/ interface`, "interface"), - }); - - // Verify ignore dtsChanged - verifyOutFileScenario({ - subScenario: "stripInternal with comments emit enabled when one-two-three are prepended in order", - modifyFs: fs => stripInternalWithDependentOrder(fs, /*removeCommentsDisabled*/ true), - modifyAgainFs: fs => replaceText(fs, sources[project.first][source.ts][part.one], `/*@internal*/ interface`, "interface"), - ignoreDtsChanged: true, - baselineOnly: true - }); - - // Verify ignore dtsChanged - verifyOutFileScenario({ - subScenario: "stripInternal jsdoc style comment when one-two-three are prepended in order", - modifyFs: fs => stripInternalWithDependentOrder(fs, /*removeCommentsDisabled*/ false, /*jsDocStyle*/ true), - modifyAgainFs: fs => replaceText(fs, sources[project.first][source.ts][part.one], `/**@internal*/ interface`, "interface"), - ignoreDtsChanged: true, - baselineOnly: true - }); - - // Verify ignore dtsChanged - verifyOutFileScenario({ - subScenario: "stripInternal jsdoc style with comments emit enabled when one-two-three are prepended in order", - modifyFs: fs => stripInternalWithDependentOrder(fs, /*removeCommentsDisabled*/ true, /*jsDocStyle*/ true), - ignoreDtsChanged: true, - baselineOnly: true - }); - }); - - // only baseline - verifyOutFileScenario({ - subScenario: "stripInternal baseline when internal is inside another internal", - modifyFs: fs => { - stripInternalOfThird(fs); - prependText(fs, sources[project.first][source.ts][part.one], `namespace ts { + }); + // only baseline + verifyOutFileScenario({ + subScenario: "stripInternal baseline when internal is inside another internal", + modifyFs: fs => { + stripInternalOfThird(fs); + prependText(fs, sources[project.first][source.ts][part.one], `namespace ts { /* @internal */ /** * Subset of properties from SourceFile that are used in multiple utility functions @@ -603,18 +550,17 @@ ${internal} enum internalEnum { a, b, c }`); someProp: string; } }`); - }, - ignoreDtsChanged: true, - ignoreDtsUnchanged: true, - baselineOnly: true - }); - - // only baseline - verifyOutFileScenario({ - subScenario: "stripInternal when few members of enum are internal", - modifyFs: fs => { - stripInternalOfThird(fs); - prependText(fs, sources[project.first][source.ts][part.one], `enum TokenFlags { + }, + ignoreDtsChanged: true, + ignoreDtsUnchanged: true, + baselineOnly: true + }); + // only baseline + verifyOutFileScenario({ + subScenario: "stripInternal when few members of enum are internal", + modifyFs: fs => { + stripInternalOfThird(fs); + prependText(fs, sources[project.first][source.ts][part.one], `enum TokenFlags { None = 0, /* @internal */ PrecedingLineBreak = 1 << 0, @@ -637,96 +583,88 @@ ${internal} enum internalEnum { a, b, c }`); NumericLiteralFlags = Scientific | Octal | HexSpecifier | BinaryOrOctalSpecifier | ContainsSeparator } `); - }, - ignoreDtsChanged: true, - ignoreDtsUnchanged: true, - baselineOnly: true - }); - - verifyOutFileScenario({ - subScenario: "stripInternal when prepend is completely internal", - baselineOnly: true, - ignoreDtsChanged: true, - ignoreDtsUnchanged: true, - modifyFs: fs => { - fs.writeFileSync(sources[project.first][source.ts][part.one], "/* @internal */ const A = 1;"); - fs.writeFileSync(sources[project.third][source.ts][part.one], "const B = 2;"); - fs.writeFileSync(sources[project.first][source.config], JSON.stringify({ - compilerOptions: { - composite: true, - declaration: true, - declarationMap: true, - skipDefaultLibCheck: true, - sourceMap: true, - outFile: "./bin/first-output.js" - }, - files: [sources[project.first][source.ts][part.one]] - })); - fs.writeFileSync(sources[project.third][source.config], JSON.stringify({ - compilerOptions: { - composite: true, - declaration: true, - declarationMap: false, - stripInternal: true, - sourceMap: true, - outFile: "./thirdjs/output/third-output.js" - }, - references: [{ path: "../first", prepend: true }], - files: [sources[project.third][source.ts][part.one]] - })); - } - }); + }, + ignoreDtsChanged: true, + ignoreDtsUnchanged: true, + baselineOnly: true }); - - describe("empty source files", () => { - function makeThirdEmptySourceFile(fs: vfs.FileSystem) { - fs.writeFileSync(sources[project.third][source.ts][part.one], "", "utf8"); + verifyOutFileScenario({ + subScenario: "stripInternal when prepend is completely internal", + baselineOnly: true, + ignoreDtsChanged: true, + ignoreDtsUnchanged: true, + modifyFs: fs => { + fs.writeFileSync(sources[project.first][source.ts][part.one], "/* @internal */ const A = 1;"); + fs.writeFileSync(sources[project.third][source.ts][part.one], "const B = 2;"); + fs.writeFileSync(sources[project.first][source.config], JSON.stringify({ + compilerOptions: { + composite: true, + declaration: true, + declarationMap: true, + skipDefaultLibCheck: true, + sourceMap: true, + outFile: "./bin/first-output.js" + }, + files: [sources[project.first][source.ts][part.one]] + })); + fs.writeFileSync(sources[project.third][source.config], JSON.stringify({ + compilerOptions: { + composite: true, + declaration: true, + declarationMap: false, + stripInternal: true, + sourceMap: true, + outFile: "./thirdjs/output/third-output.js" + }, + references: [{ path: "../first", prepend: true }], + files: [sources[project.third][source.ts][part.one]] + })); } - - // Verify ignore dtsChanged - verifyOutFileScenario({ - subScenario: "when source files are empty in the own file", - modifyFs: makeThirdEmptySourceFile, - ignoreDtsChanged: true, - baselineOnly: true - }); - - // only baseline - verifyOutFileScenario({ - subScenario: "declarationMap and sourceMap disabled", - modifyFs: fs => { - makeThirdEmptySourceFile(fs); - replaceText(fs, sources[project.third][source.config], `"composite": true,`, ""); - replaceText(fs, sources[project.third][source.config], `"sourceMap": true,`, ""); - replaceText(fs, sources[project.third][source.config], `"declarationMap": true,`, ""); - }, - ignoreDtsChanged: true, - ignoreDtsUnchanged: true, - baselineOnly: true - }); }); }); - - verifyTsc({ - scenario: "outFile", - subScenario: "non module projects without prepend", - fs: () => outFileFs, - commandLineArgs: ["--b", "/src/third", "--verbose"], - modifyFs: fs => { - // No prepend - replaceText(fs, sources[project.third][source.config], `{ "path": "../first", "prepend": true }`, `{ "path": "../first" }`); - replaceText(fs, sources[project.third][source.config], `{ "path": "../second", "prepend": true }`, `{ "path": "../second" }`); - - // Non Modules - replaceText(fs, sources[project.first][source.config], `"composite": true,`, `"composite": true, "module": "none",`); - replaceText(fs, sources[project.second][source.config], `"composite": true,`, `"composite": true, "module": "none",`); - replaceText(fs, sources[project.third][source.config], `"composite": true,`, `"composite": true, "module": "none",`); - - // Own file emit - replaceText(fs, sources[project.first][source.config], `"outFile": "./bin/first-output.js",`, ""); - replaceText(fs, sources[project.second][source.config], `"outFile": "../2/second-output.js",`, ""); - replaceText(fs, sources[project.third][source.config], `"outFile": "./thirdjs/output/third-output.js",`, ""); - }, + describe("empty source files", () => { + function makeThirdEmptySourceFile(fs: FileSystem) { + fs.writeFileSync(sources[project.third][source.ts][part.one], "", "utf8"); + } + // Verify ignore dtsChanged + verifyOutFileScenario({ + subScenario: "when source files are empty in the own file", + modifyFs: makeThirdEmptySourceFile, + ignoreDtsChanged: true, + baselineOnly: true + }); + // only baseline + verifyOutFileScenario({ + subScenario: "declarationMap and sourceMap disabled", + modifyFs: fs => { + makeThirdEmptySourceFile(fs); + replaceText(fs, sources[project.third][source.config], `"composite": true,`, ""); + replaceText(fs, sources[project.third][source.config], `"sourceMap": true,`, ""); + replaceText(fs, sources[project.third][source.config], `"declarationMap": true,`, ""); + }, + ignoreDtsChanged: true, + ignoreDtsUnchanged: true, + baselineOnly: true + }); }); }); -} + verifyTsc({ + scenario: "outFile", + subScenario: "non module projects without prepend", + fs: () => outFileFs, + commandLineArgs: ["--b", "/src/third", "--verbose"], + modifyFs: fs => { + // No prepend + replaceText(fs, sources[project.third][source.config], `{ "path": "../first", "prepend": true }`, `{ "path": "../first" }`); + replaceText(fs, sources[project.third][source.config], `{ "path": "../second", "prepend": true }`, `{ "path": "../second" }`); + // Non Modules + replaceText(fs, sources[project.first][source.config], `"composite": true,`, `"composite": true, "module": "none",`); + replaceText(fs, sources[project.second][source.config], `"composite": true,`, `"composite": true, "module": "none",`); + replaceText(fs, sources[project.third][source.config], `"composite": true,`, `"composite": true, "module": "none",`); + // Own file emit + replaceText(fs, sources[project.first][source.config], `"outFile": "./bin/first-output.js",`, ""); + replaceText(fs, sources[project.second][source.config], `"outFile": "../2/second-output.js",`, ""); + replaceText(fs, sources[project.third][source.config], `"outFile": "./thirdjs/output/third-output.js",`, ""); + }, + }); +}); diff --git a/src/testRunner/unittests/tsbuild/referencesWithRootDirInParent.ts b/src/testRunner/unittests/tsbuild/referencesWithRootDirInParent.ts index bd78954780aab..77fe80a84ea56 100644 --- a/src/testRunner/unittests/tsbuild/referencesWithRootDirInParent.ts +++ b/src/testRunner/unittests/tsbuild/referencesWithRootDirInParent.ts @@ -1,61 +1,56 @@ -namespace ts { - describe("unittests:: tsbuild:: with rootDir of project reference in parentDirectory", () => { - let projFs: vfs.FileSystem; - before(() => { - projFs = loadProjectFromDisk("tests/projects/projectReferenceWithRootDirInParent"); - }); - - after(() => { - projFs = undefined!; // Release the contents - }); - - verifyTsc({ - scenario: "projectReferenceWithRootDirInParent", - subScenario: "builds correctly", - fs: () => projFs, - commandLineArgs: ["--b", "/src/src/main", "/src/src/other"], - }); - - verifyTsc({ - scenario: "projectReferenceWithRootDirInParent", - subScenario: "reports error for same tsbuildinfo file because no rootDir in the base", - fs: () => projFs, - commandLineArgs: ["--b", "/src/src/main", "--verbose"], - modifyFs: fs => replaceText(fs, "/src/tsconfig.base.json", `"rootDir": "./src/",`, ""), - }); - - verifyTsc({ - scenario: "projectReferenceWithRootDirInParent", - subScenario: "reports error for same tsbuildinfo file", - fs: () => projFs, - commandLineArgs: ["--b", "/src/src/main", "--verbose"], - modifyFs: fs => { - fs.writeFileSync("/src/src/main/tsconfig.json", JSON.stringify({ - compilerOptions: { composite: true, outDir: "../../dist/" }, - references: [{ path: "../other" }] - })); - fs.writeFileSync("/src/src/other/tsconfig.json", JSON.stringify({ - compilerOptions: { composite: true, outDir: "../../dist/" }, - })); - }, - }); - - verifyTsc({ - scenario: "projectReferenceWithRootDirInParent", - subScenario: "reports no error when tsbuildinfo differ", - fs: () => projFs, - commandLineArgs: ["--b", "/src/src/main/tsconfig.main.json", "--verbose"], - modifyFs: fs => { - fs.renameSync("/src/src/main/tsconfig.json", "/src/src/main/tsconfig.main.json"); - fs.renameSync("/src/src/other/tsconfig.json", "/src/src/other/tsconfig.other.json"); - fs.writeFileSync("/src/src/main/tsconfig.main.json", JSON.stringify({ - compilerOptions: { composite: true, outDir: "../../dist/" }, - references: [{ path: "../other/tsconfig.other.json" }] - })); - fs.writeFileSync("/src/src/other/tsconfig.other.json", JSON.stringify({ - compilerOptions: { composite: true, outDir: "../../dist/" }, - })); - }, - }); +import { FileSystem } from "../../vfs"; +import { loadProjectFromDisk, verifyTsc, replaceText } from "../../ts"; +describe("unittests:: tsbuild:: with rootDir of project reference in parentDirectory", () => { + let projFs: FileSystem; + before(() => { + projFs = loadProjectFromDisk("tests/projects/projectReferenceWithRootDirInParent"); }); -} + after(() => { + projFs = undefined!; // Release the contents + }); + verifyTsc({ + scenario: "projectReferenceWithRootDirInParent", + subScenario: "builds correctly", + fs: () => projFs, + commandLineArgs: ["--b", "/src/src/main", "/src/src/other"], + }); + verifyTsc({ + scenario: "projectReferenceWithRootDirInParent", + subScenario: "reports error for same tsbuildinfo file because no rootDir in the base", + fs: () => projFs, + commandLineArgs: ["--b", "/src/src/main", "--verbose"], + modifyFs: fs => replaceText(fs, "/src/tsconfig.base.json", `"rootDir": "./src/",`, ""), + }); + verifyTsc({ + scenario: "projectReferenceWithRootDirInParent", + subScenario: "reports error for same tsbuildinfo file", + fs: () => projFs, + commandLineArgs: ["--b", "/src/src/main", "--verbose"], + modifyFs: fs => { + fs.writeFileSync("/src/src/main/tsconfig.json", JSON.stringify({ + compilerOptions: { composite: true, outDir: "../../dist/" }, + references: [{ path: "../other" }] + })); + fs.writeFileSync("/src/src/other/tsconfig.json", JSON.stringify({ + compilerOptions: { composite: true, outDir: "../../dist/" }, + })); + }, + }); + verifyTsc({ + scenario: "projectReferenceWithRootDirInParent", + subScenario: "reports no error when tsbuildinfo differ", + fs: () => projFs, + commandLineArgs: ["--b", "/src/src/main/tsconfig.main.json", "--verbose"], + modifyFs: fs => { + fs.renameSync("/src/src/main/tsconfig.json", "/src/src/main/tsconfig.main.json"); + fs.renameSync("/src/src/other/tsconfig.json", "/src/src/other/tsconfig.other.json"); + fs.writeFileSync("/src/src/main/tsconfig.main.json", JSON.stringify({ + compilerOptions: { composite: true, outDir: "../../dist/" }, + references: [{ path: "../other/tsconfig.other.json" }] + })); + fs.writeFileSync("/src/src/other/tsconfig.other.json", JSON.stringify({ + compilerOptions: { composite: true, outDir: "../../dist/" }, + })); + }, + }); +}); diff --git a/src/testRunner/unittests/tsbuild/resolveJsonModule.ts b/src/testRunner/unittests/tsbuild/resolveJsonModule.ts index 0a5144da7c2eb..06cbd9306dac3 100644 --- a/src/testRunner/unittests/tsbuild/resolveJsonModule.ts +++ b/src/testRunner/unittests/tsbuild/resolveJsonModule.ts @@ -1,82 +1,73 @@ -namespace ts { - describe("unittests:: tsbuild:: with resolveJsonModule option on project resolveJsonModuleAndComposite", () => { - let projFs: vfs.FileSystem; - before(() => { - projFs = loadProjectFromDisk("tests/projects/resolveJsonModuleAndComposite"); - }); - - after(() => { - projFs = undefined!; // Release the contents - }); - - verifyTsc({ - scenario: "resolveJsonModule", - subScenario: "include only", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tsconfig_withInclude.json"], - }); - - verifyTsc({ - scenario: "resolveJsonModule", - subScenario: "include of json along with other include", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tsconfig_withIncludeOfJson.json"], - }); - - verifyTsc({ - scenario: "resolveJsonModule", - subScenario: "include of json along with other include and file name matches ts file", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tsconfig_withIncludeOfJson.json"], - modifyFs: fs => { - fs.rimrafSync("/src/src/hello.json"); - fs.writeFileSync("/src/src/index.json", JSON.stringify({ hello: "world" })); - fs.writeFileSync("/src/src/index.ts", `import hello from "./index.json" +import { FileSystem } from "../../vfs"; +import { loadProjectFromDisk, verifyTsc, verifyTscIncrementalEdits, replaceText, noChangeRun } from "../../ts"; +describe("unittests:: tsbuild:: with resolveJsonModule option on project resolveJsonModuleAndComposite", () => { + let projFs: FileSystem; + before(() => { + projFs = loadProjectFromDisk("tests/projects/resolveJsonModuleAndComposite"); + }); + after(() => { + projFs = undefined!; // Release the contents + }); + verifyTsc({ + scenario: "resolveJsonModule", + subScenario: "include only", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tsconfig_withInclude.json"], + }); + verifyTsc({ + scenario: "resolveJsonModule", + subScenario: "include of json along with other include", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tsconfig_withIncludeOfJson.json"], + }); + verifyTsc({ + scenario: "resolveJsonModule", + subScenario: "include of json along with other include and file name matches ts file", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tsconfig_withIncludeOfJson.json"], + modifyFs: fs => { + fs.rimrafSync("/src/src/hello.json"); + fs.writeFileSync("/src/src/index.json", JSON.stringify({ hello: "world" })); + fs.writeFileSync("/src/src/index.ts", `import hello from "./index.json" export default hello.hello`); - }, - }); - - verifyTsc({ - scenario: "resolveJsonModule", - subScenario: "files containing json file", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tsconfig_withFiles.json"], - }); - - verifyTsc({ - scenario: "resolveJsonModule", - subScenario: "include and files", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tsconfig_withIncludeAndFiles.json"], - }); - - verifyTscIncrementalEdits({ - scenario: "resolveJsonModule", - subScenario: "sourcemap", - fs: () => projFs, - commandLineArgs: ["--b", "src/tsconfig_withFiles.json", "--verbose"], - modifyFs: fs => replaceText(fs, "src/tsconfig_withFiles.json", `"composite": true,`, `"composite": true, "sourceMap": true,`), - incrementalScenarios: [noChangeRun] - }); - - verifyTscIncrementalEdits({ - scenario: "resolveJsonModule", - subScenario: "without outDir", - fs: () => projFs, - commandLineArgs: ["--b", "src/tsconfig_withFiles.json", "--verbose"], - modifyFs: fs => replaceText(fs, "src/tsconfig_withFiles.json", `"outDir": "dist",`, ""), - incrementalScenarios: [noChangeRun] - }); + }, }); - - describe("unittests:: tsbuild:: with resolveJsonModule option on project importJsonFromProjectReference", () => { - verifyTscIncrementalEdits({ - scenario: "resolveJsonModule", - subScenario: "importing json module from project reference", - fs: () => loadProjectFromDisk("tests/projects/importJsonFromProjectReference"), - commandLineArgs: ["--b", "src/tsconfig.json", "--verbose"], - incrementalScenarios: [noChangeRun] - }); + verifyTsc({ + scenario: "resolveJsonModule", + subScenario: "files containing json file", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tsconfig_withFiles.json"], + }); + verifyTsc({ + scenario: "resolveJsonModule", + subScenario: "include and files", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tsconfig_withIncludeAndFiles.json"], + }); + verifyTscIncrementalEdits({ + scenario: "resolveJsonModule", + subScenario: "sourcemap", + fs: () => projFs, + commandLineArgs: ["--b", "src/tsconfig_withFiles.json", "--verbose"], + modifyFs: fs => replaceText(fs, "src/tsconfig_withFiles.json", `"composite": true,`, `"composite": true, "sourceMap": true,`), + incrementalScenarios: [noChangeRun] + }); + verifyTscIncrementalEdits({ + scenario: "resolveJsonModule", + subScenario: "without outDir", + fs: () => projFs, + commandLineArgs: ["--b", "src/tsconfig_withFiles.json", "--verbose"], + modifyFs: fs => replaceText(fs, "src/tsconfig_withFiles.json", `"outDir": "dist",`, ""), + incrementalScenarios: [noChangeRun] + }); +}); +describe("unittests:: tsbuild:: with resolveJsonModule option on project importJsonFromProjectReference", () => { + verifyTscIncrementalEdits({ + scenario: "resolveJsonModule", + subScenario: "importing json module from project reference", + fs: () => loadProjectFromDisk("tests/projects/importJsonFromProjectReference"), + commandLineArgs: ["--b", "src/tsconfig.json", "--verbose"], + incrementalScenarios: [noChangeRun] }); -} +}); diff --git a/src/testRunner/unittests/tsbuild/sample.ts b/src/testRunner/unittests/tsbuild/sample.ts index 10fe37cb33fa7..19cc14e269c81 100644 --- a/src/testRunner/unittests/tsbuild/sample.ts +++ b/src/testRunner/unittests/tsbuild/sample.ts @@ -1,453 +1,386 @@ -namespace ts { - describe("unittests:: tsbuild:: on 'sample1' project", () => { - let projFs: vfs.FileSystem; - const testsOutputs = ["/src/tests/index.js", "/src/tests/index.d.ts", "/src/tests/tsconfig.tsbuildinfo"]; - const logicOutputs = ["/src/logic/index.js", "/src/logic/index.js.map", "/src/logic/index.d.ts", "/src/logic/tsconfig.tsbuildinfo"]; - const coreOutputs = ["/src/core/index.js", "/src/core/index.d.ts", "/src/core/index.d.ts.map", "/src/core/tsconfig.tsbuildinfo"]; - const allExpectedOutputs = [...testsOutputs, ...logicOutputs, ...coreOutputs]; - - before(() => { - projFs = loadProjectFromDisk("tests/projects/sample1"); +import { FileSystem } from "../../vfs"; +import { loadProjectFromDisk, createSolutionBuilder, verifyTsc, replaceText, verifyTscIncrementalEdits, noChangeRun, verifyOutputsPresent, verifyOutputsAbsent, ExitStatus, BuildOptions, getFsWithTime, BuildKind, noop, changeCompilerVersion, getExpectedDiagnosticForProjectsInBuild, Diagnostics, createAbstractBuilder, ResolvedConfigFileName, emptyArray, appendText, createMap, UpToDateStatusType, ResolvedConfigFilePath, TscIncremental, libContent } from "../../ts"; +import { SolutionBuilderHost, version } from "../../fakes"; +import * as ts from "../../ts"; +describe("unittests:: tsbuild:: on 'sample1' project", () => { + let projFs: FileSystem; + const testsOutputs = ["/src/tests/index.js", "/src/tests/index.d.ts", "/src/tests/tsconfig.tsbuildinfo"]; + const logicOutputs = ["/src/logic/index.js", "/src/logic/index.js.map", "/src/logic/index.d.ts", "/src/logic/tsconfig.tsbuildinfo"]; + const coreOutputs = ["/src/core/index.js", "/src/core/index.d.ts", "/src/core/index.d.ts.map", "/src/core/tsconfig.tsbuildinfo"]; + const allExpectedOutputs = [...testsOutputs, ...logicOutputs, ...coreOutputs]; + before(() => { + projFs = loadProjectFromDisk("tests/projects/sample1"); + }); + after(() => { + projFs = undefined!; // Release the contents + }); + function getSampleFsAfterBuild() { + const fs = projFs.shadow(); + const host = SolutionBuilderHost.create(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], {}); + builder.build(); + fs.makeReadonly(); + return fs; + } + describe("sanity check of clean build of 'sample1' project", () => { + verifyTsc({ + scenario: "sample1", + subScenario: "builds correctly when outDir is specified", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tests"], + modifyFs: fs => fs.writeFileSync("/src/logic/tsconfig.json", JSON.stringify({ + compilerOptions: { composite: true, declaration: true, sourceMap: true, outDir: "outDir" }, + references: [{ path: "../core" }] + })), + }); + verifyTsc({ + scenario: "sample1", + subScenario: "builds correctly when declarationDir is specified", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tests"], + modifyFs: fs => fs.writeFileSync("/src/logic/tsconfig.json", JSON.stringify({ + compilerOptions: { composite: true, declaration: true, sourceMap: true, declarationDir: "out/decls" }, + references: [{ path: "../core" }] + })), + }); + verifyTsc({ + scenario: "sample1", + subScenario: "builds correctly when project is not composite or doesnt have any references", + fs: () => projFs, + commandLineArgs: ["--b", "/src/core", "--verbose"], + modifyFs: fs => replaceText(fs, "/src/core/tsconfig.json", `"composite": true,`, ""), }); - - after(() => { - projFs = undefined!; // Release the contents + }); + describe("dry builds", () => { + verifyTsc({ + scenario: "sample1", + subScenario: "does not write any files in a dry build", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tests", "--dry"], + }); + }); + describe("clean builds", () => { + verifyTscIncrementalEdits({ + scenario: "sample1", + subScenario: "removes all files it built", + fs: getSampleFsAfterBuild, + commandLineArgs: ["--b", "/src/tests", "--clean"], + incrementalScenarios: [noChangeRun] }); - - function getSampleFsAfterBuild() { + it("cleans till project specified", () => { const fs = projFs.shadow(); - const host = fakes.SolutionBuilderHost.create(fs); + const host = SolutionBuilderHost.create(fs); const builder = createSolutionBuilder(host, ["/src/tests"], {}); builder.build(); - fs.makeReadonly(); - return fs; - } - - describe("sanity check of clean build of 'sample1' project", () => { - verifyTsc({ - scenario: "sample1", - subScenario: "builds correctly when outDir is specified", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tests"], - modifyFs: fs => fs.writeFileSync("/src/logic/tsconfig.json", JSON.stringify({ - compilerOptions: { composite: true, declaration: true, sourceMap: true, outDir: "outDir" }, - references: [{ path: "../core" }] - })), - }); - - verifyTsc({ - scenario: "sample1", - subScenario: "builds correctly when declarationDir is specified", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tests"], - modifyFs: fs => fs.writeFileSync("/src/logic/tsconfig.json", JSON.stringify({ - compilerOptions: { composite: true, declaration: true, sourceMap: true, declarationDir: "out/decls" }, - references: [{ path: "../core" }] - })), - }); - - verifyTsc({ - scenario: "sample1", - subScenario: "builds correctly when project is not composite or doesnt have any references", - fs: () => projFs, - commandLineArgs: ["--b", "/src/core", "--verbose"], - modifyFs: fs => replaceText(fs, "/src/core/tsconfig.json", `"composite": true,`, ""), - }); - }); - - describe("dry builds", () => { - verifyTsc({ - scenario: "sample1", - subScenario: "does not write any files in a dry build", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tests", "--dry"], - }); + const result = builder.clean("/src/logic"); + host.assertDiagnosticMessages( /*empty*/); + verifyOutputsPresent(fs, testsOutputs); + verifyOutputsAbsent(fs, [...logicOutputs, ...coreOutputs]); + assert.equal(result, ExitStatus.Success); }); - - describe("clean builds", () => { - verifyTscIncrementalEdits({ - scenario: "sample1", - subScenario: "removes all files it built", - fs: getSampleFsAfterBuild, - commandLineArgs: ["--b", "/src/tests", "--clean"], - incrementalScenarios: [noChangeRun] - }); - - it("cleans till project specified", () => { - const fs = projFs.shadow(); - const host = fakes.SolutionBuilderHost.create(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], {}); - builder.build(); - const result = builder.clean("/src/logic"); - host.assertDiagnosticMessages(/*empty*/); - verifyOutputsPresent(fs, testsOutputs); - verifyOutputsAbsent(fs, [...logicOutputs, ...coreOutputs]); - assert.equal(result, ExitStatus.Success); - }); - - it("cleaning project in not build order doesnt throw error", () => { - const fs = projFs.shadow(); - const host = fakes.SolutionBuilderHost.create(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], {}); - builder.build(); - const result = builder.clean("/src/logic2"); - host.assertDiagnosticMessages(/*empty*/); - verifyOutputsPresent(fs, allExpectedOutputs); - assert.equal(result, ExitStatus.InvalidProject_OutputsSkipped); - }); + it("cleaning project in not build order doesnt throw error", () => { + const fs = projFs.shadow(); + const host = SolutionBuilderHost.create(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], {}); + builder.build(); + const result = builder.clean("/src/logic2"); + host.assertDiagnosticMessages( /*empty*/); + verifyOutputsPresent(fs, allExpectedOutputs); + assert.equal(result, ExitStatus.InvalidProject_OutputsSkipped); }); - - describe("force builds", () => { - verifyTscIncrementalEdits({ - scenario: "sample1", - subScenario: "always builds under with force option", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tests", "--force"], - incrementalScenarios: [noChangeRun] - }); + }); + describe("force builds", () => { + verifyTscIncrementalEdits({ + scenario: "sample1", + subScenario: "always builds under with force option", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tests", "--force"], + incrementalScenarios: [noChangeRun] }); - - describe("can detect when and what to rebuild", () => { - function initializeWithBuild(opts?: BuildOptions) { - const { fs, tick } = getFsWithTime(projFs); - const host = fakes.SolutionBuilderHost.create(fs); - let builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true }); - builder.build(); - host.clearDiagnostics(); - tick(); - builder = createSolutionBuilder(host, ["/src/tests"], { ...(opts || {}), verbose: true }); - return { fs, host, builder }; - } - - verifyTscIncrementalEdits({ - scenario: "sample1", - subScenario: "can detect when and what to rebuild", - fs: getSampleFsAfterBuild, - commandLineArgs: ["--b", "/src/tests", "--verbose"], - incrementalScenarios: [ - // Update a file in the leaf node (tests), only it should rebuild the last one - { - subScenario: "Only builds the leaf node project", - buildKind: BuildKind.IncrementalDtsUnchanged, - modifyFs: fs => fs.writeFileSync("/src/tests/index.ts", "const m = 10;"), - }, - // Update a file in the parent (without affecting types), should get fast downstream builds - { - subScenario: "Detects type-only changes in upstream projects", - buildKind: BuildKind.IncrementalDtsChange, - modifyFs: fs => replaceText(fs, "/src/core/index.ts", "HELLO WORLD", "WELCOME PLANET"), - }, - { - subScenario: "indicates that it would skip builds during a dry build", - buildKind: BuildKind.IncrementalDtsUnchanged, - modifyFs: noop, - commandLineArgs: ["--b", "/src/tests", "--dry"], - }, - { - subScenario: "rebuilds from start if force option is set", - buildKind: BuildKind.IncrementalDtsChange, - modifyFs: noop, - commandLineArgs: ["--b", "/src/tests", "--verbose", "--force"], - }, - { - subScenario: "rebuilds when tsconfig changes", - buildKind: BuildKind.IncrementalDtsChange, - modifyFs: fs => replaceText(fs, "/src/tests/tsconfig.json", `"composite": true`, `"composite": true, "target": "es3"`), - }, - ] - }); - - it("rebuilds completely when version in tsbuildinfo doesnt match ts version", () => { - const { host, builder } = initializeWithBuild(); - changeCompilerVersion(host); - builder.build(); - host.assertDiagnosticMessages( - getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), - [Diagnostics.Project_0_is_out_of_date_because_output_for_it_was_generated_with_version_1_that_differs_with_current_version_2, "src/core/tsconfig.json", fakes.version, version], - [Diagnostics.Building_project_0, "/src/core/tsconfig.json"], - [Diagnostics.Project_0_is_out_of_date_because_output_for_it_was_generated_with_version_1_that_differs_with_current_version_2, "src/logic/tsconfig.json", fakes.version, version], - [Diagnostics.Building_project_0, "/src/logic/tsconfig.json"], - [Diagnostics.Project_0_is_out_of_date_because_output_for_it_was_generated_with_version_1_that_differs_with_current_version_2, "src/tests/tsconfig.json", fakes.version, version], - [Diagnostics.Building_project_0, "/src/tests/tsconfig.json"], - ); - }); - - it("does not rebuild if there is no program and bundle in the ts build info event if version doesnt match ts version", () => { - const { fs, tick } = getFsWithTime(projFs); - const host = fakes.SolutionBuilderHost.create(fs, /*options*/ undefined, /*setParentNodes*/ undefined, createAbstractBuilder); - let builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true }); - builder.build(); - host.assertDiagnosticMessages( - getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), - [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/core/tsconfig.json", "src/core/anotherModule.js"], - [Diagnostics.Building_project_0, "/src/core/tsconfig.json"], - [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/logic/tsconfig.json", "src/logic/index.js"], - [Diagnostics.Building_project_0, "/src/logic/tsconfig.json"], - [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/tests/tsconfig.json", "src/tests/index.js"], - [Diagnostics.Building_project_0, "/src/tests/tsconfig.json"] - ); - verifyOutputsPresent(fs, allExpectedOutputs); - - host.clearDiagnostics(); - tick(); - builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true }); - changeCompilerVersion(host); - builder.build(); - host.assertDiagnosticMessages( - getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), - [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/core/tsconfig.json", "src/core/anotherModule.ts", "src/core/anotherModule.js"], - [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/logic/tsconfig.json", "src/logic/index.ts", "src/logic/index.js"], - [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/tests/tsconfig.json", "src/tests/index.ts", "src/tests/index.js"] - ); - }); - - verifyTscIncrementalEdits({ - scenario: "sample1", - subScenario: "rebuilds when extended config file changes", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tests", "--verbose"], - modifyFs: fs => { - fs.writeFileSync("/src/tests/tsconfig.base.json", JSON.stringify({ compilerOptions: { target: "es3" } })); - replaceText(fs, "/src/tests/tsconfig.json", `"references": [`, `"extends": "./tsconfig.base.json", "references": [`); + }); + describe("can detect when and what to rebuild", () => { + function initializeWithBuild(opts?: BuildOptions) { + const { fs, tick } = getFsWithTime(projFs); + const host = SolutionBuilderHost.create(fs); + let builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true }); + builder.build(); + host.clearDiagnostics(); + tick(); + builder = createSolutionBuilder(host, ["/src/tests"], { ...(opts || {}), verbose: true }); + return { fs, host, builder }; + } + verifyTscIncrementalEdits({ + scenario: "sample1", + subScenario: "can detect when and what to rebuild", + fs: getSampleFsAfterBuild, + commandLineArgs: ["--b", "/src/tests", "--verbose"], + incrementalScenarios: [ + // Update a file in the leaf node (tests), only it should rebuild the last one + { + subScenario: "Only builds the leaf node project", + buildKind: BuildKind.IncrementalDtsUnchanged, + modifyFs: fs => fs.writeFileSync("/src/tests/index.ts", "const m = 10;"), + }, + // Update a file in the parent (without affecting types), should get fast downstream builds + { + subScenario: "Detects type-only changes in upstream projects", + buildKind: BuildKind.IncrementalDtsChange, + modifyFs: fs => replaceText(fs, "/src/core/index.ts", "HELLO WORLD", "WELCOME PLANET"), }, - incrementalScenarios: [{ + { + subScenario: "indicates that it would skip builds during a dry build", + buildKind: BuildKind.IncrementalDtsUnchanged, + modifyFs: noop, + commandLineArgs: ["--b", "/src/tests", "--dry"], + }, + { + subScenario: "rebuilds from start if force option is set", + buildKind: BuildKind.IncrementalDtsChange, + modifyFs: noop, + commandLineArgs: ["--b", "/src/tests", "--verbose", "--force"], + }, + { + subScenario: "rebuilds when tsconfig changes", + buildKind: BuildKind.IncrementalDtsChange, + modifyFs: fs => replaceText(fs, "/src/tests/tsconfig.json", `"composite": true`, `"composite": true, "target": "es3"`), + }, + ] + }); + it("rebuilds completely when version in tsbuildinfo doesnt match ts version", () => { + const { host, builder } = initializeWithBuild(); + changeCompilerVersion(host); + builder.build(); + host.assertDiagnosticMessages(getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), [Diagnostics.Project_0_is_out_of_date_because_output_for_it_was_generated_with_version_1_that_differs_with_current_version_2, "src/core/tsconfig.json", version, ts.version], [Diagnostics.Building_project_0, "/src/core/tsconfig.json"], [Diagnostics.Project_0_is_out_of_date_because_output_for_it_was_generated_with_version_1_that_differs_with_current_version_2, "src/logic/tsconfig.json", version, ts.version], [Diagnostics.Building_project_0, "/src/logic/tsconfig.json"], [Diagnostics.Project_0_is_out_of_date_because_output_for_it_was_generated_with_version_1_that_differs_with_current_version_2, "src/tests/tsconfig.json", version, ts.version], [Diagnostics.Building_project_0, "/src/tests/tsconfig.json"]); + }); + it("does not rebuild if there is no program and bundle in the ts build info event if version doesnt match ts version", () => { + const { fs, tick } = getFsWithTime(projFs); + const host = SolutionBuilderHost.create(fs, /*options*/ undefined, /*setParentNodes*/ undefined, createAbstractBuilder); + let builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true }); + builder.build(); + host.assertDiagnosticMessages(getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/core/tsconfig.json", "src/core/anotherModule.js"], [Diagnostics.Building_project_0, "/src/core/tsconfig.json"], [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/logic/tsconfig.json", "src/logic/index.js"], [Diagnostics.Building_project_0, "/src/logic/tsconfig.json"], [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/tests/tsconfig.json", "src/tests/index.js"], [Diagnostics.Building_project_0, "/src/tests/tsconfig.json"]); + verifyOutputsPresent(fs, allExpectedOutputs); + host.clearDiagnostics(); + tick(); + builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true }); + changeCompilerVersion(host); + builder.build(); + host.assertDiagnosticMessages(getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json", "src/tests/tsconfig.json"), [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/core/tsconfig.json", "src/core/anotherModule.ts", "src/core/anotherModule.js"], [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/logic/tsconfig.json", "src/logic/index.ts", "src/logic/index.js"], [Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, "src/tests/tsconfig.json", "src/tests/index.ts", "src/tests/index.js"]); + }); + verifyTscIncrementalEdits({ + scenario: "sample1", + subScenario: "rebuilds when extended config file changes", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tests", "--verbose"], + modifyFs: fs => { + fs.writeFileSync("/src/tests/tsconfig.base.json", JSON.stringify({ compilerOptions: { target: "es3" } })); + replaceText(fs, "/src/tests/tsconfig.json", `"references": [`, `"extends": "./tsconfig.base.json", "references": [`); + }, + incrementalScenarios: [{ buildKind: BuildKind.IncrementalDtsChange, modifyFs: fs => fs.writeFileSync("/src/tests/tsconfig.base.json", JSON.stringify({ compilerOptions: {} })) }] - }); - - it("builds till project specified", () => { - const fs = projFs.shadow(); - const host = fakes.SolutionBuilderHost.create(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], {}); - const result = builder.build("/src/logic"); - host.assertDiagnosticMessages(/*empty*/); - verifyOutputsAbsent(fs, testsOutputs); - verifyOutputsPresent(fs, [...logicOutputs, ...coreOutputs]); - assert.equal(result, ExitStatus.Success); - }); - - it("building project in not build order doesnt throw error", () => { - const fs = projFs.shadow(); - const host = fakes.SolutionBuilderHost.create(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], {}); - const result = builder.build("/src/logic2"); - host.assertDiagnosticMessages(/*empty*/); - verifyOutputsAbsent(fs, allExpectedOutputs); - assert.equal(result, ExitStatus.InvalidProject_OutputsSkipped); - }); - - it("building using getNextInvalidatedProject", () => { - interface SolutionBuilderResult { - project: ResolvedConfigFileName; - result: T; - } - - const fs = projFs.shadow(); - const host = fakes.SolutionBuilderHost.create(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], {}); - verifyBuildNextResult({ - project: "/src/core/tsconfig.json" as ResolvedConfigFileName, - result: ExitStatus.Success - }, coreOutputs, [...logicOutputs, ...testsOutputs]); - - verifyBuildNextResult({ - project: "/src/logic/tsconfig.json" as ResolvedConfigFileName, - result: ExitStatus.Success - }, [...coreOutputs, ...logicOutputs], testsOutputs); - - verifyBuildNextResult({ - project: "/src/tests/tsconfig.json" as ResolvedConfigFileName, - result: ExitStatus.Success - }, allExpectedOutputs, emptyArray); - - verifyBuildNextResult(/*expected*/ undefined, allExpectedOutputs, emptyArray); - - function verifyBuildNextResult( - expected: SolutionBuilderResult | undefined, - presentOutputs: readonly string[], - absentOutputs: readonly string[] - ) { - const project = builder.getNextInvalidatedProject(); - const result = project && project.done(); - assert.deepEqual(project && { project: project.project, result }, expected); - verifyOutputsPresent(fs, presentOutputs); - verifyOutputsAbsent(fs, absentOutputs); - } - }); - - it("building using buildReferencedProject", () => { - const fs = projFs.shadow(); - const host = fakes.SolutionBuilderHost.create(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true }); - builder.buildReferences("/src/tests"); - host.assertDiagnosticMessages( - getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json"), - [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/core/tsconfig.json", "src/core/anotherModule.js"], - [Diagnostics.Building_project_0, "/src/core/tsconfig.json"], - [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/logic/tsconfig.json", "src/logic/index.js"], - [Diagnostics.Building_project_0, "/src/logic/tsconfig.json"], - ); - verifyOutputsPresent(fs, [...coreOutputs, ...logicOutputs]); - verifyOutputsAbsent(fs, testsOutputs); - }); }); - - describe("downstream-blocked compilations", () => { - verifyTsc({ - scenario: "sample1", - subScenario: "does not build downstream projects if upstream projects have errors", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tests", "--verbose"], - modifyFs: fs => replaceText(fs, "/src/logic/index.ts", "c.multiply(10, 15)", `c.muitply()`) - }); + it("builds till project specified", () => { + const fs = projFs.shadow(); + const host = SolutionBuilderHost.create(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], {}); + const result = builder.build("/src/logic"); + host.assertDiagnosticMessages( /*empty*/); + verifyOutputsAbsent(fs, testsOutputs); + verifyOutputsPresent(fs, [...logicOutputs, ...coreOutputs]); + assert.equal(result, ExitStatus.Success); + }); + it("building project in not build order doesnt throw error", () => { + const fs = projFs.shadow(); + const host = SolutionBuilderHost.create(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], {}); + const result = builder.build("/src/logic2"); + host.assertDiagnosticMessages( /*empty*/); + verifyOutputsAbsent(fs, allExpectedOutputs); + assert.equal(result, ExitStatus.InvalidProject_OutputsSkipped); }); - - describe("project invalidation", () => { - it("invalidates projects correctly", () => { - const { fs, time, tick } = getFsWithTime(projFs); - const host = fakes.SolutionBuilderHost.create(fs); - const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: false }); - - builder.build(); - host.assertDiagnosticMessages(/*empty*/); - - // Update a timestamp in the middle project - tick(); - appendText(fs, "/src/logic/index.ts", "function foo() {}"); - const originalWriteFile = fs.writeFileSync; - const writtenFiles = createMap(); - fs.writeFileSync = (path, data, encoding) => { - writtenFiles.set(path, true); - originalWriteFile.call(fs, path, data, encoding); - }; - // Because we haven't reset the build context, the builder should assume there's nothing to do right now - const status = builder.getUpToDateStatusOfProject("/src/logic"); - assert.equal(status.type, UpToDateStatusType.UpToDate, "Project should be assumed to be up-to-date"); - verifyInvalidation(/*expectedToWriteTests*/ false); - - // Rebuild this project - fs.writeFileSync("/src/logic/index.ts", `${fs.readFileSync("/src/logic/index.ts")} + it("building using getNextInvalidatedProject", () => { + interface SolutionBuilderResult { + project: ResolvedConfigFileName; + result: T; + } + const fs = projFs.shadow(); + const host = SolutionBuilderHost.create(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], {}); + verifyBuildNextResult({ + project: ("/src/core/tsconfig.json" as ResolvedConfigFileName), + result: ExitStatus.Success + }, coreOutputs, [...logicOutputs, ...testsOutputs]); + verifyBuildNextResult({ + project: ("/src/logic/tsconfig.json" as ResolvedConfigFileName), + result: ExitStatus.Success + }, [...coreOutputs, ...logicOutputs], testsOutputs); + verifyBuildNextResult({ + project: ("/src/tests/tsconfig.json" as ResolvedConfigFileName), + result: ExitStatus.Success + }, allExpectedOutputs, emptyArray); + verifyBuildNextResult(/*expected*/ undefined, allExpectedOutputs, emptyArray); + function verifyBuildNextResult(expected: SolutionBuilderResult | undefined, presentOutputs: readonly string[], absentOutputs: readonly string[]) { + const project = builder.getNextInvalidatedProject(); + const result = project && project.done(); + assert.deepEqual(project && { project: project.project, result }, expected); + verifyOutputsPresent(fs, presentOutputs); + verifyOutputsAbsent(fs, absentOutputs); + } + }); + it("building using buildReferencedProject", () => { + const fs = projFs.shadow(); + const host = SolutionBuilderHost.create(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], { verbose: true }); + builder.buildReferences("/src/tests"); + host.assertDiagnosticMessages(getExpectedDiagnosticForProjectsInBuild("src/core/tsconfig.json", "src/logic/tsconfig.json"), [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/core/tsconfig.json", "src/core/anotherModule.js"], [Diagnostics.Building_project_0, "/src/core/tsconfig.json"], [Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/logic/tsconfig.json", "src/logic/index.js"], [Diagnostics.Building_project_0, "/src/logic/tsconfig.json"]); + verifyOutputsPresent(fs, [...coreOutputs, ...logicOutputs]); + verifyOutputsAbsent(fs, testsOutputs); + }); + }); + describe("downstream-blocked compilations", () => { + verifyTsc({ + scenario: "sample1", + subScenario: "does not build downstream projects if upstream projects have errors", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tests", "--verbose"], + modifyFs: fs => replaceText(fs, "/src/logic/index.ts", "c.multiply(10, 15)", `c.muitply()`) + }); + }); + describe("project invalidation", () => { + it("invalidates projects correctly", () => { + const { fs, time, tick } = getFsWithTime(projFs); + const host = SolutionBuilderHost.create(fs); + const builder = createSolutionBuilder(host, ["/src/tests"], { dry: false, force: false, verbose: false }); + builder.build(); + host.assertDiagnosticMessages( /*empty*/); + // Update a timestamp in the middle project + tick(); + appendText(fs, "/src/logic/index.ts", "function foo() {}"); + const originalWriteFile = fs.writeFileSync; + const writtenFiles = createMap(); + fs.writeFileSync = (path, data, encoding) => { + writtenFiles.set(path, true); + originalWriteFile.call(fs, path, data, encoding); + }; + // Because we haven't reset the build context, the builder should assume there's nothing to do right now + const status = builder.getUpToDateStatusOfProject("/src/logic"); + assert.equal(status.type, UpToDateStatusType.UpToDate, "Project should be assumed to be up-to-date"); + verifyInvalidation(/*expectedToWriteTests*/ false); + // Rebuild this project + fs.writeFileSync("/src/logic/index.ts", `${fs.readFileSync("/src/logic/index.ts")} export class cNew {}`); - verifyInvalidation(/*expectedToWriteTests*/ true); - - function verifyInvalidation(expectedToWriteTests: boolean) { - // Rebuild this project - tick(); - builder.invalidateProject("/src/logic/tsconfig.json" as ResolvedConfigFilePath); - builder.buildNextInvalidatedProject(); - // The file should be updated - assert.isTrue(writtenFiles.has("/src/logic/index.js"), "JS file should have been rebuilt"); - assert.equal(fs.statSync("/src/logic/index.js").mtimeMs, time(), "JS file should have been rebuilt"); - assert.isFalse(writtenFiles.has("/src/tests/index.js"), "Downstream JS file should *not* have been rebuilt"); - assert.isBelow(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should *not* have been rebuilt"); - writtenFiles.clear(); - - // Build downstream projects should update 'tests', but not 'core' - tick(); - builder.buildNextInvalidatedProject(); - if (expectedToWriteTests) { - assert.isTrue(writtenFiles.has("/src/tests/index.js"), "Downstream JS file should have been rebuilt"); - } - else { - assert.equal(writtenFiles.size, 0, "Should not write any new files"); - } - assert.equal(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should have new timestamp"); - assert.isBelow(fs.statSync("/src/core/index.js").mtimeMs, time(), "Upstream JS file should not have been rebuilt"); + verifyInvalidation(/*expectedToWriteTests*/ true); + function verifyInvalidation(expectedToWriteTests: boolean) { + // Rebuild this project + tick(); + builder.invalidateProject(("/src/logic/tsconfig.json" as ResolvedConfigFilePath)); + builder.buildNextInvalidatedProject(); + // The file should be updated + assert.isTrue(writtenFiles.has("/src/logic/index.js"), "JS file should have been rebuilt"); + assert.equal(fs.statSync("/src/logic/index.js").mtimeMs, time(), "JS file should have been rebuilt"); + assert.isFalse(writtenFiles.has("/src/tests/index.js"), "Downstream JS file should *not* have been rebuilt"); + assert.isBelow(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should *not* have been rebuilt"); + writtenFiles.clear(); + // Build downstream projects should update 'tests', but not 'core' + tick(); + builder.buildNextInvalidatedProject(); + if (expectedToWriteTests) { + assert.isTrue(writtenFiles.has("/src/tests/index.js"), "Downstream JS file should have been rebuilt"); + } + else { + assert.equal(writtenFiles.size, 0, "Should not write any new files"); } - }); + assert.equal(fs.statSync("/src/tests/index.js").mtimeMs, time(), "Downstream JS file should have new timestamp"); + assert.isBelow(fs.statSync("/src/core/index.js").mtimeMs, time(), "Upstream JS file should not have been rebuilt"); + } }); - - const coreChanges: TscIncremental[] = [ - { - buildKind: BuildKind.IncrementalDtsChange, - modifyFs: fs => appendText(fs, "/src/core/index.ts", ` + }); + const coreChanges: TscIncremental[] = [ + { + buildKind: BuildKind.IncrementalDtsChange, + modifyFs: fs => appendText(fs, "/src/core/index.ts", ` export class someClass { }`), - }, - { - buildKind: BuildKind.IncrementalDtsUnchanged, - modifyFs: fs => appendText(fs, "/src/core/index.ts", ` + }, + { + buildKind: BuildKind.IncrementalDtsUnchanged, + modifyFs: fs => appendText(fs, "/src/core/index.ts", ` class someClass { }`), - } - ]; - - describe("lists files", () => { - verifyTscIncrementalEdits({ - scenario: "sample1", - subScenario: "listFiles", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tests", "--listFiles"], - incrementalScenarios: coreChanges - }); - verifyTscIncrementalEdits({ - scenario: "sample1", - subScenario: "listEmittedFiles", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tests", "--listEmittedFiles"], - incrementalScenarios: coreChanges - }); + } + ]; + describe("lists files", () => { + verifyTscIncrementalEdits({ + scenario: "sample1", + subScenario: "listFiles", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tests", "--listFiles"], + incrementalScenarios: coreChanges }); - - describe("emit output", () => { - verifyTscIncrementalEdits({ - subScenario: "sample", - fs: () => projFs, - scenario: "sample1", - commandLineArgs: ["--b", "/src/tests", "--verbose"], - baselineSourceMap: true, - baselineReadFileCalls: true, - incrementalScenarios: [ - ...coreChanges, - { - subScenario: "when logic config changes declaration dir", - buildKind: BuildKind.IncrementalDtsChange, - modifyFs: fs => replaceText(fs, "/src/logic/tsconfig.json", `"declaration": true,`, `"declaration": true, + verifyTscIncrementalEdits({ + scenario: "sample1", + subScenario: "listEmittedFiles", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tests", "--listEmittedFiles"], + incrementalScenarios: coreChanges + }); + }); + describe("emit output", () => { + verifyTscIncrementalEdits({ + subScenario: "sample", + fs: () => projFs, + scenario: "sample1", + commandLineArgs: ["--b", "/src/tests", "--verbose"], + baselineSourceMap: true, + baselineReadFileCalls: true, + incrementalScenarios: [ + ...coreChanges, + { + subScenario: "when logic config changes declaration dir", + buildKind: BuildKind.IncrementalDtsChange, + modifyFs: fs => replaceText(fs, "/src/logic/tsconfig.json", `"declaration": true,`, `"declaration": true, "declarationDir": "decls",`), - }, - noChangeRun, - ], - }); - - verifyTsc({ - scenario: "sample1", - subScenario: "when logic specifies tsBuildInfoFile", - fs: () => projFs, - modifyFs: fs => replaceText(fs, "/src/logic/tsconfig.json", `"composite": true,`, `"composite": true, + }, + noChangeRun, + ], + }); + verifyTsc({ + scenario: "sample1", + subScenario: "when logic specifies tsBuildInfoFile", + fs: () => projFs, + modifyFs: fs => replaceText(fs, "/src/logic/tsconfig.json", `"composite": true,`, `"composite": true, "tsBuildInfoFile": "ownFile.tsbuildinfo",`), - commandLineArgs: ["--b", "/src/tests", "--verbose"], - baselineSourceMap: true, - baselineReadFileCalls: true - }); - - verifyTscIncrementalEdits({ - subScenario: "when declaration option changes", - fs: () => projFs, - scenario: "sample1", - commandLineArgs: ["--b", "/src/core", "--verbose"], - modifyFs: fs => fs.writeFileSync("/src/core/tsconfig.json", `{ + commandLineArgs: ["--b", "/src/tests", "--verbose"], + baselineSourceMap: true, + baselineReadFileCalls: true + }); + verifyTscIncrementalEdits({ + subScenario: "when declaration option changes", + fs: () => projFs, + scenario: "sample1", + commandLineArgs: ["--b", "/src/core", "--verbose"], + modifyFs: fs => fs.writeFileSync("/src/core/tsconfig.json", `{ "compilerOptions": { "incremental": true, "skipDefaultLibCheck": true } }`), - incrementalScenarios: [{ + incrementalScenarios: [{ buildKind: BuildKind.IncrementalDtsChange, modifyFs: fs => replaceText(fs, "/src/core/tsconfig.json", `"incremental": true,`, `"incremental": true, "declaration": true,`), }], - }); - - verifyTscIncrementalEdits({ - subScenario: "when target option changes", - fs: () => projFs, - scenario: "sample1", - commandLineArgs: ["--b", "/src/core", "--verbose"], - modifyFs: fs => { - fs.writeFileSync("/lib/lib.esnext.full.d.ts", `/// + }); + verifyTscIncrementalEdits({ + subScenario: "when target option changes", + fs: () => projFs, + scenario: "sample1", + commandLineArgs: ["--b", "/src/core", "--verbose"], + modifyFs: fs => { + fs.writeFileSync("/lib/lib.esnext.full.d.ts", `/// /// `); - fs.writeFileSync("/lib/lib.esnext.d.ts", libContent); - fs.writeFileSync("/lib/lib.d.ts", `/// + fs.writeFileSync("/lib/lib.esnext.d.ts", libContent); + fs.writeFileSync("/lib/lib.d.ts", `/// /// `); - fs.writeFileSync("/src/core/tsconfig.json", `{ + fs.writeFileSync("/src/core/tsconfig.json", `{ "compilerOptions": { "incremental": true, "listFiles": true, @@ -455,36 +388,34 @@ class someClass { }`), "target": "esnext", } }`); - }, - incrementalScenarios: [{ + }, + incrementalScenarios: [{ buildKind: BuildKind.IncrementalDtsChange, modifyFs: fs => replaceText(fs, "/src/core/tsconfig.json", "esnext", "es5"), }], - }); - - verifyTscIncrementalEdits({ - subScenario: "when module option changes", - fs: () => projFs, - scenario: "sample1", - commandLineArgs: ["--b", "/src/core", "--verbose"], - modifyFs: fs => fs.writeFileSync("/src/core/tsconfig.json", `{ + }); + verifyTscIncrementalEdits({ + subScenario: "when module option changes", + fs: () => projFs, + scenario: "sample1", + commandLineArgs: ["--b", "/src/core", "--verbose"], + modifyFs: fs => fs.writeFileSync("/src/core/tsconfig.json", `{ "compilerOptions": { "incremental": true, "module": "commonjs" } }`), - incrementalScenarios: [{ + incrementalScenarios: [{ buildKind: BuildKind.IncrementalDtsChange, modifyFs: fs => replaceText(fs, "/src/core/tsconfig.json", `"module": "commonjs"`, `"module": "amd"`), }], - }); - - verifyTscIncrementalEdits({ - subScenario: "when esModuleInterop option changes", - fs: () => projFs, - scenario: "sample1", - commandLineArgs: ["--b", "/src/tests", "--verbose"], - modifyFs: fs => fs.writeFileSync("/src/tests/tsconfig.json", `{ + }); + verifyTscIncrementalEdits({ + subScenario: "when esModuleInterop option changes", + fs: () => projFs, + scenario: "sample1", + commandLineArgs: ["--b", "/src/tests", "--verbose"], + modifyFs: fs => fs.writeFileSync("/src/tests/tsconfig.json", `{ "references": [ { "path": "../core" }, { "path": "../logic" } @@ -498,11 +429,10 @@ class someClass { }`), "esModuleInterop": false } }`), - incrementalScenarios: [{ + incrementalScenarios: [{ buildKind: BuildKind.IncrementalDtsChange, modifyFs: fs => replaceText(fs, "/src/tests/tsconfig.json", `"esModuleInterop": false`, `"esModuleInterop": true`), }], - }); }); }); -} +}); diff --git a/src/testRunner/unittests/tsbuild/transitiveReferences.ts b/src/testRunner/unittests/tsbuild/transitiveReferences.ts index 735c13f5976fb..6e6493ebf0c00 100644 --- a/src/testRunner/unittests/tsbuild/transitiveReferences.ts +++ b/src/testRunner/unittests/tsbuild/transitiveReferences.ts @@ -1,47 +1,43 @@ -namespace ts { - describe("unittests:: tsbuild:: when project reference is referenced transitively", () => { - let projFs: vfs.FileSystem; - before(() => { - projFs = loadProjectFromDisk("tests/projects/transitiveReferences"); - }); - after(() => { - projFs = undefined!; // Release the contents - }); - - function modifyFsBTsToNonRelativeImport(fs: vfs.FileSystem, moduleResolution: "node" | "classic") { - fs.writeFileSync("/src/b.ts", `import {A} from 'a'; +import { FileSystem } from "../../vfs"; +import { loadProjectFromDisk, verifyTsc } from "../../ts"; +describe("unittests:: tsbuild:: when project reference is referenced transitively", () => { + let projFs: FileSystem; + before(() => { + projFs = loadProjectFromDisk("tests/projects/transitiveReferences"); + }); + after(() => { + projFs = undefined!; // Release the contents + }); + function modifyFsBTsToNonRelativeImport(fs: FileSystem, moduleResolution: "node" | "classic") { + fs.writeFileSync("/src/b.ts", `import {A} from 'a'; export const b = new A();`); - fs.writeFileSync("/src/tsconfig.b.json", JSON.stringify({ - compilerOptions: { - composite: true, - moduleResolution - }, - files: ["b.ts"], - references: [{ path: "tsconfig.a.json" }] - })); - } - - verifyTsc({ - scenario: "transitiveReferences", - subScenario: "builds correctly", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tsconfig.c.json", "--listFiles"], - }); - - verifyTsc({ - scenario: "transitiveReferences", - subScenario: "builds correctly when the referenced project uses different module resolution", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tsconfig.c.json", "--listFiles"], - modifyFs: fs => modifyFsBTsToNonRelativeImport(fs, "classic"), - }); - - verifyTsc({ - scenario: "transitiveReferences", - subScenario: "reports error about module not found with node resolution with external module name", - fs: () => projFs, - commandLineArgs: ["--b", "/src/tsconfig.c.json", "--listFiles"], - modifyFs: fs => modifyFsBTsToNonRelativeImport(fs, "node"), - }); + fs.writeFileSync("/src/tsconfig.b.json", JSON.stringify({ + compilerOptions: { + composite: true, + moduleResolution + }, + files: ["b.ts"], + references: [{ path: "tsconfig.a.json" }] + })); + } + verifyTsc({ + scenario: "transitiveReferences", + subScenario: "builds correctly", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tsconfig.c.json", "--listFiles"], + }); + verifyTsc({ + scenario: "transitiveReferences", + subScenario: "builds correctly when the referenced project uses different module resolution", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tsconfig.c.json", "--listFiles"], + modifyFs: fs => modifyFsBTsToNonRelativeImport(fs, "classic"), + }); + verifyTsc({ + scenario: "transitiveReferences", + subScenario: "reports error about module not found with node resolution with external module name", + fs: () => projFs, + commandLineArgs: ["--b", "/src/tsconfig.c.json", "--listFiles"], + modifyFs: fs => modifyFsBTsToNonRelativeImport(fs, "node"), }); -} +}); diff --git a/src/testRunner/unittests/tsbuild/watchEnvironment.ts b/src/testRunner/unittests/tsbuild/watchEnvironment.ts index f3c076b09ac0c..780ad81f52692 100644 --- a/src/testRunner/unittests/tsbuild/watchEnvironment.ts +++ b/src/testRunner/unittests/tsbuild/watchEnvironment.ts @@ -1,124 +1,111 @@ -namespace ts.tscWatch { - describe("unittests:: tsbuild:: watchEnvironment:: tsbuild:: watchMode:: with different watch environments", () => { - describe("when watchFile can create multiple watchers per file", () => { - verifyWatchFileOnMultipleProjects(/*singleWatchPerFile*/ false); - }); - - describe("when watchFile is single watcher per file", () => { - verifyWatchFileOnMultipleProjects( - /*singleWatchPerFile*/ true, - arrayToMap(["TSC_WATCHFILE"], identity, () => TestFSWithWatch.Tsc_WatchFile.SingleFileWatcherPerName) - ); - }); - - function verifyWatchFileOnMultipleProjects(singleWatchPerFile: boolean, environmentVariables?: Map) { - it("watchFile on same file multiple times because file is part of multiple projects", () => { - const project = `${TestFSWithWatch.tsbuildProjectsLocation}/myproject`; - let maxPkgs = 4; - const configPath = `${project}/tsconfig.json`; - const typing: File = { - path: `${project}/typings/xterm.d.ts`, - content: "export const typing = 10;" - }; - - const allPkgFiles = pkgs(pkgFiles); - const system = createWatchedSystem([libFile, typing, ...flatArray(allPkgFiles)], { currentDirectory: project, environmentVariables }); - writePkgReferences(); - const host = createSolutionBuilderWithWatchHost(system); - const solutionBuilder = createSolutionBuilderWithWatch(host, ["tsconfig.json"], { watch: true, verbose: true }); - solutionBuilder.build(); - checkOutputErrorsInitial(system, emptyArray, /*disableConsoleClears*/ undefined, [ - `Projects in this build: \r\n${ - concatenate( - pkgs(index => ` * pkg${index}/tsconfig.json`), - [" * tsconfig.json"] - ).join("\r\n")}\n\n`, +import { arrayToMap, identity, TestFSWithWatch, createSolutionBuilderWithWatchHost, createSolutionBuilderWithWatch, emptyArray, concatenate, last, flatMap } from "../../ts"; +import { File, createWatchedSystem, libFile, checkOutputErrorsInitial, checkWatchedFilesDetailed, checkOutputErrorsIncremental } from "../../ts.tscWatch"; +import * as ts from "../../ts"; +describe("unittests:: tsbuild:: watchEnvironment:: tsbuild:: watchMode:: with different watch environments", () => { + describe("when watchFile can create multiple watchers per file", () => { + verifyWatchFileOnMultipleProjects(/*singleWatchPerFile*/ false); + }); + describe("when watchFile is single watcher per file", () => { + verifyWatchFileOnMultipleProjects( + /*singleWatchPerFile*/ true, arrayToMap(["TSC_WATCHFILE"], identity, () => TestFSWithWatch.Tsc_WatchFile.SingleFileWatcherPerName)); + }); + function verifyWatchFileOnMultipleProjects(singleWatchPerFile: boolean, environmentVariables?: ts.Map) { + it("watchFile on same file multiple times because file is part of multiple projects", () => { + const project = `${TestFSWithWatch.tsbuildProjectsLocation}/myproject`; + let maxPkgs = 4; + const configPath = `${project}/tsconfig.json`; + const typing: File = { + path: `${project}/typings/xterm.d.ts`, + content: "export const typing = 10;" + }; + const allPkgFiles = pkgs(pkgFiles); + const system = createWatchedSystem([libFile, typing, ...flatArray(allPkgFiles)], { currentDirectory: project, environmentVariables }); + writePkgReferences(); + const host = createSolutionBuilderWithWatchHost(system); + const solutionBuilder = createSolutionBuilderWithWatch(host, ["tsconfig.json"], { watch: true, verbose: true }); + solutionBuilder.build(); + checkOutputErrorsInitial(system, emptyArray, /*disableConsoleClears*/ undefined, [ + `Projects in this build: \r\n${concatenate(pkgs(index => ` * pkg${index}/tsconfig.json`), [" * tsconfig.json"]).join("\r\n")}\n\n`, + ...flatArray(pkgs(index => [ + `Project 'pkg${index}/tsconfig.json' is out of date because output file 'pkg${index}/index.js' does not exist\n\n`, + `Building project '${project}/pkg${index}/tsconfig.json'...\n\n` + ])) + ]); + const watchFilesDetailed = arrayToMap(flatArray(allPkgFiles), f => f.path, () => 1); + watchFilesDetailed.set(configPath, 1); + watchFilesDetailed.set(typing.path, singleWatchPerFile ? 1 : maxPkgs); + checkWatchedFilesDetailed(system, watchFilesDetailed); + system.writeFile(typing.path, `${typing.content}export const typing1 = 10;`); + verifyInvoke(); + // Make change + maxPkgs--; + writePkgReferences(); + system.checkTimeoutQueueLengthAndRun(1); + checkOutputErrorsIncremental(system, emptyArray); + const lastFiles = last(allPkgFiles); + lastFiles.forEach(f => watchFilesDetailed.delete(f.path)); + watchFilesDetailed.set(typing.path, singleWatchPerFile ? 1 : maxPkgs); + checkWatchedFilesDetailed(system, watchFilesDetailed); + system.writeFile(typing.path, typing.content); + verifyInvoke(); + // Make change to remove all the watches + maxPkgs = 0; + writePkgReferences(); + system.checkTimeoutQueueLengthAndRun(1); + checkOutputErrorsIncremental(system, [ + `tsconfig.json(1,10): error TS18002: The 'files' list in config file '${configPath}' is empty.\n` + ]); + checkWatchedFilesDetailed(system, [configPath], 1); + system.writeFile(typing.path, `${typing.content}export const typing1 = 10;`); + system.checkTimeoutQueueLength(0); + function flatArray(arr: T[][]): readonly T[] { + return flatMap(arr, identity); + } + function pkgs(cb: (index: number) => T): T[] { + const result: T[] = []; + for (let index = 0; index < maxPkgs; index++) { + result.push(cb(index)); + } + return result; + } + function createPkgReference(index: number) { + return { path: `./pkg${index}` }; + } + function pkgFiles(index: number): File[] { + return [ + { + path: `${project}/pkg${index}/index.ts`, + content: `export const pkg${index} = ${index};` + }, + { + path: `${project}/pkg${index}/tsconfig.json`, + content: JSON.stringify({ + complerOptions: { composite: true }, + include: [ + "**/*.ts", + "../typings/xterm.d.ts" + ] + }) + } + ]; + } + function writePkgReferences() { + system.writeFile(configPath, JSON.stringify({ + files: [], + include: [], + references: pkgs(createPkgReference) + })); + } + function verifyInvoke() { + pkgs(() => system.checkTimeoutQueueLengthAndRun(1)); + checkOutputErrorsIncremental(system, emptyArray, /*disableConsoleClears*/ undefined, /*logsBeforeWatchDiagnostics*/ undefined, [ ...flatArray(pkgs(index => [ - `Project 'pkg${index}/tsconfig.json' is out of date because output file 'pkg${index}/index.js' does not exist\n\n`, - `Building project '${project}/pkg${index}/tsconfig.json'...\n\n` + `Project 'pkg${index}/tsconfig.json' is out of date because oldest output 'pkg${index}/index.js' is older than newest input 'typings/xterm.d.ts'\n\n`, + `Building project '${project}/pkg${index}/tsconfig.json'...\n\n`, + `Updating unchanged output timestamps of project '${project}/pkg${index}/tsconfig.json'...\n\n` ])) ]); - - const watchFilesDetailed = arrayToMap(flatArray(allPkgFiles), f => f.path, () => 1); - watchFilesDetailed.set(configPath, 1); - watchFilesDetailed.set(typing.path, singleWatchPerFile ? 1 : maxPkgs); - checkWatchedFilesDetailed(system, watchFilesDetailed); - system.writeFile(typing.path, `${typing.content}export const typing1 = 10;`); - verifyInvoke(); - - // Make change - maxPkgs--; - writePkgReferences(); - system.checkTimeoutQueueLengthAndRun(1); - checkOutputErrorsIncremental(system, emptyArray); - const lastFiles = last(allPkgFiles); - lastFiles.forEach(f => watchFilesDetailed.delete(f.path)); - watchFilesDetailed.set(typing.path, singleWatchPerFile ? 1 : maxPkgs); - checkWatchedFilesDetailed(system, watchFilesDetailed); - system.writeFile(typing.path, typing.content); - verifyInvoke(); - - // Make change to remove all the watches - maxPkgs = 0; - writePkgReferences(); - system.checkTimeoutQueueLengthAndRun(1); - checkOutputErrorsIncremental(system, [ - `tsconfig.json(1,10): error TS18002: The 'files' list in config file '${configPath}' is empty.\n` - ]); - checkWatchedFilesDetailed(system, [configPath], 1); - - system.writeFile(typing.path, `${typing.content}export const typing1 = 10;`); - system.checkTimeoutQueueLength(0); - - function flatArray(arr: T[][]): readonly T[] { - return flatMap(arr, identity); - } - function pkgs(cb: (index: number) => T): T[] { - const result: T[] = []; - for (let index = 0; index < maxPkgs; index++) { - result.push(cb(index)); - } - return result; - } - function createPkgReference(index: number) { - return { path: `./pkg${index}` }; - } - function pkgFiles(index: number): File[] { - return [ - { - path: `${project}/pkg${index}/index.ts`, - content: `export const pkg${index} = ${index};` - }, - { - path: `${project}/pkg${index}/tsconfig.json`, - content: JSON.stringify({ - complerOptions: { composite: true }, - include: [ - "**/*.ts", - "../typings/xterm.d.ts" - ] - }) - } - ]; - } - function writePkgReferences() { - system.writeFile(configPath, JSON.stringify({ - files: [], - include: [], - references: pkgs(createPkgReference) - })); - } - function verifyInvoke() { - pkgs(() => system.checkTimeoutQueueLengthAndRun(1)); - checkOutputErrorsIncremental(system, emptyArray, /*disableConsoleClears*/ undefined, /*logsBeforeWatchDiagnostics*/ undefined, [ - ...flatArray(pkgs(index => [ - `Project 'pkg${index}/tsconfig.json' is out of date because oldest output 'pkg${index}/index.js' is older than newest input 'typings/xterm.d.ts'\n\n`, - `Building project '${project}/pkg${index}/tsconfig.json'...\n\n`, - `Updating unchanged output timestamps of project '${project}/pkg${index}/tsconfig.json'...\n\n` - ])) - ]); - } - }); - } - }); -} + } + }); + } +}); diff --git a/src/testRunner/unittests/tsbuild/watchMode.ts b/src/testRunner/unittests/tsbuild/watchMode.ts index 0bdc20cff9776..1fb15e723777f 100644 --- a/src/testRunner/unittests/tsbuild/watchMode.ts +++ b/src/testRunner/unittests/tsbuild/watchMode.ts @@ -1,352 +1,305 @@ -namespace ts.tscWatch { - import projectsLocation = TestFSWithWatch.tsbuildProjectsLocation; - import getFilePathInProject = TestFSWithWatch.getTsBuildProjectFilePath; - import getFileFromProject = TestFSWithWatch.getTsBuildProjectFile; - type TsBuildWatchSystem = TestFSWithWatch.TestServerHostTrackingWrittenFiles; - - function createTsBuildWatchSystem(fileOrFolderList: readonly TestFSWithWatch.FileOrFolderOrSymLink[], params?: TestFSWithWatch.TestServerHostCreationParameters) { - return TestFSWithWatch.changeToHostTrackingWrittenFiles( - createWatchedSystem(fileOrFolderList, params) - ); +import { TestFSWithWatch, BuildOptions, createSolutionBuilderHost, emptyArray, createSolutionBuilderWithWatchHost, createSolutionBuilderWithWatch, projectSystem, SolutionBuilder, EmitAndSemanticDiagnosticsBuilderProgram, ResolvedConfigFilePath, ConfigFileProgramReloadLevel, arrayFrom, mapDefinedIterator, Path, libContent } from "../../ts"; +import { createWatchedSystem, WatchedSystem, File, libFile, verifyTscWatch, checkWatchedFiles, checkWatchedDirectories, checkOutputErrorsInitial, checkWatchedFilesDetailed, checkWatchedDirectoriesDetailed, createWatchOfConfigFile, Watch, checkArray, checkOutputErrorsIncremental, checkProgramActualFiles, replaceFileText, projectRoot } from "../../ts.tscWatch"; +import { createProjectService, TestProjectService, checkNumberOfProjects, getTypeRootsFromLocation } from "../../ts.projectSystem"; +import { dedent } from "../../Utils"; +import * as ts from "../../ts"; +import projectsLocation = ts.TestFSWithWatch.tsbuildProjectsLocation; +import getFilePathInProject = ts.TestFSWithWatch.getTsBuildProjectFilePath; +import getFileFromProject = ts.TestFSWithWatch.getTsBuildProjectFile; +type TsBuildWatchSystem = TestFSWithWatch.TestServerHostTrackingWrittenFiles; +function createTsBuildWatchSystem(fileOrFolderList: readonly TestFSWithWatch.FileOrFolderOrSymLink[], params?: TestFSWithWatch.TestServerHostCreationParameters) { + return TestFSWithWatch.changeToHostTrackingWrittenFiles(createWatchedSystem(fileOrFolderList, params)); +} +export function createSolutionBuilder(system: WatchedSystem, rootNames: readonly string[], defaultOptions?: BuildOptions) { + const host = createSolutionBuilderHost(system); + return ts.createSolutionBuilder(host, rootNames, defaultOptions || {}); +} +type OutputFileStamp = [string, Date | undefined, boolean]; +function transformOutputToOutputFileStamp(f: string, host: TsBuildWatchSystem): OutputFileStamp { + return [f, host.getModifiedTime(f), host.writtenFiles.has(host.toFullPath(f))] as OutputFileStamp; +} +describe("unittests:: tsbuild:: watchMode:: program updates", () => { + const scenario = "programUpdates"; + const project = "sample1"; + const enum SubProject { + core = "core", + logic = "logic", + tests = "tests", + ui = "ui" } - - export function createSolutionBuilder(system: WatchedSystem, rootNames: readonly string[], defaultOptions?: BuildOptions) { - const host = createSolutionBuilderHost(system); - return ts.createSolutionBuilder(host, rootNames, defaultOptions || {}); + type ReadonlyFile = Readonly; + /** [tsconfig, index] | [tsconfig, index, anotherModule, someDecl] */ + type SubProjectFiles = [ReadonlyFile, ReadonlyFile] | [ReadonlyFile, ReadonlyFile, ReadonlyFile, ReadonlyFile]; + function getProjectPath(project: string) { + return `${projectsLocation}/${project}`; } - - type OutputFileStamp = [string, Date | undefined, boolean]; - function transformOutputToOutputFileStamp(f: string, host: TsBuildWatchSystem): OutputFileStamp { - return [f, host.getModifiedTime(f), host.writtenFiles.has(host.toFullPath(f))] as OutputFileStamp; + function projectPath(subProject: SubProject) { + return getFilePathInProject(project, subProject); } - - describe("unittests:: tsbuild:: watchMode:: program updates", () => { - const scenario = "programUpdates"; - const project = "sample1"; - const enum SubProject { - core = "core", - logic = "logic", - tests = "tests", - ui = "ui" - } - type ReadonlyFile = Readonly; - /** [tsconfig, index] | [tsconfig, index, anotherModule, someDecl] */ - type SubProjectFiles = [ReadonlyFile, ReadonlyFile] | [ReadonlyFile, ReadonlyFile, ReadonlyFile, ReadonlyFile]; - function getProjectPath(project: string) { - return `${projectsLocation}/${project}`; - } - - function projectPath(subProject: SubProject) { - return getFilePathInProject(project, subProject); - } - - function projectFilePath(subProject: SubProject, baseFileName: string) { - return `${projectPath(subProject)}/${baseFileName.toLowerCase()}`; - } - - function projectFileName(subProject: SubProject, baseFileName: string) { - return `${projectPath(subProject)}/${baseFileName}`; - } - - function projectFile(subProject: SubProject, baseFileName: string): File { - return getFileFromProject(project, `${subProject}/${baseFileName}`); - } - - function subProjectFiles(subProject: SubProject, anotherModuleAndSomeDecl?: true): SubProjectFiles { - const tsconfig = projectFile(subProject, "tsconfig.json"); - const index = projectFile(subProject, "index.ts"); - if (!anotherModuleAndSomeDecl) { - return [tsconfig, index]; - } - const anotherModule = projectFile(SubProject.core, "anotherModule.ts"); - const someDecl = projectFile(SubProject.core, "some_decl.ts"); - return [tsconfig, index, anotherModule, someDecl]; - } - - function getOutputFileNames(subProject: SubProject, baseFileNameWithoutExtension: string) { - const file = projectFilePath(subProject, baseFileNameWithoutExtension); - return [`${file}.js`, `${file}.d.ts`]; - } - - function getOutputStamps(host: TsBuildWatchSystem, subProject: SubProject, baseFileNameWithoutExtension: string): OutputFileStamp[] { - return getOutputFileNames(subProject, baseFileNameWithoutExtension).map(f => transformOutputToOutputFileStamp(f, host)); + function projectFilePath(subProject: SubProject, baseFileName: string) { + return `${projectPath(subProject)}/${baseFileName.toLowerCase()}`; + } + function projectFileName(subProject: SubProject, baseFileName: string) { + return `${projectPath(subProject)}/${baseFileName}`; + } + function projectFile(subProject: SubProject, baseFileName: string): File { + return getFileFromProject(project, `${subProject}/${baseFileName}`); + } + function subProjectFiles(subProject: SubProject, anotherModuleAndSomeDecl?: true): SubProjectFiles { + const tsconfig = projectFile(subProject, "tsconfig.json"); + const index = projectFile(subProject, "index.ts"); + if (!anotherModuleAndSomeDecl) { + return [tsconfig, index]; } - - function getOutputFileStamps(host: TsBuildWatchSystem, additionalFiles?: readonly [SubProject, string][]): OutputFileStamp[] { - const result = [ - ...getOutputStamps(host, SubProject.core, "anotherModule"), - ...getOutputStamps(host, SubProject.core, "index"), - ...getOutputStamps(host, SubProject.logic, "index"), - ...getOutputStamps(host, SubProject.tests, "index"), - ]; - if (additionalFiles) { - additionalFiles.forEach(([subProject, baseFileNameWithoutExtension]) => result.push(...getOutputStamps(host, subProject, baseFileNameWithoutExtension))); - } - host.writtenFiles.clear(); - return result; + const anotherModule = projectFile(SubProject.core, "anotherModule.ts"); + const someDecl = projectFile(SubProject.core, "some_decl.ts"); + return [tsconfig, index, anotherModule, someDecl]; + } + function getOutputFileNames(subProject: SubProject, baseFileNameWithoutExtension: string) { + const file = projectFilePath(subProject, baseFileNameWithoutExtension); + return [`${file}.js`, `${file}.d.ts`]; + } + function getOutputStamps(host: TsBuildWatchSystem, subProject: SubProject, baseFileNameWithoutExtension: string): OutputFileStamp[] { + return getOutputFileNames(subProject, baseFileNameWithoutExtension).map(f => transformOutputToOutputFileStamp(f, host)); + } + function getOutputFileStamps(host: TsBuildWatchSystem, additionalFiles?: readonly [SubProject, string][]): OutputFileStamp[] { + const result = [ + ...getOutputStamps(host, SubProject.core, "anotherModule"), + ...getOutputStamps(host, SubProject.core, "index"), + ...getOutputStamps(host, SubProject.logic, "index"), + ...getOutputStamps(host, SubProject.tests, "index"), + ]; + if (additionalFiles) { + additionalFiles.forEach(([subProject, baseFileNameWithoutExtension]) => result.push(...getOutputStamps(host, subProject, baseFileNameWithoutExtension))); } - - function changeFile(sys: WatchedSystem, fileName: string, content: string, caption: string) { - sys.writeFile(fileName, content); - sys.checkTimeoutQueueLengthAndRun(1); // Builds core - return caption; + host.writtenFiles.clear(); + return result; + } + function changeFile(sys: WatchedSystem, fileName: string, content: string, caption: string) { + sys.writeFile(fileName, content); + sys.checkTimeoutQueueLengthAndRun(1); // Builds core + return caption; + } + function changeCore(sys: WatchedSystem, content: string, caption: string) { + return changeFile(sys, core[1].path, content, caption); + } + let core: SubProjectFiles; + let logic: SubProjectFiles; + let tests: SubProjectFiles; + let ui: SubProjectFiles; + let allFiles: readonly File[]; + let testProjectExpectedWatchedFiles: string[]; + let testProjectExpectedWatchedDirectoriesRecursive: string[]; + before(() => { + core = subProjectFiles(SubProject.core, /*anotherModuleAndSomeDecl*/ true); + logic = subProjectFiles(SubProject.logic); + tests = subProjectFiles(SubProject.tests); + ui = subProjectFiles(SubProject.ui); + allFiles = [libFile, ...core, ...logic, ...tests, ...ui]; + testProjectExpectedWatchedFiles = [core[0], core[1], core[2]!, ...logic, ...tests].map(f => f.path.toLowerCase()); + testProjectExpectedWatchedDirectoriesRecursive = [projectPath(SubProject.core), projectPath(SubProject.logic)]; + }); + after(() => { + core = undefined!; + logic = undefined!; + tests = undefined!; + ui = undefined!; + allFiles = undefined!; + testProjectExpectedWatchedFiles = undefined!; + testProjectExpectedWatchedDirectoriesRecursive = undefined!; + }); + verifyTscWatch({ + scenario, + subScenario: "creates solution in watch mode", + commandLineArgs: ["-b", "-w", `${project}/${SubProject.tests}`], + sys: () => createWatchedSystem(allFiles, { currentDirectory: projectsLocation }), + changes: emptyArray + }); + it("verify building references watches only those projects", () => { + const system = createTsBuildWatchSystem(allFiles, { currentDirectory: projectsLocation }); + const host = createSolutionBuilderWithWatchHost(system); + const solutionBuilder = createSolutionBuilderWithWatch(host, [`${project}/${SubProject.tests}`], { watch: true }); + solutionBuilder.buildReferences(`${project}/${SubProject.tests}`); + checkWatchedFiles(system, testProjectExpectedWatchedFiles.slice(0, testProjectExpectedWatchedFiles.length - tests.length)); + checkWatchedDirectories(system, emptyArray, /*recursive*/ false); + checkWatchedDirectories(system, testProjectExpectedWatchedDirectoriesRecursive, /*recursive*/ true); + checkOutputErrorsInitial(system, emptyArray); + const testOutput = getOutputStamps(system, SubProject.tests, "index"); + const outputFileStamps = getOutputFileStamps(system); + for (const stamp of outputFileStamps.slice(0, outputFileStamps.length - testOutput.length)) { + assert.isDefined(stamp[1], `${stamp[0]} expected to be present`); } - - function changeCore(sys: WatchedSystem, content: string, caption: string) { - return changeFile(sys, core[1].path, content, caption); + for (const stamp of testOutput) { + assert.isUndefined(stamp[1], `${stamp[0]} expected to be missing`); } - - let core: SubProjectFiles; - let logic: SubProjectFiles; - let tests: SubProjectFiles; - let ui: SubProjectFiles; - let allFiles: readonly File[]; - let testProjectExpectedWatchedFiles: string[]; - let testProjectExpectedWatchedDirectoriesRecursive: string[]; - - before(() => { - core = subProjectFiles(SubProject.core, /*anotherModuleAndSomeDecl*/ true); - logic = subProjectFiles(SubProject.logic); - tests = subProjectFiles(SubProject.tests); - ui = subProjectFiles(SubProject.ui); - allFiles = [libFile, ...core, ...logic, ...tests, ...ui]; - testProjectExpectedWatchedFiles = [core[0], core[1], core[2]!, ...logic, ...tests].map(f => f.path.toLowerCase()); - testProjectExpectedWatchedDirectoriesRecursive = [projectPath(SubProject.core), projectPath(SubProject.logic)]; - }); - - after(() => { - core = undefined!; - logic = undefined!; - tests = undefined!; - ui = undefined!; - allFiles = undefined!; - testProjectExpectedWatchedFiles = undefined!; - testProjectExpectedWatchedDirectoriesRecursive = undefined!; - }); - - verifyTscWatch({ - scenario, - subScenario: "creates solution in watch mode", - commandLineArgs: ["-b", "-w", `${project}/${SubProject.tests}`], - sys: () => createWatchedSystem(allFiles, { currentDirectory: projectsLocation }), - changes: emptyArray - }); - - it("verify building references watches only those projects", () => { - const system = createTsBuildWatchSystem(allFiles, { currentDirectory: projectsLocation }); - const host = createSolutionBuilderWithWatchHost(system); - const solutionBuilder = createSolutionBuilderWithWatch(host, [`${project}/${SubProject.tests}`], { watch: true }); - solutionBuilder.buildReferences(`${project}/${SubProject.tests}`); - - checkWatchedFiles(system, testProjectExpectedWatchedFiles.slice(0, testProjectExpectedWatchedFiles.length - tests.length)); - checkWatchedDirectories(system, emptyArray, /*recursive*/ false); - checkWatchedDirectories(system, testProjectExpectedWatchedDirectoriesRecursive, /*recursive*/ true); - - checkOutputErrorsInitial(system, emptyArray); - const testOutput = getOutputStamps(system, SubProject.tests, "index"); - const outputFileStamps = getOutputFileStamps(system); - for (const stamp of outputFileStamps.slice(0, outputFileStamps.length - testOutput.length)) { - assert.isDefined(stamp[1], `${stamp[0]} expected to be present`); + return system; + }); + describe("validates the changes and watched files", () => { + const newFileWithoutExtension = "newFile"; + const newFile: File = { + path: projectFilePath(SubProject.core, `${newFileWithoutExtension}.ts`), + content: `export const newFileConst = 30;` + }; + function verifyProjectChanges(subScenario: string, allFilesGetter: () => readonly File[]) { + function buildLogicOrUpdateTimeStamps(sys: WatchedSystem) { + sys.checkTimeoutQueueLengthAndRun(1); // Builds logic or updates timestamps + return "Build logic or update time stamps"; } - for (const stamp of testOutput) { - assert.isUndefined(stamp[1], `${stamp[0]} expected to be missing`); + function buildTests(sys: WatchedSystem) { + sys.checkTimeoutQueueLengthAndRun(1); // Build tests + sys.checkTimeoutQueueLength(0); + return "Build Tests"; } - return system; - }); - - describe("validates the changes and watched files", () => { - const newFileWithoutExtension = "newFile"; - const newFile: File = { - path: projectFilePath(SubProject.core, `${newFileWithoutExtension}.ts`), - content: `export const newFileConst = 30;` - }; - - function verifyProjectChanges(subScenario: string, allFilesGetter: () => readonly File[]) { - function buildLogicOrUpdateTimeStamps(sys: WatchedSystem) { - sys.checkTimeoutQueueLengthAndRun(1); // Builds logic or updates timestamps - return "Build logic or update time stamps"; - } - - function buildTests(sys: WatchedSystem) { - sys.checkTimeoutQueueLengthAndRun(1); // Build tests - sys.checkTimeoutQueueLength(0); - return "Build Tests"; - } - verifyTscWatch({ - scenario, - subScenario: `${subScenario}/change builds changes and reports found errors message`, - commandLineArgs: ["-b", "-w", `${project}/${SubProject.tests}`], - sys: () => createWatchedSystem( - allFilesGetter(), - { currentDirectory: projectsLocation } - ), - changes: [ - sys => changeCore(sys, `${core[1].content} + verifyTscWatch({ + scenario, + subScenario: `${subScenario}/change builds changes and reports found errors message`, + commandLineArgs: ["-b", "-w", `${project}/${SubProject.tests}`], + sys: () => createWatchedSystem(allFilesGetter(), { currentDirectory: projectsLocation }), + changes: [ + sys => changeCore(sys, `${core[1].content} export class someClass { }`, "Make change to core"), - buildLogicOrUpdateTimeStamps, - buildTests, - // Another change requeues and builds it - sys => changeCore(sys, core[1].content, "Revert core file"), - buildLogicOrUpdateTimeStamps, - buildTests, - sys => { - const change1 = `${core[1].content} + buildLogicOrUpdateTimeStamps, + buildTests, + // Another change requeues and builds it + sys => changeCore(sys, core[1].content, "Revert core file"), + buildLogicOrUpdateTimeStamps, + buildTests, + sys => { + const change1 = `${core[1].content} export class someClass { }`; - sys.writeFile(core[1].path, change1); - assert.equal(sys.writtenFiles.size, 1); - sys.writtenFiles.clear(); - sys.writeFile(core[1].path, `${change1} + sys.writeFile(core[1].path, change1); + assert.equal(sys.writtenFiles.size, 1); + sys.writtenFiles.clear(); + sys.writeFile(core[1].path, `${change1} export class someClass2 { }`); - sys.checkTimeoutQueueLengthAndRun(1); // Builds core - return "Make two changes"; - }, - buildLogicOrUpdateTimeStamps, - buildTests, - ] - }); - - verifyTscWatch({ - scenario, - subScenario: `${subScenario}/non local change does not start build of referencing projects`, - commandLineArgs: ["-b", "-w", `${project}/${SubProject.tests}`], - sys: () => createWatchedSystem( - allFilesGetter(), - { currentDirectory: projectsLocation } - ), - changes: [ - sys => changeCore(sys, `${core[1].content} + sys.checkTimeoutQueueLengthAndRun(1); // Builds core + return "Make two changes"; + }, + buildLogicOrUpdateTimeStamps, + buildTests, + ] + }); + verifyTscWatch({ + scenario, + subScenario: `${subScenario}/non local change does not start build of referencing projects`, + commandLineArgs: ["-b", "-w", `${project}/${SubProject.tests}`], + sys: () => createWatchedSystem(allFilesGetter(), { currentDirectory: projectsLocation }), + changes: [ + sys => changeCore(sys, `${core[1].content} function foo() { }`, "Make local change to core"), - buildLogicOrUpdateTimeStamps, - buildTests - ] - }); - - function changeNewFile(sys: WatchedSystem, newFileContent: string) { - return changeFile(sys, newFile.path, newFileContent, "Change to new File and build core"); - } - verifyTscWatch({ - scenario, - subScenario: `${subScenario}/builds when new file is added, and its subsequent updates`, - commandLineArgs: ["-b", "-w", `${project}/${SubProject.tests}`], - sys: () => createWatchedSystem( - allFilesGetter(), - { currentDirectory: projectsLocation } - ), - changes: [ - sys => changeNewFile(sys, newFile.content), - buildLogicOrUpdateTimeStamps, - buildTests, - sys => changeNewFile(sys, `${newFile.content} -export class someClass2 { }`), - buildLogicOrUpdateTimeStamps, - buildTests - ] - }); - } - - describe("with simple project reference graph", () => { - verifyProjectChanges( - "with simple project reference graph", - () => allFiles - ); + buildLogicOrUpdateTimeStamps, + buildTests + ] }); - - describe("with circular project reference", () => { - verifyProjectChanges( - "with circular project reference", - () => { - const [coreTsconfig, ...otherCoreFiles] = core; - const circularCoreConfig: File = { - path: coreTsconfig.path, - content: JSON.stringify({ - compilerOptions: { composite: true, declaration: true }, - references: [{ path: "../tests", circular: true }] - }) - }; - return [libFile, circularCoreConfig, ...otherCoreFiles, ...logic, ...tests]; - } - ); + function changeNewFile(sys: WatchedSystem, newFileContent: string) { + return changeFile(sys, newFile.path, newFileContent, "Change to new File and build core"); + } + verifyTscWatch({ + scenario, + subScenario: `${subScenario}/builds when new file is added, and its subsequent updates`, + commandLineArgs: ["-b", "-w", `${project}/${SubProject.tests}`], + sys: () => createWatchedSystem(allFilesGetter(), { currentDirectory: projectsLocation }), + changes: [ + sys => changeNewFile(sys, newFile.content), + buildLogicOrUpdateTimeStamps, + buildTests, + sys => changeNewFile(sys, `${newFile.content} +export class someClass2 { }`), + buildLogicOrUpdateTimeStamps, + buildTests + ] }); + } + describe("with simple project reference graph", () => { + verifyProjectChanges("with simple project reference graph", () => allFiles); }); - - verifyTscWatch({ - scenario, - subScenario: "watches config files that are not present", - commandLineArgs: ["-b", "-w", `${project}/${SubProject.tests}`], - sys: () => createWatchedSystem( - [libFile, ...core, logic[1], ...tests], - { currentDirectory: projectsLocation } - ), - changes: [ - sys => { - sys.writeFile(logic[0].path, logic[0].content); - sys.checkTimeoutQueueLengthAndRun(1); // Builds logic - return "Write logic tsconfig and build logic"; - }, - sys => { - sys.checkTimeoutQueueLengthAndRun(1); // Builds tests - sys.checkTimeoutQueueLength(0); - return "Build tests"; - } - ] - }); - - describe("when referenced using prepend, builds referencing project even for non local change", () => { - let coreIndex: File; - before(() => { - coreIndex = { - path: core[1].path, - content: `function foo() { return 10; }` + describe("with circular project reference", () => { + verifyProjectChanges("with circular project reference", () => { + const [coreTsconfig, ...otherCoreFiles] = core; + const circularCoreConfig: File = { + path: coreTsconfig.path, + content: JSON.stringify({ + compilerOptions: { composite: true, declaration: true }, + references: [{ path: "../tests", circular: true }] + }) }; + return [libFile, circularCoreConfig, ...otherCoreFiles, ...logic, ...tests]; }); - after(() => { - coreIndex = undefined!; - }); - function buildLogic(sys: WatchedSystem) { + }); + }); + verifyTscWatch({ + scenario, + subScenario: "watches config files that are not present", + commandLineArgs: ["-b", "-w", `${project}/${SubProject.tests}`], + sys: () => createWatchedSystem([libFile, ...core, logic[1], ...tests], { currentDirectory: projectsLocation }), + changes: [ + sys => { + sys.writeFile(logic[0].path, logic[0].content); sys.checkTimeoutQueueLengthAndRun(1); // Builds logic + return "Write logic tsconfig and build logic"; + }, + sys => { + sys.checkTimeoutQueueLengthAndRun(1); // Builds tests sys.checkTimeoutQueueLength(0); - return "Build logic"; + return "Build tests"; } - verifyTscWatch({ - scenario, - subScenario: "when referenced using prepend builds referencing project even for non local change", - commandLineArgs: ["-b", "-w", `${project}/${SubProject.logic}`], - sys: () => { - const coreTsConfig: File = { - path: core[0].path, - content: JSON.stringify({ - compilerOptions: { composite: true, declaration: true, outFile: "index.js" } - }) - }; - const logicTsConfig: File = { - path: logic[0].path, - content: JSON.stringify({ - compilerOptions: { composite: true, declaration: true, outFile: "index.js" }, - references: [{ path: "../core", prepend: true }] - }) - }; - const logicIndex: File = { - path: logic[1].path, - content: `function bar() { return foo() + 1 };` - }; - return createWatchedSystem([libFile, coreTsConfig, coreIndex, logicTsConfig, logicIndex], { currentDirectory: projectsLocation }); - }, - changes: [ - sys => changeCore(sys, `${coreIndex.content} + ] + }); + describe("when referenced using prepend, builds referencing project even for non local change", () => { + let coreIndex: File; + before(() => { + coreIndex = { + path: core[1].path, + content: `function foo() { return 10; }` + }; + }); + after(() => { + coreIndex = undefined!; + }); + function buildLogic(sys: WatchedSystem) { + sys.checkTimeoutQueueLengthAndRun(1); // Builds logic + sys.checkTimeoutQueueLength(0); + return "Build logic"; + } + verifyTscWatch({ + scenario, + subScenario: "when referenced using prepend builds referencing project even for non local change", + commandLineArgs: ["-b", "-w", `${project}/${SubProject.logic}`], + sys: () => { + const coreTsConfig: File = { + path: core[0].path, + content: JSON.stringify({ + compilerOptions: { composite: true, declaration: true, outFile: "index.js" } + }) + }; + const logicTsConfig: File = { + path: logic[0].path, + content: JSON.stringify({ + compilerOptions: { composite: true, declaration: true, outFile: "index.js" }, + references: [{ path: "../core", prepend: true }] + }) + }; + const logicIndex: File = { + path: logic[1].path, + content: `function bar() { return foo() + 1 };` + }; + return createWatchedSystem([libFile, coreTsConfig, coreIndex, logicTsConfig, logicIndex], { currentDirectory: projectsLocation }); + }, + changes: [ + sys => changeCore(sys, `${coreIndex.content} function myFunc() { return 10; }`, "Make non local change and build core"), - buildLogic, - sys => changeCore(sys, `${coreIndex.content} + buildLogic, + sys => changeCore(sys, `${coreIndex.content} function myFunc() { return 100; }`, "Make local change and build core"), - buildLogic, - ] - }); + buildLogic, + ] }); - - describe("when referenced project change introduces error in the down stream project and then fixes it", () => { - const subProjectLibrary = `${projectsLocation}/${project}/Library`; - const libraryTs: File = { - path: `${subProjectLibrary}/library.ts`, - content: ` + }); + describe("when referenced project change introduces error in the down stream project and then fixes it", () => { + const subProjectLibrary = `${projectsLocation}/${project}/Library`; + const libraryTs: File = { + path: `${subProjectLibrary}/library.ts`, + content: ` interface SomeObject { message: string; @@ -358,944 +311,805 @@ export function createSomeObject(): SomeObject message: "new Object" }; }` - }; - verifyTscWatch({ - scenario, - subScenario: "when referenced project change introduces error in the down stream project and then fixes it", - commandLineArgs: ["-b", "-w", "App"], - sys: () => { - const libraryTsconfig: File = { - path: `${subProjectLibrary}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { composite: true } }) - }; - const subProjectApp = `${projectsLocation}/${project}/App`; - const appTs: File = { - path: `${subProjectApp}/app.ts`, - content: `import { createSomeObject } from "../Library/library"; + }; + verifyTscWatch({ + scenario, + subScenario: "when referenced project change introduces error in the down stream project and then fixes it", + commandLineArgs: ["-b", "-w", "App"], + sys: () => { + const libraryTsconfig: File = { + path: `${subProjectLibrary}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { composite: true } }) + }; + const subProjectApp = `${projectsLocation}/${project}/App`; + const appTs: File = { + path: `${subProjectApp}/app.ts`, + content: `import { createSomeObject } from "../Library/library"; createSomeObject().message;` - }; - const appTsconfig: File = { - path: `${subProjectApp}/tsconfig.json`, - content: JSON.stringify({ references: [{ path: "../Library" }] }) - }; - - const files = [libFile, libraryTs, libraryTsconfig, appTs, appTsconfig]; - return createWatchedSystem(files, { currentDirectory: `${projectsLocation}/${project}` }); + }; + const appTsconfig: File = { + path: `${subProjectApp}/tsconfig.json`, + content: JSON.stringify({ references: [{ path: "../Library" }] }) + }; + const files = [libFile, libraryTs, libraryTsconfig, appTs, appTsconfig]; + return createWatchedSystem(files, { currentDirectory: `${projectsLocation}/${project}` }); + }, + changes: [ + sys => { + // Change message in library to message2 + sys.writeFile(libraryTs.path, libraryTs.content.replace(/message/g, "message2")); + sys.checkTimeoutQueueLengthAndRun(1); // Build library + sys.checkTimeoutQueueLengthAndRun(1); // Build App + return "Introduce error"; }, + sys => { + // Revert library changes + sys.writeFile(libraryTs.path, libraryTs.content); + sys.checkTimeoutQueueLengthAndRun(1); // Build library + sys.checkTimeoutQueueLengthAndRun(1); // Build App + return "Fix error"; + }, + ] + }); + }); + describe("reports errors in all projects on incremental compile", () => { + function verifyIncrementalErrors(subScenario: string, buildOptions: readonly string[]) { + verifyTscWatch({ + scenario, + subScenario: `reportErrors/${subScenario}`, + commandLineArgs: ["-b", "-w", `${project}/${SubProject.tests}`, ...buildOptions], + sys: () => createWatchedSystem(allFiles, { currentDirectory: projectsLocation }), changes: [ sys => { - // Change message in library to message2 - sys.writeFile(libraryTs.path, libraryTs.content.replace(/message/g, "message2")); - sys.checkTimeoutQueueLengthAndRun(1); // Build library - sys.checkTimeoutQueueLengthAndRun(1); // Build App - return "Introduce error"; + sys.writeFile(logic[1].path, `${logic[1].content} +let y: string = 10;`); + sys.checkTimeoutQueueLengthAndRun(1); // Builds logic + sys.checkTimeoutQueueLength(0); + return "change logic"; }, sys => { - // Revert library changes - sys.writeFile(libraryTs.path, libraryTs.content); - sys.checkTimeoutQueueLengthAndRun(1); // Build library - sys.checkTimeoutQueueLengthAndRun(1); // Build App - return "Fix error"; - }, + sys.writeFile(core[1].path, `${core[1].content} +let x: string = 10;`); + sys.checkTimeoutQueueLengthAndRun(1); // Builds core + sys.checkTimeoutQueueLength(0); + return "change core"; + } ] }); - - }); - - describe("reports errors in all projects on incremental compile", () => { - function verifyIncrementalErrors(subScenario: string, buildOptions: readonly string[]) { - verifyTscWatch({ - scenario, - subScenario: `reportErrors/${subScenario}`, - commandLineArgs: ["-b", "-w", `${project}/${SubProject.tests}`, ...buildOptions], - sys: () => createWatchedSystem(allFiles, { currentDirectory: projectsLocation }), - changes: [ - sys => { - sys.writeFile(logic[1].path, `${logic[1].content} -let y: string = 10;`); - - sys.checkTimeoutQueueLengthAndRun(1); // Builds logic - sys.checkTimeoutQueueLength(0); - return "change logic"; - }, - sys => { - sys.writeFile(core[1].path, `${core[1].content} -let x: string = 10;`); - - sys.checkTimeoutQueueLengthAndRun(1); // Builds core - sys.checkTimeoutQueueLength(0); - return "change core"; - } - ] - }); - } - verifyIncrementalErrors("when preserveWatchOutput is not used", emptyArray); - verifyIncrementalErrors("when preserveWatchOutput is passed on command line", ["--preserveWatchOutput"]); - - describe("when declaration emit errors are present", () => { - const solution = "solution"; - const subProject = "app"; - const subProjectLocation = `${projectsLocation}/${solution}/${subProject}`; - const fileWithError: File = { - path: `${subProjectLocation}/fileWithError.ts`, - content: `export var myClassWithError = class { + } + verifyIncrementalErrors("when preserveWatchOutput is not used", emptyArray); + verifyIncrementalErrors("when preserveWatchOutput is passed on command line", ["--preserveWatchOutput"]); + describe("when declaration emit errors are present", () => { + const solution = "solution"; + const subProject = "app"; + const subProjectLocation = `${projectsLocation}/${solution}/${subProject}`; + const fileWithError: File = { + path: `${subProjectLocation}/fileWithError.ts`, + content: `export var myClassWithError = class { tags() { } private p = 12 };` - }; - const fileWithFixedError: File = { - path: fileWithError.path, - content: fileWithError.content.replace("private p = 12", "") - }; - const fileWithoutError: File = { - path: `${subProjectLocation}/fileWithoutError.ts`, - content: `export class myClass { }` - }; - const tsconfig: File = { - path: `${subProjectLocation}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { composite: true } }) - }; - - function incrementalBuild(sys: WatchedSystem) { - sys.checkTimeoutQueueLengthAndRun(1); // Build the app - sys.checkTimeoutQueueLength(0); - } - - function fixError(sys: WatchedSystem) { - // Fix error - sys.writeFile(fileWithError.path, fileWithFixedError.content); - incrementalBuild(sys); - return "Fix error in fileWithError"; - } - - function changeFileWithoutError(sys: WatchedSystem) { - sys.writeFile(fileWithoutError.path, fileWithoutError.content.replace(/myClass/g, "myClass2")); + }; + const fileWithFixedError: File = { + path: fileWithError.path, + content: fileWithError.content.replace("private p = 12", "") + }; + const fileWithoutError: File = { + path: `${subProjectLocation}/fileWithoutError.ts`, + content: `export class myClass { }` + }; + const tsconfig: File = { + path: `${subProjectLocation}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { composite: true } }) + }; + function incrementalBuild(sys: WatchedSystem) { + sys.checkTimeoutQueueLengthAndRun(1); // Build the app + sys.checkTimeoutQueueLength(0); + } + function fixError(sys: WatchedSystem) { + // Fix error + sys.writeFile(fileWithError.path, fileWithFixedError.content); + incrementalBuild(sys); + return "Fix error in fileWithError"; + } + function changeFileWithoutError(sys: WatchedSystem) { + sys.writeFile(fileWithoutError.path, fileWithoutError.content.replace(/myClass/g, "myClass2")); + incrementalBuild(sys); + return "Change fileWithoutError"; + } + verifyTscWatch({ + scenario, + subScenario: "reportErrors/declarationEmitErrors/when fixing error files all files are emitted", + commandLineArgs: ["-b", "-w", subProject], + sys: () => createWatchedSystem([libFile, fileWithError, fileWithoutError, tsconfig], { currentDirectory: `${projectsLocation}/${solution}` }), + changes: [ + fixError + ] + }); + verifyTscWatch({ + scenario, + subScenario: "reportErrors/declarationEmitErrors/when file with no error changes", + commandLineArgs: ["-b", "-w", subProject], + sys: () => createWatchedSystem([libFile, fileWithError, fileWithoutError, tsconfig], { currentDirectory: `${projectsLocation}/${solution}` }), + changes: [ + changeFileWithoutError + ] + }); + describe("when reporting errors on introducing error", () => { + function introduceError(sys: WatchedSystem) { + sys.writeFile(fileWithError.path, fileWithError.content); incrementalBuild(sys); - return "Change fileWithoutError"; + return "Introduce error"; } - verifyTscWatch({ scenario, - subScenario: "reportErrors/declarationEmitErrors/when fixing error files all files are emitted", + subScenario: "reportErrors/declarationEmitErrors/introduceError/when fixing errors only changed file is emitted", commandLineArgs: ["-b", "-w", subProject], - sys: () => createWatchedSystem( - [libFile, fileWithError, fileWithoutError, tsconfig], - { currentDirectory: `${projectsLocation}/${solution}` } - ), + sys: () => createWatchedSystem([libFile, fileWithFixedError, fileWithoutError, tsconfig], { currentDirectory: `${projectsLocation}/${solution}` }), changes: [ + introduceError, fixError ] }); - verifyTscWatch({ scenario, - subScenario: "reportErrors/declarationEmitErrors/when file with no error changes", + subScenario: "reportErrors/declarationEmitErrors/introduceError/when file with no error changes", commandLineArgs: ["-b", "-w", subProject], - sys: () => createWatchedSystem( - [libFile, fileWithError, fileWithoutError, tsconfig], - { currentDirectory: `${projectsLocation}/${solution}` } - ), + sys: () => createWatchedSystem([libFile, fileWithFixedError, fileWithoutError, tsconfig], { currentDirectory: `${projectsLocation}/${solution}` }), changes: [ + introduceError, changeFileWithoutError ] }); - - describe("when reporting errors on introducing error", () => { - function introduceError(sys: WatchedSystem) { - sys.writeFile(fileWithError.path, fileWithError.content); - incrementalBuild(sys); - return "Introduce error"; - } - - verifyTscWatch({ - scenario, - subScenario: "reportErrors/declarationEmitErrors/introduceError/when fixing errors only changed file is emitted", - commandLineArgs: ["-b", "-w", subProject], - sys: () => createWatchedSystem( - [libFile, fileWithFixedError, fileWithoutError, tsconfig], - { currentDirectory: `${projectsLocation}/${solution}` } - ), - changes: [ - introduceError, - fixError - ] + }); + }); + }); + describe("tsc-watch and tsserver works with project references", () => { + describe("invoking when references are already built", () => { + function verifyWatchesOfProject(host: TsBuildWatchSystem, expectedWatchedFiles: readonly string[], expectedWatchedDirectoriesRecursive: readonly string[], expectedWatchedDirectories?: readonly string[]) { + checkWatchedFilesDetailed(host, expectedWatchedFiles, 1); + checkWatchedDirectoriesDetailed(host, expectedWatchedDirectories || emptyArray, 1, /*recursive*/ false); + checkWatchedDirectoriesDetailed(host, expectedWatchedDirectoriesRecursive, 1, /*recursive*/ true); + } + function createSolutionOfProject(allFiles: readonly File[], currentDirectory: string, solutionBuilderconfig: string, getOutputFileStamps: (host: TsBuildWatchSystem) => readonly OutputFileStamp[]) { + // Build the composite project + const host = createTsBuildWatchSystem(allFiles, { currentDirectory }); + const solutionBuilder = createSolutionBuilder(host, [solutionBuilderconfig], {}); + solutionBuilder.build(); + const outputFileStamps = getOutputFileStamps(host); + for (const stamp of outputFileStamps) { + assert.isDefined(stamp[1], `${stamp[0]} expected to be present`); + } + return { host, solutionBuilder }; + } + function createSolutionAndWatchModeOfProject(allFiles: readonly File[], currentDirectory: string, solutionBuilderconfig: string, watchConfig: string, getOutputFileStamps: (host: TsBuildWatchSystem) => readonly OutputFileStamp[]) { + // Build the composite project + const { host, solutionBuilder } = createSolutionOfProject(allFiles, currentDirectory, solutionBuilderconfig, getOutputFileStamps); + // Build in watch mode + const watch = createWatchOfConfigFile(watchConfig, host); + checkOutputErrorsInitial(host, emptyArray); + return { host, solutionBuilder, watch }; + } + function createSolutionAndServiceOfProject(allFiles: readonly File[], currentDirectory: string, solutionBuilderconfig: string, openFileName: string, getOutputFileStamps: (host: TsBuildWatchSystem) => readonly OutputFileStamp[]) { + // Build the composite project + const { host, solutionBuilder } = createSolutionOfProject(allFiles, currentDirectory, solutionBuilderconfig, getOutputFileStamps); + // service + const service = createProjectService(host); + service.openClientFile(openFileName); + return { host, solutionBuilder, service }; + } + function checkProjectActualFiles(service: TestProjectService, configFile: string, expectedFiles: readonly string[]) { + checkNumberOfProjects(service, { configuredProjects: 1 }); + projectSystem.checkProjectActualFiles((service.configuredProjects.get(configFile.toLowerCase())!), expectedFiles); + } + function verifyDependencies(watch: Watch, filePath: string, expected: readonly string[]) { + checkArray(`${filePath} dependencies`, watch.getBuilderProgram().getAllDependencies(watch().getSourceFile(filePath)!), expected); + } + describe("on sample project", () => { + const coreIndexDts = projectFileName(SubProject.core, "index.d.ts"); + const coreAnotherModuleDts = projectFileName(SubProject.core, "anotherModule.d.ts"); + const logicIndexDts = projectFileName(SubProject.logic, "index.d.ts"); + const expectedWatchedDirectoriesRecursive = getTypeRootsFromLocation(projectPath(SubProject.tests)); + const expectedProjectFiles = () => [libFile, ...tests, ...logic.slice(1), ...core.slice(1, core.length - 1)].map(f => f.path); + const expectedProgramFiles = () => [tests[1].path, libFile.path, coreIndexDts, coreAnotherModuleDts, logicIndexDts]; + function createSolutionAndWatchMode() { + return createSolutionAndWatchModeOfProject(allFiles, projectsLocation, `${project}/${SubProject.tests}`, tests[0].path, getOutputFileStamps); + } + function createSolutionAndService() { + return createSolutionAndServiceOfProject(allFiles, projectsLocation, `${project}/${SubProject.tests}`, tests[1].path, getOutputFileStamps); + } + function verifyWatches(host: TsBuildWatchSystem, withTsserver?: boolean) { + verifyWatchesOfProject(host, withTsserver ? + [...core.slice(0, core.length - 1), ...logic, tests[0], libFile].map(f => f.path.toLowerCase()) : + [core[0], logic[0], ...tests, libFile].map(f => f.path).concat([coreIndexDts, coreAnotherModuleDts, logicIndexDts].map(f => f.toLowerCase())), expectedWatchedDirectoriesRecursive); + } + function verifyScenario(edit: (host: TsBuildWatchSystem, solutionBuilder: SolutionBuilder) => void, expectedProgramFilesAfterEdit: () => readonly string[], expectedProjectFilesAfterEdit: () => readonly string[]) { + it("with tsc-watch", () => { + const { host, solutionBuilder, watch } = createSolutionAndWatchMode(); + edit(host, solutionBuilder); + host.checkTimeoutQueueLengthAndRun(1); + checkOutputErrorsIncremental(host, emptyArray); + checkProgramActualFiles(watch(), expectedProgramFilesAfterEdit()); }); - - verifyTscWatch({ - scenario, - subScenario: "reportErrors/declarationEmitErrors/introduceError/when file with no error changes", - commandLineArgs: ["-b", "-w", subProject], - sys: () => createWatchedSystem( - [libFile, fileWithFixedError, fileWithoutError, tsconfig], - { currentDirectory: `${projectsLocation}/${solution}` } - ), - changes: [ - introduceError, - changeFileWithoutError - ] + it("with tsserver", () => { + const { host, solutionBuilder, service } = createSolutionAndService(); + edit(host, solutionBuilder); + host.checkTimeoutQueueLengthAndRun(2); + checkProjectActualFiles(service, tests[0].path, expectedProjectFilesAfterEdit()); + }); + } + describe("verifies dependencies and watches", () => { + it("with tsc-watch", () => { + const { host, watch } = createSolutionAndWatchMode(); + verifyWatches(host); + verifyDependencies(watch, coreIndexDts, [coreIndexDts]); + verifyDependencies(watch, coreAnotherModuleDts, [coreAnotherModuleDts]); + verifyDependencies(watch, logicIndexDts, [logicIndexDts, coreAnotherModuleDts]); + verifyDependencies(watch, tests[1].path, expectedProgramFiles().filter(f => f !== libFile.path)); + }); + it("with tsserver", () => { + const { host } = createSolutionAndService(); + verifyWatches(host, /*withTsserver*/ true); }); }); + describe("local edit in ts file, result in watch compilation because logic.d.ts is written", () => { + verifyScenario((host, solutionBuilder) => { + host.writeFile(logic[1].path, `${logic[1].content} +function foo() { +}`); + solutionBuilder.invalidateProject((logic[0].path.toLowerCase() as ResolvedConfigFilePath)); + solutionBuilder.buildNextInvalidatedProject(); + // not ideal, but currently because of d.ts but no new file is written + // There will be timeout queued even though file contents are same + }, expectedProgramFiles, expectedProjectFiles); + }); + describe("non local edit in ts file, rebuilds in watch compilation", () => { + verifyScenario((host, solutionBuilder) => { + host.writeFile(logic[1].path, `${logic[1].content} +export function gfoo() { +}`); + solutionBuilder.invalidateProject((logic[0].path.toLowerCase() as ResolvedConfigFilePath)); + solutionBuilder.buildNextInvalidatedProject(); + }, expectedProgramFiles, expectedProjectFiles); + }); + describe("change in project reference config file builds correctly", () => { + verifyScenario((host, solutionBuilder) => { + host.writeFile(logic[0].path, JSON.stringify({ + compilerOptions: { composite: true, declaration: true, declarationDir: "decls" }, + references: [{ path: "../core" }] + })); + solutionBuilder.invalidateProject((logic[0].path.toLowerCase() as ResolvedConfigFilePath), ConfigFileProgramReloadLevel.Full); + solutionBuilder.buildNextInvalidatedProject(); + }, () => [tests[1].path, libFile.path, coreIndexDts, coreAnotherModuleDts, projectFilePath(SubProject.logic, "decls/index.d.ts")], expectedProjectFiles); + }); }); - }); - - describe("tsc-watch and tsserver works with project references", () => { - describe("invoking when references are already built", () => { - function verifyWatchesOfProject(host: TsBuildWatchSystem, expectedWatchedFiles: readonly string[], expectedWatchedDirectoriesRecursive: readonly string[], expectedWatchedDirectories?: readonly string[]) { - checkWatchedFilesDetailed(host, expectedWatchedFiles, 1); - checkWatchedDirectoriesDetailed(host, expectedWatchedDirectories || emptyArray, 1, /*recursive*/ false); - checkWatchedDirectoriesDetailed(host, expectedWatchedDirectoriesRecursive, 1, /*recursive*/ true); + describe("on transitive references", () => { + const project = "transitiveReferences"; + const aTsFile = getFileFromProject(project, "a.ts"); + const bTsFile = getFileFromProject(project, "b.ts"); + const cTsFile = getFileFromProject(project, "c.ts"); + const aTsconfigFile = getFileFromProject(project, "tsconfig.a.json"); + const bTsconfigFile = getFileFromProject(project, "tsconfig.b.json"); + const cTsconfigFile = getFileFromProject(project, "tsconfig.c.json"); + const refs = getFileFromProject(project, "refs/a.d.ts"); + function getRootFile(multiFolder: boolean, fileFromDisk: File, multiFolderPath: string): File { + return multiFolder ? { + path: getFilePathInProject(project, multiFolderPath), + content: fileFromDisk.content + // Replace the relative imports + .replace("./", "../") + } : fileFromDisk; } - - function createSolutionOfProject(allFiles: readonly File[], - currentDirectory: string, - solutionBuilderconfig: string, - getOutputFileStamps: (host: TsBuildWatchSystem) => readonly OutputFileStamp[]) { - // Build the composite project - const host = createTsBuildWatchSystem(allFiles, { currentDirectory }); - const solutionBuilder = createSolutionBuilder(host, [solutionBuilderconfig], {}); - solutionBuilder.build(); - const outputFileStamps = getOutputFileStamps(host); - for (const stamp of outputFileStamps) { - assert.isDefined(stamp[1], `${stamp[0]} expected to be present`); - } - return { host, solutionBuilder }; + function dtsFile(extensionLessFile: string) { + return getFilePathInProject(project, `${extensionLessFile}.d.ts`); } - - function createSolutionAndWatchModeOfProject( - allFiles: readonly File[], - currentDirectory: string, - solutionBuilderconfig: string, - watchConfig: string, - getOutputFileStamps: (host: TsBuildWatchSystem) => readonly OutputFileStamp[]) { - // Build the composite project - const { host, solutionBuilder } = createSolutionOfProject(allFiles, currentDirectory, solutionBuilderconfig, getOutputFileStamps); - - // Build in watch mode - const watch = createWatchOfConfigFile(watchConfig, host); - checkOutputErrorsInitial(host, emptyArray); - - return { host, solutionBuilder, watch }; + function jsFile(extensionLessFile: string) { + return getFilePathInProject(project, `${extensionLessFile}.js`); } - - function createSolutionAndServiceOfProject(allFiles: readonly File[], - currentDirectory: string, - solutionBuilderconfig: string, - openFileName: string, - getOutputFileStamps: (host: TsBuildWatchSystem) => readonly OutputFileStamp[]) { - // Build the composite project - const { host, solutionBuilder } = createSolutionOfProject(allFiles, currentDirectory, solutionBuilderconfig, getOutputFileStamps); - - // service - const service = projectSystem.createProjectService(host); - service.openClientFile(openFileName); - - return { host, solutionBuilder, service }; - } - - function checkProjectActualFiles(service: projectSystem.TestProjectService, configFile: string, expectedFiles: readonly string[]) { - projectSystem.checkNumberOfProjects(service, { configuredProjects: 1 }); - projectSystem.checkProjectActualFiles(service.configuredProjects.get(configFile.toLowerCase())!, expectedFiles); + function verifyWatchState(host: TsBuildWatchSystem, watch: Watch, expectedProgramFiles: readonly string[], expectedWatchedFiles: readonly string[], expectedWatchedDirectoriesRecursive: readonly string[], dependencies: readonly [string, readonly string[]][], expectedWatchedDirectories?: readonly string[]) { + checkProgramActualFiles(watch(), expectedProgramFiles); + verifyWatchesOfProject(host, expectedWatchedFiles, expectedWatchedDirectoriesRecursive, expectedWatchedDirectories); + for (const [file, deps] of dependencies) { + verifyDependencies(watch, file, deps); + } } - - function verifyDependencies(watch: Watch, filePath: string, expected: readonly string[]) { - checkArray(`${filePath} dependencies`, watch.getBuilderProgram().getAllDependencies(watch().getSourceFile(filePath)!), expected); + function getTsConfigFile(multiFolder: boolean, fileFromDisk: File, folder: string): File { + if (!multiFolder) + return fileFromDisk; + return { + path: getFilePathInProject(project, `${folder}/tsconfig.json`), + content: fileFromDisk.content + // Replace files array + .replace(`${folder}.ts`, "index.ts") + // Replace path mappings + .replace("./*", "../*") + .replace("./refs", "../refs") + // Replace references + .replace("tsconfig.a.json", "../a") + .replace("tsconfig.b.json", "../b") + }; } - - describe("on sample project", () => { - const coreIndexDts = projectFileName(SubProject.core, "index.d.ts"); - const coreAnotherModuleDts = projectFileName(SubProject.core, "anotherModule.d.ts"); - const logicIndexDts = projectFileName(SubProject.logic, "index.d.ts"); - const expectedWatchedDirectoriesRecursive = projectSystem.getTypeRootsFromLocation(projectPath(SubProject.tests)); - const expectedProjectFiles = () => [libFile, ...tests, ...logic.slice(1), ...core.slice(1, core.length - 1)].map(f => f.path); - const expectedProgramFiles = () => [tests[1].path, libFile.path, coreIndexDts, coreAnotherModuleDts, logicIndexDts]; - + // function writeFile(file: File) { + // Harness.IO.writeFile(file.path.replace(projectsLocation, "c:/temp"), file.content); + // } + function verifyTransitiveReferences(multiFolder: boolean) { + const aTs = getRootFile(multiFolder, aTsFile, "a/index.ts"); + const bTs = getRootFile(multiFolder, bTsFile, "b/index.ts"); + const cTs = getRootFile(multiFolder, cTsFile, "c/index.ts"); + const configToBuild = multiFolder ? "c/tsconfig.json" : "tsconfig.c.json"; + const aTsconfig = getTsConfigFile(multiFolder, aTsconfigFile, "a"); + const bTsconfig = getTsConfigFile(multiFolder, bTsconfigFile, "b"); + const cTsconfig = getTsConfigFile(multiFolder, cTsconfigFile, "c"); + // if (multiFolder) { + // writeFile(aTs); + // writeFile(bTs); + // writeFile(cTs); + // writeFile(aTsconfig); + // writeFile(bTsconfig); + // writeFile(cTsconfig); + // } + const allFiles = [libFile, aTs, bTs, cTs, aTsconfig, bTsconfig, cTsconfig, refs]; + const aDts = dtsFile(multiFolder ? "a/index" : "a"), bDts = dtsFile(multiFolder ? "b/index" : "b"); + const expectedFiles = [jsFile(multiFolder ? "a/index" : "a"), aDts, jsFile(multiFolder ? "b/index" : "b"), bDts, jsFile(multiFolder ? "c/index" : "c")]; + const expectedProgramFiles = [cTs.path, libFile.path, aDts, refs.path, bDts]; + const expectedProjectFiles = [cTs.path, libFile.path, aTs.path, refs.path, bTs.path]; + const expectedWatchedFiles = expectedProgramFiles.concat(cTsconfig.path, bTsconfig.path, aTsconfig.path).map(s => s.toLowerCase()); + const expectedProjectWatchedFiles = expectedProjectFiles.concat(cTsconfig.path, bTsconfig.path, aTsconfig.path).map(s => s.toLowerCase()); + const expectedWatchedDirectories = multiFolder ? [ + getProjectPath(project).toLowerCase() // watches for directories created for resolution of b + ] : emptyArray; + const nrefsPath = multiFolder ? ["../nrefs/*"] : ["./nrefs/*"]; + const expectedWatchedDirectoriesRecursive = [ + ...(multiFolder ? [ + getFilePathInProject(project, "a"), + getFilePathInProject(project, "b"), + ] : []), + getFilePathInProject(project, "refs"), + ...getTypeRootsFromLocation(multiFolder ? getFilePathInProject(project, "c") : getProjectPath(project)) + ].map(s => s.toLowerCase()); + const defaultDependencies: readonly [string, readonly string[]][] = [ + [aDts, [aDts]], + [bDts, [bDts, aDts]], + [refs.path, [refs.path]], + [cTs.path, [cTs.path, refs.path, bDts, aDts]] + ]; function createSolutionAndWatchMode() { - return createSolutionAndWatchModeOfProject(allFiles, projectsLocation, `${project}/${SubProject.tests}`, tests[0].path, getOutputFileStamps); + return createSolutionAndWatchModeOfProject(allFiles, getProjectPath(project), configToBuild, configToBuild, getOutputFileStamps); } - function createSolutionAndService() { - return createSolutionAndServiceOfProject(allFiles, projectsLocation, `${project}/${SubProject.tests}`, tests[1].path, getOutputFileStamps); + return createSolutionAndServiceOfProject(allFiles, getProjectPath(project), configToBuild, cTs.path, getOutputFileStamps); } - - function verifyWatches(host: TsBuildWatchSystem, withTsserver?: boolean) { - verifyWatchesOfProject( - host, - withTsserver ? - [...core.slice(0, core.length - 1), ...logic, tests[0], libFile].map(f => f.path.toLowerCase()) : - [core[0], logic[0], ...tests, libFile].map(f => f.path).concat([coreIndexDts, coreAnotherModuleDts, logicIndexDts].map(f => f.toLowerCase())), - expectedWatchedDirectoriesRecursive - ); + function getOutputFileStamps(host: TsBuildWatchSystem) { + return expectedFiles.map(file => transformOutputToOutputFileStamp(file, host)); } - - function verifyScenario( - edit: (host: TsBuildWatchSystem, solutionBuilder: SolutionBuilder) => void, - expectedProgramFilesAfterEdit: () => readonly string[], - expectedProjectFilesAfterEdit: () => readonly string[] - ) { + function verifyProgram(host: TsBuildWatchSystem, watch: Watch) { + verifyWatchState(host, watch, expectedProgramFiles, expectedWatchedFiles, expectedWatchedDirectoriesRecursive, defaultDependencies, expectedWatchedDirectories); + } + function verifyProject(host: TsBuildWatchSystem, service: TestProjectService, orphanInfos?: readonly string[]) { + verifyServerState({ host, service, expectedProjectFiles, expectedProjectWatchedFiles, expectedWatchedDirectoriesRecursive, orphanInfos }); + } + interface VerifyServerState { + host: TsBuildWatchSystem; + service: TestProjectService; + expectedProjectFiles: readonly string[]; + expectedProjectWatchedFiles: readonly string[]; + expectedWatchedDirectoriesRecursive: readonly string[]; + orphanInfos?: readonly string[]; + } + function verifyServerState({ host, service, expectedProjectFiles, expectedProjectWatchedFiles, expectedWatchedDirectoriesRecursive, orphanInfos }: VerifyServerState) { + checkProjectActualFiles(service, cTsconfig.path, expectedProjectFiles.concat(cTsconfig.path)); + const watchedFiles = expectedProjectWatchedFiles.filter(f => f !== cTs.path.toLowerCase()); + const actualOrphan = arrayFrom(mapDefinedIterator(service.filenameToScriptInfo.values(), v => v.containingProjects.length === 0 ? v.fileName : undefined)); + assert.equal(actualOrphan.length, orphanInfos ? orphanInfos.length : 0, `Orphans found: ${JSON.stringify(actualOrphan, /*replacer*/ undefined, " ")}`); + if (orphanInfos && orphanInfos.length) { + for (const orphan of orphanInfos) { + const info = service.getScriptInfoForPath((orphan as Path)); + assert.isDefined(info, `${orphan} expected to be present. Actual: ${JSON.stringify(actualOrphan, /*replacer*/ undefined, " ")}`); + assert.equal(info!.containingProjects.length, 0); + watchedFiles.push(orphan); + } + } + verifyWatchesOfProject(host, watchedFiles, expectedWatchedDirectoriesRecursive, expectedWatchedDirectories); + } + interface VerifyScenario { + edit: (host: TsBuildWatchSystem, solutionBuilder: SolutionBuilder) => void; + expectedEditErrors: readonly string[]; + expectedProgramFiles: readonly string[]; + expectedProjectFiles: readonly string[]; + expectedWatchedFiles: readonly string[]; + expectedProjectWatchedFiles: readonly string[]; + expectedWatchedDirectoriesRecursive: readonly string[]; + dependencies: readonly [string, readonly string[]][]; + revert?: (host: TsBuildWatchSystem) => void; + orphanInfosAfterEdit?: readonly string[]; + orphanInfosAfterRevert?: readonly string[]; + } + function verifyScenario({ edit, expectedEditErrors, expectedProgramFiles, expectedProjectFiles, expectedWatchedFiles, expectedProjectWatchedFiles, expectedWatchedDirectoriesRecursive, dependencies, revert, orphanInfosAfterEdit, orphanInfosAfterRevert }: VerifyScenario) { it("with tsc-watch", () => { const { host, solutionBuilder, watch } = createSolutionAndWatchMode(); - edit(host, solutionBuilder); - host.checkTimeoutQueueLengthAndRun(1); - checkOutputErrorsIncremental(host, emptyArray); - checkProgramActualFiles(watch(), expectedProgramFilesAfterEdit()); - + checkOutputErrorsIncremental(host, expectedEditErrors); + verifyWatchState(host, watch, expectedProgramFiles, expectedWatchedFiles, expectedWatchedDirectoriesRecursive, dependencies, expectedWatchedDirectories); + if (revert) { + revert(host); + host.checkTimeoutQueueLengthAndRun(1); + checkOutputErrorsIncremental(host, emptyArray); + verifyProgram(host, watch); + } }); - + if (!multiFolder) + return; // With side by side file open is in inferred project without any settings it("with tsserver", () => { const { host, solutionBuilder, service } = createSolutionAndService(); - edit(host, solutionBuilder); - host.checkTimeoutQueueLengthAndRun(2); - checkProjectActualFiles(service, tests[0].path, expectedProjectFilesAfterEdit()); + verifyServerState({ host, service, expectedProjectFiles, expectedProjectWatchedFiles, expectedWatchedDirectoriesRecursive, orphanInfos: orphanInfosAfterEdit }); + if (revert) { + revert(host); + host.checkTimeoutQueueLengthAndRun(2); + verifyProject(host, service, orphanInfosAfterRevert); + } }); } - describe("verifies dependencies and watches", () => { + // Initial build it("with tsc-watch", () => { const { host, watch } = createSolutionAndWatchMode(); - verifyWatches(host); - verifyDependencies(watch, coreIndexDts, [coreIndexDts]); - verifyDependencies(watch, coreAnotherModuleDts, [coreAnotherModuleDts]); - verifyDependencies(watch, logicIndexDts, [logicIndexDts, coreAnotherModuleDts]); - verifyDependencies(watch, tests[1].path, expectedProgramFiles().filter(f => f !== libFile.path)); + verifyProgram(host, watch); }); - + if (!multiFolder) + return; it("with tsserver", () => { - const { host } = createSolutionAndService(); - verifyWatches(host, /*withTsserver*/ true); + const { host, service } = createSolutionAndService(); + verifyProject(host, service); }); }); - - describe("local edit in ts file, result in watch compilation because logic.d.ts is written", () => { - verifyScenario((host, solutionBuilder) => { - host.writeFile(logic[1].path, `${logic[1].content} -function foo() { -}`); - solutionBuilder.invalidateProject(logic[0].path.toLowerCase() as ResolvedConfigFilePath); - solutionBuilder.buildNextInvalidatedProject(); - - // not ideal, but currently because of d.ts but no new file is written - // There will be timeout queued even though file contents are same - }, expectedProgramFiles, expectedProjectFiles); + describe("non local edit updates the program and watch correctly", () => { + verifyScenario({ + edit: (host, solutionBuilder) => { + // edit + host.writeFile(bTs.path, `${bTs.content}\nexport function gfoo() {\n}`); + solutionBuilder.invalidateProject((bTsconfig.path.toLowerCase() as ResolvedConfigFilePath)); + solutionBuilder.buildNextInvalidatedProject(); + }, + expectedEditErrors: emptyArray, + expectedProgramFiles, + expectedProjectFiles, + expectedWatchedFiles, + expectedProjectWatchedFiles, + expectedWatchedDirectoriesRecursive, + dependencies: defaultDependencies + }); }); - - describe("non local edit in ts file, rebuilds in watch compilation", () => { - verifyScenario((host, solutionBuilder) => { - host.writeFile(logic[1].path, `${logic[1].content} -export function gfoo() { -}`); - solutionBuilder.invalidateProject(logic[0].path.toLowerCase() as ResolvedConfigFilePath); - solutionBuilder.buildNextInvalidatedProject(); - }, expectedProgramFiles, expectedProjectFiles); + describe("edit on config file", () => { + const nrefReplacer = (f: string) => f.replace("refs", "nrefs"); + const nrefs: File = { + path: getFilePathInProject(project, "nrefs/a.d.ts"), + content: refs.content + }; + verifyScenario({ + edit: host => { + const cTsConfigJson = JSON.parse(cTsconfig.content); + host.ensureFileOrFolder(nrefs); + cTsConfigJson.compilerOptions.paths = { "@ref/*": nrefsPath }; + host.writeFile(cTsconfig.path, JSON.stringify(cTsConfigJson)); + }, + expectedEditErrors: emptyArray, + expectedProgramFiles: expectedProgramFiles.map(nrefReplacer), + expectedProjectFiles: expectedProjectFiles.map(nrefReplacer), + expectedWatchedFiles: expectedWatchedFiles.map(nrefReplacer), + expectedProjectWatchedFiles: expectedProjectWatchedFiles.map(nrefReplacer), + expectedWatchedDirectoriesRecursive: expectedWatchedDirectoriesRecursive.map(nrefReplacer), + dependencies: [ + [aDts, [aDts]], + [bDts, [bDts, aDts]], + [nrefs.path, [nrefs.path]], + [cTs.path, [cTs.path, nrefs.path, bDts, aDts]] + ], + // revert the update + revert: host => host.writeFile(cTsconfig.path, cTsconfig.content), + // AfterEdit:: Extra watched files on server since the script infos arent deleted till next file open + orphanInfosAfterEdit: [refs.path.toLowerCase()], + // AfterRevert:: Extra watched files on server since the script infos arent deleted till next file open + orphanInfosAfterRevert: [nrefs.path.toLowerCase()] + }); }); - - describe("change in project reference config file builds correctly", () => { - verifyScenario((host, solutionBuilder) => { - host.writeFile(logic[0].path, JSON.stringify({ - compilerOptions: { composite: true, declaration: true, declarationDir: "decls" }, - references: [{ path: "../core" }] - })); - solutionBuilder.invalidateProject(logic[0].path.toLowerCase() as ResolvedConfigFilePath, ConfigFileProgramReloadLevel.Full); - solutionBuilder.buildNextInvalidatedProject(); - }, () => [tests[1].path, libFile.path, coreIndexDts, coreAnotherModuleDts, projectFilePath(SubProject.logic, "decls/index.d.ts")], expectedProjectFiles); + describe("edit in referenced config file", () => { + const nrefs: File = { + path: getFilePathInProject(project, "nrefs/a.d.ts"), + content: "export declare class A {}" + }; + const expectedProgramFiles = [cTs.path, bDts, nrefs.path, refs.path, libFile.path]; + const expectedProjectFiles = [cTs.path, bTs.path, nrefs.path, refs.path, libFile.path]; + const [, ...expectedWatchedDirectoriesRecursiveWithoutA] = expectedWatchedDirectoriesRecursive; // Not looking in a folder for resolution in multi folder scenario + verifyScenario({ + edit: host => { + const bTsConfigJson = JSON.parse(bTsconfig.content); + host.ensureFileOrFolder(nrefs); + bTsConfigJson.compilerOptions.paths = { "@ref/*": nrefsPath }; + host.writeFile(bTsconfig.path, JSON.stringify(bTsConfigJson)); + }, + expectedEditErrors: emptyArray, + expectedProgramFiles, + expectedProjectFiles, + expectedWatchedFiles: expectedProgramFiles.concat(cTsconfig.path, bTsconfig.path, aTsconfig.path).map(s => s.toLowerCase()), + expectedProjectWatchedFiles: expectedProjectFiles.concat(cTsconfig.path, bTsconfig.path, aTsconfig.path).map(s => s.toLowerCase()), + expectedWatchedDirectoriesRecursive: (multiFolder ? expectedWatchedDirectoriesRecursiveWithoutA : expectedWatchedDirectoriesRecursive).concat(getFilePathInProject(project, "nrefs").toLowerCase()), + dependencies: [ + [nrefs.path, [nrefs.path]], + [bDts, [bDts, nrefs.path]], + [refs.path, [refs.path]], + [cTs.path, [cTs.path, refs.path, bDts, nrefs.path]], + ], + // revert the update + revert: host => host.writeFile(bTsconfig.path, bTsconfig.content), + // AfterEdit:: Extra watched files on server since the script infos arent deleted till next file open + orphanInfosAfterEdit: [aTs.path.toLowerCase()], + // AfterRevert:: Extra watched files on server since the script infos arent deleted till next file open + orphanInfosAfterRevert: [nrefs.path.toLowerCase()] + }); }); - }); - - describe("on transitive references", () => { - const project = "transitiveReferences"; - const aTsFile = getFileFromProject(project, "a.ts"); - const bTsFile = getFileFromProject(project, "b.ts"); - const cTsFile = getFileFromProject(project, "c.ts"); - const aTsconfigFile = getFileFromProject(project, "tsconfig.a.json"); - const bTsconfigFile = getFileFromProject(project, "tsconfig.b.json"); - const cTsconfigFile = getFileFromProject(project, "tsconfig.c.json"); - const refs = getFileFromProject(project, "refs/a.d.ts"); - - function getRootFile(multiFolder: boolean, fileFromDisk: File, multiFolderPath: string): File { - return multiFolder ? { - path: getFilePathInProject(project, multiFolderPath), - content: fileFromDisk.content - // Replace the relative imports - .replace("./", "../") - } : fileFromDisk; - } - - function dtsFile(extensionLessFile: string) { - return getFilePathInProject(project, `${extensionLessFile}.d.ts`); - } - - function jsFile(extensionLessFile: string) { - return getFilePathInProject(project, `${extensionLessFile}.js`); - } - - function verifyWatchState( - host: TsBuildWatchSystem, - watch: Watch, - expectedProgramFiles: readonly string[], - expectedWatchedFiles: readonly string[], - expectedWatchedDirectoriesRecursive: readonly string[], - dependencies: readonly [string, readonly string[]][], - expectedWatchedDirectories?: readonly string[]) { - checkProgramActualFiles(watch(), expectedProgramFiles); - verifyWatchesOfProject(host, expectedWatchedFiles, expectedWatchedDirectoriesRecursive, expectedWatchedDirectories); - for (const [file, deps] of dependencies) { - verifyDependencies(watch, file, deps); - } - } - - function getTsConfigFile(multiFolder: boolean, fileFromDisk: File, folder: string): File { - if (!multiFolder) return fileFromDisk; - - return { - path: getFilePathInProject(project, `${folder}/tsconfig.json`), - content: fileFromDisk.content - // Replace files array - .replace(`${folder}.ts`, "index.ts") - // Replace path mappings - .replace("./*", "../*") - .replace("./refs", "../refs") - // Replace references - .replace("tsconfig.a.json", "../a") - .replace("tsconfig.b.json", "../b") + describe("deleting referenced config file", () => { + const expectedProgramFiles = [cTs.path, bTs.path, refs.path, libFile.path]; + const expectedWatchedFiles = expectedProgramFiles.concat(cTsconfig.path, bTsconfig.path).map(s => s.toLowerCase()); + const [, ...expectedWatchedDirectoriesRecursiveWithoutA] = expectedWatchedDirectoriesRecursive; // Not looking in a folder for resolution in multi folder scenario + // Resolutions should change now + // Should map to b.ts instead with options from our own config + verifyScenario({ + edit: host => host.deleteFile(bTsconfig.path), + expectedEditErrors: [ + `${multiFolder ? "c/tsconfig.json" : "tsconfig.c.json"}(9,21): error TS6053: File '/user/username/projects/transitiveReferences/${multiFolder ? "b" : "tsconfig.b.json"}' not found.\n` + ], + expectedProgramFiles, + expectedProjectFiles: expectedProgramFiles, + expectedWatchedFiles, + expectedProjectWatchedFiles: expectedWatchedFiles, + expectedWatchedDirectoriesRecursive: multiFolder ? expectedWatchedDirectoriesRecursiveWithoutA : expectedWatchedDirectoriesRecursive, + dependencies: [ + [bTs.path, [bTs.path, refs.path]], + [refs.path, [refs.path]], + [cTs.path, [cTs.path, refs.path, bTs.path]], + ], + // revert the update + revert: host => host.writeFile(bTsconfig.path, bTsconfig.content), + // AfterEdit:: Extra watched files on server since the script infos arent deleted till next file open + orphanInfosAfterEdit: [aTs.path.toLowerCase(), aTsconfig.path.toLowerCase()], + }); + }); + describe("deleting transitively referenced config file", () => { + verifyScenario({ + edit: host => host.deleteFile(aTsconfig.path), + expectedEditErrors: [ + `${multiFolder ? "b/tsconfig.json" : "tsconfig.b.json"}(10,21): error TS6053: File '/user/username/projects/transitiveReferences/${multiFolder ? "a" : "tsconfig.a.json"}' not found.\n` + ], + expectedProgramFiles: expectedProgramFiles.map(s => s.replace(aDts, aTs.path)), + expectedProjectFiles, + expectedWatchedFiles: expectedWatchedFiles.map(s => s.replace(aDts.toLowerCase(), aTs.path.toLocaleLowerCase())), + expectedProjectWatchedFiles, + expectedWatchedDirectoriesRecursive, + dependencies: [ + [aTs.path, [aTs.path]], + [bDts, [bDts, aTs.path]], + [refs.path, [refs.path]], + [cTs.path, [cTs.path, refs.path, bDts, aTs.path]], + ], + // revert the update + revert: host => host.writeFile(aTsconfig.path, aTsconfig.content), + }); + }); + } + describe("when config files are side by side", () => { + verifyTransitiveReferences(/*multiFolder*/ false); + it("when referenced project uses different module resolution", () => { + const bTs: File = { + path: bTsFile.path, + content: `import {A} from "a";export const b = new A();` }; - } - - // function writeFile(file: File) { - // Harness.IO.writeFile(file.path.replace(projectsLocation, "c:/temp"), file.content); - // } - - function verifyTransitiveReferences(multiFolder: boolean) { - const aTs = getRootFile(multiFolder, aTsFile, "a/index.ts"); - const bTs = getRootFile(multiFolder, bTsFile, "b/index.ts"); - const cTs = getRootFile(multiFolder, cTsFile, "c/index.ts"); - - const configToBuild = multiFolder ? "c/tsconfig.json" : "tsconfig.c.json"; - const aTsconfig = getTsConfigFile(multiFolder, aTsconfigFile, "a"); - const bTsconfig = getTsConfigFile(multiFolder, bTsconfigFile, "b"); - const cTsconfig = getTsConfigFile(multiFolder, cTsconfigFile, "c"); - - // if (multiFolder) { - // writeFile(aTs); - // writeFile(bTs); - // writeFile(cTs); - // writeFile(aTsconfig); - // writeFile(bTsconfig); - // writeFile(cTsconfig); - // } - - const allFiles = [libFile, aTs, bTs, cTs, aTsconfig, bTsconfig, cTsconfig, refs]; - const aDts = dtsFile(multiFolder ? "a/index" : "a"), bDts = dtsFile(multiFolder ? "b/index" : "b"); - const expectedFiles = [jsFile(multiFolder ? "a/index" : "a"), aDts, jsFile(multiFolder ? "b/index" : "b"), bDts, jsFile(multiFolder ? "c/index" : "c")]; - const expectedProgramFiles = [cTs.path, libFile.path, aDts, refs.path, bDts]; - const expectedProjectFiles = [cTs.path, libFile.path, aTs.path, refs.path, bTs.path]; - const expectedWatchedFiles = expectedProgramFiles.concat(cTsconfig.path, bTsconfig.path, aTsconfig.path).map(s => s.toLowerCase()); - const expectedProjectWatchedFiles = expectedProjectFiles.concat(cTsconfig.path, bTsconfig.path, aTsconfig.path).map(s => s.toLowerCase()); - const expectedWatchedDirectories = multiFolder ? [ - getProjectPath(project).toLowerCase() // watches for directories created for resolution of b - ] : emptyArray; - const nrefsPath = multiFolder ? ["../nrefs/*"] : ["./nrefs/*"]; + const bTsconfig: File = { + path: bTsconfigFile.path, + content: JSON.stringify({ + compilerOptions: { composite: true, moduleResolution: "classic" }, + files: ["b.ts"], + references: [{ path: "tsconfig.a.json" }] + }) + }; + const allFiles = [libFile, aTsFile, bTs, cTsFile, aTsconfigFile, bTsconfig, cTsconfigFile, refs]; + const aDts = dtsFile("a"), bDts = dtsFile("b"); + const expectedFiles = [jsFile("a"), aDts, jsFile("b"), bDts, jsFile("c")]; + const expectedProgramFiles = [cTsFile.path, libFile.path, aDts, refs.path, bDts]; + const expectedWatchedFiles = expectedProgramFiles.concat(cTsconfigFile.path, bTsconfigFile.path, aTsconfigFile.path).map(s => s.toLowerCase()); const expectedWatchedDirectoriesRecursive = [ - ...(multiFolder ? [ - getFilePathInProject(project, "a"), // Failed to package json - getFilePathInProject(project, "b"), // Failed to package json - ] : []), - getFilePathInProject(project, "refs"), // Failed lookup since refs/a.ts does not exist - ...projectSystem.getTypeRootsFromLocation(multiFolder ? getFilePathInProject(project, "c") : getProjectPath(project)) + getFilePathInProject(project, "refs"), + ...getTypeRootsFromLocation(getProjectPath(project)) ].map(s => s.toLowerCase()); - const defaultDependencies: readonly [string, readonly string[]][] = [ [aDts, [aDts]], [bDts, [bDts, aDts]], [refs.path, [refs.path]], - [cTs.path, [cTs.path, refs.path, bDts, aDts]] + [cTsFile.path, [cTsFile.path, refs.path, bDts, aDts]] ]; - - function createSolutionAndWatchMode() { - return createSolutionAndWatchModeOfProject(allFiles, getProjectPath(project), configToBuild, configToBuild, getOutputFileStamps); - } - - function createSolutionAndService() { - return createSolutionAndServiceOfProject(allFiles, getProjectPath(project), configToBuild, cTs.path, getOutputFileStamps); - } - function getOutputFileStamps(host: TsBuildWatchSystem) { return expectedFiles.map(file => transformOutputToOutputFileStamp(file, host)); } - - function verifyProgram(host: TsBuildWatchSystem, watch: Watch) { - verifyWatchState(host, watch, expectedProgramFiles, expectedWatchedFiles, expectedWatchedDirectoriesRecursive, defaultDependencies, expectedWatchedDirectories); - } - - function verifyProject(host: TsBuildWatchSystem, service: projectSystem.TestProjectService, orphanInfos?: readonly string[]) { - verifyServerState({ host, service, expectedProjectFiles, expectedProjectWatchedFiles, expectedWatchedDirectoriesRecursive, orphanInfos }); - } - - interface VerifyServerState { - host: TsBuildWatchSystem; - service: projectSystem.TestProjectService; - expectedProjectFiles: readonly string[]; - expectedProjectWatchedFiles: readonly string[]; - expectedWatchedDirectoriesRecursive: readonly string[]; - orphanInfos?: readonly string[]; - } - function verifyServerState({ host, service, expectedProjectFiles, expectedProjectWatchedFiles, expectedWatchedDirectoriesRecursive, orphanInfos }: VerifyServerState) { - checkProjectActualFiles(service, cTsconfig.path, expectedProjectFiles.concat(cTsconfig.path)); - const watchedFiles = expectedProjectWatchedFiles.filter(f => f !== cTs.path.toLowerCase()); - const actualOrphan = arrayFrom(mapDefinedIterator( - service.filenameToScriptInfo.values(), - v => v.containingProjects.length === 0 ? v.fileName : undefined - )); - assert.equal(actualOrphan.length, orphanInfos ? orphanInfos.length : 0, `Orphans found: ${JSON.stringify(actualOrphan, /*replacer*/ undefined, " ")}`); - if (orphanInfos && orphanInfos.length) { - for (const orphan of orphanInfos) { - const info = service.getScriptInfoForPath(orphan as Path); - assert.isDefined(info, `${orphan} expected to be present. Actual: ${JSON.stringify(actualOrphan, /*replacer*/ undefined, " ")}`); - assert.equal(info!.containingProjects.length, 0); - watchedFiles.push(orphan); - } - } - verifyWatchesOfProject(host, watchedFiles, expectedWatchedDirectoriesRecursive, expectedWatchedDirectories); - } - - interface VerifyScenario { - edit: (host: TsBuildWatchSystem, solutionBuilder: SolutionBuilder) => void; - expectedEditErrors: readonly string[]; - expectedProgramFiles: readonly string[]; - expectedProjectFiles: readonly string[]; - expectedWatchedFiles: readonly string[]; - expectedProjectWatchedFiles: readonly string[]; - expectedWatchedDirectoriesRecursive: readonly string[]; - dependencies: readonly [string, readonly string[]][]; - revert?: (host: TsBuildWatchSystem) => void; - orphanInfosAfterEdit?: readonly string[]; - orphanInfosAfterRevert?: readonly string[]; - } - function verifyScenario({ edit, expectedEditErrors, expectedProgramFiles, expectedProjectFiles, expectedWatchedFiles, expectedProjectWatchedFiles, expectedWatchedDirectoriesRecursive, dependencies, revert, orphanInfosAfterEdit, orphanInfosAfterRevert }: VerifyScenario) { - it("with tsc-watch", () => { - const { host, solutionBuilder, watch } = createSolutionAndWatchMode(); - - edit(host, solutionBuilder); - - host.checkTimeoutQueueLengthAndRun(1); - checkOutputErrorsIncremental(host, expectedEditErrors); - verifyWatchState(host, watch, expectedProgramFiles, expectedWatchedFiles, expectedWatchedDirectoriesRecursive, dependencies, expectedWatchedDirectories); - - if (revert) { - revert(host); - - host.checkTimeoutQueueLengthAndRun(1); - checkOutputErrorsIncremental(host, emptyArray); - verifyProgram(host, watch); - } - }); - - if (!multiFolder) return; // With side by side file open is in inferred project without any settings - - it("with tsserver", () => { - const { host, solutionBuilder, service } = createSolutionAndService(); - - edit(host, solutionBuilder); - - host.checkTimeoutQueueLengthAndRun(2); - verifyServerState({ host, service, expectedProjectFiles, expectedProjectWatchedFiles, expectedWatchedDirectoriesRecursive, orphanInfos: orphanInfosAfterEdit }); - - if (revert) { - revert(host); - - host.checkTimeoutQueueLengthAndRun(2); - verifyProject(host, service, orphanInfosAfterRevert); - } - }); - } - - describe("verifies dependencies and watches", () => { - // Initial build - it("with tsc-watch", () => { - const { host, watch } = createSolutionAndWatchMode(); - verifyProgram(host, watch); - }); - if (!multiFolder) return; - it("with tsserver", () => { - const { host, service } = createSolutionAndService(); - verifyProject(host, service); - }); - }); - - describe("non local edit updates the program and watch correctly", () => { - verifyScenario({ - edit: (host, solutionBuilder) => { - // edit - host.writeFile(bTs.path, `${bTs.content}\nexport function gfoo() {\n}`); - solutionBuilder.invalidateProject((bTsconfig.path.toLowerCase() as ResolvedConfigFilePath)); - solutionBuilder.buildNextInvalidatedProject(); - }, - expectedEditErrors: emptyArray, - expectedProgramFiles, - expectedProjectFiles, - expectedWatchedFiles, - expectedProjectWatchedFiles, - expectedWatchedDirectoriesRecursive, - dependencies: defaultDependencies - }); - }); - - describe("edit on config file", () => { - const nrefReplacer = (f: string) => f.replace("refs", "nrefs"); - const nrefs: File = { - path: getFilePathInProject(project, "nrefs/a.d.ts"), - content: refs.content - }; - verifyScenario({ - edit: host => { - const cTsConfigJson = JSON.parse(cTsconfig.content); - host.ensureFileOrFolder(nrefs); - cTsConfigJson.compilerOptions.paths = { "@ref/*": nrefsPath }; - host.writeFile(cTsconfig.path, JSON.stringify(cTsConfigJson)); - }, - expectedEditErrors: emptyArray, - expectedProgramFiles: expectedProgramFiles.map(nrefReplacer), - expectedProjectFiles: expectedProjectFiles.map(nrefReplacer), - expectedWatchedFiles: expectedWatchedFiles.map(nrefReplacer), - expectedProjectWatchedFiles: expectedProjectWatchedFiles.map(nrefReplacer), - expectedWatchedDirectoriesRecursive: expectedWatchedDirectoriesRecursive.map(nrefReplacer), - dependencies: [ - [aDts, [aDts]], - [bDts, [bDts, aDts]], - [nrefs.path, [nrefs.path]], - [cTs.path, [cTs.path, nrefs.path, bDts, aDts]] - ], - // revert the update - revert: host => host.writeFile(cTsconfig.path, cTsconfig.content), - // AfterEdit:: Extra watched files on server since the script infos arent deleted till next file open - orphanInfosAfterEdit: [refs.path.toLowerCase()], - // AfterRevert:: Extra watched files on server since the script infos arent deleted till next file open - orphanInfosAfterRevert: [nrefs.path.toLowerCase()] - }); - }); - - describe("edit in referenced config file", () => { - const nrefs: File = { - path: getFilePathInProject(project, "nrefs/a.d.ts"), - content: "export declare class A {}" - }; - const expectedProgramFiles = [cTs.path, bDts, nrefs.path, refs.path, libFile.path]; - const expectedProjectFiles = [cTs.path, bTs.path, nrefs.path, refs.path, libFile.path]; - const [, ...expectedWatchedDirectoriesRecursiveWithoutA] = expectedWatchedDirectoriesRecursive; // Not looking in a folder for resolution in multi folder scenario - verifyScenario({ - edit: host => { - const bTsConfigJson = JSON.parse(bTsconfig.content); - host.ensureFileOrFolder(nrefs); - bTsConfigJson.compilerOptions.paths = { "@ref/*": nrefsPath }; - host.writeFile(bTsconfig.path, JSON.stringify(bTsConfigJson)); - }, - expectedEditErrors: emptyArray, - expectedProgramFiles, - expectedProjectFiles, - expectedWatchedFiles: expectedProgramFiles.concat(cTsconfig.path, bTsconfig.path, aTsconfig.path).map(s => s.toLowerCase()), - expectedProjectWatchedFiles: expectedProjectFiles.concat(cTsconfig.path, bTsconfig.path, aTsconfig.path).map(s => s.toLowerCase()), - expectedWatchedDirectoriesRecursive: (multiFolder ? expectedWatchedDirectoriesRecursiveWithoutA : expectedWatchedDirectoriesRecursive).concat(getFilePathInProject(project, "nrefs").toLowerCase()), - dependencies: [ - [nrefs.path, [nrefs.path]], - [bDts, [bDts, nrefs.path]], - [refs.path, [refs.path]], - [cTs.path, [cTs.path, refs.path, bDts, nrefs.path]], - ], - // revert the update - revert: host => host.writeFile(bTsconfig.path, bTsconfig.content), - // AfterEdit:: Extra watched files on server since the script infos arent deleted till next file open - orphanInfosAfterEdit: [aTs.path.toLowerCase()], - // AfterRevert:: Extra watched files on server since the script infos arent deleted till next file open - orphanInfosAfterRevert: [nrefs.path.toLowerCase()] - }); - }); - - describe("deleting referenced config file", () => { - const expectedProgramFiles = [cTs.path, bTs.path, refs.path, libFile.path]; - const expectedWatchedFiles = expectedProgramFiles.concat(cTsconfig.path, bTsconfig.path).map(s => s.toLowerCase()); - const [, ...expectedWatchedDirectoriesRecursiveWithoutA] = expectedWatchedDirectoriesRecursive; // Not looking in a folder for resolution in multi folder scenario - // Resolutions should change now - // Should map to b.ts instead with options from our own config - verifyScenario({ - edit: host => host.deleteFile(bTsconfig.path), - expectedEditErrors: [ - `${multiFolder ? "c/tsconfig.json" : "tsconfig.c.json"}(9,21): error TS6053: File '/user/username/projects/transitiveReferences/${multiFolder ? "b" : "tsconfig.b.json"}' not found.\n` - ], - expectedProgramFiles, - expectedProjectFiles: expectedProgramFiles, - expectedWatchedFiles, - expectedProjectWatchedFiles: expectedWatchedFiles, - expectedWatchedDirectoriesRecursive: multiFolder ? expectedWatchedDirectoriesRecursiveWithoutA : expectedWatchedDirectoriesRecursive, - dependencies: [ - [bTs.path, [bTs.path, refs.path]], - [refs.path, [refs.path]], - [cTs.path, [cTs.path, refs.path, bTs.path]], - ], - // revert the update - revert: host => host.writeFile(bTsconfig.path, bTsconfig.content), - // AfterEdit:: Extra watched files on server since the script infos arent deleted till next file open - orphanInfosAfterEdit: [aTs.path.toLowerCase(), aTsconfig.path.toLowerCase()], - }); - }); - - describe("deleting transitively referenced config file", () => { - verifyScenario({ - edit: host => host.deleteFile(aTsconfig.path), - expectedEditErrors: [ - `${multiFolder ? "b/tsconfig.json" : "tsconfig.b.json"}(10,21): error TS6053: File '/user/username/projects/transitiveReferences/${multiFolder ? "a" : "tsconfig.a.json"}' not found.\n` - ], - expectedProgramFiles: expectedProgramFiles.map(s => s.replace(aDts, aTs.path)), - expectedProjectFiles, - expectedWatchedFiles: expectedWatchedFiles.map(s => s.replace(aDts.toLowerCase(), aTs.path.toLocaleLowerCase())), - expectedProjectWatchedFiles, - expectedWatchedDirectoriesRecursive, - dependencies: [ - [aTs.path, [aTs.path]], - [bDts, [bDts, aTs.path]], - [refs.path, [refs.path]], - [cTs.path, [cTs.path, refs.path, bDts, aTs.path]], - ], - // revert the update - revert: host => host.writeFile(aTsconfig.path, aTsconfig.content), - }); - }); - } - - describe("when config files are side by side", () => { - verifyTransitiveReferences(/*multiFolder*/ false); - - it("when referenced project uses different module resolution", () => { - const bTs: File = { - path: bTsFile.path, - content: `import {A} from "a";export const b = new A();` - }; - const bTsconfig: File = { - path: bTsconfigFile.path, - content: JSON.stringify({ - compilerOptions: { composite: true, moduleResolution: "classic" }, - files: ["b.ts"], - references: [{ path: "tsconfig.a.json" }] - }) - }; - const allFiles = [libFile, aTsFile, bTs, cTsFile, aTsconfigFile, bTsconfig, cTsconfigFile, refs]; - const aDts = dtsFile("a"), bDts = dtsFile("b"); - const expectedFiles = [jsFile("a"), aDts, jsFile("b"), bDts, jsFile("c")]; - const expectedProgramFiles = [cTsFile.path, libFile.path, aDts, refs.path, bDts]; - const expectedWatchedFiles = expectedProgramFiles.concat(cTsconfigFile.path, bTsconfigFile.path, aTsconfigFile.path).map(s => s.toLowerCase()); - const expectedWatchedDirectoriesRecursive = [ - getFilePathInProject(project, "refs"), // Failed lookup since refs/a.ts does not exist - ...projectSystem.getTypeRootsFromLocation(getProjectPath(project)) - ].map(s => s.toLowerCase()); - - const defaultDependencies: readonly [string, readonly string[]][] = [ - [aDts, [aDts]], - [bDts, [bDts, aDts]], - [refs.path, [refs.path]], - [cTsFile.path, [cTsFile.path, refs.path, bDts, aDts]] - ]; - function getOutputFileStamps(host: TsBuildWatchSystem) { - return expectedFiles.map(file => transformOutputToOutputFileStamp(file, host)); - } - const { host, watch } = createSolutionAndWatchModeOfProject(allFiles, getProjectPath(project), "tsconfig.c.json", "tsconfig.c.json", getOutputFileStamps); - verifyWatchState(host, watch, expectedProgramFiles, expectedWatchedFiles, expectedWatchedDirectoriesRecursive, defaultDependencies); - }); - }); - describe("when config files are in side by side folders", () => { - verifyTransitiveReferences(/*multiFolder*/ true); + const { host, watch } = createSolutionAndWatchModeOfProject(allFiles, getProjectPath(project), "tsconfig.c.json", "tsconfig.c.json", getOutputFileStamps); + verifyWatchState(host, watch, expectedProgramFiles, expectedWatchedFiles, expectedWatchedDirectoriesRecursive, defaultDependencies); }); }); + describe("when config files are in side by side folders", () => { + verifyTransitiveReferences(/*multiFolder*/ true); + }); }); }); - - verifyTscWatch({ - scenario, - subScenario: "incremental updates in verbose mode", - commandLineArgs: ["-b", "-w", `${project}/${SubProject.tests}`, "-verbose"], - sys: () => createWatchedSystem(allFiles, { currentDirectory: projectsLocation }), - changes: [ - sys => { - sys.writeFile(logic[1].path, `${logic[1].content} + }); + verifyTscWatch({ + scenario, + subScenario: "incremental updates in verbose mode", + commandLineArgs: ["-b", "-w", `${project}/${SubProject.tests}`, "-verbose"], + sys: () => createWatchedSystem(allFiles, { currentDirectory: projectsLocation }), + changes: [ + sys => { + sys.writeFile(logic[1].path, `${logic[1].content} function someFn() { }`); - sys.checkTimeoutQueueLengthAndRun(1); // build logic - sys.checkTimeoutQueueLengthAndRun(1); // build tests - return "Make non dts change"; - }, - sys => { - sys.writeFile(logic[1].path, `${logic[1].content} + sys.checkTimeoutQueueLengthAndRun(1); // build logic + sys.checkTimeoutQueueLengthAndRun(1); // build tests + return "Make non dts change"; + }, + sys => { + sys.writeFile(logic[1].path, `${logic[1].content} export function someFn() { }`); - sys.checkTimeoutQueueLengthAndRun(1); // build logic - sys.checkTimeoutQueueLengthAndRun(1); // build tests - return "Make dts change"; - } - ], - }); + sys.checkTimeoutQueueLengthAndRun(1); // build logic + sys.checkTimeoutQueueLengthAndRun(1); // build tests + return "Make dts change"; + } + ], }); - - describe("unittests:: tsbuild:: watchMode:: with demo project", () => { - const projectLocation = `${projectsLocation}/demo`; - let coreFiles: File[]; - let animalFiles: File[]; - let zooFiles: File[]; - let solutionFile: File; - let baseConfig: File; - let allFiles: File[]; - before(() => { - coreFiles = subProjectFiles("core", ["tsconfig.json", "utilities.ts"]); - animalFiles = subProjectFiles("animals", ["tsconfig.json", "animal.ts", "dog.ts", "index.ts"]); - zooFiles = subProjectFiles("zoo", ["tsconfig.json", "zoo.ts"]); - solutionFile = projectFile("tsconfig.json"); - baseConfig = projectFile("tsconfig-base.json"); - allFiles = [...coreFiles, ...animalFiles, ...zooFiles, solutionFile, baseConfig, { path: libFile.path, content: libContent }]; - }); - - after(() => { - coreFiles = undefined!; - animalFiles = undefined!; - zooFiles = undefined!; - solutionFile = undefined!; - baseConfig = undefined!; - allFiles = undefined!; - }); - - verifyTscWatch({ - scenario: "demo", - subScenario: "updates with circular reference", - commandLineArgs: ["-b", "-w", "-verbose"], - sys: () => { - const sys = createWatchedSystem(allFiles, { currentDirectory: projectLocation }); - sys.writeFile(coreFiles[0].path, coreFiles[0].content.replace( - "}", - `}, +}); +describe("unittests:: tsbuild:: watchMode:: with demo project", () => { + const projectLocation = `${projectsLocation}/demo`; + let coreFiles: File[]; + let animalFiles: File[]; + let zooFiles: File[]; + let solutionFile: File; + let baseConfig: File; + let allFiles: File[]; + before(() => { + coreFiles = subProjectFiles("core", ["tsconfig.json", "utilities.ts"]); + animalFiles = subProjectFiles("animals", ["tsconfig.json", "animal.ts", "dog.ts", "index.ts"]); + zooFiles = subProjectFiles("zoo", ["tsconfig.json", "zoo.ts"]); + solutionFile = projectFile("tsconfig.json"); + baseConfig = projectFile("tsconfig-base.json"); + allFiles = [...coreFiles, ...animalFiles, ...zooFiles, solutionFile, baseConfig, { path: libFile.path, content: libContent }]; + }); + after(() => { + coreFiles = undefined!; + animalFiles = undefined!; + zooFiles = undefined!; + solutionFile = undefined!; + baseConfig = undefined!; + allFiles = undefined!; + }); + verifyTscWatch({ + scenario: "demo", + subScenario: "updates with circular reference", + commandLineArgs: ["-b", "-w", "-verbose"], + sys: () => { + const sys = createWatchedSystem(allFiles, { currentDirectory: projectLocation }); + sys.writeFile(coreFiles[0].path, coreFiles[0].content.replace("}", `}, "references": [ { "path": "../zoo" } - ]` - )); - return sys; - }, - changes: [ - sys => { - sys.writeFile(coreFiles[0].path, coreFiles[0].content); - sys.checkTimeoutQueueLengthAndRun(1); // build core - sys.checkTimeoutQueueLengthAndRun(1); // build animals - sys.checkTimeoutQueueLengthAndRun(1); // build zoo - sys.checkTimeoutQueueLengthAndRun(1); // build solution - sys.checkTimeoutQueueLength(0); - return "Fix error"; - } - ] - }); - - verifyTscWatch({ - scenario: "demo", - subScenario: "updates with bad reference", - commandLineArgs: ["-b", "-w", "-verbose"], - sys: () => { - const sys = createWatchedSystem(allFiles, { currentDirectory: projectLocation }); - sys.writeFile(coreFiles[1].path, `import * as A from '../animals'; + ]`)); + return sys; + }, + changes: [ + sys => { + sys.writeFile(coreFiles[0].path, coreFiles[0].content); + sys.checkTimeoutQueueLengthAndRun(1); // build core + sys.checkTimeoutQueueLengthAndRun(1); // build animals + sys.checkTimeoutQueueLengthAndRun(1); // build zoo + sys.checkTimeoutQueueLengthAndRun(1); // build solution + sys.checkTimeoutQueueLength(0); + return "Fix error"; + } + ] + }); + verifyTscWatch({ + scenario: "demo", + subScenario: "updates with bad reference", + commandLineArgs: ["-b", "-w", "-verbose"], + sys: () => { + const sys = createWatchedSystem(allFiles, { currentDirectory: projectLocation }); + sys.writeFile(coreFiles[1].path, `import * as A from '../animals'; ${coreFiles[1].content}`); - return sys; - }, - changes: [ - sys => { - sys.writeFile(coreFiles[1].path, ` + return sys; + }, + changes: [ + sys => { + sys.writeFile(coreFiles[1].path, ` import * as A from '../animals'; ${coreFiles[1].content}`); - sys.checkTimeoutQueueLengthAndRun(1); // build core - sys.checkTimeoutQueueLength(0); - return "Prepend a line"; - } - ] - }); - - function subProjectFiles(subProject: string, fileNames: readonly string[]): File[] { - return fileNames.map(file => projectFile(`${subProject}/${file}`)); - } - - function projectFile(fileName: string): File { - return getFileFromProject("demo", fileName); - } + sys.checkTimeoutQueueLengthAndRun(1); // build core + sys.checkTimeoutQueueLength(0); + return "Prepend a line"; + } + ] }); - - describe("unittests:: tsbuild:: watchMode:: with noEmitOnError", () => { - verifyTscWatch({ - scenario: "noEmitOnError", - subScenario: "does not emit any files on error", - commandLineArgs: ["-b", "-w", "-verbose"], - sys: () => createWatchedSystem( - [ - ...["tsconfig.json", "shared/types/db.ts", "src/main.ts", "src/other.ts"] - .map(f => getFileFromProject("noEmitOnError", f)), - { path: libFile.path, content: libContent } - ], - { currentDirectory: `${projectsLocation}/noEmitOnError` } - ), - changes: [ - sys => { - sys.writeFile(`${projectsLocation}/noEmitOnError/src/main.ts`, `import { A } from "../shared/types/db"; + function subProjectFiles(subProject: string, fileNames: readonly string[]): File[] { + return fileNames.map(file => projectFile(`${subProject}/${file}`)); + } + function projectFile(fileName: string): File { + return getFileFromProject("demo", fileName); + } +}); +describe("unittests:: tsbuild:: watchMode:: with noEmitOnError", () => { + verifyTscWatch({ + scenario: "noEmitOnError", + subScenario: "does not emit any files on error", + commandLineArgs: ["-b", "-w", "-verbose"], + sys: () => createWatchedSystem([ + ...["tsconfig.json", "shared/types/db.ts", "src/main.ts", "src/other.ts"] + .map(f => getFileFromProject("noEmitOnError", f)), + { path: libFile.path, content: libContent } + ], { currentDirectory: `${projectsLocation}/noEmitOnError` }), + changes: [ + sys => { + sys.writeFile(`${projectsLocation}/noEmitOnError/src/main.ts`, `import { A } from "../shared/types/db"; const a = { lastName: 'sdsd' };`); - sys.checkTimeoutQueueLengthAndRun(1); // build project - sys.checkTimeoutQueueLength(0); - return "Fix error"; - } - ] - }); + sys.checkTimeoutQueueLengthAndRun(1); // build project + sys.checkTimeoutQueueLength(0); + return "Fix error"; + } + ] }); - - describe("unittests:: tsbuild:: watchMode:: with reexport when referenced project reexports definitions from another file", () => { - verifyTscWatch({ - scenario: "reexport", - subScenario: "Reports errors correctly", - commandLineArgs: ["-b", "-w", "-verbose", "src"], - sys: () => createWatchedSystem( - [ - ...[ - "src/tsconfig.json", - "src/main/tsconfig.json", "src/main/index.ts", - "src/pure/tsconfig.json", "src/pure/index.ts", "src/pure/session.ts" - ] - .map(f => getFileFromProject("reexport", f)), - { path: libFile.path, content: libContent } - ], - { currentDirectory: `${projectsLocation}/reexport` } - ), - changes: [ - sys => { - replaceFileText(sys, `${projectsLocation}/reexport/src/pure/session.ts`, "// ", ""); - sys.checkTimeoutQueueLengthAndRun(1); // build src/pure - sys.checkTimeoutQueueLengthAndRun(1); // build src/main - sys.checkTimeoutQueueLengthAndRun(1); // build src - sys.checkTimeoutQueueLength(0); - return "Introduce error"; - }, - sys => { - replaceFileText(sys, `${projectsLocation}/reexport/src/pure/session.ts`, "bar: ", "// bar: "); - sys.checkTimeoutQueueLengthAndRun(1); // build src/pure - sys.checkTimeoutQueueLengthAndRun(1); // build src/main - sys.checkTimeoutQueueLengthAndRun(1); // build src - sys.checkTimeoutQueueLength(0); - return "Fix error"; - } +}); +describe("unittests:: tsbuild:: watchMode:: with reexport when referenced project reexports definitions from another file", () => { + verifyTscWatch({ + scenario: "reexport", + subScenario: "Reports errors correctly", + commandLineArgs: ["-b", "-w", "-verbose", "src"], + sys: () => createWatchedSystem([ + ...[ + "src/tsconfig.json", + "src/main/tsconfig.json", "src/main/index.ts", + "src/pure/tsconfig.json", "src/pure/index.ts", "src/pure/session.ts" ] - }); + .map(f => getFileFromProject("reexport", f)), + { path: libFile.path, content: libContent } + ], { currentDirectory: `${projectsLocation}/reexport` }), + changes: [ + sys => { + replaceFileText(sys, `${projectsLocation}/reexport/src/pure/session.ts`, "// ", ""); + sys.checkTimeoutQueueLengthAndRun(1); // build src/pure + sys.checkTimeoutQueueLengthAndRun(1); // build src/main + sys.checkTimeoutQueueLengthAndRun(1); // build src + sys.checkTimeoutQueueLength(0); + return "Introduce error"; + }, + sys => { + replaceFileText(sys, `${projectsLocation}/reexport/src/pure/session.ts`, "bar: ", "// bar: "); + sys.checkTimeoutQueueLengthAndRun(1); // build src/pure + sys.checkTimeoutQueueLengthAndRun(1); // build src/main + sys.checkTimeoutQueueLengthAndRun(1); // build src + sys.checkTimeoutQueueLength(0); + return "Fix error"; + } + ] }); - - describe("unittests:: tsbuild:: watchMode:: configFileErrors:: reports syntax errors in config file", () => { - verifyTscWatch({ - scenario: "configFileErrors", - subScenario: "reports syntax errors in config file", - sys: () => createWatchedSystem( - [ - { path: `${projectRoot}/a.ts`, content: "export function foo() { }" }, - { path: `${projectRoot}/b.ts`, content: "export function bar() { }" }, - { - path: `${projectRoot}/tsconfig.json`, - content: Utils.dedent` +}); +describe("unittests:: tsbuild:: watchMode:: configFileErrors:: reports syntax errors in config file", () => { + verifyTscWatch({ + scenario: "configFileErrors", + subScenario: "reports syntax errors in config file", + sys: () => createWatchedSystem([ + { path: `${projectRoot}/a.ts`, content: "export function foo() { }" }, + { path: `${projectRoot}/b.ts`, content: "export function bar() { }" }, + { + path: `${projectRoot}/tsconfig.json`, + content: dedent ` { "compilerOptions": { "composite": true, @@ -1305,42 +1119,39 @@ const a = { "b.ts" ] }` - }, - libFile - ], - { currentDirectory: projectRoot } - ), - commandLineArgs: ["--b", "-w"], - changes: [ - sys => { - replaceFileText(sys, `${projectRoot}/tsconfig.json`, ",", `, + }, + libFile + ], { currentDirectory: projectRoot }), + commandLineArgs: ["--b", "-w"], + changes: [ + sys => { + replaceFileText(sys, `${projectRoot}/tsconfig.json`, ",", `, "declaration": true,`); - sys.checkTimeoutQueueLengthAndRun(1); // build the project - sys.checkTimeoutQueueLength(0); - return "reports syntax errors after change to config file"; - }, - sys => { - replaceFileText(sys, `${projectRoot}/a.ts`, "foo", "fooBar"); - sys.checkTimeoutQueueLengthAndRun(1); // build the project - sys.checkTimeoutQueueLength(0); - return "reports syntax errors after change to ts file"; - }, - sys => { - replaceFileText(sys, `${projectRoot}/tsconfig.json`, "", ""); - sys.checkTimeoutQueueLengthAndRun(1); // build the project - sys.checkTimeoutQueueLength(0); - return "reports error when there is no change to tsconfig file"; - }, - sys => { - sys.writeFile(`${projectRoot}/tsconfig.json`, JSON.stringify({ - compilerOptions: { composite: true, declaration: true }, - files: ["a.ts", "b.ts"] - })); - sys.checkTimeoutQueueLengthAndRun(1); // build the project - sys.checkTimeoutQueueLength(0); - return "builds after fixing config file errors"; - } - ] - }); + sys.checkTimeoutQueueLengthAndRun(1); // build the project + sys.checkTimeoutQueueLength(0); + return "reports syntax errors after change to config file"; + }, + sys => { + replaceFileText(sys, `${projectRoot}/a.ts`, "foo", "fooBar"); + sys.checkTimeoutQueueLengthAndRun(1); // build the project + sys.checkTimeoutQueueLength(0); + return "reports syntax errors after change to ts file"; + }, + sys => { + replaceFileText(sys, `${projectRoot}/tsconfig.json`, "", ""); + sys.checkTimeoutQueueLengthAndRun(1); // build the project + sys.checkTimeoutQueueLength(0); + return "reports error when there is no change to tsconfig file"; + }, + sys => { + sys.writeFile(`${projectRoot}/tsconfig.json`, JSON.stringify({ + compilerOptions: { composite: true, declaration: true }, + files: ["a.ts", "b.ts"] + })); + sys.checkTimeoutQueueLengthAndRun(1); // build the project + sys.checkTimeoutQueueLength(0); + return "builds after fixing config file errors"; + } + ] }); -} +}); diff --git a/src/testRunner/unittests/tsc/composite.ts b/src/testRunner/unittests/tsc/composite.ts index 8eb0ea3bd8595..afcf3cf3b5919 100644 --- a/src/testRunner/unittests/tsc/composite.ts +++ b/src/testRunner/unittests/tsc/composite.ts @@ -1,11 +1,12 @@ -namespace ts { - describe("unittests:: tsc:: composite::", () => { - verifyTsc({ - scenario: "composite", - subScenario: "when setting composite false on command line", - fs: () => loadProjectFromFiles({ - "/src/project/src/main.ts": "export const x = 10;", - "/src/project/tsconfig.json": Utils.dedent` +import { verifyTsc, loadProjectFromFiles } from "../../ts"; +import { dedent } from "../../Utils"; +describe("unittests:: tsc:: composite::", () => { + verifyTsc({ + scenario: "composite", + subScenario: "when setting composite false on command line", + fs: () => loadProjectFromFiles({ + "/src/project/src/main.ts": "export const x = 10;", + "/src/project/tsconfig.json": dedent ` { "compilerOptions": { "target": "es5", @@ -16,16 +17,15 @@ namespace ts { "src/**/*.ts" ] }`, - }), - commandLineArgs: ["--composite", "false", "--p", "src/project"], - }); - - verifyTsc({ - scenario: "composite", - subScenario: "when setting composite null on command line", - fs: () => loadProjectFromFiles({ - "/src/project/src/main.ts": "export const x = 10;", - "/src/project/tsconfig.json": Utils.dedent` + }), + commandLineArgs: ["--composite", "false", "--p", "src/project"], + }); + verifyTsc({ + scenario: "composite", + subScenario: "when setting composite null on command line", + fs: () => loadProjectFromFiles({ + "/src/project/src/main.ts": "export const x = 10;", + "/src/project/tsconfig.json": dedent ` { "compilerOptions": { "target": "es5", @@ -36,16 +36,15 @@ namespace ts { "src/**/*.ts" ] }`, - }), - commandLineArgs: ["--composite", "null", "--p", "src/project"], - }); - - verifyTsc({ - scenario: "composite", - subScenario: "when setting composite false on command line but has tsbuild info in config", - fs: () => loadProjectFromFiles({ - "/src/project/src/main.ts": "export const x = 10;", - "/src/project/tsconfig.json": Utils.dedent` + }), + commandLineArgs: ["--composite", "null", "--p", "src/project"], + }); + verifyTsc({ + scenario: "composite", + subScenario: "when setting composite false on command line but has tsbuild info in config", + fs: () => loadProjectFromFiles({ + "/src/project/src/main.ts": "export const x = 10;", + "/src/project/tsconfig.json": dedent ` { "compilerOptions": { "target": "es5", @@ -57,16 +56,15 @@ namespace ts { "src/**/*.ts" ] }`, - }), - commandLineArgs: ["--composite", "false", "--p", "src/project"], - }); - - verifyTsc({ - scenario: "composite", - subScenario: "when setting composite false and tsbuildinfo as null on command line but has tsbuild info in config", - fs: () => loadProjectFromFiles({ - "/src/project/src/main.ts": "export const x = 10;", - "/src/project/tsconfig.json": Utils.dedent` + }), + commandLineArgs: ["--composite", "false", "--p", "src/project"], + }); + verifyTsc({ + scenario: "composite", + subScenario: "when setting composite false and tsbuildinfo as null on command line but has tsbuild info in config", + fs: () => loadProjectFromFiles({ + "/src/project/src/main.ts": "export const x = 10;", + "/src/project/tsconfig.json": dedent ` { "compilerOptions": { "target": "es5", @@ -78,8 +76,7 @@ namespace ts { "src/**/*.ts" ] }`, - }), - commandLineArgs: ["--composite", "false", "--p", "src/project", "--tsBuildInfoFile", "null"], - }); + }), + commandLineArgs: ["--composite", "false", "--p", "src/project", "--tsBuildInfoFile", "null"], }); -} +}); diff --git a/src/testRunner/unittests/tsc/declarationEmit.ts b/src/testRunner/unittests/tsc/declarationEmit.ts index bad536ed60889..8453c172dcc6c 100644 --- a/src/testRunner/unittests/tsc/declarationEmit.ts +++ b/src/testRunner/unittests/tsc/declarationEmit.ts @@ -1,15 +1,17 @@ -namespace ts { - describe("unittests:: tsc:: declarationEmit::", () => { - verifyTsc({ - scenario: "declarationEmit", - subScenario: "when same version is referenced through source and another symlinked package", - fs: () => { - const fsaPackageJson = Utils.dedent` +import { verifyTsc, loadProjectFromFiles } from "../../ts"; +import { dedent } from "../../Utils"; +import { Symlink } from "../../vfs"; +describe("unittests:: tsc:: declarationEmit::", () => { + verifyTsc({ + scenario: "declarationEmit", + subScenario: "when same version is referenced through source and another symlinked package", + fs: () => { + const fsaPackageJson = dedent ` { "name": "typescript-fsa", "version": "3.0.0-beta-2" }`; - const fsaIndex = Utils.dedent` + const fsaIndex = dedent ` export interface Action { type: string; payload: Payload; @@ -23,8 +25,8 @@ namespace ts { } export declare function actionCreatorFactory(prefix?: string | null): ActionCreatorFactory; export default actionCreatorFactory;`; - return loadProjectFromFiles({ - "/src/plugin-two/index.d.ts": Utils.dedent` + return loadProjectFromFiles({ + "/src/plugin-two/index.d.ts": dedent ` declare const _default: { features: { featureOne: { @@ -46,37 +48,36 @@ namespace ts { }; }; export default _default;`, - "/src/plugin-two/node_modules/typescript-fsa/package.json": fsaPackageJson, - "/src/plugin-two/node_modules/typescript-fsa/index.d.ts": fsaIndex, - "/src/plugin-one/tsconfig.json": Utils.dedent` + "/src/plugin-two/node_modules/typescript-fsa/package.json": fsaPackageJson, + "/src/plugin-two/node_modules/typescript-fsa/index.d.ts": fsaIndex, + "/src/plugin-one/tsconfig.json": dedent ` { "compilerOptions": { "target": "es5", "declaration": true, }, }`, - "/src/plugin-one/index.ts": Utils.dedent` + "/src/plugin-one/index.ts": dedent ` import pluginTwo from "plugin-two"; // include this to add reference to symlink`, - "/src/plugin-one/action.ts": Utils.dedent` + "/src/plugin-one/action.ts": dedent ` import { actionCreatorFactory } from "typescript-fsa"; // Include version of shared lib const action = actionCreatorFactory("somekey"); const featureOne = action<{ route: string }>("feature-one"); export const actions = { featureOne };`, - "/src/plugin-one/node_modules/typescript-fsa/package.json": fsaPackageJson, - "/src/plugin-one/node_modules/typescript-fsa/index.d.ts": fsaIndex, - "/src/plugin-one/node_modules/plugin-two": new vfs.Symlink("/src/plugin-two"), - }); - }, - commandLineArgs: ["-p", "src/plugin-one", "--listFiles"] - }); - - verifyTsc({ - scenario: "declarationEmit", - subScenario: "when pkg references sibling package through indirect symlink", - fs: () => loadProjectFromFiles({ - "/src/pkg1/dist/index.d.ts": Utils.dedent` + "/src/plugin-one/node_modules/typescript-fsa/package.json": fsaPackageJson, + "/src/plugin-one/node_modules/typescript-fsa/index.d.ts": fsaIndex, + "/src/plugin-one/node_modules/plugin-two": new Symlink("/src/plugin-two"), + }); + }, + commandLineArgs: ["-p", "src/plugin-one", "--listFiles"] + }); + verifyTsc({ + scenario: "declarationEmit", + subScenario: "when pkg references sibling package through indirect symlink", + fs: () => loadProjectFromFiles({ + "/src/pkg1/dist/index.d.ts": dedent ` export * from './types';`, - "/src/pkg1/dist/types.d.ts": Utils.dedent` + "/src/pkg1/dist/types.d.ts": dedent ` export declare type A = { id: string; }; @@ -90,7 +91,7 @@ namespace ts { toString(): string; static create(key: string): MetadataAccessor; }`, - "/src/pkg1/package.json": Utils.dedent` + "/src/pkg1/package.json": dedent ` { "name": "@raymondfeng/pkg1", "version": "1.0.0", @@ -98,11 +99,11 @@ namespace ts { "main": "dist/index.js", "typings": "dist/index.d.ts" }`, - "/src/pkg2/dist/index.d.ts": Utils.dedent` + "/src/pkg2/dist/index.d.ts": dedent ` export * from './types';`, - "/src/pkg2/dist/types.d.ts": Utils.dedent` + "/src/pkg2/dist/types.d.ts": dedent ` export {MetadataAccessor} from '@raymondfeng/pkg1';`, - "/src/pkg2/package.json": Utils.dedent` + "/src/pkg2/package.json": dedent ` { "name": "@raymondfeng/pkg2", "version": "1.0.0", @@ -110,12 +111,12 @@ namespace ts { "main": "dist/index.js", "typings": "dist/index.d.ts" }`, - "/src/pkg3/src/index.ts": Utils.dedent` + "/src/pkg3/src/index.ts": dedent ` export * from './keys';`, - "/src/pkg3/src/keys.ts": Utils.dedent` + "/src/pkg3/src/keys.ts": dedent ` import {MetadataAccessor} from "@raymondfeng/pkg2"; export const ADMIN = MetadataAccessor.create('1');`, - "/src/pkg3/tsconfig.json": Utils.dedent` + "/src/pkg3/tsconfig.json": dedent ` { "compilerOptions": { "outDir": "dist", @@ -127,10 +128,9 @@ namespace ts { "declaration": true } }`, - "/src/pkg2/node_modules/@raymondfeng/pkg1": new vfs.Symlink("/src/pkg1"), - "/src/pkg3/node_modules/@raymondfeng/pkg2": new vfs.Symlink("/src/pkg2"), - }), - commandLineArgs: ["-p", "src/pkg3", "--listFiles"] - }); + "/src/pkg2/node_modules/@raymondfeng/pkg1": new Symlink("/src/pkg1"), + "/src/pkg3/node_modules/@raymondfeng/pkg2": new Symlink("/src/pkg2"), + }), + commandLineArgs: ["-p", "src/pkg3", "--listFiles"] }); -} +}); diff --git a/src/testRunner/unittests/tsc/helpers.ts b/src/testRunner/unittests/tsc/helpers.ts index e2a38981ed280..994a2778568ab 100644 --- a/src/testRunner/unittests/tsc/helpers.ts +++ b/src/testRunner/unittests/tsc/helpers.ts @@ -1,149 +1,130 @@ -namespace ts { - export type TscCompileSystem = fakes.System & { - writtenFiles: Map; - baseLine(): void; - }; - - export enum BuildKind { - Initial = "initial-build", - IncrementalDtsChange = "incremental-declaration-changes", - IncrementalDtsUnchanged = "incremental-declaration-doesnt-change", - IncrementalHeadersChange = "incremental-headers-change-without-dts-changes", - NoChangeRun ="no-change-run" - } - - export const noChangeRun: TscIncremental = { - buildKind: BuildKind.NoChangeRun, - modifyFs: noop - }; - - export interface TscCompile { - scenario: string; - subScenario: string; - buildKind?: BuildKind; // Should be defined for tsc --b - fs: () => vfs.FileSystem; - commandLineArgs: readonly string[]; - - modifyFs?: (fs: vfs.FileSystem) => void; - baselineSourceMap?: boolean; - baselineReadFileCalls?: boolean; - } - - export type CommandLineProgram = [Program, EmitAndSemanticDiagnosticsBuilderProgram?]; - export interface CommandLineCallbacks { - cb: ExecuteCommandLineCallbacks; - getPrograms: () => readonly CommandLineProgram[]; - } - - function isBuilderProgram(program: Program | EmitAndSemanticDiagnosticsBuilderProgram): program is EmitAndSemanticDiagnosticsBuilderProgram { - return !!(program as EmitAndSemanticDiagnosticsBuilderProgram).getState; - } - function isAnyProgram(program: Program | EmitAndSemanticDiagnosticsBuilderProgram | ParsedCommandLine): program is Program | EmitAndSemanticDiagnosticsBuilderProgram { - return !!(program as Program | EmitAndSemanticDiagnosticsBuilderProgram).getCompilerOptions; - } - export function commandLineCallbacks( - sys: System & { writtenFiles: Map; }, - originalReadCall?: System["readFile"] - ): CommandLineCallbacks { - let programs: CommandLineProgram[] | undefined; - - return { - cb: program => { - if (isAnyProgram(program)) { - baselineBuildInfo(program.getCompilerOptions(), sys, originalReadCall); - (programs || (programs = [])).push(isBuilderProgram(program) ? - [program.getProgram(), program] : - [program] - ); - } - else { - baselineBuildInfo(program.options, sys, originalReadCall); - } - }, - getPrograms: () => { - const result = programs || emptyArray; - programs = undefined; - return result; +import { System, patchHostForBuildInfoReadWrite } from "../../fakes"; +import { TscIncremental, noop, Program, EmitAndSemanticDiagnosticsBuilderProgram, ExecuteCommandLineCallbacks, ParsedCommandLine, baselineBuildInfo, emptyArray, createMap, MapLike, getProperty, executeCommandLine, ExitStatus, generateSourceMapBaselineFiles, isBuild, getFsWithTime } from "../../ts"; +import { FileSystem, formatPatch } from "../../vfs"; +import { Baseline } from "../../Harness"; +import * as ts from "../../ts"; +export type TscCompileSystem = System & { + writtenFiles: ts.Map; + baseLine(): void; +}; +export enum BuildKind { + Initial = "initial-build", + IncrementalDtsChange = "incremental-declaration-changes", + IncrementalDtsUnchanged = "incremental-declaration-doesnt-change", + IncrementalHeadersChange = "incremental-headers-change-without-dts-changes", + NoChangeRun = "no-change-run" +} +export const noChangeRun: TscIncremental = { + buildKind: BuildKind.NoChangeRun, + modifyFs: noop +}; +export interface TscCompile { + scenario: string; + subScenario: string; + buildKind?: BuildKind; // Should be defined for tsc --b + fs: () => FileSystem; + commandLineArgs: readonly string[]; + modifyFs?: (fs: FileSystem) => void; + baselineSourceMap?: boolean; + baselineReadFileCalls?: boolean; +} +export type CommandLineProgram = [Program, EmitAndSemanticDiagnosticsBuilderProgram?]; +export interface CommandLineCallbacks { + cb: ExecuteCommandLineCallbacks; + getPrograms: () => readonly CommandLineProgram[]; +} +function isBuilderProgram(program: Program | EmitAndSemanticDiagnosticsBuilderProgram): program is EmitAndSemanticDiagnosticsBuilderProgram { + return !!(program as EmitAndSemanticDiagnosticsBuilderProgram).getState; +} +function isAnyProgram(program: Program | EmitAndSemanticDiagnosticsBuilderProgram | ParsedCommandLine): program is Program | EmitAndSemanticDiagnosticsBuilderProgram { + return !!(program as Program | EmitAndSemanticDiagnosticsBuilderProgram).getCompilerOptions; +} +export function commandLineCallbacks(sys: ts.System & { + writtenFiles: ts.Map; +}, originalReadCall?: ts.System["readFile"]): CommandLineCallbacks { + let programs: CommandLineProgram[] | undefined; + return { + cb: program => { + if (isAnyProgram(program)) { + baselineBuildInfo(program.getCompilerOptions(), sys, originalReadCall); + (programs || (programs = [])).push(isBuilderProgram(program) ? + [program.getProgram(), program] : + [program]); } - }; - } - - export function tscCompile(input: TscCompile) { - const baseFs = input.fs(); - const fs = baseFs.shadow(); - const { - scenario, subScenario, buildKind, - commandLineArgs, modifyFs, - baselineSourceMap, baselineReadFileCalls - } = input; - if (modifyFs) modifyFs(fs); - - // Create system - const sys = new fakes.System(fs, { executingFilePath: "/lib/tsc" }) as TscCompileSystem; - fakes.patchHostForBuildInfoReadWrite(sys); - const writtenFiles = sys.writtenFiles = createMap(); - const originalWriteFile = sys.writeFile; - sys.writeFile = (fileName, content, writeByteOrderMark) => { - assert.isFalse(writtenFiles.has(fileName)); - writtenFiles.set(fileName, true); - return originalWriteFile.call(sys, fileName, content, writeByteOrderMark); - }; - const actualReadFileMap: MapLike = {}; - const originalReadFile = sys.readFile; - sys.readFile = path => { - // Dont record libs - if (path.startsWith("/src/")) { - actualReadFileMap[path] = (getProperty(actualReadFileMap, path) || 0) + 1; + else { + baselineBuildInfo(program.options, sys, originalReadCall); } - return originalReadFile.call(sys, path); - }; - - sys.write(`${sys.getExecutingFilePath()} ${commandLineArgs.join(" ")}\n`); - sys.exit = exitCode => sys.exitCode = exitCode; - executeCommandLine( - sys, - commandLineCallbacks(sys, originalReadFile).cb, - commandLineArgs, - ); - sys.write(`exitCode:: ExitStatus.${ExitStatus[sys.exitCode as ExitStatus]}\n`); - if (baselineReadFileCalls) { - sys.write(`readFiles:: ${JSON.stringify(actualReadFileMap, /*replacer*/ undefined, " ")} `); + }, + getPrograms: () => { + const result = programs || emptyArray; + programs = undefined; + return result; } - if (baselineSourceMap) generateSourceMapBaselineFiles(sys); - - // Baseline the errors - fs.writeFileSync(`/lib/${buildKind || BuildKind.Initial}Output.txt`, sys.output.join("")); - fs.makeReadonly(); - - sys.baseLine = () => { - const patch = fs.diff(baseFs, { includeChangedFileWithSameContent: true }); - // eslint-disable-next-line no-null/no-null - Harness.Baseline.runBaseline(`${isBuild(commandLineArgs) ? "tsbuild" : "tsc"}/${scenario}/${buildKind || BuildKind.Initial}/${subScenario.split(" ").join("-")}.js`, patch ? vfs.formatPatch(patch) : null); - }; - return sys; - } - - export function verifyTscBaseline(sys: () => TscCompileSystem) { - it(`Generates files matching the baseline`, () => { - sys().baseLine(); - }); + }; +} +export function tscCompile(input: TscCompile) { + const baseFs = input.fs(); + const fs = baseFs.shadow(); + const { scenario, subScenario, buildKind, commandLineArgs, modifyFs, baselineSourceMap, baselineReadFileCalls } = input; + if (modifyFs) + modifyFs(fs); + // Create system + const sys = (new System(fs, { executingFilePath: "/lib/tsc" }) as TscCompileSystem); + patchHostForBuildInfoReadWrite(sys); + const writtenFiles = sys.writtenFiles = createMap(); + const originalWriteFile = sys.writeFile; + sys.writeFile = (fileName, content, writeByteOrderMark) => { + assert.isFalse(writtenFiles.has(fileName)); + writtenFiles.set(fileName, true); + return originalWriteFile.call(sys, fileName, content, writeByteOrderMark); + }; + const actualReadFileMap: MapLike = {}; + const originalReadFile = sys.readFile; + sys.readFile = path => { + // Dont record libs + if (path.startsWith("/src/")) { + actualReadFileMap[path] = (getProperty(actualReadFileMap, path) || 0) + 1; + } + return originalReadFile.call(sys, path); + }; + sys.write(`${sys.getExecutingFilePath()} ${commandLineArgs.join(" ")}\n`); + sys.exit = exitCode => sys.exitCode = exitCode; + executeCommandLine(sys, commandLineCallbacks(sys, originalReadFile).cb, commandLineArgs); + sys.write(`exitCode:: ExitStatus.${ExitStatus[sys.exitCode as ExitStatus]}\n`); + if (baselineReadFileCalls) { + sys.write(`readFiles:: ${JSON.stringify(actualReadFileMap, /*replacer*/ undefined, " ")} `); } - - export function verifyTsc(input: TscCompile) { - describe(input.scenario, () => { - describe(input.subScenario, () => { - let sys: TscCompileSystem; - before(() => { - sys = tscCompile({ - ...input, - fs: () => getFsWithTime(input.fs()).fs.makeReadonly() - }); - }); - after(() => { - sys = undefined!; + if (baselineSourceMap) + generateSourceMapBaselineFiles(sys); + // Baseline the errors + fs.writeFileSync(`/lib/${buildKind || BuildKind.Initial}Output.txt`, sys.output.join("")); + fs.makeReadonly(); + sys.baseLine = () => { + const patch = fs.diff(baseFs, { includeChangedFileWithSameContent: true }); + // eslint-disable-next-line no-null/no-null + Baseline.runBaseline(`${isBuild(commandLineArgs) ? "tsbuild" : "tsc"}/${scenario}/${buildKind || BuildKind.Initial}/${subScenario.split(" ").join("-")}.js`, patch ? formatPatch(patch) : null); + }; + return sys; +} +export function verifyTscBaseline(sys: () => TscCompileSystem) { + it(`Generates files matching the baseline`, () => { + sys().baseLine(); + }); +} +export function verifyTsc(input: TscCompile) { + describe(input.scenario, () => { + describe(input.subScenario, () => { + let sys: TscCompileSystem; + before(() => { + sys = tscCompile({ + ...input, + fs: () => getFsWithTime(input.fs()).fs.makeReadonly() }); - verifyTscBaseline(() => sys); }); + after(() => { + sys = undefined!; + }); + verifyTscBaseline(() => sys); }); - } + }); } diff --git a/src/testRunner/unittests/tsc/incremental.ts b/src/testRunner/unittests/tsc/incremental.ts index 862e5cdf6b458..a267ad12514ff 100644 --- a/src/testRunner/unittests/tsc/incremental.ts +++ b/src/testRunner/unittests/tsc/incremental.ts @@ -1,11 +1,12 @@ -namespace ts { - describe("unittests:: tsc:: incremental::", () => { - verifyTscIncrementalEdits({ - scenario: "incremental", - subScenario: "when passing filename for buildinfo on commandline", - fs: () => loadProjectFromFiles({ - "/src/project/src/main.ts": "export const x = 10;", - "/src/project/tsconfig.json": Utils.dedent` +import { verifyTscIncrementalEdits, loadProjectFromFiles, noChangeRun, BuildKind, appendText, loadProjectFromDisk } from "../../ts"; +import { dedent } from "../../Utils"; +describe("unittests:: tsc:: incremental::", () => { + verifyTscIncrementalEdits({ + scenario: "incremental", + subScenario: "when passing filename for buildinfo on commandline", + fs: () => loadProjectFromFiles({ + "/src/project/src/main.ts": "export const x = 10;", + "/src/project/tsconfig.json": dedent ` { "compilerOptions": { "target": "es5", @@ -15,52 +16,49 @@ namespace ts { "src/**/*.ts" ] }`, - }), - commandLineArgs: ["--incremental", "--p", "src/project", "--tsBuildInfoFile", "src/project/.tsbuildinfo"], - incrementalScenarios: [noChangeRun] - }); - - verifyTscIncrementalEdits({ - scenario: "incremental", - subScenario: "when passing rootDir from commandline", - fs: () => loadProjectFromFiles({ - "/src/project/src/main.ts": "export const x = 10;", - "/src/project/tsconfig.json": Utils.dedent` + }), + commandLineArgs: ["--incremental", "--p", "src/project", "--tsBuildInfoFile", "src/project/.tsbuildinfo"], + incrementalScenarios: [noChangeRun] + }); + verifyTscIncrementalEdits({ + scenario: "incremental", + subScenario: "when passing rootDir from commandline", + fs: () => loadProjectFromFiles({ + "/src/project/src/main.ts": "export const x = 10;", + "/src/project/tsconfig.json": dedent ` { "compilerOptions": { "incremental": true, "outDir": "dist", }, }`, - }), - commandLineArgs: ["--p", "src/project", "--rootDir", "src/project/src"], - incrementalScenarios: [noChangeRun] - }); - - verifyTscIncrementalEdits({ - scenario: "incremental", - subScenario: "with only dts files", - fs: () => loadProjectFromFiles({ - "/src/project/src/main.d.ts": "export const x = 10;", - "/src/project/src/another.d.ts": "export const y = 10;", - "/src/project/tsconfig.json": "{}", - }), - commandLineArgs: ["--incremental", "--p", "src/project"], - incrementalScenarios: [ - noChangeRun, - { - buildKind: BuildKind.IncrementalDtsUnchanged, - modifyFs: fs => appendText(fs, "/src/project/src/main.d.ts", "export const xy = 100;") - } - ] - }); - - verifyTscIncrementalEdits({ - scenario: "incremental", - subScenario: "when passing rootDir is in the tsconfig", - fs: () => loadProjectFromFiles({ - "/src/project/src/main.ts": "export const x = 10;", - "/src/project/tsconfig.json": Utils.dedent` + }), + commandLineArgs: ["--p", "src/project", "--rootDir", "src/project/src"], + incrementalScenarios: [noChangeRun] + }); + verifyTscIncrementalEdits({ + scenario: "incremental", + subScenario: "with only dts files", + fs: () => loadProjectFromFiles({ + "/src/project/src/main.d.ts": "export const x = 10;", + "/src/project/src/another.d.ts": "export const y = 10;", + "/src/project/tsconfig.json": "{}", + }), + commandLineArgs: ["--incremental", "--p", "src/project"], + incrementalScenarios: [ + noChangeRun, + { + buildKind: BuildKind.IncrementalDtsUnchanged, + modifyFs: fs => appendText(fs, "/src/project/src/main.d.ts", "export const xy = 100;") + } + ] + }); + verifyTscIncrementalEdits({ + scenario: "incremental", + subScenario: "when passing rootDir is in the tsconfig", + fs: () => loadProjectFromFiles({ + "/src/project/src/main.ts": "export const x = 10;", + "/src/project/tsconfig.json": dedent ` { "compilerOptions": { "incremental": true, @@ -68,25 +66,23 @@ namespace ts { "rootDir": "./" }, }`, - }), - commandLineArgs: ["--p", "src/project"], - incrementalScenarios: [noChangeRun] - }); - - verifyTscIncrementalEdits({ - scenario: "incremental", - subScenario: "with noEmitOnError", - fs: () => loadProjectFromDisk("tests/projects/noEmitOnError"), - commandLineArgs: ["--incremental", "-p", "src"], - incrementalScenarios: [ - { - buildKind: BuildKind.IncrementalDtsUnchanged, - modifyFs: fs => fs.writeFileSync("/src/src/main.ts", `import { A } from "../shared/types/db"; + }), + commandLineArgs: ["--p", "src/project"], + incrementalScenarios: [noChangeRun] + }); + verifyTscIncrementalEdits({ + scenario: "incremental", + subScenario: "with noEmitOnError", + fs: () => loadProjectFromDisk("tests/projects/noEmitOnError"), + commandLineArgs: ["--incremental", "-p", "src"], + incrementalScenarios: [ + { + buildKind: BuildKind.IncrementalDtsUnchanged, + modifyFs: fs => fs.writeFileSync("/src/src/main.ts", `import { A } from "../shared/types/db"; const a = { lastName: 'sdsd' };`, "utf-8") - } - ] - }); + } + ] }); -} +}); diff --git a/src/testRunner/unittests/tsc/listFilesOnly.ts b/src/testRunner/unittests/tsc/listFilesOnly.ts index 97c9bd7d5e43c..9aeca627cee5a 100644 --- a/src/testRunner/unittests/tsc/listFilesOnly.ts +++ b/src/testRunner/unittests/tsc/listFilesOnly.ts @@ -1,23 +1,22 @@ -namespace ts { - describe("unittests:: tsc:: listFilesOnly::", () => { - verifyTsc({ - scenario: "listFilesOnly", - subScenario: "combined with watch", - fs: () => loadProjectFromFiles({ - "/src/test.ts": Utils.dedent` +import { verifyTsc, loadProjectFromFiles } from "../../ts"; +import { dedent } from "../../Utils"; +describe("unittests:: tsc:: listFilesOnly::", () => { + verifyTsc({ + scenario: "listFilesOnly", + subScenario: "combined with watch", + fs: () => loadProjectFromFiles({ + "/src/test.ts": dedent ` export const x = 1;`, - }), - commandLineArgs: ["/src/test.ts", "--watch", "--listFilesOnly"] - }); - - verifyTsc({ - scenario: "listFilesOnly", - subScenario: "loose file", - fs: () => loadProjectFromFiles({ - "/src/test.ts": Utils.dedent` + }), + commandLineArgs: ["/src/test.ts", "--watch", "--listFilesOnly"] + }); + verifyTsc({ + scenario: "listFilesOnly", + subScenario: "loose file", + fs: () => loadProjectFromFiles({ + "/src/test.ts": dedent ` export const x = 1;`, - }), - commandLineArgs: ["/src/test.ts", "--listFilesOnly"] - }); + }), + commandLineArgs: ["/src/test.ts", "--listFilesOnly"] }); -} +}); diff --git a/src/testRunner/unittests/tsc/runWithoutArgs.ts b/src/testRunner/unittests/tsc/runWithoutArgs.ts index ed53a16229490..ef75c837e8c0a 100644 --- a/src/testRunner/unittests/tsc/runWithoutArgs.ts +++ b/src/testRunner/unittests/tsc/runWithoutArgs.ts @@ -1,10 +1,9 @@ -namespace ts { - describe("unittests:: tsc:: runWithoutArgs::", () => { - verifyTsc({ - scenario: "runWithoutArgs", - subScenario: "show help with ExitStatus.DiagnosticsPresent_OutputsSkipped", - fs: () => loadProjectFromFiles({}), - commandLineArgs: [] - }); +import { verifyTsc, loadProjectFromFiles } from "../../ts"; +describe("unittests:: tsc:: runWithoutArgs::", () => { + verifyTsc({ + scenario: "runWithoutArgs", + subScenario: "show help with ExitStatus.DiagnosticsPresent_OutputsSkipped", + fs: () => loadProjectFromFiles({}), + commandLineArgs: [] }); -} +}); diff --git a/src/testRunner/unittests/tscWatch/consoleClearing.ts b/src/testRunner/unittests/tscWatch/consoleClearing.ts index 27fffcd6e1272..bd453cff7539a 100644 --- a/src/testRunner/unittests/tscWatch/consoleClearing.ts +++ b/src/testRunner/unittests/tscWatch/consoleClearing.ts @@ -1,69 +1,63 @@ -namespace ts.tscWatch { - describe("unittests:: tsc-watch:: console clearing", () => { - const scenario = "consoleClearing"; - const file: File = { - path: "/f.ts", - content: "" +import { File, WatchedSystem, verifyTscWatch, createWatchedSystem, libFile, createWatchOfConfigFile, runWatchBaseline } from "../../ts.tscWatch"; +import { emptyArray, CompilerOptions, TestFSWithWatch } from "../../ts"; +describe("unittests:: tsc-watch:: console clearing", () => { + const scenario = "consoleClearing"; + const file: File = { + path: "/f.ts", + content: "" + }; + function makeChangeToFile(sys: WatchedSystem) { + sys.modifyFile(file.path, "//"); + sys.runQueuedTimeoutCallbacks(); + return "Comment added to file f"; + } + function checkConsoleClearingUsingCommandLineOptions(subScenario: string, commandLineOptions?: string[]) { + verifyTscWatch({ + scenario, + subScenario, + commandLineArgs: ["--w", file.path, ...commandLineOptions || emptyArray], + sys: () => createWatchedSystem([file, libFile]), + changes: [ + makeChangeToFile + ], + }); + } + checkConsoleClearingUsingCommandLineOptions("without --diagnostics or --extendedDiagnostics"); + checkConsoleClearingUsingCommandLineOptions("with --diagnostics", ["--diagnostics"]); + checkConsoleClearingUsingCommandLineOptions("with --extendedDiagnostics", ["--extendedDiagnostics"]); + checkConsoleClearingUsingCommandLineOptions("with --preserveWatchOutput", ["--preserveWatchOutput"]); + describe("when preserveWatchOutput is true in config file", () => { + const compilerOptions: CompilerOptions = { + preserveWatchOutput: true }; - - function makeChangeToFile(sys: WatchedSystem) { - sys.modifyFile(file.path, "//"); - sys.runQueuedTimeoutCallbacks(); - return "Comment added to file f"; - } - - function checkConsoleClearingUsingCommandLineOptions(subScenario: string, commandLineOptions?: string[]) { - verifyTscWatch({ - scenario, - subScenario, - commandLineArgs: ["--w", file.path, ...commandLineOptions || emptyArray], - sys: () => createWatchedSystem([file, libFile]), - changes: [ - makeChangeToFile - ], - }); - } - - checkConsoleClearingUsingCommandLineOptions("without --diagnostics or --extendedDiagnostics"); - checkConsoleClearingUsingCommandLineOptions("with --diagnostics", ["--diagnostics"]); - checkConsoleClearingUsingCommandLineOptions("with --extendedDiagnostics", ["--extendedDiagnostics"]); - checkConsoleClearingUsingCommandLineOptions("with --preserveWatchOutput", ["--preserveWatchOutput"]); - - describe("when preserveWatchOutput is true in config file", () => { - const compilerOptions: CompilerOptions = { - preserveWatchOutput: true - }; - const configFile: File = { - path: "/tsconfig.json", - content: JSON.stringify({ compilerOptions }) - }; - const files = [file, configFile, libFile]; - it("using createWatchOfConfigFile ", () => { - const sys = TestFSWithWatch.changeToHostTrackingWrittenFiles( - createWatchedSystem(files) - ); - const watch = createWatchOfConfigFile(configFile.path, sys); - // Initially console is cleared if --preserveOutput is not provided since the config file is yet to be parsed - runWatchBaseline({ - scenario, - subScenario: "when preserveWatchOutput is true in config file/createWatchOfConfigFile", - commandLineArgs: ["--w", "-p", configFile.path], - sys, - getPrograms: () => [[watch(), watch.getBuilderProgram()]], - changes: [ - makeChangeToFile - ] - }); - }); - verifyTscWatch({ + const configFile: File = { + path: "/tsconfig.json", + content: JSON.stringify({ compilerOptions }) + }; + const files = [file, configFile, libFile]; + it("using createWatchOfConfigFile ", () => { + const sys = TestFSWithWatch.changeToHostTrackingWrittenFiles(createWatchedSystem(files)); + const watch = createWatchOfConfigFile(configFile.path, sys); + // Initially console is cleared if --preserveOutput is not provided since the config file is yet to be parsed + runWatchBaseline({ scenario, - subScenario: "when preserveWatchOutput is true in config file/when createWatchProgram is invoked with configFileParseResult on WatchCompilerHostOfConfigFile", + subScenario: "when preserveWatchOutput is true in config file/createWatchOfConfigFile", commandLineArgs: ["--w", "-p", configFile.path], - sys: () => createWatchedSystem(files), + sys, + getPrograms: () => [[watch(), watch.getBuilderProgram()]], changes: [ makeChangeToFile - ], + ] }); }); + verifyTscWatch({ + scenario, + subScenario: "when preserveWatchOutput is true in config file/when createWatchProgram is invoked with configFileParseResult on WatchCompilerHostOfConfigFile", + commandLineArgs: ["--w", "-p", configFile.path], + sys: () => createWatchedSystem(files), + changes: [ + makeChangeToFile + ], + }); }); -} +}); diff --git a/src/testRunner/unittests/tscWatch/emit.ts b/src/testRunner/unittests/tscWatch/emit.ts index 98be86a0f7392..3bac33f723d2a 100644 --- a/src/testRunner/unittests/tscWatch/emit.ts +++ b/src/testRunner/unittests/tscWatch/emit.ts @@ -1,511 +1,470 @@ -namespace ts.tscWatch { - const scenario = "emit"; - describe("unittests:: tsc-watch:: emit with outFile or out setting", () => { - function verifyOutAndOutFileSetting(subScenario: string, out?: string, outFile?: string) { - verifyTscWatch({ - scenario, - subScenario: `emit with outFile or out setting/${subScenario}`, - commandLineArgs: ["--w", "-p", "/a/tsconfig.json"], - sys: () => { - const config: File = { - path: "/a/tsconfig.json", - content: JSON.stringify({ compilerOptions: { out, outFile } }) - }; - const f1: File = { - path: "/a/a.ts", - content: "let x = 1" - }; - const f2: File = { - path: "/a/b.ts", - content: "let y = 1" - }; - return createWatchedSystem([f1, f2, config, libFile]); - }, - changes: [ - sys => { - sys.writeFile("/a/a.ts", "let x = 11"); - sys.runQueuedTimeoutCallbacks(); - return "Make change in the file"; - } - ] - }); - } - verifyOutAndOutFileSetting("config does not have out or outFile"); - verifyOutAndOutFileSetting("config has out", "/a/out.js"); - verifyOutAndOutFileSetting("config has outFile", /*out*/ undefined, "/a/out.js"); - - function verifyFilesEmittedOnce(subScenario: string, useOutFile: boolean) { - verifyTscWatch({ - scenario, - subScenario: `emit with outFile or out setting/${subScenario}`, - commandLineArgs: ["--w", "-p", "/a/b/project/tsconfig.json"], - sys: () => { - const file1: File = { - path: "/a/b/output/AnotherDependency/file1.d.ts", - content: "declare namespace Common.SomeComponent.DynamicMenu { enum Z { Full = 0, Min = 1, Average = 2, } }" - }; - const file2: File = { - path: "/a/b/dependencies/file2.d.ts", - content: "declare namespace Dependencies.SomeComponent { export class SomeClass { version: string; } }" - }; - const file3: File = { - path: "/a/b/project/src/main.ts", - content: "namespace Main { export function fooBar() {} }" - }; - const file4: File = { - path: "/a/b/project/src/main2.ts", - content: "namespace main.file4 { import DynamicMenu = Common.SomeComponent.DynamicMenu; export function foo(a: DynamicMenu.z) { } }" - }; - const configFile: File = { - path: "/a/b/project/tsconfig.json", - content: JSON.stringify({ - compilerOptions: useOutFile ? - { outFile: "../output/common.js", target: "es5" } : - { outDir: "../output", target: "es5" }, - files: [file1.path, file2.path, file3.path, file4.path] - }) - }; - return createWatchedSystem([file1, file2, file3, file4, libFile, configFile]); - }, - changes: emptyArray - }); - } - verifyFilesEmittedOnce("with --outFile and multiple declaration files in the program", /*useOutFile*/ true); - verifyFilesEmittedOnce("without --outFile and multiple declaration files in the program", /*useOutFile*/ false); - }); - - describe("unittests:: tsc-watch:: emit for configured projects", () => { - const file1Consumer1Path = "/a/b/file1Consumer1.ts"; - const file1Consumer2Path = "/a/b/file1Consumer2.ts"; - const moduleFile1Path = "/a/b/moduleFile1.ts"; - const moduleFile2Path = "/a/b/moduleFile2.ts"; - const globalFilePath = "/a/b/globalFile3.ts"; - const configFilePath = "/a/b/tsconfig.json"; - interface VerifyTscWatchEmit { - subScenario: string; - /** custom config file options */ - configObj?: any; - /** Additional files and folders to add */ - getAdditionalFileOrFolder?: () => File[]; - /** initial list of files to emit if not the default list */ - firstReloadFileList?: string[]; - changes: TscWatchCompileChange[] - } - function verifyTscWatchEmit({ - subScenario, - configObj, - getAdditionalFileOrFolder, - firstReloadFileList, - changes - }: VerifyTscWatchEmit) { - verifyTscWatch({ - scenario, - subScenario: `emit for configured projects/${subScenario}`, - commandLineArgs: ["--w", "-p", configFilePath], - sys: () => { - const moduleFile1: File = { - path: moduleFile1Path, - content: "export function Foo() { };", - }; - - const file1Consumer1: File = { - path: file1Consumer1Path, - content: `import {Foo} from "./moduleFile1"; export var y = 10;`, - }; - - const file1Consumer2: File = { - path: file1Consumer2Path, - content: `import {Foo} from "./moduleFile1"; let z = 10;`, - }; - - const moduleFile2: File = { - path: moduleFile2Path, - content: `export var Foo4 = 10;`, - }; - - const globalFile3: File = { - path: globalFilePath, - content: `interface GlobalFoo { age: number }` - }; - const configFile: File = { - path: configFilePath, - content: JSON.stringify(configObj || {}) - }; - const additionalFiles = getAdditionalFileOrFolder?.() || emptyArray; - const files = [moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile, ...additionalFiles]; - return createWatchedSystem(firstReloadFileList ? - map(firstReloadFileList, fileName => find(files, file => file.path === fileName)!) : - files - ); - }, - changes - }); - } - - function modifyModuleFile1Shape(sys: WatchedSystem) { - sys.writeFile(moduleFile1Path, `export var T: number;export function Foo() { };`); - } - function changeModuleFile1Shape(sys: WatchedSystem) { - modifyModuleFile1Shape(sys); - sys.checkTimeoutQueueLengthAndRun(1); - return "Change the content of moduleFile1 to `export var T: number;export function Foo() { };`"; - } - - verifyTscWatchEmit({ - subScenario: "should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", - changes: [ - changeModuleFile1Shape, - sys => { - sys.writeFile(moduleFile1Path, `export var T: number;export function Foo() { console.log('hi'); };`); - sys.checkTimeoutQueueLengthAndRun(1); - return "Change the content of moduleFile1 to `export var T: number;export function Foo() { console.log('hi'); };`"; - } - ] - }); - - verifyTscWatchEmit({ - subScenario: "should be up-to-date with the reference map changes", - changes: [ - sys => { - sys.writeFile(file1Consumer1Path, `export let y = Foo();`); - sys.checkTimeoutQueueLengthAndRun(1); - return "Change file1Consumer1 content to `export let y = Foo();`"; - }, - changeModuleFile1Shape, - sys => { - sys.writeFile(file1Consumer1Path, `import {Foo} from "./moduleFile1";let y = Foo();`); - sys.checkTimeoutQueueLengthAndRun(1); - return "Add the import statements back to file1Consumer1"; - }, - sys => { - sys.writeFile(moduleFile1Path, `export let y = Foo();`); - sys.checkTimeoutQueueLengthAndRun(1); - return "Change the content of moduleFile1 to `export var T: number;export var T2: string;export function Foo() { };`"; - }, - sys => { - // Change file1Consumer1 content to `export let y = Foo();` - // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` - sys.writeFile(file1Consumer1Path, `import {Foo} from "./moduleFile1";let y = Foo();`); - modifyModuleFile1Shape(sys); - sys.checkTimeoutQueueLengthAndRun(1); - return "Multiple file edits in one go"; - } - ] - }); - - verifyTscWatchEmit({ - subScenario: "should be up-to-date with deleted files", - changes: [ - sys => { - modifyModuleFile1Shape(sys); - sys.deleteFile(file1Consumer2Path); - sys.checkTimeoutQueueLengthAndRun(1); - return "change moduleFile1 shape and delete file1Consumer2"; - } - ] - }); - - verifyTscWatchEmit({ - subScenario: "should be up-to-date with newly created files", - changes: [ - sys => { - sys.writeFile("/a/b/file1Consumer3.ts", `import {Foo} from "./moduleFile1"; let y = Foo();`); - modifyModuleFile1Shape(sys); - sys.checkTimeoutQueueLengthAndRun(1); - return "change moduleFile1 shape and create file1Consumer3"; - } - ] - }); - - verifyTscWatchEmit({ - subScenario: "should detect changes in non-root files", - configObj: { files: [file1Consumer1Path] }, - changes: [ - changeModuleFile1Shape, - sys => { - sys.appendFile(moduleFile1Path, "var T1: number;"); - sys.checkTimeoutQueueLengthAndRun(1); - return "change file1 internal, and verify only file1 is affected"; - } - ] - }); - - verifyTscWatchEmit({ - subScenario: "should return all files if a global file changed shape", - changes: [ - sys => { - sys.appendFile(globalFilePath, "var T2: string;"); - sys.checkTimeoutQueueLengthAndRun(1); - return "change shape of global file"; - } - ] - }); - - verifyTscWatchEmit({ - subScenario: "should always return the file itself if '--isolatedModules' is specified", - configObj: { compilerOptions: { isolatedModules: true } }, - changes: [ - changeModuleFile1Shape - ] - }); - - verifyTscWatchEmit({ - subScenario: "should always return the file itself if '--out' or '--outFile' is specified", - configObj: { compilerOptions: { module: "system", outFile: "/a/b/out.js" } }, - changes: [ - changeModuleFile1Shape - ] - }); - - verifyTscWatchEmit({ - subScenario: "should return cascaded affected file list", - getAdditionalFileOrFolder: () => [{ - path: "/a/b/file1Consumer1Consumer1.ts", - content: `import {y} from "./file1Consumer1";` - }], - changes: [ - sys => { - sys.appendFile(file1Consumer1Path, "export var T: number;"); - sys.checkTimeoutQueueLengthAndRun(1); - return "change file1Consumer1"; - }, - changeModuleFile1Shape, - sys => { - sys.appendFile(file1Consumer1Path, "export var T2: number;"); - sys.writeFile(moduleFile1Path, `export var T2: number;export function Foo() { };`); - sys.checkTimeoutQueueLengthAndRun(1); - return "change file1Consumer1 and moduleFile1"; - } - ] - }); - - verifyTscWatchEmit({ - subScenario: "should work fine for files with circular references", - getAdditionalFileOrFolder: () => [ - { - path: "/a/b/file1.ts", - content: `/// -export var t1 = 10;` - }, - { - path: "/a/b/file2.ts", - content: `/// -export var t2 = 10;` - } - ], - firstReloadFileList: [libFile.path, "/a/b/file1.ts", "/a/b/file2.ts", configFilePath], - changes: [ - sys => { - sys.appendFile("/a/b/file1.ts", "export var t3 = 10;"); - sys.checkTimeoutQueueLengthAndRun(1); - return "change file1"; - } - ] - }); - - verifyTscWatchEmit({ - subScenario: "should detect removed code file", - getAdditionalFileOrFolder: () => [{ - path: "/a/b/referenceFile1.ts", - content: `/// -export var x = Foo();` - }], - firstReloadFileList: [libFile.path, "/a/b/referenceFile1.ts", moduleFile1Path, configFilePath], - changes: [ - sys => { - sys.deleteFile(moduleFile1Path); - sys.checkTimeoutQueueLengthAndRun(1); - return "delete moduleFile1"; - } - ] - }); - - verifyTscWatchEmit({ - subScenario: "should detect non existing code file", - getAdditionalFileOrFolder: () => [{ - path: "/a/b/referenceFile1.ts", - content: `/// -export var x = Foo();` - }], - firstReloadFileList: [libFile.path, "/a/b/referenceFile1.ts", configFilePath], - changes: [ - sys => { - sys.appendFile("/a/b/referenceFile1.ts", "export var yy = Foo();"); - sys.checkTimeoutQueueLengthAndRun(1); - return "edit refereceFile1"; - }, - sys => { - sys.writeFile(moduleFile2Path, "export var Foo4 = 10;"); - sys.checkTimeoutQueueLengthAndRun(1); - return "create moduleFile2"; - } - ] - }); - }); - - describe("unittests:: tsc-watch:: emit file content", () => { - function verifyNewLine(subScenario: string, newLine: string) { - verifyTscWatch({ - scenario, - subScenario: `emit file content/${subScenario}`, - commandLineArgs: ["--w", "/a/app.ts"], - sys: () => createWatchedSystem( - [ - { - path: "/a/app.ts", - content: ["var x = 1;", "var y = 2;"].join(newLine) - }, - libFile - ], - { newLine } - ), - changes: [ - sys => { - sys.appendFile("/a/app.ts", newLine + "var z = 3;"); - sys.checkTimeoutQueueLengthAndRun(1); - return "Append a line"; - } - ], - }); - } - verifyNewLine("handles new lines lineFeed", "\n"); - verifyNewLine("handles new lines carriageReturn lineFeed", "\r\n"); - +import { verifyTscWatch, File, createWatchedSystem, libFile, TscWatchCompileChange, WatchedSystem } from "../../ts.tscWatch"; +import { emptyArray, map, find } from "../../ts"; +const scenario = "emit"; +describe("unittests:: tsc-watch:: emit with outFile or out setting", () => { + function verifyOutAndOutFileSetting(subScenario: string, out?: string, outFile?: string) { verifyTscWatch({ scenario, - subScenario: "emit file content/should emit specified file", - commandLineArgs: ["-w", "-p", "/a/b/tsconfig.json"], + subScenario: `emit with outFile or out setting/${subScenario}`, + commandLineArgs: ["--w", "-p", "/a/tsconfig.json"], sys: () => { - const file1 = { - path: "/a/b/f1.ts", - content: `export function Foo() { return 10; }` - }; - - const file2 = { - path: "/a/b/f2.ts", - content: `import {Foo} from "./f1"; export let y = Foo();` + const config: File = { + path: "/a/tsconfig.json", + content: JSON.stringify({ compilerOptions: { out, outFile } }) }; - - const file3 = { - path: "/a/b/f3.ts", - content: `import {y} from "./f2"; let x = y;` + const f1: File = { + path: "/a/a.ts", + content: "let x = 1" }; - - const configFile = { - path: "/a/b/tsconfig.json", - content: "{}" + const f2: File = { + path: "/a/b.ts", + content: "let y = 1" }; - return createWatchedSystem([file1, file2, file3, configFile, libFile]); + return createWatchedSystem([f1, f2, config, libFile]); }, changes: [ sys => { - sys.appendFile("/a/b/f1.ts", "export function foo2() { return 2; }"); - sys.checkTimeoutQueueLengthAndRun(1); - return "Append content to f1"; + sys.writeFile("/a/a.ts", "let x = 11"); + sys.runQueuedTimeoutCallbacks(); + return "Make change in the file"; } - ], + ] }); - + } + verifyOutAndOutFileSetting("config does not have out or outFile"); + verifyOutAndOutFileSetting("config has out", "/a/out.js"); + verifyOutAndOutFileSetting("config has outFile", /*out*/ undefined, "/a/out.js"); + function verifyFilesEmittedOnce(subScenario: string, useOutFile: boolean) { verifyTscWatch({ scenario, - subScenario: "emit file content/elides const enums correctly in incremental compilation", - commandLineArgs: ["-w", "/user/someone/projects/myproject/file3.ts"], + subScenario: `emit with outFile or out setting/${subScenario}`, + commandLineArgs: ["--w", "-p", "/a/b/project/tsconfig.json"], sys: () => { - const currentDirectory = "/user/someone/projects/myproject"; const file1: File = { - path: `${currentDirectory}/file1.ts`, - content: "export const enum E1 { V = 1 }" + path: "/a/b/output/AnotherDependency/file1.d.ts", + content: "declare namespace Common.SomeComponent.DynamicMenu { enum Z { Full = 0, Min = 1, Average = 2, } }" }; const file2: File = { - path: `${currentDirectory}/file2.ts`, - content: `import { E1 } from "./file1"; export const enum E2 { V = E1.V }` + path: "/a/b/dependencies/file2.d.ts", + content: "declare namespace Dependencies.SomeComponent { export class SomeClass { version: string; } }" }; const file3: File = { - path: `${currentDirectory}/file3.ts`, - content: `import { E2 } from "./file2"; const v: E2 = E2.V;` + path: "/a/b/project/src/main.ts", + content: "namespace Main { export function fooBar() {} }" }; - return createWatchedSystem([file1, file2, file3, libFile]); - }, - changes: [ - sys => { - sys.appendFile("/user/someone/projects/myproject/file3.ts", "function foo2() { return 2; }"); - sys.checkTimeoutQueueLengthAndRun(1); - return "Append content to file3"; - } - ], - }); - - verifyTscWatch({ - scenario, - subScenario: "emit file content/file is deleted and created as part of change", - commandLineArgs: ["-w"], - sys: () => { - const projectLocation = "/home/username/project"; - const file: File = { - path: `${projectLocation}/app/file.ts`, - content: "var a = 10;" + const file4: File = { + path: "/a/b/project/src/main2.ts", + content: "namespace main.file4 { import DynamicMenu = Common.SomeComponent.DynamicMenu; export function foo(a: DynamicMenu.z) { } }" }; const configFile: File = { - path: `${projectLocation}/tsconfig.json`, + path: "/a/b/project/tsconfig.json", content: JSON.stringify({ - include: [ - "app/**/*.ts" - ] + compilerOptions: useOutFile ? + { outFile: "../output/common.js", target: "es5" } : + { outDir: "../output", target: "es5" }, + files: [file1.path, file2.path, file3.path, file4.path] }) }; - const files = [file, configFile, libFile]; - return createWatchedSystem(files, { currentDirectory: projectLocation, useCaseSensitiveFileNames: true }); + return createWatchedSystem([file1, file2, file3, file4, libFile, configFile]); }, - changes: [ - sys => { - sys.appendFile("/home/username/project/app/file.ts", "\nvar b = 10;", { invokeFileDeleteCreateAsPartInsteadOfChange: true }); - sys.runQueuedTimeoutCallbacks(); - return "file is deleted and then created to modify content"; - } - ] + changes: emptyArray }); - }); - - describe("unittests:: tsc-watch:: emit with when module emit is specified as node", () => { + } + verifyFilesEmittedOnce("with --outFile and multiple declaration files in the program", /*useOutFile*/ true); + verifyFilesEmittedOnce("without --outFile and multiple declaration files in the program", /*useOutFile*/ false); +}); +describe("unittests:: tsc-watch:: emit for configured projects", () => { + const file1Consumer1Path = "/a/b/file1Consumer1.ts"; + const file1Consumer2Path = "/a/b/file1Consumer2.ts"; + const moduleFile1Path = "/a/b/moduleFile1.ts"; + const moduleFile2Path = "/a/b/moduleFile2.ts"; + const globalFilePath = "/a/b/globalFile3.ts"; + const configFilePath = "/a/b/tsconfig.json"; + interface VerifyTscWatchEmit { + subScenario: string; + /** custom config file options */ + configObj?: any; + /** Additional files and folders to add */ + getAdditionalFileOrFolder?: () => File[]; + /** initial list of files to emit if not the default list */ + firstReloadFileList?: string[]; + changes: TscWatchCompileChange[]; + } + function verifyTscWatchEmit({ subScenario, configObj, getAdditionalFileOrFolder, firstReloadFileList, changes }: VerifyTscWatchEmit) { verifyTscWatch({ scenario, - subScenario: "when module emit is specified as node/when instead of filechanged recursive directory watcher is invoked", - commandLineArgs: ["--w", "--p", "/a/rootFolder/project/tsconfig.json"], + subScenario: `emit for configured projects/${subScenario}`, + commandLineArgs: ["--w", "-p", configFilePath], sys: () => { - const configFile: File = { - path: "/a/rootFolder/project/tsconfig.json", - content: JSON.stringify({ - compilerOptions: { - module: "none", - allowJs: true, - outDir: "Static/scripts/" - }, - include: [ - "Scripts/**/*" - ], - }) + const moduleFile1: File = { + path: moduleFile1Path, + content: "export function Foo() { };", }; - const file1: File = { - path: "/a/rootFolder/project/Scripts/TypeScript.ts", - content: "var z = 10;" + const file1Consumer1: File = { + path: file1Consumer1Path, + content: `import {Foo} from "./moduleFile1"; export var y = 10;`, }; - const file2: File = { - path: "/a/rootFolder/project/Scripts/Javascript.js", - content: "var zz = 10;" + const file1Consumer2: File = { + path: file1Consumer2Path, + content: `import {Foo} from "./moduleFile1"; let z = 10;`, + }; + const moduleFile2: File = { + path: moduleFile2Path, + content: `export var Foo4 = 10;`, }; - return createWatchedSystem([configFile, file1, file2, libFile]); + const globalFile3: File = { + path: globalFilePath, + content: `interface GlobalFoo { age: number }` + }; + const configFile: File = { + path: configFilePath, + content: JSON.stringify(configObj || {}) + }; + const additionalFiles = getAdditionalFileOrFolder?.() || emptyArray; + const files = [moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile, ...additionalFiles]; + return createWatchedSystem(firstReloadFileList ? + map(firstReloadFileList, fileName => find(files, file => file.path === fileName)!) : + files); + }, + changes + }); + } + function modifyModuleFile1Shape(sys: WatchedSystem) { + sys.writeFile(moduleFile1Path, `export var T: number;export function Foo() { };`); + } + function changeModuleFile1Shape(sys: WatchedSystem) { + modifyModuleFile1Shape(sys); + sys.checkTimeoutQueueLengthAndRun(1); + return "Change the content of moduleFile1 to `export var T: number;export function Foo() { };`"; + } + verifyTscWatchEmit({ + subScenario: "should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", + changes: [ + changeModuleFile1Shape, + sys => { + sys.writeFile(moduleFile1Path, `export var T: number;export function Foo() { console.log('hi'); };`); + sys.checkTimeoutQueueLengthAndRun(1); + return "Change the content of moduleFile1 to `export var T: number;export function Foo() { console.log('hi'); };`"; + } + ] + }); + verifyTscWatchEmit({ + subScenario: "should be up-to-date with the reference map changes", + changes: [ + sys => { + sys.writeFile(file1Consumer1Path, `export let y = Foo();`); + sys.checkTimeoutQueueLengthAndRun(1); + return "Change file1Consumer1 content to `export let y = Foo();`"; + }, + changeModuleFile1Shape, + sys => { + sys.writeFile(file1Consumer1Path, `import {Foo} from "./moduleFile1";let y = Foo();`); + sys.checkTimeoutQueueLengthAndRun(1); + return "Add the import statements back to file1Consumer1"; + }, + sys => { + sys.writeFile(moduleFile1Path, `export let y = Foo();`); + sys.checkTimeoutQueueLengthAndRun(1); + return "Change the content of moduleFile1 to `export var T: number;export var T2: string;export function Foo() { };`"; + }, + sys => { + // Change file1Consumer1 content to `export let y = Foo();` + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + sys.writeFile(file1Consumer1Path, `import {Foo} from "./moduleFile1";let y = Foo();`); + modifyModuleFile1Shape(sys); + sys.checkTimeoutQueueLengthAndRun(1); + return "Multiple file edits in one go"; + } + ] + }); + verifyTscWatchEmit({ + subScenario: "should be up-to-date with deleted files", + changes: [ + sys => { + modifyModuleFile1Shape(sys); + sys.deleteFile(file1Consumer2Path); + sys.checkTimeoutQueueLengthAndRun(1); + return "change moduleFile1 shape and delete file1Consumer2"; + } + ] + }); + verifyTscWatchEmit({ + subScenario: "should be up-to-date with newly created files", + changes: [ + sys => { + sys.writeFile("/a/b/file1Consumer3.ts", `import {Foo} from "./moduleFile1"; let y = Foo();`); + modifyModuleFile1Shape(sys); + sys.checkTimeoutQueueLengthAndRun(1); + return "change moduleFile1 shape and create file1Consumer3"; + } + ] + }); + verifyTscWatchEmit({ + subScenario: "should detect changes in non-root files", + configObj: { files: [file1Consumer1Path] }, + changes: [ + changeModuleFile1Shape, + sys => { + sys.appendFile(moduleFile1Path, "var T1: number;"); + sys.checkTimeoutQueueLengthAndRun(1); + return "change file1 internal, and verify only file1 is affected"; + } + ] + }); + verifyTscWatchEmit({ + subScenario: "should return all files if a global file changed shape", + changes: [ + sys => { + sys.appendFile(globalFilePath, "var T2: string;"); + sys.checkTimeoutQueueLengthAndRun(1); + return "change shape of global file"; + } + ] + }); + verifyTscWatchEmit({ + subScenario: "should always return the file itself if '--isolatedModules' is specified", + configObj: { compilerOptions: { isolatedModules: true } }, + changes: [ + changeModuleFile1Shape + ] + }); + verifyTscWatchEmit({ + subScenario: "should always return the file itself if '--out' or '--outFile' is specified", + configObj: { compilerOptions: { module: "system", outFile: "/a/b/out.js" } }, + changes: [ + changeModuleFile1Shape + ] + }); + verifyTscWatchEmit({ + subScenario: "should return cascaded affected file list", + getAdditionalFileOrFolder: () => [{ + path: "/a/b/file1Consumer1Consumer1.ts", + content: `import {y} from "./file1Consumer1";` + }], + changes: [ + sys => { + sys.appendFile(file1Consumer1Path, "export var T: number;"); + sys.checkTimeoutQueueLengthAndRun(1); + return "change file1Consumer1"; + }, + changeModuleFile1Shape, + sys => { + sys.appendFile(file1Consumer1Path, "export var T2: number;"); + sys.writeFile(moduleFile1Path, `export var T2: number;export function Foo() { };`); + sys.checkTimeoutQueueLengthAndRun(1); + return "change file1Consumer1 and moduleFile1"; + } + ] + }); + verifyTscWatchEmit({ + subScenario: "should work fine for files with circular references", + getAdditionalFileOrFolder: () => [ + { + path: "/a/b/file1.ts", + content: `/// +export var t1 = 10;` + }, + { + path: "/a/b/file2.ts", + content: `/// +export var t2 = 10;` + } + ], + firstReloadFileList: [libFile.path, "/a/b/file1.ts", "/a/b/file2.ts", configFilePath], + changes: [ + sys => { + sys.appendFile("/a/b/file1.ts", "export var t3 = 10;"); + sys.checkTimeoutQueueLengthAndRun(1); + return "change file1"; + } + ] + }); + verifyTscWatchEmit({ + subScenario: "should detect removed code file", + getAdditionalFileOrFolder: () => [{ + path: "/a/b/referenceFile1.ts", + content: `/// +export var x = Foo();` + }], + firstReloadFileList: [libFile.path, "/a/b/referenceFile1.ts", moduleFile1Path, configFilePath], + changes: [ + sys => { + sys.deleteFile(moduleFile1Path); + sys.checkTimeoutQueueLengthAndRun(1); + return "delete moduleFile1"; + } + ] + }); + verifyTscWatchEmit({ + subScenario: "should detect non existing code file", + getAdditionalFileOrFolder: () => [{ + path: "/a/b/referenceFile1.ts", + content: `/// +export var x = Foo();` + }], + firstReloadFileList: [libFile.path, "/a/b/referenceFile1.ts", configFilePath], + changes: [ + sys => { + sys.appendFile("/a/b/referenceFile1.ts", "export var yy = Foo();"); + sys.checkTimeoutQueueLengthAndRun(1); + return "edit refereceFile1"; }, + sys => { + sys.writeFile(moduleFile2Path, "export var Foo4 = 10;"); + sys.checkTimeoutQueueLengthAndRun(1); + return "create moduleFile2"; + } + ] + }); +}); +describe("unittests:: tsc-watch:: emit file content", () => { + function verifyNewLine(subScenario: string, newLine: string) { + verifyTscWatch({ + scenario, + subScenario: `emit file content/${subScenario}`, + commandLineArgs: ["--w", "/a/app.ts"], + sys: () => createWatchedSystem([ + { + path: "/a/app.ts", + content: ["var x = 1;", "var y = 2;"].join(newLine) + }, + libFile + ], { newLine }), changes: [ sys => { - sys.modifyFile( - "/a/rootFolder/project/Scripts/TypeScript.ts", - "var zz30 = 100;", - { invokeDirectoryWatcherInsteadOfFileChanged: true }, - ); - sys.runQueuedTimeoutCallbacks(); - return "Modify typescript file"; + sys.appendFile("/a/app.ts", newLine + "var z = 3;"); + sys.checkTimeoutQueueLengthAndRun(1); + return "Append a line"; } ], }); + } + verifyNewLine("handles new lines lineFeed", "\n"); + verifyNewLine("handles new lines carriageReturn lineFeed", "\r\n"); + verifyTscWatch({ + scenario, + subScenario: "emit file content/should emit specified file", + commandLineArgs: ["-w", "-p", "/a/b/tsconfig.json"], + sys: () => { + const file1 = { + path: "/a/b/f1.ts", + content: `export function Foo() { return 10; }` + }; + const file2 = { + path: "/a/b/f2.ts", + content: `import {Foo} from "./f1"; export let y = Foo();` + }; + const file3 = { + path: "/a/b/f3.ts", + content: `import {y} from "./f2"; let x = y;` + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: "{}" + }; + return createWatchedSystem([file1, file2, file3, configFile, libFile]); + }, + changes: [ + sys => { + sys.appendFile("/a/b/f1.ts", "export function foo2() { return 2; }"); + sys.checkTimeoutQueueLengthAndRun(1); + return "Append content to f1"; + } + ], + }); + verifyTscWatch({ + scenario, + subScenario: "emit file content/elides const enums correctly in incremental compilation", + commandLineArgs: ["-w", "/user/someone/projects/myproject/file3.ts"], + sys: () => { + const currentDirectory = "/user/someone/projects/myproject"; + const file1: File = { + path: `${currentDirectory}/file1.ts`, + content: "export const enum E1 { V = 1 }" + }; + const file2: File = { + path: `${currentDirectory}/file2.ts`, + content: `import { E1 } from "./file1"; export const enum E2 { V = E1.V }` + }; + const file3: File = { + path: `${currentDirectory}/file3.ts`, + content: `import { E2 } from "./file2"; const v: E2 = E2.V;` + }; + return createWatchedSystem([file1, file2, file3, libFile]); + }, + changes: [ + sys => { + sys.appendFile("/user/someone/projects/myproject/file3.ts", "function foo2() { return 2; }"); + sys.checkTimeoutQueueLengthAndRun(1); + return "Append content to file3"; + } + ], + }); + verifyTscWatch({ + scenario, + subScenario: "emit file content/file is deleted and created as part of change", + commandLineArgs: ["-w"], + sys: () => { + const projectLocation = "/home/username/project"; + const file: File = { + path: `${projectLocation}/app/file.ts`, + content: "var a = 10;" + }; + const configFile: File = { + path: `${projectLocation}/tsconfig.json`, + content: JSON.stringify({ + include: [ + "app/**/*.ts" + ] + }) + }; + const files = [file, configFile, libFile]; + return createWatchedSystem(files, { currentDirectory: projectLocation, useCaseSensitiveFileNames: true }); + }, + changes: [ + sys => { + sys.appendFile("/home/username/project/app/file.ts", "\nvar b = 10;", { invokeFileDeleteCreateAsPartInsteadOfChange: true }); + sys.runQueuedTimeoutCallbacks(); + return "file is deleted and then created to modify content"; + } + ] + }); +}); +describe("unittests:: tsc-watch:: emit with when module emit is specified as node", () => { + verifyTscWatch({ + scenario, + subScenario: "when module emit is specified as node/when instead of filechanged recursive directory watcher is invoked", + commandLineArgs: ["--w", "--p", "/a/rootFolder/project/tsconfig.json"], + sys: () => { + const configFile: File = { + path: "/a/rootFolder/project/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + module: "none", + allowJs: true, + outDir: "Static/scripts/" + }, + include: [ + "Scripts/**/*" + ], + }) + }; + const file1: File = { + path: "/a/rootFolder/project/Scripts/TypeScript.ts", + content: "var z = 10;" + }; + const file2: File = { + path: "/a/rootFolder/project/Scripts/Javascript.js", + content: "var zz = 10;" + }; + return createWatchedSystem([configFile, file1, file2, libFile]); + }, + changes: [ + sys => { + sys.modifyFile("/a/rootFolder/project/Scripts/TypeScript.ts", "var zz30 = 100;", { invokeDirectoryWatcherInsteadOfFileChanged: true }); + sys.runQueuedTimeoutCallbacks(); + return "Modify typescript file"; + } + ], }); -} +}); diff --git a/src/testRunner/unittests/tscWatch/emitAndErrorUpdates.ts b/src/testRunner/unittests/tscWatch/emitAndErrorUpdates.ts index 65f311ebcd100..d4da420cc3d2d 100644 --- a/src/testRunner/unittests/tscWatch/emitAndErrorUpdates.ts +++ b/src/testRunner/unittests/tscWatch/emitAndErrorUpdates.ts @@ -1,159 +1,130 @@ -namespace ts.tscWatch { - describe("unittests:: tsc-watch:: Emit times and Error updates in builder after program changes", () => { - const config: File = { - path: `${projectRoot}/tsconfig.json`, - content: `{}` - }; - interface VerifyEmitAndErrorUpdatesWorker extends VerifyEmitAndErrorUpdates { - configFile: () => File; - } - function verifyEmitAndErrorUpdatesWorker({ +import { File, projectRoot, verifyTscWatch, createWatchedSystem, libFile, TscWatchCompileChange } from "../../ts.tscWatch"; +import { CompilerOptions, TestFSWithWatch, libContent } from "../../ts"; +describe("unittests:: tsc-watch:: Emit times and Error updates in builder after program changes", () => { + const config: File = { + path: `${projectRoot}/tsconfig.json`, + content: `{}` + }; + interface VerifyEmitAndErrorUpdatesWorker extends VerifyEmitAndErrorUpdates { + configFile: () => File; + } + function verifyEmitAndErrorUpdatesWorker({ subScenario, files, currentDirectory, lib, configFile, changes, }: VerifyEmitAndErrorUpdatesWorker) { + verifyTscWatch({ + scenario: "emitAndErrorUpdates", subScenario, - files, - currentDirectory, - lib, - configFile, - changes, - }: VerifyEmitAndErrorUpdatesWorker) { - verifyTscWatch({ - scenario: "emitAndErrorUpdates", - subScenario, - commandLineArgs: ["--w"], - sys: () => createWatchedSystem( - [...files(), configFile(), lib?.() || libFile], - { currentDirectory: currentDirectory || projectRoot } - ), - changes - }); - } - - function changeCompilerOptions(input: VerifyEmitAndErrorUpdates, additionalOptions: CompilerOptions): File { - const configFile = input.configFile?.() || config; - const content = JSON.parse(configFile.content); - content.compilerOptions = { ...content.compilerOptions, ...additionalOptions }; - return { path: configFile.path, content: JSON.stringify(content) }; - } - - interface VerifyEmitAndErrorUpdates { - subScenario: string - files: () => File[]; - currentDirectory?: string; - lib?: () => File; - changes: TscWatchCompileChange[]; - configFile?: () => File; - } - function verifyEmitAndErrorUpdates(input: VerifyEmitAndErrorUpdates) { - verifyEmitAndErrorUpdatesWorker({ - ...input, - subScenario: `default/${input.subScenario}`, - configFile: () => input.configFile?.() || config - }); - - verifyEmitAndErrorUpdatesWorker({ - ...input, - subScenario: `defaultAndD/${input.subScenario}`, - configFile: () => changeCompilerOptions(input, { declaration: true }) - }); - - verifyEmitAndErrorUpdatesWorker({ - ...input, - subScenario: `isolatedModules/${input.subScenario}`, - configFile: () => changeCompilerOptions(input, { isolatedModules: true }) - }); - - verifyEmitAndErrorUpdatesWorker({ - ...input, - subScenario: `isolatedModulesAndD/${input.subScenario}`, - configFile: () => changeCompilerOptions(input, { isolatedModules: true, declaration: true }) - }); - - verifyEmitAndErrorUpdatesWorker({ - ...input, - subScenario: `assumeChangesOnlyAffectDirectDependencies/${input.subScenario}`, - configFile: () => changeCompilerOptions(input, { assumeChangesOnlyAffectDirectDependencies: true }) - }); - - verifyEmitAndErrorUpdatesWorker({ - ...input, - subScenario: `assumeChangesOnlyAffectDirectDependenciesAndD/${input.subScenario}`, - configFile: () => changeCompilerOptions(input, { assumeChangesOnlyAffectDirectDependencies: true, declaration: true }) - }); - } - - describe("deep import changes", () => { - const aFile: File = { - path: `${projectRoot}/a.ts`, - content: `import {B} from './b'; + commandLineArgs: ["--w"], + sys: () => createWatchedSystem([...files(), configFile(), lib?.() || libFile], { currentDirectory: currentDirectory || projectRoot }), + changes + }); + } + function changeCompilerOptions(input: VerifyEmitAndErrorUpdates, additionalOptions: CompilerOptions): File { + const configFile = input.configFile?.() || config; + const content = JSON.parse(configFile.content); + content.compilerOptions = { ...content.compilerOptions, ...additionalOptions }; + return { path: configFile.path, content: JSON.stringify(content) }; + } + interface VerifyEmitAndErrorUpdates { + subScenario: string; + files: () => File[]; + currentDirectory?: string; + lib?: () => File; + changes: TscWatchCompileChange[]; + configFile?: () => File; + } + function verifyEmitAndErrorUpdates(input: VerifyEmitAndErrorUpdates) { + verifyEmitAndErrorUpdatesWorker({ + ...input, + subScenario: `default/${input.subScenario}`, + configFile: () => input.configFile?.() || config + }); + verifyEmitAndErrorUpdatesWorker({ + ...input, + subScenario: `defaultAndD/${input.subScenario}`, + configFile: () => changeCompilerOptions(input, { declaration: true }) + }); + verifyEmitAndErrorUpdatesWorker({ + ...input, + subScenario: `isolatedModules/${input.subScenario}`, + configFile: () => changeCompilerOptions(input, { isolatedModules: true }) + }); + verifyEmitAndErrorUpdatesWorker({ + ...input, + subScenario: `isolatedModulesAndD/${input.subScenario}`, + configFile: () => changeCompilerOptions(input, { isolatedModules: true, declaration: true }) + }); + verifyEmitAndErrorUpdatesWorker({ + ...input, + subScenario: `assumeChangesOnlyAffectDirectDependencies/${input.subScenario}`, + configFile: () => changeCompilerOptions(input, { assumeChangesOnlyAffectDirectDependencies: true }) + }); + verifyEmitAndErrorUpdatesWorker({ + ...input, + subScenario: `assumeChangesOnlyAffectDirectDependenciesAndD/${input.subScenario}`, + configFile: () => changeCompilerOptions(input, { assumeChangesOnlyAffectDirectDependencies: true, declaration: true }) + }); + } + describe("deep import changes", () => { + const aFile: File = { + path: `${projectRoot}/a.ts`, + content: `import {B} from './b'; declare var console: any; let b = new B(); console.log(b.c.d);` - }; - - function verifyDeepImportChange(subScenario: string, bFile: File, cFile: File) { - verifyEmitAndErrorUpdates({ - subScenario: `deepImportChanges/${subScenario}`, - files: () => [aFile, bFile, cFile], - changes: [ - sys => { - sys.writeFile(cFile.path, cFile.content.replace("d", "d2")); - sys.runQueuedTimeoutCallbacks(); - return "Rename property d to d2 of class C"; - } - ], - }); - } - - describe("updates errors when deep import file changes", () => { - const bFile: File = { - path: `${projectRoot}/b.ts`, - content: `import {C} from './c'; + }; + function verifyDeepImportChange(subScenario: string, bFile: File, cFile: File) { + verifyEmitAndErrorUpdates({ + subScenario: `deepImportChanges/${subScenario}`, + files: () => [aFile, bFile, cFile], + changes: [ + sys => { + sys.writeFile(cFile.path, cFile.content.replace("d", "d2")); + sys.runQueuedTimeoutCallbacks(); + return "Rename property d to d2 of class C"; + } + ], + }); + } + describe("updates errors when deep import file changes", () => { + const bFile: File = { + path: `${projectRoot}/b.ts`, + content: `import {C} from './c'; export class B { c = new C(); }` - }; - const cFile: File = { - path: `${projectRoot}/c.ts`, - content: `export class C + }; + const cFile: File = { + path: `${projectRoot}/c.ts`, + content: `export class C { d = 1; }` - }; - verifyDeepImportChange( - "updates errors when deep import file changes", - bFile, - cFile - ); - }); - - describe("updates errors when deep import through declaration file changes", () => { - const bFile: File = { - path: `${projectRoot}/b.d.ts`, - content: `import {C} from './c'; + }; + verifyDeepImportChange("updates errors when deep import file changes", bFile, cFile); + }); + describe("updates errors when deep import through declaration file changes", () => { + const bFile: File = { + path: `${projectRoot}/b.d.ts`, + content: `import {C} from './c'; export class B { c: C; }` - }; - const cFile: File = { - path: `${projectRoot}/c.d.ts`, - content: `export class C + }; + const cFile: File = { + path: `${projectRoot}/c.d.ts`, + content: `export class C { d: number; }` - }; - verifyDeepImportChange( - "updates errors when deep import through declaration file changes", - bFile, - cFile - ); - }); + }; + verifyDeepImportChange("updates errors when deep import through declaration file changes", bFile, cFile); }); - - describe("updates errors in file not exporting a deep multilevel import that changes", () => { - const aFile: File = { - path: `${projectRoot}/a.ts`, - content: `export interface Point { + }); + describe("updates errors in file not exporting a deep multilevel import that changes", () => { + const aFile: File = { + path: `${projectRoot}/a.ts`, + content: `export interface Point { name: string; c: Coords; } @@ -161,16 +132,16 @@ export interface Coords { x2: number; y: number; }` - }; - const bFile: File = { - path: `${projectRoot}/b.ts`, - content: `import { Point } from "./a"; + }; + const bFile: File = { + path: `${projectRoot}/b.ts`, + content: `import { Point } from "./a"; export interface PointWrapper extends Point { }` - }; - const cFile: File = { - path: `${projectRoot}/c.ts`, - content: `import { PointWrapper } from "./b"; + }; + const cFile: File = { + path: `${projectRoot}/c.ts`, + content: `import { PointWrapper } from "./b"; export function getPoint(): PointWrapper { return { name: "test", @@ -180,53 +151,52 @@ export function getPoint(): PointWrapper { } } };` - }; - const dFile: File = { - path: `${projectRoot}/d.ts`, - content: `import { getPoint } from "./c"; + }; + const dFile: File = { + path: `${projectRoot}/d.ts`, + content: `import { getPoint } from "./c"; getPoint().c.x;` - }; - const eFile: File = { - path: `${projectRoot}/e.ts`, - content: `import "./d";` - }; - verifyEmitAndErrorUpdates({ - subScenario: "file not exporting a deep multilevel import that changes", - files: () => [aFile, bFile, cFile, dFile, eFile], - changes: [ - sys => { - sys.writeFile(aFile.path, aFile.content.replace("x2", "x")); - sys.runQueuedTimeoutCallbacks(); - return "Rename property x2 to x of interface Coords"; - } - ] - }); + }; + const eFile: File = { + path: `${projectRoot}/e.ts`, + content: `import "./d";` + }; + verifyEmitAndErrorUpdates({ + subScenario: "file not exporting a deep multilevel import that changes", + files: () => [aFile, bFile, cFile, dFile, eFile], + changes: [ + sys => { + sys.writeFile(aFile.path, aFile.content.replace("x2", "x")); + sys.runQueuedTimeoutCallbacks(); + return "Rename property x2 to x of interface Coords"; + } + ] }); - - describe("updates errors when file transitively exported file changes", () => { - const config: File = { - path: `${projectRoot}/tsconfig.json`, - content: JSON.stringify({ - files: ["app.ts"], - compilerOptions: { baseUrl: "." } - }) - }; - const app: File = { - path: `${projectRoot}/app.ts`, - content: `import { Data } from "lib2/public"; + }); + describe("updates errors when file transitively exported file changes", () => { + const config: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ + files: ["app.ts"], + compilerOptions: { baseUrl: "." } + }) + }; + const app: File = { + path: `${projectRoot}/app.ts`, + content: `import { Data } from "lib2/public"; export class App { public constructor() { new Data().test(); } }` - }; - const lib2Public: File = { - path: `${projectRoot}/lib2/public.ts`, - content: `export * from "./data";` - }; - const lib2Data: File = { - path: `${projectRoot}/lib2/data.ts`, - content: `import { ITest } from "lib1/public"; + }; + const lib2Public: File = { + path: `${projectRoot}/lib2/public.ts`, + content: `export * from "./data";` + }; + const lib2Data: File = { + path: `${projectRoot}/lib2/data.ts`, + content: `import { ITest } from "lib1/public"; export class Data { public test() { const result: ITest = { @@ -235,47 +205,42 @@ export class Data { return result; } }` - }; - const lib1Public: File = { - path: `${projectRoot}/lib1/public.ts`, - content: `export * from "./tools/public";` - }; - const lib1ToolsPublic: File = { - path: `${projectRoot}/lib1/tools/public.ts`, - content: `export * from "./tools.interface";` - }; - const lib1ToolsInterface: File = { - path: `${projectRoot}/lib1/tools/tools.interface.ts`, - content: `export interface ITest { + }; + const lib1Public: File = { + path: `${projectRoot}/lib1/public.ts`, + content: `export * from "./tools/public";` + }; + const lib1ToolsPublic: File = { + path: `${projectRoot}/lib1/tools/public.ts`, + content: `export * from "./tools.interface";` + }; + const lib1ToolsInterface: File = { + path: `${projectRoot}/lib1/tools/tools.interface.ts`, + content: `export interface ITest { title: string; }` - }; - - function verifyTransitiveExports(subScenario: string, files: readonly File[]) { - verifyEmitAndErrorUpdates({ - subScenario: `updates errors when file transitively exported file changes/${subScenario}`, - files: () => [lib1ToolsInterface, lib1ToolsPublic, app, lib2Public, lib1Public, ...files], - configFile: () => config, - changes: [ - sys => { - sys.writeFile(lib1ToolsInterface.path, lib1ToolsInterface.content.replace("title", "title2")); - sys.runQueuedTimeoutCallbacks(); - return "Rename property title to title2 of interface ITest"; - } - ] - }); - } - describe("when there are no circular import and exports", () => { - verifyTransitiveExports( - "when there are no circular import and exports", - [lib2Data] - ); + }; + function verifyTransitiveExports(subScenario: string, files: readonly File[]) { + verifyEmitAndErrorUpdates({ + subScenario: `updates errors when file transitively exported file changes/${subScenario}`, + files: () => [lib1ToolsInterface, lib1ToolsPublic, app, lib2Public, lib1Public, ...files], + configFile: () => config, + changes: [ + sys => { + sys.writeFile(lib1ToolsInterface.path, lib1ToolsInterface.content.replace("title", "title2")); + sys.runQueuedTimeoutCallbacks(); + return "Rename property title to title2 of interface ITest"; + } + ] }); - - describe("when there are circular import and exports", () => { - const lib2Data: File = { - path: `${projectRoot}/lib2/data.ts`, - content: `import { ITest } from "lib1/public"; import { Data2 } from "./data2"; + } + describe("when there are no circular import and exports", () => { + verifyTransitiveExports("when there are no circular import and exports", [lib2Data]); + }); + describe("when there are circular import and exports", () => { + const lib2Data: File = { + path: `${projectRoot}/lib2/data.ts`, + content: `import { ITest } from "lib1/public"; import { Data2 } from "./data2"; export class Data { public dat?: Data2; public test() { const result: ITest = { @@ -284,38 +249,33 @@ export class Data { return result; } }` - }; - const lib2Data2: File = { - path: `${projectRoot}/lib2/data2.ts`, - content: `import { Data } from "./data"; + }; + const lib2Data2: File = { + path: `${projectRoot}/lib2/data2.ts`, + content: `import { Data } from "./data"; export class Data2 { public dat?: Data; }` - }; - verifyTransitiveExports( - "when there are circular import and exports", - [lib2Data, lib2Data2] - ); - }); + }; + verifyTransitiveExports("when there are circular import and exports", [lib2Data, lib2Data2]); }); - - verifyEmitAndErrorUpdates({ - subScenario: "with noEmitOnError", - currentDirectory: `${TestFSWithWatch.tsbuildProjectsLocation}/noEmitOnError`, - files: () => ["shared/types/db.ts", "src/main.ts", "src/other.ts"] - .map(f => TestFSWithWatch.getTsBuildProjectFile("noEmitOnError", f)), - lib: () => ({ path: libFile.path, content: libContent }), - configFile: () => TestFSWithWatch.getTsBuildProjectFile("noEmitOnError", "tsconfig.json"), - changes: [ - sys => { - sys.writeFile(`${TestFSWithWatch.tsbuildProjectsLocation}/noEmitOnError/src/main.ts`, `import { A } from "../shared/types/db"; + }); + verifyEmitAndErrorUpdates({ + subScenario: "with noEmitOnError", + currentDirectory: `${TestFSWithWatch.tsbuildProjectsLocation}/noEmitOnError`, + files: () => ["shared/types/db.ts", "src/main.ts", "src/other.ts"] + .map(f => TestFSWithWatch.getTsBuildProjectFile("noEmitOnError", f)), + lib: () => ({ path: libFile.path, content: libContent }), + configFile: () => TestFSWithWatch.getTsBuildProjectFile("noEmitOnError", "tsconfig.json"), + changes: [ + sys => { + sys.writeFile(`${TestFSWithWatch.tsbuildProjectsLocation}/noEmitOnError/src/main.ts`, `import { A } from "../shared/types/db"; const a = { lastName: 'sdsd' };`); - sys.checkTimeoutQueueLengthAndRun(1); // build project - return "Fix the error"; - } - ] - }); + sys.checkTimeoutQueueLengthAndRun(1); // build project + return "Fix the error"; + } + ] }); -} +}); diff --git a/src/testRunner/unittests/tscWatch/forceConsistentCasingInFileNames.ts b/src/testRunner/unittests/tscWatch/forceConsistentCasingInFileNames.ts index cae63450b0860..780528cd62a37 100644 --- a/src/testRunner/unittests/tscWatch/forceConsistentCasingInFileNames.ts +++ b/src/testRunner/unittests/tscWatch/forceConsistentCasingInFileNames.ts @@ -1,50 +1,49 @@ -namespace ts.tscWatch { - describe("unittests:: tsc-watch:: forceConsistentCasingInFileNames", () => { - const loggerFile: File = { - path: `${projectRoot}/logger.ts`, - content: `export class logger { }` - }; - const anotherFile: File = { - path: `${projectRoot}/another.ts`, - content: `import { logger } from "./logger"; new logger();` - }; - const tsconfig: File = { - path: `${projectRoot}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { forceConsistentCasingInFileNames: true } - }) - }; - - function verifyConsistentFileNames({ subScenario, changes }: { subScenario: string; changes: TscWatchCompileChange[]; }) { - verifyTscWatch({ - scenario: "forceConsistentCasingInFileNames", - subScenario, - commandLineArgs: ["--w", "--p", tsconfig.path], - sys: () => createWatchedSystem([loggerFile, anotherFile, tsconfig, libFile, tsconfig]), - changes - }); - } - - verifyConsistentFileNames({ - subScenario: "when changing module name with different casing", - changes: [ - sys => { - sys.writeFile(anotherFile.path, anotherFile.content.replace("./logger", "./Logger")); - sys.runQueuedTimeoutCallbacks(); - return "Change module name from logger to Logger"; - } - ] - }); - - verifyConsistentFileNames({ - subScenario: "when renaming file with different casing", - changes: [ - sys => { - sys.renameFile(loggerFile.path, `${projectRoot}/Logger.ts`); - sys.runQueuedTimeoutCallbacks(); - return "Change name of file from logger to Logger"; - } - ] +import { File, projectRoot, TscWatchCompileChange, verifyTscWatch, createWatchedSystem, libFile } from "../../ts.tscWatch"; +describe("unittests:: tsc-watch:: forceConsistentCasingInFileNames", () => { + const loggerFile: File = { + path: `${projectRoot}/logger.ts`, + content: `export class logger { }` + }; + const anotherFile: File = { + path: `${projectRoot}/another.ts`, + content: `import { logger } from "./logger"; new logger();` + }; + const tsconfig: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { forceConsistentCasingInFileNames: true } + }) + }; + function verifyConsistentFileNames({ subScenario, changes }: { + subScenario: string; + changes: TscWatchCompileChange[]; + }) { + verifyTscWatch({ + scenario: "forceConsistentCasingInFileNames", + subScenario, + commandLineArgs: ["--w", "--p", tsconfig.path], + sys: () => createWatchedSystem([loggerFile, anotherFile, tsconfig, libFile, tsconfig]), + changes }); + } + verifyConsistentFileNames({ + subScenario: "when changing module name with different casing", + changes: [ + sys => { + sys.writeFile(anotherFile.path, anotherFile.content.replace("./logger", "./Logger")); + sys.runQueuedTimeoutCallbacks(); + return "Change module name from logger to Logger"; + } + ] + }); + verifyConsistentFileNames({ + subScenario: "when renaming file with different casing", + changes: [ + sys => { + sys.renameFile(loggerFile.path, `${projectRoot}/Logger.ts`); + sys.runQueuedTimeoutCallbacks(); + return "Change name of file from logger to Logger"; + } + ] }); -} +}); diff --git a/src/testRunner/unittests/tscWatch/helpers.ts b/src/testRunner/unittests/tscWatch/helpers.ts index 34d278031b871..54d571bb52c5a 100644 --- a/src/testRunner/unittests/tscWatch/helpers.ts +++ b/src/testRunner/unittests/tscWatch/helpers.ts @@ -1,425 +1,348 @@ -namespace ts.tscWatch { - export const projects = `/user/username/projects`; - export const projectRoot = `${projects}/myproject`; - export import WatchedSystem = TestFSWithWatch.TestServerHost; - export type File = TestFSWithWatch.File; - export type SymLink = TestFSWithWatch.SymLink; - export import libFile = TestFSWithWatch.libFile; - export import createWatchedSystem = TestFSWithWatch.createWatchedSystem; - export import checkArray = TestFSWithWatch.checkArray; - export import checkWatchedFiles = TestFSWithWatch.checkWatchedFiles; - export import checkWatchedFilesDetailed = TestFSWithWatch.checkWatchedFilesDetailed; - export import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories; - export import checkWatchedDirectoriesDetailed = TestFSWithWatch.checkWatchedDirectoriesDetailed; - export import checkOutputContains = TestFSWithWatch.checkOutputContains; - export import checkOutputDoesNotContain = TestFSWithWatch.checkOutputDoesNotContain; - - export const commonFile1: File = { - path: "/a/b/commonFile1.ts", - content: "let x = 1" - }; - export const commonFile2: File = { - path: "/a/b/commonFile2.ts", - content: "let y = 1" - }; - - export function checkProgramActualFiles(program: Program, expectedFiles: readonly string[]) { - checkArray(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles); - } - - export function checkProgramRootFiles(program: Program, expectedFiles: readonly string[]) { - checkArray(`Program rootFileNames`, program.getRootFileNames(), expectedFiles); - } - - export interface Watch { - (): Program; - getBuilderProgram(): EmitAndSemanticDiagnosticsBuilderProgram; - close(): void; - } - - export function createWatchOfConfigFile(configFileName: string, host: WatchedSystem, optionsToExtend?: CompilerOptions, watchOptionsToExtend?: WatchOptions, maxNumberOfFilesToIterateForInvalidation?: number) { - const compilerHost = createWatchCompilerHostOfConfigFile(configFileName, optionsToExtend || {}, watchOptionsToExtend, host); - compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation; - const watch = createWatchProgram(compilerHost); - const result = (() => watch.getCurrentProgram().getProgram()) as Watch; - result.getBuilderProgram = () => watch.getCurrentProgram(); - result.close = () => watch.close(); - return result; - } - - export function createWatchOfFilesAndCompilerOptions(rootFiles: string[], host: WatchedSystem, options: CompilerOptions = {}, watchOptions?: WatchOptions, maxNumberOfFilesToIterateForInvalidation?: number) { - const compilerHost = createWatchCompilerHostOfFilesAndCompilerOptions(rootFiles, options, watchOptions, host); - compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation; - const watch = createWatchProgram(compilerHost); - return () => watch.getCurrentProgram().getProgram(); - } - - const elapsedRegex = /^Elapsed:: [0-9]+ms/; - const buildVerboseLogRegEx = /^.+ \- /; - export enum HostOutputKind { - Log, - Diagnostic, - WatchDiagnostic - } - - export interface HostOutputLog { - kind: HostOutputKind.Log; - expected: string; - caption?: string; - } - - export interface HostOutputDiagnostic { - kind: HostOutputKind.Diagnostic; - diagnostic: Diagnostic | string; - } - - export interface HostOutputWatchDiagnostic { - kind: HostOutputKind.WatchDiagnostic; - diagnostic: Diagnostic | string; - } - - export type HostOutput = HostOutputLog | HostOutputDiagnostic | HostOutputWatchDiagnostic; - - export function checkOutputErrors( - host: WatchedSystem, - expected: readonly HostOutput[], - disableConsoleClears?: boolean | undefined - ) { - let screenClears = 0; - const outputs = host.getOutput(); - assert.equal(outputs.length, expected.length, JSON.stringify(outputs)); - let index = 0; - forEach(expected, expected => { - switch (expected.kind) { - case HostOutputKind.Log: - return assertLog(expected); - case HostOutputKind.Diagnostic: - return assertDiagnostic(expected); - case HostOutputKind.WatchDiagnostic: - return assertWatchDiagnostic(expected); - default: - return Debug.assertNever(expected); - } - }); - assert.equal(host.screenClears.length, screenClears, "Expected number of screen clears"); - host.clearOutput(); - - function isDiagnostic(diagnostic: Diagnostic | string): diagnostic is Diagnostic { - return !!(diagnostic as Diagnostic).messageText; - } - - function assertDiagnostic({ diagnostic }: HostOutputDiagnostic) { - const expected = isDiagnostic(diagnostic) ? formatDiagnostic(diagnostic, host) : diagnostic; - assert.equal(outputs[index], expected, getOutputAtFailedMessage("Diagnostic", expected)); - index++; - } - - function getCleanLogString(log: string) { - return log.replace(elapsedRegex, "").replace(buildVerboseLogRegEx, ""); - } - - function assertLog({ caption, expected }: HostOutputLog) { - const actual = outputs[index]; - assert.equal(getCleanLogString(actual), getCleanLogString(expected), getOutputAtFailedMessage(caption || "Log", expected)); - index++; - } - - function assertWatchDiagnostic({ diagnostic }: HostOutputWatchDiagnostic) { - if (isString(diagnostic)) { - assert.equal(outputs[index], diagnostic, getOutputAtFailedMessage("Diagnostic", diagnostic)); - } - else { - const expected = getWatchDiagnosticWithoutDate(diagnostic); - if (!disableConsoleClears && contains(screenStartingMessageCodes, diagnostic.code)) { - assert.equal(host.screenClears[screenClears], index, `Expected screen clear at this diagnostic: ${expected}`); - screenClears++; - } - assert.isTrue(endsWith(outputs[index], expected), getOutputAtFailedMessage("Watch diagnostic", expected)); - } - index++; - } - - function getOutputAtFailedMessage(caption: string, expectedOutput: string) { - return `Expected ${caption}: ${JSON.stringify(expectedOutput)} at ${index} in ${JSON.stringify(outputs)}`; - } - - function getWatchDiagnosticWithoutDate(diagnostic: Diagnostic) { - const newLines = contains(screenStartingMessageCodes, diagnostic.code) - ? `${host.newLine}${host.newLine}` - : host.newLine; - return ` - ${flattenDiagnosticMessageText(diagnostic.messageText, host.newLine)}${newLines}`; +import { TestFSWithWatch, Program, EmitAndSemanticDiagnosticsBuilderProgram, CompilerOptions, WatchOptions, createWatchCompilerHostOfConfigFile, createWatchProgram, createWatchCompilerHostOfFilesAndCompilerOptions, Diagnostic, forEach, Debug, formatDiagnostic, isString, contains, screenStartingMessageCodes, endsWith, flattenDiagnosticMessageText, createCompilerDiagnostic, Diagnostics, map, emptyArray, ExitStatus, getErrorSummaryText, DiagnosticMessage, DiagnosticMessageChain, SourceFile, getLocaleSpecificMessage, formatStringFromArgs, toPath, CommandLineProgram, commandLineCallbacks, executeCommandLine, isBuild, generateSourceMapBaselineFiles } from "../../ts"; +import { patchHostForBuildInfoReadWrite } from "../../fakes"; +import { Baseline } from "../../Harness"; +import * as ts from "../../ts"; +export const projects = `/user/username/projects`; +export const projectRoot = `${projects}/myproject`; +export import WatchedSystem = ts.TestFSWithWatch.TestServerHost; +export type File = TestFSWithWatch.File; +export type SymLink = TestFSWithWatch.SymLink; +export import libFile = ts.TestFSWithWatch.libFile; +export import createWatchedSystem = ts.TestFSWithWatch.createWatchedSystem; +export import checkArray = ts.TestFSWithWatch.checkArray; +export import checkWatchedFiles = ts.TestFSWithWatch.checkWatchedFiles; +export import checkWatchedFilesDetailed = ts.TestFSWithWatch.checkWatchedFilesDetailed; +export import checkWatchedDirectories = ts.TestFSWithWatch.checkWatchedDirectories; +export import checkWatchedDirectoriesDetailed = ts.TestFSWithWatch.checkWatchedDirectoriesDetailed; +export import checkOutputContains = ts.TestFSWithWatch.checkOutputContains; +export import checkOutputDoesNotContain = ts.TestFSWithWatch.checkOutputDoesNotContain; +export const commonFile1: File = { + path: "/a/b/commonFile1.ts", + content: "let x = 1" +}; +export const commonFile2: File = { + path: "/a/b/commonFile2.ts", + content: "let y = 1" +}; +export function checkProgramActualFiles(program: Program, expectedFiles: readonly string[]) { + checkArray(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles); +} +export function checkProgramRootFiles(program: Program, expectedFiles: readonly string[]) { + checkArray(`Program rootFileNames`, program.getRootFileNames(), expectedFiles); +} +export interface Watch { + (): Program; + getBuilderProgram(): EmitAndSemanticDiagnosticsBuilderProgram; + close(): void; +} +export function createWatchOfConfigFile(configFileName: string, host: WatchedSystem, optionsToExtend?: CompilerOptions, watchOptionsToExtend?: WatchOptions, maxNumberOfFilesToIterateForInvalidation?: number) { + const compilerHost = createWatchCompilerHostOfConfigFile(configFileName, optionsToExtend || {}, watchOptionsToExtend, host); + compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation; + const watch = createWatchProgram(compilerHost); + const result = (() => watch.getCurrentProgram().getProgram()) as Watch; + result.getBuilderProgram = () => watch.getCurrentProgram(); + result.close = () => watch.close(); + return result; +} +export function createWatchOfFilesAndCompilerOptions(rootFiles: string[], host: WatchedSystem, options: CompilerOptions = {}, watchOptions?: WatchOptions, maxNumberOfFilesToIterateForInvalidation?: number) { + const compilerHost = createWatchCompilerHostOfFilesAndCompilerOptions(rootFiles, options, watchOptions, host); + compilerHost.maxNumberOfFilesToIterateForInvalidation = maxNumberOfFilesToIterateForInvalidation; + const watch = createWatchProgram(compilerHost); + return () => watch.getCurrentProgram().getProgram(); +} +const elapsedRegex = /^Elapsed:: [0-9]+ms/; +const buildVerboseLogRegEx = /^.+ \- /; +export enum HostOutputKind { + Log, + Diagnostic, + WatchDiagnostic +} +export interface HostOutputLog { + kind: HostOutputKind.Log; + expected: string; + caption?: string; +} +export interface HostOutputDiagnostic { + kind: HostOutputKind.Diagnostic; + diagnostic: Diagnostic | string; +} +export interface HostOutputWatchDiagnostic { + kind: HostOutputKind.WatchDiagnostic; + diagnostic: Diagnostic | string; +} +export type HostOutput = HostOutputLog | HostOutputDiagnostic | HostOutputWatchDiagnostic; +export function checkOutputErrors(host: WatchedSystem, expected: readonly HostOutput[], disableConsoleClears?: boolean | undefined) { + let screenClears = 0; + const outputs = host.getOutput(); + assert.equal(outputs.length, expected.length, JSON.stringify(outputs)); + let index = 0; + forEach(expected, expected => { + switch (expected.kind) { + case HostOutputKind.Log: + return assertLog(expected); + case HostOutputKind.Diagnostic: + return assertDiagnostic(expected); + case HostOutputKind.WatchDiagnostic: + return assertWatchDiagnostic(expected); + default: + return Debug.assertNever(expected); } - } - - export function hostOutputLog(expected: string, caption?: string): HostOutputLog { - return { kind: HostOutputKind.Log, expected, caption }; - } - export function hostOutputDiagnostic(diagnostic: Diagnostic | string): HostOutputDiagnostic { - return { kind: HostOutputKind.Diagnostic, diagnostic }; - } - export function hostOutputWatchDiagnostic(diagnostic: Diagnostic | string): HostOutputWatchDiagnostic { - return { kind: HostOutputKind.WatchDiagnostic, diagnostic }; - } - - export function startingCompilationInWatchMode() { - return hostOutputWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Starting_compilation_in_watch_mode)); - } - export function foundErrorsWatching(errors: readonly any[]) { - return hostOutputWatchDiagnostic(errors.length === 1 ? - createCompilerDiagnostic(Diagnostics.Found_1_error_Watching_for_file_changes) : - createCompilerDiagnostic(Diagnostics.Found_0_errors_Watching_for_file_changes, errors.length) - ); - } - export function fileChangeDetected() { - return hostOutputWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation)); - } - - export function checkOutputErrorsInitial(host: WatchedSystem, errors: readonly Diagnostic[] | readonly string[], disableConsoleClears?: boolean, logsBeforeErrors?: string[]) { - checkOutputErrors( - host, - [ - startingCompilationInWatchMode(), - ...map(logsBeforeErrors || emptyArray, expected => hostOutputLog(expected, "logBeforeError")), - ...map(errors, hostOutputDiagnostic), - foundErrorsWatching(errors) - ], - disableConsoleClears - ); - } - - export function checkOutputErrorsIncremental(host: WatchedSystem, errors: readonly Diagnostic[] | readonly string[], disableConsoleClears?: boolean, logsBeforeWatchDiagnostic?: string[], logsBeforeErrors?: string[]) { - checkOutputErrors( - host, - [ - ...map(logsBeforeWatchDiagnostic || emptyArray, expected => hostOutputLog(expected, "logsBeforeWatchDiagnostic")), - fileChangeDetected(), - ...map(logsBeforeErrors || emptyArray, expected => hostOutputLog(expected, "logBeforeError")), - ...map(errors, hostOutputDiagnostic), - foundErrorsWatching(errors) - ], - disableConsoleClears - ); - } - - export function checkOutputErrorsIncrementalWithExit(host: WatchedSystem, errors: readonly Diagnostic[] | readonly string[], expectedExitCode: ExitStatus, disableConsoleClears?: boolean, logsBeforeWatchDiagnostic?: string[], logsBeforeErrors?: string[]) { - checkOutputErrors( - host, - [ - ...map(logsBeforeWatchDiagnostic || emptyArray, expected => hostOutputLog(expected, "logsBeforeWatchDiagnostic")), - fileChangeDetected(), - ...map(logsBeforeErrors || emptyArray, expected => hostOutputLog(expected, "logBeforeError")), - ...map(errors, hostOutputDiagnostic), - ], - disableConsoleClears - ); - assert.equal(host.exitCode, expectedExitCode); - } - - export function checkNormalBuildErrors(host: WatchedSystem, errors: readonly Diagnostic[] | readonly string[], reportErrorSummary?: boolean) { - checkOutputErrors( - host, - [ - ...map(errors, hostOutputDiagnostic), - ...reportErrorSummary ? - [hostOutputWatchDiagnostic(getErrorSummaryText(errors.length, host.newLine))] : - emptyArray - ] - ); - } - - function isDiagnosticMessageChain(message: DiagnosticMessage | DiagnosticMessageChain): message is DiagnosticMessageChain { - return !!(message as DiagnosticMessageChain).messageText; - } - - export function getDiagnosticOfFileFrom(file: SourceFile | undefined, start: number | undefined, length: number | undefined, message: DiagnosticMessage | DiagnosticMessageChain, ..._args: (string | number)[]): Diagnostic { - let text: DiagnosticMessageChain | string; - if (isDiagnosticMessageChain(message)) { - text = message; + }); + assert.equal(host.screenClears.length, screenClears, "Expected number of screen clears"); + host.clearOutput(); + function isDiagnostic(diagnostic: Diagnostic | string): diagnostic is Diagnostic { + return !!(diagnostic as Diagnostic).messageText; + } + function assertDiagnostic({ diagnostic }: HostOutputDiagnostic) { + const expected = isDiagnostic(diagnostic) ? formatDiagnostic(diagnostic, host) : diagnostic; + assert.equal(outputs[index], expected, getOutputAtFailedMessage("Diagnostic", expected)); + index++; + } + function getCleanLogString(log: string) { + return log.replace(elapsedRegex, "").replace(buildVerboseLogRegEx, ""); + } + function assertLog({ caption, expected }: HostOutputLog) { + const actual = outputs[index]; + assert.equal(getCleanLogString(actual), getCleanLogString(expected), getOutputAtFailedMessage(caption || "Log", expected)); + index++; + } + function assertWatchDiagnostic({ diagnostic }: HostOutputWatchDiagnostic) { + if (isString(diagnostic)) { + assert.equal(outputs[index], diagnostic, getOutputAtFailedMessage("Diagnostic", diagnostic)); } else { - text = getLocaleSpecificMessage(message); - if (arguments.length > 4) { - text = formatStringFromArgs(text, arguments, 4); + const expected = getWatchDiagnosticWithoutDate(diagnostic); + if (!disableConsoleClears && contains(screenStartingMessageCodes, diagnostic.code)) { + assert.equal(host.screenClears[screenClears], index, `Expected screen clear at this diagnostic: ${expected}`); + screenClears++; } + assert.isTrue(endsWith(outputs[index], expected), getOutputAtFailedMessage("Watch diagnostic", expected)); } - return { - file, - start, - length, - - messageText: text, - category: message.category, - code: message.code, - }; - } - - export function getDiagnosticWithoutFile(message: DiagnosticMessage | DiagnosticMessageChain, ...args: (string | number)[]): Diagnostic { - return getDiagnosticOfFileFrom(/*file*/ undefined, /*start*/ undefined, /*length*/ undefined, message, ...args); - } - - export function getDiagnosticOfFile(file: SourceFile, start: number, length: number, message: DiagnosticMessage | DiagnosticMessageChain, ...args: (string | number)[]): Diagnostic { - return getDiagnosticOfFileFrom(file, start, length, message, ...args); - } - - export function getDiagnosticOfFileFromProgram(program: Program, filePath: string, start: number, length: number, message: DiagnosticMessage | DiagnosticMessageChain, ...args: (string | number)[]): Diagnostic { - return getDiagnosticOfFileFrom(program.getSourceFileByPath(toPath(filePath, program.getCurrentDirectory(), s => s.toLowerCase())), - start, length, message, ...args); - } - - export function getUnknownCompilerOption(program: Program, configFile: File, option: string) { - const quotedOption = `"${option}"`; - return getDiagnosticOfFile(program.getCompilerOptions().configFile!, configFile.content.indexOf(quotedOption), quotedOption.length, Diagnostics.Unknown_compiler_option_0, option); - } - - export function getUnknownDidYouMeanCompilerOption(program: Program, configFile: File, option: string, didYouMean: string) { - const quotedOption = `"${option}"`; - return getDiagnosticOfFile(program.getCompilerOptions().configFile!, configFile.content.indexOf(quotedOption), quotedOption.length, Diagnostics.Unknown_compiler_option_0_Did_you_mean_1, option, didYouMean); + index++; } - - export function getDiagnosticModuleNotFoundOfFile(program: Program, file: File, moduleName: string) { - const quotedModuleName = `"${moduleName}"`; - return getDiagnosticOfFileFromProgram(program, file.path, file.content.indexOf(quotedModuleName), quotedModuleName.length, Diagnostics.Cannot_find_module_0, moduleName); + function getOutputAtFailedMessage(caption: string, expectedOutput: string) { + return `Expected ${caption}: ${JSON.stringify(expectedOutput)} at ${index} in ${JSON.stringify(outputs)}`; } - - export type TscWatchCompileChange = (sys: TestFSWithWatch.TestServerHostTrackingWrittenFiles, programs: readonly CommandLineProgram[]) => string; - export interface TscWatchCheckOptions { - baselineSourceMap?: boolean; + function getWatchDiagnosticWithoutDate(diagnostic: Diagnostic) { + const newLines = contains(screenStartingMessageCodes, diagnostic.code) + ? `${host.newLine}${host.newLine}` + : host.newLine; + return ` - ${flattenDiagnosticMessageText(diagnostic.messageText, host.newLine)}${newLines}`; } - export interface TscWatchCompileBase extends TscWatchCheckOptions { - scenario: string; - subScenario: string; - commandLineArgs: readonly string[]; - changes: TscWatchCompileChange[]; - } - export interface TscWatchCompile extends TscWatchCompileBase { - sys: () => WatchedSystem; - maxNumberOfFilesToIterateForInvalidation?: number; +} +export function hostOutputLog(expected: string, caption?: string): HostOutputLog { + return { kind: HostOutputKind.Log, expected, caption }; +} +export function hostOutputDiagnostic(diagnostic: Diagnostic | string): HostOutputDiagnostic { + return { kind: HostOutputKind.Diagnostic, diagnostic }; +} +export function hostOutputWatchDiagnostic(diagnostic: Diagnostic | string): HostOutputWatchDiagnostic { + return { kind: HostOutputKind.WatchDiagnostic, diagnostic }; +} +export function startingCompilationInWatchMode() { + return hostOutputWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Starting_compilation_in_watch_mode)); +} +export function foundErrorsWatching(errors: readonly any[]) { + return hostOutputWatchDiagnostic(errors.length === 1 ? + createCompilerDiagnostic(Diagnostics.Found_1_error_Watching_for_file_changes) : + createCompilerDiagnostic(Diagnostics.Found_0_errors_Watching_for_file_changes, errors.length)); +} +export function fileChangeDetected() { + return hostOutputWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation)); +} +export function checkOutputErrorsInitial(host: WatchedSystem, errors: readonly Diagnostic[] | readonly string[], disableConsoleClears?: boolean, logsBeforeErrors?: string[]) { + checkOutputErrors(host, [ + startingCompilationInWatchMode(), + ...map(logsBeforeErrors || emptyArray, expected => hostOutputLog(expected, "logBeforeError")), + ...map(errors, hostOutputDiagnostic), + foundErrorsWatching(errors) + ], disableConsoleClears); +} +export function checkOutputErrorsIncremental(host: WatchedSystem, errors: readonly Diagnostic[] | readonly string[], disableConsoleClears?: boolean, logsBeforeWatchDiagnostic?: string[], logsBeforeErrors?: string[]) { + checkOutputErrors(host, [ + ...map(logsBeforeWatchDiagnostic || emptyArray, expected => hostOutputLog(expected, "logsBeforeWatchDiagnostic")), + fileChangeDetected(), + ...map(logsBeforeErrors || emptyArray, expected => hostOutputLog(expected, "logBeforeError")), + ...map(errors, hostOutputDiagnostic), + foundErrorsWatching(errors) + ], disableConsoleClears); +} +export function checkOutputErrorsIncrementalWithExit(host: WatchedSystem, errors: readonly Diagnostic[] | readonly string[], expectedExitCode: ExitStatus, disableConsoleClears?: boolean, logsBeforeWatchDiagnostic?: string[], logsBeforeErrors?: string[]) { + checkOutputErrors(host, [ + ...map(logsBeforeWatchDiagnostic || emptyArray, expected => hostOutputLog(expected, "logsBeforeWatchDiagnostic")), + fileChangeDetected(), + ...map(logsBeforeErrors || emptyArray, expected => hostOutputLog(expected, "logBeforeError")), + ...map(errors, hostOutputDiagnostic), + ], disableConsoleClears); + assert.equal(host.exitCode, expectedExitCode); +} +export function checkNormalBuildErrors(host: WatchedSystem, errors: readonly Diagnostic[] | readonly string[], reportErrorSummary?: boolean) { + checkOutputErrors(host, [ + ...map(errors, hostOutputDiagnostic), + ...reportErrorSummary ? + [hostOutputWatchDiagnostic(getErrorSummaryText(errors.length, host.newLine))] : emptyArray + ]); +} +function isDiagnosticMessageChain(message: DiagnosticMessage | DiagnosticMessageChain): message is DiagnosticMessageChain { + return !!(message as DiagnosticMessageChain).messageText; +} +export function getDiagnosticOfFileFrom(file: SourceFile | undefined, start: number | undefined, length: number | undefined, message: DiagnosticMessage | DiagnosticMessageChain, ..._args: (string | number)[]): Diagnostic { + let text: DiagnosticMessageChain | string; + if (isDiagnosticMessageChain(message)) { + text = message; + } + else { + text = getLocaleSpecificMessage(message); + if (arguments.length > 4) { + text = formatStringFromArgs(text, arguments, 4); + } } - - export type SystemSnap = ReturnType; - function tscWatchCompile(input: TscWatchCompile) { - it("Generates files matching the baseline", () => { - const sys = TestFSWithWatch.changeToHostTrackingWrittenFiles( - fakes.patchHostForBuildInfoReadWrite(input.sys()) - ); - const { - scenario, subScenario, - commandLineArgs, changes, - baselineSourceMap - } = input; - - const { cb, getPrograms } = commandLineCallbacks(sys); - executeCommandLine( - sys, - cb, - commandLineArgs, - input.maxNumberOfFilesToIterateForInvalidation - ); - runWatchBaseline({ - scenario, - subScenario, - commandLineArgs, - sys, - getPrograms, - baselineSourceMap, - changes - }); + return { + file, + start, + length, + messageText: text, + category: message.category, + code: message.code, + }; +} +export function getDiagnosticWithoutFile(message: DiagnosticMessage | DiagnosticMessageChain, ...args: (string | number)[]): Diagnostic { + return getDiagnosticOfFileFrom(/*file*/ undefined, /*start*/ undefined, /*length*/ undefined, message, ...args); +} +export function getDiagnosticOfFile(file: SourceFile, start: number, length: number, message: DiagnosticMessage | DiagnosticMessageChain, ...args: (string | number)[]): Diagnostic { + return getDiagnosticOfFileFrom(file, start, length, message, ...args); +} +export function getDiagnosticOfFileFromProgram(program: Program, filePath: string, start: number, length: number, message: DiagnosticMessage | DiagnosticMessageChain, ...args: (string | number)[]): Diagnostic { + return getDiagnosticOfFileFrom(program.getSourceFileByPath(toPath(filePath, program.getCurrentDirectory(), s => s.toLowerCase())), start, length, message, ...args); +} +export function getUnknownCompilerOption(program: Program, configFile: File, option: string) { + const quotedOption = `"${option}"`; + return getDiagnosticOfFile((program.getCompilerOptions().configFile!), configFile.content.indexOf(quotedOption), quotedOption.length, Diagnostics.Unknown_compiler_option_0, option); +} +export function getUnknownDidYouMeanCompilerOption(program: Program, configFile: File, option: string, didYouMean: string) { + const quotedOption = `"${option}"`; + return getDiagnosticOfFile((program.getCompilerOptions().configFile!), configFile.content.indexOf(quotedOption), quotedOption.length, Diagnostics.Unknown_compiler_option_0_Did_you_mean_1, option, didYouMean); +} +export function getDiagnosticModuleNotFoundOfFile(program: Program, file: File, moduleName: string) { + const quotedModuleName = `"${moduleName}"`; + return getDiagnosticOfFileFromProgram(program, file.path, file.content.indexOf(quotedModuleName), quotedModuleName.length, Diagnostics.Cannot_find_module_0, moduleName); +} +export type TscWatchCompileChange = (sys: TestFSWithWatch.TestServerHostTrackingWrittenFiles, programs: readonly CommandLineProgram[]) => string; +export interface TscWatchCheckOptions { + baselineSourceMap?: boolean; +} +export interface TscWatchCompileBase extends TscWatchCheckOptions { + scenario: string; + subScenario: string; + commandLineArgs: readonly string[]; + changes: TscWatchCompileChange[]; +} +export interface TscWatchCompile extends TscWatchCompileBase { + sys: () => WatchedSystem; + maxNumberOfFilesToIterateForInvalidation?: number; +} +export type SystemSnap = ReturnType; +function tscWatchCompile(input: TscWatchCompile) { + it("Generates files matching the baseline", () => { + const sys = TestFSWithWatch.changeToHostTrackingWrittenFiles(patchHostForBuildInfoReadWrite(input.sys())); + const { scenario, subScenario, commandLineArgs, changes, baselineSourceMap } = input; + const { cb, getPrograms } = commandLineCallbacks(sys); + executeCommandLine(sys, cb, commandLineArgs, input.maxNumberOfFilesToIterateForInvalidation); + runWatchBaseline({ + scenario, + subScenario, + commandLineArgs, + sys, + getPrograms, + baselineSourceMap, + changes }); - } - - export interface RunWatchBaseline extends TscWatchCompileBase { - sys: TestFSWithWatch.TestServerHostTrackingWrittenFiles; - getPrograms: () => readonly CommandLineProgram[]; - } - export function runWatchBaseline({ - scenario, subScenario, commandLineArgs, - getPrograms, sys, - baselineSourceMap, - changes - }: RunWatchBaseline) { - const baseline: string[] = []; - baseline.push(`${sys.getExecutingFilePath()} ${commandLineArgs.join(" ")}`); - let programs = watchBaseline({ + }); +} +export interface RunWatchBaseline extends TscWatchCompileBase { + sys: TestFSWithWatch.TestServerHostTrackingWrittenFiles; + getPrograms: () => readonly CommandLineProgram[]; +} +export function runWatchBaseline({ scenario, subScenario, commandLineArgs, getPrograms, sys, baselineSourceMap, changes }: RunWatchBaseline) { + const baseline: string[] = []; + baseline.push(`${sys.getExecutingFilePath()} ${commandLineArgs.join(" ")}`); + let programs = watchBaseline({ + baseline, + getPrograms, + sys, + oldSnap: undefined, + baselineSourceMap + }); + for (const change of changes) { + const oldSnap = sys.snap(); + const caption = change(sys, programs); + baseline.push(`Change:: ${caption}`, ""); + programs = watchBaseline({ baseline, getPrograms, sys, - oldSnap: undefined, + oldSnap, baselineSourceMap }); - - for (const change of changes) { - const oldSnap = sys.snap(); - const caption = change(sys, programs); - baseline.push(`Change:: ${caption}`, ""); - programs = watchBaseline({ - baseline, - getPrograms, - sys, - oldSnap, - baselineSourceMap - }); - } - Harness.Baseline.runBaseline(`${isBuild(commandLineArgs) ? "tsbuild/watchMode" : "tscWatch"}/${scenario}/${subScenario.split(" ").join("-")}.js`, baseline.join("\r\n")); - } - - export interface WatchBaseline extends TscWatchCheckOptions { - baseline: string[]; - sys: TestFSWithWatch.TestServerHostTrackingWrittenFiles; - getPrograms: () => readonly CommandLineProgram[]; - oldSnap: SystemSnap | undefined; - } - export function watchBaseline({ baseline, getPrograms, sys, oldSnap, baselineSourceMap }: WatchBaseline) { - if (baselineSourceMap) generateSourceMapBaselineFiles(sys); - sys.diff(baseline, oldSnap); - sys.serializeOutput(baseline); - const programs = getPrograms(); - for (const program of programs) { - baselineProgram(baseline, program); - } - sys.serializeWatches(baseline); - baseline.push(`exitCode:: ExitStatus.${ExitStatus[sys.exitCode as ExitStatus]}`, ""); - sys.writtenFiles.forEach((value, key) => { - assert.equal(value, 1, `Expected to write file ${key} only once`); - }); - sys.writtenFiles.clear(); - return programs; } - - function baselineProgram(baseline: string[], [program, builderProgram]: CommandLineProgram) { - const options = program.getCompilerOptions(); - baseline.push(`Program root files: ${JSON.stringify(program.getRootFileNames())}`); - baseline.push(`Program options: ${JSON.stringify(options)}`); - baseline.push("Program files::"); + Baseline.runBaseline(`${isBuild(commandLineArgs) ? "tsbuild/watchMode" : "tscWatch"}/${scenario}/${subScenario.split(" ").join("-")}.js`, baseline.join("\r\n")); +} +export interface WatchBaseline extends TscWatchCheckOptions { + baseline: string[]; + sys: TestFSWithWatch.TestServerHostTrackingWrittenFiles; + getPrograms: () => readonly CommandLineProgram[]; + oldSnap: SystemSnap | undefined; +} +export function watchBaseline({ baseline, getPrograms, sys, oldSnap, baselineSourceMap }: WatchBaseline) { + if (baselineSourceMap) + generateSourceMapBaselineFiles(sys); + sys.diff(baseline, oldSnap); + sys.serializeOutput(baseline); + const programs = getPrograms(); + for (const program of programs) { + baselineProgram(baseline, program); + } + sys.serializeWatches(baseline); + baseline.push(`exitCode:: ExitStatus.${ExitStatus[sys.exitCode as ExitStatus]}`, ""); + sys.writtenFiles.forEach((value, key) => { + assert.equal(value, 1, `Expected to write file ${key} only once`); + }); + sys.writtenFiles.clear(); + return programs; +} +function baselineProgram(baseline: string[], [program, builderProgram]: CommandLineProgram) { + const options = program.getCompilerOptions(); + baseline.push(`Program root files: ${JSON.stringify(program.getRootFileNames())}`); + baseline.push(`Program options: ${JSON.stringify(options)}`); + baseline.push("Program files::"); + for (const file of program.getSourceFiles()) { + baseline.push(file.fileName); + } + baseline.push(""); + if (!builderProgram) + return; + const state = builderProgram.getState(); + if (state.semanticDiagnosticsPerFile?.size) { + baseline.push("Semantic diagnostics in builder refreshed for::"); for (const file of program.getSourceFiles()) { - baseline.push(file.fileName); - } - baseline.push(""); - if (!builderProgram) return; - const state = builderProgram.getState(); - if (state.semanticDiagnosticsPerFile?.size) { - baseline.push("Semantic diagnostics in builder refreshed for::"); - for (const file of program.getSourceFiles()) { - if (!state.semanticDiagnosticsFromOldState || !state.semanticDiagnosticsFromOldState.has(file.resolvedPath)) { - baseline.push(file.fileName); - } + if (!state.semanticDiagnosticsFromOldState || !state.semanticDiagnosticsFromOldState.has(file.resolvedPath)) { + baseline.push(file.fileName); } } - else { - baseline.push("No cached semantic diagnostics in the builder::"); - } - baseline.push(""); } - - export function verifyTscWatch(input: TscWatchCompile) { - describe(input.scenario, () => { - describe(input.subScenario, () => { - tscWatchCompile(input); - }); - }); - } - - export function replaceFileText(sys: WatchedSystem, file: string, searchValue: string | RegExp, replaceValue: string) { - const content = Debug.checkDefined(sys.readFile(file)); - sys.writeFile(file, content.replace(searchValue, replaceValue)); + else { + baseline.push("No cached semantic diagnostics in the builder::"); } + baseline.push(""); +} +export function verifyTscWatch(input: TscWatchCompile) { + describe(input.scenario, () => { + describe(input.subScenario, () => { + tscWatchCompile(input); + }); + }); +} +export function replaceFileText(sys: WatchedSystem, file: string, searchValue: string | RegExp, replaceValue: string) { + const content = Debug.checkDefined(sys.readFile(file)); + sys.writeFile(file, content.replace(searchValue, replaceValue)); } diff --git a/src/testRunner/unittests/tscWatch/incremental.ts b/src/testRunner/unittests/tscWatch/incremental.ts index 845d72e216070..cd7cc084e3d6d 100644 --- a/src/testRunner/unittests/tscWatch/incremental.ts +++ b/src/testRunner/unittests/tscWatch/incremental.ts @@ -1,194 +1,170 @@ -namespace ts.tscWatch { - describe("unittests:: tsc-watch:: emit file --incremental", () => { - const project = "/users/username/projects/project"; - - const configFile: File = { - path: `${project}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { incremental: true } }) - }; - - interface VerifyIncrementalWatchEmitInput { - subScenario: string; - files: () => readonly File[]; - optionsToExtend?: readonly string[]; - modifyFs?: (host: WatchedSystem) => void; +import { File, WatchedSystem, createWatchedSystem, SystemSnap, watchBaseline, libFile } from "../../ts.tscWatch"; +import { TestFSWithWatch, emptyArray, commandLineCallbacks, isBuild, executeCommandLine, createDiagnosticReporter, parseConfigFileWithSystem, performIncrementalCompilation, getConfigFileParsingDiagnostics, noop, createIncrementalProgram, createIncrementalCompilerHost, ModuleKind, Path, Diagnostics } from "../../ts"; +import { patchHostForBuildInfoReadWrite } from "../../fakes"; +import { Baseline } from "../../Harness"; +describe("unittests:: tsc-watch:: emit file --incremental", () => { + const project = "/users/username/projects/project"; + const configFile: File = { + path: `${project}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { incremental: true } }) + }; + interface VerifyIncrementalWatchEmitInput { + subScenario: string; + files: () => readonly File[]; + optionsToExtend?: readonly string[]; + modifyFs?: (host: WatchedSystem) => void; + } + function verifyIncrementalWatchEmit(input: VerifyIncrementalWatchEmitInput) { + describe(input.subScenario, () => { + it("with tsc --w", () => { + verifyIncrementalWatchEmitWorker(input, /*incremental*/ false); + }); + it("with tsc", () => { + verifyIncrementalWatchEmitWorker(input, /*incremental*/ true); + }); + }); + } + function verifyIncrementalWatchEmitWorker({ subScenario, files, optionsToExtend, modifyFs }: VerifyIncrementalWatchEmitInput, incremental: boolean) { + const sys = TestFSWithWatch.changeToHostTrackingWrittenFiles(patchHostForBuildInfoReadWrite(createWatchedSystem(files(), { currentDirectory: project }))); + if (incremental) + sys.exit = exitCode => sys.exitCode = exitCode; + const argsToPass = [incremental ? "-i" : "-w", ...(optionsToExtend || emptyArray)]; + const baseline: string[] = []; + baseline.push(`${sys.getExecutingFilePath()} ${argsToPass.join(" ")}`); + const { cb, getPrograms } = commandLineCallbacks(sys); + build(/*oldSnap*/ undefined); + if (modifyFs) { + const oldSnap = sys.snap(); + modifyFs(sys); + baseline.push(`Change::`, ""); + build(oldSnap); } - function verifyIncrementalWatchEmit(input: VerifyIncrementalWatchEmitInput) { - describe(input.subScenario, () => { - it("with tsc --w", () => { - verifyIncrementalWatchEmitWorker(input, /*incremental*/ false); - }); - it("with tsc", () => { - verifyIncrementalWatchEmitWorker(input, /*incremental*/ true); - }); + Baseline.runBaseline(`${isBuild(argsToPass) ? "tsbuild/watchMode" : "tscWatch"}/incremental/${subScenario.split(" ").join("-")}-${incremental ? "incremental" : "watch"}.js`, baseline.join("\r\n")); + function build(oldSnap: SystemSnap | undefined) { + const closer = executeCommandLine(sys, cb, argsToPass); + watchBaseline({ + baseline, + getPrograms, + sys, + oldSnap }); + if (closer) + closer.close(); } - - function verifyIncrementalWatchEmitWorker( - { subScenario, files, optionsToExtend, modifyFs }: VerifyIncrementalWatchEmitInput, - incremental: boolean - ) { - const sys = TestFSWithWatch.changeToHostTrackingWrittenFiles( - fakes.patchHostForBuildInfoReadWrite(createWatchedSystem(files(), { currentDirectory: project })) - ); - if (incremental) sys.exit = exitCode => sys.exitCode = exitCode; - const argsToPass = [incremental ? "-i" : "-w", ...(optionsToExtend || emptyArray)]; - const baseline: string[] = []; - baseline.push(`${sys.getExecutingFilePath()} ${argsToPass.join(" ")}`); - const { cb, getPrograms } = commandLineCallbacks(sys); - build(/*oldSnap*/ undefined); - - if (modifyFs) { - const oldSnap = sys.snap(); - modifyFs(sys); - baseline.push(`Change::`, ""); - build(oldSnap); - } - - Harness.Baseline.runBaseline(`${isBuild(argsToPass) ? "tsbuild/watchMode" : "tscWatch"}/incremental/${subScenario.split(" ").join("-")}-${incremental ? "incremental" : "watch"}.js`, baseline.join("\r\n")); - - function build(oldSnap: SystemSnap | undefined) { - const closer = executeCommandLine( - sys, - cb, - argsToPass, - ); - watchBaseline({ - baseline, - getPrograms, - sys, - oldSnap + } + describe("non module compilation", () => { + const file1: File = { + path: `${project}/file1.ts`, + content: "const x = 10;" + }; + const file2: File = { + path: `${project}/file2.ts`, + content: "const y = 20;" + }; + describe("own file emit without errors", () => { + function verify(subScenario: string, optionsToExtend?: readonly string[]) { + const modifiedFile2Content = file2.content.replace("y", "z").replace("20", "10"); + verifyIncrementalWatchEmit({ + files: () => [libFile, file1, file2, configFile], + optionsToExtend, + subScenario: `own file emit without errors/${subScenario}`, + modifyFs: host => host.writeFile(file2.path, modifiedFile2Content), }); - if (closer) closer.close(); } - } - - describe("non module compilation", () => { - const file1: File = { - path: `${project}/file1.ts`, - content: "const x = 10;" - }; - const file2: File = { - path: `${project}/file2.ts`, - content: "const y = 20;" - }; - describe("own file emit without errors", () => { - function verify(subScenario: string, optionsToExtend?: readonly string[]) { - const modifiedFile2Content = file2.content.replace("y", "z").replace("20", "10"); - verifyIncrementalWatchEmit({ - files: () => [libFile, file1, file2, configFile], - optionsToExtend, - subScenario: `own file emit without errors/${subScenario}`, - modifyFs: host => host.writeFile(file2.path, modifiedFile2Content), - }); - } - verify("without commandline options"); - verify("with commandline parameters that are not relative", ["-p", "tsconfig.json"]); - }); - - verifyIncrementalWatchEmit({ - files: () => [libFile, file1, configFile, { + verify("without commandline options"); + verify("with commandline parameters that are not relative", ["-p", "tsconfig.json"]); + }); + verifyIncrementalWatchEmit({ + files: () => [libFile, file1, configFile, { path: file2.path, content: `const y: string = 20;` }], - subScenario: "own file emit with errors", - modifyFs: host => host.writeFile(file1.path, file1.content.replace("x", "z")), - }); - - verifyIncrementalWatchEmit({ - files: () => [libFile, file1, file2, { + subScenario: "own file emit with errors", + modifyFs: host => host.writeFile(file1.path, file1.content.replace("x", "z")), + }); + verifyIncrementalWatchEmit({ + files: () => [libFile, file1, file2, { path: configFile.path, content: JSON.stringify({ compilerOptions: { incremental: true, outFile: "out.js" } }) }], - subScenario: "with --out", - }); + subScenario: "with --out", }); - - describe("module compilation", () => { - const file1: File = { - path: `${project}/file1.ts`, - content: "export const x = 10;" - }; - const file2: File = { - path: `${project}/file2.ts`, - content: "export const y = 20;" - }; - const config: File = { - path: configFile.path, - content: JSON.stringify({ compilerOptions: { incremental: true, module: "amd" } }) + }); + describe("module compilation", () => { + const file1: File = { + path: `${project}/file1.ts`, + content: "export const x = 10;" + }; + const file2: File = { + path: `${project}/file2.ts`, + content: "export const y = 20;" + }; + const config: File = { + path: configFile.path, + content: JSON.stringify({ compilerOptions: { incremental: true, module: "amd" } }) + }; + verifyIncrementalWatchEmit({ + files: () => [libFile, file1, file2, config], + subScenario: "module compilation/own file emit without errors", + modifyFs: host => host.writeFile(file2.path, file2.content.replace("y", "z").replace("20", "10")), + }); + describe("own file emit with errors", () => { + const fileModified: File = { + path: file2.path, + content: `export const y: string = 20;` }; - verifyIncrementalWatchEmit({ - files: () => [libFile, file1, file2, config], - subScenario: "module compilation/own file emit without errors", - modifyFs: host => host.writeFile(file2.path, file2.content.replace("y", "z").replace("20", "10")), + files: () => [libFile, file1, fileModified, config], + subScenario: "module compilation/own file emit with errors", + modifyFs: host => host.writeFile(file1.path, file1.content.replace("x = 10", "z = 10")), }); - - describe("own file emit with errors", () => { - const fileModified: File = { - path: file2.path, - content: `export const y: string = 20;` - }; - - verifyIncrementalWatchEmit({ - files: () => [libFile, file1, fileModified, config], - subScenario: "module compilation/own file emit with errors", - modifyFs: host => host.writeFile(file1.path, file1.content.replace("x = 10", "z = 10")), + it("verify that state is read correctly", () => { + const system = createWatchedSystem([libFile, file1, fileModified, config], { currentDirectory: project }); + const reportDiagnostic = createDiagnosticReporter(system); + const parsedConfig = (parseConfigFileWithSystem("tsconfig.json", {}, /*watchOptionsToExtend*/ undefined, system, reportDiagnostic)!); + performIncrementalCompilation({ + rootNames: parsedConfig.fileNames, + options: parsedConfig.options, + projectReferences: parsedConfig.projectReferences, + configFileParsingDiagnostics: getConfigFileParsingDiagnostics(parsedConfig), + reportDiagnostic, + system }); - - it("verify that state is read correctly", () => { - const system = createWatchedSystem([libFile, file1, fileModified, config], { currentDirectory: project }); - const reportDiagnostic = createDiagnosticReporter(system); - const parsedConfig = parseConfigFileWithSystem("tsconfig.json", {}, /*watchOptionsToExtend*/ undefined, system, reportDiagnostic)!; - performIncrementalCompilation({ - rootNames: parsedConfig.fileNames, - options: parsedConfig.options, - projectReferences: parsedConfig.projectReferences, - configFileParsingDiagnostics: getConfigFileParsingDiagnostics(parsedConfig), - reportDiagnostic, - system - }); - - const command = parseConfigFileWithSystem("tsconfig.json", {}, /*watchOptionsToExtend*/ undefined, system, noop)!; - const builderProgram = createIncrementalProgram({ - rootNames: command.fileNames, - options: command.options, - projectReferences: command.projectReferences, - configFileParsingDiagnostics: getConfigFileParsingDiagnostics(command), - host: createIncrementalCompilerHost(command.options, system) - }); - - const state = builderProgram.getState(); - assert.equal(state.changedFilesSet!.size, 0, "changes"); - - assert.equal(state.fileInfos.size, 3, "FileInfo size"); - assert.deepEqual(state.fileInfos.get(libFile.path), { - version: system.createHash(libFile.content), - signature: system.createHash(libFile.content) - }); - assert.deepEqual(state.fileInfos.get(file1.path), { - version: system.createHash(file1.content), - signature: system.createHash(`${file1.content.replace("export ", "export declare ")}\n`) - }); - assert.deepEqual(state.fileInfos.get(file2.path), { - version: system.createHash(fileModified.content), - signature: system.createHash("export declare const y: string;\n") - }); - - assert.deepEqual(state.compilerOptions, { - incremental: true, - module: ModuleKind.AMD, - configFilePath: config.path - }); - - assert.equal(state.referencedMap!.size, 0); - assert.equal(state.exportedModulesMap!.size, 0); - - assert.equal(state.semanticDiagnosticsPerFile!.size, 3); - assert.deepEqual(state.semanticDiagnosticsPerFile!.get(libFile.path), emptyArray); - assert.deepEqual(state.semanticDiagnosticsPerFile!.get(file1.path), emptyArray); - assert.deepEqual(state.semanticDiagnosticsPerFile!.get(file2.path), [{ - file: state.program!.getSourceFileByPath(file2.path as Path)!, + const command = (parseConfigFileWithSystem("tsconfig.json", {}, /*watchOptionsToExtend*/ undefined, system, noop)!); + const builderProgram = createIncrementalProgram({ + rootNames: command.fileNames, + options: command.options, + projectReferences: command.projectReferences, + configFileParsingDiagnostics: getConfigFileParsingDiagnostics(command), + host: createIncrementalCompilerHost(command.options, system) + }); + const state = builderProgram.getState(); + assert.equal(state.changedFilesSet!.size, 0, "changes"); + assert.equal(state.fileInfos.size, 3, "FileInfo size"); + assert.deepEqual(state.fileInfos.get(libFile.path), { + version: system.createHash(libFile.content), + signature: system.createHash(libFile.content) + }); + assert.deepEqual(state.fileInfos.get(file1.path), { + version: system.createHash(file1.content), + signature: system.createHash(`${file1.content.replace("export ", "export declare ")}\n`) + }); + assert.deepEqual(state.fileInfos.get(file2.path), { + version: system.createHash(fileModified.content), + signature: system.createHash("export declare const y: string;\n") + }); + assert.deepEqual(state.compilerOptions, { + incremental: true, + module: ModuleKind.AMD, + configFilePath: config.path + }); + assert.equal(state.referencedMap!.size, 0); + assert.equal(state.exportedModulesMap!.size, 0); + assert.equal(state.semanticDiagnosticsPerFile!.size, 3); + assert.deepEqual(state.semanticDiagnosticsPerFile!.get(libFile.path), emptyArray); + assert.deepEqual(state.semanticDiagnosticsPerFile!.get(file1.path), emptyArray); + assert.deepEqual(state.semanticDiagnosticsPerFile!.get(file2.path), [{ + file: (state.program!.getSourceFileByPath((file2.path as Path))!), start: 13, length: 1, code: Diagnostics.Type_0_is_not_assignable_to_type_1.code, @@ -198,72 +174,69 @@ namespace ts.tscWatch { reportsUnnecessary: undefined, source: undefined }]); - }); }); - - verifyIncrementalWatchEmit({ - files: () => [libFile, file1, file2, { + }); + verifyIncrementalWatchEmit({ + files: () => [libFile, file1, file2, { path: configFile.path, content: JSON.stringify({ compilerOptions: { incremental: true, module: "amd", outFile: "out.js" } }) }], - subScenario: "module compilation/with --out", - }); + subScenario: "module compilation/with --out", }); - - verifyIncrementalWatchEmit({ - files: () => { - const config: File = { - path: configFile.path, - content: JSON.stringify({ - compilerOptions: { - incremental: true, - target: "es5", - module: "commonjs", - declaration: true, - emitDeclarationOnly: true - } - }) - }; - const aTs: File = { - path: `${project}/a.ts`, - content: `import { B } from "./b"; + }); + verifyIncrementalWatchEmit({ + files: () => { + const config: File = { + path: configFile.path, + content: JSON.stringify({ + compilerOptions: { + incremental: true, + target: "es5", + module: "commonjs", + declaration: true, + emitDeclarationOnly: true + } + }) + }; + const aTs: File = { + path: `${project}/a.ts`, + content: `import { B } from "./b"; export interface A { b: B; } ` - }; - const bTs: File = { - path: `${project}/b.ts`, - content: `import { C } from "./c"; + }; + const bTs: File = { + path: `${project}/b.ts`, + content: `import { C } from "./c"; export interface B { b: C; } ` - }; - const cTs: File = { - path: `${project}/c.ts`, - content: `import { A } from "./a"; + }; + const cTs: File = { + path: `${project}/c.ts`, + content: `import { A } from "./a"; export interface C { a: A; } ` - }; - const indexTs: File = { - path: `${project}/index.ts`, - content: `export { A } from "./a"; + }; + const indexTs: File = { + path: `${project}/index.ts`, + content: `export { A } from "./a"; export { B } from "./b"; export { C } from "./c"; ` - }; - return [libFile, aTs, bTs, cTs, indexTs, config]; - }, - subScenario: "incremental with circular references", - modifyFs: host => host.writeFile(`${project}/a.ts`, `import { B } from "./b"; + }; + return [libFile, aTs, bTs, cTs, indexTs, config]; + }, + subScenario: "incremental with circular references", + modifyFs: host => host.writeFile(`${project}/a.ts`, `import { B } from "./b"; export interface A { b: B; foo: any; } `) - }); }); -} +}); diff --git a/src/testRunner/unittests/tscWatch/programUpdates.ts b/src/testRunner/unittests/tscWatch/programUpdates.ts index b500092269bb6..36fa3fab6e247 100644 --- a/src/testRunner/unittests/tscWatch/programUpdates.ts +++ b/src/testRunner/unittests/tscWatch/programUpdates.ts @@ -1,957 +1,915 @@ -namespace ts.tscWatch { - describe("unittests:: tsc-watch:: program updates", () => { - const scenario = "programUpdates"; - const configFilePath = "/a/b/tsconfig.json"; - const configFile: File = { - path: configFilePath, - content: `{}` - }; - verifyTscWatch({ - scenario, - subScenario: "create watch without config file", - commandLineArgs: ["-w", "/a/b/c/app.ts"], - sys: () => { - const appFile: File = { - path: "/a/b/c/app.ts", - content: ` +import { File, verifyTscWatch, createWatchedSystem, libFile, commonFile1, commonFile2, createWatchOfFilesAndCompilerOptions, checkProgramActualFiles, WatchedSystem, projectRoot, replaceFileText } from "../../ts.tscWatch"; +import { emptyArray, getDirectoryPath, CompilerOptions, generateTSConfig, ModuleKind } from "../../ts"; +describe("unittests:: tsc-watch:: program updates", () => { + const scenario = "programUpdates"; + const configFilePath = "/a/b/tsconfig.json"; + const configFile: File = { + path: configFilePath, + content: `{}` + }; + verifyTscWatch({ + scenario, + subScenario: "create watch without config file", + commandLineArgs: ["-w", "/a/b/c/app.ts"], + sys: () => { + const appFile: File = { + path: "/a/b/c/app.ts", + content: ` import {f} from "./module" console.log(f) ` - }; - - const moduleFile: File = { - path: "/a/b/c/module.d.ts", - content: `export let x: number` - }; - return createWatchedSystem([appFile, moduleFile, libFile]); - }, - changes: emptyArray - }); - - verifyTscWatch({ - scenario, - subScenario: "can handle tsconfig file name with difference casing", - commandLineArgs: ["-w", "-p", "/A/B/tsconfig.json"], - sys: () => { - const f1 = { - path: "/a/b/app.ts", - content: "let x = 1" - }; - const config = { - path: configFilePath, - content: JSON.stringify({ - include: ["app.ts"] - }) - }; - return createWatchedSystem([f1, libFile, config], { useCaseSensitiveFileNames: false }); - }, - changes: emptyArray - }); - - verifyTscWatch({ - scenario, - subScenario: "create configured project without file list", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => { - const configFile: File = { - path: configFilePath, - content: ` + }; + const moduleFile: File = { + path: "/a/b/c/module.d.ts", + content: `export let x: number` + }; + return createWatchedSystem([appFile, moduleFile, libFile]); + }, + changes: emptyArray + }); + verifyTscWatch({ + scenario, + subScenario: "can handle tsconfig file name with difference casing", + commandLineArgs: ["-w", "-p", "/A/B/tsconfig.json"], + sys: () => { + const f1 = { + path: "/a/b/app.ts", + content: "let x = 1" + }; + const config = { + path: configFilePath, + content: JSON.stringify({ + include: ["app.ts"] + }) + }; + return createWatchedSystem([f1, libFile, config], { useCaseSensitiveFileNames: false }); + }, + changes: emptyArray + }); + verifyTscWatch({ + scenario, + subScenario: "create configured project without file list", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => { + const configFile: File = { + path: configFilePath, + content: ` { "compilerOptions": {}, "exclude": [ "e" ] }` - }; - const file1: File = { - path: "/a/b/c/f1.ts", - content: "let x = 1" - }; - const file2: File = { - path: "/a/b/d/f2.ts", - content: "let y = 1" - }; - const file3: File = { - path: "/a/b/e/f3.ts", - content: "let z = 1" - }; - return createWatchedSystem([configFile, libFile, file1, file2, file3]); - }, - changes: emptyArray - }); - - verifyTscWatch({ - scenario, - subScenario: "add new files to a configured program without file list", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => createWatchedSystem([commonFile1, libFile, configFile]), - changes: [ - sys => { - sys.writeFile(commonFile2.path, commonFile2.content); - sys.checkTimeoutQueueLengthAndRun(1); - return "Create commonFile2"; - } - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "should ignore non-existing files specified in the config file", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => { - const configFile: File = { - path: configFilePath, - content: `{ + }; + const file1: File = { + path: "/a/b/c/f1.ts", + content: "let x = 1" + }; + const file2: File = { + path: "/a/b/d/f2.ts", + content: "let y = 1" + }; + const file3: File = { + path: "/a/b/e/f3.ts", + content: "let z = 1" + }; + return createWatchedSystem([configFile, libFile, file1, file2, file3]); + }, + changes: emptyArray + }); + verifyTscWatch({ + scenario, + subScenario: "add new files to a configured program without file list", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => createWatchedSystem([commonFile1, libFile, configFile]), + changes: [ + sys => { + sys.writeFile(commonFile2.path, commonFile2.content); + sys.checkTimeoutQueueLengthAndRun(1); + return "Create commonFile2"; + } + ] + }); + verifyTscWatch({ + scenario, + subScenario: "should ignore non-existing files specified in the config file", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => { + const configFile: File = { + path: configFilePath, + content: `{ "compilerOptions": {}, "files": [ "commonFile1.ts", "commonFile3.ts" ] }` - }; - return createWatchedSystem([commonFile1, commonFile2, libFile, configFile]); - }, - changes: emptyArray - }); - - verifyTscWatch({ - scenario, - subScenario: "handle recreated files correctly", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => { - return createWatchedSystem([libFile, commonFile1, commonFile2, configFile]); + }; + return createWatchedSystem([commonFile1, commonFile2, libFile, configFile]); + }, + changes: emptyArray + }); + verifyTscWatch({ + scenario, + subScenario: "handle recreated files correctly", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => { + return createWatchedSystem([libFile, commonFile1, commonFile2, configFile]); + }, + changes: [ + sys => { + sys.deleteFile(commonFile2.path); + sys.checkTimeoutQueueLengthAndRun(1); + return "delete file2"; }, - changes: [ - sys => { - sys.deleteFile(commonFile2.path); - sys.checkTimeoutQueueLengthAndRun(1); - return "delete file2"; - }, - sys => { - sys.writeFile(commonFile2.path, commonFile2.content); - sys.checkTimeoutQueueLengthAndRun(1); - return "recreate file2"; - } - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "handles the missing files - that were added to program because they were added with tripleSlashRefs", - commandLineArgs: ["-w", "/a/b/commonFile1.ts"], - sys: () => { - const file1: File = { - path: commonFile1.path, - content: `/// + sys => { + sys.writeFile(commonFile2.path, commonFile2.content); + sys.checkTimeoutQueueLengthAndRun(1); + return "recreate file2"; + } + ] + }); + verifyTscWatch({ + scenario, + subScenario: "handles the missing files - that were added to program because they were added with tripleSlashRefs", + commandLineArgs: ["-w", "/a/b/commonFile1.ts"], + sys: () => { + const file1: File = { + path: commonFile1.path, + content: `/// let x = y` - }; - return createWatchedSystem([file1, libFile]); - }, - changes: [ - sys => { - sys.writeFile(commonFile2.path, commonFile2.content); - sys.checkTimeoutQueueLengthAndRun(1); - return "create file2"; - } - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "should reflect change in config file", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => { - const configFile: File = { - path: configFilePath, - content: `{ + }; + return createWatchedSystem([file1, libFile]); + }, + changes: [ + sys => { + sys.writeFile(commonFile2.path, commonFile2.content); + sys.checkTimeoutQueueLengthAndRun(1); + return "create file2"; + } + ] + }); + verifyTscWatch({ + scenario, + subScenario: "should reflect change in config file", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => { + const configFile: File = { + path: configFilePath, + content: `{ "compilerOptions": {}, "files": ["${commonFile1.path}", "${commonFile2.path}"] }` - }; - return createWatchedSystem([libFile, commonFile1, commonFile2, configFile]); - }, - changes: [ - sys => { - sys.writeFile(configFilePath, `{ + }; + return createWatchedSystem([libFile, commonFile1, commonFile2, configFile]); + }, + changes: [ + sys => { + sys.writeFile(configFilePath, `{ "compilerOptions": {}, "files": ["${commonFile1.path}"] }`); - sys.checkTimeoutQueueLengthAndRun(1); - return "Change config"; - } - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "works correctly when config file is changed but its content havent", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => { - const configFile: File = { - path: configFilePath, - content: `{ + sys.checkTimeoutQueueLengthAndRun(1); + return "Change config"; + } + ] + }); + verifyTscWatch({ + scenario, + subScenario: "works correctly when config file is changed but its content havent", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => { + const configFile: File = { + path: configFilePath, + content: `{ "compilerOptions": {}, "files": ["${commonFile1.path}", "${commonFile2.path}"] }` - }; - return createWatchedSystem([libFile, commonFile1, commonFile2, configFile]); - }, - changes: [ - sys => { - sys.modifyFile(configFilePath, `{ + }; + return createWatchedSystem([libFile, commonFile1, commonFile2, configFile]); + }, + changes: [ + sys => { + sys.modifyFile(configFilePath, `{ "compilerOptions": {}, "files": ["${commonFile1.path}", "${commonFile2.path}"] }`); - sys.checkTimeoutQueueLengthAndRun(1); - return "Modify config without changing content"; - } - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "Updates diagnostics when '--noUnusedLabels' changes", - commandLineArgs: ["-w", "-p", "/tsconfig.json"], - sys: () => { - const aTs: File = { - path: "/a.ts", - content: "label: while (1) {}" - }; - const tsconfig: File = { - path: "/tsconfig.json", - content: JSON.stringify({ - compilerOptions: { allowUnusedLabels: true } - }) - }; - return createWatchedSystem([libFile, aTs, tsconfig]); + sys.checkTimeoutQueueLengthAndRun(1); + return "Modify config without changing content"; + } + ] + }); + verifyTscWatch({ + scenario, + subScenario: "Updates diagnostics when '--noUnusedLabels' changes", + commandLineArgs: ["-w", "-p", "/tsconfig.json"], + sys: () => { + const aTs: File = { + path: "/a.ts", + content: "label: while (1) {}" + }; + const tsconfig: File = { + path: "/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { allowUnusedLabels: true } + }) + }; + return createWatchedSystem([libFile, aTs, tsconfig]); + }, + changes: [ + sys => { + sys.modifyFile("/tsconfig.json", JSON.stringify({ + compilerOptions: { allowUnusedLabels: false } + })); + sys.checkTimeoutQueueLengthAndRun(1); + return "Disable allowUnsusedLabels"; }, - changes: [ - sys => { - sys.modifyFile("/tsconfig.json", JSON.stringify({ - compilerOptions: { allowUnusedLabels: false } - })); - sys.checkTimeoutQueueLengthAndRun(1); - return "Disable allowUnsusedLabels"; - }, - sys => { - sys.modifyFile("/tsconfig.json", JSON.stringify({ - compilerOptions: { allowUnusedLabels: true } - })); - sys.checkTimeoutQueueLengthAndRun(1); - return "Enable allowUnsusedLabels"; - } - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "updates diagnostics and emit for decorators", - commandLineArgs: ["-w"], - sys: () => { - const aTs: File = { - path: "/a.ts", - content: `import {B} from './b' + sys => { + sys.modifyFile("/tsconfig.json", JSON.stringify({ + compilerOptions: { allowUnusedLabels: true } + })); + sys.checkTimeoutQueueLengthAndRun(1); + return "Enable allowUnsusedLabels"; + } + ] + }); + verifyTscWatch({ + scenario, + subScenario: "updates diagnostics and emit for decorators", + commandLineArgs: ["-w"], + sys: () => { + const aTs: File = { + path: "/a.ts", + content: `import {B} from './b' @((_) => {}) export class A { constructor(p: B) {} }`, - }; - const bTs: File = { - path: "/b.ts", - content: `export class B {}`, - }; - const tsconfig: File = { - path: "/tsconfig.json", - content: JSON.stringify({ - compilerOptions: { target: "es6", importsNotUsedAsValues: "error" } - }) - }; - return createWatchedSystem([libFile, aTs, bTs, tsconfig]); + }; + const bTs: File = { + path: "/b.ts", + content: `export class B {}`, + }; + const tsconfig: File = { + path: "/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { target: "es6", importsNotUsedAsValues: "error" } + }) + }; + return createWatchedSystem([libFile, aTs, bTs, tsconfig]); + }, + changes: [ + sys => { + sys.modifyFile("/tsconfig.json", JSON.stringify({ + compilerOptions: { target: "es6", importsNotUsedAsValues: "error", experimentalDecorators: true } + })); + sys.checkTimeoutQueueLengthAndRun(1); + return "Enable experimentalDecorators"; }, - changes: [ - sys => { - sys.modifyFile("/tsconfig.json", JSON.stringify({ - compilerOptions: { target: "es6", importsNotUsedAsValues: "error", experimentalDecorators: true } - })); - sys.checkTimeoutQueueLengthAndRun(1); - return "Enable experimentalDecorators"; - }, - sys => { - sys.modifyFile("/tsconfig.json", JSON.stringify({ - compilerOptions: { target: "es6", importsNotUsedAsValues: "error", experimentalDecorators: true, emitDecoratorMetadata: true } - })); - sys.checkTimeoutQueueLengthAndRun(1); - return "Enable emitDecoratorMetadata"; - } - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "files explicitly excluded in config file", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => { - const configFile: File = { - path: configFilePath, - content: `{ + sys => { + sys.modifyFile("/tsconfig.json", JSON.stringify({ + compilerOptions: { target: "es6", importsNotUsedAsValues: "error", experimentalDecorators: true, emitDecoratorMetadata: true } + })); + sys.checkTimeoutQueueLengthAndRun(1); + return "Enable emitDecoratorMetadata"; + } + ] + }); + verifyTscWatch({ + scenario, + subScenario: "files explicitly excluded in config file", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => { + const configFile: File = { + path: configFilePath, + content: `{ "compilerOptions": {}, "exclude": ["/a/c"] }` - }; - const excludedFile1: File = { - path: "/a/c/excluedFile1.ts", - content: `let t = 1;` - }; - return createWatchedSystem([libFile, commonFile1, commonFile2, excludedFile1, configFile]); - }, - changes: emptyArray - }); - - verifyTscWatch({ - scenario, - subScenario: "should properly handle module resolution changes in config file", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => { - const file1: File = { - path: "/a/b/file1.ts", - content: `import { T } from "module1";` - }; - const nodeModuleFile: File = { - path: "/a/b/node_modules/module1.ts", - content: `export interface T {}` - }; - const classicModuleFile: File = { - path: "/a/module1.ts", - content: `export interface T {}` - }; - const configFile: File = { - path: configFilePath, - content: `{ + }; + const excludedFile1: File = { + path: "/a/c/excluedFile1.ts", + content: `let t = 1;` + }; + return createWatchedSystem([libFile, commonFile1, commonFile2, excludedFile1, configFile]); + }, + changes: emptyArray + }); + verifyTscWatch({ + scenario, + subScenario: "should properly handle module resolution changes in config file", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => { + const file1: File = { + path: "/a/b/file1.ts", + content: `import { T } from "module1";` + }; + const nodeModuleFile: File = { + path: "/a/b/node_modules/module1.ts", + content: `export interface T {}` + }; + const classicModuleFile: File = { + path: "/a/module1.ts", + content: `export interface T {}` + }; + const configFile: File = { + path: configFilePath, + content: `{ "compilerOptions": { "moduleResolution": "node" }, "files": ["${file1.path}"] }` - }; - return createWatchedSystem([libFile, file1, nodeModuleFile, classicModuleFile, configFile]); - }, - changes: [ - sys => { - sys.writeFile(configFile.path, `{ + }; + return createWatchedSystem([libFile, file1, nodeModuleFile, classicModuleFile, configFile]); + }, + changes: [ + sys => { + sys.writeFile(configFile.path, `{ "compilerOptions": { "moduleResolution": "classic" }, "files": ["/a/b/file1.ts"] }`); - sys.checkTimeoutQueueLengthAndRun(1); - return "Change module resolution to classic"; - } - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "should tolerate config file errors and still try to build a project", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => { - const configFile: File = { - path: configFilePath, - content: `{ + sys.checkTimeoutQueueLengthAndRun(1); + return "Change module resolution to classic"; + } + ] + }); + verifyTscWatch({ + scenario, + subScenario: "should tolerate config file errors and still try to build a project", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => { + const configFile: File = { + path: configFilePath, + content: `{ "compilerOptions": { "module": "none", "allowAnything": true }, "someOtherProperty": {} }` - }; - return createWatchedSystem([commonFile1, commonFile2, libFile, configFile]); - }, - changes: emptyArray - }); - - verifyTscWatch({ - scenario, - subScenario: "changes in files are reflected in project structure", - commandLineArgs: ["-w", "/a/b/f1.ts"], - sys: () => { - const file1 = { - path: "/a/b/f1.ts", - content: `export * from "./f2"` - }; - const file2 = { - path: "/a/b/f2.ts", - content: `export let x = 1` - }; - const file3 = { - path: "/a/c/f3.ts", - content: `export let y = 1;` - }; - return createWatchedSystem([file1, file2, file3, libFile]); - }, - changes: [ - sys => { - // now inferred project should inclule file3 - sys.modifyFile("/a/b/f2.ts", `export * from "../c/f3"`); - sys.checkTimeoutQueueLengthAndRun(1); - return "Modify f2 to include f3"; - } - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "deleted files affect project structure", - commandLineArgs: ["-w", "/a/b/f1.ts"], - sys: () => { - const file1 = { - path: "/a/b/f1.ts", - content: `export * from "./f2"` - }; - const file2 = { - path: "/a/b/f2.ts", - content: `export * from "../c/f3"` - }; - const file3 = { - path: "/a/c/f3.ts", - content: `export let y = 1;` - }; - return createWatchedSystem([file1, file2, file3, libFile]); - }, - changes: [ - sys => { - sys.deleteFile("/a/b/f2.ts"); - sys.checkTimeoutQueueLengthAndRun(1); - return "Delete f2"; - } - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "deleted files affect project structure-2", - commandLineArgs: ["-w", "/a/b/f1.ts", "/a/c/f3.ts"], - sys: () => { - const file1 = { - path: "/a/b/f1.ts", - content: `export * from "./f2"` - }; - const file2 = { - path: "/a/b/f2.ts", - content: `export * from "../c/f3"` - }; - const file3 = { - path: "/a/c/f3.ts", - content: `export let y = 1;` - }; - return createWatchedSystem([file1, file2, file3, libFile]); - }, - changes: [ - sys => { - sys.deleteFile("/a/b/f2.ts"); - sys.checkTimeoutQueueLengthAndRun(1); - return "Delete f2"; - } - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "config file includes the file", - commandLineArgs: ["-w", "-p", "/a/c/tsconfig.json"], - sys: () => { - const file1 = { - path: "/a/b/f1.ts", - content: "export let x = 5" - }; - const file2 = { - path: "/a/c/f2.ts", - content: `import {x} from "../b/f1"` - }; - const file3 = { - path: "/a/c/f3.ts", - content: "export let y = 1" - }; - const configFile = { - path: "/a/c/tsconfig.json", - content: JSON.stringify({ compilerOptions: {}, files: ["f2.ts", "f3.ts"] }) - }; - return createWatchedSystem([file1, file2, file3, libFile, configFile]); - }, - changes: emptyArray - }); - - it("correctly migrate files between projects", () => { + }; + return createWatchedSystem([commonFile1, commonFile2, libFile, configFile]); + }, + changes: emptyArray + }); + verifyTscWatch({ + scenario, + subScenario: "changes in files are reflected in project structure", + commandLineArgs: ["-w", "/a/b/f1.ts"], + sys: () => { const file1 = { path: "/a/b/f1.ts", - content: ` - export * from "../c/f2"; - export * from "../d/f3";` + content: `export * from "./f2"` + }; + const file2 = { + path: "/a/b/f2.ts", + content: `export let x = 1` + }; + const file3 = { + path: "/a/c/f3.ts", + content: `export let y = 1;` + }; + return createWatchedSystem([file1, file2, file3, libFile]); + }, + changes: [ + sys => { + // now inferred project should inclule file3 + sys.modifyFile("/a/b/f2.ts", `export * from "../c/f3"`); + sys.checkTimeoutQueueLengthAndRun(1); + return "Modify f2 to include f3"; + } + ] + }); + verifyTscWatch({ + scenario, + subScenario: "deleted files affect project structure", + commandLineArgs: ["-w", "/a/b/f1.ts"], + sys: () => { + const file1 = { + path: "/a/b/f1.ts", + content: `export * from "./f2"` + }; + const file2 = { + path: "/a/b/f2.ts", + content: `export * from "../c/f3"` + }; + const file3 = { + path: "/a/c/f3.ts", + content: `export let y = 1;` + }; + return createWatchedSystem([file1, file2, file3, libFile]); + }, + changes: [ + sys => { + sys.deleteFile("/a/b/f2.ts"); + sys.checkTimeoutQueueLengthAndRun(1); + return "Delete f2"; + } + ] + }); + verifyTscWatch({ + scenario, + subScenario: "deleted files affect project structure-2", + commandLineArgs: ["-w", "/a/b/f1.ts", "/a/c/f3.ts"], + sys: () => { + const file1 = { + path: "/a/b/f1.ts", + content: `export * from "./f2"` + }; + const file2 = { + path: "/a/b/f2.ts", + content: `export * from "../c/f3"` + }; + const file3 = { + path: "/a/c/f3.ts", + content: `export let y = 1;` + }; + return createWatchedSystem([file1, file2, file3, libFile]); + }, + changes: [ + sys => { + sys.deleteFile("/a/b/f2.ts"); + sys.checkTimeoutQueueLengthAndRun(1); + return "Delete f2"; + } + ] + }); + verifyTscWatch({ + scenario, + subScenario: "config file includes the file", + commandLineArgs: ["-w", "-p", "/a/c/tsconfig.json"], + sys: () => { + const file1 = { + path: "/a/b/f1.ts", + content: "export let x = 5" }; const file2 = { path: "/a/c/f2.ts", - content: "export let x = 1;" + content: `import {x} from "../b/f1"` }; const file3 = { - path: "/a/d/f3.ts", - content: "export let y = 1;" + path: "/a/c/f3.ts", + content: "export let y = 1" }; - const host = createWatchedSystem([file1, file2, file3]); - const watch = createWatchOfFilesAndCompilerOptions([file2.path, file3.path], host); - checkProgramActualFiles(watch(), [file2.path, file3.path]); - - const watch2 = createWatchOfFilesAndCompilerOptions([file1.path], host); - checkProgramActualFiles(watch2(), [file1.path, file2.path, file3.path]); - - // Previous program shouldnt be updated - checkProgramActualFiles(watch(), [file2.path, file3.path]); - host.checkTimeoutQueueLength(0); - }); - - verifyTscWatch({ - scenario, - subScenario: "can correctly update configured project when set of root files has changed (new file on disk)", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x = 1" - }; - return createWatchedSystem([file1, libFile, configFile]); - }, - changes: [ - sys => { - sys.writeFile("/a/b/f2.ts", "let y = 1"); - sys.checkTimeoutQueueLengthAndRun(1); - return "Write f2"; - } - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "can correctly update configured project when set of root files has changed (new file in list of files)", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x = 1" - }; - const file2 = { - path: "/a/b/f2.ts", - content: "let y = 1" - }; - const configFile = { - path: configFilePath, - content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts"] }) - }; - return createWatchedSystem([file1, file2, libFile, configFile]); - }, - changes: [ - sys => { - sys.writeFile(configFilePath, JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] })); - sys.checkTimeoutQueueLengthAndRun(1); - return "Modify config to make f2 as root too"; - } - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "can update configured project when set of root files was not changed", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x = 1" - }; - const file2 = { - path: "/a/b/f2.ts", - content: "let y = 1" - }; - const configFile = { - path: configFilePath, - content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] }) - }; - return createWatchedSystem([file1, file2, libFile, configFile]); - }, - changes: [ - sys => { - sys.writeFile(configFilePath, JSON.stringify({ compilerOptions: { outFile: "out.js" }, files: ["f1.ts", "f2.ts"] })); - sys.checkTimeoutQueueLengthAndRun(1); - return "Modify config to set outFile option"; - } - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "config file is deleted", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x = 1;" - }; - const file2 = { - path: "/a/b/f2.ts", - content: "let y = 2;" - }; - return createWatchedSystem([file1, file2, libFile, configFile]); - }, - changes: [ - sys => { - sys.deleteFile(configFilePath); - sys.checkTimeoutQueueLengthAndRun(1); - return "Delete config file"; - } - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "Proper errors document is not contained in project", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => { - const file1 = { - path: "/a/b/app.ts", - content: "" - }; - const corruptedConfig = { - path: configFilePath, - content: "{" - }; - return createWatchedSystem([file1, libFile, corruptedConfig]); - }, - changes: emptyArray - }); - - verifyTscWatch({ - scenario, - subScenario: "correctly handles changes in lib section of config file", - commandLineArgs: ["-w", "-p", "/src/tsconfig.json"], - sys: () => { - const libES5 = { - path: "/compiler/lib.es5.d.ts", - content: `${libFile.content} + const configFile = { + path: "/a/c/tsconfig.json", + content: JSON.stringify({ compilerOptions: {}, files: ["f2.ts", "f3.ts"] }) + }; + return createWatchedSystem([file1, file2, file3, libFile, configFile]); + }, + changes: emptyArray + }); + it("correctly migrate files between projects", () => { + const file1 = { + path: "/a/b/f1.ts", + content: ` + export * from "../c/f2"; + export * from "../d/f3";` + }; + const file2 = { + path: "/a/c/f2.ts", + content: "export let x = 1;" + }; + const file3 = { + path: "/a/d/f3.ts", + content: "export let y = 1;" + }; + const host = createWatchedSystem([file1, file2, file3]); + const watch = createWatchOfFilesAndCompilerOptions([file2.path, file3.path], host); + checkProgramActualFiles(watch(), [file2.path, file3.path]); + const watch2 = createWatchOfFilesAndCompilerOptions([file1.path], host); + checkProgramActualFiles(watch2(), [file1.path, file2.path, file3.path]); + // Previous program shouldnt be updated + checkProgramActualFiles(watch(), [file2.path, file3.path]); + host.checkTimeoutQueueLength(0); + }); + verifyTscWatch({ + scenario, + subScenario: "can correctly update configured project when set of root files has changed (new file on disk)", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + return createWatchedSystem([file1, libFile, configFile]); + }, + changes: [ + sys => { + sys.writeFile("/a/b/f2.ts", "let y = 1"); + sys.checkTimeoutQueueLengthAndRun(1); + return "Write f2"; + } + ] + }); + verifyTscWatch({ + scenario, + subScenario: "can correctly update configured project when set of root files has changed (new file in list of files)", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const configFile = { + path: configFilePath, + content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts"] }) + }; + return createWatchedSystem([file1, file2, libFile, configFile]); + }, + changes: [ + sys => { + sys.writeFile(configFilePath, JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] })); + sys.checkTimeoutQueueLengthAndRun(1); + return "Modify config to make f2 as root too"; + } + ] + }); + verifyTscWatch({ + scenario, + subScenario: "can update configured project when set of root files was not changed", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const configFile = { + path: configFilePath, + content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] }) + }; + return createWatchedSystem([file1, file2, libFile, configFile]); + }, + changes: [ + sys => { + sys.writeFile(configFilePath, JSON.stringify({ compilerOptions: { outFile: "out.js" }, files: ["f1.ts", "f2.ts"] })); + sys.checkTimeoutQueueLengthAndRun(1); + return "Modify config to set outFile option"; + } + ] + }); + verifyTscWatch({ + scenario, + subScenario: "config file is deleted", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1;" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 2;" + }; + return createWatchedSystem([file1, file2, libFile, configFile]); + }, + changes: [ + sys => { + sys.deleteFile(configFilePath); + sys.checkTimeoutQueueLengthAndRun(1); + return "Delete config file"; + } + ] + }); + verifyTscWatch({ + scenario, + subScenario: "Proper errors document is not contained in project", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => { + const file1 = { + path: "/a/b/app.ts", + content: "" + }; + const corruptedConfig = { + path: configFilePath, + content: "{" + }; + return createWatchedSystem([file1, libFile, corruptedConfig]); + }, + changes: emptyArray + }); + verifyTscWatch({ + scenario, + subScenario: "correctly handles changes in lib section of config file", + commandLineArgs: ["-w", "-p", "/src/tsconfig.json"], + sys: () => { + const libES5 = { + path: "/compiler/lib.es5.d.ts", + content: `${libFile.content} declare const eval: any` - }; - const libES2015Promise = { - path: "/compiler/lib.es2015.promise.d.ts", - content: `declare class Promise {}` - }; - const app = { - path: "/src/app.ts", - content: "var x: Promise;" - }; - const config1 = { - path: "/src/tsconfig.json", - content: JSON.stringify( - { - compilerOptions: { - module: "commonjs", - target: "es5", - noImplicitAny: true, - sourceMap: false, - lib: [ - "es5" - ] - } - }) - }; - return createWatchedSystem([libES5, libES2015Promise, app, config1], { executingFilePath: "/compiler/tsc.js" }); - }, - changes: [ - sys => { - sys.writeFile("/src/tsconfig.json", JSON.stringify( - { - compilerOptions: { - module: "commonjs", - target: "es5", - noImplicitAny: true, - sourceMap: false, - lib: [ - "es5", - "es2015.promise" - ] - } - }) - ); - sys.checkTimeoutQueueLengthAndRun(1); - return "Change the lib in config"; - } - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "should handle non-existing directories in config file", - commandLineArgs: ["-w", "-p", "/a/tsconfig.json"], - sys: () => { - const f = { - path: "/a/src/app.ts", - content: "let x = 1;" - }; - const config = { - path: "/a/tsconfig.json", - content: JSON.stringify({ - compilerOptions: {}, - include: [ - "src/**/*", - "notexistingfolder/*" + }; + const libES2015Promise = { + path: "/compiler/lib.es2015.promise.d.ts", + content: `declare class Promise {}` + }; + const app = { + path: "/src/app.ts", + content: "var x: Promise;" + }; + const config1 = { + path: "/src/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + module: "commonjs", + target: "es5", + noImplicitAny: true, + sourceMap: false, + lib: [ + "es5" ] - }) - }; - return createWatchedSystem([f, config, libFile]); - }, - changes: emptyArray - }); - - function changeModuleFileToModuleFile1(sys: WatchedSystem) { - sys.renameFile("/a/b/moduleFile.ts", "/a/b/moduleFile1.ts"); - sys.deleteFile("/a/b/moduleFile.js"); - sys.runQueuedTimeoutCallbacks(); - return "Rename moduleFile to moduleFile1"; - } - function changeModuleFile1ToModuleFile(sys: WatchedSystem) { - sys.renameFile("/a/b/moduleFile1.ts", "/a/b/moduleFile.ts"); - sys.runQueuedTimeoutCallbacks(); - return "Rename moduleFile1 back to moduleFile"; - } - - verifyTscWatch({ - scenario, - subScenario: "rename a module file and rename back should restore the states for inferred projects", - commandLineArgs: ["-w", "/a/b/file1.ts"], - sys: () => { - const moduleFile = { - path: "/a/b/moduleFile.ts", - content: "export function bar() { };" - }; - const file1 = { - path: "/a/b/file1.ts", - content: 'import * as T from "./moduleFile"; T.bar();' - }; - return createWatchedSystem([moduleFile, file1, libFile]); - }, - changes: [ - changeModuleFileToModuleFile1, - changeModuleFile1ToModuleFile - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "rename a module file and rename back should restore the states for configured projects", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => { - const moduleFile = { - path: "/a/b/moduleFile.ts", - content: "export function bar() { };" - }; - const file1 = { - path: "/a/b/file1.ts", - content: 'import * as T from "./moduleFile"; T.bar();' - }; - return createWatchedSystem([moduleFile, file1, configFile, libFile]); - }, - changes: [ - changeModuleFileToModuleFile1, - changeModuleFile1ToModuleFile - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "types should load from config file path if config exists", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => { - const f1 = { - path: "/a/b/app.ts", - content: "let x = 1" - }; - const config = { - path: configFilePath, - content: JSON.stringify({ compilerOptions: { types: ["node"], typeRoots: [] } }) - }; - const node = { - path: "/a/b/node_modules/@types/node/index.d.ts", - content: "declare var process: any" - }; - const cwd = { - path: "/a/c" - }; - return createWatchedSystem([f1, config, node, cwd, libFile], { currentDirectory: cwd.path }); - }, - changes: emptyArray - }); - - verifyTscWatch({ - scenario, - subScenario: "add the missing module file for inferred project-should remove the module not found error", - commandLineArgs: ["-w", "/a/b/file1.ts"], - sys: () => { - const file1 = { - path: "/a/b/file1.ts", - content: 'import * as T from "./moduleFile"; T.bar();' - }; - return createWatchedSystem([file1, libFile]); - }, - changes: [ - sys => { - sys.writeFile("/a/b/moduleFile.ts", "export function bar() { }"); - sys.runQueuedTimeoutCallbacks(); - return "Create module file"; - } - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "Configure file diagnostics events are generated when the config file has errors", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => { - const file = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const configFile = { - path: configFilePath, - content: `{ + } + }) + }; + return createWatchedSystem([libES5, libES2015Promise, app, config1], { executingFilePath: "/compiler/tsc.js" }); + }, + changes: [ + sys => { + sys.writeFile("/src/tsconfig.json", JSON.stringify({ + compilerOptions: { + module: "commonjs", + target: "es5", + noImplicitAny: true, + sourceMap: false, + lib: [ + "es5", + "es2015.promise" + ] + } + })); + sys.checkTimeoutQueueLengthAndRun(1); + return "Change the lib in config"; + } + ] + }); + verifyTscWatch({ + scenario, + subScenario: "should handle non-existing directories in config file", + commandLineArgs: ["-w", "-p", "/a/tsconfig.json"], + sys: () => { + const f = { + path: "/a/src/app.ts", + content: "let x = 1;" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: {}, + include: [ + "src/**/*", + "notexistingfolder/*" + ] + }) + }; + return createWatchedSystem([f, config, libFile]); + }, + changes: emptyArray + }); + function changeModuleFileToModuleFile1(sys: WatchedSystem) { + sys.renameFile("/a/b/moduleFile.ts", "/a/b/moduleFile1.ts"); + sys.deleteFile("/a/b/moduleFile.js"); + sys.runQueuedTimeoutCallbacks(); + return "Rename moduleFile to moduleFile1"; + } + function changeModuleFile1ToModuleFile(sys: WatchedSystem) { + sys.renameFile("/a/b/moduleFile1.ts", "/a/b/moduleFile.ts"); + sys.runQueuedTimeoutCallbacks(); + return "Rename moduleFile1 back to moduleFile"; + } + verifyTscWatch({ + scenario, + subScenario: "rename a module file and rename back should restore the states for inferred projects", + commandLineArgs: ["-w", "/a/b/file1.ts"], + sys: () => { + const moduleFile = { + path: "/a/b/moduleFile.ts", + content: "export function bar() { };" + }; + const file1 = { + path: "/a/b/file1.ts", + content: 'import * as T from "./moduleFile"; T.bar();' + }; + return createWatchedSystem([moduleFile, file1, libFile]); + }, + changes: [ + changeModuleFileToModuleFile1, + changeModuleFile1ToModuleFile + ] + }); + verifyTscWatch({ + scenario, + subScenario: "rename a module file and rename back should restore the states for configured projects", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => { + const moduleFile = { + path: "/a/b/moduleFile.ts", + content: "export function bar() { };" + }; + const file1 = { + path: "/a/b/file1.ts", + content: 'import * as T from "./moduleFile"; T.bar();' + }; + return createWatchedSystem([moduleFile, file1, configFile, libFile]); + }, + changes: [ + changeModuleFileToModuleFile1, + changeModuleFile1ToModuleFile + ] + }); + verifyTscWatch({ + scenario, + subScenario: "types should load from config file path if config exists", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => { + const f1 = { + path: "/a/b/app.ts", + content: "let x = 1" + }; + const config = { + path: configFilePath, + content: JSON.stringify({ compilerOptions: { types: ["node"], typeRoots: [] } }) + }; + const node = { + path: "/a/b/node_modules/@types/node/index.d.ts", + content: "declare var process: any" + }; + const cwd = { + path: "/a/c" + }; + return createWatchedSystem([f1, config, node, cwd, libFile], { currentDirectory: cwd.path }); + }, + changes: emptyArray + }); + verifyTscWatch({ + scenario, + subScenario: "add the missing module file for inferred project-should remove the module not found error", + commandLineArgs: ["-w", "/a/b/file1.ts"], + sys: () => { + const file1 = { + path: "/a/b/file1.ts", + content: 'import * as T from "./moduleFile"; T.bar();' + }; + return createWatchedSystem([file1, libFile]); + }, + changes: [ + sys => { + sys.writeFile("/a/b/moduleFile.ts", "export function bar() { }"); + sys.runQueuedTimeoutCallbacks(); + return "Create module file"; + } + ] + }); + verifyTscWatch({ + scenario, + subScenario: "Configure file diagnostics events are generated when the config file has errors", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => { + const file = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile = { + path: configFilePath, + content: `{ "compilerOptions": { "foo": "bar", "allowJS": true } }` - }; - return createWatchedSystem([file, configFile, libFile]); - }, - changes: emptyArray - }); - - verifyTscWatch({ - scenario, - subScenario: "if config file doesnt have errors, they are not reported", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => { - const file = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const configFile = { - path: configFilePath, - content: `{ + }; + return createWatchedSystem([file, configFile, libFile]); + }, + changes: emptyArray + }); + verifyTscWatch({ + scenario, + subScenario: "if config file doesnt have errors, they are not reported", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => { + const file = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile = { + path: configFilePath, + content: `{ "compilerOptions": {} }` - }; - return createWatchedSystem([file, configFile, libFile]); - }, - changes: emptyArray - }); - - verifyTscWatch({ - scenario, - subScenario: "Reports errors when the config file changes", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => { - const file = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - return createWatchedSystem([file, configFile, libFile]); - }, - changes: [ - sys => { - sys.writeFile(configFilePath, `{ + }; + return createWatchedSystem([file, configFile, libFile]); + }, + changes: emptyArray + }); + verifyTscWatch({ + scenario, + subScenario: "Reports errors when the config file changes", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => { + const file = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + return createWatchedSystem([file, configFile, libFile]); + }, + changes: [ + sys => { + sys.writeFile(configFilePath, `{ "compilerOptions": { "haha": 123 } }`); - sys.runQueuedTimeoutCallbacks(); - return "change config file to add error"; - }, - sys => { - sys.writeFile(configFilePath, `{ + sys.runQueuedTimeoutCallbacks(); + return "change config file to add error"; + }, + sys => { + sys.writeFile(configFilePath, `{ "compilerOptions": { } }`); - sys.runQueuedTimeoutCallbacks(); - return "change config file to remove error"; - } - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "non-existing directories listed in config file input array should be tolerated without crashing the server", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => { - const configFile = { - path: configFilePath, - content: `{ + sys.runQueuedTimeoutCallbacks(); + return "change config file to remove error"; + } + ] + }); + verifyTscWatch({ + scenario, + subScenario: "non-existing directories listed in config file input array should be tolerated without crashing the server", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => { + const configFile = { + path: configFilePath, + content: `{ "compilerOptions": {}, "include": ["app/*", "test/**/*", "something"] }` - }; - const file1 = { - path: "/a/b/file1.ts", - content: "let t = 10;" - }; - return createWatchedSystem([file1, configFile, libFile]); - }, - changes: emptyArray - }); - - verifyTscWatch({ - scenario, - subScenario: "non-existing directories listed in config file input array should be able to handle @types if input file list is empty", - commandLineArgs: ["-w", "-p", "/a/tsconfig.json"], - sys: () => { - const f = { - path: "/a/app.ts", - content: "let x = 1" - }; - const config = { - path: "/a/tsconfig.json", - content: JSON.stringify({ - compiler: {}, - files: [] - }) - }; - const t1 = { - path: "/a/node_modules/@types/typings/index.d.ts", - content: `export * from "./lib"` - }; - const t2 = { - path: "/a/node_modules/@types/typings/lib.d.ts", - content: `export const x: number` - }; - return createWatchedSystem([f, config, t1, t2, libFile], { currentDirectory: getDirectoryPath(f.path) }); - }, - changes: emptyArray - }); - - it("should support files without extensions", () => { + }; + const file1 = { + path: "/a/b/file1.ts", + content: "let t = 10;" + }; + return createWatchedSystem([file1, configFile, libFile]); + }, + changes: emptyArray + }); + verifyTscWatch({ + scenario, + subScenario: "non-existing directories listed in config file input array should be able to handle @types if input file list is empty", + commandLineArgs: ["-w", "-p", "/a/tsconfig.json"], + sys: () => { const f = { - path: "/a/compile", + path: "/a/app.ts", content: "let x = 1" }; - const host = createWatchedSystem([f, libFile]); - const watch = createWatchOfFilesAndCompilerOptions([f.path], host, { allowNonTsExtensions: true }); - checkProgramActualFiles(watch(), [f.path, libFile.path]); - }); - - verifyTscWatch({ - scenario, - subScenario: "Options Diagnostic locations reported correctly with changes in configFile contents when options change", - commandLineArgs: ["-w", "-p", configFilePath], - sys: () => { - const file = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const configFile = { - path: configFilePath, - content: ` + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compiler: {}, + files: [] + }) + }; + const t1 = { + path: "/a/node_modules/@types/typings/index.d.ts", + content: `export * from "./lib"` + }; + const t2 = { + path: "/a/node_modules/@types/typings/lib.d.ts", + content: `export const x: number` + }; + return createWatchedSystem([f, config, t1, t2, libFile], { currentDirectory: getDirectoryPath(f.path) }); + }, + changes: emptyArray + }); + it("should support files without extensions", () => { + const f = { + path: "/a/compile", + content: "let x = 1" + }; + const host = createWatchedSystem([f, libFile]); + const watch = createWatchOfFilesAndCompilerOptions([f.path], host, { allowNonTsExtensions: true }); + checkProgramActualFiles(watch(), [f.path, libFile.path]); + }); + verifyTscWatch({ + scenario, + subScenario: "Options Diagnostic locations reported correctly with changes in configFile contents when options change", + commandLineArgs: ["-w", "-p", configFilePath], + sys: () => { + const file = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile = { + path: configFilePath, + content: ` { // comment // More comment @@ -960,217 +918,190 @@ declare const eval: any` "mapRoot": "./" } }` - }; - return createWatchedSystem([file, libFile, configFile]); - }, - changes: [ - sys => { - sys.writeFile(configFilePath, ` + }; + return createWatchedSystem([file, libFile, configFile]); + }, + changes: [ + sys => { + sys.writeFile(configFilePath, ` { "compilerOptions": { "inlineSourceMap": true, "mapRoot": "./" } }`); - sys.runQueuedTimeoutCallbacks(); - return "Remove the comment from config file"; - } - ] - }); - - describe("should not trigger recompilation because of program emit", () => { - function verifyWithOptions(subScenario: string, options: CompilerOptions) { - verifyTscWatch({ - scenario, - subScenario: `should not trigger recompilation because of program emit/${subScenario}`, - commandLineArgs: ["-w", "-p", `${projectRoot}/tsconfig.json`], - maxNumberOfFilesToIterateForInvalidation: 1, - sys: () => { - const file1: File = { - path: `${projectRoot}/file1.ts`, - content: "export const c = 30;" - }; - const file2: File = { - path: `${projectRoot}/src/file2.ts`, - content: `import {c} from "file1"; export const d = 30;` - }; - const tsconfig: File = { - path: `${projectRoot}/tsconfig.json`, - content: generateTSConfig(options, emptyArray, "\n") - }; - return createWatchedSystem([file1, file2, libFile, tsconfig], { currentDirectory: projectRoot }); - }, - changes: emptyArray - }); + sys.runQueuedTimeoutCallbacks(); + return "Remove the comment from config file"; } - - verifyWithOptions( - "without outDir or outFile is specified", - { module: ModuleKind.AMD } - ); - - verifyWithOptions( - "with outFile", - { module: ModuleKind.AMD, outFile: "build/outFile.js" } - ); - - verifyWithOptions( - "when outDir is specified", - { module: ModuleKind.AMD, outDir: "build" } - ); - - verifyWithOptions( - "when outDir and declarationDir is specified", - { module: ModuleKind.AMD, outDir: "build", declaration: true, declarationDir: "decls" } - ); - - verifyWithOptions( - "declarationDir is specified", - { module: ModuleKind.AMD, declaration: true, declarationDir: "decls" } - ); - }); - - verifyTscWatch({ - scenario, - subScenario: "shouldnt report error about unused function incorrectly when file changes from global to module", - commandLineArgs: ["-w", "/a/b/file.ts", "--noUnusedLocals"], - sys: () => { - const file: File = { - path: "/a/b/file.ts", - content: `function one() {} + ] + }); + describe("should not trigger recompilation because of program emit", () => { + function verifyWithOptions(subScenario: string, options: CompilerOptions) { + verifyTscWatch({ + scenario, + subScenario: `should not trigger recompilation because of program emit/${subScenario}`, + commandLineArgs: ["-w", "-p", `${projectRoot}/tsconfig.json`], + maxNumberOfFilesToIterateForInvalidation: 1, + sys: () => { + const file1: File = { + path: `${projectRoot}/file1.ts`, + content: "export const c = 30;" + }; + const file2: File = { + path: `${projectRoot}/src/file2.ts`, + content: `import {c} from "file1"; export const d = 30;` + }; + const tsconfig: File = { + path: `${projectRoot}/tsconfig.json`, + content: generateTSConfig(options, emptyArray, "\n") + }; + return createWatchedSystem([file1, file2, libFile, tsconfig], { currentDirectory: projectRoot }); + }, + changes: emptyArray + }); + } + verifyWithOptions("without outDir or outFile is specified", { module: ModuleKind.AMD }); + verifyWithOptions("with outFile", { module: ModuleKind.AMD, outFile: "build/outFile.js" }); + verifyWithOptions("when outDir is specified", { module: ModuleKind.AMD, outDir: "build" }); + verifyWithOptions("when outDir and declarationDir is specified", { module: ModuleKind.AMD, outDir: "build", declaration: true, declarationDir: "decls" }); + verifyWithOptions("declarationDir is specified", { module: ModuleKind.AMD, declaration: true, declarationDir: "decls" }); + }); + verifyTscWatch({ + scenario, + subScenario: "shouldnt report error about unused function incorrectly when file changes from global to module", + commandLineArgs: ["-w", "/a/b/file.ts", "--noUnusedLocals"], + sys: () => { + const file: File = { + path: "/a/b/file.ts", + content: `function one() {} function two() { return function three() { one(); } }` - }; - return createWatchedSystem([file, libFile]); - }, - changes: [ - sys => { - sys.writeFile("/a/b/file.ts", `function one() {} + }; + return createWatchedSystem([file, libFile]); + }, + changes: [ + sys => { + sys.writeFile("/a/b/file.ts", `function one() {} export function two() { return function three() { one(); } }`); - sys.runQueuedTimeoutCallbacks(); - return "Change file to module"; - } - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "watched files when file is deleted and new file is added as part of change", - commandLineArgs: ["-w", "-p", "/home/username/project/tsconfig.json"], - sys: () => { - const projectLocation = "/home/username/project"; - const file: File = { - path: `${projectLocation}/src/file1.ts`, - content: "var a = 10;" - }; - const configFile: File = { - path: `${projectLocation}/tsconfig.json`, - content: "{}" - }; - return createWatchedSystem([file, libFile, configFile]); - }, - changes: [ - sys => { - sys.renameFile("/home/username/project/src/file1.ts", "/home/username/project/src/file2.ts"); - sys.runQueuedTimeoutCallbacks(); - return "Rename file1 to file2"; - } - ] - }); - - function changeParameterTypeOfBFile(sys: WatchedSystem, parameterName: string, toType: string) { - replaceFileText(sys, `${projectRoot}/b.ts`, new RegExp(`${parameterName}\: [a-z]*`), `${parameterName}: ${toType}`); - sys.runQueuedTimeoutCallbacks(); - return `Changed ${parameterName} type to ${toType}`; - } - - verifyTscWatch({ - scenario, - subScenario: "updates errors correctly when declaration emit is disabled in compiler options", - commandLineArgs: ["-w"], - sys: () => { - const aFile: File = { - path: `${projectRoot}/a.ts`, - content: `import test from './b'; + sys.runQueuedTimeoutCallbacks(); + return "Change file to module"; + } + ] + }); + verifyTscWatch({ + scenario, + subScenario: "watched files when file is deleted and new file is added as part of change", + commandLineArgs: ["-w", "-p", "/home/username/project/tsconfig.json"], + sys: () => { + const projectLocation = "/home/username/project"; + const file: File = { + path: `${projectLocation}/src/file1.ts`, + content: "var a = 10;" + }; + const configFile: File = { + path: `${projectLocation}/tsconfig.json`, + content: "{}" + }; + return createWatchedSystem([file, libFile, configFile]); + }, + changes: [ + sys => { + sys.renameFile("/home/username/project/src/file1.ts", "/home/username/project/src/file2.ts"); + sys.runQueuedTimeoutCallbacks(); + return "Rename file1 to file2"; + } + ] + }); + function changeParameterTypeOfBFile(sys: WatchedSystem, parameterName: string, toType: string) { + replaceFileText(sys, `${projectRoot}/b.ts`, new RegExp(`${parameterName}\: [a-z]*`), `${parameterName}: ${toType}`); + sys.runQueuedTimeoutCallbacks(); + return `Changed ${parameterName} type to ${toType}`; + } + verifyTscWatch({ + scenario, + subScenario: "updates errors correctly when declaration emit is disabled in compiler options", + commandLineArgs: ["-w"], + sys: () => { + const aFile: File = { + path: `${projectRoot}/a.ts`, + content: `import test from './b'; test(4, 5);` - }; - const bFile: File = { - path: `${projectRoot}/b.ts`, - content: `function test(x: number, y: number) { + }; + const bFile: File = { + path: `${projectRoot}/b.ts`, + content: `function test(x: number, y: number) { return x + y / 5; } export default test;` - }; - const tsconfigFile: File = { - path: `${projectRoot}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - module: "commonjs", - noEmit: true, - strict: true, - } - }) - }; - return createWatchedSystem([aFile, bFile, libFile, tsconfigFile], { currentDirectory: projectRoot }); - }, - changes: [ - sys => changeParameterTypeOfBFile(sys, "x", "string"), - sys => changeParameterTypeOfBFile(sys, "x", "number"), - sys => changeParameterTypeOfBFile(sys, "y", "string"), - sys => changeParameterTypeOfBFile(sys, "y", "number"), - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "updates errors when strictNullChecks changes", - commandLineArgs: ["-w"], - sys: () => { - const aFile: File = { - path: `${projectRoot}/a.ts`, - content: `declare function foo(): null | { hello: any }; + }; + const tsconfigFile: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + module: "commonjs", + noEmit: true, + strict: true, + } + }) + }; + return createWatchedSystem([aFile, bFile, libFile, tsconfigFile], { currentDirectory: projectRoot }); + }, + changes: [ + sys => changeParameterTypeOfBFile(sys, "x", "string"), + sys => changeParameterTypeOfBFile(sys, "x", "number"), + sys => changeParameterTypeOfBFile(sys, "y", "string"), + sys => changeParameterTypeOfBFile(sys, "y", "number"), + ] + }); + verifyTscWatch({ + scenario, + subScenario: "updates errors when strictNullChecks changes", + commandLineArgs: ["-w"], + sys: () => { + const aFile: File = { + path: `${projectRoot}/a.ts`, + content: `declare function foo(): null | { hello: any }; foo().hello` - }; - const config: File = { - path: `${projectRoot}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: {} }) - }; - return createWatchedSystem([aFile, config, libFile], { currentDirectory: projectRoot }); + }; + const config: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: {} }) + }; + return createWatchedSystem([aFile, config, libFile], { currentDirectory: projectRoot }); + }, + changes: [ + sys => { + sys.writeFile(`${projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: { strictNullChecks: true } })); + sys.runQueuedTimeoutCallbacks(); + return "Enable strict null checks"; }, - changes: [ - sys => { - sys.writeFile(`${projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: { strictNullChecks: true } })); - sys.runQueuedTimeoutCallbacks(); - return "Enable strict null checks"; - }, - sys => { - sys.writeFile(`${projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: { strict: true, alwaysStrict: false } })); // Avoid changing 'alwaysStrict' or must re-bind - sys.runQueuedTimeoutCallbacks(); - return "Set always strict false"; - }, - sys => { - sys.writeFile(`${projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: {} })); - sys.runQueuedTimeoutCallbacks(); - return "Disable strict"; - }, - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "updates errors when noErrorTruncation changes", - commandLineArgs: ["-w"], - sys: () => { - const aFile: File = { - path: `${projectRoot}/a.ts`, - content: `declare var v: { + sys => { + sys.writeFile(`${projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: { strict: true, alwaysStrict: false } })); // Avoid changing 'alwaysStrict' or must re-bind + sys.runQueuedTimeoutCallbacks(); + return "Set always strict false"; + }, + sys => { + sys.writeFile(`${projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: {} })); + sys.runQueuedTimeoutCallbacks(); + return "Disable strict"; + }, + ] + }); + verifyTscWatch({ + scenario, + subScenario: "updates errors when noErrorTruncation changes", + commandLineArgs: ["-w"], + sys: () => { + const aFile: File = { + path: `${projectRoot}/a.ts`, + content: `declare var v: { reallyLongPropertyName1: string | number | boolean | object | symbol | bigint; reallyLongPropertyName2: string | number | boolean | object | symbol | bigint; reallyLongPropertyName3: string | number | boolean | object | symbol | bigint; @@ -1180,354 +1111,337 @@ foo().hello` reallyLongPropertyName7: string | number | boolean | object | symbol | bigint; }; v === 'foo';` - }; - const config: File = { - path: `${projectRoot}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: {} }) - }; - return createWatchedSystem([aFile, config, libFile], { currentDirectory: projectRoot }); + }; + const config: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: {} }) + }; + return createWatchedSystem([aFile, config, libFile], { currentDirectory: projectRoot }); + }, + changes: [ + sys => { + sys.writeFile(`${projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: { noErrorTruncation: true } })); + sys.runQueuedTimeoutCallbacks(); + return "Enable noErrorTruncation"; }, - changes: [ - sys => { - sys.writeFile(`${projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: { noErrorTruncation: true } })); - sys.runQueuedTimeoutCallbacks(); - return "Enable noErrorTruncation"; - }, - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "updates diagnostics and emit when useDefineForClassFields changes", - commandLineArgs: ["-w"], - sys: () => { - const aFile: File = { - path: `/a.ts`, - content: `class C { get prop() { return 1; } } + ] + }); + verifyTscWatch({ + scenario, + subScenario: "updates diagnostics and emit when useDefineForClassFields changes", + commandLineArgs: ["-w"], + sys: () => { + const aFile: File = { + path: `/a.ts`, + content: `class C { get prop() { return 1; } } class D extends C { prop = 1; }` - }; - const config: File = { - path: `/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { target: "es6" } }) - }; - return createWatchedSystem([aFile, config, libFile]); + }; + const config: File = { + path: `/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { target: "es6" } }) + }; + return createWatchedSystem([aFile, config, libFile]); + }, + changes: [ + sys => { + sys.writeFile(`/tsconfig.json`, JSON.stringify({ compilerOptions: { target: "es6", useDefineForClassFields: true } })); + sys.runQueuedTimeoutCallbacks(); + return "Enable useDefineForClassFields"; }, - changes: [ - sys => { - sys.writeFile(`/tsconfig.json`, JSON.stringify({ compilerOptions: { target: "es6", useDefineForClassFields: true } })); - sys.runQueuedTimeoutCallbacks(); - return "Enable useDefineForClassFields"; - }, - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "updates errors and emit when importsNotUsedAsValues changes", - commandLineArgs: ["-w"], - sys: () => { - const aFile: File = { - path: `${projectRoot}/a.ts`, - content: `export class C {}` - }; - const bFile: File = { - path: `${projectRoot}/b.ts`, - content: `import {C} from './a'; + ] + }); + verifyTscWatch({ + scenario, + subScenario: "updates errors and emit when importsNotUsedAsValues changes", + commandLineArgs: ["-w"], + sys: () => { + const aFile: File = { + path: `${projectRoot}/a.ts`, + content: `export class C {}` + }; + const bFile: File = { + path: `${projectRoot}/b.ts`, + content: `import {C} from './a'; export function f(p: C) { return p; }` - }; - const config: File = { - path: `${projectRoot}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: {} }) - }; - return createWatchedSystem([aFile, bFile, config, libFile], { currentDirectory: projectRoot }); + }; + const config: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: {} }) + }; + return createWatchedSystem([aFile, bFile, config, libFile], { currentDirectory: projectRoot }); + }, + changes: [ + sys => { + sys.writeFile(`${projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: { importsNotUsedAsValues: "remove" } })); + sys.runQueuedTimeoutCallbacks(); + return 'Set to "remove"'; }, - changes: [ - sys => { - sys.writeFile(`${projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: { importsNotUsedAsValues: "remove" } })); - sys.runQueuedTimeoutCallbacks(); - return 'Set to "remove"'; - }, - sys => { - sys.writeFile(`${projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: { importsNotUsedAsValues: "error" } })); - sys.runQueuedTimeoutCallbacks(); - return 'Set to "error"'; - }, - sys => { - sys.writeFile(`${projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: { importsNotUsedAsValues: "preserve" } })); - sys.runQueuedTimeoutCallbacks(); - return 'Set to "preserve"'; - }, - ] - }); - - - verifyTscWatch({ - scenario, - subScenario: "updates errors when forceConsistentCasingInFileNames changes", - commandLineArgs: ["-w"], - sys: () => { - const aFile: File = { - path: `/a.ts`, - content: `export class C {}` - }; - const bFile: File = { - path: `/b.ts`, - content: `import {C} from './a'; import * as A from './A';` - }; - const config: File = { - path: `/tsconfig.json`, - content: JSON.stringify({ compilerOptions: {} }) - }; - return createWatchedSystem([aFile, bFile, config, libFile], { useCaseSensitiveFileNames: false }); + sys => { + sys.writeFile(`${projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: { importsNotUsedAsValues: "error" } })); + sys.runQueuedTimeoutCallbacks(); + return 'Set to "error"'; }, - changes: [ - sys => { - sys.writeFile(`/tsconfig.json`, JSON.stringify({ compilerOptions: { forceConsistentCasingInFileNames: true } })); - sys.runQueuedTimeoutCallbacks(); - return "Enable forceConsistentCasingInFileNames"; - }, - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "updates moduleResolution when resolveJsonModule changes", - commandLineArgs: ["-w"], - sys: () => { - const aFile: File = { - path: `${projectRoot}/a.ts`, - content: `import * as data from './data.json'` - }; - const jsonFile: File = { - path: `${projectRoot}/data.json`, - content: `{ "foo": 1 }` - }; - const config: File = { - path: `${projectRoot}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { moduleResolution: "node" } }) - }; - return createWatchedSystem([aFile, jsonFile, config, libFile], { currentDirectory: projectRoot }); + sys => { + sys.writeFile(`${projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: { importsNotUsedAsValues: "preserve" } })); + sys.runQueuedTimeoutCallbacks(); + return 'Set to "preserve"'; }, - changes: [ - sys => { - sys.writeFile(`${projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: { moduleResolution: "node", resolveJsonModule: true } })); - sys.runQueuedTimeoutCallbacks(); - return "Enable resolveJsonModule"; - }, - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "updates errors when ambient modules of program changes", - commandLineArgs: ["-w"], - sys: () => { - const aFile: File = { - path: `${projectRoot}/a.ts`, - content: `declare module 'a' { + ] + }); + verifyTscWatch({ + scenario, + subScenario: "updates errors when forceConsistentCasingInFileNames changes", + commandLineArgs: ["-w"], + sys: () => { + const aFile: File = { + path: `/a.ts`, + content: `export class C {}` + }; + const bFile: File = { + path: `/b.ts`, + content: `import {C} from './a'; import * as A from './A';` + }; + const config: File = { + path: `/tsconfig.json`, + content: JSON.stringify({ compilerOptions: {} }) + }; + return createWatchedSystem([aFile, bFile, config, libFile], { useCaseSensitiveFileNames: false }); + }, + changes: [ + sys => { + sys.writeFile(`/tsconfig.json`, JSON.stringify({ compilerOptions: { forceConsistentCasingInFileNames: true } })); + sys.runQueuedTimeoutCallbacks(); + return "Enable forceConsistentCasingInFileNames"; + }, + ] + }); + verifyTscWatch({ + scenario, + subScenario: "updates moduleResolution when resolveJsonModule changes", + commandLineArgs: ["-w"], + sys: () => { + const aFile: File = { + path: `${projectRoot}/a.ts`, + content: `import * as data from './data.json'` + }; + const jsonFile: File = { + path: `${projectRoot}/data.json`, + content: `{ "foo": 1 }` + }; + const config: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { moduleResolution: "node" } }) + }; + return createWatchedSystem([aFile, jsonFile, config, libFile], { currentDirectory: projectRoot }); + }, + changes: [ + sys => { + sys.writeFile(`${projectRoot}/tsconfig.json`, JSON.stringify({ compilerOptions: { moduleResolution: "node", resolveJsonModule: true } })); + sys.runQueuedTimeoutCallbacks(); + return "Enable resolveJsonModule"; + }, + ] + }); + verifyTscWatch({ + scenario, + subScenario: "updates errors when ambient modules of program changes", + commandLineArgs: ["-w"], + sys: () => { + const aFile: File = { + path: `${projectRoot}/a.ts`, + content: `declare module 'a' { type foo = number; }` - }; - const config: File = { - path: `${projectRoot}/tsconfig.json`, - content: "{}" - }; - return createWatchedSystem([aFile, config, libFile], { currentDirectory: projectRoot }); - }, - changes: [ - sys => { - // Create bts with same file contents - sys.writeFile(`${projectRoot}/b.ts`, `declare module 'a' { + }; + const config: File = { + path: `${projectRoot}/tsconfig.json`, + content: "{}" + }; + return createWatchedSystem([aFile, config, libFile], { currentDirectory: projectRoot }); + }, + changes: [ + sys => { + // Create bts with same file contents + sys.writeFile(`${projectRoot}/b.ts`, `declare module 'a' { type foo = number; }`); - sys.runQueuedTimeoutCallbacks(); - return "Create b.ts with same content"; - }, - sys => { - sys.deleteFile(`${projectRoot}/b.ts`); - sys.runQueuedTimeoutCallbacks(); - return "Delete b.ts"; - }, - ] - }); - - describe("updates errors in lib file", () => { - const field = "fullscreen"; - const fieldWithoutReadonly = `interface Document { + sys.runQueuedTimeoutCallbacks(); + return "Create b.ts with same content"; + }, + sys => { + sys.deleteFile(`${projectRoot}/b.ts`); + sys.runQueuedTimeoutCallbacks(); + return "Delete b.ts"; + }, + ] + }); + describe("updates errors in lib file", () => { + const field = "fullscreen"; + const fieldWithoutReadonly = `interface Document { ${field}: boolean; }`; - - const libFileWithDocument: File = { - path: libFile.path, - content: `${libFile.content} + const libFileWithDocument: File = { + path: libFile.path, + content: `${libFile.content} interface Document { readonly ${field}: boolean; }` - }; - - function verifyLibFileErrorsWith(subScenario: string, aFile: File) { - function verifyLibErrors(subScenario: string, commandLineOptions: readonly string[]) { - verifyTscWatch({ - scenario, - subScenario: `updates errors in lib file/${subScenario}`, - commandLineArgs: ["-w", aFile.path, ...commandLineOptions], - sys: () => createWatchedSystem([aFile, libFileWithDocument], { currentDirectory: projectRoot }), - changes: [ - sys => { - sys.writeFile(aFile.path, aFile.content.replace(fieldWithoutReadonly, "var x: string;")); - sys.runQueuedTimeoutCallbacks(); - return "Remove document declaration from file"; - }, - sys => { - sys.writeFile(aFile.path, aFile.content); - sys.runQueuedTimeoutCallbacks(); - return "Rever the file to contain document declaration"; - }, - ] - }); - } - - verifyLibErrors(`${subScenario}/with default options`, emptyArray); - verifyLibErrors(`${subScenario}/with skipLibCheck`, ["--skipLibCheck"]); - verifyLibErrors(`${subScenario}/with skipDefaultLibCheck`, ["--skipDefaultLibCheck"]); + }; + function verifyLibFileErrorsWith(subScenario: string, aFile: File) { + function verifyLibErrors(subScenario: string, commandLineOptions: readonly string[]) { + verifyTscWatch({ + scenario, + subScenario: `updates errors in lib file/${subScenario}`, + commandLineArgs: ["-w", aFile.path, ...commandLineOptions], + sys: () => createWatchedSystem([aFile, libFileWithDocument], { currentDirectory: projectRoot }), + changes: [ + sys => { + sys.writeFile(aFile.path, aFile.content.replace(fieldWithoutReadonly, "var x: string;")); + sys.runQueuedTimeoutCallbacks(); + return "Remove document declaration from file"; + }, + sys => { + sys.writeFile(aFile.path, aFile.content); + sys.runQueuedTimeoutCallbacks(); + return "Rever the file to contain document declaration"; + }, + ] + }); } - - describe("when non module file changes", () => { - const aFile: File = { - path: `${projectRoot}/a.ts`, - content: `${fieldWithoutReadonly} + verifyLibErrors(`${subScenario}/with default options`, emptyArray); + verifyLibErrors(`${subScenario}/with skipLibCheck`, ["--skipLibCheck"]); + verifyLibErrors(`${subScenario}/with skipDefaultLibCheck`, ["--skipDefaultLibCheck"]); + } + describe("when non module file changes", () => { + const aFile: File = { + path: `${projectRoot}/a.ts`, + content: `${fieldWithoutReadonly} var y: number;` - }; - verifyLibFileErrorsWith("when non module file changes", aFile); - }); - - describe("when module file with global definitions changes", () => { - const aFile: File = { - path: `${projectRoot}/a.ts`, - content: `export {} + }; + verifyLibFileErrorsWith("when non module file changes", aFile); + }); + describe("when module file with global definitions changes", () => { + const aFile: File = { + path: `${projectRoot}/a.ts`, + content: `export {} declare global { ${fieldWithoutReadonly} var y: number; }` - }; - verifyLibFileErrorsWith("when module file with global definitions changes", aFile); - }); + }; + verifyLibFileErrorsWith("when module file with global definitions changes", aFile); }); - - function changeWhenLibCheckChanges(sys: WatchedSystem, compilerOptions: CompilerOptions) { - const configFileContent = JSON.stringify({ compilerOptions }); - sys.writeFile(`${projectRoot}/tsconfig.json`, configFileContent); - sys.runQueuedTimeoutCallbacks(); - return `Changing config to ${configFileContent}`; - } - - verifyTscWatch({ - scenario, - subScenario: "when skipLibCheck and skipDefaultLibCheck changes", - commandLineArgs: ["-w"], - sys: () => { - const field = "fullscreen"; - const aFile: File = { - path: `${projectRoot}/a.ts`, - content: `interface Document { + }); + function changeWhenLibCheckChanges(sys: WatchedSystem, compilerOptions: CompilerOptions) { + const configFileContent = JSON.stringify({ compilerOptions }); + sys.writeFile(`${projectRoot}/tsconfig.json`, configFileContent); + sys.runQueuedTimeoutCallbacks(); + return `Changing config to ${configFileContent}`; + } + verifyTscWatch({ + scenario, + subScenario: "when skipLibCheck and skipDefaultLibCheck changes", + commandLineArgs: ["-w"], + sys: () => { + const field = "fullscreen"; + const aFile: File = { + path: `${projectRoot}/a.ts`, + content: `interface Document { ${field}: boolean; }` - }; - const bFile: File = { - path: `${projectRoot}/b.d.ts`, - content: `interface Document { + }; + const bFile: File = { + path: `${projectRoot}/b.d.ts`, + content: `interface Document { ${field}: boolean; }` - }; - const libFileWithDocument: File = { - path: libFile.path, - content: `${libFile.content} + }; + const libFileWithDocument: File = { + path: libFile.path, + content: `${libFile.content} interface Document { readonly ${field}: boolean; }` - }; - const configFile: File = { - path: `${projectRoot}/tsconfig.json`, - content: "{}" - }; - return createWatchedSystem([aFile, bFile, configFile, libFileWithDocument], { currentDirectory: projectRoot }); - }, - changes: [ - sys => changeWhenLibCheckChanges(sys, { skipLibCheck: true }), - sys => changeWhenLibCheckChanges(sys, { skipDefaultLibCheck: true }), - sys => changeWhenLibCheckChanges(sys, {}), - sys => changeWhenLibCheckChanges(sys, { skipDefaultLibCheck: true }), - sys => changeWhenLibCheckChanges(sys, { skipLibCheck: true }), - sys => changeWhenLibCheckChanges(sys, {}), - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "reports errors correctly with isolatedModules", - commandLineArgs: ["-w"], - sys: () => { - const aFile: File = { - path: `${projectRoot}/a.ts`, - content: `export const a: string = "";` - }; - const bFile: File = { - path: `${projectRoot}/b.ts`, - content: `import { a } from "./a"; + }; + const configFile: File = { + path: `${projectRoot}/tsconfig.json`, + content: "{}" + }; + return createWatchedSystem([aFile, bFile, configFile, libFileWithDocument], { currentDirectory: projectRoot }); + }, + changes: [ + sys => changeWhenLibCheckChanges(sys, { skipLibCheck: true }), + sys => changeWhenLibCheckChanges(sys, { skipDefaultLibCheck: true }), + sys => changeWhenLibCheckChanges(sys, {}), + sys => changeWhenLibCheckChanges(sys, { skipDefaultLibCheck: true }), + sys => changeWhenLibCheckChanges(sys, { skipLibCheck: true }), + sys => changeWhenLibCheckChanges(sys, {}), + ] + }); + verifyTscWatch({ + scenario, + subScenario: "reports errors correctly with isolatedModules", + commandLineArgs: ["-w"], + sys: () => { + const aFile: File = { + path: `${projectRoot}/a.ts`, + content: `export const a: string = "";` + }; + const bFile: File = { + path: `${projectRoot}/b.ts`, + content: `import { a } from "./a"; const b: string = a;` - }; - const configFile: File = { - path: `${projectRoot}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - isolatedModules: true - } - }) - }; - return createWatchedSystem([aFile, bFile, configFile, libFile], { currentDirectory: projectRoot }); - }, - changes: [ - sys => { - sys.writeFile(`${projectRoot}/a.ts`, `export const a: number = 1`); - sys.runQueuedTimeoutCallbacks(); - return "Change shape of a"; - }, - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "reports errors correctly with file not in rootDir", - commandLineArgs: ["-w"], - sys: () => { - const aFile: File = { - path: `${projectRoot}/a.ts`, - content: `import { x } from "../b";` - }; - const bFile: File = { - path: `/user/username/projects/b.ts`, - content: `export const x = 10;` - }; - const configFile: File = { - path: `${projectRoot}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - rootDir: ".", - outDir: "lib" - } - }) - }; - return createWatchedSystem([aFile, bFile, configFile, libFile], { currentDirectory: projectRoot }); + }; + const configFile: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + isolatedModules: true + } + }) + }; + return createWatchedSystem([aFile, bFile, configFile, libFile], { currentDirectory: projectRoot }); + }, + changes: [ + sys => { + sys.writeFile(`${projectRoot}/a.ts`, `export const a: number = 1`); + sys.runQueuedTimeoutCallbacks(); + return "Change shape of a"; }, - changes: [ - sys => { - sys.writeFile(`${projectRoot}/a.ts`, ` + ] + }); + verifyTscWatch({ + scenario, + subScenario: "reports errors correctly with file not in rootDir", + commandLineArgs: ["-w"], + sys: () => { + const aFile: File = { + path: `${projectRoot}/a.ts`, + content: `import { x } from "../b";` + }; + const bFile: File = { + path: `/user/username/projects/b.ts`, + content: `export const x = 10;` + }; + const configFile: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + rootDir: ".", + outDir: "lib" + } + }) + }; + return createWatchedSystem([aFile, bFile, configFile, libFile], { currentDirectory: projectRoot }); + }, + changes: [ + sys => { + sys.writeFile(`${projectRoot}/a.ts`, ` import { x } from "../b";`); - sys.runQueuedTimeoutCallbacks(); - return "Make changes to file a"; - }, - ] - }); + sys.runQueuedTimeoutCallbacks(); + return "Make changes to file a"; + }, + ] }); -} +}); diff --git a/src/testRunner/unittests/tscWatch/resolutionCache.ts b/src/testRunner/unittests/tscWatch/resolutionCache.ts index 2d974af594cf0..88f5f790e3046 100644 --- a/src/testRunner/unittests/tscWatch/resolutionCache.ts +++ b/src/testRunner/unittests/tscWatch/resolutionCache.ts @@ -1,438 +1,400 @@ -namespace ts.tscWatch { - describe("unittests:: tsc-watch:: resolutionCache:: tsc-watch module resolution caching", () => { - const scenario = "resolutionCache"; - it("works", () => { - const root = { - path: "/a/d/f0.ts", - content: `import {x} from "f1"` - }; - const imported = { - path: "/a/f1.ts", - content: `foo()` - }; - - const files = [root, imported, libFile]; - const host = createWatchedSystem(files); - const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); - - const f1IsNotModule = getDiagnosticOfFileFromProgram(watch(), root.path, root.content.indexOf('"f1"'), '"f1"'.length, Diagnostics.File_0_is_not_a_module, imported.path); - const cannotFindFoo = getDiagnosticOfFileFromProgram(watch(), imported.path, imported.content.indexOf("foo"), "foo".length, Diagnostics.Cannot_find_name_0, "foo"); - - // ensure that imported file was found - checkOutputErrorsInitial(host, [f1IsNotModule, cannotFindFoo]); - - const originalFileExists = host.fileExists; - { - const newContent = `import {x} from "f1" +import { libFile, createWatchedSystem, createWatchOfFilesAndCompilerOptions, getDiagnosticOfFileFromProgram, checkOutputErrorsInitial, checkOutputErrorsIncremental, getDiagnosticModuleNotFoundOfFile, verifyTscWatch, File, projectRoot, SymLink } from "../../ts.tscWatch"; +import { ModuleKind, Diagnostics, notImplemented, emptyArray } from "../../ts"; +describe("unittests:: tsc-watch:: resolutionCache:: tsc-watch module resolution caching", () => { + const scenario = "resolutionCache"; + it("works", () => { + const root = { + path: "/a/d/f0.ts", + content: `import {x} from "f1"` + }; + const imported = { + path: "/a/f1.ts", + content: `foo()` + }; + const files = [root, imported, libFile]; + const host = createWatchedSystem(files); + const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); + const f1IsNotModule = getDiagnosticOfFileFromProgram(watch(), root.path, root.content.indexOf('"f1"'), '"f1"'.length, Diagnostics.File_0_is_not_a_module, imported.path); + const cannotFindFoo = getDiagnosticOfFileFromProgram(watch(), imported.path, imported.content.indexOf("foo"), "foo".length, Diagnostics.Cannot_find_name_0, "foo"); + // ensure that imported file was found + checkOutputErrorsInitial(host, [f1IsNotModule, cannotFindFoo]); + const originalFileExists = host.fileExists; + { + const newContent = `import {x} from "f1" var x: string = 1;`; - root.content = newContent; - host.reloadFS(files); - - // patch fileExists to make sure that disk is not touched - host.fileExists = notImplemented; - - // trigger synchronization to make sure that import will be fetched from the cache - host.runQueuedTimeoutCallbacks(); - - // ensure file has correct number of errors after edit - checkOutputErrorsIncremental(host, [ - f1IsNotModule, - getDiagnosticOfFileFromProgram(watch(), root.path, newContent.indexOf("var x") + "var ".length, "x".length, Diagnostics.Type_0_is_not_assignable_to_type_1, 1, "string"), - cannotFindFoo - ]); - } - { - let fileExistsIsCalled = false; - host.fileExists = (fileName): boolean => { - if (fileName === "lib.d.ts") { - return false; - } - fileExistsIsCalled = true; - assert.isTrue(fileName.indexOf("/f2.") !== -1); - return originalFileExists.call(host, fileName); - }; - - root.content = `import {x} from "f2"`; - host.reloadFS(files); - - // trigger synchronization to make sure that system will try to find 'f2' module on disk - host.runQueuedTimeoutCallbacks(); - - // ensure file has correct number of errors after edit - checkOutputErrorsIncremental(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), root, "f2") - ]); - - assert.isTrue(fileExistsIsCalled); - } - { - let fileExistsCalled = false; - host.fileExists = (fileName): boolean => { - if (fileName === "lib.d.ts") { - return false; - } - fileExistsCalled = true; - assert.isTrue(fileName.indexOf("/f1.") !== -1); - return originalFileExists.call(host, fileName); - }; - - const newContent = `import {x} from "f1"`; - root.content = newContent; - - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - - checkOutputErrorsIncremental(host, [f1IsNotModule, cannotFindFoo]); - assert.isTrue(fileExistsCalled); - } - }); - - it("loads missing files from disk", () => { - const root = { - path: `/a/foo.ts`, - content: `import {x} from "bar"` - }; - - const imported = { - path: `/a/bar.d.ts`, - content: `export const y = 1;` - }; - - const files = [root, libFile]; - const host = createWatchedSystem(files); - const originalFileExists = host.fileExists; - - let fileExistsCalledForBar = false; - host.fileExists = fileName => { + root.content = newContent; + host.reloadFS(files); + // patch fileExists to make sure that disk is not touched + host.fileExists = notImplemented; + // trigger synchronization to make sure that import will be fetched from the cache + host.runQueuedTimeoutCallbacks(); + // ensure file has correct number of errors after edit + checkOutputErrorsIncremental(host, [ + f1IsNotModule, + getDiagnosticOfFileFromProgram(watch(), root.path, newContent.indexOf("var x") + "var ".length, "x".length, Diagnostics.Type_0_is_not_assignable_to_type_1, 1, "string"), + cannotFindFoo + ]); + } + { + let fileExistsIsCalled = false; + host.fileExists = (fileName): boolean => { if (fileName === "lib.d.ts") { return false; } - if (!fileExistsCalledForBar) { - fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; - } - + fileExistsIsCalled = true; + assert.isTrue(fileName.indexOf("/f2.") !== -1); return originalFileExists.call(host, fileName); }; - - const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); - - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); - checkOutputErrorsInitial(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), root, "bar") - ]); - - fileExistsCalledForBar = false; - root.content = `import {y} from "bar"`; - host.reloadFS(files.concat(imported)); - + root.content = `import {x} from "f2"`; + host.reloadFS(files); + // trigger synchronization to make sure that system will try to find 'f2' module on disk host.runQueuedTimeoutCallbacks(); - checkOutputErrorsIncremental(host, emptyArray); - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); - }); - - it("should compile correctly when resolved module goes missing and then comes back (module is not part of the root)", () => { - const root = { - path: `/a/foo.ts`, - content: `import {x} from "bar"` - }; - - const imported = { - path: `/a/bar.d.ts`, - content: `export const y = 1;export const x = 10;` - }; - - const files = [root, libFile]; - const filesWithImported = files.concat(imported); - const host = createWatchedSystem(filesWithImported); - const originalFileExists = host.fileExists; - let fileExistsCalledForBar = false; - host.fileExists = fileName => { + // ensure file has correct number of errors after edit + checkOutputErrorsIncremental(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), root, "f2") + ]); + assert.isTrue(fileExistsIsCalled); + } + { + let fileExistsCalled = false; + host.fileExists = (fileName): boolean => { if (fileName === "lib.d.ts") { return false; } - if (!fileExistsCalledForBar) { - fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; - } + fileExistsCalled = true; + assert.isTrue(fileName.indexOf("/f1.") !== -1); return originalFileExists.call(host, fileName); }; - - const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); - - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); - checkOutputErrorsInitial(host, emptyArray); - - fileExistsCalledForBar = false; + const newContent = `import {x} from "f1"`; + root.content = newContent; host.reloadFS(files); host.runQueuedTimeoutCallbacks(); - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); - checkOutputErrorsIncremental(host, [ - getDiagnosticModuleNotFoundOfFile(watch(), root, "bar") - ]); - - fileExistsCalledForBar = false; - host.reloadFS(filesWithImported); - host.checkTimeoutQueueLengthAndRun(1); - checkOutputErrorsIncremental(host, emptyArray); - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); - }); - - verifyTscWatch({ - scenario, - subScenario: "works when module resolution changes to ambient module", - commandLineArgs: ["-w", "/a/b/foo.ts"], - sys: () => createWatchedSystem([{ + checkOutputErrorsIncremental(host, [f1IsNotModule, cannotFindFoo]); + assert.isTrue(fileExistsCalled); + } + }); + it("loads missing files from disk", () => { + const root = { + path: `/a/foo.ts`, + content: `import {x} from "bar"` + }; + const imported = { + path: `/a/bar.d.ts`, + content: `export const y = 1;` + }; + const files = [root, libFile]; + const host = createWatchedSystem(files); + const originalFileExists = host.fileExists; + let fileExistsCalledForBar = false; + host.fileExists = fileName => { + if (fileName === "lib.d.ts") { + return false; + } + if (!fileExistsCalledForBar) { + fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; + } + return originalFileExists.call(host, fileName); + }; + const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); + checkOutputErrorsInitial(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), root, "bar") + ]); + fileExistsCalledForBar = false; + root.content = `import {y} from "bar"`; + host.reloadFS(files.concat(imported)); + host.runQueuedTimeoutCallbacks(); + checkOutputErrorsIncremental(host, emptyArray); + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); + }); + it("should compile correctly when resolved module goes missing and then comes back (module is not part of the root)", () => { + const root = { + path: `/a/foo.ts`, + content: `import {x} from "bar"` + }; + const imported = { + path: `/a/bar.d.ts`, + content: `export const y = 1;export const x = 10;` + }; + const files = [root, libFile]; + const filesWithImported = files.concat(imported); + const host = createWatchedSystem(filesWithImported); + const originalFileExists = host.fileExists; + let fileExistsCalledForBar = false; + host.fileExists = fileName => { + if (fileName === "lib.d.ts") { + return false; + } + if (!fileExistsCalledForBar) { + fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; + } + return originalFileExists.call(host, fileName); + }; + const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); + checkOutputErrorsInitial(host, emptyArray); + fileExistsCalledForBar = false; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); + checkOutputErrorsIncremental(host, [ + getDiagnosticModuleNotFoundOfFile(watch(), root, "bar") + ]); + fileExistsCalledForBar = false; + host.reloadFS(filesWithImported); + host.checkTimeoutQueueLengthAndRun(1); + checkOutputErrorsIncremental(host, emptyArray); + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); + }); + verifyTscWatch({ + scenario, + subScenario: "works when module resolution changes to ambient module", + commandLineArgs: ["-w", "/a/b/foo.ts"], + sys: () => createWatchedSystem([{ path: "/a/b/foo.ts", content: `import * as fs from "fs";` }, libFile], { currentDirectory: "/a/b" }), - changes: [ - sys => { - sys.ensureFileOrFolder({ - path: "/a/b/node_modules/@types/node/package.json", - content: ` + changes: [ + sys => { + sys.ensureFileOrFolder({ + path: "/a/b/node_modules/@types/node/package.json", + content: ` { "main": "" } ` - }); - sys.ensureFileOrFolder({ - path: "/a/b/node_modules/@types/node/index.d.ts", - content: ` + }); + sys.ensureFileOrFolder({ + path: "/a/b/node_modules/@types/node/index.d.ts", + content: ` declare module "fs" { export interface Stats { isFile(): boolean; } }` - }); - sys.runQueuedTimeoutCallbacks(); - return "npm install node types"; - } - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "works when included file with ambient module changes", - commandLineArgs: ["--w", "/a/b/foo.ts", "/a/b/bar.d.ts"], - sys: () => { - const root = { - path: "/a/b/foo.ts", - content: ` + }); + sys.runQueuedTimeoutCallbacks(); + return "npm install node types"; + } + ] + }); + verifyTscWatch({ + scenario, + subScenario: "works when included file with ambient module changes", + commandLineArgs: ["--w", "/a/b/foo.ts", "/a/b/bar.d.ts"], + sys: () => { + const root = { + path: "/a/b/foo.ts", + content: ` import * as fs from "fs"; import * as u from "url"; ` - }; - - const file = { - path: "/a/b/bar.d.ts", - content: ` + }; + const file = { + path: "/a/b/bar.d.ts", + content: ` declare module "url" { export interface Url { href?: string; } } ` - }; - return createWatchedSystem([root, file, libFile], { currentDirectory: "/a/b" }); - }, - changes: [ - sys => { - sys.appendFile("/a/b/bar.d.ts", ` + }; + return createWatchedSystem([root, file, libFile], { currentDirectory: "/a/b" }); + }, + changes: [ + sys => { + sys.appendFile("/a/b/bar.d.ts", ` declare module "fs" { export interface Stats { isFile(): boolean; } } `); - sys.runQueuedTimeoutCallbacks(); - return "Add fs definition"; - } - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "works when reusing program with files from external library", - commandLineArgs: ["--w", "-p", "/a/b/projects/myProject/src"], - sys: () => { - const configDir = "/a/b/projects/myProject/src/"; - const file1: File = { - path: configDir + "file1.ts", - content: 'import module1 = require("module1");\nmodule1("hello");' - }; - const file2: File = { - path: configDir + "file2.ts", - content: 'import module11 = require("module1");\nmodule11("hello");' - }; - const module1: File = { - path: "/a/b/projects/myProject/node_modules/module1/index.js", - content: "module.exports = options => { return options.toString(); }" - }; - const configFile: File = { - path: configDir + "tsconfig.json", - content: JSON.stringify({ - compilerOptions: { - allowJs: true, - rootDir: ".", - outDir: "../dist", - moduleResolution: "node", - maxNodeModuleJsDepth: 1 - } - }) - }; - return createWatchedSystem([file1, file2, module1, libFile, configFile], { currentDirectory: "/a/b/projects/myProject/" }); - }, - changes: [ - sys => { - sys.appendFile("/a/b/projects/myProject/src/file1.ts", "\n;"); - sys.runQueuedTimeoutCallbacks(); - return "Add new line to file1"; - } - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "works when renaming node_modules folder that already contains @types folder", - commandLineArgs: ["--w", `${projectRoot}/a.ts`], - sys: () => { - const file: File = { - path: `${projectRoot}/a.ts`, - content: `import * as q from "qqq";` - }; - const module: File = { - path: `${projectRoot}/node_modules2/@types/qqq/index.d.ts`, - content: "export {}" - }; - return createWatchedSystem([file, libFile, module], { currentDirectory: projectRoot }); - }, - changes: [ - sys => { - sys.renameFolder(`${projectRoot}/node_modules2`, `${projectRoot}/node_modules`); - sys.runQueuedTimeoutCallbacks(); - return "npm install"; - } - ] - }); - - describe("ignores files/folder changes in node_modules that start with '.'", () => { - function verifyIgnore(subScenario: string, commandLineArgs: readonly string[]) { - verifyTscWatch({ - scenario, - subScenario: `ignores changes in node_modules that start with dot/${subScenario}`, - commandLineArgs, - maxNumberOfFilesToIterateForInvalidation: 1, - sys: () => { - const file1: File = { - path: `${projectRoot}/test.ts`, - content: `import { x } from "somemodule";` - }; - const file2: File = { - path: `${projectRoot}/node_modules/somemodule/index.d.ts`, - content: `export const x = 10;` - }; - const config: File = { - path: `${projectRoot}/tsconfig.json`, - content: "{}" - }; - return createWatchedSystem([libFile, file1, file2, config]); - }, - changes: [ - sys => { - const npmCacheFile: File = { - path: `${projectRoot}/node_modules/.cache/babel-loader/89c02171edab901b9926470ba6d5677e.ts`, - content: JSON.stringify({ something: 10 }) - }; - sys.ensureFileOrFolder(npmCacheFile); - sys.checkTimeoutQueueLength(0); - return "npm install file and folder that start with '.'"; - } - ] - }); + sys.runQueuedTimeoutCallbacks(); + return "Add fs definition"; + } + ] + }); + verifyTscWatch({ + scenario, + subScenario: "works when reusing program with files from external library", + commandLineArgs: ["--w", "-p", "/a/b/projects/myProject/src"], + sys: () => { + const configDir = "/a/b/projects/myProject/src/"; + const file1: File = { + path: configDir + "file1.ts", + content: 'import module1 = require("module1");\nmodule1("hello");' + }; + const file2: File = { + path: configDir + "file2.ts", + content: 'import module11 = require("module1");\nmodule11("hello");' + }; + const module1: File = { + path: "/a/b/projects/myProject/node_modules/module1/index.js", + content: "module.exports = options => { return options.toString(); }" + }; + const configFile: File = { + path: configDir + "tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + allowJs: true, + rootDir: ".", + outDir: "../dist", + moduleResolution: "node", + maxNodeModuleJsDepth: 1 + } + }) + }; + return createWatchedSystem([file1, file2, module1, libFile, configFile], { currentDirectory: "/a/b/projects/myProject/" }); + }, + changes: [ + sys => { + sys.appendFile("/a/b/projects/myProject/src/file1.ts", "\n;"); + sys.runQueuedTimeoutCallbacks(); + return "Add new line to file1"; + } + ] + }); + verifyTscWatch({ + scenario, + subScenario: "works when renaming node_modules folder that already contains @types folder", + commandLineArgs: ["--w", `${projectRoot}/a.ts`], + sys: () => { + const file: File = { + path: `${projectRoot}/a.ts`, + content: `import * as q from "qqq";` + }; + const module: File = { + path: `${projectRoot}/node_modules2/@types/qqq/index.d.ts`, + content: "export {}" + }; + return createWatchedSystem([file, libFile, module], { currentDirectory: projectRoot }); + }, + changes: [ + sys => { + sys.renameFolder(`${projectRoot}/node_modules2`, `${projectRoot}/node_modules`); + sys.runQueuedTimeoutCallbacks(); + return "npm install"; } - verifyIgnore("watch without configFile", ["--w", `${projectRoot}/test.ts`]); - verifyIgnore("watch with configFile", ["--w", "-p", `${projectRoot}/tsconfig.json`]); - }); - - verifyTscWatch({ - scenario, - subScenario: "when types in compiler option are global and installed at later point", - commandLineArgs: ["--w", "-p", `${projectRoot}/tsconfig.json`], - sys: () => { - const app: File = { - path: `${projectRoot}/lib/app.ts`, - content: `myapp.component("hello");` - }; - const tsconfig: File = { - path: `${projectRoot}/tsconfig.json`, + ] + }); + describe("ignores files/folder changes in node_modules that start with '.'", () => { + function verifyIgnore(subScenario: string, commandLineArgs: readonly string[]) { + verifyTscWatch({ + scenario, + subScenario: `ignores changes in node_modules that start with dot/${subScenario}`, + commandLineArgs, + maxNumberOfFilesToIterateForInvalidation: 1, + sys: () => { + const file1: File = { + path: `${projectRoot}/test.ts`, + content: `import { x } from "somemodule";` + }; + const file2: File = { + path: `${projectRoot}/node_modules/somemodule/index.d.ts`, + content: `export const x = 10;` + }; + const config: File = { + path: `${projectRoot}/tsconfig.json`, + content: "{}" + }; + return createWatchedSystem([libFile, file1, file2, config]); + }, + changes: [ + sys => { + const npmCacheFile: File = { + path: `${projectRoot}/node_modules/.cache/babel-loader/89c02171edab901b9926470ba6d5677e.ts`, + content: JSON.stringify({ something: 10 }) + }; + sys.ensureFileOrFolder(npmCacheFile); + sys.checkTimeoutQueueLength(0); + return "npm install file and folder that start with '.'"; + } + ] + }); + } + verifyIgnore("watch without configFile", ["--w", `${projectRoot}/test.ts`]); + verifyIgnore("watch with configFile", ["--w", "-p", `${projectRoot}/tsconfig.json`]); + }); + verifyTscWatch({ + scenario, + subScenario: "when types in compiler option are global and installed at later point", + commandLineArgs: ["--w", "-p", `${projectRoot}/tsconfig.json`], + sys: () => { + const app: File = { + path: `${projectRoot}/lib/app.ts`, + content: `myapp.component("hello");` + }; + const tsconfig: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + module: "none", + types: ["@myapp/ts-types"] + } + }) + }; + return createWatchedSystem([app, tsconfig, libFile]); + }, + changes: [ + sys => { + sys.ensureFileOrFolder({ + path: `${projectRoot}/node_modules/@myapp/ts-types/package.json`, content: JSON.stringify({ - compilerOptions: { - module: "none", - types: ["@myapp/ts-types"] - } + version: "1.65.1", + types: "types/somefile.define.d.ts" }) - }; - return createWatchedSystem([app, tsconfig, libFile]); - }, - changes: [ - sys => { - sys.ensureFileOrFolder({ - path: `${projectRoot}/node_modules/@myapp/ts-types/package.json`, - content: JSON.stringify({ - version: "1.65.1", - types: "types/somefile.define.d.ts" - }) - }); - sys.ensureFileOrFolder({ - path: `${projectRoot}/node_modules/@myapp/ts-types/types/somefile.define.d.ts`, - content: ` + }); + sys.ensureFileOrFolder({ + path: `${projectRoot}/node_modules/@myapp/ts-types/types/somefile.define.d.ts`, + content: ` declare namespace myapp { function component(str: string): number; }` - }); - sys.checkTimeoutQueueLengthAndRun(1); - return "npm install ts-types"; - } - ] - }); - - verifyTscWatch({ - scenario, - subScenario: "with modules linked to sibling folder", - commandLineArgs: ["-w"], - sys: () => { - const mainPackageRoot = `${projectRoot}/main`; - const linkedPackageRoot = `${projectRoot}/linked-package`; - const mainFile: File = { - path: `${mainPackageRoot}/index.ts`, - content: "import { Foo } from '@scoped/linked-package'" - }; - const config: File = { - path: `${mainPackageRoot}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { module: "commonjs", moduleResolution: "node", baseUrl: ".", rootDir: "." }, - files: ["index.ts"] - }) - }; - const linkedPackageInMain: SymLink = { - path: `${mainPackageRoot}/node_modules/@scoped/linked-package`, - symLink: `${linkedPackageRoot}` - }; - const linkedPackageJson: File = { - path: `${linkedPackageRoot}/package.json`, - content: JSON.stringify({ name: "@scoped/linked-package", version: "0.0.1", types: "dist/index.d.ts", main: "dist/index.js" }) - }; - const linkedPackageIndex: File = { - path: `${linkedPackageRoot}/dist/index.d.ts`, - content: "export * from './other';" - }; - const linkedPackageOther: File = { - path: `${linkedPackageRoot}/dist/other.d.ts`, - content: 'export declare const Foo = "BAR";' - }; - const files = [libFile, mainFile, config, linkedPackageInMain, linkedPackageJson, linkedPackageIndex, linkedPackageOther]; - return createWatchedSystem(files, { currentDirectory: mainPackageRoot }); - }, - changes: emptyArray - }); + }); + sys.checkTimeoutQueueLengthAndRun(1); + return "npm install ts-types"; + } + ] }); -} + verifyTscWatch({ + scenario, + subScenario: "with modules linked to sibling folder", + commandLineArgs: ["-w"], + sys: () => { + const mainPackageRoot = `${projectRoot}/main`; + const linkedPackageRoot = `${projectRoot}/linked-package`; + const mainFile: File = { + path: `${mainPackageRoot}/index.ts`, + content: "import { Foo } from '@scoped/linked-package'" + }; + const config: File = { + path: `${mainPackageRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { module: "commonjs", moduleResolution: "node", baseUrl: ".", rootDir: "." }, + files: ["index.ts"] + }) + }; + const linkedPackageInMain: SymLink = { + path: `${mainPackageRoot}/node_modules/@scoped/linked-package`, + symLink: `${linkedPackageRoot}` + }; + const linkedPackageJson: File = { + path: `${linkedPackageRoot}/package.json`, + content: JSON.stringify({ name: "@scoped/linked-package", version: "0.0.1", types: "dist/index.d.ts", main: "dist/index.js" }) + }; + const linkedPackageIndex: File = { + path: `${linkedPackageRoot}/dist/index.d.ts`, + content: "export * from './other';" + }; + const linkedPackageOther: File = { + path: `${linkedPackageRoot}/dist/other.d.ts`, + content: 'export declare const Foo = "BAR";' + }; + const files = [libFile, mainFile, config, linkedPackageInMain, linkedPackageJson, linkedPackageIndex, linkedPackageOther]; + return createWatchedSystem(files, { currentDirectory: mainPackageRoot }); + }, + changes: emptyArray + }); +}); diff --git a/src/testRunner/unittests/tscWatch/watchApi.ts b/src/testRunner/unittests/tscWatch/watchApi.ts index 716fc162261f0..0bdca3fe3be9a 100644 --- a/src/testRunner/unittests/tscWatch/watchApi.ts +++ b/src/testRunner/unittests/tscWatch/watchApi.ts @@ -1,66 +1,63 @@ -namespace ts.tscWatch { - describe("unittests:: tsc-watch:: watchAPI:: tsc-watch with custom module resolution", () => { - const configFileJson: any = { - compilerOptions: { module: "commonjs", resolveJsonModule: true }, - files: ["index.ts"] - }; - const mainFile: File = { - path: `${projectRoot}/index.ts`, - content: "import settings from './settings.json';" - }; - const config: File = { - path: `${projectRoot}/tsconfig.json`, - content: JSON.stringify(configFileJson) - }; - const settingsJson: File = { - path: `${projectRoot}/settings.json`, - content: JSON.stringify({ content: "Print this" }) - }; - - it("verify that module resolution with json extension works when returned without extension", () => { - const files = [libFile, mainFile, config, settingsJson]; - const host = createWatchedSystem(files, { currentDirectory: projectRoot }); - const compilerHost = createWatchCompilerHostOfConfigFile(config.path, {}, /*watchOptionsToExtend*/ undefined, host); - const parsedCommandResult = parseJsonConfigFileContent(configFileJson, host, config.path); - compilerHost.resolveModuleNames = (moduleNames, containingFile) => moduleNames.map(m => { - const result = resolveModuleName(m, containingFile, parsedCommandResult.options, compilerHost); - const resolvedModule = result.resolvedModule!; - return { - resolvedFileName: resolvedModule.resolvedFileName, - isExternalLibraryImport: resolvedModule.isExternalLibraryImport, - originalFileName: resolvedModule.originalPath, - }; - }); - const watch = createWatchProgram(compilerHost); - const program = watch.getCurrentProgram().getProgram(); - checkProgramActualFiles(program, [mainFile.path, libFile.path, settingsJson.path]); +import { File, projectRoot, libFile, createWatchedSystem, checkProgramActualFiles } from "../../ts.tscWatch"; +import { createWatchCompilerHostOfConfigFile, parseJsonConfigFileContent, resolveModuleName, createWatchProgram, WatchStatusReporter } from "../../ts"; +describe("unittests:: tsc-watch:: watchAPI:: tsc-watch with custom module resolution", () => { + const configFileJson: any = { + compilerOptions: { module: "commonjs", resolveJsonModule: true }, + files: ["index.ts"] + }; + const mainFile: File = { + path: `${projectRoot}/index.ts`, + content: "import settings from './settings.json';" + }; + const config: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify(configFileJson) + }; + const settingsJson: File = { + path: `${projectRoot}/settings.json`, + content: JSON.stringify({ content: "Print this" }) + }; + it("verify that module resolution with json extension works when returned without extension", () => { + const files = [libFile, mainFile, config, settingsJson]; + const host = createWatchedSystem(files, { currentDirectory: projectRoot }); + const compilerHost = createWatchCompilerHostOfConfigFile(config.path, {}, /*watchOptionsToExtend*/ undefined, host); + const parsedCommandResult = parseJsonConfigFileContent(configFileJson, host, config.path); + compilerHost.resolveModuleNames = (moduleNames, containingFile) => moduleNames.map(m => { + const result = resolveModuleName(m, containingFile, parsedCommandResult.options, compilerHost); + const resolvedModule = result.resolvedModule!; + return { + resolvedFileName: resolvedModule.resolvedFileName, + isExternalLibraryImport: resolvedModule.isExternalLibraryImport, + originalFileName: resolvedModule.originalPath, + }; }); + const watch = createWatchProgram(compilerHost); + const program = watch.getCurrentProgram().getProgram(); + checkProgramActualFiles(program, [mainFile.path, libFile.path, settingsJson.path]); }); - - describe("unittests:: tsc-watch:: watchAPI:: tsc-watch expose error count to watch status reporter", () => { - const configFileJson: any = { - compilerOptions: { module: "commonjs" }, - files: ["index.ts"] +}); +describe("unittests:: tsc-watch:: watchAPI:: tsc-watch expose error count to watch status reporter", () => { + const configFileJson: any = { + compilerOptions: { module: "commonjs" }, + files: ["index.ts"] + }; + const config: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify(configFileJson) + }; + const mainFile: File = { + path: `${projectRoot}/index.ts`, + content: "let compiler = new Compiler(); for (let i = 0; j < 5; i++) {}" + }; + it("verify that the error count is correctly passed down to the watch status reporter", () => { + const files = [libFile, mainFile, config]; + const host = createWatchedSystem(files, { currentDirectory: projectRoot }); + let watchedErrorCount; + const reportWatchStatus: WatchStatusReporter = (_, __, ___, errorCount) => { + watchedErrorCount = errorCount; }; - const config: File = { - path: `${projectRoot}/tsconfig.json`, - content: JSON.stringify(configFileJson) - }; - const mainFile: File = { - path: `${projectRoot}/index.ts`, - content: "let compiler = new Compiler(); for (let i = 0; j < 5; i++) {}" - }; - - it("verify that the error count is correctly passed down to the watch status reporter", () => { - const files = [libFile, mainFile, config]; - const host = createWatchedSystem(files, { currentDirectory: projectRoot }); - let watchedErrorCount; - const reportWatchStatus: WatchStatusReporter = (_, __, ___, errorCount) => { - watchedErrorCount = errorCount; - }; - const compilerHost = createWatchCompilerHostOfConfigFile(config.path, {}, /*watchOptionsToExtend*/ undefined, host, /*createProgram*/ undefined, /*reportDiagnostic*/ undefined, reportWatchStatus); - createWatchProgram(compilerHost); - assert.equal(watchedErrorCount, 2, "The error count was expected to be 2 for the file change"); - }); + const compilerHost = createWatchCompilerHostOfConfigFile(config.path, {}, /*watchOptionsToExtend*/ undefined, host, /*createProgram*/ undefined, /*reportDiagnostic*/ undefined, reportWatchStatus); + createWatchProgram(compilerHost); + assert.equal(watchedErrorCount, 2, "The error count was expected to be 2 for the file change"); }); -} +}); diff --git a/src/testRunner/unittests/tscWatch/watchEnvironment.ts b/src/testRunner/unittests/tscWatch/watchEnvironment.ts index 3df1e2f4aea94..22cda04809477 100644 --- a/src/testRunner/unittests/tscWatch/watchEnvironment.ts +++ b/src/testRunner/unittests/tscWatch/watchEnvironment.ts @@ -1,299 +1,288 @@ -namespace ts.tscWatch { - import Tsc_WatchDirectory = TestFSWithWatch.Tsc_WatchDirectory; - describe("unittests:: tsc-watch:: watchEnvironment:: tsc-watch with different polling/non polling options", () => { - const scenario = "watchEnvironment"; +import { verifyTscWatch, File, createWatchedSystem, libFile, SymLink, projectRoot, commonFile1, commonFile2 } from "../../ts.tscWatch"; +import { createMap, TestFSWithWatch, unchangedPollThresholds, PollingInterval, emptyArray } from "../../ts"; +import * as ts from "../../ts"; +import Tsc_WatchDirectory = ts.TestFSWithWatch.Tsc_WatchDirectory; +describe("unittests:: tsc-watch:: watchEnvironment:: tsc-watch with different polling/non polling options", () => { + const scenario = "watchEnvironment"; + verifyTscWatch({ + scenario, + subScenario: "watchFile/using dynamic priority polling", + commandLineArgs: ["--w", `/a/username/project/typescript.ts`], + sys: () => { + const projectFolder = "/a/username/project"; + const file1: File = { + path: `${projectFolder}/typescript.ts`, + content: "var z = 10;" + }; + const environmentVariables = createMap(); + environmentVariables.set("TSC_WATCHFILE", TestFSWithWatch.Tsc_WatchFile.DynamicPolling); + return createWatchedSystem([file1, libFile], { environmentVariables }); + }, + changes: [ + (sys, programs) => { + const initialProgram = programs[0][0]; + const mediumPollingIntervalThreshold = unchangedPollThresholds[PollingInterval.Medium]; + for (let index = 0; index < mediumPollingIntervalThreshold; index++) { + // Transition libFile and file1 to low priority queue + sys.checkTimeoutQueueLengthAndRun(1); + assert.deepEqual(programs[0][0], initialProgram); + } + return "Time spent to Transition libFile and file1 to low priority queue"; + }, + sys => { + // Make a change to file + sys.writeFile("/a/username/project/typescript.ts", "var zz30 = 100;"); + // During this timeout the file would be detected as unchanged + sys.checkTimeoutQueueLengthAndRun(1); + return "Make change to file"; + }, + sys => { + // Callbacks: medium priority + high priority queue and scheduled program update + sys.checkTimeoutQueueLengthAndRun(3); + // This should detect change in the file + return "Callbacks: medium priority + high priority queue and scheduled program update"; + }, + (sys, programs) => { + const initialProgram = programs[0][0]; + const mediumPollingIntervalThreshold = unchangedPollThresholds[PollingInterval.Medium]; + const newThreshold = unchangedPollThresholds[PollingInterval.Low] + mediumPollingIntervalThreshold; + for (let fileUnchangeDetected = 1; fileUnchangeDetected < newThreshold; fileUnchangeDetected++) { + // For high + Medium/low polling interval + sys.checkTimeoutQueueLengthAndRun(2); + assert.deepEqual(programs[0][0], initialProgram); + } + // Everything goes in high polling interval queue + sys.checkTimeoutQueueLengthAndRun(1); + return "Polling queues polled and everything is in the high polling queue"; + } + ] + }); + describe("tsc-watch when watchDirectories implementation", () => { + function verifyRenamingFileInSubFolder(subScenario: string, tscWatchDirectory: Tsc_WatchDirectory) { + const projectFolder = "/a/username/project"; + const projectSrcFolder = `${projectFolder}/src`; + const configFile: File = { + path: `${projectFolder}/tsconfig.json`, + content: JSON.stringify({ + watchOptions: { + synchronousWatchDirectory: true + } + }) + }; + const file: File = { + path: `${projectSrcFolder}/file1.ts`, + content: "" + }; + verifyTscWatch({ + scenario, + subScenario: `watchDirectories/${subScenario}`, + commandLineArgs: ["--w", "-p", configFile.path], + sys: () => { + const files = [file, configFile, libFile]; + const environmentVariables = createMap(); + environmentVariables.set("TSC_WATCHDIRECTORY", tscWatchDirectory); + return createWatchedSystem(files, { environmentVariables }); + }, + changes: [ + sys => { + // Rename the file: + sys.renameFile(file.path, file.path.replace("file1.ts", "file2.ts")); + if (tscWatchDirectory === Tsc_WatchDirectory.DynamicPolling) { + // With dynamic polling the fs change would be detected only by running timeouts + sys.runQueuedTimeoutCallbacks(); + } + // Delayed update program + sys.runQueuedTimeoutCallbacks(); + return "Rename file1 to file2"; + }, + ], + }); + } + verifyRenamingFileInSubFolder("uses watchFile when renaming file in subfolder", Tsc_WatchDirectory.WatchFile); + verifyRenamingFileInSubFolder("uses non recursive watchDirectory when renaming file in subfolder", Tsc_WatchDirectory.NonRecursiveWatchDirectory); + verifyRenamingFileInSubFolder("uses non recursive dynamic polling when renaming file in subfolder", Tsc_WatchDirectory.DynamicPolling); verifyTscWatch({ scenario, - subScenario: "watchFile/using dynamic priority polling", - commandLineArgs: ["--w", `/a/username/project/typescript.ts`], + subScenario: "watchDirectories/when there are symlinks to folders in recursive folders", + commandLineArgs: ["--w"], sys: () => { - const projectFolder = "/a/username/project"; + const cwd = "/home/user/projects/myproject"; const file1: File = { - path: `${projectFolder}/typescript.ts`, - content: "var z = 10;" + path: `${cwd}/src/file.ts`, + content: `import * as a from "a"` + }; + const tsconfig: File = { + path: `${cwd}/tsconfig.json`, + content: `{ "compilerOptions": { "extendedDiagnostics": true, "traceResolution": true }}` + }; + const realA: File = { + path: `${cwd}/node_modules/reala/index.d.ts`, + content: `export {}` + }; + const realB: File = { + path: `${cwd}/node_modules/realb/index.d.ts`, + content: `export {}` + }; + const symLinkA: SymLink = { + path: `${cwd}/node_modules/a`, + symLink: `${cwd}/node_modules/reala` + }; + const symLinkB: SymLink = { + path: `${cwd}/node_modules/b`, + symLink: `${cwd}/node_modules/realb` }; + const symLinkBInA: SymLink = { + path: `${cwd}/node_modules/reala/node_modules/b`, + symLink: `${cwd}/node_modules/b` + }; + const symLinkAInB: SymLink = { + path: `${cwd}/node_modules/realb/node_modules/a`, + symLink: `${cwd}/node_modules/a` + }; + const files = [libFile, file1, tsconfig, realA, realB, symLinkA, symLinkB, symLinkBInA, symLinkAInB]; const environmentVariables = createMap(); - environmentVariables.set("TSC_WATCHFILE", TestFSWithWatch.Tsc_WatchFile.DynamicPolling); - return createWatchedSystem([file1, libFile], { environmentVariables }); + environmentVariables.set("TSC_WATCHDIRECTORY", Tsc_WatchDirectory.NonRecursiveWatchDirectory); + return createWatchedSystem(files, { environmentVariables, currentDirectory: cwd }); + }, + changes: emptyArray + }); + verifyTscWatch({ + scenario, + subScenario: "watchDirectories/with non synchronous watch directory", + commandLineArgs: ["--w", "-p", `${projectRoot}/tsconfig.json`], + sys: () => { + const configFile: File = { + path: `${projectRoot}/tsconfig.json`, + content: "{}" + }; + const file1: File = { + path: `${projectRoot}/src/file1.ts`, + content: `import { x } from "file2";` + }; + const file2: File = { + path: `${projectRoot}/node_modules/file2/index.d.ts`, + content: `export const x = 10;` + }; + const files = [libFile, file1, file2, configFile]; + return createWatchedSystem(files, { runWithoutRecursiveWatches: true }); }, changes: [ - (sys, programs) => { - const initialProgram = programs[0][0]; - const mediumPollingIntervalThreshold = unchangedPollThresholds[PollingInterval.Medium]; - for (let index = 0; index < mediumPollingIntervalThreshold; index++) { - // Transition libFile and file1 to low priority queue - sys.checkTimeoutQueueLengthAndRun(1); - assert.deepEqual(programs[0][0], initialProgram); - } - return "Time spent to Transition libFile and file1 to low priority queue"; + sys => { + sys.checkTimeoutQueueLengthAndRun(1); // To update directory callbacks for file1.js output + sys.checkTimeoutQueueLengthAndRun(1); // Update program again + sys.checkTimeoutQueueLength(0); + return "Pending updates because of file1.js creation"; }, sys => { - // Make a change to file - sys.writeFile("/a/username/project/typescript.ts", "var zz30 = 100;"); - - // During this timeout the file would be detected as unchanged - sys.checkTimeoutQueueLengthAndRun(1); - return "Make change to file"; + // Remove directory node_modules + sys.deleteFolder(`${projectRoot}/node_modules`, /*recursive*/ true); + sys.checkTimeoutQueueLength(2); // 1. For updating program and 2. for updating child watches + sys.runQueuedTimeoutCallbacks(sys.getNextTimeoutId() - 2); // Update program + return "Remove directory node_modules"; }, sys => { - // Callbacks: medium priority + high priority queue and scheduled program update - sys.checkTimeoutQueueLengthAndRun(3); - // This should detect change in the file - return "Callbacks: medium priority + high priority queue and scheduled program update"; + sys.checkTimeoutQueueLengthAndRun(1); // To update directory watchers + sys.checkTimeoutQueueLengthAndRun(1); // To Update program + sys.checkTimeoutQueueLength(0); + return "Pending directory watchers and program update"; }, - (sys, programs) => { - const initialProgram = programs[0][0]; - const mediumPollingIntervalThreshold = unchangedPollThresholds[PollingInterval.Medium]; - const newThreshold = unchangedPollThresholds[PollingInterval.Low] + mediumPollingIntervalThreshold; - for (let fileUnchangeDetected = 1; fileUnchangeDetected < newThreshold; fileUnchangeDetected++) { - // For high + Medium/low polling interval - sys.checkTimeoutQueueLengthAndRun(2); - assert.deepEqual(programs[0][0], initialProgram); - } - - // Everything goes in high polling interval queue - sys.checkTimeoutQueueLengthAndRun(1); - return "Polling queues polled and everything is in the high polling queue"; + sys => { + // npm install + sys.createDirectory(`${projectRoot}/node_modules`); + sys.checkTimeoutQueueLength(1); // To update folder structure + return "Start npm install"; + }, + sys => { + sys.createDirectory(`${projectRoot}/node_modules/file2`); + sys.checkTimeoutQueueLength(1); // To update folder structure + return "npm install folder creation of file2"; + }, + sys => { + sys.writeFile(`${projectRoot}/node_modules/file2/index.d.ts`, `export const x = 10;`); + sys.checkTimeoutQueueLength(1); // To update folder structure + return "npm install index file in file2"; + }, + sys => { + sys.runQueuedTimeoutCallbacks(); + sys.checkTimeoutQueueLength(1); // To Update the program + return "Updates the program"; + }, + sys => { + sys.runQueuedTimeoutCallbacks(); + sys.checkTimeoutQueueLength(0); + return "Pending updates"; } - ] + ], }); - - describe("tsc-watch when watchDirectories implementation", () => { - function verifyRenamingFileInSubFolder(subScenario: string, tscWatchDirectory: Tsc_WatchDirectory) { - const projectFolder = "/a/username/project"; - const projectSrcFolder = `${projectFolder}/src`; + }); + describe("handles watch compiler options", () => { + verifyTscWatch({ + scenario, + subScenario: "watchOptions/with watchFile option", + commandLineArgs: ["-w", "-p", "/a/b/tsconfig.json"], + sys: () => { const configFile: File = { - path: `${projectFolder}/tsconfig.json`, + path: "/a/b/tsconfig.json", content: JSON.stringify({ watchOptions: { - synchronousWatchDirectory: true + watchFile: "UseFsEvents" } }) }; - const file: File = { - path: `${projectSrcFolder}/file1.ts`, - content: "" + const files = [libFile, commonFile1, commonFile2, configFile]; + return createWatchedSystem(files); + }, + changes: emptyArray + }); + verifyTscWatch({ + scenario, + subScenario: "watchOptions/with watchDirectory option", + commandLineArgs: ["-w", "-p", "/a/b/tsconfig.json"], + sys: () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ + watchOptions: { + watchDirectory: "UseFsEvents" + } + }) }; - verifyTscWatch({ - scenario, - subScenario: `watchDirectories/${subScenario}`, - commandLineArgs: ["--w", "-p", configFile.path], - sys: () => { - const files = [file, configFile, libFile]; - const environmentVariables = createMap(); - environmentVariables.set("TSC_WATCHDIRECTORY", tscWatchDirectory); - return createWatchedSystem(files, { environmentVariables }); - }, - changes: [ - sys => { - // Rename the file: - sys.renameFile(file.path, file.path.replace("file1.ts", "file2.ts")); - if (tscWatchDirectory === Tsc_WatchDirectory.DynamicPolling) { - // With dynamic polling the fs change would be detected only by running timeouts - sys.runQueuedTimeoutCallbacks(); - } - // Delayed update program - sys.runQueuedTimeoutCallbacks(); - return "Rename file1 to file2"; - }, - ], - }); - } - - verifyRenamingFileInSubFolder("uses watchFile when renaming file in subfolder", Tsc_WatchDirectory.WatchFile); - - verifyRenamingFileInSubFolder("uses non recursive watchDirectory when renaming file in subfolder", Tsc_WatchDirectory.NonRecursiveWatchDirectory); - - verifyRenamingFileInSubFolder("uses non recursive dynamic polling when renaming file in subfolder", Tsc_WatchDirectory.DynamicPolling); - - verifyTscWatch({ - scenario, - subScenario: "watchDirectories/when there are symlinks to folders in recursive folders", - commandLineArgs: ["--w"], - sys: () => { - const cwd = "/home/user/projects/myproject"; - const file1: File = { - path: `${cwd}/src/file.ts`, - content: `import * as a from "a"` - }; - const tsconfig: File = { - path: `${cwd}/tsconfig.json`, - content: `{ "compilerOptions": { "extendedDiagnostics": true, "traceResolution": true }}` - }; - const realA: File = { - path: `${cwd}/node_modules/reala/index.d.ts`, - content: `export {}` - }; - const realB: File = { - path: `${cwd}/node_modules/realb/index.d.ts`, - content: `export {}` - }; - const symLinkA: SymLink = { - path: `${cwd}/node_modules/a`, - symLink: `${cwd}/node_modules/reala` - }; - const symLinkB: SymLink = { - path: `${cwd}/node_modules/b`, - symLink: `${cwd}/node_modules/realb` - }; - const symLinkBInA: SymLink = { - path: `${cwd}/node_modules/reala/node_modules/b`, - symLink: `${cwd}/node_modules/b` - }; - const symLinkAInB: SymLink = { - path: `${cwd}/node_modules/realb/node_modules/a`, - symLink: `${cwd}/node_modules/a` - }; - const files = [libFile, file1, tsconfig, realA, realB, symLinkA, symLinkB, symLinkBInA, symLinkAInB]; - const environmentVariables = createMap(); - environmentVariables.set("TSC_WATCHDIRECTORY", Tsc_WatchDirectory.NonRecursiveWatchDirectory); - return createWatchedSystem(files, { environmentVariables, currentDirectory: cwd }); - }, - changes: emptyArray - }); - - verifyTscWatch({ - scenario, - subScenario: "watchDirectories/with non synchronous watch directory", - commandLineArgs: ["--w", "-p", `${projectRoot}/tsconfig.json`], - sys: () => { - const configFile: File = { - path: `${projectRoot}/tsconfig.json`, - content: "{}" - }; - const file1: File = { - path: `${projectRoot}/src/file1.ts`, - content: `import { x } from "file2";` - }; - const file2: File = { - path: `${projectRoot}/node_modules/file2/index.d.ts`, - content: `export const x = 10;` - }; - const files = [libFile, file1, file2, configFile]; - return createWatchedSystem(files, { runWithoutRecursiveWatches: true }); - }, - changes: [ - sys => { - sys.checkTimeoutQueueLengthAndRun(1); // To update directory callbacks for file1.js output - sys.checkTimeoutQueueLengthAndRun(1); // Update program again - sys.checkTimeoutQueueLength(0); - return "Pending updates because of file1.js creation"; - }, - sys => { - // Remove directory node_modules - sys.deleteFolder(`${projectRoot}/node_modules`, /*recursive*/ true); - sys.checkTimeoutQueueLength(2); // 1. For updating program and 2. for updating child watches - sys.runQueuedTimeoutCallbacks(sys.getNextTimeoutId() - 2); // Update program - return "Remove directory node_modules"; - }, - sys => { - sys.checkTimeoutQueueLengthAndRun(1); // To update directory watchers - sys.checkTimeoutQueueLengthAndRun(1); // To Update program - sys.checkTimeoutQueueLength(0); - return "Pending directory watchers and program update"; - }, - sys => { - // npm install - sys.createDirectory(`${projectRoot}/node_modules`); - sys.checkTimeoutQueueLength(1); // To update folder structure - return "Start npm install"; - }, - sys => { - sys.createDirectory(`${projectRoot}/node_modules/file2`); - sys.checkTimeoutQueueLength(1); // To update folder structure - return "npm install folder creation of file2"; - }, - sys => { - sys.writeFile(`${projectRoot}/node_modules/file2/index.d.ts`, `export const x = 10;`); - sys.checkTimeoutQueueLength(1); // To update folder structure - return "npm install index file in file2"; - }, - sys => { - sys.runQueuedTimeoutCallbacks(); - sys.checkTimeoutQueueLength(1); // To Update the program - return "Updates the program"; - }, - sys => { - sys.runQueuedTimeoutCallbacks(); - sys.checkTimeoutQueueLength(0); - return "Pending updates"; - } - ], - }); + const files = [libFile, commonFile1, commonFile2, configFile]; + return createWatchedSystem(files, { runWithoutRecursiveWatches: true }); + }, + changes: emptyArray }); - - describe("handles watch compiler options", () => { - verifyTscWatch({ - scenario, - subScenario: "watchOptions/with watchFile option", - commandLineArgs: ["-w", "-p", "/a/b/tsconfig.json"], - sys: () => { - const configFile: File = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ - watchOptions: { - watchFile: "UseFsEvents" - } - }) - }; - const files = [libFile, commonFile1, commonFile2, configFile]; - return createWatchedSystem(files); - }, - changes: emptyArray - }); - - verifyTscWatch({ - scenario, - subScenario: "watchOptions/with watchDirectory option", - commandLineArgs: ["-w", "-p", "/a/b/tsconfig.json"], - sys: () => { - const configFile: File = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ - watchOptions: { - watchDirectory: "UseFsEvents" - } - }) - }; - const files = [libFile, commonFile1, commonFile2, configFile]; - return createWatchedSystem(files, { runWithoutRecursiveWatches: true }); - }, - changes: emptyArray - }); - - verifyTscWatch({ - scenario, - subScenario: "watchOptions/with fallbackPolling option", - commandLineArgs: ["-w", "-p", "/a/b/tsconfig.json"], - sys: () => { - const configFile: File = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ - watchOptions: { - fallbackPolling: "PriorityInterval" - } - }) - }; - const files = [libFile, commonFile1, commonFile2, configFile]; - return createWatchedSystem(files, { runWithoutRecursiveWatches: true, runWithFallbackPolling: true }); - }, - changes: emptyArray - }); - - verifyTscWatch({ - scenario, - subScenario: "watchOptions/with watchFile as watch options to extend", - commandLineArgs: ["-w", "-p", "/a/b/tsconfig.json", "--watchFile", "UseFsEvents"], - sys: () => { - const configFile: File = { - path: "/a/b/tsconfig.json", - content: "{}" - }; - const files = [libFile, commonFile1, commonFile2, configFile]; - return createWatchedSystem(files); - }, - changes: emptyArray - }); + verifyTscWatch({ + scenario, + subScenario: "watchOptions/with fallbackPolling option", + commandLineArgs: ["-w", "-p", "/a/b/tsconfig.json"], + sys: () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ + watchOptions: { + fallbackPolling: "PriorityInterval" + } + }) + }; + const files = [libFile, commonFile1, commonFile2, configFile]; + return createWatchedSystem(files, { runWithoutRecursiveWatches: true, runWithFallbackPolling: true }); + }, + changes: emptyArray + }); + verifyTscWatch({ + scenario, + subScenario: "watchOptions/with watchFile as watch options to extend", + commandLineArgs: ["-w", "-p", "/a/b/tsconfig.json", "--watchFile", "UseFsEvents"], + sys: () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: "{}" + }; + const files = [libFile, commonFile1, commonFile2, configFile]; + return createWatchedSystem(files); + }, + changes: emptyArray }); }); -} +}); diff --git a/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts b/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts index 6d5ee91d68d67..3d2f4f7bbdf50 100644 --- a/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts +++ b/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts @@ -1,181 +1,170 @@ -namespace ts.projectSystem { - describe("unittests:: tsserver:: applyChangesToOpenFiles", () => { - const configFile: File = { - path: "/a/b/tsconfig.json", - content: "{}" - }; - const file3: File = { - path: "/a/b/file3.ts", - content: "let xyz = 1;" - }; - const app: File = { - path: "/a/b/app.ts", - content: "let z = 1;" - }; - - function fileContentWithComment(file: File) { - return `// some copy right notice +import { File, TestSession, createServerHost, commonFile1, commonFile2, libFile, createSession, protocol } from "../../ts.projectSystem"; +import { ProjectService, Project } from "../../ts.server"; +describe("unittests:: tsserver:: applyChangesToOpenFiles", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: "{}" + }; + const file3: File = { + path: "/a/b/file3.ts", + content: "let xyz = 1;" + }; + const app: File = { + path: "/a/b/app.ts", + content: "let z = 1;" + }; + function fileContentWithComment(file: File) { + return `// some copy right notice ${file.content}`; - } - - function verifyText(service: server.ProjectService, file: string, expected: string) { - const info = service.getScriptInfo(file)!; - const snap = info.getSnapshot(); - // Verified applied in reverse order - assert.equal(snap.getText(0, snap.getLength()), expected, `Text of changed file: ${file}`); - } - - function verifyProjectVersion(project: server.Project, expected: number) { - assert.equal(Number(project.getProjectVersion()), expected); - } - - interface Verify { - applyChangesToOpen: (session: TestSession) => void; - openFile1Again: (session: TestSession) => void; - } - function verify({ applyChangesToOpen, openFile1Again }: Verify) { - const host = createServerHost([app, file3, commonFile1, commonFile2, libFile, configFile]); - const session = createSession(host); - session.executeCommandSeq({ - command: protocol.CommandTypes.Open, - arguments: { file: app.path } - }); - const service = session.getProjectService(); - const project = service.configuredProjects.get(configFile.path)!; - assert.isDefined(project); - verifyProjectVersion(project, 1); - session.executeCommandSeq({ - command: protocol.CommandTypes.Open, + } + function verifyText(service: ProjectService, file: string, expected: string) { + const info = service.getScriptInfo(file)!; + const snap = info.getSnapshot(); + // Verified applied in reverse order + assert.equal(snap.getText(0, snap.getLength()), expected, `Text of changed file: ${file}`); + } + function verifyProjectVersion(project: Project, expected: number) { + assert.equal(Number(project.getProjectVersion()), expected); + } + interface Verify { + applyChangesToOpen: (session: TestSession) => void; + openFile1Again: (session: TestSession) => void; + } + function verify({ applyChangesToOpen, openFile1Again }: Verify) { + const host = createServerHost([app, file3, commonFile1, commonFile2, libFile, configFile]); + const session = createSession(host); + session.executeCommandSeq({ + command: protocol.CommandTypes.Open, + arguments: { file: app.path } + }); + const service = session.getProjectService(); + const project = service.configuredProjects.get(configFile.path)!; + assert.isDefined(project); + verifyProjectVersion(project, 1); + session.executeCommandSeq({ + command: protocol.CommandTypes.Open, + arguments: { + file: file3.path, + fileContent: fileContentWithComment(file3) + } + }); + verifyProjectVersion(project, 2); + // Verify Texts + verifyText(service, commonFile1.path, commonFile1.content); + verifyText(service, commonFile2.path, commonFile2.content); + verifyText(service, app.path, app.content); + verifyText(service, file3.path, fileContentWithComment(file3)); + // Apply changes + applyChangesToOpen(session); + // Verify again + verifyProjectVersion(project, 3); + // Open file contents + verifyText(service, commonFile1.path, fileContentWithComment(commonFile1)); + verifyText(service, commonFile2.path, fileContentWithComment(commonFile2)); + verifyText(service, app.path, "let zzz = 10;let zz = 10;let z = 1;"); + verifyText(service, file3.path, file3.content); + // Open file1 again + openFile1Again(session); + assert.isTrue(service.getScriptInfo(commonFile1.path)!.isScriptOpen()); + // Verify that file1 contents are changed + verifyProjectVersion(project, 4); + verifyText(service, commonFile1.path, commonFile1.content); + verifyText(service, commonFile2.path, fileContentWithComment(commonFile2)); + verifyText(service, app.path, "let zzz = 10;let zz = 10;let z = 1;"); + verifyText(service, file3.path, file3.content); + } + it("with applyChangedToOpenFiles request", () => { + verify({ + applyChangesToOpen: session => session.executeCommandSeq({ + command: protocol.CommandTypes.ApplyChangedToOpenFiles, arguments: { - file: file3.path, - fileContent: fileContentWithComment(file3) + openFiles: [ + { + fileName: commonFile1.path, + content: fileContentWithComment(commonFile1) + }, + { + fileName: commonFile2.path, + content: fileContentWithComment(commonFile2) + } + ], + changedFiles: [ + { + fileName: app.path, + changes: [ + { + span: { start: 0, length: 0 }, + newText: "let zzz = 10;" + }, + { + span: { start: 0, length: 0 }, + newText: "let zz = 10;" + } + ] + } + ], + closedFiles: [ + file3.path + ] } - }); - verifyProjectVersion(project, 2); - - // Verify Texts - verifyText(service, commonFile1.path, commonFile1.content); - verifyText(service, commonFile2.path, commonFile2.content); - verifyText(service, app.path, app.content); - verifyText(service, file3.path, fileContentWithComment(file3)); - - // Apply changes - applyChangesToOpen(session); - - // Verify again - verifyProjectVersion(project, 3); - // Open file contents - verifyText(service, commonFile1.path, fileContentWithComment(commonFile1)); - verifyText(service, commonFile2.path, fileContentWithComment(commonFile2)); - verifyText(service, app.path, "let zzz = 10;let zz = 10;let z = 1;"); - verifyText(service, file3.path, file3.content); - - // Open file1 again - openFile1Again(session); - assert.isTrue(service.getScriptInfo(commonFile1.path)!.isScriptOpen()); - - // Verify that file1 contents are changed - verifyProjectVersion(project, 4); - verifyText(service, commonFile1.path, commonFile1.content); - verifyText(service, commonFile2.path, fileContentWithComment(commonFile2)); - verifyText(service, app.path, "let zzz = 10;let zz = 10;let z = 1;"); - verifyText(service, file3.path, file3.content); - } - - it("with applyChangedToOpenFiles request", () => { - verify({ - applyChangesToOpen: session => session.executeCommandSeq({ - command: protocol.CommandTypes.ApplyChangedToOpenFiles, - arguments: { - openFiles: [ - { - fileName: commonFile1.path, - content: fileContentWithComment(commonFile1) - }, - { - fileName: commonFile2.path, - content: fileContentWithComment(commonFile2) - } - ], - changedFiles: [ - { - fileName: app.path, - changes: [ - { - span: { start: 0, length: 0 }, - newText: "let zzz = 10;" - }, - { - span: { start: 0, length: 0 }, - newText: "let zz = 10;" - } - ] - } - ], - closedFiles: [ - file3.path - ] - } - }), - openFile1Again: session => session.executeCommandSeq({ - command: protocol.CommandTypes.ApplyChangedToOpenFiles, - arguments: { - openFiles: [{ + }), + openFile1Again: session => session.executeCommandSeq({ + command: protocol.CommandTypes.ApplyChangedToOpenFiles, + arguments: { + openFiles: [{ fileName: commonFile1.path, content: commonFile1.content }] - } - }), - }); + } + }), }); - - it("with updateOpen request", () => { - verify({ - applyChangesToOpen: session => session.executeCommandSeq({ - command: protocol.CommandTypes.UpdateOpen, - arguments: { - openFiles: [ - { - file: commonFile1.path, - fileContent: fileContentWithComment(commonFile1) - }, - { - file: commonFile2.path, - fileContent: fileContentWithComment(commonFile2) - } - ], - changedFiles: [ - { - fileName: app.path, - textChanges: [ - { - start: { line: 1, offset: 1 }, - end: { line: 1, offset: 1 }, - newText: "let zzz = 10;", - }, - { - start: { line: 1, offset: 1 }, - end: { line: 1, offset: 1 }, - newText: "let zz = 10;", - } - ] - } - ], - closedFiles: [ - file3.path - ] - } - }), - openFile1Again: session => session.executeCommandSeq({ - command: protocol.CommandTypes.UpdateOpen, - arguments: { - openFiles: [{ + }); + it("with updateOpen request", () => { + verify({ + applyChangesToOpen: session => session.executeCommandSeq({ + command: protocol.CommandTypes.UpdateOpen, + arguments: { + openFiles: [ + { + file: commonFile1.path, + fileContent: fileContentWithComment(commonFile1) + }, + { + file: commonFile2.path, + fileContent: fileContentWithComment(commonFile2) + } + ], + changedFiles: [ + { + fileName: app.path, + textChanges: [ + { + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 1 }, + newText: "let zzz = 10;", + }, + { + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 1 }, + newText: "let zz = 10;", + } + ] + } + ], + closedFiles: [ + file3.path + ] + } + }), + openFile1Again: session => session.executeCommandSeq({ + command: protocol.CommandTypes.UpdateOpen, + arguments: { + openFiles: [{ file: commonFile1.path, fileContent: commonFile1.content }] - } - }), - }); + } + }), }); }); -} +}); diff --git a/src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts b/src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts index 5e6dc224cfcac..91e073475f178 100644 --- a/src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts +++ b/src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts @@ -1,520 +1,458 @@ -namespace ts.projectSystem { - function getNumberOfWatchesInvokedForRecursiveWatches(recursiveWatchedDirs: string[], file: string) { - return countWhere(recursiveWatchedDirs, dir => file.length > dir.length && startsWith(file, dir) && file[dir.length] === directorySeparator); +import { countWhere, startsWith, directorySeparator, MultiMap, createMultiMap, arrayFrom, TestFSWithWatch, ModuleKind, getDirectoryPath, returnTrue, ScriptTarget, Diagnostics, flattenDiagnosticMessageText, forEachAncestorDirectory, combinePaths, createMap, map, server, Path, mapDefined, forEach, find, filter } from "../../ts"; +import { TestServerHost, File, createServerHost, createProjectService, checkNumberOfProjects, mapCombinedPathsInAncestor, nodeModulesAtTypes, nodeModules, createSession, checkNumberOfConfiguredProjects, checkProjectActualFiles, makeSessionRequest, protocol, libFile, getNodeModuleDirectories, checkWatchedFiles, checkWatchedDirectories, getTypeRootsFromLocation } from "../../ts.projectSystem"; +import { NormalizedPath } from "../../ts.server"; +import * as ts from "../../ts"; +function getNumberOfWatchesInvokedForRecursiveWatches(recursiveWatchedDirs: string[], file: string) { + return countWhere(recursiveWatchedDirs, dir => file.length > dir.length && startsWith(file, dir) && file[dir.length] === directorySeparator); +} +describe("unittests:: tsserver:: CachingFileSystemInformation:: tsserverProjectSystem CachingFileSystemInformation", () => { + enum CalledMapsWithSingleArg { + fileExists = "fileExists", + directoryExists = "directoryExists", + getDirectories = "getDirectories", + readFile = "readFile" } - - describe("unittests:: tsserver:: CachingFileSystemInformation:: tsserverProjectSystem CachingFileSystemInformation", () => { - enum CalledMapsWithSingleArg { - fileExists = "fileExists", - directoryExists = "directoryExists", - getDirectories = "getDirectories", - readFile = "readFile" - } - enum CalledMapsWithFiveArgs { - readDirectory = "readDirectory" - } - type CalledMaps = CalledMapsWithSingleArg | CalledMapsWithFiveArgs; - type CalledWithFiveArgs = [readonly string[], readonly string[], readonly string[], number]; - function createCallsTrackingHost(host: TestServerHost) { - const calledMaps: Record> & Record> = { - fileExists: setCallsTrackingWithSingleArgFn(CalledMapsWithSingleArg.fileExists), - directoryExists: setCallsTrackingWithSingleArgFn(CalledMapsWithSingleArg.directoryExists), - getDirectories: setCallsTrackingWithSingleArgFn(CalledMapsWithSingleArg.getDirectories), - readFile: setCallsTrackingWithSingleArgFn(CalledMapsWithSingleArg.readFile), - readDirectory: setCallsTrackingWithFiveArgFn(CalledMapsWithFiveArgs.readDirectory) + enum CalledMapsWithFiveArgs { + readDirectory = "readDirectory" + } + type CalledMaps = CalledMapsWithSingleArg | CalledMapsWithFiveArgs; + type CalledWithFiveArgs = [readonly string[], readonly string[], readonly string[], number]; + function createCallsTrackingHost(host: TestServerHost) { + const calledMaps: Record> & Record> = { + fileExists: setCallsTrackingWithSingleArgFn(CalledMapsWithSingleArg.fileExists), + directoryExists: setCallsTrackingWithSingleArgFn(CalledMapsWithSingleArg.directoryExists), + getDirectories: setCallsTrackingWithSingleArgFn(CalledMapsWithSingleArg.getDirectories), + readFile: setCallsTrackingWithSingleArgFn(CalledMapsWithSingleArg.readFile), + readDirectory: setCallsTrackingWithFiveArgFn(CalledMapsWithFiveArgs.readDirectory) + }; + return { + verifyNoCall, + verifyCalledOnEachEntryNTimes, + verifyCalledOnEachEntry, + verifyNoHostCalls, + verifyNoHostCallsExceptFileExistsOnce, + verifyCalledOn, + clear + }; + function setCallsTrackingWithSingleArgFn(prop: CalledMapsWithSingleArg) { + const calledMap = createMultiMap(); + const cb = (host)[prop].bind(host); + (host)[prop] = (f: string) => { + calledMap.add(f, /*value*/ true); + return cb(f); }; - - return { - verifyNoCall, - verifyCalledOnEachEntryNTimes, - verifyCalledOnEachEntry, - verifyNoHostCalls, - verifyNoHostCallsExceptFileExistsOnce, - verifyCalledOn, - clear + return calledMap; + } + function setCallsTrackingWithFiveArgFn(prop: CalledMapsWithFiveArgs) { + const calledMap = createMultiMap<[U, V, W, X]>(); + const cb = (host)[prop].bind(host); + (host)[prop] = (f: string, arg1?: U, arg2?: V, arg3?: W, arg4?: X) => { + calledMap.add(f, [arg1!, arg2!, arg3!, arg4!]); // TODO: GH#18217 + return cb(f, arg1, arg2, arg3, arg4); }; - - function setCallsTrackingWithSingleArgFn(prop: CalledMapsWithSingleArg) { - const calledMap = createMultiMap(); - const cb = (host)[prop].bind(host); - (host)[prop] = (f: string) => { - calledMap.add(f, /*value*/ true); - return cb(f); - }; - return calledMap; - } - - function setCallsTrackingWithFiveArgFn(prop: CalledMapsWithFiveArgs) { - const calledMap = createMultiMap<[U, V, W, X]>(); - const cb = (host)[prop].bind(host); - (host)[prop] = (f: string, arg1?: U, arg2?: V, arg3?: W, arg4?: X) => { - calledMap.add(f, [arg1!, arg2!, arg3!, arg4!]); // TODO: GH#18217 - return cb(f, arg1, arg2, arg3, arg4); - }; - return calledMap; - } - - function verifyCalledOn(callback: CalledMaps, name: string) { - const calledMap = calledMaps[callback]; - const result = calledMap.get(name); - assert.isTrue(result && !!result.length, `${callback} should be called with name: ${name}: ${arrayFrom(calledMap.keys())}`); - } - - function verifyNoCall(callback: CalledMaps) { - const calledMap = calledMaps[callback]; - assert.equal(calledMap.size, 0, `${callback} shouldn't be called: ${arrayFrom(calledMap.keys())}`); - } - - function verifyCalledOnEachEntry(callback: CalledMaps, expectedKeys: Map) { - TestFSWithWatch.checkMap(callback, calledMaps[callback], expectedKeys); - } - - function verifyCalledOnEachEntryNTimes(callback: CalledMaps, expectedKeys: readonly string[], nTimes: number) { - TestFSWithWatch.checkMap(callback, calledMaps[callback], expectedKeys, nTimes); - } - - function verifyNoHostCalls() { - iterateOnCalledMaps(key => verifyNoCall(key)); - } - - function verifyNoHostCallsExceptFileExistsOnce(expectedKeys: readonly string[]) { - verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.fileExists, expectedKeys, 1); - verifyNoCall(CalledMapsWithSingleArg.directoryExists); - verifyNoCall(CalledMapsWithSingleArg.getDirectories); - verifyNoCall(CalledMapsWithSingleArg.readFile); - verifyNoCall(CalledMapsWithFiveArgs.readDirectory); - } - - function clear() { - iterateOnCalledMaps(key => calledMaps[key].clear()); + return calledMap; + } + function verifyCalledOn(callback: CalledMaps, name: string) { + const calledMap = calledMaps[callback]; + const result = calledMap.get(name); + assert.isTrue(result && !!result.length, `${callback} should be called with name: ${name}: ${arrayFrom(calledMap.keys())}`); + } + function verifyNoCall(callback: CalledMaps) { + const calledMap = calledMaps[callback]; + assert.equal(calledMap.size, 0, `${callback} shouldn't be called: ${arrayFrom(calledMap.keys())}`); + } + function verifyCalledOnEachEntry(callback: CalledMaps, expectedKeys: ts.Map) { + TestFSWithWatch.checkMap(callback, calledMaps[callback], expectedKeys); + } + function verifyCalledOnEachEntryNTimes(callback: CalledMaps, expectedKeys: readonly string[], nTimes: number) { + TestFSWithWatch.checkMap(callback, calledMaps[callback], expectedKeys, nTimes); + } + function verifyNoHostCalls() { + iterateOnCalledMaps(key => verifyNoCall(key)); + } + function verifyNoHostCallsExceptFileExistsOnce(expectedKeys: readonly string[]) { + verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.fileExists, expectedKeys, 1); + verifyNoCall(CalledMapsWithSingleArg.directoryExists); + verifyNoCall(CalledMapsWithSingleArg.getDirectories); + verifyNoCall(CalledMapsWithSingleArg.readFile); + verifyNoCall(CalledMapsWithFiveArgs.readDirectory); + } + function clear() { + iterateOnCalledMaps(key => calledMaps[key].clear()); + } + function iterateOnCalledMaps(cb: (key: CalledMaps) => void) { + for (const key in CalledMapsWithSingleArg) { + cb(key as CalledMapsWithSingleArg); } - - function iterateOnCalledMaps(cb: (key: CalledMaps) => void) { - for (const key in CalledMapsWithSingleArg) { - cb(key as CalledMapsWithSingleArg); - } - for (const key in CalledMapsWithFiveArgs) { - cb(key as CalledMapsWithFiveArgs); - } + for (const key in CalledMapsWithFiveArgs) { + cb(key as CalledMapsWithFiveArgs); } } - - it("works using legacy resolution logic", () => { - let rootContent = `import {x} from "f1"`; - const root: File = { - path: "/c/d/f0.ts", - content: rootContent - }; - - const imported: File = { - path: "/c/f1.ts", - content: `foo()` - }; - - const host = createServerHost([root, imported]); - const projectService = createProjectService(host); - projectService.setCompilerOptionsForInferredProjects({ module: ModuleKind.AMD, noLib: true }); - projectService.openClientFile(root.path); - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - const project = projectService.inferredProjects[0]; - const rootScriptInfo = project.getRootScriptInfos()[0]; - assert.equal(rootScriptInfo.fileName, root.path); - - // ensure that imported file was found - verifyImportedDiagnostics(); - - const callsTrackingHost = createCallsTrackingHost(host); - - // trigger synchronization to make sure that import will be fetched from the cache - // ensure file has correct number of errors after edit - editContent(`import {x} from "f1"; + } + it("works using legacy resolution logic", () => { + let rootContent = `import {x} from "f1"`; + const root: File = { + path: "/c/d/f0.ts", + content: rootContent + }; + const imported: File = { + path: "/c/f1.ts", + content: `foo()` + }; + const host = createServerHost([root, imported]); + const projectService = createProjectService(host); + projectService.setCompilerOptionsForInferredProjects({ module: ModuleKind.AMD, noLib: true }); + projectService.openClientFile(root.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const project = projectService.inferredProjects[0]; + const rootScriptInfo = project.getRootScriptInfos()[0]; + assert.equal(rootScriptInfo.fileName, root.path); + // ensure that imported file was found + verifyImportedDiagnostics(); + const callsTrackingHost = createCallsTrackingHost(host); + // trigger synchronization to make sure that import will be fetched from the cache + // ensure file has correct number of errors after edit + editContent(`import {x} from "f1"; var x: string = 1;`); - verifyImportedDiagnostics(); - callsTrackingHost.verifyNoHostCalls(); - + verifyImportedDiagnostics(); + callsTrackingHost.verifyNoHostCalls(); + // trigger synchronization to make sure that the host will try to find 'f2' module on disk + editContent(`import {x} from "f2"`); + try { // trigger synchronization to make sure that the host will try to find 'f2' module on disk - editContent(`import {x} from "f2"`); - try { - // trigger synchronization to make sure that the host will try to find 'f2' module on disk - verifyImportedDiagnostics(); - assert.isTrue(false, `should not find file '${imported.path}'`); - } - catch (e) { - assert.isTrue(e.message.indexOf(`Could not find source file: '${imported.path}'.`) === 0, `Actual: ${e.message}`); - } - const f2Lookups = getLocationsForModuleLookup("f2"); - callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.fileExists, f2Lookups, 1); - const f2DirLookups = getLocationsForDirectoryLookup(); - callsTrackingHost.verifyCalledOnEachEntry(CalledMapsWithSingleArg.directoryExists, f2DirLookups); + verifyImportedDiagnostics(); + assert.isTrue(false, `should not find file '${imported.path}'`); + } + catch (e) { + assert.isTrue(e.message.indexOf(`Could not find source file: '${imported.path}'.`) === 0, `Actual: ${e.message}`); + } + const f2Lookups = getLocationsForModuleLookup("f2"); + callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.fileExists, f2Lookups, 1); + const f2DirLookups = getLocationsForDirectoryLookup(); + callsTrackingHost.verifyCalledOnEachEntry(CalledMapsWithSingleArg.directoryExists, f2DirLookups); + callsTrackingHost.verifyNoCall(CalledMapsWithSingleArg.getDirectories); + callsTrackingHost.verifyNoCall(CalledMapsWithSingleArg.readFile); + callsTrackingHost.verifyNoCall(CalledMapsWithFiveArgs.readDirectory); + editContent(`import {x} from "f1"`); + verifyImportedDiagnostics(); + const f1Lookups = f2Lookups.map(s => s.replace("f2", "f1")); + f1Lookups.length = f1Lookups.indexOf(imported.path) + 1; + const f1DirLookups = ["/c/d", "/c", ...mapCombinedPathsInAncestor(getDirectoryPath(root.path), nodeModulesAtTypes, returnTrue)]; + vertifyF1Lookups(); + // setting compiler options discards module resolution cache + callsTrackingHost.clear(); + projectService.setCompilerOptionsForInferredProjects({ module: ModuleKind.AMD, noLib: true, target: ScriptTarget.ES5 }); + verifyImportedDiagnostics(); + vertifyF1Lookups(); + function vertifyF1Lookups() { + callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.fileExists, f1Lookups, 1); + callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.directoryExists, f1DirLookups, 1); callsTrackingHost.verifyNoCall(CalledMapsWithSingleArg.getDirectories); callsTrackingHost.verifyNoCall(CalledMapsWithSingleArg.readFile); callsTrackingHost.verifyNoCall(CalledMapsWithFiveArgs.readDirectory); - - editContent(`import {x} from "f1"`); - verifyImportedDiagnostics(); - const f1Lookups = f2Lookups.map(s => s.replace("f2", "f1")); - f1Lookups.length = f1Lookups.indexOf(imported.path) + 1; - const f1DirLookups = ["/c/d", "/c", ...mapCombinedPathsInAncestor(getDirectoryPath(root.path), nodeModulesAtTypes, returnTrue)]; - vertifyF1Lookups(); - - // setting compiler options discards module resolution cache + } + function editContent(newContent: string) { callsTrackingHost.clear(); - projectService.setCompilerOptionsForInferredProjects({ module: ModuleKind.AMD, noLib: true, target: ScriptTarget.ES5 }); - verifyImportedDiagnostics(); - vertifyF1Lookups(); - - function vertifyF1Lookups() { - callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.fileExists, f1Lookups, 1); - callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.directoryExists, f1DirLookups, 1); - callsTrackingHost.verifyNoCall(CalledMapsWithSingleArg.getDirectories); - callsTrackingHost.verifyNoCall(CalledMapsWithSingleArg.readFile); - callsTrackingHost.verifyNoCall(CalledMapsWithFiveArgs.readDirectory); - } - - function editContent(newContent: string) { - callsTrackingHost.clear(); - rootScriptInfo.editContent(0, rootContent.length, newContent); - rootContent = newContent; - } - - function verifyImportedDiagnostics() { - const diags = project.getLanguageService().getSemanticDiagnostics(imported.path); - assert.equal(diags.length, 1); - const diag = diags[0]; - assert.equal(diag.code, Diagnostics.Cannot_find_name_0.code); - assert.equal(flattenDiagnosticMessageText(diag.messageText, "\n"), "Cannot find name 'foo'."); - } - - function getLocationsForModuleLookup(module: string) { - const locations: string[] = []; - forEachAncestorDirectory(getDirectoryPath(root.path), ancestor => { - locations.push( - combinePaths(ancestor, `${module}.ts`), - combinePaths(ancestor, `${module}.tsx`), - combinePaths(ancestor, `${module}.d.ts`) - ); - }); - forEachAncestorDirectory(getDirectoryPath(root.path), ancestor => { - locations.push( - combinePaths(ancestor, `${module}.js`), - combinePaths(ancestor, `${module}.jsx`) - ); - }); - return locations; - } - - function getLocationsForDirectoryLookup() { - const result = createMap(); - forEachAncestorDirectory(getDirectoryPath(root.path), ancestor => { - // To resolve modules - result.set(ancestor, 2); - // for type roots - result.set(combinePaths(ancestor, nodeModules), 1); - result.set(combinePaths(ancestor, nodeModulesAtTypes), 1); - }); - return result; - } - }); - - it("loads missing files from disk", () => { - const root: File = { - path: "/c/foo.ts", - content: `import {y} from "bar"` - }; - - const imported: File = { - path: "/c/bar.d.ts", - content: `export var y = 1` - }; - - const host = createServerHost([root]); - const projectService = createProjectService(host); - projectService.setCompilerOptionsForInferredProjects({ module: ModuleKind.AMD, noLib: true }); - const callsTrackingHost = createCallsTrackingHost(host); - projectService.openClientFile(root.path); - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - const project = projectService.inferredProjects[0]; - const rootScriptInfo = project.getRootScriptInfos()[0]; - assert.equal(rootScriptInfo.fileName, root.path); - - let diags = project.getLanguageService().getSemanticDiagnostics(root.path); + rootScriptInfo.editContent(0, rootContent.length, newContent); + rootContent = newContent; + } + function verifyImportedDiagnostics() { + const diags = project.getLanguageService().getSemanticDiagnostics(imported.path); assert.equal(diags.length, 1); const diag = diags[0]; - assert.equal(diag.code, Diagnostics.Cannot_find_module_0.code); - assert.equal(flattenDiagnosticMessageText(diag.messageText, "\n"), "Cannot find module 'bar'."); - callsTrackingHost.verifyCalledOn(CalledMapsWithSingleArg.fileExists, imported.path); - - - callsTrackingHost.clear(); - host.reloadFS([root, imported]); - host.runQueuedTimeoutCallbacks(); - diags = project.getLanguageService().getSemanticDiagnostics(root.path); - assert.equal(diags.length, 0); - callsTrackingHost.verifyCalledOn(CalledMapsWithSingleArg.fileExists, imported.path); - }); - - it("when calling goto definition of module", () => { - const clientFile: File = { - path: "/a/b/controllers/vessels/client.ts", - content: ` + assert.equal(diag.code, Diagnostics.Cannot_find_name_0.code); + assert.equal(flattenDiagnosticMessageText(diag.messageText, "\n"), "Cannot find name 'foo'."); + } + function getLocationsForModuleLookup(module: string) { + const locations: string[] = []; + forEachAncestorDirectory(getDirectoryPath(root.path), ancestor => { + locations.push(combinePaths(ancestor, `${module}.ts`), combinePaths(ancestor, `${module}.tsx`), combinePaths(ancestor, `${module}.d.ts`)); + }); + forEachAncestorDirectory(getDirectoryPath(root.path), ancestor => { + locations.push(combinePaths(ancestor, `${module}.js`), combinePaths(ancestor, `${module}.jsx`)); + }); + return locations; + } + function getLocationsForDirectoryLookup() { + const result = createMap(); + forEachAncestorDirectory(getDirectoryPath(root.path), ancestor => { + // To resolve modules + result.set(ancestor, 2); + // for type roots + result.set(combinePaths(ancestor, nodeModules), 1); + result.set(combinePaths(ancestor, nodeModulesAtTypes), 1); + }); + return result; + } + }); + it("loads missing files from disk", () => { + const root: File = { + path: "/c/foo.ts", + content: `import {y} from "bar"` + }; + const imported: File = { + path: "/c/bar.d.ts", + content: `export var y = 1` + }; + const host = createServerHost([root]); + const projectService = createProjectService(host); + projectService.setCompilerOptionsForInferredProjects({ module: ModuleKind.AMD, noLib: true }); + const callsTrackingHost = createCallsTrackingHost(host); + projectService.openClientFile(root.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const project = projectService.inferredProjects[0]; + const rootScriptInfo = project.getRootScriptInfos()[0]; + assert.equal(rootScriptInfo.fileName, root.path); + let diags = project.getLanguageService().getSemanticDiagnostics(root.path); + assert.equal(diags.length, 1); + const diag = diags[0]; + assert.equal(diag.code, Diagnostics.Cannot_find_module_0.code); + assert.equal(flattenDiagnosticMessageText(diag.messageText, "\n"), "Cannot find module 'bar'."); + callsTrackingHost.verifyCalledOn(CalledMapsWithSingleArg.fileExists, imported.path); + callsTrackingHost.clear(); + host.reloadFS([root, imported]); + host.runQueuedTimeoutCallbacks(); + diags = project.getLanguageService().getSemanticDiagnostics(root.path); + assert.equal(diags.length, 0); + callsTrackingHost.verifyCalledOn(CalledMapsWithSingleArg.fileExists, imported.path); + }); + it("when calling goto definition of module", () => { + const clientFile: File = { + path: "/a/b/controllers/vessels/client.ts", + content: ` import { Vessel } from '~/models/vessel'; const v = new Vessel(); ` - }; - const anotherModuleFile: File = { - path: "/a/b/utils/db.ts", - content: "export class Bookshelf { }" - }; - const moduleFile: File = { - path: "/a/b/models/vessel.ts", - content: ` + }; + const anotherModuleFile: File = { + path: "/a/b/utils/db.ts", + content: "export class Bookshelf { }" + }; + const moduleFile: File = { + path: "/a/b/models/vessel.ts", + content: ` import { Bookshelf } from '~/utils/db'; export class Vessel extends Bookshelf {} ` + }; + const tsconfigFile: File = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + target: "es6", + module: "es6", + baseUrl: "./", + paths: { + "~/*": ["*"] // resolve any `~/foo/bar` to `/foo/bar` + } + }, + exclude: [ + "api", + "build", + "node_modules", + "public", + "seeds", + "sql_updates", + "tests.build" + ] + }) + }; + const projectFiles = [clientFile, anotherModuleFile, moduleFile, tsconfigFile]; + const host = createServerHost(projectFiles); + const session = createSession(host); + const projectService = session.getProjectService(); + const { configFileName } = projectService.openClientFile(clientFile.path); + assert.isDefined(configFileName, `should find config`); + checkNumberOfConfiguredProjects(projectService, 1); + const project = projectService.configuredProjects.get(tsconfigFile.path)!; + checkProjectActualFiles(project, map(projectFiles, f => f.path)); + const callsTrackingHost = createCallsTrackingHost(host); + // Get definitions shouldnt make host requests + const getDefinitionRequest = makeSessionRequest(protocol.CommandTypes.Definition, { + file: clientFile.path, + position: clientFile.content.indexOf("/vessel") + 1, + line: undefined!, + offset: undefined! // TODO: GH#18217 + }); + const response = (session.executeCommand(getDefinitionRequest).response as server.protocol.FileSpan[]); + assert.equal(response[0].file, moduleFile.path, "Should go to definition of vessel: response: " + JSON.stringify(response)); + callsTrackingHost.verifyNoHostCalls(); + // Open the file should call only file exists on module directory and use cached value for parental directory + const { configFileName: config2 } = projectService.openClientFile(moduleFile.path); + assert.equal(config2, configFileName); + callsTrackingHost.verifyNoHostCallsExceptFileExistsOnce(["/a/b/models/tsconfig.json", "/a/b/models/jsconfig.json"]); + checkNumberOfConfiguredProjects(projectService, 1); + assert.strictEqual(projectService.configuredProjects.get(tsconfigFile.path), project); + }); + describe("WatchDirectories for config file with", () => { + function verifyWatchDirectoriesCaseSensitivity(useCaseSensitiveFileNames: boolean) { + const frontendDir = "/Users/someuser/work/applications/frontend"; + const toCanonical: (s: string) => Path = useCaseSensitiveFileNames ? s => s as Path : s => s.toLowerCase() as Path; + const canonicalFrontendDir = toCanonical(frontendDir); + const file1: File = { + path: `${frontendDir}/src/app/utils/Analytic.ts`, + content: "export class SomeClass { };" + }; + const file2: File = { + path: `${frontendDir}/src/app/redux/configureStore.ts`, + content: "export class configureStore { }" }; + const file3: File = { + path: `${frontendDir}/src/app/utils/Cookie.ts`, + content: "export class Cookie { }" + }; + const es2016LibFile: File = { + path: "/a/lib/lib.es2016.full.d.ts", + content: libFile.content + }; + const typeRoots = ["types", "node_modules/@types"]; + const types = ["node", "jest"]; const tsconfigFile: File = { - path: "/a/b/tsconfig.json", + path: `${frontendDir}/tsconfig.json`, content: JSON.stringify({ compilerOptions: { - target: "es6", - module: "es6", - baseUrl: "./", // all paths are relative to the baseUrl + strict: true, + strictNullChecks: true, + target: "es2016", + module: "commonjs", + moduleResolution: "node", + sourceMap: true, + noEmitOnError: true, + experimentalDecorators: true, + emitDecoratorMetadata: true, + types, + noUnusedLocals: true, + outDir: "./compiled", + typeRoots, + baseUrl: ".", paths: { - "~/*": ["*"] // resolve any `~/foo/bar` to `/foo/bar` + "*": [ + "types/*" + ] } }, + include: [ + "src/**/*" + ], exclude: [ - "api", - "build", "node_modules", - "public", - "seeds", - "sql_updates", - "tests.build" + "compiled" ] }) }; - const projectFiles = [clientFile, anotherModuleFile, moduleFile, tsconfigFile]; - const host = createServerHost(projectFiles); - const session = createSession(host); - const projectService = session.getProjectService(); - const { configFileName } = projectService.openClientFile(clientFile.path); - - assert.isDefined(configFileName, `should find config`); + const projectFiles = [file1, file2, es2016LibFile, tsconfigFile]; + const host = createServerHost(projectFiles, { useCaseSensitiveFileNames }); + const projectService = createProjectService(host); + const canonicalConfigPath = toCanonical(tsconfigFile.path); + const { configFileName } = projectService.openClientFile(file1.path); + assert.equal(configFileName, (tsconfigFile.path as NormalizedPath), `should find config`); checkNumberOfConfiguredProjects(projectService, 1); - - const project = projectService.configuredProjects.get(tsconfigFile.path)!; - checkProjectActualFiles(project, map(projectFiles, f => f.path)); - + const watchingRecursiveDirectories = [`${canonicalFrontendDir}/src`, `${canonicalFrontendDir}/types`, `${canonicalFrontendDir}/node_modules`].concat(getNodeModuleDirectories(getDirectoryPath(canonicalFrontendDir))); + const project = projectService.configuredProjects.get(canonicalConfigPath)!; + verifyProjectAndWatchedDirectories(); const callsTrackingHost = createCallsTrackingHost(host); - - // Get definitions shouldnt make host requests - const getDefinitionRequest = makeSessionRequest(protocol.CommandTypes.Definition, { - file: clientFile.path, - position: clientFile.content.indexOf("/vessel") + 1, - line: undefined!, // TODO: GH#18217 - offset: undefined! // TODO: GH#18217 - }); - const response = session.executeCommand(getDefinitionRequest).response as server.protocol.FileSpan[]; - assert.equal(response[0].file, moduleFile.path, "Should go to definition of vessel: response: " + JSON.stringify(response)); - callsTrackingHost.verifyNoHostCalls(); - - // Open the file should call only file exists on module directory and use cached value for parental directory - const { configFileName: config2 } = projectService.openClientFile(moduleFile.path); - assert.equal(config2, configFileName); - callsTrackingHost.verifyNoHostCallsExceptFileExistsOnce(["/a/b/models/tsconfig.json", "/a/b/models/jsconfig.json"]); - + // Create file cookie.ts + projectFiles.push(file3); + host.reloadFS(projectFiles); + host.runQueuedTimeoutCallbacks(); + const canonicalFile3Path = useCaseSensitiveFileNames ? file3.path : file3.path.toLocaleLowerCase(); + const numberOfTimesWatchInvoked = getNumberOfWatchesInvokedForRecursiveWatches(watchingRecursiveDirectories, canonicalFile3Path); + callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.fileExists, [canonicalFile3Path], numberOfTimesWatchInvoked); + callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.directoryExists, [canonicalFile3Path], numberOfTimesWatchInvoked); + callsTrackingHost.verifyNoCall(CalledMapsWithSingleArg.getDirectories); + callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.readFile, [file3.path], 1); + callsTrackingHost.verifyNoCall(CalledMapsWithFiveArgs.readDirectory); checkNumberOfConfiguredProjects(projectService, 1); - assert.strictEqual(projectService.configuredProjects.get(tsconfigFile.path), project); - }); - - describe("WatchDirectories for config file with", () => { - function verifyWatchDirectoriesCaseSensitivity(useCaseSensitiveFileNames: boolean) { - const frontendDir = "/Users/someuser/work/applications/frontend"; - const toCanonical: (s: string) => Path = useCaseSensitiveFileNames ? s => s as Path : s => s.toLowerCase() as Path; - const canonicalFrontendDir = toCanonical(frontendDir); - const file1: File = { - path: `${frontendDir}/src/app/utils/Analytic.ts`, - content: "export class SomeClass { };" - }; - const file2: File = { - path: `${frontendDir}/src/app/redux/configureStore.ts`, - content: "export class configureStore { }" - }; - const file3: File = { - path: `${frontendDir}/src/app/utils/Cookie.ts`, - content: "export class Cookie { }" - }; - const es2016LibFile: File = { - path: "/a/lib/lib.es2016.full.d.ts", - content: libFile.content - }; - const typeRoots = ["types", "node_modules/@types"]; - const types = ["node", "jest"]; - const tsconfigFile: File = { - path: `${frontendDir}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - strict: true, - strictNullChecks: true, - target: "es2016", - module: "commonjs", - moduleResolution: "node", - sourceMap: true, - noEmitOnError: true, - experimentalDecorators: true, - emitDecoratorMetadata: true, - types, - noUnusedLocals: true, - outDir: "./compiled", - typeRoots, - baseUrl: ".", - paths: { - "*": [ - "types/*" - ] - } - }, - include: [ - "src/**/*" - ], - exclude: [ - "node_modules", - "compiled" - ] - }) - }; - const projectFiles = [file1, file2, es2016LibFile, tsconfigFile]; - const host = createServerHost(projectFiles, { useCaseSensitiveFileNames }); - const projectService = createProjectService(host); - const canonicalConfigPath = toCanonical(tsconfigFile.path); - const { configFileName } = projectService.openClientFile(file1.path); - assert.equal(configFileName, tsconfigFile.path as server.NormalizedPath, `should find config`); - checkNumberOfConfiguredProjects(projectService, 1); - const watchingRecursiveDirectories = [`${canonicalFrontendDir}/src`, `${canonicalFrontendDir}/types`, `${canonicalFrontendDir}/node_modules`].concat(getNodeModuleDirectories(getDirectoryPath(canonicalFrontendDir))); - - const project = projectService.configuredProjects.get(canonicalConfigPath)!; - verifyProjectAndWatchedDirectories(); - - const callsTrackingHost = createCallsTrackingHost(host); - - // Create file cookie.ts - projectFiles.push(file3); - host.reloadFS(projectFiles); - host.runQueuedTimeoutCallbacks(); - - const canonicalFile3Path = useCaseSensitiveFileNames ? file3.path : file3.path.toLocaleLowerCase(); - const numberOfTimesWatchInvoked = getNumberOfWatchesInvokedForRecursiveWatches(watchingRecursiveDirectories, canonicalFile3Path); - callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.fileExists, [canonicalFile3Path], numberOfTimesWatchInvoked); - callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.directoryExists, [canonicalFile3Path], numberOfTimesWatchInvoked); - callsTrackingHost.verifyNoCall(CalledMapsWithSingleArg.getDirectories); - callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.readFile, [file3.path], 1); - callsTrackingHost.verifyNoCall(CalledMapsWithFiveArgs.readDirectory); - - checkNumberOfConfiguredProjects(projectService, 1); - assert.strictEqual(projectService.configuredProjects.get(canonicalConfigPath), project); - verifyProjectAndWatchedDirectories(); - - callsTrackingHost.clear(); - - const { configFileName: configFile2 } = projectService.openClientFile(file3.path); - assert.equal(configFile2, configFileName); - - checkNumberOfConfiguredProjects(projectService, 1); - assert.strictEqual(projectService.configuredProjects.get(canonicalConfigPath), project); - verifyProjectAndWatchedDirectories(); - callsTrackingHost.verifyNoHostCalls(); - - function getFilePathIfNotOpen(f: File) { - const path = toCanonical(f.path); - const info = projectService.getScriptInfoForPath(toCanonical(f.path)); - return info && info.isScriptOpen() ? undefined : path; - } - - function verifyProjectAndWatchedDirectories() { - checkProjectActualFiles(project, map(projectFiles, f => f.path)); - checkWatchedFiles(host, mapDefined(projectFiles, getFilePathIfNotOpen)); - checkWatchedDirectories(host, watchingRecursiveDirectories, /*recursive*/ true); - checkWatchedDirectories(host, [], /*recursive*/ false); - } + assert.strictEqual(projectService.configuredProjects.get(canonicalConfigPath), project); + verifyProjectAndWatchedDirectories(); + callsTrackingHost.clear(); + const { configFileName: configFile2 } = projectService.openClientFile(file3.path); + assert.equal(configFile2, configFileName); + checkNumberOfConfiguredProjects(projectService, 1); + assert.strictEqual(projectService.configuredProjects.get(canonicalConfigPath), project); + verifyProjectAndWatchedDirectories(); + callsTrackingHost.verifyNoHostCalls(); + function getFilePathIfNotOpen(f: File) { + const path = toCanonical(f.path); + const info = projectService.getScriptInfoForPath(toCanonical(f.path)); + return info && info.isScriptOpen() ? undefined : path; } - - it("case insensitive file system", () => { - verifyWatchDirectoriesCaseSensitivity(/*useCaseSensitiveFileNames*/ false); - }); - - it("case sensitive file system", () => { - verifyWatchDirectoriesCaseSensitivity(/*useCaseSensitiveFileNames*/ true); - }); - }); - - describe("Subfolder invalidations correctly include parent folder failed lookup locations", () => { - function runFailedLookupTest(resolution: "Node" | "Classic") { - const projectLocation = "/proj"; - const file1: File = { - path: `${projectLocation}/foo/boo/app.ts`, - content: `import * as debug from "debug"` - }; - const file2: File = { - path: `${projectLocation}/foo/boo/moo/app.ts`, - content: `import * as debug from "debug"` - }; - const tsconfig: File = { - path: `${projectLocation}/tsconfig.json`, - content: JSON.stringify({ - files: ["foo/boo/app.ts", "foo/boo/moo/app.ts"], - moduleResolution: resolution - }) - }; - - const files = [file1, file2, tsconfig, libFile]; - const host = createServerHost(files); - const service = createProjectService(host); - service.openClientFile(file1.path); - - const project = service.configuredProjects.get(tsconfig.path)!; - checkProjectActualFiles(project, files.map(f => f.path)); - assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(file1.path).map(diag => diag.messageText), ["Cannot find module 'debug'."]); - assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(file2.path).map(diag => diag.messageText), ["Cannot find module 'debug'."]); - - const debugTypesFile: File = { - path: `${projectLocation}/node_modules/debug/index.d.ts`, - content: "export {}" - }; - files.push(debugTypesFile); - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - checkProjectActualFiles(project, files.map(f => f.path)); - assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(file1.path).map(diag => diag.messageText), []); - assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(file2.path).map(diag => diag.messageText), []); + function verifyProjectAndWatchedDirectories() { + checkProjectActualFiles(project, map(projectFiles, f => f.path)); + checkWatchedFiles(host, mapDefined(projectFiles, getFilePathIfNotOpen)); + checkWatchedDirectories(host, watchingRecursiveDirectories, /*recursive*/ true); + checkWatchedDirectories(host, [], /*recursive*/ false); } - - it("Includes the parent folder FLLs in node module resolution mode", () => { - runFailedLookupTest("Node"); + } + it("case insensitive file system", () => { + verifyWatchDirectoriesCaseSensitivity(/*useCaseSensitiveFileNames*/ false); + }); + it("case sensitive file system", () => { + verifyWatchDirectoriesCaseSensitivity(/*useCaseSensitiveFileNames*/ true); + }); + }); + describe("Subfolder invalidations correctly include parent folder failed lookup locations", () => { + function runFailedLookupTest(resolution: "Node" | "Classic") { + const projectLocation = "/proj"; + const file1: File = { + path: `${projectLocation}/foo/boo/app.ts`, + content: `import * as debug from "debug"` + }; + const file2: File = { + path: `${projectLocation}/foo/boo/moo/app.ts`, + content: `import * as debug from "debug"` + }; + const tsconfig: File = { + path: `${projectLocation}/tsconfig.json`, + content: JSON.stringify({ + files: ["foo/boo/app.ts", "foo/boo/moo/app.ts"], + moduleResolution: resolution + }) + }; + const files = [file1, file2, tsconfig, libFile]; + const host = createServerHost(files); + const service = createProjectService(host); + service.openClientFile(file1.path); + const project = service.configuredProjects.get(tsconfig.path)!; + checkProjectActualFiles(project, files.map(f => f.path)); + assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(file1.path).map(diag => diag.messageText), ["Cannot find module 'debug'."]); + assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(file2.path).map(diag => diag.messageText), ["Cannot find module 'debug'."]); + const debugTypesFile: File = { + path: `${projectLocation}/node_modules/debug/index.d.ts`, + content: "export {}" + }; + files.push(debugTypesFile); + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + checkProjectActualFiles(project, files.map(f => f.path)); + assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(file1.path).map(diag => diag.messageText), []); + assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(file2.path).map(diag => diag.messageText), []); + } + it("Includes the parent folder FLLs in node module resolution mode", () => { + runFailedLookupTest("Node"); + }); + it("Includes the parent folder FLLs in classic module resolution mode", () => { + runFailedLookupTest("Classic"); + }); + }); + describe("Verify npm install in directory with tsconfig file works when", () => { + function verifyNpmInstall(timeoutDuringPartialInstallation: boolean) { + const root = "/user/username/rootfolder/otherfolder"; + const getRootedFileOrFolder = (fileOrFolder: File) => { + fileOrFolder.path = root + fileOrFolder.path; + return fileOrFolder; + }; + const app: File = getRootedFileOrFolder({ + path: "/a/b/app.ts", + content: "import _ from 'lodash';" }); - it("Includes the parent folder FLLs in classic module resolution mode", () => { - runFailedLookupTest("Classic"); + const tsconfigJson: File = getRootedFileOrFolder({ + path: "/a/b/tsconfig.json", + content: '{ "compilerOptions": { } }' }); - }); - - describe("Verify npm install in directory with tsconfig file works when", () => { - function verifyNpmInstall(timeoutDuringPartialInstallation: boolean) { - const root = "/user/username/rootfolder/otherfolder"; - const getRootedFileOrFolder = (fileOrFolder: File) => { - fileOrFolder.path = root + fileOrFolder.path; - return fileOrFolder; - }; - const app: File = getRootedFileOrFolder({ - path: "/a/b/app.ts", - content: "import _ from 'lodash';" - }); - const tsconfigJson: File = getRootedFileOrFolder({ - path: "/a/b/tsconfig.json", - content: '{ "compilerOptions": { } }' - }); - const packageJson: File = getRootedFileOrFolder({ - path: "/a/b/package.json", - content: ` + const packageJson: File = getRootedFileOrFolder({ + path: "/a/b/package.json", + content: ` { "name": "test", "version": "1.0.0", @@ -536,170 +474,150 @@ namespace ts.projectSystem { "license": "ISC" } ` - }); - const appFolder = getDirectoryPath(app.path); - const projectFiles = [app, libFile, tsconfigJson]; - const typeRootDirectories = getTypeRootsFromLocation(getDirectoryPath(tsconfigJson.path)); - const otherFiles = [packageJson]; - const host = createServerHost(projectFiles.concat(otherFiles)); - const projectService = createProjectService(host); - const { configFileName } = projectService.openClientFile(app.path); - assert.equal(configFileName, tsconfigJson.path as server.NormalizedPath, `should find config`); // TODO: GH#18217 - const recursiveWatchedDirectories: string[] = [`${appFolder}`, `${appFolder}/node_modules`].concat(getNodeModuleDirectories(getDirectoryPath(appFolder))); - verifyProject(); - - let npmInstallComplete = false; - - // Simulate npm install - const filesAndFoldersToAdd: File[] = [ - { path: "/a/b/node_modules" }, - { path: "/a/b/node_modules/.staging/@types" }, - { path: "/a/b/node_modules/.staging/lodash-b0733faa" }, - { path: "/a/b/node_modules/.staging/@types/lodash-e56c4fe7" }, - { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff" }, - { path: "/a/b/node_modules/.staging/rxjs-22375c61" }, - { path: "/a/b/node_modules/.staging/typescript-8493ea5d" }, - { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff/package.json", content: "{\n \"name\": \"symbol-observable\",\n \"version\": \"1.0.4\",\n \"description\": \"Symbol.observable ponyfill\",\n \"license\": \"MIT\",\n \"repository\": \"blesh/symbol-observable\",\n \"author\": {\n \"name\": \"Ben Lesh\",\n \"email\": \"ben@benlesh.com\"\n },\n \"engines\": {\n \"node\": \">=0.10.0\"\n },\n \"scripts\": {\n \"test\": \"npm run build && mocha && tsc ./ts-test/test.ts && node ./ts-test/test.js && check-es3-syntax -p lib/ --kill\",\n \"build\": \"babel es --out-dir lib\",\n \"prepublish\": \"npm test\"\n },\n \"files\": [\n \"" }, - { path: "/a/b/node_modules/.staging/lodash-b0733faa/package.json", content: "{\n \"name\": \"lodash\",\n \"version\": \"4.17.4\",\n \"description\": \"Lodash modular utilities.\",\n \"keywords\": \"modules, stdlib, util\",\n \"homepage\": \"https://lodash.com/\",\n \"repository\": \"lodash/lodash\",\n \"icon\": \"https://lodash.com/icon.svg\",\n \"license\": \"MIT\",\n \"main\": \"lodash.js\",\n \"author\": \"John-David Dalton (http://allyoucanleet.com/)\",\n \"contributors\": [\n \"John-David Dalton (http://allyoucanleet.com/)\",\n \"Mathias Bynens \",\n \"contributors\": [\n {\n \"name\": \"Ben Lesh\",\n \"email\": \"ben@benlesh.com\"\n },\n {\n \"name\": \"Paul Taylor\",\n \"email\": \"paul.e.taylor@me.com\"\n },\n {\n \"name\": \"Jeff Cross\",\n \"email\": \"crossj@google.com\"\n },\n {\n \"name\": \"Matthew Podwysocki\",\n \"email\": \"matthewp@microsoft.com\"\n },\n {\n \"name\": \"OJ Kwon\",\n \"email\": \"kwon.ohjoong@gmail.com\"\n },\n {\n \"name\": \"Andre Staltz\",\n \"email\": \"andre@staltz.com\"\n }\n ],\n \"license\": \"Apache-2.0\",\n \"bugs\": {\n \"url\": \"https://github.com/ReactiveX/RxJS/issues\"\n },\n \"homepage\": \"https://github.com/ReactiveX/RxJS\",\n \"devDependencies\": {\n \"babel-polyfill\": \"^6.23.0\",\n \"benchmark\": \"^2.1.0\",\n \"benchpress\": \"2.0.0-beta.1\",\n \"chai\": \"^3.5.0\",\n \"color\": \"^0.11.1\",\n \"colors\": \"1.1.2\",\n \"commitizen\": \"^2.8.6\",\n \"coveralls\": \"^2.11.13\",\n \"cz-conventional-changelog\": \"^1.2.0\",\n \"danger\": \"^1.1.0\",\n \"doctoc\": \"^1.0.0\",\n \"escape-string-regexp\": \"^1.0.5 \",\n \"esdoc\": \"^0.4.7\",\n \"eslint\": \"^3.8.0\",\n \"fs-extra\": \"^2.1.2\",\n \"get-folder-size\": \"^1.0.0\",\n \"glob\": \"^7.0.3\",\n \"gm\": \"^1.22.0\",\n \"google-closure-compiler-js\": \"^20170218.0.0\",\n \"gzip-size\": \"^3.0.0\",\n \"http-server\": \"^0.9.0\",\n \"husky\": \"^0.13.3\",\n \"lint-staged\": \"3.2.5\",\n \"lodash\": \"^4.15.0\",\n \"madge\": \"^1.4.3\",\n \"markdown-doctest\": \"^0.9.1\",\n \"minimist\": \"^1.2.0\",\n \"mkdirp\": \"^0.5.1\",\n \"mocha\": \"^3.0.2\",\n \"mocha-in-sauce\": \"0.0.1\",\n \"npm-run-all\": \"^4.0.2\",\n \"npm-scripts-info\": \"^0.3.4\",\n \"nyc\": \"^10.2.0\",\n \"opn-cli\": \"^3.1.0\",\n \"platform\": \"^1.3.1\",\n \"promise\": \"^7.1.1\",\n \"protractor\": \"^3.1.1\",\n \"rollup\": \"0.36.3\",\n \"rollup-plugin-inject\": \"^2.0.0\",\n \"rollup-plugin-node-resolve\": \"^2.0.0\",\n \"rx\": \"latest\",\n \"rxjs\": \"latest\",\n \"shx\": \"^0.2.2\",\n \"sinon\": \"^2.1.0\",\n \"sinon-chai\": \"^2.9.0\",\n \"source-map-support\": \"^0.4.0\",\n \"tslib\": \"^1.5.0\",\n \"eslint\": \"^4.4.2\",\n \"typescript\": \"~2.0.6\",\n \"typings\": \"^2.0.0\",\n \"validate-commit-msg\": \"^2.14.0\",\n \"watch\": \"^1.0.1\",\n \"webpack\": \"^1.13.1\",\n \"xmlhttprequest\": \"1.8.0\"\n },\n \"engines\": {\n \"npm\": \">=2.0.0\"\n },\n \"typings\": \"Rx.d.ts\",\n \"dependencies\": {\n \"symbol-observable\": \"^1.0.1\"\n }\n}" }, - { path: "/a/b/node_modules/.staging/typescript-8493ea5d/package.json", content: "{\n \"name\": \"typescript\",\n \"author\": \"Microsoft Corp.\",\n \"homepage\": \"http://typescriptlang.org/\",\n \"version\": \"2.4.2\",\n \"license\": \"Apache-2.0\",\n \"description\": \"TypeScript is a language for application scale JavaScript development\",\n \"keywords\": [\n \"TypeScript\",\n \"Microsoft\",\n \"compiler\",\n \"language\",\n \"javascript\"\n ],\n \"bugs\": {\n \"url\": \"https://github.com/Microsoft/TypeScript/issues\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/Microsoft/TypeScript.git\"\n },\n \"main\": \"./lib/typescript.js\",\n \"typings\": \"./lib/typescript.d.ts\",\n \"bin\": {\n \"tsc\": \"./bin/tsc\",\n \"tsserver\": \"./bin/tsserver\"\n },\n \"engines\": {\n \"node\": \">=4.2.0\"\n },\n \"devDependencies\": {\n \"@types/browserify\": \"latest\",\n \"@types/chai\": \"latest\",\n \"@types/convert-source-map\": \"latest\",\n \"@types/del\": \"latest\",\n \"@types/glob\": \"latest\",\n \"@types/gulp\": \"latest\",\n \"@types/gulp-concat\": \"latest\",\n \"@types/gulp-help\": \"latest\",\n \"@types/gulp-newer\": \"latest\",\n \"@types/gulp-sourcemaps\": \"latest\",\n \"@types/merge2\": \"latest\",\n \"@types/minimatch\": \"latest\",\n \"@types/minimist\": \"latest\",\n \"@types/mkdirp\": \"latest\",\n \"@types/mocha\": \"latest\",\n \"@types/node\": \"latest\",\n \"@types/q\": \"latest\",\n \"@types/run-sequence\": \"latest\",\n \"@types/through2\": \"latest\",\n \"browserify\": \"latest\",\n \"chai\": \"latest\",\n \"convert-source-map\": \"latest\",\n \"del\": \"latest\",\n \"gulp\": \"latest\",\n \"gulp-clone\": \"latest\",\n \"gulp-concat\": \"latest\",\n \"gulp-help\": \"latest\",\n \"gulp-insert\": \"latest\",\n \"gulp-newer\": \"latest\",\n \"gulp-sourcemaps\": \"latest\",\n \"gulp-typescript\": \"latest\",\n \"into-stream\": \"latest\",\n \"istanbul\": \"latest\",\n \"jake\": \"latest\",\n \"merge2\": \"latest\",\n \"minimist\": \"latest\",\n \"mkdirp\": \"latest\",\n \"mocha\": \"latest\",\n \"mocha-fivemat-progress-reporter\": \"latest\",\n \"q\": \"latest\",\n \"run-sequence\": \"latest\",\n \"sorcery\": \"latest\",\n \"through2\": \"latest\",\n \"travis-fold\": \"latest\",\n \"ts-node\": \"latest\",\n \"eslint\": \"5.16.0\",\n \"typescript\": \"^2.4\"\n },\n \"scripts\": {\n \"pretest\": \"jake tests\",\n \"test\": \"jake runtests-parallel\",\n \"build\": \"npm run build:compiler && npm run build:tests\",\n \"build:compiler\": \"jake local\",\n \"build:tests\": \"jake tests\",\n \"start\": \"node lib/tsc\",\n \"clean\": \"jake clean\",\n \"gulp\": \"gulp\",\n \"jake\": \"jake\",\n \"lint\": \"jake lint\",\n \"setup-hooks\": \"node scripts/link-hooks.js\"\n },\n \"browser\": {\n \"buffer\": false,\n \"fs\": false,\n \"os\": false,\n \"path\": false\n }\n}" }, - { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff/index.js", content: "module.exports = require('./lib/index');\n" }, - { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff/index.d.ts", content: "declare const observableSymbol: symbol;\nexport default observableSymbol;\n" }, - { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff/lib" }, - { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff/lib/index.js", content: "'use strict';\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n\nvar _ponyfill = require('./ponyfill');\n\nvar _ponyfill2 = _interopRequireDefault(_ponyfill);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }\n\nvar root; /* global window */\n\n\nif (typeof self !== 'undefined') {\n root = self;\n} else if (typeof window !== 'undefined') {\n root = window;\n} else if (typeof global !== 'undefined') {\n root = global;\n} else if (typeof module !== 'undefined') {\n root = module;\n} else {\n root = Function('return this')();\n}\n\nvar result = (0, _ponyfill2['default'])(root);\nexports['default'] = result;" }, - ].map(getRootedFileOrFolder); - verifyAfterPartialOrCompleteNpmInstall(2); - - filesAndFoldersToAdd.push(...[ - { path: "/a/b/node_modules/.staging/typescript-8493ea5d/lib" }, - { path: "/a/b/node_modules/.staging/rxjs-22375c61/add/operator" }, - { path: "/a/b/node_modules/.staging/@types/lodash-e56c4fe7/package.json", content: "{\n \"name\": \"@types/lodash\",\n \"version\": \"4.14.74\",\n \"description\": \"TypeScript definitions for Lo-Dash\",\n \"license\": \"MIT\",\n \"contributors\": [\n {\n \"name\": \"Brian Zengel\",\n \"url\": \"https://github.com/bczengel\"\n },\n {\n \"name\": \"Ilya Mochalov\",\n \"url\": \"https://github.com/chrootsu\"\n },\n {\n \"name\": \"Stepan Mikhaylyuk\",\n \"url\": \"https://github.com/stepancar\"\n },\n {\n \"name\": \"Eric L Anderson\",\n \"url\": \"https://github.com/ericanderson\"\n },\n {\n \"name\": \"AJ Richardson\",\n \"url\": \"https://github.com/aj-r\"\n },\n {\n \"name\": \"Junyoung Clare Jang\",\n \"url\": \"https://github.com/ailrun\"\n }\n ],\n \"main\": \"\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://www.github.com/DefinitelyTyped/DefinitelyTyped.git\"\n },\n \"scripts\": {},\n \"dependencies\": {},\n \"typesPublisherContentHash\": \"12af578ffaf8d86d2df37e591857906a86b983fa9258414326544a0fe6af0de8\",\n \"typeScriptVersion\": \"2.2\"\n}" }, - { path: "/a/b/node_modules/.staging/lodash-b0733faa/index.js", content: "module.exports = require('./lodash');" }, - { path: "/a/b/node_modules/.staging/typescript-8493ea5d/package.json.3017591594" } - ].map(getRootedFileOrFolder)); - // Since we added/removed in .staging no timeout - verifyAfterPartialOrCompleteNpmInstall(0); - - // Remove file "/a/b/node_modules/.staging/typescript-8493ea5d/package.json.3017591594" - filesAndFoldersToAdd.length--; - verifyAfterPartialOrCompleteNpmInstall(0); - - filesAndFoldersToAdd.push(...[ - { path: "/a/b/node_modules/.staging/rxjs-22375c61/bundles" }, - { path: "/a/b/node_modules/.staging/rxjs-22375c61/operator" }, - { path: "/a/b/node_modules/.staging/rxjs-22375c61/src/add/observable/dom" }, - { path: "/a/b/node_modules/.staging/@types/lodash-e56c4fe7/index.d.ts", content: "\n// Stub for lodash\nexport = _;\nexport as namespace _;\ndeclare var _: _.LoDashStatic;\ndeclare namespace _ {\n interface LoDashStatic {\n someProp: string;\n }\n class SomeClass {\n someMethod(): void;\n }\n}" } - ].map(getRootedFileOrFolder)); - verifyAfterPartialOrCompleteNpmInstall(0); - - filesAndFoldersToAdd.push(...[ - { path: "/a/b/node_modules/.staging/rxjs-22375c61/src/scheduler" }, - { path: "/a/b/node_modules/.staging/rxjs-22375c61/src/util" }, - { path: "/a/b/node_modules/.staging/rxjs-22375c61/symbol" }, - { path: "/a/b/node_modules/.staging/rxjs-22375c61/testing" }, - { path: "/a/b/node_modules/.staging/rxjs-22375c61/package.json.2252192041", content: "{\n \"_args\": [\n [\n {\n \"raw\": \"rxjs@^5.4.2\",\n \"scope\": null,\n \"escapedName\": \"rxjs\",\n \"name\": \"rxjs\",\n \"rawSpec\": \"^5.4.2\",\n \"spec\": \">=5.4.2 <6.0.0\",\n \"type\": \"range\"\n },\n \"C:\\\\Users\\\\shkamat\\\\Desktop\\\\app\"\n ]\n ],\n \"_from\": \"rxjs@>=5.4.2 <6.0.0\",\n \"_id\": \"rxjs@5.4.3\",\n \"_inCache\": true,\n \"_location\": \"/rxjs\",\n \"_nodeVersion\": \"7.7.2\",\n \"_npmOperationalInternal\": {\n \"host\": \"s3://npm-registry-packages\",\n \"tmp\": \"tmp/rxjs-5.4.3.tgz_1502407898166_0.6800217325799167\"\n },\n \"_npmUser\": {\n \"name\": \"blesh\",\n \"email\": \"ben@benlesh.com\"\n },\n \"_npmVersion\": \"5.3.0\",\n \"_phantomChildren\": {},\n \"_requested\": {\n \"raw\": \"rxjs@^5.4.2\",\n \"scope\": null,\n \"escapedName\": \"rxjs\",\n \"name\": \"rxjs\",\n \"rawSpec\": \"^5.4.2\",\n \"spec\": \">=5.4.2 <6.0.0\",\n \"type\": \"range\"\n },\n \"_requiredBy\": [\n \"/\"\n ],\n \"_resolved\": \"https://registry.npmjs.org/rxjs/-/rxjs-5.4.3.tgz\",\n \"_shasum\": \"0758cddee6033d68e0fd53676f0f3596ce3d483f\",\n \"_shrinkwrap\": null,\n \"_spec\": \"rxjs@^5.4.2\",\n \"_where\": \"C:\\\\Users\\\\shkamat\\\\Desktop\\\\app\",\n \"author\": {\n \"name\": \"Ben Lesh\",\n \"email\": \"ben@benlesh.com\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/ReactiveX/RxJS/issues\"\n },\n \"config\": {\n \"commitizen\": {\n \"path\": \"cz-conventional-changelog\"\n }\n },\n \"contributors\": [\n {\n \"name\": \"Ben Lesh\",\n \"email\": \"ben@benlesh.com\"\n },\n {\n \"name\": \"Paul Taylor\",\n \"email\": \"paul.e.taylor@me.com\"\n },\n {\n \"name\": \"Jeff Cross\",\n \"email\": \"crossj@google.com\"\n },\n {\n \"name\": \"Matthew Podwysocki\",\n \"email\": \"matthewp@microsoft.com\"\n },\n {\n \"name\": \"OJ Kwon\",\n \"email\": \"kwon.ohjoong@gmail.com\"\n },\n {\n \"name\": \"Andre Staltz\",\n \"email\": \"andre@staltz.com\"\n }\n ],\n \"dependencies\": {\n \"symbol-observable\": \"^1.0.1\"\n },\n \"description\": \"Reactive Extensions for modern JavaScript\",\n \"devDependencies\": {\n \"babel-polyfill\": \"^6.23.0\",\n \"benchmark\": \"^2.1.0\",\n \"benchpress\": \"2.0.0-beta.1\",\n \"chai\": \"^3.5.0\",\n \"color\": \"^0.11.1\",\n \"colors\": \"1.1.2\",\n \"commitizen\": \"^2.8.6\",\n \"coveralls\": \"^2.11.13\",\n \"cz-conventional-changelog\": \"^1.2.0\",\n \"danger\": \"^1.1.0\",\n \"doctoc\": \"^1.0.0\",\n \"escape-string-regexp\": \"^1.0.5 \",\n \"esdoc\": \"^0.4.7\",\n \"eslint\": \"^3.8.0\",\n \"fs-extra\": \"^2.1.2\",\n \"get-folder-size\": \"^1.0.0\",\n \"glob\": \"^7.0.3\",\n \"gm\": \"^1.22.0\",\n \"google-closure-compiler-js\": \"^20170218.0.0\",\n \"gzip-size\": \"^3.0.0\",\n \"http-server\": \"^0.9.0\",\n \"husky\": \"^0.13.3\",\n \"lint-staged\": \"3.2.5\",\n \"lodash\": \"^4.15.0\",\n \"madge\": \"^1.4.3\",\n \"markdown-doctest\": \"^0.9.1\",\n \"minimist\": \"^1.2.0\",\n \"mkdirp\": \"^0.5.1\",\n \"mocha\": \"^3.0.2\",\n \"mocha-in-sauce\": \"0.0.1\",\n \"npm-run-all\": \"^4.0.2\",\n \"npm-scripts-info\": \"^0.3.4\",\n \"nyc\": \"^10.2.0\",\n \"opn-cli\": \"^3.1.0\",\n \"platform\": \"^1.3.1\",\n \"promise\": \"^7.1.1\",\n \"protractor\": \"^3.1.1\",\n \"rollup\": \"0.36.3\",\n \"rollup-plugin-inject\": \"^2.0.0\",\n \"rollup-plugin-node-resolve\": \"^2.0.0\",\n \"rx\": \"latest\",\n \"rxjs\": \"latest\",\n \"shx\": \"^0.2.2\",\n \"sinon\": \"^2.1.0\",\n \"sinon-chai\": \"^2.9.0\",\n \"source-map-support\": \"^0.4.0\",\n \"tslib\": \"^1.5.0\",\n \"eslint\": \"^5.16.0\",\n \"typescript\": \"~2.0.6\",\n \"typings\": \"^2.0.0\",\n \"validate-commit-msg\": \"^2.14.0\",\n \"watch\": \"^1.0.1\",\n \"webpack\": \"^1.13.1\",\n \"xmlhttprequest\": \"1.8.0\"\n },\n \"directories\": {},\n \"dist\": {\n \"integrity\": \"sha512-fSNi+y+P9ss+EZuV0GcIIqPUK07DEaMRUtLJvdcvMyFjc9dizuDjere+A4V7JrLGnm9iCc+nagV/4QdMTkqC4A==\",\n \"shasum\": \"0758cddee6033d68e0fd53676f0f3596ce3d483f\",\n \"tarball\": \"https://registry.npmjs.org/rxjs/-/rxjs-5.4.3.tgz\"\n },\n \"engines\": {\n \"npm\": \">=2.0.0\"\n },\n \"homepage\": \"https://github.com/ReactiveX/RxJS\",\n \"keywords\": [\n \"Rx\",\n \"RxJS\",\n \"ReactiveX\",\n \"ReactiveExtensions\",\n \"Streams\",\n \"Observables\",\n \"Observable\",\n \"Stream\",\n \"ES6\",\n \"ES2015\"\n ],\n \"license\": \"Apache-2.0\",\n \"lint-staged\": {\n \"*.@(js)\": [\n \"eslint --fix\",\n \"git add\"\n ],\n \"*.@(ts)\": [\n \"eslint -c .eslintrc --ext .ts . --fix\",\n \"git add\"\n ]\n },\n \"main\": \"Rx.js\",\n \"maintainers\": [\n {\n \"name\": \"blesh\",\n \"email\": \"ben@benlesh.com\"\n }\n ],\n \"name\": \"rxjs\",\n \"optionalDependencies\": {},\n \"readme\": \"ERROR: No README data found!\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+ssh://git@github.com/ReactiveX/RxJS.git\"\n },\n \"scripts-info\": {\n \"info\": \"List available script\",\n \"build_all\": \"Build all packages (ES6, CJS, UMD) and generate packages\",\n \"build_cjs\": \"Build CJS package with clean up existing build, copy source into dist\",\n \"build_es6\": \"Build ES6 package with clean up existing build, copy source into dist\",\n \"build_closure_core\": \"Minify Global core build using closure compiler\",\n \"build_global\": \"Build Global package, then minify build\",\n \"build_perf\": \"Build CJS & Global build, run macro performance test\",\n \"build_test\": \"Build CJS package & test spec, execute mocha test runner\",\n \"build_cover\": \"Run lint to current code, build CJS & test spec, execute test coverage\",\n \"build_docs\": \"Build ES6 & global package, create documentation using it\",\n \"build_spec\": \"Build test specs\",\n \"check_circular_dependencies\": \"Check codebase has circular dependencies\",\n \"clean_spec\": \"Clean up existing test spec build output\",\n \"clean_dist_cjs\": \"Clean up existing CJS package output\",\n \"clean_dist_es6\": \"Clean up existing ES6 package output\",\n \"clean_dist_global\": \"Clean up existing Global package output\",\n \"commit\": \"Run git commit wizard\",\n \"compile_dist_cjs\": \"Compile codebase into CJS module\",\n \"compile_module_es6\": \"Compile codebase into ES6\",\n \"cover\": \"Execute test coverage\",\n \"lint_perf\": \"Run lint against performance test suite\",\n \"lint_spec\": \"Run lint against test spec\",\n \"lint_src\": \"Run lint against source\",\n \"lint\": \"Run lint against everything\",\n \"perf\": \"Run macro performance benchmark\",\n \"perf_micro\": \"Run micro performance benchmark\",\n \"test_mocha\": \"Execute mocha test runner against existing test spec build\",\n \"test_browser\": \"Execute mocha test runner on browser against existing test spec build\",\n \"test\": \"Clean up existing test spec build, build test spec and execute mocha test runner\",\n \"tests2png\": \"Generate marble diagram image from test spec\",\n \"watch\": \"Watch codebase, trigger compile when source code changes\"\n },\n \"typings\": \"Rx.d.ts\",\n \"version\": \"5.4.3\"\n}\n" } - ].map(getRootedFileOrFolder)); - verifyAfterPartialOrCompleteNpmInstall(0); - - // remove /a/b/node_modules/.staging/rxjs-22375c61/package.json.2252192041 - filesAndFoldersToAdd.length--; - // and add few more folders/files - filesAndFoldersToAdd.push(...[ - { path: "/a/b/node_modules/symbol-observable" }, - { path: "/a/b/node_modules/@types" }, - { path: "/a/b/node_modules/@types/lodash" }, - { path: "/a/b/node_modules/lodash" }, - { path: "/a/b/node_modules/rxjs" }, - { path: "/a/b/node_modules/typescript" }, - { path: "/a/b/node_modules/.bin" } - ].map(getRootedFileOrFolder)); - // From the type root update - verifyAfterPartialOrCompleteNpmInstall(2); - - forEach(filesAndFoldersToAdd, f => { - f.path = f.path - .replace("/a/b/node_modules/.staging", "/a/b/node_modules") - .replace(/[\-\.][\d\w][\d\w][\d\w][\d\w][\d\w][\d\w][\d\w][\d\w]/g, ""); - }); - - const lodashIndexPath = root + "/a/b/node_modules/@types/lodash/index.d.ts"; - projectFiles.push(find(filesAndFoldersToAdd, f => f.path === lodashIndexPath)!); - // we would now not have failed lookup in the parent of appFolder since lodash is available - recursiveWatchedDirectories.length = 2; - // npm installation complete, timeout after reload fs - npmInstallComplete = true; - verifyAfterPartialOrCompleteNpmInstall(2); - - function verifyAfterPartialOrCompleteNpmInstall(timeoutQueueLengthWhenRunningTimeouts: number) { - host.reloadFS(projectFiles.concat(otherFiles, filesAndFoldersToAdd)); - if (npmInstallComplete || timeoutDuringPartialInstallation) { - host.checkTimeoutQueueLengthAndRun(timeoutQueueLengthWhenRunningTimeouts); - } - else { - host.checkTimeoutQueueLength(2); - } - verifyProject(); + }); + const appFolder = getDirectoryPath(app.path); + const projectFiles = [app, libFile, tsconfigJson]; + const typeRootDirectories = getTypeRootsFromLocation(getDirectoryPath(tsconfigJson.path)); + const otherFiles = [packageJson]; + const host = createServerHost(projectFiles.concat(otherFiles)); + const projectService = createProjectService(host); + const { configFileName } = projectService.openClientFile(app.path); + assert.equal(configFileName, (tsconfigJson.path as NormalizedPath), `should find config`); // TODO: GH#18217 + const recursiveWatchedDirectories: string[] = [`${appFolder}`, `${appFolder}/node_modules`].concat(getNodeModuleDirectories(getDirectoryPath(appFolder))); + verifyProject(); + let npmInstallComplete = false; + // Simulate npm install + const filesAndFoldersToAdd: File[] = [ + { path: "/a/b/node_modules" }, + { path: "/a/b/node_modules/.staging/@types" }, + { path: "/a/b/node_modules/.staging/lodash-b0733faa" }, + { path: "/a/b/node_modules/.staging/@types/lodash-e56c4fe7" }, + { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff" }, + { path: "/a/b/node_modules/.staging/rxjs-22375c61" }, + { path: "/a/b/node_modules/.staging/typescript-8493ea5d" }, + { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff/package.json", content: "{\n \"name\": \"symbol-observable\",\n \"version\": \"1.0.4\",\n \"description\": \"Symbol.observable ponyfill\",\n \"license\": \"MIT\",\n \"repository\": \"blesh/symbol-observable\",\n \"author\": {\n \"name\": \"Ben Lesh\",\n \"email\": \"ben@benlesh.com\"\n },\n \"engines\": {\n \"node\": \">=0.10.0\"\n },\n \"scripts\": {\n \"test\": \"npm run build && mocha && tsc ./ts-test/test.ts && node ./ts-test/test.js && check-es3-syntax -p lib/ --kill\",\n \"build\": \"babel es --out-dir lib\",\n \"prepublish\": \"npm test\"\n },\n \"files\": [\n \"" }, + { path: "/a/b/node_modules/.staging/lodash-b0733faa/package.json", content: "{\n \"name\": \"lodash\",\n \"version\": \"4.17.4\",\n \"description\": \"Lodash modular utilities.\",\n \"keywords\": \"modules, stdlib, util\",\n \"homepage\": \"https://lodash.com/\",\n \"repository\": \"lodash/lodash\",\n \"icon\": \"https://lodash.com/icon.svg\",\n \"license\": \"MIT\",\n \"main\": \"lodash.js\",\n \"author\": \"John-David Dalton (http://allyoucanleet.com/)\",\n \"contributors\": [\n \"John-David Dalton (http://allyoucanleet.com/)\",\n \"Mathias Bynens \",\n \"contributors\": [\n {\n \"name\": \"Ben Lesh\",\n \"email\": \"ben@benlesh.com\"\n },\n {\n \"name\": \"Paul Taylor\",\n \"email\": \"paul.e.taylor@me.com\"\n },\n {\n \"name\": \"Jeff Cross\",\n \"email\": \"crossj@google.com\"\n },\n {\n \"name\": \"Matthew Podwysocki\",\n \"email\": \"matthewp@microsoft.com\"\n },\n {\n \"name\": \"OJ Kwon\",\n \"email\": \"kwon.ohjoong@gmail.com\"\n },\n {\n \"name\": \"Andre Staltz\",\n \"email\": \"andre@staltz.com\"\n }\n ],\n \"license\": \"Apache-2.0\",\n \"bugs\": {\n \"url\": \"https://github.com/ReactiveX/RxJS/issues\"\n },\n \"homepage\": \"https://github.com/ReactiveX/RxJS\",\n \"devDependencies\": {\n \"babel-polyfill\": \"^6.23.0\",\n \"benchmark\": \"^2.1.0\",\n \"benchpress\": \"2.0.0-beta.1\",\n \"chai\": \"^3.5.0\",\n \"color\": \"^0.11.1\",\n \"colors\": \"1.1.2\",\n \"commitizen\": \"^2.8.6\",\n \"coveralls\": \"^2.11.13\",\n \"cz-conventional-changelog\": \"^1.2.0\",\n \"danger\": \"^1.1.0\",\n \"doctoc\": \"^1.0.0\",\n \"escape-string-regexp\": \"^1.0.5 \",\n \"esdoc\": \"^0.4.7\",\n \"eslint\": \"^3.8.0\",\n \"fs-extra\": \"^2.1.2\",\n \"get-folder-size\": \"^1.0.0\",\n \"glob\": \"^7.0.3\",\n \"gm\": \"^1.22.0\",\n \"google-closure-compiler-js\": \"^20170218.0.0\",\n \"gzip-size\": \"^3.0.0\",\n \"http-server\": \"^0.9.0\",\n \"husky\": \"^0.13.3\",\n \"lint-staged\": \"3.2.5\",\n \"lodash\": \"^4.15.0\",\n \"madge\": \"^1.4.3\",\n \"markdown-doctest\": \"^0.9.1\",\n \"minimist\": \"^1.2.0\",\n \"mkdirp\": \"^0.5.1\",\n \"mocha\": \"^3.0.2\",\n \"mocha-in-sauce\": \"0.0.1\",\n \"npm-run-all\": \"^4.0.2\",\n \"npm-scripts-info\": \"^0.3.4\",\n \"nyc\": \"^10.2.0\",\n \"opn-cli\": \"^3.1.0\",\n \"platform\": \"^1.3.1\",\n \"promise\": \"^7.1.1\",\n \"protractor\": \"^3.1.1\",\n \"rollup\": \"0.36.3\",\n \"rollup-plugin-inject\": \"^2.0.0\",\n \"rollup-plugin-node-resolve\": \"^2.0.0\",\n \"rx\": \"latest\",\n \"rxjs\": \"latest\",\n \"shx\": \"^0.2.2\",\n \"sinon\": \"^2.1.0\",\n \"sinon-chai\": \"^2.9.0\",\n \"source-map-support\": \"^0.4.0\",\n \"tslib\": \"^1.5.0\",\n \"eslint\": \"^4.4.2\",\n \"typescript\": \"~2.0.6\",\n \"typings\": \"^2.0.0\",\n \"validate-commit-msg\": \"^2.14.0\",\n \"watch\": \"^1.0.1\",\n \"webpack\": \"^1.13.1\",\n \"xmlhttprequest\": \"1.8.0\"\n },\n \"engines\": {\n \"npm\": \">=2.0.0\"\n },\n \"typings\": \"Rx.d.ts\",\n \"dependencies\": {\n \"symbol-observable\": \"^1.0.1\"\n }\n}" }, + { path: "/a/b/node_modules/.staging/typescript-8493ea5d/package.json", content: "{\n \"name\": \"typescript\",\n \"author\": \"Microsoft Corp.\",\n \"homepage\": \"http://typescriptlang.org/\",\n \"version\": \"2.4.2\",\n \"license\": \"Apache-2.0\",\n \"description\": \"TypeScript is a language for application scale JavaScript development\",\n \"keywords\": [\n \"TypeScript\",\n \"Microsoft\",\n \"compiler\",\n \"language\",\n \"javascript\"\n ],\n \"bugs\": {\n \"url\": \"https://github.com/Microsoft/TypeScript/issues\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/Microsoft/TypeScript.git\"\n },\n \"main\": \"./lib/typescript.js\",\n \"typings\": \"./lib/typescript.d.ts\",\n \"bin\": {\n \"tsc\": \"./bin/tsc\",\n \"tsserver\": \"./bin/tsserver\"\n },\n \"engines\": {\n \"node\": \">=4.2.0\"\n },\n \"devDependencies\": {\n \"@types/browserify\": \"latest\",\n \"@types/chai\": \"latest\",\n \"@types/convert-source-map\": \"latest\",\n \"@types/del\": \"latest\",\n \"@types/glob\": \"latest\",\n \"@types/gulp\": \"latest\",\n \"@types/gulp-concat\": \"latest\",\n \"@types/gulp-help\": \"latest\",\n \"@types/gulp-newer\": \"latest\",\n \"@types/gulp-sourcemaps\": \"latest\",\n \"@types/merge2\": \"latest\",\n \"@types/minimatch\": \"latest\",\n \"@types/minimist\": \"latest\",\n \"@types/mkdirp\": \"latest\",\n \"@types/mocha\": \"latest\",\n \"@types/node\": \"latest\",\n \"@types/q\": \"latest\",\n \"@types/run-sequence\": \"latest\",\n \"@types/through2\": \"latest\",\n \"browserify\": \"latest\",\n \"chai\": \"latest\",\n \"convert-source-map\": \"latest\",\n \"del\": \"latest\",\n \"gulp\": \"latest\",\n \"gulp-clone\": \"latest\",\n \"gulp-concat\": \"latest\",\n \"gulp-help\": \"latest\",\n \"gulp-insert\": \"latest\",\n \"gulp-newer\": \"latest\",\n \"gulp-sourcemaps\": \"latest\",\n \"gulp-typescript\": \"latest\",\n \"into-stream\": \"latest\",\n \"istanbul\": \"latest\",\n \"jake\": \"latest\",\n \"merge2\": \"latest\",\n \"minimist\": \"latest\",\n \"mkdirp\": \"latest\",\n \"mocha\": \"latest\",\n \"mocha-fivemat-progress-reporter\": \"latest\",\n \"q\": \"latest\",\n \"run-sequence\": \"latest\",\n \"sorcery\": \"latest\",\n \"through2\": \"latest\",\n \"travis-fold\": \"latest\",\n \"ts-node\": \"latest\",\n \"eslint\": \"5.16.0\",\n \"typescript\": \"^2.4\"\n },\n \"scripts\": {\n \"pretest\": \"jake tests\",\n \"test\": \"jake runtests-parallel\",\n \"build\": \"npm run build:compiler && npm run build:tests\",\n \"build:compiler\": \"jake local\",\n \"build:tests\": \"jake tests\",\n \"start\": \"node lib/tsc\",\n \"clean\": \"jake clean\",\n \"gulp\": \"gulp\",\n \"jake\": \"jake\",\n \"lint\": \"jake lint\",\n \"setup-hooks\": \"node scripts/link-hooks.js\"\n },\n \"browser\": {\n \"buffer\": false,\n \"fs\": false,\n \"os\": false,\n \"path\": false\n }\n}" }, + { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff/index.js", content: "module.exports = require('./lib/index');\n" }, + { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff/index.d.ts", content: "declare const observableSymbol: symbol;\nexport default observableSymbol;\n" }, + { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff/lib" }, + { path: "/a/b/node_modules/.staging/symbol-observable-24bcbbff/lib/index.js", content: "'use strict';\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n\nvar _ponyfill = require('./ponyfill');\n\nvar _ponyfill2 = _interopRequireDefault(_ponyfill);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }\n\nvar root; /* global window */\n\n\nif (typeof self !== 'undefined') {\n root = self;\n} else if (typeof window !== 'undefined') {\n root = window;\n} else if (typeof global !== 'undefined') {\n root = global;\n} else if (typeof module !== 'undefined') {\n root = module;\n} else {\n root = Function('return this')();\n}\n\nvar result = (0, _ponyfill2['default'])(root);\nexports['default'] = result;" }, + ].map(getRootedFileOrFolder); + verifyAfterPartialOrCompleteNpmInstall(2); + filesAndFoldersToAdd.push(...[ + { path: "/a/b/node_modules/.staging/typescript-8493ea5d/lib" }, + { path: "/a/b/node_modules/.staging/rxjs-22375c61/add/operator" }, + { path: "/a/b/node_modules/.staging/@types/lodash-e56c4fe7/package.json", content: "{\n \"name\": \"@types/lodash\",\n \"version\": \"4.14.74\",\n \"description\": \"TypeScript definitions for Lo-Dash\",\n \"license\": \"MIT\",\n \"contributors\": [\n {\n \"name\": \"Brian Zengel\",\n \"url\": \"https://github.com/bczengel\"\n },\n {\n \"name\": \"Ilya Mochalov\",\n \"url\": \"https://github.com/chrootsu\"\n },\n {\n \"name\": \"Stepan Mikhaylyuk\",\n \"url\": \"https://github.com/stepancar\"\n },\n {\n \"name\": \"Eric L Anderson\",\n \"url\": \"https://github.com/ericanderson\"\n },\n {\n \"name\": \"AJ Richardson\",\n \"url\": \"https://github.com/aj-r\"\n },\n {\n \"name\": \"Junyoung Clare Jang\",\n \"url\": \"https://github.com/ailrun\"\n }\n ],\n \"main\": \"\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://www.github.com/DefinitelyTyped/DefinitelyTyped.git\"\n },\n \"scripts\": {},\n \"dependencies\": {},\n \"typesPublisherContentHash\": \"12af578ffaf8d86d2df37e591857906a86b983fa9258414326544a0fe6af0de8\",\n \"typeScriptVersion\": \"2.2\"\n}" }, + { path: "/a/b/node_modules/.staging/lodash-b0733faa/index.js", content: "module.exports = require('./lodash');" }, + { path: "/a/b/node_modules/.staging/typescript-8493ea5d/package.json.3017591594" } + ].map(getRootedFileOrFolder)); + // Since we added/removed in .staging no timeout + verifyAfterPartialOrCompleteNpmInstall(0); + // Remove file "/a/b/node_modules/.staging/typescript-8493ea5d/package.json.3017591594" + filesAndFoldersToAdd.length--; + verifyAfterPartialOrCompleteNpmInstall(0); + filesAndFoldersToAdd.push(...[ + { path: "/a/b/node_modules/.staging/rxjs-22375c61/bundles" }, + { path: "/a/b/node_modules/.staging/rxjs-22375c61/operator" }, + { path: "/a/b/node_modules/.staging/rxjs-22375c61/src/add/observable/dom" }, + { path: "/a/b/node_modules/.staging/@types/lodash-e56c4fe7/index.d.ts", content: "\n// Stub for lodash\nexport = _;\nexport as namespace _;\ndeclare var _: _.LoDashStatic;\ndeclare namespace _ {\n interface LoDashStatic {\n someProp: string;\n }\n class SomeClass {\n someMethod(): void;\n }\n}" } + ].map(getRootedFileOrFolder)); + verifyAfterPartialOrCompleteNpmInstall(0); + filesAndFoldersToAdd.push(...[ + { path: "/a/b/node_modules/.staging/rxjs-22375c61/src/scheduler" }, + { path: "/a/b/node_modules/.staging/rxjs-22375c61/src/util" }, + { path: "/a/b/node_modules/.staging/rxjs-22375c61/symbol" }, + { path: "/a/b/node_modules/.staging/rxjs-22375c61/testing" }, + { path: "/a/b/node_modules/.staging/rxjs-22375c61/package.json.2252192041", content: "{\n \"_args\": [\n [\n {\n \"raw\": \"rxjs@^5.4.2\",\n \"scope\": null,\n \"escapedName\": \"rxjs\",\n \"name\": \"rxjs\",\n \"rawSpec\": \"^5.4.2\",\n \"spec\": \">=5.4.2 <6.0.0\",\n \"type\": \"range\"\n },\n \"C:\\\\Users\\\\shkamat\\\\Desktop\\\\app\"\n ]\n ],\n \"_from\": \"rxjs@>=5.4.2 <6.0.0\",\n \"_id\": \"rxjs@5.4.3\",\n \"_inCache\": true,\n \"_location\": \"/rxjs\",\n \"_nodeVersion\": \"7.7.2\",\n \"_npmOperationalInternal\": {\n \"host\": \"s3://npm-registry-packages\",\n \"tmp\": \"tmp/rxjs-5.4.3.tgz_1502407898166_0.6800217325799167\"\n },\n \"_npmUser\": {\n \"name\": \"blesh\",\n \"email\": \"ben@benlesh.com\"\n },\n \"_npmVersion\": \"5.3.0\",\n \"_phantomChildren\": {},\n \"_requested\": {\n \"raw\": \"rxjs@^5.4.2\",\n \"scope\": null,\n \"escapedName\": \"rxjs\",\n \"name\": \"rxjs\",\n \"rawSpec\": \"^5.4.2\",\n \"spec\": \">=5.4.2 <6.0.0\",\n \"type\": \"range\"\n },\n \"_requiredBy\": [\n \"/\"\n ],\n \"_resolved\": \"https://registry.npmjs.org/rxjs/-/rxjs-5.4.3.tgz\",\n \"_shasum\": \"0758cddee6033d68e0fd53676f0f3596ce3d483f\",\n \"_shrinkwrap\": null,\n \"_spec\": \"rxjs@^5.4.2\",\n \"_where\": \"C:\\\\Users\\\\shkamat\\\\Desktop\\\\app\",\n \"author\": {\n \"name\": \"Ben Lesh\",\n \"email\": \"ben@benlesh.com\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/ReactiveX/RxJS/issues\"\n },\n \"config\": {\n \"commitizen\": {\n \"path\": \"cz-conventional-changelog\"\n }\n },\n \"contributors\": [\n {\n \"name\": \"Ben Lesh\",\n \"email\": \"ben@benlesh.com\"\n },\n {\n \"name\": \"Paul Taylor\",\n \"email\": \"paul.e.taylor@me.com\"\n },\n {\n \"name\": \"Jeff Cross\",\n \"email\": \"crossj@google.com\"\n },\n {\n \"name\": \"Matthew Podwysocki\",\n \"email\": \"matthewp@microsoft.com\"\n },\n {\n \"name\": \"OJ Kwon\",\n \"email\": \"kwon.ohjoong@gmail.com\"\n },\n {\n \"name\": \"Andre Staltz\",\n \"email\": \"andre@staltz.com\"\n }\n ],\n \"dependencies\": {\n \"symbol-observable\": \"^1.0.1\"\n },\n \"description\": \"Reactive Extensions for modern JavaScript\",\n \"devDependencies\": {\n \"babel-polyfill\": \"^6.23.0\",\n \"benchmark\": \"^2.1.0\",\n \"benchpress\": \"2.0.0-beta.1\",\n \"chai\": \"^3.5.0\",\n \"color\": \"^0.11.1\",\n \"colors\": \"1.1.2\",\n \"commitizen\": \"^2.8.6\",\n \"coveralls\": \"^2.11.13\",\n \"cz-conventional-changelog\": \"^1.2.0\",\n \"danger\": \"^1.1.0\",\n \"doctoc\": \"^1.0.0\",\n \"escape-string-regexp\": \"^1.0.5 \",\n \"esdoc\": \"^0.4.7\",\n \"eslint\": \"^3.8.0\",\n \"fs-extra\": \"^2.1.2\",\n \"get-folder-size\": \"^1.0.0\",\n \"glob\": \"^7.0.3\",\n \"gm\": \"^1.22.0\",\n \"google-closure-compiler-js\": \"^20170218.0.0\",\n \"gzip-size\": \"^3.0.0\",\n \"http-server\": \"^0.9.0\",\n \"husky\": \"^0.13.3\",\n \"lint-staged\": \"3.2.5\",\n \"lodash\": \"^4.15.0\",\n \"madge\": \"^1.4.3\",\n \"markdown-doctest\": \"^0.9.1\",\n \"minimist\": \"^1.2.0\",\n \"mkdirp\": \"^0.5.1\",\n \"mocha\": \"^3.0.2\",\n \"mocha-in-sauce\": \"0.0.1\",\n \"npm-run-all\": \"^4.0.2\",\n \"npm-scripts-info\": \"^0.3.4\",\n \"nyc\": \"^10.2.0\",\n \"opn-cli\": \"^3.1.0\",\n \"platform\": \"^1.3.1\",\n \"promise\": \"^7.1.1\",\n \"protractor\": \"^3.1.1\",\n \"rollup\": \"0.36.3\",\n \"rollup-plugin-inject\": \"^2.0.0\",\n \"rollup-plugin-node-resolve\": \"^2.0.0\",\n \"rx\": \"latest\",\n \"rxjs\": \"latest\",\n \"shx\": \"^0.2.2\",\n \"sinon\": \"^2.1.0\",\n \"sinon-chai\": \"^2.9.0\",\n \"source-map-support\": \"^0.4.0\",\n \"tslib\": \"^1.5.0\",\n \"eslint\": \"^5.16.0\",\n \"typescript\": \"~2.0.6\",\n \"typings\": \"^2.0.0\",\n \"validate-commit-msg\": \"^2.14.0\",\n \"watch\": \"^1.0.1\",\n \"webpack\": \"^1.13.1\",\n \"xmlhttprequest\": \"1.8.0\"\n },\n \"directories\": {},\n \"dist\": {\n \"integrity\": \"sha512-fSNi+y+P9ss+EZuV0GcIIqPUK07DEaMRUtLJvdcvMyFjc9dizuDjere+A4V7JrLGnm9iCc+nagV/4QdMTkqC4A==\",\n \"shasum\": \"0758cddee6033d68e0fd53676f0f3596ce3d483f\",\n \"tarball\": \"https://registry.npmjs.org/rxjs/-/rxjs-5.4.3.tgz\"\n },\n \"engines\": {\n \"npm\": \">=2.0.0\"\n },\n \"homepage\": \"https://github.com/ReactiveX/RxJS\",\n \"keywords\": [\n \"Rx\",\n \"RxJS\",\n \"ReactiveX\",\n \"ReactiveExtensions\",\n \"Streams\",\n \"Observables\",\n \"Observable\",\n \"Stream\",\n \"ES6\",\n \"ES2015\"\n ],\n \"license\": \"Apache-2.0\",\n \"lint-staged\": {\n \"*.@(js)\": [\n \"eslint --fix\",\n \"git add\"\n ],\n \"*.@(ts)\": [\n \"eslint -c .eslintrc --ext .ts . --fix\",\n \"git add\"\n ]\n },\n \"main\": \"Rx.js\",\n \"maintainers\": [\n {\n \"name\": \"blesh\",\n \"email\": \"ben@benlesh.com\"\n }\n ],\n \"name\": \"rxjs\",\n \"optionalDependencies\": {},\n \"readme\": \"ERROR: No README data found!\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+ssh://git@github.com/ReactiveX/RxJS.git\"\n },\n \"scripts-info\": {\n \"info\": \"List available script\",\n \"build_all\": \"Build all packages (ES6, CJS, UMD) and generate packages\",\n \"build_cjs\": \"Build CJS package with clean up existing build, copy source into dist\",\n \"build_es6\": \"Build ES6 package with clean up existing build, copy source into dist\",\n \"build_closure_core\": \"Minify Global core build using closure compiler\",\n \"build_global\": \"Build Global package, then minify build\",\n \"build_perf\": \"Build CJS & Global build, run macro performance test\",\n \"build_test\": \"Build CJS package & test spec, execute mocha test runner\",\n \"build_cover\": \"Run lint to current code, build CJS & test spec, execute test coverage\",\n \"build_docs\": \"Build ES6 & global package, create documentation using it\",\n \"build_spec\": \"Build test specs\",\n \"check_circular_dependencies\": \"Check codebase has circular dependencies\",\n \"clean_spec\": \"Clean up existing test spec build output\",\n \"clean_dist_cjs\": \"Clean up existing CJS package output\",\n \"clean_dist_es6\": \"Clean up existing ES6 package output\",\n \"clean_dist_global\": \"Clean up existing Global package output\",\n \"commit\": \"Run git commit wizard\",\n \"compile_dist_cjs\": \"Compile codebase into CJS module\",\n \"compile_module_es6\": \"Compile codebase into ES6\",\n \"cover\": \"Execute test coverage\",\n \"lint_perf\": \"Run lint against performance test suite\",\n \"lint_spec\": \"Run lint against test spec\",\n \"lint_src\": \"Run lint against source\",\n \"lint\": \"Run lint against everything\",\n \"perf\": \"Run macro performance benchmark\",\n \"perf_micro\": \"Run micro performance benchmark\",\n \"test_mocha\": \"Execute mocha test runner against existing test spec build\",\n \"test_browser\": \"Execute mocha test runner on browser against existing test spec build\",\n \"test\": \"Clean up existing test spec build, build test spec and execute mocha test runner\",\n \"tests2png\": \"Generate marble diagram image from test spec\",\n \"watch\": \"Watch codebase, trigger compile when source code changes\"\n },\n \"typings\": \"Rx.d.ts\",\n \"version\": \"5.4.3\"\n}\n" } + ].map(getRootedFileOrFolder)); + verifyAfterPartialOrCompleteNpmInstall(0); + // remove /a/b/node_modules/.staging/rxjs-22375c61/package.json.2252192041 + filesAndFoldersToAdd.length--; + // and add few more folders/files + filesAndFoldersToAdd.push(...[ + { path: "/a/b/node_modules/symbol-observable" }, + { path: "/a/b/node_modules/@types" }, + { path: "/a/b/node_modules/@types/lodash" }, + { path: "/a/b/node_modules/lodash" }, + { path: "/a/b/node_modules/rxjs" }, + { path: "/a/b/node_modules/typescript" }, + { path: "/a/b/node_modules/.bin" } + ].map(getRootedFileOrFolder)); + // From the type root update + verifyAfterPartialOrCompleteNpmInstall(2); + forEach(filesAndFoldersToAdd, f => { + f.path = f.path + .replace("/a/b/node_modules/.staging", "/a/b/node_modules") + .replace(/[\-\.][\d\w][\d\w][\d\w][\d\w][\d\w][\d\w][\d\w][\d\w]/g, ""); + }); + const lodashIndexPath = root + "/a/b/node_modules/@types/lodash/index.d.ts"; + projectFiles.push((find(filesAndFoldersToAdd, f => f.path === lodashIndexPath)!)); + // we would now not have failed lookup in the parent of appFolder since lodash is available + recursiveWatchedDirectories.length = 2; + // npm installation complete, timeout after reload fs + npmInstallComplete = true; + verifyAfterPartialOrCompleteNpmInstall(2); + function verifyAfterPartialOrCompleteNpmInstall(timeoutQueueLengthWhenRunningTimeouts: number) { + host.reloadFS(projectFiles.concat(otherFiles, filesAndFoldersToAdd)); + if (npmInstallComplete || timeoutDuringPartialInstallation) { + host.checkTimeoutQueueLengthAndRun(timeoutQueueLengthWhenRunningTimeouts); } - - function verifyProject() { - checkNumberOfConfiguredProjects(projectService, 1); - - const project = projectService.configuredProjects.get(tsconfigJson.path)!; - const projectFilePaths = map(projectFiles, f => f.path); - checkProjectActualFiles(project, projectFilePaths); - - const filesWatched = filter(projectFilePaths, p => p !== app.path && p.indexOf("/a/b/node_modules") === -1); - checkWatchedFiles(host, filesWatched); - checkWatchedDirectories(host, typeRootDirectories.concat(recursiveWatchedDirectories), /*recursive*/ true); - checkWatchedDirectories(host, [], /*recursive*/ false); + else { + host.checkTimeoutQueueLength(2); } + verifyProject(); } - - it("timeouts occur inbetween installation", () => { - verifyNpmInstall(/*timeoutDuringPartialInstallation*/ true); - }); - - it("timeout occurs after installation", () => { - verifyNpmInstall(/*timeoutDuringPartialInstallation*/ false); - }); + function verifyProject() { + checkNumberOfConfiguredProjects(projectService, 1); + const project = projectService.configuredProjects.get(tsconfigJson.path)!; + const projectFilePaths = map(projectFiles, f => f.path); + checkProjectActualFiles(project, projectFilePaths); + const filesWatched = filter(projectFilePaths, p => p !== app.path && p.indexOf("/a/b/node_modules") === -1); + checkWatchedFiles(host, filesWatched); + checkWatchedDirectories(host, typeRootDirectories.concat(recursiveWatchedDirectories), /*recursive*/ true); + checkWatchedDirectories(host, [], /*recursive*/ false); + } + } + it("timeouts occur inbetween installation", () => { + verifyNpmInstall(/*timeoutDuringPartialInstallation*/ true); }); - - it("when node_modules dont receive event for the @types file addition", () => { - const projectLocation = "/user/username/folder/myproject"; - const app: File = { - path: `${projectLocation}/app.ts`, - content: `import * as debug from "debug"` - }; - const tsconfig: File = { - path: `${projectLocation}/tsconfig.json`, - content: "" - }; - - const files = [app, tsconfig, libFile]; - const host = createServerHost(files); - const service = createProjectService(host); - service.openClientFile(app.path); - - const project = service.configuredProjects.get(tsconfig.path)!; - checkProjectActualFiles(project, files.map(f => f.path)); - assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(app.path).map(diag => diag.messageText), ["Cannot find module 'debug'."]); - - const debugTypesFile: File = { - path: `${projectLocation}/node_modules/@types/debug/index.d.ts`, - content: "export {}" - }; - files.push(debugTypesFile); - // Do not invoke recursive directory watcher for anything other than node_module/@types - const invoker = host.invokeFsWatchesRecursiveCallbacks; - host.invokeFsWatchesRecursiveCallbacks = (fullPath, eventName, entryFullPath) => { - if (fullPath.endsWith("@types")) { - invoker.call(host, fullPath, eventName, entryFullPath); - } - }; - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - checkProjectActualFiles(project, files.map(f => f.path)); - assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(app.path).map(diag => diag.messageText), []); + it("timeout occurs after installation", () => { + verifyNpmInstall(/*timeoutDuringPartialInstallation*/ false); }); }); -} + it("when node_modules dont receive event for the @types file addition", () => { + const projectLocation = "/user/username/folder/myproject"; + const app: File = { + path: `${projectLocation}/app.ts`, + content: `import * as debug from "debug"` + }; + const tsconfig: File = { + path: `${projectLocation}/tsconfig.json`, + content: "" + }; + const files = [app, tsconfig, libFile]; + const host = createServerHost(files); + const service = createProjectService(host); + service.openClientFile(app.path); + const project = service.configuredProjects.get(tsconfig.path)!; + checkProjectActualFiles(project, files.map(f => f.path)); + assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(app.path).map(diag => diag.messageText), ["Cannot find module 'debug'."]); + const debugTypesFile: File = { + path: `${projectLocation}/node_modules/@types/debug/index.d.ts`, + content: "export {}" + }; + files.push(debugTypesFile); + // Do not invoke recursive directory watcher for anything other than node_module/@types + const invoker = host.invokeFsWatchesRecursiveCallbacks; + host.invokeFsWatchesRecursiveCallbacks = (fullPath, eventName, entryFullPath) => { + if (fullPath.endsWith("@types")) { + invoker.call(host, fullPath, eventName, entryFullPath); + } + }; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + checkProjectActualFiles(project, files.map(f => f.path)); + assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(app.path).map(diag => diag.messageText), []); + }); +}); diff --git a/src/testRunner/unittests/tsserver/cancellationToken.ts b/src/testRunner/unittests/tsserver/cancellationToken.ts index 4e5d8ab00f983..96cf74c825dc0 100644 --- a/src/testRunner/unittests/tsserver/cancellationToken.ts +++ b/src/testRunner/unittests/tsserver/cancellationToken.ts @@ -1,274 +1,244 @@ -namespace ts.projectSystem { - describe("unittests:: tsserver:: cancellationToken", () => { - // Disable sourcemap support for the duration of the test, as sourcemapping the errors generated during this test is slow and not something we care to test - let oldPrepare: AnyFunction; - before(() => { - oldPrepare = (Error as any).prepareStackTrace; - delete (Error as any).prepareStackTrace; - }); - - after(() => { - (Error as any).prepareStackTrace = oldPrepare; +import { AnyFunction, noop, projectSystem, OperationCanceledException } from "../../ts"; +import { createServerHost, createSession, TestServerCancellationToken } from "../../ts.projectSystem"; +import { ServerCancellationToken, protocol, extractMessage } from "../../ts.server"; +describe("unittests:: tsserver:: cancellationToken", () => { + // Disable sourcemap support for the duration of the test, as sourcemapping the errors generated during this test is slow and not something we care to test + let oldPrepare: AnyFunction; + before(() => { + oldPrepare = (Error as any).prepareStackTrace; + delete (Error as any).prepareStackTrace; + }); + after(() => { + (Error as any).prepareStackTrace = oldPrepare; + }); + it("is attached to request", () => { + const f1 = { + path: "/a/b/app.ts", + content: "let xyz = 1;" + }; + const host = createServerHost([f1]); + let expectedRequestId: number; + const cancellationToken: ServerCancellationToken = { + isCancellationRequested: () => false, + setRequest: requestId => { + if (expectedRequestId === undefined) { + assert.isTrue(false, "unexpected call"); + } + assert.equal(requestId, expectedRequestId); + }, + resetRequest: noop + }; + const session = createSession(host, { cancellationToken }); + expectedRequestId = session.getNextSeq(); + session.executeCommandSeq(({ + command: "open", + arguments: { file: f1.path } + })); + expectedRequestId = session.getNextSeq(); + session.executeCommandSeq(({ + command: "geterr", + arguments: { files: [f1.path] } + })); + expectedRequestId = session.getNextSeq(); + session.executeCommandSeq(({ + command: "occurrences", + arguments: { file: f1.path, line: 1, offset: 6 } + })); + expectedRequestId = 2; + host.runQueuedImmediateCallbacks(); + expectedRequestId = 2; + host.runQueuedImmediateCallbacks(); + }); + it("Geterr is cancellable", () => { + const f1 = { + path: "/a/app.ts", + content: "let x = 1" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: {} + }) + }; + const cancellationToken = new TestServerCancellationToken(); + const host = createServerHost([f1, config]); + const session = createSession(host, { + canUseEvents: true, + eventHandler: noop, + cancellationToken }); - - it("is attached to request", () => { - const f1 = { - path: "/a/b/app.ts", - content: "let xyz = 1;" - }; - const host = createServerHost([f1]); - let expectedRequestId: number; - const cancellationToken: server.ServerCancellationToken = { - isCancellationRequested: () => false, - setRequest: requestId => { - if (expectedRequestId === undefined) { - assert.isTrue(false, "unexpected call"); - } - assert.equal(requestId, expectedRequestId); - }, - resetRequest: noop - }; - - const session = createSession(host, { cancellationToken }); - - expectedRequestId = session.getNextSeq(); - session.executeCommandSeq({ + { + session.executeCommandSeq(({ command: "open", arguments: { file: f1.path } - }); - - expectedRequestId = session.getNextSeq(); - session.executeCommandSeq({ + })); + // send geterr for missing file + session.executeCommandSeq(({ + command: "geterr", + arguments: { files: ["/a/missing"] } + })); + // Queued files + assert.equal(host.getOutput().length, 0, "expected 0 message"); + host.checkTimeoutQueueLengthAndRun(1); + // Completed event since file is missing + assert.equal(host.getOutput().length, 1, "expect 1 message"); + verifyRequestCompleted(session.getSeq(), 0); + } + { + const getErrId = session.getNextSeq(); + // send geterr for a valid file + session.executeCommandSeq(({ + command: "geterr", + arguments: { files: [f1.path] } + })); + assert.equal(host.getOutput().length, 0, "expect 0 messages"); + // run new request + session.executeCommandSeq(({ + command: "projectInfo", + arguments: { file: f1.path } + })); + session.clearMessages(); + // cancel previously issued Geterr + cancellationToken.setRequestToCancel(getErrId); + host.runQueuedTimeoutCallbacks(); + assert.equal(host.getOutput().length, 1, "expect 1 message"); + verifyRequestCompleted(getErrId, 0); + cancellationToken.resetToken(); + } + { + const getErrId = session.getNextSeq(); + session.executeCommandSeq(({ command: "geterr", arguments: { files: [f1.path] } - }); - - expectedRequestId = session.getNextSeq(); - session.executeCommandSeq({ - command: "occurrences", - arguments: { file: f1.path, line: 1, offset: 6 } - }); - - expectedRequestId = 2; + })); + assert.equal(host.getOutput().length, 0, "expect 0 messages"); + // run first step + host.runQueuedTimeoutCallbacks(); + assert.equal(host.getOutput().length, 1, "expect 1 message"); + const e1 = (getMessage(0)); + assert.equal(e1.event, "syntaxDiag"); + session.clearMessages(); + cancellationToken.setRequestToCancel(getErrId); host.runQueuedImmediateCallbacks(); - expectedRequestId = 2; + assert.equal(host.getOutput().length, 1, "expect 1 message"); + verifyRequestCompleted(getErrId, 0); + cancellationToken.resetToken(); + } + { + const getErrId = session.getNextSeq(); + session.executeCommandSeq(({ + command: "geterr", + arguments: { files: [f1.path] } + })); + assert.equal(host.getOutput().length, 0, "expect 0 messages"); + // run first step + host.runQueuedTimeoutCallbacks(); + assert.equal(host.getOutput().length, 1, "expect 1 message"); + const e1 = (getMessage(0)); + assert.equal(e1.event, "syntaxDiag"); + session.clearMessages(); + // the semanticDiag message host.runQueuedImmediateCallbacks(); + assert.equal(host.getOutput().length, 1); + const e2 = (getMessage(0)); + assert.equal(e2.event, "semanticDiag"); + session.clearMessages(); + host.runQueuedImmediateCallbacks(1); + assert.equal(host.getOutput().length, 2); + const e3 = (getMessage(0)); + assert.equal(e3.event, "suggestionDiag"); + verifyRequestCompleted(getErrId, 1); + cancellationToken.resetToken(); + } + { + const getErr1 = session.getNextSeq(); + session.executeCommandSeq(({ + command: "geterr", + arguments: { files: [f1.path] } + })); + assert.equal(host.getOutput().length, 0, "expect 0 messages"); + // run first step + host.runQueuedTimeoutCallbacks(); + assert.equal(host.getOutput().length, 1, "expect 1 message"); + const e1 = (getMessage(0)); + assert.equal(e1.event, "syntaxDiag"); + session.clearMessages(); + session.executeCommandSeq(({ + command: "geterr", + arguments: { files: [f1.path] } + })); + // make sure that getErr1 is completed + verifyRequestCompleted(getErr1, 0); + } + function verifyRequestCompleted(expectedSeq: number, n: number) { + const event = (getMessage(n)); + assert.equal(event.event, "requestCompleted"); + assert.equal(event.body.request_seq, expectedSeq, "expectedSeq"); + session.clearMessages(); + } + function getMessage(n: number) { + return JSON.parse(extractMessage(host.getOutput()[n])); + } + }); + it("Lower priority tasks are cancellable", () => { + const f1 = { + path: "/a/app.ts", + content: `{ let x = 1; } var foo = "foo"; var bar = "bar"; var fooBar = "fooBar";` + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: {} + }) + }; + const cancellationToken = new TestServerCancellationToken(/*cancelAfterRequest*/ 3); + const host = createServerHost([f1, config]); + const session = createSession(host, { + canUseEvents: true, + eventHandler: noop, + cancellationToken, + throttleWaitMilliseconds: 0 }); - - it("Geterr is cancellable", () => { - const f1 = { - path: "/a/app.ts", - content: "let x = 1" - }; - const config = { - path: "/a/tsconfig.json", - content: JSON.stringify({ - compilerOptions: {} - }) - }; - - const cancellationToken = new TestServerCancellationToken(); - const host = createServerHost([f1, config]); - const session = createSession(host, { - canUseEvents: true, - eventHandler: noop, - cancellationToken - }); - { - session.executeCommandSeq({ - command: "open", - arguments: { file: f1.path } - }); - // send geterr for missing file - session.executeCommandSeq({ - command: "geterr", - arguments: { files: ["/a/missing"] } - }); - // Queued files - assert.equal(host.getOutput().length, 0, "expected 0 message"); - host.checkTimeoutQueueLengthAndRun(1); - // Completed event since file is missing - assert.equal(host.getOutput().length, 1, "expect 1 message"); - verifyRequestCompleted(session.getSeq(), 0); - } - { - const getErrId = session.getNextSeq(); - // send geterr for a valid file - session.executeCommandSeq({ - command: "geterr", - arguments: { files: [f1.path] } - }); - - assert.equal(host.getOutput().length, 0, "expect 0 messages"); - - // run new request - session.executeCommandSeq({ - command: "projectInfo", - arguments: { file: f1.path } - }); - session.clearMessages(); - - // cancel previously issued Geterr - cancellationToken.setRequestToCancel(getErrId); - host.runQueuedTimeoutCallbacks(); - - assert.equal(host.getOutput().length, 1, "expect 1 message"); - verifyRequestCompleted(getErrId, 0); - - cancellationToken.resetToken(); - } - { - const getErrId = session.getNextSeq(); - session.executeCommandSeq({ - command: "geterr", - arguments: { files: [f1.path] } - }); - assert.equal(host.getOutput().length, 0, "expect 0 messages"); - - // run first step - host.runQueuedTimeoutCallbacks(); - assert.equal(host.getOutput().length, 1, "expect 1 message"); - const e1 = getMessage(0); - assert.equal(e1.event, "syntaxDiag"); - session.clearMessages(); - - cancellationToken.setRequestToCancel(getErrId); - host.runQueuedImmediateCallbacks(); - assert.equal(host.getOutput().length, 1, "expect 1 message"); - verifyRequestCompleted(getErrId, 0); - - cancellationToken.resetToken(); - } - { - const getErrId = session.getNextSeq(); - session.executeCommandSeq({ - command: "geterr", - arguments: { files: [f1.path] } - }); - assert.equal(host.getOutput().length, 0, "expect 0 messages"); - - // run first step - host.runQueuedTimeoutCallbacks(); - assert.equal(host.getOutput().length, 1, "expect 1 message"); - const e1 = getMessage(0); - assert.equal(e1.event, "syntaxDiag"); - session.clearMessages(); - - // the semanticDiag message - host.runQueuedImmediateCallbacks(); - assert.equal(host.getOutput().length, 1); - const e2 = getMessage(0); - assert.equal(e2.event, "semanticDiag"); - session.clearMessages(); - - host.runQueuedImmediateCallbacks(1); - assert.equal(host.getOutput().length, 2); - const e3 = getMessage(0); - assert.equal(e3.event, "suggestionDiag"); - verifyRequestCompleted(getErrId, 1); - - cancellationToken.resetToken(); - } - { - const getErr1 = session.getNextSeq(); - session.executeCommandSeq({ - command: "geterr", - arguments: { files: [f1.path] } - }); - assert.equal(host.getOutput().length, 0, "expect 0 messages"); - // run first step - host.runQueuedTimeoutCallbacks(); - assert.equal(host.getOutput().length, 1, "expect 1 message"); - const e1 = getMessage(0); - assert.equal(e1.event, "syntaxDiag"); - session.clearMessages(); - - session.executeCommandSeq({ - command: "geterr", - arguments: { files: [f1.path] } - }); - // make sure that getErr1 is completed - verifyRequestCompleted(getErr1, 0); - } - - function verifyRequestCompleted(expectedSeq: number, n: number) { - const event = getMessage(n); - assert.equal(event.event, "requestCompleted"); - assert.equal(event.body.request_seq, expectedSeq, "expectedSeq"); - session.clearMessages(); - } - - function getMessage(n: number) { - return JSON.parse(server.extractMessage(host.getOutput()[n])); - } - }); - - it("Lower priority tasks are cancellable", () => { - const f1 = { - path: "/a/app.ts", - content: `{ let x = 1; } var foo = "foo"; var bar = "bar"; var fooBar = "fooBar";` - }; - const config = { - path: "/a/tsconfig.json", - content: JSON.stringify({ - compilerOptions: {} - }) - }; - const cancellationToken = new TestServerCancellationToken(/*cancelAfterRequest*/ 3); - const host = createServerHost([f1, config]); - const session = createSession(host, { - canUseEvents: true, - eventHandler: noop, - cancellationToken, - throttleWaitMilliseconds: 0 - }); - { - session.executeCommandSeq({ - command: "open", - arguments: { file: f1.path } - }); - - // send navbar request (normal priority) - session.executeCommandSeq({ - command: "navbar", - arguments: { file: f1.path } - }); - - // ensure the nav bar request can be canceled - verifyExecuteCommandSeqIsCancellable({ - command: "navbar", - arguments: { file: f1.path } - }); - - // send outlining spans request (normal priority) - session.executeCommandSeq({ - command: "outliningSpans", - arguments: { file: f1.path } - }); - - // ensure the outlining spans request can be canceled - verifyExecuteCommandSeqIsCancellable({ - command: "outliningSpans", - arguments: { file: f1.path } - }); + { + session.executeCommandSeq(({ + command: "open", + arguments: { file: f1.path } + })); + // send navbar request (normal priority) + session.executeCommandSeq(({ + command: "navbar", + arguments: { file: f1.path } + })); + // ensure the nav bar request can be canceled + verifyExecuteCommandSeqIsCancellable(({ + command: "navbar", + arguments: { file: f1.path } + })); + // send outlining spans request (normal priority) + session.executeCommandSeq(({ + command: "outliningSpans", + arguments: { file: f1.path } + })); + // ensure the outlining spans request can be canceled + verifyExecuteCommandSeqIsCancellable(({ + command: "outliningSpans", + arguments: { file: f1.path } + })); + } + function verifyExecuteCommandSeqIsCancellable(request: Partial) { + // Set the next request to be cancellable + // The cancellation token will cancel the request the third time + // isCancellationRequested() is called. + cancellationToken.setRequestToCancel(session.getNextSeq()); + let operationCanceledExceptionThrown = false; + try { + session.executeCommandSeq(request); } - - function verifyExecuteCommandSeqIsCancellable(request: Partial) { - // Set the next request to be cancellable - // The cancellation token will cancel the request the third time - // isCancellationRequested() is called. - cancellationToken.setRequestToCancel(session.getNextSeq()); - let operationCanceledExceptionThrown = false; - - try { - session.executeCommandSeq(request); - } - catch (e) { - assert(e instanceof OperationCanceledException); - operationCanceledExceptionThrown = true; - } - assert(operationCanceledExceptionThrown, "Operation Canceled Exception not thrown for request: " + JSON.stringify(request)); + catch (e) { + assert(e instanceof OperationCanceledException); + operationCanceledExceptionThrown = true; } - }); + assert(operationCanceledExceptionThrown, "Operation Canceled Exception not thrown for request: " + JSON.stringify(request)); + } }); -} +}); diff --git a/src/testRunner/unittests/tsserver/compileOnSave.ts b/src/testRunner/unittests/tsserver/compileOnSave.ts index 9fca91842e59a..ebf2fff1b195f 100644 --- a/src/testRunner/unittests/tsserver/compileOnSave.ts +++ b/src/testRunner/unittests/tsserver/compileOnSave.ts @@ -1,898 +1,793 @@ -namespace ts.projectSystem { - import CommandNames = server.CommandNames; - const nullCancellationToken = server.nullCancellationToken; - - function createTestTypingsInstaller(host: server.ServerHost) { - return new TestTypingsInstaller("/a/data/", /*throttleLimit*/5, host); - } - - describe("unittests:: tsserver:: compileOnSave:: affected list", () => { - function sendAffectedFileRequestAndCheckResult(session: server.Session, request: server.protocol.Request, expectedFileList: { projectFileName: string, files: File[] }[]) { - const response = session.executeCommand(request).response as server.protocol.CompileOnSaveAffectedFileListSingleProject[]; - const actualResult = response.sort((list1, list2) => compareStringsCaseSensitive(list1.projectFileName, list2.projectFileName)); - expectedFileList = expectedFileList.sort((list1, list2) => compareStringsCaseSensitive(list1.projectFileName, list2.projectFileName)); - - assert.equal(actualResult.length, expectedFileList.length, `Actual result project number is different from the expected project number`); - - for (let i = 0; i < actualResult.length; i++) { - const actualResultSingleProject = actualResult[i]; - const expectedResultSingleProject = expectedFileList[i]; - assert.equal(actualResultSingleProject.projectFileName, expectedResultSingleProject.projectFileName, `Actual result contains different projects than the expected result`); - - const actualResultSingleProjectFileNameList = actualResultSingleProject.fileNames.sort(); - const expectedResultSingleProjectFileNameList = map(expectedResultSingleProject.files, f => f.path).sort(); - assert.isTrue( - arrayIsEqualTo(actualResultSingleProjectFileNameList, expectedResultSingleProjectFileNameList), - `For project ${actualResultSingleProject.projectFileName}, the actual result is ${actualResultSingleProjectFileNameList}, while expected ${expectedResultSingleProjectFileNameList}`); - } +import { server, compareStringsCaseSensitive, map, arrayIsEqualTo, CompilerOptions, projectSystem, Extension, stringContains } from "../../ts"; +import { ServerHost, Session, protocol, ITypingsInstaller, SessionOptions, nullTypingsInstaller } from "../../ts.server"; +import { TestTypingsInstaller, File, createHasErrorMessageLogger, makeSessionRequest, createServerHost, libFile, openFilesForSession, checkNumberOfProjects, checkProjectRootFiles, createSession, toExternalFiles, TestSession, checkProjectActualFiles } from "../../ts.projectSystem"; +import { byteLength } from "../../Utils"; +import { projectRoot } from "../../ts.tscWatch"; +import * as ts from "../../ts"; +import CommandNames = ts.server.CommandNames; +const nullCancellationToken = server.nullCancellationToken; +function createTestTypingsInstaller(host: ServerHost) { + return new TestTypingsInstaller("/a/data/", /*throttleLimit*/ 5, host); +} +describe("unittests:: tsserver:: compileOnSave:: affected list", () => { + function sendAffectedFileRequestAndCheckResult(session: Session, request: protocol.Request, expectedFileList: { + projectFileName: string; + files: File[]; + }[]) { + const response = (session.executeCommand(request).response as protocol.CompileOnSaveAffectedFileListSingleProject[]); + const actualResult = response.sort((list1, list2) => compareStringsCaseSensitive(list1.projectFileName, list2.projectFileName)); + expectedFileList = expectedFileList.sort((list1, list2) => compareStringsCaseSensitive(list1.projectFileName, list2.projectFileName)); + assert.equal(actualResult.length, expectedFileList.length, `Actual result project number is different from the expected project number`); + for (let i = 0; i < actualResult.length; i++) { + const actualResultSingleProject = actualResult[i]; + const expectedResultSingleProject = expectedFileList[i]; + assert.equal(actualResultSingleProject.projectFileName, expectedResultSingleProject.projectFileName, `Actual result contains different projects than the expected result`); + const actualResultSingleProjectFileNameList = actualResultSingleProject.fileNames.sort(); + const expectedResultSingleProjectFileNameList = map(expectedResultSingleProject.files, f => f.path).sort(); + assert.isTrue(arrayIsEqualTo(actualResultSingleProjectFileNameList, expectedResultSingleProjectFileNameList), `For project ${actualResultSingleProject.projectFileName}, the actual result is ${actualResultSingleProjectFileNameList}, while expected ${expectedResultSingleProjectFileNameList}`); } - - function createSession(host: server.ServerHost, typingsInstaller?: server.ITypingsInstaller): server.Session { - const opts: server.SessionOptions = { - host, - cancellationToken: nullCancellationToken, - useSingleInferredProject: false, - useInferredProjectPerProjectRoot: false, - typingsInstaller: typingsInstaller || server.nullTypingsInstaller, - byteLength: Utils.byteLength, - hrtime: process.hrtime, - logger: createHasErrorMessageLogger().logger, - canUseEvents: false + } + function createSession(host: ServerHost, typingsInstaller?: ITypingsInstaller): Session { + const opts: SessionOptions = { + host, + cancellationToken: nullCancellationToken, + useSingleInferredProject: false, + useInferredProjectPerProjectRoot: false, + typingsInstaller: typingsInstaller || nullTypingsInstaller, + byteLength: byteLength, + hrtime: process.hrtime, + logger: createHasErrorMessageLogger().logger, + canUseEvents: false + }; + return new Session(opts); + } + describe("for configured projects", () => { + let moduleFile1: File; + let file1Consumer1: File; + let file1Consumer2: File; + let moduleFile2: File; + let globalFile3: File; + let configFile: File; + let changeModuleFile1ShapeRequest1: protocol.Request; + let changeModuleFile1InternalRequest1: protocol.Request; + // A compile on save affected file request using file1 + let moduleFile1FileListRequest: protocol.Request; + beforeEach(() => { + moduleFile1 = { + path: "/a/b/moduleFile1.ts", + content: "export function Foo() { };" }; - return new server.Session(opts); - } - - describe("for configured projects", () => { - let moduleFile1: File; - let file1Consumer1: File; - let file1Consumer2: File; - let moduleFile2: File; - let globalFile3: File; - let configFile: File; - let changeModuleFile1ShapeRequest1: server.protocol.Request; - let changeModuleFile1InternalRequest1: server.protocol.Request; - // A compile on save affected file request using file1 - let moduleFile1FileListRequest: server.protocol.Request; - - beforeEach(() => { - moduleFile1 = { - path: "/a/b/moduleFile1.ts", - content: "export function Foo() { };" - }; - - file1Consumer1 = { - path: "/a/b/file1Consumer1.ts", - content: `import {Foo} from "./moduleFile1"; export var y = 10;` - }; - - file1Consumer2 = { - path: "/a/b/file1Consumer2.ts", - content: `import {Foo} from "./moduleFile1"; let z = 10;` - }; - - moduleFile2 = { - path: "/a/b/moduleFile2.ts", - content: `export var Foo4 = 10;` - }; - - globalFile3 = { - path: "/a/b/globalFile3.ts", - content: `interface GlobalFoo { age: number }` - }; - - configFile = { - path: "/a/b/tsconfig.json", - content: `{ + file1Consumer1 = { + path: "/a/b/file1Consumer1.ts", + content: `import {Foo} from "./moduleFile1"; export var y = 10;` + }; + file1Consumer2 = { + path: "/a/b/file1Consumer2.ts", + content: `import {Foo} from "./moduleFile1"; let z = 10;` + }; + moduleFile2 = { + path: "/a/b/moduleFile2.ts", + content: `export var Foo4 = 10;` + }; + globalFile3 = { + path: "/a/b/globalFile3.ts", + content: `interface GlobalFoo { age: number }` + }; + configFile = { + path: "/a/b/tsconfig.json", + content: `{ "compileOnSave": true }` - }; - - // Change the content of file1 to `export var T: number;export function Foo() { };` - changeModuleFile1ShapeRequest1 = makeSessionRequest(CommandNames.Change, { - file: moduleFile1.path, - line: 1, - offset: 1, - endLine: 1, - endOffset: 1, - insertString: `export var T: number;` - }); - - // Change the content of file1 to `export var T: number;export function Foo() { };` - changeModuleFile1InternalRequest1 = makeSessionRequest(CommandNames.Change, { - file: moduleFile1.path, - line: 1, - offset: 1, - endLine: 1, - endOffset: 1, - insertString: `var T1: number;` - }); - - moduleFile1FileListRequest = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: moduleFile1.path, projectFileName: configFile.path }); + }; + // Change the content of file1 to `export var T: number;export function Foo() { };` + changeModuleFile1ShapeRequest1 = makeSessionRequest(CommandNames.Change, { + file: moduleFile1.path, + line: 1, + offset: 1, + endLine: 1, + endOffset: 1, + insertString: `export var T: number;` }); - - it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => { - const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]); - const typingsInstaller = createTestTypingsInstaller(host); - const session = createSession(host, typingsInstaller); - - openFilesForSession([moduleFile1, file1Consumer1], session); - - // Send an initial compileOnSave request - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); - session.executeCommand(changeModuleFile1ShapeRequest1); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); - - // Change the content of file1 to `export var T: number;export function Foo() { console.log('hi'); };` - const changeFile1InternalRequest = makeSessionRequest(CommandNames.Change, { - file: moduleFile1.path, - line: 1, - offset: 46, - endLine: 1, - endOffset: 46, - insertString: `console.log('hi');` - }); - session.executeCommand(changeFile1InternalRequest); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]); + // Change the content of file1 to `export var T: number;export function Foo() { };` + changeModuleFile1InternalRequest1 = makeSessionRequest(CommandNames.Change, { + file: moduleFile1.path, + line: 1, + offset: 1, + endLine: 1, + endOffset: 1, + insertString: `var T1: number;` }); - - it("should be up-to-date with the reference map changes", () => { - const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]); - const typingsInstaller = createTestTypingsInstaller(host); - const session = createSession(host, typingsInstaller); - - openFilesForSession([moduleFile1, file1Consumer1], session); - - // Send an initial compileOnSave request - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); - - // Change file2 content to `let y = Foo();` - const removeFile1Consumer1ImportRequest = makeSessionRequest(CommandNames.Change, { - file: file1Consumer1.path, - line: 1, - offset: 1, - endLine: 1, - endOffset: 28, - insertString: "" - }); - session.executeCommand(removeFile1Consumer1ImportRequest); - session.executeCommand(changeModuleFile1ShapeRequest1); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer2] }]); - - // Add the import statements back to file2 - const addFile2ImportRequest = makeSessionRequest(CommandNames.Change, { - file: file1Consumer1.path, - line: 1, - offset: 1, - endLine: 1, - endOffset: 1, - insertString: `import {Foo} from "./moduleFile1";` - }); - session.executeCommand(addFile2ImportRequest); - - // Change the content of file1 to `export var T2: string;export var T: number;export function Foo() { };` - const changeModuleFile1ShapeRequest2 = makeSessionRequest(CommandNames.Change, { - file: moduleFile1.path, - line: 1, - offset: 1, - endLine: 1, - endOffset: 1, - insertString: `export var T2: string;` - }); - session.executeCommand(changeModuleFile1ShapeRequest2); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); + moduleFile1FileListRequest = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: moduleFile1.path, projectFileName: configFile.path }); + }); + it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => { + const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = createSession(host, typingsInstaller); + openFilesForSession([moduleFile1, file1Consumer1], session); + // Send an initial compileOnSave request + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); + session.executeCommand(changeModuleFile1ShapeRequest1); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); + // Change the content of file1 to `export var T: number;export function Foo() { console.log('hi'); };` + const changeFile1InternalRequest = makeSessionRequest(CommandNames.Change, { + file: moduleFile1.path, + line: 1, + offset: 46, + endLine: 1, + endOffset: 46, + insertString: `console.log('hi');` }); - - it("should be up-to-date with changes made in non-open files", () => { - const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]); - const typingsInstaller = createTestTypingsInstaller(host); - const session = createSession(host, typingsInstaller); - - openFilesForSession([moduleFile1], session); - - // Send an initial compileOnSave request - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); - - file1Consumer1.content = `let y = 10;`; - host.reloadFS([moduleFile1, file1Consumer1, file1Consumer2, configFile, libFile]); - - session.executeCommand(changeModuleFile1ShapeRequest1); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer2] }]); + session.executeCommand(changeFile1InternalRequest); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]); + }); + it("should be up-to-date with the reference map changes", () => { + const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = createSession(host, typingsInstaller); + openFilesForSession([moduleFile1, file1Consumer1], session); + // Send an initial compileOnSave request + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); + // Change file2 content to `let y = Foo();` + const removeFile1Consumer1ImportRequest = makeSessionRequest(CommandNames.Change, { + file: file1Consumer1.path, + line: 1, + offset: 1, + endLine: 1, + endOffset: 28, + insertString: "" }); - - it("should be up-to-date with deleted files", () => { - const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]); - const typingsInstaller = createTestTypingsInstaller(host); - const session = createSession(host, typingsInstaller); - - openFilesForSession([moduleFile1], session); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); - - session.executeCommand(changeModuleFile1ShapeRequest1); - // Delete file1Consumer2 - host.reloadFS([moduleFile1, file1Consumer1, configFile, libFile]); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1] }]); + session.executeCommand(removeFile1Consumer1ImportRequest); + session.executeCommand(changeModuleFile1ShapeRequest1); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer2] }]); + // Add the import statements back to file2 + const addFile2ImportRequest = makeSessionRequest(CommandNames.Change, { + file: file1Consumer1.path, + line: 1, + offset: 1, + endLine: 1, + endOffset: 1, + insertString: `import {Foo} from "./moduleFile1";` }); - - it("should be up-to-date with newly created files", () => { - const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]); - const typingsInstaller = createTestTypingsInstaller(host); - const session = createSession(host, typingsInstaller); - - openFilesForSession([moduleFile1], session); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); - - const file1Consumer3: File = { - path: "/a/b/file1Consumer3.ts", - content: `import {Foo} from "./moduleFile1"; let y = Foo();` - }; - host.reloadFS([moduleFile1, file1Consumer1, file1Consumer2, file1Consumer3, globalFile3, configFile, libFile]); - host.runQueuedTimeoutCallbacks(); - session.executeCommand(changeModuleFile1ShapeRequest1); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2, file1Consumer3] }]); + session.executeCommand(addFile2ImportRequest); + // Change the content of file1 to `export var T2: string;export var T: number;export function Foo() { };` + const changeModuleFile1ShapeRequest2 = makeSessionRequest(CommandNames.Change, { + file: moduleFile1.path, + line: 1, + offset: 1, + endLine: 1, + endOffset: 1, + insertString: `export var T2: string;` }); - - it("should detect changes in non-root files", () => { - moduleFile1 = { - path: "/a/b/moduleFile1.ts", - content: "export function Foo() { };" - }; - - file1Consumer1 = { - path: "/a/b/file1Consumer1.ts", - content: `import {Foo} from "./moduleFile1"; let y = Foo();` - }; - - configFile = { - path: "/a/b/tsconfig.json", - content: `{ + session.executeCommand(changeModuleFile1ShapeRequest2); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); + }); + it("should be up-to-date with changes made in non-open files", () => { + const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = createSession(host, typingsInstaller); + openFilesForSession([moduleFile1], session); + // Send an initial compileOnSave request + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); + file1Consumer1.content = `let y = 10;`; + host.reloadFS([moduleFile1, file1Consumer1, file1Consumer2, configFile, libFile]); + session.executeCommand(changeModuleFile1ShapeRequest1); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer2] }]); + }); + it("should be up-to-date with deleted files", () => { + const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = createSession(host, typingsInstaller); + openFilesForSession([moduleFile1], session); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); + session.executeCommand(changeModuleFile1ShapeRequest1); + // Delete file1Consumer2 + host.reloadFS([moduleFile1, file1Consumer1, configFile, libFile]); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1] }]); + }); + it("should be up-to-date with newly created files", () => { + const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = createSession(host, typingsInstaller); + openFilesForSession([moduleFile1], session); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); + const file1Consumer3: File = { + path: "/a/b/file1Consumer3.ts", + content: `import {Foo} from "./moduleFile1"; let y = Foo();` + }; + host.reloadFS([moduleFile1, file1Consumer1, file1Consumer2, file1Consumer3, globalFile3, configFile, libFile]); + host.runQueuedTimeoutCallbacks(); + session.executeCommand(changeModuleFile1ShapeRequest1); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2, file1Consumer3] }]); + }); + it("should detect changes in non-root files", () => { + moduleFile1 = { + path: "/a/b/moduleFile1.ts", + content: "export function Foo() { };" + }; + file1Consumer1 = { + path: "/a/b/file1Consumer1.ts", + content: `import {Foo} from "./moduleFile1"; let y = Foo();` + }; + configFile = { + path: "/a/b/tsconfig.json", + content: `{ "compileOnSave": true, "files": ["${file1Consumer1.path}"] }` - }; - - const host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]); - const typingsInstaller = createTestTypingsInstaller(host); - const session = createSession(host, typingsInstaller); - - openFilesForSession([moduleFile1, file1Consumer1], session); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1] }]); - - // change file1 shape now, and verify both files are affected - session.executeCommand(changeModuleFile1ShapeRequest1); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1] }]); - - // change file1 internal, and verify only file1 is affected - session.executeCommand(changeModuleFile1InternalRequest1); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]); - }); - - it("should return all files if a global file changed shape", () => { - const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]); - const typingsInstaller = createTestTypingsInstaller(host); - const session = createSession(host, typingsInstaller); - - openFilesForSession([globalFile3], session); - const changeGlobalFile3ShapeRequest = makeSessionRequest(CommandNames.Change, { - file: globalFile3.path, - line: 1, - offset: 1, - endLine: 1, - endOffset: 1, - insertString: `var T2: string;` - }); - - // check after file1 shape changes - session.executeCommand(changeGlobalFile3ShapeRequest); - const globalFile3FileListRequest = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: globalFile3.path }); - sendAffectedFileRequestAndCheckResult(session, globalFile3FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2] }]); - }); - - it("should return empty array if CompileOnSave is not enabled", () => { - configFile = { - path: "/a/b/tsconfig.json", - content: `{}` - }; - - const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, configFile, libFile]); - const typingsInstaller = createTestTypingsInstaller(host); - const session = createSession(host, typingsInstaller); - openFilesForSession([moduleFile1], session); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, []); + }; + const host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = createSession(host, typingsInstaller); + openFilesForSession([moduleFile1, file1Consumer1], session); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1] }]); + // change file1 shape now, and verify both files are affected + session.executeCommand(changeModuleFile1ShapeRequest1); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1] }]); + // change file1 internal, and verify only file1 is affected + session.executeCommand(changeModuleFile1InternalRequest1); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]); + }); + it("should return all files if a global file changed shape", () => { + const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = createSession(host, typingsInstaller); + openFilesForSession([globalFile3], session); + const changeGlobalFile3ShapeRequest = makeSessionRequest(CommandNames.Change, { + file: globalFile3.path, + line: 1, + offset: 1, + endLine: 1, + endOffset: 1, + insertString: `var T2: string;` }); - - it("should return empty array if noEmit is set", () => { - configFile = { - path: "/a/b/tsconfig.json", - content: `{ + // check after file1 shape changes + session.executeCommand(changeGlobalFile3ShapeRequest); + const globalFile3FileListRequest = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: globalFile3.path }); + sendAffectedFileRequestAndCheckResult(session, globalFile3FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2] }]); + }); + it("should return empty array if CompileOnSave is not enabled", () => { + configFile = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, configFile, libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = createSession(host, typingsInstaller); + openFilesForSession([moduleFile1], session); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, []); + }); + it("should return empty array if noEmit is set", () => { + configFile = { + path: "/a/b/tsconfig.json", + content: `{ "compileOnSave": true, "compilerOptions": { "noEmit": true } }` - }; - - const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, configFile, libFile]); - const typingsInstaller = createTestTypingsInstaller(host); - const session = createSession(host, typingsInstaller); - openFilesForSession([moduleFile1], session); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, []); - }); - - it("should save when compileOnSave is enabled in base tsconfig.json", () => { - configFile = { - path: "/a/b/tsconfig.json", - content: `{ + }; + const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, configFile, libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = createSession(host, typingsInstaller); + openFilesForSession([moduleFile1], session); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, []); + }); + it("should save when compileOnSave is enabled in base tsconfig.json", () => { + configFile = { + path: "/a/b/tsconfig.json", + content: `{ "extends": "/a/tsconfig.json" }` - }; - - const configFile2: File = { - path: "/a/tsconfig.json", - content: `{ + }; + const configFile2: File = { + path: "/a/tsconfig.json", + content: `{ "compileOnSave": true }` - }; - - const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, configFile2, configFile, libFile]); - const typingsInstaller = createTestTypingsInstaller(host); - const session = createSession(host, typingsInstaller); - - openFilesForSession([moduleFile1, file1Consumer1], session); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); - }); - - it("should always return the file itself if '--isolatedModules' is specified", () => { - configFile = { - path: "/a/b/tsconfig.json", - content: `{ + }; + const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, configFile2, configFile, libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = createSession(host, typingsInstaller); + openFilesForSession([moduleFile1, file1Consumer1], session); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); + }); + it("should always return the file itself if '--isolatedModules' is specified", () => { + configFile = { + path: "/a/b/tsconfig.json", + content: `{ "compileOnSave": true, "compilerOptions": { "isolatedModules": true } }` - }; - - const host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]); - const typingsInstaller = createTestTypingsInstaller(host); - const session = createSession(host, typingsInstaller); - openFilesForSession([moduleFile1], session); - - const file1ChangeShapeRequest = makeSessionRequest(CommandNames.Change, { - file: moduleFile1.path, - line: 1, - offset: 27, - endLine: 1, - endOffset: 27, - insertString: `Point,` - }); - session.executeCommand(file1ChangeShapeRequest); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]); + }; + const host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = createSession(host, typingsInstaller); + openFilesForSession([moduleFile1], session); + const file1ChangeShapeRequest = makeSessionRequest(CommandNames.Change, { + file: moduleFile1.path, + line: 1, + offset: 27, + endLine: 1, + endOffset: 27, + insertString: `Point,` }); - - it("should always return the file itself if '--out' or '--outFile' is specified", () => { - configFile = { - path: "/a/b/tsconfig.json", - content: `{ + session.executeCommand(file1ChangeShapeRequest); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]); + }); + it("should always return the file itself if '--out' or '--outFile' is specified", () => { + configFile = { + path: "/a/b/tsconfig.json", + content: `{ "compileOnSave": true, "compilerOptions": { "module": "system", "outFile": "/a/b/out.js" } }` - }; - - const host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]); - const typingsInstaller = createTestTypingsInstaller(host); - const session = createSession(host, typingsInstaller); - openFilesForSession([moduleFile1], session); - - const file1ChangeShapeRequest = makeSessionRequest(CommandNames.Change, { - file: moduleFile1.path, - line: 1, - offset: 27, - endLine: 1, - endOffset: 27, - insertString: `Point,` - }); - session.executeCommand(file1ChangeShapeRequest); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]); + }; + const host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = createSession(host, typingsInstaller); + openFilesForSession([moduleFile1], session); + const file1ChangeShapeRequest = makeSessionRequest(CommandNames.Change, { + file: moduleFile1.path, + line: 1, + offset: 27, + endLine: 1, + endOffset: 27, + insertString: `Point,` }); - - it("should return cascaded affected file list", () => { - const file1Consumer1Consumer1: File = { - path: "/a/b/file1Consumer1Consumer1.ts", - content: `import {y} from "./file1Consumer1";` - }; - const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer1Consumer1, globalFile3, configFile, libFile]); - const typingsInstaller = createTestTypingsInstaller(host); - const session = createSession(host, typingsInstaller); - - openFilesForSession([moduleFile1, file1Consumer1], session); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer1Consumer1] }]); - - const changeFile1Consumer1ShapeRequest = makeSessionRequest(CommandNames.Change, { - file: file1Consumer1.path, - line: 2, - offset: 1, - endLine: 2, - endOffset: 1, - insertString: `export var T: number;` - }); - session.executeCommand(changeModuleFile1ShapeRequest1); - session.executeCommand(changeFile1Consumer1ShapeRequest); - sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer1Consumer1] }]); + session.executeCommand(file1ChangeShapeRequest); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]); + }); + it("should return cascaded affected file list", () => { + const file1Consumer1Consumer1: File = { + path: "/a/b/file1Consumer1Consumer1.ts", + content: `import {y} from "./file1Consumer1";` + }; + const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer1Consumer1, globalFile3, configFile, libFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = createSession(host, typingsInstaller); + openFilesForSession([moduleFile1, file1Consumer1], session); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer1Consumer1] }]); + const changeFile1Consumer1ShapeRequest = makeSessionRequest(CommandNames.Change, { + file: file1Consumer1.path, + line: 2, + offset: 1, + endLine: 2, + endOffset: 1, + insertString: `export var T: number;` }); - - it("should work fine for files with circular references", () => { - const file1: File = { - path: "/a/b/file1.ts", - content: ` + session.executeCommand(changeModuleFile1ShapeRequest1); + session.executeCommand(changeFile1Consumer1ShapeRequest); + sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer1Consumer1] }]); + }); + it("should work fine for files with circular references", () => { + const file1: File = { + path: "/a/b/file1.ts", + content: ` /// export var t1 = 10;` - }; - const file2: File = { - path: "/a/b/file2.ts", - content: ` + }; + const file2: File = { + path: "/a/b/file2.ts", + content: ` /// export var t2 = 10;` - }; - const host = createServerHost([file1, file2, configFile]); - const typingsInstaller = createTestTypingsInstaller(host); - const session = createSession(host, typingsInstaller); - - openFilesForSession([file1, file2], session); - const file1AffectedListRequest = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: file1.path }); - sendAffectedFileRequestAndCheckResult(session, file1AffectedListRequest, [{ projectFileName: configFile.path, files: [file1, file2] }]); - }); - - it("should return results for all projects if not specifying projectFileName", () => { - const file1: File = { path: "/a/b/file1.ts", content: "export var t = 10;" }; - const file2: File = { path: "/a/b/file2.ts", content: `import {t} from "./file1"; var t2 = 11;` }; - const file3: File = { path: "/a/c/file2.ts", content: `import {t} from "../b/file1"; var t3 = 11;` }; - const configFile1: File = { path: "/a/b/tsconfig.json", content: `{ "compileOnSave": true }` }; - const configFile2: File = { path: "/a/c/tsconfig.json", content: `{ "compileOnSave": true }` }; - - const host = createServerHost([file1, file2, file3, configFile1, configFile2]); - const session = createSession(host); - - openFilesForSession([file1, file2, file3], session); - const file1AffectedListRequest = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: file1.path }); - - sendAffectedFileRequestAndCheckResult(session, file1AffectedListRequest, [ - { projectFileName: configFile1.path, files: [file1, file2] }, - { projectFileName: configFile2.path, files: [file1, file3] } - ]); - }); - - it("should detect removed code file", () => { - const referenceFile1: File = { - path: "/a/b/referenceFile1.ts", - content: ` + }; + const host = createServerHost([file1, file2, configFile]); + const typingsInstaller = createTestTypingsInstaller(host); + const session = createSession(host, typingsInstaller); + openFilesForSession([file1, file2], session); + const file1AffectedListRequest = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: file1.path }); + sendAffectedFileRequestAndCheckResult(session, file1AffectedListRequest, [{ projectFileName: configFile.path, files: [file1, file2] }]); + }); + it("should return results for all projects if not specifying projectFileName", () => { + const file1: File = { path: "/a/b/file1.ts", content: "export var t = 10;" }; + const file2: File = { path: "/a/b/file2.ts", content: `import {t} from "./file1"; var t2 = 11;` }; + const file3: File = { path: "/a/c/file2.ts", content: `import {t} from "../b/file1"; var t3 = 11;` }; + const configFile1: File = { path: "/a/b/tsconfig.json", content: `{ "compileOnSave": true }` }; + const configFile2: File = { path: "/a/c/tsconfig.json", content: `{ "compileOnSave": true }` }; + const host = createServerHost([file1, file2, file3, configFile1, configFile2]); + const session = createSession(host); + openFilesForSession([file1, file2, file3], session); + const file1AffectedListRequest = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: file1.path }); + sendAffectedFileRequestAndCheckResult(session, file1AffectedListRequest, [ + { projectFileName: configFile1.path, files: [file1, file2] }, + { projectFileName: configFile2.path, files: [file1, file3] } + ]); + }); + it("should detect removed code file", () => { + const referenceFile1: File = { + path: "/a/b/referenceFile1.ts", + content: ` /// export var x = Foo();` - }; - const host = createServerHost([moduleFile1, referenceFile1, configFile]); - const session = createSession(host); - - openFilesForSession([referenceFile1], session); - host.reloadFS([referenceFile1, configFile]); - - const request = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: referenceFile1.path }); - sendAffectedFileRequestAndCheckResult(session, request, [ - { projectFileName: configFile.path, files: [referenceFile1] } - ]); - const requestForMissingFile = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: moduleFile1.path }); - sendAffectedFileRequestAndCheckResult(session, requestForMissingFile, []); - }); - - it("should detect non-existing code file", () => { - const referenceFile1: File = { - path: "/a/b/referenceFile1.ts", - content: ` + }; + const host = createServerHost([moduleFile1, referenceFile1, configFile]); + const session = createSession(host); + openFilesForSession([referenceFile1], session); + host.reloadFS([referenceFile1, configFile]); + const request = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: referenceFile1.path }); + sendAffectedFileRequestAndCheckResult(session, request, [ + { projectFileName: configFile.path, files: [referenceFile1] } + ]); + const requestForMissingFile = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: moduleFile1.path }); + sendAffectedFileRequestAndCheckResult(session, requestForMissingFile, []); + }); + it("should detect non-existing code file", () => { + const referenceFile1: File = { + path: "/a/b/referenceFile1.ts", + content: ` /// export var x = Foo();` - }; - const host = createServerHost([referenceFile1, configFile]); - const session = createSession(host); - - openFilesForSession([referenceFile1], session); - const request = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: referenceFile1.path }); - sendAffectedFileRequestAndCheckResult(session, request, [ - { projectFileName: configFile.path, files: [referenceFile1] } - ]); - }); - }); - - describe("for changes in declaration files", () => { - function testDTS(dtsFileContents: string, tsFileContents: string, opts: CompilerOptions, expectDTSEmit: boolean) { - const dtsFile = { - path: "/a/runtime/a.d.ts", - content: dtsFileContents - }; - const f2 = { - path: "/a/b.ts", - content: tsFileContents - }; - const config = { - path: "/a/tsconfig.json", - content: JSON.stringify({ - compilerOptions: opts, - compileOnSave: true - }) - }; - const host = createServerHost([dtsFile, f2, config]); - const session = projectSystem.createSession(host); - session.executeCommand({ - seq: 1, - type: "request", - command: "open", - arguments: { file: dtsFile.path } - }); - const projectService = session.getProjectService(); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const project = projectService.configuredProjects.get(config.path)!; - checkProjectRootFiles(project, [dtsFile.path, f2.path]); - session.executeCommand({ - seq: 2, - type: "request", - command: "open", - arguments: { file: f2.path } - }); - checkNumberOfProjects(session.getProjectService(), { configuredProjects: 1 }); - const { response } = session.executeCommand({ - seq: 3, - type: "request", - command: "compileOnSaveAffectedFileList", - arguments: { file: dtsFile.path } - }); - if (expectDTSEmit) { - assert.equal((response as protocol.CompileOnSaveAffectedFileListSingleProject[]).length, 1, "expected output from 1 project"); - assert.equal((response as protocol.CompileOnSaveAffectedFileListSingleProject[])[0].fileNames.length, 2, "expected to affect 2 files"); - } - else { - assert.equal((response as protocol.CompileOnSaveAffectedFileListSingleProject[]).length, 0, "expected no output"); - } - - - const { response: response2 } = session.executeCommand({ - seq: 4, - type: "request", - command: "compileOnSaveAffectedFileList", - arguments: { file: f2.path } - }); - assert.equal((response2 as protocol.CompileOnSaveAffectedFileListSingleProject[]).length, 1, "expected output from 1 project"); - } - - it("should return empty array if change is made in a global declaration file", () => { - testDTS( - /*dtsFileContents*/ "declare const x: string;", - /*tsFileContents*/ "var y = 1;", - /*opts*/ {}, - /*expectDTSEmit*/ false - ); - }); - - it("should return empty array if change is made in a module declaration file", () => { - testDTS( - /*dtsFileContents*/ "export const x: string;", - /*tsFileContents*/ "import { x } from './runtime/a;", - /*opts*/ {}, - /*expectDTSEmit*/ false - ); - }); - - it("should return results if change is made in a global declaration file with declaration emit", () => { - testDTS( - /*dtsFileContents*/ "declare const x: string;", - /*tsFileContents*/ "var y = 1;", - /*opts*/ { declaration: true }, - /*expectDTSEmit*/ true - ); - }); - - it("should return results if change is made in a global declaration file with composite enabled", () => { - testDTS( - /*dtsFileContents*/ "declare const x: string;", - /*tsFileContents*/ "var y = 1;", - /*opts*/ { composite: true }, - /*expectDTSEmit*/ true - ); - }); - - it("should return results if change is made in a global declaration file with decorator emit enabled", () => { - testDTS( - /*dtsFileContents*/ "declare const x: string;", - /*tsFileContents*/ "var y = 1;", - /*opts*/ { experimentalDecorators: true, emitDecoratorMetadata: true }, - /*expectDTSEmit*/ true - ); - }); - }); - - describe("tsserverProjectSystem emit with outFile or out setting", () => { - function test(opts: CompilerOptions, expectedUsesOutFile: boolean) { - const f1 = { - path: "/a/a.ts", - content: "let x = 1" - }; - const f2 = { - path: "/a/b.ts", - content: "let y = 1" - }; - const config = { - path: "/a/tsconfig.json", - content: JSON.stringify({ - compilerOptions: opts, - compileOnSave: true - }) - }; - const host = createServerHost([f1, f2, config]); - const session = projectSystem.createSession(host); - session.executeCommand({ - seq: 1, - type: "request", - command: "open", - arguments: { file: f1.path } - }); - checkNumberOfProjects(session.getProjectService(), { configuredProjects: 1 }); - const { response } = session.executeCommand({ - seq: 2, - type: "request", - command: "compileOnSaveAffectedFileList", - arguments: { file: f1.path } - }); - assert.equal((response).length, 1, "expected output for 1 project"); - assert.equal((response)[0].fileNames.length, 2, "expected output for 1 project"); - assert.equal((response)[0].projectUsesOutFile, expectedUsesOutFile, "usesOutFile"); - } - - it("projectUsesOutFile should not be returned if not set", () => { - test({}, /*expectedUsesOutFile*/ false); - }); - it("projectUsesOutFile should be true if outFile is set", () => { - test({ outFile: "/a/out.js" }, /*expectedUsesOutFile*/ true); - }); - it("projectUsesOutFile should be true if out is set", () => { - test({ out: "/a/out.js" }, /*expectedUsesOutFile*/ true); - }); + }; + const host = createServerHost([referenceFile1, configFile]); + const session = createSession(host); + openFilesForSession([referenceFile1], session); + const request = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: referenceFile1.path }); + sendAffectedFileRequestAndCheckResult(session, request, [ + { projectFileName: configFile.path, files: [referenceFile1] } + ]); }); }); - - describe("unittests:: tsserver:: compileOnSave:: EmitFile test", () => { - it("should respect line endings", () => { - test("\n"); - test("\r\n"); - - function test(newLine: string) { - const lines = ["var x = 1;", "var y = 2;"]; - const path = "/a/app"; - const f = { - path: path + Extension.Ts, - content: lines.join(newLine) - }; - const host = createServerHost([f], { newLine }); - const session = createSession(host); - const openRequest: server.protocol.OpenRequest = { - seq: 1, - type: "request", - command: server.protocol.CommandTypes.Open, - arguments: { file: f.path } - }; - session.executeCommand(openRequest); - const emitFileRequest: server.protocol.CompileOnSaveEmitFileRequest = { - seq: 2, - type: "request", - command: server.protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { file: f.path } - }; - session.executeCommand(emitFileRequest); - const emitOutput = host.readFile(path + Extension.Js); - assert.equal(emitOutput, f.content + newLine, "content of emit output should be identical with the input + newline"); - } - }); - - it("should emit specified file", () => { - const file1 = { - path: "/a/b/f1.ts", - content: `export function Foo() { return 10; }` + describe("for changes in declaration files", () => { + function testDTS(dtsFileContents: string, tsFileContents: string, opts: CompilerOptions, expectDTSEmit: boolean) { + const dtsFile = { + path: "/a/runtime/a.d.ts", + content: dtsFileContents }; - const file2 = { - path: "/a/b/f2.ts", - content: `import {Foo} from "./f1"; let y = Foo();` + const f2 = { + path: "/a/b.ts", + content: tsFileContents }; - const configFile = { - path: "/a/b/tsconfig.json", - content: `{}` + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: opts, + compileOnSave: true + }) }; - const host = createServerHost([file1, file2, configFile, libFile], { newLine: "\r\n" }); - const typingsInstaller = createTestTypingsInstaller(host); - const session = createSession(host, { typingsInstaller }); - - openFilesForSession([file1, file2], session); - const compileFileRequest = makeSessionRequest(CommandNames.CompileOnSaveEmitFile, { file: file1.path, projectFileName: configFile.path }); - session.executeCommand(compileFileRequest); - - const expectedEmittedFileName = "/a/b/f1.js"; - assert.isTrue(host.fileExists(expectedEmittedFileName)); - assert.equal(host.readFile(expectedEmittedFileName), `"use strict";\r\nexports.__esModule = true;\r\nexports.Foo = void 0;\r\nfunction Foo() { return 10; }\r\nexports.Foo = Foo;\r\n`); + const host = createServerHost([dtsFile, f2, config]); + const session = projectSystem.createSession(host); + session.executeCommand(({ + seq: 1, + type: "request", + command: "open", + arguments: { file: dtsFile.path } + })); + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = projectService.configuredProjects.get(config.path)!; + checkProjectRootFiles(project, [dtsFile.path, f2.path]); + session.executeCommand(({ + seq: 2, + type: "request", + command: "open", + arguments: { file: f2.path } + })); + checkNumberOfProjects(session.getProjectService(), { configuredProjects: 1 }); + const { response } = session.executeCommand(({ + seq: 3, + type: "request", + command: "compileOnSaveAffectedFileList", + arguments: { file: dtsFile.path } + })); + if (expectDTSEmit) { + assert.equal((response as projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]).length, 1, "expected output from 1 project"); + assert.equal((response as projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[])[0].fileNames.length, 2, "expected to affect 2 files"); + } + else { + assert.equal((response as projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]).length, 0, "expected no output"); + } + const { response: response2 } = session.executeCommand(({ + seq: 4, + type: "request", + command: "compileOnSaveAffectedFileList", + arguments: { file: f2.path } + })); + assert.equal((response2 as projectSystem.protocol.CompileOnSaveAffectedFileListSingleProject[]).length, 1, "expected output from 1 project"); + } + it("should return empty array if change is made in a global declaration file", () => { + testDTS( + /*dtsFileContents*/ "declare const x: string;", + /*tsFileContents*/ "var y = 1;", + /*opts*/ {}, + /*expectDTSEmit*/ false); }); - - it("shoud not emit js files in external projects", () => { - const file1 = { - path: "/a/b/file1.ts", - content: "consonle.log('file1');" - }; - // file2 has errors. The emitting should not be blocked. - const file2 = { - path: "/a/b/file2.js", - content: "console.log'file2');" + it("should return empty array if change is made in a module declaration file", () => { + testDTS( + /*dtsFileContents*/ "export const x: string;", + /*tsFileContents*/ "import { x } from './runtime/a;", + /*opts*/ {}, + /*expectDTSEmit*/ false); + }); + it("should return results if change is made in a global declaration file with declaration emit", () => { + testDTS( + /*dtsFileContents*/ "declare const x: string;", + /*tsFileContents*/ "var y = 1;", + /*opts*/ { declaration: true }, + /*expectDTSEmit*/ true); + }); + it("should return results if change is made in a global declaration file with composite enabled", () => { + testDTS( + /*dtsFileContents*/ "declare const x: string;", + /*tsFileContents*/ "var y = 1;", + /*opts*/ { composite: true }, + /*expectDTSEmit*/ true); + }); + it("should return results if change is made in a global declaration file with decorator emit enabled", () => { + testDTS( + /*dtsFileContents*/ "declare const x: string;", + /*tsFileContents*/ "var y = 1;", + /*opts*/ { experimentalDecorators: true, emitDecoratorMetadata: true }, + /*expectDTSEmit*/ true); + }); + }); + describe("tsserverProjectSystem emit with outFile or out setting", () => { + function test(opts: CompilerOptions, expectedUsesOutFile: boolean) { + const f1 = { + path: "/a/a.ts", + content: "let x = 1" }; - const file3 = { - path: "/a/b/file3.js", - content: "console.log('file3');" + const f2 = { + path: "/a/b.ts", + content: "let y = 1" }; - const externalProjectName = "/a/b/externalproject"; - const host = createServerHost([file1, file2, file3, libFile]); - const session = createSession(host); - const projectService = session.getProjectService(); - - projectService.openExternalProject({ - rootFiles: toExternalFiles([file1.path, file2.path]), - options: { - allowJs: true, - outFile: "dist.js", + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: opts, compileOnSave: true - }, - projectFileName: externalProjectName - }); - - const emitRequest = makeSessionRequest(CommandNames.CompileOnSaveEmitFile, { file: file1.path }); - session.executeCommand(emitRequest); - - const expectedOutFileName = "/a/b/dist.js"; - assert.isTrue(host.fileExists(expectedOutFileName)); - const outFileContent = host.readFile(expectedOutFileName)!; - assert.isTrue(outFileContent.indexOf(file1.content) !== -1); - assert.isTrue(outFileContent.indexOf(file2.content) === -1); - assert.isTrue(outFileContent.indexOf(file3.content) === -1); + }) + }; + const host = createServerHost([f1, f2, config]); + const session = projectSystem.createSession(host); + session.executeCommand(({ + seq: 1, + type: "request", + command: "open", + arguments: { file: f1.path } + })); + checkNumberOfProjects(session.getProjectService(), { configuredProjects: 1 }); + const { response } = session.executeCommand(({ + seq: 2, + type: "request", + command: "compileOnSaveAffectedFileList", + arguments: { file: f1.path } + })); + assert.equal((response).length, 1, "expected output for 1 project"); + assert.equal((response)[0].fileNames.length, 2, "expected output for 1 project"); + assert.equal((response)[0].projectUsesOutFile, expectedUsesOutFile, "usesOutFile"); + } + it("projectUsesOutFile should not be returned if not set", () => { + test({}, /*expectedUsesOutFile*/ false); + }); + it("projectUsesOutFile should be true if outFile is set", () => { + test({ outFile: "/a/out.js" }, /*expectedUsesOutFile*/ true); }); - - it("should use project root as current directory so that compile on save results in correct file mapping", () => { - const inputFileName = "Foo.ts"; - const file1 = { - path: `/root/TypeScriptProject3/TypeScriptProject3/${inputFileName}`, - content: "consonle.log('file1');" + it("projectUsesOutFile should be true if out is set", () => { + test({ out: "/a/out.js" }, /*expectedUsesOutFile*/ true); + }); + }); +}); +describe("unittests:: tsserver:: compileOnSave:: EmitFile test", () => { + it("should respect line endings", () => { + test("\n"); + test("\r\n"); + function test(newLine: string) { + const lines = ["var x = 1;", "var y = 2;"]; + const path = "/a/app"; + const f = { + path: path + Extension.Ts, + content: lines.join(newLine) }; - const externalProjectName = "/root/TypeScriptProject3/TypeScriptProject3/TypeScriptProject3.csproj"; - const host = createServerHost([file1, libFile]); + const host = createServerHost([f], { newLine }); const session = createSession(host); - const projectService = session.getProjectService(); - - const outFileName = "bar.js"; - projectService.openExternalProject({ - rootFiles: toExternalFiles([file1.path]), - options: { - outFile: outFileName, - sourceMap: true, - compileOnSave: true - }, - projectFileName: externalProjectName - }); - - const emitRequest = makeSessionRequest(CommandNames.CompileOnSaveEmitFile, { file: file1.path }); - session.executeCommand(emitRequest); - - // Verify js file - const expectedOutFileName = "/root/TypeScriptProject3/TypeScriptProject3/" + outFileName; - assert.isTrue(host.fileExists(expectedOutFileName)); - const outFileContent = host.readFile(expectedOutFileName)!; - verifyContentHasString(outFileContent, file1.content); - verifyContentHasString(outFileContent, `//# ${"sourceMappingURL"}=${outFileName}.map`); // Sometimes tools can sometimes see this line as a source mapping url comment, so we obfuscate it a little - - // Verify map file - const expectedMapFileName = expectedOutFileName + ".map"; - assert.isTrue(host.fileExists(expectedMapFileName)); - const mapFileContent = host.readFile(expectedMapFileName)!; - verifyContentHasString(mapFileContent, `"sources":["${inputFileName}"]`); - - function verifyContentHasString(content: string, str: string) { - assert.isTrue(stringContains(content, str), `Expected "${content}" to have "${str}"`); - } - }); + const openRequest: protocol.OpenRequest = { + seq: 1, + type: "request", + command: protocol.CommandTypes.Open, + arguments: { file: f.path } + }; + session.executeCommand(openRequest); + const emitFileRequest: protocol.CompileOnSaveEmitFileRequest = { + seq: 2, + type: "request", + command: protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { file: f.path } + }; + session.executeCommand(emitFileRequest); + const emitOutput = host.readFile(path + Extension.Js); + assert.equal(emitOutput, f.content + newLine, "content of emit output should be identical with the input + newline"); + } }); - - describe("unittests:: tsserver:: compileOnSave:: CompileOnSaveAffectedFileListRequest with and without projectFileName in request", () => { - const core: File = { - path: `${tscWatch.projectRoot}/core/core.ts`, - content: "let z = 10;" + it("should emit specified file", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `export function Foo() { return 10; }` }; - const app1: File = { - path: `${tscWatch.projectRoot}/app1/app.ts`, - content: "let x = 10;" + const file2 = { + path: "/a/b/f2.ts", + content: `import {Foo} from "./f1"; let y = Foo();` }; - const app2: File = { - path: `${tscWatch.projectRoot}/app2/app.ts`, - content: "let y = 10;" + const configFile = { + path: "/a/b/tsconfig.json", + content: `{}` }; - const app1Config: File = { - path: `${tscWatch.projectRoot}/app1/tsconfig.json`, - content: JSON.stringify({ - files: ["app.ts", "../core/core.ts"], - compilerOptions: { outFile: "build/output.js" }, - compileOnSave: true - }) + const host = createServerHost([file1, file2, configFile, libFile], { newLine: "\r\n" }); + const typingsInstaller = createTestTypingsInstaller(host); + const session = createSession(host, { typingsInstaller }); + openFilesForSession([file1, file2], session); + const compileFileRequest = makeSessionRequest(CommandNames.CompileOnSaveEmitFile, { file: file1.path, projectFileName: configFile.path }); + session.executeCommand(compileFileRequest); + const expectedEmittedFileName = "/a/b/f1.js"; + assert.isTrue(host.fileExists(expectedEmittedFileName)); + assert.equal(host.readFile(expectedEmittedFileName), `"use strict";\r\nexports.__esModule = true;\r\nexports.Foo = void 0;\r\nfunction Foo() { return 10; }\r\nexports.Foo = Foo;\r\n`); + }); + it("shoud not emit js files in external projects", () => { + const file1 = { + path: "/a/b/file1.ts", + content: "consonle.log('file1');" + }; + // file2 has errors. The emitting should not be blocked. + const file2 = { + path: "/a/b/file2.js", + content: "console.log'file2');" + }; + const file3 = { + path: "/a/b/file3.js", + content: "console.log('file3');" }; - const app2Config: File = { - path: `${tscWatch.projectRoot}/app2/tsconfig.json`, - content: JSON.stringify({ - files: ["app.ts", "../core/core.ts"], - compilerOptions: { outFile: "build/output.js" }, + const externalProjectName = "/a/b/externalproject"; + const host = createServerHost([file1, file2, file3, libFile]); + const session = createSession(host); + const projectService = session.getProjectService(); + projectService.openExternalProject({ + rootFiles: toExternalFiles([file1.path, file2.path]), + options: { + allowJs: true, + outFile: "dist.js", compileOnSave: true - }) + }, + projectFileName: externalProjectName + }); + const emitRequest = makeSessionRequest(CommandNames.CompileOnSaveEmitFile, { file: file1.path }); + session.executeCommand(emitRequest); + const expectedOutFileName = "/a/b/dist.js"; + assert.isTrue(host.fileExists(expectedOutFileName)); + const outFileContent = host.readFile(expectedOutFileName)!; + assert.isTrue(outFileContent.indexOf(file1.content) !== -1); + assert.isTrue(outFileContent.indexOf(file2.content) === -1); + assert.isTrue(outFileContent.indexOf(file3.content) === -1); + }); + it("should use project root as current directory so that compile on save results in correct file mapping", () => { + const inputFileName = "Foo.ts"; + const file1 = { + path: `/root/TypeScriptProject3/TypeScriptProject3/${inputFileName}`, + content: "consonle.log('file1');" }; - const files = [libFile, core, app1, app2, app1Config, app2Config]; - - function insertString(session: TestSession, file: File) { - session.executeCommandSeq({ - command: protocol.CommandTypes.Change, - arguments: { - file: file.path, - line: 1, - offset: 1, - endLine: 1, - endOffset: 1, - insertString: "let k = 1" - } - }); - } - - function getSession() { - const host = createServerHost(files); - const session = createSession(host); - openFilesForSession([app1, app2, core], session); - const service = session.getProjectService(); - checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); - const project1 = service.configuredProjects.get(app1Config.path)!; - const project2 = service.configuredProjects.get(app2Config.path)!; - checkProjectActualFiles(project1, [libFile.path, app1.path, core.path, app1Config.path]); - checkProjectActualFiles(project2, [libFile.path, app2.path, core.path, app2Config.path]); - insertString(session, app1); - insertString(session, app2); - assert.equal(project1.dirty, true); - assert.equal(project2.dirty, true); - return session; - } - - it("when projectFile is specified", () => { - const session = getSession(); - const response = session.executeCommandSeq({ - command: protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { - file: core.path, - projectFileName: app1Config.path - } - }).response; - assert.deepEqual(response, [ - { projectFileName: app1Config.path, fileNames: [core.path, app1.path], projectUsesOutFile: true } - ]); - assert.equal(session.getProjectService().configuredProjects.get(app1Config.path)!.dirty, false); - assert.equal(session.getProjectService().configuredProjects.get(app2Config.path)!.dirty, true); + const externalProjectName = "/root/TypeScriptProject3/TypeScriptProject3/TypeScriptProject3.csproj"; + const host = createServerHost([file1, libFile]); + const session = createSession(host); + const projectService = session.getProjectService(); + const outFileName = "bar.js"; + projectService.openExternalProject({ + rootFiles: toExternalFiles([file1.path]), + options: { + outFile: outFileName, + sourceMap: true, + compileOnSave: true + }, + projectFileName: externalProjectName }); - - it("when projectFile is not specified", () => { - const session = getSession(); - const response = session.executeCommandSeq({ - command: protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { - file: core.path - } - }).response; - assert.deepEqual(response, [ - { projectFileName: app1Config.path, fileNames: [core.path, app1.path], projectUsesOutFile: true }, - { projectFileName: app2Config.path, fileNames: [core.path, app2.path], projectUsesOutFile: true } - ]); - assert.equal(session.getProjectService().configuredProjects.get(app1Config.path)!.dirty, false); - assert.equal(session.getProjectService().configuredProjects.get(app2Config.path)!.dirty, false); + const emitRequest = makeSessionRequest(CommandNames.CompileOnSaveEmitFile, { file: file1.path }); + session.executeCommand(emitRequest); + // Verify js file + const expectedOutFileName = "/root/TypeScriptProject3/TypeScriptProject3/" + outFileName; + assert.isTrue(host.fileExists(expectedOutFileName)); + const outFileContent = host.readFile(expectedOutFileName)!; + verifyContentHasString(outFileContent, file1.content); + verifyContentHasString(outFileContent, `//# ${"sourceMappingURL"}=${outFileName}.map`); // Sometimes tools can sometimes see this line as a source mapping url comment, so we obfuscate it a little + // Verify map file + const expectedMapFileName = expectedOutFileName + ".map"; + assert.isTrue(host.fileExists(expectedMapFileName)); + const mapFileContent = host.readFile(expectedMapFileName)!; + verifyContentHasString(mapFileContent, `"sources":["${inputFileName}"]`); + function verifyContentHasString(content: string, str: string) { + assert.isTrue(stringContains(content, str), `Expected "${content}" to have "${str}"`); + } + }); +}); +describe("unittests:: tsserver:: compileOnSave:: CompileOnSaveAffectedFileListRequest with and without projectFileName in request", () => { + const core: File = { + path: `${projectRoot}/core/core.ts`, + content: "let z = 10;" + }; + const app1: File = { + path: `${projectRoot}/app1/app.ts`, + content: "let x = 10;" + }; + const app2: File = { + path: `${projectRoot}/app2/app.ts`, + content: "let y = 10;" + }; + const app1Config: File = { + path: `${projectRoot}/app1/tsconfig.json`, + content: JSON.stringify({ + files: ["app.ts", "../core/core.ts"], + compilerOptions: { outFile: "build/output.js" }, + compileOnSave: true + }) + }; + const app2Config: File = { + path: `${projectRoot}/app2/tsconfig.json`, + content: JSON.stringify({ + files: ["app.ts", "../core/core.ts"], + compilerOptions: { outFile: "build/output.js" }, + compileOnSave: true + }) + }; + const files = [libFile, core, app1, app2, app1Config, app2Config]; + function insertString(session: TestSession, file: File) { + session.executeCommandSeq({ + command: projectSystem.protocol.CommandTypes.Change, + arguments: { + file: file.path, + line: 1, + offset: 1, + endLine: 1, + endOffset: 1, + insertString: "let k = 1" + } }); + } + function getSession() { + const host = createServerHost(files); + const session = createSession(host); + openFilesForSession([app1, app2, core], session); + const service = session.getProjectService(); + checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); + const project1 = service.configuredProjects.get(app1Config.path)!; + const project2 = service.configuredProjects.get(app2Config.path)!; + checkProjectActualFiles(project1, [libFile.path, app1.path, core.path, app1Config.path]); + checkProjectActualFiles(project2, [libFile.path, app2.path, core.path, app2Config.path]); + insertString(session, app1); + insertString(session, app2); + assert.equal(project1.dirty, true); + assert.equal(project2.dirty, true); + return session; + } + it("when projectFile is specified", () => { + const session = getSession(); + const response = session.executeCommandSeq({ + command: projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { + file: core.path, + projectFileName: app1Config.path + } + }).response; + assert.deepEqual(response, [ + { projectFileName: app1Config.path, fileNames: [core.path, app1.path], projectUsesOutFile: true } + ]); + assert.equal(session.getProjectService().configuredProjects.get(app1Config.path)!.dirty, false); + assert.equal(session.getProjectService().configuredProjects.get(app2Config.path)!.dirty, true); }); -} + it("when projectFile is not specified", () => { + const session = getSession(); + const response = session.executeCommandSeq({ + command: projectSystem.protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { + file: core.path + } + }).response; + assert.deepEqual(response, [ + { projectFileName: app1Config.path, fileNames: [core.path, app1.path], projectUsesOutFile: true }, + { projectFileName: app2Config.path, fileNames: [core.path, app2.path], projectUsesOutFile: true } + ]); + assert.equal(session.getProjectService().configuredProjects.get(app1Config.path)!.dirty, false); + assert.equal(session.getProjectService().configuredProjects.get(app2Config.path)!.dirty, false); + }); +}); diff --git a/src/testRunner/unittests/tsserver/completions.ts b/src/testRunner/unittests/tsserver/completions.ts index 857f1b6ebc1e0..c2c787ba38570 100644 --- a/src/testRunner/unittests/tsserver/completions.ts +++ b/src/testRunner/unittests/tsserver/completions.ts @@ -1,250 +1,241 @@ -namespace ts.projectSystem { - describe("unittests:: tsserver:: completions", () => { - it("works", () => { - const aTs: File = { - path: "/a.ts", - content: "export const foo = 0;", - }; - const bTs: File = { - path: "/b.ts", - content: "foo", - }; - const tsconfig: File = { - path: "/tsconfig.json", - content: "{}", - }; - - const session = createSession(createServerHost([aTs, bTs, tsconfig])); - openFilesForSession([aTs, bTs], session); - - const requestLocation: protocol.FileLocationRequestArgs = { - file: bTs.path, - line: 1, - offset: 3, - }; - - const response = executeSessionRequest(session, protocol.CommandTypes.CompletionInfo, { - ...requestLocation, - includeExternalModuleExports: true, - prefix: "foo", - }); - const entry: protocol.CompletionEntry = { - hasAction: true, - insertText: undefined, - isRecommended: undefined, - kind: ScriptElementKind.constElement, - kindModifiers: ScriptElementKindModifier.exportedModifier, - name: "foo", - replacementSpan: undefined, - sortText: Completions.SortText.AutoImportSuggestions, - source: "/a", - }; - assert.deepEqual(response, { - isGlobalCompletion: true, - isMemberCompletion: false, - isNewIdentifierLocation: false, - entries: [entry], - }); - - const detailsRequestArgs: protocol.CompletionDetailsRequestArgs = { - ...requestLocation, - entryNames: [{ name: "foo", source: "/a" }], - }; - - const detailsResponse = executeSessionRequest(session, protocol.CommandTypes.CompletionDetails, detailsRequestArgs); - const detailsCommon: protocol.CompletionEntryDetails & CompletionEntryDetails = { - displayParts: [ - keywordPart(SyntaxKind.ConstKeyword), - spacePart(), - displayPart("foo", SymbolDisplayPartKind.localName), - punctuationPart(SyntaxKind.ColonToken), - spacePart(), - displayPart("0", SymbolDisplayPartKind.stringLiteral), +import { File, createSession, createServerHost, openFilesForSession, protocol, executeSessionRequest, libFile, TestTypingsInstaller, checkNumberOfProjects, checkProjectActualFiles } from "../../ts.projectSystem"; +import { ScriptElementKind, ScriptElementKindModifier, Completions, CompletionEntryDetails, keywordPart, SyntaxKind, spacePart, displayPart, SymbolDisplayPartKind, punctuationPart, emptyArray, createTextChange, createTextSpan } from "../../ts"; +describe("unittests:: tsserver:: completions", () => { + it("works", () => { + const aTs: File = { + path: "/a.ts", + content: "export const foo = 0;", + }; + const bTs: File = { + path: "/b.ts", + content: "foo", + }; + const tsconfig: File = { + path: "/tsconfig.json", + content: "{}", + }; + const session = createSession(createServerHost([aTs, bTs, tsconfig])); + openFilesForSession([aTs, bTs], session); + const requestLocation: protocol.FileLocationRequestArgs = { + file: bTs.path, + line: 1, + offset: 3, + }; + const response = executeSessionRequest(session, protocol.CommandTypes.CompletionInfo, { + ...requestLocation, + includeExternalModuleExports: true, + prefix: "foo", + }); + const entry: protocol.CompletionEntry = { + hasAction: true, + insertText: undefined, + isRecommended: undefined, + kind: ScriptElementKind.constElement, + kindModifiers: ScriptElementKindModifier.exportedModifier, + name: "foo", + replacementSpan: undefined, + sortText: Completions.SortText.AutoImportSuggestions, + source: "/a", + }; + assert.deepEqual(response, { + isGlobalCompletion: true, + isMemberCompletion: false, + isNewIdentifierLocation: false, + entries: [entry], + }); + const detailsRequestArgs: protocol.CompletionDetailsRequestArgs = { + ...requestLocation, + entryNames: [{ name: "foo", source: "/a" }], + }; + const detailsResponse = executeSessionRequest(session, protocol.CommandTypes.CompletionDetails, detailsRequestArgs); + const detailsCommon: protocol.CompletionEntryDetails & CompletionEntryDetails = { + displayParts: [ + keywordPart(SyntaxKind.ConstKeyword), + spacePart(), + displayPart("foo", SymbolDisplayPartKind.localName), + punctuationPart(SyntaxKind.ColonToken), + spacePart(), + displayPart("0", SymbolDisplayPartKind.stringLiteral), + ], + documentation: emptyArray, + kind: ScriptElementKind.constElement, + kindModifiers: ScriptElementKindModifier.exportedModifier, + name: "foo", + source: [{ text: "./a", kind: "text" }], + tags: undefined, + }; + assert.deepEqual(detailsResponse, [ + { + codeActions: [ + { + description: `Import 'foo' from module "./a"`, + changes: [ + { + fileName: "/b.ts", + textChanges: [ + { + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 1 }, + newText: 'import { foo } from "./a";\n\n', + }, + ], + }, + ], + commands: undefined, + }, ], - documentation: emptyArray, - kind: ScriptElementKind.constElement, - kindModifiers: ScriptElementKindModifier.exportedModifier, - name: "foo", - source: [{ text: "./a", kind: "text" }], - tags: undefined, - }; - assert.deepEqual(detailsResponse, [ - { - codeActions: [ - { - description: `Import 'foo' from module "./a"`, - changes: [ - { - fileName: "/b.ts", - textChanges: [ - { - start: { line: 1, offset: 1 }, - end: { line: 1, offset: 1 }, - newText: 'import { foo } from "./a";\n\n', - }, - ], - }, - ], - commands: undefined, - }, - ], - ...detailsCommon, - }, - ]); - - interface CompletionDetailsFullRequest extends protocol.FileLocationRequest { - readonly command: protocol.CommandTypes.CompletionDetailsFull; - readonly arguments: protocol.CompletionDetailsRequestArgs; - } - interface CompletionDetailsFullResponse extends protocol.Response { - readonly body?: readonly CompletionEntryDetails[]; + ...detailsCommon, + }, + ]); + interface CompletionDetailsFullRequest extends protocol.FileLocationRequest { + readonly command: protocol.CommandTypes.CompletionDetailsFull; + readonly arguments: protocol.CompletionDetailsRequestArgs; + } + interface CompletionDetailsFullResponse extends protocol.Response { + readonly body?: readonly CompletionEntryDetails[]; + } + const detailsFullResponse = executeSessionRequest(session, protocol.CommandTypes.CompletionDetailsFull, detailsRequestArgs); + assert.deepEqual(detailsFullResponse, [ + { + codeActions: [ + { + description: `Import 'foo' from module "./a"`, + changes: [ + { + fileName: "/b.ts", + textChanges: [createTextChange(createTextSpan(0, 0), 'import { foo } from "./a";\n\n')], + }, + ], + commands: undefined, + } + ], + ...detailsCommon, } - const detailsFullResponse = executeSessionRequest(session, protocol.CommandTypes.CompletionDetailsFull, detailsRequestArgs); - assert.deepEqual(detailsFullResponse, [ - { - codeActions: [ - { - description: `Import 'foo' from module "./a"`, - changes: [ - { - fileName: "/b.ts", - textChanges: [createTextChange(createTextSpan(0, 0), 'import { foo } from "./a";\n\n')], - }, - ], - commands: undefined, - } - ], - ...detailsCommon, + ]); + }); + it("works when files are included from two different drives of windows", () => { + const projectRoot = "e:/myproject"; + const appPackage: File = { + path: `${projectRoot}/package.json`, + content: JSON.stringify({ + name: "test", + version: "0.1.0", + dependencies: { + "react": "^16.12.0", + "react-router-dom": "^5.1.2", } - ]); - }); - - it("works when files are included from two different drives of windows", () => { - const projectRoot = "e:/myproject"; - const appPackage: File = { - path: `${projectRoot}/package.json`, - content: JSON.stringify({ - name: "test", - version: "0.1.0", - dependencies: { - "react": "^16.12.0", - "react-router-dom": "^5.1.2", - } - }) - }; - const appFile: File = { - path: `${projectRoot}/src/app.js`, - content: `import React from 'react'; + }) + }; + const appFile: File = { + path: `${projectRoot}/src/app.js`, + content: `import React from 'react'; import { BrowserRouter as Router, } from "react-router-dom"; ` - }; - const localNodeModules = `${projectRoot}/node_modules`; - const localAtTypes = `${localNodeModules}/@types`; - const localReactPackage: File = { - path: `${localAtTypes}/react/package.json`, - content: JSON.stringify({ - name: "@types/react", - version: "16.9.14", - }) - }; - const localReact: File = { - path: `${localAtTypes}/react/index.d.ts`, - content: `import * as PropTypes from 'prop-types'; + }; + const localNodeModules = `${projectRoot}/node_modules`; + const localAtTypes = `${localNodeModules}/@types`; + const localReactPackage: File = { + path: `${localAtTypes}/react/package.json`, + content: JSON.stringify({ + name: "@types/react", + version: "16.9.14", + }) + }; + const localReact: File = { + path: `${localAtTypes}/react/index.d.ts`, + content: `import * as PropTypes from 'prop-types'; ` - }; - const localReactRouterDomPackage: File = { - path: `${localNodeModules}/react-router-dom/package.json`, - content: JSON.stringify({ - name: "react-router-dom", - version: "5.1.2", - }) - }; - const localReactRouterDom: File = { - path: `${localNodeModules}/react-router-dom/index.js`, - content: `export function foo() {}` - }; - const localPropTypesPackage: File = { - path: `${localAtTypes}/prop-types/package.json`, - content: JSON.stringify({ - name: "@types/prop-types", - version: "15.7.3", - }) - }; - const localPropTypes: File = { - path: `${localAtTypes}/prop-types/index.d.ts`, - content: `export type ReactComponentLike = + }; + const localReactRouterDomPackage: File = { + path: `${localNodeModules}/react-router-dom/package.json`, + content: JSON.stringify({ + name: "react-router-dom", + version: "5.1.2", + }) + }; + const localReactRouterDom: File = { + path: `${localNodeModules}/react-router-dom/index.js`, + content: `export function foo() {}` + }; + const localPropTypesPackage: File = { + path: `${localAtTypes}/prop-types/package.json`, + content: JSON.stringify({ + name: "@types/prop-types", + version: "15.7.3", + }) + }; + const localPropTypes: File = { + path: `${localAtTypes}/prop-types/index.d.ts`, + content: `export type ReactComponentLike = | string | ((props: any, context?: any) => any) | (new (props: any, context?: any) => any); ` - }; - - const globalCacheLocation = `c:/typescript`; - const globalAtTypes = `${globalCacheLocation}/node_modules/@types`; - const globalReactRouterDomPackage: File = { - path: `${globalAtTypes}/react-router-dom/package.json`, - content: JSON.stringify({ - name: "@types/react-router-dom", - version: "5.1.2", - }) - }; - const globalReactRouterDom: File = { - path: `${globalAtTypes}/react-router-dom/index.d.ts`, - content: `import * as React from 'react'; + }; + const globalCacheLocation = `c:/typescript`; + const globalAtTypes = `${globalCacheLocation}/node_modules/@types`; + const globalReactRouterDomPackage: File = { + path: `${globalAtTypes}/react-router-dom/package.json`, + content: JSON.stringify({ + name: "@types/react-router-dom", + version: "5.1.2", + }) + }; + const globalReactRouterDom: File = { + path: `${globalAtTypes}/react-router-dom/index.d.ts`, + content: `import * as React from 'react'; export interface BrowserRouterProps { basename?: string; getUserConfirmation?: ((message: string, callback: (ok: boolean) => void) => void); forceRefresh?: boolean; keyLength?: number; }` - }; - const globalReactPackage: File = { - path: `${globalAtTypes}/react/package.json`, - content: localReactPackage.content - }; - const globalReact: File = { - path: `${globalAtTypes}/react/index.d.ts`, - content: localReact.content - }; - - const filesInProject = [ - appFile, - localReact, - localPropTypes, - globalReactRouterDom, - globalReact, - ]; - const files = [ - ...filesInProject, - appPackage, libFile, - localReactPackage, - localReactRouterDomPackage, localReactRouterDom, - localPropTypesPackage, - globalReactRouterDomPackage, - globalReactPackage, - ]; - - const host = createServerHost(files, { windowsStyleRoot: "c:/" }); - const session = createSession(host, { - typingsInstaller: new TestTypingsInstaller(globalCacheLocation, /*throttleLimit*/ 5, host), - }); - const service = session.getProjectService(); - openFilesForSession([appFile], session); - checkNumberOfProjects(service, { inferredProjects: 1 }); - const windowsStyleLibFilePath = "c:/" + libFile.path.substring(1); - checkProjectActualFiles(service.inferredProjects[0], filesInProject.map(f => f.path).concat(windowsStyleLibFilePath)); - session.executeCommandSeq({ - command: protocol.CommandTypes.CompletionInfo, - arguments: { - file: appFile.path, - line: 5, - offset: 1, - includeExternalModuleExports: true, - includeInsertTextCompletions: true - } - }); + }; + const globalReactPackage: File = { + path: `${globalAtTypes}/react/package.json`, + content: localReactPackage.content + }; + const globalReact: File = { + path: `${globalAtTypes}/react/index.d.ts`, + content: localReact.content + }; + const filesInProject = [ + appFile, + localReact, + localPropTypes, + globalReactRouterDom, + globalReact, + ]; + const files = [ + ...filesInProject, + appPackage, + libFile, + localReactPackage, + localReactRouterDomPackage, localReactRouterDom, + localPropTypesPackage, + globalReactRouterDomPackage, + globalReactPackage, + ]; + const host = createServerHost(files, { windowsStyleRoot: "c:/" }); + const session = createSession(host, { + typingsInstaller: new TestTypingsInstaller(globalCacheLocation, /*throttleLimit*/ 5, host), + }); + const service = session.getProjectService(); + openFilesForSession([appFile], session); + checkNumberOfProjects(service, { inferredProjects: 1 }); + const windowsStyleLibFilePath = "c:/" + libFile.path.substring(1); + checkProjectActualFiles(service.inferredProjects[0], filesInProject.map(f => f.path).concat(windowsStyleLibFilePath)); + session.executeCommandSeq({ + command: protocol.CommandTypes.CompletionInfo, + arguments: { + file: appFile.path, + line: 5, + offset: 1, + includeExternalModuleExports: true, + includeInsertTextCompletions: true + } }); }); -} +}); diff --git a/src/testRunner/unittests/tsserver/configFileSearch.ts b/src/testRunner/unittests/tsserver/configFileSearch.ts index 5b8bce403c5dc..d4a6d8dacfb8e 100644 --- a/src/testRunner/unittests/tsserver/configFileSearch.ts +++ b/src/testRunner/unittests/tsserver/configFileSearch.ts @@ -1,190 +1,167 @@ -namespace ts.projectSystem { - describe("unittests:: tsserver:: searching for config file", () => { - it("should stop at projectRootPath if given", () => { - const f1 = { - path: "/a/file1.ts", - content: "" - }; - const configFile = { - path: "/tsconfig.json", - content: "{}" - }; - const host = createServerHost([f1, configFile]); - const service = createProjectService(host); - service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, "/a"); - - checkNumberOfConfiguredProjects(service, 0); - checkNumberOfInferredProjects(service, 1); - - service.closeClientFile(f1.path); - service.openClientFile(f1.path); - checkNumberOfConfiguredProjects(service, 1); - checkNumberOfInferredProjects(service, 0); - }); - - it("should use projectRootPath when searching for inferred project again", () => { - const projectDir = "/a/b/projects/project"; - const configFileLocation = `${projectDir}/src`; - const f1 = { - path: `${configFileLocation}/file1.ts`, - content: "" - }; - const configFile = { - path: `${configFileLocation}/tsconfig.json`, - content: "{}" - }; - const configFile2 = { - path: "/a/b/projects/tsconfig.json", - content: "{}" - }; - const host = createServerHost([f1, libFile, configFile, configFile2]); - const service = createProjectService(host); - service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectDir); - checkNumberOfProjects(service, { configuredProjects: 1 }); - assert.isDefined(service.configuredProjects.get(configFile.path)); - checkWatchedFiles(host, [libFile.path, configFile.path]); - checkWatchedDirectories(host, [], /*recursive*/ false); - const typeRootLocations = getTypeRootsFromLocation(configFileLocation); - checkWatchedDirectories(host, typeRootLocations.concat(configFileLocation), /*recursive*/ true); - - // Delete config file - should create inferred project and not configured project - host.reloadFS([f1, libFile, configFile2]); +import { createServerHost, createProjectService, checkNumberOfConfiguredProjects, checkNumberOfInferredProjects, libFile, checkNumberOfProjects, checkWatchedFiles, checkWatchedDirectories, getTypeRootsFromLocation, File, TestServerHost, TestProjectService, checkProjectActualFiles, getConfigFilesToWatch, checkWatchedFilesDetailed } from "../../ts.projectSystem"; +import { getDirectoryPath, Debug, emptyArray } from "../../ts"; +describe("unittests:: tsserver:: searching for config file", () => { + it("should stop at projectRootPath if given", () => { + const f1 = { + path: "/a/file1.ts", + content: "" + }; + const configFile = { + path: "/tsconfig.json", + content: "{}" + }; + const host = createServerHost([f1, configFile]); + const service = createProjectService(host); + service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, "/a"); + checkNumberOfConfiguredProjects(service, 0); + checkNumberOfInferredProjects(service, 1); + service.closeClientFile(f1.path); + service.openClientFile(f1.path); + checkNumberOfConfiguredProjects(service, 1); + checkNumberOfInferredProjects(service, 0); + }); + it("should use projectRootPath when searching for inferred project again", () => { + const projectDir = "/a/b/projects/project"; + const configFileLocation = `${projectDir}/src`; + const f1 = { + path: `${configFileLocation}/file1.ts`, + content: "" + }; + const configFile = { + path: `${configFileLocation}/tsconfig.json`, + content: "{}" + }; + const configFile2 = { + path: "/a/b/projects/tsconfig.json", + content: "{}" + }; + const host = createServerHost([f1, libFile, configFile, configFile2]); + const service = createProjectService(host); + service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectDir); + checkNumberOfProjects(service, { configuredProjects: 1 }); + assert.isDefined(service.configuredProjects.get(configFile.path)); + checkWatchedFiles(host, [libFile.path, configFile.path]); + checkWatchedDirectories(host, [], /*recursive*/ false); + const typeRootLocations = getTypeRootsFromLocation(configFileLocation); + checkWatchedDirectories(host, typeRootLocations.concat(configFileLocation), /*recursive*/ true); + // Delete config file - should create inferred project and not configured project + host.reloadFS([f1, libFile, configFile2]); + host.runQueuedTimeoutCallbacks(); + checkNumberOfProjects(service, { inferredProjects: 1 }); + checkWatchedFiles(host, [libFile.path, configFile.path, `${configFileLocation}/jsconfig.json`, `${projectDir}/tsconfig.json`, `${projectDir}/jsconfig.json`]); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, typeRootLocations, /*recursive*/ true); + }); + it("should use projectRootPath when searching for inferred project again 2", () => { + const projectDir = "/a/b/projects/project"; + const configFileLocation = `${projectDir}/src`; + const f1 = { + path: `${configFileLocation}/file1.ts`, + content: "" + }; + const configFile = { + path: `${configFileLocation}/tsconfig.json`, + content: "{}" + }; + const configFile2 = { + path: "/a/b/projects/tsconfig.json", + content: "{}" + }; + const host = createServerHost([f1, libFile, configFile, configFile2]); + const service = createProjectService(host, { useSingleInferredProject: true }, { useInferredProjectPerProjectRoot: true }); + service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectDir); + checkNumberOfProjects(service, { configuredProjects: 1 }); + assert.isDefined(service.configuredProjects.get(configFile.path)); + checkWatchedFiles(host, [libFile.path, configFile.path]); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, getTypeRootsFromLocation(configFileLocation).concat(configFileLocation), /*recursive*/ true); + // Delete config file - should create inferred project with project root path set + host.reloadFS([f1, libFile, configFile2]); + host.runQueuedTimeoutCallbacks(); + checkNumberOfProjects(service, { inferredProjects: 1 }); + assert.equal(service.inferredProjects[0].projectRootPath, projectDir); + checkWatchedFiles(host, [libFile.path, configFile.path, `${configFileLocation}/jsconfig.json`, `${projectDir}/tsconfig.json`, `${projectDir}/jsconfig.json`]); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, getTypeRootsFromLocation(projectDir), /*recursive*/ true); + }); + describe("when the opened file is not from project root", () => { + const projectRoot = "/a/b/projects/project"; + const file: File = { + path: `${projectRoot}/src/index.ts`, + content: "let y = 10" + }; + const tsconfig: File = { + path: `${projectRoot}/tsconfig.json`, + content: "{}" + }; + const files = [file, libFile]; + const filesWithConfig = files.concat(tsconfig); + const dirOfFile = getDirectoryPath(file.path); + function openClientFile(files: File[]) { + const host = createServerHost(files); + const projectService = createProjectService(host); + projectService.openClientFile(file.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, "/a/b/projects/proj"); + return { host, projectService }; + } + function verifyConfiguredProject(host: TestServerHost, projectService: TestProjectService, orphanInferredProject?: boolean) { + projectService.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: orphanInferredProject ? 1 : 0 }); + const project = Debug.checkDefined(projectService.configuredProjects.get(tsconfig.path)); + if (orphanInferredProject) { + const inferredProject = projectService.inferredProjects[0]; + assert.isTrue(inferredProject.isOrphan()); + } + checkProjectActualFiles(project, [file.path, libFile.path, tsconfig.path]); + checkWatchedFiles(host, [libFile.path, tsconfig.path]); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectories(host, (orphanInferredProject ? [projectRoot, `${dirOfFile}/node_modules/@types`] : [projectRoot]).concat(getTypeRootsFromLocation(projectRoot)), /*recursive*/ true); + } + function verifyInferredProject(host: TestServerHost, projectService: TestProjectService) { + projectService.checkNumberOfProjects({ inferredProjects: 1 }); + const project = projectService.inferredProjects[0]; + assert.isDefined(project); + const filesToWatch = [libFile.path, ...getConfigFilesToWatch(dirOfFile)]; + checkProjectActualFiles(project, [file.path, libFile.path]); + checkWatchedFiles(host, filesToWatch); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectories(host, getTypeRootsFromLocation(dirOfFile), /*recursive*/ true); + } + it("tsconfig for the file exists", () => { + const { host, projectService } = openClientFile(filesWithConfig); + verifyConfiguredProject(host, projectService); + host.reloadFS(files); host.runQueuedTimeoutCallbacks(); - checkNumberOfProjects(service, { inferredProjects: 1 }); - checkWatchedFiles(host, [libFile.path, configFile.path, `${configFileLocation}/jsconfig.json`, `${projectDir}/tsconfig.json`, `${projectDir}/jsconfig.json`]); - checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, typeRootLocations, /*recursive*/ true); + verifyInferredProject(host, projectService); + host.reloadFS(filesWithConfig); + host.runQueuedTimeoutCallbacks(); + verifyConfiguredProject(host, projectService, /*orphanInferredProject*/ true); }); - - it("should use projectRootPath when searching for inferred project again 2", () => { - const projectDir = "/a/b/projects/project"; - const configFileLocation = `${projectDir}/src`; - const f1 = { - path: `${configFileLocation}/file1.ts`, - content: "" - }; - const configFile = { - path: `${configFileLocation}/tsconfig.json`, - content: "{}" - }; - const configFile2 = { - path: "/a/b/projects/tsconfig.json", - content: "{}" - }; - const host = createServerHost([f1, libFile, configFile, configFile2]); - const service = createProjectService(host, { useSingleInferredProject: true }, { useInferredProjectPerProjectRoot: true }); - service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectDir); - checkNumberOfProjects(service, { configuredProjects: 1 }); - assert.isDefined(service.configuredProjects.get(configFile.path)); - checkWatchedFiles(host, [libFile.path, configFile.path]); - checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, getTypeRootsFromLocation(configFileLocation).concat(configFileLocation), /*recursive*/ true); - - // Delete config file - should create inferred project with project root path set - host.reloadFS([f1, libFile, configFile2]); + it("tsconfig for the file does not exist", () => { + const { host, projectService } = openClientFile(files); + verifyInferredProject(host, projectService); + host.reloadFS(filesWithConfig); host.runQueuedTimeoutCallbacks(); - checkNumberOfProjects(service, { inferredProjects: 1 }); - assert.equal(service.inferredProjects[0].projectRootPath, projectDir); - checkWatchedFiles(host, [libFile.path, configFile.path, `${configFileLocation}/jsconfig.json`, `${projectDir}/tsconfig.json`, `${projectDir}/jsconfig.json`]); - checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, getTypeRootsFromLocation(projectDir), /*recursive*/ true); + verifyConfiguredProject(host, projectService, /*orphanInferredProject*/ true); + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + verifyInferredProject(host, projectService); }); - - describe("when the opened file is not from project root", () => { - const projectRoot = "/a/b/projects/project"; - const file: File = { - path: `${projectRoot}/src/index.ts`, - content: "let y = 10" - }; - const tsconfig: File = { - path: `${projectRoot}/tsconfig.json`, - content: "{}" - }; - const files = [file, libFile]; - const filesWithConfig = files.concat(tsconfig); - const dirOfFile = getDirectoryPath(file.path); - - function openClientFile(files: File[]) { - const host = createServerHost(files); - const projectService = createProjectService(host); - - projectService.openClientFile(file.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, "/a/b/projects/proj"); - return { host, projectService }; - } - - function verifyConfiguredProject(host: TestServerHost, projectService: TestProjectService, orphanInferredProject?: boolean) { - projectService.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: orphanInferredProject ? 1 : 0 }); - const project = Debug.checkDefined(projectService.configuredProjects.get(tsconfig.path)); - - if (orphanInferredProject) { - const inferredProject = projectService.inferredProjects[0]; - assert.isTrue(inferredProject.isOrphan()); - } - - checkProjectActualFiles(project, [file.path, libFile.path, tsconfig.path]); - checkWatchedFiles(host, [libFile.path, tsconfig.path]); - checkWatchedDirectories(host, emptyArray, /*recursive*/ false); - checkWatchedDirectories(host, (orphanInferredProject ? [projectRoot, `${dirOfFile}/node_modules/@types`] : [projectRoot]).concat(getTypeRootsFromLocation(projectRoot)), /*recursive*/ true); - } - - function verifyInferredProject(host: TestServerHost, projectService: TestProjectService) { - projectService.checkNumberOfProjects({ inferredProjects: 1 }); - const project = projectService.inferredProjects[0]; - assert.isDefined(project); - - const filesToWatch = [libFile.path, ...getConfigFilesToWatch(dirOfFile)]; - - checkProjectActualFiles(project, [file.path, libFile.path]); - checkWatchedFiles(host, filesToWatch); - checkWatchedDirectories(host, emptyArray, /*recursive*/ false); - checkWatchedDirectories(host, getTypeRootsFromLocation(dirOfFile), /*recursive*/ true); - } - - it("tsconfig for the file exists", () => { - const { host, projectService } = openClientFile(filesWithConfig); - verifyConfiguredProject(host, projectService); - - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - verifyInferredProject(host, projectService); - - host.reloadFS(filesWithConfig); - host.runQueuedTimeoutCallbacks(); - verifyConfiguredProject(host, projectService, /*orphanInferredProject*/ true); - }); - - it("tsconfig for the file does not exist", () => { - const { host, projectService } = openClientFile(files); - verifyInferredProject(host, projectService); - - host.reloadFS(filesWithConfig); - host.runQueuedTimeoutCallbacks(); - verifyConfiguredProject(host, projectService, /*orphanInferredProject*/ true); - - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - verifyInferredProject(host, projectService); - }); + }); + describe("should not search and watch config files from directories that cannot be watched", () => { + const root = "/root/teams/VSCode68/Shared Documents/General/jt-ts-test-workspace"; + function verifyConfigFileWatch(projectRootPath: string | undefined) { + const path = `${root}/x.js`; + const host = createServerHost([libFile, { path, content: "const x = 10" }], { useCaseSensitiveFileNames: true }); + const service = createProjectService(host); + service.openClientFile(path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectRootPath); + checkNumberOfProjects(service, { inferredProjects: 1 }); + checkProjectActualFiles(service.inferredProjects[0], [path, libFile.path]); + checkWatchedFilesDetailed(host, [libFile.path, ...getConfigFilesToWatch(root)], 1); + } + it("when projectRootPath is not present", () => { + verifyConfigFileWatch(/*projectRootPath*/ undefined); }); - - describe("should not search and watch config files from directories that cannot be watched", () => { - const root = "/root/teams/VSCode68/Shared Documents/General/jt-ts-test-workspace"; - function verifyConfigFileWatch(projectRootPath: string | undefined) { - const path = `${root}/x.js`; - const host = createServerHost([libFile, { path, content: "const x = 10" }], { useCaseSensitiveFileNames: true }); - const service = createProjectService(host); - service.openClientFile(path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectRootPath); - checkNumberOfProjects(service, { inferredProjects: 1 }); - checkProjectActualFiles(service.inferredProjects[0], [path, libFile.path]); - checkWatchedFilesDetailed(host, [libFile.path, ...getConfigFilesToWatch(root)], 1); - } - - it("when projectRootPath is not present", () => { - verifyConfigFileWatch(/*projectRootPath*/ undefined); - }); - it("when projectRootPath is present but file is not from project root", () => { - verifyConfigFileWatch("/a/b"); - }); + it("when projectRootPath is present but file is not from project root", () => { + verifyConfigFileWatch("/a/b"); }); }); -} +}); diff --git a/src/testRunner/unittests/tsserver/configuredProjects.ts b/src/testRunner/unittests/tsserver/configuredProjects.ts index 06bd87fa0dd20..d9d0675ed1866 100644 --- a/src/testRunner/unittests/tsserver/configuredProjects.ts +++ b/src/testRunner/unittests/tsserver/configuredProjects.ts @@ -1,1236 +1,1121 @@ -namespace ts.projectSystem { - describe("unittests:: tsserver:: ConfiguredProjects", () => { - it("create configured project without file list", () => { - const configFile: File = { - path: "/a/b/tsconfig.json", - content: ` +import { File, createServerHost, libFile, createProjectService, checkNumberOfInferredProjects, checkNumberOfConfiguredProjects, configuredProjectAt, checkProjectActualFiles, checkProjectRootFiles, checkWatchedFiles, checkWatchedDirectories, nodeModulesAtTypes, getConfigFilesToWatch, commonFile1, commonFile2, checkNumberOfProjects, checkOpenFiles, createSessionWithEventTracking, protocol, SymLink, createSession, openFilesForSession, verifyGetErrRequest, checkWatchedDirectoriesDetailed, getTypeRootsFromLocation } from "../../ts.projectSystem"; +import { getDirectoryPath, combinePaths, Path, find, createTextSpan, map, mapDefined, emptyArray } from "../../ts"; +import { projectRoot } from "../../ts.tscWatch"; +import { maxProgramSizeForNonTsFiles, toNormalizedPath, ProjectLanguageServiceStateEvent, NormalizedPath, ProjectService } from "../../ts.server"; +describe("unittests:: tsserver:: ConfiguredProjects", () => { + it("create configured project without file list", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: ` { "compilerOptions": {}, "exclude": [ "e" ] }` - }; - const file1: File = { - path: "/a/b/c/f1.ts", - content: "let x = 1" - }; - const file2: File = { - path: "/a/b/d/f2.ts", - content: "let y = 1" - }; - const file3: File = { - path: "/a/b/e/f3.ts", - content: "let z = 1" - }; - - const host = createServerHost([configFile, libFile, file1, file2, file3]); - const projectService = createProjectService(host); - const { configFileName, configFileErrors } = projectService.openClientFile(file1.path); - - assert(configFileName, "should find config file"); - assert.isTrue(!configFileErrors || configFileErrors.length === 0, `expect no errors in config file, got ${JSON.stringify(configFileErrors)}`); - checkNumberOfInferredProjects(projectService, 0); - checkNumberOfConfiguredProjects(projectService, 1); - - const project = configuredProjectAt(projectService, 0); - checkProjectActualFiles(project, [file1.path, libFile.path, file2.path, configFile.path]); - checkProjectRootFiles(project, [file1.path, file2.path]); - // watching all files except one that was open - checkWatchedFiles(host, [configFile.path, file2.path, libFile.path]); - const configFileDirectory = getDirectoryPath(configFile.path); - checkWatchedDirectories(host, [configFileDirectory, combinePaths(configFileDirectory, nodeModulesAtTypes)], /*recursive*/ true); - }); - - it("create configured project with the file list", () => { - const configFile: File = { - path: "/a/b/tsconfig.json", - content: ` + }; + const file1: File = { + path: "/a/b/c/f1.ts", + content: "let x = 1" + }; + const file2: File = { + path: "/a/b/d/f2.ts", + content: "let y = 1" + }; + const file3: File = { + path: "/a/b/e/f3.ts", + content: "let z = 1" + }; + const host = createServerHost([configFile, libFile, file1, file2, file3]); + const projectService = createProjectService(host); + const { configFileName, configFileErrors } = projectService.openClientFile(file1.path); + assert(configFileName, "should find config file"); + assert.isTrue(!configFileErrors || configFileErrors.length === 0, `expect no errors in config file, got ${JSON.stringify(configFileErrors)}`); + checkNumberOfInferredProjects(projectService, 0); + checkNumberOfConfiguredProjects(projectService, 1); + const project = configuredProjectAt(projectService, 0); + checkProjectActualFiles(project, [file1.path, libFile.path, file2.path, configFile.path]); + checkProjectRootFiles(project, [file1.path, file2.path]); + // watching all files except one that was open + checkWatchedFiles(host, [configFile.path, file2.path, libFile.path]); + const configFileDirectory = getDirectoryPath(configFile.path); + checkWatchedDirectories(host, [configFileDirectory, combinePaths(configFileDirectory, nodeModulesAtTypes)], /*recursive*/ true); + }); + it("create configured project with the file list", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: ` { "compilerOptions": {}, "include": ["*.ts"] }` - }; - const file1: File = { - path: "/a/b/f1.ts", - content: "let x = 1" - }; - const file2: File = { - path: "/a/b/f2.ts", - content: "let y = 1" - }; - const file3: File = { - path: "/a/b/c/f3.ts", - content: "let z = 1" - }; - - const host = createServerHost([configFile, libFile, file1, file2, file3]); - const projectService = createProjectService(host); - const { configFileName, configFileErrors } = projectService.openClientFile(file1.path); - - assert(configFileName, "should find config file"); - assert.isTrue(!configFileErrors || configFileErrors.length === 0, `expect no errors in config file, got ${JSON.stringify(configFileErrors)}`); - checkNumberOfInferredProjects(projectService, 0); - checkNumberOfConfiguredProjects(projectService, 1); - - const project = configuredProjectAt(projectService, 0); - checkProjectActualFiles(project, [file1.path, libFile.path, file2.path, configFile.path]); - checkProjectRootFiles(project, [file1.path, file2.path]); - // watching all files except one that was open - checkWatchedFiles(host, [configFile.path, file2.path, libFile.path]); - checkWatchedDirectories(host, [getDirectoryPath(configFile.path)], /*recursive*/ false); - }); - - it("add and then remove a config file in a folder with loose files", () => { - const configFile: File = { - path: `${tscWatch.projectRoot}/tsconfig.json`, - content: `{ + }; + const file1: File = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2: File = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const file3: File = { + path: "/a/b/c/f3.ts", + content: "let z = 1" + }; + const host = createServerHost([configFile, libFile, file1, file2, file3]); + const projectService = createProjectService(host); + const { configFileName, configFileErrors } = projectService.openClientFile(file1.path); + assert(configFileName, "should find config file"); + assert.isTrue(!configFileErrors || configFileErrors.length === 0, `expect no errors in config file, got ${JSON.stringify(configFileErrors)}`); + checkNumberOfInferredProjects(projectService, 0); + checkNumberOfConfiguredProjects(projectService, 1); + const project = configuredProjectAt(projectService, 0); + checkProjectActualFiles(project, [file1.path, libFile.path, file2.path, configFile.path]); + checkProjectRootFiles(project, [file1.path, file2.path]); + // watching all files except one that was open + checkWatchedFiles(host, [configFile.path, file2.path, libFile.path]); + checkWatchedDirectories(host, [getDirectoryPath(configFile.path)], /*recursive*/ false); + }); + it("add and then remove a config file in a folder with loose files", () => { + const configFile: File = { + path: `${projectRoot}/tsconfig.json`, + content: `{ "files": ["commonFile1.ts"] }` - }; - const commonFile1: File = { - path: `${tscWatch.projectRoot}/commonFile1.ts`, - content: "let x = 1" - }; - const commonFile2: File = { - path: `${tscWatch.projectRoot}/commonFile2.ts`, - content: "let y = 1" - }; - - const filesWithoutConfig = [libFile, commonFile1, commonFile2]; - const host = createServerHost(filesWithoutConfig); - - const filesWithConfig = [libFile, commonFile1, commonFile2, configFile]; - const projectService = createProjectService(host); - projectService.openClientFile(commonFile1.path); - projectService.openClientFile(commonFile2.path); - - projectService.checkNumberOfProjects({ inferredProjects: 2 }); - checkProjectActualFiles(projectService.inferredProjects[0], [commonFile1.path, libFile.path]); - checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, libFile.path]); - - const watchedFiles = getConfigFilesToWatch(tscWatch.projectRoot).concat(libFile.path); - checkWatchedFiles(host, watchedFiles); - - // Add a tsconfig file - host.reloadFS(filesWithConfig); - host.checkTimeoutQueueLengthAndRun(2); // load configured project from disk + ensureProjectsForOpenFiles - - projectService.checkNumberOfProjects({ inferredProjects: 2, configuredProjects: 1 }); - assert.isTrue(projectService.inferredProjects[0].isOrphan()); - checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, libFile.path]); - checkProjectActualFiles(projectService.configuredProjects.get(configFile.path)!, [libFile.path, commonFile1.path, configFile.path]); - - checkWatchedFiles(host, watchedFiles); - - // remove the tsconfig file - host.reloadFS(filesWithoutConfig); - - projectService.checkNumberOfProjects({ inferredProjects: 2 }); - assert.isTrue(projectService.inferredProjects[0].isOrphan()); - checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, libFile.path]); - - host.checkTimeoutQueueLengthAndRun(1); // Refresh inferred projects - - projectService.checkNumberOfProjects({ inferredProjects: 2 }); - checkProjectActualFiles(projectService.inferredProjects[0], [commonFile1.path, libFile.path]); - checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, libFile.path]); - checkWatchedFiles(host, watchedFiles); - }); - - it("add new files to a configured project without file list", () => { - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{}` - }; - const host = createServerHost([commonFile1, libFile, configFile]); - const projectService = createProjectService(host); - projectService.openClientFile(commonFile1.path); - const configFileDir = getDirectoryPath(configFile.path); - checkWatchedDirectories(host, [configFileDir, combinePaths(configFileDir, nodeModulesAtTypes)], /*recursive*/ true); - checkNumberOfConfiguredProjects(projectService, 1); - - const project = configuredProjectAt(projectService, 0); - checkProjectRootFiles(project, [commonFile1.path]); - - // add a new ts file - host.reloadFS([commonFile1, commonFile2, libFile, configFile]); - host.checkTimeoutQueueLengthAndRun(2); - // project service waits for 250ms to update the project structure, therefore the assertion needs to wait longer. - checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); - }); - - it("should ignore non-existing files specified in the config file", () => { - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{ + }; + const commonFile1: File = { + path: `${projectRoot}/commonFile1.ts`, + content: "let x = 1" + }; + const commonFile2: File = { + path: `${projectRoot}/commonFile2.ts`, + content: "let y = 1" + }; + const filesWithoutConfig = [libFile, commonFile1, commonFile2]; + const host = createServerHost(filesWithoutConfig); + const filesWithConfig = [libFile, commonFile1, commonFile2, configFile]; + const projectService = createProjectService(host); + projectService.openClientFile(commonFile1.path); + projectService.openClientFile(commonFile2.path); + projectService.checkNumberOfProjects({ inferredProjects: 2 }); + checkProjectActualFiles(projectService.inferredProjects[0], [commonFile1.path, libFile.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, libFile.path]); + const watchedFiles = getConfigFilesToWatch(projectRoot).concat(libFile.path); + checkWatchedFiles(host, watchedFiles); + // Add a tsconfig file + host.reloadFS(filesWithConfig); + host.checkTimeoutQueueLengthAndRun(2); // load configured project from disk + ensureProjectsForOpenFiles + projectService.checkNumberOfProjects({ inferredProjects: 2, configuredProjects: 1 }); + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, libFile.path]); + checkProjectActualFiles((projectService.configuredProjects.get(configFile.path)!), [libFile.path, commonFile1.path, configFile.path]); + checkWatchedFiles(host, watchedFiles); + // remove the tsconfig file + host.reloadFS(filesWithoutConfig); + projectService.checkNumberOfProjects({ inferredProjects: 2 }); + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, libFile.path]); + host.checkTimeoutQueueLengthAndRun(1); // Refresh inferred projects + projectService.checkNumberOfProjects({ inferredProjects: 2 }); + checkProjectActualFiles(projectService.inferredProjects[0], [commonFile1.path, libFile.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [commonFile2.path, libFile.path]); + checkWatchedFiles(host, watchedFiles); + }); + it("add new files to a configured project without file list", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const host = createServerHost([commonFile1, libFile, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(commonFile1.path); + const configFileDir = getDirectoryPath(configFile.path); + checkWatchedDirectories(host, [configFileDir, combinePaths(configFileDir, nodeModulesAtTypes)], /*recursive*/ true); + checkNumberOfConfiguredProjects(projectService, 1); + const project = configuredProjectAt(projectService, 0); + checkProjectRootFiles(project, [commonFile1.path]); + // add a new ts file + host.reloadFS([commonFile1, commonFile2, libFile, configFile]); + host.checkTimeoutQueueLengthAndRun(2); + // project service waits for 250ms to update the project structure, therefore the assertion needs to wait longer. + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + }); + it("should ignore non-existing files specified in the config file", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ "compilerOptions": {}, "files": [ "commonFile1.ts", "commonFile3.ts" ] }` - }; - const host = createServerHost([commonFile1, commonFile2, configFile]); - const projectService = createProjectService(host); - projectService.openClientFile(commonFile1.path); - projectService.openClientFile(commonFile2.path); - - checkNumberOfConfiguredProjects(projectService, 1); - const project = configuredProjectAt(projectService, 0); - checkProjectRootFiles(project, [commonFile1.path]); - checkNumberOfInferredProjects(projectService, 1); - }); - - it("handle recreated files correctly", () => { - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{}` - }; - const host = createServerHost([commonFile1, commonFile2, configFile]); - const projectService = createProjectService(host); - projectService.openClientFile(commonFile1.path); - - checkNumberOfConfiguredProjects(projectService, 1); - const project = configuredProjectAt(projectService, 0); - checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); - - // delete commonFile2 - host.reloadFS([commonFile1, configFile]); - host.checkTimeoutQueueLengthAndRun(2); - checkProjectRootFiles(project, [commonFile1.path]); - - // re-add commonFile2 - host.reloadFS([commonFile1, commonFile2, configFile]); - host.checkTimeoutQueueLengthAndRun(2); - checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); - }); - - it("files explicitly excluded in config file", () => { - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{ + }; + const host = createServerHost([commonFile1, commonFile2, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(commonFile1.path); + projectService.openClientFile(commonFile2.path); + checkNumberOfConfiguredProjects(projectService, 1); + const project = configuredProjectAt(projectService, 0); + checkProjectRootFiles(project, [commonFile1.path]); + checkNumberOfInferredProjects(projectService, 1); + }); + it("handle recreated files correctly", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const host = createServerHost([commonFile1, commonFile2, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(commonFile1.path); + checkNumberOfConfiguredProjects(projectService, 1); + const project = configuredProjectAt(projectService, 0); + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + // delete commonFile2 + host.reloadFS([commonFile1, configFile]); + host.checkTimeoutQueueLengthAndRun(2); + checkProjectRootFiles(project, [commonFile1.path]); + // re-add commonFile2 + host.reloadFS([commonFile1, commonFile2, configFile]); + host.checkTimeoutQueueLengthAndRun(2); + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + }); + it("files explicitly excluded in config file", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ "compilerOptions": {}, "exclude": ["/a/c"] }` - }; - const excludedFile1: File = { - path: "/a/c/excluedFile1.ts", - content: `let t = 1;` - }; - - const host = createServerHost([commonFile1, commonFile2, excludedFile1, configFile]); - const projectService = createProjectService(host); - - projectService.openClientFile(commonFile1.path); - checkNumberOfConfiguredProjects(projectService, 1); - const project = configuredProjectAt(projectService, 0); - checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); - projectService.openClientFile(excludedFile1.path); - checkNumberOfInferredProjects(projectService, 1); - }); - - it("should properly handle module resolution changes in config file", () => { - const file1: File = { - path: "/a/b/file1.ts", - content: `import { T } from "module1";` - }; - const nodeModuleFile: File = { - path: "/a/b/node_modules/module1.ts", - content: `export interface T {}` - }; - const classicModuleFile: File = { - path: "/a/module1.ts", - content: `export interface T {}` - }; - const randomFile: File = { - path: "/a/file1.ts", - content: `export interface T {}` - }; - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{ + }; + const excludedFile1: File = { + path: "/a/c/excluedFile1.ts", + content: `let t = 1;` + }; + const host = createServerHost([commonFile1, commonFile2, excludedFile1, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(commonFile1.path); + checkNumberOfConfiguredProjects(projectService, 1); + const project = configuredProjectAt(projectService, 0); + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + projectService.openClientFile(excludedFile1.path); + checkNumberOfInferredProjects(projectService, 1); + }); + it("should properly handle module resolution changes in config file", () => { + const file1: File = { + path: "/a/b/file1.ts", + content: `import { T } from "module1";` + }; + const nodeModuleFile: File = { + path: "/a/b/node_modules/module1.ts", + content: `export interface T {}` + }; + const classicModuleFile: File = { + path: "/a/module1.ts", + content: `export interface T {}` + }; + const randomFile: File = { + path: "/a/file1.ts", + content: `export interface T {}` + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ "compilerOptions": { "moduleResolution": "node" }, "files": ["${file1.path}"] }` - }; - const files = [file1, nodeModuleFile, classicModuleFile, configFile, randomFile]; - const host = createServerHost(files); - const projectService = createProjectService(host); - projectService.openClientFile(file1.path); - projectService.openClientFile(nodeModuleFile.path); - projectService.openClientFile(classicModuleFile.path); - - checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); - const project = configuredProjectAt(projectService, 0); - const inferredProject0 = projectService.inferredProjects[0]; - checkProjectActualFiles(project, [file1.path, nodeModuleFile.path, configFile.path]); - checkProjectActualFiles(projectService.inferredProjects[0], [classicModuleFile.path]); - - configFile.content = `{ + }; + const files = [file1, nodeModuleFile, classicModuleFile, configFile, randomFile]; + const host = createServerHost(files); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + projectService.openClientFile(nodeModuleFile.path); + projectService.openClientFile(classicModuleFile.path); + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); + const project = configuredProjectAt(projectService, 0); + const inferredProject0 = projectService.inferredProjects[0]; + checkProjectActualFiles(project, [file1.path, nodeModuleFile.path, configFile.path]); + checkProjectActualFiles(projectService.inferredProjects[0], [classicModuleFile.path]); + configFile.content = `{ "compilerOptions": { "moduleResolution": "classic" }, "files": ["${file1.path}"] }`; - host.reloadFS(files); - host.checkTimeoutQueueLengthAndRun(2); - - checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); // will not remove project 1 - checkProjectActualFiles(project, [file1.path, classicModuleFile.path, configFile.path]); - assert.strictEqual(projectService.inferredProjects[0], inferredProject0); - assert.isTrue(projectService.inferredProjects[0].isOrphan()); - const inferredProject1 = projectService.inferredProjects[1]; - checkProjectActualFiles(projectService.inferredProjects[1], [nodeModuleFile.path]); - - // Open random file and it will reuse first inferred project - projectService.openClientFile(randomFile.path); - checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); - checkProjectActualFiles(project, [file1.path, classicModuleFile.path, configFile.path]); - assert.strictEqual(projectService.inferredProjects[0], inferredProject0); - checkProjectActualFiles(projectService.inferredProjects[0], [randomFile.path]); // Reuses first inferred project - assert.strictEqual(projectService.inferredProjects[1], inferredProject1); - checkProjectActualFiles(projectService.inferredProjects[1], [nodeModuleFile.path]); - }); - - it("should keep the configured project when the opened file is referenced by the project but not its root", () => { - const file1: File = { - path: "/a/b/main.ts", - content: "import { objA } from './obj-a';" - }; - const file2: File = { - path: "/a/b/obj-a.ts", - content: `export const objA = Object.assign({foo: "bar"}, {bar: "baz"});` - }; - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{ + host.reloadFS(files); + host.checkTimeoutQueueLengthAndRun(2); + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); // will not remove project 1 + checkProjectActualFiles(project, [file1.path, classicModuleFile.path, configFile.path]); + assert.strictEqual(projectService.inferredProjects[0], inferredProject0); + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + const inferredProject1 = projectService.inferredProjects[1]; + checkProjectActualFiles(projectService.inferredProjects[1], [nodeModuleFile.path]); + // Open random file and it will reuse first inferred project + projectService.openClientFile(randomFile.path); + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); + checkProjectActualFiles(project, [file1.path, classicModuleFile.path, configFile.path]); + assert.strictEqual(projectService.inferredProjects[0], inferredProject0); + checkProjectActualFiles(projectService.inferredProjects[0], [randomFile.path]); // Reuses first inferred project + assert.strictEqual(projectService.inferredProjects[1], inferredProject1); + checkProjectActualFiles(projectService.inferredProjects[1], [nodeModuleFile.path]); + }); + it("should keep the configured project when the opened file is referenced by the project but not its root", () => { + const file1: File = { + path: "/a/b/main.ts", + content: "import { objA } from './obj-a';" + }; + const file2: File = { + path: "/a/b/obj-a.ts", + content: `export const objA = Object.assign({foo: "bar"}, {bar: "baz"});` + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ "compilerOptions": { "target": "es6" }, "files": [ "main.ts" ] }` - }; - const host = createServerHost([file1, file2, configFile]); - const projectService = createProjectService(host); - projectService.openClientFile(file1.path); - projectService.closeClientFile(file1.path); - projectService.openClientFile(file2.path); - checkNumberOfConfiguredProjects(projectService, 1); - checkNumberOfInferredProjects(projectService, 0); - }); - - it("should keep the configured project when the opened file is referenced by the project but not its root", () => { - const file1: File = { - path: "/a/b/main.ts", - content: "import { objA } from './obj-a';" - }; - const file2: File = { - path: "/a/b/obj-a.ts", - content: `export const objA = Object.assign({foo: "bar"}, {bar: "baz"});` - }; - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{ + }; + const host = createServerHost([file1, file2, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + projectService.closeClientFile(file1.path); + projectService.openClientFile(file2.path); + checkNumberOfConfiguredProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 0); + }); + it("should keep the configured project when the opened file is referenced by the project but not its root", () => { + const file1: File = { + path: "/a/b/main.ts", + content: "import { objA } from './obj-a';" + }; + const file2: File = { + path: "/a/b/obj-a.ts", + content: `export const objA = Object.assign({foo: "bar"}, {bar: "baz"});` + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ "compilerOptions": { "target": "es6" }, "files": [ "main.ts" ] }` - }; - const host = createServerHost([file1, file2, configFile]); - const projectService = createProjectService(host); - projectService.openClientFile(file1.path); - projectService.closeClientFile(file1.path); - projectService.openClientFile(file2.path); - checkNumberOfConfiguredProjects(projectService, 1); - checkNumberOfInferredProjects(projectService, 0); - }); - - it("should tolerate config file errors and still try to build a project", () => { - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{ + }; + const host = createServerHost([file1, file2, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + projectService.closeClientFile(file1.path); + projectService.openClientFile(file2.path); + checkNumberOfConfiguredProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 0); + }); + it("should tolerate config file errors and still try to build a project", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ "compilerOptions": { "target": "es6", "allowAnything": true }, "someOtherProperty": {} }` - }; - const host = createServerHost([commonFile1, commonFile2, libFile, configFile]); - const projectService = createProjectService(host); - projectService.openClientFile(commonFile1.path); - checkNumberOfConfiguredProjects(projectService, 1); - checkProjectRootFiles(configuredProjectAt(projectService, 0), [commonFile1.path, commonFile2.path]); - }); - - it("should reuse same project if file is opened from the configured project that has no open files", () => { - const file1 = { - path: "/a/b/main.ts", - content: "let x =1;" - }; - const file2 = { - path: "/a/b/main2.ts", - content: "let y =1;" - }; - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{ + }; + const host = createServerHost([commonFile1, commonFile2, libFile, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(commonFile1.path); + checkNumberOfConfiguredProjects(projectService, 1); + checkProjectRootFiles(configuredProjectAt(projectService, 0), [commonFile1.path, commonFile2.path]); + }); + it("should reuse same project if file is opened from the configured project that has no open files", () => { + const file1 = { + path: "/a/b/main.ts", + content: "let x =1;" + }; + const file2 = { + path: "/a/b/main2.ts", + content: "let y =1;" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ "compilerOptions": { "target": "es6" }, "files": [ "main.ts", "main2.ts" ] }` - }; - const host = createServerHost([file1, file2, configFile, libFile]); - const projectService = createProjectService(host, { useSingleInferredProject: true }); - projectService.openClientFile(file1.path); - checkNumberOfConfiguredProjects(projectService, 1); - const project = projectService.configuredProjects.get(configFile.path)!; - assert.isTrue(project.hasOpenRef()); // file1 - - projectService.closeClientFile(file1.path); - checkNumberOfConfiguredProjects(projectService, 1); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - assert.isFalse(project.hasOpenRef()); // No open files - assert.isFalse(project.isClosed()); - - projectService.openClientFile(file2.path); - checkNumberOfConfiguredProjects(projectService, 1); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - assert.isTrue(project.hasOpenRef()); // file2 - assert.isFalse(project.isClosed()); - }); - - it("should not close configured project after closing last open file, but should be closed on next file open if its not the file from same project", () => { - const file1 = { - path: "/a/b/main.ts", - content: "let x =1;" - }; - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{ + }; + const host = createServerHost([file1, file2, configFile, libFile]); + const projectService = createProjectService(host, { useSingleInferredProject: true }); + projectService.openClientFile(file1.path); + checkNumberOfConfiguredProjects(projectService, 1); + const project = projectService.configuredProjects.get(configFile.path)!; + assert.isTrue(project.hasOpenRef()); // file1 + projectService.closeClientFile(file1.path); + checkNumberOfConfiguredProjects(projectService, 1); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + assert.isFalse(project.hasOpenRef()); // No open files + assert.isFalse(project.isClosed()); + projectService.openClientFile(file2.path); + checkNumberOfConfiguredProjects(projectService, 1); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + assert.isTrue(project.hasOpenRef()); // file2 + assert.isFalse(project.isClosed()); + }); + it("should not close configured project after closing last open file, but should be closed on next file open if its not the file from same project", () => { + const file1 = { + path: "/a/b/main.ts", + content: "let x =1;" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ "compilerOptions": { "target": "es6" }, "files": [ "main.ts" ] }` - }; - const host = createServerHost([file1, configFile, libFile]); - const projectService = createProjectService(host, { useSingleInferredProject: true }); - projectService.openClientFile(file1.path); - checkNumberOfConfiguredProjects(projectService, 1); - const project = projectService.configuredProjects.get(configFile.path)!; - assert.isTrue(project.hasOpenRef()); // file1 - - projectService.closeClientFile(file1.path); - checkNumberOfConfiguredProjects(projectService, 1); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - assert.isFalse(project.hasOpenRef()); // No files - assert.isFalse(project.isClosed()); - - projectService.openClientFile(libFile.path); - checkNumberOfConfiguredProjects(projectService, 0); - assert.isFalse(project.hasOpenRef()); // No files + project closed - assert.isTrue(project.isClosed()); - }); - - it("open file become a part of configured project if it is referenced from root file", () => { - const file1 = { - path: `${tscWatch.projectRoot}/a/b/f1.ts`, - content: "export let x = 5" - }; - const file2 = { - path: `${tscWatch.projectRoot}/a/c/f2.ts`, - content: `import {x} from "../b/f1"` - }; - const file3 = { - path: `${tscWatch.projectRoot}/a/c/f3.ts`, - content: "export let y = 1" - }; - const configFile = { - path: `${tscWatch.projectRoot}/a/c/tsconfig.json`, - content: JSON.stringify({ compilerOptions: {}, files: ["f2.ts", "f3.ts"] }) - }; - - const host = createServerHost([file1, file2, file3]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); - - projectService.openClientFile(file3.path); - checkNumberOfProjects(projectService, { inferredProjects: 2 }); - checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); - checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); - - host.reloadFS([file1, file2, file3, configFile]); - host.checkTimeoutQueueLengthAndRun(2); // load configured project from disk + ensureProjectsForOpenFiles - checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, file3.path, configFile.path]); - assert.isTrue(projectService.inferredProjects[0].isOrphan()); - assert.isTrue(projectService.inferredProjects[1].isOrphan()); - }); - - it("can correctly update configured project when set of root files has changed (new file on disk)", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x = 1" - }; - const file2 = { - path: "/a/b/f2.ts", - content: "let y = 1" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: {} }) - }; - - const host = createServerHost([file1, configFile]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, configFile.path]); - - host.reloadFS([file1, file2, configFile]); - - host.checkTimeoutQueueLengthAndRun(2); - - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectRootFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path]); - }); - - it("can correctly update configured project when set of root files has changed (new file in list of files)", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x = 1" - }; - const file2 = { - path: "/a/b/f2.ts", - content: "let y = 1" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts"] }) - }; - - const host = createServerHost([file1, file2, configFile]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, configFile.path]); - - const modifiedConfigFile = { - path: configFile.path, - content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] }) - }; - - host.reloadFS([file1, file2, modifiedConfigFile]); - - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - host.checkTimeoutQueueLengthAndRun(2); - checkProjectRootFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path]); - }); - - it("can update configured project when set of root files was not changed", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x = 1" - }; - const file2 = { - path: "/a/b/f2.ts", - content: "let y = 1" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] }) - }; - - const host = createServerHost([file1, file2, configFile]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, configFile.path]); - - const modifiedConfigFile = { - path: configFile.path, - content: JSON.stringify({ compilerOptions: { outFile: "out.js" }, files: ["f1.ts", "f2.ts"] }) - }; - - host.reloadFS([file1, file2, modifiedConfigFile]); - - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectRootFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path]); - }); - - it("Open ref of configured project when open file gets added to the project as part of configured file update", () => { - const file1: File = { - path: "/a/b/src/file1.ts", - content: "let x = 1;" - }; - const file2: File = { - path: "/a/b/src/file2.ts", - content: "let y = 1;" - }; - const file3: File = { - path: "/a/b/file3.ts", - content: "let z = 1;" - }; - const file4: File = { - path: "/a/file4.ts", - content: "let z = 1;" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ files: ["src/file1.ts", "file3.ts"] }) - }; - - const files = [file1, file2, file3, file4]; - const host = createServerHost(files.concat(configFile)); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - projectService.openClientFile(file2.path); - projectService.openClientFile(file3.path); - projectService.openClientFile(file4.path); - - const infos = files.map(file => projectService.getScriptInfoForPath(file.path as Path)!); - checkOpenFiles(projectService, files); - checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); - const configProject1 = projectService.configuredProjects.get(configFile.path)!; - assert.isTrue(configProject1.hasOpenRef()); // file1 and file3 - checkProjectActualFiles(configProject1, [file1.path, file3.path, configFile.path]); - const inferredProject1 = projectService.inferredProjects[0]; - checkProjectActualFiles(inferredProject1, [file2.path]); - const inferredProject2 = projectService.inferredProjects[1]; - checkProjectActualFiles(inferredProject2, [file4.path]); - - configFile.content = "{}"; - host.reloadFS(files.concat(configFile)); - host.runQueuedTimeoutCallbacks(); - - verifyScriptInfos(); - checkOpenFiles(projectService, files); - verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ true, 2); // file1, file2, file3 - assert.isTrue(projectService.inferredProjects[0].isOrphan()); - const inferredProject3 = projectService.inferredProjects[1]; - checkProjectActualFiles(inferredProject3, [file4.path]); - assert.strictEqual(inferredProject3, inferredProject2); - - projectService.closeClientFile(file1.path); - projectService.closeClientFile(file2.path); - projectService.closeClientFile(file4.path); - - verifyScriptInfos(); - checkOpenFiles(projectService, [file3]); - verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ true, 2); // file3 - assert.isTrue(projectService.inferredProjects[0].isOrphan()); - assert.isTrue(projectService.inferredProjects[1].isOrphan()); - - projectService.openClientFile(file4.path); - verifyScriptInfos(); - checkOpenFiles(projectService, [file3, file4]); - verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ true, 1); // file3 - const inferredProject4 = projectService.inferredProjects[0]; - checkProjectActualFiles(inferredProject4, [file4.path]); - - projectService.closeClientFile(file3.path); - verifyScriptInfos(); - checkOpenFiles(projectService, [file4]); - verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ false, 1); // No open files - const inferredProject5 = projectService.inferredProjects[0]; - checkProjectActualFiles(inferredProject4, [file4.path]); - assert.strictEqual(inferredProject5, inferredProject4); - - const file5: File = { - path: "/file5.ts", - content: "let zz = 1;" - }; - host.reloadFS(files.concat(configFile, file5)); - projectService.openClientFile(file5.path); - verifyScriptInfosAreUndefined([file1, file2, file3]); - assert.strictEqual(projectService.getScriptInfoForPath(file4.path as Path), find(infos, info => info.path === file4.path)); - assert.isDefined(projectService.getScriptInfoForPath(file5.path as Path)); - checkOpenFiles(projectService, [file4, file5]); - checkNumberOfProjects(projectService, { inferredProjects: 2 }); - checkProjectActualFiles(projectService.inferredProjects[0], [file4.path]); - checkProjectActualFiles(projectService.inferredProjects[1], [file5.path]); - - function verifyScriptInfos() { - infos.forEach(info => assert.strictEqual(projectService.getScriptInfoForPath(info.path), info)); + }; + const host = createServerHost([file1, configFile, libFile]); + const projectService = createProjectService(host, { useSingleInferredProject: true }); + projectService.openClientFile(file1.path); + checkNumberOfConfiguredProjects(projectService, 1); + const project = projectService.configuredProjects.get(configFile.path)!; + assert.isTrue(project.hasOpenRef()); // file1 + projectService.closeClientFile(file1.path); + checkNumberOfConfiguredProjects(projectService, 1); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + assert.isFalse(project.hasOpenRef()); // No files + assert.isFalse(project.isClosed()); + projectService.openClientFile(libFile.path); + checkNumberOfConfiguredProjects(projectService, 0); + assert.isFalse(project.hasOpenRef()); // No files + project closed + assert.isTrue(project.isClosed()); + }); + it("open file become a part of configured project if it is referenced from root file", () => { + const file1 = { + path: `${projectRoot}/a/b/f1.ts`, + content: "export let x = 5" + }; + const file2 = { + path: `${projectRoot}/a/c/f2.ts`, + content: `import {x} from "../b/f1"` + }; + const file3 = { + path: `${projectRoot}/a/c/f3.ts`, + content: "export let y = 1" + }; + const configFile = { + path: `${projectRoot}/a/c/tsconfig.json`, + content: JSON.stringify({ compilerOptions: {}, files: ["f2.ts", "f3.ts"] }) + }; + const host = createServerHost([file1, file2, file3]); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); + projectService.openClientFile(file3.path); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); + host.reloadFS([file1, file2, file3, configFile]); + host.checkTimeoutQueueLengthAndRun(2); // load configured project from disk + ensureProjectsForOpenFiles + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, file3.path, configFile.path]); + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + assert.isTrue(projectService.inferredProjects[1].isOrphan()); + }); + it("can correctly update configured project when set of root files has changed (new file on disk)", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {} }) + }; + const host = createServerHost([file1, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, configFile.path]); + host.reloadFS([file1, file2, configFile]); + host.checkTimeoutQueueLengthAndRun(2); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectRootFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path]); + }); + it("can correctly update configured project when set of root files has changed (new file in list of files)", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts"] }) + }; + const host = createServerHost([file1, file2, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, configFile.path]); + const modifiedConfigFile = { + path: configFile.path, + content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] }) + }; + host.reloadFS([file1, file2, modifiedConfigFile]); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + host.checkTimeoutQueueLengthAndRun(2); + checkProjectRootFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path]); + }); + it("can update configured project when set of root files was not changed", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] }) + }; + const host = createServerHost([file1, file2, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, configFile.path]); + const modifiedConfigFile = { + path: configFile.path, + content: JSON.stringify({ compilerOptions: { outFile: "out.js" }, files: ["f1.ts", "f2.ts"] }) + }; + host.reloadFS([file1, file2, modifiedConfigFile]); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectRootFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path]); + }); + it("Open ref of configured project when open file gets added to the project as part of configured file update", () => { + const file1: File = { + path: "/a/b/src/file1.ts", + content: "let x = 1;" + }; + const file2: File = { + path: "/a/b/src/file2.ts", + content: "let y = 1;" + }; + const file3: File = { + path: "/a/b/file3.ts", + content: "let z = 1;" + }; + const file4: File = { + path: "/a/file4.ts", + content: "let z = 1;" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ files: ["src/file1.ts", "file3.ts"] }) + }; + const files = [file1, file2, file3, file4]; + const host = createServerHost(files.concat(configFile)); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + projectService.openClientFile(file2.path); + projectService.openClientFile(file3.path); + projectService.openClientFile(file4.path); + const infos = files.map(file => projectService.getScriptInfoForPath((file.path as Path))!); + checkOpenFiles(projectService, files); + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); + const configProject1 = projectService.configuredProjects.get(configFile.path)!; + assert.isTrue(configProject1.hasOpenRef()); // file1 and file3 + checkProjectActualFiles(configProject1, [file1.path, file3.path, configFile.path]); + const inferredProject1 = projectService.inferredProjects[0]; + checkProjectActualFiles(inferredProject1, [file2.path]); + const inferredProject2 = projectService.inferredProjects[1]; + checkProjectActualFiles(inferredProject2, [file4.path]); + configFile.content = "{}"; + host.reloadFS(files.concat(configFile)); + host.runQueuedTimeoutCallbacks(); + verifyScriptInfos(); + checkOpenFiles(projectService, files); + verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ true, 2); // file1, file2, file3 + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + const inferredProject3 = projectService.inferredProjects[1]; + checkProjectActualFiles(inferredProject3, [file4.path]); + assert.strictEqual(inferredProject3, inferredProject2); + projectService.closeClientFile(file1.path); + projectService.closeClientFile(file2.path); + projectService.closeClientFile(file4.path); + verifyScriptInfos(); + checkOpenFiles(projectService, [file3]); + verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ true, 2); // file3 + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + assert.isTrue(projectService.inferredProjects[1].isOrphan()); + projectService.openClientFile(file4.path); + verifyScriptInfos(); + checkOpenFiles(projectService, [file3, file4]); + verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ true, 1); // file3 + const inferredProject4 = projectService.inferredProjects[0]; + checkProjectActualFiles(inferredProject4, [file4.path]); + projectService.closeClientFile(file3.path); + verifyScriptInfos(); + checkOpenFiles(projectService, [file4]); + verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ false, 1); // No open files + const inferredProject5 = projectService.inferredProjects[0]; + checkProjectActualFiles(inferredProject4, [file4.path]); + assert.strictEqual(inferredProject5, inferredProject4); + const file5: File = { + path: "/file5.ts", + content: "let zz = 1;" + }; + host.reloadFS(files.concat(configFile, file5)); + projectService.openClientFile(file5.path); + verifyScriptInfosAreUndefined([file1, file2, file3]); + assert.strictEqual(projectService.getScriptInfoForPath((file4.path as Path)), find(infos, info => info.path === file4.path)); + assert.isDefined(projectService.getScriptInfoForPath((file5.path as Path))); + checkOpenFiles(projectService, [file4, file5]); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file4.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [file5.path]); + function verifyScriptInfos() { + infos.forEach(info => assert.strictEqual(projectService.getScriptInfoForPath(info.path), info)); + } + function verifyScriptInfosAreUndefined(files: File[]) { + for (const file of files) { + assert.isUndefined(projectService.getScriptInfoForPath((file.path as Path))); } - - function verifyScriptInfosAreUndefined(files: File[]) { - for (const file of files) { - assert.isUndefined(projectService.getScriptInfoForPath(file.path as Path)); + } + function verifyConfiguredProjectStateAfterUpdate(hasOpenRef: boolean, inferredProjects: number) { + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects }); + const configProject2 = projectService.configuredProjects.get(configFile.path)!; + assert.strictEqual(configProject2, configProject1); + checkProjectActualFiles(configProject2, [file1.path, file2.path, file3.path, configFile.path]); + assert.equal(configProject2.hasOpenRef(), hasOpenRef); + } + }); + it("Open ref of configured project when open file gets added to the project as part of configured file update buts its open file references are all closed when the update happens", () => { + const file1: File = { + path: "/a/b/src/file1.ts", + content: "let x = 1;" + }; + const file2: File = { + path: "/a/b/src/file2.ts", + content: "let y = 1;" + }; + const file3: File = { + path: "/a/b/file3.ts", + content: "let z = 1;" + }; + const file4: File = { + path: "/a/file4.ts", + content: "let z = 1;" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ files: ["src/file1.ts", "file3.ts"] }) + }; + const files = [file1, file2, file3]; + const hostFiles = files.concat(file4, configFile); + const host = createServerHost(hostFiles); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + projectService.openClientFile(file2.path); + projectService.openClientFile(file3.path); + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); + const configuredProject = projectService.configuredProjects.get(configFile.path)!; + assert.isTrue(configuredProject.hasOpenRef()); // file1 and file3 + checkProjectActualFiles(configuredProject, [file1.path, file3.path, configFile.path]); + const inferredProject1 = projectService.inferredProjects[0]; + checkProjectActualFiles(inferredProject1, [file2.path]); + projectService.closeClientFile(file1.path); + projectService.closeClientFile(file3.path); + assert.isFalse(configuredProject.hasOpenRef()); // No files + configFile.content = "{}"; + host.reloadFS(files.concat(configFile)); + // Time out is not yet run so there is project update pending + assert.isTrue(configuredProject.hasOpenRef()); // Pending update and file2 might get into the project + projectService.openClientFile(file4.path); + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), configuredProject); + assert.isTrue(configuredProject.hasOpenRef()); // Pending update and F2 might get into the project + assert.strictEqual(projectService.inferredProjects[0], inferredProject1); + const inferredProject2 = projectService.inferredProjects[1]; + checkProjectActualFiles(inferredProject2, [file4.path]); + host.runQueuedTimeoutCallbacks(); + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), configuredProject); + assert.isTrue(configuredProject.hasOpenRef()); // file2 + checkProjectActualFiles(configuredProject, [file1.path, file2.path, file3.path, configFile.path]); + assert.strictEqual(projectService.inferredProjects[0], inferredProject1); + assert.isTrue(inferredProject1.isOrphan()); + assert.strictEqual(projectService.inferredProjects[1], inferredProject2); + checkProjectActualFiles(inferredProject2, [file4.path]); + }); + it("files are properly detached when language service is disabled", () => { + const f1 = { + path: "/a/app.js", + content: "var x = 1" + }; + const f2 = { + path: "/a/largefile.js", + content: "" + }; + const f3 = { + path: "/a/lib.js", + content: "var x = 1" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ compilerOptions: { allowJs: true } }) + }; + const host = createServerHost([f1, f2, f3, config]); + const originalGetFileSize = host.getFileSize; + host.getFileSize = (filePath: string) => filePath === f2.path ? maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); + const projectService = createProjectService(host); + projectService.openClientFile(f1.path); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + const project = projectService.configuredProjects.get(config.path)!; + assert.isTrue(project.hasOpenRef()); // f1 + assert.isFalse(project.isClosed()); + projectService.closeClientFile(f1.path); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(config.path), project); + assert.isFalse(project.hasOpenRef()); // No files + assert.isFalse(project.isClosed()); + for (const f of [f1, f2, f3]) { + // All the script infos should be present and contain the project since it is still alive. + const scriptInfo = (projectService.getScriptInfoForNormalizedPath(toNormalizedPath(f.path))!); + assert.equal(scriptInfo.containingProjects.length, 1, `expect 1 containing projects for '${f.path}'`); + assert.equal(scriptInfo.containingProjects[0], project, `expect configured project to be the only containing project for '${f.path}'`); + } + const f4 = { + path: "/aa.js", + content: "var x = 1" + }; + host.reloadFS([f1, f2, f3, config, f4]); + projectService.openClientFile(f4.path); + projectService.checkNumberOfProjects({ inferredProjects: 1 }); + assert.isFalse(project.hasOpenRef()); // No files + assert.isTrue(project.isClosed()); + for (const f of [f1, f2, f3]) { + // All the script infos should not be present since the project is closed and orphan script infos are collected + assert.isUndefined(projectService.getScriptInfoForNormalizedPath(toNormalizedPath(f.path))); + } + }); + it("syntactic features work even if language service is disabled", () => { + const f1 = { + path: "/a/app.js", + content: "let x = 1;" + }; + const f2 = { + path: "/a/largefile.js", + content: "" + }; + const config = { + path: "/a/jsconfig.json", + content: "{}" + }; + const host = createServerHost([f1, f2, config]); + const originalGetFileSize = host.getFileSize; + host.getFileSize = (filePath: string) => filePath === f2.path ? maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); + const { session, events } = createSessionWithEventTracking(host, ProjectLanguageServiceStateEvent); + session.executeCommand(({ + seq: 0, + type: "request", + command: "open", + arguments: { file: f1.path } + })); + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = configuredProjectAt(projectService, 0); + assert.isFalse(project.languageServiceEnabled, "Language service enabled"); + assert.equal(events.length, 1, "should receive event"); + assert.equal(events[0].data.project, project, "project name"); + assert.isFalse(events[0].data.languageServiceEnabled, "Language service state"); + const options = projectService.getFormatCodeOptions((f1.path as NormalizedPath)); + const edits = project.getLanguageService().getFormattingEditsForDocument(f1.path, options); + assert.deepEqual(edits, [{ span: createTextSpan(/*start*/ 7, /*length*/ 3), newText: " " }]); + }); + it("when multiple projects are open, detects correct default project", () => { + const barConfig: File = { + path: `${projectRoot}/bar/tsconfig.json`, + content: JSON.stringify({ + include: ["index.ts"], + compilerOptions: { + lib: ["dom", "es2017"] } - } - - function verifyConfiguredProjectStateAfterUpdate(hasOpenRef: boolean, inferredProjects: number) { - checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects }); - const configProject2 = projectService.configuredProjects.get(configFile.path)!; - assert.strictEqual(configProject2, configProject1); - checkProjectActualFiles(configProject2, [file1.path, file2.path, file3.path, configFile.path]); - assert.equal(configProject2.hasOpenRef(), hasOpenRef); - } - }); - - it("Open ref of configured project when open file gets added to the project as part of configured file update buts its open file references are all closed when the update happens", () => { - const file1: File = { - path: "/a/b/src/file1.ts", - content: "let x = 1;" - }; - const file2: File = { - path: "/a/b/src/file2.ts", - content: "let y = 1;" - }; - const file3: File = { - path: "/a/b/file3.ts", - content: "let z = 1;" - }; - const file4: File = { - path: "/a/file4.ts", - content: "let z = 1;" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ files: ["src/file1.ts", "file3.ts"] }) - }; - - const files = [file1, file2, file3]; - const hostFiles = files.concat(file4, configFile); - const host = createServerHost(hostFiles); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - projectService.openClientFile(file2.path); - projectService.openClientFile(file3.path); - - checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); - const configuredProject = projectService.configuredProjects.get(configFile.path)!; - assert.isTrue(configuredProject.hasOpenRef()); // file1 and file3 - checkProjectActualFiles(configuredProject, [file1.path, file3.path, configFile.path]); - const inferredProject1 = projectService.inferredProjects[0]; - checkProjectActualFiles(inferredProject1, [file2.path]); - - projectService.closeClientFile(file1.path); - projectService.closeClientFile(file3.path); - assert.isFalse(configuredProject.hasOpenRef()); // No files - - configFile.content = "{}"; - host.reloadFS(files.concat(configFile)); - // Time out is not yet run so there is project update pending - assert.isTrue(configuredProject.hasOpenRef()); // Pending update and file2 might get into the project - - projectService.openClientFile(file4.path); - - checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), configuredProject); - assert.isTrue(configuredProject.hasOpenRef()); // Pending update and F2 might get into the project - assert.strictEqual(projectService.inferredProjects[0], inferredProject1); - const inferredProject2 = projectService.inferredProjects[1]; - checkProjectActualFiles(inferredProject2, [file4.path]); - - host.runQueuedTimeoutCallbacks(); - checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), configuredProject); - assert.isTrue(configuredProject.hasOpenRef()); // file2 - checkProjectActualFiles(configuredProject, [file1.path, file2.path, file3.path, configFile.path]); - assert.strictEqual(projectService.inferredProjects[0], inferredProject1); - assert.isTrue(inferredProject1.isOrphan()); - assert.strictEqual(projectService.inferredProjects[1], inferredProject2); - checkProjectActualFiles(inferredProject2, [file4.path]); - }); - - it("files are properly detached when language service is disabled", () => { - const f1 = { - path: "/a/app.js", - content: "var x = 1" - }; - const f2 = { - path: "/a/largefile.js", - content: "" - }; - const f3 = { - path: "/a/lib.js", - content: "var x = 1" - }; - const config = { - path: "/a/tsconfig.json", - content: JSON.stringify({ compilerOptions: { allowJs: true } }) - }; - const host = createServerHost([f1, f2, f3, config]); - const originalGetFileSize = host.getFileSize; - host.getFileSize = (filePath: string) => - filePath === f2.path ? server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); - - const projectService = createProjectService(host); - projectService.openClientFile(f1.path); - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - const project = projectService.configuredProjects.get(config.path)!; - assert.isTrue(project.hasOpenRef()); // f1 - assert.isFalse(project.isClosed()); - - projectService.closeClientFile(f1.path); - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(config.path), project); - assert.isFalse(project.hasOpenRef()); // No files - assert.isFalse(project.isClosed()); - - for (const f of [f1, f2, f3]) { - // All the script infos should be present and contain the project since it is still alive. - const scriptInfo = projectService.getScriptInfoForNormalizedPath(server.toNormalizedPath(f.path))!; - assert.equal(scriptInfo.containingProjects.length, 1, `expect 1 containing projects for '${f.path}'`); - assert.equal(scriptInfo.containingProjects[0], project, `expect configured project to be the only containing project for '${f.path}'`); - } - - const f4 = { - path: "/aa.js", - content: "var x = 1" - }; - host.reloadFS([f1, f2, f3, config, f4]); - projectService.openClientFile(f4.path); - projectService.checkNumberOfProjects({ inferredProjects: 1 }); - assert.isFalse(project.hasOpenRef()); // No files - assert.isTrue(project.isClosed()); - - for (const f of [f1, f2, f3]) { - // All the script infos should not be present since the project is closed and orphan script infos are collected - assert.isUndefined(projectService.getScriptInfoForNormalizedPath(server.toNormalizedPath(f.path))); - } - }); - - it("syntactic features work even if language service is disabled", () => { - const f1 = { - path: "/a/app.js", - content: "let x = 1;" - }; - const f2 = { - path: "/a/largefile.js", - content: "" - }; - const config = { - path: "/a/jsconfig.json", - content: "{}" - }; - const host = createServerHost([f1, f2, config]); - const originalGetFileSize = host.getFileSize; - host.getFileSize = (filePath: string) => - filePath === f2.path ? server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); - const { session, events } = createSessionWithEventTracking(host, server.ProjectLanguageServiceStateEvent); - session.executeCommand({ - seq: 0, - type: "request", - command: "open", - arguments: { file: f1.path } - }); - - const projectService = session.getProjectService(); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const project = configuredProjectAt(projectService, 0); - assert.isFalse(project.languageServiceEnabled, "Language service enabled"); - assert.equal(events.length, 1, "should receive event"); - assert.equal(events[0].data.project, project, "project name"); - assert.isFalse(events[0].data.languageServiceEnabled, "Language service state"); - - const options = projectService.getFormatCodeOptions(f1.path as server.NormalizedPath); - const edits = project.getLanguageService().getFormattingEditsForDocument(f1.path, options); - assert.deepEqual(edits, [{ span: createTextSpan(/*start*/ 7, /*length*/ 3), newText: " " }]); - }); - - it("when multiple projects are open, detects correct default project", () => { - const barConfig: File = { - path: `${tscWatch.projectRoot}/bar/tsconfig.json`, - content: JSON.stringify({ - include: ["index.ts"], - compilerOptions: { - lib: ["dom", "es2017"] - } - }) - }; - const barIndex: File = { - path: `${tscWatch.projectRoot}/bar/index.ts`, - content: ` + }) + }; + const barIndex: File = { + path: `${projectRoot}/bar/index.ts`, + content: ` export function bar() { console.log("hello world"); }` - }; - const fooConfig: File = { - path: `${tscWatch.projectRoot}/foo/tsconfig.json`, - content: JSON.stringify({ - include: ["index.ts"], - compilerOptions: { - lib: ["es2017"] - } - }) - }; - const fooIndex: File = { - path: `${tscWatch.projectRoot}/foo/index.ts`, - content: ` + }; + const fooConfig: File = { + path: `${projectRoot}/foo/tsconfig.json`, + content: JSON.stringify({ + include: ["index.ts"], + compilerOptions: { + lib: ["es2017"] + } + }) + }; + const fooIndex: File = { + path: `${projectRoot}/foo/index.ts`, + content: ` import { bar } from "bar"; bar();` - }; - const barSymLink: SymLink = { - path: `${tscWatch.projectRoot}/foo/node_modules/bar`, - symLink: `${tscWatch.projectRoot}/bar` - }; - - const lib2017: File = { - path: `${getDirectoryPath(libFile.path)}/lib.es2017.d.ts`, - content: libFile.content - }; - const libDom: File = { - path: `${getDirectoryPath(libFile.path)}/lib.dom.d.ts`, - content: ` + }; + const barSymLink: SymLink = { + path: `${projectRoot}/foo/node_modules/bar`, + symLink: `${projectRoot}/bar` + }; + const lib2017: File = { + path: `${getDirectoryPath(libFile.path)}/lib.es2017.d.ts`, + content: libFile.content + }; + const libDom: File = { + path: `${getDirectoryPath(libFile.path)}/lib.dom.d.ts`, + content: ` declare var console: { log(...args: any[]): void; };` - }; - const host = createServerHost([barConfig, barIndex, fooConfig, fooIndex, barSymLink, lib2017, libDom]); - const session = createSession(host, { canUseEvents: true, }); - openFilesForSession([fooIndex, barIndex], session); - verifyGetErrRequest({ - session, - host, - expected: [ - { file: barIndex, syntax: [], semantic: [], suggestion: [] }, - { file: fooIndex, syntax: [], semantic: [], suggestion: [] }, - ] - }); - }); - - it("when file name starts with ^", () => { - const file: File = { - path: `${tscWatch.projectRoot}/file.ts`, - content: "const x = 10;" - }; - const app: File = { - path: `${tscWatch.projectRoot}/^app.ts`, - content: "const y = 10;" - }; - const tsconfig: File = { - path: `${tscWatch.projectRoot}/tsconfig.json`, - content: "{}" - }; - const host = createServerHost([file, app, tsconfig, libFile]); - const service = createProjectService(host); - service.openClientFile(file.path); + }; + const host = createServerHost([barConfig, barIndex, fooConfig, fooIndex, barSymLink, lib2017, libDom]); + const session = createSession(host, { canUseEvents: true, }); + openFilesForSession([fooIndex, barIndex], session); + verifyGetErrRequest({ + session, + host, + expected: [ + { file: barIndex, syntax: [], semantic: [], suggestion: [] }, + { file: fooIndex, syntax: [], semantic: [], suggestion: [] }, + ] }); - - describe("when creating new file", () => { - const foo: File = { - path: `${tscWatch.projectRoot}/src/foo.ts`, - content: "export function foo() { }" - }; - const bar: File = { - path: `${tscWatch.projectRoot}/src/bar.ts`, - content: "export function bar() { }" - }; - const config: File = { - path: `${tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ - include: ["./src"] - }) - }; - const fooBar: File = { - path: `${tscWatch.projectRoot}/src/sub/fooBar.ts`, - content: "export function fooBar() { }" - }; - function verifySessionWorker({ withExclude, openFileBeforeCreating, checkProjectBeforeError, checkProjectAfterError, }: VerifySession, errorOnNewFileBeforeOldFile: boolean) { - const host = createServerHost([ - foo, bar, libFile, { path: `${tscWatch.projectRoot}/src/sub` }, - withExclude ? - { - path: config.path, - content: JSON.stringify({ - include: ["./src"], - exclude: ["./src/sub"] - }) - } : - config - ]); - const session = createSession(host, { - canUseEvents: true - }); - session.executeCommandSeq({ - command: protocol.CommandTypes.Open, - arguments: { - file: foo.path, - fileContent: foo.content, - projectRootPath: tscWatch.projectRoot - } - }); - if (!openFileBeforeCreating) { - host.writeFile(fooBar.path, fooBar.content); - } - session.executeCommandSeq({ - command: protocol.CommandTypes.Open, - arguments: { - file: fooBar.path, - fileContent: fooBar.content, - projectRootPath: tscWatch.projectRoot - } - }); - if (openFileBeforeCreating) { - host.writeFile(fooBar.path, fooBar.content); + }); + it("when file name starts with ^", () => { + const file: File = { + path: `${projectRoot}/file.ts`, + content: "const x = 10;" + }; + const app: File = { + path: `${projectRoot}/^app.ts`, + content: "const y = 10;" + }; + const tsconfig: File = { + path: `${projectRoot}/tsconfig.json`, + content: "{}" + }; + const host = createServerHost([file, app, tsconfig, libFile]); + const service = createProjectService(host); + service.openClientFile(file.path); + }); + describe("when creating new file", () => { + const foo: File = { + path: `${projectRoot}/src/foo.ts`, + content: "export function foo() { }" + }; + const bar: File = { + path: `${projectRoot}/src/bar.ts`, + content: "export function bar() { }" + }; + const config: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ + include: ["./src"] + }) + }; + const fooBar: File = { + path: `${projectRoot}/src/sub/fooBar.ts`, + content: "export function fooBar() { }" + }; + function verifySessionWorker({ withExclude, openFileBeforeCreating, checkProjectBeforeError, checkProjectAfterError, }: VerifySession, errorOnNewFileBeforeOldFile: boolean) { + const host = createServerHost([ + foo, bar, + libFile, + { path: `${projectRoot}/src/sub` }, + withExclude ? + { + path: config.path, + content: JSON.stringify({ + include: ["./src"], + exclude: ["./src/sub"] + }) + } : + config + ]); + const session = createSession(host, { + canUseEvents: true + }); + session.executeCommandSeq({ + command: protocol.CommandTypes.Open, + arguments: { + file: foo.path, + fileContent: foo.content, + projectRootPath: projectRoot } - const service = session.getProjectService(); - checkProjectBeforeError(service); - verifyGetErrRequest({ - session, - host, - expected: errorOnNewFileBeforeOldFile ? - [ - { file: fooBar, syntax: [], semantic: [], suggestion: [] }, - { file: foo, syntax: [], semantic: [], suggestion: [] }, - ] : - [ - { file: foo, syntax: [], semantic: [], suggestion: [] }, - { file: fooBar, syntax: [], semantic: [], suggestion: [] }, - ], - existingTimeouts: 2 - }); - checkProjectAfterError(service); - } - interface VerifySession { - withExclude?: boolean; - openFileBeforeCreating: boolean; - checkProjectBeforeError: (service: server.ProjectService) => void; - checkProjectAfterError: (service: server.ProjectService) => void; - } - function verifySession(input: VerifySession) { - it("when error on new file are asked before old one", () => { - verifySessionWorker(input, /*errorOnNewFileBeforeOldFile*/ true); - }); - - it("when error on new file are asked after old one", () => { - verifySessionWorker(input, /*errorOnNewFileBeforeOldFile*/ false); - }); - } - function checkFooBarInInferredProject(service: server.ProjectService) { - checkNumberOfProjects(service, { configuredProjects: 1, inferredProjects: 1 }); - checkProjectActualFiles(service.configuredProjects.get(config.path)!, [foo.path, bar.path, libFile.path, config.path]); - checkProjectActualFiles(service.inferredProjects[0], [fooBar.path, libFile.path]); + }); + if (!openFileBeforeCreating) { + host.writeFile(fooBar.path, fooBar.content); } - function checkFooBarInConfiguredProject(service: server.ProjectService) { - checkNumberOfProjects(service, { configuredProjects: 1 }); - checkProjectActualFiles(service.configuredProjects.get(config.path)!, [foo.path, bar.path, fooBar.path, libFile.path, config.path]); + session.executeCommandSeq({ + command: protocol.CommandTypes.Open, + arguments: { + file: fooBar.path, + fileContent: fooBar.content, + projectRootPath: projectRoot + } + }); + if (openFileBeforeCreating) { + host.writeFile(fooBar.path, fooBar.content); } - describe("when new file creation directory watcher is invoked before file is opened in editor", () => { + const service = session.getProjectService(); + checkProjectBeforeError(service); + verifyGetErrRequest({ + session, + host, + expected: errorOnNewFileBeforeOldFile ? + [ + { file: fooBar, syntax: [], semantic: [], suggestion: [] }, + { file: foo, syntax: [], semantic: [], suggestion: [] }, + ] : + [ + { file: foo, syntax: [], semantic: [], suggestion: [] }, + { file: fooBar, syntax: [], semantic: [], suggestion: [] }, + ], + existingTimeouts: 2 + }); + checkProjectAfterError(service); + } + interface VerifySession { + withExclude?: boolean; + openFileBeforeCreating: boolean; + checkProjectBeforeError: (service: ProjectService) => void; + checkProjectAfterError: (service: ProjectService) => void; + } + function verifySession(input: VerifySession) { + it("when error on new file are asked before old one", () => { + verifySessionWorker(input, /*errorOnNewFileBeforeOldFile*/ true); + }); + it("when error on new file are asked after old one", () => { + verifySessionWorker(input, /*errorOnNewFileBeforeOldFile*/ false); + }); + } + function checkFooBarInInferredProject(service: ProjectService) { + checkNumberOfProjects(service, { configuredProjects: 1, inferredProjects: 1 }); + checkProjectActualFiles((service.configuredProjects.get(config.path)!), [foo.path, bar.path, libFile.path, config.path]); + checkProjectActualFiles(service.inferredProjects[0], [fooBar.path, libFile.path]); + } + function checkFooBarInConfiguredProject(service: ProjectService) { + checkNumberOfProjects(service, { configuredProjects: 1 }); + checkProjectActualFiles((service.configuredProjects.get(config.path)!), [foo.path, bar.path, fooBar.path, libFile.path, config.path]); + } + describe("when new file creation directory watcher is invoked before file is opened in editor", () => { + verifySession({ + openFileBeforeCreating: false, + checkProjectBeforeError: checkFooBarInConfiguredProject, + checkProjectAfterError: checkFooBarInConfiguredProject + }); + describe("when new file is excluded from config", () => { verifySession({ + withExclude: true, openFileBeforeCreating: false, - checkProjectBeforeError: checkFooBarInConfiguredProject, - checkProjectAfterError: checkFooBarInConfiguredProject - }); - describe("when new file is excluded from config", () => { - verifySession({ - withExclude: true, - openFileBeforeCreating: false, - checkProjectBeforeError: checkFooBarInInferredProject, - checkProjectAfterError: checkFooBarInInferredProject - }); + checkProjectBeforeError: checkFooBarInInferredProject, + checkProjectAfterError: checkFooBarInInferredProject }); }); - - describe("when new file creation directory watcher is invoked after file is opened in editor", () => { + }); + describe("when new file creation directory watcher is invoked after file is opened in editor", () => { + verifySession({ + openFileBeforeCreating: true, + checkProjectBeforeError: checkFooBarInInferredProject, + checkProjectAfterError: service => { + // Both projects exist but fooBar is in configured project after the update + // Inferred project is yet to be updated so still has fooBar + checkNumberOfProjects(service, { configuredProjects: 1, inferredProjects: 1 }); + checkProjectActualFiles((service.configuredProjects.get(config.path)!), [foo.path, bar.path, fooBar.path, libFile.path, config.path]); + checkProjectActualFiles(service.inferredProjects[0], [fooBar.path, libFile.path]); + assert.isTrue(service.inferredProjects[0].dirty); + assert.equal(service.inferredProjects[0].getRootFilesMap().size, 0); + } + }); + describe("when new file is excluded from config", () => { verifySession({ + withExclude: true, openFileBeforeCreating: true, checkProjectBeforeError: checkFooBarInInferredProject, - checkProjectAfterError: service => { - // Both projects exist but fooBar is in configured project after the update - // Inferred project is yet to be updated so still has fooBar - checkNumberOfProjects(service, { configuredProjects: 1, inferredProjects: 1 }); - checkProjectActualFiles(service.configuredProjects.get(config.path)!, [foo.path, bar.path, fooBar.path, libFile.path, config.path]); - checkProjectActualFiles(service.inferredProjects[0], [fooBar.path, libFile.path]); - assert.isTrue(service.inferredProjects[0].dirty); - assert.equal(service.inferredProjects[0].getRootFilesMap().size, 0); - } - }); - describe("when new file is excluded from config", () => { - verifySession({ - withExclude: true, - openFileBeforeCreating: true, - checkProjectBeforeError: checkFooBarInInferredProject, - checkProjectAfterError: checkFooBarInInferredProject - }); + checkProjectAfterError: checkFooBarInInferredProject }); }); }); }); - - describe("unittests:: tsserver:: ConfiguredProjects:: non-existing directories listed in config file input array", () => { - it("should be tolerated without crashing the server", () => { - const configFile = { - path: "/a/b/tsconfig.json", - content: `{ +}); +describe("unittests:: tsserver:: ConfiguredProjects:: non-existing directories listed in config file input array", () => { + it("should be tolerated without crashing the server", () => { + const configFile = { + path: "/a/b/tsconfig.json", + content: `{ "compilerOptions": {}, "include": ["app/*", "test/**/*", "something"] }` - }; - const file1 = { - path: "/a/b/file1.ts", - content: "let t = 10;" - }; - - const host = createServerHost([file1, configFile]); - const projectService = createProjectService(host); - projectService.openClientFile(file1.path); - host.runQueuedTimeoutCallbacks(); - - // Since file1 refers to config file as the default project, it needs to be kept alive - checkNumberOfProjects(projectService, { inferredProjects: 1, configuredProjects: 1 }); - const inferredProject = projectService.inferredProjects[0]; - assert.isTrue(inferredProject.containsFile(file1.path)); - assert.isFalse(projectService.configuredProjects.get(configFile.path)!.containsFile(file1.path)); - }); - - it("should be able to handle @types if input file list is empty", () => { - const f = { - path: "/a/app.ts", - content: "let x = 1" - }; - const config = { - path: "/a/tsconfig.json", - content: JSON.stringify({ - compiler: {}, - files: [] - }) - }; - const t1 = { - path: "/a/node_modules/@types/typings/index.d.ts", - content: `export * from "./lib"` - }; - const t2 = { - path: "/a/node_modules/@types/typings/lib.d.ts", - content: `export const x: number` - }; - const host = createServerHost([f, config, t1, t2], { currentDirectory: getDirectoryPath(f.path) }); - const projectService = createProjectService(host); - - projectService.openClientFile(f.path); - // Since f refers to config file as the default project, it needs to be kept alive - projectService.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 }); - }); - - it("should tolerate invalid include files that start in subDirectory", () => { - const f = { - path: `${tscWatch.projectRoot}/src/server/index.ts`, - content: "let x = 1" - }; - const config = { - path: `${tscWatch.projectRoot}/src/server/tsconfig.json`, - content: JSON.stringify({ - compiler: { - module: "commonjs", - outDir: "../../build" - }, - include: [ - "../src/**/*.ts" - ] - }) - }; - const host = createServerHost([f, config, libFile], { useCaseSensitiveFileNames: true }); - const projectService = createProjectService(host); - - projectService.openClientFile(f.path); - // Since f refers to config file as the default project, it needs to be kept alive - projectService.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 }); - }); - - it("Changed module resolution reflected when specifying files list", () => { - const file1: File = { - path: "/a/b/file1.ts", - content: 'import classc from "file2"' - }; - const file2a: File = { - path: "/a/file2.ts", - content: "export classc { method2a() { return 10; } }" - }; - const file2: File = { - path: "/a/b/file2.ts", - content: "export classc { method2() { return 10; } }" - }; - const configFile: File = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ files: [file1.path], compilerOptions: { module: "amd" } }) - }; - const files = [file1, file2a, configFile, libFile]; - const host = createServerHost(files); - const projectService = createProjectService(host); - projectService.openClientFile(file1.path); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const project = projectService.configuredProjects.get(configFile.path)!; - assert.isDefined(project); - checkProjectActualFiles(project, map(files, file => file.path)); - checkWatchedFiles(host, mapDefined(files, file => file === file1 ? undefined : file.path)); - checkWatchedDirectoriesDetailed(host, ["/a/b"], 1, /*recursive*/ false); - checkWatchedDirectoriesDetailed(host, ["/a/b/node_modules/@types"], 1, /*recursive*/ true); - - files.push(file2); - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - checkProjectActualFiles(project, mapDefined(files, file => file === file2a ? undefined : file.path)); - checkWatchedFiles(host, mapDefined(files, file => file === file1 ? undefined : file.path)); - checkWatchedDirectories(host, emptyArray, /*recursive*/ false); - checkWatchedDirectoriesDetailed(host, ["/a/b/node_modules/@types"], 1, /*recursive*/ true); - - // On next file open the files file2a should be closed and not watched any more - projectService.openClientFile(file2.path); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - checkProjectActualFiles(project, mapDefined(files, file => file === file2a ? undefined : file.path)); - checkWatchedFiles(host, [libFile.path, configFile.path]); - checkWatchedDirectories(host, emptyArray, /*recursive*/ false); - checkWatchedDirectoriesDetailed(host, ["/a/b/node_modules/@types"], 1, /*recursive*/ true); - }); - - it("Failed lookup locations uses parent most node_modules directory", () => { - const root = "/user/username/rootfolder"; - const file1: File = { - path: "/a/b/src/file1.ts", - content: 'import { classc } from "module1"' - }; - const module1: File = { - path: "/a/b/node_modules/module1/index.d.ts", - content: `import { class2 } from "module2"; + }; + const file1 = { + path: "/a/b/file1.ts", + content: "let t = 10;" + }; + const host = createServerHost([file1, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + host.runQueuedTimeoutCallbacks(); + // Since file1 refers to config file as the default project, it needs to be kept alive + checkNumberOfProjects(projectService, { inferredProjects: 1, configuredProjects: 1 }); + const inferredProject = projectService.inferredProjects[0]; + assert.isTrue(inferredProject.containsFile((file1.path))); + assert.isFalse(projectService.configuredProjects.get(configFile.path)!.containsFile((file1.path))); + }); + it("should be able to handle @types if input file list is empty", () => { + const f = { + path: "/a/app.ts", + content: "let x = 1" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compiler: {}, + files: [] + }) + }; + const t1 = { + path: "/a/node_modules/@types/typings/index.d.ts", + content: `export * from "./lib"` + }; + const t2 = { + path: "/a/node_modules/@types/typings/lib.d.ts", + content: `export const x: number` + }; + const host = createServerHost([f, config, t1, t2], { currentDirectory: getDirectoryPath(f.path) }); + const projectService = createProjectService(host); + projectService.openClientFile(f.path); + // Since f refers to config file as the default project, it needs to be kept alive + projectService.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 }); + }); + it("should tolerate invalid include files that start in subDirectory", () => { + const f = { + path: `${projectRoot}/src/server/index.ts`, + content: "let x = 1" + }; + const config = { + path: `${projectRoot}/src/server/tsconfig.json`, + content: JSON.stringify({ + compiler: { + module: "commonjs", + outDir: "../../build" + }, + include: [ + "../src/**/*.ts" + ] + }) + }; + const host = createServerHost([f, config, libFile], { useCaseSensitiveFileNames: true }); + const projectService = createProjectService(host); + projectService.openClientFile(f.path); + // Since f refers to config file as the default project, it needs to be kept alive + projectService.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 }); + }); + it("Changed module resolution reflected when specifying files list", () => { + const file1: File = { + path: "/a/b/file1.ts", + content: 'import classc from "file2"' + }; + const file2a: File = { + path: "/a/file2.ts", + content: "export classc { method2a() { return 10; } }" + }; + const file2: File = { + path: "/a/b/file2.ts", + content: "export classc { method2() { return 10; } }" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ files: [file1.path], compilerOptions: { module: "amd" } }) + }; + const files = [file1, file2a, configFile, libFile]; + const host = createServerHost(files); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = projectService.configuredProjects.get(configFile.path)!; + assert.isDefined(project); + checkProjectActualFiles(project, map(files, file => file.path)); + checkWatchedFiles(host, mapDefined(files, file => file === file1 ? undefined : file.path)); + checkWatchedDirectoriesDetailed(host, ["/a/b"], 1, /*recursive*/ false); + checkWatchedDirectoriesDetailed(host, ["/a/b/node_modules/@types"], 1, /*recursive*/ true); + files.push(file2); + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + checkProjectActualFiles(project, mapDefined(files, file => file === file2a ? undefined : file.path)); + checkWatchedFiles(host, mapDefined(files, file => file === file1 ? undefined : file.path)); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectoriesDetailed(host, ["/a/b/node_modules/@types"], 1, /*recursive*/ true); + // On next file open the files file2a should be closed and not watched any more + projectService.openClientFile(file2.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + checkProjectActualFiles(project, mapDefined(files, file => file === file2a ? undefined : file.path)); + checkWatchedFiles(host, [libFile.path, configFile.path]); + checkWatchedDirectories(host, emptyArray, /*recursive*/ false); + checkWatchedDirectoriesDetailed(host, ["/a/b/node_modules/@types"], 1, /*recursive*/ true); + }); + it("Failed lookup locations uses parent most node_modules directory", () => { + const root = "/user/username/rootfolder"; + const file1: File = { + path: "/a/b/src/file1.ts", + content: 'import { classc } from "module1"' + }; + const module1: File = { + path: "/a/b/node_modules/module1/index.d.ts", + content: `import { class2 } from "module2"; export classc { method2a(): class2; }` - }; - const module2: File = { - path: "/a/b/node_modules/module2/index.d.ts", - content: "export class2 { method2() { return 10; } }" - }; - const module3: File = { - path: "/a/b/node_modules/module/node_modules/module3/index.d.ts", - content: "export class3 { method2() { return 10; } }" - }; - const configFile: File = { - path: "/a/b/src/tsconfig.json", - content: JSON.stringify({ files: ["file1.ts"] }) - }; - const nonLibFiles = [file1, module1, module2, module3, configFile]; - nonLibFiles.forEach(f => f.path = root + f.path); - const files = nonLibFiles.concat(libFile); - const host = createServerHost(files); - const projectService = createProjectService(host); - projectService.openClientFile(file1.path); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const project = projectService.configuredProjects.get(configFile.path)!; - assert.isDefined(project); - checkProjectActualFiles(project, [file1.path, libFile.path, module1.path, module2.path, configFile.path]); - checkWatchedFiles(host, [libFile.path, configFile.path]); - checkWatchedDirectories(host, [], /*recursive*/ false); - const watchedRecursiveDirectories = getTypeRootsFromLocation(root + "/a/b/src"); - watchedRecursiveDirectories.push(`${root}/a/b/src/node_modules`, `${root}/a/b/node_modules`); - checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); - }); + }; + const module2: File = { + path: "/a/b/node_modules/module2/index.d.ts", + content: "export class2 { method2() { return 10; } }" + }; + const module3: File = { + path: "/a/b/node_modules/module/node_modules/module3/index.d.ts", + content: "export class3 { method2() { return 10; } }" + }; + const configFile: File = { + path: "/a/b/src/tsconfig.json", + content: JSON.stringify({ files: ["file1.ts"] }) + }; + const nonLibFiles = [file1, module1, module2, module3, configFile]; + nonLibFiles.forEach(f => f.path = root + f.path); + const files = nonLibFiles.concat(libFile); + const host = createServerHost(files); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = projectService.configuredProjects.get(configFile.path)!; + assert.isDefined(project); + checkProjectActualFiles(project, [file1.path, libFile.path, module1.path, module2.path, configFile.path]); + checkWatchedFiles(host, [libFile.path, configFile.path]); + checkWatchedDirectories(host, [], /*recursive*/ false); + const watchedRecursiveDirectories = getTypeRootsFromLocation(root + "/a/b/src"); + watchedRecursiveDirectories.push(`${root}/a/b/src/node_modules`, `${root}/a/b/node_modules`); + checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); }); -} +}); diff --git a/src/testRunner/unittests/tsserver/declarationFileMaps.ts b/src/testRunner/unittests/tsserver/declarationFileMaps.ts index 0fb08aa051bce..18ed174af0bfb 100644 --- a/src/testRunner/unittests/tsserver/declarationFileMaps.ts +++ b/src/testRunner/unittests/tsserver/declarationFileMaps.ts @@ -1,716 +1,643 @@ -namespace ts.projectSystem { - function documentSpanFromSubstring({ file, text, contextText, options, contextOptions }: DocumentSpanFromSubstring): DocumentSpan { - const contextSpan = contextText !== undefined ? documentSpanFromSubstring({ file, text: contextText, options: contextOptions }) : undefined; - return { - fileName: file.path, - textSpan: textSpanFromSubstring(file.content, text, options), - ...contextSpan && { contextSpan: contextSpan.textSpan } - }; +import { DocumentSpanFromSubstring, textSpanFromSubstring, File, TestSession, openFilesForSession, closeFilesForSession, createServerHost, createSession, checkNumberOfProjects, checkProjectActualFiles, executeSessionRequest, protocol, protocolFileLocationFromSubstring, protocolFileSpanWithContextFromSubstring, protocolTextSpanFromSubstring, CommandNames, protocolFileSpanFromSubstring, makeReferenceItem, protocolLocationFromSubstring, protocolRenameSpanFromSubstring } from "../../ts.projectSystem"; +import { DocumentSpan, RenameLocation, ReferenceEntry, Debug, getFileEmitOutput, OutputFile, CompilerOptions, RawSourceMap, ScriptElementKind, ReferencedSymbol, keywordPart, SyntaxKind, spacePart, displayPart, SymbolDisplayPartKind, punctuationPart, ScriptElementKindModifier } from "../../ts"; +import { NormalizedPath } from "../../ts.server"; +function documentSpanFromSubstring({ file, text, contextText, options, contextOptions }: DocumentSpanFromSubstring): DocumentSpan { + const contextSpan = contextText !== undefined ? documentSpanFromSubstring({ file, text: contextText, options: contextOptions }) : undefined; + return { + fileName: file.path, + textSpan: textSpanFromSubstring(file.content, text, options), + ...contextSpan && { contextSpan: contextSpan.textSpan } + }; +} +function renameLocation(input: DocumentSpanFromSubstring): RenameLocation { + return documentSpanFromSubstring(input); +} +interface MakeReferenceEntry extends DocumentSpanFromSubstring { + isDefinition: boolean; +} +function makeReferenceEntry({ isDefinition, ...rest }: MakeReferenceEntry): ReferenceEntry { + return { + ...documentSpanFromSubstring(rest), + isDefinition, + isWriteAccess: isDefinition, + isInString: undefined, + }; +} +function checkDeclarationFiles(file: File, session: TestSession, expectedFiles: readonly File[]): void { + openFilesForSession([file], session); + const project = Debug.checkDefined(session.getProjectService().getDefaultProjectForFile((file.path as NormalizedPath), /*ensureProject*/ false)); + const program = project.getCurrentProgram()!; + const output = getFileEmitOutput(program, Debug.checkDefined(program.getSourceFile(file.path)), /*emitOnlyDtsFiles*/ true); + closeFilesForSession([file], session); + Debug.assert(!output.emitSkipped); + assert.deepEqual(output.outputFiles, expectedFiles.map((e): OutputFile => ({ name: e.path, text: e.content, writeByteOrderMark: false }))); +} +describe("unittests:: tsserver:: with declaration file maps:: project references", () => { + const aTs: File = { + path: "/a/a.ts", + content: "export function fnA() {}\nexport interface IfaceA {}\nexport const instanceA: IfaceA = {};", + }; + const compilerOptions: CompilerOptions = { + outDir: "bin", + declaration: true, + declarationMap: true, + composite: true, + }; + const configContent = JSON.stringify({ compilerOptions }); + const aTsconfig: File = { path: "/a/tsconfig.json", content: configContent }; + const aDtsMapContent: RawSourceMap = { + version: 3, + file: "a.d.ts", + sourceRoot: "", + sources: ["../a.ts"], + names: [], + mappings: "AAAA,wBAAgB,GAAG,SAAK;AACxB,MAAM,WAAW,MAAM;CAAG;AAC1B,eAAO,MAAM,SAAS,EAAE,MAAW,CAAC" + }; + const aDtsMap: File = { + path: "/a/bin/a.d.ts.map", + content: JSON.stringify(aDtsMapContent), + }; + const aDts: File = { + path: "/a/bin/a.d.ts", + // Need to mangle the sourceMappingURL part or it breaks the build + content: `export declare function fnA(): void;\nexport interface IfaceA {\n}\nexport declare const instanceA: IfaceA;\n//# source${""}MappingURL=a.d.ts.map`, + }; + const bTs: File = { + path: "/b/b.ts", + content: "export function fnB() {}", + }; + const bTsconfig: File = { path: "/b/tsconfig.json", content: configContent }; + const bDtsMapContent: RawSourceMap = { + version: 3, + file: "b.d.ts", + sourceRoot: "", + sources: ["../b.ts"], + names: [], + mappings: "AAAA,wBAAgB,GAAG,SAAK", + }; + const bDtsMap: File = { + path: "/b/bin/b.d.ts.map", + content: JSON.stringify(bDtsMapContent), + }; + const bDts: File = { + // Need to mangle the sourceMappingURL part or it breaks the build + path: "/b/bin/b.d.ts", + content: `export declare function fnB(): void;\n//# source${""}MappingURL=b.d.ts.map`, + }; + const dummyFile: File = { + path: "/dummy/dummy.ts", + content: "let a = 10;" + }; + const userTs: File = { + path: "/user/user.ts", + content: 'import * as a from "../a/bin/a";\nimport * as b from "../b/bin/b";\nexport function fnUser() { a.fnA(); b.fnB(); a.instanceA; }', + }; + const userTsForConfigProject: File = { + path: "/user/user.ts", + content: 'import * as a from "../a/a";\nimport * as b from "../b/b";\nexport function fnUser() { a.fnA(); b.fnB(); a.instanceA; }', + }; + const userTsconfig: File = { + path: "/user/tsconfig.json", + content: JSON.stringify({ + file: ["user.ts"], + references: [{ path: "../a" }, { path: "../b" }] + }) + }; + function makeSampleProjects(addUserTsConfig?: boolean) { + const host = createServerHost([aTs, aTsconfig, aDtsMap, aDts, bTsconfig, bTs, bDtsMap, bDts, ...(addUserTsConfig ? [userTsForConfigProject, userTsconfig] : [userTs]), dummyFile]); + const session = createSession(host); + checkDeclarationFiles(aTs, session, [aDtsMap, aDts]); + checkDeclarationFiles(bTs, session, [bDtsMap, bDts]); + // Testing what happens if we delete the original sources. + host.deleteFile(bTs.path); + openFilesForSession([userTs], session); + const service = session.getProjectService(); + // If config file then userConfig project and bConfig project since it is referenced + checkNumberOfProjects(service, addUserTsConfig ? { configuredProjects: 2 } : { inferredProjects: 1 }); + return session; } - - function renameLocation(input: DocumentSpanFromSubstring): RenameLocation { - return documentSpanFromSubstring(input); + function verifyInferredProjectUnchanged(session: TestSession) { + checkProjectActualFiles(session.getProjectService().inferredProjects[0], [userTs.path, aDts.path, bDts.path]); } - - interface MakeReferenceEntry extends DocumentSpanFromSubstring { - isDefinition: boolean; + function verifyDummyProject(session: TestSession) { + checkProjectActualFiles(session.getProjectService().inferredProjects[0], [dummyFile.path]); } - function makeReferenceEntry({ isDefinition, ...rest }: MakeReferenceEntry): ReferenceEntry { - return { - ...documentSpanFromSubstring(rest), - isDefinition, - isWriteAccess: isDefinition, - isInString: undefined, - }; + function verifyOnlyOrphanInferredProject(session: TestSession) { + openFilesForSession([dummyFile], session); + checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1 }); + verifyDummyProject(session); } - - function checkDeclarationFiles(file: File, session: TestSession, expectedFiles: readonly File[]): void { - openFilesForSession([file], session); - const project = Debug.checkDefined(session.getProjectService().getDefaultProjectForFile(file.path as server.NormalizedPath, /*ensureProject*/ false)); - const program = project.getCurrentProgram()!; - const output = getFileEmitOutput(program, Debug.checkDefined(program.getSourceFile(file.path)), /*emitOnlyDtsFiles*/ true); - closeFilesForSession([file], session); - - Debug.assert(!output.emitSkipped); - assert.deepEqual(output.outputFiles, expectedFiles.map((e): OutputFile => ({ name: e.path, text: e.content, writeByteOrderMark: false }))); + function verifySingleInferredProject(session: TestSession) { + checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1 }); + verifyInferredProjectUnchanged(session); + // Close user file should close all the projects after opening dummy file + closeFilesForSession([userTs], session); + verifyOnlyOrphanInferredProject(session); } - - describe("unittests:: tsserver:: with declaration file maps:: project references", () => { - const aTs: File = { - path: "/a/a.ts", - content: "export function fnA() {}\nexport interface IfaceA {}\nexport const instanceA: IfaceA = {};", - }; - const compilerOptions: CompilerOptions = { - outDir: "bin", - declaration: true, - declarationMap: true, - composite: true, - }; - const configContent = JSON.stringify({ compilerOptions }); - const aTsconfig: File = { path: "/a/tsconfig.json", content: configContent }; - - const aDtsMapContent: RawSourceMap = { - version: 3, - file: "a.d.ts", - sourceRoot: "", - sources: ["../a.ts"], - names: [], - mappings: "AAAA,wBAAgB,GAAG,SAAK;AACxB,MAAM,WAAW,MAAM;CAAG;AAC1B,eAAO,MAAM,SAAS,EAAE,MAAW,CAAC" - }; - const aDtsMap: File = { - path: "/a/bin/a.d.ts.map", - content: JSON.stringify(aDtsMapContent), - }; - const aDts: File = { - path: "/a/bin/a.d.ts", - // Need to mangle the sourceMappingURL part or it breaks the build - content: `export declare function fnA(): void;\nexport interface IfaceA {\n}\nexport declare const instanceA: IfaceA;\n//# source${""}MappingURL=a.d.ts.map`, - }; - - const bTs: File = { - path: "/b/b.ts", - content: "export function fnB() {}", - }; - const bTsconfig: File = { path: "/b/tsconfig.json", content: configContent }; - - const bDtsMapContent: RawSourceMap = { - version: 3, - file: "b.d.ts", - sourceRoot: "", - sources: ["../b.ts"], - names: [], - mappings: "AAAA,wBAAgB,GAAG,SAAK", - }; - const bDtsMap: File = { - path: "/b/bin/b.d.ts.map", - content: JSON.stringify(bDtsMapContent), - }; - const bDts: File = { - // Need to mangle the sourceMappingURL part or it breaks the build - path: "/b/bin/b.d.ts", - content: `export declare function fnB(): void;\n//# source${""}MappingURL=b.d.ts.map`, - }; - - const dummyFile: File = { - path: "/dummy/dummy.ts", - content: "let a = 10;" - }; - - const userTs: File = { - path: "/user/user.ts", - content: 'import * as a from "../a/bin/a";\nimport * as b from "../b/bin/b";\nexport function fnUser() { a.fnA(); b.fnB(); a.instanceA; }', - }; - - const userTsForConfigProject: File = { - path: "/user/user.ts", - content: 'import * as a from "../a/a";\nimport * as b from "../b/b";\nexport function fnUser() { a.fnA(); b.fnB(); a.instanceA; }', - }; - - const userTsconfig: File = { - path: "/user/tsconfig.json", - content: JSON.stringify({ - file: ["user.ts"], - references: [{ path: "../a" }, { path: "../b" }] + function verifyATsConfigProject(session: TestSession) { + checkProjectActualFiles((session.getProjectService().configuredProjects.get(aTsconfig.path)!), [aTs.path, aTsconfig.path]); + } + function verifyATsConfigOriginalProject(session: TestSession) { + checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 }); + verifyInferredProjectUnchanged(session); + verifyATsConfigProject(session); + // Close user file should close all the projects + closeFilesForSession([userTs], session); + verifyOnlyOrphanInferredProject(session); + } + function verifyATsConfigWhenOpened(session: TestSession) { + checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 }); + verifyInferredProjectUnchanged(session); + verifyATsConfigProject(session); + closeFilesForSession([userTs], session); + openFilesForSession([dummyFile], session); + checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 }); + verifyDummyProject(session); + verifyATsConfigProject(session); // ATsConfig should still be alive + } + function verifyUserTsConfigProject(session: TestSession) { + checkProjectActualFiles((session.getProjectService().configuredProjects.get(userTsconfig.path)!), [userTs.path, aTs.path, userTsconfig.path]); + } + it("goToDefinition", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, protocol.CommandTypes.Definition, protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual(response, [ + protocolFileSpanWithContextFromSubstring({ + file: aTs, + text: "fnA", + contextText: "export function fnA() {}" }) - }; - - function makeSampleProjects(addUserTsConfig?: boolean) { - const host = createServerHost([aTs, aTsconfig, aDtsMap, aDts, bTsconfig, bTs, bDtsMap, bDts, ...(addUserTsConfig ? [userTsForConfigProject, userTsconfig] : [userTs]), dummyFile]); - const session = createSession(host); - - checkDeclarationFiles(aTs, session, [aDtsMap, aDts]); - checkDeclarationFiles(bTs, session, [bDtsMap, bDts]); - - // Testing what happens if we delete the original sources. - host.deleteFile(bTs.path); - - openFilesForSession([userTs], session); - const service = session.getProjectService(); - // If config file then userConfig project and bConfig project since it is referenced - checkNumberOfProjects(service, addUserTsConfig ? { configuredProjects: 2 } : { inferredProjects: 1 }); - return session; - } - - function verifyInferredProjectUnchanged(session: TestSession) { - checkProjectActualFiles(session.getProjectService().inferredProjects[0], [userTs.path, aDts.path, bDts.path]); - } - - function verifyDummyProject(session: TestSession) { - checkProjectActualFiles(session.getProjectService().inferredProjects[0], [dummyFile.path]); - } - - function verifyOnlyOrphanInferredProject(session: TestSession) { - openFilesForSession([dummyFile], session); - checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1 }); - verifyDummyProject(session); - } - - function verifySingleInferredProject(session: TestSession) { - checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1 }); - verifyInferredProjectUnchanged(session); - - // Close user file should close all the projects after opening dummy file - closeFilesForSession([userTs], session); - verifyOnlyOrphanInferredProject(session); - } - - function verifyATsConfigProject(session: TestSession) { - checkProjectActualFiles(session.getProjectService().configuredProjects.get(aTsconfig.path)!, [aTs.path, aTsconfig.path]); - } - - function verifyATsConfigOriginalProject(session: TestSession) { - checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 }); - verifyInferredProjectUnchanged(session); - verifyATsConfigProject(session); - // Close user file should close all the projects - closeFilesForSession([userTs], session); - verifyOnlyOrphanInferredProject(session); - } - - function verifyATsConfigWhenOpened(session: TestSession) { - checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 }); - verifyInferredProjectUnchanged(session); - verifyATsConfigProject(session); - - closeFilesForSession([userTs], session); - openFilesForSession([dummyFile], session); - checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 }); - verifyDummyProject(session); - verifyATsConfigProject(session); // ATsConfig should still be alive - } - - function verifyUserTsConfigProject(session: TestSession) { - checkProjectActualFiles(session.getProjectService().configuredProjects.get(userTsconfig.path)!, [userTs.path, aTs.path, userTsconfig.path]); - } - - it("goToDefinition", () => { - const session = makeSampleProjects(); - const response = executeSessionRequest(session, protocol.CommandTypes.Definition, protocolFileLocationFromSubstring(userTs, "fnA()")); - assert.deepEqual(response, [ + ]); + verifySingleInferredProject(session); + }); + it("getDefinitionAndBoundSpan", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, protocol.CommandTypes.DefinitionAndBoundSpan, protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual(response, { + textSpan: protocolTextSpanFromSubstring(userTs.content, "fnA"), + definitions: [ protocolFileSpanWithContextFromSubstring({ file: aTs, text: "fnA", contextText: "export function fnA() {}" }) - ]); - verifySingleInferredProject(session); - }); - - it("getDefinitionAndBoundSpan", () => { - const session = makeSampleProjects(); - const response = executeSessionRequest(session, protocol.CommandTypes.DefinitionAndBoundSpan, protocolFileLocationFromSubstring(userTs, "fnA()")); - assert.deepEqual(response, { - textSpan: protocolTextSpanFromSubstring(userTs.content, "fnA"), - definitions: [ - protocolFileSpanWithContextFromSubstring({ - file: aTs, - text: "fnA", - contextText: "export function fnA() {}" - }) - ], - }); - verifySingleInferredProject(session); - }); - - it("getDefinitionAndBoundSpan with file navigation", () => { - const session = makeSampleProjects(/*addUserTsConfig*/ true); - const response = executeSessionRequest(session, protocol.CommandTypes.DefinitionAndBoundSpan, protocolFileLocationFromSubstring(userTs, "fnA()")); - assert.deepEqual(response, { - textSpan: protocolTextSpanFromSubstring(userTs.content, "fnA"), - definitions: [ - protocolFileSpanWithContextFromSubstring({ - file: aTs, - text: "fnA", - contextText: "export function fnA() {}" - }) - ], - }); - checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); - verifyUserTsConfigProject(session); - - // Navigate to the definition - closeFilesForSession([userTs], session); - openFilesForSession([aTs], session); - - // UserTs configured project should be alive - checkNumberOfProjects(session.getProjectService(), { configuredProjects: 3 }); - verifyUserTsConfigProject(session); - verifyATsConfigProject(session); - - closeFilesForSession([aTs], session); - verifyOnlyOrphanInferredProject(session); - }); - - it("goToType", () => { - const session = makeSampleProjects(); - const response = executeSessionRequest(session, protocol.CommandTypes.TypeDefinition, protocolFileLocationFromSubstring(userTs, "instanceA")); - assert.deepEqual(response, [ - protocolFileSpanWithContextFromSubstring({ - file: aTs, - text: "IfaceA", - contextText: "export interface IfaceA {}" - }) - ]); - verifySingleInferredProject(session); + ], }); - - it("goToImplementation", () => { - const session = makeSampleProjects(); - const response = executeSessionRequest(session, protocol.CommandTypes.Implementation, protocolFileLocationFromSubstring(userTs, "fnA()")); - assert.deepEqual(response, [ + verifySingleInferredProject(session); + }); + it("getDefinitionAndBoundSpan with file navigation", () => { + const session = makeSampleProjects(/*addUserTsConfig*/ true); + const response = executeSessionRequest(session, protocol.CommandTypes.DefinitionAndBoundSpan, protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual(response, { + textSpan: protocolTextSpanFromSubstring(userTs.content, "fnA"), + definitions: [ protocolFileSpanWithContextFromSubstring({ file: aTs, text: "fnA", contextText: "export function fnA() {}" - })]); - verifySingleInferredProject(session); + }) + ], }); - - it("goToDefinition -- target does not exist", () => { - const session = makeSampleProjects(); - const response = executeSessionRequest(session, CommandNames.Definition, protocolFileLocationFromSubstring(userTs, "fnB()")); - // bTs does not exist, so stick with bDts - assert.deepEqual(response, [ - protocolFileSpanWithContextFromSubstring({ + checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); + verifyUserTsConfigProject(session); + // Navigate to the definition + closeFilesForSession([userTs], session); + openFilesForSession([aTs], session); + // UserTs configured project should be alive + checkNumberOfProjects(session.getProjectService(), { configuredProjects: 3 }); + verifyUserTsConfigProject(session); + verifyATsConfigProject(session); + closeFilesForSession([aTs], session); + verifyOnlyOrphanInferredProject(session); + }); + it("goToType", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, protocol.CommandTypes.TypeDefinition, protocolFileLocationFromSubstring(userTs, "instanceA")); + assert.deepEqual(response, [ + protocolFileSpanWithContextFromSubstring({ + file: aTs, + text: "IfaceA", + contextText: "export interface IfaceA {}" + }) + ]); + verifySingleInferredProject(session); + }); + it("goToImplementation", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, protocol.CommandTypes.Implementation, protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual(response, [ + protocolFileSpanWithContextFromSubstring({ + file: aTs, + text: "fnA", + contextText: "export function fnA() {}" + }) + ]); + verifySingleInferredProject(session); + }); + it("goToDefinition -- target does not exist", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, CommandNames.Definition, protocolFileLocationFromSubstring(userTs, "fnB()")); + // bTs does not exist, so stick with bDts + assert.deepEqual(response, [ + protocolFileSpanWithContextFromSubstring({ + file: bDts, + text: "fnB", + contextText: "export declare function fnB(): void;" + }) + ]); + verifySingleInferredProject(session); + }); + it("navigateTo", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, CommandNames.Navto, { file: userTs.path, searchValue: "fn" }); + assert.deepEqual(response, [ + { + ...protocolFileSpanFromSubstring({ file: bDts, - text: "fnB", - contextText: "export declare function fnB(): void;" - }) - ]); - verifySingleInferredProject(session); + text: "export declare function fnB(): void;" + }), + name: "fnB", + matchKind: "prefix", + isCaseSensitive: true, + kind: ScriptElementKind.functionElement, + kindModifiers: "export,declare", + }, + { + ...protocolFileSpanFromSubstring({ + file: userTs, + text: "export function fnUser() { a.fnA(); b.fnB(); a.instanceA; }" + }), + name: "fnUser", + matchKind: "prefix", + isCaseSensitive: true, + kind: ScriptElementKind.functionElement, + kindModifiers: "export", + }, + { + ...protocolFileSpanFromSubstring({ + file: aTs, + text: "export function fnA() {}" + }), + name: "fnA", + matchKind: "prefix", + isCaseSensitive: true, + kind: ScriptElementKind.functionElement, + kindModifiers: "export", + }, + ]); + verifyATsConfigOriginalProject(session); + }); + const referenceATs = (aTs: File): protocol.ReferencesResponseItem => makeReferenceItem({ + file: aTs, + isDefinition: true, + text: "fnA", + contextText: "export function fnA() {}", + lineText: "export function fnA() {}" + }); + const referencesUserTs = (userTs: File): readonly protocol.ReferencesResponseItem[] => [ + makeReferenceItem({ + file: userTs, + isDefinition: false, + text: "fnA", + lineText: "export function fnUser() { a.fnA(); b.fnB(); a.instanceA; }" + }), + ]; + it("findAllReferences", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual(response, { + refs: [...referencesUserTs(userTs), referenceATs(aTs)], + symbolName: "fnA", + symbolStartOffset: protocolLocationFromSubstring(userTs.content, "fnA()").offset, + symbolDisplayString: "function fnA(): void", }); - - it("navigateTo", () => { - const session = makeSampleProjects(); - const response = executeSessionRequest(session, CommandNames.Navto, { file: userTs.path, searchValue: "fn" }); - assert.deepEqual(response, [ - { - ...protocolFileSpanFromSubstring({ - file: bDts, - text: "export declare function fnB(): void;" + verifyATsConfigOriginalProject(session); + }); + it("findAllReferences -- starting at definition", () => { + const session = makeSampleProjects(); + openFilesForSession([aTs], session); // If it's not opened, the reference isn't found. + const response = executeSessionRequest(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(aTs, "fnA")); + assert.deepEqual(response, { + refs: [referenceATs(aTs), ...referencesUserTs(userTs)], + symbolName: "fnA", + symbolStartOffset: protocolLocationFromSubstring(aTs.content, "fnA").offset, + symbolDisplayString: "function fnA(): void", + }); + verifyATsConfigWhenOpened(session); + }); + interface ReferencesFullRequest extends protocol.FileLocationRequest { + readonly command: protocol.CommandTypes.ReferencesFull; + } + interface ReferencesFullResponse extends protocol.Response { + readonly body: readonly ReferencedSymbol[]; + } + it("findAllReferencesFull", () => { + const session = makeSampleProjects(); + const responseFull = executeSessionRequest(session, protocol.CommandTypes.ReferencesFull, protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual(responseFull, [ + { + definition: { + ...documentSpanFromSubstring({ + file: aTs, + text: "fnA", + contextText: "export function fnA() {}" }), - name: "fnB", - matchKind: "prefix", - isCaseSensitive: true, kind: ScriptElementKind.functionElement, - kindModifiers: "export,declare", + name: "function fnA(): void", + containerKind: ScriptElementKind.unknown, + containerName: "", + displayParts: [ + keywordPart(SyntaxKind.FunctionKeyword), + spacePart(), + displayPart("fnA", SymbolDisplayPartKind.functionName), + punctuationPart(SyntaxKind.OpenParenToken), + punctuationPart(SyntaxKind.CloseParenToken), + punctuationPart(SyntaxKind.ColonToken), + spacePart(), + keywordPart(SyntaxKind.VoidKeyword), + ], }, - { - ...protocolFileSpanFromSubstring({ - file: userTs, - text: "export function fnUser() { a.fnA(); b.fnB(); a.instanceA; }" + references: [ + makeReferenceEntry({ file: userTs, /*isDefinition*/ isDefinition: false, text: "fnA" }), + makeReferenceEntry({ file: aTs, /*isDefinition*/ isDefinition: true, text: "fnA", contextText: "export function fnA() {}" }), + ], + }, + ]); + verifyATsConfigOriginalProject(session); + }); + it("findAllReferencesFull definition is in mapped file", () => { + const aTs: File = { path: "/a/a.ts", content: `function f() {}` }; + const aTsconfig: File = { + path: "/a/tsconfig.json", + content: JSON.stringify({ compilerOptions: { declaration: true, declarationMap: true, outFile: "../bin/a.js" } }), + }; + const bTs: File = { path: "/b/b.ts", content: `f();` }; + const bTsconfig: File = { path: "/b/tsconfig.json", content: JSON.stringify({ references: [{ path: "../a" }] }) }; + const aDts: File = { path: "/bin/a.d.ts", content: `declare function f(): void;\n//# sourceMappingURL=a.d.ts.map` }; + const aDtsMap: File = { + path: "/bin/a.d.ts.map", + content: JSON.stringify({ version: 3, file: "a.d.ts", sourceRoot: "", sources: ["../a/a.ts"], names: [], mappings: "AAAA,iBAAS,CAAC,SAAK" }), + }; + const session = createSession(createServerHost([aTs, aTsconfig, bTs, bTsconfig, aDts, aDtsMap])); + checkDeclarationFiles(aTs, session, [aDtsMap, aDts]); + openFilesForSession([bTs], session); + checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); // configured project of b is alive since a references b + const responseFull = executeSessionRequest(session, protocol.CommandTypes.ReferencesFull, protocolFileLocationFromSubstring(bTs, "f()")); + assert.deepEqual(responseFull, [ + { + definition: { + ...documentSpanFromSubstring({ + file: aTs, + text: "f", + options: { index: 1 }, + contextText: "function f() {}" }), - name: "fnUser", - matchKind: "prefix", - isCaseSensitive: true, + containerKind: ScriptElementKind.unknown, + containerName: "", + displayParts: [ + keywordPart(SyntaxKind.FunctionKeyword), + spacePart(), + displayPart("f", SymbolDisplayPartKind.functionName), + punctuationPart(SyntaxKind.OpenParenToken), + punctuationPart(SyntaxKind.CloseParenToken), + punctuationPart(SyntaxKind.ColonToken), + spacePart(), + keywordPart(SyntaxKind.VoidKeyword), + ], kind: ScriptElementKind.functionElement, - kindModifiers: "export", + name: "function f(): void", }, - { - ...protocolFileSpanFromSubstring({ + references: [ + makeReferenceEntry({ file: aTs, - text: "export function fnA() {}" + text: "f", + options: { index: 1 }, + contextText: "function f() {}", + isDefinition: true }), - name: "fnA", - matchKind: "prefix", - isCaseSensitive: true, - kind: ScriptElementKind.functionElement, - kindModifiers: "export", - }, - ]); - - verifyATsConfigOriginalProject(session); - }); - - const referenceATs = (aTs: File): protocol.ReferencesResponseItem => makeReferenceItem({ - file: aTs, - isDefinition: true, - text: "fnA", - contextText: "export function fnA() {}", - lineText: "export function fnA() {}" + { + fileName: bTs.path, + isDefinition: false, + isInString: undefined, + isWriteAccess: false, + textSpan: { start: 0, length: 1 }, + }, + ], + } + ]); + }); + it("findAllReferences -- target does not exist", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(userTs, "fnB()")); + assert.deepEqual(response, { + refs: [ + makeReferenceItem({ + file: bDts, + isDefinition: true, + text: "fnB", + contextText: "export declare function fnB(): void;", + lineText: "export declare function fnB(): void;" + }), + makeReferenceItem({ + file: userTs, + isDefinition: false, + text: "fnB", + lineText: "export function fnUser() { a.fnA(); b.fnB(); a.instanceA; }" + }), + ], + symbolName: "fnB", + symbolStartOffset: protocolLocationFromSubstring(userTs.content, "fnB()").offset, + symbolDisplayString: "function fnB(): void", }); - const referencesUserTs = (userTs: File): readonly protocol.ReferencesResponseItem[] => [ - makeReferenceItem({ - file: userTs, - isDefinition: false, + verifySingleInferredProject(session); + }); + const renameATs = (aTs: File): protocol.SpanGroup => ({ + file: aTs.path, + locs: [ + protocolRenameSpanFromSubstring({ + fileText: aTs.content, text: "fnA", - lineText: "export function fnUser() { a.fnA(); b.fnB(); a.instanceA; }" - }), - ]; - - it("findAllReferences", () => { - const session = makeSampleProjects(); - - const response = executeSessionRequest(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(userTs, "fnA()")); - assert.deepEqual(response, { - refs: [...referencesUserTs(userTs), referenceATs(aTs)], - symbolName: "fnA", - symbolStartOffset: protocolLocationFromSubstring(userTs.content, "fnA()").offset, - symbolDisplayString: "function fnA(): void", - }); - - verifyATsConfigOriginalProject(session); + contextText: "export function fnA() {}" + }) + ], + }); + const renameUserTs = (userTs: File): protocol.SpanGroup => ({ + file: userTs.path, + locs: [ + protocolRenameSpanFromSubstring({ + fileText: userTs.content, + text: "fnA" + }) + ], + }); + it("renameLocations", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual(response, { + info: { + canRename: true, + displayName: "fnA", + fileToRename: undefined, + fullDisplayName: '"/a/bin/a".fnA', + kind: ScriptElementKind.functionElement, + kindModifiers: [ScriptElementKindModifier.exportedModifier, ScriptElementKindModifier.ambientModifier].join(","), + triggerSpan: protocolTextSpanFromSubstring(userTs.content, "fnA"), + }, + locs: [renameUserTs(userTs), renameATs(aTs)], }); - - it("findAllReferences -- starting at definition", () => { - const session = makeSampleProjects(); - openFilesForSession([aTs], session); // If it's not opened, the reference isn't found. - const response = executeSessionRequest(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(aTs, "fnA")); - assert.deepEqual(response, { - refs: [referenceATs(aTs), ...referencesUserTs(userTs)], - symbolName: "fnA", - symbolStartOffset: protocolLocationFromSubstring(aTs.content, "fnA").offset, - symbolDisplayString: "function fnA(): void", - }); - verifyATsConfigWhenOpened(session); + verifyATsConfigOriginalProject(session); + }); + it("renameLocations -- starting at definition", () => { + const session = makeSampleProjects(); + openFilesForSession([aTs], session); // If it's not opened, the reference isn't found. + const response = executeSessionRequest(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(aTs, "fnA")); + assert.deepEqual(response, { + info: { + canRename: true, + displayName: "fnA", + fileToRename: undefined, + fullDisplayName: '"/a/a".fnA', + kind: ScriptElementKind.functionElement, + kindModifiers: ScriptElementKindModifier.exportedModifier, + triggerSpan: protocolTextSpanFromSubstring(aTs.content, "fnA"), + }, + locs: [renameATs(aTs), renameUserTs(userTs)], }); - - interface ReferencesFullRequest extends protocol.FileLocationRequest { readonly command: protocol.CommandTypes.ReferencesFull; } - interface ReferencesFullResponse extends protocol.Response { readonly body: readonly ReferencedSymbol[]; } - - it("findAllReferencesFull", () => { - const session = makeSampleProjects(); - - const responseFull = executeSessionRequest(session, protocol.CommandTypes.ReferencesFull, protocolFileLocationFromSubstring(userTs, "fnA()")); - - assert.deepEqual(responseFull, [ + verifyATsConfigWhenOpened(session); + }); + it("renameLocationsFull", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, protocol.CommandTypes.RenameLocationsFull, protocolFileLocationFromSubstring(userTs, "fnA()")); + assert.deepEqual(response, [ + renameLocation({ file: userTs, text: "fnA" }), + renameLocation({ file: aTs, text: "fnA", contextText: "export function fnA() {}" }), + ]); + verifyATsConfigOriginalProject(session); + }); + it("renameLocations -- target does not exist", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(userTs, "fnB()")); + assert.deepEqual(response, { + info: { + canRename: true, + displayName: "fnB", + fileToRename: undefined, + fullDisplayName: '"/b/bin/b".fnB', + kind: ScriptElementKind.functionElement, + kindModifiers: [ScriptElementKindModifier.exportedModifier, ScriptElementKindModifier.ambientModifier].join(","), + triggerSpan: protocolTextSpanFromSubstring(userTs.content, "fnB"), + }, + locs: [ { - definition: { - ...documentSpanFromSubstring({ - file: aTs, - text: "fnA", - contextText: "export function fnA() {}" - }), - kind: ScriptElementKind.functionElement, - name: "function fnA(): void", - containerKind: ScriptElementKind.unknown, - containerName: "", - displayParts: [ - keywordPart(SyntaxKind.FunctionKeyword), - spacePart(), - displayPart("fnA", SymbolDisplayPartKind.functionName), - punctuationPart(SyntaxKind.OpenParenToken), - punctuationPart(SyntaxKind.CloseParenToken), - punctuationPart(SyntaxKind.ColonToken), - spacePart(), - keywordPart(SyntaxKind.VoidKeyword), - ], - }, - references: [ - makeReferenceEntry({ file: userTs, /*isDefinition*/ isDefinition: false, text: "fnA" }), - makeReferenceEntry({ file: aTs, /*isDefinition*/ isDefinition: true, text: "fnA", contextText: "export function fnA() {}" }), + file: bDts.path, + locs: [ + protocolRenameSpanFromSubstring({ + fileText: bDts.content, + text: "fnB", + contextText: "export declare function fnB(): void;" + }) ], }, - ]); - verifyATsConfigOriginalProject(session); - }); - - it("findAllReferencesFull definition is in mapped file", () => { - const aTs: File = { path: "/a/a.ts", content: `function f() {}` }; - const aTsconfig: File = { - path: "/a/tsconfig.json", - content: JSON.stringify({ compilerOptions: { declaration: true, declarationMap: true, outFile: "../bin/a.js" } }), - }; - const bTs: File = { path: "/b/b.ts", content: `f();` }; - const bTsconfig: File = { path: "/b/tsconfig.json", content: JSON.stringify({ references: [{ path: "../a" }] }) }; - const aDts: File = { path: "/bin/a.d.ts", content: `declare function f(): void;\n//# sourceMappingURL=a.d.ts.map` }; - const aDtsMap: File = { - path: "/bin/a.d.ts.map", - content: JSON.stringify({ version: 3, file: "a.d.ts", sourceRoot: "", sources: ["../a/a.ts"], names: [], mappings: "AAAA,iBAAS,CAAC,SAAK" }), - }; - - const session = createSession(createServerHost([aTs, aTsconfig, bTs, bTsconfig, aDts, aDtsMap])); - checkDeclarationFiles(aTs, session, [aDtsMap, aDts]); - openFilesForSession([bTs], session); - checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); // configured project of b is alive since a references b - - const responseFull = executeSessionRequest(session, protocol.CommandTypes.ReferencesFull, protocolFileLocationFromSubstring(bTs, "f()")); - - assert.deepEqual(responseFull, [ { - definition: { - ...documentSpanFromSubstring({ - file: aTs, - text: "f", - options: { index: 1 }, - contextText: "function f() {}" - }), - containerKind: ScriptElementKind.unknown, - containerName: "", - displayParts: [ - keywordPart(SyntaxKind.FunctionKeyword), - spacePart(), - displayPart("f", SymbolDisplayPartKind.functionName), - punctuationPart(SyntaxKind.OpenParenToken), - punctuationPart(SyntaxKind.CloseParenToken), - punctuationPart(SyntaxKind.ColonToken), - spacePart(), - keywordPart(SyntaxKind.VoidKeyword), - ], - kind: ScriptElementKind.functionElement, - name: "function f(): void", - }, - references: [ - makeReferenceEntry({ - file: aTs, - text: "f", - options: { index: 1 }, - contextText: "function f() {}", - isDefinition: true - }), - { - fileName: bTs.path, - isDefinition: false, - isInString: undefined, - isWriteAccess: false, - textSpan: { start: 0, length: 1 }, - }, + file: userTs.path, + locs: [ + protocolRenameSpanFromSubstring({ + fileText: userTs.content, + text: "fnB" + }) ], - } - ]); + }, + ], }); - - it("findAllReferences -- target does not exist", () => { - const session = makeSampleProjects(); - - const response = executeSessionRequest(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(userTs, "fnB()")); - assert.deepEqual(response, { - refs: [ - makeReferenceItem({ - file: bDts, - isDefinition: true, - text: "fnB", - contextText: "export declare function fnB(): void;", - lineText: "export declare function fnB(): void;" - }), - makeReferenceItem({ - file: userTs, - isDefinition: false, - text: "fnB", - lineText: "export function fnUser() { a.fnA(); b.fnB(); a.instanceA; }" - }), + verifySingleInferredProject(session); + }); + it("getEditsForFileRename", () => { + const session = makeSampleProjects(); + const response = executeSessionRequest(session, protocol.CommandTypes.GetEditsForFileRename, { + oldFilePath: aTs.path, + newFilePath: "/a/aNew.ts", + }); + assert.deepEqual(response, [ + { + fileName: userTs.path, + textChanges: [ + { ...protocolTextSpanFromSubstring(userTs.content, "../a/bin/a"), newText: "../a/bin/aNew" }, ], - symbolName: "fnB", - symbolStartOffset: protocolLocationFromSubstring(userTs.content, "fnB()").offset, - symbolDisplayString: "function fnB(): void", - }); - verifySingleInferredProject(session); + }, + ]); + verifySingleInferredProject(session); + }); + it("getEditsForFileRename when referencing project doesnt include file and its renamed", () => { + const aTs: File = { path: "/a/src/a.ts", content: "" }; + const aTsconfig: File = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + composite: true, + declaration: true, + declarationMap: true, + outDir: "./build", + } + }), + }; + const bTs: File = { path: "/b/src/b.ts", content: "" }; + const bTsconfig: File = { + path: "/b/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + composite: true, + outDir: "./build", + }, + include: ["./src"], + references: [{ path: "../a" }], + }), + }; + const host = createServerHost([aTs, aTsconfig, bTs, bTsconfig]); + const session = createSession(host); + openFilesForSession([aTs, bTs], session); + const response = executeSessionRequest(session, CommandNames.GetEditsForFileRename, { + oldFilePath: aTs.path, + newFilePath: "/a/src/a1.ts", }); - - const renameATs = (aTs: File): protocol.SpanGroup => ({ - file: aTs.path, - locs: [ - protocolRenameSpanFromSubstring({ - fileText: aTs.content, + assert.deepEqual(response, []); // Should not change anything + }); + it("does not jump to source if inlined sources", () => { + const aDtsInlinedSources: RawSourceMap = { + ...aDtsMapContent, + sourcesContent: [aTs.content] + }; + const aDtsMapInlinedSources: File = { + path: aDtsMap.path, + content: JSON.stringify(aDtsInlinedSources) + }; + const host = createServerHost([aTs, aDtsMapInlinedSources, aDts, bTs, bDtsMap, bDts, userTs, dummyFile]); + const session = createSession(host); + openFilesForSession([userTs], session); + const service = session.getProjectService(); + // If config file then userConfig project and bConfig project since it is referenced + checkNumberOfProjects(service, { inferredProjects: 1 }); + // Inlined so does not jump to aTs + assert.deepEqual(executeSessionRequest(session, protocol.CommandTypes.DefinitionAndBoundSpan, protocolFileLocationFromSubstring(userTs, "fnA()")), { + textSpan: protocolTextSpanFromSubstring(userTs.content, "fnA"), + definitions: [ + protocolFileSpanWithContextFromSubstring({ + file: aDts, text: "fnA", - contextText: "export function fnA() {}" + contextText: "export declare function fnA(): void;" }) ], }); - const renameUserTs = (userTs: File): protocol.SpanGroup => ({ - file: userTs.path, - locs: [ - protocolRenameSpanFromSubstring({ - fileText: userTs.content, - text: "fnA" + // Not inlined, jumps to bTs + assert.deepEqual(executeSessionRequest(session, protocol.CommandTypes.DefinitionAndBoundSpan, protocolFileLocationFromSubstring(userTs, "fnB()")), { + textSpan: protocolTextSpanFromSubstring(userTs.content, "fnB"), + definitions: [ + protocolFileSpanWithContextFromSubstring({ + file: bTs, + text: "fnB", + contextText: "export function fnB() {}" }) ], }); - - it("renameLocations", () => { - const session = makeSampleProjects(); - const response = executeSessionRequest(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(userTs, "fnA()")); - assert.deepEqual(response, { - info: { - canRename: true, - displayName: "fnA", - fileToRename: undefined, - fullDisplayName: '"/a/bin/a".fnA', // Ideally this would use the original source's path instead of the declaration file's path. - kind: ScriptElementKind.functionElement, - kindModifiers: [ScriptElementKindModifier.exportedModifier, ScriptElementKindModifier.ambientModifier].join(","), - triggerSpan: protocolTextSpanFromSubstring(userTs.content, "fnA"), - }, - locs: [renameUserTs(userTs), renameATs(aTs)], - }); - verifyATsConfigOriginalProject(session); - }); - - it("renameLocations -- starting at definition", () => { - const session = makeSampleProjects(); - openFilesForSession([aTs], session); // If it's not opened, the reference isn't found. - const response = executeSessionRequest(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(aTs, "fnA")); - assert.deepEqual(response, { - info: { - canRename: true, - displayName: "fnA", - fileToRename: undefined, - fullDisplayName: '"/a/a".fnA', - kind: ScriptElementKind.functionElement, - kindModifiers: ScriptElementKindModifier.exportedModifier, - triggerSpan: protocolTextSpanFromSubstring(aTs.content, "fnA"), - }, - locs: [renameATs(aTs), renameUserTs(userTs)], - }); - verifyATsConfigWhenOpened(session); - }); - - it("renameLocationsFull", () => { - const session = makeSampleProjects(); - const response = executeSessionRequest(session, protocol.CommandTypes.RenameLocationsFull, protocolFileLocationFromSubstring(userTs, "fnA()")); - assert.deepEqual(response, [ - renameLocation({ file: userTs, text: "fnA" }), - renameLocation({ file: aTs, text: "fnA", contextText: "export function fnA() {}" }), - ]); - verifyATsConfigOriginalProject(session); - }); - - it("renameLocations -- target does not exist", () => { - const session = makeSampleProjects(); - const response = executeSessionRequest(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(userTs, "fnB()")); - assert.deepEqual(response, { - info: { - canRename: true, - displayName: "fnB", - fileToRename: undefined, - fullDisplayName: '"/b/bin/b".fnB', - kind: ScriptElementKind.functionElement, - kindModifiers: [ScriptElementKindModifier.exportedModifier, ScriptElementKindModifier.ambientModifier].join(","), - triggerSpan: protocolTextSpanFromSubstring(userTs.content, "fnB"), - }, - locs: [ - { - file: bDts.path, - locs: [ - protocolRenameSpanFromSubstring({ - fileText: bDts.content, - text: "fnB", - contextText: "export declare function fnB(): void;" - }) - ], - }, - { - file: userTs.path, - locs: [ - protocolRenameSpanFromSubstring({ - fileText: userTs.content, - text: "fnB" - }) - ], - }, - ], - }); - verifySingleInferredProject(session); - }); - - it("getEditsForFileRename", () => { - const session = makeSampleProjects(); - const response = executeSessionRequest(session, protocol.CommandTypes.GetEditsForFileRename, { - oldFilePath: aTs.path, - newFilePath: "/a/aNew.ts", - }); - assert.deepEqual(response, [ - { - fileName: userTs.path, - textChanges: [ - { ...protocolTextSpanFromSubstring(userTs.content, "../a/bin/a"), newText: "../a/bin/aNew" }, - ], - }, - ]); - verifySingleInferredProject(session); - }); - - it("getEditsForFileRename when referencing project doesnt include file and its renamed", () => { - const aTs: File = { path: "/a/src/a.ts", content: "" }; - const aTsconfig: File = { - path: "/a/tsconfig.json", - content: JSON.stringify({ - compilerOptions: { - composite: true, - declaration: true, - declarationMap: true, - outDir: "./build", - } - }), - }; - const bTs: File = { path: "/b/src/b.ts", content: "" }; - const bTsconfig: File = { - path: "/b/tsconfig.json", - content: JSON.stringify({ - compilerOptions: { - composite: true, - outDir: "./build", - }, - include: ["./src"], - references: [{ path: "../a" }], - }), - }; - - const host = createServerHost([aTs, aTsconfig, bTs, bTsconfig]); - const session = createSession(host); - openFilesForSession([aTs, bTs], session); - const response = executeSessionRequest(session, CommandNames.GetEditsForFileRename, { - oldFilePath: aTs.path, - newFilePath: "/a/src/a1.ts", - }); - assert.deepEqual(response, []); // Should not change anything - }); - - it("does not jump to source if inlined sources", () => { - const aDtsInlinedSources: RawSourceMap = { - ...aDtsMapContent, - sourcesContent: [aTs.content] - }; - const aDtsMapInlinedSources: File = { - path: aDtsMap.path, - content: JSON.stringify(aDtsInlinedSources) - }; - const host = createServerHost([aTs, aDtsMapInlinedSources, aDts, bTs, bDtsMap, bDts, userTs, dummyFile]); - const session = createSession(host); - - openFilesForSession([userTs], session); - const service = session.getProjectService(); - // If config file then userConfig project and bConfig project since it is referenced - checkNumberOfProjects(service, { inferredProjects: 1 }); - - // Inlined so does not jump to aTs - assert.deepEqual( - executeSessionRequest( - session, - protocol.CommandTypes.DefinitionAndBoundSpan, - protocolFileLocationFromSubstring(userTs, "fnA()") - ), - { - textSpan: protocolTextSpanFromSubstring(userTs.content, "fnA"), - definitions: [ - protocolFileSpanWithContextFromSubstring({ - file: aDts, - text: "fnA", - contextText: "export declare function fnA(): void;" - }) - ], - } - ); - - // Not inlined, jumps to bTs - assert.deepEqual( - executeSessionRequest( - session, - protocol.CommandTypes.DefinitionAndBoundSpan, - protocolFileLocationFromSubstring(userTs, "fnB()") - ), - { - textSpan: protocolTextSpanFromSubstring(userTs.content, "fnB"), - definitions: [ - protocolFileSpanWithContextFromSubstring({ - file: bTs, - text: "fnB", - contextText: "export function fnB() {}" - }) - ], - } - ); - - verifySingleInferredProject(session); - }); + verifySingleInferredProject(session); }); -} +}); diff --git a/src/testRunner/unittests/tsserver/documentRegistry.ts b/src/testRunner/unittests/tsserver/documentRegistry.ts index 94098ab1bd9ba..1ece40143d4c9 100644 --- a/src/testRunner/unittests/tsserver/documentRegistry.ts +++ b/src/testRunner/unittests/tsserver/documentRegistry.ts @@ -1,93 +1,80 @@ -namespace ts.projectSystem { - describe("unittests:: tsserver:: document registry in project service", () => { - const importModuleContent = `import {a} from "./module1"`; - const file: File = { - path: `${tscWatch.projectRoot}/index.ts`, - content: importModuleContent - }; - const moduleFile: File = { - path: `${tscWatch.projectRoot}/module1.d.ts`, - content: "export const a: number;" - }; - const configFile: File = { - path: `${tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ files: ["index.ts"] }) - }; - - function getProject(service: TestProjectService) { - return service.configuredProjects.get(configFile.path)!; - } - - function checkProject(service: TestProjectService, moduleIsOrphan: boolean) { - // Update the project - const project = getProject(service); - project.getLanguageService(); - checkProjectActualFiles(project, [file.path, libFile.path, configFile.path, ...(moduleIsOrphan ? [] : [moduleFile.path])]); - const moduleInfo = service.getScriptInfo(moduleFile.path)!; - assert.isDefined(moduleInfo); - assert.equal(moduleInfo.isOrphan(), moduleIsOrphan); - const key = service.documentRegistry.getKeyForCompilationSettings(project.getCompilationSettings()); - assert.deepEqual(service.documentRegistry.getLanguageServiceRefCounts(moduleInfo.path), [[key, moduleIsOrphan ? undefined : 1]]); - } - - function createServiceAndHost() { - const host = createServerHost([file, moduleFile, libFile, configFile]); - const service = createProjectService(host); - service.openClientFile(file.path); - checkProject(service, /*moduleIsOrphan*/ false); - return { host, service }; - } - - function changeFileToNotImportModule(service: TestProjectService) { - const info = service.getScriptInfo(file.path)!; - service.applyChangesToFile(info, singleIterator({ span: { start: 0, length: importModuleContent.length }, newText: "" })); - checkProject(service, /*moduleIsOrphan*/ true); - } - - function changeFileToImportModule(service: TestProjectService) { - const info = service.getScriptInfo(file.path)!; - service.applyChangesToFile(info, singleIterator({ span: { start: 0, length: 0 }, newText: importModuleContent })); - checkProject(service, /*moduleIsOrphan*/ false); - } - - it("Caches the source file if script info is orphan", () => { - const { service } = createServiceAndHost(); - const project = getProject(service); - - const moduleInfo = service.getScriptInfo(moduleFile.path)!; - const sourceFile = moduleInfo.cacheSourceFile!.sourceFile; - assert.equal(project.getSourceFile(moduleInfo.path), sourceFile); - - // edit file - changeFileToNotImportModule(service); - assert.equal(moduleInfo.cacheSourceFile!.sourceFile, sourceFile); - - // write content back - changeFileToImportModule(service); - assert.equal(moduleInfo.cacheSourceFile!.sourceFile, sourceFile); - assert.equal(project.getSourceFile(moduleInfo.path), sourceFile); - }); - - it("Caches the source file if script info is orphan, and orphan script info changes", () => { - const { host, service } = createServiceAndHost(); - const project = getProject(service); - - const moduleInfo = service.getScriptInfo(moduleFile.path)!; - const sourceFile = moduleInfo.cacheSourceFile!.sourceFile; - assert.equal(project.getSourceFile(moduleInfo.path), sourceFile); - - // edit file - changeFileToNotImportModule(service); - assert.equal(moduleInfo.cacheSourceFile!.sourceFile, sourceFile); - - const updatedModuleContent = moduleFile.content + "\nexport const b: number;"; - host.writeFile(moduleFile.path, updatedModuleContent); - - // write content back - changeFileToImportModule(service); - assert.notEqual(moduleInfo.cacheSourceFile!.sourceFile, sourceFile); - assert.equal(project.getSourceFile(moduleInfo.path), moduleInfo.cacheSourceFile!.sourceFile); - assert.equal(moduleInfo.cacheSourceFile!.sourceFile.text, updatedModuleContent); - }); +import { File, TestProjectService, checkProjectActualFiles, libFile, createServerHost, createProjectService } from "../../ts.projectSystem"; +import { projectRoot } from "../../ts.tscWatch"; +import { singleIterator } from "../../ts"; +describe("unittests:: tsserver:: document registry in project service", () => { + const importModuleContent = `import {a} from "./module1"`; + const file: File = { + path: `${projectRoot}/index.ts`, + content: importModuleContent + }; + const moduleFile: File = { + path: `${projectRoot}/module1.d.ts`, + content: "export const a: number;" + }; + const configFile: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ files: ["index.ts"] }) + }; + function getProject(service: TestProjectService) { + return service.configuredProjects.get(configFile.path)!; + } + function checkProject(service: TestProjectService, moduleIsOrphan: boolean) { + // Update the project + const project = getProject(service); + project.getLanguageService(); + checkProjectActualFiles(project, [file.path, libFile.path, configFile.path, ...(moduleIsOrphan ? [] : [moduleFile.path])]); + const moduleInfo = service.getScriptInfo(moduleFile.path)!; + assert.isDefined(moduleInfo); + assert.equal(moduleInfo.isOrphan(), moduleIsOrphan); + const key = service.documentRegistry.getKeyForCompilationSettings(project.getCompilationSettings()); + assert.deepEqual(service.documentRegistry.getLanguageServiceRefCounts(moduleInfo.path), [[key, moduleIsOrphan ? undefined : 1]]); + } + function createServiceAndHost() { + const host = createServerHost([file, moduleFile, libFile, configFile]); + const service = createProjectService(host); + service.openClientFile(file.path); + checkProject(service, /*moduleIsOrphan*/ false); + return { host, service }; + } + function changeFileToNotImportModule(service: TestProjectService) { + const info = service.getScriptInfo(file.path)!; + service.applyChangesToFile(info, singleIterator({ span: { start: 0, length: importModuleContent.length }, newText: "" })); + checkProject(service, /*moduleIsOrphan*/ true); + } + function changeFileToImportModule(service: TestProjectService) { + const info = service.getScriptInfo(file.path)!; + service.applyChangesToFile(info, singleIterator({ span: { start: 0, length: 0 }, newText: importModuleContent })); + checkProject(service, /*moduleIsOrphan*/ false); + } + it("Caches the source file if script info is orphan", () => { + const { service } = createServiceAndHost(); + const project = getProject(service); + const moduleInfo = service.getScriptInfo(moduleFile.path)!; + const sourceFile = moduleInfo.cacheSourceFile!.sourceFile; + assert.equal(project.getSourceFile(moduleInfo.path), sourceFile); + // edit file + changeFileToNotImportModule(service); + assert.equal(moduleInfo.cacheSourceFile!.sourceFile, sourceFile); + // write content back + changeFileToImportModule(service); + assert.equal(moduleInfo.cacheSourceFile!.sourceFile, sourceFile); + assert.equal(project.getSourceFile(moduleInfo.path), sourceFile); }); -} + it("Caches the source file if script info is orphan, and orphan script info changes", () => { + const { host, service } = createServiceAndHost(); + const project = getProject(service); + const moduleInfo = service.getScriptInfo(moduleFile.path)!; + const sourceFile = moduleInfo.cacheSourceFile!.sourceFile; + assert.equal(project.getSourceFile(moduleInfo.path), sourceFile); + // edit file + changeFileToNotImportModule(service); + assert.equal(moduleInfo.cacheSourceFile!.sourceFile, sourceFile); + const updatedModuleContent = moduleFile.content + "\nexport const b: number;"; + host.writeFile(moduleFile.path, updatedModuleContent); + // write content back + changeFileToImportModule(service); + assert.notEqual(moduleInfo.cacheSourceFile!.sourceFile, sourceFile); + assert.equal(project.getSourceFile(moduleInfo.path), moduleInfo.cacheSourceFile!.sourceFile); + assert.equal(moduleInfo.cacheSourceFile!.sourceFile.text, updatedModuleContent); + }); +}); diff --git a/src/testRunner/unittests/tsserver/duplicatePackages.ts b/src/testRunner/unittests/tsserver/duplicatePackages.ts index c6e4868ba4ab7..49e6767d26482 100644 --- a/src/testRunner/unittests/tsserver/duplicatePackages.ts +++ b/src/testRunner/unittests/tsserver/duplicatePackages.ts @@ -1,54 +1,50 @@ -namespace ts.projectSystem { - describe("unittests:: tsserver:: duplicate packages", () => { - // Tests that 'moduleSpecifiers.ts' will import from the redirecting file, and not from the file it redirects to, if that can provide a global module specifier. - it("works with import fixes", () => { - const packageContent = "export const foo: number;"; - const packageJsonContent = JSON.stringify({ name: "foo", version: "1.2.3" }); - const aFooIndex: File = { path: "/a/node_modules/foo/index.d.ts", content: packageContent }; - const aFooPackage: File = { path: "/a/node_modules/foo/package.json", content: packageJsonContent }; - const bFooIndex: File = { path: "/b/node_modules/foo/index.d.ts", content: packageContent }; - const bFooPackage: File = { path: "/b/node_modules/foo/package.json", content: packageJsonContent }; - - const userContent = 'import("foo");\nfoo'; - const aUser: File = { path: "/a/user.ts", content: userContent }; - const bUser: File = { path: "/b/user.ts", content: userContent }; - const tsconfig: File = { - path: "/tsconfig.json", - content: "{}", - }; - - const host = createServerHost([aFooIndex, aFooPackage, bFooIndex, bFooPackage, aUser, bUser, tsconfig]); - const session = createSession(host); - - openFilesForSession([aUser, bUser], session); - - for (const user of [aUser, bUser]) { - const response = executeSessionRequest(session, protocol.CommandTypes.GetCodeFixes, { - file: user.path, - startLine: 2, - startOffset: 1, - endLine: 2, - endOffset: 4, - errorCodes: [Diagnostics.Cannot_find_name_0.code], - }); - assert.deepEqual(response, [ - { - description: `Import 'foo' from module "foo"`, - fixName: "import", - changes: [{ +import { File, createServerHost, createSession, openFilesForSession, executeSessionRequest, protocol } from "../../ts.projectSystem"; +import { Diagnostics } from "../../ts"; +describe("unittests:: tsserver:: duplicate packages", () => { + // Tests that 'moduleSpecifiers.ts' will import from the redirecting file, and not from the file it redirects to, if that can provide a global module specifier. + it("works with import fixes", () => { + const packageContent = "export const foo: number;"; + const packageJsonContent = JSON.stringify({ name: "foo", version: "1.2.3" }); + const aFooIndex: File = { path: "/a/node_modules/foo/index.d.ts", content: packageContent }; + const aFooPackage: File = { path: "/a/node_modules/foo/package.json", content: packageJsonContent }; + const bFooIndex: File = { path: "/b/node_modules/foo/index.d.ts", content: packageContent }; + const bFooPackage: File = { path: "/b/node_modules/foo/package.json", content: packageJsonContent }; + const userContent = 'import("foo");\nfoo'; + const aUser: File = { path: "/a/user.ts", content: userContent }; + const bUser: File = { path: "/b/user.ts", content: userContent }; + const tsconfig: File = { + path: "/tsconfig.json", + content: "{}", + }; + const host = createServerHost([aFooIndex, aFooPackage, bFooIndex, bFooPackage, aUser, bUser, tsconfig]); + const session = createSession(host); + openFilesForSession([aUser, bUser], session); + for (const user of [aUser, bUser]) { + const response = executeSessionRequest(session, protocol.CommandTypes.GetCodeFixes, { + file: user.path, + startLine: 2, + startOffset: 1, + endLine: 2, + endOffset: 4, + errorCodes: [Diagnostics.Cannot_find_name_0.code], + }); + assert.deepEqual(response, [ + { + description: `Import 'foo' from module "foo"`, + fixName: "import", + changes: [{ fileName: user.path, textChanges: [{ - start: { line: 1, offset: 1 }, - end: { line: 1, offset: 1 }, - newText: 'import { foo } from "foo";\n\n', - }], + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 1 }, + newText: 'import { foo } from "foo";\n\n', + }], }], - commands: undefined, - fixId: undefined, - fixAllDescription: undefined - }, - ]); - } - }); + commands: undefined, + fixId: undefined, + fixAllDescription: undefined + }, + ]); + } }); -} +}); diff --git a/src/testRunner/unittests/tsserver/dynamicFiles.ts b/src/testRunner/unittests/tsserver/dynamicFiles.ts index 214fd020d65b9..671875b561de1 100644 --- a/src/testRunner/unittests/tsserver/dynamicFiles.ts +++ b/src/testRunner/unittests/tsserver/dynamicFiles.ts @@ -1,214 +1,193 @@ -namespace ts.projectSystem { - export function verifyDynamic(service: server.ProjectService, path: string) { - const info = Debug.checkDefined(service.filenameToScriptInfo.get(path), `Expected ${path} in :: ${JSON.stringify(arrayFrom(service.filenameToScriptInfo.entries(), ([key, f]) => ({ key, fileName: f.fileName, path: f.path })))}`); - assert.isTrue(info.isDynamic); - } - - function verifyPathRecognizedAsDynamic(path: string) { - const file: File = { - path, - content: `/// +import { ProjectService, toNormalizedPath } from "../../ts.server"; +import { Debug, arrayFrom, Diagnostics, ModuleKind, ScriptElementKind } from "../../ts"; +import { File, createServerHost, libFile, createProjectService, checkProjectRootFiles, checkProjectActualFiles, createSession, openFilesForSession, executeSessionRequestNoResponse, protocol, executeSessionRequest, checkNumberOfProjects } from "../../ts.projectSystem"; +import { projectRoot } from "../../ts.tscWatch"; +export function verifyDynamic(service: ProjectService, path: string) { + const info = Debug.checkDefined(service.filenameToScriptInfo.get(path), `Expected ${path} in :: ${JSON.stringify(arrayFrom(service.filenameToScriptInfo.entries(), ([key, f]) => ({ key, fileName: f.fileName, path: f.path })))}`); + assert.isTrue(info.isDynamic); +} +function verifyPathRecognizedAsDynamic(path: string) { + const file: File = { + path, + content: `/// /// var x = 10;` + }; + const host = createServerHost([libFile]); + const projectService = createProjectService(host); + projectService.openClientFile(file.path, file.content); + verifyDynamic(projectService, projectService.toPath(file.path)); + projectService.checkNumberOfProjects({ inferredProjects: 1 }); + const project = projectService.inferredProjects[0]; + checkProjectRootFiles(project, [file.path]); + checkProjectActualFiles(project, [file.path, libFile.path]); +} +describe("unittests:: tsserver:: dynamicFiles:: Untitled files", () => { + const untitledFile = "untitled:^Untitled-1"; + it("Can convert positions to locations", () => { + const aTs: File = { path: "/proj/a.ts", content: "" }; + const tsconfig: File = { path: "/proj/tsconfig.json", content: "{}" }; + const session = createSession(createServerHost([aTs, tsconfig]), { useInferredProjectPerProjectRoot: true }); + openFilesForSession([aTs], session); + executeSessionRequestNoResponse(session, protocol.CommandTypes.Open, { + file: untitledFile, + fileContent: `/// \nlet foo = 1;\nfooo/**/`, + scriptKindName: "TS", + projectRootPath: "/proj", + }); + verifyDynamic(session.getProjectService(), `/proj/untitled:^untitled-1`); + const response = executeSessionRequest(session, protocol.CommandTypes.GetCodeFixes, { + file: untitledFile, + startLine: 3, + startOffset: 1, + endLine: 3, + endOffset: 5, + errorCodes: [Diagnostics.Cannot_find_name_0_Did_you_mean_1.code], + }); + assert.deepEqual(response, [ + { + description: "Change spelling to 'foo'", + fixName: "spelling", + changes: [{ + fileName: untitledFile, + textChanges: [{ + start: { line: 3, offset: 1 }, + end: { line: 3, offset: 5 }, + newText: "foo", + }], + }], + commands: undefined, + fixId: undefined, + fixAllDescription: undefined + }, + ]); + }); + it("opening untitled files", () => { + const config: File = { + path: `${projectRoot}/tsconfig.json`, + content: "{}" + }; + const host = createServerHost([config, libFile], { useCaseSensitiveFileNames: true, currentDirectory: projectRoot }); + const service = createProjectService(host); + service.openClientFile(untitledFile, "const x = 10;", /*scriptKind*/ undefined, projectRoot); + checkNumberOfProjects(service, { inferredProjects: 1 }); + checkProjectActualFiles(service.inferredProjects[0], [untitledFile, libFile.path]); + verifyDynamic(service, `${projectRoot}/${untitledFile}`); + const untitled: File = { + path: `${projectRoot}/Untitled-1.ts`, + content: "const x = 10;" }; - const host = createServerHost([libFile]); + host.writeFile(untitled.path, untitled.content); + host.checkTimeoutQueueLength(0); + service.openClientFile(untitled.path, untitled.content, /*scriptKind*/ undefined, projectRoot); + checkNumberOfProjects(service, { configuredProjects: 1, inferredProjects: 1 }); + checkProjectActualFiles((service.configuredProjects.get(config.path)!), [untitled.path, libFile.path, config.path]); + checkProjectActualFiles(service.inferredProjects[0], [untitledFile, libFile.path]); + service.closeClientFile(untitledFile); + checkProjectActualFiles((service.configuredProjects.get(config.path)!), [untitled.path, libFile.path, config.path]); + checkProjectActualFiles(service.inferredProjects[0], [untitledFile, libFile.path]); + service.openClientFile(untitledFile, "const x = 10;", /*scriptKind*/ undefined, projectRoot); + verifyDynamic(service, `${projectRoot}/${untitledFile}`); + checkProjectActualFiles((service.configuredProjects.get(config.path)!), [untitled.path, libFile.path, config.path]); + checkProjectActualFiles(service.inferredProjects[0], [untitledFile, libFile.path]); + }); +}); +describe("unittests:: tsserver:: dynamicFiles:: ", () => { + it("dynamic file without external project", () => { + const file: File = { + path: "^walkThroughSnippet:/Users/UserName/projects/someProject/out/someFile#1.js", + content: "var x = 10;" + }; + const host = createServerHost([libFile], { useCaseSensitiveFileNames: true }); const projectService = createProjectService(host); - projectService.openClientFile(file.path, file.content); - verifyDynamic(projectService, projectService.toPath(file.path)); - + projectService.setCompilerOptionsForInferredProjects({ + module: ModuleKind.CommonJS, + allowJs: true, + allowSyntheticDefaultImports: true, + allowNonTsExtensions: true + }); + projectService.openClientFile(file.path, "var x = 10;"); projectService.checkNumberOfProjects({ inferredProjects: 1 }); const project = projectService.inferredProjects[0]; checkProjectRootFiles(project, [file.path]); checkProjectActualFiles(project, [file.path, libFile.path]); - } - - describe("unittests:: tsserver:: dynamicFiles:: Untitled files", () => { - const untitledFile = "untitled:^Untitled-1"; - it("Can convert positions to locations", () => { - const aTs: File = { path: "/proj/a.ts", content: "" }; - const tsconfig: File = { path: "/proj/tsconfig.json", content: "{}" }; - const session = createSession(createServerHost([aTs, tsconfig]), { useInferredProjectPerProjectRoot: true }); - - openFilesForSession([aTs], session); - - executeSessionRequestNoResponse(session, protocol.CommandTypes.Open, { - file: untitledFile, - fileContent: `/// \nlet foo = 1;\nfooo/**/`, - scriptKindName: "TS", - projectRootPath: "/proj", - }); - verifyDynamic(session.getProjectService(), `/proj/untitled:^untitled-1`); - const response = executeSessionRequest(session, protocol.CommandTypes.GetCodeFixes, { - file: untitledFile, - startLine: 3, - startOffset: 1, - endLine: 3, - endOffset: 5, - errorCodes: [Diagnostics.Cannot_find_name_0_Did_you_mean_1.code], - }); - assert.deepEqual(response, [ - { - description: "Change spelling to 'foo'", - fixName: "spelling", - changes: [{ - fileName: untitledFile, - textChanges: [{ - start: { line: 3, offset: 1 }, - end: { line: 3, offset: 5 }, - newText: "foo", - }], - }], - commands: undefined, - fixId: undefined, - fixAllDescription: undefined - }, - ]); - }); - - it("opening untitled files", () => { - const config: File = { - path: `${tscWatch.projectRoot}/tsconfig.json`, - content: "{}" - }; - const host = createServerHost([config, libFile], { useCaseSensitiveFileNames: true, currentDirectory: tscWatch.projectRoot }); - const service = createProjectService(host); - service.openClientFile(untitledFile, "const x = 10;", /*scriptKind*/ undefined, tscWatch.projectRoot); - checkNumberOfProjects(service, { inferredProjects: 1 }); - checkProjectActualFiles(service.inferredProjects[0], [untitledFile, libFile.path]); - verifyDynamic(service, `${tscWatch.projectRoot}/${untitledFile}`); - - const untitled: File = { - path: `${tscWatch.projectRoot}/Untitled-1.ts`, - content: "const x = 10;" - }; - host.writeFile(untitled.path, untitled.content); - host.checkTimeoutQueueLength(0); - service.openClientFile(untitled.path, untitled.content, /*scriptKind*/ undefined, tscWatch.projectRoot); - checkNumberOfProjects(service, { configuredProjects: 1, inferredProjects: 1 }); - checkProjectActualFiles(service.configuredProjects.get(config.path)!, [untitled.path, libFile.path, config.path]); - checkProjectActualFiles(service.inferredProjects[0], [untitledFile, libFile.path]); - - service.closeClientFile(untitledFile); - checkProjectActualFiles(service.configuredProjects.get(config.path)!, [untitled.path, libFile.path, config.path]); - checkProjectActualFiles(service.inferredProjects[0], [untitledFile, libFile.path]); - - service.openClientFile(untitledFile, "const x = 10;", /*scriptKind*/ undefined, tscWatch.projectRoot); - verifyDynamic(service, `${tscWatch.projectRoot}/${untitledFile}`); - checkProjectActualFiles(service.configuredProjects.get(config.path)!, [untitled.path, libFile.path, config.path]); - checkProjectActualFiles(service.inferredProjects[0], [untitledFile, libFile.path]); + verifyDynamic(projectService, `/${file.path}`); + assert.strictEqual(projectService.ensureDefaultProjectForFile(toNormalizedPath(file.path)), project); + const indexOfX = file.content.indexOf("x"); + assert.deepEqual(project.getLanguageService(/*ensureSynchronized*/ true).getQuickInfoAtPosition(file.path, indexOfX), { + kind: ScriptElementKind.variableElement, + kindModifiers: "", + textSpan: { start: indexOfX, length: 1 }, + displayParts: [ + { text: "var", kind: "keyword" }, + { text: " ", kind: "space" }, + { text: "x", kind: "localName" }, + { text: ":", kind: "punctuation" }, + { text: " ", kind: "space" }, + { text: "number", kind: "keyword" } + ], + documentation: [], + tags: undefined, }); }); - - describe("unittests:: tsserver:: dynamicFiles:: ", () => { - it("dynamic file without external project", () => { - const file: File = { - path: "^walkThroughSnippet:/Users/UserName/projects/someProject/out/someFile#1.js", - content: "var x = 10;" - }; - const host = createServerHost([libFile], { useCaseSensitiveFileNames: true }); - const projectService = createProjectService(host); - projectService.setCompilerOptionsForInferredProjects({ - module: ModuleKind.CommonJS, - allowJs: true, - allowSyntheticDefaultImports: true, - allowNonTsExtensions: true - }); - projectService.openClientFile(file.path, "var x = 10;"); - - projectService.checkNumberOfProjects({ inferredProjects: 1 }); - const project = projectService.inferredProjects[0]; - checkProjectRootFiles(project, [file.path]); - checkProjectActualFiles(project, [file.path, libFile.path]); - verifyDynamic(projectService, `/${file.path}`); - - assert.strictEqual(projectService.ensureDefaultProjectForFile(server.toNormalizedPath(file.path)), project); - const indexOfX = file.content.indexOf("x"); - assert.deepEqual(project.getLanguageService(/*ensureSynchronized*/ true).getQuickInfoAtPosition(file.path, indexOfX), { - kind: ScriptElementKind.variableElement, - kindModifiers: "", - textSpan: { start: indexOfX, length: 1 }, - displayParts: [ - { text: "var", kind: "keyword" }, - { text: " ", kind: "space" }, - { text: "x", kind: "localName" }, - { text: ":", kind: "punctuation" }, - { text: " ", kind: "space" }, - { text: "number", kind: "keyword" } - ], - documentation: [], - tags: undefined, + it("dynamic file with reference paths without external project", () => { + verifyPathRecognizedAsDynamic("^walkThroughSnippet:/Users/UserName/projects/someProject/out/someFile#1.js"); + }); + describe("dynamic file with projectRootPath", () => { + const file: File = { + path: "^walkThroughSnippet:/Users/UserName/projects/someProject/out/someFile#1.js", + content: "var x = 10;" + }; + const configFile: File = { + path: `${projectRoot}/tsconfig.json`, + content: "{}" + }; + const configProjectFile: File = { + path: `${projectRoot}/a.ts`, + content: "let y = 10;" + }; + it("with useInferredProjectPerProjectRoot", () => { + const host = createServerHost([libFile, configFile, configProjectFile], { useCaseSensitiveFileNames: true }); + const session = createSession(host, { useInferredProjectPerProjectRoot: true }); + openFilesForSession([{ file: file.path, projectRootPath: projectRoot }], session); + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file.path, libFile.path]); + verifyDynamic(projectService, `${projectRoot}/${file.path}`); + session.executeCommandSeq({ + command: protocol.CommandTypes.GetOutliningSpans, + arguments: { + file: file.path + } }); + // Without project root + const file2Path = file.path.replace("#1", "#2"); + projectService.openClientFile(file2Path, file.content); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file.path, libFile.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [file2Path, libFile.path]); }); - - it("dynamic file with reference paths without external project", () => { - verifyPathRecognizedAsDynamic("^walkThroughSnippet:/Users/UserName/projects/someProject/out/someFile#1.js"); + it("fails when useInferredProjectPerProjectRoot is false", () => { + const host = createServerHost([libFile, configFile, configProjectFile], { useCaseSensitiveFileNames: true }); + const projectService = createProjectService(host); + try { + projectService.openClientFile(file.path, file.content, /*scriptKind*/ undefined, projectRoot); + } + catch (e) { + assert.strictEqual(e.message.replace(/\r?\n/, "\n"), `Debug Failure. False expression.\nVerbose Debug Information: {"fileName":"^walkThroughSnippet:/Users/UserName/projects/someProject/out/someFile#1.js","currentDirectory":"/user/username/projects/myproject","hostCurrentDirectory":"/","openKeys":[]}\nDynamic files must always be opened with service's current directory or service should support inferred project per projectRootPath.`); + } + const file2Path = file.path.replace("#1", "#2"); + projectService.openClientFile(file2Path, file.content); + projectService.checkNumberOfProjects({ inferredProjects: 1 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file2Path, libFile.path]); }); - - describe("dynamic file with projectRootPath", () => { - const file: File = { - path: "^walkThroughSnippet:/Users/UserName/projects/someProject/out/someFile#1.js", - content: "var x = 10;" - }; - const configFile: File = { - path: `${tscWatch.projectRoot}/tsconfig.json`, - content: "{}" - }; - const configProjectFile: File = { - path: `${tscWatch.projectRoot}/a.ts`, - content: "let y = 10;" - }; - it("with useInferredProjectPerProjectRoot", () => { - const host = createServerHost([libFile, configFile, configProjectFile], { useCaseSensitiveFileNames: true }); - const session = createSession(host, { useInferredProjectPerProjectRoot: true }); - openFilesForSession([{ file: file.path, projectRootPath: tscWatch.projectRoot }], session); - - const projectService = session.getProjectService(); - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - checkProjectActualFiles(projectService.inferredProjects[0], [file.path, libFile.path]); - verifyDynamic(projectService, `${tscWatch.projectRoot}/${file.path}`); - - session.executeCommandSeq({ - command: protocol.CommandTypes.GetOutliningSpans, - arguments: { - file: file.path - } - }); - - // Without project root - const file2Path = file.path.replace("#1", "#2"); - projectService.openClientFile(file2Path, file.content); - checkNumberOfProjects(projectService, { inferredProjects: 2 }); - checkProjectActualFiles(projectService.inferredProjects[0], [file.path, libFile.path]); - checkProjectActualFiles(projectService.inferredProjects[1], [file2Path, libFile.path]); - }); - - it("fails when useInferredProjectPerProjectRoot is false", () => { - const host = createServerHost([libFile, configFile, configProjectFile], { useCaseSensitiveFileNames: true }); - const projectService = createProjectService(host); - try { - projectService.openClientFile(file.path, file.content, /*scriptKind*/ undefined, tscWatch.projectRoot); - } - catch (e) { - assert.strictEqual( - e.message.replace(/\r?\n/, "\n"), - `Debug Failure. False expression.\nVerbose Debug Information: {"fileName":"^walkThroughSnippet:/Users/UserName/projects/someProject/out/someFile#1.js","currentDirectory":"/user/username/projects/myproject","hostCurrentDirectory":"/","openKeys":[]}\nDynamic files must always be opened with service's current directory or service should support inferred project per projectRootPath.` - ); - } - const file2Path = file.path.replace("#1", "#2"); - projectService.openClientFile(file2Path, file.content); - projectService.checkNumberOfProjects({ inferredProjects: 1 }); - checkProjectActualFiles(projectService.inferredProjects[0], [file2Path, libFile.path]); - }); + }); + describe("verify accepts known schemas as dynamic file", () => { + it("walkThroughSnippet", () => { + verifyPathRecognizedAsDynamic("walkThroughSnippet:/usr/share/code/resources/app/out/vs/workbench/contrib/welcome/walkThrough/browser/editor/^vs_code_editor_walkthrough.md#1.ts"); }); - - describe("verify accepts known schemas as dynamic file", () => { - it("walkThroughSnippet", () => { - verifyPathRecognizedAsDynamic("walkThroughSnippet:/usr/share/code/resources/app/out/vs/workbench/contrib/welcome/walkThrough/browser/editor/^vs_code_editor_walkthrough.md#1.ts"); - }); - - it("untitled", () => { - verifyPathRecognizedAsDynamic("untitled:/Users/matb/projects/san/^newFile.ts"); - }); + it("untitled", () => { + verifyPathRecognizedAsDynamic("untitled:/Users/matb/projects/san/^newFile.ts"); }); }); -} +}); diff --git a/src/testRunner/unittests/tsserver/events/largeFileReferenced.ts b/src/testRunner/unittests/tsserver/events/largeFileReferenced.ts index 1807f104ee2d6..36bb3ad8e8fb2 100644 --- a/src/testRunner/unittests/tsserver/events/largeFileReferenced.ts +++ b/src/testRunner/unittests/tsserver/events/largeFileReferenced.ts @@ -1,75 +1,67 @@ -namespace ts.projectSystem { - describe("unittests:: tsserver:: events:: LargeFileReferencedEvent with large file", () => { - - function getLargeFile(useLargeTsFile: boolean) { - return `src/large.${useLargeTsFile ? "ts" : "js"}`; - } - - function createSessionWithEventHandler(files: File[], useLargeTsFile: boolean) { - const largeFile: File = { - path: `${tscWatch.projectRoot}/${getLargeFile(useLargeTsFile)}`, - content: "export var x = 10;", - fileSize: server.maxFileSize + 1 - }; - files.push(largeFile); - const host = createServerHost(files); - const { session, events: largeFileReferencedEvents } = createSessionWithEventTracking(host, server.LargeFileReferencedEvent); - - return { session, verifyLargeFile }; - - function verifyLargeFile(project: server.Project) { - checkProjectActualFiles(project, files.map(f => f.path)); - - // large file for non ts file should be empty and for ts file should have content - const service = session.getProjectService(); - const info = service.getScriptInfo(largeFile.path)!; - assert.equal(info.cacheSourceFile!.sourceFile.text, useLargeTsFile ? largeFile.content : ""); - - assert.deepEqual(largeFileReferencedEvents, useLargeTsFile ? emptyArray : [{ - eventName: server.LargeFileReferencedEvent, - data: { file: largeFile.path, fileSize: largeFile.fileSize, maxFileSize: server.maxFileSize } +import { File, createServerHost, createSessionWithEventTracking, checkProjectActualFiles, libFile, openFilesForSession, checkNumberOfProjects } from "../../../ts.projectSystem"; +import { projectRoot } from "../../../ts.tscWatch"; +import { maxFileSize, LargeFileReferencedEvent, Project } from "../../../ts.server"; +import { emptyArray } from "../../../ts"; +describe("unittests:: tsserver:: events:: LargeFileReferencedEvent with large file", () => { + function getLargeFile(useLargeTsFile: boolean) { + return `src/large.${useLargeTsFile ? "ts" : "js"}`; + } + function createSessionWithEventHandler(files: File[], useLargeTsFile: boolean) { + const largeFile: File = { + path: `${projectRoot}/${getLargeFile(useLargeTsFile)}`, + content: "export var x = 10;", + fileSize: maxFileSize + 1 + }; + files.push(largeFile); + const host = createServerHost(files); + const { session, events: largeFileReferencedEvents } = createSessionWithEventTracking(host, LargeFileReferencedEvent); + return { session, verifyLargeFile }; + function verifyLargeFile(project: Project) { + checkProjectActualFiles(project, files.map(f => f.path)); + // large file for non ts file should be empty and for ts file should have content + const service = session.getProjectService(); + const info = service.getScriptInfo(largeFile.path)!; + assert.equal(info.cacheSourceFile!.sourceFile.text, useLargeTsFile ? largeFile.content : ""); + assert.deepEqual(largeFileReferencedEvents, useLargeTsFile ? emptyArray : [{ + eventName: LargeFileReferencedEvent, + data: { file: largeFile.path, fileSize: largeFile.fileSize, maxFileSize: maxFileSize } }]); - } } - - function verifyLargeFile(useLargeTsFile: boolean) { - it("when large file is included by tsconfig", () => { - const file: File = { - path: `${tscWatch.projectRoot}/src/file.ts`, - content: "export var y = 10;" - }; - const tsconfig: File = { - path: `${tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ files: ["src/file.ts", getLargeFile(useLargeTsFile)], compilerOptions: { target: 1, allowJs: true } }) - }; - const files = [file, libFile, tsconfig]; - const { session, verifyLargeFile } = createSessionWithEventHandler(files, useLargeTsFile); - const service = session.getProjectService(); - openFilesForSession([file], session); - checkNumberOfProjects(service, { configuredProjects: 1 }); - verifyLargeFile(service.configuredProjects.get(tsconfig.path)!); - }); - - it("when large file is included by module resolution", () => { - const file: File = { - path: `${tscWatch.projectRoot}/src/file.ts`, - content: `export var y = 10;import {x} from "./large"` - }; - const files = [file, libFile]; - const { session, verifyLargeFile } = createSessionWithEventHandler(files, useLargeTsFile); - const service = session.getProjectService(); - openFilesForSession([file], session); - checkNumberOfProjects(service, { inferredProjects: 1 }); - verifyLargeFile(service.inferredProjects[0]); - }); - } - - describe("large file is ts file", () => { - verifyLargeFile(/*useLargeTsFile*/ true); + } + function verifyLargeFile(useLargeTsFile: boolean) { + it("when large file is included by tsconfig", () => { + const file: File = { + path: `${projectRoot}/src/file.ts`, + content: "export var y = 10;" + }; + const tsconfig: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ files: ["src/file.ts", getLargeFile(useLargeTsFile)], compilerOptions: { target: 1, allowJs: true } }) + }; + const files = [file, libFile, tsconfig]; + const { session, verifyLargeFile } = createSessionWithEventHandler(files, useLargeTsFile); + const service = session.getProjectService(); + openFilesForSession([file], session); + checkNumberOfProjects(service, { configuredProjects: 1 }); + verifyLargeFile(service.configuredProjects.get(tsconfig.path)!); }); - - describe("large file is js file", () => { - verifyLargeFile(/*useLargeTsFile*/ false); + it("when large file is included by module resolution", () => { + const file: File = { + path: `${projectRoot}/src/file.ts`, + content: `export var y = 10;import {x} from "./large"` + }; + const files = [file, libFile]; + const { session, verifyLargeFile } = createSessionWithEventHandler(files, useLargeTsFile); + const service = session.getProjectService(); + openFilesForSession([file], session); + checkNumberOfProjects(service, { inferredProjects: 1 }); + verifyLargeFile(service.inferredProjects[0]); }); + } + describe("large file is ts file", () => { + verifyLargeFile(/*useLargeTsFile*/ true); + }); + describe("large file is js file", () => { + verifyLargeFile(/*useLargeTsFile*/ false); }); -} +}); diff --git a/src/testRunner/unittests/tsserver/events/projectLanguageServiceState.ts b/src/testRunner/unittests/tsserver/events/projectLanguageServiceState.ts index 231c46c350b7a..423ad4423007d 100644 --- a/src/testRunner/unittests/tsserver/events/projectLanguageServiceState.ts +++ b/src/testRunner/unittests/tsserver/events/projectLanguageServiceState.ts @@ -1,51 +1,48 @@ -namespace ts.projectSystem { - describe("unittests:: tsserver:: events:: ProjectLanguageServiceStateEvent", () => { - it("language service disabled events are triggered", () => { - const f1 = { - path: "/a/app.js", - content: "let x = 1;" - }; - const f2 = { - path: "/a/largefile.js", - content: "" - }; - const config = { - path: "/a/jsconfig.json", - content: "{}" - }; - const configWithExclude = { - path: config.path, - content: JSON.stringify({ exclude: ["largefile.js"] }) - }; - const host = createServerHost([f1, f2, config]); - const originalGetFileSize = host.getFileSize; - host.getFileSize = (filePath: string) => - filePath === f2.path ? server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); - - const { session, events } = createSessionWithEventTracking(host, server.ProjectLanguageServiceStateEvent); - session.executeCommand({ - seq: 0, - type: "request", - command: "open", - arguments: { file: f1.path } - }); - const projectService = session.getProjectService(); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const project = configuredProjectAt(projectService, 0); - assert.isFalse(project.languageServiceEnabled, "Language service enabled"); - assert.equal(events.length, 1, "should receive event"); - assert.equal(events[0].data.project, project, "project name"); - assert.equal(events[0].data.project.getProjectName(), config.path, "config path"); - assert.isFalse(events[0].data.languageServiceEnabled, "Language service state"); - - host.reloadFS([f1, f2, configWithExclude]); - host.checkTimeoutQueueLengthAndRun(2); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.isTrue(project.languageServiceEnabled, "Language service enabled"); - assert.equal(events.length, 2, "should receive event"); - assert.equal(events[1].data.project, project, "project"); - assert.equal(events[1].data.project.getProjectName(), config.path, "config path"); - assert.isTrue(events[1].data.languageServiceEnabled, "Language service state"); - }); +import { createServerHost, createSessionWithEventTracking, protocol, checkNumberOfProjects, configuredProjectAt } from "../../../ts.projectSystem"; +import { maxProgramSizeForNonTsFiles, ProjectLanguageServiceStateEvent } from "../../../ts.server"; +describe("unittests:: tsserver:: events:: ProjectLanguageServiceStateEvent", () => { + it("language service disabled events are triggered", () => { + const f1 = { + path: "/a/app.js", + content: "let x = 1;" + }; + const f2 = { + path: "/a/largefile.js", + content: "" + }; + const config = { + path: "/a/jsconfig.json", + content: "{}" + }; + const configWithExclude = { + path: config.path, + content: JSON.stringify({ exclude: ["largefile.js"] }) + }; + const host = createServerHost([f1, f2, config]); + const originalGetFileSize = host.getFileSize; + host.getFileSize = (filePath: string) => filePath === f2.path ? maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); + const { session, events } = createSessionWithEventTracking(host, ProjectLanguageServiceStateEvent); + session.executeCommand(({ + seq: 0, + type: "request", + command: "open", + arguments: { file: f1.path } + })); + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = configuredProjectAt(projectService, 0); + assert.isFalse(project.languageServiceEnabled, "Language service enabled"); + assert.equal(events.length, 1, "should receive event"); + assert.equal(events[0].data.project, project, "project name"); + assert.equal(events[0].data.project.getProjectName(), config.path, "config path"); + assert.isFalse(events[0].data.languageServiceEnabled, "Language service state"); + host.reloadFS([f1, f2, configWithExclude]); + host.checkTimeoutQueueLengthAndRun(2); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.isTrue(project.languageServiceEnabled, "Language service enabled"); + assert.equal(events.length, 2, "should receive event"); + assert.equal(events[1].data.project, project, "project"); + assert.equal(events[1].data.project.getProjectName(), config.path, "config path"); + assert.isTrue(events[1].data.languageServiceEnabled, "Language service state"); }); -} +}); diff --git a/src/testRunner/unittests/tsserver/events/projectLoading.ts b/src/testRunner/unittests/tsserver/events/projectLoading.ts index c4c7d78b7402e..f370a4e19dc3a 100644 --- a/src/testRunner/unittests/tsserver/events/projectLoading.ts +++ b/src/testRunner/unittests/tsserver/events/projectLoading.ts @@ -1,212 +1,187 @@ -namespace ts.projectSystem { - describe("unittests:: tsserver:: events:: ProjectLoadingStart and ProjectLoadingFinish events", () => { - const aTs: File = { - path: `${tscWatch.projects}/a/a.ts`, - content: "export class A { }" - }; - const configA: File = { - path: `${tscWatch.projects}/a/tsconfig.json`, - content: "{}" - }; - const bTsPath = `${tscWatch.projects}/b/b.ts`; - const configBPath = `${tscWatch.projects}/b/tsconfig.json`; - const files = [libFile, aTs, configA]; - - function verifyProjectLoadingStartAndFinish(createSession: (host: TestServerHost) => { - session: TestSession; - getNumberOfEvents: () => number; - clearEvents: () => void; - verifyProjectLoadEvents: (expected: [server.ProjectLoadingStartEvent, server.ProjectLoadingFinishEvent]) => void; - }) { - function createSessionToVerifyEvent(files: readonly File[]) { - const host = createServerHost(files); - const originalReadFile = host.readFile; - const { session, getNumberOfEvents, clearEvents, verifyProjectLoadEvents } = createSession(host); - host.readFile = file => { - if (file === configA.path || file === configBPath) { - assert.equal(getNumberOfEvents(), 1, "Event for loading is sent before reading config file"); - } - return originalReadFile.call(host, file); - }; - const service = session.getProjectService(); - return { host, session, verifyEvent, verifyEventWithOpenTs, service, getNumberOfEvents }; - - function verifyEvent(project: server.Project, reason: string) { - verifyProjectLoadEvents([ - { eventName: server.ProjectLoadingStartEvent, data: { project, reason } }, - { eventName: server.ProjectLoadingFinishEvent, data: { project } } - ]); - clearEvents(); - } - - function verifyEventWithOpenTs(file: File, configPath: string, configuredProjects: number) { - openFilesForSession([file], session); - checkNumberOfProjects(service, { configuredProjects }); - const project = service.configuredProjects.get(configPath)!; - assert.isDefined(project); - verifyEvent(project, `Creating possible configured project for ${file.path} to open`); +import { File, libFile, TestServerHost, TestSession, createServerHost, openFilesForSession, checkNumberOfProjects, protocol, protocolLocationFromSubstring, toExternalFiles, createSessionWithEventTracking, createSessionWithDefaultEventHandler } from "../../../ts.projectSystem"; +import { projects } from "../../../ts.tscWatch"; +import { ProjectLoadingStartEvent, ProjectLoadingFinishEvent, Project } from "../../../ts.server"; +describe("unittests:: tsserver:: events:: ProjectLoadingStart and ProjectLoadingFinish events", () => { + const aTs: File = { + path: `${projects}/a/a.ts`, + content: "export class A { }" + }; + const configA: File = { + path: `${projects}/a/tsconfig.json`, + content: "{}" + }; + const bTsPath = `${projects}/b/b.ts`; + const configBPath = `${projects}/b/tsconfig.json`; + const files = [libFile, aTs, configA]; + function verifyProjectLoadingStartAndFinish(createSession: (host: TestServerHost) => { + session: TestSession; + getNumberOfEvents: () => number; + clearEvents: () => void; + verifyProjectLoadEvents: (expected: [ProjectLoadingStartEvent, ProjectLoadingFinishEvent]) => void; + }) { + function createSessionToVerifyEvent(files: readonly File[]) { + const host = createServerHost(files); + const originalReadFile = host.readFile; + const { session, getNumberOfEvents, clearEvents, verifyProjectLoadEvents } = createSession(host); + host.readFile = file => { + if (file === configA.path || file === configBPath) { + assert.equal(getNumberOfEvents(), 1, "Event for loading is sent before reading config file"); } + return originalReadFile.call(host, file); + }; + const service = session.getProjectService(); + return { host, session, verifyEvent, verifyEventWithOpenTs, service, getNumberOfEvents }; + function verifyEvent(project: Project, reason: string) { + verifyProjectLoadEvents([ + { eventName: ProjectLoadingStartEvent, data: { project, reason } }, + { eventName: ProjectLoadingFinishEvent, data: { project } } + ]); + clearEvents(); } - - it("when project is created by open file", () => { + function verifyEventWithOpenTs(file: File, configPath: string, configuredProjects: number) { + openFilesForSession([file], session); + checkNumberOfProjects(service, { configuredProjects }); + const project = service.configuredProjects.get(configPath)!; + assert.isDefined(project); + verifyEvent(project, `Creating possible configured project for ${file.path} to open`); + } + } + it("when project is created by open file", () => { + const bTs: File = { + path: bTsPath, + content: "export class B {}" + }; + const configB: File = { + path: configBPath, + content: "{}" + }; + const { verifyEventWithOpenTs } = createSessionToVerifyEvent(files.concat(bTs, configB)); + verifyEventWithOpenTs(aTs, configA.path, 1); + verifyEventWithOpenTs(bTs, configB.path, 2); + }); + it("when change is detected in the config file", () => { + const { host, verifyEvent, verifyEventWithOpenTs, service } = createSessionToVerifyEvent(files); + verifyEventWithOpenTs(aTs, configA.path, 1); + host.writeFile(configA.path, configA.content); + host.checkTimeoutQueueLengthAndRun(2); + const project = service.configuredProjects.get(configA.path)!; + verifyEvent(project, `Change in config file detected`); + }); + describe("when opening original location project", () => { + it("with project references", () => { + verify(); + }); + it("when disableSourceOfProjectReferenceRedirect is true", () => { + verify(/*disableSourceOfProjectReferenceRedirect*/ true); + }); + function verify(disableSourceOfProjectReferenceRedirect?: true) { + const aDTs: File = { + path: `${projects}/a/a.d.ts`, + content: `export declare class A { +} +//# sourceMappingURL=a.d.ts.map +` + }; + const aDTsMap: File = { + path: `${projects}/a/a.d.ts.map`, + content: `{"version":3,"file":"a.d.ts","sourceRoot":"","sources":["./a.ts"],"names":[],"mappings":"AAAA,qBAAa,CAAC;CAAI"}` + }; const bTs: File = { path: bTsPath, - content: "export class B {}" + content: `import {A} from "../a/a"; new A();` }; const configB: File = { path: configBPath, - content: "{}" + content: JSON.stringify({ + ...(disableSourceOfProjectReferenceRedirect && { + compilerOptions: { + disableSourceOfProjectReferenceRedirect + } + }), + references: [{ path: "../a" }] + }) }; - const { verifyEventWithOpenTs } = createSessionToVerifyEvent(files.concat(bTs, configB)); - verifyEventWithOpenTs(aTs, configA.path, 1); - verifyEventWithOpenTs(bTs, configB.path, 2); - }); - - it("when change is detected in the config file", () => { - const { host, verifyEvent, verifyEventWithOpenTs, service } = createSessionToVerifyEvent(files); - verifyEventWithOpenTs(aTs, configA.path, 1); - - host.writeFile(configA.path, configA.content); - host.checkTimeoutQueueLengthAndRun(2); - const project = service.configuredProjects.get(configA.path)!; - verifyEvent(project, `Change in config file detected`); - }); - - describe("when opening original location project", () => { - it("with project references", () => { - verify(); - }); - - it("when disableSourceOfProjectReferenceRedirect is true", () => { - verify(/*disableSourceOfProjectReferenceRedirect*/ true); + const { service, session, verifyEventWithOpenTs, verifyEvent } = createSessionToVerifyEvent(files.concat(aDTs, aDTsMap, bTs, configB)); + verifyEventWithOpenTs(bTs, configB.path, 1); + session.executeCommandSeq({ + command: protocol.CommandTypes.References, + arguments: { + file: bTs.path, + ...protocolLocationFromSubstring(bTs.content, "A()") + } }); - - function verify(disableSourceOfProjectReferenceRedirect?: true) { - const aDTs: File = { - path: `${tscWatch.projects}/a/a.d.ts`, - content: `export declare class A { -} -//# sourceMappingURL=a.d.ts.map -` - }; - const aDTsMap: File = { - path: `${tscWatch.projects}/a/a.d.ts.map`, - content: `{"version":3,"file":"a.d.ts","sourceRoot":"","sources":["./a.ts"],"names":[],"mappings":"AAAA,qBAAa,CAAC;CAAI"}` - }; - const bTs: File = { - path: bTsPath, - content: `import {A} from "../a/a"; new A();` - }; - const configB: File = { - path: configBPath, - content: JSON.stringify({ - ...(disableSourceOfProjectReferenceRedirect && { - compilerOptions: { - disableSourceOfProjectReferenceRedirect - } - }), - references: [{ path: "../a" }] - }) - }; - - const { service, session, verifyEventWithOpenTs, verifyEvent } = createSessionToVerifyEvent(files.concat(aDTs, aDTsMap, bTs, configB)); - verifyEventWithOpenTs(bTs, configB.path, 1); - - session.executeCommandSeq({ - command: protocol.CommandTypes.References, - arguments: { - file: bTs.path, - ...protocolLocationFromSubstring(bTs.content, "A()") - } - }); - - checkNumberOfProjects(service, { configuredProjects: 2 }); - const project = service.configuredProjects.get(configA.path)!; - assert.isDefined(project); - verifyEvent( - project, - disableSourceOfProjectReferenceRedirect ? - `Creating project for original file: ${aTs.path} for location: ${aDTs.path}` : - `Creating project for original file: ${aTs.path}` - ); + checkNumberOfProjects(service, { configuredProjects: 2 }); + const project = service.configuredProjects.get(configA.path)!; + assert.isDefined(project); + verifyEvent(project, disableSourceOfProjectReferenceRedirect ? + `Creating project for original file: ${aTs.path} for location: ${aDTs.path}` : + `Creating project for original file: ${aTs.path}`); + } + }); + describe("with external projects and config files ", () => { + const projectFileName = `${projects}/a/project.csproj`; + function createSession(lazyConfiguredProjectsFromExternalProject: boolean) { + const { session, service, verifyEvent: verifyEventWorker, getNumberOfEvents } = createSessionToVerifyEvent(files); + service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); + service.openExternalProject(({ + projectFileName, + rootFiles: toExternalFiles([aTs.path, configA.path]), + options: {} + })); + checkNumberOfProjects(service, { configuredProjects: 1 }); + return { session, service, verifyEvent, getNumberOfEvents }; + function verifyEvent() { + const projectA = service.configuredProjects.get(configA.path)!; + assert.isDefined(projectA); + verifyEventWorker(projectA, `Creating configured project in external project: ${projectFileName}`); } + } + it("when lazyConfiguredProjectsFromExternalProject is false", () => { + const { verifyEvent } = createSession(/*lazyConfiguredProjectsFromExternalProject*/ false); + verifyEvent(); }); - - describe("with external projects and config files ", () => { - const projectFileName = `${tscWatch.projects}/a/project.csproj`; - - function createSession(lazyConfiguredProjectsFromExternalProject: boolean) { - const { session, service, verifyEvent: verifyEventWorker, getNumberOfEvents } = createSessionToVerifyEvent(files); - service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); - service.openExternalProject({ - projectFileName, - rootFiles: toExternalFiles([aTs.path, configA.path]), - options: {} - }); - checkNumberOfProjects(service, { configuredProjects: 1 }); - return { session, service, verifyEvent, getNumberOfEvents }; - - function verifyEvent() { - const projectA = service.configuredProjects.get(configA.path)!; - assert.isDefined(projectA); - verifyEventWorker(projectA, `Creating configured project in external project: ${projectFileName}`); - } - } - - it("when lazyConfiguredProjectsFromExternalProject is false", () => { - const { verifyEvent } = createSession(/*lazyConfiguredProjectsFromExternalProject*/ false); - verifyEvent(); - }); - - it("when lazyConfiguredProjectsFromExternalProject is true and file is opened", () => { - const { verifyEvent, getNumberOfEvents, session } = createSession(/*lazyConfiguredProjectsFromExternalProject*/ true); - assert.equal(getNumberOfEvents(), 0); - - openFilesForSession([aTs], session); - verifyEvent(); - }); - - it("when lazyConfiguredProjectsFromExternalProject is disabled", () => { - const { verifyEvent, getNumberOfEvents, service } = createSession(/*lazyConfiguredProjectsFromExternalProject*/ true); - assert.equal(getNumberOfEvents(), 0); - - service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject: false } }); - verifyEvent(); - }); + it("when lazyConfiguredProjectsFromExternalProject is true and file is opened", () => { + const { verifyEvent, getNumberOfEvents, session } = createSession(/*lazyConfiguredProjectsFromExternalProject*/ true); + assert.equal(getNumberOfEvents(), 0); + openFilesForSession([aTs], session); + verifyEvent(); }); - } - - describe("when using event handler", () => { - verifyProjectLoadingStartAndFinish(host => { - const { session, events } = createSessionWithEventTracking(host, server.ProjectLoadingStartEvent, server.ProjectLoadingFinishEvent); - return { - session, - getNumberOfEvents: () => events.length, - clearEvents: () => events.length = 0, - verifyProjectLoadEvents: expected => assert.deepEqual(events, expected) - }; + it("when lazyConfiguredProjectsFromExternalProject is disabled", () => { + const { verifyEvent, getNumberOfEvents, service } = createSession(/*lazyConfiguredProjectsFromExternalProject*/ true); + assert.equal(getNumberOfEvents(), 0); + service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject: false } }); + verifyEvent(); }); }); - - describe("when using default event handler", () => { - verifyProjectLoadingStartAndFinish(host => { - const { session, getEvents, clearEvents } = createSessionWithDefaultEventHandler(host, [server.ProjectLoadingStartEvent, server.ProjectLoadingFinishEvent]); - return { - session, - getNumberOfEvents: () => getEvents().length, - clearEvents, - verifyProjectLoadEvents - }; - - function verifyProjectLoadEvents(expected: [server.ProjectLoadingStartEvent, server.ProjectLoadingFinishEvent]) { - const actual = getEvents().map(e => ({ eventName: e.event, data: e.body })); - const mappedExpected = expected.map(e => { - const { project, ...rest } = e.data; - return { eventName: e.eventName, data: { projectName: project.getProjectName(), ...rest } }; - }); - assert.deepEqual(actual, mappedExpected); - } - }); + } + describe("when using event handler", () => { + verifyProjectLoadingStartAndFinish(host => { + const { session, events } = createSessionWithEventTracking(host, ProjectLoadingStartEvent, ProjectLoadingFinishEvent); + return { + session, + getNumberOfEvents: () => events.length, + clearEvents: () => events.length = 0, + verifyProjectLoadEvents: expected => assert.deepEqual(events, expected) + }; }); }); -} + describe("when using default event handler", () => { + verifyProjectLoadingStartAndFinish(host => { + const { session, getEvents, clearEvents } = createSessionWithDefaultEventHandler(host, [ProjectLoadingStartEvent, ProjectLoadingFinishEvent]); + return { + session, + getNumberOfEvents: () => getEvents().length, + clearEvents, + verifyProjectLoadEvents + }; + function verifyProjectLoadEvents(expected: [ProjectLoadingStartEvent, ProjectLoadingFinishEvent]) { + const actual = getEvents().map(e => ({ eventName: e.event, data: e.body })); + const mappedExpected = expected.map(e => { + const { project, ...rest } = e.data; + return { eventName: e.eventName, data: { projectName: project.getProjectName(), ...rest } }; + }); + assert.deepEqual(actual, mappedExpected); + } + }); + }); +}); diff --git a/src/testRunner/unittests/tsserver/events/projectUpdatedInBackground.ts b/src/testRunner/unittests/tsserver/events/projectUpdatedInBackground.ts index 958202cdd19a8..02a08133c708d 100644 --- a/src/testRunner/unittests/tsserver/events/projectUpdatedInBackground.ts +++ b/src/testRunner/unittests/tsserver/events/projectUpdatedInBackground.ts @@ -1,591 +1,502 @@ -namespace ts.projectSystem { - describe("unittests:: tsserver:: events:: ProjectsUpdatedInBackground", () => { - function verifyFiles(caption: string, actual: readonly string[], expected: readonly string[]) { - assert.equal(actual.length, expected.length, `Incorrect number of ${caption}. Actual: ${actual} Expected: ${expected}`); - const seen = createMap(); - forEach(actual, f => { - assert.isFalse(seen.has(f), `${caption}: Found duplicate ${f}. Actual: ${actual} Expected: ${expected}`); - seen.set(f, true); - assert.isTrue(contains(expected, f), `${caption}: Expected not to contain ${f}. Actual: ${actual} Expected: ${expected}`); - }); - } - - function createVerifyInitialOpen(session: TestSession, verifyProjectsUpdatedInBackgroundEventHandler: (events: server.ProjectsUpdatedInBackgroundEvent[]) => void) { - return (file: File) => { - session.executeCommandSeq({ - command: server.CommandNames.Open, - arguments: { - file: file.path - } - }); - verifyProjectsUpdatedInBackgroundEventHandler([]); +import { createMap, forEach, contains, CompilerOptions, map, find, filter, ResolutionCacheHost } from "../../../ts"; +import { TestSession, File, protocol, TestServerHost, createServerHost, libFile, checkNumberOfProjects, checkProjectActualFiles, checkWatchedDirectories, createSessionWithEventTracking, createSessionWithDefaultEventHandler } from "../../../ts.projectSystem"; +import { ProjectsUpdatedInBackgroundEvent, CommandNames } from "../../../ts.server"; +describe("unittests:: tsserver:: events:: ProjectsUpdatedInBackground", () => { + function verifyFiles(caption: string, actual: readonly string[], expected: readonly string[]) { + assert.equal(actual.length, expected.length, `Incorrect number of ${caption}. Actual: ${actual} Expected: ${expected}`); + const seen = createMap(); + forEach(actual, f => { + assert.isFalse(seen.has(f), `${caption}: Found duplicate ${f}. Actual: ${actual} Expected: ${expected}`); + seen.set(f, true); + assert.isTrue(contains(expected, f), `${caption}: Expected not to contain ${f}. Actual: ${actual} Expected: ${expected}`); + }); + } + function createVerifyInitialOpen(session: TestSession, verifyProjectsUpdatedInBackgroundEventHandler: (events: ProjectsUpdatedInBackgroundEvent[]) => void) { + return (file: File) => { + session.executeCommandSeq(({ + command: CommandNames.Open, + arguments: { + file: file.path + } + })); + verifyProjectsUpdatedInBackgroundEventHandler([]); + }; + } + interface ProjectsUpdatedInBackgroundEventVerifier { + session: TestSession; + verifyProjectsUpdatedInBackgroundEventHandler(events: ProjectsUpdatedInBackgroundEvent[]): void; + verifyInitialOpen(file: File): void; + } + function verifyProjectsUpdatedInBackgroundEvent(createSession: (host: TestServerHost) => ProjectsUpdatedInBackgroundEventVerifier) { + it("when adding new file", () => { + const commonFile1: File = { + path: "/a/b/file1.ts", + content: "export var x = 10;" }; - } - - interface ProjectsUpdatedInBackgroundEventVerifier { - session: TestSession; - verifyProjectsUpdatedInBackgroundEventHandler(events: server.ProjectsUpdatedInBackgroundEvent[]): void; - verifyInitialOpen(file: File): void; - } - - function verifyProjectsUpdatedInBackgroundEvent(createSession: (host: TestServerHost) => ProjectsUpdatedInBackgroundEventVerifier) { - it("when adding new file", () => { - const commonFile1: File = { - path: "/a/b/file1.ts", - content: "export var x = 10;" - }; - const commonFile2: File = { - path: "/a/b/file2.ts", - content: "export var y = 10;" - }; - const commonFile3: File = { - path: "/a/b/file3.ts", - content: "export var z = 10;" - }; - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{}` - }; - const openFiles = [commonFile1.path]; - const host = createServerHost([commonFile1, libFile, configFile]); - const { verifyProjectsUpdatedInBackgroundEventHandler, verifyInitialOpen } = createSession(host); - verifyInitialOpen(commonFile1); - - host.reloadFS([commonFile1, libFile, configFile, commonFile2]); - host.runQueuedTimeoutCallbacks(); - verifyProjectsUpdatedInBackgroundEventHandler([{ - eventName: server.ProjectsUpdatedInBackgroundEvent, + const commonFile2: File = { + path: "/a/b/file2.ts", + content: "export var y = 10;" + }; + const commonFile3: File = { + path: "/a/b/file3.ts", + content: "export var z = 10;" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const openFiles = [commonFile1.path]; + const host = createServerHost([commonFile1, libFile, configFile]); + const { verifyProjectsUpdatedInBackgroundEventHandler, verifyInitialOpen } = createSession(host); + verifyInitialOpen(commonFile1); + host.reloadFS([commonFile1, libFile, configFile, commonFile2]); + host.runQueuedTimeoutCallbacks(); + verifyProjectsUpdatedInBackgroundEventHandler([{ + eventName: ProjectsUpdatedInBackgroundEvent, data: { openFiles } }]); - - host.reloadFS([commonFile1, commonFile2, libFile, configFile, commonFile3]); - host.runQueuedTimeoutCallbacks(); - verifyProjectsUpdatedInBackgroundEventHandler([{ - eventName: server.ProjectsUpdatedInBackgroundEvent, + host.reloadFS([commonFile1, commonFile2, libFile, configFile, commonFile3]); + host.runQueuedTimeoutCallbacks(); + verifyProjectsUpdatedInBackgroundEventHandler([{ + eventName: ProjectsUpdatedInBackgroundEvent, data: { openFiles } }]); - }); - - describe("with --out or --outFile setting", () => { - function verifyEventWithOutSettings(compilerOptions: CompilerOptions = {}) { - const config: File = { - path: "/a/tsconfig.json", - content: JSON.stringify({ - compilerOptions - }) - }; - - const f1: File = { - path: "/a/a.ts", - content: "export let x = 1" - }; - const f2: File = { - path: "/a/b.ts", - content: "export let y = 1" - }; - - const openFiles = [f1.path]; - const files = [f1, config, libFile]; - const host = createServerHost(files); - const { verifyInitialOpen, verifyProjectsUpdatedInBackgroundEventHandler } = createSession(host); - verifyInitialOpen(f1); - - files.push(f2); - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - - verifyProjectsUpdatedInBackgroundEventHandler([{ - eventName: server.ProjectsUpdatedInBackgroundEvent, + }); + describe("with --out or --outFile setting", () => { + function verifyEventWithOutSettings(compilerOptions: CompilerOptions = {}) { + const config: File = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions + }) + }; + const f1: File = { + path: "/a/a.ts", + content: "export let x = 1" + }; + const f2: File = { + path: "/a/b.ts", + content: "export let y = 1" + }; + const openFiles = [f1.path]; + const files = [f1, config, libFile]; + const host = createServerHost(files); + const { verifyInitialOpen, verifyProjectsUpdatedInBackgroundEventHandler } = createSession(host); + verifyInitialOpen(f1); + files.push(f2); + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + verifyProjectsUpdatedInBackgroundEventHandler([{ + eventName: ProjectsUpdatedInBackgroundEvent, data: { openFiles } }]); - - f2.content = "export let x = 11"; - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - verifyProjectsUpdatedInBackgroundEventHandler([{ - eventName: server.ProjectsUpdatedInBackgroundEvent, + f2.content = "export let x = 11"; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + verifyProjectsUpdatedInBackgroundEventHandler([{ + eventName: ProjectsUpdatedInBackgroundEvent, data: { openFiles } }]); - } - - it("when both options are not set", () => { - verifyEventWithOutSettings(); - }); - - it("when --out is set", () => { - const outJs = "/a/out.js"; - verifyEventWithOutSettings({ out: outJs }); - }); - - it("when --outFile is set", () => { - const outJs = "/a/out.js"; - verifyEventWithOutSettings({ outFile: outJs }); - }); + } + it("when both options are not set", () => { + verifyEventWithOutSettings(); }); - - describe("with modules and configured project", () => { - const file1Consumer1Path = "/a/b/file1Consumer1.ts"; - const moduleFile1Path = "/a/b/moduleFile1.ts"; - const configFilePath = "/a/b/tsconfig.json"; - interface InitialStateParams { - /** custom config file options */ - configObj?: any; - /** Additional files and folders to add */ - getAdditionalFileOrFolder?(): File[]; - /** initial list of files to reload in fs and first file in this list being the file to open */ - firstReloadFileList?: string[]; + it("when --out is set", () => { + const outJs = "/a/out.js"; + verifyEventWithOutSettings({ out: outJs }); + }); + it("when --outFile is set", () => { + const outJs = "/a/out.js"; + verifyEventWithOutSettings({ outFile: outJs }); + }); + }); + describe("with modules and configured project", () => { + const file1Consumer1Path = "/a/b/file1Consumer1.ts"; + const moduleFile1Path = "/a/b/moduleFile1.ts"; + const configFilePath = "/a/b/tsconfig.json"; + interface InitialStateParams { + /** custom config file options */ + configObj?: any; + /** Additional files and folders to add */ + getAdditionalFileOrFolder?(): File[]; + /** initial list of files to reload in fs and first file in this list being the file to open */ + firstReloadFileList?: string[]; + } + function getInitialState({ configObj = {}, getAdditionalFileOrFolder, firstReloadFileList }: InitialStateParams = {}) { + const moduleFile1: File = { + path: moduleFile1Path, + content: "export function Foo() { };", + }; + const file1Consumer1: File = { + path: file1Consumer1Path, + content: `import {Foo} from "./moduleFile1"; export var y = 10;`, + }; + const file1Consumer2: File = { + path: "/a/b/file1Consumer2.ts", + content: `import {Foo} from "./moduleFile1"; let z = 10;`, + }; + const moduleFile2: File = { + path: "/a/b/moduleFile2.ts", + content: `export var Foo4 = 10;`, + }; + const globalFile3: File = { + path: "/a/b/globalFile3.ts", + content: `interface GlobalFoo { age: number }` + }; + const additionalFiles = getAdditionalFileOrFolder ? getAdditionalFileOrFolder() : []; + const configFile = { + path: configFilePath, + content: JSON.stringify(configObj || { compilerOptions: {} }) + }; + const files: File[] = [file1Consumer1, moduleFile1, file1Consumer2, moduleFile2, ...additionalFiles, globalFile3, libFile, configFile]; + const filesToReload = firstReloadFileList && getFiles(firstReloadFileList) || files; + const host = createServerHost([filesToReload[0], configFile]); + // Initial project creation + const { session, verifyProjectsUpdatedInBackgroundEventHandler, verifyInitialOpen } = createSession(host); + const openFiles = [filesToReload[0].path]; + verifyInitialOpen(filesToReload[0]); + // Since this is first event, it will have all the files + verifyProjectsUpdatedInBackgroundEvent(filesToReload); + return { + moduleFile1, file1Consumer1, file1Consumer2, moduleFile2, globalFile3, configFile, + files, + updateContentOfOpenFile, + verifyNoProjectsUpdatedInBackgroundEvent, + verifyProjectsUpdatedInBackgroundEvent + }; + function getFiles(filelist: string[]) { + return map(filelist, getFile); } - function getInitialState({ configObj = {}, getAdditionalFileOrFolder, firstReloadFileList }: InitialStateParams = {}) { - const moduleFile1: File = { - path: moduleFile1Path, - content: "export function Foo() { };", - }; - - const file1Consumer1: File = { - path: file1Consumer1Path, - content: `import {Foo} from "./moduleFile1"; export var y = 10;`, - }; - - const file1Consumer2: File = { - path: "/a/b/file1Consumer2.ts", - content: `import {Foo} from "./moduleFile1"; let z = 10;`, - }; - - const moduleFile2: File = { - path: "/a/b/moduleFile2.ts", - content: `export var Foo4 = 10;`, - }; - - const globalFile3: File = { - path: "/a/b/globalFile3.ts", - content: `interface GlobalFoo { age: number }` - }; - - const additionalFiles = getAdditionalFileOrFolder ? getAdditionalFileOrFolder() : []; - const configFile = { - path: configFilePath, - content: JSON.stringify(configObj || { compilerOptions: {} }) - }; - - const files: File[] = [file1Consumer1, moduleFile1, file1Consumer2, moduleFile2, ...additionalFiles, globalFile3, libFile, configFile]; - - const filesToReload = firstReloadFileList && getFiles(firstReloadFileList) || files; - const host = createServerHost([filesToReload[0], configFile]); - - // Initial project creation - const { session, verifyProjectsUpdatedInBackgroundEventHandler, verifyInitialOpen } = createSession(host); - const openFiles = [filesToReload[0].path]; - verifyInitialOpen(filesToReload[0]); - - // Since this is first event, it will have all the files - verifyProjectsUpdatedInBackgroundEvent(filesToReload); - - return { - moduleFile1, file1Consumer1, file1Consumer2, moduleFile2, globalFile3, configFile, - files, - updateContentOfOpenFile, - verifyNoProjectsUpdatedInBackgroundEvent, - verifyProjectsUpdatedInBackgroundEvent - }; - - function getFiles(filelist: string[]) { - return map(filelist, getFile); - } - - function getFile(fileName: string) { - return find(files, file => file.path === fileName)!; - } - - function verifyNoProjectsUpdatedInBackgroundEvent(filesToReload?: File[]) { - host.reloadFS(filesToReload || files); - host.runQueuedTimeoutCallbacks(); - verifyProjectsUpdatedInBackgroundEventHandler([]); - } - - function verifyProjectsUpdatedInBackgroundEvent(filesToReload?: File[]) { - host.reloadFS(filesToReload || files); - host.runQueuedTimeoutCallbacks(); - verifyProjectsUpdatedInBackgroundEventHandler([{ - eventName: server.ProjectsUpdatedInBackgroundEvent, + function getFile(fileName: string) { + return find(files, file => file.path === fileName)!; + } + function verifyNoProjectsUpdatedInBackgroundEvent(filesToReload?: File[]) { + host.reloadFS(filesToReload || files); + host.runQueuedTimeoutCallbacks(); + verifyProjectsUpdatedInBackgroundEventHandler([]); + } + function verifyProjectsUpdatedInBackgroundEvent(filesToReload?: File[]) { + host.reloadFS(filesToReload || files); + host.runQueuedTimeoutCallbacks(); + verifyProjectsUpdatedInBackgroundEventHandler([{ + eventName: ProjectsUpdatedInBackgroundEvent, data: { openFiles } }]); - } - - function updateContentOfOpenFile(file: File, newContent: string) { - session.executeCommandSeq({ - command: server.CommandNames.Change, - arguments: { - file: file.path, - insertString: newContent, - endLine: 1, - endOffset: file.content.length, - line: 1, - offset: 1 - } - }); - file.content = newContent; - } } - - it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => { - const { moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState(); - - // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyProjectsUpdatedInBackgroundEvent(); - - // Change the content of moduleFile1 to `export var T: number;export function Foo() { console.log('hi'); };` - moduleFile1.content = `export var T: number;export function Foo() { console.log('hi'); };`; - verifyProjectsUpdatedInBackgroundEvent(); - }); - - it("should be up-to-date with the reference map changes", () => { - const { moduleFile1, file1Consumer1, updateContentOfOpenFile, verifyProjectsUpdatedInBackgroundEvent, verifyNoProjectsUpdatedInBackgroundEvent } = getInitialState(); - - // Change file1Consumer1 content to `export let y = Foo();` - updateContentOfOpenFile(file1Consumer1, "export let y = Foo();"); - verifyNoProjectsUpdatedInBackgroundEvent(); - - // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyProjectsUpdatedInBackgroundEvent(); - - // Add the import statements back to file1Consumer1 - updateContentOfOpenFile(file1Consumer1, `import {Foo} from "./moduleFile1";let y = Foo();`); - verifyNoProjectsUpdatedInBackgroundEvent(); - - // Change the content of moduleFile1 to `export var T: number;export var T2: string;export function Foo() { };` - moduleFile1.content = `export var T: number;export var T2: string;export function Foo() { };`; - verifyProjectsUpdatedInBackgroundEvent(); - - // Multiple file edits in one go: - - // Change file1Consumer1 content to `export let y = Foo();` - // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` - updateContentOfOpenFile(file1Consumer1, `export let y = Foo();`); - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyProjectsUpdatedInBackgroundEvent(); - }); - - it("should be up-to-date with deleted files", () => { - const { moduleFile1, file1Consumer2, files, verifyProjectsUpdatedInBackgroundEvent } = getInitialState(); - - // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` - moduleFile1.content = `export var T: number;export function Foo() { };`; - - // Delete file1Consumer2 - const filesToLoad = filter(files, file => file !== file1Consumer2); - verifyProjectsUpdatedInBackgroundEvent(filesToLoad); - }); - - it("should be up-to-date with newly created files", () => { - const { moduleFile1, files, verifyProjectsUpdatedInBackgroundEvent, } = getInitialState(); - - const file1Consumer3: File = { - path: "/a/b/file1Consumer3.ts", - content: `import {Foo} from "./moduleFile1"; let y = Foo();` - }; - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyProjectsUpdatedInBackgroundEvent(files.concat(file1Consumer3)); - }); - - it("should detect changes in non-root files", () => { - const { moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ - configObj: { files: [file1Consumer1Path] }, + function updateContentOfOpenFile(file: File, newContent: string) { + session.executeCommandSeq({ + command: CommandNames.Change, + arguments: { + file: file.path, + insertString: newContent, + endLine: 1, + endOffset: file.content.length, + line: 1, + offset: 1 + } }); - - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyProjectsUpdatedInBackgroundEvent(); - - // change file1 internal, and verify only file1 is affected - moduleFile1.content += "var T1: number;"; - verifyProjectsUpdatedInBackgroundEvent(); - }); - - it("should return all files if a global file changed shape", () => { - const { globalFile3, verifyProjectsUpdatedInBackgroundEvent } = getInitialState(); - - globalFile3.content += "var T2: string;"; - verifyProjectsUpdatedInBackgroundEvent(); + file.content = newContent; + } + } + it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => { + const { moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState(); + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(); + // Change the content of moduleFile1 to `export var T: number;export function Foo() { console.log('hi'); };` + moduleFile1.content = `export var T: number;export function Foo() { console.log('hi'); };`; + verifyProjectsUpdatedInBackgroundEvent(); + }); + it("should be up-to-date with the reference map changes", () => { + const { moduleFile1, file1Consumer1, updateContentOfOpenFile, verifyProjectsUpdatedInBackgroundEvent, verifyNoProjectsUpdatedInBackgroundEvent } = getInitialState(); + // Change file1Consumer1 content to `export let y = Foo();` + updateContentOfOpenFile(file1Consumer1, "export let y = Foo();"); + verifyNoProjectsUpdatedInBackgroundEvent(); + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(); + // Add the import statements back to file1Consumer1 + updateContentOfOpenFile(file1Consumer1, `import {Foo} from "./moduleFile1";let y = Foo();`); + verifyNoProjectsUpdatedInBackgroundEvent(); + // Change the content of moduleFile1 to `export var T: number;export var T2: string;export function Foo() { };` + moduleFile1.content = `export var T: number;export var T2: string;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(); + // Multiple file edits in one go: + // Change file1Consumer1 content to `export let y = Foo();` + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + updateContentOfOpenFile(file1Consumer1, `export let y = Foo();`); + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(); + }); + it("should be up-to-date with deleted files", () => { + const { moduleFile1, file1Consumer2, files, verifyProjectsUpdatedInBackgroundEvent } = getInitialState(); + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + moduleFile1.content = `export var T: number;export function Foo() { };`; + // Delete file1Consumer2 + const filesToLoad = filter(files, file => file !== file1Consumer2); + verifyProjectsUpdatedInBackgroundEvent(filesToLoad); + }); + it("should be up-to-date with newly created files", () => { + const { moduleFile1, files, verifyProjectsUpdatedInBackgroundEvent, } = getInitialState(); + const file1Consumer3: File = { + path: "/a/b/file1Consumer3.ts", + content: `import {Foo} from "./moduleFile1"; let y = Foo();` + }; + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(files.concat(file1Consumer3)); + }); + it("should detect changes in non-root files", () => { + const { moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ + configObj: { files: [file1Consumer1Path] }, }); - - it("should always return the file itself if '--isolatedModules' is specified", () => { - const { moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ - configObj: { compilerOptions: { isolatedModules: true } } - }); - - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyProjectsUpdatedInBackgroundEvent(); + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(); + // change file1 internal, and verify only file1 is affected + moduleFile1.content += "var T1: number;"; + verifyProjectsUpdatedInBackgroundEvent(); + }); + it("should return all files if a global file changed shape", () => { + const { globalFile3, verifyProjectsUpdatedInBackgroundEvent } = getInitialState(); + globalFile3.content += "var T2: string;"; + verifyProjectsUpdatedInBackgroundEvent(); + }); + it("should always return the file itself if '--isolatedModules' is specified", () => { + const { moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ + configObj: { compilerOptions: { isolatedModules: true } } }); - - it("should always return the file itself if '--out' or '--outFile' is specified", () => { - const outFilePath = "/a/b/out.js"; - const { moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ - configObj: { compilerOptions: { module: "system", outFile: outFilePath } } - }); - - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyProjectsUpdatedInBackgroundEvent(); + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(); + }); + it("should always return the file itself if '--out' or '--outFile' is specified", () => { + const outFilePath = "/a/b/out.js"; + const { moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ + configObj: { compilerOptions: { module: "system", outFile: outFilePath } } }); - - it("should return cascaded affected file list", () => { - const file1Consumer1Consumer1: File = { - path: "/a/b/file1Consumer1Consumer1.ts", - content: `import {y} from "./file1Consumer1";` - }; - const { moduleFile1, file1Consumer1, updateContentOfOpenFile, verifyNoProjectsUpdatedInBackgroundEvent, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ - getAdditionalFileOrFolder: () => [file1Consumer1Consumer1] - }); - - updateContentOfOpenFile(file1Consumer1, file1Consumer1.content + "export var T: number;"); - verifyNoProjectsUpdatedInBackgroundEvent(); - - // Doesnt change the shape of file1Consumer1 - moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyProjectsUpdatedInBackgroundEvent(); - - // Change both files before the timeout - updateContentOfOpenFile(file1Consumer1, file1Consumer1.content + "export var T2: number;"); - moduleFile1.content = `export var T2: number;export function Foo() { };`; - verifyProjectsUpdatedInBackgroundEvent(); + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(); + }); + it("should return cascaded affected file list", () => { + const file1Consumer1Consumer1: File = { + path: "/a/b/file1Consumer1Consumer1.ts", + content: `import {y} from "./file1Consumer1";` + }; + const { moduleFile1, file1Consumer1, updateContentOfOpenFile, verifyNoProjectsUpdatedInBackgroundEvent, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ + getAdditionalFileOrFolder: () => [file1Consumer1Consumer1] }); - - it("should work fine for files with circular references", () => { - const file1: File = { - path: "/a/b/file1.ts", - content: ` + updateContentOfOpenFile(file1Consumer1, file1Consumer1.content + "export var T: number;"); + verifyNoProjectsUpdatedInBackgroundEvent(); + // Doesnt change the shape of file1Consumer1 + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(); + // Change both files before the timeout + updateContentOfOpenFile(file1Consumer1, file1Consumer1.content + "export var T2: number;"); + moduleFile1.content = `export var T2: number;export function Foo() { };`; + verifyProjectsUpdatedInBackgroundEvent(); + }); + it("should work fine for files with circular references", () => { + const file1: File = { + path: "/a/b/file1.ts", + content: ` /// export var t1 = 10;` - }; - const file2: File = { - path: "/a/b/file2.ts", - content: ` + }; + const file2: File = { + path: "/a/b/file2.ts", + content: ` /// export var t2 = 10;` - }; - const { configFile, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ - getAdditionalFileOrFolder: () => [file1, file2], - firstReloadFileList: [file1.path, libFile.path, file2.path, configFilePath] - }); - - file2.content += "export var t3 = 10;"; - verifyProjectsUpdatedInBackgroundEvent([file1, file2, libFile, configFile]); + }; + const { configFile, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ + getAdditionalFileOrFolder: () => [file1, file2], + firstReloadFileList: [file1.path, libFile.path, file2.path, configFilePath] }); - - it("should detect removed code file", () => { - const referenceFile1: File = { - path: "/a/b/referenceFile1.ts", - content: ` + file2.content += "export var t3 = 10;"; + verifyProjectsUpdatedInBackgroundEvent([file1, file2, libFile, configFile]); + }); + it("should detect removed code file", () => { + const referenceFile1: File = { + path: "/a/b/referenceFile1.ts", + content: ` /// export var x = Foo();` - }; - const { configFile, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ - getAdditionalFileOrFolder: () => [referenceFile1], - firstReloadFileList: [referenceFile1.path, libFile.path, moduleFile1Path, configFilePath] - }); - - verifyProjectsUpdatedInBackgroundEvent([libFile, referenceFile1, configFile]); + }; + const { configFile, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ + getAdditionalFileOrFolder: () => [referenceFile1], + firstReloadFileList: [referenceFile1.path, libFile.path, moduleFile1Path, configFilePath] }); - - it("should detect non-existing code file", () => { - const referenceFile1: File = { - path: "/a/b/referenceFile1.ts", - content: ` + verifyProjectsUpdatedInBackgroundEvent([libFile, referenceFile1, configFile]); + }); + it("should detect non-existing code file", () => { + const referenceFile1: File = { + path: "/a/b/referenceFile1.ts", + content: ` /// export var x = Foo();` - }; - const { configFile, moduleFile2, updateContentOfOpenFile, verifyNoProjectsUpdatedInBackgroundEvent, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ - getAdditionalFileOrFolder: () => [referenceFile1], - firstReloadFileList: [referenceFile1.path, libFile.path, configFilePath] - }); - - updateContentOfOpenFile(referenceFile1, referenceFile1.content + "export var yy = Foo();"); - verifyNoProjectsUpdatedInBackgroundEvent([libFile, referenceFile1, configFile]); - - // Create module File2 and see both files are saved - verifyProjectsUpdatedInBackgroundEvent([libFile, moduleFile2, referenceFile1, configFile]); + }; + const { configFile, moduleFile2, updateContentOfOpenFile, verifyNoProjectsUpdatedInBackgroundEvent, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ + getAdditionalFileOrFolder: () => [referenceFile1], + firstReloadFileList: [referenceFile1.path, libFile.path, configFilePath] }); + updateContentOfOpenFile(referenceFile1, referenceFile1.content + "export var yy = Foo();"); + verifyNoProjectsUpdatedInBackgroundEvent([libFile, referenceFile1, configFile]); + // Create module File2 and see both files are saved + verifyProjectsUpdatedInBackgroundEvent([libFile, moduleFile2, referenceFile1, configFile]); }); - - describe("resolution when resolution cache size", () => { - function verifyWithMaxCacheLimit(limitHit: boolean, useSlashRootAsSomeNotRootFolderInUserDirectory: boolean) { - const rootFolder = useSlashRootAsSomeNotRootFolderInUserDirectory ? "/user/username/rootfolder/otherfolder/" : "/"; - const file1: File = { - path: rootFolder + "a/b/project/file1.ts", - content: 'import a from "file2"' - }; - const file2: File = { - path: rootFolder + "a/b/node_modules/file2.d.ts", - content: "export class a { }" - }; - const file3: File = { - path: rootFolder + "a/b/project/file3.ts", - content: "export class c { }" - }; - const configFile: File = { - path: rootFolder + "a/b/project/tsconfig.json", - content: JSON.stringify({ compilerOptions: { typeRoots: [] } }) - }; - - const projectFiles = [file1, file3, libFile, configFile]; - const openFiles = [file1.path]; - const watchedRecursiveDirectories = useSlashRootAsSomeNotRootFolderInUserDirectory ? - // Folders of node_modules lookup not in changedRoot - ["a/b/project", "a/b/project/node_modules", "a/b/node_modules", "a/node_modules", "node_modules"].map(v => rootFolder + v) : - // Folder of tsconfig - ["/a/b/project", "/a/b/project/node_modules"]; - const host = createServerHost(projectFiles); - const { session, verifyInitialOpen, verifyProjectsUpdatedInBackgroundEventHandler } = createSession(host); - const projectService = session.getProjectService(); - verifyInitialOpen(file1); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const project = projectService.configuredProjects.get(configFile.path)!; - verifyProject(); - if (limitHit) { - (project as ResolutionCacheHost).maxNumberOfFilesToIterateForInvalidation = 1; - } - - file3.content += "export class d {}"; - host.reloadFS(projectFiles); - host.checkTimeoutQueueLengthAndRun(2); - - // Since this is first event - verifyProject(); - verifyProjectsUpdatedInBackgroundEventHandler([{ - eventName: server.ProjectsUpdatedInBackgroundEvent, + }); + describe("resolution when resolution cache size", () => { + function verifyWithMaxCacheLimit(limitHit: boolean, useSlashRootAsSomeNotRootFolderInUserDirectory: boolean) { + const rootFolder = useSlashRootAsSomeNotRootFolderInUserDirectory ? "/user/username/rootfolder/otherfolder/" : "/"; + const file1: File = { + path: rootFolder + "a/b/project/file1.ts", + content: 'import a from "file2"' + }; + const file2: File = { + path: rootFolder + "a/b/node_modules/file2.d.ts", + content: "export class a { }" + }; + const file3: File = { + path: rootFolder + "a/b/project/file3.ts", + content: "export class c { }" + }; + const configFile: File = { + path: rootFolder + "a/b/project/tsconfig.json", + content: JSON.stringify({ compilerOptions: { typeRoots: [] } }) + }; + const projectFiles = [file1, file3, libFile, configFile]; + const openFiles = [file1.path]; + const watchedRecursiveDirectories = useSlashRootAsSomeNotRootFolderInUserDirectory ? + // Folders of node_modules lookup not in changedRoot + ["a/b/project", "a/b/project/node_modules", "a/b/node_modules", "a/node_modules", "node_modules"].map(v => rootFolder + v) : + // Folder of tsconfig + ["/a/b/project", "/a/b/project/node_modules"]; + const host = createServerHost(projectFiles); + const { session, verifyInitialOpen, verifyProjectsUpdatedInBackgroundEventHandler } = createSession(host); + const projectService = session.getProjectService(); + verifyInitialOpen(file1); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = projectService.configuredProjects.get(configFile.path)!; + verifyProject(); + if (limitHit) { + (project as ResolutionCacheHost).maxNumberOfFilesToIterateForInvalidation = 1; + } + file3.content += "export class d {}"; + host.reloadFS(projectFiles); + host.checkTimeoutQueueLengthAndRun(2); + // Since this is first event + verifyProject(); + verifyProjectsUpdatedInBackgroundEventHandler([{ + eventName: ProjectsUpdatedInBackgroundEvent, data: { openFiles } }]); - - projectFiles.push(file2); - host.reloadFS(projectFiles); - host.runQueuedTimeoutCallbacks(); - if (useSlashRootAsSomeNotRootFolderInUserDirectory) { - watchedRecursiveDirectories.length = 3; - } - else { - // file2 addition wont be detected - projectFiles.pop(); - assert.isTrue(host.fileExists(file2.path)); - } - verifyProject(); - - verifyProjectsUpdatedInBackgroundEventHandler(useSlashRootAsSomeNotRootFolderInUserDirectory ? [{ - eventName: server.ProjectsUpdatedInBackgroundEvent, + projectFiles.push(file2); + host.reloadFS(projectFiles); + host.runQueuedTimeoutCallbacks(); + if (useSlashRootAsSomeNotRootFolderInUserDirectory) { + watchedRecursiveDirectories.length = 3; + } + else { + // file2 addition wont be detected + projectFiles.pop(); + assert.isTrue(host.fileExists(file2.path)); + } + verifyProject(); + verifyProjectsUpdatedInBackgroundEventHandler(useSlashRootAsSomeNotRootFolderInUserDirectory ? [{ + eventName: ProjectsUpdatedInBackgroundEvent, data: { openFiles } }] : []); - - function verifyProject() { - checkProjectActualFiles(project, map(projectFiles, file => file.path)); - checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); - } - } - - it("limit not hit and project is not at root level", () => { - verifyWithMaxCacheLimit(/*limitHit*/ false, /*useSlashRootAsSomeNotRootFolderInUserDirectory*/ true); - }); - - it("limit hit and project is not at root level", () => { - verifyWithMaxCacheLimit(/*limitHit*/ true, /*useSlashRootAsSomeNotRootFolderInUserDirectory*/ true); - }); - - it("limit not hit and project is at root level", () => { - verifyWithMaxCacheLimit(/*limitHit*/ false, /*useSlashRootAsSomeNotRootFolderInUserDirectory*/ false); - }); - - it("limit hit and project is at root level", () => { - verifyWithMaxCacheLimit(/*limitHit*/ true, /*useSlashRootAsSomeNotRootFolderInUserDirectory*/ false); - }); - }); - } - - describe("when event handler is set in the session", () => { - verifyProjectsUpdatedInBackgroundEvent(createSessionWithProjectChangedEventHandler); - - function createSessionWithProjectChangedEventHandler(host: TestServerHost): ProjectsUpdatedInBackgroundEventVerifier { - const { session, events: projectChangedEvents } = createSessionWithEventTracking(host, server.ProjectsUpdatedInBackgroundEvent); - return { - session, - verifyProjectsUpdatedInBackgroundEventHandler, - verifyInitialOpen: createVerifyInitialOpen(session, verifyProjectsUpdatedInBackgroundEventHandler) - }; - - function eventToString(event: server.ProjectsUpdatedInBackgroundEvent) { - return JSON.stringify(event && { eventName: event.eventName, data: event.data }); - } - - function eventsToString(events: readonly server.ProjectsUpdatedInBackgroundEvent[]) { - return "[" + map(events, eventToString).join(",") + "]"; - } - - function verifyProjectsUpdatedInBackgroundEventHandler(expectedEvents: readonly server.ProjectsUpdatedInBackgroundEvent[]) { - assert.equal(projectChangedEvents.length, expectedEvents.length, `Incorrect number of events Actual: ${eventsToString(projectChangedEvents)} Expected: ${eventsToString(expectedEvents)}`); - forEach(projectChangedEvents, (actualEvent, i) => { - const expectedEvent = expectedEvents[i]; - assert.strictEqual(actualEvent.eventName, expectedEvent.eventName); - verifyFiles("openFiles", actualEvent.data.openFiles, expectedEvent.data.openFiles); - }); - - // Verified the events, reset them - projectChangedEvents.length = 0; + function verifyProject() { + checkProjectActualFiles(project, map(projectFiles, file => file.path)); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); } } - }); - - describe("when event handler is not set but session is created with canUseEvents = true", () => { - describe("without noGetErrOnBackgroundUpdate, diagnostics for open files are queued", () => { - verifyProjectsUpdatedInBackgroundEvent(createSessionThatUsesEvents); + it("limit not hit and project is not at root level", () => { + verifyWithMaxCacheLimit(/*limitHit*/ false, /*useSlashRootAsSomeNotRootFolderInUserDirectory*/ true); }); - - describe("with noGetErrOnBackgroundUpdate, diagnostics for open file are not queued", () => { - verifyProjectsUpdatedInBackgroundEvent(host => createSessionThatUsesEvents(host, /*noGetErrOnBackgroundUpdate*/ true)); + it("limit hit and project is not at root level", () => { + verifyWithMaxCacheLimit(/*limitHit*/ true, /*useSlashRootAsSomeNotRootFolderInUserDirectory*/ true); }); - - - function createSessionThatUsesEvents(host: TestServerHost, noGetErrOnBackgroundUpdate?: boolean): ProjectsUpdatedInBackgroundEventVerifier { - const { session, getEvents, clearEvents } = createSessionWithDefaultEventHandler(host, server.ProjectsUpdatedInBackgroundEvent, { noGetErrOnBackgroundUpdate }); - - return { - session, - verifyProjectsUpdatedInBackgroundEventHandler, - verifyInitialOpen: createVerifyInitialOpen(session, verifyProjectsUpdatedInBackgroundEventHandler) - }; - - function verifyProjectsUpdatedInBackgroundEventHandler(expected: readonly server.ProjectsUpdatedInBackgroundEvent[]) { - const expectedEvents: protocol.ProjectsUpdatedInBackgroundEventBody[] = map(expected, e => { - return { - openFiles: e.data.openFiles - }; - }); - const events = getEvents(); - assert.equal(events.length, expectedEvents.length, `Incorrect number of events Actual: ${map(events, e => e.body)} Expected: ${expectedEvents}`); - forEach(events, (actualEvent, i) => { - const expectedEvent = expectedEvents[i]; - verifyFiles("openFiles", actualEvent.body.openFiles, expectedEvent.openFiles); - }); - - // Verified the events, reset them - clearEvents(); - - if (events.length) { - host.checkTimeoutQueueLength(noGetErrOnBackgroundUpdate ? 0 : 1); // Error checking queued only if not noGetErrOnBackgroundUpdate - } - } + it("limit not hit and project is at root level", () => { + verifyWithMaxCacheLimit(/*limitHit*/ false, /*useSlashRootAsSomeNotRootFolderInUserDirectory*/ false); + }); + it("limit hit and project is at root level", () => { + verifyWithMaxCacheLimit(/*limitHit*/ true, /*useSlashRootAsSomeNotRootFolderInUserDirectory*/ false); + }); + }); + } + describe("when event handler is set in the session", () => { + verifyProjectsUpdatedInBackgroundEvent(createSessionWithProjectChangedEventHandler); + function createSessionWithProjectChangedEventHandler(host: TestServerHost): ProjectsUpdatedInBackgroundEventVerifier { + const { session, events: projectChangedEvents } = createSessionWithEventTracking(host, ProjectsUpdatedInBackgroundEvent); + return { + session, + verifyProjectsUpdatedInBackgroundEventHandler, + verifyInitialOpen: createVerifyInitialOpen(session, verifyProjectsUpdatedInBackgroundEventHandler) + }; + function eventToString(event: ProjectsUpdatedInBackgroundEvent) { + return JSON.stringify(event && { eventName: event.eventName, data: event.data }); + } + function eventsToString(events: readonly ProjectsUpdatedInBackgroundEvent[]) { + return "[" + map(events, eventToString).join(",") + "]"; } + function verifyProjectsUpdatedInBackgroundEventHandler(expectedEvents: readonly ProjectsUpdatedInBackgroundEvent[]) { + assert.equal(projectChangedEvents.length, expectedEvents.length, `Incorrect number of events Actual: ${eventsToString(projectChangedEvents)} Expected: ${eventsToString(expectedEvents)}`); + forEach(projectChangedEvents, (actualEvent, i) => { + const expectedEvent = expectedEvents[i]; + assert.strictEqual(actualEvent.eventName, expectedEvent.eventName); + verifyFiles("openFiles", actualEvent.data.openFiles, expectedEvent.data.openFiles); + }); + // Verified the events, reset them + projectChangedEvents.length = 0; + } + } + }); + describe("when event handler is not set but session is created with canUseEvents = true", () => { + describe("without noGetErrOnBackgroundUpdate, diagnostics for open files are queued", () => { + verifyProjectsUpdatedInBackgroundEvent(createSessionThatUsesEvents); + }); + describe("with noGetErrOnBackgroundUpdate, diagnostics for open file are not queued", () => { + verifyProjectsUpdatedInBackgroundEvent(host => createSessionThatUsesEvents(host, /*noGetErrOnBackgroundUpdate*/ true)); }); + function createSessionThatUsesEvents(host: TestServerHost, noGetErrOnBackgroundUpdate?: boolean): ProjectsUpdatedInBackgroundEventVerifier { + const { session, getEvents, clearEvents } = createSessionWithDefaultEventHandler(host, ProjectsUpdatedInBackgroundEvent, { noGetErrOnBackgroundUpdate }); + return { + session, + verifyProjectsUpdatedInBackgroundEventHandler, + verifyInitialOpen: createVerifyInitialOpen(session, verifyProjectsUpdatedInBackgroundEventHandler) + }; + function verifyProjectsUpdatedInBackgroundEventHandler(expected: readonly ProjectsUpdatedInBackgroundEvent[]) { + const expectedEvents: protocol.ProjectsUpdatedInBackgroundEventBody[] = map(expected, e => { + return { + openFiles: e.data.openFiles + }; + }); + const events = getEvents(); + assert.equal(events.length, expectedEvents.length, `Incorrect number of events Actual: ${map(events, e => e.body)} Expected: ${expectedEvents}`); + forEach(events, (actualEvent, i) => { + const expectedEvent = expectedEvents[i]; + verifyFiles("openFiles", actualEvent.body.openFiles, expectedEvent.openFiles); + }); + // Verified the events, reset them + clearEvents(); + if (events.length) { + host.checkTimeoutQueueLength(noGetErrOnBackgroundUpdate ? 0 : 1); // Error checking queued only if not noGetErrOnBackgroundUpdate + } + } + } }); -} +}); diff --git a/src/testRunner/unittests/tsserver/externalProjects.ts b/src/testRunner/unittests/tsserver/externalProjects.ts index 54a488c0c94c2..08289ea878f46 100644 --- a/src/testRunner/unittests/tsserver/externalProjects.ts +++ b/src/testRunner/unittests/tsserver/externalProjects.ts @@ -1,903 +1,797 @@ -namespace ts.projectSystem { - describe("unittests:: tsserver:: ExternalProjects", () => { - describe("can handle tsconfig file name with difference casing", () => { - function verifyConfigFileCasing(lazyConfiguredProjectsFromExternalProject: boolean) { - const f1 = { - path: "/a/b/app.ts", - content: "let x = 1" - }; - const config = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ - include: [] - }) - }; - - const host = createServerHost([f1, config], { useCaseSensitiveFileNames: false }); - const service = createProjectService(host); - service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); - const upperCaseConfigFilePath = combinePaths(getDirectoryPath(config.path).toUpperCase(), getBaseFileName(config.path)); - service.openExternalProject({ - projectFileName: "/a/b/project.csproj", - rootFiles: toExternalFiles([f1.path, upperCaseConfigFilePath]), - options: {} - }); - service.checkNumberOfProjects({ configuredProjects: 1 }); - const project = service.configuredProjects.get(config.path)!; - if (lazyConfiguredProjectsFromExternalProject) { - assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.Full); // External project referenced configured project pending to be reloaded - checkProjectActualFiles(project, emptyArray); - } - else { - assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project loaded - checkProjectActualFiles(project, [upperCaseConfigFilePath]); - } - - service.openClientFile(f1.path); - service.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 }); - - assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project is updated - checkProjectActualFiles(project, [upperCaseConfigFilePath]); - checkProjectActualFiles(service.inferredProjects[0], [f1.path]); - } - - it("when lazyConfiguredProjectsFromExternalProject not set", () => { - verifyConfigFileCasing(/*lazyConfiguredProjectsFromExternalProject*/ false); - }); - - it("when lazyConfiguredProjectsFromExternalProject is set", () => { - verifyConfigFileCasing(/*lazyConfiguredProjectsFromExternalProject*/ true); - }); - }); - - it("load global plugins", () => { - const f1 = { - path: "/a/file1.ts", - content: "let x = [1, 2];" - }; - const p1 = { projectFileName: "/a/proj1.csproj", rootFiles: [toExternalFile(f1.path)], options: {} }; - - const host = createServerHost([f1]); - host.require = (_initialPath, moduleName) => { - assert.equal(moduleName, "myplugin"); - return { - module: () => ({ - create(info: server.PluginCreateInfo) { - const proxy = Harness.LanguageService.makeDefaultProxy(info); - proxy.getSemanticDiagnostics = filename => { - const prev = info.languageService.getSemanticDiagnostics(filename); - const sourceFile: SourceFile = info.project.getSourceFile(toPath(filename, /*basePath*/ undefined, createGetCanonicalFileName(info.serverHost.useCaseSensitiveFileNames)))!; - prev.push({ - category: DiagnosticCategory.Warning, - file: sourceFile, - code: 9999, - length: 3, - messageText: `Plugin diagnostic`, - start: 0 - }); - return prev; - }; - return proxy; - } - }), - error: undefined - }; - }; - const session = createSession(host, { globalPlugins: ["myplugin"] }); - - session.executeCommand({ - seq: 1, - type: "request", - command: "openExternalProjects", - arguments: { projects: [p1] } - }); - - const projectService = session.getProjectService(); - checkNumberOfProjects(projectService, { externalProjects: 1 }); - assert.equal(projectService.externalProjects[0].getProjectName(), p1.projectFileName); - - const handlerResponse = session.executeCommand({ - seq: 2, - type: "request", - command: "semanticDiagnosticsSync", - arguments: { - file: f1.path, - projectFileName: p1.projectFileName - } - }); - - assert.isDefined(handlerResponse.response); - const response = handlerResponse.response as protocol.Diagnostic[]; - assert.equal(response.length, 1); - assert.equal(response[0].text, "Plugin diagnostic"); - }); - - it("remove not-listed external projects", () => { +import { createServerHost, createProjectService, protocol, toExternalFiles, checkProjectActualFiles, toExternalFile, createSession, checkNumberOfProjects, File, checkNumberOfExternalProjects, checkNumberOfInferredProjects, verifyDynamic, libFile, checkProjectRootFiles, configuredProjectAt } from "../../ts.projectSystem"; +import { combinePaths, getDirectoryPath, getBaseFileName, ConfigFileProgramReloadLevel, emptyArray, SourceFile, toPath, createGetCanonicalFileName, DiagnosticCategory, arrayIterator, ModuleResolutionKind, map, singleIterator, ScriptKind } from "../../ts"; +import { PluginCreateInfo, maxProgramSizeForNonTsFiles } from "../../ts.server"; +import { LanguageService } from "../../Harness"; +import { projectRoot } from "../../ts.tscWatch"; +describe("unittests:: tsserver:: ExternalProjects", () => { + describe("can handle tsconfig file name with difference casing", () => { + function verifyConfigFileCasing(lazyConfiguredProjectsFromExternalProject: boolean) { const f1 = { - path: "/a/app.ts", - content: "let x = 1" - }; - const f2 = { - path: "/b/app.ts", + path: "/a/b/app.ts", content: "let x = 1" }; - const f3 = { - path: "/c/app.ts", - content: "let x = 1" + const config = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ + include: [] + }) }; - const makeProject = (f: File) => ({ projectFileName: f.path + ".csproj", rootFiles: [toExternalFile(f.path)], options: {} }); - const p1 = makeProject(f1); - const p2 = makeProject(f2); - const p3 = makeProject(f3); - - const host = createServerHost([f1, f2, f3]); - const session = createSession(host); - - session.executeCommand({ - seq: 1, - type: "request", - command: "openExternalProjects", - arguments: { projects: [p1, p2] } - }); - - const projectService = session.getProjectService(); - checkNumberOfProjects(projectService, { externalProjects: 2 }); - assert.equal(projectService.externalProjects[0].getProjectName(), p1.projectFileName); - assert.equal(projectService.externalProjects[1].getProjectName(), p2.projectFileName); - - session.executeCommand({ - seq: 2, - type: "request", - command: "openExternalProjects", - arguments: { projects: [p1, p3] } - }); - checkNumberOfProjects(projectService, { externalProjects: 2 }); - assert.equal(projectService.externalProjects[0].getProjectName(), p1.projectFileName); - assert.equal(projectService.externalProjects[1].getProjectName(), p3.projectFileName); - - session.executeCommand({ - seq: 3, - type: "request", - command: "openExternalProjects", - arguments: { projects: [] } - }); - checkNumberOfProjects(projectService, { externalProjects: 0 }); - - session.executeCommand({ - seq: 3, - type: "request", - command: "openExternalProjects", - arguments: { projects: [p2] } - }); - assert.equal(projectService.externalProjects[0].getProjectName(), p2.projectFileName); + const host = createServerHost([f1, config], { useCaseSensitiveFileNames: false }); + const service = createProjectService(host); + service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); + const upperCaseConfigFilePath = combinePaths(getDirectoryPath(config.path).toUpperCase(), getBaseFileName(config.path)); + service.openExternalProject(({ + projectFileName: "/a/b/project.csproj", + rootFiles: toExternalFiles([f1.path, upperCaseConfigFilePath]), + options: {} + })); + service.checkNumberOfProjects({ configuredProjects: 1 }); + const project = service.configuredProjects.get(config.path)!; + if (lazyConfiguredProjectsFromExternalProject) { + assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.Full); // External project referenced configured project pending to be reloaded + checkProjectActualFiles(project, emptyArray); + } + else { + assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project loaded + checkProjectActualFiles(project, [upperCaseConfigFilePath]); + } + service.openClientFile(f1.path); + service.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 }); + assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project is updated + checkProjectActualFiles(project, [upperCaseConfigFilePath]); + checkProjectActualFiles(service.inferredProjects[0], [f1.path]); + } + it("when lazyConfiguredProjectsFromExternalProject not set", () => { + verifyConfigFileCasing(/*lazyConfiguredProjectsFromExternalProject*/ false); }); - - it("should not close external project with no open files", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x =1;" - }; - const file2 = { - path: "/a/b/f2.ts", - content: "let y =1;" - }; - const externalProjectName = "externalproject"; - const host = createServerHost([file1, file2]); - const projectService = createProjectService(host); - projectService.openExternalProject({ - rootFiles: toExternalFiles([file1.path, file2.path]), - options: {}, - projectFileName: externalProjectName - }); - - checkNumberOfExternalProjects(projectService, 1); - checkNumberOfInferredProjects(projectService, 0); - - // open client file - should not lead to creation of inferred project - projectService.openClientFile(file1.path, file1.content); - checkNumberOfExternalProjects(projectService, 1); - checkNumberOfInferredProjects(projectService, 0); - - // close client file - external project should still exists - projectService.closeClientFile(file1.path); - checkNumberOfExternalProjects(projectService, 1); - checkNumberOfInferredProjects(projectService, 0); - - projectService.closeExternalProject(externalProjectName); - checkNumberOfExternalProjects(projectService, 0); - checkNumberOfInferredProjects(projectService, 0); + it("when lazyConfiguredProjectsFromExternalProject is set", () => { + verifyConfigFileCasing(/*lazyConfiguredProjectsFromExternalProject*/ true); }); - - it("external project for dynamic file", () => { - const externalProjectName = "^ScriptDocument1 file1.ts"; - const externalFiles = toExternalFiles(["^ScriptDocument1 file1.ts"]); - const host = createServerHost([]); - const projectService = createProjectService(host); - projectService.openExternalProject({ - rootFiles: externalFiles, - options: {}, - projectFileName: externalProjectName - }); - - checkNumberOfExternalProjects(projectService, 1); - checkNumberOfInferredProjects(projectService, 0); - verifyDynamic(projectService, "/^scriptdocument1 file1.ts"); - - externalFiles[0].content = "let x =1;"; - projectService.applyChangesInOpenFiles(arrayIterator(externalFiles)); + }); + it("load global plugins", () => { + const f1 = { + path: "/a/file1.ts", + content: "let x = [1, 2];" + }; + const p1 = { projectFileName: "/a/proj1.csproj", rootFiles: [toExternalFile(f1.path)], options: {} }; + const host = createServerHost([f1]); + host.require = (_initialPath, moduleName) => { + assert.equal(moduleName, "myplugin"); + return { + module: () => ({ + create(info: PluginCreateInfo) { + const proxy = LanguageService.makeDefaultProxy(info); + proxy.getSemanticDiagnostics = filename => { + const prev = info.languageService.getSemanticDiagnostics(filename); + const sourceFile: SourceFile = (info.project.getSourceFile(toPath(filename, /*basePath*/ undefined, createGetCanonicalFileName(info.serverHost.useCaseSensitiveFileNames)))!); + prev.push({ + category: DiagnosticCategory.Warning, + file: sourceFile, + code: 9999, + length: 3, + messageText: `Plugin diagnostic`, + start: 0 + }); + return prev; + }; + return proxy; + } + }), + error: undefined + }; + }; + const session = createSession(host, { globalPlugins: ["myplugin"] }); + session.executeCommand(({ + seq: 1, + type: "request", + command: "openExternalProjects", + arguments: { projects: [p1] } + })); + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { externalProjects: 1 }); + assert.equal(projectService.externalProjects[0].getProjectName(), p1.projectFileName); + const handlerResponse = session.executeCommand(({ + seq: 2, + type: "request", + command: "semanticDiagnosticsSync", + arguments: { + file: f1.path, + projectFileName: p1.projectFileName + } + })); + assert.isDefined(handlerResponse.response); + const response = (handlerResponse.response as protocol.Diagnostic[]); + assert.equal(response.length, 1); + assert.equal(response[0].text, "Plugin diagnostic"); + }); + it("remove not-listed external projects", () => { + const f1 = { + path: "/a/app.ts", + content: "let x = 1" + }; + const f2 = { + path: "/b/app.ts", + content: "let x = 1" + }; + const f3 = { + path: "/c/app.ts", + content: "let x = 1" + }; + const makeProject = (f: File) => ({ projectFileName: f.path + ".csproj", rootFiles: [toExternalFile(f.path)], options: {} }); + const p1 = makeProject(f1); + const p2 = makeProject(f2); + const p3 = makeProject(f3); + const host = createServerHost([f1, f2, f3]); + const session = createSession(host); + session.executeCommand(({ + seq: 1, + type: "request", + command: "openExternalProjects", + arguments: { projects: [p1, p2] } + })); + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { externalProjects: 2 }); + assert.equal(projectService.externalProjects[0].getProjectName(), p1.projectFileName); + assert.equal(projectService.externalProjects[1].getProjectName(), p2.projectFileName); + session.executeCommand(({ + seq: 2, + type: "request", + command: "openExternalProjects", + arguments: { projects: [p1, p3] } + })); + checkNumberOfProjects(projectService, { externalProjects: 2 }); + assert.equal(projectService.externalProjects[0].getProjectName(), p1.projectFileName); + assert.equal(projectService.externalProjects[1].getProjectName(), p3.projectFileName); + session.executeCommand(({ + seq: 3, + type: "request", + command: "openExternalProjects", + arguments: { projects: [] } + })); + checkNumberOfProjects(projectService, { externalProjects: 0 }); + session.executeCommand(({ + seq: 3, + type: "request", + command: "openExternalProjects", + arguments: { projects: [p2] } + })); + assert.equal(projectService.externalProjects[0].getProjectName(), p2.projectFileName); + }); + it("should not close external project with no open files", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x =1;" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y =1;" + }; + const externalProjectName = "externalproject"; + const host = createServerHost([file1, file2]); + const projectService = createProjectService(host); + projectService.openExternalProject({ + rootFiles: toExternalFiles([file1.path, file2.path]), + options: {}, + projectFileName: externalProjectName }); - - it("when file name starts with ^", () => { - const file: File = { - path: `${tscWatch.projectRoot}/file.ts`, - content: "const x = 10;" - }; - const app: File = { - path: `${tscWatch.projectRoot}/^app.ts`, - content: "const y = 10;" - }; - const host = createServerHost([file, app, libFile]); - const service = createProjectService(host); - service.openExternalProjects([{ - projectFileName: `${tscWatch.projectRoot}/myproject.njsproj`, + checkNumberOfExternalProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 0); + // open client file - should not lead to creation of inferred project + projectService.openClientFile(file1.path, file1.content); + checkNumberOfExternalProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 0); + // close client file - external project should still exists + projectService.closeClientFile(file1.path); + checkNumberOfExternalProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 0); + projectService.closeExternalProject(externalProjectName); + checkNumberOfExternalProjects(projectService, 0); + checkNumberOfInferredProjects(projectService, 0); + }); + it("external project for dynamic file", () => { + const externalProjectName = "^ScriptDocument1 file1.ts"; + const externalFiles = toExternalFiles(["^ScriptDocument1 file1.ts"]); + const host = createServerHost([]); + const projectService = createProjectService(host); + projectService.openExternalProject({ + rootFiles: externalFiles, + options: {}, + projectFileName: externalProjectName + }); + checkNumberOfExternalProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 0); + verifyDynamic(projectService, "/^scriptdocument1 file1.ts"); + externalFiles[0].content = "let x =1;"; + projectService.applyChangesInOpenFiles(arrayIterator(externalFiles)); + }); + it("when file name starts with ^", () => { + const file: File = { + path: `${projectRoot}/file.ts`, + content: "const x = 10;" + }; + const app: File = { + path: `${projectRoot}/^app.ts`, + content: "const y = 10;" + }; + const host = createServerHost([file, app, libFile]); + const service = createProjectService(host); + service.openExternalProjects([{ + projectFileName: `${projectRoot}/myproject.njsproj`, rootFiles: [ toExternalFile(file.path), toExternalFile(app.path) ], - options: { }, + options: {}, }]); + }); + it("external project that included config files", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x =1;" + }; + const config1 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ + compilerOptions: {}, + files: ["f1.ts"] + }) + }; + const file2 = { + path: "/a/c/f2.ts", + content: "let y =1;" + }; + const config2 = { + path: "/a/c/tsconfig.json", + content: JSON.stringify({ + compilerOptions: {}, + files: ["f2.ts"] + }) + }; + const file3 = { + path: "/a/d/f3.ts", + content: "let z =1;" + }; + const externalProjectName = "externalproject"; + const host = createServerHost([file1, file2, file3, config1, config2]); + const projectService = createProjectService(host); + projectService.openExternalProject({ + rootFiles: toExternalFiles([config1.path, config2.path, file3.path]), + options: {}, + projectFileName: externalProjectName }); - - it("external project that included config files", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x =1;" - }; - const config1 = { - path: "/a/b/tsconfig.json", - content: JSON.stringify( - { - compilerOptions: {}, - files: ["f1.ts"] - } - ) - }; - const file2 = { - path: "/a/c/f2.ts", - content: "let y =1;" - }; - const config2 = { - path: "/a/c/tsconfig.json", - content: JSON.stringify( - { - compilerOptions: {}, - files: ["f2.ts"] - } - ) - }; - const file3 = { - path: "/a/d/f3.ts", - content: "let z =1;" - }; - const externalProjectName = "externalproject"; - const host = createServerHost([file1, file2, file3, config1, config2]); - const projectService = createProjectService(host); - projectService.openExternalProject({ - rootFiles: toExternalFiles([config1.path, config2.path, file3.path]), - options: {}, - projectFileName: externalProjectName - }); - - checkNumberOfProjects(projectService, { configuredProjects: 2 }); - const proj1 = projectService.configuredProjects.get(config1.path); - const proj2 = projectService.configuredProjects.get(config2.path); - assert.isDefined(proj1); - assert.isDefined(proj2); - - // open client file - should not lead to creation of inferred project - projectService.openClientFile(file1.path, file1.content); - checkNumberOfProjects(projectService, { configuredProjects: 2 }); - assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); - assert.strictEqual(projectService.configuredProjects.get(config2.path), proj2); - - projectService.openClientFile(file3.path, file3.content); - checkNumberOfProjects(projectService, { configuredProjects: 2, inferredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); - assert.strictEqual(projectService.configuredProjects.get(config2.path), proj2); - - projectService.closeExternalProject(externalProjectName); - // open file 'file1' from configured project keeps project alive - checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); - assert.isUndefined(projectService.configuredProjects.get(config2.path)); - - projectService.closeClientFile(file3.path); - checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); - assert.isUndefined(projectService.configuredProjects.get(config2.path)); - assert.isTrue(projectService.inferredProjects[0].isOrphan()); - - projectService.closeClientFile(file1.path); - checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); - assert.isUndefined(projectService.configuredProjects.get(config2.path)); - assert.isTrue(projectService.inferredProjects[0].isOrphan()); - - projectService.openClientFile(file2.path, file2.content); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.isUndefined(projectService.configuredProjects.get(config1.path)); - assert.isDefined(projectService.configuredProjects.get(config2.path)); + checkNumberOfProjects(projectService, { configuredProjects: 2 }); + const proj1 = projectService.configuredProjects.get(config1.path); + const proj2 = projectService.configuredProjects.get(config2.path); + assert.isDefined(proj1); + assert.isDefined(proj2); + // open client file - should not lead to creation of inferred project + projectService.openClientFile(file1.path, file1.content); + checkNumberOfProjects(projectService, { configuredProjects: 2 }); + assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); + assert.strictEqual(projectService.configuredProjects.get(config2.path), proj2); + projectService.openClientFile(file3.path, file3.content); + checkNumberOfProjects(projectService, { configuredProjects: 2, inferredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); + assert.strictEqual(projectService.configuredProjects.get(config2.path), proj2); + projectService.closeExternalProject(externalProjectName); + // open file 'file1' from configured project keeps project alive + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); + assert.isUndefined(projectService.configuredProjects.get(config2.path)); + projectService.closeClientFile(file3.path); + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); + assert.isUndefined(projectService.configuredProjects.get(config2.path)); + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + projectService.closeClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); + assert.isUndefined(projectService.configuredProjects.get(config2.path)); + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + projectService.openClientFile(file2.path, file2.content); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.isUndefined(projectService.configuredProjects.get(config1.path)); + assert.isDefined(projectService.configuredProjects.get(config2.path)); + }); + it("external project with included config file opened after configured project", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {} }) + }; + const externalProjectName = "externalproject"; + const host = createServerHost([file1, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + projectService.openExternalProject({ + rootFiles: toExternalFiles([configFile.path]), + options: {}, + projectFileName: externalProjectName }); - - it("external project with included config file opened after configured project", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x = 1" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: {} }) - }; - const externalProjectName = "externalproject"; - const host = createServerHost([file1, configFile]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - - projectService.openExternalProject({ - rootFiles: toExternalFiles([configFile.path]), - options: {}, - projectFileName: externalProjectName - }); - - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - - projectService.closeClientFile(file1.path); - // configured project is alive since it is opened as part of external project - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - - projectService.closeExternalProject(externalProjectName); - checkNumberOfProjects(projectService, { configuredProjects: 0 }); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + projectService.closeClientFile(file1.path); + // configured project is alive since it is opened as part of external project + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + projectService.closeExternalProject(externalProjectName); + checkNumberOfProjects(projectService, { configuredProjects: 0 }); + }); + it("external project with included config file opened after configured project and then closed", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/f2.ts", + content: "let x = 1" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {} }) + }; + const externalProjectName = "externalproject"; + const host = createServerHost([file1, file2, libFile, configFile]); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = projectService.configuredProjects.get(configFile.path); + projectService.openExternalProject({ + rootFiles: toExternalFiles([configFile.path]), + options: {}, + projectFileName: externalProjectName }); - - it("external project with included config file opened after configured project and then closed", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x = 1" - }; - const file2 = { - path: "/a/f2.ts", - content: "let x = 1" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ compilerOptions: {} }) - }; - const externalProjectName = "externalproject"; - const host = createServerHost([file1, file2, libFile, configFile]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const project = projectService.configuredProjects.get(configFile.path); - - projectService.openExternalProject({ - rootFiles: toExternalFiles([configFile.path]), - options: {}, - projectFileName: externalProjectName - }); - - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - - projectService.closeExternalProject(externalProjectName); - // configured project is alive since file is still open - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - - projectService.closeClientFile(file1.path); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - - projectService.openClientFile(file2.path); - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - assert.isUndefined(projectService.configuredProjects.get(configFile.path)); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + projectService.closeExternalProject(externalProjectName); + // configured project is alive since file is still open + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + projectService.closeClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + projectService.openClientFile(file2.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + assert.isUndefined(projectService.configuredProjects.get(configFile.path)); + }); + it("can correctly update external project when set of root files has changed", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const host = createServerHost([file1, file2]); + const projectService = createProjectService(host); + projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path]) }); + checkNumberOfProjects(projectService, { externalProjects: 1 }); + checkProjectActualFiles(projectService.externalProjects[0], [file1.path]); + projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, file2.path]) }); + checkNumberOfProjects(projectService, { externalProjects: 1 }); + checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); + }); + it("can update external project when set of root files was not changed", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `export * from "m"` + }; + const file2 = { + path: "/a/b/f2.ts", + content: "export let y = 1" + }; + const file3 = { + path: "/a/m.ts", + content: "export let y = 1" + }; + const host = createServerHost([file1, file2, file3]); + const projectService = createProjectService(host); + projectService.openExternalProject({ projectFileName: "project", options: { moduleResolution: ModuleResolutionKind.NodeJs }, rootFiles: toExternalFiles([file1.path, file2.path]) }); + checkNumberOfProjects(projectService, { externalProjects: 1 }); + checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); + checkProjectActualFiles(projectService.externalProjects[0], [file1.path, file2.path]); + projectService.openExternalProject({ projectFileName: "project", options: { moduleResolution: ModuleResolutionKind.Classic }, rootFiles: toExternalFiles([file1.path, file2.path]) }); + checkNumberOfProjects(projectService, { externalProjects: 1 }); + checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); + checkProjectActualFiles(projectService.externalProjects[0], [file1.path, file2.path, file3.path]); + }); + it("language service disabled state is updated in external projects", () => { + const f1 = { + path: "/a/app.js", + content: "var x = 1" + }; + const f2 = { + path: "/a/largefile.js", + content: "" + }; + const host = createServerHost([f1, f2]); + const originalGetFileSize = host.getFileSize; + host.getFileSize = (filePath: string) => filePath === f2.path ? maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); + const service = createProjectService(host); + const projectFileName = "/a/proj.csproj"; + service.openExternalProject({ + projectFileName, + rootFiles: toExternalFiles([f1.path, f2.path]), + options: {} }); - - it("can correctly update external project when set of root files has changed", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x = 1" - }; - const file2 = { - path: "/a/b/f2.ts", - content: "let y = 1" - }; - const host = createServerHost([file1, file2]); - const projectService = createProjectService(host); - - projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path]) }); - checkNumberOfProjects(projectService, { externalProjects: 1 }); - checkProjectActualFiles(projectService.externalProjects[0], [file1.path]); - - projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, file2.path]) }); - checkNumberOfProjects(projectService, { externalProjects: 1 }); - checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); + service.checkNumberOfProjects({ externalProjects: 1 }); + assert.isFalse(service.externalProjects[0].languageServiceEnabled, "language service should be disabled - 1"); + service.openExternalProject({ + projectFileName, + rootFiles: toExternalFiles([f1.path]), + options: {} }); - - it("can update external project when set of root files was not changed", () => { - const file1 = { - path: "/a/b/f1.ts", - content: `export * from "m"` - }; - const file2 = { - path: "/a/b/f2.ts", - content: "export let y = 1" + service.checkNumberOfProjects({ externalProjects: 1 }); + assert.isTrue(service.externalProjects[0].languageServiceEnabled, "language service should be enabled"); + service.openExternalProject({ + projectFileName, + rootFiles: toExternalFiles([f1.path, f2.path]), + options: {} + }); + service.checkNumberOfProjects({ externalProjects: 1 }); + assert.isFalse(service.externalProjects[0].languageServiceEnabled, "language service should be disabled - 2"); + }); + describe("deleting config file opened from the external project works", () => { + function verifyDeletingConfigFile(lazyConfiguredProjectsFromExternalProject: boolean) { + const site = { + path: "/user/someuser/project/js/site.js", + content: "" }; - const file3 = { - path: "/a/m.ts", - content: "export let y = 1" + const configFile = { + path: "/user/someuser/project/tsconfig.json", + content: "{}" }; - - const host = createServerHost([file1, file2, file3]); + const projectFileName = "/user/someuser/project/WebApplication6.csproj"; + const host = createServerHost([libFile, site, configFile]); const projectService = createProjectService(host); - - projectService.openExternalProject({ projectFileName: "project", options: { moduleResolution: ModuleResolutionKind.NodeJs }, rootFiles: toExternalFiles([file1.path, file2.path]) }); - checkNumberOfProjects(projectService, { externalProjects: 1 }); - checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); - checkProjectActualFiles(projectService.externalProjects[0], [file1.path, file2.path]); - - projectService.openExternalProject({ projectFileName: "project", options: { moduleResolution: ModuleResolutionKind.Classic }, rootFiles: toExternalFiles([file1.path, file2.path]) }); - checkNumberOfProjects(projectService, { externalProjects: 1 }); - checkProjectRootFiles(projectService.externalProjects[0], [file1.path, file2.path]); - checkProjectActualFiles(projectService.externalProjects[0], [file1.path, file2.path, file3.path]); + projectService.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); + const externalProject: protocol.ExternalProject = { + projectFileName, + rootFiles: [toExternalFile(site.path), toExternalFile(configFile.path)], + options: { allowJs: false }, + typeAcquisition: { include: [] } + }; + projectService.openExternalProjects([externalProject]); + let knownProjects = projectService.synchronizeProjectList([]); + checkNumberOfProjects(projectService, { configuredProjects: 1, externalProjects: 0, inferredProjects: 0 }); + const configProject = configuredProjectAt(projectService, 0); + checkProjectActualFiles(configProject, lazyConfiguredProjectsFromExternalProject ? emptyArray : // Since no files opened from this project, its not loaded + [configFile.path]); + host.reloadFS([libFile, site]); + host.checkTimeoutQueueLengthAndRun(1); + knownProjects = projectService.synchronizeProjectList(map(knownProjects, proj => proj.info!)); // TODO: GH#18217 GH#20039 + checkNumberOfProjects(projectService, { configuredProjects: 0, externalProjects: 0, inferredProjects: 0 }); + externalProject.rootFiles.length = 1; + projectService.openExternalProjects([externalProject]); + checkNumberOfProjects(projectService, { configuredProjects: 0, externalProjects: 1, inferredProjects: 0 }); + checkProjectActualFiles(projectService.externalProjects[0], [site.path, libFile.path]); + } + it("when lazyConfiguredProjectsFromExternalProject not set", () => { + verifyDeletingConfigFile(/*lazyConfiguredProjectsFromExternalProject*/ false); }); - - it("language service disabled state is updated in external projects", () => { + it("when lazyConfiguredProjectsFromExternalProject is set", () => { + verifyDeletingConfigFile(/*lazyConfiguredProjectsFromExternalProject*/ true); + }); + }); + describe("correctly handling add/remove tsconfig - 1", () => { + function verifyAddRemoveConfig(lazyConfiguredProjectsFromExternalProject: boolean) { const f1 = { - path: "/a/app.js", - content: "var x = 1" + path: "/a/b/app.ts", + content: "let x = 1;" }; const f2 = { - path: "/a/largefile.js", + path: "/a/b/lib.ts", + content: "" + }; + const tsconfig = { + path: "/a/b/tsconfig.json", content: "" }; const host = createServerHost([f1, f2]); - const originalGetFileSize = host.getFileSize; - host.getFileSize = (filePath: string) => - filePath === f2.path ? server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); - - const service = createProjectService(host); - const projectFileName = "/a/proj.csproj"; - - service.openExternalProject({ - projectFileName, + const projectService = createProjectService(host); + projectService.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); + // open external project + const projectName = "/a/b/proj1"; + projectService.openExternalProject({ + projectFileName: projectName, rootFiles: toExternalFiles([f1.path, f2.path]), options: {} }); - service.checkNumberOfProjects({ externalProjects: 1 }); - assert.isFalse(service.externalProjects[0].languageServiceEnabled, "language service should be disabled - 1"); - - service.openExternalProject({ - projectFileName, - rootFiles: toExternalFiles([f1.path]), + projectService.openClientFile(f1.path); + projectService.checkNumberOfProjects({ externalProjects: 1 }); + checkProjectActualFiles(projectService.externalProjects[0], [f1.path, f2.path]); + // rename lib.ts to tsconfig.json + host.reloadFS([f1, tsconfig]); + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path, tsconfig.path]), options: {} }); - service.checkNumberOfProjects({ externalProjects: 1 }); - assert.isTrue(service.externalProjects[0].languageServiceEnabled, "language service should be enabled"); - - service.openExternalProject({ - projectFileName, + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + if (lazyConfiguredProjectsFromExternalProject) { + checkProjectActualFiles(configuredProjectAt(projectService, 0), emptyArray); // Configured project created but not loaded till actually needed + projectService.ensureInferredProjectsUpToDate_TestOnly(); + } + checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, tsconfig.path]); + // rename tsconfig.json back to lib.ts + host.reloadFS([f1, f2]); + projectService.openExternalProject({ + projectFileName: projectName, rootFiles: toExternalFiles([f1.path, f2.path]), options: {} }); - service.checkNumberOfProjects({ externalProjects: 1 }); - assert.isFalse(service.externalProjects[0].languageServiceEnabled, "language service should be disabled - 2"); + projectService.checkNumberOfProjects({ externalProjects: 1 }); + checkProjectActualFiles(projectService.externalProjects[0], [f1.path, f2.path]); + } + it("when lazyConfiguredProjectsFromExternalProject not set", () => { + verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ false); }); - - describe("deleting config file opened from the external project works", () => { - function verifyDeletingConfigFile(lazyConfiguredProjectsFromExternalProject: boolean) { - const site = { - path: "/user/someuser/project/js/site.js", - content: "" - }; - const configFile = { - path: "/user/someuser/project/tsconfig.json", - content: "{}" - }; - const projectFileName = "/user/someuser/project/WebApplication6.csproj"; - const host = createServerHost([libFile, site, configFile]); - const projectService = createProjectService(host); - projectService.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); - - const externalProject: protocol.ExternalProject = { - projectFileName, - rootFiles: [toExternalFile(site.path), toExternalFile(configFile.path)], - options: { allowJs: false }, - typeAcquisition: { include: [] } - }; - - projectService.openExternalProjects([externalProject]); - - let knownProjects = projectService.synchronizeProjectList([]); - checkNumberOfProjects(projectService, { configuredProjects: 1, externalProjects: 0, inferredProjects: 0 }); - - const configProject = configuredProjectAt(projectService, 0); - checkProjectActualFiles(configProject, lazyConfiguredProjectsFromExternalProject ? - emptyArray : // Since no files opened from this project, its not loaded - [configFile.path]); - - host.reloadFS([libFile, site]); - host.checkTimeoutQueueLengthAndRun(1); - - knownProjects = projectService.synchronizeProjectList(map(knownProjects, proj => proj.info!)); // TODO: GH#18217 GH#20039 - checkNumberOfProjects(projectService, { configuredProjects: 0, externalProjects: 0, inferredProjects: 0 }); - - externalProject.rootFiles.length = 1; - projectService.openExternalProjects([externalProject]); - - checkNumberOfProjects(projectService, { configuredProjects: 0, externalProjects: 1, inferredProjects: 0 }); - checkProjectActualFiles(projectService.externalProjects[0], [site.path, libFile.path]); - } - it("when lazyConfiguredProjectsFromExternalProject not set", () => { - verifyDeletingConfigFile(/*lazyConfiguredProjectsFromExternalProject*/ false); - }); - it("when lazyConfiguredProjectsFromExternalProject is set", () => { - verifyDeletingConfigFile(/*lazyConfiguredProjectsFromExternalProject*/ true); - }); + it("when lazyConfiguredProjectsFromExternalProject is set", () => { + verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ true); }); - - describe("correctly handling add/remove tsconfig - 1", () => { - function verifyAddRemoveConfig(lazyConfiguredProjectsFromExternalProject: boolean) { - const f1 = { - path: "/a/b/app.ts", - content: "let x = 1;" - }; - const f2 = { - path: "/a/b/lib.ts", - content: "" - }; - const tsconfig = { - path: "/a/b/tsconfig.json", - content: "" - }; - const host = createServerHost([f1, f2]); - const projectService = createProjectService(host); - projectService.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); - - // open external project - const projectName = "/a/b/proj1"; - projectService.openExternalProject({ - projectFileName: projectName, - rootFiles: toExternalFiles([f1.path, f2.path]), - options: {} - }); - projectService.openClientFile(f1.path); - projectService.checkNumberOfProjects({ externalProjects: 1 }); - checkProjectActualFiles(projectService.externalProjects[0], [f1.path, f2.path]); - - // rename lib.ts to tsconfig.json - host.reloadFS([f1, tsconfig]); - projectService.openExternalProject({ - projectFileName: projectName, - rootFiles: toExternalFiles([f1.path, tsconfig.path]), - options: {} - }); - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - if (lazyConfiguredProjectsFromExternalProject) { - checkProjectActualFiles(configuredProjectAt(projectService, 0), emptyArray); // Configured project created but not loaded till actually needed - projectService.ensureInferredProjectsUpToDate_TestOnly(); - } - checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, tsconfig.path]); - - // rename tsconfig.json back to lib.ts - host.reloadFS([f1, f2]); - projectService.openExternalProject({ - projectFileName: projectName, - rootFiles: toExternalFiles([f1.path, f2.path]), - options: {} - }); - - projectService.checkNumberOfProjects({ externalProjects: 1 }); - checkProjectActualFiles(projectService.externalProjects[0], [f1.path, f2.path]); - } - it("when lazyConfiguredProjectsFromExternalProject not set", () => { - verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ false); - }); - it("when lazyConfiguredProjectsFromExternalProject is set", () => { - verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ true); - }); - }); - - describe("correctly handling add/remove tsconfig - 2", () => { - function verifyAddRemoveConfig(lazyConfiguredProjectsFromExternalProject: boolean) { - const f1 = { - path: "/a/b/app.ts", - content: "let x = 1;" - }; - const cLib = { - path: "/a/b/c/lib.ts", - content: "" - }; - const cTsconfig = { - path: "/a/b/c/tsconfig.json", - content: "{}" - }; - const dLib = { - path: "/a/b/d/lib.ts", - content: "" - }; - const dTsconfig = { - path: "/a/b/d/tsconfig.json", - content: "{}" - }; - const host = createServerHost([f1, cLib, cTsconfig, dLib, dTsconfig]); - const projectService = createProjectService(host); - projectService.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); - - // open external project - const projectName = "/a/b/proj1"; - projectService.openExternalProject({ - projectFileName: projectName, - rootFiles: toExternalFiles([f1.path]), - options: {} - }); - - projectService.checkNumberOfProjects({ externalProjects: 1 }); - checkProjectActualFiles(projectService.externalProjects[0], [f1.path]); - - // add two config file as root files - projectService.openExternalProject({ - projectFileName: projectName, - rootFiles: toExternalFiles([f1.path, cTsconfig.path, dTsconfig.path]), - options: {} - }); - projectService.checkNumberOfProjects({ configuredProjects: 2 }); - if (lazyConfiguredProjectsFromExternalProject) { - checkProjectActualFiles(configuredProjectAt(projectService, 0), emptyArray); // Configured project created but not loaded till actually needed - checkProjectActualFiles(configuredProjectAt(projectService, 1), emptyArray); // Configured project created but not loaded till actually needed - projectService.ensureInferredProjectsUpToDate_TestOnly(); - } - checkProjectActualFiles(configuredProjectAt(projectService, 0), [cLib.path, cTsconfig.path]); - checkProjectActualFiles(configuredProjectAt(projectService, 1), [dLib.path, dTsconfig.path]); - - // remove one config file - projectService.openExternalProject({ - projectFileName: projectName, - rootFiles: toExternalFiles([f1.path, dTsconfig.path]), - options: {} - }); - - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [dLib.path, dTsconfig.path]); - - // remove second config file - projectService.openExternalProject({ - projectFileName: projectName, - rootFiles: toExternalFiles([f1.path]), - options: {} - }); - - projectService.checkNumberOfProjects({ externalProjects: 1 }); - checkProjectActualFiles(projectService.externalProjects[0], [f1.path]); - - // open two config files - // add two config file as root files - projectService.openExternalProject({ - projectFileName: projectName, - rootFiles: toExternalFiles([f1.path, cTsconfig.path, dTsconfig.path]), - options: {} - }); - projectService.checkNumberOfProjects({ configuredProjects: 2 }); - if (lazyConfiguredProjectsFromExternalProject) { - checkProjectActualFiles(configuredProjectAt(projectService, 0), emptyArray); // Configured project created but not loaded till actually needed - checkProjectActualFiles(configuredProjectAt(projectService, 1), emptyArray); // Configured project created but not loaded till actually needed - projectService.ensureInferredProjectsUpToDate_TestOnly(); - } - checkProjectActualFiles(configuredProjectAt(projectService, 0), [cLib.path, cTsconfig.path]); - checkProjectActualFiles(configuredProjectAt(projectService, 1), [dLib.path, dTsconfig.path]); - - // close all projects - no projects should be opened - projectService.closeExternalProject(projectName); - projectService.checkNumberOfProjects({}); - } - - it("when lazyConfiguredProjectsFromExternalProject not set", () => { - verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ false); - }); - it("when lazyConfiguredProjectsFromExternalProject is set", () => { - verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ true); - }); - }); - - it("correctly handles changes in lib section of config file", () => { - const libES5 = { - path: "/compiler/lib.es5.d.ts", - content: "declare const eval: any" - }; - const libES2015Promise = { - path: "/compiler/lib.es2015.promise.d.ts", - content: "declare class Promise {}" - }; - const app = { - path: "/src/app.ts", - content: "var x: Promise;" + }); + describe("correctly handling add/remove tsconfig - 2", () => { + function verifyAddRemoveConfig(lazyConfiguredProjectsFromExternalProject: boolean) { + const f1 = { + path: "/a/b/app.ts", + content: "let x = 1;" }; - const config1 = { - path: "/src/tsconfig.json", - content: JSON.stringify( - { - compilerOptions: { - module: "commonjs", - target: "es5", - noImplicitAny: true, - sourceMap: false, - lib: [ - "es5" - ] - } - }) + const cLib = { + path: "/a/b/c/lib.ts", + content: "" }; - const config2 = { - path: config1.path, - content: JSON.stringify( - { - compilerOptions: { - module: "commonjs", - target: "es5", - noImplicitAny: true, - sourceMap: false, - lib: [ - "es5", - "es2015.promise" - ] - } - }) + const cTsconfig = { + path: "/a/b/c/tsconfig.json", + content: "{}" }; - const host = createServerHost([libES5, libES2015Promise, app, config1], { executingFilePath: "/compiler/tsc.js" }); - const projectService = createProjectService(host); - projectService.openClientFile(app.path); - - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [libES5.path, app.path, config1.path]); - - host.reloadFS([libES5, libES2015Promise, app, config2]); - host.checkTimeoutQueueLengthAndRun(2); - - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [libES5.path, libES2015Promise.path, app.path, config2.path]); - }); - - it("should handle non-existing directories in config file", () => { - const f = { - path: "/a/src/app.ts", - content: "let x = 1;" + const dLib = { + path: "/a/b/d/lib.ts", + content: "" }; - const config = { - path: "/a/tsconfig.json", - content: JSON.stringify({ - compilerOptions: {}, - include: [ - "src/**/*", - "notexistingfolder/*" - ] - }) + const dTsconfig = { + path: "/a/b/d/tsconfig.json", + content: "{}" }; - const host = createServerHost([f, config]); + const host = createServerHost([f1, cLib, cTsconfig, dLib, dTsconfig]); const projectService = createProjectService(host); - projectService.openClientFile(f.path); - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - const project = projectService.configuredProjects.get(config.path)!; - assert.isTrue(project.hasOpenRef()); // f - - projectService.closeClientFile(f.path); - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(config.path), project); - assert.isFalse(project.hasOpenRef()); // No files - assert.isFalse(project.isClosed()); - - projectService.openClientFile(f.path); + projectService.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } }); + // open external project + const projectName = "/a/b/proj1"; + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path]), + options: {} + }); + projectService.checkNumberOfProjects({ externalProjects: 1 }); + checkProjectActualFiles(projectService.externalProjects[0], [f1.path]); + // add two config file as root files + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path, cTsconfig.path, dTsconfig.path]), + options: {} + }); + projectService.checkNumberOfProjects({ configuredProjects: 2 }); + if (lazyConfiguredProjectsFromExternalProject) { + checkProjectActualFiles(configuredProjectAt(projectService, 0), emptyArray); // Configured project created but not loaded till actually needed + checkProjectActualFiles(configuredProjectAt(projectService, 1), emptyArray); // Configured project created but not loaded till actually needed + projectService.ensureInferredProjectsUpToDate_TestOnly(); + } + checkProjectActualFiles(configuredProjectAt(projectService, 0), [cLib.path, cTsconfig.path]); + checkProjectActualFiles(configuredProjectAt(projectService, 1), [dLib.path, dTsconfig.path]); + // remove one config file + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path, dTsconfig.path]), + options: {} + }); projectService.checkNumberOfProjects({ configuredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects.get(config.path), project); - assert.isTrue(project.hasOpenRef()); // f - assert.isFalse(project.isClosed()); - }); - - it("handles loads existing configured projects of external projects when lazyConfiguredProjectsFromExternalProject is disabled", () => { - const f1 = { - path: "/a/b/app.ts", - content: "let x = 1" - }; - const config = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({}) - }; - const projectFileName = "/a/b/project.csproj"; - const host = createServerHost([f1, config]); - const service = createProjectService(host); - service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject: true } }); - service.openExternalProject({ - projectFileName, - rootFiles: toExternalFiles([f1.path, config.path]), + checkProjectActualFiles(configuredProjectAt(projectService, 0), [dLib.path, dTsconfig.path]); + // remove second config file + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path]), options: {} }); - service.checkNumberOfProjects({ configuredProjects: 1 }); - const project = service.configuredProjects.get(config.path)!; - assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.Full); // External project referenced configured project pending to be reloaded - checkProjectActualFiles(project, emptyArray); - - service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject: false } }); - assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project loaded - checkProjectActualFiles(project, [config.path, f1.path]); - - service.closeExternalProject(projectFileName); - service.checkNumberOfProjects({}); - - service.openExternalProject({ - projectFileName, - rootFiles: toExternalFiles([f1.path, config.path]), + projectService.checkNumberOfProjects({ externalProjects: 1 }); + checkProjectActualFiles(projectService.externalProjects[0], [f1.path]); + // open two config files + // add two config file as root files + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path, cTsconfig.path, dTsconfig.path]), options: {} }); - service.checkNumberOfProjects({ configuredProjects: 1 }); - const project2 = service.configuredProjects.get(config.path)!; - assert.equal(project2.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project loaded - checkProjectActualFiles(project2, [config.path, f1.path]); + projectService.checkNumberOfProjects({ configuredProjects: 2 }); + if (lazyConfiguredProjectsFromExternalProject) { + checkProjectActualFiles(configuredProjectAt(projectService, 0), emptyArray); // Configured project created but not loaded till actually needed + checkProjectActualFiles(configuredProjectAt(projectService, 1), emptyArray); // Configured project created but not loaded till actually needed + projectService.ensureInferredProjectsUpToDate_TestOnly(); + } + checkProjectActualFiles(configuredProjectAt(projectService, 0), [cLib.path, cTsconfig.path]); + checkProjectActualFiles(configuredProjectAt(projectService, 1), [dLib.path, dTsconfig.path]); + // close all projects - no projects should be opened + projectService.closeExternalProject(projectName); + projectService.checkNumberOfProjects({}); + } + it("when lazyConfiguredProjectsFromExternalProject not set", () => { + verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ false); }); - - it("handles creation of external project with jsconfig before jsconfig creation watcher is invoked", () => { - const projectFileName = `${tscWatch.projectRoot}/WebApplication36.csproj`; - const tsconfig: File = { - path: `${tscWatch.projectRoot}/tsconfig.json`, - content: "{}" - }; - const files = [libFile, tsconfig]; - const host = createServerHost(files); - const service = createProjectService(host); - - // Create external project - service.openExternalProjects([{ + it("when lazyConfiguredProjectsFromExternalProject is set", () => { + verifyAddRemoveConfig(/*lazyConfiguredProjectsFromExternalProject*/ true); + }); + }); + it("correctly handles changes in lib section of config file", () => { + const libES5 = { + path: "/compiler/lib.es5.d.ts", + content: "declare const eval: any" + }; + const libES2015Promise = { + path: "/compiler/lib.es2015.promise.d.ts", + content: "declare class Promise {}" + }; + const app = { + path: "/src/app.ts", + content: "var x: Promise;" + }; + const config1 = { + path: "/src/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + module: "commonjs", + target: "es5", + noImplicitAny: true, + sourceMap: false, + lib: [ + "es5" + ] + } + }) + }; + const config2 = { + path: config1.path, + content: JSON.stringify({ + compilerOptions: { + module: "commonjs", + target: "es5", + noImplicitAny: true, + sourceMap: false, + lib: [ + "es5", + "es2015.promise" + ] + } + }) + }; + const host = createServerHost([libES5, libES2015Promise, app, config1], { executingFilePath: "/compiler/tsc.js" }); + const projectService = createProjectService(host); + projectService.openClientFile(app.path); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [libES5.path, app.path, config1.path]); + host.reloadFS([libES5, libES2015Promise, app, config2]); + host.checkTimeoutQueueLengthAndRun(2); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [libES5.path, libES2015Promise.path, app.path, config2.path]); + }); + it("should handle non-existing directories in config file", () => { + const f = { + path: "/a/src/app.ts", + content: "let x = 1;" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: {}, + include: [ + "src/**/*", + "notexistingfolder/*" + ] + }) + }; + const host = createServerHost([f, config]); + const projectService = createProjectService(host); + projectService.openClientFile(f.path); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + const project = projectService.configuredProjects.get(config.path)!; + assert.isTrue(project.hasOpenRef()); // f + projectService.closeClientFile(f.path); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(config.path), project); + assert.isFalse(project.hasOpenRef()); // No files + assert.isFalse(project.isClosed()); + projectService.openClientFile(f.path); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(config.path), project); + assert.isTrue(project.hasOpenRef()); // f + assert.isFalse(project.isClosed()); + }); + it("handles loads existing configured projects of external projects when lazyConfiguredProjectsFromExternalProject is disabled", () => { + const f1 = { + path: "/a/b/app.ts", + content: "let x = 1" + }; + const config = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({}) + }; + const projectFileName = "/a/b/project.csproj"; + const host = createServerHost([f1, config]); + const service = createProjectService(host); + service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject: true } }); + service.openExternalProject(({ + projectFileName, + rootFiles: toExternalFiles([f1.path, config.path]), + options: {} + })); + service.checkNumberOfProjects({ configuredProjects: 1 }); + const project = service.configuredProjects.get(config.path)!; + assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.Full); // External project referenced configured project pending to be reloaded + checkProjectActualFiles(project, emptyArray); + service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject: false } }); + assert.equal(project.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project loaded + checkProjectActualFiles(project, [config.path, f1.path]); + service.closeExternalProject(projectFileName); + service.checkNumberOfProjects({}); + service.openExternalProject(({ + projectFileName, + rootFiles: toExternalFiles([f1.path, config.path]), + options: {} + })); + service.checkNumberOfProjects({ configuredProjects: 1 }); + const project2 = service.configuredProjects.get(config.path)!; + assert.equal(project2.pendingReload, ConfigFileProgramReloadLevel.None); // External project referenced configured project loaded + checkProjectActualFiles(project2, [config.path, f1.path]); + }); + it("handles creation of external project with jsconfig before jsconfig creation watcher is invoked", () => { + const projectFileName = `${projectRoot}/WebApplication36.csproj`; + const tsconfig: File = { + path: `${projectRoot}/tsconfig.json`, + content: "{}" + }; + const files = [libFile, tsconfig]; + const host = createServerHost(files); + const service = createProjectService(host); + // Create external project + service.openExternalProjects([{ projectFileName, rootFiles: [{ fileName: tsconfig.path }], options: { allowJs: false } }]); - checkNumberOfProjects(service, { configuredProjects: 1 }); - const configProject = service.configuredProjects.get(tsconfig.path.toLowerCase())!; - checkProjectActualFiles(configProject, [tsconfig.path]); - - // write js file, open external project and open it for edit - const jsFilePath = `${tscWatch.projectRoot}/javascript.js`; - host.writeFile(jsFilePath, ""); - service.openExternalProjects([{ + checkNumberOfProjects(service, { configuredProjects: 1 }); + const configProject = service.configuredProjects.get(tsconfig.path.toLowerCase())!; + checkProjectActualFiles(configProject, [tsconfig.path]); + // write js file, open external project and open it for edit + const jsFilePath = `${projectRoot}/javascript.js`; + host.writeFile(jsFilePath, ""); + service.openExternalProjects([{ projectFileName, rootFiles: [{ fileName: tsconfig.path }, { fileName: jsFilePath }], options: { allowJs: false } }]); - service.applyChangesInOpenFiles(singleIterator({ fileName: jsFilePath, scriptKind: ScriptKind.JS, content: "" })); - checkNumberOfProjects(service, { configuredProjects: 1, inferredProjects: 1 }); - checkProjectActualFiles(configProject, [tsconfig.path]); - const inferredProject = service.inferredProjects[0]; - checkProjectActualFiles(inferredProject, [libFile.path, jsFilePath]); - - // write jsconfig file - const jsConfig: File = { - path: `${tscWatch.projectRoot}/jsconfig.json`, - content: "{}" - }; - // Dont invoke file creation watchers as the repro suggests - host.ensureFileOrFolder(jsConfig, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ true); - - // Open external project - service.openExternalProjects([{ + service.applyChangesInOpenFiles(singleIterator({ fileName: jsFilePath, scriptKind: ScriptKind.JS, content: "" })); + checkNumberOfProjects(service, { configuredProjects: 1, inferredProjects: 1 }); + checkProjectActualFiles(configProject, [tsconfig.path]); + const inferredProject = service.inferredProjects[0]; + checkProjectActualFiles(inferredProject, [libFile.path, jsFilePath]); + // write jsconfig file + const jsConfig: File = { + path: `${projectRoot}/jsconfig.json`, + content: "{}" + }; + // Dont invoke file creation watchers as the repro suggests + host.ensureFileOrFolder(jsConfig, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ true); + // Open external project + service.openExternalProjects([{ projectFileName, rootFiles: [{ fileName: jsConfig.path }, { fileName: tsconfig.path }, { fileName: jsFilePath }], options: { allowJs: false } }]); - checkNumberOfProjects(service, { configuredProjects: 2, inferredProjects: 1 }); - checkProjectActualFiles(configProject, [tsconfig.path]); - assert.isTrue(inferredProject.isOrphan()); - const jsConfigProject = service.configuredProjects.get(jsConfig.path.toLowerCase())!; - checkProjectActualFiles(jsConfigProject, [jsConfig.path, jsFilePath, libFile.path]); - }); + checkNumberOfProjects(service, { configuredProjects: 2, inferredProjects: 1 }); + checkProjectActualFiles(configProject, [tsconfig.path]); + assert.isTrue(inferredProject.isOrphan()); + const jsConfigProject = service.configuredProjects.get(jsConfig.path.toLowerCase())!; + checkProjectActualFiles(jsConfigProject, [jsConfig.path, jsFilePath, libFile.path]); }); -} +}); diff --git a/src/testRunner/unittests/tsserver/forceConsistentCasingInFileNames.ts b/src/testRunner/unittests/tsserver/forceConsistentCasingInFileNames.ts index 0f669cd9599aa..7cc89657567d2 100644 --- a/src/testRunner/unittests/tsserver/forceConsistentCasingInFileNames.ts +++ b/src/testRunner/unittests/tsserver/forceConsistentCasingInFileNames.ts @@ -1,176 +1,153 @@ -namespace ts.projectSystem { - describe("unittests:: tsserver:: forceConsistentCasingInFileNames", () => { - it("works when extends is specified with a case insensitive file system", () => { - const rootPath = "/Users/username/dev/project"; - const file1: File = { - path: `${rootPath}/index.ts`, - content: 'import {x} from "file2";', - }; - const file2: File = { - path: `${rootPath}/file2.js`, - content: "", - }; - const file2Dts: File = { - path: `${rootPath}/types/file2/index.d.ts`, - content: "export declare const x: string;", - }; - const tsconfigAll: File = { - path: `${rootPath}/tsconfig.all.json`, - content: JSON.stringify({ - compilerOptions: { - baseUrl: ".", - paths: { file2: ["./file2.js"] }, - typeRoots: ["./types"], - forceConsistentCasingInFileNames: true, - }, - }), - }; - const tsconfig: File = { - path: `${rootPath}/tsconfig.json`, - content: JSON.stringify({ extends: "./tsconfig.all.json" }), - }; - - const host = createServerHost([file1, file2, file2Dts, libFile, tsconfig, tsconfigAll], { useCaseSensitiveFileNames: false }); - const session = createSession(host); - - openFilesForSession([file1], session); - const projectService = session.getProjectService(); - - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - - const diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); - assert.deepEqual(diagnostics, []); +import { File, createServerHost, libFile, createSession, openFilesForSession, checkNumberOfProjects, configuredProjectAt, checkProjectActualFiles, verifyGetErrRequest, closeFilesForSession, protocol, protocolTextSpanFromSubstring, createDiagnostic } from "../../ts.projectSystem"; +import { projectRoot } from "../../ts.tscWatch"; +import { Diagnostics } from "../../ts"; +describe("unittests:: tsserver:: forceConsistentCasingInFileNames", () => { + it("works when extends is specified with a case insensitive file system", () => { + const rootPath = "/Users/username/dev/project"; + const file1: File = { + path: `${rootPath}/index.ts`, + content: 'import {x} from "file2";', + }; + const file2: File = { + path: `${rootPath}/file2.js`, + content: "", + }; + const file2Dts: File = { + path: `${rootPath}/types/file2/index.d.ts`, + content: "export declare const x: string;", + }; + const tsconfigAll: File = { + path: `${rootPath}/tsconfig.all.json`, + content: JSON.stringify({ + compilerOptions: { + baseUrl: ".", + paths: { file2: ["./file2.js"] }, + typeRoots: ["./types"], + forceConsistentCasingInFileNames: true, + }, + }), + }; + const tsconfig: File = { + path: `${rootPath}/tsconfig.json`, + content: JSON.stringify({ extends: "./tsconfig.all.json" }), + }; + const host = createServerHost([file1, file2, file2Dts, libFile, tsconfig, tsconfigAll], { useCaseSensitiveFileNames: false }); + const session = createSession(host); + openFilesForSession([file1], session); + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); + assert.deepEqual(diagnostics, []); + }); + it("works when renaming file with different casing", () => { + const loggerFile: File = { + path: `${projectRoot}/Logger.ts`, + content: `export class logger { }` + }; + const anotherFile: File = { + path: `${projectRoot}/another.ts`, + content: `import { logger } from "./Logger"; new logger();` + }; + const tsconfig: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { forceConsistentCasingInFileNames: true } + }) + }; + const host = createServerHost([loggerFile, anotherFile, tsconfig, libFile, tsconfig]); + const session = createSession(host, { canUseEvents: true }); + openFilesForSession([{ file: loggerFile, projectRootPath: projectRoot }], session); + const service = session.getProjectService(); + checkNumberOfProjects(service, { configuredProjects: 1 }); + const project = service.configuredProjects.get(tsconfig.path)!; + checkProjectActualFiles(project, [loggerFile.path, anotherFile.path, libFile.path, tsconfig.path]); + verifyGetErrRequest({ + host, + session, + expected: [ + { file: loggerFile.path, syntax: [], semantic: [], suggestion: [] } + ] }); - - it("works when renaming file with different casing", () => { - const loggerFile: File = { - path: `${tscWatch.projectRoot}/Logger.ts`, - content: `export class logger { }` - }; - const anotherFile: File = { - path: `${tscWatch.projectRoot}/another.ts`, - content: `import { logger } from "./Logger"; new logger();` - }; - const tsconfig: File = { - path: `${tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { forceConsistentCasingInFileNames: true } - }) - }; - - const host = createServerHost([loggerFile, anotherFile, tsconfig, libFile, tsconfig]); - const session = createSession(host, { canUseEvents: true }); - openFilesForSession([{ file: loggerFile, projectRootPath: tscWatch.projectRoot }], session); - const service = session.getProjectService(); - checkNumberOfProjects(service, { configuredProjects: 1 }); - const project = service.configuredProjects.get(tsconfig.path)!; - checkProjectActualFiles(project, [loggerFile.path, anotherFile.path, libFile.path, tsconfig.path]); - verifyGetErrRequest({ - host, - session, - expected: [ - { file: loggerFile.path, syntax: [], semantic: [], suggestion: [] } - ] - }); - - const newLoggerPath = loggerFile.path.toLowerCase(); - host.renameFile(loggerFile.path, newLoggerPath); - closeFilesForSession([loggerFile], session); - openFilesForSession([{ file: newLoggerPath, content: loggerFile.content, projectRootPath: tscWatch.projectRoot }], session); - - // Apply edits for rename - openFilesForSession([{ file: anotherFile, projectRootPath: tscWatch.projectRoot }], session); - session.executeCommandSeq({ - command: protocol.CommandTypes.UpdateOpen, - arguments: { - changedFiles: [{ + const newLoggerPath = loggerFile.path.toLowerCase(); + host.renameFile(loggerFile.path, newLoggerPath); + closeFilesForSession([loggerFile], session); + openFilesForSession([{ file: newLoggerPath, content: loggerFile.content, projectRootPath: projectRoot }], session); + // Apply edits for rename + openFilesForSession([{ file: anotherFile, projectRootPath: projectRoot }], session); + session.executeCommandSeq({ + command: protocol.CommandTypes.UpdateOpen, + arguments: { + changedFiles: [{ fileName: anotherFile.path, textChanges: [{ - newText: "./logger", - ...protocolTextSpanFromSubstring( - anotherFile.content, - "./Logger" - ) - }] + newText: "./logger", + ...protocolTextSpanFromSubstring(anotherFile.content, "./Logger") + }] }] - } - }); - - // Check errors in both files - verifyGetErrRequest({ - host, - session, - expected: [ - { file: newLoggerPath, syntax: [], semantic: [], suggestion: [] }, - { file: anotherFile.path, syntax: [], semantic: [], suggestion: [] } - ] - }); + } + }); + // Check errors in both files + verifyGetErrRequest({ + host, + session, + expected: [ + { file: newLoggerPath, syntax: [], semantic: [], suggestion: [] }, + { file: anotherFile.path, syntax: [], semantic: [], suggestion: [] } + ] }); - - it("when changing module name with different casing", () => { - const loggerFile: File = { - path: `${tscWatch.projectRoot}/Logger.ts`, - content: `export class logger { }` - }; - const anotherFile: File = { - path: `${tscWatch.projectRoot}/another.ts`, - content: `import { logger } from "./Logger"; new logger();` - }; - const tsconfig: File = { - path: `${tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { forceConsistentCasingInFileNames: true } - }) - }; - - const host = createServerHost([loggerFile, anotherFile, tsconfig, libFile, tsconfig]); - const session = createSession(host, { canUseEvents: true }); - openFilesForSession([{ file: anotherFile, projectRootPath: tscWatch.projectRoot }], session); - const service = session.getProjectService(); - checkNumberOfProjects(service, { configuredProjects: 1 }); - const project = service.configuredProjects.get(tsconfig.path)!; - checkProjectActualFiles(project, [loggerFile.path, anotherFile.path, libFile.path, tsconfig.path]); - verifyGetErrRequest({ - host, - session, - expected: [ - { file: anotherFile.path, syntax: [], semantic: [], suggestion: [] } - ] - }); - - session.executeCommandSeq({ - command: protocol.CommandTypes.UpdateOpen, - arguments: { - changedFiles: [{ + }); + it("when changing module name with different casing", () => { + const loggerFile: File = { + path: `${projectRoot}/Logger.ts`, + content: `export class logger { }` + }; + const anotherFile: File = { + path: `${projectRoot}/another.ts`, + content: `import { logger } from "./Logger"; new logger();` + }; + const tsconfig: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { forceConsistentCasingInFileNames: true } + }) + }; + const host = createServerHost([loggerFile, anotherFile, tsconfig, libFile, tsconfig]); + const session = createSession(host, { canUseEvents: true }); + openFilesForSession([{ file: anotherFile, projectRootPath: projectRoot }], session); + const service = session.getProjectService(); + checkNumberOfProjects(service, { configuredProjects: 1 }); + const project = service.configuredProjects.get(tsconfig.path)!; + checkProjectActualFiles(project, [loggerFile.path, anotherFile.path, libFile.path, tsconfig.path]); + verifyGetErrRequest({ + host, + session, + expected: [ + { file: anotherFile.path, syntax: [], semantic: [], suggestion: [] } + ] + }); + session.executeCommandSeq({ + command: protocol.CommandTypes.UpdateOpen, + arguments: { + changedFiles: [{ fileName: anotherFile.path, textChanges: [{ - newText: "./logger", - ...protocolTextSpanFromSubstring( - anotherFile.content, - "./Logger" - ) - }] + newText: "./logger", + ...protocolTextSpanFromSubstring(anotherFile.content, "./Logger") + }] }] - } - }); - - const location = protocolTextSpanFromSubstring(anotherFile.content, `"./Logger"`); - // Check errors in both files - verifyGetErrRequest({ - host, - session, - expected: [{ + } + }); + const location = protocolTextSpanFromSubstring(anotherFile.content, `"./Logger"`); + // Check errors in both files + verifyGetErrRequest({ + host, + session, + expected: [{ file: anotherFile.path, syntax: [], - semantic: [createDiagnostic( - location.start, - location.end, - Diagnostics.File_name_0_differs_from_already_included_file_name_1_only_in_casing, - [loggerFile.path.toLowerCase(), loggerFile.path] - )], + semantic: [createDiagnostic(location.start, location.end, Diagnostics.File_name_0_differs_from_already_included_file_name_1_only_in_casing, [loggerFile.path.toLowerCase(), loggerFile.path])], suggestion: [] }] - }); }); }); -} +}); diff --git a/src/testRunner/unittests/tsserver/formatSettings.ts b/src/testRunner/unittests/tsserver/formatSettings.ts index 2d09ed8a8a0eb..a810fcff7d755 100644 --- a/src/testRunner/unittests/tsserver/formatSettings.ts +++ b/src/testRunner/unittests/tsserver/formatSettings.ts @@ -1,39 +1,32 @@ -namespace ts.projectSystem { - describe("unittests:: tsserver:: format settings", () => { - it("can be set globally", () => { - const f1 = { - path: "/a/b/app.ts", - content: "let x;" - }; - const host = createServerHost([f1]); - const projectService = createProjectService(host); - projectService.openClientFile(f1.path); - - const defaultSettings = projectService.getFormatCodeOptions(f1.path as server.NormalizedPath); - - // set global settings - const newGlobalSettings1 = { ...defaultSettings, placeOpenBraceOnNewLineForControlBlocks: !defaultSettings.placeOpenBraceOnNewLineForControlBlocks }; - projectService.setHostConfiguration({ formatOptions: newGlobalSettings1 }); - - // get format options for file - should be equal to new global settings - const s1 = projectService.getFormatCodeOptions(server.toNormalizedPath(f1.path)); - assert.deepEqual(s1, newGlobalSettings1, "file settings should be the same with global settings"); - - // set per file format options - const newPerFileSettings = { ...defaultSettings, insertSpaceAfterCommaDelimiter: !defaultSettings.insertSpaceAfterCommaDelimiter }; - projectService.setHostConfiguration({ formatOptions: newPerFileSettings, file: f1.path }); - - // get format options for file - should be equal to new per-file settings - const s2 = projectService.getFormatCodeOptions(server.toNormalizedPath(f1.path)); - assert.deepEqual(s2, newPerFileSettings, "file settings should be the same with per-file settings"); - - // set new global settings - they should not affect ones that were set per-file - const newGlobalSettings2 = { ...defaultSettings, insertSpaceAfterSemicolonInForStatements: !defaultSettings.insertSpaceAfterSemicolonInForStatements }; - projectService.setHostConfiguration({ formatOptions: newGlobalSettings2 }); - - // get format options for file - should be equal to new per-file settings - const s3 = projectService.getFormatCodeOptions(server.toNormalizedPath(f1.path)); - assert.deepEqual(s3, newPerFileSettings, "file settings should still be the same with per-file settings"); - }); +import { createServerHost, createProjectService } from "../../ts.projectSystem"; +import { NormalizedPath, toNormalizedPath } from "../../ts.server"; +describe("unittests:: tsserver:: format settings", () => { + it("can be set globally", () => { + const f1 = { + path: "/a/b/app.ts", + content: "let x;" + }; + const host = createServerHost([f1]); + const projectService = createProjectService(host); + projectService.openClientFile(f1.path); + const defaultSettings = projectService.getFormatCodeOptions((f1.path as NormalizedPath)); + // set global settings + const newGlobalSettings1 = { ...defaultSettings, placeOpenBraceOnNewLineForControlBlocks: !defaultSettings.placeOpenBraceOnNewLineForControlBlocks }; + projectService.setHostConfiguration({ formatOptions: newGlobalSettings1 }); + // get format options for file - should be equal to new global settings + const s1 = projectService.getFormatCodeOptions(toNormalizedPath(f1.path)); + assert.deepEqual(s1, newGlobalSettings1, "file settings should be the same with global settings"); + // set per file format options + const newPerFileSettings = { ...defaultSettings, insertSpaceAfterCommaDelimiter: !defaultSettings.insertSpaceAfterCommaDelimiter }; + projectService.setHostConfiguration({ formatOptions: newPerFileSettings, file: f1.path }); + // get format options for file - should be equal to new per-file settings + const s2 = projectService.getFormatCodeOptions(toNormalizedPath(f1.path)); + assert.deepEqual(s2, newPerFileSettings, "file settings should be the same with per-file settings"); + // set new global settings - they should not affect ones that were set per-file + const newGlobalSettings2 = { ...defaultSettings, insertSpaceAfterSemicolonInForStatements: !defaultSettings.insertSpaceAfterSemicolonInForStatements }; + projectService.setHostConfiguration({ formatOptions: newGlobalSettings2 }); + // get format options for file - should be equal to new per-file settings + const s3 = projectService.getFormatCodeOptions(toNormalizedPath(f1.path)); + assert.deepEqual(s3, newPerFileSettings, "file settings should still be the same with per-file settings"); }); -} +}); diff --git a/src/testRunner/unittests/tsserver/getApplicableRefactors.ts b/src/testRunner/unittests/tsserver/getApplicableRefactors.ts index bec4a65456051..373f8bfd851ec 100644 --- a/src/testRunner/unittests/tsserver/getApplicableRefactors.ts +++ b/src/testRunner/unittests/tsserver/getApplicableRefactors.ts @@ -1,12 +1,10 @@ -namespace ts.projectSystem { - describe("unittests:: tsserver:: getApplicableRefactors", () => { - it("works when taking position", () => { - const aTs: File = { path: "/a.ts", content: "" }; - const session = createSession(createServerHost([aTs])); - openFilesForSession([aTs], session); - const response = executeSessionRequest( - session, protocol.CommandTypes.GetApplicableRefactors, { file: aTs.path, line: 1, offset: 1 }); - assert.deepEqual(response, []); - }); +import { File, createSession, createServerHost, openFilesForSession, executeSessionRequest, protocol } from "../../ts.projectSystem"; +describe("unittests:: tsserver:: getApplicableRefactors", () => { + it("works when taking position", () => { + const aTs: File = { path: "/a.ts", content: "" }; + const session = createSession(createServerHost([aTs])); + openFilesForSession([aTs], session); + const response = executeSessionRequest(session, protocol.CommandTypes.GetApplicableRefactors, { file: aTs.path, line: 1, offset: 1 }); + assert.deepEqual(response, []); }); -} +}); diff --git a/src/testRunner/unittests/tsserver/getEditsForFileRename.ts b/src/testRunner/unittests/tsserver/getEditsForFileRename.ts index 5dcd3e96fdb1d..ea89db3196f84 100644 --- a/src/testRunner/unittests/tsserver/getEditsForFileRename.ts +++ b/src/testRunner/unittests/tsserver/getEditsForFileRename.ts @@ -1,105 +1,96 @@ -namespace ts.projectSystem { - describe("unittests:: tsserver:: getEditsForFileRename", () => { - it("works for host implementing 'resolveModuleNames' and 'getResolvedModuleWithFailedLookupLocationsFromCache'", () => { - const userTs: File = { - path: "/user.ts", - content: 'import { x } from "./old";', - }; - const newTs: File = { - path: "/new.ts", - content: "export const x = 0;", - }; - const tsconfig: File = { - path: "/tsconfig.json", - content: "{}", - }; - - const host = createServerHost([userTs, newTs, tsconfig]); - const projectService = createProjectService(host); - projectService.openClientFile(userTs.path); - const project = projectService.configuredProjects.get(tsconfig.path)!; - - Debug.assert(!!project.resolveModuleNames); - - const edits = project.getLanguageService().getEditsForFileRename("/old.ts", "/new.ts", testFormatSettings, emptyOptions); - assert.deepEqual(edits, [{ +import { File, createServerHost, createProjectService, textSpanFromSubstring, createSession, openFilesForSession, executeSessionRequest, protocol, CommandNames, protocolTextSpanFromSubstring } from "../../ts.projectSystem"; +import { Debug, testFormatSettings, emptyOptions, FileTextChanges } from "../../ts"; +describe("unittests:: tsserver:: getEditsForFileRename", () => { + it("works for host implementing 'resolveModuleNames' and 'getResolvedModuleWithFailedLookupLocationsFromCache'", () => { + const userTs: File = { + path: "/user.ts", + content: 'import { x } from "./old";', + }; + const newTs: File = { + path: "/new.ts", + content: "export const x = 0;", + }; + const tsconfig: File = { + path: "/tsconfig.json", + content: "{}", + }; + const host = createServerHost([userTs, newTs, tsconfig]); + const projectService = createProjectService(host); + projectService.openClientFile(userTs.path); + const project = projectService.configuredProjects.get(tsconfig.path)!; + Debug.assert(!!project.resolveModuleNames); + const edits = project.getLanguageService().getEditsForFileRename("/old.ts", "/new.ts", testFormatSettings, emptyOptions); + assert.deepEqual(edits, [{ fileName: "/user.ts", textChanges: [{ - span: textSpanFromSubstring(userTs.content, "./old"), - newText: "./new", - }], + span: textSpanFromSubstring(userTs.content, "./old"), + newText: "./new", + }], }]); + }); + it("works with multiple projects", () => { + const aUserTs: File = { + path: "/a/user.ts", + content: 'import { x } from "./old";', + }; + const aOldTs: File = { + path: "/a/old.ts", + content: "export const x = 0;", + }; + const aTsconfig: File = { + path: "/a/tsconfig.json", + content: JSON.stringify({ files: ["./old.ts", "./user.ts"] }), + }; + const bUserTs: File = { + path: "/b/user.ts", + content: 'import { x } from "../a/old";', + }; + const bTsconfig: File = { + path: "/b/tsconfig.json", + content: "{}", + }; + const host = createServerHost([aUserTs, aOldTs, aTsconfig, bUserTs, bTsconfig]); + const session = createSession(host); + openFilesForSession([aUserTs, bUserTs], session); + const response = executeSessionRequest(session, CommandNames.GetEditsForFileRename, { + oldFilePath: aOldTs.path, + newFilePath: "/a/new.ts", }); - - it("works with multiple projects", () => { - const aUserTs: File = { - path: "/a/user.ts", - content: 'import { x } from "./old";', - }; - const aOldTs: File = { - path: "/a/old.ts", - content: "export const x = 0;", - }; - const aTsconfig: File = { - path: "/a/tsconfig.json", - content: JSON.stringify({ files: ["./old.ts", "./user.ts"] }), - }; - const bUserTs: File = { - path: "/b/user.ts", - content: 'import { x } from "../a/old";', - }; - const bTsconfig: File = { - path: "/b/tsconfig.json", - content: "{}", - }; - - const host = createServerHost([aUserTs, aOldTs, aTsconfig, bUserTs, bTsconfig]); - const session = createSession(host); - openFilesForSession([aUserTs, bUserTs], session); - - const response = executeSessionRequest(session, CommandNames.GetEditsForFileRename, { - oldFilePath: aOldTs.path, - newFilePath: "/a/new.ts", - }); - assert.deepEqual(response, [ - { - fileName: aTsconfig.path, - textChanges: [{ ...protocolTextSpanFromSubstring(aTsconfig.content, "./old.ts"), newText: "new.ts" }], - }, - { - fileName: aUserTs.path, - textChanges: [{ ...protocolTextSpanFromSubstring(aUserTs.content, "./old"), newText: "./new" }], - }, - { - fileName: bUserTs.path, - textChanges: [{ ...protocolTextSpanFromSubstring(bUserTs.content, "../a/old"), newText: "../a/new" }], - }, - ]); - }); - - it("works with file moved to inferred project", () => { - const aTs: File = { path: "/a.ts", content: 'import {} from "./b";' }; - const cTs: File = { path: "/c.ts", content: "export {};" }; - const tsconfig: File = { path: "/tsconfig.json", content: JSON.stringify({ files: ["./a.ts", "./b.ts"] }) }; - - const host = createServerHost([aTs, cTs, tsconfig]); - const session = createSession(host); - openFilesForSession([aTs, cTs], session); - - const response = executeSessionRequest(session, CommandNames.GetEditsForFileRename, { - oldFilePath: "/b.ts", - newFilePath: cTs.path, - }); - assert.deepEqual(response, [ - { - fileName: "/tsconfig.json", - textChanges: [{ ...protocolTextSpanFromSubstring(tsconfig.content, "./b.ts"), newText: "c.ts" }], - }, - { - fileName: "/a.ts", - textChanges: [{ ...protocolTextSpanFromSubstring(aTs.content, "./b"), newText: "./c" }], - }, - ]); + assert.deepEqual(response, [ + { + fileName: aTsconfig.path, + textChanges: [{ ...protocolTextSpanFromSubstring(aTsconfig.content, "./old.ts"), newText: "new.ts" }], + }, + { + fileName: aUserTs.path, + textChanges: [{ ...protocolTextSpanFromSubstring(aUserTs.content, "./old"), newText: "./new" }], + }, + { + fileName: bUserTs.path, + textChanges: [{ ...protocolTextSpanFromSubstring(bUserTs.content, "../a/old"), newText: "../a/new" }], + }, + ]); + }); + it("works with file moved to inferred project", () => { + const aTs: File = { path: "/a.ts", content: 'import {} from "./b";' }; + const cTs: File = { path: "/c.ts", content: "export {};" }; + const tsconfig: File = { path: "/tsconfig.json", content: JSON.stringify({ files: ["./a.ts", "./b.ts"] }) }; + const host = createServerHost([aTs, cTs, tsconfig]); + const session = createSession(host); + openFilesForSession([aTs, cTs], session); + const response = executeSessionRequest(session, CommandNames.GetEditsForFileRename, { + oldFilePath: "/b.ts", + newFilePath: cTs.path, }); + assert.deepEqual(response, [ + { + fileName: "/tsconfig.json", + textChanges: [{ ...protocolTextSpanFromSubstring(tsconfig.content, "./b.ts"), newText: "c.ts" }], + }, + { + fileName: "/a.ts", + textChanges: [{ ...protocolTextSpanFromSubstring(aTs.content, "./b"), newText: "./c" }], + }, + ]); }); -} +}); diff --git a/src/testRunner/unittests/tsserver/getExportReferences.ts b/src/testRunner/unittests/tsserver/getExportReferences.ts index 45bd08e39b8de..316c48d3f71f2 100644 --- a/src/testRunner/unittests/tsserver/getExportReferences.ts +++ b/src/testRunner/unittests/tsserver/getExportReferences.ts @@ -1,185 +1,138 @@ -namespace ts.projectSystem { - describe("unittests:: tsserver:: getExportReferences", () => { - const exportVariable = "export const value = 0;"; - const exportArrayDestructured = "export const [valueA, valueB] = [0, 1];"; - const exportObjectDestructured = "export const { valueC, valueD: renamedD } = { valueC: 0, valueD: 1 };"; - const exportNestedObject = "export const { nest: [valueE, { valueF }] } = { nest: [0, { valueF: 1 }] };"; - - const mainTs: File = { - path: "/main.ts", - content: 'import { value, valueA, valueB, valueC, renamedD, valueE, valueF } from "./mod";', - }; - const modTs: File = { - path: "/mod.ts", - content: `${exportVariable} +import { File, createServerHost, createSession, openFilesForSession, protocol, makeReferenceItem, MakeReferenceItem, executeSessionRequest, protocolFileLocationFromSubstring, protocolLocationFromSubstring } from "../../ts.projectSystem"; +describe("unittests:: tsserver:: getExportReferences", () => { + const exportVariable = "export const value = 0;"; + const exportArrayDestructured = "export const [valueA, valueB] = [0, 1];"; + const exportObjectDestructured = "export const { valueC, valueD: renamedD } = { valueC: 0, valueD: 1 };"; + const exportNestedObject = "export const { nest: [valueE, { valueF }] } = { nest: [0, { valueF: 1 }] };"; + const mainTs: File = { + path: "/main.ts", + content: 'import { value, valueA, valueB, valueC, renamedD, valueE, valueF } from "./mod";', + }; + const modTs: File = { + path: "/mod.ts", + content: `${exportVariable} ${exportArrayDestructured} ${exportObjectDestructured} ${exportNestedObject} `, + }; + const tsconfig: File = { + path: "/tsconfig.json", + content: "{}", + }; + function makeSampleSession() { + const host = createServerHost([mainTs, modTs, tsconfig]); + const session = createSession(host); + openFilesForSession([mainTs, modTs], session); + return session; + } + const referenceMainTs = (mainTs: File, text: string): protocol.ReferencesResponseItem => makeReferenceItem({ + file: mainTs, + isDefinition: true, + lineText: mainTs.content, + contextText: mainTs.content, + text, + }); + const referenceModTs = (texts: { + text: string; + lineText: string; + contextText?: string; + }, override: Partial = {}): protocol.ReferencesResponseItem => makeReferenceItem({ + file: modTs, + isDefinition: true, + ...texts, + ...override, + }); + it("should get const variable declaration references", () => { + const session = makeSampleSession(); + const response = executeSessionRequest(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(modTs, "value")); + const expectResponse = { + refs: [ + referenceModTs({ text: "value", lineText: exportVariable, contextText: exportVariable }), + referenceMainTs(mainTs, "value"), + ], + symbolDisplayString: "const value: 0", + symbolName: "value", + symbolStartOffset: protocolLocationFromSubstring(modTs.content, "value").offset, + }; + assert.deepEqual(response, expectResponse); + }); + it("should get array destructuring declaration references", () => { + const session = makeSampleSession(); + const response = executeSessionRequest(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(modTs, "valueA")); + const expectResponse = { + refs: [ + referenceModTs({ + text: "valueA", + lineText: exportArrayDestructured, + contextText: exportArrayDestructured, + }), + referenceMainTs(mainTs, "valueA"), + ], + symbolDisplayString: "const valueA: number", + symbolName: "valueA", + symbolStartOffset: protocolLocationFromSubstring(modTs.content, "valueA").offset, }; - const tsconfig: File = { - path: "/tsconfig.json", - content: "{}", + assert.deepEqual(response, expectResponse); + }); + it("should get object destructuring declaration references", () => { + const session = makeSampleSession(); + const response = executeSessionRequest(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(modTs, "valueC")); + const expectResponse = { + refs: [ + referenceModTs({ + text: "valueC", + lineText: exportObjectDestructured, + contextText: exportObjectDestructured, + }), + referenceMainTs(mainTs, "valueC"), + referenceModTs({ text: "valueC", lineText: exportObjectDestructured, contextText: "valueC: 0" }, { options: { index: 1 } }), + ], + symbolDisplayString: "const valueC: number", + symbolName: "valueC", + symbolStartOffset: protocolLocationFromSubstring(modTs.content, "valueC").offset, + }; + assert.deepEqual(response, expectResponse); + }); + it("should get object declaration references that renames destructured property", () => { + const session = makeSampleSession(); + const response = executeSessionRequest(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(modTs, "renamedD")); + const expectResponse = { + refs: [ + referenceModTs({ + text: "renamedD", + lineText: exportObjectDestructured, + contextText: exportObjectDestructured, + }), + referenceMainTs(mainTs, "renamedD"), + ], + symbolDisplayString: "const renamedD: number", + symbolName: "renamedD", + symbolStartOffset: protocolLocationFromSubstring(modTs.content, "renamedD").offset, + }; + assert.deepEqual(response, expectResponse); + }); + it("should get nested object declaration references", () => { + const session = makeSampleSession(); + const response = executeSessionRequest(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(modTs, "valueF")); + const expectResponse = { + refs: [ + referenceModTs({ + text: "valueF", + lineText: exportNestedObject, + contextText: exportNestedObject, + }), + referenceMainTs(mainTs, "valueF"), + referenceModTs({ + text: "valueF", + lineText: exportNestedObject, + contextText: "valueF: 1", + }, { options: { index: 1 } }), + ], + symbolDisplayString: "const valueF: number", + symbolName: "valueF", + symbolStartOffset: protocolLocationFromSubstring(modTs.content, "valueF").offset, }; - - function makeSampleSession() { - const host = createServerHost([mainTs, modTs, tsconfig]); - const session = createSession(host); - openFilesForSession([mainTs, modTs], session); - return session; - } - - const referenceMainTs = (mainTs: File, text: string): protocol.ReferencesResponseItem => - makeReferenceItem({ - file: mainTs, - isDefinition: true, - lineText: mainTs.content, - contextText: mainTs.content, - text, - }); - - const referenceModTs = ( - texts: { text: string; lineText: string; contextText?: string }, - override: Partial = {}, - ): protocol.ReferencesResponseItem => - makeReferenceItem({ - file: modTs, - isDefinition: true, - ...texts, - ...override, - }); - - it("should get const variable declaration references", () => { - const session = makeSampleSession(); - - const response = executeSessionRequest( - session, - protocol.CommandTypes.References, - protocolFileLocationFromSubstring(modTs, "value"), - ); - - const expectResponse = { - refs: [ - referenceModTs({ text: "value", lineText: exportVariable, contextText: exportVariable }), - referenceMainTs(mainTs, "value"), - ], - symbolDisplayString: "const value: 0", - symbolName: "value", - symbolStartOffset: protocolLocationFromSubstring(modTs.content, "value").offset, - }; - - assert.deepEqual(response, expectResponse); - }); - - it("should get array destructuring declaration references", () => { - const session = makeSampleSession(); - const response = executeSessionRequest( - session, - protocol.CommandTypes.References, - protocolFileLocationFromSubstring(modTs, "valueA"), - ); - - const expectResponse = { - refs: [ - referenceModTs({ - text: "valueA", - lineText: exportArrayDestructured, - contextText: exportArrayDestructured, - }), - referenceMainTs(mainTs, "valueA"), - ], - symbolDisplayString: "const valueA: number", - symbolName: "valueA", - symbolStartOffset: protocolLocationFromSubstring(modTs.content, "valueA").offset, - }; - - assert.deepEqual(response, expectResponse); - }); - - it("should get object destructuring declaration references", () => { - const session = makeSampleSession(); - const response = executeSessionRequest( - session, - protocol.CommandTypes.References, - protocolFileLocationFromSubstring(modTs, "valueC"), - ); - const expectResponse = { - refs: [ - referenceModTs({ - text: "valueC", - lineText: exportObjectDestructured, - contextText: exportObjectDestructured, - }), - referenceMainTs(mainTs, "valueC"), - referenceModTs( - { text: "valueC", lineText: exportObjectDestructured, contextText: "valueC: 0" }, - { options: { index: 1 } }, - ), - ], - symbolDisplayString: "const valueC: number", - symbolName: "valueC", - symbolStartOffset: protocolLocationFromSubstring(modTs.content, "valueC").offset, - }; - - assert.deepEqual(response, expectResponse); - }); - - it("should get object declaration references that renames destructured property", () => { - const session = makeSampleSession(); - const response = executeSessionRequest( - session, - protocol.CommandTypes.References, - protocolFileLocationFromSubstring(modTs, "renamedD"), - ); - - const expectResponse = { - refs: [ - referenceModTs({ - text: "renamedD", - lineText: exportObjectDestructured, - contextText: exportObjectDestructured, - }), - referenceMainTs(mainTs, "renamedD"), - ], - symbolDisplayString: "const renamedD: number", - symbolName: "renamedD", - symbolStartOffset: protocolLocationFromSubstring(modTs.content, "renamedD").offset, - }; - - assert.deepEqual(response, expectResponse); - }); - - it("should get nested object declaration references", () => { - const session = makeSampleSession(); - const response = executeSessionRequest( - session, - protocol.CommandTypes.References, - protocolFileLocationFromSubstring(modTs, "valueF"), - ); - - const expectResponse = { - refs: [ - referenceModTs({ - text: "valueF", - lineText: exportNestedObject, - contextText: exportNestedObject, - }), - referenceMainTs(mainTs, "valueF"), - referenceModTs( - { - text: "valueF", - lineText: exportNestedObject, - contextText: "valueF: 1", - }, - { options: { index: 1 } }, - ), - ], - symbolDisplayString: "const valueF: number", - symbolName: "valueF", - symbolStartOffset: protocolLocationFromSubstring(modTs.content, "valueF").offset, - }; - - assert.deepEqual(response, expectResponse); - }); + assert.deepEqual(response, expectResponse); }); -} +}); diff --git a/src/testRunner/unittests/tsserver/helpers.ts b/src/testRunner/unittests/tsserver/helpers.ts index ce776323879a4..f15d340994126 100644 --- a/src/testRunner/unittests/tsserver/helpers.ts +++ b/src/testRunner/unittests/tsserver/helpers.ts @@ -1,34 +1,30 @@ -namespace ts.projectSystem { - export import TI = server.typingsInstaller; - export import protocol = server.protocol; - export import CommandNames = server.CommandNames; - - export import TestServerHost = TestFSWithWatch.TestServerHost; - export type File = TestFSWithWatch.File; - export type SymLink = TestFSWithWatch.SymLink; - export type Folder = TestFSWithWatch.Folder; - export import createServerHost = TestFSWithWatch.createServerHost; - export import checkArray = TestFSWithWatch.checkArray; - export import libFile = TestFSWithWatch.libFile; - export import checkWatchedFiles = TestFSWithWatch.checkWatchedFiles; - export import checkWatchedFilesDetailed = TestFSWithWatch.checkWatchedFilesDetailed; - export import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories; - export import checkWatchedDirectoriesDetailed = TestFSWithWatch.checkWatchedDirectoriesDetailed; - - export import commonFile1 = tscWatch.commonFile1; - export import commonFile2 = tscWatch.commonFile2; - - const outputEventRegex = /Content\-Length: [\d]+\r\n\r\n/; - export function mapOutputToJson(s: string) { - return convertToObject( - parseJsonText("json.json", s.replace(outputEventRegex, "")), - [] - ); - } - - export const customTypesMap = { - path: "/typesMap.json", - content: `{ +import { TestFSWithWatch, convertToObject, parseJsonText, Path, noop, returnFalse, returnUndefined, Debug, returnTrue, createMap, MapLike, notImplemented, TypeAcquisition, SortedReadonlyArray, isString, map, DiagnosticCategory, DiagnosticRelatedInformation, filterMutate, sys, version, server, clear, mapDefined, isArray, HostCancellationToken, normalizePath, forEachAncestorDirectory, combinePaths, directorySeparator, arrayFrom, computeLineStarts, computeLineAndCharacterOfPosition, textSpanEnd, TextSpan, createTextSpan, DiagnosticMessage, formatStringFromArgs, flattenDiagnosticMessageText, diagnosticCategoryName } from "../../ts"; +import { Logger, ITypingsInstaller, ProjectService, ServerHost, SetTypings, InvalidateCachedTypings, Project, createInstallTypingsRequest, FileStats, ProjectServiceEvent, ConfigFileDiagEvent, ProjectInfoTelemetryEventData, ProjectInfoTelemetryEvent, OpenFileInfo, OpenFileInfoTelemetryEventData, OpenFileInfoTelemetryEvent, Session, toEvent, SessionOptions, nullCancellationToken, ProjectServiceEventHandler, ProjectServiceOptions, ProjectKind, ServerCancellationToken, formatMessage } from "../../ts.server"; +import { byteLength } from "../../Utils"; +import * as ts from "../../ts"; +export import TI = ts.server.typingsInstaller; +export import protocol = ts.server.protocol; +export import CommandNames = ts.server.CommandNames; +export import TestServerHost = ts.TestFSWithWatch.TestServerHost; +export type File = TestFSWithWatch.File; +export type SymLink = TestFSWithWatch.SymLink; +export type Folder = TestFSWithWatch.Folder; +export import createServerHost = ts.TestFSWithWatch.createServerHost; +export import checkArray = ts.TestFSWithWatch.checkArray; +export import libFile = ts.TestFSWithWatch.libFile; +export import checkWatchedFiles = ts.TestFSWithWatch.checkWatchedFiles; +export import checkWatchedFilesDetailed = ts.TestFSWithWatch.checkWatchedFilesDetailed; +export import checkWatchedDirectories = ts.TestFSWithWatch.checkWatchedDirectories; +export import checkWatchedDirectoriesDetailed = ts.TestFSWithWatch.checkWatchedDirectoriesDetailed; +export import commonFile1 = ts.tscWatch.commonFile1; +export import commonFile2 = ts.tscWatch.commonFile2; +const outputEventRegex = /Content\-Length: [\d]+\r\n\r\n/; +export function mapOutputToJson(s: string) { + return convertToObject(parseJsonText("json.json", s.replace(outputEventRegex, "")), []); +} +export const customTypesMap = { + path: ("/typesMap.json"), + content: `{ "typesMap": { "jquery": { "match": "jquery(-(\\\\.?\\\\d+)+)?(\\\\.intellisense)?(\\\\.min)?\\\\.js$", @@ -48,700 +44,607 @@ namespace ts.projectSystem { "lodash": "lodash" } }` +}; +export interface PostExecAction { + readonly success: boolean; + readonly callback: TI.RequestCompletedAction; +} +export const nullLogger: Logger = { + close: noop, + hasLevel: returnFalse, + loggingEnabled: returnFalse, + perftrc: noop, + info: noop, + msg: noop, + startGroup: noop, + endGroup: noop, + getLogFileName: returnUndefined, +}; +export function createHasErrorMessageLogger() { + let hasErrorMsg = false; + const { close, hasLevel, loggingEnabled, startGroup, endGroup, info, getLogFileName, perftrc } = nullLogger; + const logger: Logger = { + close, hasLevel, loggingEnabled, startGroup, endGroup, info, getLogFileName, perftrc, + msg: (s, type) => { + Debug.fail(`Error: ${s}, type: ${type}`); + hasErrorMsg = true; + } }; - - export interface PostExecAction { - readonly success: boolean; - readonly callback: TI.RequestCompletedAction; - } - - export const nullLogger: server.Logger = { - close: noop, - hasLevel: returnFalse, - loggingEnabled: returnFalse, - perftrc: noop, - info: noop, - msg: noop, - startGroup: noop, - endGroup: noop, - getLogFileName: returnUndefined, + return { logger, hasErrorMsg: () => hasErrorMsg }; +} +export function createLoggerWritingToConsole(): Logger { + const { close, startGroup, endGroup, getLogFileName } = nullLogger; + return { + close, + hasLevel: returnTrue, + loggingEnabled: returnTrue, + perftrc: s => console.log(s), + info: s => console.log(s), + msg: (s, type) => console.log(`${type}:: ${s}`), + startGroup, + endGroup, + getLogFileName }; - - export function createHasErrorMessageLogger() { - let hasErrorMsg = false; - const { close, hasLevel, loggingEnabled, startGroup, endGroup, info, getLogFileName, perftrc } = nullLogger; - const logger: server.Logger = { - close, hasLevel, loggingEnabled, startGroup, endGroup, info, getLogFileName, perftrc, - msg: (s, type) => { - Debug.fail(`Error: ${s}, type: ${type}`); - hasErrorMsg = true; - } - }; - return { logger, hasErrorMsg: () => hasErrorMsg }; - } - - export function createLoggerWritingToConsole(): server.Logger { - const { close, startGroup, endGroup, getLogFileName } = nullLogger; - return { - close, - hasLevel: returnTrue, - loggingEnabled: returnTrue, - perftrc: s => console.log(s), - info: s => console.log(s), - msg: (s, type) => console.log(`${type}:: ${s}`), - startGroup, - endGroup, - getLogFileName +} +export class TestTypingsInstaller extends TI.TypingsInstaller implements ITypingsInstaller { + protected projectService!: ProjectService; + constructor(readonly globalTypingsCacheLocation: string, throttleLimit: number, installTypingHost: ServerHost, readonly typesRegistry = createMap>(), log?: TI.Log) { + super(installTypingHost, globalTypingsCacheLocation, TestFSWithWatch.safeList.path, customTypesMap.path, throttleLimit, log); + } + protected postExecActions: PostExecAction[] = []; + isKnownTypesPackageName = notImplemented; + installPackage = notImplemented; + inspectValue = notImplemented; + executePendingCommands() { + const actionsToRun = this.postExecActions; + this.postExecActions = []; + for (const action of actionsToRun) { + action.callback(action.success); + } + } + checkPendingCommands(expectedCount: number) { + assert.equal(this.postExecActions.length, expectedCount, `Expected ${expectedCount} post install actions`); + } + onProjectClosed = noop; + attach(projectService: ProjectService) { + this.projectService = projectService; + } + getInstallTypingHost() { + return this.installTypingHost; + } + installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void { + this.addPostExecAction("success", cb); + } + sendResponse(response: SetTypings | InvalidateCachedTypings) { + this.projectService.updateTypingsForProject(response); + } + enqueueInstallTypingsRequest(project: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray) { + const request = createInstallTypingsRequest(project, typeAcquisition, unresolvedImports, this.globalTypingsCacheLocation); + this.install(request); + } + addPostExecAction(stdout: string | string[], cb: TI.RequestCompletedAction) { + const out = isString(stdout) ? stdout : createNpmPackageJsonString(stdout); + const action: PostExecAction = { + success: !!out, + callback: cb }; + this.postExecActions.push(action); } - - export class TestTypingsInstaller extends TI.TypingsInstaller implements server.ITypingsInstaller { - protected projectService!: server.ProjectService; - constructor( - readonly globalTypingsCacheLocation: string, - throttleLimit: number, - installTypingHost: server.ServerHost, - readonly typesRegistry = createMap>(), - log?: TI.Log) { - super(installTypingHost, globalTypingsCacheLocation, TestFSWithWatch.safeList.path, customTypesMap.path, throttleLimit, log); - } - - protected postExecActions: PostExecAction[] = []; - - isKnownTypesPackageName = notImplemented; - installPackage = notImplemented; - inspectValue = notImplemented; - - executePendingCommands() { - const actionsToRun = this.postExecActions; - this.postExecActions = []; - for (const action of actionsToRun) { - action.callback(action.success); - } - } - - checkPendingCommands(expectedCount: number) { - assert.equal(this.postExecActions.length, expectedCount, `Expected ${expectedCount} post install actions`); - } - - onProjectClosed = noop; - - attach(projectService: server.ProjectService) { - this.projectService = projectService; - } - - getInstallTypingHost() { - return this.installTypingHost; - } - - installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void { - this.addPostExecAction("success", cb); - } - - sendResponse(response: server.SetTypings | server.InvalidateCachedTypings) { - this.projectService.updateTypingsForProject(response); - } - - enqueueInstallTypingsRequest(project: server.Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray) { - const request = server.createInstallTypingsRequest(project, typeAcquisition, unresolvedImports, this.globalTypingsCacheLocation); - this.install(request); - } - - addPostExecAction(stdout: string | string[], cb: TI.RequestCompletedAction) { - const out = isString(stdout) ? stdout : createNpmPackageJsonString(stdout); - const action: PostExecAction = { - success: !!out, - callback: cb - }; - this.postExecActions.push(action); - } - } - - function createNpmPackageJsonString(installedTypings: string[]): string { - const dependencies: MapLike = {}; - for (const typing of installedTypings) { - dependencies[typing] = "1.0.0"; - } - return JSON.stringify({ dependencies }); - } - - export function createTypesRegistry(...list: string[]): Map> { - const versionMap = { - "latest": "1.3.0", - "ts2.0": "1.0.0", - "ts2.1": "1.0.0", - "ts2.2": "1.2.0", - "ts2.3": "1.3.0", - "ts2.4": "1.3.0", - "ts2.5": "1.3.0", - "ts2.6": "1.3.0", - "ts2.7": "1.3.0" - }; - const map = createMap>(); - for (const l of list) { - map.set(l, versionMap); - } - return map; - } - - export function toExternalFile(fileName: string): protocol.ExternalFile { - return { fileName }; - } - - export function toExternalFiles(fileNames: string[]) { - return map(fileNames, toExternalFile); - } - - export function fileStats(nonZeroStats: Partial): server.FileStats { - return { ts: 0, tsSize: 0, tsx: 0, tsxSize: 0, dts: 0, dtsSize: 0, js: 0, jsSize: 0, jsx: 0, jsxSize: 0, deferred: 0, deferredSize: 0, ...nonZeroStats }; - } - - export interface ConfigFileDiagnostic { - fileName: string | undefined; - start: number | undefined; - length: number | undefined; - messageText: string; - category: DiagnosticCategory; - code: number; - reportsUnnecessary?: {}; - source?: string; - relatedInformation?: DiagnosticRelatedInformation[]; - } - - export class TestServerEventManager { - private events: server.ProjectServiceEvent[] = []; - readonly session: TestSession; - readonly service: server.ProjectService; - readonly host: TestServerHost; - constructor(files: File[], suppressDiagnosticEvents?: boolean) { - this.host = createServerHost(files); - this.session = createSession(this.host, { - canUseEvents: true, - eventHandler: event => this.events.push(event), - suppressDiagnosticEvents, - }); - this.service = this.session.getProjectService(); - } - - getEvents(): readonly server.ProjectServiceEvent[] { - const events = this.events; - this.events = []; - return events; - } - - getEvent(eventName: T["eventName"]): T["data"] { - let eventData: T["data"] | undefined; - filterMutate(this.events, e => { - if (e.eventName === eventName) { - if (eventData !== undefined) { - assert(false, "more than one event found"); - } - eventData = e.data; - return false; - } - return true; - }); - return Debug.checkDefined(eventData); - } - - hasZeroEvent(eventName: T["eventName"]) { - this.events.forEach(event => assert.notEqual(event.eventName, eventName)); - } - - checkSingleConfigFileDiagEvent(configFileName: string, triggerFile: string, errors: readonly ConfigFileDiagnostic[]) { - const eventData = this.getEvent(server.ConfigFileDiagEvent); - assert.equal(eventData.configFileName, configFileName); - assert.equal(eventData.triggerFile, triggerFile); - const actual = eventData.diagnostics.map(({ file, messageText, ...rest }) => ({ fileName: file && file.fileName, messageText: isString(messageText) ? messageText : "", ...rest })); - if (errors) { - assert.deepEqual(actual, errors); - } - } - - assertProjectInfoTelemetryEvent(partial: Partial, configFile = "/tsconfig.json"): void { - assert.deepEqual(this.getEvent(server.ProjectInfoTelemetryEvent), { - projectId: sys.createSHA256Hash!(configFile), - fileStats: fileStats({ ts: 1 }), - compilerOptions: {}, - extends: false, - files: false, - include: false, - exclude: false, - compileOnSave: false, - typeAcquisition: { - enable: false, - exclude: false, - include: false, - }, - configFileName: "tsconfig.json", - projectType: "configured", - languageServiceEnabled: true, - version: ts.version, // eslint-disable-line @typescript-eslint/no-unnecessary-qualifier - ...partial, - }); - } - - assertOpenFileTelemetryEvent(info: server.OpenFileInfo): void { - assert.deepEqual(this.getEvent(server.OpenFileInfoTelemetryEvent), { info }); - } - assertNoOpenFilesTelemetryEvent(): void { - this.hasZeroEvent(server.OpenFileInfoTelemetryEvent); - } +} +function createNpmPackageJsonString(installedTypings: string[]): string { + const dependencies: MapLike = {}; + for (const typing of installedTypings) { + dependencies[typing] = "1.0.0"; } - - export class TestSession extends server.Session { - private seq = 0; - public events: protocol.Event[] = []; - public testhost: TestServerHost = this.host as TestServerHost; - - getProjectService() { - return this.projectService; - } - - public getSeq() { - return this.seq; - } - - public getNextSeq() { - return this.seq + 1; - } - - public executeCommandSeq(request: Partial) { - this.seq++; - request.seq = this.seq; - request.type = "request"; - return this.executeCommand(request); - } - - public event(body: T, eventName: string) { - this.events.push(server.toEvent(eventName, body)); - super.event(body, eventName); - } - - public clearMessages() { - clear(this.events); - this.testhost.clearOutput(); - } + return JSON.stringify({ dependencies }); +} +export function createTypesRegistry(...list: string[]): ts.Map> { + const versionMap = { + "latest": "1.3.0", + "ts2.0": "1.0.0", + "ts2.1": "1.0.0", + "ts2.2": "1.2.0", + "ts2.3": "1.3.0", + "ts2.4": "1.3.0", + "ts2.5": "1.3.0", + "ts2.6": "1.3.0", + "ts2.7": "1.3.0" + }; + const map = createMap>(); + for (const l of list) { + map.set(l, versionMap); } - - export function createSession(host: server.ServerHost, opts: Partial = {}) { - if (opts.typingsInstaller === undefined) { - opts.typingsInstaller = new TestTypingsInstaller("/a/data/", /*throttleLimit*/ 5, host); - } - - if (opts.eventHandler !== undefined) { - opts.canUseEvents = true; - } - - const sessionOptions: server.SessionOptions = { - host, - cancellationToken: server.nullCancellationToken, - useSingleInferredProject: false, - useInferredProjectPerProjectRoot: false, - typingsInstaller: undefined!, // TODO: GH#18217 - byteLength: Utils.byteLength, - hrtime: process.hrtime, - logger: opts.logger || createHasErrorMessageLogger().logger, - canUseEvents: false - }; - - return new TestSession({ ...sessionOptions, ...opts }); - } - - export function createSessionWithEventTracking(host: server.ServerHost, eventName: T["eventName"], ...eventNames: T["eventName"][]) { - const events: T[] = []; - const session = createSession(host, { - eventHandler: e => { - if (e.eventName === eventName || eventNames.some(eventName => e.eventName === eventName)) { - events.push(e as T); + return map; +} +export function toExternalFile(fileName: string): protocol.ExternalFile { + return { fileName }; +} +export function toExternalFiles(fileNames: string[]) { + return map(fileNames, toExternalFile); +} +export function fileStats(nonZeroStats: Partial): FileStats { + return { ts: 0, tsSize: 0, tsx: 0, tsxSize: 0, dts: 0, dtsSize: 0, js: 0, jsSize: 0, jsx: 0, jsxSize: 0, deferred: 0, deferredSize: 0, ...nonZeroStats }; +} +export interface ConfigFileDiagnostic { + fileName: string | undefined; + start: number | undefined; + length: number | undefined; + messageText: string; + category: DiagnosticCategory; + code: number; + reportsUnnecessary?: {}; + source?: string; + relatedInformation?: DiagnosticRelatedInformation[]; +} +export class TestServerEventManager { + private events: ProjectServiceEvent[] = []; + readonly session: TestSession; + readonly service: ProjectService; + readonly host: TestServerHost; + constructor(files: File[], suppressDiagnosticEvents?: boolean) { + this.host = createServerHost(files); + this.session = createSession(this.host, { + canUseEvents: true, + eventHandler: event => this.events.push(event), + suppressDiagnosticEvents, + }); + this.service = this.session.getProjectService(); + } + getEvents(): readonly ProjectServiceEvent[] { + const events = this.events; + this.events = []; + return events; + } + getEvent(eventName: T["eventName"]): T["data"] { + let eventData: T["data"] | undefined; + filterMutate(this.events, e => { + if (e.eventName === eventName) { + if (eventData !== undefined) { + assert(false, "more than one event found"); } + eventData = e.data; + return false; } + return true; }); - - return { session, events }; - } - - export function createSessionWithDefaultEventHandler(host: TestServerHost, eventNames: T["event"] | T["event"][], opts: Partial = {}) { - const session = createSession(host, { canUseEvents: true, ...opts }); - - return { - session, - getEvents, - clearEvents - }; - - function getEvents() { - return mapDefined(host.getOutput(), s => { - const e = mapOutputToJson(s); - return (isArray(eventNames) ? eventNames.some(eventName => e.event === eventName) : e.event === eventNames) ? e as T : undefined; - }); - } - - function clearEvents() { - session.clearMessages(); - } - } - - export interface CreateProjectServiceParameters { - cancellationToken?: HostCancellationToken; - logger?: server.Logger; - useSingleInferredProject?: boolean; - typingsInstaller?: server.ITypingsInstaller; - eventHandler?: server.ProjectServiceEventHandler; - } - - export class TestProjectService extends server.ProjectService { - constructor(host: server.ServerHost, logger: server.Logger, cancellationToken: HostCancellationToken, useSingleInferredProject: boolean, - typingsInstaller: server.ITypingsInstaller, eventHandler: server.ProjectServiceEventHandler, opts: Partial = {}) { - super({ - host, - logger, - cancellationToken, - useSingleInferredProject, - useInferredProjectPerProjectRoot: false, - typingsInstaller, - typesMapLocation: customTypesMap.path, - eventHandler, - ...opts - }); - } - - checkNumberOfProjects(count: { inferredProjects?: number, configuredProjects?: number, externalProjects?: number }) { - checkNumberOfProjects(this, count); - } - } - export function createProjectService(host: server.ServerHost, parameters: CreateProjectServiceParameters = {}, options?: Partial) { - const cancellationToken = parameters.cancellationToken || server.nullCancellationToken; - const logger = parameters.logger || createHasErrorMessageLogger().logger; - const useSingleInferredProject = parameters.useSingleInferredProject !== undefined ? parameters.useSingleInferredProject : false; - return new TestProjectService(host, logger, cancellationToken, useSingleInferredProject, parameters.typingsInstaller!, parameters.eventHandler!, options); // TODO: GH#18217 - } - - export function checkNumberOfConfiguredProjects(projectService: server.ProjectService, expected: number) { - assert.equal(projectService.configuredProjects.size, expected, `expected ${expected} configured project(s)`); - } - - export function checkNumberOfExternalProjects(projectService: server.ProjectService, expected: number) { - assert.equal(projectService.externalProjects.length, expected, `expected ${expected} external project(s)`); - } - - export function checkNumberOfInferredProjects(projectService: server.ProjectService, expected: number) { - assert.equal(projectService.inferredProjects.length, expected, `expected ${expected} inferred project(s)`); - } - - export function checkNumberOfProjects(projectService: server.ProjectService, count: { inferredProjects?: number, configuredProjects?: number, externalProjects?: number }) { - checkNumberOfConfiguredProjects(projectService, count.configuredProjects || 0); - checkNumberOfExternalProjects(projectService, count.externalProjects || 0); - checkNumberOfInferredProjects(projectService, count.inferredProjects || 0); - } - - export function configuredProjectAt(projectService: server.ProjectService, index: number) { - const values = projectService.configuredProjects.values(); - while (index > 0) { - const iterResult = values.next(); - if (iterResult.done) return Debug.fail("Expected a result."); - index--; - } - const iterResult = values.next(); - if (iterResult.done) return Debug.fail("Expected a result."); - return iterResult.value; - } - - export function checkProjectActualFiles(project: server.Project, expectedFiles: readonly string[]) { - checkArray(`${server.ProjectKind[project.projectKind]} project, actual files`, project.getFileNames(), expectedFiles); - } - - export function checkProjectRootFiles(project: server.Project, expectedFiles: readonly string[]) { - checkArray(`${server.ProjectKind[project.projectKind]} project, rootFileNames`, project.getRootFiles(), expectedFiles); - } - - export function mapCombinedPathsInAncestor(dir: string, path2: string, mapAncestor: (ancestor: string) => boolean) { - dir = normalizePath(dir); - const result: string[] = []; - forEachAncestorDirectory(dir, ancestor => { - if (mapAncestor(ancestor)) { - result.push(combinePaths(ancestor, path2)); - } + return Debug.checkDefined(eventData); + } + hasZeroEvent(eventName: T["eventName"]) { + this.events.forEach(event => assert.notEqual(event.eventName, eventName)); + } + checkSingleConfigFileDiagEvent(configFileName: string, triggerFile: string, errors: readonly ConfigFileDiagnostic[]) { + const eventData = this.getEvent(ConfigFileDiagEvent); + assert.equal(eventData.configFileName, configFileName); + assert.equal(eventData.triggerFile, triggerFile); + const actual = eventData.diagnostics.map(({ file, messageText, ...rest }) => ({ fileName: file && file.fileName, messageText: isString(messageText) ? messageText : "", ...rest })); + if (errors) { + assert.deepEqual(actual, errors); + } + } + assertProjectInfoTelemetryEvent(partial: Partial, configFile = "/tsconfig.json"): void { + assert.deepEqual(this.getEvent(ProjectInfoTelemetryEvent), { + projectId: sys.createSHA256Hash!(configFile), + fileStats: fileStats({ ts: 1 }), + compilerOptions: {}, + extends: false, + files: false, + include: false, + exclude: false, + compileOnSave: false, + typeAcquisition: { + enable: false, + exclude: false, + include: false, + }, + configFileName: "tsconfig.json", + projectType: "configured", + languageServiceEnabled: true, + version: version, + ...partial, }); - return result; - } - - export function getRootsToWatchWithAncestorDirectory(dir: string, path2: string) { - return mapCombinedPathsInAncestor(dir, path2, ancestor => ancestor.split(directorySeparator).length > 4); - } - - export const nodeModules = "node_modules"; - export function getNodeModuleDirectories(dir: string) { - return getRootsToWatchWithAncestorDirectory(dir, nodeModules); - } - - export const nodeModulesAtTypes = "node_modules/@types"; - export function getTypeRootsFromLocation(currentDirectory: string) { - return getRootsToWatchWithAncestorDirectory(currentDirectory, nodeModulesAtTypes); - } - - export function getConfigFilesToWatch(folder: string) { - return [ - ...getRootsToWatchWithAncestorDirectory(folder, "tsconfig.json"), - ...getRootsToWatchWithAncestorDirectory(folder, "jsconfig.json") - ]; - } - - export function checkOpenFiles(projectService: server.ProjectService, expectedFiles: File[]) { - checkArray("Open files", arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath(path as Path)!.fileName), expectedFiles.map(file => file.path)); - } - - export function checkScriptInfos(projectService: server.ProjectService, expectedFiles: readonly string[], additionInfo?: string) { - checkArray(`ScriptInfos files: ${additionInfo || ""}`, arrayFrom(projectService.filenameToScriptInfo.values(), info => info.fileName), expectedFiles); - } - - export function protocolLocationFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): protocol.Location { - const start = nthIndexOf(str, substring, options ? options.index : 0); - Debug.assert(start !== -1); - return protocolToLocation(str)(start); - } - - export function protocolToLocation(text: string): (pos: number) => protocol.Location { - const lineStarts = computeLineStarts(text); - return pos => { - const x = computeLineAndCharacterOfPosition(lineStarts, pos); - return { line: x.line + 1, offset: x.character + 1 }; - }; } - - export function protocolTextSpanFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): protocol.TextSpan { - const span = textSpanFromSubstring(str, substring, options); - const toLocation = protocolToLocation(str); - return { start: toLocation(span.start), end: toLocation(textSpanEnd(span)) }; - } - - export interface DocumentSpanFromSubstring { - file: File; - text: string; - options?: SpanFromSubstringOptions; - contextText?: string; - contextOptions?: SpanFromSubstringOptions; - } - export function protocolFileSpanFromSubstring({ file, text, options }: DocumentSpanFromSubstring): protocol.FileSpan { - return { file: file.path, ...protocolTextSpanFromSubstring(file.content, text, options) }; - } - - interface FileSpanWithContextFromSubString { - file: File; - text: string; - options?: SpanFromSubstringOptions; - contextText?: string; - contextOptions?: SpanFromSubstringOptions; - } - export function protocolFileSpanWithContextFromSubstring({ contextText, contextOptions, ...rest }: FileSpanWithContextFromSubString): protocol.FileSpanWithContext { - const result = protocolFileSpanFromSubstring(rest); - const contextSpan = contextText !== undefined ? - protocolFileSpanFromSubstring({ file: rest.file, text: contextText, options: contextOptions }) : - undefined; - return contextSpan ? - { - ...result, - contextStart: contextSpan.start, - contextEnd: contextSpan.end - } : - result; - } - - export interface ProtocolTextSpanWithContextFromString { - fileText: string; - text: string; - options?: SpanFromSubstringOptions; - contextText?: string; - contextOptions?: SpanFromSubstringOptions; - } - export function protocolTextSpanWithContextFromSubstring({ fileText, text, options, contextText, contextOptions }: ProtocolTextSpanWithContextFromString): protocol.TextSpanWithContext { - const span = textSpanFromSubstring(fileText, text, options); - const toLocation = protocolToLocation(fileText); - const contextSpan = contextText !== undefined ? textSpanFromSubstring(fileText, contextText, contextOptions) : undefined; - return { - start: toLocation(span.start), - end: toLocation(textSpanEnd(span)), - ...contextSpan && { - contextStart: toLocation(contextSpan.start), - contextEnd: toLocation(textSpanEnd(contextSpan)) - } - }; + assertOpenFileTelemetryEvent(info: OpenFileInfo): void { + assert.deepEqual(this.getEvent(OpenFileInfoTelemetryEvent), { info }); } - - export interface ProtocolRenameSpanFromSubstring extends ProtocolTextSpanWithContextFromString { - prefixSuffixText?: { - readonly prefixText?: string; - readonly suffixText?: string; - }; + assertNoOpenFilesTelemetryEvent(): void { + this.hasZeroEvent(OpenFileInfoTelemetryEvent); } - export function protocolRenameSpanFromSubstring({ prefixSuffixText, ...rest }: ProtocolRenameSpanFromSubstring): protocol.RenameTextSpan { - return { - ...protocolTextSpanWithContextFromSubstring(rest), - ...prefixSuffixText - }; +} +export class TestSession extends Session { + private seq = 0; + public events: protocol.Event[] = []; + public testhost: TestServerHost = this.host as TestServerHost; + getProjectService() { + return this.projectService; } - - export function textSpanFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): TextSpan { - const start = nthIndexOf(str, substring, options ? options.index : 0); - Debug.assert(start !== -1); - return createTextSpan(start, substring.length); - } - - export function protocolFileLocationFromSubstring(file: File, substring: string, options?: SpanFromSubstringOptions): protocol.FileLocationRequestArgs { - return { file: file.path, ...protocolLocationFromSubstring(file.content, substring, options) }; - } - - export interface SpanFromSubstringOptions { - readonly index: number; - } - - function nthIndexOf(str: string, substr: string, n: number): number { - let index = -1; - for (; n >= 0; n--) { - index = str.indexOf(substr, index + 1); - if (index === -1) return -1; - } - return index; - } - - /** - * Test server cancellation token used to mock host token cancellation requests. - * The cancelAfterRequest constructor param specifies how many isCancellationRequested() calls - * should be made before canceling the token. The id of the request to cancel should be set with - * setRequestToCancel(); - */ - export class TestServerCancellationToken implements server.ServerCancellationToken { - private currentId: number | undefined = -1; - private requestToCancel = -1; - private isCancellationRequestedCount = 0; - - constructor(private cancelAfterRequest = 0) { - } - - setRequest(requestId: number) { - this.currentId = requestId; - } - - setRequestToCancel(requestId: number) { - this.resetToken(); - this.requestToCancel = requestId; - } - - resetRequest(requestId: number) { - assert.equal(requestId, this.currentId, "unexpected request id in cancellation"); - this.currentId = undefined; - } - - isCancellationRequested() { - this.isCancellationRequestedCount++; - // If the request id is the request to cancel and isCancellationRequestedCount - // has been met then cancel the request. Ex: cancel the request if it is a - // nav bar request & isCancellationRequested() has already been called three times. - return this.requestToCancel === this.currentId && this.isCancellationRequestedCount >= this.cancelAfterRequest; - } - - resetToken() { - this.currentId = -1; - this.isCancellationRequestedCount = 0; - this.requestToCancel = -1; - } + public getSeq() { + return this.seq; } - - export function makeSessionRequest(command: string, args: T): protocol.Request { - return { - seq: 0, - type: "request", - command, - arguments: args - }; + public getNextSeq() { + return this.seq + 1; } - - export function executeSessionRequest(session: server.Session, command: TRequest["command"], args: TRequest["arguments"]): TResponse["body"] { - return session.executeCommand(makeSessionRequest(command, args)).response as TResponse["body"]; + public executeCommandSeq(request: Partial) { + this.seq++; + request.seq = this.seq; + request.type = "request"; + return this.executeCommand(request); } - - export function executeSessionRequestNoResponse(session: server.Session, command: TRequest["command"], args: TRequest["arguments"]): void { - session.executeCommand(makeSessionRequest(command, args)); + public event(body: T, eventName: string) { + this.events.push(toEvent(eventName, body)); + super.event(body, eventName); } - - export function openFilesForSession(files: readonly (File | { readonly file: File | string, readonly projectRootPath: string, content?: string })[], session: server.Session): void { - for (const file of files) { - session.executeCommand(makeSessionRequest(CommandNames.Open, - "projectRootPath" in file ? { file: typeof file.file === "string" ? file.file : file.file.path, projectRootPath: file.projectRootPath } : { file: file.path })); // eslint-disable-line no-in-operator - } + public clearMessages() { + clear(this.events); + this.testhost.clearOutput(); } - - export function closeFilesForSession(files: readonly File[], session: server.Session): void { - for (const file of files) { - session.executeCommand(makeSessionRequest(CommandNames.Close, { file: file.path })); +} +export function createSession(host: ServerHost, opts: Partial = {}) { + if (opts.typingsInstaller === undefined) { + opts.typingsInstaller = new TestTypingsInstaller("/a/data/", /*throttleLimit*/ 5, host); + } + if (opts.eventHandler !== undefined) { + opts.canUseEvents = true; + } + const sessionOptions: SessionOptions = { + host, + cancellationToken: nullCancellationToken, + useSingleInferredProject: false, + useInferredProjectPerProjectRoot: false, + typingsInstaller: undefined!, + byteLength: byteLength, + hrtime: process.hrtime, + logger: opts.logger || createHasErrorMessageLogger().logger, + canUseEvents: false + }; + return new TestSession({ ...sessionOptions, ...opts }); +} +export function createSessionWithEventTracking(host: ServerHost, eventName: T["eventName"], ...eventNames: T["eventName"][]) { + const events: T[] = []; + const session = createSession(host, { + eventHandler: e => { + if (e.eventName === eventName || eventNames.some(eventName => e.eventName === eventName)) { + events.push(e as T); + } } + }); + return { session, events }; +} +export function createSessionWithDefaultEventHandler(host: TestServerHost, eventNames: T["event"] | T["event"][], opts: Partial = {}) { + const session = createSession(host, { canUseEvents: true, ...opts }); + return { + session, + getEvents, + clearEvents + }; + function getEvents() { + return mapDefined(host.getOutput(), s => { + const e = mapOutputToJson(s); + return (isArray(eventNames) ? eventNames.some(eventName => e.event === eventName) : e.event === eventNames) ? e as T : undefined; + }); } - - export interface ErrorInformation { - diagnosticMessage: DiagnosticMessage; - errorTextArguments?: string[]; - } - - function getProtocolDiagnosticMessage({ diagnosticMessage, errorTextArguments = [] }: ErrorInformation) { - return formatStringFromArgs(diagnosticMessage.message, errorTextArguments); - } - - export function verifyDiagnostics(actual: readonly server.protocol.Diagnostic[], expected: readonly ErrorInformation[]) { - const expectedErrors = expected.map(getProtocolDiagnosticMessage); - assert.deepEqual(actual.map(diag => flattenDiagnosticMessageText(diag.text, "\n")), expectedErrors); - } - - export function verifyNoDiagnostics(actual: server.protocol.Diagnostic[]) { - verifyDiagnostics(actual, []); + function clearEvents() { + session.clearMessages(); } - - export function checkErrorMessage(session: TestSession, eventName: protocol.DiagnosticEventKind, diagnostics: protocol.DiagnosticEventBody, isMostRecent = false): void { - checkNthEvent(session, server.toEvent(eventName, diagnostics), 0, isMostRecent); +} +export interface CreateProjectServiceParameters { + cancellationToken?: HostCancellationToken; + logger?: Logger; + useSingleInferredProject?: boolean; + typingsInstaller?: ITypingsInstaller; + eventHandler?: ProjectServiceEventHandler; +} +export class TestProjectService extends ProjectService { + constructor(host: ServerHost, logger: Logger, cancellationToken: HostCancellationToken, useSingleInferredProject: boolean, typingsInstaller: ITypingsInstaller, eventHandler: ProjectServiceEventHandler, opts: Partial = {}) { + super({ + host, + logger, + cancellationToken, + useSingleInferredProject, + useInferredProjectPerProjectRoot: false, + typingsInstaller, + typesMapLocation: customTypesMap.path, + eventHandler, + ...opts + }); } - - export function createDiagnostic(start: protocol.Location, end: protocol.Location, message: DiagnosticMessage, args: readonly string[] = [], category = diagnosticCategoryName(message), reportsUnnecessary?: {}, relatedInformation?: protocol.DiagnosticRelatedInformation[]): protocol.Diagnostic { - return { start, end, text: formatStringFromArgs(message.message, args), code: message.code, category, reportsUnnecessary, relatedInformation, source: undefined }; + checkNumberOfProjects(count: { + inferredProjects?: number; + configuredProjects?: number; + externalProjects?: number; + }) { + checkNumberOfProjects(this, count); } - - export function checkCompleteEvent(session: TestSession, numberOfCurrentEvents: number, expectedSequenceId: number, isMostRecent = true): void { - checkNthEvent(session, server.toEvent("requestCompleted", { request_seq: expectedSequenceId }), numberOfCurrentEvents - 1, isMostRecent); +} +export function createProjectService(host: ServerHost, parameters: CreateProjectServiceParameters = {}, options?: Partial) { + const cancellationToken = parameters.cancellationToken || nullCancellationToken; + const logger = parameters.logger || createHasErrorMessageLogger().logger; + const useSingleInferredProject = parameters.useSingleInferredProject !== undefined ? parameters.useSingleInferredProject : false; + return new TestProjectService(host, logger, cancellationToken, useSingleInferredProject, parameters.typingsInstaller!, parameters.eventHandler!, options); // TODO: GH#18217 +} +export function checkNumberOfConfiguredProjects(projectService: ProjectService, expected: number) { + assert.equal(projectService.configuredProjects.size, expected, `expected ${expected} configured project(s)`); +} +export function checkNumberOfExternalProjects(projectService: ProjectService, expected: number) { + assert.equal(projectService.externalProjects.length, expected, `expected ${expected} external project(s)`); +} +export function checkNumberOfInferredProjects(projectService: ProjectService, expected: number) { + assert.equal(projectService.inferredProjects.length, expected, `expected ${expected} inferred project(s)`); +} +export function checkNumberOfProjects(projectService: ProjectService, count: { + inferredProjects?: number; + configuredProjects?: number; + externalProjects?: number; +}) { + checkNumberOfConfiguredProjects(projectService, count.configuredProjects || 0); + checkNumberOfExternalProjects(projectService, count.externalProjects || 0); + checkNumberOfInferredProjects(projectService, count.inferredProjects || 0); +} +export function configuredProjectAt(projectService: ProjectService, index: number) { + const values = projectService.configuredProjects.values(); + while (index > 0) { + const iterResult = values.next(); + if (iterResult.done) + return Debug.fail("Expected a result."); + index--; + } + const iterResult = values.next(); + if (iterResult.done) + return Debug.fail("Expected a result."); + return iterResult.value; +} +export function checkProjectActualFiles(project: Project, expectedFiles: readonly string[]) { + checkArray(`${ProjectKind[project.projectKind]} project, actual files`, project.getFileNames(), expectedFiles); +} +export function checkProjectRootFiles(project: Project, expectedFiles: readonly string[]) { + checkArray(`${ProjectKind[project.projectKind]} project, rootFileNames`, project.getRootFiles(), expectedFiles); +} +export function mapCombinedPathsInAncestor(dir: string, path2: string, mapAncestor: (ancestor: string) => boolean) { + dir = normalizePath(dir); + const result: string[] = []; + forEachAncestorDirectory(dir, ancestor => { + if (mapAncestor(ancestor)) { + result.push(combinePaths(ancestor, path2)); + } + }); + return result; +} +export function getRootsToWatchWithAncestorDirectory(dir: string, path2: string) { + return mapCombinedPathsInAncestor(dir, path2, ancestor => ancestor.split(directorySeparator).length > 4); +} +export const nodeModules = "node_modules"; +export function getNodeModuleDirectories(dir: string) { + return getRootsToWatchWithAncestorDirectory(dir, nodeModules); +} +export const nodeModulesAtTypes = "node_modules/@types"; +export function getTypeRootsFromLocation(currentDirectory: string) { + return getRootsToWatchWithAncestorDirectory(currentDirectory, nodeModulesAtTypes); +} +export function getConfigFilesToWatch(folder: string) { + return [ + ...getRootsToWatchWithAncestorDirectory(folder, "tsconfig.json"), + ...getRootsToWatchWithAncestorDirectory(folder, "jsconfig.json") + ]; +} +export function checkOpenFiles(projectService: ProjectService, expectedFiles: File[]) { + checkArray("Open files", arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath((path as Path))!.fileName), expectedFiles.map(file => file.path)); +} +export function checkScriptInfos(projectService: ProjectService, expectedFiles: readonly string[], additionInfo?: string) { + checkArray(`ScriptInfos files: ${additionInfo || ""}`, arrayFrom(projectService.filenameToScriptInfo.values(), info => info.fileName), expectedFiles); +} +export function protocolLocationFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): protocol.Location { + const start = nthIndexOf(str, substring, options ? options.index : 0); + Debug.assert(start !== -1); + return protocolToLocation(str)(start); +} +export function protocolToLocation(text: string): (pos: number) => protocol.Location { + const lineStarts = computeLineStarts(text); + return pos => { + const x = computeLineAndCharacterOfPosition(lineStarts, pos); + return { line: x.line + 1, offset: x.character + 1 }; + }; +} +export function protocolTextSpanFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): protocol.TextSpan { + const span = textSpanFromSubstring(str, substring, options); + const toLocation = protocolToLocation(str); + return { start: toLocation(span.start), end: toLocation(textSpanEnd(span)) }; +} +export interface DocumentSpanFromSubstring { + file: File; + text: string; + options?: SpanFromSubstringOptions; + contextText?: string; + contextOptions?: SpanFromSubstringOptions; +} +export function protocolFileSpanFromSubstring({ file, text, options }: DocumentSpanFromSubstring): protocol.FileSpan { + return { file: file.path, ...protocolTextSpanFromSubstring(file.content, text, options) }; +} +interface FileSpanWithContextFromSubString { + file: File; + text: string; + options?: SpanFromSubstringOptions; + contextText?: string; + contextOptions?: SpanFromSubstringOptions; +} +export function protocolFileSpanWithContextFromSubstring({ contextText, contextOptions, ...rest }: FileSpanWithContextFromSubString): protocol.FileSpanWithContext { + const result = protocolFileSpanFromSubstring(rest); + const contextSpan = contextText !== undefined ? + protocolFileSpanFromSubstring({ file: rest.file, text: contextText, options: contextOptions }) : + undefined; + return contextSpan ? + { + ...result, + contextStart: contextSpan.start, + contextEnd: contextSpan.end + } : + result; +} +export interface ProtocolTextSpanWithContextFromString { + fileText: string; + text: string; + options?: SpanFromSubstringOptions; + contextText?: string; + contextOptions?: SpanFromSubstringOptions; +} +export function protocolTextSpanWithContextFromSubstring({ fileText, text, options, contextText, contextOptions }: ProtocolTextSpanWithContextFromString): protocol.TextSpanWithContext { + const span = textSpanFromSubstring(fileText, text, options); + const toLocation = protocolToLocation(fileText); + const contextSpan = contextText !== undefined ? textSpanFromSubstring(fileText, contextText, contextOptions) : undefined; + return { + start: toLocation(span.start), + end: toLocation(textSpanEnd(span)), + ...contextSpan && { + contextStart: toLocation(contextSpan.start), + contextEnd: toLocation(textSpanEnd(contextSpan)) + } + }; +} +export interface ProtocolRenameSpanFromSubstring extends ProtocolTextSpanWithContextFromString { + prefixSuffixText?: { + readonly prefixText?: string; + readonly suffixText?: string; + }; +} +export function protocolRenameSpanFromSubstring({ prefixSuffixText, ...rest }: ProtocolRenameSpanFromSubstring): protocol.RenameTextSpan { + return { + ...protocolTextSpanWithContextFromSubstring(rest), + ...prefixSuffixText + }; +} +export function textSpanFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): TextSpan { + const start = nthIndexOf(str, substring, options ? options.index : 0); + Debug.assert(start !== -1); + return createTextSpan(start, substring.length); +} +export function protocolFileLocationFromSubstring(file: File, substring: string, options?: SpanFromSubstringOptions): protocol.FileLocationRequestArgs { + return { file: file.path, ...protocolLocationFromSubstring(file.content, substring, options) }; +} +export interface SpanFromSubstringOptions { + readonly index: number; +} +function nthIndexOf(str: string, substr: string, n: number): number { + let index = -1; + for (; n >= 0; n--) { + index = str.indexOf(substr, index + 1); + if (index === -1) + return -1; + } + return index; +} +/** + * Test server cancellation token used to mock host token cancellation requests. + * The cancelAfterRequest constructor param specifies how many isCancellationRequested() calls + * should be made before canceling the token. The id of the request to cancel should be set with + * setRequestToCancel(); + */ +export class TestServerCancellationToken implements ServerCancellationToken { + private currentId: number | undefined = -1; + private requestToCancel = -1; + private isCancellationRequestedCount = 0; + constructor(private cancelAfterRequest = 0) { + } + setRequest(requestId: number) { + this.currentId = requestId; + } + setRequestToCancel(requestId: number) { + this.resetToken(); + this.requestToCancel = requestId; + } + resetRequest(requestId: number) { + assert.equal(requestId, this.currentId, "unexpected request id in cancellation"); + this.currentId = undefined; + } + isCancellationRequested() { + this.isCancellationRequestedCount++; + // If the request id is the request to cancel and isCancellationRequestedCount + // has been met then cancel the request. Ex: cancel the request if it is a + // nav bar request & isCancellationRequested() has already been called three times. + return this.requestToCancel === this.currentId && this.isCancellationRequestedCount >= this.cancelAfterRequest; + } + resetToken() { + this.currentId = -1; + this.isCancellationRequestedCount = 0; + this.requestToCancel = -1; } - - export function checkProjectUpdatedInBackgroundEvent(session: TestSession, openFiles: string[]) { - checkNthEvent(session, server.toEvent("projectsUpdatedInBackground", { openFiles }), 0, /*isMostRecent*/ true); +} +export function makeSessionRequest(command: string, args: T): protocol.Request { + return { + seq: 0, + type: "request", + command, + arguments: args + }; +} +export function executeSessionRequest(session: Session, command: TRequest["command"], args: TRequest["arguments"]): TResponse["body"] { + return session.executeCommand(makeSessionRequest(command, args)).response as TResponse["body"]; +} +export function executeSessionRequestNoResponse(session: Session, command: TRequest["command"], args: TRequest["arguments"]): void { + session.executeCommand(makeSessionRequest(command, args)); +} +export function openFilesForSession(files: readonly (File | { + readonly file: File | string; + readonly projectRootPath: string; + content?: string; +})[], session: Session): void { + for (const file of files) { + session.executeCommand(makeSessionRequest(CommandNames.Open, "projectRootPath" in file ? { file: typeof file.file === "string" ? file.file : file.file.path, projectRootPath: file.projectRootPath } : { file: file.path })); // eslint-disable-line no-in-operator } - - export function checkNoDiagnosticEvents(session: TestSession) { - for (const event of session.events) { - assert.isFalse(event.event.endsWith("Diag"), JSON.stringify(event)); - } +} +export function closeFilesForSession(files: readonly File[], session: Session): void { + for (const file of files) { + session.executeCommand(makeSessionRequest(CommandNames.Close, { file: file.path })); } - - export function checkNthEvent(session: TestSession, expectedEvent: protocol.Event, index: number, isMostRecent: boolean) { - const events = session.events; - assert.deepEqual(events[index], expectedEvent, `Expected ${JSON.stringify(expectedEvent)} at ${index} in ${JSON.stringify(events)}`); - - const outputs = session.testhost.getOutput(); - assert.equal(outputs[index], server.formatMessage(expectedEvent, nullLogger, Utils.byteLength, session.testhost.newLine)); - - if (isMostRecent) { - assert.strictEqual(events.length, index + 1, JSON.stringify(events)); - assert.strictEqual(outputs.length, index + 1, JSON.stringify(outputs)); - } +} +export interface ErrorInformation { + diagnosticMessage: DiagnosticMessage; + errorTextArguments?: string[]; +} +function getProtocolDiagnosticMessage({ diagnosticMessage, errorTextArguments = [] }: ErrorInformation) { + return formatStringFromArgs(diagnosticMessage.message, errorTextArguments); +} +export function verifyDiagnostics(actual: readonly server.protocol.Diagnostic[], expected: readonly ErrorInformation[]) { + const expectedErrors = expected.map(getProtocolDiagnosticMessage); + assert.deepEqual(actual.map(diag => flattenDiagnosticMessageText(diag.text, "\n")), expectedErrors); +} +export function verifyNoDiagnostics(actual: server.protocol.Diagnostic[]) { + verifyDiagnostics(actual, []); +} +export function checkErrorMessage(session: TestSession, eventName: protocol.DiagnosticEventKind, diagnostics: protocol.DiagnosticEventBody, isMostRecent = false): void { + checkNthEvent(session, toEvent(eventName, diagnostics), 0, isMostRecent); +} +export function createDiagnostic(start: protocol.Location, end: protocol.Location, message: DiagnosticMessage, args: readonly string[] = [], category = diagnosticCategoryName(message), reportsUnnecessary?: {}, relatedInformation?: protocol.DiagnosticRelatedInformation[]): protocol.Diagnostic { + return { start, end, text: formatStringFromArgs(message.message, args), code: message.code, category, reportsUnnecessary, relatedInformation, source: undefined }; +} +export function checkCompleteEvent(session: TestSession, numberOfCurrentEvents: number, expectedSequenceId: number, isMostRecent = true): void { + checkNthEvent(session, toEvent("requestCompleted", { request_seq: expectedSequenceId }), numberOfCurrentEvents - 1, isMostRecent); +} +export function checkProjectUpdatedInBackgroundEvent(session: TestSession, openFiles: string[]) { + checkNthEvent(session, toEvent("projectsUpdatedInBackground", { openFiles }), 0, /*isMostRecent*/ true); +} +export function checkNoDiagnosticEvents(session: TestSession) { + for (const event of session.events) { + assert.isFalse(event.event.endsWith("Diag"), JSON.stringify(event)); } - - export interface MakeReferenceItem extends DocumentSpanFromSubstring { - isDefinition: boolean; - isWriteAccess?: boolean; - lineText: string; - } - - export function makeReferenceItem({ isDefinition, isWriteAccess, lineText, ...rest }: MakeReferenceItem): protocol.ReferencesResponseItem { - return { - ...protocolFileSpanWithContextFromSubstring(rest), - isDefinition, - isWriteAccess: isWriteAccess === undefined ? isDefinition : isWriteAccess, - lineText, - }; +} +export function checkNthEvent(session: TestSession, expectedEvent: protocol.Event, index: number, isMostRecent: boolean) { + const events = session.events; + assert.deepEqual(events[index], expectedEvent, `Expected ${JSON.stringify(expectedEvent)} at ${index} in ${JSON.stringify(events)}`); + const outputs = session.testhost.getOutput(); + assert.equal(outputs[index], formatMessage(expectedEvent, nullLogger, byteLength, session.testhost.newLine)); + if (isMostRecent) { + assert.strictEqual(events.length, index + 1, JSON.stringify(events)); + assert.strictEqual(outputs.length, index + 1, JSON.stringify(outputs)); } } +export interface MakeReferenceItem extends DocumentSpanFromSubstring { + isDefinition: boolean; + isWriteAccess?: boolean; + lineText: string; +} +export function makeReferenceItem({ isDefinition, isWriteAccess, lineText, ...rest }: MakeReferenceItem): protocol.ReferencesResponseItem { + return { + ...protocolFileSpanWithContextFromSubstring(rest), + isDefinition, + isWriteAccess: isWriteAccess === undefined ? isDefinition : isWriteAccess, + lineText, + }; +} diff --git a/src/testRunner/unittests/tsserver/importHelpers.ts b/src/testRunner/unittests/tsserver/importHelpers.ts index b1ec5955eaae4..7a568d8185acb 100644 --- a/src/testRunner/unittests/tsserver/importHelpers.ts +++ b/src/testRunner/unittests/tsserver/importHelpers.ts @@ -1,18 +1,17 @@ -namespace ts.projectSystem { - describe("unittests:: tsserver:: import helpers", () => { - it("should not crash in tsserver", () => { - const f1 = { - path: "/a/app.ts", - content: "export async function foo() { return 100; }" - }; - const tslib = { - path: "/a/node_modules/tslib/index.d.ts", - content: "" - }; - const host = createServerHost([f1, tslib]); - const service = createProjectService(host); - service.openExternalProject({ projectFileName: "p", rootFiles: [toExternalFile(f1.path)], options: { importHelpers: true } }); - service.checkNumberOfProjects({ externalProjects: 1 }); - }); +import { createServerHost, createProjectService, toExternalFile } from "../../ts.projectSystem"; +describe("unittests:: tsserver:: import helpers", () => { + it("should not crash in tsserver", () => { + const f1 = { + path: "/a/app.ts", + content: "export async function foo() { return 100; }" + }; + const tslib = { + path: "/a/node_modules/tslib/index.d.ts", + content: "" + }; + const host = createServerHost([f1, tslib]); + const service = createProjectService(host); + service.openExternalProject({ projectFileName: "p", rootFiles: [toExternalFile(f1.path)], options: { importHelpers: true } }); + service.checkNumberOfProjects({ externalProjects: 1 }); }); -} +}); diff --git a/src/testRunner/unittests/tsserver/importSuggestionsCache.ts b/src/testRunner/unittests/tsserver/importSuggestionsCache.ts index 657c376db3aba..96b5d389c1ee4 100644 --- a/src/testRunner/unittests/tsserver/importSuggestionsCache.ts +++ b/src/testRunner/unittests/tsserver/importSuggestionsCache.ts @@ -1,60 +1,55 @@ -namespace ts.projectSystem { - const aTs: File = { - path: "/a.ts", - content: "export const foo = 0;", - }; - const bTs: File = { - path: "/b.ts", - content: "foo", - }; - const tsconfig: File = { - path: "/tsconfig.json", - content: "{}", - }; - const ambientDeclaration: File = { - path: "/ambient.d.ts", - content: "declare module 'ambient' {}" +import { File, createServerHost, createSession, openFilesForSession, configuredProjectAt, protocol, executeSessionRequest } from "../../ts.projectSystem"; +const aTs: File = { + path: "/a.ts", + content: "export const foo = 0;", +}; +const bTs: File = { + path: "/b.ts", + content: "foo", +}; +const tsconfig: File = { + path: "/tsconfig.json", + content: "{}", +}; +const ambientDeclaration: File = { + path: "/ambient.d.ts", + content: "declare module 'ambient' {}" +}; +describe("unittests:: tsserver:: importSuggestionsCache", () => { + it("caches auto-imports in the same file", () => { + const { importSuggestionsCache, checker } = setup(); + assert.ok(importSuggestionsCache.get(bTs.path, checker)); + }); + it("invalidates the cache when new files are added", () => { + const { host, importSuggestionsCache, checker } = setup(); + host.reloadFS([aTs, bTs, ambientDeclaration, tsconfig, { ...aTs, path: "/src/a2.ts" }]); + host.runQueuedTimeoutCallbacks(); + assert.isUndefined(importSuggestionsCache.get(bTs.path, checker)); + }); + it("invalidates the cache when files are deleted", () => { + const { host, projectService, importSuggestionsCache, checker } = setup(); + projectService.closeClientFile(aTs.path); + host.reloadFS([bTs, ambientDeclaration, tsconfig]); + host.runQueuedTimeoutCallbacks(); + assert.isUndefined(importSuggestionsCache.get(bTs.path, checker)); + }); +}); +function setup() { + const host = createServerHost([aTs, bTs, ambientDeclaration, tsconfig]); + const session = createSession(host); + openFilesForSession([aTs, bTs], session); + const projectService = session.getProjectService(); + const project = configuredProjectAt(projectService, 0); + const requestLocation: protocol.FileLocationRequestArgs = { + file: bTs.path, + line: 1, + offset: 3, }; - - describe("unittests:: tsserver:: importSuggestionsCache", () => { - it("caches auto-imports in the same file", () => { - const { importSuggestionsCache, checker } = setup(); - assert.ok(importSuggestionsCache.get(bTs.path, checker)); - }); - - it("invalidates the cache when new files are added", () => { - const { host, importSuggestionsCache, checker } = setup(); - host.reloadFS([aTs, bTs, ambientDeclaration, tsconfig, { ...aTs, path: "/src/a2.ts" }]); - host.runQueuedTimeoutCallbacks(); - assert.isUndefined(importSuggestionsCache.get(bTs.path, checker)); - }); - - it("invalidates the cache when files are deleted", () => { - const { host, projectService, importSuggestionsCache, checker } = setup(); - projectService.closeClientFile(aTs.path); - host.reloadFS([bTs, ambientDeclaration, tsconfig]); - host.runQueuedTimeoutCallbacks(); - assert.isUndefined(importSuggestionsCache.get(bTs.path, checker)); - }); + executeSessionRequest(session, protocol.CommandTypes.CompletionInfo, { + ...requestLocation, + includeExternalModuleExports: true, + prefix: "foo", }); - - function setup() { - const host = createServerHost([aTs, bTs, ambientDeclaration, tsconfig]); - const session = createSession(host); - openFilesForSession([aTs, bTs], session); - const projectService = session.getProjectService(); - const project = configuredProjectAt(projectService, 0); - const requestLocation: protocol.FileLocationRequestArgs = { - file: bTs.path, - line: 1, - offset: 3, - }; - executeSessionRequest(session, protocol.CommandTypes.CompletionInfo, { - ...requestLocation, - includeExternalModuleExports: true, - prefix: "foo", - }); - const checker = project.getLanguageService().getProgram()!.getTypeChecker(); - return { host, project, projectService, importSuggestionsCache: project.getImportSuggestionsCache(), checker }; - } + const checker = project.getLanguageService().getProgram()!.getTypeChecker(); + return { host, project, projectService, importSuggestionsCache: project.getImportSuggestionsCache(), checker }; } diff --git a/src/testRunner/unittests/tsserver/inferredProjects.ts b/src/testRunner/unittests/tsserver/inferredProjects.ts index 04a849353b80f..52d45e0323aa5 100644 --- a/src/testRunner/unittests/tsserver/inferredProjects.ts +++ b/src/testRunner/unittests/tsserver/inferredProjects.ts @@ -1,433 +1,393 @@ -namespace ts.projectSystem { - describe("unittests:: tsserver:: Inferred projects", () => { - it("create inferred project", () => { - const appFile: File = { - path: `${tscWatch.projectRoot}/app.ts`, - content: ` +import { File, createServerHost, libFile, createProjectService, checkNumberOfConfiguredProjects, checkNumberOfInferredProjects, checkArray, checkWatchedFiles, getConfigFilesToWatch, checkWatchedDirectories, nodeModulesAtTypes, checkProjectActualFiles, checkNumberOfProjects, createSession, CommandNames } from "../../ts.projectSystem"; +import { projectRoot } from "../../ts.tscWatch"; +import { combinePaths, ModuleResolutionKind, ScriptTarget, ScriptKind, createMap } from "../../ts"; +import { protocol, InferredProject } from "../../ts.server"; +describe("unittests:: tsserver:: Inferred projects", () => { + it("create inferred project", () => { + const appFile: File = { + path: `${projectRoot}/app.ts`, + content: ` import {f} from "./module" console.log(f) ` - }; - - const moduleFile: File = { - path: `${tscWatch.projectRoot}/module.d.ts`, - content: `export let x: number` - }; - const host = createServerHost([appFile, moduleFile, libFile]); - const projectService = createProjectService(host); - const { configFileName } = projectService.openClientFile(appFile.path); - - assert(!configFileName, `should not find config, got: '${configFileName}`); - checkNumberOfConfiguredProjects(projectService, 0); - checkNumberOfInferredProjects(projectService, 1); - - const project = projectService.inferredProjects[0]; - - checkArray("inferred project", project.getFileNames(), [appFile.path, libFile.path, moduleFile.path]); - checkWatchedFiles(host, getConfigFilesToWatch(tscWatch.projectRoot).concat(libFile.path, moduleFile.path)); - checkWatchedDirectories(host, [tscWatch.projectRoot], /*recursive*/ false); - checkWatchedDirectories(host, [combinePaths(tscWatch.projectRoot, nodeModulesAtTypes)], /*recursive*/ true); - }); - - it("should use only one inferred project if 'useOneInferredProject' is set", () => { - const file1 = { - path: `${tscWatch.projectRoot}/a/b/main.ts`, - content: "let x =1;" - }; - const configFile: File = { - path: `${tscWatch.projectRoot}/a/b/tsconfig.json`, - content: `{ + }; + const moduleFile: File = { + path: `${projectRoot}/module.d.ts`, + content: `export let x: number` + }; + const host = createServerHost([appFile, moduleFile, libFile]); + const projectService = createProjectService(host); + const { configFileName } = projectService.openClientFile(appFile.path); + assert(!configFileName, `should not find config, got: '${configFileName}`); + checkNumberOfConfiguredProjects(projectService, 0); + checkNumberOfInferredProjects(projectService, 1); + const project = projectService.inferredProjects[0]; + checkArray("inferred project", project.getFileNames(), [appFile.path, libFile.path, moduleFile.path]); + checkWatchedFiles(host, getConfigFilesToWatch(projectRoot).concat(libFile.path, moduleFile.path)); + checkWatchedDirectories(host, [projectRoot], /*recursive*/ false); + checkWatchedDirectories(host, [combinePaths(projectRoot, nodeModulesAtTypes)], /*recursive*/ true); + }); + it("should use only one inferred project if 'useOneInferredProject' is set", () => { + const file1 = { + path: `${projectRoot}/a/b/main.ts`, + content: "let x =1;" + }; + const configFile: File = { + path: `${projectRoot}/a/b/tsconfig.json`, + content: `{ "compilerOptions": { "target": "es6" }, "files": [ "main.ts" ] }` - }; - const file2 = { - path: `${tscWatch.projectRoot}/a/c/main.ts`, - content: "let x =1;" - }; - - const file3 = { - path: `${tscWatch.projectRoot}/a/d/main.ts`, - content: "let x =1;" - }; - - const host = createServerHost([file1, file2, file3, libFile]); - const projectService = createProjectService(host, { useSingleInferredProject: true }); - projectService.openClientFile(file1.path); - projectService.openClientFile(file2.path); - projectService.openClientFile(file3.path); - - checkNumberOfConfiguredProjects(projectService, 0); - checkNumberOfInferredProjects(projectService, 1); - checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path, libFile.path]); - - - host.reloadFS([file1, configFile, file2, file3, libFile]); - host.checkTimeoutQueueLengthAndRun(2); // load configured project from disk + ensureProjectsForOpenFiles - checkNumberOfConfiguredProjects(projectService, 1); - checkNumberOfInferredProjects(projectService, 1); - checkProjectActualFiles(projectService.inferredProjects[0], [file2.path, file3.path, libFile.path]); - }); - - it("disable inferred project", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x =1;" - }; - - const host = createServerHost([file1]); - const projectService = createProjectService(host, { useSingleInferredProject: true }, { syntaxOnly: true }); - - projectService.openClientFile(file1.path, file1.content); - - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - const proj = projectService.inferredProjects[0]; - assert.isDefined(proj); - - assert.isFalse(proj.languageServiceEnabled); - }); - - it("project settings for inferred projects", () => { - const file1 = { - path: "/a/b/app.ts", - content: `import {x} from "mod"` - }; - const modFile = { - path: "/a/mod.ts", - content: "export let x: number" - }; - const host = createServerHost([file1, modFile]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - projectService.openClientFile(modFile.path); - - checkNumberOfProjects(projectService, { inferredProjects: 2 }); - const inferredProjects = projectService.inferredProjects.slice(); - checkProjectActualFiles(inferredProjects[0], [file1.path]); - checkProjectActualFiles(inferredProjects[1], [modFile.path]); - - projectService.setCompilerOptionsForInferredProjects({ moduleResolution: ModuleResolutionKind.Classic }); - host.checkTimeoutQueueLengthAndRun(3); - checkNumberOfProjects(projectService, { inferredProjects: 2 }); - assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]); - assert.strictEqual(projectService.inferredProjects[1], inferredProjects[1]); - checkProjectActualFiles(inferredProjects[0], [file1.path, modFile.path]); - assert.isTrue(inferredProjects[1].isOrphan()); - }); - - it("should support files without extensions", () => { - const f = { - path: "/a/compile", - content: "let x = 1" - }; - const host = createServerHost([f]); - const session = createSession(host); - session.executeCommand({ - seq: 1, - type: "request", - command: "compilerOptionsForInferredProjects", - arguments: { - options: { - allowJs: true - } - } - }); - session.executeCommand({ - seq: 2, - type: "request", - command: "open", - arguments: { - file: f.path, - fileContent: f.content, - scriptKindName: "JS" + }; + const file2 = { + path: `${projectRoot}/a/c/main.ts`, + content: "let x =1;" + }; + const file3 = { + path: `${projectRoot}/a/d/main.ts`, + content: "let x =1;" + }; + const host = createServerHost([file1, file2, file3, libFile]); + const projectService = createProjectService(host, { useSingleInferredProject: true }); + projectService.openClientFile(file1.path); + projectService.openClientFile(file2.path); + projectService.openClientFile(file3.path); + checkNumberOfConfiguredProjects(projectService, 0); + checkNumberOfInferredProjects(projectService, 1); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path, libFile.path]); + host.reloadFS([file1, configFile, file2, file3, libFile]); + host.checkTimeoutQueueLengthAndRun(2); // load configured project from disk + ensureProjectsForOpenFiles + checkNumberOfConfiguredProjects(projectService, 1); + checkNumberOfInferredProjects(projectService, 1); + checkProjectActualFiles(projectService.inferredProjects[0], [file2.path, file3.path, libFile.path]); + }); + it("disable inferred project", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x =1;" + }; + const host = createServerHost([file1]); + const projectService = createProjectService(host, { useSingleInferredProject: true }, { syntaxOnly: true }); + projectService.openClientFile(file1.path, file1.content); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const proj = projectService.inferredProjects[0]; + assert.isDefined(proj); + assert.isFalse(proj.languageServiceEnabled); + }); + it("project settings for inferred projects", () => { + const file1 = { + path: "/a/b/app.ts", + content: `import {x} from "mod"` + }; + const modFile = { + path: "/a/mod.ts", + content: "export let x: number" + }; + const host = createServerHost([file1, modFile]); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + projectService.openClientFile(modFile.path); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + const inferredProjects = projectService.inferredProjects.slice(); + checkProjectActualFiles(inferredProjects[0], [file1.path]); + checkProjectActualFiles(inferredProjects[1], [modFile.path]); + projectService.setCompilerOptionsForInferredProjects({ moduleResolution: ModuleResolutionKind.Classic }); + host.checkTimeoutQueueLengthAndRun(3); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]); + assert.strictEqual(projectService.inferredProjects[1], inferredProjects[1]); + checkProjectActualFiles(inferredProjects[0], [file1.path, modFile.path]); + assert.isTrue(inferredProjects[1].isOrphan()); + }); + it("should support files without extensions", () => { + const f = { + path: "/a/compile", + content: "let x = 1" + }; + const host = createServerHost([f]); + const session = createSession(host); + session.executeCommand(({ + seq: 1, + type: "request", + command: "compilerOptionsForInferredProjects", + arguments: { + options: { + allowJs: true } - }); - const projectService = session.getProjectService(); - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - checkProjectActualFiles(projectService.inferredProjects[0], [f.path]); + } + })); + session.executeCommand(({ + seq: 2, + type: "request", + command: "open", + arguments: { + file: f.path, + fileContent: f.content, + scriptKindName: "JS" + } + })); + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + checkProjectActualFiles(projectService.inferredProjects[0], [f.path]); + }); + it("inferred projects per project root", () => { + const file1 = { path: "/a/file1.ts", content: "let x = 1;", projectRootPath: "/a" }; + const file2 = { path: "/a/file2.ts", content: "let y = 2;", projectRootPath: "/a" }; + const file3 = { path: "/b/file2.ts", content: "let x = 3;", projectRootPath: "/b" }; + const file4 = { path: "/c/file3.ts", content: "let z = 4;" }; + const host = createServerHost([file1, file2, file3, file4]); + const session = createSession(host, { + useSingleInferredProject: true, + useInferredProjectPerProjectRoot: true }); - - it("inferred projects per project root", () => { - const file1 = { path: "/a/file1.ts", content: "let x = 1;", projectRootPath: "/a" }; - const file2 = { path: "/a/file2.ts", content: "let y = 2;", projectRootPath: "/a" }; - const file3 = { path: "/b/file2.ts", content: "let x = 3;", projectRootPath: "/b" }; - const file4 = { path: "/c/file3.ts", content: "let z = 4;" }; - const host = createServerHost([file1, file2, file3, file4]); - const session = createSession(host, { - useSingleInferredProject: true, - useInferredProjectPerProjectRoot: true - }); - session.executeCommand({ - seq: 1, - type: "request", - command: CommandNames.CompilerOptionsForInferredProjects, - arguments: { - options: { - allowJs: true, - target: ScriptTarget.ESNext - } - } - }); - session.executeCommand({ - seq: 2, - type: "request", - command: CommandNames.CompilerOptionsForInferredProjects, - arguments: { - options: { - allowJs: true, - target: ScriptTarget.ES2015 - }, - projectRootPath: "/b" - } - }); - session.executeCommand({ - seq: 3, - type: "request", - command: CommandNames.Open, - arguments: { - file: file1.path, - fileContent: file1.content, - scriptKindName: "JS", - projectRootPath: file1.projectRootPath - } - }); - session.executeCommand({ - seq: 4, - type: "request", - command: CommandNames.Open, - arguments: { - file: file2.path, - fileContent: file2.content, - scriptKindName: "JS", - projectRootPath: file2.projectRootPath - } - }); - session.executeCommand({ - seq: 5, - type: "request", - command: CommandNames.Open, - arguments: { - file: file3.path, - fileContent: file3.content, - scriptKindName: "JS", - projectRootPath: file3.projectRootPath - } - }); - session.executeCommand({ - seq: 6, - type: "request", - command: CommandNames.Open, - arguments: { - file: file4.path, - fileContent: file4.content, - scriptKindName: "JS" + session.executeCommand(({ + seq: 1, + type: "request", + command: CommandNames.CompilerOptionsForInferredProjects, + arguments: { + options: { + allowJs: true, + target: ScriptTarget.ESNext } - }); - - const projectService = session.getProjectService(); - checkNumberOfProjects(projectService, { inferredProjects: 3 }); - checkProjectActualFiles(projectService.inferredProjects[0], [file4.path]); - checkProjectActualFiles(projectService.inferredProjects[1], [file1.path, file2.path]); - checkProjectActualFiles(projectService.inferredProjects[2], [file3.path]); - assert.equal(projectService.inferredProjects[0].getCompilationSettings().target, ScriptTarget.ESNext); - assert.equal(projectService.inferredProjects[1].getCompilationSettings().target, ScriptTarget.ESNext); - assert.equal(projectService.inferredProjects[2].getCompilationSettings().target, ScriptTarget.ES2015); + } + })); + session.executeCommand(({ + seq: 2, + type: "request", + command: CommandNames.CompilerOptionsForInferredProjects, + arguments: { + options: { + allowJs: true, + target: ScriptTarget.ES2015 + }, + projectRootPath: "/b" + } + })); + session.executeCommand(({ + seq: 3, + type: "request", + command: CommandNames.Open, + arguments: { + file: file1.path, + fileContent: file1.content, + scriptKindName: "JS", + projectRootPath: file1.projectRootPath + } + })); + session.executeCommand(({ + seq: 4, + type: "request", + command: CommandNames.Open, + arguments: { + file: file2.path, + fileContent: file2.content, + scriptKindName: "JS", + projectRootPath: file2.projectRootPath + } + })); + session.executeCommand(({ + seq: 5, + type: "request", + command: CommandNames.Open, + arguments: { + file: file3.path, + fileContent: file3.content, + scriptKindName: "JS", + projectRootPath: file3.projectRootPath + } + })); + session.executeCommand(({ + seq: 6, + type: "request", + command: CommandNames.Open, + arguments: { + file: file4.path, + fileContent: file4.content, + scriptKindName: "JS" + } + })); + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { inferredProjects: 3 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file4.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [file1.path, file2.path]); + checkProjectActualFiles(projectService.inferredProjects[2], [file3.path]); + assert.equal(projectService.inferredProjects[0].getCompilationSettings().target, ScriptTarget.ESNext); + assert.equal(projectService.inferredProjects[1].getCompilationSettings().target, ScriptTarget.ESNext); + assert.equal(projectService.inferredProjects[2].getCompilationSettings().target, ScriptTarget.ES2015); + }); + function checkInferredProject(inferredProject: InferredProject, actualFiles: File[], target: ScriptTarget) { + checkProjectActualFiles(inferredProject, actualFiles.map(f => f.path)); + assert.equal(inferredProject.getCompilationSettings().target, target); + } + function verifyProjectRootWithCaseSensitivity(useCaseSensitiveFileNames: boolean) { + const files: [File, File, File, File] = [ + { path: "/a/file1.ts", content: "let x = 1;" }, + { path: "/A/file2.ts", content: "let y = 2;" }, + { path: "/b/file2.ts", content: "let x = 3;" }, + { path: "/c/file3.ts", content: "let z = 4;" } + ]; + const host = createServerHost(files, { useCaseSensitiveFileNames }); + const projectService = createProjectService(host, { useSingleInferredProject: true, }, { useInferredProjectPerProjectRoot: true }); + projectService.setCompilerOptionsForInferredProjects({ + allowJs: true, + target: ScriptTarget.ESNext }); - - function checkInferredProject(inferredProject: server.InferredProject, actualFiles: File[], target: ScriptTarget) { - checkProjectActualFiles(inferredProject, actualFiles.map(f => f.path)); - assert.equal(inferredProject.getCompilationSettings().target, target); + projectService.setCompilerOptionsForInferredProjects({ + allowJs: true, + target: ScriptTarget.ES2015 + }, "/a"); + openClientFiles(["/a", "/a", "/b", undefined]); + verifyInferredProjectsState([ + [[files[3]], ScriptTarget.ESNext], + [[files[0], files[1]], ScriptTarget.ES2015], + [[files[2]], ScriptTarget.ESNext] + ]); + closeClientFiles(); + openClientFiles(["/a", "/A", "/b", undefined]); + if (useCaseSensitiveFileNames) { + verifyInferredProjectsState([ + [[files[3]], ScriptTarget.ESNext], + [[files[0]], ScriptTarget.ES2015], + [[files[1]], ScriptTarget.ESNext], + [[files[2]], ScriptTarget.ESNext] + ]); } - - function verifyProjectRootWithCaseSensitivity(useCaseSensitiveFileNames: boolean) { - const files: [File, File, File, File] = [ - { path: "/a/file1.ts", content: "let x = 1;" }, - { path: "/A/file2.ts", content: "let y = 2;" }, - { path: "/b/file2.ts", content: "let x = 3;" }, - { path: "/c/file3.ts", content: "let z = 4;" } - ]; - const host = createServerHost(files, { useCaseSensitiveFileNames }); - const projectService = createProjectService(host, { useSingleInferredProject: true, }, { useInferredProjectPerProjectRoot: true }); - projectService.setCompilerOptionsForInferredProjects({ - allowJs: true, - target: ScriptTarget.ESNext - }); - projectService.setCompilerOptionsForInferredProjects({ - allowJs: true, - target: ScriptTarget.ES2015 - }, "/a"); - - openClientFiles(["/a", "/a", "/b", undefined]); + else { verifyInferredProjectsState([ [[files[3]], ScriptTarget.ESNext], [[files[0], files[1]], ScriptTarget.ES2015], [[files[2]], ScriptTarget.ESNext] ]); - closeClientFiles(); - - openClientFiles(["/a", "/A", "/b", undefined]); - if (useCaseSensitiveFileNames) { - verifyInferredProjectsState([ - [[files[3]], ScriptTarget.ESNext], - [[files[0]], ScriptTarget.ES2015], - [[files[1]], ScriptTarget.ESNext], - [[files[2]], ScriptTarget.ESNext] - ]); - } - else { - verifyInferredProjectsState([ - [[files[3]], ScriptTarget.ESNext], - [[files[0], files[1]], ScriptTarget.ES2015], - [[files[2]], ScriptTarget.ESNext] - ]); - } - closeClientFiles(); - - projectService.setCompilerOptionsForInferredProjects({ - allowJs: true, - target: ScriptTarget.ES2017 - }, "/A"); - - openClientFiles(["/a", "/a", "/b", undefined]); + } + closeClientFiles(); + projectService.setCompilerOptionsForInferredProjects({ + allowJs: true, + target: ScriptTarget.ES2017 + }, "/A"); + openClientFiles(["/a", "/a", "/b", undefined]); + verifyInferredProjectsState([ + [[files[3]], ScriptTarget.ESNext], + [[files[0], files[1]], useCaseSensitiveFileNames ? ScriptTarget.ES2015 : ScriptTarget.ES2017], + [[files[2]], ScriptTarget.ESNext] + ]); + closeClientFiles(); + openClientFiles(["/a", "/A", "/b", undefined]); + if (useCaseSensitiveFileNames) { verifyInferredProjectsState([ [[files[3]], ScriptTarget.ESNext], - [[files[0], files[1]], useCaseSensitiveFileNames ? ScriptTarget.ES2015 : ScriptTarget.ES2017], + [[files[0]], ScriptTarget.ES2015], + [[files[1]], ScriptTarget.ES2017], [[files[2]], ScriptTarget.ESNext] ]); - closeClientFiles(); - - openClientFiles(["/a", "/A", "/b", undefined]); - if (useCaseSensitiveFileNames) { - verifyInferredProjectsState([ - [[files[3]], ScriptTarget.ESNext], - [[files[0]], ScriptTarget.ES2015], - [[files[1]], ScriptTarget.ES2017], - [[files[2]], ScriptTarget.ESNext] - ]); - } - else { - verifyInferredProjectsState([ - [[files[3]], ScriptTarget.ESNext], - [[files[0], files[1]], ScriptTarget.ES2017], - [[files[2]], ScriptTarget.ESNext] - ]); - } - closeClientFiles(); - - function openClientFiles(projectRoots: [string | undefined, string | undefined, string | undefined, string | undefined]) { - files.forEach((file, index) => { - projectService.openClientFile(file.path, file.content, ScriptKind.JS, projectRoots[index]); - }); - } - - function closeClientFiles() { - files.forEach(file => projectService.closeClientFile(file.path)); - } - - function verifyInferredProjectsState(expected: [File[], ScriptTarget][]) { - checkNumberOfProjects(projectService, { inferredProjects: expected.length }); - projectService.inferredProjects.forEach((p, index) => { - const [actualFiles, target] = expected[index]; - checkInferredProject(p, actualFiles, target); - }); - } } - - it("inferred projects per project root with case sensitive system", () => { - verifyProjectRootWithCaseSensitivity(/*useCaseSensitiveFileNames*/ true); - }); - - it("inferred projects per project root with case insensitive system", () => { - verifyProjectRootWithCaseSensitivity(/*useCaseSensitiveFileNames*/ false); - }); - - it("should still retain configured project created while opening the file", () => { - const appFile: File = { - path: `${tscWatch.projectRoot}/app.ts`, - content: `const app = 20;` - }; - const config: File = { - path: `${tscWatch.projectRoot}/tsconfig.json`, - content: "{}" - }; - const jsFile1: File = { - path: `${tscWatch.projectRoot}/jsFile1.js`, - content: `const jsFile1 = 10;` - }; - const jsFile2: File = { - path: `${tscWatch.projectRoot}/jsFile2.js`, - content: `const jsFile2 = 10;` - }; - const host = createServerHost([appFile, libFile, config, jsFile1, jsFile2]); - const projectService = createProjectService(host); - const originalSet = projectService.configuredProjects.set; - const originalDelete = projectService.configuredProjects.delete; - const configuredCreated = createMap(); - const configuredRemoved = createMap(); - projectService.configuredProjects.set = (key, value) => { - assert.isFalse(configuredCreated.has(key)); - configuredCreated.set(key, true); - return originalSet.call(projectService.configuredProjects, key, value); - }; - projectService.configuredProjects.delete = key => { - assert.isFalse(configuredRemoved.has(key)); - configuredRemoved.set(key, true); - return originalDelete.call(projectService.configuredProjects, key); - }; - - // Do not remove config project when opening jsFile that is not present as part of config project - projectService.openClientFile(jsFile1.path); - checkNumberOfProjects(projectService, { inferredProjects: 1, configuredProjects: 1 }); - checkProjectActualFiles(projectService.inferredProjects[0], [jsFile1.path, libFile.path]); - const project = projectService.configuredProjects.get(config.path)!; - checkProjectActualFiles(project, [appFile.path, config.path, libFile.path]); - checkConfiguredProjectCreatedAndNotDeleted(); - - // Do not remove config project when opening jsFile that is not present as part of config project - projectService.closeClientFile(jsFile1.path); - checkNumberOfProjects(projectService, { inferredProjects: 1, configuredProjects: 1 }); - projectService.openClientFile(jsFile2.path); - checkNumberOfProjects(projectService, { inferredProjects: 1, configuredProjects: 1 }); - checkProjectActualFiles(projectService.inferredProjects[0], [jsFile2.path, libFile.path]); - checkProjectActualFiles(project, [appFile.path, config.path, libFile.path]); - checkConfiguredProjectNotCreatedAndNotDeleted(); - - // Do not remove config project when opening jsFile that is not present as part of config project - projectService.openClientFile(jsFile1.path); - checkNumberOfProjects(projectService, { inferredProjects: 2, configuredProjects: 1 }); - checkProjectActualFiles(projectService.inferredProjects[0], [jsFile2.path, libFile.path]); - checkProjectActualFiles(projectService.inferredProjects[1], [jsFile1.path, libFile.path]); - checkProjectActualFiles(project, [appFile.path, config.path, libFile.path]); - checkConfiguredProjectNotCreatedAndNotDeleted(); - - // When opening file that doesnt fall back to the config file, we remove the config project - projectService.openClientFile(libFile.path); - checkNumberOfProjects(projectService, { inferredProjects: 2 }); - checkProjectActualFiles(projectService.inferredProjects[0], [jsFile2.path, libFile.path]); - checkProjectActualFiles(projectService.inferredProjects[1], [jsFile1.path, libFile.path]); - checkConfiguredProjectNotCreatedButDeleted(); - - function checkConfiguredProjectCreatedAndNotDeleted() { - assert.equal(configuredCreated.size, 1); - assert.isTrue(configuredCreated.has(config.path)); - assert.equal(configuredRemoved.size, 0); - configuredCreated.clear(); - } - - function checkConfiguredProjectNotCreatedAndNotDeleted() { - assert.equal(configuredCreated.size, 0); - assert.equal(configuredRemoved.size, 0); - } - - function checkConfiguredProjectNotCreatedButDeleted() { - assert.equal(configuredCreated.size, 0); - assert.equal(configuredRemoved.size, 1); - assert.isTrue(configuredRemoved.has(config.path)); - configuredRemoved.clear(); - } - }); + else { + verifyInferredProjectsState([ + [[files[3]], ScriptTarget.ESNext], + [[files[0], files[1]], ScriptTarget.ES2017], + [[files[2]], ScriptTarget.ESNext] + ]); + } + closeClientFiles(); + function openClientFiles(projectRoots: [string | undefined, string | undefined, string | undefined, string | undefined]) { + files.forEach((file, index) => { + projectService.openClientFile(file.path, file.content, ScriptKind.JS, projectRoots[index]); + }); + } + function closeClientFiles() { + files.forEach(file => projectService.closeClientFile(file.path)); + } + function verifyInferredProjectsState(expected: [File[], ScriptTarget][]) { + checkNumberOfProjects(projectService, { inferredProjects: expected.length }); + projectService.inferredProjects.forEach((p, index) => { + const [actualFiles, target] = expected[index]; + checkInferredProject(p, actualFiles, target); + }); + } + } + it("inferred projects per project root with case sensitive system", () => { + verifyProjectRootWithCaseSensitivity(/*useCaseSensitiveFileNames*/ true); + }); + it("inferred projects per project root with case insensitive system", () => { + verifyProjectRootWithCaseSensitivity(/*useCaseSensitiveFileNames*/ false); + }); + it("should still retain configured project created while opening the file", () => { + const appFile: File = { + path: `${projectRoot}/app.ts`, + content: `const app = 20;` + }; + const config: File = { + path: `${projectRoot}/tsconfig.json`, + content: "{}" + }; + const jsFile1: File = { + path: `${projectRoot}/jsFile1.js`, + content: `const jsFile1 = 10;` + }; + const jsFile2: File = { + path: `${projectRoot}/jsFile2.js`, + content: `const jsFile2 = 10;` + }; + const host = createServerHost([appFile, libFile, config, jsFile1, jsFile2]); + const projectService = createProjectService(host); + const originalSet = projectService.configuredProjects.set; + const originalDelete = projectService.configuredProjects.delete; + const configuredCreated = createMap(); + const configuredRemoved = createMap(); + projectService.configuredProjects.set = (key, value) => { + assert.isFalse(configuredCreated.has(key)); + configuredCreated.set(key, true); + return originalSet.call(projectService.configuredProjects, key, value); + }; + projectService.configuredProjects.delete = key => { + assert.isFalse(configuredRemoved.has(key)); + configuredRemoved.set(key, true); + return originalDelete.call(projectService.configuredProjects, key); + }; + // Do not remove config project when opening jsFile that is not present as part of config project + projectService.openClientFile(jsFile1.path); + checkNumberOfProjects(projectService, { inferredProjects: 1, configuredProjects: 1 }); + checkProjectActualFiles(projectService.inferredProjects[0], [jsFile1.path, libFile.path]); + const project = projectService.configuredProjects.get(config.path)!; + checkProjectActualFiles(project, [appFile.path, config.path, libFile.path]); + checkConfiguredProjectCreatedAndNotDeleted(); + // Do not remove config project when opening jsFile that is not present as part of config project + projectService.closeClientFile(jsFile1.path); + checkNumberOfProjects(projectService, { inferredProjects: 1, configuredProjects: 1 }); + projectService.openClientFile(jsFile2.path); + checkNumberOfProjects(projectService, { inferredProjects: 1, configuredProjects: 1 }); + checkProjectActualFiles(projectService.inferredProjects[0], [jsFile2.path, libFile.path]); + checkProjectActualFiles(project, [appFile.path, config.path, libFile.path]); + checkConfiguredProjectNotCreatedAndNotDeleted(); + // Do not remove config project when opening jsFile that is not present as part of config project + projectService.openClientFile(jsFile1.path); + checkNumberOfProjects(projectService, { inferredProjects: 2, configuredProjects: 1 }); + checkProjectActualFiles(projectService.inferredProjects[0], [jsFile2.path, libFile.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [jsFile1.path, libFile.path]); + checkProjectActualFiles(project, [appFile.path, config.path, libFile.path]); + checkConfiguredProjectNotCreatedAndNotDeleted(); + // When opening file that doesnt fall back to the config file, we remove the config project + projectService.openClientFile(libFile.path); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + checkProjectActualFiles(projectService.inferredProjects[0], [jsFile2.path, libFile.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [jsFile1.path, libFile.path]); + checkConfiguredProjectNotCreatedButDeleted(); + function checkConfiguredProjectCreatedAndNotDeleted() { + assert.equal(configuredCreated.size, 1); + assert.isTrue(configuredCreated.has(config.path)); + assert.equal(configuredRemoved.size, 0); + configuredCreated.clear(); + } + function checkConfiguredProjectNotCreatedAndNotDeleted() { + assert.equal(configuredCreated.size, 0); + assert.equal(configuredRemoved.size, 0); + } + function checkConfiguredProjectNotCreatedButDeleted() { + assert.equal(configuredCreated.size, 0); + assert.equal(configuredRemoved.size, 1); + assert.isTrue(configuredRemoved.has(config.path)); + configuredRemoved.clear(); + } }); -} +}); diff --git a/src/testRunner/unittests/tsserver/languageService.ts b/src/testRunner/unittests/tsserver/languageService.ts index 86d426664df7c..f239358b3ac8e 100644 --- a/src/testRunner/unittests/tsserver/languageService.ts +++ b/src/testRunner/unittests/tsserver/languageService.ts @@ -1,19 +1,18 @@ -namespace ts.projectSystem { - describe("unittests:: tsserver:: Language service", () => { - it("should work correctly on case-sensitive file systems", () => { - const lib = { - path: "/a/Lib/lib.d.ts", - content: "let x: number" - }; - const f = { - path: "/a/b/app.ts", - content: "let x = 1;" - }; - const host = createServerHost([lib, f], { executingFilePath: "/a/Lib/tsc.js", useCaseSensitiveFileNames: true }); - const projectService = createProjectService(host); - projectService.openClientFile(f.path); - projectService.checkNumberOfProjects({ inferredProjects: 1 }); - projectService.inferredProjects[0].getLanguageService().getProgram(); - }); +import { createServerHost, createProjectService } from "../../ts.projectSystem"; +describe("unittests:: tsserver:: Language service", () => { + it("should work correctly on case-sensitive file systems", () => { + const lib = { + path: "/a/Lib/lib.d.ts", + content: "let x: number" + }; + const f = { + path: "/a/b/app.ts", + content: "let x = 1;" + }; + const host = createServerHost([lib, f], { executingFilePath: "/a/Lib/tsc.js", useCaseSensitiveFileNames: true }); + const projectService = createProjectService(host); + projectService.openClientFile(f.path); + projectService.checkNumberOfProjects({ inferredProjects: 1 }); + projectService.inferredProjects[0].getLanguageService().getProgram(); }); -} +}); diff --git a/src/testRunner/unittests/tsserver/maxNodeModuleJsDepth.ts b/src/testRunner/unittests/tsserver/maxNodeModuleJsDepth.ts index eb254789d671e..e266e72268f47 100644 --- a/src/testRunner/unittests/tsserver/maxNodeModuleJsDepth.ts +++ b/src/testRunner/unittests/tsserver/maxNodeModuleJsDepth.ts @@ -1,55 +1,47 @@ -namespace ts.projectSystem { - describe("unittests:: tsserver:: maxNodeModuleJsDepth for inferred projects", () => { - it("should be set to 2 if the project has js root files", () => { - const file1: File = { - path: "/a/b/file1.js", - content: `var t = require("test"); t.` - }; - const moduleFile: File = { - path: "/a/b/node_modules/test/index.js", - content: `var v = 10; module.exports = v;` - }; - - const host = createServerHost([file1, moduleFile]); - const projectService = createProjectService(host); - projectService.openClientFile(file1.path); - - let project = projectService.inferredProjects[0]; - let options = project.getCompilationSettings(); - assert.isTrue(options.maxNodeModuleJsDepth === 2); - - // Assert the option sticks - projectService.setCompilerOptionsForInferredProjects({ target: ScriptTarget.ES2016 }); - project = projectService.inferredProjects[0]; - options = project.getCompilationSettings(); - assert.isTrue(options.maxNodeModuleJsDepth === 2); - }); - - it("should return to normal state when all js root files are removed from project", () => { - const file1 = { - path: "/a/file1.ts", - content: "let x =1;" - }; - const file2 = { - path: "/a/file2.js", - content: "let x =1;" - }; - - const host = createServerHost([file1, file2, libFile]); - const projectService = createProjectService(host, { useSingleInferredProject: true }); - - projectService.openClientFile(file1.path); - checkNumberOfInferredProjects(projectService, 1); - let project = projectService.inferredProjects[0]; - assert.isUndefined(project.getCompilationSettings().maxNodeModuleJsDepth); - - projectService.openClientFile(file2.path); - project = projectService.inferredProjects[0]; - assert.isTrue(project.getCompilationSettings().maxNodeModuleJsDepth === 2); - - projectService.closeClientFile(file2.path); - project = projectService.inferredProjects[0]; - assert.isUndefined(project.getCompilationSettings().maxNodeModuleJsDepth); - }); +import { File, createServerHost, createProjectService, libFile, checkNumberOfInferredProjects } from "../../ts.projectSystem"; +import { ScriptTarget } from "../../ts"; +describe("unittests:: tsserver:: maxNodeModuleJsDepth for inferred projects", () => { + it("should be set to 2 if the project has js root files", () => { + const file1: File = { + path: "/a/b/file1.js", + content: `var t = require("test"); t.` + }; + const moduleFile: File = { + path: "/a/b/node_modules/test/index.js", + content: `var v = 10; module.exports = v;` + }; + const host = createServerHost([file1, moduleFile]); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + let project = projectService.inferredProjects[0]; + let options = project.getCompilationSettings(); + assert.isTrue(options.maxNodeModuleJsDepth === 2); + // Assert the option sticks + projectService.setCompilerOptionsForInferredProjects({ target: ScriptTarget.ES2016 }); + project = projectService.inferredProjects[0]; + options = project.getCompilationSettings(); + assert.isTrue(options.maxNodeModuleJsDepth === 2); }); -} + it("should return to normal state when all js root files are removed from project", () => { + const file1 = { + path: "/a/file1.ts", + content: "let x =1;" + }; + const file2 = { + path: "/a/file2.js", + content: "let x =1;" + }; + const host = createServerHost([file1, file2, libFile]); + const projectService = createProjectService(host, { useSingleInferredProject: true }); + projectService.openClientFile(file1.path); + checkNumberOfInferredProjects(projectService, 1); + let project = projectService.inferredProjects[0]; + assert.isUndefined(project.getCompilationSettings().maxNodeModuleJsDepth); + projectService.openClientFile(file2.path); + project = projectService.inferredProjects[0]; + assert.isTrue(project.getCompilationSettings().maxNodeModuleJsDepth === 2); + projectService.closeClientFile(file2.path); + project = projectService.inferredProjects[0]; + assert.isUndefined(project.getCompilationSettings().maxNodeModuleJsDepth); + }); +}); diff --git a/src/testRunner/unittests/tsserver/metadataInResponse.ts b/src/testRunner/unittests/tsserver/metadataInResponse.ts index 10b1c58d737a9..e7c243ac6eafe 100644 --- a/src/testRunner/unittests/tsserver/metadataInResponse.ts +++ b/src/testRunner/unittests/tsserver/metadataInResponse.ts @@ -1,99 +1,94 @@ -namespace ts.projectSystem { - describe("unittests:: tsserver:: with metadata in response", () => { - const metadata = "Extra Info"; - function verifyOutput(host: TestServerHost, expectedResponse: protocol.Response) { - const output = host.getOutput().map(mapOutputToJson); - assert.deepEqual(output, [expectedResponse]); - host.clearOutput(); - } - - function verifyCommandWithMetadata(session: TestSession, host: TestServerHost, command: Partial, expectedResponseBody: U) { - command.seq = session.getSeq(); - command.type = "request"; - session.onMessage(JSON.stringify(command)); - verifyOutput(host, expectedResponseBody ? - { seq: 0, type: "response", command: command.command!, request_seq: command.seq, success: true, body: expectedResponseBody, metadata } : - { seq: 0, type: "response", command: command.command!, request_seq: command.seq, success: false, message: "No content available." } - ); - } - - const aTs: File = { path: "/a.ts", content: `class c { prop = "hello"; foo() { return this.prop; } }` }; - const tsconfig: File = { - path: "/tsconfig.json", - content: JSON.stringify({ - compilerOptions: { plugins: [{ name: "myplugin" }] } - }) - }; - function createHostWithPlugin(files: readonly File[]) { - const host = createServerHost(files); - host.require = (_initialPath, moduleName) => { - assert.equal(moduleName, "myplugin"); - return { - module: () => ({ - create(info: server.PluginCreateInfo) { - const proxy = Harness.LanguageService.makeDefaultProxy(info); - proxy.getCompletionsAtPosition = (filename, position, options) => { - const result = info.languageService.getCompletionsAtPosition(filename, position, options); - if (result) { - result.metadata = metadata; - } - return result; - }; - return proxy; - } - }), - error: undefined - }; - }; - return host; - } - - describe("With completion requests", () => { - const completionRequestArgs: protocol.CompletionsRequestArgs = { - file: aTs.path, - line: 1, - offset: aTs.content.indexOf("this.") + 1 + "this.".length +import { TestServerHost, protocol, mapOutputToJson, TestSession, File, createServerHost, createSession, openFilesForSession } from "../../ts.projectSystem"; +import { server, ScriptElementKind } from "../../ts"; +import { PluginCreateInfo } from "../../ts.server"; +import { LanguageService } from "../../Harness"; +describe("unittests:: tsserver:: with metadata in response", () => { + const metadata = "Extra Info"; + function verifyOutput(host: TestServerHost, expectedResponse: protocol.Response) { + const output = host.getOutput().map(mapOutputToJson); + assert.deepEqual(output, [expectedResponse]); + host.clearOutput(); + } + function verifyCommandWithMetadata(session: TestSession, host: TestServerHost, command: Partial, expectedResponseBody: U) { + command.seq = session.getSeq(); + command.type = "request"; + session.onMessage(JSON.stringify(command)); + verifyOutput(host, expectedResponseBody ? + { seq: 0, type: "response", command: command.command!, request_seq: command.seq, success: true, body: expectedResponseBody, metadata } : + { seq: 0, type: "response", command: command.command!, request_seq: command.seq, success: false, message: "No content available." }); + } + const aTs: File = { path: "/a.ts", content: `class c { prop = "hello"; foo() { return this.prop; } }` }; + const tsconfig: File = { + path: "/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { plugins: [{ name: "myplugin" }] } + }) + }; + function createHostWithPlugin(files: readonly File[]) { + const host = createServerHost(files); + host.require = (_initialPath, moduleName) => { + assert.equal(moduleName, "myplugin"); + return { + module: () => ({ + create(info: PluginCreateInfo) { + const proxy = LanguageService.makeDefaultProxy(info); + proxy.getCompletionsAtPosition = (filename, position, options) => { + const result = info.languageService.getCompletionsAtPosition(filename, position, options); + if (result) { + result.metadata = metadata; + } + return result; + }; + return proxy; + } + }), + error: undefined }; - const expectedCompletionEntries: readonly protocol.CompletionEntry[] = [ - { name: "foo", kind: ScriptElementKind.memberFunctionElement, kindModifiers: "", sortText: "0" }, - { name: "prop", kind: ScriptElementKind.memberVariableElement, kindModifiers: "", sortText: "0" } - ]; - - it("can pass through metadata when the command returns array", () => { - const host = createHostWithPlugin([aTs, tsconfig]); - const session = createSession(host); - openFilesForSession([aTs], session); - verifyCommandWithMetadata(session, host, { - command: protocol.CommandTypes.Completions, - arguments: completionRequestArgs - }, expectedCompletionEntries); - }); - - it("can pass through metadata when the command returns object", () => { - const host = createHostWithPlugin([aTs, tsconfig]); - const session = createSession(host); - openFilesForSession([aTs], session); - verifyCommandWithMetadata(session, host, { - command: protocol.CommandTypes.CompletionInfo, - arguments: completionRequestArgs - }, { - isGlobalCompletion: false, - isMemberCompletion: true, - isNewIdentifierLocation: false, - entries: expectedCompletionEntries - }); - }); - - it("returns undefined correctly", () => { - const aTs: File = { path: "/a.ts", content: `class c { prop = "hello"; foo() { const x = 0; } }` }; - const host = createHostWithPlugin([aTs, tsconfig]); - const session = createSession(host); - openFilesForSession([aTs], session); - verifyCommandWithMetadata(session, host, { - command: protocol.CommandTypes.Completions, - arguments: { file: aTs.path, line: 1, offset: aTs.content.indexOf("x") + 1 } - }, /*expectedResponseBody*/ undefined); + }; + return host; + } + describe("With completion requests", () => { + const completionRequestArgs: protocol.CompletionsRequestArgs = { + file: aTs.path, + line: 1, + offset: aTs.content.indexOf("this.") + 1 + "this.".length + }; + const expectedCompletionEntries: readonly protocol.CompletionEntry[] = [ + { name: "foo", kind: ScriptElementKind.memberFunctionElement, kindModifiers: "", sortText: "0" }, + { name: "prop", kind: ScriptElementKind.memberVariableElement, kindModifiers: "", sortText: "0" } + ]; + it("can pass through metadata when the command returns array", () => { + const host = createHostWithPlugin([aTs, tsconfig]); + const session = createSession(host); + openFilesForSession([aTs], session); + verifyCommandWithMetadata(session, host, { + command: protocol.CommandTypes.Completions, + arguments: completionRequestArgs + }, expectedCompletionEntries); + }); + it("can pass through metadata when the command returns object", () => { + const host = createHostWithPlugin([aTs, tsconfig]); + const session = createSession(host); + openFilesForSession([aTs], session); + verifyCommandWithMetadata(session, host, { + command: protocol.CommandTypes.CompletionInfo, + arguments: completionRequestArgs + }, { + isGlobalCompletion: false, + isMemberCompletion: true, + isNewIdentifierLocation: false, + entries: expectedCompletionEntries }); }); + it("returns undefined correctly", () => { + const aTs: File = { path: "/a.ts", content: `class c { prop = "hello"; foo() { const x = 0; } }` }; + const host = createHostWithPlugin([aTs, tsconfig]); + const session = createSession(host); + openFilesForSession([aTs], session); + verifyCommandWithMetadata(session, host, { + command: protocol.CommandTypes.Completions, + arguments: { file: aTs.path, line: 1, offset: aTs.content.indexOf("x") + 1 } + }, /*expectedResponseBody*/ undefined); + }); }); -} +}); diff --git a/src/testRunner/unittests/tsserver/navTo.ts b/src/testRunner/unittests/tsserver/navTo.ts index 0ce98a076de3f..edecf6e286789 100644 --- a/src/testRunner/unittests/tsserver/navTo.ts +++ b/src/testRunner/unittests/tsserver/navTo.ts @@ -1,48 +1,45 @@ -namespace ts.projectSystem { - describe("unittests:: tsserver:: navigate-to for javascript project", () => { - function containsNavToItem(items: protocol.NavtoItem[], itemName: string, itemKind: string) { - return find(items, item => item.name === itemName && item.kind === itemKind) !== undefined; - } - - it("should not include type symbols", () => { - const file1: File = { - path: "/a/b/file1.js", - content: "function foo() {}" - }; - const configFile: File = { - path: "/a/b/jsconfig.json", - content: "{}" - }; - const host = createServerHost([file1, configFile, libFile]); - const session = createSession(host); - openFilesForSession([file1], session); - - // Try to find some interface type defined in lib.d.ts - const libTypeNavToRequest = makeSessionRequest(CommandNames.Navto, { searchValue: "Document", file: file1.path, projectFileName: configFile.path }); - const items = session.executeCommand(libTypeNavToRequest).response as protocol.NavtoItem[]; - assert.isFalse(containsNavToItem(items, "Document", "interface"), `Found lib.d.ts symbol in JavaScript project nav to request result.`); - - const localFunctionNavToRequst = makeSessionRequest(CommandNames.Navto, { searchValue: "foo", file: file1.path, projectFileName: configFile.path }); - const items2 = session.executeCommand(localFunctionNavToRequst).response as protocol.NavtoItem[]; - assert.isTrue(containsNavToItem(items2, "foo", "function"), `Cannot find function symbol "foo".`); - }); - - it("should de-duplicate symbols", () => { - const configFile1: File = { - path: "/a/tsconfig.json", - content: `{ +import { protocol, File, createServerHost, libFile, createSession, openFilesForSession, makeSessionRequest, CommandNames } from "../../ts.projectSystem"; +import { find } from "../../ts"; +describe("unittests:: tsserver:: navigate-to for javascript project", () => { + function containsNavToItem(items: protocol.NavtoItem[], itemName: string, itemKind: string) { + return find(items, item => item.name === itemName && item.kind === itemKind) !== undefined; + } + it("should not include type symbols", () => { + const file1: File = { + path: "/a/b/file1.js", + content: "function foo() {}" + }; + const configFile: File = { + path: "/a/b/jsconfig.json", + content: "{}" + }; + const host = createServerHost([file1, configFile, libFile]); + const session = createSession(host); + openFilesForSession([file1], session); + // Try to find some interface type defined in lib.d.ts + const libTypeNavToRequest = makeSessionRequest(CommandNames.Navto, { searchValue: "Document", file: file1.path, projectFileName: configFile.path }); + const items = (session.executeCommand(libTypeNavToRequest).response as protocol.NavtoItem[]); + assert.isFalse(containsNavToItem(items, "Document", "interface"), `Found lib.d.ts symbol in JavaScript project nav to request result.`); + const localFunctionNavToRequst = makeSessionRequest(CommandNames.Navto, { searchValue: "foo", file: file1.path, projectFileName: configFile.path }); + const items2 = (session.executeCommand(localFunctionNavToRequst).response as protocol.NavtoItem[]); + assert.isTrue(containsNavToItem(items2, "foo", "function"), `Cannot find function symbol "foo".`); + }); + it("should de-duplicate symbols", () => { + const configFile1: File = { + path: "/a/tsconfig.json", + content: `{ "compilerOptions": { "composite": true } }` - }; - const file1: File = { - path: "/a/index.ts", - content: "export const abcdef = 1;" - }; - const configFile2: File = { - path: "/b/tsconfig.json", - content: `{ + }; + const file1: File = { + path: "/a/index.ts", + content: "export const abcdef = 1;" + }; + const configFile2: File = { + path: "/b/tsconfig.json", + content: `{ "compilerOptions": { "composite": true }, @@ -50,22 +47,20 @@ namespace ts.projectSystem { { "path": "../a" } ] }` - }; - const file2: File = { - path: "/b/index.ts", - content: `import a = require("../a"); + }; + const file2: File = { + path: "/b/index.ts", + content: `import a = require("../a"); export const ghijkl = a.abcdef;` - }; - const host = createServerHost([configFile1, file1, configFile2, file2]); - const session = createSession(host); - openFilesForSession([file1, file2], session); - - const request = makeSessionRequest(CommandNames.Navto, { searchValue: "abcdef", file: file1.path }); - const items = session.executeCommand(request).response as protocol.NavtoItem[]; - assert.strictEqual(items.length, 1); - const item = items[0]; - assert.strictEqual(item.name, "abcdef"); - assert.strictEqual(item.file, file1.path); - }); + }; + const host = createServerHost([configFile1, file1, configFile2, file2]); + const session = createSession(host); + openFilesForSession([file1, file2], session); + const request = makeSessionRequest(CommandNames.Navto, { searchValue: "abcdef", file: file1.path }); + const items = (session.executeCommand(request).response as protocol.NavtoItem[]); + assert.strictEqual(items.length, 1); + const item = items[0]; + assert.strictEqual(item.name, "abcdef"); + assert.strictEqual(item.file, file1.path); }); -} +}); diff --git a/src/testRunner/unittests/tsserver/occurences.ts b/src/testRunner/unittests/tsserver/occurences.ts index 25dfb3c9e90f2..8c7d68fd9b43b 100644 --- a/src/testRunner/unittests/tsserver/occurences.ts +++ b/src/testRunner/unittests/tsserver/occurences.ts @@ -1,47 +1,33 @@ -namespace ts.projectSystem { - describe("unittests:: tsserver:: occurrence highlight on string", () => { - it("should be marked if only on string values", () => { - const file1: File = { - path: "/a/b/file1.ts", - content: `let t1 = "div";\nlet t2 = "div";\nlet t3 = { "div": 123 };\nlet t4 = t3["div"];` - }; - - const host = createServerHost([file1]); - const session = createSession(host); - const projectService = session.getProjectService(); - - projectService.openClientFile(file1.path); - { - const highlightRequest = makeSessionRequest( - CommandNames.Occurrences, - { file: file1.path, line: 1, offset: 11 } - ); - const highlightResponse = session.executeCommand(highlightRequest).response as protocol.OccurrencesResponseItem[]; - const firstOccurence = highlightResponse[0]; - assert.isTrue(firstOccurence.isInString, "Highlights should be marked with isInString"); - } - - { - const highlightRequest = makeSessionRequest( - CommandNames.Occurrences, - { file: file1.path, line: 3, offset: 13 } - ); - const highlightResponse = session.executeCommand(highlightRequest).response as protocol.OccurrencesResponseItem[]; - assert.isTrue(highlightResponse.length === 2); - const firstOccurence = highlightResponse[0]; - assert.isUndefined(firstOccurence.isInString, "Highlights should not be marked with isInString if on property name"); - } - - { - const highlightRequest = makeSessionRequest( - CommandNames.Occurrences, - { file: file1.path, line: 4, offset: 14 } - ); - const highlightResponse = session.executeCommand(highlightRequest).response as protocol.OccurrencesResponseItem[]; - assert.isTrue(highlightResponse.length === 2); - const firstOccurence = highlightResponse[0]; - assert.isUndefined(firstOccurence.isInString, "Highlights should not be marked with isInString if on indexer"); - } - }); +import { File, createServerHost, createSession, makeSessionRequest, protocol, CommandNames } from "../../ts.projectSystem"; +describe("unittests:: tsserver:: occurrence highlight on string", () => { + it("should be marked if only on string values", () => { + const file1: File = { + path: "/a/b/file1.ts", + content: `let t1 = "div";\nlet t2 = "div";\nlet t3 = { "div": 123 };\nlet t4 = t3["div"];` + }; + const host = createServerHost([file1]); + const session = createSession(host); + const projectService = session.getProjectService(); + projectService.openClientFile(file1.path); + { + const highlightRequest = makeSessionRequest(CommandNames.Occurrences, { file: file1.path, line: 1, offset: 11 }); + const highlightResponse = (session.executeCommand(highlightRequest).response as protocol.OccurrencesResponseItem[]); + const firstOccurence = highlightResponse[0]; + assert.isTrue(firstOccurence.isInString, "Highlights should be marked with isInString"); + } + { + const highlightRequest = makeSessionRequest(CommandNames.Occurrences, { file: file1.path, line: 3, offset: 13 }); + const highlightResponse = (session.executeCommand(highlightRequest).response as protocol.OccurrencesResponseItem[]); + assert.isTrue(highlightResponse.length === 2); + const firstOccurence = highlightResponse[0]; + assert.isUndefined(firstOccurence.isInString, "Highlights should not be marked with isInString if on property name"); + } + { + const highlightRequest = makeSessionRequest(CommandNames.Occurrences, { file: file1.path, line: 4, offset: 14 }); + const highlightResponse = (session.executeCommand(highlightRequest).response as protocol.OccurrencesResponseItem[]); + assert.isTrue(highlightResponse.length === 2); + const firstOccurence = highlightResponse[0]; + assert.isUndefined(firstOccurence.isInString, "Highlights should not be marked with isInString if on indexer"); + } }); -} +}); diff --git a/src/testRunner/unittests/tsserver/openFile.ts b/src/testRunner/unittests/tsserver/openFile.ts index 62a7fcc729ac8..b3baef0818486 100644 --- a/src/testRunner/unittests/tsserver/openFile.ts +++ b/src/testRunner/unittests/tsserver/openFile.ts @@ -1,132 +1,119 @@ -namespace ts.projectSystem { - describe("unittests:: tsserver:: Open-file", () => { - it("can be reloaded with empty content", () => { - const f = { - path: "/a/b/app.ts", - content: "let x = 1" - }; - const projectFileName = "externalProject"; - const host = createServerHost([f]); - const projectService = createProjectService(host); - // create a project - projectService.openExternalProject({ projectFileName, rootFiles: [toExternalFile(f.path)], options: {} }); - projectService.checkNumberOfProjects({ externalProjects: 1 }); - - const p = projectService.externalProjects[0]; - // force to load the content of the file - p.updateGraph(); - - const scriptInfo = p.getScriptInfo(f.path)!; - checkSnapLength(scriptInfo.getSnapshot(), f.content.length); - - // open project and replace its content with empty string - projectService.openClientFile(f.path, ""); - checkSnapLength(scriptInfo.getSnapshot(), 0); +import { createServerHost, createProjectService, toExternalFile, File, libFile, checkProjectActualFiles } from "../../ts.projectSystem"; +import { IScriptSnapshot, ScriptKind } from "../../ts"; +describe("unittests:: tsserver:: Open-file", () => { + it("can be reloaded with empty content", () => { + const f = { + path: "/a/b/app.ts", + content: "let x = 1" + }; + const projectFileName = "externalProject"; + const host = createServerHost([f]); + const projectService = createProjectService(host); + // create a project + projectService.openExternalProject({ projectFileName, rootFiles: [toExternalFile(f.path)], options: {} }); + projectService.checkNumberOfProjects({ externalProjects: 1 }); + const p = projectService.externalProjects[0]; + // force to load the content of the file + p.updateGraph(); + const scriptInfo = p.getScriptInfo(f.path)!; + checkSnapLength(scriptInfo.getSnapshot(), f.content.length); + // open project and replace its content with empty string + projectService.openClientFile(f.path, ""); + checkSnapLength(scriptInfo.getSnapshot(), 0); + }); + function checkSnapLength(snap: IScriptSnapshot, expectedLength: number) { + assert.equal(snap.getLength(), expectedLength, "Incorrect snapshot size"); + } + function verifyOpenFileWorks(useCaseSensitiveFileNames: boolean) { + const file1: File = { + path: "/a/b/src/app.ts", + content: "let x = 10;" + }; + const file2: File = { + path: "/a/B/lib/module2.ts", + content: "let z = 10;" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: "" + }; + const configFile2: File = { + path: "/a/tsconfig.json", + content: "" + }; + const host = createServerHost([file1, file2, configFile, configFile2], { + useCaseSensitiveFileNames }); - function checkSnapLength(snap: IScriptSnapshot, expectedLength: number) { - assert.equal(snap.getLength(), expectedLength, "Incorrect snapshot size"); + const service = createProjectService(host); + // Open file1 -> configFile + verifyConfigFileName(file1, "/a", configFile); + verifyConfigFileName(file1, "/a/b", configFile); + verifyConfigFileName(file1, "/a/B", configFile); + // Open file2 use root "/a/b" + verifyConfigFileName(file2, "/a", useCaseSensitiveFileNames ? configFile2 : configFile); + verifyConfigFileName(file2, "/a/b", useCaseSensitiveFileNames ? configFile2 : configFile); + verifyConfigFileName(file2, "/a/B", useCaseSensitiveFileNames ? undefined : configFile); + function verifyConfigFileName(file: File, projectRoot: string, expectedConfigFile: File | undefined) { + const { configFileName } = service.openClientFile(file.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectRoot); + assert.equal(configFileName, expectedConfigFile && expectedConfigFile.path); + service.closeClientFile(file.path); } - - function verifyOpenFileWorks(useCaseSensitiveFileNames: boolean) { - const file1: File = { - path: "/a/b/src/app.ts", - content: "let x = 10;" - }; - const file2: File = { - path: "/a/B/lib/module2.ts", - content: "let z = 10;" - }; - const configFile: File = { - path: "/a/b/tsconfig.json", - content: "" - }; - const configFile2: File = { - path: "/a/tsconfig.json", - content: "" - }; - const host = createServerHost([file1, file2, configFile, configFile2], { - useCaseSensitiveFileNames - }); - const service = createProjectService(host); - - // Open file1 -> configFile - verifyConfigFileName(file1, "/a", configFile); - verifyConfigFileName(file1, "/a/b", configFile); - verifyConfigFileName(file1, "/a/B", configFile); - - // Open file2 use root "/a/b" - verifyConfigFileName(file2, "/a", useCaseSensitiveFileNames ? configFile2 : configFile); - verifyConfigFileName(file2, "/a/b", useCaseSensitiveFileNames ? configFile2 : configFile); - verifyConfigFileName(file2, "/a/B", useCaseSensitiveFileNames ? undefined : configFile); - - function verifyConfigFileName(file: File, projectRoot: string, expectedConfigFile: File | undefined) { - const { configFileName } = service.openClientFile(file.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectRoot); - assert.equal(configFileName, expectedConfigFile && expectedConfigFile.path); - service.closeClientFile(file.path); - } + } + it("works when project root is used with case-sensitive system", () => { + verifyOpenFileWorks(/*useCaseSensitiveFileNames*/ true); + }); + it("works when project root is used with case-insensitive system", () => { + verifyOpenFileWorks(/*useCaseSensitiveFileNames*/ false); + }); + it("uses existing project even if project refresh is pending", () => { + const projectFolder = "/user/someuser/projects/myproject"; + const aFile: File = { + path: `${projectFolder}/src/a.ts`, + content: "export const x = 0;" + }; + const configFile: File = { + path: `${projectFolder}/tsconfig.json`, + content: "{}" + }; + const files = [aFile, configFile, libFile]; + const host = createServerHost(files); + const service = createProjectService(host); + service.openClientFile(aFile.path, /*fileContent*/ undefined, ScriptKind.TS, projectFolder); + verifyProject(); + const bFile: File = { + path: `${projectFolder}/src/b.ts`, + content: `export {}; declare module "./a" { export const y: number; }` + }; + files.push(bFile); + host.reloadFS(files); + service.openClientFile(bFile.path, /*fileContent*/ undefined, ScriptKind.TS, projectFolder); + verifyProject(); + function verifyProject() { + assert.isDefined(service.configuredProjects.get(configFile.path)); + const project = service.configuredProjects.get(configFile.path)!; + checkProjectActualFiles(project, files.map(f => f.path)); + } + }); + it("can open same file again", () => { + const projectFolder = "/user/someuser/projects/myproject"; + const aFile: File = { + path: `${projectFolder}/src/a.ts`, + content: "export const x = 0;" + }; + const configFile: File = { + path: `${projectFolder}/tsconfig.json`, + content: "{}" + }; + const files = [aFile, configFile, libFile]; + const host = createServerHost(files); + const service = createProjectService(host); + verifyProject(aFile.content); + verifyProject(`${aFile.content}export const y = 10;`); + function verifyProject(aFileContent: string) { + service.openClientFile(aFile.path, aFileContent, ScriptKind.TS, projectFolder); + const project = service.configuredProjects.get(configFile.path)!; + checkProjectActualFiles(project, files.map(f => f.path)); + assert.equal(project.getCurrentProgram()?.getSourceFile(aFile.path)!.text, aFileContent); } - it("works when project root is used with case-sensitive system", () => { - verifyOpenFileWorks(/*useCaseSensitiveFileNames*/ true); - }); - - it("works when project root is used with case-insensitive system", () => { - verifyOpenFileWorks(/*useCaseSensitiveFileNames*/ false); - }); - - it("uses existing project even if project refresh is pending", () => { - const projectFolder = "/user/someuser/projects/myproject"; - const aFile: File = { - path: `${projectFolder}/src/a.ts`, - content: "export const x = 0;" - }; - const configFile: File = { - path: `${projectFolder}/tsconfig.json`, - content: "{}" - }; - const files = [aFile, configFile, libFile]; - const host = createServerHost(files); - const service = createProjectService(host); - service.openClientFile(aFile.path, /*fileContent*/ undefined, ScriptKind.TS, projectFolder); - verifyProject(); - - const bFile: File = { - path: `${projectFolder}/src/b.ts`, - content: `export {}; declare module "./a" { export const y: number; }` - }; - files.push(bFile); - host.reloadFS(files); - service.openClientFile(bFile.path, /*fileContent*/ undefined, ScriptKind.TS, projectFolder); - verifyProject(); - - function verifyProject() { - assert.isDefined(service.configuredProjects.get(configFile.path)); - const project = service.configuredProjects.get(configFile.path)!; - checkProjectActualFiles(project, files.map(f => f.path)); - } - }); - - it("can open same file again", () => { - const projectFolder = "/user/someuser/projects/myproject"; - const aFile: File = { - path: `${projectFolder}/src/a.ts`, - content: "export const x = 0;" - }; - const configFile: File = { - path: `${projectFolder}/tsconfig.json`, - content: "{}" - }; - const files = [aFile, configFile, libFile]; - const host = createServerHost(files); - const service = createProjectService(host); - verifyProject(aFile.content); - verifyProject(`${aFile.content}export const y = 10;`); - - function verifyProject(aFileContent: string) { - service.openClientFile(aFile.path, aFileContent, ScriptKind.TS, projectFolder); - const project = service.configuredProjects.get(configFile.path)!; - checkProjectActualFiles(project, files.map(f => f.path)); - assert.equal(project.getCurrentProgram()?.getSourceFile(aFile.path)!.text, aFileContent); - } - }); }); -} +}); diff --git a/src/testRunner/unittests/tsserver/packageJsonInfo.ts b/src/testRunner/unittests/tsserver/packageJsonInfo.ts index 20be7ce56e3ab..67d62bd46ab0b 100644 --- a/src/testRunner/unittests/tsserver/packageJsonInfo.ts +++ b/src/testRunner/unittests/tsserver/packageJsonInfo.ts @@ -1,101 +1,92 @@ -namespace ts.projectSystem { - const tsConfig: File = { - path: "/tsconfig.json", - content: "{}" - }; - const packageJsonContent = { - dependencies: { - redux: "*" - }, - peerDependencies: { - react: "*" - }, - optionalDependencies: { - typescript: "*" - }, - devDependencies: { - webpack: "*" - } - }; - const packageJson: File = { - path: "/package.json", - content: JSON.stringify(packageJsonContent, undefined, 2) - }; - - describe("unittests:: tsserver:: packageJsonInfo", () => { - it("detects new package.json files that are added, caches them, and watches them", () => { - // Initialize project without package.json - const { project, host } = setup([tsConfig]); - assert.isUndefined(project.packageJsonCache.getInDirectory("/" as Path)); - - // Add package.json - host.reloadFS([tsConfig, packageJson]); - let packageJsonInfo = project.packageJsonCache.getInDirectory("/" as Path)!; - assert.ok(packageJsonInfo); - assert.ok(packageJsonInfo.dependencies); - assert.ok(packageJsonInfo.devDependencies); - assert.ok(packageJsonInfo.peerDependencies); - assert.ok(packageJsonInfo.optionalDependencies); - - // Edit package.json - host.reloadFS([ - tsConfig, - { - ...packageJson, - content: JSON.stringify({ - ...packageJsonContent, - dependencies: undefined - }) - } - ]); - packageJsonInfo = project.packageJsonCache.getInDirectory("/" as Path)!; - assert.isUndefined(packageJsonInfo.dependencies); - }); - - it("finds package.json on demand, watches for deletion, and removes them from cache", () => { - // Initialize project with package.json - const { project, host } = setup(); - project.getPackageJsonsVisibleToFile("/src/whatever/blah.ts" as Path); - assert.ok(project.packageJsonCache.getInDirectory("/" as Path)); - - // Delete package.json - host.reloadFS([tsConfig]); - assert.isUndefined(project.packageJsonCache.getInDirectory("/" as Path)); - }); - - it("finds multiple package.json files when present", () => { - // Initialize project with package.json at root - const { project, host } = setup(); - // Add package.json in /src - host.reloadFS([tsConfig, packageJson, { ...packageJson, path: "/src/package.json" }]); - assert.lengthOf(project.getPackageJsonsVisibleToFile("/a.ts" as Path), 1); - assert.lengthOf(project.getPackageJsonsVisibleToFile("/src/b.ts" as Path), 2); - }); - - it("handles errors in json parsing of package.json", () => { - const packageJsonContent = `{ "mod" }`; - const { project, host } = setup([tsConfig, { path: packageJson.path, content: packageJsonContent }]); - project.getPackageJsonsVisibleToFile("/src/whatever/blah.ts" as Path); - const packageJsonInfo = project.packageJsonCache.getInDirectory("/" as Path)!; - assert.isUndefined(packageJsonInfo); - - host.writeFile(packageJson.path, packageJson.content); - project.getPackageJsonsVisibleToFile("/src/whatever/blah.ts" as Path); - const packageJsonInfo2 = project.packageJsonCache.getInDirectory("/" as Path)!; - assert.ok(packageJsonInfo2); - assert.ok(packageJsonInfo2.dependencies); - assert.ok(packageJsonInfo2.devDependencies); - assert.ok(packageJsonInfo2.peerDependencies); - assert.ok(packageJsonInfo2.optionalDependencies); - }); - }); - - function setup(files: readonly File[] = [tsConfig, packageJson]) { - const host = createServerHost(files); - const session = createSession(host); - const projectService = session.getProjectService(); - projectService.openClientFile(files[0].path); - const project = configuredProjectAt(projectService, 0); - return { host, session, project, projectService }; +import { File, createServerHost, createSession, configuredProjectAt } from "../../ts.projectSystem"; +import { Path } from "../../ts"; +const tsConfig: File = { + path: "/tsconfig.json", + content: "{}" +}; +const packageJsonContent = { + dependencies: { + redux: "*" + }, + peerDependencies: { + react: "*" + }, + optionalDependencies: { + typescript: "*" + }, + devDependencies: { + webpack: "*" } +}; +const packageJson: File = { + path: "/package.json", + content: JSON.stringify(packageJsonContent, undefined, 2) +}; +describe("unittests:: tsserver:: packageJsonInfo", () => { + it("detects new package.json files that are added, caches them, and watches them", () => { + // Initialize project without package.json + const { project, host } = setup([tsConfig]); + assert.isUndefined(project.packageJsonCache.getInDirectory(("/" as Path))); + // Add package.json + host.reloadFS([tsConfig, packageJson]); + let packageJsonInfo = (project.packageJsonCache.getInDirectory(("/" as Path))!); + assert.ok(packageJsonInfo); + assert.ok(packageJsonInfo.dependencies); + assert.ok(packageJsonInfo.devDependencies); + assert.ok(packageJsonInfo.peerDependencies); + assert.ok(packageJsonInfo.optionalDependencies); + // Edit package.json + host.reloadFS([ + tsConfig, + { + ...packageJson, + content: JSON.stringify({ + ...packageJsonContent, + dependencies: undefined + }) + } + ]); + packageJsonInfo = (project.packageJsonCache.getInDirectory(("/" as Path))!); + assert.isUndefined(packageJsonInfo.dependencies); + }); + it("finds package.json on demand, watches for deletion, and removes them from cache", () => { + // Initialize project with package.json + const { project, host } = setup(); + project.getPackageJsonsVisibleToFile(("/src/whatever/blah.ts" as Path)); + assert.ok(project.packageJsonCache.getInDirectory(("/" as Path))); + // Delete package.json + host.reloadFS([tsConfig]); + assert.isUndefined(project.packageJsonCache.getInDirectory(("/" as Path))); + }); + it("finds multiple package.json files when present", () => { + // Initialize project with package.json at root + const { project, host } = setup(); + // Add package.json in /src + host.reloadFS([tsConfig, packageJson, { ...packageJson, path: "/src/package.json" }]); + assert.lengthOf(project.getPackageJsonsVisibleToFile(("/a.ts" as Path)), 1); + assert.lengthOf(project.getPackageJsonsVisibleToFile(("/src/b.ts" as Path)), 2); + }); + it("handles errors in json parsing of package.json", () => { + const packageJsonContent = `{ "mod" }`; + const { project, host } = setup([tsConfig, { path: packageJson.path, content: packageJsonContent }]); + project.getPackageJsonsVisibleToFile(("/src/whatever/blah.ts" as Path)); + const packageJsonInfo = (project.packageJsonCache.getInDirectory(("/" as Path))!); + assert.isUndefined(packageJsonInfo); + host.writeFile(packageJson.path, packageJson.content); + project.getPackageJsonsVisibleToFile(("/src/whatever/blah.ts" as Path)); + const packageJsonInfo2 = (project.packageJsonCache.getInDirectory(("/" as Path))!); + assert.ok(packageJsonInfo2); + assert.ok(packageJsonInfo2.dependencies); + assert.ok(packageJsonInfo2.devDependencies); + assert.ok(packageJsonInfo2.peerDependencies); + assert.ok(packageJsonInfo2.optionalDependencies); + }); +}); +function setup(files: readonly File[] = [tsConfig, packageJson]) { + const host = createServerHost(files); + const session = createSession(host); + const projectService = session.getProjectService(); + projectService.openClientFile(files[0].path); + const project = configuredProjectAt(projectService, 0); + return { host, session, project, projectService }; } diff --git a/src/testRunner/unittests/tsserver/projectErrors.ts b/src/testRunner/unittests/tsserver/projectErrors.ts index 2d8822493b03d..b3d51785afccf 100644 --- a/src/testRunner/unittests/tsserver/projectErrors.ts +++ b/src/testRunner/unittests/tsserver/projectErrors.ts @@ -1,302 +1,284 @@ -namespace ts.projectSystem { - describe("unittests:: tsserver:: Project Errors", () => { - function checkProjectErrors(projectFiles: server.ProjectFilesWithTSDiagnostics, expectedErrors: readonly string[]): void { - assert.isTrue(projectFiles !== undefined, "missing project files"); - checkProjectErrorsWorker(projectFiles.projectErrors, expectedErrors); - } - - function checkProjectErrorsWorker(errors: readonly Diagnostic[], expectedErrors: readonly string[]): void { - assert.equal(errors ? errors.length : 0, expectedErrors.length, `expected ${expectedErrors.length} error in the list`); - if (expectedErrors.length) { - for (let i = 0; i < errors.length; i++) { - const actualMessage = flattenDiagnosticMessageText(errors[i].messageText, "\n"); - const expectedMessage = expectedErrors[i]; - assert.isTrue(actualMessage.indexOf(expectedMessage) === 0, `error message does not match, expected ${actualMessage} to start with ${expectedMessage}`); - } +import { ProjectFilesWithTSDiagnostics, protocol, CommandNames, Logger, Msg } from "../../ts.server"; +import { Diagnostic, flattenDiagnosticMessageText, zipWith, startsWith, getBaseFileName, find, noop, returnUndefined, projectSystem, Path, Diagnostics, formatStringFromArgs, getDirectoryPath, emptyArray, ModuleKind, arrayToMap, identity, map, mapDefined } from "../../ts"; +import { createServerHost, libFile, createSession, toExternalFiles, checkNumberOfProjects, openFilesForSession, configuredProjectAt, createProjectService, checkProjectRootFiles, File, verifyGetErrRequest, createDiagnostic, checkCompleteEvent, checkProjectActualFiles, closeFilesForSession, ConfigFileDiagnostic, TestServerEventManager, executeSessionRequest, protocolTextSpanFromSubstring, Folder, checkNumberOfConfiguredProjects, checkWatchedFilesDetailed, checkWatchedDirectoriesDetailed, checkWatchedDirectories } from "../../ts.projectSystem"; +import { projectRoot } from "../../ts.tscWatch"; +describe("unittests:: tsserver:: Project Errors", () => { + function checkProjectErrors(projectFiles: ProjectFilesWithTSDiagnostics, expectedErrors: readonly string[]): void { + assert.isTrue(projectFiles !== undefined, "missing project files"); + checkProjectErrorsWorker(projectFiles.projectErrors, expectedErrors); + } + function checkProjectErrorsWorker(errors: readonly Diagnostic[], expectedErrors: readonly string[]): void { + assert.equal(errors ? errors.length : 0, expectedErrors.length, `expected ${expectedErrors.length} error in the list`); + if (expectedErrors.length) { + for (let i = 0; i < errors.length; i++) { + const actualMessage = flattenDiagnosticMessageText(errors[i].messageText, "\n"); + const expectedMessage = expectedErrors[i]; + assert.isTrue(actualMessage.indexOf(expectedMessage) === 0, `error message does not match, expected ${actualMessage} to start with ${expectedMessage}`); } } - - function checkDiagnosticsWithLinePos(errors: server.protocol.DiagnosticWithLinePosition[], expectedErrors: string[]) { - assert.equal(errors ? errors.length : 0, expectedErrors.length, `expected ${expectedErrors.length} error in the list`); - if (expectedErrors.length) { - zipWith(errors, expectedErrors, ({ message: actualMessage }, expectedMessage) => { - assert.isTrue(startsWith(actualMessage, actualMessage), `error message does not match, expected ${actualMessage} to start with ${expectedMessage}`); - }); - } + } + function checkDiagnosticsWithLinePos(errors: protocol.DiagnosticWithLinePosition[], expectedErrors: string[]) { + assert.equal(errors ? errors.length : 0, expectedErrors.length, `expected ${expectedErrors.length} error in the list`); + if (expectedErrors.length) { + zipWith(errors, expectedErrors, ({ message: actualMessage }, expectedMessage) => { + assert.isTrue(startsWith(actualMessage, actualMessage), `error message does not match, expected ${actualMessage} to start with ${expectedMessage}`); + }); } - - it("external project - diagnostics for missing files", () => { - const file1 = { - path: "/a/b/app.ts", - content: "" - }; - const file2 = { - path: "/a/b/applib.ts", - content: "" - }; - const host = createServerHost([file1, libFile]); - const session = createSession(host); - const projectService = session.getProjectService(); - const projectFileName = "/a/b/test.csproj"; - const compilerOptionsRequest: server.protocol.CompilerOptionsDiagnosticsRequest = { - type: "request", - command: server.CommandNames.CompilerOptionsDiagnosticsFull, - seq: 2, - arguments: { projectFileName } - }; - - { - projectService.openExternalProject({ - projectFileName, - options: {}, - rootFiles: toExternalFiles([file1.path, file2.path]) - }); - - checkNumberOfProjects(projectService, { externalProjects: 1 }); - const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; - // only file1 exists - expect error - checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]); - } - host.reloadFS([file2, libFile]); - { - // only file2 exists - expect error - checkNumberOfProjects(projectService, { externalProjects: 1 }); - const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; - checkDiagnosticsWithLinePos(diags, ["File '/a/b/app.ts' not found."]); - } - - host.reloadFS([file1, file2, libFile]); - { - // both files exist - expect no errors - checkNumberOfProjects(projectService, { externalProjects: 1 }); - const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; - checkDiagnosticsWithLinePos(diags, []); - } - }); - - it("configured projects - diagnostics for missing files", () => { - const file1 = { - path: "/a/b/app.ts", - content: "" - }; - const file2 = { - path: "/a/b/applib.ts", - content: "" - }; - const config = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) }) - }; - const host = createServerHost([file1, config, libFile]); - const session = createSession(host); - const projectService = session.getProjectService(); - openFilesForSession([file1], session); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const project = configuredProjectAt(projectService, 0); - const compilerOptionsRequest: server.protocol.CompilerOptionsDiagnosticsRequest = { - type: "request", - command: server.CommandNames.CompilerOptionsDiagnosticsFull, - seq: 2, - arguments: { projectFileName: project.getProjectName() } - }; - let diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; + } + it("external project - diagnostics for missing files", () => { + const file1 = { + path: "/a/b/app.ts", + content: "" + }; + const file2 = { + path: "/a/b/applib.ts", + content: "" + }; + const host = createServerHost([file1, libFile]); + const session = createSession(host); + const projectService = session.getProjectService(); + const projectFileName = "/a/b/test.csproj"; + const compilerOptionsRequest: protocol.CompilerOptionsDiagnosticsRequest = { + type: "request", + command: CommandNames.CompilerOptionsDiagnosticsFull, + seq: 2, + arguments: { projectFileName } + }; + { + projectService.openExternalProject({ + projectFileName, + options: {}, + rootFiles: toExternalFiles([file1.path, file2.path]) + }); + checkNumberOfProjects(projectService, { externalProjects: 1 }); + const diags = (session.executeCommand(compilerOptionsRequest).response as protocol.DiagnosticWithLinePosition[]); + // only file1 exists - expect error checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]); - - host.reloadFS([file1, file2, config, libFile]); - - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; + } + host.reloadFS([file2, libFile]); + { + // only file2 exists - expect error + checkNumberOfProjects(projectService, { externalProjects: 1 }); + const diags = (session.executeCommand(compilerOptionsRequest).response as protocol.DiagnosticWithLinePosition[]); + checkDiagnosticsWithLinePos(diags, ["File '/a/b/app.ts' not found."]); + } + host.reloadFS([file1, file2, libFile]); + { + // both files exist - expect no errors + checkNumberOfProjects(projectService, { externalProjects: 1 }); + const diags = (session.executeCommand(compilerOptionsRequest).response as protocol.DiagnosticWithLinePosition[]); checkDiagnosticsWithLinePos(diags, []); - }); - - it("configured projects - diagnostics for corrupted config 1", () => { - const file1 = { - path: "/a/b/app.ts", - content: "" - }; - const file2 = { - path: "/a/b/lib.ts", - content: "" - }; - const correctConfig = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) }) - }; - const corruptedConfig = { - path: correctConfig.path, - content: correctConfig.content.substr(1) - }; - const host = createServerHost([file1, file2, corruptedConfig]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - { - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - const configuredProject = find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!; - assert.isTrue(configuredProject !== undefined, "should find configured project"); - checkProjectErrors(configuredProject, []); - const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors(); - checkProjectErrorsWorker(projectErrors, [ - "'{' expected." - ]); - assert.isNotNull(projectErrors[0].file); - assert.equal(projectErrors[0].file!.fileName, corruptedConfig.path); - } - // fix config and trigger watcher - host.reloadFS([file1, file2, correctConfig]); - { - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - const configuredProject = find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!; - assert.isTrue(configuredProject !== undefined, "should find configured project"); - checkProjectErrors(configuredProject, []); - const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors(); - checkProjectErrorsWorker(projectErrors, []); - } - }); - - it("configured projects - diagnostics for corrupted config 2", () => { - const file1 = { - path: "/a/b/app.ts", - content: "" - }; - const file2 = { - path: "/a/b/lib.ts", - content: "" - }; - const correctConfig = { - path: "/a/b/tsconfig.json", - content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) }) - }; - const corruptedConfig = { - path: correctConfig.path, - content: correctConfig.content.substr(1) - }; - const host = createServerHost([file1, file2, correctConfig]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - { - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - const configuredProject = find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!; - assert.isTrue(configuredProject !== undefined, "should find configured project"); - checkProjectErrors(configuredProject, []); - const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors(); - checkProjectErrorsWorker(projectErrors, []); - } - // break config and trigger watcher - host.reloadFS([file1, file2, corruptedConfig]); - { - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - const configuredProject = find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!; - assert.isTrue(configuredProject !== undefined, "should find configured project"); - checkProjectErrors(configuredProject, []); - const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors(); - checkProjectErrorsWorker(projectErrors, [ - "'{' expected." - ]); - assert.isNotNull(projectErrors[0].file); - assert.equal(projectErrors[0].file!.fileName, corruptedConfig.path); - } - }); + } }); - - describe("unittests:: tsserver:: Project Errors are reported as appropriate", () => { - function createErrorLogger() { - let hasError = false; - const errorLogger: server.Logger = { - close: noop, - hasLevel: () => true, - loggingEnabled: () => true, - perftrc: noop, - info: noop, - msg: (_s, type) => { - if (type === server.Msg.Err) { - hasError = true; - } - }, - startGroup: noop, - endGroup: noop, - getLogFileName: returnUndefined - }; - return { - errorLogger, - hasError: () => hasError - }; + it("configured projects - diagnostics for missing files", () => { + const file1 = { + path: "/a/b/app.ts", + content: "" + }; + const file2 = { + path: "/a/b/applib.ts", + content: "" + }; + const config = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) }) + }; + const host = createServerHost([file1, config, libFile]); + const session = createSession(host); + const projectService = session.getProjectService(); + openFilesForSession([file1], session); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = configuredProjectAt(projectService, 0); + const compilerOptionsRequest: protocol.CompilerOptionsDiagnosticsRequest = { + type: "request", + command: CommandNames.CompilerOptionsDiagnosticsFull, + seq: 2, + arguments: { projectFileName: project.getProjectName() } + }; + let diags = (session.executeCommand(compilerOptionsRequest).response as protocol.DiagnosticWithLinePosition[]); + checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]); + host.reloadFS([file1, file2, config, libFile]); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + diags = (session.executeCommand(compilerOptionsRequest).response as protocol.DiagnosticWithLinePosition[]); + checkDiagnosticsWithLinePos(diags, []); + }); + it("configured projects - diagnostics for corrupted config 1", () => { + const file1 = { + path: "/a/b/app.ts", + content: "" + }; + const file2 = { + path: "/a/b/lib.ts", + content: "" + }; + const correctConfig = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) }) + }; + const corruptedConfig = { + path: correctConfig.path, + content: correctConfig.content.substr(1) + }; + const host = createServerHost([file1, file2, corruptedConfig]); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + { + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + const configuredProject = (find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!); + assert.isTrue(configuredProject !== undefined, "should find configured project"); + checkProjectErrors(configuredProject, []); + const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors(); + checkProjectErrorsWorker(projectErrors, [ + "'{' expected." + ]); + assert.isNotNull(projectErrors[0].file); + assert.equal(projectErrors[0].file!.fileName, corruptedConfig.path); } - - it("document is not contained in project", () => { - const file1 = { - path: "/a/b/app.ts", - content: "" - }; - const corruptedConfig = { - path: "/a/b/tsconfig.json", - content: "{" - }; - const host = createServerHost([file1, corruptedConfig]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); + // fix config and trigger watcher + host.reloadFS([file1, file2, correctConfig]); + { projectService.checkNumberOfProjects({ configuredProjects: 1 }); - - const project = projectService.findProject(corruptedConfig.path)!; - checkProjectRootFiles(project, [file1.path]); - }); - - describe("when opening new file that doesnt exist on disk yet", () => { - function verifyNonExistentFile(useProjectRoot: boolean) { - const folderPath = "/user/someuser/projects/someFolder"; - const fileInRoot: File = { - path: `/src/somefile.d.ts`, - content: "class c { }" - }; - const fileInProjectRoot: File = { - path: `${folderPath}/src/somefile.d.ts`, - content: "class c { }" - }; - const host = createServerHost([libFile, fileInRoot, fileInProjectRoot]); - const { hasError, errorLogger } = createErrorLogger(); - const session = createSession(host, { canUseEvents: true, logger: errorLogger, useInferredProjectPerProjectRoot: true }); - - const projectService = session.getProjectService(); - const untitledFile = "untitled:Untitled-1"; - const refPathNotFound1 = "../../../../../../typings/@epic/Core.d.ts"; - const refPathNotFound2 = "./src/somefile.d.ts"; - const fileContent = `/// -/// `; - session.executeCommandSeq({ - command: server.CommandNames.Open, - arguments: { - file: untitledFile, - fileContent, - scriptKindName: "TS", - projectRootPath: useProjectRoot ? folderPath : undefined - } - }); - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - const infoForUntitledAtProjectRoot = projectService.getScriptInfoForPath(`${folderPath.toLowerCase()}/${untitledFile.toLowerCase()}` as Path); - const infoForUnitiledAtRoot = projectService.getScriptInfoForPath(`/${untitledFile.toLowerCase()}` as Path); - const infoForSomefileAtProjectRoot = projectService.getScriptInfoForPath(`/${folderPath.toLowerCase()}/src/somefile.d.ts` as Path); - const infoForSomefileAtRoot = projectService.getScriptInfoForPath(`${fileInRoot.path.toLowerCase()}` as Path); - if (useProjectRoot) { - assert.isDefined(infoForUntitledAtProjectRoot); - assert.isUndefined(infoForUnitiledAtRoot); + const configuredProject = (find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!); + assert.isTrue(configuredProject !== undefined, "should find configured project"); + checkProjectErrors(configuredProject, []); + const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors(); + checkProjectErrorsWorker(projectErrors, []); + } + }); + it("configured projects - diagnostics for corrupted config 2", () => { + const file1 = { + path: "/a/b/app.ts", + content: "" + }; + const file2 = { + path: "/a/b/lib.ts", + content: "" + }; + const correctConfig = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) }) + }; + const corruptedConfig = { + path: correctConfig.path, + content: correctConfig.content.substr(1) + }; + const host = createServerHost([file1, file2, correctConfig]); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + { + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + const configuredProject = (find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!); + assert.isTrue(configuredProject !== undefined, "should find configured project"); + checkProjectErrors(configuredProject, []); + const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors(); + checkProjectErrorsWorker(projectErrors, []); + } + // break config and trigger watcher + host.reloadFS([file1, file2, corruptedConfig]); + { + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + const configuredProject = (find(projectService.synchronizeProjectList([]), f => f.info!.projectName === corruptedConfig.path)!); + assert.isTrue(configuredProject !== undefined, "should find configured project"); + checkProjectErrors(configuredProject, []); + const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors(); + checkProjectErrorsWorker(projectErrors, [ + "'{' expected." + ]); + assert.isNotNull(projectErrors[0].file); + assert.equal(projectErrors[0].file!.fileName, corruptedConfig.path); + } + }); +}); +describe("unittests:: tsserver:: Project Errors are reported as appropriate", () => { + function createErrorLogger() { + let hasError = false; + const errorLogger: Logger = { + close: noop, + hasLevel: () => true, + loggingEnabled: () => true, + perftrc: noop, + info: noop, + msg: (_s, type) => { + if (type === Msg.Err) { + hasError = true; } - else { - assert.isDefined(infoForUnitiledAtRoot); - assert.isUndefined(infoForUntitledAtProjectRoot); + }, + startGroup: noop, + endGroup: noop, + getLogFileName: returnUndefined + }; + return { + errorLogger, + hasError: () => hasError + }; + } + it("document is not contained in project", () => { + const file1 = { + path: "/a/b/app.ts", + content: "" + }; + const corruptedConfig = { + path: "/a/b/tsconfig.json", + content: "{" + }; + const host = createServerHost([file1, corruptedConfig]); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + const project = projectService.findProject(corruptedConfig.path)!; + checkProjectRootFiles(project, [file1.path]); + }); + describe("when opening new file that doesnt exist on disk yet", () => { + function verifyNonExistentFile(useProjectRoot: boolean) { + const folderPath = "/user/someuser/projects/someFolder"; + const fileInRoot: File = { + path: `/src/somefile.d.ts`, + content: "class c { }" + }; + const fileInProjectRoot: File = { + path: `${folderPath}/src/somefile.d.ts`, + content: "class c { }" + }; + const host = createServerHost([libFile, fileInRoot, fileInProjectRoot]); + const { hasError, errorLogger } = createErrorLogger(); + const session = createSession(host, { canUseEvents: true, logger: errorLogger, useInferredProjectPerProjectRoot: true }); + const projectService = session.getProjectService(); + const untitledFile = "untitled:Untitled-1"; + const refPathNotFound1 = "../../../../../../typings/@epic/Core.d.ts"; + const refPathNotFound2 = "./src/somefile.d.ts"; + const fileContent = `/// +/// `; + session.executeCommandSeq({ + command: CommandNames.Open, + arguments: { + file: untitledFile, + fileContent, + scriptKindName: "TS", + projectRootPath: useProjectRoot ? folderPath : undefined } - assert.isUndefined(infoForSomefileAtRoot); - assert.isUndefined(infoForSomefileAtProjectRoot); - - // Since this is not js project so no typings are queued - host.checkTimeoutQueueLength(0); - - const errorOffset = fileContent.indexOf(refPathNotFound1) + 1; - verifyGetErrRequest({ - session, - host, - expected: [{ + }); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const infoForUntitledAtProjectRoot = projectService.getScriptInfoForPath((`${folderPath.toLowerCase()}/${untitledFile.toLowerCase()}` as Path)); + const infoForUnitiledAtRoot = projectService.getScriptInfoForPath((`/${untitledFile.toLowerCase()}` as Path)); + const infoForSomefileAtProjectRoot = projectService.getScriptInfoForPath((`/${folderPath.toLowerCase()}/src/somefile.d.ts` as Path)); + const infoForSomefileAtRoot = projectService.getScriptInfoForPath((`${fileInRoot.path.toLowerCase()}` as Path)); + if (useProjectRoot) { + assert.isDefined(infoForUntitledAtProjectRoot); + assert.isUndefined(infoForUnitiledAtRoot); + } + else { + assert.isDefined(infoForUnitiledAtRoot); + assert.isUndefined(infoForUntitledAtProjectRoot); + } + assert.isUndefined(infoForSomefileAtRoot); + assert.isUndefined(infoForSomefileAtProjectRoot); + // Since this is not js project so no typings are queued + host.checkTimeoutQueueLength(0); + const errorOffset = fileContent.indexOf(refPathNotFound1) + 1; + verifyGetErrRequest({ + session, + host, + expected: [{ file: untitledFile, syntax: [], semantic: [ @@ -305,183 +287,168 @@ namespace ts.projectSystem { ], suggestion: [] }], - onErrEvent: () => assert.isFalse(hasError()) - }); - } - - it("has projectRoot", () => { - verifyNonExistentFile(/*useProjectRoot*/ true); - }); - - it("does not have projectRoot", () => { - verifyNonExistentFile(/*useProjectRoot*/ false); + onErrEvent: () => assert.isFalse(hasError()) }); + } + it("has projectRoot", () => { + verifyNonExistentFile(/*useProjectRoot*/ true); }); - - it("folder rename updates project structure and reports no errors", () => { - const projectDir = "/a/b/projects/myproject"; - const app: File = { - path: `${projectDir}/bar/app.ts`, - content: "class Bar implements foo.Foo { getFoo() { return ''; } get2() { return 1; } }" - }; - const foo: File = { - path: `${projectDir}/foo/foo.ts`, - content: "declare namespace foo { interface Foo { get2(): number; getFoo(): string; } }" - }; - const configFile: File = { - path: `${projectDir}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { module: "none", targer: "es5" }, exclude: ["node_modules"] }) - }; - const host = createServerHost([app, foo, configFile]); - const session = createSession(host, { canUseEvents: true, }); - const projectService = session.getProjectService(); - - session.executeCommandSeq({ - command: server.CommandNames.Open, - arguments: { file: app.path, } - }); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.isDefined(projectService.configuredProjects.get(configFile.path)); - verifyErrorsInApp(); - - host.renameFolder(`${projectDir}/foo`, `${projectDir}/foo2`); - host.runQueuedTimeoutCallbacks(); - host.runQueuedTimeoutCallbacks(); - verifyErrorsInApp(); - - function verifyErrorsInApp() { - verifyGetErrRequest({ - session, - host, - expected: [{ + it("does not have projectRoot", () => { + verifyNonExistentFile(/*useProjectRoot*/ false); + }); + }); + it("folder rename updates project structure and reports no errors", () => { + const projectDir = "/a/b/projects/myproject"; + const app: File = { + path: `${projectDir}/bar/app.ts`, + content: "class Bar implements foo.Foo { getFoo() { return ''; } get2() { return 1; } }" + }; + const foo: File = { + path: `${projectDir}/foo/foo.ts`, + content: "declare namespace foo { interface Foo { get2(): number; getFoo(): string; } }" + }; + const configFile: File = { + path: `${projectDir}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { module: "none", targer: "es5" }, exclude: ["node_modules"] }) + }; + const host = createServerHost([app, foo, configFile]); + const session = createSession(host, { canUseEvents: true, }); + const projectService = session.getProjectService(); + session.executeCommandSeq({ + command: CommandNames.Open, + arguments: { file: app.path, } + }); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.isDefined(projectService.configuredProjects.get(configFile.path)); + verifyErrorsInApp(); + host.renameFolder(`${projectDir}/foo`, `${projectDir}/foo2`); + host.runQueuedTimeoutCallbacks(); + host.runQueuedTimeoutCallbacks(); + verifyErrorsInApp(); + function verifyErrorsInApp() { + verifyGetErrRequest({ + session, + host, + expected: [{ file: app, syntax: [], semantic: [], suggestion: [] }], - }); - } - }); - - it("Getting errors before opening file", () => { - const file: File = { - path: "/a/b/project/file.ts", - content: "let x: number = false;" - }; - const host = createServerHost([file, libFile]); - const { hasError, errorLogger } = createErrorLogger(); - const session = createSession(host, { canUseEvents: true, logger: errorLogger }); - - session.clearMessages(); - const expectedSequenceId = session.getNextSeq(); - session.executeCommandSeq({ - command: server.CommandNames.Geterr, - arguments: { - delay: 0, - files: [file.path] - } }); - - host.checkTimeoutQueueLengthAndRun(1); - assert.isFalse(hasError()); - checkCompleteEvent(session, 1, expectedSequenceId); - session.clearMessages(); - }); - - it("Reports errors correctly when file referenced by inferred project root, is opened right after closing the root file", () => { - const app: File = { - path: `${tscWatch.projectRoot}/src/client/app.js`, - content: "" - }; - const serverUtilities: File = { - path: `${tscWatch.projectRoot}/src/server/utilities.js`, - content: `function getHostName() { return "hello"; } export { getHostName };` - }; - const backendTest: File = { - path: `${tscWatch.projectRoot}/test/backend/index.js`, - content: `import { getHostName } from '../../src/server/utilities';export default getHostName;` - }; - const files = [libFile, app, serverUtilities, backendTest]; - const host = createServerHost(files); - const session = createSession(host, { useInferredProjectPerProjectRoot: true, canUseEvents: true }); - openFilesForSession([{ file: app, projectRootPath: tscWatch.projectRoot }], session); - const service = session.getProjectService(); - checkNumberOfProjects(service, { inferredProjects: 1 }); - const project = service.inferredProjects[0]; - checkProjectActualFiles(project, [libFile.path, app.path]); - openFilesForSession([{ file: backendTest, projectRootPath: tscWatch.projectRoot }], session); - checkNumberOfProjects(service, { inferredProjects: 1 }); - checkProjectActualFiles(project, files.map(f => f.path)); - checkErrors([backendTest.path, app.path]); - closeFilesForSession([backendTest], session); - openFilesForSession([{ file: serverUtilities.path, projectRootPath: tscWatch.projectRoot }], session); - checkErrors([serverUtilities.path, app.path]); - - function checkErrors(openFiles: [string, string]) { - verifyGetErrRequest({ - session, - host, - expected: openFiles.map(file => ({ file, syntax: [], semantic: [], suggestion: [] })), - existingTimeouts: 2 - }); + } + }); + it("Getting errors before opening file", () => { + const file: File = { + path: "/a/b/project/file.ts", + content: "let x: number = false;" + }; + const host = createServerHost([file, libFile]); + const { hasError, errorLogger } = createErrorLogger(); + const session = createSession(host, { canUseEvents: true, logger: errorLogger }); + session.clearMessages(); + const expectedSequenceId = session.getNextSeq(); + session.executeCommandSeq({ + command: CommandNames.Geterr, + arguments: { + delay: 0, + files: [file.path] } }); - - it("Correct errors when resolution resolves to file that has same ambient module and is also module", () => { - const projectRootPath = "/users/username/projects/myproject"; - const aFile: File = { - path: `${projectRootPath}/src/a.ts`, - content: `import * as myModule from "@custom/plugin"; + host.checkTimeoutQueueLengthAndRun(1); + assert.isFalse(hasError()); + checkCompleteEvent(session, 1, expectedSequenceId); + session.clearMessages(); + }); + it("Reports errors correctly when file referenced by inferred project root, is opened right after closing the root file", () => { + const app: File = { + path: `${projectRoot}/src/client/app.js`, + content: "" + }; + const serverUtilities: File = { + path: `${projectRoot}/src/server/utilities.js`, + content: `function getHostName() { return "hello"; } export { getHostName };` + }; + const backendTest: File = { + path: `${projectRoot}/test/backend/index.js`, + content: `import { getHostName } from '../../src/server/utilities';export default getHostName;` + }; + const files = [libFile, app, serverUtilities, backendTest]; + const host = createServerHost(files); + const session = createSession(host, { useInferredProjectPerProjectRoot: true, canUseEvents: true }); + openFilesForSession([{ file: app, projectRootPath: projectRoot }], session); + const service = session.getProjectService(); + checkNumberOfProjects(service, { inferredProjects: 1 }); + const project = service.inferredProjects[0]; + checkProjectActualFiles(project, [libFile.path, app.path]); + openFilesForSession([{ file: backendTest, projectRootPath: projectRoot }], session); + checkNumberOfProjects(service, { inferredProjects: 1 }); + checkProjectActualFiles(project, files.map(f => f.path)); + checkErrors([backendTest.path, app.path]); + closeFilesForSession([backendTest], session); + openFilesForSession([{ file: serverUtilities.path, projectRootPath: projectRoot }], session); + checkErrors([serverUtilities.path, app.path]); + function checkErrors(openFiles: [string, string]) { + verifyGetErrRequest({ + session, + host, + expected: openFiles.map(file => ({ file, syntax: [], semantic: [], suggestion: [] })), + existingTimeouts: 2 + }); + } + }); + it("Correct errors when resolution resolves to file that has same ambient module and is also module", () => { + const projectRootPath = "/users/username/projects/myproject"; + const aFile: File = { + path: `${projectRootPath}/src/a.ts`, + content: `import * as myModule from "@custom/plugin"; function foo() { // hello }` - }; - const config: File = { - path: `${projectRootPath}/tsconfig.json`, - content: JSON.stringify({ include: ["src"] }) - }; - const plugin: File = { - path: `${projectRootPath}/node_modules/@custom/plugin/index.d.ts`, - content: `import './proposed'; + }; + const config: File = { + path: `${projectRootPath}/tsconfig.json`, + content: JSON.stringify({ include: ["src"] }) + }; + const plugin: File = { + path: `${projectRootPath}/node_modules/@custom/plugin/index.d.ts`, + content: `import './proposed'; declare module '@custom/plugin' { export const version: string; }` - }; - const pluginProposed: File = { - path: `${projectRootPath}/node_modules/@custom/plugin/proposed.d.ts`, - content: `declare module '@custom/plugin' { + }; + const pluginProposed: File = { + path: `${projectRootPath}/node_modules/@custom/plugin/proposed.d.ts`, + content: `declare module '@custom/plugin' { export const bar = 10; }` - }; - const files = [libFile, aFile, config, plugin, pluginProposed]; - const host = createServerHost(files); - const session = createSession(host, { canUseEvents: true }); - const service = session.getProjectService(); - openFilesForSession([aFile], session); - - checkNumberOfProjects(service, { configuredProjects: 1 }); - session.clearMessages(); - checkErrors(); - - session.executeCommandSeq({ - command: protocol.CommandTypes.Change, - arguments: { - file: aFile.path, - line: 3, - offset: 8, - endLine: 3, - endOffset: 8, - insertString: "o" - } - }); - checkErrors(); - - function checkErrors() { - host.checkTimeoutQueueLength(0); - verifyGetErrRequest({ - session, - host, - expected: [{ + }; + const files = [libFile, aFile, config, plugin, pluginProposed]; + const host = createServerHost(files); + const session = createSession(host, { canUseEvents: true }); + const service = session.getProjectService(); + openFilesForSession([aFile], session); + checkNumberOfProjects(service, { configuredProjects: 1 }); + session.clearMessages(); + checkErrors(); + session.executeCommandSeq({ + command: projectSystem.protocol.CommandTypes.Change, + arguments: { + file: aFile.path, + line: 3, + offset: 8, + endLine: 3, + endOffset: 8, + insertString: "o" + } + }); + checkErrors(); + function checkErrors() { + host.checkTimeoutQueueLength(0); + verifyGetErrRequest({ + session, + host, + expected: [{ file: aFile, syntax: [], semantic: [], @@ -490,561 +457,503 @@ declare module '@custom/plugin' { createDiagnostic({ line: 2, offset: 10 }, { line: 2, offset: 13 }, Diagnostics._0_is_declared_but_its_value_is_never_read, ["foo"], "suggestion", /*reportsUnnecessary*/ true) ] }] - }); - } - }); - }); - - describe("unittests:: tsserver:: Project Errors for Configure file diagnostics events", () => { - function getUnknownCompilerOptionDiagnostic(configFile: File, prop: string, didYouMean?: string): ConfigFileDiagnostic { - const d = didYouMean ? Diagnostics.Unknown_compiler_option_0_Did_you_mean_1 : Diagnostics.Unknown_compiler_option_0; - const start = configFile.content.indexOf(prop) - 1; // start at "prop" - return { - fileName: configFile.path, - start, - length: prop.length + 2, - messageText: formatStringFromArgs(d.message, didYouMean ? [prop, didYouMean] : [prop]), - category: d.category, - code: d.code, - reportsUnnecessary: undefined - }; - } - - function getFileNotFoundDiagnostic(configFile: File, relativeFileName: string): ConfigFileDiagnostic { - const findString = `{"path":"./${relativeFileName}"}`; - const d = Diagnostics.File_0_not_found; - const start = configFile.content.indexOf(findString); - return { - fileName: configFile.path, - start, - length: findString.length, - messageText: formatStringFromArgs(d.message, [`${getDirectoryPath(configFile.path)}/${relativeFileName}`]), - category: d.category, - code: d.code, - reportsUnnecessary: undefined - }; + }); } - - it("are generated when the config file has errors", () => { - const file: File = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{ + }); +}); +describe("unittests:: tsserver:: Project Errors for Configure file diagnostics events", () => { + function getUnknownCompilerOptionDiagnostic(configFile: File, prop: string, didYouMean?: string): ConfigFileDiagnostic { + const d = didYouMean ? Diagnostics.Unknown_compiler_option_0_Did_you_mean_1 : Diagnostics.Unknown_compiler_option_0; + const start = configFile.content.indexOf(prop) - 1; // start at "prop" + return { + fileName: configFile.path, + start, + length: prop.length + 2, + messageText: formatStringFromArgs(d.message, didYouMean ? [prop, didYouMean] : [prop]), + category: d.category, + code: d.code, + reportsUnnecessary: undefined + }; + } + function getFileNotFoundDiagnostic(configFile: File, relativeFileName: string): ConfigFileDiagnostic { + const findString = `{"path":"./${relativeFileName}"}`; + const d = Diagnostics.File_0_not_found; + const start = configFile.content.indexOf(findString); + return { + fileName: configFile.path, + start, + length: findString.length, + messageText: formatStringFromArgs(d.message, [`${getDirectoryPath(configFile.path)}/${relativeFileName}`]), + category: d.category, + code: d.code, + reportsUnnecessary: undefined + }; + } + it("are generated when the config file has errors", () => { + const file: File = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ "compilerOptions": { "foo": "bar", "allowJS": true } }` - }; - const serverEventManager = new TestServerEventManager([file, libFile, configFile]); - openFilesForSession([file], serverEventManager.session); - serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, [ - getUnknownCompilerOptionDiagnostic(configFile, "foo"), - getUnknownCompilerOptionDiagnostic(configFile, "allowJS", "allowJs") - ]); - }); - - it("are generated when the config file doesn't have errors", () => { - const file: File = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{ + }; + const serverEventManager = new TestServerEventManager([file, libFile, configFile]); + openFilesForSession([file], serverEventManager.session); + serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, [ + getUnknownCompilerOptionDiagnostic(configFile, "foo"), + getUnknownCompilerOptionDiagnostic(configFile, "allowJS", "allowJs") + ]); + }); + it("are generated when the config file doesn't have errors", () => { + const file: File = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ "compilerOptions": {} }` - }; - const serverEventManager = new TestServerEventManager([file, libFile, configFile]); - openFilesForSession([file], serverEventManager.session); - serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, emptyArray); - }); - - it("are generated when the config file changes", () => { - const file: File = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const configFile = { - path: "/a/b/tsconfig.json", - content: `{ + }; + const serverEventManager = new TestServerEventManager([file, libFile, configFile]); + openFilesForSession([file], serverEventManager.session); + serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, emptyArray); + }); + it("are generated when the config file changes", () => { + const file: File = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: `{ "compilerOptions": {} }` - }; - - const files = [file, libFile, configFile]; - const serverEventManager = new TestServerEventManager(files); - openFilesForSession([file], serverEventManager.session); - serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, emptyArray); - - configFile.content = `{ + }; + const files = [file, libFile, configFile]; + const serverEventManager = new TestServerEventManager(files); + openFilesForSession([file], serverEventManager.session); + serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, emptyArray); + configFile.content = `{ "compilerOptions": { "haha": 123 } }`; - serverEventManager.host.reloadFS(files); - serverEventManager.host.runQueuedTimeoutCallbacks(); - serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, configFile.path, [ - getUnknownCompilerOptionDiagnostic(configFile, "haha") - ]); - - configFile.content = `{ + serverEventManager.host.reloadFS(files); + serverEventManager.host.runQueuedTimeoutCallbacks(); + serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, configFile.path, [ + getUnknownCompilerOptionDiagnostic(configFile, "haha") + ]); + configFile.content = `{ "compilerOptions": {} }`; - serverEventManager.host.reloadFS(files); - serverEventManager.host.runQueuedTimeoutCallbacks(); - serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, configFile.path, emptyArray); - }); - - it("are not generated when the config file does not include file opened and config file has errors", () => { - const file: File = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const file2: File = { - path: "/a/b/test.ts", - content: "let x = 10" - }; - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{ + serverEventManager.host.reloadFS(files); + serverEventManager.host.runQueuedTimeoutCallbacks(); + serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, configFile.path, emptyArray); + }); + it("are not generated when the config file does not include file opened and config file has errors", () => { + const file: File = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const file2: File = { + path: "/a/b/test.ts", + content: "let x = 10" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ "compilerOptions": { "foo": "bar", "allowJS": true }, "files": ["app.ts"] }` - }; - const serverEventManager = new TestServerEventManager([file, file2, libFile, configFile]); - openFilesForSession([file2], serverEventManager.session); - serverEventManager.hasZeroEvent("configFileDiag"); - }); - - it("are not generated when the config file has errors but suppressDiagnosticEvents is true", () => { - const file: File = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{ + }; + const serverEventManager = new TestServerEventManager([file, file2, libFile, configFile]); + openFilesForSession([file2], serverEventManager.session); + serverEventManager.hasZeroEvent("configFileDiag"); + }); + it("are not generated when the config file has errors but suppressDiagnosticEvents is true", () => { + const file: File = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ "compilerOptions": { "foo": "bar", "allowJS": true } }` - }; - const serverEventManager = new TestServerEventManager([file, libFile, configFile], /*suppressDiagnosticEvents*/ true); - openFilesForSession([file], serverEventManager.session); - serverEventManager.hasZeroEvent("configFileDiag"); - }); - - it("are not generated when the config file does not include file opened and doesnt contain any errors", () => { - const file: File = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const file2: File = { - path: "/a/b/test.ts", - content: "let x = 10" - }; - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{ + }; + const serverEventManager = new TestServerEventManager([file, libFile, configFile], /*suppressDiagnosticEvents*/ true); + openFilesForSession([file], serverEventManager.session); + serverEventManager.hasZeroEvent("configFileDiag"); + }); + it("are not generated when the config file does not include file opened and doesnt contain any errors", () => { + const file: File = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const file2: File = { + path: "/a/b/test.ts", + content: "let x = 10" + }; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ "files": ["app.ts"] }` - }; - - const serverEventManager = new TestServerEventManager([file, file2, libFile, configFile]); - openFilesForSession([file2], serverEventManager.session); - serverEventManager.hasZeroEvent("configFileDiag"); - }); - - it("contains the project reference errors", () => { - const file: File = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const noSuchTsconfig = "no-such-tsconfig.json"; - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{ + }; + const serverEventManager = new TestServerEventManager([file, file2, libFile, configFile]); + openFilesForSession([file2], serverEventManager.session); + serverEventManager.hasZeroEvent("configFileDiag"); + }); + it("contains the project reference errors", () => { + const file: File = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const noSuchTsconfig = "no-such-tsconfig.json"; + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ "files": ["app.ts"], "references": [{"path":"./${noSuchTsconfig}"}] }` - }; - - const serverEventManager = new TestServerEventManager([file, libFile, configFile]); - openFilesForSession([file], serverEventManager.session); - serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, [ - getFileNotFoundDiagnostic(configFile, noSuchTsconfig) - ]); - }); + }; + const serverEventManager = new TestServerEventManager([file, libFile, configFile]); + openFilesForSession([file], serverEventManager.session); + serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, [ + getFileNotFoundDiagnostic(configFile, noSuchTsconfig) + ]); }); - - describe("unittests:: tsserver:: Project Errors dont include overwrite emit error", () => { - it("for inferred project", () => { - const f1 = { - path: "/a/b/f1.js", - content: "function test1() { }" - }; - const host = createServerHost([f1, libFile]); - const session = createSession(host); - openFilesForSession([f1], session); - - const projectService = session.getProjectService(); - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - const projectName = projectService.inferredProjects[0].getProjectName(); - - const diags = session.executeCommand({ - type: "request", - command: server.CommandNames.CompilerOptionsDiagnosticsFull, - seq: 2, - arguments: { projectFileName: projectName } - }).response as readonly protocol.DiagnosticWithLinePosition[]; - assert.isTrue(diags.length === 0); - - session.executeCommand({ - type: "request", - command: server.CommandNames.CompilerOptionsForInferredProjects, - seq: 3, - arguments: { options: { module: ModuleKind.CommonJS } } - }); - const diagsAfterUpdate = session.executeCommand({ - type: "request", - command: server.CommandNames.CompilerOptionsDiagnosticsFull, - seq: 4, - arguments: { projectFileName: projectName } - }).response as readonly protocol.DiagnosticWithLinePosition[]; - assert.isTrue(diagsAfterUpdate.length === 0); - }); - - it("for external project", () => { - const f1 = { - path: "/a/b/f1.js", - content: "function test1() { }" - }; - const host = createServerHost([f1, libFile]); - const session = createSession(host); - const projectService = session.getProjectService(); - const projectFileName = "/a/b/project.csproj"; - const externalFiles = toExternalFiles([f1.path]); - projectService.openExternalProject({ +}); +describe("unittests:: tsserver:: Project Errors dont include overwrite emit error", () => { + it("for inferred project", () => { + const f1 = { + path: "/a/b/f1.js", + content: "function test1() { }" + }; + const host = createServerHost([f1, libFile]); + const session = createSession(host); + openFilesForSession([f1], session); + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const projectName = projectService.inferredProjects[0].getProjectName(); + const diags = (session.executeCommand(({ + type: "request", + command: CommandNames.CompilerOptionsDiagnosticsFull, + seq: 2, + arguments: { projectFileName: projectName } + })).response as readonly projectSystem.protocol.DiagnosticWithLinePosition[]); + assert.isTrue(diags.length === 0); + session.executeCommand(({ + type: "request", + command: CommandNames.CompilerOptionsForInferredProjects, + seq: 3, + arguments: { options: { module: ModuleKind.CommonJS } } + })); + const diagsAfterUpdate = (session.executeCommand(({ + type: "request", + command: CommandNames.CompilerOptionsDiagnosticsFull, + seq: 4, + arguments: { projectFileName: projectName } + })).response as readonly projectSystem.protocol.DiagnosticWithLinePosition[]); + assert.isTrue(diagsAfterUpdate.length === 0); + }); + it("for external project", () => { + const f1 = { + path: "/a/b/f1.js", + content: "function test1() { }" + }; + const host = createServerHost([f1, libFile]); + const session = createSession(host); + const projectService = session.getProjectService(); + const projectFileName = "/a/b/project.csproj"; + const externalFiles = toExternalFiles([f1.path]); + projectService.openExternalProject(({ + projectFileName, + rootFiles: externalFiles, + options: {} + })); + checkNumberOfProjects(projectService, { externalProjects: 1 }); + const diags = (session.executeCommand(({ + type: "request", + command: CommandNames.CompilerOptionsDiagnosticsFull, + seq: 2, + arguments: { projectFileName } + })).response as readonly protocol.DiagnosticWithLinePosition[]); + assert.isTrue(diags.length === 0); + session.executeCommand(({ + type: "request", + command: CommandNames.OpenExternalProject, + seq: 3, + arguments: { projectFileName, rootFiles: externalFiles, - options: {} - }); - - checkNumberOfProjects(projectService, { externalProjects: 1 }); - - const diags = session.executeCommand({ - type: "request", - command: server.CommandNames.CompilerOptionsDiagnosticsFull, - seq: 2, - arguments: { projectFileName } - }).response as readonly server.protocol.DiagnosticWithLinePosition[]; - assert.isTrue(diags.length === 0); - - session.executeCommand({ - type: "request", - command: server.CommandNames.OpenExternalProject, - seq: 3, - arguments: { - projectFileName, - rootFiles: externalFiles, - options: { module: ModuleKind.CommonJS } - } - }); - const diagsAfterUpdate = session.executeCommand({ - type: "request", - command: server.CommandNames.CompilerOptionsDiagnosticsFull, - seq: 4, - arguments: { projectFileName } - }).response as readonly server.protocol.DiagnosticWithLinePosition[]; - assert.isTrue(diagsAfterUpdate.length === 0); - }); + options: { module: ModuleKind.CommonJS } + } + })); + const diagsAfterUpdate = (session.executeCommand(({ + type: "request", + command: CommandNames.CompilerOptionsDiagnosticsFull, + seq: 4, + arguments: { projectFileName } + })).response as readonly protocol.DiagnosticWithLinePosition[]); + assert.isTrue(diagsAfterUpdate.length === 0); }); - - describe("unittests:: tsserver:: Project Errors reports Options Diagnostic locations correctly with changes in configFile contents", () => { - it("when options change", () => { - const file = { - path: "/a/b/app.ts", - content: "let x = 10" - }; - const configFileContentBeforeComment = `{`; - const configFileContentComment = ` +}); +describe("unittests:: tsserver:: Project Errors reports Options Diagnostic locations correctly with changes in configFile contents", () => { + it("when options change", () => { + const file = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFileContentBeforeComment = `{`; + const configFileContentComment = ` // comment`; - const configFileContentAfterComment = ` + const configFileContentAfterComment = ` "compilerOptions": { "inlineSourceMap": true, "mapRoot": "./" } }`; - const configFileContentWithComment = configFileContentBeforeComment + configFileContentComment + configFileContentAfterComment; - const configFileContentWithoutCommentLine = configFileContentBeforeComment + configFileContentAfterComment; - - const configFile = { - path: "/a/b/tsconfig.json", - content: configFileContentWithComment - }; - const host = createServerHost([file, libFile, configFile]); - const session = createSession(host); - openFilesForSession([file], session); - - const projectService = session.getProjectService(); - checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const projectName = configuredProjectAt(projectService, 0).getProjectName(); - - const diags = session.executeCommand({ - type: "request", - command: server.CommandNames.SemanticDiagnosticsSync, - seq: 2, - arguments: { file: configFile.path, projectFileName: projectName, includeLinePosition: true } - }).response as readonly server.protocol.DiagnosticWithLinePosition[]; - assert.isTrue(diags.length === 3); - - configFile.content = configFileContentWithoutCommentLine; - host.reloadFS([file, configFile]); - - const diagsAfterEdit = session.executeCommand({ - type: "request", - command: server.CommandNames.SemanticDiagnosticsSync, - seq: 2, - arguments: { file: configFile.path, projectFileName: projectName, includeLinePosition: true } - }).response as readonly server.protocol.DiagnosticWithLinePosition[]; - assert.isTrue(diagsAfterEdit.length === 3); - - verifyDiagnostic(diags[0], diagsAfterEdit[0]); - verifyDiagnostic(diags[1], diagsAfterEdit[1]); - verifyDiagnostic(diags[2], diagsAfterEdit[2]); - - function verifyDiagnostic(beforeEditDiag: server.protocol.DiagnosticWithLinePosition, afterEditDiag: server.protocol.DiagnosticWithLinePosition) { - assert.equal(beforeEditDiag.message, afterEditDiag.message); - assert.equal(beforeEditDiag.code, afterEditDiag.code); - assert.equal(beforeEditDiag.category, afterEditDiag.category); - assert.equal(beforeEditDiag.startLocation.line, afterEditDiag.startLocation.line + 1); - assert.equal(beforeEditDiag.startLocation.offset, afterEditDiag.startLocation.offset); - assert.equal(beforeEditDiag.endLocation.line, afterEditDiag.endLocation.line + 1); - assert.equal(beforeEditDiag.endLocation.offset, afterEditDiag.endLocation.offset); - } - }); + const configFileContentWithComment = configFileContentBeforeComment + configFileContentComment + configFileContentAfterComment; + const configFileContentWithoutCommentLine = configFileContentBeforeComment + configFileContentAfterComment; + const configFile = { + path: "/a/b/tsconfig.json", + content: configFileContentWithComment + }; + const host = createServerHost([file, libFile, configFile]); + const session = createSession(host); + openFilesForSession([file], session); + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const projectName = configuredProjectAt(projectService, 0).getProjectName(); + const diags = (session.executeCommand(({ + type: "request", + command: CommandNames.SemanticDiagnosticsSync, + seq: 2, + arguments: { file: configFile.path, projectFileName: projectName, includeLinePosition: true } + })).response as readonly protocol.DiagnosticWithLinePosition[]); + assert.isTrue(diags.length === 3); + configFile.content = configFileContentWithoutCommentLine; + host.reloadFS([file, configFile]); + const diagsAfterEdit = (session.executeCommand(({ + type: "request", + command: CommandNames.SemanticDiagnosticsSync, + seq: 2, + arguments: { file: configFile.path, projectFileName: projectName, includeLinePosition: true } + })).response as readonly protocol.DiagnosticWithLinePosition[]); + assert.isTrue(diagsAfterEdit.length === 3); + verifyDiagnostic(diags[0], diagsAfterEdit[0]); + verifyDiagnostic(diags[1], diagsAfterEdit[1]); + verifyDiagnostic(diags[2], diagsAfterEdit[2]); + function verifyDiagnostic(beforeEditDiag: protocol.DiagnosticWithLinePosition, afterEditDiag: protocol.DiagnosticWithLinePosition) { + assert.equal(beforeEditDiag.message, afterEditDiag.message); + assert.equal(beforeEditDiag.code, afterEditDiag.code); + assert.equal(beforeEditDiag.category, afterEditDiag.category); + assert.equal(beforeEditDiag.startLocation.line, afterEditDiag.startLocation.line + 1); + assert.equal(beforeEditDiag.startLocation.offset, afterEditDiag.startLocation.offset); + assert.equal(beforeEditDiag.endLocation.line, afterEditDiag.endLocation.line + 1); + assert.equal(beforeEditDiag.endLocation.offset, afterEditDiag.endLocation.offset); + } }); - - describe("unittests:: tsserver:: Project Errors with config file change", () => { - it("Updates diagnostics when '--noUnusedLabels' changes", () => { - const aTs: File = { path: "/a.ts", content: "label: while (1) {}" }; - const options = (allowUnusedLabels: boolean) => `{ "compilerOptions": { "allowUnusedLabels": ${allowUnusedLabels} } }`; - const tsconfig: File = { path: "/tsconfig.json", content: options(/*allowUnusedLabels*/ true) }; - - const host = createServerHost([aTs, tsconfig]); - const session = createSession(host); - openFilesForSession([aTs], session); - - host.modifyFile(tsconfig.path, options(/*allowUnusedLabels*/ false)); - host.runQueuedTimeoutCallbacks(); - - const response = executeSessionRequest(session, protocol.CommandTypes.SemanticDiagnosticsSync, { file: aTs.path }) as protocol.Diagnostic[] | undefined; - assert.deepEqual(response, [ - { - start: { line: 1, offset: 1 }, - end: { line: 1, offset: 1 + "label".length }, - text: "Unused label.", - category: "error", - code: Diagnostics.Unused_label.code, - relatedInformation: undefined, - reportsUnnecessary: true, - source: undefined, - }, - ]); - }); +}); +describe("unittests:: tsserver:: Project Errors with config file change", () => { + it("Updates diagnostics when '--noUnusedLabels' changes", () => { + const aTs: File = { path: "/a.ts", content: "label: while (1) {}" }; + const options = (allowUnusedLabels: boolean) => `{ "compilerOptions": { "allowUnusedLabels": ${allowUnusedLabels} } }`; + const tsconfig: File = { path: "/tsconfig.json", content: options(/*allowUnusedLabels*/ true) }; + const host = createServerHost([aTs, tsconfig]); + const session = createSession(host); + openFilesForSession([aTs], session); + host.modifyFile(tsconfig.path, options(/*allowUnusedLabels*/ false)); + host.runQueuedTimeoutCallbacks(); + const response = (executeSessionRequest(session, projectSystem.protocol.CommandTypes.SemanticDiagnosticsSync, { file: aTs.path }) as projectSystem.protocol.Diagnostic[] | undefined); + assert.deepEqual(response, [ + { + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 1 + "label".length }, + text: "Unused label.", + category: "error", + code: Diagnostics.Unused_label.code, + relatedInformation: undefined, + reportsUnnecessary: true, + source: undefined, + }, + ]); }); - - describe("unittests:: tsserver:: Project Errors with resolveJsonModule", () => { - function createSessionForTest({ include }: { include: readonly string[]; }) { - const test: File = { - path: `${tscWatch.projectRoot}/src/test.ts`, - content: `import * as blabla from "./blabla.json"; +}); +describe("unittests:: tsserver:: Project Errors with resolveJsonModule", () => { + function createSessionForTest({ include }: { + include: readonly string[]; + }) { + const test: File = { + path: `${projectRoot}/src/test.ts`, + content: `import * as blabla from "./blabla.json"; declare var console: any; console.log(blabla);` - }; - const blabla: File = { - path: `${tscWatch.projectRoot}/src/blabla.json`, - content: "{}" - }; - const tsconfig: File = { - path: `${tscWatch.projectRoot}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - resolveJsonModule: true, - composite: true - }, - include - }) - }; - - const host = createServerHost([test, blabla, libFile, tsconfig]); - const session = createSession(host, { canUseEvents: true }); - openFilesForSession([test], session); - return { host, session, test, blabla, tsconfig }; - } - - it("should not report incorrect error when json is root file found by tsconfig", () => { - const { host, session, test } = createSessionForTest({ - include: ["./src/*.ts", "./src/*.json"] - }); - verifyGetErrRequest({ - session, - host, - expected: [ - { - file: test, - syntax: [], - semantic: [], - suggestion: [] - } - ] - }); + }; + const blabla: File = { + path: `${projectRoot}/src/blabla.json`, + content: "{}" + }; + const tsconfig: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + resolveJsonModule: true, + composite: true + }, + include + }) + }; + const host = createServerHost([test, blabla, libFile, tsconfig]); + const session = createSession(host, { canUseEvents: true }); + openFilesForSession([test], session); + return { host, session, test, blabla, tsconfig }; + } + it("should not report incorrect error when json is root file found by tsconfig", () => { + const { host, session, test } = createSessionForTest({ + include: ["./src/*.ts", "./src/*.json"] }); - - it("should report error when json is not root file found by tsconfig", () => { - const { host, session, test, blabla, tsconfig } = createSessionForTest({ - include: ["./src/*.ts"] - }); - const span = protocolTextSpanFromSubstring(test.content, `"./blabla.json"`); - verifyGetErrRequest({ - session, - host, - expected: [{ + verifyGetErrRequest({ + session, + host, + expected: [ + { + file: test, + syntax: [], + semantic: [], + suggestion: [] + } + ] + }); + }); + it("should report error when json is not root file found by tsconfig", () => { + const { host, session, test, blabla, tsconfig } = createSessionForTest({ + include: ["./src/*.ts"] + }); + const span = protocolTextSpanFromSubstring(test.content, `"./blabla.json"`); + verifyGetErrRequest({ + session, + host, + expected: [{ file: test, syntax: [], semantic: [ - createDiagnostic( - span.start, - span.end, - Diagnostics.File_0_is_not_listed_within_the_file_list_of_project_1_Projects_must_list_all_files_or_use_an_include_pattern, - [blabla.path, tsconfig.path] - ) + createDiagnostic(span.start, span.end, Diagnostics.File_0_is_not_listed_within_the_file_list_of_project_1_Projects_must_list_all_files_or_use_an_include_pattern, [blabla.path, tsconfig.path]) ], suggestion: [] }] - }); }); }); - - describe("unittests:: tsserver:: Project Errors with npm install when", () => { - function verifyNpmInstall(timeoutDuringPartialInstallation: boolean) { - const main: File = { - path: `${tscWatch.projectRoot}/src/main.ts`, - content: "import * as _a from '@angular/core';" - }; - const config: File = { - path: `${tscWatch.projectRoot}/tsconfig.json`, - content: "{}" - }; - // Move things from staging to node_modules without triggering watch - const moduleFile: File = { - path: `${tscWatch.projectRoot}/node_modules/@angular/core/index.d.ts`, - content: `export const y = 10;` - }; - const projectFiles = [main, libFile, config]; - const host = createServerHost(projectFiles); - const session = createSession(host, { canUseEvents: true }); - const service = session.getProjectService(); - openFilesForSession([{ file: main, projectRootPath: tscWatch.projectRoot }], session); - const span = protocolTextSpanFromSubstring(main.content, `'@angular/core'`); - const moduleNotFoundErr: protocol.Diagnostic[] = [ - createDiagnostic( - span.start, - span.end, - Diagnostics.Cannot_find_module_0, - ["@angular/core"] - ) - ]; - const expectedRecursiveWatches = arrayToMap([`${tscWatch.projectRoot}`, `${tscWatch.projectRoot}/src`, `${tscWatch.projectRoot}/node_modules`, `${tscWatch.projectRoot}/node_modules/@types`], identity, () => 1); - verifyProject(); - verifyErrors(moduleNotFoundErr); - - let npmInstallComplete = false; - - // Simulate npm install - let filesAndFoldersToAdd: (File | Folder)[] = [ - { path: `${tscWatch.projectRoot}/node_modules` }, // This should queue update - { path: `${tscWatch.projectRoot}/node_modules/.staging` }, - { path: `${tscWatch.projectRoot}/node_modules/.staging/@babel` }, - { path: `${tscWatch.projectRoot}/node_modules/.staging/@babel/helper-plugin-utils-a06c629f` }, - { path: `${tscWatch.projectRoot}/node_modules/.staging/core-js-db53158d` }, - ]; - verifyWhileNpmInstall({ timeouts: 2, semantic: moduleNotFoundErr }); - - filesAndFoldersToAdd = [ - { path: `${tscWatch.projectRoot}/node_modules/.staging/@angular/platform-browser-dynamic-5efaaa1a` }, - { path: `${tscWatch.projectRoot}/node_modules/.staging/@angular/cli-c1e44b05/models/analytics.d.ts`, content: `export const x = 10;` }, - { path: `${tscWatch.projectRoot}/node_modules/.staging/@angular/core-0963aebf/index.d.ts`, content: `export const y = 10;` }, - ]; - // Since we added/removed in .staging no timeout - verifyWhileNpmInstall({ timeouts: 0, semantic: moduleNotFoundErr }); - - filesAndFoldersToAdd = []; - host.ensureFileOrFolder(moduleFile, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ true, /*ignoreParentWatch*/ true); - // Since we added/removed in .staging no timeout - verifyWhileNpmInstall({ timeouts: 0, semantic: moduleNotFoundErr }); - - // Remove staging folder to remove errors - host.deleteFolder(`${tscWatch.projectRoot}/node_modules/.staging`, /*recursive*/ true); - npmInstallComplete = true; - projectFiles.push(moduleFile); - // Additional watch for watching script infos from node_modules - expectedRecursiveWatches.set(`${tscWatch.projectRoot}/node_modules`, 2); - verifyWhileNpmInstall({ timeouts: 2, semantic: [] }); - - function verifyWhileNpmInstall({ timeouts, semantic }: { timeouts: number; semantic: protocol.Diagnostic[] }) { - filesAndFoldersToAdd.forEach(f => host.ensureFileOrFolder(f)); - if (npmInstallComplete || timeoutDuringPartialInstallation) { - host.checkTimeoutQueueLengthAndRun(timeouts); - } - else { - host.checkTimeoutQueueLength(2); - } - verifyProject(); - verifyErrors(semantic, !npmInstallComplete && !timeoutDuringPartialInstallation ? 2 : undefined); +}); +describe("unittests:: tsserver:: Project Errors with npm install when", () => { + function verifyNpmInstall(timeoutDuringPartialInstallation: boolean) { + const main: File = { + path: `${projectRoot}/src/main.ts`, + content: "import * as _a from '@angular/core';" + }; + const config: File = { + path: `${projectRoot}/tsconfig.json`, + content: "{}" + }; + // Move things from staging to node_modules without triggering watch + const moduleFile: File = { + path: `${projectRoot}/node_modules/@angular/core/index.d.ts`, + content: `export const y = 10;` + }; + const projectFiles = [main, libFile, config]; + const host = createServerHost(projectFiles); + const session = createSession(host, { canUseEvents: true }); + const service = session.getProjectService(); + openFilesForSession([{ file: main, projectRootPath: projectRoot }], session); + const span = protocolTextSpanFromSubstring(main.content, `'@angular/core'`); + const moduleNotFoundErr: projectSystem.protocol.Diagnostic[] = [ + createDiagnostic(span.start, span.end, Diagnostics.Cannot_find_module_0, ["@angular/core"]) + ]; + const expectedRecursiveWatches = arrayToMap([`${projectRoot}`, `${projectRoot}/src`, `${projectRoot}/node_modules`, `${projectRoot}/node_modules/@types`], identity, () => 1); + verifyProject(); + verifyErrors(moduleNotFoundErr); + let npmInstallComplete = false; + // Simulate npm install + let filesAndFoldersToAdd: (File | Folder)[] = [ + { path: `${projectRoot}/node_modules` }, + { path: `${projectRoot}/node_modules/.staging` }, + { path: `${projectRoot}/node_modules/.staging/@babel` }, + { path: `${projectRoot}/node_modules/.staging/@babel/helper-plugin-utils-a06c629f` }, + { path: `${projectRoot}/node_modules/.staging/core-js-db53158d` }, + ]; + verifyWhileNpmInstall({ timeouts: 2, semantic: moduleNotFoundErr }); + filesAndFoldersToAdd = [ + { path: `${projectRoot}/node_modules/.staging/@angular/platform-browser-dynamic-5efaaa1a` }, + { path: `${projectRoot}/node_modules/.staging/@angular/cli-c1e44b05/models/analytics.d.ts`, content: `export const x = 10;` }, + { path: `${projectRoot}/node_modules/.staging/@angular/core-0963aebf/index.d.ts`, content: `export const y = 10;` }, + ]; + // Since we added/removed in .staging no timeout + verifyWhileNpmInstall({ timeouts: 0, semantic: moduleNotFoundErr }); + filesAndFoldersToAdd = []; + host.ensureFileOrFolder(moduleFile, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ true, /*ignoreParentWatch*/ true); + // Since we added/removed in .staging no timeout + verifyWhileNpmInstall({ timeouts: 0, semantic: moduleNotFoundErr }); + // Remove staging folder to remove errors + host.deleteFolder(`${projectRoot}/node_modules/.staging`, /*recursive*/ true); + npmInstallComplete = true; + projectFiles.push(moduleFile); + // Additional watch for watching script infos from node_modules + expectedRecursiveWatches.set(`${projectRoot}/node_modules`, 2); + verifyWhileNpmInstall({ timeouts: 2, semantic: [] }); + function verifyWhileNpmInstall({ timeouts, semantic }: { + timeouts: number; + semantic: projectSystem.protocol.Diagnostic[]; + }) { + filesAndFoldersToAdd.forEach(f => host.ensureFileOrFolder(f)); + if (npmInstallComplete || timeoutDuringPartialInstallation) { + host.checkTimeoutQueueLengthAndRun(timeouts); } - - function verifyProject() { - checkNumberOfConfiguredProjects(service, 1); - - const project = service.configuredProjects.get(config.path)!; - checkProjectActualFiles(project, map(projectFiles, f => f.path)); - - checkWatchedFilesDetailed(host, mapDefined(projectFiles, f => f === main || f === moduleFile ? undefined : f.path), 1); - checkWatchedDirectoriesDetailed(host, expectedRecursiveWatches, /*recursive*/ true); - checkWatchedDirectories(host, [], /*recursive*/ false); + else { + host.checkTimeoutQueueLength(2); } - - function verifyErrors(semantic: protocol.Diagnostic[], existingTimeouts?: number) { - verifyGetErrRequest({ - session, - host, - expected: [{ + verifyProject(); + verifyErrors(semantic, !npmInstallComplete && !timeoutDuringPartialInstallation ? 2 : undefined); + } + function verifyProject() { + checkNumberOfConfiguredProjects(service, 1); + const project = service.configuredProjects.get(config.path)!; + checkProjectActualFiles(project, map(projectFiles, f => f.path)); + checkWatchedFilesDetailed(host, mapDefined(projectFiles, f => f === main || f === moduleFile ? undefined : f.path), 1); + checkWatchedDirectoriesDetailed(host, expectedRecursiveWatches, /*recursive*/ true); + checkWatchedDirectories(host, [], /*recursive*/ false); + } + function verifyErrors(semantic: projectSystem.protocol.Diagnostic[], existingTimeouts?: number) { + verifyGetErrRequest({ + session, + host, + expected: [{ file: main, syntax: [], semantic, suggestion: [] }], - existingTimeouts - }); - - } + existingTimeouts + }); } - - it("timeouts occur inbetween installation", () => { - verifyNpmInstall(/*timeoutDuringPartialInstallation*/ true); - }); - - it("timeout occurs after installation", () => { - verifyNpmInstall(/*timeoutDuringPartialInstallation*/ false); - }); + } + it("timeouts occur inbetween installation", () => { + verifyNpmInstall(/*timeoutDuringPartialInstallation*/ true); + }); + it("timeout occurs after installation", () => { + verifyNpmInstall(/*timeoutDuringPartialInstallation*/ false); }); -} +}); diff --git a/src/testRunner/unittests/tsserver/projectReferenceCompileOnSave.ts b/src/testRunner/unittests/tsserver/projectReferenceCompileOnSave.ts index 86332b9621013..b66625a680ac8 100644 --- a/src/testRunner/unittests/tsserver/projectReferenceCompileOnSave.ts +++ b/src/testRunner/unittests/tsserver/projectReferenceCompileOnSave.ts @@ -1,239 +1,215 @@ -namespace ts.projectSystem { - describe("unittests:: tsserver:: with project references and compile on save", () => { - const dependecyLocation = `${tscWatch.projectRoot}/dependency`; - const usageLocation = `${tscWatch.projectRoot}/usage`; - const dependencyTs: File = { - path: `${dependecyLocation}/fns.ts`, - content: `export function fn1() { } +import { projectRoot, createSolutionBuilder } from "../../ts.tscWatch"; +import { File, createServerHost, libFile, createSession, openFilesForSession, protocol, protocolToLocation } from "../../ts.projectSystem"; +import { TestFSWithWatch, EmitOutput, emptyArray, changeExtension } from "../../ts"; +describe("unittests:: tsserver:: with project references and compile on save", () => { + const dependecyLocation = `${projectRoot}/dependency`; + const usageLocation = `${projectRoot}/usage`; + const dependencyTs: File = { + path: `${dependecyLocation}/fns.ts`, + content: `export function fn1() { } export function fn2() { } ` - }; - const dependencyConfig: File = { - path: `${dependecyLocation}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { composite: true, declarationDir: "../decls" }, - compileOnSave: true - }) - }; - const usageTs: File = { - path: `${usageLocation}/usage.ts`, - content: `import { + }; + const dependencyConfig: File = { + path: `${dependecyLocation}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { composite: true, declarationDir: "../decls" }, + compileOnSave: true + }) + }; + const usageTs: File = { + path: `${usageLocation}/usage.ts`, + content: `import { fn1, fn2, } from '../decls/fns' fn1(); fn2(); ` - }; - const usageConfig: File = { - path: `${usageLocation}/tsconfig.json`, - content: JSON.stringify({ - compileOnSave: true, - references: [{ path: "../dependency" }] - }) - }; - - interface VerifySingleScenarioWorker extends VerifySingleScenario { - withProject: boolean; - } - function verifySingleScenarioWorker({ - withProject, scenario, openFiles, requestArgs, change, expectedResult - }: VerifySingleScenarioWorker) { - it(scenario, () => { - const host = TestFSWithWatch.changeToHostTrackingWrittenFiles( - createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, libFile]) - ); - const session = createSession(host); - openFilesForSession(openFiles(), session); - const reqArgs = requestArgs(); - const { - expectedAffected, - expectedEmit: { expectedEmitSuccess, expectedFiles }, - expectedEmitOutput - } = expectedResult(withProject); - - if (change) { - session.executeCommandSeq({ - command: protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: { file: dependencyTs.path } + }; + const usageConfig: File = { + path: `${usageLocation}/tsconfig.json`, + content: JSON.stringify({ + compileOnSave: true, + references: [{ path: "../dependency" }] + }) + }; + interface VerifySingleScenarioWorker extends VerifySingleScenario { + withProject: boolean; + } + function verifySingleScenarioWorker({ withProject, scenario, openFiles, requestArgs, change, expectedResult }: VerifySingleScenarioWorker) { + it(scenario, () => { + const host = TestFSWithWatch.changeToHostTrackingWrittenFiles(createServerHost([dependencyTs, dependencyConfig, usageTs, usageConfig, libFile])); + const session = createSession(host); + openFilesForSession(openFiles(), session); + const reqArgs = requestArgs(); + const { expectedAffected, expectedEmit: { expectedEmitSuccess, expectedFiles }, expectedEmitOutput } = expectedResult(withProject); + if (change) { + session.executeCommandSeq({ + command: protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { file: dependencyTs.path } + }); + const { file, insertString } = change(); + if (session.getProjectService().openFiles.has(file.path)) { + const toLocation = protocolToLocation(file.content); + const location = toLocation(file.content.length); + session.executeCommandSeq({ + command: protocol.CommandTypes.Change, + arguments: { + file: file.path, + ...location, + endLine: location.line, + endOffset: location.offset, + insertString + } }); - const { file, insertString } = change(); - if (session.getProjectService().openFiles.has(file.path)) { - const toLocation = protocolToLocation(file.content); - const location = toLocation(file.content.length); - session.executeCommandSeq({ - command: protocol.CommandTypes.Change, - arguments: { - file: file.path, - ...location, - endLine: location.line, - endOffset: location.offset, - insertString - } - }); - } - else { - host.writeFile(file.path, `${file.content}${insertString}`); - } - host.writtenFiles.clear(); } - - const args = withProject ? reqArgs : { file: reqArgs.file }; - // Verify CompileOnSaveAffectedFileList - const actualAffectedFiles = session.executeCommandSeq({ - command: protocol.CommandTypes.CompileOnSaveAffectedFileList, - arguments: args - }).response as protocol.CompileOnSaveAffectedFileListSingleProject[]; - assert.deepEqual(actualAffectedFiles, expectedAffected, "Affected files"); - - // Verify CompileOnSaveEmit - const actualEmit = session.executeCommandSeq({ - command: protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: args - }).response; - assert.deepEqual(actualEmit, expectedEmitSuccess, "Emit files"); - assert.equal(host.writtenFiles.size, expectedFiles.length); - for (const file of expectedFiles) { - assert.equal(host.readFile(file.path), file.content, `Expected to write ${file.path}`); - assert.isTrue(host.writtenFiles.has(file.path), `${file.path} is newly written`); + else { + host.writeFile(file.path, `${file.content}${insertString}`); } - - // Verify EmitOutput - const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = session.executeCommandSeq({ - command: protocol.CommandTypes.EmitOutput, - arguments: args - }).response as EmitOutput; - assert.deepEqual(actualEmitOutput, expectedEmitOutput, "Emit output"); - }); - } - - interface VerifySingleScenario { - scenario: string; - openFiles: () => readonly File[]; - requestArgs: () => protocol.FileRequestArgs; - skipWithoutProject?: boolean; - change?: () => SingleScenarioChange; - expectedResult: GetSingleScenarioResult; - } - function verifySingleScenario(scenario: VerifySingleScenario) { - if (!scenario.skipWithoutProject) { - describe("without specifying project file", () => { - verifySingleScenarioWorker({ - withProject: false, - ...scenario - }); - }); + host.writtenFiles.clear(); + } + const args = withProject ? reqArgs : { file: reqArgs.file }; + // Verify CompileOnSaveAffectedFileList + const actualAffectedFiles = (session.executeCommandSeq({ + command: protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: args + }).response as protocol.CompileOnSaveAffectedFileListSingleProject[]); + assert.deepEqual(actualAffectedFiles, expectedAffected, "Affected files"); + // Verify CompileOnSaveEmit + const actualEmit = session.executeCommandSeq({ + command: protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: args + }).response; + assert.deepEqual(actualEmit, expectedEmitSuccess, "Emit files"); + assert.equal(host.writtenFiles.size, expectedFiles.length); + for (const file of expectedFiles) { + assert.equal(host.readFile(file.path), file.content, `Expected to write ${file.path}`); + assert.isTrue(host.writtenFiles.has(file.path), `${file.path} is newly written`); } - describe("with specifying project file", () => { + // Verify EmitOutput + const { exportedModulesFromDeclarationEmit: _1, ...actualEmitOutput } = (session.executeCommandSeq({ + command: protocol.CommandTypes.EmitOutput, + arguments: args + }).response as EmitOutput); + assert.deepEqual(actualEmitOutput, expectedEmitOutput, "Emit output"); + }); + } + interface VerifySingleScenario { + scenario: string; + openFiles: () => readonly File[]; + requestArgs: () => protocol.FileRequestArgs; + skipWithoutProject?: boolean; + change?: () => SingleScenarioChange; + expectedResult: GetSingleScenarioResult; + } + function verifySingleScenario(scenario: VerifySingleScenario) { + if (!scenario.skipWithoutProject) { + describe("without specifying project file", () => { verifySingleScenarioWorker({ - withProject: true, + withProject: false, ...scenario }); }); } - - interface SingleScenarioExpectedEmit { - expectedEmitSuccess: boolean; - expectedFiles: readonly File[]; - } - interface SingleScenarioResult { - expectedAffected: protocol.CompileOnSaveAffectedFileListSingleProject[]; - expectedEmit: SingleScenarioExpectedEmit; - expectedEmitOutput: EmitOutput; - } - type GetSingleScenarioResult = (withProject: boolean) => SingleScenarioResult; - interface SingleScenarioChange { - file: File; - insertString: string; - } - interface ScenarioDetails { - scenarioName: string; - requestArgs: () => protocol.FileRequestArgs; - skipWithoutProject?: boolean; - initial: GetSingleScenarioResult; - localChangeToDependency: GetSingleScenarioResult; - localChangeToUsage: GetSingleScenarioResult; - changeToDependency: GetSingleScenarioResult; - changeToUsage: GetSingleScenarioResult; - } - interface VerifyScenario { - openFiles: () => readonly File[]; - scenarios: readonly ScenarioDetails[]; - } - - const localChange = "function fn3() { }"; - const change = `export ${localChange}`; - const changeJs = `function fn3() { } + describe("with specifying project file", () => { + verifySingleScenarioWorker({ + withProject: true, + ...scenario + }); + }); + } + interface SingleScenarioExpectedEmit { + expectedEmitSuccess: boolean; + expectedFiles: readonly File[]; + } + interface SingleScenarioResult { + expectedAffected: protocol.CompileOnSaveAffectedFileListSingleProject[]; + expectedEmit: SingleScenarioExpectedEmit; + expectedEmitOutput: EmitOutput; + } + type GetSingleScenarioResult = (withProject: boolean) => SingleScenarioResult; + interface SingleScenarioChange { + file: File; + insertString: string; + } + interface ScenarioDetails { + scenarioName: string; + requestArgs: () => protocol.FileRequestArgs; + skipWithoutProject?: boolean; + initial: GetSingleScenarioResult; + localChangeToDependency: GetSingleScenarioResult; + localChangeToUsage: GetSingleScenarioResult; + changeToDependency: GetSingleScenarioResult; + changeToUsage: GetSingleScenarioResult; + } + interface VerifyScenario { + openFiles: () => readonly File[]; + scenarios: readonly ScenarioDetails[]; + } + const localChange = "function fn3() { }"; + const change = `export ${localChange}`; + const changeJs = `function fn3() { } exports.fn3 = fn3;`; - const changeDts = "export declare function fn3(): void;"; - function verifyScenario({ openFiles, scenarios }: VerifyScenario) { - for (const { - scenarioName, requestArgs, skipWithoutProject, initial, - localChangeToDependency, localChangeToUsage, - changeToDependency, changeToUsage - } of scenarios) { - describe(scenarioName, () => { - verifySingleScenario({ - scenario: "with initial file open", - openFiles, - requestArgs, - skipWithoutProject, - expectedResult: initial - }); - - verifySingleScenario({ - scenario: "with local change to dependency", - openFiles, - requestArgs, - skipWithoutProject, - change: () => ({ file: dependencyTs, insertString: localChange }), - expectedResult: localChangeToDependency - }); - - verifySingleScenario({ - scenario: "with local change to usage", - openFiles, - requestArgs, - skipWithoutProject, - change: () => ({ file: usageTs, insertString: localChange }), - expectedResult: localChangeToUsage - }); - - verifySingleScenario({ - scenario: "with change to dependency", - openFiles, - requestArgs, - skipWithoutProject, - change: () => ({ file: dependencyTs, insertString: change }), - expectedResult: changeToDependency - }); - - verifySingleScenario({ - scenario: "with change to usage", - openFiles, - requestArgs, - skipWithoutProject, - change: () => ({ file: usageTs, insertString: change }), - expectedResult: changeToUsage - }); + const changeDts = "export declare function fn3(): void;"; + function verifyScenario({ openFiles, scenarios }: VerifyScenario) { + for (const { scenarioName, requestArgs, skipWithoutProject, initial, localChangeToDependency, localChangeToUsage, changeToDependency, changeToUsage } of scenarios) { + describe(scenarioName, () => { + verifySingleScenario({ + scenario: "with initial file open", + openFiles, + requestArgs, + skipWithoutProject, + expectedResult: initial }); - } - } - - function expectedAffectedFiles(config: File, fileNames: File[]): protocol.CompileOnSaveAffectedFileListSingleProject { - return { - projectFileName: config.path, - fileNames: fileNames.map(f => f.path), - projectUsesOutFile: false - }; + verifySingleScenario({ + scenario: "with local change to dependency", + openFiles, + requestArgs, + skipWithoutProject, + change: () => ({ file: dependencyTs, insertString: localChange }), + expectedResult: localChangeToDependency + }); + verifySingleScenario({ + scenario: "with local change to usage", + openFiles, + requestArgs, + skipWithoutProject, + change: () => ({ file: usageTs, insertString: localChange }), + expectedResult: localChangeToUsage + }); + verifySingleScenario({ + scenario: "with change to dependency", + openFiles, + requestArgs, + skipWithoutProject, + change: () => ({ file: dependencyTs, insertString: change }), + expectedResult: changeToDependency + }); + verifySingleScenario({ + scenario: "with change to usage", + openFiles, + requestArgs, + skipWithoutProject, + change: () => ({ file: usageTs, insertString: change }), + expectedResult: changeToUsage + }); + }); } - - function expectedUsageEmit(appendJsText?: string): SingleScenarioExpectedEmit { - const appendJs = appendJsText ? `${appendJsText} + } + function expectedAffectedFiles(config: File, fileNames: File[]): protocol.CompileOnSaveAffectedFileListSingleProject { + return { + projectFileName: config.path, + fileNames: fileNames.map(f => f.path), + projectUsesOutFile: false + }; + } + function expectedUsageEmit(appendJsText?: string): SingleScenarioExpectedEmit { + const appendJs = appendJsText ? `${appendJsText} ` : ""; - return { - expectedEmitSuccess: true, - expectedFiles: [{ + return { + expectedEmitSuccess: true, + expectedFiles: [{ path: `${usageLocation}/usage.js`, content: `"use strict"; exports.__esModule = true;${appendJsText === changeJs ? "\nexports.fn3 = void 0;" : ""} @@ -242,49 +218,44 @@ fns_1.fn1(); fns_1.fn2(); ${appendJs}` }] - }; - } - - function expectedEmitOutput({ expectedFiles }: SingleScenarioExpectedEmit): EmitOutput { - return { - outputFiles: expectedFiles.map(({ path, content }) => ({ - name: path, - text: content, - writeByteOrderMark: false - })), - emitSkipped: false - }; - } - - function expectedUsageEmitOutput(appendJsText?: string): EmitOutput { - return expectedEmitOutput(expectedUsageEmit(appendJsText)); - } - - function noEmit(): SingleScenarioExpectedEmit { - return { - expectedEmitSuccess: false, - expectedFiles: emptyArray - }; - } - - function noEmitOutput(): EmitOutput { - return { - emitSkipped: true, - outputFiles: [] - }; - } - - function expectedDependencyEmit(appendJsText?: string, appendDtsText?: string): SingleScenarioExpectedEmit { - const appendJs = appendJsText ? `${appendJsText} + }; + } + function expectedEmitOutput({ expectedFiles }: SingleScenarioExpectedEmit): EmitOutput { + return { + outputFiles: expectedFiles.map(({ path, content }) => ({ + name: path, + text: content, + writeByteOrderMark: false + })), + emitSkipped: false + }; + } + function expectedUsageEmitOutput(appendJsText?: string): EmitOutput { + return expectedEmitOutput(expectedUsageEmit(appendJsText)); + } + function noEmit(): SingleScenarioExpectedEmit { + return { + expectedEmitSuccess: false, + expectedFiles: emptyArray + }; + } + function noEmitOutput(): EmitOutput { + return { + emitSkipped: true, + outputFiles: [] + }; + } + function expectedDependencyEmit(appendJsText?: string, appendDtsText?: string): SingleScenarioExpectedEmit { + const appendJs = appendJsText ? `${appendJsText} ` : ""; - const appendDts = appendDtsText ? `${appendDtsText} + const appendDts = appendDtsText ? `${appendDtsText} ` : ""; - return { - expectedEmitSuccess: true, - expectedFiles: [ - { - path: `${dependecyLocation}/fns.js`, - content: `"use strict"; + return { + expectedEmitSuccess: true, + expectedFiles: [ + { + path: `${dependecyLocation}/fns.js`, + content: `"use strict"; exports.__esModule = true; ${appendJsText === changeJs ? "exports.fn3 = " : ""}exports.fn2 = exports.fn1 = void 0; function fn1() { } @@ -292,198 +263,184 @@ exports.fn1 = fn1; function fn2() { } exports.fn2 = fn2; ${appendJs}` - }, - { - path: `${tscWatch.projectRoot}/decls/fns.d.ts`, - content: `export declare function fn1(): void; + }, + { + path: `${projectRoot}/decls/fns.d.ts`, + content: `export declare function fn1(): void; export declare function fn2(): void; ${appendDts}` - } - ] + } + ] + }; + } + function expectedDependencyEmitOutput(appendJsText?: string, appendDtsText?: string): EmitOutput { + return expectedEmitOutput(expectedDependencyEmit(appendJsText, appendDtsText)); + } + function scenarioDetailsOfUsage(isDependencyOpen?: boolean): ScenarioDetails[] { + return [ + { + scenarioName: "Of usageTs", + requestArgs: () => ({ file: usageTs.path, projectFileName: usageConfig.path }), + initial: () => initialUsageTs(), + // no change to usage so same as initial only usage file + localChangeToDependency: () => initialUsageTs(), + localChangeToUsage: () => initialUsageTs(localChange), + changeToDependency: () => initialUsageTs(), + changeToUsage: () => initialUsageTs(changeJs) + }, + { + scenarioName: "Of dependencyTs in usage project", + requestArgs: () => ({ file: dependencyTs.path, projectFileName: usageConfig.path }), + skipWithoutProject: !!isDependencyOpen, + initial: () => initialDependencyTs(), + localChangeToDependency: () => initialDependencyTs(/*noUsageFiles*/ true), + localChangeToUsage: () => initialDependencyTs(/*noUsageFiles*/ true), + changeToDependency: () => initialDependencyTs(), + changeToUsage: () => initialDependencyTs(/*noUsageFiles*/ true) + } + ]; + function initialUsageTs(jsText?: string) { + return { + expectedAffected: [ + expectedAffectedFiles(usageConfig, [usageTs]) + ], + expectedEmit: expectedUsageEmit(jsText), + expectedEmitOutput: expectedUsageEmitOutput(jsText) }; } - - function expectedDependencyEmitOutput(appendJsText?: string, appendDtsText?: string): EmitOutput { - return expectedEmitOutput(expectedDependencyEmit(appendJsText, appendDtsText)); + function initialDependencyTs(noUsageFiles?: true) { + return { + expectedAffected: [ + expectedAffectedFiles(usageConfig, noUsageFiles ? [] : [usageTs]) + ], + expectedEmit: noEmit(), + expectedEmitOutput: noEmitOutput() + }; } - - function scenarioDetailsOfUsage(isDependencyOpen?: boolean): ScenarioDetails[] { - return [ - { - scenarioName: "Of usageTs", - requestArgs: () => ({ file: usageTs.path, projectFileName: usageConfig.path }), - initial: () => initialUsageTs(), - // no change to usage so same as initial only usage file - localChangeToDependency: () => initialUsageTs(), - localChangeToUsage: () => initialUsageTs(localChange), - changeToDependency: () => initialUsageTs(), - changeToUsage: () => initialUsageTs(changeJs) - }, - { - scenarioName: "Of dependencyTs in usage project", - requestArgs: () => ({ file: dependencyTs.path, projectFileName: usageConfig.path }), - skipWithoutProject: !!isDependencyOpen, - initial: () => initialDependencyTs(), - localChangeToDependency: () => initialDependencyTs(/*noUsageFiles*/ true), - localChangeToUsage: () => initialDependencyTs(/*noUsageFiles*/ true), - changeToDependency: () => initialDependencyTs(), - changeToUsage: () => initialDependencyTs(/*noUsageFiles*/ true) - } - ]; - - function initialUsageTs(jsText?: string) { - return { - expectedAffected: [ - expectedAffectedFiles(usageConfig, [usageTs]) - ], - expectedEmit: expectedUsageEmit(jsText), - expectedEmitOutput: expectedUsageEmitOutput(jsText) - }; - } - - function initialDependencyTs(noUsageFiles?: true) { - return { - expectedAffected: [ - expectedAffectedFiles(usageConfig, noUsageFiles ? [] : [usageTs]) + } + function scenarioDetailsOfDependencyWhenOpen(): ScenarioDetails { + return { + scenarioName: "Of dependencyTs", + requestArgs: () => ({ file: dependencyTs.path, projectFileName: dependencyConfig.path }), + initial, + localChangeToDependency: withProject => ({ + expectedAffected: withProject ? + [ + expectedAffectedFiles(dependencyConfig, [dependencyTs]) + ] : + [ + expectedAffectedFiles(usageConfig, []), + expectedAffectedFiles(dependencyConfig, [dependencyTs]) ], - expectedEmit: noEmit(), - expectedEmitOutput: noEmitOutput() - }; - } - } - - function scenarioDetailsOfDependencyWhenOpen(): ScenarioDetails { + expectedEmit: expectedDependencyEmit(localChange), + expectedEmitOutput: expectedDependencyEmitOutput(localChange) + }), + localChangeToUsage: withProject => initial(withProject, /*noUsageFiles*/ true), + changeToDependency: withProject => initial(withProject, /*noUsageFiles*/ undefined, changeJs, changeDts), + changeToUsage: withProject => initial(withProject, /*noUsageFiles*/ true) + }; + function initial(withProject: boolean, noUsageFiles?: true, appendJs?: string, appendDts?: string): SingleScenarioResult { return { - scenarioName: "Of dependencyTs", - requestArgs: () => ({ file: dependencyTs.path, projectFileName: dependencyConfig.path }), - initial, - localChangeToDependency: withProject => ({ - expectedAffected: withProject ? - [ - expectedAffectedFiles(dependencyConfig, [dependencyTs]) - ] : - [ - expectedAffectedFiles(usageConfig, []), - expectedAffectedFiles(dependencyConfig, [dependencyTs]) - ], - expectedEmit: expectedDependencyEmit(localChange), - expectedEmitOutput: expectedDependencyEmitOutput(localChange) - }), - localChangeToUsage: withProject => initial(withProject, /*noUsageFiles*/ true), - changeToDependency: withProject => initial(withProject, /*noUsageFiles*/ undefined, changeJs, changeDts), - changeToUsage: withProject => initial(withProject, /*noUsageFiles*/ true) + expectedAffected: withProject ? + [ + expectedAffectedFiles(dependencyConfig, [dependencyTs]) + ] : + [ + expectedAffectedFiles(usageConfig, noUsageFiles ? [] : [usageTs]), + expectedAffectedFiles(dependencyConfig, [dependencyTs]) + ], + expectedEmit: expectedDependencyEmit(appendJs, appendDts), + expectedEmitOutput: expectedDependencyEmitOutput(appendJs, appendDts) }; - - function initial(withProject: boolean, noUsageFiles?: true, appendJs?: string, appendDts?: string): SingleScenarioResult { - return { - expectedAffected: withProject ? - [ - expectedAffectedFiles(dependencyConfig, [dependencyTs]) - ] : - [ - expectedAffectedFiles(usageConfig, noUsageFiles ? [] : [usageTs]), - expectedAffectedFiles(dependencyConfig, [dependencyTs]) - ], - expectedEmit: expectedDependencyEmit(appendJs, appendDts), - expectedEmitOutput: expectedDependencyEmitOutput(appendJs, appendDts) - }; - } } - - describe("when dependency project is not open", () => { - verifyScenario({ - openFiles: () => [usageTs], - scenarios: scenarioDetailsOfUsage() - }); + } + describe("when dependency project is not open", () => { + verifyScenario({ + openFiles: () => [usageTs], + scenarios: scenarioDetailsOfUsage() }); - - describe("when the depedency file is open", () => { - verifyScenario({ - openFiles: () => [usageTs, dependencyTs], - scenarios: [ - ...scenarioDetailsOfUsage(/*isDependencyOpen*/ true), - scenarioDetailsOfDependencyWhenOpen(), - ] - }); + }); + describe("when the depedency file is open", () => { + verifyScenario({ + openFiles: () => [usageTs, dependencyTs], + scenarios: [ + ...scenarioDetailsOfUsage(/*isDependencyOpen*/ true), + scenarioDetailsOfDependencyWhenOpen(), + ] }); }); - - describe("unittests:: tsserver:: with project references and compile on save with external projects", () => { - it("compile on save emits same output as project build", () => { - const tsbaseJson: File = { - path: `${tscWatch.projectRoot}/tsbase.json`, - content: JSON.stringify({ - compileOnSave: true, - compilerOptions: { - module: "none", - composite: true - } - }) - }; - const buttonClass = `${tscWatch.projectRoot}/buttonClass`; - const buttonConfig: File = { - path: `${buttonClass}/tsconfig.json`, - content: JSON.stringify({ - extends: "../tsbase.json", - compilerOptions: { - outFile: "Source.js" - }, - files: ["Source.ts"] - }) - }; - const buttonSource: File = { - path: `${buttonClass}/Source.ts`, - content: `module Hmi { +}); +describe("unittests:: tsserver:: with project references and compile on save with external projects", () => { + it("compile on save emits same output as project build", () => { + const tsbaseJson: File = { + path: `${projectRoot}/tsbase.json`, + content: JSON.stringify({ + compileOnSave: true, + compilerOptions: { + module: "none", + composite: true + } + }) + }; + const buttonClass = `${projectRoot}/buttonClass`; + const buttonConfig: File = { + path: `${buttonClass}/tsconfig.json`, + content: JSON.stringify({ + extends: "../tsbase.json", + compilerOptions: { + outFile: "Source.js" + }, + files: ["Source.ts"] + }) + }; + const buttonSource: File = { + path: `${buttonClass}/Source.ts`, + content: `module Hmi { export class Button { public static myStaticFunction() { } } }` - }; - - const siblingClass = `${tscWatch.projectRoot}/SiblingClass`; - const siblingConfig: File = { - path: `${siblingClass}/tsconfig.json`, - content: JSON.stringify({ - extends: "../tsbase.json", - references: [{ + }; + const siblingClass = `${projectRoot}/SiblingClass`; + const siblingConfig: File = { + path: `${siblingClass}/tsconfig.json`, + content: JSON.stringify({ + extends: "../tsbase.json", + references: [{ path: "../buttonClass/" }], - compilerOptions: { - outFile: "Source.js" - }, - files: ["Source.ts"] - }) - }; - const siblingSource: File = { - path: `${siblingClass}/Source.ts`, - content: `module Hmi { + compilerOptions: { + outFile: "Source.js" + }, + files: ["Source.ts"] + }) + }; + const siblingSource: File = { + path: `${siblingClass}/Source.ts`, + content: `module Hmi { export class Sibling { public mySiblingFunction() { } } }` - }; - const host = createServerHost([libFile, tsbaseJson, buttonConfig, buttonSource, siblingConfig, siblingSource], { useCaseSensitiveFileNames: true }); - - // ts build should succeed - const solutionBuilder = tscWatch.createSolutionBuilder(host, [siblingConfig.path], {}); - solutionBuilder.build(); - assert.equal(host.getOutput().length, 0, JSON.stringify(host.getOutput(), /*replacer*/ undefined, " ")); - const sourceJs = changeExtension(siblingSource.path, ".js"); - const expectedSiblingJs = host.readFile(sourceJs); - - const session = createSession(host); - openFilesForSession([siblingSource], session); - - session.executeCommandSeq({ - command: protocol.CommandTypes.CompileOnSaveEmitFile, - arguments: { - file: siblingSource.path, - projectFileName: siblingConfig.path - } - }); - assert.equal(host.readFile(sourceJs), expectedSiblingJs); + }; + const host = createServerHost([libFile, tsbaseJson, buttonConfig, buttonSource, siblingConfig, siblingSource], { useCaseSensitiveFileNames: true }); + // ts build should succeed + const solutionBuilder = createSolutionBuilder(host, [siblingConfig.path], {}); + solutionBuilder.build(); + assert.equal(host.getOutput().length, 0, JSON.stringify(host.getOutput(), /*replacer*/ undefined, " ")); + const sourceJs = changeExtension(siblingSource.path, ".js"); + const expectedSiblingJs = host.readFile(sourceJs); + const session = createSession(host); + openFilesForSession([siblingSource], session); + session.executeCommandSeq({ + command: protocol.CommandTypes.CompileOnSaveEmitFile, + arguments: { + file: siblingSource.path, + projectFileName: siblingConfig.path + } }); + assert.equal(host.readFile(sourceJs), expectedSiblingJs); }); -} +}); diff --git a/src/testRunner/unittests/tsserver/projectReferenceErrors.ts b/src/testRunner/unittests/tsserver/projectReferenceErrors.ts index a034d41dc5213..61c4bb142d150 100644 --- a/src/testRunner/unittests/tsserver/projectReferenceErrors.ts +++ b/src/testRunner/unittests/tsserver/projectReferenceErrors.ts @@ -1,333 +1,308 @@ -namespace ts.projectSystem { - export interface GetErrDiagnostics { - file: string | File; - syntax?: protocol.Diagnostic[]; - semantic?: protocol.Diagnostic[]; - suggestion?: protocol.Diagnostic[]; +import { File, protocol, TestSession, TestServerHost, checkErrorMessage, checkCompleteEvent, createServerHost, libFile, createSession, openFilesForSession, createSessionWithEventTracking, createDiagnostic } from "../../ts.projectSystem"; +import { isString, noop, emptyArray, Diagnostics } from "../../ts"; +import { projectRoot } from "../../ts.tscWatch"; +import { ConfigFileDiagEvent } from "../../ts.server"; +export interface GetErrDiagnostics { + file: string | File; + syntax?: protocol.Diagnostic[]; + semantic?: protocol.Diagnostic[]; + suggestion?: protocol.Diagnostic[]; +} +export interface VerifyGetErrRequestBase { + session: TestSession; + host: TestServerHost; + onErrEvent?: () => void; + existingTimeouts?: number; +} +export interface VerifyGetErrRequest extends VerifyGetErrRequestBase { + expected: readonly GetErrDiagnostics[]; +} +export function verifyGetErrRequest(request: VerifyGetErrRequest) { + const { session, expected } = request; + session.clearMessages(); + const expectedSequenceId = session.getNextSeq(); + session.executeCommandSeq({ + command: protocol.CommandTypes.Geterr, + arguments: { + delay: 0, + files: expected.map(f => filePath(f.file)) + } + }); + checkAllErrors({ ...request, expectedSequenceId }); +} +export interface CheckAllErrors extends VerifyGetErrRequest { + expectedSequenceId: number; +} +function checkAllErrors({ expected, expectedSequenceId, ...rest }: CheckAllErrors) { + for (let i = 0; i < expected.length; i++) { + checkErrorsInFile({ + ...rest, + expected: expected[i], + expectedSequenceId: i === expected.length - 1 ? expectedSequenceId : undefined, + }); + } +} +function filePath(file: string | File) { + return isString(file) ? file : file.path; +} +interface CheckErrorsInFile extends VerifyGetErrRequestBase { + expected: GetErrDiagnostics; + expectedSequenceId?: number; +} +function checkErrorsInFile({ session, host, onErrEvent, existingTimeouts, expectedSequenceId, expected: { file, syntax, semantic, suggestion }, }: CheckErrorsInFile) { + onErrEvent = onErrEvent || noop; + if (existingTimeouts !== undefined) { + host.checkTimeoutQueueLength(existingTimeouts + 1); + host.runQueuedTimeoutCallbacks(host.getNextTimeoutId() - 1); } - export interface VerifyGetErrRequestBase { - session: TestSession; - host: TestServerHost; - onErrEvent?: () => void; - existingTimeouts?: number; + else { + host.checkTimeoutQueueLengthAndRun(1); } - export interface VerifyGetErrRequest extends VerifyGetErrRequestBase { - expected: readonly GetErrDiagnostics[]; + if (syntax) { + onErrEvent(); + checkErrorMessage(session, "syntaxDiag", { file: filePath(file), diagnostics: syntax }); } - export function verifyGetErrRequest(request: VerifyGetErrRequest) { - const { session, expected } = request; + if (semantic) { session.clearMessages(); - const expectedSequenceId = session.getNextSeq(); - session.executeCommandSeq({ - command: protocol.CommandTypes.Geterr, - arguments: { - delay: 0, - files: expected.map(f => filePath(f.file)) + host.runQueuedImmediateCallbacks(1); + onErrEvent(); + checkErrorMessage(session, "semanticDiag", { file: filePath(file), diagnostics: semantic }); + } + if (suggestion) { + session.clearMessages(); + host.runQueuedImmediateCallbacks(1); + onErrEvent(); + checkErrorMessage(session, "suggestionDiag", { file: filePath(file), diagnostics: suggestion }); + } + if (expectedSequenceId !== undefined) { + checkCompleteEvent(session, syntax || semantic || suggestion ? 2 : 1, expectedSequenceId); + } + session.clearMessages(); +} +describe("unittests:: tsserver:: with project references and error reporting", () => { + const dependecyLocation = `${projectRoot}/dependency`; + const usageLocation = `${projectRoot}/usage`; + function verifyErrorsUsingGeterr({ allFiles, openFiles, expectedGetErr }: VerifyScenario) { + it("verifies the errors in open file", () => { + const host = createServerHost([...allFiles(), libFile]); + const session = createSession(host, { canUseEvents: true, }); + openFilesForSession(openFiles(), session); + verifyGetErrRequest({ session, host, expected: expectedGetErr() }); + }); + } + function verifyErrorsUsingGeterrForProject({ allFiles, openFiles, expectedGetErrForProject }: VerifyScenario) { + it("verifies the errors in projects", () => { + const host = createServerHost([...allFiles(), libFile]); + const session = createSession(host, { canUseEvents: true, }); + openFilesForSession(openFiles(), session); + session.clearMessages(); + for (const expected of expectedGetErrForProject()) { + const expectedSequenceId = session.getNextSeq(); + session.executeCommandSeq({ + command: protocol.CommandTypes.GeterrForProject, + arguments: { + delay: 0, + file: expected.project + } + }); + checkAllErrors({ session, host, expected: expected.errors, expectedSequenceId }); } }); - checkAllErrors({ ...request, expectedSequenceId }); } - - export interface CheckAllErrors extends VerifyGetErrRequest { - expectedSequenceId: number; + function verifyErrorsUsingSyncMethods({ allFiles, openFiles, expectedSyncDiagnostics }: VerifyScenario) { + it("verifies the errors using sync commands", () => { + const host = createServerHost([...allFiles(), libFile]); + const session = createSession(host); + openFilesForSession(openFiles(), session); + for (const { file, project, syntax, semantic, suggestion } of expectedSyncDiagnostics()) { + const actualSyntax = (session.executeCommandSeq({ + command: protocol.CommandTypes.SyntacticDiagnosticsSync, + arguments: { + file: filePath(file), + projectFileName: project + } + }).response as protocol.Diagnostic[]); + assert.deepEqual(actualSyntax, syntax, `Syntax diagnostics for file: ${filePath(file)}, project: ${project}`); + const actualSemantic = (session.executeCommandSeq({ + command: protocol.CommandTypes.SemanticDiagnosticsSync, + arguments: { + file: filePath(file), + projectFileName: project + } + }).response as protocol.Diagnostic[]); + assert.deepEqual(actualSemantic, semantic, `Semantic diagnostics for file: ${filePath(file)}, project: ${project}`); + const actualSuggestion = (session.executeCommandSeq({ + command: protocol.CommandTypes.SuggestionDiagnosticsSync, + arguments: { + file: filePath(file), + projectFileName: project + } + }).response as protocol.Diagnostic[]); + assert.deepEqual(actualSuggestion, suggestion, `Suggestion diagnostics for file: ${filePath(file)}, project: ${project}`); + } + }); } - function checkAllErrors({ expected, expectedSequenceId, ...rest }: CheckAllErrors) { - for (let i = 0; i < expected.length; i++) { - checkErrorsInFile({ - ...rest, - expected: expected[i], - expectedSequenceId: i === expected.length - 1 ? expectedSequenceId : undefined, - }); - } + function verifyConfigFileErrors({ allFiles, openFiles, expectedConfigFileDiagEvents }: VerifyScenario) { + it("verify config file errors", () => { + const host = createServerHost([...allFiles(), libFile]); + const { session, events } = createSessionWithEventTracking(host, ConfigFileDiagEvent); + for (const file of openFiles()) { + session.executeCommandSeq({ + command: protocol.CommandTypes.Open, + arguments: { file: file.path } + }); + } + assert.deepEqual(events, expectedConfigFileDiagEvents().map(data => ({ + eventName: ConfigFileDiagEvent, + data + }))); + }); } - - function filePath(file: string | File) { - return isString(file) ? file : file.path; + interface GetErrForProjectDiagnostics { + project: string; + errors: readonly GetErrDiagnostics[]; } - interface CheckErrorsInFile extends VerifyGetErrRequestBase { - expected: GetErrDiagnostics; - expectedSequenceId?: number; + interface SyncDiagnostics extends GetErrDiagnostics { + project?: string; } - function checkErrorsInFile({ - session, host, onErrEvent, existingTimeouts, expectedSequenceId, - expected: { file, syntax, semantic, suggestion }, - }: CheckErrorsInFile) { - onErrEvent = onErrEvent || noop; - if (existingTimeouts !== undefined) { - host.checkTimeoutQueueLength(existingTimeouts + 1); - host.runQueuedTimeoutCallbacks(host.getNextTimeoutId() - 1); - } - else { - host.checkTimeoutQueueLengthAndRun(1); - } - if (syntax) { - onErrEvent(); - checkErrorMessage(session, "syntaxDiag", { file: filePath(file), diagnostics: syntax }); - } - if (semantic) { - session.clearMessages(); - - host.runQueuedImmediateCallbacks(1); - onErrEvent(); - checkErrorMessage(session, "semanticDiag", { file: filePath(file), diagnostics: semantic }); - } - if (suggestion) { - session.clearMessages(); - - host.runQueuedImmediateCallbacks(1); - onErrEvent(); - checkErrorMessage(session, "suggestionDiag", { file: filePath(file), diagnostics: suggestion }); - } - if (expectedSequenceId !== undefined) { - checkCompleteEvent(session, syntax || semantic || suggestion ? 2 : 1, expectedSequenceId); - } - session.clearMessages(); + interface VerifyScenario { + allFiles: () => readonly File[]; + openFiles: () => readonly File[]; + expectedGetErr: () => readonly GetErrDiagnostics[]; + expectedGetErrForProject: () => readonly GetErrForProjectDiagnostics[]; + expectedSyncDiagnostics: () => readonly SyncDiagnostics[]; + expectedConfigFileDiagEvents: () => readonly ConfigFileDiagEvent["data"][]; } - - describe("unittests:: tsserver:: with project references and error reporting", () => { - const dependecyLocation = `${tscWatch.projectRoot}/dependency`; - const usageLocation = `${tscWatch.projectRoot}/usage`; - - function verifyErrorsUsingGeterr({ allFiles, openFiles, expectedGetErr }: VerifyScenario) { - it("verifies the errors in open file", () => { - const host = createServerHost([...allFiles(), libFile]); - const session = createSession(host, { canUseEvents: true, }); - openFilesForSession(openFiles(), session); - - verifyGetErrRequest({ session, host, expected: expectedGetErr() }); - }); - } - - function verifyErrorsUsingGeterrForProject({ allFiles, openFiles, expectedGetErrForProject }: VerifyScenario) { - it("verifies the errors in projects", () => { - const host = createServerHost([...allFiles(), libFile]); - const session = createSession(host, { canUseEvents: true, }); - openFilesForSession(openFiles(), session); - - session.clearMessages(); - for (const expected of expectedGetErrForProject()) { - const expectedSequenceId = session.getNextSeq(); - session.executeCommandSeq({ - command: protocol.CommandTypes.GeterrForProject, - arguments: { - delay: 0, - file: expected.project - } - }); - - checkAllErrors({ session, host, expected: expected.errors, expectedSequenceId }); - } - }); - } - - function verifyErrorsUsingSyncMethods({ allFiles, openFiles, expectedSyncDiagnostics }: VerifyScenario) { - it("verifies the errors using sync commands", () => { - const host = createServerHost([...allFiles(), libFile]); - const session = createSession(host); - openFilesForSession(openFiles(), session); - for (const { file, project, syntax, semantic, suggestion } of expectedSyncDiagnostics()) { - const actualSyntax = session.executeCommandSeq({ - command: protocol.CommandTypes.SyntacticDiagnosticsSync, - arguments: { - file: filePath(file), - projectFileName: project - } - }).response as protocol.Diagnostic[]; - assert.deepEqual(actualSyntax, syntax, `Syntax diagnostics for file: ${filePath(file)}, project: ${project}`); - const actualSemantic = session.executeCommandSeq({ - command: protocol.CommandTypes.SemanticDiagnosticsSync, - arguments: { - file: filePath(file), - projectFileName: project - } - }).response as protocol.Diagnostic[]; - assert.deepEqual(actualSemantic, semantic, `Semantic diagnostics for file: ${filePath(file)}, project: ${project}`); - const actualSuggestion = session.executeCommandSeq({ - command: protocol.CommandTypes.SuggestionDiagnosticsSync, - arguments: { - file: filePath(file), - projectFileName: project - } - }).response as protocol.Diagnostic[]; - assert.deepEqual(actualSuggestion, suggestion, `Suggestion diagnostics for file: ${filePath(file)}, project: ${project}`); - } - }); - } - - function verifyConfigFileErrors({ allFiles, openFiles, expectedConfigFileDiagEvents }: VerifyScenario) { - it("verify config file errors", () => { - const host = createServerHost([...allFiles(), libFile]); - const { session, events } = createSessionWithEventTracking(host, server.ConfigFileDiagEvent); - - for (const file of openFiles()) { - session.executeCommandSeq({ - command: protocol.CommandTypes.Open, - arguments: { file: file.path } - }); - } - - assert.deepEqual(events, expectedConfigFileDiagEvents().map(data => ({ - eventName: server.ConfigFileDiagEvent, - data - }))); - }); - } - - interface GetErrForProjectDiagnostics { - project: string; - errors: readonly GetErrDiagnostics[]; - } - interface SyncDiagnostics extends GetErrDiagnostics { - project?: string; - } - interface VerifyScenario { - allFiles: () => readonly File[]; - openFiles: () => readonly File[]; - expectedGetErr: () => readonly GetErrDiagnostics[]; - expectedGetErrForProject: () => readonly GetErrForProjectDiagnostics[]; - expectedSyncDiagnostics: () => readonly SyncDiagnostics[]; - expectedConfigFileDiagEvents: () => readonly server.ConfigFileDiagEvent["data"][]; - } - function verifyScenario(scenario: VerifyScenario) { - verifyErrorsUsingGeterr(scenario); - verifyErrorsUsingGeterrForProject(scenario); - verifyErrorsUsingSyncMethods(scenario); - verifyConfigFileErrors(scenario); + function verifyScenario(scenario: VerifyScenario) { + verifyErrorsUsingGeterr(scenario); + verifyErrorsUsingGeterrForProject(scenario); + verifyErrorsUsingSyncMethods(scenario); + verifyConfigFileErrors(scenario); + } + function emptyDiagnostics(file: File): GetErrDiagnostics { + return { + file, + syntax: emptyArray, + semantic: emptyArray, + suggestion: emptyArray + }; + } + function syncDiagnostics(diagnostics: GetErrDiagnostics, project: string): SyncDiagnostics { + return { project, ...diagnostics }; + } + interface VerifyUsageAndDependency { + allFiles: readonly [File, File, File, File]; // dependencyTs, dependencyConfig, usageTs, usageConfig + usageDiagnostics(): GetErrDiagnostics; + dependencyDiagnostics(): GetErrDiagnostics; + } + function verifyUsageAndDependency({ allFiles, usageDiagnostics, dependencyDiagnostics }: VerifyUsageAndDependency) { + const [dependencyTs, dependencyConfig, usageTs, usageConfig] = allFiles; + function usageProjectDiagnostics(): GetErrForProjectDiagnostics { + return { + project: usageTs.path, + errors: [ + usageDiagnostics(), + emptyDiagnostics(dependencyTs) + ] + }; } - - function emptyDiagnostics(file: File): GetErrDiagnostics { + function dependencyProjectDiagnostics(): GetErrForProjectDiagnostics { return { - file, - syntax: emptyArray, - semantic: emptyArray, - suggestion: emptyArray + project: dependencyTs.path, + errors: [ + dependencyDiagnostics() + ] }; } - - function syncDiagnostics(diagnostics: GetErrDiagnostics, project: string): SyncDiagnostics { - return { project, ...diagnostics }; + function usageConfigDiag(): ConfigFileDiagEvent["data"] { + return { + triggerFile: usageTs.path, + configFileName: usageConfig.path, + diagnostics: emptyArray + }; } - - interface VerifyUsageAndDependency { - allFiles: readonly [File, File, File, File]; // dependencyTs, dependencyConfig, usageTs, usageConfig - usageDiagnostics(): GetErrDiagnostics; - dependencyDiagnostics(): GetErrDiagnostics; - + function dependencyConfigDiag(): ConfigFileDiagEvent["data"] { + return { + triggerFile: dependencyTs.path, + configFileName: dependencyConfig.path, + diagnostics: emptyArray + }; } - function verifyUsageAndDependency({ allFiles, usageDiagnostics, dependencyDiagnostics }: VerifyUsageAndDependency) { - const [dependencyTs, dependencyConfig, usageTs, usageConfig] = allFiles; - function usageProjectDiagnostics(): GetErrForProjectDiagnostics { - return { - project: usageTs.path, - errors: [ - usageDiagnostics(), - emptyDiagnostics(dependencyTs) - ] - }; - } - - function dependencyProjectDiagnostics(): GetErrForProjectDiagnostics { - return { - project: dependencyTs.path, - errors: [ - dependencyDiagnostics() - ] - }; - } - - function usageConfigDiag(): server.ConfigFileDiagEvent["data"] { - return { - triggerFile: usageTs.path, - configFileName: usageConfig.path, - diagnostics: emptyArray - }; - } - - function dependencyConfigDiag(): server.ConfigFileDiagEvent["data"] { - return { - triggerFile: dependencyTs.path, - configFileName: dependencyConfig.path, - diagnostics: emptyArray - }; - } - - describe("when dependency project is not open", () => { - verifyScenario({ - allFiles: () => allFiles, - openFiles: () => [usageTs], - expectedGetErr: () => [ - usageDiagnostics() - ], - expectedGetErrForProject: () => [ - usageProjectDiagnostics(), - { - project: dependencyTs.path, - errors: [ - emptyDiagnostics(dependencyTs), - usageDiagnostics() - ] - } - ], - expectedSyncDiagnostics: () => [ - // Without project - usageDiagnostics(), - emptyDiagnostics(dependencyTs), - // With project - syncDiagnostics(usageDiagnostics(), usageConfig.path), - syncDiagnostics(emptyDiagnostics(dependencyTs), usageConfig.path), - ], - expectedConfigFileDiagEvents: () => [ - usageConfigDiag() - ], - }); + describe("when dependency project is not open", () => { + verifyScenario({ + allFiles: () => allFiles, + openFiles: () => [usageTs], + expectedGetErr: () => [ + usageDiagnostics() + ], + expectedGetErrForProject: () => [ + usageProjectDiagnostics(), + { + project: dependencyTs.path, + errors: [ + emptyDiagnostics(dependencyTs), + usageDiagnostics() + ] + } + ], + expectedSyncDiagnostics: () => [ + // Without project + usageDiagnostics(), + emptyDiagnostics(dependencyTs), + // With project + syncDiagnostics(usageDiagnostics(), usageConfig.path), + syncDiagnostics(emptyDiagnostics(dependencyTs), usageConfig.path), + ], + expectedConfigFileDiagEvents: () => [ + usageConfigDiag() + ], }); - - describe("when the depedency file is open", () => { - verifyScenario({ - allFiles: () => allFiles, - openFiles: () => [usageTs, dependencyTs], - expectedGetErr: () => [ - usageDiagnostics(), - dependencyDiagnostics(), - ], - expectedGetErrForProject: () => [ - usageProjectDiagnostics(), - dependencyProjectDiagnostics() - ], - expectedSyncDiagnostics: () => [ - // Without project - usageDiagnostics(), - dependencyDiagnostics(), - // With project - syncDiagnostics(usageDiagnostics(), usageConfig.path), - syncDiagnostics(emptyDiagnostics(dependencyTs), usageConfig.path), - syncDiagnostics(dependencyDiagnostics(), dependencyConfig.path), - ], - expectedConfigFileDiagEvents: () => [ - usageConfigDiag(), - dependencyConfigDiag() - ], - }); + }); + describe("when the depedency file is open", () => { + verifyScenario({ + allFiles: () => allFiles, + openFiles: () => [usageTs, dependencyTs], + expectedGetErr: () => [ + usageDiagnostics(), + dependencyDiagnostics(), + ], + expectedGetErrForProject: () => [ + usageProjectDiagnostics(), + dependencyProjectDiagnostics() + ], + expectedSyncDiagnostics: () => [ + // Without project + usageDiagnostics(), + dependencyDiagnostics(), + // With project + syncDiagnostics(usageDiagnostics(), usageConfig.path), + syncDiagnostics(emptyDiagnostics(dependencyTs), usageConfig.path), + syncDiagnostics(dependencyDiagnostics(), dependencyConfig.path), + ], + expectedConfigFileDiagEvents: () => [ + usageConfigDiag(), + dependencyConfigDiag() + ], }); - } - - describe("with module scenario", () => { - const dependencyTs: File = { - path: `${dependecyLocation}/fns.ts`, - content: `export function fn1() { } + }); + } + describe("with module scenario", () => { + const dependencyTs: File = { + path: `${dependecyLocation}/fns.ts`, + content: `export function fn1() { } export function fn2() { } // Introduce error for fnErr import in main // export function fnErr() { } // Error in dependency ts file export let x: string = 10;` - }; - const dependencyConfig: File = { - path: `${dependecyLocation}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { composite: true, declarationDir: "../decls" } }) - }; - const usageTs: File = { - path: `${usageLocation}/usage.ts`, - content: `import { + }; + const dependencyConfig: File = { + path: `${dependecyLocation}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { composite: true, declarationDir: "../decls" } }) + }; + const usageTs: File = { + path: `${usageLocation}/usage.ts`, + content: `import { fn1, fn2, fnErr @@ -336,122 +311,92 @@ fn1(); fn2(); fnErr(); ` + }; + const usageConfig: File = { + path: `${usageLocation}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { composite: true }, + references: [{ path: "../dependency" }] + }) + }; + function usageDiagnostics(): GetErrDiagnostics { + return { + file: usageTs, + syntax: emptyArray, + semantic: [ + createDiagnostic({ line: 4, offset: 5 }, { line: 4, offset: 10 }, Diagnostics.Module_0_has_no_exported_member_1, [`"../dependency/fns"`, "fnErr"], "error") + ], + suggestion: emptyArray }; - const usageConfig: File = { - path: `${usageLocation}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { composite: true }, - references: [{ path: "../dependency" }] - }) + } + function dependencyDiagnostics(): GetErrDiagnostics { + return { + file: dependencyTs, + syntax: emptyArray, + semantic: [ + createDiagnostic({ line: 6, offset: 12 }, { line: 6, offset: 13 }, Diagnostics.Type_0_is_not_assignable_to_type_1, ["10", "string"], "error") + ], + suggestion: emptyArray }; - function usageDiagnostics(): GetErrDiagnostics { - return { - file: usageTs, - syntax: emptyArray, - semantic: [ - createDiagnostic( - { line: 4, offset: 5 }, - { line: 4, offset: 10 }, - Diagnostics.Module_0_has_no_exported_member_1, - [`"../dependency/fns"`, "fnErr"], - "error", - ) - ], - suggestion: emptyArray - }; - } - - function dependencyDiagnostics(): GetErrDiagnostics { - return { - file: dependencyTs, - syntax: emptyArray, - semantic: [ - createDiagnostic( - { line: 6, offset: 12 }, - { line: 6, offset: 13 }, - Diagnostics.Type_0_is_not_assignable_to_type_1, - ["10", "string"], - "error", - ) - ], - suggestion: emptyArray - }; - } - - verifyUsageAndDependency({ - allFiles: [dependencyTs, dependencyConfig, usageTs, usageConfig], - usageDiagnostics, - dependencyDiagnostics - }); + } + verifyUsageAndDependency({ + allFiles: [dependencyTs, dependencyConfig, usageTs, usageConfig], + usageDiagnostics, + dependencyDiagnostics }); - - describe("with non module --out", () => { - const dependencyTs: File = { - path: `${dependecyLocation}/fns.ts`, - content: `function fn1() { } + }); + describe("with non module --out", () => { + const dependencyTs: File = { + path: `${dependecyLocation}/fns.ts`, + content: `function fn1() { } function fn2() { } // Introduce error for fnErr import in main // function fnErr() { } // Error in dependency ts file let x: string = 10;` - }; - const dependencyConfig: File = { - path: `${dependecyLocation}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { composite: true, outFile: "../dependency.js" } }) - }; - const usageTs: File = { - path: `${usageLocation}/usage.ts`, - content: `fn1(); + }; + const dependencyConfig: File = { + path: `${dependecyLocation}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { composite: true, outFile: "../dependency.js" } }) + }; + const usageTs: File = { + path: `${usageLocation}/usage.ts`, + content: `fn1(); fn2(); fnErr(); ` + }; + const usageConfig: File = { + path: `${usageLocation}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { composite: true, outFile: "../usage.js" }, + references: [{ path: "../dependency" }] + }) + }; + function usageDiagnostics(): GetErrDiagnostics { + return { + file: usageTs, + syntax: emptyArray, + semantic: [ + createDiagnostic({ line: 3, offset: 1 }, { line: 3, offset: 6 }, Diagnostics.Cannot_find_name_0, ["fnErr"], "error") + ], + suggestion: emptyArray }; - const usageConfig: File = { - path: `${usageLocation}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { composite: true, outFile: "../usage.js" }, - references: [{ path: "../dependency" }] - }) + } + function dependencyDiagnostics(): GetErrDiagnostics { + return { + file: dependencyTs, + syntax: emptyArray, + semantic: [ + createDiagnostic({ line: 6, offset: 5 }, { line: 6, offset: 6 }, Diagnostics.Type_0_is_not_assignable_to_type_1, ["10", "string"], "error") + ], + suggestion: emptyArray }; - function usageDiagnostics(): GetErrDiagnostics { - return { - file: usageTs, - syntax: emptyArray, - semantic: [ - createDiagnostic( - { line: 3, offset: 1 }, - { line: 3, offset: 6 }, - Diagnostics.Cannot_find_name_0, - ["fnErr"], - "error", - ) - ], - suggestion: emptyArray - }; - } - - function dependencyDiagnostics(): GetErrDiagnostics { - return { - file: dependencyTs, - syntax: emptyArray, - semantic: [ - createDiagnostic( - { line: 6, offset: 5 }, - { line: 6, offset: 6 }, - Diagnostics.Type_0_is_not_assignable_to_type_1, - ["10", "string"], - "error", - ) - ], - suggestion: emptyArray - }; - } - - verifyUsageAndDependency({ - allFiles: [dependencyTs, dependencyConfig, usageTs, usageConfig], - usageDiagnostics, - dependencyDiagnostics - }); + } + verifyUsageAndDependency({ + allFiles: [dependencyTs, dependencyConfig, usageTs, usageConfig], + usageDiagnostics, + dependencyDiagnostics }); }); -} +}); diff --git a/src/testRunner/unittests/tsserver/projectReferences.ts b/src/testRunner/unittests/tsserver/projectReferences.ts index 8baa3a1f0cf63..1855e37592eef 100644 --- a/src/testRunner/unittests/tsserver/projectReferences.ts +++ b/src/testRunner/unittests/tsserver/projectReferences.ts @@ -1,171 +1,158 @@ -namespace ts.projectSystem { - describe("unittests:: tsserver:: with project references and tsbuild", () => { - function createHost(files: readonly TestFSWithWatch.FileOrFolderOrSymLink[], rootNames: readonly string[]) { - const host = createServerHost(files); - - // ts build should succeed - const solutionBuilder = tscWatch.createSolutionBuilder(host, rootNames, {}); - solutionBuilder.build(); - assert.equal(host.getOutput().length, 0, JSON.stringify(host.getOutput(), /*replacer*/ undefined, " ")); - - return host; +import { TestFSWithWatch, endsWith, Path, ScriptElementKind, isString, isArray, last, contains, CompilerOptions } from "../../ts"; +import { createServerHost, File, libFile, createSession, checkNumberOfProjects, protocol, checkProjectActualFiles, openFilesForSession, protocolFileSpanFromSubstring, protocolFileSpanWithContextFromSubstring, protocolLocationFromSubstring, TestSession, TestServerHost, checkScriptInfos, checkWatchedFiles, closeFilesForSession, protocolFileLocationFromSubstring, makeReferenceItem, createProjectService, SymLink, verifyGetErrRequest } from "../../ts.projectSystem"; +import { createSolutionBuilder, projectRoot } from "../../ts.tscWatch"; +import { ScriptInfo } from "../../ts.server"; +describe("unittests:: tsserver:: with project references and tsbuild", () => { + function createHost(files: readonly TestFSWithWatch.FileOrFolderOrSymLink[], rootNames: readonly string[]) { + const host = createServerHost(files); + // ts build should succeed + const solutionBuilder = createSolutionBuilder(host, rootNames, {}); + solutionBuilder.build(); + assert.equal(host.getOutput().length, 0, JSON.stringify(host.getOutput(), /*replacer*/ undefined, " ")); + return host; + } + describe("with container project", () => { + function getProjectFiles(project: string): [File, File] { + return [ + TestFSWithWatch.getTsBuildProjectFile(project, "tsconfig.json"), + TestFSWithWatch.getTsBuildProjectFile(project, "index.ts"), + ]; } - - describe("with container project", () => { - function getProjectFiles(project: string): [File, File] { - return [ - TestFSWithWatch.getTsBuildProjectFile(project, "tsconfig.json"), - TestFSWithWatch.getTsBuildProjectFile(project, "index.ts"), - ]; - } - - const project = "container"; - const containerLib = getProjectFiles("container/lib"); - const containerExec = getProjectFiles("container/exec"); - const containerCompositeExec = getProjectFiles("container/compositeExec"); - const containerConfig = TestFSWithWatch.getTsBuildProjectFile(project, "tsconfig.json"); - const files = [libFile, ...containerLib, ...containerExec, ...containerCompositeExec, containerConfig]; - - it("does not error on container only project", () => { - const host = createHost(files, [containerConfig.path]); - - // Open external project for the folder - const session = createSession(host); - const service = session.getProjectService(); - service.openExternalProjects([{ + const project = "container"; + const containerLib = getProjectFiles("container/lib"); + const containerExec = getProjectFiles("container/exec"); + const containerCompositeExec = getProjectFiles("container/compositeExec"); + const containerConfig = TestFSWithWatch.getTsBuildProjectFile(project, "tsconfig.json"); + const files = [libFile, ...containerLib, ...containerExec, ...containerCompositeExec, containerConfig]; + it("does not error on container only project", () => { + const host = createHost(files, [containerConfig.path]); + // Open external project for the folder + const session = createSession(host); + const service = session.getProjectService(); + service.openExternalProjects([{ projectFileName: TestFSWithWatch.getTsBuildProjectFilePath(project, project), rootFiles: files.map(f => ({ fileName: f.path })), options: {} }]); - checkNumberOfProjects(service, { configuredProjects: 4 }); - files.forEach(f => { - const args: protocol.FileRequestArgs = { - file: f.path, - projectFileName: endsWith(f.path, "tsconfig.json") ? f.path : undefined - }; - const syntaxDiagnostics = session.executeCommandSeq({ - command: protocol.CommandTypes.SyntacticDiagnosticsSync, - arguments: args - }).response; - assert.deepEqual(syntaxDiagnostics, []); - const semanticDiagnostics = session.executeCommandSeq({ - command: protocol.CommandTypes.SemanticDiagnosticsSync, - arguments: args - }).response; - assert.deepEqual(semanticDiagnostics, []); - }); - const containerProject = service.configuredProjects.get(containerConfig.path)!; - checkProjectActualFiles(containerProject, [containerConfig.path]); - const optionsDiagnostics = session.executeCommandSeq({ - command: protocol.CommandTypes.CompilerOptionsDiagnosticsFull, - arguments: { projectFileName: containerProject.projectName } + checkNumberOfProjects(service, { configuredProjects: 4 }); + files.forEach(f => { + const args: protocol.FileRequestArgs = { + file: f.path, + projectFileName: endsWith(f.path, "tsconfig.json") ? f.path : undefined + }; + const syntaxDiagnostics = session.executeCommandSeq({ + command: protocol.CommandTypes.SyntacticDiagnosticsSync, + arguments: args }).response; - assert.deepEqual(optionsDiagnostics, []); + assert.deepEqual(syntaxDiagnostics, []); + const semanticDiagnostics = session.executeCommandSeq({ + command: protocol.CommandTypes.SemanticDiagnosticsSync, + arguments: args + }).response; + assert.deepEqual(semanticDiagnostics, []); }); - - it("can successfully find references with --out options", () => { - const host = createHost(files, [containerConfig.path]); - const session = createSession(host); - openFilesForSession([containerCompositeExec[1]], session); - const service = session.getProjectService(); - checkNumberOfProjects(service, { configuredProjects: 2 }); // compositeExec and solution - const solutionProject = service.configuredProjects.get(containerConfig.path)!; - assert.isTrue(solutionProject.isInitialLoadPending()); - const { file: myConstFile, start: myConstStart, end: myConstEnd } = protocolFileSpanFromSubstring({ - file: containerCompositeExec[1], - text: "myConst", - }); - const response = session.executeCommandSeq({ - command: protocol.CommandTypes.Rename, - arguments: { file: myConstFile, ...myConstStart } - }).response as protocol.RenameResponseBody; - - const locationOfMyConstInLib = protocolFileSpanWithContextFromSubstring({ - file: containerLib[1], - text: "myConst", - contextText: "export const myConst = 30;" - }); - const { file: _, ...renameTextOfMyConstInLib } = locationOfMyConstInLib; - const locationOfMyConstInExec = protocolFileSpanWithContextFromSubstring({ - file: containerExec[1], - text: "myConst" - }); - const { file: myConstInExecFile, ...renameTextOfMyConstInExec } = locationOfMyConstInExec; - assert.deepEqual(response.locs, [ - { file: locationOfMyConstInLib.file, locs: [renameTextOfMyConstInLib] }, - { file: myConstFile, locs: [{ start: myConstStart, end: myConstEnd }] }, - { file: myConstInExecFile, locs: [renameTextOfMyConstInExec] }, - ]); - checkNumberOfProjects(service, { configuredProjects: 4 }); - assert.isFalse(solutionProject.isInitialLoadPending()); + const containerProject = service.configuredProjects.get(containerConfig.path)!; + checkProjectActualFiles(containerProject, [containerConfig.path]); + const optionsDiagnostics = session.executeCommandSeq({ + command: protocol.CommandTypes.CompilerOptionsDiagnosticsFull, + arguments: { projectFileName: containerProject.projectName } + }).response; + assert.deepEqual(optionsDiagnostics, []); + }); + it("can successfully find references with --out options", () => { + const host = createHost(files, [containerConfig.path]); + const session = createSession(host); + openFilesForSession([containerCompositeExec[1]], session); + const service = session.getProjectService(); + checkNumberOfProjects(service, { configuredProjects: 2 }); // compositeExec and solution + const solutionProject = service.configuredProjects.get(containerConfig.path)!; + assert.isTrue(solutionProject.isInitialLoadPending()); + const { file: myConstFile, start: myConstStart, end: myConstEnd } = protocolFileSpanFromSubstring({ + file: containerCompositeExec[1], + text: "myConst", }); - - it("ancestor and project ref management", () => { - const tempFile: File = { - path: `/user/username/projects/temp/temp.ts`, - content: "let x = 10" - }; - const host = createHost(files.concat([tempFile]), [containerConfig.path]); - const session = createSession(host); - openFilesForSession([containerCompositeExec[1]], session); - const service = session.getProjectService(); - checkNumberOfProjects(service, { configuredProjects: 2 }); // compositeExec and solution - const solutionProject = service.configuredProjects.get(containerConfig.path)!; - assert.isTrue(solutionProject.isInitialLoadPending()); - - // Open temp file and verify all projects alive - openFilesForSession([tempFile], session); - checkNumberOfProjects(service, { configuredProjects: 2, inferredProjects: 1 }); - assert.isTrue(solutionProject.isInitialLoadPending()); - - const locationOfMyConst = protocolLocationFromSubstring(containerCompositeExec[1].content, "myConst"); - session.executeCommandSeq({ - command: protocol.CommandTypes.Rename, - arguments: { - file: containerCompositeExec[1].path, - ...locationOfMyConst - } - }); - - // Ref projects are loaded - checkNumberOfProjects(service, { configuredProjects: 4, inferredProjects: 1 }); - assert.isFalse(solutionProject.isInitialLoadPending()); - - // Open temp file and verify all projects alive - service.closeClientFile(tempFile.path); - openFilesForSession([tempFile], session); - checkNumberOfProjects(service, { configuredProjects: 4, inferredProjects: 1 }); - - // Close all files and open temp file, only inferred project should be alive - service.closeClientFile(containerCompositeExec[1].path); - service.closeClientFile(tempFile.path); - openFilesForSession([tempFile], session); - checkNumberOfProjects(service, { inferredProjects: 1 }); + const response = (session.executeCommandSeq({ + command: protocol.CommandTypes.Rename, + arguments: { file: myConstFile, ...myConstStart } + }).response as protocol.RenameResponseBody); + const locationOfMyConstInLib = protocolFileSpanWithContextFromSubstring({ + file: containerLib[1], + text: "myConst", + contextText: "export const myConst = 30;" + }); + const { file: _, ...renameTextOfMyConstInLib } = locationOfMyConstInLib; + const locationOfMyConstInExec = protocolFileSpanWithContextFromSubstring({ + file: containerExec[1], + text: "myConst" }); + const { file: myConstInExecFile, ...renameTextOfMyConstInExec } = locationOfMyConstInExec; + assert.deepEqual(response.locs, [ + { file: locationOfMyConstInLib.file, locs: [renameTextOfMyConstInLib] }, + { file: myConstFile, locs: [{ start: myConstStart, end: myConstEnd }] }, + { file: myConstInExecFile, locs: [renameTextOfMyConstInExec] }, + ]); + checkNumberOfProjects(service, { configuredProjects: 4 }); + assert.isFalse(solutionProject.isInitialLoadPending()); }); - - describe("with main and depedency project", () => { - const dependecyLocation = `${tscWatch.projectRoot}/dependency`; - const dependecyDeclsLocation = `${tscWatch.projectRoot}/decls`; - const mainLocation = `${tscWatch.projectRoot}/main`; - const dependencyTs: File = { - path: `${dependecyLocation}/FnS.ts`, - content: `export function fn1() { } + it("ancestor and project ref management", () => { + const tempFile: File = { + path: `/user/username/projects/temp/temp.ts`, + content: "let x = 10" + }; + const host = createHost(files.concat([tempFile]), [containerConfig.path]); + const session = createSession(host); + openFilesForSession([containerCompositeExec[1]], session); + const service = session.getProjectService(); + checkNumberOfProjects(service, { configuredProjects: 2 }); // compositeExec and solution + const solutionProject = service.configuredProjects.get(containerConfig.path)!; + assert.isTrue(solutionProject.isInitialLoadPending()); + // Open temp file and verify all projects alive + openFilesForSession([tempFile], session); + checkNumberOfProjects(service, { configuredProjects: 2, inferredProjects: 1 }); + assert.isTrue(solutionProject.isInitialLoadPending()); + const locationOfMyConst = protocolLocationFromSubstring(containerCompositeExec[1].content, "myConst"); + session.executeCommandSeq({ + command: protocol.CommandTypes.Rename, + arguments: { + file: containerCompositeExec[1].path, + ...locationOfMyConst + } + }); + // Ref projects are loaded + checkNumberOfProjects(service, { configuredProjects: 4, inferredProjects: 1 }); + assert.isFalse(solutionProject.isInitialLoadPending()); + // Open temp file and verify all projects alive + service.closeClientFile(tempFile.path); + openFilesForSession([tempFile], session); + checkNumberOfProjects(service, { configuredProjects: 4, inferredProjects: 1 }); + // Close all files and open temp file, only inferred project should be alive + service.closeClientFile(containerCompositeExec[1].path); + service.closeClientFile(tempFile.path); + openFilesForSession([tempFile], session); + checkNumberOfProjects(service, { inferredProjects: 1 }); + }); + }); + describe("with main and depedency project", () => { + const dependecyLocation = `${projectRoot}/dependency`; + const dependecyDeclsLocation = `${projectRoot}/decls`; + const mainLocation = `${projectRoot}/main`; + const dependencyTs: File = { + path: `${dependecyLocation}/FnS.ts`, + content: `export function fn1() { } export function fn2() { } export function fn3() { } export function fn4() { } export function fn5() { } ` - }; - const dependencyTsPath = dependencyTs.path.toLowerCase(); - const dependencyConfig: File = { - path: `${dependecyLocation}/tsconfig.json`, - content: JSON.stringify({ compilerOptions: { composite: true, declarationMap: true, declarationDir: "../decls" } }) - }; - - const mainTs: File = { - path: `${mainLocation}/main.ts`, - content: `import { + }; + const dependencyTsPath = dependencyTs.path.toLowerCase(); + const dependencyConfig: File = { + path: `${dependecyLocation}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { composite: true, declarationMap: true, declarationDir: "../decls" } }) + }; + const mainTs: File = { + path: `${mainLocation}/main.ts`, + content: `import { fn1, fn2, fn3, @@ -179,839 +166,697 @@ fn3(); fn4(); fn5(); ` + }; + const mainConfig: File = { + path: `${mainLocation}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { composite: true, declarationMap: true }, + references: [{ path: "../dependency" }] + }) + }; + const randomFile: File = { + path: `${projectRoot}/random/random.ts`, + content: "let a = 10;" + }; + const randomConfig: File = { + path: `${projectRoot}/random/tsconfig.json`, + content: "{}" + }; + const dtsLocation = `${dependecyDeclsLocation}/FnS.d.ts`; + const dtsPath = (dtsLocation.toLowerCase() as Path); + const dtsMapLocation = `${dependecyDeclsLocation}/FnS.d.ts.map`; + const dtsMapPath = (dtsMapLocation.toLowerCase() as Path); + const files = [dependencyTs, dependencyConfig, mainTs, mainConfig, libFile, randomFile, randomConfig]; + function verifyScriptInfos(session: TestSession, host: TestServerHost, openInfos: readonly string[], closedInfos: readonly string[], otherWatchedFiles: readonly string[], additionalInfo: string) { + checkScriptInfos(session.getProjectService(), openInfos.concat(closedInfos), additionalInfo); + checkWatchedFiles(host, closedInfos.concat(otherWatchedFiles).map(f => f.toLowerCase()), additionalInfo); + } + function verifyInfosWithRandom(session: TestSession, host: TestServerHost, openInfos: readonly string[], closedInfos: readonly string[], otherWatchedFiles: readonly string[], reqName: string) { + verifyScriptInfos(session, host, openInfos.concat(randomFile.path), closedInfos, otherWatchedFiles.concat(randomConfig.path), reqName); + } + function verifyOnlyRandomInfos(session: TestSession, host: TestServerHost) { + verifyScriptInfos(session, host, [randomFile.path], [libFile.path], [randomConfig.path], "Random"); + } + function declarationSpan(fn: number): protocol.TextSpanWithContext { + return { + start: { line: fn, offset: 17 }, + end: { line: fn, offset: 20 }, + contextStart: { line: fn, offset: 1 }, + contextEnd: { line: fn, offset: 26 } }; - const mainConfig: File = { - path: `${mainLocation}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { composite: true, declarationMap: true }, - references: [{ path: "../dependency" }] - }) - }; - - const randomFile: File = { - path: `${tscWatch.projectRoot}/random/random.ts`, - content: "let a = 10;" + } + function importSpan(fn: number): protocol.TextSpanWithContext { + return { + start: { line: fn + 1, offset: 5 }, + end: { line: fn + 1, offset: 8 }, + contextStart: { line: 1, offset: 1 }, + contextEnd: { line: 7, offset: 22 } }; - const randomConfig: File = { - path: `${tscWatch.projectRoot}/random/tsconfig.json`, - content: "{}" + } + function usageSpan(fn: number): protocol.TextSpan { + return { start: { line: fn + 8, offset: 1 }, end: { line: fn + 8, offset: 4 } }; + } + function goToDefFromMainTs(fn: number): Action { + const textSpan = usageSpan(fn); + const definition: protocol.FileSpan = { file: dependencyTs.path, ...declarationSpan(fn) }; + return { + reqName: "goToDef", + request: { + command: protocol.CommandTypes.DefinitionAndBoundSpan, + arguments: { file: mainTs.path, ...textSpan.start } + }, + expectedResponse: { + // To dependency + definitions: [definition], + textSpan + } }; - const dtsLocation = `${dependecyDeclsLocation}/FnS.d.ts`; - const dtsPath = dtsLocation.toLowerCase() as Path; - const dtsMapLocation = `${dependecyDeclsLocation}/FnS.d.ts.map`; - const dtsMapPath = dtsMapLocation.toLowerCase() as Path; - - const files = [dependencyTs, dependencyConfig, mainTs, mainConfig, libFile, randomFile, randomConfig]; - - function verifyScriptInfos(session: TestSession, host: TestServerHost, openInfos: readonly string[], closedInfos: readonly string[], otherWatchedFiles: readonly string[], additionalInfo: string) { - checkScriptInfos(session.getProjectService(), openInfos.concat(closedInfos), additionalInfo); - checkWatchedFiles(host, closedInfos.concat(otherWatchedFiles).map(f => f.toLowerCase()), additionalInfo); - } - - function verifyInfosWithRandom(session: TestSession, host: TestServerHost, openInfos: readonly string[], closedInfos: readonly string[], otherWatchedFiles: readonly string[], reqName: string) { - verifyScriptInfos(session, host, openInfos.concat(randomFile.path), closedInfos, otherWatchedFiles.concat(randomConfig.path), reqName); - } - - function verifyOnlyRandomInfos(session: TestSession, host: TestServerHost) { - verifyScriptInfos(session, host, [randomFile.path], [libFile.path], [randomConfig.path], "Random"); - } - - function declarationSpan(fn: number): protocol.TextSpanWithContext { - return { - start: { line: fn, offset: 17 }, - end: { line: fn, offset: 20 }, - contextStart: { line: fn, offset: 1 }, - contextEnd: { line: fn, offset: 26 } - }; - } - function importSpan(fn: number): protocol.TextSpanWithContext { - return { - start: { line: fn + 1, offset: 5 }, - end: { line: fn + 1, offset: 8 }, - contextStart: { line: 1, offset: 1 }, - contextEnd: { line: 7, offset: 22 } - }; - } - function usageSpan(fn: number): protocol.TextSpan { - return { start: { line: fn + 8, offset: 1 }, end: { line: fn + 8, offset: 4 } }; - } - - function goToDefFromMainTs(fn: number): Action { - const textSpan = usageSpan(fn); - const definition: protocol.FileSpan = { file: dependencyTs.path, ...declarationSpan(fn) }; - return { - reqName: "goToDef", - request: { - command: protocol.CommandTypes.DefinitionAndBoundSpan, - arguments: { file: mainTs.path, ...textSpan.start } - }, - expectedResponse: { - // To dependency - definitions: [definition], - textSpan - } - }; - } - - function goToDefFromMainTsWithNoMap(fn: number): Action { - const textSpan = usageSpan(fn); - const definition = declarationSpan(fn); - const declareSpaceLength = "declare ".length; - return { - reqName: "goToDef", - request: { - command: protocol.CommandTypes.DefinitionAndBoundSpan, - arguments: { file: mainTs.path, ...textSpan.start } - }, - expectedResponse: { - // To the dts - definitions: [{ + } + function goToDefFromMainTsWithNoMap(fn: number): Action { + const textSpan = usageSpan(fn); + const definition = declarationSpan(fn); + const declareSpaceLength = "declare ".length; + return { + reqName: "goToDef", + request: { + command: protocol.CommandTypes.DefinitionAndBoundSpan, + arguments: { file: mainTs.path, ...textSpan.start } + }, + expectedResponse: { + // To the dts + definitions: [{ file: dtsPath, start: { line: fn, offset: definition.start.offset + declareSpaceLength }, end: { line: fn, offset: definition.end.offset + declareSpaceLength }, contextStart: { line: fn, offset: 1 }, contextEnd: { line: fn, offset: 37 } }], - textSpan - } - }; - } - - function goToDefFromMainTsWithNoDts(fn: number): Action { - const textSpan = usageSpan(fn); - return { - reqName: "goToDef", - request: { - command: protocol.CommandTypes.DefinitionAndBoundSpan, - arguments: { file: mainTs.path, ...textSpan.start } - }, - expectedResponse: { - // To import declaration - definitions: [{ file: mainTs.path, ...importSpan(fn) }], - textSpan - } - }; - } - - function goToDefFromMainTsWithDependencyChange(fn: number): Action { - const textSpan = usageSpan(fn); - return { - reqName: "goToDef", - request: { - command: protocol.CommandTypes.DefinitionAndBoundSpan, - arguments: { file: mainTs.path, ...textSpan.start } + textSpan + } + }; + } + function goToDefFromMainTsWithNoDts(fn: number): Action { + const textSpan = usageSpan(fn); + return { + reqName: "goToDef", + request: { + command: protocol.CommandTypes.DefinitionAndBoundSpan, + arguments: { file: mainTs.path, ...textSpan.start } + }, + expectedResponse: { + // To import declaration + definitions: [{ file: mainTs.path, ...importSpan(fn) }], + textSpan + } + }; + } + function goToDefFromMainTsWithDependencyChange(fn: number): Action { + const textSpan = usageSpan(fn); + return { + reqName: "goToDef", + request: { + command: protocol.CommandTypes.DefinitionAndBoundSpan, + arguments: { file: mainTs.path, ...textSpan.start } + }, + expectedResponse: { + // Definition on fn + 1 line + definitions: [{ file: dependencyTs.path, ...declarationSpan(fn + 1) }], + textSpan + } + }; + } + function goToDefFromMainTsProjectInfoVerifier(withRefs: boolean): ProjectInfoVerifier { + return { + openFile: mainTs, + openFileLastLine: 14, + configFile: mainConfig, + expectedProjectActualFiles: withRefs ? + [mainTs.path, libFile.path, mainConfig.path, dependencyTs.path] : + [mainTs.path, libFile.path, mainConfig.path, dtsPath] + }; + } + function renameFromDependencyTs(fn: number): Action { + const defSpan = declarationSpan(fn); + const { contextStart: _, contextEnd: _1, ...triggerSpan } = defSpan; + return { + reqName: "rename", + request: { + command: protocol.CommandTypes.Rename, + arguments: { file: dependencyTs.path, ...triggerSpan.start } + }, + expectedResponse: { + info: { + canRename: true, + fileToRename: undefined, + displayName: `fn${fn}`, + fullDisplayName: `"${dependecyLocation}/FnS".fn${fn}`, + kind: ScriptElementKind.functionElement, + kindModifiers: "export", + triggerSpan }, - expectedResponse: { - // Definition on fn + 1 line - definitions: [{ file: dependencyTs.path, ...declarationSpan(fn + 1) }], - textSpan - } - }; - } - - function goToDefFromMainTsProjectInfoVerifier(withRefs: boolean): ProjectInfoVerifier { - return { - openFile: mainTs, - openFileLastLine: 14, - configFile: mainConfig, - expectedProjectActualFiles: withRefs ? - [mainTs.path, libFile.path, mainConfig.path, dependencyTs.path] : - [mainTs.path, libFile.path, mainConfig.path, dtsPath] - }; - } - - function renameFromDependencyTs(fn: number): Action { - const defSpan = declarationSpan(fn); - const { contextStart: _, contextEnd: _1, ...triggerSpan } = defSpan; - return { - reqName: "rename", - request: { - command: protocol.CommandTypes.Rename, - arguments: { file: dependencyTs.path, ...triggerSpan.start } + locs: [ + { file: dependencyTs.path, locs: [defSpan] } + ] + } + }; + } + function renameFromDependencyTsWithDependencyChange(fn: number): Action { + const { expectedResponse: { info, locs }, ...rest } = renameFromDependencyTs(fn + 1); + return { + ...rest, + expectedResponse: { + info: { + ...(info as protocol.RenameInfoSuccess), + displayName: `fn${fn}`, + fullDisplayName: `"${dependecyLocation}/FnS".fn${fn}`, }, - expectedResponse: { - info: { - canRename: true, - fileToRename: undefined, - displayName: `fn${fn}`, - fullDisplayName: `"${dependecyLocation}/FnS".fn${fn}`, - kind: ScriptElementKind.functionElement, - kindModifiers: "export", - triggerSpan - }, - locs: [ - { file: dependencyTs.path, locs: [defSpan] } - ] - } - }; - } - - function renameFromDependencyTsWithDependencyChange(fn: number): Action { - const { expectedResponse: { info, locs }, ...rest } = renameFromDependencyTs(fn + 1); - - return { - ...rest, - expectedResponse: { - info: { - ...info as protocol.RenameInfoSuccess, - displayName: `fn${fn}`, - fullDisplayName: `"${dependecyLocation}/FnS".fn${fn}`, - }, - locs - } - }; - } - - function renameFromDependencyTsProjectInfoVerifier(): ProjectInfoVerifier { - return { - openFile: dependencyTs, - openFileLastLine: 6, - configFile: dependencyConfig, - expectedProjectActualFiles: [dependencyTs.path, libFile.path, dependencyConfig.path] - }; - } - - function renameFromDependencyTsWithBothProjectsOpen(fn: number): Action { - const { reqName, request, expectedResponse } = renameFromDependencyTs(fn); - const { info, locs } = expectedResponse; - return { - reqName, - request, - expectedResponse: { - info, - locs: [ - locs[0], - { - file: mainTs.path, - locs: [ - importSpan(fn), - usageSpan(fn) - ] - } - ] - } - }; - } - - function renameFromDependencyTsWithBothProjectsOpenWithDependencyChange(fn: number): Action { - const { reqName, request, expectedResponse, } = renameFromDependencyTsWithDependencyChange(fn); - const { info, locs } = expectedResponse; - return { - reqName, - request, - expectedResponse: { - info, - locs: [ - locs[0], - { - file: mainTs.path, - locs: [ - importSpan(fn), - usageSpan(fn) - ] - } - ] - } - }; - } - - function removePath(array: readonly string[], ...delPaths: string[]) { - return array.filter(a => { - const aLower = a.toLowerCase(); - return delPaths.every(dPath => dPath !== aLower); - }); - } - - interface Action { - reqName: string; - request: Partial; - expectedResponse: Response; - } - interface ActionInfo { - action: (fn: number) => Action; - closedInfos: readonly string[]; - otherWatchedFiles: readonly string[]; - expectsDts: boolean; - expectsMap: boolean; - freshMapInfo?: boolean; - freshDocumentMapper?: boolean; - skipDtsMapCheck?: boolean; - } - type ActionKey = keyof ActionInfoVerifier; - type ActionInfoGetterFn = () => ActionInfo; - type ActionInfoSpreader = [ - ActionKey, // Key to get initial value and pass this value to spread function - (actionInfo: ActionInfo) => Partial> - ]; - type ActionInfoGetter = ActionInfoGetterFn | ActionKey | ActionInfoSpreader; - interface ProjectInfoVerifier { - openFile: File; - openFileLastLine: number; - configFile: File; - expectedProjectActualFiles: readonly string[]; - } - interface ActionInfoVerifier { - main: ActionInfoGetter; - change: ActionInfoGetter; - dtsChange: ActionInfoGetter; - mapChange: ActionInfoGetter; - noMap: ActionInfoGetter; - mapFileCreated: ActionInfoGetter; - mapFileDeleted: ActionInfoGetter; - noDts: ActionInfoGetter; - dtsFileCreated: ActionInfoGetter; - dtsFileDeleted: ActionInfoGetter; - dependencyChange: ActionInfoGetter; - noBuild: ActionInfoGetter; - } - interface DocumentPositionMapperVerifier extends ProjectInfoVerifier, ActionInfoVerifier { - } - - interface VerifierAndWithRefs { - withRefs: boolean; - disableSourceOfProjectReferenceRedirect?: true; - verifier: (withRefs: boolean, disableSourceOfProjectReferenceRedirect?: true) => readonly DocumentPositionMapperVerifier[]; - } - - function openFiles(verifiers: readonly DocumentPositionMapperVerifier[]) { - return verifiers.map(v => v.openFile); - } - interface OpenTsFile extends VerifierAndWithRefs { - onHostCreate?: (host: TestServerHost) => void; - } - function openTsFile({ withRefs, disableSourceOfProjectReferenceRedirect, verifier, onHostCreate }: OpenTsFile) { - const host = createHost(files, [mainConfig.path]); - if (!withRefs) { - // Erase project reference - host.writeFile(mainConfig.path, JSON.stringify({ - compilerOptions: { composite: true, declarationMap: true } - })); + locs } - else if (disableSourceOfProjectReferenceRedirect) { - // Erase project reference - host.writeFile(mainConfig.path, JSON.stringify({ - compilerOptions: { - composite: true, - declarationMap: true, - disableSourceOfProjectReferenceRedirect: !!disableSourceOfProjectReferenceRedirect - }, - references: [{ path: "../dependency" }] - })); + }; + } + function renameFromDependencyTsProjectInfoVerifier(): ProjectInfoVerifier { + return { + openFile: dependencyTs, + openFileLastLine: 6, + configFile: dependencyConfig, + expectedProjectActualFiles: [dependencyTs.path, libFile.path, dependencyConfig.path] + }; + } + function renameFromDependencyTsWithBothProjectsOpen(fn: number): Action { + const { reqName, request, expectedResponse } = renameFromDependencyTs(fn); + const { info, locs } = expectedResponse; + return { + reqName, + request, + expectedResponse: { + info, + locs: [ + locs[0], + { + file: mainTs.path, + locs: [ + importSpan(fn), + usageSpan(fn) + ] + } + ] } - if (onHostCreate) { - onHostCreate(host); + }; + } + function renameFromDependencyTsWithBothProjectsOpenWithDependencyChange(fn: number): Action { + const { reqName, request, expectedResponse, } = renameFromDependencyTsWithDependencyChange(fn); + const { info, locs } = expectedResponse; + return { + reqName, + request, + expectedResponse: { + info, + locs: [ + locs[0], + { + file: mainTs.path, + locs: [ + importSpan(fn), + usageSpan(fn) + ] + } + ] } - const session = createSession(host); - const verifiers = verifier(withRefs && !disableSourceOfProjectReferenceRedirect, disableSourceOfProjectReferenceRedirect); - openFilesForSession([...openFiles(verifiers), randomFile], session); - return { host, session, verifiers }; - } - - function checkProject(session: TestSession, verifiers: readonly DocumentPositionMapperVerifier[], noDts?: true) { - const service = session.getProjectService(); - checkNumberOfProjects(service, { configuredProjects: 1 + verifiers.length }); - verifiers.forEach(({ configFile, expectedProjectActualFiles }) => { - checkProjectActualFiles( - service.configuredProjects.get(configFile.path.toLowerCase())!, - noDts ? - expectedProjectActualFiles.filter(f => f.toLowerCase() !== dtsPath) : - expectedProjectActualFiles - ); - }); + }; + } + function removePath(array: readonly string[], ...delPaths: string[]) { + return array.filter(a => { + const aLower = a.toLowerCase(); + return delPaths.every(dPath => dPath !== aLower); + }); + } + interface Action { + reqName: string; + request: Partial; + expectedResponse: Response; + } + interface ActionInfo { + action: (fn: number) => Action; + closedInfos: readonly string[]; + otherWatchedFiles: readonly string[]; + expectsDts: boolean; + expectsMap: boolean; + freshMapInfo?: boolean; + freshDocumentMapper?: boolean; + skipDtsMapCheck?: boolean; + } + type ActionKey = keyof ActionInfoVerifier; + type ActionInfoGetterFn = () => ActionInfo; + type ActionInfoSpreader = [ActionKey, // Key to get initial value and pass this value to spread function + (actionInfo: ActionInfo) => Partial>]; + type ActionInfoGetter = ActionInfoGetterFn | ActionKey | ActionInfoSpreader; + interface ProjectInfoVerifier { + openFile: File; + openFileLastLine: number; + configFile: File; + expectedProjectActualFiles: readonly string[]; + } + interface ActionInfoVerifier { + main: ActionInfoGetter; + change: ActionInfoGetter; + dtsChange: ActionInfoGetter; + mapChange: ActionInfoGetter; + noMap: ActionInfoGetter; + mapFileCreated: ActionInfoGetter; + mapFileDeleted: ActionInfoGetter; + noDts: ActionInfoGetter; + dtsFileCreated: ActionInfoGetter; + dtsFileDeleted: ActionInfoGetter; + dependencyChange: ActionInfoGetter; + noBuild: ActionInfoGetter; + } + interface DocumentPositionMapperVerifier extends ProjectInfoVerifier, ActionInfoVerifier { + } + interface VerifierAndWithRefs { + withRefs: boolean; + disableSourceOfProjectReferenceRedirect?: true; + verifier: (withRefs: boolean, disableSourceOfProjectReferenceRedirect?: true) => readonly DocumentPositionMapperVerifier[]; + } + function openFiles(verifiers: readonly DocumentPositionMapperVerifier[]) { + return verifiers.map(v => v.openFile); + } + interface OpenTsFile extends VerifierAndWithRefs { + onHostCreate?: (host: TestServerHost) => void; + } + function openTsFile({ withRefs, disableSourceOfProjectReferenceRedirect, verifier, onHostCreate }: OpenTsFile) { + const host = createHost(files, [mainConfig.path]); + if (!withRefs) { + // Erase project reference + host.writeFile(mainConfig.path, JSON.stringify({ + compilerOptions: { composite: true, declarationMap: true } + })); } - - function firstAction(session: TestSession, verifiers: readonly DocumentPositionMapperVerifier[]) { - for (const { action } of getActionInfo(verifiers, "main")) { - const { request } = action(1); - session.executeCommandSeq(request); - } + else if (disableSourceOfProjectReferenceRedirect) { + // Erase project reference + host.writeFile(mainConfig.path, JSON.stringify({ + compilerOptions: { + composite: true, + declarationMap: true, + disableSourceOfProjectReferenceRedirect: !!disableSourceOfProjectReferenceRedirect + }, + references: [{ path: "../dependency" }] + })); } - - function verifyAction(session: TestSession, { reqName, request, expectedResponse }: Action) { - const { response } = session.executeCommandSeq(request); - assert.deepEqual(response, expectedResponse, `Failed Request: ${reqName}`); + if (onHostCreate) { + onHostCreate(host); } - - function verifyScriptInfoPresence(session: TestSession, path: string, expectedToBePresent: boolean, reqName: string) { - const info = session.getProjectService().filenameToScriptInfo.get(path); - if (expectedToBePresent) { - assert.isDefined(info, `${reqName}:: ${path} expected to be present`); - } - else { - assert.isUndefined(info, `${reqName}:: ${path} expected to be not present`); - } - return info; + const session = createSession(host); + const verifiers = verifier(withRefs && !disableSourceOfProjectReferenceRedirect, disableSourceOfProjectReferenceRedirect); + openFilesForSession([...openFiles(verifiers), randomFile], session); + return { host, session, verifiers }; + } + function checkProject(session: TestSession, verifiers: readonly DocumentPositionMapperVerifier[], noDts?: true) { + const service = session.getProjectService(); + checkNumberOfProjects(service, { configuredProjects: 1 + verifiers.length }); + verifiers.forEach(({ configFile, expectedProjectActualFiles }) => { + checkProjectActualFiles((service.configuredProjects.get(configFile.path.toLowerCase())!), noDts ? + expectedProjectActualFiles.filter(f => f.toLowerCase() !== dtsPath) : + expectedProjectActualFiles); + }); + } + function firstAction(session: TestSession, verifiers: readonly DocumentPositionMapperVerifier[]) { + for (const { action } of getActionInfo(verifiers, "main")) { + const { request } = action(1); + session.executeCommandSeq(request); } - - interface VerifyDocumentPositionMapper { - session: TestSession; - dependencyMap: server.ScriptInfo | undefined; - documentPositionMapper: server.ScriptInfo["documentPositionMapper"]; - equal: boolean; - debugInfo: string; + } + function verifyAction(session: TestSession, { reqName, request, expectedResponse }: Action) { + const { response } = session.executeCommandSeq(request); + assert.deepEqual(response, expectedResponse, `Failed Request: ${reqName}`); + } + function verifyScriptInfoPresence(session: TestSession, path: string, expectedToBePresent: boolean, reqName: string) { + const info = session.getProjectService().filenameToScriptInfo.get(path); + if (expectedToBePresent) { + assert.isDefined(info, `${reqName}:: ${path} expected to be present`); } - function verifyDocumentPositionMapper({ session, dependencyMap, documentPositionMapper, equal, debugInfo }: VerifyDocumentPositionMapper) { - assert.strictEqual(session.getProjectService().filenameToScriptInfo.get(dtsMapPath), dependencyMap, debugInfo); - if (dependencyMap) { - if (equal) { - assert.strictEqual(dependencyMap.documentPositionMapper, documentPositionMapper, debugInfo); - } - else { - assert.notStrictEqual(dependencyMap.documentPositionMapper, documentPositionMapper, debugInfo); - } - } + else { + assert.isUndefined(info, `${reqName}:: ${path} expected to be not present`); } - - function getActionInfoOfVerfier(verifier: DocumentPositionMapperVerifier, actionKey: ActionKey): ActionInfo { - const actionInfoGetter = verifier[actionKey]; - if (isString(actionInfoGetter)) { - return getActionInfoOfVerfier(verifier, actionInfoGetter); + return info; + } + interface VerifyDocumentPositionMapper { + session: TestSession; + dependencyMap: ScriptInfo | undefined; + documentPositionMapper: ScriptInfo["documentPositionMapper"]; + equal: boolean; + debugInfo: string; + } + function verifyDocumentPositionMapper({ session, dependencyMap, documentPositionMapper, equal, debugInfo }: VerifyDocumentPositionMapper) { + assert.strictEqual(session.getProjectService().filenameToScriptInfo.get(dtsMapPath), dependencyMap, debugInfo); + if (dependencyMap) { + if (equal) { + assert.strictEqual(dependencyMap.documentPositionMapper, documentPositionMapper, debugInfo); } - - if (isArray(actionInfoGetter)) { - const initialValue = getActionInfoOfVerfier(verifier, actionInfoGetter[0]); - return { - ...initialValue, - ...actionInfoGetter[1](initialValue) - }; + else { + assert.notStrictEqual(dependencyMap.documentPositionMapper, documentPositionMapper, debugInfo); } - - return actionInfoGetter(); } - - function getActionInfo(verifiers: readonly DocumentPositionMapperVerifier[], actionKey: ActionKey): ActionInfo[] { - return verifiers.map(v => getActionInfoOfVerfier(v, actionKey)); - } - - interface VerifyAllFnAction { - session: TestSession; - host: TestServerHost; - verifiers: readonly DocumentPositionMapperVerifier[]; - actionKey: ActionKey; - sourceMapPath?: server.ScriptInfo["sourceMapFilePath"]; - dependencyMap?: server.ScriptInfo | undefined; - documentPositionMapper?: server.ScriptInfo["documentPositionMapper"]; + } + function getActionInfoOfVerfier(verifier: DocumentPositionMapperVerifier, actionKey: ActionKey): ActionInfo { + const actionInfoGetter = verifier[actionKey]; + if (isString(actionInfoGetter)) { + return getActionInfoOfVerfier(verifier, actionInfoGetter); } - interface VerifyAllFnActionResult { - actionInfos: readonly ActionInfo[]; - actionKey: ActionKey; - dependencyMap: server.ScriptInfo | undefined; - documentPositionMapper: server.ScriptInfo["documentPositionMapper"] | undefined; + if (isArray(actionInfoGetter)) { + const initialValue = getActionInfoOfVerfier(verifier, actionInfoGetter[0]); + return { + ...initialValue, + ...actionInfoGetter[1](initialValue) + }; } - function verifyAllFnAction({ - session, - host, - verifiers, - actionKey, - dependencyMap, - documentPositionMapper, - }: VerifyAllFnAction): VerifyAllFnActionResult { - const actionInfos = getActionInfo(verifiers, actionKey); - let sourceMapPath: server.ScriptInfo["sourceMapFilePath"] | undefined; - // action - let first = true; - for (const { - action, - closedInfos, - otherWatchedFiles, - expectsDts, - expectsMap, - freshMapInfo, - freshDocumentMapper, - skipDtsMapCheck - } of actionInfos) { - for (let fn = 1; fn <= 5; fn++) { - const fnAction = action(fn); - verifyAction(session, fnAction); - const debugInfo = `${actionKey}:: ${fnAction.reqName}:: ${fn}`; - const dtsInfo = verifyScriptInfoPresence(session, dtsPath, expectsDts, debugInfo); - const dtsMapInfo = verifyScriptInfoPresence(session, dtsMapPath, expectsMap, debugInfo); - verifyInfosWithRandom( - session, - host, - openFiles(verifiers).map(f => f.path), - closedInfos, - otherWatchedFiles, - debugInfo - ); - - if (dtsInfo) { - if (first || (fn === 1 && freshMapInfo)) { - if (!skipDtsMapCheck) { - if (dtsMapInfo) { - assert.equal(dtsInfo.sourceMapFilePath, dtsMapPath, debugInfo); - } - else { - assert.isNotString(dtsInfo.sourceMapFilePath, debugInfo); - assert.isNotFalse(dtsInfo.sourceMapFilePath, debugInfo); - assert.isDefined(dtsInfo.sourceMapFilePath, debugInfo); - } + return actionInfoGetter(); + } + function getActionInfo(verifiers: readonly DocumentPositionMapperVerifier[], actionKey: ActionKey): ActionInfo[] { + return verifiers.map(v => getActionInfoOfVerfier(v, actionKey)); + } + interface VerifyAllFnAction { + session: TestSession; + host: TestServerHost; + verifiers: readonly DocumentPositionMapperVerifier[]; + actionKey: ActionKey; + sourceMapPath?: ScriptInfo["sourceMapFilePath"]; + dependencyMap?: ScriptInfo | undefined; + documentPositionMapper?: ScriptInfo["documentPositionMapper"]; + } + interface VerifyAllFnActionResult { + actionInfos: readonly ActionInfo[]; + actionKey: ActionKey; + dependencyMap: ScriptInfo | undefined; + documentPositionMapper: ScriptInfo["documentPositionMapper"] | undefined; + } + function verifyAllFnAction({ session, host, verifiers, actionKey, dependencyMap, documentPositionMapper, }: VerifyAllFnAction): VerifyAllFnActionResult { + const actionInfos = getActionInfo(verifiers, actionKey); + let sourceMapPath: ScriptInfo["sourceMapFilePath"] | undefined; + // action + let first = true; + for (const { action, closedInfos, otherWatchedFiles, expectsDts, expectsMap, freshMapInfo, freshDocumentMapper, skipDtsMapCheck } of actionInfos) { + for (let fn = 1; fn <= 5; fn++) { + const fnAction = action(fn); + verifyAction(session, fnAction); + const debugInfo = `${actionKey}:: ${fnAction.reqName}:: ${fn}`; + const dtsInfo = verifyScriptInfoPresence(session, dtsPath, expectsDts, debugInfo); + const dtsMapInfo = verifyScriptInfoPresence(session, dtsMapPath, expectsMap, debugInfo); + verifyInfosWithRandom(session, host, openFiles(verifiers).map(f => f.path), closedInfos, otherWatchedFiles, debugInfo); + if (dtsInfo) { + if (first || (fn === 1 && freshMapInfo)) { + if (!skipDtsMapCheck) { + if (dtsMapInfo) { + assert.equal(dtsInfo.sourceMapFilePath, dtsMapPath, debugInfo); + } + else { + assert.isNotString(dtsInfo.sourceMapFilePath, debugInfo); + assert.isNotFalse(dtsInfo.sourceMapFilePath, debugInfo); + assert.isDefined(dtsInfo.sourceMapFilePath, debugInfo); } - } - else { - assert.equal(dtsInfo.sourceMapFilePath, sourceMapPath, debugInfo); } } - - if (!first && (fn !== 1 || !freshMapInfo)) { - verifyDocumentPositionMapper({ - session, - dependencyMap, - documentPositionMapper, - equal: fn !== 1 || !freshDocumentMapper, - debugInfo - }); + else { + assert.equal(dtsInfo.sourceMapFilePath, sourceMapPath, debugInfo); } - sourceMapPath = dtsInfo && dtsInfo.sourceMapFilePath; - dependencyMap = dtsMapInfo; - documentPositionMapper = dependencyMap && dependencyMap.documentPositionMapper; - first = false; } - } - - return { actionInfos, actionKey, dependencyMap, documentPositionMapper }; - } - - function verifyScriptInfoCollection( - session: TestSession, - host: TestServerHost, - verifiers: readonly DocumentPositionMapperVerifier[], - { dependencyMap, documentPositionMapper, actionInfos, actionKey }: VerifyAllFnActionResult - ) { - // Collecting at this point retains dependency.d.ts and map - closeFilesForSession([randomFile], session); - openFilesForSession([randomFile], session); - - const { closedInfos, otherWatchedFiles } = last(actionInfos); - const debugInfo = `${actionKey} Collection`; - verifyInfosWithRandom( - session, - host, - openFiles(verifiers).map(f => f.path), - closedInfos, - otherWatchedFiles, - debugInfo - ); - verifyDocumentPositionMapper({ - session, - dependencyMap, - documentPositionMapper, - equal: true, - debugInfo - }); - - // Closing open file, removes dependencies too - closeFilesForSession([...openFiles(verifiers), randomFile], session); - openFilesForSession([randomFile], session); - verifyOnlyRandomInfos(session, host); - } - - function verifyScenarioAndScriptInfoCollection( - session: TestSession, - host: TestServerHost, - verifiers: readonly DocumentPositionMapperVerifier[], - actionKey: ActionKey, - noDts?: true - ) { - // Main scenario action - const result = verifyAllFnAction({ session, host, verifiers, actionKey }); - checkProject(session, verifiers, noDts); - verifyScriptInfoCollection(session, host, verifiers, result); - } - - function verifyScenarioWithChangesWorker( - { - scenarioName, - verifier, - withRefs, - disableSourceOfProjectReferenceRedirect, - change, - afterChangeActionKey - }: VerifyScenarioWithChanges, - timeoutBeforeAction: boolean, - ) { - it(scenarioName, () => { - const { host, session, verifiers } = openTsFile({ verifier, withRefs, disableSourceOfProjectReferenceRedirect }); - - // Create DocumentPositionMapper - firstAction(session, verifiers); - const dependencyMap = session.getProjectService().filenameToScriptInfo.get(dtsMapPath); - const documentPositionMapper = dependencyMap && dependencyMap.documentPositionMapper; - - // change - change(host, session, verifiers); - if (timeoutBeforeAction) { - host.runQueuedTimeoutCallbacks(); - checkProject(session, verifiers); + if (!first && (fn !== 1 || !freshMapInfo)) { verifyDocumentPositionMapper({ session, dependencyMap, documentPositionMapper, - equal: true, - debugInfo: "After change timeout" + equal: fn !== 1 || !freshDocumentMapper, + debugInfo }); } - - // action - verifyAllFnAction({ + sourceMapPath = dtsInfo && dtsInfo.sourceMapFilePath; + dependencyMap = dtsMapInfo; + documentPositionMapper = dependencyMap && dependencyMap.documentPositionMapper; + first = false; + } + } + return { actionInfos, actionKey, dependencyMap, documentPositionMapper }; + } + function verifyScriptInfoCollection(session: TestSession, host: TestServerHost, verifiers: readonly DocumentPositionMapperVerifier[], { dependencyMap, documentPositionMapper, actionInfos, actionKey }: VerifyAllFnActionResult) { + // Collecting at this point retains dependency.d.ts and map + closeFilesForSession([randomFile], session); + openFilesForSession([randomFile], session); + const { closedInfos, otherWatchedFiles } = last(actionInfos); + const debugInfo = `${actionKey} Collection`; + verifyInfosWithRandom(session, host, openFiles(verifiers).map(f => f.path), closedInfos, otherWatchedFiles, debugInfo); + verifyDocumentPositionMapper({ + session, + dependencyMap, + documentPositionMapper, + equal: true, + debugInfo + }); + // Closing open file, removes dependencies too + closeFilesForSession([...openFiles(verifiers), randomFile], session); + openFilesForSession([randomFile], session); + verifyOnlyRandomInfos(session, host); + } + function verifyScenarioAndScriptInfoCollection(session: TestSession, host: TestServerHost, verifiers: readonly DocumentPositionMapperVerifier[], actionKey: ActionKey, noDts?: true) { + // Main scenario action + const result = verifyAllFnAction({ session, host, verifiers, actionKey }); + checkProject(session, verifiers, noDts); + verifyScriptInfoCollection(session, host, verifiers, result); + } + function verifyScenarioWithChangesWorker({ scenarioName, verifier, withRefs, disableSourceOfProjectReferenceRedirect, change, afterChangeActionKey }: VerifyScenarioWithChanges, timeoutBeforeAction: boolean) { + it(scenarioName, () => { + const { host, session, verifiers } = openTsFile({ verifier, withRefs, disableSourceOfProjectReferenceRedirect }); + // Create DocumentPositionMapper + firstAction(session, verifiers); + const dependencyMap = session.getProjectService().filenameToScriptInfo.get(dtsMapPath); + const documentPositionMapper = dependencyMap && dependencyMap.documentPositionMapper; + // change + change(host, session, verifiers); + if (timeoutBeforeAction) { + host.runQueuedTimeoutCallbacks(); + checkProject(session, verifiers); + verifyDocumentPositionMapper({ session, - host, - verifiers, - actionKey: afterChangeActionKey, dependencyMap, - documentPositionMapper + documentPositionMapper, + equal: true, + debugInfo: "After change timeout" }); + } + // action + verifyAllFnAction({ + session, + host, + verifiers, + actionKey: afterChangeActionKey, + dependencyMap, + documentPositionMapper }); - } - - interface VerifyScenarioWithChanges extends VerifierAndWithRefs { - scenarioName: string; - change: (host: TestServerHost, session: TestSession, verifiers: readonly DocumentPositionMapperVerifier[]) => void; - afterChangeActionKey: ActionKey; - } - function verifyScenarioWithChanges(verify: VerifyScenarioWithChanges) { - describe("when timeout occurs before request", () => { - verifyScenarioWithChangesWorker(verify, /*timeoutBeforeAction*/ true); + }); + } + interface VerifyScenarioWithChanges extends VerifierAndWithRefs { + scenarioName: string; + change: (host: TestServerHost, session: TestSession, verifiers: readonly DocumentPositionMapperVerifier[]) => void; + afterChangeActionKey: ActionKey; + } + function verifyScenarioWithChanges(verify: VerifyScenarioWithChanges) { + describe("when timeout occurs before request", () => { + verifyScenarioWithChangesWorker(verify, /*timeoutBeforeAction*/ true); + }); + describe("when timeout does not occur before request", () => { + verifyScenarioWithChangesWorker(verify, /*timeoutBeforeAction*/ false); + }); + } + interface VerifyScenarioWhenFileNotPresent extends VerifierAndWithRefs { + scenarioName: string; + fileLocation: string; + fileNotPresentKey: ActionKey; + fileCreatedKey: ActionKey; + fileDeletedKey: ActionKey; + noDts?: true; + } + function verifyScenarioWhenFileNotPresent({ scenarioName, verifier, withRefs, disableSourceOfProjectReferenceRedirect, fileLocation, fileNotPresentKey, fileCreatedKey, fileDeletedKey, noDts }: VerifyScenarioWhenFileNotPresent) { + describe(scenarioName, () => { + it("when file is not present", () => { + const { host, session, verifiers } = openTsFile({ + verifier, + withRefs, + disableSourceOfProjectReferenceRedirect, + onHostCreate: host => host.deleteFile(fileLocation) + }); + checkProject(session, verifiers, noDts); + verifyScenarioAndScriptInfoCollection(session, host, verifiers, fileNotPresentKey, noDts); }); - - describe("when timeout does not occur before request", () => { - verifyScenarioWithChangesWorker(verify, /*timeoutBeforeAction*/ false); + it("when file is created after actions on projects", () => { + let fileContents: string | undefined; + const { host, session, verifiers } = openTsFile({ + verifier, + withRefs, + disableSourceOfProjectReferenceRedirect, + onHostCreate: host => { + fileContents = host.readFile(fileLocation); + host.deleteFile(fileLocation); + } + }); + firstAction(session, verifiers); + host.writeFile(fileLocation, fileContents!); + verifyScenarioAndScriptInfoCollection(session, host, verifiers, fileCreatedKey); }); - } - - interface VerifyScenarioWhenFileNotPresent extends VerifierAndWithRefs { - scenarioName: string; - fileLocation: string; - fileNotPresentKey: ActionKey; - fileCreatedKey: ActionKey; - fileDeletedKey: ActionKey; - noDts?: true; - } - function verifyScenarioWhenFileNotPresent({ - scenarioName, + it("when file is deleted after actions on the projects", () => { + const { host, session, verifiers } = openTsFile({ verifier, disableSourceOfProjectReferenceRedirect, withRefs }); + firstAction(session, verifiers); + // The dependency file is deleted when orphan files are collected + host.deleteFile(fileLocation); + // Verify with deleted action key + verifyAllFnAction({ session, host, verifiers, actionKey: fileDeletedKey }); + checkProject(session, verifiers, noDts); + // Script info collection should behave as fileNotPresentKey + verifyScriptInfoCollection(session, host, verifiers, { + actionInfos: getActionInfo(verifiers, fileNotPresentKey), + actionKey: fileNotPresentKey, + dependencyMap: undefined, + documentPositionMapper: undefined + }); + }); + }); + } + function verifyScenarioWorker({ mainScenario, verifier }: VerifyScenario, withRefs: boolean, disableSourceOfProjectReferenceRedirect?: true) { + it(mainScenario, () => { + const { host, session, verifiers } = openTsFile({ withRefs, disableSourceOfProjectReferenceRedirect, verifier }); + checkProject(session, verifiers); + verifyScenarioAndScriptInfoCollection(session, host, verifiers, "main"); + }); + // Edit + verifyScenarioWithChanges({ + scenarioName: "when usage file changes, document position mapper doesnt change", verifier, withRefs, disableSourceOfProjectReferenceRedirect, - fileLocation, - fileNotPresentKey, - fileCreatedKey, - fileDeletedKey, - noDts - }: VerifyScenarioWhenFileNotPresent) { - describe(scenarioName, () => { - it("when file is not present", () => { - const { host, session, verifiers } = openTsFile({ - verifier, - withRefs, - disableSourceOfProjectReferenceRedirect, - onHostCreate: host => host.deleteFile(fileLocation) - }); - checkProject(session, verifiers, noDts); - - verifyScenarioAndScriptInfoCollection(session, host, verifiers, fileNotPresentKey, noDts); - }); - - it("when file is created after actions on projects", () => { - let fileContents: string | undefined; - const { host, session, verifiers } = openTsFile({ - verifier, - withRefs, - disableSourceOfProjectReferenceRedirect, - onHostCreate: host => { - fileContents = host.readFile(fileLocation); - host.deleteFile(fileLocation); - } - }); - firstAction(session, verifiers); - - host.writeFile(fileLocation, fileContents!); - verifyScenarioAndScriptInfoCollection(session, host, verifiers, fileCreatedKey); - }); - - it("when file is deleted after actions on the projects", () => { - const { host, session, verifiers } = openTsFile({ verifier, disableSourceOfProjectReferenceRedirect, withRefs }); - firstAction(session, verifiers); - - // The dependency file is deleted when orphan files are collected - host.deleteFile(fileLocation); - // Verify with deleted action key - verifyAllFnAction({ session, host, verifiers, actionKey: fileDeletedKey }); - checkProject(session, verifiers, noDts); - - // Script info collection should behave as fileNotPresentKey - verifyScriptInfoCollection( - session, - host, - verifiers, - { - actionInfos: getActionInfo(verifiers, fileNotPresentKey), - actionKey: fileNotPresentKey, - dependencyMap: undefined, - documentPositionMapper: undefined - } - ); - }); - }); - } - - function verifyScenarioWorker({ mainScenario, verifier }: VerifyScenario, withRefs: boolean, disableSourceOfProjectReferenceRedirect?: true) { - it(mainScenario, () => { - const { host, session, verifiers } = openTsFile({ withRefs, disableSourceOfProjectReferenceRedirect, verifier }); - checkProject(session, verifiers); - verifyScenarioAndScriptInfoCollection(session, host, verifiers, "main"); - }); - - // Edit + change: (_host, session, verifiers) => verifiers.forEach(verifier => session.executeCommandSeq({ + command: protocol.CommandTypes.Change, + arguments: { + file: verifier.openFile.path, + line: verifier.openFileLastLine, + offset: 1, + endLine: verifier.openFileLastLine, + endOffset: 1, + insertString: "const x = 10;" + } + })), + afterChangeActionKey: "change" + }); + // Edit dts to add new fn + verifyScenarioWithChanges({ + scenarioName: "when dependency .d.ts changes, document position mapper doesnt change", + verifier, + withRefs, + disableSourceOfProjectReferenceRedirect, + change: host => host.writeFile(dtsLocation, host.readFile(dtsLocation)!.replace("//# sourceMappingURL=FnS.d.ts.map", `export declare function fn6(): void; +//# sourceMappingURL=FnS.d.ts.map`)), + afterChangeActionKey: "dtsChange" + }); + // Edit map file to represent added new line + verifyScenarioWithChanges({ + scenarioName: "when dependency file's map changes", + verifier, + withRefs, + disableSourceOfProjectReferenceRedirect, + change: host => host.writeFile(dtsMapLocation, `{"version":3,"file":"FnS.d.ts","sourceRoot":"","sources":["../dependency/FnS.ts"],"names":[],"mappings":"AAAA,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,eAAO,MAAM,CAAC,KAAK,CAAC"}`), + afterChangeActionKey: "mapChange" + }); + verifyScenarioWhenFileNotPresent({ + scenarioName: "with depedency files map file", + verifier, + withRefs, + disableSourceOfProjectReferenceRedirect, + fileLocation: dtsMapLocation, + fileNotPresentKey: "noMap", + fileCreatedKey: "mapFileCreated", + fileDeletedKey: "mapFileDeleted" + }); + verifyScenarioWhenFileNotPresent({ + scenarioName: "with depedency .d.ts file", + verifier, + withRefs, + disableSourceOfProjectReferenceRedirect, + fileLocation: dtsLocation, + fileNotPresentKey: "noDts", + fileCreatedKey: "dtsFileCreated", + fileDeletedKey: "dtsFileDeleted", + noDts: true + }); + if (withRefs && !disableSourceOfProjectReferenceRedirect) { verifyScenarioWithChanges({ - scenarioName: "when usage file changes, document position mapper doesnt change", + scenarioName: "when defining project source changes", verifier, withRefs, - disableSourceOfProjectReferenceRedirect, - change: (_host, session, verifiers) => verifiers.forEach( - verifier => session.executeCommandSeq({ - command: protocol.CommandTypes.Change, - arguments: { - file: verifier.openFile.path, - line: verifier.openFileLastLine, - offset: 1, - endLine: verifier.openFileLastLine, - endOffset: 1, - insertString: "const x = 10;" - } - }) - ), - afterChangeActionKey: "change" + change: (host, session, verifiers) => { + // Make change, without rebuild of solution + if (contains(openFiles(verifiers), dependencyTs)) { + session.executeCommandSeq({ + command: protocol.CommandTypes.Change, + arguments: { + file: dependencyTs.path, line: 1, offset: 1, endLine: 1, endOffset: 1, insertString: `function fooBar() { } +` + } + }); + } + else { + host.writeFile(dependencyTs.path, `function fooBar() { } +${dependencyTs.content}`); + } + }, + afterChangeActionKey: "dependencyChange" }); - - // Edit dts to add new fn - verifyScenarioWithChanges({ - scenarioName: "when dependency .d.ts changes, document position mapper doesnt change", - verifier, - withRefs, - disableSourceOfProjectReferenceRedirect, - change: host => host.writeFile( - dtsLocation, - host.readFile(dtsLocation)!.replace( - "//# sourceMappingURL=FnS.d.ts.map", - `export declare function fn6(): void; -//# sourceMappingURL=FnS.d.ts.map` - ) - ), - afterChangeActionKey: "dtsChange" + it("when projects are not built", () => { + const host = createServerHost(files); + const session = createSession(host); + const verifiers = verifier(withRefs); + openFilesForSession([...openFiles(verifiers), randomFile], session); + verifyScenarioAndScriptInfoCollection(session, host, verifiers, "noBuild"); }); - - // Edit map file to represent added new line - verifyScenarioWithChanges({ - scenarioName: "when dependency file's map changes", - verifier, - withRefs, - disableSourceOfProjectReferenceRedirect, - change: host => host.writeFile( - dtsMapLocation, - `{"version":3,"file":"FnS.d.ts","sourceRoot":"","sources":["../dependency/FnS.ts"],"names":[],"mappings":"AAAA,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,eAAO,MAAM,CAAC,KAAK,CAAC"}` - ), - afterChangeActionKey: "mapChange" + } + } + interface VerifyScenario { + mainScenario: string; + verifier: (withRefs: boolean, disableSourceOfProjectReferenceRedirect?: true) => readonly DocumentPositionMapperVerifier[]; + } + function verifyScenario(scenario: VerifyScenario) { + describe(scenario.mainScenario, () => { + describe("when main tsconfig doesnt have project reference", () => { + verifyScenarioWorker(scenario, /*withRefs*/ false); }); - - verifyScenarioWhenFileNotPresent({ - scenarioName: "with depedency files map file", - verifier, - withRefs, - disableSourceOfProjectReferenceRedirect, - fileLocation: dtsMapLocation, - fileNotPresentKey: "noMap", - fileCreatedKey: "mapFileCreated", - fileDeletedKey: "mapFileDeleted" + describe("when main tsconfig has project reference", () => { + verifyScenarioWorker(scenario, /*withRefs*/ true); }); - - verifyScenarioWhenFileNotPresent({ - scenarioName: "with depedency .d.ts file", - verifier, - withRefs, - disableSourceOfProjectReferenceRedirect, - fileLocation: dtsLocation, - fileNotPresentKey: "noDts", - fileCreatedKey: "dtsFileCreated", - fileDeletedKey: "dtsFileDeleted", - noDts: true + describe("when main tsconfig has disableSourceOfProjectReferenceRedirect along with project reference", () => { + verifyScenarioWorker(scenario, /*withRefs*/ true, /*disableSourceOfProjectReferenceRedirect*/ true); }); - - if (withRefs && !disableSourceOfProjectReferenceRedirect) { - verifyScenarioWithChanges({ - scenarioName: "when defining project source changes", - verifier, - withRefs, - change: (host, session, verifiers) => { - // Make change, without rebuild of solution - if (contains(openFiles(verifiers), dependencyTs)) { - session.executeCommandSeq({ - command: protocol.CommandTypes.Change, - arguments: { - file: dependencyTs.path, line: 1, offset: 1, endLine: 1, endOffset: 1, insertString: `function fooBar() { } -`} - }); - } - else { - host.writeFile(dependencyTs.path, `function fooBar() { } -${dependencyTs.content}`); - } - }, - afterChangeActionKey: "dependencyChange" - }); - - it("when projects are not built", () => { - const host = createServerHost(files); - const session = createSession(host); - const verifiers = verifier(withRefs); - openFilesForSession([...openFiles(verifiers), randomFile], session); - verifyScenarioAndScriptInfoCollection(session, host, verifiers, "noBuild"); - }); - } - } - - interface VerifyScenario { - mainScenario: string; - verifier: (withRefs: boolean, disableSourceOfProjectReferenceRedirect?: true) => readonly DocumentPositionMapperVerifier[]; - } - function verifyScenario(scenario: VerifyScenario) { - describe(scenario.mainScenario, () => { - describe("when main tsconfig doesnt have project reference", () => { - verifyScenarioWorker(scenario, /*withRefs*/ false); - }); - describe("when main tsconfig has project reference", () => { - verifyScenarioWorker(scenario, /*withRefs*/ true); - }); - describe("when main tsconfig has disableSourceOfProjectReferenceRedirect along with project reference", () => { - verifyScenarioWorker(scenario, /*withRefs*/ true, /*disableSourceOfProjectReferenceRedirect*/ true); - }); - }); - } - - describe("from project that uses dependency", () => { - verifyScenario({ - mainScenario: "can go to definition correctly", - verifier: (withRefs, disableSourceOfProjectReferenceRedirect) => [ - { - ...goToDefFromMainTsProjectInfoVerifier(withRefs), - main: () => ({ - action: goToDefFromMainTs, - closedInfos: withRefs ? - [dependencyTs.path, dependencyConfig.path, libFile.path] : - disableSourceOfProjectReferenceRedirect ? - [dependencyTs.path, libFile.path, dtsPath, dtsMapLocation, dependencyConfig.path] : - [dependencyTs.path, libFile.path, dtsPath, dtsMapLocation], - otherWatchedFiles: [mainConfig.path], - expectsDts: !withRefs, // Dts script info present only if no project reference - expectsMap: !withRefs // Map script info present only if no project reference - }), - change: "main", - dtsChange: "main", - mapChange: ["main", () => ({ + }); + } + describe("from project that uses dependency", () => { + verifyScenario({ + mainScenario: "can go to definition correctly", + verifier: (withRefs, disableSourceOfProjectReferenceRedirect) => [ + { + ...goToDefFromMainTsProjectInfoVerifier(withRefs), + main: () => ({ + action: goToDefFromMainTs, + closedInfos: withRefs ? + [dependencyTs.path, dependencyConfig.path, libFile.path] : + disableSourceOfProjectReferenceRedirect ? + [dependencyTs.path, libFile.path, dtsPath, dtsMapLocation, dependencyConfig.path] : + [dependencyTs.path, libFile.path, dtsPath, dtsMapLocation], + otherWatchedFiles: [mainConfig.path], + expectsDts: !withRefs, + expectsMap: !withRefs // Map script info present only if no project reference + }), + change: "main", + dtsChange: "main", + mapChange: ["main", () => ({ freshDocumentMapper: true })], - noMap: withRefs ? - "main" : - ["main", main => ({ + noMap: withRefs ? + "main" : + ["main", main => ({ action: goToDefFromMainTsWithNoMap, // Because map is deleted, dts and dependency are released closedInfos: removePath(main.closedInfos, dtsMapPath, dependencyTsPath), @@ -1019,67 +864,66 @@ ${dependencyTs.content}`); otherWatchedFiles: main.otherWatchedFiles.concat(dtsMapLocation), expectsMap: false })], - mapFileCreated: "main", - mapFileDeleted: withRefs ? - "main" : - ["noMap", noMap => ({ + mapFileCreated: "main", + mapFileDeleted: withRefs ? + "main" : + ["noMap", noMap => ({ // The script info for depedency is collected only after file open closedInfos: noMap.closedInfos.concat(dependencyTs.path) })], - noDts: withRefs ? - "main" : - ["main", main => ({ + noDts: withRefs ? + "main" : + ["main", main => ({ action: goToDefFromMainTsWithNoDts, // No dts, no map, no dependency closedInfos: removePath(main.closedInfos, dtsPath, dtsMapPath, dependencyTsPath), expectsDts: false, expectsMap: false })], - dtsFileCreated: "main", - dtsFileDeleted: withRefs ? - "main" : - ["noDts", noDts => ({ + dtsFileCreated: "main", + dtsFileDeleted: withRefs ? + "main" : + ["noDts", noDts => ({ // The script info for map is collected only after file open closedInfos: noDts.closedInfos.concat(dependencyTs.path, dtsMapLocation), expectsMap: true })], - dependencyChange: ["main", () => ({ + dependencyChange: ["main", () => ({ action: goToDefFromMainTsWithDependencyChange, })], - noBuild: "noDts" - } - ] - }); + noBuild: "noDts" + } + ] }); - - describe("from defining project", () => { - verifyScenario({ - mainScenario: "rename locations from dependency", - verifier: () => [ - { - ...renameFromDependencyTsProjectInfoVerifier(), - main: () => ({ - action: renameFromDependencyTs, - closedInfos: [libFile.path, dtsLocation, dtsMapLocation], - otherWatchedFiles: [dependencyConfig.path], - expectsDts: true, - expectsMap: true - }), - change: "main", - dtsChange: "main", - mapChange: ["main", () => ({ + }); + describe("from defining project", () => { + verifyScenario({ + mainScenario: "rename locations from dependency", + verifier: () => [ + { + ...renameFromDependencyTsProjectInfoVerifier(), + main: () => ({ + action: renameFromDependencyTs, + closedInfos: [libFile.path, dtsLocation, dtsMapLocation], + otherWatchedFiles: [dependencyConfig.path], + expectsDts: true, + expectsMap: true + }), + change: "main", + dtsChange: "main", + mapChange: ["main", () => ({ freshDocumentMapper: true })], - noMap: ["main", main => ({ + noMap: ["main", main => ({ // No map closedInfos: removePath(main.closedInfos, dtsMapPath), // watch map otherWatchedFiles: [...main.otherWatchedFiles, dtsMapLocation], expectsMap: false })], - mapFileCreated: "main", - mapFileDeleted: "noMap", - noDts: ["main", main => ({ + mapFileCreated: "main", + mapFileDeleted: "noMap", + noDts: ["main", main => ({ // no dts or map since dts itself doesnt exist closedInfos: removePath(main.closedInfos, dtsMapPath, dtsPath), // watch deleted file @@ -1087,127 +931,126 @@ ${dependencyTs.content}`); expectsDts: false, expectsMap: false })], - dtsFileCreated: "main", - dtsFileDeleted: ["noDts", noDts => ({ + dtsFileCreated: "main", + dtsFileDeleted: ["noDts", noDts => ({ // Map is collected after file open closedInfos: noDts.closedInfos.concat(dtsMapLocation), expectsMap: true })], - dependencyChange: ["main", () => ({ + dependencyChange: ["main", () => ({ action: renameFromDependencyTsWithDependencyChange })], - noBuild: "noDts" - } - ] - }); + noBuild: "noDts" + } + ] }); - - describe("when opening depedency and usage project", () => { - verifyScenario({ - mainScenario: "goto Definition in usage and rename locations from defining project", - verifier: (withRefs, disableSourceOfProjectReferenceRedirect) => [ - { - ...goToDefFromMainTsProjectInfoVerifier(withRefs), - main: () => ({ - action: goToDefFromMainTs, - // DependencyTs is open, so omit it from closed infos - closedInfos: withRefs ? - [dependencyConfig.path, libFile.path] : - disableSourceOfProjectReferenceRedirect ? - [libFile.path, dtsPath, dtsMapLocation, dependencyConfig.path] : - [libFile.path, dtsPath, dtsMapLocation], - otherWatchedFiles: withRefs || disableSourceOfProjectReferenceRedirect ? - [mainConfig.path] : // dependencyConfig is in closed info - [mainConfig.path, dependencyConfig.path], - expectsDts: !withRefs, // Dts script info present only if no project reference - expectsMap: !withRefs // Map script info present only if no project reference - }), - change: withRefs ? - ["main", main => ({ + }); + describe("when opening depedency and usage project", () => { + verifyScenario({ + mainScenario: "goto Definition in usage and rename locations from defining project", + verifier: (withRefs, disableSourceOfProjectReferenceRedirect) => [ + { + ...goToDefFromMainTsProjectInfoVerifier(withRefs), + main: () => ({ + action: goToDefFromMainTs, + // DependencyTs is open, so omit it from closed infos + closedInfos: withRefs ? + [dependencyConfig.path, libFile.path] : + disableSourceOfProjectReferenceRedirect ? + [libFile.path, dtsPath, dtsMapLocation, dependencyConfig.path] : + [libFile.path, dtsPath, dtsMapLocation], + otherWatchedFiles: withRefs || disableSourceOfProjectReferenceRedirect ? + [mainConfig.path] : // dependencyConfig is in closed info + [mainConfig.path, dependencyConfig.path], + expectsDts: !withRefs, + expectsMap: !withRefs // Map script info present only if no project reference + }), + change: withRefs ? + ["main", main => ({ // Because before this rename is done the closed info remains same as rename's main operation closedInfos: main.closedInfos.concat(dtsLocation, dtsMapLocation), expectsDts: true, expectsMap: true })] : - "main", - dtsChange: "change", - mapChange: "change", - noMap: withRefs ? - "main" : - ["main", main => ({ + "main", + dtsChange: "change", + mapChange: "change", + noMap: withRefs ? + "main" : + ["main", main => ({ action: goToDefFromMainTsWithNoMap, closedInfos: removePath(main.closedInfos, dtsMapPath), otherWatchedFiles: main.otherWatchedFiles.concat(dtsMapLocation), expectsMap: false })], - mapFileCreated: withRefs ? - ["main", main => ({ + mapFileCreated: withRefs ? + ["main", main => ({ // Because before this rename is done the closed info remains same as rename's main closedInfos: main.closedInfos.concat(dtsLocation), expectsDts: true, // This operation doesnt need map so the map info path in dts is not refreshed skipDtsMapCheck: withRefs })] : - "main", - mapFileDeleted: withRefs ? - ["noMap", noMap => ({ + "main", + mapFileDeleted: withRefs ? + ["noMap", noMap => ({ // Because before this rename is done the closed info remains same as rename's noMap operation closedInfos: noMap.closedInfos.concat(dtsLocation), expectsDts: true, // This operation doesnt need map so the map info path in dts is not refreshed skipDtsMapCheck: true })] : - "noMap", - noDts: withRefs ? - "main" : - ["main", main => ({ + "noMap", + noDts: withRefs ? + "main" : + ["main", main => ({ action: goToDefFromMainTsWithNoDts, closedInfos: removePath(main.closedInfos, dtsMapPath, dtsPath), expectsDts: false, expectsMap: false })], - dtsFileCreated: withRefs ? - ["main", main => ({ + dtsFileCreated: withRefs ? + ["main", main => ({ // Since the project for dependency is not updated, the watcher from rename for dts still there otherWatchedFiles: main.otherWatchedFiles.concat(dtsLocation) })] : - "main", - dtsFileDeleted: ["noDts", noDts => ({ + "main", + dtsFileDeleted: ["noDts", noDts => ({ // Map collection after file open closedInfos: noDts.closedInfos.concat(dtsMapLocation), expectsMap: true })], - dependencyChange: ["change", () => ({ + dependencyChange: ["change", () => ({ action: goToDefFromMainTsWithDependencyChange, })], - noBuild: "noDts" - }, - { - ...renameFromDependencyTsProjectInfoVerifier(), - main: () => ({ - action: renameFromDependencyTsWithBothProjectsOpen, - // DependencyTs is open, so omit it from closed infos - closedInfos: withRefs ? - [dependencyConfig.path, libFile.path, dtsLocation, dtsMapLocation] : - disableSourceOfProjectReferenceRedirect ? - [libFile.path, dtsPath, dtsMapLocation, dependencyConfig.path] : - [libFile.path, dtsPath, dtsMapLocation], - otherWatchedFiles: withRefs || disableSourceOfProjectReferenceRedirect ? - [mainConfig.path] : // dependencyConfig is in closed info - [mainConfig.path, dependencyConfig.path], - expectsDts: true, - expectsMap: true, - freshMapInfo: withRefs - }), - change: ["main", () => ({ + noBuild: "noDts" + }, + { + ...renameFromDependencyTsProjectInfoVerifier(), + main: () => ({ + action: renameFromDependencyTsWithBothProjectsOpen, + // DependencyTs is open, so omit it from closed infos + closedInfos: withRefs ? + [dependencyConfig.path, libFile.path, dtsLocation, dtsMapLocation] : + disableSourceOfProjectReferenceRedirect ? + [libFile.path, dtsPath, dtsMapLocation, dependencyConfig.path] : + [libFile.path, dtsPath, dtsMapLocation], + otherWatchedFiles: withRefs || disableSourceOfProjectReferenceRedirect ? + [mainConfig.path] : // dependencyConfig is in closed info + [mainConfig.path, dependencyConfig.path], + expectsDts: true, + expectsMap: true, + freshMapInfo: withRefs + }), + change: ["main", () => ({ freshMapInfo: false })], - dtsChange: "change", - mapChange: ["main", () => ({ + dtsChange: "change", + mapChange: ["main", () => ({ freshMapInfo: false, freshDocumentMapper: withRefs })], - noMap: ["main", main => ({ + noMap: ["main", main => ({ action: withRefs ? renameFromDependencyTsWithBothProjectsOpen : renameFromDependencyTs, @@ -1216,9 +1059,9 @@ ${dependencyTs.content}`); expectsMap: false, freshDocumentMapper: withRefs })], - mapFileCreated: "main", - mapFileDeleted: "noMap", - noDts: ["change", change => ({ + mapFileCreated: "main", + mapFileDeleted: "noMap", + noDts: ["change", change => ({ action: withRefs ? renameFromDependencyTsWithBothProjectsOpen : renameFromDependencyTs, @@ -1227,617 +1070,584 @@ ${dependencyTs.content}`); expectsDts: false, expectsMap: false })], - dtsFileCreated: "main", - dtsFileDeleted: ["noDts", noDts => ({ + dtsFileCreated: "main", + dtsFileDeleted: ["noDts", noDts => ({ // Map collection after file open closedInfos: noDts.closedInfos.concat(dtsMapLocation), expectsMap: true })], - dependencyChange: ["change", () => ({ + dependencyChange: ["change", () => ({ action: renameFromDependencyTsWithBothProjectsOpenWithDependencyChange })], - noBuild: "noDts" - } - ] - }); - }); - }); - - describe("when root file is file from referenced project", () => { - function verify(disableSourceOfProjectReferenceRedirect: boolean) { - const projectLocation = `/user/username/projects/project`; - const commonConfig: File = { - path: `${projectLocation}/src/common/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - composite: true, - declarationMap: true, - outDir: "../../out", - baseUrl: "..", - disableSourceOfProjectReferenceRedirect - }, - include: ["./**/*"] - }) - }; - const keyboardTs: File = { - path: `${projectLocation}/src/common/input/keyboard.ts`, - content: `function bar() { return "just a random function so .d.ts location doesnt match"; } -export function evaluateKeyboardEvent() { }` - }; - const keyboardTestTs: File = { - path: `${projectLocation}/src/common/input/keyboard.test.ts`, - content: `import { evaluateKeyboardEvent } from 'common/input/keyboard'; -function testEvaluateKeyboardEvent() { - return evaluateKeyboardEvent(); -} -` - }; - const srcConfig: File = { - path: `${projectLocation}/src/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - composite: true, - declarationMap: true, - outDir: "../out", - baseUrl: ".", - paths: { - "common/*": ["./common/*"], - }, - tsBuildInfoFile: "../out/src.tsconfig.tsbuildinfo", - disableSourceOfProjectReferenceRedirect - }, - include: ["./**/*"], - references: [ - { path: "./common" } - ] - }) - }; - const terminalTs: File = { - path: `${projectLocation}/src/terminal.ts`, - content: `import { evaluateKeyboardEvent } from 'common/input/keyboard'; -function foo() { - return evaluateKeyboardEvent(); -} -` - }; - const host = createHost( - [commonConfig, keyboardTs, keyboardTestTs, srcConfig, terminalTs, libFile], - [srcConfig.path] - ); - const session = createSession(host); - openFilesForSession([keyboardTs, terminalTs], session); - - const searchStr = "evaluateKeyboardEvent"; - const importStr = `import { evaluateKeyboardEvent } from 'common/input/keyboard';`; - const result = session.executeCommandSeq({ - command: protocol.CommandTypes.References, - arguments: protocolFileLocationFromSubstring(keyboardTs, searchStr) - }).response as protocol.ReferencesResponseBody; - assert.deepEqual(result, { - refs: [ - makeReferenceItem({ - file: keyboardTs, - text: searchStr, - contextText: `export function evaluateKeyboardEvent() { }`, - isDefinition: true, - lineText: `export function evaluateKeyboardEvent() { }` - }), - makeReferenceItem({ - file: keyboardTestTs, - text: searchStr, - contextText: importStr, - isDefinition: true, - lineText: importStr - }), - makeReferenceItem({ - file: keyboardTestTs, - text: searchStr, - options: { index: 1 }, - isDefinition: false, - lineText: ` return evaluateKeyboardEvent();` - }), - makeReferenceItem({ - file: terminalTs, - text: searchStr, - contextText: importStr, - isDefinition: true, - lineText: importStr - }), - makeReferenceItem({ - file: terminalTs, - text: searchStr, - options: { index: 1 }, - isDefinition: false, - lineText: ` return evaluateKeyboardEvent();` - }), - ], - symbolName: searchStr, - symbolStartOffset: protocolLocationFromSubstring(keyboardTs.content, searchStr).offset, - symbolDisplayString: "function evaluateKeyboardEvent(): void" - }); - } - - it(`when using declaration file maps to navigate between projects`, () => { - verify(/*disableSourceOfProjectReferenceRedirect*/ true); - }); - it(`when using original source files in the project`, () => { - verify(/*disableSourceOfProjectReferenceRedirect*/ false); + noBuild: "noDts" + } + ] }); }); - - it("reusing d.ts files from composite and non composite projects", () => { - const configA: File = { - path: `${tscWatch.projectRoot}/compositea/tsconfig.json`, + }); + describe("when root file is file from referenced project", () => { + function verify(disableSourceOfProjectReferenceRedirect: boolean) { + const projectLocation = `/user/username/projects/project`; + const commonConfig: File = { + path: `${projectLocation}/src/common/tsconfig.json`, content: JSON.stringify({ compilerOptions: { composite: true, - outDir: "../dist/", - rootDir: "../", - baseUrl: "../", - paths: { "@ref/*": ["./dist/*"] } - } + declarationMap: true, + outDir: "../../out", + baseUrl: "..", + disableSourceOfProjectReferenceRedirect + }, + include: ["./**/*"] }) }; - const aTs: File = { - path: `${tscWatch.projectRoot}/compositea/a.ts`, - content: `import { b } from "@ref/compositeb/b";` - }; - const a2Ts: File = { - path: `${tscWatch.projectRoot}/compositea/a2.ts`, - content: `export const x = 10;` - }; - const configB: File = { - path: `${tscWatch.projectRoot}/compositeb/tsconfig.json`, - content: configA.content - }; - const bTs: File = { - path: `${tscWatch.projectRoot}/compositeb/b.ts`, - content: "export function b() {}" + const keyboardTs: File = { + path: `${projectLocation}/src/common/input/keyboard.ts`, + content: `function bar() { return "just a random function so .d.ts location doesnt match"; } +export function evaluateKeyboardEvent() { }` }; - const bDts: File = { - path: `${tscWatch.projectRoot}/dist/compositeb/b.d.ts`, - content: "export declare function b(): void;" + const keyboardTestTs: File = { + path: `${projectLocation}/src/common/input/keyboard.test.ts`, + content: `import { evaluateKeyboardEvent } from 'common/input/keyboard'; +function testEvaluateKeyboardEvent() { + return evaluateKeyboardEvent(); +} +` }; - const configC: File = { - path: `${tscWatch.projectRoot}/compositec/tsconfig.json`, + const srcConfig: File = { + path: `${projectLocation}/src/tsconfig.json`, content: JSON.stringify({ compilerOptions: { composite: true, - outDir: "../dist/", - rootDir: "../", - baseUrl: "../", - paths: { "@ref/*": ["./*"] } + declarationMap: true, + outDir: "../out", + baseUrl: ".", + paths: { + "common/*": ["./common/*"], + }, + tsBuildInfoFile: "../out/src.tsconfig.tsbuildinfo", + disableSourceOfProjectReferenceRedirect }, - references: [{ path: "../compositeb" }] + include: ["./**/*"], + references: [ + { path: "./common" } + ] }) }; - const cTs: File = { - path: `${tscWatch.projectRoot}/compositec/c.ts`, - content: aTs.content + const terminalTs: File = { + path: `${projectLocation}/src/terminal.ts`, + content: `import { evaluateKeyboardEvent } from 'common/input/keyboard'; +function foo() { + return evaluateKeyboardEvent(); +} +` }; - const files = [libFile, aTs, a2Ts, configA, bDts, bTs, configB, cTs, configC]; - const host = createServerHost(files); - const service = createProjectService(host); - service.openClientFile(aTs.path); - service.checkNumberOfProjects({ configuredProjects: 1 }); - - // project A referencing b.d.ts without project reference - const projectA = service.configuredProjects.get(configA.path)!; - assert.isDefined(projectA); - checkProjectActualFiles(projectA, [aTs.path, a2Ts.path, bDts.path, libFile.path, configA.path]); - - // reuses b.d.ts but sets the path and resolved path since projectC has project references - // as the real resolution was to b.ts - service.openClientFile(cTs.path); - service.checkNumberOfProjects({ configuredProjects: 2 }); - const projectC = service.configuredProjects.get(configC.path)!; - checkProjectActualFiles(projectC, [cTs.path, bTs.path, libFile.path, configC.path]); - - // Now new project for project A tries to reuse b but there is no filesByName mapping for b's source location - host.writeFile(a2Ts.path, `${a2Ts.content}export const y = 30;`); - assert.isTrue(projectA.dirty); - projectA.updateGraph(); + const host = createHost([commonConfig, keyboardTs, keyboardTestTs, srcConfig, terminalTs, libFile], [srcConfig.path]); + const session = createSession(host); + openFilesForSession([keyboardTs, terminalTs], session); + const searchStr = "evaluateKeyboardEvent"; + const importStr = `import { evaluateKeyboardEvent } from 'common/input/keyboard';`; + const result = (session.executeCommandSeq({ + command: protocol.CommandTypes.References, + arguments: protocolFileLocationFromSubstring(keyboardTs, searchStr) + }).response as protocol.ReferencesResponseBody); + assert.deepEqual(result, { + refs: [ + makeReferenceItem({ + file: keyboardTs, + text: searchStr, + contextText: `export function evaluateKeyboardEvent() { }`, + isDefinition: true, + lineText: `export function evaluateKeyboardEvent() { }` + }), + makeReferenceItem({ + file: keyboardTestTs, + text: searchStr, + contextText: importStr, + isDefinition: true, + lineText: importStr + }), + makeReferenceItem({ + file: keyboardTestTs, + text: searchStr, + options: { index: 1 }, + isDefinition: false, + lineText: ` return evaluateKeyboardEvent();` + }), + makeReferenceItem({ + file: terminalTs, + text: searchStr, + contextText: importStr, + isDefinition: true, + lineText: importStr + }), + makeReferenceItem({ + file: terminalTs, + text: searchStr, + options: { index: 1 }, + isDefinition: false, + lineText: ` return evaluateKeyboardEvent();` + }), + ], + symbolName: searchStr, + symbolStartOffset: protocolLocationFromSubstring(keyboardTs.content, searchStr).offset, + symbolDisplayString: "function evaluateKeyboardEvent(): void" + }); + } + it(`when using declaration file maps to navigate between projects`, () => { + verify(/*disableSourceOfProjectReferenceRedirect*/ true); }); - - describe("when references are monorepo like with symlinks", () => { - interface Packages { - bPackageJson: File; - aTest: File; - bFoo: File; - bBar: File; - bSymlink: SymLink; - } - function verifySymlinkScenario(packages: () => Packages) { - describe("when solution is not built", () => { - it("with preserveSymlinks turned off", () => { - verifySession(packages(), /*alreadyBuilt*/ false, {}); - }); - - it("with preserveSymlinks turned on", () => { - verifySession(packages(), /*alreadyBuilt*/ false, { preserveSymlinks: true }); - }); + it(`when using original source files in the project`, () => { + verify(/*disableSourceOfProjectReferenceRedirect*/ false); + }); + }); + it("reusing d.ts files from composite and non composite projects", () => { + const configA: File = { + path: `${projectRoot}/compositea/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + composite: true, + outDir: "../dist/", + rootDir: "../", + baseUrl: "../", + paths: { "@ref/*": ["./dist/*"] } + } + }) + }; + const aTs: File = { + path: `${projectRoot}/compositea/a.ts`, + content: `import { b } from "@ref/compositeb/b";` + }; + const a2Ts: File = { + path: `${projectRoot}/compositea/a2.ts`, + content: `export const x = 10;` + }; + const configB: File = { + path: `${projectRoot}/compositeb/tsconfig.json`, + content: configA.content + }; + const bTs: File = { + path: `${projectRoot}/compositeb/b.ts`, + content: "export function b() {}" + }; + const bDts: File = { + path: `${projectRoot}/dist/compositeb/b.d.ts`, + content: "export declare function b(): void;" + }; + const configC: File = { + path: `${projectRoot}/compositec/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + composite: true, + outDir: "../dist/", + rootDir: "../", + baseUrl: "../", + paths: { "@ref/*": ["./*"] } + }, + references: [{ path: "../compositeb" }] + }) + }; + const cTs: File = { + path: `${projectRoot}/compositec/c.ts`, + content: aTs.content + }; + const files = [libFile, aTs, a2Ts, configA, bDts, bTs, configB, cTs, configC]; + const host = createServerHost(files); + const service = createProjectService(host); + service.openClientFile(aTs.path); + service.checkNumberOfProjects({ configuredProjects: 1 }); + // project A referencing b.d.ts without project reference + const projectA = service.configuredProjects.get(configA.path)!; + assert.isDefined(projectA); + checkProjectActualFiles(projectA, [aTs.path, a2Ts.path, bDts.path, libFile.path, configA.path]); + // reuses b.d.ts but sets the path and resolved path since projectC has project references + // as the real resolution was to b.ts + service.openClientFile(cTs.path); + service.checkNumberOfProjects({ configuredProjects: 2 }); + const projectC = service.configuredProjects.get(configC.path)!; + checkProjectActualFiles(projectC, [cTs.path, bTs.path, libFile.path, configC.path]); + // Now new project for project A tries to reuse b but there is no filesByName mapping for b's source location + host.writeFile(a2Ts.path, `${a2Ts.content}export const y = 30;`); + assert.isTrue(projectA.dirty); + projectA.updateGraph(); + }); + describe("when references are monorepo like with symlinks", () => { + interface Packages { + bPackageJson: File; + aTest: File; + bFoo: File; + bBar: File; + bSymlink: SymLink; + } + function verifySymlinkScenario(packages: () => Packages) { + describe("when solution is not built", () => { + it("with preserveSymlinks turned off", () => { + verifySession(packages(), /*alreadyBuilt*/ false, {}); }); - - describe("when solution is already built", () => { - it("with preserveSymlinks turned off", () => { - verifySession(packages(), /*alreadyBuilt*/ true, {}); - }); - - it("with preserveSymlinks turned on", () => { - verifySession(packages(), /*alreadyBuilt*/ true, { preserveSymlinks: true }); - }); + it("with preserveSymlinks turned on", () => { + verifySession(packages(), /*alreadyBuilt*/ false, { preserveSymlinks: true }); }); - } - - function verifySession({ bPackageJson, aTest, bFoo, bBar, bSymlink }: Packages, alreadyBuilt: boolean, extraOptions: CompilerOptions) { - const aConfig = config("A", extraOptions, ["../B"]); - const bConfig = config("B", extraOptions); - const files = [libFile, bPackageJson, aConfig, bConfig, aTest, bFoo, bBar, bSymlink]; - const host = alreadyBuilt ? - createHost(files, [aConfig.path]) : - createServerHost(files); - - // Create symlink in node module - const session = createSession(host, { canUseEvents: true }); - openFilesForSession([aTest], session); - const service = session.getProjectService(); - const project = service.configuredProjects.get(aConfig.path.toLowerCase())!; - assert.deepEqual(project.getAllProjectErrors(), []); - checkProjectActualFiles( - project, - [aConfig.path, aTest.path, bFoo.path, bBar.path, libFile.path] - ); - verifyGetErrRequest({ - host, - session, - expected: [ - { file: aTest, syntax: [], semantic: [], suggestion: [] } - ] + }); + describe("when solution is already built", () => { + it("with preserveSymlinks turned off", () => { + verifySession(packages(), /*alreadyBuilt*/ true, {}); }); - session.executeCommandSeq({ - command: protocol.CommandTypes.UpdateOpen, - arguments: { - changedFiles: [{ + it("with preserveSymlinks turned on", () => { + verifySession(packages(), /*alreadyBuilt*/ true, { preserveSymlinks: true }); + }); + }); + } + function verifySession({ bPackageJson, aTest, bFoo, bBar, bSymlink }: Packages, alreadyBuilt: boolean, extraOptions: CompilerOptions) { + const aConfig = config("A", extraOptions, ["../B"]); + const bConfig = config("B", extraOptions); + const files = [libFile, bPackageJson, aConfig, bConfig, aTest, bFoo, bBar, bSymlink]; + const host = alreadyBuilt ? + createHost(files, [aConfig.path]) : + createServerHost(files); + // Create symlink in node module + const session = createSession(host, { canUseEvents: true }); + openFilesForSession([aTest], session); + const service = session.getProjectService(); + const project = service.configuredProjects.get(aConfig.path.toLowerCase())!; + assert.deepEqual(project.getAllProjectErrors(), []); + checkProjectActualFiles(project, [aConfig.path, aTest.path, bFoo.path, bBar.path, libFile.path]); + verifyGetErrRequest({ + host, + session, + expected: [ + { file: aTest, syntax: [], semantic: [], suggestion: [] } + ] + }); + session.executeCommandSeq({ + command: protocol.CommandTypes.UpdateOpen, + arguments: { + changedFiles: [{ fileName: aTest.path, textChanges: [{ - newText: "\n", - start: { line: 5, offset: 1 }, - end: { line: 5, offset: 1 } - }] + newText: "\n", + start: { line: 5, offset: 1 }, + end: { line: 5, offset: 1 } + }] }] - } - }); - verifyGetErrRequest({ - host, - session, - expected: [ - { file: aTest, syntax: [], semantic: [], suggestion: [] } - ] - }); - } - - function config(packageName: string, extraOptions: CompilerOptions, references?: string[]): File { - return { - path: `${tscWatch.projectRoot}/packages/${packageName}/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - outDir: "lib", - rootDir: "src", - composite: true, - ...extraOptions - }, - include: ["src"], - ...(references ? { references: references.map(path => ({ path })) } : {}) - }) - }; - } - - function file(packageName: string, fileName: string, content: string): File { - return { - path: `${tscWatch.projectRoot}/packages/${packageName}/src/${fileName}`, - content - }; - } - - function verifyMonoRepoLike(scope = "") { - describe("when packageJson has types field and has index.ts", () => { - verifySymlinkScenario(() => ({ - bPackageJson: { - path: `${tscWatch.projectRoot}/packages/B/package.json`, - content: JSON.stringify({ - main: "lib/index.js", - types: "lib/index.d.ts" - }) - }, - aTest: file("A", "index.ts", `import { foo } from '${scope}b'; + } + }); + verifyGetErrRequest({ + host, + session, + expected: [ + { file: aTest, syntax: [], semantic: [], suggestion: [] } + ] + }); + } + function config(packageName: string, extraOptions: CompilerOptions, references?: string[]): File { + return { + path: `${projectRoot}/packages/${packageName}/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + outDir: "lib", + rootDir: "src", + composite: true, + ...extraOptions + }, + include: ["src"], + ...(references ? { references: references.map(path => ({ path })) } : {}) + }) + }; + } + function file(packageName: string, fileName: string, content: string): File { + return { + path: `${projectRoot}/packages/${packageName}/src/${fileName}`, + content + }; + } + function verifyMonoRepoLike(scope = "") { + describe("when packageJson has types field and has index.ts", () => { + verifySymlinkScenario(() => ({ + bPackageJson: { + path: `${projectRoot}/packages/B/package.json`, + content: JSON.stringify({ + main: "lib/index.js", + types: "lib/index.d.ts" + }) + }, + aTest: file("A", "index.ts", `import { foo } from '${scope}b'; import { bar } from '${scope}b/lib/bar'; foo(); bar(); `), - bFoo: file("B", "index.ts", `export function foo() { }`), - bBar: file("B", "bar.ts", `export function bar() { }`), - bSymlink: { - path: `${tscWatch.projectRoot}/node_modules/${scope}b`, - symLink: `${tscWatch.projectRoot}/packages/B` - } - })); - }); - - describe("when referencing file from subFolder", () => { - verifySymlinkScenario(() => ({ - bPackageJson: { - path: `${tscWatch.projectRoot}/packages/B/package.json`, - content: "{}" - }, - aTest: file("A", "test.ts", `import { foo } from '${scope}b/lib/foo'; + bFoo: file("B", "index.ts", `export function foo() { }`), + bBar: file("B", "bar.ts", `export function bar() { }`), + bSymlink: { + path: `${projectRoot}/node_modules/${scope}b`, + symLink: `${projectRoot}/packages/B` + } + })); + }); + describe("when referencing file from subFolder", () => { + verifySymlinkScenario(() => ({ + bPackageJson: { + path: `${projectRoot}/packages/B/package.json`, + content: "{}" + }, + aTest: file("A", "test.ts", `import { foo } from '${scope}b/lib/foo'; import { bar } from '${scope}b/lib/bar/foo'; foo(); bar(); `), - bFoo: file("B", "foo.ts", `export function foo() { }`), - bBar: file("B", "bar/foo.ts", `export function bar() { }`), - bSymlink: { - path: `${tscWatch.projectRoot}/node_modules/${scope}b`, - symLink: `${tscWatch.projectRoot}/packages/B` - } - })); - }); - } - describe("when package is not scoped", () => { - verifyMonoRepoLike(); - }); - describe("when package is scoped", () => { - verifyMonoRepoLike("@issue/"); + bFoo: file("B", "foo.ts", `export function foo() { }`), + bBar: file("B", "bar/foo.ts", `export function bar() { }`), + bSymlink: { + path: `${projectRoot}/node_modules/${scope}b`, + symLink: `${projectRoot}/packages/B` + } + })); }); + } + describe("when package is not scoped", () => { + verifyMonoRepoLike(); }); - - it("when finding local reference doesnt load ancestor/sibling projects", () => { - const solutionLocation = "/user/username/projects/solution"; - const solution: File = { - path: `${solutionLocation}/tsconfig.json`, - content: JSON.stringify({ - files: [], - include: [], - references: [ - { path: "./compiler" }, - { path: "./services" }, - ] - }) - }; - const compilerConfig: File = { - path: `${solutionLocation}/compiler/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - composite: true, - module: "none" - }, - files: ["./types.ts", "./program.ts"] - }) - }; - const typesFile: File = { - path: `${solutionLocation}/compiler/types.ts`, - content: ` + describe("when package is scoped", () => { + verifyMonoRepoLike("@issue/"); + }); + }); + it("when finding local reference doesnt load ancestor/sibling projects", () => { + const solutionLocation = "/user/username/projects/solution"; + const solution: File = { + path: `${solutionLocation}/tsconfig.json`, + content: JSON.stringify({ + files: [], + include: [], + references: [ + { path: "./compiler" }, + { path: "./services" }, + ] + }) + }; + const compilerConfig: File = { + path: `${solutionLocation}/compiler/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + composite: true, + module: "none" + }, + files: ["./types.ts", "./program.ts"] + }) + }; + const typesFile: File = { + path: `${solutionLocation}/compiler/types.ts`, + content: ` namespace ts { export interface Program { getSourceFiles(): string[]; } }` - }; - const programFile: File = { - path: `${solutionLocation}/compiler/program.ts`, - content: ` + }; + const programFile: File = { + path: `${solutionLocation}/compiler/program.ts`, + content: ` namespace ts { export const program: Program = { getSourceFiles: () => [getSourceFile()] }; function getSourceFile() { return "something"; } }` - }; - const servicesConfig: File = { - path: `${solutionLocation}/services/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - composite: true - }, - files: ["./services.ts"], - references: [ - { path: "../compiler" } - ] - }) - }; - const servicesFile: File = { - path: `${solutionLocation}/services/services.ts`, - content: ` + }; + const servicesConfig: File = { + path: `${solutionLocation}/services/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + composite: true + }, + files: ["./services.ts"], + references: [ + { path: "../compiler" } + ] + }) + }; + const servicesFile: File = { + path: `${solutionLocation}/services/services.ts`, + content: ` namespace ts { const result = program.getSourceFiles(); }` - }; - - const files = [libFile, solution, compilerConfig, typesFile, programFile, servicesConfig, servicesFile, libFile]; - const host = createServerHost(files); - const session = createSession(host); - const service = session.getProjectService(); - service.openClientFile(programFile.path); - checkNumberOfProjects(service, { configuredProjects: 2 }); - const compilerProject = service.configuredProjects.get(compilerConfig.path)!; - checkProjectActualFiles(compilerProject, [libFile.path, typesFile.path, programFile.path, compilerConfig.path]); - const solutionProject = service.configuredProjects.get(solution.path)!; - assert.isTrue(solutionProject.isInitialLoadPending()); - - // Find all references for getSourceFile - const response = session.executeCommandSeq({ - command: protocol.CommandTypes.References, - arguments: protocolFileLocationFromSubstring(programFile, "getSourceFile", { index: 1 }) - }).response as protocol.ReferencesResponseBody; - assert.deepEqual(response, { - refs: [ - makeReferenceItem({ - file: programFile, - text: "getSourceFile", - options: { index: 1 }, - isDefinition: false, - lineText: ` getSourceFiles: () => [getSourceFile()]`, - }), - makeReferenceItem({ - file: programFile, - text: "getSourceFile", - options: { index: 2 }, - contextText: `function getSourceFile() { return "something"; }`, - isDefinition: true, - lineText: ` function getSourceFile() { return "something"; }`, - }) - ], - symbolName: "getSourceFile", - symbolStartOffset: protocolLocationFromSubstring(programFile.content, "getSourceFile", { index: 1 }).offset, - symbolDisplayString: "function getSourceFile(): string" - }); - // Shouldnt load more projects - checkNumberOfProjects(service, { configuredProjects: 2 }); - assert.isTrue(solutionProject.isInitialLoadPending()); - - // Find all references for getSourceFiles - const getSourceFilesResponse = session.executeCommandSeq({ - command: protocol.CommandTypes.References, - arguments: protocolFileLocationFromSubstring(programFile, "getSourceFiles") - }).response as protocol.ReferencesResponseBody; - assert.deepEqual(getSourceFilesResponse, { - refs: [ - makeReferenceItem({ - file: typesFile, - text: "getSourceFiles", - contextText: `getSourceFiles(): string[];`, - isDefinition: true, - isWriteAccess: false, - lineText: ` getSourceFiles(): string[];`, - }), - makeReferenceItem({ - file: programFile, - text: "getSourceFiles", - contextText: `getSourceFiles: () => [getSourceFile()]`, - isDefinition: true, - lineText: ` getSourceFiles: () => [getSourceFile()]`, - }), - makeReferenceItem({ - file: servicesFile, - text: "getSourceFiles", - isDefinition: false, - lineText: ` const result = program.getSourceFiles();`, - }) - ], - symbolName: "getSourceFiles", - symbolStartOffset: protocolLocationFromSubstring(typesFile.content, "getSourceFiles").offset, - symbolDisplayString: "(method) ts.Program.getSourceFiles(): string[]" - }); - - // Should load more projects - checkNumberOfProjects(service, { configuredProjects: 3 }); - assert.isFalse(solutionProject.isInitialLoadPending()); - checkProjectActualFiles(solutionProject, [solution.path]); - checkProjectActualFiles(service.configuredProjects.get(servicesConfig.path)!, [servicesFile.path, servicesConfig.path, libFile.path, typesFile.path, programFile.path]); - }); - - it("when disableSolutionSearching is true, solution and siblings are not loaded", () => { - const solutionLocation = "/user/username/projects/solution"; - const solution: File = { - path: `${solutionLocation}/tsconfig.json`, - content: JSON.stringify({ - files: [], - include: [], - references: [ - { path: "./compiler" }, - { path: "./services" }, - ] + }; + const files = [libFile, solution, compilerConfig, typesFile, programFile, servicesConfig, servicesFile, libFile]; + const host = createServerHost(files); + const session = createSession(host); + const service = session.getProjectService(); + service.openClientFile(programFile.path); + checkNumberOfProjects(service, { configuredProjects: 2 }); + const compilerProject = service.configuredProjects.get(compilerConfig.path)!; + checkProjectActualFiles(compilerProject, [libFile.path, typesFile.path, programFile.path, compilerConfig.path]); + const solutionProject = service.configuredProjects.get(solution.path)!; + assert.isTrue(solutionProject.isInitialLoadPending()); + // Find all references for getSourceFile + const response = (session.executeCommandSeq({ + command: protocol.CommandTypes.References, + arguments: protocolFileLocationFromSubstring(programFile, "getSourceFile", { index: 1 }) + }).response as protocol.ReferencesResponseBody); + assert.deepEqual(response, { + refs: [ + makeReferenceItem({ + file: programFile, + text: "getSourceFile", + options: { index: 1 }, + isDefinition: false, + lineText: ` getSourceFiles: () => [getSourceFile()]`, + }), + makeReferenceItem({ + file: programFile, + text: "getSourceFile", + options: { index: 2 }, + contextText: `function getSourceFile() { return "something"; }`, + isDefinition: true, + lineText: ` function getSourceFile() { return "something"; }`, }) - }; - const compilerConfig: File = { - path: `${solutionLocation}/compiler/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - composite: true, - module: "none", - disableSolutionSearching: true - }, - files: ["./types.ts", "./program.ts"] + ], + symbolName: "getSourceFile", + symbolStartOffset: protocolLocationFromSubstring(programFile.content, "getSourceFile", { index: 1 }).offset, + symbolDisplayString: "function getSourceFile(): string" + }); + // Shouldnt load more projects + checkNumberOfProjects(service, { configuredProjects: 2 }); + assert.isTrue(solutionProject.isInitialLoadPending()); + // Find all references for getSourceFiles + const getSourceFilesResponse = (session.executeCommandSeq({ + command: protocol.CommandTypes.References, + arguments: protocolFileLocationFromSubstring(programFile, "getSourceFiles") + }).response as protocol.ReferencesResponseBody); + assert.deepEqual(getSourceFilesResponse, { + refs: [ + makeReferenceItem({ + file: typesFile, + text: "getSourceFiles", + contextText: `getSourceFiles(): string[];`, + isDefinition: true, + isWriteAccess: false, + lineText: ` getSourceFiles(): string[];`, + }), + makeReferenceItem({ + file: programFile, + text: "getSourceFiles", + contextText: `getSourceFiles: () => [getSourceFile()]`, + isDefinition: true, + lineText: ` getSourceFiles: () => [getSourceFile()]`, + }), + makeReferenceItem({ + file: servicesFile, + text: "getSourceFiles", + isDefinition: false, + lineText: ` const result = program.getSourceFiles();`, }) - }; - const typesFile: File = { - path: `${solutionLocation}/compiler/types.ts`, - content: ` + ], + symbolName: "getSourceFiles", + symbolStartOffset: protocolLocationFromSubstring(typesFile.content, "getSourceFiles").offset, + symbolDisplayString: "(method) ts.Program.getSourceFiles(): string[]" + }); + // Should load more projects + checkNumberOfProjects(service, { configuredProjects: 3 }); + assert.isFalse(solutionProject.isInitialLoadPending()); + checkProjectActualFiles(solutionProject, [solution.path]); + checkProjectActualFiles((service.configuredProjects.get(servicesConfig.path)!), [servicesFile.path, servicesConfig.path, libFile.path, typesFile.path, programFile.path]); + }); + it("when disableSolutionSearching is true, solution and siblings are not loaded", () => { + const solutionLocation = "/user/username/projects/solution"; + const solution: File = { + path: `${solutionLocation}/tsconfig.json`, + content: JSON.stringify({ + files: [], + include: [], + references: [ + { path: "./compiler" }, + { path: "./services" }, + ] + }) + }; + const compilerConfig: File = { + path: `${solutionLocation}/compiler/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + composite: true, + module: "none", + disableSolutionSearching: true + }, + files: ["./types.ts", "./program.ts"] + }) + }; + const typesFile: File = { + path: `${solutionLocation}/compiler/types.ts`, + content: ` namespace ts { export interface Program { getSourceFiles(): string[]; } }` - }; - const programFile: File = { - path: `${solutionLocation}/compiler/program.ts`, - content: ` + }; + const programFile: File = { + path: `${solutionLocation}/compiler/program.ts`, + content: ` namespace ts { export const program: Program = { getSourceFiles: () => [getSourceFile()] }; function getSourceFile() { return "something"; } }` - }; - const servicesConfig: File = { - path: `${solutionLocation}/services/tsconfig.json`, - content: JSON.stringify({ - compilerOptions: { - composite: true - }, - files: ["./services.ts"], - references: [ - { path: "../compiler" } - ] - }) - }; - const servicesFile: File = { - path: `${solutionLocation}/services/services.ts`, - content: ` + }; + const servicesConfig: File = { + path: `${solutionLocation}/services/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + composite: true + }, + files: ["./services.ts"], + references: [ + { path: "../compiler" } + ] + }) + }; + const servicesFile: File = { + path: `${solutionLocation}/services/services.ts`, + content: ` namespace ts { const result = program.getSourceFiles(); }` - }; - - const files = [libFile, solution, compilerConfig, typesFile, programFile, servicesConfig, servicesFile, libFile]; - const host = createServerHost(files); - const session = createSession(host); - const service = session.getProjectService(); - service.openClientFile(programFile.path); - checkNumberOfProjects(service, { configuredProjects: 1 }); - const compilerProject = service.configuredProjects.get(compilerConfig.path)!; - checkProjectActualFiles(compilerProject, [libFile.path, typesFile.path, programFile.path, compilerConfig.path]); - - // Find all references - const getSourceFilesResponse = session.executeCommandSeq({ - command: protocol.CommandTypes.References, - arguments: protocolFileLocationFromSubstring(programFile, "getSourceFiles") - }).response as protocol.ReferencesResponseBody; - assert.deepEqual(getSourceFilesResponse, { - refs: [ - makeReferenceItem({ - file: typesFile, - text: "getSourceFiles", - contextText: `getSourceFiles(): string[];`, - isDefinition: true, - isWriteAccess: false, - lineText: ` getSourceFiles(): string[];`, - }), - makeReferenceItem({ - file: programFile, - text: "getSourceFiles", - contextText: `getSourceFiles: () => [getSourceFile()]`, - isDefinition: true, - lineText: ` getSourceFiles: () => [getSourceFile()]`, - }), - ], - symbolName: "getSourceFiles", - symbolStartOffset: protocolLocationFromSubstring(typesFile.content, "getSourceFiles").offset, - symbolDisplayString: "(method) ts.Program.getSourceFiles(): string[]" - }); - - // No new solutions/projects loaded - checkNumberOfProjects(service, { configuredProjects: 1 }); + }; + const files = [libFile, solution, compilerConfig, typesFile, programFile, servicesConfig, servicesFile, libFile]; + const host = createServerHost(files); + const session = createSession(host); + const service = session.getProjectService(); + service.openClientFile(programFile.path); + checkNumberOfProjects(service, { configuredProjects: 1 }); + const compilerProject = service.configuredProjects.get(compilerConfig.path)!; + checkProjectActualFiles(compilerProject, [libFile.path, typesFile.path, programFile.path, compilerConfig.path]); + // Find all references + const getSourceFilesResponse = (session.executeCommandSeq({ + command: protocol.CommandTypes.References, + arguments: protocolFileLocationFromSubstring(programFile, "getSourceFiles") + }).response as protocol.ReferencesResponseBody); + assert.deepEqual(getSourceFilesResponse, { + refs: [ + makeReferenceItem({ + file: typesFile, + text: "getSourceFiles", + contextText: `getSourceFiles(): string[];`, + isDefinition: true, + isWriteAccess: false, + lineText: ` getSourceFiles(): string[];`, + }), + makeReferenceItem({ + file: programFile, + text: "getSourceFiles", + contextText: `getSourceFiles: () => [getSourceFile()]`, + isDefinition: true, + lineText: ` getSourceFiles: () => [getSourceFile()]`, + }), + ], + symbolName: "getSourceFiles", + symbolStartOffset: protocolLocationFromSubstring(typesFile.content, "getSourceFiles").offset, + symbolDisplayString: "(method) ts.Program.getSourceFiles(): string[]" }); + // No new solutions/projects loaded + checkNumberOfProjects(service, { configuredProjects: 1 }); }); -} +}); diff --git a/src/testRunner/unittests/tsserver/projects.ts b/src/testRunner/unittests/tsserver/projects.ts index 80e1efcac22d3..0485342abebf9 100644 --- a/src/testRunner/unittests/tsserver/projects.ts +++ b/src/testRunner/unittests/tsserver/projects.ts @@ -1,1343 +1,1165 @@ -namespace ts.projectSystem { - describe("unittests:: tsserver:: Projects", () => { - it("handles the missing files - that were added to program because they were added with /// { - const file1: File = { - path: "/a/b/commonFile1.ts", - content: `/// +import { File, createServerHost, libFile, createSession, openFilesForSession, checkNumberOfInferredProjects, checkProjectRootFiles, checkProjectActualFiles, makeSessionRequest, verifyDiagnostics, commonFile2, verifyNoDiagnostics, commonFile1, createProjectService, configuredProjectAt, checkNumberOfConfiguredProjects, toExternalFiles, checkNumberOfProjects, customTypesMap, checkWatchedFiles, toExternalFile, createHasErrorMessageLogger, closeFilesForSession, verifyGetErrRequest } from "../../ts.projectSystem"; +import { protocol, CommandNames, ITypingsInstaller, createInstallTypingsRequest, emptyArray, NormalizedPath } from "../../ts.server"; +import { Diagnostics, noop, singleIterator, emptyOptions, returnFalse, notImplemented, removeMinAndVersionNumbers, ScriptKind, projectSystem, combinePaths, getDirectoryPath, createTextSpan, getSnapshotText, ScriptElementKind, ScriptTarget, normalizePath, Debug, Path, mapDefined } from "../../ts"; +import { projectRoot } from "../../ts.tscWatch"; +import * as ts from "../../ts"; +describe("unittests:: tsserver:: Projects", () => { + it("handles the missing files - that were added to program because they were added with /// { + const file1: File = { + path: "/a/b/commonFile1.ts", + content: `/// let x = y` - }; - const host = createServerHost([file1, libFile]); - const session = createSession(host); - openFilesForSession([file1], session); - const projectService = session.getProjectService(); - - checkNumberOfInferredProjects(projectService, 1); - const project = projectService.inferredProjects[0]; - checkProjectRootFiles(project, [file1.path]); - checkProjectActualFiles(project, [file1.path, libFile.path]); - const getErrRequest = makeSessionRequest( - server.CommandNames.SemanticDiagnosticsSync, - { file: file1.path } - ); - - // Two errors: CommonFile2 not found and cannot find name y - let diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; - verifyDiagnostics(diags, [ - { diagnosticMessage: Diagnostics.Cannot_find_name_0, errorTextArguments: ["y"] }, - { diagnosticMessage: Diagnostics.File_0_not_found, errorTextArguments: [commonFile2.path] } - ]); - - host.reloadFS([file1, commonFile2, libFile]); - host.runQueuedTimeoutCallbacks(); - checkNumberOfInferredProjects(projectService, 1); - assert.strictEqual(projectService.inferredProjects[0], project, "Inferred project should be same"); - checkProjectRootFiles(project, [file1.path]); - checkProjectActualFiles(project, [file1.path, libFile.path, commonFile2.path]); - diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; - verifyNoDiagnostics(diags); - }); - - it("should create new inferred projects for files excluded from a configured project", () => { - const configFile: File = { - path: "/a/b/tsconfig.json", - content: `{ + }; + const host = createServerHost([file1, libFile]); + const session = createSession(host); + openFilesForSession([file1], session); + const projectService = session.getProjectService(); + checkNumberOfInferredProjects(projectService, 1); + const project = projectService.inferredProjects[0]; + checkProjectRootFiles(project, [file1.path]); + checkProjectActualFiles(project, [file1.path, libFile.path]); + const getErrRequest = makeSessionRequest(CommandNames.SemanticDiagnosticsSync, { file: file1.path }); + // Two errors: CommonFile2 not found and cannot find name y + let diags = (session.executeCommand(getErrRequest).response as protocol.Diagnostic[]); + verifyDiagnostics(diags, [ + { diagnosticMessage: Diagnostics.Cannot_find_name_0, errorTextArguments: ["y"] }, + { diagnosticMessage: Diagnostics.File_0_not_found, errorTextArguments: [commonFile2.path] } + ]); + host.reloadFS([file1, commonFile2, libFile]); + host.runQueuedTimeoutCallbacks(); + checkNumberOfInferredProjects(projectService, 1); + assert.strictEqual(projectService.inferredProjects[0], project, "Inferred project should be same"); + checkProjectRootFiles(project, [file1.path]); + checkProjectActualFiles(project, [file1.path, libFile.path, commonFile2.path]); + diags = (session.executeCommand(getErrRequest).response as protocol.Diagnostic[]); + verifyNoDiagnostics(diags); + }); + it("should create new inferred projects for files excluded from a configured project", () => { + const configFile: File = { + path: "/a/b/tsconfig.json", + content: `{ "compilerOptions": {}, "files": ["${commonFile1.path}", "${commonFile2.path}"] }` - }; - const files = [commonFile1, commonFile2, configFile]; - const host = createServerHost(files); - const projectService = createProjectService(host); - projectService.openClientFile(commonFile1.path); - - const project = configuredProjectAt(projectService, 0); - checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); - configFile.content = `{ + }; + const files = [commonFile1, commonFile2, configFile]; + const host = createServerHost(files); + const projectService = createProjectService(host); + projectService.openClientFile(commonFile1.path); + const project = configuredProjectAt(projectService, 0); + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + configFile.content = `{ "compilerOptions": {}, "files": ["${commonFile1.path}"] }`; - host.reloadFS(files); - - checkNumberOfConfiguredProjects(projectService, 1); - checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); - host.checkTimeoutQueueLengthAndRun(2); // Update the configured project + refresh inferred projects - checkNumberOfConfiguredProjects(projectService, 1); - checkProjectRootFiles(project, [commonFile1.path]); - - projectService.openClientFile(commonFile2.path); - checkNumberOfInferredProjects(projectService, 1); - }); - - it("should disable features when the files are too large", () => { - const file1 = { - path: "/a/b/f1.js", - content: "let x =1;", - fileSize: 10 * 1024 * 1024 - }; - const file2 = { - path: "/a/b/f2.js", - content: "let y =1;", - fileSize: 6 * 1024 * 1024 - }; - const file3 = { - path: "/a/b/f3.js", - content: "let y =1;", - fileSize: 6 * 1024 * 1024 - }; - - const proj1name = "proj1", proj2name = "proj2", proj3name = "proj3"; - - const host = createServerHost([file1, file2, file3]); - const projectService = createProjectService(host); - - projectService.openExternalProject({ rootFiles: toExternalFiles([file1.path]), options: {}, projectFileName: proj1name }); - const proj1 = projectService.findProject(proj1name)!; - assert.isTrue(proj1.languageServiceEnabled); - - projectService.openExternalProject({ rootFiles: toExternalFiles([file2.path]), options: {}, projectFileName: proj2name }); - const proj2 = projectService.findProject(proj2name)!; - assert.isTrue(proj2.languageServiceEnabled); - - projectService.openExternalProject({ rootFiles: toExternalFiles([file3.path]), options: {}, projectFileName: proj3name }); - const proj3 = projectService.findProject(proj3name)!; - assert.isFalse(proj3.languageServiceEnabled); - }); - - it("should not crash when opening a file in a project with a disabled language service", () => { + host.reloadFS(files); + checkNumberOfConfiguredProjects(projectService, 1); + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + host.checkTimeoutQueueLengthAndRun(2); // Update the configured project + refresh inferred projects + checkNumberOfConfiguredProjects(projectService, 1); + checkProjectRootFiles(project, [commonFile1.path]); + projectService.openClientFile(commonFile2.path); + checkNumberOfInferredProjects(projectService, 1); + }); + it("should disable features when the files are too large", () => { + const file1 = { + path: "/a/b/f1.js", + content: "let x =1;", + fileSize: 10 * 1024 * 1024 + }; + const file2 = { + path: "/a/b/f2.js", + content: "let y =1;", + fileSize: 6 * 1024 * 1024 + }; + const file3 = { + path: "/a/b/f3.js", + content: "let y =1;", + fileSize: 6 * 1024 * 1024 + }; + const proj1name = "proj1", proj2name = "proj2", proj3name = "proj3"; + const host = createServerHost([file1, file2, file3]); + const projectService = createProjectService(host); + projectService.openExternalProject({ rootFiles: toExternalFiles([file1.path]), options: {}, projectFileName: proj1name }); + const proj1 = projectService.findProject(proj1name)!; + assert.isTrue(proj1.languageServiceEnabled); + projectService.openExternalProject({ rootFiles: toExternalFiles([file2.path]), options: {}, projectFileName: proj2name }); + const proj2 = projectService.findProject(proj2name)!; + assert.isTrue(proj2.languageServiceEnabled); + projectService.openExternalProject({ rootFiles: toExternalFiles([file3.path]), options: {}, projectFileName: proj3name }); + const proj3 = projectService.findProject(proj3name)!; + assert.isFalse(proj3.languageServiceEnabled); + }); + it("should not crash when opening a file in a project with a disabled language service", () => { + const file1 = { + path: "/a/b/f1.js", + content: "let x =1;", + fileSize: 50 * 1024 * 1024 + }; + const file2 = { + path: "/a/b/f2.js", + content: "let x =1;", + fileSize: 100 + }; + const projName = "proj1"; + const host = createServerHost([file1, file2]); + const projectService = createProjectService(host, { useSingleInferredProject: true }, { eventHandler: noop }); + projectService.openExternalProject({ rootFiles: toExternalFiles([file1.path, file2.path]), options: {}, projectFileName: projName }); + const proj1 = projectService.findProject(projName)!; + assert.isFalse(proj1.languageServiceEnabled); + assert.doesNotThrow(() => projectService.openClientFile(file2.path)); + }); + describe("ignoreConfigFiles", () => { + it("external project including config file", () => { const file1 = { - path: "/a/b/f1.js", - content: "let x =1;", - fileSize: 50 * 1024 * 1024 + path: "/a/b/f1.ts", + content: "let x =1;" }; - const file2 = { - path: "/a/b/f2.js", - content: "let x =1;", - fileSize: 100 + const config1 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ + compilerOptions: {}, + files: ["f1.ts"] + }) }; - - const projName = "proj1"; - - const host = createServerHost([file1, file2]); - const projectService = createProjectService(host, { useSingleInferredProject: true }, { eventHandler: noop }); - - projectService.openExternalProject({ rootFiles: toExternalFiles([file1.path, file2.path]), options: {}, projectFileName: projName }); - const proj1 = projectService.findProject(projName)!; - assert.isFalse(proj1.languageServiceEnabled); - - assert.doesNotThrow(() => projectService.openClientFile(file2.path)); - }); - - describe("ignoreConfigFiles", () => { - it("external project including config file", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x =1;" - }; - const config1 = { - path: "/a/b/tsconfig.json", - content: JSON.stringify( - { - compilerOptions: {}, - files: ["f1.ts"] - } - ) - }; - - const externalProjectName = "externalproject"; - const host = createServerHost([file1, config1]); - const projectService = createProjectService(host, { useSingleInferredProject: true }, { syntaxOnly: true }); - projectService.openExternalProject({ - rootFiles: toExternalFiles([file1.path, config1.path]), - options: {}, - projectFileName: externalProjectName - }); - - checkNumberOfProjects(projectService, { externalProjects: 1 }); - const proj = projectService.externalProjects[0]; - assert.isDefined(proj); - - assert.isTrue(proj.fileExists(file1.path)); - }); - - it("loose file included in config file (openClientFile)", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x =1;" - }; - const config1 = { - path: "/a/b/tsconfig.json", - content: JSON.stringify( - { - compilerOptions: {}, - files: ["f1.ts"] - } - ) - }; - - const host = createServerHost([file1, config1]); - const projectService = createProjectService(host, { useSingleInferredProject: true }, { syntaxOnly: true }); - projectService.openClientFile(file1.path, file1.content); - - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - const proj = projectService.inferredProjects[0]; - assert.isDefined(proj); - - assert.isTrue(proj.fileExists(file1.path)); - }); - - it("loose file included in config file (applyCodeChanges)", () => { - const file1 = { - path: "/a/b/f1.ts", - content: "let x =1;" - }; - const config1 = { - path: "/a/b/tsconfig.json", - content: JSON.stringify( - { - compilerOptions: {}, - files: ["f1.ts"] - } - ) - }; - - const host = createServerHost([file1, config1]); - const projectService = createProjectService(host, { useSingleInferredProject: true }, { syntaxOnly: true }); - projectService.applyChangesInOpenFiles(singleIterator({ fileName: file1.path, content: file1.content })); - - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - const proj = projectService.inferredProjects[0]; - assert.isDefined(proj); - - assert.isTrue(proj.fileExists(file1.path)); + const externalProjectName = "externalproject"; + const host = createServerHost([file1, config1]); + const projectService = createProjectService(host, { useSingleInferredProject: true }, { syntaxOnly: true }); + projectService.openExternalProject({ + rootFiles: toExternalFiles([file1.path, config1.path]), + options: {}, + projectFileName: externalProjectName }); + checkNumberOfProjects(projectService, { externalProjects: 1 }); + const proj = projectService.externalProjects[0]; + assert.isDefined(proj); + assert.isTrue(proj.fileExists(file1.path)); }); - - it("reload regular file after closing", () => { - const f1 = { - path: "/a/b/app.ts", - content: "x." - }; - const f2 = { - path: "/a/b/lib.ts", - content: "let x: number;" - }; - - const host = createServerHost([f1, f2, libFile]); - const service = createProjectService(host); - service.openExternalProject({ projectFileName: "/a/b/project", rootFiles: toExternalFiles([f1.path, f2.path]), options: {} }); - - service.openClientFile(f1.path); - service.openClientFile(f2.path, "let x: string"); - - service.checkNumberOfProjects({ externalProjects: 1 }); - checkProjectActualFiles(service.externalProjects[0], [f1.path, f2.path, libFile.path]); - - const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, emptyOptions)!; - // should contain completions for string - assert.isTrue(completions1.entries.some(e => e.name === "charAt"), "should contain 'charAt'"); - assert.isFalse(completions1.entries.some(e => e.name === "toExponential"), "should not contain 'toExponential'"); - - service.closeClientFile(f2.path); - const completions2 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, emptyOptions)!; - // should contain completions for string - assert.isFalse(completions2.entries.some(e => e.name === "charAt"), "should not contain 'charAt'"); - assert.isTrue(completions2.entries.some(e => e.name === "toExponential"), "should contain 'toExponential'"); - }); - - it("clear mixed content file after closing", () => { - const f1 = { - path: "/a/b/app.ts", - content: " " - }; - const f2 = { - path: "/a/b/lib.html", - content: "" - }; - - const host = createServerHost([f1, f2, libFile]); - const service = createProjectService(host); - service.openExternalProject({ projectFileName: "/a/b/project", rootFiles: [{ fileName: f1.path }, { fileName: f2.path, hasMixedContent: true }], options: {} }); - - service.openClientFile(f1.path); - service.openClientFile(f2.path, "let somelongname: string"); - - service.checkNumberOfProjects({ externalProjects: 1 }); - checkProjectActualFiles(service.externalProjects[0], [f1.path, f2.path, libFile.path]); - - const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, emptyOptions)!; - assert.isTrue(completions1.entries.some(e => e.name === "somelongname"), "should contain 'somelongname'"); - - service.closeClientFile(f2.path); - const completions2 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, emptyOptions)!; - assert.isFalse(completions2.entries.some(e => e.name === "somelongname"), "should not contain 'somelongname'"); - const sf2 = service.externalProjects[0].getLanguageService().getProgram()!.getSourceFile(f2.path)!; - assert.equal(sf2.text, ""); - }); - - it("changes in closed files are reflected in project structure", () => { + it("loose file included in config file (openClientFile)", () => { const file1 = { path: "/a/b/f1.ts", - content: `export * from "./f2"` - }; - const file2 = { - path: "/a/b/f2.ts", - content: `export let x = 1` + content: "let x =1;" }; - const file3 = { - path: "/a/c/f3.ts", - content: `export let y = 1;` + const config1 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ + compilerOptions: {}, + files: ["f1.ts"] + }) }; - const host = createServerHost([file1, file2, file3]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); + const host = createServerHost([file1, config1]); + const projectService = createProjectService(host, { useSingleInferredProject: true }, { syntaxOnly: true }); + projectService.openClientFile(file1.path, file1.content); checkNumberOfProjects(projectService, { inferredProjects: 1 }); - const inferredProject0 = projectService.inferredProjects[0]; - checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path]); - - projectService.openClientFile(file3.path); - checkNumberOfProjects(projectService, { inferredProjects: 2 }); - assert.strictEqual(projectService.inferredProjects[0], inferredProject0); - checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path]); - const inferredProject1 = projectService.inferredProjects[1]; - checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); - - const modifiedFile2 = { - path: file2.path, - content: `export * from "../c/f3"` // now inferred project should inclule file3 - }; - - host.reloadFS([file1, modifiedFile2, file3]); - host.checkTimeoutQueueLengthAndRun(2); - checkNumberOfProjects(projectService, { inferredProjects: 2 }); - assert.strictEqual(projectService.inferredProjects[0], inferredProject0); - checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, modifiedFile2.path, file3.path]); - assert.strictEqual(projectService.inferredProjects[1], inferredProject1); - assert.isTrue(inferredProject1.isOrphan()); + const proj = projectService.inferredProjects[0]; + assert.isDefined(proj); + assert.isTrue(proj.fileExists(file1.path)); }); - - it("deleted files affect project structure", () => { + it("loose file included in config file (applyCodeChanges)", () => { const file1 = { path: "/a/b/f1.ts", - content: `export * from "./f2"` + content: "let x =1;" }; - const file2 = { - path: "/a/b/f2.ts", - content: `export * from "../c/f3"` - }; - const file3 = { - path: "/a/c/f3.ts", - content: `export let y = 1;` + const config1 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ + compilerOptions: {}, + files: ["f1.ts"] + }) }; - const host = createServerHost([file1, file2, file3]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - - checkNumberOfProjects(projectService, { inferredProjects: 1 }); - - checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path]); - - projectService.openClientFile(file3.path); + const host = createServerHost([file1, config1]); + const projectService = createProjectService(host, { useSingleInferredProject: true }, { syntaxOnly: true }); + projectService.applyChangesInOpenFiles(singleIterator({ fileName: file1.path, content: file1.content })); checkNumberOfProjects(projectService, { inferredProjects: 1 }); - - host.reloadFS([file1, file3]); - host.checkTimeoutQueueLengthAndRun(2); - - checkNumberOfProjects(projectService, { inferredProjects: 2 }); - - checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); - checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); + const proj = projectService.inferredProjects[0]; + assert.isDefined(proj); + assert.isTrue(proj.fileExists(file1.path)); }); - - it("ignores files excluded by a custom safe type list", () => { - const file1 = { - path: "/a/b/f1.js", - content: "export let x = 5" - }; - const office = { - path: "/lib/duckquack-3.min.js", - content: "whoa do @@ not parse me ok thanks!!!" - }; - const host = createServerHost([file1, office, customTypesMap]); - const projectService = createProjectService(host); - try { - projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, office.path]) }); - const proj = projectService.externalProjects[0]; - assert.deepEqual(proj.getFileNames(/*excludeFilesFromExternalLibraries*/ true), [file1.path]); - assert.deepEqual(proj.getTypeAcquisition().include, ["duck-types"]); + }); + it("reload regular file after closing", () => { + const f1 = { + path: "/a/b/app.ts", + content: "x." + }; + const f2 = { + path: "/a/b/lib.ts", + content: "let x: number;" + }; + const host = createServerHost([f1, f2, libFile]); + const service = createProjectService(host); + service.openExternalProject({ projectFileName: "/a/b/project", rootFiles: toExternalFiles([f1.path, f2.path]), options: {} }); + service.openClientFile(f1.path); + service.openClientFile(f2.path, "let x: string"); + service.checkNumberOfProjects({ externalProjects: 1 }); + checkProjectActualFiles(service.externalProjects[0], [f1.path, f2.path, libFile.path]); + const completions1 = (service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, emptyOptions)!); + // should contain completions for string + assert.isTrue(completions1.entries.some(e => e.name === "charAt"), "should contain 'charAt'"); + assert.isFalse(completions1.entries.some(e => e.name === "toExponential"), "should not contain 'toExponential'"); + service.closeClientFile(f2.path); + const completions2 = (service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, emptyOptions)!); + // should contain completions for string + assert.isFalse(completions2.entries.some(e => e.name === "charAt"), "should not contain 'charAt'"); + assert.isTrue(completions2.entries.some(e => e.name === "toExponential"), "should contain 'toExponential'"); + }); + it("clear mixed content file after closing", () => { + const f1 = { + path: "/a/b/app.ts", + content: " " + }; + const f2 = { + path: "/a/b/lib.html", + content: "" + }; + const host = createServerHost([f1, f2, libFile]); + const service = createProjectService(host); + service.openExternalProject({ projectFileName: "/a/b/project", rootFiles: [{ fileName: f1.path }, { fileName: f2.path, hasMixedContent: true }], options: {} }); + service.openClientFile(f1.path); + service.openClientFile(f2.path, "let somelongname: string"); + service.checkNumberOfProjects({ externalProjects: 1 }); + checkProjectActualFiles(service.externalProjects[0], [f1.path, f2.path, libFile.path]); + const completions1 = (service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, emptyOptions)!); + assert.isTrue(completions1.entries.some(e => e.name === "somelongname"), "should contain 'somelongname'"); + service.closeClientFile(f2.path); + const completions2 = (service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, emptyOptions)!); + assert.isFalse(completions2.entries.some(e => e.name === "somelongname"), "should not contain 'somelongname'"); + const sf2 = service.externalProjects[0].getLanguageService().getProgram()!.getSourceFile(f2.path)!; + assert.equal(sf2.text, ""); + }); + it("changes in closed files are reflected in project structure", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `export * from "./f2"` + }; + const file2 = { + path: "/a/b/f2.ts", + content: `export let x = 1` + }; + const file3 = { + path: "/a/c/f3.ts", + content: `export let y = 1;` + }; + const host = createServerHost([file1, file2, file3]); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const inferredProject0 = projectService.inferredProjects[0]; + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path]); + projectService.openClientFile(file3.path); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + assert.strictEqual(projectService.inferredProjects[0], inferredProject0); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path]); + const inferredProject1 = projectService.inferredProjects[1]; + checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); + const modifiedFile2 = { + path: file2.path, + content: `export * from "../c/f3"` // now inferred project should inclule file3 + }; + host.reloadFS([file1, modifiedFile2, file3]); + host.checkTimeoutQueueLengthAndRun(2); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + assert.strictEqual(projectService.inferredProjects[0], inferredProject0); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, modifiedFile2.path, file3.path]); + assert.strictEqual(projectService.inferredProjects[1], inferredProject1); + assert.isTrue(inferredProject1.isOrphan()); + }); + it("deleted files affect project structure", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `export * from "./f2"` + }; + const file2 = { + path: "/a/b/f2.ts", + content: `export * from "../c/f3"` + }; + const file3 = { + path: "/a/c/f3.ts", + content: `export let y = 1;` + }; + const host = createServerHost([file1, file2, file3]); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path]); + projectService.openClientFile(file3.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + host.reloadFS([file1, file3]); + host.checkTimeoutQueueLengthAndRun(2); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); + }); + it("ignores files excluded by a custom safe type list", () => { + const file1 = { + path: "/a/b/f1.js", + content: "export let x = 5" + }; + const office = { + path: "/lib/duckquack-3.min.js", + content: "whoa do @@ not parse me ok thanks!!!" + }; + const host = createServerHost([file1, office, customTypesMap]); + const projectService = createProjectService(host); + try { + projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, office.path]) }); + const proj = projectService.externalProjects[0]; + assert.deepEqual(proj.getFileNames(/*excludeFilesFromExternalLibraries*/ true), [file1.path]); + assert.deepEqual(proj.getTypeAcquisition().include, ["duck-types"]); + } + finally { + projectService.resetSafeList(); + } + }); + it("file with name constructor.js doesnt cause issue with typeAcquisition when safe type list", () => { + const file1 = { + path: "/a/b/f1.js", + content: `export let x = 5; import { s } from "s"` + }; + const constructorFile = { + path: "/a/b/constructor.js", + content: "const x = 10;" + }; + const bliss = { + path: "/a/b/bliss.js", + content: "export function is() { return true; }" + }; + const host = createServerHost([file1, libFile, constructorFile, bliss, customTypesMap]); + let request: string | undefined; + const cachePath = "/a/data"; + const typingsInstaller: ITypingsInstaller = { + isKnownTypesPackageName: returnFalse, + installPackage: notImplemented, + enqueueInstallTypingsRequest: (proj, typeAcquisition, unresolvedImports) => { + assert.isUndefined(request); + request = JSON.stringify(createInstallTypingsRequest(proj, typeAcquisition, unresolvedImports || emptyArray, cachePath)); + }, + attach: noop, + onProjectClosed: noop, + globalTypingsCacheLocation: cachePath + }; + const projectName = "project"; + const projectService = createProjectService(host, { typingsInstaller }); + projectService.openExternalProject({ projectFileName: projectName, options: {}, rootFiles: toExternalFiles([file1.path, constructorFile.path, bliss.path]) }); + assert.equal(request, JSON.stringify({ + projectName, + fileNames: [libFile.path, file1.path, constructorFile.path, bliss.path], + compilerOptions: { allowNonTsExtensions: true, noEmitForJsFiles: true }, + typeAcquisition: { include: ["blissfuljs"], exclude: [], enable: true }, + unresolvedImports: ["s"], + projectRootPath: "/", + cachePath, + kind: "discover" + })); + const response = JSON.parse(request!); + request = undefined; + projectService.updateTypingsForProject({ + kind: "action::set", + projectName: response.projectName, + typeAcquisition: response.typeAcquisition, + compilerOptions: response.compilerOptions, + typings: ts.emptyArray, + unresolvedImports: response.unresolvedImports, + }); + host.checkTimeoutQueueLengthAndRun(2); + assert.isUndefined(request); + }); + it("ignores files excluded by the default type list", () => { + const file1 = { + path: "/a/b/f1.js", + content: "export let x = 5" + }; + const minFile = { + path: "/c/moment.min.js", + content: "unspecified" + }; + const kendoFile1 = { + path: "/q/lib/kendo/kendo.all.min.js", + content: "unspecified" + }; + const kendoFile2 = { + path: "/q/lib/kendo/kendo.ui.min.js", + content: "unspecified" + }; + const kendoFile3 = { + path: "/q/lib/kendo-ui/kendo.all.js", + content: "unspecified" + }; + const officeFile1 = { + path: "/scripts/Office/1/excel-15.debug.js", + content: "unspecified" + }; + const officeFile2 = { + path: "/scripts/Office/1/powerpoint.js", + content: "unspecified" + }; + const files = [file1, minFile, kendoFile1, kendoFile2, kendoFile3, officeFile1, officeFile2]; + const host = createServerHost(files); + const projectService = createProjectService(host); + try { + projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles(files.map(f => f.path)) }); + const proj = projectService.externalProjects[0]; + assert.deepEqual(proj.getFileNames(/*excludeFilesFromExternalLibraries*/ true), [file1.path]); + assert.deepEqual(proj.getTypeAcquisition().include, ["kendo-ui", "office"]); + } + finally { + projectService.resetSafeList(); + } + }); + it("removes version numbers correctly", () => { + const testData: [string, string][] = [ + ["jquery-max", "jquery-max"], + ["jquery.min", "jquery"], + ["jquery-min.4.2.3", "jquery"], + ["jquery.min.4.2.1", "jquery"], + ["minimum", "minimum"], + ["min", "min"], + ["min.3.2", "min"], + ["jquery", "jquery"] + ]; + for (const t of testData) { + assert.equal(removeMinAndVersionNumbers(t[0]), t[1], t[0]); + } + }); + it("ignores files excluded by a legacy safe type list", () => { + const file1 = { + path: "/a/b/bliss.js", + content: "let x = 5" + }; + const file2 = { + path: "/a/b/foo.js", + content: "" + }; + const file3 = { + path: "/a/b/Bacon.js", + content: "let y = 5" + }; + const host = createServerHost([file1, file2, file3, customTypesMap]); + const projectService = createProjectService(host); + try { + projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, file2.path]), typeAcquisition: { enable: true } }); + const proj = projectService.externalProjects[0]; + assert.deepEqual(proj.getFileNames(), [file2.path]); + } + finally { + projectService.resetSafeList(); + } + }); + it("correctly migrate files between projects", () => { + const file1 = { + path: "/a/b/f1.ts", + content: ` + export * from "../c/f2"; + export * from "../d/f3";` + }; + const file2 = { + path: "/a/c/f2.ts", + content: "export let x = 1;" + }; + const file3 = { + path: "/a/d/f3.ts", + content: "export let y = 1;" + }; + const host = createServerHost([file1, file2, file3]); + const projectService = createProjectService(host); + projectService.openClientFile(file2.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file2.path]); + let inferredProjects = projectService.inferredProjects.slice(); + projectService.openClientFile(file3.path); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]); + checkProjectActualFiles(projectService.inferredProjects[0], [file2.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); + inferredProjects = projectService.inferredProjects.slice(); + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + assert.notStrictEqual(projectService.inferredProjects[0], inferredProjects[0]); + assert.notStrictEqual(projectService.inferredProjects[0], inferredProjects[1]); + checkProjectRootFiles(projectService.inferredProjects[0], [file1.path]); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, file2.path, file3.path]); + inferredProjects = projectService.inferredProjects.slice(); + projectService.closeClientFile(file1.path); + checkNumberOfProjects(projectService, { inferredProjects: 3 }); + assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]); + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); + checkProjectActualFiles(projectService.inferredProjects[2], [file3.path]); + inferredProjects = projectService.inferredProjects.slice(); + projectService.closeClientFile(file3.path); + checkNumberOfProjects(projectService, { inferredProjects: 3 }); + assert.strictEqual(projectService.inferredProjects[0], inferredProjects[0]); + assert.strictEqual(projectService.inferredProjects[1], inferredProjects[1]); + assert.strictEqual(projectService.inferredProjects[2], inferredProjects[2]); + assert.isTrue(projectService.inferredProjects[0].isOrphan()); + checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); + assert.isTrue(projectService.inferredProjects[2].isOrphan()); + projectService.openClientFile(file3.path); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + assert.strictEqual(projectService.inferredProjects[0], inferredProjects[2]); + assert.strictEqual(projectService.inferredProjects[1], inferredProjects[1]); + checkProjectActualFiles(projectService.inferredProjects[0], [file3.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); + }); + it("regression test for crash in acquireOrUpdateDocument", () => { + const tsFile = { + fileName: "/a/b/file1.ts", + path: "/a/b/file1.ts", + content: "" + }; + const jsFile = { + path: "/a/b/file1.js", + content: "var x = 10;", + fileName: "/a/b/file1.js", + scriptKind: "JS" as "JS" + }; + const host = createServerHost([]); + const projectService = createProjectService(host); + projectService.applyChangesInOpenFiles(singleIterator(tsFile)); + const projs = projectService.synchronizeProjectList([]); + projectService.findProject(projs[0].info!.projectName)!.getLanguageService().getNavigationBarItems(tsFile.fileName); + projectService.synchronizeProjectList([projs[0].info!]); + projectService.applyChangesInOpenFiles(singleIterator(jsFile)); + }); + it("config file is deleted", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1;" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 2;" + }; + const config = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {} }) + }; + const host = createServerHost([file1, file2, config]); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); + projectService.openClientFile(file2.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); + host.reloadFS([file1, file2]); + host.checkTimeoutQueueLengthAndRun(1); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); + }); + it("loading files with correct priority", () => { + const f1 = { + path: "/a/main.ts", + content: "let x = 1" + }; + const f2 = { + path: "/a/main.js", + content: "var y = 1" + }; + const f3 = { + path: "/main.js", + content: "var y = 1" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { allowJs: true } + }) + }; + const host = createServerHost([f1, f2, f3, config]); + const projectService = createProjectService(host); + projectService.setHostConfiguration({ + extraFileExtensions: [ + { extension: ".js", isMixedContent: false }, + { extension: ".html", isMixedContent: true } + ] + }); + projectService.openClientFile(f1.path); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, config.path]); + // Since f2 refers to config file as the default project, it needs to be kept alive + projectService.closeClientFile(f1.path); + projectService.openClientFile(f2.path); + projectService.checkNumberOfProjects({ inferredProjects: 1, configuredProjects: 1 }); + assert.isDefined(projectService.configuredProjects.get(config.path)); + checkProjectActualFiles(projectService.inferredProjects[0], [f2.path]); + // Should close configured project with next file open + projectService.closeClientFile(f2.path); + projectService.openClientFile(f3.path); + projectService.checkNumberOfProjects({ inferredProjects: 1 }); + assert.isUndefined(projectService.configuredProjects.get(config.path)); + checkProjectActualFiles(projectService.inferredProjects[0], [f3.path]); + }); + it("tsconfig script block support", () => { + const file1 = { + path: "/a/b/f1.ts", + content: ` ` + }; + const file2 = { + path: "/a/b/f2.html", + content: `var hello = "hello";` + }; + const config = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: { allowJs: true } }) + }; + const host = createServerHost([file1, file2, config]); + const session = createSession(host); + openFilesForSession([file1], session); + const projectService = session.getProjectService(); + // HTML file will not be included in any projects yet + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const configuredProj = configuredProjectAt(projectService, 0); + checkProjectActualFiles(configuredProj, [file1.path, config.path]); + // Specify .html extension as mixed content + const extraFileExtensions = [{ extension: ".html", scriptKind: ScriptKind.JS, isMixedContent: true }]; + const configureHostRequest = makeSessionRequest(projectSystem.CommandNames.Configure, { extraFileExtensions }); + session.executeCommand(configureHostRequest); + // The configured project should now be updated to include html file + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(configuredProjectAt(projectService, 0), configuredProj, "Same configured project should be updated"); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); + // Open HTML file + projectService.applyChangesInOpenFiles(singleIterator({ + fileName: file2.path, + hasMixedContent: true, + scriptKind: ScriptKind.JS, + content: `var hello = "hello";` + })); + // Now HTML file is included in the project + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); + // Check identifiers defined in HTML content are available in .ts file + const project = configuredProjectAt(projectService, 0); + let completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 1, emptyOptions); + assert(completions && completions.entries[1].name === "hello", `expected entry hello to be in completion list`); + assert(completions && completions.entries[0].name === "globalThis", `first entry should be globalThis (not strictly relevant for this test).`); + // Close HTML file + projectService.applyChangesInOpenFiles( + /*openFiles*/ undefined, + /*changedFiles*/ undefined, + /*closedFiles*/ [file2.path]); + // HTML file is still included in project + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); + // Check identifiers defined in HTML content are not available in .ts file + completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 5, emptyOptions); + assert(completions && completions.entries[0].name !== "hello", `unexpected hello entry in completion list`); + }); + it("no tsconfig script block diagnostic errors", () => { + // #1. Ensure no diagnostic errors when allowJs is true + const file1 = { + path: "/a/b/f1.ts", + content: ` ` + }; + const file2 = { + path: "/a/b/f2.html", + content: `var hello = "hello";` + }; + const config1 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: { allowJs: true } }) + }; + let host = createServerHost([file1, file2, config1, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") }); + let session = createSession(host); + // Specify .html extension as mixed content in a configure host request + const extraFileExtensions = [{ extension: ".html", scriptKind: ScriptKind.JS, isMixedContent: true }]; + const configureHostRequest = makeSessionRequest(projectSystem.CommandNames.Configure, { extraFileExtensions }); + session.executeCommand(configureHostRequest); + openFilesForSession([file1], session); + let projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + let diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); + assert.deepEqual(diagnostics, []); + // #2. Ensure no errors when allowJs is false + const config2 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: { allowJs: false } }) + }; + host = createServerHost([file1, file2, config2, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") }); + session = createSession(host); + session.executeCommand(configureHostRequest); + openFilesForSession([file1], session); + projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); + assert.deepEqual(diagnostics, []); + // #3. Ensure no errors when compiler options aren't specified + const config3 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({}) + }; + host = createServerHost([file1, file2, config3, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") }); + session = createSession(host); + session.executeCommand(configureHostRequest); + openFilesForSession([file1], session); + projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); + assert.deepEqual(diagnostics, []); + // #4. Ensure no errors when files are explicitly specified in tsconfig + const config4 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: { allowJs: true }, files: [file1.path, file2.path] }) + }; + host = createServerHost([file1, file2, config4, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") }); + session = createSession(host); + session.executeCommand(configureHostRequest); + openFilesForSession([file1], session); + projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); + assert.deepEqual(diagnostics, []); + // #4. Ensure no errors when files are explicitly excluded in tsconfig + const config5 = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: { allowJs: true }, exclude: [file2.path] }) + }; + host = createServerHost([file1, file2, config5, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") }); + session = createSession(host); + session.executeCommand(configureHostRequest); + openFilesForSession([file1], session); + projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); + assert.deepEqual(diagnostics, []); + }); + it("project structure update is deferred if files are not added\removed", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `import {x} from "./f2"` + }; + const file2 = { + path: "/a/b/f2.ts", + content: "export let x = 1" + }; + const host = createServerHost([file1, file2]); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + projectService.openClientFile(file2.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + projectService.applyChangesInOpenFiles( + /*openFiles*/ undefined, + /*changedFiles*/ singleIterator({ fileName: file1.path, changes: singleIterator({ span: createTextSpan(0, file1.path.length), newText: "let y = 1" }) }), + /*closedFiles*/ undefined); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + projectService.ensureInferredProjectsUpToDate_TestOnly(); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + }); + it("files with mixed content are handled correctly", () => { + const file1 = { + path: "/a/b/f1.html", + content: `